FreeCAD
578 строк · 22.8 Кб
1# ***************************************************************************
2# * (c) 2009, 2010 Yorik van Havre <yorik@uncreated.net> *
3# * (c) 2009, 2010 Ken Cline <cline@frii.com> *
4# * (c) 2020 Eliud Cabrera Castillo <e.cabrera-castillo@tum.de> *
5# * *
6# * This file is part of the FreeCAD CAx development system. *
7# * *
8# * This program is free software; you can redistribute it and/or modify *
9# * it under the terms of the GNU Lesser General Public License (LGPL) *
10# * as published by the Free Software Foundation; either version 2 of *
11# * the License, or (at your option) any later version. *
12# * for detail see the LICENCE text file. *
13# * *
14# * FreeCAD is distributed in the hope that it will be useful, *
15# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
16# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
17# * GNU Library General Public License for more details. *
18# * *
19# * You should have received a copy of the GNU Library General Public *
20# * License along with FreeCAD; if not, write to the Free Software *
21# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
22# * USA *
23# * *
24# ***************************************************************************
25"""Provides GUI tools to trim and extend lines.
26
27It also extends closed faces to create solids, that is, it can be used
28to extrude a closed profile.
29
30Make sure the snapping is active so that the extrusion is done following
31the direction of a line, and up to the distance specified
32by the snapping point.
33"""
34## @package gui_trimex
35# \ingroup draftguitools
36# \brief Provides GUI tools to trim and extend lines.
37
38## \addtogroup draftguitools
39# @{
40import math41from PySide.QtCore import QT_TRANSLATE_NOOP42
43import FreeCAD as App44import FreeCADGui as Gui45import Draft46import Draft_rc47import DraftVecUtils48import draftutils.utils as utils49import draftutils.gui_utils as gui_utils50import draftguitools.gui_base_original as gui_base_original51import draftguitools.gui_tool_utils as gui_tool_utils52import draftguitools.gui_trackers as trackers53
54from draftutils.messages import _msg, _err, _toolmsg55from draftutils.translate import translate56
57# The module is used to prevent complaints from code checkers (flake8)
58True if Draft_rc.__name__ else False59
60
61class Trimex(gui_base_original.Modifier):62"""Gui Command for the Trimex tool.63
64This tool trims or extends lines, wires and arcs,
65or extrudes single faces.
66
67SHIFT constrains to the last point
68or extrudes in direction to the face normal.
69"""
70
71def GetResources(self):72"""Set icon, menu and tooltip."""73
74return {'Pixmap': 'Draft_Trimex',75'Accel': "T, R",76'MenuText': QT_TRANSLATE_NOOP("Draft_Trimex", "Trimex"),77'ToolTip': QT_TRANSLATE_NOOP("Draft_Trimex",78"Trims or extends the selected object, or extrudes single"79+ " faces.\nCTRL snaps, SHIFT constrains to current segment"80+ " or to normal, ALT inverts.")}81
82def Activated(self):83"""Execute when the command is called."""84super().Activated(name="Trimex")85self.edges = []86self.placement = None87self.ghost = []88self.linetrack = None89self.color = None90self.width = None91if self.ui:92if not Gui.Selection.getSelection():93self.ui.selectUi(on_close_call=self.finish)94_msg(translate("draft", "Select objects to trim or extend"))95self.call = \96self.view.addEventCallback("SoEvent",97gui_tool_utils.selectObject)98else:99self.proceed()100
101def proceed(self):102"""Proceed with execution of the command after proper selection."""103if self.call:104self.view.removeEventCallback("SoEvent", self.call)105sel = Gui.Selection.getSelection()106if len(sel) == 2:107self.trimObjects(sel)108self.finish()109return110self.obj = sel[0]111self.ui.trimUi(title=translate("draft",self.featureName))112self.linetrack = trackers.lineTracker()113
114import DraftGeomUtils115import Part116
117if "Shape" not in self.obj.PropertiesList:118return119if "Placement" in self.obj.PropertiesList:120self.placement = self.obj.Placement121if len(self.obj.Shape.Faces) == 1:122# simple extrude mode, the object itself is extruded123self.extrudeMode = True124self.ghost = [trackers.ghostTracker([self.obj])]125self.normal = self.obj.Shape.Faces[0].normalAt(0.5, 0.5)126self.ghost += [trackers.lineTracker() for _ in self.obj.Shape.Vertexes]127elif len(self.obj.Shape.Faces) > 1:128# face extrude mode, a new object is created129ss = Gui.Selection.getSelectionEx()[0]130if len(ss.SubObjects) == 1:131if ss.SubObjects[0].ShapeType == "Face":132self.obj = self.doc.addObject("Part::Feature", "Face")133self.obj.Shape = ss.SubObjects[0]134self.extrudeMode = True135self.ghost = [trackers.ghostTracker([self.obj])]136self.normal = self.obj.Shape.Faces[0].normalAt(0.5, 0.5)137self.ghost += [trackers.lineTracker() for _ in self.obj.Shape.Vertexes]138else:139# normal wire trimex mode140self.color = self.obj.ViewObject.LineColor141self.width = self.obj.ViewObject.LineWidth142# self.obj.ViewObject.Visibility = False143self.obj.ViewObject.LineColor = (0.5, 0.5, 0.5)144self.obj.ViewObject.LineWidth = 1145self.extrudeMode = False146if self.obj.Shape.Wires:147self.edges = self.obj.Shape.Wires[0].Edges148self.edges = Part.__sortEdges__(self.edges)149else:150self.edges = self.obj.Shape.Edges151self.ghost = []152lc = self.color153sc = (lc[0], lc[1], lc[2])154sw = self.width155for e in self.edges:156if DraftGeomUtils.geomType(e) == "Line":157self.ghost.append(trackers.lineTracker(scolor=sc,158swidth=sw))159else:160self.ghost.append(trackers.arcTracker(scolor=sc,161swidth=sw))162if not self.ghost:163self.finish()164for g in self.ghost:165g.on()166self.activePoint = 0167self.nodes = []168self.shift = False169self.alt = False170self.force = None171self.cv = None172self.call = self.view.addEventCallback("SoEvent", self.action)173_toolmsg(translate("draft", "Pick distance"))174
175def action(self, arg):176"""Handle the 3D scene events.177
178This is installed as an EventCallback in the Inventor view.
179
180Parameters
181----------
182arg: dict
183Dictionary with strings that indicates the type of event received
184from the 3D view.
185"""
186if arg["Type"] == "SoKeyboardEvent":187if arg["Key"] == "ESCAPE":188self.finish()189elif arg["Type"] == "SoLocation2Event": # mouse movement detection190self.shift = gui_tool_utils.hasMod(arg, gui_tool_utils.get_mod_constrain_key())191self.alt = gui_tool_utils.hasMod(arg, gui_tool_utils.get_mod_alt_key())192self.ctrl = gui_tool_utils.hasMod(arg, gui_tool_utils.get_mod_snap_key())193if self.extrudeMode:194arg["ShiftDown"] = False195elif hasattr(Gui, "Snapper"):196Gui.Snapper.setSelectMode(not self.ctrl)197self.point, cp, info = gui_tool_utils.getPoint(self, arg)198if gui_tool_utils.hasMod(arg, gui_tool_utils.get_mod_snap_key()):199self.snapped = None200else:201self.snapped = self.view.getObjectInfo((arg["Position"][0],202arg["Position"][1]))203if self.extrudeMode:204dist, ang = (self.extrude(self.shift), None)205else:206# If the geomType of the edge is "Line" ang will be None,207# else dist will be None.208dist, ang = self.redraw(self.point, self.snapped,209self.shift, self.alt)210
211if dist:212self.ui.labelRadius.setText(translate("draft", "Distance"))213self.ui.radiusValue.setToolTip(translate("draft",214"Offset distance"))215self.ui.setRadiusValue(dist, unit="Length")216else:217self.ui.labelRadius.setText(translate("draft", "Angle"))218self.ui.radiusValue.setToolTip(translate("draft",219"Offset angle"))220self.ui.setRadiusValue(ang, unit="Angle")221self.ui.radiusValue.setFocus()222self.ui.radiusValue.selectAll()223gui_tool_utils.redraw3DView()224
225elif arg["Type"] == "SoMouseButtonEvent":226if (arg["State"] == "DOWN") and (arg["Button"] == "BUTTON1"):227cursor = arg["Position"]228self.shift = gui_tool_utils.hasMod(arg, gui_tool_utils.get_mod_constrain_key())229self.alt = gui_tool_utils.hasMod(arg, gui_tool_utils.get_mod_alt_key())230if gui_tool_utils.hasMod(arg, gui_tool_utils.get_mod_snap_key()):231self.snapped = None232else:233self.snapped = self.view.getObjectInfo((cursor[0],234cursor[1]))235self.trimObject()236self.finish()237
238def extrude(self, shift=False, real=False):239"""Redraw the ghost in extrude mode."""240self.newpoint = self.obj.Shape.Faces[0].CenterOfMass241dvec = self.point.sub(self.newpoint)242if not shift:243delta = DraftVecUtils.project(dvec, self.normal)244else:245delta = dvec246if self.force and delta.Length:247ratio = self.force/delta.Length248delta.multiply(ratio)249if real:250return delta251self.ghost[0].trans.translation.setValue([delta.x, delta.y, delta.z])252for i in range(1, len(self.ghost)):253base = self.obj.Shape.Vertexes[i-1].Point254self.ghost[i].p1(base)255self.ghost[i].p2(base.add(delta))256return delta.Length257
258def redraw(self, point, snapped=None, shift=False, alt=False, real=None):259"""Redraw the ghost normally."""260# initializing261reverse = False262for g in self.ghost:263g.off()264if real:265newedges = []266
267import DraftGeomUtils268import Part269
270# finding the active point271vlist = []272for e in self.edges:273vlist.append(e.Vertexes[0].Point)274vlist.append(self.edges[-1].Vertexes[-1].Point)275if shift:276npoint = self.activePoint277else:278npoint = DraftGeomUtils.findClosest(point, vlist)279if npoint > len(self.edges)/2:280reverse = True281if alt:282reverse = not reverse283self.activePoint = npoint284
285# sorting out directions286if reverse and (npoint > 0):287npoint = npoint - 1288if (npoint > len(self.edges) - 1):289edge = self.edges[-1]290ghost = self.ghost[-1]291else:292edge = self.edges[npoint]293ghost = self.ghost[npoint]294if reverse:295v1 = edge.Vertexes[-1].Point296v2 = edge.Vertexes[0].Point297else:298v1 = edge.Vertexes[0].Point299v2 = edge.Vertexes[-1].Point300
301# snapping302if snapped:303snapped = self.doc.getObject(snapped['Object'])304if hasattr(snapped, "Shape"):305pts = []306for e in snapped.Shape.Edges:307int = DraftGeomUtils.findIntersection(edge, e, True, True)308if int:309pts.extend(int)310if pts:311point = pts[DraftGeomUtils.findClosest(point, pts)]312
313# modifying active edge314if DraftGeomUtils.geomType(edge) == "Line":315ang = None316ve = DraftGeomUtils.vec(edge)317chord = v1.sub(point)318n = ve.cross(chord)319if n.Length == 0:320self.newpoint = point321else:322perp = ve.cross(n)323proj = DraftVecUtils.project(chord, perp)324self.newpoint = App.Vector.add(point, proj)325dist = v1.sub(self.newpoint).Length326ghost.p1(self.newpoint)327ghost.p2(v2)328if real:329if self.force:330ray = self.newpoint.sub(v1)331ray.multiply(self.force / ray.Length)332self.newpoint = App.Vector.add(v1, ray)333newedges.append(Part.LineSegment(self.newpoint, v2).toShape())334else:335dist = None336center = edge.Curve.Center337rad = edge.Curve.Radius338ang1 = DraftVecUtils.angle(v2.sub(center))339ang2 = DraftVecUtils.angle(point.sub(center))340_rot_rad = DraftVecUtils.rotate(App.Vector(rad, 0, 0), -ang2)341self.newpoint = App.Vector.add(center, _rot_rad)342ang = math.degrees(-ang2)343# if ang1 > ang2:344# ang1, ang2 = ang2, ang1345# print("last calculated:",346# math.degrees(-ang1),347# math.degrees(-ang2))348ghost.setEndAngle(-ang2)349ghost.setStartAngle(-ang1)350ghost.setCenter(center)351ghost.setRadius(rad)352if real:353if self.force:354angle = math.radians(self.force)355newray = DraftVecUtils.rotate(App.Vector(rad, 0, 0),356-angle)357self.newpoint = App.Vector.add(center, newray)358chord = self.newpoint.sub(v2)359perp = chord.cross(App.Vector(0, 0, 1))360scaledperp = DraftVecUtils.scaleTo(perp, rad)361midpoint = App.Vector.add(center, scaledperp)362_sh = Part.Arc(self.newpoint, midpoint, v2).toShape()363newedges.append(_sh)364ghost.on()365
366# resetting the visible edges367if not reverse:368li = list(range(npoint + 1, len(self.edges)))369else:370li = list(range(npoint - 1, -1, -1))371for i in li:372edge = self.edges[i]373ghost = self.ghost[i]374if DraftGeomUtils.geomType(edge) == "Line":375ghost.p1(edge.Vertexes[0].Point)376ghost.p2(edge.Vertexes[-1].Point)377else:378ang1 = DraftVecUtils.angle(edge.Vertexes[0].Point.sub(center))379ang2 = DraftVecUtils.angle(edge.Vertexes[-1].Point.sub(center))380# if ang1 > ang2:381# ang1, ang2 = ang2, ang1382ghost.setEndAngle(-ang2)383ghost.setStartAngle(-ang1)384ghost.setCenter(edge.Curve.Center)385ghost.setRadius(edge.Curve.Radius)386if real:387newedges.append(edge)388ghost.on()389
390# finishing391if real:392return newedges393else:394return [dist, ang]395
396def trimObject(self):397"""Trim the actual object."""398import Part399
400if self.extrudeMode:401delta = self.extrude(self.shift, real=True)402# print("delta", delta)403self.doc.openTransaction("Extrude")404Gui.addModule("Draft")405obj = Draft.extrude(self.obj, delta, solid=True)406self.doc.commitTransaction()407self.obj = obj408else:409edges = self.redraw(self.point, self.snapped,410self.shift, self.alt, real=True)411newshape = Part.Wire(edges)412self.doc.openTransaction("Trim/extend")413if utils.getType(self.obj) in ["Wire", "BSpline"]:414p = []415if self.placement:416invpl = self.placement.inverse()417for v in newshape.Vertexes:418np = v.Point419if self.placement:420np = invpl.multVec(np)421p.append(np)422self.obj.Points = p423elif utils.getType(self.obj) == "Part::Line":424p = []425if self.placement:426invpl = self.placement.inverse()427for v in newshape.Vertexes:428np = v.Point429if self.placement:430np = invpl.multVec(np)431p.append(np)432if ((p[0].x == self.obj.X1)433and (p[0].y == self.obj.Y1)434and (p[0].z == self.obj.Z1)):435self.obj.X2 = p[-1].x436self.obj.Y2 = p[-1].y437self.obj.Z2 = p[-1].z438elif ((p[-1].x == self.obj.X1)439and (p[-1].y == self.obj.Y1)440and (p[-1].z == self.obj.Z1)):441self.obj.X2 = p[0].x442self.obj.Y2 = p[0].y443self.obj.Z2 = p[0].z444elif ((p[0].x == self.obj.X2)445and (p[0].y == self.obj.Y2)446and (p[0].z == self.obj.Z2)):447self.obj.X1 = p[-1].x448self.obj.Y1 = p[-1].y449self.obj.Z1 = p[-1].z450else:451self.obj.X1 = p[0].x452self.obj.Y1 = p[0].y453self.obj.Z1 = p[0].z454elif utils.getType(self.obj) == "Circle":455angles = self.ghost[0].getAngles()456# print("original", self.obj.FirstAngle," ",self.obj.LastAngle)457# print("new", angles)458if angles[0] > angles[1]:459angles = (angles[1], angles[0])460self.obj.FirstAngle = angles[0]461self.obj.LastAngle = angles[1]462else:463self.obj.Shape = newshape464self.doc.commitTransaction()465self.doc.recompute()466for g in self.ghost:467g.off()468
469def trimObjects(self, objectslist):470"""Attempt to trim two objects together."""471import Part472import DraftGeomUtils473
474wires = []475for obj in objectslist:476if not utils.getType(obj) in ["Wire", "Circle"]:477_err(translate("draft",478"Unable to trim these objects, "479"only Draft wires and arcs are supported."))480return481if len(obj.Shape.Wires) > 1:482_err(translate("draft",483"Unable to trim these objects, "484"too many wires"))485return486if len(obj.Shape.Wires) == 1:487wires.append(obj.Shape.Wires[0])488else:489wires.append(Part.Wire(obj.Shape.Edges))490ints = []491edge1 = None492edge2 = None493for i1, e1 in enumerate(wires[0].Edges):494for i2, e2 in enumerate(wires[1].Edges):495i = DraftGeomUtils.findIntersection(e1, e2, dts=False)496if len(i) == 1:497ints.append(i[0])498edge1 = i1499edge2 = i2500if not ints:501_err(translate("draft", "These objects don't intersect."))502return503if len(ints) != 1:504_err(translate("draft", "Too many intersection points."))505return506
507v11 = wires[0].Vertexes[0].Point508v12 = wires[0].Vertexes[-1].Point509v21 = wires[1].Vertexes[0].Point510v22 = wires[1].Vertexes[-1].Point511if DraftVecUtils.closest(ints[0], [v11, v12]) == 1:512last1 = True513else:514last1 = False515if DraftVecUtils.closest(ints[0], [v21, v22]) == 1:516last2 = True517else:518last2 = False519for i, obj in enumerate(objectslist):520if i == 0:521ed = edge1522la = last1523else:524ed = edge2525la = last2526if utils.getType(obj) == "Wire":527if la:528pts = obj.Points[:ed + 1] + ints529else:530pts = ints + obj.Points[ed + 1:]531obj.Points = pts532else:533vec = ints[0].sub(obj.Placement.Base)534vec = obj.Placement.inverse().Rotation.multVec(vec)535_x = App.Vector(1, 0, 0)536_ang = -DraftVecUtils.angle(vec,537obj.Placement.Rotation.multVec(_x),538obj.Shape.Edges[0].Curve.Axis)539ang = math.degrees(_ang)540if la:541obj.LastAngle = ang542else:543obj.FirstAngle = ang544self.doc.recompute()545
546def finish(self, cont=False):547"""Terminate the operation of the Trimex tool."""548self.end_callbacks(self.call)549self.force = None550if self.ui:551if self.linetrack:552self.linetrack.finalize()553if self.ghost:554for g in self.ghost:555g.finalize()556if self.obj:557self.obj.ViewObject.Visibility = True558if self.color:559self.obj.ViewObject.LineColor = self.color560if self.width:561self.obj.ViewObject.LineWidth = self.width562gui_utils.select(self.obj)563super().finish()564
565def numericRadius(self, dist):566"""Validate the entry fields in the user interface.567
568This function is called by the toolbar or taskpanel interface
569when valid x, y, and z have been entered in the input fields.
570"""
571self.force = dist572self.trimObject()573self.finish()574
575
576Gui.addCommand('Draft_Trimex', Trimex())577
578## @}
579