# *************************************************************************** # * (c) 2009 Yorik van Havre * # * (c) 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 Bezier curves with the Draft Workbench. In particular, a cubic Bezier curve is defined, as it is one of the most useful curves for many applications. See https://en.wikipedia.org/wiki/B%C3%A9zier_curve """ ## @package gui_beziers # \ingroup DRAFT # \brief Provides tools for creating Bezier curves with the Draft Workbench. from PySide.QtCore import QT_TRANSLATE_NOOP import FreeCADGui as Gui import draftutils.utils as utils import draftutils.todo as todo import draftguitools.gui_base_original as gui_base_original import draftguitools.gui_tool_utils as gui_tool_utils import draftguitools.gui_lines as gui_lines import draftguitools.gui_trackers as trackers from draftutils.messages import _msg, _err from draftutils.translate import translate class BezCurve(gui_lines.Line): """Gui command for the Bezier Curve tool.""" def __init__(self): super(BezCurve, self).__init__(wiremode=True) self.degree = None def GetResources(self): """Set icon, menu and tooltip.""" _menu = "Bezier curve" _tip = ("Creates an N-degree Bezier curve. " "The more points you pick, the higher the degree.\n" "CTRL to snap, SHIFT to constrain.") return {'Pixmap': 'Draft_BezCurve', 'Accel': "B, Z", 'MenuText': QT_TRANSLATE_NOOP("Draft_BezCurve", _menu), 'ToolTip': QT_TRANSLATE_NOOP("Draft_BezCurve", _tip)} def Activated(self): """Execute when the command is called. Activate the specific bezier curve tracker. """ super(BezCurve, self).Activated(name=translate("draft", "BezCurve")) if self.doc: self.bezcurvetrack = trackers.bezcurveTracker() def action(self, arg): """Handle the 3D scene events. This is installed as an EventCallback in the Inventor view by the `Activated` method of the parent class. Parameters ---------- arg: dict Dictionary with strings that indicates the type of event received from the 3D view. """ if arg["Type"] == "SoKeyboardEvent": if arg["Key"] == "ESCAPE": self.finish() elif arg["Type"] == "SoLocation2Event": # mouse movement detection (self.point, ctrlPoint, info) = gui_tool_utils.getPoint(self, arg, noTracker=True) # existing points + this pointer position self.bezcurvetrack.update(self.node + [self.point], degree=self.degree) gui_tool_utils.redraw3DView() elif (arg["Type"] == "SoMouseButtonEvent" and arg["State"] == "DOWN" and arg["Button"] == "BUTTON1"): # left click if arg["Position"] == self.pos: self.finish(False, cont=True) if (not self.node) and (not self.support): # first point gui_tool_utils.getSupport(arg) (self.point, ctrlPoint, info) = gui_tool_utils.getPoint(self, arg, noTracker=True) if self.point: self.ui.redraw() self.pos = arg["Position"] self.node.append(self.point) # add point to "clicked list" # sb add a control point, # if mod(len(cpoints), 2) == 0 # then create 2 handle points? self.drawUpdate(self.point) if not self.isWire and len(self.node) == 2: self.finish(False, cont=True) if len(self.node) > 2: # does this make sense for a BCurve? # DNC: allows to close the curve # by placing ends close to each other # with tol = Draft tolerance # old code has been to insensitive if (self.point-self.node[0]).Length < utils.tolerance(): self.undolast() self.finish(True, cont=True) _msg(translate("draft", "Bezier curve has been closed")) def undolast(self): """Undo last line segment.""" if len(self.node) > 1: self.node.pop() self.bezcurvetrack.update(self.node, degree=self.degree) self.obj.Shape = self.updateShape(self.node) _msg(translate("draft", "Last point has been removed")) def drawUpdate(self, point): """Draw and update to the curve.""" if len(self.node) == 1: self.bezcurvetrack.on() if self.planetrack: self.planetrack.set(self.node[0]) _msg(translate("draft", "Pick next point")) else: self.obj.Shape = self.updateShape(self.node) _msg(translate("draft", "Pick next point, " "or finish (A) or close (O)")) def updateShape(self, pts): """Create shape for display during creation process.""" import Part edges = [] if len(pts) >= 2: # allow lower degree segment poles = pts[1:] else: poles = [] if self.degree: segpoleslst = [poles[x:x+self.degree] for x in range(0, len(poles), (self.degree or 1))] else: segpoleslst = [pts] startpoint = pts[0] for segpoles in segpoleslst: c = Part.BezierCurve() # last segment may have lower degree c.increase(len(segpoles)) c.setPoles([startpoint] + segpoles) edges.append(Part.Edge(c)) startpoint = segpoles[-1] w = Part.Wire(edges) return w def finish(self, closed=False, cont=False): """Terminate the operation and close the curve if asked. Parameters ---------- closed: bool, optional Close the line if `True`. """ if self.ui: if hasattr(self, "bezcurvetrack"): self.bezcurvetrack.finalize() if not utils.getParam("UiMode", 1): Gui.Control.closeDialog() if self.obj: # remove temporary object, if any old = self.obj.Name todo.ToDo.delay(self.doc.removeObject, old) if len(self.node) > 1: # The command to run is built as a series of text strings # to be committed through the `draftutils.todo.ToDo` class. try: rot, sup, pts, fil = self.getStrings() Gui.addModule("Draft") _cmd = 'Draft.makeBezCurve' _cmd += '(' _cmd += 'points, ' _cmd += 'closed=' + str(closed) + ', ' _cmd += 'support=' + sup + ', ' _cmd += 'degree=' + str(self.degree) _cmd += ')' _cmd_list = ['points = ' + pts, 'bez = ' + _cmd, 'Draft.autogroup(bez)', 'FreeCAD.ActiveDocument.recompute()'] self.commit(translate("draft", "Create BezCurve"), _cmd_list) except Exception: _err("Draft: error delaying commit") # `Creator` is the grandfather class, the parent of `Line`; # we need to call it to perform final cleanup tasks. # # Calling it directly like this is a bit messy; maybe we need # another method that performs cleanup (superfinish) # that is not re-implemented by any of the child classes. gui_base_original.Creator.finish(self) if self.ui and self.ui.continueMode: self.Activated() Gui.addCommand('Draft_BezCurve', BezCurve()) class CubicBezCurve(gui_lines.Line): """Gui command for the 3rd degree Bezier Curve tool.""" def __init__(self): super(CubicBezCurve, self).__init__(wiremode=True) self.degree = 3 def GetResources(self): """Set icon, menu and tooltip.""" _menu = "Cubic bezier curve" _tip = ("Creates a Bezier curve made of 2nd degree (quadratic) " "and 3rd degree (cubic) segments. " "Click and drag to define each segment.\n" "After the curve is created you can go back to edit " "each control point and set the properties of each knot.\n" "CTRL to snap, SHIFT to constrain.") return {'Pixmap': 'Draft_CubicBezCurve', # 'Accel': "B, Z", 'MenuText': QT_TRANSLATE_NOOP("Draft_CubicBezCurve", _menu), 'ToolTip': QT_TRANSLATE_NOOP("Draft_CubicBezCurve", _tip)} def Activated(self): """Execute when the command is called. Activate the specific BezCurve tracker. """ super(CubicBezCurve, self).Activated(name=translate("draft", "CubicBezCurve")) if self.doc: self.bezcurvetrack = trackers.bezcurveTracker() def action(self, arg): """Handle the 3D scene events. This is installed as an EventCallback in the Inventor view by the `Activated` method of the parent class. Parameters ---------- arg: dict Dictionary with strings that indicates the type of event received from the 3D view. """ if arg["Type"] == "SoKeyboardEvent": if arg["Key"] == "ESCAPE": self.finish() elif arg["Type"] == "SoLocation2Event": # mouse movement detection (self.point, ctrlPoint, info) = gui_tool_utils.getPoint(self, arg, noTracker=True) if (len(self.node) - 1) % self.degree == 0 and len(self.node) > 2: prevctrl = 2 * self.node[-1] - self.point # Existing points + this pointer position self.bezcurvetrack.update(self.node[0:-2] + [prevctrl] + [self.node[-1]] + [self.point], degree=self.degree) else: # Existing points + this pointer position self.bezcurvetrack.update(self.node + [self.point], degree=self.degree) gui_tool_utils.redraw3DView() elif arg["Type"] == "SoMouseButtonEvent": # Press and hold the button if arg["State"] == "DOWN" and arg["Button"] == "BUTTON1": if arg["Position"] == self.pos: if len(self.node) > 2: self.node = self.node[0:-2] else: self.node = [] return else: if (not self.node) and (not self.support): # first point gui_tool_utils.getSupport(arg) (self.point, ctrlPoint, info) = gui_tool_utils.getPoint(self, arg, noTracker=True) if self.point: self.ui.redraw() self.pos = arg["Position"] # add point to "clicked list" self.node.append(self.point) # sb add a control point, # if mod(len(cpoints), 2) == 0 # then create 2 handle points? self.drawUpdate(self.point) if not self.isWire and len(self.node) == 2: self.finish(False, cont=True) # does this make sense for a BCurve? if len(self.node) > 2: # add point to "clicked list" self.node.append(self.point) self.drawUpdate(self.point) # DNC: allows to close the curve # by placing ends close to each other # with tol = Draft tolerance # old code has been to insensitive _diff = (self.point - self.node[0]).Length if (_diff < utils.tolerance() and len(self.node) >= 4): # self.undolast() self.node = self.node[0:-2] # close the curve with a smooth symmetric knot _sym = 2 * self.node[0] - self.node[1] self.node.append(_sym) self.finish(True, cont=True) _msg(translate("draft", "Bezier curve has been closed")) # Release the held button if arg["State"] == "UP" and arg["Button"] == "BUTTON1": if arg["Position"] == self.pos: self.node = self.node[0:-2] return else: if (not self.node) and (not self.support): # first point return if self.point: self.ui.redraw() self.pos = arg["Position"] # add point to "clicked list" self.node.append(self.point) # sb add a control point, # if mod(len(cpoints),2) == 0 # then create 2 handle points? self.drawUpdate(self.point) if not self.isWire and len(self.node) == 2: self.finish(False, cont=True) # Does this make sense for a BCurve? if len(self.node) > 2: self.node[-3] = 2 * self.node[-2] - self.node[-1] self.drawUpdate(self.point) # DNC: allows to close the curve # by placing ends close to each other # with tol = Draft tolerance # old code has been to insensitive def undolast(self): """Undo last line segment.""" if len(self.node) > 1: self.node.pop() self.bezcurvetrack.update(self.node, degree=self.degree) self.obj.Shape = self.updateShape(self.node) _msg(translate("draft", "Last point has been removed")) def drawUpdate(self, point): """Create shape for display during creation process.""" if len(self.node) == 1: self.bezcurvetrack.on() if self.planetrack: self.planetrack.set(self.node[0]) _msg(translate("draft", "Click and drag to define next knot")) elif (len(self.node) - 1) % self.degree == 1 and len(self.node) > 2: # is a knot self.obj.Shape = self.updateShape(self.node[:-1]) _msg(translate("draft", "Click and drag to define next knot, " "or finish (A) or close (O)")) def updateShape(self, pts): """Create shape for display during creation process.""" import Part # Not quite right. draws 1 big bez. sb segmented edges = [] if len(pts) >= 2: # allow lower degree segment poles = pts[1:] else: poles = [] if self.degree: segpoleslst = [poles[x:x+self.degree] for x in range(0, len(poles), (self.degree or 1))] else: segpoleslst = [pts] startpoint = pts[0] for segpoles in segpoleslst: c = Part.BezierCurve() # last segment may have lower degree c.increase(len(segpoles)) c.setPoles([startpoint] + segpoles) edges.append(Part.Edge(c)) startpoint = segpoles[-1] w = Part.Wire(edges) return w def finish(self, closed=False, cont=False): """Terminate the operation and close the curve if asked. Parameters ---------- closed: bool, optional Close the line if `True`. """ if self.ui: if hasattr(self, "bezcurvetrack"): self.bezcurvetrack.finalize() if not utils.getParam("UiMode", 1): Gui.Control.closeDialog() if self.obj: # remove temporary object, if any old = self.obj.Name todo.ToDo.delay(self.doc.removeObject, old) if closed is False: cleannd = (len(self.node) - 1) % self.degree if cleannd == 0: self.node = self.node[0:-3] if cleannd > 0: self.node = self.node[0:-cleannd] if len(self.node) > 1: try: # The command to run is built as a series of text strings # to be committed through the `draftutils.todo.ToDo` class. rot, sup, pts, fil = self.getStrings() Gui.addModule("Draft") _cmd = 'Draft.makeBezCurve' _cmd += '(' _cmd += 'points, ' _cmd += 'closed=' + str(closed) + ', ' _cmd += 'support=' + sup + ', ' _cmd += 'degree=' + str(self.degree) _cmd += ')' _cmd_list = ['points = ' + pts, 'bez = ' + _cmd, 'Draft.autogroup(bez)', 'FreeCAD.ActiveDocument.recompute()'] self.commit(translate("draft", "Create BezCurve"), _cmd_list) except Exception: _err("Draft: error delaying commit") # `Creator` is the grandfather class, the parent of `Line`; # we need to call it to perform final cleanup tasks. # # Calling it directly like this is a bit messy; maybe we need # another method that performs cleanup (superfinish) # that is not re-implemented by any of the child classes. gui_base_original.Creator.finish(self) if self.ui and self.ui.continueMode: self.Activated() Gui.addCommand('Draft_CubicBezCurve', CubicBezCurve()) class BezierGroup: """Gui Command group for the Bezier curve tools.""" def GetResources(self): """Set icon, menu and tooltip.""" _menu = "Bezier tools" _tip = ("Create various types of Bezier curves.") return {'MenuText': QT_TRANSLATE_NOOP("Draft_BezierTools", _menu), 'ToolTip': QT_TRANSLATE_NOOP("Draft_BezierTools", _tip)} def GetCommands(self): """Return a tuple of commands in the group.""" return ('Draft_CubicBezCurve', 'Draft_BezCurve') def IsActive(self): """Return True when this command should be available. It is `True` when there is a document. """ if Gui.ActiveDocument: return True else: return False Gui.addCommand('Draft_BezierTools', BezierGroup())