freecad-cam/Mod/Draft/draftguitools/gui_trimex.py
2026-02-01 01:59:24 +01:00

600 lines
24 KiB
Python

# ***************************************************************************
# * (c) 2009, 2010 Yorik van Havre <yorik@uncreated.net> *
# * (c) 2009, 2010 Ken Cline <cline@frii.com> *
# * (c) 2020 Eliud Cabrera Castillo <e.cabrera-castillo@tum.de> *
# * *
# * 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 GUI tools to trim and extend lines.
It also extends closed faces to create solids, that is, it can be used
to extrude a closed profile.
Make sure the snapping is active so that the extrusion is done following
the direction of a line, and up to the distance specified
by the snapping point.
"""
## @package gui_trimex
# \ingroup draftguitools
# \brief Provides GUI tools to trim and extend lines.
## \addtogroup draftguitools
# @{
import math
from PySide.QtCore import QT_TRANSLATE_NOOP
import FreeCAD as App
import FreeCADGui as Gui
import Draft
import Draft_rc
import DraftVecUtils
import draftutils.utils as utils
import draftutils.gui_utils as gui_utils
import draftguitools.gui_base_original as gui_base_original
import draftguitools.gui_tool_utils as gui_tool_utils
import draftguitools.gui_trackers as trackers
from draftutils.messages import _msg, _err, _toolmsg
from draftutils.translate import translate
# The module is used to prevent complaints from code checkers (flake8)
True if Draft_rc.__name__ else False
class Trimex(gui_base_original.Modifier):
"""Gui Command for the Trimex tool.
This tool trims or extends lines, wires and arcs,
or extrudes single faces.
SHIFT constrains to the last point
or extrudes in direction to the face normal.
"""
def GetResources(self):
"""Set icon, menu and tooltip."""
return {'Pixmap': 'Draft_Trimex',
'Accel': "T, R",
'MenuText': QT_TRANSLATE_NOOP("Draft_Trimex", "Trimex"),
'ToolTip': QT_TRANSLATE_NOOP("Draft_Trimex",
"Trims or extends the selected object, or extrudes single"
+ " faces.\nCTRL snaps, SHIFT constrains to current segment"
+ " or to normal, ALT inverts.")}
def Activated(self):
"""Execute when the command is called."""
super().Activated(name="Trimex")
self.edges = []
self.placement = None
self.ghost = []
self.linetrack = None
self.color = None
self.width = None
if self.ui:
if not Gui.Selection.getSelection():
self.ui.selectUi(on_close_call=self.finish)
_msg(translate("draft", "Select objects to trim or extend"))
self.call = \
self.view.addEventCallback("SoEvent",
gui_tool_utils.selectObject)
else:
self.proceed()
def proceed(self):
"""Proceed with execution of the command after proper selection."""
if self.call:
self.view.removeEventCallback("SoEvent", self.call)
sel = Gui.Selection.getSelection()
if len(sel) == 2:
self.trimObjects(sel)
self.finish()
return
self.obj = sel[0]
self.ui.trimUi(title=translate("draft",self.featureName))
self.linetrack = trackers.lineTracker()
import DraftGeomUtils
import Part
if "Shape" not in self.obj.PropertiesList:
self.obj = None
self.finish()
_err(translate("draft", "This object is not supported."))
return
if "Placement" in self.obj.PropertiesList:
self.placement = self.obj.Placement
if len(self.obj.Shape.Faces) == 1:
# simple extrude mode, the object itself is extruded
self.extrudeMode = True
self.ghost = [trackers.ghostTracker([self.obj])]
self.normal = self.obj.Shape.Faces[0].normalAt(0.5, 0.5)
self.ghost += [trackers.lineTracker() for _ in self.obj.Shape.Vertexes]
elif len(self.obj.Shape.Faces) > 1:
# face extrude mode, a new object is created
ss = Gui.Selection.getSelectionEx()[0]
if len(ss.SubObjects) == 1 and ss.SubObjects[0].ShapeType == "Face":
self.obj = self.doc.addObject("Part::Feature", "Face")
self.obj.Shape = ss.SubObjects[0]
self.extrudeMode = True
self.ghost = [trackers.ghostTracker([self.obj])]
self.normal = self.obj.Shape.Faces[0].normalAt(0.5, 0.5)
self.ghost += [trackers.lineTracker() for _ in self.obj.Shape.Vertexes]
else:
self.obj = None
self.finish()
_err(translate("draft", "Only a single face can be extruded."))
return
else:
# normal wire trimex mode
self.color = self.obj.ViewObject.LineColor
self.width = self.obj.ViewObject.LineWidth
if self.obj.Shape.Wires:
self.edges = self.obj.Shape.Wires[0].Edges
self.edges = Part.__sortEdges__(self.edges)
else:
self.edges = self.obj.Shape.Edges
for e in self.edges:
if isinstance(e.Curve,(Part.BSplineCurve, Part.BezierCurve)):
self.obj = None
self.finish()
_err(translate("draft", "Trimex is not supported yet on this type of object."))
return
# self.obj.ViewObject.Visibility = False
self.obj.ViewObject.LineColor = (0.5, 0.5, 0.5)
self.obj.ViewObject.LineWidth = 1
self.extrudeMode = False
self.ghost = []
lc = self.color
sc = (lc[0], lc[1], lc[2])
sw = self.width
for e in self.edges:
if DraftGeomUtils.geomType(e) == "Line":
self.ghost.append(trackers.lineTracker(scolor=sc,
swidth=sw))
else:
self.ghost.append(trackers.arcTracker(scolor=sc,
swidth=sw))
if not self.ghost:
self.obj = None
self.finish()
return
for g in self.ghost:
g.on()
self.activePoint = 0
self.nodes = []
self.shift = False
self.alt = False
self.force = None
self.cv = None
self.call = self.view.addEventCallback("SoEvent", self.action)
_toolmsg(translate("draft", "Pick distance"))
def action(self, arg):
"""Handle the 3D scene events.
This is installed as an EventCallback in the Inventor view.
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.shift = gui_tool_utils.hasMod(arg, gui_tool_utils.get_mod_constrain_key())
self.alt = gui_tool_utils.hasMod(arg, gui_tool_utils.get_mod_alt_key())
self.ctrl = gui_tool_utils.hasMod(arg, gui_tool_utils.get_mod_snap_key())
if self.extrudeMode:
arg["ShiftDown"] = False
elif hasattr(Gui, "Snapper"):
Gui.Snapper.setSelectMode(not self.ctrl)
self.point, cp, info = gui_tool_utils.getPoint(self, arg)
if gui_tool_utils.hasMod(arg, gui_tool_utils.get_mod_snap_key()):
self.snapped = None
else:
self.snapped = self.view.getObjectInfo((arg["Position"][0],
arg["Position"][1]))
if self.extrudeMode:
dist, ang = (self.extrude(self.shift), None)
else:
# If the geomType of the edge is "Line" ang will be None,
# else dist will be None.
dist, ang = self.redraw(self.point, self.snapped,
self.shift, self.alt)
if dist:
self.ui.labelRadius.setText(translate("draft", "Distance"))
self.ui.radiusValue.setToolTip(translate("draft",
"Offset distance"))
self.ui.setRadiusValue(dist, unit="Length")
elif ang:
self.ui.labelRadius.setText(translate("draft", "Angle"))
self.ui.radiusValue.setToolTip(translate("draft",
"Offset angle"))
self.ui.setRadiusValue(ang, unit="Angle")
else:
# both dist and ang are None, this indicates an impossible
# situation. Setting 0 with no unit will show "0 ??" and not
# compute any value
self.ui.setRadiusValue(0)
self.ui.radiusValue.setFocus()
self.ui.radiusValue.selectAll()
gui_tool_utils.redraw3DView()
elif arg["Type"] == "SoMouseButtonEvent":
if (arg["State"] == "DOWN") and (arg["Button"] == "BUTTON1"):
cursor = arg["Position"]
self.shift = gui_tool_utils.hasMod(arg, gui_tool_utils.get_mod_constrain_key())
self.alt = gui_tool_utils.hasMod(arg, gui_tool_utils.get_mod_alt_key())
if gui_tool_utils.hasMod(arg, gui_tool_utils.get_mod_snap_key()):
self.snapped = None
else:
self.snapped = self.view.getObjectInfo((cursor[0],
cursor[1]))
self.trimObject()
self.finish()
def extrude(self, shift=False, real=False):
"""Redraw the ghost in extrude mode."""
self.newpoint = self.obj.Shape.Faces[0].CenterOfMass
dvec = self.point.sub(self.newpoint)
if not shift:
delta = DraftVecUtils.project(dvec, self.normal)
else:
delta = dvec
if self.force and delta.Length:
ratio = self.force/delta.Length
delta.multiply(ratio)
if real:
return delta
self.ghost[0].trans.translation.setValue([delta.x, delta.y, delta.z])
for i in range(1, len(self.ghost)):
base = self.obj.Shape.Vertexes[i-1].Point
self.ghost[i].p1(base)
self.ghost[i].p2(base.add(delta))
return delta.Length
def redraw(self, point, snapped=None, shift=False, alt=False, real=None):
"""Redraw the ghost normally."""
# initializing
reverse = False
for g in self.ghost:
g.off()
if real:
newedges = []
import DraftGeomUtils
import Part
# finding the active point
vlist = []
for e in self.edges:
vlist.append(e.Vertexes[0].Point)
vlist.append(self.edges[-1].Vertexes[-1].Point)
if shift:
npoint = self.activePoint
else:
npoint = DraftGeomUtils.findClosest(point, vlist)
if npoint > len(self.edges)/2:
reverse = True
if alt:
reverse = not reverse
self.activePoint = npoint
# sorting out directions
if reverse and (npoint > 0):
npoint = npoint - 1
if (npoint > len(self.edges) - 1):
edge = self.edges[-1]
ghost = self.ghost[-1]
else:
edge = self.edges[npoint]
ghost = self.ghost[npoint]
if reverse:
v1 = edge.Vertexes[-1].Point
v2 = edge.Vertexes[0].Point
else:
v1 = edge.Vertexes[0].Point
v2 = edge.Vertexes[-1].Point
# snapping
if snapped:
snapped = self.doc.getObject(snapped['Object'])
if hasattr(snapped, "Shape"):
pts = []
for e in snapped.Shape.Edges:
int = DraftGeomUtils.findIntersection(edge, e, True, True)
if int:
pts.extend(int)
if pts:
point = pts[DraftGeomUtils.findClosest(point, pts)]
# modifying active edge
if DraftGeomUtils.geomType(edge) == "Line":
ang = None
ve = DraftGeomUtils.vec(edge)
chord = v1.sub(point)
n = ve.cross(chord)
if n.Length == 0:
self.newpoint = point
else:
perp = ve.cross(n)
proj = DraftVecUtils.project(chord, perp)
self.newpoint = App.Vector.add(point, proj)
dist = v1.sub(self.newpoint).Length
ghost.p1(self.newpoint)
ghost.p2(v2)
if real:
if self.force:
ray = self.newpoint.sub(v1)
if ray.Length:
ray.multiply(self.force / ray.Length)
self.newpoint = App.Vector.add(v1, ray)
newedges.append(Part.LineSegment(self.newpoint, v2).toShape())
else:
dist = None
center = edge.Curve.Center
rad = edge.Curve.Radius
ang1 = DraftVecUtils.angle(v2.sub(center))
ang2 = DraftVecUtils.angle(point.sub(center))
_rot_rad = DraftVecUtils.rotate(App.Vector(rad, 0, 0), -ang2)
self.newpoint = App.Vector.add(center, _rot_rad)
ang = math.degrees(-ang2)
# if ang1 > ang2:
# ang1, ang2 = ang2, ang1
# print("last calculated:",
# math.degrees(-ang1),
# math.degrees(-ang2))
ghost.setEndAngle(-ang2)
ghost.setStartAngle(-ang1)
ghost.setCenter(center)
ghost.setRadius(rad)
if real:
if self.force:
angle = math.radians(self.force)
newray = DraftVecUtils.rotate(App.Vector(rad, 0, 0),
-angle)
self.newpoint = App.Vector.add(center, newray)
chord = self.newpoint.sub(v2)
perp = chord.cross(App.Vector(0, 0, 1))
scaledperp = DraftVecUtils.scaleTo(perp, rad)
midpoint = App.Vector.add(center, scaledperp)
_sh = Part.Arc(self.newpoint, midpoint, v2).toShape()
newedges.append(_sh)
ghost.on()
# resetting the visible edges
if not reverse:
li = list(range(npoint + 1, len(self.edges)))
else:
li = list(range(npoint - 1, -1, -1))
for i in li:
edge = self.edges[i]
ghost = self.ghost[i]
if DraftGeomUtils.geomType(edge) == "Line":
ghost.p1(edge.Vertexes[0].Point)
ghost.p2(edge.Vertexes[-1].Point)
else:
ang1 = DraftVecUtils.angle(edge.Vertexes[0].Point.sub(center))
ang2 = DraftVecUtils.angle(edge.Vertexes[-1].Point.sub(center))
# if ang1 > ang2:
# ang1, ang2 = ang2, ang1
ghost.setEndAngle(-ang2)
ghost.setStartAngle(-ang1)
ghost.setCenter(edge.Curve.Center)
ghost.setRadius(edge.Curve.Radius)
if real:
newedges.append(edge)
ghost.on()
# finishing
if real:
return newedges
else:
return [dist, ang]
def trimObject(self):
"""Trim the actual object."""
import Part
if self.extrudeMode:
delta = self.extrude(self.shift, real=True)
# print("delta", delta)
self.doc.openTransaction("Extrude")
Gui.addModule("Draft")
obj = Draft.extrude(self.obj, delta, solid=True)
self.doc.commitTransaction()
self.obj = obj
else:
edges = self.redraw(self.point, self.snapped,
self.shift, self.alt, real=True)
newshape = Part.Wire(edges)
self.doc.openTransaction("Trim/extend")
if utils.getType(self.obj) in ["Wire", "BSpline"]:
p = []
if self.placement:
invpl = self.placement.inverse()
for v in newshape.Vertexes:
np = v.Point
if self.placement:
np = invpl.multVec(np)
p.append(np)
self.obj.Points = p
elif utils.getType(self.obj) == "Part::Line":
p = []
if self.placement:
invpl = self.placement.inverse()
for v in newshape.Vertexes:
np = v.Point
if self.placement:
np = invpl.multVec(np)
p.append(np)
if ((p[0].x == self.obj.X1)
and (p[0].y == self.obj.Y1)
and (p[0].z == self.obj.Z1)):
self.obj.X2 = p[-1].x
self.obj.Y2 = p[-1].y
self.obj.Z2 = p[-1].z
elif ((p[-1].x == self.obj.X1)
and (p[-1].y == self.obj.Y1)
and (p[-1].z == self.obj.Z1)):
self.obj.X2 = p[0].x
self.obj.Y2 = p[0].y
self.obj.Z2 = p[0].z
elif ((p[0].x == self.obj.X2)
and (p[0].y == self.obj.Y2)
and (p[0].z == self.obj.Z2)):
self.obj.X1 = p[-1].x
self.obj.Y1 = p[-1].y
self.obj.Z1 = p[-1].z
else:
self.obj.X1 = p[0].x
self.obj.Y1 = p[0].y
self.obj.Z1 = p[0].z
elif utils.getType(self.obj) == "Circle":
angles = self.ghost[0].getAngles()
# print("original", self.obj.FirstAngle," ",self.obj.LastAngle)
# print("new", angles)
if angles[0] > angles[1]:
angles = (angles[1], angles[0])
self.obj.FirstAngle = angles[0]
self.obj.LastAngle = angles[1]
else:
self.obj.Shape = newshape
self.doc.commitTransaction()
self.doc.recompute()
for g in self.ghost:
g.off()
def trimObjects(self, objectslist):
"""Attempt to trim two objects together."""
import Part
import DraftGeomUtils
wires = []
for obj in objectslist:
if not utils.getType(obj) in ["Wire", "Circle"]:
_err(translate("draft",
"Unable to trim these objects, "
"only Draft wires and arcs are supported."))
return
if len(obj.Shape.Wires) > 1:
_err(translate("draft",
"Unable to trim these objects, "
"too many wires"))
return
if len(obj.Shape.Wires) == 1:
wires.append(obj.Shape.Wires[0])
else:
wires.append(Part.Wire(obj.Shape.Edges))
ints = []
edge1 = None
edge2 = None
for i1, e1 in enumerate(wires[0].Edges):
for i2, e2 in enumerate(wires[1].Edges):
i = DraftGeomUtils.findIntersection(e1, e2, dts=False)
if len(i) == 1:
ints.append(i[0])
edge1 = i1
edge2 = i2
if not ints:
_err(translate("draft", "These objects don't intersect."))
return
if len(ints) != 1:
_err(translate("draft", "Too many intersection points."))
return
v11 = wires[0].Vertexes[0].Point
v12 = wires[0].Vertexes[-1].Point
v21 = wires[1].Vertexes[0].Point
v22 = wires[1].Vertexes[-1].Point
if DraftVecUtils.closest(ints[0], [v11, v12]) == 1:
last1 = True
else:
last1 = False
if DraftVecUtils.closest(ints[0], [v21, v22]) == 1:
last2 = True
else:
last2 = False
for i, obj in enumerate(objectslist):
if i == 0:
ed = edge1
la = last1
else:
ed = edge2
la = last2
if utils.getType(obj) == "Wire":
if la:
pts = obj.Points[:ed + 1] + ints
else:
pts = ints + obj.Points[ed + 1:]
obj.Points = pts
else:
vec = ints[0].sub(obj.Placement.Base)
vec = obj.Placement.inverse().Rotation.multVec(vec)
_x = App.Vector(1, 0, 0)
_ang = -DraftVecUtils.angle(vec,
obj.Placement.Rotation.multVec(_x),
obj.Shape.Edges[0].Curve.Axis)
ang = math.degrees(_ang)
if la:
obj.LastAngle = ang
else:
obj.FirstAngle = ang
self.doc.recompute()
def finish(self, cont=False):
"""Terminate the operation of the Trimex tool."""
self.end_callbacks(self.call)
self.force = None
if self.ui:
if self.linetrack:
self.linetrack.finalize()
if self.ghost:
for g in self.ghost:
g.finalize()
if self.obj:
self.obj.ViewObject.Visibility = True
if self.color:
self.obj.ViewObject.LineColor = self.color
if self.width:
self.obj.ViewObject.LineWidth = self.width
gui_utils.select(self.obj)
super().finish()
def numericRadius(self, dist):
"""Validate the entry fields in the user interface.
This function is called by the toolbar or taskpanel interface
when valid x, y, and z have been entered in the input fields.
"""
self.force = dist
self.trimObject()
self.finish()
Gui.addCommand('Draft_Trimex', Trimex())
## @}