FreeCAD / src /Mod /Part /AttachmentEditor /TaskAttachmentEditor.py
AbdulElahGwaith's picture
Upload folder using huggingface_hub
985c397 verified
# SPDX-License-Identifier: LGPL-2.1-or-later
# /***************************************************************************
# * Copyright (c) 2016 Victor Titov (DeepSOIC) <vv.titov@gmail.com> *
# * *
# * This file is part of the FreeCAD CAx development system. *
# * *
# * This library is free software; you can redistribute it and/or *
# * modify it under the terms of the GNU Library General Public *
# * License as published by the Free Software Foundation; either *
# * version 2 of the License, or (at your option) any later version. *
# * *
# * This library 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 this library; see the file COPYING.LIB. If not, *
# * write to the Free Software Foundation, Inc., 59 Temple Place, *
# * Suite 330, Boston, MA 02111-1307, USA *
# * *
# ***************************************************************************/
from __future__ import absolute_import
import FreeCAD as App
import Part
mm = App.Units.MilliMetre
deg = App.Units.Degree
Q = App.Units.Quantity
from AttachmentEditor.FrozenClass import FrozenClass
try:
from Show import TempoVis
from Show.DepGraphTools import getAllDependent
except ImportError as err:
def TempoVis(doc, tag):
return None
def getAllDependent(feature):
return []
App.Console.PrintWarning(
"AttachmentEditor: Failed to import some code from Show module. Functionality will be limited.\n"
)
App.Console.PrintWarning(str(err))
if App.GuiUp:
import FreeCADGui as Gui
from PySide import QtCore, QtGui
from FreeCADGui import PySideUic as uic
# -------------------------- translation-related code ----------------------------------------
# Thanks, yorik! (see forum thread "A new Part tool is being born... JoinFeatures!"
# https://forum.freecad.org/viewtopic.php?f=22&t=11112&start=30#p90239 )
try:
_fromUtf8 = QtCore.QString.fromUtf8
except AttributeError:
def _fromUtf8(s):
return s
translate = App.Qt.translate
# --------------------------/translation-related code ----------------------------------------
def linkSubList_convertToOldStyle(references):
(
"input: [(obj1, (sub1, sub2)), (obj2, (sub1, sub2))]\n"
"output: [(obj1, sub1), (obj1, sub2), (obj2, sub1), (obj2, sub2)]"
)
result = []
for tup in references:
if type(tup[1]) is tuple or type(tup[1]) is list:
for subname in tup[1]:
result.append((tup[0], subname))
if len(tup[1]) == 0:
result.append((tup[0], ""))
elif isinstance(tup[1], str):
# old style references, no conversion required
result.append(tup)
return result
def StrFromLink(feature, subname):
return feature.Name + ((":" + subname) if subname else "")
def LinkFromStr(strlink, document):
if len(strlink) == 0:
return None
pieces = strlink.split(":")
feature = document.getObject(pieces[0])
subname = ""
if feature is None:
raise ValueError(
translate("AttachmentEditor", "No object named {}", None).format(pieces[0])
)
if len(pieces) == 2:
subname = pieces[1]
elif len(pieces) > 2:
raise ValueError(
translate(
"AttachmentEditor", "Failed to parse link (more than one colon encountered)", None
)
)
return (
feature,
str(subname),
) # wrap in str to remove unicode, which confuses assignment to PropertyLinkSubList.
def StrListFromRefs(references):
"""input: PropertyLinkSubList. Output: list of strings for UI."""
references_oldstyle = linkSubList_convertToOldStyle(references)
return [StrFromLink(feature, subelement) for (feature, subelement) in references_oldstyle]
def RefsFromStrList(strings, document):
"""input: strings as from UI. Output: list of tuples that can be assigned to PropertyLinkSubList."""
refs = []
for st in strings:
lnk = LinkFromStr(st, document)
if lnk is not None:
refs.append(lnk)
return refs
def GetSelectionAsLinkSubList():
sel = Gui.Selection.getSelectionEx("", 0)
result = []
for selobj in sel:
for subname in selobj.SubElementNames:
result.append((selobj.Object, subname))
if len(selobj.SubElementNames) == 0:
result.append((selobj.Object, ""))
return result
def PlacementsFuzzyCompare(plm1, plm2):
pos_eq = (plm1.Base - plm2.Base).Length < 1e-7 # 1e-7 is OCC's Precision::Confusion
q1 = plm1.Rotation.Q
q2 = plm2.Rotation.Q
# rotations are equal if q1 == q2 or q1 == -q2.
# Invert one of Q's if their scalar product is negative, before comparison.
if q1[0] * q2[0] + q1[1] * q2[1] + q1[2] * q2[2] + q1[3] * q2[3] < 0:
q2 = [-v for v in q2]
rot_eq = (
abs(q1[0] - q2[0]) + abs(q1[1] - q2[1]) + abs(q1[2] - q2[2]) + abs(q1[3] - q2[3])
) < 1e-12 # 1e-12 is OCC's Precision::Angular (in radians)
return pos_eq and rot_eq
class CancelError(Exception):
def __init__(self):
self.message = "Canceled by user"
self.isCancelError = True
class AttachmentEditorTaskPanel(FrozenClass):
"""The editmode TaskPanel for attachment editing"""
KEYmode = (
QtCore.Qt.ItemDataRole.UserRole
) # Key to use in Item.data(key) to obtain a mode associated with list item
KEYon = (
QtCore.Qt.ItemDataRole.UserRole + 1
) # Key to use in Item.data(key) to obtain if the mode is valid
def __define_attributes(self):
self.obj = None # feature being attached
self.attacher = None # AttachEngine that is being actively used by the dialog. Its parameters are constantly and actively kept in sync with the dialog.
self.obj_is_attachable = (
True # False when editing non-attachable objects (alignment, not attachment)
)
self.last_sugr = None # result of last execution of suggestor
self.form = None # Qt widget of dialog interface
self.block = False # when True, event handlers return without doing anything (instead of doing-undoing blockSignals to everything)
self.refLines = [] # reference lineEdit widgets, packed into a list for convenience
self.refButtons = [] # buttons next to reference lineEdits
self.attachmentOffsetEdits = [] # all edit boxes related to attachmentOffset
self.i_active_ref = (
-1
) # index of reference being selected (-1 means no reaction to selecting)
self.auto_next = False # if true, references being selected are appended ('Selecting' state is automatically advanced to next button)
self.tv = None # TempoVis class instance
self.create_transaction = True # if false, dialog doesn't mess with transactions.
self.callback_OK = None
self.callback_Cancel = None
self.callback_Apply = None
self._freeze()
def __init__(
self,
obj_to_attach,
take_selection=False,
create_transaction=True,
callback_OK=None,
callback_Cancel=None,
callback_Apply=None,
):
self.__define_attributes()
self.create_transaction = create_transaction
self.callback_OK = callback_OK
self.callback_Cancel = callback_Cancel
self.callback_Apply = callback_Apply
self.obj = obj_to_attach
try:
if not self.obj.hasExtension("Part::AttachExtension"):
self.obj.addExtension("Part::AttachExtensionPython")
except Exception:
pass
if hasattr(obj_to_attach, "Attacher"):
self.attacher = obj_to_attach.Attacher
elif hasattr(obj_to_attach, "AttacherType"):
self.attacher = Part.AttachEngine(obj_to_attach.AttacherType)
else:
movable = True
if not hasattr(self.obj, "Placement"):
movable = False
if "Hidden" in self.obj.getEditorMode(
"Placement"
) or "ReadOnly" in self.obj.getEditorMode("Placement"):
movable = False
if not movable:
if self.callback_Cancel:
self.callback_Cancel()
raise ValueError(
translate(
"AttachmentEditor",
"Object {} is neither movable nor attachable, can't edit attachment",
None,
).format(self.obj.Label)
)
self.obj_is_attachable = False
self.attacher = Part.AttachEngine()
mb = QtGui.QMessageBox()
mb.setIcon(mb.Icon.Warning)
mb.setText(
translate(
"AttachmentEditor",
"{} is not attachable. The attachment editor can still be used to align the object, but the attachment will not be parametric.",
None,
).format(obj_to_attach.Label)
)
mb.setWindowTitle(translate("AttachmentEditor", "Attachment", None))
btnAbort = mb.addButton(QtGui.QMessageBox.StandardButton.Abort)
btnOK = mb.addButton(
translate("AttachmentEditor", "Continue", None),
QtGui.QMessageBox.ButtonRole.ActionRole,
)
mb.setDefaultButton(btnOK)
mb.exec_()
if mb.clickedButton() is btnAbort:
if self.callback_Cancel:
self.callback_Cancel()
raise CancelError()
import os
self.form = uic.loadUi(os.path.dirname(__file__) + os.path.sep + "TaskAttachmentEditor.ui")
self.form.setWindowIcon(QtGui.QIcon(":/icons/tools/Part_Attachment.svg"))
self.form.setWindowTitle(translate("AttachmentEditor", "Attachment", None))
self.form.attachmentOffsetX.setProperty("unit", "mm")
self.form.attachmentOffsetY.setProperty("unit", "mm")
self.form.attachmentOffsetZ.setProperty("unit", "mm")
Gui.ExpressionBinding(self.form.attachmentOffsetX).bind(self.obj, "AttachmentOffset.Base.x")
Gui.ExpressionBinding(self.form.attachmentOffsetY).bind(self.obj, "AttachmentOffset.Base.y")
Gui.ExpressionBinding(self.form.attachmentOffsetZ).bind(self.obj, "AttachmentOffset.Base.z")
Gui.ExpressionBinding(self.form.attachmentOffsetYaw).bind(
self.obj, "AttachmentOffset.Rotation.Yaw"
)
Gui.ExpressionBinding(self.form.attachmentOffsetPitch).bind(
self.obj, "AttachmentOffset.Rotation.Pitch"
)
Gui.ExpressionBinding(self.form.attachmentOffsetRoll).bind(
self.obj, "AttachmentOffset.Rotation.Roll"
)
self.refLines = [
self.form.lineRef1,
self.form.lineRef2,
self.form.lineRef3,
self.form.lineRef4,
]
self.refButtons = [
self.form.buttonRef1,
self.form.buttonRef2,
self.form.buttonRef3,
self.form.buttonRef4,
]
self.attachmentOffsetEdits = [
self.form.attachmentOffsetX,
self.form.attachmentOffsetY,
self.form.attachmentOffsetZ,
self.form.attachmentOffsetYaw,
self.form.attachmentOffsetPitch,
self.form.attachmentOffsetRoll,
]
self.block = False
for i in range(len(self.refLines)):
QtCore.QObject.connect(
self.refLines[i],
QtCore.SIGNAL("textEdited(QString)"),
lambda txt, i=i: self.lineRefChanged(i, txt),
)
for i in range(len(self.refLines)):
QtCore.QObject.connect(
self.refButtons[i], QtCore.SIGNAL("clicked()"), lambda i=i: self.refButtonClicked(i)
)
for i in range(len(self.attachmentOffsetEdits)):
QtCore.QObject.connect(
self.attachmentOffsetEdits[i],
QtCore.SIGNAL("valueChanged(double)"),
lambda val, i=i: self.attachmentOffsetChanged(i, val),
)
QtCore.QObject.connect(
self.form.checkBoxFlip, QtCore.SIGNAL("clicked()"), self.checkBoxFlipClicked
)
QtCore.QObject.connect(
self.form.listOfModes, QtCore.SIGNAL("itemSelectionChanged()"), self.modeSelected
)
if self.create_transaction:
self.obj.Document.openTransaction(
translate("AttachmentEditor", "Edit attachment of {}", None).format(self.obj.Name)
)
self.readParameters()
if len(self.attacher.References) == 0 and take_selection:
sel = GetSelectionAsLinkSubList()
for i in range(len(sel))[::-1]:
if sel[i][0] is obj_to_attach:
sel.pop(i)
self.attacher.References = sel
# need to update textboxes
self.fillAllRefLines()
if len(self.attacher.References) == 0:
self.i_active_ref = 0
self.auto_next = True
else:
self.i_active_ref = -1
self.auto_next = False
Gui.Selection.addObserver(self, 0)
self.updatePreview()
self.updateRefButtons()
self.tv = TempoVis(self.obj.Document, tag="PartGui.TaskAttachmentEditor")
if self.tv: # tv will still be None if Show module is unavailable
self.tv.hide_all_dependent(self.obj)
self.tv.show(self.obj)
self.tv.setUnpickable(self.obj)
self.tv.modifyVPProperty(self.obj, "Transparency", 70)
self.tv.show([obj for (obj, subname) in self.attacher.References])
# task dialog handling
def getStandardButtons(self):
return (
QtGui.QDialogButtonBox.Ok | QtGui.QDialogButtonBox.Cancel | QtGui.QDialogButtonBox.Apply
)
def clicked(self, button):
if button == QtGui.QDialogButtonBox.Apply:
if self.obj_is_attachable:
self.writeParameters()
if self.create_transaction:
self.obj.Document.commitTransaction()
self.obj.Document.openTransaction(
translate("AttachmentEditor", "Edit attachment of {}", None).format(
self.obj.Name
)
)
self.updatePreview()
if self.callback_Apply:
self.callback_Apply()
def accept(self):
if self.obj_is_attachable:
self.writeParameters()
if self.create_transaction:
self.obj.Document.commitTransaction()
self.cleanUp()
Gui.Control.closeDialog()
if self.callback_OK:
self.callback_OK()
def reject(self):
if self.create_transaction:
self.obj.Document.abortTransaction()
self.cleanUp()
Gui.Control.closeDialog()
if self.callback_Cancel:
self.callback_Cancel()
# selectionObserver stuff
def addSelection(self, docname, objname, subname, pnt):
i = self.i_active_ref
if i < 0:
# not selecting any reference
return
if i > 0 and self.auto_next:
prevref = LinkFromStr(self.refLines[i - 1].text(), self.obj.Document)
if prevref[0].Name == objname and subname == "":
# whole object was selected by double-clicking
# its subelement was already written to line[i-1], so we decrease i to overwrite the lineRefChanged
i -= 1
if i > len(self.refLines) - 1:
# all 4 references have been selected, finish
assert self.auto_next
self.i_active_ref = -1
self.updateRefButtons()
return
if i > -1:
if ";" in subname:
# Test code:
# Gui.Selection.getSelectionEx("", 0)[0].SubElementNames
# Return value examples:
# ('Part.Body.;Edge10;:Hf19,E.Edge10',)
# ('Part.Body.;#1a:1;:H-f28,E;:Hf28,E.Edge15',)
# ('Part.Body.;#16:1;:G#28;FUS;:H-f28:a,E;:Hf28,E.Edge10',)
# ('Part.Body.Pad.;#1d:1;:H-f28,F.Face7',)
# ('Part.Box.Edge10',)
# ('Part.Sketch.;g1;SKT.Edge1',)
tmp = subname.split(";")
subname = tmp[0] + tmp[-1].split(".")[-1]
parent = self.obj.getParentGeoFeatureGroup()
if parent is not None:
path = [objname] + subname.split(".")
if parent.Name in path:
path = path[path.index(parent.Name) + 1 :]
if len(path) > 1:
objname = path[0]
subname = ".".join(path[1:])
# assign the selected reference
if objname == self.obj.Name:
self.form.message.setText(
translate("AttachmentEditor", "Ignored. Can't attach object to itself!", None)
)
return
if App.getDocument(docname).getObject(objname) in getAllDependent(self.obj):
self.form.message.setText(
translate(
"AttachmentEditor",
"{} depends on object being attached, can't use it for attachment",
None,
).format(objname)
)
return
self.refLines[i].setText(
StrFromLink(App.getDocument(docname).getObject(objname), subname)
)
self.lineRefChanged(i, "")
if self.auto_next:
i += 1
self.i_active_ref = i
self.updateRefButtons()
# slots
def attachmentOffsetChanged(self, index, value):
if self.block:
return
plm = self.attacher.AttachmentOffset
pos = plm.Base
if index == 0:
pos.x = Q(self.form.attachmentOffsetX.text()).Value
if index == 1:
pos.y = Q(self.form.attachmentOffsetY.text()).Value
if index == 2:
pos.z = Q(self.form.attachmentOffsetZ.text()).Value
if index >= 0 and index <= 2:
plm.Base = pos
if index >= 3 and index <= 5:
yaw = Q(self.form.attachmentOffsetYaw.text()).Value
pitch = Q(self.form.attachmentOffsetPitch.text()).Value
roll = Q(self.form.attachmentOffsetRoll.text()).Value
rot = App.Rotation(yaw, pitch, roll)
plm.Rotation = rot
self.attacher.AttachmentOffset = plm
self.updatePreview()
def checkBoxFlipClicked(self):
if self.block:
return
self.attacher.Reverse = self.form.checkBoxFlip.isChecked()
self.updatePreview()
def lineRefChanged(self, index, value):
if self.block:
return
# not parsing links here, because doing it in updatePreview will display error message
self.updatePreview()
def refButtonClicked(self, index):
if self.block:
return
if self.i_active_ref == index:
# stop selecting
self.i_active_ref = -1
else:
# start selecting
self.i_active_ref = index
self.auto_next = False
self.updateRefButtons()
def modeSelected(self):
if self.block:
return
self.attacher.Mode = self.getCurrentMode()
self.updatePreview()
# internal methods
def writeParameters(self):
"Transfer from the dialog to the object"
self.attacher.writeParametersToFeature(self.obj)
def readParameters(self):
"Transfer from the object to the dialog"
if self.obj_is_attachable:
self.attacher.readParametersFromFeature(self.obj)
plm = self.attacher.AttachmentOffset
try:
old_selfblock = self.block
self.block = True
self.form.attachmentOffsetX.lineEdit().setText((plm.Base.x * mm).UserString)
self.form.attachmentOffsetY.lineEdit().setText((plm.Base.y * mm).UserString)
self.form.attachmentOffsetZ.lineEdit().setText((plm.Base.z * mm).UserString)
self.form.attachmentOffsetYaw.lineEdit().setText(
(plm.Rotation.toEuler()[0] * deg).UserString
)
self.form.attachmentOffsetPitch.lineEdit().setText(
(plm.Rotation.toEuler()[1] * deg).UserString
)
self.form.attachmentOffsetRoll.lineEdit().setText(
(plm.Rotation.toEuler()[2] * deg).UserString
)
self.form.checkBoxFlip.setChecked(self.attacher.Reverse)
self.fillAllRefLines()
finally:
self.block = old_selfblock
def fillAllRefLines(self):
old_block = self.block
try:
self.block = True
strings = StrListFromRefs(self.attacher.References)
if len(strings) < len(self.refLines):
strings.extend([""] * (len(self.refLines) - len(strings)))
for i in range(len(self.refLines)):
self.refLines[i].setText(strings[i])
finally:
self.block = old_block
def parseAllRefLines(self):
self.attacher.References = RefsFromStrList(
[le.text() for le in self.refLines], self.obj.Document
)
def updateListOfModes(self):
"""needs suggestor to have been called, and assigned to self.last_sugr"""
try:
old_selfblock = self.block
self.block = True
list_widget = self.form.listOfModes
list_widget.clear()
sugr = self.last_sugr
# always have the option to choose Deactivated mode
valid_modes = ["Deactivated"] + sugr["allApplicableModes"]
# add valid modes
for m in valid_modes:
item = QtGui.QListWidgetItem()
txt = self.attacher.getModeInfo(m)["UserFriendlyName"]
item.setText(txt)
item.setData(self.KEYmode, m)
item.setData(self.KEYon, True)
if m == sugr["bestFitMode"]:
f = item.font()
f.setBold(True)
item.setFont(f)
list_widget.addItem(item)
item.setSelected(self.attacher.Mode == m)
# add potential modes
for m in sugr["reachableModes"].keys():
item = QtGui.QListWidgetItem()
txt = self.attacher.getModeInfo(m)["UserFriendlyName"]
listlistrefs = sugr["reachableModes"][m]
if len(listlistrefs) == 1:
listrefs_userfriendly = [
self.attacher.getRefTypeInfo(t)["UserFriendlyName"] for t in listlistrefs[0]
]
txt = translate("AttachmentEditor", "{} (add {})", None).format(
txt, "+".join(listrefs_userfriendly)
)
else:
txt = translate("AttachmentEditor", "{} (add more references)", None).format(
txt
)
item.setText(txt)
item.setData(self.KEYmode, m)
item.setData(self.KEYon, True)
if m == sugr["bestFitMode"]:
f = item.font()
f.setBold(True)
item.setFont(f)
# disable this item
f = item.flags()
f = f & ~(QtCore.Qt.ItemFlag.ItemIsEnabled | QtCore.Qt.ItemFlag.ItemIsSelectable)
item.setFlags(f)
list_widget.addItem(item)
# re-scan the list to fill in tooltips
for item in list_widget.findItems("", QtCore.Qt.MatchContains):
m = item.data(self.KEYmode)
on = item.data(self.KEYon)
mi = self.attacher.getModeInfo(m)
cmb = []
for refstr in mi["ReferenceCombinations"]:
refstr_userfriendly = [
self.attacher.getRefTypeInfo(t)["UserFriendlyName"] for t in refstr
]
cmb.append(", ".join(refstr_userfriendly))
tip = mi["BriefDocu"]
if m != "Deactivated":
tip += "\n\n"
tip += translate(
"AttachmentEditor", "Reference combinations:", None
) + " \n\n".join(cmb)
item.setToolTip(tip)
finally:
self.block = old_selfblock
def updateRefButtons(self):
try:
old_selfblock = self.block
self.block = True
for i in range(len(self.refButtons)):
btn = self.refButtons[i]
btn.setCheckable(True)
btn.setChecked(self.i_active_ref == i)
typ = translate("AttachmentEditor", "Reference{}", None).format(str(i + 1))
if self.last_sugr is not None:
typestr = self.last_sugr["references_Types"]
if i < len(typestr):
typ = self.attacher.getRefTypeInfo(typestr[i])["UserFriendlyName"]
btn.setText(
translate("AttachmentEditor", "Selecting…", None)
if self.i_active_ref == i
else typ
)
finally:
self.block = old_selfblock
def getCurrentMode(self):
list_widget = self.form.listOfModes
sel = list_widget.selectedItems()
if len(sel) == 1:
if sel[0].data(self.KEYon):
return str(
sel[0].data(self.KEYmode)
) # data() returns unicode, which confuses attacher
# nothing selected in list. Return suggested
if self.last_sugr is not None:
if self.last_sugr["message"] == "OK":
return self.last_sugr["bestFitMode"]
# no suggested mode. Return current, so it doesn't change
return self.attacher.Mode
def updatePreview(self):
new_plm = None
try:
self.parseAllRefLines()
self.last_sugr = self.attacher.suggestModes()
if self.last_sugr["message"] == "LinkBroken":
raise ValueError(
translate("AttachmentEditor", "Failed to resolve links. {}", None).format(
self.last_sugr["error"]
)
)
self.updateListOfModes()
self.attacher.Mode = self.getCurrentMode()
new_plm = self.attacher.calculateAttachedPlacement(self.obj.Placement)
if new_plm is None:
self.form.message.setText(translate("AttachmentEditor", "Not attached", None))
else:
self.form.message.setText(
translate("AttachmentEditor", "Attached with mode {}", None).format(
self.attacher.getModeInfo(self.getCurrentMode())["UserFriendlyName"]
)
)
if PlacementsFuzzyCompare(self.obj.Placement, new_plm) == False:
# assign only if placement changed. this avoids touching the object
# when entering and exiting dialog without changing anything
self.obj.Placement = new_plm
except Exception as err:
self.form.message.setText(
translate("AttachmentEditor", "Error: {}", None).format(str(err))
)
if new_plm is not None:
self.form.groupBox_AttachmentOffset.setTitle(
translate("AttachmentEditor", "Attachment Offset (in local coordinates):", None)
)
self.form.groupBox_AttachmentOffset.setEnabled(True)
else:
self.form.groupBox_AttachmentOffset.setTitle(
translate("AttachmentEditor", "Attachment Offset (inactive - not attached):", None)
)
self.form.groupBox_AttachmentOffset.setEnabled(False)
def cleanUp(self):
"""stuff that needs to be done when dialog is closed."""
Gui.Selection.removeObserver(self)
if self.tv:
self.tv.restore()