FreeCAD

Форк
0
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

27
It also extends closed faces to create solids, that is, it can be used
28
to extrude a closed profile.
29

30
Make sure the snapping is active so that the extrusion is done following
31
the direction of a line, and up to the distance specified
32
by 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
# @{
40
import math
41
from PySide.QtCore import QT_TRANSLATE_NOOP
42

43
import FreeCAD as App
44
import FreeCADGui as Gui
45
import Draft
46
import Draft_rc
47
import DraftVecUtils
48
import draftutils.utils as utils
49
import draftutils.gui_utils as gui_utils
50
import draftguitools.gui_base_original as gui_base_original
51
import draftguitools.gui_tool_utils as gui_tool_utils
52
import draftguitools.gui_trackers as trackers
53

54
from draftutils.messages import _msg, _err, _toolmsg
55
from draftutils.translate import translate
56

57
# The module is used to prevent complaints from code checkers (flake8)
58
True if Draft_rc.__name__ else False
59

60

61
class Trimex(gui_base_original.Modifier):
62
    """Gui Command for the Trimex tool.
63

64
    This tool trims or extends lines, wires and arcs,
65
    or extrudes single faces.
66

67
    SHIFT constrains to the last point
68
    or extrudes in direction to the face normal.
69
    """
70

71
    def GetResources(self):
72
        """Set icon, menu and tooltip."""
73

74
        return {'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

82
    def Activated(self):
83
        """Execute when the command is called."""
84
        super().Activated(name="Trimex")
85
        self.edges = []
86
        self.placement = None
87
        self.ghost = []
88
        self.linetrack = None
89
        self.color = None
90
        self.width = None
91
        if self.ui:
92
            if not Gui.Selection.getSelection():
93
                self.ui.selectUi(on_close_call=self.finish)
94
                _msg(translate("draft", "Select objects to trim or extend"))
95
                self.call = \
96
                    self.view.addEventCallback("SoEvent",
97
                                               gui_tool_utils.selectObject)
98
            else:
99
                self.proceed()
100

101
    def proceed(self):
102
        """Proceed with execution of the command after proper selection."""
103
        if self.call:
104
            self.view.removeEventCallback("SoEvent", self.call)
105
        sel = Gui.Selection.getSelection()
106
        if len(sel) == 2:
107
            self.trimObjects(sel)
108
            self.finish()
109
            return
110
        self.obj = sel[0]
111
        self.ui.trimUi(title=translate("draft",self.featureName))
112
        self.linetrack = trackers.lineTracker()
113

114
        import DraftGeomUtils
115
        import Part
116

117
        if "Shape" not in self.obj.PropertiesList:
118
            return
119
        if "Placement" in self.obj.PropertiesList:
120
            self.placement = self.obj.Placement
121
        if len(self.obj.Shape.Faces) == 1:
122
            # simple extrude mode, the object itself is extruded
123
            self.extrudeMode = True
124
            self.ghost = [trackers.ghostTracker([self.obj])]
125
            self.normal = self.obj.Shape.Faces[0].normalAt(0.5, 0.5)
126
            self.ghost += [trackers.lineTracker() for _ in self.obj.Shape.Vertexes]
127
        elif len(self.obj.Shape.Faces) > 1:
128
            # face extrude mode, a new object is created
129
            ss = Gui.Selection.getSelectionEx()[0]
130
            if len(ss.SubObjects) == 1:
131
                if ss.SubObjects[0].ShapeType == "Face":
132
                    self.obj = self.doc.addObject("Part::Feature", "Face")
133
                    self.obj.Shape = ss.SubObjects[0]
134
                    self.extrudeMode = True
135
                    self.ghost = [trackers.ghostTracker([self.obj])]
136
                    self.normal = self.obj.Shape.Faces[0].normalAt(0.5, 0.5)
137
                    self.ghost += [trackers.lineTracker() for _ in self.obj.Shape.Vertexes]
138
        else:
139
            # normal wire trimex mode
140
            self.color = self.obj.ViewObject.LineColor
141
            self.width = self.obj.ViewObject.LineWidth
142
            # self.obj.ViewObject.Visibility = False
143
            self.obj.ViewObject.LineColor = (0.5, 0.5, 0.5)
144
            self.obj.ViewObject.LineWidth = 1
145
            self.extrudeMode = False
146
            if self.obj.Shape.Wires:
147
                self.edges = self.obj.Shape.Wires[0].Edges
148
                self.edges = Part.__sortEdges__(self.edges)
149
            else:
150
                self.edges = self.obj.Shape.Edges
151
            self.ghost = []
152
            lc = self.color
153
            sc = (lc[0], lc[1], lc[2])
154
            sw = self.width
155
            for e in self.edges:
156
                if DraftGeomUtils.geomType(e) == "Line":
157
                    self.ghost.append(trackers.lineTracker(scolor=sc,
158
                                                           swidth=sw))
159
                else:
160
                    self.ghost.append(trackers.arcTracker(scolor=sc,
161
                                                          swidth=sw))
162
        if not self.ghost:
163
            self.finish()
164
        for g in self.ghost:
165
            g.on()
166
        self.activePoint = 0
167
        self.nodes = []
168
        self.shift = False
169
        self.alt = False
170
        self.force = None
171
        self.cv = None
172
        self.call = self.view.addEventCallback("SoEvent", self.action)
173
        _toolmsg(translate("draft", "Pick distance"))
174

175
    def action(self, arg):
176
        """Handle the 3D scene events.
177

178
        This is installed as an EventCallback in the Inventor view.
179

180
        Parameters
181
        ----------
182
        arg: dict
183
            Dictionary with strings that indicates the type of event received
184
            from the 3D view.
185
        """
186
        if arg["Type"] == "SoKeyboardEvent":
187
            if arg["Key"] == "ESCAPE":
188
                self.finish()
189
        elif arg["Type"] == "SoLocation2Event":  # mouse movement detection
190
            self.shift = gui_tool_utils.hasMod(arg, gui_tool_utils.get_mod_constrain_key())
191
            self.alt = gui_tool_utils.hasMod(arg, gui_tool_utils.get_mod_alt_key())
192
            self.ctrl = gui_tool_utils.hasMod(arg, gui_tool_utils.get_mod_snap_key())
193
            if self.extrudeMode:
194
                arg["ShiftDown"] = False
195
            elif hasattr(Gui, "Snapper"):
196
                Gui.Snapper.setSelectMode(not self.ctrl)
197
            self.point, cp, info = gui_tool_utils.getPoint(self, arg)
198
            if gui_tool_utils.hasMod(arg, gui_tool_utils.get_mod_snap_key()):
199
                self.snapped = None
200
            else:
201
                self.snapped = self.view.getObjectInfo((arg["Position"][0],
202
                                                        arg["Position"][1]))
203
            if self.extrudeMode:
204
                dist, ang = (self.extrude(self.shift), None)
205
            else:
206
                # If the geomType of the edge is "Line" ang will be None,
207
                # else dist will be None.
208
                dist, ang = self.redraw(self.point, self.snapped,
209
                                        self.shift, self.alt)
210

211
            if dist:
212
                self.ui.labelRadius.setText(translate("draft", "Distance"))
213
                self.ui.radiusValue.setToolTip(translate("draft",
214
                                                         "Offset distance"))
215
                self.ui.setRadiusValue(dist, unit="Length")
216
            else:
217
                self.ui.labelRadius.setText(translate("draft", "Angle"))
218
                self.ui.radiusValue.setToolTip(translate("draft",
219
                                                         "Offset angle"))
220
                self.ui.setRadiusValue(ang, unit="Angle")
221
            self.ui.radiusValue.setFocus()
222
            self.ui.radiusValue.selectAll()
223
            gui_tool_utils.redraw3DView()
224

225
        elif arg["Type"] == "SoMouseButtonEvent":
226
            if (arg["State"] == "DOWN") and (arg["Button"] == "BUTTON1"):
227
                cursor = arg["Position"]
228
                self.shift = gui_tool_utils.hasMod(arg, gui_tool_utils.get_mod_constrain_key())
229
                self.alt = gui_tool_utils.hasMod(arg, gui_tool_utils.get_mod_alt_key())
230
                if gui_tool_utils.hasMod(arg, gui_tool_utils.get_mod_snap_key()):
231
                    self.snapped = None
232
                else:
233
                    self.snapped = self.view.getObjectInfo((cursor[0],
234
                                                            cursor[1]))
235
                self.trimObject()
236
                self.finish()
237

238
    def extrude(self, shift=False, real=False):
239
        """Redraw the ghost in extrude mode."""
240
        self.newpoint = self.obj.Shape.Faces[0].CenterOfMass
241
        dvec = self.point.sub(self.newpoint)
242
        if not shift:
243
            delta = DraftVecUtils.project(dvec, self.normal)
244
        else:
245
            delta = dvec
246
        if self.force and delta.Length:
247
            ratio = self.force/delta.Length
248
            delta.multiply(ratio)
249
        if real:
250
            return delta
251
        self.ghost[0].trans.translation.setValue([delta.x, delta.y, delta.z])
252
        for i in range(1, len(self.ghost)):
253
            base = self.obj.Shape.Vertexes[i-1].Point
254
            self.ghost[i].p1(base)
255
            self.ghost[i].p2(base.add(delta))
256
        return delta.Length
257

258
    def redraw(self, point, snapped=None, shift=False, alt=False, real=None):
259
        """Redraw the ghost normally."""
260
        # initializing
261
        reverse = False
262
        for g in self.ghost:
263
            g.off()
264
        if real:
265
            newedges = []
266

267
        import DraftGeomUtils
268
        import Part
269

270
        # finding the active point
271
        vlist = []
272
        for e in self.edges:
273
            vlist.append(e.Vertexes[0].Point)
274
        vlist.append(self.edges[-1].Vertexes[-1].Point)
275
        if shift:
276
            npoint = self.activePoint
277
        else:
278
            npoint = DraftGeomUtils.findClosest(point, vlist)
279
        if npoint > len(self.edges)/2:
280
            reverse = True
281
        if alt:
282
            reverse = not reverse
283
        self.activePoint = npoint
284

285
        # sorting out directions
286
        if reverse and (npoint > 0):
287
            npoint = npoint - 1
288
        if (npoint > len(self.edges) - 1):
289
            edge = self.edges[-1]
290
            ghost = self.ghost[-1]
291
        else:
292
            edge = self.edges[npoint]
293
            ghost = self.ghost[npoint]
294
        if reverse:
295
            v1 = edge.Vertexes[-1].Point
296
            v2 = edge.Vertexes[0].Point
297
        else:
298
            v1 = edge.Vertexes[0].Point
299
            v2 = edge.Vertexes[-1].Point
300

301
        # snapping
302
        if snapped:
303
            snapped = self.doc.getObject(snapped['Object'])
304
            if hasattr(snapped, "Shape"):
305
                pts = []
306
                for e in snapped.Shape.Edges:
307
                    int = DraftGeomUtils.findIntersection(edge, e, True, True)
308
                    if int:
309
                        pts.extend(int)
310
                if pts:
311
                    point = pts[DraftGeomUtils.findClosest(point, pts)]
312

313
        # modifying active edge
314
        if DraftGeomUtils.geomType(edge) == "Line":
315
            ang = None
316
            ve = DraftGeomUtils.vec(edge)
317
            chord = v1.sub(point)
318
            n = ve.cross(chord)
319
            if n.Length == 0:
320
                self.newpoint = point
321
            else:
322
                perp = ve.cross(n)
323
                proj = DraftVecUtils.project(chord, perp)
324
                self.newpoint = App.Vector.add(point, proj)
325
            dist = v1.sub(self.newpoint).Length
326
            ghost.p1(self.newpoint)
327
            ghost.p2(v2)
328
            if real:
329
                if self.force:
330
                    ray = self.newpoint.sub(v1)
331
                    ray.multiply(self.force / ray.Length)
332
                    self.newpoint = App.Vector.add(v1, ray)
333
                newedges.append(Part.LineSegment(self.newpoint, v2).toShape())
334
        else:
335
            dist = None
336
            center = edge.Curve.Center
337
            rad = edge.Curve.Radius
338
            ang1 = DraftVecUtils.angle(v2.sub(center))
339
            ang2 = DraftVecUtils.angle(point.sub(center))
340
            _rot_rad = DraftVecUtils.rotate(App.Vector(rad, 0, 0), -ang2)
341
            self.newpoint = App.Vector.add(center, _rot_rad)
342
            ang = math.degrees(-ang2)
343
            # if ang1 > ang2:
344
            #     ang1, ang2 = ang2, ang1
345
            # print("last calculated:",
346
            #       math.degrees(-ang1),
347
            #       math.degrees(-ang2))
348
            ghost.setEndAngle(-ang2)
349
            ghost.setStartAngle(-ang1)
350
            ghost.setCenter(center)
351
            ghost.setRadius(rad)
352
            if real:
353
                if self.force:
354
                    angle = math.radians(self.force)
355
                    newray = DraftVecUtils.rotate(App.Vector(rad, 0, 0),
356
                                                  -angle)
357
                    self.newpoint = App.Vector.add(center, newray)
358
                chord = self.newpoint.sub(v2)
359
                perp = chord.cross(App.Vector(0, 0, 1))
360
                scaledperp = DraftVecUtils.scaleTo(perp, rad)
361
                midpoint = App.Vector.add(center, scaledperp)
362
                _sh = Part.Arc(self.newpoint, midpoint, v2).toShape()
363
                newedges.append(_sh)
364
        ghost.on()
365

366
        # resetting the visible edges
367
        if not reverse:
368
            li = list(range(npoint + 1, len(self.edges)))
369
        else:
370
            li = list(range(npoint - 1, -1, -1))
371
        for i in li:
372
            edge = self.edges[i]
373
            ghost = self.ghost[i]
374
            if DraftGeomUtils.geomType(edge) == "Line":
375
                ghost.p1(edge.Vertexes[0].Point)
376
                ghost.p2(edge.Vertexes[-1].Point)
377
            else:
378
                ang1 = DraftVecUtils.angle(edge.Vertexes[0].Point.sub(center))
379
                ang2 = DraftVecUtils.angle(edge.Vertexes[-1].Point.sub(center))
380
                # if ang1 > ang2:
381
                #     ang1, ang2 = ang2, ang1
382
                ghost.setEndAngle(-ang2)
383
                ghost.setStartAngle(-ang1)
384
                ghost.setCenter(edge.Curve.Center)
385
                ghost.setRadius(edge.Curve.Radius)
386
            if real:
387
                newedges.append(edge)
388
            ghost.on()
389

390
        # finishing
391
        if real:
392
            return newedges
393
        else:
394
            return [dist, ang]
395

396
    def trimObject(self):
397
        """Trim the actual object."""
398
        import Part
399

400
        if self.extrudeMode:
401
            delta = self.extrude(self.shift, real=True)
402
            # print("delta", delta)
403
            self.doc.openTransaction("Extrude")
404
            Gui.addModule("Draft")
405
            obj = Draft.extrude(self.obj, delta, solid=True)
406
            self.doc.commitTransaction()
407
            self.obj = obj
408
        else:
409
            edges = self.redraw(self.point, self.snapped,
410
                                self.shift, self.alt, real=True)
411
            newshape = Part.Wire(edges)
412
            self.doc.openTransaction("Trim/extend")
413
            if utils.getType(self.obj) in ["Wire", "BSpline"]:
414
                p = []
415
                if self.placement:
416
                    invpl = self.placement.inverse()
417
                for v in newshape.Vertexes:
418
                    np = v.Point
419
                    if self.placement:
420
                        np = invpl.multVec(np)
421
                    p.append(np)
422
                self.obj.Points = p
423
            elif utils.getType(self.obj) == "Part::Line":
424
                p = []
425
                if self.placement:
426
                    invpl = self.placement.inverse()
427
                for v in newshape.Vertexes:
428
                    np = v.Point
429
                    if self.placement:
430
                        np = invpl.multVec(np)
431
                    p.append(np)
432
                if ((p[0].x == self.obj.X1)
433
                        and (p[0].y == self.obj.Y1)
434
                        and (p[0].z == self.obj.Z1)):
435
                    self.obj.X2 = p[-1].x
436
                    self.obj.Y2 = p[-1].y
437
                    self.obj.Z2 = p[-1].z
438
                elif ((p[-1].x == self.obj.X1)
439
                      and (p[-1].y == self.obj.Y1)
440
                      and (p[-1].z == self.obj.Z1)):
441
                    self.obj.X2 = p[0].x
442
                    self.obj.Y2 = p[0].y
443
                    self.obj.Z2 = p[0].z
444
                elif ((p[0].x == self.obj.X2)
445
                      and (p[0].y == self.obj.Y2)
446
                      and (p[0].z == self.obj.Z2)):
447
                    self.obj.X1 = p[-1].x
448
                    self.obj.Y1 = p[-1].y
449
                    self.obj.Z1 = p[-1].z
450
                else:
451
                    self.obj.X1 = p[0].x
452
                    self.obj.Y1 = p[0].y
453
                    self.obj.Z1 = p[0].z
454
            elif utils.getType(self.obj) == "Circle":
455
                angles = self.ghost[0].getAngles()
456
                # print("original", self.obj.FirstAngle," ",self.obj.LastAngle)
457
                # print("new", angles)
458
                if angles[0] > angles[1]:
459
                    angles = (angles[1], angles[0])
460
                self.obj.FirstAngle = angles[0]
461
                self.obj.LastAngle = angles[1]
462
            else:
463
                self.obj.Shape = newshape
464
            self.doc.commitTransaction()
465
        self.doc.recompute()
466
        for g in self.ghost:
467
            g.off()
468

469
    def trimObjects(self, objectslist):
470
        """Attempt to trim two objects together."""
471
        import Part
472
        import DraftGeomUtils
473

474
        wires = []
475
        for obj in objectslist:
476
            if 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."))
480
                return
481
            if len(obj.Shape.Wires) > 1:
482
                _err(translate("draft",
483
                               "Unable to trim these objects, "
484
                               "too many wires"))
485
                return
486
            if len(obj.Shape.Wires) == 1:
487
                wires.append(obj.Shape.Wires[0])
488
            else:
489
                wires.append(Part.Wire(obj.Shape.Edges))
490
        ints = []
491
        edge1 = None
492
        edge2 = None
493
        for i1, e1 in enumerate(wires[0].Edges):
494
            for i2, e2 in enumerate(wires[1].Edges):
495
                i = DraftGeomUtils.findIntersection(e1, e2, dts=False)
496
                if len(i) == 1:
497
                    ints.append(i[0])
498
                    edge1 = i1
499
                    edge2 = i2
500
        if not ints:
501
            _err(translate("draft", "These objects don't intersect."))
502
            return
503
        if len(ints) != 1:
504
            _err(translate("draft", "Too many intersection points."))
505
            return
506

507
        v11 = wires[0].Vertexes[0].Point
508
        v12 = wires[0].Vertexes[-1].Point
509
        v21 = wires[1].Vertexes[0].Point
510
        v22 = wires[1].Vertexes[-1].Point
511
        if DraftVecUtils.closest(ints[0], [v11, v12]) == 1:
512
            last1 = True
513
        else:
514
            last1 = False
515
        if DraftVecUtils.closest(ints[0], [v21, v22]) == 1:
516
            last2 = True
517
        else:
518
            last2 = False
519
        for i, obj in enumerate(objectslist):
520
            if i == 0:
521
                ed = edge1
522
                la = last1
523
            else:
524
                ed = edge2
525
                la = last2
526
            if utils.getType(obj) == "Wire":
527
                if la:
528
                    pts = obj.Points[:ed + 1] + ints
529
                else:
530
                    pts = ints + obj.Points[ed + 1:]
531
                obj.Points = pts
532
            else:
533
                vec = ints[0].sub(obj.Placement.Base)
534
                vec = obj.Placement.inverse().Rotation.multVec(vec)
535
                _x = App.Vector(1, 0, 0)
536
                _ang = -DraftVecUtils.angle(vec,
537
                                            obj.Placement.Rotation.multVec(_x),
538
                                            obj.Shape.Edges[0].Curve.Axis)
539
                ang = math.degrees(_ang)
540
                if la:
541
                    obj.LastAngle = ang
542
                else:
543
                    obj.FirstAngle = ang
544
        self.doc.recompute()
545

546
    def finish(self, cont=False):
547
        """Terminate the operation of the Trimex tool."""
548
        self.end_callbacks(self.call)
549
        self.force = None
550
        if self.ui:
551
            if self.linetrack:
552
                self.linetrack.finalize()
553
            if self.ghost:
554
                for g in self.ghost:
555
                    g.finalize()
556
            if self.obj:
557
                self.obj.ViewObject.Visibility = True
558
                if self.color:
559
                    self.obj.ViewObject.LineColor = self.color
560
                if self.width:
561
                    self.obj.ViewObject.LineWidth = self.width
562
                gui_utils.select(self.obj)
563
        super().finish()
564

565
    def numericRadius(self, dist):
566
        """Validate the entry fields in the user interface.
567

568
        This function is called by the toolbar or taskpanel interface
569
        when valid x, y, and z have been entered in the input fields.
570
        """
571
        self.force = dist
572
        self.trimObject()
573
        self.finish()
574

575

576
Gui.addCommand('Draft_Trimex', Trimex())
577

578
## @}
579

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.