diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 09ab5698fd..175d975d86 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -6,7 +6,7 @@ open_collective: # Replace with a single Open Collective username ko_fi: # Replace with a single Ko-fi username tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry -liberapay: # Replace with a single Liberapay username +liberapay: FreeCAD issuehunt: # Replace with a single IssueHunt username otechie: # Replace with a single Otechie username custom: ['https://www.patreon.com/yorikvanhavre', 'https://www.patreon.com/kkremitzki', 'https://www.patreon.com/thundereal'] diff --git a/package/fedora/freecad.spec b/package/fedora/freecad.spec index 837a82381e..7d7702518c 100644 --- a/package/fedora/freecad.spec +++ b/package/fedora/freecad.spec @@ -10,8 +10,6 @@ %global plugins Drawing Fem FreeCAD Image Import Inspection Mesh MeshPart Part Points QtUnit Raytracing ReverseEngineering Robot Sketcher Start Web PartDesignGui _PartDesign Path PathGui Spreadsheet SpreadsheetGui area DraftUtils DraftUtils libDriver libDriverDAT libDriverSTL libDriverUNV libMEFISTO2 libSMDS libSMESH libSMESHDS libStdMeshers Measure TechDraw TechDrawGui libarea-native Surface SurfaceGui PathSimulator # Some configuration options for other environments -# rpmbuild --with=occ: Compile using OpenCASCADE instead of OCE -%global occ %{?_with_occ: 1} %{?!_with_occ: 0} # rpmbuild --with=bundled_zipios: use bundled version of zipios++ %global bundled_zipios %{?_with_bundled_zipios: 1} %{?!_with_bundled_zipios: 0} # rpmbuild --without=bundled_pycxx: don't use bundled version of pycxx @@ -50,13 +48,7 @@ BuildRequires: git BuildRequires: Coin3-devel BuildRequires: Inventor-devel -%if %{occ} -BuildRequires: OpenCASCADE-devel -%else -BuildRequires: OCE-devel -BuildRequires: OCE-draw -%endif - +BuildRequires: opencascade-devel BuildRequires: boost-devel BuildRequires: boost-python3-devel BuildRequires: eigen3-devel @@ -79,19 +71,15 @@ BuildRequires: netgen-mesher-devel-private BuildRequires: python3-pivy BuildRequires: mesa-libEGL-devel BuildRequires: pcl-devel -%if 0%{?fedora} > 29 BuildRequires: pyside2-tools -%endif BuildRequires: python3 BuildRequires: python3-devel BuildRequires: python3-matplotlib %if ! %{bundled_pycxx} BuildRequires: python3-pycxx-devel %endif -%if 0%{?fedora} > 29 BuildRequires: python3-pyside2-devel BuildRequires: python3-shiboken2-devel -%endif BuildRequires: qt5-devel BuildRequires: qt5-qtwebkit-devel %if ! %{bundled_smesh} @@ -210,9 +198,7 @@ LDFLAGS='-Wl,--as-needed -Wl,--no-undefined'; export LDFLAGS -DCOIN3D_INCLUDE_DIR=%{_includedir}/Coin3 \ -DCOIN3D_DOC_PATH=%{_datadir}/Coin3/Coin \ -DFREECAD_USE_EXTERNAL_PIVY=TRUE \ -%if %{occ} -DUSE_OCC=TRUE \ -%endif %if ! %{bundled_smesh} -DFREECAD_USE_EXTERNAL_SMESH=TRUE \ -DSMESH_FOUND=TRUE \ diff --git a/src/3rdParty/CMakeLists.txt b/src/3rdParty/CMakeLists.txt index b509ba7814..7bde0b1ac7 100644 --- a/src/3rdParty/CMakeLists.txt +++ b/src/3rdParty/CMakeLists.txt @@ -2,3 +2,5 @@ if (BUILD_SMESH AND NOT FREECAD_USE_EXTERNAL_SMESH) add_subdirectory(salomesmesh) endif() + +add_subdirectory(lazy_loader) \ No newline at end of file diff --git a/src/3rdParty/lazy_loader/CMakeLists.txt b/src/3rdParty/lazy_loader/CMakeLists.txt new file mode 100644 index 0000000000..7ed973983c --- /dev/null +++ b/src/3rdParty/lazy_loader/CMakeLists.txt @@ -0,0 +1,12 @@ +SET(lazy_loader + lazy_loader.py + __init__.py +) +add_custom_target(lazy_loader ALL SOURCES + ${lazy_loader} +) +SET_PYTHON_PREFIX_SUFFIX(lazy_loader) + +fc_copy_sources(lazy_loader "${CMAKE_BINARY_DIR}/Ext/lazy_loader" ${lazy_loader}) +install (FILES ${lazy_loader} DESTINATION "${CMAKE_INSTALL_PREFIX}/Ext/lazy_loader") + diff --git a/src/3rdParty/lazy_loader/__init__.py b/src/3rdParty/lazy_loader/__init__.py new file mode 100644 index 0000000000..db8200b5bc --- /dev/null +++ b/src/3rdParty/lazy_loader/__init__.py @@ -0,0 +1,13 @@ +""" +LazyLoader will defer import of a module until first usage. Usage: +from lazy_loader.lazy_loader import LazyLoader +numpy = LazyLoader("numpy", globals(), "numpy") + +or + +whatever = LazyLoader("module", globals(), "module.whatever") + +or to replicate import module as something + +something = LazyLoader("module", globals(), "module") +""" \ No newline at end of file diff --git a/src/3rdParty/lazy_loader/lazy_loader.py b/src/3rdParty/lazy_loader/lazy_loader.py new file mode 100644 index 0000000000..2a443fc30c --- /dev/null +++ b/src/3rdParty/lazy_loader/lazy_loader.py @@ -0,0 +1,60 @@ +# Copyright 2015 The TensorFlow Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== + +"""A LazyLoader class.""" + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import importlib +import types + + +class LazyLoader(types.ModuleType): + """Lazily import a module, mainly to avoid pulling in large dependencies. + + `contrib`, and `ffmpeg` are examples of modules that are large and not always + needed, and this allows them to only be loaded when they are used. + """ + + # The lint error here is incorrect. + def __init__(self, local_name, parent_module_globals, name, warning=None): # pylint: disable=super-on-old-class + self._local_name = local_name + self._parent_module_globals = parent_module_globals + self._warning = warning + + super(LazyLoader, self).__init__(name) + + def _load(self): + """Load the module and insert it into the parent's globals.""" + # Import the target module and insert it into the parent's namespace + module = importlib.import_module(self.__name__) + self._parent_module_globals[self._local_name] = module + + # Update this object's dict so that if someone keeps a reference to the + # LazyLoader, lookups are efficient (__getattr__ is only called on lookups + # that fail). + self.__dict__.update(module.__dict__) + + return module + + def __getattr__(self, item): + module = self._load() + return getattr(module, item) + + def __dir__(self): + module = self._load() + return dir(module) \ No newline at end of file diff --git a/src/App/PropertyLinks.cpp b/src/App/PropertyLinks.cpp index 390a773a79..b936c9665e 100644 --- a/src/App/PropertyLinks.cpp +++ b/src/App/PropertyLinks.cpp @@ -4337,6 +4337,14 @@ void PropertyXLinkList::setPyObject(PyObject *value) PropertyXLinkSubList::setPyObject(value); } +//for consistency with PropertyLinkList +const std::vector PropertyXLinkList::getValues(void) const +{ + std::vector xLinks; + getLinks(xLinks); + return(xLinks); +} + //************************************************************************** // PropertyXLinkContainer //++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ diff --git a/src/App/PropertyLinks.h b/src/App/PropertyLinks.h index 9dfa74e8ec..183cfcbfba 100644 --- a/src/App/PropertyLinks.h +++ b/src/App/PropertyLinks.h @@ -1309,6 +1309,9 @@ public: virtual PyObject *getPyObject(void) override; virtual void setPyObject(PyObject *) override; + + //for consistency with PropertyLinkList + const std::vector getValues(void) const; }; diff --git a/src/Mod/Arch/ArchWindow.py b/src/Mod/Arch/ArchWindow.py index 9fe8773919..8c2029a505 100644 --- a/src/Mod/Arch/ArchWindow.py +++ b/src/Mod/Arch/ArchWindow.py @@ -1226,7 +1226,7 @@ class _Window(ArchComponent.Component): bb.enlarge(10) step = obj.LouvreWidth.Value+obj.LouvreSpacing.Value if step < bb.ZLength: - box = Part.makeBox(bb.XLength,bb.YLength,obj.LouvreWidth.Value) + box = Part.makeBox(bb.XLength,bb.YLength,obj.LouvreSpacing.Value) boxes = [] for i in range(int(bb.ZLength/step)+1): b = box.copy() diff --git a/src/Mod/Arch/InitGui.py b/src/Mod/Arch/InitGui.py index 53d154c1b9..3791fba309 100644 --- a/src/Mod/Arch/InitGui.py +++ b/src/Mod/Arch/InitGui.py @@ -180,6 +180,7 @@ FreeCADGui.addWorkbench(ArchWorkbench) import Arch_rc from PySide.QtCore import QT_TRANSLATE_NOOP FreeCADGui.addPreferencePage(":/ui/preferences-ifc.ui", QT_TRANSLATE_NOOP("Draft", "Import-Export")) +FreeCADGui.addPreferencePage(":/ui/preferences-ifc-export.ui", QT_TRANSLATE_NOOP("Draft", "Import-Export")) FreeCADGui.addPreferencePage(":/ui/preferences-dae.ui", QT_TRANSLATE_NOOP("Draft", "Import-Export")) FreeCAD.__unit_test__ += ["TestArch"] diff --git a/src/Mod/Arch/Resources/Arch.qrc b/src/Mod/Arch/Resources/Arch.qrc index ce9f1a9864..fa7a65c2a0 100644 --- a/src/Mod/Arch/Resources/Arch.qrc +++ b/src/Mod/Arch/Resources/Arch.qrc @@ -112,6 +112,7 @@ ui/preferences-archdefaults.ui ui/preferences-dae.ui ui/preferences-ifc.ui + ui/preferences-ifc-export.ui translations/Arch_af.qm translations/Arch_ar.qm translations/Arch_ca.qm diff --git a/src/Mod/Arch/Resources/ui/preferences-ifc-export.ui b/src/Mod/Arch/Resources/ui/preferences-ifc-export.ui new file mode 100644 index 0000000000..3d1df06a58 --- /dev/null +++ b/src/Mod/Arch/Resources/ui/preferences-ifc-export.ui @@ -0,0 +1,359 @@ + + + Gui::Dialog::DlgSettingsArch + + + + 0 + 0 + 463 + 421 + + + + IFC-Export + + + + 6 + + + 9 + + + 9 + + + 9 + + + 9 + + + + + Show this dialog when exporting + + + ifcShowDialog + + + Mod/Arch + + + + + + + Export options + + + + + + Some IFC viewers don't like objects exported as extrusions. +Use this to force all objects to be exported as BREP geometry. + + + Force export as Brep + + + ifcExportAsBrep + + + Mod/Arch + + + + + + + Use triangulation options set in the DAE options page + + + Use DAE triangulation options + + + ifcUseDaeOptions + + + Mod/Arch + + + + + + + Curved shapes that cannot be represented as curves in IFC +are decomposed into flat facets. +If this is checked, additional calculation is done to join coplanar facets. + + + Join coplanar facets when triangulating + + + ifcJoinCoplanarFacets + + + Mod/Arch + + + + + + + When exporting objects without unique ID (UID), the generated UID +will be stored inside the FreeCAD object for reuse next time that object +is exported. This leads to smaller differences between file versions. + + + Store IFC unique ID in FreeCAD objects + + + true + + + ifcStoreUid + + + Mod/Arch + + + + + + + IFCOpenShell is a library that allows to import IFC files. +Its serializer functionality allows to give it an OCC shape and it will +produce adequate IFC geometry: NURBS, faceted, or anything else. +Note: The serializer is still an experimental feature! + + + Use IfcOpenShell serializer if available + + + ifcSerialize + + + Mod/Arch + + + + + + + 2D objects will be exported as IfcAnnotation + + + Export 2D objects as IfcAnnotations + + + true + + + ifcExport2D + + + Mod/Arch + + + + + + + All FreeCAD object properties will be stored inside the exported objects, +allowing to recreate a full parametric model on reimport. + + + Export full FreeCAD parametric model + + + IfcExportFreeCADProperties + + + Mod/Arch + + + + + + + When possible, similar entities will be used only once in the file if possible. +This can reduce the file size a lot, but will make it less easily readable. + + + Reuse similar entities + + + true + + + ifcCompress + + + Mod/Arch + + + + + + + When possible, IFC objects that are extruded rectangles will be +exported as IfcRectangleProfileDef. +However, some other applications might have problems importing that entity. +If this is your case, you can disable this and then all profiles will be exported as IfcArbitraryClosedProfileDef. + + + Disable IfcRectangleProfileDef + + + DisableIfcRectangleProfileDef + + + Mod/Arch + + + + + + + Some IFC types such as IfcWall or IfcBeam have special standard versions +like IfcWallStandardCase or IfcBeamStandardCase. +If this option is turned on, FreeCAD will automatically export such objects +as standard cases when the necessary conditions are met. + + + Auto-detect and export as standard cases when applicable + + + getStandardCase + + + Mod/Arch + + + + + + + If no site is found in the FreeCAD document, a default one will be added. +A site is not mandatory but a common practice is to have at least one in the file. + + + Add default site if one is not found in the document + + + IfcAddDefaultSite + + + Mod/Arch + + + + + + + If no building is found in the FreeCAD document, a default one will be added. +Warning: The IFC standard asks for at least one building in each file. By turning this option off, you will produce a non-standard IFC file. +However, at FreeCAD, we believe having a building should not be mandatory, and this option is there to have a chance to demonstrate our point of view. + + + Add default building if one is not found in the document (no standard) + + + true + + + IfcAddDefaultBuilding + + + Mod/Arch + + + + + + + If no building storey is found in the FreeCAD document, a default one will be added. +A building storey is not mandatory but a common practice to have at least one in the file. + + + Add default building storey if one is not found in the document + + + IfcAddDefaultStorey + + + Mod/Arch + + + + + + + + + IFC file units + + + + + + + The units you want your IFC file to be exported to. Note that IFC file are ALWAYS written in metric units. Imperial units are only a conversion applied on top of it. But some BIM applications will use this to choose which unit to work with when opening the file. + + + ifcUnit + + + Mod/Arch + + + + Metric + + + + + Imperial + + + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + qPixmapFromMimeSource + + + Gui::PrefCheckBox + QCheckBox +
Gui/PrefWidgets.h
+
+ + Gui::PrefComboBox + QComboBox +
Gui/PrefWidgets.h
+
+
+ + +
diff --git a/src/Mod/Arch/Resources/ui/preferences-ifc.ui b/src/Mod/Arch/Resources/ui/preferences-ifc.ui index 28835df005..089594c571 100644 --- a/src/Mod/Arch/Resources/ui/preferences-ifc.ui +++ b/src/Mod/Arch/Resources/ui/preferences-ifc.ui @@ -7,7 +7,7 @@ 0 0 463 - 937 + 495 @@ -17,13 +17,22 @@ 6 - + + 9 + + + 9 + + + 9 + + 9 - Show this dialog when importing and exporting + Show this dialog when importing ifcShowDialog @@ -388,289 +397,6 @@ FreeCAD object properties - - - - Export options - - - - - - Some IFC viewers don't like objects exported as extrusions. -Use this to force all objects to be exported as BREP geometry. - - - Force export as Brep - - - ifcExportAsBrep - - - Mod/Arch - - - - - - - Use triangulation options set in the DAE options page - - - Use DAE triangulation options - - - ifcUseDaeOptions - - - Mod/Arch - - - - - - - Curved shapes that cannot be represented as curves in IFC -are decomposed into flat facets. -If this is checked, additional calculation is done to join coplanar facets. - - - Join coplanar facets when triangulating - - - ifcJoinCoplanarFacets - - - Mod/Arch - - - - - - - When exporting objects without unique ID (UID), the generated UID -will be stored inside the FreeCAD object for reuse next time that object -is exported. This leads to smaller differences between file versions. - - - Store IFC unique ID in FreeCAD objects - - - true - - - ifcStoreUid - - - Mod/Arch - - - - - - - IFCOpenShell is a library that allows to import IFC files. -Its serializer functionality allows to give it an OCC shape and it will -produce adequate IFC geometry: NURBS, faceted, or anything else. -Note: The serializer is still an experimental feature! - - - Use IfcOpenShell serializer if available - - - ifcSerialize - - - Mod/Arch - - - - - - - 2D objects will be exported as IfcAnnotation - - - Export 2D objects as IfcAnnotations - - - true - - - ifcExport2D - - - Mod/Arch - - - - - - - All FreeCAD object properties will be stored inside the exported objects, -allowing to recreate a full parametric model on reimport. - - - Export full FreeCAD parametric model - - - IfcExportFreeCADProperties - - - Mod/Arch - - - - - - - When possible, similar entities will be used only once in the file if possible. -This can reduce the file size a lot, but will make it less easily readable. - - - Reuse similar entities - - - true - - - ifcCompress - - - Mod/Arch - - - - - - - When possible, IFC objects that are extruded rectangles will be -exported as IfcRectangleProfileDef. -However, some other applications might have problems importing that entity. -If this is your case, you can disable this and then all profiles will be exported as IfcArbitraryClosedProfileDef. - - - Disable IfcRectangleProfileDef - - - DisableIfcRectangleProfileDef - - - Mod/Arch - - - - - - - Some IFC types such as IfcWall or IfcBeam have special standard versions -like IfcWallStandardCase or IfcBeamStandardCase. -If this option is turned on, FreeCAD will automatically export such objects -as standard cases when the necessary conditions are met. - - - Auto-detect and export as standard cases when applicable - - - getStandardCase - - - Mod/Arch - - - - - - - If no site is found in the FreeCAD document, a default one will be added. -A site is not mandatory but a common practice is to have at least one in the file. - - - Add default site if one is not found in the document - - - IfcAddDefaultSite - - - Mod/Arch - - - - - - - If no building is found in the FreeCAD document, a default one will be added. -Warning: The IFC standard asks for at least one building in each file. By turning this option off, you will produce a non-standard IFC file. -However, at FreeCAD, we believe having a building should not be mandatory, and this option is there to have a chance to demonstrate our point of view. - - - Add default building if one is not found in the document (no standard) - - - true - - - IfcAddDefaultBuilding - - - Mod/Arch - - - - - - - If no building storey is found in the FreeCAD document, a default one will be added. -A building storey is not mandatory but a common practice to have at least one in the file. - - - Add default building storey if one is not found in the document - - - IfcAddDefaultStorey - - - Mod/Arch - - - - - - - - - IFC file units - - - - - - - The units you want your IFC file to be exported to. Note that IFC file are ALWAYS written in metric units. Imperial units are only a conversion applied on top of it. But some BIM applications will use this to choose which unit to work with when opening the file. - - - ifcUnit - - - Mod/Arch - - - - Metric - - - - - Imperial - - - - - - - - - diff --git a/src/Mod/Arch/exportIFC.py b/src/Mod/Arch/exportIFC.py index f0b3c71cbe..2aea1be3b6 100644 --- a/src/Mod/Arch/exportIFC.py +++ b/src/Mod/Arch/exportIFC.py @@ -69,8 +69,10 @@ translationtable = { "Stair Flight":"StairFlight", "Curtain Wall":"CurtainWall", "Pipe Segment":"PipeSegment", - "Pipe Fitting":"PipeFitting" -} + "Pipe Fitting":"PipeFitting", + "VisGroup":"Group", + "Undefined":"BuildingElementProxy", + } # the base IFC template for export @@ -113,7 +115,7 @@ def getPreferences(): if FreeCAD.GuiUp and p.GetBool("ifcShowDialog",False): import FreeCADGui - FreeCADGui.showPreferences("Import-Export",0) + FreeCADGui.showPreferences("Import-Export",1) ifcunit = p.GetInt("ifcUnit",0) f = 0.001 u = "metre" @@ -137,8 +139,17 @@ def getPreferences(): 'ADD_DEFAULT_STOREY': p.GetBool("IfcAddDefaultStorey",False), 'ADD_DEFAULT_BUILDING': p.GetBool("IfcAddDefaultBuilding",True), 'IFC_UNIT': u, - 'SCALE_FACTOR': f + 'SCALE_FACTOR': f, + 'GET_STANDARD': p.GetBool("getStandardType",False) } + if hasattr(ifcopenshell,"schema_identifier"): + schema = ifcopenshell.schema_identifier + elif hasattr(ifcopenshell,"version") and (float(ifcopenshell.version[:3]) >= 0.6): + # v0.6 onwards allows to set our own schema + schema = ["IFC4", "IFC2X3"][FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Arch").GetInt("IfcVersion",0)] + else: + schema = "IFC2X3" + preferences["SCHEMA"] = schema return preferences @@ -151,9 +162,6 @@ def export(exportList,filename,colors=None,preferences=None): colors is an optional dictionary of objName:shapeColorTuple or objName:diffuseColorList elements to be used in non-GUI mode if you want to be able to export colors.""" - if preferences is None: - preferences = getPreferences() - try: global ifcopenshell import ifcopenshell @@ -162,6 +170,11 @@ def export(exportList,filename,colors=None,preferences=None): FreeCAD.Console.PrintMessage("Visit https://www.freecadweb.org/wiki/Arch_IFC to learn how to install it\n") return + if preferences is None: + preferences = getPreferences() + + # process template + version = FreeCAD.Version() owner = FreeCAD.ActiveDocument.CreatedBy email = '' @@ -171,16 +184,8 @@ def export(exportList,filename,colors=None,preferences=None): email = s[1].strip(">") global template template = ifctemplate.replace("$version",version[0]+"."+version[1]+" build "+version[2]) - getstd = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Arch").GetBool("getStandardType",False) - if hasattr(ifcopenshell,"schema_identifier"): - schema = ifcopenshell.schema_identifier - elif hasattr(ifcopenshell,"version") and (float(ifcopenshell.version[:3]) >= 0.6): - # v0.6 allows to set our own schema - schema = ["IFC4", "IFC2X3"][FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Arch").GetInt("IfcVersion",0)] - else: - schema = "IFC2X3" - if preferences['DEBUG']: print("Exporting an",schema,"file...") - template = template.replace("$ifcschema",schema) + if preferences['DEBUG']: print("Exporting an",preferences['SCHEMA'],"file...") + template = template.replace("$ifcschema",preferences['SCHEMA']) template = template.replace("$owner",owner) template = template.replace("$company",FreeCAD.ActiveDocument.Company) template = template.replace("$email",email) @@ -196,6 +201,9 @@ def export(exportList,filename,colors=None,preferences=None): of.write(template) of.close() os.close(templatefilehandle) + + # create IFC file + global ifcfile, surfstyles, clones, sharedobjects, profiledefs, shapedefs ifcfile = ifcopenshell.open(templatefile) ifcfile = exportIFCHelper.writeUnits(ifcfile,preferences["IFC_UNIT"]) @@ -211,13 +219,17 @@ def export(exportList,filename,colors=None,preferences=None): if obj.Shape: if obj.Shape.Edges and (not obj.Shape.Faces): annotations.append(obj) + # clean objects list of unwanted types + objectslist = [obj for obj in objectslist if obj not in annotations] objectslist = Arch.pruneIncluded(objectslist,strict=True) objectslist = [obj for obj in objectslist if Draft.getType(obj) not in ["Dimension","Material","MaterialContainer","WorkingPlaneProxy"]] if preferences['FULL_PARAMETRIC']: objectslist = Arch.getAllChildren(objectslist) + # create project and context + contextCreator = exportIFCHelper.ContextCreator(ifcfile, objectslist) context = contextCreator.model_view_subcontext project = contextCreator.project @@ -227,6 +239,8 @@ def export(exportList,filename,colors=None,preferences=None): decl = Draft.getObjectsOfType(objectslist, "Site")[0].Declination.getValueAs(FreeCAD.Units.Radian) contextCreator.model_context.TrueNorth.DirectionRatios = (math.cos(decl+math.pi/2), math.sin(decl+math.pi/2)) + # define holders for the different types we create + products = {} # { Name: IfcEntity, ... } subproducts = {} # { Name: IfcEntity, ... } for storing additions/subtractions and other types of subcomponents of a product surfstyles = {} # { (r,g,b): IfcEntity, ... } @@ -267,33 +281,81 @@ def export(exportList,filename,colors=None,preferences=None): # getting generic data - name = obj.Label - if six.PY2: - name = name.encode("utf8") - description = obj.Description if hasattr(obj,"Description") else "" - if six.PY2: - description = description.encode("utf8") - - # getting uid - - uid = None - if hasattr(obj,"IfcData"): - if "IfcUID" in obj.IfcData.keys(): - uid = str(obj.IfcData["IfcUID"]) - if not uid: - uid = ifcopenshell.guid.new() - # storing the uid for further use - if preferences['STORE_UID'] and hasattr(obj,"IfcData"): - d = obj.IfcData - d["IfcUID"] = uid - obj.IfcData = d - + name = getText("Name",obj) + description = getText("Description",obj) + uid = getUID(obj,preferences) ifctype = getIfcTypeFromObj(obj) if ifctype == "IfcGroup": groups[obj.Name] = [o.Name for o in obj.Group] continue + # handle assemblies (arrays, app::parts, references, etc...) + + assemblyElements = [] + + if ifctype == "IfcArray": + if obj.ArrayType == "ortho": + clonedeltas = [] + for i in range(obj.NumberX): + clonedeltas.append(obj.Placement.Base+(i*obj.IntervalX)) + for j in range(obj.NumberY): + if j > 0: + clonedeltas.append(obj.Placement.Base+(i*obj.IntervalX)+(j*obj.IntervalY)) + for k in range(obj.NumberZ): + if k > 0: + clonedeltas.append(obj.Placement.Base+(i*obj.IntervalX)+(j*obj.IntervalY)+(k*obj.IntervalZ)) + #print("clonedeltas:",clonedeltas) + for delta in clonedeltas: + representation,placement,shapetype = getRepresentation( + ifcfile, + context, + obj.Base, + forcebrep=(getBrepFlag(obj.Base,preferences)), + colors=colors, + preferences=preferences, + forceclone=delta + ) + subproduct = createProduct( + ifcfile, + obj.Base, + getIfcTypeFromObj(obj.Base), + getUID(obj.Base,preferences), + history, + getText("Name",obj.Base), + getText("Description",obj.Base), + placement, + representation, + preferences) + + assemblyElements.append(subproduct) + ifctype = "IfcElementAssembly" + + elif ifctype == "IfcApp::Part": + for subobj in [FreeCAD.ActiveDocument.getObject(n[:-1]) for n in obj.getSubObjects()]: + representation,placement,shapetype = getRepresentation( + ifcfile, + context, + subobj, + forcebrep=(getBrepFlag(subobj,preferences)), + colors=colors, + preferences=preferences + ) + subproduct = createProduct( + ifcfile, + subobj, + getIfcTypeFromObj(subobj), + getUID(subobj,preferences), + history, + getText("Name",subobj), + getText("Description",subobj), + placement, + representation, + preferences) + + assemblyElements.append(subproduct) + ifctype = "IfcElementAssembly" + # export grids if ifctype in ["IfcAxis","IfcAxisSystem","IfcGrid"]: @@ -356,61 +418,54 @@ def export(exportList,filename,colors=None,preferences=None): if ifctype not in ArchIFCSchema.IfcProducts.keys(): ifctype = "IfcBuildingElementProxy" - # getting the "Force BREP" flag - - brepflag = False - if hasattr(obj,"IfcData"): - if "FlagForceBrep" in obj.IfcData.keys(): - if obj.IfcData["FlagForceBrep"] == "True": - brepflag = True - # getting the representation representation,placement,shapetype = getRepresentation( ifcfile, context, obj, - forcebrep=(brepflag or preferences['FORCE_BREP']), + forcebrep=(getBrepFlag(obj,preferences)), colors=colors, preferences=preferences ) - if getstd: + if preferences['GET_STANDARD']: if isStandardCase(obj,ifctype): ifctype += "StandardCase" - if preferences['DEBUG']: print(str(count).ljust(3)," : ", ifctype, " (",shapetype,") : ",name) - - # setting the arguments - - kwargs = { - "GlobalId": uid, - "OwnerHistory": history, - "Name": name, - "Description": description, - "ObjectPlacement": placement, - "Representation": representation - } - if ifctype == "IfcSite": - kwargs.update({ - "RefLatitude":dd2dms(obj.Latitude), - "RefLongitude":dd2dms(obj.Longitude), - "RefElevation":obj.Elevation.Value*preferences['SCALE_FACTOR'], - "SiteAddress":buildAddress(obj,ifcfile), - "CompositionType": "ELEMENT" - }) - if schema == "IFC2X3": - kwargs = exportIFC2X3Attributes(obj, kwargs, preferences['SCALE_FACTOR']) - else: - kwargs = exportIfcAttributes(obj, kwargs, preferences['SCALE_FACTOR']) + if preferences['DEBUG']: + print(str(count).ljust(3)," : ", ifctype, " (",shapetype,") : ",name) # creating the product - #print(obj.Label," : ",ifctype," : ",kwargs) - product = getattr(ifcfile,"create"+ifctype)(**kwargs) + product = createProduct( + ifcfile, + obj, + ifctype, + uid, + history, + name, + description, + placement, + representation, + preferences) + products[obj.Name] = product if ifctype in ["IfcBuilding","IfcBuildingStorey","IfcSite","IfcSpace"]: spatialelements[obj.Name] = product + # gather assembly subelements + + if assemblyElements: + ifcfile.createIfcRelAggregates( + ifcopenshell.guid.new(), + history, + 'Assembly', + '', + products[obj.Name], + assemblyElements + ) + if preferences['DEBUG']: print(" aggregating",len(assemblyElements),"object(s)") + # additions if hasattr(obj,"Additions") and (shapetype in ["extrusion","no shape"]): @@ -1115,7 +1170,7 @@ def export(exportList,filename,colors=None,preferences=None): rgb = tuple([float(f) for f in m.Material[colorslot].strip("()").split(",")]) break if rgb: - psa = ifcbin.createIfcPresentationStyleAssignment(l,rgb[0],rgb[1],rgb[2]) + psa = ifcbin.createIfcPresentationStyleAssignment(l,rgb[0],rgb[1],rgb[2],ifc4=(preferences["SCHEMA"]=="IFC4")) isi = ifcfile.createIfcStyledItem(None,[psa],None) isr = ifcfile.createIfcStyledRepresentation(context,"Style","Material",[isi]) imd = ifcfile.createIfcMaterialDefinitionRepresentation(None,None,[isr],mat) @@ -1497,12 +1552,6 @@ def getIfcTypeFromObj(obj): if ifctype in translationtable.keys(): ifctype = translationtable[ifctype] - if ifctype == "VisGroup": - ifctype = "Group" - if ifctype == "Undefined": - ifctype = "BuildingElementProxy" - if ifctype == "Furniture": - ifctype = "FurnishingElement" return "Ifc" + ifctype @@ -1542,6 +1591,7 @@ def exportIFC2X3Attributes(obj, kwargs, scale=0.001): def exportIfcAttributes(obj, kwargs, scale=0.001): + ifctype = getIfcTypeFromObj(obj) for property in obj.PropertiesList: if obj.getGroupOfProperty(property) == "IFC Attributes" and obj.getPropertyByName(property): value = obj.getPropertyByName(property) @@ -1549,7 +1599,10 @@ def exportIfcAttributes(obj, kwargs, scale=0.001): value = float(value) if property in ["ElevationWithFlooring","Elevation"]: value = value*scale # some properties must be changed to meters - kwargs.update({property: value}) + if (ifctype == "IfcFurnishingElement") and (property == "PredefinedType"): + pass # IFC2x3 Furniture objects get converted to IfcFurnishingElement and have no PredefinedType anymore + else: + kwargs.update({property: value}) return kwargs @@ -1743,9 +1796,10 @@ def getProfile(ifcfile,p): return profile -def getRepresentation(ifcfile,context,obj,forcebrep=False,subtraction=False,tessellation=1,colors=None,preferences=None): +def getRepresentation(ifcfile,context,obj,forcebrep=False,subtraction=False,tessellation=1,colors=None,preferences=None,forceclone=False): - """returns an IfcShapeRepresentation object or None""" + """returns an IfcShapeRepresentation object or None. forceclone can be False (does nothing), + "store" or True (stores the object as clone base) or a Vector (creates a clone)""" import Part import DraftGeomUtils @@ -1756,20 +1810,27 @@ def getRepresentation(ifcfile,context,obj,forcebrep=False,subtraction=False,tess shapetype = "no shape" tostore = False subplacement = None + skipshape = False # check for clones - if (not subtraction) and (not forcebrep): + if ((not subtraction) and (not forcebrep)) or forceclone: + if forceclone: + if not obj.Name in clones: + clones[obj.Name] = [] for k,v in clones.items(): if (obj.Name == k) or (obj.Name in v): if k in sharedobjects: # base shape already exists repmap = sharedobjects[k] pla = obj.getGlobalPlacement() + pos = FreeCAD.Vector(pla.Base) + if isinstance(forceclone,FreeCAD.Vector): + pos += forceclone axis1 = ifcbin.createIfcDirection(tuple(pla.Rotation.multVec(FreeCAD.Vector(1,0,0)))) axis2 = ifcbin.createIfcDirection(tuple(pla.Rotation.multVec(FreeCAD.Vector(0,1,0)))) axis3 = ifcbin.createIfcDirection(tuple(pla.Rotation.multVec(FreeCAD.Vector(0,0,1)))) - origin = ifcbin.createIfcCartesianPoint(tuple(FreeCAD.Vector(pla.Base).multiply(preferences['SCALE_FACTOR']))) + origin = ifcbin.createIfcCartesianPoint(tuple(pos.multiply(preferences['SCALE_FACTOR']))) transf = ifcbin.createIfcCartesianTransformationOperator3D(axis1,axis2,origin,1.0,axis3) mapitem = ifcfile.createIfcMappedItem(repmap,transf) shapes = [mapitem] @@ -1783,7 +1844,11 @@ def getRepresentation(ifcfile,context,obj,forcebrep=False,subtraction=False,tess if obj.isDerivedFrom("Part::Feature") and (len(obj.Shape.Solids) > 1) and hasattr(obj,"Axis") and obj.Axis: forcebrep = True - if (not shapes) and (not forcebrep): + # specific cases that must ignore their own shape + if Draft.getType(obj) in ["Array"]: + skipshape = True + + if (not shapes) and (not forcebrep) and (not skipshape): profile = None ev = FreeCAD.Vector() if hasattr(obj,"Proxy"): @@ -1858,7 +1923,7 @@ def getRepresentation(ifcfile,context,obj,forcebrep=False,subtraction=False,tess solidType = "SweptSolid" shapetype = "extrusion" - if not shapes: + if (not shapes) and (not skipshape): # check if we keep a null shape (additions-only object) @@ -2106,3 +2171,72 @@ def getRepresentation(ifcfile,context,obj,forcebrep=False,subtraction=False,tess productdef = ifcfile.createIfcProductDefinitionShape(None,None,[representation]) return productdef,placement,shapetype + + +def getBrepFlag(obj,preferences): + """returns True if the object must be exported as BREP""" + brepflag = False + if preferences['FORCE_BREP']: + return True + if hasattr(obj,"IfcData"): + if "FlagForceBrep" in obj.IfcData.keys(): + if obj.IfcData["FlagForceBrep"] == "True": + brepflag = True + return brepflag + + +def createProduct(ifcfile,obj,ifctype,uid,history,name,description,placement,representation,preferences): + """creates a product in the given IFC file""" + + kwargs = { + "GlobalId": uid, + "OwnerHistory": history, + "Name": name, + "Description": description, + "ObjectPlacement": placement, + "Representation": representation + } + if ifctype == "IfcSite": + kwargs.update({ + "RefLatitude":dd2dms(obj.Latitude), + "RefLongitude":dd2dms(obj.Longitude), + "RefElevation":obj.Elevation.Value*preferences['SCALE_FACTOR'], + "SiteAddress":buildAddress(obj,ifcfile), + "CompositionType": "ELEMENT" + }) + if preferences['SCHEMA'] == "IFC2X3": + kwargs = exportIFC2X3Attributes(obj, kwargs, preferences['SCALE_FACTOR']) + else: + kwargs = exportIfcAttributes(obj, kwargs, preferences['SCALE_FACTOR']) + product = getattr(ifcfile,"create"+ifctype)(**kwargs) + return product + + +def getUID(obj,preferences): + """gets or creates an UUID for an object""" + + uid = None + if hasattr(obj,"IfcData"): + if "IfcUID" in obj.IfcData.keys(): + uid = str(obj.IfcData["IfcUID"]) + if not uid: + uid = ifcopenshell.guid.new() + # storing the uid for further use + if preferences['STORE_UID'] and hasattr(obj,"IfcData"): + d = obj.IfcData + d["IfcUID"] = uid + obj.IfcData = d + return uid + + +def getText(field,obj): + """Returns the value of a text property of an object""" + + result = "" + if field == "Name": + field = "Label" + if hasattr(obj,field): + result = getattr(obj,field) + if six.PY2: + result = result.encode("utf8") + return result diff --git a/src/Mod/Arch/exportIFCHelper.py b/src/Mod/Arch/exportIFCHelper.py index c82c75778c..11ff3b313f 100644 --- a/src/Mod/Arch/exportIFCHelper.py +++ b/src/Mod/Arch/exportIFCHelper.py @@ -370,7 +370,7 @@ class recycler: self.sstyles[key] = c return c - def createIfcPresentationStyleAssignment(self,name,r,g,b,t=0): + def createIfcPresentationStyleAssignment(self,name,r,g,b,t=0,ifc4=False): if name: key = name+str((r,g,b,t)) else: @@ -380,7 +380,10 @@ class recycler: return self.psas[key] else: iss = self.createIfcSurfaceStyle(name,r,g,b,t) - c = self.ifcfile.createIfcPresentationStyleAssignment([iss]) + if ifc4: + c = iss + else: + c = self.ifcfile.createIfcPresentationStyleAssignment([iss]) if self.compress: self.psas[key] = c return c diff --git a/src/Mod/Arch/importOBJ.py b/src/Mod/Arch/importOBJ.py index b449846717..12919344ce 100644 --- a/src/Mod/Arch/importOBJ.py +++ b/src/Mod/Arch/importOBJ.py @@ -72,14 +72,23 @@ def getIndices(obj,shape,offsetv,offsetvn): try: if not isinstance(e.Curve,Part.LineSegment): if not curves: - myshape = obj.Shape.copy(False) - myshape.Placement=obj.getGlobalPlacement() + if obj.isDerivedFrom("App::Link"): + myshape = obj.LinkedObject.Shape.copy(False) + myshape.Placement=obj.LinkPlacement + else: + myshape = obj.Shape.copy(False) + myshape.Placement=obj.getGlobalPlacement() mesh=MeshPart.meshFromShape(Shape=myshape, LinearDeflection=0.1, AngularDeflection=0.7, Relative=True) FreeCAD.Console.PrintWarning(translate("Arch","Found a shape containing curves, triangulating")+"\n") break except: # unimplemented curve type - myshape = obj.Shape.copy(False) - myshape.Placement=obj.getGlobalPlacement() + if obj.isDerivedFrom("App::Link"): + if obj.Shape: + myshape = obj.Shape.copy(False) + myshape.Placement=obj.LinkPlacement + else: + myshape = obj.Shape.copy(False) + myshape.Placement=obj.getGlobalPlacement() mesh=MeshPart.meshFromShape(Shape=myshape, LinearDeflection=0.1, AngularDeflection=0.7, Relative=True) FreeCAD.Console.PrintWarning(translate("Arch","Found a shape containing curves, triangulating")+"\n") break @@ -157,7 +166,7 @@ def export(exportList,filename,colors=None): materials = [] outfile.write("mtllib " + os.path.basename(filenamemtl) + "\n") for obj in objectslist: - if obj.isDerivedFrom("Part::Feature") or obj.isDerivedFrom("Mesh::Feature"): + if obj.isDerivedFrom("Part::Feature") or obj.isDerivedFrom("Mesh::Feature") or obj.isDerivedFrom("App::Link"): hires = None if FreeCAD.GuiUp: visible = obj.ViewObject.isVisible() diff --git a/src/Mod/Cloud/App/AppCloud.cpp b/src/Mod/Cloud/App/AppCloud.cpp index e669c67cf9..ef53719043 100644 --- a/src/Mod/Cloud/App/AppCloud.cpp +++ b/src/Mod/Cloud/App/AppCloud.cpp @@ -62,46 +62,46 @@ PyMOD_INIT_FUNC(Cloud) PyMOD_Return(mod); } -Py::Object Cloud::Module::sCloudUrl(const Py::Tuple& args) +Py::Object Cloud::Module::sCloudURL(const Py::Tuple& args) { - char *Url; - if (!PyArg_ParseTuple(args.ptr(), "et","utf-8",&Url)) // convert args: Python->C + char *URL; + if (!PyArg_ParseTuple(args.ptr(), "et","utf-8",&URL)) // convert args: Python->C return Py::None(); - if (this->Url.getStrValue() != Url) { - this->Url.setValue(Url); + if (this->URL.getStrValue() != URL) { + this->URL.setValue(URL); } return Py::None(); } -Py::Object Cloud::Module::sCloudAccessKey(const Py::Tuple& args) +Py::Object Cloud::Module::sCloudTokenAuth(const Py::Tuple& args) { - char *AccessKey; - if (!PyArg_ParseTuple(args.ptr(), "et","utf-8", &AccessKey)) // convert args: Python->C + char *TokenAuth; + if (!PyArg_ParseTuple(args.ptr(), "et","utf-8", &TokenAuth)) // convert args: Python->C return Py::None(); - if (this->AccessKey.getStrValue() != AccessKey) { - this->AccessKey.setValue(AccessKey); + if (this->TokenAuth.getStrValue() != TokenAuth) { + this->TokenAuth.setValue(TokenAuth); } return Py::None(); } -Py::Object Cloud::Module::sCloudSecretKey(const Py::Tuple& args) +Py::Object Cloud::Module::sCloudTokenSecret(const Py::Tuple& args) { - char *SecretKey; - if (!PyArg_ParseTuple(args.ptr(), "et","utf-8", &SecretKey)) // convert args: Python->C + char *TokenSecret; + if (!PyArg_ParseTuple(args.ptr(), "et","utf-8", &TokenSecret)) // convert args: Python->C return Py::None(); - if (this->SecretKey.getStrValue() != SecretKey) { - this->SecretKey.setValue(SecretKey); + if (this->TokenSecret.getStrValue() != TokenSecret) { + this->TokenSecret.setValue(TokenSecret); } return Py::None(); } -Py::Object Cloud::Module::sCloudTcpPort(const Py::Tuple& args) +Py::Object Cloud::Module::sCloudTCPPort(const Py::Tuple& args) { - char *TcpPort; - if (!PyArg_ParseTuple(args.ptr(), "et","utf-8", &TcpPort)) // convert args: Python->C + char *TCPPort; + if (!PyArg_ParseTuple(args.ptr(), "et","utf-8", &TCPPort)) // convert args: Python->C return Py::None(); - if (this->TcpPort.getStrValue() != TcpPort) { - this->TcpPort.setValue(TcpPort); + if (this->TCPPort.getStrValue() != TCPPort) { + this->TCPPort.setValue(TCPPort); } return Py::None(); } @@ -207,7 +207,7 @@ void Cloud::CloudWriter::createBucket() char path[1024]; sprintf(path, "/%s/", this->Bucket); - RequestData = Cloud::ComputeDigestAmzS3v2("PUT", "application/xml", path, this->SecretKey, NULL, 0); + RequestData = Cloud::ComputeDigestAmzS3v2("PUT", "application/xml", path, this->TokenSecret, NULL, 0); // Let's build the Header and call to curl curl_global_init(CURL_GLOBAL_ALL); @@ -220,22 +220,22 @@ void Cloud::CloudWriter::createBucket() if ( curl ) { struct curl_slist *chunk = NULL; - char Url[256]; + char URL[256]; // Let's build our own header - std::string strUrl(this->Url); - eraseSubStr(strUrl,"http://"); - eraseSubStr(strUrl,"https://"); + std::string strURL(this->URL); + eraseSubStr(strURL,"http://"); + eraseSubStr(strURL,"https://"); - chunk = Cloud::BuildHeaderAmzS3v2( strUrl.c_str(), this->TcpPort, this->AccessKey, RequestData); + chunk = Cloud::BuildHeaderAmzS3v2( strURL.c_str(), this->TCPPort, this->TokenAuth, RequestData); delete RequestData; curl_easy_setopt(curl, CURLOPT_HTTPHEADER, chunk); - // Lets build the Url for our Curl call + // Lets build the URL for our Curl call - sprintf(Url,"%s:%s/%s/", this->Url,this->TcpPort, + sprintf(URL,"%s:%s/%s/", this->URL,this->TCPPort, this->Bucket); - curl_easy_setopt(curl, CURLOPT_URL, Url); + curl_easy_setopt(curl, CURLOPT_URL, URL); curl_easy_setopt(curl, CURLOPT_UPLOAD, 1L); curl_easy_setopt(curl, CURLOPT_PUT, 1L); @@ -336,14 +336,14 @@ char *Cloud::MD5Sum(const char *ptr, long size) return(output); } -struct curl_slist *Cloud::BuildHeaderAmzS3v2(const char *Url, const char *TcpPort, const char *PublicKey, struct Cloud::AmzData *Data) +struct curl_slist *Cloud::BuildHeaderAmzS3v2(const char *URL, const char *TCPPort, const char *PublicKey, struct Cloud::AmzData *Data) { char header_data[1024]; struct curl_slist *chunk = NULL; // Build the Host: entry - sprintf(header_data,"Host: %s:%s", Url, TcpPort); + sprintf(header_data,"Host: %s:%s", URL, TCPPort); chunk = curl_slist_append(chunk, header_data); // Build the Date entry @@ -375,7 +375,7 @@ struct curl_slist *Cloud::BuildHeaderAmzS3v2(const char *Url, const char *TcpPor return chunk; } -Cloud::CloudWriter::CloudWriter(const char* Url, const char* AccessKey, const char* SecretKey, const char* TcpPort, const char* Bucket) +Cloud::CloudWriter::CloudWriter(const char* URL, const char* TokenAuth, const char* TokenSecret, const char* TCPPort, const char* Bucket) { struct Cloud::AmzData *RequestData; CURL *curl; @@ -383,15 +383,15 @@ Cloud::CloudWriter::CloudWriter(const char* Url, const char* AccessKey, const ch std::string s; - this->Url=Url; - this->AccessKey=AccessKey; - this->SecretKey=SecretKey; - this->TcpPort=TcpPort; + this->URL=URL; + this->TokenAuth=TokenAuth; + this->TokenSecret=TokenSecret; + this->TCPPort=TCPPort; this->Bucket=Bucket; this->FileName=""; char path[1024]; sprintf(path,"/%s/", this->Bucket); - RequestData = Cloud::ComputeDigestAmzS3v2("GET", "application/xml", path, this->SecretKey, NULL, 0); + RequestData = Cloud::ComputeDigestAmzS3v2("GET", "application/xml", path, this->TokenSecret, NULL, 0); // Let's build the Header and call to curl curl_global_init(CURL_GLOBAL_ALL); curl = curl_easy_init(); @@ -403,21 +403,21 @@ Cloud::CloudWriter::CloudWriter(const char* Url, const char* AccessKey, const ch { // Let's build our own header struct curl_slist *chunk = NULL; - char Url[256]; - std::string strUrl(this->Url); - eraseSubStr(strUrl,"http://"); - eraseSubStr(strUrl,"https://"); + char URL[256]; + std::string strURL(this->URL); + eraseSubStr(strURL,"http://"); + eraseSubStr(strURL,"https://"); - chunk = Cloud::BuildHeaderAmzS3v2( strUrl.c_str(), this->TcpPort, this->AccessKey, RequestData); + chunk = Cloud::BuildHeaderAmzS3v2( strURL.c_str(), this->TCPPort, this->TokenAuth, RequestData); delete RequestData; curl_easy_setopt(curl, CURLOPT_HTTPHEADER, chunk); - // Lets build the Url for our Curl call + // Lets build the URL for our Curl call - sprintf(Url,"%s:%s/%s/", this->Url,this->TcpPort, + sprintf(URL,"%s:%s/%s/", this->URL,this->TCPPort, this->Bucket); - curl_easy_setopt(curl, CURLOPT_URL, Url); + curl_easy_setopt(curl, CURLOPT_URL, URL); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWrite_CallbackFunc_StdString); curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); @@ -557,7 +557,7 @@ Cloud::CloudReader::~CloudReader() { } -Cloud::CloudReader::CloudReader(const char* Url, const char* AccessKey, const char* SecretKey, const char* TcpPort, const char* Bucket) +Cloud::CloudReader::CloudReader(const char* URL, const char* TokenAuth, const char* TokenSecret, const char* TCPPort, const char* Bucket) { struct Cloud::AmzData *RequestData; CURL *curl; @@ -565,10 +565,10 @@ Cloud::CloudReader::CloudReader(const char* Url, const char* AccessKey, const ch bool GetBucketContentList=true; - this->Url=Url; - this->AccessKey=AccessKey; - this->SecretKey=SecretKey; - this->TcpPort=TcpPort; + this->URL=URL; + this->TokenAuth=TokenAuth; + this->TokenSecret=TokenSecret; + this->TCPPort=TCPPort; this->Bucket=Bucket; char path[1024]; @@ -584,7 +584,7 @@ Cloud::CloudReader::CloudReader(const char* Url, const char* AccessKey, const ch while ( GetBucketContentList ) { std::string s; - RequestData = Cloud::ComputeDigestAmzS3v2("GET", "application/xml", path, this->SecretKey, NULL, 0); + RequestData = Cloud::ComputeDigestAmzS3v2("GET", "application/xml", path, this->TokenSecret, NULL, 0); curl = curl_easy_init(); #ifdef ALLOW_SELF_SIGNED_CERTIFICATE curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0); @@ -594,22 +594,22 @@ Cloud::CloudReader::CloudReader(const char* Url, const char* AccessKey, const ch { // Let's build our own header struct curl_slist *chunk = NULL; - char Url[256]; - std::string strUrl(this->Url); - eraseSubStr(strUrl,"http://"); - eraseSubStr(strUrl,"https://"); + char URL[256]; + std::string strURL(this->URL); + eraseSubStr(strURL,"http://"); + eraseSubStr(strURL,"https://"); - chunk = Cloud::BuildHeaderAmzS3v2( strUrl.c_str(), this->TcpPort, this->AccessKey, RequestData); + chunk = Cloud::BuildHeaderAmzS3v2( strURL.c_str(), this->TCPPort, this->TokenAuth, RequestData); delete RequestData; curl_easy_setopt(curl, CURLOPT_HTTPHEADER, chunk); if ( strlen(NextFileName) == 0 ) - sprintf(Url,"%s:%s/%s/?list-type=2", this->Url,this->TcpPort, + sprintf(URL,"%s:%s/%s/?list-type=2", this->URL,this->TCPPort, this->Bucket); else - sprintf(Url,"%s:%s/%s/?list-type=2&continuation-token=%s", this->Url,this->TcpPort, + sprintf(URL,"%s:%s/%s/?list-type=2&continuation-token=%s", this->URL,this->TCPPort, this->Bucket, NextFileName); - curl_easy_setopt(curl, CURLOPT_URL, Url); + curl_easy_setopt(curl, CURLOPT_URL, URL); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWrite_CallbackFunc_StdString); @@ -669,7 +669,7 @@ void Cloud::CloudReader::DownloadFile(Cloud::CloudReader::FileEntry *entry) // We must get the directory content char path[1024]; sprintf(path, "/%s/%s", this->Bucket, entry->FileName); - RequestData = Cloud::ComputeDigestAmzS3v2("GET", "application/octet-stream", path, this->SecretKey, NULL, 0); + RequestData = Cloud::ComputeDigestAmzS3v2("GET", "application/octet-stream", path, this->TokenSecret, NULL, 0); // Let's build the Header and call to curl curl_global_init(CURL_GLOBAL_ALL); @@ -681,20 +681,20 @@ void Cloud::CloudReader::DownloadFile(Cloud::CloudReader::FileEntry *entry) if ( curl ) { struct curl_slist *chunk = NULL; - char Url[256]; + char URL[256]; // Let's build our own header - std::string strUrl(this->Url); - eraseSubStr(strUrl,"http://"); - eraseSubStr(strUrl,"https://"); + std::string strURL(this->URL); + eraseSubStr(strURL,"http://"); + eraseSubStr(strURL,"https://"); - chunk = Cloud::BuildHeaderAmzS3v2( strUrl.c_str(), this->TcpPort, this->AccessKey, RequestData); + chunk = Cloud::BuildHeaderAmzS3v2( strURL.c_str(), this->TCPPort, this->TokenAuth, RequestData); delete RequestData; curl_easy_setopt(curl, CURLOPT_HTTPHEADER, chunk); - sprintf(Url,"%s:%s/%s/%s", this->Url,this->TcpPort, + sprintf(URL,"%s:%s/%s/%s", this->URL,this->TCPPort, this->Bucket, entry->FileName); - curl_easy_setopt(curl, CURLOPT_URL, Url); + curl_easy_setopt(curl, CURLOPT_URL, URL); // curl_easy_setopt(curl, CURLOPT_VERBOSE, 1); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWrite_CallbackFunc_StdString); @@ -783,7 +783,7 @@ void Cloud::CloudWriter::pushCloud(const char *FileName, const char *data, long char path[1024]; sprintf(path, "/%s/%s", this->Bucket, FileName); - RequestData = Cloud::ComputeDigestAmzS3v2("PUT", "application/octet-stream", path, this->SecretKey, data, size); + RequestData = Cloud::ComputeDigestAmzS3v2("PUT", "application/octet-stream", path, this->TokenSecret, data, size); // Let's build the Header and call to curl curl_global_init(CURL_GLOBAL_ALL); @@ -795,23 +795,23 @@ void Cloud::CloudWriter::pushCloud(const char *FileName, const char *data, long if ( curl ) { struct curl_slist *chunk = NULL; - char Url[256]; + char URL[256]; // Let's build our own header - std::string strUrl(this->Url); - eraseSubStr(strUrl,"http://"); - eraseSubStr(strUrl,"https://"); + std::string strURL(this->URL); + eraseSubStr(strURL,"http://"); + eraseSubStr(strURL,"https://"); - chunk = Cloud::BuildHeaderAmzS3v2( strUrl.c_str(), this->TcpPort, this->AccessKey, RequestData); + chunk = Cloud::BuildHeaderAmzS3v2( strURL.c_str(), this->TCPPort, this->TokenAuth, RequestData); delete RequestData; curl_easy_setopt(curl, CURLOPT_HTTPHEADER, chunk); - // Lets build the Url for our Curl call + // Lets build the URL for our Curl call - sprintf(Url,"%s:%s/%s/%s", this->Url,this->TcpPort, + sprintf(URL,"%s:%s/%s/%s", this->URL,this->TCPPort, this->Bucket,FileName); - curl_easy_setopt(curl, CURLOPT_URL, Url); + curl_easy_setopt(curl, CURLOPT_URL, URL); // curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L); @@ -898,10 +898,10 @@ bool Cloud::Module::cloudSave(const char *BucketName) if ( strcmp(BucketName, doc->Label.getValue()) != 0 ) doc->Label.setValue(BucketName); - Cloud::CloudWriter mywriter((const char*)this->Url.getStrValue().c_str(), - (const char*)this->AccessKey.getStrValue().c_str(), - (const char*)this->SecretKey.getStrValue().c_str(), - (const char*)this->TcpPort.getStrValue().c_str(), + Cloud::CloudWriter mywriter((const char*)this->URL.getStrValue().c_str(), + (const char*)this->TokenAuth.getStrValue().c_str(), + (const char*)this->TokenSecret.getStrValue().c_str(), + (const char*)this->TCPPort.getStrValue().c_str(), BucketName); mywriter.putNextEntry("Document.xml"); @@ -972,10 +972,10 @@ bool Cloud::Module::cloudRestore (const char *BucketName) std::stringstream oss; - Cloud::CloudReader myreader((const char*)this->Url.getStrValue().c_str(), - (const char*)this->AccessKey.getStrValue().c_str(), - (const char*)this->SecretKey.getStrValue().c_str(), - (const char*)this->TcpPort.getStrValue().c_str(), + Cloud::CloudReader myreader((const char*)this->URL.getStrValue().c_str(), + (const char*)this->TokenAuth.getStrValue().c_str(), + (const char*)this->TokenSecret.getStrValue().c_str(), + (const char*)this->TCPPort.getStrValue().c_str(), BucketName); // we shall pass there the initial Document.xml file diff --git a/src/Mod/Cloud/App/AppCloud.h b/src/Mod/Cloud/App/AppCloud.h index 63ee9e852a..23dcc75967 100644 --- a/src/Mod/Cloud/App/AppCloud.h +++ b/src/Mod/Cloud/App/AppCloud.h @@ -58,13 +58,13 @@ struct AmzData { void eraseSubStr(std::string & Str, const std::string & toErase); size_t CurlWrite_CallbackFunc_StdString(void *contents, size_t size, size_t nmemb, std::string *s); struct AmzData *ComputeDigestAmzS3v2(char *operation, char *data_type, const char *target, const char *Secret, const char *ptr, long size); -struct curl_slist *BuildHeaderAmzS3v2(const char *Url, const char *TcpPort, const char *PublicKey, struct AmzData *Data); +struct curl_slist *BuildHeaderAmzS3v2(const char *URL, const char *TCPPort, const char *PublicKey, struct AmzData *Data); char *MD5Sum(const char *ptr, long size); class CloudAppExport CloudReader { public: - CloudReader(const char* Url, const char* AccessKey, const char* SecretKey, const char* TcpPort, const char* Bucket); + CloudReader(const char* URL, const char* AccessKey, const char* SecretKey, const char* TCPPort, const char* Bucket); virtual ~CloudReader(); int file=0; int continuation=0; @@ -86,10 +86,10 @@ public: protected: std::list FileList; char* NextFileName; - const char* Url; - const char* TcpPort; - const char* AccessKey; - const char* SecretKey; + const char* URL; + const char* TCPPort; + const char* TokenAuth; + const char* TokenSecret; const char* Bucket; }; @@ -98,28 +98,28 @@ class Module : public Py::ExtensionModule public: Module() : Py::ExtensionModule("Cloud") { - add_varargs_method("cloudurl",&Module::sCloudUrl, - "cloudurl(string) -- Connect to a Cloud Storage service." + add_varargs_method("URL",&Module::sCloudURL, + "URL(string) -- Connect to a Cloud Storage service." ); - add_varargs_method("cloudaccesskey",&Module::sCloudAccessKey, - "cloudurl(string) -- Connect to a Cloud Storage service." + add_varargs_method("TokenAuth",&Module::sCloudTokenAuth, + "TokenAuth(string) -- Token Authorization string." ); - add_varargs_method("cloudsecretkey",&Module::sCloudSecretKey, - "cloudurl(string) -- Connect to a Cloud Storage service." + add_varargs_method("TokenSecret",&Module::sCloudTokenSecret, + "TokenSecret(string) -- Token Secret string." ); - add_varargs_method("cloudtcpport",&Module::sCloudTcpPort, - "cloudurl(string) -- Connect to a Cloud Storage service." + add_varargs_method("TCPPort",&Module::sCloudTCPPort, + "TCPPort(string) -- Port number." ); - add_varargs_method("cloudsave",&Module::sCloudSave, - "cloudurl(string) -- Connect to a Cloud Storage service." + add_varargs_method("Save",&Module::sCloudSave, + "Save(string) -- Save the active document to the Cloud." ); - add_varargs_method("cloudrestore",&Module::sCloudRestore, - "cloudurl(string) -- Connect to a Cloud Storage service." + add_varargs_method("Restore",&Module::sCloudRestore, + "Restore(string) -- Restore to the active document from the Cloud." ); initialize("This module is the Cloud module."); // register with Python @@ -127,18 +127,18 @@ public: virtual ~Module() {} - App::PropertyString Url; - App::PropertyString TcpPort; - App::PropertyString AccessKey; - App::PropertyString SecretKey; + App::PropertyString URL; + App::PropertyString TCPPort; + App::PropertyString TokenAuth; + App::PropertyString TokenSecret; bool cloudSave(const char* BucketName); bool cloudRestore(const char* BucketName); private: - Py::Object sCloudUrl (const Py::Tuple& args); - Py::Object sCloudAccessKey (const Py::Tuple& args); - Py::Object sCloudSecretKey (const Py::Tuple& args); - Py::Object sCloudTcpPort (const Py::Tuple& args); + Py::Object sCloudURL (const Py::Tuple& args); + Py::Object sCloudTokenAuth (const Py::Tuple& args); + Py::Object sCloudTokenSecret (const Py::Tuple& args); + Py::Object sCloudTCPPort (const Py::Tuple& args); Py::Object sCloudSave (const Py::Tuple& args); Py::Object sCloudRestore (const Py::Tuple& args); @@ -158,7 +158,7 @@ class CloudAppExport CloudWriter : public Base::Writer public: int print=0; char errorCode[1024]=""; - CloudWriter(const char* Url, const char* AccessKey, const char* SecretKey, const char* TcpPort, const char* Bucket); + CloudWriter(const char* URL, const char* TokenAuth, const char* TokenSecret, const char* TCPPort, const char* Bucket); virtual ~CloudWriter(); void pushCloud(const char *FileName, const char *data, long size); void putNextEntry(const char* file); @@ -173,10 +173,10 @@ public: protected: std::string FileName; - const char* Url; - const char* TcpPort; - const char* AccessKey; - const char* SecretKey; + const char* URL; + const char* TCPPort; + const char* TokenAuth; + const char* TokenSecret; const char* Bucket; std::stringstream FileStream; }; diff --git a/src/Mod/Draft/CMakeLists.txt b/src/Mod/Draft/CMakeLists.txt index c32795e1c3..f076366bb0 100644 --- a/src/Mod/Draft/CMakeLists.txt +++ b/src/Mod/Draft/CMakeLists.txt @@ -49,6 +49,7 @@ SET(Draft_tests SET(Draft_utilities draftutils/__init__.py draftutils/init_tools.py + draftutils/init_draft_statusbar.py draftutils/utils.py draftutils/gui_utils.py draftutils/todo.py @@ -63,6 +64,10 @@ SET(Draft_objects draftobjects/orthoarray.py draftobjects/polararray.py draftobjects/arc_3points.py + draftobjects/draft_annotation.py + draftobjects/label.py + draftobjects/dimension.py + draftobjects/text.py draftobjects/README.md ) @@ -71,6 +76,10 @@ SET(Draft_view_providers draftviewproviders/view_circulararray.py draftviewproviders/view_orthoarray.py draftviewproviders/view_polararray.py + draftviewproviders/view_draft_annotation.py + draftviewproviders/view_label.py + draftviewproviders/view_dimension.py + draftviewproviders/view_text.py draftviewproviders/README.md ) @@ -87,6 +96,14 @@ SET(Draft_GUI_tools draftguitools/gui_snapper.py draftguitools/gui_trackers.py draftguitools/gui_edit.py + draftguitools/gui_lineops.py + draftguitools/gui_togglemodes.py + draftguitools/gui_groups.py + draftguitools/gui_grid.py + draftguitools/gui_heal.py + draftguitools/gui_dimension_ops.py + draftguitools/gui_lineslope.py + draftguitools/gui_arcs.py draftguitools/README.md ) diff --git a/src/Mod/Draft/Draft.py b/src/Mod/Draft/Draft.py index 2f0aea0d45..9fd870b6f2 100644 --- a/src/Mod/Draft/Draft.py +++ b/src/Mod/Draft/Draft.py @@ -169,6 +169,80 @@ from draftutils.gui_utils import select from draftutils.gui_utils import loadTexture from draftutils.gui_utils import load_texture +#--------------------------------------------------------------------------- +# Draft objects +#--------------------------------------------------------------------------- + + + +#--------------------------------------------------------------------------- +# Draft annotation objects +#--------------------------------------------------------------------------- + +from draftobjects.dimension import make_dimension, make_angular_dimension +from draftobjects.dimension import LinearDimension, AngularDimension + +makeDimension = make_dimension +makeAngularDimension = make_angular_dimension +_Dimension = LinearDimension +_AngularDimension = AngularDimension + +if gui: + from draftviewproviders.view_dimension import ViewProviderLinearDimension + from draftviewproviders.view_dimension import ViewProviderAngularDimension + _ViewProviderDimension = ViewProviderLinearDimension + _ViewProviderAngularDimension = ViewProviderAngularDimension + + +from draftobjects.label import make_label +from draftobjects.label import Label + +makeLabel = make_label +DraftLabel = Label + +if gui: + from draftviewproviders.view_label import ViewProviderLabel + ViewProviderDraftLabel = ViewProviderLabel + + +from draftobjects.text import make_text +from draftobjects.text import Text +makeText = make_text +DraftText = Text + +if gui: + from draftviewproviders.view_text import ViewProviderText + ViewProviderDraftText = ViewProviderText + +def convertDraftTexts(textslist=[]): + """ + converts the given Draft texts (or all that is found + in the active document) to the new object + This function was already present at splitting time during v 0.19 + """ + if not isinstance(textslist,list): + textslist = [textslist] + if not textslist: + for o in FreeCAD.ActiveDocument.Objects: + if o.TypeId == "App::Annotation": + textslist.append(o) + todelete = [] + for o in textslist: + l = o.Label + o.Label = l+".old" + obj = makeText(o.LabelText,point=o.Position) + obj.Label = l + todelete.append(o.Name) + for p in o.InList: + if p.isDerivedFrom("App::DocumentObjectGroup"): + if o in p.Group: + g = p.Group + g.append(obj) + p.Group = g + for n in todelete: + FreeCAD.ActiveDocument.removeObject(n) + + def makeCircle(radius, placement=None, face=None, startangle=None, endangle=None, support=None): """makeCircle(radius,[placement,face,startangle,endangle]) @@ -251,116 +325,6 @@ def makeRectangle(length, height, placement=None, face=None, support=None): return obj -def makeDimension(p1,p2,p3=None,p4=None): - """makeDimension(p1,p2,[p3]) or makeDimension(object,i1,i2,p3) - or makeDimension(objlist,indices,p3): Creates a Dimension object with - the dimension line passign through p3.The current line width and color - will be used. There are multiple ways to create a dimension, depending on - the arguments you pass to it: - - (p1,p2,p3): creates a standard dimension from p1 to p2 - - (object,i1,i2,p3): creates a linked dimension to the given object, - measuring the distance between its vertices indexed i1 and i2 - - (object,i1,mode,p3): creates a linked dimension - to the given object, i1 is the index of the (curved) edge to measure, - and mode is either "radius" or "diameter". - """ - if not FreeCAD.ActiveDocument: - FreeCAD.Console.PrintError("No active document. Aborting\n") - return - obj = FreeCAD.ActiveDocument.addObject("App::FeaturePython","Dimension") - _Dimension(obj) - if gui: - _ViewProviderDimension(obj.ViewObject) - if isinstance(p1,Vector) and isinstance(p2,Vector): - obj.Start = p1 - obj.End = p2 - if not p3: - p3 = p2.sub(p1) - p3.multiply(0.5) - p3 = p1.add(p3) - elif isinstance(p2,int) and isinstance(p3,int): - l = [] - idx = (p2,p3) - l.append((p1,"Vertex"+str(p2+1))) - l.append((p1,"Vertex"+str(p3+1))) - obj.LinkedGeometry = l - obj.Support = p1 - p3 = p4 - if not p3: - v1 = obj.Base.Shape.Vertexes[idx[0]].Point - v2 = obj.Base.Shape.Vertexes[idx[1]].Point - p3 = v2.sub(v1) - p3.multiply(0.5) - p3 = v1.add(p3) - elif isinstance(p3,str): - l = [] - l.append((p1,"Edge"+str(p2+1))) - if p3 == "radius": - #l.append((p1,"Center")) - if FreeCAD.GuiUp: - obj.ViewObject.Override = "R $dim" - obj.Diameter = False - elif p3 == "diameter": - #l.append((p1,"Diameter")) - if FreeCAD.GuiUp: - obj.ViewObject.Override = "Ø $dim" - obj.Diameter = True - obj.LinkedGeometry = l - obj.Support = p1 - p3 = p4 - if not p3: - p3 = p1.Shape.Edges[p2].Curve.Center.add(Vector(1,0,0)) - obj.Dimline = p3 - if hasattr(FreeCAD,"DraftWorkingPlane"): - normal = FreeCAD.DraftWorkingPlane.axis - else: - normal = FreeCAD.Vector(0,0,1) - if gui: - # invert the normal if we are viewing it from the back - vnorm = get3DView().getViewDirection() - if vnorm.getAngle(normal) < math.pi/2: - normal = normal.negative() - obj.Normal = normal - if gui: - formatObject(obj) - select(obj) - - return obj - -def makeAngularDimension(center,angles,p3,normal=None): - """makeAngularDimension(center,angle1,angle2,p3,[normal]): creates an angular Dimension - from the given center, with the given list of angles, passing through p3. - """ - if not FreeCAD.ActiveDocument: - FreeCAD.Console.PrintError("No active document. Aborting\n") - return - obj = FreeCAD.ActiveDocument.addObject("App::FeaturePython","Dimension") - _AngularDimension(obj) - obj.Center = center - for a in range(len(angles)): - if angles[a] > 2*math.pi: - angles[a] = angles[a]-(2*math.pi) - obj.FirstAngle = math.degrees(angles[1]) - obj.LastAngle = math.degrees(angles[0]) - obj.Dimline = p3 - if not normal: - if hasattr(FreeCAD,"DraftWorkingPlane"): - normal = FreeCAD.DraftWorkingPlane.axis - else: - normal = Vector(0,0,1) - if gui: - # invert the normal if we are viewing it from the back - vnorm = get3DView().getViewDirection() - if vnorm.getAngle(normal) < math.pi/2: - normal = normal.negative() - obj.Normal = normal - if gui: - _ViewProviderAngularDimension(obj.ViewObject) - formatObject(obj) - select(obj) - - return obj - def makeWire(pointslist,closed=False,placement=None,face=None,support=None,bs2wire=False): """makeWire(pointslist,[closed],[placement]): Creates a Wire object from the given list of vectors. If closed is True or first @@ -535,35 +499,6 @@ def makeBezCurve(pointslist,closed=False,placement=None,face=None,support=None,d return obj -def makeText(stringslist,point=Vector(0,0,0),screen=False): - """makeText(strings,[point],[screen]): Creates a Text object at the given point, - containing the strings given in the strings list, one string by line (strings - can also be one single string). The current color and text height and font - specified in preferences are used. - If screen is True, the text always faces the view direction.""" - if not FreeCAD.ActiveDocument: - FreeCAD.Console.PrintError("No active document. Aborting\n") - return - typecheck([(point,Vector)], "makeText") - if not isinstance(stringslist,list): stringslist = [stringslist] - obj = FreeCAD.ActiveDocument.addObject("App::FeaturePython","Text") - DraftText(obj) - obj.Text = stringslist - obj.Placement.Base = point - if FreeCAD.GuiUp: - ViewProviderDraftText(obj.ViewObject) - if screen: - obj.ViewObject.DisplayMode = "3D text" - h = getParam("textheight",0.20) - if screen: - h = h*10 - obj.ViewObject.FontSize = h - obj.ViewObject.FontName = getParam("textfont","") - obj.ViewObject.LineSpacing = 1 - formatObject(obj) - select(obj) - return obj - def makeCopy(obj,force=None,reparent=False): """makeCopy(object): returns an exact copy of an object""" if not FreeCAD.ActiveDocument: @@ -3299,6 +3234,8 @@ class _ViewProviderDraft: return ":/icons/Draft_N-Polygon.svg" elif tp in ('Circle', 'Ellipse', 'BSpline', 'BezCurve', 'Fillet'): return ":/icons/Draft_N-Curve.svg" + elif tp in ("ShapeString"): + return ":/icons/Draft_ShapeString_tree.svg" else: return ":/icons/Draft_Draft.svg" @@ -3366,855 +3303,6 @@ class _ViewProviderDraftLink: else: return obj.ElementList -class _Dimension(_DraftObject): - """The Draft Dimension object""" - def __init__(self, obj): - _DraftObject.__init__(self,obj,"Dimension") - obj.addProperty("App::PropertyVectorDistance","Start","Draft",QT_TRANSLATE_NOOP("App::Property","Startpoint of dimension")) - obj.addProperty("App::PropertyVectorDistance","End","Draft",QT_TRANSLATE_NOOP("App::Property","Endpoint of dimension")) - obj.addProperty("App::PropertyVector","Normal","Draft",QT_TRANSLATE_NOOP("App::Property","The normal direction of this dimension")) - obj.addProperty("App::PropertyVector","Direction","Draft",QT_TRANSLATE_NOOP("App::Property","The normal direction of this dimension")) - obj.addProperty("App::PropertyVectorDistance","Dimline","Draft",QT_TRANSLATE_NOOP("App::Property","Point through which the dimension line passes")) - obj.addProperty("App::PropertyLink","Support","Draft",QT_TRANSLATE_NOOP("App::Property","The object measured by this dimension")) - obj.addProperty("App::PropertyLinkSubList","LinkedGeometry","Draft",QT_TRANSLATE_NOOP("App::Property","The geometry this dimension is linked to")) - obj.addProperty("App::PropertyLength","Distance","Draft",QT_TRANSLATE_NOOP("App::Property","The measurement of this dimension")) - obj.addProperty("App::PropertyBool","Diameter","Draft",QT_TRANSLATE_NOOP("App::Property","For arc/circle measurements, false = radius, true = diameter")) - obj.Start = FreeCAD.Vector(0,0,0) - obj.End = FreeCAD.Vector(1,0,0) - obj.Dimline = FreeCAD.Vector(0,1,0) - obj.Normal = FreeCAD.Vector(0,0,1) - - def onChanged(self,obj,prop): - if hasattr(obj,"Distance"): - obj.setEditorMode('Distance',1) - #if hasattr(obj,"Normal"): - # obj.setEditorMode('Normal',2) - if hasattr(obj,"Support"): - obj.setEditorMode('Support',2) - - def execute(self, obj): - import DraftGeomUtils - # set start point and end point according to the linked geometry - if obj.LinkedGeometry: - if len(obj.LinkedGeometry) == 1: - lobj = obj.LinkedGeometry[0][0] - lsub = obj.LinkedGeometry[0][1] - if len(lsub) == 1: - if "Edge" in lsub[0]: - n = int(lsub[0][4:])-1 - edge = lobj.Shape.Edges[n] - if DraftGeomUtils.geomType(edge) == "Line": - obj.Start = edge.Vertexes[0].Point - obj.End = edge.Vertexes[-1].Point - elif DraftGeomUtils.geomType(edge) == "Circle": - c = edge.Curve.Center - r = edge.Curve.Radius - a = edge.Curve.Axis - ray = obj.Dimline.sub(c).projectToPlane(Vector(0,0,0),a) - if (ray.Length == 0): - ray = a.cross(Vector(1,0,0)) - if (ray.Length == 0): - ray = a.cross(Vector(0,1,0)) - ray = DraftVecUtils.scaleTo(ray,r) - if hasattr(obj,"Diameter"): - if obj.Diameter: - obj.Start = c.add(ray.negative()) - obj.End = c.add(ray) - else: - obj.Start = c - obj.End = c.add(ray) - elif len(lsub) == 2: - if ("Vertex" in lsub[0]) and ("Vertex" in lsub[1]): - n1 = int(lsub[0][6:])-1 - n2 = int(lsub[1][6:])-1 - obj.Start = lobj.Shape.Vertexes[n1].Point - obj.End = lobj.Shape.Vertexes[n2].Point - elif len(obj.LinkedGeometry) == 2: - lobj1 = obj.LinkedGeometry[0][0] - lobj2 = obj.LinkedGeometry[1][0] - lsub1 = obj.LinkedGeometry[0][1] - lsub2 = obj.LinkedGeometry[1][1] - if (len(lsub1) == 1) and (len(lsub2) == 1): - if ("Vertex" in lsub1[0]) and ("Vertex" in lsub2[1]): - n1 = int(lsub1[0][6:])-1 - n2 = int(lsub2[0][6:])-1 - obj.Start = lobj1.Shape.Vertexes[n1].Point - obj.End = lobj2.Shape.Vertexes[n2].Point - # set the distance property - total_len = (obj.Start.sub(obj.End)).Length - if round(obj.Distance.Value, precision()) != round(total_len, precision()): - obj.Distance = total_len - if gui: - if obj.ViewObject: - obj.ViewObject.update() - - -class _ViewProviderDimension(_ViewProviderDraft): - """A View Provider for the Draft Dimension object""" - def __init__(self, obj): - obj.addProperty("App::PropertyLength","FontSize","Draft",QT_TRANSLATE_NOOP("App::Property","Font size")) - obj.addProperty("App::PropertyInteger","Decimals","Draft",QT_TRANSLATE_NOOP("App::Property","The number of decimals to show")) - obj.addProperty("App::PropertyLength","ArrowSize","Draft",QT_TRANSLATE_NOOP("App::Property","Arrow size")) - obj.addProperty("App::PropertyLength","TextSpacing","Draft",QT_TRANSLATE_NOOP("App::Property","The spacing between the text and the dimension line")) - obj.addProperty("App::PropertyEnumeration","ArrowType","Draft",QT_TRANSLATE_NOOP("App::Property","Arrow type")) - obj.addProperty("App::PropertyFont","FontName","Draft",QT_TRANSLATE_NOOP("App::Property","Font name")) - obj.addProperty("App::PropertyFloat","LineWidth","Draft",QT_TRANSLATE_NOOP("App::Property","Line width")) - obj.addProperty("App::PropertyColor","LineColor","Draft",QT_TRANSLATE_NOOP("App::Property","Line color")) - obj.addProperty("App::PropertyDistance","ExtLines","Draft",QT_TRANSLATE_NOOP("App::Property","Length of the extension lines")) - obj.addProperty("App::PropertyDistance","DimOvershoot","Draft",QT_TRANSLATE_NOOP("App::Property","The distance the dimension line is extended past the extension lines")) - obj.addProperty("App::PropertyDistance","ExtOvershoot","Draft",QT_TRANSLATE_NOOP("App::Property","Length of the extension line above the dimension line")) - obj.addProperty("App::PropertyBool","FlipArrows","Draft",QT_TRANSLATE_NOOP("App::Property","Rotate the dimension arrows 180 degrees")) - obj.addProperty("App::PropertyBool","FlipText","Draft",QT_TRANSLATE_NOOP("App::Property","Rotate the dimension text 180 degrees")) - obj.addProperty("App::PropertyBool","ShowUnit","Draft",QT_TRANSLATE_NOOP("App::Property","Show the unit suffix")) - obj.addProperty("App::PropertyBool","ShowLine","Draft",QT_TRANSLATE_NOOP("App::Property","Shows the dimension line and arrows")) - obj.addProperty("App::PropertyVectorDistance","TextPosition","Draft",QT_TRANSLATE_NOOP("App::Property","The position of the text. Leave (0,0,0) for automatic position")) - obj.addProperty("App::PropertyString","Override","Draft",QT_TRANSLATE_NOOP("App::Property","Text override. Use $dim to insert the dimension length")) - obj.addProperty("App::PropertyString","UnitOverride","Draft",QT_TRANSLATE_NOOP("App::Property","A unit to express the measurement. Leave blank for system default")) - obj.FontSize = getParam("textheight",0.20) - obj.TextSpacing = getParam("dimspacing",0.05) - obj.FontName = getParam("textfont","") - obj.ArrowSize = getParam("arrowsize",0.1) - obj.ArrowType = arrowtypes - obj.ArrowType = arrowtypes[getParam("dimsymbol",0)] - obj.ExtLines = getParam("extlines",0.3) - obj.DimOvershoot = getParam("dimovershoot",0) - obj.ExtOvershoot = getParam("extovershoot",0) - obj.Decimals = getParam("dimPrecision",2) - obj.ShowUnit = getParam("showUnit",True) - obj.ShowLine = True - _ViewProviderDraft.__init__(self,obj) - - def attach(self, vobj): - """called on object creation""" - from pivy import coin - self.Object = vobj.Object - self.color = coin.SoBaseColor() - self.font = coin.SoFont() - self.font3d = coin.SoFont() - self.text = coin.SoAsciiText() - self.text3d = coin.SoText2() - self.text.string = "d" # some versions of coin crash if string is not set - self.text3d.string = "d" - self.textpos = coin.SoTransform() - self.text.justification = self.text3d.justification = coin.SoAsciiText.CENTER - label = coin.SoSeparator() - label.addChild(self.textpos) - label.addChild(self.color) - label.addChild(self.font) - label.addChild(self.text) - label3d = coin.SoSeparator() - label3d.addChild(self.textpos) - label3d.addChild(self.color) - label3d.addChild(self.font3d) - label3d.addChild(self.text3d) - self.coord1 = coin.SoCoordinate3() - self.trans1 = coin.SoTransform() - self.coord2 = coin.SoCoordinate3() - self.trans2 = coin.SoTransform() - self.transDimOvershoot1 = coin.SoTransform() - self.transDimOvershoot2 = coin.SoTransform() - self.transExtOvershoot1 = coin.SoTransform() - self.transExtOvershoot2 = coin.SoTransform() - self.marks = coin.SoSeparator() - self.marksDimOvershoot = coin.SoSeparator() - self.marksExtOvershoot = coin.SoSeparator() - self.drawstyle = coin.SoDrawStyle() - self.line = coin.SoType.fromName("SoBrepEdgeSet").createInstance() - self.coords = coin.SoCoordinate3() - self.node = coin.SoGroup() - self.node.addChild(self.color) - self.node.addChild(self.drawstyle) - self.lineswitch2 = coin.SoSwitch() - self.lineswitch2.whichChild = -3 - self.node.addChild(self.lineswitch2) - self.lineswitch2.addChild(self.coords) - self.lineswitch2.addChild(self.line) - self.lineswitch2.addChild(self.marks) - self.lineswitch2.addChild(self.marksDimOvershoot) - self.lineswitch2.addChild(self.marksExtOvershoot) - self.node.addChild(label) - self.node3d = coin.SoGroup() - self.node3d.addChild(self.color) - self.node3d.addChild(self.drawstyle) - self.lineswitch3 = coin.SoSwitch() - self.lineswitch3.whichChild = -3 - self.node3d.addChild(self.lineswitch3) - self.lineswitch3.addChild(self.coords) - self.lineswitch3.addChild(self.line) - self.lineswitch3.addChild(self.marks) - self.lineswitch3.addChild(self.marksDimOvershoot) - self.lineswitch3.addChild(self.marksExtOvershoot) - self.node3d.addChild(label3d) - vobj.addDisplayMode(self.node,"2D") - vobj.addDisplayMode(self.node3d,"3D") - self.updateData(vobj.Object,"Start") - self.onChanged(vobj,"FontSize") - self.onChanged(vobj,"FontName") - self.onChanged(vobj,"ArrowType") - self.onChanged(vobj,"LineColor") - self.onChanged(vobj,"DimOvershoot") - self.onChanged(vobj,"ExtOvershoot") - - def updateData(self, obj, prop): - """called when the base object is changed""" - import DraftGui - if prop in ["Start","End","Dimline","Direction"]: - - if obj.Start == obj.End: - return - - if not hasattr(self,"node"): - return - - import Part, DraftGeomUtils - from pivy import coin - - # calculate the 4 points - self.p1 = obj.Start - self.p4 = obj.End - base = None - if hasattr(obj,"Direction"): - if not DraftVecUtils.isNull(obj.Direction): - v2 = self.p1.sub(obj.Dimline) - v3 = self.p4.sub(obj.Dimline) - v2 = DraftVecUtils.project(v2,obj.Direction) - v3 = DraftVecUtils.project(v3,obj.Direction) - self.p2 = obj.Dimline.add(v2) - self.p3 = obj.Dimline.add(v3) - if DraftVecUtils.equals(self.p2,self.p3): - base = None - proj = None - else: - base = Part.LineSegment(self.p2,self.p3).toShape() - proj = DraftGeomUtils.findDistance(self.p1,base) - if proj: - proj = proj.negative() - if not base: - if DraftVecUtils.equals(self.p1,self.p4): - base = None - proj = None - else: - base = Part.LineSegment(self.p1,self.p4).toShape() - proj = DraftGeomUtils.findDistance(obj.Dimline,base) - if proj: - self.p2 = self.p1.add(proj.negative()) - self.p3 = self.p4.add(proj.negative()) - else: - self.p2 = self.p1 - self.p3 = self.p4 - if proj: - if hasattr(obj.ViewObject,"ExtLines"): - dmax = obj.ViewObject.ExtLines.Value - if dmax and (proj.Length > dmax): - if (dmax > 0): - self.p1 = self.p2.add(DraftVecUtils.scaleTo(proj,dmax)) - self.p4 = self.p3.add(DraftVecUtils.scaleTo(proj,dmax)) - else: - rest = proj.Length + dmax - self.p1 = self.p2.add(DraftVecUtils.scaleTo(proj,rest)) - self.p4 = self.p3.add(DraftVecUtils.scaleTo(proj,rest)) - else: - proj = (self.p3.sub(self.p2)).cross(Vector(0,0,1)) - - # calculate the arrows positions - self.trans1.translation.setValue((self.p2.x,self.p2.y,self.p2.z)) - self.coord1.point.setValue((self.p2.x,self.p2.y,self.p2.z)) - self.trans2.translation.setValue((self.p3.x,self.p3.y,self.p3.z)) - self.coord2.point.setValue((self.p3.x,self.p3.y,self.p3.z)) - - # calculate dimension and extension lines overshoots positions - self.transDimOvershoot1.translation.setValue((self.p2.x,self.p2.y,self.p2.z)) - self.transDimOvershoot2.translation.setValue((self.p3.x,self.p3.y,self.p3.z)) - self.transExtOvershoot1.translation.setValue((self.p2.x,self.p2.y,self.p2.z)) - self.transExtOvershoot2.translation.setValue((self.p3.x,self.p3.y,self.p3.z)) - - # calculate the text position and orientation - if hasattr(obj,"Normal"): - if DraftVecUtils.isNull(obj.Normal): - if proj: - norm = (self.p3.sub(self.p2).cross(proj)).negative() - else: - norm = Vector(0,0,1) - else: - norm = FreeCAD.Vector(obj.Normal) - else: - if proj: - norm = (self.p3.sub(self.p2).cross(proj)).negative() - else: - norm = Vector(0,0,1) - if not DraftVecUtils.isNull(norm): - norm.normalize() - u = self.p3.sub(self.p2) - u.normalize() - v1 = norm.cross(u) - rot1 = FreeCAD.Placement(DraftVecUtils.getPlaneRotation(u,v1,norm)).Rotation.Q - self.transDimOvershoot1.rotation.setValue((rot1[0],rot1[1],rot1[2],rot1[3])) - self.transDimOvershoot2.rotation.setValue((rot1[0],rot1[1],rot1[2],rot1[3])) - if hasattr(obj.ViewObject,"FlipArrows"): - if obj.ViewObject.FlipArrows: - u = u.negative() - v2 = norm.cross(u) - rot2 = FreeCAD.Placement(DraftVecUtils.getPlaneRotation(u,v2,norm)).Rotation.Q - self.trans1.rotation.setValue((rot2[0],rot2[1],rot2[2],rot2[3])) - self.trans2.rotation.setValue((rot2[0],rot2[1],rot2[2],rot2[3])) - if self.p1 != self.p2: - u3 = self.p1.sub(self.p2) - u3.normalize() - v3 = norm.cross(u3) - rot3 = FreeCAD.Placement(DraftVecUtils.getPlaneRotation(u3,v3,norm)).Rotation.Q - self.transExtOvershoot1.rotation.setValue((rot3[0],rot3[1],rot3[2],rot3[3])) - self.transExtOvershoot2.rotation.setValue((rot3[0],rot3[1],rot3[2],rot3[3])) - if hasattr(obj.ViewObject,"TextSpacing"): - offset = DraftVecUtils.scaleTo(v1,obj.ViewObject.TextSpacing.Value) - else: - offset = DraftVecUtils.scaleTo(v1,0.05) - rott = rot1 - if hasattr(obj.ViewObject,"FlipText"): - if obj.ViewObject.FlipText: - rott = FreeCAD.Rotation(*rott).multiply(FreeCAD.Rotation(norm,180)).Q - offset = offset.negative() - # setting text - try: - m = obj.ViewObject.DisplayMode - except: # swallow all exceptions here since it always fails on first run (Displaymode enum no set yet) - m = ["2D","3D"][getParam("dimstyle",0)] - if m == "3D": - offset = offset.negative() - self.tbase = (self.p2.add((self.p3.sub(self.p2).multiply(0.5)))).add(offset) - if hasattr(obj.ViewObject,"TextPosition"): - if not DraftVecUtils.isNull(obj.ViewObject.TextPosition): - self.tbase = obj.ViewObject.TextPosition - self.textpos.translation.setValue([self.tbase.x,self.tbase.y,self.tbase.z]) - self.textpos.rotation = coin.SbRotation(rott[0],rott[1],rott[2],rott[3]) - su = True - if hasattr(obj.ViewObject,"ShowUnit"): - su = obj.ViewObject.ShowUnit - # set text value - l = self.p3.sub(self.p2).Length - unit = None - if hasattr(obj.ViewObject,"UnitOverride"): - unit = obj.ViewObject.UnitOverride - # special representation if "Building US" scheme - if FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Units").GetInt("UserSchema",0) == 5: - s = FreeCAD.Units.Quantity(l,FreeCAD.Units.Length).UserString - self.string = s.replace("' ","'- ") - self.string = s.replace("+"," ") - elif hasattr(obj.ViewObject,"Decimals"): - self.string = DraftGui.displayExternal(l,obj.ViewObject.Decimals,'Length',su,unit) - else: - self.string = DraftGui.displayExternal(l,None,'Length',su,unit) - if hasattr(obj.ViewObject,"Override"): - if obj.ViewObject.Override: - self.string = obj.ViewObject.Override.replace("$dim",\ - self.string) - self.text.string = self.text3d.string = stringencodecoin(self.string) - - # set the lines - if m == "3D": - # calculate the spacing of the text - textsize = (len(self.string)*obj.ViewObject.FontSize.Value)/4.0 - spacing = ((self.p3.sub(self.p2)).Length/2.0) - textsize - self.p2a = self.p2.add(DraftVecUtils.scaleTo(self.p3.sub(self.p2),spacing)) - self.p2b = self.p3.add(DraftVecUtils.scaleTo(self.p2.sub(self.p3),spacing)) - self.coords.point.setValues([[self.p1.x,self.p1.y,self.p1.z], - [self.p2.x,self.p2.y,self.p2.z], - [self.p2a.x,self.p2a.y,self.p2a.z], - [self.p2b.x,self.p2b.y,self.p2b.z], - [self.p3.x,self.p3.y,self.p3.z], - [self.p4.x,self.p4.y,self.p4.z]]) - #self.line.numVertices.setValues([3,3]) - self.line.coordIndex.setValues(0,7,(0,1,2,-1,3,4,5)) - else: - self.coords.point.setValues([[self.p1.x,self.p1.y,self.p1.z], - [self.p2.x,self.p2.y,self.p2.z], - [self.p3.x,self.p3.y,self.p3.z], - [self.p4.x,self.p4.y,self.p4.z]]) - #self.line.numVertices.setValue(4) - self.line.coordIndex.setValues(0,4,(0,1,2,3)) - - def onChanged(self, vobj, prop): - """called when a view property has changed""" - - if (prop == "FontSize") and hasattr(vobj,"FontSize"): - if hasattr(self,"font"): - self.font.size = vobj.FontSize.Value - if hasattr(self,"font3d"): - self.font3d.size = vobj.FontSize.Value*100 - vobj.Object.touch() - elif (prop == "FontName") and hasattr(vobj,"FontName"): - if hasattr(self,"font") and hasattr(self,"font3d"): - self.font.name = self.font3d.name = str(vobj.FontName) - vobj.Object.touch() - elif (prop == "LineColor") and hasattr(vobj,"LineColor"): - if hasattr(self,"color"): - c = vobj.LineColor - self.color.rgb.setValue(c[0],c[1],c[2]) - elif (prop == "LineWidth") and hasattr(vobj,"LineWidth"): - if hasattr(self,"drawstyle"): - self.drawstyle.lineWidth = vobj.LineWidth - elif (prop in ["ArrowSize","ArrowType"]) and hasattr(vobj,"ArrowSize"): - if hasattr(self,"node") and hasattr(self,"p2"): - from pivy import coin - - if not hasattr(vobj,"ArrowType"): - return - - if self.p3.x < self.p2.x: - inv = False - else: - inv = True - - # set scale - symbol = arrowtypes.index(vobj.ArrowType) - s = vobj.ArrowSize.Value - self.trans1.scaleFactor.setValue((s,s,s)) - self.trans2.scaleFactor.setValue((s,s,s)) - - # remove existing nodes - self.node.removeChild(self.marks) - self.node3d.removeChild(self.marks) - - # set new nodes - self.marks = coin.SoSeparator() - self.marks.addChild(self.color) - s1 = coin.SoSeparator() - if symbol == "Circle": - s1.addChild(self.coord1) - else: - s1.addChild(self.trans1) - s1.addChild(dimSymbol(symbol,invert=not(inv))) - self.marks.addChild(s1) - s2 = coin.SoSeparator() - if symbol == "Circle": - s2.addChild(self.coord2) - else: - s2.addChild(self.trans2) - s2.addChild(dimSymbol(symbol,invert=inv)) - self.marks.addChild(s2) - self.node.insertChild(self.marks,2) - self.node3d.insertChild(self.marks,2) - vobj.Object.touch() - elif (prop == "DimOvershoot") and hasattr(vobj,"DimOvershoot"): - from pivy import coin - - # set scale - s = vobj.DimOvershoot.Value - self.transDimOvershoot1.scaleFactor.setValue((s,s,s)) - self.transDimOvershoot2.scaleFactor.setValue((s,s,s)) - - # remove existing nodes - self.node.removeChild(self.marksDimOvershoot) - self.node3d.removeChild(self.marksDimOvershoot) - - # set new nodes - self.marksDimOvershoot = coin.SoSeparator() - if vobj.DimOvershoot.Value: - self.marksDimOvershoot.addChild(self.color) - s1 = coin.SoSeparator() - s1.addChild(self.transDimOvershoot1) - s1.addChild(dimDash((-1,0,0),(0,0,0))) - self.marksDimOvershoot.addChild(s1) - s2 = coin.SoSeparator() - s2.addChild(self.transDimOvershoot2) - s2.addChild(dimDash((0,0,0),(1,0,0))) - self.marksDimOvershoot.addChild(s2) - self.node.insertChild(self.marksDimOvershoot,2) - self.node3d.insertChild(self.marksDimOvershoot,2) - vobj.Object.touch() - elif (prop == "ExtOvershoot") and hasattr(vobj,"ExtOvershoot"): - from pivy import coin - - # set scale - s = vobj.ExtOvershoot.Value - self.transExtOvershoot1.scaleFactor.setValue((s,s,s)) - self.transExtOvershoot2.scaleFactor.setValue((s,s,s)) - - # remove existing nodes - self.node.removeChild(self.marksExtOvershoot) - self.node3d.removeChild(self.marksExtOvershoot) - - # set new nodes - self.marksExtOvershoot = coin.SoSeparator() - if vobj.ExtOvershoot.Value: - self.marksExtOvershoot.addChild(self.color) - s1 = coin.SoSeparator() - s1.addChild(self.transExtOvershoot1) - s1.addChild(dimDash((0,0,0),(-1,0,0))) - self.marksExtOvershoot.addChild(s1) - s2 = coin.SoSeparator() - s2.addChild(self.transExtOvershoot2) - s2.addChild(dimDash((0,0,0),(-1,0,0))) - self.marksExtOvershoot.addChild(s2) - self.node.insertChild(self.marksExtOvershoot,2) - self.node3d.insertChild(self.marksExtOvershoot,2) - vobj.Object.touch() - elif (prop == "ShowLine") and hasattr(vobj,"ShowLine"): - if vobj.ShowLine: - self.lineswitch2.whichChild = -3 - self.lineswitch3.whichChild = -3 - else: - self.lineswitch2.whichChild = -1 - self.lineswitch3.whichChild = -1 - else: - self.updateData(vobj.Object,"Start") - - def doubleClicked(self,vobj): - self.setEdit(vobj) - - def getDisplayModes(self,vobj): - return ["2D","3D"] - - def getDefaultDisplayMode(self): - if hasattr(self,"defaultmode"): - return self.defaultmode - else: - return ["2D","3D"][getParam("dimstyle",0)] - - def setDisplayMode(self,mode): - return mode - - def is_linked_to_circle(self): - import DraftGeomUtils - _obj = self.Object - if _obj.LinkedGeometry and len(_obj.LinkedGeometry) == 1: - lobj = _obj.LinkedGeometry[0][0] - lsub = _obj.LinkedGeometry[0][1] - if len(lsub) == 1 and "Edge" in lsub[0]: - n = int(lsub[0][4:]) - 1 - edge = lobj.Shape.Edges[n] - if DraftGeomUtils.geomType(edge) == "Circle": - return True - return False - - def getIcon(self): - if self.is_linked_to_circle(): - return ":/icons/Draft_DimensionRadius.svg" - return ":/icons/Draft_Dimension_Tree.svg" - - def __getstate__(self): - return self.Object.ViewObject.DisplayMode - - def __setstate__(self,state): - if state: - self.defaultmode = state - self.setDisplayMode(state) - -class _AngularDimension(_DraftObject): - """The Draft AngularDimension object""" - def __init__(self, obj): - _DraftObject.__init__(self,obj,"AngularDimension") - obj.addProperty("App::PropertyAngle","FirstAngle","Draft",QT_TRANSLATE_NOOP("App::Property","Start angle of the dimension")) - obj.addProperty("App::PropertyAngle","LastAngle","Draft",QT_TRANSLATE_NOOP("App::Property","End angle of the dimension")) - obj.addProperty("App::PropertyVectorDistance","Dimline","Draft",QT_TRANSLATE_NOOP("App::Property","Point through which the dimension line passes")) - obj.addProperty("App::PropertyVectorDistance","Center","Draft",QT_TRANSLATE_NOOP("App::Property","The center point of this dimension")) - obj.addProperty("App::PropertyVector","Normal","Draft",QT_TRANSLATE_NOOP("App::Property","The normal direction of this dimension")) - obj.addProperty("App::PropertyLink","Support","Draft",QT_TRANSLATE_NOOP("App::Property","The object measured by this dimension")) - obj.addProperty("App::PropertyLinkSubList","LinkedGeometry","Draft",QT_TRANSLATE_NOOP("App::Property","The geometry this dimension is linked to")) - obj.addProperty("App::PropertyAngle","Angle","Draft",QT_TRANSLATE_NOOP("App::Property","The measurement of this dimension")) - obj.FirstAngle = 0 - obj.LastAngle = 90 - obj.Dimline = FreeCAD.Vector(0,1,0) - obj.Center = FreeCAD.Vector(0,0,0) - obj.Normal = FreeCAD.Vector(0,0,1) - - def onChanged(self,obj,prop): - if hasattr(obj,"Angle"): - obj.setEditorMode('Angle',1) - if hasattr(obj,"Normal"): - obj.setEditorMode('Normal',2) - if hasattr(obj,"Support"): - obj.setEditorMode('Support',2) - - def execute(self, fp): - if fp.ViewObject: - fp.ViewObject.update() - -class _ViewProviderAngularDimension(_ViewProviderDraft): - """A View Provider for the Draft Angular Dimension object""" - def __init__(self, obj): - obj.addProperty("App::PropertyLength","FontSize","Draft",QT_TRANSLATE_NOOP("App::Property","Font size")) - obj.addProperty("App::PropertyInteger","Decimals","Draft",QT_TRANSLATE_NOOP("App::Property","The number of decimals to show")) - obj.addProperty("App::PropertyFont","FontName","Draft",QT_TRANSLATE_NOOP("App::Property","Font name")) - obj.addProperty("App::PropertyLength","ArrowSize","Draft",QT_TRANSLATE_NOOP("App::Property","Arrow size")) - obj.addProperty("App::PropertyLength","TextSpacing","Draft",QT_TRANSLATE_NOOP("App::Property","The spacing between the text and the dimension line")) - obj.addProperty("App::PropertyEnumeration","ArrowType","Draft",QT_TRANSLATE_NOOP("App::Property","Arrow type")) - obj.addProperty("App::PropertyFloat","LineWidth","Draft",QT_TRANSLATE_NOOP("App::Property","Line width")) - obj.addProperty("App::PropertyColor","LineColor","Draft",QT_TRANSLATE_NOOP("App::Property","Line color")) - obj.addProperty("App::PropertyBool","FlipArrows","Draft",QT_TRANSLATE_NOOP("App::Property","Rotate the dimension arrows 180 degrees")) - obj.addProperty("App::PropertyBool","ShowUnit","Draft",QT_TRANSLATE_NOOP("App::Property","Show the unit suffix")) - obj.addProperty("App::PropertyVectorDistance","TextPosition","Draft",QT_TRANSLATE_NOOP("App::Property","The position of the text. Leave (0,0,0) for automatic position")) - obj.addProperty("App::PropertyString","Override","Draft",QT_TRANSLATE_NOOP("App::Property","Text override. Use 'dim' to insert the dimension length")) - obj.FontSize = getParam("textheight",0.20) - obj.FontName = getParam("textfont","") - obj.TextSpacing = getParam("dimspacing",0.05) - obj.ArrowSize = getParam("arrowsize",0.1) - obj.ArrowType = arrowtypes - obj.ArrowType = arrowtypes[getParam("dimsymbol",0)] - obj.Override = '' - obj.Decimals = getParam("dimPrecision",2) - obj.ShowUnit = getParam("showUnit",True) - _ViewProviderDraft.__init__(self,obj) - - def attach(self, vobj): - from pivy import coin - self.Object = vobj.Object - self.color = coin.SoBaseColor() - self.color.rgb.setValue(vobj.LineColor[0],vobj.LineColor[1],vobj.LineColor[2]) - self.font = coin.SoFont() - self.font3d = coin.SoFont() - self.text = coin.SoAsciiText() - self.text3d = coin.SoText2() - self.text.string = "d" # some versions of coin crash if string is not set - self.text3d.string = "d" - self.text.justification = self.text3d.justification = coin.SoAsciiText.CENTER - self.textpos = coin.SoTransform() - label = coin.SoSeparator() - label.addChild(self.textpos) - label.addChild(self.color) - label.addChild(self.font) - label.addChild(self.text) - label3d = coin.SoSeparator() - label3d.addChild(self.textpos) - label3d.addChild(self.color) - label3d.addChild(self.font3d) - label3d.addChild(self.text3d) - self.coord1 = coin.SoCoordinate3() - self.trans1 = coin.SoTransform() - self.coord2 = coin.SoCoordinate3() - self.trans2 = coin.SoTransform() - self.marks = coin.SoSeparator() - self.drawstyle = coin.SoDrawStyle() - self.coords = coin.SoCoordinate3() - self.arc = coin.SoType.fromName("SoBrepEdgeSet").createInstance() - self.node = coin.SoGroup() - self.node.addChild(self.color) - self.node.addChild(self.drawstyle) - self.node.addChild(self.coords) - self.node.addChild(self.arc) - self.node.addChild(self.marks) - self.node.addChild(label) - self.node3d = coin.SoGroup() - self.node3d.addChild(self.color) - self.node3d.addChild(self.drawstyle) - self.node3d.addChild(self.coords) - self.node3d.addChild(self.arc) - self.node3d.addChild(self.marks) - self.node3d.addChild(label3d) - vobj.addDisplayMode(self.node,"2D") - vobj.addDisplayMode(self.node3d,"3D") - self.updateData(vobj.Object,None) - self.onChanged(vobj,"FontSize") - self.onChanged(vobj,"FontName") - self.onChanged(vobj,"ArrowType") - self.onChanged(vobj,"LineColor") - - def updateData(self, obj, prop): - if hasattr(self,"arc"): - from pivy import coin - import Part, DraftGeomUtils - import DraftGui - text = None - ivob = None - arcsegs = 24 - - # calculate the arc data - if DraftVecUtils.isNull(obj.Normal): - norm = Vector(0,0,1) - else: - norm = obj.Normal - radius = (obj.Dimline.sub(obj.Center)).Length - self.circle = Part.makeCircle(radius,obj.Center,norm,obj.FirstAngle.Value,obj.LastAngle.Value) - self.p2 = self.circle.Vertexes[0].Point - self.p3 = self.circle.Vertexes[-1].Point - mp = DraftGeomUtils.findMidpoint(self.circle.Edges[0]) - ray = mp.sub(obj.Center) - - # set text value - if obj.LastAngle.Value > obj.FirstAngle.Value: - a = obj.LastAngle.Value - obj.FirstAngle.Value - else: - a = (360 - obj.FirstAngle.Value) + obj.LastAngle.Value - su = True - if hasattr(obj.ViewObject,"ShowUnit"): - su = obj.ViewObject.ShowUnit - if hasattr(obj.ViewObject,"Decimals"): - self.string = DraftGui.displayExternal(a,obj.ViewObject.Decimals,'Angle',su) - else: - self.string = DraftGui.displayExternal(a,None,'Angle',su) - if obj.ViewObject.Override: - self.string = obj.ViewObject.Override.replace("$dim",\ - self.string) - self.text.string = self.text3d.string = stringencodecoin(self.string) - - # check display mode - try: - m = obj.ViewObject.DisplayMode - except: # swallow all exceptions here since it always fails on first run (Displaymode enum no set yet) - m = ["2D","3D"][getParam("dimstyle",0)] - - # set the arc - if m == "3D": - # calculate the spacing of the text - spacing = (len(self.string)*obj.ViewObject.FontSize.Value)/8.0 - pts1 = [] - cut = None - pts2 = [] - for i in range(arcsegs+1): - p = self.circle.valueAt(self.circle.FirstParameter+((self.circle.LastParameter-self.circle.FirstParameter)/arcsegs)*i) - if (p.sub(mp)).Length <= spacing: - if cut is None: - cut = i - else: - if cut is None: - pts1.append([p.x,p.y,p.z]) - else: - pts2.append([p.x,p.y,p.z]) - self.coords.point.setValues(pts1+pts2) - i1 = len(pts1) - i2 = i1+len(pts2) - self.arc.coordIndex.setValues(0,len(pts1)+len(pts2)+1,list(range(len(pts1)))+[-1]+list(range(i1,i2))) - if (len(pts1) >= 3) and (len(pts2) >= 3): - self.circle1 = Part.Arc(Vector(pts1[0][0],pts1[0][1],pts1[0][2]),Vector(pts1[1][0],pts1[1][1],pts1[1][2]),Vector(pts1[-1][0],pts1[-1][1],pts1[-1][2])).toShape() - self.circle2 = Part.Arc(Vector(pts2[0][0],pts2[0][1],pts2[0][2]),Vector(pts2[1][0],pts2[1][1],pts2[1][2]),Vector(pts2[-1][0],pts2[-1][1],pts2[-1][2])).toShape() - else: - pts = [] - for i in range(arcsegs+1): - p = self.circle.valueAt(self.circle.FirstParameter+((self.circle.LastParameter-self.circle.FirstParameter)/arcsegs)*i) - pts.append([p.x,p.y,p.z]) - self.coords.point.setValues(pts) - self.arc.coordIndex.setValues(0,arcsegs+1,list(range(arcsegs+1))) - - # set the arrow coords and rotation - self.trans1.translation.setValue((self.p2.x,self.p2.y,self.p2.z)) - self.coord1.point.setValue((self.p2.x,self.p2.y,self.p2.z)) - self.trans2.translation.setValue((self.p3.x,self.p3.y,self.p3.z)) - self.coord2.point.setValue((self.p3.x,self.p3.y,self.p3.z)) - # calculate small chords to make arrows look better - arrowlength = 4*obj.ViewObject.ArrowSize.Value - u1 = (self.circle.valueAt(self.circle.FirstParameter+arrowlength)).sub(self.circle.valueAt(self.circle.FirstParameter)).normalize() - u2 = (self.circle.valueAt(self.circle.LastParameter)).sub(self.circle.valueAt(self.circle.LastParameter-arrowlength)).normalize() - if hasattr(obj.ViewObject,"FlipArrows"): - if obj.ViewObject.FlipArrows: - u1 = u1.negative() - u2 = u2.negative() - w2 = self.circle.Curve.Axis - w1 = w2.negative() - v1 = w1.cross(u1) - v2 = w2.cross(u2) - q1 = FreeCAD.Placement(DraftVecUtils.getPlaneRotation(u1,v1,w1)).Rotation.Q - q2 = FreeCAD.Placement(DraftVecUtils.getPlaneRotation(u2,v2,w2)).Rotation.Q - self.trans1.rotation.setValue((q1[0],q1[1],q1[2],q1[3])) - self.trans2.rotation.setValue((q2[0],q2[1],q2[2],q2[3])) - - # setting text pos & rot - self.tbase = mp - if hasattr(obj.ViewObject,"TextPosition"): - if not DraftVecUtils.isNull(obj.ViewObject.TextPosition): - self.tbase = obj.ViewObject.TextPosition - - u3 = ray.cross(norm).normalize() - v3 = norm.cross(u3) - r = FreeCAD.Placement(DraftVecUtils.getPlaneRotation(u3,v3,norm)).Rotation - offset = r.multVec(Vector(0,1,0)) - - if hasattr(obj.ViewObject,"TextSpacing"): - offset = DraftVecUtils.scaleTo(offset,obj.ViewObject.TextSpacing.Value) - else: - offset = DraftVecUtils.scaleTo(offset,0.05) - if m == "3D": - offset = offset.negative() - self.tbase = self.tbase.add(offset) - q = r.Q - self.textpos.translation.setValue([self.tbase.x,self.tbase.y,self.tbase.z]) - self.textpos.rotation = coin.SbRotation(q[0],q[1],q[2],q[3]) - - # set the angle property - if round(obj.Angle,precision()) != round(a,precision()): - obj.Angle = a - - def onChanged(self, vobj, prop): - if prop == "FontSize": - if hasattr(self,"font"): - self.font.size = vobj.FontSize.Value - if hasattr(self,"font3d"): - self.font3d.size = vobj.FontSize.Value*100 - vobj.Object.touch() - elif prop == "FontName": - if hasattr(self,"font") and hasattr(self,"font3d"): - self.font.name = self.font3d.name = str(vobj.FontName) - vobj.Object.touch() - elif prop == "LineColor": - if hasattr(self,"color"): - c = vobj.LineColor - self.color.rgb.setValue(c[0],c[1],c[2]) - elif prop == "LineWidth": - if hasattr(self,"drawstyle"): - self.drawstyle.lineWidth = vobj.LineWidth - elif prop in ["ArrowSize","ArrowType"]: - if hasattr(self,"node") and hasattr(self,"p2"): - from pivy import coin - - if not hasattr(vobj,"ArrowType"): - return - - # set scale - symbol = arrowtypes.index(vobj.ArrowType) - s = vobj.ArrowSize.Value - self.trans1.scaleFactor.setValue((s,s,s)) - self.trans2.scaleFactor.setValue((s,s,s)) - - # remove existing nodes - self.node.removeChild(self.marks) - self.node3d.removeChild(self.marks) - - # set new nodes - self.marks = coin.SoSeparator() - self.marks.addChild(self.color) - s1 = coin.SoSeparator() - if symbol == "Circle": - s1.addChild(self.coord1) - else: - s1.addChild(self.trans1) - s1.addChild(dimSymbol(symbol,invert=False)) - self.marks.addChild(s1) - s2 = coin.SoSeparator() - if symbol == "Circle": - s2.addChild(self.coord2) - else: - s2.addChild(self.trans2) - s2.addChild(dimSymbol(symbol,invert=True)) - self.marks.addChild(s2) - self.node.insertChild(self.marks,2) - self.node3d.insertChild(self.marks,2) - vobj.Object.touch() - else: - self.updateData(vobj.Object, None) - - def doubleClicked(self,vobj): - self.setEdit(vobj) - - def getDisplayModes(self,obj): - modes=[] - modes.extend(["2D","3D"]) - return modes - - def getDefaultDisplayMode(self): - if hasattr(self,"defaultmode"): - return self.defaultmode - else: - return ["2D","3D"][getParam("dimstyle",0)] - - def getIcon(self): - return ":/icons/Draft_DimensionAngular.svg" - - def __getstate__(self): - return self.Object.ViewObject.DisplayMode - - def __setstate__(self,state): - if state: - self.defaultmode = state - self.setDisplayMode(state) - class _Rectangle(_DraftObject): """The Rectangle object""" @@ -6246,482 +5334,4 @@ class ViewProviderWorkingPlaneProxy: return None -def makeLabel(targetpoint=None,target=None,direction=None,distance=None,labeltype=None,placement=None): - obj = FreeCAD.ActiveDocument.addObject("App::FeaturePython","dLabel") - DraftLabel(obj) - if FreeCAD.GuiUp: - ViewProviderDraftLabel(obj.ViewObject) - if targetpoint: - obj.TargetPoint = targetpoint - if target: - obj.Target = target - if direction: - obj.StraightDirection = direction - if distance: - obj.StraightDistance = distance - if labeltype: - obj.LabelType = labeltype - if placement: - obj.Placement = placement - - return obj - - -class DraftLabel: - """The Draft Label object""" - - def __init__(self,obj): - obj.Proxy = self - obj.addProperty("App::PropertyPlacement","Placement","Base",QT_TRANSLATE_NOOP("App::Property","The placement of this object")) - obj.addProperty("App::PropertyDistance","StraightDistance","Base",QT_TRANSLATE_NOOP("App::Property","The length of the straight segment")) - obj.addProperty("App::PropertyVector","TargetPoint","Base",QT_TRANSLATE_NOOP("App::Property","The point indicated by this label")) - obj.addProperty("App::PropertyVectorList","Points","Base",QT_TRANSLATE_NOOP("App::Property","The points defining the label polyline")) - obj.addProperty("App::PropertyEnumeration","StraightDirection","Base",QT_TRANSLATE_NOOP("App::Property","The direction of the straight segment")) - obj.addProperty("App::PropertyEnumeration","LabelType","Base",QT_TRANSLATE_NOOP("App::Property","The type of information shown by this label")) - obj.addProperty("App::PropertyLinkSub","Target","Base",QT_TRANSLATE_NOOP("App::Property","The target object of this label")) - obj.addProperty("App::PropertyStringList","CustomText","Base",QT_TRANSLATE_NOOP("App::Property","The text to display when type is set to custom")) - obj.addProperty("App::PropertyStringList","Text","Base",QT_TRANSLATE_NOOP("App::Property","The text displayed by this label")) - self.Type = "Label" - obj.StraightDirection = ["Horizontal","Vertical","Custom"] - obj.LabelType = ["Custom","Name","Label","Position","Length","Area","Volume","Tag","Material"] - obj.setEditorMode("Text",1) - obj.StraightDistance = 1 - obj.TargetPoint = Vector(2,-1,0) - obj.CustomText = "Label" - - def execute(self,obj): - if obj.StraightDirection != "Custom": - p1 = obj.Placement.Base - if obj.StraightDirection == "Horizontal": - p2 = Vector(obj.StraightDistance.Value,0,0) - else: - p2 = Vector(0,obj.StraightDistance.Value,0) - p2 = obj.Placement.multVec(p2) - # p3 = obj.Placement.multVec(obj.TargetPoint) - p3 = obj.TargetPoint - obj.Points = [p1,p2,p3] - if obj.LabelType == "Custom": - if obj.CustomText: - obj.Text = obj.CustomText - elif obj.Target and obj.Target[0]: - if obj.LabelType == "Name": - obj.Text = [obj.Target[0].Name] - elif obj.LabelType == "Label": - obj.Text = [obj.Target[0].Label] - elif obj.LabelType == "Tag": - if hasattr(obj.Target[0],"Tag"): - obj.Text = [obj.Target[0].Tag] - elif obj.LabelType == "Material": - if hasattr(obj.Target[0],"Material"): - if hasattr(obj.Target[0].Material,"Label"): - obj.Text = [obj.Target[0].Material.Label] - elif obj.LabelType == "Position": - p = obj.Target[0].Placement.Base - if obj.Target[1]: - if "Vertex" in obj.Target[1][0]: - p = obj.Target[0].Shape.Vertexes[int(obj.Target[1][0][6:])-1].Point - obj.Text = [FreeCAD.Units.Quantity(x,FreeCAD.Units.Length).UserString for x in tuple(p)] - elif obj.LabelType == "Length": - if hasattr(obj.Target[0],'Shape'): - if hasattr(obj.Target[0].Shape,"Length"): - obj.Text = [FreeCAD.Units.Quantity(obj.Target[0].Shape.Length,FreeCAD.Units.Length).UserString] - if obj.Target[1] and ("Edge" in obj.Target[1][0]): - obj.Text = [FreeCAD.Units.Quantity(obj.Target[0].Shape.Edges[int(obj.Target[1][0][4:])-1].Length,FreeCAD.Units.Length).UserString] - elif obj.LabelType == "Area": - if hasattr(obj.Target[0],'Shape'): - if hasattr(obj.Target[0].Shape,"Area"): - obj.Text = [FreeCAD.Units.Quantity(obj.Target[0].Shape.Area,FreeCAD.Units.Area).UserString.replace("^2","²")] - if obj.Target[1] and ("Face" in obj.Target[1][0]): - obj.Text = [FreeCAD.Units.Quantity(obj.Target[0].Shape.Faces[int(obj.Target[1][0][4:])-1].Area,FreeCAD.Units.Area).UserString] - elif obj.LabelType == "Volume": - if hasattr(obj.Target[0],'Shape'): - if hasattr(obj.Target[0].Shape,"Volume"): - obj.Text = [FreeCAD.Units.Quantity(obj.Target[0].Shape.Volume,FreeCAD.Units.Volume).UserString.replace("^3","³")] - - def onChanged(self,obj,prop): - pass - - def __getstate__(self): - return self.Type - - def __setstate__(self,state): - if state: - self.Type = state - - -class ViewProviderDraftLabel: - """A View Provider for the Draft Label""" - - def __init__(self,vobj): - vobj.addProperty("App::PropertyLength","TextSize","Base",QT_TRANSLATE_NOOP("App::Property","The size of the text")) - vobj.addProperty("App::PropertyFont","TextFont","Base",QT_TRANSLATE_NOOP("App::Property","The font of the text")) - vobj.addProperty("App::PropertyLength","ArrowSize","Base",QT_TRANSLATE_NOOP("App::Property","The size of the arrow")) - vobj.addProperty("App::PropertyEnumeration","TextAlignment","Base",QT_TRANSLATE_NOOP("App::Property","The vertical alignment of the text")) - vobj.addProperty("App::PropertyEnumeration","ArrowType","Base",QT_TRANSLATE_NOOP("App::Property","The type of arrow of this label")) - vobj.addProperty("App::PropertyEnumeration","Frame","Base",QT_TRANSLATE_NOOP("App::Property","The type of frame around the text of this object")) - vobj.addProperty("App::PropertyBool","Line","Base",QT_TRANSLATE_NOOP("App::Property","Display a leader line or not")) - vobj.addProperty("App::PropertyFloat","LineWidth","Base",QT_TRANSLATE_NOOP("App::Property","Line width")) - vobj.addProperty("App::PropertyColor","LineColor","Base",QT_TRANSLATE_NOOP("App::Property","Line color")) - vobj.addProperty("App::PropertyColor","TextColor","Base",QT_TRANSLATE_NOOP("App::Property","Text color")) - vobj.addProperty("App::PropertyInteger","MaxChars","Base",QT_TRANSLATE_NOOP("App::Property","The maximum number of characters on each line of the text box")) - vobj.Proxy = self - self.Object = vobj.Object - vobj.TextAlignment = ["Top","Middle","Bottom"] - vobj.TextAlignment = "Middle" - vobj.LineWidth = getParam("linewidth",1) - vobj.TextFont = getParam("textfont") - vobj.TextSize = getParam("textheight",1) - vobj.ArrowSize = getParam("arrowsize",1) - vobj.ArrowType = arrowtypes - vobj.ArrowType = arrowtypes[getParam("dimsymbol")] - vobj.Frame = ["None","Rectangle"] - vobj.Line = True - - def getIcon(self): - import Draft_rc - return ":/icons/Draft_Label.svg" - - def claimChildren(self): - return [] - - def attach(self,vobj): - from pivy import coin - self.arrow = coin.SoSeparator() - self.arrowpos = coin.SoTransform() - self.arrow.addChild(self.arrowpos) - self.matline = coin.SoMaterial() - self.drawstyle = coin.SoDrawStyle() - self.drawstyle.style = coin.SoDrawStyle.LINES - self.lcoords = coin.SoCoordinate3() - self.line = coin.SoType.fromName("SoBrepEdgeSet").createInstance() - self.mattext = coin.SoMaterial() - textdrawstyle = coin.SoDrawStyle() - textdrawstyle.style = coin.SoDrawStyle.FILLED - self.textpos = coin.SoTransform() - self.font = coin.SoFont() - self.text2d = coin.SoText2() - self.text3d = coin.SoAsciiText() - self.text2d.string = self.text3d.string = "Label" # need to init with something, otherwise, crash! - self.text2d.justification = coin.SoText2.RIGHT - self.text3d.justification = coin.SoAsciiText.RIGHT - self.fcoords = coin.SoCoordinate3() - self.frame = coin.SoType.fromName("SoBrepEdgeSet").createInstance() - self.lineswitch = coin.SoSwitch() - switchnode = coin.SoSeparator() - switchnode.addChild(self.line) - switchnode.addChild(self.arrow) - self.lineswitch.addChild(switchnode) - self.lineswitch.whichChild = 0 - self.node2d = coin.SoGroup() - self.node2d.addChild(self.matline) - self.node2d.addChild(self.arrow) - self.node2d.addChild(self.drawstyle) - self.node2d.addChild(self.lcoords) - self.node2d.addChild(self.lineswitch) - self.node2d.addChild(self.mattext) - self.node2d.addChild(textdrawstyle) - self.node2d.addChild(self.textpos) - self.node2d.addChild(self.font) - self.node2d.addChild(self.text2d) - self.node2d.addChild(self.fcoords) - self.node2d.addChild(self.frame) - self.node3d = coin.SoGroup() - self.node3d.addChild(self.matline) - self.node3d.addChild(self.arrow) - self.node3d.addChild(self.drawstyle) - self.node3d.addChild(self.lcoords) - self.node3d.addChild(self.lineswitch) - self.node3d.addChild(self.mattext) - self.node3d.addChild(textdrawstyle) - self.node3d.addChild(self.textpos) - self.node3d.addChild(self.font) - self.node3d.addChild(self.text3d) - self.node3d.addChild(self.fcoords) - self.node3d.addChild(self.frame) - vobj.addDisplayMode(self.node2d,"2D text") - vobj.addDisplayMode(self.node3d,"3D text") - self.onChanged(vobj,"LineColor") - self.onChanged(vobj,"TextColor") - self.onChanged(vobj,"ArrowSize") - self.onChanged(vobj,"Line") - - def getDisplayModes(self,vobj): - return ["2D text","3D text"] - - def getDefaultDisplayMode(self): - return "3D text" - - def setDisplayMode(self,mode): - return mode - - def updateData(self,obj,prop): - if prop == "Points": - from pivy import coin - if len(obj.Points) >= 2: - self.line.coordIndex.deleteValues(0) - self.lcoords.point.setValues(obj.Points) - self.line.coordIndex.setValues(0,len(obj.Points),range(len(obj.Points))) - self.onChanged(obj.ViewObject,"TextSize") - self.onChanged(obj.ViewObject,"ArrowType") - if obj.StraightDistance > 0: - self.text2d.justification = coin.SoText2.RIGHT - self.text3d.justification = coin.SoAsciiText.RIGHT - else: - self.text2d.justification = coin.SoText2.LEFT - self.text3d.justification = coin.SoAsciiText.LEFT - elif prop == "Text": - if obj.Text: - if sys.version_info.major >= 3: - self.text2d.string.setValues([l for l in obj.Text if l]) - self.text3d.string.setValues([l for l in obj.Text if l]) - else: - self.text2d.string.setValues([l.encode("utf8") for l in obj.Text if l]) - self.text3d.string.setValues([l.encode("utf8") for l in obj.Text if l]) - self.onChanged(obj.ViewObject,"TextAlignment") - - def getTextSize(self,vobj): - from pivy import coin - if vobj.DisplayMode == "3D text": - text = self.text3d - else: - text = self.text2d - v = FreeCADGui.ActiveDocument.ActiveView.getViewer().getSoRenderManager().getViewportRegion() - b = coin.SoGetBoundingBoxAction(v) - text.getBoundingBox(b) - return b.getBoundingBox().getSize().getValue() - - def onChanged(self,vobj,prop): - if prop == "LineColor": - if hasattr(vobj,"LineColor"): - l = vobj.LineColor - self.matline.diffuseColor.setValue([l[0],l[1],l[2]]) - elif prop == "TextColor": - if hasattr(vobj,"TextColor"): - l = vobj.TextColor - self.mattext.diffuseColor.setValue([l[0],l[1],l[2]]) - elif prop == "LineWidth": - if hasattr(vobj,"LineWidth"): - self.drawstyle.lineWidth = vobj.LineWidth - elif (prop == "TextFont"): - if hasattr(vobj,"TextFont"): - self.font.name = vobj.TextFont.encode("utf8") - elif prop in ["TextSize","TextAlignment"]: - if hasattr(vobj,"TextSize") and hasattr(vobj,"TextAlignment"): - self.font.size = vobj.TextSize.Value - v = Vector(1,0,0) - if vobj.Object.StraightDistance > 0: - v = v.negative() - v.multiply(vobj.TextSize/10) - tsize = self.getTextSize(vobj) - if len(vobj.Object.Text) > 1: - v = v.add(Vector(0,(tsize[1]-1)*2,0)) - if vobj.TextAlignment == "Top": - v = v.add(Vector(0,-tsize[1]*2,0)) - elif vobj.TextAlignment == "Middle": - v = v.add(Vector(0,-tsize[1],0)) - v = vobj.Object.Placement.Rotation.multVec(v) - pos = vobj.Object.Placement.Base.add(v) - self.textpos.translation.setValue(pos) - self.textpos.rotation.setValue(vobj.Object.Placement.Rotation.Q) - elif prop == "Line": - if hasattr(vobj,"Line"): - if vobj.Line: - self.lineswitch.whichChild = 0 - else: - self.lineswitch.whichChild = -1 - elif prop == "ArrowType": - if hasattr(vobj,"ArrowType"): - if len(vobj.Object.Points) > 1: - if hasattr(self,"symbol"): - if self.arrow.findChild(self.symbol) != -1: - self.arrow.removeChild(self.symbol) - s = arrowtypes.index(vobj.ArrowType) - self.symbol = dimSymbol(s) - self.arrow.addChild(self.symbol) - self.arrowpos.translation.setValue(vobj.Object.Points[-1]) - v1 = vobj.Object.Points[-2].sub(vobj.Object.Points[-1]) - if not DraftVecUtils.isNull(v1): - v1.normalize() - import DraftGeomUtils - v2 = Vector(0,0,1) - if round(v2.getAngle(v1),4) in [0,round(math.pi,4)]: - v2 = Vector(0,1,0) - v3 = v1.cross(v2).negative() - q = FreeCAD.Placement(DraftVecUtils.getPlaneRotation(v1,v3,v2)).Rotation.Q - self.arrowpos.rotation.setValue((q[0],q[1],q[2],q[3])) - elif prop == "ArrowSize": - if hasattr(vobj,"ArrowSize"): - s = vobj.ArrowSize.Value - if s: - self.arrowpos.scaleFactor.setValue((s,s,s)) - elif prop == "Frame": - if hasattr(vobj,"Frame"): - self.frame.coordIndex.deleteValues(0) - if vobj.Frame == "Rectangle": - tsize = self.getTextSize(vobj) - pts = [] - base = vobj.Object.Placement.Base.sub(Vector(self.textpos.translation.getValue().getValue())) - pts.append(base.add(Vector(0,tsize[1]*3,0))) - pts.append(pts[-1].add(Vector(-tsize[0]*6,0,0))) - pts.append(pts[-1].add(Vector(0,-tsize[1]*6,0))) - pts.append(pts[-1].add(Vector(tsize[0]*6,0,0))) - pts.append(pts[0]) - self.fcoords.point.setValues(pts) - self.frame.coordIndex.setValues(0,len(pts),range(len(pts))) - - def __getstate__(self): - return None - - def __setstate__(self,state): - return None - - -class DraftText: - """The Draft Text object""" - - def __init__(self,obj): - obj.Proxy = self - obj.addProperty("App::PropertyPlacement","Placement","Base",QT_TRANSLATE_NOOP("App::Property","The placement of this object")) - obj.addProperty("App::PropertyStringList","Text","Base",QT_TRANSLATE_NOOP("App::Property","The text displayed by this object")) - self.Type = "DraftText" - - def execute(self,obj): - pass - - -class ViewProviderDraftText: - """A View Provider for the Draft Label""" - - def __init__(self,vobj): - vobj.addProperty("App::PropertyLength","FontSize","Base",QT_TRANSLATE_NOOP("App::Property","The size of the text")) - vobj.addProperty("App::PropertyFont","FontName","Base",QT_TRANSLATE_NOOP("App::Property","The font of the text")) - vobj.addProperty("App::PropertyEnumeration","Justification","Base",QT_TRANSLATE_NOOP("App::Property","The vertical alignment of the text")) - vobj.addProperty("App::PropertyColor","TextColor","Base",QT_TRANSLATE_NOOP("App::Property","Text color")) - vobj.addProperty("App::PropertyFloat","LineSpacing","Base",QT_TRANSLATE_NOOP("App::Property","Line spacing (relative to font size)")) - vobj.Proxy = self - self.Object = vobj.Object - vobj.Justification = ["Left","Center","Right"] - vobj.FontName = getParam("textfont","sans") - vobj.FontSize = getParam("textheight",1) - - def getIcon(self): - import Draft_rc - return ":/icons/Draft_Text.svg" - - def claimChildren(self): - return [] - - def attach(self,vobj): - from pivy import coin - self.mattext = coin.SoMaterial() - textdrawstyle = coin.SoDrawStyle() - textdrawstyle.style = coin.SoDrawStyle.FILLED - self.trans = coin.SoTransform() - self.font = coin.SoFont() - self.text2d = coin.SoAsciiText() - self.text3d = coin.SoText2() - self.text2d.string = self.text3d.string = "Label" # need to init with something, otherwise, crash! - self.text2d.justification = coin.SoAsciiText.LEFT - self.text3d.justification = coin.SoText2.LEFT - self.node2d = coin.SoGroup() - self.node2d.addChild(self.trans) - self.node2d.addChild(self.mattext) - self.node2d.addChild(textdrawstyle) - self.node2d.addChild(self.font) - self.node2d.addChild(self.text2d) - self.node3d = coin.SoGroup() - self.node3d.addChild(self.trans) - self.node3d.addChild(self.mattext) - self.node3d.addChild(textdrawstyle) - self.node3d.addChild(self.font) - self.node3d.addChild(self.text3d) - vobj.addDisplayMode(self.node2d,"2D text") - vobj.addDisplayMode(self.node3d,"3D text") - self.onChanged(vobj,"TextColor") - self.onChanged(vobj,"FontSize") - self.onChanged(vobj,"FontName") - self.onChanged(vobj,"Justification") - self.onChanged(vobj,"LineSpacing") - - def getDisplayModes(self,vobj): - return ["2D text","3D text"] - - def setDisplayMode(self,mode): - return mode - - def updateData(self,obj,prop): - if prop == "Text": - if obj.Text: - if sys.version_info.major >= 3: - self.text2d.string.setValues([l for l in obj.Text if l]) - self.text3d.string.setValues([l for l in obj.Text if l]) - else: - self.text2d.string.setValues([l.encode("utf8") for l in obj.Text if l]) - self.text3d.string.setValues([l.encode("utf8") for l in obj.Text if l]) - elif prop == "Placement": - self.trans.translation.setValue(obj.Placement.Base) - self.trans.rotation.setValue(obj.Placement.Rotation.Q) - - def onChanged(self,vobj,prop): - if prop == "TextColor": - if "TextColor" in vobj.PropertiesList: - l = vobj.TextColor - self.mattext.diffuseColor.setValue([l[0],l[1],l[2]]) - elif (prop == "FontName"): - if "FontName" in vobj.PropertiesList: - self.font.name = vobj.FontName.encode("utf8") - elif prop == "FontSize": - if "FontSize" in vobj.PropertiesList: - self.font.size = vobj.FontSize.Value - elif prop == "Justification": - from pivy import coin - try: - if getattr(vobj, "Justification", None) is not None: - if vobj.Justification == "Left": - self.text2d.justification = coin.SoAsciiText.LEFT - self.text3d.justification = coin.SoText2.LEFT - elif vobj.Justification == "Right": - self.text2d.justification = coin.SoAsciiText.RIGHT - self.text3d.justification = coin.SoText2.RIGHT - else: - self.text2d.justification = coin.SoAsciiText.CENTER - self.text3d.justification = coin.SoText2.CENTER - except AssertionError: - pass # Race condition - Justification enum has not been set yet - elif prop == "LineSpacing": - if "LineSpacing" in vobj.PropertiesList: - self.text2d.spacing = vobj.LineSpacing - self.text3d.spacing = vobj.LineSpacing - - def __getstate__(self): - return None - - def __setstate__(self,state): - return None - - -def convertDraftTexts(textslist=[]): - """converts the given Draft texts (or all that are found in the active document) to the new object""" - if not isinstance(textslist,list): - textslist = [textslist] - if not textslist: - for o in FreeCAD.ActiveDocument.Objects: - if o.TypeId == "App::Annotation": - textslist.append(o) - todelete = [] - for o in textslist: - l = o.Label - o.Label = l+".old" - obj = makeText(o.LabelText,point=o.Position) - obj.Label = l - todelete.append(o.Name) - for p in o.InList: - if p.isDerivedFrom("App::DocumentObjectGroup"): - if o in p.Group: - g = p.Group - g.append(obj) - p.Group = g - for n in todelete: - FreeCAD.ActiveDocument.removeObject(n) - ## @} diff --git a/src/Mod/Draft/DraftGeomUtils.py b/src/Mod/Draft/DraftGeomUtils.py index cd6c9c1b68..eb13163993 100644 --- a/src/Mod/Draft/DraftGeomUtils.py +++ b/src/Mod/Draft/DraftGeomUtils.py @@ -305,7 +305,7 @@ def findIntersection(edge1, edge2, except: return [] norm3 = vec1.cross(vec2) - if not DraftVecUtils.isNull(norm3) : + if not DraftVecUtils.isNull(norm3) and (norm3.x+norm3.y+norm3.z != 0): k = ((pt3.z-pt1.z)*(vec2.x-vec2.y)+(pt3.y-pt1.y)*(vec2.z-vec2.x)+ \ (pt3.x-pt1.x)*(vec2.y-vec2.z))/(norm3.x+norm3.y+norm3.z) vec1.scale(k,k,k) @@ -1178,6 +1178,17 @@ def isReallyClosed(wire): if DraftVecUtils.equals(v1,v2): return True return False +def getSplineNormal(edge): + """Find the normal of a BSpline edge""" + startPoint = edge.valueAt(edge.FirstParameter) + endPoint = edge.valueAt(edge.LastParameter) + midParameter = edge.FirstParameter + (edge.LastParameter - edge.FirstParameter)/2 + midPoint = edge.valueAt(midParameter) + v1 = midPoint - startPoint + v2 = midPoint - endPoint + n = v1.cross(v2) + n.normalize() + return n def getNormal(shape): """Find the normal of a shape, if possible.""" @@ -1189,11 +1200,18 @@ def getNormal(shape): elif shape.ShapeType == "Edge": if geomType(shape.Edges[0]) in ["Circle","Ellipse"]: n = shape.Edges[0].Curve.Axis + elif geomType(edge) == "BSplineCurve" or \ + geomType(edge) == "BezierCurve": + n = getSplineNormal(edge) else: for e in shape.Edges: if geomType(e) in ["Circle","Ellipse"]: n = e.Curve.Axis break + elif geomType(e) == "BSplineCurve" or \ + geomType(e) == "BezierCurve": + n = getSplineNormal(e) + break e1 = vec(shape.Edges[0]) for i in range(1,len(shape.Edges)): e2 = vec(shape.Edges[i]) diff --git a/src/Mod/Draft/DraftGui.py b/src/Mod/Draft/DraftGui.py index 4c12910ac3..8076310f39 100644 --- a/src/Mod/Draft/DraftGui.py +++ b/src/Mod/Draft/DraftGui.py @@ -1893,8 +1893,7 @@ class DraftToolBar: return str(a) def togglesnap(self): - if hasattr(FreeCADGui,"Snapper"): - FreeCADGui.Snapper.toggle() + FreeCADGui.doCommand('FreeCADGui.runCommand("Draft_Snap_Lock")') def togglenearsnap(self): if hasattr(FreeCADGui,"Snapper"): diff --git a/src/Mod/Draft/DraftTools.py b/src/Mod/Draft/DraftTools.py index 569c8d0704..0b54833b5f 100644 --- a/src/Mod/Draft/DraftTools.py +++ b/src/Mod/Draft/DraftTools.py @@ -77,6 +77,22 @@ if not hasattr(FreeCAD, "DraftWorkingPlane"): import draftguitools.gui_edit import draftguitools.gui_selectplane import draftguitools.gui_planeproxy +from draftguitools.gui_lineops import FinishLine +from draftguitools.gui_lineops import CloseLine +from draftguitools.gui_lineops import UndoLine +from draftguitools.gui_togglemodes import ToggleConstructionMode +from draftguitools.gui_togglemodes import ToggleContinueMode +from draftguitools.gui_togglemodes import ToggleDisplayMode +from draftguitools.gui_groups import AddToGroup +from draftguitools.gui_groups import SelectGroup +from draftguitools.gui_groups import SetAutoGroup +from draftguitools.gui_groups import Draft_AddConstruction +from draftguitools.gui_grid import ToggleGrid +from draftguitools.gui_heal import Heal +from draftguitools.gui_dimension_ops import Draft_FlipDimension +from draftguitools.gui_lineslope import Draft_Slope +from draftguitools.gui_arcs import Draft_Arc_3Points +import draftguitools.gui_arrays # import DraftFillet import drafttaskpanels.task_shapestring as task_shapestring import drafttaskpanels.task_scale as task_scale @@ -980,66 +996,6 @@ class CubicBezCurve(Line): self.Activated() -class FinishLine: - """a FreeCAD command to finish any running Line drawing operation""" - - def Activated(self): - if (FreeCAD.activeDraftCommand != None): - if (FreeCAD.activeDraftCommand.featureName == "Line"): - FreeCAD.activeDraftCommand.finish(False) - - def GetResources(self): - return {'Pixmap' : 'Draft_Finish', - 'MenuText': QtCore.QT_TRANSLATE_NOOP("Draft_FinishLine", "Finish line"), - 'ToolTip': QtCore.QT_TRANSLATE_NOOP("Draft_FinishLine", "Finishes a line without closing it")} - - def IsActive(self): - if FreeCADGui.ActiveDocument: - return True - else: - return False - - -class CloseLine: - """a FreeCAD command to close any running Line drawing operation""" - - def Activated(self): - if (FreeCAD.activeDraftCommand != None): - if (FreeCAD.activeDraftCommand.featureName == "Line"): - FreeCAD.activeDraftCommand.finish(True) - - def GetResources(self): - return {'Pixmap' : 'Draft_Lock', - 'MenuText': QtCore.QT_TRANSLATE_NOOP("Draft_CloseLine", "Close Line"), - 'ToolTip': QtCore.QT_TRANSLATE_NOOP("Draft_CloseLine", "Closes the line being drawn")} - - def IsActive(self): - if FreeCADGui.ActiveDocument: - return True - else: - return False - - -class UndoLine: - """a FreeCAD command to undo last drawn segment of a line""" - - def Activated(self): - if (FreeCAD.activeDraftCommand != None): - if (FreeCAD.activeDraftCommand.featureName == "Line"): - FreeCAD.activeDraftCommand.undolast() - - def GetResources(self): - return {'Pixmap' : 'Draft_Rotate', - 'MenuText': QtCore.QT_TRANSLATE_NOOP("Draft_UndoLine", "Undo last segment"), - 'ToolTip': QtCore.QT_TRANSLATE_NOOP("Draft_UndoLine", "Undoes the last drawn segment of the line being drawn")} - - def IsActive(self): - if FreeCADGui.ActiveDocument: - return True - else: - return False - - class Rectangle(Creator): """the Draft_Rectangle FreeCAD command definition""" @@ -3067,6 +3023,8 @@ class Offset(Modifier): ['Draft.offset(FreeCAD.ActiveDocument.'+self.sel.Name+','+d+',copy='+str(copymode)+',occ='+str(occmode)+')', 'FreeCAD.ActiveDocument.recompute()']) self.finish() + else: + FreeCAD.Console.PrintError(translate("draft","Offset direction is not defined. Please move the mouse on either side of the object first to indicate a direction")+"/n") class Stretch(Modifier): @@ -4179,30 +4137,6 @@ class Scale(Modifier): for ghost in self.ghosts: ghost.finalize() -class ToggleConstructionMode(): - """The Draft_ToggleConstructionMode FreeCAD command definition""" - - def GetResources(self): - return {'Pixmap' : 'Draft_Construction', - 'MenuText': QtCore.QT_TRANSLATE_NOOP("Draft_ToggleConstructionMode", "Toggle Construction Mode"), - 'Accel' : "C, M", - 'ToolTip': QtCore.QT_TRANSLATE_NOOP("Draft_ToggleConstructionMode", "Toggles the Construction Mode for next objects.")} - - def Activated(self): - FreeCADGui.draftToolBar.constrButton.toggle() - - -class ToggleContinueMode(): - """The Draft_ToggleContinueMode FreeCAD command definition""" - - def GetResources(self): - return {'Pixmap' : 'Draft_Rotate', - 'MenuText': QtCore.QT_TRANSLATE_NOOP("Draft_ToggleContinueMode", "Toggle Continue Mode"), - 'ToolTip': QtCore.QT_TRANSLATE_NOOP("Draft_ToggleContinueMode", "Toggles the Continue Mode for next commands.")} - - def Activated(self): - FreeCADGui.draftToolBar.toggleContinue() - class Drawing(Modifier): """The Draft Drawing command definition""" @@ -4275,30 +4209,6 @@ class Drawing(Modifier): return page -class ToggleDisplayMode(): - """The ToggleDisplayMode FreeCAD command definition""" - - def GetResources(self): - return {'Pixmap' : 'Draft_SwitchMode', - 'Accel' : "Shift+Space", - 'MenuText': QtCore.QT_TRANSLATE_NOOP("Draft_ToggleDisplayMode", "Toggle display mode"), - 'ToolTip' : QtCore.QT_TRANSLATE_NOOP("Draft_ToggleDisplayMode", "Swaps display mode of selected objects between wireframe and flatlines")} - - def IsActive(self): - if FreeCADGui.Selection.getSelection(): - return True - else: - return False - - def Activated(self): - for obj in FreeCADGui.Selection.getSelection(): - if obj.ViewObject.DisplayMode == "Flat Lines": - if "Wireframe" in obj.ViewObject.listDisplayModes(): - obj.ViewObject.DisplayMode = "Wireframe" - elif obj.ViewObject.DisplayMode == "Wireframe": - if "Flat Lines" in obj.ViewObject.listDisplayModes(): - obj.ViewObject.DisplayMode = "Flat Lines" - class SubelementHighlight(Modifier): """The Draft_SubelementHighlight FreeCAD command definition""" @@ -4388,49 +4298,6 @@ class SubelementHighlight(Modifier): # This can occur if objects have had graph changing operations pass -class AddToGroup(): - """The AddToGroup FreeCAD command definition""" - - def GetResources(self): - return {'Pixmap' : 'Draft_AddToGroup', - 'MenuText': QtCore.QT_TRANSLATE_NOOP("Draft_AddToGroup", "Move to group..."), - 'ToolTip': QtCore.QT_TRANSLATE_NOOP("Draft_AddToGroup", "Moves the selected object(s) to an existing group")} - - def IsActive(self): - if FreeCADGui.Selection.getSelection(): - return True - else: - return False - - def Activated(self): - self.groups = ["Ungroup"] - self.groups.extend(Draft.getGroupNames()) - self.labels = ["Ungroup"] - for g in self.groups: - o = FreeCAD.ActiveDocument.getObject(g) - if o: self.labels.append(o.Label) - self.ui = FreeCADGui.draftToolBar - self.ui.sourceCmd = self - self.ui.popupMenu(self.labels) - - def proceed(self,labelname): - self.ui.sourceCmd = None - if labelname == "Ungroup": - for obj in FreeCADGui.Selection.getSelection(): - try: - Draft.ungroup(obj) - except: - pass - else: - if labelname in self.labels: - i = self.labels.index(labelname) - g = FreeCAD.ActiveDocument.getObject(self.groups[i]) - for obj in FreeCADGui.Selection.getSelection(): - try: - g.addObject(obj) - except: - pass - class WireToBSpline(Modifier): """The Draft_Wire2BSpline FreeCAD command definition""" @@ -4479,38 +4346,6 @@ class WireToBSpline(Modifier): self.finish() -class SelectGroup(): - """The SelectGroup FreeCAD command definition""" - - def GetResources(self): - return {'Pixmap' : 'Draft_SelectGroup', - 'MenuText': QtCore.QT_TRANSLATE_NOOP("Draft_SelectGroup", "Select group"), - 'ToolTip': QtCore.QT_TRANSLATE_NOOP("Draft_SelectGroup", "Selects all objects with the same parents as this group")} - - def IsActive(self): - if FreeCADGui.Selection.getSelection(): - return True - else: - return False - - def Activated(self): - sellist = [] - sel = FreeCADGui.Selection.getSelection() - if len(sel) == 1: - if sel[0].isDerivedFrom("App::DocumentObjectGroup"): - cts = Draft.getGroupContents(FreeCADGui.Selection.getSelection()) - for o in cts: - FreeCADGui.Selection.addSelection(o) - return - for ob in sel: - for child in ob.OutList: - FreeCADGui.Selection.addSelection(child) - for parent in ob.InList: - FreeCADGui.Selection.addSelection(parent) - for child in parent.OutList: - FreeCADGui.Selection.addSelection(child) - - class Shape2DView(Modifier): """The Shape2DView FreeCAD command definition""" @@ -4614,9 +4449,16 @@ class Draft2Sketch(Modifier): class Array(Modifier): - """The Shape2DView FreeCAD command definition""" + """GuiCommand for the Draft_Array tool. - def __init__(self,use_link=False): + Parameters + ---------- + use_link: bool, optional + It defaults to `False`. If it is `True`, the created object + will be a `Link array`. + """ + + def __init__(self, use_link=False): Modifier.__init__(self) self.use_link = use_link @@ -4647,11 +4489,12 @@ class Array(Modifier): 'FreeCAD.ActiveDocument.recompute()']) self.finish() + class LinkArray(Array): - "The Shape2DView FreeCAD command definition" + """GuiCommand for the Draft_LinkArray tool.""" def __init__(self): - Array.__init__(self,True) + Array.__init__(self, use_link=True) def GetResources(self): return {'Pixmap' : 'Draft_LinkArray', @@ -4820,17 +4663,6 @@ class Point(Creator): if self.ui.continueMode: self.Activated() -class ShowSnapBar(): - """The ShowSnapBar FreeCAD command definition""" - - def GetResources(self): - return {'MenuText': QtCore.QT_TRANSLATE_NOOP("Draft_ShowSnapBar", "Show Snap Bar"), - 'ToolTip' : QtCore.QT_TRANSLATE_NOOP("Draft_ShowSnapBar", "Shows Draft snap toolbar")} - - def Activated(self): - if hasattr(FreeCADGui,"Snapper"): - FreeCADGui.Snapper.show() - class Draft_Clone(Modifier): """The Draft Clone command definition""" @@ -4880,45 +4712,6 @@ class Draft_Clone(Modifier): ToDo.delay(FreeCADGui.runCommand, "Draft_Move") -class ToggleGrid(): - """The Draft ToggleGrid command definition""" - - def GetResources(self): - return {'Pixmap' : 'Draft_Grid', - 'Accel' : "G,R", - 'MenuText': QtCore.QT_TRANSLATE_NOOP("Draft_ToggleGrid", "Toggle Grid"), - 'ToolTip' : QtCore.QT_TRANSLATE_NOOP("Draft_ToggleGrid", "Toggles the Draft grid on/off"), - 'CmdType' : 'ForEdit'} - - def Activated(self): - if hasattr(FreeCADGui,"Snapper"): - FreeCADGui.Snapper.setTrackers() - if FreeCADGui.Snapper.grid: - if FreeCADGui.Snapper.grid.Visible: - FreeCADGui.Snapper.grid.off() - FreeCADGui.Snapper.forceGridOff=True - else: - FreeCADGui.Snapper.grid.on() - FreeCADGui.Snapper.forceGridOff=False - -class Heal(): - """The Draft Heal command definition""" - - def GetResources(self): - return {'Pixmap' : 'Draft_Heal', - 'MenuText': QtCore.QT_TRANSLATE_NOOP("Draft_Heal", "Heal"), - 'ToolTip' : QtCore.QT_TRANSLATE_NOOP("Draft_Heal", "Heal faulty Draft objects saved from an earlier FreeCAD version")} - - def Activated(self): - s = FreeCADGui.Selection.getSelection() - FreeCAD.ActiveDocument.openTransaction("Heal") - if s: - Draft.heal(s) - else: - Draft.heal() - FreeCAD.ActiveDocument.commitTransaction() - - class Draft_Facebinder(Creator): """The Draft Facebinder command definition""" @@ -4952,20 +4745,6 @@ class Draft_Facebinder(Creator): FreeCAD.ActiveDocument.recompute() self.finish() -class Draft_FlipDimension(): - def GetResources(self): - return {'Pixmap' : 'Draft_FlipDimension', - 'MenuText': QtCore.QT_TRANSLATE_NOOP("Draft_FlipDimension", "Flip Dimension"), - 'ToolTip' : QtCore.QT_TRANSLATE_NOOP("Draft_FlipDimension", "Flip the normal direction of a dimension")} - - def Activated(self): - for o in FreeCADGui.Selection.getSelection(): - if Draft.getType(o) in ["Dimension","AngularDimension"]: - FreeCAD.ActiveDocument.openTransaction("Flip dimension") - FreeCADGui.doCommand("FreeCAD.ActiveDocument."+o.Name+".Normal = FreeCAD.ActiveDocument."+o.Name+".Normal.negative()") - FreeCAD.ActiveDocument.commitTransaction() - FreeCAD.ActiveDocument.recompute() - class Mirror(Modifier): """The Draft_Mirror FreeCAD command definition""" @@ -5091,117 +4870,6 @@ class Mirror(Modifier): self.finish() -class Draft_Slope(): - - def GetResources(self): - return {'Pixmap' : 'Draft_Slope', - 'MenuText': QtCore.QT_TRANSLATE_NOOP("Draft_Slope", "Set Slope"), - 'ToolTip' : QtCore.QT_TRANSLATE_NOOP("Draft_Slope", "Sets the slope of a selected Line or Wire")} - - def Activated(self): - if not FreeCADGui.Selection.getSelection(): - return - for obj in FreeCADGui.Selection.getSelection(): - if Draft.getType(obj) != "Wire": - FreeCAD.Console.PrintMessage(translate("draft", "This tool only works with Wires and Lines")+"\n") - return - w = QtGui.QWidget() - w.setWindowTitle(translate("Draft","Slope")) - layout = QtGui.QHBoxLayout(w) - label = QtGui.QLabel(w) - label.setText(translate("Draft", "Slope")+":") - layout.addWidget(label) - self.spinbox = QtGui.QDoubleSpinBox(w) - self.spinbox.setMinimum(-9999.99) - self.spinbox.setMaximum(9999.99) - self.spinbox.setSingleStep(0.01) - self.spinbox.setToolTip(translate("Draft", "Slope to give selected Wires/Lines: 0 = horizontal, 1 = 45deg up, -1 = 45deg down")) - layout.addWidget(self.spinbox) - taskwidget = QtGui.QWidget() - taskwidget.form = w - taskwidget.accept = self.accept - FreeCADGui.Control.showDialog(taskwidget) - - def accept(self): - if hasattr(self,"spinbox"): - pc = self.spinbox.value() - FreeCAD.ActiveDocument.openTransaction("Change slope") - for obj in FreeCADGui.Selection.getSelection(): - if Draft.getType(obj) == "Wire": - if len(obj.Points) > 1: - lp = None - np = [] - for p in obj.Points: - if not lp: - lp = p - else: - v = p.sub(lp) - z = pc*FreeCAD.Vector(v.x,v.y,0).Length - lp = FreeCAD.Vector(p.x,p.y,lp.z+z) - np.append(lp) - obj.Points = np - FreeCAD.ActiveDocument.commitTransaction() - FreeCADGui.Control.closeDialog() - FreeCAD.ActiveDocument.recompute() - - -class SetAutoGroup(): - """The SetAutoGroup FreeCAD command definition""" - - def GetResources(self): - return {'Pixmap' : 'Draft_AutoGroup', - 'MenuText': QtCore.QT_TRANSLATE_NOOP("Draft_AutoGroup", "AutoGroup"), - 'ToolTip': QtCore.QT_TRANSLATE_NOOP("Draft_AutoGroup", "Select a group to automatically add all Draft & Arch objects to")} - - def IsActive(self): - if FreeCADGui.ActiveDocument: - return True - else: - return False - - def Activated(self): - if hasattr(FreeCADGui,"draftToolBar"): - self.ui = FreeCADGui.draftToolBar - s = FreeCADGui.Selection.getSelection() - if len(s) == 1: - if (Draft.getType(s[0]) == "Layer") or \ - ( FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/BIM").GetBool("AutogroupAddGroups",False) and \ - (s[0].isDerivedFrom("App::DocumentObjectGroup") or (Draft.getType(s[0]) in ["Site","Building","Floor","BuildingPart",]))): - self.ui.setAutoGroup(s[0].Name) - return - self.groups = ["None"] - gn = [o.Name for o in FreeCAD.ActiveDocument.Objects if Draft.getType(o) == "Layer"] - if FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/BIM").GetBool("AutogroupAddGroups",False): - gn.extend(Draft.getGroupNames()) - if gn: - self.groups.extend(gn) - self.labels = [translate("draft","None")] - self.icons = [self.ui.getIcon(":/icons/button_invalid.svg")] - for g in gn: - o = FreeCAD.ActiveDocument.getObject(g) - if o: - self.labels.append(o.Label) - self.icons.append(o.ViewObject.Icon) - self.labels.append(translate("draft","Add new Layer")) - self.icons.append(self.ui.getIcon(":/icons/document-new.svg")) - self.ui.sourceCmd = self - from PySide import QtCore - pos = self.ui.autoGroupButton.mapToGlobal(QtCore.QPoint(0,self.ui.autoGroupButton.geometry().height())) - self.ui.popupMenu(self.labels,self.icons,pos) - - def proceed(self,labelname): - self.ui.sourceCmd = None - if labelname in self.labels: - if labelname == self.labels[0]: - self.ui.setAutoGroup(None) - elif labelname == self.labels[-1]: - FreeCADGui.runCommand("Draft_Layer") - else: - i = self.labels.index(labelname) - self.ui.setAutoGroup(self.groups[i]) - - - class Draft_Label(Creator): """The Draft_Label command definition""" @@ -5272,6 +4940,7 @@ class Draft_Label(Creator): FreeCAD.ActiveDocument.openTransaction("Create Label") FreeCADGui.addModule("Draft") FreeCADGui.doCommand("l = Draft.makeLabel("+tp+sel+"direction='"+direction+"',distance="+str(dist)+",labeltype='"+self.labeltype+"',"+pl+")") + FreeCADGui.doCommand("Draft.autogroup(l)") FreeCAD.ActiveDocument.recompute() FreeCAD.ActiveDocument.commitTransaction() self.finish() @@ -5336,97 +5005,25 @@ class Draft_Label(Creator): self.create() -class Draft_AddConstruction(): - - def GetResources(self): - return {'Pixmap' : 'Draft_Construction', - 'MenuText': QtCore.QT_TRANSLATE_NOOP("Draft_AddConstruction", "Add to Construction group"), - 'ToolTip' : QtCore.QT_TRANSLATE_NOOP("Draft_AddConstruction", "Adds the selected objects to the Construction group")} - - def Activated(self): - import FreeCADGui - if hasattr(FreeCADGui,"draftToolBar"): - col = FreeCADGui.draftToolBar.getDefaultColor("constr") - col = (float(col[0]),float(col[1]),float(col[2]),0.0) - gname = Draft.getParam("constructiongroupname","Construction") - grp = FreeCAD.ActiveDocument.getObject(gname) - if not grp: - grp = FreeCAD.ActiveDocument.addObject("App::DocumentObjectGroup",gname) - for obj in FreeCADGui.Selection.getSelection(): - grp.addObject(obj) - obrep = obj.ViewObject - if "TextColor" in obrep.PropertiesList: - obrep.TextColor = col - if "PointColor" in obrep.PropertiesList: - obrep.PointColor = col - if "LineColor" in obrep.PropertiesList: - obrep.LineColor = col - if "ShapeColor" in obrep.PropertiesList: - obrep.ShapeColor = col - if hasattr(obrep,"Transparency"): - obrep.Transparency = 80 - - -class Draft_Arc_3Points: - - - def GetResources(self): - - return {'Pixmap' : "Draft_Arc_3Points.svg", - 'MenuText': QtCore.QT_TRANSLATE_NOOP("Draft_Arc_3Points", "Arc 3 points"), - 'ToolTip' : QtCore.QT_TRANSLATE_NOOP("Draft_Arc_3Points", "Creates an arc by 3 points"), - 'Accel' : 'A,T'} - - def IsActive(self): - - if FreeCAD.ActiveDocument: - return True - else: - return False - - def Activated(self): - - self.points = [] - self.normal = None - self.tracker = trackers.arcTracker() - self.tracker.autoinvert = False - if hasattr(FreeCAD,"DraftWorkingPlane"): - FreeCAD.DraftWorkingPlane.setup() - FreeCADGui.Snapper.getPoint(callback=self.getPoint,movecallback=self.drawArc) - - def getPoint(self,point,info): - if not point: # cancelled - self.tracker.off() - return - if not(point in self.points): # avoid same point twice - self.points.append(point) - if len(self.points) < 3: - if len(self.points) == 2: - self.tracker.on() - FreeCADGui.Snapper.getPoint(last=self.points[-1],callback=self.getPoint,movecallback=self.drawArc) - else: - import draftobjects.arc_3points as arc3 - if Draft.getParam("UsePartPrimitives",False): - arc3.make_arc_3points([self.points[0], - self.points[1], - self.points[2]], primitive=True) - else: - arc3.make_arc_3points([self.points[0], - self.points[1], - self.points[2]], primitive=False) - self.tracker.off() - FreeCAD.ActiveDocument.recompute() - - def drawArc(self,point,info): - if len(self.points) == 2: - if point.sub(self.points[1]).Length > 0.001: - self.tracker.setBy3Points(self.points[0],self.points[1],point) - - #--------------------------------------------------------------------------- # Snap tools #--------------------------------------------------------------------------- -import draftguitools.gui_snaps +from draftguitools.gui_snaps import Draft_Snap_Lock +from draftguitools.gui_snaps import Draft_Snap_Midpoint +from draftguitools.gui_snaps import Draft_Snap_Perpendicular +from draftguitools.gui_snaps import Draft_Snap_Grid +from draftguitools.gui_snaps import Draft_Snap_Intersection +from draftguitools.gui_snaps import Draft_Snap_Parallel +from draftguitools.gui_snaps import Draft_Snap_Endpoint +from draftguitools.gui_snaps import Draft_Snap_Angle +from draftguitools.gui_snaps import Draft_Snap_Center +from draftguitools.gui_snaps import Draft_Snap_Extension +from draftguitools.gui_snaps import Draft_Snap_Near +from draftguitools.gui_snaps import Draft_Snap_Ortho +from draftguitools.gui_snaps import Draft_Snap_Special +from draftguitools.gui_snaps import Draft_Snap_Dimensions +from draftguitools.gui_snaps import Draft_Snap_WorkingPlane +from draftguitools.gui_snaps import ShowSnapBar #--------------------------------------------------------------------------- # Adds the icons & commands to the FreeCAD command manager, and sets defaults @@ -5447,7 +5044,6 @@ class CommandArcGroup: def IsActive(self): return not FreeCAD.ActiveDocument is None FreeCADGui.addCommand('Draft_Arc',Arc()) -FreeCADGui.addCommand('Draft_Arc_3Points',Draft_Arc_3Points()) FreeCADGui.addCommand('Draft_ArcTools', CommandArcGroup()) FreeCADGui.addCommand('Draft_Text',Text()) FreeCADGui.addCommand('Draft_Rectangle',Rectangle()) @@ -5492,27 +5088,12 @@ FreeCADGui.addCommand('Draft_Clone',Draft_Clone()) FreeCADGui.addCommand('Draft_PathArray',PathArray()) FreeCADGui.addCommand('Draft_PathLinkArray',PathLinkArray()) FreeCADGui.addCommand('Draft_PointArray',PointArray()) -FreeCADGui.addCommand('Draft_Heal',Heal()) FreeCADGui.addCommand('Draft_Mirror',Mirror()) -FreeCADGui.addCommand('Draft_Slope',Draft_Slope()) FreeCADGui.addCommand('Draft_Stretch',Stretch()) # context commands -FreeCADGui.addCommand('Draft_FinishLine',FinishLine()) -FreeCADGui.addCommand('Draft_CloseLine',CloseLine()) -FreeCADGui.addCommand('Draft_UndoLine',UndoLine()) -FreeCADGui.addCommand('Draft_ToggleConstructionMode',ToggleConstructionMode()) -FreeCADGui.addCommand('Draft_ToggleContinueMode',ToggleContinueMode()) FreeCADGui.addCommand('Draft_ApplyStyle',ApplyStyle()) -FreeCADGui.addCommand('Draft_ToggleDisplayMode',ToggleDisplayMode()) -FreeCADGui.addCommand('Draft_AddToGroup',AddToGroup()) -FreeCADGui.addCommand('Draft_SelectGroup',SelectGroup()) FreeCADGui.addCommand('Draft_Shape2DView',Shape2DView()) -FreeCADGui.addCommand('Draft_ShowSnapBar',ShowSnapBar()) -FreeCADGui.addCommand('Draft_ToggleGrid',ToggleGrid()) -FreeCADGui.addCommand('Draft_FlipDimension',Draft_FlipDimension()) -FreeCADGui.addCommand('Draft_AutoGroup',SetAutoGroup()) -FreeCADGui.addCommand('Draft_AddConstruction',Draft_AddConstruction()) # a global place to look for active draft Command FreeCAD.activeDraftCommand = None diff --git a/src/Mod/Draft/InitGui.py b/src/Mod/Draft/InitGui.py index 1141e911dd..66330e4c3a 100644 --- a/src/Mod/Draft/InitGui.py +++ b/src/Mod/Draft/InitGui.py @@ -82,10 +82,6 @@ class DraftWorkbench(FreeCADGui.Workbench): import DraftTools import DraftGui import DraftFillet - from draftguitools import gui_circulararray - from draftguitools import gui_polararray - from draftguitools import gui_orthoarray - from draftguitools import gui_arrays FreeCADGui.addLanguagePath(":/translations") FreeCADGui.addIconPath(":/icons") except Exception as exc: @@ -102,11 +98,13 @@ class DraftWorkbench(FreeCADGui.Workbench): self.context_commands = it.get_draft_context_commands() self.line_commands = it.get_draft_line_commands() self.utility_commands = it.get_draft_utility_commands() + self.utility_small = it.get_draft_small_commands() # Set up toolbars self.appendToolbar(QT_TRANSLATE_NOOP("Draft", "Draft creation tools"), self.drawing_commands) self.appendToolbar(QT_TRANSLATE_NOOP("Draft", "Draft annotation tools"), self.annotation_commands) self.appendToolbar(QT_TRANSLATE_NOOP("Draft", "Draft modification tools"), self.modification_commands) + self.appendToolbar(QT_TRANSLATE_NOOP("Draft", "Draft utility tools"), self.utility_small) # Set up menus self.appendMenu(QT_TRANSLATE_NOOP("Draft", "&Drafting"), self.drawing_commands) @@ -118,6 +116,7 @@ class DraftWorkbench(FreeCADGui.Workbench): if hasattr(FreeCADGui, "draftToolBar"): if not hasattr(FreeCADGui.draftToolBar, "loadedPreferences"): FreeCADGui.addPreferencePage(":/ui/preferences-draft.ui", QT_TRANSLATE_NOOP("Draft", "Draft")) + FreeCADGui.addPreferencePage(":/ui/preferences-draftinterface.ui", QT_TRANSLATE_NOOP("Draft", "Draft")) FreeCADGui.addPreferencePage(":/ui/preferences-draftsnap.ui", QT_TRANSLATE_NOOP("Draft", "Draft")) FreeCADGui.addPreferencePage(":/ui/preferences-draftvisual.ui", QT_TRANSLATE_NOOP("Draft", "Draft")) FreeCADGui.addPreferencePage(":/ui/preferences-drafttexts.ui", QT_TRANSLATE_NOOP("Draft", "Draft")) @@ -131,6 +130,8 @@ class DraftWorkbench(FreeCADGui.Workbench): FreeCADGui.draftToolBar.Activated() if hasattr(FreeCADGui, "Snapper"): FreeCADGui.Snapper.show() + import draftutils.init_draft_statusbar as dsb + dsb.show_draft_statusbar() FreeCAD.Console.PrintLog("Draft workbench activated.\n") def Deactivated(self): @@ -139,6 +140,8 @@ class DraftWorkbench(FreeCADGui.Workbench): FreeCADGui.draftToolBar.Deactivated() if hasattr(FreeCADGui, "Snapper"): FreeCADGui.Snapper.hide() + import draftutils.init_draft_statusbar as dsb + dsb.hide_draft_statusbar() FreeCAD.Console.PrintLog("Draft workbench deactivated.\n") def ContextMenu(self, recipient): @@ -152,8 +155,15 @@ class DraftWorkbench(FreeCADGui.Workbench): else: self.appendContextMenu("Draft", self.drawing_commands) else: - if FreeCAD.activeDraftCommand.featureName == translate("draft","Line"): - # BUG: line subcommands are not usable while another command is active + if FreeCAD.activeDraftCommand.featureName in (translate("draft", "Line"), + translate("draft", "Wire"), + translate("draft", "Polyline"), + translate("draft", "BSpline"), + translate("draft", "BezCurve"), + translate("draft", "CubicBezCurve")): + # BUG: the line subcommands are in fact listed + # in the context menu, but they are de-activated + # so they don't work. self.appendContextMenu("", self.line_commands) else: if FreeCADGui.Selection.getSelection(): diff --git a/src/Mod/Draft/Resources/Draft.qrc b/src/Mod/Draft/Resources/Draft.qrc index fc9da4a2e6..c4096b88f3 100644 --- a/src/Mod/Draft/Resources/Draft.qrc +++ b/src/Mod/Draft/Resources/Draft.qrc @@ -1,8 +1,10 @@ icons/Draft_2DShapeView.svg + icons/Draft_AddConstruction.svg icons/Draft_AddPoint.svg icons/Draft_AddToGroup.svg + icons/Draft_Annotation_Style.svg icons/Draft_Apply.svg icons/Draft_Arc.svg icons/Draft_Arc_3Points.svg @@ -19,6 +21,7 @@ icons/Draft_CircularArray.svg icons/Draft_Clone.svg icons/Draft_Construction.svg + icons/Draft_Continue.svg icons/Draft_CubicBezCurve.svg icons/Draft_Cursor.svg icons/Draft_DelPoint.svg @@ -66,6 +69,7 @@ icons/Draft_SelectGroup.svg icons/Draft_SelectPlane.svg icons/Draft_ShapeString.svg + icons/Draft_ShapeString_tree.svg icons/Draft_Slope.svg icons/Draft_Snap.svg icons/Draft_Split.svg @@ -149,6 +153,7 @@ translations/Draft_zh-CN.qm translations/Draft_zh-TW.qm ui/preferences-draft.ui + ui/preferences-draftinterface.ui ui/preferences-draftsnap.ui ui/preferences-drafttexts.ui ui/preferences-draftvisual.ui @@ -161,5 +166,6 @@ ui/TaskPanel_PolarArray.ui ui/TaskSelectPlane.ui ui/TaskShapeString.ui + ui/dialog_AnnotationStyleEditor.ui diff --git a/src/Mod/Draft/Resources/icons/Draft_AddConstruction.svg b/src/Mod/Draft/Resources/icons/Draft_AddConstruction.svg new file mode 100644 index 0000000000..f1a2be3a75 --- /dev/null +++ b/src/Mod/Draft/Resources/icons/Draft_AddConstruction.svg @@ -0,0 +1,227 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + Mon Oct 10 13:44:52 2011 +0000 + + + [vocx] + + + + + FreeCAD LGPL2+ + + + + + FreeCAD + + + FreeCAD/src/Mod/Draft/Resources/icons/Draft_AddConstruction.svg + http://www.freecadweb.org/wiki/index.php?title=Artwork + + + [agryson] Alexander Gryson, [wmayer] + + + + + trowel + tool + plus sign + + + A trowel, and a plus sign + + + + + + + + + + diff --git a/src/Mod/Draft/Resources/icons/Draft_Annotation_Style.svg b/src/Mod/Draft/Resources/icons/Draft_Annotation_Style.svg new file mode 100644 index 0000000000..7e85ad57fd --- /dev/null +++ b/src/Mod/Draft/Resources/icons/Draft_Annotation_Style.svg @@ -0,0 +1,139 @@ + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + Mon Oct 10 13:44:52 2011 +0000 + + + [wmayer] + + + + + FreeCAD LGPL2+ + + + + + FreeCAD + + + FreeCAD/src/Mod/Draft/Resources/icons/Draft_Text.svg + http://www.freecadweb.org/wiki/index.php?title=Artwork + + + [agryson] Alexander Gryson + + + The capital letter A, slightly italicized + + + A + letter + + + + + + + + + + + + + + + + + + diff --git a/src/Mod/Draft/Resources/icons/Draft_Continue.svg b/src/Mod/Draft/Resources/icons/Draft_Continue.svg new file mode 100644 index 0000000000..519d134230 --- /dev/null +++ b/src/Mod/Draft/Resources/icons/Draft_Continue.svg @@ -0,0 +1,146 @@ + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + Mon Oct 10 13:44:52 2011 +0000 + + + [vocx] + + + + + FreeCAD LGPL2+ + + + + + FreeCAD + + + FreeCAD/src/Mod/Draft/Resources/icons/Draft_Continue.svg + http://www.freecadweb.org/wiki/index.php?title=Artwork + + + [agryson] Alexander Gryson, [wmayer] + + + + + arrow + double + right + + + A large blue arrow pointing to the right, and another one following it. + + + + + + + + + + diff --git a/src/Mod/Draft/Resources/icons/Draft_ShapeString_tree.svg b/src/Mod/Draft/Resources/icons/Draft_ShapeString_tree.svg new file mode 100644 index 0000000000..a71d1bb7bf --- /dev/null +++ b/src/Mod/Draft/Resources/icons/Draft_ShapeString_tree.svg @@ -0,0 +1,270 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +   + + + + + image/svg+xml + + + + Mon Apr 15 13:25:25 2013 -0400 + + + [vocx] + + + + + FreeCAD LGPL2+ + + + + + FreeCAD + + + FreeCAD/src/Mod/Draft/Resources/icons/Draft_ShapeString_tree.svg + http://www.freecadweb.org/wiki/index.php?title=Artwork + + + [agryson] Alexander Gryson, [wandererfan] + + + + + S + letter + + + A capital letter S, slightly italicized; color variation + + + + diff --git a/src/Mod/Draft/Resources/ui/TaskPanel_CircularArray.ui b/src/Mod/Draft/Resources/ui/TaskPanel_CircularArray.ui index ece65b12ee..b70551ce8b 100644 --- a/src/Mod/Draft/Resources/ui/TaskPanel_CircularArray.ui +++ b/src/Mod/Draft/Resources/ui/TaskPanel_CircularArray.ui @@ -7,7 +7,7 @@ 0 0 445 - 488 + 511 @@ -54,7 +54,8 @@ - The coordinates of the point through which the axis of rotation passes. + The coordinates of the point through which the axis of rotation passes. +Change the direction of the axis itself in the property editor. Center of rotation @@ -127,7 +128,7 @@ - Reset the coordinates of the center of rotation + Reset the coordinates of the center of rotation. Reset point @@ -142,7 +143,8 @@ - If checked, the resulting objects in the array will be fused if they touch each other + If checked, the resulting objects in the array will be fused if they touch each other. +This only works if "Link array" is off. Fuse @@ -152,10 +154,14 @@ - If checked, the resulting objects in the array will be Links instead of simple copies + If checked, the resulting object will be a "Link array" instead of a regular array. +A Link array is more efficient when creating multiple copies, but it cannot be fused together. - Use Links + Link array + + + true @@ -166,7 +172,8 @@ - Distance from one element in the array to the next element in the same layer. It cannot be zero. + Distance from one element in one ring of the array to the next element in the same ring. +It cannot be zero. Tangential distance @@ -176,7 +183,8 @@ - Distance from one element in the array to the next element in the same layer. It cannot be zero. + Distance from one element in one ring of the array to the next element in the same ring. +It cannot be zero. @@ -189,7 +197,7 @@ - Distance from the center of the array to the outer layers + Distance from one layer of objects to the next layer of objects. Radial distance @@ -199,7 +207,7 @@ - Distance from the center of the array to the outer layers + Distance from one layer of objects to the next layer of objects. @@ -212,7 +220,10 @@ - Number that controls how the objects will be distributed + The number of symmetry lines in the circular array. + + + 1 1 @@ -222,7 +233,11 @@ - Number of circular arrays to create, including a copy of the original object. It must be at least 2. + Number of circular layers or rings to create, including a copy of the original object. +It must be at least 2. + + + 2 3 @@ -232,7 +247,8 @@ - Number of circular arrays to create, including a copy of the original object. It must be at least 2. + Number of circular layers or rings to create, including a copy of the original object. +It must be at least 2. Number of circular layers @@ -242,7 +258,7 @@ - Number that controls how the objects will be distributed + The number of symmetry lines in the circular array. Symmetry diff --git a/src/Mod/Draft/Resources/ui/TaskPanel_OrthoArray.ui b/src/Mod/Draft/Resources/ui/TaskPanel_OrthoArray.ui index 52541fe2f9..d4e8889bcc 100644 --- a/src/Mod/Draft/Resources/ui/TaskPanel_OrthoArray.ui +++ b/src/Mod/Draft/Resources/ui/TaskPanel_OrthoArray.ui @@ -41,10 +41,12 @@ - Distance between the elements in the Z direction. Normally, only the Z value is necessary; the other two values can give an additional shift in their respective directions. + Distance between the elements in the Z direction. +Normally, only the Z value is necessary; the other two values can give an additional shift in their respective directions. +Negative values will result in copies produced in the negative direction. - Interval Z + Z intervals @@ -117,7 +119,7 @@ - Reset the distances + Reset the distances. Reset Z @@ -132,7 +134,8 @@ - If checked, the resulting objects in the array will be fused if they touch each other + If checked, the resulting objects in the array will be fused if they touch each other. +This only works if "Link array" is off. Fuse @@ -142,10 +145,14 @@ - If checked, the resulting objects in the array will be Links instead of simple copies + If checked, the resulting object will be a "Link array" instead of a regular array. +A Link array is more efficient when creating multiple copies, but it cannot be fused together. - Use Links + Link array + + + true @@ -164,73 +171,6 @@ - - - - Number of elements in the array in the specified direction, including a copy of the original object. The number must be at least 1 in each direction. - - - Number of elements - - - - - - - - X - - - - - - - Z - - - - - - - Y - - - - - - - 1 - - - 2 - - - - - - - 1 - - - 2 - - - - - - - 1 - - - 1 - - - - - - - - @@ -241,10 +181,12 @@ - Distance between the elements in the X direction. Normally, only the X value is necessary; the other two values can give an additional shift in their respective directions. + Distance between the elements in the X direction. +Normally, only the X value is necessary; the other two values can give an additional shift in their respective directions. +Negative values will result in copies produced in the negative direction. - Interval X + X intervals @@ -317,7 +259,7 @@ - Reset the distances + Reset the distances. Reset X @@ -330,10 +272,12 @@ - Distance between the elements in the Y direction. Normally, only the Y value is necessary; the other two values can give an additional shift in their respective directions. + Distance between the elements in the Y direction. +Normally, only the Y value is necessary; the other two values can give an additional shift in their respective directions. +Negative values will result in copies produced in the negative direction. - Interval Y + Y intervals @@ -406,7 +350,7 @@ - Reset the distances + Reset the distances. Reset Y @@ -416,6 +360,74 @@ + + + + Number of elements in the array in the specified direction, including a copy of the original object. +The number must be at least 1 in each direction. + + + Number of elements + + + + + + + + X + + + + + + + Z + + + + + + + Y + + + + + + + 1 + + + 2 + + + + + + + 1 + + + 2 + + + + + + + 1 + + + 1 + + + + + + + + @@ -429,10 +441,21 @@ + spinbox_n_X + spinbox_n_Y + spinbox_n_Z input_X_x input_X_y input_X_z button_reset_X + input_Y_x + input_Y_y + input_Y_z + button_reset_Y + input_Z_x + input_Z_y + input_Z_z + button_reset_Z checkbox_fuse checkbox_link diff --git a/src/Mod/Draft/Resources/ui/TaskPanel_PolarArray.ui b/src/Mod/Draft/Resources/ui/TaskPanel_PolarArray.ui index 5c85cf69f3..0994d58d79 100644 --- a/src/Mod/Draft/Resources/ui/TaskPanel_PolarArray.ui +++ b/src/Mod/Draft/Resources/ui/TaskPanel_PolarArray.ui @@ -54,7 +54,8 @@ - The coordinates of the point through which the axis of rotation passes. + The coordinates of the point through which the axis of rotation passes. +Change the direction of the axis itself in the property editor. Center of rotation @@ -127,7 +128,7 @@ - Reset the coordinates of the center of rotation + Reset the coordinates of the center of rotation. Reset point @@ -142,7 +143,8 @@ - If checked, the resulting objects in the array will be fused if they touch each other + If checked, the resulting objects in the array will be fused if they touch each other. +This only works if "Link array" is off. Fuse @@ -152,10 +154,14 @@ - If checked, the resulting objects in the array will be Links instead of simple copies + If checked, the resulting object will be a "Link array" instead of a regular array. +A Link array is more efficient when creating multiple copies, but it cannot be fused together. - Use Links + Link array + + + true @@ -166,7 +172,9 @@ - Sweeping angle of the polar distribution + Sweeping angle of the polar distribution. +A negative angle produces a polar pattern in the opposite direction. +The maximum absolute value is 360 degrees. Polar angle @@ -176,20 +184,29 @@ - Sweeping angle of the polar distribution + Sweeping angle of the polar distribution. +A negative angle produces a polar pattern in the opposite direction. +The maximum absolute value is 360 degrees. + + -360.000000000000000 + + + 360.000000000000000 + - 180.000000000000000 + 360.000000000000000 - Number of elements in the array, including a copy of the original object. It must be at least 2. + Number of elements in the array, including a copy of the original object. +It must be at least 2. Number of elements @@ -199,10 +216,14 @@ - Number of elements in the array, including a copy of the original object. It must be at least 2. + Number of elements in the array, including a copy of the original object. +It must be at least 2. + + + 2 - 4 + 5 diff --git a/src/Mod/Draft/Resources/ui/dialog_AnnotationStyleEditor.ui b/src/Mod/Draft/Resources/ui/dialog_AnnotationStyleEditor.ui new file mode 100644 index 0000000000..eddf18cecc --- /dev/null +++ b/src/Mod/Draft/Resources/ui/dialog_AnnotationStyleEditor.ui @@ -0,0 +1,447 @@ + + + Dialog + + + + 0 + 0 + 418 + 694 + + + + Dialog + + + + + + Style name + + + + + + The name of your style. Existing style names can be edited + + + false + + + + + + + + + Add new... + + + + + + + + false + + + + 80 + 16777215 + + + + Renames the selected style + + + Rename + + + + + + + false + + + + 80 + 16777215 + + + + Deletes the selected style + + + Delete + + + + + + + + + + Text + + + + + + Font size + + + + + + + Font name + + + + + + + Line spacing + + + + + + + The size of the text in real-world units + + + + + + + + + + The spacing between lines of text in real-world units + + + + + + + + + + The font to use for texts and dimensions + + + + + + + + + + Units + + + + + + Scale multiplier + + + + + + + Decimals + + + + + + + Únit override + + + + + + + Show unit + + + + + + + A multiplier value that affects distances shown by dimensions + + + 4 + + + 1.000000000000000 + + + + + + + Forces dimensions to be shown in a specific unit + + + + + + + The number of decimals to show on dimensions + + + + + + + Shows the units suffix on dimensions or not + + + Qt::RightToLeft + + + + + + + + + + + + + Line and arrows + + + + + + Line width + + + + + + + Extension overshoot + + + + + + + Arrow size + + + + + + + Show lines + + + + + + + Dimension overshoot + + + + + + + Extension lines + + + + + + + Arrow type + + + + + + + Line / text color + + + + + + + Shows the dimension line or not + + + Qt::RightToLeft + + + + + + true + + + + + + + The width of the dimension lines + + + px + + + 1 + + + + + + + The color of dimension lines, arrows and texts + + + + 0 + 0 + 0 + + + + + + + + The typeof arrows to use for dimensions + + + + Dot + + + + + Arrow + + + + + Tick + + + + + + + + The size of dimension arrows + + + + + + + + + + How far must the main dimension line extend pass the measured points + + + + + + + + + + The length of extension lines + + + + + + + + + + How far must the extension lines extend above the main dimension line + + + + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + Gui::QuantitySpinBox + QWidget +
Gui/QuantitySpinBox.h
+
+ + Gui::ColorButton + QPushButton +
Gui/Widgets.h
+
+
+ + + + buttonBox + accepted() + Dialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + Dialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + +
diff --git a/src/Mod/Draft/Resources/ui/preferences-draft.ui b/src/Mod/Draft/Resources/ui/preferences-draft.ui index 3a41695f63..005858b37e 100644 --- a/src/Mod/Draft/Resources/ui/preferences-draft.ui +++ b/src/Mod/Draft/Resources/ui/preferences-draft.ui @@ -6,8 +6,8 @@ 0 0 - 584 - 881 + 500 + 560
@@ -17,16 +17,7 @@ 6 - - 9 - - - 9 - - - 9 - - + 9 @@ -41,41 +32,6 @@ General Draft Settings - - - - If this is checked, copy mode will be kept across command, otherwise commands will always start in no-copy mode - - - Global copy mode - - - false - - - copymode - - - Mod/Draft - - - - - - - Normally, after copying objects, the copies get selected. If this option is checked, the base objects will be selected instead. - - - Select base objects after copying - - - selectBaseObjects - - - Mod/Draft - - - @@ -233,68 +189,149 @@ Values with differences below this value will be treated as same. This value wil - + - When this is checked, the Draft tools will create Part primitives instead of Draft objects, when available. + If this option is checked, the layers drop-down list will also show groups, allowing you to automatically add objects to groups too. - Use Part Primitives when available - - - UsePartPrimitives - - - Mod/Draft - - - - - - - If this is checked, objects will appear as filled by default. Otherwise, they will appear as wireframe - - - Fill objects with faces whenever possible + Show groups in layers list drop-down button - true + false - fillmode + AutogroupAddGroups Mod/Draft + +
+
+ + + + Draft tools options + + + + 9 + - - - When drawing lines, set focus on Length instead of X coordinate + + + 0 - - focusOnLength - - - Mod/Draft - - - - - - - If this option is set, when creating Draft objects on top of an existing face of another object, the "Support" property of the Draft object will be set to the base object. This was the standard behaviour before FreeCAD 0.19 - - - Set the Support property when possible - - - useSupport - - - Mod/Draft - - + + + + When drawing lines, set focus on Length instead of X coordinate. +This allows to point the direction and type the distance. + + + Set focus on Length instead of X coordinate + + + focusOnLength + + + Mod/Draft + + + + + + + If this option is set, when creating Draft objects on top of an existing face of another object, the "Support" property of the Draft object will be set to the base object. This was the standard behaviour before FreeCAD 0.19 + + + Set the Support property when possible + + + useSupport + + + Mod/Draft + + + + + + + If this is checked, objects will appear as filled by default. +Otherwise, they will appear as wireframe + + + Fill objects with faces whenever possible + + + true + + + fillmode + + + Mod/Draft + + + + + + + Normally, after copying objects, the copies get selected. +If this option is checked, the base objects will be selected instead. + + + Select base objects after copying + + + selectBaseObjects + + + Mod/Draft + + + + + + + If this is checked, copy mode will be kept across command, +otherwise commands will always start in no-copy mode + + + Global copy mode + + + false + + + copymode + + + Mod/Draft + + + + + + + Force Draft Tools to create Part primitives instead of Draft objects. +Note that this is not fully supported, and many object will be not editable with Draft Modifiers. + + + Use Part Primitives when available + + + UsePartPrimitives + + + Mod/Draft + + + + @@ -320,25 +357,6 @@ Values with differences below this value will be treated as same. This value wil
- - - - If this option is checked, the layers drop-down list will also show groups, allowing you to automatically add objects to groups too. - - - Show groups in layers list drop-down button - - - false - - - AutogroupAddGroups - - - Mod/Draft - - -
@@ -408,7 +426,7 @@ Values with differences below this value will be treated as same. This value wil This is the default color for objects being drawn while in construction mode. - + 44 125 @@ -428,965 +446,6 @@ Values with differences below this value will be treated as same. This value wil
- - - - In-Command Shortcuts - - - - - - - - - - Relative - - - - - - - true - - - - 0 - 0 - - - - - 25 - 16777215 - - - - R - - - 1 - - - - - - false - - - inCommandShortcutRelative - - - Mod/Draft - - - - - - - - - - - - - Continue - - - - - - - true - - - - 0 - 0 - - - - - 25 - 16777215 - - - - T - - - 1 - - - - - - false - - - inCommandShortcutContinue - - - Mod/Draft - - - - - - - - - - - - - Close - - - - - - - true - - - - 0 - 0 - - - - - 25 - 16777215 - - - - O - - - 1 - - - - - - false - - - inCommandShortcutClose - - - Mod/Draft - - - - - - - - - - - - - - - Copy - - - - - - - true - - - - 0 - 0 - - - - - 25 - 16777215 - - - - P - - - 1 - - - - - - false - - - inCommandShortcutCopy - - - Mod/Draft - - - - - - - - - - - Subelement Mode - - - - - - - true - - - - 0 - 0 - - - - - 25 - 16777215 - - - - D - - - 1 - - - - - - false - - - inCommandShortcutSubelementMode - - - Mod/Draft - - - - - - - - - - - Fill - - - - - - - true - - - - 0 - 0 - - - - - 25 - 16777215 - - - - L - - - 1 - - - - - - false - - - inCommandShortcutFill - - - Mod/Draft - - - - - - - - - - - - - - - Exit - - - - - - - true - - - - 0 - 0 - - - - - 25 - 16777215 - - - - A - - - 1 - - - - - - false - - - inCommandShortcutExit - - - Mod/Draft - - - - - - - - - - - Select Edge - - - - - - - true - - - - 0 - 0 - - - - - 25 - 16777215 - - - - E - - - 1 - - - - - - false - - - inCommandShortcutSelectEdge - - - Mod/Draft - - - - - - - - - - - Add Hold - - - - - - - true - - - - 0 - 0 - - - - - 25 - 16777215 - - - - Q - - - 1 - - - - - - false - - - inCommandShortcutAddHold - - - Mod/Draft - - - - - - - - - - - - - - - Length - - - - - - - true - - - - 0 - 0 - - - - - 25 - 16777215 - - - - H - - - 1 - - - - - - false - - - inCommandShortcutLength - - - Mod/Draft - - - - - - - - - - - Wipe - - - - - - - true - - - - 0 - 0 - - - - - 25 - 16777215 - - - - W - - - 1 - - - - - - false - - - inCommandShortcutWipe - - - Mod/Draft - - - - - - - - - - - Set WP - - - - - - - true - - - - 0 - 0 - - - - - 25 - 16777215 - - - - U - - - 1 - - - - - - false - - - inCommandShortcutSetWP - - - Mod/Draft - - - - - - - - - - - - - - - Cycle Snap - - - - - - - true - - - - 0 - 0 - - - - - 25 - 16777215 - - - - ` - - - 1 - - - - - - false - - - inCommandShortcutCycleSnap - - - Mod/Draft - - - - - - - - - - - - - - - - - - - - - Snap - - - - - - - true - - - - 0 - 0 - - - - - 25 - 16777215 - - - - S - - - 1 - - - - - - false - - - inCommandShortcutSnap - - - Mod/Draft - - - - - - - - - - - Increase Radius - - - - - - - true - - - - 0 - 0 - - - - - 25 - 16777215 - - - - [ - - - 1 - - - - - - false - - - inCommandShortcutIncreaseRadius - - - Mod/Draft - - - - - - - - - - - Decrease Radius - - - - - - - true - - - - 0 - 0 - - - - - 25 - 16777215 - - - - ] - - - 1 - - - - - - false - - - inCommandShortcutDecreaseRadius - - - Mod/Draft - - - - - - - - - - - - - - - Restrict X - - - - - - - true - - - - 0 - 0 - - - - - 25 - 16777215 - - - - X - - - 1 - - - - - - false - - - inCommandShortcutRestrictX - - - Mod/Draft - - - - - - - - - - - Restrict Y - - - - - - - true - - - - 0 - 0 - - - - - 25 - 16777215 - - - - Y - - - 1 - - - - - - false - - - inCommandShortcutRestrictY - - - Mod/Draft - - - - - - - - - - - Restrict Z - - - - - - - true - - - - 0 - 0 - - - - - 25 - 16777215 - - - - Z - - - 1 - - - - - - false - - - RestrictZ - - - Mod/Draft - - - - - - - - - - diff --git a/src/Mod/Draft/Resources/ui/preferences-draftinterface.ui b/src/Mod/Draft/Resources/ui/preferences-draftinterface.ui new file mode 100644 index 0000000000..7f231e4ce7 --- /dev/null +++ b/src/Mod/Draft/Resources/ui/preferences-draftinterface.ui @@ -0,0 +1,975 @@ + + + Gui::Dialog::DlgSettingsDraft + + + + 0 + 0 + 456 + 338 + + + + User interface settings + + + + 6 + + + 9 + + + + + In-Command Shortcuts + + + + + + 0 + + + + + true + + + + 0 + 0 + + + + + 25 + 16777215 + + + + ` + + + 1 + + + + + + false + + + inCommandShortcutCycleSnap + + + Mod/Draft + + + + + + + true + + + + 0 + 0 + + + + + 25 + 16777215 + + + + S + + + 1 + + + + + + false + + + inCommandShortcutSnap + + + Mod/Draft + + + + + + + Close + + + + + + + Relative + + + + + + + true + + + + 0 + 0 + + + + + 25 + 16777215 + + + + R + + + 1 + + + + + + false + + + inCommandShortcutRelative + + + Mod/Draft + + + + + + + true + + + + 0 + 0 + + + + + 25 + 16777215 + + + + O + + + 1 + + + + + + false + + + inCommandShortcutClose + + + Mod/Draft + + + + + + + true + + + + 0 + 0 + + + + + 25 + 16777215 + + + + T + + + 1 + + + + + + false + + + inCommandShortcutContinue + + + Mod/Draft + + + + + + + Continue + + + + + + + Copy + + + + + + + Increase Radius + + + + + + + Cycle Snap + + + + + + + true + + + + 0 + 0 + + + + + 25 + 16777215 + + + + [ + + + 1 + + + + + + false + + + inCommandShortcutIncreaseRadius + + + Mod/Draft + + + + + + + true + + + + 0 + 0 + + + + + 25 + 16777215 + + + + ] + + + 1 + + + + + + false + + + inCommandShortcutDecreaseRadius + + + Mod/Draft + + + + + + + Snap + + + + + + + Decrease Radius + + + + + + + Length + + + + + + + Wipe + + + + + + + true + + + + 0 + 0 + + + + + 25 + 16777215 + + + + D + + + 1 + + + + + + false + + + inCommandShortcutSubelementMode + + + Mod/Draft + + + + + + + Add Hold + + + + + + + true + + + + 0 + 0 + + + + + 25 + 16777215 + + + + L + + + 1 + + + + + + false + + + inCommandShortcutFill + + + Mod/Draft + + + + + + + Exit + + + + + + + true + + + + 0 + 0 + + + + + 25 + 16777215 + + + + P + + + 1 + + + + + + false + + + inCommandShortcutCopy + + + Mod/Draft + + + + + + + Fill + + + + + + + Subelement Mode + + + + + + + true + + + + 0 + 0 + + + + + 25 + 16777215 + + + + E + + + 1 + + + + + + false + + + inCommandShortcutSelectEdge + + + Mod/Draft + + + + + + + true + + + + 0 + 0 + + + + + 25 + 16777215 + + + + H + + + 1 + + + + + + false + + + inCommandShortcutLength + + + Mod/Draft + + + + + + + Select Edge + + + + + + + true + + + + 0 + 0 + + + + + 25 + 16777215 + + + + W + + + 1 + + + + + + false + + + inCommandShortcutWipe + + + Mod/Draft + + + + + + + true + + + + 0 + 0 + + + + + 25 + 16777215 + + + + A + + + 1 + + + + + + false + + + inCommandShortcutExit + + + Mod/Draft + + + + + + + true + + + + 0 + 0 + + + + + 25 + 16777215 + + + + Q + + + 1 + + + + + + false + + + inCommandShortcutAddHold + + + Mod/Draft + + + + + + + Set WP + + + + + + + true + + + + 0 + 0 + + + + + 25 + 16777215 + + + + U + + + 1 + + + + + + false + + + inCommandShortcutSetWP + + + Mod/Draft + + + + + + + Restrict X + + + + + + + true + + + + 0 + 0 + + + + + 25 + 16777215 + + + + X + + + 1 + + + + + + false + + + inCommandShortcutRestrictX + + + Mod/Draft + + + + + + + Restrict Y + + + + + + + true + + + + 0 + 0 + + + + + 25 + 16777215 + + + + Y + + + 1 + + + + + + false + + + inCommandShortcutRestrictY + + + Mod/Draft + + + + + + + Restrict Z + + + + + + + true + + + + 0 + 0 + + + + + 25 + 16777215 + + + + Z + + + 1 + + + + + + false + + + RestrictZ + + + Mod/Draft + + + + + + + + + + + + + 0 + 0 + + + + Enable draft statusbar customization + + + Draft Statusbar + + + true + + + DisplayStatusbar + + + Mod/Draft + + + + + + 0 + + + + + Enable snap statusbar widget + + + Draft snap widget + + + true + + + DisplayStatusbarSnapWidget + + + Mod/Draft + + + + + + + Enable draft statusbar annotation scale widget + + + Annotation scale widget + + + DisplayStatusbarScaleWidget + + + Mod/Draft + + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + qPixmapFromMimeSource + + + Gui::PrefCheckBox + QCheckBox +
Gui/PrefWidgets.h
+
+ + Gui::PrefLineEdit + QLineEdit +
Gui/PrefWidgets.h
+
+
+ + +
diff --git a/src/Mod/Draft/draftguitools/gui_annotationstyleeditor.py b/src/Mod/Draft/draftguitools/gui_annotationstyleeditor.py new file mode 100644 index 0000000000..08c1dd698b --- /dev/null +++ b/src/Mod/Draft/draftguitools/gui_annotationstyleeditor.py @@ -0,0 +1,252 @@ +# -*- coding: utf-8 -*- + +# *************************************************************************** +# * Copyright (c) 2020 Yorik van Havre * +# * * +# * This program is free software; you can redistribute it and/or modify * +# * it under the terms of the GNU Lesser General Public License (LGPL) * +# * as published by the Free Software Foundation; either version 2 of * +# * the License, or (at your option) any later version. * +# * for detail see the LICENCE text file. * +# * * +# * This program is distributed in the hope that it will be useful, * +# * but WITHOUT ANY WARRANTY; without even the implied warranty of * +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# * GNU Library General Public License for more details. * +# * * +# * You should have received a copy of the GNU Library General Public * +# * License along with this program; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# *************************************************************************** + +""" +Provides all gui and tools to create and edit annotation styles +Provides Draft_AnnotationStyleEditor command +""" + +import FreeCAD,FreeCADGui +import json + +EMPTYSTYLE = { + "FontName":"Sans", + "FontSize":0, + "LineSpacing":0, + "ScaleMultiplier":1, + "ShowUnit":False, + "UnitOverride":"", + "Decimals":0, + "ShowLines":True, + "LineWidth":1, + "LineColor":255, + "ArrowType":0, + "ArrowSize":0, + "DimensionOvershoot":0, + "ExtensionLines":0, + "ExtensionOvershoot":0, + } + + +class Draft_AnnotationStyleEditor: + + def __init__(self): + + self.styles = {} + + def GetResources(self): + + return {'Pixmap' : ":icons/Draft_AnnotationStyleEditor.svg", + 'MenuText': QT_TRANSLATE_NOOP("Draft_AnnotationStyleEditor", "Annotation styles..."), + 'ToolTip' : QT_TRANSLATE_NOOP("Draft_AnnotationStyleEditor", "Manage or create annotation styles")} + + def IsActive(self): + + return bool(FreeCAD.ActiveDocument) + + def Activated(self): + + from PySide import QtGui + + # load dialog + self.form = FreeCADGui.PySideUic.loadUi(":/ui/dialog_AnnotationStyleEditor.ui") + + # center the dialog over FreeCAD window + mw = FreeCADGui.getMainWindow() + self.form.move(mw.frameGeometry().topLeft() + mw.rect().center() - self.form.rect().center()) + + # set icons + self.form.pushButtonDelete.setIcon(QtGui.QIcon(":/icons/edit_Cancel.svg")) + self.form.pushButtonRename.setIcon(QtGui.QIcon(":/icons/edit_Cancel.svg")) + + # fill the styles combo + self.styles = self.read_meta() + for style in self.styles.keys(): + self.form.comboBoxStyles.addItem(style) + + # connect signals/slots + self.form.comboBoxStyles.currentIndexChanged.connect(self.on_style_changed) + self.form.pushButtonDelete.clicked.connect(self.on_delete) + self.form.pushButtonRename.clicked.connect(self.on_rename) + for attr in EMPTYSTYLE.keys(): + control = getattr(self.form,attr) + for signal in ["textChanged","valueChanged","stateChanged"]: + if hasattr(control,signal): + getattr(control,signal).connect(self.update_style) + break + + # show editor dialog + result = self.form.exec_() + + # process if OK was clicked + if result: + self.save_meta(self.styles) + + return + + def read_meta(self): + + """reads the document Meta property and returns a dict""" + + styles = {} + meta = FreeCAD.ActiveDocument.Meta + for key,value in meta.items(): + if key.startswith("Draft_Style_"): + styles[key[12:]] = json.loads(value) + return styles + + def save_meta(self,styles): + + """saves a dict to the document Meta property and updates objects""" + + # save meta + changedstyles = [] + meta = FreeCAD.ActiveDocument.Meta + for key,value in styles.items(): + strvalue = json.dumps(value) + if meta["Draft_Style_"+key] and (meta["Draft_Style_"+key] != strvalue): + changedstyles.append(style) + meta["Draft_Style_"+key] = strvalue + FreeCAD.ActiveDocument.Meta = meta + + # propagate changes to all annotations + for obj in self.get_annotations(): + if obj.ViewObject.AnnotationStyle in styles.keys(): + if obj.ViewObject.AnnotationStyle in changedstyles: + for attr,attrvalue in styles[obj.ViewObject.AnnotationStyle].items(): + if hasattr(obj.ViewObject,attr): + setattr(obj.ViewObject,attr,attrvalue) + else: + obj.ViewObject.AnnotationStyle = " " + obj.ViewObject.AnnotationStyle == [" "] + styles.keys() + + def on_style_changed(self,index): + + """called when the styles combobox is changed""" + + from PySide import QtGui + + if index <= 1: + # nothing happens + self.form.pushButtonDelete.setEnabled(False) + self.form.pushButtonRename.setEnabled(False) + self.fill_editor(None) + if index == 1: + # Add new... entry + reply = QtGui.QInputDialog.getText(None, "Create new style","Style name:") + if reply[1]: + # OK or Enter pressed + name = reply[0] + if name in self.styles: + reply = QtGui.QMessageBox.information(None,"Style exists","This style name already exists") + else: + # create new default style + self.styles[name] = EMPTYSTYLE + self.form.comboBoxStyles.addItem(name) + self.form.comboBoxStyles.setCurrentIndex(self.form.comboBoxStyles.count()-1) + elif index > 1: + # Existing style + self.form.pushButtonDelete.setEnabled(True) + self.form.pushButtonRename.setEnabled(True) + self.fill_editor(self.form.comboBoxStyles.itemText(index)) + + def on_delete(self): + + """called when the Delete button is pressed""" + + from PySide import QtGui + + index = self.form.comboBox.currentIndex() + style = self.form.comboBoxStyles.itemText(index) + if self.get_style_users(style): + reply = QtGui.QMessageBox.question(None, "Style in use", "This style is used by some objects in this document. Are you sure?", + QtGui.QMessageBox.Yes | QtGui.QMessageBox.No, QtGui.QMessageBox.No) + if reply == QtGui.QMessageBox.No: + return + self.form.comboBoxStyles.removeItem(index) + del self.styles[style] + + def on_rename(self): + + """called when the Rename button is pressed""" + + from PySide import QtGui + + index = self.form.comboBox.currentIndex() + style = self.form.comboBoxStyles.itemText(index) + reply = QtGui.QInputDialog.getText(None, "Rename style","New name:",QtGui.QLineEdit.Normal,style) + if reply[1]: + # OK or Enter pressed + newname = reply[0] + self.form.comboBoxStyles.setItemText(index,newname) + value = self.styles[style] + del self.styles[style] + self.styles[newname] = value + + def fill_editor(self,style): + + """fills the editor fields with the contents of a style""" + + if style is None: + style = EMPTYSTYLE + for key,value in style.items(): + setattr(self.form,key,value) + + def update_style(self,arg=None): + + """updates the current style with the values from the editor""" + + index = self.form.comboBox.currentIndex() + if index > 1: + values = {} + style = self.form.comboBoxStyles.itemText(index) + for key in EMPTYSTYLE.keys(): + control = getattr(self.form,key) + for attr in ["text","value","state"]: + if hasattr(control,attr): + values[key] = getattr(control,attr) + self.styles[style] = values + + def get_annotations(self): + + """gets all the objects that support annotation styles""" + + users = [] + for obj in FreeCAD.ActiveDocument.Objects: + vobj = obj.ViewObject + if hasattr(vobj,"AnnotationStyle"): + users.append(obj) + return users + + def get_style_users(self,style): + + """get all objects using a certain style""" + + users = [] + for obj in self.get_annotations(): + if obj.ViewObject.AnnotationStyle == style: + users.append(obj) + return users + + +FreeCADGui.addCommand('Draft_AnnotationStyleEditor', Draft_AnnotationStyleEditor()) diff --git a/src/Mod/Draft/draftguitools/gui_arcs.py b/src/Mod/Draft/draftguitools/gui_arcs.py new file mode 100644 index 0000000000..eed62a60fb --- /dev/null +++ b/src/Mod/Draft/draftguitools/gui_arcs.py @@ -0,0 +1,156 @@ +# *************************************************************************** +# * (c) 2009, 2010 Yorik van Havre * +# * (c) 2009, 2010 Ken Cline * +# * (c) 2020 Eliud Cabrera Castillo * +# * * +# * This file is part of the FreeCAD CAx development system. * +# * * +# * This program is free software; you can redistribute it and/or modify * +# * it under the terms of the GNU Lesser General Public License (LGPL) * +# * as published by the Free Software Foundation; either version 2 of * +# * the License, or (at your option) any later version. * +# * for detail see the LICENCE text file. * +# * * +# * FreeCAD is distributed in the hope that it will be useful, * +# * but WITHOUT ANY WARRANTY; without even the implied warranty of * +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# * GNU Library General Public License for more details. * +# * * +# * You should have received a copy of the GNU Library General Public * +# * License along with FreeCAD; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# *************************************************************************** +"""Provides tools for creating arcs with the Draft Workbench.""" +## @package gui_arcs +# \ingroup DRAFT +# \brief Provides tools for creating arcs with the Draft Workbench. +from PySide.QtCore import QT_TRANSLATE_NOOP + +import FreeCAD as App +import FreeCADGui as Gui +import Draft_rc +import draftobjects.arc_3points as arc3 +import draftguitools.gui_base as gui_base +import draftguitools.gui_trackers as trackers +import draftutils.utils as utils +from draftutils.translate import _tr + +# The module is used to prevent complaints from code checkers (flake8) +True if Draft_rc.__name__ else False + + +class Arc_3Points(gui_base.GuiCommandSimplest): + """GuiCommand for the Draft_Arc_3Points tool.""" + + def __init__(self): + super().__init__(name=_tr("Arc by 3 points")) + + def GetResources(self): + """Set icon, menu and tooltip.""" + _menu = "Arc by 3 points" + _tip = ("Creates a circular arc by picking 3 points.\n" + "CTRL to snap, SHIFT to constrain.") + + d = {'Pixmap': "Draft_Arc_3Points", + 'MenuText': QT_TRANSLATE_NOOP("Draft_Arc_3Points", _menu), + 'ToolTip': QT_TRANSLATE_NOOP("Draft_Arc_3Points", _tip), + 'Accel': 'A,T'} + return d + + def Activated(self): + """Execute when the command is called.""" + super().Activated() + + # Reset the values + self.points = [] + self.normal = None + self.tracker = trackers.arcTracker() + self.tracker.autoinvert = False + + # Set up the working plane and launch the Snapper + # with the indicated callbacks: one for when the user clicks + # on the 3D view, and another for when the user moves the pointer. + if hasattr(App, "DraftWorkingPlane"): + App.DraftWorkingPlane.setup() + + Gui.Snapper.getPoint(callback=self.getPoint, + movecallback=self.drawArc) + + def getPoint(self, point, info): + """Get the point by clicking on the 3D view. + + Every time the user clicks on the 3D view this method is run. + In this case, a point is appended to the list of points, + and the tracker is updated. + The object is finally created when three points are picked. + + Parameters + ---------- + point: Base::Vector + The point selected in the 3D view. + + info: str + Some information obtained about the point passed by the Snapper. + """ + # If there is not point, the command was cancelled + # so the command exits. + if not point: + self.tracker.off() + return + + # Avoid adding the same point twice + if point not in self.points: + self.points.append(point) + + if len(self.points) < 3: + # If one or two points were picked, set up again the Snapper + # to get further points, but update the `last` property + # with the last selected point. + # + # When two points are selected then we can turn on + # the arc tracker to show the preview of the final curve. + if len(self.points) == 2: + self.tracker.on() + Gui.Snapper.getPoint(last=self.points[-1], + callback=self.getPoint, + movecallback=self.drawArc) + else: + # If three points were already picked in the 3D view + # proceed with creating the final object. + # Draw a simple `Part::Feature` if the parameter is `True`. + if utils.get_param("UsePartPrimitives", False): + arc3.make_arc_3points([self.points[0], + self.points[1], + self.points[2]], primitive=True) + else: + arc3.make_arc_3points([self.points[0], + self.points[1], + self.points[2]], primitive=False) + self.tracker.off() + self.doc.recompute() + + def drawArc(self, point, info): + """Draw preview arc when we move the pointer in the 3D view. + + It uses the `gui_trackers.arcTracker.setBy3Points` method. + + Parameters + ---------- + point: Base::Vector + The dynamic point passed by the callback + as we move the pointer in the 3D view. + + info: str + Some information obtained from the point by the Snapper. + """ + if len(self.points) == 2: + if point.sub(self.points[1]).Length > 0.001: + self.tracker.setBy3Points(self.points[0], + self.points[1], + point) + + +Draft_Arc_3Points = Arc_3Points +Gui.addCommand('Draft_Arc_3Points', Arc_3Points()) diff --git a/src/Mod/Draft/draftguitools/gui_arrays.py b/src/Mod/Draft/draftguitools/gui_arrays.py index 81bff17fbb..33da02a8aa 100644 --- a/src/Mod/Draft/draftguitools/gui_arrays.py +++ b/src/Mod/Draft/draftguitools/gui_arrays.py @@ -28,6 +28,16 @@ from PySide.QtCore import QT_TRANSLATE_NOOP import FreeCAD as App import FreeCADGui as Gui +import Draft_rc +import draftguitools.gui_circulararray +import draftguitools.gui_polararray +import draftguitools.gui_orthoarray + +# The module is used to prevent complaints from code checkers (flake8) +True if Draft_rc.__name__ else False +True if draftguitools.gui_circulararray.__name__ else False +True if draftguitools.gui_polararray.__name__ else False +True if draftguitools.gui_orthoarray.__name__ else False class ArrayGroupCommand: @@ -35,22 +45,24 @@ class ArrayGroupCommand: def GetCommands(self): """Tuple of array commands.""" - return tuple(["Draft_OrthoArray", - "Draft_PolarArray", "Draft_CircularArray", - "Draft_PathArray", "Draft_PathLinkArray", - "Draft_PointArray"]) + return ("Draft_OrthoArray", + "Draft_PolarArray", "Draft_CircularArray", + "Draft_PathArray", "Draft_PathLinkArray", + "Draft_PointArray") def GetResources(self): - """Add menu and tooltip.""" + """Set icon, menu and tooltip.""" _tooltip = ("Create various types of arrays, " "including rectangular, polar, circular, " "path, and point") - return {'MenuText': QT_TRANSLATE_NOOP("Draft", "Array tools"), - 'ToolTip': QT_TRANSLATE_NOOP("Arch", _tooltip)} + + return {'Pixmap': 'Draft_Array', + 'MenuText': QT_TRANSLATE_NOOP("Draft", "Array tools"), + 'ToolTip': QT_TRANSLATE_NOOP("Draft", _tooltip)} def IsActive(self): """Return True when this command should be available.""" - if App.ActiveDocument: + if App.activeDocument(): return True else: return False diff --git a/src/Mod/Draft/draftguitools/gui_base.py b/src/Mod/Draft/draftguitools/gui_base.py index 1a149cc8cc..86cc109d88 100644 --- a/src/Mod/Draft/draftguitools/gui_base.py +++ b/src/Mod/Draft/draftguitools/gui_base.py @@ -30,9 +30,103 @@ import FreeCAD as App import FreeCADGui as Gui import draftutils.todo as todo +from draftutils.messages import _msg, _log -class GuiCommandBase: +class GuiCommandSimplest(object): + """Simplest base class for GuiCommands. + + This class only sets up the command name and the document object + to use for the command. + When it is executed, it logs the command name to the log file, + and prints the command name to the console. + + It implements the `IsActive` method, which must return `True` + when the command should be available. + It should return `True` when there is an active document, + otherwise the command (button or menu) should be disabled. + + This class is meant to be inherited by other GuiCommand classes + to quickly log the command name, and set the correct document object. + + Parameter + --------- + name: str, optional + It defaults to `'None'`. + The name of the action that is being run, + for example, `'Heal'`, `'Flip dimensions'`, + `'Line'`, `'Circle'`, etc. + + doc: App::Document, optional + It defaults to the value of `App.activeDocument()`. + The document object itself, which indicates where the actions + of the command will be executed. + + Attributes + ---------- + command_name: str + This is the command name, which is assigned by `name`. + + doc: App::Document + This is the document object itself, which is assigned by `doc`. + + This attribute should be used by functions to make sure + that the operations are performed in the correct document + and not in other documents. + To set the active document we can use + + >>> App.setActiveDocument(self.doc.Name) + """ + + def __init__(self, name="None", doc=App.activeDocument()): + self.command_name = name + self.doc = doc + + def IsActive(self): + """Return True when this command should be available. + + It is `True` when there is a document. + """ + if App.activeDocument(): + return True + else: + return False + + def Activated(self): + """Execute when the command is called. + + Log the command name to the log file and console. + Also update the `doc` attribute. + """ + self.doc = App.activeDocument() + _log("Document: {}".format(self.doc.Label)) + _log("GuiCommand: {}".format(self.command_name)) + _msg("{}".format(16*"-")) + _msg("GuiCommand: {}".format(self.command_name)) + + +class GuiCommandNeedsSelection(GuiCommandSimplest): + """Base class for GuiCommands that need a selection to be available. + + It re-implements the `IsActive` method to return `True` + when there is both an active document and an active selection. + + It inherits `GuiCommandSimplest` to set up the document + and other behavior. See this class for more information. + """ + + def IsActive(self): + """Return True when this command should be available. + + It is `True` when there is a selection. + """ + if App.activeDocument() and Gui.Selection.getSelection(): + return True + else: + return False + + +class GuiCommandBase(object): """Generic class that is the basis of all Gui commands. This class should eventually replace `DraftTools.DraftTool`, diff --git a/src/Mod/Draft/draftguitools/gui_circulararray.py b/src/Mod/Draft/draftguitools/gui_circulararray.py index 955230df14..cc229af358 100644 --- a/src/Mod/Draft/draftguitools/gui_circulararray.py +++ b/src/Mod/Draft/draftguitools/gui_circulararray.py @@ -20,7 +20,7 @@ # * USA * # * * # *************************************************************************** -"""Provides the Draft CircularArray tool.""" +"""Provides the Draft CircularArray GuiCommand.""" ## @package gui_circulararray # \ingroup DRAFT # \brief This module provides the Draft CircularArray tool. @@ -31,13 +31,15 @@ from PySide.QtCore import QT_TRANSLATE_NOOP import FreeCAD as App import FreeCADGui as Gui import Draft -import Draft_rc +import Draft_rc # include resources, icons, ui files +from draftutils.messages import _msg, _log +from draftutils.translate import _tr from draftguitools import gui_base from drafttaskpanels import task_circulararray import draftutils.todo as todo # The module is used to prevent complaints from code checkers (flake8) -True if Draft_rc.__name__ else False +bool(Draft_rc.__name__) class GuiCommandCircularArray(gui_base.GuiCommandBase): @@ -45,7 +47,7 @@ class GuiCommandCircularArray(gui_base.GuiCommandBase): def __init__(self): super().__init__() - self.command_name = "CircularArray" + self.command_name = "Circular array" self.location = None self.mouse_event = None self.view = None @@ -56,14 +58,15 @@ class GuiCommandCircularArray(gui_base.GuiCommandBase): def GetResources(self): """Set icon, menu and tooltip.""" - _msg = ("Creates copies of a selected object, " + _tip = ("Creates copies of a selected object, " "and places the copies in a circular pattern.\n" "The properties of the array can be further modified after " "the new object is created, including turning it into " "a different type of array.") + d = {'Pixmap': 'Draft_CircularArray', 'MenuText': QT_TRANSLATE_NOOP("Draft", "Circular array"), - 'ToolTip': QT_TRANSLATE_NOOP("Draft", _msg)} + 'ToolTip': QT_TRANSLATE_NOOP("Draft", _tip)} return d def Activated(self): @@ -72,6 +75,10 @@ class GuiCommandCircularArray(gui_base.GuiCommandBase): We add callbacks that connect the 3D view with the widgets of the task panel. """ + _log("GuiCommand: {}".format(_tr(self.command_name))) + _msg("{}".format(16*"-")) + _msg("GuiCommand: {}".format(_tr(self.command_name))) + self.location = coin.SoLocation2Event.getClassTypeId() self.mouse_event = coin.SoMouseButtonEvent.getClassTypeId() self.view = Draft.get3DView() diff --git a/src/Mod/Draft/draftguitools/gui_dimension_ops.py b/src/Mod/Draft/draftguitools/gui_dimension_ops.py new file mode 100644 index 0000000000..3f541e6daf --- /dev/null +++ b/src/Mod/Draft/draftguitools/gui_dimension_ops.py @@ -0,0 +1,82 @@ +# *************************************************************************** +# * (c) 2009, 2010 Yorik van Havre * +# * (c) 2009, 2010 Ken Cline * +# * (c) 2020 Eliud Cabrera Castillo * +# * * +# * This file is part of the FreeCAD CAx development system. * +# * * +# * This program is free software; you can redistribute it and/or modify * +# * it under the terms of the GNU Lesser General Public License (LGPL) * +# * as published by the Free Software Foundation; either version 2 of * +# * the License, or (at your option) any later version. * +# * for detail see the LICENCE text file. * +# * * +# * FreeCAD is distributed in the hope that it will be useful, * +# * but WITHOUT ANY WARRANTY; without even the implied warranty of * +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# * GNU Library General Public License for more details. * +# * * +# * You should have received a copy of the GNU Library General Public * +# * License along with FreeCAD; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# *************************************************************************** +"""Provides tools to modify Draft dimensions. + +For example, a tool to flip the direction of the text in the dimension +as the normal is sometimes not correctly calculated automatically. +""" +## @package gui_dimension_ops +# \ingroup DRAFT +# \brief Provides tools to modify Draft dimensions. + +from PySide.QtCore import QT_TRANSLATE_NOOP + +import FreeCADGui as Gui +import draftutils.utils as utils +import draftguitools.gui_base as gui_base +from draftutils.translate import _tr + + +class FlipDimension(gui_base.GuiCommandNeedsSelection): + """The Draft FlipDimension command definition. + + Flip the normal direction of the selected dimensions. + + It inherits `GuiCommandNeedsSelection` to set up the document + and other behavior. See this class for more information. + """ + + def __init__(self): + super().__init__(name=_tr("Flip dimension")) + + def GetResources(self): + """Set icon, menu and tooltip.""" + _tip = ("Flip the normal direction of the selected dimensions " + "(linear, radial, angular).\n" + "If other objects are selected they are ignored.") + + return {'Pixmap': 'Draft_FlipDimension', + 'MenuText': QT_TRANSLATE_NOOP("Draft_FlipDimension", + "Flip dimension"), + 'ToolTip': QT_TRANSLATE_NOOP("Draft_FlipDimension", + _tip)} + + def Activated(self): + """Execute when the command is called.""" + super().Activated() + + for o in Gui.Selection.getSelection(): + if utils.get_type(o) in ("Dimension", "AngularDimension"): + self.doc.openTransaction("Flip dimension") + _cmd = "App.activeDocument()." + o.Name + ".Normal" + _cmd += " = " + _cmd += "App.activeDocument()." + o.Name + ".Normal.negative()" + Gui.doCommand(_cmd) + self.doc.commitTransaction() + self.doc.recompute() + + +Draft_FlipDimension = FlipDimension +Gui.addCommand('Draft_FlipDimension', FlipDimension()) diff --git a/src/Mod/Draft/draftguitools/gui_grid.py b/src/Mod/Draft/draftguitools/gui_grid.py new file mode 100644 index 0000000000..d500a14e09 --- /dev/null +++ b/src/Mod/Draft/draftguitools/gui_grid.py @@ -0,0 +1,79 @@ +# *************************************************************************** +# * (c) 2009, 2010 Yorik van Havre * +# * (c) 2009, 2010 Ken Cline * +# * (c) 2020 Eliud Cabrera Castillo * +# * * +# * This file is part of the FreeCAD CAx development system. * +# * * +# * This program is free software; you can redistribute it and/or modify * +# * it under the terms of the GNU Lesser General Public License (LGPL) * +# * as published by the Free Software Foundation; either version 2 of * +# * the License, or (at your option) any later version. * +# * for detail see the LICENCE text file. * +# * * +# * FreeCAD is distributed in the hope that it will be useful, * +# * but WITHOUT ANY WARRANTY; without even the implied warranty of * +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# * GNU Library General Public License for more details. * +# * * +# * You should have received a copy of the GNU Library General Public * +# * License along with FreeCAD; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# *************************************************************************** +"""Provide the Draft_ToggleGrid command to show the Draft grid.""" +## @package gui_grid +# \ingroup DRAFT +# \brief Provide the Draft_ToggleGrid command to show the Draft grid. + +from PySide.QtCore import QT_TRANSLATE_NOOP + +import FreeCADGui as Gui +import draftguitools.gui_base as gui_base +from draftutils.translate import _tr + + +class ToggleGrid(gui_base.GuiCommandSimplest): + """The Draft ToggleGrid command definition. + + If the grid tracker is invisible (hidden), it makes it visible (shown); + and if it is visible, it hides it. + + It inherits `GuiCommandSimplest` to set up the document + and other behavior. See this class for more information. + """ + + def __init__(self): + super().__init__(name=_tr("Toggle grid")) + + def GetResources(self): + """Set icon, menu and tooltip.""" + _tip = "Toggles the Draft grid on and off." + + d = {'Pixmap': 'Draft_Grid', + 'Accel': "G,R", + 'MenuText': QT_TRANSLATE_NOOP("Draft_ToggleGrid", + "Toggle grid"), + 'ToolTip': QT_TRANSLATE_NOOP("Draft_ToggleGrid", + _tip), + 'CmdType': 'ForEdit'} + + return d + + def Activated(self): + """Execute when the command is called.""" + super().Activated() + + if hasattr(Gui, "Snapper"): + Gui.Snapper.setTrackers() + if Gui.Snapper.grid: + if Gui.Snapper.grid.Visible: + Gui.Snapper.grid.off() + Gui.Snapper.forceGridOff = True + else: + Gui.Snapper.grid.on() + Gui.Snapper.forceGridOff = False + + +Gui.addCommand('Draft_ToggleGrid', ToggleGrid()) diff --git a/src/Mod/Draft/draftguitools/gui_groups.py b/src/Mod/Draft/draftguitools/gui_groups.py new file mode 100644 index 0000000000..3aadd4e965 --- /dev/null +++ b/src/Mod/Draft/draftguitools/gui_groups.py @@ -0,0 +1,396 @@ +# *************************************************************************** +# * (c) 2009, 2010 Yorik van Havre * +# * (c) 2009, 2010 Ken Cline * +# * (c) 2020 Eliud Cabrera Castillo * +# * * +# * This file is part of the FreeCAD CAx development system. * +# * * +# * This program is free software; you can redistribute it and/or modify * +# * it under the terms of the GNU Lesser General Public License (LGPL) * +# * as published by the Free Software Foundation; either version 2 of * +# * the License, or (at your option) any later version. * +# * for detail see the LICENCE text file. * +# * * +# * FreeCAD is distributed in the hope that it will be useful, * +# * but WITHOUT ANY WARRANTY; without even the implied warranty of * +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# * GNU Library General Public License for more details. * +# * * +# * You should have received a copy of the GNU Library General Public * +# * License along with FreeCAD; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# *************************************************************************** +"""Provides tools to do various operations with groups. + +For example, add objects to groups, select objects inside groups, +set the automatic group in which to create objects, and add objects +to the construction group. +""" +## @package gui_groups +# \ingroup DRAFT +# \brief Provides tools to do various operations with groups. + +import PySide.QtCore as QtCore +from PySide.QtCore import QT_TRANSLATE_NOOP + +import FreeCAD as App +import FreeCADGui as Gui +import Draft_rc +import draftutils.utils as utils +import draftguitools.gui_base as gui_base +from draftutils.translate import _tr, translate + +# The module is used to prevent complaints from code checkers (flake8) +True if Draft_rc.__name__ else False + + +class AddToGroup(gui_base.GuiCommandNeedsSelection): + """GuiCommand for the Draft_AddToGroup tool. + + It adds selected objects to a group, or removes them from any group. + + It inherits `GuiCommandNeedsSelection` to only be available + when there is a document and a selection. + See this class for more information. + """ + + def __init__(self): + super().__init__(name=_tr("Add to group")) + self.ungroup = QT_TRANSLATE_NOOP("Draft_AddToGroup", + "Ungroup") + + def GetResources(self): + """Set icon, menu and tooltip.""" + _tooltip = ("Moves the selected objects to an existing group, " + "or removes them from any group.\n" + "Create a group first to use this tool.") + + d = {'Pixmap': 'Draft_AddToGroup', + 'MenuText': QT_TRANSLATE_NOOP("Draft_AddToGroup", + "Move to group"), + 'ToolTip': QT_TRANSLATE_NOOP("Draft_AddToGroup", + _tooltip)} + return d + + def Activated(self): + """Execute when the command is called.""" + super().Activated() + + self.groups = [self.ungroup] + self.groups.extend(utils.get_group_names()) + + self.labels = [self.ungroup] + for group in self.groups: + obj = self.doc.getObject(group) + if obj: + self.labels.append(obj.Label) + + # It uses the `DraftToolBar` class defined in the `DraftGui` module + # and globally initialized in the `Gui` namespace, + # in order to pop up a menu with group labels + # or the default `Ungroup` text. + # Once the desired option is chosen + # it launches the `proceed` method. + self.ui = Gui.draftToolBar + self.ui.sourceCmd = self + self.ui.popupMenu(self.labels) + + def proceed(self, labelname): + """Place the selected objects in the chosen group or ungroup them. + + Parameters + ---------- + labelname: str + The passed string with the name of the group. + It puts the selected objects inside this group. + """ + # Deactivate the source command of the `DraftToolBar` class + # so that it doesn't do more with this command. + self.ui.sourceCmd = None + + # If the selected group matches the ungroup label, + # remove the selection from all groups. + if labelname == self.ungroup: + for obj in Gui.Selection.getSelection(): + try: + utils.ungroup(obj) + except Exception: + pass + else: + # Otherwise try to add all selected objects to the chosen group + if labelname in self.labels: + i = self.labels.index(labelname) + g = self.doc.getObject(self.groups[i]) + for obj in Gui.Selection.getSelection(): + try: + g.addObject(obj) + except Exception: + pass + + +Gui.addCommand('Draft_AddToGroup', AddToGroup()) + + +class SelectGroup(gui_base.GuiCommandNeedsSelection): + """GuiCommand for the Draft_SelectGroup tool. + + If the selection is a group, it selects all objects + with the same "parents" as this object. This means all objects + that are inside this group, including those in nested sub-groups. + + If the selection is a simple object inside a group, + it will select the "brother" objects, that is, those objects that are + at the same level as this object, including the upper group + that contains them all. + + NOTE: the second functionality is a bit strange, as it produces results + that are not very intuitive. Maybe we should change it and restrict + this command to only groups (`App::DocumentObjectGroup`) because + in this case it works in an intuitive manner, selecting + only the objects under the group. + + It inherits `GuiCommandNeedsSelection` to only be available + when there is a document and a selection. + See this class for more information. + """ + + def __init__(self): + super().__init__(name=_tr("Select group")) + + def GetResources(self): + """Set icon, menu and tooltip.""" + _tooltip = ("If the selection is a group, it selects all objects " + "that are inside this group, including those in " + "nested sub-groups.\n" + "\n" + "If the selection is a simple object inside a group, " + 'it will select the "brother" objects, that is,\n' + "those that are at the same level as this object, " + "including the upper group that contains them all.") + + d = {'Pixmap': 'Draft_SelectGroup', + 'MenuText': QT_TRANSLATE_NOOP("Draft_SelectGroup", + "Select group"), + 'ToolTip': QT_TRANSLATE_NOOP("Draft_SelectGroup", + _tooltip)} + return d + + def Activated(self): + """Execute when the command is called. + + If the selection is a single group, it selects all objects + inside this group. + + In other cases it selects all objects (children) + in the OutList of this object, and also all objects (parents) + in the InList of this object. + For all parents, it also selects the children of these. + """ + super().Activated() + + sel = Gui.Selection.getSelection() + if len(sel) == 1: + if sel[0].isDerivedFrom("App::DocumentObjectGroup"): + cts = utils.get_group_contents(Gui.Selection.getSelection()) + for o in cts: + Gui.Selection.addSelection(o) + return + for obj in sel: + # This selects the objects in the `OutList` + # which are actually `parents` but appear below in the tree. + # Regular objects usually have an empty `OutList` + # so this is skipped. + # But for groups, it selects the objects + # that it contains under it. + for child in obj.OutList: + Gui.Selection.addSelection(child) + + # This selects the upper group that contains `obj`. + # Then for this group, it selects the objects in its `OutList`, + # which are at the same level as `obj` (brothers). + for parent in obj.InList: + Gui.Selection.addSelection(parent) + for child in parent.OutList: + Gui.Selection.addSelection(child) + # ------------------------------------------------------------------- + # NOTE: the terminology here may be confusing. + # Those in the `InList` are actually `children` (dependents) + # but appear above in the tree view, + # and this is the reason they are called `parents`. + # + # Those in the `OutList` are actually `parents` (suppliers) + # but appear below in the tree, and this is the reason + # they are called `children`. + # + # InList + # | + # - object + # | + # - OutList + # + # ------------------------------------------------------------------- + + +Gui.addCommand('Draft_SelectGroup', SelectGroup()) + + +class SetAutoGroup(gui_base.GuiCommandSimplest): + """GuiCommand for the Draft_AutoGroup tool.""" + + def __init__(self): + super().__init__(name=_tr("Autogroup")) + + def GetResources(self): + """Set icon, menu and tooltip.""" + _tip = "Select a group to add all Draft and Arch objects to." + + return {'Pixmap': 'Draft_AutoGroup', + 'MenuText': QT_TRANSLATE_NOOP("Draft_AutoGroup", "Autogroup"), + 'ToolTip': QT_TRANSLATE_NOOP("Draft_AutoGroup", _tip)} + + def Activated(self): + """Execute when the command is called. + + It calls the `setAutogroup` method of the `DraftToolBar` class + installed inside the global `Gui` namespace. + """ + super().Activated() + + if not hasattr(Gui, "draftToolBar"): + return + + # It uses the `DraftToolBar` class defined in the `DraftGui` module + # and globally initialized in the `Gui` namespace to run + # some actions. + # If there is only a group selected, it runs the `AutoGroup` method. + self.ui = Gui.draftToolBar + s = Gui.Selection.getSelection() + if len(s) == 1: + if (utils.get_type(s[0]) == "Layer") or \ +- (App.ParamGet("User parameter:BaseApp/Preferences/Mod/BIM").GetBool("AutogroupAddGroups", False) + and (s[0].isDerivedFrom("App::DocumentObjectGroup") + or utils.get_type(s[0]) in ["Site", "Building", + "Floor", "BuildingPart"])): + self.ui.setAutoGroup(s[0].Name) + return + + # Otherwise it builds a list of layers, with names and icons, + # including the options "None" and "Add new layer". + self.groups = ["None"] + gn = [o.Name for o in self.doc.Objects if utils.get_type(o) == "Layer"] + if App.ParamGet("User parameter:BaseApp/Preferences/Mod/BIM").GetBool("AutogroupAddGroups", False): + gn.extend(utils.get_group_names()) + if gn: + self.groups.extend(gn) + self.labels = [translate("draft", "None")] + self.icons = [self.ui.getIcon(":/icons/button_invalid.svg")] + for g in gn: + o = self.doc.getObject(g) + if o: + self.labels.append(o.Label) + self.icons.append(o.ViewObject.Icon) + self.labels.append(translate("draft", "Add new Layer")) + self.icons.append(self.ui.getIcon(":/icons/document-new.svg")) + + # With the lists created is uses the interface + # to pop up a menu with layer options. + # Once the desired option is chosen + # it launches the `proceed` method. + self.ui.sourceCmd = self + pos = self.ui.autoGroupButton.mapToGlobal(QtCore.QPoint(0, self.ui.autoGroupButton.geometry().height())) + self.ui.popupMenu(self.labels, self.icons, pos) + + def proceed(self, labelname): + """Set the defined autogroup, or create a new layer. + + Parameters + ---------- + labelname: str + The passed string with the name of the group or layer. + """ + # Deactivate the source command of the `DraftToolBar` class + # so that it doesn't do more with this command + # when it finishes. + self.ui.sourceCmd = None + + if labelname in self.labels: + if labelname == self.labels[0]: + # First option "None" deactivates autogrouping + self.ui.setAutoGroup(None) + elif labelname == self.labels[-1]: + # Last option "Add new layer" creates new layer + Gui.runCommand("Draft_Layer") + else: + # Set autogroup to the chosen layer + i = self.labels.index(labelname) + self.ui.setAutoGroup(self.groups[i]) + + +Gui.addCommand('Draft_AutoGroup', SetAutoGroup()) + + +class AddToConstruction(gui_base.GuiCommandSimplest): + """Gui Command for the AddToConstruction tool. + + It adds the selected objects to the construction group + defined in the `DraftToolBar` class which is initialized + in the `Gui` namespace when the workbench loads. + + It adds a construction group if it doesn't exist. + + Added objects are also given the visual properties of the construction + group. + """ + + def __init__(self): + super().__init__(name=_tr("Add to construction group")) + + def GetResources(self): + """Set icon, menu and tooltip.""" + _menu = "Add to Construction group" + _tip = ("Adds the selected objects to the construction group,\n" + "and changes their appearance to the construction style.\n" + "It creates a construction group if it doesn't exist.") + + d = {'Pixmap': 'Draft_AddConstruction', + 'MenuText': QT_TRANSLATE_NOOP("Draft_AddConstruction", _menu), + 'ToolTip': QT_TRANSLATE_NOOP("Draft_AddConstruction", _tip)} + return d + + def Activated(self): + """Execute when the command is called.""" + super().Activated() + + if not hasattr(Gui, "draftToolBar"): + return + + col = Gui.draftToolBar.getDefaultColor("constr") + col = (float(col[0]), float(col[1]), float(col[2]), 0.0) + + # Get the construction group or create it if it doesn't exist + gname = utils.get_param("constructiongroupname", "Construction") + grp = self.doc.getObject(gname) + if not grp: + grp = self.doc.addObject("App::DocumentObjectGroup", gname) + + for obj in Gui.Selection.getSelection(): + grp.addObject(obj) + + # Change the appearance to the construction colors + vobj = obj.ViewObject + if "TextColor" in vobj.PropertiesList: + vobj.TextColor = col + if "PointColor" in vobj.PropertiesList: + vobj.PointColor = col + if "LineColor" in vobj.PropertiesList: + vobj.LineColor = col + if "ShapeColor" in vobj.PropertiesList: + vobj.ShapeColor = col + if hasattr(vobj, "Transparency"): + vobj.Transparency = 80 + + +Draft_AddConstruction = AddToConstruction +Gui.addCommand('Draft_AddConstruction', AddToConstruction()) diff --git a/src/Mod/Draft/draftguitools/gui_heal.py b/src/Mod/Draft/draftguitools/gui_heal.py new file mode 100644 index 0000000000..0c6a3ac300 --- /dev/null +++ b/src/Mod/Draft/draftguitools/gui_heal.py @@ -0,0 +1,76 @@ +# *************************************************************************** +# * (c) 2009, 2010 Yorik van Havre * +# * (c) 2009, 2010 Ken Cline * +# * (c) 2020 Eliud Cabrera Castillo * +# * * +# * This file is part of the FreeCAD CAx development system. * +# * * +# * This program is free software; you can redistribute it and/or modify * +# * it under the terms of the GNU Lesser General Public License (LGPL) * +# * as published by the Free Software Foundation; either version 2 of * +# * the License, or (at your option) any later version. * +# * for detail see the LICENCE text file. * +# * * +# * FreeCAD is distributed in the hope that it will be useful, * +# * but WITHOUT ANY WARRANTY; without even the implied warranty of * +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# * GNU Library General Public License for more details. * +# * * +# * You should have received a copy of the GNU Library General Public * +# * License along with FreeCAD; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# *************************************************************************** +"""Provides the Draft_Heal command to heal older Draft files.""" +## @package gui_health +# \ingroup DRAFT +# \brief Provides the Draft_Heal command to heal older Draft files. + +from PySide.QtCore import QT_TRANSLATE_NOOP + +import FreeCADGui as Gui +import Draft +import draftguitools.gui_base as gui_base +from draftutils.translate import _tr + + +class Heal(gui_base.GuiCommandSimplest): + """The Draft Heal command definition. + + Heal faulty Draft objects saved with an earlier version of the program. + + It inherits `GuiCommandSimplest` to set up the document + and other behavior. See this class for more information. + """ + + def __init__(self): + super().__init__(name=_tr("Heal")) + + def GetResources(self): + """Set icon, menu and tooltip.""" + _tip = ("Heal faulty Draft objects saved with an earlier version " + "of the program.\n" + "If an object is selected it will try to heal that object " + "in particular,\n" + "otherwise it will try to heal all objects " + "in the active document.") + + return {'Pixmap': 'Draft_Heal', + 'MenuText': QT_TRANSLATE_NOOP("Draft_Heal", "Heal"), + 'ToolTip': QT_TRANSLATE_NOOP("Draft_Heal", _tip)} + + def Activated(self): + """Execute when the command is called.""" + super().Activated() + + s = Gui.Selection.getSelection() + self.doc.openTransaction("Heal") + if s: + Draft.heal(s) + else: + Draft.heal() + self.doc.commitTransaction() + + +Gui.addCommand('Draft_Heal', Heal()) diff --git a/src/Mod/Draft/draftguitools/gui_line_add_delete.py b/src/Mod/Draft/draftguitools/gui_line_add_delete.py new file mode 100644 index 0000000000..8482f6dd60 --- /dev/null +++ b/src/Mod/Draft/draftguitools/gui_line_add_delete.py @@ -0,0 +1,109 @@ +# *************************************************************************** +# * (c) 2009, 2010 Yorik van Havre * +# * (c) 2009, 2010 Ken Cline * +# * (c) 2020 Eliud Cabrera Castillo * +# * * +# * This file is part of the FreeCAD CAx development system. * +# * * +# * This program is free software; you can redistribute it and/or modify * +# * it under the terms of the GNU Lesser General Public License (LGPL) * +# * as published by the Free Software Foundation; either version 2 of * +# * the License, or (at your option) any later version. * +# * for detail see the LICENCE text file. * +# * * +# * FreeCAD is distributed in the hope that it will be useful, * +# * but WITHOUT ANY WARRANTY; without even the implied warranty of * +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# * GNU Library General Public License for more details. * +# * * +# * You should have received a copy of the GNU Library General Public * +# * License along with FreeCAD; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# *************************************************************************** +"""Provides certain add and remove line operations of the Draft Workbench. + +These GuiCommands aren't really used anymore, as the same actions +are implemented directly in the Draft_Edit command. +""" +## @package gui_line_add_delete +# \ingroup DRAFT +# \brief Provides certain add and remove line operations. +from PySide.QtCore import QT_TRANSLATE_NOOP + +import FreeCADGui as Gui +import Draft_rc +import DraftTools +import draftutils.utils as utils + +# The module is used to prevent complaints from code checkers (flake8) +True if Draft_rc.__name__ else False + + +class AddPoint(DraftTools.Modifier): + """GuiCommand to add a point to a line being drawn.""" + + def __init__(self): + self.running = False + + def GetResources(self): + """Set icon, menu and tooltip.""" + _menu = "Add point" + _tip = "Adds a point to an existing Wire or B-spline." + + return {'Pixmap': 'Draft_AddPoint', + 'MenuText': QT_TRANSLATE_NOOP("Draft_AddPoint", _menu), + 'ToolTip': QT_TRANSLATE_NOOP("Draft_AddPoint", _tip)} + + def IsActive(self): + """Return True when there is selection and the command is active.""" + if Gui.Selection.getSelection(): + return True + else: + return False + + def Activated(self): + """Execute when the command is called.""" + selection = Gui.Selection.getSelection() + if selection: + if (utils.get_type(selection[0]) in ['Wire', 'BSpline']): + Gui.runCommand("Draft_Edit") + Gui.draftToolBar.vertUi(True) + + +Gui.addCommand('Draft_AddPoint', AddPoint()) + + +class DelPoint(DraftTools.Modifier): + """GuiCommand to delete a point to a line being drawn.""" + + def __init__(self): + self.running = False + + def GetResources(self): + """Set icon, menu and tooltip.""" + _menu = "Remove point" + _tip = "Removes a point from an existing Wire or B-spline." + + return {'Pixmap': 'Draft_DelPoint', + 'MenuText': QT_TRANSLATE_NOOP("Draft_DelPoint", _menu), + 'ToolTip': QT_TRANSLATE_NOOP("Draft_DelPoint", _tip)} + + def IsActive(self): + """Return True when there is selection and the command is active.""" + if Gui.Selection.getSelection(): + return True + else: + return False + + def Activated(self): + """Execute when the command is called.""" + selection = Gui.Selection.getSelection() + if selection: + if (utils.get_type(selection[0]) in ['Wire', 'BSpline']): + Gui.runCommand("Draft_Edit") + Gui.draftToolBar.vertUi(False) + + +Gui.addCommand('Draft_DelPoint', DelPoint()) diff --git a/src/Mod/Draft/draftguitools/gui_lineops.py b/src/Mod/Draft/draftguitools/gui_lineops.py new file mode 100644 index 0000000000..75d0d589fc --- /dev/null +++ b/src/Mod/Draft/draftguitools/gui_lineops.py @@ -0,0 +1,163 @@ +# *************************************************************************** +# * (c) 2009, 2010 Yorik van Havre * +# * (c) 2009, 2010 Ken Cline * +# * (c) 2020 Eliud Cabrera Castillo * +# * * +# * This file is part of the FreeCAD CAx development system. * +# * * +# * This program is free software; you can redistribute it and/or modify * +# * it under the terms of the GNU Lesser General Public License (LGPL) * +# * as published by the Free Software Foundation; either version 2 of * +# * the License, or (at your option) any later version. * +# * for detail see the LICENCE text file. * +# * * +# * FreeCAD is distributed in the hope that it will be useful, * +# * but WITHOUT ANY WARRANTY; without even the implied warranty of * +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# * GNU Library General Public License for more details. * +# * * +# * You should have received a copy of the GNU Library General Public * +# * License along with FreeCAD; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# *************************************************************************** +"""Provides certain line operations of the Draft Workbench. + +These GuiCommands aren't really used anymore, as the same actions +are called from the task panel interface by other methods. +""" +## @package gui_lineops +# \ingroup DRAFT +# \brief Provides certain line operations in the Draft Workbench. +from PySide.QtCore import QT_TRANSLATE_NOOP + +import FreeCAD as App +import FreeCADGui as Gui +import Draft_rc +import draftguitools.gui_base as gui_base +from draftutils.messages import _msg +from draftutils.translate import _tr + +# The module is used to prevent complaints from code checkers (flake8) +True if Draft_rc.__name__ else False + + +class LineAction(gui_base.GuiCommandSimplest): + """Base class for Line context GuiCommands. + + This is inherited by the other GuiCommand classes to run + a set of similar actions when editing a line, wire, spline, + or bezier curve. + + It inherits `GuiCommandSimplest` to set up the document + and other behavior. See this class for more information. + """ + + def Activated(self, action="None"): + """Execute when the command is called. + + Parameters + ---------- + action: str + Indicates the type of action to perform with the line object. + It can be `'finish'`, `'close'`, or `'undo'`. + """ + if hasattr(App, "activeDraftCommand"): + _command = App.activeDraftCommand + else: + _msg(_tr("No active command.")) + return + + if (_command is not None + and _command.featureName in ("Line", "Polyline", + "BSpline", "BezCurve", + "CubicBezCurve")): + if action == "finish": + _command.finish(False) + elif action == "close": + _command.finish(True) + elif action == "undo": + _command.undolast() + + +class FinishLine(LineAction): + """GuiCommand to finish any running line drawing operation.""" + + def __init__(self): + super().__init__(name=_tr("Finish line")) + + def GetResources(self): + """Set icon, menu and tooltip.""" + _tip = "Finishes a line without closing it." + + d = {'Pixmap': 'Draft_Finish', + 'MenuText': QT_TRANSLATE_NOOP("Draft_FinishLine", "Finish line"), + 'ToolTip': QT_TRANSLATE_NOOP("Draft_FinishLine", _tip), + 'CmdType': 'ForEdit'} + return d + + def Activated(self): + """Execute when the command is called. + + It calls the `finish(False)` method of the active Draft command. + """ + super().Activated(action="finish") + + +Gui.addCommand('Draft_FinishLine', FinishLine()) + + +class CloseLine(LineAction): + """GuiCommand to close the line being drawn and finish the operation.""" + + def __init__(self): + super().__init__(name=_tr("Close line")) + + def GetResources(self): + """Set icon, menu and tooltip.""" + _tip = "Closes the line being drawn, and finishes the operation." + + d = {'Pixmap': 'Draft_Lock', + 'MenuText': QT_TRANSLATE_NOOP("Draft_CloseLine", "Close Line"), + 'ToolTip': QT_TRANSLATE_NOOP("Draft_CloseLine", _tip), + 'CmdType': 'ForEdit'} + return d + + def Activated(self): + """Execute when the command is called. + + It calls the `finish(True)` method of the active Draft command. + """ + super().Activated(action="close") + + +Gui.addCommand('Draft_CloseLine', CloseLine()) + + +class UndoLine(LineAction): + """GuiCommand to undo the last drawn segment of a line.""" + + def __init__(self): + super().__init__(name=_tr("Undo line")) + + def GetResources(self): + """Set icon, menu and tooltip.""" + _tip = "Undoes the last drawn segment of the line being drawn." + + d = {'Pixmap': 'Draft_Rotate', + 'MenuText': QT_TRANSLATE_NOOP("Draft_UndoLine", + "Undo last segment"), + 'ToolTip': QT_TRANSLATE_NOOP("Draft_UndoLine", _tip), + 'CmdType': 'ForEdit'} + return d + + def Activated(self): + """Execute when the command is called. + + It calls the `undolast` method of the active Draft command. + """ + super().Activated(action="undo") + + +Gui.addCommand('Draft_UndoLine', UndoLine()) diff --git a/src/Mod/Draft/draftguitools/gui_lineslope.py b/src/Mod/Draft/draftguitools/gui_lineslope.py new file mode 100644 index 0000000000..3613c76444 --- /dev/null +++ b/src/Mod/Draft/draftguitools/gui_lineslope.py @@ -0,0 +1,156 @@ +# *************************************************************************** +# * (c) 2009, 2010 Yorik van Havre * +# * (c) 2009, 2010 Ken Cline * +# * (c) 2020 Eliud Cabrera Castillo * +# * * +# * This file is part of the FreeCAD CAx development system. * +# * * +# * This program is free software; you can redistribute it and/or modify * +# * it under the terms of the GNU Lesser General Public License (LGPL) * +# * as published by the Free Software Foundation; either version 2 of * +# * the License, or (at your option) any later version. * +# * for detail see the LICENCE text file. * +# * * +# * FreeCAD is distributed in the hope that it will be useful, * +# * but WITHOUT ANY WARRANTY; without even the implied warranty of * +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# * GNU Library General Public License for more details. * +# * * +# * You should have received a copy of the GNU Library General Public * +# * License along with FreeCAD; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# *************************************************************************** +"""Provides tools to change the slope of a line over the working plane. + +It currently only works for a line in the XY plane, it changes the height +of one of its points in the Z direction to create a sloped line. +""" +## @package gui_lineslope +# \ingroup DRAFT +# \brief Provides tools to change the slope of a line over the working plane. + +import PySide.QtGui as QtGui +from PySide.QtCore import QT_TRANSLATE_NOOP + +import FreeCAD as App +import FreeCADGui as Gui +import Draft_rc +import draftutils.utils as utils +import draftguitools.gui_base as gui_base +from draftutils.translate import _tr, translate + +# The module is used to prevent complaints from code checkers (flake8) +True if Draft_rc.__name__ else False + + +class LineSlope(gui_base.GuiCommandNeedsSelection): + """Gui Command for the Line slope tool. + + For a line in the XY plane, it changes the height of one of its points + to create a sloped line. + + To Do + ----- + Make it work also with lines lying on the YZ and XZ planes, + or in an arbitrary plane, for which the normal is known. + """ + + def __init__(self): + super().__init__(name=_tr("Change slope")) + + def GetResources(self): + """Set icon, menu and tooltip.""" + _menu = "Set slope" + _tip = ("Sets the slope of the selected line " + "by changing the value of the Z value of one of its points.\n" + "If a polyline is selected, it will apply the slope " + "transformation to each of its segments.\n\n" + "The slope will always change the Z value, therefore " + "this command only works well for\n" + "straight Draft lines that are drawn in the XY plane. " + "Selected objects that aren't single lines will be ignored.") + + return {'Pixmap': 'Draft_Slope', + 'MenuText': QT_TRANSLATE_NOOP("Draft_Slope", _menu), + 'ToolTip': QT_TRANSLATE_NOOP("Draft_Slope", _tip)} + + def Activated(self): + """Execute when the command is called.""" + super().Activated() + + # for obj in Gui.Selection.getSelection(): + # if utils.get_type(obj) != "Wire": + # _msg(translate("draft", + # "This tool only works with " + # "Draft Lines and Wires")) + # return + + # TODO: create a .ui file with QtCreator and import it here + # instead of creating the interface programmatically, + # see the `gui_othoarray` module for an example. + w = QtGui.QWidget() + w.setWindowTitle(translate("Draft", "Slope")) + layout = QtGui.QHBoxLayout(w) + label = QtGui.QLabel(w) + label.setText(translate("Draft", "Slope")+":") + layout.addWidget(label) + self.spinbox = QtGui.QDoubleSpinBox(w) + self.spinbox.setMinimum(-9999.99) + self.spinbox.setMaximum(9999.99) + self.spinbox.setSingleStep(0.01) + _tip = ("New slope of the selected lines.\n" + "This is the tangent of the horizontal angle:\n" + "0 = horizontal\n" + "1 = 45 deg up\n" + "-1 = 45deg down\n") + label.setToolTip(translate("Draft", _tip)) + self.spinbox.setToolTip(translate("Draft", _tip)) + layout.addWidget(self.spinbox) + + # In order to display our interface inside the task panel + # we must contain our interface inside a parent widget. + # Then our interface must be installed in this parent widget + # inside the attribute called "form". + taskwidget = QtGui.QWidget() + taskwidget.form = w + + # The "accept" attribute of the parent widget + # should also contain a reference to a function that will be called + # when we press the "OK" button. + # Then we must show the container widget. + taskwidget.accept = self.accept + Gui.Control.showDialog(taskwidget) + + def accept(self): + """Execute when clicking the OK button or pressing Enter key. + + It changes the slope of the line that lies on the XY plane. + + TODO: make it work also with lines lying on the YZ and XZ planes. + """ + if hasattr(self, "spinbox"): + pc = self.spinbox.value() + self.doc.openTransaction("Change slope") + for obj in Gui.Selection.getSelection(): + if utils.get_type(obj) == "Wire": + if len(obj.Points) > 1: + lp = None + np = [] + for p in obj.Points: + if not lp: + lp = p + else: + v = p.sub(lp) + z = pc * App.Vector(v.x, v.y, 0).Length + lp = App.Vector(p.x, p.y, lp.z + z) + np.append(lp) + obj.Points = np + self.doc.commitTransaction() + Gui.Control.closeDialog() + self.doc.recompute() + + +Draft_Slope = LineSlope +Gui.addCommand('Draft_Slope', LineSlope()) diff --git a/src/Mod/Draft/draftguitools/gui_orthoarray.py b/src/Mod/Draft/draftguitools/gui_orthoarray.py index fdd2f0b4f3..07f8537dd7 100644 --- a/src/Mod/Draft/draftguitools/gui_orthoarray.py +++ b/src/Mod/Draft/draftguitools/gui_orthoarray.py @@ -31,13 +31,15 @@ from PySide.QtCore import QT_TRANSLATE_NOOP import FreeCAD as App import FreeCADGui as Gui import Draft -import Draft_rc +import Draft_rc # include resources, icons, ui files +from draftutils.messages import _msg, _log +from draftutils.translate import _tr from draftguitools import gui_base from drafttaskpanels import task_orthoarray import draftutils.todo as todo # The module is used to prevent complaints from code checkers (flake8) -True if Draft_rc.__name__ else False +bool(Draft_rc.__name__) class GuiCommandOrthoArray(gui_base.GuiCommandBase): @@ -45,7 +47,7 @@ class GuiCommandOrthoArray(gui_base.GuiCommandBase): def __init__(self): super().__init__() - self.command_name = "OrthoArray" + self.command_name = "Orthogonal array" # self.location = None self.mouse_event = None self.view = None @@ -56,14 +58,15 @@ class GuiCommandOrthoArray(gui_base.GuiCommandBase): def GetResources(self): """Set icon, menu and tooltip.""" - _msg = ("Creates copies of a selected object, " + _tip = ("Creates copies of a selected object, " "and places the copies in an orthogonal pattern.\n" "The properties of the array can be further modified after " "the new object is created, including turning it into " "a different type of array.") + d = {'Pixmap': 'Draft_Array', 'MenuText': QT_TRANSLATE_NOOP("Draft", "Array"), - 'ToolTip': QT_TRANSLATE_NOOP("Draft", _msg)} + 'ToolTip': QT_TRANSLATE_NOOP("Draft", _tip)} return d def Activated(self): @@ -72,6 +75,10 @@ class GuiCommandOrthoArray(gui_base.GuiCommandBase): We add callbacks that connect the 3D view with the widgets of the task panel. """ + _log("GuiCommand: {}".format(_tr(self.command_name))) + _msg("{}".format(16*"-")) + _msg("GuiCommand: {}".format(_tr(self.command_name))) + # self.location = coin.SoLocation2Event.getClassTypeId() self.mouse_event = coin.SoMouseButtonEvent.getClassTypeId() self.view = Draft.get3DView() diff --git a/src/Mod/Draft/draftguitools/gui_polararray.py b/src/Mod/Draft/draftguitools/gui_polararray.py index 0c277c5bcf..f31ed2bf01 100644 --- a/src/Mod/Draft/draftguitools/gui_polararray.py +++ b/src/Mod/Draft/draftguitools/gui_polararray.py @@ -20,7 +20,7 @@ # * USA * # * * # *************************************************************************** -"""Provides the Draft PolarArray tool.""" +"""Provides the Draft PolarArray GuiCommand.""" ## @package gui_polararray # \ingroup DRAFT # \brief This module provides the Draft PolarArray tool. @@ -31,13 +31,15 @@ from PySide.QtCore import QT_TRANSLATE_NOOP import FreeCAD as App import FreeCADGui as Gui import Draft -import Draft_rc +import Draft_rc # include resources, icons, ui files +from draftutils.messages import _msg, _log +from draftutils.translate import _tr from draftguitools import gui_base from drafttaskpanels import task_polararray import draftutils.todo as todo # The module is used to prevent complaints from code checkers (flake8) -True if Draft_rc.__name__ else False +bool(Draft_rc.__name__) class GuiCommandPolarArray(gui_base.GuiCommandBase): @@ -45,7 +47,7 @@ class GuiCommandPolarArray(gui_base.GuiCommandBase): def __init__(self): super().__init__() - self.command_name = "PolarArray" + self.command_name = "Polar array" self.location = None self.mouse_event = None self.view = None @@ -56,14 +58,15 @@ class GuiCommandPolarArray(gui_base.GuiCommandBase): def GetResources(self): """Set icon, menu and tooltip.""" - _msg = ("Creates copies of a selected object, " + _tip = ("Creates copies of a selected object, " "and places the copies in a polar pattern.\n" "The properties of the array can be further modified after " "the new object is created, including turning it into " "a different type of array.") + d = {'Pixmap': 'Draft_PolarArray', 'MenuText': QT_TRANSLATE_NOOP("Draft", "Polar array"), - 'ToolTip': QT_TRANSLATE_NOOP("Draft", _msg)} + 'ToolTip': QT_TRANSLATE_NOOP("Draft", _tip)} return d def Activated(self): @@ -72,6 +75,10 @@ class GuiCommandPolarArray(gui_base.GuiCommandBase): We add callbacks that connect the 3D view with the widgets of the task panel. """ + _log("GuiCommand: {}".format(_tr(self.command_name))) + _msg("{}".format(16*"-")) + _msg("GuiCommand: {}".format(_tr(self.command_name))) + self.location = coin.SoLocation2Event.getClassTypeId() self.mouse_event = coin.SoMouseButtonEvent.getClassTypeId() self.view = Draft.get3DView() diff --git a/src/Mod/Draft/draftguitools/gui_snapper.py b/src/Mod/Draft/draftguitools/gui_snapper.py index 37a4a9e211..84ee1dc9e4 100644 --- a/src/Mod/Draft/draftguitools/gui_snapper.py +++ b/src/Mod/Draft/draftguitools/gui_snapper.py @@ -32,21 +32,28 @@ defined by `gui_trackers.gridTracker`. # This module provides tools to handle point snapping and # everything that goes with it (toolbar buttons, cursor icons, etc.). +from pivy import coin +from PySide import QtCore, QtGui + import collections as coll import inspect import itertools import math -from pivy import coin -from PySide import QtCore, QtGui -import FreeCAD -import FreeCADGui import Draft import DraftVecUtils -from FreeCAD import Vector +import DraftGeomUtils + +import FreeCAD as App +import FreeCADGui as Gui + +import Part + import draftguitools.gui_trackers as trackers +from draftutils.init_tools import get_draft_snap_commands from draftutils.messages import _msg, _wrn + __title__ = "FreeCAD Draft Snap tools" __author__ = "Yorik van Havre" __url__ = "https://www.freecadweb.org" @@ -117,6 +124,27 @@ class Snapper: self.callbackMove = None self.snapObjectIndex = 0 + # snap keys, it's important tha they are in this order for + # saving in preferences and for properly restore the toolbar + self.snaps = ['Lock', # 0 + 'Near', # 1 former "passive" snap + 'Extension', # 2 + 'Parallel', # 3 + 'Grid', # 4 + "Endpoint", # 5 + 'Midpoint', # 6 + 'Perpendicular', # 7 + 'Angle', # 8 + 'Center', # 9 + 'Ortho', # 10 + 'Intersection', # 11 + 'Special', # 12 + 'Dimensions', # 13 + 'WorkingPlane' # 14 + ] + + self.init_active_snaps() + # the snapmarker has "dot","circle" and "square" available styles if self.snapStyle: self.mk = coll.OrderedDict([('passive', 'empty'), @@ -159,6 +187,21 @@ class Snapper: ('intersection', ':/icons/Snap_Intersection.svg'), ('special', ':/icons/Snap_Special.svg')]) + + def init_active_snaps(self): + """ + set self.active_snaps according to user prefs + """ + self.active_snaps = [] + param = App.ParamGet("User parameter:BaseApp/Preferences/Mod/Draft") + snap_modes = param.GetString("snapModes") + i = 0 + for snap in snap_modes: + if bool(int(snap)): + self.active_snaps.append(self.snaps[i]) + i += 1 + + def cstr(self, lastpoint, constrain, point): """Return constraints if needed.""" if constrain or self.mask: @@ -170,6 +213,7 @@ class Snapper: self.radiusTracker.update(fpt) return fpt + def snap(self, screenpos, lastpoint=None, active=True, constrain=False, noTracker=False): @@ -193,12 +237,13 @@ class Snapper: global Part, DraftGeomUtils import Part, DraftGeomUtils + self.spoint = None if not hasattr(self, "toolbar"): self.makeSnapToolBar() - mw = FreeCADGui.getMainWindow() - bt = mw.findChild(QtGui.QToolBar, "Draft Snap") + mw = Gui.getMainWindow() + bt = mw.findChild(QtGui.QToolBar,"Draft Snap") if not bt: mw.addToolBar(self.toolbar) else: @@ -298,10 +343,12 @@ class Snapper: self.running = False return fp + def cycleSnapObject(self): """Increse the index of the snap object by one.""" self.snapObjectIndex = self.snapObjectIndex + 1 + def snapToObject(self, lastpoint, active, constrain, eline, point, oldActive): """Snap to an object.""" @@ -310,7 +357,7 @@ class Snapper: subname = self.snapInfo['SubName'] obj = parent.getSubObject(subname, retType=1) else: - obj = FreeCAD.ActiveDocument.getObject(self.snapInfo['Object']) + obj = App.ActiveDocument.getObject(self.snapInfo['Object']) parent = obj subname = self.snapInfo['Component'] if not obj: @@ -434,9 +481,9 @@ class Snapper: # calculating the nearest snap point shortest = 1000000000000000000 - origin = Vector(self.snapInfo['x'], - self.snapInfo['y'], - self.snapInfo['z']) + origin = App.Vector(self.snapInfo['x'], + self.snapInfo['y'], + self.snapInfo['z']) winner = None fp = point for snap in snaps: @@ -454,7 +501,7 @@ class Snapper: if self.radius: dv = point.sub(winner[2]) if (dv.Length > self.radius): - if (not oldActive) and self.isEnabled("passive"): + if (not oldActive) and self.isEnabled("Near"): winner = self.snapToVertex(self.snapInfo) # setting the cursors @@ -479,28 +526,31 @@ class Snapper: self.running = False return self.spoint + def toWP(self, point): """Project the given point on the working plane, if needed.""" if self.isEnabled("WorkingPlane"): - if hasattr(FreeCAD, "DraftWorkingPlane"): - return FreeCAD.DraftWorkingPlane.projectPoint(point) + if hasattr(App, "DraftWorkingPlane"): + return App.DraftWorkingPlane.projectPoint(point) return point + def getApparentPoint(self, x, y): """Return a 3D point, projected on the current working plane.""" view = Draft.get3DView() pt = view.getPoint(x, y) if self.mask != "z": - if hasattr(FreeCAD, "DraftWorkingPlane"): + if hasattr(App,"DraftWorkingPlane"): if view.getCameraType() == "Perspective": camera = view.getCameraNode() p = camera.getField("position").getValue() - dv = pt.sub(Vector(p[0], p[1], p[2])) + dv = pt.sub(App.Vector(p[0], p[1], p[2])) else: dv = view.getViewDirection() - return FreeCAD.DraftWorkingPlane.projectPoint(pt, dv) + return App.DraftWorkingPlane.projectPoint(pt, dv) return pt + def snapToDim(self, obj): snaps = [] if obj.ViewObject: @@ -509,6 +559,7 @@ class Snapper: snaps.append([obj.ViewObject.Proxy.p3, 'endpoint', self.toWP(obj.ViewObject.Proxy.p3)]) return snaps + def snapToExtensions(self, point, last, constrain, eline): """Return a point snapped to extension or parallel line. @@ -526,7 +577,7 @@ class Snapper: self.extLine.on() self.setCursor(tsnap[1]) return tsnap[2], eline - if self.isEnabled("extension"): + if self.isEnabled("Extension"): tsnap = self.snapToExtOrtho(last, constrain, eline) if tsnap: if (tsnap[0].sub(point)).Length < self.radius: @@ -553,74 +604,79 @@ class Snapper: self.setCursor(tsnap[1]) return tsnap[2], eline - for o in (self.lastObj[1], self.lastObj[0]): - if o and (self.isEnabled('extension') - or self.isEnabled('parallel')): - ob = FreeCAD.ActiveDocument.getObject(o) - if ob: - if ob.isDerivedFrom("Part::Feature"): - edges = ob.Shape.Edges - if Draft.getType(ob) == "Wall": - for so in [ob]+ob.Additions: - if Draft.getType(so) == "Wall": - if so.Base: - edges.extend(so.Base.Shape.Edges) - edges.reverse() - if (not self.maxEdges) or (len(edges) <= self.maxEdges): - for e in edges: - if DraftGeomUtils.geomType(e) == "Line": - np = self.getPerpendicular(e,point) - if not DraftGeomUtils.isPtOnEdge(np,e): - if (np.sub(point)).Length < self.radius: - if self.isEnabled('extension'): - if np != e.Vertexes[0].Point: - p0 = e.Vertexes[0].Point - if self.tracker and not self.selectMode: - self.tracker.setCoords(np) - self.tracker.setMarker(self.mk['extension']) - self.tracker.on() - if self.extLine: - if self.snapStyle: - dv = np.sub(p0) - self.extLine.p1(p0.add(dv.multiply(0.5))) - else: - self.extLine.p1(p0) - self.extLine.p2(np) - self.extLine.on() - self.setCursor('extension') - ne = Part.LineSegment(p0,np).toShape() - # storing extension line for intersection calculations later - if len(self.lastExtensions) == 0: - self.lastExtensions.append(ne) - elif len(self.lastExtensions) == 1: - if not DraftGeomUtils.areColinear(ne,self.lastExtensions[0]): - self.lastExtensions.append(self.lastExtensions[0]) - self.lastExtensions[0] = ne - else: - if (not DraftGeomUtils.areColinear(ne,self.lastExtensions[0])) and \ - (not DraftGeomUtils.areColinear(ne,self.lastExtensions[1])): - self.lastExtensions[1] = self.lastExtensions[0] - self.lastExtensions[0] = ne - return np,ne + for o in (self.lastObj[1], self.lastObj[0]): + if o and (self.isEnabled('Extension') + or self.isEnabled('Parallel')): + ob = App.ActiveDocument.getObject(o) + if not ob: + continue + if not ob.isDerivedFrom("Part::Feature"): + continue + edges = ob.Shape.Edges + if Draft.getType(ob) == "Wall": + for so in [ob]+ob.Additions: + if Draft.getType(so) == "Wall": + if so.Base: + edges.extend(so.Base.Shape.Edges) + edges.reverse() + if (not self.maxEdges) or (len(edges) <= self.maxEdges): + for e in edges: + if DraftGeomUtils.geomType(e) != "Line": + continue + np = self.getPerpendicular(e,point) + if DraftGeomUtils.isPtOnEdge(np,e): + continue + if (np.sub(point)).Length < self.radius: + if self.isEnabled('Extension'): + if np != e.Vertexes[0].Point: + p0 = e.Vertexes[0].Point + if self.tracker and not self.selectMode: + self.tracker.setCoords(np) + self.tracker.setMarker(self.mk['extension']) + self.tracker.on() + if self.extLine: + if self.snapStyle: + dv = np.sub(p0) + self.extLine.p1(p0.add(dv.multiply(0.5))) else: - if self.isEnabled('parallel'): - if last: - ve = DraftGeomUtils.vec(e) - if not DraftVecUtils.isNull(ve): - de = Part.LineSegment(last,last.add(ve)).toShape() - np = self.getPerpendicular(de,point) - if (np.sub(point)).Length < self.radius: - if self.tracker and not self.selectMode: - self.tracker.setCoords(np) - self.tracker.setMarker(self.mk['parallel']) - self.tracker.on() - self.setCursor('parallel') - return np,de + self.extLine.p1(p0) + self.extLine.p2(np) + self.extLine.on() + self.setCursor('extension') + ne = Part.LineSegment(p0,np).toShape() + # storing extension line for intersection calculations later + if len(self.lastExtensions) == 0: + self.lastExtensions.append(ne) + elif len(self.lastExtensions) == 1: + if not DraftGeomUtils.areColinear(ne,self.lastExtensions[0]): + self.lastExtensions.append(self.lastExtensions[0]) + self.lastExtensions[0] = ne + else: + if (not DraftGeomUtils.areColinear(ne,self.lastExtensions[0])) and \ + (not DraftGeomUtils.areColinear(ne,self.lastExtensions[1])): + self.lastExtensions[1] = self.lastExtensions[0] + self.lastExtensions[0] = ne + return np,ne + else: + if self.isEnabled('Parallel'): + if last: + ve = DraftGeomUtils.vec(e) + if not DraftVecUtils.isNull(ve): + de = Part.LineSegment(last,last.add(ve)).toShape() + np = self.getPerpendicular(de,point) + if (np.sub(point)).Length < self.radius: + if self.tracker and not self.selectMode: + self.tracker.setCoords(np) + self.tracker.setMarker(self.mk['parallel']) + self.tracker.on() + self.setCursor('parallel') + return np,de return point,eline + def snapToCrossExtensions(self, point): """Snap to the intersection of the last 2 extension lines.""" - if self.isEnabled('extension'): + if self.isEnabled('Extension'): if len(self.lastExtensions) == 2: np = DraftGeomUtils.findIntersection(self.lastExtensions[0], self.lastExtensions[1], True, True) if np: @@ -648,19 +704,20 @@ class Snapper: return p return None - def snapToPolar(self, point, last): + + def snapToPolar(self,point,last): """Snap to polar lines from the given point.""" - if self.isEnabled('ortho') and (not self.mask): + if self.isEnabled('Ortho') and (not self.mask): if last: vecs = [] - if hasattr(FreeCAD, "DraftWorkingPlane"): - ax = [FreeCAD.DraftWorkingPlane.u, - FreeCAD.DraftWorkingPlane.v, - FreeCAD.DraftWorkingPlane.axis] + if hasattr(App,"DraftWorkingPlane"): + ax = [App.DraftWorkingPlane.u, + App.DraftWorkingPlane.v, + App.DraftWorkingPlane.axis] else: - ax = [FreeCAD.Vector(1, 0, 0), - FreeCAD.Vector(0, 1, 0), - FreeCAD.Vector(0, 0, 1)] + ax = [App.Vector(1, 0, 0), + App.Vector(0, 1, 0), + App.Vector(0, 0, 1)] for a in self.polarAngles: if a == 90: vecs.extend([ax[0], ax[0].negative()]) @@ -687,11 +744,12 @@ class Snapper: return np,de return point, None + def snapToGrid(self, point): """Return a grid snap point if available.""" if self.grid: if self.grid.Visible: - if self.isEnabled("grid"): + if self.isEnabled("Grid"): np = self.grid.getClosestNode(point) if np: dv = point.sub(np) @@ -704,10 +762,11 @@ class Snapper: return np return point + def snapToEndpoints(self, shape): """Return a list of endpoints snap locations.""" snaps = [] - if self.isEnabled("endpoint"): + if self.isEnabled("Endpoint"): if hasattr(shape, "Vertexes"): for v in shape.Vertexes: snaps.append([v.Point, 'endpoint', self.toWP(v.Point)]) @@ -722,20 +781,22 @@ class Snapper: snaps.append([v, 'endpoint', self.toWP(v)]) return snaps + def snapToMidpoint(self, shape): """Return a list of midpoints snap locations.""" snaps = [] - if self.isEnabled("midpoint"): + if self.isEnabled("Midpoint"): if isinstance(shape, Part.Edge): mp = DraftGeomUtils.findMidpoint(shape) if mp: snaps.append([mp, 'midpoint', self.toWP(mp)]) return snaps + def snapToPerpendicular(self, shape, last): """Return a list of perpendicular snap locations.""" snaps = [] - if self.isEnabled("perpendicular"): + if self.isEnabled("Perpendicular"): if last: if isinstance(shape, Part.Edge): if DraftGeomUtils.geomType(shape) == "Line": @@ -755,10 +816,11 @@ class Snapper: snaps.append([np, 'perpendicular', self.toWP(np)]) return snaps + def snapToOrtho(self, shape, last, constrain): """Return a list of ortho snap locations.""" snaps = [] - if self.isEnabled("ortho"): + if self.isEnabled("Ortho"): if constrain: if isinstance(shape, Part.Edge): if last: @@ -772,9 +834,10 @@ class Snapper: snaps.append([p, 'ortho', self.toWP(p)]) return snaps + def snapToExtOrtho(self, last, constrain, eline): """Return an ortho X extension snap location.""" - if self.isEnabled("extension") and self.isEnabled("ortho"): + if self.isEnabled("Extension") and self.isEnabled("Ortho"): if constrain and last and self.constraintAxis and self.extLine: tmpEdge1 = Part.LineSegment(last, last.add(self.constraintAxis)).toShape() tmpEdge2 = Part.LineSegment(self.extLine.p1(), self.extLine.p2()).toShape() @@ -793,6 +856,7 @@ class Snapper: return None return None + def snapToHold(self, point): """Return a snap location that is orthogonal to hold points. @@ -800,15 +864,15 @@ class Snapper: """ if not self.holdPoints: return None - if hasattr(FreeCAD, "DraftWorkingPlane"): - u = FreeCAD.DraftWorkingPlane.u - v = FreeCAD.DraftWorkingPlane.v + if hasattr(App, "DraftWorkingPlane"): + u = App.DraftWorkingPlane.u + v = App.DraftWorkingPlane.v else: - u = FreeCAD.Vector(1, 0, 0) - v = FreeCAD.Vector(0, 1, 0) + u = App.Vector(1, 0, 0) + v = App.Vector(0, 1, 0) if len(self.holdPoints) > 1: # first try mid points - if self.isEnabled("midpoint"): + if self.isEnabled("Midpoint"): l = list(self.holdPoints) for p1, p2 in itertools.combinations(l, 2): p3 = p1.add((p2.sub(p1)).multiply(0.5)) @@ -843,9 +907,10 @@ class Snapper: return [p, 'extension', fp] return None + def snapToExtPerpendicular(self, last): """Return a perpendicular X extension snap location.""" - if self.isEnabled("extension") and self.isEnabled("perpendicular"): + if self.isEnabled("Extension") and self.isEnabled("Perpendicular"): if last and self.extLine: if self.extLine.p1() != self.extLine.p2(): tmpEdge = Part.LineSegment(self.extLine.p1(), self.extLine.p2()).toShape() @@ -853,10 +918,11 @@ class Snapper: return [np, 'perpendicular', np] return None + def snapToElines(self, e1, e2): """Return a snap at the infinite intersection of the given edges.""" snaps = [] - if self.isEnabled("intersection") and self.isEnabled("extension"): + if self.isEnabled("Intersection") and self.isEnabled("Extension"): if e1 and e2: # get the intersection points pts = DraftGeomUtils.findIntersection(e1, e2, True, True) @@ -865,10 +931,11 @@ class Snapper: snaps.append([p, 'intersection', self.toWP(p)]) return snaps + def snapToAngles(self, shape): """Return a list of angle snap locations.""" snaps = [] - if self.isEnabled("angle"): + if self.isEnabled("Angle"): rad = shape.Curve.Radius pos = shape.Curve.Center for i in (0, 30, 45, 60, 90, @@ -882,10 +949,11 @@ class Snapper: snaps.append([cur, 'angle', self.toWP(cur)]) return snaps + def snapToCenter(self, shape): """Return a list of center snap locations.""" snaps = [] - if self.isEnabled("center"): + if self.isEnabled("Center"): pos = shape.Curve.Center c = self.toWP(pos) if hasattr(shape.Curve, "Radius"): @@ -895,30 +963,32 @@ class Snapper: 195, 217.5, 232.5, 255, 285, 307.5, 322.5, 345): ang = math.radians(i) - cur = Vector(math.sin(ang) * rad + pos.x, - math.cos(ang) * rad + pos.y, - pos.z) + cur = App.Vector(math.sin(ang) * rad + pos.x, + math.cos(ang) * rad + pos.y, + pos.z) snaps.append([cur, 'center', c]) else: snaps.append([c, 'center', c]) return snaps + def snapToFace(self, shape): """Return a face center snap location.""" snaps = [] - if self.isEnabled("center"): + if self.isEnabled("Center"): pos = shape.CenterOfMass c = self.toWP(pos) snaps.append([pos, 'center', c]) return snaps + def snapToIntersection(self, shape): """Return a list of intersection snap locations.""" snaps = [] - if self.isEnabled("intersection"): + if self.isEnabled("Intersection"): # get the stored objects to calculate intersections if self.lastObj[0]: - obj = FreeCAD.ActiveDocument.getObject(self.lastObj[0]) + obj = App.ActiveDocument.getObject(self.lastObj[0]) if obj: if obj.isDerivedFrom("Part::Feature") or (Draft.getType(obj) == "Axis"): if (not self.maxEdges) or (len(obj.Shape.Edges) <= self.maxEdges): @@ -944,10 +1014,11 @@ class Snapper: # when trying to read their types return snaps + def snapToPolygon(self, obj): """Return a list of polygon center snap locations.""" snaps = [] - if self.isEnabled("center"): + if self.isEnabled("Center"): c = obj.Placement.Base for edge in obj.Shape.Edges: p1 = edge.Vertexes[0].Point @@ -958,23 +1029,24 @@ class Snapper: snaps.append([v2, 'center', self.toWP(c)]) return snaps + def snapToVertex(self, info, active=False): - """Return a vertex snap location.""" - p = Vector(info['x'], info['y'], info['z']) + p = App.Vector(info['x'], info['y'], info['z']) if active: - if self.isEnabled("passive"): + if self.isEnabled("Near"): return [p, 'endpoint', self.toWP(p)] else: return [] - elif self.isEnabled("passive"): + elif self.isEnabled("Near"): return [p, 'passive', p] else: return [] + def snapToSpecials(self, obj, lastpoint=None, eline=None): """Return special snap locations, if any.""" snaps = [] - if self.isEnabled("special"): + if self.isEnabled("Special"): if (Draft.getType(obj) == "Wall"): # special snapping for wall: snap to its base shape if it is linear @@ -1007,6 +1079,7 @@ class Snapper: return snaps + def getScreenDist(self, dist, cursor): """Return a distance in 3D space from a screen pixels distance.""" view = Draft.get3DView() @@ -1014,6 +1087,7 @@ class Snapper: p2 = view.getPoint((cursor[0] + dist, cursor[1])) return (p2.sub(p1)).Length + def getPerpendicular(self, edge, pt): """Return a point on an edge, perpendicular to the given point.""" dv = pt.sub(edge.Vertexes[0].Point) @@ -1021,6 +1095,7 @@ class Snapper: np = (edge.Vertexes[0].Point).add(nv) return np + def setArchDims(self, p1, p2): """Show arc dimensions between 2 points.""" if self.isEnabled("Dimensions"): @@ -1037,16 +1112,17 @@ class Snapper: if self.dim2.Distance: self.dim2.on() + def setCursor(self, mode=None): """Set or reset the cursor to the given mode or resets.""" if self.selectMode: - mw = FreeCADGui.getMainWindow() + mw = Gui.getMainWindow() for w in mw.findChild(QtGui.QMdiArea).findChildren(QtGui.QWidget): if w.metaObject().className() == "SIM::Coin3D::Quarter::QuarterWidget": w.unsetCursor() self.cursorMode = None elif not mode: - mw = FreeCADGui.getMainWindow() + mw = Gui.getMainWindow() for w in mw.findChild(QtGui.QMdiArea).findChildren(QtGui.QWidget): if w.metaObject().className() == "SIM::Coin3D::Quarter::QuarterWidget": w.unsetCursor() @@ -1064,17 +1140,19 @@ class Snapper: qp.drawPixmap(QtCore.QPoint(16, 8), tp) qp.end() cur = QtGui.QCursor(newicon, 8, 8) - mw = FreeCADGui.getMainWindow() + mw = Gui.getMainWindow() for w in mw.findChild(QtGui.QMdiArea).findChildren(QtGui.QWidget): if w.metaObject().className() == "SIM::Coin3D::Quarter::QuarterWidget": w.setCursor(cur) self.cursorMode = mode + def restack(self): """Lower the grid tracker so it doesn't obscure other objects.""" if self.grid: self.grid.lowerTracker() + def off(self, hideSnapBar=False): """Finish snapping.""" if self.tracker: @@ -1108,6 +1186,7 @@ class Snapper: self.running = False self.holdPoints = [] + def setSelectMode(self, mode): """Set the snapper into select mode (hides snapping temporarily).""" self.selectMode = mode @@ -1117,16 +1196,18 @@ class Snapper: if self.trackLine: self.trackLine.off() + def setAngle(self, delta=None): """Keep the current angle.""" if delta: self.mask = delta - elif isinstance(self.mask, FreeCAD.Vector): + elif isinstance(self.mask, App.Vector): self.mask = None elif self.trackLine: if self.trackLine.Visible: self.mask = self.trackLine.p2().sub(self.trackLine.p1()) + def constrain(self, point, basepoint=None, axis=None): """Return a constrained point. @@ -1138,15 +1219,15 @@ class Snapper: used as basepoint. """ # without the Draft module fully loaded, no axes system!" - if not hasattr(FreeCAD, "DraftWorkingPlane"): + if not hasattr(App, "DraftWorkingPlane"): return point - point = Vector(point) + point = App.Vector(point) # setup trackers if needed if not self.constrainLine: if self.snapStyle: - self.constrainLine = trackers.lineTracker(scolor=FreeCADGui.draftToolBar.getDefaultColor("snap")) + self.constrainLine = trackers.lineTracker(scolor=Gui.draftToolBar.getDefaultColor("snap")) else: self.constrainLine = trackers.lineTracker(dotted=True) @@ -1162,23 +1243,23 @@ class Snapper: if self.mask: self.affinity = self.mask if not self.affinity: - self.affinity = FreeCAD.DraftWorkingPlane.getClosestAxis(delta) - if isinstance(axis, FreeCAD.Vector): + self.affinity = App.DraftWorkingPlane.getClosestAxis(delta) + if isinstance(axis, App.Vector): self.constraintAxis = axis elif axis == "x": - self.constraintAxis = FreeCAD.DraftWorkingPlane.u + self.constraintAxis = App.DraftWorkingPlane.u elif axis == "y": - self.constraintAxis = FreeCAD.DraftWorkingPlane.v + self.constraintAxis = App.DraftWorkingPlane.v elif axis == "z": - self.constraintAxis = FreeCAD.DraftWorkingPlane.axis + self.constraintAxis = App.DraftWorkingPlane.axis else: if self.affinity == "x": - self.constraintAxis = FreeCAD.DraftWorkingPlane.u + self.constraintAxis = App.DraftWorkingPlane.u elif self.affinity == "y": - self.constraintAxis = FreeCAD.DraftWorkingPlane.v + self.constraintAxis = App.DraftWorkingPlane.v elif self.affinity == "z": - self.constraintAxis = FreeCAD.DraftWorkingPlane.axis - elif isinstance(self.affinity, FreeCAD.Vector): + self.constraintAxis = App.DraftWorkingPlane.axis + elif isinstance(self.affinity, App.Vector): self.constraintAxis = self.affinity else: self.constraintAxis = None @@ -1201,6 +1282,7 @@ class Snapper: return npoint + def unconstrain(self): """Unset the basepoint and the constrain line.""" self.basepoint = None @@ -1208,6 +1290,7 @@ class Snapper: if self.constrainLine: self.constrainLine.off() + def getPoint(self, last=None, callback=None, movecallback=None, extradlg=None, title=None, mode="point"): """Get a 3D point from the screen. @@ -1229,7 +1312,7 @@ class Snapper: if point: print "got a 3D point: ",point - FreeCADGui.Snapper.getPoint(callback=cb) + Gui.Snapper.getPoint(callback=cb) If the callback function accepts more than one argument, it will also receive the last snapped object. Finally, a qt widget @@ -1243,7 +1326,7 @@ class Snapper: self.pt = None self.lastSnappedObject = None self.holdPoints = [] - self.ui = FreeCADGui.draftToolBar + self.ui = Gui.draftToolBar self.view = Draft.get3DView() # remove any previous leftover callbacks @@ -1259,20 +1342,20 @@ class Snapper: mousepos = event.getPosition() ctrl = event.wasCtrlDown() shift = event.wasShiftDown() - self.pt = FreeCADGui.Snapper.snap(mousepos, lastpoint=last, - active=ctrl, constrain=shift) - if hasattr(FreeCAD, "DraftWorkingPlane"): + self.pt = Gui.Snapper.snap(mousepos, lastpoint=last, + active=ctrl, constrain=shift) + if hasattr(App, "DraftWorkingPlane"): self.ui.displayPoint(self.pt, last, - plane=FreeCAD.DraftWorkingPlane, - mask=FreeCADGui.Snapper.affinity) + plane=App.DraftWorkingPlane, + mask=App.Snapper.affinity) if movecallback: movecallback(self.pt, self.snapInfo) def getcoords(point, relative=False): """Get the global coordinates from a point.""" self.pt = point - if relative and last and hasattr(FreeCAD, "DraftWorkingPlane"): - v = FreeCAD.DraftWorkingPlane.getGlobalCoords(point) + if relative and last and hasattr(App, "DraftWorkingPlane"): + v = App.DraftWorkingPlane.getGlobalCoords(point) self.pt = last.add(v) accept() @@ -1289,8 +1372,8 @@ class Snapper: self.view.removeEventCallbackPivy(coin.SoLocation2Event.getClassTypeId(), self.callbackMove) self.callbackClick = None self.callbackMove = None - obj = FreeCADGui.Snapper.lastSnappedObject - FreeCADGui.Snapper.off() + obj = Gui.Snapper.lastSnappedObject + Gui.Snapper.off() self.ui.offUi() if callback: if len(inspect.getargspec(callback).args) > 1: @@ -1306,7 +1389,7 @@ class Snapper: self.view.removeEventCallbackPivy(coin.SoLocation2Event.getClassTypeId(), self.callbackMove) self.callbackClick = None self.callbackMove = None - FreeCADGui.Snapper.off() + Gui.Snapper.off() self.ui.offUi() if callback: if len(inspect.getargspec(callback).args) > 1: @@ -1330,123 +1413,98 @@ class Snapper: self.callbackClick = self.view.addEventCallbackPivy(coin.SoMouseButtonEvent.getClassTypeId(),click) self.callbackMove = self.view.addEventCallbackPivy(coin.SoLocation2Event.getClassTypeId(),move) + def makeSnapToolBar(self): """Build the Snap toolbar.""" - mw = FreeCADGui.getMainWindow() + mw = Gui.getMainWindow() self.toolbar = QtGui.QToolBar(mw) mw.addToolBar(QtCore.Qt.TopToolBarArea, self.toolbar) self.toolbar.setObjectName("Draft Snap") self.toolbar.setWindowTitle(QtCore.QCoreApplication.translate("Workbench", "Draft Snap")) - self.toolbarButtons = [] - # grid button - self.gridbutton = QtGui.QAction(mw) - self.gridbutton.setIcon(QtGui.QIcon.fromTheme("Draft_Grid", QtGui.QIcon(":/icons/Draft_Grid.svg"))) - self.gridbutton.setText(QtCore.QCoreApplication.translate("Draft_ToggleGrid", "Grid")) - self.gridbutton.setToolTip(QtCore.QCoreApplication.translate("Draft_ToggleGrid", "Toggles the Draft grid On/Off")) - self.gridbutton.setObjectName("GridButton") - self.gridbutton.setWhatsThis("Draft_ToggleGrid") - QtCore.QObject.connect(self.gridbutton, QtCore.SIGNAL("triggered()"), self.toggleGrid) - self.toolbar.addAction(self.gridbutton) + snap_gui_commands = get_draft_snap_commands() + self.init_draft_snap_buttons(snap_gui_commands, self.toolbar, "_Button") + self.restore_snap_buttons_state(self.toolbar,"_Button") - # master button - self.masterbutton = QtGui.QAction(mw) - self.masterbutton.setIcon(QtGui.QIcon.fromTheme("Snap_Lock", QtGui.QIcon(":/icons/Snap_Lock.svg"))) - self.masterbutton.setText(QtCore.QCoreApplication.translate("Draft_Snap_Lock", "Lock")) - self.masterbutton.setToolTip(QtCore.QCoreApplication.translate("Draft_Snap_Lock", "Toggle On/Off")) - self.masterbutton.setObjectName("SnapButtonMain") - self.masterbutton.setWhatsThis("Draft_ToggleSnap") - self.masterbutton.setCheckable(True) - self.masterbutton.setChecked(True) - QtCore.QObject.connect(self.masterbutton, - QtCore.SIGNAL("toggled(bool)"), self.toggle) - self.toolbar.addAction(self.masterbutton) - for c,i in self.cursors.items(): - if i: - b = QtGui.QAction(mw) - b.setIcon(QtGui.QIcon.fromTheme(i.replace(':/icons/', '').replace('.svg', ''), QtGui.QIcon(i))) - if c == "passive": - b.setText(QtCore.QCoreApplication.translate("Draft_Snap_Near", "Nearest")) - b.setToolTip(QtCore.QCoreApplication.translate("Draft_Snap_Near", "Nearest")) - else: - b.setText(QtCore.QCoreApplication.translate("Draft_Snap_"+c.capitalize(), c.capitalize())) - b.setToolTip(QtCore.QCoreApplication.translate("Draft_Snap_"+c.capitalize(), c.capitalize())) - b.setObjectName("SnapButton" + c) - b.setWhatsThis("Draft_" + c.capitalize()) - b.setCheckable(True) - b.setChecked(True) - self.toolbar.addAction(b) - self.toolbarButtons.append(b) - QtCore.QObject.connect(b, QtCore.SIGNAL("toggled(bool)"), - self.saveSnapModes) + if not Draft.getParam("showSnapBar",True): + self.toolbar.hide() - # adding non-snap button - for n in ("Dimensions", "WorkingPlane"): - b = QtGui.QAction(mw) - b.setIcon(QtGui.QIcon.fromTheme("Snap_" + n, QtGui.QIcon(":/icons/Snap_"+n+".svg"))) - b.setText(QtCore.QCoreApplication.translate("Draft_Snap_" + n,n)) - b.setToolTip(QtCore.QCoreApplication.translate("Draft_Snap_" + n,n)) - b.setObjectName("SnapButton" + n) - b.setWhatsThis("Draft_" + n) + + def init_draft_snap_buttons(self, commands, context, button_suffix): + """ + Init Draft Snap toolbar buttons. + + Parameters: + commands Snap command list, + use: get_draft_snap_commands(): + context The toolbar or action group the buttons have + to be added to + button_suffix The suffix that have to be applied to the command name + to define the button name + """ + for gc in commands: + # setup toolbar buttons + command = 'Gui.runCommand("' + gc + '")' + b = QtGui.QAction(context) + b.setIcon(QtGui.QIcon(':/icons/' + gc[6:] + '.svg')) + b.setText(QtCore.QCoreApplication.translate("Draft_Snap", "Snap " + gc[11:])) + b.setToolTip(QtCore.QCoreApplication.translate("Draft_Snap", "Snap " + gc[11:])) + b.setObjectName(gc + button_suffix) + b.setWhatsThis("Draft_" + gc[11:].capitalize()) b.setCheckable(True) b.setChecked(True) - self.toolbar.addAction(b) - QtCore.QObject.connect(b, QtCore.SIGNAL("toggled(bool)"), - self.saveSnapModes) - self.toolbarButtons.append(b) + context.addAction(b) + QtCore.QObject.connect(b, + QtCore.SIGNAL("triggered()"), + lambda f=Gui.doCommand, + arg=command:f(arg)) - # set status tip where needed - for b in self.toolbar.actions(): + for b in context.actions(): if len(b.statusTip()) == 0: b.setStatusTip(b.toolTip()) - # restoring states - t = Draft.getParam("snapModes", "111111111101111") - if t: - c = 0 - for b in [self.masterbutton] + self.toolbarButtons: - if len(t) > c: - state = bool(int(t[c])) - b.setChecked(state) + + def restore_snap_buttons_state(self, toolbar, button_suffix): + """ + Restore toolbar button's checked state according to + "snapModes" saved in preferences + """ + # set status tip where needed + param = App.ParamGet("User parameter:BaseApp/Preferences/Mod/Draft") + snap_modes = param.GetString("snapModes") + + for button in toolbar.actions(): + if len(button.statusTip()) == 0: + button.setStatusTip(button.toolTip()) + + # restore toolbar buttons state + if snap_modes: + for action in toolbar.findChildren(QtGui.QAction): + snap = action.objectName()[11:].replace(button_suffix, "") + if snap in Gui.Snapper.snaps: + i = Gui.Snapper.snaps.index(snap) + state = bool(int(snap_modes[i])) + action.setChecked(state) if state: - b.setToolTip(b.toolTip() + " (ON)") + action.setToolTip(action.toolTip() + " (ON)") else: - b.setToolTip(b.toolTip() + " (OFF)") - c += 1 - if not Draft.getParam("showSnapBar", True): - self.toolbar.hide() + action.setToolTip(action.toolTip() + " (OFF)") + + + def get_snap_toolbar(self): + """Retuns snap toolbar object.""" + mw = Gui.getMainWindow() + if mw: + toolbar = mw.findChild(QtGui.QToolBar, "Draft Snap") + if toolbar: + return toolbar + return None + def toggleGrid(self): - """Run Draft_ToggleGrid.""" - FreeCADGui.runCommand("Draft_ToggleGrid") + """Toggle FreeCAD Draft Grid.""" + Gui.runCommand("Draft_ToggleGrid") - def saveSnapModes(self): - """Save the snap modes for next sessions.""" - t = '' - for b in [self.masterbutton] + self.toolbarButtons: - t += str(int(b.isChecked())) - if b.isChecked(): - b.setToolTip(b.toolTip().replace("OFF", "ON")) - else: - b.setToolTip(b.toolTip().replace("ON", "OFF")) - Draft.setParam("snapModes", t) - - def toggle(self, checked=None): - """Toggle the snap mode.""" - if hasattr(self, "toolbarButtons"): - if checked is None: - self.masterbutton.toggle() - elif checked: - if hasattr(self, "savedButtonStates"): - for i in range(len(self.toolbarButtons)): - self.toolbarButtons[i].setEnabled(True) - self.toolbarButtons[i].setChecked(self.savedButtonStates[i]) - else: - self.savedButtonStates = [] - for i in range(len(self.toolbarButtons)): - self.savedButtonStates.append(self.toolbarButtons[i].isChecked()) - self.toolbarButtons[i].setEnabled(False) - self.saveSnapModes() def showradius(self): """Show the snap radius indicator.""" @@ -1456,42 +1514,82 @@ class Snapper: self.radiusTracker.update(self.radius) self.radiusTracker.on() - def isEnabled(self, but): - """Return true if the given button is turned on.""" - for b in self.toolbarButtons: - if str(b.objectName()) == "SnapButton" + but: - return (b.isEnabled() and b.isChecked()) - return False + + def isEnabled(self, snap): + """Returns true if the given snap is on""" + if "Lock" in self.active_snaps and snap in self.active_snaps: + return True + else: + return False + + + def toggle_snap(self, snap, set_to = None): + """Sets the given snap on/off according to the given parameter""" + if set_to: # set mode + if set_to is True: + if not snap in self.active_snaps: + self.active_snaps.append(snap) + status = True + elif set_to is False: + if snap in self.active_snaps: + self.active_snaps.remove(snap) + status = False + else: # toggle mode, default + if not snap in self.active_snaps: + self.active_snaps.append(snap) + status = True + elif snap in self.active_snaps: + self.active_snaps.remove(snap) + status = False + self.save_snap_state() + return status + + + def save_snap_state(self): + """ + Save snap state to user preferences to be restored in next session. + """ + param = App.ParamGet("User parameter:BaseApp/Preferences/Mod/Draft") + snap_modes = "" + for snap in self.snaps: + if snap in self.active_snaps: + snap_modes += "1" + else: + snap_modes += "0" + param.SetString("snapModes",snap_modes) + def show(self): """Show the toolbar and the grid.""" if not hasattr(self, "toolbar"): self.makeSnapToolBar() - mw = FreeCADGui.getMainWindow() - bt = mw.findChild(QtGui.QToolBar, "Draft Snap") + bt = self.get_snap_toolbar() if not bt: + mw = FreeCADGui.getMainWindow() mw.addToolBar(self.toolbar) self.toolbar.setParent(mw) self.toolbar.show() self.toolbar.toggleViewAction().setVisible(True) - if FreeCADGui.ActiveDocument: + if Gui.ActiveDocument: self.setTrackers() - if not FreeCAD.ActiveDocument.Objects: - if FreeCADGui.ActiveDocument.ActiveView: - if FreeCADGui.ActiveDocument.ActiveView.getCameraType() == 'Orthographic': - c = FreeCADGui.ActiveDocument.ActiveView.getCameraNode() + if not App.ActiveDocument.Objects: + if Gui.ActiveDocument.ActiveView: + if Gui.ActiveDocument.ActiveView.getCameraType() == 'Orthographic': + c = Gui.ActiveDocument.ActiveView.getCameraNode() if c.orientation.getValue().getValue() == (0.0, 0.0, 0.0, 1.0): - p = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Draft") + p = App.ParamGet("User parameter:BaseApp/Preferences/Mod/Draft") h = p.GetInt("defaultCameraHeight",0) if h: c.height.setValue(h) + def hide(self): """Hide the toolbar.""" if hasattr(self, "toolbar"): self.toolbar.hide() self.toolbar.toggleViewAction().setVisible(True) + def setGrid(self): """Set the grid, if visible.""" self.setTrackers() @@ -1499,6 +1597,7 @@ class Snapper: if self.grid.Visible: self.grid.set() + def setTrackers(self): """Set the trackers.""" v = Draft.get3DView() @@ -1523,7 +1622,7 @@ class Snapper: self.tracker = trackers.snapTracker() self.trackLine = trackers.lineTracker() if self.snapStyle: - c = FreeCADGui.draftToolBar.getDefaultColor("snap") + c = Gui.draftToolBar.getDefaultColor("snap") self.extLine = trackers.lineTracker(scolor=c) self.extLine2 = trackers.lineTracker(scolor=c) else: @@ -1546,9 +1645,11 @@ class Snapper: self.trackers[8].append(self.extLine2) self.trackers[9].append(self.holdTracker) self.activeview = v + if self.grid and (not self.forceGridOff): self.grid.set() + def addHoldPoint(self): """Add hold snap point to list of hold points.""" if self.spoint and self.spoint not in self.holdPoints: diff --git a/src/Mod/Draft/draftguitools/gui_snaps.py b/src/Mod/Draft/draftguitools/gui_snaps.py index 019d93c47a..b26a8becea 100644 --- a/src/Mod/Draft/draftguitools/gui_snaps.py +++ b/src/Mod/Draft/draftguitools/gui_snaps.py @@ -28,64 +28,154 @@ # \brief Provide the Draft_Snap commands used by the snapping mechanism # in Draft. +from PySide import QtGui from PySide.QtCore import QT_TRANSLATE_NOOP import FreeCADGui as Gui +import draftguitools.gui_base as gui_base +from draftutils.translate import _tr -class Draft_Snap_Lock: - """Command to activate or deactivate all snap commands.""" + +# UTILITIES ----------------------------------------------------------------- + + +def get_snap_statusbar_widget(): + """Return snap statusbar button.""" + mw = Gui.getMainWindow() + if mw: + sb = mw.statusBar() + if sb: + return sb.findChild(QtGui.QToolBar,"draft_snap_widget") + return None + + +def sync_snap_toolbar_button(button, status): + """Set snap toolbar button to given state.""" + snap_toolbar = Gui.Snapper.get_snap_toolbar() + if not snap_toolbar: + return + for a in snap_toolbar.actions(): + if a.objectName() == button: + if button == "Draft_Snap_Lock_Button": + # for lock button + snap_toolbar.actions()[0].setChecked(status) + for a in snap_toolbar.actions()[1:]: + a.setEnabled(status) + else: + # for every other button + a.setChecked(status) + if a.isChecked(): + a.setToolTip(a.toolTip().replace("OFF","ON")) + else: + a.setToolTip(a.toolTip().replace("ON","OFF")) + + +def sync_snap_statusbar_button(button, status): + """Set snap statusbar button to given state.""" + ssw = get_snap_statusbar_widget() + if not ssw: + return + for child in ssw.children(): + if child.objectName() == "Snap_Statusbutton": + ssb = child + actions = [] + for a in ssb.menu().actions() + ssw.children()[-6:]: + actions.append(a) + + if button == "Draft_Snap_Lock_Statusbutton": + ssb.setChecked(status) + for a in actions[1:]: + a.setEnabled(status) + else: + for a in actions: + if a.objectName() == button: + a.setChecked(status) + + +# SNAP GUI TOOLS ------------------------------------------------------------ + + +class Draft_Snap_Lock(gui_base.GuiCommandSimplest): + """GuiCommand for the Draft_Snap_Lock tool. + + Activate or deactivate all snap methods at once. + """ + + def __init__(self): + super(Draft_Snap_Lock, self).__init__(name=_tr("Main toggle snap")) def GetResources(self): """Set icon, menu and tooltip.""" - _menu = "Toggle On/Off" + _menu = "Main snapping toggle On/Off" _tip = ("Activates or deactivates " - "all snap tools at once") + "all snap methods at once.") + return {'Pixmap': 'Snap_Lock', 'Accel': "Shift+S", 'MenuText': QT_TRANSLATE_NOOP("Draft_Snap_Lock", _menu), 'ToolTip': QT_TRANSLATE_NOOP("Draft_Snap_Lock", _tip)} def Activated(self): - """Execute this when the command is called.""" + """Execute when the command is called.""" + super(Draft_Snap_Lock, self).Activated() + if hasattr(Gui, "Snapper"): - if hasattr(Gui.Snapper, "masterbutton"): - Gui.Snapper.masterbutton.toggle() + status = Gui.Snapper.toggle_snap('Lock') + # change interface consistently + sync_snap_toolbar_button("Draft_Snap_Lock"+"_Button", status) + sync_snap_statusbar_button("Draft_Snap_Lock"+"_Statusbutton", status) Gui.addCommand('Draft_Snap_Lock', Draft_Snap_Lock()) -class Draft_Snap_Midpoint: - """Command to snap to the midpoint of an edge.""" +class Draft_Snap_Midpoint(gui_base.GuiCommandSimplest): + """GuiCommand for the Draft_Snap_Midpoint tool. + + Set snapping to the midpoint of an edge. + """ + + def __init__(self): + super(Draft_Snap_Midpoint, self).__init__(name=_tr("Midpoint snap")) def GetResources(self): """Set icon, menu and tooltip.""" _menu = "Midpoint" - _tip = "Snaps to midpoints of edges" + _tip = "Set snapping to the midpoint of an edge." + return {'Pixmap': 'Snap_Midpoint', 'MenuText': QT_TRANSLATE_NOOP("Draft_Snap_Midpoint", _menu), 'ToolTip': QT_TRANSLATE_NOOP("Draft_Snap_Midpoint", _tip)} def Activated(self): - """Execute this when the command is called.""" + """Execute when the command is called.""" + super(Draft_Snap_Midpoint, self).Activated() + if hasattr(Gui, "Snapper"): - if hasattr(Gui.Snapper, "toolbarButtons"): - for b in Gui.Snapper.toolbarButtons: - if b.objectName() == "SnapButtonmidpoint": - b.toggle() + status = Gui.Snapper.toggle_snap('Midpoint') + # change interface consistently + sync_snap_toolbar_button("Draft_Snap_Midpoint"+"_Button", status) + sync_snap_statusbar_button("Draft_Snap_Midpoint_Statusbutton", status) Gui.addCommand('Draft_Snap_Midpoint', Draft_Snap_Midpoint()) -class Draft_Snap_Perpendicular: - """Command to snap to perdendicular of an edge.""" +class Draft_Snap_Perpendicular(gui_base.GuiCommandSimplest): + """GuiCommand for the Draft_Snap_Perpendicular tool. + + Set snapping to a direction that is perpendicular to an edge. + """ + + def __init__(self): + super(Draft_Snap_Perpendicular, self).__init__(name=_tr("Perpendicular snap")) def GetResources(self): """Set icon, menu and tooltip.""" _menu = "Perpendicular" - _tip = "Snaps to perpendicular points on edges" + _tip = "Set snapping to a direction that is perpendicular to an edge." + return {'Pixmap': 'Snap_Perpendicular', 'MenuText': QT_TRANSLATE_NOOP("Draft_Snap_Perpendicular", _menu), @@ -93,46 +183,64 @@ class Draft_Snap_Perpendicular: _tip)} def Activated(self): - """Execute this when the command is called.""" + """Execute when the command is called.""" + super(Draft_Snap_Perpendicular, self).Activated() + if hasattr(Gui, "Snapper"): - if hasattr(Gui.Snapper, "toolbarButtons"): - for b in Gui.Snapper.toolbarButtons: - if b.objectName() == "SnapButtonperpendicular": - b.toggle() + status = Gui.Snapper.toggle_snap('Perpendicular') + # change interface consistently + sync_snap_toolbar_button("Draft_Snap_Perpendicular"+"_Button", status) + sync_snap_statusbar_button("Draft_Snap_Perpendicular_Statusbutton", status) Gui.addCommand('Draft_Snap_Perpendicular', Draft_Snap_Perpendicular()) -class Draft_Snap_Grid: - """Command to snap to the intersection of grid lines.""" +class Draft_Snap_Grid(gui_base.GuiCommandSimplest): + """GuiCommand for the Draft_Snap_Grid tool. + + Set snapping to the intersection of grid lines. + """ + + def __init__(self): + super(Draft_Snap_Grid, self).__init__(name=_tr("Grid snap")) def GetResources(self): """Set icon, menu and tooltip.""" - _tip = "Snaps to grid points" + _tip = "Set snapping to the intersection of grid lines." + return {'Pixmap': 'Snap_Grid', 'MenuText': QT_TRANSLATE_NOOP("Draft_Snap_Grid", "Grid"), 'ToolTip': QT_TRANSLATE_NOOP("Draft_Snap_Grid", _tip)} def Activated(self): - """Execute this when the command is called.""" + """Execute when the command is called.""" + super(Draft_Snap_Grid, self).Activated() + if hasattr(Gui, "Snapper"): - if hasattr(Gui.Snapper, "toolbarButtons"): - for b in Gui.Snapper.toolbarButtons: - if b.objectName() == "SnapButtongrid": - b.toggle() + status = Gui.Snapper.toggle_snap('Grid') + # change interface consistently + sync_snap_toolbar_button("Draft_Snap_Grid"+"_Button", status) + sync_snap_statusbar_button("Draft_Snap_Grid_Statusbutton", status) Gui.addCommand('Draft_Snap_Grid', Draft_Snap_Grid()) -class Draft_Snap_Intersection: - """Command to snap to the intersection of two edges.""" +class Draft_Snap_Intersection(gui_base.GuiCommandSimplest): + """GuiCommand for the Draft_Snap_Intersection tool. + + Set snapping to the intersection of edges. + """ + + def __init__(self): + super(Draft_Snap_Intersection, self).__init__(name=_tr("Intersection snap")) def GetResources(self): """Set icon, menu and tooltip.""" _menu = "Intersection" - _tip = "Snaps to edges intersections" + _tip = "Set snapping to the intersection of edges." + return {'Pixmap': 'Snap_Intersection', 'MenuText': QT_TRANSLATE_NOOP("Draft_Snap_Intersection", _menu), @@ -140,227 +248,333 @@ class Draft_Snap_Intersection: _tip)} def Activated(self): - """Execute this when the command is called.""" + """Execute when the command is called.""" + super(Draft_Snap_Intersection, self).Activated() + if hasattr(Gui, "Snapper"): - if hasattr(Gui.Snapper, "toolbarButtons"): - for b in Gui.Snapper.toolbarButtons: - if b.objectName() == "SnapButtonintersection": - b.toggle() + status = Gui.Snapper.toggle_snap('Intersection') + # change interface consistently + sync_snap_toolbar_button("Draft_Snap_Intersection"+"_Button", status) + sync_snap_statusbar_button("Draft_Snap_Intersection_Statusbutton", status) Gui.addCommand('Draft_Snap_Intersection', Draft_Snap_Intersection()) -class Draft_Snap_Parallel: - """Command to snap to the parallel of an edge.""" +class Draft_Snap_Parallel(gui_base.GuiCommandSimplest): + """GuiCommand for the Draft_Snap_Parallel tool. + + Set snapping to a direction that is parallel to an edge. + """ + + def __init__(self): + super(Draft_Snap_Parallel, self).__init__(name=_tr("Parallel snap")) def GetResources(self): """Set icon, menu and tooltip.""" _menu = "Parallel" - _tip = "Snaps to parallel directions of edges" + _tip = "Set snapping to a direction that is parallel to an edge." + return {'Pixmap': 'Snap_Parallel', 'MenuText': QT_TRANSLATE_NOOP("Draft_Snap_Parallel", _menu), 'ToolTip': QT_TRANSLATE_NOOP("Draft_Snap_Parallel", _tip)} def Activated(self): - """Execute this when the command is called.""" + """Execute when the command is called.""" + super(Draft_Snap_Parallel, self).Activated() + if hasattr(Gui, "Snapper"): - if hasattr(Gui.Snapper, "toolbarButtons"): - for b in Gui.Snapper.toolbarButtons: - if b.objectName() == "SnapButtonparallel": - b.toggle() + status = Gui.Snapper.toggle_snap('Parallel') + # change interface consistently + sync_snap_toolbar_button("Draft_Snap_Parallel"+"_Button", status) + sync_snap_statusbar_button("Draft_Snap_Parallel_Statusbutton", status) Gui.addCommand('Draft_Snap_Parallel', Draft_Snap_Parallel()) -class Draft_Snap_Endpoint: - """Command to snap to an endpoint of an edge.""" +class Draft_Snap_Endpoint(gui_base.GuiCommandSimplest): + """GuiCommand for the Draft_Snap_Endpoint tool. + + Set snapping to endpoints of an edge. + """ + + def __init__(self): + super(Draft_Snap_Endpoint, self).__init__(name=_tr("Endpoint snap")) def GetResources(self): """Set icon, menu and tooltip.""" _menu = "Endpoint" - _tip = "Snaps to endpoints of edges" + _tip = "Set snapping to endpoints of an edge." + return {'Pixmap': 'Snap_Endpoint', 'MenuText': QT_TRANSLATE_NOOP("Draft_Snap_Endpoint", _menu), 'ToolTip': QT_TRANSLATE_NOOP("Draft_Snap_Endpoint", _tip)} def Activated(self): - """Execute this when the command is called.""" + """Execute when the command is called.""" + super(Draft_Snap_Endpoint, self).Activated() + if hasattr(Gui, "Snapper"): - if hasattr(Gui.Snapper, "toolbarButtons"): - for b in Gui.Snapper.toolbarButtons: - if b.objectName() == "SnapButtonendpoint": - b.toggle() + status = Gui.Snapper.toggle_snap('Endpoint') + # change interface consistently + sync_snap_toolbar_button("Draft_Snap_Endpoint"+"_Button", status) + sync_snap_statusbar_button("Draft_Snap_Endpoint_Statusbutton", status) Gui.addCommand('Draft_Snap_Endpoint', Draft_Snap_Endpoint()) -class Draft_Snap_Angle: - """Command to snap to 90 degree angles.""" +class Draft_Snap_Angle(gui_base.GuiCommandSimplest): + """GuiCommand for the Draft_Snap_Angle tool. + + Set snapping to points in a circular arc located at multiples + of 30 and 45 degree angles. + """ + + def __init__(self): + super(Draft_Snap_Angle, self).__init__(name=_tr("Angle snap (30 and 45 degrees)")) def GetResources(self): """Set icon, menu and tooltip.""" - _tip = "Snaps to 45 and 90 degrees points on arcs and circles" + _menu = "Angles (30 and 45 degrees)" + _tip = ("Set snapping to points in a circular arc located " + "at multiples of 30 and 45 degree angles.") + return {'Pixmap': 'Snap_Angle', - 'MenuText': QT_TRANSLATE_NOOP("Draft_Snap_Angle", "Angles"), + 'MenuText': QT_TRANSLATE_NOOP("Draft_Snap_Angle", _menu), 'ToolTip': QT_TRANSLATE_NOOP("Draft_Snap_Angle", _tip)} def Activated(self): - """Execute this when the command is called.""" + """Execute when the command is called.""" + super(Draft_Snap_Angle, self).Activated() + if hasattr(Gui, "Snapper"): - if hasattr(Gui.Snapper, "toolbarButtons"): - for b in Gui.Snapper.toolbarButtons: - if b.objectName() == "SnapButtonangle": - b.toggle() + status = Gui.Snapper.toggle_snap('Angle') + # change interface consistently + sync_snap_toolbar_button("Draft_Snap_Angle"+"_Button", status) + sync_snap_statusbar_button("Draft_Snap_Angle_Statusbutton", status) Gui.addCommand('Draft_Snap_Angle', Draft_Snap_Angle()) -class Draft_Snap_Center: - """Command to snap to the centers of arcs and circumferences.""" +class Draft_Snap_Center(gui_base.GuiCommandSimplest): + """GuiCommand for the Draft_Snap_Center tool. + + Set snapping to the center of a circular arc. + """ + + def __init__(self): + super(Draft_Snap_Center, self).__init__(name=_tr("Arc center snap")) def GetResources(self): """Set icon, menu and tooltip.""" - _tip = "Snaps to center of circles and arcs" + _tip = "Set snapping to the center of a circular arc." + return {'Pixmap': 'Snap_Center', 'MenuText': QT_TRANSLATE_NOOP("Draft_Snap_Center", "Center"), 'ToolTip': QT_TRANSLATE_NOOP("Draft_Snap_Center", _tip)} def Activated(self): - """Execute this when the command is called.""" + """Execute when the command is called.""" + super(Draft_Snap_Center, self).Activated() + if hasattr(Gui, "Snapper"): - if hasattr(Gui.Snapper, "toolbarButtons"): - for b in Gui.Snapper.toolbarButtons: - if b.objectName() == "SnapButtoncenter": - b.toggle() + status = Gui.Snapper.toggle_snap('Center') + # change interface consistently + sync_snap_toolbar_button("Draft_Snap_Center"+"_Button", status) + sync_snap_statusbar_button("Draft_Snap_Center_Statusbutton", status) Gui.addCommand('Draft_Snap_Center', Draft_Snap_Center()) -class Draft_Snap_Extension: - """Command to snap to the extension of an edge.""" +class Draft_Snap_Extension(gui_base.GuiCommandSimplest): + """GuiCommand for the Draft_Snap_Extension tool. + + Set snapping to the extension of an edge. + """ + + def __init__(self): + super(Draft_Snap_Extension, self).__init__(name=_tr("Edge extension snap")) def GetResources(self): """Set icon, menu and tooltip.""" _menu = "Extension" - _tip = "Snaps to extension of edges" + _tip = "Set snapping to the extension of an edge." + return {'Pixmap': 'Snap_Extension', 'MenuText': QT_TRANSLATE_NOOP("Draft_Snap_Extension", _menu), 'ToolTip': QT_TRANSLATE_NOOP("Draft_Snap_Extension", _tip)} def Activated(self): - """Execute this when the command is called.""" + """Execute when the command is called.""" + super(Draft_Snap_Extension, self).Activated() + if hasattr(Gui, "Snapper"): - if hasattr(Gui.Snapper, "toolbarButtons"): - for b in Gui.Snapper.toolbarButtons: - if b.objectName() == "SnapButtonextension": - b.toggle() + status = Gui.Snapper.toggle_snap('Extension') + # change interface consistently + sync_snap_toolbar_button("Draft_Snap_Extension"+"_Button", status) + sync_snap_statusbar_button("Draft_Snap_Extension_Statusbutton", status) Gui.addCommand('Draft_Snap_Extension', Draft_Snap_Extension()) -class Draft_Snap_Near: - """Command to snap to the nearest point of an edge.""" +class Draft_Snap_Near(gui_base.GuiCommandSimplest): + """GuiCommand for the Draft_Snap_Near tool. + + Set snapping to the nearest point of an edge. + """ + + def __init__(self): + super(Draft_Snap_Near, self).__init__(name=_tr("Near snap")) def GetResources(self): """Set icon, menu and tooltip.""" - _tip = "Snaps to nearest point on edges" + _tip = "Set snapping to the nearest point of an edge." + return {'Pixmap': 'Snap_Near', 'MenuText': QT_TRANSLATE_NOOP("Draft_Snap_Near", "Nearest"), 'ToolTip': QT_TRANSLATE_NOOP("Draft_Snap_Near", _tip)} def Activated(self): - """Execute this when the command is called.""" + """Execute when the command is called.""" + super(Draft_Snap_Near, self).Activated() + if hasattr(Gui, "Snapper"): - if hasattr(Gui.Snapper, "toolbarButtons"): - for b in Gui.Snapper.toolbarButtons: - if b.objectName() == "SnapButtonpassive": - b.toggle() + status = Gui.Snapper.toggle_snap('Near') + # change interface consistently + sync_snap_toolbar_button("Draft_Snap_Near"+"_Button", status) + sync_snap_statusbar_button("Draft_Snap_Near_Statusbutton", status) Gui.addCommand('Draft_Snap_Near', Draft_Snap_Near()) -class Draft_Snap_Ortho: - """Command to snap to the orthogonal directions.""" +class Draft_Snap_Ortho(gui_base.GuiCommandSimplest): + """GuiCommand for the Draft_Snap_Ortho tool. + + Set snapping to a direction that is a multiple of 45 degrees + from a point. + """ + + def __init__(self): + super(Draft_Snap_Ortho, self).__init__(name=_tr("Orthogonal snap")) def GetResources(self): """Set icon, menu and tooltip.""" - _tip = "Snaps to orthogonal and 45 degrees directions" + _menu = "Orthogonal angles (45 degrees)" + _tip = ("Set snapping to a direction that is a multiple " + "of 45 degrees from a point.") + return {'Pixmap': 'Snap_Ortho', - 'MenuText': QT_TRANSLATE_NOOP("Draft_Snap_Ortho", "Ortho"), + 'MenuText': QT_TRANSLATE_NOOP("Draft_Snap_Ortho", _menu), 'ToolTip': QT_TRANSLATE_NOOP("Draft_Snap_Ortho", _tip)} def Activated(self): - """Execute this when the command is called.""" + """Execute when the command is called.""" + super(Draft_Snap_Ortho, self).Activated() + if hasattr(Gui, "Snapper"): - if hasattr(Gui.Snapper, "toolbarButtons"): - for b in Gui.Snapper.toolbarButtons: - if b.objectName() == "SnapButtonortho": - b.toggle() + status = Gui.Snapper.toggle_snap('Ortho') + # change interface consistently + sync_snap_toolbar_button("Draft_Snap_Ortho"+"_Button", status) + sync_snap_statusbar_button("Draft_Snap_Ortho"+"_Statusbutton", status) Gui.addCommand('Draft_Snap_Ortho', Draft_Snap_Ortho()) -class Draft_Snap_Special: - """Command to snap to the special point of an object.""" +class Draft_Snap_Special(gui_base.GuiCommandSimplest): + """GuiCommand for the Draft_Snap_Special tool. + + Set snapping to the special points defined inside an object. + """ + + def __init__(self): + super(Draft_Snap_Special, self).__init__(name=_tr("Special point snap")) def GetResources(self): """Set icon, menu and tooltip.""" _menu = "Special" - _tip = "Snaps to special locations of objects" + _tip = "Set snapping to the special points defined inside an object." + return {'Pixmap': 'Snap_Special', 'MenuText': QT_TRANSLATE_NOOP("Draft_Snap_Special", _menu), 'ToolTip': QT_TRANSLATE_NOOP("Draft_Snap_Special", _tip)} def Activated(self): - """Execute this when the command is called.""" + """Execute when the command is called.""" + super(Draft_Snap_Special, self).Activated() + if hasattr(Gui, "Snapper"): - if hasattr(Gui.Snapper, "toolbarButtons"): - for b in Gui.Snapper.toolbarButtons: - if b.objectName() == "SnapButtonspecial": - b.toggle() + status = Gui.Snapper.toggle_snap('Special') + # change interface consistently + sync_snap_toolbar_button("Draft_Snap_Special"+"_Button", status) + sync_snap_statusbar_button("Draft_Snap_Special_Statusbutton", status) Gui.addCommand('Draft_Snap_Special', Draft_Snap_Special()) -class Draft_Snap_Dimensions: - """Command to temporary show dimensions when snapping.""" +class Draft_Snap_Dimensions(gui_base.GuiCommandSimplest): + """GuiCommand for the Draft_Snap_Dimensions tool. + + Show temporary linear dimensions when editing an object + and using other snapping methods. + """ + + def __init__(self): + super(Draft_Snap_Dimensions, self).__init__(name=_tr("Dimension display")) def GetResources(self): """Set icon, menu and tooltip.""" - _menu = "Dimensions" - _tip = "Shows temporary dimensions when snapping to Arch objects" + _menu = "Show dimensions" + _tip = ("Show temporary linear dimensions when editing an object " + "and using other snapping methods.") + return {'Pixmap': 'Snap_Dimensions', 'MenuText': QT_TRANSLATE_NOOP("Draft_Snap_Dimensions", _menu), 'ToolTip': QT_TRANSLATE_NOOP("Draft_Snap_Dimensions", _tip)} def Activated(self): - """Execute this when the command is called.""" + """Execute when the command is called.""" + super(Draft_Snap_Dimensions, self).Activated() + if hasattr(Gui, "Snapper"): - if hasattr(Gui.Snapper, "toolbarButtons"): - for b in Gui.Snapper.toolbarButtons: - if b.objectName() == "SnapButtonDimensions": - b.toggle() + status = Gui.Snapper.toggle_snap('Dimensions') + # change interface consistently + sync_snap_toolbar_button("Draft_Snap_Dimensions"+"_Button", status) + sync_snap_statusbar_button("Draft_Snap_Dimensions"+"_Statusbutton", status) Gui.addCommand('Draft_Snap_Dimensions', Draft_Snap_Dimensions()) -class Draft_Snap_WorkingPlane: - """Command to snap to a point in the current working plane.""" +class Draft_Snap_WorkingPlane(gui_base.GuiCommandSimplest): + """GuiCommand for the Draft_Snap_WorkingPlane tool. + + Restricts snapping to a point in the current working plane. + If you select a point outside the working plane, for example, + by using other snapping methods, it will snap to that point's + projection in the current working plane. + """ + + def __init__(self): + super(Draft_Snap_WorkingPlane, self).__init__(name=_tr("Working plane snap")) def GetResources(self): """Set icon, menu and tooltip.""" _menu = "Working plane" - _tip = "Restricts the snapped point to the current working plane" + _tip = ("Restricts snapping to a point in the current " + "working plane.\n" + "If you select a point outside the working plane, " + "for example, by using other snapping methods,\n" + "it will snap to that point's projection " + "in the current working plane.") + return {'Pixmap': 'Snap_WorkingPlane', 'MenuText': QT_TRANSLATE_NOOP("Draft_Snap_WorkingPlane", _menu), @@ -368,12 +582,44 @@ class Draft_Snap_WorkingPlane: _tip)} def Activated(self): - """Execute this when the command is called.""" + """Execute when the command is called.""" + super(Draft_Snap_WorkingPlane, self).Activated() + if hasattr(Gui, "Snapper"): - if hasattr(Gui.Snapper, "toolbarButtons"): - for b in Gui.Snapper.toolbarButtons: - if b.objectName() == "SnapButtonWorkingPlane": - b.toggle() + status = Gui.Snapper.toggle_snap('WorkingPlane') + # change interface consistently + sync_snap_toolbar_button("Draft_Snap_WorkingPlane"+"_Button", status) + sync_snap_statusbar_button("Draft_Snap_WorkingPlane_Statusbutton", status) Gui.addCommand('Draft_Snap_WorkingPlane', Draft_Snap_WorkingPlane()) + + +class ShowSnapBar(gui_base.GuiCommandSimplest): + """GuiCommand for the Draft_ShowSnapBar tool. + + Show the snap toolbar if it is hidden. + """ + + def __init__(self): + super(ShowSnapBar, self).__init__(name=_tr("Show snap toolbar")) + + def GetResources(self): + """Set icon, menu and tooltip.""" + _tip = "Show the snap toolbar if it is hidden." + + return {'Pixmap': 'Draft_Snap', + 'MenuText': QT_TRANSLATE_NOOP("Draft_ShowSnapBar", + "Show snap toolbar"), + 'ToolTip': QT_TRANSLATE_NOOP("Draft_ShowSnapBar", + _tip)} + + def Activated(self): + """Execute when the command is called.""" + super(ShowSnapBar, self).Activated() + + if hasattr(Gui, "Snapper"): + Gui.Snapper.show() + + +Gui.addCommand('Draft_ShowSnapBar', ShowSnapBar()) diff --git a/src/Mod/Draft/draftguitools/gui_togglemodes.py b/src/Mod/Draft/draftguitools/gui_togglemodes.py new file mode 100644 index 0000000000..954024be08 --- /dev/null +++ b/src/Mod/Draft/draftguitools/gui_togglemodes.py @@ -0,0 +1,207 @@ +# *************************************************************************** +# * (c) 2009, 2010 Yorik van Havre * +# * (c) 2009, 2010 Ken Cline * +# * (c) 2020 Eliud Cabrera Castillo * +# * * +# * This file is part of the FreeCAD CAx development system. * +# * * +# * This program is free software; you can redistribute it and/or modify * +# * it under the terms of the GNU Lesser General Public License (LGPL) * +# * as published by the Free Software Foundation; either version 2 of * +# * the License, or (at your option) any later version. * +# * for detail see the LICENCE text file. * +# * * +# * FreeCAD is distributed in the hope that it will be useful, * +# * but WITHOUT ANY WARRANTY; without even the implied warranty of * +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# * GNU Library General Public License for more details. * +# * * +# * You should have received a copy of the GNU Library General Public * +# * License along with FreeCAD; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# *************************************************************************** +"""Provides tools to control the mode of other tools in the Draft Workbench. + +For example, a construction mode, a continue mode to repeat commands, +and to toggle the appearance of certain shapes to wireframe. +""" +## @package gui_togglemodes +# \ingroup DRAFT +# \brief Provides certain mode operations of the Draft Workbench. +from PySide.QtCore import QT_TRANSLATE_NOOP + +import FreeCADGui as Gui +import Draft_rc +import draftguitools.gui_base as gui_base +from draftutils.messages import _msg +from draftutils.translate import _tr + +# The module is used to prevent complaints from code checkers (flake8) +True if Draft_rc.__name__ else False + + +class BaseMode(gui_base.GuiCommandSimplest): + """Base class for mode context GuiCommands. + + This is inherited by the other GuiCommand classes to run + a set of similar actions when changing modes. + + It inherits `GuiCommandSimplest` to set up the document + and other behavior. See this class for more information. + """ + + def Activated(self, mode="None"): + """Execute when the command is called. + + Parameters + ---------- + action: str + Indicates the type of mode to switch to. + It can be `'construction'` or `'continue'`. + """ + super().Activated() + + if hasattr(Gui, "draftToolBar"): + _ui = Gui.draftToolBar + else: + _msg(_tr("No active Draft Toolbar.")) + return + + if _ui is not None: + if mode == "construction" and hasattr(_ui, "constrButton"): + _ui.constrButton.toggle() + elif mode == "continue": + _ui.toggleContinue() + + +class ToggleConstructionMode(BaseMode): + """GuiCommand for the Draft_ToggleConstructionMode tool. + + When construction mode is active, the following objects created + will be included in the construction group, and will be drawn + with the specified color and properties. + """ + + def __init__(self): + super().__init__(name=_tr("Construction mode")) + + def GetResources(self): + """Set icon, menu and tooltip.""" + _menu = "Toggle construction mode" + _tip = ("Toggles the Construction mode.\n" + "When this is active, the following objects created " + "will be included in the construction group, " + "and will be drawn with the specified color " + "and properties.") + + d = {'Pixmap': 'Draft_Construction', + 'MenuText': QT_TRANSLATE_NOOP("Draft_ToggleConstructionMode", + _menu), + 'Accel': "C, M", + 'ToolTip': QT_TRANSLATE_NOOP("Draft_ToggleConstructionMode", + _tip)} + return d + + def Activated(self): + """Execute when the command is called. + + It calls the `toggle()` method of the construction button + in the `DraftToolbar` class. + """ + super().Activated(mode="construction") + + +Gui.addCommand('Draft_ToggleConstructionMode', ToggleConstructionMode()) + + +class ToggleContinueMode(BaseMode): + """GuiCommand for the Draft_ToggleContinueMode tool. + + When continue mode is active, any drawing tool that is terminated + will automatically start again. This can be used to draw several + objects one after the other in succession. + """ + + def __init__(self): + super().__init__(name=_tr("Continue mode")) + + def GetResources(self): + """Set icon, menu and tooltip.""" + _menu = "Toggle continue mode" + _tip = ("Toggles the Continue mode.\n" + "When this is active, any drawing tool that is terminated " + "will automatically start again.\n" + "This can be used to draw several objects " + "one after the other in succession.") + + d = {'Pixmap': 'Draft_Continue', + 'MenuText': QT_TRANSLATE_NOOP("Draft_ToggleContinueMode", + _menu), + 'ToolTip': QT_TRANSLATE_NOOP("Draft_ToggleContinueMode", + _tip)} + return d + + def Activated(self): + """Execute when the command is called. + + It calls the `toggleContinue()` method of the `DraftToolbar` class. + """ + super().Activated(mode="continue") + + +Gui.addCommand('Draft_ToggleContinueMode', ToggleContinueMode()) + + +class ToggleDisplayMode(gui_base.GuiCommandNeedsSelection): + """GuiCommand for the Draft_ToggleDisplayMode tool. + + Switches the display mode of selected objects from flatlines + to wireframe and back. + + It inherits `GuiCommandNeedsSelection` to only be available + when there is a document and a selection. + See this class for more information. + """ + + def __init__(self): + super().__init__(name=_tr("Toggle display mode")) + + def GetResources(self): + """Set icon, menu and tooltip.""" + _menu = "Toggle normal/wireframe display" + _tip = ("Switches the display mode of selected objects " + "from flatlines to wireframe and back.\n" + "This is helpful to quickly visualize objects " + "that are hidden by other objects.\n" + "This is intended to be used with closed shapes " + "and solids, and doesn't affect open wires.") + + d = {'Pixmap': 'Draft_SwitchMode', + 'Accel': "Shift+Space", + 'MenuText': QT_TRANSLATE_NOOP("Draft_ToggleDisplayMode", + _menu), + 'ToolTip': QT_TRANSLATE_NOOP("Draft_ToggleDisplayMode", + _tip)} + return d + + def Activated(self): + """Execute when the command is called. + + It tests the view provider of the selected objects + and changes their `DisplayMode` from `'Wireframe'` + to `'Flat Lines'`, and the other way around, if possible. + """ + super().Activated() + + for obj in Gui.Selection.getSelection(): + if obj.ViewObject.DisplayMode == "Flat Lines": + if "Wireframe" in obj.ViewObject.listDisplayModes(): + obj.ViewObject.DisplayMode = "Wireframe" + elif obj.ViewObject.DisplayMode == "Wireframe": + if "Flat Lines" in obj.ViewObject.listDisplayModes(): + obj.ViewObject.DisplayMode = "Flat Lines" + + +Gui.addCommand('Draft_ToggleDisplayMode', ToggleDisplayMode()) diff --git a/src/Mod/Draft/draftobjects/dimension.py b/src/Mod/Draft/draftobjects/dimension.py new file mode 100644 index 0000000000..79d1262046 --- /dev/null +++ b/src/Mod/Draft/draftobjects/dimension.py @@ -0,0 +1,397 @@ +# -*- coding: utf-8 -*- +# *************************************************************************** +# * Copyright (c) 2009, 2010 Yorik van Havre * +# * Copyright (c) 2009, 2010 Ken Cline * +# * * +# * This file is part of the FreeCAD CAx development system. * +# * * +# * This program is free software; you can redistribute it and/or modify * +# * it under the terms of the GNU Lesser General Public License (LGPL) * +# * as published by the Free Software Foundation; either version 2 of * +# * the License, or (at your option) any later version. * +# * for detail see the LICENCE text file. * +# * * +# * FreeCAD is distributed in the hope that it will be useful, * +# * but WITHOUT ANY WARRANTY; without even the implied warranty of * +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# * GNU Library General Public License for more details. * +# * * +# * You should have received a copy of the GNU Library General Public * +# * License along with FreeCAD; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# *************************************************************************** +"""This module provides the object code for Draft Dimension. +""" +## @package dimension +# \ingroup DRAFT +# \brief This module provides the object code for Draft Dimension. + +import FreeCAD as App +import math +from PySide.QtCore import QT_TRANSLATE_NOOP +import DraftGeomUtils, DraftVecUtils +import draftutils.gui_utils as gui_utils +import draftutils.utils as utils +from draftobjects.draft_annotation import DraftAnnotation + +if App.GuiUp: + from draftviewproviders.view_dimension import ViewProviderDimensionBase + from draftviewproviders.view_dimension import ViewProviderLinearDimension + from draftviewproviders.view_dimension import ViewProviderAngularDimension + +def make_dimension(p1,p2,p3=None,p4=None): + """makeDimension(p1,p2,[p3]) or makeDimension(object,i1,i2,p3) + or makeDimension(objlist,indices,p3): Creates a Dimension object with + the dimension line passign through p3.The current line width and color + will be used. There are multiple ways to create a dimension, depending on + the arguments you pass to it: + - (p1,p2,p3): creates a standard dimension from p1 to p2 + - (object,i1,i2,p3): creates a linked dimension to the given object, + measuring the distance between its vertices indexed i1 and i2 + - (object,i1,mode,p3): creates a linked dimension + to the given object, i1 is the index of the (curved) edge to measure, + and mode is either "radius" or "diameter". + """ + if not App.ActiveDocument: + App.Console.PrintError("No active document. Aborting\n") + return + obj = App.ActiveDocument.addObject("App::FeaturePython","Dimension") + LinearDimension(obj) + if App.GuiUp: + ViewProviderLinearDimension(obj.ViewObject) + if isinstance(p1,App.Vector) and isinstance(p2,App.Vector): + obj.Start = p1 + obj.End = p2 + if not p3: + p3 = p2.sub(p1) + p3.multiply(0.5) + p3 = p1.add(p3) + elif isinstance(p2,int) and isinstance(p3,int): + l = [] + idx = (p2,p3) + l.append((p1,"Vertex"+str(p2+1))) + l.append((p1,"Vertex"+str(p3+1))) + obj.LinkedGeometry = l + obj.Support = p1 + p3 = p4 + if not p3: + v1 = obj.Base.Shape.Vertexes[idx[0]].Point + v2 = obj.Base.Shape.Vertexes[idx[1]].Point + p3 = v2.sub(v1) + p3.multiply(0.5) + p3 = v1.add(p3) + elif isinstance(p3,str): + l = [] + l.append((p1,"Edge"+str(p2+1))) + if p3 == "radius": + #l.append((p1,"Center")) + if App.GuiUp: + obj.ViewObject.Override = "R $dim" + obj.Diameter = False + elif p3 == "diameter": + #l.append((p1,"Diameter")) + if App.GuiUp: + obj.ViewObject.Override = "Ø $dim" + obj.Diameter = True + obj.LinkedGeometry = l + obj.Support = p1 + p3 = p4 + if not p3: + p3 = p1.Shape.Edges[p2].Curve.Center.add(App.Vector(1,0,0)) + obj.Dimline = p3 + if hasattr(App,"DraftWorkingPlane"): + normal = App.DraftWorkingPlane.axis + else: + normal = App.Vector(0,0,1) + if App.GuiUp: + # invert the normal if we are viewing it from the back + vnorm = gui_utils.get3DView().getViewDirection() + if vnorm.getAngle(normal) < math.pi/2: + normal = normal.negative() + obj.Normal = normal + + if App.GuiUp: + gui_utils.format_object(obj) + gui_utils.select(obj) + + return obj + + +def make_angular_dimension(center,angles,p3,normal=None): + """makeAngularDimension(center,angle1,angle2,p3,[normal]): creates an angular Dimension + from the given center, with the given list of angles, passing through p3. + """ + if not App.ActiveDocument: + App.Console.PrintError("No active document. Aborting\n") + return + obj = App.ActiveDocument.addObject("App::FeaturePython","Dimension") + AngularDimension(obj) + if App.GuiUp: + ViewProviderAngularDimension(obj.ViewObject) + obj.Center = center + for a in range(len(angles)): + if angles[a] > 2*math.pi: + angles[a] = angles[a]-(2*math.pi) + obj.FirstAngle = math.degrees(angles[1]) + obj.LastAngle = math.degrees(angles[0]) + obj.Dimline = p3 + if not normal: + if hasattr(App,"DraftWorkingPlane"): + normal = App.DraftWorkingPlane.axis + else: + normal = App.Vector(0,0,1) + if App.GuiUp: + # invert the normal if we are viewing it from the back + vnorm = gui_utils.get3DView().getViewDirection() + if vnorm.getAngle(normal) < math.pi/2: + normal = normal.negative() + + obj.Normal = normal + + if App.GuiUp: + gui_utils.format_object(obj) + gui_utils.select(obj) + + return obj + + + +class DimensionBase(DraftAnnotation): + """ + The Draft Dimension Base object + This class is not used directly, but inherited by all dimension + objects. + """ + + def __init__(self, obj, tp = "Dimension"): + """Add common dimension properties to the object and set them""" + + super(DimensionBase, self).__init__(obj, tp) + + # Draft + obj.addProperty("App::PropertyVector", + "Normal", + "Draft", + QT_TRANSLATE_NOOP("App::Property", + "The normal direction of this dimension")) + + obj.addProperty("App::PropertyLink", + "Support", + "Draft", + QT_TRANSLATE_NOOP("App::Property", + "The object measured by this dimension")) + + obj.addProperty("App::PropertyLinkSubList", + "LinkedGeometry", + "Draft", + QT_TRANSLATE_NOOP("App::Property", + "The geometry this dimension is linked to")) + + obj.addProperty("App::PropertyVectorDistance", + "Dimline", + "Draft", + QT_TRANSLATE_NOOP("App::Property", + "Point on which the dimension \n" + "line is placed.")) + + obj.Dimline = App.Vector(0,1,0) + obj.Normal = App.Vector(0,0,1) + + + def execute(self, obj): + '''Do something when recompute object''' + + return + + + def onChanged(self,obj,prop): + '''Do something when a property has changed''' + + return + + +class LinearDimension(DimensionBase): + """ + The Draft Linear Dimension object + """ + + def __init__(self, obj): + + super(LinearDimension, self).__init__(obj, "LinearDimension") + + obj.Proxy = self + + self.init_properties(obj) + + + def init_properties(self, obj): + """Add Linear Dimension specific properties to the object and set them""" + + # Draft + obj.addProperty("App::PropertyVectorDistance", + "Start", + "Draft", + QT_TRANSLATE_NOOP("App::Property", + "Startpoint of dimension")) + + obj.addProperty("App::PropertyVectorDistance", + "End", + "Draft", + QT_TRANSLATE_NOOP("App::Property", + "Endpoint of dimension")) + + obj.addProperty("App::PropertyVector", + "Direction", + "Draft", + QT_TRANSLATE_NOOP("App::Property", + "The normal direction of this dimension")) + + obj.addProperty("App::PropertyLength", + "Distance", + "Draft", + QT_TRANSLATE_NOOP("App::Property", + "The measurement of this dimension")) + + obj.addProperty("App::PropertyBool", + "Diameter", + "Draft", + QT_TRANSLATE_NOOP("App::Property", + "For arc/circle measurements, false = radius, true = diameter")) + + obj.Start = App.Vector(0,0,0) + obj.End = App.Vector(1,0,0) + + def onChanged(self,obj,prop): + '''Do something when a property has changed''' + if hasattr(obj, "Distance"): + obj.setEditorMode('Distance', 1) + #if hasattr(obj,"Normal"): + # obj.setEditorMode('Normal', 2) + if hasattr(obj, "Support"): + obj.setEditorMode('Support', 2) + + + def execute(self, obj): + """ Set start point and end point according to the linked geometry""" + if obj.LinkedGeometry: + if len(obj.LinkedGeometry) == 1: + lobj = obj.LinkedGeometry[0][0] + lsub = obj.LinkedGeometry[0][1] + if len(lsub) == 1: + if "Edge" in lsub[0]: + n = int(lsub[0][4:])-1 + edge = lobj.Shape.Edges[n] + if DraftGeomUtils.geomType(edge) == "Line": + obj.Start = edge.Vertexes[0].Point + obj.End = edge.Vertexes[-1].Point + elif DraftGeomUtils.geomType(edge) == "Circle": + c = edge.Curve.Center + r = edge.Curve.Radius + a = edge.Curve.Axis + ray = obj.Dimline.sub(c).projectToPlane(App.Vector(0,0,0),a) + if (ray.Length == 0): + ray = a.cross(App.Vector(1,0,0)) + if (ray.Length == 0): + ray = a.cross(App.Vector(0,1,0)) + ray = DraftVecUtils.scaleTo(ray,r) + if hasattr(obj,"Diameter"): + if obj.Diameter: + obj.Start = c.add(ray.negative()) + obj.End = c.add(ray) + else: + obj.Start = c + obj.End = c.add(ray) + elif len(lsub) == 2: + if ("Vertex" in lsub[0]) and ("Vertex" in lsub[1]): + n1 = int(lsub[0][6:])-1 + n2 = int(lsub[1][6:])-1 + obj.Start = lobj.Shape.Vertexes[n1].Point + obj.End = lobj.Shape.Vertexes[n2].Point + elif len(obj.LinkedGeometry) == 2: + lobj1 = obj.LinkedGeometry[0][0] + lobj2 = obj.LinkedGeometry[1][0] + lsub1 = obj.LinkedGeometry[0][1] + lsub2 = obj.LinkedGeometry[1][1] + if (len(lsub1) == 1) and (len(lsub2) == 1): + if ("Vertex" in lsub1[0]) and ("Vertex" in lsub2[1]): + n1 = int(lsub1[0][6:])-1 + n2 = int(lsub2[0][6:])-1 + obj.Start = lobj1.Shape.Vertexes[n1].Point + obj.End = lobj2.Shape.Vertexes[n2].Point + # set the distance property + total_len = (obj.Start.sub(obj.End)).Length + if round(obj.Distance.Value, utils.precision()) != round(total_len, utils.precision()): + obj.Distance = total_len + if App.GuiUp: + if obj.ViewObject: + obj.ViewObject.update() + + + +class AngularDimension(DimensionBase): + """ + The Draft AngularDimension object + """ + + def __init__(self, obj): + + super(AngularDimension, self).__init__(obj, "AngularDimension") + + self.init_properties(obj) + + obj.Proxy = self + + + def init_properties(self, obj): + """Add Angular Dimension specific properties to the object and set them""" + + obj.addProperty("App::PropertyAngle", + "FirstAngle", + "Draft", + QT_TRANSLATE_NOOP("App::Property", + "Start angle of the dimension")) + + obj.addProperty("App::PropertyAngle", + "LastAngle", + "Draft", + QT_TRANSLATE_NOOP("App::Property", + "End angle of the dimension")) + + obj.addProperty("App::PropertyVectorDistance", + "Center", + "Draft", + QT_TRANSLATE_NOOP("App::Property", + "The center point of this dimension")) + + obj.addProperty("App::PropertyAngle", + "Angle", + "Draft", + QT_TRANSLATE_NOOP("App::Property", + "The measurement of this dimension")) + + obj.FirstAngle = 0 + obj.LastAngle = 90 + obj.Dimline = App.Vector(0,1,0) + obj.Center = App.Vector(0,0,0) + obj.Normal = App.Vector(0,0,1) + + + def execute(self, fp): + '''Do something when recompute object''' + if fp.ViewObject: + fp.ViewObject.update() + + + def onChanged(self,obj,prop): + '''Do something when a property has changed''' + super().onChanged(obj, prop) + if hasattr(obj,"Angle"): + obj.setEditorMode('Angle',1) + if hasattr(obj,"Normal"): + obj.setEditorMode('Normal',2) + if hasattr(obj,"Support"): + obj.setEditorMode('Support',2) + + diff --git a/src/Mod/Draft/draftobjects/draft_annotation.py b/src/Mod/Draft/draftobjects/draft_annotation.py new file mode 100644 index 0000000000..1ee3ab5432 --- /dev/null +++ b/src/Mod/Draft/draftobjects/draft_annotation.py @@ -0,0 +1,64 @@ +# *************************************************************************** +# * (c) 2020 Carlo Pavan * +# * * +# * This file is part of the FreeCAD CAx development system. * +# * * +# * This program is free software; you can redistribute it and/or modify * +# * it under the terms of the GNU Lesser General Public License (LGPL) * +# * as published by the Free Software Foundation; either version 2 of * +# * the License, or (at your option) any later version. * +# * for detail see the LICENCE text file. * +# * * +# * FreeCAD is distributed in the hope that it will be useful, * +# * but WITHOUT ANY WARRANTY; without even the implied warranty of * +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# * GNU Library General Public License for more details. * +# * * +# * You should have received a copy of the GNU Library General Public * +# * License along with FreeCAD; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# *************************************************************************** + +"""This module provides the object code for Draft Annotation. +""" +## @package annotation +# \ingroup DRAFT +# \brief This module provides the object code for Draft Annotation. + +import FreeCAD as App +from PySide.QtCore import QT_TRANSLATE_NOOP +from draftutils import gui_utils + +class DraftAnnotation(object): + """The Draft Annotation Base object + This class is not used directly, but inherited by all annotation + objects. + """ + def __init__(self, obj, tp="Annotation"): + """Add general Annotation properties to the object""" + + self.Type = tp + + + def __getstate__(self): + return self.Type + + + def __setstate__(self,state): + if state: + self.Type = state + + + def execute(self,obj): + '''Do something when recompute object''' + + return + + + def onChanged(self, obj, prop): + '''Do something when a property has changed''' + + return + diff --git a/src/Mod/Draft/draftobjects/label.py b/src/Mod/Draft/draftobjects/label.py new file mode 100644 index 0000000000..e025c30703 --- /dev/null +++ b/src/Mod/Draft/draftobjects/label.py @@ -0,0 +1,239 @@ +# -*- coding: utf-8 -*- +# *************************************************************************** +# * Copyright (c) 2009, 2010 Yorik van Havre * +# * Copyright (c) 2009, 2010 Ken Cline * +# * * +# * This file is part of the FreeCAD CAx development system. * +# * * +# * This program is free software; you can redistribute it and/or modify * +# * it under the terms of the GNU Lesser General Public License (LGPL) * +# * as published by the Free Software Foundation; either version 2 of * +# * the License, or (at your option) any later version. * +# * for detail see the LICENCE text file. * +# * * +# * FreeCAD is distributed in the hope that it will be useful, * +# * but WITHOUT ANY WARRANTY; without even the implied warranty of * +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# * GNU Library General Public License for more details. * +# * * +# * You should have received a copy of the GNU Library General Public * +# * License along with FreeCAD; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# *************************************************************************** + +"""This module provides the object code for Draft Label. +""" +## @package label +# \ingroup DRAFT +# \brief This module provides the object code for Draft Label. + +import FreeCAD as App +import math +from PySide.QtCore import QT_TRANSLATE_NOOP +import DraftGeomUtils +import draftutils.gui_utils as gui_utils +import draftutils.utils as utils +from draftobjects.draft_annotation import DraftAnnotation + +if App.GuiUp: + from draftviewproviders.view_label import ViewProviderLabel + + + +def make_label(targetpoint=None, target=None, direction=None, + distance=None, labeltype=None, placement=None): + """ + make_label(targetpoint, target, direction, distance, labeltype, placement) + + Function to create a Draft Label annotation object + + Parameters + ---------- + targetpoint : App::Vector + To be completed + + target : LinkSub + To be completed + + direction : String + Straight direction of the label + ["Horizontal","Vertical","Custom"] + + distance : Quantity + Length of the straight segment of label leader line + + labeltype : String + Label type in + ["Custom","Name","Label","Position", + "Length","Area","Volume","Tag","Material"] + + placement : Base::Placement + To be completed + + Returns + ------- + obj : App::DocumentObject + Newly created label object + """ + obj = App.ActiveDocument.addObject("App::FeaturePython", + "dLabel") + Label(obj) + if App.GuiUp: + ViewProviderLabel(obj.ViewObject) + if targetpoint: + obj.TargetPoint = targetpoint + if target: + obj.Target = target + if direction: + obj.StraightDirection = direction + if distance: + obj.StraightDistance = distance + if labeltype: + obj.LabelType = labeltype + if placement: + obj.Placement = placement + + if App.GuiUp: + gui_utils.format_object(obj) + gui_utils.select(obj) + + return obj + + + +class Label(DraftAnnotation): + """The Draft Label object""" + + def __init__(self, obj): + + super(Label, self).__init__(obj, "Label") + + self.init_properties(obj) + + obj.Proxy = self + + + def init_properties(self, obj): + """Add properties to the object and set them""" + + obj.addProperty("App::PropertyPlacement", + "Placement", + "Base", + QT_TRANSLATE_NOOP("App::Property", + "The placement of this object")) + + obj.addProperty("App::PropertyDistance", + "StraightDistance", + "Base", + QT_TRANSLATE_NOOP("App::Property", + "The length of the straight segment")) + + obj.addProperty("App::PropertyVector", + "TargetPoint", + "Base", + QT_TRANSLATE_NOOP("App::Property", + "The point indicated by this label")) + + obj.addProperty("App::PropertyVectorList", + "Points", + "Base", + QT_TRANSLATE_NOOP("App::Property", + "The points defining the label polyline")) + + obj.addProperty("App::PropertyEnumeration", + "StraightDirection", + "Base", + QT_TRANSLATE_NOOP("App::Property", + "The direction of the straight segment")) + + obj.addProperty("App::PropertyEnumeration", + "LabelType", + "Base", + QT_TRANSLATE_NOOP("App::Property", + "The type of information shown by this label")) + + obj.addProperty("App::PropertyLinkSub", + "Target", + "Base", + QT_TRANSLATE_NOOP("App::Property", + "The target object of this label")) + + obj.addProperty("App::PropertyStringList", + "CustomText", + "Base", + QT_TRANSLATE_NOOP("App::Property", + "The text to display when type is set to custom")) + + obj.addProperty("App::PropertyStringList", + "Text", + "Base", + QT_TRANSLATE_NOOP("App::Property", + "The text displayed by this label")) + + obj.StraightDirection = ["Horizontal","Vertical","Custom"] + obj.LabelType = ["Custom","Name","Label","Position", + "Length","Area","Volume","Tag","Material"] + obj.setEditorMode("Text",1) + obj.StraightDistance = 1 + obj.TargetPoint = App.Vector(2,-1,0) + obj.CustomText = "Label" + + + def execute(self,obj): + '''Do something when recompute object''' + + if obj.StraightDirection != "Custom": + p1 = obj.Placement.Base + if obj.StraightDirection == "Horizontal": + p2 = App.Vector(obj.StraightDistance.Value,0,0) + else: + p2 = App.Vector(0,obj.StraightDistance.Value,0) + p2 = obj.Placement.multVec(p2) + # p3 = obj.Placement.multVec(obj.TargetPoint) + p3 = obj.TargetPoint + obj.Points = [p1,p2,p3] + if obj.LabelType == "Custom": + if obj.CustomText: + obj.Text = obj.CustomText + elif obj.Target and obj.Target[0]: + if obj.LabelType == "Name": + obj.Text = [obj.Target[0].Name] + elif obj.LabelType == "Label": + obj.Text = [obj.Target[0].Label] + elif obj.LabelType == "Tag": + if hasattr(obj.Target[0],"Tag"): + obj.Text = [obj.Target[0].Tag] + elif obj.LabelType == "Material": + if hasattr(obj.Target[0],"Material"): + if hasattr(obj.Target[0].Material,"Label"): + obj.Text = [obj.Target[0].Material.Label] + elif obj.LabelType == "Position": + p = obj.Target[0].Placement.Base + if obj.Target[1]: + if "Vertex" in obj.Target[1][0]: + p = obj.Target[0].Shape.Vertexes[int(obj.Target[1][0][6:])-1].Point + obj.Text = [App.Units.Quantity(x,App.Units.Length).UserString for x in tuple(p)] + elif obj.LabelType == "Length": + if hasattr(obj.Target[0],'Shape'): + if hasattr(obj.Target[0].Shape,"Length"): + obj.Text = [App.Units.Quantity(obj.Target[0].Shape.Length,App.Units.Length).UserString] + if obj.Target[1] and ("Edge" in obj.Target[1][0]): + obj.Text = [App.Units.Quantity(obj.Target[0].Shape.Edges[int(obj.Target[1][0][4:])-1].Length,App.Units.Length).UserString] + elif obj.LabelType == "Area": + if hasattr(obj.Target[0],'Shape'): + if hasattr(obj.Target[0].Shape,"Area"): + obj.Text = [App.Units.Quantity(obj.Target[0].Shape.Area,App.Units.Area).UserString.replace("^2","²")] + if obj.Target[1] and ("Face" in obj.Target[1][0]): + obj.Text = [App.Units.Quantity(obj.Target[0].Shape.Faces[int(obj.Target[1][0][4:])-1].Area,App.Units.Area).UserString] + elif obj.LabelType == "Volume": + if hasattr(obj.Target[0],'Shape'): + if hasattr(obj.Target[0].Shape,"Volume"): + obj.Text = [App.Units.Quantity(obj.Target[0].Shape.Volume,App.Units.Volume).UserString.replace("^3","³")] + + + def onChanged(self,obj,prop): + '''Do something when a property has changed''' + + return \ No newline at end of file diff --git a/src/Mod/Draft/draftobjects/text.py b/src/Mod/Draft/draftobjects/text.py new file mode 100644 index 0000000000..4ad02b3488 --- /dev/null +++ b/src/Mod/Draft/draftobjects/text.py @@ -0,0 +1,131 @@ +# *************************************************************************** +# * Copyright (c) 2009, 2010 Yorik van Havre * +# * Copyright (c) 2009, 2010 Ken Cline * +# * * +# * This file is part of the FreeCAD CAx development system. * +# * * +# * This program is free software; you can redistribute it and/or modify * +# * it under the terms of the GNU Lesser General Public License (LGPL) * +# * as published by the Free Software Foundation; either version 2 of * +# * the License, or (at your option) any later version. * +# * for detail see the LICENCE text file. * +# * * +# * FreeCAD is distributed in the hope that it will be useful, * +# * but WITHOUT ANY WARRANTY; without even the implied warranty of * +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# * GNU Library General Public License for more details. * +# * * +# * You should have received a copy of the GNU Library General Public * +# * License along with FreeCAD; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# *************************************************************************** + +"""This module provides the object code for Draft Text. +""" +## @package text +# \ingroup DRAFT +# \brief This module provides the object code for Draft Text. + +import FreeCAD as App +import math +from PySide.QtCore import QT_TRANSLATE_NOOP +import DraftGeomUtils +import draftutils.gui_utils as gui_utils +import draftutils.utils as utils +from draftobjects.draft_annotation import DraftAnnotation + +if App.GuiUp: + from draftviewproviders.view_text import ViewProviderText + + + +def make_text(stringslist, point=App.Vector(0,0,0), screen=False): + """makeText(strings, point, screen) + + Creates a Text object containing the given strings. + The current color and text height and font + specified in preferences are used. + + Parameters + ---------- + stringlist : List + Given list of strings, one string by line (strings can also + be one single string) + + point : App::Vector + insert point of the text + + screen : Bool + If screen is True, the text always faces the view direction. + + """ + + if not App.ActiveDocument: + App.Console.PrintError("No active document. Aborting\n") + return + + utils.type_check([(point, App.Vector)], "makeText") + if not isinstance(stringslist,list): stringslist = [stringslist] + + obj = App.ActiveDocument.addObject("App::FeaturePython","Text") + Text(obj) + obj.Text = stringslist + obj.Placement.Base = point + + if App.GuiUp: + ViewProviderText(obj.ViewObject) + if screen: + obj.ViewObject.DisplayMode = "3D text" + h = utils.get_param("textheight",0.20) + if screen: + h = h*10 + obj.ViewObject.FontSize = h + obj.ViewObject.FontName = utils.get_param("textfont","") + obj.ViewObject.LineSpacing = 1 + gui_utils.format_object(obj) + gui_utils.select(obj) + + return obj + + + +class Text(DraftAnnotation): + """The Draft Text object""" + + def __init__(self, obj): + + super(Text, self).__init__(obj, "Text") + + self.init_properties(obj) + + obj.Proxy = self + + + def init_properties(self, obj): + """Add Text specific properties to the object and set them""" + + obj.addProperty("App::PropertyPlacement", + "Placement", + "Base", + QT_TRANSLATE_NOOP("App::Property", + "The placement of this object")) + + obj.addProperty("App::PropertyStringList", + "Text", + "Base", + QT_TRANSLATE_NOOP("App::Property", + "The text displayed by this object")) + + + def execute(self,obj): + '''Do something when recompute object''' + + return + + + def onChanged(self,obj,prop): + '''Do something when a property has changed''' + + return diff --git a/src/Mod/Draft/drafttaskpanels/task_circulararray.py b/src/Mod/Draft/drafttaskpanels/task_circulararray.py index d1e7d4327d..bcab6bf807 100644 --- a/src/Mod/Draft/drafttaskpanels/task_circulararray.py +++ b/src/Mod/Draft/drafttaskpanels/task_circulararray.py @@ -1,9 +1,3 @@ -"""This module provides the task panel for the Draft CircularArray tool. -""" -## @package task_circulararray -# \ingroup DRAFT -# \brief This module provides the task panel code for the CircularArray tool. - # *************************************************************************** # * (c) 2019 Eliud Cabrera Castillo * # * * @@ -26,183 +20,257 @@ # * USA * # * * # *************************************************************************** +"""Provides the task panel code for the Draft CircularArray tool.""" +## @package task_circulararray +# \ingroup DRAFT +# \brief This module provides the task panel code for the CircularArray tool. + +import PySide.QtGui as QtGui +from PySide.QtCore import QT_TRANSLATE_NOOP import FreeCAD as App import FreeCADGui as Gui -# import Draft -import Draft_rc +import Draft_rc # include resources, icons, ui files import DraftVecUtils +import draftutils.utils as utils +from draftutils.messages import _msg, _wrn, _err, _log +from draftutils.translate import _tr +from FreeCAD import Units as U -import PySide.QtCore as QtCore -import PySide.QtGui as QtGui -from PySide.QtCore import QT_TRANSLATE_NOOP -# import DraftTools -from DraftGui import translate -# from DraftGui import displayExternal - -_Quantity = App.Units.Quantity - - -def _Msg(text, end="\n"): - """Print message with newline""" - App.Console.PrintMessage(text + end) - - -def _Wrn(text, end="\n"): - """Print warning with newline""" - App.Console.PrintWarning(text + end) - - -def _tr(text): - """Function to translate with the context set""" - return translate("Draft", text) - - -# So the resource file doesn't trigger errors from code checkers (flake8) -True if Draft_rc.__name__ else False +# The module is used to prevent complaints from code checkers (flake8) +bool(Draft_rc.__name__) class TaskPanelCircularArray: """TaskPanel code for the CircularArray command. The names of the widgets are defined in the `.ui` file. - In this class all those widgets are automatically created - under the name `self.form.` + This `.ui` file `must` be loaded into an attribute + called `self.form` so that it is loaded into the task panel correctly. + + In this class all widgets are automatically created + as `self.form.`. The `.ui` file may use special FreeCAD widgets such as `Gui::InputField` (based on `QLineEdit`) and `Gui::QuantitySpinBox` (based on `QAbstractSpinBox`). See the Doxygen documentation of the corresponding files in `src/Gui/`, for example, `InputField.h` and `QuantitySpinBox.h`. + + Attributes + ---------- + source_command: gui_base.GuiCommandBase + This attribute holds a reference to the calling class + of this task panel. + This parent class, which is derived from `gui_base.GuiCommandBase`, + is responsible for calling this task panel, for installing + certain callbacks, and for removing them. + + It also delays the execution of the internal creation commands + by using the `draftutils.todo.ToDo` class. + + See Also + -------- + * https://forum.freecadweb.org/viewtopic.php?f=10&t=40007 + * https://forum.freecadweb.org/viewtopic.php?t=5374#p43038 """ def __init__(self): + self.name = "Circular array" + _log(_tr("Task panel:") + "{}".format(_tr(self.name))) + + # The .ui file must be loaded into an attribute + # called `self.form` so that it is displayed in the task panel. ui_file = ":/ui/TaskPanel_CircularArray.ui" self.form = Gui.PySideUic.loadUi(ui_file) - self.name = self.form.windowTitle() icon_name = "Draft_CircularArray" svg = ":/icons/" + icon_name pix = QtGui.QPixmap(svg) icon = QtGui.QIcon.fromTheme(icon_name, QtGui.QIcon(svg)) self.form.setWindowIcon(icon) + self.form.setWindowTitle(_tr(self.name)) + self.form.label_icon.setPixmap(pix.scaled(32, 32)) - start_distance = _Quantity(1000.0, App.Units.Length) - distance_unit = start_distance.getUserPreferred()[2] - self.form.spinbox_r_distance.setProperty('rawValue', - 2 * start_distance.Value) - self.form.spinbox_r_distance.setProperty('unit', distance_unit) - self.form.spinbox_tan_distance.setProperty('rawValue', - start_distance.Value) - self.form.spinbox_tan_distance.setProperty('unit', distance_unit) + # ------------------------------------------------------------------- + # Default values for the internal function, + # and for the task panel interface + start_distance = U.Quantity(50.0, App.Units.Length) + length_unit = start_distance.getUserPreferred()[2] self.r_distance = 2 * start_distance.Value self.tan_distance = start_distance.Value - self.form.spinbox_number.setValue(3) - self.form.spinbox_symmetry.setValue(1) + self.form.spinbox_r_distance.setProperty('rawValue', + self.r_distance) + self.form.spinbox_r_distance.setProperty('unit', length_unit) + self.form.spinbox_tan_distance.setProperty('rawValue', + self.tan_distance) + self.form.spinbox_tan_distance.setProperty('unit', length_unit) - self.number = self.form.spinbox_number.value() - self.symmetry = self.form.spinbox_symmetry.value() + self.number = 3 + self.symmetry = 1 + self.form.spinbox_number.setValue(self.number) + self.form.spinbox_symmetry.setValue(self.symmetry) + + # TODO: the axis is currently fixed, it should be editable + # or selectable from the task panel self.axis = App.Vector(0, 0, 1) - start_point = _Quantity(0.0, App.Units.Length) + start_point = U.Quantity(0.0, App.Units.Length) length_unit = start_point.getUserPreferred()[2] - self.form.input_c_x.setProperty('rawValue', start_point.Value) + + self.center = App.Vector(start_point.Value, + start_point.Value, + start_point.Value) + + self.form.input_c_x.setProperty('rawValue', self.center.x) self.form.input_c_x.setProperty('unit', length_unit) - self.form.input_c_y.setProperty('rawValue', start_point.Value) + self.form.input_c_y.setProperty('rawValue', self.center.y) self.form.input_c_y.setProperty('unit', length_unit) - self.form.input_c_z.setProperty('rawValue', start_point.Value) + self.form.input_c_z.setProperty('rawValue', self.center.z) self.form.input_c_z.setProperty('unit', length_unit) - self.valid_input = True - self.c_x_str = "" - self.c_y_str = "" - self.c_z_str = "" - self.center = App.Vector(0, 0, 0) + self.fuse = utils.get_param("Draft_array_fuse", False) + self.use_link = utils.get_param("Draft_array_Link", True) - # Old style for Qt4 - # QtCore.QObject.connect(self.form.button_reset, - # QtCore.SIGNAL("clicked()"), - # self.reset_point) - # New style for Qt5 - self.form.button_reset.clicked.connect(self.reset_point) + self.form.checkbox_fuse.setChecked(self.fuse) + self.form.checkbox_link.setChecked(self.use_link) + # ------------------------------------------------------------------- + + # Some objects need to be selected before we can execute the function. + self.selection = None + + # This is used to test the input of the internal function. + # It should be changed to True before we can execute the function. + self.valid_input = False + + self.set_widget_callbacks() + + self.tr_true = QT_TRANSLATE_NOOP("Draft", "True") + self.tr_false = QT_TRANSLATE_NOOP("Draft", "False") # The mask is not used at the moment, but could be used in the future # by a callback to restrict the coordinates of the pointer. self.mask = "" - # When the checkbox changes, change the fuse value - self.fuse = False - QtCore.QObject.connect(self.form.checkbox_fuse, - QtCore.SIGNAL("stateChanged(int)"), - self.set_fuse) + def set_widget_callbacks(self): + """Set up the callbacks (slots) for the widget signals.""" + # New style for Qt5 + self.form.button_reset.clicked.connect(self.reset_point) - self.use_link = False - QtCore.QObject.connect(self.form.checkbox_link, - QtCore.SIGNAL("stateChanged(int)"), - self.set_link) + # When the checkbox changes, change the internal value + self.form.checkbox_fuse.stateChanged.connect(self.set_fuse) + self.form.checkbox_link.stateChanged.connect(self.set_link) + + # Old style for Qt4, avoid! + # QtCore.QObject.connect(self.form.button_reset, + # QtCore.SIGNAL("clicked()"), + # self.reset_point) + # QtCore.QObject.connect(self.form.checkbox_fuse, + # QtCore.SIGNAL("stateChanged(int)"), + # self.set_fuse) + # QtCore.QObject.connect(self.form.checkbox_link, + # QtCore.SIGNAL("stateChanged(int)"), + # self.set_link) def accept(self): - """Function that executes when clicking the OK button""" - selection = Gui.Selection.getSelection() - self.number = self.form.spinbox_number.value() + """Execute when clicking the OK button or Enter key.""" + self.selection = Gui.Selection.getSelection() - tan_d_str = self.form.spinbox_tan_distance.text() - self.tan_distance = _Quantity(tan_d_str).Value - self.valid_input = self.validate_input(selection, + (self.r_distance, + self.tan_distance) = self.get_distances() + + (self.number, + self.symmetry) = self.get_number_symmetry() + + self.axis = self.get_axis() + self.center = self.get_center() + + self.valid_input = self.validate_input(self.selection, + self.r_distance, + self.tan_distance, self.number, - self.tan_distance) + self.symmetry, + self.axis, + self.center) if self.valid_input: - self.create_object(selection) - self.print_messages(selection) + self.create_object() + self.print_messages() self.finish() - def validate_input(self, selection, number, tan_distance): - """Check that the input is valid""" + def validate_input(self, selection, + r_distance, tan_distance, + number, symmetry, + axis, center): + """Check that the input is valid. + + Some values may not need to be checked because + the interface may not allow to input wrong data. + """ if not selection: - _Wrn(_tr("At least one element must be selected")) + _err(_tr("At least one element must be selected.")) return False + if number < 2: - _Wrn(_tr("Number of elements must be at least 2")) + _err(_tr("Number of layers must be at least 2.")) return False - # Todo: each of the elements of the selection could be tested, - # not only the first one. - if selection[0].isDerivedFrom("App::FeaturePython"): - _Wrn(_tr("Selection is not suitable for array")) - _Wrn(_tr("Object:") + " {}".format(selection[0].Label)) + + # TODO: this should handle multiple objects. + # Each of the elements of the selection should be tested. + obj = selection[0] + if obj.isDerivedFrom("App::FeaturePython"): + _err(_tr("Selection is not suitable for array.")) + _err(_tr("Object:") + " {}".format(selection[0].Label)) return False + + if r_distance == 0: + _wrn(_tr("Radial distance is zero. " + "Resulting array may not look correct.")) + elif r_distance < 0: + _wrn(_tr("Radial distance is negative. " + "It is made positive to proceed.")) + self.r_distance = abs(r_distance) + if tan_distance == 0: - _Wrn(_tr("Tangential distance cannot be zero")) + _err(_tr("Tangential distance cannot be zero.")) return False - return True + elif tan_distance < 0: + _wrn(_tr("Tangential distance is negative. " + "It is made positive to proceed.")) + self.tan_distance = abs(tan_distance) - def create_object(self, selection): - """Create the actual object""" - r_d_str = self.form.spinbox_r_distance.text() - tan_d_str = self.form.spinbox_tan_distance.text() - self.r_distance = _Quantity(r_d_str).Value - self.tan_distance = _Quantity(tan_d_str).Value - - self.number = self.form.spinbox_number.value() - self.symmetry = self.form.spinbox_symmetry.value() - self.center = self.set_point() - - if len(selection) == 1: - sel_obj = selection[0] - else: - # This can be changed so a compound of multiple - # selected objects is produced - sel_obj = selection[0] + # The other arguments are not tested but they should be present. + if symmetry and axis and center: + pass self.fuse = self.form.checkbox_fuse.isChecked() self.use_link = self.form.checkbox_link.isChecked() + return True + + def create_object(self): + """Create the new object. + + At this stage we already tested that the input is correct + so the necessary attributes are already set. + Then we proceed with the internal function to create the new object. + """ + if len(self.selection) == 1: + sel_obj = self.selection[0] + else: + # TODO: this should handle multiple objects. + # For example, it could take the shapes of all objects, + # make a compound and then use it as input for the array function. + sel_obj = self.selection[0] # This creates the object immediately # obj = Draft.makeArray(sel_obj, - # self.center, self.angle, self.number) + # self.r_distance, self.tan_distance, + # self.axis, self.center, + # self.number, self.symmetry, + # self.use_link) # if obj: # obj.Fuse = self.fuse @@ -210,96 +278,122 @@ class TaskPanelCircularArray: # of this class, the GuiCommand. # This is needed to schedule geometry manipulation # that would crash Coin3D if done in the event callback. - _cmd = "obj = Draft.makeArray(" - _cmd += "FreeCAD.ActiveDocument." + sel_obj.Name + ", " - _cmd += "arg1=" + str(self.r_distance) + ", " - _cmd += "arg2=" + str(self.tan_distance) + ", " - _cmd += "arg3=" + DraftVecUtils.toString(self.axis) + ", " - _cmd += "arg4=" + DraftVecUtils.toString(self.center) + ", " - _cmd += "arg5=" + str(self.number) + ", " - _cmd += "arg6=" + str(self.symmetry) + ", " + _cmd = "draftobjects.circulararray.make_circular_array" + _cmd += "(" + _cmd += "App.ActiveDocument." + sel_obj.Name + ", " + _cmd += "r_distance=" + str(self.r_distance) + ", " + _cmd += "tan_distance=" + str(self.tan_distance) + ", " + _cmd += "number=" + str(self.number) + ", " + _cmd += "symmetry=" + str(self.symmetry) + ", " + _cmd += "axis=" + DraftVecUtils.toString(self.axis) + ", " + _cmd += "center=" + DraftVecUtils.toString(self.center) + ", " _cmd += "use_link=" + str(self.use_link) _cmd += ")" - _cmd_list = ["FreeCADGui.addModule('Draft')", - _cmd, + _cmd_list = ["Gui.addModule('Draft')", + "Gui.addModule('draftobjects.circulararray')", + "obj = " + _cmd, "obj.Fuse = " + str(self.fuse), "Draft.autogroup(obj)", - "FreeCAD.ActiveDocument.recompute()"] - self.source_command.commit("Circular array", _cmd_list) + "App.ActiveDocument.recompute()"] - def set_point(self): - """Assign the values to the center""" - self.c_x_str = self.form.input_c_x.text() - self.c_y_str = self.form.input_c_y.text() - self.c_z_str = self.form.input_c_z.text() - center = App.Vector(_Quantity(self.c_x_str).Value, - _Quantity(self.c_y_str).Value, - _Quantity(self.c_z_str).Value) + # We commit the command list through the parent command + self.source_command.commit(_tr(self.name), _cmd_list) + + def get_distances(self): + """Get the distance parameters from the widgets.""" + r_d_str = self.form.spinbox_r_distance.text() + tan_d_str = self.form.spinbox_tan_distance.text() + return (U.Quantity(r_d_str).Value, + U.Quantity(tan_d_str).Value) + + def get_number_symmetry(self): + """Get the number and symmetry parameters from the widgets.""" + number = self.form.spinbox_number.value() + symmetry = self.form.spinbox_symmetry.value() + return number, symmetry + + def get_center(self): + """Get the value of the center from the widgets.""" + c_x_str = self.form.input_c_x.text() + c_y_str = self.form.input_c_y.text() + c_z_str = self.form.input_c_z.text() + center = App.Vector(U.Quantity(c_x_str).Value, + U.Quantity(c_y_str).Value, + U.Quantity(c_z_str).Value) return center + def get_axis(self): + """Get the axis that will be used for the array. NOT IMPLEMENTED. + + It should consider a second selection of an edge or wire to use + as an axis. + """ + return self.axis + def reset_point(self): - """Reset the point to the original distance""" + """Reset the center point to the original distance.""" self.form.input_c_x.setProperty('rawValue', 0) self.form.input_c_y.setProperty('rawValue', 0) self.form.input_c_z.setProperty('rawValue', 0) - self.center = self.set_point() - _Msg(_tr("Center reset:") + self.center = self.get_center() + _msg(_tr("Center reset:") + " ({0}, {1}, {2})".format(self.center.x, self.center.y, self.center.z)) - def print_fuse_state(self): - """Print the state translated""" - if self.fuse: - translated_state = QT_TRANSLATE_NOOP("Draft", "True") + def print_fuse_state(self, fuse): + """Print the fuse state translated.""" + if fuse: + state = self.tr_true else: - translated_state = QT_TRANSLATE_NOOP("Draft", "False") - _Msg(_tr("Fuse:") + " {}".format(translated_state)) + state = self.tr_false + _msg(_tr("Fuse:") + " {}".format(state)) def set_fuse(self): - """This function is called when the fuse checkbox changes""" + """Execute as a callback when the fuse checkbox changes.""" self.fuse = self.form.checkbox_fuse.isChecked() - self.print_fuse_state() + self.print_fuse_state(self.fuse) + utils.set_param("Draft_array_fuse", self.fuse) - def print_link_state(self): - """Print the state translated""" - if self.use_link: - translated_state = QT_TRANSLATE_NOOP("Draft", "True") + def print_link_state(self, use_link): + """Print the link state translated.""" + if use_link: + state = self.tr_true else: - translated_state = QT_TRANSLATE_NOOP("Draft", "False") - _Msg(_tr("Use Link object:") + " {}".format(translated_state)) + state = self.tr_false + _msg(_tr("Create Link array:") + " {}".format(state)) def set_link(self): - """This function is called when the fuse checkbox changes""" + """Execute as a callback when the link checkbox changes.""" self.use_link = self.form.checkbox_link.isChecked() - self.print_link_state() + self.print_link_state(self.use_link) + utils.set_param("Draft_array_Link", self.use_link) - def print_messages(self, selection): - """Print messages about the operation""" - if len(selection) == 1: - sel_obj = selection[0] + def print_messages(self): + """Print messages about the operation.""" + if len(self.selection) == 1: + sel_obj = self.selection[0] else: - # This can be changed so a compound of multiple - # selected objects is produced - sel_obj = selection[0] - _Msg("{}".format(16*"-")) - _Msg("{}".format(self.name)) - _Msg(_tr("Object:") + " {}".format(sel_obj.Label)) - _Msg(_tr("Radial distance:") + " {}".format(self.r_distance)) - _Msg(_tr("Tangential distance:") + " {}".format(self.tan_distance)) - _Msg(_tr("Number of circular layers:") + " {}".format(self.number)) - _Msg(_tr("Symmetry parameter:") + " {}".format(self.symmetry)) - _Msg(_tr("Center of rotation:") + # TODO: this should handle multiple objects. + # For example, it could take the shapes of all objects, + # make a compound and then use it as input for the array function. + sel_obj = self.selection[0] + _msg(_tr("Object:") + " {}".format(sel_obj.Label)) + _msg(_tr("Radial distance:") + " {}".format(self.r_distance)) + _msg(_tr("Tangential distance:") + " {}".format(self.tan_distance)) + _msg(_tr("Number of circular layers:") + " {}".format(self.number)) + _msg(_tr("Symmetry parameter:") + " {}".format(self.symmetry)) + _msg(_tr("Center of rotation:") + " ({0}, {1}, {2})".format(self.center.x, self.center.y, self.center.z)) - self.print_fuse_state() - self.print_link_state() + self.print_fuse_state(self.fuse) + self.print_link_state(self.use_link) def display_point(self, point=None, plane=None, mask=None): - """Displays the coordinates in the x, y, and z widgets. + """Display the coordinates in the x, y, and z widgets. This function should be used in a Coin callback so that the coordinate values are automatically updated when the @@ -307,21 +401,21 @@ class TaskPanelCircularArray: This was copied from `DraftGui.py` but needs to be improved for this particular command. - point : + point: Base::Vector3 is a vector that arrives by the callback. - plane : + plane: WorkingPlane is a `WorkingPlane` instance, for example, `App.DraftWorkingPlane`. It is not used at the moment, but could be used to set up the grid. - mask : + mask: str is a string that specifies which coordinate is being edited. It is used to restrict edition of a single coordinate. It is not used at the moment but could be used with a callback. """ # Get the coordinates to display - dp = None + d_p = None if point: - dp = point + d_p = point # Set the widgets to the value of the mouse pointer. # @@ -336,25 +430,28 @@ class TaskPanelCircularArray: # sbx = self.form.spinbox_c_x # sby = self.form.spinbox_c_y # sbz = self.form.spinbox_c_z - if dp: + if d_p: if self.mask in ('y', 'z'): - # sbx.setText(displayExternal(dp.x, None, 'Length')) - self.form.input_c_x.setProperty('rawValue', dp.x) + # sbx.setText(displayExternal(d_p.x, None, 'Length')) + self.form.input_c_x.setProperty('rawValue', d_p.x) else: - # sbx.setText(displayExternal(dp.x, None, 'Length')) - self.form.input_c_x.setProperty('rawValue', dp.x) + # sbx.setText(displayExternal(d_p.x, None, 'Length')) + self.form.input_c_x.setProperty('rawValue', d_p.x) if self.mask in ('x', 'z'): - # sby.setText(displayExternal(dp.y, None, 'Length')) - self.form.input_c_y.setProperty('rawValue', dp.y) + # sby.setText(displayExternal(d_p.y, None, 'Length')) + self.form.input_c_y.setProperty('rawValue', d_p.y) else: - # sby.setText(displayExternal(dp.y, None, 'Length')) - self.form.input_c_y.setProperty('rawValue', dp.y) + # sby.setText(displayExternal(d_p.y, None, 'Length')) + self.form.input_c_y.setProperty('rawValue', d_p.y) if self.mask in ('x', 'y'): - # sbz.setText(displayExternal(dp.z, None, 'Length')) - self.form.input_c_z.setProperty('rawValue', dp.z) + # sbz.setText(displayExternal(d_p.z, None, 'Length')) + self.form.input_c_z.setProperty('rawValue', d_p.z) else: - # sbz.setText(displayExternal(dp.z, None, 'Length')) - self.form.input_c_z.setProperty('rawValue', dp.z) + # sbz.setText(displayExternal(d_p.z, None, 'Length')) + self.form.input_c_z.setProperty('rawValue', d_p.z) + + if plane: + pass # Set masks if (mask == "x") or (self.mask == "x"): @@ -379,7 +476,7 @@ class TaskPanelCircularArray: self.set_focus() def set_focus(self, key=None): - """Set the focus on the widget that receives the key signal""" + """Set the focus on the widget that receives the key signal.""" if key is None or key == "x": self.form.input_c_x.setFocus() self.form.input_c_x.selectAll() @@ -391,12 +488,16 @@ class TaskPanelCircularArray: self.form.input_c_z.selectAll() def reject(self): - """Function that executes when clicking the Cancel button""" - _Msg(_tr("Aborted:") + " {}".format(self.name)) + """Execute when clicking the Cancel button or pressing Escape.""" + _msg(_tr("Aborted:") + " {}".format(_tr(self.name))) self.finish() def finish(self): - """Function that runs at the end after OK or Cancel""" + """Finish the command, after accept or reject. + + It finally calls the parent class to execute + the delayed functions, and perform cleanup. + """ # App.ActiveDocument.commitTransaction() Gui.ActiveDocument.resetEdit() # Runs the parent command to complete the call diff --git a/src/Mod/Draft/drafttaskpanels/task_orthoarray.py b/src/Mod/Draft/drafttaskpanels/task_orthoarray.py index 1eea177091..554d124599 100644 --- a/src/Mod/Draft/drafttaskpanels/task_orthoarray.py +++ b/src/Mod/Draft/drafttaskpanels/task_orthoarray.py @@ -1,8 +1,3 @@ -"""Provide the task panel for the Draft OrthoArray tool.""" -## @package task_orthoarray -# \ingroup DRAFT -# \brief Provide the task panel for the Draft OrthoArray tool. - # *************************************************************************** # * (c) 2020 Eliud Cabrera Castillo * # * * @@ -25,175 +20,227 @@ # * USA * # * * # *************************************************************************** - -import FreeCAD as App -import FreeCADGui as Gui -# import Draft -import Draft_rc -import DraftVecUtils +"""Provides the task panel for the Draft OrthoArray tool.""" +## @package task_orthoarray +# \ingroup DRAFT +# \brief Provide the task panel for the Draft OrthoArray tool. import PySide.QtGui as QtGui from PySide.QtCore import QT_TRANSLATE_NOOP -# import DraftTools -from draftutils.translate import translate -# from DraftGui import displayExternal -_Quantity = App.Units.Quantity +import FreeCAD as App +import FreeCADGui as Gui +import Draft_rc # include resources, icons, ui files +import DraftVecUtils +import draftutils.utils as utils +from draftutils.messages import _msg, _err, _log +from draftutils.translate import _tr +from FreeCAD import Units as U - -def _Msg(text, end="\n"): - """Print message with newline.""" - App.Console.PrintMessage(text + end) - - -def _Wrn(text, end="\n"): - """Print warning with newline.""" - App.Console.PrintWarning(text + end) - - -def _tr(text): - """Translate with the context set.""" - return translate("Draft", text) - - -# So the resource file doesn't trigger errors from code checkers (flake8) -True if Draft_rc else False +# The module is used to prevent complaints from code checkers (flake8) +bool(Draft_rc.__name__) class TaskPanelOrthoArray: - """TaskPanel for the OrthoArray command. + """TaskPanel code for the OrthoArray command. The names of the widgets are defined in the `.ui` file. - In this class all those widgets are automatically created - under the name `self.form.` + This `.ui` file `must` be loaded into an attribute + called `self.form` so that it is loaded into the task panel correctly. + + In this class all widgets are automatically created + as `self.form.`. The `.ui` file may use special FreeCAD widgets such as `Gui::InputField` (based on `QLineEdit`) and `Gui::QuantitySpinBox` (based on `QAbstractSpinBox`). See the Doxygen documentation of the corresponding files in `src/Gui/`, for example, `InputField.h` and `QuantitySpinBox.h`. + + Attributes + ---------- + source_command: gui_base.GuiCommandBase + This attribute holds a reference to the calling class + of this task panel. + This parent class, which is derived from `gui_base.GuiCommandBase`, + is responsible for calling this task panel, for installing + certain callbacks, and for removing them. + + It also delays the execution of the internal creation commands + by using the `draftutils.todo.ToDo` class. + + See Also + -------- + * https://forum.freecadweb.org/viewtopic.php?f=10&t=40007 + * https://forum.freecadweb.org/viewtopic.php?t=5374#p43038 """ def __init__(self): + self.name = "Orthogonal array" + _log(_tr("Task panel:") + "{}".format(_tr(self.name))) + + # The .ui file must be loaded into an attribute + # called `self.form` so that it is displayed in the task panel. ui_file = ":/ui/TaskPanel_OrthoArray.ui" self.form = Gui.PySideUic.loadUi(ui_file) - self.name = self.form.windowTitle() icon_name = "Draft_Array" svg = ":/icons/" + icon_name pix = QtGui.QPixmap(svg) icon = QtGui.QIcon.fromTheme(icon_name, QtGui.QIcon(svg)) self.form.setWindowIcon(icon) + self.form.setWindowTitle(_tr(self.name)) + self.form.label_icon.setPixmap(pix.scaled(32, 32)) - start_x = _Quantity(100.0, App.Units.Length) + # ------------------------------------------------------------------- + # Default values for the internal function, + # and for the task panel interface + start_x = U.Quantity(100.0, App.Units.Length) start_y = start_x start_z = start_x - start_zero = _Quantity(0.0, App.Units.Length) length_unit = start_x.getUserPreferred()[2] - self.form.input_X_x.setProperty('rawValue', start_x.Value) + self.v_x = App.Vector(start_x.Value, 0, 0) + self.v_y = App.Vector(0, start_y.Value, 0) + self.v_z = App.Vector(0, 0, start_z.Value) + + self.form.input_X_x.setProperty('rawValue', self.v_x.x) self.form.input_X_x.setProperty('unit', length_unit) - self.form.input_X_y.setProperty('rawValue', start_zero.Value) + self.form.input_X_y.setProperty('rawValue', self.v_x.y) self.form.input_X_y.setProperty('unit', length_unit) - self.form.input_X_z.setProperty('rawValue', start_zero.Value) + self.form.input_X_z.setProperty('rawValue', self.v_x.z) self.form.input_X_z.setProperty('unit', length_unit) - self.form.input_Y_x.setProperty('rawValue', start_zero.Value) + self.form.input_Y_x.setProperty('rawValue', self.v_y.x) self.form.input_Y_x.setProperty('unit', length_unit) - self.form.input_Y_y.setProperty('rawValue', start_y.Value) + self.form.input_Y_y.setProperty('rawValue', self.v_y.y) self.form.input_Y_y.setProperty('unit', length_unit) - self.form.input_Y_z.setProperty('rawValue', start_zero.Value) + self.form.input_Y_z.setProperty('rawValue', self.v_y.z) self.form.input_Y_z.setProperty('unit', length_unit) - self.form.input_Z_x.setProperty('rawValue', start_zero.Value) + self.form.input_Z_x.setProperty('rawValue', self.v_z.x) self.form.input_Z_x.setProperty('unit', length_unit) - self.form.input_Z_y.setProperty('rawValue', start_zero.Value) + self.form.input_Z_y.setProperty('rawValue', self.v_z.y) self.form.input_Z_y.setProperty('unit', length_unit) - self.form.input_Z_z.setProperty('rawValue', start_z.Value) + self.form.input_Z_z.setProperty('rawValue', self.v_z.z) self.form.input_Z_z.setProperty('unit', length_unit) - self.v_X = App.Vector(100, 0, 0) - self.v_Y = App.Vector(0, 100, 0) - self.v_Z = App.Vector(0, 0, 100) + self.n_x = 2 + self.n_y = 2 + self.n_z = 1 - # Old style for Qt4, avoid! - # QtCore.QObject.connect(self.form.button_reset, - # QtCore.SIGNAL("clicked()"), - # self.reset_point) + self.form.spinbox_n_X.setValue(self.n_x) + self.form.spinbox_n_Y.setValue(self.n_y) + self.form.spinbox_n_Z.setValue(self.n_z) + + self.fuse = utils.get_param("Draft_array_fuse", False) + self.use_link = utils.get_param("Draft_array_Link", True) + + self.form.checkbox_fuse.setChecked(self.fuse) + self.form.checkbox_link.setChecked(self.use_link) + # ------------------------------------------------------------------- + + # Some objects need to be selected before we can execute the function. + self.selection = None + + # This is used to test the input of the internal function. + # It should be changed to True before we can execute the function. + self.valid_input = False + + self.set_widget_callbacks() + + self.tr_true = QT_TRANSLATE_NOOP("Draft", "True") + self.tr_false = QT_TRANSLATE_NOOP("Draft", "False") + + def set_widget_callbacks(self): + """Set up the callbacks (slots) for the widget signals.""" # New style for Qt5 self.form.button_reset_X.clicked.connect(lambda: self.reset_v("X")) self.form.button_reset_Y.clicked.connect(lambda: self.reset_v("Y")) self.form.button_reset_Z.clicked.connect(lambda: self.reset_v("Z")) - self.n_X = 2 - self.n_Y = 2 - self.n_Z = 1 - - self.form.spinbox_n_X.setValue(self.n_X) - self.form.spinbox_n_Y.setValue(self.n_Y) - self.form.spinbox_n_Z.setValue(self.n_Z) - - self.valid_input = False - - # When the checkbox changes, change the fuse value - self.fuse = False + # When the checkbox changes, change the internal value self.form.checkbox_fuse.stateChanged.connect(self.set_fuse) - - self.use_link = False self.form.checkbox_link.stateChanged.connect(self.set_link) + # Old style for Qt4, avoid! + # QtCore.QObject.connect(self.form.button_reset, + # QtCore.SIGNAL("clicked()"), + # self.reset_point) + def accept(self): - """Execute when clicking the OK button.""" - selection = Gui.Selection.getSelection() - n_X = self.form.spinbox_n_X.value() - n_Y = self.form.spinbox_n_Y.value() - n_Z = self.form.spinbox_n_Z.value() - self.valid_input = self.validate_input(selection, - n_X, - n_Y, - n_Z) + """Execute when clicking the OK button or Enter key.""" + self.selection = Gui.Selection.getSelection() + + (self.v_x, + self.v_y, + self.v_z) = self.get_intervals() + + (self.n_x, + self.n_y, + self.n_z) = self.get_numbers() + + self.valid_input = self.validate_input(self.selection, + self.v_x, self.v_y, self.v_z, + self.n_x, self.n_y, self.n_z) if self.valid_input: - self.create_object(selection) - self.print_messages(selection) + self.create_object() + self.print_messages() self.finish() - def validate_input(self, selection, n_X, n_Y, n_Z): - """Check that the input is valid.""" + def validate_input(self, selection, + v_x, v_y, v_z, + n_x, n_y, n_z): + """Check that the input is valid. + + Some values may not need to be checked because + the interface may not allow to input wrong data. + """ if not selection: - _Wrn(_tr("At least one element must be selected")) + _err(_tr("At least one element must be selected.")) return False - if n_X < 1 or n_Y < 1 or n_Z < 1: - _Wrn(_tr("Number of elements must be at least 1")) + + if n_x < 1 or n_y < 1 or n_z < 1: + _err(_tr("Number of elements must be at least 1.")) return False - # Todo: each of the elements of the selection could be tested, - # not only the first one. + + # TODO: this should handle multiple objects. + # Each of the elements of the selection should be tested. obj = selection[0] if obj.isDerivedFrom("App::FeaturePython"): - _Wrn(_tr("Selection is not suitable for array")) - _Wrn(_tr("Object:") + " {0} ({1})".format(obj.Label, obj.TypeId)) + _err(_tr("Selection is not suitable for array.")) + _err(_tr("Object:") + " {0} ({1})".format(obj.Label, obj.TypeId)) return False - return True - def create_object(self, selection): - """Create the actual object.""" - self.v_X, self.v_Y, self.v_Z = self.set_intervals() - self.n_X, self.n_Y, self.n_Z = self.set_numbers() - - if len(selection) == 1: - sel_obj = selection[0] - else: - # This can be changed so a compound of multiple - # selected objects is produced - sel_obj = selection[0] + # The other arguments are not tested but they should be present. + if v_x and v_y and v_z: + pass self.fuse = self.form.checkbox_fuse.isChecked() self.use_link = self.form.checkbox_link.isChecked() + return True + + def create_object(self): + """Create the new object. + + At this stage we already tested that the input is correct + so the necessary attributes are already set. + Then we proceed with the internal function to create the new object. + """ + if len(self.selection) == 1: + sel_obj = self.selection[0] + else: + # TODO: this should handle multiple objects. + # For example, it could take the shapes of all objects, + # make a compound and then use it as input for the array function. + sel_obj = self.selection[0] # This creates the object immediately # obj = Draft.makeArray(sel_obj, - # self.v_X, self.v_Y, self.v_Z, - # self.n_X, self.n_Y, self.n_Z) + # self.v_x, self.v_y, self.v_z, + # self.n_x, self.n_y, self.n_z, + # self.use_link) # if obj: # obj.Fuse = self.fuse @@ -201,146 +248,162 @@ class TaskPanelOrthoArray: # of this class, the GuiCommand. # This is needed to schedule geometry manipulation # that would crash Coin3D if done in the event callback. - _cmd = "obj = Draft.makeArray(" - _cmd += "FreeCAD.ActiveDocument." + sel_obj.Name + ", " - _cmd += "arg1=" + DraftVecUtils.toString(self.v_X) + ", " - _cmd += "arg2=" + DraftVecUtils.toString(self.v_Y) + ", " - _cmd += "arg3=" + DraftVecUtils.toString(self.v_Z) + ", " - _cmd += "arg4=" + str(self.n_X) + ", " - _cmd += "arg5=" + str(self.n_Y) + ", " - _cmd += "arg6=" + str(self.n_Z) + ", " + _cmd = "draftobjects.orthoarray.make_ortho_array" + _cmd += "(" + _cmd += "App.ActiveDocument." + sel_obj.Name + ", " + _cmd += "v_x=" + DraftVecUtils.toString(self.v_x) + ", " + _cmd += "v_y=" + DraftVecUtils.toString(self.v_y) + ", " + _cmd += "v_z=" + DraftVecUtils.toString(self.v_z) + ", " + _cmd += "n_x=" + str(self.n_x) + ", " + _cmd += "n_y=" + str(self.n_y) + ", " + _cmd += "n_z=" + str(self.n_z) + ", " _cmd += "use_link=" + str(self.use_link) _cmd += ")" - _cmd_list = ["FreeCADGui.addModule('Draft')", - _cmd, + _cmd_list = ["Gui.addModule('Draft')", + "Gui.addModule('draftobjects.orthoarray')", + "obj = " + _cmd, "obj.Fuse = " + str(self.fuse), "Draft.autogroup(obj)", - "FreeCAD.ActiveDocument.recompute()"] - self.source_command.commit("Ortho array", _cmd_list) + "App.ActiveDocument.recompute()"] - def set_numbers(self): - """Assign the number of elements.""" - self.n_X = self.form.spinbox_n_X.value() - self.n_Y = self.form.spinbox_n_Y.value() - self.n_Z = self.form.spinbox_n_Z.value() - return self.n_X, self.n_Y, self.n_Z + # We commit the command list through the parent command + self.source_command.commit(_tr(self.name), _cmd_list) - def set_intervals(self): - """Assign the interval vectors.""" - v_X_x_str = self.form.input_X_x.text() - v_X_y_str = self.form.input_X_y.text() - v_X_z_str = self.form.input_X_z.text() - self.v_X = App.Vector(_Quantity(v_X_x_str).Value, - _Quantity(v_X_y_str).Value, - _Quantity(v_X_z_str).Value) + def get_numbers(self): + """Get the number of elements from the widgets.""" + return (self.form.spinbox_n_X.value(), + self.form.spinbox_n_Y.value(), + self.form.spinbox_n_Z.value()) - v_Y_x_str = self.form.input_Y_x.text() - v_Y_y_str = self.form.input_Y_y.text() - v_Y_z_str = self.form.input_Y_z.text() - self.v_Y = App.Vector(_Quantity(v_Y_x_str).Value, - _Quantity(v_Y_y_str).Value, - _Quantity(v_Y_z_str).Value) + def get_intervals(self): + """Get the interval vectors from the widgets.""" + v_x_x_str = self.form.input_X_x.text() + v_x_y_str = self.form.input_X_y.text() + v_x_z_str = self.form.input_X_z.text() + v_x = App.Vector(U.Quantity(v_x_x_str).Value, + U.Quantity(v_x_y_str).Value, + U.Quantity(v_x_z_str).Value) - v_Z_x_str = self.form.input_Z_x.text() - v_Z_y_str = self.form.input_Z_y.text() - v_Z_z_str = self.form.input_Z_z.text() - self.v_Z = App.Vector(_Quantity(v_Z_x_str).Value, - _Quantity(v_Z_y_str).Value, - _Quantity(v_Z_z_str).Value) - return self.v_X, self.v_Y, self.v_Z + v_y_x_str = self.form.input_Y_x.text() + v_y_y_str = self.form.input_Y_y.text() + v_y_z_str = self.form.input_Y_z.text() + v_y = App.Vector(U.Quantity(v_y_x_str).Value, + U.Quantity(v_y_y_str).Value, + U.Quantity(v_y_z_str).Value) + + v_z_x_str = self.form.input_Z_x.text() + v_z_y_str = self.form.input_Z_y.text() + v_z_z_str = self.form.input_Z_z.text() + v_z = App.Vector(U.Quantity(v_z_x_str).Value, + U.Quantity(v_z_y_str).Value, + U.Quantity(v_z_z_str).Value) + return v_x, v_y, v_z def reset_v(self, interval): - """Reset the interval to zero distance.""" + """Reset the interval to zero distance. + + Parameters + ---------- + interval: str + Either "X", "Y", "Z", to reset the interval vector + for that direction. + """ if interval == "X": self.form.input_X_x.setProperty('rawValue', 100) self.form.input_X_y.setProperty('rawValue', 0) self.form.input_X_z.setProperty('rawValue', 0) - _Msg(_tr("Interval X reset:") - + " ({0}, {1}, {2})".format(self.v_X.x, - self.v_X.y, - self.v_X.z)) + self.v_x, self.v_y, self.v_z = self.get_intervals() + _msg(_tr("Interval X reset:") + + " ({0}, {1}, {2})".format(self.v_x.x, + self.v_x.y, + self.v_x.z)) elif interval == "Y": self.form.input_Y_x.setProperty('rawValue', 0) self.form.input_Y_y.setProperty('rawValue', 100) self.form.input_Y_z.setProperty('rawValue', 0) - _Msg(_tr("Interval Y reset:") - + " ({0}, {1}, {2})".format(self.v_Y.x, - self.v_Y.y, - self.v_Y.z)) + self.v_x, self.v_y, self.v_z = self.get_intervals() + _msg(_tr("Interval Y reset:") + + " ({0}, {1}, {2})".format(self.v_y.x, + self.v_y.y, + self.v_y.z)) elif interval == "Z": self.form.input_Z_x.setProperty('rawValue', 0) self.form.input_Z_y.setProperty('rawValue', 0) self.form.input_Z_z.setProperty('rawValue', 100) - _Msg(_tr("Interval Z reset:") - + " ({0}, {1}, {2})".format(self.v_Z.x, - self.v_Z.y, - self.v_Z.z)) + self.v_x, self.v_y, self.v_z = self.get_intervals() + _msg(_tr("Interval Z reset:") + + " ({0}, {1}, {2})".format(self.v_z.x, + self.v_z.y, + self.v_z.z)) - self.n_X, self.n_Y, self.n_Z = self.set_intervals() - - def print_fuse_state(self): - """Print the state translated.""" - if self.fuse: - translated_state = QT_TRANSLATE_NOOP("Draft", "True") + def print_fuse_state(self, fuse): + """Print the fuse state translated.""" + if fuse: + state = self.tr_true else: - translated_state = QT_TRANSLATE_NOOP("Draft", "False") - _Msg(_tr("Fuse:") + " {}".format(translated_state)) + state = self.tr_false + _msg(_tr("Fuse:") + " {}".format(state)) def set_fuse(self): - """Run callback when the fuse checkbox changes.""" + """Execute as a callback when the fuse checkbox changes.""" self.fuse = self.form.checkbox_fuse.isChecked() - self.print_fuse_state() + self.print_fuse_state(self.fuse) + utils.set_param("Draft_array_fuse", self.fuse) - def print_link_state(self): - """Print the state translated.""" - if self.use_link: - translated_state = QT_TRANSLATE_NOOP("Draft", "True") + def print_link_state(self, use_link): + """Print the link state translated.""" + if use_link: + state = self.tr_true else: - translated_state = QT_TRANSLATE_NOOP("Draft", "False") - _Msg(_tr("Use Link object:") + " {}".format(translated_state)) + state = self.tr_false + _msg(_tr("Create Link array:") + " {}".format(state)) def set_link(self): - """Run callback when the link checkbox changes.""" + """Execute as a callback when the link checkbox changes.""" self.use_link = self.form.checkbox_link.isChecked() - self.print_link_state() + self.print_link_state(self.use_link) + utils.set_param("Draft_array_Link", self.use_link) - def print_messages(self, selection): + def print_messages(self): """Print messages about the operation.""" - if len(selection) == 1: - sel_obj = selection[0] + if len(self.selection) == 1: + sel_obj = self.selection[0] else: - # This can be changed so a compound of multiple - # selected objects is produced - sel_obj = selection[0] - _Msg("{}".format(16*"-")) - _Msg("{}".format(self.name)) - _Msg(_tr("Object:") + " {}".format(sel_obj.Label)) - _Msg(_tr("Number of X elements:") + " {}".format(self.n_X)) - _Msg(_tr("Interval X:") - + " ({0}, {1}, {2})".format(self.v_X.x, - self.v_X.y, - self.v_X.z)) - _Msg(_tr("Number of Y elements:") + " {}".format(self.n_Y)) - _Msg(_tr("Interval Y:") - + " ({0}, {1}, {2})".format(self.v_Y.x, - self.v_Y.y, - self.v_Y.z)) - _Msg(_tr("Number of Z elements:") + " {}".format(self.n_Z)) - _Msg(_tr("Interval Z:") - + " ({0}, {1}, {2})".format(self.v_Z.x, - self.v_Z.y, - self.v_Z.z)) - self.print_fuse_state() - self.print_link_state() + # TODO: this should handle multiple objects. + # For example, it could take the shapes of all objects, + # make a compound and then use it as input for the array function. + sel_obj = self.selection[0] + _msg(_tr("Object:") + " {}".format(sel_obj.Label)) + _msg(_tr("Number of X elements:") + " {}".format(self.n_x)) + _msg(_tr("Interval X:") + + " ({0}, {1}, {2})".format(self.v_x.x, + self.v_x.y, + self.v_x.z)) + _msg(_tr("Number of Y elements:") + " {}".format(self.n_y)) + _msg(_tr("Interval Y:") + + " ({0}, {1}, {2})".format(self.v_y.x, + self.v_y.y, + self.v_y.z)) + _msg(_tr("Number of Z elements:") + " {}".format(self.n_z)) + _msg(_tr("Interval Z:") + + " ({0}, {1}, {2})".format(self.v_z.x, + self.v_z.y, + self.v_z.z)) + self.print_fuse_state(self.fuse) + self.print_link_state(self.use_link) def reject(self): - """Run when clicking the Cancel button.""" - _Msg(_tr("Aborted:") + " {}".format(self.name)) + """Execute when clicking the Cancel button or pressing Escape.""" + _msg(_tr("Aborted:") + " {}".format(_tr(self.name))) self.finish() def finish(self): - """Run at the end after OK or Cancel.""" + """Finish the command, after accept or reject. + + It finally calls the parent class to execute + the delayed functions, and perform cleanup. + """ # App.ActiveDocument.commitTransaction() Gui.ActiveDocument.resetEdit() # Runs the parent command to complete the call diff --git a/src/Mod/Draft/drafttaskpanels/task_polararray.py b/src/Mod/Draft/drafttaskpanels/task_polararray.py index 042a18a8a3..48d55c2a53 100644 --- a/src/Mod/Draft/drafttaskpanels/task_polararray.py +++ b/src/Mod/Draft/drafttaskpanels/task_polararray.py @@ -1,9 +1,3 @@ -"""This module provides the task panel for the Draft PolarArray tool. -""" -## @package task_polararray -# \ingroup DRAFT -# \brief This module provides the task panel code for the PolarArray tool. - # *************************************************************************** # * (c) 2019 Eliud Cabrera Castillo * # * * @@ -26,163 +20,221 @@ # * USA * # * * # *************************************************************************** +"""Provides the task panel for the Draft PolarArray tool.""" +## @package task_polararray +# \ingroup DRAFT +# \brief This module provides the task panel code for the PolarArray tool. + +import PySide.QtGui as QtGui +from PySide.QtCore import QT_TRANSLATE_NOOP import FreeCAD as App import FreeCADGui as Gui -# import Draft -import Draft_rc +import Draft_rc # include resources, icons, ui files import DraftVecUtils +import draftutils.utils as utils +from draftutils.messages import _msg, _wrn, _err, _log +from draftutils.translate import _tr +from FreeCAD import Units as U -import PySide.QtCore as QtCore -import PySide.QtGui as QtGui -from PySide.QtCore import QT_TRANSLATE_NOOP -# import DraftTools -from DraftGui import translate -# from DraftGui import displayExternal - -_Quantity = App.Units.Quantity - - -def _Msg(text, end="\n"): - """Print message with newline""" - App.Console.PrintMessage(text + end) - - -def _Wrn(text, end="\n"): - """Print warning with newline""" - App.Console.PrintWarning(text + end) - - -def _tr(text): - """Function to translate with the context set""" - return translate("Draft", text) - - -# So the resource file doesn't trigger errors from code checkers (flake8) -True if Draft_rc.__name__ else False +# The module is used to prevent complaints from code checkers (flake8) +bool(Draft_rc.__name__) class TaskPanelPolarArray: - """TaskPanel for the PolarArray command. + """TaskPanel code for the PolarArray command. The names of the widgets are defined in the `.ui` file. - In this class all those widgets are automatically created - under the name `self.form.` + This `.ui` file `must` be loaded into an attribute + called `self.form` so that it is loaded into the task panel correctly. + + In this class all widgets are automatically created + as `self.form.`. The `.ui` file may use special FreeCAD widgets such as `Gui::InputField` (based on `QLineEdit`) and `Gui::QuantitySpinBox` (based on `QAbstractSpinBox`). See the Doxygen documentation of the corresponding files in `src/Gui/`, for example, `InputField.h` and `QuantitySpinBox.h`. + + Attributes + ---------- + source_command: gui_base.GuiCommandBase + This attribute holds a reference to the calling class + of this task panel. + This parent class, which is derived from `gui_base.GuiCommandBase`, + is responsible for calling this task panel, for installing + certain callbacks, and for removing them. + + It also delays the execution of the internal creation commands + by using the `draftutils.todo.ToDo` class. + + See Also + -------- + * https://forum.freecadweb.org/viewtopic.php?f=10&t=40007 + * https://forum.freecadweb.org/viewtopic.php?t=5374#p43038 """ def __init__(self): + self.name = "Polar array" + _log(_tr("Task panel:") + "{}".format(_tr(self.name))) + + # The .ui file must be loaded into an attribute + # called `self.form` so that it is displayed in the task panel. ui_file = ":/ui/TaskPanel_PolarArray.ui" self.form = Gui.PySideUic.loadUi(ui_file) - self.name = self.form.windowTitle() icon_name = "Draft_PolarArray" svg = ":/icons/" + icon_name pix = QtGui.QPixmap(svg) icon = QtGui.QIcon.fromTheme(icon_name, QtGui.QIcon(svg)) self.form.setWindowIcon(icon) + self.form.setWindowTitle(_tr(self.name)) + self.form.label_icon.setPixmap(pix.scaled(32, 32)) - start_angle = _Quantity(180.0, App.Units.Angle) + # ------------------------------------------------------------------- + # Default values for the internal function, + # and for the task panel interface + start_angle = U.Quantity(360.0, App.Units.Angle) angle_unit = start_angle.getUserPreferred()[2] - self.form.spinbox_angle.setProperty('rawValue', start_angle.Value) - self.form.spinbox_angle.setProperty('unit', angle_unit) - self.form.spinbox_number.setValue(4) - self.angle_str = self.form.spinbox_angle.text() self.angle = start_angle.Value + self.number = 5 - self.number = self.form.spinbox_number.value() + self.form.spinbox_angle.setProperty('rawValue', self.angle) + self.form.spinbox_angle.setProperty('unit', angle_unit) - start_point = _Quantity(0.0, App.Units.Length) + self.form.spinbox_number.setValue(self.number) + + start_point = U.Quantity(0.0, App.Units.Length) length_unit = start_point.getUserPreferred()[2] - self.form.input_c_x.setProperty('rawValue', start_point.Value) + + self.center = App.Vector(start_point.Value, + start_point.Value, + start_point.Value) + + self.form.input_c_x.setProperty('rawValue', self.center.x) self.form.input_c_x.setProperty('unit', length_unit) - self.form.input_c_y.setProperty('rawValue', start_point.Value) + self.form.input_c_y.setProperty('rawValue', self.center.y) self.form.input_c_y.setProperty('unit', length_unit) - self.form.input_c_z.setProperty('rawValue', start_point.Value) + self.form.input_c_z.setProperty('rawValue', self.center.z) self.form.input_c_z.setProperty('unit', length_unit) - self.valid_input = True - self.c_x_str = "" - self.c_y_str = "" - self.c_z_str = "" - self.center = App.Vector(0, 0, 0) + self.fuse = utils.get_param("Draft_array_fuse", False) + self.use_link = utils.get_param("Draft_array_Link", True) - # Old style for Qt4 - # QtCore.QObject.connect(self.form.button_reset, - # QtCore.SIGNAL("clicked()"), - # self.reset_point) - # New style for Qt5 - self.form.button_reset.clicked.connect(self.reset_point) + self.form.checkbox_fuse.setChecked(self.fuse) + self.form.checkbox_link.setChecked(self.use_link) + # ------------------------------------------------------------------- + + # Some objects need to be selected before we can execute the function. + self.selection = None + + # This is used to test the input of the internal function. + # It should be changed to True before we can execute the function. + self.valid_input = False + + self.set_widget_callbacks() + + self.tr_true = QT_TRANSLATE_NOOP("Draft", "True") + self.tr_false = QT_TRANSLATE_NOOP("Draft", "False") # The mask is not used at the moment, but could be used in the future # by a callback to restrict the coordinates of the pointer. self.mask = "" - # When the checkbox changes, change the fuse value - self.fuse = False - QtCore.QObject.connect(self.form.checkbox_fuse, - QtCore.SIGNAL("stateChanged(int)"), - self.set_fuse) + def set_widget_callbacks(self): + """Set up the callbacks (slots) for the widget signals.""" + # New style for Qt5 + self.form.button_reset.clicked.connect(self.reset_point) - self.use_link = False - QtCore.QObject.connect(self.form.checkbox_link, - QtCore.SIGNAL("stateChanged(int)"), - self.set_link) + # When the checkbox changes, change the internal value + self.form.checkbox_fuse.stateChanged.connect(self.set_fuse) + self.form.checkbox_link.stateChanged.connect(self.set_link) + + # Old style for Qt4, avoid! + # QtCore.QObject.connect(self.form.button_reset, + # QtCore.SIGNAL("clicked()"), + # self.reset_point) def accept(self): - """Function that executes when clicking the OK button""" - selection = Gui.Selection.getSelection() - self.number = self.form.spinbox_number.value() - self.valid_input = self.validate_input(selection, - self.number) + """Execute when clicking the OK button or Enter key.""" + self.selection = Gui.Selection.getSelection() + + (self.number, + self.angle) = self.get_number_angle() + + self.center = self.get_center() + + self.valid_input = self.validate_input(self.selection, + self.number, + self.angle, + self.center) if self.valid_input: - self.create_object(selection) - self.print_messages(selection) + self.create_object() + self.print_messages() self.finish() - def validate_input(self, selection, number): - """Check that the input is valid""" + def validate_input(self, selection, + number, angle, center): + """Check that the input is valid. + + Some values may not need to be checked because + the interface may not allow to input wrong data. + """ if not selection: - _Wrn(_tr("At least one element must be selected")) + _err(_tr("At least one element must be selected.")) return False + + # TODO: this should handle multiple objects. + # Each of the elements of the selection should be tested. + obj = selection[0] + if obj.isDerivedFrom("App::FeaturePython"): + _err(_tr("Selection is not suitable for array.")) + _err(_tr("Object:") + " {}".format(selection[0].Label)) + return False + if number < 2: - _Wrn(_tr("Number of elements must be at least 2")) + _err(_tr("Number of elements must be at least 2.")) return False - # Todo: each of the elements of the selection could be tested, - # not only the first one. - if selection[0].isDerivedFrom("App::FeaturePython"): - _Wrn(_tr("Selection is not suitable for array")) - _Wrn(_tr("Object:") + " {}".format(selection[0].Label)) - return False - return True - def create_object(self, selection): - """Create the actual object""" - self.angle_str = self.form.spinbox_angle.text() - self.angle = _Quantity(self.angle_str).Value + if angle > 360: + _wrn(_tr("The angle is above 360 degrees. " + "It is set to this value to proceed.")) + self.angle = 360 + elif angle < -360: + _wrn(_tr("The angle is below -360 degrees. " + "It is set to this value to proceed.")) + self.angle = -360 - self.center = self.set_point() - - if len(selection) == 1: - sel_obj = selection[0] - else: - # This can be changed so a compound of multiple - # selected objects is produced - sel_obj = selection[0] + # The other arguments are not tested but they should be present. + if center: + pass self.fuse = self.form.checkbox_fuse.isChecked() self.use_link = self.form.checkbox_link.isChecked() + return True + + def create_object(self): + """Create the new object. + + At this stage we already tested that the input is correct + so the necessary attributes are already set. + Then we proceed with the internal function to create the new object. + """ + if len(self.selection) == 1: + sel_obj = self.selection[0] + else: + # TODO: this should handle multiple objects. + # For example, it could take the shapes of all objects, + # make a compound and then use it as input for the array function. + sel_obj = self.selection[0] # This creates the object immediately # obj = Draft.makeArray(sel_obj, - # self.center, self.angle, self.number) + # self.center, self.angle, self.number, + # self.use_link) # if obj: # obj.Fuse = self.fuse @@ -190,91 +242,104 @@ class TaskPanelPolarArray: # of this class, the GuiCommand. # This is needed to schedule geometry manipulation # that would crash Coin3D if done in the event callback. - _cmd = "obj = Draft.makeArray(" - _cmd += "FreeCAD.ActiveDocument." + sel_obj.Name + ", " - _cmd += "arg1=" + DraftVecUtils.toString(self.center) + ", " - _cmd += "arg2=" + str(self.angle) + ", " - _cmd += "arg3=" + str(self.number) + ", " + _cmd = "draftobjects.polararray.make_polar_array" + _cmd += "(" + _cmd += "App.ActiveDocument." + sel_obj.Name + ", " + _cmd += "number=" + str(self.number) + ", " + _cmd += "angle=" + str(self.angle) + ", " + _cmd += "center=" + DraftVecUtils.toString(self.center) + ", " _cmd += "use_link=" + str(self.use_link) _cmd += ")" - _cmd_list = ["FreeCADGui.addModule('Draft')", - _cmd, + _cmd_list = ["Gui.addModule('Draft')", + "Gui.addModule('draftobjects.polararray')", + "obj = " + _cmd, "obj.Fuse = " + str(self.fuse), "Draft.autogroup(obj)", - "FreeCAD.ActiveDocument.recompute()"] - self.source_command.commit("Polar array", _cmd_list) + "App.ActiveDocument.recompute()"] - def set_point(self): - """Assign the values to the center""" - self.c_x_str = self.form.input_c_x.text() - self.c_y_str = self.form.input_c_y.text() - self.c_z_str = self.form.input_c_z.text() - center = App.Vector(_Quantity(self.c_x_str).Value, - _Quantity(self.c_y_str).Value, - _Quantity(self.c_z_str).Value) + # We commit the command list through the parent command + self.source_command.commit(_tr(self.name), _cmd_list) + + def get_number_angle(self): + """Get the number and angle parameters from the widgets.""" + number = self.form.spinbox_number.value() + + angle_str = self.form.spinbox_angle.text() + angle = U.Quantity(angle_str).Value + return number, angle + + def get_center(self): + """Get the value of the center from the widgets.""" + c_x_str = self.form.input_c_x.text() + c_y_str = self.form.input_c_y.text() + c_z_str = self.form.input_c_z.text() + center = App.Vector(U.Quantity(c_x_str).Value, + U.Quantity(c_y_str).Value, + U.Quantity(c_z_str).Value) return center def reset_point(self): - """Reset the point to the original distance""" + """Reset the center point to the original distance.""" self.form.input_c_x.setProperty('rawValue', 0) self.form.input_c_y.setProperty('rawValue', 0) self.form.input_c_z.setProperty('rawValue', 0) - self.center = self.set_point() - _Msg(_tr("Center reset:") + self.center = self.get_center() + _msg(_tr("Center reset:") + " ({0}, {1}, {2})".format(self.center.x, self.center.y, self.center.z)) - def print_fuse_state(self): - """Print the state translated""" - if self.fuse: - translated_state = QT_TRANSLATE_NOOP("Draft", "True") + def print_fuse_state(self, fuse): + """Print the fuse state translated.""" + if fuse: + state = self.tr_true else: - translated_state = QT_TRANSLATE_NOOP("Draft", "False") - _Msg(_tr("Fuse:") + " {}".format(translated_state)) + state = self.tr_false + _msg(_tr("Fuse:") + " {}".format(state)) def set_fuse(self): - """This function is called when the fuse checkbox changes""" + """Execute as a callback when the fuse checkbox changes.""" self.fuse = self.form.checkbox_fuse.isChecked() - self.print_fuse_state() + self.print_fuse_state(self.fuse) + utils.set_param("Draft_array_fuse", self.fuse) - def print_link_state(self): - """Print the state translated""" - if self.use_link: - translated_state = QT_TRANSLATE_NOOP("Draft", "True") + def print_link_state(self, use_link): + """Print the link state translated.""" + if use_link: + state = self.tr_true else: - translated_state = QT_TRANSLATE_NOOP("Draft", "False") - _Msg(_tr("Use Link object:") + " {}".format(translated_state)) + state = self.tr_false + _msg(_tr("Create Link array:") + " {}".format(state)) def set_link(self): - """This function is called when the fuse checkbox changes""" + """Execute as a callback when the link checkbox changes.""" self.use_link = self.form.checkbox_link.isChecked() - self.print_link_state() + self.print_link_state(self.use_link) + utils.set_param("Draft_array_Link", self.use_link) - def print_messages(self, selection): - """Print messages about the operation""" - if len(selection) == 1: - sel_obj = selection[0] + def print_messages(self): + """Print messages about the operation.""" + if len(self.selection) == 1: + sel_obj = self.selection[0] else: - # This can be changed so a compound of multiple - # selected objects is produced - sel_obj = selection[0] - _Msg("{}".format(16*"-")) - _Msg("{}".format(self.name)) - _Msg(_tr("Object:") + " {}".format(sel_obj.Label)) - _Msg(_tr("Start angle:") + " {}".format(self.angle_str)) - _Msg(_tr("Number of elements:") + " {}".format(self.number)) - _Msg(_tr("Center of rotation:") + # TODO: this should handle multiple objects. + # For example, it could take the shapes of all objects, + # make a compound and then use it as input for the array function. + sel_obj = self.selection[0] + _msg(_tr("Object:") + " {}".format(sel_obj.Label)) + _msg(_tr("Number of elements:") + " {}".format(self.number)) + _msg(_tr("Polar angle:") + " {}".format(self.angle)) + _msg(_tr("Center of rotation:") + " ({0}, {1}, {2})".format(self.center.x, self.center.y, self.center.z)) - self.print_fuse_state() - self.print_link_state() + self.print_fuse_state(self.fuse) + self.print_link_state(self.use_link) def display_point(self, point=None, plane=None, mask=None): - """Displays the coordinates in the x, y, and z widgets. + """Display the coordinates in the x, y, and z widgets. This function should be used in a Coin callback so that the coordinate values are automatically updated when the @@ -331,6 +396,9 @@ class TaskPanelPolarArray: # sbz.setText(displayExternal(dp.z, None, 'Length')) self.form.input_c_z.setProperty('rawValue', dp.z) + if plane: + pass + # Set masks if (mask == "x") or (self.mask == "x"): self.form.input_c_x.setEnabled(True) @@ -354,7 +422,7 @@ class TaskPanelPolarArray: self.set_focus() def set_focus(self, key=None): - """Set the focus on the widget that receives the key signal""" + """Set the focus on the widget that receives the key signal.""" if key is None or key == "x": self.form.input_c_x.setFocus() self.form.input_c_x.selectAll() @@ -366,12 +434,16 @@ class TaskPanelPolarArray: self.form.input_c_z.selectAll() def reject(self): - """Function that executes when clicking the Cancel button""" - _Msg(_tr("Aborted:") + " {}".format(self.name)) + """Execute when clicking the Cancel button or pressing Escape.""" + _msg(_tr("Aborted:") + " {}".format(_tr(self.name))) self.finish() def finish(self): - """Function that runs at the end after OK or Cancel""" + """Finish the command, after accept or reject. + + It finally calls the parent class to execute + the delayed functions, and perform cleanup. + """ # App.ActiveDocument.commitTransaction() Gui.ActiveDocument.resetEdit() # Runs the parent command to complete the call diff --git a/src/Mod/Draft/draftutils/gui_utils.py b/src/Mod/Draft/draftutils/gui_utils.py index 37b4c899d9..b9ef6c8a58 100644 --- a/src/Mod/Draft/draftutils/gui_utils.py +++ b/src/Mod/Draft/draftutils/gui_utils.py @@ -98,55 +98,68 @@ def autogroup(obj): obj: App::DocumentObject Any type of object that will be stored in the group. """ + + # check for required conditions for autogroup to work if not App.GuiUp: return + if not hasattr(Gui,"draftToolBar"): + return + if not hasattr(Gui.draftToolBar,"autogroup"): + return + if Gui.draftToolBar.isConstructionMode(): + return + + # autogroup code + if Gui.draftToolBar.autogroup is not None: + active_group = App.ActiveDocument.getObject(Gui.draftToolBar.autogroup) + if active_group: + found = False + for o in active_group.Group: + if o.Name == obj.Name: + found = True + if not found: + gr = active_group.Group + gr.append(obj) + active_group.Group = gr + + else: - doc = App.ActiveDocument - view = Gui.ActiveDocument.ActiveView - - # Look for active Arch container - active_arch_obj = Gui.ActiveDocument.ActiveView.getActiveObject("Arch") - if hasattr(Gui, "draftToolBar"): - if (hasattr(Gui.draftToolBar, "autogroup") - and not Gui.draftToolBar.isConstructionMode()): - if Gui.draftToolBar.autogroup is not None: - active_group = doc.getObject(Gui.draftToolBar.autogroup) - if active_group: - found = False - for o in active_group.Group: - if o.Name == obj.Name: - found = True - if not found: - gr = active_group.Group - gr.append(obj) - active_group.Group = gr - elif active_arch_obj: - active_arch_obj.addObject(obj) - elif view.getActiveObject("part", False) is not None: - # Add object to active part and change its placement - # accordingly so the object does not jump - # to a different position, works with App::Link if not scaled. - # Modified accordingly to realthunder suggestions - p, parent, sub = view.getActiveObject("part", False) - matrix = parent.getSubObject(sub, retType=4) - if matrix.hasScale() == 1: - _msg(translate("Draft", - "Unable to insert new object into " - "a scaled part")) - return - inverse_placement = App.Placement(matrix.inverse()) - if get_type(obj) == 'Point': - # point vector have a kind of placement, so should be - # processed before generic object with placement - point_vector = App.Vector(obj.X, obj.Y, obj.Z) - real_point = inverse_placement.multVec(point_vector) - obj.X = real_point.x - obj.Y = real_point.y - obj.Z = real_point.z - elif hasattr(obj, "Placement"): - place = inverse_placement.multiply(obj.Placement) - obj.Placement = App.Placement(place) - p.addObject(obj) + if Gui.ActiveDocument.ActiveView.getActiveObject("Arch"): + # add object to active Arch Container + Gui.ActiveDocument.ActiveView.getActiveObject("Arch").addObject(obj) + + elif Gui.ActiveDocument.ActiveView.getActiveObject("part", False) is not None: + # add object to active part and change it's placement accordingly + # so object does not jump to different position, works with App::Link + # if not scaled. Modified accordingly to realthunder suggestions + p, parent, sub = Gui.ActiveDocument.ActiveView.getActiveObject("part", False) + matrix = parent.getSubObject(sub, retType=4) + if matrix.hasScale() == 1: + err = translate("Draft", + "Unable to insert new object into " + "a scaled part") + App.Console.PrintMessage(err) + return + inverse_placement = App.Placement(matrix.inverse()) + if get_type(obj) == 'Point': + point_vector = App.Vector(obj.X, obj.Y, obj.Z) + real_point = inverse_placement.multVec(point_vector) + obj.X = real_point.x + obj.Y = real_point.y + obj.Z = real_point.z + elif get_type(obj) in ["Dimension"]: + obj.Start = inverse_placement.multVec(obj.Start) + obj.End = inverse_placement.multVec(obj.End) + obj.Dimline = inverse_placement.multVec(obj.Dimline) + obj.Normal = inverse_placement.Rotation.multVec(obj.Normal) + obj.Direction = inverse_placement.Rotation.multVec(obj.Direction) + elif get_type(obj) in ["Label"]: + obj.Placement = App.Placement(inverse_placement.multiply(obj.Placement)) + obj.TargetPoint = inverse_placement.multVec(obj.TargetPoint) + elif hasattr(obj,"Placement"): + # every object that have a placement is processed here + obj.Placement = App.Placement(inverse_placement.multiply(obj.Placement)) + p.addObject(obj) def dim_symbol(symbol=None, invert=False): diff --git a/src/Mod/Draft/draftutils/init_draft_statusbar.py b/src/Mod/Draft/draftutils/init_draft_statusbar.py new file mode 100644 index 0000000000..56e1612d75 --- /dev/null +++ b/src/Mod/Draft/draftutils/init_draft_statusbar.py @@ -0,0 +1,379 @@ +# *************************************************************************** +# * * +# * Copyright (c) 2020 Carlo Pavan * +# * * +# * This file is part of the FreeCAD CAx development system. * +# * * +# * This program is free software; you can redistribute it and/or modify * +# * it under the terms of the GNU Lesser General Public License (LGPL) * +# * as published by the Free Software Foundation; either version 2 of * +# * the License, or (at your option) any later version. * +# * for detail see the LICENCE text file. * +# * * +# * FreeCAD is distributed in the hope that it will be useful, * +# * but WITHOUT ANY WARRANTY; without even the implied warranty of * +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# * GNU Library General Public License for more details. * +# * * +# * You should have received a copy of the GNU Library General Public * +# * License along with FreeCAD; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# *************************************************************************** +"""Draft Statusbar commands. + +This module provide the code for the Draft Statusbar, activated by initGui +""" +## @package init_draft_statusbar +# \ingroup DRAFT +# \brief This module provides the code for the Draft Statusbar. + +from PySide import QtCore +from PySide import QtGui +from PySide.QtCore import QT_TRANSLATE_NOOP + +import FreeCAD as App +import FreeCADGui as Gui + +from draftutils.init_tools import get_draft_snap_commands + +#---------------------------------------------------------------------------- +# SCALE WIDGET FUNCTIONS +#---------------------------------------------------------------------------- + +draft_scales_metrics = ["1:1000", "1:500", "1:250", "1:200", "1:100", + "1:50", "1:25","1:20", "1:10", "1:5","1:2", + "1:1", + "2:1", "5:1", "10:1", "20:1", + QT_TRANSLATE_NOOP("draft","custom"), + ] + +draft_scales_arch_imperial = ["1/16in=1ft", "3/32in=1ft", "1/8in=1ft", + "3/16in=1ft", "1/4in=1ft","3/8in=1ft", + "1/2in=1ft", "3/4in=1ft", "1in=1ft", + "1.5in=1ft", "3in=1ft", + QT_TRANSLATE_NOOP("draft","custom"), + ] + +draft_scales_eng_imperial = ["1in=10ft", "1in=20ft", "1in=30ft", + "1in=40ft", "1in=50ft", "1in=60ft", + "1in=70ft", "1in=80ft", "1in=90ft", + "1in=100ft", + QT_TRANSLATE_NOOP("draft","custom"), + ] + +def get_scales(unit_system = 0): + """ + returns the list of preset scales accordin to unit system. + + Parameters: + unit_system = 0 : default from user preferences + 1 : metrics + 2 : imperial architectural + 3 : imperial engineering + """ + + if unit_system == 0: + param = App.ParamGet("User parameter:BaseApp/Preferences/Units") + scale_units_system = param.GetInt("UserSchema", 0) + if scale_units_system in [0, 1, 4, 6]: + return draft_scales_metrics + elif scale_units_system in [2, 3, 5]: + return draft_scales_arch_imperial + elif scale_units_system in [7]: + return draft_scales_eng_imperial + elif unit_system == 1: + return draft_scales_metrics + elif unit_system == 2: + return draft_scales_arch_imperial + elif unit_system == 3: + return draft_scales_eng_imperial + + +def scale_to_label(scale): + """ + transform a float number into a 1:X or X:1 scale and return it as label + """ + f = 1/scale + f = round(f,2) + f = f.as_integer_ratio() + if f[1] == 1 or f[0] == 1: + label = str(f[1]) + ":" + str(f[0]) + return label + else: + return str(scale) + +def label_to_scale(label): + """ + transform a scale string into scale factor as float + """ + try : + scale = float(label) + return scale + except : + if ":" in label: + f = label.split(":") + elif "=" in label: + f = label.split("=") + else: + return + if len(f) == 2: + try: + num = App.Units.Quantity(f[0]).Value + den = App.Units.Quantity(f[1]).Value + scale = num/den + return scale + except: + err = QT_TRANSLATE_NOOP("draft", + "Unable to convert input into a " + "scale factor") + App.Console.PrintWarning(err) + return None + +def _set_scale(action): + """ + triggered by scale pushbutton, set DraftAnnotationScale in preferences + """ + # set the label of the scale button + param = App.ParamGet("User parameter:BaseApp/Preferences/Mod/Draft") + + mw = Gui.getMainWindow() + sb = mw.statusBar() + scale_widget = sb.findChild(QtGui.QToolBar,"draft_status_scale_widget") + if action.text() == QT_TRANSLATE_NOOP("draft","custom"): + dialog_text = QT_TRANSLATE_NOOP("draft", + "Set custom annotation scale in " + "format x:x, x=x" + ) + custom_scale = QtGui.QInputDialog.getText(None, "Set custom scale", + dialog_text) + if custom_scale[1]: + print(custom_scale[0]) + scale = label_to_scale(custom_scale[0]) + if scale is None: + return + param.SetFloat("DraftAnnotationScale", scale) + cs = scale_to_label(scale) + scale_widget.scaleLabel.setText(cs) + else: + text_scale = action.text() + scale_widget.scaleLabel.setText(text_scale) + scale = label_to_scale(text_scale) + param.SetFloat("DraftAnnotationScale", scale) + +#---------------------------------------------------------------------------- +# MAIN DRAFT STATUSBAR FUNCTIONS +#---------------------------------------------------------------------------- + +def init_draft_statusbar(sb): + """ + this function initializes draft statusbar + """ + param = App.ParamGet("User parameter:BaseApp/Preferences/Mod/Draft") + + # SNAP WIDGET - init ---------------------------------------------------- + + snap_widget = QtGui.QToolBar() + snap_widget.setObjectName("draft_snap_widget") + snap_widget.setIconSize(QtCore.QSize(16,16)) + + # GRID BUTTON - init + gridbutton = QtGui.QPushButton(snap_widget) + gridbutton.setIcon(QtGui.QIcon.fromTheme("Draft", + QtGui.QIcon(":/icons/" + "Draft_Grid.svg"))) + gridbutton.setToolTip(QT_TRANSLATE_NOOP("Draft", + "Toggles Grid On/Off")) + gridbutton.setObjectName("Grid_Statusbutton") + gridbutton.setWhatsThis("Draft_ToggleGrid") + gridbutton.setFlat(True) + QtCore.QObject.connect(gridbutton,QtCore.SIGNAL("clicked()"), + lambda f=Gui.doCommand, + arg='Gui.runCommand("Draft_ToggleGrid")':f(arg)) + snap_widget.addWidget(gridbutton) + + # SNAP BUTTON - init + snappref = param.GetString("snapModes","111111111101111")[0] + snapbutton = QtGui.QPushButton(snap_widget) + snapbutton.setIcon(QtGui.QIcon.fromTheme("Draft", + QtGui.QIcon(":/icons/" + "Snap_Lock.svg"))) + snapbutton.setObjectName("Snap_Statusbutton") + snapbutton.setWhatsThis("Draft_ToggleLockSnap") + snapbutton.setToolTip(QT_TRANSLATE_NOOP("Draft", + "Object snapping")) + snapbutton.setCheckable(True) + snapbutton.setChecked(bool(int(snappref))) + snapbutton.setFlat(True) + + snaps_menu = QtGui.QMenu(snapbutton) + snaps_menu.setObjectName("draft_statusbar_snap_toolbar") + + snap_gui_commands = get_draft_snap_commands() + if 'Draft_Snap_Ortho' in snap_gui_commands: + snap_gui_commands.remove('Draft_Snap_Ortho') + if 'Draft_Snap_WorkingPlane' in snap_gui_commands: + snap_gui_commands.remove('Draft_Snap_WorkingPlane') + if 'Draft_Snap_Dimensions' in snap_gui_commands: + snap_gui_commands.remove('Draft_Snap_Dimensions') + Gui.Snapper.init_draft_snap_buttons(snap_gui_commands,snaps_menu, "_Statusbutton") + Gui.Snapper.restore_snap_buttons_state(snaps_menu, "_Statusbutton") + + snapbutton.setMenu(snaps_menu) + snap_widget.addWidget(snapbutton) + + + # DIMENSION BUTTON - init + dimpref = param.GetString("snapModes","111111111101111")[13] + dimbutton = QtGui.QPushButton(snap_widget) + dimbutton.setIcon(QtGui.QIcon.fromTheme("Draft", + QtGui.QIcon(":/icons/" + "Snap_Dimensions.svg"))) + dimbutton.setToolTip(QT_TRANSLATE_NOOP("Draft", + "Toggles Visual Aid Dimensions On/Off")) + dimbutton.setObjectName("Draft_Snap_Dimensions_Statusbutton") + dimbutton.setWhatsThis("Draft_ToggleDimensions") + dimbutton.setFlat(True) + dimbutton.setCheckable(True) + dimbutton.setChecked(bool(int(dimpref))) + QtCore.QObject.connect(dimbutton,QtCore.SIGNAL("clicked()"), + lambda f=Gui.doCommand, + arg='Gui.runCommand("Draft_Snap_Dimensions")':f(arg)) + snap_widget.addWidget(dimbutton) + + # ORTHO BUTTON - init + ortopref = param.GetString("snapModes","111111111101111")[10] + orthobutton = QtGui.QPushButton(snap_widget) + orthobutton.setIcon(QtGui.QIcon.fromTheme("Draft", + QtGui.QIcon(":/icons/" + "Snap_Ortho.svg"))) + orthobutton.setObjectName("Draft_Snap_Ortho"+"_Statusbutton") + orthobutton.setWhatsThis("Draft_ToggleOrtho") + orthobutton.setToolTip(QT_TRANSLATE_NOOP("Draft", + "Toggles Ortho On/Off")) + orthobutton.setFlat(True) + orthobutton.setCheckable(True) + orthobutton.setChecked(bool(int(ortopref))) + QtCore.QObject.connect(orthobutton,QtCore.SIGNAL("clicked()"), + lambda f=Gui.doCommand, + arg='Gui.runCommand("Draft_Snap_Ortho")':f(arg)) + snap_widget.addWidget(orthobutton) + + # WORKINGPLANE BUTTON - init + wppref = param.GetString("snapModes","111111111101111")[14] + wpbutton = QtGui.QPushButton(snap_widget) + wpbutton.setIcon(QtGui.QIcon.fromTheme("Draft", + QtGui.QIcon(":/icons/" + "Snap_WorkingPlane.svg"))) + wpbutton.setObjectName("Draft_Snap_WorkingPlane_Statusbutton") + wpbutton.setWhatsThis("Draft_ToggleWorkingPlaneSnap") + wpbutton.setToolTip(QT_TRANSLATE_NOOP("Draft", + "Toggles Constrain to Working Plane On/Off")) + wpbutton.setFlat(True) + wpbutton.setCheckable(True) + wpbutton.setChecked(bool(int(wppref))) + QtCore.QObject.connect(wpbutton,QtCore.SIGNAL("clicked()"), + lambda f=Gui.doCommand, + arg='Gui.runCommand("Draft_Snap_WorkingPlane")':f(arg)) + snap_widget.addWidget(wpbutton) + + # add snap widget to the statusbar + sb.insertPermanentWidget(2, snap_widget) + snap_widget.show() + + + # SCALE WIDGET ---------------------------------------------------------- + scale_widget = QtGui.QToolBar() + scale_widget.setObjectName("draft_status_scale_widget") + + # get scales list according to system units + draft_scales = get_scales() + + # get draft annotation scale + draft_annotation_scale = param.GetFloat("DraftAnnotationScale", 1.0) + + # initializes scale widget + scale_widget.draft_scales = draft_scales + scaleLabel = QtGui.QPushButton("Scale") + scaleLabel.setObjectName("ScaleLabel") + scaleLabel.setFlat(True) + menu = QtGui.QMenu(scaleLabel) + gUnits = QtGui.QActionGroup(menu) + for u in draft_scales: + a = QtGui.QAction(gUnits) + a.setText(u) + menu.addAction(a) + scaleLabel.setMenu(menu) + gUnits.triggered.connect(_set_scale) + scale_label = scale_to_label(draft_annotation_scale) + scaleLabel.setText(scale_label) + tooltip = "Set the scale used by draft annotation tools" + scaleLabel.setToolTip(QT_TRANSLATE_NOOP("draft",tooltip)) + scale_widget.addWidget(scaleLabel) + scale_widget.scaleLabel = scaleLabel + + # add scale widget to the statusbar + sb.insertPermanentWidget(3, scale_widget) + scale_widget.show() + +def show_draft_statusbar(): + """ + shows draft statusbar if present or initializes it + """ + params = App.ParamGet("User parameter:BaseApp/Preferences/Mod/Draft") + display_statusbar = params.GetBool("DisplayStatusbar", True) + + if not display_statusbar: + return + + mw = Gui.getMainWindow() + if mw: + sb = mw.statusBar() + + scale_widget = sb.findChild(QtGui.QToolBar, + "draft_status_scale_widget") + if scale_widget: + scale_widget.show() + elif params.GetBool("DisplayStatusbarScaleWidget", True): + init_draft_statusbar(sb) + + snap_widget = sb.findChild(QtGui.QToolBar,"draft_snap_widget") + if snap_widget: + snap_widget.show() + elif params.GetBool("DisplayStatusbarSnapWidget", True): + init_draft_statusbar(sb) + + +def hide_draft_statusbar(): + """ + hides draft statusbar if present + """ + mw = Gui.getMainWindow() + if not mw: + return + sb = mw.statusBar() + + # hide scale widget + scale_widget = sb.findChild(QtGui.QToolBar, + "draft_status_scale_widget") + if scale_widget: + scale_widget.hide() + else: + # when switching workbenches, the toolbar sometimes "jumps" + # out of the status bar to any other dock area... + scale_widget = mw.findChild(QtGui.QToolBar, + "draft_status_scale_widget") + if scale_widget: + scale_widget.hide() + + # hide snap widget + snap_widget = sb.findChild(QtGui.QToolBar,"draft_snap_widget") + if snap_widget: + snap_widget.hide() + else: + # when switching workbenches, the toolbar sometimes "jumps" + # out of the status bar to any other dock area... + snap_widget = mw.findChild(QtGui.QToolBar,"draft_snap_widget") + if snap_widget: + snap_widget.hide() \ No newline at end of file diff --git a/src/Mod/Draft/draftutils/init_tools.py b/src/Mod/Draft/draftutils/init_tools.py index d9e71c7578..a4787f1a9d 100644 --- a/src/Mod/Draft/draftutils/init_tools.py +++ b/src/Mod/Draft/draftutils/init_tools.py @@ -57,6 +57,17 @@ def get_draft_array_commands(): return ["Draft_ArrayTools"] +def get_draft_small_commands(): + """Return a list with only some utilities.""" + return ["Draft_Layer", + "Draft_WorkingPlaneProxy", + "Draft_ToggleDisplayMode", + "Draft_AddToGroup", + "Draft_SelectGroup", + "Draft_AddConstruction", + "Draft_Heal"] + + def get_draft_modification_commands(): """Return the modification commands list.""" lst = ["Draft_Move", "Draft_Rotate", @@ -73,9 +84,9 @@ def get_draft_modification_commands(): "Draft_Upgrade", "Draft_Downgrade", "Separator", "Draft_WireToBSpline", "Draft_Draft2Sketch", + "Draft_Slope", "Draft_FlipDimension", "Separator", - "Draft_Shape2DView", "Draft_Drawing", - "Draft_WorkingPlaneProxy"] + "Draft_Shape2DView", "Draft_Drawing"] return lst @@ -104,15 +115,15 @@ def get_draft_utility_commands(): def get_draft_snap_commands(): """Return the snapping commands list.""" - return ['Draft_Snap_Lock', 'Draft_Snap_Midpoint', - 'Draft_Snap_Perpendicular', - 'Draft_Snap_Grid', 'Draft_Snap_Intersection', - 'Draft_Snap_Parallel', - 'Draft_Snap_Endpoint', 'Draft_Snap_Angle', - 'Draft_Snap_Center', - 'Draft_Snap_Extension', 'Draft_Snap_Near', - 'Draft_Snap_Ortho', 'Draft_Snap_Special', - 'Draft_Snap_Dimensions', 'Draft_Snap_WorkingPlane'] + return ['Draft_Snap_Lock', + 'Draft_Snap_Endpoint', 'Draft_Snap_Midpoint', + 'Draft_Snap_Center', 'Draft_Snap_Angle', + 'Draft_Snap_Intersection', 'Draft_Snap_Perpendicular', + 'Draft_Snap_Extension', 'Draft_Snap_Parallel', + 'Draft_Snap_Special', 'Draft_Snap_Near', + 'Draft_Snap_Ortho', 'Draft_Snap_Grid', + 'Draft_Snap_WorkingPlane', 'Draft_Snap_Dimensions', + ] def init_draft_toolbars(workbench): diff --git a/src/Mod/Draft/draftutils/utils.py b/src/Mod/Draft/draftutils/utils.py index b13d3e3d87..ee93388f47 100644 --- a/src/Mod/Draft/draftutils/utils.py +++ b/src/Mod/Draft/draftutils/utils.py @@ -157,7 +157,8 @@ def get_param_type(param): "SvgLinesBlack", "dxfStdSize", "showSnapBar", "hideSnapBar", "alwaysShowGrid", "renderPolylineWidth", "showPlaneTracker", "UsePartPrimitives", - "DiscretizeEllipses", "showUnit"): + "DiscretizeEllipses", "showUnit", + "Draft_array_fuse", "Draft_array_Link"): return "bool" elif param in ("color", "constructioncolor", "snapcolor", "gridColor"): diff --git a/src/Mod/Draft/draftviewproviders/view_dimension.py b/src/Mod/Draft/draftviewproviders/view_dimension.py new file mode 100644 index 0000000000..b73553f21a --- /dev/null +++ b/src/Mod/Draft/draftviewproviders/view_dimension.py @@ -0,0 +1,929 @@ +# *************************************************************************** +# * Copyright (c) 2009, 2010 Yorik van Havre * +# * Copyright (c) 2009, 2010 Ken Cline * +# * * +# * This file is part of the FreeCAD CAx development system. * +# * * +# * This program is free software; you can redistribute it and/or modify * +# * it under the terms of the GNU Lesser General Public License (LGPL) * +# * as published by the Free Software Foundation; either version 2 of * +# * the License, or (at your option) any later version. * +# * for detail see the LICENCE text file. * +# * * +# * FreeCAD is distributed in the hope that it will be useful, * +# * but WITHOUT ANY WARRANTY; without even the implied warranty of * +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# * GNU Library General Public License for more details. * +# * * +# * You should have received a copy of the GNU Library General Public * +# * License along with FreeCAD; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# *************************************************************************** +"""This module provides the Draft Dimensions view provider classes +""" +## @package dimension +# \ingroup DRAFT +# \brief This module provides the view provider code for Draft Dimensions. + + +import FreeCAD as App +import DraftVecUtils, DraftGeomUtils +from pivy import coin +from PySide.QtCore import QT_TRANSLATE_NOOP +import draftutils.utils as utils +import draftutils.gui_utils as gui_utils +from draftviewproviders.view_draft_annotation import ViewProviderDraftAnnotation + +class ViewProviderDimensionBase(ViewProviderDraftAnnotation): + """ + A View Provider for the Draft Dimension object + This class is not used directly, but inherited by all dimension + view providers. + + DIMENSION VIEW PROVIDER NOMENCLATURE: + + | txt | e + ----o--------------------------------o----- + | | + | | d + | | + + a b c b a + + a = DimOvershoot (vobj) + b = Arrows (vobj) + c = Dimline (obj) + d = ExtLines (vobj) + e = ExtOvershoot (vobj) + txt = label (vobj) + + COIN OBJECT STRUCTURE: + vobj.node.color + .drawstyle + .lineswitch1.coords + .line + .marks + .marksDimOvershoot + .marksExtOvershoot + .label.textpos + .color + .font + .text + + vobj.node3d.color + .drawstyle + .lineswitch3.coords + .line + .marks + .marksDimOvershoot + .marksExtOvershoot + .label3d.textpos + .color + .font3d + .text3d + + """ + def __init__(self, vobj): + + super(ViewProviderDimensionBase, self).__init__(vobj) + + # text properties + vobj.addProperty("App::PropertyFont","FontName", + "Text", + QT_TRANSLATE_NOOP("App::Property","Font name")) + vobj.addProperty("App::PropertyLength","FontSize", + "Text", + QT_TRANSLATE_NOOP("App::Property","Font size")) + vobj.addProperty("App::PropertyLength","TextSpacing", + "Text", + QT_TRANSLATE_NOOP("App::Property", + "Spacing between text and dimension line")) + vobj.addProperty("App::PropertyBool","FlipText", + "Text", + QT_TRANSLATE_NOOP("App::Property", + "Rotate the dimension text 180 degrees")) + vobj.addProperty("App::PropertyVectorDistance","TextPosition", + "Text", + QT_TRANSLATE_NOOP("App::Property", + "Text Position. \n" + "Leave (0,0,0) for automatic position")) + vobj.addProperty("App::PropertyString","Override", + "Text", + QT_TRANSLATE_NOOP("App::Property", + "Text override. \n" + "Use $dim to insert the dimension length")) + + # units properties + vobj.addProperty("App::PropertyInteger","Decimals", + "Units", + QT_TRANSLATE_NOOP("App::Property", + "The number of decimals to show")) + vobj.addProperty("App::PropertyBool","ShowUnit", + "Units", + QT_TRANSLATE_NOOP("App::Property", + "Show the unit suffix")) + vobj.addProperty("App::PropertyString","UnitOverride", + "Units", + QT_TRANSLATE_NOOP("App::Property", + "A unit to express the measurement. \n" + "Leave blank for system default")) + + # graphics properties + vobj.addProperty("App::PropertyLength","ArrowSize", + "Graphics", + QT_TRANSLATE_NOOP("App::Property","Arrow size")) + vobj.addProperty("App::PropertyEnumeration","ArrowType", + "Graphics", + QT_TRANSLATE_NOOP("App::Property","Arrow type")) + vobj.addProperty("App::PropertyBool","FlipArrows", + "Graphics", + QT_TRANSLATE_NOOP("App::Property", + "Rotate the dimension arrows 180 degrees")) + vobj.addProperty("App::PropertyDistance","DimOvershoot", + "Graphics", + QT_TRANSLATE_NOOP("App::Property", + "The distance the dimension line is extended\n" + "past the extension lines")) + vobj.addProperty("App::PropertyDistance","ExtLines", + "Graphics", + QT_TRANSLATE_NOOP("App::Property", + "Length of the extension lines")) + vobj.addProperty("App::PropertyDistance","ExtOvershoot", + "Graphics", + QT_TRANSLATE_NOOP("App::Property", + "Length of the extension line \n" + "above the dimension line")) + vobj.addProperty("App::PropertyBool","ShowLine", + "Graphics", + QT_TRANSLATE_NOOP("App::Property", + "Shows the dimension line and arrows")) + + vobj.FontSize = utils.get_param("textheight",0.20) + vobj.TextSpacing = utils.get_param("dimspacing",0.05) + vobj.FontName = utils.get_param("textfont","") + vobj.ArrowSize = utils.get_param("arrowsize",0.1) + vobj.ArrowType = utils.ARROW_TYPES + vobj.ArrowType = utils.ARROW_TYPES[utils.get_param("dimsymbol",0)] + vobj.ExtLines = utils.get_param("extlines",0.3) + vobj.DimOvershoot = utils.get_param("dimovershoot",0) + vobj.ExtOvershoot = utils.get_param("extovershoot",0) + vobj.Decimals = utils.get_param("dimPrecision",2) + vobj.ShowUnit = utils.get_param("showUnit",True) + vobj.ShowLine = True + + def updateData(self, obj, prop): + """called when the base object is changed""" + return + + def onChanged(self, vobj, prop): + """called when a view property has changed""" + return + + def doubleClicked(self,vobj): + self.setEdit(vobj) + + def getDisplayModes(self,vobj): + return ["2D","3D"] + + def getDefaultDisplayMode(self): + if hasattr(self,"defaultmode"): + return self.defaultmode + else: + return ["2D","3D"][utils.get_param("dimstyle",0)] + + def setDisplayMode(self,mode): + return mode + + def getIcon(self): + if self.is_linked_to_circle(): + return ":/icons/Draft_DimensionRadius.svg" + return ":/icons/Draft_Dimension_Tree.svg" + + def __getstate__(self): + return self.Object.ViewObject.DisplayMode + + def __setstate__(self,state): + if state: + self.defaultmode = state + self.setDisplayMode(state) + + + +class ViewProviderLinearDimension(ViewProviderDimensionBase): + """ + A View Provider for the Draft Linear Dimension object + """ + def __init__(self, vobj): + + super(ViewProviderLinearDimension, self).__init__(vobj) + + self.Object = vobj.Object + vobj.Proxy = self + + + def attach(self, vobj): + '''Setup the scene sub-graph of the view provider''' + self.Object = vobj.Object + self.color = coin.SoBaseColor() + self.font = coin.SoFont() + self.font3d = coin.SoFont() + self.text = coin.SoAsciiText() + self.text3d = coin.SoText2() + self.text.string = "d" # some versions of coin crash if string is not set + self.text3d.string = "d" + self.textpos = coin.SoTransform() + self.text.justification = self.text3d.justification = coin.SoAsciiText.CENTER + label = coin.SoSeparator() + label.addChild(self.textpos) + label.addChild(self.color) + label.addChild(self.font) + label.addChild(self.text) + label3d = coin.SoSeparator() + label3d.addChild(self.textpos) + label3d.addChild(self.color) + label3d.addChild(self.font3d) + label3d.addChild(self.text3d) + self.coord1 = coin.SoCoordinate3() + self.trans1 = coin.SoTransform() + self.coord2 = coin.SoCoordinate3() + self.trans2 = coin.SoTransform() + self.transDimOvershoot1 = coin.SoTransform() + self.transDimOvershoot2 = coin.SoTransform() + self.transExtOvershoot1 = coin.SoTransform() + self.transExtOvershoot2 = coin.SoTransform() + self.marks = coin.SoSeparator() + self.marksDimOvershoot = coin.SoSeparator() + self.marksExtOvershoot = coin.SoSeparator() + self.drawstyle = coin.SoDrawStyle() + self.line = coin.SoType.fromName("SoBrepEdgeSet").createInstance() + self.coords = coin.SoCoordinate3() + self.node = coin.SoGroup() + self.node.addChild(self.color) + self.node.addChild(self.drawstyle) + self.lineswitch2 = coin.SoSwitch() + self.lineswitch2.whichChild = -3 + self.node.addChild(self.lineswitch2) + self.lineswitch2.addChild(self.coords) + self.lineswitch2.addChild(self.line) + self.lineswitch2.addChild(self.marks) + self.lineswitch2.addChild(self.marksDimOvershoot) + self.lineswitch2.addChild(self.marksExtOvershoot) + self.node.addChild(label) + self.node3d = coin.SoGroup() + self.node3d.addChild(self.color) + self.node3d.addChild(self.drawstyle) + self.lineswitch3 = coin.SoSwitch() + self.lineswitch3.whichChild = -3 + self.node3d.addChild(self.lineswitch3) + self.lineswitch3.addChild(self.coords) + self.lineswitch3.addChild(self.line) + self.lineswitch3.addChild(self.marks) + self.lineswitch3.addChild(self.marksDimOvershoot) + self.lineswitch3.addChild(self.marksExtOvershoot) + self.node3d.addChild(label3d) + vobj.addDisplayMode(self.node,"2D") + vobj.addDisplayMode(self.node3d,"3D") + self.updateData(vobj.Object,"Start") + self.onChanged(vobj,"FontSize") + self.onChanged(vobj,"FontName") + self.onChanged(vobj,"ArrowType") + self.onChanged(vobj,"LineColor") + self.onChanged(vobj,"DimOvershoot") + self.onChanged(vobj,"ExtOvershoot") + + def updateData(self, obj, prop): + """called when the base object is changed""" + import DraftGui + if prop in ["Start","End","Dimline","Direction"]: + + if obj.Start == obj.End: + return + + if not hasattr(self,"node"): + return + + import Part, DraftGeomUtils + from pivy import coin + + # calculate the 4 points + self.p1 = obj.Start + self.p4 = obj.End + base = None + if hasattr(obj,"Direction"): + if not DraftVecUtils.isNull(obj.Direction): + v2 = self.p1.sub(obj.Dimline) + v3 = self.p4.sub(obj.Dimline) + v2 = DraftVecUtils.project(v2,obj.Direction) + v3 = DraftVecUtils.project(v3,obj.Direction) + self.p2 = obj.Dimline.add(v2) + self.p3 = obj.Dimline.add(v3) + if DraftVecUtils.equals(self.p2,self.p3): + base = None + proj = None + else: + base = Part.LineSegment(self.p2,self.p3).toShape() + proj = DraftGeomUtils.findDistance(self.p1,base) + if proj: + proj = proj.negative() + if not base: + if DraftVecUtils.equals(self.p1,self.p4): + base = None + proj = None + else: + base = Part.LineSegment(self.p1,self.p4).toShape() + proj = DraftGeomUtils.findDistance(obj.Dimline,base) + if proj: + self.p2 = self.p1.add(proj.negative()) + self.p3 = self.p4.add(proj.negative()) + else: + self.p2 = self.p1 + self.p3 = self.p4 + if proj: + if hasattr(obj.ViewObject,"ExtLines") and hasattr(obj.ViewObject,"ScaleMultiplier"): + dmax = obj.ViewObject.ExtLines.Value * obj.ViewObject.ScaleMultiplier + if dmax and (proj.Length > dmax): + if (dmax > 0): + self.p1 = self.p2.add(DraftVecUtils.scaleTo(proj,dmax)) + self.p4 = self.p3.add(DraftVecUtils.scaleTo(proj,dmax)) + else: + rest = proj.Length + dmax + self.p1 = self.p2.add(DraftVecUtils.scaleTo(proj,rest)) + self.p4 = self.p3.add(DraftVecUtils.scaleTo(proj,rest)) + else: + proj = (self.p3.sub(self.p2)).cross(App.Vector(0,0,1)) + + # calculate the arrows positions + self.trans1.translation.setValue((self.p2.x,self.p2.y,self.p2.z)) + self.coord1.point.setValue((self.p2.x,self.p2.y,self.p2.z)) + self.trans2.translation.setValue((self.p3.x,self.p3.y,self.p3.z)) + self.coord2.point.setValue((self.p3.x,self.p3.y,self.p3.z)) + + # calculate dimension and extension lines overshoots positions + self.transDimOvershoot1.translation.setValue((self.p2.x,self.p2.y,self.p2.z)) + self.transDimOvershoot2.translation.setValue((self.p3.x,self.p3.y,self.p3.z)) + self.transExtOvershoot1.translation.setValue((self.p2.x,self.p2.y,self.p2.z)) + self.transExtOvershoot2.translation.setValue((self.p3.x,self.p3.y,self.p3.z)) + + # calculate the text position and orientation + if hasattr(obj,"Normal"): + if DraftVecUtils.isNull(obj.Normal): + if proj: + norm = (self.p3.sub(self.p2).cross(proj)).negative() + else: + norm = App.Vector(0,0,1) + else: + norm = App.Vector(obj.Normal) + else: + if proj: + norm = (self.p3.sub(self.p2).cross(proj)).negative() + else: + norm = App.Vector(0,0,1) + if not DraftVecUtils.isNull(norm): + norm.normalize() + u = self.p3.sub(self.p2) + u.normalize() + v1 = norm.cross(u) + rot1 = App.Placement(DraftVecUtils.getPlaneRotation(u,v1,norm)).Rotation.Q + self.transDimOvershoot1.rotation.setValue((rot1[0],rot1[1],rot1[2],rot1[3])) + self.transDimOvershoot2.rotation.setValue((rot1[0],rot1[1],rot1[2],rot1[3])) + if hasattr(obj.ViewObject,"FlipArrows"): + if obj.ViewObject.FlipArrows: + u = u.negative() + v2 = norm.cross(u) + rot2 = App.Placement(DraftVecUtils.getPlaneRotation(u,v2,norm)).Rotation.Q + self.trans1.rotation.setValue((rot2[0],rot2[1],rot2[2],rot2[3])) + self.trans2.rotation.setValue((rot2[0],rot2[1],rot2[2],rot2[3])) + if self.p1 != self.p2: + u3 = self.p1.sub(self.p2) + u3.normalize() + v3 = norm.cross(u3) + rot3 = App.Placement(DraftVecUtils.getPlaneRotation(u3,v3,norm)).Rotation.Q + self.transExtOvershoot1.rotation.setValue((rot3[0],rot3[1],rot3[2],rot3[3])) + self.transExtOvershoot2.rotation.setValue((rot3[0],rot3[1],rot3[2],rot3[3])) + if hasattr(obj.ViewObject,"TextSpacing") and hasattr(obj.ViewObject,"ScaleMultiplier"): + ts = obj.ViewObject.TextSpacing.Value * obj.ViewObject.ScaleMultiplier + offset = DraftVecUtils.scaleTo(v1,ts) + else: + offset = DraftVecUtils.scaleTo(v1,0.05) + rott = rot1 + if hasattr(obj.ViewObject,"FlipText"): + if obj.ViewObject.FlipText: + rott = App.Rotation(*rott).multiply(App.Rotation(norm,180)).Q + offset = offset.negative() + # setting text + try: + m = obj.ViewObject.DisplayMode + except: # swallow all exceptions here since it always fails on first run (Displaymode enum no set yet) + m = ["2D","3D"][utils.get_param("dimstyle",0)] + if m == "3D": + offset = offset.negative() + self.tbase = (self.p2.add((self.p3.sub(self.p2).multiply(0.5)))).add(offset) + if hasattr(obj.ViewObject,"TextPosition"): + if not DraftVecUtils.isNull(obj.ViewObject.TextPosition): + self.tbase = obj.ViewObject.TextPosition + self.textpos.translation.setValue([self.tbase.x,self.tbase.y,self.tbase.z]) + self.textpos.rotation = coin.SbRotation(rott[0],rott[1],rott[2],rott[3]) + su = True + if hasattr(obj.ViewObject,"ShowUnit"): + su = obj.ViewObject.ShowUnit + # set text value + l = self.p3.sub(self.p2).Length + unit = None + if hasattr(obj.ViewObject,"UnitOverride"): + unit = obj.ViewObject.UnitOverride + # special representation if "Building US" scheme + if App.ParamGet("User parameter:BaseApp/Preferences/Units").GetInt("UserSchema",0) == 5: + s = App.Units.Quantity(l,App.Units.Length).UserString + self.string = s.replace("' ","'- ") + self.string = s.replace("+"," ") + elif hasattr(obj.ViewObject,"Decimals"): + self.string = DraftGui.displayExternal(l,obj.ViewObject.Decimals,'Length',su,unit) + else: + self.string = DraftGui.displayExternal(l,None,'Length',su,unit) + if hasattr(obj.ViewObject,"Override"): + if obj.ViewObject.Override: + self.string = obj.ViewObject.Override.replace("$dim",\ + self.string) + self.text.string = self.text3d.string = utils.string_encode_coin(self.string) + + # set the lines + if m == "3D": + # calculate the spacing of the text + textsize = (len(self.string)*obj.ViewObject.FontSize.Value)/4.0 + spacing = ((self.p3.sub(self.p2)).Length/2.0) - textsize + self.p2a = self.p2.add(DraftVecUtils.scaleTo(self.p3.sub(self.p2),spacing)) + self.p2b = self.p3.add(DraftVecUtils.scaleTo(self.p2.sub(self.p3),spacing)) + self.coords.point.setValues([[self.p1.x,self.p1.y,self.p1.z], + [self.p2.x,self.p2.y,self.p2.z], + [self.p2a.x,self.p2a.y,self.p2a.z], + [self.p2b.x,self.p2b.y,self.p2b.z], + [self.p3.x,self.p3.y,self.p3.z], + [self.p4.x,self.p4.y,self.p4.z]]) + #self.line.numVertices.setValues([3,3]) + self.line.coordIndex.setValues(0,7,(0,1,2,-1,3,4,5)) + else: + self.coords.point.setValues([[self.p1.x,self.p1.y,self.p1.z], + [self.p2.x,self.p2.y,self.p2.z], + [self.p3.x,self.p3.y,self.p3.z], + [self.p4.x,self.p4.y,self.p4.z]]) + #self.line.numVertices.setValue(4) + self.line.coordIndex.setValues(0,4,(0,1,2,3)) + + def onChanged(self, vobj, prop): + """called when a view property has changed""" + if prop == "ScaleMultiplier" and hasattr(vobj,"ScaleMultiplier"): + # update all dimension values + if hasattr(self,"font"): + self.font.size = vobj.FontSize.Value*vobj.ScaleMultiplier + if hasattr(self,"font3d"): + self.font3d.size = vobj.FontSize.Value*100*vobj.ScaleMultiplier + if hasattr(self,"node") and hasattr(self,"p2") and hasattr(vobj,"ArrowSize"): + self.remove_dim_arrows() + self.draw_dim_arrows(vobj) + if hasattr(vobj,"DimOvershoot"): + self.remove_dim_overshoot() + self.draw_dim_overshoot(vobj) + if hasattr(vobj,"ExtOvershoot"): + self.remove_ext_overshoot() + self.draw_ext_overshoot(vobj) + self.updateData(vobj.Object,"Start") + vobj.Object.touch() + + elif (prop == "FontSize") and hasattr(vobj,"FontSize"): + if hasattr(self,"font") and hasattr(vobj,"ScaleMultiplier"): + self.font.size = vobj.FontSize.Value*vobj.ScaleMultiplier + if hasattr(self,"font3d") and hasattr(vobj,"ScaleMultiplier"): + self.font3d.size = vobj.FontSize.Value*100*vobj.ScaleMultiplier + vobj.Object.touch() + + elif (prop == "FontName") and hasattr(vobj,"FontName"): + if hasattr(self,"font") and hasattr(self,"font3d"): + self.font.name = self.font3d.name = str(vobj.FontName) + vobj.Object.touch() + + elif (prop == "LineColor") and hasattr(vobj,"LineColor"): + if hasattr(self,"color"): + c = vobj.LineColor + self.color.rgb.setValue(c[0],c[1],c[2]) + + elif (prop == "LineWidth") and hasattr(vobj,"LineWidth"): + if hasattr(self,"drawstyle"): + self.drawstyle.lineWidth = vobj.LineWidth + + elif (prop in ["ArrowSize","ArrowType"]) and hasattr(vobj,"ArrowSize"): + if hasattr(self,"node") and hasattr(self,"p2"): + if hasattr(vobj,"ScaleMultiplier"): + self.remove_dim_arrows() + self.draw_dim_arrows(vobj) + vobj.Object.touch() + + elif (prop == "DimOvershoot") and hasattr(vobj,"DimOvershoot"): + if hasattr(vobj,"ScaleMultiplier"): + self.remove_dim_overshoot() + self.draw_dim_overshoot(vobj) + vobj.Object.touch() + + elif (prop == "ExtOvershoot") and hasattr(vobj,"ExtOvershoot"): + if hasattr(vobj,"ScaleMultiplier"): + self.remove_ext_overshoot() + self.draw_ext_overshoot(vobj) + vobj.Object.touch() + + elif (prop == "ShowLine") and hasattr(vobj,"ShowLine"): + if vobj.ShowLine: + self.lineswitch2.whichChild = -3 + self.lineswitch3.whichChild = -3 + else: + self.lineswitch2.whichChild = -1 + self.lineswitch3.whichChild = -1 + else: + self.updateData(vobj.Object,"Start") + + def remove_dim_arrows(self): + # remove existing nodes + self.node.removeChild(self.marks) + self.node3d.removeChild(self.marks) + + def draw_dim_arrows(self, vobj): + from pivy import coin + + if not hasattr(vobj,"ArrowType"): + return + + if self.p3.x < self.p2.x: + inv = False + else: + inv = True + + # set scale + symbol = utils.ARROW_TYPES.index(vobj.ArrowType) + s = vobj.ArrowSize.Value * vobj.ScaleMultiplier + self.trans1.scaleFactor.setValue((s,s,s)) + self.trans2.scaleFactor.setValue((s,s,s)) + + + # set new nodes + self.marks = coin.SoSeparator() + self.marks.addChild(self.color) + s1 = coin.SoSeparator() + if symbol == "Circle": + s1.addChild(self.coord1) + else: + s1.addChild(self.trans1) + s1.addChild(gui_utils.dim_symbol(symbol,invert=not(inv))) + self.marks.addChild(s1) + s2 = coin.SoSeparator() + if symbol == "Circle": + s2.addChild(self.coord2) + else: + s2.addChild(self.trans2) + s2.addChild(gui_utils.dim_symbol(symbol,invert=inv)) + self.marks.addChild(s2) + self.node.insertChild(self.marks,2) + self.node3d.insertChild(self.marks,2) + + def remove_dim_overshoot(self): + self.node.removeChild(self.marksDimOvershoot) + self.node3d.removeChild(self.marksDimOvershoot) + + + def draw_dim_overshoot(self, vobj): + from pivy import coin + + # set scale + s = vobj.DimOvershoot.Value * vobj.ScaleMultiplier + self.transDimOvershoot1.scaleFactor.setValue((s,s,s)) + self.transDimOvershoot2.scaleFactor.setValue((s,s,s)) + + # remove existing nodes + + # set new nodes + self.marksDimOvershoot = coin.SoSeparator() + if vobj.DimOvershoot.Value: + self.marksDimOvershoot.addChild(self.color) + s1 = coin.SoSeparator() + s1.addChild(self.transDimOvershoot1) + s1.addChild(gui_utils.dimDash((-1,0,0),(0,0,0))) + self.marksDimOvershoot.addChild(s1) + s2 = coin.SoSeparator() + s2.addChild(self.transDimOvershoot2) + s2.addChild(gui_utils.dimDash((0,0,0),(1,0,0))) + self.marksDimOvershoot.addChild(s2) + self.node.insertChild(self.marksDimOvershoot,2) + self.node3d.insertChild(self.marksDimOvershoot,2) + + + def remove_ext_overshoot(self): + self.node.removeChild(self.marksExtOvershoot) + self.node3d.removeChild(self.marksExtOvershoot) + + + def draw_ext_overshoot(self, vobj): + from pivy import coin + + # set scale + s = vobj.ExtOvershoot.Value * vobj.ScaleMultiplier + self.transExtOvershoot1.scaleFactor.setValue((s,s,s)) + self.transExtOvershoot2.scaleFactor.setValue((s,s,s)) + + # set new nodes + self.marksExtOvershoot = coin.SoSeparator() + if vobj.ExtOvershoot.Value: + self.marksExtOvershoot.addChild(self.color) + s1 = coin.SoSeparator() + s1.addChild(self.transExtOvershoot1) + s1.addChild(gui_utils.dimDash((0,0,0),(-1,0,0))) + self.marksExtOvershoot.addChild(s1) + s2 = coin.SoSeparator() + s2.addChild(self.transExtOvershoot2) + s2.addChild(gui_utils.dimDash((0,0,0),(-1,0,0))) + self.marksExtOvershoot.addChild(s2) + self.node.insertChild(self.marksExtOvershoot,2) + self.node3d.insertChild(self.marksExtOvershoot,2) + + def is_linked_to_circle(self): + _obj = self.Object + if _obj.LinkedGeometry and len(_obj.LinkedGeometry) == 1: + lobj = _obj.LinkedGeometry[0][0] + lsub = _obj.LinkedGeometry[0][1] + if len(lsub) == 1 and "Edge" in lsub[0]: + n = int(lsub[0][4:]) - 1 + edge = lobj.Shape.Edges[n] + if DraftGeomUtils.geomType(edge) == "Circle": + return True + return False + + def getIcon(self): + if self.is_linked_to_circle(): + return ":/icons/Draft_DimensionRadius.svg" + return ":/icons/Draft_Dimension_Tree.svg" + + +class ViewProviderAngularDimension(ViewProviderDimensionBase): + """A View Provider for the Draft Angular Dimension object""" + def __init__(self, vobj): + + super(ViewProviderAngularDimension, self).__init__(vobj) + + vobj.addProperty("App::PropertyBool","FlipArrows", + "Graphics",QT_TRANSLATE_NOOP("App::Property", + "Rotate the dimension arrows 180 degrees")) + + self.Object = vobj.Object + vobj.Proxy = self + + def attach(self, vobj): + '''Setup the scene sub-graph of the view provider''' + from pivy import coin + self.Object = vobj.Object + self.color = coin.SoBaseColor() + if hasattr(vobj,"LineColor"): + self.color.rgb.setValue(vobj.LineColor[0],vobj.LineColor[1],vobj.LineColor[2]) + self.font = coin.SoFont() + self.font3d = coin.SoFont() + self.text = coin.SoAsciiText() + self.text3d = coin.SoText2() + self.text.string = "d" # some versions of coin crash if string is not set + self.text3d.string = "d" + self.text.justification = self.text3d.justification = coin.SoAsciiText.CENTER + self.textpos = coin.SoTransform() + label = coin.SoSeparator() + label.addChild(self.textpos) + label.addChild(self.color) + label.addChild(self.font) + label.addChild(self.text) + label3d = coin.SoSeparator() + label3d.addChild(self.textpos) + label3d.addChild(self.color) + label3d.addChild(self.font3d) + label3d.addChild(self.text3d) + self.coord1 = coin.SoCoordinate3() + self.trans1 = coin.SoTransform() + self.coord2 = coin.SoCoordinate3() + self.trans2 = coin.SoTransform() + self.marks = coin.SoSeparator() + self.drawstyle = coin.SoDrawStyle() + self.coords = coin.SoCoordinate3() + self.arc = coin.SoType.fromName("SoBrepEdgeSet").createInstance() + self.node = coin.SoGroup() + self.node.addChild(self.color) + self.node.addChild(self.drawstyle) + self.node.addChild(self.coords) + self.node.addChild(self.arc) + self.node.addChild(self.marks) + self.node.addChild(label) + self.node3d = coin.SoGroup() + self.node3d.addChild(self.color) + self.node3d.addChild(self.drawstyle) + self.node3d.addChild(self.coords) + self.node3d.addChild(self.arc) + self.node3d.addChild(self.marks) + self.node3d.addChild(label3d) + vobj.addDisplayMode(self.node,"2D") + vobj.addDisplayMode(self.node3d,"3D") + self.updateData(vobj.Object,None) + self.onChanged(vobj,"FontSize") + self.onChanged(vobj,"FontName") + self.onChanged(vobj,"ArrowType") + self.onChanged(vobj,"LineColor") + + def updateData(self, obj, prop): + if hasattr(self,"arc"): + from pivy import coin + import Part, DraftGeomUtils + import DraftGui + arcsegs = 24 + + # calculate the arc data + if DraftVecUtils.isNull(obj.Normal): + norm = App.Vector(0,0,1) + else: + norm = obj.Normal + radius = (obj.Dimline.sub(obj.Center)).Length + self.circle = Part.makeCircle(radius,obj.Center,norm,obj.FirstAngle.Value,obj.LastAngle.Value) + self.p2 = self.circle.Vertexes[0].Point + self.p3 = self.circle.Vertexes[-1].Point + mp = DraftGeomUtils.findMidpoint(self.circle.Edges[0]) + ray = mp.sub(obj.Center) + + # set text value + if obj.LastAngle.Value > obj.FirstAngle.Value: + a = obj.LastAngle.Value - obj.FirstAngle.Value + else: + a = (360 - obj.FirstAngle.Value) + obj.LastAngle.Value + su = True + if hasattr(obj.ViewObject,"ShowUnit"): + su = obj.ViewObject.ShowUnit + if hasattr(obj.ViewObject,"Decimals"): + self.string = DraftGui.displayExternal(a,obj.ViewObject.Decimals,'Angle',su) + else: + self.string = DraftGui.displayExternal(a,None,'Angle',su) + if obj.ViewObject.Override: + self.string = obj.ViewObject.Override.replace("$dim",\ + self.string) + self.text.string = self.text3d.string = utils.string_encode_coin(self.string) + + # check display mode + try: + m = obj.ViewObject.DisplayMode + except: # swallow all exceptions here since it always fails on first run (Displaymode enum no set yet) + m = ["2D","3D"][utils.get_param("dimstyle",0)] + + # set the arc + if m == "3D": + # calculate the spacing of the text + spacing = (len(self.string)*obj.ViewObject.FontSize.Value)/8.0 + pts1 = [] + cut = None + pts2 = [] + for i in range(arcsegs+1): + p = self.circle.valueAt(self.circle.FirstParameter+((self.circle.LastParameter-self.circle.FirstParameter)/arcsegs)*i) + if (p.sub(mp)).Length <= spacing: + if cut is None: + cut = i + else: + if cut is None: + pts1.append([p.x,p.y,p.z]) + else: + pts2.append([p.x,p.y,p.z]) + self.coords.point.setValues(pts1+pts2) + i1 = len(pts1) + i2 = i1+len(pts2) + self.arc.coordIndex.setValues(0,len(pts1)+len(pts2)+1,list(range(len(pts1)))+[-1]+list(range(i1,i2))) + if (len(pts1) >= 3) and (len(pts2) >= 3): + self.circle1 = Part.Arc(App.Vector(pts1[0][0],pts1[0][1],pts1[0][2]),App.Vector(pts1[1][0],pts1[1][1],pts1[1][2]),App.Vector(pts1[-1][0],pts1[-1][1],pts1[-1][2])).toShape() + self.circle2 = Part.Arc(App.Vector(pts2[0][0],pts2[0][1],pts2[0][2]),App.Vector(pts2[1][0],pts2[1][1],pts2[1][2]),App.Vector(pts2[-1][0],pts2[-1][1],pts2[-1][2])).toShape() + else: + pts = [] + for i in range(arcsegs+1): + p = self.circle.valueAt(self.circle.FirstParameter+((self.circle.LastParameter-self.circle.FirstParameter)/arcsegs)*i) + pts.append([p.x,p.y,p.z]) + self.coords.point.setValues(pts) + self.arc.coordIndex.setValues(0,arcsegs+1,list(range(arcsegs+1))) + + # set the arrow coords and rotation + self.trans1.translation.setValue((self.p2.x,self.p2.y,self.p2.z)) + self.coord1.point.setValue((self.p2.x,self.p2.y,self.p2.z)) + self.trans2.translation.setValue((self.p3.x,self.p3.y,self.p3.z)) + self.coord2.point.setValue((self.p3.x,self.p3.y,self.p3.z)) + # calculate small chords to make arrows look better + arrowlength = 4*obj.ViewObject.ArrowSize.Value + u1 = (self.circle.valueAt(self.circle.FirstParameter+arrowlength)).sub(self.circle.valueAt(self.circle.FirstParameter)).normalize() + u2 = (self.circle.valueAt(self.circle.LastParameter)).sub(self.circle.valueAt(self.circle.LastParameter-arrowlength)).normalize() + if hasattr(obj.ViewObject,"FlipArrows"): + if obj.ViewObject.FlipArrows: + u1 = u1.negative() + u2 = u2.negative() + w2 = self.circle.Curve.Axis + w1 = w2.negative() + v1 = w1.cross(u1) + v2 = w2.cross(u2) + q1 = App.Placement(DraftVecUtils.getPlaneRotation(u1,v1,w1)).Rotation.Q + q2 = App.Placement(DraftVecUtils.getPlaneRotation(u2,v2,w2)).Rotation.Q + self.trans1.rotation.setValue((q1[0],q1[1],q1[2],q1[3])) + self.trans2.rotation.setValue((q2[0],q2[1],q2[2],q2[3])) + + # setting text pos & rot + self.tbase = mp + if hasattr(obj.ViewObject,"TextPosition"): + if not DraftVecUtils.isNull(obj.ViewObject.TextPosition): + self.tbase = obj.ViewObject.TextPosition + + u3 = ray.cross(norm).normalize() + v3 = norm.cross(u3) + r = App.Placement(DraftVecUtils.getPlaneRotation(u3,v3,norm)).Rotation + offset = r.multVec(App.Vector(0,1,0)) + + if hasattr(obj.ViewObject,"TextSpacing"): + offset = DraftVecUtils.scaleTo(offset,obj.ViewObject.TextSpacing.Value) + else: + offset = DraftVecUtils.scaleTo(offset,0.05) + if m == "3D": + offset = offset.negative() + self.tbase = self.tbase.add(offset) + q = r.Q + self.textpos.translation.setValue([self.tbase.x,self.tbase.y,self.tbase.z]) + self.textpos.rotation = coin.SbRotation(q[0],q[1],q[2],q[3]) + + # set the angle property + if round(obj.Angle,utils.precision()) != round(a,utils.precision()): + obj.Angle = a + + def onChanged(self, vobj, prop): + if prop == "ScaleMultiplier" and hasattr(vobj,"ScaleMultiplier"): + # update all dimension values + if hasattr(self,"font"): + self.font.size = vobj.FontSize.Value*vobj.ScaleMultiplier + if hasattr(self,"font3d"): + self.font3d.size = vobj.FontSize.Value*100*vobj.ScaleMultiplier + if hasattr(self,"node") and hasattr(self,"p2") and hasattr(vobj,"ArrowSize"): + self.remove_dim_arrows() + self.draw_dim_arrows(vobj) + self.updateData(vobj.Object,"Start") + vobj.Object.touch() + elif prop == "FontSize" and hasattr(vobj,"ScaleMultiplier"): + if hasattr(self,"font"): + self.font.size = vobj.FontSize.Value*vobj.ScaleMultiplier + if hasattr(self,"font3d"): + self.font3d.size = vobj.FontSize.Value*100*vobj.ScaleMultiplier + vobj.Object.touch() + elif prop == "FontName": + if hasattr(self,"font") and hasattr(self,"font3d"): + self.font.name = self.font3d.name = str(vobj.FontName) + vobj.Object.touch() + elif prop == "LineColor": + if hasattr(self,"color") and hasattr(vobj,"LineColor"): + c = vobj.LineColor + self.color.rgb.setValue(c[0],c[1],c[2]) + elif prop == "LineWidth": + if hasattr(self,"drawstyle"): + self.drawstyle.lineWidth = vobj.LineWidth + elif prop in ["ArrowSize","ArrowType"] and hasattr(vobj,"ScaleMultiplier"): + if hasattr(self,"node") and hasattr(self,"p2"): + self.remove_dim_arrows() + self.draw_dim_arrows(vobj) + vobj.Object.touch() + else: + self.updateData(vobj.Object, None) + + def remove_dim_arrows(self): + # remove existing nodes + self.node.removeChild(self.marks) + self.node3d.removeChild(self.marks) + + def draw_dim_arrows(self, vobj): + from pivy import coin + + if not hasattr(vobj,"ArrowType"): + return + + # set scale + symbol = utils.ARROW_TYPES.index(vobj.ArrowType) + s = vobj.ArrowSize.Value * vobj.ScaleMultiplier + self.trans1.scaleFactor.setValue((s,s,s)) + self.trans2.scaleFactor.setValue((s,s,s)) + + # set new nodes + self.marks = coin.SoSeparator() + self.marks.addChild(self.color) + s1 = coin.SoSeparator() + if symbol == "Circle": + s1.addChild(self.coord1) + else: + s1.addChild(self.trans1) + s1.addChild(gui_utils.dim_symbol(symbol,invert=False)) + self.marks.addChild(s1) + s2 = coin.SoSeparator() + if symbol == "Circle": + s2.addChild(self.coord2) + else: + s2.addChild(self.trans2) + s2.addChild(gui_utils.dim_symbol(symbol,invert=True)) + self.marks.addChild(s2) + self.node.insertChild(self.marks,2) + self.node3d.insertChild(self.marks,2) + + def getIcon(self): + return ":/icons/Draft_DimensionAngular.svg" \ No newline at end of file diff --git a/src/Mod/Draft/draftviewproviders/view_draft_annotation.py b/src/Mod/Draft/draftviewproviders/view_draft_annotation.py new file mode 100644 index 0000000000..4f386a8686 --- /dev/null +++ b/src/Mod/Draft/draftviewproviders/view_draft_annotation.py @@ -0,0 +1,118 @@ +# *************************************************************************** +# * * +# * This file is part of the FreeCAD CAx development system. * +# * * +# * This program is free software; you can redistribute it and/or modify * +# * it under the terms of the GNU Lesser General Public License (LGPL) * +# * as published by the Free Software Foundation; either version 2 of * +# * the License, or (at your option) any later version. * +# * for detail see the LICENCE text file. * +# * * +# * FreeCAD is distributed in the hope that it will be useful, * +# * but WITHOUT ANY WARRANTY; without even the implied warranty of * +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# * GNU Library General Public License for more details. * +# * * +# * You should have received a copy of the GNU Library General Public * +# * License along with FreeCAD; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# *************************************************************************** +"""This module provides the Draft Annotations view provider base class +""" +## @package annotation +# \ingroup DRAFT +# \brief This module provides the Draft Annotations view provider base class + + +import FreeCAD as App +from PySide.QtCore import QT_TRANSLATE_NOOP + +if App.GuiUp: + import FreeCADGui as Gui + + +class ViewProviderDraftAnnotation(object): + """ + The base class for Draft Annotation Viewproviders + This class is not used directly, but inherited by all annotation + view providers. + """ + + def __init__(self, vobj): + #vobj.Proxy = self + #self.Object = vobj.Object + + # annotation properties + vobj.addProperty("App::PropertyFloat","ScaleMultiplier", + "Annotation",QT_TRANSLATE_NOOP("App::Property", + "Dimension size overall multiplier")) + + # graphics properties + vobj.addProperty("App::PropertyFloat","LineWidth", + "Graphics",QT_TRANSLATE_NOOP("App::Property","Line width")) + vobj.addProperty("App::PropertyColor","LineColor", + "Graphics",QT_TRANSLATE_NOOP("App::Property","Line color")) + + param = App.ParamGet("User parameter:BaseApp/Preferences/Mod/Draft") + annotation_scale = param.GetFloat("DraftAnnotationScale", 1.0) + if annotation_scale != 0: + vobj.ScaleMultiplier = 1 / annotation_scale + + + def __getstate__(self): + return None + + def __setstate__(self, state): + return None + + def attach(self,vobj): + self.Object = vobj.Object + return + + def updateData(self, obj, prop): + return + + def getDisplayModes(self, vobj): + modes=[] + return modes + + def setDisplayMode(self, mode): + return mode + + def onChanged(self, vobj, prop): + return + + def execute(self,vobj): + return + + def setEdit(self,vobj,mode=0): + if mode == 0: + Gui.runCommand("Draft_Edit") + return True + return False + + def unsetEdit(self,vobj,mode=0): + if App.activeDraftCommand: + App.activeDraftCommand.finish() + Gui.Control.closeDialog() + return False + + def getIcon(self): + return ":/icons/Draft_Draft.svg" + + def claimChildren(self): + """perhaps this is not useful???""" + objs = [] + if hasattr(self.Object,"Base"): + objs.append(self.Object.Base) + if hasattr(self.Object,"Objects"): + objs.extend(self.Object.Objects) + if hasattr(self.Object,"Components"): + objs.extend(self.Object.Components) + if hasattr(self.Object,"Group"): + objs.extend(self.Object.Group) + return objs + + diff --git a/src/Mod/Draft/draftviewproviders/view_label.py b/src/Mod/Draft/draftviewproviders/view_label.py new file mode 100644 index 0000000000..8f48ac063e --- /dev/null +++ b/src/Mod/Draft/draftviewproviders/view_label.py @@ -0,0 +1,322 @@ +# *************************************************************************** +# * Copyright (c) 2009, 2010 Yorik van Havre * +# * Copyright (c) 2009, 2010 Ken Cline * +# * * +# * This file is part of the FreeCAD CAx development system. * +# * * +# * This program is free software; you can redistribute it and/or modify * +# * it under the terms of the GNU Lesser General Public License (LGPL) * +# * as published by the Free Software Foundation; either version 2 of * +# * the License, or (at your option) any later version. * +# * for detail see the LICENCE text file. * +# * * +# * FreeCAD is distributed in the hope that it will be useful, * +# * but WITHOUT ANY WARRANTY; without even the implied warranty of * +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# * GNU Library General Public License for more details. * +# * * +# * You should have received a copy of the GNU Library General Public * +# * License along with FreeCAD; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# *************************************************************************** +"""This module provides the Draft Label view provider classes +""" +## @package label +# \ingroup DRAFT +# \brief This module provides the view provider code for Draft Label. + + +import FreeCAD as App +import DraftVecUtils, DraftGeomUtils +import math, sys +from pivy import coin +from PySide.QtCore import QT_TRANSLATE_NOOP +import draftutils.utils as utils +import draftutils.gui_utils as gui_utils +from draftviewproviders.view_draft_annotation import ViewProviderDraftAnnotation + +if App.GuiUp: + import FreeCADGui as Gui + + +class ViewProviderLabel(ViewProviderDraftAnnotation): + """A View Provider for the Label annotation object""" + + def __init__(self,vobj): + + super(ViewProviderLabel, self).__init__(vobj) + + self.set_properties(vobj) + + self.Object = vobj.Object + vobj.Proxy = self + + def set_properties(self, vobj): + + # Text properties + + vobj.addProperty("App::PropertyLength","TextSize", + "Text",QT_TRANSLATE_NOOP("App::Property", + "The size of the text")) + + vobj.addProperty("App::PropertyFont","TextFont", + "Text",QT_TRANSLATE_NOOP("App::Property", + "The font of the text")) + + vobj.addProperty("App::PropertyEnumeration","TextAlignment", + "Text",QT_TRANSLATE_NOOP("App::Property", + "The vertical alignment of the text")) + + vobj.addProperty("App::PropertyColor","TextColor", + "Text",QT_TRANSLATE_NOOP("App::Property", + "Text color")) + + vobj.addProperty("App::PropertyInteger","MaxChars", + "Text",QT_TRANSLATE_NOOP("App::Property", + "The maximum number of characters on each line of the text box")) + + # Graphics properties + + vobj.addProperty("App::PropertyLength","ArrowSize", + "Graphics",QT_TRANSLATE_NOOP("App::Property", + "The size of the arrow")) + + vobj.addProperty("App::PropertyEnumeration","ArrowType", + "Graphics",QT_TRANSLATE_NOOP("App::Property", + "The type of arrow of this label")) + + vobj.addProperty("App::PropertyEnumeration","Frame", + "Graphics",QT_TRANSLATE_NOOP("App::Property", + "The type of frame around the text of this object")) + + vobj.addProperty("App::PropertyBool","Line", + "Graphics",QT_TRANSLATE_NOOP("App::Property", + "Display a leader line or not")) + + vobj.addProperty("App::PropertyFloat","LineWidth", + "Graphics",QT_TRANSLATE_NOOP("App::Property", + "Line width")) + + vobj.addProperty("App::PropertyColor","LineColor", + "Graphics",QT_TRANSLATE_NOOP("App::Property", + "Line color")) + + self.Object = vobj.Object + vobj.TextAlignment = ["Top","Middle","Bottom"] + vobj.TextAlignment = "Middle" + vobj.LineWidth = utils.get_param("linewidth",1) + vobj.TextFont = utils.get_param("textfont") + vobj.TextSize = utils.get_param("textheight",1) + vobj.ArrowSize = utils.get_param("arrowsize",1) + vobj.ArrowType = utils.ARROW_TYPES + vobj.ArrowType = utils.ARROW_TYPES[utils.get_param("dimsymbol")] + vobj.Frame = ["None","Rectangle"] + vobj.Line = True + param = App.ParamGet("User parameter:BaseApp/Preferences/Mod/Draft") + annotation_scale = param.GetFloat("DraftAnnotationScale", 1.0) + vobj.ScaleMultiplier = 1 / annotation_scale + + + def getIcon(self): + return ":/icons/Draft_Label.svg" + + def claimChildren(self): + return [] + + def attach(self,vobj): + '''Setup the scene sub-graph of the view provider''' + self.arrow = coin.SoSeparator() + self.arrowpos = coin.SoTransform() + self.arrow.addChild(self.arrowpos) + self.matline = coin.SoMaterial() + self.drawstyle = coin.SoDrawStyle() + self.drawstyle.style = coin.SoDrawStyle.LINES + self.lcoords = coin.SoCoordinate3() + self.line = coin.SoType.fromName("SoBrepEdgeSet").createInstance() + self.mattext = coin.SoMaterial() + textdrawstyle = coin.SoDrawStyle() + textdrawstyle.style = coin.SoDrawStyle.FILLED + self.textpos = coin.SoTransform() + self.font = coin.SoFont() + self.text2d = coin.SoText2() + self.text3d = coin.SoAsciiText() + self.text2d.string = self.text3d.string = "Label" # need to init with something, otherwise, crash! + self.text2d.justification = coin.SoText2.RIGHT + self.text3d.justification = coin.SoAsciiText.RIGHT + self.fcoords = coin.SoCoordinate3() + self.frame = coin.SoType.fromName("SoBrepEdgeSet").createInstance() + self.lineswitch = coin.SoSwitch() + switchnode = coin.SoSeparator() + switchnode.addChild(self.line) + switchnode.addChild(self.arrow) + self.lineswitch.addChild(switchnode) + self.lineswitch.whichChild = 0 + self.node2d = coin.SoGroup() + self.node2d.addChild(self.matline) + self.node2d.addChild(self.arrow) + self.node2d.addChild(self.drawstyle) + self.node2d.addChild(self.lcoords) + self.node2d.addChild(self.lineswitch) + self.node2d.addChild(self.mattext) + self.node2d.addChild(textdrawstyle) + self.node2d.addChild(self.textpos) + self.node2d.addChild(self.font) + self.node2d.addChild(self.text2d) + self.node2d.addChild(self.fcoords) + self.node2d.addChild(self.frame) + self.node3d = coin.SoGroup() + self.node3d.addChild(self.matline) + self.node3d.addChild(self.arrow) + self.node3d.addChild(self.drawstyle) + self.node3d.addChild(self.lcoords) + self.node3d.addChild(self.lineswitch) + self.node3d.addChild(self.mattext) + self.node3d.addChild(textdrawstyle) + self.node3d.addChild(self.textpos) + self.node3d.addChild(self.font) + self.node3d.addChild(self.text3d) + self.node3d.addChild(self.fcoords) + self.node3d.addChild(self.frame) + vobj.addDisplayMode(self.node2d,"2D text") + vobj.addDisplayMode(self.node3d,"3D text") + self.onChanged(vobj,"LineColor") + self.onChanged(vobj,"TextColor") + self.onChanged(vobj,"ArrowSize") + self.onChanged(vobj,"Line") + + def getDisplayModes(self,vobj): + return ["2D text","3D text"] + + def getDefaultDisplayMode(self): + return "3D text" + + def setDisplayMode(self,mode): + return mode + + def updateData(self,obj,prop): + if prop == "Points": + from pivy import coin + if len(obj.Points) >= 2: + self.line.coordIndex.deleteValues(0) + self.lcoords.point.setValues(obj.Points) + self.line.coordIndex.setValues(0,len(obj.Points),range(len(obj.Points))) + self.onChanged(obj.ViewObject,"TextSize") + self.onChanged(obj.ViewObject,"ArrowType") + if obj.StraightDistance > 0: + self.text2d.justification = coin.SoText2.RIGHT + self.text3d.justification = coin.SoAsciiText.RIGHT + else: + self.text2d.justification = coin.SoText2.LEFT + self.text3d.justification = coin.SoAsciiText.LEFT + elif prop == "Text": + if obj.Text: + if sys.version_info.major >= 3: + self.text2d.string.setValues([l for l in obj.Text if l]) + self.text3d.string.setValues([l for l in obj.Text if l]) + else: + self.text2d.string.setValues([l.encode("utf8") for l in obj.Text if l]) + self.text3d.string.setValues([l.encode("utf8") for l in obj.Text if l]) + self.onChanged(obj.ViewObject,"TextAlignment") + + def getTextSize(self,vobj): + if vobj.DisplayMode == "3D text": + text = self.text3d + else: + text = self.text2d + v = Gui.ActiveDocument.ActiveView.getViewer().getSoRenderManager().getViewportRegion() + b = coin.SoGetBoundingBoxAction(v) + text.getBoundingBox(b) + return b.getBoundingBox().getSize().getValue() + + def onChanged(self,vobj,prop): + if prop == "ScaleMultiplier": + if not hasattr(vobj,"ScaleMultiplier"): + return + if hasattr(vobj,"TextSize") and hasattr(vobj,"TextAlignment"): + self.update_label(vobj) + if hasattr(vobj,"ArrowSize"): + s = vobj.ArrowSize.Value * vobj.ScaleMultiplier + if s: + self.arrowpos.scaleFactor.setValue((s,s,s)) + elif prop == "LineColor": + if hasattr(vobj,"LineColor"): + l = vobj.LineColor + self.matline.diffuseColor.setValue([l[0],l[1],l[2]]) + elif prop == "TextColor": + if hasattr(vobj,"TextColor"): + l = vobj.TextColor + self.mattext.diffuseColor.setValue([l[0],l[1],l[2]]) + elif prop == "LineWidth": + if hasattr(vobj,"LineWidth"): + self.drawstyle.lineWidth = vobj.LineWidth + elif (prop == "TextFont"): + if hasattr(vobj,"TextFont"): + self.font.name = vobj.TextFont.encode("utf8") + elif prop in ["TextSize","TextAlignment"] and hasattr(vobj,"ScaleMultiplier"): + if hasattr(vobj,"TextSize") and hasattr(vobj,"TextAlignment"): + self.update_label(vobj) + elif prop == "Line": + if hasattr(vobj,"Line"): + if vobj.Line: + self.lineswitch.whichChild = 0 + else: + self.lineswitch.whichChild = -1 + elif prop == "ArrowType": + if hasattr(vobj,"ArrowType"): + if len(vobj.Object.Points) > 1: + if hasattr(self,"symbol"): + if self.arrow.findChild(self.symbol) != -1: + self.arrow.removeChild(self.symbol) + s = utils.ARROW_TYPES.index(vobj.ArrowType) + self.symbol = gui_utils.dim_symbol(s) + self.arrow.addChild(self.symbol) + self.arrowpos.translation.setValue(vobj.Object.Points[-1]) + v1 = vobj.Object.Points[-2].sub(vobj.Object.Points[-1]) + if not DraftVecUtils.isNull(v1): + v1.normalize() + v2 = App.Vector(0,0,1) + if round(v2.getAngle(v1),4) in [0,round(math.pi,4)]: + v2 = App.Vector(0,1,0) + v3 = v1.cross(v2).negative() + q = App.Placement(DraftVecUtils.getPlaneRotation(v1,v3,v2)).Rotation.Q + self.arrowpos.rotation.setValue((q[0],q[1],q[2],q[3])) + elif prop == "ArrowSize": + if hasattr(vobj,"ArrowSize") and hasattr(vobj,"ScaleMultiplier"): + s = vobj.ArrowSize.Value * vobj.ScaleMultiplier + if s: + self.arrowpos.scaleFactor.setValue((s,s,s)) + elif prop == "Frame": + if hasattr(vobj,"Frame"): + self.frame.coordIndex.deleteValues(0) + if vobj.Frame == "Rectangle": + tsize = self.getTextSize(vobj) + pts = [] + base = vobj.Object.Placement.Base.sub(App.Vector(self.textpos.translation.getValue().getValue())) + pts.append(base.add(App.Vector(0,tsize[1]*3,0))) + pts.append(pts[-1].add(App.Vector(-tsize[0]*6,0,0))) + pts.append(pts[-1].add(App.Vector(0,-tsize[1]*6,0))) + pts.append(pts[-1].add(App.Vector(tsize[0]*6,0,0))) + pts.append(pts[0]) + self.fcoords.point.setValues(pts) + self.frame.coordIndex.setValues(0,len(pts),range(len(pts))) + + + def update_label(self, vobj): + self.font.size = vobj.TextSize.Value * vobj.ScaleMultiplier + v = App.Vector(1,0,0) + if vobj.Object.StraightDistance > 0: + v = v.negative() + v.multiply(vobj.TextSize/10) + tsize = self.getTextSize(vobj) + if (tsize is not None) and (len(vobj.Object.Text) > 1): + v = v.add(App.Vector(0,(tsize[1]-1)*2,0)) + if vobj.TextAlignment == "Top": + v = v.add(App.Vector(0,-tsize[1]*2,0)) + elif vobj.TextAlignment == "Middle": + v = v.add(App.Vector(0,-tsize[1],0)) + v = vobj.Object.Placement.Rotation.multVec(v) + pos = vobj.Object.Placement.Base.add(v) + self.textpos.translation.setValue(pos) + self.textpos.rotation.setValue(vobj.Object.Placement.Rotation.Q) \ No newline at end of file diff --git a/src/Mod/Draft/draftviewproviders/view_text.py b/src/Mod/Draft/draftviewproviders/view_text.py new file mode 100644 index 0000000000..bbf2636315 --- /dev/null +++ b/src/Mod/Draft/draftviewproviders/view_text.py @@ -0,0 +1,180 @@ +# *************************************************************************** +# * Copyright (c) 2009, 2010 Yorik van Havre * +# * Copyright (c) 2009, 2010 Ken Cline * +# * * +# * This file is part of the FreeCAD CAx development system. * +# * * +# * This program is free software; you can redistribute it and/or modify * +# * it under the terms of the GNU Lesser General Public License (LGPL) * +# * as published by the Free Software Foundation; either version 2 of * +# * the License, or (at your option) any later version. * +# * for detail see the LICENCE text file. * +# * * +# * FreeCAD is distributed in the hope that it will be useful, * +# * but WITHOUT ANY WARRANTY; without even the implied warranty of * +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# * GNU Library General Public License for more details. * +# * * +# * You should have received a copy of the GNU Library General Public * +# * License along with FreeCAD; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# *************************************************************************** +"""This module provides the Draft Text view provider classes +""" +## @package text +# \ingroup DRAFT +# \brief This module provides the view provider code for Draft Text. + + +import FreeCAD as App +import DraftVecUtils, DraftGeomUtils +import math, sys +from pivy import coin +from PySide.QtCore import QT_TRANSLATE_NOOP +import draftutils.utils as utils +import draftutils.gui_utils as gui_utils +from draftviewproviders.view_draft_annotation import ViewProviderDraftAnnotation + + +class ViewProviderText(ViewProviderDraftAnnotation): + """A View Provider for the Draft Text annotation""" + + + def __init__(self,vobj): + + super(ViewProviderText, self).__init__(vobj) + + self.set_properties(vobj) + + self.Object = vobj.Object + vobj.Proxy = self + + + def set_properties(self, vobj): + + vobj.addProperty("App::PropertyFloat","ScaleMultiplier", + "Annotation",QT_TRANSLATE_NOOP("App::Property", + "Dimension size overall multiplier")) + + vobj.addProperty("App::PropertyLength","FontSize", + "Text",QT_TRANSLATE_NOOP("App::Property", + "The size of the text")) + vobj.addProperty("App::PropertyFont","FontName", + "Text",QT_TRANSLATE_NOOP("App::Property", + "The font of the text")) + vobj.addProperty("App::PropertyEnumeration","Justification", + "Text",QT_TRANSLATE_NOOP("App::Property", + "The vertical alignment of the text")) + vobj.addProperty("App::PropertyColor","TextColor", + "Text",QT_TRANSLATE_NOOP("App::Property", + "Text color")) + vobj.addProperty("App::PropertyFloat","LineSpacing", + "Text",QT_TRANSLATE_NOOP("App::Property", + "Line spacing (relative to font size)")) + + param = App.ParamGet("User parameter:BaseApp/Preferences/Mod/Draft") + annotation_scale = param.GetFloat("DraftAnnotationScale", 1.0) + vobj.ScaleMultiplier = 1 / annotation_scale + + vobj.Justification = ["Left","Center","Right"] + vobj.FontName = utils.get_param("textfont","sans") + vobj.FontSize = utils.get_param("textheight",1) + + + def getIcon(self): + return ":/icons/Draft_Text.svg" + + + def claimChildren(self): + return [] + + + def attach(self,vobj): + '''Setup the scene sub-graph of the view provider''' + self.mattext = coin.SoMaterial() + textdrawstyle = coin.SoDrawStyle() + textdrawstyle.style = coin.SoDrawStyle.FILLED + self.trans = coin.SoTransform() + self.font = coin.SoFont() + self.text2d = coin.SoAsciiText() + self.text3d = coin.SoText2() + self.text2d.string = self.text3d.string = "Label" # need to init with something, otherwise, crash! + self.text2d.justification = coin.SoAsciiText.LEFT + self.text3d.justification = coin.SoText2.LEFT + self.node2d = coin.SoGroup() + self.node2d.addChild(self.trans) + self.node2d.addChild(self.mattext) + self.node2d.addChild(textdrawstyle) + self.node2d.addChild(self.font) + self.node2d.addChild(self.text2d) + self.node3d = coin.SoGroup() + self.node3d.addChild(self.trans) + self.node3d.addChild(self.mattext) + self.node3d.addChild(textdrawstyle) + self.node3d.addChild(self.font) + self.node3d.addChild(self.text3d) + vobj.addDisplayMode(self.node2d,"2D text") + vobj.addDisplayMode(self.node3d,"3D text") + self.onChanged(vobj,"TextColor") + self.onChanged(vobj,"FontSize") + self.onChanged(vobj,"FontName") + self.onChanged(vobj,"Justification") + self.onChanged(vobj,"LineSpacing") + + + def getDisplayModes(self,vobj): + return ["2D text","3D text"] + + + def setDisplayMode(self,mode): + return mode + + + def updateData(self,obj,prop): + if prop == "Text": + if obj.Text: + if sys.version_info.major >= 3: + self.text2d.string.setValues([l for l in obj.Text if l]) + self.text3d.string.setValues([l for l in obj.Text if l]) + else: + self.text2d.string.setValues([l.encode("utf8") for l in obj.Text if l]) + self.text3d.string.setValues([l.encode("utf8") for l in obj.Text if l]) + elif prop == "Placement": + self.trans.translation.setValue(obj.Placement.Base) + self.trans.rotation.setValue(obj.Placement.Rotation.Q) + + + def onChanged(self,vobj,prop): + if prop == "ScaleMultiplier": + if "ScaleMultiplier" in vobj.PropertiesList and "FontSize" in vobj.PropertiesList: + self.font.size = vobj.FontSize.Value * vobj.ScaleMultiplier + elif prop == "TextColor": + if "TextColor" in vobj.PropertiesList: + l = vobj.TextColor + self.mattext.diffuseColor.setValue([l[0],l[1],l[2]]) + elif (prop == "FontName"): + if "FontName" in vobj.PropertiesList: + self.font.name = vobj.FontName.encode("utf8") + elif prop == "FontSize": + if "FontSize" in vobj.PropertiesList and "ScaleMultiplier" in vobj.PropertiesList: + self.font.size = vobj.FontSize.Value * vobj.ScaleMultiplier + elif prop == "Justification": + try: + if getattr(vobj, "Justification", None) is not None: + if vobj.Justification == "Left": + self.text2d.justification = coin.SoAsciiText.LEFT + self.text3d.justification = coin.SoText2.LEFT + elif vobj.Justification == "Right": + self.text2d.justification = coin.SoAsciiText.RIGHT + self.text3d.justification = coin.SoText2.RIGHT + else: + self.text2d.justification = coin.SoAsciiText.CENTER + self.text3d.justification = coin.SoText2.CENTER + except AssertionError: + pass # Race condition - Justification enum has not been set yet + elif prop == "LineSpacing": + if "LineSpacing" in vobj.PropertiesList: + self.text2d.spacing = vobj.LineSpacing + self.text3d.spacing = vobj.LineSpacing \ No newline at end of file diff --git a/src/Mod/Draft/importDWG.py b/src/Mod/Draft/importDWG.py index 54dd50d35f..2820d02fca 100644 --- a/src/Mod/Draft/importDWG.py +++ b/src/Mod/Draft/importDWG.py @@ -261,7 +261,7 @@ def convertToDwg(dxffilename, dwgfilename): import shutil if shutil.which("dxf2dwg"): - proc = subprocess.Popen(("dxf2dwg", dxffilename, "-o", dwgfilename)) + proc = subprocess.Popen(("dxf2dwg", dxffilename, "-y", "-o", dwgfilename)) proc.communicate() return dwgfilename diff --git a/src/Mod/Draft/importDXF.py b/src/Mod/Draft/importDXF.py index 760b3a8654..e972cb8816 100644 --- a/src/Mod/Draft/importDXF.py +++ b/src/Mod/Draft/importDXF.py @@ -4169,7 +4169,7 @@ def readPreferences(): # reading parameters p = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Draft") if FreeCAD.GuiUp and p.GetBool("dxfShowDialog", False): - FreeCADGui.showPreferences("Import-Export", 2) + FreeCADGui.showPreferences("Import-Export", 3) global dxfCreatePart, dxfCreateDraft, dxfCreateSketch global dxfDiscretizeCurves, dxfStarBlocks global dxfMakeBlocks, dxfJoin, dxfRenderPolylineWidth diff --git a/src/Mod/Fem/CMakeLists.txt b/src/Mod/Fem/CMakeLists.txt index 18f21804ca..304ee9d7e3 100755 --- a/src/Mod/Fem/CMakeLists.txt +++ b/src/Mod/Fem/CMakeLists.txt @@ -72,6 +72,7 @@ SET(FemInOut_SRCS feminout/importCcxFrdResults.py feminout/importFenicsMesh.py feminout/importInpMesh.py + feminout/importPyMesh.py feminout/importToolsFem.py feminout/importVTKResults.py feminout/importYamlJsonMesh.py diff --git a/src/Mod/Fem/Init.py b/src/Mod/Fem/Init.py index f12096a6fa..d06b24904f 100644 --- a/src/Mod/Fem/Init.py +++ b/src/Mod/Fem/Init.py @@ -27,6 +27,8 @@ import FreeCAD +FreeCAD.addExportType("FEM mesh Python (*.meshpy)", "feminout.importPyMesh") + FreeCAD.addExportType("FEM mesh TetGen (*.poly)", "feminout.convert2TetGen") # see FemMesh::read() and FemMesh::write() methods in src/Mod/Fem/App/FemMesh.cpp diff --git a/src/Mod/Fem/femexamples/manager.py b/src/Mod/Fem/femexamples/manager.py index 8a499620b4..21ca3f2204 100644 --- a/src/Mod/Fem/femexamples/manager.py +++ b/src/Mod/Fem/femexamples/manager.py @@ -67,7 +67,7 @@ def run_analysis(doc, base_name, filepath=""): # print([obj.Name for obj in doc.Objects]) # filepath - if filepath is "": + if filepath == "": filepath = join(gettmp(), "FEM_examples") if not exists(filepath): makedirs(filepath) @@ -78,7 +78,7 @@ def run_analysis(doc, base_name, filepath=""): from femtools.femutils import is_derived_from if ( is_derived_from(m, "Fem::FemSolverObjectPython") - and m.Proxy.Type is not "Fem::FemSolverCalculixCcxTools" + and m.Proxy.Type != "Fem::FemSolverCalculixCcxTools" ): solver = m break diff --git a/src/Mod/Fem/femguiobjects/_ViewProviderFemResultMechanical.py b/src/Mod/Fem/femguiobjects/_ViewProviderFemResultMechanical.py index 5c1551fc91..b4e25b14be 100644 --- a/src/Mod/Fem/femguiobjects/_ViewProviderFemResultMechanical.py +++ b/src/Mod/Fem/femguiobjects/_ViewProviderFemResultMechanical.py @@ -703,6 +703,8 @@ def get_displacement_scale_factor(res_obj): z_span = abs(p_z_max - p_z_min) span = max(x_span, y_span, z_span) max_disp = max(x_max, y_max, z_max) + if max_disp == 0.0: + return 0.0 # avoid float division by zero # FIXME - add max_allowed_disp to Preferences max_allowed_disp = 0.01 * span scale = max_allowed_disp / max_disp diff --git a/src/Mod/Fem/feminout/importFenicsMesh.py b/src/Mod/Fem/feminout/importFenicsMesh.py index 2425c40131..4c4fd8643e 100644 --- a/src/Mod/Fem/feminout/importFenicsMesh.py +++ b/src/Mod/Fem/feminout/importFenicsMesh.py @@ -186,7 +186,7 @@ def export(objectslist, fileString, group_values_dict_nogui=None): writeFenicsXML.write_fenics_mesh_xml(obj, fileString) elif fileExtension.lower() == ".xdmf": mesh_groups = importToolsFem.get_FemMeshObjectMeshGroups(obj) - if mesh_groups is not (): + if mesh_groups != (): # if there are groups found, make task panel available if GuiUp if FreeCAD.GuiUp == 1: panel = WriteXDMFTaskPanel(obj, fileString) diff --git a/src/Mod/Fem/feminout/importPyMesh.py b/src/Mod/Fem/feminout/importPyMesh.py new file mode 100644 index 0000000000..7c64bec66a --- /dev/null +++ b/src/Mod/Fem/feminout/importPyMesh.py @@ -0,0 +1,148 @@ +# *************************************************************************** +# * Copyright (c) 2016 Bernd Hahnebach * +# * * +# * This file is part of the FreeCAD CAx development system. * +# * * +# * This program is free software; you can redistribute it and/or modify * +# * it under the terms of the GNU Lesser General Public License (LGPL) * +# * as published by the Free Software Foundation; either version 2 of * +# * the License, or (at your option) any later version. * +# * for detail see the LICENCE text file. * +# * * +# * This program is distributed in the hope that it will be useful, * +# * but WITHOUT ANY WARRANTY; without even the implied warranty of * +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# * GNU Library General Public License for more details. * +# * * +# * You should have received a copy of the GNU Library General Public * +# * License along with this program; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# *************************************************************************** + +__title__ = "FreeCAD Python Mesh reader and writer" +__author__ = "Bernd Hahnebach" +__url__ = "http://www.freecadweb.org" + +## @package importPyMesh +# \ingroup FEM +# \brief FreeCAD Python Mesh reader and writer for FEM workbench + +import FreeCAD +from femmesh import meshtools + +# ************************************************************************************************ +# ********* generic FreeCAD import and export methods ******************************************** +# names are fix given from FreeCAD, these methods are called from FreeCAD +# they are set in FEM modules Init.py + +if open.__module__ == "__builtin__": + # because we'll redefine open below (Python2) + pyopen = open +elif open.__module__ == "io": + # because we'll redefine open below (Python3) + pyopen = open + + +# export mesh to python +def export( + objectslist, + filename +): + "called when freecad exports a file" + if len(objectslist) != 1: + FreeCAD.Console.PrintError("This exporter can only export one object.\n") + return + obj = objectslist[0] + if not obj.isDerivedFrom("Fem::FemMeshObject"): + FreeCAD.Console.PrintError("No FEM mesh object selected.\n") + return + femnodes_mesh = obj.FemMesh.Nodes + femelement_table = meshtools.get_femelement_table(obj.FemMesh) + if meshtools.is_solid_femmesh(obj.FemMesh): + fem_mesh_type = "Solid" + elif meshtools.is_face_femmesh(obj.FemMesh): + fem_mesh_type = "Face" + elif meshtools.is_edge_femmesh(obj.FemMesh): + fem_mesh_type = "Edge" + else: + FreeCAD.Console.PrintError("Export of this FEM mesh to Python not supported.\n") + return + f = pyopen(filename, "w") + write_python_mesh_to_file(femnodes_mesh, femelement_table, fem_mesh_type, f) + f.close() + + +# ************************************************************************************************ +# ********* module specific methods ************************************************************** +# writer: +# - a method directly writes a FemMesh to the mesh file +# - a method takes a file handle, mesh data and writes to the file handle + +# ********* writer ******************************************************************************* + +def write( + fem_mesh, + filename +): + """directly write a FemMesh to a Python mesh file + fem_mesh: a FemMesh""" + + if not fem_mesh.isDerivedFrom("Fem::FemMesh"): + FreeCAD.Console.PrintError("Not a FemMesh was given as parameter.\n") + return + femnodes_mesh = fem_mesh.Nodes + femelement_table = meshtools.get_femelement_table(fem_mesh) + if meshtools.is_solid_femmesh(fem_mesh): + fem_mesh_type = "Solid" + elif meshtools.is_face_femmesh(fem_mesh): + fem_mesh_type = "Face" + elif meshtools.is_edge_femmesh(fem_mesh): + fem_mesh_type = "Edge" + else: + FreeCAD.Console.PrintError("Export of this FEM mesh to Python not supported.\n") + return + f = pyopen(filename, "w") + write_python_mesh_to_file(femnodes_mesh, femelement_table, fem_mesh_type, f) + f.close() + + +def write_python_mesh_to_file(femnodes_mesh, femelement_table, fem_mesh_type, f): + + mesh_name = "femmesh" + + # nodes + f.write("def create_nodes(femmesh):\n") + f.write(" # nodes\n") + for node in femnodes_mesh: + # print(node, ' --> ', femnodes_mesh[node]) + vec = femnodes_mesh[node] + f.write( + " {0}.addNode({1}, {2}, {3}, {4})\n" + .format(mesh_name, vec.x, vec.y, vec.z, node) + ) + f.write(" return True\n") + f.write("\n\n") + + # elements + f.write("def create_elements(femmesh):\n") + f.write(" # elements\n") + for element in femelement_table: + # print(element, ' --> ', femelement_table[element]) + if fem_mesh_type == "Solid": + f.write( + " {0}.addVolume({1}, {2})\n" + .format(mesh_name, list(femelement_table[element]), element) + ) + elif fem_mesh_type == "Face": + f.write( + " {0}.addFace({1}, {2})\n" + .format(mesh_name, list(femelement_table[element]), element) + ) + elif fem_mesh_type == "Edge": + f.write( + " {0}.addEdge({1}, {2})\n" + .format(mesh_name, list(femelement_table[element]), element) + ) + f.write(" return True\n") diff --git a/src/Mod/Fem/feminout/writeFenicsXDMF.py b/src/Mod/Fem/feminout/writeFenicsXDMF.py index cd3a4fb6bd..4d82b2007a 100644 --- a/src/Mod/Fem/feminout/writeFenicsXDMF.py +++ b/src/Mod/Fem/feminout/writeFenicsXDMF.py @@ -317,7 +317,7 @@ def write_fenics_mesh_xdmf( fem_mesh = fem_mesh_obj.FemMesh gmshgroups = get_FemMeshObjectMeshGroups(fem_mesh_obj) - if gmshgroups is not (): + if gmshgroups != (): Console.PrintMessage("found mesh groups\n") for g in gmshgroups: diff --git a/src/Mod/Fem/femmesh/meshtools.py b/src/Mod/Fem/femmesh/meshtools.py index a35a4d82d8..c3f1cd0a96 100644 --- a/src/Mod/Fem/femmesh/meshtools.py +++ b/src/Mod/Fem/femmesh/meshtools.py @@ -1426,6 +1426,7 @@ def build_mesh_faces_of_volume_elements( FreeCAD.Console.PrintLog("VolElement: {}\n".format(veID)) vol_node_ct = len(femelement_table[veID]) face_node_indexs = sorted(face_nodenumber_table[veID]) + node_numbers = () if vol_node_ct == 10: FreeCAD.Console.PrintLog(" --> tetra10 --> tria6 face\n") # node order of face in tetra10 volume element @@ -1441,7 +1442,7 @@ def build_mesh_faces_of_volume_elements( else: FreeCAD.Console.PrintError( "Error in build_mesh_faces_of_volume_elements(): " - "hexa20: face not found! {}\n" + "tetra10: face not found! {}\n" .format(face_node_indexs) ) elif vol_node_ct == 4: @@ -1459,7 +1460,7 @@ def build_mesh_faces_of_volume_elements( else: FreeCAD.Console.PrintError( "Error in build_mesh_faces_of_volume_elements(): " - "hexa20: face not found! {}\n" + "tetra4: face not found! {}\n" .format(face_node_indexs) ) elif vol_node_ct == 20: @@ -1503,7 +1504,7 @@ def build_mesh_faces_of_volume_elements( else: FreeCAD.Console.PrintError( "Error in build_mesh_faces_of_volume_elements(): " - "hexa20: face not found! {}\n" + "hexa8: face not found! {}\n" .format(face_node_indexs) ) elif vol_node_ct == 15: @@ -1543,7 +1544,7 @@ def build_mesh_faces_of_volume_elements( else: FreeCAD.Console.PrintError( "Error in build_mesh_faces_of_volume_elements(): " - "pent6: face not found! {}\n" + "penta6: face not found! {}\n" .format(face_node_indexs) ) else: @@ -1699,17 +1700,17 @@ def get_contact_obj_faces( "(example: multiple element faces per master or slave\n" ) - FreeCAD.Console.PrintLog("Slave: {}, {}\n".format(slave_ref[0].Name, slave_ref)) - FreeCAD.Console.PrintLog("Master: {}, {}\n".format(master_ref[0].Name, master_ref)) + FreeCAD.Console.PrintLog(" Slave: {}, {}\n".format(slave_ref[0].Name, slave_ref)) + FreeCAD.Console.PrintLog(" Master: {}, {}\n".format(master_ref[0].Name, master_ref)) if is_solid_femmesh(femmesh): - # get the nodes, sorted and duplicates removed + FreeCAD.Console.PrintLog(" Get the nodes, sorted and duplicates removed.\n") slaveface_nds = sorted(list(set(get_femnodes_by_refshape(femmesh, slave_ref)))) masterface_nds = sorted(list(set(get_femnodes_by_refshape(femmesh, master_ref)))) - # FreeCAD.Console.PrintLog("slaveface_nds: {}\n".format(slaveface_nds)) - # FreeCAD.Console.PrintLog("masterface_nds: {}\n".format(slaveface_nds)) + FreeCAD.Console.PrintLog(" slaveface_nds: {}\n".format(slaveface_nds)) + FreeCAD.Console.PrintLog(" masterface_nds: {}\n".format(slaveface_nds)) - # fill the bit_pattern_dict and search for the faces + FreeCAD.Console.PrintLog(" Fill the bit_pattern_dict and search for the faces.\n") slave_bit_pattern_dict = get_bit_pattern_dict( femelement_table, femnodes_ele_table, @@ -1721,16 +1722,18 @@ def get_contact_obj_faces( masterface_nds ) - # get the faces ids + FreeCAD.Console.PrintLog(" Get the FaceIDs.\n") slave_faces = get_ccxelement_faces_from_binary_search(slave_bit_pattern_dict) master_faces = get_ccxelement_faces_from_binary_search(master_bit_pattern_dict) elif is_face_femmesh(femmesh): slave_ref_shape = slave_ref[0].Shape.getElement(slave_ref[1][0]) master_ref_shape = master_ref[0].Shape.getElement(master_ref[1][0]) - # get the faces ids + + FreeCAD.Console.PrintLog(" Get the FaceIDs.\n") slave_face_ids = femmesh.getFacesByFace(slave_ref_shape) master_face_ids = femmesh.getFacesByFace(master_ref_shape) + # build slave_faces and master_faces # face 2 for tria6 element # is it face 2 for all shell elements @@ -1739,8 +1742,13 @@ def get_contact_obj_faces( for fid in master_face_ids: master_faces.append([fid, 2]) - FreeCAD.Console.PrintLog("slave_faces: {}\n".format(slave_faces)) - FreeCAD.Console.PrintLog("master_faces: {}\n".format(master_faces)) + FreeCAD.Console.PrintLog(" Master and slave face ready to use for writer:\n") + FreeCAD.Console.PrintLog(" slave_faces: {}\n".format(slave_faces)) + FreeCAD.Console.PrintLog(" master_faces: {}\n".format(master_faces)) + if len(slave_faces) == 0: + FreeCAD.Console.PrintError("No faces found for contact slave face.\n") + if len(master_faces) == 0: + FreeCAD.Console.PrintError("No faces found for contact master face.\n") return [slave_faces, master_faces] @@ -1846,21 +1854,36 @@ def get_analysis_group_elements( aAnalysis, aPart ): - """ all Reference shapes of all Analysis member are searched in the Shape of aPart. - If found in shape they are added to a dict - {ConstraintName : ["ShapeType of the Elements"], [ElementID, ElementID, ...], ...} """ + all Reference shapes of all Analysis member are searched in the Shape of aPart. + If found in shape they are added to a dict + {ConstraintName : ["ShapeType of the Elements"], [ElementID, ElementID, ...], ...} + """ + from femtools.femutils import is_of_type group_elements = {} # { name : [element, element, ... , element]} empty_references = [] + # find the objects with empty references, if there are more than one of this type + # they are for all shapes not in the references of the other objects + # ATM: empty references if there are more than one obj of this type are allowed for: + # solid meshes: material + # face meshes: materials, ShellThickness + # edge meshes: material, BeamSection/FluidSection + # BTW: some constraints do have empty references in any case (ex. constraint self weight) for m in aAnalysis.Group: - if hasattr(m, "References") and "ReadOnly" not in m.getEditorMode("References"): - # some C++ Constraints have a not used References Property - # it is set to Hidden in ReadOnly and PropertyEditor - if m.References: + if hasattr(m, "References"): + if len(m.References) > 0: grp_ele = get_reference_group_elements(m, aPart) group_elements[grp_ele[0]] = grp_ele[1] - else: - FreeCAD.Console.PrintMessage(" Empty reference: " + m.Name + "\n") + elif ( + len(m.References) == 0 + and ( + is_of_type(m, "Fem::Material") + # TODO test and implement ElementGeometry1D and ElementGeometry2D + # or is_of_type(m, "Fem::ElementGeometry1D") + # or is_of_type(m, "Fem::ElementGeometry2D") + ) + ): + FreeCAD.Console.PrintMessage(" Empty reference: {}\n".format(m.Name)) empty_references.append(m) if empty_references: if len(empty_references) == 1: @@ -1876,11 +1899,8 @@ def get_analysis_group_elements( FreeCAD.Console.PrintMessage( "We are going to try to get the empty material references anyway.\n" ) - # FemElementGeometry2D, ElementGeometry1D and - # FemElementFluid1D could have empty references, - # but on solid meshes only materials should have empty references for er in empty_references: - FreeCAD.Console.PrintMessage(er.Name + "\n") + FreeCAD.Console.PrintMessage("{}\n".format(er.Name)) group_elements = get_anlysis_empty_references_group_elements( group_elements, aAnalysis, @@ -1990,12 +2010,9 @@ def get_anlysis_empty_references_group_elements( aAnalysis, aShape ): - """get the elementIDs if the Reference shape is empty + """ + get the elementIDs if the Reference shape is empty see get_analysis_group_elements() for more information - on solid meshes only material objects could have an - empty reference without there being something wrong! - face meshes could have empty ShellThickness and - edge meshes could have empty BeamSection/FluidSection """ # FreeCAD.Console.PrintMessage("{}\n".format(group_elements)) material_ref_shapes = [] diff --git a/src/Mod/Fem/femobjects/_FemConstraintSelfWeight.py b/src/Mod/Fem/femobjects/_FemConstraintSelfWeight.py index 1e5d3e5917..95f47f8653 100644 --- a/src/Mod/Fem/femobjects/_FemConstraintSelfWeight.py +++ b/src/Mod/Fem/femobjects/_FemConstraintSelfWeight.py @@ -66,3 +66,9 @@ class _FemConstraintSelfWeight(FemConstraint.Proxy): obj.Gravity_x = 0.0 obj.Gravity_y = 0.0 obj.Gravity_z = -1.0 + + # https://wiki.freecadweb.org/Scripted_objects#Property_Type + # https://forum.freecadweb.org/viewtopic.php?f=18&t=13460&start=20#p109709 + # https://forum.freecadweb.org/viewtopic.php?t=25524 + # obj.setEditorMode("References", 1) # read only in PropertyEditor, but writeable by Python + obj.setEditorMode("References", 2) # do not show in Editor diff --git a/src/Mod/Mesh/App/CMakeLists.txt b/src/Mod/Mesh/App/CMakeLists.txt index ca6c698a70..7ef7aa1354 100644 --- a/src/Mod/Mesh/App/CMakeLists.txt +++ b/src/Mod/Mesh/App/CMakeLists.txt @@ -94,6 +94,10 @@ SET(Core_SRCS Core/Utilities.h Core/Visitor.cpp Core/Visitor.h + Core/CylinderFit.cpp + Core/CylinderFit.h + Core/SphereFit.cpp + Core/SphereFit.h ) SOURCE_GROUP("Core" FILES ${Core_SRCS}) diff --git a/src/Mod/Mesh/App/Core/Approximation.cpp b/src/Mod/Mesh/App/Core/Approximation.cpp index 97658d35fa..c04343db0d 100644 --- a/src/Mod/Mesh/App/Core/Approximation.cpp +++ b/src/Mod/Mesh/App/Core/Approximation.cpp @@ -32,6 +32,8 @@ #include "Approximation.h" #include "Elements.h" #include "Utilities.h" +#include "CylinderFit.h" +#include "SphereFit.h" #include #include @@ -1051,6 +1053,29 @@ float CylinderFit::Fit() _fRadius = float(radius); _fLastResult = double(fit); + +#if defined(_DEBUG) + Base::Console().Message(" WildMagic Cylinder Fit: Base: (%0.4f, %0.4f, %0.4f), Axis: (%0.6f, %0.6f, %0.6f), Radius: %0.4f, Std Dev: %0.4f\n", + _vBase.x, _vBase.y, _vBase.z, _vAxis.x, _vAxis.y, _vAxis.z, _fRadius, GetStdDeviation()); +#endif + + MeshCoreFit::CylinderFit cylFit; + cylFit.AddPoints(_vPoints); + //cylFit.SetApproximations(_fRadius, Base::Vector3d(_vBase.x, _vBase.y, _vBase.z), Base::Vector3d(_vAxis.x, _vAxis.y, _vAxis.z)); + + float result = cylFit.Fit(); + if (result < FLOAT_MAX) { + Base::Vector3d base = cylFit.GetBase(); + Base::Vector3d dir = cylFit.GetAxis(); +#if defined(_DEBUG) + Base::Console().Message("MeshCoreFit::Cylinder Fit: Base: (%0.4f, %0.4f, %0.4f), Axis: (%0.6f, %0.6f, %0.6f), Radius: %0.4f, Std Dev: %0.4f, Iterations: %d\n", + base.x, base.y, base.z, dir.x, dir.y, dir.z, cylFit.GetRadius(), cylFit.GetStdDeviation(), cylFit.GetNumIterations()); +#endif + _vBase = Base::convertTo(base); + _vAxis = Base::convertTo(dir); + _fRadius = (float)cylFit.GetRadius(); + _fLastResult = result; + } #else int m = static_cast(_vPoints.size()); int n = 7; @@ -1238,24 +1263,88 @@ float SphereFit::Fit() _fRadius = float(sphere.Radius); // TODO - _fLastResult = 0; + +#if defined(_DEBUG) + Base::Console().Message(" WildMagic Sphere Fit: Center: (%0.4f, %0.4f, %0.4f), Radius: %0.4f, Std Dev: %0.4f\n", + _vCenter.x, _vCenter.y, _vCenter.z, _fRadius, GetStdDeviation()); +#endif + + MeshCoreFit::SphereFit sphereFit; + sphereFit.AddPoints(_vPoints); + sphereFit.ComputeApproximations(); + float result = sphereFit.Fit(); + if (result < FLOAT_MAX) { + Base::Vector3d center = sphereFit.GetCenter(); +#if defined(_DEBUG) + Base::Console().Message("MeshCoreFit::Sphere Fit: Center: (%0.4f, %0.4f, %0.4f), Radius: %0.4f, Std Dev: %0.4f, Iterations: %d\n", + center.x, center.y, center.z, sphereFit.GetRadius(), sphereFit.GetStdDeviation(), sphereFit.GetNumIterations()); +#endif + _vCenter = Base::convertTo(center); + _fRadius = (float)sphereFit.GetRadius(); + _fLastResult = result; + } + return _fLastResult; } -float SphereFit::GetDistanceToSphere(const Base::Vector3f &) const +float SphereFit::GetDistanceToSphere(const Base::Vector3f& rcPoint) const { - return FLOAT_MAX; + float fResult = FLOAT_MAX; + if (_bIsFitted) { + fResult = Base::Vector3f(rcPoint - _vCenter).Length() - _fRadius; + } + return fResult; } float SphereFit::GetStdDeviation() const { - return FLOAT_MAX; + // Mean: M=(1/N)*SUM Xi + // Variance: VAR=(N/N-3)*[(1/N)*SUM(Xi^2)-M^2] + // Standard deviation: SD=SQRT(VAR) + // Standard error of the mean: SE=SD/SQRT(N) + if (!_bIsFitted) + return FLOAT_MAX; + + float fSumXi = 0.0f, fSumXi2 = 0.0f, + fMean = 0.0f, fDist = 0.0f; + + float ulPtCt = float(CountPoints()); + std::list< Base::Vector3f >::const_iterator cIt; + + for (cIt = _vPoints.begin(); cIt != _vPoints.end(); ++cIt) { + fDist = GetDistanceToSphere( *cIt ); + fSumXi += fDist; + fSumXi2 += ( fDist * fDist ); + } + + fMean = (1.0f / ulPtCt) * fSumXi; + return sqrt((ulPtCt / (ulPtCt - 3.0f)) * ((1.0f / ulPtCt) * fSumXi2 - fMean * fMean)); } void SphereFit::ProjectToSphere() { + for (std::list< Base::Vector3f >::iterator it = _vPoints.begin(); it != _vPoints.end(); ++it) { + Base::Vector3f& cPnt = *it; + // Compute unit vector from sphere centre to point. + // Because this vector is orthogonal to the sphere's surface at the + // intersection point we can easily compute the projection point on the + // closest surface point using the radius of the sphere + Base::Vector3f diff = cPnt - _vCenter; + double length = diff.Length(); + if (length == 0.0) + { + // Point is exactly at the sphere center, so it can be projected in any direction onto the sphere! + // So here just project in +Z direction + cPnt.z += _fRadius; + } + else + { + diff /= length; // normalizing the vector + cPnt = _vCenter + diff * _fRadius; + } + } } // ------------------------------------------------------------------------------- diff --git a/src/Mod/Mesh/App/Core/CylinderFit.cpp b/src/Mod/Mesh/App/Core/CylinderFit.cpp new file mode 100644 index 0000000000..3cd2fa770a --- /dev/null +++ b/src/Mod/Mesh/App/Core/CylinderFit.cpp @@ -0,0 +1,654 @@ +/*************************************************************************** + * Copyright (c) 2020 Graeme van der Vlugt * + * * + * This file is part of the FreeCAD CAx development system. * + * * + * This library is free software; you can redistribute it and/or * + * modify it under the terms of the GNU Library General Public * + * License as published by the Free Software Foundation; either * + * version 2 of the License, or (at your option) any later version. * + * * + * This library is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU Library General Public License for more details. * + * * + * You should have received a copy of the GNU Library General Public * + * License along with this library; see the file COPYING.LIB. If not, * + * write to the Free Software Foundation, Inc., 59 Temple Place, * + * Suite 330, Boston, MA 02111-1307, USA * + * * + ***************************************************************************/ + +// Definitions: +// Cylinder axis goes through a point (Xc,Yc,Zc) and has direction (L,M,N) +// Cylinder radius is R +// A point on the axis (X0i,Y0i,Z0i) can be described by: +// (X0i,Y0i,Z0i) = (Xc,Yc,Zc) + s(L,M,N) +// where s is the distance from (Xc,Yc,Zc) to (X0i,Y0i,Z0i) when (L,M,N) is +// of unit length (normalized). +// The distance between a cylinder surface point (Xi,Yi,Zi) and its +// projection onto the axis (X0i,Y0i,Z0i) is the radius: +// (Xi - X0i)^2 + (Yi - Y0i)^2 + (Zi - Z0i)^2 = R^2 +// Also the vector to a cylinder surface point (Xi,Yi,Zi) from its +// projection onto the axis (X0i,Y0i,Z0i) is orthogonal to the axis so we can +// write: +// (Xi - X0i, Yi - Y0i, Zi - Z0i).(L,M,N) = 0 or +// L(Xi - X0i) + M(Yi - Y0i) + N(Zi - Z0i) = 0 +// If we substitute these various equations into each other and further add +// the constraint that L^2 + M^2 + N^2 = 1 then we can arrive at a single +// equation for the cylinder surface points: +// (Xi - Xc + L*L*(Xc - Xi) + L*M*(Yc - Yi) + L*N*(Zc - Zi))^2 + +// (Yi - Yc + M*L*(Xc - Xi) + M*M*(Yc - Yi) + M*N*(Zc - Zi))^2 + +// (Zi - Zc + N*L*(Xc - Xi) + N*M*(Yc - Yi) + N*N*(Zc - Zi))^2 - R^2 = 0 +// This equation is what is used in the least squares solution below. Because +// we are constraining the direction vector to a unit length and also because +// we need to stop the axis point from moving along the axis we need to fix one +// of the ordinates in the solution. So from our initial approximations for the +// axis direction (L0,M0,N0): +// if (L0 > M0) && (L0 > N0) then fix Xc = 0 and use L = sqrt(1 - M^2 - N^2) +// else if (M0 > L0) && (M0 > N0) then fix Yc = 0 and use M = sqrt(1 - L^2 - N^2) +// else if (N0 > L0) && (N0 > M0) then fix Zc = 0 and use N = sqrt(1 - L^2 - M^2) +// We thus solve for 5 unknown parameters. +// Thus for the solution to succeed the initial axis direction should be reasonable. + +#include "PreCompiled.h" + +#ifndef _PreComp_ +# include +# include +# include +#endif + +#include "CylinderFit.h" +#include +#include + +using namespace MeshCoreFit; + +CylinderFit::CylinderFit() + : _vBase(0,0,0) + , _vAxis(0,0,1) + , _dRadius(0) + , _numIter(0) + , _posConvLimit(0.0001) + , _dirConvLimit(0.000001) + , _vConvLimit(0.001) + , _maxIter(50) +{ +} + +CylinderFit::~CylinderFit() +{ +} + +// Set approximations before calling the fitting +void CylinderFit::SetApproximations(double radius, const Base::Vector3d &base, const Base::Vector3d &axis) +{ + _bIsFitted = false; + _fLastResult = FLOAT_MAX; + _numIter = 0; + _dRadius = radius; + _vBase = base; + _vAxis = axis; + _vAxis.Normalize(); +} + +// Set iteration convergence criteria for the fit if special values are needed. +// The default values set in the constructor are suitable for most uses +void CylinderFit::SetConvergenceCriteria(double posConvLimit, double dirConvLimit, double vConvLimit, int maxIter) +{ + if (posConvLimit > 0.0) + _posConvLimit = posConvLimit; + if (dirConvLimit > 0.0) + _dirConvLimit = dirConvLimit; + if (vConvLimit > 0.0) + _vConvLimit = vConvLimit; + if (maxIter > 0) + _maxIter = maxIter; +} + + +double CylinderFit::GetRadius() const +{ + if (_bIsFitted) + return _dRadius; + else + return 0.0; +} + +Base::Vector3d CylinderFit::GetBase() const +{ + if (_bIsFitted) + return _vBase; + else + return Base::Vector3d(); +} + +Base::Vector3d CylinderFit::GetAxis() const +{ + if (_bIsFitted) + return _vAxis; + else + return Base::Vector3d(); +} + +int CylinderFit::GetNumIterations() const +{ + if (_bIsFitted) + return _numIter; + else + return 0; +} + +float CylinderFit::GetDistanceToCylinder(const Base::Vector3f &rcPoint) const +{ + float fResult = FLOAT_MAX; + if (_bIsFitted) + { + fResult = Base::Vector3d(rcPoint.x, rcPoint.y, rcPoint.z).DistanceToLine(_vBase, _vAxis) - _dRadius; + } + return fResult; +} + +float CylinderFit::GetStdDeviation() const +{ + // Mean: M=(1/N)*SUM Xi + // Variance: VAR=(N/N-3)*[(1/N)*SUM(Xi^2)-M^2] + // Standard deviation: SD=SQRT(VAR) + // Standard error of the mean: SE=SD/SQRT(N) + if (!_bIsFitted) + return FLOAT_MAX; + + float fSumXi = 0.0f, fSumXi2 = 0.0f, + fMean = 0.0f, fDist = 0.0f; + + float ulPtCt = float(CountPoints()); + std::list< Base::Vector3f >::const_iterator cIt; + + for (cIt = _vPoints.begin(); cIt != _vPoints.end(); ++cIt) { + fDist = GetDistanceToCylinder( *cIt ); + fSumXi += fDist; + fSumXi2 += ( fDist * fDist ); + } + + fMean = (1.0f / ulPtCt) * fSumXi; + return sqrt((ulPtCt / (ulPtCt - 3.0f)) * ((1.0f / ulPtCt) * fSumXi2 - fMean * fMean)); +} + +void CylinderFit::ProjectToCylinder() +{ + Base::Vector3f cBase(_vBase.x, _vBase.y, _vBase.z); + Base::Vector3f cAxis(_vAxis.x, _vAxis.y, _vAxis.z); + + for (std::list< Base::Vector3f >::iterator it = _vPoints.begin(); it != _vPoints.end(); ++it) { + Base::Vector3f& cPnt = *it; + if (cPnt.DistanceToLine(cBase, cAxis) > 0) { + Base::Vector3f proj; + cBase.ProjectToPlane(cPnt, cAxis, proj); + Base::Vector3f diff = cPnt - proj; + diff.Normalize(); + cPnt = proj + diff * _dRadius; + } + else { + // Point is on the cylinder axis, so it can be moved in + // any direction perpendicular to the cylinder axis + Base::Vector3f cMov(cPnt); + do { + float x = (float(rand()) / float(RAND_MAX)); + float y = (float(rand()) / float(RAND_MAX)); + float z = (float(rand()) / float(RAND_MAX)); + cMov.Move(x,y,z); + } + while (cMov.DistanceToLine(cBase, cAxis) == 0); + + Base::Vector3f proj; + cMov.ProjectToPlane(cPnt, cAxis, proj); + Base::Vector3f diff = cPnt - proj; + diff.Normalize(); + cPnt = proj + diff * _dRadius; + } + } +} + +// Compute approximations for the parameters using all points by computing a +// line through the points. This doesn't work well if the points are only from +// one small surface area. +// In that case rather use SetApproximations() with a better estimate. +void CylinderFit::ComputeApproximationsLine() +{ + _bIsFitted = false; + _fLastResult = FLOAT_MAX; + _numIter = 0; + _vBase.Set(0.0, 0.0, 0.0); + _vAxis.Set(0.0, 0.0, 0.0); + _dRadius = 0.0; + if (_vPoints.size() > 0) + { + std::vector input; + std::transform(_vPoints.begin(), _vPoints.end(), std::back_inserter(input), + [](const Base::Vector3f& v) { return Wm4::Vector3d(v.x, v.y, v.z); }); + Wm4::Line3 kLine = Wm4::OrthogonalLineFit3(input.size(), input.data()); + _vBase.Set(kLine.Origin.X(), kLine.Origin.Y(), kLine.Origin.Z()); + _vAxis.Set(kLine.Direction.X(), kLine.Direction.Y(), kLine.Direction.Z()); + + for (std::list< Base::Vector3f >::const_iterator cIt = _vPoints.begin(); cIt != _vPoints.end(); ++cIt) + _dRadius += Base::Vector3d(cIt->x, cIt->y, cIt->z).DistanceToLine(_vBase, _vAxis); + _dRadius /= (double)_vPoints.size(); + } +} + +float CylinderFit::Fit() +{ + _bIsFitted = false; + _fLastResult = FLOAT_MAX; + _numIter = 0; + + // A minimum of 5 surface points is needed to define a cylinder + if (CountPoints() < 5) + return FLOAT_MAX; + + // If approximations have not been set/computed then compute some now using the line fit method + if (_dRadius == 0.0) + ComputeApproximationsLine(); + + // Check parameters to define the best solution direction + // There are 7 parameters but 2 are actually dependent on the others + // so we are actually solving for 5 parameters. + // order of parameters depending on the solution direction: + // solL: Yc, Zc, M, N, R + // solM: Xc, Zc, L, N, R + // solN: Xc, Yc, L, M, R + SolutionD solDir; + findBestSolDirection(solDir); + + // Initialise some matrices and vectors + std::vector< Base::Vector3d > residuals(CountPoints(), Base::Vector3d(0.0, 0.0, 0.0)); + Matrix5x5 atpa; + Eigen::VectorXd atpl(5); + + // Iteration loop... + double sigma0; + bool cont = true; + while (cont && (_numIter < _maxIter)) + { + ++_numIter; + + // Set up the quasi parameteric normal equations + setupNormalEquationMatrices(solDir, residuals, atpa, atpl); + + // Solve the equations for the unknown corrections + Eigen::LLT< Matrix5x5 > llt(atpa); + if (llt.info() != Eigen::Success) + return FLOAT_MAX; + Eigen::VectorXd x = llt.solve(atpl); + + // Check parameter convergence + cont = false; + if ((fabs(x(0)) > _posConvLimit) || (fabs(x(1)) > _posConvLimit) || // the two position parameter corrections + (fabs(x(2)) > _dirConvLimit) || (fabs(x(3)) > _dirConvLimit) || // the two direction parameter corrections + (fabs(x(4)) > _posConvLimit)) // the radius correction + cont = true; + + // Before updating the unknowns, compute the residuals and sigma0 and check the residual convergence + bool vConverged; + if (!computeResiduals(solDir, x, residuals, sigma0, _vConvLimit, vConverged)) + return FLOAT_MAX; + if (!vConverged) + cont = true; + + // Update the parameters + if (!updateParameters(solDir, x)) + return FLOAT_MAX; + } + + // Check for convergence + if (cont) + return FLOAT_MAX; + + _bIsFitted = true; + _fLastResult = sigma0; + + return _fLastResult; +} + +// Checks initial parameter values and defines the best solution direction to use +// Axis direction = (L,M,N) +// solution L: L is biggest axis component and L = f(M,N) and X = 0 (we move the base point along axis so that x = 0) +// solution M: M is biggest axis component and M = f(L,N) and Y = 0 (we move the base point along axis so that y = 0) +// solution N: N is biggest axis component and N = f(L,M) and Z = 0 (we move the base point along axis so that z = 0) +// IMPLEMENT: use fix X,Y,or Z to value of associated centre of gravity coordinate +// (because 0 could be along way away from cylinder points) +void CylinderFit::findBestSolDirection(SolutionD &solDir) +{ + // Choose the best of the three solution 'directions' to use + // This is to avoid a square root of a negative number when computing the dependent parameters + Base::Vector3d dir = _vAxis; + Base::Vector3d pos = _vBase; + dir.Normalize(); + double biggest = dir.x; + solDir = solL; + if (fabs (dir.y) > fabs (biggest)) + { + biggest = dir.y; + solDir = solM; + } + if (fabs (dir.z) > fabs (biggest)) + { + biggest = dir.z; + solDir = solN; + } + if (biggest < 0.0) + dir.Set(-dir.x, -dir.y, -dir.z); // multiplies by -1 + + double lambda; + switch (solDir) + { + case solL: + lambda = -pos.x / dir.x; + pos.x = 0.0; + pos.y = pos.y + lambda * dir.y; + pos.z = pos.z + lambda * dir.z; + break; + case solM: + lambda = -pos.y / dir.y; + pos.x = pos.x + lambda * dir.x; + pos.y = 0.0; + pos.z = pos.z + lambda * dir.z; + break; + case solN: + lambda = -pos.z / dir.z; + pos.x = pos.x + lambda * dir.x; + pos.y = pos.y + lambda * dir.y; + pos.z = 0.0; + break; + } + _vAxis = dir; + _vBase = pos; +} + +// Set up the normal equation matrices +// atpa ... 5x5 normal matrix +// atpl ... 5x1 matrix (right-hand side of equation) +void CylinderFit::setupNormalEquationMatrices(SolutionD solDir, const std::vector< Base::Vector3d > &residuals, Matrix5x5 &atpa, Eigen::VectorXd &atpl) const +{ + // Zero matrices + atpa.setZero(); + atpl.setZero(); + + // For each point, setup the observation equation coefficients and add their + // contribution into the the normal equation matrices + double a[5], b[3]; + double f0, qw; + std::vector< Base::Vector3d >::const_iterator vIt = residuals.begin(); + std::list< Base::Vector3f >::const_iterator cIt; + for (cIt = _vPoints.begin(); cIt != _vPoints.end(); ++cIt, ++vIt) + { + // if (using this point) { // currently all given points are used (could modify this if eliminating outliers, etc.... + setupObservation(solDir, *cIt, *vIt, a, f0, qw, b); + addObservationU(a, f0, qw, atpa, atpl); + // } + } + setLowerPart(atpa); +} + +// Sets up contributions of given observation to the quasi parameteric +// normal equation matrices. Assumes uncorrelated coordinates. +// point ... point +// residual ... residual for this point computed from previous iteration (zero for first iteration) +// a[5] ... parameter partials +// f0 ... reference to f0 term +// qw ... reference to quasi weight (here we are assuming equal unit weights for each observed point coordinate) +// b[3] ... observation partials +void CylinderFit::setupObservation(SolutionD solDir, const Base::Vector3f &point, const Base::Vector3d &residual, double a[5], double &f0, double &qw, double b[3]) const +{ + // This adjustment requires an update of the observation approximations + // because the residuals do not have a linear relationship. + // New estimates for the observations: + double xEstimate = (double)point.x + residual.x; + double yEstimate = (double)point.y + residual.y; + double zEstimate = (double)point.z + residual.z; + + // intermediate parameters + double lambda = _vAxis.x * (xEstimate - _vBase.x) + _vAxis.y * (yEstimate - _vBase.y) + _vAxis.z * (zEstimate - _vBase.z); + double x0 = _vBase.x + lambda * _vAxis.x; + double y0 = _vBase.y + lambda * _vAxis.y; + double z0 = _vBase.z + lambda * _vAxis.z; + double dx = xEstimate - x0; + double dy = yEstimate - y0; + double dz = zEstimate - z0; + double dx00 = _vBase.x - xEstimate; + double dy00 = _vBase.y - yEstimate; + double dz00 = _vBase.z - zEstimate; + + // partials of the observations + b[0] = 2.0 * (dx - _vAxis.x * _vAxis.x * dx - _vAxis.x * _vAxis.y * dy - _vAxis.x * _vAxis.z * dz); + b[1] = 2.0 * (dy - _vAxis.x * _vAxis.y * dx - _vAxis.y * _vAxis.y * dy - _vAxis.y * _vAxis.z * dz); + b[2] = 2.0 * (dz - _vAxis.x * _vAxis.z * dx - _vAxis.y * _vAxis.z * dy - _vAxis.z * _vAxis.z * dz); + + // partials of the parameters + switch (solDir) + { + double ddxdl, ddydl, ddzdl; + double ddxdm, ddydm, ddzdm; + double ddxdn, ddydn, ddzdn; + case solL: + // order of parameters: Yc, Zc, M, N, R + ddxdm = -2.0 * _vAxis.y * dx00 + (_vAxis.x - _vAxis.y * _vAxis.y / _vAxis.x) * dy00 - (_vAxis.y * _vAxis.z / _vAxis.x) * dz00; + ddydm = (_vAxis.x - _vAxis.y * _vAxis.y / _vAxis.x) * dx00 + 2.0 * _vAxis.y * dy00 + _vAxis.z * dz00; + ddzdm = -(_vAxis.y * _vAxis.z / _vAxis.x) * dx00 + _vAxis.z * dy00; + ddxdn = -2.0 * _vAxis.z * dx00 - (_vAxis.y * _vAxis.z / _vAxis.x) * dy00 + (_vAxis.x - _vAxis.z * _vAxis.z / _vAxis.x) * dz00; + ddydn = -(_vAxis.y * _vAxis.z / _vAxis.x) * dx00 + _vAxis.y * dz00; + ddzdn = (_vAxis.x - _vAxis.z * _vAxis.z / _vAxis.x) * dx00 + _vAxis.y * dy00 + 2.0 * _vAxis.z * dz00; + a[0] = -b[1]; + a[1] = -b[2]; + a[2] = 2.0 * (dx * ddxdm + dy * ddydm + dz * ddzdm); + a[3] = 2.0 * (dx * ddxdn + dy * ddydn + dz * ddzdn); + a[4] = -2.0 * _dRadius; + break; + case solM: + // order of parameters: Xc, Zc, L, N, R + ddxdl = 2.0 * _vAxis.x * dx00 + (_vAxis.y - _vAxis.x * _vAxis.x / _vAxis.y) * dy00 + _vAxis.z * dz00; + ddydl = (_vAxis.y - _vAxis.x * _vAxis.x / _vAxis.y) * dx00 - 2.0 * _vAxis.x * dy00 - (_vAxis.x * _vAxis.z / _vAxis.y) * dz00; + ddzdl = _vAxis.z * dx00 - (_vAxis.x * _vAxis.z / _vAxis.y) * dy00; + ddxdn = -(_vAxis.x * _vAxis.z / _vAxis.y) * dy00 + _vAxis.x * dz00; + ddydn = -(_vAxis.x * _vAxis.z / _vAxis.y) * dx00 - 2.0 * _vAxis.z * dy00 + (_vAxis.y - _vAxis.z * _vAxis.z / _vAxis.y) * dz00; + ddzdn = _vAxis.x * dx00 + (_vAxis.y - _vAxis.z * _vAxis.z / _vAxis.y) * dy00 + 2.0 * _vAxis.z * dz00; + a[0] = -b[0]; + a[1] = -b[2]; + a[2] = 2.0 * (dx * ddxdl + dy * ddydl + dz * ddzdl); + a[3] = 2.0 * (dx * ddxdn + dy * ddydn + dz * ddzdn); + a[4] = -2.0 * _dRadius; + break; + case solN: + // order of parameters: Xc, Yc, L, M, R + ddxdl = 2.0 * _vAxis.x * dx00 + _vAxis.y * dy00 + (_vAxis.z - _vAxis.x * _vAxis.x / _vAxis.z) * dz00; + ddydl = _vAxis.y * dx00 - (_vAxis.x * _vAxis.y / _vAxis.z) * dz00; + ddzdl = (_vAxis.z - _vAxis.x * _vAxis.x / _vAxis.z) * dx00 - (_vAxis.x * _vAxis.y / _vAxis.z) * dy00 - 2.0 * _vAxis.x * dz00; + ddxdm = _vAxis.x * dy00 - (_vAxis.x * _vAxis.y / _vAxis.z) * dz00; + ddydm = _vAxis.x * dx00 + 2.0 * _vAxis.y * dy00 + (_vAxis.z - _vAxis.y * _vAxis.y / _vAxis.z) * dz00; + ddzdm = - (_vAxis.x * _vAxis.y / _vAxis.z) * dx00 + (_vAxis.z - _vAxis.y * _vAxis.y / _vAxis.z) * dy00 - 2.0 * _vAxis.y * dz00; + a[0] = -b[0]; + a[1] = -b[1]; + a[2] = 2.0 * (dx * ddxdl + dy * ddydl + dz * ddzdl); + a[3] = 2.0 * (dx * ddxdm + dy * ddydm + dz * ddzdm); + a[4] = -2.0 * _dRadius; + break; + } + + // free term + f0 = _dRadius * _dRadius - dx * dx - dy * dy - dz * dz + b[0] * residual.x + b[1] * residual.y + b[2] * residual.z; + + // quasi weight (using equal weights for cylinder point coordinate observations) + //w[0] = 1.0; + //w[1] = 1.0; + //w[2] = 1.0; + //qw = 1.0 / (b[0] * b[0] / w[0] + b[1] * b[1] / w[1] + b[2] * b[2] / w[2]); + qw = 1.0 / (b[0] * b[0] + b[1] * b[1] + b[2] * b[2]); +} + +// Computes contribution of the given observation equation on the normal equation matrices +// Call this for each observation (point) +// Here we only add the contribution to the upper part of the normal matrix +// and then after all observations have been added we need to set the lower part +// (which is symmetrical to the upper part) +// a[5] ... parameter partials +// li ... free term (f0) +// pi ... weight of observation (= quasi weight qw for this solution) +// atpa ... 5x5 normal equation matrix +// atpl ... 5x1 matrix/vector (right-hand side of equations) +void CylinderFit::addObservationU(double a[5], double li, double pi, Matrix5x5 &atpa, Eigen::VectorXd &atpl) const +{ + for (int i = 0; i < 5; ++i) + { + double aipi = a[i] * pi; + for (int j = i; j < 5; ++j) + { + atpa(i, j) += aipi * a[j]; + //atpa(j, i) = atpa(i, j); // it's a symmetrical matrix, we'll set this later after all observations processed + } + atpl(i) += aipi * li; + } +} + +// Set the lower part of the normal matrix equal to the upper part +// This is done after all the observations have been added +void CylinderFit::setLowerPart(Matrix5x5 &atpa) const +{ + for (int i = 0; i < 5; ++i) + for (int j = i+1; j < 5; ++j) // skip the diagonal elements + atpa(j, i) = atpa(i, j); +} + +// Compute the residuals and sigma0 and check the residual convergence +bool CylinderFit::computeResiduals(SolutionD solDir, const Eigen::VectorXd &x, std::vector< Base::Vector3d > &residuals, double &sigma0, double vConvLimit, bool &vConverged) const +{ + vConverged = true; + int nPtsUsed = 0; + sigma0 = 0.0; + double a[5], b[3]; + double f0, qw; + //double maxdVx = 0.0; + //double maxdVy = 0.0; + //double maxdVz = 0.0; + //double rmsVv = 0.0; + std::vector< Base::Vector3d >::iterator vIt = residuals.begin(); + std::list< Base::Vector3f >::const_iterator cIt; + for (cIt = _vPoints.begin(); cIt != _vPoints.end(); ++cIt, ++vIt) + { + // if (using this point) { // currently all given points are used (could modify this if eliminating outliers, etc.... + ++nPtsUsed; + Base::Vector3d &v = *vIt; + setupObservation(solDir, *cIt, v, a, f0, qw, b); + double qv = -f0; + for (int i = 0; i < 5; ++i) + qv += a[i] * x(i); + + // We are using equal weights for cylinder point coordinate observations (see setupObservation) + // i.e. w[0] = w[1] = w[2] = 1.0; + //double vx = -qw * qv * b[0] / w[0]; + //double vy = -qw * qv * b[1] / w[1]; + //double vz = -qw * qv * b[2] / w[2]; + double vx = -qw * qv * b[0]; + double vy = -qw * qv * b[1]; + double vz = -qw * qv * b[2]; + double dVx = fabs(vx - v.x); + double dVy = fabs(vy - v.y); + double dVz = fabs(vz - v.z); + v.x = vx; + v.y = vy; + v.z = vz; + + //double vv = v.x * v.x + v.y * v.y + v.z * v.z; + //rmsVv += vv * vv; + + //sigma0 += v.x * w[0] * v.x + v.y * w[1] * v.y + v.z * w[2] * v.z; + sigma0 += v.x * v.x + v.y * v.y + v.z * v.z; + + if ((dVx > vConvLimit) || (dVy > vConvLimit) || (dVz > vConvLimit)) + vConverged = false; + + //if (dVx > maxdVx) + // maxdVx = dVx; + //if (dVy > maxdVy) + // maxdVy = dVy; + //if (dVz > maxdVz) + // maxdVz = dVz; + } + + // Compute degrees of freedom and sigma0 + if (nPtsUsed < 5) // A minimum of 5 surface points is needed to define a cylinder + { + sigma0 = 0.0; + return false; + } + int df = nPtsUsed - 5; + if (df == 0) + sigma0 = 0.0; + else + sigma0 = sqrt (sigma0 / (double)df); + + //rmsVv = sqrt(rmsVv / (double)nPtsUsed); + //Base::Console().Message("X: %0.3e %0.3e %0.3e %0.3e %0.3e , Max dV: %0.4f %0.4f %0.4f , RMS Vv: %0.4f\n", x(0), x(1), x(2), x(3), x(4), maxdVx, maxdVy, maxdVz, rmsVv); + + return true; +} + +// Update the parameters after solving the normal equations +bool CylinderFit::updateParameters(SolutionD solDir, const Eigen::VectorXd &x) +{ + // Update the parameters used as unknowns in the solution + switch (solDir) + { + case solL: // order of parameters: Yc, Zc, M, N, R + _vBase.y += x(0); + _vBase.z += x(1); + _vAxis.y += x(2); + _vAxis.z += x(3); + _dRadius += x(4); + break; + case solM: // order of parameters: Xc, Zc, L, N, R + _vBase.x += x(0); + _vBase.z += x(1); + _vAxis.x += x(2); + _vAxis.z += x(3); + _dRadius += x(4); + break; + case solN: // order of parameters: Xc, Yc, L, M, R + _vBase.x += x(0); + _vBase.y += x(1); + _vAxis.x += x(2); + _vAxis.y += x(3); + _dRadius += x(4); + break; + } + + // Update the dependent axis direction parameter + double l2, m2, n2; + switch (solDir) + { + case solL: + l2 = 1.0 - _vAxis.y * _vAxis.y - _vAxis.z * _vAxis.z; + if (l2 <= 0.0) + return false; // L*L <= 0 ! + _vAxis.x = sqrt(l2); + //_vBase.x = 0.0; // should already be 0 + break; + case solM: + m2 = 1.0 - _vAxis.x * _vAxis.x - _vAxis.z * _vAxis.z; + if (m2 <= 0.0) + return false; // M*M <= 0 ! + _vAxis.y = sqrt(m2); + //_vBase.y = 0.0; // should already be 0 + break; + case solN: + n2 = 1.0 - _vAxis.x * _vAxis.x - _vAxis.y * _vAxis.y; + if (n2 <= 0.0) + return false; // N*N <= 0 ! + _vAxis.z = sqrt(n2); + //_vBase.z = 0.0; // should already be 0 + break; + } + + return true; +} diff --git a/src/Mod/Mesh/App/Core/CylinderFit.h b/src/Mod/Mesh/App/Core/CylinderFit.h new file mode 100644 index 0000000000..520fad2c9f --- /dev/null +++ b/src/Mod/Mesh/App/Core/CylinderFit.h @@ -0,0 +1,151 @@ +/*************************************************************************** + * Copyright (c) 2020 Graeme van der Vlugt * + * * + * This file is part of the FreeCAD CAx development system. * + * * + * This library is free software; you can redistribute it and/or * + * modify it under the terms of the GNU Library General Public * + * License as published by the Free Software Foundation; either * + * version 2 of the License, or (at your option) any later version. * + * * + * This library is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU Library General Public License for more details. * + * * + * You should have received a copy of the GNU Library General Public * + * License along with this library; see the file COPYING.LIB. If not, * + * write to the Free Software Foundation, Inc., 59 Temple Place, * + * Suite 330, Boston, MA 02111-1307, USA * + * * + ***************************************************************************/ + + +#ifndef MESH_CYLINDER_FIT_H +#define MESH_CYLINDER_FIT_H + +#include "Approximation.h" +#include + +// ------------------------------------------------------------------------------- +namespace MeshCoreFit { + +typedef Eigen::Matrix Matrix5x5; + +/** + * Best-fit cylinder for a given set of points. + * Doesn't expect points on any top or bottom end-planes, only points on the side surface + */ +class MeshExport CylinderFit : public MeshCore::Approximation +{ +protected: + // Solution 'direction' enumeration + enum SolutionD {solL = 0, // solution L: L is biggest axis component and L = f(M,N) + solM = 1, // solution M: M is biggest axis component and M = f(L,N) + solN = 2 // solution N: N is biggest axis component and N = f(L,M) + }; +public: + /** + * Construction + */ + CylinderFit(); + /** + * Destruction + */ + virtual ~CylinderFit(); + + /** + * Set approximations before calling Fit() + */ + void SetApproximations(double radius, const Base::Vector3d &base, const Base::Vector3d &axis); + /** + * Set iteration convergence criteria for the fit if special values are needed. + * The default values set in the constructor are suitable for most uses + */ + void SetConvergenceCriteria(double posConvLimit, double dirConvLimit, double vConvLimit, int maxIter); + /** + * Returns the radius of the fitted cylinder. If Fit() has not been called then zero is returned. + */ + double GetRadius() const; + /** + * Returns the base of the fitted cylinder. If Fit() has not been called the null vector is returned. + */ + Base::Vector3d GetBase() const; + /** + * Returns the axis of the fitted cylinder. If Fit() has not been called the null vector is returned. + */ + Base::Vector3d GetAxis() const; + /** + * Returns the number of iterations that Fit() needed to converge. If Fit() has not been called then zero is returned. + */ + int GetNumIterations() const; + /** + * Fit a cylinder into the given points. If the fit fails FLOAT_MAX is returned. + */ + float Fit(); + /** + * Returns the distance from the point \a rcPoint to the fitted cylinder. If Fit() has not been + * called FLOAT_MAX is returned. + */ + float GetDistanceToCylinder(const Base::Vector3f &rcPoint) const; + /** + * Returns the standard deviation from the points to the fitted cylinder. If Fit() has not been + * called FLOAT_MAX is returned. + */ + float GetStdDeviation() const; + /** + * Projects the points onto the fitted cylinder. + */ + void ProjectToCylinder(); + +protected: + /** + * Compute approximations for the parameters using all points using the line fit method + */ + void ComputeApproximationsLine(); + /** + * Checks initial parameter values and defines the best solution direction to use + */ + void findBestSolDirection(SolutionD &solDir); + /** + * Set up the normal equations + */ + void setupNormalEquationMatrices(SolutionD solDir, const std::vector< Base::Vector3d > &residuals, Matrix5x5 &atpa, Eigen::VectorXd &atpl) const; + /** + * Sets up contributions of given observation to the normal equation matrices. + */ + void setupObservation(SolutionD solDir, const Base::Vector3f &point, const Base::Vector3d &residual, double a[5], double &f0, double &qw, double b[3]) const; + /** + * Computes contribution of the given observation equation on the normal equation matrices + */ + void addObservationU(double a[5], double li, double pi, Matrix5x5 &atpa, Eigen::VectorXd &atpl) const; + /** + * Set the lower part of the normal matrix equal to the upper part + */ + void setLowerPart(Matrix5x5 &atpa) const; + + /** + * Compute the residuals and sigma0 and check the residual convergence + */ + bool computeResiduals(SolutionD solDir, const Eigen::VectorXd &x, std::vector< Base::Vector3d > &residuals, double &sigma0, double vConvLimit, bool &vConverged) const; + /** + * Update the parameters after solving the normal equations + */ + bool updateParameters(SolutionD solDir, const Eigen::VectorXd &x); + +protected: + Base::Vector3d _vBase; /**< Base vector of the cylinder (point on axis). */ + Base::Vector3d _vAxis; /**< Axis of the cylinder. */ + double _dRadius; /**< Radius of the cylinder. */ + int _numIter; /**< Number of iterations for solution to converge. */ + double _posConvLimit; /**< Position and radius parameter convergence threshold. */ + double _dirConvLimit; /**< Direction parameter convergence threshold. */ + double _vConvLimit; /**< Residual convergence threshold. */ + int _maxIter; /**< Maximum number of iterations. */ + +}; + + +} // namespace MeshCore + +#endif // MESH_CYLINDER_FIT_H diff --git a/src/Mod/Mesh/App/Core/SphereFit.cpp b/src/Mod/Mesh/App/Core/SphereFit.cpp new file mode 100644 index 0000000000..5285c69d1f --- /dev/null +++ b/src/Mod/Mesh/App/Core/SphereFit.cpp @@ -0,0 +1,427 @@ +/*************************************************************************** + * Copyright (c) 2020 Graeme van der Vlugt * + * * + * This file is part of the FreeCAD CAx development system. * + * * + * This library is free software; you can redistribute it and/or * + * modify it under the terms of the GNU Library General Public * + * License as published by the Free Software Foundation; either * + * version 2 of the License, or (at your option) any later version. * + * * + * This library is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU Library General Public License for more details. * + * * + * You should have received a copy of the GNU Library General Public * + * License along with this library; see the file COPYING.LIB. If not, * + * write to the Free Software Foundation, Inc., 59 Temple Place, * + * Suite 330, Boston, MA 02111-1307, USA * + * * + ***************************************************************************/ + +#include "PreCompiled.h" + +#ifndef _PreComp_ +# include +# include +# include +#endif + +#include "SphereFit.h" +#include + +using namespace MeshCoreFit; + +SphereFit::SphereFit() + : _vCenter(0,0,0) + , _dRadius(0) + , _numIter(0) + , _posConvLimit(0.0001) + , _vConvLimit(0.001) + , _maxIter(50) +{ +} + +SphereFit::~SphereFit() +{ +} + +// Set approximations before calling the fitting +void SphereFit::SetApproximations(double radius, const Base::Vector3d ¢er) +{ + _bIsFitted = false; + _fLastResult = FLOAT_MAX; + _numIter = 0; + _dRadius = radius; + _vCenter = center; +} + +// Set iteration convergence criteria for the fit if special values are needed. +// The default values set in the constructor are suitable for most uses +void SphereFit::SetConvergenceCriteria(double posConvLimit, double vConvLimit, int maxIter) +{ + if (posConvLimit > 0.0) + _posConvLimit = posConvLimit; + if (vConvLimit > 0.0) + _vConvLimit = vConvLimit; + if (maxIter > 0) + _maxIter = maxIter; +} + + +double SphereFit::GetRadius() const +{ + if (_bIsFitted) + return _dRadius; + else + return 0.0; +} + +Base::Vector3d SphereFit::GetCenter() const +{ + if (_bIsFitted) + return _vCenter; + else + return Base::Vector3d(); +} + +int SphereFit::GetNumIterations() const +{ + if (_bIsFitted) + return _numIter; + else + return 0; +} + +float SphereFit::GetDistanceToSphere(const Base::Vector3f &rcPoint) const +{ + float fResult = FLOAT_MAX; + if (_bIsFitted) + { + fResult = Base::Vector3d((double)rcPoint.x - _vCenter.x, (double)rcPoint.y - _vCenter.y, (double)rcPoint.z - _vCenter.z).Length() - _dRadius; + } + return fResult; +} + +float SphereFit::GetStdDeviation() const +{ + // Mean: M=(1/N)*SUM Xi + // Variance: VAR=(N/N-3)*[(1/N)*SUM(Xi^2)-M^2] + // Standard deviation: SD=SQRT(VAR) + // Standard error of the mean: SE=SD/SQRT(N) + if (!_bIsFitted) + return FLOAT_MAX; + + float fSumXi = 0.0f, fSumXi2 = 0.0f, + fMean = 0.0f, fDist = 0.0f; + + float ulPtCt = float(CountPoints()); + std::list< Base::Vector3f >::const_iterator cIt; + + for (cIt = _vPoints.begin(); cIt != _vPoints.end(); ++cIt) { + fDist = GetDistanceToSphere( *cIt ); + fSumXi += fDist; + fSumXi2 += ( fDist * fDist ); + } + + fMean = (1.0f / ulPtCt) * fSumXi; + return sqrt((ulPtCt / (ulPtCt - 3.0f)) * ((1.0f / ulPtCt) * fSumXi2 - fMean * fMean)); +} + +void SphereFit::ProjectToSphere() +{ + for (std::list< Base::Vector3f >::iterator it = _vPoints.begin(); it != _vPoints.end(); ++it) { + Base::Vector3f& cPnt = *it; + + // Compute unit vector from sphere centre to point. + // Because this vector is orthogonal to the sphere's surface at the + // intersection point we can easily compute the projection point on the + // closest surface point using the radius of the sphere + Base::Vector3d diff((double)cPnt.x - _vCenter.x, (double)cPnt.y - _vCenter.y, (double)cPnt.z - _vCenter.z); + double length = diff.Length(); + if (length == 0.0) + { + // Point is exactly at the sphere center, so it can be projected in any direction onto the sphere! + // So here just project in +Z direction + cPnt.z += (float)_dRadius; + } + else + { + diff /= length; // normalizing the vector + Base::Vector3d proj = _vCenter + diff * _dRadius; + cPnt.x = (float)proj.x; + cPnt.y = (float)proj.y; + cPnt.z = (float)proj.z; + } + } +} + +// Compute approximations for the parameters using all points: +// Set centre to centre of gravity of points and radius to the average +// distance from the centre of gravity to the points. +void SphereFit::ComputeApproximations() +{ + _bIsFitted = false; + _fLastResult = FLOAT_MAX; + _numIter = 0; + _vCenter.Set(0.0, 0.0, 0.0); + _dRadius = 0.0; + if (_vPoints.size() > 0) + { + std::list< Base::Vector3f >::const_iterator cIt; + for (cIt = _vPoints.begin(); cIt != _vPoints.end(); ++cIt) + { + _vCenter.x += cIt->x; + _vCenter.y += cIt->y; + _vCenter.z += cIt->z; + } + _vCenter /= (double)_vPoints.size(); + + for (cIt = _vPoints.begin(); cIt != _vPoints.end(); ++cIt) + { + Base::Vector3d diff((double)cIt->x - _vCenter.x, (double)cIt->y - _vCenter.y, (double)cIt->z - _vCenter.z); + _dRadius += diff.Length(); + } + _dRadius /= (double)_vPoints.size(); + } +} + +float SphereFit::Fit() +{ + _bIsFitted = false; + _fLastResult = FLOAT_MAX; + _numIter = 0; + + // A minimum of 4 surface points is needed to define a sphere + if (CountPoints() < 4) + return FLOAT_MAX; + + // If approximations have not been set/computed then compute some now + if (_dRadius == 0.0) + ComputeApproximations(); + + // Initialise some matrices and vectors + std::vector< Base::Vector3d > residuals(CountPoints(), Base::Vector3d(0.0, 0.0, 0.0)); + Matrix4x4 atpa; + Eigen::VectorXd atpl(4); + + // Iteration loop... + double sigma0; + bool cont = true; + while (cont && (_numIter < _maxIter)) + { + ++_numIter; + + // Set up the quasi parameteric normal equations + setupNormalEquationMatrices(residuals, atpa, atpl); + + // Solve the equations for the unknown corrections + Eigen::LLT< Matrix4x4 > llt(atpa); + if (llt.info() != Eigen::Success) + return FLOAT_MAX; + Eigen::VectorXd x = llt.solve(atpl); + + // Check parameter convergence (order of parameters: X,Y,Z,R) + cont = false; + if ((fabs(x(0)) > _posConvLimit) || (fabs(x(1)) > _posConvLimit) || + (fabs(x(2)) > _posConvLimit) || (fabs(x(3)) > _posConvLimit)) + cont = true; + + // Before updating the unknowns, compute the residuals and sigma0 and check the residual convergence + bool vConverged; + if (!computeResiduals(x, residuals, sigma0, _vConvLimit, vConverged)) + return FLOAT_MAX; + if (!vConverged) + cont = true; + + // Update the parameters (order of parameters: X,Y,Z,R) + _vCenter.x += x(0); + _vCenter.y += x(1); + _vCenter.z += x(2); + _dRadius += x(3); + } + + // Check for convergence + if (cont) + return FLOAT_MAX; + + _bIsFitted = true; + _fLastResult = sigma0; + + return _fLastResult; +} + +// Set up the normal equation matrices +// atpa ... 4x4 normal matrix +// atpl ... 4x1 matrix (right-hand side of equation) +void SphereFit::setupNormalEquationMatrices(const std::vector< Base::Vector3d > &residuals, Matrix4x4 &atpa, Eigen::VectorXd &atpl) const +{ + // Zero matrices + atpa.setZero(); + atpl.setZero(); + + // For each point, setup the observation equation coefficients and add their + // contribution into the the normal equation matrices + double a[4], b[3]; + double f0, qw; + std::vector< Base::Vector3d >::const_iterator vIt = residuals.begin(); + std::list< Base::Vector3f >::const_iterator cIt; + for (cIt = _vPoints.begin(); cIt != _vPoints.end(); ++cIt, ++vIt) + { + // if (using this point) { // currently all given points are used (could modify this if eliminating outliers, etc.... + setupObservation(*cIt, *vIt, a, f0, qw, b); + addObservationU(a, f0, qw, atpa, atpl); + // } + } + setLowerPart(atpa); +} + +// Sets up contributions of given observation to the quasi parameteric +// normal equation matrices. Assumes uncorrelated coordinates. +// point ... point +// residual ... residual for this point computed from previous iteration (zero for first iteration) +// a[4] ... parameter partials (order of parameters: X,Y,Z,R) +// f0 ... reference to f0 term +// qw ... reference to quasi weight (here we are assuming equal unit weights for each observed point coordinate) +// b[3] ... observation partials +void SphereFit::setupObservation(const Base::Vector3f &point, const Base::Vector3d &residual, double a[4], double &f0, double &qw, double b[3]) const +{ + // This adjustment requires an update of the observation approximations + // because the residuals do not have a linear relationship. + // New estimates for the observations: + double xEstimate = (double)point.x + residual.x; + double yEstimate = (double)point.y + residual.y; + double zEstimate = (double)point.z + residual.z; + + // partials of the observations + double dx = xEstimate - _vCenter.x; + double dy = yEstimate - _vCenter.y; + double dz = zEstimate - _vCenter.z; + b[0] = 2.0 * dx; + b[1] = 2.0 * dy; + b[2] = 2.0 * dz; + + // partials of the parameters + a[0] = -b[0]; + a[1] = -b[1]; + a[2] = -b[2]; + a[3] = -2.0 * _dRadius; + + // free term + f0 = _dRadius * _dRadius - dx * dx - dy * dy - dz * dz + b[0] * residual.x + b[1] * residual.y + b[2] * residual.z; + + // quasi weight (using equal weights for sphere point coordinate observations) + //w[0] = 1.0; + //w[1] = 1.0; + //w[2] = 1.0; + //qw = 1.0 / (b[0] * b[0] / w[0] + b[1] * b[1] / w[1] + b[2] * b[2] / w[2]); + qw = 1.0 / (b[0] * b[0] + b[1] * b[1] + b[2] * b[2]); +} + +// Computes contribution of the given observation equation on the normal equation matrices +// Call this for each observation (point) +// Here we only add the contribution to the upper part of the normal matrix +// and then after all observations have been added we need to set the lower part +// (which is symmetrical to the upper part) +// a[4] ... parameter partials +// li ... free term (f0) +// pi ... weight of observation (= quasi weight qw for this solution) +// atpa ... 4x4 normal equation matrix +// atpl ... 4x1 matrix/vector (right-hand side of equations) +void SphereFit::addObservationU(double a[4], double li, double pi, Matrix4x4 &atpa, Eigen::VectorXd &atpl) const +{ + for (int i = 0; i < 4; ++i) + { + double aipi = a[i] * pi; + for (int j = i; j < 4; ++j) + { + atpa(i, j) += aipi * a[j]; + //atpa(j, i) = atpa(i, j); // it's a symmetrical matrix, we'll set this later after all observations processed + } + atpl(i) += aipi * li; + } +} + +// Set the lower part of the normal matrix equal to the upper part +// This is done after all the observations have been added +void SphereFit::setLowerPart(Matrix4x4 &atpa) const +{ + for (int i = 0; i < 4; ++i) + for (int j = i+1; j < 4; ++j) // skip the diagonal elements + atpa(j, i) = atpa(i, j); +} + +// Compute the residuals and sigma0 and check the residual convergence +bool SphereFit::computeResiduals(const Eigen::VectorXd &x, std::vector< Base::Vector3d > &residuals, double &sigma0, double vConvLimit, bool &vConverged) const +{ + vConverged = true; + int nPtsUsed = 0; + sigma0 = 0.0; + double a[4], b[3]; + double f0, qw; + //double maxdVx = 0.0; + //double maxdVy = 0.0; + //double maxdVz = 0.0; + //double rmsVv = 0.0; + std::vector< Base::Vector3d >::iterator vIt = residuals.begin(); + std::list< Base::Vector3f >::const_iterator cIt; + for (cIt = _vPoints.begin(); cIt != _vPoints.end(); ++cIt, ++vIt) + { + // if (using this point) { // currently all given points are used (could modify this if eliminating outliers, etc.... + ++nPtsUsed; + Base::Vector3d &v = *vIt; + setupObservation(*cIt, v, a, f0, qw, b); + double qv = -f0; + for (int i = 0; i < 4; ++i) + qv += a[i] * x(i); + + // We are using equal weights for sphere point coordinate observations (see setupObservation) + // i.e. w[0] = w[1] = w[2] = 1.0; + //double vx = -qw * qv * b[0] / w[0]; + //double vy = -qw * qv * b[1] / w[1]; + //double vz = -qw * qv * b[2] / w[2]; + double vx = -qw * qv * b[0]; + double vy = -qw * qv * b[1]; + double vz = -qw * qv * b[2]; + double dVx = fabs(vx - v.x); + double dVy = fabs(vy - v.y); + double dVz = fabs(vz - v.z); + v.x = vx; + v.y = vy; + v.z = vz; + + //double vv = v.x * v.x + v.y * v.y + v.z * v.z; + //rmsVv += vv * vv; + + //sigma0 += v.x * w[0] * v.x + v.y * w[1] * v.y + v.z * w[2] * v.z; + sigma0 += v.x * v.x + v.y * v.y + v.z * v.z; + + if ((dVx > vConvLimit) || (dVy > vConvLimit) || (dVz > vConvLimit)) + vConverged = false; + + //if (dVx > maxdVx) + // maxdVx = dVx; + //if (dVy > maxdVy) + // maxdVy = dVy; + //if (dVz > maxdVz) + // maxdVz = dVz; + } + + // Compute degrees of freedom and sigma0 + if (nPtsUsed < 4) // A minimum of 4 surface points is needed to define a sphere + { + sigma0 = 0.0; + return false; + } + int df = nPtsUsed - 4; + if (df == 0) + sigma0 = 0.0; + else + sigma0 = sqrt (sigma0 / (double)df); + + //rmsVv = sqrt(rmsVv / (double)nPtsUsed); + //Base::Console().Message("X: %0.3e %0.3e %0.3e %0.3e , Max dV: %0.4f %0.4f %0.4f , RMS Vv: %0.4f\n", x(0), x(1), x(2), x(3), maxdVx, maxdVy, maxdVz, rmsVv); + + return true; +} diff --git a/src/Mod/Mesh/App/Core/SphereFit.h b/src/Mod/Mesh/App/Core/SphereFit.h new file mode 100644 index 0000000000..1b475f36a0 --- /dev/null +++ b/src/Mod/Mesh/App/Core/SphereFit.h @@ -0,0 +1,130 @@ +/*************************************************************************** + * Copyright (c) 2020 Graeme van der Vlugt * + * * + * This file is part of the FreeCAD CAx development system. * + * * + * This library is free software; you can redistribute it and/or * + * modify it under the terms of the GNU Library General Public * + * License as published by the Free Software Foundation; either * + * version 2 of the License, or (at your option) any later version. * + * * + * This library is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU Library General Public License for more details. * + * * + * You should have received a copy of the GNU Library General Public * + * License along with this library; see the file COPYING.LIB. If not, * + * write to the Free Software Foundation, Inc., 59 Temple Place, * + * Suite 330, Boston, MA 02111-1307, USA * + * * + ***************************************************************************/ + + +#ifndef MESH_SPHERE_FIT_H +#define MESH_SPHERE_FIT_H + +#include "Approximation.h" +#include + +// ------------------------------------------------------------------------------- +namespace MeshCoreFit { + +typedef Eigen::Matrix Matrix4x4; + +/** + * Best-fit sphere for a given set of points. + */ +class MeshExport SphereFit : public MeshCore::Approximation +{ +public: + /** + * Construction + */ + SphereFit(); + /** + * Destruction + */ + virtual ~SphereFit(); + + /** + * Set approximations before calling Fit() + */ + void SetApproximations(double radius, const Base::Vector3d ¢er); + /** + * Set iteration convergence criteria for the fit if special values are needed. + * The default values set in the constructor are suitable for most uses + */ + void SetConvergenceCriteria(double posConvLimit, double vConvLimit, int maxIter); + /** + * Returns the radius of the fitted sphere. If Fit() has not been called then zero is returned. + */ + double GetRadius() const; + /** + * Returns the center of the fitted sphere. If Fit() has not been called the null vector is returned. + */ + Base::Vector3d GetCenter() const; + /** + * Returns the number of iterations that Fit() needed to converge. If Fit() has not been called then zero is returned. + */ + int GetNumIterations() const; + /** + * Compute approximations for the parameters using all points + */ + void ComputeApproximations(); + /** + * Fit a sphere onto the given points. If the fit fails FLOAT_MAX is returned. + */ + float Fit(); + /** + * Returns the distance from the point \a rcPoint to the fitted sphere. If Fit() has not been + * called FLOAT_MAX is returned. + */ + float GetDistanceToSphere(const Base::Vector3f &rcPoint) const; + /** + * Returns the standard deviation from the points to the fitted sphere. If Fit() has not been + * called FLOAT_MAX is returned. + */ + float GetStdDeviation() const; + /** + * Projects the points onto the fitted sphere. + */ + void ProjectToSphere(); + +protected: + /** + * Set up the normal equations + */ + void setupNormalEquationMatrices(const std::vector< Base::Vector3d > &residuals, Matrix4x4 &atpa, Eigen::VectorXd &atpl) const; + /** + * Sets up contributions of given observation to the normal equation matrices. + */ + void setupObservation(const Base::Vector3f &point, const Base::Vector3d &residual, double a[4], double &f0, double &qw, double b[3]) const; + /** + * Computes contribution of the given observation equation on the normal equation matrices + */ + void addObservationU(double a[4], double li, double pi, Matrix4x4 &atpa, Eigen::VectorXd &atpl) const; + /** + * Set the lower part of the normal matrix equal to the upper part + */ + void setLowerPart(Matrix4x4 &atpa) const; + + /** + * Compute the residuals and sigma0 and check the residual convergence + */ + bool computeResiduals(const Eigen::VectorXd &x, std::vector< Base::Vector3d > &residuals, double &sigma0, double vConvLimit, bool &vConverged) const; + +protected: + Base::Vector3d _vCenter;/**< Center of sphere. */ + double _dRadius; /**< Radius of the sphere. */ + int _numIter; /**< Number of iterations for solution to converge. */ + double _posConvLimit; /**< Position and radius parameter convergence threshold. */ + double _vConvLimit; /**< Residual convergence threshold. */ + int _maxIter; /**< Maximum number of iterations. */ + +}; + + +} // namespace MeshCore + +#endif // MESH_SPHERE_FIT_H diff --git a/src/Mod/Mesh/App/MeshPy.xml b/src/Mod/Mesh/App/MeshPy.xml index 974b027428..a43402f289 100644 --- a/src/Mod/Mesh/App/MeshPy.xml +++ b/src/Mod/Mesh/App/MeshPy.xml @@ -469,7 +469,7 @@ an empty dictionary if there is no intersection. getPlanarSegments(dev,[min faces=0]) -> list Get all planes of the mesh as segment. In the worst case each triangle can be regarded as single -plane if none of its neighours is coplanar. +plane if none of its neighbors are coplanar. diff --git a/src/Mod/Part/App/AppPartPy.cpp b/src/Mod/Part/App/AppPartPy.cpp index da506351e5..6bbee793bb 100644 --- a/src/Mod/Part/App/AppPartPy.cpp +++ b/src/Mod/Part/App/AppPartPy.cpp @@ -62,6 +62,7 @@ # include # include # include +# include # include # include # include @@ -749,7 +750,7 @@ private: p1.Transform(loc.Transformation()); p2.Transform(loc.Transformation()); p3.Transform(loc.Transformation()); - // TODO: verify if tolerence should be hard coded + // TODO: verify if tolerance should be hard coded if (!p1.IsEqual(p2, 0.01) && !p2.IsEqual(p3, 0.01) && !p3.IsEqual(p1, 0.01)) { PyObject *t1 = PyTuple_Pack(3, PyFloat_FromDouble(p1.X()), PyFloat_FromDouble(p1.Y()), PyFloat_FromDouble(p1.Z())); PyObject *t2 = PyTuple_Pack(3, PyFloat_FromDouble(p2.X()), PyFloat_FromDouble(p2.Y()), PyFloat_FromDouble(p2.Z())); diff --git a/src/Mod/Part/Gui/DlgSettingsObjectColor.ui b/src/Mod/Part/Gui/DlgSettingsObjectColor.ui index 9de8a266bd..0a4d195120 100644 --- a/src/Mod/Part/Gui/DlgSettingsObjectColor.ui +++ b/src/Mod/Part/Gui/DlgSettingsObjectColor.ui @@ -40,7 +40,7 @@ The default color for new shapes - + 204 204 @@ -89,7 +89,7 @@ The default line color for new shapes - + 25 25 @@ -157,7 +157,7 @@ The default color for new vertices - + 25 25 @@ -225,7 +225,7 @@ The color of bounding boxes in the 3D view - + 255 255 @@ -248,6 +248,12 @@ 0 + + Bottom side of surface will be rendered the same way than top. +If not checked, it depends on the option "Backlight color" +(preferences section Display -> 3D View); either the backlight color +will be used or black. + Two-side rendering @@ -303,6 +309,9 @@
+ + Text color for document annotations + AnnotationTextColor diff --git a/src/Mod/PartDesign/CMakeLists.txt b/src/Mod/PartDesign/CMakeLists.txt index 177550408a..435ce86d35 100644 --- a/src/Mod/PartDesign/CMakeLists.txt +++ b/src/Mod/PartDesign/CMakeLists.txt @@ -16,6 +16,8 @@ if(BUILD_GUI) TestPartDesignGui.py InvoluteGearFeature.py InvoluteGearFeature.ui + SprocketFeature.py + SprocketFeature.ui ) endif(BUILD_GUI) @@ -60,6 +62,13 @@ set(PartDesign_GearScripts fcgear/svggear.py ) +set(PartDesign_SprocketScripts + fcsprocket/__init__.py + fcsprocket/fcsprocket.py + fcsprocket/fcsprocketdialog.py + fcsprocket/sprocket.py +) + set(PartDesign_WizardShaft WizardShaft/__init__.py WizardShaft/WizardShaft.svg @@ -76,6 +85,7 @@ add_custom_target(PartDesignScripts ALL SOURCES ${PartDesign_OtherScripts} ${PartDesign_TestScripts} ${PartDesign_GearScripts} + ${PartDesign_SprocketScripts} ) fc_target_copy_resource(PartDesignScripts @@ -85,6 +95,7 @@ fc_target_copy_resource(PartDesignScripts ${PartDesign_OtherScripts} ${PartDesign_TestScripts} ${PartDesign_GearScripts} + ${PartDesign_SprocketScripts} ) INSTALL( @@ -113,7 +124,13 @@ INSTALL( ${PartDesign_GearScripts} DESTINATION Mod/PartDesign/fcgear - +) + +INSTALL( + FILES + ${PartDesign_SprocketScripts} + DESTINATION + Mod/PartDesign/fcsprocket ) if(BUILD_FEM) diff --git a/src/Mod/PartDesign/Gui/Resources/PartDesign.qrc b/src/Mod/PartDesign/Gui/Resources/PartDesign.qrc index e8db9a825e..34550529ba 100644 --- a/src/Mod/PartDesign/Gui/Resources/PartDesign.qrc +++ b/src/Mod/PartDesign/Gui/Resources/PartDesign.qrc @@ -37,6 +37,7 @@ icons/PartDesign_Revolution.svg icons/PartDesign_Scaled.svg icons/PartDesign_ShapeBinder.svg + icons/PartDesign_Sprocket.svg icons/PartDesign_SubShapeBinder.svg icons/PartDesign_Subtractive_Box.svg icons/PartDesign_Subtractive_Cone.svg diff --git a/src/Mod/PartDesign/Gui/Resources/icons/PartDesign_Sprocket.svg b/src/Mod/PartDesign/Gui/Resources/icons/PartDesign_Sprocket.svg new file mode 100644 index 0000000000..15bc199d19 --- /dev/null +++ b/src/Mod/PartDesign/Gui/Resources/icons/PartDesign_Sprocket.svg @@ -0,0 +1,492 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/src/Mod/PartDesign/Gui/Workbench.cpp b/src/Mod/PartDesign/Gui/Workbench.cpp index 350c14dbe3..b8489d36fe 100644 --- a/src/Mod/PartDesign/Gui/Workbench.cpp +++ b/src/Mod/PartDesign/Gui/Workbench.cpp @@ -509,9 +509,10 @@ Gui::MenuItem* Workbench::setupMenuBar() const << "PartDesign_Boolean" << "Separator" //<< "PartDesign_Hole" - << "PartDesign_InvoluteGear" - << "Separator" - << "PartDesign_Migrate"; + << "PartDesign_Migrate" + << "PartDesign_Sprocket" + << "PartDesign_InvoluteGear"; + // For 0.13 a couple of python packages like numpy, matplotlib and others // are not deployed with the installer on Windows. Thus, the WizardShaft is diff --git a/src/Mod/PartDesign/InitGui.py b/src/Mod/PartDesign/InitGui.py index f25bd79358..c935ffb033 100644 --- a/src/Mod/PartDesign/InitGui.py +++ b/src/Mod/PartDesign/InitGui.py @@ -51,6 +51,7 @@ class PartDesignWorkbench ( Workbench ): import PartDesign try: from PartDesign import InvoluteGearFeature + from PartDesign import SprocketFeature except ImportError: print("Involute gear module cannot be loaded") #try: diff --git a/src/Mod/PartDesign/SprocketFeature.py b/src/Mod/PartDesign/SprocketFeature.py new file mode 100644 index 0000000000..95977518cb --- /dev/null +++ b/src/Mod/PartDesign/SprocketFeature.py @@ -0,0 +1,248 @@ +#*************************************************************************** +#* Copyright (c) 2020 Adam Spontarelli * +#* * +#* This program is free software; you can redistribute it and/or modify * +#* it under the terms of the GNU Lesser General Public License (LGPL) * +#* as published by the Free Software Foundation; either version 2 of * +#* the License, or (at your option) any later version. * +#* for detail see the LICENCE text file. * +#* * +#* This program is distributed in the hope that it will be useful, * +#* but WITHOUT ANY WARRANTY; without even the implied warranty of * +#* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +#* GNU Library General Public License for more details. * +#* * +#* You should have received a copy of the GNU Library General Public * +#* License along with this program; if not, write to the Free Software * +#* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +#* USA * +#* * +#*************************************************************************** + +import FreeCAD, Part +from fcsprocket import fcsprocket +from fcsprocket import sprocket + +if FreeCAD.GuiUp: + import FreeCADGui + from PySide import QtCore, QtGui + from FreeCADGui import PySideUic as uic + +__title__="PartDesign SprocketObject management" +__author__ = "Adam Spontarelli" +__url__ = "http://www.freecadweb.org" + + +def makeSprocket(name): + """ + makeSprocket(name): makes a Sprocket + """ + obj = FreeCAD.ActiveDocument.addObject("Part::Part2DObjectPython",name) + Sprocket(obj) + if FreeCAD.GuiUp: + ViewProviderSprocket(obj.ViewObject) + #FreeCAD.ActiveDocument.recompute() + if FreeCAD.GuiUp: + body=FreeCADGui.ActiveDocument.ActiveView.getActiveObject("pdbody") + part=FreeCADGui.ActiveDocument.ActiveView.getActiveObject("part") + if body: + body.Group=body.Group+[obj] + elif part: + part.Group=part.Group+[obj] + return obj + +class CommandSprocket: + + """ + the Fem Sprocket command definition + """ + + def GetResources(self): + return {'Pixmap' : 'PartDesign_Sprocket', + 'MenuText': QtCore.QT_TRANSLATE_NOOP("PartDesign_Sprocket","Sprocket..."), + 'Accel': "", + 'ToolTip': QtCore.QT_TRANSLATE_NOOP("PartDesign_Sprocket","Creates or edit the sprocket definition.")} + + def Activated(self): + + FreeCAD.ActiveDocument.openTransaction("Create Sprocket") + FreeCADGui.addModule("SprocketFeature") + FreeCADGui.doCommand("SprocketFeature.makeSprocket('Sprocket')") + FreeCADGui.doCommand("Gui.activeDocument().setEdit(App.ActiveDocument.ActiveObject.Name,0)") + + def IsActive(self): + if FreeCAD.ActiveDocument: + return True + else: + return False + + +class Sprocket: + """ + The Sprocket object + """ + + def __init__(self,obj): + self.Type = "Sprocket" + obj.addProperty("App::PropertyInteger","NumberOfTeeth","Sprocket","Number of gear teeth") + obj.addProperty("App::PropertyLength","Pitch","Sprocket","Chain Pitch") + obj.addProperty("App::PropertyLength","RollerDiameter","Sprocket","Roller Diameter") + obj.addProperty("App::PropertyString","ANSISize","Sprocket","ANSI Size") + + obj.NumberOfTeeth = 50 + obj.Pitch = "0.375 in" + obj.RollerDiameter = "0.20 in" + obj.ANSISize = "35" + + obj.Proxy = self + + + def execute(self,obj): + w = fcsprocket.FCWireBuilder() + sprocket.CreateSprocket(w, obj.Pitch.Value, obj.NumberOfTeeth, obj.RollerDiameter.Value) + + sprocketw = Part.Wire([o.toShape() for o in w.wire]) + obj.Shape = sprocketw + obj.positionBySupport(); + return + + +class ViewProviderSprocket: + """ + A View Provider for the Sprocket object + """ + + def __init__(self,vobj): + vobj.Proxy = self + + def getIcon(self): + return ":/icons/PartDesign_Sprocket.svg" + + def attach(self, vobj): + self.ViewObject = vobj + self.Object = vobj.Object + + def setEdit(self,vobj,mode): + taskd = SprocketTaskPanel(self.Object,mode) + taskd.obj = vobj.Object + taskd.update() + FreeCADGui.Control.showDialog(taskd) + return True + + def unsetEdit(self,vobj,mode): + FreeCADGui.Control.closeDialog() + return + + def __getstate__(self): + return None + + def __setstate__(self,state): + return None + + +class SprocketTaskPanel: + """ + The editmode TaskPanel for Sprocket objects + """ + + def __init__(self,obj,mode): + self.obj = obj + + self.form=FreeCADGui.PySideUic.loadUi(FreeCAD.getHomePath() + "Mod/PartDesign/SprocketFeature.ui") + self.form.setWindowIcon(QtGui.QIcon(":/icons/PartDesign_Sprocket.svg")) + + QtCore.QObject.connect(self.form.Quantity_Pitch, QtCore.SIGNAL("valueChanged(double)"), self.pitchChanged) + QtCore.QObject.connect(self.form.Quantity_RollerDiameter, QtCore.SIGNAL("valueChanged(double)"), self.rollerDiameterChanged) + QtCore.QObject.connect(self.form.spinBox_NumberOfTeeth, QtCore.SIGNAL("valueChanged(int)"), self.numTeethChanged) + QtCore.QObject.connect(self.form.comboBox_ANSISize, QtCore.SIGNAL("currentTextChanged(const QString)"), self.ANSISizeChanged) + + self.update() + + if mode == 0: # fresh created + self.obj.Proxy.execute(self.obj) # calculate once + FreeCAD.Gui.SendMsgToActiveView("ViewFit") + + def transferTo(self): + """ + Transfer from the dialog to the object + """ + self.obj.NumberOfTeeth = self.form.spinBox_NumberOfTeeth.value() + self.obj.Pitch = self.form.Quantity_Pitch.text() + self.obj.RollerDiameter = self.form.Quantity_RollerDiameter.text() + self.obj.ANSISize = self.form.comboBox_ANSISize.currentText() + + def transferFrom(self): + """ + Transfer from the object to the dialog + """ + self.form.spinBox_NumberOfTeeth.setValue(self.obj.NumberOfTeeth) + self.form.Quantity_Pitch.setText(self.obj.Pitch.UserString) + self.form.Quantity_RollerDiameter.setText(self.obj.RollerDiameter.UserString) + self.form.comboBox_ANSISize.setCurrentText(self.obj.ANSISize) + + def pitchChanged(self, value): + self.obj.Pitch = value + self.obj.Proxy.execute(self.obj) + FreeCAD.Gui.SendMsgToActiveView("ViewFit") + + def ANSISizeChanged(self, size): + """ + ANSI B29.1-2011 standard roller chain sizes in USCS units (inches) + {size: [Pitch, Roller Diameter]} + """ + ANSIRollerTable = {"25": [0.250, 0.130], + "35": [0.375, 0.200], + "41": [0.500, 0.306], + "40": [0.500, 0.312], + "50": [0.625, 0.400], + "60": [0.750, 0.469], + "80": [1.000, 0.625], + "100":[1.250, 0.750], + "120":[1.500, 0.875], + "140":[1.750, 1.000], + "160":[2.000, 1.125], + "180":[2.250, 1.460], + "200":[2.500, 1.562], + "240":[3.000, 1.875]} + + self.obj.Pitch = str(ANSIRollerTable[size][0]) + " in" + self.obj.RollerDiameter = str(ANSIRollerTable[size][1]) + " in" + self.form.Quantity_Pitch.setText(self.obj.Pitch.UserString) + self.form.Quantity_RollerDiameter.setText(self.obj.RollerDiameter.UserString) + + self.obj.Proxy.execute(self.obj) + FreeCAD.Gui.SendMsgToActiveView("ViewFit") + + def rollerDiameterChanged(self, value): + self.obj.RollerDiameter = value + self.obj.Proxy.execute(self.obj) + + def numTeethChanged(self, value): + self.obj.NumberOfTeeth = value + self.obj.Proxy.execute(self.obj) + FreeCAD.Gui.SendMsgToActiveView("ViewFit") + + def getStandardButtons(self): + return int(QtGui.QDialogButtonBox.Ok) | int(QtGui.QDialogButtonBox.Cancel)| int(QtGui.QDialogButtonBox.Apply) + + def clicked(self,button): + if button == QtGui.QDialogButtonBox.Apply: + self.transferTo() + self.obj.Proxy.execute(self.obj) + + def update(self): + self.transferFrom() + + def accept(self): + self.transferTo() + FreeCAD.ActiveDocument.recompute() + FreeCADGui.ActiveDocument.resetEdit() + + def reject(self): + FreeCADGui.ActiveDocument.resetEdit() + FreeCAD.ActiveDocument.abortTransaction() + + + +if FreeCAD.GuiUp: + FreeCADGui.addCommand('PartDesign_Sprocket', CommandSprocket()) diff --git a/src/Mod/PartDesign/SprocketFeature.ui b/src/Mod/PartDesign/SprocketFeature.ui new file mode 100644 index 0000000000..c1960a8837 --- /dev/null +++ b/src/Mod/PartDesign/SprocketFeature.ui @@ -0,0 +1,232 @@ + + + SprocketParameter + + + + 0 + 0 + 195 + 142 + + + + Sprocket parameter + + + + + + + Number of teeth: + + + + + + + 3 + + + 9999 + + + 50 + + + + + + + + ANSI Size: + + + + + + + + + 25 + + + + + 35 + + + + + 41 + + + + + 40 + + + + + 50 + + + + + 60 + + + + + 80 + + + + + 100 + + + + + 120 + + + + + 140 + + + + + 160 + + + + + 180 + + + + + 200 + + + + + 240 + + + + + + + + + + Chain Pitch: + + + + + + + + + + 0 + 0 + + + + + 80 + 20 + + + + + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + in + + + 3 + + + 2000.000000000000000 + + + 0.01 + + + + 0.001 + + + 0.375 + + + + + + + + + Roller Diameter: + + + + + + + + 0 + 0 + + + + + 80 + 20 + + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + in + + + 3 + + + 50.000000000000000 + + + 0.01 + + + + 0.01 + + + 0.20 + + + + + + + + + Gui::InputField + QLineEdit +
Gui/InputField.h
+
+
+ + +
diff --git a/src/Mod/PartDesign/fcsprocket/README.md b/src/Mod/PartDesign/fcsprocket/README.md new file mode 100644 index 0000000000..3759e676ce --- /dev/null +++ b/src/Mod/PartDesign/fcsprocket/README.md @@ -0,0 +1,24 @@ +================================================ + FCSprocket: a Sprocket Generator for FreeCAD +================================================ + +This is a simple sprocket generation tool. Sprockets are used in combination +with roller chain to transmit power. The tooth profiles are drawn according +to ANSI standards from the methods outlined in: + + Standard handbook of chains : chains for power transmission and material + handling. Boca Raton: Taylor & Francis, 2006. Print. + + AND + + Oberg, Erik, et al. Machinery's handbook : a reference book for the + mechanical engineer, designer, manufacturing engineer, draftsman, + toolmaker, and machinist. New York: Industrial Press, 2016. Print. + + +This code is based on the work of David Douard and his implementation of the +gear generator (fcgear) found in FreeCAD. + + +Copyright 2020 Adam Spontarelli . +Distributed under the LGPL licence. diff --git a/src/Mod/PartDesign/fcsprocket/__init__.py b/src/Mod/PartDesign/fcsprocket/__init__.py new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/src/Mod/PartDesign/fcsprocket/__init__.py @@ -0,0 +1 @@ + diff --git a/src/Mod/PartDesign/fcsprocket/fcsprocket.py b/src/Mod/PartDesign/fcsprocket/fcsprocket.py new file mode 100644 index 0000000000..533c5d47c5 --- /dev/null +++ b/src/Mod/PartDesign/fcsprocket/fcsprocket.py @@ -0,0 +1,105 @@ +# (c) 2014 David Douard +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License (LGPL) +# as published by the Free Software Foundation; either version 2 of +# the License, or (at your option) any later version. +# for detail see the LICENCE text file. +# +# FCGear is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Library General Public License for more details. +# +# You should have received a copy of the GNU Library General Public +# License along with FCGear; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + +from math import cos, sin, pi, acos, asin, atan, sqrt + +import FreeCAD, Part +from FreeCAD import Base, Console +from . import sprocket +rotate = sprocket.rotate + +def makeSprocket(P, N, Dr): + if FreeCAD.ActiveDocument is None: + FreeCAD.newDocument("Sprocket") + doc = FreeCAD.ActiveDocument + w = FCWireBuilder() + sprocket.CreateSprocket(w, P, N, Dr) + sprocketw = Part.Wire([o.toShape() for o in w.wire]) + sprocket = doc.addObject("Part::Feature", "Sprocket") + sprocket.Shape = sprocketw + return sprocket + +class FCWireBuilder(object): + """A helper class to prepare a Part.Wire object""" + def __init__(self): + self.pos = None + self.theta = 0.0 + self.wire = [] + + def move(self, p): + """set current position""" + self.pos = Base.Vector(*p) + + def line(self, p): + """Add a segment between self.pos and p""" + p = rotate(p, self.theta) + end = Base.Vector(*p) + self.wire.append(Part.LineSegment(self.pos, end)) + self.pos = end + + def arc(self, p, r, sweep): + """"Add an arc from self.pos to p which radius is r + sweep (0 or 1) determine the orientation of the arc + """ + p = rotate(p, self.theta) + end = Base.Vector(*p) + mid = Base.Vector(*(midpoints(p, self.pos, r)[sweep])) + self.wire.append(Part.Arc(self.pos, mid, end)) + self.pos = end + + def curve(self, *points): + """Add a Bezier curve from self.pos to points[-1] + every other points are the control points of the Bezier curve (which + will thus be of degree len(points) ) + """ + points = [Base.Vector(*rotate(p, self.theta)) for p in points] + bz = Part.BezierCurve() + bz.setPoles([self.pos] + points) + self.wire.append(bz) + self.pos = points[-1] + + def close(self): + pass + +def midpoints(p1, p2, r): + """A very ugly function that returns the midpoint of a p1 and p2 + on the circle which radius is r and which pass through p1 and + p2 + + Return the 2 possible solutions + """ + vx, vy = p2[0]-p1[0], p2[1]-p1[1] + b = (vx**2 + vy**2)**.5 + v = (vx/b, vy/b) + cosA = b**2 / (2*b*r) + A = acos(cosA) + + vx, vy = rotate(v, A) + c1 = (p1[0]+r*vx, p1[1]+r*vy) + m1x, m1y = ((p1[0]+p2[0])/2 - c1[0], (p1[1]+p2[1])/2 - c1[1]) + dm1 = (m1x**2+m1y**2)**.5 + m1x, m1y = (c1[0] + r*m1x/dm1, c1[1] + r*m1y/dm1) + m1 = (m1x, m1y) + + vx, vy = rotate(v, -A) + c2 = (p1[0]+r*vx, p1[1]+r*vy) + m2x, m2y = ((p1[0]+p2[0])/2 - c2[0], (p1[1]+p2[1])/2 - c2[1]) + dm2 = (m2x**2+m2y**2)**.5 + m2x, m2y = (c2[0] + r*m2x/dm2, c2[1] + r*m2y/dm2) + m2 = (m2x, m2y) + + return m1, m2 diff --git a/src/Mod/PartDesign/fcsprocket/fcsprocketdialog.py b/src/Mod/PartDesign/fcsprocket/fcsprocketdialog.py new file mode 100644 index 0000000000..7360a7235d --- /dev/null +++ b/src/Mod/PartDesign/fcsprocket/fcsprocketdialog.py @@ -0,0 +1,66 @@ +# (c) 2020 Adam Spontarelli +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License (LGPL) +# as published by the Free Software Foundation; either version 2 of +# the License, or (at your option) any later version. +# for detail see the LICENCE text file. +# +# FCGear is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Library General Public License for more details. +# +# You should have received a copy of the GNU Library General Public +# License along with FCGear; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + +from PySide import QtGui as qt +import fcsprocket +import FreeCAD, FreeCADGui + +class SprocketCreationFrame(qt.QFrame): + def __init__(self, parent=None): + super(SprocketCreationFrame, self).__init__(parent) + self.P = qt.QSpinBox(value=0.375) + self.N = qt.QDoubleSpinBox(value=45) + self.Dr = qt.QDoubleSpinBox(value=0.20) + + l = qt.QFormLayout(self) + l.setFieldGrowthPolicy(l.ExpandingFieldsGrow) + l.addRow('Number of teeth:', self.N) + l.addRow('Chain Pitch (in):', self.P) + l.addRow('Roller Diameter (in):', self.Dr) + + +class SprocketDialog(qt.QDialog): + def __init__(self, parent=None): + super(SprocketDialog, self).__init__(parent) + self.gc = SprocketCreationFrame() + + btns = qt.QDialogButtonBox.Ok | qt.QDialogButtonBox.Cancel + buttonBox = qt.QDialogButtonBox(btns, + accepted=self.accept, + rejected=self.reject) + l = qt.QVBoxLayout(self) + l.addWidget(self.gc) + l.addWidget(buttonBox) + self.setWindowTitle('Sprocket creation dialog') + + def accept(self): + if FreeCAD.ActiveDocument is None: + FreeCAD.newDocument("Sprocket") + + gear = fcgear.makeSprocket(self.gc.m.value(), + self.gc.Z.value(), + self.gc.angle.value(), + not self.gc.split.currentIndex()) + FreeCADGui.SendMsgToActiveView("ViewFit") + return super(SprocketDialog, self).accept() + + +if __name__ == '__main__': + a = qt.QApplication([]) + w = SprocketDialog() + w.show() + a.exec_() diff --git a/src/Mod/PartDesign/fcsprocket/sprocket.py b/src/Mod/PartDesign/fcsprocket/sprocket.py new file mode 100644 index 0000000000..9c1bc1c6b2 --- /dev/null +++ b/src/Mod/PartDesign/fcsprocket/sprocket.py @@ -0,0 +1,139 @@ +# (c) 2020 Adam Spontarelli +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License (LGPL) +# as published by the Free Software Foundation; either version 2 of +# the License, or (at your option) any later version. +# for detail see the LICENCE text file. +# +# FCGear is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Library General Public License for more details. +# +# You should have received a copy of the GNU Library General Public +# License along with FCGear; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + +from math import cos, sin, tan, sqrt, radians, acos, atan, asin, degrees + + +def CreateSprocket(w, P, N, Dr): + """ + Create a sprocket + + w is the wirebuilder object (in which the sprocket will be constructed) + P is the chain pitch + N is the number of teeth + Dr is the roller diameter + + Remaining variables can be found in Standard Handbook of Chains + """ + Ds = 1.005 * Dr + (0.003 * 25.4) + R = Ds / 2 + alpha = 35 + 60/N + beta = 18 - 56 / N + M = 0.8 * Dr * cos(radians(35) + radians(60/N)) + T = 0.8 * Dr * sin(radians(35) + radians(60/N)) + E = 1.3025 * Dr + (0.0015 * 25.4) + W = 1.4 * Dr * cos(radians(180/N)) + V = 1.4 * Dr * sin(radians(180/N)) + F = Dr * (0.8 * cos(radians(18) - radians(56)/N) + 1.4 * + cos(radians(17) - radians(64) / N) - 1.3025) - (0.0015 * 25.4) + PD = P / (sin(radians(180)/N)) + H = sqrt(F**2 - (1.4 * Dr - P/2)**2) + OD = P * (0.6 + 1/tan(radians(180/N))) + + # The sprocket tooth gullet consists of four segments + x0 = 0 + y0 = PD/2 - R + + # ---- Segment 1 ----- + alpha = 35 + 60/N + x1 = -R * cos(radians(alpha)) + y1 = PD/2 - R * sin(radians(alpha)) + arc_end = [x1, y1] + + # ---- Segment 2 ----- + alpha = 35 + 60/N + beta = 18 - 56 / N + x2 = M - E * cos(radians(alpha-beta)) + y2 = T - E * sin(radians(alpha-beta)) + PD/2 + + # # ---- Segment 3 ----- + y2o = y2 - PD/2 + hyp = sqrt((-W-x2)**2 + (-V-y2o)**2) + AP = sqrt(hyp**2 - F**2) + gamma = atan((y2o + V)/(x2 + W)) + alpha = asin(AP / hyp) + beta = 180 - (90 - degrees(alpha)) - (90 - degrees(gamma)) + x3o = AP * sin(radians(beta)) + y3o = AP * cos(radians(beta)) + x3 = x2 - x3o + y3 = y2 + y3o + + # ---- Segment 4 ----- + alpha = 180/N + m = -1/tan(radians(alpha)) + yf = PD/2 - V + A = 1 + m**2 + B = 2*m*yf - 2*W + C = W**2 + yf**2 - F**2 + # x4a = (-B - sqrt(B**2 - 4 * A * C)) / (2*A) + x4b = (-B + sqrt(B**2 - 4 * A * C)) / (2*A) + x4 = -x4b + y4 = m * x4 + + p0 = [x0,y0] + p1 = [x1,y1] + p2 = [x2,y2] + p3 = [x3,y3] + p4 = [x4,y4] + p5 = [-x1,y1] + p6 = [-x2,y2] + p7 = [-x3,y3] + p8 = [-x4,y4] + + w.move(p4) # vectors are lists [x,y] + w.arc(p3, F, 0) + w.line(p2) + w.arc(p1, E, 1) + w.arc(p0, R, 1) + + # ---- Mirror ----- + w.arc(p5, R, 1) + w.arc(p6, E, 1) + w.line(p7) + w.arc(p8, F, 0) + + # ---- Polar Array ---- + alpha = -radians(360/N) + for n in range(1,N): + # falling gullet slope + w.arc(rotate(p3, alpha*n), F, 0) + w.line(rotate(p2, alpha*n)) + w.arc(rotate(p1, alpha*n), E, 1) + w.arc(rotate(p0, alpha*n), R, 1) + + # rising gullet slope + w.arc(rotate(p5, alpha*n), R, 1) + w.line(rotate(p6, alpha*n)) + w.arc(rotate(p7, alpha*n), E, 0) + w.arc(rotate(p8, alpha*n), F, 0) + + w.close() + + return w + + +def rotate(pt, rads): + """ + rotate pt by rads radians about origin + """ + sinA = sin(rads) + cosA = cos(rads) + return (pt[0] * cosA - pt[1] * sinA, + pt[0] * sinA + pt[1] * cosA) + + + diff --git a/src/Mod/Path/App/Area.cpp b/src/Mod/Path/App/Area.cpp index 1badf749da..c0f775879b 100644 --- a/src/Mod/Path/App/Area.cpp +++ b/src/Mod/Path/App/Area.cpp @@ -1189,10 +1189,8 @@ static int foreachSubshape(const TopoDS_Shape &shape, BRep_Builder builder; TopoDS_Compound comp; builder.MakeCompound(comp); - for(auto &s : openShapes) { - for(TopExp_Explorer it(s,TopAbs_EDGE); it.More(); it.Next()) - builder.Add(comp,s); - } + for(auto &s : openShapes) + builder.Add(comp,s); func(comp, TopAbs_COMPOUND); return TopAbs_COMPOUND; } diff --git a/src/Mod/Path/CMakeLists.txt b/src/Mod/Path/CMakeLists.txt index ded7c91a93..78415e1e32 100644 --- a/src/Mod/Path/CMakeLists.txt +++ b/src/Mod/Path/CMakeLists.txt @@ -107,6 +107,7 @@ SET(PathScripts_SRCS PathScripts/PathStop.py PathScripts/PathSurface.py PathScripts/PathSurfaceGui.py + PathScripts/PathSurfaceSupport.py PathScripts/PathToolBit.py PathScripts/PathToolBitCmd.py PathScripts/PathToolBitEdit.py diff --git a/src/Mod/Path/Gui/Resources/panels/PageOpDeburrEdit.ui b/src/Mod/Path/Gui/Resources/panels/PageOpDeburrEdit.ui index 452e6ab7e3..31eb02df70 100644 --- a/src/Mod/Path/Gui/Resources/panels/PageOpDeburrEdit.ui +++ b/src/Mod/Path/Gui/Resources/panels/PageOpDeburrEdit.ui @@ -6,14 +6,14 @@ 0 0 - 399 - 333 + 321 + 529
Form - + @@ -31,6 +31,24 @@ + + + 0 + 0 + + + + + 125 + 0 + + + + + 16777215 + 16777215 + + Tool Controller @@ -45,6 +63,24 @@ + + + 0 + 0 + + + + + 125 + 0 + + + + + 16777215 + 16777215 + + Coolant Mode @@ -60,254 +96,301 @@ + + + + 6 + + + 12 + + + 12 + + + + + + 0 + 0 + + + + + 125 + 0 + + + + + 16777215 + 16777215 + + + + Direction + + + + + + + <html><head/><body><p>The direction in which the profile is performed, clockwise or counter clockwise.</p></body></html> + + + CW + + + 0 + + + + CW + + + + + CCW + + + + + + - - - - - - - 0 - - - 0 - - - 0 - - - 9 - - - - - - QFormLayout::AllNonFixedFieldsGrow - - - - - W = - - + + + + + + 125 + 0 + + + + + 16777215 + 16777215 + + + + + + + QLayout::SetDefaultConstraint + + + + + + + + + + 50 + 0 + + + + W = + + + + + + + <html><head/><body><p>Width of chamfer cut.</p></body></html> + + + mm + + + + - - - - <html><head/><body><p>Width of chamfer cut.</p></body></html> - - - mm - - + + + + + + + 50 + 0 + + + + h = + + + + + + + <html><head/><body><p>Extra depth of tool immersion.</p></body></html> + + + mm + + + + - - - - h = - - - - - - - <html><head/><body><p>Extra depth of tool immersion.</p></body></html> - - - mm - - - - - - - - - - - - - Join: - - - - - + + - Qt::Horizontal + Qt::Vertical + + + QSizePolicy::Fixed - 40 - 20 + 20 + 10 - - - - <html><head/><body><p>Miter joint</p></body></html> - - - - - - true - - - true - - - - - - - <html><head/><body><p>Round joint</p></body></html> - - - - - - true - - - true - - - true - - + + + + + + + 50 + 0 + + + + Join: + + + + + + + <html><head/><body><p>Round joint</p></body></html> + + + + + + true + + + true + + + true + + + + + + + <html><head/><body><p>Miter joint</p></body></html> + + + + + + true + + + true + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + - - - - - - - - - - - - 0 - 0 - - - - - 150 - 150 - - - - - 150 - 150 - - - - TextLabel - - - true - - - Qt::AlignCenter - - - - - - - Qt::Vertical - - - - 20 - 40 - - - + + + + + Qt::Vertical + + + + 20 + 40 + + + + + - - - - - - - - - 9 - - - 9 - - - 9 - - - 9 - - - 8 - - - - - <html><head/><body><p>The direction in which the profile is performed, clockwise or counter clockwise.</p></body></html> - - - CW - - - 0 - - - - CW - + + + + + + + + + + + + 0 + 0 + + + + + 150 + 150 + + + + + 150 + 150 + + + + TextLabel + + + true + + + Qt::AlignCenter + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + - - - CCW - - - - - - - - - 0 - 0 - - - - Direction - - - - - - - Qt::Horizontal - - - QSizePolicy::Preferred - - - - 30 - 20 - - - - - - + +
+
+
diff --git a/src/Mod/Path/Gui/Resources/panels/PageOpSurfaceEdit.ui b/src/Mod/Path/Gui/Resources/panels/PageOpSurfaceEdit.ui index e4aaf19e5e..bb14461cc4 100644 --- a/src/Mod/Path/Gui/Resources/panels/PageOpSurfaceEdit.ui +++ b/src/Mod/Path/Gui/Resources/panels/PageOpSurfaceEdit.ui @@ -6,7 +6,7 @@ 0 0 - 350 + 368 400
@@ -59,6 +59,9 @@ + + <html><head/><body><p>Planar: Flat, 3D surface scan. Rotational: 4th-axis rotational scan.</p></body></html> + Planar @@ -73,6 +76,9 @@ + + <html><head/><body><p>Complete the operation in a single pass at depth, or mulitiple passes to final depth.</p></body></html> + Single-pass @@ -127,6 +133,9 @@ + + <html><head/><body><p>Enable optimization of linear paths (co-linear points). Removes unnecessary co-linear points from G-Code output.</p></body></html> + Optimize Linear Paths @@ -148,6 +157,9 @@ + + <html><head/><body><p>Make True, if specifying a Start Point</p></body></html> + Use Start Point @@ -169,6 +181,9 @@ + + <html><head/><body><p>Set the Z-axis depth offset from the target surface.</p></body></html> + mm @@ -184,6 +199,9 @@ 0
+ + <html><head/><body><p>Additional offset to the selected bounding box along the X axis."</p></body></html> + mm @@ -191,6 +209,9 @@ + + <html><head/><body><p>Additional offset to the selected bounding box along the Y axis."</p></body></html> + mm @@ -200,6 +221,9 @@ + + <html><head/><body><p>Set the sampling resolution. Smaller values quickly increase processing time.</p></body></html> + mm @@ -214,6 +238,9 @@ + + <html><head/><body><p>Dropcutter lines are created parallel to this axis.</p></body></html> + X @@ -228,6 +255,9 @@ + + <html><head/><body><p>Select the overall boundary for the operation.</p></body></html> + Stock @@ -242,6 +272,9 @@ + + <html><head/><body><p>Enable separate optimization of transitions between, and breaks within, each step over path.</p></body></html> + Optimize StepOver Transitions @@ -256,16 +289,9 @@ - - - Line - - - - - ZigZag - - + + <html><head/><body><p>Set the geometric clearing pattern to use for the operation.</p></body></html> + Circular @@ -276,6 +302,26 @@ CircularZigZag + + + Line + + + + + Offset + + + + + Spiral + + + + + ZigZag + + @@ -299,8 +345,8 @@ Gui::InputField - QWidget -
gui::inputfield.h
+ QLineEdit +
Gui/InputField.h
diff --git a/src/Mod/Path/Gui/Resources/panels/PageOpWaterlineEdit.ui b/src/Mod/Path/Gui/Resources/panels/PageOpWaterlineEdit.ui index 5e0edef1c9..82533fe061 100644 --- a/src/Mod/Path/Gui/Resources/panels/PageOpWaterlineEdit.ui +++ b/src/Mod/Path/Gui/Resources/panels/PageOpWaterlineEdit.ui @@ -50,6 +50,9 @@ 8 + + <html><head/><body><p>Select the overall boundary for the operation.</p></body></html> + Stock @@ -70,6 +73,9 @@ 0 + + <html><head/><body><p>Positive values push the cutter toward, or beyond, the boundary. Negative values retract the cutter away from the boundary.</p></body></html> +
@@ -79,6 +85,9 @@ 8 + + <html><head/><body><p>Complete the operation in a single pass at depth, or mulitiple passes to final depth.</p></body></html> + Single-pass @@ -93,6 +102,9 @@ + + <html><head/><body><p>Select the algorithm to use: OCL Dropcutter*, or Experimental (Not OCL based).</p></body></html> + OCL Dropcutter @@ -107,6 +119,9 @@ + + <html><head/><body><p>Enable optimization of linear paths (co-linear points). Removes unnecessary co-linear points from G-Code output.</p></body></html> + Optimize Linear Paths @@ -132,6 +147,9 @@ 8 + + <html><head/><body><p>Set the geometric clearing pattern to use for the operation.</p></body></html> + None @@ -213,6 +231,9 @@ + + <html><head/><body><p>Set the sampling resolution. Smaller values quickly increase processing time.</p></body></html> + mm @@ -260,8 +281,8 @@ Gui::InputField - QWidget -
gui::inputfield.h
+ QLineEdit +
Gui/InputField.h
diff --git a/src/Mod/Path/PathScripts/PathAreaOp.py b/src/Mod/Path/PathScripts/PathAreaOp.py index 3ccb4a39d6..b40e932eb8 100644 --- a/src/Mod/Path/PathScripts/PathAreaOp.py +++ b/src/Mod/Path/PathScripts/PathAreaOp.py @@ -28,9 +28,12 @@ import PathScripts.PathLog as PathLog import PathScripts.PathOp as PathOp import PathScripts.PathUtils as PathUtils import PathScripts.PathGeom as PathGeom -import Draft import math -import Part + +# lazily loaded modules +from lazy_loader.lazy_loader import LazyLoader +Draft = LazyLoader('Draft', globals(), 'Draft') +Part = LazyLoader('Part', globals(), 'Part') # from PathScripts.PathUtils import waiting_effects from PySide import QtCore diff --git a/src/Mod/Path/PathScripts/PathCircularHoleBase.py b/src/Mod/Path/PathScripts/PathCircularHoleBase.py index 570d7c09e8..09e57a770d 100644 --- a/src/Mod/Path/PathScripts/PathCircularHoleBase.py +++ b/src/Mod/Path/PathScripts/PathCircularHoleBase.py @@ -28,10 +28,7 @@ # * * # *************************************************************************** -import ArchPanel import FreeCAD -import DraftGeomUtils -import Part import PathScripts.PathLog as PathLog import PathScripts.PathOp as PathOp import PathScripts.PathUtils as PathUtils @@ -39,8 +36,14 @@ import PathScripts.PathUtils as PathUtils from PySide import QtCore import PathScripts.PathGeom as PathGeom +# lazily loaded modules +from lazy_loader.lazy_loader import LazyLoader +ArchPanel = LazyLoader('ArchPanel', globals(), 'ArchPanel') +Draft = LazyLoader('Draft', globals(), 'Draft') +Part = LazyLoader('Part', globals(), 'Part') +DraftGeomUtils = LazyLoader('DraftGeomUtils', globals(), 'DraftGeomUtils') + import math -import Draft if FreeCAD.GuiUp: import FreeCADGui diff --git a/src/Mod/Path/PathScripts/PathDeburr.py b/src/Mod/Path/PathScripts/PathDeburr.py index aa1b1e6d21..26f0fe2acc 100644 --- a/src/Mod/Path/PathScripts/PathDeburr.py +++ b/src/Mod/Path/PathScripts/PathDeburr.py @@ -24,7 +24,6 @@ # *************************************************************************** import FreeCAD -import Part import PathScripts.PathEngraveBase as PathEngraveBase import PathScripts.PathLog as PathLog import PathScripts.PathOp as PathOp @@ -33,6 +32,10 @@ import math from PySide import QtCore +# lazily loaded modules +from lazy_loader.lazy_loader import LazyLoader +Part = LazyLoader('Part', globals(), 'Part') + __title__ = "Path Deburr Operation" __author__ = "sliptonic (Brad Collette), Schildkroet" __url__ = "http://www.freecadweb.org" diff --git a/src/Mod/Path/PathScripts/PathDeburrGui.py b/src/Mod/Path/PathScripts/PathDeburrGui.py index ed25c644fa..6334b6e9fc 100644 --- a/src/Mod/Path/PathScripts/PathDeburrGui.py +++ b/src/Mod/Path/PathScripts/PathDeburrGui.py @@ -101,6 +101,8 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage): signals.append(self.form.joinRound.clicked) signals.append(self.form.coolantController.currentIndexChanged) signals.append(self.form.direction.currentIndexChanged) + signals.append(self.form.value_W.valueChanged) + signals.append(self.form.value_h.valueChanged) return signals def registerSignalHandlers(self, obj): diff --git a/src/Mod/Path/PathScripts/PathDressupDogbone.py b/src/Mod/Path/PathScripts/PathDressupDogbone.py index 80e697a691..b168532455 100644 --- a/src/Mod/Path/PathScripts/PathDressupDogbone.py +++ b/src/Mod/Path/PathScripts/PathDressupDogbone.py @@ -22,10 +22,8 @@ # * * # *************************************************************************** from __future__ import print_function -import DraftGeomUtils import FreeCAD import math -import Part import Path import PathScripts.PathDressup as PathDressup import PathScripts.PathGeom as PathGeom @@ -35,6 +33,11 @@ import PathScripts.PathUtils as PathUtils from PySide import QtCore +# lazily loaded modules +from lazy_loader.lazy_loader import LazyLoader +DraftGeomUtils = LazyLoader('DraftGeomUtils', globals(), 'DraftGeomUtils') +Part = LazyLoader('Part', globals(), 'Part') + LOG_MODULE = PathLog.thisModule() PathLog.setLevel(PathLog.Level.NOTICE, LOG_MODULE) diff --git a/src/Mod/Path/PathScripts/PathDressupDragknife.py b/src/Mod/Path/PathScripts/PathDressupDragknife.py index 927c07fc44..38badc176d 100644 --- a/src/Mod/Path/PathScripts/PathDressupDragknife.py +++ b/src/Mod/Path/PathScripts/PathDressupDragknife.py @@ -27,9 +27,12 @@ import FreeCAD import Path from PySide import QtCore import math -import DraftVecUtils as D import PathScripts.PathUtils as PathUtils +# lazily loaded modules +from lazy_loader.lazy_loader import LazyLoader +D = LazyLoader('DraftVecUtils', globals(), 'DraftVecUtils') + __doc__ = """Dragknife Dressup object and FreeCAD command""" if FreeCAD.GuiUp: diff --git a/src/Mod/Path/PathScripts/PathDressupHoldingTags.py b/src/Mod/Path/PathScripts/PathDressupHoldingTags.py index e1dd5daba6..d619f0fd13 100644 --- a/src/Mod/Path/PathScripts/PathDressupHoldingTags.py +++ b/src/Mod/Path/PathScripts/PathDressupHoldingTags.py @@ -22,7 +22,6 @@ # * * # *************************************************************************** import FreeCAD -import Part import Path import PathScripts.PathDressup as PathDressup import PathScripts.PathGeom as PathGeom @@ -36,6 +35,10 @@ from PathScripts.PathDressupTagPreferences import HoldingTagPreferences from PathScripts.PathUtils import waiting_effects from PySide import QtCore +# lazily loaded modules +from lazy_loader.lazy_loader import LazyLoader +Part = LazyLoader('Part', globals(), 'Part') + PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule()) #PathLog.trackModule() diff --git a/src/Mod/Path/PathScripts/PathDressupRampEntry.py b/src/Mod/Path/PathScripts/PathDressupRampEntry.py index ad363b984b..1255472d23 100644 --- a/src/Mod/Path/PathScripts/PathDressupRampEntry.py +++ b/src/Mod/Path/PathScripts/PathDressupRampEntry.py @@ -23,7 +23,6 @@ # *************************************************************************** import FreeCAD import Path -import Part import PathScripts.PathDressup as PathDressup import PathScripts.PathGeom as PathGeom import PathScripts.PathLog as PathLog @@ -32,6 +31,10 @@ import math from PathScripts import PathUtils from PySide import QtCore +# lazily loaded modules +from lazy_loader.lazy_loader import LazyLoader +Part = LazyLoader('Part', globals(), 'Part') + if FreeCAD.GuiUp: import FreeCADGui diff --git a/src/Mod/Path/PathScripts/PathDressupTag.py b/src/Mod/Path/PathScripts/PathDressupTag.py index 403210fbd3..a610e7939a 100644 --- a/src/Mod/Path/PathScripts/PathDressupTag.py +++ b/src/Mod/Path/PathScripts/PathDressupTag.py @@ -22,14 +22,17 @@ # * * # *************************************************************************** import FreeCAD -import DraftGeomUtils -import Part import PathScripts.PathDressup as PathDressup import PathScripts.PathGeom as PathGeom import PathScripts.PathLog as PathLog import PathScripts.PathUtils as PathUtils import math +# lazily loaded modules +from lazy_loader.lazy_loader import LazyLoader +DraftGeomUtils = LazyLoader('DraftGeomUtils', globals(), 'DraftGeomUtils') +Part = LazyLoader('Part', globals(), 'Part') + from PathScripts.PathDressupTagPreferences import HoldingTagPreferences from PySide import QtCore diff --git a/src/Mod/Path/PathScripts/PathDressupZCorrect.py b/src/Mod/Path/PathScripts/PathDressupZCorrect.py index b3e766d89e..8f26281d66 100644 --- a/src/Mod/Path/PathScripts/PathDressupZCorrect.py +++ b/src/Mod/Path/PathScripts/PathDressupZCorrect.py @@ -27,7 +27,6 @@ # *************************************************************************** import FreeCAD import FreeCADGui -import Part import Path import PathScripts.PathGeom as PathGeom import PathScripts.PathLog as PathLog @@ -35,6 +34,10 @@ import PathScripts.PathUtils as PathUtils from PySide import QtCore, QtGui +# lazily loaded modules +from lazy_loader.lazy_loader import LazyLoader +Part = LazyLoader('Part', globals(), 'Part') + """Z Depth Correction Dressup. This dressup takes a probe file as input and does bilinear interpolation of the Zdepths to correct for a surface which is not parallel to the milling table/bed. The probe file should conform to the format specified by the linuxcnc G38 probe logging: 9-number coordinate consisting of XYZABCUVW http://linuxcnc.org/docs/html/gcode/g-code.html#gcode:g38 """ diff --git a/src/Mod/Path/PathScripts/PathEngrave.py b/src/Mod/Path/PathScripts/PathEngrave.py index 8063e73484..f1e2d4cdae 100644 --- a/src/Mod/Path/PathScripts/PathEngrave.py +++ b/src/Mod/Path/PathScripts/PathEngrave.py @@ -22,9 +22,7 @@ # * * # *************************************************************************** -import ArchPanel import FreeCAD -import Part import Path import PathScripts.PathEngraveBase as PathEngraveBase import PathScripts.PathLog as PathLog @@ -33,6 +31,11 @@ import PathScripts.PathUtils as PathUtils from PySide import QtCore +# lazily loaded modules +from lazy_loader.lazy_loader import LazyLoader +ArchPanel = LazyLoader('ArchPanel', globals(), 'ArchPanel') +Part = LazyLoader('Part', globals(), 'Part') + __doc__ = "Class and implementation of Path Engrave operation" PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule()) diff --git a/src/Mod/Path/PathScripts/PathEngraveBase.py b/src/Mod/Path/PathScripts/PathEngraveBase.py index 459e60a839..7606ec9b58 100644 --- a/src/Mod/Path/PathScripts/PathEngraveBase.py +++ b/src/Mod/Path/PathScripts/PathEngraveBase.py @@ -22,7 +22,6 @@ # * * # *************************************************************************** -import DraftGeomUtils import Path import PathScripts.PathGeom as PathGeom import PathScripts.PathLog as PathLog @@ -30,6 +29,10 @@ import PathScripts.PathOp as PathOp import PathScripts.PathOpTools as PathOpTools import copy +# lazily loaded modules +from lazy_loader.lazy_loader import LazyLoader +DraftGeomUtils = LazyLoader('DraftGeomUtils', globals(), 'DraftGeomUtils') + from PySide import QtCore __doc__ = "Base class for all ops in the engrave family." diff --git a/src/Mod/Path/PathScripts/PathGeom.py b/src/Mod/Path/PathScripts/PathGeom.py index fba8d1ddb1..5782fa747f 100644 --- a/src/Mod/Path/PathScripts/PathGeom.py +++ b/src/Mod/Path/PathScripts/PathGeom.py @@ -23,7 +23,6 @@ # *************************************************************************** import FreeCAD -import Part import Path import PathScripts.PathLog as PathLog import math @@ -31,6 +30,10 @@ import math from FreeCAD import Vector from PySide import QtCore +# lazily loaded modules +from lazy_loader.lazy_loader import LazyLoader +Part = LazyLoader('Part', globals(), 'Part') + __title__ = "PathGeom - geometry utilities for Path" __author__ = "sliptonic (Brad Collette)" __url__ = "http://www.freecadweb.org" diff --git a/src/Mod/Path/PathScripts/PathGetPoint.py b/src/Mod/Path/PathScripts/PathGetPoint.py index fc54f69498..93ac673fc2 100644 --- a/src/Mod/Path/PathScripts/PathGetPoint.py +++ b/src/Mod/Path/PathScripts/PathGetPoint.py @@ -22,11 +22,14 @@ # * * # *************************************************************************** -import Draft import FreeCAD import FreeCADGui import PathScripts.PathLog as PathLog +# lazily loaded modules +from lazy_loader.lazy_loader import LazyLoader +Draft = LazyLoader('Draft', globals(), 'Draft') + from PySide import QtCore, QtGui from pivy import coin diff --git a/src/Mod/Path/PathScripts/PathInspect.py b/src/Mod/Path/PathScripts/PathInspect.py index 448bc6b685..19cc291a78 100644 --- a/src/Mod/Path/PathScripts/PathInspect.py +++ b/src/Mod/Path/PathScripts/PathInspect.py @@ -112,7 +112,6 @@ class GCodeEditorDialog(QtGui.QDialog): font.setPointSize(p.GetInt("FontSize", 10)) self.editor.setFont(font) self.editor.setText("G01 X55 Y4.5 F300.0") - self.highlighter = GCodeHighlighter(self.editor.document()) layout.addWidget(self.editor) # Note @@ -192,11 +191,25 @@ class GCodeEditorDialog(QtGui.QDialog): def show(obj): "show(obj): shows the G-code data of the given Path object in a dialog" - + + prefs = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Path") + # default Max Highlighter Size = 512 Ko + defaultMHS = 512 * 1024 + mhs = prefs.GetUnsigned('inspecteditorMaxHighlighterSize', defaultMHS) + if hasattr(obj, "Path"): if obj.Path: dia = GCodeEditorDialog(obj.Path) dia.editor.setText(obj.Path.toGCode()) + gcodeSize = len(dia.editor.toPlainText()) + if (gcodeSize <= mhs): + # because of poor performance, syntax highlighting is + # limited to mhs octets (default 512 KB). + # It seems than the response time curve has an inflexion near 500 KB + # beyond 500 KB, the response time increases exponentially. + dia.highlighter = GCodeHighlighter(dia.editor.document()) + else: + FreeCAD.Console.PrintMessage(translate("Path", "GCode size too big ({} o), disabling syntax highlighter.".format(gcodeSize))) result = dia.exec_() # exec_() returns 0 or 1 depending on the button pressed (Ok or # Cancel) diff --git a/src/Mod/Path/PathScripts/PathJob.py b/src/Mod/Path/PathScripts/PathJob.py index c57291b470..2225c2fa9f 100644 --- a/src/Mod/Path/PathScripts/PathJob.py +++ b/src/Mod/Path/PathScripts/PathJob.py @@ -22,8 +22,6 @@ # * * # *************************************************************************** -import ArchPanel -import Draft import FreeCAD import PathScripts.PathIconViewProvider as PathIconViewProvider import PathScripts.PathLog as PathLog @@ -34,6 +32,11 @@ import PathScripts.PathToolController as PathToolController import PathScripts.PathUtil as PathUtil import json +# lazily loaded modules +from lazy_loader.lazy_loader import LazyLoader +ArchPanel = LazyLoader('ArchPanel', globals(), 'ArchPanel') +Draft = LazyLoader('Draft', globals(), 'Draft') + from PathScripts.PathPostProcessor import PostProcessor from PySide import QtCore diff --git a/src/Mod/Path/PathScripts/PathJobGui.py b/src/Mod/Path/PathScripts/PathJobGui.py index 186b2066bd..0b033cc4a9 100644 --- a/src/Mod/Path/PathScripts/PathJobGui.py +++ b/src/Mod/Path/PathScripts/PathJobGui.py @@ -22,8 +22,6 @@ # * * # *************************************************************************** -import Draft -import DraftVecUtils import FreeCAD import FreeCADGui import PathScripts.PathJob as PathJob @@ -42,6 +40,11 @@ import PathScripts.PathUtils as PathUtils import math import traceback +# lazily loaded modules +from lazy_loader.lazy_loader import LazyLoader +Draft = LazyLoader('Draft', globals(), 'Draft') +DraftVecUtils = LazyLoader('DraftVecUtils', globals(), 'DraftVecUtils') + from PySide import QtCore, QtGui from collections import Counter from contextlib import contextmanager diff --git a/src/Mod/Path/PathScripts/PathMillFace.py b/src/Mod/Path/PathScripts/PathMillFace.py index ed1fcfcbb0..b2f2f699c3 100644 --- a/src/Mod/Path/PathScripts/PathMillFace.py +++ b/src/Mod/Path/PathScripts/PathMillFace.py @@ -25,7 +25,6 @@ from __future__ import print_function import FreeCAD -import Part import PathScripts.PathLog as PathLog import PathScripts.PathPocketBase as PathPocketBase import PathScripts.PathUtils as PathUtils @@ -33,6 +32,10 @@ import PathScripts.PathUtils as PathUtils from PySide import QtCore import numpy +# lazily loaded modules +from lazy_loader.lazy_loader import LazyLoader +Part = LazyLoader('Part', globals(), 'Part') + __title__ = "Path Mill Face Operation" __author__ = "sliptonic (Brad Collette)" __url__ = "http://www.freecadweb.org" diff --git a/src/Mod/Path/PathScripts/PathOp.py b/src/Mod/Path/PathScripts/PathOp.py index 41297bf474..fab24c28c2 100644 --- a/src/Mod/Path/PathScripts/PathOp.py +++ b/src/Mod/Path/PathScripts/PathOp.py @@ -23,7 +23,6 @@ # *************************************************************************** import FreeCAD -import Part import Path import PathScripts.PathGeom as PathGeom import PathScripts.PathLog as PathLog @@ -33,6 +32,10 @@ import PathScripts.PathUtils as PathUtils from PathScripts.PathUtils import waiting_effects from PySide import QtCore +# lazily loaded modules +from lazy_loader.lazy_loader import LazyLoader +Part = LazyLoader('Part', globals(), 'Part') + __title__ = "Base class for all operations." __author__ = "sliptonic (Brad Collette)" __url__ = "http://www.freecadweb.org" diff --git a/src/Mod/Path/PathScripts/PathOpTools.py b/src/Mod/Path/PathScripts/PathOpTools.py index 50a0484c36..40e7105ca5 100644 --- a/src/Mod/Path/PathScripts/PathOpTools.py +++ b/src/Mod/Path/PathScripts/PathOpTools.py @@ -23,13 +23,16 @@ # *************************************************************************** import FreeCAD -import Part import PathScripts.PathGeom as PathGeom import PathScripts.PathLog as PathLog import math from PySide import QtCore +# lazily loaded modules +from lazy_loader.lazy_loader import LazyLoader +Part = LazyLoader('Part', globals(), 'Part') + __title__ = "PathOpTools - Tools for Path operations." __author__ = "sliptonic (Brad Collette)" __url__ = "http://www.freecadweb.org" diff --git a/src/Mod/Path/PathScripts/PathPocket.py b/src/Mod/Path/PathScripts/PathPocket.py index 355178ff52..27fd1a9441 100644 --- a/src/Mod/Path/PathScripts/PathPocket.py +++ b/src/Mod/Path/PathScripts/PathPocket.py @@ -23,7 +23,6 @@ # *************************************************************************** import FreeCAD -import Part import PathScripts.PathLog as PathLog import PathScripts.PathOp as PathOp import PathScripts.PathPocketBase as PathPocketBase @@ -31,6 +30,10 @@ import PathScripts.PathUtils as PathUtils from PySide import QtCore +# lazily loaded modules +from lazy_loader.lazy_loader import LazyLoader +Part = LazyLoader('Part', globals(), 'Part') + __title__ = "Path 3D Pocket Operation" __author__ = "Yorik van Havre " __url__ = "http://www.freecadweb.org" diff --git a/src/Mod/Path/PathScripts/PathPocketShape.py b/src/Mod/Path/PathScripts/PathPocketShape.py index 7c98f71d9c..6cc24e5ba4 100644 --- a/src/Mod/Path/PathScripts/PathPocketShape.py +++ b/src/Mod/Path/PathScripts/PathPocketShape.py @@ -24,15 +24,18 @@ # *************************************************************************** import FreeCAD -import Part import PathScripts.PathGeom as PathGeom import PathScripts.PathLog as PathLog import PathScripts.PathOp as PathOp import PathScripts.PathPocketBase as PathPocketBase import PathScripts.PathUtils as PathUtils -import TechDraw import math -import Draft + +# lazily loaded modules +from lazy_loader.lazy_loader import LazyLoader +Draft = LazyLoader('Draft', globals(), 'Draft') +Part = LazyLoader('Part', globals(), 'Part') +TechDraw = LazyLoader('TechDraw', globals(), 'TechDraw') from PySide import QtCore diff --git a/src/Mod/Path/PathScripts/PathPocketShapeGui.py b/src/Mod/Path/PathScripts/PathPocketShapeGui.py index c498e64750..f085d062c1 100644 --- a/src/Mod/Path/PathScripts/PathPocketShapeGui.py +++ b/src/Mod/Path/PathScripts/PathPocketShapeGui.py @@ -24,7 +24,6 @@ import FreeCAD import FreeCADGui -import Part import PathScripts.PathGeom as PathGeom import PathScripts.PathGui as PathGui import PathScripts.PathLog as PathLog @@ -35,6 +34,10 @@ import PathScripts.PathPocketBaseGui as PathPocketBaseGui from PySide import QtCore, QtGui from pivy import coin +# lazily loaded modules +from lazy_loader.lazy_loader import LazyLoader +Part = LazyLoader('Part', globals(), 'Part') + __title__ = "Path Pocket Shape Operation UI" __author__ = "sliptonic (Brad Collette)" __url__ = "http://www.freecadweb.org" diff --git a/src/Mod/Path/PathScripts/PathProfileContour.py b/src/Mod/Path/PathScripts/PathProfileContour.py index 8a08ceeea1..5aaeb8c56d 100644 --- a/src/Mod/Path/PathScripts/PathProfileContour.py +++ b/src/Mod/Path/PathScripts/PathProfileContour.py @@ -24,9 +24,7 @@ from __future__ import print_function -import ArchPanel import FreeCAD -import Part import Path import PathScripts.PathProfileBase as PathProfileBase import PathScripts.PathLog as PathLog @@ -34,6 +32,11 @@ import PathScripts.PathLog as PathLog from PathScripts import PathUtils from PySide import QtCore +# lazily loaded modules +from lazy_loader.lazy_loader import LazyLoader +ArchPanel = LazyLoader('ArchPanel', globals(), 'ArchPanel') +Part = LazyLoader('Part', globals(), 'Part') + FreeCAD.setLogLevel('Path.Area', 0) PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule()) diff --git a/src/Mod/Path/PathScripts/PathProfileEdges.py b/src/Mod/Path/PathScripts/PathProfileEdges.py index e01ee25d4c..26ee36d541 100644 --- a/src/Mod/Path/PathScripts/PathProfileEdges.py +++ b/src/Mod/Path/PathScripts/PathProfileEdges.py @@ -23,18 +23,20 @@ # *************************************************************************** import FreeCAD -import Part import Path import PathScripts.PathLog as PathLog import PathScripts.PathOp as PathOp import PathScripts.PathProfileBase as PathProfileBase import PathScripts.PathUtils as PathUtils -import DraftGeomUtils -import Draft import math import PySide +# lazily loaded modules +from lazy_loader.lazy_loader import LazyLoader +Part = LazyLoader('Part', globals(), 'Part') +DraftGeomUtils = LazyLoader('DraftGeomUtils', globals(), 'DraftGeomUtils') + PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule()) # PathLog.trackModule(PathLog.thisModule()) @@ -48,6 +50,7 @@ __title__ = "Path Profile Edges Operation" __author__ = "sliptonic (Brad Collette)" __url__ = "http://www.freecadweb.org" __doc__ = "Path Profile operation based on edges." +__contributors__ = "russ4262 (Russell Johnson)" class ObjectProfile(PathProfileBase.ObjectProfile): @@ -66,8 +69,10 @@ class ObjectProfile(PathProfileBase.ObjectProfile): '''areaOpShapes(obj) ... returns envelope for all wires formed by the base edges.''' PathLog.track() - self.tmpGrp = FreeCAD.ActiveDocument.addObject('App::DocumentObjectGroup', 'tmpDebugGrp') - tmpGrpNm = self.tmpGrp.Name + inaccessible = translate('PathProfileEdges', 'The selected edge(s) are inaccessible. If multiple, re-ordering selection might work.') + if PathLog.getLevel(PathLog.thisModule()) == 4: + self.tmpGrp = FreeCAD.ActiveDocument.addObject('App::DocumentObjectGroup', 'tmpDebugGrp') + tmpGrpNm = self.tmpGrp.Name self.JOB = PathUtils.findParentJob(obj) self.offsetExtra = abs(obj.OffsetExtra.Value) @@ -101,7 +106,7 @@ class ObjectProfile(PathProfileBase.ObjectProfile): # f = Part.makeFace(wire, 'Part::FaceMakerSimple') # if planar error, Comment out previous line, uncomment the next two (origWire, flatWire) = self._flattenWire(obj, wire, obj.FinalDepth.Value) - f = origWire.Shape.Wires[0] + f = origWire.Wires[0] if f is not False: # shift the compound to the bottom of the base object for proper sectioning zShift = zMin - f.BoundBox.ZMin @@ -110,7 +115,7 @@ class ObjectProfile(PathProfileBase.ObjectProfile): env = PathUtils.getEnvelope(base.Shape, subshape=f, depthparams=self.depthparams) shapes.append((env, False)) else: - PathLog.error(translate('PathProfileEdges', 'The selected edge(s) are inaccessible.')) + PathLog.error(inaccessible) else: if self.JOB.GeometryTolerance.Value == 0.0: msg = self.JOB.Label + '.GeometryTolerance = 0.0.' @@ -118,76 +123,64 @@ class ObjectProfile(PathProfileBase.ObjectProfile): PathLog.error(msg) else: cutWireObjs = False - (origWire, flatWire) = self._flattenWire(obj, wire, obj.FinalDepth.Value) - cutShp = self._getCutAreaCrossSection(obj, base, origWire, flatWire) - if cutShp is not False: - cutWireObjs = self._extractPathWire(obj, base, flatWire, cutShp) + flattened = self._flattenWire(obj, wire, obj.FinalDepth.Value) + if flattened: + (origWire, flatWire) = flattened + if PathLog.getLevel(PathLog.thisModule()) == 4: + os = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpFlatWire') + os.Shape = flatWire + os.purgeTouched() + self.tmpGrp.addObject(os) + cutShp = self._getCutAreaCrossSection(obj, base, origWire, flatWire) + if cutShp is not False: + cutWireObjs = self._extractPathWire(obj, base, flatWire, cutShp) - if cutWireObjs is not False: - for cW in cutWireObjs: - shapes.append((cW, False)) - self.profileEdgesIsOpen = True + if cutWireObjs is not False: + for cW in cutWireObjs: + shapes.append((cW, False)) + self.profileEdgesIsOpen = True + else: + PathLog.error(inaccessible) else: - PathLog.error(translate('PathProfileEdges', 'The selected edge(s) are inaccessible.')) + PathLog.error(inaccessible) # Delete the temporary objects - if PathLog.getLevel(PathLog.thisModule()) != 4: - for to in self.tmpGrp.Group: - FreeCAD.ActiveDocument.removeObject(to.Name) - FreeCAD.ActiveDocument.removeObject(tmpGrpNm) - else: + if PathLog.getLevel(PathLog.thisModule()) == 4: if FreeCAD.GuiUp: import FreeCADGui FreeCADGui.ActiveDocument.getObject(tmpGrpNm).Visibility = False - + self.tmpGrp.purgeTouched() + return shapes def _flattenWire(self, obj, wire, trgtDep): '''_flattenWire(obj, wire)... Return a flattened version of the wire''' PathLog.debug('_flattenWire()') wBB = wire.BoundBox - tmpGrp = self.tmpGrp - - OW = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpOriginalWire') - OW.Shape = wire - OW.purgeTouched() - tmpGrp.addObject(OW) if wBB.ZLength > 0.0: PathLog.debug('Wire is not horizontally co-planar. Flattening it.') # Extrude non-horizontal wire extFwdLen = wBB.ZLength * 2.2 - mbbEXT = self._extrudeObject(OW, extFwdLen, False) + mbbEXT = wire.extrude(FreeCAD.Vector(0, 0, extFwdLen)) # Create cross-section of shape and translate sliceZ = wire.BoundBox.ZMin + (extFwdLen / 2) - crsectFaceShp = self._makeCrossSection(mbbEXT.Shape, sliceZ, trgtDep) + crsectFaceShp = self._makeCrossSection(mbbEXT, sliceZ, trgtDep) if crsectFaceShp is not False: - # srtWire = Part.Wire(Part.__sortEdges__(crsectFaceShp.Edges)) - FW = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpFlattenedWire') - FW.Shape = crsectFaceShp # srtWire - FW.recompute() - FW.purgeTouched() - tmpGrp.addObject(FW) - - return (OW, FW) + return (wire, crsectFaceShp) else: return False else: srtWire = Part.Wire(Part.__sortEdges__(wire.Edges)) srtWire.translate(FreeCAD.Vector(0, 0, trgtDep - srtWire.BoundBox.ZMin)) - FW = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpOriginalWireSorted') - FW.Shape = srtWire - FW.purgeTouched() - tmpGrp.addObject(FW) - return (OW, FW) + return (wire, srtWire) # Open-edges methods - def _getCutAreaCrossSection(self, obj, base, origWire, flatWireObj): + def _getCutAreaCrossSection(self, obj, base, origWire, flatWire): PathLog.debug('_getCutAreaCrossSection()') - tmpGrp = self.tmpGrp FCAD = FreeCAD.ActiveDocument tolerance = self.JOB.GeometryTolerance.Value toolDiam = 2 * self.radius # self.radius defined in PathAreaOp or PathProfileBase modules @@ -195,16 +188,16 @@ class ObjectProfile(PathProfileBase.ObjectProfile): bbBfr = (self.ofstRadius * 2) * 1.25 if bbBfr < minBfr: bbBfr = minBfr - fwBB = flatWireObj.Shape.BoundBox - wBB = origWire.Shape.BoundBox + fwBB = flatWire.BoundBox + wBB = origWire.BoundBox minArea = (self.ofstRadius - tolerance)**2 * math.pi - useWire = origWire.Shape.Wires[0] + useWire = origWire.Wires[0] numOrigEdges = len(useWire.Edges) sdv = wBB.ZMax fdv = obj.FinalDepth.Value extLenFwd = sdv - fdv - WIRE = flatWireObj.Shape.Wires[0] + WIRE = flatWire.Wires[0] numEdges = len(WIRE.Edges) # Identify first/last edges and first/last vertex on wire @@ -239,15 +232,24 @@ class ObjectProfile(PathProfileBase.ObjectProfile): # Create intersection tags for determining which side of wire to cut (begInt, begExt, iTAG, eTAG) = self._makeIntersectionTags(useWire, numOrigEdges, fdv) + if not begInt or not begExt: + return False self.iTAG = iTAG self.eTAG = eTAG # Create extended wire boundbox, and extrude extBndbox = self._makeExtendedBoundBox(wBB, bbBfr, fdv) - extBndboxEXT = self._extrudeObject(extBndbox, extLenFwd) # (objToExt, extFwdLen) + extBndboxEXT = extBndbox.extrude(FreeCAD.Vector(0, 0, extLenFwd)) # Cut model(selected edges) from extended edges boundbox - cutArea = extBndboxEXT.Shape.cut(base.Shape) + cutArea = extBndboxEXT.cut(base.Shape) + if PathLog.getLevel(PathLog.thisModule()) == 4: + CA = FCAD.addObject('Part::Feature', 'tmpCutArea') + CA.Shape = cutArea + CA.recompute() + CA.purgeTouched() + self.tmpGrp.addObject(CA) + # Get top and bottom faces of cut area (CA), and combine faces when necessary topFc = list() @@ -267,8 +269,8 @@ class ObjectProfile(PathProfileBase.ObjectProfile): topComp.translate(FreeCAD.Vector(0, 0, fdv - topComp.BoundBox.ZMin)) # Translate face to final depth if len(botFc) > 1: PathLog.debug('len(botFc) > 1') - bndboxFace = Part.Face(extBndbox.Shape.Wires[0]) - tmpFace = Part.Face(extBndbox.Shape.Wires[0]) + bndboxFace = Part.Face(extBndbox.Wires[0]) + tmpFace = Part.Face(extBndbox.Wires[0]) for f in botFc: Q = tmpFace.cut(cutArea.Faces[f]) tmpFace = Q @@ -277,38 +279,20 @@ class ObjectProfile(PathProfileBase.ObjectProfile): botComp = Part.makeCompound([cutArea.Faces[f] for f in botFc]) # Part.makeCompound([CA.Shape.Faces[f] for f in botFc]) botComp.translate(FreeCAD.Vector(0, 0, fdv - botComp.BoundBox.ZMin)) # Translate face to final depth - # Convert compound shapes to FC objects for use in multicommon operation - TP = FCAD.addObject('Part::Feature', 'tmpTopCompound') - TP.Shape = topComp - TP.recompute() - TP.purgeTouched() - tmpGrp.addObject(TP) - BT = FCAD.addObject('Part::Feature', 'tmpBotCompound') - BT.Shape = botComp - BT.recompute() - BT.purgeTouched() - tmpGrp.addObject(BT) - # Make common of the two - comFC = FCAD.addObject('Part::MultiCommon', 'tmpCommonTopBotFaces') - comFC.Shapes = [TP, BT] - comFC.recompute() - TP.purgeTouched() - BT.purgeTouched() - comFC.purgeTouched() - tmpGrp.addObject(comFC) + comFC = topComp.common(botComp) # Determine with which set of intersection tags the model intersects (cmnIntArea, cmnExtArea) = self._checkTagIntersection(iTAG, eTAG, 'QRY', comFC) if cmnExtArea > cmnIntArea: PathLog.debug('Cutting on Ext side.') self.cutSide = 'E' - self.cutSideTags = eTAG.Shape + self.cutSideTags = eTAG tagCOM = begExt.CenterOfMass else: PathLog.debug('Cutting on Int side.') self.cutSide = 'I' - self.cutSideTags = iTAG.Shape + self.cutSideTags = iTAG tagCOM = begInt.CenterOfMass # Make two beginning style(oriented) 'L' shape stops @@ -328,9 +312,9 @@ class ObjectProfile(PathProfileBase.ObjectProfile): pathStops.translate(FreeCAD.Vector(0, 0, fdv - pathStops.BoundBox.ZMin)) # Identify closed wire in cross-section that corresponds to user-selected edge(s) - workShp = comFC.Shape + workShp = comFC fcShp = workShp - wire = origWire.Shape # flatWireObj.Shape + wire = origWire WS = workShp.Wires lenWS = len(WS) if lenWS < 3: @@ -351,7 +335,6 @@ class ObjectProfile(PathProfileBase.ObjectProfile): if wi is None: PathLog.error('The cut area cross-section wire does not coincide with selected edge. Wires[] index is None.') - tmpGrp.purgeTouched() return False else: PathLog.debug('Cross-section Wires[] index is {}.'.format(wi)) @@ -365,13 +348,8 @@ class ObjectProfile(PathProfileBase.ObjectProfile): if wi > 0: # and isInterior is False: PathLog.debug('Multiple wires in cut area. First choice is not 0. Testing.') testArea = fcShp.cut(base.Shape) - # testArea = fcShp - TA = FreeCAD.ActiveDocument.addObject('Part::Feature','tmpCutFaceTest') - TA.Shape = testArea - TA.purgeTouched() - tmpGrp.addObject(TA) - isReady = self._checkTagIntersection(iTAG, eTAG, self.cutSide, TA) + isReady = self._checkTagIntersection(iTAG, eTAG, self.cutSide, testArea) PathLog.debug('isReady {}.'.format(isReady)) if isReady is False: @@ -392,38 +370,18 @@ class ObjectProfile(PathProfileBase.ObjectProfile): # Add path stops at ends of wire cutShp = workShp.cut(pathStops) - - CF = FreeCAD.ActiveDocument.addObject('Part::Feature','tmpCutFace') - CF.Shape = cutShp - CF.recompute() - CF.purgeTouched() - tmpGrp.addObject(CF) - - tmpGrp.purgeTouched() - return cutShp # CF.Shape + return cutShp def _checkTagIntersection(self, iTAG, eTAG, cutSide, tstObj): # Identify intersection of Common area and Interior Tags - intCmn = FreeCAD.ActiveDocument.addObject('Part::MultiCommon', 'tmpCmnIntTags') - intCmn.Shapes = [tstObj, iTAG] - intCmn.recompute() - tstObj.purgeTouched() - iTAG.purgeTouched() - intCmn.purgeTouched() - self.tmpGrp.addObject(intCmn) + intCmn = tstObj.common(iTAG) # Identify intersection of Common area and Exterior Tags - extCmn = FreeCAD.ActiveDocument.addObject('Part::MultiCommon', 'tmpCmnExtTags') - extCmn.Shapes = [tstObj, eTAG] - extCmn.recompute() - tstObj.purgeTouched() - eTAG.purgeTouched() - extCmn.purgeTouched() - self.tmpGrp.addObject(extCmn) + extCmn = tstObj.common(eTAG) # Calculate common intersection (solid model side, or the non-cut side) area with tags, to determine physical cut side - cmnIntArea = intCmn.Shape.Area - cmnExtArea = extCmn.Shape.Area + cmnIntArea = intCmn.Area + cmnExtArea = extCmn.Area if cutSide == 'QRY': return (cmnIntArea, cmnExtArea) @@ -437,16 +395,15 @@ class ObjectProfile(PathProfileBase.ObjectProfile): return True return False - def _extractPathWire(self, obj, base, fWire, cutShp): + def _extractPathWire(self, obj, base, flatWire, cutShp): PathLog.debug('_extractPathWire()') subLoops = list() rtnWIRES = list() osWrIdxs = list() subDistFactor = 1.0 # Raise to include sub wires at greater distance from original - tmpGrp = self.tmpGrp fdv = obj.FinalDepth.Value - wire = fWire.Shape + wire = flatWire lstVrtIdx = len(wire.Vertexes) - 1 lstVrt = wire.Vertexes[lstVrtIdx] frstVrt = wire.Vertexes[0] @@ -465,14 +422,14 @@ class ObjectProfile(PathProfileBase.ObjectProfile): osArea = ofstShp.Area except Exception as ee: PathLog.error('No area to offset shape returned.') - tmpGrp.purgeTouched() return False - os = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpOffsetShape') - os.Shape = ofstShp - os.recompute() - os.purgeTouched() - tmpGrp.addObject(os) + if PathLog.getLevel(PathLog.thisModule()) == 4: + os = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpOffsetShape') + os.Shape = ofstShp + os.recompute() + os.purgeTouched() + self.tmpGrp.addObject(os) numOSWires = len(ofstShp.Wires) for w in range(0, numOSWires): @@ -488,10 +445,12 @@ class ObjectProfile(PathProfileBase.ObjectProfile): min0 = N[4] min0i = n (w0, vi0, pnt0, vrt0, d0) = NEAR0[0] # min0i - near0 = Draft.makeWire([cent0, pnt0], placement=pl, closed=False, face=False, support=None) - near0.recompute() - near0.purgeTouched() - tmpGrp.addObject(near0) + if PathLog.getLevel(PathLog.thisModule()) == 4: + near0 = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpNear0') + near0.Shape = Part.makeLine(cent0, pnt0) + near0.recompute() + near0.purgeTouched() + self.tmpGrp.addObject(near0) NEAR1 = self._findNearestVertex(ofstShp, cent1) min1i = 0 @@ -502,24 +461,23 @@ class ObjectProfile(PathProfileBase.ObjectProfile): min1 = N[4] min1i = n (w1, vi1, pnt1, vrt1, d1) = NEAR1[0] # min1i - near1 = Draft.makeWire([cent1, pnt1], placement=pl, closed=False, face=False, support=None) - near1.recompute() - near1.purgeTouched() - tmpGrp.addObject(near1) + if PathLog.getLevel(PathLog.thisModule()) == 4: + near1 = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpNear1') + near1.Shape = Part.makeLine(cent1, pnt1) + near1.recompute() + near1.purgeTouched() + self.tmpGrp.addObject(near1) if w0 != w1: - PathLog.debug('w0 is {}.'.format(w0)) - PathLog.debug('w1 is {}.'.format(w1)) - PathLog.warning('Offset wire endpoint indexes are not equal: {}, {}'.format(w0, w1)) + PathLog.warning('Offset wire endpoint indexes are not equal - w0, w1: {}, {}'.format(w0, w1)) - ''' - PathLog.debug('min0i is {}.'.format(min0i)) - PathLog.debug('min1i is {}.'.format(min1i)) - PathLog.debug('NEAR0[{}] is {}.'.format(w0, NEAR0[w0])) - PathLog.debug('NEAR1[{}] is {}.'.format(w1, NEAR1[w1])) - PathLog.debug('NEAR0 is {}.'.format(NEAR0)) - PathLog.debug('NEAR1 is {}.'.format(NEAR1)) - ''' + if PathLog.getLevel(PathLog.thisModule()) == 4: + PathLog.debug('min0i is {}.'.format(min0i)) + PathLog.debug('min1i is {}.'.format(min1i)) + PathLog.debug('NEAR0[{}] is {}.'.format(w0, NEAR0[w0])) + PathLog.debug('NEAR1[{}] is {}.'.format(w1, NEAR1[w1])) + PathLog.debug('NEAR0 is {}.'.format(NEAR0)) + PathLog.debug('NEAR1 is {}.'.format(NEAR1)) mainWire = ofstShp.Wires[w0] @@ -550,7 +508,6 @@ class ObjectProfile(PathProfileBase.ObjectProfile): # Eif # Break offset loop into two wires - one of which is the desired profile path wire. - # (edgeIdxs0, edgeIdxs1) = self._separateWireAtVertexes(mainWire, ofstShp.Vertexes[vi0], ofstShp.Vertexes[vi1]) (edgeIdxs0, edgeIdxs1) = self._separateWireAtVertexes(mainWire, mainWire.Vertexes[vi0], mainWire.Vertexes[vi1]) edgs0 = list() edgs1 = list() @@ -570,7 +527,6 @@ class ObjectProfile(PathProfileBase.ObjectProfile): rtnWIRES.append(part1) rtnWIRES.extend(subLoops) - tmpGrp.purgeTouched() return rtnWIRES def _extractFaceOffset(self, obj, fcShape, isHole): @@ -605,13 +561,7 @@ class ObjectProfile(PathProfileBase.ObjectProfile): area.add(fcShape) # obj.Shape to use for extracting offset area.setParams(**areaParams) # set parameters - # Save parameters for debugging - # obj.AreaParams = str(area.getParams()) - # PathLog.debug("Area with params: {}".format(area.getParams())) - - offsetShape = area.getShape() - - return offsetShape + return area.getShape() def _findNearestVertex(self, shape, point): PathLog.debug('_findNearestVertex()') @@ -792,29 +742,17 @@ class ObjectProfile(PathProfileBase.ObjectProfile): return False def _makeExtendedBoundBox(self, wBB, bbBfr, zDep): - pl = FreeCAD.Placement() - pl.Rotation = FreeCAD.Rotation(FreeCAD.Vector(0, 0, 1), 0) - pl.Base = FreeCAD.Vector(0, 0, 0) - p1 = FreeCAD.Vector(wBB.XMin - bbBfr, wBB.YMin - bbBfr, zDep) p2 = FreeCAD.Vector(wBB.XMax + bbBfr, wBB.YMin - bbBfr, zDep) p3 = FreeCAD.Vector(wBB.XMax + bbBfr, wBB.YMax + bbBfr, zDep) p4 = FreeCAD.Vector(wBB.XMin - bbBfr, wBB.YMax + bbBfr, zDep) - bb = Draft.makeWire([p1, p2, p3, p4], placement=pl, closed=True, face=False, support=None) - bb.Label = 'ProfileEdges_BoundBox' - bb.recompute() - bb.purgeTouched() - self.tmpGrp.addObject(bb) - return bb + L1 = Part.makeLine(p1, p2) + L2 = Part.makeLine(p2, p3) + L3 = Part.makeLine(p3, p4) + L4 = Part.makeLine(p4, p1) - def _makeSimpleCircle(self, rad, plcmnt, isFace=False, label='SimpleCircle'): - C = Draft.makeCircle(rad, placement=plcmnt, face=isFace) - C.Label = 'tmp' + label - C.recompute() - C.purgeTouched() - self.tmpGrp.addObject(C) - return C + return Part.Face(Part.Wire([L1, L2, L3, L4])) def _makeIntersectionTags(self, useWire, numOrigEdges, fdv): # Create circular probe tags around perimiter of wire @@ -822,8 +760,8 @@ class ObjectProfile(PathProfileBase.ObjectProfile): intTags = list() tagRad = (self.radius / 2) tagCnt = 0 - begInt = None - begExt = None + begInt = False + begExt = False for e in range(0, numOrigEdges): E = useWire.Edges[e] LE = E.Length @@ -843,31 +781,22 @@ class ObjectProfile(PathProfileBase.ObjectProfile): cp1 = E.valueAt(E.getParameterByLength(0)) cp2 = E.valueAt(E.getParameterByLength(aspc)) (intTObj, extTObj) = self._makeOffsetCircleTag(cp1, cp2, tagRad, fdv, 'BeginEdge[{}]_'.format(e)) - if intTObj is not False: - begInt = intTObj.Shape - begExt = extTObj.Shape + if intTObj and extTObj: + begInt = intTObj + begExt = extTObj else: d = i * mid cp1 = E.valueAt(E.getParameterByLength(d - spc)) cp2 = E.valueAt(E.getParameterByLength(d + spc)) (intTObj, extTObj) = self._makeOffsetCircleTag(cp1, cp2, tagRad, fdv, 'Edge[{}]_'.format(e)) - if intTObj is not False: + if intTObj and extTObj: tagCnt += nt intTags.append(intTObj) extTags.append(extTObj) tagArea = math.pi * tagRad**2 * tagCnt - # FreeCAD object required for Part::MultiCommon usage - intTagsComp = Part.makeCompound([T.Shape for T in intTags]) - iTAG = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpInteriorTags') - iTAG.Shape = intTagsComp - iTAG.purgeTouched() - self.tmpGrp.addObject(iTAG) - # FreeCAD object required for Part::MultiCommon usage - extTagsComp = Part.makeCompound([T.Shape for T in extTags]) - eTAG = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpExteriorTags') - eTAG.Shape = extTagsComp - eTAG.purgeTouched() - self.tmpGrp.addObject(eTAG) + iTAG = Part.makeCompound(intTags) + eTAG = Part.makeCompound(extTags) + return (begInt, begExt, iTAG, eTAG) def _makeOffsetCircleTag(self, p1, p2, cutterRad, depth, lbl, reverse=False): @@ -887,32 +816,19 @@ class ObjectProfile(PathProfileBase.ObjectProfile): pl = FreeCAD.Placement() pl.Rotation = FreeCAD.Rotation(FreeCAD.Vector(0, 0, 1), 0) # make exterior tag - adjlbl = lbl + 'Ext' - pl.Base = extPnt.add(FreeCAD.Vector(0, 0, depth)) - extTag = self._makeSimpleCircle((cutterRad / 2), pl, True, adjlbl) + eCntr = extPnt.add(FreeCAD.Vector(0, 0, depth)) + ecw = Part.Wire(Part.makeCircle((cutterRad / 2), eCntr).Edges[0]) + extTag = Part.Face(ecw) # make interior tag - adjlbl = lbl + 'Int' perpI = FreeCAD.Vector(-1 * toMid.y, toMid.x, 0.0).multiply(cutFactor) # interior tag intPnt = pb.add(toMid.add(perpI)) - pl.Base = intPnt.add(FreeCAD.Vector(0, 0, depth)) - intTag = self._makeSimpleCircle((cutterRad / 2), pl, True, adjlbl) + iCntr = intPnt.add(FreeCAD.Vector(0, 0, depth)) + icw = Part.Wire(Part.makeCircle((cutterRad / 2), iCntr).Edges[0]) + intTag = Part.Face(icw) return (intTag, extTag) - def _extrudeObject(self, objToExt, extFwdLen, solid=True): - # Extrude non-horizontal wire - E = FreeCAD.ActiveDocument.addObject('Part::Extrusion', 'tmpExtrusion') - E.Base = objToExt - E.DirMode = 'Custom' - E.Dir = FreeCAD.Vector(0, 0, 1) - E.LengthFwd = extFwdLen - E.Solid = solid - E.recompute() - E.purgeTouched() - self.tmpGrp.addObject(E) - return E - def _makeStop(self, sType, pA, pB, lbl): rad = self.radius ofstRad = self.ofstRadius @@ -950,7 +866,13 @@ class ObjectProfile(PathProfileBase.ObjectProfile): p5 = self._makePerp2DVector(p3, p4, -1 * (1 + extra)) # E4 p6 = self._makePerp2DVector(p4, p5, -1 * (ofstRad + extra)) # E5 p7 = E # E6 - S = Draft.makeWire([p1, p2, p3, p4, p5, p6, p7], placement=pl, closed=True, face=True) + L1 = Part.makeLine(p1, p2) + L2 = Part.makeLine(p2, p3) + L3 = Part.makeLine(p3, p4) + L4 = Part.makeLine(p4, p5) + L5 = Part.makeLine(p5, p6) + L6 = Part.makeLine(p6, p7) + wire = Part.Wire([L1, L2, L3, L4, L5, L6]) else: # 'L' stop shape and edge legend # : @@ -970,13 +892,22 @@ class ObjectProfile(PathProfileBase.ObjectProfile): p4 = self._makePerp2DVector(p2, p3, -1 * (0.5 + abs(self.offsetExtra))) # FIRST POINT p5 = self._makePerp2DVector(p3, p4, -1 * (0.25 + abs(self.offsetExtra))) # E1 SECOND p6 = p1 # E4 - S = Draft.makeWire([p1, p2, p3, p4, p5, p6], placement=pl, closed=True, face=True) + L1 = Part.makeLine(p1, p2) + L2 = Part.makeLine(p2, p3) + L3 = Part.makeLine(p3, p4) + L4 = Part.makeLine(p4, p5) + L5 = Part.makeLine(p5, p6) + wire = Part.Wire([L1, L2, L3, L4, L5]) # Eif - S.Label = 'tmp' + lbl - S.recompute() - S.purgeTouched() - self.tmpGrp.addObject(S) - return S.Shape + face = Part.Face(wire) + if PathLog.getLevel(PathLog.thisModule()) == 4: + os = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmp' + lbl) + os.Shape = face + os.recompute() + os.purgeTouched() + self.tmpGrp.addObject(os) + + return face def _makePerp2DVector(self, v1, v2, dist): p1 = FreeCAD.Vector(v1.x, v1.y, 0.0) diff --git a/src/Mod/Path/PathScripts/PathProfileFaces.py b/src/Mod/Path/PathScripts/PathProfileFaces.py index 6f2233b215..281d848699 100644 --- a/src/Mod/Path/PathScripts/PathProfileFaces.py +++ b/src/Mod/Path/PathScripts/PathProfileFaces.py @@ -23,9 +23,7 @@ # * * # *************************************************************************** -import ArchPanel import FreeCAD -import Part import Path import PathScripts.PathLog as PathLog import PathScripts.PathOp as PathOp @@ -35,6 +33,11 @@ import numpy from PySide import QtCore +# lazily loaded modules +from lazy_loader.lazy_loader import LazyLoader +ArchPanel = LazyLoader('ArchPanel', globals(), 'ArchPanel') +Part = LazyLoader('Part', globals(), 'Part') + __title__ = "Path Profile Faces Operation" __author__ = "sliptonic (Brad Collette), Schildkroet" __url__ = "http://www.freecadweb.org" diff --git a/src/Mod/Path/PathScripts/PathSimulatorGui.py b/src/Mod/Path/PathScripts/PathSimulatorGui.py index 50014e0272..428dcc394c 100644 --- a/src/Mod/Path/PathScripts/PathSimulatorGui.py +++ b/src/Mod/Path/PathScripts/PathSimulatorGui.py @@ -1,6 +1,4 @@ import FreeCAD -import Mesh -import Part import Path import PathScripts.PathDressup as PathDressup import PathScripts.PathGeom as PathGeom @@ -13,6 +11,11 @@ from FreeCAD import Vector, Base _filePath = os.path.dirname(os.path.abspath(__file__)) +# lazily loaded modules +from lazy_loader.lazy_loader import LazyLoader +Mesh = LazyLoader('Mesh', globals(), 'Mesh') +Part = LazyLoader('Part', globals(), 'Part') + if FreeCAD.GuiUp: import FreeCADGui from PySide import QtGui, QtCore diff --git a/src/Mod/Path/PathScripts/PathStock.py b/src/Mod/Path/PathScripts/PathStock.py index dfb15658a0..e1ab825627 100644 --- a/src/Mod/Path/PathScripts/PathStock.py +++ b/src/Mod/Path/PathScripts/PathStock.py @@ -23,13 +23,16 @@ '''used to create material stock around a machined part- for visualization ''' import FreeCAD -import Part import PathScripts.PathIconViewProvider as PathIconViewProvider import PathScripts.PathLog as PathLog import math from PySide import QtCore +# lazily loaded modules +from lazy_loader.lazy_loader import LazyLoader +Part = LazyLoader('Part', globals(), 'Part') + PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule()) #PathLog.trackModule(PathLog.thisModule()) diff --git a/src/Mod/Path/PathScripts/PathSurface.py b/src/Mod/Path/PathScripts/PathSurface.py index 9e63b6f359..9c6f2d3c0c 100644 --- a/src/Mod/Path/PathScripts/PathSurface.py +++ b/src/Mod/Path/PathScripts/PathSurface.py @@ -1,3741 +1,2189 @@ -# -*- coding: utf-8 -*- - -# *************************************************************************** -# * * -# * Copyright (c) 2016 sliptonic * -# * * -# * This program is free software; you can redistribute it and/or modify * -# * it under the terms of the GNU Lesser General Public License (LGPL) * -# * as published by the Free Software Foundation; either version 2 of * -# * the License, or (at your option) any later version. * -# * for detail see the LICENCE text file. * -# * * -# * This program is distributed in the hope that it will be useful, * -# * but WITHOUT ANY WARRANTY; without even the implied warranty of * -# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * -# * GNU Library General Public License for more details. * -# * * -# * You should have received a copy of the GNU Library General Public * -# * License along with this program; if not, write to the Free Software * -# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * -# * USA * -# * * -# *************************************************************************** -# * * -# * Additional modifications and contributions beginning 2019 * -# * by Russell Johnson 2020-03-18 12:29 CST * -# * * -# *************************************************************************** - -from __future__ import print_function - -import FreeCAD -import MeshPart -import Path -import PathScripts.PathLog as PathLog -import PathScripts.PathUtils as PathUtils -import PathScripts.PathOp as PathOp - -from PySide import QtCore -import time -import math -import Part -import Draft - -if FreeCAD.GuiUp: - import FreeCADGui - -__title__ = "Path Surface Operation" -__author__ = "sliptonic (Brad Collette)" -__url__ = "http://www.freecadweb.org" -__doc__ = "Class and implementation of Mill Facing operation." - -PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule()) -# PathLog.trackModule(PathLog.thisModule()) - - -# Qt translation handling -def translate(context, text, disambig=None): - return QtCore.QCoreApplication.translate(context, text, disambig) - - -# OCL must be installed -try: - import ocl -except ImportError: - FreeCAD.Console.PrintError( - translate("Path_Surface", "This operation requires OpenCamLib to be installed.") + "\n") - import sys - sys.exit(translate("Path_Surface", "This operation requires OpenCamLib to be installed.")) - - -class ObjectSurface(PathOp.ObjectOp): - '''Proxy object for Surfacing operation.''' - - def baseObject(self): - '''baseObject() ... returns super of receiver - Used to call base implementation in overwritten functions.''' - return super(self.__class__, self) - - def opFeatures(self, obj): - '''opFeatures(obj) ... return all standard features and edges based geomtries''' - return PathOp.FeatureTool | PathOp.FeatureDepths | PathOp.FeatureHeights | PathOp.FeatureStepDown | PathOp.FeatureCoolant | PathOp.FeatureBaseFaces - - def initOperation(self, obj): - '''initPocketOp(obj) ... create operation specific properties''' - self.initOpProperties(obj) - - # For debugging - if PathLog.getLevel(PathLog.thisModule()) != 4: - obj.setEditorMode('ShowTempObjects', 2) # hide - - if not hasattr(obj, 'DoNotSetDefaultValues'): - self.setEditorProperties(obj) - - def initOpProperties(self, obj): - '''initOpProperties(obj) ... create operation specific properties''' - - PROPS = [ - ("App::PropertyBool", "ShowTempObjects", "Debug", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Show the temporary path construction objects when module is in DEBUG mode.")), - - ("App::PropertyDistance", "AngularDeflection", "Mesh Conversion", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Smaller values yield a finer, more accurate mesh. Smaller values increase processing time a lot.")), - ("App::PropertyDistance", "LinearDeflection", "Mesh Conversion", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Smaller values yield a finer, more accurate mesh. Smaller values do not increase processing time much.")), - - ("App::PropertyFloat", "CutterTilt", "Rotational", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Stop index(angle) for rotational scan")), - ("App::PropertyEnumeration", "DropCutterDir", "Rotational", - QtCore.QT_TRANSLATE_NOOP("App::Property", "The direction along which dropcutter lines are created")), - ("App::PropertyVectorDistance", "DropCutterExtraOffset", "Rotational", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Additional offset to the selected bounding box")), - ("App::PropertyEnumeration", "RotationAxis", "Rotational", - QtCore.QT_TRANSLATE_NOOP("App::Property", "The model will be rotated around this axis.")), - ("App::PropertyFloat", "StartIndex", "Rotational", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Start index(angle) for rotational scan")), - ("App::PropertyFloat", "StopIndex", "Rotational", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Stop index(angle) for rotational scan")), - - ("App::PropertyEnumeration", "ScanType", "Surface", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Planar: Flat, 3D surface scan. Rotational: 4th-axis rotational scan.")), - - ("App::PropertyInteger", "AvoidLastX_Faces", "Selected Geometry Settings", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Avoid cutting the last 'N' faces in the Base Geometry list of selected faces.")), - ("App::PropertyBool", "AvoidLastX_InternalFeatures", "Selected Geometry Settings", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Do not cut internal features on avoided faces.")), - ("App::PropertyDistance", "BoundaryAdjustment", "Selected Geometry Settings", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Positive values push the cutter toward, or beyond, the boundary. Negative values retract the cutter away from the boundary.")), - ("App::PropertyBool", "BoundaryEnforcement", "Selected Geometry Settings", - QtCore.QT_TRANSLATE_NOOP("App::Property", "If true, the cutter will remain inside the boundaries of the model or selected face(s).")), - ("App::PropertyEnumeration", "HandleMultipleFeatures", "Selected Geometry Settings", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Choose how to process multiple Base Geometry features.")), - ("App::PropertyDistance", "InternalFeaturesAdjustment", "Selected Geometry Settings", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Positive values push the cutter toward, or into, the feature. Negative values retract the cutter away from the feature.")), - ("App::PropertyBool", "InternalFeaturesCut", "Selected Geometry Settings", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Ignore internal feature areas within a larger selected face.")), - - ("App::PropertyEnumeration", "BoundBox", "Clearing Options", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Select the overall boundary for the operation. ")), - ("App::PropertyVectorDistance", "CircularCenterCustom", "Clearing Options", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the start point for circular cut patterns.")), - ("App::PropertyEnumeration", "CircularCenterAt", "Clearing Options", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Choose location of the center point for starting the circular pattern.")), - ("App::PropertyEnumeration", "CutMode", "Clearing Options", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the direction for the cutting tool to engage the material: Climb (ClockWise) or Conventional (CounterClockWise)")), - ("App::PropertyEnumeration", "CutPattern", "Clearing Options", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the geometric clearing pattern to use for the operation.")), - ("App::PropertyFloat", "CutPatternAngle", "Clearing Options", - QtCore.QT_TRANSLATE_NOOP("App::Property", "The yaw angle used for certain clearing patterns")), - ("App::PropertyBool", "CutPatternReversed", "Clearing Options", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Reverse the cut order of the stepover paths. For circular cut patterns, begin at the outside and work toward the center.")), - ("App::PropertyDistance", "DepthOffset", "Clearing Options", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the Z-axis depth offset from the target surface.")), - ("App::PropertyEnumeration", "LayerMode", "Clearing Options", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Complete the operation in a single pass at depth, or mulitiple passes to final depth.")), - ("App::PropertyEnumeration", "ProfileEdges", "Clearing Options", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Profile the edges of the selection.")), - ("App::PropertyDistance", "SampleInterval", "Clearing Options", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the sampling resolution. Smaller values quickly increase processing time.")), - ("App::PropertyPercent", "StepOver", "Clearing Options", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the stepover percentage, based on the tool's diameter.")), - - ("App::PropertyBool", "OptimizeLinearPaths", "Optimization", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Enable optimization of linear paths (co-linear points). Removes unnecessary co-linear points from G-Code output.")), - ("App::PropertyBool", "OptimizeStepOverTransitions", "Optimization", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Enable separate optimization of transitions between, and breaks within, each step over path.")), - ("App::PropertyBool", "CircularUseG2G3", "Optimization", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Convert co-planar arcs to G2/G3 gcode commands for `Circular` and `CircularZigZag` cut patterns.")), - ("App::PropertyDistance", "GapThreshold", "Optimization", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Collinear and co-radial artifact gaps that are smaller than this threshold are closed in the path.")), - ("App::PropertyString", "GapSizes", "Optimization", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Feedback: three smallest gaps identified in the path geometry.")), - - ("App::PropertyVectorDistance", "StartPoint", "Start Point", - QtCore.QT_TRANSLATE_NOOP("App::Property", "The custom start point for the path of this operation")), - ("App::PropertyBool", "UseStartPoint", "Start Point", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Make True, if specifying a Start Point")) - ] - - missing = list() - for (prtyp, nm, grp, tt) in PROPS: - if not hasattr(obj, nm): - obj.addProperty(prtyp, nm, grp, tt) - missing.append(nm) - - # Set enumeration lists for enumeration properties - if len(missing) > 0: - ENUMS = self._propertyEnumerations() - for n in ENUMS: - if n in missing: - cmdStr = 'obj.{}={}'.format(n, ENUMS[n]) - exec(cmdStr) - - self.addedAllProperties = True - - def _propertyEnumerations(self): - # Enumeration lists for App::PropertyEnumeration properties - return { - 'BoundBox': ['BaseBoundBox', 'Stock'], - 'CircularCenterAt': ['CenterOfMass', 'CenterOfBoundBox', 'XminYmin', 'Custom'], - 'CutMode': ['Conventional', 'Climb'], - 'CutPattern': ['Line', 'Circular', 'CircularZigZag', 'ZigZag'], # Additional goals ['Offset', 'Spiral', 'ZigZagOffset', 'Grid', 'Triangle'] - 'DropCutterDir': ['X', 'Y'], - 'HandleMultipleFeatures': ['Collectively', 'Individually'], - 'LayerMode': ['Single-pass', 'Multi-pass'], - 'ProfileEdges': ['None', 'Only', 'First', 'Last'], - 'RotationAxis': ['X', 'Y'], - 'ScanType': ['Planar', 'Rotational'] - } - - def setEditorProperties(self, obj): - # Used to hide inputs in properties list - - mode = 2 # 2=hidden - if obj.ScanType == 'Planar': - show = 0 - hide = 2 - # if obj.CutPattern in ['Line', 'ZigZag']: - if obj.CutPattern in ['Circular', 'CircularZigZag']: - show = 2 # hide - hide = 0 # show - obj.setEditorMode('CutPatternAngle', show) - obj.setEditorMode('CircularCenterAt', hide) - obj.setEditorMode('CircularCenterCustom', hide) - elif obj.ScanType == 'Rotational': - mode = 0 # show and editable - obj.setEditorMode('DropCutterDir', mode) - obj.setEditorMode('DropCutterExtraOffset', mode) - obj.setEditorMode('RotationAxis', mode) - obj.setEditorMode('StartIndex', mode) - obj.setEditorMode('StopIndex', mode) - obj.setEditorMode('CutterTilt', mode) - - def onChanged(self, obj, prop): - if hasattr(self, 'addedAllProperties'): - if self.addedAllProperties is True: - if prop == 'ScanType': - self.setEditorProperties(obj) - if prop == 'CutPattern': - self.setEditorProperties(obj) - - def opOnDocumentRestored(self, obj): - self.initOpProperties(obj) - - if PathLog.getLevel(PathLog.thisModule()) != 4: - obj.setEditorMode('ShowTempObjects', 2) # hide - else: - obj.setEditorMode('ShowTempObjects', 0) # show - - self.setEditorProperties(obj) - - def opSetDefaultValues(self, obj, job): - '''opSetDefaultValues(obj, job) ... initialize defaults''' - job = PathUtils.findParentJob(obj) - - obj.OptimizeLinearPaths = True - obj.InternalFeaturesCut = True - obj.OptimizeStepOverTransitions = False - obj.CircularUseG2G3 = False - obj.BoundaryEnforcement = True - obj.UseStartPoint = False - obj.AvoidLastX_InternalFeatures = True - obj.CutPatternReversed = False - obj.StartPoint.x = 0.0 - obj.StartPoint.y = 0.0 - obj.StartPoint.z = obj.ClearanceHeight.Value - obj.ProfileEdges = 'None' - obj.LayerMode = 'Single-pass' - obj.ScanType = 'Planar' - obj.RotationAxis = 'X' - obj.CutMode = 'Conventional' - obj.CutPattern = 'Line' - obj.HandleMultipleFeatures = 'Collectively' # 'Individually' - obj.CircularCenterAt = 'CenterOfMass' # 'CenterOfBoundBox', 'XminYmin', 'Custom' - obj.GapSizes = 'No gaps identified.' - obj.StepOver = 100 - obj.CutPatternAngle = 0.0 - obj.CutterTilt = 0.0 - obj.StartIndex = 0.0 - obj.StopIndex = 360.0 - obj.SampleInterval.Value = 1.0 - obj.BoundaryAdjustment.Value = 0.0 - obj.InternalFeaturesAdjustment.Value = 0.0 - obj.AvoidLastX_Faces = 0 - obj.CircularCenterCustom.x = 0.0 - obj.CircularCenterCustom.y = 0.0 - obj.CircularCenterCustom.z = 0.0 - obj.GapThreshold.Value = 0.005 - obj.AngularDeflection.Value = 0.25 - obj.LinearDeflection.Value = job.GeometryTolerance - # For debugging - obj.ShowTempObjects = False - - # need to overwrite the default depth calculations for facing - d = None - if job: - if job.Stock: - d = PathUtils.guessDepths(job.Stock.Shape, None) - PathLog.debug("job.Stock exists") - else: - PathLog.debug("job.Stock NOT exist") - else: - PathLog.debug("job NOT exist") - - if d is not None: - obj.OpFinalDepth.Value = d.final_depth - obj.OpStartDepth.Value = d.start_depth - else: - obj.OpFinalDepth.Value = -10 - obj.OpStartDepth.Value = 10 - - PathLog.debug('Default OpFinalDepth: {}'.format(obj.OpFinalDepth.Value)) - PathLog.debug('Defualt OpStartDepth: {}'.format(obj.OpStartDepth.Value)) - - def opApplyPropertyLimits(self, obj): - '''opApplyPropertyLimits(obj) ... Apply necessary limits to user input property values before performing main operation.''' - # Limit start index - if obj.StartIndex < 0.0: - obj.StartIndex = 0.0 - if obj.StartIndex > 360.0: - obj.StartIndex = 360.0 - - # Limit stop index - if obj.StopIndex > 360.0: - obj.StopIndex = 360.0 - if obj.StopIndex < 0.0: - obj.StopIndex = 0.0 - - # Limit cutter tilt - if obj.CutterTilt < -90.0: - obj.CutterTilt = -90.0 - if obj.CutterTilt > 90.0: - obj.CutterTilt = 90.0 - - # Limit sample interval - if obj.SampleInterval.Value < 0.0001: - obj.SampleInterval.Value = 0.0001 - PathLog.error(translate('PathSurface', 'Sample interval limits are 0.001 to 25.4 millimeters.')) - if obj.SampleInterval.Value > 25.4: - obj.SampleInterval.Value = 25.4 - PathLog.error(translate('PathSurface', 'Sample interval limits are 0.001 to 25.4 millimeters.')) - - # Limit cut pattern angle - if obj.CutPatternAngle < -360.0: - obj.CutPatternAngle = 0.0 - PathLog.error(translate('PathSurface', 'Cut pattern angle limits are +-360 degrees.')) - if obj.CutPatternAngle >= 360.0: - obj.CutPatternAngle = 0.0 - PathLog.error(translate('PathSurface', 'Cut pattern angle limits are +- 360 degrees.')) - - # Limit StepOver to natural number percentage - if obj.StepOver > 100: - obj.StepOver = 100 - if obj.StepOver < 1: - obj.StepOver = 1 - - # Limit AvoidLastX_Faces to zero and positive values - if obj.AvoidLastX_Faces < 0: - obj.AvoidLastX_Faces = 0 - PathLog.error(translate('PathSurface', 'AvoidLastX_Faces: Only zero or positive values permitted.')) - if obj.AvoidLastX_Faces > 100: - obj.AvoidLastX_Faces = 100 - PathLog.error(translate('PathSurface', 'AvoidLastX_Faces: Avoid last X faces count limited to 100.')) - - def opExecute(self, obj): - '''opExecute(obj) ... process surface operation''' - PathLog.track() - - self.modelSTLs = list() - self.safeSTLs = list() - self.modelTypes = list() - self.boundBoxes = list() - self.profileShapes = list() - self.collectiveShapes = list() - self.individualShapes = list() - self.avoidShapes = list() - self.deflection = None - self.tempGroup = None - self.CutClimb = False - self.closedGap = False - self.gaps = [0.1, 0.2, 0.3] - CMDS = list() - modelVisibility = list() - FCAD = FreeCAD.ActiveDocument - - # Set debugging behavior - self.showDebugObjects = False # Set to true if you want a visual DocObjects created for some path construction objects - self.showDebugObjects = obj.ShowTempObjects - deleteTempsFlag = True # Set to False for debugging - if PathLog.getLevel(PathLog.thisModule()) == 4: - deleteTempsFlag = False - else: - self.showDebugObjects = False - - # mark beginning of operation and identify parent Job - PathLog.info('\nBegin 3D Surface operation...') - startTime = time.time() - - # Identify parent Job - JOB = PathUtils.findParentJob(obj) - if JOB is None: - PathLog.error(translate('PathSurface', "No JOB")) - return - self.stockZMin = JOB.Stock.Shape.BoundBox.ZMin - - # set cut mode; reverse as needed - if obj.CutMode == 'Climb': - self.CutClimb = True - if obj.CutPatternReversed is True: - if self.CutClimb is True: - self.CutClimb = False - else: - self.CutClimb = True - - # Begin GCode for operation with basic information - # ... and move cutter to clearance height and startpoint - output = '' - if obj.Comment != '': - self.commandlist.append(Path.Command('N ({})'.format(str(obj.Comment)), {})) - self.commandlist.append(Path.Command('N ({})'.format(obj.Label), {})) - self.commandlist.append(Path.Command('N (Tool type: {})'.format(str(obj.ToolController.Tool.ToolType)), {})) - self.commandlist.append(Path.Command('N (Compensated Tool Path. Diameter: {})'.format(str(obj.ToolController.Tool.Diameter)), {})) - self.commandlist.append(Path.Command('N (Sample interval: {})'.format(str(obj.SampleInterval.Value)), {})) - self.commandlist.append(Path.Command('N (Step over %: {})'.format(str(obj.StepOver)), {})) - self.commandlist.append(Path.Command('N ({})'.format(output), {})) - self.commandlist.append(Path.Command('G0', {'Z': obj.ClearanceHeight.Value, 'F': self.vertRapid})) - if obj.UseStartPoint is True: - self.commandlist.append(Path.Command('G0', {'X': obj.StartPoint.x, 'Y': obj.StartPoint.y, 'F': self.horizRapid})) - - # Instantiate additional class operation variables - self.resetOpVariables() - - # Impose property limits - self.opApplyPropertyLimits(obj) - - # Create temporary group for temporary objects, removing existing - # if self.showDebugObjects is True: - tempGroupName = 'tempPathSurfaceGroup' - if FCAD.getObject(tempGroupName): - for to in FCAD.getObject(tempGroupName).Group: - FCAD.removeObject(to.Name) - FCAD.removeObject(tempGroupName) # remove temp directory if already exists - if FCAD.getObject(tempGroupName + '001'): - for to in FCAD.getObject(tempGroupName + '001').Group: - FCAD.removeObject(to.Name) - FCAD.removeObject(tempGroupName + '001') # remove temp directory if already exists - tempGroup = FCAD.addObject('App::DocumentObjectGroup', tempGroupName) - tempGroupName = tempGroup.Name - self.tempGroup = tempGroup - tempGroup.purgeTouched() - # Add temp object to temp group folder with following code: - # ... self.tempGroup.addObject(OBJ) - - # Setup cutter for OCL and cutout value for operation - based on tool controller properties - self.cutter = self.setOclCutter(obj) - self.safeCutter = self.setOclCutter(obj, safe=True) - if self.cutter is False or self.safeCutter is False: - PathLog.error(translate('PathSurface', "Canceling 3D Surface operation. Error creating OCL cutter.")) - return - toolDiam = self.cutter.getDiameter() - self.cutOut = (toolDiam * (float(obj.StepOver) / 100.0)) - self.radius = toolDiam / 2.0 - self.gaps = [toolDiam, toolDiam, toolDiam] - - # Get height offset values for later use - self.SafeHeightOffset = JOB.SetupSheet.SafeHeightOffset.Value - self.ClearHeightOffset = JOB.SetupSheet.ClearanceHeightOffset.Value - - # Calculate default depthparams for operation - self.depthParams = PathUtils.depth_params(obj.ClearanceHeight.Value, obj.SafeHeight.Value, obj.StartDepth.Value, obj.StepDown.Value, 0.0, obj.FinalDepth.Value) - self.midDep = (obj.StartDepth.Value + obj.FinalDepth.Value) / 2.0 - - # make circle for workplane - self.wpc = Part.makeCircle(2.0) - - # Set deflection values for mesh generation - try: # try/except is for Path Jobs created before GeometryTolerance - self.deflection = JOB.GeometryTolerance.Value - except AttributeError as ee: - PathLog.warning('Error setting Mesh deflection: {}. Using PathPreferences.defaultGeometryTolerance().'.format(ee)) - import PathScripts.PathPreferences as PathPreferences - self.deflection = PathPreferences.defaultGeometryTolerance() - - # Save model visibilities for restoration - if FreeCAD.GuiUp: - for m in range(0, len(JOB.Model.Group)): - mNm = JOB.Model.Group[m].Name - modelVisibility.append(FreeCADGui.ActiveDocument.getObject(mNm).Visibility) - - # Setup STL, model type, and bound box containers for each model in Job - for m in range(0, len(JOB.Model.Group)): - M = JOB.Model.Group[m] - self.modelSTLs.append(False) - self.safeSTLs.append(False) - self.profileShapes.append(False) - # Set bound box - if obj.BoundBox == 'BaseBoundBox': - if M.TypeId.startswith('Mesh'): - self.modelTypes.append('M') # Mesh - self.boundBoxes.append(M.Mesh.BoundBox) - else: - self.modelTypes.append('S') # Solid - self.boundBoxes.append(M.Shape.BoundBox) - elif obj.BoundBox == 'Stock': - self.modelTypes.append('S') # Solid - self.boundBoxes.append(JOB.Stock.Shape.BoundBox) - - # ###### MAIN COMMANDS FOR OPERATION ###### - - # Begin processing obj.Base data and creating GCode - # Process selected faces, if available - pPM = self._preProcessModel(JOB, obj) - if pPM is False: - PathLog.error('Unable to pre-process obj.Base.') - else: - (FACES, VOIDS) = pPM - - # Create OCL.stl model objects - self._prepareModelSTLs(JOB, obj) - - for m in range(0, len(JOB.Model.Group)): - Mdl = JOB.Model.Group[m] - if FACES[m] is False: - PathLog.error('No data for model base: {}'.format(JOB.Model.Group[m].Label)) - else: - if m > 0: - # Raise to clearance between models - CMDS.append(Path.Command('N (Transition to base: {}.)'.format(Mdl.Label))) - CMDS.append(Path.Command('G0', {'Z': obj.ClearanceHeight.Value, 'F': self.vertRapid})) - PathLog.info('Working on Model.Group[{}]: {}'.format(m, Mdl.Label)) - # make stock-model-voidShapes STL model for avoidance detection on transitions - self._makeSafeSTL(JOB, obj, m, FACES[m], VOIDS[m]) - #time.sleep(0.2) - # Process model/faces - OCL objects must be ready - CMDS.extend(self._processCutAreas(JOB, obj, m, FACES[m], VOIDS[m])) - - # Save gcode produced - self.commandlist.extend(CMDS) - - # ###### CLOSING COMMANDS FOR OPERATION ###### - - # Delete temporary objects - # Restore model visibilities for restoration - if FreeCAD.GuiUp: - FreeCADGui.ActiveDocument.getObject(tempGroupName).Visibility = False - for m in range(0, len(JOB.Model.Group)): - M = JOB.Model.Group[m] - M.Visibility = modelVisibility[m] - - if deleteTempsFlag is True: - for to in tempGroup.Group: - if hasattr(to, 'Group'): - for go in to.Group: - FCAD.removeObject(go.Name) - FCAD.removeObject(to.Name) - FCAD.removeObject(tempGroupName) - else: - if len(tempGroup.Group) == 0: - FCAD.removeObject(tempGroupName) - else: - tempGroup.purgeTouched() - - # Provide user feedback for gap sizes - gaps = list() - for g in self.gaps: - if g != toolDiam: - gaps.append(g) - if len(gaps) > 0: - obj.GapSizes = '{} mm'.format(gaps) - else: - if self.closedGap is True: - obj.GapSizes = 'Closed gaps < Gap Threshold.' - else: - obj.GapSizes = 'No gaps identified.' - - # clean up class variables - self.resetOpVariables() - self.deleteOpVariables() - - self.modelSTLs = None - self.safeSTLs = None - self.modelTypes = None - self.boundBoxes = None - self.gaps = None - self.closedGap = None - self.SafeHeightOffset = None - self.ClearHeightOffset = None - self.depthParams = None - self.midDep = None - self.wpc = None - self.deflection = None - del self.modelSTLs - del self.safeSTLs - del self.modelTypes - del self.boundBoxes - del self.gaps - del self.closedGap - del self.SafeHeightOffset - del self.ClearHeightOffset - del self.depthParams - del self.midDep - del self.wpc - del self.deflection - - execTime = time.time() - startTime - PathLog.info('Operation time: {} sec.'.format(execTime)) - - return True - - # Methods for constructing the cut area - def _preProcessModel(self, JOB, obj): - PathLog.debug('_preProcessModel()') - - FACES = list() - VOIDS = list() - fShapes = list() - vShapes = list() - preProcEr = translate('PathSurface', 'Error pre-processing Face') - warnFinDep = translate('PathSurface', 'Final Depth might need to be lower. Internal features detected in Face') - GRP = JOB.Model.Group - lenGRP = len(GRP) - - # Crete place holders for each base model in Job - for m in range(0, lenGRP): - FACES.append(False) - VOIDS.append(False) - fShapes.append(False) - vShapes.append(False) - - # The user has selected subobjects from the base. Pre-Process each. - if obj.Base and len(obj.Base) > 0: - PathLog.debug(' -obj.Base exists. Pre-processing for selected faces.') - - (FACES, VOIDS) = self._identifyFacesAndVoids(JOB, obj, FACES, VOIDS) - - # Cycle through each base model, processing faces for each - for m in range(0, lenGRP): - base = GRP[m] - (mFS, mVS, mPS) = self._preProcessFacesAndVoids(obj, base, m, FACES, VOIDS) - fShapes[m] = mFS - vShapes[m] = mVS - self.profileShapes[m] = mPS - else: - PathLog.debug(' -No obj.Base data.') - for m in range(0, lenGRP): - self.modelSTLs[m] = True - - # Process each model base, as a whole, as needed - # PathLog.debug(' -Pre-processing all models in Job.') - for m in range(0, lenGRP): - if fShapes[m] is False: - PathLog.debug(' -Pre-processing {} as a whole.'.format(GRP[m].Label)) - if obj.BoundBox == 'BaseBoundBox': - base = GRP[m] - elif obj.BoundBox == 'Stock': - base = JOB.Stock - - pPEB = self._preProcessEntireBase(obj, base, m) - if pPEB is False: - PathLog.error(' -Failed to pre-process base as a whole.') - else: - (fcShp, prflShp) = pPEB - if fcShp is not False: - if fcShp is True: - PathLog.debug(' -fcShp is True.') - fShapes[m] = True - else: - fShapes[m] = [fcShp] - if prflShp is not False: - if fcShp is not False: - PathLog.debug('vShapes[{}]: {}'.format(m, vShapes[m])) - if vShapes[m] is not False: - PathLog.debug(' -Cutting void from base profile shape.') - adjPS = prflShp.cut(vShapes[m][0]) - self.profileShapes[m] = [adjPS] - else: - PathLog.debug(' -vShapes[m] is False.') - self.profileShapes[m] = [prflShp] - else: - PathLog.debug(' -Saving base profile shape.') - self.profileShapes[m] = [prflShp] - PathLog.debug('self.profileShapes[{}]: {}'.format(m, self.profileShapes[m])) - # Efor - - return (fShapes, vShapes) - - def _identifyFacesAndVoids(self, JOB, obj, F, V): - TUPS = list() - GRP = JOB.Model.Group - lenGRP = len(GRP) - - # Separate selected faces into (base, face) tuples and flag model(s) for STL creation - for (bs, SBS) in obj.Base: - for sb in SBS: - # Flag model for STL creation - mdlIdx = None - for m in range(0, lenGRP): - if bs is GRP[m]: - self.modelSTLs[m] = True - mdlIdx = m - break - TUPS.append((mdlIdx, bs, sb)) # (model idx, base, sub) - - # Apply `AvoidXFaces` value - faceCnt = len(TUPS) - add = faceCnt - obj.AvoidLastX_Faces - for bst in range(0, faceCnt): - (m, base, sub) = TUPS[bst] - shape = getattr(base.Shape, sub) - if isinstance(shape, Part.Face): - faceIdx = int(sub[4:]) - 1 - if bst < add: - if F[m] is False: - F[m] = list() - F[m].append((shape, faceIdx)) - else: - if V[m] is False: - V[m] = list() - V[m].append((shape, faceIdx)) - return (F, V) - - def _preProcessFacesAndVoids(self, obj, base, m, FACES, VOIDS): - mFS = False - mVS = False - mPS = False - mIFS = list() - BB = base.Shape.BoundBox - - if FACES[m] is not False: - isHole = False - if obj.HandleMultipleFeatures == 'Collectively': - cont = True - fsL = list() # face shape list - ifL = list() # avoid shape list - outFCS = list() - - # Get collective envelope slice of selected faces - for (fcshp, fcIdx) in FACES[m]: - fNum = fcIdx + 1 - fsL.append(fcshp) - gFW = self._getFaceWires(base, fcshp, fcIdx) - if gFW is False: - PathLog.debug('Failed to get wires from Face{}'.format(fNum)) - elif gFW[0] is False: - PathLog.debug('Cannot process Face{}. Check that it has horizontal surface exposure.'.format(fNum)) - else: - ((otrFace, raised), intWires) = gFW - outFCS.append(otrFace) - if obj.InternalFeaturesCut is False: - if intWires is not False: - for (iFace, rsd) in intWires: - ifL.append(iFace) - - PathLog.debug('Attempting to get cross-section of collective faces.') - if len(outFCS) == 0: - PathLog.error('Cannot process selected faces. Check horizontal surface exposure.'.format(fNum)) - cont = False - else: - cfsL = Part.makeCompound(outFCS) - - # Handle profile edges request - if cont is True and obj.ProfileEdges != 'None': - ofstVal = self._calculateOffsetValue(obj, isHole) - psOfst = self._extractFaceOffset(cfsL, ofstVal) - if psOfst is not False: - mPS = [psOfst] - if obj.ProfileEdges == 'Only': - mFS = True - cont = False - else: - PathLog.error(' -Failed to create profile geometry for selected faces.') - cont = False - - if cont: - if self.showDebugObjects is True: - T = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpCollectiveShape') - T.Shape = cfsL - T.purgeTouched() - self.tempGroup.addObject(T) - - ofstVal = self._calculateOffsetValue(obj, isHole) - faceOfstShp = self._extractFaceOffset(cfsL, ofstVal) - if faceOfstShp is False: - PathLog.error(' -Failed to create offset face.') - cont = False - - if cont: - lenIfL = len(ifL) - if obj.InternalFeaturesCut is False: - if lenIfL == 0: - PathLog.debug(' -No internal features saved.') - else: - if lenIfL == 1: - casL = ifL[0] - else: - casL = Part.makeCompound(ifL) - if self.showDebugObjects is True: - C = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpCompoundIntFeat') - C.Shape = casL - C.purgeTouched() - self.tempGroup.addObject(C) - ofstVal = self._calculateOffsetValue(obj, isHole=True) - intOfstShp = self._extractFaceOffset(casL, ofstVal) - mIFS.append(intOfstShp) - # faceOfstShp = faceOfstShp.cut(intOfstShp) - - mFS = [faceOfstShp] - # Eif - - elif obj.HandleMultipleFeatures == 'Individually': - for (fcshp, fcIdx) in FACES[m]: - cont = True - fsL = list() # face shape list - ifL = list() # avoid shape list - fNum = fcIdx + 1 - outerFace = False - - gFW = self._getFaceWires(base, fcshp, fcIdx) - if gFW is False: - PathLog.debug('Failed to get wires from Face{}'.format(fNum)) - cont = False - elif gFW[0] is False: - PathLog.debug('Cannot process Face{}. Check that it has horizontal surface exposure.'.format(fNum)) - cont = False - outerFace = False - else: - ((otrFace, raised), intWires) = gFW - outerFace = otrFace - if obj.InternalFeaturesCut is False: - if intWires is not False: - for (iFace, rsd) in intWires: - ifL.append(iFace) - - if outerFace is not False: - PathLog.debug('Attempting to create offset face of Face{}'.format(fNum)) - - if obj.ProfileEdges != 'None': - ofstVal = self._calculateOffsetValue(obj, isHole) - psOfst = self._extractFaceOffset(outerFace, ofstVal) - if psOfst is not False: - if mPS is False: - mPS = list() - mPS.append(psOfst) - if obj.ProfileEdges == 'Only': - if mFS is False: - mFS = list() - mFS.append(True) - cont = False - else: - PathLog.error(' -Failed to create profile geometry for Face{}.'.format(fNum)) - cont = False - - if cont: - ofstVal = self._calculateOffsetValue(obj, isHole) - faceOfstShp = self._extractFaceOffset(outerFace, ofstVal) - - lenIfl = len(ifL) - if obj.InternalFeaturesCut is False and lenIfl > 0: - if lenIfl == 1: - casL = ifL[0] - else: - casL = Part.makeCompound(ifL) - - ofstVal = self._calculateOffsetValue(obj, isHole=True) - intOfstShp = self._extractFaceOffset(casL, ofstVal) - mIFS.append(intOfstShp) - # faceOfstShp = faceOfstShp.cut(intOfstShp) - - if mFS is False: - mFS = list() - mFS.append(faceOfstShp) - # Eif - # Efor - # Eif - # Eif - - if len(mIFS) > 0: - if mVS is False: - mVS = list() - for ifs in mIFS: - mVS.append(ifs) - - if VOIDS[m] is not False: - PathLog.debug('Processing avoid faces.') - cont = True - isHole = False - outFCS = list() - intFEAT = list() - - for (fcshp, fcIdx) in VOIDS[m]: - fNum = fcIdx + 1 - gFW = self._getFaceWires(base, fcshp, fcIdx) - if gFW is False: - PathLog.debug('Failed to get wires from avoid Face{}'.format(fNum)) - cont = False - else: - ((otrFace, raised), intWires) = gFW - outFCS.append(otrFace) - if obj.AvoidLastX_InternalFeatures is False: - if intWires is not False: - for (iFace, rsd) in intWires: - intFEAT.append(iFace) - - lenOtFcs = len(outFCS) - if lenOtFcs == 0: - cont = False - else: - if lenOtFcs == 1: - avoid = outFCS[0] - else: - avoid = Part.makeCompound(outFCS) - - if self.showDebugObjects is True: - PathLog.debug('*** tmpAvoidArea') - P = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpVoidEnvelope') - P.Shape = avoid - # P.recompute() - P.purgeTouched() - self.tempGroup.addObject(P) - - if cont: - if self.showDebugObjects is True: - PathLog.debug('*** tmpVoidCompound') - P = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpVoidCompound') - P.Shape = avoid - # P.recompute() - P.purgeTouched() - self.tempGroup.addObject(P) - ofstVal = self._calculateOffsetValue(obj, isHole, isVoid=True) - avdOfstShp = self._extractFaceOffset(avoid, ofstVal) - if avdOfstShp is False: - PathLog.error('Failed to create collective offset avoid face.') - cont = False - - if cont: - avdShp = avdOfstShp - - if obj.AvoidLastX_InternalFeatures is False and len(intFEAT) > 0: - if len(intFEAT) > 1: - ifc = Part.makeCompound(intFEAT) - else: - ifc = intFEAT[0] - ofstVal = self._calculateOffsetValue(obj, isHole=True) - ifOfstShp = self._extractFaceOffset(ifc, ofstVal) - if ifOfstShp is False: - PathLog.error('Failed to create collective offset avoid internal features.') - else: - avdShp = avdOfstShp.cut(ifOfstShp) - - if mVS is False: - mVS = list() - mVS.append(avdShp) - - - return (mFS, mVS, mPS) - - def _getFaceWires(self, base, fcshp, fcIdx): - outFace = False - INTFCS = list() - fNum = fcIdx + 1 - # preProcEr = translate('PathSurface', 'Error pre-processing Face') - warnFinDep = translate('PathSurface', 'Final Depth might need to be lower. Internal features detected in Face') - - PathLog.debug('_getFaceWires() from Face{}'.format(fNum)) - WIRES = self._extractWiresFromFace(base, fcshp) - if WIRES is False: - PathLog.error('Failed to extract wires from Face{}'.format(fNum)) - return False - - # Process remaining internal features, adding to FCS list - lenW = len(WIRES) - for w in range(0, lenW): - (wire, rsd) = WIRES[w] - PathLog.debug('Processing Wire{} in Face{}. isRaised: {}'.format(w + 1, fNum, rsd)) - if wire.isClosed() is False: - PathLog.debug(' -wire is not closed.') - else: - slc = self._flattenWireToFace(wire) - if slc is False: - PathLog.error('FAILED to identify horizontal exposure on Face{}.'.format(fNum)) - else: - if w == 0: - outFace = (slc, rsd) - else: - # add to VOIDS so cutter avoids area. - PathLog.warning(warnFinDep + str(fNum) + '.') - INTFCS.append((slc, rsd)) - if len(INTFCS) == 0: - return (outFace, False) - else: - return (outFace, INTFCS) - - def _preProcessEntireBase(self, obj, base, m): - cont = True - isHole = False - prflShp = False - # Create envelope, extract cross-section and make offset co-planar shape - # baseEnv = PathUtils.getEnvelope(base.Shape, subshape=None, depthparams=self.depthParams) - - try: - baseEnv = PathUtils.getEnvelope(partshape=base.Shape, subshape=None, depthparams=self.depthParams) # Produces .Shape - except Exception as ee: - PathLog.error(str(ee)) - shell = base.Shape.Shells[0] - solid = Part.makeSolid(shell) - try: - baseEnv = PathUtils.getEnvelope(partshape=solid, subshape=None, depthparams=self.depthParams) # Produces .Shape - except Exception as eee: - PathLog.error(str(eee)) - cont = False - #time.sleep(0.2) - - if cont: - csFaceShape = self._getShapeSlice(baseEnv) - if csFaceShape is False: - PathLog.debug('_getShapeSlice(baseEnv) failed') - csFaceShape = self._getCrossSection(baseEnv) - if csFaceShape is False: - PathLog.debug('_getCrossSection(baseEnv) failed') - csFaceShape = self._getSliceFromEnvelope(baseEnv) - if csFaceShape is False: - PathLog.error('Failed to slice baseEnv shape.') - cont = False - - if cont is True and obj.ProfileEdges != 'None': - PathLog.debug(' -Attempting profile geometry for model base.') - ofstVal = self._calculateOffsetValue(obj, isHole) - psOfst = self._extractFaceOffset(csFaceShape, ofstVal) - if psOfst is not False: - if obj.ProfileEdges == 'Only': - return (True, psOfst) - prflShp = psOfst - else: - PathLog.error(' -Failed to create profile geometry.') - cont = False - - if cont: - ofstVal = self._calculateOffsetValue(obj, isHole) - faceOffsetShape = self._extractFaceOffset(csFaceShape, ofstVal) - if faceOffsetShape is False: - PathLog.error('_extractFaceOffset() failed.') - else: - faceOffsetShape.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - faceOffsetShape.BoundBox.ZMin)) - return (faceOffsetShape, prflShp) - return False - - def _extractWiresFromFace(self, base, fc): - '''_extractWiresFromFace(base, fc) ... - Attempts to return all closed wires within a parent face, including the outer most wire of the parent. - The wires are ordered by area. Each wire is also categorized as a pocket(False) or raised protrusion(True). - ''' - PathLog.debug('_extractWiresFromFace()') - - WIRES = list() - lenWrs = len(fc.Wires) - PathLog.debug(' -Wire count: {}'.format(lenWrs)) - - def index0(tup): - return tup[0] - - # Cycle through wires in face - for w in range(0, lenWrs): - PathLog.debug(' -Analyzing wire_{}'.format(w + 1)) - wire = fc.Wires[w] - checkEdges = False - cont = True - - # Check for closed edges (circles, ellipses, etc...) - for E in wire.Edges: - if E.isClosed() is True: - checkEdges = True - break - - if checkEdges is True: - PathLog.debug(' -checkEdges is True') - for e in range(0, len(wire.Edges)): - edge = wire.Edges[e] - if edge.isClosed() is True and edge.Mass > 0.01: - PathLog.debug(' -Found closed edge') - raised = False - ip = self._isPocket(base, fc, edge) - if ip is False: - raised = True - ebb = edge.BoundBox - eArea = ebb.XLength * ebb.YLength - F = Part.Face(Part.Wire([edge])) - WIRES.append((eArea, F.Wires[0], raised)) - cont = False - - if cont: - PathLog.debug(' -cont is True') - # If only one wire and not checkEdges, return first wire - if lenWrs == 1: - return [(wire, False)] - - raised = False - wbb = wire.BoundBox - wArea = wbb.XLength * wbb.YLength - if w > 0: - ip = self._isPocket(base, fc, wire) - if ip is False: - raised = True - WIRES.append((wArea, Part.Wire(wire.Edges), raised)) - - nf = len(WIRES) - if nf > 0: - PathLog.debug(' -number of wires found is {}'.format(nf)) - if nf == 1: - (area, W, raised) = WIRES[0] - return [(W, raised)] - else: - sortedWIRES = sorted(WIRES, key=index0, reverse=True) - return [(W, raised) for (area, W, raised) in sortedWIRES] # outer, then inner by area size - - return False - - def _calculateOffsetValue(self, obj, isHole, isVoid=False): - '''_calculateOffsetValue(obj, isHole, isVoid) ... internal function. - Calculate the offset for the Path.Area() function.''' - JOB = PathUtils.findParentJob(obj) - tolrnc = JOB.GeometryTolerance.Value - - if isVoid is False: - if isHole is True: - offset = -1 * obj.InternalFeaturesAdjustment.Value - offset += self.radius + (tolrnc / 10.0) - else: - offset = -1 * obj.BoundaryAdjustment.Value - if obj.BoundaryEnforcement is True: - offset += self.radius + (tolrnc / 10.0) - else: - offset -= self.radius + (tolrnc / 10.0) - offset = 0.0 - offset - else: - offset = -1 * obj.BoundaryAdjustment.Value - offset += self.radius + (tolrnc / 10.0) - - return offset - - def _extractFaceOffset(self, fcShape, offset): - '''_extractFaceOffset(fcShape, offset) ... internal function. - Original _buildPathArea() version copied from PathAreaOp.py module. This version is modified. - Adjustments made based on notes by @sliptonic at this webpage: https://github.com/sliptonic/FreeCAD/wiki/PathArea-notes.''' - PathLog.debug('_extractFaceOffset()') - - if fcShape.BoundBox.ZMin != 0.0: - fcShape.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - fcShape.BoundBox.ZMin)) - - areaParams = {} - areaParams['Offset'] = offset - areaParams['Fill'] = 1 - areaParams['Coplanar'] = 0 - areaParams['SectionCount'] = 1 # -1 = full(all per depthparams??) sections - areaParams['Reorient'] = True - areaParams['OpenMode'] = 0 - areaParams['MaxArcPoints'] = 400 # 400 - areaParams['Project'] = True - - area = Path.Area() # Create instance of Area() class object - # area.setPlane(PathUtils.makeWorkplane(fcShape)) # Set working plane - area.setPlane(PathUtils.makeWorkplane(self.wpc)) # Set working plane to normal at Z=1 - area.add(fcShape) - area.setParams(**areaParams) # set parameters - - offsetShape = area.getShape() - wCnt = len(offsetShape.Wires) - if wCnt == 0: - return False - elif wCnt == 1: - ofstFace = Part.Face(offsetShape.Wires[0]) - else: - W = list() - for wr in offsetShape.Wires: - W.append(Part.Face(wr)) - ofstFace = Part.makeCompound(W) - - return ofstFace # offsetShape - - def _isPocket(self, b, f, w): - '''_isPocket(b, f, w)... - Attempts to determine if the wire(w) in face(f) of base(b) is a pocket or raised protrusion. - Returns True if pocket, False if raised protrusion.''' - e = w.Edges[0] - for fi in range(0, len(b.Shape.Faces)): - face = b.Shape.Faces[fi] - for ei in range(0, len(face.Edges)): - edge = face.Edges[ei] - if e.isSame(edge) is True: - if f is face: - # Alternative: run loop to see if all edges are same - pass # same source face, look for another - else: - if face.CenterOfMass.z < f.CenterOfMass.z: - return True - return False - - def _flattenWireToFace(self, wire): - PathLog.debug('_flattenWireToFace()') - if wire.isClosed() is False: - PathLog.debug(' -wire.isClosed() is False') - return False - - # If wire is planar horizontal, convert to a face and return - if wire.BoundBox.ZLength == 0.0: - slc = Part.Face(wire) - return slc - - # Attempt to create a new wire for manipulation, if not, use original - newWire = Part.Wire(wire.Edges) - if newWire.isClosed() is True: - nWire = newWire - else: - PathLog.debug(' -newWire.isClosed() is False') - nWire = wire - - # Attempt extrusion, and then try a manual slice and then cross-section - ext = self._getExtrudedShape(nWire) - if ext is False: - PathLog.debug('_getExtrudedShape() failed') - else: - slc = self._getShapeSlice(ext) - if slc is not False: - return slc - cs = self._getCrossSection(ext, True) - if cs is not False: - return cs - - # Attempt creating an envelope, and then try a manual slice and then cross-section - env = self._getShapeEnvelope(nWire) - if env is False: - PathLog.debug('_getShapeEnvelope() failed') - else: - slc = self._getShapeSlice(env) - if slc is not False: - return slc - cs = self._getCrossSection(env, True) - if cs is not False: - return cs - - # Attempt creating a projection - slc = self._getProjectedFace(nWire) - if slc is False: - PathLog.debug('_getProjectedFace() failed') - else: - return slc - - return False - - def _getExtrudedShape(self, wire): - PathLog.debug('_getExtrudedShape()') - wBB = wire.BoundBox - extFwd = math.floor(2.0 * wBB.ZLength) + 10.0 - - try: - # slower, but renders collective faces correctly. Method 5 in TESTING - shell = wire.extrude(FreeCAD.Vector(0.0, 0.0, extFwd)) - except Exception as ee: - PathLog.error(' -extrude wire failed: \n{}'.format(ee)) - return False - - SHP = Part.makeSolid(shell) - return SHP - - def _getShapeSlice(self, shape): - PathLog.debug('_getShapeSlice()') - - bb = shape.BoundBox - mid = (bb.ZMin + bb.ZMax) / 2.0 - xmin = bb.XMin - 1.0 - xmax = bb.XMax + 1.0 - ymin = bb.YMin - 1.0 - ymax = bb.YMax + 1.0 - p1 = FreeCAD.Vector(xmin, ymin, mid) - p2 = FreeCAD.Vector(xmax, ymin, mid) - p3 = FreeCAD.Vector(xmax, ymax, mid) - p4 = FreeCAD.Vector(xmin, ymax, mid) - - e1 = Part.makeLine(p1, p2) - e2 = Part.makeLine(p2, p3) - e3 = Part.makeLine(p3, p4) - e4 = Part.makeLine(p4, p1) - face = Part.Face(Part.Wire([e1, e2, e3, e4])) - fArea = face.BoundBox.XLength * face.BoundBox.YLength # face.Wires[0].Area - sArea = shape.BoundBox.XLength * shape.BoundBox.YLength - midArea = (fArea + sArea) / 2.0 - - slcShp = shape.common(face) - slcArea = slcShp.BoundBox.XLength * slcShp.BoundBox.YLength - - if slcArea < midArea: - for W in slcShp.Wires: - if W.isClosed() is False: - PathLog.debug(' -wire.isClosed() is False') - return False - if len(slcShp.Wires) == 1: - wire = slcShp.Wires[0] - slc = Part.Face(wire) - slc.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - slc.BoundBox.ZMin)) - return slc - else: - fL = list() - for W in slcShp.Wires: - slc = Part.Face(W) - slc.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - slc.BoundBox.ZMin)) - fL.append(slc) - comp = Part.makeCompound(fL) - if self.showDebugObjects is True: - PathLog.debug('*** tmpSliceCompound') - P = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpSliceCompound') - P.Shape = comp - # P.recompute() - P.purgeTouched() - self.tempGroup.addObject(P) - return comp - - PathLog.debug(' -slcArea !< midArea') - PathLog.debug(' -slcShp.Edges count: {}. Might be a vertically oriented face.'.format(len(slcShp.Edges))) - return False - - def _getProjectedFace(self, wire): - PathLog.debug('_getProjectedFace()') - F = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpProjectionWire') - F.Shape = wire - F.purgeTouched() - self.tempGroup.addObject(F) - try: - prj = Draft.makeShape2DView(F, FreeCAD.Vector(0, 0, 1)) - prj.recompute() - prj.purgeTouched() - self.tempGroup.addObject(prj) - except Exception as ee: - PathLog.error(str(ee)) - return False - else: - pWire = Part.Wire(prj.Shape.Edges) - if pWire.isClosed() is False: - # PathLog.debug(' -pWire.isClosed() is False') - return False - slc = Part.Face(pWire) - slc.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - slc.BoundBox.ZMin)) - return slc - return False - - def _getCrossSection(self, shape, withExtrude=False): - PathLog.debug('_getCrossSection()') - wires = list() - bb = shape.BoundBox - mid = (bb.ZMin + bb.ZMax) / 2.0 - - for i in shape.slice(FreeCAD.Vector(0, 0, 1), mid): - wires.append(i) - - if len(wires) > 0: - comp = Part.Compound(wires) # produces correct cross-section wire ! - comp.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - comp.BoundBox.ZMin)) - csWire = comp.Wires[0] - if csWire.isClosed() is False: - PathLog.debug(' -comp.Wires[0] is not closed') - return False - if withExtrude is True: - ext = self._getExtrudedShape(csWire) - CS = self._getShapeSlice(ext) - if CS is False: - return False - else: - CS = Part.Face(csWire) - CS.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - CS.BoundBox.ZMin)) - return CS - else: - PathLog.debug(' -No wires from .slice() method') - - return False - - def _getShapeEnvelope(self, shape): - PathLog.debug('_getShapeEnvelope()') - - wBB = shape.BoundBox - extFwd = wBB.ZLength + 10.0 - minz = wBB.ZMin - maxz = wBB.ZMin + extFwd - stpDwn = (maxz - minz) / 4.0 - dep_par = PathUtils.depth_params(maxz + 5.0, maxz + 3.0, maxz, stpDwn, 0.0, minz) - - try: - env = PathUtils.getEnvelope(partshape=shape, depthparams=dep_par) # Produces .Shape - except Exception as ee: - PathLog.error('try: PathUtils.getEnvelope() failed.\n' + str(ee)) - return False - else: - return env - - return False - - def _getSliceFromEnvelope(self, env): - PathLog.debug('_getSliceFromEnvelope()') - eBB = env.BoundBox - extFwd = eBB.ZLength + 10.0 - maxz = eBB.ZMin + extFwd - - maxMax = env.Edges[0].BoundBox.ZMin - emax = math.floor(maxz - 1.0) - E = list() - for e in range(0, len(env.Edges)): - emin = env.Edges[e].BoundBox.ZMin - if emin > emax: - E.append(env.Edges[e]) - tf = Part.Face(Part.Wire(Part.__sortEdges__(E))) - tf.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - tf.BoundBox.ZMin)) - - return tf - - def _prepareModelSTLs(self, JOB, obj): - PathLog.debug('_prepareModelSTLs()') - for m in range(0, len(JOB.Model.Group)): - M = JOB.Model.Group[m] - - # PathLog.debug(f" -self.modelTypes[{m}] == 'M'") - if self.modelTypes[m] == 'M': - #TODO: test if this works - facets = M.Mesh.Facets.Points - else: - facets = Part.getFacets(M.Shape) - - if self.modelSTLs[m] is True: - stl = ocl.STLSurf() - - for tri in facets: - t = ocl.Triangle(ocl.Point(tri[0][0], tri[0][1], tri[0][2]), - ocl.Point(tri[1][0], tri[1][1], tri[1][2]), - ocl.Point(tri[2][0], tri[2][1], tri[2][2])) - stl.addTriangle(t) - self.modelSTLs[m] = stl - return - - def _makeSafeSTL(self, JOB, obj, mdlIdx, faceShapes, voidShapes): - '''_makeSafeSTL(JOB, obj, mdlIdx, faceShapes, voidShapes)... - Creates and OCL.stl object with combined data with waste stock, - model, and avoided faces. Travel lines can be checked against this - STL object to determine minimum travel height to clear stock and model.''' - PathLog.debug('_makeSafeSTL()') - - fuseShapes = list() - Mdl = JOB.Model.Group[mdlIdx] - FCAD = FreeCAD.ActiveDocument - mBB = Mdl.Shape.BoundBox - sBB = JOB.Stock.Shape.BoundBox - - # add Model shape to safeSTL shape - fuseShapes.append(Mdl.Shape) - - if obj.BoundBox == 'BaseBoundBox': - cont = False - extFwd = (sBB.ZLength) - zmin = mBB.ZMin - zmax = mBB.ZMin + extFwd - stpDwn = (zmax - zmin) / 4.0 - dep_par = PathUtils.depth_params(zmax + 5.0, zmax + 3.0, zmax, stpDwn, 0.0, zmin) - - try: - envBB = PathUtils.getEnvelope(partshape=Mdl.Shape, depthparams=dep_par) # Produces .Shape - cont = True - except Exception as ee: - PathLog.error(str(ee)) - shell = Mdl.Shape.Shells[0] - solid = Part.makeSolid(shell) - try: - envBB = PathUtils.getEnvelope(partshape=solid, depthparams=dep_par) # Produces .Shape - cont = True - except Exception as eee: - PathLog.error(str(eee)) - - if cont: - stckWst = JOB.Stock.Shape.cut(envBB) - if obj.BoundaryAdjustment > 0.0: - cmpndFS = Part.makeCompound(faceShapes) - baBB = PathUtils.getEnvelope(partshape=cmpndFS, depthparams=self.depthParams) # Produces .Shape - adjStckWst = stckWst.cut(baBB) - else: - adjStckWst = stckWst - fuseShapes.append(adjStckWst) - else: - PathLog.warning('Path transitions might not avoid the model. Verify paths.') - #time.sleep(0.3) - else: - # If boundbox is Job.Stock, add hidden pad under stock as base plate - toolDiam = self.cutter.getDiameter() - zMin = JOB.Stock.Shape.BoundBox.ZMin - xMin = JOB.Stock.Shape.BoundBox.XMin - toolDiam - yMin = JOB.Stock.Shape.BoundBox.YMin - toolDiam - bL = JOB.Stock.Shape.BoundBox.XLength + (2 * toolDiam) - bW = JOB.Stock.Shape.BoundBox.YLength + (2 * toolDiam) - bH = 1.0 - crnr = FreeCAD.Vector(xMin, yMin, zMin - 1.0) - B = Part.makeBox(bL, bW, bH, crnr, FreeCAD.Vector(0, 0, 1)) - fuseShapes.append(B) - - if voidShapes is not False: - voidComp = Part.makeCompound(voidShapes) - voidEnv = PathUtils.getEnvelope(partshape=voidComp, depthparams=self.depthParams) # Produces .Shape - fuseShapes.append(voidEnv) - - fused = Part.makeCompound(fuseShapes) - - if self.showDebugObjects is True: - T = FreeCAD.ActiveDocument.addObject('Part::Feature', 'safeSTLShape') - T.Shape = fused - T.purgeTouched() - self.tempGroup.addObject(T) - - facets = Part.getFacets(fused) - - stl = ocl.STLSurf() - for tri in facets: - t = ocl.Triangle(ocl.Point(tri[0][0], tri[0][1], tri[0][2]), - ocl.Point(tri[1][0], tri[1][1], tri[1][2]), - ocl.Point(tri[2][0], tri[2][1], tri[2][2])) - stl.addTriangle(t) - - self.safeSTLs[mdlIdx] = stl - - def _processCutAreas(self, JOB, obj, mdlIdx, FCS, VDS): - '''_processCutAreas(JOB, obj, mdlIdx, FCS, VDS)... - This method applies any avoided faces or regions to the selected faces. - It then calls the correct scan method depending on the ScanType property.''' - PathLog.debug('_processCutAreas()') - - final = list() - base = JOB.Model.Group[mdlIdx] - - # Process faces Collectively or Individually - if obj.HandleMultipleFeatures == 'Collectively': - if FCS is True: - COMP = False - else: - ADD = Part.makeCompound(FCS) - if VDS is not False: - DEL = Part.makeCompound(VDS) - COMP = ADD.cut(DEL) - else: - COMP = ADD - - if obj.ScanType == 'Planar': - final.extend(self._processPlanarOp(JOB, obj, mdlIdx, COMP, 0)) - elif obj.ScanType == 'Rotational': - final.extend(self._processRotationalOp(obj, base, COMP)) - - elif obj.HandleMultipleFeatures == 'Individually': - for fsi in range(0, len(FCS)): - fShp = FCS[fsi] - # self.deleteOpVariables(all=False) - self.resetOpVariables(all=False) - - if fShp is True: - COMP = False - else: - ADD = Part.makeCompound([fShp]) - if VDS is not False: - DEL = Part.makeCompound(VDS) - COMP = ADD.cut(DEL) - else: - COMP = ADD - - if obj.ScanType == 'Planar': - final.extend(self._processPlanarOp(JOB, obj, mdlIdx, COMP, fsi)) - elif obj.ScanType == 'Rotational': - final.extend(self._processRotationalOp(JOB, obj, mdlIdx, COMP)) - COMP = None - # Eif - - return final - - # Methods for creating path geometry - def _processPlanarOp(self, JOB, obj, mdlIdx, cmpdShp, fsi): - '''_processPlanarOp(JOB, obj, mdlIdx, cmpdShp)... - This method compiles the main components for the procedural portion of a planar operation (non-rotational). - It creates the OCL PathDropCutter objects: model and safeTravel. - It makes the necessary facial geometries for the actual cut area. - It calls the correct Single or Multi-pass method as needed. - It returns the gcode for the operation. ''' - PathLog.debug('_processPlanarOp()') - final = list() - SCANDATA = list() - # base = JOB.Model.Group[mdlIdx] - - # Compute number and size of stepdowns, and final depth - if obj.LayerMode == 'Single-pass': - depthparams = [obj.FinalDepth.Value] - elif obj.LayerMode == 'Multi-pass': - depthparams = [i for i in self.depthParams] - lenDP = len(depthparams) - - # Prepare PathDropCutter objects with STL data - pdc = self._planarGetPDC(self.modelSTLs[mdlIdx], depthparams[lenDP - 1], obj.SampleInterval.Value) - safePDC = self._planarGetPDC(self.safeSTLs[mdlIdx], - depthparams[lenDP - 1], obj.SampleInterval.Value, useSafeCutter=False) - - profScan = list() - if obj.ProfileEdges != 'None': - prflShp = self.profileShapes[mdlIdx][fsi] - if prflShp is False: - PathLog.error('No profile shape is False.') - return list() - if self.showDebugObjects is True: - P = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpNewProfileShape') - P.Shape = prflShp - # P.recompute() - P.purgeTouched() - self.tempGroup.addObject(P) - # get offset path geometry and perform OCL scan with that geometry - pathOffsetGeom = self._planarMakeProfileGeom(obj, prflShp) - if pathOffsetGeom is False: - PathLog.error('No profile geometry returned.') - return list() - profScan = [self._planarPerformOclScan(obj, pdc, pathOffsetGeom, offsetPoints=True)] - - geoScan = list() - if obj.ProfileEdges != 'Only': - if self.showDebugObjects is True: - F = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpCutArea') - F.Shape = cmpdShp - # F.recompute() - F.purgeTouched() - self.tempGroup.addObject(F) - # get internal path geometry and perform OCL scan with that geometry - pathGeom = self._planarMakePathGeom(obj, cmpdShp) - if pathGeom is False: - PathLog.error('No path geometry returned.') - return list() - geoScan = self._planarPerformOclScan(obj, pdc, pathGeom, offsetPoints=False) - - if obj.ProfileEdges == 'Only': # ['None', 'Only', 'First', 'Last'] - SCANDATA.extend(profScan) - if obj.ProfileEdges == 'None': - SCANDATA.extend(geoScan) - if obj.ProfileEdges == 'First': - SCANDATA.extend(profScan) - SCANDATA.extend(geoScan) - if obj.ProfileEdges == 'Last': - SCANDATA.extend(geoScan) - SCANDATA.extend(profScan) - - # Apply depth offset - if obj.DepthOffset.Value != 0.0: - self._planarApplyDepthOffset(SCANDATA, obj.DepthOffset.Value) - - if len(SCANDATA) == 0: - PathLog.error('No scan data to convert to Gcode.') - return list() - - # If cut pattern is `Circular`, there are zero(almost zero) straight lines to optimize - # Store initial `OptimizeLinearPaths` value for later restoration - self.preOLP = obj.OptimizeLinearPaths - if obj.CutPattern in ['Circular', 'CircularZigZag']: - obj.OptimizeLinearPaths = False - - # Process OCL scan data - if obj.LayerMode == 'Single-pass': - final.extend(self._planarDropCutSingle(JOB, obj, pdc, safePDC, depthparams, SCANDATA)) - elif obj.LayerMode == 'Multi-pass': - final.extend(self._planarDropCutMulti(JOB, obj, pdc, safePDC, depthparams, SCANDATA)) - - # If cut pattern is `Circular`, restore initial OLP value - if obj.CutPattern in ['Circular', 'CircularZigZag']: - obj.OptimizeLinearPaths = self.preOLP - - # Raise to safe height between individual faces. - if obj.HandleMultipleFeatures == 'Individually': - final.insert(0, Path.Command('G0', {'Z': obj.SafeHeight.Value, 'F': self.vertRapid})) - - return final - - def _planarMakePathGeom(self, obj, faceShp): - '''_planarMakePathGeom(obj, faceShp)... - Creates the line/arc cut pattern geometry and returns the intersection with the received faceShp. - The resulting intersecting line/arc geometries are then converted to lines or arcs for OCL.''' - PathLog.debug('_planarMakePathGeom()') - GeoSet = list() - - # Apply drop cutter extra offset and set the max and min XY area of the operation - xmin = faceShp.BoundBox.XMin - xmax = faceShp.BoundBox.XMax - ymin = faceShp.BoundBox.YMin - ymax = faceShp.BoundBox.YMax - zmin = faceShp.BoundBox.ZMin - zmax = faceShp.BoundBox.ZMax - - # Compute weighted center of mass of all faces combined - fCnt = 0 - totArea = 0.0 - zeroCOM = FreeCAD.Vector(0.0, 0.0, 0.0) - for F in faceShp.Faces: - comF = F.CenterOfMass - areaF = F.Area - totArea += areaF - fCnt += 1 - zeroCOM = zeroCOM.add(FreeCAD.Vector(comF.x, comF.y, 0.0).multiply(areaF)) - if fCnt == 0: - PathLog.error(translate('PathSurface', 'Cannot calculate the Center Of Mass. Using Center of Boundbox.')) - zeroCOM = FreeCAD.Vector((xmin + xmax) / 2.0, (ymin + ymax) / 2.0, 0.0) - else: - avgArea = totArea / fCnt - zeroCOM.multiply(1 / fCnt) - zeroCOM.multiply(1 / avgArea) - COM = FreeCAD.Vector(zeroCOM.x, zeroCOM.y, 0.0) - - # get X, Y, Z spans; Compute center of rotation - deltaX = abs(xmax-xmin) - deltaY = abs(ymax-ymin) - deltaZ = abs(zmax-zmin) - deltaC = math.sqrt(deltaX**2 + deltaY**2) - lineLen = deltaC + (2.0 * self.cutter.getDiameter()) # Line length to span boundbox diag with 2x cutter diameter extra on each end - halfLL = math.ceil(lineLen / 2.0) - cutPasses = math.ceil(lineLen / self.cutOut) + 1 # Number of lines(passes) required to cover lineLen - halfPasses = math.ceil(cutPasses / 2.0) - bbC = faceShp.BoundBox.Center - - # Generate the Draft line/circle sets to be intersected with the cut-face-area - if obj.CutPattern in ['ZigZag', 'Line']: - MaxLC = -1 - centRot = FreeCAD.Vector(0.0, 0.0, 0.0) # Bottom left corner of face/selection/model - cAng = math.atan(deltaX / deltaY) # BoundaryBox angle - - # Determine end points and create top lines - x1 = centRot.x - halfLL - x2 = centRot.x + halfLL - diag = None - if obj.CutPatternAngle == 0 or obj.CutPatternAngle == 180: - MaxLC = math.floor(deltaY / self.cutOut) - diag = deltaY - elif obj.CutPatternAngle == 90 or obj.CutPatternAngle == 270: - MaxLC = math.floor(deltaX / self.cutOut) - diag = deltaX - else: - perpDist = math.cos(cAng - math.radians(obj.CutPatternAngle)) * deltaC - MaxLC = math.floor(perpDist / self.cutOut) - diag = perpDist - y1 = centRot.y + diag - # y2 = y1 - - p1 = FreeCAD.Vector(x1, y1, 0.0) - p2 = FreeCAD.Vector(x2, y1, 0.0) - topLineTuple = (p1, p2) - ny1 = centRot.y - diag - n1 = FreeCAD.Vector(x1, ny1, 0.0) - n2 = FreeCAD.Vector(x2, ny1, 0.0) - negTopLineTuple = (n1, n2) - - # Create end points for set of lines to intersect with cross-section face - pntTuples = list() - for lc in range((-1 * (halfPasses - 1)), halfPasses + 1): - # if lc == (cutPasses - MaxLC - 1): - # pntTuples.append(negTopLineTuple) - # if lc == (MaxLC + 1): - # pntTuples.append(topLineTuple) - x1 = centRot.x - halfLL - x2 = centRot.x + halfLL - y1 = centRot.y + (lc * self.cutOut) - # y2 = y1 - p1 = FreeCAD.Vector(x1, y1, 0.0) - p2 = FreeCAD.Vector(x2, y1, 0.0) - pntTuples.append( (p1, p2) ) - - # Convert end points to lines - for (p1, p2) in pntTuples: - line = Part.makeLine(p1, p2) - GeoSet.append(line) - elif obj.CutPattern in ['Circular', 'CircularZigZag']: - zTgt = faceShp.BoundBox.ZMin - axisRot = FreeCAD.Vector(0.0, 0.0, 1.0) - cntr = FreeCAD.Placement() - cntr.Rotation = FreeCAD.Rotation(axisRot, 0.0) - - if obj.CircularCenterAt == 'CenterOfMass': - cntr.Base = FreeCAD.Vector(COM.x, COM.y, zTgt) # COM # Use center of Mass - elif obj.CircularCenterAt == 'CenterOfBoundBox': - cent = faceShp.BoundBox.Center - cntr.Base = FreeCAD.Vector(cent.x, cent.y, zTgt) - elif obj.CircularCenterAt == 'XminYmin': - cntr.Base = FreeCAD.Vector(faceShp.BoundBox.XMin, faceShp.BoundBox.YMin, zTgt) - elif obj.CircularCenterAt == 'Custom': - newCent = FreeCAD.Vector(obj.CircularCenterCustom.x, obj.CircularCenterCustom.y, zTgt) - cntr.Base = newCent - - # recalculate cutPasses value, if need be - radialPasses = halfPasses - if obj.CircularCenterAt != 'CenterOfBoundBox': - # make 4 corners of boundbox in XY plane, find which is greatest distance to new circular center - EBB = faceShp.BoundBox - CORNERS = [ - FreeCAD.Vector(EBB.XMin, EBB.YMin, 0.0), - FreeCAD.Vector(EBB.XMin, EBB.YMax, 0.0), - FreeCAD.Vector(EBB.XMax, EBB.YMax, 0.0), - FreeCAD.Vector(EBB.XMax, EBB.YMin, 0.0), - ] - dMax = 0.0 - for c in range(0, 4): - dist = CORNERS[c].sub(cntr.Base).Length - if dist > dMax: - dMax = dist - lineLen = dMax + (2.0 * self.cutter.getDiameter()) # Line length to span boundbox diag with 2x cutter diameter extra on each end - radialPasses = math.ceil(lineLen / self.cutOut) + 1 # Number of lines(passes) required to cover lineLen - - # Update COM point and current CircularCenter - if obj.CircularCenterAt != 'Custom': - obj.CircularCenterCustom = cntr.Base - - minRad = self.cutter.getDiameter() * 0.45 - siX3 = 3 * obj.SampleInterval.Value - minRadSI = (siX3 / 2.0) / math.pi - if minRad < minRadSI: - minRad = minRadSI - - # Make small center circle to start pattern - if obj.StepOver > 50: - circle = Part.makeCircle(minRad, cntr.Base) - GeoSet.append(circle) - - for lc in range(1, radialPasses + 1): - rad = (lc * self.cutOut) - if rad >= minRad: - circle = Part.makeCircle(rad, cntr.Base) - GeoSet.append(circle) - # Efor - COM = cntr.Base - # Eif - - if obj.CutPatternReversed is True: - GeoSet.reverse() - - if faceShp.BoundBox.ZMin != 0.0: - faceShp.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - faceShp.BoundBox.ZMin)) - - # Create compound object to bind all lines in Lineset - geomShape = Part.makeCompound(GeoSet) - - # Position and rotate the Line and ZigZag geometry - if obj.CutPattern in ['Line', 'ZigZag']: - if obj.CutPatternAngle != 0.0: - geomShape.Placement.Rotation = FreeCAD.Rotation(FreeCAD.Vector(0, 0, 1), obj.CutPatternAngle) - geomShape.Placement.Base = FreeCAD.Vector(bbC.x, bbC.y, 0.0 - geomShape.BoundBox.ZMin) - - if self.showDebugObjects is True: - F = FreeCAD.ActiveDocument.addObject('Part::Feature','tmpGeometrySet') - F.Shape = geomShape - F.purgeTouched() - self.tempGroup.addObject(F) - - # Identify intersection of cross-section face and lineset - cmnShape = faceShp.common(geomShape) - - if self.showDebugObjects is True: - F = FreeCAD.ActiveDocument.addObject('Part::Feature','tmpPathGeometry') - F.Shape = cmnShape - F.purgeTouched() - self.tempGroup.addObject(F) - - self.tmpCOM = FreeCAD.Vector(COM.x, COM.y, faceShp.BoundBox.ZMin) - return cmnShape - - def _planarMakeProfileGeom(self, obj, subShp): - PathLog.debug('_planarMakeProfileGeom()') - - offsetLists = list() - dist = obj.SampleInterval.Value / 5.0 - defl = obj.SampleInterval.Value / 5.0 - - # Reference https://forum.freecadweb.org/viewtopic.php?t=28861#p234939 - for fc in subShp.Faces: - # Reverse order of wires in each face - inside to outside - for w in range(len(fc.Wires) - 1, -1, -1): - W = fc.Wires[w] - PNTS = W.discretize(Distance=dist) - # PNTS = W.discretize(Deflection=defl) - if self.CutClimb is True: - PNTS.reverse() - offsetLists.append(PNTS) - - return offsetLists - - def _planarPerformOclScan(self, obj, pdc, pathGeom, offsetPoints=False): - '''_planarPerformOclScan(obj, pdc, pathGeom, offsetPoints=False)... - Switching function for calling the appropriate path-geometry to OCL points conversion function - for the various cut patterns.''' - PathLog.debug('_planarPerformOclScan()') - SCANS = list() - - if offsetPoints is True: - PNTSET = self._pathGeomToOffsetPointSet(obj, pathGeom) - for D in PNTSET: - stpOvr = list() - ofst = list() - for I in D: - if I == 'BRK': - stpOvr.append(ofst) - stpOvr.append(I) - ofst = list() - else: - # D format is ((p1, p2), (p3, p4)) - (A, B) = I - ofst.extend(self._planarDropCutScan(pdc, A, B)) - if len(ofst) > 0: - stpOvr.append(ofst) - SCANS.extend(stpOvr) - elif obj.CutPattern == 'Line': - stpOvr = list() - PNTSET = self._pathGeomToLinesPointSet(obj, pathGeom) - for D in PNTSET: - for I in D: - if I == 'BRK': - stpOvr.append(I) - else: - # D format is ((p1, p2), (p3, p4)) - (A, B) = I - stpOvr.append(self._planarDropCutScan(pdc, A, B)) - SCANS.append(stpOvr) - stpOvr = list() - elif obj.CutPattern == 'ZigZag': - stpOvr = list() - PNTSET = self._pathGeomToZigzagPointSet(obj, pathGeom) - for (dirFlg, LNS) in PNTSET: - for SEG in LNS: - if SEG == 'BRK': - stpOvr.append(SEG) - else: - # D format is ((p1, p2), (p3, p4)) - (A, B) = SEG - stpOvr.append(self._planarDropCutScan(pdc, A, B)) - SCANS.append(stpOvr) - stpOvr = list() - elif obj.CutPattern in ['Circular', 'CircularZigZag']: - # PNTSET is list, by stepover. - # Each stepover is a list containing arc/loop descriptions, (sp, ep, cp) - PNTSET = self._pathGeomToArcPointSet(obj, pathGeom) - - for so in range(0, len(PNTSET)): - stpOvr = list() - erFlg = False - (aTyp, dirFlg, ARCS) = PNTSET[so] - - if dirFlg == 1: # 1 - cMode = True - else: - cMode = False - - for a in range(0, len(ARCS)): - Arc = ARCS[a] - if Arc == 'BRK': - stpOvr.append('BRK') - else: - scan = self._planarCircularDropCutScan(pdc, Arc, cMode) - if scan is False: - erFlg = True - else: - if aTyp == 'L': - scan.append(FreeCAD.Vector(scan[0].x, scan[0].y, scan[0].z)) - stpOvr.append(scan) - if erFlg is False: - SCANS.append(stpOvr) - - return SCANS - - def _pathGeomToOffsetPointSet(self, obj, compGeoShp): - '''_pathGeomToOffsetPointSet(obj, compGeoShp)... - Convert a compound set of 3D profile segmented wires to 2D segments, applying linear optimization.''' - PathLog.debug('_pathGeomToOffsetPointSet()') - - LINES = list() - optimize = obj.OptimizeLinearPaths - ofstCnt = len(compGeoShp) - - # Cycle through offeset loops - for ei in range(0, ofstCnt): - OS = compGeoShp[ei] - lenOS = len(OS) - - if ei > 0: - LINES.append('BRK') - - fp = FreeCAD.Vector(OS[0].x, OS[0].y, OS[0].z) - OS.append(fp) - - # Cycle through points in each loop - prev = OS[0] - pnt = OS[1] - for v in range(1, lenOS): - nxt = OS[v + 1] - if optimize is True: - iPOL = prev.isOnLineSegment(nxt, pnt) - if iPOL is True: - pnt = nxt - else: - tup = ((prev.x, prev.y), (pnt.x, pnt.y)) - LINES.append(tup) - prev = pnt - pnt = nxt - else: - tup = ((prev.x, prev.y), (pnt.x, pnt.y)) - LINES.append(tup) - prev = pnt - pnt = nxt - if iPOL is True: - tup = ((prev.x, prev.y), (pnt.x, pnt.y)) - LINES.append(tup) - # Efor - - return [LINES] - - def _pathGeomToLinesPointSet(self, obj, compGeoShp): - '''_pathGeomToLinesPointSet(obj, compGeoShp)... - Convert a compound set of sequential line segments to directionally-oriented collinear groupings.''' - PathLog.debug('_pathGeomToLinesPointSet()') - # Extract intersection line segments for return value as list() - LINES = list() - inLine = list() - chkGap = False - lnCnt = 0 - ec = len(compGeoShp.Edges) - cutClimb = self.CutClimb - toolDiam = 2.0 * self.radius - cpa = obj.CutPatternAngle - - edg0 = compGeoShp.Edges[0] - p1 = (edg0.Vertexes[0].X, edg0.Vertexes[0].Y) - p2 = (edg0.Vertexes[1].X, edg0.Vertexes[1].Y) - if cutClimb is True: - tup = (p2, p1) - lst = FreeCAD.Vector(p1[0], p1[1], 0.0) - else: - tup = (p1, p2) - lst = FreeCAD.Vector(p2[0], p2[1], 0.0) - inLine.append(tup) - sp = FreeCAD.Vector(p1[0], p1[1], 0.0) # start point - - for ei in range(1, ec): - chkGap = False - edg = compGeoShp.Edges[ei] # Get edge for vertexes - v1 = (edg.Vertexes[0].X, edg.Vertexes[0].Y) # vertex 0 - v2 = (edg.Vertexes[1].X, edg.Vertexes[1].Y) # vertex 1 - - ep = FreeCAD.Vector(v2[0], v2[1], 0.0) # end point - cp = FreeCAD.Vector(v1[0], v1[1], 0.0) # check point (first / middle point) - iC = sp.isOnLineSegment(ep, cp) - if iC is True: - inLine.append('BRK') - chkGap = True - else: - if cutClimb is True: - inLine.reverse() - LINES.append(inLine) # Save inLine segments - lnCnt += 1 - inLine = list() # reset collinear container - if cutClimb is True: - sp = cp # FreeCAD.Vector(v1[0], v1[1], 0.0) - else: - sp = ep - - if cutClimb is True: - tup = (v2, v1) - if chkGap is True: - gap = abs(toolDiam - lst.sub(ep).Length) - lst = cp - else: - tup = (v1, v2) - if chkGap is True: - gap = abs(toolDiam - lst.sub(cp).Length) - lst = ep - - if chkGap is True: - if gap < obj.GapThreshold.Value: - b = inLine.pop() # pop off 'BRK' marker - (vA, vB) = inLine.pop() # pop off previous line segment for combining with current - tup = (vA, tup[1]) - self.closedGap = True - else: - # PathLog.debug('---- Gap: {} mm'.format(gap)) - gap = round(gap, 6) - if gap < self.gaps[0]: - self.gaps.insert(0, gap) - self.gaps.pop() - inLine.append(tup) - # Efor - lnCnt += 1 - if cutClimb is True: - inLine.reverse() - LINES.append(inLine) # Save inLine segments - - # Handle last inLine set, reversing it. - if obj.CutPatternReversed is True: - if cpa != 0.0 and cpa % 90.0 == 0.0: - F = LINES.pop(0) - rev = list() - for iL in F: - if iL == 'BRK': - rev.append(iL) - else: - (p1, p2) = iL - rev.append((p2, p1)) - rev.reverse() - LINES.insert(0, rev) - - isEven = lnCnt % 2 - if isEven == 0: - PathLog.debug('Line count is ODD.') - else: - PathLog.debug('Line count is even.') - - return LINES - - def _pathGeomToZigzagPointSet(self, obj, compGeoShp): - '''_pathGeomToZigzagPointSet(obj, compGeoShp)... - Convert a compound set of sequential line segments to directionally-oriented collinear groupings - with a ZigZag directional indicator included for each collinear group.''' - PathLog.debug('_pathGeomToZigzagPointSet()') - # Extract intersection line segments for return value as list() - LINES = list() - inLine = list() - lnCnt = 0 - chkGap = False - ec = len(compGeoShp.Edges) - toolDiam = 2.0 * self.radius - - if self.CutClimb is True: - dirFlg = -1 - else: - dirFlg = 1 - - edg0 = compGeoShp.Edges[0] - p1 = (edg0.Vertexes[0].X, edg0.Vertexes[0].Y) - p2 = (edg0.Vertexes[1].X, edg0.Vertexes[1].Y) - if dirFlg == 1: - tup = (p1, p2) - lst = FreeCAD.Vector(p2[0], p2[1], 0.0) - sp = FreeCAD.Vector(p1[0], p1[1], 0.0) # start point - else: - tup = (p2, p1) - lst = FreeCAD.Vector(p1[0], p1[1], 0.0) - sp = FreeCAD.Vector(p2[0], p2[1], 0.0) # start point - inLine.append(tup) - otr = lst - - for ei in range(1, ec): - edg = compGeoShp.Edges[ei] - v1 = (edg.Vertexes[0].X, edg.Vertexes[0].Y) - v2 = (edg.Vertexes[1].X, edg.Vertexes[1].Y) - - cp = FreeCAD.Vector(v1[0], v1[1], 0.0) # check point (start point of segment) - ep = FreeCAD.Vector(v2[0], v2[1], 0.0) # end point - iC = sp.isOnLineSegment(ep, cp) - if iC is True: - inLine.append('BRK') - chkGap = True - gap = abs(toolDiam - lst.sub(cp).Length) - else: - chkGap = False - if dirFlg == -1: - inLine.reverse() - LINES.append((dirFlg, inLine)) - lnCnt += 1 - dirFlg = -1 * dirFlg # Change zig to zag - inLine = list() # reset collinear container - sp = cp # FreeCAD.Vector(v1[0], v1[1], 0.0) - otr = ep - - lst = ep - if dirFlg == 1: - tup = (v1, v2) - else: - tup = (v2, v1) - - if chkGap is True: - if gap < obj.GapThreshold.Value: - b = inLine.pop() # pop off 'BRK' marker - (vA, vB) = inLine.pop() # pop off previous line segment for combining with current - if dirFlg == 1: - tup = (vA, tup[1]) - else: - #tup = (vA, tup[1]) - #tup = (tup[1], vA) - tup = (tup[0], vB) - self.closedGap = True - else: - gap = round(gap, 6) - if gap < self.gaps[0]: - self.gaps.insert(0, gap) - self.gaps.pop() - inLine.append(tup) - # Efor - lnCnt += 1 - - # Fix directional issue with LAST line when line count is even - isEven = lnCnt % 2 - if isEven == 0: # Changed to != with 90 degree CutPatternAngle - PathLog.debug('Line count is even.') - else: - PathLog.debug('Line count is ODD.') - dirFlg = -1 * dirFlg - if obj.CutPatternReversed is False: - if self.CutClimb is True: - dirFlg = -1 * dirFlg - - if obj.CutPatternReversed is True: - dirFlg = -1 * dirFlg - - # Handle last inLine list - if dirFlg == 1: - rev = list() - for iL in inLine: - if iL == 'BRK': - rev.append(iL) - else: - (p1, p2) = iL - rev.append((p2, p1)) - - if obj.CutPatternReversed is False: - rev.reverse() - else: - rev2 = list() - for iL in rev: - if iL == 'BRK': - rev2.append(iL) - else: - (p1, p2) = iL - rev2.append((p2, p1)) - rev2.reverse() - rev = rev2 - - LINES.append((dirFlg, rev)) - else: - LINES.append((dirFlg, inLine)) - - return LINES - - def _pathGeomToArcPointSet(self, obj, compGeoShp): - '''_pathGeomToArcPointSet(obj, compGeoShp)... - Convert a compound set of arcs/circles to a set of directionally-oriented arc end points - and the corresponding center point.''' - # Extract intersection line segments for return value as list() - PathLog.debug('_pathGeomToArcPointSet()') - ARCS = list() - stpOvrEI = list() - segEI = list() - isSame = False - sameRad = None - COM = self.tmpCOM - toolDiam = 2.0 * self.radius - ec = len(compGeoShp.Edges) - - def gapDist(sp, ep): - X = (ep[0] - sp[0])**2 - Y = (ep[1] - sp[1])**2 - Z = (ep[2] - sp[2])**2 - # return math.sqrt(X + Y + Z) - return math.sqrt(X + Y) # the 'z' value is zero in both points - - # Separate arc data into Loops and Arcs - for ei in range(0, ec): - edg = compGeoShp.Edges[ei] - if edg.Closed is True: - stpOvrEI.append(('L', ei, False)) - else: - if isSame is False: - segEI.append(ei) - isSame = True - pnt = FreeCAD.Vector(edg.Vertexes[0].X, edg.Vertexes[0].Y, 0.0) - sameRad = pnt.sub(COM).Length - else: - # Check if arc is co-radial to current SEGS - pnt = FreeCAD.Vector(edg.Vertexes[0].X, edg.Vertexes[0].Y, 0.0) - if abs(sameRad - pnt.sub(COM).Length) > 0.00001: - isSame = False - - if isSame is True: - segEI.append(ei) - else: - # Move co-radial arc segments - stpOvrEI.append(['A', segEI, False]) - # Start new list of arc segments - segEI = [ei] - isSame = True - pnt = FreeCAD.Vector(edg.Vertexes[0].X, edg.Vertexes[0].Y, 0.0) - sameRad = pnt.sub(COM).Length - # Process trailing `segEI` data, if available - if isSame is True: - stpOvrEI.append(['A', segEI, False]) - - # Identify adjacent arcs with y=0 start/end points that connect - for so in range(0, len(stpOvrEI)): - SO = stpOvrEI[so] - if SO[0] == 'A': - startOnAxis = list() - endOnAxis = list() - EI = SO[1] # list of corresponding compGeoShp.Edges indexes - - # Identify startOnAxis and endOnAxis arcs - for i in range(0, len(EI)): - ei = EI[i] # edge index - E = compGeoShp.Edges[ei] # edge object - if abs(COM.y - E.Vertexes[0].Y) < 0.00001: - startOnAxis.append((i, ei, E.Vertexes[0])) - elif abs(COM.y - E.Vertexes[1].Y) < 0.00001: - endOnAxis.append((i, ei, E.Vertexes[1])) - - # Look for connections between startOnAxis and endOnAxis arcs. Consolidate data when connected - lenSOA = len(startOnAxis) - lenEOA = len(endOnAxis) - if lenSOA > 0 and lenEOA > 0: - delIdxs = list() - lstFindIdx = 0 - for soa in range(0, lenSOA): - (iS, eiS, vS) = startOnAxis[soa] - for eoa in range(0, len(endOnAxis)): - (iE, eiE, vE) = endOnAxis[eoa] - dist = vE.X - vS.X - if abs(dist) < 0.00001: # They connect on axis at same radius - SO[2] = (eiE, eiS) - break - elif dist > 0: - break # stop searching - # Eif - # Eif - # Efor - - # Construct arc data tuples for OCL - dirFlg = 1 - # cutPat = obj.CutPattern - if self.CutClimb is False: # True yields Climb when set to Conventional - dirFlg = -1 - - # Cycle through stepOver data - for so in range(0, len(stpOvrEI)): - SO = stpOvrEI[so] - if SO[0] == 'L': # L = Loop/Ring/Circle - lei = SO[1] # loop Edges index - v1 = compGeoShp.Edges[lei].Vertexes[0] - - space = obj.SampleInterval.Value / 2.0 - - p1 = FreeCAD.Vector(v1.X, v1.Y, v1.Z) - sp = (v1.X, v1.Y, 0.0) - rad = p1.sub(COM).Length - spcRadRatio = space/rad - if spcRadRatio < 1.0: - tolrncAng = math.asin(spcRadRatio) - else: - tolrncAng = 0.999998 * math.pi - X = COM.x + (rad * math.cos(tolrncAng)) - Y = v1.Y - space # rad * math.sin(tolrncAng) - - sp = (v1.X, v1.Y, 0.0) - ep = (X, Y, 0.0) - cp = (COM.x, COM.y, 0.0) - if dirFlg == 1: - arc = (sp, ep, cp) - else: - arc = (ep, sp, cp) # OCL.Arc(firstPnt, lastPnt, centerPnt, dir=True(CCW direction)) - ARCS.append(('L', dirFlg, [arc])) - else: # SO[0] == 'A' A = Arc - PRTS = list() - EI = SO[1] # list of corresponding Edges indexes - CONN = SO[2] # list of corresponding connected edges tuples (iE, iS) - chkGap = False - lst = None - - if CONN is not False: - (iE, iS) = CONN - v1 = compGeoShp.Edges[iE].Vertexes[0] - v2 = compGeoShp.Edges[iS].Vertexes[1] - sp = (v1.X, v1.Y, 0.0) - ep = (v2.X, v2.Y, 0.0) - cp = (COM.x, COM.y, 0.0) - if dirFlg == 1: - arc = (sp, ep, cp) - lst = ep - else: - arc = (ep, sp, cp) # OCL.Arc(firstPnt, lastPnt, centerPnt, dir=True(CCW direction)) - lst = sp - PRTS.append(arc) - # Pop connected edge index values from arc segments index list - iEi = EI.index(iE) - iSi = EI.index(iS) - if iEi > iSi: - EI.pop(iEi) - EI.pop(iSi) - else: - EI.pop(iSi) - EI.pop(iEi) - if len(EI) > 0: - PRTS.append('BRK') - chkGap = True - cnt = 0 - for ei in EI: - if cnt > 0: - PRTS.append('BRK') - chkGap = True - v1 = compGeoShp.Edges[ei].Vertexes[0] - v2 = compGeoShp.Edges[ei].Vertexes[1] - sp = (v1.X, v1.Y, 0.0) - ep = (v2.X, v2.Y, 0.0) - cp = (COM.x, COM.y, 0.0) - if dirFlg == 1: - arc = (sp, ep, cp) - if chkGap is True: - gap = abs(toolDiam - gapDist(lst, sp)) # abs(toolDiam - lst.sub(sp).Length) - lst = ep - else: - arc = (ep, sp, cp) # OCL.Arc(firstPnt, lastPnt, centerPnt, dir=True(CCW direction)) - if chkGap is True: - gap = abs(toolDiam - gapDist(lst, ep)) # abs(toolDiam - lst.sub(ep).Length) - lst = sp - if chkGap is True: - if gap < obj.GapThreshold.Value: - b = PRTS.pop() # pop off 'BRK' marker - (vA, vB, vC) = PRTS.pop() # pop off previous arc segment for combining with current - arc = (vA, arc[1], vC) - self.closedGap = True - else: - # PathLog.debug('---- Gap: {} mm'.format(gap)) - gap = round(gap, 6) - if gap < self.gaps[0]: - self.gaps.insert(0, gap) - self.gaps.pop() - PRTS.append(arc) - cnt += 1 - - if dirFlg == -1: - PRTS.reverse() - - ARCS.append(('A', dirFlg, PRTS)) - # Eif - if obj.CutPattern == 'CircularZigZag': - dirFlg = -1 * dirFlg - # Efor - - return ARCS - - def _planarDropCutScan(self, pdc, A, B): - #PNTS = list() - (x1, y1) = A - (x2, y2) = B - path = ocl.Path() # create an empty path object - p1 = ocl.Point(x1, y1, 0) # start-point of line - p2 = ocl.Point(x2, y2, 0) # end-point of line - lo = ocl.Line(p1, p2) # line-object - path.append(lo) # add the line to the path - pdc.setPath(path) - pdc.run() # run dropcutter algorithm on path - CLP = pdc.getCLPoints() - PNTS = [FreeCAD.Vector(p.x, p.y, p.z) for p in CLP] - return PNTS # pdc.getCLPoints() - - def _planarCircularDropCutScan(self, pdc, Arc, cMode): - PNTS = list() - path = ocl.Path() # create an empty path object - (sp, ep, cp) = Arc - - # process list of segment tuples (vect, vect) - path = ocl.Path() # create an empty path object - p1 = ocl.Point(sp[0], sp[1], 0) # start point of arc - p2 = ocl.Point(ep[0], ep[1], 0) # end point of arc - C = ocl.Point(cp[0], cp[1], 0) # center point of arc - ao = ocl.Arc(p1, p2, C, cMode) # arc object - path.append(ao) # add the arc to the path - pdc.setPath(path) - pdc.run() # run dropcutter algorithm on path - CLP = pdc.getCLPoints() - - # Convert OCL object data to FreeCAD vectors - for p in CLP: - PNTS.append(FreeCAD.Vector(p.x, p.y, p.z)) - - return PNTS - - # Main planar scan functions - def _planarDropCutSingle(self, JOB, obj, pdc, safePDC, depthparams, SCANDATA): - PathLog.debug('_planarDropCutSingle()') - - GCODE = [Path.Command('N (Beginning of Single-pass layer.)', {})] - tolrnc = JOB.GeometryTolerance.Value - prevDepth = obj.SafeHeight.Value - lenDP = len(depthparams) - lenSCANDATA = len(SCANDATA) - gDIR = ['G3', 'G2'] - - if self.CutClimb is True: - gDIR = ['G2', 'G3'] - - # Set `ProfileEdges` specific trigger indexes - peIdx = lenSCANDATA # off by default - if obj.ProfileEdges == 'Only': - peIdx = -1 - elif obj.ProfileEdges == 'First': - peIdx = 0 - elif obj.ProfileEdges == 'Last': - peIdx = lenSCANDATA - 1 - - # Send cutter to x,y position of first point on first line - first = SCANDATA[0][0][0] # [step][item][point] - GCODE.append(Path.Command('G0', {'X': first.x, 'Y': first.y, 'F': self.horizRapid})) - - # Cycle through step-over sections (line segments or arcs) - odd = True - lstStpEnd = None - prevDepth = obj.SafeHeight.Value # Not used for Single-pass - for so in range(0, lenSCANDATA): - cmds = list() - PRTS = SCANDATA[so] - lenPRTS = len(PRTS) - first = PRTS[0][0] # first point of arc/line stepover group - start = PRTS[0][0] # will change with each line/arc segment - last = None - cmds.append(Path.Command('N (Begin step {}.)'.format(so), {})) - - if so > 0: - if obj.CutPattern == 'CircularZigZag': - if odd is True: - odd = False - else: - odd = True - minTrnsHght = self._getMinSafeTravelHeight(safePDC, lstStpEnd, first) # Check safe travel height against fullSTL - # cmds.append(Path.Command('N (Transition: last, first: {}, {}: minSTH: {})'.format(lstStpEnd, first, minTrnsHght), {})) - cmds.extend(self._stepTransitionCmds(obj, lstStpEnd, first, minTrnsHght, tolrnc)) - - # Override default `OptimizeLinearPaths` behavior to allow `ProfileEdges` optimization - if so == peIdx or peIdx == -1: - obj.OptimizeLinearPaths = self.preOLP - - # Cycle through current step-over parts - for i in range(0, lenPRTS): - prt = PRTS[i] - lenPrt = len(prt) - if prt == 'BRK': - nxtStart = PRTS[i + 1][0] - minSTH = self._getMinSafeTravelHeight(safePDC, last, nxtStart) # Check safe travel height against fullSTL - cmds.append(Path.Command('N (Break)', {})) - cmds.extend(self._breakCmds(obj, last, nxtStart, minSTH, tolrnc)) - else: - cmds.append(Path.Command('N (part {}.)'.format(i + 1), {})) - start = prt[0] - last = prt[lenPrt - 1] - if so == peIdx or peIdx == -1: - cmds.extend(self._planarSinglepassProcess(obj, prt)) - elif obj.CutPattern in ['Circular', 'CircularZigZag'] and obj.CircularUseG2G3 is True and lenPrt > 2: - (rtnVal, gcode) = self._arcsToG2G3(prt, lenPrt, odd, gDIR, tolrnc) - if rtnVal is True: - cmds.extend(gcode) - else: - cmds.extend(self._planarSinglepassProcess(obj, prt)) - else: - cmds.extend(self._planarSinglepassProcess(obj, prt)) - cmds.append(Path.Command('N (End of step {}.)'.format(so), {})) - GCODE.extend(cmds) # save line commands - lstStpEnd = last - - # Return `OptimizeLinearPaths` to disabled - if so == peIdx or peIdx == -1: - if obj.CutPattern in ['Circular', 'CircularZigZag']: - obj.OptimizeLinearPaths = False - # Efor - - return GCODE - - def _planarSinglepassProcess(self, obj, PNTS): - if obj.OptimizeLinearPaths: - # first item will be compared to the last point, but I think that should work - output = [Path.Command('G1', {'X': PNTS[i].x, 'Y': PNTS[i].y, 'Z': PNTS[i].z, 'F': self.horizFeed}) - for i in range(0, len(PNTS) - 1) - if not PNTS[i].isOnLineSegment(PNTS[i -1],PNTS[i + 1])] - output.append(Path.Command('G1', {'X': PNTS[-1].x, 'Y': PNTS[-1].y, 'Z': PNTS[-1].z, 'F': self.horizFeed})) - else: - output = [Path.Command('G1', {'X': pnt.x, 'Y': pnt.y, 'Z': pnt.z, 'F': self.horizFeed}) for pnt in PNTS] - - return output - - def _planarDropCutMulti(self, JOB, obj, pdc, safePDC, depthparams, SCANDATA): - GCODE = [Path.Command('N (Beginning of Multi-pass layers.)', {})] - tolrnc = JOB.GeometryTolerance.Value - lenDP = len(depthparams) - prevDepth = depthparams[0] - lenSCANDATA = len(SCANDATA) - gDIR = ['G3', 'G2'] - - if self.CutClimb is True: - gDIR = ['G2', 'G3'] - - # Set `ProfileEdges` specific trigger indexes - peIdx = lenSCANDATA # off by default - if obj.ProfileEdges == 'Only': - peIdx = -1 - elif obj.ProfileEdges == 'First': - peIdx = 0 - elif obj.ProfileEdges == 'Last': - peIdx = lenSCANDATA - 1 - - # Process each layer in depthparams - prvLyrFirst = None - prvLyrLast = None - lastPrvStpLast = None - actvLyrs = 0 - for lyr in range(0, lenDP): - odd = True # ZigZag directional switch - lyrHasCmds = False - lstStpEnd = None - actvSteps = 0 - LYR = list() - prvStpFirst = None - if lyr > 0: - if prvStpLast is not None: - lastPrvStpLast = prvStpLast - prvStpLast = None - lyrDep = depthparams[lyr] - PathLog.debug('Multi-pass lyrDep: {}'.format(round(lyrDep, 4))) - - # Cycle through step-over sections (line segments or arcs) - for so in range(0, len(SCANDATA)): - SO = SCANDATA[so] - lenSO = len(SO) - - # Pre-process step-over parts for layer depth and holds - ADJPRTS = list() - LMAX = list() - soHasPnts = False - brkFlg = False - for i in range(0, lenSO): - prt = SO[i] - lenPrt = len(prt) - if prt == 'BRK': - if brkFlg is True: - ADJPRTS.append(prt) - LMAX.append(prt) - brkFlg = False - else: - (PTS, lMax) = self._planarMultipassPreProcess(obj, prt, prevDepth, lyrDep) - if len(PTS) > 0: - ADJPRTS.append(PTS) - soHasPnts = True - brkFlg = True - LMAX.append(lMax) - # Efor - lenAdjPrts = len(ADJPRTS) - - # Process existing parts within current step over - prtsHasCmds = False - stepHasCmds = False - prtsCmds = list() - stpOvrCmds = list() - transCmds = list() - if soHasPnts is True: - first = ADJPRTS[0][0] # first point of arc/line stepover group - - # Manage step over transition and CircularZigZag direction - if so > 0: - # PathLog.debug(' stepover index: {}'.format(so)) - # Control ZigZag direction - if obj.CutPattern == 'CircularZigZag': - if odd is True: - odd = False - else: - odd = True - # Control step over transition - if prvStpLast is None: - prvStpLast = lastPrvStpLast - minTrnsHght = self._getMinSafeTravelHeight(safePDC, prvStpLast, first, minDep=None) # Check safe travel height against fullSTL - transCmds.append(Path.Command('N (--Step {} transition)'.format(so), {})) - transCmds.extend(self._stepTransitionCmds(obj, prvStpLast, first, minTrnsHght, tolrnc)) - - # Override default `OptimizeLinearPaths` behavior to allow `ProfileEdges` optimization - if so == peIdx or peIdx == -1: - obj.OptimizeLinearPaths = self.preOLP - - # Cycle through current step-over parts - for i in range(0, lenAdjPrts): - prt = ADJPRTS[i] - lenPrt = len(prt) - # PathLog.debug(' adj parts index - lenPrt: {} - {}'.format(i, lenPrt)) - if prt == 'BRK' and prtsHasCmds is True: - nxtStart = ADJPRTS[i + 1][0] - minSTH = self._getMinSafeTravelHeight(safePDC, last, nxtStart, minDep=None) # Check safe travel height against fullSTL - prtsCmds.append(Path.Command('N (--Break)', {})) - prtsCmds.extend(self._breakCmds(obj, last, nxtStart, minSTH, tolrnc)) - else: - segCmds = False - prtsCmds.append(Path.Command('N (part {})'.format(i + 1), {})) - last = prt[lenPrt - 1] - if so == peIdx or peIdx == -1: - segCmds = self._planarSinglepassProcess(obj, prt) - elif obj.CutPattern in ['Circular', 'CircularZigZag'] and obj.CircularUseG2G3 is True and lenPrt > 2: - (rtnVal, gcode) = self._arcsToG2G3(prt, lenPrt, odd, gDIR, tolrnc) - if rtnVal is True: - segCmds = gcode - else: - segCmds = self._planarSinglepassProcess(obj, prt) - else: - segCmds = self._planarSinglepassProcess(obj, prt) - - if segCmds is not False: - prtsCmds.extend(segCmds) - prtsHasCmds = True - prvStpLast = last - # Eif - # Efor - # Eif - - # Return `OptimizeLinearPaths` to disabled - if so == peIdx or peIdx == -1: - if obj.CutPattern in ['Circular', 'CircularZigZag']: - obj.OptimizeLinearPaths = False - - # Compile step over(prts) commands - if prtsHasCmds is True: - stepHasCmds = True - actvSteps += 1 - prvStpFirst = first - stpOvrCmds.extend(transCmds) - stpOvrCmds.append(Path.Command('N (Begin step {}.)'.format(so), {})) - stpOvrCmds.append(Path.Command('G0', {'X': first.x, 'Y': first.y, 'F': self.horizRapid})) - stpOvrCmds.extend(prtsCmds) - stpOvrCmds.append(Path.Command('N (End of step {}.)'.format(so), {})) - - # Layer transition at first active step over in current layer - if actvSteps == 1: - prvLyrFirst = first - LYR.append(Path.Command('N (Layer {} begins)'.format(lyr), {})) - if lyr > 0: - LYR.append(Path.Command('N (Layer transition)', {})) - LYR.append(Path.Command('G0', {'Z': obj.SafeHeight.Value, 'F': self.vertRapid})) - LYR.append(Path.Command('G0', {'X': first.x, 'Y': first.y, 'F': self.horizRapid})) - - if stepHasCmds is True: - lyrHasCmds = True - LYR.extend(stpOvrCmds) - # Eif - - # Close layer, saving commands, if any - if lyrHasCmds is True: - prvLyrLast = last - GCODE.extend(LYR) # save line commands - GCODE.append(Path.Command('N (End of layer {})'.format(lyr), {})) - - # Set previous depth - prevDepth = lyrDep - # Efor - - PathLog.debug('Multi-pass op has {} layers (step downs).'.format(lyr + 1)) - - return GCODE - - def _planarMultipassPreProcess(self, obj, LN, prvDep, layDep): - ALL = list() - PTS = list() - brkFlg = False - optLinTrans = obj.OptimizeStepOverTransitions - safe = math.ceil(obj.SafeHeight.Value) - - if optLinTrans is True: - for P in LN: - ALL.append(P) - # Handle layer depth AND hold points - if P.z <= layDep: - PTS.append(FreeCAD.Vector(P.x, P.y, layDep)) - elif P.z > prvDep: - PTS.append(FreeCAD.Vector(P.x, P.y, safe)) - else: - PTS.append(FreeCAD.Vector(P.x, P.y, P.z)) - # Efor - else: - for P in LN: - ALL.append(P) - # Handle layer depth only - if P.z <= layDep: - PTS.append(FreeCAD.Vector(P.x, P.y, layDep)) - else: - PTS.append(FreeCAD.Vector(P.x, P.y, P.z)) - # Efor - - if optLinTrans is True: - # Remove leading and trailing Hold Points - popList = list() - for i in range(0, len(PTS)): # identify leading string - if PTS[i].z == safe: - popList.append(i) - else: - break - popList.sort(reverse=True) - for p in popList: # Remove hold points - PTS.pop(p) - ALL.pop(p) - popList = list() - for i in range(len(PTS) - 1, -1, -1): # identify trailing string - if PTS[i].z == safe: - popList.append(i) - else: - break - popList.sort(reverse=True) - for p in popList: # Remove hold points - PTS.pop(p) - ALL.pop(p) - - # Determine max Z height for remaining points on line - lMax = obj.FinalDepth.Value - if len(ALL) > 0: - lMax = ALL[0].z - for P in ALL: - if P.z > lMax: - lMax = P.z - - return (PTS, lMax) - - def _planarMultipassProcess(self, obj, PNTS, lMax): - output = list() - optimize = obj.OptimizeLinearPaths - safe = math.ceil(obj.SafeHeight.Value) - lenPNTS = len(PNTS) - lastPNTS = lenPNTS - 1 - prcs = True - onHold = False - onLine = False - clrScnLn = lMax + 2.0 - - # Initialize first three points - nxt = None - pnt = PNTS[0] - prev = FreeCAD.Vector(-442064564.6, 258539656553.27, 3538553425.847) - - # Add temp end point - PNTS.append(FreeCAD.Vector(-4895747464.6, -25855763553.2, 35865763425)) - - # Begin processing ocl points list into gcode - for i in range(0, lenPNTS): - prcs = True - nxt = PNTS[i + 1] - - if pnt.z == safe: - prcs = False - if onHold is False: - onHold = True - output.append( Path.Command('N (Start hold)', {}) ) - output.append( Path.Command('G0', {'Z': clrScnLn, 'F': self.vertRapid}) ) - else: - if onHold is True: - onHold = False - output.append( Path.Command('N (End hold)', {}) ) - output.append( Path.Command('G0', {'X': pnt.x, 'Y': pnt.y, 'F': self.horizRapid}) ) - - # Process point - if prcs is True: - if optimize is True: - iPOL = prev.isOnLineSegment(nxt, pnt) - if iPOL is True: - onLine = True - else: - onLine = False - output.append(Path.Command('G1', {'X': pnt.x, 'Y': pnt.y, 'Z': pnt.z, 'F': self.horizFeed})) - else: - output.append(Path.Command('G1', {'X': pnt.x, 'Y': pnt.y, 'Z': pnt.z, 'F': self.horizFeed})) - - # Rotate point data - if onLine is False: - prev = pnt - pnt = nxt - # Efor - - temp = PNTS.pop() # Remove temp end point - - return output - - def _stepTransitionCmds(self, obj, lstPnt, first, minSTH, tolrnc): - cmds = list() - rtpd = False - horizGC = 'G0' - hSpeed = self.horizRapid - height = obj.SafeHeight.Value - - if obj.CutPattern in ['Line', 'Circular']: - if obj.OptimizeStepOverTransitions is True: - height = minSTH + 2.0 - # if obj.LayerMode == 'Multi-pass': - # rtpd = minSTH - elif obj.CutPattern in ['ZigZag', 'CircularZigZag']: - if obj.OptimizeStepOverTransitions is True: - zChng = first.z - lstPnt.z - # PathLog.debug('first.z: {}'.format(first.z)) - # PathLog.debug('lstPnt.z: {}'.format(lstPnt.z)) - # PathLog.debug('zChng: {}'.format(zChng)) - # PathLog.debug('minSTH: {}'.format(minSTH)) - if abs(zChng) < tolrnc: # transitions to same Z height - PathLog.debug('abs(zChng) < tolrnc') - if (minSTH - first.z) > tolrnc: - PathLog.debug('(minSTH - first.z) > tolrnc') - height = minSTH + 2.0 - else: - PathLog.debug('ELSE (minSTH - first.z) > tolrnc') - horizGC = 'G1' - height = first.z - elif (minSTH + (2.0 * tolrnc)) >= max(first.z, lstPnt.z): - height = False # allow end of Zig to cut to beginning of Zag - - - # Create raise, shift, and optional lower commands - if height is not False: - cmds.append(Path.Command('G0', {'Z': height, 'F': self.vertRapid})) - cmds.append(Path.Command(horizGC, {'X': first.x, 'Y': first.y, 'F': hSpeed})) - if rtpd is not False: # ReturnToPreviousDepth - cmds.append(Path.Command('G0', {'Z': rtpd, 'F': self.vertRapid})) - - return cmds - - def _breakCmds(self, obj, lstPnt, first, minSTH, tolrnc): - cmds = list() - rtpd = False - horizGC = 'G0' - hSpeed = self.horizRapid - height = obj.SafeHeight.Value - - if obj.CutPattern in ['Line', 'Circular']: - if obj.OptimizeStepOverTransitions is True: - height = minSTH + 2.0 - elif obj.CutPattern in ['ZigZag', 'CircularZigZag']: - if obj.OptimizeStepOverTransitions is True: - zChng = first.z - lstPnt.z - if abs(zChng) < tolrnc: # transitions to same Z height - if (minSTH - first.z) > tolrnc: - height = minSTH + 2.0 - else: - height = first.z + 2.0 # first.z - - cmds.append(Path.Command('G0', {'Z': height, 'F': self.vertRapid})) - cmds.append(Path.Command(horizGC, {'X': first.x, 'Y': first.y, 'F': hSpeed})) - if rtpd is not False: # ReturnToPreviousDepth - cmds.append(Path.Command('G0', {'Z': rtpd, 'F': self.vertRapid})) - - return cmds - - def _arcsToG2G3(self, LN, numPts, odd, gDIR, tolrnc): - cmds = list() - strtPnt = LN[0] - endPnt = LN[numPts - 1] - strtHght = strtPnt.z - coPlanar = True - isCircle = False - inrPnt = None - gdi = 0 - if odd is True: - gdi = 1 - - # Test if pnt set is circle - if abs(strtPnt.x - endPnt.x) < tolrnc: - if abs(strtPnt.y - endPnt.y) < tolrnc: - if abs(strtPnt.z - endPnt.z) < tolrnc: - isCircle = True - isCircle = False - - if isCircle is True: - # convert LN to G2/G3 arc, consolidating GCode - # https://wiki.shapeoko.com/index.php/G-Code#G2_-_clockwise_arc - # https://www.cnccookbook.com/cnc-g-code-arc-circle-g02-g03/ - # Dividing circle into two arcs allows for G2/G3 on inclined surfaces - - # ijk = self.tmpCOM - strtPnt # vector from start to center - ijk = self.tmpCOM - strtPnt # vector from start to center - xyz = self.tmpCOM.add(ijk) # end point - cmds.append(Path.Command('G1', {'X': strtPnt.x, 'Y': strtPnt.y, 'Z': strtPnt.z, 'F': self.horizFeed})) - cmds.append(Path.Command(gDIR[gdi], {'X': xyz.x, 'Y': xyz.y, 'Z': xyz.z, - 'I': ijk.x, 'J': ijk.y, 'K': ijk.z, # leave same xyz.z height - 'F': self.horizFeed})) - cmds.append(Path.Command('G1', {'X': xyz.x, 'Y': xyz.y, 'Z': xyz.z, 'F': self.horizFeed})) - ijk = self.tmpCOM - xyz # vector from start to center - rst = strtPnt # end point - cmds.append(Path.Command(gDIR[gdi], {'X': rst.x, 'Y': rst.y, 'Z': rst.z, - 'I': ijk.x, 'J': ijk.y, 'K': ijk.z, # leave same xyz.z height - 'F': self.horizFeed})) - cmds.append(Path.Command('G1', {'X': strtPnt.x, 'Y': strtPnt.y, 'Z': strtPnt.z, 'F': self.horizFeed})) - else: - for pt in LN: - if abs(pt.z - strtHght) > tolrnc: # test for horizontal coplanar - coPlanar = False - break - if coPlanar is True: - # ijk = self.tmpCOM - strtPnt - ijk = self.tmpCOM.sub(strtPnt) # vector from start to center - xyz = endPnt - cmds.append(Path.Command('G1', {'X': strtPnt.x, 'Y': strtPnt.y, 'Z': strtPnt.z, 'F': self.horizFeed})) - cmds.append(Path.Command(gDIR[gdi], {'X': xyz.x, 'Y': xyz.y, 'Z': xyz.z, - 'I': ijk.x, 'J': ijk.y, 'K': ijk.z, # leave same xyz.z height - 'F': self.horizFeed})) - cmds.append(Path.Command('G1', {'X': endPnt.x, 'Y': endPnt.y, 'Z': endPnt.z, 'F': self.horizFeed})) - - return (coPlanar, cmds) - - def _planarApplyDepthOffset(self, SCANDATA, DepthOffset): - PathLog.debug('Applying DepthOffset value: {}'.format(DepthOffset)) - lenScans = len(SCANDATA) - for s in range(0, lenScans): - SO = SCANDATA[s] # StepOver - numParts = len(SO) - for prt in range(0, numParts): - PRT = SO[prt] - if PRT != 'BRK': - numPts = len(PRT) - for pt in range(0, numPts): - SCANDATA[s][prt][pt].z += DepthOffset - - def _planarGetPDC(self, stl, finalDep, SampleInterval, useSafeCutter=False): - pdc = ocl.PathDropCutter() # create a pdc [PathDropCutter] object - pdc.setSTL(stl) # add stl model - if useSafeCutter is True: - pdc.setCutter(self.safeCutter) # add safeCutter - else: - pdc.setCutter(self.cutter) # add cutter - pdc.setZ(finalDep) # set minimumZ (final / target depth value) - pdc.setSampling(SampleInterval) # set sampling size - return pdc - - # Main rotational scan functions - def _processRotationalOp(self, JOB, obj, mdlIdx, compoundFaces=None): - PathLog.debug('_processRotationalOp(self, obj, mdlIdx, compoundFaces=None)') - initIdx = 0.0 - final = list() - - JOB = PathUtils.findParentJob(obj) - base = JOB.Model.Group[mdlIdx] - bb = self.boundBoxes[mdlIdx] - stl = self.modelSTLs[mdlIdx] - - # Rotate model to initial index - initIdx = obj.CutterTilt + obj.StartIndex - if initIdx != 0.0: - self.basePlacement = FreeCAD.ActiveDocument.getObject(base.Name).Placement - if obj.RotationAxis == 'X': - base.Placement = FreeCAD.Placement(FreeCAD.Vector(0.0, 0.0, 0.0), FreeCAD.Rotation(FreeCAD.Vector(1.0, 0.0, 0.0), initIdx)) - else: - base.Placement = FreeCAD.Placement(FreeCAD.Vector(0.0, 0.0, 0.0), FreeCAD.Rotation(FreeCAD.Vector(0.0, 1.0, 0.0), initIdx)) - - # Prepare global holdpoint container - if self.holdPoint is None: - self.holdPoint = ocl.Point(float("inf"), float("inf"), float("inf")) - if self.layerEndPnt is None: - self.layerEndPnt = ocl.Point(float("inf"), float("inf"), float("inf")) - - # Avoid division by zero in rotational scan calculations - if obj.FinalDepth.Value <= 0.0: - zero = obj.SampleInterval.Value # 0.00001 - self.FinalDepth = zero - obj.FinalDepth.Value = 0.0 - else: - self.FinalDepth = obj.FinalDepth.Value - - # Determine boundbox radius based upon xzy limits data - if math.fabs(bb.ZMin) > math.fabs(bb.ZMax): - vlim = bb.ZMin - else: - vlim = bb.ZMax - if obj.RotationAxis == 'X': - # Rotation is around X-axis, cutter moves along same axis - if math.fabs(bb.YMin) > math.fabs(bb.YMax): - hlim = bb.YMin - else: - hlim = bb.YMax - else: - # Rotation is around Y-axis, cutter moves along same axis - if math.fabs(bb.XMin) > math.fabs(bb.XMax): - hlim = bb.XMin - else: - hlim = bb.XMax - - # Compute max radius of stock, as it rotates, and rotational clearance & safe heights - self.bbRadius = math.sqrt(hlim**2 + vlim**2) - self.clearHeight = self.bbRadius + JOB.SetupSheet.ClearanceHeightOffset.Value - self.safeHeight = self.bbRadius + JOB.SetupSheet.ClearanceHeightOffset.Value - - final = self._rotationalDropCutterOp(obj, stl, bb) - - return final - - def _rotationalDropCutterOp(self, obj, stl, bb): - self.resetTolerance = 0.0000001 # degrees - self.layerEndzMax = 0.0 - commands = [] - scanLines = [] - advances = [] - iSTG = [] - rSTG = [] - rings = [] - lCnt = 0 - rNum = 0 - # stepDeg = 1.1 - # layCircum = 1.1 - # begIdx = 0.0 - # endIdx = 0.0 - # arc = 0.0 - # sumAdv = 0.0 - bbRad = self.bbRadius - - def invertAdvances(advances): - idxs = [1.1] - for adv in advances: - idxs.append(-1 * adv) - idxs.pop(0) - return idxs - - def linesToPointRings(scanLines): - rngs = [] - numPnts = len(scanLines[0]) # Number of points per line along axis, at obj.SampleInterval.Value spacing - for line in scanLines: # extract circular set(ring) of points from scan lines - if len(line) != numPnts: - PathLog.debug('Error: line lengths not equal') - return rngs - - for num in range(0, numPnts): - rngs.append([1.1]) # Initiate new ring - for line in scanLines: # extract circular set(ring) of points from scan lines - rngs[num].append(line[num]) - rngs[num].pop(0) - return rngs - - def indexAdvances(arc, stepDeg): - indexes = [0.0] - numSteps = int(math.floor(arc / stepDeg)) - for ns in range(0, numSteps): - indexes.append(stepDeg) - - travel = sum(indexes) - if arc == 360.0: - indexes.insert(0, 0.0) - else: - indexes.append(arc - travel) - - return indexes - - # Compute number and size of stepdowns, and final depth - if obj.LayerMode == 'Single-pass': - depthparams = [self.FinalDepth] - else: - dep_par = PathUtils.depth_params(self.clearHeight, self.safeHeight, self.bbRadius, obj.StepDown.Value, 0.0, self.FinalDepth) - depthparams = [i for i in dep_par] - prevDepth = depthparams[0] - lenDP = len(depthparams) - - # Set drop cutter extra offset - cdeoX = obj.DropCutterExtraOffset.x - cdeoY = obj.DropCutterExtraOffset.y - - # Set updated bound box values and redefine the new min/mas XY area of the operation based on greatest point radius of model - bb.ZMin = -1 * bbRad - bb.ZMax = bbRad - if obj.RotationAxis == 'X': - bb.YMin = -1 * bbRad - bb.YMax = bbRad - ymin = 0.0 - ymax = 0.0 - xmin = bb.XMin - cdeoX - xmax = bb.XMax + cdeoX - else: - bb.XMin = -1 * bbRad - bb.XMax = bbRad - ymin = bb.YMin - cdeoY - ymax = bb.YMax + cdeoY - xmin = 0.0 - xmax = 0.0 - - # Calculate arc - begIdx = obj.StartIndex - endIdx = obj.StopIndex - if endIdx < begIdx: - begIdx -= 360.0 - arc = endIdx - begIdx - - # Begin gcode operation with raising cutter to safe height - commands.append(Path.Command('G0', {'Z': self.safeHeight, 'F': self.vertRapid})) - - # Complete rotational scans at layer and translate into gcode - for layDep in depthparams: - t_before = time.time() - - # Compute circumference and step angles for current layer - layCircum = 2 * math.pi * layDep - if lenDP == 1: - layCircum = 2 * math.pi * bbRad - - # Set axial feed rates - self.axialFeed = 360 / layCircum * self.horizFeed - self.axialRapid = 360 / layCircum * self.horizRapid - - # Determine step angle. - if obj.RotationAxis == obj.DropCutterDir: # Same == indexed - stepDeg = (self.cutOut / layCircum) * 360.0 - else: - stepDeg = (obj.SampleInterval.Value / layCircum) * 360.0 - - # Limit step angle and determine rotational index angles [indexes]. - if stepDeg > 120.0: - stepDeg = 120.0 - advances = indexAdvances(arc, stepDeg) # Reset for each step down layer - - # Perform rotational indexed scans to layer depth - if obj.RotationAxis == obj.DropCutterDir: # Same == indexed OR parallel - sample = obj.SampleInterval.Value - else: - sample = self.cutOut - scanLines = self._indexedDropCutScan(obj, stl, advances, xmin, ymin, xmax, ymax, layDep, sample) - - # Complete rotation if necessary - if arc == 360.0: - advances.append(360.0 - sum(advances)) - advances.pop(0) - zero = scanLines.pop(0) - scanLines.append(zero) - - # Translate OCL scans into gcode - if obj.RotationAxis == obj.DropCutterDir: # Same == indexed (cutter runs parallel to axis) - # Invert advances if RotationAxis == Y - if obj.RotationAxis == 'Y': - advances = invertAdvances(advances) - - # Translate scan to gcode - # sumAdv = 0.0 - sumAdv = begIdx - for sl in range(0, len(scanLines)): - sumAdv += advances[sl] - # Translate scan to gcode - iSTG = self._indexedScanToGcode(obj, sl, scanLines[sl], sumAdv, prevDepth, layDep, lenDP) - commands.extend(iSTG) - - # Add rise to clear height before beginning next index in CutPattern: Line - # if obj.CutPattern == 'Line': - # commands.append(Path.Command('G0', {'Z': self.clearHeight, 'F': self.vertRapid})) - - # Raise cutter to safe height after each index cut - commands.append(Path.Command('G0', {'Z': self.clearHeight, 'F': self.vertRapid})) - # Eol - else: - if self.CutClimb is False: - advances = invertAdvances(advances) - advances.reverse() - scanLines.reverse() - - # Invert advances if RotationAxis == Y - if obj.RotationAxis == 'Y': - advances = invertAdvances(advances) - - # Begin gcode operation with raising cutter to safe height - commands.append(Path.Command('G0', {'Z': self.clearHeight, 'F': self.vertRapid})) - - # Convert rotational scans into gcode - rings = linesToPointRings(scanLines) - rNum = 0 - for rng in rings: - rSTG = self._rotationalScanToGcode(obj, rng, rNum, prevDepth, layDep, advances) - commands.extend(rSTG) - if arc != 360.0: - clrZ = self.layerEndzMax + self.SafeHeightOffset - commands.append(Path.Command('G0', {'Z': clrZ, 'F': self.vertRapid})) - rNum += 1 - # Eol - - # Add rise to clear height before beginning next index in CutPattern: Line - # if obj.CutPattern == 'Line': - # commands.append(Path.Command('G0', {'Z': self.clearHeight, 'F': self.vertRapid})) - - prevDepth = layDep - lCnt += 1 # increment layer count - PathLog.debug("--Layer " + str(lCnt) + ": " + str(len(advances)) + " OCL scans and gcode in " + str(time.time() - t_before) + " s") - #time.sleep(0.2) - # Eol - return commands - - def _indexedDropCutScan(self, obj, stl, advances, xmin, ymin, xmax, ymax, layDep, sample): - cutterOfst = 0.0 - # radsRot = 0.0 - # reset = 0.0 - iCnt = 0 - Lines = [] - result = None - - pdc = ocl.PathDropCutter() # create a pdc - pdc.setCutter(self.cutter) - pdc.setZ(layDep) # set minimumZ (final / ta9rget depth value) - pdc.setSampling(sample) - - # if self.useTiltCutter == True: - if obj.CutterTilt != 0.0: - cutterOfst = layDep * math.sin(math.radians(obj.CutterTilt)) - PathLog.debug("CutterTilt: cutterOfst is " + str(cutterOfst)) - - sumAdv = 0.0 - for adv in advances: - sumAdv += adv - if adv > 0.0: - # Rotate STL object using OCL method - radsRot = math.radians(adv) - if obj.RotationAxis == 'X': - stl.rotate(radsRot, 0.0, 0.0) - else: - stl.rotate(0.0, radsRot, 0.0) - - # Set STL after rotation is made - pdc.setSTL(stl) - - # add Line objects to the path in this loop - if obj.RotationAxis == 'X': - p1 = ocl.Point(xmin, cutterOfst, 0.0) # start-point of line - p2 = ocl.Point(xmax, cutterOfst, 0.0) # end-point of line - else: - p1 = ocl.Point(cutterOfst, ymin, 0.0) # start-point of line - p2 = ocl.Point(cutterOfst, ymax, 0.0) # end-point of line - - # Create line object - if obj.RotationAxis == obj.DropCutterDir: # parallel cut - if obj.CutPattern == 'ZigZag': - if (iCnt % 2 == 0.0): # even - lo = ocl.Line(p1, p2) - else: # odd - lo = ocl.Line(p2, p1) - elif obj.CutPattern == 'Line': - if self.CutClimb is True: - lo = ocl.Line(p2, p1) - else: - lo = ocl.Line(p1, p2) - else: - lo = ocl.Line(p1, p2) # line-object - - path = ocl.Path() # create an empty path object - path.append(lo) # add the line to the path - pdc.setPath(path) # set path - pdc.run() # run drop-cutter on the path - result = pdc.getCLPoints() - Lines.append(result) # request the list of points - - iCnt += 1 - # End loop - # Rotate STL object back to original position using OCL method - reset = -1 * math.radians(sumAdv - self.resetTolerance) - if obj.RotationAxis == 'X': - stl.rotate(reset, 0.0, 0.0) - else: - stl.rotate(0.0, reset, 0.0) - self.resetTolerance = 0.0 - - return Lines - - def _indexedScanToGcode(self, obj, li, CLP, idxAng, prvDep, layerDepth, numDeps): - # generate the path commands - output = [] - optimize = obj.OptimizeLinearPaths - holdCount = 0 - holdStart = False - holdStop = False - zMax = prvDep - lenCLP = len(CLP) - lastCLP = lenCLP - 1 - prev = ocl.Point(float("inf"), float("inf"), float("inf")) - nxt = ocl.Point(float("inf"), float("inf"), float("inf")) - pnt = ocl.Point(float("inf"), float("inf"), float("inf")) - - # Create first point - pnt.x = CLP[0].x - pnt.y = CLP[0].y - pnt.z = CLP[0].z + float(obj.DepthOffset.Value) - - # Rotate to correct index location - if obj.RotationAxis == 'X': - output.append(Path.Command('G0', {'A': idxAng, 'F': self.axialFeed})) - else: - output.append(Path.Command('G0', {'B': idxAng, 'F': self.axialFeed})) - - if li > 0: - if pnt.z > self.layerEndPnt.z: - clrZ = pnt.z + 2.0 - output.append(Path.Command('G1', {'Z': clrZ, 'F': self.vertRapid})) - else: - output.append(Path.Command('G0', {'Z': self.clearHeight, 'F': self.vertRapid})) - - output.append(Path.Command('G0', {'X': pnt.x, 'Y': pnt.y, 'F': self.horizRapid})) - output.append(Path.Command('G1', {'Z': pnt.z, 'F': self.vertFeed})) - - for i in range(0, lenCLP): - if i < lastCLP: - nxt.x = CLP[i + 1].x - nxt.y = CLP[i + 1].y - nxt.z = CLP[i + 1].z + float(obj.DepthOffset.Value) - else: - optimize = False - - # Update zMax values - if pnt.z > zMax: - zMax = pnt.z - - if obj.LayerMode == 'Multi-pass': - # if z travels above previous layer, start/continue hold high cycle - if pnt.z > prvDep and optimize is True: - if self.onHold is False: - holdStart = True - self.onHold = True - - if self.onHold is True: - if holdStart is True: - # go to current coordinate - output.append(Path.Command('G1', {'X': pnt.x, 'Y': pnt.y, 'Z': pnt.z, 'F': self.horizFeed})) - # Save holdStart coordinate and prvDep values - self.holdPoint.x = pnt.x - self.holdPoint.y = pnt.y - self.holdPoint.z = pnt.z - holdCount += 1 # Increment hold count - holdStart = False # cancel holdStart - - # hold cutter high until Z value drops below prvDep - if pnt.z <= prvDep: - holdStop = True - - if holdStop is True: - # Send hold and current points to - zMax += 2.0 - for cmd in self.holdStopCmds(obj, zMax, prvDep, pnt, "Hold Stop: in-line"): - output.append(cmd) - # reset necessary hold related settings - zMax = prvDep - holdStop = False - self.onHold = False - self.holdPoint = ocl.Point(float("inf"), float("inf"), float("inf")) - - if self.onHold is False: - if not optimize or not FreeCAD.Vector(prev.x, prev.y, prev.z).isOnLineSegment(FreeCAD.Vector(nxt.x, nxt.y, nxt.z), FreeCAD.Vector(pnt.x, pnt.y, pnt.z)): - output.append(Path.Command('G1', {'X': pnt.x, 'Y': pnt.y, 'Z': pnt.z, 'F': self.horizFeed})) - # elif i == lastCLP: - # output.append(Path.Command('G1', {'X': pnt.x, 'Y': pnt.y, 'Z': pnt.z, 'F': self.horizFeed})) - - # Rotate point data - prev.x = pnt.x - prev.y = pnt.y - prev.z = pnt.z - pnt.x = nxt.x - pnt.y = nxt.y - pnt.z = nxt.z - output.append(Path.Command('N (End index angle ' + str(round(idxAng, 4)) + ')', {})) - - # Save layer end point for use in transitioning to next layer - self.layerEndPnt.x = pnt.x - self.layerEndPnt.y = pnt.y - self.layerEndPnt.z = pnt.z - - return output - - def _rotationalScanToGcode(self, obj, RNG, rN, prvDep, layDep, advances): - '''_rotationalScanToGcode(obj, RNG, rN, prvDep, layDep, advances) ... - Convert rotational scan data to gcode path commands.''' - output = [] - nxtAng = 0 - zMax = 0.0 - # prev = ocl.Point(float("inf"), float("inf"), float("inf")) - nxt = ocl.Point(float("inf"), float("inf"), float("inf")) - pnt = ocl.Point(float("inf"), float("inf"), float("inf")) - - begIdx = obj.StartIndex - endIdx = obj.StopIndex - if endIdx < begIdx: - begIdx -= 360.0 - - # Rotate to correct index location - axisOfRot = 'A' - if obj.RotationAxis == 'Y': - axisOfRot = 'B' - - # Create first point - ang = 0.0 + obj.CutterTilt - pnt.x = RNG[0].x - pnt.y = RNG[0].y - pnt.z = RNG[0].z + float(obj.DepthOffset.Value) - - # Adjust feed rate based on radius/circumference of cutter. - # Original feed rate based on travel at circumference. - if rN > 0: - # if pnt.z > self.layerEndPnt.z: - if pnt.z >= self.layerEndzMax: - clrZ = pnt.z + 5.0 - output.append(Path.Command('G1', {'Z': clrZ, 'F': self.vertRapid})) - else: - output.append(Path.Command('G1', {'Z': self.clearHeight, 'F': self.vertRapid})) - - output.append(Path.Command('G0', {axisOfRot: ang, 'F': self.axialFeed})) - output.append(Path.Command('G1', {'X': pnt.x, 'Y': pnt.y, 'F': self.axialFeed})) - output.append(Path.Command('G1', {'Z': pnt.z, 'F': self.axialFeed})) - - lenRNG = len(RNG) - lastIdx = lenRNG - 1 - for i in range(0, lenRNG): - if i < lastIdx: - nxtAng = ang + advances[i + 1] - nxt.x = RNG[i + 1].x - nxt.y = RNG[i + 1].y - nxt.z = RNG[i + 1].z + float(obj.DepthOffset.Value) - - if pnt.z > zMax: - zMax = pnt.z - - output.append(Path.Command('G1', {'X': pnt.x, 'Y': pnt.y, 'Z': pnt.z, axisOfRot: ang, 'F': self.axialFeed})) - pnt.x = nxt.x - pnt.y = nxt.y - pnt.z = nxt.z - ang = nxtAng - - # Save layer end point for use in transitioning to next layer - self.layerEndPnt.x = RNG[0].x - self.layerEndPnt.y = RNG[0].y - self.layerEndPnt.z = RNG[0].z - self.layerEndzMax = zMax - - # Move cutter to final point - # output.append(Path.Command('G1', {'X': self.layerEndPnt.x, 'Y': self.layerEndPnt.y, 'Z': self.layerEndPnt.z, axisOfRot: endang, 'F': self.axialFeed})) - - return output - - - '''_loopToGcode(obj, layDep, loop) ... Convert set of loop points to Gcode.''' - # generate the path commands - output = [] - optimize = obj.OptimizeLinearPaths - - prev = ocl.Point(float("inf"), float("inf"), float("inf")) - nxt = ocl.Point(float("inf"), float("inf"), float("inf")) - pnt = ocl.Point(float("inf"), float("inf"), float("inf")) - - # Create first point - pnt.x = loop[0].x - pnt.y = loop[0].y - pnt.z = layDep - - # Position cutter to begin loop - output.append(Path.Command('G0', {'Z': obj.ClearanceHeight.Value, 'F': self.vertRapid})) - output.append(Path.Command('G0', {'X': pnt.x, 'Y': pnt.y, 'F': self.horizRapid})) - output.append(Path.Command('G1', {'Z': pnt.z, 'F': self.vertFeed})) - - lenCLP = len(loop) - lastIdx = lenCLP - 1 - # Cycle through each point on loop - for i in range(0, lenCLP): - if i < lastIdx: - nxt.x = loop[i + 1].x - nxt.y = loop[i + 1].y - nxt.z = layDep - else: - optimize = False - - if not optimize or not FreeCAD.Vector(prev.x, prev.y, prev.z).isOnLineSegment(FreeCAD.Vector(nxt.x, nxt.y, nxt.z), FreeCAD.Vector(pnt.x, pnt.y, pnt.z)): - output.append(Path.Command('G1', {'X': pnt.x, 'Y': pnt.y, 'F': self.horizFeed})) - - # Rotate point data - prev.x = pnt.x - prev.y = pnt.y - prev.z = pnt.z - pnt.x = nxt.x - pnt.y = nxt.y - pnt.z = nxt.z - - # Save layer end point for use in transitioning to next layer - self.layerEndPnt.x = pnt.x - self.layerEndPnt.y = pnt.y - self.layerEndPnt.z = pnt.z - - return output - - def holdStopCmds(self, obj, zMax, pd, p2, txt): - '''holdStopCmds(obj, zMax, pd, p2, txt) ... Gcode commands to be executed at beginning of hold.''' - cmds = [] - msg = 'N (' + txt + ')' - cmds.append(Path.Command(msg, {})) # Raise cutter rapid to zMax in line of travel - cmds.append(Path.Command('G0', {'Z': zMax, 'F': self.vertRapid})) # Raise cutter rapid to zMax in line of travel - cmds.append(Path.Command('G0', {'X': p2.x, 'Y': p2.y, 'F': self.horizRapid})) # horizontal rapid to current XY coordinate - if zMax != pd: - cmds.append(Path.Command('G0', {'Z': pd, 'F': self.vertRapid})) # drop cutter down rapidly to prevDepth depth - cmds.append(Path.Command('G0', {'Z': p2.z, 'F': self.vertFeed})) # drop cutter down to current Z depth, returning to normal cut path and speed - return cmds - - def resetOpVariables(self, all=True): - '''resetOpVariables() ... Reset class variables used for instance of operation.''' - self.holdPoint = None - self.layerEndPnt = None - self.onHold = False - self.SafeHeightOffset = 2.0 - self.ClearHeightOffset = 4.0 - self.layerEndzMax = 0.0 - self.resetTolerance = 0.0 - self.holdPntCnt = 0 - self.bbRadius = 0.0 - self.axialFeed = 0.0 - self.axialRapid = 0.0 - self.FinalDepth = 0.0 - self.clearHeight = 0.0 - self.safeHeight = 0.0 - self.faceZMax = -999999999999.0 - if all is True: - self.cutter = None - self.stl = None - self.fullSTL = None - self.cutOut = 0.0 - self.radius = 0.0 - self.useTiltCutter = False - return True - - def deleteOpVariables(self, all=True): - '''deleteOpVariables() ... Reset class variables used for instance of operation.''' - del self.holdPoint - del self.layerEndPnt - del self.onHold - del self.SafeHeightOffset - del self.ClearHeightOffset - del self.layerEndzMax - del self.resetTolerance - del self.holdPntCnt - del self.bbRadius - del self.axialFeed - del self.axialRapid - del self.FinalDepth - del self.clearHeight - del self.safeHeight - del self.faceZMax - if all is True: - del self.cutter - del self.stl - del self.fullSTL - del self.cutOut - del self.radius - del self.useTiltCutter - return True - - def setOclCutter(self, obj, safe=False): - ''' setOclCutter(obj) ... Translation function to convert FreeCAD tool definition to OCL formatted tool. ''' - # Set cutter details - # https://www.freecadweb.org/api/dd/dfe/classPath_1_1Tool.html#details - diam_1 = float(obj.ToolController.Tool.Diameter) - lenOfst = obj.ToolController.Tool.LengthOffset if hasattr(obj.ToolController.Tool, 'LengthOffset') else 0 - FR = obj.ToolController.Tool.FlatRadius if hasattr(obj.ToolController.Tool, 'FlatRadius') else 0 - CEH = obj.ToolController.Tool.CuttingEdgeHeight if hasattr(obj.ToolController.Tool, 'CuttingEdgeHeight') else 0 - CEA = obj.ToolController.Tool.CuttingEdgeAngle if hasattr(obj.ToolController.Tool, 'CuttingEdgeAngle') else 0 - - # Make safeCutter with 2 mm buffer around physical cutter - if safe is True: - diam_1 += 4.0 - if FR != 0.0: - FR += 2.0 - - PathLog.debug('ToolType: {}'.format(obj.ToolController.Tool.ToolType)) - if obj.ToolController.Tool.ToolType == 'EndMill': - # Standard End Mill - return ocl.CylCutter(diam_1, (CEH + lenOfst)) - - elif obj.ToolController.Tool.ToolType == 'BallEndMill' and FR == 0.0: - # Standard Ball End Mill - # OCL -> BallCutter::BallCutter(diameter, length) - self.useTiltCutter = True - return ocl.BallCutter(diam_1, (diam_1 / 2 + lenOfst)) - - elif obj.ToolController.Tool.ToolType == 'BallEndMill' and FR > 0.0: - # Bull Nose or Corner Radius cutter - # Reference: https://www.fine-tools.com/halbstabfraeser.html - # OCL -> BallCutter::BallCutter(diameter, length) - return ocl.BullCutter(diam_1, FR, (CEH + lenOfst)) - - elif obj.ToolController.Tool.ToolType == 'Engraver' and FR > 0.0: - # Bull Nose or Corner Radius cutter - # Reference: https://www.fine-tools.com/halbstabfraeser.html - # OCL -> ConeCutter::ConeCutter(diameter, angle, lengthOffset) - return ocl.ConeCutter(diam_1, (CEA / 2), lenOfst) - - elif obj.ToolController.Tool.ToolType == 'ChamferMill': - # Bull Nose or Corner Radius cutter - # Reference: https://www.fine-tools.com/halbstabfraeser.html - # OCL -> ConeCutter::ConeCutter(diameter, angle, lengthOffset) - return ocl.ConeCutter(diam_1, (CEA / 2), lenOfst) - else: - # Default to standard end mill - PathLog.warning("Defaulting cutter to standard end mill.") - return ocl.CylCutter(diam_1, (CEH + lenOfst)) - - # http://www.carbidecutter.net/products/carbide-burr-cone-shape-sm.html - ''' - # Available FreeCAD cutter types - some still need translation to available OCL cutter classes. - Drill, CenterDrill, CounterSink, CounterBore, FlyCutter, Reamer, Tap, - EndMill, SlotCutter, BallEndMill, ChamferMill, CornerRound, Engraver - ''' - # Adittional problem is with new ToolBit user-defined cutter shapes. - # Some sort of translation/conversion will have to be defined to make compatible with OCL. - PathLog.error('Unable to set OCL cutter.') - return False - - def _getMinSafeTravelHeight(self, pdc, p1, p2, minDep=None): - A = (p1.x, p1.y) - B = (p2.x, p2.y) - LINE = self._planarDropCutScan(pdc, A, B) - zMax = max([obj.z for obj in LINE]) - if minDep is not None: - if zMax < minDep: - zMax = minDep - return zMax - - -def SetupProperties(): - ''' SetupProperties() ... Return list of properties required for operation.''' - setup = [] - setup.append('AvoidLastX_Faces') - setup.append('AvoidLastX_InternalFeatures') - setup.append('BoundBox') - setup.append('BoundaryAdjustment') - setup.append('CircularCenterAt') - setup.append('CircularCenterCustom') - setup.append('CircularUseG2G3') - setup.append('InternalFeaturesCut') - setup.append('InternalFeaturesAdjustment') - setup.append('CutMode') - setup.append('CutPattern') - setup.append('CutPatternAngle') - setup.append('CutPatternReversed') - setup.append('CutterTilt') - setup.append('DepthOffset') - setup.append('DropCutterDir') - setup.append('GapSizes') - setup.append('GapThreshold') - setup.append('HandleMultipleFeatures') - setup.append('LayerMode') - setup.append('OptimizeStepOverTransitions') - setup.append('ProfileEdges') - setup.append('BoundaryEnforcement') - setup.append('RotationAxis') - setup.append('SampleInterval') - setup.append('ScanType') - setup.append('StartIndex') - setup.append('StartPoint') - setup.append('StepOver') - setup.append('StopIndex') - setup.append('UseStartPoint') - setup.append('AngularDeflection') - setup.append('LinearDeflection') - # For debugging - setup.append('ShowTempObjects') - return setup - - -def Create(name, obj=None): - '''Create(name) ... Creates and returns a Surface operation.''' - if obj is None: - obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython", name) - obj.Proxy = ObjectSurface(obj, name) - return obj +# -*- coding: utf-8 -*- + +# *************************************************************************** +# * * +# * Copyright (c) 2016 sliptonic * +# * * +# * This program is free software; you can redistribute it and/or modify * +# * it under the terms of the GNU Lesser General Public License (LGPL) * +# * as published by the Free Software Foundation; either version 2 of * +# * the License, or (at your option) any later version. * +# * for detail see the LICENCE text file. * +# * * +# * This program is distributed in the hope that it will be useful, * +# * but WITHOUT ANY WARRANTY; without even the implied warranty of * +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# * GNU Library General Public License for more details. * +# * * +# * You should have received a copy of the GNU Library General Public * +# * License along with this program; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# *************************************************************************** + + +from __future__ import print_function + +__title__ = "Path Surface Operation" +__author__ = "sliptonic (Brad Collette)" +__url__ = "http://www.freecadweb.org" +__doc__ = "Class and implementation of 3D Surface operation." +__contributors__ = "russ4262 (Russell Johnson)" + +import FreeCAD +from PySide import QtCore + +# OCL must be installed +try: + import ocl +except ImportError: + msg = QtCore.QCoreApplication.translate("PathSurface", "This operation requires OpenCamLib to be installed.") + FreeCAD.Console.PrintError(msg + "\n") + raise ImportError + # import sys + # sys.exit(msg) + +import Path +import PathScripts.PathLog as PathLog +import PathScripts.PathUtils as PathUtils +import PathScripts.PathOp as PathOp +import PathScripts.PathSurfaceSupport as PathSurfaceSupport +import time +import math + +# lazily loaded modules +from lazy_loader.lazy_loader import LazyLoader +MeshPart = LazyLoader('MeshPart', globals(), 'MeshPart') +Part = LazyLoader('Part', globals(), 'Part') + +if FreeCAD.GuiUp: + import FreeCADGui + +PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule()) +# PathLog.trackModule(PathLog.thisModule()) + + +# Qt translation handling +def translate(context, text, disambig=None): + return QtCore.QCoreApplication.translate(context, text, disambig) + + +class ObjectSurface(PathOp.ObjectOp): + '''Proxy object for Surfacing operation.''' + + def baseObject(self): + '''baseObject() ... returns super of receiver + Used to call base implementation in overwritten functions.''' + return super(self.__class__, self) + + def opFeatures(self, obj): + '''opFeatures(obj) ... return all standard features and edges based geometries''' + return PathOp.FeatureTool | PathOp.FeatureDepths | PathOp.FeatureHeights | PathOp.FeatureStepDown | PathOp.FeatureCoolant | PathOp.FeatureBaseFaces + + def initOperation(self, obj): + '''initPocketOp(obj) ... create operation specific properties''' + self.initOpProperties(obj) + + # For debugging + if PathLog.getLevel(PathLog.thisModule()) != 4: + obj.setEditorMode('ShowTempObjects', 2) # hide + + if not hasattr(obj, 'DoNotSetDefaultValues'): + self.setEditorProperties(obj) + + def initOpProperties(self, obj, warn=False): + '''initOpProperties(obj) ... create operation specific properties''' + missing = list() + + for (prtyp, nm, grp, tt) in self.opProperties(): + if not hasattr(obj, nm): + obj.addProperty(prtyp, nm, grp, tt) + missing.append(nm) + if warn: + newPropMsg = translate('PathSurface', 'New property added to') + ' "{}": '.format(obj.Label) + nm + '. ' + newPropMsg += translate('PathSurface', 'Check its default value.') + PathLog.warning(newPropMsg) + + # Set enumeration lists for enumeration properties + if len(missing) > 0: + ENUMS = self.propertyEnumerations() + for n in ENUMS: + if n in missing: + setattr(obj, n, ENUMS[n]) + + self.addedAllProperties = True + + def opProperties(self): + '''opProperties(obj) ... Store operation specific properties''' + + return [ + ("App::PropertyBool", "ShowTempObjects", "Debug", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Show the temporary path construction objects when module is in DEBUG mode.")), + + ("App::PropertyDistance", "AngularDeflection", "Mesh Conversion", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Smaller values yield a finer, more accurate mesh. Smaller values increase processing time a lot.")), + ("App::PropertyDistance", "LinearDeflection", "Mesh Conversion", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Smaller values yield a finer, more accurate mesh. Smaller values do not increase processing time much.")), + + ("App::PropertyFloat", "CutterTilt", "Rotation", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Stop index(angle) for rotational scan")), + ("App::PropertyEnumeration", "DropCutterDir", "Rotation", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Dropcutter lines are created parallel to this axis.")), + ("App::PropertyVectorDistance", "DropCutterExtraOffset", "Rotation", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Additional offset to the selected bounding box")), + ("App::PropertyEnumeration", "RotationAxis", "Rotation", + QtCore.QT_TRANSLATE_NOOP("App::Property", "The model will be rotated around this axis.")), + ("App::PropertyFloat", "StartIndex", "Rotation", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Start index(angle) for rotational scan")), + ("App::PropertyFloat", "StopIndex", "Rotation", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Stop index(angle) for rotational scan")), + + ("App::PropertyEnumeration", "ScanType", "Surface", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Planar: Flat, 3D surface scan. Rotational: 4th-axis rotational scan.")), + + ("App::PropertyInteger", "AvoidLastX_Faces", "Selected Geometry Settings", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Avoid cutting the last 'N' faces in the Base Geometry list of selected faces.")), + ("App::PropertyBool", "AvoidLastX_InternalFeatures", "Selected Geometry Settings", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Do not cut internal features on avoided faces.")), + ("App::PropertyDistance", "BoundaryAdjustment", "Selected Geometry Settings", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Positive values push the cutter toward, or beyond, the boundary. Negative values retract the cutter away from the boundary.")), + ("App::PropertyBool", "BoundaryEnforcement", "Selected Geometry Settings", + QtCore.QT_TRANSLATE_NOOP("App::Property", "If true, the cutter will remain inside the boundaries of the model or selected face(s).")), + ("App::PropertyEnumeration", "HandleMultipleFeatures", "Selected Geometry Settings", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Choose how to process multiple Base Geometry features.")), + ("App::PropertyDistance", "InternalFeaturesAdjustment", "Selected Geometry Settings", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Positive values push the cutter toward, or into, the feature. Negative values retract the cutter away from the feature.")), + ("App::PropertyBool", "InternalFeaturesCut", "Selected Geometry Settings", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Ignore internal feature areas within a larger selected face.")), + + ("App::PropertyEnumeration", "BoundBox", "Clearing Options", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Select the overall boundary for the operation.")), + ("App::PropertyEnumeration", "CutMode", "Clearing Options", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the direction for the cutting tool to engage the material: Climb (ClockWise) or Conventional (CounterClockWise)")), + ("App::PropertyEnumeration", "CutPattern", "Clearing Options", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the geometric clearing pattern to use for the operation.")), + ("App::PropertyFloat", "CutPatternAngle", "Clearing Options", + QtCore.QT_TRANSLATE_NOOP("App::Property", "The yaw angle used for certain clearing patterns")), + ("App::PropertyBool", "CutPatternReversed", "Clearing Options", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Reverse the cut order of the stepover paths. For circular cut patterns, begin at the outside and work toward the center.")), + ("App::PropertyDistance", "DepthOffset", "Clearing Options", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the Z-axis depth offset from the target surface.")), + ("App::PropertyEnumeration", "LayerMode", "Clearing Options", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Complete the operation in a single pass at depth, or mulitiple passes to final depth.")), + ("App::PropertyVectorDistance", "PatternCenterCustom", "Clearing Options", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the start point for the cut pattern.")), + ("App::PropertyEnumeration", "PatternCenterAt", "Clearing Options", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Choose location of the center point for starting the cut pattern.")), + ("App::PropertyEnumeration", "ProfileEdges", "Clearing Options", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Profile the edges of the selection.")), + ("App::PropertyDistance", "SampleInterval", "Clearing Options", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the sampling resolution. Smaller values quickly increase processing time.")), + ("App::PropertyPercent", "StepOver", "Clearing Options", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the stepover percentage, based on the tool's diameter.")), + + ("App::PropertyBool", "OptimizeLinearPaths", "Optimization", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Enable optimization of linear paths (co-linear points). Removes unnecessary co-linear points from G-Code output.")), + ("App::PropertyBool", "OptimizeStepOverTransitions", "Optimization", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Enable separate optimization of transitions between, and breaks within, each step over path.")), + ("App::PropertyBool", "CircularUseG2G3", "Optimization", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Convert co-planar arcs to G2/G3 gcode commands for `Circular` and `CircularZigZag` cut patterns.")), + ("App::PropertyDistance", "GapThreshold", "Optimization", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Collinear and co-radial artifact gaps that are smaller than this threshold are closed in the path.")), + ("App::PropertyString", "GapSizes", "Optimization", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Feedback: three smallest gaps identified in the path geometry.")), + + ("App::PropertyVectorDistance", "StartPoint", "Start Point", + QtCore.QT_TRANSLATE_NOOP("App::Property", "The custom start point for the path of this operation")), + ("App::PropertyBool", "UseStartPoint", "Start Point", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Make True, if specifying a Start Point")) + ] + + def propertyEnumerations(self): + # Enumeration lists for App::PropertyEnumeration properties + return { + 'BoundBox': ['BaseBoundBox', 'Stock'], + 'PatternCenterAt': ['CenterOfMass', 'CenterOfBoundBox', 'XminYmin', 'Custom'], + 'CutMode': ['Conventional', 'Climb'], + 'CutPattern': ['Line', 'Circular', 'CircularZigZag', 'Offset', 'Spiral', 'ZigZag'], # Additional goals ['Offset', 'ZigZagOffset', 'Grid', 'Triangle'] + 'DropCutterDir': ['X', 'Y'], + 'HandleMultipleFeatures': ['Collectively', 'Individually'], + 'LayerMode': ['Single-pass', 'Multi-pass'], + 'ProfileEdges': ['None', 'Only', 'First', 'Last'], + 'RotationAxis': ['X', 'Y'], + 'ScanType': ['Planar', 'Rotational'] + } + + def setEditorProperties(self, obj): + # Used to hide inputs in properties list + + P0 = R2 = 0 # 0 = show + P2 = R0 = 2 # 2 = hide + if obj.ScanType == 'Planar': + # if obj.CutPattern in ['Line', 'ZigZag']: + if obj.CutPattern in ['Circular', 'CircularZigZag', 'Spiral']: + P0 = 2 + P2 = 0 + elif obj.CutPattern == 'Offset': + P0 = 2 + elif obj.ScanType == 'Rotational': + R2 = P0 = P2 = 2 + R0 = 0 + obj.setEditorMode('DropCutterDir', R0) + obj.setEditorMode('DropCutterExtraOffset', R0) + obj.setEditorMode('RotationAxis', R0) + obj.setEditorMode('StartIndex', R0) + obj.setEditorMode('StopIndex', R0) + obj.setEditorMode('CutterTilt', R0) + obj.setEditorMode('CutPattern', R2) + obj.setEditorMode('CutPatternAngle', P0) + obj.setEditorMode('PatternCenterAt', P2) + obj.setEditorMode('PatternCenterCustom', P2) + + def onChanged(self, obj, prop): + if hasattr(self, 'addedAllProperties'): + if self.addedAllProperties is True: + if prop == 'ScanType': + self.setEditorProperties(obj) + if prop == 'CutPattern': + self.setEditorProperties(obj) + + def opOnDocumentRestored(self, obj): + self.initOpProperties(obj, warn=True) + + if PathLog.getLevel(PathLog.thisModule()) != 4: + obj.setEditorMode('ShowTempObjects', 2) # hide + else: + obj.setEditorMode('ShowTempObjects', 0) # show + + # Repopulate enumerations in case of changes + ENUMS = self.propertyEnumerations() + for n in ENUMS: + restore = False + if hasattr(obj, n): + val = obj.getPropertyByName(n) + restore = True + setattr(obj, n, ENUMS[n]) + if restore: + setattr(obj, n, val) + + self.setEditorProperties(obj) + + def opSetDefaultValues(self, obj, job): + '''opSetDefaultValues(obj, job) ... initialize defaults''' + job = PathUtils.findParentJob(obj) + + obj.OptimizeLinearPaths = True + obj.InternalFeaturesCut = True + obj.OptimizeStepOverTransitions = False + obj.CircularUseG2G3 = False + obj.BoundaryEnforcement = True + obj.UseStartPoint = False + obj.AvoidLastX_InternalFeatures = True + obj.CutPatternReversed = False + obj.StartPoint.x = 0.0 + obj.StartPoint.y = 0.0 + obj.StartPoint.z = obj.ClearanceHeight.Value + obj.ProfileEdges = 'None' + obj.LayerMode = 'Single-pass' + obj.ScanType = 'Planar' + obj.RotationAxis = 'X' + obj.CutMode = 'Conventional' + obj.CutPattern = 'Line' + obj.HandleMultipleFeatures = 'Collectively' # 'Individually' + obj.PatternCenterAt = 'CenterOfMass' # 'CenterOfBoundBox', 'XminYmin', 'Custom' + obj.GapSizes = 'No gaps identified.' + obj.StepOver = 100 + obj.CutPatternAngle = 0.0 + obj.CutterTilt = 0.0 + obj.StartIndex = 0.0 + obj.StopIndex = 360.0 + obj.SampleInterval.Value = 1.0 + obj.BoundaryAdjustment.Value = 0.0 + obj.InternalFeaturesAdjustment.Value = 0.0 + obj.AvoidLastX_Faces = 0 + obj.PatternCenterCustom.x = 0.0 + obj.PatternCenterCustom.y = 0.0 + obj.PatternCenterCustom.z = 0.0 + obj.GapThreshold.Value = 0.005 + obj.AngularDeflection.Value = 0.25 + obj.LinearDeflection.Value = job.GeometryTolerance.Value + # For debugging + obj.ShowTempObjects = False + + if job.GeometryTolerance.Value == 0.0: + PathLog.warning(translate('PathSurface', 'The GeometryTolerance for this Job is 0.0. Initializing LinearDeflection to 0.0001 mm.')) + obj.LinearDeflection.Value = 0.0001 + + # need to overwrite the default depth calculations for facing + d = None + if job: + if job.Stock: + d = PathUtils.guessDepths(job.Stock.Shape, None) + PathLog.debug("job.Stock exists") + else: + PathLog.debug("job.Stock NOT exist") + else: + PathLog.debug("job NOT exist") + + if d is not None: + obj.OpFinalDepth.Value = d.final_depth + obj.OpStartDepth.Value = d.start_depth + else: + obj.OpFinalDepth.Value = -10 + obj.OpStartDepth.Value = 10 + + PathLog.debug('Default OpFinalDepth: {}'.format(obj.OpFinalDepth.Value)) + PathLog.debug('Defualt OpStartDepth: {}'.format(obj.OpStartDepth.Value)) + + def opApplyPropertyLimits(self, obj): + '''opApplyPropertyLimits(obj) ... Apply necessary limits to user input property values before performing main operation.''' + # Limit start index + if obj.StartIndex < 0.0: + obj.StartIndex = 0.0 + if obj.StartIndex > 360.0: + obj.StartIndex = 360.0 + + # Limit stop index + if obj.StopIndex > 360.0: + obj.StopIndex = 360.0 + if obj.StopIndex < 0.0: + obj.StopIndex = 0.0 + + # Limit cutter tilt + if obj.CutterTilt < -90.0: + obj.CutterTilt = -90.0 + if obj.CutterTilt > 90.0: + obj.CutterTilt = 90.0 + + # Limit sample interval + if obj.SampleInterval.Value < 0.0001: + obj.SampleInterval.Value = 0.0001 + PathLog.error(translate('PathSurface', 'Sample interval limits are 0.001 to 25.4 millimeters.')) + if obj.SampleInterval.Value > 25.4: + obj.SampleInterval.Value = 25.4 + PathLog.error(translate('PathSurface', 'Sample interval limits are 0.001 to 25.4 millimeters.')) + + # Limit cut pattern angle + if obj.CutPatternAngle < -360.0: + obj.CutPatternAngle = 0.0 + PathLog.error(translate('PathSurface', 'Cut pattern angle limits are +-360 degrees.')) + if obj.CutPatternAngle >= 360.0: + obj.CutPatternAngle = 0.0 + PathLog.error(translate('PathSurface', 'Cut pattern angle limits are +- 360 degrees.')) + + # Limit StepOver to natural number percentage + if obj.StepOver > 100: + obj.StepOver = 100 + if obj.StepOver < 1: + obj.StepOver = 1 + + # Limit AvoidLastX_Faces to zero and positive values + if obj.AvoidLastX_Faces < 0: + obj.AvoidLastX_Faces = 0 + PathLog.error(translate('PathSurface', 'AvoidLastX_Faces: Only zero or positive values permitted.')) + if obj.AvoidLastX_Faces > 100: + obj.AvoidLastX_Faces = 100 + PathLog.error(translate('PathSurface', 'AvoidLastX_Faces: Avoid last X faces count limited to 100.')) + + def opExecute(self, obj): + '''opExecute(obj) ... process surface operation''' + PathLog.track() + + self.modelSTLs = list() + self.safeSTLs = list() + self.modelTypes = list() + self.boundBoxes = list() + self.profileShapes = list() + self.collectiveShapes = list() + self.individualShapes = list() + self.avoidShapes = list() + self.tempGroup = None + self.CutClimb = False + self.closedGap = False + self.tmpCOM = None + self.gaps = [0.1, 0.2, 0.3] + CMDS = list() + modelVisibility = list() + FCAD = FreeCAD.ActiveDocument + + try: + dotIdx = __name__.index('.') + 1 + except Exception: + dotIdx = 0 + self.module = __name__[dotIdx:] + + # Set debugging behavior + self.showDebugObjects = False # Set to true if you want a visual DocObjects created for some path construction objects + self.showDebugObjects = obj.ShowTempObjects + deleteTempsFlag = True # Set to False for debugging + if PathLog.getLevel(PathLog.thisModule()) == 4: + deleteTempsFlag = False + else: + self.showDebugObjects = False + + # mark beginning of operation and identify parent Job + PathLog.info('\nBegin 3D Surface operation...') + startTime = time.time() + + # Identify parent Job + JOB = PathUtils.findParentJob(obj) + self.JOB = JOB + if JOB is None: + PathLog.error(translate('PathSurface', "No JOB")) + return + self.stockZMin = JOB.Stock.Shape.BoundBox.ZMin + + # set cut mode; reverse as needed + if obj.CutMode == 'Climb': + self.CutClimb = True + if obj.CutPatternReversed is True: + if self.CutClimb is True: + self.CutClimb = False + else: + self.CutClimb = True + + # Begin GCode for operation with basic information + # ... and move cutter to clearance height and startpoint + output = '' + if obj.Comment != '': + self.commandlist.append(Path.Command('N ({})'.format(str(obj.Comment)), {})) + self.commandlist.append(Path.Command('N ({})'.format(obj.Label), {})) + self.commandlist.append(Path.Command('N (Tool type: {})'.format(str(obj.ToolController.Tool.ToolType)), {})) + self.commandlist.append(Path.Command('N (Compensated Tool Path. Diameter: {})'.format(str(obj.ToolController.Tool.Diameter)), {})) + self.commandlist.append(Path.Command('N (Sample interval: {})'.format(str(obj.SampleInterval.Value)), {})) + self.commandlist.append(Path.Command('N (Step over %: {})'.format(str(obj.StepOver)), {})) + self.commandlist.append(Path.Command('N ({})'.format(output), {})) + self.commandlist.append(Path.Command('G0', {'Z': obj.ClearanceHeight.Value, 'F': self.vertRapid})) + if obj.UseStartPoint is True: + self.commandlist.append(Path.Command('G0', {'X': obj.StartPoint.x, 'Y': obj.StartPoint.y, 'F': self.horizRapid})) + + # Instantiate additional class operation variables + self.resetOpVariables() + + # Impose property limits + self.opApplyPropertyLimits(obj) + + # Create temporary group for temporary objects, removing existing + tempGroupName = 'tempPathSurfaceGroup' + if FCAD.getObject(tempGroupName): + for to in FCAD.getObject(tempGroupName).Group: + FCAD.removeObject(to.Name) + FCAD.removeObject(tempGroupName) # remove temp directory if already exists + if FCAD.getObject(tempGroupName + '001'): + for to in FCAD.getObject(tempGroupName + '001').Group: + FCAD.removeObject(to.Name) + FCAD.removeObject(tempGroupName + '001') # remove temp directory if already exists + tempGroup = FCAD.addObject('App::DocumentObjectGroup', tempGroupName) + tempGroupName = tempGroup.Name + self.tempGroup = tempGroup + tempGroup.purgeTouched() + # Add temp object to temp group folder with following code: + # ... self.tempGroup.addObject(OBJ) + + # Setup cutter for OCL and cutout value for operation - based on tool controller properties + self.cutter = self.setOclCutter(obj) + if self.cutter is False: + PathLog.error(translate('PathSurface', "Canceling 3D Surface operation. Error creating OCL cutter.")) + return + self.toolDiam = self.cutter.getDiameter() + self.radius = self.toolDiam / 2.0 + self.cutOut = (self.toolDiam * (float(obj.StepOver) / 100.0)) + self.gaps = [self.toolDiam, self.toolDiam, self.toolDiam] + + # Get height offset values for later use + self.SafeHeightOffset = JOB.SetupSheet.SafeHeightOffset.Value + self.ClearHeightOffset = JOB.SetupSheet.ClearanceHeightOffset.Value + + # Calculate default depthparams for operation + self.depthParams = PathUtils.depth_params(obj.ClearanceHeight.Value, obj.SafeHeight.Value, obj.StartDepth.Value, obj.StepDown.Value, 0.0, obj.FinalDepth.Value) + self.midDep = (obj.StartDepth.Value + obj.FinalDepth.Value) / 2.0 + + # Save model visibilities for restoration + if FreeCAD.GuiUp: + for m in range(0, len(JOB.Model.Group)): + mNm = JOB.Model.Group[m].Name + modelVisibility.append(FreeCADGui.ActiveDocument.getObject(mNm).Visibility) + + # Setup STL, model type, and bound box containers for each model in Job + for m in range(0, len(JOB.Model.Group)): + M = JOB.Model.Group[m] + self.modelSTLs.append(False) + self.safeSTLs.append(False) + self.profileShapes.append(False) + # Set bound box + if obj.BoundBox == 'BaseBoundBox': + if M.TypeId.startswith('Mesh'): + self.modelTypes.append('M') # Mesh + self.boundBoxes.append(M.Mesh.BoundBox) + else: + self.modelTypes.append('S') # Solid + self.boundBoxes.append(M.Shape.BoundBox) + elif obj.BoundBox == 'Stock': + self.modelTypes.append('S') # Solid + self.boundBoxes.append(JOB.Stock.Shape.BoundBox) + + # ###### MAIN COMMANDS FOR OPERATION ###### + + # Begin processing obj.Base data and creating GCode + PSF = PathSurfaceSupport.ProcessSelectedFaces(JOB, obj) + PSF.setShowDebugObjects(tempGroup, self.showDebugObjects) + PSF.radius = self.radius + PSF.depthParams = self.depthParams + pPM = PSF.preProcessModel(self.module) + # Process selected faces, if available + if pPM is False: + PathLog.error('Unable to pre-process obj.Base.') + else: + (FACES, VOIDS) = pPM + self.modelSTLs = PSF.modelSTLs + self.profileShapes = PSF.profileShapes + + # Create OCL.stl model objects + self._prepareModelSTLs(JOB, obj) + + for m in range(0, len(JOB.Model.Group)): + Mdl = JOB.Model.Group[m] + if FACES[m] is False: + PathLog.error('No data for model base: {}'.format(JOB.Model.Group[m].Label)) + else: + if m > 0: + # Raise to clearance between models + CMDS.append(Path.Command('N (Transition to base: {}.)'.format(Mdl.Label))) + CMDS.append(Path.Command('G0', {'Z': obj.ClearanceHeight.Value, 'F': self.vertRapid})) + PathLog.info('Working on Model.Group[{}]: {}'.format(m, Mdl.Label)) + # make stock-model-voidShapes STL model for avoidance detection on transitions + self._makeSafeSTL(JOB, obj, m, FACES[m], VOIDS[m]) + # Process model/faces - OCL objects must be ready + CMDS.extend(self._processCutAreas(JOB, obj, m, FACES[m], VOIDS[m])) + + # Save gcode produced + self.commandlist.extend(CMDS) + + # ###### CLOSING COMMANDS FOR OPERATION ###### + + # Delete temporary objects + # Restore model visibilities for restoration + if FreeCAD.GuiUp: + FreeCADGui.ActiveDocument.getObject(tempGroupName).Visibility = False + for m in range(0, len(JOB.Model.Group)): + M = JOB.Model.Group[m] + M.Visibility = modelVisibility[m] + + if deleteTempsFlag is True: + for to in tempGroup.Group: + if hasattr(to, 'Group'): + for go in to.Group: + FCAD.removeObject(go.Name) + FCAD.removeObject(to.Name) + FCAD.removeObject(tempGroupName) + else: + if len(tempGroup.Group) == 0: + FCAD.removeObject(tempGroupName) + else: + tempGroup.purgeTouched() + + # Provide user feedback for gap sizes + gaps = list() + for g in self.gaps: + if g != self.toolDiam: + gaps.append(g) + if len(gaps) > 0: + obj.GapSizes = '{} mm'.format(gaps) + else: + if self.closedGap is True: + obj.GapSizes = 'Closed gaps < Gap Threshold.' + else: + obj.GapSizes = 'No gaps identified.' + + # clean up class variables + self.resetOpVariables() + self.deleteOpVariables() + + self.modelSTLs = None + self.safeSTLs = None + self.modelTypes = None + self.boundBoxes = None + self.gaps = None + self.closedGap = None + self.SafeHeightOffset = None + self.ClearHeightOffset = None + self.depthParams = None + self.midDep = None + del self.modelSTLs + del self.safeSTLs + del self.modelTypes + del self.boundBoxes + del self.gaps + del self.closedGap + del self.SafeHeightOffset + del self.ClearHeightOffset + del self.depthParams + del self.midDep + + execTime = time.time() - startTime + PathLog.info('Operation time: {} sec.'.format(execTime)) + + return True + + # Methods for constructing the cut area + def _prepareModelSTLs(self, JOB, obj): + PathLog.debug('_prepareModelSTLs()') + for m in range(0, len(JOB.Model.Group)): + M = JOB.Model.Group[m] + + # PathLog.debug(f" -self.modelTypes[{m}] == 'M'") + if self.modelTypes[m] == 'M': + # TODO: test if this works + facets = M.Mesh.Facets.Points + else: + facets = Part.getFacets(M.Shape) + + if self.modelSTLs[m] is True: + stl = ocl.STLSurf() + + for tri in facets: + t = ocl.Triangle(ocl.Point(tri[0][0], tri[0][1], tri[0][2]), + ocl.Point(tri[1][0], tri[1][1], tri[1][2]), + ocl.Point(tri[2][0], tri[2][1], tri[2][2])) + stl.addTriangle(t) + self.modelSTLs[m] = stl + return + + def _makeSafeSTL(self, JOB, obj, mdlIdx, faceShapes, voidShapes): + '''_makeSafeSTL(JOB, obj, mdlIdx, faceShapes, voidShapes)... + Creates and OCL.stl object with combined data with waste stock, + model, and avoided faces. Travel lines can be checked against this + STL object to determine minimum travel height to clear stock and model.''' + PathLog.debug('_makeSafeSTL()') + + fuseShapes = list() + Mdl = JOB.Model.Group[mdlIdx] + mBB = Mdl.Shape.BoundBox + sBB = JOB.Stock.Shape.BoundBox + + # add Model shape to safeSTL shape + fuseShapes.append(Mdl.Shape) + + if obj.BoundBox == 'BaseBoundBox': + cont = False + extFwd = (sBB.ZLength) + zmin = mBB.ZMin + zmax = mBB.ZMin + extFwd + stpDwn = (zmax - zmin) / 4.0 + dep_par = PathUtils.depth_params(zmax + 5.0, zmax + 3.0, zmax, stpDwn, 0.0, zmin) + + try: + envBB = PathUtils.getEnvelope(partshape=Mdl.Shape, depthparams=dep_par) # Produces .Shape + cont = True + except Exception as ee: + PathLog.error(str(ee)) + shell = Mdl.Shape.Shells[0] + solid = Part.makeSolid(shell) + try: + envBB = PathUtils.getEnvelope(partshape=solid, depthparams=dep_par) # Produces .Shape + cont = True + except Exception as eee: + PathLog.error(str(eee)) + + if cont: + stckWst = JOB.Stock.Shape.cut(envBB) + if obj.BoundaryAdjustment > 0.0: + cmpndFS = Part.makeCompound(faceShapes) + baBB = PathUtils.getEnvelope(partshape=cmpndFS, depthparams=self.depthParams) # Produces .Shape + adjStckWst = stckWst.cut(baBB) + else: + adjStckWst = stckWst + fuseShapes.append(adjStckWst) + else: + PathLog.warning('Path transitions might not avoid the model. Verify paths.') + else: + # If boundbox is Job.Stock, add hidden pad under stock as base plate + toolDiam = self.cutter.getDiameter() + zMin = JOB.Stock.Shape.BoundBox.ZMin + xMin = JOB.Stock.Shape.BoundBox.XMin - toolDiam + yMin = JOB.Stock.Shape.BoundBox.YMin - toolDiam + bL = JOB.Stock.Shape.BoundBox.XLength + (2 * toolDiam) + bW = JOB.Stock.Shape.BoundBox.YLength + (2 * toolDiam) + bH = 1.0 + crnr = FreeCAD.Vector(xMin, yMin, zMin - 1.0) + B = Part.makeBox(bL, bW, bH, crnr, FreeCAD.Vector(0, 0, 1)) + fuseShapes.append(B) + + if voidShapes is not False: + voidComp = Part.makeCompound(voidShapes) + voidEnv = PathUtils.getEnvelope(partshape=voidComp, depthparams=self.depthParams) # Produces .Shape + fuseShapes.append(voidEnv) + + fused = Part.makeCompound(fuseShapes) + + if self.showDebugObjects: + T = FreeCAD.ActiveDocument.addObject('Part::Feature', 'safeSTLShape') + T.Shape = fused + T.purgeTouched() + self.tempGroup.addObject(T) + + facets = Part.getFacets(fused) + + stl = ocl.STLSurf() + for tri in facets: + t = ocl.Triangle(ocl.Point(tri[0][0], tri[0][1], tri[0][2]), + ocl.Point(tri[1][0], tri[1][1], tri[1][2]), + ocl.Point(tri[2][0], tri[2][1], tri[2][2])) + stl.addTriangle(t) + + self.safeSTLs[mdlIdx] = stl + + def _processCutAreas(self, JOB, obj, mdlIdx, FCS, VDS): + '''_processCutAreas(JOB, obj, mdlIdx, FCS, VDS)... + This method applies any avoided faces or regions to the selected faces. + It then calls the correct scan method depending on the ScanType property.''' + PathLog.debug('_processCutAreas()') + + final = list() + + # Process faces Collectively or Individually + if obj.HandleMultipleFeatures == 'Collectively': + if FCS is True: + COMP = False + else: + ADD = Part.makeCompound(FCS) + if VDS is not False: + DEL = Part.makeCompound(VDS) + COMP = ADD.cut(DEL) + else: + COMP = ADD + + if obj.ScanType == 'Planar': + final.extend(self._processPlanarOp(JOB, obj, mdlIdx, COMP, 0)) + elif obj.ScanType == 'Rotational': + final.extend(self._processRotationalOp(JOB, obj, mdlIdx, COMP)) + + elif obj.HandleMultipleFeatures == 'Individually': + for fsi in range(0, len(FCS)): + fShp = FCS[fsi] + # self.deleteOpVariables(all=False) + self.resetOpVariables(all=False) + + if fShp is True: + COMP = False + else: + ADD = Part.makeCompound([fShp]) + if VDS is not False: + DEL = Part.makeCompound(VDS) + COMP = ADD.cut(DEL) + else: + COMP = ADD + + if obj.ScanType == 'Planar': + final.extend(self._processPlanarOp(JOB, obj, mdlIdx, COMP, fsi)) + elif obj.ScanType == 'Rotational': + final.extend(self._processRotationalOp(JOB, obj, mdlIdx, COMP)) + COMP = None + # Eif + + return final + + # Methods for creating path geometry + def _processPlanarOp(self, JOB, obj, mdlIdx, cmpdShp, fsi): + '''_processPlanarOp(JOB, obj, mdlIdx, cmpdShp)... + This method compiles the main components for the procedural portion of a planar operation (non-rotational). + It creates the OCL PathDropCutter objects: model and safeTravel. + It makes the necessary facial geometries for the actual cut area. + It calls the correct Single or Multi-pass method as needed. + It returns the gcode for the operation. ''' + PathLog.debug('_processPlanarOp()') + final = list() + SCANDATA = list() + + def getTransition(two): + first = two[0][0][0] # [step][item][point] + safe = obj.SafeHeight.Value + 0.1 + trans = [[FreeCAD.Vector(first.x, first.y, safe)]] + return trans + + # Compute number and size of stepdowns, and final depth + if obj.LayerMode == 'Single-pass': + depthparams = [obj.FinalDepth.Value] + elif obj.LayerMode == 'Multi-pass': + depthparams = [i for i in self.depthParams] + lenDP = len(depthparams) + + # Prepare PathDropCutter objects with STL data + pdc = self._planarGetPDC(self.modelSTLs[mdlIdx], depthparams[lenDP - 1], obj.SampleInterval.Value, self.cutter) + safePDC = self._planarGetPDC(self.safeSTLs[mdlIdx], depthparams[lenDP - 1], obj.SampleInterval.Value, self.cutter) + + profScan = list() + if obj.ProfileEdges != 'None': + prflShp = self.profileShapes[mdlIdx][fsi] + if prflShp is False: + PathLog.error('No profile shape is False.') + return list() + if self.showDebugObjects: + P = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpNewProfileShape') + P.Shape = prflShp + P.purgeTouched() + self.tempGroup.addObject(P) + # get offset path geometry and perform OCL scan with that geometry + pathOffsetGeom = self._offsetFacesToPointData(obj, prflShp) + if pathOffsetGeom is False: + PathLog.error('No profile geometry returned.') + return list() + profScan = [self._planarPerformOclScan(obj, pdc, pathOffsetGeom, True)] + + geoScan = list() + if obj.ProfileEdges != 'Only': + if self.showDebugObjects: + F = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpCutArea') + F.Shape = cmpdShp + F.purgeTouched() + self.tempGroup.addObject(F) + # get internal path geometry and perform OCL scan with that geometry + PGG = PathSurfaceSupport.PathGeometryGenerator(obj, cmpdShp, obj.CutPattern) + if self.showDebugObjects: + PGG.setDebugObjectsGroup(self.tempGroup) + self.tmpCOM = PGG.getCenterOfPattern() + pathGeom = PGG.generatePathGeometry() + if pathGeom is False: + PathLog.error('No path geometry returned.') + return list() + if obj.CutPattern == 'Offset': + useGeom = self._offsetFacesToPointData(obj, pathGeom, profile=False) + if useGeom is False: + PathLog.error('No profile geometry returned.') + return list() + geoScan = [self._planarPerformOclScan(obj, pdc, useGeom, True)] + else: + geoScan = self._planarPerformOclScan(obj, pdc, pathGeom, False) + + if obj.ProfileEdges == 'Only': # ['None', 'Only', 'First', 'Last'] + SCANDATA.extend(profScan) + if obj.ProfileEdges == 'None': + SCANDATA.extend(geoScan) + if obj.ProfileEdges == 'First': + profScan.append(getTransition(geoScan)) + SCANDATA.extend(profScan) + SCANDATA.extend(geoScan) + if obj.ProfileEdges == 'Last': + SCANDATA.extend(geoScan) + SCANDATA.extend(profScan) + + if len(SCANDATA) == 0: + PathLog.error('No scan data to convert to Gcode.') + return list() + + # Apply depth offset + if obj.DepthOffset.Value != 0.0: + self._planarApplyDepthOffset(SCANDATA, obj.DepthOffset.Value) + + # If cut pattern is `Circular`, there are zero(almost zero) straight lines to optimize + # Store initial `OptimizeLinearPaths` value for later restoration + self.preOLP = obj.OptimizeLinearPaths + if obj.CutPattern in ['Circular', 'CircularZigZag']: + obj.OptimizeLinearPaths = False + + # Process OCL scan data + if obj.LayerMode == 'Single-pass': + final.extend(self._planarDropCutSingle(JOB, obj, pdc, safePDC, depthparams, SCANDATA)) + elif obj.LayerMode == 'Multi-pass': + final.extend(self._planarDropCutMulti(JOB, obj, pdc, safePDC, depthparams, SCANDATA)) + + # If cut pattern is `Circular`, restore initial OLP value + if obj.CutPattern in ['Circular', 'CircularZigZag']: + obj.OptimizeLinearPaths = self.preOLP + + # Raise to safe height between individual faces. + if obj.HandleMultipleFeatures == 'Individually': + final.insert(0, Path.Command('G0', {'Z': obj.SafeHeight.Value, 'F': self.vertRapid})) + + return final + + def _offsetFacesToPointData(self, obj, subShp, profile=True): + PathLog.debug('_offsetFacesToPointData()') + + offsetLists = list() + dist = obj.SampleInterval.Value / 5.0 + # defl = obj.SampleInterval.Value / 5.0 + + if not profile: + # Reverse order of wires in each face - inside to outside + for w in range(len(subShp.Wires) - 1, -1, -1): + W = subShp.Wires[w] + PNTS = W.discretize(Distance=dist) + # PNTS = W.discretize(Deflection=defl) + if self.CutClimb: + PNTS.reverse() + offsetLists.append(PNTS) + else: + # Reference https://forum.freecadweb.org/viewtopic.php?t=28861#p234939 + for fc in subShp.Faces: + # Reverse order of wires in each face - inside to outside + for w in range(len(fc.Wires) - 1, -1, -1): + W = fc.Wires[w] + PNTS = W.discretize(Distance=dist) + # PNTS = W.discretize(Deflection=defl) + if self.CutClimb: + PNTS.reverse() + offsetLists.append(PNTS) + + return offsetLists + + def _planarPerformOclScan(self, obj, pdc, pathGeom, offsetPoints=False): + '''_planarPerformOclScan(obj, pdc, pathGeom, offsetPoints=False)... + Switching function for calling the appropriate path-geometry to OCL points conversion function + for the various cut patterns.''' + PathLog.debug('_planarPerformOclScan()') + SCANS = list() + + if offsetPoints or obj.CutPattern == 'Offset': + PNTSET = PathSurfaceSupport.pathGeomToOffsetPointSet(obj, pathGeom) + for D in PNTSET: + stpOvr = list() + ofst = list() + for I in D: + if I == 'BRK': + stpOvr.append(ofst) + stpOvr.append(I) + ofst = list() + else: + # D format is ((p1, p2), (p3, p4)) + (A, B) = I + ofst.extend(self._planarDropCutScan(pdc, A, B)) + if len(ofst) > 0: + stpOvr.append(ofst) + SCANS.extend(stpOvr) + elif obj.CutPattern in ['Line', 'Spiral', 'ZigZag']: + stpOvr = list() + if obj.CutPattern == 'Line': + PNTSET = PathSurfaceSupport.pathGeomToLinesPointSet(obj, pathGeom, self.CutClimb, self.toolDiam, self.closedGap, self.gaps) + elif obj.CutPattern == 'ZigZag': + PNTSET = PathSurfaceSupport.pathGeomToZigzagPointSet(obj, pathGeom, self.CutClimb, self.toolDiam, self.closedGap, self.gaps) + elif obj.CutPattern == 'Spiral': + PNTSET = PathSurfaceSupport.pathGeomToSpiralPointSet(obj, pathGeom) + + for STEP in PNTSET: + for LN in STEP: + if LN == 'BRK': + stpOvr.append(LN) + else: + # D format is ((p1, p2), (p3, p4)) + (A, B) = LN + stpOvr.append(self._planarDropCutScan(pdc, A, B)) + SCANS.append(stpOvr) + stpOvr = list() + elif obj.CutPattern in ['Circular', 'CircularZigZag']: + # PNTSET is list, by stepover. + # Each stepover is a list containing arc/loop descriptions, (sp, ep, cp) + PNTSET = PathSurfaceSupport.pathGeomToCircularPointSet(obj, pathGeom, self.CutClimb, self.toolDiam, self.closedGap, self.gaps, self.tmpCOM) + + for so in range(0, len(PNTSET)): + stpOvr = list() + erFlg = False + (aTyp, dirFlg, ARCS) = PNTSET[so] + + if dirFlg == 1: # 1 + cMode = True + else: + cMode = False + + for a in range(0, len(ARCS)): + Arc = ARCS[a] + if Arc == 'BRK': + stpOvr.append('BRK') + else: + scan = self._planarCircularDropCutScan(pdc, Arc, cMode) + if scan is False: + erFlg = True + else: + if aTyp == 'L': + scan.append(FreeCAD.Vector(scan[0].x, scan[0].y, scan[0].z)) + stpOvr.append(scan) + if erFlg is False: + SCANS.append(stpOvr) + # Eif + + return SCANS + + def _planarDropCutScan(self, pdc, A, B): + #PNTS = list() + (x1, y1) = A + (x2, y2) = B + path = ocl.Path() # create an empty path object + p1 = ocl.Point(x1, y1, 0) # start-point of line + p2 = ocl.Point(x2, y2, 0) # end-point of line + lo = ocl.Line(p1, p2) # line-object + path.append(lo) # add the line to the path + pdc.setPath(path) + pdc.run() # run dropcutter algorithm on path + CLP = pdc.getCLPoints() + PNTS = [FreeCAD.Vector(p.x, p.y, p.z) for p in CLP] + return PNTS # pdc.getCLPoints() + + def _planarCircularDropCutScan(self, pdc, Arc, cMode): + PNTS = list() + path = ocl.Path() # create an empty path object + (sp, ep, cp) = Arc + + # process list of segment tuples (vect, vect) + p1 = ocl.Point(sp[0], sp[1], 0) # start point of arc + p2 = ocl.Point(ep[0], ep[1], 0) # end point of arc + C = ocl.Point(cp[0], cp[1], 0) # center point of arc + ao = ocl.Arc(p1, p2, C, cMode) # arc object + path.append(ao) # add the arc to the path + pdc.setPath(path) + pdc.run() # run dropcutter algorithm on path + CLP = pdc.getCLPoints() + + # Convert OCL object data to FreeCAD vectors + return [FreeCAD.Vector(p.x, p.y, p.z) for p in CLP] + + # Main planar scan functions + def _planarDropCutSingle(self, JOB, obj, pdc, safePDC, depthparams, SCANDATA): + PathLog.debug('_planarDropCutSingle()') + + GCODE = [Path.Command('N (Beginning of Single-pass layer.)', {})] + tolrnc = JOB.GeometryTolerance.Value + lenSCANDATA = len(SCANDATA) + gDIR = ['G3', 'G2'] + + if self.CutClimb: + gDIR = ['G2', 'G3'] + + # Set `ProfileEdges` specific trigger indexes + peIdx = lenSCANDATA # off by default + if obj.ProfileEdges == 'Only': + peIdx = -1 + elif obj.ProfileEdges == 'First': + peIdx = 0 + elif obj.ProfileEdges == 'Last': + peIdx = lenSCANDATA - 1 + + # Send cutter to x,y position of first point on first line + first = SCANDATA[0][0][0] # [step][item][point] + GCODE.append(Path.Command('G0', {'X': first.x, 'Y': first.y, 'F': self.horizRapid})) + + # Cycle through step-over sections (line segments or arcs) + odd = True + lstStpEnd = None + for so in range(0, lenSCANDATA): + cmds = list() + PRTS = SCANDATA[so] + lenPRTS = len(PRTS) + first = PRTS[0][0] # first point of arc/line stepover group + start = PRTS[0][0] # will change with each line/arc segment + last = None + cmds.append(Path.Command('N (Begin step {}.)'.format(so), {})) + + if so > 0: + if obj.CutPattern == 'CircularZigZag': + if odd: + odd = False + else: + odd = True + minTrnsHght = self._getMinSafeTravelHeight(safePDC, lstStpEnd, first) # Check safe travel height against fullSTL + # cmds.append(Path.Command('N (Transition: last, first: {}, {}: minSTH: {})'.format(lstStpEnd, first, minTrnsHght), {})) + cmds.extend(self._stepTransitionCmds(obj, lstStpEnd, first, minTrnsHght, tolrnc)) + + # Override default `OptimizeLinearPaths` behavior to allow `ProfileEdges` optimization + if so == peIdx or peIdx == -1: + obj.OptimizeLinearPaths = self.preOLP + + # Cycle through current step-over parts + for i in range(0, lenPRTS): + prt = PRTS[i] + lenPrt = len(prt) + if prt == 'BRK': + nxtStart = PRTS[i + 1][0] + minSTH = self._getMinSafeTravelHeight(safePDC, last, nxtStart) # Check safe travel height against fullSTL + cmds.append(Path.Command('N (Break)', {})) + cmds.extend(self._breakCmds(obj, last, nxtStart, minSTH, tolrnc)) + else: + cmds.append(Path.Command('N (part {}.)'.format(i + 1), {})) + start = prt[0] + last = prt[lenPrt - 1] + if so == peIdx or peIdx == -1: + cmds.extend(self._planarSinglepassProcess(obj, prt)) + elif obj.CutPattern in ['Circular', 'CircularZigZag'] and obj.CircularUseG2G3 is True and lenPrt > 2: + (rtnVal, gcode) = self._arcsToG2G3(prt, lenPrt, odd, gDIR, tolrnc) + if rtnVal: + cmds.extend(gcode) + else: + cmds.extend(self._planarSinglepassProcess(obj, prt)) + else: + cmds.extend(self._planarSinglepassProcess(obj, prt)) + cmds.append(Path.Command('N (End of step {}.)'.format(so), {})) + GCODE.extend(cmds) # save line commands + lstStpEnd = last + + # Return `OptimizeLinearPaths` to disabled + if so == peIdx or peIdx == -1: + if obj.CutPattern in ['Circular', 'CircularZigZag']: + obj.OptimizeLinearPaths = False + # Efor + + return GCODE + + def _planarSinglepassProcess(self, obj, PNTS): + output = [] + optimize = obj.OptimizeLinearPaths + lenPNTS = len(PNTS) + lop = None + onLine = False + + # Initialize first three points + nxt = None + pnt = PNTS[0] + prev = FreeCAD.Vector(-442064564.6, 258539656553.27, 3538553425.847) + + # Add temp end point + PNTS.append(FreeCAD.Vector(-4895747464.6, -25855763553.2, 35865763425)) + + # Begin processing ocl points list into gcode + for i in range(0, lenPNTS): + # Calculate next point for consideration with current point + nxt = PNTS[i + 1] + + # Process point + if optimize: + if pnt.isOnLineSegment(prev, nxt): + onLine = True + else: + onLine = False + output.append(Path.Command('G1', {'X': pnt.x, 'Y': pnt.y, 'Z': pnt.z, 'F': self.horizFeed})) + else: + output.append(Path.Command('G1', {'X': pnt.x, 'Y': pnt.y, 'Z': pnt.z, 'F': self.horizFeed})) + + # Rotate point data + if onLine is False: + prev = pnt + pnt = nxt + # Efor + + PNTS.pop() # Remove temp end point + + return output + + def _planarDropCutMulti(self, JOB, obj, pdc, safePDC, depthparams, SCANDATA): + GCODE = [Path.Command('N (Beginning of Multi-pass layers.)', {})] + tolrnc = JOB.GeometryTolerance.Value + lenDP = len(depthparams) + prevDepth = depthparams[0] + lenSCANDATA = len(SCANDATA) + gDIR = ['G3', 'G2'] + + if self.CutClimb: + gDIR = ['G2', 'G3'] + + # Set `ProfileEdges` specific trigger indexes + peIdx = lenSCANDATA # off by default + if obj.ProfileEdges == 'Only': + peIdx = -1 + elif obj.ProfileEdges == 'First': + peIdx = 0 + elif obj.ProfileEdges == 'Last': + peIdx = lenSCANDATA - 1 + + # Process each layer in depthparams + prvLyrFirst = None + prvLyrLast = None + lastPrvStpLast = None + for lyr in range(0, lenDP): + odd = True # ZigZag directional switch + lyrHasCmds = False + actvSteps = 0 + LYR = list() + prvStpFirst = None + if lyr > 0: + if prvStpLast is not None: + lastPrvStpLast = prvStpLast + prvStpLast = None + lyrDep = depthparams[lyr] + PathLog.debug('Multi-pass lyrDep: {}'.format(round(lyrDep, 4))) + + # Cycle through step-over sections (line segments or arcs) + for so in range(0, len(SCANDATA)): + SO = SCANDATA[so] + lenSO = len(SO) + + # Pre-process step-over parts for layer depth and holds + ADJPRTS = list() + LMAX = list() + soHasPnts = False + brkFlg = False + for i in range(0, lenSO): + prt = SO[i] + lenPrt = len(prt) + if prt == 'BRK': + if brkFlg: + ADJPRTS.append(prt) + LMAX.append(prt) + brkFlg = False + else: + (PTS, lMax) = self._planarMultipassPreProcess(obj, prt, prevDepth, lyrDep) + if len(PTS) > 0: + ADJPRTS.append(PTS) + soHasPnts = True + brkFlg = True + LMAX.append(lMax) + # Efor + lenAdjPrts = len(ADJPRTS) + + # Process existing parts within current step over + prtsHasCmds = False + stepHasCmds = False + prtsCmds = list() + stpOvrCmds = list() + transCmds = list() + if soHasPnts is True: + first = ADJPRTS[0][0] # first point of arc/line stepover group + + # Manage step over transition and CircularZigZag direction + if so > 0: + # PathLog.debug(' stepover index: {}'.format(so)) + # Control ZigZag direction + if obj.CutPattern == 'CircularZigZag': + if odd is True: + odd = False + else: + odd = True + # Control step over transition + if prvStpLast is None: + prvStpLast = lastPrvStpLast + minTrnsHght = self._getMinSafeTravelHeight(safePDC, prvStpLast, first, minDep=None) # Check safe travel height against fullSTL + transCmds.append(Path.Command('N (--Step {} transition)'.format(so), {})) + transCmds.extend(self._stepTransitionCmds(obj, prvStpLast, first, minTrnsHght, tolrnc)) + + # Override default `OptimizeLinearPaths` behavior to allow `ProfileEdges` optimization + if so == peIdx or peIdx == -1: + obj.OptimizeLinearPaths = self.preOLP + + # Cycle through current step-over parts + for i in range(0, lenAdjPrts): + prt = ADJPRTS[i] + lenPrt = len(prt) + # PathLog.debug(' adj parts index - lenPrt: {} - {}'.format(i, lenPrt)) + if prt == 'BRK' and prtsHasCmds is True: + nxtStart = ADJPRTS[i + 1][0] + minSTH = self._getMinSafeTravelHeight(safePDC, last, nxtStart, minDep=None) # Check safe travel height against fullSTL + prtsCmds.append(Path.Command('N (--Break)', {})) + prtsCmds.extend(self._breakCmds(obj, last, nxtStart, minSTH, tolrnc)) + else: + segCmds = False + prtsCmds.append(Path.Command('N (part {})'.format(i + 1), {})) + last = prt[lenPrt - 1] + if so == peIdx or peIdx == -1: + segCmds = self._planarSinglepassProcess(obj, prt) + elif obj.CutPattern in ['Circular', 'CircularZigZag'] and obj.CircularUseG2G3 is True and lenPrt > 2: + (rtnVal, gcode) = self._arcsToG2G3(prt, lenPrt, odd, gDIR, tolrnc) + if rtnVal is True: + segCmds = gcode + else: + segCmds = self._planarSinglepassProcess(obj, prt) + else: + segCmds = self._planarSinglepassProcess(obj, prt) + + if segCmds is not False: + prtsCmds.extend(segCmds) + prtsHasCmds = True + prvStpLast = last + # Eif + # Efor + # Eif + + # Return `OptimizeLinearPaths` to disabled + if so == peIdx or peIdx == -1: + if obj.CutPattern in ['Circular', 'CircularZigZag']: + obj.OptimizeLinearPaths = False + + # Compile step over(prts) commands + if prtsHasCmds is True: + stepHasCmds = True + actvSteps += 1 + prvStpFirst = first + stpOvrCmds.extend(transCmds) + stpOvrCmds.append(Path.Command('N (Begin step {}.)'.format(so), {})) + stpOvrCmds.append(Path.Command('G0', {'X': first.x, 'Y': first.y, 'F': self.horizRapid})) + stpOvrCmds.extend(prtsCmds) + stpOvrCmds.append(Path.Command('N (End of step {}.)'.format(so), {})) + + # Layer transition at first active step over in current layer + if actvSteps == 1: + prvLyrFirst = first + LYR.append(Path.Command('N (Layer {} begins)'.format(lyr), {})) + if lyr > 0: + LYR.append(Path.Command('N (Layer transition)', {})) + LYR.append(Path.Command('G0', {'Z': obj.SafeHeight.Value, 'F': self.vertRapid})) + LYR.append(Path.Command('G0', {'X': first.x, 'Y': first.y, 'F': self.horizRapid})) + + if stepHasCmds is True: + lyrHasCmds = True + LYR.extend(stpOvrCmds) + # Eif + + # Close layer, saving commands, if any + if lyrHasCmds is True: + prvLyrLast = last + GCODE.extend(LYR) # save line commands + GCODE.append(Path.Command('N (End of layer {})'.format(lyr), {})) + + # Set previous depth + prevDepth = lyrDep + # Efor + + PathLog.debug('Multi-pass op has {} layers (step downs).'.format(lyr + 1)) + + return GCODE + + def _planarMultipassPreProcess(self, obj, LN, prvDep, layDep): + ALL = list() + PTS = list() + optLinTrans = obj.OptimizeStepOverTransitions + safe = math.ceil(obj.SafeHeight.Value) + + if optLinTrans is True: + for P in LN: + ALL.append(P) + # Handle layer depth AND hold points + if P.z <= layDep: + PTS.append(FreeCAD.Vector(P.x, P.y, layDep)) + elif P.z > prvDep: + PTS.append(FreeCAD.Vector(P.x, P.y, safe)) + else: + PTS.append(FreeCAD.Vector(P.x, P.y, P.z)) + # Efor + else: + for P in LN: + ALL.append(P) + # Handle layer depth only + if P.z <= layDep: + PTS.append(FreeCAD.Vector(P.x, P.y, layDep)) + else: + PTS.append(FreeCAD.Vector(P.x, P.y, P.z)) + # Efor + + if optLinTrans is True: + # Remove leading and trailing Hold Points + popList = list() + for i in range(0, len(PTS)): # identify leading string + if PTS[i].z == safe: + popList.append(i) + else: + break + popList.sort(reverse=True) + for p in popList: # Remove hold points + PTS.pop(p) + ALL.pop(p) + popList = list() + for i in range(len(PTS) - 1, -1, -1): # identify trailing string + if PTS[i].z == safe: + popList.append(i) + else: + break + popList.sort(reverse=True) + for p in popList: # Remove hold points + PTS.pop(p) + ALL.pop(p) + + # Determine max Z height for remaining points on line + lMax = obj.FinalDepth.Value + if len(ALL) > 0: + lMax = ALL[0].z + for P in ALL: + if P.z > lMax: + lMax = P.z + + return (PTS, lMax) + + def _planarMultipassProcess(self, obj, PNTS, lMax): + output = list() + optimize = obj.OptimizeLinearPaths + safe = math.ceil(obj.SafeHeight.Value) + lenPNTS = len(PNTS) + prcs = True + onHold = False + onLine = False + clrScnLn = lMax + 2.0 + + # Initialize first three points + nxt = None + pnt = PNTS[0] + prev = FreeCAD.Vector(-442064564.6, 258539656553.27, 3538553425.847) + + # Add temp end point + PNTS.append(FreeCAD.Vector(-4895747464.6, -25855763553.2, 35865763425)) + + # Begin processing ocl points list into gcode + for i in range(0, lenPNTS): + prcs = True + nxt = PNTS[i + 1] + + if pnt.z == safe: + prcs = False + if onHold is False: + onHold = True + output.append( Path.Command('N (Start hold)', {}) ) + output.append( Path.Command('G0', {'Z': clrScnLn, 'F': self.vertRapid}) ) + else: + if onHold is True: + onHold = False + output.append( Path.Command('N (End hold)', {}) ) + output.append( Path.Command('G0', {'X': pnt.x, 'Y': pnt.y, 'F': self.horizRapid}) ) + + # Process point + if prcs is True: + if optimize is True: + # iPOL = prev.isOnLineSegment(nxt, pnt) + iPOL = pnt.isOnLineSegment(prev, nxt) + if iPOL is True: + onLine = True + else: + onLine = False + output.append(Path.Command('G1', {'X': pnt.x, 'Y': pnt.y, 'Z': pnt.z, 'F': self.horizFeed})) + else: + output.append(Path.Command('G1', {'X': pnt.x, 'Y': pnt.y, 'Z': pnt.z, 'F': self.horizFeed})) + + # Rotate point data + if onLine is False: + prev = pnt + pnt = nxt + # Efor + + PNTS.pop() # Remove temp end point + + return output + + def _stepTransitionCmds(self, obj, lstPnt, first, minSTH, tolrnc): + cmds = list() + rtpd = False + horizGC = 'G0' + hSpeed = self.horizRapid + height = obj.SafeHeight.Value + + if obj.CutPattern in ['Line', 'Circular']: + if obj.OptimizeStepOverTransitions is True: + height = minSTH + 2.0 + # if obj.LayerMode == 'Multi-pass': + # rtpd = minSTH + elif obj.CutPattern in ['ZigZag', 'CircularZigZag']: + if obj.OptimizeStepOverTransitions is True: + zChng = first.z - lstPnt.z + # PathLog.debug('first.z: {}'.format(first.z)) + # PathLog.debug('lstPnt.z: {}'.format(lstPnt.z)) + # PathLog.debug('zChng: {}'.format(zChng)) + # PathLog.debug('minSTH: {}'.format(minSTH)) + if abs(zChng) < tolrnc: # transitions to same Z height + PathLog.debug('abs(zChng) < tolrnc') + if (minSTH - first.z) > tolrnc: + PathLog.debug('(minSTH - first.z) > tolrnc') + height = minSTH + 2.0 + else: + PathLog.debug('ELSE (minSTH - first.z) > tolrnc') + horizGC = 'G1' + height = first.z + elif (minSTH + (2.0 * tolrnc)) >= max(first.z, lstPnt.z): + height = False # allow end of Zig to cut to beginning of Zag + + + # Create raise, shift, and optional lower commands + if height is not False: + cmds.append(Path.Command('G0', {'Z': height, 'F': self.vertRapid})) + cmds.append(Path.Command(horizGC, {'X': first.x, 'Y': first.y, 'F': hSpeed})) + if rtpd is not False: # ReturnToPreviousDepth + cmds.append(Path.Command('G0', {'Z': rtpd, 'F': self.vertRapid})) + + return cmds + + def _breakCmds(self, obj, lstPnt, first, minSTH, tolrnc): + cmds = list() + rtpd = False + horizGC = 'G0' + hSpeed = self.horizRapid + height = obj.SafeHeight.Value + + if obj.CutPattern in ['Line', 'Circular']: + if obj.OptimizeStepOverTransitions is True: + height = minSTH + 2.0 + elif obj.CutPattern in ['ZigZag', 'CircularZigZag']: + if obj.OptimizeStepOverTransitions is True: + zChng = first.z - lstPnt.z + if abs(zChng) < tolrnc: # transitions to same Z height + if (minSTH - first.z) > tolrnc: + height = minSTH + 2.0 + else: + height = first.z + 2.0 # first.z + + cmds.append(Path.Command('G0', {'Z': height, 'F': self.vertRapid})) + cmds.append(Path.Command(horizGC, {'X': first.x, 'Y': first.y, 'F': hSpeed})) + if rtpd is not False: # ReturnToPreviousDepth + cmds.append(Path.Command('G0', {'Z': rtpd, 'F': self.vertRapid})) + + return cmds + + def _arcsToG2G3(self, LN, numPts, odd, gDIR, tolrnc): + cmds = list() + strtPnt = LN[0] + endPnt = LN[numPts - 1] + strtHght = strtPnt.z + coPlanar = True + isCircle = False + gdi = 0 + if odd is True: + gdi = 1 + + # Test if pnt set is circle + if abs(strtPnt.x - endPnt.x) < tolrnc: + if abs(strtPnt.y - endPnt.y) < tolrnc: + if abs(strtPnt.z - endPnt.z) < tolrnc: + isCircle = True + isCircle = False + + if isCircle is True: + # convert LN to G2/G3 arc, consolidating GCode + # https://wiki.shapeoko.com/index.php/G-Code#G2_-_clockwise_arc + # https://www.cnccookbook.com/cnc-g-code-arc-circle-g02-g03/ + # Dividing circle into two arcs allows for G2/G3 on inclined surfaces + + # ijk = self.tmpCOM - strtPnt # vector from start to center + ijk = self.tmpCOM - strtPnt # vector from start to center + xyz = self.tmpCOM.add(ijk) # end point + cmds.append(Path.Command('G1', {'X': strtPnt.x, 'Y': strtPnt.y, 'Z': strtPnt.z, 'F': self.horizFeed})) + cmds.append(Path.Command(gDIR[gdi], {'X': xyz.x, 'Y': xyz.y, 'Z': xyz.z, + 'I': ijk.x, 'J': ijk.y, 'K': ijk.z, # leave same xyz.z height + 'F': self.horizFeed})) + cmds.append(Path.Command('G1', {'X': xyz.x, 'Y': xyz.y, 'Z': xyz.z, 'F': self.horizFeed})) + ijk = self.tmpCOM - xyz # vector from start to center + rst = strtPnt # end point + cmds.append(Path.Command(gDIR[gdi], {'X': rst.x, 'Y': rst.y, 'Z': rst.z, + 'I': ijk.x, 'J': ijk.y, 'K': ijk.z, # leave same xyz.z height + 'F': self.horizFeed})) + cmds.append(Path.Command('G1', {'X': strtPnt.x, 'Y': strtPnt.y, 'Z': strtPnt.z, 'F': self.horizFeed})) + else: + for pt in LN: + if abs(pt.z - strtHght) > tolrnc: # test for horizontal coplanar + coPlanar = False + break + if coPlanar is True: + # ijk = self.tmpCOM - strtPnt + ijk = self.tmpCOM.sub(strtPnt) # vector from start to center + xyz = endPnt + cmds.append(Path.Command('G1', {'X': strtPnt.x, 'Y': strtPnt.y, 'Z': strtPnt.z, 'F': self.horizFeed})) + cmds.append(Path.Command(gDIR[gdi], {'X': xyz.x, 'Y': xyz.y, 'Z': xyz.z, + 'I': ijk.x, 'J': ijk.y, 'K': ijk.z, # leave same xyz.z height + 'F': self.horizFeed})) + cmds.append(Path.Command('G1', {'X': endPnt.x, 'Y': endPnt.y, 'Z': endPnt.z, 'F': self.horizFeed})) + + return (coPlanar, cmds) + + def _planarApplyDepthOffset(self, SCANDATA, DepthOffset): + PathLog.debug('Applying DepthOffset value: {}'.format(DepthOffset)) + lenScans = len(SCANDATA) + for s in range(0, lenScans): + SO = SCANDATA[s] # StepOver + numParts = len(SO) + for prt in range(0, numParts): + PRT = SO[prt] + if PRT != 'BRK': + numPts = len(PRT) + for pt in range(0, numPts): + SCANDATA[s][prt][pt].z += DepthOffset + + def _planarGetPDC(self, stl, finalDep, SampleInterval, cutter): + pdc = ocl.PathDropCutter() # create a pdc [PathDropCutter] object + pdc.setSTL(stl) # add stl model + pdc.setCutter(cutter) # add cutter + pdc.setZ(finalDep) # set minimumZ (final / target depth value) + pdc.setSampling(SampleInterval) # set sampling size + return pdc + + + # Main rotational scan functions + def _processRotationalOp(self, JOB, obj, mdlIdx, compoundFaces=None): + PathLog.debug('_processRotationalOp(self, JOB, obj, mdlIdx, compoundFaces=None)') + + base = JOB.Model.Group[mdlIdx] + bb = self.boundBoxes[mdlIdx] + stl = self.modelSTLs[mdlIdx] + + # Rotate model to initial index + initIdx = obj.CutterTilt + obj.StartIndex + if initIdx != 0.0: + self.basePlacement = FreeCAD.ActiveDocument.getObject(base.Name).Placement + if obj.RotationAxis == 'X': + base.Placement = FreeCAD.Placement(FreeCAD.Vector(0.0, 0.0, 0.0), FreeCAD.Rotation(FreeCAD.Vector(1.0, 0.0, 0.0), initIdx)) + else: + base.Placement = FreeCAD.Placement(FreeCAD.Vector(0.0, 0.0, 0.0), FreeCAD.Rotation(FreeCAD.Vector(0.0, 1.0, 0.0), initIdx)) + + # Prepare global holdpoint container + if self.holdPoint is None: + self.holdPoint = FreeCAD.Vector(0.0, 0.0, 0.0) + if self.layerEndPnt is None: + self.layerEndPnt = FreeCAD.Vector(0.0, 0.0, 0.0) + + # Avoid division by zero in rotational scan calculations + if obj.FinalDepth.Value == 0.0: + zero = obj.SampleInterval.Value # 0.00001 + self.FinalDepth = zero + # obj.FinalDepth.Value = 0.0 + else: + self.FinalDepth = obj.FinalDepth.Value + + # Determine boundbox radius based upon xzy limits data + if math.fabs(bb.ZMin) > math.fabs(bb.ZMax): + vlim = bb.ZMin + else: + vlim = bb.ZMax + if obj.RotationAxis == 'X': + # Rotation is around X-axis, cutter moves along same axis + if math.fabs(bb.YMin) > math.fabs(bb.YMax): + hlim = bb.YMin + else: + hlim = bb.YMax + else: + # Rotation is around Y-axis, cutter moves along same axis + if math.fabs(bb.XMin) > math.fabs(bb.XMax): + hlim = bb.XMin + else: + hlim = bb.XMax + + # Compute max radius of stock, as it rotates, and rotational clearance & safe heights + self.bbRadius = math.sqrt(hlim**2 + vlim**2) + self.clearHeight = self.bbRadius + JOB.SetupSheet.ClearanceHeightOffset.Value + self.safeHeight = self.bbRadius + JOB.SetupSheet.ClearanceHeightOffset.Value + + return self._rotationalDropCutterOp(obj, stl, bb) + + def _rotationalDropCutterOp(self, obj, stl, bb): + self.resetTolerance = 0.0000001 # degrees + self.layerEndzMax = 0.0 + commands = [] + scanLines = [] + advances = [] + iSTG = [] + rSTG = [] + rings = [] + lCnt = 0 + rNum = 0 + bbRad = self.bbRadius + + def invertAdvances(advances): + idxs = [1.1] + for adv in advances: + idxs.append(-1 * adv) + idxs.pop(0) + return idxs + + def linesToPointRings(scanLines): + rngs = [] + numPnts = len(scanLines[0]) # Number of points per line along axis, at obj.SampleInterval.Value spacing + for line in scanLines: # extract circular set(ring) of points from scan lines + if len(line) != numPnts: + PathLog.debug('Error: line lengths not equal') + return rngs + + for num in range(0, numPnts): + rngs.append([1.1]) # Initiate new ring + for line in scanLines: # extract circular set(ring) of points from scan lines + rngs[num].append(line[num]) + rngs[num].pop(0) + return rngs + + def indexAdvances(arc, stepDeg): + indexes = [0.0] + numSteps = int(math.floor(arc / stepDeg)) + for ns in range(0, numSteps): + indexes.append(stepDeg) + + travel = sum(indexes) + if arc == 360.0: + indexes.insert(0, 0.0) + else: + indexes.append(arc - travel) + + return indexes + + # Compute number and size of stepdowns, and final depth + if obj.LayerMode == 'Single-pass': + depthparams = [self.FinalDepth] + else: + dep_par = PathUtils.depth_params(self.clearHeight, self.safeHeight, self.bbRadius, obj.StepDown.Value, 0.0, self.FinalDepth) + depthparams = [i for i in dep_par] + prevDepth = depthparams[0] + lenDP = len(depthparams) + + # Set drop cutter extra offset + cdeoX = obj.DropCutterExtraOffset.x + cdeoY = obj.DropCutterExtraOffset.y + + # Set updated bound box values and redefine the new min/mas XY area of the operation based on greatest point radius of model + bb.ZMin = -1 * bbRad + bb.ZMax = bbRad + if obj.RotationAxis == 'X': + bb.YMin = -1 * bbRad + bb.YMax = bbRad + ymin = 0.0 + ymax = 0.0 + xmin = bb.XMin - cdeoX + xmax = bb.XMax + cdeoX + else: + bb.XMin = -1 * bbRad + bb.XMax = bbRad + ymin = bb.YMin - cdeoY + ymax = bb.YMax + cdeoY + xmin = 0.0 + xmax = 0.0 + + # Calculate arc + begIdx = obj.StartIndex + endIdx = obj.StopIndex + if endIdx < begIdx: + begIdx -= 360.0 + arc = endIdx - begIdx + + # Begin gcode operation with raising cutter to safe height + commands.append(Path.Command('G0', {'Z': self.safeHeight, 'F': self.vertRapid})) + + # Complete rotational scans at layer and translate into gcode + for layDep in depthparams: + t_before = time.time() + + # Compute circumference and step angles for current layer + layCircum = 2 * math.pi * layDep + if lenDP == 1: + layCircum = 2 * math.pi * bbRad + + # Set axial feed rates + self.axialFeed = 360 / layCircum * self.horizFeed + self.axialRapid = 360 / layCircum * self.horizRapid + + # Determine step angle. + if obj.RotationAxis == obj.DropCutterDir: # Same == indexed + stepDeg = (self.cutOut / layCircum) * 360.0 + else: + stepDeg = (obj.SampleInterval.Value / layCircum) * 360.0 + + # Limit step angle and determine rotational index angles [indexes]. + if stepDeg > 120.0: + stepDeg = 120.0 + advances = indexAdvances(arc, stepDeg) # Reset for each step down layer + + # Perform rotational indexed scans to layer depth + if obj.RotationAxis == obj.DropCutterDir: # Same == indexed OR parallel + sample = obj.SampleInterval.Value + else: + sample = self.cutOut + scanLines = self._indexedDropCutScan(obj, stl, advances, xmin, ymin, xmax, ymax, layDep, sample) + + # Complete rotation if necessary + if arc == 360.0: + advances.append(360.0 - sum(advances)) + advances.pop(0) + zero = scanLines.pop(0) + scanLines.append(zero) + + # Translate OCL scans into gcode + if obj.RotationAxis == obj.DropCutterDir: # Same == indexed (cutter runs parallel to axis) + + # Translate scan to gcode + sumAdv = begIdx + for sl in range(0, len(scanLines)): + sumAdv += advances[sl] + # Translate scan to gcode + iSTG = self._indexedScanToGcode(obj, sl, scanLines[sl], sumAdv, prevDepth, layDep, lenDP) + commands.extend(iSTG) + + # Raise cutter to safe height after each index cut + commands.append(Path.Command('G0', {'Z': self.clearHeight, 'F': self.vertRapid})) + # Eol + else: + if self.CutClimb is False: + advances = invertAdvances(advances) + advances.reverse() + scanLines.reverse() + + # Begin gcode operation with raising cutter to safe height + commands.append(Path.Command('G0', {'Z': self.clearHeight, 'F': self.vertRapid})) + + # Convert rotational scans into gcode + rings = linesToPointRings(scanLines) + rNum = 0 + for rng in rings: + rSTG = self._rotationalScanToGcode(obj, rng, rNum, prevDepth, layDep, advances) + commands.extend(rSTG) + if arc != 360.0: + clrZ = self.layerEndzMax + self.SafeHeightOffset + commands.append(Path.Command('G0', {'Z': clrZ, 'F': self.vertRapid})) + rNum += 1 + # Eol + + prevDepth = layDep + lCnt += 1 # increment layer count + PathLog.debug("--Layer " + str(lCnt) + ": " + str(len(advances)) + " OCL scans and gcode in " + str(time.time() - t_before) + " s") + # Eol + + return commands + + def _indexedDropCutScan(self, obj, stl, advances, xmin, ymin, xmax, ymax, layDep, sample): + cutterOfst = 0.0 + iCnt = 0 + Lines = [] + result = None + + pdc = ocl.PathDropCutter() # create a pdc + pdc.setCutter(self.cutter) + pdc.setZ(layDep) # set minimumZ (final / ta9rget depth value) + pdc.setSampling(sample) + + # if self.useTiltCutter == True: + if obj.CutterTilt != 0.0: + cutterOfst = layDep * math.sin(math.radians(obj.CutterTilt)) + PathLog.debug("CutterTilt: cutterOfst is " + str(cutterOfst)) + + sumAdv = 0.0 + for adv in advances: + sumAdv += adv + if adv > 0.0: + # Rotate STL object using OCL method + radsRot = math.radians(adv) + if obj.RotationAxis == 'X': + stl.rotate(radsRot, 0.0, 0.0) + else: + stl.rotate(0.0, radsRot, 0.0) + + # Set STL after rotation is made + pdc.setSTL(stl) + + # add Line objects to the path in this loop + if obj.RotationAxis == 'X': + p1 = ocl.Point(xmin, cutterOfst, 0.0) # start-point of line + p2 = ocl.Point(xmax, cutterOfst, 0.0) # end-point of line + else: + p1 = ocl.Point(cutterOfst, ymin, 0.0) # start-point of line + p2 = ocl.Point(cutterOfst, ymax, 0.0) # end-point of line + + # Create line object + if obj.RotationAxis == obj.DropCutterDir: # parallel cut + if obj.CutPattern == 'ZigZag': + if (iCnt % 2 == 0.0): # even + lo = ocl.Line(p1, p2) + else: # odd + lo = ocl.Line(p2, p1) + elif obj.CutPattern == 'Line': + if self.CutClimb is True: + lo = ocl.Line(p2, p1) + else: + lo = ocl.Line(p1, p2) + else: + lo = ocl.Line(p1, p2) # line-object + + path = ocl.Path() # create an empty path object + path.append(lo) # add the line to the path + pdc.setPath(path) # set path + pdc.run() # run drop-cutter on the path + result = pdc.getCLPoints() # request the list of points + + # Convert list of OCL objects to list of Vectors for faster access and Apply depth offset + if obj.DepthOffset.Value != 0.0: + Lines.append([FreeCAD.Vector(p.x, p.y, p.z + obj.DepthOffset.Value) for p in result]) + else: + Lines.append([FreeCAD.Vector(p.x, p.y, p.z) for p in result]) + + iCnt += 1 + # End loop + + # Rotate STL object back to original position using OCL method + reset = -1 * math.radians(sumAdv - self.resetTolerance) + if obj.RotationAxis == 'X': + stl.rotate(reset, 0.0, 0.0) + else: + stl.rotate(0.0, reset, 0.0) + self.resetTolerance = 0.0 + + return Lines + + def _indexedScanToGcode(self, obj, li, CLP, idxAng, prvDep, layerDepth, numDeps): + # generate the path commands + output = [] + optimize = obj.OptimizeLinearPaths + holdCount = 0 + holdStart = False + holdStop = False + zMax = prvDep + lenCLP = len(CLP) + lastCLP = lenCLP - 1 + prev = FreeCAD.Vector(0.0, 0.0, 0.0) + nxt = FreeCAD.Vector(0.0, 0.0, 0.0) + + # Create first point + pnt = CLP[0] + + # Rotate to correct index location + if obj.RotationAxis == 'X': + output.append(Path.Command('G0', {'A': idxAng, 'F': self.axialFeed})) + else: + output.append(Path.Command('G0', {'B': idxAng, 'F': self.axialFeed})) + + if li > 0: + if pnt.z > self.layerEndPnt.z: + clrZ = pnt.z + 2.0 + output.append(Path.Command('G1', {'Z': clrZ, 'F': self.vertRapid})) + else: + output.append(Path.Command('G0', {'Z': self.clearHeight, 'F': self.vertRapid})) + + output.append(Path.Command('G0', {'X': pnt.x, 'Y': pnt.y, 'F': self.horizRapid})) + output.append(Path.Command('G1', {'Z': pnt.z, 'F': self.vertFeed})) + + for i in range(0, lenCLP): + if i < lastCLP: + nxt = CLP[i + 1] + else: + optimize = False + + # Update zMax values + if pnt.z > zMax: + zMax = pnt.z + + if obj.LayerMode == 'Multi-pass': + # if z travels above previous layer, start/continue hold high cycle + if pnt.z > prvDep and optimize is True: + if self.onHold is False: + holdStart = True + self.onHold = True + + if self.onHold is True: + if holdStart is True: + # go to current coordinate + output.append(Path.Command('G1', {'X': pnt.x, 'Y': pnt.y, 'Z': pnt.z, 'F': self.horizFeed})) + # Save holdStart coordinate and prvDep values + self.holdPoint = pnt + holdCount += 1 # Increment hold count + holdStart = False # cancel holdStart + + # hold cutter high until Z value drops below prvDep + if pnt.z <= prvDep: + holdStop = True + + if holdStop is True: + # Send hold and current points to + zMax += 2.0 + for cmd in self.holdStopCmds(obj, zMax, prvDep, pnt, "Hold Stop: in-line"): + output.append(cmd) + # reset necessary hold related settings + zMax = prvDep + holdStop = False + self.onHold = False + self.holdPoint = FreeCAD.Vector(0.0, 0.0, 0.0) + + if self.onHold is False: + if not optimize or not pnt.isOnLineSegment(prev, nxt): + output.append(Path.Command('G1', {'X': pnt.x, 'Y': pnt.y, 'Z': pnt.z, 'F': self.horizFeed})) + + # Rotate point data + prev = pnt + pnt = nxt + output.append(Path.Command('N (End index angle ' + str(round(idxAng, 4)) + ')', {})) + + # Save layer end point for use in transitioning to next layer + self.layerEndPnt = pnt + + return output + + def _rotationalScanToGcode(self, obj, RNG, rN, prvDep, layDep, advances): + '''_rotationalScanToGcode(obj, RNG, rN, prvDep, layDep, advances) ... + Convert rotational scan data to gcode path commands.''' + output = [] + nxtAng = 0 + zMax = 0.0 + nxt = FreeCAD.Vector(0.0, 0.0, 0.0) + + begIdx = obj.StartIndex + endIdx = obj.StopIndex + if endIdx < begIdx: + begIdx -= 360.0 + + # Rotate to correct index location + axisOfRot = 'A' + if obj.RotationAxis == 'Y': + axisOfRot = 'B' + + # Create first point + ang = 0.0 + obj.CutterTilt + pnt = RNG[0] + + # Adjust feed rate based on radius/circumference of cutter. + # Original feed rate based on travel at circumference. + if rN > 0: + if pnt.z >= self.layerEndzMax: + clrZ = pnt.z + 5.0 + output.append(Path.Command('G1', {'Z': clrZ, 'F': self.vertRapid})) + else: + output.append(Path.Command('G1', {'Z': self.clearHeight, 'F': self.vertRapid})) + + output.append(Path.Command('G0', {axisOfRot: ang, 'F': self.axialFeed})) + output.append(Path.Command('G1', {'X': pnt.x, 'Y': pnt.y, 'F': self.axialFeed})) + output.append(Path.Command('G1', {'Z': pnt.z, 'F': self.axialFeed})) + + lenRNG = len(RNG) + lastIdx = lenRNG - 1 + for i in range(0, lenRNG): + if i < lastIdx: + nxtAng = ang + advances[i + 1] + nxt = RNG[i + 1] + + if pnt.z > zMax: + zMax = pnt.z + + output.append(Path.Command('G1', {'X': pnt.x, 'Y': pnt.y, 'Z': pnt.z, axisOfRot: ang, 'F': self.axialFeed})) + pnt = nxt + ang = nxtAng + + # Save layer end point for use in transitioning to next layer + self.layerEndPnt = RNG[0] + self.layerEndzMax = zMax + + return output + + def holdStopCmds(self, obj, zMax, pd, p2, txt): + '''holdStopCmds(obj, zMax, pd, p2, txt) ... Gcode commands to be executed at beginning of hold.''' + cmds = [] + msg = 'N (' + txt + ')' + cmds.append(Path.Command(msg, {})) # Raise cutter rapid to zMax in line of travel + cmds.append(Path.Command('G0', {'Z': zMax, 'F': self.vertRapid})) # Raise cutter rapid to zMax in line of travel + cmds.append(Path.Command('G0', {'X': p2.x, 'Y': p2.y, 'F': self.horizRapid})) # horizontal rapid to current XY coordinate + if zMax != pd: + cmds.append(Path.Command('G0', {'Z': pd, 'F': self.vertRapid})) # drop cutter down rapidly to prevDepth depth + cmds.append(Path.Command('G0', {'Z': p2.z, 'F': self.vertFeed})) # drop cutter down to current Z depth, returning to normal cut path and speed + return cmds + + # Additional support methods + def resetOpVariables(self, all=True): + '''resetOpVariables() ... Reset class variables used for instance of operation.''' + self.holdPoint = None + self.layerEndPnt = None + self.onHold = False + self.SafeHeightOffset = 2.0 + self.ClearHeightOffset = 4.0 + self.layerEndzMax = 0.0 + self.resetTolerance = 0.0 + self.holdPntCnt = 0 + self.bbRadius = 0.0 + self.axialFeed = 0.0 + self.axialRapid = 0.0 + self.FinalDepth = 0.0 + self.clearHeight = 0.0 + self.safeHeight = 0.0 + self.faceZMax = -999999999999.0 + if all is True: + self.cutter = None + self.stl = None + self.fullSTL = None + self.cutOut = 0.0 + self.radius = 0.0 + self.useTiltCutter = False + return True + + def deleteOpVariables(self, all=True): + '''deleteOpVariables() ... Reset class variables used for instance of operation.''' + del self.holdPoint + del self.layerEndPnt + del self.onHold + del self.SafeHeightOffset + del self.ClearHeightOffset + del self.layerEndzMax + del self.resetTolerance + del self.holdPntCnt + del self.bbRadius + del self.axialFeed + del self.axialRapid + del self.FinalDepth + del self.clearHeight + del self.safeHeight + del self.faceZMax + if all is True: + del self.cutter + del self.stl + del self.fullSTL + del self.cutOut + del self.radius + del self.useTiltCutter + return True + + def setOclCutter(self, obj, safe=False): + ''' setOclCutter(obj) ... Translation function to convert FreeCAD tool definition to OCL formatted tool. ''' + # Set cutter details + # https://www.freecadweb.org/api/dd/dfe/classPath_1_1Tool.html#details + diam_1 = float(obj.ToolController.Tool.Diameter) + lenOfst = obj.ToolController.Tool.LengthOffset if hasattr(obj.ToolController.Tool, 'LengthOffset') else 0 + FR = obj.ToolController.Tool.FlatRadius if hasattr(obj.ToolController.Tool, 'FlatRadius') else 0 + CEH = obj.ToolController.Tool.CuttingEdgeHeight if hasattr(obj.ToolController.Tool, 'CuttingEdgeHeight') else 0 + CEA = obj.ToolController.Tool.CuttingEdgeAngle if hasattr(obj.ToolController.Tool, 'CuttingEdgeAngle') else 0 + + # Make safeCutter with 2 mm buffer around physical cutter + if safe is True: + diam_1 += 4.0 + if FR != 0.0: + FR += 2.0 + + PathLog.debug('ToolType: {}'.format(obj.ToolController.Tool.ToolType)) + if obj.ToolController.Tool.ToolType == 'EndMill': + # Standard End Mill + return ocl.CylCutter(diam_1, (CEH + lenOfst)) + + elif obj.ToolController.Tool.ToolType == 'BallEndMill' and FR == 0.0: + # Standard Ball End Mill + # OCL -> BallCutter::BallCutter(diameter, length) + self.useTiltCutter = True + return ocl.BallCutter(diam_1, (diam_1 / 2 + lenOfst)) + + elif obj.ToolController.Tool.ToolType == 'BallEndMill' and FR > 0.0: + # Bull Nose or Corner Radius cutter + # Reference: https://www.fine-tools.com/halbstabfraeser.html + # OCL -> BallCutter::BallCutter(diameter, length) + return ocl.BullCutter(diam_1, FR, (CEH + lenOfst)) + + elif obj.ToolController.Tool.ToolType == 'Engraver' and FR > 0.0: + # Bull Nose or Corner Radius cutter + # Reference: https://www.fine-tools.com/halbstabfraeser.html + # OCL -> ConeCutter::ConeCutter(diameter, angle, lengthOffset) + return ocl.ConeCutter(diam_1, (CEA / 2), lenOfst) + + elif obj.ToolController.Tool.ToolType == 'ChamferMill': + # Bull Nose or Corner Radius cutter + # Reference: https://www.fine-tools.com/halbstabfraeser.html + # OCL -> ConeCutter::ConeCutter(diameter, angle, lengthOffset) + return ocl.ConeCutter(diam_1, (CEA / 2), lenOfst) + else: + # Default to standard end mill + PathLog.warning("Defaulting cutter to standard end mill.") + return ocl.CylCutter(diam_1, (CEH + lenOfst)) + + def _getMinSafeTravelHeight(self, pdc, p1, p2, minDep=None): + A = (p1.x, p1.y) + B = (p2.x, p2.y) + LINE = self._planarDropCutScan(pdc, A, B) + zMax = max([obj.z for obj in LINE]) + if minDep is not None: + if zMax < minDep: + zMax = minDep + return zMax + + +def SetupProperties(): + ''' SetupProperties() ... Return list of properties required for operation.''' + setup = ['AvoidLastX_Faces', 'AvoidLastX_InternalFeatures', 'BoundBox'] + setup.extend(['BoundaryAdjustment', 'PatternCenterAt', 'PatternCenterCustom']) + setup.extend(['CircularUseG2G3', 'InternalFeaturesCut', 'InternalFeaturesAdjustment']) + setup.extend(['CutMode', 'CutPattern', 'CutPatternAngle', 'CutPatternReversed']) + setup.extend(['CutterTilt', 'DepthOffset', 'DropCutterDir', 'GapSizes', 'GapThreshold']) + setup.extend(['HandleMultipleFeatures', 'LayerMode', 'OptimizeStepOverTransitions']) + setup.extend(['ProfileEdges', 'BoundaryEnforcement', 'RotationAxis', 'SampleInterval']) + setup.extend(['ScanType', 'StartIndex', 'StartPoint', 'StepOver', 'StopIndex']) + setup.extend(['UseStartPoint', 'AngularDeflection', 'LinearDeflection', 'ShowTempObjects']) + return setup + + +def Create(name, obj=None): + '''Create(name) ... Creates and returns a Surface operation.''' + if obj is None: + obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython", name) + obj.Proxy = ObjectSurface(obj, name) + return obj diff --git a/src/Mod/Path/PathScripts/PathSurfaceGui.py b/src/Mod/Path/PathScripts/PathSurfaceGui.py index 41f11f6007..7ff1342360 100644 --- a/src/Mod/Path/PathScripts/PathSurfaceGui.py +++ b/src/Mod/Path/PathScripts/PathSurfaceGui.py @@ -41,7 +41,7 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage): def initPage(self, obj): self.setTitle("3D Surface") - self.updateVisibility() + # self.updateVisibility() def getForm(self): '''getForm() ... returns UI''' @@ -118,6 +118,8 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage): else: self.form.optimizeStepOverTransitions.setCheckState(QtCore.Qt.Unchecked) + self.updateVisibility() + def getSignalsForUpdate(self, obj): '''getSignalsForUpdate(obj) ... return list of signals for updating obj''' signals = [] @@ -140,16 +142,26 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage): return signals def updateVisibility(self): - if self.form.scanType.currentText() == "Planar": - self.form.cutPattern.setEnabled(True) - self.form.boundBoxExtraOffsetX.setEnabled(False) - self.form.boundBoxExtraOffsetY.setEnabled(False) - self.form.dropCutterDirSelect.setEnabled(False) - else: - self.form.cutPattern.setEnabled(False) - self.form.boundBoxExtraOffsetX.setEnabled(True) - self.form.boundBoxExtraOffsetY.setEnabled(True) - self.form.dropCutterDirSelect.setEnabled(True) + if self.form.scanType.currentText() == 'Planar': + self.form.cutPattern.show() + self.form.cutPattern_label.show() + self.form.optimizeStepOverTransitions.show() + + self.form.boundBoxExtraOffsetX.hide() + self.form.boundBoxExtraOffsetY.hide() + self.form.boundBoxExtraOffset_label.hide() + self.form.dropCutterDirSelect.hide() + self.form.dropCutterDirSelect_label.hide() + elif self.form.scanType.currentText() == 'Rotational': + self.form.cutPattern.hide() + self.form.cutPattern_label.hide() + self.form.optimizeStepOverTransitions.hide() + + self.form.boundBoxExtraOffsetX.show() + self.form.boundBoxExtraOffsetY.show() + self.form.boundBoxExtraOffset_label.show() + self.form.dropCutterDirSelect.show() + self.form.dropCutterDirSelect_label.show() def registerSignalHandlers(self, obj): self.form.scanType.currentIndexChanged.connect(self.updateVisibility) diff --git a/src/Mod/Path/PathScripts/PathSurfaceSupport.py b/src/Mod/Path/PathScripts/PathSurfaceSupport.py new file mode 100644 index 0000000000..e991b28163 --- /dev/null +++ b/src/Mod/Path/PathScripts/PathSurfaceSupport.py @@ -0,0 +1,1855 @@ +# -*- coding: utf-8 -*- + +# *************************************************************************** +# * * +# * Copyright (c) 2020 russ4262 * +# * * +# * This program is free software; you can redistribute it and/or modify * +# * it under the terms of the GNU Lesser General Public License (LGPL) * +# * as published by the Free Software Foundation; either version 2 of * +# * the License, or (at your option) any later version. * +# * for detail see the LICENCE text file. * +# * * +# * This program is distributed in the hope that it will be useful, * +# * but WITHOUT ANY WARRANTY; without even the implied warranty of * +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# * GNU Library General Public License for more details. * +# * * +# * You should have received a copy of the GNU Library General Public * +# * License along with this program; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# *************************************************************************** + +from __future__ import print_function + +__title__ = "Path Surface Support Module" +__author__ = "russ4262 (Russell Johnson)" +__url__ = "http://www.freecadweb.org" +__doc__ = "Support functions and classes for 3D Surface and Waterline operations." +# __name__ = "PathSurfaceSupport" +__contributors__ = "" + +import FreeCAD +from PySide import QtCore +import Path +import PathScripts.PathLog as PathLog +import PathScripts.PathUtils as PathUtils +import math +import Part + + +PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule()) +# PathLog.trackModule(PathLog.thisModule()) + + +# Qt translation handling +def translate(context, text, disambig=None): + return QtCore.QCoreApplication.translate(context, text, disambig) + + +class PathGeometryGenerator: + '''Creates a path geometry shape from an assigned pattern for conversion to tool paths. + PathGeometryGenerator(obj, shape, pattern) + `obj` is the operation object, `shape` is the horizontal planar shape object, + and `pattern` is the name of the geometric pattern to apply. + Frist, call the getCenterOfPattern() method for the CenterOfMass for patterns allowing a custom center. + Next, call the generatePathGeometry() method to request the path geometry shape.''' + + # Register valid patterns here by name + # Create a corresponding processing method below. Precede the name with an underscore(_) + patterns = ('Circular', 'CircularZigZag', 'Line', 'Offset', 'Spiral', 'ZigZag') + + def __init__(self, obj, shape, pattern): + '''__init__(obj, shape, pattern)... Instantiate PathGeometryGenerator class. + Required arguments are the operation object, horizontal planar shape, and pattern name.''' + self.debugObjectsGroup = False + self.pattern = 'None' + self.shape = None + self.pathGeometry = None + self.rawGeoList = None + self.centerOfMass = None + self.centerofPattern = None + self.deltaX = None + self.deltaY = None + self.deltaC = None + self.halfDiag = None + self.halfPasses = None + self.obj = obj + self.toolDiam = float(obj.ToolController.Tool.Diameter) + self.cutOut = self.toolDiam * (float(obj.StepOver) / 100.0) + self.wpc = Part.makeCircle(2.0) # make circle for workplane + + # validate requested pattern + if pattern in self.patterns: + if hasattr(self, '_' + pattern): + self.pattern = pattern + + if shape.BoundBox.ZMin != 0.0: + shape.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - shape.BoundBox.ZMin)) + if shape.BoundBox.ZLength == 0.0: + self.shape = shape + else: + PathLog.warning('Shape appears to not be horizontal planar. ZMax is {}.'.format(shape.BoundBox.ZMax)) + + self._prepareConstants() + + def _prepareConstants(self): + # Apply drop cutter extra offset and set the max and min XY area of the operation + xmin = self.shape.BoundBox.XMin + xmax = self.shape.BoundBox.XMax + ymin = self.shape.BoundBox.YMin + ymax = self.shape.BoundBox.YMax + + # Compute weighted center of mass of all faces combined + if self.pattern in ['Circular', 'CircularZigZag', 'Spiral']: + if self.obj.PatternCenterAt == 'CenterOfMass': + fCnt = 0 + totArea = 0.0 + zeroCOM = FreeCAD.Vector(0.0, 0.0, 0.0) + for F in self.shape.Faces: + comF = F.CenterOfMass + areaF = F.Area + totArea += areaF + fCnt += 1 + zeroCOM = zeroCOM.add(FreeCAD.Vector(comF.x, comF.y, 0.0).multiply(areaF)) + if fCnt == 0: + PathLog.error(translate(self.module, 'Cannot calculate the Center Of Mass. Using Center of Boundbox instead.')) + bbC = self.shape.BoundBox.Center + zeroCOM = FreeCAD.Vector(bbC.x, bbC.y, 0.0) + else: + avgArea = totArea / fCnt + zeroCOM.multiply(1 / fCnt) + zeroCOM.multiply(1 / avgArea) + self.centerOfMass = FreeCAD.Vector(zeroCOM.x, zeroCOM.y, 0.0) + self.centerOfPattern = self._getPatternCenter() + else: + bbC = self.shape.BoundBox.Center + self.centerOfPattern = FreeCAD.Vector(bbC.x, bbC.y, 0.0) + + # get X, Y, Z spans; Compute center of rotation + self.deltaX = self.shape.BoundBox.XLength + self.deltaY = self.shape.BoundBox.YLength + self.deltaC = self.shape.BoundBox.DiagonalLength # math.sqrt(self.deltaX**2 + self.deltaY**2) + lineLen = self.deltaC + (2.0 * self.toolDiam) # Line length to span boundbox diag with 2x cutter diameter extra on each end + self.halfDiag = math.ceil(lineLen / 2.0) + cutPasses = math.ceil(lineLen / self.cutOut) + 1 # Number of lines(passes) required to cover boundbox diagonal + self.halfPasses = math.ceil(cutPasses / 2.0) + + # Public methods + def setDebugObjectsGroup(self, tmpGrpObject): + '''setDebugObjectsGroup(tmpGrpObject)... + Pass the temporary object group to show temporary construction objects''' + self.debugObjectsGroup = tmpGrpObject + + def getCenterOfPattern(self): + '''getCenterOfPattern()... + Returns the Center Of Mass for the current class instance.''' + return self.centerOfPattern + + def generatePathGeometry(self): + '''generatePathGeometry()... + Call this function to obtain the path geometry shape, generated by this class.''' + if self.pattern == 'None': + PathLog.warning('PGG: No pattern set.') + return False + + if self.shape is None: + PathLog.warning('PGG: No shape set.') + return False + + cmd = 'self._' + self.pattern + '()' + exec(cmd) + + if self.obj.CutPatternReversed is True: + self.rawGeoList.reverse() + + # Create compound object to bind all lines in Lineset + geomShape = Part.makeCompound(self.rawGeoList) + + # Position and rotate the Line and ZigZag geometry + if self.pattern in ['Line', 'ZigZag']: + if self.obj.CutPatternAngle != 0.0: + geomShape.Placement.Rotation = FreeCAD.Rotation(FreeCAD.Vector(0, 0, 1), self.obj.CutPatternAngle) + bbC = self.shape.BoundBox.Center + geomShape.Placement.Base = FreeCAD.Vector(bbC.x, bbC.y, 0.0 - geomShape.BoundBox.ZMin) + + if self.debugObjectsGroup: + F = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpGeometrySet') + F.Shape = geomShape + F.purgeTouched() + self.debugObjectsGroup.addObject(F) + + if self.pattern == 'Offset': + return geomShape + + # Identify intersection of cross-section face and lineset + cmnShape = self.shape.common(geomShape) + + if self.debugObjectsGroup: + F = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpPathGeometry') + F.Shape = cmnShape + F.purgeTouched() + self.debugObjectsGroup.addObject(F) + + return cmnShape + + # Cut pattern methods + def _Circular(self): + GeoSet = list() + radialPasses = self._getRadialPasses() + minRad = self.toolDiam * 0.45 + siX3 = 3 * self.obj.SampleInterval.Value + minRadSI = (siX3 / 2.0) / math.pi + + if minRad < minRadSI: + minRad = minRadSI + + PathLog.debug(' -centerOfPattern: {}'.format(self.centerOfPattern)) + # Make small center circle to start pattern + if self.obj.StepOver > 50: + circle = Part.makeCircle(minRad, self.centerOfPattern) + GeoSet.append(circle) + + for lc in range(1, radialPasses + 1): + rad = (lc * self.cutOut) + if rad >= minRad: + circle = Part.makeCircle(rad, self.centerOfPattern) + GeoSet.append(circle) + # Efor + self.rawGeoList = GeoSet + + def _CircularZigZag(self): + self._Circular() # Use _Circular generator + + def _Line(self): + GeoSet = list() + centRot = FreeCAD.Vector(0.0, 0.0, 0.0) # Bottom left corner of face/selection/model + cAng = math.atan(self.deltaX / self.deltaY) # BoundaryBox angle + + # Determine end points and create top lines + x1 = centRot.x - self.halfDiag + x2 = centRot.x + self.halfDiag + diag = None + if self.obj.CutPatternAngle == 0 or self.obj.CutPatternAngle == 180: + diag = self.deltaY + elif self.obj.CutPatternAngle == 90 or self.obj.CutPatternAngle == 270: + diag = self.deltaX + else: + perpDist = math.cos(cAng - math.radians(self.obj.CutPatternAngle)) * self.deltaC + diag = perpDist + y1 = centRot.y + diag + # y2 = y1 + + # Create end points for set of lines to intersect with cross-section face + pntTuples = list() + for lc in range((-1 * (self.halfPasses - 1)), self.halfPasses + 1): + x1 = centRot.x - self.halfDiag + x2 = centRot.x + self.halfDiag + y1 = centRot.y + (lc * self.cutOut) + # y2 = y1 + p1 = FreeCAD.Vector(x1, y1, 0.0) + p2 = FreeCAD.Vector(x2, y1, 0.0) + pntTuples.append((p1, p2)) + + # Convert end points to lines + for (p1, p2) in pntTuples: + line = Part.makeLine(p1, p2) + GeoSet.append(line) + + self.rawGeoList = GeoSet + + def _Offset(self): + self.rawGeoList = self._extractOffsetFaces() + + def _Spiral(self): + GeoSet = list() + SEGS = list() + draw = True + loopRadians = 0.0 # Used to keep track of complete loops/cycles + sumRadians = 0.0 + loopCnt = 0 + segCnt = 0 + twoPi = 2.0 * math.pi + maxDist = math.ceil(self.cutOut * self._getRadialPasses()) # self.halfDiag + move = self.centerOfPattern # Use to translate the center of the spiral + lastPoint = FreeCAD.Vector(0.0, 0.0, 0.0) + + # Set tool properties and calculate cutout + cutOut = self.cutOut / twoPi + segLen = self.obj.SampleInterval.Value # CutterDiameter / 10.0 # SampleInterval.Value + stepAng = segLen / ((loopCnt + 1) * self.cutOut) # math.pi / 18.0 # 10 degrees + stopRadians = maxDist / cutOut + + if self.obj.CutPatternReversed: + if self.obj.CutMode == 'Conventional': + getPoint = self._makeOppSpiralPnt + else: + getPoint = self._makeRegSpiralPnt + + while draw: + radAng = sumRadians + stepAng + p1 = lastPoint + p2 = getPoint(move, cutOut, radAng) # cutOut is 'b' in the equation r = b * radAng + sumRadians += stepAng # Increment sumRadians + loopRadians += stepAng # Increment loopRadians + if loopRadians > twoPi: + loopCnt += 1 + loopRadians -= twoPi + stepAng = segLen / ((loopCnt + 1) * self.cutOut) # adjust stepAng with each loop/cycle + segCnt += 1 + lastPoint = p2 + if sumRadians > stopRadians: + draw = False + # Create line and show in Object tree + lineSeg = Part.makeLine(p2, p1) + SEGS.append(lineSeg) + # Ewhile + SEGS.reverse() + else: + if self.obj.CutMode == 'Climb': + getPoint = self._makeOppSpiralPnt + else: + getPoint = self._makeRegSpiralPnt + + while draw: + radAng = sumRadians + stepAng + p1 = lastPoint + p2 = getPoint(move, cutOut, radAng) # cutOut is 'b' in the equation r = b * radAng + sumRadians += stepAng # Increment sumRadians + loopRadians += stepAng # Increment loopRadians + if loopRadians > twoPi: + loopCnt += 1 + loopRadians -= twoPi + stepAng = segLen / ((loopCnt + 1) * self.cutOut) # adjust stepAng with each loop/cycle + segCnt += 1 + lastPoint = p2 + if sumRadians > stopRadians: + draw = False + # Create line and show in Object tree + lineSeg = Part.makeLine(p1, p2) + SEGS.append(lineSeg) + # Ewhile + # Eif + spiral = Part.Wire([ls.Edges[0] for ls in SEGS]) + GeoSet.append(spiral) + + self.rawGeoList = GeoSet + + def _ZigZag(self): + self._Line() # Use _Line generator + + # Support methods + def _getPatternCenter(self): + centerAt = self.obj.PatternCenterAt + + if centerAt == 'CenterOfMass': + cntrPnt = FreeCAD.Vector(self.centerOfMass.x, self.centerOfMass.y, 0.0) + elif centerAt == 'CenterOfBoundBox': + cent = self.shape.BoundBox.Center + cntrPnt = FreeCAD.Vector(cent.x, cent.y, 0.0) + elif centerAt == 'XminYmin': + cntrPnt = FreeCAD.Vector(self.shape.BoundBox.XMin, self.shape.BoundBox.YMin, 0.0) + elif centerAt == 'Custom': + cntrPnt = FreeCAD.Vector(self.obj.PatternCenterCustom.x, self.obj.PatternCenterCustom.y, 0.0) + + # Update centerOfPattern point + if centerAt != 'Custom': + self.obj.PatternCenterCustom = cntrPnt + self.centerOfPattern = cntrPnt + + return cntrPnt + + def _getRadialPasses(self): + # recalculate number of passes, if need be + radialPasses = self.halfPasses + if self.obj.PatternCenterAt != 'CenterOfBoundBox': + # make 4 corners of boundbox in XY plane, find which is greatest distance to new circular center + EBB = self.shape.BoundBox + CORNERS = [ + FreeCAD.Vector(EBB.XMin, EBB.YMin, 0.0), + FreeCAD.Vector(EBB.XMin, EBB.YMax, 0.0), + FreeCAD.Vector(EBB.XMax, EBB.YMax, 0.0), + FreeCAD.Vector(EBB.XMax, EBB.YMin, 0.0), + ] + dMax = 0.0 + for c in range(0, 4): + dist = CORNERS[c].sub(self.centerOfPattern).Length + if dist > dMax: + dMax = dist + diag = dMax + (2.0 * self.toolDiam) # Line length to span boundbox diag with 2x cutter diameter extra on each end + radialPasses = math.ceil(diag / self.cutOut) + 1 # Number of lines(passes) required to cover boundbox diagonal + + return radialPasses + + def _makeRegSpiralPnt(self, move, b, radAng): + x = b * radAng * math.cos(radAng) + y = b * radAng * math.sin(radAng) + return FreeCAD.Vector(x, y, 0.0).add(move) + + def _makeOppSpiralPnt(self, move, b, radAng): + x = b * radAng * math.cos(radAng) + y = b * radAng * math.sin(radAng) + return FreeCAD.Vector(-1 * x, y, 0.0).add(move) + + def _extractOffsetFaces(self): + PathLog.debug('_extractOffsetFaces()') + wires = list() + faces = list() + ofst = 0.0 # - self.cutOut + shape = self.shape + cont = True + cnt = 0 + while cont: + ofstArea = self._getFaceOffset(shape, ofst) + if not ofstArea: + PathLog.warning('PGG: No offset clearing area returned.') + cont = False + break + for F in ofstArea.Faces: + faces.append(F) + for w in F.Wires: + wires.append(w) + shape = ofstArea + if cnt == 0: + ofst = 0.0 - self.cutOut + cnt += 1 + return wires + + def _getFaceOffset(self, shape, offset): + '''_getFaceOffset(shape, offset) ... internal function. + Original _buildPathArea() version copied from PathAreaOp.py module. This version is modified. + Adjustments made based on notes by @sliptonic at this webpage: https://github.com/sliptonic/FreeCAD/wiki/PathArea-notes.''' + PathLog.debug('_getFaceOffset()') + + areaParams = {} + areaParams['Offset'] = offset + areaParams['Fill'] = 1 # 1 + areaParams['Coplanar'] = 0 + areaParams['SectionCount'] = 1 # -1 = full(all per depthparams??) sections + areaParams['Reorient'] = True + areaParams['OpenMode'] = 0 + areaParams['MaxArcPoints'] = 400 # 400 + areaParams['Project'] = True + + area = Path.Area() # Create instance of Area() class object + # area.setPlane(PathUtils.makeWorkplane(shape)) # Set working plane + area.setPlane(PathUtils.makeWorkplane(self.wpc)) # Set working plane to normal at Z=1 + area.add(shape) + area.setParams(**areaParams) # set parameters + + offsetShape = area.getShape() + wCnt = len(offsetShape.Wires) + if wCnt == 0: + return False + elif wCnt == 1: + ofstFace = Part.Face(offsetShape.Wires[0]) + else: + W = list() + for wr in offsetShape.Wires: + W.append(Part.Face(wr)) + ofstFace = Part.makeCompound(W) + + return ofstFace +# Eclass + + +class ProcessSelectedFaces: + """ProcessSelectedFaces(JOB, obj) class. + This class processes the `obj.Base` object for selected geometery. + Calling the preProcessModel(module) method returns + two compound objects as a tuple: (FACES, VOIDS) or False.""" + + def __init__(self, JOB, obj): + self.modelSTLs = list() + self.profileShapes = list() + self.tempGroup = False + self.showDebugObjects = False + self.checkBase = False + self.module = None + self.radius = None + self.depthParams = None + self.msgNoFaces = translate(self.module, 'Face selection is unavailable for Rotational scans. Ignoring selected faces.') + self.JOB = JOB + self.obj = obj + self.profileEdges = 'None' + + if hasattr(obj, 'ProfileEdges'): + self.profileEdges = obj.ProfileEdges + + # Setup STL, model type, and bound box containers for each model in Job + for m in range(0, len(JOB.Model.Group)): + M = JOB.Model.Group[m] + self.modelSTLs.append(False) + self.profileShapes.append(False) + + # make circle for workplane + self.wpc = Part.makeCircle(2.0) + + def PathSurface(self): + if self.obj.Base: + if len(self.obj.Base) > 0: + self.checkBase = True + if self.obj.ScanType == 'Rotational': + self.checkBase = False + PathLog.warning(self.msgNoFaces) + + def PathWaterline(self): + if self.obj.Base: + if len(self.obj.Base) > 0: + self.checkBase = True + if self.obj.Algorithm in ['OCL Dropcutter', 'Experimental']: + self.checkBase = False + PathLog.warning(self.msgNoFaces) + + # public class methods + def setShowDebugObjects(self, grpObj, val): + self.tempGroup = grpObj + self.showDebugObjects = val + + def preProcessModel(self, module): + PathLog.debug('preProcessModel()') + + if not self._isReady(module): + return False + + FACES = list() + VOIDS = list() + fShapes = list() + vShapes = list() + GRP = self.JOB.Model.Group + lenGRP = len(GRP) + + # Crete place holders for each base model in Job + for m in range(0, lenGRP): + FACES.append(False) + VOIDS.append(False) + fShapes.append(False) + vShapes.append(False) + + # The user has selected subobjects from the base. Pre-Process each. + if self.checkBase: + PathLog.debug(' -obj.Base exists. Pre-processing for selected faces.') + + # (FACES, VOIDS) = self._identifyFacesAndVoids(FACES, VOIDS) + (F, V) = self._identifyFacesAndVoids(FACES, VOIDS) + + # Cycle through each base model, processing faces for each + for m in range(0, lenGRP): + base = GRP[m] + (mFS, mVS, mPS) = self._preProcessFacesAndVoids(base, m, FACES, VOIDS) + fShapes[m] = mFS + vShapes[m] = mVS + self.profileShapes[m] = mPS + else: + PathLog.debug(' -No obj.Base data.') + for m in range(0, lenGRP): + self.modelSTLs[m] = True + + # Process each model base, as a whole, as needed + # PathLog.debug(' -Pre-processing all models in Job.') + for m in range(0, lenGRP): + if fShapes[m] is False: + PathLog.debug(' -Pre-processing {} as a whole.'.format(GRP[m].Label)) + if self.obj.BoundBox == 'BaseBoundBox': + base = GRP[m] + elif self.obj.BoundBox == 'Stock': + base = self.JOB.Stock + + pPEB = self._preProcessEntireBase(base, m) + if pPEB is False: + PathLog.error(' -Failed to pre-process base as a whole.') + else: + (fcShp, prflShp) = pPEB + if fcShp is not False: + if fcShp is True: + PathLog.debug(' -fcShp is True.') + fShapes[m] = True + else: + fShapes[m] = [fcShp] + if prflShp is not False: + if fcShp is not False: + PathLog.debug('vShapes[{}]: {}'.format(m, vShapes[m])) + if vShapes[m] is not False: + PathLog.debug(' -Cutting void from base profile shape.') + adjPS = prflShp.cut(vShapes[m][0]) + self.profileShapes[m] = [adjPS] + else: + PathLog.debug(' -vShapes[m] is False.') + self.profileShapes[m] = [prflShp] + else: + PathLog.debug(' -Saving base profile shape.') + self.profileShapes[m] = [prflShp] + PathLog.debug('self.profileShapes[{}]: {}'.format(m, self.profileShapes[m])) + # Efor + + return (fShapes, vShapes) + + # private class methods + def _isReady(self, module): + '''_isReady(module)... Internal method. + Checks if required attributes are available for processing obj.Base (the Base Geometry).''' + if hasattr(self, module): + self.module = module + modMethod = getattr(self, module) # gets the attribute only + modMethod() # executes as method + else: + return False + + if not self.radius: + return False + + if not self.depthParams: + return False + + return True + + def _identifyFacesAndVoids(self, F, V): + TUPS = list() + GRP = self.JOB.Model.Group + lenGRP = len(GRP) + + # Separate selected faces into (base, face) tuples and flag model(s) for STL creation + for (bs, SBS) in self.obj.Base: + for sb in SBS: + # Flag model for STL creation + mdlIdx = None + for m in range(0, lenGRP): + if bs is GRP[m]: + self.modelSTLs[m] = True + mdlIdx = m + break + TUPS.append((mdlIdx, bs, sb)) # (model idx, base, sub) + + # Apply `AvoidXFaces` value + faceCnt = len(TUPS) + add = faceCnt - self.obj.AvoidLastX_Faces + for bst in range(0, faceCnt): + (m, base, sub) = TUPS[bst] + shape = getattr(base.Shape, sub) + if isinstance(shape, Part.Face): + faceIdx = int(sub[4:]) - 1 + if bst < add: + if F[m] is False: + F[m] = list() + F[m].append((shape, faceIdx)) + else: + if V[m] is False: + V[m] = list() + V[m].append((shape, faceIdx)) + return (F, V) + + def _preProcessFacesAndVoids(self, base, m, FACES, VOIDS): + mFS = False + mVS = False + mPS = False + mIFS = list() + + if FACES[m] is not False: + isHole = False + if self.obj.HandleMultipleFeatures == 'Collectively': + cont = True + fsL = list() # face shape list + ifL = list() # avoid shape list + outFCS = list() + + # Get collective envelope slice of selected faces + for (fcshp, fcIdx) in FACES[m]: + fNum = fcIdx + 1 + fsL.append(fcshp) + gFW = self._getFaceWires(base, fcshp, fcIdx) + if gFW is False: + PathLog.debug('Failed to get wires from Face{}'.format(fNum)) + elif gFW[0] is False: + PathLog.debug('Cannot process Face{}. Check that it has horizontal surface exposure.'.format(fNum)) + else: + ((otrFace, raised), intWires) = gFW + outFCS.append(otrFace) + if self.obj.InternalFeaturesCut is False: + if intWires is not False: + for (iFace, rsd) in intWires: + ifL.append(iFace) + + PathLog.debug('Attempting to get cross-section of collective faces.') + if len(outFCS) == 0: + PathLog.error('Cannot process selected faces. Check horizontal surface exposure.'.format(fNum)) + cont = False + else: + cfsL = Part.makeCompound(outFCS) + + # Handle profile edges request + if cont is True and self.profileEdges != 'None': + ofstVal = self._calculateOffsetValue(isHole) + psOfst = extractFaceOffset(cfsL, ofstVal, self.wpc) + if psOfst is not False: + mPS = [psOfst] + if self.profileEdges == 'Only': + mFS = True + cont = False + else: + PathLog.error(' -Failed to create profile geometry for selected faces.') + cont = False + + if cont: + if self.showDebugObjects: + T = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpCollectiveShape') + T.Shape = cfsL + T.purgeTouched() + self.tempGroup.addObject(T) + + ofstVal = self._calculateOffsetValue(isHole) + faceOfstShp = extractFaceOffset(cfsL, ofstVal, self.wpc) + if faceOfstShp is False: + PathLog.error(' -Failed to create offset face.') + cont = False + + if cont: + lenIfL = len(ifL) + if self.obj.InternalFeaturesCut is False: + if lenIfL == 0: + PathLog.debug(' -No internal features saved.') + else: + if lenIfL == 1: + casL = ifL[0] + else: + casL = Part.makeCompound(ifL) + if self.showDebugObjects: + C = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpCompoundIntFeat') + C.Shape = casL + C.purgeTouched() + self.tempGroup.addObject(C) + ofstVal = self._calculateOffsetValue(isHole=True) + intOfstShp = extractFaceOffset(casL, ofstVal, self.wpc) + mIFS.append(intOfstShp) + # faceOfstShp = faceOfstShp.cut(intOfstShp) + + mFS = [faceOfstShp] + # Eif + + elif self.obj.HandleMultipleFeatures == 'Individually': + for (fcshp, fcIdx) in FACES[m]: + cont = True + ifL = list() # avoid shape list + fNum = fcIdx + 1 + outerFace = False + + gFW = self._getFaceWires(base, fcshp, fcIdx) + if gFW is False: + PathLog.debug('Failed to get wires from Face{}'.format(fNum)) + cont = False + elif gFW[0] is False: + PathLog.debug('Cannot process Face{}. Check that it has horizontal surface exposure.'.format(fNum)) + cont = False + outerFace = False + else: + ((otrFace, raised), intWires) = gFW + outerFace = otrFace + if self.obj.InternalFeaturesCut is False: + if intWires is not False: + for (iFace, rsd) in intWires: + ifL.append(iFace) + + if outerFace is not False: + PathLog.debug('Attempting to create offset face of Face{}'.format(fNum)) + + if self.profileEdges != 'None': + ofstVal = self._calculateOffsetValue(isHole) + psOfst = extractFaceOffset(outerFace, ofstVal, self.wpc) + if psOfst is not False: + if mPS is False: + mPS = list() + mPS.append(psOfst) + if self.profileEdges == 'Only': + if mFS is False: + mFS = list() + mFS.append(True) + cont = False + else: + PathLog.error(' -Failed to create profile geometry for Face{}.'.format(fNum)) + cont = False + + if cont: + ofstVal = self._calculateOffsetValue(isHole) + faceOfstShp = extractFaceOffset(outerFace, ofstVal, self.wpc) + + lenIfl = len(ifL) + if self.obj.InternalFeaturesCut is False and lenIfl > 0: + if lenIfl == 1: + casL = ifL[0] + else: + casL = Part.makeCompound(ifL) + + ofstVal = self._calculateOffsetValue(isHole=True) + intOfstShp = extractFaceOffset(casL, ofstVal, self.wpc) + mIFS.append(intOfstShp) + # faceOfstShp = faceOfstShp.cut(intOfstShp) + + if mFS is False: + mFS = list() + mFS.append(faceOfstShp) + # Eif + # Efor + # Eif + # Eif + + if len(mIFS) > 0: + if mVS is False: + mVS = list() + for ifs in mIFS: + mVS.append(ifs) + + if VOIDS[m] is not False: + PathLog.debug('Processing avoid faces.') + cont = True + isHole = False + outFCS = list() + intFEAT = list() + + for (fcshp, fcIdx) in VOIDS[m]: + fNum = fcIdx + 1 + gFW = self._getFaceWires(base, fcshp, fcIdx) + if gFW is False: + PathLog.debug('Failed to get wires from avoid Face{}'.format(fNum)) + cont = False + else: + ((otrFace, raised), intWires) = gFW + outFCS.append(otrFace) + if self.obj.AvoidLastX_InternalFeatures is False: + if intWires is not False: + for (iFace, rsd) in intWires: + intFEAT.append(iFace) + + lenOtFcs = len(outFCS) + if lenOtFcs == 0: + cont = False + else: + if lenOtFcs == 1: + avoid = outFCS[0] + else: + avoid = Part.makeCompound(outFCS) + + if self.showDebugObjects: + PathLog.debug('*** tmpAvoidArea') + P = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpVoidEnvelope') + P.Shape = avoid + P.purgeTouched() + self.tempGroup.addObject(P) + + if cont: + if self.showDebugObjects: + PathLog.debug('*** tmpVoidCompound') + P = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpVoidCompound') + P.Shape = avoid + P.purgeTouched() + self.tempGroup.addObject(P) + ofstVal = self._calculateOffsetValue(isHole, isVoid=True) + avdOfstShp = extractFaceOffset(avoid, ofstVal, self.wpc) + if avdOfstShp is False: + PathLog.error('Failed to create collective offset avoid face.') + cont = False + + if cont: + avdShp = avdOfstShp + + if self.obj.AvoidLastX_InternalFeatures is False and len(intFEAT) > 0: + if len(intFEAT) > 1: + ifc = Part.makeCompound(intFEAT) + else: + ifc = intFEAT[0] + ofstVal = self._calculateOffsetValue(isHole=True) + ifOfstShp = extractFaceOffset(ifc, ofstVal, self.wpc) + if ifOfstShp is False: + PathLog.error('Failed to create collective offset avoid internal features.') + else: + avdShp = avdOfstShp.cut(ifOfstShp) + + if mVS is False: + mVS = list() + mVS.append(avdShp) + + + return (mFS, mVS, mPS) + + def _getFaceWires(self, base, fcshp, fcIdx): + outFace = False + INTFCS = list() + fNum = fcIdx + 1 + warnFinDep = translate(self.module, 'Final Depth might need to be lower. Internal features detected in Face') + + PathLog.debug('_getFaceWires() from Face{}'.format(fNum)) + WIRES = self._extractWiresFromFace(base, fcshp) + if WIRES is False: + PathLog.error('Failed to extract wires from Face{}'.format(fNum)) + return False + + # Process remaining internal features, adding to FCS list + lenW = len(WIRES) + for w in range(0, lenW): + (wire, rsd) = WIRES[w] + PathLog.debug('Processing Wire{} in Face{}. isRaised: {}'.format(w + 1, fNum, rsd)) + if wire.isClosed() is False: + PathLog.debug(' -wire is not closed.') + else: + slc = self._flattenWireToFace(wire) + if slc is False: + PathLog.error('FAILED to identify horizontal exposure on Face{}.'.format(fNum)) + else: + if w == 0: + outFace = (slc, rsd) + else: + # add to VOIDS so cutter avoids area. + PathLog.warning(warnFinDep + str(fNum) + '.') + INTFCS.append((slc, rsd)) + if len(INTFCS) == 0: + return (outFace, False) + else: + return (outFace, INTFCS) + + def _preProcessEntireBase(self, base, m): + cont = True + isHole = False + prflShp = False + # Create envelope, extract cross-section and make offset co-planar shape + # baseEnv = PathUtils.getEnvelope(base.Shape, subshape=None, depthparams=self.depthParams) + + try: + baseEnv = PathUtils.getEnvelope(partshape=base.Shape, subshape=None, depthparams=self.depthParams) # Produces .Shape + except Exception as ee: + PathLog.error(str(ee)) + shell = base.Shape.Shells[0] + solid = Part.makeSolid(shell) + try: + baseEnv = PathUtils.getEnvelope(partshape=solid, subshape=None, depthparams=self.depthParams) # Produces .Shape + except Exception as eee: + PathLog.error(str(eee)) + cont = False + + if cont: + csFaceShape = getShapeSlice(baseEnv) + if csFaceShape is False: + PathLog.debug('getShapeSlice(baseEnv) failed') + csFaceShape = getCrossSection(baseEnv) + if csFaceShape is False: + PathLog.debug('getCrossSection(baseEnv) failed') + csFaceShape = getSliceFromEnvelope(baseEnv) + if csFaceShape is False: + PathLog.error('Failed to slice baseEnv shape.') + cont = False + + if cont is True and self.profileEdges != 'None': + PathLog.debug(' -Attempting profile geometry for model base.') + ofstVal = self._calculateOffsetValue(isHole) + psOfst = extractFaceOffset(csFaceShape, ofstVal, self.wpc) + if psOfst is not False: + if self.profileEdges == 'Only': + return (True, psOfst) + prflShp = psOfst + else: + PathLog.error(' -Failed to create profile geometry.') + cont = False + + if cont: + ofstVal = self._calculateOffsetValue(isHole) + faceOffsetShape = extractFaceOffset(csFaceShape, ofstVal, self.wpc) + if faceOffsetShape is False: + PathLog.error('extractFaceOffset() failed.') + else: + faceOffsetShape.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - faceOffsetShape.BoundBox.ZMin)) + return (faceOffsetShape, prflShp) + return False + + def _extractWiresFromFace(self, base, fc): + '''_extractWiresFromFace(base, fc) ... + Attempts to return all closed wires within a parent face, including the outer most wire of the parent. + The wires are ordered by area. Each wire is also categorized as a pocket(False) or raised protrusion(True). + ''' + PathLog.debug('_extractWiresFromFace()') + + WIRES = list() + lenWrs = len(fc.Wires) + PathLog.debug(' -Wire count: {}'.format(lenWrs)) + + def index0(tup): + return tup[0] + + # Cycle through wires in face + for w in range(0, lenWrs): + PathLog.debug(' -Analyzing wire_{}'.format(w + 1)) + wire = fc.Wires[w] + checkEdges = False + cont = True + + # Check for closed edges (circles, ellipses, etc...) + for E in wire.Edges: + if E.isClosed() is True: + checkEdges = True + break + + if checkEdges is True: + PathLog.debug(' -checkEdges is True') + for e in range(0, len(wire.Edges)): + edge = wire.Edges[e] + if edge.isClosed() is True and edge.Mass > 0.01: + PathLog.debug(' -Found closed edge') + raised = False + ip = self._isPocket(base, fc, edge) + if ip is False: + raised = True + ebb = edge.BoundBox + eArea = ebb.XLength * ebb.YLength + F = Part.Face(Part.Wire([edge])) + WIRES.append((eArea, F.Wires[0], raised)) + cont = False + + if cont: + PathLog.debug(' -cont is True') + # If only one wire and not checkEdges, return first wire + if lenWrs == 1: + return [(wire, False)] + + raised = False + wbb = wire.BoundBox + wArea = wbb.XLength * wbb.YLength + if w > 0: + ip = self._isPocket(base, fc, wire) + if ip is False: + raised = True + WIRES.append((wArea, Part.Wire(wire.Edges), raised)) + + nf = len(WIRES) + if nf > 0: + PathLog.debug(' -number of wires found is {}'.format(nf)) + if nf == 1: + (area, W, raised) = WIRES[0] + owLen = fc.OuterWire.Length + wLen = W.Length + if abs(owLen - wLen) > 0.0000001: + OW = Part.Wire(Part.__sortEdges__(fc.OuterWire.Edges)) + return [(OW, False), (W, raised)] + else: + return [(W, raised)] + else: + sortedWIRES = sorted(WIRES, key=index0, reverse=True) + WRS = [(W, raised) for (area, W, raised) in sortedWIRES] # outer, then inner by area size + # Check if OuterWire is larger than largest in WRS list + (W, raised) = WRS[0] + owLen = fc.OuterWire.Length + wLen = W.Length + if abs(owLen - wLen) > 0.0000001: + OW = Part.Wire(Part.__sortEdges__(fc.OuterWire.Edges)) + WRS.insert(0, (OW, False)) + return WRS + + return False + + def _calculateOffsetValue(self, isHole, isVoid=False): + '''_calculateOffsetValue(self.obj, isHole, isVoid) ... internal function. + Calculate the offset for the Path.Area() function.''' + self.JOB = PathUtils.findParentJob(self.obj) + tolrnc = self.JOB.GeometryTolerance.Value + + if isVoid is False: + if isHole is True: + offset = -1 * self.obj.InternalFeaturesAdjustment.Value + offset += self.radius + (tolrnc / 10.0) + else: + offset = -1 * self.obj.BoundaryAdjustment.Value + if self.obj.BoundaryEnforcement is True: + offset += self.radius + (tolrnc / 10.0) + else: + offset -= self.radius + (tolrnc / 10.0) + offset = 0.0 - offset + else: + offset = -1 * self.obj.BoundaryAdjustment.Value + offset += self.radius + (tolrnc / 10.0) + + return offset + + def _isPocket(self, b, f, w): + '''_isPocket(b, f, w)... + Attempts to determine if the wire(w) in face(f) of base(b) is a pocket or raised protrusion. + Returns True if pocket, False if raised protrusion.''' + e = w.Edges[0] + for fi in range(0, len(b.Shape.Faces)): + face = b.Shape.Faces[fi] + for ei in range(0, len(face.Edges)): + edge = face.Edges[ei] + if e.isSame(edge) is True: + if f is face: + # Alternative: run loop to see if all edges are same + pass # same source face, look for another + else: + if face.CenterOfMass.z < f.CenterOfMass.z: + return True + return False + + def _flattenWireToFace(self, wire): + PathLog.debug('_flattenWireToFace()') + if wire.isClosed() is False: + PathLog.debug(' -wire.isClosed() is False') + return False + + # If wire is planar horizontal, convert to a face and return + if wire.BoundBox.ZLength == 0.0: + slc = Part.Face(wire) + return slc + + # Attempt to create a new wire for manipulation, if not, use original + newWire = Part.Wire(wire.Edges) + if newWire.isClosed() is True: + nWire = newWire + else: + PathLog.debug(' -newWire.isClosed() is False') + nWire = wire + + # Attempt extrusion, and then try a manual slice and then cross-section + ext = getExtrudedShape(nWire) + if ext is False: + PathLog.debug('getExtrudedShape() failed') + else: + slc = getShapeSlice(ext) + if slc is not False: + return slc + cs = getCrossSection(ext, True) + if cs is not False: + return cs + + # Attempt creating an envelope, and then try a manual slice and then cross-section + env = getShapeEnvelope(nWire) + if env is False: + PathLog.debug('getShapeEnvelope() failed') + else: + slc = getShapeSlice(env) + if slc is not False: + return slc + cs = getCrossSection(env, True) + if cs is not False: + return cs + + # Attempt creating a projection + slc = getProjectedFace(self.tempGroup, nWire) + if slc is False: + PathLog.debug('getProjectedFace() failed') + else: + return slc + + return False +# Eclass + + +# Functions for getting a shape envelope and cross-section +def getExtrudedShape(wire): + PathLog.debug('getExtrudedShape()') + wBB = wire.BoundBox + extFwd = math.floor(2.0 * wBB.ZLength) + 10.0 + + try: + shell = wire.extrude(FreeCAD.Vector(0.0, 0.0, extFwd)) + except Exception as ee: + PathLog.error(' -extrude wire failed: \n{}'.format(ee)) + return False + + SHP = Part.makeSolid(shell) + return SHP + +def getShapeSlice(shape): + PathLog.debug('getShapeSlice()') + + bb = shape.BoundBox + mid = (bb.ZMin + bb.ZMax) / 2.0 + xmin = bb.XMin - 1.0 + xmax = bb.XMax + 1.0 + ymin = bb.YMin - 1.0 + ymax = bb.YMax + 1.0 + p1 = FreeCAD.Vector(xmin, ymin, mid) + p2 = FreeCAD.Vector(xmax, ymin, mid) + p3 = FreeCAD.Vector(xmax, ymax, mid) + p4 = FreeCAD.Vector(xmin, ymax, mid) + + e1 = Part.makeLine(p1, p2) + e2 = Part.makeLine(p2, p3) + e3 = Part.makeLine(p3, p4) + e4 = Part.makeLine(p4, p1) + face = Part.Face(Part.Wire([e1, e2, e3, e4])) + fArea = face.BoundBox.XLength * face.BoundBox.YLength # face.Wires[0].Area + sArea = shape.BoundBox.XLength * shape.BoundBox.YLength + midArea = (fArea + sArea) / 2.0 + + slcShp = shape.common(face) + slcArea = slcShp.BoundBox.XLength * slcShp.BoundBox.YLength + + if slcArea < midArea: + for W in slcShp.Wires: + if W.isClosed() is False: + PathLog.debug(' -wire.isClosed() is False') + return False + if len(slcShp.Wires) == 1: + wire = slcShp.Wires[0] + slc = Part.Face(wire) + slc.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - slc.BoundBox.ZMin)) + return slc + else: + fL = list() + for W in slcShp.Wires: + slc = Part.Face(W) + slc.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - slc.BoundBox.ZMin)) + fL.append(slc) + comp = Part.makeCompound(fL) + return comp + + # PathLog.debug(' -slcArea !< midArea') + # PathLog.debug(' -slcShp.Edges count: {}. Might be a vertically oriented face.'.format(len(slcShp.Edges))) + return False + +def getProjectedFace(tempGroup, wire): + import Draft + PathLog.debug('getProjectedFace()') + F = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpProjectionWire') + F.Shape = wire + F.purgeTouched() + tempGroup.addObject(F) + try: + prj = Draft.makeShape2DView(F, FreeCAD.Vector(0, 0, 1)) + prj.recompute() + prj.purgeTouched() + tempGroup.addObject(prj) + except Exception as ee: + PathLog.error(str(ee)) + return False + else: + pWire = Part.Wire(prj.Shape.Edges) + if pWire.isClosed() is False: + # PathLog.debug(' -pWire.isClosed() is False') + return False + slc = Part.Face(pWire) + slc.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - slc.BoundBox.ZMin)) + return slc + +def getCrossSection(shape, withExtrude=False): + PathLog.debug('getCrossSection()') + wires = list() + bb = shape.BoundBox + mid = (bb.ZMin + bb.ZMax) / 2.0 + + for i in shape.slice(FreeCAD.Vector(0, 0, 1), mid): + wires.append(i) + + if len(wires) > 0: + comp = Part.Compound(wires) # produces correct cross-section wire ! + comp.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - comp.BoundBox.ZMin)) + csWire = comp.Wires[0] + if csWire.isClosed() is False: + PathLog.debug(' -comp.Wires[0] is not closed') + return False + if withExtrude is True: + ext = getExtrudedShape(csWire) + CS = getShapeSlice(ext) + if CS is False: + return False + else: + CS = Part.Face(csWire) + CS.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - CS.BoundBox.ZMin)) + return CS + else: + PathLog.debug(' -No wires from .slice() method') + + return False + +def getShapeEnvelope(shape): + PathLog.debug('getShapeEnvelope()') + + wBB = shape.BoundBox + extFwd = wBB.ZLength + 10.0 + minz = wBB.ZMin + maxz = wBB.ZMin + extFwd + stpDwn = (maxz - minz) / 4.0 + dep_par = PathUtils.depth_params(maxz + 5.0, maxz + 3.0, maxz, stpDwn, 0.0, minz) + + try: + env = PathUtils.getEnvelope(partshape=shape, depthparams=dep_par) # Produces .Shape + except Exception as ee: + PathLog.error('try: PathUtils.getEnvelope() failed.\n' + str(ee)) + return False + else: + return env + +def getSliceFromEnvelope(env): + PathLog.debug('getSliceFromEnvelope()') + eBB = env.BoundBox + extFwd = eBB.ZLength + 10.0 + maxz = eBB.ZMin + extFwd + + emax = math.floor(maxz - 1.0) + E = list() + for e in range(0, len(env.Edges)): + emin = env.Edges[e].BoundBox.ZMin + if emin > emax: + E.append(env.Edges[e]) + tf = Part.Face(Part.Wire(Part.__sortEdges__(E))) + tf.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - tf.BoundBox.ZMin)) + + return tf + + +# Function to extract offset face from shape +def extractFaceOffset(fcShape, offset, wpc, makeComp=True): + '''extractFaceOffset(fcShape, offset) ... internal function. + Original _buildPathArea() version copied from PathAreaOp.py module. This version is modified. + Adjustments made based on notes by @sliptonic at this webpage: https://github.com/sliptonic/FreeCAD/wiki/PathArea-notes.''' + PathLog.debug('extractFaceOffset()') + + if fcShape.BoundBox.ZMin != 0.0: + fcShape.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - fcShape.BoundBox.ZMin)) + + areaParams = {} + areaParams['Offset'] = offset + areaParams['Fill'] = 1 # 1 + areaParams['Coplanar'] = 0 + areaParams['SectionCount'] = 1 # -1 = full(all per depthparams??) sections + areaParams['Reorient'] = True + areaParams['OpenMode'] = 0 + areaParams['MaxArcPoints'] = 400 # 400 + areaParams['Project'] = True + + area = Path.Area() # Create instance of Area() class object + # area.setPlane(PathUtils.makeWorkplane(fcShape)) # Set working plane + area.setPlane(PathUtils.makeWorkplane(wpc)) # Set working plane to normal at Z=1 + area.add(fcShape) + area.setParams(**areaParams) # set parameters + + offsetShape = area.getShape() + wCnt = len(offsetShape.Wires) + if wCnt == 0: + return False + elif wCnt == 1: + ofstFace = Part.Face(offsetShape.Wires[0]) + if not makeComp: + ofstFace = [ofstFace] + else: + W = list() + for wr in offsetShape.Wires: + W.append(Part.Face(wr)) + if makeComp: + ofstFace = Part.makeCompound(W) + else: + ofstFace = W + + return ofstFace # offsetShape + + +# Functions to convert path geometry into line/arc segments for OCL input or directly to g-code +def pathGeomToLinesPointSet(obj, compGeoShp, cutClimb, toolDiam, closedGap, gaps): + '''pathGeomToLinesPointSet(obj, compGeoShp)... + Convert a compound set of sequential line segments to directionally-oriented collinear groupings.''' + PathLog.debug('pathGeomToLinesPointSet()') + # Extract intersection line segments for return value as list() + LINES = list() + inLine = list() + chkGap = False + lnCnt = 0 + ec = len(compGeoShp.Edges) + cpa = obj.CutPatternAngle + + edg0 = compGeoShp.Edges[0] + p1 = (edg0.Vertexes[0].X, edg0.Vertexes[0].Y) + p2 = (edg0.Vertexes[1].X, edg0.Vertexes[1].Y) + if cutClimb is True: + tup = (p2, p1) + lst = FreeCAD.Vector(p1[0], p1[1], 0.0) + else: + tup = (p1, p2) + lst = FreeCAD.Vector(p2[0], p2[1], 0.0) + inLine.append(tup) + sp = FreeCAD.Vector(p1[0], p1[1], 0.0) # start point + + for ei in range(1, ec): + chkGap = False + edg = compGeoShp.Edges[ei] # Get edge for vertexes + v1 = (edg.Vertexes[0].X, edg.Vertexes[0].Y) # vertex 0 + v2 = (edg.Vertexes[1].X, edg.Vertexes[1].Y) # vertex 1 + + ep = FreeCAD.Vector(v2[0], v2[1], 0.0) # end point + cp = FreeCAD.Vector(v1[0], v1[1], 0.0) # check point (first / middle point) + # iC = sp.isOnLineSegment(ep, cp) + iC = cp.isOnLineSegment(sp, ep) + if iC is True: + inLine.append('BRK') + chkGap = True + else: + if cutClimb is True: + inLine.reverse() + LINES.append(inLine) # Save inLine segments + lnCnt += 1 + inLine = list() # reset collinear container + if cutClimb is True: + sp = cp # FreeCAD.Vector(v1[0], v1[1], 0.0) + else: + sp = ep + + if cutClimb is True: + tup = (v2, v1) + if chkGap is True: + gap = abs(toolDiam - lst.sub(ep).Length) + lst = cp + else: + tup = (v1, v2) + if chkGap is True: + gap = abs(toolDiam - lst.sub(cp).Length) + lst = ep + + if chkGap is True: + if gap < obj.GapThreshold.Value: + b = inLine.pop() # pop off 'BRK' marker + (vA, vB) = inLine.pop() # pop off previous line segment for combining with current + tup = (vA, tup[1]) + closedGap = True + else: + # PathLog.debug('---- Gap: {} mm'.format(gap)) + gap = round(gap, 6) + if gap < gaps[0]: + gaps.insert(0, gap) + gaps.pop() + inLine.append(tup) + # Efor + lnCnt += 1 + if cutClimb is True: + inLine.reverse() + LINES.append(inLine) # Save inLine segments + + # Handle last inLine set, reversing it. + if obj.CutPatternReversed is True: + if cpa != 0.0 and cpa % 90.0 == 0.0: + F = LINES.pop(0) + rev = list() + for iL in F: + if iL == 'BRK': + rev.append(iL) + else: + (p1, p2) = iL + rev.append((p2, p1)) + rev.reverse() + LINES.insert(0, rev) + + isEven = lnCnt % 2 + if isEven == 0: + PathLog.debug('Line count is ODD.') + else: + PathLog.debug('Line count is even.') + + return LINES + +def pathGeomToZigzagPointSet(obj, compGeoShp, cutClimb, toolDiam, closedGap, gaps): + '''_pathGeomToZigzagPointSet(obj, compGeoShp)... + Convert a compound set of sequential line segments to directionally-oriented collinear groupings + with a ZigZag directional indicator included for each collinear group.''' + PathLog.debug('_pathGeomToZigzagPointSet()') + # Extract intersection line segments for return value as list() + LINES = list() + inLine = list() + lnCnt = 0 + chkGap = False + ec = len(compGeoShp.Edges) + + if cutClimb is True: + dirFlg = -1 + else: + dirFlg = 1 + + edg0 = compGeoShp.Edges[0] + p1 = (edg0.Vertexes[0].X, edg0.Vertexes[0].Y) + p2 = (edg0.Vertexes[1].X, edg0.Vertexes[1].Y) + if dirFlg == 1: + tup = (p1, p2) + lst = FreeCAD.Vector(p2[0], p2[1], 0.0) + sp = FreeCAD.Vector(p1[0], p1[1], 0.0) # start point + else: + tup = (p2, p1) + lst = FreeCAD.Vector(p1[0], p1[1], 0.0) + sp = FreeCAD.Vector(p2[0], p2[1], 0.0) # start point + inLine.append(tup) + + for ei in range(1, ec): + edg = compGeoShp.Edges[ei] + v1 = (edg.Vertexes[0].X, edg.Vertexes[0].Y) + v2 = (edg.Vertexes[1].X, edg.Vertexes[1].Y) + + cp = FreeCAD.Vector(v1[0], v1[1], 0.0) # check point (start point of segment) + ep = FreeCAD.Vector(v2[0], v2[1], 0.0) # end point + # iC = sp.isOnLineSegment(ep, cp) + iC = cp.isOnLineSegment(sp, ep) + if iC is True: + inLine.append('BRK') + chkGap = True + gap = abs(toolDiam - lst.sub(cp).Length) + else: + chkGap = False + if dirFlg == -1: + inLine.reverse() + # LINES.append((dirFlg, inLine)) + LINES.append(inLine) + lnCnt += 1 + dirFlg = -1 * dirFlg # Change zig to zag + inLine = list() # reset collinear container + sp = cp # FreeCAD.Vector(v1[0], v1[1], 0.0) + + lst = ep + if dirFlg == 1: + tup = (v1, v2) + else: + tup = (v2, v1) + + if chkGap is True: + if gap < obj.GapThreshold.Value: + b = inLine.pop() # pop off 'BRK' marker + (vA, vB) = inLine.pop() # pop off previous line segment for combining with current + if dirFlg == 1: + tup = (vA, tup[1]) + else: + tup = (tup[0], vB) + closedGap = True + else: + gap = round(gap, 6) + if gap < gaps[0]: + gaps.insert(0, gap) + gaps.pop() + inLine.append(tup) + # Efor + lnCnt += 1 + + # Fix directional issue with LAST line when line count is even + isEven = lnCnt % 2 + if isEven == 0: # Changed to != with 90 degree CutPatternAngle + PathLog.debug('Line count is even.') + else: + PathLog.debug('Line count is ODD.') + dirFlg = -1 * dirFlg + if obj.CutPatternReversed is False: + if cutClimb is True: + dirFlg = -1 * dirFlg + + if obj.CutPatternReversed: + dirFlg = -1 * dirFlg + + # Handle last inLine list + if dirFlg == 1: + rev = list() + for iL in inLine: + if iL == 'BRK': + rev.append(iL) + else: + (p1, p2) = iL + rev.append((p2, p1)) + + if not obj.CutPatternReversed: + rev.reverse() + else: + rev2 = list() + for iL in rev: + if iL == 'BRK': + rev2.append(iL) + else: + (p1, p2) = iL + rev2.append((p2, p1)) + rev2.reverse() + rev = rev2 + + # LINES.append((dirFlg, rev)) + LINES.append(rev) + else: + # LINES.append((dirFlg, inLine)) + LINES.append(inLine) + + return LINES + +def pathGeomToCircularPointSet(obj, compGeoShp, cutClimb, toolDiam, closedGap, gaps, COM): + '''pathGeomToCircularPointSet(obj, compGeoShp)... + Convert a compound set of arcs/circles to a set of directionally-oriented arc end points + and the corresponding center point.''' + # Extract intersection line segments for return value as list() + PathLog.debug('pathGeomToCircularPointSet()') + ARCS = list() + stpOvrEI = list() + segEI = list() + isSame = False + sameRad = None + ec = len(compGeoShp.Edges) + + def gapDist(sp, ep): + X = (ep[0] - sp[0])**2 + Y = (ep[1] - sp[1])**2 + return math.sqrt(X + Y) # the 'z' value is zero in both points + + # Separate arc data into Loops and Arcs + for ei in range(0, ec): + edg = compGeoShp.Edges[ei] + if edg.Closed is True: + stpOvrEI.append(('L', ei, False)) + else: + if isSame is False: + segEI.append(ei) + isSame = True + pnt = FreeCAD.Vector(edg.Vertexes[0].X, edg.Vertexes[0].Y, 0.0) + sameRad = pnt.sub(COM).Length + else: + # Check if arc is co-radial to current SEGS + pnt = FreeCAD.Vector(edg.Vertexes[0].X, edg.Vertexes[0].Y, 0.0) + if abs(sameRad - pnt.sub(COM).Length) > 0.00001: + isSame = False + + if isSame is True: + segEI.append(ei) + else: + # Move co-radial arc segments + stpOvrEI.append(['A', segEI, False]) + # Start new list of arc segments + segEI = [ei] + isSame = True + pnt = FreeCAD.Vector(edg.Vertexes[0].X, edg.Vertexes[0].Y, 0.0) + sameRad = pnt.sub(COM).Length + # Process trailing `segEI` data, if available + if isSame is True: + stpOvrEI.append(['A', segEI, False]) + + # Identify adjacent arcs with y=0 start/end points that connect + for so in range(0, len(stpOvrEI)): + SO = stpOvrEI[so] + if SO[0] == 'A': + startOnAxis = list() + endOnAxis = list() + EI = SO[1] # list of corresponding compGeoShp.Edges indexes + + # Identify startOnAxis and endOnAxis arcs + for i in range(0, len(EI)): + ei = EI[i] # edge index + E = compGeoShp.Edges[ei] # edge object + if abs(COM.y - E.Vertexes[0].Y) < 0.00001: + startOnAxis.append((i, ei, E.Vertexes[0])) + elif abs(COM.y - E.Vertexes[1].Y) < 0.00001: + endOnAxis.append((i, ei, E.Vertexes[1])) + + # Look for connections between startOnAxis and endOnAxis arcs. Consolidate data when connected + lenSOA = len(startOnAxis) + lenEOA = len(endOnAxis) + if lenSOA > 0 and lenEOA > 0: + for soa in range(0, lenSOA): + (iS, eiS, vS) = startOnAxis[soa] + for eoa in range(0, len(endOnAxis)): + (iE, eiE, vE) = endOnAxis[eoa] + dist = vE.X - vS.X + if abs(dist) < 0.00001: # They connect on axis at same radius + SO[2] = (eiE, eiS) + break + elif dist > 0: + break # stop searching + # Eif + # Eif + # Efor + + # Construct arc data tuples for OCL + dirFlg = 1 + if not cutClimb: # True yields Climb when set to Conventional + dirFlg = -1 + + # Cycle through stepOver data + for so in range(0, len(stpOvrEI)): + SO = stpOvrEI[so] + if SO[0] == 'L': # L = Loop/Ring/Circle + # PathLog.debug("SO[0] == 'Loop'") + lei = SO[1] # loop Edges index + v1 = compGeoShp.Edges[lei].Vertexes[0] + + # space = obj.SampleInterval.Value / 10.0 + # space = 0.000001 + space = toolDiam * 0.005 # If too small, OCL will fail to scan the loop + + # p1 = FreeCAD.Vector(v1.X, v1.Y, v1.Z) + p1 = FreeCAD.Vector(v1.X, v1.Y, 0.0) # z=0.0 for waterline; z=v1.Z for 3D Surface + rad = p1.sub(COM).Length + spcRadRatio = space/rad + if spcRadRatio < 1.0: + tolrncAng = math.asin(spcRadRatio) + else: + tolrncAng = 0.99999998 * math.pi + EX = COM.x + (rad * math.cos(tolrncAng)) + EY = v1.Y - space # rad * math.sin(tolrncAng) + + sp = (v1.X, v1.Y, 0.0) + ep = (EX, EY, 0.0) + cp = (COM.x, COM.y, 0.0) + if dirFlg == 1: + arc = (sp, ep, cp) + else: + arc = (ep, sp, cp) # OCL.Arc(firstPnt, lastPnt, centerPnt, dir=True(CCW direction)) + ARCS.append(('L', dirFlg, [arc])) + else: # SO[0] == 'A' A = Arc + # PathLog.debug("SO[0] == 'Arc'") + PRTS = list() + EI = SO[1] # list of corresponding Edges indexes + CONN = SO[2] # list of corresponding connected edges tuples (iE, iS) + chkGap = False + lst = None + + if CONN is not False: + (iE, iS) = CONN + v1 = compGeoShp.Edges[iE].Vertexes[0] + v2 = compGeoShp.Edges[iS].Vertexes[1] + sp = (v1.X, v1.Y, 0.0) + ep = (v2.X, v2.Y, 0.0) + cp = (COM.x, COM.y, 0.0) + if dirFlg == 1: + arc = (sp, ep, cp) + lst = ep + else: + arc = (ep, sp, cp) # OCL.Arc(firstPnt, lastPnt, centerPnt, dir=True(CCW direction)) + lst = sp + PRTS.append(arc) + # Pop connected edge index values from arc segments index list + iEi = EI.index(iE) + iSi = EI.index(iS) + if iEi > iSi: + EI.pop(iEi) + EI.pop(iSi) + else: + EI.pop(iSi) + EI.pop(iEi) + if len(EI) > 0: + PRTS.append('BRK') + chkGap = True + cnt = 0 + for ei in EI: + if cnt > 0: + PRTS.append('BRK') + chkGap = True + v1 = compGeoShp.Edges[ei].Vertexes[0] + v2 = compGeoShp.Edges[ei].Vertexes[1] + sp = (v1.X, v1.Y, 0.0) + ep = (v2.X, v2.Y, 0.0) + cp = (COM.x, COM.y, 0.0) + if dirFlg == 1: + arc = (sp, ep, cp) + if chkGap is True: + gap = abs(toolDiam - gapDist(lst, sp)) # abs(toolDiam - lst.sub(sp).Length) + lst = ep + else: + arc = (ep, sp, cp) # OCL.Arc(firstPnt, lastPnt, centerPnt, dir=True(CCW direction)) + if chkGap is True: + gap = abs(toolDiam - gapDist(lst, ep)) # abs(toolDiam - lst.sub(ep).Length) + lst = sp + if chkGap is True: + if gap < obj.GapThreshold.Value: + PRTS.pop() # pop off 'BRK' marker + (vA, vB, vC) = PRTS.pop() # pop off previous arc segment for combining with current + arc = (vA, arc[1], vC) + closedGap = True + else: + # PathLog.debug('---- Gap: {} mm'.format(gap)) + gap = round(gap, 6) + if gap < gaps[0]: + gaps.insert(0, gap) + gaps.pop() + PRTS.append(arc) + cnt += 1 + + if dirFlg == -1: + PRTS.reverse() + + ARCS.append(('A', dirFlg, PRTS)) + # Eif + if obj.CutPattern == 'CircularZigZag': + dirFlg = -1 * dirFlg + # Efor + + return ARCS + +def pathGeomToSpiralPointSet(obj, compGeoShp): + '''_pathGeomToSpiralPointSet(obj, compGeoShp)... + Convert a compound set of sequential line segments to directional, connected groupings.''' + PathLog.debug('_pathGeomToSpiralPointSet()') + # Extract intersection line segments for return value as list() + LINES = list() + inLine = list() + lnCnt = 0 + ec = len(compGeoShp.Edges) + start = 2 + + if obj.CutPatternReversed: + edg1 = compGeoShp.Edges[0] # Skip first edge, as it is the closing edge: center to outer tail + ec -= 1 + start = 1 + else: + edg1 = compGeoShp.Edges[1] # Skip first edge, as it is the closing edge: center to outer tail + p1 = FreeCAD.Vector(edg1.Vertexes[0].X, edg1.Vertexes[0].Y, 0.0) + p2 = FreeCAD.Vector(edg1.Vertexes[1].X, edg1.Vertexes[1].Y, 0.0) + tup = ((p1.x, p1.y), (p2.x, p2.y)) + inLine.append(tup) + lst = p2 + + for ei in range(start, ec): # Skipped first edge, started with second edge above as edg1 + edg = compGeoShp.Edges[ei] # Get edge for vertexes + sp = FreeCAD.Vector(edg.Vertexes[0].X, edg.Vertexes[0].Y, 0.0) # check point (first / middle point) + ep = FreeCAD.Vector(edg.Vertexes[1].X, edg.Vertexes[1].Y, 0.0) # end point + tup = ((sp.x, sp.y), (ep.x, ep.y)) + + if sp.sub(p2).Length < 0.000001: + inLine.append(tup) + else: + LINES.append(inLine) # Save inLine segments + lnCnt += 1 + inLine = list() # reset container + inLine.append(tup) + p1 = sp + p2 = ep + # Efor + + lnCnt += 1 + LINES.append(inLine) # Save inLine segments + + return LINES + +def pathGeomToOffsetPointSet(obj, compGeoShp): + '''pathGeomToOffsetPointSet(obj, compGeoShp)... + Convert a compound set of 3D profile segmented wires to 2D segments, applying linear optimization.''' + PathLog.debug('pathGeomToOffsetPointSet()') + + LINES = list() + optimize = obj.OptimizeLinearPaths + ofstCnt = len(compGeoShp) + + # Cycle through offeset loops + for ei in range(0, ofstCnt): + OS = compGeoShp[ei] + lenOS = len(OS) + + if ei > 0: + LINES.append('BRK') + + fp = FreeCAD.Vector(OS[0].x, OS[0].y, OS[0].z) + OS.append(fp) + + # Cycle through points in each loop + prev = OS[0] + pnt = OS[1] + for v in range(1, lenOS): + nxt = OS[v + 1] + if optimize: + # iPOL = prev.isOnLineSegment(nxt, pnt) + iPOL = pnt.isOnLineSegment(prev, nxt) + if iPOL: + pnt = nxt + else: + tup = ((prev.x, prev.y), (pnt.x, pnt.y)) + LINES.append(tup) + prev = pnt + pnt = nxt + else: + tup = ((prev.x, prev.y), (pnt.x, pnt.y)) + LINES.append(tup) + prev = pnt + pnt = nxt + if iPOL: + tup = ((prev.x, prev.y), (pnt.x, pnt.y)) + LINES.append(tup) + # Efor + + return [LINES] \ No newline at end of file diff --git a/src/Mod/Path/PathScripts/PathToolBit.py b/src/Mod/Path/PathScripts/PathToolBit.py index 5f40fc6219..82612f8608 100644 --- a/src/Mod/Path/PathScripts/PathToolBit.py +++ b/src/Mod/Path/PathScripts/PathToolBit.py @@ -23,7 +23,6 @@ # *************************************************************************** import FreeCAD -import Part import PathScripts.PathGeom as PathGeom import PathScripts.PathLog as PathLog import PathScripts.PathPreferences as PathPreferences @@ -36,6 +35,10 @@ import math import os import zipfile +# lazily loaded modules +from lazy_loader.lazy_loader import LazyLoader +Part = LazyLoader('Part', globals(), 'Part') + __title__ = "Tool bits." __author__ = "sliptonic (Brad Collette)" __url__ = "http://www.freecadweb.org" diff --git a/src/Mod/Path/PathScripts/PathToolControllerGui.py b/src/Mod/Path/PathScripts/PathToolControllerGui.py index 3a05fbe301..f71658172a 100644 --- a/src/Mod/Path/PathScripts/PathToolControllerGui.py +++ b/src/Mod/Path/PathScripts/PathToolControllerGui.py @@ -24,7 +24,6 @@ import FreeCAD import FreeCADGui -import Part import PathScripts import PathScripts.PathGui as PathGui import PathScripts.PathLog as PathLog @@ -34,6 +33,10 @@ import PathScripts.PathUtil as PathUtil from PySide import QtCore, QtGui +# lazily loaded modules +from lazy_loader.lazy_loader import LazyLoader +Part = LazyLoader('Part', globals(), 'Part') + # Qt translation handling def translate(context, text, disambig=None): return QtCore.QCoreApplication.translate(context, text, disambig) diff --git a/src/Mod/Path/PathScripts/PathUtils.py b/src/Mod/Path/PathScripts/PathUtils.py index 0ea9c90c30..f5c8b0416f 100644 --- a/src/Mod/Path/PathScripts/PathUtils.py +++ b/src/Mod/Path/PathScripts/PathUtils.py @@ -23,21 +23,24 @@ # *************************************************************************** '''PathUtils -common functions used in PathScripts for filtering, sorting, and generating gcode toolpath data ''' import FreeCAD -import Part import Path import PathScripts import PathScripts.PathGeom as PathGeom -import TechDraw import math import numpy -from DraftGeomUtils import geomType from FreeCAD import Vector from PathScripts import PathJob from PathScripts import PathLog from PySide import QtCore from PySide import QtGui +# lazily loaded modules +from lazy_loader.lazy_loader import LazyLoader +DraftGeomUtils = LazyLoader('DraftGeomUtils', globals(), 'DraftGeomUtils') +Part = LazyLoader('Part', globals(), 'Part') +TechDraw = LazyLoader('TechDraw', globals(), 'TechDraw') + PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule()) #PathLog.trackModule(PathLog.thisModule()) @@ -334,13 +337,13 @@ def getEnvelope(partshape, subshape=None, depthparams=None): def reverseEdge(e): - if geomType(e) == "Circle": + if DraftGeomUtils.geomType(e) == "Circle": arcstpt = e.valueAt(e.FirstParameter) arcmid = e.valueAt((e.LastParameter - e.FirstParameter) * 0.5 + e.FirstParameter) arcendpt = e.valueAt(e.LastParameter) arcofCirc = Part.ArcOfCircle(arcendpt, arcmid, arcstpt) newedge = arcofCirc.toShape() - elif geomType(e) == "LineSegment" or geomType(e) == "Line": + elif DraftGeomUtils.geomType(e) == "LineSegment" or DraftGeomUtils.geomType(e) == "Line": stpt = e.valueAt(e.FirstParameter) endpt = e.valueAt(e.LastParameter) newedge = Part.makeLine(endpt, stpt) diff --git a/src/Mod/Path/PathScripts/PathWaterline.py b/src/Mod/Path/PathScripts/PathWaterline.py index 0362680580..ba930db881 100644 --- a/src/Mod/Path/PathScripts/PathWaterline.py +++ b/src/Mod/Path/PathScripts/PathWaterline.py @@ -1,3481 +1,1971 @@ -# -*- coding: utf-8 -*- - -# *************************************************************************** -# * * -# * Copyright (c) 2019 Russell Johnson (russ4262) * -# * Copyright (c) 2019 sliptonic * -# * * -# * This program is free software; you can redistribute it and/or modify * -# * it under the terms of the GNU Lesser General Public License (LGPL) * -# * as published by the Free Software Foundation; either version 2 of * -# * the License, or (at your option) any later version. * -# * for detail see the LICENCE text file. * -# * * -# * This program is distributed in the hope that it will be useful, * -# * but WITHOUT ANY WARRANTY; without even the implied warranty of * -# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * -# * GNU Library General Public License for more details. * -# * * -# * You should have received a copy of the GNU Library General Public * -# * License along with this program; if not, write to the Free Software * -# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * -# * USA * -# * * -# *************************************************************************** - -from __future__ import print_function - -import FreeCAD -import MeshPart -import Path -import PathScripts.PathLog as PathLog -import PathScripts.PathUtils as PathUtils -import PathScripts.PathOp as PathOp - -from PySide import QtCore -import time -import math -import Part -import Draft - -if FreeCAD.GuiUp: - import FreeCADGui - -__title__ = "Path Waterline Operation" -__author__ = "russ4262 (Russell Johnson), sliptonic (Brad Collette)" -__url__ = "http://www.freecadweb.org" -__doc__ = "Class and implementation of Mill Facing operation." - -PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule()) -# PathLog.trackModule(PathLog.thisModule()) - - -# Qt translation handling -def translate(context, text, disambig=None): - return QtCore.QCoreApplication.translate(context, text, disambig) - - -# OCL must be installed -try: - import ocl -except ImportError: - FreeCAD.Console.PrintError( - translate("Path_Waterline", "This operation requires OpenCamLib to be installed.") + "\n") - import sys - sys.exit(translate("Path_Waterline", "This operation requires OpenCamLib to be installed.")) - - -class ObjectWaterline(PathOp.ObjectOp): - '''Proxy object for Surfacing operation.''' - - def baseObject(self): - '''baseObject() ... returns super of receiver - Used to call base implementation in overwritten functions.''' - return super(self.__class__, self) - - def opFeatures(self, obj): - '''opFeatures(obj) ... return all standard features and edges based geomtries''' - return PathOp.FeatureTool | PathOp.FeatureDepths | PathOp.FeatureHeights | PathOp.FeatureStepDown | PathOp.FeatureCoolant | PathOp.FeatureBaseFaces - - def initOperation(self, obj): - '''initPocketOp(obj) ... - Initialize the operation - property creation and property editor status.''' - self.initOpProperties(obj) - - # For debugging - if PathLog.getLevel(PathLog.thisModule()) != 4: - obj.setEditorMode('ShowTempObjects', 2) # hide - - if not hasattr(obj, 'DoNotSetDefaultValues'): - self.setEditorProperties(obj) - - def initOpProperties(self, obj): - '''initOpProperties(obj) ... create operation specific properties''' - PROPS = [ - ("App::PropertyBool", "ShowTempObjects", "Debug", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Show the temporary path construction objects when module is in DEBUG mode.")), - - ("App::PropertyDistance", "AngularDeflection", "Mesh Conversion", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Smaller values yield a finer, more accurate the mesh. Smaller values increase processing time a lot.")), - ("App::PropertyDistance", "LinearDeflection", "Mesh Conversion", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Smaller values yield a finer, more accurate the mesh. Smaller values do not increase processing time much.")), - - ("App::PropertyInteger", "AvoidLastX_Faces", "Selected Geometry Settings", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Avoid cutting the last 'N' faces in the Base Geometry list of selected faces.")), - ("App::PropertyBool", "AvoidLastX_InternalFeatures", "Selected Geometry Settings", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Do not cut internal features on avoided faces.")), - ("App::PropertyDistance", "BoundaryAdjustment", "Selected Geometry Settings", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Positive values push the cutter toward, or beyond, the boundary. Negative values retract the cutter away from the boundary.")), - ("App::PropertyBool", "BoundaryEnforcement", "Selected Geometry Settings", - QtCore.QT_TRANSLATE_NOOP("App::Property", "If true, the cutter will remain inside the boundaries of the model or selected face(s).")), - ("App::PropertyEnumeration", "HandleMultipleFeatures", "Selected Geometry Settings", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Choose how to process multiple Base Geometry features.")), - ("App::PropertyDistance", "InternalFeaturesAdjustment", "Selected Geometry Settings", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Positive values push the cutter toward, or into, the feature. Negative values retract the cutter away from the feature.")), - ("App::PropertyBool", "InternalFeaturesCut", "Selected Geometry Settings", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Ignore internal feature areas within a larger selected face.")), - - ("App::PropertyEnumeration", "Algorithm", "Clearing Options", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Select the algorithm to use: OCL Dropcutter*, or Experimental.")), - ("App::PropertyEnumeration", "BoundBox", "Clearing Options", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Select the overall boundary for the operation. ")), - ("App::PropertyVectorDistance", "CircularCenterCustom", "Clearing Options", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the start point for circular cut patterns.")), - ("App::PropertyEnumeration", "CircularCenterAt", "Clearing Options", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Choose location of the center point for starting the circular pattern.")), - ("App::PropertyEnumeration", "ClearLastLayer", "Clearing Options", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Set to clear last layer in a `Multi-pass` operation.")), - ("App::PropertyEnumeration", "CutMode", "Clearing Options", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the direction for the cutting tool to engage the material: Climb (ClockWise) or Conventional (CounterClockWise)")), - ("App::PropertyEnumeration", "CutPattern", "Clearing Options", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the geometric clearing pattern to use for the operation.")), - ("App::PropertyFloat", "CutPatternAngle", "Clearing Options", - QtCore.QT_TRANSLATE_NOOP("App::Property", "The yaw angle used for certain clearing patterns")), - ("App::PropertyBool", "CutPatternReversed", "Clearing Options", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Reverse the cut order of the stepover paths. For circular cut patterns, begin at the outside and work toward the center.")), - ("App::PropertyDistance", "DepthOffset", "Clearing Options", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the Z-axis depth offset from the target surface.")), - ("App::PropertyEnumeration", "LayerMode", "Clearing Options", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Complete the operation in a single pass at depth, or mulitiple passes to final depth.")), - ("App::PropertyEnumeration", "ProfileEdges", "Clearing Options", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Profile the edges of the selection.")), - ("App::PropertyDistance", "SampleInterval", "Clearing Options", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the sampling resolution. Smaller values quickly increase processing time.")), - ("App::PropertyPercent", "StepOver", "Clearing Options", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the stepover percentage, based on the tool's diameter.")), - - ("App::PropertyBool", "OptimizeLinearPaths", "Optimization", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Enable optimization of linear paths (co-linear points). Removes unnecessary co-linear points from G-Code output.")), - ("App::PropertyBool", "OptimizeStepOverTransitions", "Optimization", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Enable separate optimization of transitions between, and breaks within, each step over path.")), - ("App::PropertyDistance", "GapThreshold", "Optimization", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Collinear and co-radial artifact gaps that are smaller than this threshold are closed in the path.")), - ("App::PropertyString", "GapSizes", "Optimization", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Feedback: three smallest gaps identified in the path geometry.")), - - ("App::PropertyVectorDistance", "StartPoint", "Start Point", - QtCore.QT_TRANSLATE_NOOP("App::Property", "The custom start point for the path of this operation")), - ("App::PropertyBool", "UseStartPoint", "Start Point", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Make True, if specifying a Start Point")) - ] - - missing = list() - for (prtyp, nm, grp, tt) in PROPS: - if not hasattr(obj, nm): - obj.addProperty(prtyp, nm, grp, tt) - missing.append(nm) - - # Set enumeration lists for enumeration properties - if len(missing) > 0: - ENUMS = self._propertyEnumerations() - for n in ENUMS: - if n in missing: - cmdStr = 'obj.{}={}'.format(n, ENUMS[n]) - exec(cmdStr) - - self.addedAllProperties = True - - def _propertyEnumerations(self): - # Enumeration lists for App::PropertyEnumeration properties - return { - 'Algorithm': ['OCL Dropcutter', 'Experimental'], - 'BoundBox': ['BaseBoundBox', 'Stock'], - 'CircularCenterAt': ['CenterOfMass', 'CenterOfBoundBox', 'XminYmin', 'Custom'], - 'ClearLastLayer': ['Off', 'Line', 'Circular', 'CircularZigZag', 'Offset', 'ZigZag'], - 'CutMode': ['Conventional', 'Climb'], - 'CutPattern': ['None', 'Line', 'Circular', 'CircularZigZag', 'Offset', 'ZigZag'], # Additional goals ['Offset', 'Spiral', 'ZigZagOffset', 'Grid', 'Triangle'] - 'HandleMultipleFeatures': ['Collectively', 'Individually'], - 'LayerMode': ['Single-pass', 'Multi-pass'], - 'ProfileEdges': ['None', 'Only', 'First', 'Last'], - } - - def setEditorProperties(self, obj): - # Used to hide inputs in properties list - show = 0 - hide = 2 - cpShow = 0 - expMode = 0 - obj.setEditorMode('BoundaryEnforcement', hide) - obj.setEditorMode('ProfileEdges', hide) - obj.setEditorMode('InternalFeaturesAdjustment', hide) - obj.setEditorMode('InternalFeaturesCut', hide) - obj.setEditorMode('GapSizes', hide) - obj.setEditorMode('GapThreshold', hide) - obj.setEditorMode('AvoidLastX_Faces', hide) - obj.setEditorMode('AvoidLastX_InternalFeatures', hide) - obj.setEditorMode('BoundaryAdjustment', hide) - obj.setEditorMode('HandleMultipleFeatures', hide) - if hasattr(obj, 'EnableRotation'): - obj.setEditorMode('EnableRotation', hide) - if obj.CutPattern == 'None': - show = 2 - hide = 2 - cpShow = 2 - # elif obj.CutPattern in ['Line', 'ZigZag']: - # show = 0 - # hide = 2 - elif obj.CutPattern in ['Circular', 'CircularZigZag']: - show = 2 # hide - hide = 0 # show - # obj.setEditorMode('StepOver', cpShow) - obj.setEditorMode('CutPatternAngle', show) - obj.setEditorMode('CircularCenterAt', hide) - obj.setEditorMode('CircularCenterCustom', hide) - if obj.Algorithm == 'Experimental': - expMode = 2 - obj.setEditorMode('SampleInterval', expMode) - obj.setEditorMode('LinearDeflection', expMode) - obj.setEditorMode('AngularDeflection', expMode) - - def onChanged(self, obj, prop): - if hasattr(self, 'addedAllProperties'): - if self.addedAllProperties is True: - if prop in ['Algorithm', 'CutPattern']: - self.setEditorProperties(obj) - - def opOnDocumentRestored(self, obj): - self.initOpProperties(obj) - - if PathLog.getLevel(PathLog.thisModule()) != 4: - obj.setEditorMode('ShowTempObjects', 2) # hide - else: - obj.setEditorMode('ShowTempObjects', 0) # show - - self.setEditorProperties(obj) - - def opSetDefaultValues(self, obj, job): - '''opSetDefaultValues(obj, job) ... initialize defaults''' - job = PathUtils.findParentJob(obj) - - obj.OptimizeLinearPaths = True - obj.InternalFeaturesCut = True - obj.OptimizeStepOverTransitions = False - obj.BoundaryEnforcement = True - obj.UseStartPoint = False - obj.AvoidLastX_InternalFeatures = True - obj.CutPatternReversed = False - obj.StartPoint.x = 0.0 - obj.StartPoint.y = 0.0 - obj.StartPoint.z = obj.ClearanceHeight.Value - obj.Algorithm = 'OCL Dropcutter' - obj.ProfileEdges = 'None' - obj.LayerMode = 'Single-pass' - obj.CutMode = 'Conventional' - obj.CutPattern = 'None' - obj.HandleMultipleFeatures = 'Collectively' # 'Individually' - obj.CircularCenterAt = 'CenterOfMass' # 'CenterOfBoundBox', 'XminYmin', 'Custom' - obj.GapSizes = 'No gaps identified.' - obj.ClearLastLayer = 'Off' - obj.StepOver = 100 - obj.CutPatternAngle = 0.0 - obj.DepthOffset.Value = 0.0 - obj.SampleInterval.Value = 1.0 - obj.BoundaryAdjustment.Value = 0.0 - obj.InternalFeaturesAdjustment.Value = 0.0 - obj.AvoidLastX_Faces = 0 - obj.CircularCenterCustom.x = 0.0 - obj.CircularCenterCustom.y = 0.0 - obj.CircularCenterCustom.z = 0.0 - obj.GapThreshold.Value = 0.005 - obj.LinearDeflection.Value = 0.0001 - obj.AngularDeflection.Value = 0.25 - # For debugging - obj.ShowTempObjects = False - - # need to overwrite the default depth calculations for facing - d = None - if job: - if job.Stock: - d = PathUtils.guessDepths(job.Stock.Shape, None) - PathLog.debug("job.Stock exists") - else: - PathLog.debug("job.Stock NOT exist") - else: - PathLog.debug("job NOT exist") - - if d is not None: - obj.OpFinalDepth.Value = d.final_depth - obj.OpStartDepth.Value = d.start_depth - else: - obj.OpFinalDepth.Value = -10 - obj.OpStartDepth.Value = 10 - - PathLog.debug('Default OpFinalDepth: {}'.format(obj.OpFinalDepth.Value)) - PathLog.debug('Defualt OpStartDepth: {}'.format(obj.OpStartDepth.Value)) - - def opApplyPropertyLimits(self, obj): - '''opApplyPropertyLimits(obj) ... Apply necessary limits to user input property values before performing main operation.''' - # Limit sample interval - if obj.SampleInterval.Value < 0.001: - obj.SampleInterval.Value = 0.001 - PathLog.error(translate('PathWaterline', 'Sample interval limits are 0.001 to 25.4 millimeters.')) - if obj.SampleInterval.Value > 25.4: - obj.SampleInterval.Value = 25.4 - PathLog.error(translate('PathWaterline', 'Sample interval limits are 0.001 to 25.4 millimeters.')) - - # Limit cut pattern angle - if obj.CutPatternAngle < -360.0: - obj.CutPatternAngle = 0.0 - PathLog.error(translate('PathWaterline', 'Cut pattern angle limits are +-360 degrees.')) - if obj.CutPatternAngle >= 360.0: - obj.CutPatternAngle = 0.0 - PathLog.error(translate('PathWaterline', 'Cut pattern angle limits are +- 360 degrees.')) - - # Limit StepOver to natural number percentage - if obj.StepOver > 100: - obj.StepOver = 100 - if obj.StepOver < 1: - obj.StepOver = 1 - - # Limit AvoidLastX_Faces to zero and positive values - if obj.AvoidLastX_Faces < 0: - obj.AvoidLastX_Faces = 0 - PathLog.error(translate('PathWaterline', 'AvoidLastX_Faces: Only zero or positive values permitted.')) - if obj.AvoidLastX_Faces > 100: - obj.AvoidLastX_Faces = 100 - PathLog.error(translate('PathWaterline', 'AvoidLastX_Faces: Avoid last X faces count limited to 100.')) - - def opExecute(self, obj): - '''opExecute(obj) ... process surface operation''' - PathLog.track() - - self.modelSTLs = list() - self.safeSTLs = list() - self.modelTypes = list() - self.boundBoxes = list() - self.profileShapes = list() - self.collectiveShapes = list() - self.individualShapes = list() - self.avoidShapes = list() - self.geoTlrnc = None - self.tempGroup = None - self.CutClimb = False - self.closedGap = False - self.gaps = [0.1, 0.2, 0.3] - CMDS = list() - modelVisibility = list() - FCAD = FreeCAD.ActiveDocument - - # Set debugging behavior - self.showDebugObjects = False # Set to true if you want a visual DocObjects created for some path construction objects - self.showDebugObjects = obj.ShowTempObjects - deleteTempsFlag = True # Set to False for debugging - if PathLog.getLevel(PathLog.thisModule()) == 4: - deleteTempsFlag = False - else: - self.showDebugObjects = False - - # mark beginning of operation and identify parent Job - PathLog.info('\nBegin Waterline operation...') - startTime = time.time() - - # Identify parent Job - JOB = PathUtils.findParentJob(obj) - if JOB is None: - PathLog.error(translate('PathWaterline', "No JOB")) - return - self.stockZMin = JOB.Stock.Shape.BoundBox.ZMin - - # set cut mode; reverse as needed - if obj.CutMode == 'Climb': - self.CutClimb = True - if obj.CutPatternReversed is True: - if self.CutClimb is True: - self.CutClimb = False - else: - self.CutClimb = True - - # Begin GCode for operation with basic information - # ... and move cutter to clearance height and startpoint - output = '' - if obj.Comment != '': - self.commandlist.append(Path.Command('N ({})'.format(str(obj.Comment)), {})) - self.commandlist.append(Path.Command('N ({})'.format(obj.Label), {})) - self.commandlist.append(Path.Command('N (Tool type: {})'.format(str(obj.ToolController.Tool.ToolType)), {})) - self.commandlist.append(Path.Command('N (Compensated Tool Path. Diameter: {})'.format(str(obj.ToolController.Tool.Diameter)), {})) - self.commandlist.append(Path.Command('N (Sample interval: {})'.format(str(obj.SampleInterval.Value)), {})) - self.commandlist.append(Path.Command('N (Step over %: {})'.format(str(obj.StepOver)), {})) - self.commandlist.append(Path.Command('N ({})'.format(output), {})) - self.commandlist.append(Path.Command('G0', {'Z': obj.ClearanceHeight.Value, 'F': self.vertRapid})) - if obj.UseStartPoint is True: - self.commandlist.append(Path.Command('G0', {'X': obj.StartPoint.x, 'Y': obj.StartPoint.y, 'F': self.horizRapid})) - - # Instantiate additional class operation variables - self.resetOpVariables() - - # Impose property limits - self.opApplyPropertyLimits(obj) - - # Create temporary group for temporary objects, removing existing - # if self.showDebugObjects is True: - tempGroupName = 'tempPathWaterlineGroup' - if FCAD.getObject(tempGroupName): - for to in FCAD.getObject(tempGroupName).Group: - FCAD.removeObject(to.Name) - FCAD.removeObject(tempGroupName) # remove temp directory if already exists - if FCAD.getObject(tempGroupName + '001'): - for to in FCAD.getObject(tempGroupName + '001').Group: - FCAD.removeObject(to.Name) - FCAD.removeObject(tempGroupName + '001') # remove temp directory if already exists - tempGroup = FCAD.addObject('App::DocumentObjectGroup', tempGroupName) - tempGroupName = tempGroup.Name - self.tempGroup = tempGroup - tempGroup.purgeTouched() - # Add temp object to temp group folder with following code: - # ... self.tempGroup.addObject(OBJ) - - # Setup cutter for OCL and cutout value for operation - based on tool controller properties - self.cutter = self.setOclCutter(obj) - self.safeCutter = self.setOclCutter(obj, safe=True) - if self.cutter is False or self.safeCutter is False: - PathLog.error(translate('PathWaterline', "Canceling Waterline operation. Error creating OCL cutter.")) - return - toolDiam = self.cutter.getDiameter() - self.cutOut = (toolDiam * (float(obj.StepOver) / 100.0)) - self.radius = toolDiam / 2.0 - self.gaps = [toolDiam, toolDiam, toolDiam] - - # Get height offset values for later use - self.SafeHeightOffset = JOB.SetupSheet.SafeHeightOffset.Value - self.ClearHeightOffset = JOB.SetupSheet.ClearanceHeightOffset.Value - - # Set deflection values for mesh generation - useDGT = False - try: # try/except is for Path Jobs created before GeometryTolerance - self.geoTlrnc = JOB.GeometryTolerance.Value - if self.geoTlrnc == 0.0: - useDGT = True - except AttributeError as ee: - PathLog.warning('{}\nPlease set Job.GeometryTolerance to an acceptable value. Using PathPreferences.defaultGeometryTolerance().'.format(ee)) - useDGT = True - if useDGT: - import PathScripts.PathPreferences as PathPreferences - self.geoTlrnc = PathPreferences.defaultGeometryTolerance() - - # Calculate default depthparams for operation - self.depthParams = PathUtils.depth_params(obj.ClearanceHeight.Value, obj.SafeHeight.Value, obj.StartDepth.Value, obj.StepDown.Value, 0.0, obj.FinalDepth.Value) - self.midDep = (obj.StartDepth.Value + obj.FinalDepth.Value) / 2.0 - - # make circle for workplane - self.wpc = Part.makeCircle(2.0) - - # Save model visibilities for restoration - if FreeCAD.GuiUp: - for m in range(0, len(JOB.Model.Group)): - mNm = JOB.Model.Group[m].Name - modelVisibility.append(FreeCADGui.ActiveDocument.getObject(mNm).Visibility) - - # Setup STL, model type, and bound box containers for each model in Job - for m in range(0, len(JOB.Model.Group)): - M = JOB.Model.Group[m] - self.modelSTLs.append(False) - self.safeSTLs.append(False) - self.profileShapes.append(False) - # Set bound box - if obj.BoundBox == 'BaseBoundBox': - if M.TypeId.startswith('Mesh'): - self.modelTypes.append('M') # Mesh - self.boundBoxes.append(M.Mesh.BoundBox) - else: - self.modelTypes.append('S') # Solid - self.boundBoxes.append(M.Shape.BoundBox) - elif obj.BoundBox == 'Stock': - self.modelTypes.append('S') # Solid - self.boundBoxes.append(JOB.Stock.Shape.BoundBox) - - # ###### MAIN COMMANDS FOR OPERATION ###### - - # Begin processing obj.Base data and creating GCode - # Process selected faces, if available - pPM = self._preProcessModel(JOB, obj) - if pPM is False: - PathLog.error('Unable to pre-process obj.Base.') - else: - (FACES, VOIDS) = pPM - - # Create OCL.stl model objects - if obj.Algorithm == 'OCL Dropcutter': - self._prepareModelSTLs(JOB, obj) - PathLog.debug('obj.LinearDeflection.Value: {}'.format(obj.LinearDeflection.Value)) - PathLog.debug('obj.AngularDeflection.Value: {}'.format(obj.AngularDeflection.Value)) - - for m in range(0, len(JOB.Model.Group)): - Mdl = JOB.Model.Group[m] - if FACES[m] is False: - PathLog.error('No data for model base: {}'.format(JOB.Model.Group[m].Label)) - else: - if m > 0: - # Raise to clearance between models - CMDS.append(Path.Command('N (Transition to base: {}.)'.format(Mdl.Label))) - CMDS.append(Path.Command('G0', {'Z': obj.ClearanceHeight.Value, 'F': self.vertRapid})) - PathLog.info('Working on Model.Group[{}]: {}'.format(m, Mdl.Label)) - # make stock-model-voidShapes STL model for avoidance detection on transitions - if obj.Algorithm == 'OCL Dropcutter': - self._makeSafeSTL(JOB, obj, m, FACES[m], VOIDS[m]) - # time.sleep(0.2) - # Process model/faces - OCL objects must be ready - CMDS.extend(self._processCutAreas(JOB, obj, m, FACES[m], VOIDS[m])) - - # Save gcode produced - self.commandlist.extend(CMDS) - - # ###### CLOSING COMMANDS FOR OPERATION ###### - - # Delete temporary objects - # Restore model visibilities for restoration - if FreeCAD.GuiUp: - FreeCADGui.ActiveDocument.getObject(tempGroupName).Visibility = False - for m in range(0, len(JOB.Model.Group)): - M = JOB.Model.Group[m] - M.Visibility = modelVisibility[m] - - if deleteTempsFlag is True: - for to in tempGroup.Group: - if hasattr(to, 'Group'): - for go in to.Group: - FCAD.removeObject(go.Name) - FCAD.removeObject(to.Name) - FCAD.removeObject(tempGroupName) - else: - if len(tempGroup.Group) == 0: - FCAD.removeObject(tempGroupName) - else: - tempGroup.purgeTouched() - - # Provide user feedback for gap sizes - gaps = list() - for g in self.gaps: - if g != toolDiam: - gaps.append(g) - if len(gaps) > 0: - obj.GapSizes = '{} mm'.format(gaps) - else: - if self.closedGap is True: - obj.GapSizes = 'Closed gaps < Gap Threshold.' - else: - obj.GapSizes = 'No gaps identified.' - - # clean up class variables - self.resetOpVariables() - self.deleteOpVariables() - - self.modelSTLs = None - self.safeSTLs = None - self.modelTypes = None - self.boundBoxes = None - self.gaps = None - self.closedGap = None - self.SafeHeightOffset = None - self.ClearHeightOffset = None - self.depthParams = None - self.midDep = None - self.wpc = None - del self.modelSTLs - del self.safeSTLs - del self.modelTypes - del self.boundBoxes - del self.gaps - del self.closedGap - del self.SafeHeightOffset - del self.ClearHeightOffset - del self.depthParams - del self.midDep - del self.wpc - - execTime = time.time() - startTime - PathLog.info('Operation time: {} sec.'.format(execTime)) - - return True - - # Methods for constructing the cut area - def _preProcessModel(self, JOB, obj): - PathLog.debug('_preProcessModel()') - - FACES = list() - VOIDS = list() - fShapes = list() - vShapes = list() - preProcEr = translate('PathWaterline', 'Error pre-processing Face') - warnFinDep = translate('PathWaterline', 'Final Depth might need to be lower. Internal features detected in Face') - GRP = JOB.Model.Group - lenGRP = len(GRP) - - # Crete place holders for each base model in Job - for m in range(0, lenGRP): - FACES.append(False) - VOIDS.append(False) - fShapes.append(False) - vShapes.append(False) - - # The user has selected subobjects from the base. Pre-Process each. - if obj.Base and len(obj.Base) > 0: - PathLog.debug(' -obj.Base exists. Pre-processing for selected faces.') - - (FACES, VOIDS) = self._identifyFacesAndVoids(JOB, obj, FACES, VOIDS) - - # Cycle through each base model, processing faces for each - for m in range(0, lenGRP): - base = GRP[m] - (mFS, mVS, mPS) = self._preProcessFacesAndVoids(obj, base, m, FACES, VOIDS) - fShapes[m] = mFS - vShapes[m] = mVS - self.profileShapes[m] = mPS - else: - PathLog.debug(' -No obj.Base data.') - for m in range(0, lenGRP): - self.modelSTLs[m] = True - - # Process each model base, as a whole, as needed - # PathLog.debug(' -Pre-processing all models in Job.') - for m in range(0, lenGRP): - if fShapes[m] is False: - PathLog.debug(' -Pre-processing {} as a whole.'.format(GRP[m].Label)) - if obj.BoundBox == 'BaseBoundBox': - base = GRP[m] - elif obj.BoundBox == 'Stock': - base = JOB.Stock - - pPEB = self._preProcessEntireBase(obj, base, m) - if pPEB is False: - PathLog.error(' -Failed to pre-process base as a whole.') - else: - (fcShp, prflShp) = pPEB - if fcShp is not False: - if fcShp is True: - PathLog.debug(' -fcShp is True.') - fShapes[m] = True - else: - fShapes[m] = [fcShp] - if prflShp is not False: - if fcShp is not False: - PathLog.debug('vShapes[{}]: {}'.format(m, vShapes[m])) - if vShapes[m] is not False: - PathLog.debug(' -Cutting void from base profile shape.') - adjPS = prflShp.cut(vShapes[m][0]) - self.profileShapes[m] = [adjPS] - else: - PathLog.debug(' -vShapes[m] is False.') - self.profileShapes[m] = [prflShp] - else: - PathLog.debug(' -Saving base profile shape.') - self.profileShapes[m] = [prflShp] - PathLog.debug('self.profileShapes[{}]: {}'.format(m, self.profileShapes[m])) - # Efor - - return (fShapes, vShapes) - - def _identifyFacesAndVoids(self, JOB, obj, F, V): - TUPS = list() - GRP = JOB.Model.Group - lenGRP = len(GRP) - - # Separate selected faces into (base, face) tuples and flag model(s) for STL creation - for (bs, SBS) in obj.Base: - for sb in SBS: - # Flag model for STL creation - mdlIdx = None - for m in range(0, lenGRP): - if bs is GRP[m]: - self.modelSTLs[m] = True - mdlIdx = m - break - TUPS.append((mdlIdx, bs, sb)) # (model idx, base, sub) - - # Apply `AvoidXFaces` value - faceCnt = len(TUPS) - add = faceCnt - obj.AvoidLastX_Faces - for bst in range(0, faceCnt): - (m, base, sub) = TUPS[bst] - shape = getattr(base.Shape, sub) - if isinstance(shape, Part.Face): - faceIdx = int(sub[4:]) - 1 - if bst < add: - if F[m] is False: - F[m] = list() - F[m].append((shape, faceIdx)) - else: - if V[m] is False: - V[m] = list() - V[m].append((shape, faceIdx)) - return (F, V) - - def _preProcessFacesAndVoids(self, obj, base, m, FACES, VOIDS): - mFS = False - mVS = False - mPS = False - mIFS = list() - BB = base.Shape.BoundBox - - if FACES[m] is not False: - isHole = False - if obj.HandleMultipleFeatures == 'Collectively': - cont = True - fsL = list() # face shape list - ifL = list() # avoid shape list - outFCS = list() - - # Get collective envelope slice of selected faces - for (fcshp, fcIdx) in FACES[m]: - fNum = fcIdx + 1 - fsL.append(fcshp) - gFW = self._getFaceWires(base, fcshp, fcIdx) - if gFW is False: - PathLog.debug('Failed to get wires from Face{}'.format(fNum)) - elif gFW[0] is False: - PathLog.debug('Cannot process Face{}. Check that it has horizontal surface exposure.'.format(fNum)) - else: - ((otrFace, raised), intWires) = gFW - outFCS.append(otrFace) - if obj.InternalFeaturesCut is False: - if intWires is not False: - for (iFace, rsd) in intWires: - ifL.append(iFace) - - PathLog.debug('Attempting to get cross-section of collective faces.') - if len(outFCS) == 0: - PathLog.error('Cannot process selected faces. Check horizontal surface exposure.'.format(fNum)) - cont = False - else: - cfsL = Part.makeCompound(outFCS) - - # Handle profile edges request - if cont is True and obj.ProfileEdges != 'None': - ofstVal = self._calculateOffsetValue(obj, isHole) - psOfst = self._extractFaceOffset(obj, cfsL, ofstVal) - if psOfst is not False: - mPS = [psOfst] - if obj.ProfileEdges == 'Only': - mFS = True - cont = False - else: - PathLog.error(' -Failed to create profile geometry for selected faces.') - cont = False - - if cont is True: - if self.showDebugObjects is True: - T = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpCollectiveShape') - T.Shape = cfsL - T.purgeTouched() - self.tempGroup.addObject(T) - - ofstVal = self._calculateOffsetValue(obj, isHole) - faceOfstShp = self._extractFaceOffset(obj, cfsL, ofstVal) - if faceOfstShp is False: - PathLog.error(' -Failed to create offset face.') - cont = False - - if cont is True: - lenIfL = len(ifL) - if obj.InternalFeaturesCut is False: - if lenIfL == 0: - PathLog.debug(' -No internal features saved.') - else: - if lenIfL == 1: - casL = ifL[0] - else: - casL = Part.makeCompound(ifL) - if self.showDebugObjects is True: - C = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpCompoundIntFeat') - C.Shape = casL - C.purgeTouched() - self.tempGroup.addObject(C) - ofstVal = self._calculateOffsetValue(obj, isHole=True) - intOfstShp = self._extractFaceOffset(obj, casL, ofstVal) - mIFS.append(intOfstShp) - # faceOfstShp = faceOfstShp.cut(intOfstShp) - - mFS = [faceOfstShp] - # Eif - - elif obj.HandleMultipleFeatures == 'Individually': - for (fcshp, fcIdx) in FACES[m]: - cont = True - fsL = list() # face shape list - ifL = list() # avoid shape list - fNum = fcIdx + 1 - outerFace = False - - gFW = self._getFaceWires(base, fcshp, fcIdx) - if gFW is False: - PathLog.debug('Failed to get wires from Face{}'.format(fNum)) - cont = False - elif gFW[0] is False: - PathLog.debug('Cannot process Face{}. Check that it has horizontal surface exposure.'.format(fNum)) - cont = False - outerFace = False - else: - ((otrFace, raised), intWires) = gFW - outerFace = otrFace - if obj.InternalFeaturesCut is False: - if intWires is not False: - for (iFace, rsd) in intWires: - ifL.append(iFace) - - if outerFace is not False: - PathLog.debug('Attempting to create offset face of Face{}'.format(fNum)) - - if obj.ProfileEdges != 'None': - ofstVal = self._calculateOffsetValue(obj, isHole) - psOfst = self._extractFaceOffset(obj, outerFace, ofstVal) - if psOfst is not False: - if mPS is False: - mPS = list() - mPS.append(psOfst) - if obj.ProfileEdges == 'Only': - if mFS is False: - mFS = list() - mFS.append(True) - cont = False - else: - PathLog.error(' -Failed to create profile geometry for Face{}.'.format(fNum)) - cont = False - - if cont is True: - ofstVal = self._calculateOffsetValue(obj, isHole) - faceOfstShp = self._extractFaceOffset(obj, slc, ofstVal) - - lenIfl = len(ifL) - if obj.InternalFeaturesCut is False and lenIfl > 0: - if lenIfl == 1: - casL = ifL[0] - else: - casL = Part.makeCompound(ifL) - - ofstVal = self._calculateOffsetValue(obj, isHole=True) - intOfstShp = self._extractFaceOffset(obj, casL, ofstVal) - mIFS.append(intOfstShp) - # faceOfstShp = faceOfstShp.cut(intOfstShp) - - if mFS is False: - mFS = list() - mFS.append(faceOfstShp) - # Eif - # Efor - # Eif - # Eif - - if len(mIFS) > 0: - if mVS is False: - mVS = list() - for ifs in mIFS: - mVS.append(ifs) - - if VOIDS[m] is not False: - PathLog.debug('Processing avoid faces.') - cont = True - isHole = False - outFCS = list() - intFEAT = list() - - for (fcshp, fcIdx) in VOIDS[m]: - fNum = fcIdx + 1 - gFW = self._getFaceWires(base, fcshp, fcIdx) - if gFW is False: - PathLog.debug('Failed to get wires from avoid Face{}'.format(fNum)) - cont = False - else: - ((otrFace, raised), intWires) = gFW - outFCS.append(otrFace) - if obj.AvoidLastX_InternalFeatures is False: - if intWires is not False: - for (iFace, rsd) in intWires: - intFEAT.append(iFace) - - lenOtFcs = len(outFCS) - if lenOtFcs == 0: - cont = False - else: - if lenOtFcs == 1: - avoid = outFCS[0] - else: - avoid = Part.makeCompound(outFCS) - - if self.showDebugObjects is True: - PathLog.debug('*** tmpAvoidArea') - P = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpVoidEnvelope') - P.Shape = avoid - # P.recompute() - P.purgeTouched() - self.tempGroup.addObject(P) - - if cont is True: - if self.showDebugObjects is True: - PathLog.debug('*** tmpVoidCompound') - P = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpVoidCompound') - P.Shape = avoid - # P.recompute() - P.purgeTouched() - self.tempGroup.addObject(P) - ofstVal = self._calculateOffsetValue(obj, isHole, isVoid=True) - avdOfstShp = self._extractFaceOffset(obj, avoid, ofstVal) - if avdOfstShp is False: - PathLog.error('Failed to create collective offset avoid face.') - cont = False - - if cont is True: - avdShp = avdOfstShp - - if obj.AvoidLastX_InternalFeatures is False and len(intFEAT) > 0: - if len(intFEAT) > 1: - ifc = Part.makeCompound(intFEAT) - else: - ifc = intFEAT[0] - ofstVal = self._calculateOffsetValue(obj, isHole=True) - ifOfstShp = self._extractFaceOffset(obj, ifc, ofstVal) - if ifOfstShp is False: - PathLog.error('Failed to create collective offset avoid internal features.') - else: - avdShp = avdOfstShp.cut(ifOfstShp) - - if mVS is False: - mVS = list() - mVS.append(avdShp) - - - return (mFS, mVS, mPS) - - def _getFaceWires(self, base, fcshp, fcIdx): - outFace = False - INTFCS = list() - fNum = fcIdx + 1 - # preProcEr = translate('PathWaterline', 'Error pre-processing Face') - warnFinDep = translate('PathWaterline', 'Final Depth might need to be lower. Internal features detected in Face') - - PathLog.debug('_getFaceWires() from Face{}'.format(fNum)) - WIRES = self._extractWiresFromFace(base, fcshp) - if WIRES is False: - PathLog.error('Failed to extract wires from Face{}'.format(fNum)) - return False - - # Process remaining internal features, adding to FCS list - lenW = len(WIRES) - for w in range(0, lenW): - (wire, rsd) = WIRES[w] - PathLog.debug('Processing Wire{} in Face{}. isRaised: {}'.format(w + 1, fNum, rsd)) - if wire.isClosed() is False: - PathLog.debug(' -wire is not closed.') - else: - slc = self._flattenWireToFace(wire) - if slc is False: - PathLog.error('FAILED to identify horizontal exposure on Face{}.'.format(fNum)) - else: - if w == 0: - outFace = (slc, rsd) - else: - # add to VOIDS so cutter avoids area. - PathLog.warning(warnFinDep + str(fNum) + '.') - INTFCS.append((slc, rsd)) - if len(INTFCS) == 0: - return (outFace, False) - else: - return (outFace, INTFCS) - - def _preProcessEntireBase(self, obj, base, m): - cont = True - isHole = False - prflShp = False - # Create envelope, extract cross-section and make offset co-planar shape - # baseEnv = PathUtils.getEnvelope(base.Shape, subshape=None, depthparams=self.depthParams) - - try: - baseEnv = PathUtils.getEnvelope(partshape=base.Shape, subshape=None, depthparams=self.depthParams) # Produces .Shape - except Exception as ee: - PathLog.error(str(ee)) - shell = base.Shape.Shells[0] - solid = Part.makeSolid(shell) - try: - baseEnv = PathUtils.getEnvelope(partshape=solid, subshape=None, depthparams=self.depthParams) # Produces .Shape - except Exception as eee: - PathLog.error(str(eee)) - cont = False - # time.sleep(0.2) - - if cont is True: - csFaceShape = self._getShapeSlice(baseEnv) - if csFaceShape is False: - PathLog.debug('_getShapeSlice(baseEnv) failed') - csFaceShape = self._getCrossSection(baseEnv) - if csFaceShape is False: - PathLog.debug('_getCrossSection(baseEnv) failed') - csFaceShape = self._getSliceFromEnvelope(baseEnv) - if csFaceShape is False: - PathLog.error('Failed to slice baseEnv shape.') - cont = False - - if cont is True and obj.ProfileEdges != 'None': - PathLog.debug(' -Attempting profile geometry for model base.') - ofstVal = self._calculateOffsetValue(obj, isHole) - psOfst = self._extractFaceOffset(obj, csFaceShape, ofstVal) - if psOfst is not False: - if obj.ProfileEdges == 'Only': - return (True, psOfst) - prflShp = psOfst - else: - PathLog.error(' -Failed to create profile geometry.') - cont = False - - if cont is True: - ofstVal = self._calculateOffsetValue(obj, isHole) - faceOffsetShape = self._extractFaceOffset(obj, csFaceShape, ofstVal) - if faceOffsetShape is False: - PathLog.error('_extractFaceOffset() failed.') - else: - faceOffsetShape.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - faceOffsetShape.BoundBox.ZMin)) - return (faceOffsetShape, prflShp) - return False - - def _extractWiresFromFace(self, base, fc): - '''_extractWiresFromFace(base, fc) ... - Attempts to return all closed wires within a parent face, including the outer most wire of the parent. - The wires are ordered by area. Each wire is also categorized as a pocket(False) or raised protrusion(True). - ''' - PathLog.debug('_extractWiresFromFace()') - - WIRES = list() - lenWrs = len(fc.Wires) - PathLog.debug(' -Wire count: {}'.format(lenWrs)) - - def index0(tup): - return tup[0] - - # Cycle through wires in face - for w in range(0, lenWrs): - PathLog.debug(' -Analyzing wire_{}'.format(w + 1)) - wire = fc.Wires[w] - checkEdges = False - cont = True - - # Check for closed edges (circles, ellipses, etc...) - for E in wire.Edges: - if E.isClosed() is True: - checkEdges = True - break - - if checkEdges is True: - PathLog.debug(' -checkEdges is True') - for e in range(0, len(wire.Edges)): - edge = wire.Edges[e] - if edge.isClosed() is True and edge.Mass > 0.01: - PathLog.debug(' -Found closed edge') - raised = False - ip = self._isPocket(base, fc, edge) - if ip is False: - raised = True - ebb = edge.BoundBox - eArea = ebb.XLength * ebb.YLength - F = Part.Face(Part.Wire([edge])) - WIRES.append((eArea, F.Wires[0], raised)) - cont = False - - if cont is True: - PathLog.debug(' -cont is True') - # If only one wire and not checkEdges, return first wire - if lenWrs == 1: - return [(wire, False)] - - raised = False - wbb = wire.BoundBox - wArea = wbb.XLength * wbb.YLength - if w > 0: - ip = self._isPocket(base, fc, wire) - if ip is False: - raised = True - WIRES.append((wArea, Part.Wire(wire.Edges), raised)) - - nf = len(WIRES) - if nf > 0: - PathLog.debug(' -number of wires found is {}'.format(nf)) - if nf == 1: - (area, W, raised) = WIRES[0] - return [(W, raised)] - else: - sortedWIRES = sorted(WIRES, key=index0, reverse=True) - return [(W, raised) for (area, W, raised) in sortedWIRES] # outer, then inner by area size - - return False - - def _calculateOffsetValue(self, obj, isHole, isVoid=False): - '''_calculateOffsetValue(obj, isHole, isVoid) ... internal function. - Calculate the offset for the Path.Area() function.''' - JOB = PathUtils.findParentJob(obj) - tolrnc = JOB.GeometryTolerance.Value - - if isVoid is False: - if isHole is True: - offset = -1 * obj.InternalFeaturesAdjustment.Value - offset += self.radius # (self.radius + (tolrnc / 10.0)) - else: - offset = -1 * obj.BoundaryAdjustment.Value - if obj.BoundaryEnforcement is True: - offset += self.radius # (self.radius + (tolrnc / 10.0)) - else: - offset -= self.radius # (self.radius + (tolrnc / 10.0)) - offset = 0.0 - offset - else: - offset = -1 * obj.BoundaryAdjustment.Value - offset += self.radius # (self.radius + (tolrnc / 10.0)) - - return offset - - def _extractFaceOffset(self, obj, fcShape, offset, makeComp=True): - '''_extractFaceOffset(fcShape, offset) ... internal function. - Original _buildPathArea() version copied from PathAreaOp.py module. This version is modified. - Adjustments made based on notes by @sliptonic at this webpage: https://github.com/sliptonic/FreeCAD/wiki/PathArea-notes.''' - PathLog.debug('_extractFaceOffset()') - - if fcShape.BoundBox.ZMin != 0.0: - fcShape.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - fcShape.BoundBox.ZMin)) - - areaParams = {} - areaParams['Offset'] = offset - areaParams['Fill'] = 1 # 1 - areaParams['Coplanar'] = 0 - areaParams['SectionCount'] = 1 # -1 = full(all per depthparams??) sections - areaParams['Reorient'] = True - areaParams['OpenMode'] = 0 - areaParams['MaxArcPoints'] = 400 # 400 - areaParams['Project'] = True - # areaParams['Tolerance'] = 0.001 - - area = Path.Area() # Create instance of Area() class object - # area.setPlane(PathUtils.makeWorkplane(fcShape)) # Set working plane - area.setPlane(PathUtils.makeWorkplane(self.wpc)) # Set working plane to normal at Z=1 - area.add(fcShape) - area.setParams(**areaParams) # set parameters - - # Save parameters for debugging - # obj.AreaParams = str(area.getParams()) - # PathLog.debug("Area with params: {}".format(area.getParams())) - - offsetShape = area.getShape() - wCnt = len(offsetShape.Wires) - if wCnt == 0: - return False - elif wCnt == 1: - ofstFace = Part.Face(offsetShape.Wires[0]) - if not makeComp: - ofstFace = [ofstFace] - else: - W = list() - for wr in offsetShape.Wires: - W.append(Part.Face(wr)) - if makeComp: - ofstFace = Part.makeCompound(W) - else: - ofstFace = W - - return ofstFace # offsetShape - - def _isPocket(self, b, f, w): - '''_isPocket(b, f, w)... - Attempts to determine if the wire(w) in face(f) of base(b) is a pocket or raised protrusion. - Returns True if pocket, False if raised protrusion.''' - e = w.Edges[0] - for fi in range(0, len(b.Shape.Faces)): - face = b.Shape.Faces[fi] - for ei in range(0, len(face.Edges)): - edge = face.Edges[ei] - if e.isSame(edge) is True: - if f is face: - # Alternative: run loop to see if all edges are same - pass # same source face, look for another - else: - if face.CenterOfMass.z < f.CenterOfMass.z: - return True - return False - - def _flattenWireToFace(self, wire): - PathLog.debug('_flattenWireToFace()') - if wire.isClosed() is False: - PathLog.debug(' -wire.isClosed() is False') - return False - - # If wire is planar horizontal, convert to a face and return - if wire.BoundBox.ZLength == 0.0: - slc = Part.Face(wire) - return slc - - # Attempt to create a new wire for manipulation, if not, use original - newWire = Part.Wire(wire.Edges) - if newWire.isClosed() is True: - nWire = newWire - else: - PathLog.debug(' -newWire.isClosed() is False') - nWire = wire - - # Attempt extrusion, and then try a manual slice and then cross-section - ext = self._getExtrudedShape(nWire) - if ext is False: - PathLog.debug('_getExtrudedShape() failed') - else: - slc = self._getShapeSlice(ext) - if slc is not False: - return slc - cs = self._getCrossSection(ext, True) - if cs is not False: - return cs - - # Attempt creating an envelope, and then try a manual slice and then cross-section - env = self._getShapeEnvelope(nWire) - if env is False: - PathLog.debug('_getShapeEnvelope() failed') - else: - slc = self._getShapeSlice(env) - if slc is not False: - return slc - cs = self._getCrossSection(env, True) - if cs is not False: - return cs - - # Attempt creating a projection - slc = self._getProjectedFace(nWire) - if slc is False: - PathLog.debug('_getProjectedFace() failed') - else: - return slc - - return False - - def _getExtrudedShape(self, wire): - PathLog.debug('_getExtrudedShape()') - wBB = wire.BoundBox - extFwd = math.floor(2.0 * wBB.ZLength) + 10.0 - - try: - # slower, but renders collective faces correctly. Method 5 in TESTING - shell = wire.extrude(FreeCAD.Vector(0.0, 0.0, extFwd)) - except Exception as ee: - PathLog.error(' -extrude wire failed: \n{}'.format(ee)) - return False - - SHP = Part.makeSolid(shell) - return SHP - - def _getShapeSlice(self, shape): - PathLog.debug('_getShapeSlice()') - - bb = shape.BoundBox - mid = (bb.ZMin + bb.ZMax) / 2.0 - xmin = bb.XMin - 1.0 - xmax = bb.XMax + 1.0 - ymin = bb.YMin - 1.0 - ymax = bb.YMax + 1.0 - p1 = FreeCAD.Vector(xmin, ymin, mid) - p2 = FreeCAD.Vector(xmax, ymin, mid) - p3 = FreeCAD.Vector(xmax, ymax, mid) - p4 = FreeCAD.Vector(xmin, ymax, mid) - - e1 = Part.makeLine(p1, p2) - e2 = Part.makeLine(p2, p3) - e3 = Part.makeLine(p3, p4) - e4 = Part.makeLine(p4, p1) - face = Part.Face(Part.Wire([e1, e2, e3, e4])) - fArea = face.BoundBox.XLength * face.BoundBox.YLength # face.Wires[0].Area - sArea = shape.BoundBox.XLength * shape.BoundBox.YLength - midArea = (fArea + sArea) / 2.0 - - slcShp = shape.common(face) - slcArea = slcShp.BoundBox.XLength * slcShp.BoundBox.YLength - - if slcArea < midArea: - for W in slcShp.Wires: - if W.isClosed() is False: - PathLog.debug(' -wire.isClosed() is False') - return False - if len(slcShp.Wires) == 1: - wire = slcShp.Wires[0] - slc = Part.Face(wire) - slc.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - slc.BoundBox.ZMin)) - return slc - else: - fL = list() - for W in slcShp.Wires: - slc = Part.Face(W) - slc.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - slc.BoundBox.ZMin)) - fL.append(slc) - comp = Part.makeCompound(fL) - if self.showDebugObjects is True: - PathLog.debug('*** tmpSliceCompound') - P = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpSliceCompound') - P.Shape = comp - # P.recompute() - P.purgeTouched() - self.tempGroup.addObject(P) - return comp - - PathLog.debug(' -slcArea !< midArea') - PathLog.debug(' -slcShp.Edges count: {}. Might be a vertically oriented face.'.format(len(slcShp.Edges))) - return False - - def _getProjectedFace(self, wire): - PathLog.debug('_getProjectedFace()') - F = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpProjectionWire') - F.Shape = wire - F.purgeTouched() - self.tempGroup.addObject(F) - try: - prj = Draft.makeShape2DView(F, FreeCAD.Vector(0, 0, 1)) - prj.recompute() - prj.purgeTouched() - self.tempGroup.addObject(prj) - except Exception as ee: - PathLog.error(str(ee)) - return False - else: - pWire = Part.Wire(prj.Shape.Edges) - if pWire.isClosed() is False: - # PathLog.debug(' -pWire.isClosed() is False') - return False - slc = Part.Face(pWire) - slc.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - slc.BoundBox.ZMin)) - return slc - return False - - def _getCrossSection(self, shape, withExtrude=False): - PathLog.debug('_getCrossSection()') - wires = list() - bb = shape.BoundBox - mid = (bb.ZMin + bb.ZMax) / 2.0 - - for i in shape.slice(FreeCAD.Vector(0, 0, 1), mid): - wires.append(i) - - if len(wires) > 0: - comp = Part.Compound(wires) # produces correct cross-section wire ! - comp.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - comp.BoundBox.ZMin)) - csWire = comp.Wires[0] - if csWire.isClosed() is False: - PathLog.debug(' -comp.Wires[0] is not closed') - return False - if withExtrude is True: - ext = self._getExtrudedShape(csWire) - CS = self._getShapeSlice(ext) - else: - CS = Part.Face(csWire) - CS.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - CS.BoundBox.ZMin)) - return CS - else: - PathLog.debug(' -No wires from .slice() method') - - return False - - def _getShapeEnvelope(self, shape): - PathLog.debug('_getShapeEnvelope()') - - wBB = shape.BoundBox - extFwd = wBB.ZLength + 10.0 - minz = wBB.ZMin - maxz = wBB.ZMin + extFwd - stpDwn = (maxz - minz) / 4.0 - dep_par = PathUtils.depth_params(maxz + 5.0, maxz + 3.0, maxz, stpDwn, 0.0, minz) - - try: - env = PathUtils.getEnvelope(partshape=shape, depthparams=dep_par) # Produces .Shape - except Exception as ee: - PathLog.error('try: PathUtils.getEnvelope() failed.\n' + str(ee)) - return False - else: - return env - - return False - - def _getSliceFromEnvelope(self, env): - PathLog.debug('_getSliceFromEnvelope()') - eBB = env.BoundBox - extFwd = eBB.ZLength + 10.0 - maxz = eBB.ZMin + extFwd - - maxMax = env.Edges[0].BoundBox.ZMin - emax = math.floor(maxz - 1.0) - E = list() - for e in range(0, len(env.Edges)): - emin = env.Edges[e].BoundBox.ZMin - if emin > emax: - E.append(env.Edges[e]) - tf = Part.Face(Part.Wire(Part.__sortEdges__(E))) - tf.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - tf.BoundBox.ZMin)) - - return tf - - def _prepareModelSTLs(self, JOB, obj): - PathLog.debug('_prepareModelSTLs()') - for m in range(0, len(JOB.Model.Group)): - M = JOB.Model.Group[m] - - # PathLog.debug(f" -self.modelTypes[{m}] == 'M'") - if self.modelTypes[m] == 'M': - #TODO: test if this works - facets = M.Mesh.Facets.Points - else: - facets = Path.getFacets(M.Shape) - - if self.modelSTLs[m] is True: - stl = ocl.STLSurf() - - for tri in facets: - t = ocl.Triangle(ocl.Point(tri[0][0], tri[0][1], tri[0][2] + obj.DepthOffset.Value), - ocl.Point(tri[1][0], tri[1][1], tri[1][2] + obj.DepthOffset.Value), - ocl.Point(tri[2][0], tri[2][1], tri[2][2] + obj.DepthOffset.Value)) - stl.addTriangle(t) - self.modelSTLs[m] = stl - return - - def _makeSafeSTL(self, JOB, obj, mdlIdx, faceShapes, voidShapes): - '''_makeSafeSTL(JOB, obj, mdlIdx, faceShapes, voidShapes)... - Creates and OCL.stl object with combined data with waste stock, - model, and avoided faces. Travel lines can be checked against this - STL object to determine minimum travel height to clear stock and model.''' - PathLog.debug('_makeSafeSTL()') - - fuseShapes = list() - Mdl = JOB.Model.Group[mdlIdx] - FCAD = FreeCAD.ActiveDocument - mBB = Mdl.Shape.BoundBox - sBB = JOB.Stock.Shape.BoundBox - - # add Model shape to safeSTL shape - fuseShapes.append(Mdl.Shape) - - if obj.BoundBox == 'BaseBoundBox': - cont = False - extFwd = (sBB.ZLength) - zmin = mBB.ZMin - zmax = mBB.ZMin + extFwd - stpDwn = (zmax - zmin) / 4.0 - dep_par = PathUtils.depth_params(zmax + 5.0, zmax + 3.0, zmax, stpDwn, 0.0, zmin) - - try: - envBB = PathUtils.getEnvelope(partshape=Mdl.Shape, depthparams=dep_par) # Produces .Shape - cont = True - except Exception as ee: - PathLog.error(str(ee)) - shell = Mdl.Shape.Shells[0] - solid = Part.makeSolid(shell) - try: - envBB = PathUtils.getEnvelope(partshape=solid, depthparams=dep_par) # Produces .Shape - cont = True - except Exception as eee: - PathLog.error(str(eee)) - - if cont is True: - stckWst = JOB.Stock.Shape.cut(envBB) - if obj.BoundaryAdjustment > 0.0: - cmpndFS = Part.makeCompound(faceShapes) - baBB = PathUtils.getEnvelope(partshape=cmpndFS, depthparams=self.depthParams) # Produces .Shape - adjStckWst = stckWst.cut(baBB) - else: - adjStckWst = stckWst - fuseShapes.append(adjStckWst) - else: - PathLog.warning('Path transitions might not avoid the model. Verify paths.') - # time.sleep(0.3) - - else: - # If boundbox is Job.Stock, add hidden pad under stock as base plate - toolDiam = self.cutter.getDiameter() - zMin = JOB.Stock.Shape.BoundBox.ZMin - xMin = JOB.Stock.Shape.BoundBox.XMin - toolDiam - yMin = JOB.Stock.Shape.BoundBox.YMin - toolDiam - bL = JOB.Stock.Shape.BoundBox.XLength + (2 * toolDiam) - bW = JOB.Stock.Shape.BoundBox.YLength + (2 * toolDiam) - bH = 1.0 - crnr = FreeCAD.Vector(xMin, yMin, zMin - 1.0) - B = Part.makeBox(bL, bW, bH, crnr, FreeCAD.Vector(0, 0, 1)) - fuseShapes.append(B) - - if voidShapes is not False: - voidComp = Part.makeCompound(voidShapes) - voidEnv = PathUtils.getEnvelope(partshape=voidComp, depthparams=self.depthParams) # Produces .Shape - fuseShapes.append(voidEnv) - - fused = Part.makeCompound(fuseShapes) - - if self.showDebugObjects is True: - T = FreeCAD.ActiveDocument.addObject('Part::Feature', 'safeSTLShape') - T.Shape = fused - T.purgeTouched() - self.tempGroup.addObject(T) - - facets = Path.getFacets(fused) - - stl = ocl.STLSurf() - for tri in facets: - t = ocl.Triangle(ocl.Point(tri[0][0], tri[0][1], tri[0][2]), - ocl.Point(tri[1][0], tri[1][1], tri[1][2]), - ocl.Point(tri[2][0], tri[2][1], tri[2][2])) - stl.addTriangle(t) - - self.safeSTLs[mdlIdx] = stl - - def _processCutAreas(self, JOB, obj, mdlIdx, FCS, VDS): - '''_processCutAreas(JOB, obj, mdlIdx, FCS, VDS)... - This method applies any avoided faces or regions to the selected faces. - It then calls the correct method.''' - PathLog.debug('_processCutAreas()') - - final = list() - base = JOB.Model.Group[mdlIdx] - - # Process faces Collectively or Individually - if obj.HandleMultipleFeatures == 'Collectively': - if FCS is True: - COMP = False - else: - ADD = Part.makeCompound(FCS) - if VDS is not False: - DEL = Part.makeCompound(VDS) - COMP = ADD.cut(DEL) - else: - COMP = ADD - - final.append(Path.Command('G0', {'Z': obj.SafeHeight.Value, 'F': self.vertRapid})) - if obj.Algorithm == 'OCL Dropcutter': - final.extend(self._oclWaterlineOp(JOB, obj, mdlIdx, COMP)) # independent method set for Waterline - else: - final.extend(self._experimentalWaterlineOp(JOB, obj, mdlIdx, COMP)) # independent method set for Waterline - - elif obj.HandleMultipleFeatures == 'Individually': - for fsi in range(0, len(FCS)): - fShp = FCS[fsi] - # self.deleteOpVariables(all=False) - self.resetOpVariables(all=False) - - if fShp is True: - COMP = False - else: - ADD = Part.makeCompound([fShp]) - if VDS is not False: - DEL = Part.makeCompound(VDS) - COMP = ADD.cut(DEL) - else: - COMP = ADD - - final.append(Path.Command('G0', {'Z': obj.SafeHeight.Value, 'F': self.vertRapid})) - if obj.Algorithm == 'OCL Dropcutter': - final.extend(self._oclWaterlineOp(JOB, obj, mdlIdx, COMP)) # independent method set for Waterline - else: - final.extend(self._experimentalWaterlineOp(JOB, obj, mdlIdx, COMP)) # independent method set for Waterline - COMP = None - # Eif - - return final - - # Methods for creating path geometry - def _planarMakePathGeom(self, obj, faceShp): - '''_planarMakePathGeom(obj, faceShp)... - Creates the line/arc cut pattern geometry and returns the intersection with the received faceShp. - The resulting intersecting line/arc geometries are then converted to lines or arcs for OCL.''' - PathLog.debug('_planarMakePathGeom()') - GeoSet = list() - - # Apply drop cutter extra offset and set the max and min XY area of the operation - xmin = faceShp.BoundBox.XMin - xmax = faceShp.BoundBox.XMax - ymin = faceShp.BoundBox.YMin - ymax = faceShp.BoundBox.YMax - zmin = faceShp.BoundBox.ZMin - zmax = faceShp.BoundBox.ZMax - - # Compute weighted center of mass of all faces combined - fCnt = 0 - totArea = 0.0 - zeroCOM = FreeCAD.Vector(0.0, 0.0, 0.0) - for F in faceShp.Faces: - comF = F.CenterOfMass - areaF = F.Area - totArea += areaF - fCnt += 1 - zeroCOM = zeroCOM.add(FreeCAD.Vector(comF.x, comF.y, 0.0).multiply(areaF)) - if fCnt == 0: - PathLog.error(translate('PathSurface', 'Cannot calculate the Center Of Mass. Using Center of Boundbox.')) - zeroCOM = FreeCAD.Vector((xmin + xmax) / 2.0, (ymin + ymax) / 2.0, 0.0) - else: - avgArea = totArea / fCnt - zeroCOM.multiply(1 / fCnt) - zeroCOM.multiply(1 / avgArea) - COM = FreeCAD.Vector(zeroCOM.x, zeroCOM.y, 0.0) - - # get X, Y, Z spans; Compute center of rotation - deltaX = abs(xmax-xmin) - deltaY = abs(ymax-ymin) - deltaZ = abs(zmax-zmin) - deltaC = math.sqrt(deltaX**2 + deltaY**2) - lineLen = deltaC + (2.0 * self.cutter.getDiameter()) # Line length to span boundbox diag with 2x cutter diameter extra on each end - halfLL = math.ceil(lineLen / 2.0) - cutPasses = math.ceil(lineLen / self.cutOut) + 1 # Number of lines(passes) required to cover lineLen - halfPasses = math.ceil(cutPasses / 2.0) - bbC = faceShp.BoundBox.Center - - # Generate the Draft line/circle sets to be intersected with the cut-face-area - if obj.CutPattern in ['ZigZag', 'Line']: - MaxLC = -1 - centRot = FreeCAD.Vector(0.0, 0.0, 0.0) # Bottom left corner of face/selection/model - cAng = math.atan(deltaX / deltaY) # BoundaryBox angle - - # Determine end points and create top lines - x1 = centRot.x - halfLL - x2 = centRot.x + halfLL - diag = None - if obj.CutPatternAngle == 0 or obj.CutPatternAngle == 180: - MaxLC = math.floor(deltaY / self.cutOut) - diag = deltaY - elif obj.CutPatternAngle == 90 or obj.CutPatternAngle == 270: - MaxLC = math.floor(deltaX / self.cutOut) - diag = deltaX - else: - perpDist = math.cos(cAng - math.radians(obj.CutPatternAngle)) * deltaC - MaxLC = math.floor(perpDist / self.cutOut) - diag = perpDist - y1 = centRot.y + diag - # y2 = y1 - - p1 = FreeCAD.Vector(x1, y1, 0.0) - p2 = FreeCAD.Vector(x2, y1, 0.0) - topLineTuple = (p1, p2) - ny1 = centRot.y - diag - n1 = FreeCAD.Vector(x1, ny1, 0.0) - n2 = FreeCAD.Vector(x2, ny1, 0.0) - negTopLineTuple = (n1, n2) - - # Create end points for set of lines to intersect with cross-section face - pntTuples = list() - for lc in range((-1 * (halfPasses - 1)), halfPasses + 1): - # if lc == (cutPasses - MaxLC - 1): - # pntTuples.append(negTopLineTuple) - # if lc == (MaxLC + 1): - # pntTuples.append(topLineTuple) - x1 = centRot.x - halfLL - x2 = centRot.x + halfLL - y1 = centRot.y + (lc * self.cutOut) - # y2 = y1 - p1 = FreeCAD.Vector(x1, y1, 0.0) - p2 = FreeCAD.Vector(x2, y1, 0.0) - pntTuples.append( (p1, p2) ) - - # Convert end points to lines - for (p1, p2) in pntTuples: - line = Part.makeLine(p1, p2) - GeoSet.append(line) - elif obj.CutPattern in ['Circular', 'CircularZigZag']: - zTgt = faceShp.BoundBox.ZMin - axisRot = FreeCAD.Vector(0.0, 0.0, 1.0) - cntr = FreeCAD.Placement() - cntr.Rotation = FreeCAD.Rotation(axisRot, 0.0) - - if obj.CircularCenterAt == 'CenterOfMass': - cntr.Base = FreeCAD.Vector(COM.x, COM.y, zTgt) # COM # Use center of Mass - elif obj.CircularCenterAt == 'CenterOfBoundBox': - cent = faceShp.BoundBox.Center - cntr.Base = FreeCAD.Vector(cent.x, cent.y, zTgt) - elif obj.CircularCenterAt == 'XminYmin': - cntr.Base = FreeCAD.Vector(faceShp.BoundBox.XMin, faceShp.BoundBox.YMin, zTgt) - elif obj.CircularCenterAt == 'Custom': - newCent = FreeCAD.Vector(obj.CircularCenterCustom.x, obj.CircularCenterCustom.y, zTgt) - cntr.Base = newCent - - # recalculate cutPasses value, if need be - radialPasses = halfPasses - if obj.CircularCenterAt != 'CenterOfBoundBox': - # make 4 corners of boundbox in XY plane, find which is greatest distance to new circular center - EBB = faceShp.BoundBox - CORNERS = [ - FreeCAD.Vector(EBB.XMin, EBB.YMin, 0.0), - FreeCAD.Vector(EBB.XMin, EBB.YMax, 0.0), - FreeCAD.Vector(EBB.XMax, EBB.YMax, 0.0), - FreeCAD.Vector(EBB.XMax, EBB.YMin, 0.0), - ] - dMax = 0.0 - for c in range(0, 4): - dist = CORNERS[c].sub(cntr.Base).Length - if dist > dMax: - dMax = dist - lineLen = dMax + (2.0 * self.cutter.getDiameter()) # Line length to span boundbox diag with 2x cutter diameter extra on each end - radialPasses = math.ceil(lineLen / self.cutOut) + 1 # Number of lines(passes) required to cover lineLen - - # Update COM point and current CircularCenter - if obj.CircularCenterAt != 'Custom': - obj.CircularCenterCustom = cntr.Base - - minRad = self.cutter.getDiameter() * 0.45 - siX3 = 3 * obj.SampleInterval.Value - minRadSI = (siX3 / 2.0) / math.pi - if minRad < minRadSI: - minRad = minRadSI - - # Make small center circle to start pattern - if obj.StepOver > 50: - circle = Part.makeCircle(minRad, cntr.Base) - GeoSet.append(circle) - - for lc in range(1, radialPasses + 1): - rad = (lc * self.cutOut) - if rad >= minRad: - circle = Part.makeCircle(rad, cntr.Base) - GeoSet.append(circle) - # Efor - COM = cntr.Base - # Eif - - if obj.CutPatternReversed is True: - GeoSet.reverse() - - if faceShp.BoundBox.ZMin != 0.0: - faceShp.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - faceShp.BoundBox.ZMin)) - - # Create compound object to bind all lines in Lineset - geomShape = Part.makeCompound(GeoSet) - - # Position and rotate the Line and ZigZag geometry - if obj.CutPattern in ['Line', 'ZigZag']: - if obj.CutPatternAngle != 0.0: - geomShape.Placement.Rotation = FreeCAD.Rotation(FreeCAD.Vector(0, 0, 1), obj.CutPatternAngle) - geomShape.Placement.Base = FreeCAD.Vector(bbC.x, bbC.y, 0.0 - geomShape.BoundBox.ZMin) - - if self.showDebugObjects is True: - F = FreeCAD.ActiveDocument.addObject('Part::Feature','tmpGeometrySet') - F.Shape = geomShape - F.purgeTouched() - self.tempGroup.addObject(F) - - # Identify intersection of cross-section face and lineset - cmnShape = faceShp.common(geomShape) - - if self.showDebugObjects is True: - F = FreeCAD.ActiveDocument.addObject('Part::Feature','tmpPathGeometry') - F.Shape = cmnShape - F.purgeTouched() - self.tempGroup.addObject(F) - - self.tmpCOM = FreeCAD.Vector(COM.x, COM.y, faceShp.BoundBox.ZMin) - return cmnShape - - def _pathGeomToLinesPointSet(self, obj, compGeoShp): - '''_pathGeomToLinesPointSet(obj, compGeoShp)... - Convert a compound set of sequential line segments to directionally-oriented collinear groupings.''' - PathLog.debug('_pathGeomToLinesPointSet()') - # Extract intersection line segments for return value as list() - LINES = list() - inLine = list() - chkGap = False - lnCnt = 0 - ec = len(compGeoShp.Edges) - cutClimb = self.CutClimb - toolDiam = 2.0 * self.radius - cpa = obj.CutPatternAngle - - edg0 = compGeoShp.Edges[0] - p1 = (edg0.Vertexes[0].X, edg0.Vertexes[0].Y) - p2 = (edg0.Vertexes[1].X, edg0.Vertexes[1].Y) - if cutClimb is True: - tup = (p2, p1) - lst = FreeCAD.Vector(p1[0], p1[1], 0.0) - else: - tup = (p1, p2) - lst = FreeCAD.Vector(p2[0], p2[1], 0.0) - inLine.append(tup) - sp = FreeCAD.Vector(p1[0], p1[1], 0.0) # start point - - for ei in range(1, ec): - chkGap = False - edg = compGeoShp.Edges[ei] # Get edge for vertexes - v1 = (edg.Vertexes[0].X, edg.Vertexes[0].Y) # vertex 0 - v2 = (edg.Vertexes[1].X, edg.Vertexes[1].Y) # vertex 1 - - ep = FreeCAD.Vector(v2[0], v2[1], 0.0) # end point - cp = FreeCAD.Vector(v1[0], v1[1], 0.0) # check point (first / middle point) - iC = sp.isOnLineSegment(ep, cp) - if iC is True: - inLine.append('BRK') - chkGap = True - else: - if cutClimb is True: - inLine.reverse() - LINES.append(inLine) # Save inLine segments - lnCnt += 1 - inLine = list() # reset collinear container - if cutClimb is True: - sp = cp # FreeCAD.Vector(v1[0], v1[1], 0.0) - else: - sp = ep - - if cutClimb is True: - tup = (v2, v1) - if chkGap is True: - gap = abs(toolDiam - lst.sub(ep).Length) - lst = cp - else: - tup = (v1, v2) - if chkGap is True: - gap = abs(toolDiam - lst.sub(cp).Length) - lst = ep - - if chkGap is True: - if gap < obj.GapThreshold.Value: - b = inLine.pop() # pop off 'BRK' marker - (vA, vB) = inLine.pop() # pop off previous line segment for combining with current - tup = (vA, tup[1]) - self.closedGap = True - else: - # PathLog.debug('---- Gap: {} mm'.format(gap)) - gap = round(gap, 6) - if gap < self.gaps[0]: - self.gaps.insert(0, gap) - self.gaps.pop() - inLine.append(tup) - # Efor - lnCnt += 1 - if cutClimb is True: - inLine.reverse() - LINES.append(inLine) # Save inLine segments - - # Handle last inLine set, reversing it. - if obj.CutPatternReversed is True: - if cpa != 0.0 and cpa % 90.0 == 0.0: - F = LINES.pop(0) - rev = list() - for iL in F: - if iL == 'BRK': - rev.append(iL) - else: - (p1, p2) = iL - rev.append((p2, p1)) - rev.reverse() - LINES.insert(0, rev) - - isEven = lnCnt % 2 - if isEven == 0: - PathLog.debug('Line count is ODD.') - else: - PathLog.debug('Line count is even.') - - return LINES - - def _pathGeomToZigzagPointSet(self, obj, compGeoShp): - '''_pathGeomToZigzagPointSet(obj, compGeoShp)... - Convert a compound set of sequential line segments to directionally-oriented collinear groupings - with a ZigZag directional indicator included for each collinear group.''' - PathLog.debug('_pathGeomToZigzagPointSet()') - # Extract intersection line segments for return value as list() - LINES = list() - inLine = list() - lnCnt = 0 - chkGap = False - ec = len(compGeoShp.Edges) - toolDiam = 2.0 * self.radius - - if self.CutClimb is True: - dirFlg = -1 - else: - dirFlg = 1 - - edg0 = compGeoShp.Edges[0] - p1 = (edg0.Vertexes[0].X, edg0.Vertexes[0].Y) - p2 = (edg0.Vertexes[1].X, edg0.Vertexes[1].Y) - if dirFlg == 1: - tup = (p1, p2) - lst = FreeCAD.Vector(p2[0], p2[1], 0.0) - sp = FreeCAD.Vector(p1[0], p1[1], 0.0) # start point - else: - tup = (p2, p1) - lst = FreeCAD.Vector(p1[0], p1[1], 0.0) - sp = FreeCAD.Vector(p2[0], p2[1], 0.0) # start point - inLine.append(tup) - otr = lst - - for ei in range(1, ec): - edg = compGeoShp.Edges[ei] - v1 = (edg.Vertexes[0].X, edg.Vertexes[0].Y) - v2 = (edg.Vertexes[1].X, edg.Vertexes[1].Y) - - cp = FreeCAD.Vector(v1[0], v1[1], 0.0) # check point (start point of segment) - ep = FreeCAD.Vector(v2[0], v2[1], 0.0) # end point - iC = sp.isOnLineSegment(ep, cp) - if iC is True: - inLine.append('BRK') - chkGap = True - gap = abs(toolDiam - lst.sub(cp).Length) - else: - chkGap = False - if dirFlg == -1: - inLine.reverse() - LINES.append((dirFlg, inLine)) - lnCnt += 1 - dirFlg = -1 * dirFlg # Change zig to zag - inLine = list() # reset collinear container - sp = cp # FreeCAD.Vector(v1[0], v1[1], 0.0) - otr = ep - - lst = ep - if dirFlg == 1: - tup = (v1, v2) - else: - tup = (v2, v1) - - if chkGap is True: - if gap < obj.GapThreshold.Value: - b = inLine.pop() # pop off 'BRK' marker - (vA, vB) = inLine.pop() # pop off previous line segment for combining with current - if dirFlg == 1: - tup = (vA, tup[1]) - else: - #tup = (vA, tup[1]) - #tup = (tup[1], vA) - tup = (tup[0], vB) - self.closedGap = True - else: - gap = round(gap, 6) - if gap < self.gaps[0]: - self.gaps.insert(0, gap) - self.gaps.pop() - inLine.append(tup) - # Efor - lnCnt += 1 - - # Fix directional issue with LAST line when line count is even - isEven = lnCnt % 2 - if isEven == 0: # Changed to != with 90 degree CutPatternAngle - PathLog.debug('Line count is even.') - else: - PathLog.debug('Line count is ODD.') - dirFlg = -1 * dirFlg - if obj.CutPatternReversed is False: - if self.CutClimb is True: - dirFlg = -1 * dirFlg - - if obj.CutPatternReversed is True: - dirFlg = -1 * dirFlg - - # Handle last inLine list - if dirFlg == 1: - rev = list() - for iL in inLine: - if iL == 'BRK': - rev.append(iL) - else: - (p1, p2) = iL - rev.append((p2, p1)) - - if obj.CutPatternReversed is False: - rev.reverse() - else: - rev2 = list() - for iL in rev: - if iL == 'BRK': - rev2.append(iL) - else: - (p1, p2) = iL - rev2.append((p2, p1)) - rev2.reverse() - rev = rev2 - - LINES.append((dirFlg, rev)) - else: - LINES.append((dirFlg, inLine)) - - return LINES - - def _pathGeomToArcPointSet(self, obj, compGeoShp): - '''_pathGeomToArcPointSet(obj, compGeoShp)... - Convert a compound set of arcs/circles to a set of directionally-oriented arc end points - and the corresponding center point.''' - # Extract intersection line segments for return value as list() - PathLog.debug('_pathGeomToArcPointSet()') - ARCS = list() - stpOvrEI = list() - segEI = list() - isSame = False - sameRad = None - COM = self.tmpCOM - toolDiam = 2.0 * self.radius - ec = len(compGeoShp.Edges) - - def gapDist(sp, ep): - X = (ep[0] - sp[0])**2 - Y = (ep[1] - sp[1])**2 - Z = (ep[2] - sp[2])**2 - # return math.sqrt(X + Y + Z) - return math.sqrt(X + Y) # the 'z' value is zero in both points - - # Separate arc data into Loops and Arcs - for ei in range(0, ec): - edg = compGeoShp.Edges[ei] - if edg.Closed is True: - stpOvrEI.append(('L', ei, False)) - else: - if isSame is False: - segEI.append(ei) - isSame = True - pnt = FreeCAD.Vector(edg.Vertexes[0].X, edg.Vertexes[0].Y, 0.0) - sameRad = pnt.sub(COM).Length - else: - # Check if arc is co-radial to current SEGS - pnt = FreeCAD.Vector(edg.Vertexes[0].X, edg.Vertexes[0].Y, 0.0) - if abs(sameRad - pnt.sub(COM).Length) > 0.00001: - isSame = False - - if isSame is True: - segEI.append(ei) - else: - # Move co-radial arc segments - stpOvrEI.append(['A', segEI, False]) - # Start new list of arc segments - segEI = [ei] - isSame = True - pnt = FreeCAD.Vector(edg.Vertexes[0].X, edg.Vertexes[0].Y, 0.0) - sameRad = pnt.sub(COM).Length - # Process trailing `segEI` data, if available - if isSame is True: - stpOvrEI.append(['A', segEI, False]) - - # Identify adjacent arcs with y=0 start/end points that connect - for so in range(0, len(stpOvrEI)): - SO = stpOvrEI[so] - if SO[0] == 'A': - startOnAxis = list() - endOnAxis = list() - EI = SO[1] # list of corresponding compGeoShp.Edges indexes - - # Identify startOnAxis and endOnAxis arcs - for i in range(0, len(EI)): - ei = EI[i] # edge index - E = compGeoShp.Edges[ei] # edge object - if abs(COM.y - E.Vertexes[0].Y) < 0.00001: - startOnAxis.append((i, ei, E.Vertexes[0])) - elif abs(COM.y - E.Vertexes[1].Y) < 0.00001: - endOnAxis.append((i, ei, E.Vertexes[1])) - - # Look for connections between startOnAxis and endOnAxis arcs. Consolidate data when connected - lenSOA = len(startOnAxis) - lenEOA = len(endOnAxis) - if lenSOA > 0 and lenEOA > 0: - delIdxs = list() - lstFindIdx = 0 - for soa in range(0, lenSOA): - (iS, eiS, vS) = startOnAxis[soa] - for eoa in range(0, len(endOnAxis)): - (iE, eiE, vE) = endOnAxis[eoa] - dist = vE.X - vS.X - if abs(dist) < 0.00001: # They connect on axis at same radius - SO[2] = (eiE, eiS) - break - elif dist > 0: - break # stop searching - # Eif - # Eif - # Efor - - # Construct arc data tuples for OCL - dirFlg = 1 - # cutPat = obj.CutPattern - if self.CutClimb is False: # True yields Climb when set to Conventional - dirFlg = -1 - - # Cycle through stepOver data - for so in range(0, len(stpOvrEI)): - SO = stpOvrEI[so] - if SO[0] == 'L': # L = Loop/Ring/Circle - # PathLog.debug("SO[0] == 'Loop'") - lei = SO[1] # loop Edges index - v1 = compGeoShp.Edges[lei].Vertexes[0] - - # space = obj.SampleInterval.Value / 2.0 - space = 0.0000001 - - # p1 = FreeCAD.Vector(v1.X, v1.Y, v1.Z) - p1 = FreeCAD.Vector(v1.X, v1.Y, 0.0) - rad = p1.sub(COM).Length - spcRadRatio = space/rad - if spcRadRatio < 1.0: - tolrncAng = math.asin(spcRadRatio) - else: - tolrncAng = 0.9999998 * math.pi - EX = COM.x + (rad * math.cos(tolrncAng)) - EY = v1.Y - space # rad * math.sin(tolrncAng) - - sp = (v1.X, v1.Y, 0.0) - ep = (EX, EY, 0.0) - cp = (COM.x, COM.y, 0.0) - if dirFlg == 1: - arc = (sp, ep, cp) - else: - arc = (ep, sp, cp) # OCL.Arc(firstPnt, lastPnt, centerPnt, dir=True(CCW direction)) - ARCS.append(('L', dirFlg, [arc])) - else: # SO[0] == 'A' A = Arc - # PathLog.debug("SO[0] == 'Arc'") - PRTS = list() - EI = SO[1] # list of corresponding Edges indexes - CONN = SO[2] # list of corresponding connected edges tuples (iE, iS) - chkGap = False - lst = None - - if CONN is not False: - (iE, iS) = CONN - v1 = compGeoShp.Edges[iE].Vertexes[0] - v2 = compGeoShp.Edges[iS].Vertexes[1] - sp = (v1.X, v1.Y, 0.0) - ep = (v2.X, v2.Y, 0.0) - cp = (COM.x, COM.y, 0.0) - if dirFlg == 1: - arc = (sp, ep, cp) - lst = ep - else: - arc = (ep, sp, cp) # OCL.Arc(firstPnt, lastPnt, centerPnt, dir=True(CCW direction)) - lst = sp - PRTS.append(arc) - # Pop connected edge index values from arc segments index list - iEi = EI.index(iE) - iSi = EI.index(iS) - if iEi > iSi: - EI.pop(iEi) - EI.pop(iSi) - else: - EI.pop(iSi) - EI.pop(iEi) - if len(EI) > 0: - PRTS.append('BRK') - chkGap = True - cnt = 0 - for ei in EI: - if cnt > 0: - PRTS.append('BRK') - chkGap = True - v1 = compGeoShp.Edges[ei].Vertexes[0] - v2 = compGeoShp.Edges[ei].Vertexes[1] - sp = (v1.X, v1.Y, 0.0) - ep = (v2.X, v2.Y, 0.0) - cp = (COM.x, COM.y, 0.0) - if dirFlg == 1: - arc = (sp, ep, cp) - if chkGap is True: - gap = abs(toolDiam - gapDist(lst, sp)) # abs(toolDiam - lst.sub(sp).Length) - lst = ep - else: - arc = (ep, sp, cp) # OCL.Arc(firstPnt, lastPnt, centerPnt, dir=True(CCW direction)) - if chkGap is True: - gap = abs(toolDiam - gapDist(lst, ep)) # abs(toolDiam - lst.sub(ep).Length) - lst = sp - if chkGap is True: - if gap < obj.GapThreshold.Value: - b = PRTS.pop() # pop off 'BRK' marker - (vA, vB, vC) = PRTS.pop() # pop off previous arc segment for combining with current - arc = (vA, arc[1], vC) - self.closedGap = True - else: - # PathLog.debug('---- Gap: {} mm'.format(gap)) - gap = round(gap, 6) - if gap < self.gaps[0]: - self.gaps.insert(0, gap) - self.gaps.pop() - PRTS.append(arc) - cnt += 1 - - if dirFlg == -1: - PRTS.reverse() - - ARCS.append(('A', dirFlg, PRTS)) - # Eif - if obj.CutPattern == 'CircularZigZag': - dirFlg = -1 * dirFlg - # Efor - - return ARCS - - def _getExperimentalWaterlinePaths(self, obj, PNTSET, csHght): - '''_getExperimentalWaterlinePaths(obj, PNTSET, csHght)... - Switching function for calling the appropriate path-geometry to OCL points conversion function - for the various cut patterns.''' - PathLog.debug('_getExperimentalWaterlinePaths()') - SCANS = list() - - if obj.CutPattern == 'Line': - stpOvr = list() - for D in PNTSET: - for SEG in D: - if SEG == 'BRK': - stpOvr.append(SEG) - else: - # D format is ((p1, p2), (p3, p4)) - (A, B) = SEG - P1 = FreeCAD.Vector(A[0], A[1], csHght) - P2 = FreeCAD.Vector(B[0], B[1], csHght) - stpOvr.append((P1, P2)) - SCANS.append(stpOvr) - stpOvr = list() - elif obj.CutPattern == 'ZigZag': - stpOvr = list() - for (dirFlg, LNS) in PNTSET: - for SEG in LNS: - if SEG == 'BRK': - stpOvr.append(SEG) - else: - # D format is ((p1, p2), (p3, p4)) - (A, B) = SEG - P1 = FreeCAD.Vector(A[0], A[1], csHght) - P2 = FreeCAD.Vector(B[0], B[1], csHght) - stpOvr.append((P1, P2)) - SCANS.append(stpOvr) - stpOvr = list() - elif obj.CutPattern in ['Circular', 'CircularZigZag']: - # PNTSET is list, by stepover. - # Each stepover is a list containing arc/loop descriptions, (sp, ep, cp) - for so in range(0, len(PNTSET)): - stpOvr = list() - erFlg = False - (aTyp, dirFlg, ARCS) = PNTSET[so] - - if dirFlg == 1: # 1 - cMode = True # Climb mode - else: - cMode = False - - for a in range(0, len(ARCS)): - Arc = ARCS[a] - if Arc == 'BRK': - stpOvr.append('BRK') - else: - (sp, ep, cp) = Arc - S = FreeCAD.Vector(sp[0], sp[1], csHght) - E = FreeCAD.Vector(ep[0], ep[1], csHght) - C = FreeCAD.Vector(cp[0], cp[1], csHght) - scan = (S, E, C, cMode) - if scan is False: - erFlg = True - else: - ##if aTyp == 'L': - ## stpOvr.append(FreeCAD.Vector(scan[0][0].x, scan[0][0].y, scan[0][0].z)) - stpOvr.append(scan) - if erFlg is False: - SCANS.append(stpOvr) - - return SCANS - - # Main planar scan functions - def _stepTransitionCmds(self, obj, lstPnt, first, minSTH, tolrnc): - cmds = list() - rtpd = False - horizGC = 'G0' - hSpeed = self.horizRapid - height = obj.SafeHeight.Value - - if obj.CutPattern in ['Line', 'Circular']: - if obj.OptimizeStepOverTransitions is True: - height = minSTH + 2.0 - # if obj.LayerMode == 'Multi-pass': - # rtpd = minSTH - elif obj.CutPattern in ['ZigZag', 'CircularZigZag']: - if obj.OptimizeStepOverTransitions is True: - zChng = first.z - lstPnt.z - # PathLog.debug('first.z: {}'.format(first.z)) - # PathLog.debug('lstPnt.z: {}'.format(lstPnt.z)) - # PathLog.debug('zChng: {}'.format(zChng)) - # PathLog.debug('minSTH: {}'.format(minSTH)) - if abs(zChng) < tolrnc: # transitions to same Z height - # PathLog.debug('abs(zChng) < tolrnc') - if (minSTH - first.z) > tolrnc: - # PathLog.debug('(minSTH - first.z) > tolrnc') - height = minSTH + 2.0 - else: - # PathLog.debug('ELSE (minSTH - first.z) > tolrnc') - horizGC = 'G1' - height = first.z - elif (minSTH + (2.0 * tolrnc)) >= max(first.z, lstPnt.z): - height = False # allow end of Zig to cut to beginning of Zag - - - # Create raise, shift, and optional lower commands - if height is not False: - cmds.append(Path.Command('G0', {'Z': height, 'F': self.vertRapid})) - cmds.append(Path.Command(horizGC, {'X': first.x, 'Y': first.y, 'F': hSpeed})) - if rtpd is not False: # ReturnToPreviousDepth - cmds.append(Path.Command('G0', {'Z': rtpd, 'F': self.vertRapid})) - - return cmds - - def _breakCmds(self, obj, lstPnt, first, minSTH, tolrnc): - cmds = list() - rtpd = False - horizGC = 'G0' - hSpeed = self.horizRapid - height = obj.SafeHeight.Value - - if obj.CutPattern in ['Line', 'Circular']: - if obj.OptimizeStepOverTransitions is True: - height = minSTH + 2.0 - elif obj.CutPattern in ['ZigZag', 'CircularZigZag']: - if obj.OptimizeStepOverTransitions is True: - zChng = first.z - lstPnt.z - if abs(zChng) < tolrnc: # transitions to same Z height - if (minSTH - first.z) > tolrnc: - height = minSTH + 2.0 - else: - height = first.z + 2.0 # first.z - - cmds.append(Path.Command('G0', {'Z': height, 'F': self.vertRapid})) - cmds.append(Path.Command(horizGC, {'X': first.x, 'Y': first.y, 'F': hSpeed})) - if rtpd is not False: # ReturnToPreviousDepth - cmds.append(Path.Command('G0', {'Z': rtpd, 'F': self.vertRapid})) - - return cmds - - def _planarGetPDC(self, stl, finalDep, SampleInterval, useSafeCutter=False): - pdc = ocl.PathDropCutter() # create a pdc [PathDropCutter] object - pdc.setSTL(stl) # add stl model - if useSafeCutter is True: - pdc.setCutter(self.safeCutter) # add safeCutter - else: - pdc.setCutter(self.cutter) # add cutter - pdc.setZ(finalDep) # set minimumZ (final / target depth value) - pdc.setSampling(SampleInterval) # set sampling size - return pdc - - # Main waterline functions - def _oclWaterlineOp(self, JOB, obj, mdlIdx, subShp=None): - '''_oclWaterlineOp(obj, base) ... Main waterline function to perform waterline extraction from model.''' - commands = [] - - t_begin = time.time() - # JOB = PathUtils.findParentJob(obj) - base = JOB.Model.Group[mdlIdx] - bb = self.boundBoxes[mdlIdx] - stl = self.modelSTLs[mdlIdx] - - # Prepare global holdpoint and layerEndPnt containers - if self.holdPoint is None: - self.holdPoint = ocl.Point(float("inf"), float("inf"), float("inf")) - if self.layerEndPnt is None: - self.layerEndPnt = ocl.Point(float("inf"), float("inf"), float("inf")) - - # Set extra offset to diameter of cutter to allow cutter to move around perimeter of model - toolDiam = self.cutter.getDiameter() - cdeoX = 0.6 * toolDiam - cdeoY = 0.6 * toolDiam - - if subShp is None: - # Get correct boundbox - if obj.BoundBox == 'Stock': - BS = JOB.Stock - bb = BS.Shape.BoundBox - elif obj.BoundBox == 'BaseBoundBox': - BS = base - bb = base.Shape.BoundBox - - env = PathUtils.getEnvelope(partshape=BS.Shape, depthparams=self.depthParams) # Produces .Shape - - xmin = bb.XMin - xmax = bb.XMax - ymin = bb.YMin - ymax = bb.YMax - zmin = bb.ZMin - zmax = bb.ZMax - else: - xmin = subShp.BoundBox.XMin - xmax = subShp.BoundBox.XMax - ymin = subShp.BoundBox.YMin - ymax = subShp.BoundBox.YMax - zmin = subShp.BoundBox.ZMin - zmax = subShp.BoundBox.ZMax - - smplInt = obj.SampleInterval.Value - minSampInt = 0.001 # value is mm - if smplInt < minSampInt: - smplInt = minSampInt - - # Determine bounding box length for the OCL scan - bbLength = math.fabs(ymax - ymin) - numScanLines = int(math.ceil(bbLength / smplInt) + 1) # Number of lines - - # Compute number and size of stepdowns, and final depth - if obj.LayerMode == 'Single-pass': - depthparams = [obj.FinalDepth.Value] - else: - depthparams = [dp for dp in self.depthParams] - lenDP = len(depthparams) - - # Prepare PathDropCutter objects with STL data - safePDC = self._planarGetPDC(self.safeSTLs[mdlIdx], - depthparams[lenDP - 1], obj.SampleInterval.Value, useSafeCutter=False) - - # Scan the piece to depth at smplInt - oclScan = [] - oclScan = self._waterlineDropCutScan(stl, smplInt, xmin, xmax, ymin, depthparams[lenDP - 1], numScanLines) - # oclScan = SCANS - lenOS = len(oclScan) - ptPrLn = int(lenOS / numScanLines) - - # Convert oclScan list of points to multi-dimensional list - scanLines = [] - for L in range(0, numScanLines): - scanLines.append([]) - for P in range(0, ptPrLn): - pi = L * ptPrLn + P - scanLines[L].append(oclScan[pi]) - lenSL = len(scanLines) - pntsPerLine = len(scanLines[0]) - PathLog.debug("--OCL scan: " + str(lenSL * pntsPerLine) + " points, with " + str(numScanLines) + " lines and " + str(pntsPerLine) + " pts/line") - - # Extract Wl layers per depthparams - lyr = 0 - cmds = [] - layTime = time.time() - self.topoMap = [] - for layDep in depthparams: - cmds = self._getWaterline(obj, scanLines, layDep, lyr, lenSL, pntsPerLine) - commands.extend(cmds) - lyr += 1 - PathLog.debug("--All layer scans combined took " + str(time.time() - layTime) + " s") - return commands - - def _waterlineDropCutScan(self, stl, smplInt, xmin, xmax, ymin, fd, numScanLines): - '''_waterlineDropCutScan(stl, smplInt, xmin, xmax, ymin, fd, numScanLines) ... - Perform OCL scan for waterline purpose.''' - pdc = ocl.PathDropCutter() # create a pdc - pdc.setSTL(stl) - pdc.setCutter(self.cutter) - pdc.setZ(fd) # set minimumZ (final / target depth value) - pdc.setSampling(smplInt) - - # Create line object as path - path = ocl.Path() # create an empty path object - for nSL in range(0, numScanLines): - yVal = ymin + (nSL * smplInt) - p1 = ocl.Point(xmin, yVal, fd) # start-point of line - p2 = ocl.Point(xmax, yVal, fd) # end-point of line - path.append(ocl.Line(p1, p2)) - # path.append(l) # add the line to the path - pdc.setPath(path) - pdc.run() # run drop-cutter on the path - - # return the list the points - return pdc.getCLPoints() - - def _getWaterline(self, obj, scanLines, layDep, lyr, lenSL, pntsPerLine): - '''_getWaterline(obj, scanLines, layDep, lyr, lenSL, pntsPerLine) ... Get waterline.''' - commands = [] - cmds = [] - loopList = [] - self.topoMap = [] - # Create topo map from scanLines (highs and lows) - self.topoMap = self._createTopoMap(scanLines, layDep, lenSL, pntsPerLine) - # Add buffer lines and columns to topo map - self._bufferTopoMap(lenSL, pntsPerLine) - # Identify layer waterline from OCL scan - self._highlightWaterline(4, 9) - # Extract waterline and convert to gcode - loopList = self._extractWaterlines(obj, scanLines, lyr, layDep) - # save commands - for loop in loopList: - cmds = self._loopToGcode(obj, layDep, loop) - commands.extend(cmds) - return commands - - def _createTopoMap(self, scanLines, layDep, lenSL, pntsPerLine): - '''_createTopoMap(scanLines, layDep, lenSL, pntsPerLine) ... Create topo map version of OCL scan data.''' - topoMap = [] - for L in range(0, lenSL): - topoMap.append([]) - for P in range(0, pntsPerLine): - if scanLines[L][P].z > layDep: - topoMap[L].append(2) - else: - topoMap[L].append(0) - return topoMap - - def _bufferTopoMap(self, lenSL, pntsPerLine): - '''_bufferTopoMap(lenSL, pntsPerLine) ... Add buffer boarder of zeros to all sides to topoMap data.''' - pre = [0, 0] - post = [0, 0] - for p in range(0, pntsPerLine): - pre.append(0) - post.append(0) - for l in range(0, lenSL): - self.topoMap[l].insert(0, 0) - self.topoMap[l].append(0) - self.topoMap.insert(0, pre) - self.topoMap.append(post) - return True - - def _highlightWaterline(self, extraMaterial, insCorn): - '''_highlightWaterline(extraMaterial, insCorn) ... Highlight the waterline data, separating from extra material.''' - TM = self.topoMap - lastPnt = len(TM[1]) - 1 - lastLn = len(TM) - 1 - highFlag = 0 - - # ("--Convert parallel data to ridges") - for lin in range(1, lastLn): - for pt in range(1, lastPnt): # Ignore first and last points - if TM[lin][pt] == 0: - if TM[lin][pt + 1] == 2: # step up - TM[lin][pt] = 1 - if TM[lin][pt - 1] == 2: # step down - TM[lin][pt] = 1 - - # ("--Convert perpendicular data to ridges and highlight ridges") - for pt in range(1, lastPnt): # Ignore first and last points - for lin in range(1, lastLn): - if TM[lin][pt] == 0: - highFlag = 0 - if TM[lin + 1][pt] == 2: # step up - TM[lin][pt] = 1 - if TM[lin - 1][pt] == 2: # step down - TM[lin][pt] = 1 - elif TM[lin][pt] == 2: - highFlag += 1 - if highFlag == 3: - if TM[lin - 1][pt - 1] < 2 or TM[lin - 1][pt + 1] < 2: - highFlag = 2 - else: - TM[lin - 1][pt] = extraMaterial - highFlag = 2 - - # ("--Square corners") - for pt in range(1, lastPnt): - for lin in range(1, lastLn): - if TM[lin][pt] == 1: # point == 1 - cont = True - if TM[lin + 1][pt] == 0: # forward == 0 - if TM[lin + 1][pt - 1] == 1: # forward left == 1 - if TM[lin][pt - 1] == 2: # left == 2 - TM[lin + 1][pt] = 1 # square the corner - cont = False - - if cont is True and TM[lin + 1][pt + 1] == 1: # forward right == 1 - if TM[lin][pt + 1] == 2: # right == 2 - TM[lin + 1][pt] = 1 # square the corner - cont = True - - if TM[lin - 1][pt] == 0: # back == 0 - if TM[lin - 1][pt - 1] == 1: # back left == 1 - if TM[lin][pt - 1] == 2: # left == 2 - TM[lin - 1][pt] = 1 # square the corner - cont = False - - if cont is True and TM[lin - 1][pt + 1] == 1: # back right == 1 - if TM[lin][pt + 1] == 2: # right == 2 - TM[lin - 1][pt] = 1 # square the corner - - # remove inside corners - for pt in range(1, lastPnt): - for lin in range(1, lastLn): - if TM[lin][pt] == 1: # point == 1 - if TM[lin][pt + 1] == 1: - if TM[lin - 1][pt + 1] == 1 or TM[lin + 1][pt + 1] == 1: - TM[lin][pt + 1] = insCorn - elif TM[lin][pt - 1] == 1: - if TM[lin - 1][pt - 1] == 1 or TM[lin + 1][pt - 1] == 1: - TM[lin][pt - 1] = insCorn - - return True - - def _extractWaterlines(self, obj, oclScan, lyr, layDep): - '''_extractWaterlines(obj, oclScan, lyr, layDep) ... Extract water lines from OCL scan data.''' - srch = True - lastPnt = len(self.topoMap[0]) - 1 - lastLn = len(self.topoMap) - 1 - maxSrchs = 5 - srchCnt = 1 - loopList = [] - loop = [] - loopNum = 0 - - if self.CutClimb is True: - lC = [-1, -1, -1, 0, 1, 1, 1, 0, -1, -1, -1, 0, 1, 1, 1, 0, -1, -1, -1, 0, 1, 1, 1, 0] - pC = [-1, 0, 1, 1, 1, 0, -1, -1, -1, 0, 1, 1, 1, 0, -1, -1, -1, 0, 1, 1, 1, 0, -1, -1] - else: - lC = [1, 1, 1, 0, -1, -1, -1, 0, 1, 1, 1, 0, -1, -1, -1, 0, 1, 1, 1, 0, -1, -1, -1, 0] - pC = [-1, 0, 1, 1, 1, 0, -1, -1, -1, 0, 1, 1, 1, 0, -1, -1, -1, 0, 1, 1, 1, 0, -1, -1] - - while srch is True: - srch = False - if srchCnt > maxSrchs: - PathLog.debug("Max search scans, " + str(maxSrchs) + " reached\nPossible incomplete waterline result!") - break - for L in range(1, lastLn): - for P in range(1, lastPnt): - if self.topoMap[L][P] == 1: - # start loop follow - srch = True - loopNum += 1 - loop = self._trackLoop(oclScan, lC, pC, L, P, loopNum) - self.topoMap[L][P] = 0 # Mute the starting point - loopList.append(loop) - srchCnt += 1 - PathLog.debug("Search count for layer " + str(lyr) + " is " + str(srchCnt) + ", with " + str(loopNum) + " loops.") - return loopList - - def _trackLoop(self, oclScan, lC, pC, L, P, loopNum): - '''_trackLoop(oclScan, lC, pC, L, P, loopNum) ... Track the loop direction.''' - loop = [oclScan[L - 1][P - 1]] # Start loop point list - cur = [L, P, 1] - prv = [L, P - 1, 1] - nxt = [L, P + 1, 1] - follow = True - ptc = 0 - ptLmt = 200000 - while follow is True: - ptc += 1 - if ptc > ptLmt: - PathLog.debug("Loop number " + str(loopNum) + " at [" + str(nxt[0]) + ", " + str(nxt[1]) + "] pnt count exceeds, " + str(ptLmt) + ". Stopped following loop.") - break - nxt = self._findNextWlPoint(lC, pC, cur[0], cur[1], prv[0], prv[1]) # get next point - loop.append(oclScan[nxt[0] - 1][nxt[1] - 1]) # add it to loop point list - self.topoMap[nxt[0]][nxt[1]] = nxt[2] # Mute the point, if not Y stem - if nxt[0] == L and nxt[1] == P: # check if loop complete - follow = False - elif nxt[0] == cur[0] and nxt[1] == cur[1]: # check if line cannot be detected - follow = False - prv = cur - cur = nxt - return loop - - def _findNextWlPoint(self, lC, pC, cl, cp, pl, pp): - '''_findNextWlPoint(lC, pC, cl, cp, pl, pp) ... - Find the next waterline point in the point cloud layer provided.''' - dl = cl - pl - dp = cp - pp - num = 0 - i = 3 - s = 0 - mtch = 0 - found = False - while mtch < 8: # check all 8 points around current point - if lC[i] == dl: - if pC[i] == dp: - s = i - 3 - found = True - # Check for y branch where current point is connection between branches - for y in range(1, mtch): - if lC[i + y] == dl: - if pC[i + y] == dp: - num = 1 - break - break - i += 1 - mtch += 1 - if found is False: - # ("_findNext: No start point found.") - return [cl, cp, num] - - for r in range(0, 8): - l = cl + lC[s + r] - p = cp + pC[s + r] - if self.topoMap[l][p] == 1: - return [l, p, num] - - # ("_findNext: No next pnt found") - return [cl, cp, num] - - def _loopToGcode(self, obj, layDep, loop): - '''_loopToGcode(obj, layDep, loop) ... Convert set of loop points to Gcode.''' - # generate the path commands - output = [] - optimize = obj.OptimizeLinearPaths - - prev = ocl.Point(float("inf"), float("inf"), float("inf")) - nxt = ocl.Point(float("inf"), float("inf"), float("inf")) - pnt = ocl.Point(float("inf"), float("inf"), float("inf")) - - # Create first point - pnt.x = loop[0].x - pnt.y = loop[0].y - pnt.z = layDep - - # Position cutter to begin loop - output.append(Path.Command('G0', {'Z': obj.ClearanceHeight.Value, 'F': self.vertRapid})) - output.append(Path.Command('G0', {'X': pnt.x, 'Y': pnt.y, 'F': self.horizRapid})) - output.append(Path.Command('G1', {'Z': pnt.z, 'F': self.vertFeed})) - - lenCLP = len(loop) - lastIdx = lenCLP - 1 - # Cycle through each point on loop - for i in range(0, lenCLP): - if i < lastIdx: - nxt.x = loop[i + 1].x - nxt.y = loop[i + 1].y - nxt.z = layDep - else: - optimize = False - - if not optimize or not FreeCAD.Vector(prev.x, prev.y, prev.z).isOnLineSegment(FreeCAD.Vector(nxt.x, nxt.y, nxt.z), FreeCAD.Vector(pnt.x, pnt.y, pnt.z)): - output.append(Path.Command('G1', {'X': pnt.x, 'Y': pnt.y, 'F': self.horizFeed})) - - # Rotate point data - prev.x = pnt.x - prev.y = pnt.y - prev.z = pnt.z - pnt.x = nxt.x - pnt.y = nxt.y - pnt.z = nxt.z - - # Save layer end point for use in transitioning to next layer - self.layerEndPnt.x = pnt.x - self.layerEndPnt.y = pnt.y - self.layerEndPnt.z = pnt.z - - return output - - # Main waterline functions - def _experimentalWaterlineOp(self, JOB, obj, mdlIdx, subShp=None): - '''_waterlineOp(JOB, obj, mdlIdx, subShp=None) ... - Main waterline function to perform waterline extraction from model.''' - PathLog.debug('_experimentalWaterlineOp()') - - msg = translate('PathWaterline', 'Experimental Waterline does not currently support selected faces.') - PathLog.info('\n..... ' + msg) - - commands = [] - t_begin = time.time() - base = JOB.Model.Group[mdlIdx] - bb = self.boundBoxes[mdlIdx] - stl = self.modelSTLs[mdlIdx] - safeSTL = self.safeSTLs[mdlIdx] - self.endVector = None - - finDep = obj.FinalDepth.Value + (self.geoTlrnc / 10.0) - depthParams = PathUtils.depth_params(obj.ClearanceHeight.Value, obj.SafeHeight.Value, obj.StartDepth.Value, obj.StepDown.Value, 0.0, finDep) - - # Compute number and size of stepdowns, and final depth - if obj.LayerMode == 'Single-pass': - depthparams = [finDep] - else: - depthparams = [dp for dp in depthParams] - lenDP = len(depthparams) - PathLog.debug('Experimental Waterline depthparams:\n{}'.format(depthparams)) - - # Prepare PathDropCutter objects with STL data - # safePDC = self._planarGetPDC(safeSTL, depthparams[lenDP - 1], obj.SampleInterval.Value, useSafeCutter=False) - - buffer = self.cutter.getDiameter() * 2.0 - borderFace = Part.Face(self._makeExtendedBoundBox(JOB.Stock.Shape.BoundBox, buffer, 0.0)) - - # Get correct boundbox - if obj.BoundBox == 'Stock': - stockEnv = self._getShapeEnvelope(JOB.Stock.Shape) - bbFace = self._getCrossSection(stockEnv) # returned at Z=0.0 - elif obj.BoundBox == 'BaseBoundBox': - baseEnv = self._getShapeEnvelope(base.Shape) - bbFace = self._getCrossSection(baseEnv) # returned at Z=0.0 - - trimFace = borderFace.cut(bbFace) - if self.showDebugObjects is True: - TF = FreeCAD.ActiveDocument.addObject('Part::Feature', 'trimFace') - TF.Shape = trimFace - TF.purgeTouched() - self.tempGroup.addObject(TF) - - # Cycle through layer depths - CUTAREAS = self._getCutAreas(base.Shape, depthparams, bbFace, trimFace, borderFace) - if not CUTAREAS: - PathLog.error('No cross-section cut areas identified.') - return commands - - caCnt = 0 - ofst = obj.BoundaryAdjustment.Value - ofst -= self.radius # (self.radius + (tolrnc / 10.0)) - caLen = len(CUTAREAS) - lastCA = caLen - 1 - lastClearArea = None - lastCsHght = None - clearLastLayer = True - for ca in range(0, caLen): - area = CUTAREAS[ca] - csHght = area.BoundBox.ZMin - csHght += obj.DepthOffset.Value - cont = False - caCnt += 1 - if area.Area > 0.0: - cont = True - caWireCnt = len(area.Wires) - 1 # first wire is boundFace wire - PathLog.debug('cutAreaWireCnt: {}'.format(caWireCnt)) - if self.showDebugObjects is True: - CA = FreeCAD.ActiveDocument.addObject('Part::Feature', 'cutArea_{}'.format(caCnt)) - CA.Shape = area - CA.purgeTouched() - self.tempGroup.addObject(CA) - else: - PathLog.error('Cut area at {} is zero.'.format(round(csHght, 4))) - - # get offset wire(s) based upon cross-section cut area - if cont: - area.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - area.BoundBox.ZMin)) - activeArea = area.cut(trimFace) - activeAreaWireCnt = len(activeArea.Wires) # first wire is boundFace wire - PathLog.debug('activeAreaWireCnt: {}'.format(activeAreaWireCnt)) - if self.showDebugObjects is True: - CA = FreeCAD.ActiveDocument.addObject('Part::Feature', 'activeArea_{}'.format(caCnt)) - CA.Shape = activeArea - CA.purgeTouched() - self.tempGroup.addObject(CA) - ofstArea = self._extractFaceOffset(obj, activeArea, ofst, makeComp=False) - if not ofstArea: - PathLog.error('No offset area returned for cut area depth: {}'.format(csHght)) - cont = False - - if cont: - # Identify solid areas in the offset data - ofstSolidFacesList = self._getSolidAreasFromPlanarFaces(ofstArea) - if ofstSolidFacesList: - clearArea = Part.makeCompound(ofstSolidFacesList) - if self.showDebugObjects is True: - CA = FreeCAD.ActiveDocument.addObject('Part::Feature', 'clearArea_{}'.format(caCnt)) - CA.Shape = clearArea - CA.purgeTouched() - self.tempGroup.addObject(CA) - else: - cont = False - PathLog.error('ofstSolids is False.') - - if cont: - # Make waterline path for current CUTAREA depth (csHght) - commands.extend(self._wiresToWaterlinePath(obj, clearArea, csHght)) - clearArea.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - clearArea.BoundBox.ZMin)) - lastClearArea = clearArea - lastCsHght = csHght - - # Clear layer as needed - (useOfst, usePat, clearLastLayer) = self._clearLayer(obj, ca, lastCA, clearLastLayer) - ##if self.showDebugObjects is True and (usePat or useOfst): - ## OA = FreeCAD.ActiveDocument.addObject('Part::Feature', 'clearPatternArea_{}'.format(round(csHght, 2))) - ## OA.Shape = clearArea - ## OA.purgeTouched() - ## self.tempGroup.addObject(OA) - if usePat: - commands.extend(self._makeCutPatternLayerPaths(JOB, obj, clearArea, csHght)) - if useOfst: - commands.extend(self._makeOffsetLayerPaths(JOB, obj, clearArea, csHght)) - # Efor - - if clearLastLayer: - (useOfst, usePat, cLL) = self._clearLayer(obj, 1, 1, False) - clearArea.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - lastClearArea.BoundBox.ZMin)) - if usePat: - commands.extend(self._makeCutPatternLayerPaths(JOB, obj, lastClearArea, lastCsHght)) - - if useOfst: - commands.extend(self._makeOffsetLayerPaths(JOB, obj, lastClearArea, lastCsHght)) - - PathLog.info("Waterline: All layer scans combined took " + str(time.time() - t_begin) + " s") - return commands - - def _getCutAreas(self, shape, depthparams, bbFace, trimFace, borderFace): - '''_getCutAreas(JOB, shape, depthparams, bbFace, borderFace) ... - Takes shape, depthparams and base-envelope-cross-section, and - returns a list of cut areas - one for each depth.''' - PathLog.debug('_getCutAreas()') - - CUTAREAS = list() - lastLayComp = None - isFirst = True - lenDP = len(depthparams) - - # Cycle through layer depths - for dp in range(0, lenDP): - csHght = depthparams[dp] - PathLog.debug('Depth {} is {}'.format(dp + 1, csHght)) - - # Get slice at depth of shape - csFaces = self._getModelCrossSection(shape, csHght) # returned at Z=0.0 - if not csFaces: - PathLog.error('No cross-section wires at {}'.format(csHght)) - else: - PathLog.debug('cross-section face count {}'.format(len(csFaces))) - if len(csFaces) > 0: - useFaces = self._getSolidAreasFromPlanarFaces(csFaces) - else: - useFaces = False - - if useFaces: - PathLog.debug('useFacesCnt: {}'.format(len(useFaces))) - compAdjFaces = Part.makeCompound(useFaces) - - if self.showDebugObjects is True: - CA = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpSolids_{}'.format(dp + 1)) - CA.Shape = compAdjFaces - CA.purgeTouched() - self.tempGroup.addObject(CA) - - if isFirst: - allPrevComp = compAdjFaces - cutArea = borderFace.cut(compAdjFaces) - else: - preCutArea = borderFace.cut(compAdjFaces) - cutArea = preCutArea.cut(allPrevComp) # cut out higher layers to avoid cutting recessed areas - allPrevComp = allPrevComp.fuse(compAdjFaces) - cutArea.translate(FreeCAD.Vector(0.0, 0.0, csHght - cutArea.BoundBox.ZMin)) - CUTAREAS.append(cutArea) - isFirst = False - else: - PathLog.error('No waterline at depth: {} mm.'.format(csHght)) - # Efor - - if len(CUTAREAS) > 0: - return CUTAREAS - - return False - - def _wiresToWaterlinePath(self, obj, ofstPlnrShp, csHght): - PathLog.debug('_wiresToWaterlinePath()') - commands = list() - - # Translate path geometry to layer height - ofstPlnrShp.translate(FreeCAD.Vector(0.0, 0.0, csHght - ofstPlnrShp.BoundBox.ZMin)) - if self.showDebugObjects is True: - OA = FreeCAD.ActiveDocument.addObject('Part::Feature', 'waterlinePathArea_{}'.format(round(csHght, 2))) - OA.Shape = ofstPlnrShp - OA.purgeTouched() - self.tempGroup.addObject(OA) - - commands.append(Path.Command('N (Cut Area {}.)'.format(round(csHght, 2)))) - for w in range(0, len(ofstPlnrShp.Wires)): - wire = ofstPlnrShp.Wires[w] - V = wire.Vertexes - if obj.CutMode == 'Climb': - lv = len(V) - 1 - startVect = FreeCAD.Vector(V[lv].X, V[lv].Y, V[lv].Z) - else: - startVect = FreeCAD.Vector(V[0].X, V[0].Y, V[0].Z) - - commands.append(Path.Command('N (Wire {}.)'.format(w))) - (cmds, endVect) = self._wireToPath(obj, wire, startVect) - commands.extend(cmds) - commands.append(Path.Command('G0', {'Z': obj.SafeHeight.Value, 'F': self.vertRapid})) - - return commands - - def _makeCutPatternLayerPaths(self, JOB, obj, clrAreaShp, csHght): - PathLog.debug('_makeCutPatternLayerPaths()') - commands = [] - - clrAreaShp.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - clrAreaShp.BoundBox.ZMin)) - pathGeom = self._planarMakePathGeom(obj, clrAreaShp) - pathGeom.translate(FreeCAD.Vector(0.0, 0.0, csHght - pathGeom.BoundBox.ZMin)) - # clrAreaShp.translate(FreeCAD.Vector(0.0, 0.0, csHght - clrAreaShp.BoundBox.ZMin)) - - if self.showDebugObjects is True: - OA = FreeCAD.ActiveDocument.addObject('Part::Feature', 'pathGeom_{}'.format(round(csHght, 2))) - OA.Shape = pathGeom - OA.purgeTouched() - self.tempGroup.addObject(OA) - - # Convert pathGeom to gcode more efficiently - if True: - if obj.CutPattern == 'Offset': - commands.extend(self._makeOffsetLayerPaths(JOB, obj, clrAreaShp, csHght)) - else: - clrAreaShp.translate(FreeCAD.Vector(0.0, 0.0, csHght - clrAreaShp.BoundBox.ZMin)) - if obj.CutPattern == 'Line': - pntSet = self._pathGeomToLinesPointSet(obj, pathGeom) - elif obj.CutPattern == 'ZigZag': - pntSet = self._pathGeomToZigzagPointSet(obj, pathGeom) - elif obj.CutPattern in ['Circular', 'CircularZigZag']: - pntSet = self._pathGeomToArcPointSet(obj, pathGeom) - stpOVRS = self._getExperimentalWaterlinePaths(obj, pntSet, csHght) - # PathLog.debug('stpOVRS:\n{}'.format(stpOVRS)) - safePDC = False - cmds = self._clearGeomToPaths(JOB, obj, safePDC, stpOVRS, csHght) - commands.extend(cmds) - else: - # Use Path.fromShape() to convert edges to paths - for w in range(0, len(pathGeom.Edges)): - wire = pathGeom.Edges[w] - V = wire.Vertexes - if obj.CutMode == 'Climb': - lv = len(V) - 1 - startVect = FreeCAD.Vector(V[lv].X, V[lv].Y, V[lv].Z) - else: - startVect = FreeCAD.Vector(V[0].X, V[0].Y, V[0].Z) - - commands.append(Path.Command('N (Wire {}.)'.format(w))) - (cmds, endVect) = self._wireToPath(obj, wire, startVect) - commands.extend(cmds) - commands.append(Path.Command('G0', {'Z': obj.SafeHeight.Value, 'F': self.vertRapid})) - - return commands - - def _makeOffsetLayerPaths(self, JOB, obj, clrAreaShp, csHght): - PathLog.debug('_makeOffsetLayerPaths()') - PathLog.warning('Using `Offset` for clearing bottom layer.') - cmds = list() - # ofst = obj.BoundaryAdjustment.Value - ofst = 0.0 - self.cutOut # - self.cutter.getDiameter() # (self.radius + (tolrnc / 10.0)) - shape = clrAreaShp - cont = True - cnt = 0 - while cont: - ofstArea = self._extractFaceOffset(obj, shape, ofst, makeComp=True) - if not ofstArea: - PathLog.warning('No offset clearing area returned.') - break - for F in ofstArea.Faces: - cmds.extend(self._wiresToWaterlinePath(obj, F, csHght)) - shape = ofstArea - if cnt == 0: - ofst = 0.0 - self.cutOut # self.cutter.Diameter() - cnt += 1 - return cmds - - def _clearGeomToPaths(self, JOB, obj, safePDC, SCANDATA, csHght): - PathLog.debug('_clearGeomToPaths()') - - GCODE = [Path.Command('N (Beginning of Single-pass layer.)', {})] - tolrnc = JOB.GeometryTolerance.Value - prevDepth = obj.SafeHeight.Value - lenSCANDATA = len(SCANDATA) - gDIR = ['G3', 'G2'] - - if self.CutClimb is True: - gDIR = ['G2', 'G3'] - - # Send cutter to x,y position of first point on first line - first = SCANDATA[0][0][0] # [step][item][point] - GCODE.append(Path.Command('G0', {'X': first.x, 'Y': first.y, 'F': self.horizRapid})) - - # Cycle through step-over sections (line segments or arcs) - odd = True - lstStpEnd = None - prevDepth = obj.SafeHeight.Value # Not used for Single-pass - for so in range(0, lenSCANDATA): - cmds = list() - PRTS = SCANDATA[so] - lenPRTS = len(PRTS) - first = PRTS[0][0] # first point of arc/line stepover group - start = PRTS[0][0] # will change with each line/arc segment - last = None - cmds.append(Path.Command('N (Begin step {}.)'.format(so), {})) - - if so > 0: - if obj.CutPattern == 'CircularZigZag': - if odd is True: - odd = False - else: - odd = True - # minTrnsHght = self._getMinSafeTravelHeight(safePDC, lstStpEnd, first) # Check safe travel height against fullSTL - minTrnsHght = obj.SafeHeight.Value - # cmds.append(Path.Command('N (Transition: last, first: {}, {}: minSTH: {})'.format(lstStpEnd, first, minTrnsHght), {})) - cmds.extend(self._stepTransitionCmds(obj, lstStpEnd, first, minTrnsHght, tolrnc)) - - # Cycle through current step-over parts - for i in range(0, lenPRTS): - prt = PRTS[i] - lenPrt = len(prt) - # PathLog.debug('prt: {}'.format(prt)) - if prt == 'BRK': - nxtStart = PRTS[i + 1][0] - # minSTH = self._getMinSafeTravelHeight(safePDC, last, nxtStart) # Check safe travel height against fullSTL - minSTH = obj.SafeHeight.Value - cmds.append(Path.Command('N (Break)', {})) - cmds.extend(self._breakCmds(obj, last, nxtStart, minSTH, tolrnc)) - else: - cmds.append(Path.Command('N (part {}.)'.format(i + 1), {})) - if obj.CutPattern in ['Line', 'ZigZag']: - start, last = prt - cmds.append(Path.Command('G1', {'X': start.x, 'Y': start.y, 'Z': start.z, 'F': self.horizFeed})) - cmds.append(Path.Command('G1', {'X': last.x, 'Y': last.y, 'F': self.horizFeed})) - elif obj.CutPattern in ['Circular', 'CircularZigZag']: - start, last, centPnt, cMode = prt - gcode = self._makeGcodeArc(start, last, odd, gDIR, tolrnc) - cmds.extend(gcode) - cmds.append(Path.Command('N (End of step {}.)'.format(so), {})) - GCODE.extend(cmds) # save line commands - lstStpEnd = last - # Efor - - return GCODE - - def _getSolidAreasFromPlanarFaces(self, csFaces): - PathLog.debug('_getSolidAreasFromPlanarFaces()') - holds = list() - cutFaces = list() - useFaces = list() - lenCsF = len(csFaces) - PathLog.debug('lenCsF: {}'.format(lenCsF)) - - if lenCsF == 1: - useFaces = csFaces - else: - fIds = list() - aIds = list() - pIds = list() - cIds = list() - - for af in range(0, lenCsF): - fIds.append(af) # face ids - aIds.append(af) # face ids - pIds.append(-1) # parent ids - cIds.append(False) # cut ids - holds.append(False) - - while len(fIds) > 0: - li = fIds.pop() - low = csFaces[li] # senior face - pIds = self._idInternalFeature(csFaces, fIds, pIds, li, low) - # Ewhile - ##PathLog.info('fIds: {}'.format(fIds)) - ##PathLog.info('pIds: {}'.format(pIds)) - - for af in range(lenCsF - 1, -1, -1): # cycle from last item toward first - ##PathLog.info('af: {}'.format(af)) - prnt = pIds[af] - ##PathLog.info('prnt: {}'.format(prnt)) - if prnt == -1: - stack = -1 - else: - stack = [af] - # get_face_ids_to_parent - stack.insert(0, prnt) - nxtPrnt = pIds[prnt] - # find af value for nxtPrnt - while nxtPrnt != -1: - stack.insert(0, nxtPrnt) - nxtPrnt = pIds[nxtPrnt] - cIds[af] = stack - # PathLog.debug('cIds: {}\n'.format(cIds)) - - for af in range(0, lenCsF): - # PathLog.debug('af is {}'.format(af)) - pFc = cIds[af] - if pFc == -1: - # Simple, independent region - holds[af] = csFaces[af] # place face in hold - # PathLog.debug('pFc == -1') - else: - # Compound region - # PathLog.debug('pFc is not -1') - cnt = len(pFc) - if cnt % 2.0 == 0.0: - # even is donut cut - # PathLog.debug('cnt is even') - inr = pFc[cnt - 1] - otr = pFc[cnt - 2] - # PathLog.debug('inr / otr: {} / {}'.format(inr, otr)) - holds[otr] = holds[otr].cut(csFaces[inr]) - else: - # odd is floating solid - # PathLog.debug('cnt is ODD') - holds[af] = csFaces[af] - # Efor - - for af in range(0, lenCsF): - if holds[af]: - useFaces.append(holds[af]) # save independent solid - - # Eif - - if len(useFaces) > 0: - return useFaces - - return False - - def _getModelCrossSection(self, shape, csHght): - PathLog.debug('_getCrossSection()') - wires = list() - - def byArea(fc): - return fc.Area - - for i in shape.slice(FreeCAD.Vector(0, 0, 1), csHght): - wires.append(i) - - if len(wires) > 0: - for w in wires: - if w.isClosed() is False: - return False - FCS = list() - for w in wires: - w.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - w.BoundBox.ZMin)) - FCS.append(Part.Face(w)) - FCS.sort(key=byArea, reverse=True) - return FCS - else: - PathLog.debug(' -No wires from .slice() method') - - return False - - def _isInBoundBox(self, outShp, inShp): - obb = outShp.BoundBox - ibb = inShp.BoundBox - - if obb.XMin < ibb.XMin: - if obb.XMax > ibb.XMax: - if obb.YMin < ibb.YMin: - if obb.YMax > ibb.YMax: - return True - return False - - def _idInternalFeature(self, csFaces, fIds, pIds, li, low): - Ids = list() - for i in fIds: - Ids.append(i) - while len(Ids) > 0: - hi = Ids.pop() - high = csFaces[hi] - if self._isInBoundBox(high, low): - cmn = high.common(low) - if cmn.Area > 0.0: - pIds[li] = hi - break - # Ewhile - return pIds - - def _wireToPath(self, obj, wire, startVect): - '''_wireToPath(obj, wire, startVect) ... wire to path.''' - PathLog.track() - - paths = [] - pathParams = {} # pylint: disable=assignment-from-no-return - V = wire.Vertexes - - pathParams['shapes'] = [wire] - pathParams['feedrate'] = self.horizFeed - pathParams['feedrate_v'] = self.vertFeed - pathParams['verbose'] = True - pathParams['resume_height'] = obj.SafeHeight.Value - pathParams['retraction'] = obj.ClearanceHeight.Value - pathParams['return_end'] = True - # Note that emitting preambles between moves breaks some dressups and prevents path optimization on some controllers - pathParams['preamble'] = False - pathParams['start'] = startVect - - (pp, end_vector) = Path.fromShapes(**pathParams) - paths.extend(pp.Commands) - # PathLog.debug('pp: {}, end vector: {}'.format(pp, end_vector)) - - self.endVector = end_vector # pylint: disable=attribute-defined-outside-init - - return (paths, end_vector) - - def _makeExtendedBoundBox(self, wBB, bbBfr, zDep): - pl = FreeCAD.Placement() - pl.Rotation = FreeCAD.Rotation(FreeCAD.Vector(0, 0, 1), 0) - pl.Base = FreeCAD.Vector(0, 0, 0) - - p1 = FreeCAD.Vector(wBB.XMin - bbBfr, wBB.YMin - bbBfr, zDep) - p2 = FreeCAD.Vector(wBB.XMax + bbBfr, wBB.YMin - bbBfr, zDep) - p3 = FreeCAD.Vector(wBB.XMax + bbBfr, wBB.YMax + bbBfr, zDep) - p4 = FreeCAD.Vector(wBB.XMin - bbBfr, wBB.YMax + bbBfr, zDep) - bb = Part.makePolygon([p1, p2, p3, p4, p1]) - - return bb - - def _makeGcodeArc(self, strtPnt, endPnt, odd, gDIR, tolrnc): - cmds = list() - isCircle = False - inrPnt = None - gdi = 0 - if odd is True: - gdi = 1 - - # Test if pnt set is circle - if abs(strtPnt.x - endPnt.x) < tolrnc: - if abs(strtPnt.y - endPnt.y) < tolrnc: - isCircle = True - isCircle = False - - if isCircle is True: - # convert LN to G2/G3 arc, consolidating GCode - # https://wiki.shapeoko.com/index.php/G-Code#G2_-_clockwise_arc - # https://www.cnccookbook.com/cnc-g-code-arc-circle-g02-g03/ - # Dividing circle into two arcs allows for G2/G3 on inclined surfaces - - # ijk = self.tmpCOM - strtPnt # vector from start to center - ijk = self.tmpCOM - strtPnt # vector from start to center - xyz = self.tmpCOM.add(ijk) # end point - cmds.append(Path.Command('G1', {'X': strtPnt.x, 'Y': strtPnt.y, 'Z': strtPnt.z, 'F': self.horizFeed})) - cmds.append(Path.Command(gDIR[gdi], {'X': xyz.x, 'Y': xyz.y, 'Z': xyz.z, - 'I': ijk.x, 'J': ijk.y, 'K': ijk.z, # leave same xyz.z height - 'F': self.horizFeed})) - cmds.append(Path.Command('G1', {'X': xyz.x, 'Y': xyz.y, 'Z': xyz.z, 'F': self.horizFeed})) - ijk = self.tmpCOM - xyz # vector from start to center - rst = strtPnt # end point - cmds.append(Path.Command(gDIR[gdi], {'X': rst.x, 'Y': rst.y, 'Z': rst.z, - 'I': ijk.x, 'J': ijk.y, 'K': ijk.z, # leave same xyz.z height - 'F': self.horizFeed})) - cmds.append(Path.Command('G1', {'X': strtPnt.x, 'Y': strtPnt.y, 'Z': strtPnt.z, 'F': self.horizFeed})) - else: - # ijk = self.tmpCOM - strtPnt - ijk = self.tmpCOM.sub(strtPnt) # vector from start to center - xyz = endPnt - cmds.append(Path.Command('G1', {'X': strtPnt.x, 'Y': strtPnt.y, 'Z': strtPnt.z, 'F': self.horizFeed})) - cmds.append(Path.Command(gDIR[gdi], {'X': xyz.x, 'Y': xyz.y, 'Z': xyz.z, - 'I': ijk.x, 'J': ijk.y, 'K': ijk.z, # leave same xyz.z height - 'F': self.horizFeed})) - cmds.append(Path.Command('G1', {'X': endPnt.x, 'Y': endPnt.y, 'Z': endPnt.z, 'F': self.horizFeed})) - - return cmds - - def _clearLayer(self, obj, ca, lastCA, clearLastLayer): - PathLog.debug('_clearLayer()') - usePat = False - useOfst = False - - if obj.ClearLastLayer == 'Off': - if obj.CutPattern != 'None': - usePat = True - else: - if ca == lastCA: - PathLog.debug('... Clearing bottom layer.') - if obj.ClearLastLayer == 'Offset': - obj.CutPattern = 'None' - useOfst = True - else: - obj.CutPattern = obj.ClearLastLayer - usePat = True - clearLastLayer = False - - return (useOfst, usePat, clearLastLayer) - - def resetOpVariables(self, all=True): - '''resetOpVariables() ... Reset class variables used for instance of operation.''' - self.holdPoint = None - self.layerEndPnt = None - self.onHold = False - self.SafeHeightOffset = 2.0 - self.ClearHeightOffset = 4.0 - self.layerEndzMax = 0.0 - self.resetTolerance = 0.0 - self.holdPntCnt = 0 - self.bbRadius = 0.0 - self.axialFeed = 0.0 - self.axialRapid = 0.0 - self.FinalDepth = 0.0 - self.clearHeight = 0.0 - self.safeHeight = 0.0 - self.faceZMax = -999999999999.0 - if all is True: - self.cutter = None - self.stl = None - self.fullSTL = None - self.cutOut = 0.0 - self.radius = 0.0 - self.useTiltCutter = False - return True - - def deleteOpVariables(self, all=True): - '''deleteOpVariables() ... Reset class variables used for instance of operation.''' - del self.holdPoint - del self.layerEndPnt - del self.onHold - del self.SafeHeightOffset - del self.ClearHeightOffset - del self.layerEndzMax - del self.resetTolerance - del self.holdPntCnt - del self.bbRadius - del self.axialFeed - del self.axialRapid - del self.FinalDepth - del self.clearHeight - del self.safeHeight - del self.faceZMax - if all is True: - del self.cutter - del self.stl - del self.fullSTL - del self.cutOut - del self.radius - del self.useTiltCutter - return True - - def setOclCutter(self, obj, safe=False): - ''' setOclCutter(obj) ... Translation function to convert FreeCAD tool definition to OCL formatted tool. ''' - # Set cutter details - # https://www.freecadweb.org/api/dd/dfe/classPath_1_1Tool.html#details - diam_1 = float(obj.ToolController.Tool.Diameter) - lenOfst = obj.ToolController.Tool.LengthOffset if hasattr(obj.ToolController.Tool, 'LengthOffset') else 0 - FR = obj.ToolController.Tool.FlatRadius if hasattr(obj.ToolController.Tool, 'FlatRadius') else 0 - CEH = obj.ToolController.Tool.CuttingEdgeHeight if hasattr(obj.ToolController.Tool, 'CuttingEdgeHeight') else 0 - CEA = obj.ToolController.Tool.CuttingEdgeAngle if hasattr(obj.ToolController.Tool, 'CuttingEdgeAngle') else 0 - - # Make safeCutter with 2 mm buffer around physical cutter - if safe is True: - diam_1 += 4.0 - if FR != 0.0: - FR += 2.0 - - PathLog.debug('ToolType: {}'.format(obj.ToolController.Tool.ToolType)) - if obj.ToolController.Tool.ToolType == 'EndMill': - # Standard End Mill - return ocl.CylCutter(diam_1, (CEH + lenOfst)) - - elif obj.ToolController.Tool.ToolType == 'BallEndMill' and FR == 0.0: - # Standard Ball End Mill - # OCL -> BallCutter::BallCutter(diameter, length) - self.useTiltCutter = True - return ocl.BallCutter(diam_1, (diam_1 / 2 + lenOfst)) - - elif obj.ToolController.Tool.ToolType == 'BallEndMill' and FR > 0.0: - # Bull Nose or Corner Radius cutter - # Reference: https://www.fine-tools.com/halbstabfraeser.html - # OCL -> BallCutter::BallCutter(diameter, length) - return ocl.BullCutter(diam_1, FR, (CEH + lenOfst)) - - elif obj.ToolController.Tool.ToolType == 'Engraver' and FR > 0.0: - # Bull Nose or Corner Radius cutter - # Reference: https://www.fine-tools.com/halbstabfraeser.html - # OCL -> ConeCutter::ConeCutter(diameter, angle, lengthOffset) - return ocl.ConeCutter(diam_1, (CEA / 2), lenOfst) - - elif obj.ToolController.Tool.ToolType == 'ChamferMill': - # Bull Nose or Corner Radius cutter - # Reference: https://www.fine-tools.com/halbstabfraeser.html - # OCL -> ConeCutter::ConeCutter(diameter, angle, lengthOffset) - return ocl.ConeCutter(diam_1, (CEA / 2), lenOfst) - else: - # Default to standard end mill - PathLog.warning("Defaulting cutter to standard end mill.") - return ocl.CylCutter(diam_1, (CEH + lenOfst)) - - # http://www.carbidecutter.net/products/carbide-burr-cone-shape-sm.html - ''' - # Available FreeCAD cutter types - some still need translation to available OCL cutter classes. - Drill, CenterDrill, CounterSink, CounterBore, FlyCutter, Reamer, Tap, - EndMill, SlotCutter, BallEndMill, ChamferMill, CornerRound, Engraver - ''' - # Adittional problem is with new ToolBit user-defined cutter shapes. - # Some sort of translation/conversion will have to be defined to make compatible with OCL. - PathLog.error('Unable to set OCL cutter.') - return False - - -def SetupProperties(): - ''' SetupProperties() ... Return list of properties required for operation.''' - setup = [] - setup.append('Algorithm') - setup.append('AngularDeflection') - setup.append('AvoidLastX_Faces') - setup.append('AvoidLastX_InternalFeatures') - setup.append('BoundBox') - setup.append('BoundaryAdjustment') - setup.append('CircularCenterAt') - setup.append('CircularCenterCustom') - setup.append('ClearLastLayer') - setup.append('CutMode') - setup.append('CutPattern') - setup.append('CutPatternAngle') - setup.append('CutPatternReversed') - setup.append('DepthOffset') - setup.append('GapSizes') - setup.append('GapThreshold') - setup.append('HandleMultipleFeatures') - setup.append('InternalFeaturesCut') - setup.append('InternalFeaturesAdjustment') - setup.append('LayerMode') - setup.append('LinearDeflection') - setup.append('OptimizeStepOverTransitions') - setup.append('ProfileEdges') - setup.append('BoundaryEnforcement') - setup.append('SampleInterval') - setup.append('StartPoint') - setup.append('StepOver') - setup.append('UseStartPoint') - # For debugging - setup.append('ShowTempObjects') - return setup - - -def Create(name, obj=None): - '''Create(name) ... Creates and returns a Waterline operation.''' - if obj is None: - obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython", name) - obj.Proxy = ObjectWaterline(obj, name) - return obj +# -*- coding: utf-8 -*- + +# *************************************************************************** +# * * +# * Copyright (c) 2019 Russell Johnson (russ4262) * +# * Copyright (c) 2019 sliptonic * +# * * +# * This program is free software; you can redistribute it and/or modify * +# * it under the terms of the GNU Lesser General Public License (LGPL) * +# * as published by the Free Software Foundation; either version 2 of * +# * the License, or (at your option) any later version. * +# * for detail see the LICENCE text file. * +# * * +# * This program is distributed in the hope that it will be useful, * +# * but WITHOUT ANY WARRANTY; without even the implied warranty of * +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# * GNU Library General Public License for more details. * +# * * +# * You should have received a copy of the GNU Library General Public * +# * License along with this program; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# *************************************************************************** + +from __future__ import print_function + +__title__ = "Path Waterline Operation" +__author__ = "russ4262 (Russell Johnson), sliptonic (Brad Collette)" +__url__ = "http://www.freecadweb.org" +__doc__ = "Class and implementation of Waterline operation." +__contributors__ = "" + +import FreeCAD +from PySide import QtCore + +# OCL must be installed +try: + import ocl +except ImportError: + msg = QtCore.QCoreApplication.translate("PathWaterline", "This operation requires OpenCamLib to be installed.") + FreeCAD.Console.PrintError(msg + "\n") + raise ImportError + # import sys + # sys.exit(msg) + +import Path +import PathScripts.PathLog as PathLog +import PathScripts.PathUtils as PathUtils +import PathScripts.PathOp as PathOp +import PathScripts.PathSurfaceSupport as PathSurfaceSupport +import time +import math + +# lazily loaded modules +from lazy_loader.lazy_loader import LazyLoader +MeshPart = LazyLoader('MeshPart', globals(), 'MeshPart') +Part = LazyLoader('Part', globals(), 'Part') + +if FreeCAD.GuiUp: + import FreeCADGui + +PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule()) +# PathLog.trackModule(PathLog.thisModule()) + + +# Qt translation handling +def translate(context, text, disambig=None): + return QtCore.QCoreApplication.translate(context, text, disambig) + + +class ObjectWaterline(PathOp.ObjectOp): + '''Proxy object for Surfacing operation.''' + + def baseObject(self): + '''baseObject() ... returns super of receiver + Used to call base implementation in overwritten functions.''' + return super(self.__class__, self) + + def opFeatures(self, obj): + '''opFeatures(obj) ... return all standard features and edges based geomtries''' + return PathOp.FeatureTool | PathOp.FeatureDepths | PathOp.FeatureHeights | PathOp.FeatureStepDown | PathOp.FeatureCoolant | PathOp.FeatureBaseFaces + + def initOperation(self, obj): + '''initPocketOp(obj) ... + Initialize the operation - property creation and property editor status.''' + self.initOpProperties(obj) + + # For debugging + if PathLog.getLevel(PathLog.thisModule()) != 4: + obj.setEditorMode('ShowTempObjects', 2) # hide + + if not hasattr(obj, 'DoNotSetDefaultValues'): + self.setEditorProperties(obj) + + def initOpProperties(self, obj, warn=False): + '''initOpProperties(obj) ... create operation specific properties''' + missing = list() + + for (prtyp, nm, grp, tt) in self.opProperties(): + if not hasattr(obj, nm): + obj.addProperty(prtyp, nm, grp, tt) + missing.append(nm) + if warn: + newPropMsg = translate('PathWaterline', 'New property added to') + ' "{}": '.format(obj.Label) + nm + '. ' + newPropMsg += translate('PathWaterline', 'Check its default value.') + PathLog.warning(newPropMsg) + + # Set enumeration lists for enumeration properties + if len(missing) > 0: + ENUMS = self.propertyEnumerations() + for n in ENUMS: + if n in missing: + setattr(obj, n, ENUMS[n]) + + self.addedAllProperties = True + + def opProperties(self): + '''opProperties() ... return list of tuples containing operation specific properties''' + return [ + ("App::PropertyBool", "ShowTempObjects", "Debug", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Show the temporary path construction objects when module is in DEBUG mode.")), + + ("App::PropertyDistance", "AngularDeflection", "Mesh Conversion", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Smaller values yield a finer, more accurate the mesh. Smaller values increase processing time a lot.")), + ("App::PropertyDistance", "LinearDeflection", "Mesh Conversion", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Smaller values yield a finer, more accurate the mesh. Smaller values do not increase processing time much.")), + + ("App::PropertyInteger", "AvoidLastX_Faces", "Selected Geometry Settings", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Avoid cutting the last 'N' faces in the Base Geometry list of selected faces.")), + ("App::PropertyBool", "AvoidLastX_InternalFeatures", "Selected Geometry Settings", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Do not cut internal features on avoided faces.")), + ("App::PropertyDistance", "BoundaryAdjustment", "Selected Geometry Settings", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Positive values push the cutter toward, or beyond, the boundary. Negative values retract the cutter away from the boundary.")), + ("App::PropertyBool", "BoundaryEnforcement", "Selected Geometry Settings", + QtCore.QT_TRANSLATE_NOOP("App::Property", "If true, the cutter will remain inside the boundaries of the model or selected face(s).")), + ("App::PropertyEnumeration", "HandleMultipleFeatures", "Selected Geometry Settings", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Choose how to process multiple Base Geometry features.")), + ("App::PropertyDistance", "InternalFeaturesAdjustment", "Selected Geometry Settings", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Positive values push the cutter toward, or into, the feature. Negative values retract the cutter away from the feature.")), + ("App::PropertyBool", "InternalFeaturesCut", "Selected Geometry Settings", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Ignore internal feature areas within a larger selected face.")), + + ("App::PropertyEnumeration", "Algorithm", "Clearing Options", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Select the algorithm to use: OCL Dropcutter*, or Experimental (Not OCL based).")), + ("App::PropertyEnumeration", "BoundBox", "Clearing Options", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Select the overall boundary for the operation.")), + ("App::PropertyEnumeration", "ClearLastLayer", "Clearing Options", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Set to clear last layer in a `Multi-pass` operation.")), + ("App::PropertyEnumeration", "CutMode", "Clearing Options", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the direction for the cutting tool to engage the material: Climb (ClockWise) or Conventional (CounterClockWise)")), + ("App::PropertyEnumeration", "CutPattern", "Clearing Options", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the geometric clearing pattern to use for the operation.")), + ("App::PropertyFloat", "CutPatternAngle", "Clearing Options", + QtCore.QT_TRANSLATE_NOOP("App::Property", "The yaw angle used for certain clearing patterns")), + ("App::PropertyBool", "CutPatternReversed", "Clearing Options", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Reverse the cut order of the stepover paths. For circular cut patterns, begin at the outside and work toward the center.")), + ("App::PropertyDistance", "DepthOffset", "Clearing Options", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the Z-axis depth offset from the target surface.")), + ("App::PropertyDistance", "IgnoreOuterAbove", "Clearing Options", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Ignore outer waterlines above this height.")), + ("App::PropertyEnumeration", "LayerMode", "Clearing Options", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Complete the operation in a single pass at depth, or mulitiple passes to final depth.")), + ("App::PropertyVectorDistance", "PatternCenterCustom", "Clearing Options", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the start point for the cut pattern.")), + ("App::PropertyEnumeration", "PatternCenterAt", "Clearing Options", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Choose location of the center point for starting the cut pattern.")), + ("App::PropertyDistance", "SampleInterval", "Clearing Options", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the sampling resolution. Smaller values quickly increase processing time.")), + ("App::PropertyPercent", "StepOver", "Clearing Options", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the stepover percentage, based on the tool's diameter.")), + + ("App::PropertyBool", "OptimizeLinearPaths", "Optimization", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Enable optimization of linear paths (co-linear points). Removes unnecessary co-linear points from G-Code output.")), + ("App::PropertyBool", "OptimizeStepOverTransitions", "Optimization", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Enable separate optimization of transitions between, and breaks within, each step over path.")), + ("App::PropertyDistance", "GapThreshold", "Optimization", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Collinear and co-radial artifact gaps that are smaller than this threshold are closed in the path.")), + ("App::PropertyString", "GapSizes", "Optimization", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Feedback: three smallest gaps identified in the path geometry.")), + + ("App::PropertyVectorDistance", "StartPoint", "Start Point", + QtCore.QT_TRANSLATE_NOOP("App::Property", "The custom start point for the path of this operation")), + ("App::PropertyBool", "UseStartPoint", "Start Point", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Make True, if specifying a Start Point")) + ] + + def propertyEnumerations(self): + # Enumeration lists for App::PropertyEnumeration properties + return { + 'Algorithm': ['OCL Dropcutter', 'Experimental'], + 'BoundBox': ['BaseBoundBox', 'Stock'], + 'PatternCenterAt': ['CenterOfMass', 'CenterOfBoundBox', 'XminYmin', 'Custom'], + 'ClearLastLayer': ['Off', 'Line', 'Circular', 'CircularZigZag', 'Offset', 'Spiral', 'ZigZag'], + 'CutMode': ['Conventional', 'Climb'], + 'CutPattern': ['None', 'Line', 'Circular', 'CircularZigZag', 'Offset', 'Spiral', 'ZigZag'], # Additional goals ['Offset', 'Spiral', 'ZigZagOffset', 'Grid', 'Triangle'] + 'HandleMultipleFeatures': ['Collectively', 'Individually'], + 'LayerMode': ['Single-pass', 'Multi-pass'], + } + + def setEditorProperties(self, obj): + # Used to hide inputs in properties list + expMode = G = 0 + show = hide = A = B = C = 2 + if hasattr(obj, 'EnableRotation'): + obj.setEditorMode('EnableRotation', hide) + + obj.setEditorMode('BoundaryEnforcement', hide) + obj.setEditorMode('InternalFeaturesAdjustment', hide) + obj.setEditorMode('InternalFeaturesCut', hide) + obj.setEditorMode('AvoidLastX_Faces', hide) + obj.setEditorMode('AvoidLastX_InternalFeatures', hide) + obj.setEditorMode('BoundaryAdjustment', hide) + obj.setEditorMode('HandleMultipleFeatures', hide) + obj.setEditorMode('OptimizeLinearPaths', hide) + obj.setEditorMode('OptimizeStepOverTransitions', hide) + obj.setEditorMode('GapThreshold', hide) + obj.setEditorMode('GapSizes', hide) + + if obj.Algorithm == 'OCL Dropcutter': + pass + elif obj.Algorithm == 'Experimental': + A = B = C = 0 + expMode = G = show = hide = 2 + + cutPattern = obj.CutPattern + if obj.ClearLastLayer != 'Off': + cutPattern = obj.ClearLastLayer + + if cutPattern == 'None': + show = hide = A = 2 + elif cutPattern in ['Line', 'ZigZag']: + show = 0 + elif cutPattern in ['Circular', 'CircularZigZag']: + show = 2 # hide + hide = 0 # show + elif cutPattern == 'Spiral': + G = hide = 0 + + obj.setEditorMode('CutPatternAngle', show) + obj.setEditorMode('PatternCenterAt', hide) + obj.setEditorMode('PatternCenterCustom', hide) + obj.setEditorMode('CutPatternReversed', A) + + obj.setEditorMode('ClearLastLayer', C) + obj.setEditorMode('StepOver', B) + obj.setEditorMode('IgnoreOuterAbove', B) + obj.setEditorMode('CutPattern', C) + obj.setEditorMode('SampleInterval', G) + obj.setEditorMode('LinearDeflection', expMode) + obj.setEditorMode('AngularDeflection', expMode) + + def onChanged(self, obj, prop): + if hasattr(self, 'addedAllProperties'): + if self.addedAllProperties is True: + if prop in ['Algorithm', 'CutPattern']: + self.setEditorProperties(obj) + + def opOnDocumentRestored(self, obj): + self.initOpProperties(obj, warn=True) + + if PathLog.getLevel(PathLog.thisModule()) != 4: + obj.setEditorMode('ShowTempObjects', 2) # hide + else: + obj.setEditorMode('ShowTempObjects', 0) # show + + # Repopulate enumerations in case of changes + ENUMS = self.propertyEnumerations() + for n in ENUMS: + restore = False + if hasattr(obj, n): + val = obj.getPropertyByName(n) + restore = True + setattr(obj, n, ENUMS[n]) + if restore: + setattr(obj, n, val) + + self.setEditorProperties(obj) + + def opSetDefaultValues(self, obj, job): + '''opSetDefaultValues(obj, job) ... initialize defaults''' + job = PathUtils.findParentJob(obj) + + obj.OptimizeLinearPaths = True + obj.InternalFeaturesCut = True + obj.OptimizeStepOverTransitions = False + obj.BoundaryEnforcement = True + obj.UseStartPoint = False + obj.AvoidLastX_InternalFeatures = True + obj.CutPatternReversed = False + obj.IgnoreOuterAbove = obj.StartDepth.Value + 0.00001 + obj.StartPoint = FreeCAD.Vector(0.0, 0.0, obj.ClearanceHeight.Value) + obj.Algorithm = 'OCL Dropcutter' + obj.LayerMode = 'Single-pass' + obj.CutMode = 'Conventional' + obj.CutPattern = 'None' + obj.HandleMultipleFeatures = 'Collectively' # 'Individually' + obj.PatternCenterAt = 'CenterOfMass' # 'CenterOfBoundBox', 'XminYmin', 'Custom' + obj.GapSizes = 'No gaps identified.' + obj.ClearLastLayer = 'Off' + obj.StepOver = 100 + obj.CutPatternAngle = 0.0 + obj.DepthOffset.Value = 0.0 + obj.SampleInterval.Value = 1.0 + obj.BoundaryAdjustment.Value = 0.0 + obj.InternalFeaturesAdjustment.Value = 0.0 + obj.AvoidLastX_Faces = 0 + obj.PatternCenterCustom = FreeCAD.Vector(0.0, 0.0, 0.0) + obj.GapThreshold.Value = 0.005 + obj.LinearDeflection.Value = 0.0001 + obj.AngularDeflection.Value = 0.25 + # For debugging + obj.ShowTempObjects = False + + # need to overwrite the default depth calculations for facing + d = None + if job: + if job.Stock: + d = PathUtils.guessDepths(job.Stock.Shape, None) + obj.IgnoreOuterAbove = job.Stock.Shape.BoundBox.ZMax + 0.000001 + PathLog.debug("job.Stock exists") + else: + PathLog.debug("job.Stock NOT exist") + else: + PathLog.debug("job NOT exist") + + if d is not None: + obj.OpFinalDepth.Value = d.final_depth + obj.OpStartDepth.Value = d.start_depth + else: + obj.OpFinalDepth.Value = -10 + obj.OpStartDepth.Value = 10 + + PathLog.debug('Default OpFinalDepth: {}'.format(obj.OpFinalDepth.Value)) + PathLog.debug('Defualt OpStartDepth: {}'.format(obj.OpStartDepth.Value)) + + def opApplyPropertyLimits(self, obj): + '''opApplyPropertyLimits(obj) ... Apply necessary limits to user input property values before performing main operation.''' + # Limit sample interval + if obj.SampleInterval.Value < 0.0001: + obj.SampleInterval.Value = 0.0001 + PathLog.error(translate('PathWaterline', 'Sample interval limits are 0.0001 to 25.4 millimeters.')) + if obj.SampleInterval.Value > 25.4: + obj.SampleInterval.Value = 25.4 + PathLog.error(translate('PathWaterline', 'Sample interval limits are 0.0001 to 25.4 millimeters.')) + + # Limit cut pattern angle + if obj.CutPatternAngle < -360.0: + obj.CutPatternAngle = 0.0 + PathLog.error(translate('PathWaterline', 'Cut pattern angle limits are +-360 degrees.')) + if obj.CutPatternAngle >= 360.0: + obj.CutPatternAngle = 0.0 + PathLog.error(translate('PathWaterline', 'Cut pattern angle limits are +- 360 degrees.')) + + # Limit StepOver to natural number percentage + if obj.StepOver > 100: + obj.StepOver = 100 + if obj.StepOver < 1: + obj.StepOver = 1 + + # Limit AvoidLastX_Faces to zero and positive values + if obj.AvoidLastX_Faces < 0: + obj.AvoidLastX_Faces = 0 + PathLog.error(translate('PathWaterline', 'AvoidLastX_Faces: Only zero or positive values permitted.')) + if obj.AvoidLastX_Faces > 100: + obj.AvoidLastX_Faces = 100 + PathLog.error(translate('PathWaterline', 'AvoidLastX_Faces: Avoid last X faces count limited to 100.')) + + def opExecute(self, obj): + '''opExecute(obj) ... process surface operation''' + PathLog.track() + + self.modelSTLs = list() + self.safeSTLs = list() + self.modelTypes = list() + self.boundBoxes = list() + self.profileShapes = list() + self.collectiveShapes = list() + self.individualShapes = list() + self.avoidShapes = list() + self.geoTlrnc = None + self.tempGroup = None + self.CutClimb = False + self.closedGap = False + self.tmpCOM = None + self.gaps = [0.1, 0.2, 0.3] + CMDS = list() + modelVisibility = list() + FCAD = FreeCAD.ActiveDocument + + try: + dotIdx = __name__.index('.') + 1 + except Exception: + dotIdx = 0 + self.module = __name__[dotIdx:] + + # make circle for workplane + self.wpc = Part.makeCircle(2.0) + + # Set debugging behavior + self.showDebugObjects = False # Set to true if you want a visual DocObjects created for some path construction objects + self.showDebugObjects = obj.ShowTempObjects + deleteTempsFlag = True # Set to False for debugging + if PathLog.getLevel(PathLog.thisModule()) == 4: + deleteTempsFlag = False + else: + self.showDebugObjects = False + + # mark beginning of operation and identify parent Job + PathLog.info('\nBegin Waterline operation...') + startTime = time.time() + + # Identify parent Job + JOB = PathUtils.findParentJob(obj) + if JOB is None: + PathLog.error(translate('PathWaterline', "No JOB")) + return + self.stockZMin = JOB.Stock.Shape.BoundBox.ZMin + + # set cut mode; reverse as needed + if obj.CutMode == 'Climb': + self.CutClimb = True + if obj.CutPatternReversed is True: + if self.CutClimb is True: + self.CutClimb = False + else: + self.CutClimb = True + + # Begin GCode for operation with basic information + # ... and move cutter to clearance height and startpoint + output = '' + if obj.Comment != '': + self.commandlist.append(Path.Command('N ({})'.format(str(obj.Comment)), {})) + self.commandlist.append(Path.Command('N ({})'.format(obj.Label), {})) + self.commandlist.append(Path.Command('N (Tool type: {})'.format(str(obj.ToolController.Tool.ToolType)), {})) + self.commandlist.append(Path.Command('N (Compensated Tool Path. Diameter: {})'.format(str(obj.ToolController.Tool.Diameter)), {})) + self.commandlist.append(Path.Command('N (Sample interval: {})'.format(str(obj.SampleInterval.Value)), {})) + self.commandlist.append(Path.Command('N (Step over %: {})'.format(str(obj.StepOver)), {})) + self.commandlist.append(Path.Command('N ({})'.format(output), {})) + self.commandlist.append(Path.Command('G0', {'Z': obj.ClearanceHeight.Value, 'F': self.vertRapid})) + if obj.UseStartPoint: + self.commandlist.append(Path.Command('G0', {'X': obj.StartPoint.x, 'Y': obj.StartPoint.y, 'F': self.horizRapid})) + + # Instantiate additional class operation variables + self.resetOpVariables() + + # Impose property limits + self.opApplyPropertyLimits(obj) + + # Create temporary group for temporary objects, removing existing + # if self.showDebugObjects is True: + tempGroupName = 'tempPathWaterlineGroup' + if FCAD.getObject(tempGroupName): + for to in FCAD.getObject(tempGroupName).Group: + FCAD.removeObject(to.Name) + FCAD.removeObject(tempGroupName) # remove temp directory if already exists + if FCAD.getObject(tempGroupName + '001'): + for to in FCAD.getObject(tempGroupName + '001').Group: + FCAD.removeObject(to.Name) + FCAD.removeObject(tempGroupName + '001') # remove temp directory if already exists + tempGroup = FCAD.addObject('App::DocumentObjectGroup', tempGroupName) + tempGroupName = tempGroup.Name + self.tempGroup = tempGroup + tempGroup.purgeTouched() + # Add temp object to temp group folder with following code: + # ... self.tempGroup.addObject(OBJ) + + # Setup cutter for OCL and cutout value for operation - based on tool controller properties + self.cutter = self.setOclCutter(obj) + if self.cutter is False: + PathLog.error(translate('PathWaterline', "Canceling Waterline operation. Error creating OCL cutter.")) + return + self.toolDiam = self.cutter.getDiameter() + self.radius = self.toolDiam / 2.0 + self.cutOut = (self.toolDiam * (float(obj.StepOver) / 100.0)) + self.gaps = [self.toolDiam, self.toolDiam, self.toolDiam] + + # Get height offset values for later use + self.SafeHeightOffset = JOB.SetupSheet.SafeHeightOffset.Value + self.ClearHeightOffset = JOB.SetupSheet.ClearanceHeightOffset.Value + + # Set deflection values for mesh generation + useDGT = False + try: # try/except is for Path Jobs created before GeometryTolerance + self.geoTlrnc = JOB.GeometryTolerance.Value + if self.geoTlrnc == 0.0: + useDGT = True + except AttributeError as ee: + PathLog.warning('{}\nPlease set Job.GeometryTolerance to an acceptable value. Using PathPreferences.defaultGeometryTolerance().'.format(ee)) + useDGT = True + if useDGT: + import PathScripts.PathPreferences as PathPreferences + self.geoTlrnc = PathPreferences.defaultGeometryTolerance() + + # Calculate default depthparams for operation + self.depthParams = PathUtils.depth_params(obj.ClearanceHeight.Value, obj.SafeHeight.Value, obj.StartDepth.Value, obj.StepDown.Value, 0.0, obj.FinalDepth.Value) + self.midDep = (obj.StartDepth.Value + obj.FinalDepth.Value) / 2.0 + + # Save model visibilities for restoration + if FreeCAD.GuiUp: + for m in range(0, len(JOB.Model.Group)): + mNm = JOB.Model.Group[m].Name + modelVisibility.append(FreeCADGui.ActiveDocument.getObject(mNm).Visibility) + + # Setup STL, model type, and bound box containers for each model in Job + for m in range(0, len(JOB.Model.Group)): + M = JOB.Model.Group[m] + self.modelSTLs.append(False) + self.safeSTLs.append(False) + self.profileShapes.append(False) + # Set bound box + if obj.BoundBox == 'BaseBoundBox': + if M.TypeId.startswith('Mesh'): + self.modelTypes.append('M') # Mesh + self.boundBoxes.append(M.Mesh.BoundBox) + else: + self.modelTypes.append('S') # Solid + self.boundBoxes.append(M.Shape.BoundBox) + elif obj.BoundBox == 'Stock': + self.modelTypes.append('S') # Solid + self.boundBoxes.append(JOB.Stock.Shape.BoundBox) + + # ###### MAIN COMMANDS FOR OPERATION ###### + + # Begin processing obj.Base data and creating GCode + PSF = PathSurfaceSupport.ProcessSelectedFaces(JOB, obj) + PSF.setShowDebugObjects(tempGroup, self.showDebugObjects) + PSF.radius = self.radius + PSF.depthParams = self.depthParams + pPM = PSF.preProcessModel(self.module) + # Process selected faces, if available + if pPM is False: + PathLog.error('Unable to pre-process obj.Base.') + else: + (FACES, VOIDS) = pPM + self.modelSTLs = PSF.modelSTLs + self.profileShapes = PSF.profileShapes + + # Create OCL.stl model objects + if obj.Algorithm == 'OCL Dropcutter': + self._prepareModelSTLs(JOB, obj) + PathLog.debug('obj.LinearDeflection.Value: {}'.format(obj.LinearDeflection.Value)) + PathLog.debug('obj.AngularDeflection.Value: {}'.format(obj.AngularDeflection.Value)) + + for m in range(0, len(JOB.Model.Group)): + Mdl = JOB.Model.Group[m] + if FACES[m] is False: + PathLog.error('No data for model base: {}'.format(JOB.Model.Group[m].Label)) + else: + if m > 0: + # Raise to clearance between models + CMDS.append(Path.Command('N (Transition to base: {}.)'.format(Mdl.Label))) + CMDS.append(Path.Command('G0', {'Z': obj.ClearanceHeight.Value, 'F': self.vertRapid})) + PathLog.info('Working on Model.Group[{}]: {}'.format(m, Mdl.Label)) + # make stock-model-voidShapes STL model for avoidance detection on transitions + if obj.Algorithm == 'OCL Dropcutter': + self._makeSafeSTL(JOB, obj, m, FACES[m], VOIDS[m]) + # Process model/faces - OCL objects must be ready + CMDS.extend(self._processWaterlineAreas(JOB, obj, m, FACES[m], VOIDS[m])) + + # Save gcode produced + self.commandlist.extend(CMDS) + + # ###### CLOSING COMMANDS FOR OPERATION ###### + + # Delete temporary objects + # Restore model visibilities for restoration + if FreeCAD.GuiUp: + FreeCADGui.ActiveDocument.getObject(tempGroupName).Visibility = False + for m in range(0, len(JOB.Model.Group)): + M = JOB.Model.Group[m] + M.Visibility = modelVisibility[m] + + if deleteTempsFlag is True: + for to in tempGroup.Group: + if hasattr(to, 'Group'): + for go in to.Group: + FCAD.removeObject(go.Name) + FCAD.removeObject(to.Name) + FCAD.removeObject(tempGroupName) + else: + if len(tempGroup.Group) == 0: + FCAD.removeObject(tempGroupName) + else: + tempGroup.purgeTouched() + + # Provide user feedback for gap sizes + gaps = list() + for g in self.gaps: + if g != self.toolDiam: + gaps.append(g) + if len(gaps) > 0: + obj.GapSizes = '{} mm'.format(gaps) + else: + if self.closedGap is True: + obj.GapSizes = 'Closed gaps < Gap Threshold.' + else: + obj.GapSizes = 'No gaps identified.' + + # clean up class variables + self.resetOpVariables() + self.deleteOpVariables() + + self.modelSTLs = None + self.safeSTLs = None + self.modelTypes = None + self.boundBoxes = None + self.gaps = None + self.closedGap = None + self.SafeHeightOffset = None + self.ClearHeightOffset = None + self.depthParams = None + self.midDep = None + del self.modelSTLs + del self.safeSTLs + del self.modelTypes + del self.boundBoxes + del self.gaps + del self.closedGap + del self.SafeHeightOffset + del self.ClearHeightOffset + del self.depthParams + del self.midDep + + execTime = time.time() - startTime + PathLog.info('Operation time: {} sec.'.format(execTime)) + + return True + + # Methods for constructing the cut area + def _prepareModelSTLs(self, JOB, obj): + PathLog.debug('_prepareModelSTLs()') + for m in range(0, len(JOB.Model.Group)): + M = JOB.Model.Group[m] + + # PathLog.debug(f" -self.modelTypes[{m}] == 'M'") + if self.modelTypes[m] == 'M': + # TODO: test if this works + facets = M.Mesh.Facets.Points + else: + facets = Part.getFacets(M.Shape) + + if self.modelSTLs[m] is True: + stl = ocl.STLSurf() + + for tri in facets: + t = ocl.Triangle(ocl.Point(tri[0][0], tri[0][1], tri[0][2]), + ocl.Point(tri[1][0], tri[1][1], tri[1][2]), + ocl.Point(tri[2][0], tri[2][1], tri[2][2])) + stl.addTriangle(t) + self.modelSTLs[m] = stl + return + + def _makeSafeSTL(self, JOB, obj, mdlIdx, faceShapes, voidShapes): + '''_makeSafeSTL(JOB, obj, mdlIdx, faceShapes, voidShapes)... + Creates and OCL.stl object with combined data with waste stock, + model, and avoided faces. Travel lines can be checked against this + STL object to determine minimum travel height to clear stock and model.''' + PathLog.debug('_makeSafeSTL()') + + fuseShapes = list() + Mdl = JOB.Model.Group[mdlIdx] + mBB = Mdl.Shape.BoundBox + sBB = JOB.Stock.Shape.BoundBox + + # add Model shape to safeSTL shape + fuseShapes.append(Mdl.Shape) + + if obj.BoundBox == 'BaseBoundBox': + cont = False + extFwd = (sBB.ZLength) + zmin = mBB.ZMin + zmax = mBB.ZMin + extFwd + stpDwn = (zmax - zmin) / 4.0 + dep_par = PathUtils.depth_params(zmax + 5.0, zmax + 3.0, zmax, stpDwn, 0.0, zmin) + + try: + envBB = PathUtils.getEnvelope(partshape=Mdl.Shape, depthparams=dep_par) # Produces .Shape + cont = True + except Exception as ee: + PathLog.error(str(ee)) + shell = Mdl.Shape.Shells[0] + solid = Part.makeSolid(shell) + try: + envBB = PathUtils.getEnvelope(partshape=solid, depthparams=dep_par) # Produces .Shape + cont = True + except Exception as eee: + PathLog.error(str(eee)) + + if cont: + stckWst = JOB.Stock.Shape.cut(envBB) + if obj.BoundaryAdjustment > 0.0: + cmpndFS = Part.makeCompound(faceShapes) + baBB = PathUtils.getEnvelope(partshape=cmpndFS, depthparams=self.depthParams) # Produces .Shape + adjStckWst = stckWst.cut(baBB) + else: + adjStckWst = stckWst + fuseShapes.append(adjStckWst) + else: + PathLog.warning('Path transitions might not avoid the model. Verify paths.') + else: + # If boundbox is Job.Stock, add hidden pad under stock as base plate + toolDiam = self.cutter.getDiameter() + zMin = JOB.Stock.Shape.BoundBox.ZMin + xMin = JOB.Stock.Shape.BoundBox.XMin - toolDiam + yMin = JOB.Stock.Shape.BoundBox.YMin - toolDiam + bL = JOB.Stock.Shape.BoundBox.XLength + (2 * toolDiam) + bW = JOB.Stock.Shape.BoundBox.YLength + (2 * toolDiam) + bH = 1.0 + crnr = FreeCAD.Vector(xMin, yMin, zMin - 1.0) + B = Part.makeBox(bL, bW, bH, crnr, FreeCAD.Vector(0, 0, 1)) + fuseShapes.append(B) + + if voidShapes is not False: + voidComp = Part.makeCompound(voidShapes) + voidEnv = PathUtils.getEnvelope(partshape=voidComp, depthparams=self.depthParams) # Produces .Shape + fuseShapes.append(voidEnv) + + fused = Part.makeCompound(fuseShapes) + + if self.showDebugObjects is True: + T = FreeCAD.ActiveDocument.addObject('Part::Feature', 'safeSTLShape') + T.Shape = fused + T.purgeTouched() + self.tempGroup.addObject(T) + + facets = Part.getFacets(fused) + + stl = ocl.STLSurf() + for tri in facets: + t = ocl.Triangle(ocl.Point(tri[0][0], tri[0][1], tri[0][2]), + ocl.Point(tri[1][0], tri[1][1], tri[1][2]), + ocl.Point(tri[2][0], tri[2][1], tri[2][2])) + stl.addTriangle(t) + + self.safeSTLs[mdlIdx] = stl + + def _processWaterlineAreas(self, JOB, obj, mdlIdx, FCS, VDS): + '''_processWaterlineAreas(JOB, obj, mdlIdx, FCS, VDS)... + This method applies any avoided faces or regions to the selected faces. + It then calls the correct method.''' + PathLog.debug('_processWaterlineAreas()') + + final = list() + + # Process faces Collectively or Individually + if obj.HandleMultipleFeatures == 'Collectively': + if FCS is True: + COMP = False + else: + ADD = Part.makeCompound(FCS) + if VDS is not False: + DEL = Part.makeCompound(VDS) + COMP = ADD.cut(DEL) + else: + COMP = ADD + + final.append(Path.Command('G0', {'Z': obj.SafeHeight.Value, 'F': self.vertRapid})) + if obj.Algorithm == 'OCL Dropcutter': + final.extend(self._oclWaterlineOp(JOB, obj, mdlIdx, COMP)) # independent method set for Waterline + else: + final.extend(self._experimentalWaterlineOp(JOB, obj, mdlIdx, COMP)) # independent method set for Waterline + + elif obj.HandleMultipleFeatures == 'Individually': + for fsi in range(0, len(FCS)): + fShp = FCS[fsi] + # self.deleteOpVariables(all=False) + self.resetOpVariables(all=False) + + if fShp is True: + COMP = False + else: + ADD = Part.makeCompound([fShp]) + if VDS is not False: + DEL = Part.makeCompound(VDS) + COMP = ADD.cut(DEL) + else: + COMP = ADD + + final.append(Path.Command('G0', {'Z': obj.SafeHeight.Value, 'F': self.vertRapid})) + if obj.Algorithm == 'OCL Dropcutter': + final.extend(self._oclWaterlineOp(JOB, obj, mdlIdx, COMP)) # independent method set for Waterline + else: + final.extend(self._experimentalWaterlineOp(JOB, obj, mdlIdx, COMP)) # independent method set for Waterline + COMP = None + # Eif + + return final + + # Methods for creating path geometry + def _getExperimentalWaterlinePaths(self, PNTSET, csHght, cutPattern): + '''_getExperimentalWaterlinePaths(PNTSET, csHght, cutPattern)... + Switching function for calling the appropriate path-geometry to OCL points conversion function + for the various cut patterns.''' + PathLog.debug('_getExperimentalWaterlinePaths()') + SCANS = list() + + if cutPattern in ['Line', 'Spiral']: + stpOvr = list() + for D in PNTSET: + for SEG in D: + if SEG == 'BRK': + stpOvr.append(SEG) + else: + # D format is ((p1, p2), (p3, p4)) + (A, B) = SEG + P1 = FreeCAD.Vector(A[0], A[1], csHght) + P2 = FreeCAD.Vector(B[0], B[1], csHght) + stpOvr.append((P1, P2)) + SCANS.append(stpOvr) + stpOvr = list() + elif cutPattern == 'ZigZag': + stpOvr = list() + for (dirFlg, LNS) in PNTSET: + for SEG in LNS: + if SEG == 'BRK': + stpOvr.append(SEG) + else: + # D format is ((p1, p2), (p3, p4)) + (A, B) = SEG + P1 = FreeCAD.Vector(A[0], A[1], csHght) + P2 = FreeCAD.Vector(B[0], B[1], csHght) + stpOvr.append((P1, P2)) + SCANS.append(stpOvr) + stpOvr = list() + elif cutPattern in ['Circular', 'CircularZigZag']: + # PNTSET is list, by stepover. + # Each stepover is a list containing arc/loop descriptions, (sp, ep, cp) + for so in range(0, len(PNTSET)): + stpOvr = list() + erFlg = False + (aTyp, dirFlg, ARCS) = PNTSET[so] + + if dirFlg == 1: # 1 + cMode = True # Climb mode + else: + cMode = False + + for a in range(0, len(ARCS)): + Arc = ARCS[a] + if Arc == 'BRK': + stpOvr.append('BRK') + else: + (sp, ep, cp) = Arc + S = FreeCAD.Vector(sp[0], sp[1], csHght) + E = FreeCAD.Vector(ep[0], ep[1], csHght) + C = FreeCAD.Vector(cp[0], cp[1], csHght) + scan = (S, E, C, cMode) + if scan is False: + erFlg = True + else: + ##if aTyp == 'L': + ## stpOvr.append(FreeCAD.Vector(scan[0][0].x, scan[0][0].y, scan[0][0].z)) + stpOvr.append(scan) + if erFlg is False: + SCANS.append(stpOvr) + + return SCANS + + # Main planar scan functions + def _stepTransitionCmds(self, obj, cutPattern, lstPnt, first, minSTH, tolrnc): + cmds = list() + rtpd = False + horizGC = 'G0' + hSpeed = self.horizRapid + height = obj.SafeHeight.Value + + if cutPattern in ['Line', 'Circular', 'Spiral']: + if obj.OptimizeStepOverTransitions is True: + height = minSTH + 2.0 + # if obj.LayerMode == 'Multi-pass': + # rtpd = minSTH + elif cutPattern in ['ZigZag', 'CircularZigZag']: + if obj.OptimizeStepOverTransitions is True: + zChng = first.z - lstPnt.z + # PathLog.debug('first.z: {}'.format(first.z)) + # PathLog.debug('lstPnt.z: {}'.format(lstPnt.z)) + # PathLog.debug('zChng: {}'.format(zChng)) + # PathLog.debug('minSTH: {}'.format(minSTH)) + if abs(zChng) < tolrnc: # transitions to same Z height + # PathLog.debug('abs(zChng) < tolrnc') + if (minSTH - first.z) > tolrnc: + # PathLog.debug('(minSTH - first.z) > tolrnc') + height = minSTH + 2.0 + else: + # PathLog.debug('ELSE (minSTH - first.z) > tolrnc') + horizGC = 'G1' + height = first.z + elif (minSTH + (2.0 * tolrnc)) >= max(first.z, lstPnt.z): + height = False # allow end of Zig to cut to beginning of Zag + + + # Create raise, shift, and optional lower commands + if height is not False: + cmds.append(Path.Command('G0', {'Z': height, 'F': self.vertRapid})) + cmds.append(Path.Command(horizGC, {'X': first.x, 'Y': first.y, 'F': hSpeed})) + if rtpd is not False: # ReturnToPreviousDepth + cmds.append(Path.Command('G0', {'Z': rtpd, 'F': self.vertRapid})) + + return cmds + + def _breakCmds(self, obj, cutPattern, lstPnt, first, minSTH, tolrnc): + cmds = list() + rtpd = False + horizGC = 'G0' + hSpeed = self.horizRapid + height = obj.SafeHeight.Value + + if cutPattern in ['Line', 'Circular', 'Spiral']: + if obj.OptimizeStepOverTransitions is True: + height = minSTH + 2.0 + elif cutPattern in ['ZigZag', 'CircularZigZag']: + if obj.OptimizeStepOverTransitions is True: + zChng = first.z - lstPnt.z + if abs(zChng) < tolrnc: # transitions to same Z height + if (minSTH - first.z) > tolrnc: + height = minSTH + 2.0 + else: + height = first.z + 2.0 # first.z + + cmds.append(Path.Command('G0', {'Z': height, 'F': self.vertRapid})) + cmds.append(Path.Command(horizGC, {'X': first.x, 'Y': first.y, 'F': hSpeed})) + if rtpd is not False: # ReturnToPreviousDepth + cmds.append(Path.Command('G0', {'Z': rtpd, 'F': self.vertRapid})) + + return cmds + + def _planarGetPDC(self, stl, finalDep, SampleInterval, cutter): + pdc = ocl.PathDropCutter() # create a pdc [PathDropCutter] object + pdc.setSTL(stl) # add stl model + pdc.setCutter(cutter) # add cutter + pdc.setZ(finalDep) # set minimumZ (final / target depth value) + pdc.setSampling(SampleInterval) # set sampling size + return pdc + + # OCL Dropcutter waterline functions + def _oclWaterlineOp(self, JOB, obj, mdlIdx, subShp=None): + '''_oclWaterlineOp(obj, base) ... Main waterline function to perform waterline extraction from model.''' + commands = [] + + base = JOB.Model.Group[mdlIdx] + bb = self.boundBoxes[mdlIdx] + stl = self.modelSTLs[mdlIdx] + depOfst = obj.DepthOffset.Value + + # Prepare global holdpoint and layerEndPnt containers + if self.holdPoint is None: + self.holdPoint = FreeCAD.Vector(0.0, 0.0, 0.0) + if self.layerEndPnt is None: + self.layerEndPnt = FreeCAD.Vector(0.0, 0.0, 0.0) + + # Set extra offset to diameter of cutter to allow cutter to move around perimeter of model + toolDiam = self.cutter.getDiameter() + + if subShp is None: + # Get correct boundbox + if obj.BoundBox == 'Stock': + BS = JOB.Stock + bb = BS.Shape.BoundBox + elif obj.BoundBox == 'BaseBoundBox': + BS = base + bb = base.Shape.BoundBox + + xmin = bb.XMin + xmax = bb.XMax + ymin = bb.YMin + ymax = bb.YMax + else: + xmin = subShp.BoundBox.XMin + xmax = subShp.BoundBox.XMax + ymin = subShp.BoundBox.YMin + ymax = subShp.BoundBox.YMax + + smplInt = obj.SampleInterval.Value + minSampInt = 0.001 # value is mm + if smplInt < minSampInt: + smplInt = minSampInt + + # Determine bounding box length for the OCL scan + bbLength = math.fabs(ymax - ymin) + numScanLines = int(math.ceil(bbLength / smplInt) + 1) # Number of lines + + # Compute number and size of stepdowns, and final depth + if obj.LayerMode == 'Single-pass': + depthparams = [obj.FinalDepth.Value] + else: + depthparams = [dp for dp in self.depthParams] + lenDP = len(depthparams) + + # Scan the piece to depth at smplInt + oclScan = [] + oclScan = self._waterlineDropCutScan(stl, smplInt, xmin, xmax, ymin, depthparams[lenDP - 1], numScanLines) + oclScan = [FreeCAD.Vector(P.x, P.y, P.z + depOfst) for P in oclScan] + lenOS = len(oclScan) + ptPrLn = int(lenOS / numScanLines) + + # Convert oclScan list of points to multi-dimensional list + scanLines = [] + for L in range(0, numScanLines): + scanLines.append([]) + for P in range(0, ptPrLn): + pi = L * ptPrLn + P + scanLines[L].append(oclScan[pi]) + lenSL = len(scanLines) + pntsPerLine = len(scanLines[0]) + PathLog.debug("--OCL scan: " + str(lenSL * pntsPerLine) + " points, with " + str(numScanLines) + " lines and " + str(pntsPerLine) + " pts/line") + + # Extract Wl layers per depthparams + lyr = 0 + cmds = [] + layTime = time.time() + self.topoMap = [] + for layDep in depthparams: + cmds = self._getWaterline(obj, scanLines, layDep, lyr, lenSL, pntsPerLine) + commands.extend(cmds) + lyr += 1 + PathLog.debug("--All layer scans combined took " + str(time.time() - layTime) + " s") + return commands + + def _waterlineDropCutScan(self, stl, smplInt, xmin, xmax, ymin, fd, numScanLines): + '''_waterlineDropCutScan(stl, smplInt, xmin, xmax, ymin, fd, numScanLines) ... + Perform OCL scan for waterline purpose.''' + pdc = ocl.PathDropCutter() # create a pdc + pdc.setSTL(stl) + pdc.setCutter(self.cutter) + pdc.setZ(fd) # set minimumZ (final / target depth value) + pdc.setSampling(smplInt) + + # Create line object as path + path = ocl.Path() # create an empty path object + for nSL in range(0, numScanLines): + yVal = ymin + (nSL * smplInt) + p1 = ocl.Point(xmin, yVal, fd) # start-point of line + p2 = ocl.Point(xmax, yVal, fd) # end-point of line + path.append(ocl.Line(p1, p2)) + # path.append(l) # add the line to the path + pdc.setPath(path) + pdc.run() # run drop-cutter on the path + + # return the list of points + return pdc.getCLPoints() + + def _getWaterline(self, obj, scanLines, layDep, lyr, lenSL, pntsPerLine): + '''_getWaterline(obj, scanLines, layDep, lyr, lenSL, pntsPerLine) ... Get waterline.''' + commands = [] + cmds = [] + loopList = [] + self.topoMap = [] + # Create topo map from scanLines (highs and lows) + self.topoMap = self._createTopoMap(scanLines, layDep, lenSL, pntsPerLine) + # Add buffer lines and columns to topo map + self._bufferTopoMap(lenSL, pntsPerLine) + # Identify layer waterline from OCL scan + self._highlightWaterline(4, 9) + # Extract waterline and convert to gcode + loopList = self._extractWaterlines(obj, scanLines, lyr, layDep) + # save commands + for loop in loopList: + cmds = self._loopToGcode(obj, layDep, loop) + commands.extend(cmds) + return commands + + def _createTopoMap(self, scanLines, layDep, lenSL, pntsPerLine): + '''_createTopoMap(scanLines, layDep, lenSL, pntsPerLine) ... Create topo map version of OCL scan data.''' + topoMap = [] + for L in range(0, lenSL): + topoMap.append([]) + for P in range(0, pntsPerLine): + if scanLines[L][P].z > layDep: + topoMap[L].append(2) + else: + topoMap[L].append(0) + return topoMap + + def _bufferTopoMap(self, lenSL, pntsPerLine): + '''_bufferTopoMap(lenSL, pntsPerLine) ... Add buffer boarder of zeros to all sides to topoMap data.''' + pre = [0, 0] + post = [0, 0] + for p in range(0, pntsPerLine): + pre.append(0) + post.append(0) + for l in range(0, lenSL): + self.topoMap[l].insert(0, 0) + self.topoMap[l].append(0) + self.topoMap.insert(0, pre) + self.topoMap.append(post) + return True + + def _highlightWaterline(self, extraMaterial, insCorn): + '''_highlightWaterline(extraMaterial, insCorn) ... Highlight the waterline data, separating from extra material.''' + TM = self.topoMap + lastPnt = len(TM[1]) - 1 + lastLn = len(TM) - 1 + highFlag = 0 + + # ("--Convert parallel data to ridges") + for lin in range(1, lastLn): + for pt in range(1, lastPnt): # Ignore first and last points + if TM[lin][pt] == 0: + if TM[lin][pt + 1] == 2: # step up + TM[lin][pt] = 1 + if TM[lin][pt - 1] == 2: # step down + TM[lin][pt] = 1 + + # ("--Convert perpendicular data to ridges and highlight ridges") + for pt in range(1, lastPnt): # Ignore first and last points + for lin in range(1, lastLn): + if TM[lin][pt] == 0: + highFlag = 0 + if TM[lin + 1][pt] == 2: # step up + TM[lin][pt] = 1 + if TM[lin - 1][pt] == 2: # step down + TM[lin][pt] = 1 + elif TM[lin][pt] == 2: + highFlag += 1 + if highFlag == 3: + if TM[lin - 1][pt - 1] < 2 or TM[lin - 1][pt + 1] < 2: + highFlag = 2 + else: + TM[lin - 1][pt] = extraMaterial + highFlag = 2 + + # ("--Square corners") + for pt in range(1, lastPnt): + for lin in range(1, lastLn): + if TM[lin][pt] == 1: # point == 1 + cont = True + if TM[lin + 1][pt] == 0: # forward == 0 + if TM[lin + 1][pt - 1] == 1: # forward left == 1 + if TM[lin][pt - 1] == 2: # left == 2 + TM[lin + 1][pt] = 1 # square the corner + cont = False + + if cont is True and TM[lin + 1][pt + 1] == 1: # forward right == 1 + if TM[lin][pt + 1] == 2: # right == 2 + TM[lin + 1][pt] = 1 # square the corner + cont = True + + if TM[lin - 1][pt] == 0: # back == 0 + if TM[lin - 1][pt - 1] == 1: # back left == 1 + if TM[lin][pt - 1] == 2: # left == 2 + TM[lin - 1][pt] = 1 # square the corner + cont = False + + if cont is True and TM[lin - 1][pt + 1] == 1: # back right == 1 + if TM[lin][pt + 1] == 2: # right == 2 + TM[lin - 1][pt] = 1 # square the corner + + # remove inside corners + for pt in range(1, lastPnt): + for lin in range(1, lastLn): + if TM[lin][pt] == 1: # point == 1 + if TM[lin][pt + 1] == 1: + if TM[lin - 1][pt + 1] == 1 or TM[lin + 1][pt + 1] == 1: + TM[lin][pt + 1] = insCorn + elif TM[lin][pt - 1] == 1: + if TM[lin - 1][pt - 1] == 1 or TM[lin + 1][pt - 1] == 1: + TM[lin][pt - 1] = insCorn + + return True + + def _extractWaterlines(self, obj, oclScan, lyr, layDep): + '''_extractWaterlines(obj, oclScan, lyr, layDep) ... Extract water lines from OCL scan data.''' + srch = True + lastPnt = len(self.topoMap[0]) - 1 + lastLn = len(self.topoMap) - 1 + maxSrchs = 5 + srchCnt = 1 + loopList = [] + loop = [] + loopNum = 0 + + if self.CutClimb is True: + lC = [-1, -1, -1, 0, 1, 1, 1, 0, -1, -1, -1, 0, 1, 1, 1, 0, -1, -1, -1, 0, 1, 1, 1, 0] + pC = [-1, 0, 1, 1, 1, 0, -1, -1, -1, 0, 1, 1, 1, 0, -1, -1, -1, 0, 1, 1, 1, 0, -1, -1] + else: + lC = [1, 1, 1, 0, -1, -1, -1, 0, 1, 1, 1, 0, -1, -1, -1, 0, 1, 1, 1, 0, -1, -1, -1, 0] + pC = [-1, 0, 1, 1, 1, 0, -1, -1, -1, 0, 1, 1, 1, 0, -1, -1, -1, 0, 1, 1, 1, 0, -1, -1] + + while srch is True: + srch = False + if srchCnt > maxSrchs: + PathLog.debug("Max search scans, " + str(maxSrchs) + " reached\nPossible incomplete waterline result!") + break + for L in range(1, lastLn): + for P in range(1, lastPnt): + if self.topoMap[L][P] == 1: + # start loop follow + srch = True + loopNum += 1 + loop = self._trackLoop(oclScan, lC, pC, L, P, loopNum) + self.topoMap[L][P] = 0 # Mute the starting point + loopList.append(loop) + srchCnt += 1 + PathLog.debug("Search count for layer " + str(lyr) + " is " + str(srchCnt) + ", with " + str(loopNum) + " loops.") + return loopList + + def _trackLoop(self, oclScan, lC, pC, L, P, loopNum): + '''_trackLoop(oclScan, lC, pC, L, P, loopNum) ... Track the loop direction.''' + loop = [oclScan[L - 1][P - 1]] # Start loop point list + cur = [L, P, 1] + prv = [L, P - 1, 1] + nxt = [L, P + 1, 1] + follow = True + ptc = 0 + ptLmt = 200000 + while follow is True: + ptc += 1 + if ptc > ptLmt: + PathLog.debug("Loop number " + str(loopNum) + " at [" + str(nxt[0]) + ", " + str(nxt[1]) + "] pnt count exceeds, " + str(ptLmt) + ". Stopped following loop.") + break + nxt = self._findNextWlPoint(lC, pC, cur[0], cur[1], prv[0], prv[1]) # get next point + loop.append(oclScan[nxt[0] - 1][nxt[1] - 1]) # add it to loop point list + self.topoMap[nxt[0]][nxt[1]] = nxt[2] # Mute the point, if not Y stem + if nxt[0] == L and nxt[1] == P: # check if loop complete + follow = False + elif nxt[0] == cur[0] and nxt[1] == cur[1]: # check if line cannot be detected + follow = False + prv = cur + cur = nxt + return loop + + def _findNextWlPoint(self, lC, pC, cl, cp, pl, pp): + '''_findNextWlPoint(lC, pC, cl, cp, pl, pp) ... + Find the next waterline point in the point cloud layer provided.''' + dl = cl - pl + dp = cp - pp + num = 0 + i = 3 + s = 0 + mtch = 0 + found = False + while mtch < 8: # check all 8 points around current point + if lC[i] == dl: + if pC[i] == dp: + s = i - 3 + found = True + # Check for y branch where current point is connection between branches + for y in range(1, mtch): + if lC[i + y] == dl: + if pC[i + y] == dp: + num = 1 + break + break + i += 1 + mtch += 1 + if found is False: + # ("_findNext: No start point found.") + return [cl, cp, num] + + for r in range(0, 8): + l = cl + lC[s + r] + p = cp + pC[s + r] + if self.topoMap[l][p] == 1: + return [l, p, num] + + # ("_findNext: No next pnt found") + return [cl, cp, num] + + def _loopToGcode(self, obj, layDep, loop): + '''_loopToGcode(obj, layDep, loop) ... Convert set of loop points to Gcode.''' + # generate the path commands + output = [] + + prev = FreeCAD.Vector(2135984513.165, -58351896873.17455, 13838638431.861) + nxt = FreeCAD.Vector(0.0, 0.0, 0.0) + + # Create first point + pnt = FreeCAD.Vector(loop[0].x, loop[0].y, layDep) + + # Position cutter to begin loop + output.append(Path.Command('G0', {'Z': obj.ClearanceHeight.Value, 'F': self.vertRapid})) + output.append(Path.Command('G0', {'X': pnt.x, 'Y': pnt.y, 'F': self.horizRapid})) + output.append(Path.Command('G1', {'Z': pnt.z, 'F': self.vertFeed})) + + lenCLP = len(loop) + lastIdx = lenCLP - 1 + # Cycle through each point on loop + for i in range(0, lenCLP): + if i < lastIdx: + nxt.x = loop[i + 1].x + nxt.y = loop[i + 1].y + nxt.z = layDep + + output.append(Path.Command('G1', {'X': pnt.x, 'Y': pnt.y, 'F': self.horizFeed})) + + # Rotate point data + prev = pnt + pnt = nxt + + # Save layer end point for use in transitioning to next layer + self.layerEndPnt = pnt + + return output + + # Experimental waterline functions + def _experimentalWaterlineOp(self, JOB, obj, mdlIdx, subShp=None): + '''_waterlineOp(JOB, obj, mdlIdx, subShp=None) ... + Main waterline function to perform waterline extraction from model.''' + PathLog.debug('_experimentalWaterlineOp()') + + commands = [] + t_begin = time.time() + base = JOB.Model.Group[mdlIdx] + # bb = self.boundBoxes[mdlIdx] + # stl = self.modelSTLs[mdlIdx] + # safeSTL = self.safeSTLs[mdlIdx] + self.endVector = None + + finDep = obj.FinalDepth.Value + (self.geoTlrnc / 10.0) + depthParams = PathUtils.depth_params(obj.ClearanceHeight.Value, obj.SafeHeight.Value, obj.StartDepth.Value, obj.StepDown.Value, 0.0, finDep) + + # Compute number and size of stepdowns, and final depth + if obj.LayerMode == 'Single-pass': + depthparams = [finDep] + else: + depthparams = [dp for dp in depthParams] + PathLog.debug('Experimental Waterline depthparams:\n{}'.format(depthparams)) + + # Prepare PathDropCutter objects with STL data + # safePDC = self._planarGetPDC(safeSTL, depthparams[lenDP - 1], obj.SampleInterval.Value, self.cutter) + + buffer = self.cutter.getDiameter() * 10.0 + borderFace = Part.Face(self._makeExtendedBoundBox(JOB.Stock.Shape.BoundBox, buffer, 0.0)) + + # Get correct boundbox + if obj.BoundBox == 'Stock': + stockEnv = PathSurfaceSupport.getShapeEnvelope(JOB.Stock.Shape) + bbFace = PathSurfaceSupport.getCrossSection(stockEnv) # returned at Z=0.0 + elif obj.BoundBox == 'BaseBoundBox': + baseEnv = PathSurfaceSupport.getShapeEnvelope(base.Shape) + bbFace = PathSurfaceSupport.getCrossSection(baseEnv) # returned at Z=0.0 + + trimFace = borderFace.cut(bbFace) + if self.showDebugObjects is True: + TF = FreeCAD.ActiveDocument.addObject('Part::Feature', 'trimFace') + TF.Shape = trimFace + TF.purgeTouched() + self.tempGroup.addObject(TF) + + # Cycle through layer depths + CUTAREAS = self._getCutAreas(base.Shape, depthparams, bbFace, trimFace, borderFace) + if not CUTAREAS: + PathLog.error('No cross-section cut areas identified.') + return commands + + caCnt = 0 + ofst = obj.BoundaryAdjustment.Value + ofst -= self.radius # (self.radius + (tolrnc / 10.0)) + caLen = len(CUTAREAS) + lastCA = caLen - 1 + lastClearArea = None + lastCsHght = None + clearLastLayer = True + for ca in range(0, caLen): + area = CUTAREAS[ca] + csHght = area.BoundBox.ZMin + csHght += obj.DepthOffset.Value + cont = False + caCnt += 1 + if area.Area > 0.0: + cont = True + caWireCnt = len(area.Wires) - 1 # first wire is boundFace wire + if self.showDebugObjects: + CA = FreeCAD.ActiveDocument.addObject('Part::Feature', 'cutArea_{}'.format(caCnt)) + CA.Shape = area + CA.purgeTouched() + self.tempGroup.addObject(CA) + else: + data = FreeCAD.Units.Quantity(csHght, FreeCAD.Units.Length).UserString + PathLog.debug('Cut area at {} is zero.'.format(data)) + + # get offset wire(s) based upon cross-section cut area + if cont: + area.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - area.BoundBox.ZMin)) + activeArea = area.cut(trimFace) + activeAreaWireCnt = len(activeArea.Wires) # first wire is boundFace wire + if self.showDebugObjects: + CA = FreeCAD.ActiveDocument.addObject('Part::Feature', 'activeArea_{}'.format(caCnt)) + CA.Shape = activeArea + CA.purgeTouched() + self.tempGroup.addObject(CA) + ofstArea = PathSurfaceSupport.extractFaceOffset(activeArea, ofst, self.wpc, makeComp=False) + if not ofstArea: + data = FreeCAD.Units.Quantity(csHght, FreeCAD.Units.Length).UserString + PathLog.debug('No offset area returned for cut area depth at {}.'.format(data)) + cont = False + + if cont: + # Identify solid areas in the offset data + ofstSolidFacesList = self._getSolidAreasFromPlanarFaces(ofstArea) + if ofstSolidFacesList: + clearArea = Part.makeCompound(ofstSolidFacesList) + if self.showDebugObjects is True: + CA = FreeCAD.ActiveDocument.addObject('Part::Feature', 'clearArea_{}'.format(caCnt)) + CA.Shape = clearArea + CA.purgeTouched() + self.tempGroup.addObject(CA) + else: + cont = False + data = FreeCAD.Units.Quantity(csHght, FreeCAD.Units.Length).UserString + PathLog.error('Could not determine solid faces at {}.'.format(data)) + + if cont: + # Make waterline path for current CUTAREA depth (csHght) + commands.extend(self._wiresToWaterlinePath(obj, clearArea, csHght)) + clearArea.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - clearArea.BoundBox.ZMin)) + lastClearArea = clearArea + lastCsHght = csHght + + # Clear layer as needed + (clrLyr, clearLastLayer) = self._clearLayer(obj, ca, lastCA, clearLastLayer) + if clrLyr == 'Offset': + commands.extend(self._makeOffsetLayerPaths(obj, clearArea, csHght)) + elif clrLyr: + cutPattern = obj.CutPattern + if clearLastLayer is False: + cutPattern = obj.ClearLastLayer + commands.extend(self._makeCutPatternLayerPaths(JOB, obj, clearArea, csHght, cutPattern)) + # Efor + + if clearLastLayer: + (clrLyr, cLL) = self._clearLayer(obj, 1, 1, False) + lastClearArea.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - lastClearArea.BoundBox.ZMin)) + if clrLyr == 'Offset': + commands.extend(self._makeOffsetLayerPaths(obj, lastClearArea, lastCsHght)) + elif clrLyr: + commands.extend(self._makeCutPatternLayerPaths(JOB, obj, lastClearArea, lastCsHght, obj.ClearLastLayer)) + + PathLog.info("Waterline: All layer scans combined took " + str(time.time() - t_begin) + " s") + return commands + + def _getCutAreas(self, shape, depthparams, bbFace, trimFace, borderFace): + '''_getCutAreas(JOB, shape, depthparams, bbFace, borderFace) ... + Takes shape, depthparams and base-envelope-cross-section, and + returns a list of cut areas - one for each depth.''' + PathLog.debug('_getCutAreas()') + + CUTAREAS = list() + isFirst = True + lenDP = len(depthparams) + + # Cycle through layer depths + for dp in range(0, lenDP): + csHght = depthparams[dp] + # PathLog.debug('Depth {} is {}'.format(dp + 1, csHght)) + + # Get slice at depth of shape + csFaces = self._getModelCrossSection(shape, csHght) # returned at Z=0.0 + if not csFaces: + data = FreeCAD.Units.Quantity(csHght, FreeCAD.Units.Length).UserString + else: + if len(csFaces) > 0: + useFaces = self._getSolidAreasFromPlanarFaces(csFaces) + else: + useFaces = False + + if useFaces: + compAdjFaces = Part.makeCompound(useFaces) + + if self.showDebugObjects is True: + CA = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpSolids_{}'.format(dp + 1)) + CA.Shape = compAdjFaces + CA.purgeTouched() + self.tempGroup.addObject(CA) + + if isFirst: + allPrevComp = compAdjFaces + cutArea = borderFace.cut(compAdjFaces) + else: + preCutArea = borderFace.cut(compAdjFaces) + cutArea = preCutArea.cut(allPrevComp) # cut out higher layers to avoid cutting recessed areas + allPrevComp = allPrevComp.fuse(compAdjFaces) + cutArea.translate(FreeCAD.Vector(0.0, 0.0, csHght - cutArea.BoundBox.ZMin)) + CUTAREAS.append(cutArea) + isFirst = False + else: + PathLog.error('No waterline at depth: {} mm.'.format(csHght)) + # Efor + + if len(CUTAREAS) > 0: + return CUTAREAS + + return False + + def _wiresToWaterlinePath(self, obj, ofstPlnrShp, csHght): + PathLog.debug('_wiresToWaterlinePath()') + commands = list() + + # Translate path geometry to layer height + ofstPlnrShp.translate(FreeCAD.Vector(0.0, 0.0, csHght - ofstPlnrShp.BoundBox.ZMin)) + if self.showDebugObjects is True: + OA = FreeCAD.ActiveDocument.addObject('Part::Feature', 'waterlinePathArea_{}'.format(round(csHght, 2))) + OA.Shape = ofstPlnrShp + OA.purgeTouched() + self.tempGroup.addObject(OA) + + commands.append(Path.Command('N (Cut Area {}.)'.format(round(csHght, 2)))) + start = 1 + if csHght < obj.IgnoreOuterAbove: + start = 0 + for w in range(start, len(ofstPlnrShp.Wires)): + wire = ofstPlnrShp.Wires[w] + V = wire.Vertexes + if obj.CutMode == 'Climb': + lv = len(V) - 1 + startVect = FreeCAD.Vector(V[lv].X, V[lv].Y, V[lv].Z) + else: + startVect = FreeCAD.Vector(V[0].X, V[0].Y, V[0].Z) + + commands.append(Path.Command('N (Wire {}.)'.format(w))) + (cmds, endVect) = self._wireToPath(obj, wire, startVect) + commands.extend(cmds) + commands.append(Path.Command('G0', {'Z': obj.SafeHeight.Value, 'F': self.vertRapid})) + + return commands + + def _makeCutPatternLayerPaths(self, JOB, obj, clrAreaShp, csHght, cutPattern): + PathLog.debug('_makeCutPatternLayerPaths()') + commands = [] + + clrAreaShp.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - clrAreaShp.BoundBox.ZMin)) + + # Convert pathGeom to gcode more efficiently + if cutPattern == 'Offset': + commands.extend(self._makeOffsetLayerPaths(obj, clrAreaShp, csHght)) + else: + # Request path geometry from external support class + PGG = PathSurfaceSupport.PathGeometryGenerator(obj, clrAreaShp, cutPattern) + if self.showDebugObjects: + PGG.setDebugObjectsGroup(self.tempGroup) + self.tmpCOM = PGG.getCenterOfPattern() + pathGeom = PGG.generatePathGeometry() + if not pathGeom: + PathLog.warning('No path geometry generated.') + return commands + pathGeom.translate(FreeCAD.Vector(0.0, 0.0, csHght - pathGeom.BoundBox.ZMin)) + + if self.showDebugObjects is True: + OA = FreeCAD.ActiveDocument.addObject('Part::Feature', 'pathGeom_{}'.format(round(csHght, 2))) + OA.Shape = pathGeom + OA.purgeTouched() + self.tempGroup.addObject(OA) + + if cutPattern == 'Line': + pntSet = PathSurfaceSupport.pathGeomToLinesPointSet(obj, pathGeom, self.CutClimb, self.toolDiam, self.closedGap, self.gaps) + elif cutPattern == 'ZigZag': + pntSet = PathSurfaceSupport.pathGeomToZigzagPointSet(obj, pathGeom, self.CutClimb, self.toolDiam, self.closedGap, self.gaps) + elif cutPattern in ['Circular', 'CircularZigZag']: + pntSet = PathSurfaceSupport.pathGeomToCircularPointSet(obj, pathGeom, self.CutClimb, self.toolDiam, self.closedGap, self.gaps, self.tmpCOM) + elif cutPattern == 'Spiral': + pntSet = PathSurfaceSupport.pathGeomToSpiralPointSet(obj, pathGeom) + + stpOVRS = self._getExperimentalWaterlinePaths(pntSet, csHght, cutPattern) + safePDC = False + cmds = self._clearGeomToPaths(JOB, obj, safePDC, stpOVRS, cutPattern) + commands.extend(cmds) + + return commands + + def _makeOffsetLayerPaths(self, obj, clrAreaShp, csHght): + PathLog.debug('_makeOffsetLayerPaths()') + cmds = list() + ofst = 0.0 - self.cutOut + shape = clrAreaShp + cont = True + cnt = 0 + while cont: + ofstArea = PathSurfaceSupport.extractFaceOffset(shape, ofst, self.wpc, makeComp=True) + if not ofstArea: + break + for F in ofstArea.Faces: + cmds.extend(self._wiresToWaterlinePath(obj, F, csHght)) + shape = ofstArea + if cnt == 0: + ofst = 0.0 - self.cutOut + cnt += 1 + return cmds + + def _clearGeomToPaths(self, JOB, obj, safePDC, stpOVRS, cutPattern): + PathLog.debug('_clearGeomToPaths()') + + GCODE = [Path.Command('N (Beginning of Single-pass layer.)', {})] + tolrnc = JOB.GeometryTolerance.Value + lenstpOVRS = len(stpOVRS) + lstSO = lenstpOVRS - 1 + lstStpOvr = False + gDIR = ['G3', 'G2'] + + if self.CutClimb is True: + gDIR = ['G2', 'G3'] + + # Send cutter to x,y position of first point on first line + first = stpOVRS[0][0][0] # [step][item][point] + GCODE.append(Path.Command('G0', {'X': first.x, 'Y': first.y, 'F': self.horizRapid})) + + # Cycle through step-over sections (line segments or arcs) + odd = True + lstStpEnd = None + for so in range(0, lenstpOVRS): + cmds = list() + PRTS = stpOVRS[so] + lenPRTS = len(PRTS) + first = PRTS[0][0] # first point of arc/line stepover group + last = None + cmds.append(Path.Command('N (Begin step {}.)'.format(so), {})) + if so == lstSO: + lstStpOvr = True + + if so > 0: + if cutPattern == 'CircularZigZag': + if odd: + odd = False + else: + odd = True + # minTrnsHght = self._getMinSafeTravelHeight(safePDC, lstStpEnd, first) # Check safe travel height against fullSTL + minTrnsHght = obj.SafeHeight.Value + # cmds.append(Path.Command('N (Transition: last, first: {}, {}: minSTH: {})'.format(lstStpEnd, first, minTrnsHght), {})) + cmds.extend(self._stepTransitionCmds(obj, cutPattern, lstStpEnd, first, minTrnsHght, tolrnc)) + + # Cycle through current step-over parts + for i in range(0, lenPRTS): + prt = PRTS[i] + # PathLog.debug('prt: {}'.format(prt)) + if prt == 'BRK': + nxtStart = PRTS[i + 1][0] + # minSTH = self._getMinSafeTravelHeight(safePDC, last, nxtStart) # Check safe travel height against fullSTL + minSTH = obj.SafeHeight.Value + cmds.append(Path.Command('N (Break)', {})) + cmds.extend(self._breakCmds(obj, cutPattern, last, nxtStart, minSTH, tolrnc)) + else: + cmds.append(Path.Command('N (part {}.)'.format(i + 1), {})) + if cutPattern in ['Line', 'ZigZag', 'Spiral']: + start, last = prt + cmds.append(Path.Command('G1', {'X': start.x, 'Y': start.y, 'Z': start.z, 'F': self.horizFeed})) + cmds.append(Path.Command('G1', {'X': last.x, 'Y': last.y, 'F': self.horizFeed})) + elif cutPattern in ['Circular', 'CircularZigZag']: + # isCircle = True if lenPRTS == 1 else False + isZigZag = True if cutPattern == 'CircularZigZag' else False + PathLog.debug('so, isZigZag, odd, cMode: {}, {}, {}, {}'.format(so, isZigZag, odd, prt[3])) + gcode = self._makeGcodeArc(prt, gDIR, odd, isZigZag) + cmds.extend(gcode) + cmds.append(Path.Command('N (End of step {}.)'.format(so), {})) + GCODE.extend(cmds) # save line commands + lstStpEnd = last + # Efor + + # Raise to safe height after clearing + GCODE.append(Path.Command('G0', {'Z': obj.SafeHeight.Value, 'F': self.vertRapid})) + + return GCODE + + def _getSolidAreasFromPlanarFaces(self, csFaces): + PathLog.debug('_getSolidAreasFromPlanarFaces()') + holds = list() + useFaces = list() + lenCsF = len(csFaces) + PathLog.debug('lenCsF: {}'.format(lenCsF)) + + if lenCsF == 1: + useFaces = csFaces + else: + fIds = list() + aIds = list() + pIds = list() + cIds = list() + + for af in range(0, lenCsF): + fIds.append(af) # face ids + aIds.append(af) # face ids + pIds.append(-1) # parent ids + cIds.append(False) # cut ids + holds.append(False) + + while len(fIds) > 0: + li = fIds.pop() + low = csFaces[li] # senior face + pIds = self._idInternalFeature(csFaces, fIds, pIds, li, low) + + for af in range(lenCsF - 1, -1, -1): # cycle from last item toward first + prnt = pIds[af] + if prnt == -1: + stack = -1 + else: + stack = [af] + # get_face_ids_to_parent + stack.insert(0, prnt) + nxtPrnt = pIds[prnt] + # find af value for nxtPrnt + while nxtPrnt != -1: + stack.insert(0, nxtPrnt) + nxtPrnt = pIds[nxtPrnt] + cIds[af] = stack + + for af in range(0, lenCsF): + pFc = cIds[af] + if pFc == -1: + # Simple, independent region + holds[af] = csFaces[af] # place face in hold + else: + # Compound region + cnt = len(pFc) + if cnt % 2.0 == 0.0: + # even is donut cut + inr = pFc[cnt - 1] + otr = pFc[cnt - 2] + holds[otr] = holds[otr].cut(csFaces[inr]) + else: + # odd is floating solid + holds[af] = csFaces[af] + + for af in range(0, lenCsF): + if holds[af]: + useFaces.append(holds[af]) # save independent solid + # Eif + + if len(useFaces) > 0: + return useFaces + + return False + + def _getModelCrossSection(self, shape, csHght): + PathLog.debug('getCrossSection()') + wires = list() + + def byArea(fc): + return fc.Area + + for i in shape.slice(FreeCAD.Vector(0, 0, 1), csHght): + wires.append(i) + + if len(wires) > 0: + for w in wires: + if w.isClosed() is False: + return False + FCS = list() + for w in wires: + w.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - w.BoundBox.ZMin)) + FCS.append(Part.Face(w)) + FCS.sort(key=byArea, reverse=True) + return FCS + else: + PathLog.debug(' -No wires from .slice() method') + + return False + + def _isInBoundBox(self, outShp, inShp): + obb = outShp.BoundBox + ibb = inShp.BoundBox + + if obb.XMin < ibb.XMin: + if obb.XMax > ibb.XMax: + if obb.YMin < ibb.YMin: + if obb.YMax > ibb.YMax: + return True + return False + + def _idInternalFeature(self, csFaces, fIds, pIds, li, low): + Ids = list() + for i in fIds: + Ids.append(i) + while len(Ids) > 0: + hi = Ids.pop() + high = csFaces[hi] + if self._isInBoundBox(high, low): + cmn = high.common(low) + if cmn.Area > 0.0: + pIds[li] = hi + break + + return pIds + + def _wireToPath(self, obj, wire, startVect): + '''_wireToPath(obj, wire, startVect) ... wire to path.''' + PathLog.track() + + paths = [] + pathParams = {} # pylint: disable=assignment-from-no-return + + pathParams['shapes'] = [wire] + pathParams['feedrate'] = self.horizFeed + pathParams['feedrate_v'] = self.vertFeed + pathParams['verbose'] = True + pathParams['resume_height'] = obj.SafeHeight.Value + pathParams['retraction'] = obj.ClearanceHeight.Value + pathParams['return_end'] = True + # Note that emitting preambles between moves breaks some dressups and prevents path optimization on some controllers + pathParams['preamble'] = False + pathParams['start'] = startVect + + (pp, end_vector) = Path.fromShapes(**pathParams) + paths.extend(pp.Commands) + + self.endVector = end_vector # pylint: disable=attribute-defined-outside-init + + return (paths, end_vector) + + def _makeExtendedBoundBox(self, wBB, bbBfr, zDep): + pl = FreeCAD.Placement() + pl.Rotation = FreeCAD.Rotation(FreeCAD.Vector(0, 0, 1), 0) + pl.Base = FreeCAD.Vector(0, 0, 0) + + p1 = FreeCAD.Vector(wBB.XMin - bbBfr, wBB.YMin - bbBfr, zDep) + p2 = FreeCAD.Vector(wBB.XMax + bbBfr, wBB.YMin - bbBfr, zDep) + p3 = FreeCAD.Vector(wBB.XMax + bbBfr, wBB.YMax + bbBfr, zDep) + p4 = FreeCAD.Vector(wBB.XMin - bbBfr, wBB.YMax + bbBfr, zDep) + bb = Part.makePolygon([p1, p2, p3, p4, p1]) + + return bb + + def _makeGcodeArc(self, prt, gDIR, odd, isZigZag): + cmds = list() + strtPnt, endPnt, cntrPnt, cMode = prt + gdi = 0 + if odd: + gdi = 1 + else: + if not cMode and isZigZag: + gdi = 1 + gCmd = gDIR[gdi] + + # ijk = self.tmpCOM - strtPnt + # ijk = self.tmpCOM.sub(strtPnt) # vector from start to center + ijk = cntrPnt.sub(strtPnt) # vector from start to center + xyz = endPnt + cmds.append(Path.Command('G1', {'X': strtPnt.x, 'Y': strtPnt.y, 'Z': strtPnt.z, 'F': self.horizFeed})) + cmds.append(Path.Command(gCmd, {'X': xyz.x, 'Y': xyz.y, 'Z': xyz.z, + 'I': ijk.x, 'J': ijk.y, 'K': ijk.z, # leave same xyz.z height + 'F': self.horizFeed})) + cmds.append(Path.Command('G1', {'X': endPnt.x, 'Y': endPnt.y, 'Z': endPnt.z, 'F': self.horizFeed})) + + return cmds + + def _clearLayer(self, obj, ca, lastCA, clearLastLayer): + PathLog.debug('_clearLayer()') + clrLyr = False + + if obj.ClearLastLayer == 'Off': + if obj.CutPattern != 'None': + clrLyr = obj.CutPattern + else: + obj.CutPattern = 'None' + if ca == lastCA: # if current iteration is last layer + PathLog.debug('... Clearing bottom layer.') + clrLyr = obj.ClearLastLayer + clearLastLayer = False + + return (clrLyr, clearLastLayer) + + # Support methods + def resetOpVariables(self, all=True): + '''resetOpVariables() ... Reset class variables used for instance of operation.''' + self.holdPoint = None + self.layerEndPnt = None + self.onHold = False + self.SafeHeightOffset = 2.0 + self.ClearHeightOffset = 4.0 + self.layerEndzMax = 0.0 + self.resetTolerance = 0.0 + self.holdPntCnt = 0 + self.bbRadius = 0.0 + self.axialFeed = 0.0 + self.axialRapid = 0.0 + self.FinalDepth = 0.0 + self.clearHeight = 0.0 + self.safeHeight = 0.0 + self.faceZMax = -999999999999.0 + if all is True: + self.cutter = None + self.stl = None + self.fullSTL = None + self.cutOut = 0.0 + self.radius = 0.0 + self.useTiltCutter = False + return True + + def deleteOpVariables(self, all=True): + '''deleteOpVariables() ... Reset class variables used for instance of operation.''' + del self.holdPoint + del self.layerEndPnt + del self.onHold + del self.SafeHeightOffset + del self.ClearHeightOffset + del self.layerEndzMax + del self.resetTolerance + del self.holdPntCnt + del self.bbRadius + del self.axialFeed + del self.axialRapid + del self.FinalDepth + del self.clearHeight + del self.safeHeight + del self.faceZMax + if all is True: + del self.cutter + del self.stl + del self.fullSTL + del self.cutOut + del self.radius + del self.useTiltCutter + return True + + def setOclCutter(self, obj, safe=False): + ''' setOclCutter(obj) ... Translation function to convert FreeCAD tool definition to OCL formatted tool. ''' + # Set cutter details + # https://www.freecadweb.org/api/dd/dfe/classPath_1_1Tool.html#details + diam_1 = float(obj.ToolController.Tool.Diameter) + lenOfst = obj.ToolController.Tool.LengthOffset if hasattr(obj.ToolController.Tool, 'LengthOffset') else 0 + FR = obj.ToolController.Tool.FlatRadius if hasattr(obj.ToolController.Tool, 'FlatRadius') else 0 + CEH = obj.ToolController.Tool.CuttingEdgeHeight if hasattr(obj.ToolController.Tool, 'CuttingEdgeHeight') else 0 + CEA = obj.ToolController.Tool.CuttingEdgeAngle if hasattr(obj.ToolController.Tool, 'CuttingEdgeAngle') else 0 + + # Make safeCutter with 2 mm buffer around physical cutter + if safe is True: + diam_1 += 4.0 + if FR != 0.0: + FR += 2.0 + + PathLog.debug('ToolType: {}'.format(obj.ToolController.Tool.ToolType)) + if obj.ToolController.Tool.ToolType == 'EndMill': + # Standard End Mill + return ocl.CylCutter(diam_1, (CEH + lenOfst)) + + elif obj.ToolController.Tool.ToolType == 'BallEndMill' and FR == 0.0: + # Standard Ball End Mill + # OCL -> BallCutter::BallCutter(diameter, length) + self.useTiltCutter = True + return ocl.BallCutter(diam_1, (diam_1 / 2 + lenOfst)) + + elif obj.ToolController.Tool.ToolType == 'BallEndMill' and FR > 0.0: + # Bull Nose or Corner Radius cutter + # Reference: https://www.fine-tools.com/halbstabfraeser.html + # OCL -> BallCutter::BallCutter(diameter, length) + return ocl.BullCutter(diam_1, FR, (CEH + lenOfst)) + + elif obj.ToolController.Tool.ToolType == 'Engraver' and FR > 0.0: + # Bull Nose or Corner Radius cutter + # Reference: https://www.fine-tools.com/halbstabfraeser.html + # OCL -> ConeCutter::ConeCutter(diameter, angle, lengthOffset) + return ocl.ConeCutter(diam_1, (CEA / 2), lenOfst) + + elif obj.ToolController.Tool.ToolType == 'ChamferMill': + # Bull Nose or Corner Radius cutter + # Reference: https://www.fine-tools.com/halbstabfraeser.html + # OCL -> ConeCutter::ConeCutter(diameter, angle, lengthOffset) + return ocl.ConeCutter(diam_1, (CEA / 2), lenOfst) + else: + # Default to standard end mill + PathLog.warning("Defaulting cutter to standard end mill.") + return ocl.CylCutter(diam_1, (CEH + lenOfst)) + + +def SetupProperties(): + ''' SetupProperties() ... Return list of properties required for operation.''' + setup = ['Algorithm', 'AvoidLastX_Faces', 'AvoidLastX_InternalFeatures', 'BoundBox'] + setup.extend(['BoundaryAdjustment', 'PatternCenterAt', 'PatternCenterCustom']) + setup.extend(['ClearLastLayer', 'InternalFeaturesCut', 'InternalFeaturesAdjustment']) + setup.extend(['CutMode', 'CutPattern', 'CutPatternAngle', 'CutPatternReversed']) + setup.extend(['DepthOffset', 'GapSizes', 'GapThreshold', 'StepOver']) + setup.extend(['HandleMultipleFeatures', 'LayerMode', 'OptimizeStepOverTransitions']) + setup.extend(['BoundaryEnforcement', 'SampleInterval', 'StartPoint', 'IgnoreOuterAbove']) + setup.extend(['UseStartPoint', 'AngularDeflection', 'LinearDeflection', 'ShowTempObjects']) + return setup + + +def Create(name, obj=None): + '''Create(name) ... Creates and returns a Waterline operation.''' + if obj is None: + obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython", name) + obj.Proxy = ObjectWaterline(obj, name) + return obj diff --git a/src/Mod/Path/PathScripts/PathWaterlineGui.py b/src/Mod/Path/PathScripts/PathWaterlineGui.py index eed15fc3d3..0616bbe6d2 100644 --- a/src/Mod/Path/PathScripts/PathWaterlineGui.py +++ b/src/Mod/Path/PathScripts/PathWaterlineGui.py @@ -90,6 +90,8 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage): else: self.form.optimizeEnabled.setCheckState(QtCore.Qt.Unchecked) + self.updateVisibility() + def getSignalsForUpdate(self, obj): '''getSignalsForUpdate(obj) ... return list of signals for updating obj''' signals = [] @@ -106,21 +108,32 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage): return signals def updateVisibility(self): - if self.form.algorithmSelect.currentText() == 'OCL Dropcutter': - self.form.cutPattern.setEnabled(False) - self.form.boundaryAdjustment.setEnabled(False) - self.form.stepOver.setEnabled(False) - self.form.sampleInterval.setEnabled(True) - self.form.optimizeEnabled.setEnabled(True) - else: - self.form.cutPattern.setEnabled(True) - self.form.boundaryAdjustment.setEnabled(True) + '''updateVisibility()... Updates visibility of Tasks panel objects.''' + Algorithm = self.form.algorithmSelect.currentText() + self.form.optimizeEnabled.hide() # Has no independent QLabel object + + if Algorithm == 'OCL Dropcutter': + self.form.cutPattern.hide() + self.form.cutPattern_label.hide() + self.form.boundaryAdjustment.hide() + self.form.boundaryAdjustment_label.hide() + self.form.stepOver.hide() + self.form.stepOver_label.hide() + self.form.sampleInterval.show() + self.form.sampleInterval_label.show() + elif Algorithm == 'Experimental': + self.form.cutPattern.show() + self.form.boundaryAdjustment.show() + self.form.cutPattern_label.show() + self.form.boundaryAdjustment_label.show() if self.form.cutPattern.currentText() == 'None': - self.form.stepOver.setEnabled(False) + self.form.stepOver.hide() + self.form.stepOver_label.hide() else: - self.form.stepOver.setEnabled(True) - self.form.sampleInterval.setEnabled(False) - self.form.optimizeEnabled.setEnabled(False) + self.form.stepOver.show() + self.form.stepOver_label.show() + self.form.sampleInterval.hide() + self.form.sampleInterval_label.hide() def registerSignalHandlers(self, obj): self.form.algorithmSelect.currentIndexChanged.connect(self.updateVisibility) diff --git a/src/Mod/Path/PathScripts/PostUtils.py b/src/Mod/Path/PathScripts/PostUtils.py index 26f6ba491e..5acadafc79 100644 --- a/src/Mod/Path/PathScripts/PostUtils.py +++ b/src/Mod/Path/PathScripts/PostUtils.py @@ -78,7 +78,6 @@ class GCodeEditorDialog(QtGui.QDialog): font.setPointSize(10) self.editor.setFont(font) self.editor.setText("G01 X55 Y4.5 F300.0") - self.highlighter = GCodeHighlighter(self.editor.document()) layout.addWidget(self.editor) # OK and Cancel buttons @@ -134,8 +133,23 @@ def fmt(num,dec,units): def editor(gcode): '''pops up a handy little editor to look at the code output ''' + + prefs = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Path") + # default Max Highlighter Size = 512 Ko + defaultMHS = 512 * 1024 + mhs = prefs.GetUnsigned('inspecteditorMaxHighlighterSize', defaultMHS) + dia = GCodeEditorDialog() dia.editor.setText(gcode) + gcodeSize = len(dia.editor.toPlainText()) + if (gcodeSize <= mhs): + # because of poor performance, syntax highlighting is + # limited to mhs octets (default 512 KB). + # It seems than the response time curve has an inflexion near 500 KB + # beyond 500 KB, the response time increases exponentially. + dia.highlighter = GCodeHighlighter(dia.editor.document()) + else: + FreeCAD.Console.PrintMessage(translate("Path", "GCode size too big ({} o), disabling syntax highlighter.".format(gcodeSize))) result = dia.exec_() if result: # If user selected 'OK' get modified G Code final = dia.editor.toPlainText() diff --git a/src/Mod/Sketcher/App/SketchObject.cpp b/src/Mod/Sketcher/App/SketchObject.cpp index 443840ab3f..eeb4061446 100644 --- a/src/Mod/Sketcher/App/SketchObject.cpp +++ b/src/Mod/Sketcher/App/SketchObject.cpp @@ -1939,6 +1939,17 @@ int SketchObject::trim(int GeoId, const Base::Vector3d& point) secondPos = constr->FirstPos; } }; + + auto isPointAtPosition = [this] (int GeoId1, PointPos pos1, Base::Vector3d point) { + + Base::Vector3d pp = getPoint(GeoId1,pos1); + + if( (point-pp).Length() < Precision::Confusion() ) + return true; + + return false; + + }; Part::Geometry *geo = geomlist[GeoId]; if (geo->getTypeId() == Part::GeomLineSegment::getClassTypeId()) { @@ -2128,6 +2139,26 @@ int SketchObject::trim(int GeoId, const Base::Vector3d& point) PointPos secondPos1 = Sketcher::none, secondPos2 = Sketcher::none; ConstraintType constrType1 = Sketcher::PointOnObject, constrType2 = Sketcher::PointOnObject; + + // check first if start and end points are within a confusion tolerance + if(isPointAtPosition(GeoId1, Sketcher::start, point1)) { + constrType1 = Sketcher::Coincident; + secondPos1 = Sketcher::start; + } + else if(isPointAtPosition(GeoId1, Sketcher::end, point1)) { + constrType1 = Sketcher::Coincident; + secondPos1 = Sketcher::end; + } + + if(isPointAtPosition(GeoId2, Sketcher::start, point2)) { + constrType2 = Sketcher::Coincident; + secondPos2 = Sketcher::start; + } + else if(isPointAtPosition(GeoId2, Sketcher::end, point2)) { + constrType2 = Sketcher::Coincident; + secondPos2 = Sketcher::end; + } + for (std::vector::const_iterator it=constraints.begin(); it != constraints.end(); ++it) { Constraint *constr = *(it); @@ -2162,6 +2193,7 @@ int SketchObject::trim(int GeoId, const Base::Vector3d& point) newConstr->SecondPos = Sketcher::none; // Add Second Constraint + newConstr->Type = constrType2; newConstr->First = GeoId; newConstr->FirstPos = end; newConstr->Second = GeoId2; diff --git a/src/Mod/Spreadsheet/App/PropertyColumnWidthsPyImp.cpp b/src/Mod/Spreadsheet/App/PropertyColumnWidthsPyImp.cpp new file mode 100644 index 0000000000..91e1c83f57 --- /dev/null +++ b/src/Mod/Spreadsheet/App/PropertyColumnWidthsPyImp.cpp @@ -0,0 +1,60 @@ +/*************************************************************************** + * Copyright (c) Eivind Kvedalen (eivind@kvedalen.name) 2015 * + * * + * This file is part of the FreeCAD CAx development system. * + * * + * This library is free software; you can redistribute it and/or * + * modify it under the terms of the GNU Library General Public * + * License as published by the Free Software Foundation; either * + * version 2 of the License, or (at your option) any later version. * + * * + * This library is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU Library General Public License for more details. * + * * + * You should have received a copy of the GNU Library General Public * + * License along with this library; see the file COPYING.LIB. If not, * + * write to the Free Software Foundation, Inc., 59 Temple Place, * + * Suite 330, Boston, MA 02111-1307, USA * + * * + ***************************************************************************/ + + +#include "PreCompiled.h" + +#include "PropertyColumnWidths.h" + +// inclusion of the generated files (generated out of PropertyColumnWidthsPy.xml) +#include "PropertyColumnWidthsPy.h" +#include "PropertyColumnWidthsPy.cpp" + +using namespace Spreadsheet; + +// returns a string which represents the object e.g. when printed in python +std::string PropertyColumnWidthsPy::representation(void) const +{ + return std::string(""); +} + +PyObject *PropertyColumnWidthsPy::PyMake(struct _typeobject *, PyObject *, PyObject *) // Python wrapper +{ + // create a new instance of PropertyColumnWidthsPy and the Twin object + return new PropertyColumnWidthsPy(new PropertyColumnWidths); +} + +// constructor method +int PropertyColumnWidthsPy::PyInit(PyObject* /*args*/, PyObject* /*kwd*/) +{ + return 0; +} + +PyObject *PropertyColumnWidthsPy::getCustomAttributes(const char* /*attr*/) const +{ + return 0; +} + +int PropertyColumnWidthsPy::setCustomAttributes(const char* /*attr*/, PyObject* /*obj*/) +{ + return 0; +} diff --git a/src/Mod/Spreadsheet/App/PropertyRowHeightsPyImp.cpp b/src/Mod/Spreadsheet/App/PropertyRowHeightsPyImp.cpp new file mode 100644 index 0000000000..7c7e053698 --- /dev/null +++ b/src/Mod/Spreadsheet/App/PropertyRowHeightsPyImp.cpp @@ -0,0 +1,60 @@ +/*************************************************************************** + * Copyright (c) Eivind Kvedalen (eivind@kvedalen.name) 2015 * + * * + * This file is part of the FreeCAD CAx development system. * + * * + * This library is free software; you can redistribute it and/or * + * modify it under the terms of the GNU Library General Public * + * License as published by the Free Software Foundation; either * + * version 2 of the License, or (at your option) any later version. * + * * + * This library is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU Library General Public License for more details. * + * * + * You should have received a copy of the GNU Library General Public * + * License along with this library; see the file COPYING.LIB. If not, * + * write to the Free Software Foundation, Inc., 59 Temple Place, * + * Suite 330, Boston, MA 02111-1307, USA * + * * + ***************************************************************************/ + + +#include "PreCompiled.h" + +#include "PropertyRowHeights.h" + +// inclusion of the generated files (generated out of PropertyRowHeightsPy.xml) +#include "PropertyRowHeightsPy.h" +#include "PropertyRowHeightsPy.cpp" + +using namespace Spreadsheet; + +// returns a string which represents the object e.g. when printed in python +std::string PropertyRowHeightsPy::representation(void) const +{ + return std::string(""); +} + +PyObject *PropertyRowHeightsPy::PyMake(struct _typeobject *, PyObject *, PyObject *) // Python wrapper +{ + // create a new instance of PropertyRowHeightsPy and the Twin object + return new PropertyRowHeightsPy(new PropertyRowHeights); +} + +// constructor method +int PropertyRowHeightsPy::PyInit(PyObject* /*args*/, PyObject* /*kwd*/) +{ + return 0; +} + +PyObject *PropertyRowHeightsPy::getCustomAttributes(const char* /*attr*/) const +{ + return 0; +} + +int PropertyRowHeightsPy::setCustomAttributes(const char* /*attr*/, PyObject* /*obj*/) +{ + return 0; +} diff --git a/src/Mod/Spreadsheet/App/PropertySheetPyImp.cpp b/src/Mod/Spreadsheet/App/PropertySheetPyImp.cpp new file mode 100644 index 0000000000..cd26d811b2 --- /dev/null +++ b/src/Mod/Spreadsheet/App/PropertySheetPyImp.cpp @@ -0,0 +1,60 @@ +/*************************************************************************** + * Copyright (c) Eivind Kvedalen (eivind@kvedalen.name) 2015 * + * * + * This file is part of the FreeCAD CAx development system. * + * * + * This library is free software; you can redistribute it and/or * + * modify it under the terms of the GNU Library General Public * + * License as published by the Free Software Foundation; either * + * version 2 of the License, or (at your option) any later version. * + * * + * This library is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU Library General Public License for more details. * + * * + * You should have received a copy of the GNU Library General Public * + * License along with this library; see the file COPYING.LIB. If not, * + * write to the Free Software Foundation, Inc., 59 Temple Place, * + * Suite 330, Boston, MA 02111-1307, USA * + * * + ***************************************************************************/ + + +#include "PreCompiled.h" + +#include "PropertySheet.h" + +// inclusion of the generated files (generated out of PropertySheetPy.xml) +#include "PropertySheetPy.h" +#include "PropertySheetPy.cpp" + +using namespace Spreadsheet; + +// returns a string which represents the object e.g. when printed in python +std::string PropertySheetPy::representation(void) const +{ + return std::string(""); +} + +PyObject *PropertySheetPy::PyMake(struct _typeobject *, PyObject *, PyObject *) // Python wrapper +{ + // create a new instance of PropertySheetPy and the Twin object + return new PropertySheetPy(new PropertySheet); +} + +// constructor method +int PropertySheetPy::PyInit(PyObject* /*args*/, PyObject* /*kwd*/) +{ + return 0; +} + +PyObject *PropertySheetPy::getCustomAttributes(const char* /*attr*/) const +{ + return 0; +} + +int PropertySheetPy::setCustomAttributes(const char* /*attr*/, PyObject* /*obj*/) +{ + return 0; +} diff --git a/src/Mod/TechDraw/App/DrawProjGroup.cpp b/src/Mod/TechDraw/App/DrawProjGroup.cpp index c63dd05d3c..6724e6cc85 100644 --- a/src/Mod/TechDraw/App/DrawProjGroup.cpp +++ b/src/Mod/TechDraw/App/DrawProjGroup.cpp @@ -37,6 +37,7 @@ #include #include #include +#include #include #include @@ -73,6 +74,7 @@ DrawProjGroup::DrawProjGroup(void) : ADD_PROPERTY_TYPE(Source ,(0), group, App::Prop_None,"Shape to view"); Source.setScope(App::LinkScope::Global); Source.setAllowExternal(true); + ADD_PROPERTY_TYPE(XSource ,(0),group,App::Prop_None,"External 3D Shape to view"); ADD_PROPERTY_TYPE(Anchor, (0), group, App::Prop_None, "The root view to align projections with"); Anchor.setScope(App::LinkScope::Global); @@ -93,15 +95,28 @@ DrawProjGroup::~DrawProjGroup() { } + +//TODO: this duplicates code in DVP +std::vector DrawProjGroup::getAllSources(void) const +{ +// Base::Console().Message("DPG::getAllSources()\n"); + const std::vector links = Source.getValues(); + std::vector xLinks; + XSource.getLinks(xLinks); + std::vector result = links; + if (!xLinks.empty()) { + result.insert(result.end(), xLinks.begin(), xLinks.end()); + } + return result; +} + + void DrawProjGroup::onChanged(const App::Property* prop) { //TODO: For some reason, when the projection type is changed, the isometric views show change appropriately, but the orthographic ones don't... Or vice-versa. WF: why would you change from 1st to 3rd in mid drawing? //if group hasn't been added to page yet, can't scale or distribute projItems TechDraw::DrawPage *page = getPage(); if (!isRestoring() && page) { - if (prop == &Source) { - //nothing in particular - } if (prop == &Scale) { if (!m_lockScale) { updateChildrenScale(); @@ -112,7 +127,8 @@ void DrawProjGroup::onChanged(const App::Property* prop) updateChildrenEnforce(); } - if (prop == &Source) { + if ( (prop == &Source) || + (prop == &XSource) ) { updateChildrenSource(); } @@ -156,7 +172,7 @@ App::DocumentObjectExecReturn *DrawProjGroup::execute(void) return DrawViewCollection::execute(); } - std::vector docObjs = Source.getValues(); + std::vector docObjs = getAllSources(); if (docObjs.empty()) { return DrawViewCollection::execute(); } @@ -188,6 +204,7 @@ short DrawProjGroup::mustExecute() const if (!isRestoring()) { result = Views.isTouched() || Source.isTouched() || + XSource.isTouched() || Scale.isTouched() || ScaleType.isTouched() || ProjectionType.isTouched() || @@ -428,6 +445,11 @@ App::DocumentObject * DrawProjGroup::addProjection(const char *viewProjType) view->Label.setValue(viewProjType); addView(view); //from DrawViewCollection view->Source.setValues(Source.getValues()); +// std::vector xLinks; +// XSource.getLinks(xLinks); +// view->XSource.setValues(xLinks); + view->XSource.setValues(XSource.getValues()); + // the Scale is already set by DrawView view->Type.setValue(viewProjType); if (strcmp(viewProjType, "Front") != 0 ) { //not Front! @@ -957,8 +979,13 @@ void DrawProjGroup::updateChildrenSource(void) Base::Console().Log("PROBLEM - DPG::updateChildrenSource - non DPGI entry in Views! %s\n", getNameInDocument()); throw Base::TypeError("Error: projection in DPG list is not a DPGI!"); - } else if (view->Source.getValues() != Source.getValues()) { - view->Source.setValues(Source.getValues()); + } else { + if (view->Source.getValues() != Source.getValues()) { + view->Source.setValues(Source.getValues()); + } + if (view->XSource.getValues() != XSource.getValues()) { + view->XSource.setValues(XSource.getValues()); + } } } } diff --git a/src/Mod/TechDraw/App/DrawProjGroup.h b/src/Mod/TechDraw/App/DrawProjGroup.h index 23d5367104..e9985dcf56 100644 --- a/src/Mod/TechDraw/App/DrawProjGroup.h +++ b/src/Mod/TechDraw/App/DrawProjGroup.h @@ -27,6 +27,8 @@ # include #include #include +#include + #include #include @@ -55,7 +57,9 @@ public: DrawProjGroup(); ~DrawProjGroup(); - App::PropertyLinkList Source; + App::PropertyLinkList Source; + App::PropertyXLinkList XSource; + App::PropertyEnumeration ProjectionType; App::PropertyBool AutoDistribute; @@ -134,6 +138,9 @@ public: void autoPositionChildren(void); void updateChildrenEnforce(void); + std::vector getAllSources(void) const; + + protected: void onChanged(const App::Property* prop) override; diff --git a/src/Mod/TechDraw/App/DrawProjGroupItem.cpp b/src/Mod/TechDraw/App/DrawProjGroupItem.cpp index 36ac741e83..320b719e4d 100644 --- a/src/Mod/TechDraw/App/DrawProjGroupItem.cpp +++ b/src/Mod/TechDraw/App/DrawProjGroupItem.cpp @@ -86,6 +86,7 @@ short DrawProjGroupItem::mustExecute() const result = (Direction.isTouched() || XDirection.isTouched() || Source.isTouched() || + XSource.isTouched() || Scale.isTouched()); } @@ -174,6 +175,7 @@ void DrawProjGroupItem::autoPosition() void DrawProjGroupItem::onDocumentRestored() { +// Base::Console().Message("DPGI::onDocumentRestored() - %s\n", getNameInDocument()); App::DocumentObjectExecReturn* rc = DrawProjGroupItem::execute(); if (rc) { delete rc; diff --git a/src/Mod/TechDraw/App/DrawViewDetail.cpp b/src/Mod/TechDraw/App/DrawViewDetail.cpp index 53c02f5894..8a491fea4a 100644 --- a/src/Mod/TechDraw/App/DrawViewDetail.cpp +++ b/src/Mod/TechDraw/App/DrawViewDetail.cpp @@ -255,9 +255,9 @@ void DrawViewDetail::detailExec(TopoDS_Shape shape, double scale = getScale(); BRepBuilderAPI_Copy BuilderCopy(shape); - TopoDS_Shape copyShape = BuilderCopy.Shape(); + TopoDS_Shape myShape = BuilderCopy.Shape(); - gp_Pnt gpCenter = TechDraw::findCentroid(copyShape, + gp_Pnt gpCenter = TechDraw::findCentroid(myShape, dirDetail); Base::Vector3d shapeCenter = Base::Vector3d(gpCenter.X(),gpCenter.Y(),gpCenter.Z()); m_saveCentroid = shapeCenter; //centroid of original shape @@ -265,7 +265,7 @@ void DrawViewDetail::detailExec(TopoDS_Shape shape, if (dvs != nullptr) { //section cutShape should already be on origin } else { - copyShape = TechDraw::moveShape(copyShape, //centre shape on origin + myShape = TechDraw::moveShape(myShape, //centre shape on origin -shapeCenter); } @@ -280,7 +280,7 @@ void DrawViewDetail::detailExec(TopoDS_Shape shape, Bnd_Box bbxSource; bbxSource.SetGap(0.0); - BRepBndLib::Add(copyShape, bbxSource); + BRepBndLib::Add(myShape, bbxSource); double diag = sqrt(bbxSource.SquareExtent()); Base::Vector3d toolPlaneOrigin = anchorOffset3d + dirDetail * diag * -1.0; //center tool about anchor @@ -301,33 +301,47 @@ void DrawViewDetail::detailExec(TopoDS_Shape shape, gp_Vec extrudeDir(extrudeVec.x,extrudeVec.y,extrudeVec.z); TopoDS_Shape tool = BRepPrimAPI_MakePrism(aProjFace, extrudeDir, false, true).Shape(); - BRepAlgoAPI_Common mkCommon(copyShape,tool); - if (!mkCommon.IsDone()) { - Base::Console().Warning("DVD::execute - %s - detail cut operation failed (1)\n", getNameInDocument()); - return; - } - if (mkCommon.Shape().IsNull()) { - Base::Console().Warning("DVD::execute - %s - detail cut operation failed (2)\n", getNameInDocument()); - return; - } - //Did we get a solid? - TopExp_Explorer xp; - xp.Init(mkCommon.Shape(),TopAbs_SOLID); - if (!(xp.More() == Standard_True)) { - Base::Console().Warning("DVD::execute - mkCommon.Shape is not a solid!\n"); + BRep_Builder builder; + TopoDS_Compound pieces; + builder.MakeCompound(pieces); + TopExp_Explorer expl(myShape, TopAbs_SOLID); + int indb = 0; + int outdb = 0; + for (; expl.More(); expl.Next()) { + indb++; + const TopoDS_Solid& s = TopoDS::Solid(expl.Current()); + + BRepAlgoAPI_Common mkCommon(s,tool); + if (!mkCommon.IsDone()) { +// Base::Console().Warning("DVD::execute - %s - detail cut operation failed (1)\n", getNameInDocument()); + continue; + } + if (mkCommon.Shape().IsNull()) { +// Base::Console().Warning("DVD::execute - %s - detail cut operation failed (2)\n", getNameInDocument()); + continue; + } + //this might be overkill for piecewise algo + //Did we get at least 1 solid? + TopExp_Explorer xp; + xp.Init(mkCommon.Shape(),TopAbs_SOLID); + if (!(xp.More() == Standard_True)) { +// Base::Console().Warning("DVD::execute - mkCommon.Shape is not a solid!\n"); + continue; + } + builder.Add(pieces, mkCommon.Shape()); + outdb++; } - TopoDS_Shape detail = mkCommon.Shape(); if (debugDetail()) { BRepTools::Write(tool, "DVDTool.brep"); //debug - BRepTools::Write(copyShape, "DVDCopy.brep"); //debug - BRepTools::Write(detail, "DVDCommon.brep"); //debug + BRepTools::Write(myShape, "DVDCopy.brep"); //debug + BRepTools::Write(pieces, "DVDCommon.brep"); //debug } Bnd_Box testBox; testBox.SetGap(0.0); - BRepBndLib::Add(detail, testBox); + BRepBndLib::Add(pieces, testBox); if (testBox.IsVoid()) { TechDraw::GeometryObject* go = getGeometryObject(); if (go != nullptr) { @@ -343,7 +357,7 @@ void DrawViewDetail::detailExec(TopoDS_Shape shape, // TopoDS_Compound Comp; // builder.MakeCompound(Comp); // builder.Add(Comp, tool); -// builder.Add(Comp, copyShape); +// builder.Add(Comp, myShape); gp_Pnt inputCenter; try { @@ -358,7 +372,8 @@ void DrawViewDetail::detailExec(TopoDS_Shape shape, gp_Ax2 viewAxis = dvp->getProjectionCS(stdOrg); //sb same CS as base view. //center shape on origin - TopoDS_Shape centeredShape = TechDraw::moveShape(detail, +// TopoDS_Shape centeredShape = TechDraw::moveShape(detail, + TopoDS_Shape centeredShape = TechDraw::moveShape(pieces, centroid * -1.0); TopoDS_Shape scaledShape = TechDraw::scaleShape(centeredShape, diff --git a/src/Mod/TechDraw/App/DrawViewPart.cpp b/src/Mod/TechDraw/App/DrawViewPart.cpp index e1f95e7d4b..09e791ad7c 100644 --- a/src/Mod/TechDraw/App/DrawViewPart.cpp +++ b/src/Mod/TechDraw/App/DrawViewPart.cpp @@ -143,6 +143,8 @@ DrawViewPart::DrawViewPart(void) : ADD_PROPERTY_TYPE(Source ,(0),group,App::Prop_None,"3D Shape to view"); Source.setScope(App::LinkScope::Global); Source.setAllowExternal(true); + ADD_PROPERTY_TYPE(XSource ,(0),group,App::Prop_None,"External 3D Shape to view"); + ADD_PROPERTY_TYPE(Direction ,(0.0,-1.0,0.0), group,App::Prop_None,"Projection Plane normal. The direction you are looking from."); @@ -181,7 +183,7 @@ std::vector DrawViewPart::getSourceShape2d(void) const { // Base::Console().Message("DVP::getSourceShape2d()\n"); std::vector result; - const std::vector& links = Source.getValues(); + const std::vector& links = getAllSources(); result = ShapeExtractor::getShapes2d(links); return result; } @@ -189,8 +191,9 @@ std::vector DrawViewPart::getSourceShape2d(void) const TopoDS_Shape DrawViewPart::getSourceShape(void) const { +// Base::Console().Message("DVP::getSourceShape()\n"); TopoDS_Shape result; - const std::vector& links = Source.getValues(); + const std::vector& links = getAllSources(); if (links.empty()) { bool isRestoring = getDocument()->testStatus(App::Document::Status::Restoring); if (isRestoring) { @@ -208,8 +211,10 @@ TopoDS_Shape DrawViewPart::getSourceShape(void) const TopoDS_Shape DrawViewPart::getSourceShapeFused(void) const { +// Base::Console().Message("DVP::getSourceShapeFused()\n"); TopoDS_Shape result; - const std::vector& links = Source.getValues(); +// const std::vector& links = Source.getValues(); + const std::vector& links = getAllSources(); if (links.empty()) { bool isRestoring = getDocument()->testStatus(App::Document::Status::Restoring); if (isRestoring) { @@ -225,6 +230,20 @@ TopoDS_Shape DrawViewPart::getSourceShapeFused(void) const return result; } +std::vector DrawViewPart::getAllSources(void) const +{ +// Base::Console().Message("DVP::getAllSources()\n"); + const std::vector links = Source.getValues(); + std::vector xLinks = XSource.getValues(); +// std::vector xLinks; +// XSource.getLinks(xLinks); + + std::vector result = links; + if (!xLinks.empty()) { + result.insert(result.end(), xLinks.begin(), xLinks.end()); + } + return result; +} App::DocumentObjectExecReturn *DrawViewPart::execute(void) { @@ -232,10 +251,13 @@ App::DocumentObjectExecReturn *DrawViewPart::execute(void) if (!keepUpdated()) { return App::DocumentObject::StdReturn; } + +// Base::Console().Message("DVP::execute - Source: %d XSource: %d\n", +// Source.getValues().size(), XSource.getValues().size()); App::Document* doc = getDocument(); bool isRestoring = doc->testStatus(App::Document::Status::Restoring); - const std::vector& links = Source.getValues(); + const std::vector& links = getAllSources(); if (links.empty()) { if (isRestoring) { Base::Console().Warning("DVP::execute - No Sources (but document is restoring) - %s\n", @@ -246,7 +268,6 @@ App::DocumentObjectExecReturn *DrawViewPart::execute(void) } return App::DocumentObject::StdReturn; } - std::vector sources = Source.getValues(); TopoDS_Shape shape = getSourceShape(); if (shape.IsNull()) { @@ -307,6 +328,7 @@ short DrawViewPart::mustExecute() const if (!isRestoring()) { result = (Direction.isTouched() || Source.isTouched() || + XSource.isTouched() || Perspective.isTouched() || Focus.isTouched() || Rotation.isTouched() || diff --git a/src/Mod/TechDraw/App/DrawViewPart.h b/src/Mod/TechDraw/App/DrawViewPart.h index 1c679be64e..c366e44823 100644 --- a/src/Mod/TechDraw/App/DrawViewPart.h +++ b/src/Mod/TechDraw/App/DrawViewPart.h @@ -93,6 +93,7 @@ public: virtual ~DrawViewPart(); App::PropertyLinkList Source; + App::PropertyXLinkList XSource; App::PropertyVector Direction; //TODO: Rename to YAxisDirection or whatever this actually is (ProjectionDirection) App::PropertyVector XDirection; App::PropertyBool Perspective; @@ -202,6 +203,9 @@ public: void removeAllReferencesFromGeom(); void resetReferenceVerts(); + std::vector getAllSources(void) const; + + protected: bool checkXDirection(void) const; diff --git a/src/Mod/TechDraw/App/DrawViewSection.cpp b/src/Mod/TechDraw/App/DrawViewSection.cpp index d3cb579bb6..0d76ef8ec9 100644 --- a/src/Mod/TechDraw/App/DrawViewSection.cpp +++ b/src/Mod/TechDraw/App/DrawViewSection.cpp @@ -347,7 +347,6 @@ App::DocumentObjectExecReturn *DrawViewSection::execute(void) } } - dvp->requestPaint(); //to refresh section line return DrawView::execute(); } @@ -382,13 +381,26 @@ void DrawViewSection::sectionExec(TopoDS_Shape baseShape) BRepBuilderAPI_Copy BuilderCopy(baseShape); TopoDS_Shape myShape = BuilderCopy.Shape(); - BRepAlgoAPI_Cut mkCut(myShape, prism); - if (!mkCut.IsDone()) { - Base::Console().Warning("DVS: Section cut has failed in %s\n",getNameInDocument()); - return; + BRep_Builder builder; + TopoDS_Compound pieces; + builder.MakeCompound(pieces); + TopExp_Explorer expl(myShape, TopAbs_SOLID); + int indb = 0; + int outdb = 0; + for (; expl.More(); expl.Next()) { + indb++; + const TopoDS_Solid& s = TopoDS::Solid(expl.Current()); + BRepAlgoAPI_Cut mkCut(s, prism); + if (!mkCut.IsDone()) { + Base::Console().Warning("DVS: Section cut has failed in %s\n",getNameInDocument()); + continue; + } + TopoDS_Shape cut = mkCut.Shape(); + builder.Add(pieces, cut); + outdb++; } - TopoDS_Shape rawShape = mkCut.Shape(); + TopoDS_Shape rawShape = pieces; if (debugSection()) { BRepTools::Write(myShape, "DVSCopy.brep"); //debug BRepTools::Write(aProjFace, "DVSFace.brep"); //debug diff --git a/src/Mod/TechDraw/App/ShapeExtractor.cpp b/src/Mod/TechDraw/App/ShapeExtractor.cpp index 35b4303449..febe6fcd06 100644 --- a/src/Mod/TechDraw/App/ShapeExtractor.cpp +++ b/src/Mod/TechDraw/App/ShapeExtractor.cpp @@ -73,17 +73,24 @@ std::vector ShapeExtractor::getShapes2d(const std::vector objs = gex->Group.getValues(); for (auto& d: objs) { if (is2dObject(d)) { - auto shape = Part::Feature::getShape(d); - if(!shape.IsNull()) { - shapes2d.push_back(shape); + if (d->getTypeId().isDerivedFrom(Part::Feature::getClassTypeId())) { + //need to apply global placement here. ??? because 2d shapes (Points so far) + //don't get gp from Part::feature::getShape() ???? + const Part::Feature* pf = static_cast(d); + Part::TopoShape ts = pf->Shape.getShape(); + ts.setPlacement(pf->globalPlacement()); + shapes2d.push_back(ts.getShape()); } } } } else { if (is2dObject(l)) { - auto shape = Part::Feature::getShape(l); - if(!shape.IsNull()) { - shapes2d.push_back(shape); + if (l->getTypeId().isDerivedFrom(Part::Feature::getClassTypeId())) { + //need to apply placement here + const Part::Feature* pf = static_cast(l); + Part::TopoShape ts = pf->Shape.getShape(); + ts.setPlacement(pf->globalPlacement()); + shapes2d.push_back(ts.getShape()); } } } @@ -110,6 +117,7 @@ TopoDS_Shape ShapeExtractor::getShapes(const std::vector l if(!shape.IsNull()) { // BRepTools::Write(shape, "DVPgetShape.brep"); //debug if (shape.ShapeType() > TopAbs_COMPSOLID) { //simple shape + //do we need to apply placement here too?? sourceShapes.push_back(shape); } else { //complex shape std::vector drawable = extractDrawableShapes(shape); @@ -415,16 +423,18 @@ Base::Vector3d ShapeExtractor::getLocation3dFromFeat(App::DocumentObject* obj) // if (isDraftPoint(obj) { // //Draft Points are not necc. Part::PartFeature?? // //if Draft option "use part primitives" is not set are Draft points still PartFeature? -// Base::Vector3d featPos = features[i]->(Placement.getValue()).Position(); Part::Feature* pf = dynamic_cast(obj); if (pf != nullptr) { - TopoDS_Shape ts = pf->Shape.getValue(); + Part::TopoShape pts = pf->Shape.getShape(); + pts.setPlacement(pf->globalPlacement()); + TopoDS_Shape ts = pts.getShape(); if (ts.ShapeType() == TopAbs_VERTEX) { TopoDS_Vertex v = TopoDS::Vertex(ts); result = DrawUtil::vertex2Vector(v); } } + // Base::Console().Message("SE::getLocation3dFromFeat - returns: %s\n", // DrawUtil::formatVector(result).c_str()); return result; diff --git a/src/Mod/TechDraw/Gui/AppTechDrawGui.cpp b/src/Mod/TechDraw/Gui/AppTechDrawGui.cpp index b8833756f9..98c264ac17 100644 --- a/src/Mod/TechDraw/Gui/AppTechDrawGui.cpp +++ b/src/Mod/TechDraw/Gui/AppTechDrawGui.cpp @@ -39,11 +39,13 @@ #include "Workbench.h" #include "MDIViewPage.h" -#include "DlgPrefsTechDraw1Imp.h" -#include "DlgPrefsTechDraw2Imp.h" -#include "DlgPrefsTechDraw3Imp.h" -#include "DlgPrefsTechDraw4Imp.h" -#include "DlgPrefsTechDraw5Imp.h" +#include "DlgPrefsTechDrawGeneralImp.h" +#include "DlgPrefsTechDrawScaleImp.h" +#include "DlgPrefsTechDrawAnnotationImp.h" +#include "DlgPrefsTechDrawDimensionsImp.h" +#include "DlgPrefsTechDrawColorsImp.h" +#include "DlgPrefsTechDrawAdvancedImp.h" +#include "DlgPrefsTechDrawHLRImp.h" #include "ViewProviderPage.h" #include "ViewProviderDrawingView.h" #include "ViewProviderDimension.h" @@ -149,11 +151,13 @@ PyMOD_INIT_FUNC(TechDrawGui) TechDrawGui::ViewProviderCosmeticExtension::init(); // register preferences pages - new Gui::PrefPageProducer ("TechDraw"); //General - new Gui::PrefPageProducer ("TechDraw"); //Scale - new Gui::PrefPageProducer ("TechDraw"); //Dimensions - new Gui::PrefPageProducer ("TechDraw"); //HLR - new Gui::PrefPageProducer ("TechDraw"); //Advanced + new Gui::PrefPageProducer ("TechDraw"); //General + new Gui::PrefPageProducer ("TechDraw"); //Scale + new Gui::PrefPageProducer("TechDraw"); //Dimensions + new Gui::PrefPageProducer ("TechDraw"); //Annotation + new Gui::PrefPageProducer("TechDraw"); //Colors + new Gui::PrefPageProducer ("TechDraw"); //HLR + new Gui::PrefPageProducer ("TechDraw"); //Advanced // add resources and reloads the translators loadTechDrawResource(); diff --git a/src/Mod/TechDraw/Gui/CMakeLists.txt b/src/Mod/TechDraw/Gui/CMakeLists.txt index 43ab8e5eeb..d7fc068f09 100644 --- a/src/Mod/TechDraw/Gui/CMakeLists.txt +++ b/src/Mod/TechDraw/Gui/CMakeLists.txt @@ -44,11 +44,13 @@ set(TechDrawGui_MOC_HDRS QGIViewDimension.h QGIViewBalloon.h TaskProjGroup.h - DlgPrefsTechDraw1Imp.h - DlgPrefsTechDraw2Imp.h - DlgPrefsTechDraw3Imp.h - DlgPrefsTechDraw4Imp.h - DlgPrefsTechDraw5Imp.h + DlgPrefsTechDrawGeneralImp.h + DlgPrefsTechDrawScaleImp.h + DlgPrefsTechDrawAnnotationImp.h + DlgPrefsTechDrawDimensionsImp.h + DlgPrefsTechDrawColorsImp.h + DlgPrefsTechDrawAdvancedImp.h + DlgPrefsTechDrawHLRImp.h TaskLinkDim.h DlgTemplateField.h TaskSectionView.h @@ -84,11 +86,13 @@ else() endif() set(TechDrawGui_UIC_SRCS - DlgPrefsTechDraw1.ui - DlgPrefsTechDraw2.ui - DlgPrefsTechDraw3.ui - DlgPrefsTechDraw4.ui - DlgPrefsTechDraw5.ui + DlgPrefsTechDrawGeneral.ui + DlgPrefsTechDrawScale.ui + DlgPrefsTechDrawAnnotation.ui + DlgPrefsTechDrawDimensions.ui + DlgPrefsTechDrawColors.ui + DlgPrefsTechDrawAdvanced.ui + DlgPrefsTechDrawHLR.ui TaskProjGroup.ui TaskLinkDim.ui DlgTemplateField.ui @@ -145,21 +149,27 @@ SET(TechDrawGui_SRCS TaskProjGroup.ui TaskProjGroup.cpp TaskProjGroup.h - DlgPrefsTechDraw1.ui - DlgPrefsTechDraw1Imp.cpp - DlgPrefsTechDraw1Imp.h - DlgPrefsTechDraw2.ui - DlgPrefsTechDraw2Imp.cpp - DlgPrefsTechDraw2Imp.h - DlgPrefsTechDraw3.ui - DlgPrefsTechDraw3Imp.cpp - DlgPrefsTechDraw3Imp.h - DlgPrefsTechDraw4.ui - DlgPrefsTechDraw4Imp.cpp - DlgPrefsTechDraw4Imp.h - DlgPrefsTechDraw5.ui - DlgPrefsTechDraw5Imp.cpp - DlgPrefsTechDraw5Imp.h + DlgPrefsTechDrawGeneral.ui + DlgPrefsTechDrawGeneralImp.cpp + DlgPrefsTechDrawGeneralImp.h + DlgPrefsTechDrawScale.ui + DlgPrefsTechDrawScaleImp.cpp + DlgPrefsTechDrawScaleImp.h + DlgPrefsTechDrawAnnotation.ui + DlgPrefsTechDrawAnnotationImp.cpp + DlgPrefsTechDrawAnnotationImp.h + DlgPrefsTechDrawDimensions.ui + DlgPrefsTechDrawDimensionsImp.cpp + DlgPrefsTechDrawDimensionsImp.h + DlgPrefsTechDrawColors.ui + DlgPrefsTechDrawColorsImp.cpp + DlgPrefsTechDrawColorsImp.h + DlgPrefsTechDrawAdvanced.ui + DlgPrefsTechDrawAdvancedImp.cpp + DlgPrefsTechDrawAdvancedImp.h + DlgPrefsTechDrawHLR.ui + DlgPrefsTechDrawHLRImp.cpp + DlgPrefsTechDrawHLRImp.h TaskLinkDim.ui TaskLinkDim.cpp TaskLinkDim.h diff --git a/src/Mod/TechDraw/Gui/Command.cpp b/src/Mod/TechDraw/Gui/Command.cpp index 89b680d198..7a82f0210f 100644 --- a/src/Mod/TechDraw/Gui/Command.cpp +++ b/src/Mod/TechDraw/Gui/Command.cpp @@ -30,28 +30,30 @@ #include -#include -#include -#include #include #include -#include #include +#include #include -#include #include +#include +#include +#include #include #include +#include #include +#include +#include #include #include #include #include #include #include -#include -#include #include +#include +#include #include #include @@ -304,6 +306,7 @@ void CmdTechDrawView::activated(int iMsg) //set projection direction from selected Face //use first object with a face selected std::vector shapes; + std::vector xShapes; App::DocumentObject* partObj = nullptr; std::string faceName; int resolve = 1; //mystery @@ -317,22 +320,33 @@ void CmdTechDrawView::activated(int iMsg) if (obj->isDerivedFrom(TechDraw::DrawPage::getClassTypeId()) ) { continue; } + if ( obj->isDerivedFrom(App::LinkElement::getClassTypeId()) || + obj->isDerivedFrom(App::LinkGroup::getClassTypeId()) || + obj->isDerivedFrom(App::Link::getClassTypeId()) ) { + xShapes.push_back(obj); + continue; + } + //not a Link and not null. assume to be drawable. Undrawables will be + // skipped later. if (obj != nullptr) { shapes.push_back(obj); } if(partObj != nullptr) { continue; } + //don't know if this works for an XLink for(auto& sub : sel.getSubNames()) { if (TechDraw::DrawUtil::getGeomTypeFromName(sub) == "Face") { faceName = sub; + // partObj = obj; break; } } } - if ((shapes.empty())) { + if ( shapes.empty() && + xShapes.empty() ) { QMessageBox::warning(Gui::getMainWindow(), QObject::tr("Wrong selection"), QObject::tr("No Shapes, Groups or Links in this selection")); return; @@ -350,6 +364,7 @@ void CmdTechDrawView::activated(int iMsg) throw Base::TypeError("CmdTechDrawView DVP not found\n"); } dvp->Source.setValues(shapes); + dvp->XSource.setValues(xShapes); doCommand(Doc,"App.activeDocument().%s.addView(App.activeDocument().%s)",PageName.c_str(),FeatName.c_str()); if (faceName.size()) { std::pair dirs = DrawGuiUtil::getProjDirFromFace(partObj,faceName); @@ -450,26 +465,6 @@ void CmdTechDrawSectionView::activated(int iMsg) return; } TechDraw::DrawViewPart* dvp = static_cast(*baseObj.begin()); -// std::string BaseName = dvp->getNameInDocument(); -// std::string PageName = page->getNameInDocument(); -// double baseScale = dvp->getScale(); - -// Gui::WaitCursor wc; -// openCommand("Create view"); -// std::string FeatName = getUniqueObjectName("Section"); - -// doCommand(Doc,"App.activeDocument().addObject('TechDraw::DrawViewSection','%s')",FeatName.c_str()); - -// App::DocumentObject *docObj = getDocument()->getObject(FeatName.c_str()); -// TechDraw::DrawViewSection* dsv = dynamic_cast(docObj); -// if (!dsv) { -// throw Base::TypeError("CmdTechDrawSectionView DVS not found\n"); -// } -// dsv->Source.setValues(dvp->Source.getValues()); -// doCommand(Doc,"App.activeDocument().%s.BaseView = App.activeDocument().%s",FeatName.c_str(),BaseName.c_str()); -// doCommand(Doc,"App.activeDocument().%s.ScaleType = App.activeDocument().%s.ScaleType",FeatName.c_str(),BaseName.c_str()); -// doCommand(Doc,"App.activeDocument().%s.addView(App.activeDocument().%s)",PageName.c_str(),FeatName.c_str()); -// doCommand(Doc,"App.activeDocument().%s.Scale = %0.6f",FeatName.c_str(),baseScale); Gui::Control().showDialog(new TaskDlgSectionView(dvp)); updateActive(); //ok here since dialog doesn't call doc.recompute() @@ -568,6 +563,7 @@ void CmdTechDrawProjectionGroup::activated(int iMsg) //set projection direction from selected Face //use first object with a face selected std::vector shapes; + std::vector xShapes; App::DocumentObject* partObj = nullptr; std::string faceName; int resolve = 1; //mystery @@ -577,14 +573,19 @@ void CmdTechDrawProjectionGroup::activated(int iMsg) resolve, single); for (auto& sel: selection) { -// for(auto &sel : getSelection().getSelectionEx(0,App::DocumentObject::getClassTypeId(),false)) { auto obj = sel.getObject(); if (obj->isDerivedFrom(TechDraw::DrawPage::getClassTypeId()) ) { continue; } -// if(!obj || inlist.count(obj)) //?????? -// continue; - if (obj != nullptr) { //can this happen? + if ( obj->isDerivedFrom(App::LinkElement::getClassTypeId()) || + obj->isDerivedFrom(App::LinkGroup::getClassTypeId()) || + obj->isDerivedFrom(App::Link::getClassTypeId()) ) { + xShapes.push_back(obj); + continue; + } + //not a Link and not null. assume to be drawable. Undrawables will be + // skipped later. + if (obj != nullptr) { shapes.push_back(obj); } if(partObj != nullptr) { @@ -598,9 +599,10 @@ void CmdTechDrawProjectionGroup::activated(int iMsg) } } } - if (shapes.empty()) { + if ( shapes.empty() && + xShapes.empty() ) { QMessageBox::warning(Gui::getMainWindow(), QObject::tr("Wrong selection"), - QObject::tr("No Shapes or Groups in this selection")); + QObject::tr("No Shapes, Groups or Links in this selection")); return; } @@ -618,6 +620,7 @@ void CmdTechDrawProjectionGroup::activated(int iMsg) App::DocumentObject *docObj = getDocument()->getObject(multiViewName.c_str()); auto multiView( static_cast(docObj) ); multiView->Source.setValues(shapes); + multiView->XSource.setValues(xShapes); doCommand(Doc,"App.activeDocument().%s.addProjection('Front')",multiViewName.c_str()); if (faceName.size()) { diff --git a/src/Mod/TechDraw/Gui/CommandAnnotate.cpp b/src/Mod/TechDraw/Gui/CommandAnnotate.cpp index 702f4fd9dc..d36a178fc4 100644 --- a/src/Mod/TechDraw/Gui/CommandAnnotate.cpp +++ b/src/Mod/TechDraw/Gui/CommandAnnotate.cpp @@ -581,13 +581,13 @@ void CmdTechDrawCenterLineGroup::activated(int iMsg) Gui::ActionGroup* pcAction = qobject_cast(_pcAction); pcAction->setIcon(pcAction->actions().at(iMsg)->icon()); switch(iMsg) { - case 0: + case 0: //faces execCenterLine(this); break; - case 1: + case 1: //2 lines exec2LineCenterLine(this); break; - case 2: + case 2: //2 points exec2PointCenterLine(this); break; default: @@ -743,29 +743,23 @@ void execCenterLine(Gui::Command* cmd) if (!faceNames.empty()) { Gui::Control().showDialog(new TaskDlgCenterLine(baseFeat, page, - faceNames)); + faceNames, + false)); } else if (edgeNames.empty()) { QMessageBox::warning(Gui::getMainWindow(), QObject::tr("Wrong Selection"), QObject::tr("No CenterLine in selection.")); return; } else { - std::string edgeName = edgeNames.front(); - int geomIdx = DrawUtil::getIndexFromName(edgeName); - const std::vector &geoms = baseFeat->getEdgeGeometry(); - BaseGeom* bg = geoms.at(geomIdx); -// int clIdx = bg->sourceIndex(); -// TechDraw::CenterLine* cl = baseFeat->getCenterLineByIndex(clIdx); - std::string tag = bg->getCosmeticTag(); - TechDraw::CenterLine* cl = baseFeat->getCenterLine(tag); + TechDraw::CenterLine* cl = baseFeat->getCenterLineBySelection(edgeNames.front()); if (cl == nullptr) { QMessageBox::warning(Gui::getMainWindow(), QObject::tr("Wrong Selection"), - QObject::tr("No CenterLine in selection.")); + QObject::tr("Selection is not a CenterLine.")); return; } - Gui::Control().showDialog(new TaskDlgCenterLine(baseFeat, page, - edgeNames.front())); + edgeNames.front(), + true)); } } @@ -825,25 +819,19 @@ void exec2LineCenterLine(Gui::Command* cmd) if (selectedEdges.size() == 2) { Gui::Control().showDialog(new TaskDlgCenterLine(dvp, page, - selectedEdges)); + selectedEdges, + false)); } else if (selectedEdges.size() == 1) { - std::string edgeName = selectedEdges.front(); - int geomIdx = DrawUtil::getIndexFromName(edgeName); - const std::vector &geoms = dvp->getEdgeGeometry(); - BaseGeom* bg = geoms.at(geomIdx); -// int clIdx = bg->sourceIndex(); -// TechDraw::CenterLine* cl = dvp->getCenterLineByIndex(clIdx); - std::string tag = bg->getCosmeticTag(); - TechDraw::CenterLine* cl = dvp->getCenterLine(tag); + TechDraw::CenterLine* cl = dvp->getCenterLineBySelection(selectedEdges.front()); if (cl == nullptr) { QMessageBox::warning(Gui::getMainWindow(), QObject::tr("Wrong Selection"), - QObject::tr("No CenterLine in selection.")); + QObject::tr("Selection is not a CenterLine.")); return; } else { -// Base::Console().Message("CMD::2LineCenter - show edit dialog here\n"); Gui::Control().showDialog(new TaskDlgCenterLine(dvp, page, - selectedEdges.front())); + selectedEdges.front(), + true)); } } else { //not create, not edit, what is this??? QMessageBox::warning(Gui::getMainWindow(), QObject::tr("Wrong Selection"), @@ -942,14 +930,23 @@ void exec2PointCenterLine(Gui::Command* cmd) if (!vertexNames.empty() && (vertexNames.size() == 2)) { Gui::Control().showDialog(new TaskDlgCenterLine(baseFeat, page, - vertexNames)); + vertexNames, + false)); } else if (!edgeNames.empty() && (edgeNames.size() == 1)) { + TechDraw::CenterLine* cl = baseFeat->getCenterLineBySelection(edgeNames.front()); + if (cl == nullptr) { + QMessageBox::warning(Gui::getMainWindow(), QObject::tr("Wrong Selection"), + QObject::tr("Selection is not a CenterLine.")); + return; + } + Gui::Control().showDialog(new TaskDlgCenterLine(baseFeat, page, - edgeNames.front())); + edgeNames.front(), + false)); } else if (vertexNames.empty()) { QMessageBox::warning(Gui::getMainWindow(), QObject::tr("Wrong Selection"), - QObject::tr("No CenterLine in selection.")); + QObject::tr("Need 2 Vertices or 1 CenterLine.")); return; } } diff --git a/src/Mod/TechDraw/Gui/DlgPrefsTechDraw4.ui b/src/Mod/TechDraw/Gui/DlgPrefsTechDrawAdvanced.ui similarity index 98% rename from src/Mod/TechDraw/Gui/DlgPrefsTechDraw4.ui rename to src/Mod/TechDraw/Gui/DlgPrefsTechDrawAdvanced.ui index 8c9b8c1cea..8ab7e5c067 100644 --- a/src/Mod/TechDraw/Gui/DlgPrefsTechDraw4.ui +++ b/src/Mod/TechDraw/Gui/DlgPrefsTechDrawAdvanced.ui @@ -1,7 +1,7 @@ - TechDrawGui::DlgPrefsTechDraw4Imp - + TechDrawGui::DlgPrefsTechDrawAdvancedImp + 0 @@ -251,10 +251,10 @@ Then you need to increase the tile limit. Show Section Edges - ShowUnits + ShowSectionEdges - /Mod/TechDraw/Dimensions + /Mod/TechDraw/General
diff --git a/src/Mod/TechDraw/Gui/DlgPrefsTechDraw4Imp.cpp b/src/Mod/TechDraw/Gui/DlgPrefsTechDrawAdvancedImp.cpp similarity index 88% rename from src/Mod/TechDraw/Gui/DlgPrefsTechDraw4Imp.cpp rename to src/Mod/TechDraw/Gui/DlgPrefsTechDrawAdvancedImp.cpp index 9725728ee4..9f2a2c5cf7 100644 --- a/src/Mod/TechDraw/Gui/DlgPrefsTechDraw4Imp.cpp +++ b/src/Mod/TechDraw/Gui/DlgPrefsTechDrawAdvancedImp.cpp @@ -25,23 +25,23 @@ #include "PreCompiled.h" -#include "DlgPrefsTechDraw4Imp.h" +#include "DlgPrefsTechDrawAdvancedImp.h" #include using namespace TechDrawGui; -DlgPrefsTechDraw4Imp::DlgPrefsTechDraw4Imp( QWidget* parent ) +DlgPrefsTechDrawAdvancedImp::DlgPrefsTechDrawAdvancedImp( QWidget* parent ) : PreferencePage( parent ) { this->setupUi(this); } -DlgPrefsTechDraw4Imp::~DlgPrefsTechDraw4Imp() +DlgPrefsTechDrawAdvancedImp::~DlgPrefsTechDrawAdvancedImp() { // no need to delete child widgets, Qt does it all for us } -void DlgPrefsTechDraw4Imp::saveSettings() +void DlgPrefsTechDrawAdvancedImp::saveSettings() { cbEndCap->onSave(); cbCrazyEdges->onSave(); @@ -56,7 +56,7 @@ void DlgPrefsTechDraw4Imp::saveSettings() leFormatSpec->onSave(); } -void DlgPrefsTechDraw4Imp::loadSettings() +void DlgPrefsTechDrawAdvancedImp::loadSettings() { cbEndCap->onRestore(); cbCrazyEdges->onRestore(); @@ -74,7 +74,7 @@ void DlgPrefsTechDraw4Imp::loadSettings() /** * Sets the strings of the subwidgets using the current language. */ -void DlgPrefsTechDraw4Imp::changeEvent(QEvent *e) +void DlgPrefsTechDrawAdvancedImp::changeEvent(QEvent *e) { if (e->type() == QEvent::LanguageChange) { saveSettings(); @@ -86,4 +86,4 @@ void DlgPrefsTechDraw4Imp::changeEvent(QEvent *e) } } -#include +#include diff --git a/src/Mod/TechDraw/Gui/DlgPrefsTechDraw4Imp.h b/src/Mod/TechDraw/Gui/DlgPrefsTechDrawAdvancedImp.h similarity index 81% rename from src/Mod/TechDraw/Gui/DlgPrefsTechDraw4Imp.h rename to src/Mod/TechDraw/Gui/DlgPrefsTechDrawAdvancedImp.h index 860652fdc9..4404c1ba6f 100644 --- a/src/Mod/TechDraw/Gui/DlgPrefsTechDraw4Imp.h +++ b/src/Mod/TechDraw/Gui/DlgPrefsTechDrawAdvancedImp.h @@ -1,7 +1,6 @@ /************************************************************************** * Copyright (c) 2015 FreeCAD Developers * * Author: WandererFan * - * Based on src/Mod/FEM/Gui/DlgPrefsTechDraw4Imp.cpp * * * * This file is part of the FreeCAD CAx development system. * * * @@ -23,21 +22,21 @@ ***************************************************************************/ -#ifndef DRAWINGGUI_DLGPREFSTECHDRAWIMP4_H -#define DRAWINGGUI_DLGPREFSTECHDRAWIMP4_H +#ifndef DRAWINGGUI_DLGPREFSTECHDRAWIMPADVANCED_H +#define DRAWINGGUI_DLGPREFSTECHDRAWIMPADVANCED_H -#include +#include #include namespace TechDrawGui { -class DlgPrefsTechDraw4Imp : public Gui::Dialog::PreferencePage, public Ui_DlgPrefsTechDraw4Imp +class DlgPrefsTechDrawAdvancedImp : public Gui::Dialog::PreferencePage, public Ui_DlgPrefsTechDrawAdvancedImp { Q_OBJECT public: - DlgPrefsTechDraw4Imp( QWidget* parent = 0 ); - ~DlgPrefsTechDraw4Imp(); + DlgPrefsTechDrawAdvancedImp( QWidget* parent = 0 ); + ~DlgPrefsTechDrawAdvancedImp(); protected: void saveSettings(); @@ -47,4 +46,4 @@ protected: } // namespace TechDrawGui -#endif // DRAWINGGUI_DLGPREFSTECHDRAWIMP4_H +#endif // DRAWINGGUI_DLGPREFSTECHDRAWIMPADVANCED_H diff --git a/src/Mod/TechDraw/Gui/DlgPrefsTechDraw3.ui b/src/Mod/TechDraw/Gui/DlgPrefsTechDrawAnnotation.ui similarity index 63% rename from src/Mod/TechDraw/Gui/DlgPrefsTechDraw3.ui rename to src/Mod/TechDraw/Gui/DlgPrefsTechDrawAnnotation.ui index c2d313e355..088c917a31 100644 --- a/src/Mod/TechDraw/Gui/DlgPrefsTechDraw3.ui +++ b/src/Mod/TechDraw/Gui/DlgPrefsTechDrawAnnotation.ui @@ -1,13 +1,13 @@ - TechDrawGui::DlgPrefsTechDraw3Imp - + TechDrawGui::DlgPrefsTechDrawAnnotationImp + 0 0 460 - 790 + 460 @@ -17,374 +17,9 @@ - Dimensions + Annotation - - - - - - 0 - 0 - - - - - 0 - 0 - - - - - 0 - 0 - - - - Dimensions - - - - - - - - - 0 - 0 - - - - Standard and Style - - - - - - - - 0 - 0 - - - - - 184 - 22 - - - - Standard to be used for dimensional values - - - StandardAndStyle - - - /Mod/TechDraw/Dimensions - - - - ISO Oriented - - - - - ISO Referencing - - - - - ASME Inlined - - - - - ASME Referencing - - - - - - - - - 0 - 0 - - - - Use system setting for number of decimals - - - Use Global Decimals - - - true - - - UseGlobalDecimals - - - /Mod/TechDraw/Dimensions - - - - - - - - 0 - 0 - - - - - 0 - 22 - - - - Append unit to dimension values - - - Show Units - - - ShowUnits - - - /Mod/TechDraw/Dimensions - - - - - - - Alternate Decimals - - - - - - - false - - - - 0 - 0 - - - - - 0 - 22 - - - - Number of decimals if 'Use Global Decimals' is not used - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - 2 - - - AltDecimals - - - /Mod/TechDraw/Dimensions - - - - - - - - true - - - - Font Size - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - 0 - 0 - - - - - 0 - 22 - - - - Dimension text font size - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - 4.000000000000000 - - - FontSize - - - /Mod/TechDraw/Dimensions - - - - - - - Diameter Symbol - - - - - - - - 0 - 0 - - - - - 0 - 22 - - - - - 12 - - - - Character used to indicate diameter dimensions - - - ⌀ - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - DiameterSymbol - - - /Mod/TechDraw/Dimensions - - - - - - - - true - - - - Arrow Style - - - - - - - - 0 - 0 - - - - - 0 - 22 - - - - Arrowhead style - - - -1 - - - ArrowStyle - - - Mod/TechDraw/Dimensions - - - - - - - - true - - - - Arrow Size - - - - - - - - 0 - 0 - - - - - 0 - 22 - - - - Arrowhead size - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - 5.000000000000000 - - - ArrowSize - - - Mod/TechDraw/Dimensions - - - - - - - - + @@ -1201,148 +836,6 @@
- - - - - 0 - 0 - - - - - 0 - 85 - - - - - 16777215 - 500 - - - - - 0 - 500 - - - - Conventions - - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - Projection Group Angle - - - - - - - - 0 - 0 - - - - - 184 - 0 - - - - Use first- or third-angle mutliview projection convention - - - ProjectionAngle - - - Mod/TechDraw/General - - - - First - - - - - Third - - - - - - - - Hidden Line Style - - - - - - - - 0 - 0 - - - - - 0 - 0 - - - - Style for hidden lines - - - HiddenLine - - - Mod/TechDraw/General - - - - Continuous - - - - :/icons/continuous-line.svg:/icons/continuous-line.svg - - - - - Dashed - - - - :/icons/dash-line.svg:/icons/dash-line.svg - - - - - - - - - @@ -1383,11 +876,6 @@ QWidget
Gui/QuantitySpinBox.h
- - Gui::PrefSpinBox - QSpinBox -
Gui/PrefWidgets.h
-
Gui::PrefCheckBox QCheckBox @@ -1412,22 +900,5 @@ - - - cbGlobalDecimals - toggled(bool) - sbAltDecimals - setDisabled(bool) - - - 108 - 71 - - - 425 - 124 - - - - + diff --git a/src/Mod/TechDraw/Gui/DlgPrefsTechDraw3Imp.cpp b/src/Mod/TechDraw/Gui/DlgPrefsTechDrawAnnotationImp.cpp similarity index 66% rename from src/Mod/TechDraw/Gui/DlgPrefsTechDraw3Imp.cpp rename to src/Mod/TechDraw/Gui/DlgPrefsTechDrawAnnotationImp.cpp index 624527ce47..22eb09e6d3 100644 --- a/src/Mod/TechDraw/Gui/DlgPrefsTechDraw3Imp.cpp +++ b/src/Mod/TechDraw/Gui/DlgPrefsTechDrawAnnotationImp.cpp @@ -1,6 +1,6 @@ /*************************************************************************** - * Copyright (c) 2015 FreeCAD Developers * - * Author: WandererFan * + * Copyright (c) 2020 FreeCAD Developers * + * Author: Uwe Stöhr * * Based on src/Mod/FEM/Gui/DlgSettingsFEMImp.cpp * * * * This file is part of the FreeCAD CAx development system. * @@ -31,105 +31,75 @@ #include #include "DrawGuiUtil.h" -#include "DlgPrefsTechDraw3Imp.h" +#include "DlgPrefsTechDrawAnnotationImp.h" using namespace TechDrawGui; using namespace TechDraw; -DlgPrefsTechDraw3Imp::DlgPrefsTechDraw3Imp( QWidget* parent ) +DlgPrefsTechDrawAnnotationImp::DlgPrefsTechDrawAnnotationImp( QWidget* parent ) : PreferencePage( parent ) { this->setupUi(this); - plsb_FontSize->setUnit(Base::Unit::Length); - plsb_FontSize->setMinimum(0); - plsb_ArrowSize->setUnit(Base::Unit::Length); - plsb_ArrowSize->setMinimum(0); pdsbBalloonKink->setUnit(Base::Unit::Length); pdsbBalloonKink->setMinimum(0); } -DlgPrefsTechDraw3Imp::~DlgPrefsTechDraw3Imp() +DlgPrefsTechDrawAnnotationImp::~DlgPrefsTechDrawAnnotationImp() { // no need to delete child widgets, Qt does it all for us } -void DlgPrefsTechDraw3Imp::saveSettings() +void DlgPrefsTechDrawAnnotationImp::saveSettings() { cbAutoHoriz->onSave(); - cbGlobalDecimals->onSave(); - cbHiddenLineStyle->onSave(); cbPrintCenterMarks->onSave(); - cbProjAngle->onSave(); cbPyramidOrtho->onSave(); cbSectionLineStd->onSave(); cbShowCenterMarks->onSave(); - cbShowUnits->onSave(); - leDiameter->onSave(); leLineGroup->onSave(); - pcbArrow->onSave(); pcbBalloonArrow->onSave(); pcbBalloonShape->onSave(); pcbCenterStyle->onSave(); pcbMatting->onSave(); pcbSectionStyle->onSave(); - pcbStandardAndStyle->onSave(); pdsbBalloonKink->onSave(); - plsb_ArrowSize->onSave(); - plsb_FontSize->onSave(); - sbAltDecimals->onSave(); cbCutSurface->onSave(); pcbHighlightStyle->onSave(); } -void DlgPrefsTechDraw3Imp::loadSettings() +void DlgPrefsTechDrawAnnotationImp::loadSettings() { //set defaults for Quantity widgets if property not found //Quantity widgets do not use preset value since they are based on //QAbstractSpinBox double kinkDefault = 5.0; pdsbBalloonKink->setValue(kinkDefault); - double arrowDefault = 5.0; - plsb_ArrowSize->setValue(arrowDefault); - double fontDefault = 4.0; - plsb_FontSize->setValue(fontDefault); cbAutoHoriz->onRestore(); - cbGlobalDecimals->onRestore(); - cbHiddenLineStyle->onRestore(); cbPrintCenterMarks->onRestore(); - cbProjAngle->onRestore(); cbPyramidOrtho->onRestore(); cbSectionLineStd->onRestore(); cbShowCenterMarks->onRestore(); - cbShowUnits->onRestore(); - leDiameter->onRestore(); leLineGroup->onRestore(); - pcbArrow->onRestore(); pcbBalloonArrow->onRestore(); pcbBalloonShape->onRestore(); pcbCenterStyle->onRestore(); pcbMatting->onRestore(); pcbSectionStyle->onRestore(); - pcbStandardAndStyle->onRestore(); pdsbBalloonKink->onRestore(); - plsb_ArrowSize->onRestore(); - plsb_FontSize->onRestore(); - sbAltDecimals->onRestore(); cbCutSurface->onRestore(); pcbHighlightStyle->onRestore(); DrawGuiUtil::loadArrowBox(pcbBalloonArrow); pcbBalloonArrow->setCurrentIndex(prefBalloonArrow()); - DrawGuiUtil::loadArrowBox(pcbArrow); - pcbArrow->setCurrentIndex(prefArrowStyle()); } /** * Sets the strings of the subwidgets using the current language. */ -void DlgPrefsTechDraw3Imp::changeEvent(QEvent *e) +void DlgPrefsTechDrawAnnotationImp::changeEvent(QEvent *e) { if (e->type() == QEvent::LanguageChange) { saveSettings(); @@ -141,7 +111,7 @@ void DlgPrefsTechDraw3Imp::changeEvent(QEvent *e) } } -int DlgPrefsTechDraw3Imp::prefBalloonArrow(void) const +int DlgPrefsTechDrawAnnotationImp::prefBalloonArrow(void) const { Base::Reference hGrp = App::GetApplication().GetUserParameter(). GetGroup("BaseApp")->GetGroup("Preferences")-> @@ -150,15 +120,4 @@ int DlgPrefsTechDraw3Imp::prefBalloonArrow(void) const return end; } -int DlgPrefsTechDraw3Imp::prefArrowStyle(void) const -{ - Base::Reference hGrp = App::GetApplication().GetUserParameter(). - GetGroup("BaseApp")->GetGroup("Preferences")-> - GetGroup("Mod/TechDraw/Dimensions"); - int style = hGrp->GetInt("ArrowStyle", 0); - return style; -} - - - -#include +#include diff --git a/src/Mod/TechDraw/Gui/DlgPrefsTechDrawAnnotationImp.h b/src/Mod/TechDraw/Gui/DlgPrefsTechDrawAnnotationImp.h new file mode 100644 index 0000000000..22b9ed283c --- /dev/null +++ b/src/Mod/TechDraw/Gui/DlgPrefsTechDrawAnnotationImp.h @@ -0,0 +1,51 @@ + /************************************************************************** + * Copyright (c) 2020 FreeCAD Developers * + * Author: Uwe Stöhr * + * * + * This file is part of the FreeCAD CAx development system. * + * * + * This library is free software; you can redistribute it and/or * + * modify it under the terms of the GNU Library General Public * + * License as published by the Free Software Foundation; either * + * version 2 of the License, or (at your option) any later version. * + * * + * This library is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU Library General Public License for more details. * + * * + * You should have received a copy of the GNU Library General Public * + * License along with this library; see the file COPYING.LIB. If not, * + * write to the Free Software Foundation, Inc., 59 Temple Place, * + * Suite 330, Boston, MA 02111-1307, USA * + * * + ***************************************************************************/ + + +#ifndef DRAWINGGUI_DLGPREFSTECHDRAWIMPANNOTATION_H +#define DRAWINGGUI_DLGPREFSTECHDRAWIMPANNOTATION_H + +#include +#include + +namespace TechDrawGui { + +class DlgPrefsTechDrawAnnotationImp : public Gui::Dialog::PreferencePage, public Ui_DlgPrefsTechDrawAnnotationImp +{ + Q_OBJECT + +public: + DlgPrefsTechDrawAnnotationImp( QWidget* parent = 0 ); + ~DlgPrefsTechDrawAnnotationImp(); + +protected: + void saveSettings(); + void loadSettings(); + void changeEvent(QEvent *e); + + int prefBalloonArrow(void) const; +}; + +} // namespace TechDrawGui + +#endif // DRAWINGGUI_DLGPREFSTECHDRAWIMPANNOTATION_H diff --git a/src/Mod/TechDraw/Gui/DlgPrefsTechDrawColors.ui b/src/Mod/TechDraw/Gui/DlgPrefsTechDrawColors.ui new file mode 100644 index 0000000000..80b91a87d6 --- /dev/null +++ b/src/Mod/TechDraw/Gui/DlgPrefsTechDrawColors.ui @@ -0,0 +1,573 @@ + + + TechDrawGui::DlgPrefsTechDrawColorsImp + + + + 0 + 0 + 460 + 369 + + + + + 0 + 0 + + + + + 0 + 0 + + + + Colors + + + + + + + + + + 0 + 0 + + + + + 0 + 225 + + + + + 0 + 200 + + + + Colors + + + + + + + + + true + + + + Normal + + + + + + + Normal line color + + + + 0 + 0 + 0 + + + + NormalColor + + + Mod/TechDraw/Colors + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + true + + + + Hidden Line + + + + + + + Hidden line color + + + HiddenColor + + + Mod/TechDraw/Colors + + + + + + + + true + + + + Preselected + + + + + + + Preselection color + + + + 255 + 255 + 20 + + + + PreSelectColor + + + Mod/TechDraw/Colors + + + + + + + + true + + + + Section Face + + + + + + + Section face color + + + + 225 + 225 + 225 + + + + CutSurfaceColor + + + Mod/TechDraw/Colors + + + + + + + + true + + + + Selected + + + + + + + Selected item color + + + + 28 + 173 + 28 + + + + SelectColor + + + Mod/TechDraw/Colors + + + + + + + Section Line + + + + + + + Section line color + + + SectionColor + + + /Mod/TechDraw/Decorations + + + + + + + + true + + + + Background + + + + + + + Background color around pages + + + + 80 + 80 + 80 + + + + Background + + + /Mod/TechDraw/Colors + + + + + + + + true + + + + Hatch + + + + + + + Hatch image color + + + + 0 + 0 + 0 + + + + Hatch + + + /Mod/TechDraw/Colors + + + + + + + Dimension + + + + + + + Color of dimension lines and text. + + + + 0 + 0 + 0 + + + + Color + + + Mod/TechDraw/Dimensions + + + + + + + + true + + + + Geometric Hatch + + + + + + + Geometric hatch pattern color + + + + 0 + 0 + 0 + + + + GeomHatch + + + /Mod/TechDraw/Colors + + + + + + + Centerline + + + + + + + Centerline color + + + CenterColor + + + Mod/TechDraw/Decorations + + + + + + + Vertex + + + + + + + Color of vertices in views + + + VertexColor + + + Mod/TechDraw/Decorations + + + + + + + + 0 + 20 + + + + + true + + + + Object faces will be transparent + + + Transparent Faces + + + ClearFace + + + /Mod/TechDraw/Colors + + + + + + + Face color (if not transparent) + + + + 255 + 255 + 255 + + + + FaceColor + + + /Mod/TechDraw/Colors + + + + + + + + true + + + + Detail Highlight + + + + + + + + true + + + + Leaderline + + + + + + + Default color for leader lines + + + + 0 + 0 + 0 + + + + Color + + + Mod/TechDraw/Markups + + + + + + + + 0 + 0 + 0 + + + + HighlightColor + + + /Mod/TechDraw/Decorations + + + + + + + + + + + + + 12 + true + + + + QFrame::Box + + + Items in italics are default values for new objects. They have no effect on existing objects. + + + true + + + + + + + Qt::Vertical + + + + 20 + 19 + + + + + + + + + Gui::ColorButton + QPushButton +
Gui/Widgets.h
+
+ + Gui::PrefColorButton + Gui::ColorButton +
Gui/PrefWidgets.h
+
+ + Gui::PrefCheckBox + QCheckBox +
Gui/PrefWidgets.h
+
+
+ + +
diff --git a/src/Mod/TechDraw/Gui/DlgPrefsTechDrawColorsImp.cpp b/src/Mod/TechDraw/Gui/DlgPrefsTechDrawColorsImp.cpp new file mode 100644 index 0000000000..b3fdea7de1 --- /dev/null +++ b/src/Mod/TechDraw/Gui/DlgPrefsTechDrawColorsImp.cpp @@ -0,0 +1,99 @@ +/*************************************************************************** + * Copyright (c) 2020 FreeCAD Developers * + * Author: Uwe Stöhr * + * Based on src/Mod/FEM/Gui/DlgSettingsFEMImp.cpp * + * * + * This file is part of the FreeCAD CAx development system. * + * * + * This library is free software; you can redistribute it and/or * + * modify it under the terms of the GNU Library General Public * + * License as published by the Free Software Foundation; either * + * version 2 of the License, or (at your option) any later version. * + * * + * This library is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU Library General Public License for more details. * + * * + * You should have received a copy of the GNU Library General Public * + * License along with this library; see the file COPYING.LIB. If not, * + * write to the Free Software Foundation, Inc., 59 Temple Place, * + * Suite 330, Boston, MA 02111-1307, USA * + * * + ***************************************************************************/ + + +#include "PreCompiled.h" + +#include "DlgPrefsTechDrawColorsImp.h" +#include + +using namespace TechDrawGui; + +DlgPrefsTechDrawColorsImp::DlgPrefsTechDrawColorsImp( QWidget* parent ) + : PreferencePage( parent ) +{ + this->setupUi(this); +} + +DlgPrefsTechDrawColorsImp::~DlgPrefsTechDrawColorsImp() +{ + // no need to delete child widgets, Qt does it all for us +} + +void DlgPrefsTechDrawColorsImp::saveSettings() +{ + pcbDimColor->onSave(); + pcb_Hatch->onSave(); + pcb_Background->onSave(); + pcb_PreSelect->onSave(); + pcb_Hidden->onSave(); + pcb_Select->onSave(); + pcb_Normal->onSave(); + pcb_Surface->onSave(); + pcb_GeomHatch->onSave(); + pcb_Face->onSave(); + pcb_PaintFaces->onSave(); + pcbSectionLine->onSave(); + pcbCenterColor->onSave(); + pcbVertexColor->onSave(); + pcbMarkup->onSave(); + pcbHighlight->onSave(); +} + +void DlgPrefsTechDrawColorsImp::loadSettings() +{ + pcbDimColor->onRestore(); + pcb_Hatch->onRestore(); + pcb_Background->onRestore(); + pcb_PreSelect->onRestore(); + pcb_Hidden->onRestore(); + pcb_Select->onRestore(); + pcb_Normal->onRestore(); + pcb_Surface->onRestore(); + pcb_GeomHatch->onRestore(); + pcb_Face->onRestore(); + pcb_PaintFaces->onRestore(); + pcbSectionLine->onRestore(); + pcbCenterColor->onRestore(); + pcbVertexColor->onRestore(); + pcbMarkup->onRestore(); + pcbHighlight->onRestore(); +} + +/** + * Sets the strings of the subwidgets using the current language. + */ +void DlgPrefsTechDrawColorsImp::changeEvent(QEvent *e) +{ + if (e->type() == QEvent::LanguageChange) { + saveSettings(); + retranslateUi(this); + loadSettings(); + } + else { + QWidget::changeEvent(e); + } +} + +#include diff --git a/src/Mod/TechDraw/Gui/DlgPrefsTechDrawColorsImp.h b/src/Mod/TechDraw/Gui/DlgPrefsTechDrawColorsImp.h new file mode 100644 index 0000000000..df4422960f --- /dev/null +++ b/src/Mod/TechDraw/Gui/DlgPrefsTechDrawColorsImp.h @@ -0,0 +1,49 @@ + /************************************************************************** + * Copyright (c) 2020 FreeCAD Developers * + * Author: Uwe Stöhr * + * * + * This file is part of the FreeCAD CAx development system. * + * * + * This library is free software; you can redistribute it and/or * + * modify it under the terms of the GNU Library General Public * + * License as published by the Free Software Foundation; either * + * version 2 of the License, or (at your option) any later version. * + * * + * This library is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU Library General Public License for more details. * + * * + * You should have received a copy of the GNU Library General Public * + * License along with this library; see the file COPYING.LIB. If not, * + * write to the Free Software Foundation, Inc., 59 Temple Place, * + * Suite 330, Boston, MA 02111-1307, USA * + * * + ***************************************************************************/ + + +#ifndef DRAWINGGUI_DLGPREFSTECHDRAWIMPCOLORS_H +#define DRAWINGGUI_DLGPREFSTECHDRAWIMPCOLORS_H + +#include +#include + +namespace TechDrawGui { + +class DlgPrefsTechDrawColorsImp : public Gui::Dialog::PreferencePage, public Ui_DlgPrefsTechDrawColorsImp +{ + Q_OBJECT + +public: + DlgPrefsTechDrawColorsImp( QWidget* parent = 0 ); + ~DlgPrefsTechDrawColorsImp(); + +protected: + void saveSettings(); + void loadSettings(); + void changeEvent(QEvent *e); +}; + +} // namespace TechDrawGui + +#endif // DRAWINGGUI_DLGPREFSTECHDRAWIMPCOLORS_H diff --git a/src/Mod/TechDraw/Gui/DlgPrefsTechDrawDimensions.ui b/src/Mod/TechDraw/Gui/DlgPrefsTechDrawDimensions.ui new file mode 100644 index 0000000000..7246bbd49f --- /dev/null +++ b/src/Mod/TechDraw/Gui/DlgPrefsTechDrawDimensions.ui @@ -0,0 +1,617 @@ + + + TechDrawGui::DlgPrefsTechDrawDimensionsImp + + + + 0 + 0 + 460 + 425 + + + + + 0 + 0 + + + + Dimensions + + + + + + + 0 + 0 + + + + + 0 + 0 + + + + + 0 + 0 + + + + Dimensions + + + + + + + + + 0 + 0 + + + + Standard and Style + + + + + + + + 0 + 0 + + + + + 184 + 22 + + + + Standard to be used for dimensional values + + + StandardAndStyle + + + /Mod/TechDraw/Dimensions + + + + ISO Oriented + + + + + ISO Referencing + + + + + ASME Inlined + + + + + ASME Referencing + + + + + + + + + 0 + 0 + + + + Use system setting for number of decimals + + + Use Global Decimals + + + true + + + UseGlobalDecimals + + + /Mod/TechDraw/Dimensions + + + + + + + + 0 + 0 + + + + + 0 + 22 + + + + Append unit to dimension values + + + Show Units + + + ShowUnits + + + /Mod/TechDraw/Dimensions + + + + + + + Alternate Decimals + + + + + + + false + + + + 0 + 0 + + + + + 0 + 22 + + + + Number of decimals if 'Use Global Decimals' is not used + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + 2 + + + AltDecimals + + + /Mod/TechDraw/Dimensions + + + + + + + + true + + + + Font Size + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 0 + 0 + + + + + 0 + 22 + + + + Dimension text font size + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + 4.000000000000000 + + + FontSize + + + /Mod/TechDraw/Dimensions + + + + + + + Diameter Symbol + + + + + + + + 0 + 0 + + + + + 0 + 22 + + + + + 12 + + + + Character used to indicate diameter dimensions + + + ⌀ + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + DiameterSymbol + + + /Mod/TechDraw/Dimensions + + + + + + + + true + + + + Arrow Style + + + + + + + + 0 + 0 + + + + + 0 + 22 + + + + Arrowhead style + + + -1 + + + ArrowStyle + + + Mod/TechDraw/Dimensions + + + + + + + + true + + + + Arrow Size + + + + + + + + 0 + 0 + + + + + 0 + 22 + + + + Arrowhead size + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + 5.000000000000000 + + + ArrowSize + + + Mod/TechDraw/Dimensions + + + + + + + + + + + + + 0 + 0 + + + + + 0 + 85 + + + + + 16777215 + 500 + + + + + 0 + 500 + + + + Conventions + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Projection Group Angle + + + + + + + + 0 + 0 + + + + + 184 + 0 + + + + Use first- or third-angle mutliview projection convention + + + ProjectionAngle + + + Mod/TechDraw/General + + + + First + + + + + Third + + + + + + + + Hidden Line Style + + + + + + + + 0 + 0 + + + + + 0 + 0 + + + + Style for hidden lines + + + HiddenLine + + + Mod/TechDraw/General + + + + Continuous + + + + :/icons/continuous-line.svg:/icons/continuous-line.svg + + + + + Dashed + + + + :/icons/dash-line.svg:/icons/dash-line.svg + + + + + + + + + + + + + + 12 + true + + + + QFrame::Box + + + Items in italics are default values for new objects. They have no effect on existing objects. + + + true + + + + + + + Qt::Vertical + + + + 20 + 20 + + + + + + + + + Gui::QuantitySpinBox + QWidget +
Gui/QuantitySpinBox.h
+
+ + Gui::PrefSpinBox + QSpinBox +
Gui/PrefWidgets.h
+
+ + Gui::PrefCheckBox + QCheckBox +
Gui/PrefWidgets.h
+
+ + Gui::PrefComboBox + QComboBox +
Gui/PrefWidgets.h
+
+ + Gui::PrefLineEdit + QLineEdit +
Gui/PrefWidgets.h
+
+ + Gui::PrefUnitSpinBox + Gui::QuantitySpinBox +
Gui/PrefWidgets.h
+
+
+ + + + + + cbGlobalDecimals + toggled(bool) + sbAltDecimals + setDisabled(bool) + + + 108 + 71 + + + 425 + 124 + + + + +
diff --git a/src/Mod/TechDraw/Gui/DlgPrefsTechDrawDimensionsImp.cpp b/src/Mod/TechDraw/Gui/DlgPrefsTechDrawDimensionsImp.cpp new file mode 100644 index 0000000000..bb35d48211 --- /dev/null +++ b/src/Mod/TechDraw/Gui/DlgPrefsTechDrawDimensionsImp.cpp @@ -0,0 +1,119 @@ +/*************************************************************************** + * Copyright (c) 2015 FreeCAD Developers * + * Author: WandererFan * + * Based on src/Mod/FEM/Gui/DlgSettingsFEMImp.cpp * + * * + * This file is part of the FreeCAD CAx development system. * + * * + * This library is free software; you can redistribute it and/or * + * modify it under the terms of the GNU Library General Public * + * License as published by the Free Software Foundation; either * + * version 2 of the License, or (at your option) any later version. * + * * + * This library is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU Library General Public License for more details. * + * * + * You should have received a copy of the GNU Library General Public * + * License along with this library; see the file COPYING.LIB. If not, * + * write to the Free Software Foundation, Inc., 59 Temple Place, * + * Suite 330, Boston, MA 02111-1307, USA * + * * + ***************************************************************************/ + + +#include "PreCompiled.h" + +#include + +#include +#include + +#include "DrawGuiUtil.h" +#include "DlgPrefsTechDrawDimensionsImp.h" + + +using namespace TechDrawGui; +using namespace TechDraw; + + +DlgPrefsTechDrawDimensionsImp::DlgPrefsTechDrawDimensionsImp( QWidget* parent ) + : PreferencePage( parent ) +{ + this->setupUi(this); + plsb_FontSize->setUnit(Base::Unit::Length); + plsb_FontSize->setMinimum(0); + plsb_ArrowSize->setUnit(Base::Unit::Length); + plsb_ArrowSize->setMinimum(0); +} + +DlgPrefsTechDrawDimensionsImp::~DlgPrefsTechDrawDimensionsImp() +{ + // no need to delete child widgets, Qt does it all for us +} + +void DlgPrefsTechDrawDimensionsImp::saveSettings() +{ + cbGlobalDecimals->onSave(); + cbHiddenLineStyle->onSave(); + cbProjAngle->onSave(); + cbShowUnits->onSave(); + leDiameter->onSave(); + pcbArrow->onSave(); + pcbStandardAndStyle->onSave(); + plsb_ArrowSize->onSave(); + plsb_FontSize->onSave(); + sbAltDecimals->onSave(); +} + +void DlgPrefsTechDrawDimensionsImp::loadSettings() +{ + //set defaults for Quantity widgets if property not found + //Quantity widgets do not use preset value since they are based on + //QAbstractSpinBox + double arrowDefault = 5.0; + plsb_ArrowSize->setValue(arrowDefault); + double fontDefault = 4.0; + plsb_FontSize->setValue(fontDefault); + + cbGlobalDecimals->onRestore(); + cbHiddenLineStyle->onRestore(); + cbProjAngle->onRestore(); + cbShowUnits->onRestore(); + leDiameter->onRestore(); + pcbArrow->onRestore(); + pcbStandardAndStyle->onRestore(); + plsb_ArrowSize->onRestore(); + plsb_FontSize->onRestore(); + sbAltDecimals->onRestore(); + + DrawGuiUtil::loadArrowBox(pcbArrow); + pcbArrow->setCurrentIndex(prefArrowStyle()); +} + +/** + * Sets the strings of the subwidgets using the current language. + */ +void DlgPrefsTechDrawDimensionsImp::changeEvent(QEvent *e) +{ + if (e->type() == QEvent::LanguageChange) { + saveSettings(); + retranslateUi(this); + loadSettings(); + } + else { + QWidget::changeEvent(e); + } +} + +int DlgPrefsTechDrawDimensionsImp::prefArrowStyle(void) const +{ + Base::Reference hGrp = App::GetApplication().GetUserParameter(). + GetGroup("BaseApp")->GetGroup("Preferences")-> + GetGroup("Mod/TechDraw/Dimensions"); + int style = hGrp->GetInt("ArrowStyle", 0); + return style; +} + +#include diff --git a/src/Mod/TechDraw/Gui/DlgPrefsTechDraw3Imp.h b/src/Mod/TechDraw/Gui/DlgPrefsTechDrawDimensionsImp.h similarity index 80% rename from src/Mod/TechDraw/Gui/DlgPrefsTechDraw3Imp.h rename to src/Mod/TechDraw/Gui/DlgPrefsTechDrawDimensionsImp.h index 5f54564c0a..da6f747646 100644 --- a/src/Mod/TechDraw/Gui/DlgPrefsTechDraw3Imp.h +++ b/src/Mod/TechDraw/Gui/DlgPrefsTechDrawDimensionsImp.h @@ -1,7 +1,6 @@ /************************************************************************** * Copyright (c) 2015 FreeCAD Developers * * Author: WandererFan * - * Based on src/Mod/FEM/Gui/DlgPrefsTechDraw3Imp.cpp * * * * This file is part of the FreeCAD CAx development system. * * * @@ -23,33 +22,31 @@ ***************************************************************************/ -#ifndef DRAWINGGUI_DLGPREFSTECHDRAWIMP3_H -#define DRAWINGGUI_DLGPREFSTECHDRAWIMP3_H +#ifndef DRAWINGGUI_DLGPREFSTECHDRAWIMPDIMENSIONS_H +#define DRAWINGGUI_DLGPREFSTECHDRAWIMPDIMENSIONS_H -#include +#include #include namespace TechDrawGui { -class DlgPrefsTechDraw3Imp : public Gui::Dialog::PreferencePage, public Ui_DlgPrefsTechDraw3Imp +class DlgPrefsTechDrawDimensionsImp : public Gui::Dialog::PreferencePage, public Ui_DlgPrefsTechDrawDimensionsImp { Q_OBJECT public: - DlgPrefsTechDraw3Imp( QWidget* parent = 0 ); - ~DlgPrefsTechDraw3Imp(); + DlgPrefsTechDrawDimensionsImp( QWidget* parent = 0 ); + ~DlgPrefsTechDrawDimensionsImp(); protected: void saveSettings(); void loadSettings(); void changeEvent(QEvent *e); - - int prefBalloonArrow(void) const; - int prefArrowStyle(void) const; + int prefArrowStyle(void) const; }; } // namespace TechDrawGui -#endif // DRAWINGGUI_DLGPREFSTECHDRAWIMP3_H +#endif // DRAWINGGUI_DLGPREFSTECHDRAWIMPDIMENSIONS_H diff --git a/src/Mod/TechDraw/Gui/DlgPrefsTechDraw1.ui b/src/Mod/TechDraw/Gui/DlgPrefsTechDrawGeneral.ui similarity index 58% rename from src/Mod/TechDraw/Gui/DlgPrefsTechDraw1.ui rename to src/Mod/TechDraw/Gui/DlgPrefsTechDrawGeneral.ui index fec43f04a1..d4504a266a 100644 --- a/src/Mod/TechDraw/Gui/DlgPrefsTechDraw1.ui +++ b/src/Mod/TechDraw/Gui/DlgPrefsTechDrawGeneral.ui @@ -1,13 +1,13 @@ - TechDrawGui::DlgPrefsTechDraw1Imp - + TechDrawGui::DlgPrefsTechDrawGeneralImp + 0 0 460 - 760 + 510 @@ -185,494 +185,6 @@ for ProjectionGroups
- - - - - 0 - 0 - - - - - 0 - 225 - - - - - 0 - 200 - - - - Colors - - - - - - - - - true - - - - Normal - - - - - - - Normal line color - - - - 0 - 0 - 0 - - - - NormalColor - - - Mod/TechDraw/Colors - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - true - - - - Hidden Line - - - - - - - Hidden line color - - - HiddenColor - - - Mod/TechDraw/Colors - - - - - - - - true - - - - Preselected - - - - - - - Preselection color - - - - 255 - 255 - 20 - - - - PreSelectColor - - - Mod/TechDraw/Colors - - - - - - - - true - - - - Section Face - - - - - - - Section face color - - - - 225 - 225 - 225 - - - - CutSurfaceColor - - - Mod/TechDraw/Colors - - - - - - - - true - - - - Selected - - - - - - - Selected item color - - - - 28 - 173 - 28 - - - - SelectColor - - - Mod/TechDraw/Colors - - - - - - - Section Line - - - - - - - Section line color - - - SectionColor - - - /Mod/TechDraw/Decorations - - - - - - - - true - - - - Background - - - - - - - Background color around pages - - - - 80 - 80 - 80 - - - - Background - - - /Mod/TechDraw/Colors - - - - - - - - true - - - - Hatch - - - - - - - Hatch image color - - - - 0 - 0 - 0 - - - - Hatch - - - /Mod/TechDraw/Colors - - - - - - - Dimension - - - - - - - Color of dimension lines and text. - - - - 0 - 0 - 0 - - - - Color - - - Mod/TechDraw/Dimensions - - - - - - - - true - - - - Geometric Hatch - - - - - - - Geometric hatch pattern color - - - - 0 - 0 - 0 - - - - GeomHatch - - - /Mod/TechDraw/Colors - - - - - - - Centerline - - - - - - - Centerline color - - - CenterColor - - - Mod/TechDraw/Decorations - - - - - - - Vertex - - - - - - - Color of vertices in views - - - VertexColor - - - Mod/TechDraw/Decorations - - - - - - - - 0 - 20 - - - - - true - - - - Object faces will be transparent - - - Transparent Faces - - - ClearFace - - - /Mod/TechDraw/Colors - - - - - - - Face color (if not transparent) - - - - 255 - 255 - 255 - - - - FaceColor - - - /Mod/TechDraw/Colors - - - - - - - - true - - - - Detail Highlight - - - - - - - - true - - - - Leaderline - - - - - - - Default color for leader lines - - - - 0 - 0 - 0 - - - - Color - - - Mod/TechDraw/Markups - - - - - - - - 0 - 0 - 0 - - - - HighlightColor - - - /Mod/TechDraw/Decorations - - - - - - - - @@ -1162,21 +674,11 @@ for ProjectionGroups QWidget
Gui/QuantitySpinBox.h
- - Gui::ColorButton - QPushButton -
Gui/Widgets.h
-
Gui::PrefFileChooser Gui::FileChooser
Gui/PrefWidgets.h
- - Gui::PrefColorButton - Gui::ColorButton -
Gui/PrefWidgets.h
-
Gui::PrefCheckBox QCheckBox diff --git a/src/Mod/TechDraw/Gui/DlgPrefsTechDraw1Imp.cpp b/src/Mod/TechDraw/Gui/DlgPrefsTechDrawGeneralImp.cpp similarity index 71% rename from src/Mod/TechDraw/Gui/DlgPrefsTechDraw1Imp.cpp rename to src/Mod/TechDraw/Gui/DlgPrefsTechDrawGeneralImp.cpp index 4c3c299027..52d4bc053b 100644 --- a/src/Mod/TechDraw/Gui/DlgPrefsTechDraw1Imp.cpp +++ b/src/Mod/TechDraw/Gui/DlgPrefsTechDrawGeneralImp.cpp @@ -25,12 +25,12 @@ #include "PreCompiled.h" -#include "DlgPrefsTechDraw1Imp.h" +#include "DlgPrefsTechDrawGeneralImp.h" #include using namespace TechDrawGui; -DlgPrefsTechDraw1Imp::DlgPrefsTechDraw1Imp( QWidget* parent ) +DlgPrefsTechDrawGeneralImp::DlgPrefsTechDrawGeneralImp( QWidget* parent ) : PreferencePage( parent ) { this->setupUi(this); @@ -38,12 +38,12 @@ DlgPrefsTechDraw1Imp::DlgPrefsTechDraw1Imp( QWidget* parent ) plsb_LabelSize->setMinimum(0); } -DlgPrefsTechDraw1Imp::~DlgPrefsTechDraw1Imp() +DlgPrefsTechDrawGeneralImp::~DlgPrefsTechDrawGeneralImp() { // no need to delete child widgets, Qt does it all for us } -void DlgPrefsTechDraw1Imp::saveSettings() +void DlgPrefsTechDrawGeneralImp::saveSettings() { pfc_DefTemp->onSave(); pfc_DefDir->onSave(); @@ -60,27 +60,9 @@ void DlgPrefsTechDraw1Imp::saveSettings() cb_Override->onSave(); cb_PageUpdate->onSave(); cb_AutoDist->onSave(); - - pcbDimColor->onSave(); - pcb_Hatch->onSave(); - pcb_Background->onSave(); - pcb_PreSelect->onSave(); - pcb_Hidden->onSave(); - pcb_Select->onSave(); - pcb_Normal->onSave(); - pcb_Surface->onSave(); - pcb_GeomHatch->onSave(); - pcb_Face->onSave(); - pcb_PaintFaces->onSave(); - pcbSectionLine->onSave(); - pcbCenterColor->onSave(); - pcbVertexColor->onSave(); - - pcbMarkup->onSave(); - pcbHighlight->onSave(); } -void DlgPrefsTechDraw1Imp::loadSettings() +void DlgPrefsTechDrawGeneralImp::loadSettings() { double labelDefault = 8.0; plsb_LabelSize->setValue(labelDefault); @@ -100,30 +82,12 @@ void DlgPrefsTechDraw1Imp::loadSettings() cb_Override->onRestore(); cb_PageUpdate->onRestore(); cb_AutoDist->onRestore(); - - pcbDimColor->onRestore(); - pcb_Hatch->onRestore(); - pcb_Background->onRestore(); - pcb_PreSelect->onRestore(); - pcb_Hidden->onRestore(); - pcb_Select->onRestore(); - pcb_Normal->onRestore(); - pcb_Surface->onRestore(); - pcb_GeomHatch->onRestore(); - pcb_Face->onRestore(); - pcb_PaintFaces->onRestore(); - pcbSectionLine->onRestore(); - pcbCenterColor->onRestore(); - pcbVertexColor->onRestore(); - - pcbMarkup->onRestore(); - pcbHighlight->onRestore(); } /** * Sets the strings of the subwidgets using the current language. */ -void DlgPrefsTechDraw1Imp::changeEvent(QEvent *e) +void DlgPrefsTechDrawGeneralImp::changeEvent(QEvent *e) { if (e->type() == QEvent::LanguageChange) { saveSettings(); @@ -135,4 +99,4 @@ void DlgPrefsTechDraw1Imp::changeEvent(QEvent *e) } } -#include +#include diff --git a/src/Mod/TechDraw/Gui/DlgPrefsTechDraw5Imp.h b/src/Mod/TechDraw/Gui/DlgPrefsTechDrawGeneralImp.h similarity index 81% rename from src/Mod/TechDraw/Gui/DlgPrefsTechDraw5Imp.h rename to src/Mod/TechDraw/Gui/DlgPrefsTechDrawGeneralImp.h index 4bc1b28bbc..cde3671f92 100644 --- a/src/Mod/TechDraw/Gui/DlgPrefsTechDraw5Imp.h +++ b/src/Mod/TechDraw/Gui/DlgPrefsTechDrawGeneralImp.h @@ -1,7 +1,6 @@ /************************************************************************** * Copyright (c) 2015 FreeCAD Developers * * Author: WandererFan * - * Based on src/Mod/FEM/Gui/DlgPrefsTechDraw5Imp.cpp * * * * This file is part of the FreeCAD CAx development system. * * * @@ -23,21 +22,21 @@ ***************************************************************************/ -#ifndef DRAWINGGUI_DLGPREFSTECHDRAWIMP5_H -#define DRAWINGGUI_DLGPREFSTECHDRAWIMP5_H +#ifndef DRAWINGGUI_DLGPREFSTECHDRAWIMPGENERAL_H +#define DRAWINGGUI_DLGPREFSTECHDRAWIMPGENERAL_H -#include +#include #include namespace TechDrawGui { -class DlgPrefsTechDraw5Imp : public Gui::Dialog::PreferencePage, public Ui_DlgPrefsTechDraw5Imp +class DlgPrefsTechDrawGeneralImp : public Gui::Dialog::PreferencePage, public Ui_DlgPrefsTechDrawGeneralImp { Q_OBJECT public: - DlgPrefsTechDraw5Imp( QWidget* parent = 0 ); - ~DlgPrefsTechDraw5Imp(); + DlgPrefsTechDrawGeneralImp( QWidget* parent = 0 ); + ~DlgPrefsTechDrawGeneralImp(); protected: void saveSettings(); @@ -47,4 +46,4 @@ protected: } // namespace TechDrawGui -#endif // DRAWINGGUI_DLGPREFSTECHDRAWIMP5_H +#endif // DRAWINGGUI_DLGPREFSTECHDRAWIMPGENERAL_H diff --git a/src/Mod/TechDraw/Gui/DlgPrefsTechDraw5.ui b/src/Mod/TechDraw/Gui/DlgPrefsTechDrawHLR.ui similarity index 98% rename from src/Mod/TechDraw/Gui/DlgPrefsTechDraw5.ui rename to src/Mod/TechDraw/Gui/DlgPrefsTechDrawHLR.ui index 3e5df3bc9d..9b011b92b9 100644 --- a/src/Mod/TechDraw/Gui/DlgPrefsTechDraw5.ui +++ b/src/Mod/TechDraw/Gui/DlgPrefsTechDrawHLR.ui @@ -1,7 +1,7 @@ - TechDrawGui::DlgPrefsTechDraw5Imp - + TechDrawGui::DlgPrefsTechDrawHLRImp + 0 @@ -23,7 +23,7 @@ - HLR Parameters + HLR diff --git a/src/Mod/TechDraw/Gui/DlgPrefsTechDraw5Imp.cpp b/src/Mod/TechDraw/Gui/DlgPrefsTechDrawHLRImp.cpp similarity index 88% rename from src/Mod/TechDraw/Gui/DlgPrefsTechDraw5Imp.cpp rename to src/Mod/TechDraw/Gui/DlgPrefsTechDrawHLRImp.cpp index 1a78fc83a1..96ec646b79 100644 --- a/src/Mod/TechDraw/Gui/DlgPrefsTechDraw5Imp.cpp +++ b/src/Mod/TechDraw/Gui/DlgPrefsTechDrawHLRImp.cpp @@ -25,23 +25,23 @@ #include "PreCompiled.h" -#include "DlgPrefsTechDraw5Imp.h" +#include "DlgPrefsTechDrawHLRImp.h" #include using namespace TechDrawGui; -DlgPrefsTechDraw5Imp::DlgPrefsTechDraw5Imp( QWidget* parent ) +DlgPrefsTechDrawHLRImp::DlgPrefsTechDrawHLRImp( QWidget* parent ) : PreferencePage( parent ) { this->setupUi(this); } -DlgPrefsTechDraw5Imp::~DlgPrefsTechDraw5Imp() +DlgPrefsTechDrawHLRImp::~DlgPrefsTechDrawHLRImp() { // no need to delete child widgets, Qt does it all for us } -void DlgPrefsTechDraw5Imp::saveSettings() +void DlgPrefsTechDrawHLRImp::saveSettings() { pcbSeamViz->onSave(); pcbSmoothViz->onSave(); @@ -55,7 +55,7 @@ void DlgPrefsTechDraw5Imp::saveSettings() pcbHardHid->onSave(); } -void DlgPrefsTechDraw5Imp::loadSettings() +void DlgPrefsTechDrawHLRImp::loadSettings() { pcbSeamViz->onRestore(); pcbSmoothViz->onRestore(); @@ -72,7 +72,7 @@ void DlgPrefsTechDraw5Imp::loadSettings() /** * Sets the strings of the subwidgets using the current language. */ -void DlgPrefsTechDraw5Imp::changeEvent(QEvent *e) +void DlgPrefsTechDrawHLRImp::changeEvent(QEvent *e) { if (e->type() == QEvent::LanguageChange) { saveSettings(); @@ -84,4 +84,4 @@ void DlgPrefsTechDraw5Imp::changeEvent(QEvent *e) } } -#include +#include diff --git a/src/Mod/TechDraw/Gui/DlgPrefsTechDraw1Imp.h b/src/Mod/TechDraw/Gui/DlgPrefsTechDrawHLRImp.h similarity index 81% rename from src/Mod/TechDraw/Gui/DlgPrefsTechDraw1Imp.h rename to src/Mod/TechDraw/Gui/DlgPrefsTechDrawHLRImp.h index e97fa37b3d..1d0a86ede3 100644 --- a/src/Mod/TechDraw/Gui/DlgPrefsTechDraw1Imp.h +++ b/src/Mod/TechDraw/Gui/DlgPrefsTechDrawHLRImp.h @@ -1,7 +1,6 @@ /************************************************************************** * Copyright (c) 2015 FreeCAD Developers * * Author: WandererFan * - * Based on src/Mod/FEM/Gui/DlgPrefsTechDraw1Imp.cpp * * * * This file is part of the FreeCAD CAx development system. * * * @@ -23,21 +22,21 @@ ***************************************************************************/ -#ifndef DRAWINGGUI_DLGPREFSTECHDRAWIMP1_H -#define DRAWINGGUI_DLGPREFSTECHDRAWIMP1_H +#ifndef DRAWINGGUI_DLGPREFSTECHDRAWIMPHLR_H +#define DRAWINGGUI_DLGPREFSTECHDRAWIMPHLR_H -#include +#include #include namespace TechDrawGui { -class DlgPrefsTechDraw1Imp : public Gui::Dialog::PreferencePage, public Ui_DlgPrefsTechDraw1Imp +class DlgPrefsTechDrawHLRImp : public Gui::Dialog::PreferencePage, public Ui_DlgPrefsTechDrawHLRImp { Q_OBJECT public: - DlgPrefsTechDraw1Imp( QWidget* parent = 0 ); - ~DlgPrefsTechDraw1Imp(); + DlgPrefsTechDrawHLRImp( QWidget* parent = 0 ); + ~DlgPrefsTechDrawHLRImp(); protected: void saveSettings(); @@ -47,4 +46,4 @@ protected: } // namespace TechDrawGui -#endif // DRAWINGGUI_DLGPREFSTECHDRAWIMP1_H +#endif // DRAWINGGUI_DLGPREFSTECHDRAWIMPHLR_H diff --git a/src/Mod/TechDraw/Gui/DlgPrefsTechDraw2.ui b/src/Mod/TechDraw/Gui/DlgPrefsTechDrawScale.ui similarity index 94% rename from src/Mod/TechDraw/Gui/DlgPrefsTechDraw2.ui rename to src/Mod/TechDraw/Gui/DlgPrefsTechDrawScale.ui index 5829ebe238..3fd1e3e971 100644 --- a/src/Mod/TechDraw/Gui/DlgPrefsTechDraw2.ui +++ b/src/Mod/TechDraw/Gui/DlgPrefsTechDrawScale.ui @@ -1,13 +1,13 @@ - TechDrawGui::DlgPrefsTechDraw2Imp - + TechDrawGui::DlgPrefsTechDrawScaleImp + 0 0 440 - 450 + 532 @@ -404,15 +404,48 @@ Each unit is approx. 0.1 mm wide - - - - Vertex Scale + + + + + 0 + 0 + + + + + 174 + 0 + + + + + 0 + 0 + + + + Tolerance font size adjustment. Multiplier of dimension font size. + + + + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + 0.500000000000000 + + + TolSizeAdjust + + + Mod/TechDraw/Dimensions - - + + 0 @@ -426,34 +459,26 @@ Each unit is approx. 0.1 mm wide - Scale of vertex dots. Multiplier of line width. - - - + Size of template field click handles Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - 5.000000000000000 + 3.000000000000000 - VertexScale + TemplateDotSize Mod/TechDraw/General - - - - - true - - + + - Center Mark Scale + Vertex Scale @@ -504,6 +529,52 @@ Each unit is approx. 0.1 mm wide + + + + + 0 + 0 + + + + + 174 + 0 + + + + Scale of vertex dots. Multiplier of line width. + + + + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + 5.000000000000000 + + + VertexScale + + + Mod/TechDraw/General + + + + + + + + true + + + + Center Mark Scale + + + @@ -528,46 +599,6 @@ Each unit is approx. 0.1 mm wide - - - - - 0 - 0 - - - - - 174 - 0 - - - - - 0 - 0 - - - - Tolerance font size adjustment. Multiplier of dimension font size. - - - - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - 0.500000000000000 - - - TolSizeAdjust - - - Mod/TechDraw/Dimensions - - - @@ -575,34 +606,29 @@ Each unit is approx. 0.1 mm wide - - - - - 0 - 0 - - - - - 174 - 0 - + + + + Welding Symbol Scale + + + + - Size of template field click handles + Multiplier for size of welding symbols Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - 3.000000000000000 + 1.250000000000000 - TemplateDotSize + SymbolFactor - Mod/TechDraw/General + Mod/TechDraw/Decorations @@ -646,6 +672,11 @@ Each unit is approx. 0.1 mm wide + + Gui::QuantitySpinBox + QWidget +
Gui/QuantitySpinBox.h
+
Gui::PrefComboBox QComboBox diff --git a/src/Mod/TechDraw/Gui/DlgPrefsTechDraw2Imp.cpp b/src/Mod/TechDraw/Gui/DlgPrefsTechDrawScaleImp.cpp similarity index 87% rename from src/Mod/TechDraw/Gui/DlgPrefsTechDraw2Imp.cpp rename to src/Mod/TechDraw/Gui/DlgPrefsTechDrawScaleImp.cpp index 2a24864609..233bf6c7dc 100644 --- a/src/Mod/TechDraw/Gui/DlgPrefsTechDraw2Imp.cpp +++ b/src/Mod/TechDraw/Gui/DlgPrefsTechDrawScaleImp.cpp @@ -25,12 +25,12 @@ #include "PreCompiled.h" -#include "DlgPrefsTechDraw2Imp.h" +#include "DlgPrefsTechDrawScaleImp.h" #include using namespace TechDrawGui; -DlgPrefsTechDraw2Imp::DlgPrefsTechDraw2Imp( QWidget* parent ) +DlgPrefsTechDrawScaleImp::DlgPrefsTechDrawScaleImp( QWidget* parent ) : PreferencePage( parent ) { this->setupUi(this); @@ -42,12 +42,12 @@ DlgPrefsTechDraw2Imp::DlgPrefsTechDraw2Imp( QWidget* parent ) this, SLOT(onScaleTypeChanged(int))); } -DlgPrefsTechDraw2Imp::~DlgPrefsTechDraw2Imp() +DlgPrefsTechDrawScaleImp::~DlgPrefsTechDrawScaleImp() { // no need to delete child widgets, Qt does it all for us } -void DlgPrefsTechDraw2Imp::onScaleTypeChanged(int index) +void DlgPrefsTechDrawScaleImp::onScaleTypeChanged(int index) { // disable custom scale if the scale type is not custom @@ -57,7 +57,7 @@ void DlgPrefsTechDraw2Imp::onScaleTypeChanged(int index) this->pdsbViewScale->setEnabled(false); } -void DlgPrefsTechDraw2Imp::saveSettings() +void DlgPrefsTechDrawScaleImp::saveSettings() { pdsbToleranceScale->onSave(); pdsbTemplateMark->onSave(); @@ -69,9 +69,10 @@ void DlgPrefsTechDraw2Imp::saveSettings() pdsbEdgeFuzz->onSave(); pdsbMarkFuzz->onSave(); pdsbTemplateMark->onSave(); + pdsbSymbolScale->onSave(); } -void DlgPrefsTechDraw2Imp::loadSettings() +void DlgPrefsTechDrawScaleImp::loadSettings() { double markDefault = 3.0; pdsbTemplateMark->setValue(markDefault); @@ -85,12 +86,13 @@ void DlgPrefsTechDraw2Imp::loadSettings() pdsbEdgeFuzz->onRestore(); pdsbMarkFuzz->onRestore(); pdsbTemplateMark->onRestore(); + pdsbSymbolScale->onRestore(); } /** * Sets the strings of the subwidgets using the current language. */ -void DlgPrefsTechDraw2Imp::changeEvent(QEvent *e) +void DlgPrefsTechDrawScaleImp::changeEvent(QEvent *e) { if (e->type() == QEvent::LanguageChange) { saveSettings(); @@ -102,4 +104,4 @@ void DlgPrefsTechDraw2Imp::changeEvent(QEvent *e) } } -#include +#include diff --git a/src/Mod/TechDraw/Gui/DlgPrefsTechDraw2Imp.h b/src/Mod/TechDraw/Gui/DlgPrefsTechDrawScaleImp.h similarity index 82% rename from src/Mod/TechDraw/Gui/DlgPrefsTechDraw2Imp.h rename to src/Mod/TechDraw/Gui/DlgPrefsTechDrawScaleImp.h index 04c8dec00b..d68e184267 100644 --- a/src/Mod/TechDraw/Gui/DlgPrefsTechDraw2Imp.h +++ b/src/Mod/TechDraw/Gui/DlgPrefsTechDrawScaleImp.h @@ -1,7 +1,6 @@ /************************************************************************** * Copyright (c) 2015 FreeCAD Developers * * Author: WandererFan * - * Based on src/Mod/FEM/Gui/DlgPrefsTechDraw2Imp.cpp * * * * This file is part of the FreeCAD CAx development system. * * * @@ -23,21 +22,21 @@ ***************************************************************************/ -#ifndef DRAWINGGUI_DLGPREFSTECHDRAWIMP2_H -#define DRAWINGGUI_DLGPREFSTECHDRAWIMP2_H +#ifndef DRAWINGGUI_DLGPREFSTECHDRAWIMPSCALE_H +#define DRAWINGGUI_DLGPREFSTECHDRAWIMPSCALE_H -#include +#include #include namespace TechDrawGui { -class DlgPrefsTechDraw2Imp : public Gui::Dialog::PreferencePage, public Ui_DlgPrefsTechDraw2Imp +class DlgPrefsTechDrawScaleImp : public Gui::Dialog::PreferencePage, public Ui_DlgPrefsTechDrawScaleImp { Q_OBJECT public: - DlgPrefsTechDraw2Imp( QWidget* parent = 0 ); - ~DlgPrefsTechDraw2Imp(); + DlgPrefsTechDrawScaleImp( QWidget* parent = 0 ); + ~DlgPrefsTechDrawScaleImp(); protected Q_SLOTS: void onScaleTypeChanged(int index); @@ -50,4 +49,4 @@ protected: } // namespace TechDrawGui -#endif // DRAWINGGUI_DLGPREFSTECHDRAWIMP2_H +#endif // DRAWINGGUI_DLGPREFSTECHDRAWIMPSCALE_H diff --git a/src/Mod/TechDraw/Gui/QGIGhostHighlight.h b/src/Mod/TechDraw/Gui/QGIGhostHighlight.h index 0f86095a79..de58d1c9aa 100644 --- a/src/Mod/TechDraw/Gui/QGIGhostHighlight.h +++ b/src/Mod/TechDraw/Gui/QGIGhostHighlight.h @@ -42,8 +42,8 @@ public: explicit QGIGhostHighlight(); ~QGIGhostHighlight(); - enum {Type = QGraphicsItem::UserType + 177}; - int type() const { return Type;} + enum {Type = QGraphicsItem::UserType + 177}; + int type() const override { return Type;} void setInteractive(bool state); void setRadius(double r); diff --git a/src/Mod/TechDraw/Gui/QGIHighlight.h b/src/Mod/TechDraw/Gui/QGIHighlight.h index 4b3a9f02e5..0e36945edf 100644 --- a/src/Mod/TechDraw/Gui/QGIHighlight.h +++ b/src/Mod/TechDraw/Gui/QGIHighlight.h @@ -50,8 +50,8 @@ public: explicit QGIHighlight(); ~QGIHighlight(); - enum {Type = QGraphicsItem::UserType + 176}; - int type() const { return Type;} + enum {Type = QGraphicsItem::UserType + 176}; + int type() const override { return Type;} virtual void paint(QPainter * painter, const QStyleOptionGraphicsItem * option, @@ -60,7 +60,7 @@ public: void setBounds(double x1,double y1,double x2,double y2); void setReference(char* sym); void setFont(QFont f, double fsize); - virtual void draw(); + virtual void draw() override; void setInteractive(bool state); protected: diff --git a/src/Mod/TechDraw/Gui/QGIViewAnnotation.cpp b/src/Mod/TechDraw/Gui/QGIViewAnnotation.cpp index 02c25ce9a4..64ff8f49a7 100644 --- a/src/Mod/TechDraw/Gui/QGIViewAnnotation.cpp +++ b/src/Mod/TechDraw/Gui/QGIViewAnnotation.cpp @@ -158,10 +158,19 @@ void QGIViewAnnotation::drawAnnotation() if (it != annoText.begin()) { ss << "
"; } - std::string u8String = Base::Tools::escapedUnicodeToUtf8(*it); -// what madness turns \' into \\\\\'? - std::string apos = std::regex_replace((u8String), std::regex("\\\\\'"), "'"); - ss << apos; + //TODO: there is still a bug here. entering "'" works, save and restore works, but edit after + // save and restore brings "\'" back into text. manually deleting the "\" fixes it until the next + // save/restore/edit cycle. + // a guess is that the editor for propertyStringList is too enthusiastic about substituting. + // the substituting might be necessary for using the strings in Python. + // ' doesn't seem to help in this case. + + std::string u8String = Base::Tools::escapedUnicodeToUtf8(*it); //from \x??\x?? to real utf8 + std::string apos = std::regex_replace((u8String), std::regex("\\\\"), ""); //remove doubles. + apos = std::regex_replace((apos), std::regex("\\'"), "'"); //replace escaped apos + //"less than" symbol chops off line. need to use html sub. + std::string lt = std::regex_replace((apos), std::regex("<"), "<"); + ss << lt; } ss << "

\n\n "; diff --git a/src/Mod/TechDraw/Gui/TaskCenterLine.cpp b/src/Mod/TechDraw/Gui/TaskCenterLine.cpp index 79d110eca2..f6701cfa86 100644 --- a/src/Mod/TechDraw/Gui/TaskCenterLine.cpp +++ b/src/Mod/TechDraw/Gui/TaskCenterLine.cpp @@ -74,15 +74,16 @@ using namespace TechDrawGui; //ctor for edit TaskCenterLine::TaskCenterLine(TechDraw::DrawViewPart* partFeat, TechDraw::DrawPage* page, - std::string edgeName) : + std::string edgeName, + bool editMode) : ui(new Ui_TaskCenterLine), m_partFeat(partFeat), m_basePage(page), m_createMode(false), m_edgeName(edgeName), m_type(0), //0 - Face, 1 - 2 Lines, 2 - 2 points - m_mode(0) //0 - vertical, 1 - horizontal, 2 - aligned - + m_mode(0), //0 - vertical, 1 - horizontal, 2 - aligned + m_editMode(editMode) { // Base::Console().Message("TCL::TCL() - edit mode\n"); ui->setupUi(this); @@ -104,14 +105,16 @@ TaskCenterLine::TaskCenterLine(TechDraw::DrawViewPart* partFeat, //ctor for creation TaskCenterLine::TaskCenterLine(TechDraw::DrawViewPart* partFeat, TechDraw::DrawPage* page, - std::vector subNames) : + std::vector subNames, + bool editMode) : ui(new Ui_TaskCenterLine), m_partFeat(partFeat), m_basePage(page), m_createMode(true), m_subNames(subNames), m_type(0), //0 - Face, 1 - 2 Lines, 2 - 2 points - m_mode(0) //0 - vertical, 1 - horizontal, 2 - aligned + m_mode(0), //0 - vertical, 1 - horizontal, 2 - aligned + m_editMode(editMode) { // Base::Console().Message("TCL::TCL() - create mode\n"); if ( (m_basePage == nullptr) || @@ -501,10 +504,11 @@ bool TaskCenterLine::reject() ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// TaskDlgCenterLine::TaskDlgCenterLine(TechDraw::DrawViewPart* partFeat, TechDraw::DrawPage* page, - std::vector subNames) + std::vector subNames, + bool editMode) : TaskDialog() { - widget = new TaskCenterLine(partFeat,page,subNames); + widget = new TaskCenterLine(partFeat,page,subNames, editMode); taskbox = new Gui::TaskView::TaskBox(Gui::BitmapFactory().pixmap("actions/techdraw-facecenterline"), widget->windowTitle(), true, 0); taskbox->groupLayout()->addWidget(widget); @@ -513,10 +517,11 @@ TaskDlgCenterLine::TaskDlgCenterLine(TechDraw::DrawViewPart* partFeat, TaskDlgCenterLine::TaskDlgCenterLine(TechDraw::DrawViewPart* partFeat, TechDraw::DrawPage* page, - std::string edgeName) + std::string edgeName, + bool editMode) : TaskDialog() { - widget = new TaskCenterLine(partFeat,page, edgeName); + widget = new TaskCenterLine(partFeat,page, edgeName, editMode); taskbox = new Gui::TaskView::TaskBox(Gui::BitmapFactory().pixmap("actions/techdraw-facecenterline"), widget->windowTitle(), true, 0); taskbox->groupLayout()->addWidget(widget); diff --git a/src/Mod/TechDraw/Gui/TaskCenterLine.h b/src/Mod/TechDraw/Gui/TaskCenterLine.h index e63973e944..43f6fdb0dc 100644 --- a/src/Mod/TechDraw/Gui/TaskCenterLine.h +++ b/src/Mod/TechDraw/Gui/TaskCenterLine.h @@ -75,10 +75,12 @@ class TaskCenterLine : public QWidget public: TaskCenterLine(TechDraw::DrawViewPart* baseFeat, TechDraw::DrawPage* page, - std::vector subNames); + std::vector subNames, + bool editMode); TaskCenterLine(TechDraw::DrawViewPart* baseFeat, TechDraw::DrawPage* page, - std::string edgeName); + std::string edgeName, + bool editMode); ~TaskCenterLine(); public Q_SLOTS: @@ -145,6 +147,7 @@ private: int m_clIdx; int m_type; int m_mode; + bool m_editMode; }; class TaskDlgCenterLine : public Gui::TaskView::TaskDialog @@ -154,10 +157,12 @@ class TaskDlgCenterLine : public Gui::TaskView::TaskDialog public: TaskDlgCenterLine(TechDraw::DrawViewPart* baseFeat, TechDraw::DrawPage* page, - std::vector subNames); + std::vector subNames, + bool editMode); TaskDlgCenterLine(TechDraw::DrawViewPart* baseFeat, TechDraw::DrawPage* page, - std::string edgeName); + std::string edgeName, + bool editMode); ~TaskDlgCenterLine(); public: diff --git a/src/Mod/TechDraw/Gui/TaskDetail.cpp b/src/Mod/TechDraw/Gui/TaskDetail.cpp index 270b8fb612..f7b1a6214f 100644 --- a/src/Mod/TechDraw/Gui/TaskDetail.cpp +++ b/src/Mod/TechDraw/Gui/TaskDetail.cpp @@ -46,6 +46,8 @@ #include #include #include +#include +#include #include #include @@ -97,7 +99,7 @@ TaskDetail::TaskDetail(TechDraw::DrawViewPart* baseFeat): m_baseName = m_baseFeat->getNameInDocument(); m_doc = m_baseFeat->getDocument(); - m_pageName = m_basePage->getNameInDocument(); + m_pageName = m_basePage->getNameInDocument(); ui->setupUi(this); @@ -114,12 +116,17 @@ TaskDetail::TaskDetail(TechDraw::DrawViewPart* baseFeat): connect(ui->pbDragger, SIGNAL(clicked(bool)), this, SLOT(onDraggerClicked(bool))); + + //use editingFinished signal instead of valueChanged to prevent keyboard lock out + //valueChanged fires every keystroke causing a recompute. connect(ui->qsbX, SIGNAL(editingFinished()), this, SLOT(onXEdit())); connect(ui->qsbY, SIGNAL(editingFinished()), this, SLOT(onYEdit())); connect(ui->qsbRadius, SIGNAL(editingFinished()), this, SLOT(onRadiusEdit())); + connect(ui->aeReference, SIGNAL(editingFinished()), + this, SLOT(onReferenceEdit())); m_ghost = new QGIGhostHighlight(); m_scene->addItem(m_ghost); @@ -182,6 +189,9 @@ TaskDetail::TaskDetail(TechDraw::DrawViewDetail* detailFeat): connect(ui->pbDragger, SIGNAL(clicked(bool)), this, SLOT(onDraggerClicked(bool))); + + //use editingFinished signal instead of valueChanged to prevent keyboard lock out + //valueChanged fires every keystroke causing a recompute. connect(ui->qsbX, SIGNAL(editingFinished()), this, SLOT(onXEdit())); connect(ui->qsbY, SIGNAL(editingFinished()), @@ -189,7 +199,7 @@ TaskDetail::TaskDetail(TechDraw::DrawViewDetail* detailFeat): connect(ui->qsbRadius, SIGNAL(editingFinished()), this, SLOT(onRadiusEdit())); connect(ui->aeReference, SIGNAL(editingFinished()), - this, SLOT(onReferenceEdit())); + this, SLOT(onReferenceEdit())); m_ghost = new QGIGhostHighlight(); m_scene->addItem(m_ghost); @@ -200,6 +210,7 @@ TaskDetail::TaskDetail(TechDraw::DrawViewDetail* detailFeat): TaskDetail::~TaskDetail() { + m_ghost->deleteLater(); //this might not exist if scene is destroyed before TaskDetail is deleted? delete ui; } @@ -324,8 +335,10 @@ void TaskDetail::editByHighlight() return; } + double scale = getBaseFeat()->getScale(); m_scene->clearSelection(); m_ghost->setSelected(true); + m_ghost->setRadius(ui->qsbRadius->rawValue() * scale); m_ghost->setPos(getAnchorScene()); m_ghost->draw(); m_ghost->show(); @@ -339,12 +352,25 @@ void TaskDetail::onHighlightMoved(QPointF dragEnd) ui->pbDragger->setEnabled(true); double scale = getBaseFeat()->getScale(); - double x = Rez::guiX(getBaseFeat()->X.getValue()) * scale; - double y = Rez::guiX(getBaseFeat()->Y.getValue()) * scale; - QPointF basePosScene(x, -y); //base position in scene coords + double x = Rez::guiX(getBaseFeat()->X.getValue()); + double y = Rez::guiX(getBaseFeat()->Y.getValue()); + DrawViewPart* dvp = getBaseFeat(); + DrawProjGroupItem* dpgi = dynamic_cast(dvp); + if (dpgi != nullptr) { + DrawProjGroup* dpg = dpgi->getPGroup(); + if (dpg == nullptr) { + Base::Console().Message("TD::getAnchorScene - projection group is confused\n"); + //TODO::throw something. + return; + } + x += Rez::guiX(dpg->X.getValue()); + y += Rez::guiX(dpg->Y.getValue()); + } + + QPointF basePosScene(x, -y); //base position in scene coords QPointF anchorDisplace = dragEnd - basePosScene; - QPointF newAnchorPos = Rez::appX(anchorDisplace) / scale; + QPointF newAnchorPos = Rez::appX(anchorDisplace / scale); updateUi(newAnchorPos); updateDetail(); @@ -428,17 +454,39 @@ void TaskDetail::updateDetail() //get the current Anchor highlight position in scene coords QPointF TaskDetail::getAnchorScene() { - TechDraw::DrawViewPart* dvp = getBaseFeat(); - TechDraw::DrawViewDetail* dvd = getDetailFeat(); - + DrawViewPart* dvp = getBaseFeat(); + DrawProjGroupItem* dpgi = dynamic_cast(dvp); + DrawViewDetail* dvd = getDetailFeat(); Base::Vector3d anchorPos = dvd->AnchorPoint.getValue(); - double x = dvp->X.getValue(); - double y = dvp->Y.getValue(); - Base::Vector3d basePos(x, y, 0.0); - Base::Vector3d netPos = basePos + anchorPos; - netPos = Rez::guiX(netPos * dvp->getScale()); + anchorPos.y = -anchorPos.y; + Base::Vector3d basePos; + double scale = 1; - QPointF qAnchor(netPos.x, - netPos.y); + if (dpgi == nullptr) { //base is normal view + double x = dvp->X.getValue(); + double y = dvp->Y.getValue(); + basePos = Base::Vector3d (x, -y, 0.0); + scale = dvp->getScale(); + } else { //part of projection group + + DrawProjGroup* dpg = dpgi->getPGroup(); + if (dpg == nullptr) { + Base::Console().Message("TD::getAnchorScene - projection group is confused\n"); + //TODO::throw something. + return QPointF(0.0, 0.0); + } + double x = dpg->X.getValue(); + x += dpgi->X.getValue(); + double y = dpg->Y.getValue(); + y += dpgi->Y.getValue(); + basePos = Base::Vector3d(x, -y, 0.0); + scale = dpgi->getScale(); + } + + Base::Vector3d xyScene = Rez::guiX(basePos); + Base::Vector3d anchorOffsetScene = Rez::guiX(anchorPos) * scale; + Base::Vector3d netPos = xyScene + anchorOffsetScene; + QPointF qAnchor(netPos.x, netPos.y); return qAnchor; } @@ -494,6 +542,7 @@ bool TaskDetail::accept() Gui::Document* doc = Gui::Application::Instance->getDocument(m_basePage->getDocument()); if (!doc) return false; + m_ghost->hide(); getDetailFeat()->requestPaint(); getBaseFeat()->requestPaint(); Gui::Command::doCommand(Gui::Command::Gui,"Gui.ActiveDocument.resetEdit()"); @@ -507,6 +556,7 @@ bool TaskDetail::reject() Gui::Document* doc = Gui::Application::Instance->getDocument(m_basePage->getDocument()); if (!doc) return false; + m_ghost->hide(); if (m_mode == CREATEMODE) { if (m_created) { Gui::Command::doCommand(Gui::Command::Gui,"App.activeDocument().removeObject('%s')", diff --git a/src/Mod/TechDraw/Gui/TaskDetail.ui b/src/Mod/TechDraw/Gui/TaskDetail.ui index bf7d9798f3..0aa0a2fe21 100644 --- a/src/Mod/TechDraw/Gui/TaskDetail.ui +++ b/src/Mod/TechDraw/Gui/TaskDetail.ui @@ -6,12 +6,12 @@ 0 0 - 381 - 405 + 304 + 244
- + 0 0 @@ -29,252 +29,205 @@ :/icons/actions/techdraw-DetailView.svg:/icons/actions/techdraw-DetailView.svg - + - - - - 0 - 0 - + + + + + false + + + false + + + Qt::NoFocus + + + false + + + + + + + Base View + + + + + + + Detail View + + + + + + + false + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Click to drag detail highlight to new position + + + Drag Highlight + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Qt::Horizontal - - - 300 - 300 - - - - - 300 - 300 - - - - QFrame::Box - - - QFrame::Raised - - - - - - - - - - false - - - false - - - Qt::NoFocus - - - false - - - - - - - Base View - - - - - - - Detail View - - - - - - - false - - - - - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - Click to drag detail highlight to new position - - - Drag Highlight - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - - Qt::Horizontal - - - - - - - - - size of detail view - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - 10.000000000000000 - - - - - - - X - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - Y - - - - - - - - - - - - - - x position of detail highlight within view - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - 0.000000000000000 - - - - - - - y position of detail highlight within view - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - - - - Radius - - - - - - - Reference - - - - - - - Detail identifier - - - 1 - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - + + + + + + size of detail view + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + 10.000000000000000 + + + + + + + X + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Y + + + + + + + + + + + + + + x position of detail highlight within view + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + 0.000000000000000 + + + + + + + y position of detail highlight within view + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + + Radius + + + + + + + Reference + + + + + + + Detail identifier + + + 1 + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + +
diff --git a/src/Mod/TechDraw/Gui/TaskSectionView.cpp b/src/Mod/TechDraw/Gui/TaskSectionView.cpp index 36e5dc1e93..6b571780d5 100644 --- a/src/Mod/TechDraw/Gui/TaskSectionView.cpp +++ b/src/Mod/TechDraw/Gui/TaskSectionView.cpp @@ -176,12 +176,13 @@ void TaskSectionView::setUiPrimary() this->setToolTip(QObject::tr("Select at first an orientation")); enableAll(false); - // now connect and not earlier to avoid premature apply() calls - connect(ui->leSymbol, SIGNAL(textChanged(QString)), this, SLOT(onIdentifierChanged())); - connect(ui->sbScale, SIGNAL(valueChanged(double)), this, SLOT(onScaleChanged())); - connect(ui->sbOrgX, SIGNAL(valueChanged(double)), this, SLOT(onXChanged())); - connect(ui->sbOrgY, SIGNAL(valueChanged(double)), this, SLOT(onYChanged())); - connect(ui->sbOrgZ, SIGNAL(valueChanged(double)), this, SLOT(onZChanged())); + //use editingFinished signal instead of valueChanged to prevent keyboard lock out + //valueChanged fires every keystroke causing a recompute. + connect(ui->leSymbol, SIGNAL(editingFinished()), this, SLOT(onIdentifierChanged())); + connect(ui->sbScale, SIGNAL(editingFinished()), this, SLOT(onScaleChanged())); + connect(ui->sbOrgX, SIGNAL(editingFinished()), this, SLOT(onXChanged())); + connect(ui->sbOrgY, SIGNAL(editingFinished()), this, SLOT(onYChanged())); + connect(ui->sbOrgZ, SIGNAL(editingFinished()), this, SLOT(onZChanged())); } void TaskSectionView::setUiEdit() @@ -206,12 +207,13 @@ void TaskSectionView::setUiEdit() ui->sbOrgZ->setUnit(Base::Unit::Length); ui->sbOrgZ->setValue(origin.z); - // connect affter initializing the object values - connect(ui->leSymbol, SIGNAL(textChanged(QString)), this, SLOT(onIdentifierChanged())); - connect(ui->sbScale, SIGNAL(valueChanged(double)), this, SLOT(onScaleChanged())); - connect(ui->sbOrgX, SIGNAL(valueChanged(double)), this, SLOT(onXChanged())); - connect(ui->sbOrgY, SIGNAL(valueChanged(double)), this, SLOT(onYChanged())); - connect(ui->sbOrgZ, SIGNAL(valueChanged(double)), this, SLOT(onZChanged())); + //use editingFinished signal instead of valueChanged to prevent keyboard lock out + //valueChanged fires every keystroke causing a recompute. + connect(ui->leSymbol, SIGNAL(editingFinished()), this, SLOT(onIdentifierChanged())); + connect(ui->sbScale, SIGNAL(editingFinished()), this, SLOT(onScaleChanged())); + connect(ui->sbOrgX, SIGNAL(editingFinished()), this, SLOT(onXChanged())); + connect(ui->sbOrgY, SIGNAL(editingFinished()), this, SLOT(onYChanged())); + connect(ui->sbOrgZ, SIGNAL(editingFinished()), this, SLOT(onZChanged())); } //save the start conditions diff --git a/src/Tools/SubWCRev.py b/src/Tools/SubWCRev.py index 3c6a0fd8a0..9795450226 100644 --- a/src/Tools/SubWCRev.py +++ b/src/Tools/SubWCRev.py @@ -127,6 +127,87 @@ class BazaarControl(VersionControl): def printInfo(self): print("bazaar") +class DebianGitHub(VersionControl): + #https://gist.github.com/0penBrain/7be59a48aba778c955d992aa69e524c5 + #https://gist.github.com/yershalom/a7c08f9441d1aadb13777bce4c7cdc3b + #https://github.community/t5/GitHub-API-Development-and/How-to-get-all-branches-which-contain-a-commit-from-SHA-using/td-p/25006 + def extractInfo(self, srcdir, bindir): + try: + f = open(srcdir+"/debian/git-build-recipe.manifest") + except: + return False + + # Read the first two lines + recipe = f.readline() + commit = f.readline() + f.close() + + base_url = "https://api.github.com" + owner = "FreeCAD" + repo = "FreeCAD" + sha = commit[commit.rfind(':') + 1 : -1] + self.hash = sha + + try: + import requests + request_url = "{}/repos/{}/{}/commits?per_page=1&sha={}".format(base_url, owner, repo, sha) + commit_req = requests.get(request_url) + if not commit_req.ok: + return False + + commit_date = commit_req.headers.get('last-modified') + + except: + # if connection fails then use the date of the file git-build-recipe.manifest + commit_date = recipe[recipe.rfind('~') + 1 : -1] + + + try: + # Try to convert into the same format as GitControl + t = time.strptime(commit_date, "%a, %d %b %Y %H:%M:%S GMT") + commit_date = ("%d/%02d/%02d %02d:%02d:%02d") % (t.tm_year, t.tm_mon, t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec) + except: + t = time.strptime(commit_date, "%Y%m%d%H%M") + commit_date = ("%d/%02d/%02d %02d:%02d:%02d") % (t.tm_year, t.tm_mon, t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec) + + self.date = commit_date + self.branch = "unknown" + + try: + # Try to determine the branch of the sha + # There is no function of the rest API of GH but with the url below we get HTML code + branch_url = "https://github.com/{}/{}/branch_commits/{}".format(owner, repo, sha) + branch_req = requests.get(branch_url) + if branch_req.ok: + html = branch_req.text + pattern = "
  • ") + 1 + end = link.find("<", start) + self.branch = link[start:end] + + link = commit_req.headers.get("link") + beg = link.rfind("&page=") + 6 + end = link.rfind(">") + self.rev = link[beg:end] + " (GitHub)" + except: + pass + + self.url = "git://github.com/{}/{}.git {}".format(owner, repo, self.branch) + return True + + def writeVersion(self, lines): + content = VersionControl.writeVersion(self, lines) + content.append('// Git relevant stuff\n') + content.append('#define FCRepositoryHash "%s"\n' % (self.hash)) + content.append('#define FCRepositoryBranch "%s"\n' % (self.branch)) + return content + + def printInfo(self): + print("Debian/GitHub") + class GitControl(VersionControl): #http://www.hermanradtke.com/blog/canonical-version-numbers-with-git/ #http://blog.marcingil.com/2011/11/creating-build-numbers-using-git-commits/ @@ -374,7 +455,7 @@ def main(): if o in ("-b", "--bindir"): bindir = a - vcs=[GitControl(), BazaarControl(), Subversion(), MercurialControl(), DebianChangelog(), UnknownControl()] + vcs=[GitControl(), DebianGitHub(), BazaarControl(), Subversion(), MercurialControl(), DebianChangelog(), UnknownControl()] for i in vcs: if i.extractInfo(srcdir, bindir): # Open the template file and the version file