diff --git a/src/Mod/Draft/draftguitools/gui_patharray.py b/src/Mod/Draft/draftguitools/gui_patharray.py index ac6b4d50bd..307218c86d 100644 --- a/src/Mod/Draft/draftguitools/gui_patharray.py +++ b/src/Mod/Draft/draftguitools/gui_patharray.py @@ -1,7 +1,10 @@ # *************************************************************************** -# * (c) 2009, 2010 Yorik van Havre * -# * (c) 2009, 2010 Ken Cline * -# * (c) 2020 Eliud Cabrera Castillo * +# * Copyright (c) 2009, 2010 Yorik van Havre * +# * Copyright (c) 2009, 2010 Ken Cline * +# * Copyright (c) 2013 Wandererfan * +# * Copyright (c) 2019 Zheng, Lei (realthunder)* +# * Copyright (c) 2020 Carlo Pavan * +# * Copyright (c) 2020 Eliud Cabrera Castillo * # * * # * This file is part of the FreeCAD CAx development system. * # * * @@ -11,13 +14,13 @@ # * 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, * +# * 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 FreeCAD; if not, write to the Free Software * +# * License along with this program; if not, write to the Free Software * # * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * # * USA * # * * @@ -35,12 +38,12 @@ from PySide.QtCore import QT_TRANSLATE_NOOP import FreeCAD as App import FreeCADGui as Gui -import Draft import Draft_rc +import DraftVecUtils import draftguitools.gui_base_original as gui_base_original -import draftguitools.gui_tool_utils as gui_tool_utils -from draftutils.messages import _msg -from draftutils.translate import translate, _tr + +from draftutils.messages import _err +from draftutils.translate import _tr # The module is used to prevent complaints from code checkers (flake8) True if Draft_rc.__name__ else False @@ -59,13 +62,16 @@ class PathArray(gui_base_original.Modifier): def __init__(self, use_link=False): super(PathArray, self).__init__() self.use_link = use_link + self.call = None def GetResources(self): """Set icon, menu and tooltip.""" _menu = "Path array" - _tip = ("Creates copies of a selected object along a selected path.\n" + _tip = ("Creates copies of the selected object " + "along a selected path.\n" "First select the object, and then select the path.\n" - "The path can be a polyline, B-spline or Bezier curve.") + "The path can be a polyline, B-spline, Bezier curve, " + "or even edges from other objects.") return {'Pixmap': 'Draft_PathArray', 'MenuText': QT_TRANSLATE_NOOP("Draft_PathArray", _menu), @@ -74,15 +80,24 @@ class PathArray(gui_base_original.Modifier): def Activated(self, name=_tr("Path array")): """Execute when the command is called.""" super(PathArray, self).Activated(name=name) - if not Gui.Selection.getSelectionEx(): - if self.ui: - self.ui.selectUi() - _msg(translate("draft", "Please select base and path objects")) - self.call = \ - self.view.addEventCallback("SoEvent", - gui_tool_utils.selectObject) - else: - self.proceed() + self.name = name + # This was deactivated becuase it doesn't work correctly; + # the selection needs to be made on two objects, but currently + # it only selects one. + + # if not Gui.Selection.getSelectionEx(): + # if self.ui: + # self.ui.selectUi() + # _msg(translate("draft", + # "Please select exactly two objects, " + # "the base object and the path object, " + # "before calling this command.")) + # self.call = \ + # self.view.addEventCallback("SoEvent", + # gui_tool_utils.selectObject) + # else: + # self.proceed() + self.proceed() def proceed(self): """Proceed with the command if one object was selected.""" @@ -90,21 +105,54 @@ class PathArray(gui_base_original.Modifier): self.view.removeEventCallback("SoEvent", self.call) sel = Gui.Selection.getSelectionEx() - if sel: - base = sel[0].Object - path = sel[1].Object + if len(sel) != 2: + _err(_tr("Please select exactly two objects, " + "the base object and the path object, " + "before calling this command.")) + else: + base_object = sel[0].Object + path_object = sel[1].Object - defCount = 4 - defXlate = App.Vector(0, 0, 0) - defAlign = False - pathsubs = list(sel[1].SubElementNames) + count = 4 + xlate = App.Vector(0, 0, 0) + subelements = list(sel[1].SubElementNames) + align = False + align_mode = "Original" + tan_vector = App.Vector(1, 0, 0) + force_vertical = False + vertical_vector = App.Vector(0, 0, 1) + use_link = self.use_link - App.ActiveDocument.openTransaction("PathArray") - Draft.makePathArray(base, path, - defCount, defXlate, defAlign, pathsubs, - use_link=self.use_link) - App.ActiveDocument.commitTransaction() - App.ActiveDocument.recompute() + _edge_list_str = list() + _edge_list_str = ["'" + edge + "'" for edge in subelements] + _sub_str = ", ".join(_edge_list_str) + subelements_list_str = "[" + _sub_str + "]" + + vertical_vector_str = DraftVecUtils.toString(vertical_vector) + + Gui.addModule("Draft") + _cmd = "Draft.make_path_array" + _cmd += "(" + _cmd += "App.ActiveDocument." + base_object.Name + ", " + _cmd += "App.ActiveDocument." + path_object.Name + ", " + _cmd += "count=" + str(count) + ", " + _cmd += "xlate=" + DraftVecUtils.toString(xlate) + ", " + _cmd += "subelements=" + subelements_list_str + ", " + _cmd += "align=" + str(align) + ", " + _cmd += "align_mode=" + "'" + align_mode + "', " + _cmd += "tan_vector=" + DraftVecUtils.toString(tan_vector) + ", " + _cmd += "force_vertical=" + str(force_vertical) + ", " + _cmd += "vertical_vector=" + vertical_vector_str + ", " + _cmd += "use_link=" + str(use_link) + _cmd += ")" + + _cmd_list = ["_obj_ = " + _cmd, + "Draft.autogroup(_obj_)", + "App.ActiveDocument.recompute()"] + self.commit(_tr(self.name), _cmd_list) + + # Commit the transaction and execute the commands + # through the parent class self.finish() @@ -131,7 +179,7 @@ class PathLinkArray(PathArray): def Activated(self): """Execute when the command is called.""" - super(PathLinkArray, self).Activated(name=_tr("Link path array")) + super(PathLinkArray, self).Activated(name=_tr("Path link array")) Gui.addCommand('Draft_PathLinkArray', PathLinkArray()) diff --git a/src/Mod/Draft/draftmake/make_patharray.py b/src/Mod/Draft/draftmake/make_patharray.py index 9ea3c7751c..fcfa0bd491 100644 --- a/src/Mod/Draft/draftmake/make_patharray.py +++ b/src/Mod/Draft/draftmake/make_patharray.py @@ -1,7 +1,12 @@ # *************************************************************************** # * Copyright (c) 2009, 2010 Yorik van Havre * # * Copyright (c) 2009, 2010 Ken Cline * -# * Copyright (c) 2020 FreeCAD Developers * +# * Copyright (c) 2013 Wandererfan * +# * Copyright (c) 2019 Zheng, Lei (realthunder)* +# * Copyright (c) 2020 Carlo Pavan * +# * Copyright (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) * @@ -20,98 +25,293 @@ # * USA * # * * # *************************************************************************** -"""This module provides the code for Draft make_path_array function. +"""Provides functions for creating path arrays. + +The copies will be placed along a path like a polyline, spline, or bezier +curve. """ ## @package make_patharray # \ingroup DRAFT -# \brief This module provides the code for Draft make_path_array function. +# \brief Provides functions for creating path arrays. import FreeCAD as App - import draftutils.utils as utils import draftutils.gui_utils as gui_utils -from draftutils.translate import _tr, translate +from draftutils.messages import _msg, _err +from draftutils.translate import _tr from draftobjects.patharray import PathArray -from draftviewproviders.view_draftlink import ViewProviderDraftLink if App.GuiUp: from draftviewproviders.view_array import ViewProviderDraftArray + from draftviewproviders.view_draftlink import ViewProviderDraftLink -def make_path_array(baseobject,pathobject,count,xlate=None,align=False,pathobjsubs=[],use_link=False): - """make_path_array(docobj, path, count, xlate, align, pathobjsubs, use_link) - - Make a Draft PathArray object. - - Distribute count copies of a document baseobject along a pathobject - or subobjects of a pathobject. +def make_path_array(base_object, path_object, count=4, + xlate=App.Vector(0, 0, 0), subelements=None, + align=False, align_mode="Original", + tan_vector=App.Vector(1, 0, 0), + force_vertical=False, + vertical_vector=App.Vector(0, 0, 1), + use_link=True): + """Make a Draft PathArray object. + + Distribute copies of a `base_object` along `path_object` + or `subelements` from `path_object`. - Parameters ---------- - docobj : - Object to array + base_object: Part::Feature or str + Any of object that has a `Part::TopoShape` that can be duplicated. + This means most 2D and 3D objects produced with any workbench. + If it is a string, it must be the `Label` of that object. + Since a label is not guaranteed to be unique in a document, + it will use the first object found with this label. - path : - Path object + path_object: Part::Feature or str + Path object like a polyline, B-Spline, or bezier curve that should + contain edges. + Just like `base_object` it can also be `Label`. - pathobjsubs : - TODO: Complete documentation + count: int, float, optional + It defaults to 4. + Number of copies to create along the `path_object`. + It must be at least 2. + If a `float` is provided, it will be truncated by `int(count)`. - align : - Optionally aligns baseobject to tangent/normal/binormal of path. TODO: verify + xlate: Base.Vector3, optional + It defaults to `App.Vector(0, 0, 0)`. + It translates each copy by the value of `xlate`. + This is useful to adjust for the difference between shape centre + and shape reference point. - count : - TODO: Complete documentation + subelements: list or tuple of str, optional + It defaults to `None`. + It should be a list of names of edges that must exist in `path_object`. + Then the path array will be created along these edges only, + and not the entire `path_object`. + :: + subelements = ['Edge1', 'Edge2'] - xlate : Base.Vector - Optionally translates each copy by FreeCAD.Vector xlate direction - and distance to adjust for difference in shape centre vs shape reference point. - - use_link : - TODO: Complete documentation + The edges must be contiguous, meaning that it is not allowed to + input `'Edge1'` and `'Edge3'` if they do not touch each other. + + A single string value is also allowed. + :: + subelements = 'Edge1' + + align: bool, optional + It defaults to `False`. + If it is `True` it will align `base_object` to tangent, normal, + or binormal to the `path_object`, depending on the value + of `tan_vector`. + + align_mode: str, optional + It defaults to `'Original'` which is the traditional alignment. + It can also be `'Frenet'` or `'Tangent'`. + + - Original. It does not calculate curve normal. + `X` is curve tangent, `Y` is normal parameter, Z is the cross + product `X` x `Y`. + - Frenet. It defines a local coordinate system along the path. + `X` is tanget to curve, `Y` is curve normal, `Z` is curve binormal. + If normal cannot be computed, for example, in a straight path, + a default is used. + - Tangent. It is similar to `'Original'` but includes a pre-rotation + to align the base object's `X` to the value of `tan_vector`, + then `X` follows curve tangent. + + tan_vector: Base::Vector3, optional + It defaults to `App.Vector(1, 0, 0)` or the +X axis. + It aligns the tangent of the path to this local unit vector + of the object. + + force_vertical: Base::Vector3, optional + It defaults to `False`. + If it is `True`, the value of `vertical_vector` + will be used when `align_mode` is `'Original'` or `'Tangent'`. + + vertical_vector: Base::Vector3, optional + It defaults to `App.Vector(0, 0, 1)` or the +Z axis. + It will force this vector to be the vertical direction + when `force_vertical` is `True`. + + use_link: bool, optional + It defaults to `True`, in which case the copies are `App::Link` + elements. Otherwise, the copies are shape copies which makes + the resulting array heavier. + + Returns + ------- + Part::FeaturePython + The scripted object of type `'PathArray'`. + Its `Shape` is a compound of the copies of the original object. + + None + If there is a problem it will return `None`. """ + _name = "make_path_array" + utils.print_header(_name, "Path array") - if not App.ActiveDocument: - App.Console.PrintError("No active document. Aborting\n") - return + found, doc = utils.find_doc(App.activeDocument()) + if not found: + _err(_tr("No active document. Aborting.")) + return None + + if isinstance(base_object, str): + base_object_str = base_object + + found, base_object = utils.find_object(base_object, doc) + if not found: + _msg("base_object: {}".format(base_object_str)) + _err(_tr("Wrong input: object not in document.")) + return None + + _msg("base_object: {}".format(base_object.Label)) + + if isinstance(path_object, str): + path_object_str = path_object + + found, path_object = utils.find_object(path_object, doc) + if not found: + _msg("path_object: {}".format(path_object_str)) + _err(_tr("Wrong input: object not in document.")) + return None + + _msg("path_object: {}".format(path_object.Label)) + + _msg("count: {}".format(count)) + try: + utils.type_check([(count, (int, float))], + name=_name) + except TypeError: + _err(_tr("Wrong input: must be a number.")) + return None + count = int(count) + + _msg("xlate: {}".format(xlate)) + try: + utils.type_check([(xlate, App.Vector)], + name=_name) + except TypeError: + _err(_tr("Wrong input: must be a vector.")) + return None + + _msg("subelements: {}".format(subelements)) + if subelements: + try: + # Make a list + if isinstance(subelements, str): + subelements = [subelements] + + utils.type_check([(subelements, (list, tuple, str))], + name=_name) + except TypeError: + _err(_tr("Wrong input: must be a list or tuple of strings. " + "Or a single string.")) + return None + + # The subelements list is used to build a special list + # called a LinkSubList, which includes the path_object. + # Old style: [(path_object, "Edge1"), (path_object, "Edge2")] + # New style: [(path_object, ("Edge1", "Edge2"))] + # + # If a simple list is given ["a", "b"], this will create an old-style + # SubList. + # If a nested list is given [["a", "b"]], this will create a new-style + # SubList. + # In any case, the property of the object accepts both styles. + # + # If the old style is deprecated then this code should be updated + # to create new style lists exclusively. + sub_list = list() + for sub in subelements: + sub_list.append((path_object, sub)) + else: + sub_list = None + + align = bool(align) + _msg("align: {}".format(align)) + + _msg("align_mode: {}".format(align_mode)) + try: + utils.type_check([(align_mode, str)], + name=_name) + + if align_mode not in ("Original", "Frenet", "Tangent"): + raise TypeError + except TypeError: + _err(_tr("Wrong input: must be " + "'Original', 'Frenet', or 'Tangent'.")) + return None + + _msg("tan_vector: {}".format(tan_vector)) + try: + utils.type_check([(tan_vector, App.Vector)], + name=_name) + except TypeError: + _err(_tr("Wrong input: must be a vector.")) + return None + + force_vertical = bool(force_vertical) + _msg("force_vertical: {}".format(force_vertical)) + + _msg("vertical_vector: {}".format(vertical_vector)) + try: + utils.type_check([(vertical_vector, App.Vector)], + name=_name) + except TypeError: + _err(_tr("Wrong input: must be a vector.")) + return None + + use_link = bool(use_link) + _msg("use_link: {}".format(use_link)) if use_link: - obj = App.ActiveDocument.addObject("Part::FeaturePython","PathArray", PathArray(None), None, True) + # The PathArray class must be called in this special way + # to make it a PathLinkArray + new_obj = doc.addObject("Part::FeaturePython", "PathArray", + PathArray(None), None, True) else: - obj = App.ActiveDocument.addObject("Part::FeaturePython","PathArray") - PathArray(obj) + new_obj = doc.addObject("Part::FeaturePython", "PathArray") + PathArray(new_obj) - obj.Base = baseobject - obj.PathObj = pathobject - - if pathobjsubs: - sl = [] - for sub in pathobjsubs: - sl.append((obj.PathObj,sub)) - obj.PathSubs = list(sl) - - if count > 1: - obj.Count = count - - if xlate: - obj.Xlate = xlate - - obj.Align = align + new_obj.Base = base_object + new_obj.PathObj = path_object + new_obj.Count = count + new_obj.Xlate = xlate + new_obj.PathSubs = sub_list + new_obj.Align = align + new_obj.AlignMode = align_mode + new_obj.TangentVector = tan_vector + new_obj.ForceVertical = force_vertical + new_obj.VerticalVector = vertical_vector if App.GuiUp: if use_link: - ViewProviderDraftLink(obj.ViewObject) + ViewProviderDraftLink(new_obj.ViewObject) else: - ViewProviderDraftArray(obj.ViewObject) - gui_utils.formatObject(obj,obj.Base) - if hasattr(obj.Base.ViewObject, "DiffuseColor"): - if len(obj.Base.ViewObject.DiffuseColor) > 1: - obj.ViewObject.Proxy.resetColors(obj.ViewObject) - baseobject.ViewObject.hide() - gui_utils.select(obj) - return obj + ViewProviderDraftArray(new_obj.ViewObject) + gui_utils.formatObject(new_obj, new_obj.Base) + + if hasattr(new_obj.Base.ViewObject, "DiffuseColor"): + if len(new_obj.Base.ViewObject.DiffuseColor) > 1: + new_obj.ViewObject.Proxy.resetColors(new_obj.ViewObject) + + new_obj.Base.ViewObject.hide() + gui_utils.select(new_obj) + + return new_obj -makePathArray = make_path_array +def makePathArray(baseobject, pathobject, count, + xlate=None, align=False, + pathobjsubs=[], + use_link=False): + """Create PathArray. DEPRECATED. Use 'make_path_array'.""" + utils.use_instead('make_path_array') + + return make_path_array(baseobject, pathobject, count, + xlate, pathobjsubs, + align, + use_link) diff --git a/src/Mod/Draft/draftobjects/patharray.py b/src/Mod/Draft/draftobjects/patharray.py index 20d552cb91..1ad411fcb8 100644 --- a/src/Mod/Draft/draftobjects/patharray.py +++ b/src/Mod/Draft/draftobjects/patharray.py @@ -1,7 +1,12 @@ # *************************************************************************** # * Copyright (c) 2009, 2010 Yorik van Havre * # * Copyright (c) 2009, 2010 Ken Cline * -# * Copyright (c) 2020 FreeCAD Developers * +# * Copyright (c) 2013 Wandererfan * +# * Copyright (c) 2019 Zheng, Lei (realthunder)* +# * Copyright (c) 2020 Carlo Pavan * +# * Copyright (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) * @@ -20,150 +25,270 @@ # * USA * # * * # *************************************************************************** -"""This module provides the object code for the Draft PathArray object. +"""Provides the object code for the Draft PathArray object. + +The copies will be placed along a path like a polyline, spline, or bezier +curve. """ ## @package patharray # \ingroup DRAFT -# \brief This module provides the object code for the Draft PathArray object. +# \brief Provides the object code for the Draft PathArray object. import FreeCAD as App import DraftVecUtils +import lazy_loader.lazy_loader as lz -from draftutils.utils import get_param -from draftutils.messages import _msg, _wrn -from draftutils.translate import _tr, translate +from draftutils.messages import _msg, _wrn, _err +from draftutils.translate import _tr from draftobjects.draftlink import DraftLink +# Delay import of module until first use because it is heavy +Part = lz.LazyLoader("Part", globals(), "Part") +DraftGeomUtils = lz.LazyLoader("DraftGeomUtils", globals(), "DraftGeomUtils") + + class PathArray(DraftLink): - """The Draft Path Array object - distributes copies of an object along a path. - Original mode is the historic "Align" for old (v0.18) documents. It is not - really the Fernat alignment. Uses the normal parameter from getNormal (or the - default) as a constant - it does not calculate curve normal. - X is curve tangent, Y is normal parameter, Z is (X x Y) + """The Draft Path Array object. - Tangent mode is similar to Original, but includes a pre-rotation (in execute) to - align the Base object's X to the TangentVector, then X follows curve tangent, - normal input parameter is the Z component. + The object distributes copies of an object along a path like a polyline, + spline, or bezier curve. - If the ForceVertical option is applied, the normal parameter from getNormal is - ignored, and X is curve tangent, Z is VerticalVector, Y is (X x Z) + Attributes + ---------- + Align: bool + It defaults to `False`. + It sets whether the object will be specially aligned to the path. + + AlignMode: str + It defaults to `'Original'`. + Indicates the type of alignment that will be calculated when + `Align` is `True`. + + `'Original'` mode is the historic `'Align'` for old (v0.18) documents. + It is not really the Fernat alignment. It uses the normal parameter + from `getNormal` (or the default) as a constant, it does not calculate + curve normal. + `X` is curve tangent, `Y` is normal parameter, `Z` is the cross product + `X` x `Y`. + + `'Tangent'` mode is similar to `Original`, but includes a pre-rotation + (in execute) to align the `Base` object's `X` to `TangentVector`, + then `X` follows curve tangent, normal input parameter + is the Z component. + + If `ForceVertical` is `True`, the normal parameter from `getNormal` + is ignored, and `X` is curve tangent, `Z` is `VerticalVector`, + and `Y` is the cross product `X` x `Z`. + + `'Frenet'` mode orients the copies to a coordinate system + along the path. + `X` is tangent to curve, `Y` is curve normal, `Z` is curve binormal. + If normal cannot be computed, for example, in a straight line, + the default is used. + + ForceVertical: bool + It defaults to `False`. + If it is `True`, and `AlignMode` is `'Original'` or `'Tangent'`, + it will use the vector in `VerticalVector` as the `Z` axis. + """ - Frenet mode orients the copies to a coordinate system along the path. - X is tangent to curve, Y is curve normal, Z is curve binormal. - if normal can not be computed (ex a straight line), the default is used.""" - def __init__(self, obj): super(PathArray, self).__init__(obj, "PathArray") - #For PathLinkArray, DraftLink.attach creates the link to the Base object. - def attach(self,obj): - self.setProperties(obj) - super(PathArray, self).attach(obj) + def attach(self, obj): + """Set up the properties when the object is attached. - def setProperties(self,obj): + Note: we don't exactly know why the properties are added + in the `attach` method. They should probably be added in the `__init__` + method. Maybe this is related to the link behavior of this class. + + For PathLinkArray, DraftLink.attach creates the link to the Base. + + Realthunder: before the big merge, there was only the attach() method + in the view object proxy, not the object proxy. + I added that to allow the proxy to override the C++ view provider + type. The view provider type is normally determined by object's + C++ API getViewProviderName(), and cannot be overridden by the proxy. + I introduced the attach() method in proxy to allow the core + to attach the proxy before creating the C++ view provider. + """ + self.set_properties(obj) + super(PathArray, self).attach(obj) + + def set_properties(self, obj): + """Set properties only if they don't exist.""" if not obj: return + if hasattr(obj, "PropertiesList"): - pl = obj.PropertiesList + properties = obj.PropertiesList else: - pl = [] + properties = [] - if not "Base" in pl: - _tip = _tr("The base object that must be duplicated") - obj.addProperty("App::PropertyLinkGlobal", "Base", "Objects", _tip) + if "Base" not in properties: + _tip = _tr("The base object that will be duplicated") + obj.addProperty("App::PropertyLinkGlobal", + "Base", + "Objects", + _tip) + obj.Base = None - if not "PathObj" in pl: - _tip = _tr("The path object along which to distribute objects") - obj.addProperty("App::PropertyLinkGlobal", "PathObj", "Objects", _tip) + if "PathObj" not in properties: + _tip = _tr("The object along which " + "the copies will be distributed. " + "It must contain 'Edges'.") + obj.addProperty("App::PropertyLinkGlobal", + "PathObj", + "Objects", + _tip) + obj.PathObj = None - if not "PathSubs" in pl: - _tip = _tr("Selected subobjects (edges) of PathObj") - obj.addProperty("App::PropertyLinkSubListGlobal", "PathSubs", "Objects", _tip) + if "PathSubs" not in properties: + _tip = _tr("List of connected edges in the 'Path Object'.\n" + "If these are present, the copies will be created " + "along these subelements only.\n" + "Leave this property empty to create copies along " + "the entire 'Path Object'.") + obj.addProperty("App::PropertyLinkSubListGlobal", + "PathSubs", + "Objects", + _tip) obj.PathSubs = [] - - if not "Count" in pl: - _tip = _tr("Number of copies") - obj.addProperty("App::PropertyInteger", "Count", "Parameters", _tip) - obj.Count = 2 -# copy alignment properties - if not "Align" in pl: - _tip = _tr("Orient the copies along path") - obj.addProperty("App::PropertyBool", "Align", "Alignment", _tip) + if "Count" not in properties: + _tip = _tr("Number of copies to create") + obj.addProperty("App::PropertyInteger", + "Count", + "General", + _tip) + obj.Count = 4 + + if "Align" not in properties: + _tip = _tr("Orient the copies along the path depending " + "on the 'Align Mode'.\n" + "Otherwise the copies will have the same orientation " + "as the original Base object.") + obj.addProperty("App::PropertyBool", + "Align", + "Alignment", + _tip) obj.Align = False - - if not "AlignMode" in pl: - _tip = _tr("How to orient copies on path") - obj.addProperty("App::PropertyEnumeration","AlignMode","Alignment", _tip) - obj.AlignMode = ['Original','Frenet','Tangent'] - obj.AlignMode = 'Original' - - if not "Xlate" in pl: - _tip = _tr("Optional translation vector") - obj.addProperty("App::PropertyVectorDistance","Xlate","Alignment", _tip) - obj.Xlate = App.Vector(0,0,0) - - if not "TangentVector" in pl: - _tip = _tr("Alignment vector for Tangent mode") - obj.addProperty("App::PropertyVector","TangentVector","Alignment", _tip) - obj.TangentVector = App.Vector(1,0,0) - if not "ForceVertical" in pl: - _tip = _tr("Force Original/Tangent modes to use VerticalVector as Z") - obj.addProperty("App::PropertyBool","ForceVertical","Alignment", _tip) + if "AlignMode" not in properties: + _tip = _tr("Method to orient the copies along the path.\n" + "- Original, X is curve tangent, Y is normal, " + "and Z is the cross product.\n" + "- Frenet uses a local coordinate system along " + "the path.\n" + "- Tangent is similar to 'Original' but the local X " + "axis is pre-aligned to 'Tangent Vector'.\n" + "To get better results with 'Original' and 'Tangent' " + "you may have to set 'Force Vertical' to true.") + obj.addProperty("App::PropertyEnumeration", + "AlignMode", + "Alignment", + _tip) + obj.AlignMode = ['Original', 'Frenet', 'Tangent'] + obj.AlignMode = 'Original' + + if "Xlate" not in properties: + _tip = _tr("Additional translation " + "that will be applied to each copy.\n" + "This is useful to adjust for the difference " + "between shape centre and shape reference point.") + obj.addProperty("App::PropertyVectorDistance", + "Xlate", + "Alignment", + _tip) + obj.Xlate = App.Vector(0, 0, 0) + + if "TangentVector" not in properties: + _tip = _tr("Alignment vector for 'Tangent' mode") + obj.addProperty("App::PropertyVector", + "TangentVector", + "Alignment", + _tip) + obj.TangentVector = App.Vector(1, 0, 0) + + if "ForceVertical" not in properties: + _tip = _tr("Force use of 'Vertical Vector' as Z direction " + "when using 'Original' or 'Tangent' align mode") + obj.addProperty("App::PropertyBool", + "ForceVertical", + "Alignment", + _tip) obj.ForceVertical = False - if not "VerticalVector" in pl: - _tip = _tr("ForceVertical direction") - obj.addProperty("App::PropertyVector","VerticalVector","Alignment", _tip) - obj.VerticalVector = App.Vector(0,0,1) + if "VerticalVector" not in properties: + _tip = _tr("Direction of the local Z axis " + "when 'Force Vertical' is true") + obj.addProperty("App::PropertyVector", + "VerticalVector", + "Alignment", + _tip) + obj.VerticalVector = App.Vector(0, 0, 1) - if self.use_link and "ExpandArray" not in pl: - _tip = _tr("Show array element as children object") - obj.addProperty("App::PropertyBool","ExpandArray", "Parameters", _tip) + if self.use_link and "ExpandArray" not in properties: + _tip = _tr("Show the individual array elements " + "(only for Link arrays)") + obj.addProperty("App::PropertyBool", + "ExpandArray", + "General", + _tip) obj.ExpandArray = False - obj.setPropertyStatus('Shape','Transient') + obj.setPropertyStatus('Shape', 'Transient') - def linkSetup(self,obj): + def linkSetup(self, obj): + """Set up the object as a link object.""" super(PathArray, self).linkSetup(obj) obj.configLinkProperty(ElementCount='Count') - def execute(self,obj): - import Part - import DraftGeomUtils + def execute(self, obj): + """Execute when the object is created or recomputed.""" if obj.Base and obj.PathObj: - pl = obj.Placement #placement of whole pathArray + pl = obj.Placement # placement of entire PathArray object if obj.PathSubs: w = self.getWireFromSubs(obj) - elif (hasattr(obj.PathObj.Shape,'Wires') and obj.PathObj.Shape.Wires): + elif (hasattr(obj.PathObj.Shape, 'Wires') + and obj.PathObj.Shape.Wires): w = obj.PathObj.Shape.Wires[0] elif obj.PathObj.Shape.Edges: w = Part.Wire(obj.PathObj.Shape.Edges) else: - App.Console.PrintLog ("PathArray.execute: path " + obj.PathObj.Name + " has no edges\n") + _err(obj.PathObj.Name + + _tr(", path object doesn't have 'Edges'.")) return - if (hasattr(obj, "TangentVector")) and (obj.AlignMode == "Tangent") and (obj.Align): + + if (hasattr(obj, "TangentVector") + and obj.AlignMode == "Tangent" and obj.Align): basePlacement = obj.Base.Shape.Placement baseRotation = basePlacement.Rotation - stdX = App.Vector(1.0, 0.0, 0.0) #default TangentVector - if (not DraftVecUtils.equals(stdX, obj.TangentVector)): - preRotation = App.Rotation(obj.TangentVector, stdX) #make rotation from TangentVector to X + stdX = App.Vector(1.0, 0.0, 0.0) # default TangentVector + + if not DraftVecUtils.equals(stdX, obj.TangentVector): + # make rotation from TangentVector to X + preRotation = App.Rotation(obj.TangentVector, stdX) netRotation = baseRotation.multiply(preRotation) else: netRotation = baseRotation - base = calculatePlacementsOnPath( - netRotation,w,obj.Count,obj.Xlate,obj.Align, obj.AlignMode, - obj.ForceVertical, obj.VerticalVector) + + base = placements_on_path(netRotation, + w, obj.Count, obj.Xlate, + obj.Align, obj.AlignMode, + obj.ForceVertical, + obj.VerticalVector) else: - base = calculatePlacementsOnPath( - obj.Base.Shape.Placement.Rotation,w,obj.Count,obj.Xlate,obj.Align, obj.AlignMode, - obj.ForceVertical, obj.VerticalVector) + base = placements_on_path(obj.Base.Shape.Placement.Rotation, + w, obj.Count, obj.Xlate, + obj.Align, obj.AlignMode, + obj.ForceVertical, + obj.VerticalVector) + return super(PathArray, self).buildShape(obj, pl, base) - def getWireFromSubs(self,obj): - '''Make a wire from PathObj subelements''' - import Part + def getWireFromSubs(self, obj): + """Make a wire from PathObj subelements.""" sl = [] for sub in obj.PathSubs: edgeNames = sub[1] @@ -173,37 +298,46 @@ class PathArray(DraftLink): return Part.Wire(sl) def onDocumentRestored(self, obj): + """Execute code when the document is restored. + + Add properties that don't exist. + """ self.migrate_attributes(obj) - self.setProperties(obj) + self.set_properties(obj) if self.use_link: self.linkSetup(obj) else: - obj.setPropertyStatus('Shape','-Transient') + obj.setPropertyStatus('Shape', '-Transient') + if obj.Shape.isNull(): - if getattr(obj,'PlacementList',None): - self.buildShape(obj,obj.Placement,obj.PlacementList) + if getattr(obj, 'PlacementList', None): + self.buildShape(obj, obj.Placement, obj.PlacementList) else: self.execute(obj) + _PathArray = PathArray -def calculatePlacementsOnPath(shapeRotation, pathwire, count, xlate, align, - mode = 'Original', forceNormal=False, normalOverride=None): - """Calculates the placements of a shape along a given path so that each copy will be distributed evenly""" - import Part - import DraftGeomUtils - closedpath = DraftGeomUtils.isReallyClosed(pathwire) +def placements_on_path(shapeRotation, pathwire, count, xlate, align, + mode='Original', forceNormal=False, + normalOverride=None): + """Calculate the placements of a shape along a given path. + + Each copy will be distributed evenly. + """ + closedpath = DraftGeomUtils.isReallyClosed(pathwire) normal = DraftGeomUtils.getNormal(pathwire) + if forceNormal and normalOverride: - normal = normalOverride + normal = normalOverride path = Part.__sortEdges__(pathwire.Edges) ends = [] cdist = 0 - for e in path: # find cumulative edge end distance + for e in path: # find cumulative edge end distance cdist += e.Length ends.append(cdist) @@ -211,15 +345,20 @@ def calculatePlacementsOnPath(shapeRotation, pathwire, count, xlate, align, # place the start shape pt = path[0].Vertexes[0].Point - placements.append(calculatePlacement( - shapeRotation, path[0], 0, pt, xlate, align, normal, mode, forceNormal)) + _place = calculate_placement(shapeRotation, + path[0], 0, pt, xlate, align, normal, + mode, forceNormal) + placements.append(_place) # closed path doesn't need shape on last vertex - if not(closedpath): + if not closedpath: # place the end shape pt = path[-1].Vertexes[-1].Point - placements.append(calculatePlacement( - shapeRotation, path[-1], path[-1].Length, pt, xlate, align, normal, mode, forceNormal)) + _place = calculate_placement(shapeRotation, + path[-1], path[-1].Length, + pt, xlate, align, normal, + mode, forceNormal) + placements.append(_place) if count < 3: return placements @@ -245,24 +384,35 @@ def calculatePlacementsOnPath(shapeRotation, pathwire, count, xlate, align, # place shape at proper spot on proper edge remains = ends[iend] - travel offset = path[iend].Length - remains - pt = path[iend].valueAt(getParameterFromV0(path[iend], offset)) + pt = path[iend].valueAt(get_parameter_from_v0(path[iend], offset)) - placements.append(calculatePlacement( - shapeRotation, path[iend], offset, pt, xlate, align, normal, mode, forceNormal)) + _place = calculate_placement(shapeRotation, + path[iend], offset, + pt, xlate, align, normal, + mode, forceNormal) + placements.append(_place) travel += step return placements -def calculatePlacement(globalRotation, edge, offset, RefPt, xlate, align, normal=None, - mode = 'Original', overrideNormal=False): - """Orient shape to a local coord system (tangent, normal, binormal) at parameter offset (normally length)""" - import functools - # http://en.wikipedia.org/wiki/Euler_angles (previous version) - # http://en.wikipedia.org/wiki/Quaternions - # start with null Placement point so _tr goes to right place. + +calculatePlacementsOnPath = placements_on_path + + +def calculate_placement(globalRotation, + edge, offset, RefPt, xlate, align, normal=None, + mode='Original', overrideNormal=False): + """Orient shape to a local coordinate system (tangent, normal, binormal). + + Orient shape at parameter offset, normally length. + + http://en.wikipedia.org/wiki/Euler_angles (previous version) + http://en.wikipedia.org/wiki/Quaternions + """ + # Start with a null Placement so the translation goes to the right place. + # Then apply the global orientation. placement = App.Placement() - # preserve global orientation placement.Rotation = globalRotation placement.move(RefPt + xlate) @@ -271,65 +421,77 @@ def calculatePlacement(globalRotation, edge, offset, RefPt, xlate, align, normal nullv = App.Vector(0, 0, 0) defNormal = App.Vector(0.0, 0.0, 1.0) - if not normal is None: + if normal: defNormal = normal try: - t = edge.tangentAt(getParameterFromV0(edge, offset)) + t = edge.tangentAt(get_parameter_from_v0(edge, offset)) t.normalize() except: - _msg("Draft CalculatePlacement - Cannot calculate Path tangent. Copy not aligned\n") + _wrn(_tr("Cannot calculate path tangent. Copy not aligned.")) return placement - if (mode == 'Original') or (mode == 'Tangent'): + if mode in ('Original', 'Tangent'): if normal is None: - n = defNormal + n = defNormal else: n = normal n.normalize() + try: b = t.cross(n) b.normalize() - except: # weird special case. tangent & normal parallel + except: + # weird special case, tangent and normal parallel b = nullv - _msg("PathArray computePlacement - parallel tangent, normal. Copy not aligned\n") + _wrn(_tr("Tangent and normal are parallel. Copy not aligned.")) return placement + if overrideNormal: priority = "XZY" - newRot = App.Rotation(t, b, n, priority); #t/x, b/y, n/z - else: - priority = "XZY" #must follow X, try to follow Z, Y is what it is - newRot = App.Rotation(t, n, b, priority); + newRot = App.Rotation(t, b, n, priority) # t/x, b/y, n/z + else: + # must follow X, try to follow Z, Y is what it is + priority = "XZY" + newRot = App.Rotation(t, n, b, priority) + elif mode == 'Frenet': try: - n = edge.normalAt(getParameterFromV0(edge, offset)) + n = edge.normalAt(get_parameter_from_v0(edge, offset)) n.normalize() - except App.Base.FreeCADError: # no/infinite normals here + except App.Base.FreeCADError: # no/infinite normals here n = defNormal - _msg("PathArray computePlacement - Cannot calculate Path normal, using default\n") + _msg(_tr("Cannot calculate path normal, using default.")) + try: b = t.cross(n) b.normalize() except: b = nullv - _msg("Draft PathArray.orientShape - Cannot calculate Path biNormal. Copy not aligned\n") + _wrn(_tr("Cannot calculate path binormal. Copy not aligned.")) return placement - priority = "XZY" - newRot = App.Rotation(t, n, b, priority); #t/x, n/y, b/z + + priority = "XZY" + newRot = App.Rotation(t, n, b, priority) # t/x, n/y, b/z else: _msg(_tr("AlignMode {} is not implemented".format(mode))) return placement - - #have valid t, n, b + + # Have valid tangent, normal, binormal newGRot = newRot.multiply(globalRotation) placement.Rotation = newGRot return placement -def getParameterFromV0(edge, offset): - """return parameter at distance offset from edge.Vertexes[0] - sb method in Part.TopoShapeEdge???""" +calculatePlacement = calculate_placement + + +def get_parameter_from_v0(edge, offset): + """Return parameter at distance offset from edge.Vertexes[0]. + + sb method in Part.TopoShapeEdge??? + """ lpt = edge.valueAt(edge.getParameterByLength(0)) vpt = edge.Vertexes[0].Point @@ -340,4 +502,7 @@ def getParameterFromV0(edge, offset): # this edge is right way around length = offset - return (edge.getParameterByLength(length)) + return edge.getParameterByLength(length) + + +getParameterFromV0 = get_parameter_from_v0 diff --git a/src/Mod/Draft/drafttests/test_modification.py b/src/Mod/Draft/drafttests/test_modification.py index 33516f2b0b..0ef50065a5 100644 --- a/src/Mod/Draft/drafttests/test_modification.py +++ b/src/Mod/Draft/drafttests/test_modification.py @@ -468,11 +468,13 @@ class DraftModification(unittest.TestCase): number = 4 translation = Vector(0, 1, 0) + subelements = "Edge1" align = False _msg(" Path Array") _msg(" number={}, translation={}".format(number, translation)) - _msg(" align={}".format(align)) - obj = Draft.make_path_array(poly, wire, number, translation, align) + _msg(" subelements={}, align={}".format(subelements, align)) + obj = Draft.make_path_array(poly, wire, number, + translation, subelements, align) self.assertTrue(obj, "'{}' failed".format(operation)) def test_point_array(self):