* Assembly: Initial implementation. * Disable Assembly wb as it's WIP. * Stub code for handling assembly import. Co-authored-by: sliptonic <shopinthewoods@gmail.com> Co-authored-by: Paddle <PaddleStroke@users.noreply.github.com>
346 lines
12 KiB
Python
346 lines
12 KiB
Python
# SPDX-License-Identifier: LGPL-2.1-or-later
|
|
# /****************************************************************************
|
|
# *
|
|
# Copyright (c) 2023 Ondsel <development@ondsel.com> *
|
|
# *
|
|
# This file is part of FreeCAD. *
|
|
# *
|
|
# FreeCAD is free software: you can redistribute it and/or modify it *
|
|
# under the terms of the GNU Lesser General Public License as *
|
|
# published by the Free Software Foundation, either version 2.1 of the *
|
|
# License, or (at your option) any later version. *
|
|
# *
|
|
# 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 *
|
|
# Lesser General Public License for more details. *
|
|
# *
|
|
# You should have received a copy of the GNU Lesser General Public *
|
|
# License along with FreeCAD. If not, see *
|
|
# <https://www.gnu.org/licenses/>. *
|
|
# *
|
|
# ***************************************************************************/
|
|
|
|
import os, re
|
|
import FreeCAD as App
|
|
|
|
from PySide.QtCore import QT_TRANSLATE_NOOP
|
|
|
|
if App.GuiUp:
|
|
import FreeCADGui as Gui
|
|
from PySide import QtCore, QtGui, QtWidgets
|
|
|
|
# translate = App.Qt.translate
|
|
|
|
__title__ = "Assembly Commands"
|
|
__author__ = "Ondsel"
|
|
__url__ = "https://www.freecad.org"
|
|
|
|
|
|
def activeAssembly():
|
|
doc = Gui.ActiveDocument
|
|
|
|
if doc is None or doc.ActiveView is None:
|
|
return None
|
|
|
|
active_part = doc.ActiveView.getActiveObject("part")
|
|
|
|
if active_part is not None and active_part.Type == "Assembly":
|
|
return active_part
|
|
|
|
return None
|
|
|
|
|
|
def isDocTemporary(doc):
|
|
# Guard against older versions of FreeCad which don't have the Temporary attribute
|
|
try:
|
|
docTemporary = doc.Temporary
|
|
except AttributeError:
|
|
docTemporary = False
|
|
return docTemporary
|
|
|
|
|
|
def labelName(obj):
|
|
if obj:
|
|
if obj.Name == obj.Label:
|
|
txt = obj.Label
|
|
else:
|
|
txt = obj.Label + " (" + obj.Name + ")"
|
|
return txt
|
|
else:
|
|
return None
|
|
|
|
|
|
class CommandCreateAssembly:
|
|
def __init__(self):
|
|
pass
|
|
|
|
def GetResources(self):
|
|
return {
|
|
"Pixmap": "Geoassembly",
|
|
"MenuText": QT_TRANSLATE_NOOP("Assembly_CreateAssembly", "Create Assembly"),
|
|
"Accel": "A",
|
|
"ToolTip": QT_TRANSLATE_NOOP(
|
|
"Assembly_CreateAssembly",
|
|
"Create an assembly object in the current document.",
|
|
),
|
|
"CmdType": "ForEdit",
|
|
}
|
|
|
|
def IsActive(self):
|
|
return App.ActiveDocument is not None
|
|
|
|
def Activated(self):
|
|
App.setActiveTransaction("Create assembly")
|
|
assembly = App.ActiveDocument.addObject("App::Part", "Assembly")
|
|
assembly.Type = "Assembly"
|
|
Gui.ActiveDocument.ActiveView.setActiveObject("part", assembly)
|
|
App.closeActiveTransaction()
|
|
|
|
|
|
class CommandInsertLink:
|
|
def __init__(self):
|
|
pass
|
|
|
|
def GetResources(self):
|
|
tooltip = "<p>Insert a Link into the assembly. "
|
|
tooltip += (
|
|
"This will create dynamic links to parts/bodies/primitives/assemblies, "
|
|
)
|
|
tooltip += "which can be in this document or in another document "
|
|
tooltip += "that is <b>open in the current session</b></p>"
|
|
tooltip += "<p>Press shift to add several links while clicking on the view."
|
|
|
|
return {
|
|
"Pixmap": "Assembly_InsertLink",
|
|
"MenuText": QT_TRANSLATE_NOOP("Assembly_InsertLink", "Insert Link"),
|
|
"Accel": "I",
|
|
"ToolTip": QT_TRANSLATE_NOOP("Assembly_InsertLink", tooltip),
|
|
"CmdType": "ForEdit",
|
|
}
|
|
|
|
def IsActive(self):
|
|
return activeAssembly() is not None
|
|
|
|
def Activated(self):
|
|
assembly = activeAssembly()
|
|
if not assembly:
|
|
return
|
|
view = Gui.activeDocument().activeView()
|
|
|
|
self.panel = TaskAssemblyInsertLink(assembly, view)
|
|
Gui.Control.showDialog(self.panel)
|
|
|
|
|
|
class TaskAssemblyInsertLink(QtCore.QObject):
|
|
def __init__(self, assembly, view):
|
|
super().__init__()
|
|
|
|
self.assembly = assembly
|
|
self.view = view
|
|
self.doc = App.ActiveDocument
|
|
|
|
self.form = Gui.PySideUic.loadUi(":/panels/TaskAssemblyInsertLink.ui")
|
|
self.form.installEventFilter(self)
|
|
|
|
# Actions
|
|
self.form.openFileButton.clicked.connect(self.openFile)
|
|
self.form.partList.itemClicked.connect(self.onItemClicked)
|
|
self.form.filterPartList.textChanged.connect(self.onFilterChange)
|
|
|
|
self.allParts = []
|
|
self.partsDoc = []
|
|
self.numberOfAddedParts = 0
|
|
self.translation = 0
|
|
self.partMoving = False
|
|
|
|
self.buildPartList()
|
|
|
|
App.setActiveTransaction("Insert Link")
|
|
|
|
def accept(self):
|
|
App.closeActiveTransaction()
|
|
self.deactivated()
|
|
return True
|
|
|
|
def reject(self):
|
|
App.closeActiveTransaction(True)
|
|
self.deactivated()
|
|
return True
|
|
|
|
def deactivated(self):
|
|
if self.partMoving:
|
|
self.endMove()
|
|
|
|
def buildPartList(self):
|
|
self.allParts.clear()
|
|
self.partsDoc.clear()
|
|
|
|
docList = App.listDocuments().values()
|
|
|
|
for doc in docList:
|
|
if isDocTemporary(doc):
|
|
continue
|
|
|
|
for obj in doc.findObjects("App::Part"):
|
|
# we don't want to link to itself
|
|
if obj != self.assembly:
|
|
self.allParts.append(obj)
|
|
self.partsDoc.append(doc)
|
|
|
|
for obj in doc.findObjects("PartDesign::Body"):
|
|
# but only those at top level (not nested inside other containers)
|
|
if obj.getParentGeoFeatureGroup() is None:
|
|
self.allParts.append(obj)
|
|
self.partsDoc.append(doc)
|
|
|
|
self.form.partList.clear()
|
|
for part in self.allParts:
|
|
newItem = QtGui.QListWidgetItem()
|
|
newItem.setText(part.Document.Name + "#" + labelName(part))
|
|
newItem.setIcon(part.ViewObject.Icon)
|
|
self.form.partList.addItem(newItem)
|
|
|
|
def onFilterChange(self):
|
|
filterStr = self.form.filterPartList.text().strip()
|
|
for x in range(self.form.partList.count()):
|
|
item = self.form.partList.item(x)
|
|
# check the items's text match the filter ignoring the case
|
|
matchStr = re.search(filterStr, item.text(), flags=re.IGNORECASE)
|
|
if filterStr and not matchStr:
|
|
item.setHidden(True)
|
|
else:
|
|
item.setHidden(False)
|
|
|
|
def openFile(self):
|
|
filename = None
|
|
importDoc = None
|
|
importDocIsOpen = False
|
|
dialog = QtGui.QFileDialog(
|
|
QtGui.QApplication.activeWindow(),
|
|
"Select FreeCAD document to import part from",
|
|
)
|
|
|
|
dialog.setNameFilter(
|
|
"Supported Formats *.FCStd *.fcstd (*.FCStd *.fcstd);;All files (*.*)"
|
|
)
|
|
if dialog.exec_():
|
|
filename = str(dialog.selectedFiles()[0])
|
|
# look only for filenames, not paths, as there are problems on WIN10 (Address-translation??)
|
|
requestedFile = os.path.split(filename)[1]
|
|
# see whether the file is already open
|
|
for d in App.listDocuments().values():
|
|
recentFile = os.path.split(d.FileName)[1]
|
|
if requestedFile == recentFile:
|
|
importDocIsOpen = True
|
|
break
|
|
# if not, open it
|
|
if not importDocIsOpen:
|
|
if filename.lower().endswith(".fcstd"):
|
|
App.openDocument(filename)
|
|
App.setActiveDocument(self.doc.Name)
|
|
self.buildPartList()
|
|
return
|
|
|
|
def onItemClicked(self, item):
|
|
for selected in self.form.partList.selectedIndexes():
|
|
selectedPart = self.allParts[selected.row()]
|
|
if not selectedPart:
|
|
return
|
|
|
|
if self.partMoving:
|
|
self.endMove()
|
|
|
|
# check that the current document had been saved or that it's the same document as that of the selected part
|
|
if not self.doc.FileName != "" and not self.doc == selectedPart.Document:
|
|
print(
|
|
"The current document must be saved before inserting an external part"
|
|
)
|
|
return
|
|
|
|
self.createdLink = self.assembly.newObject("App::Link", selectedPart.Name)
|
|
self.createdLink.LinkedObject = selectedPart
|
|
self.createdLink.Placement.Base = self.getTranslationVec(selectedPart)
|
|
self.createdLink.recompute()
|
|
|
|
self.numberOfAddedParts += 1
|
|
|
|
# highlight the link
|
|
Gui.Selection.clearSelection()
|
|
Gui.Selection.addSelection(
|
|
self.doc.Name, self.assembly.Name, self.createdLink.Name + "."
|
|
)
|
|
|
|
# Start moving the part if user brings mouse on view
|
|
self.initMove()
|
|
|
|
def initMove(self):
|
|
self.callbackMove = self.view.addEventCallback(
|
|
"SoLocation2Event", self.moveMouse
|
|
)
|
|
self.callbackClick = self.view.addEventCallback(
|
|
"SoMouseButtonEvent", self.clickMouse
|
|
)
|
|
self.callbackKey = self.view.addEventCallback(
|
|
"SoKeyboardEvent", self.KeyboardEvent
|
|
)
|
|
self.partMoving = True
|
|
|
|
# Selection filter to avoid selecting the part while it's moving
|
|
# filter = Gui.Selection.Filter('SELECT ???')
|
|
# Gui.Selection.addSelectionGate(filter)
|
|
|
|
def endMove(self):
|
|
self.view.removeEventCallback("SoLocation2Event", self.callbackMove)
|
|
self.view.removeEventCallback("SoMouseButtonEvent", self.callbackClick)
|
|
self.view.removeEventCallback("SoKeyboardEvent", self.callbackKey)
|
|
self.partMoving = False
|
|
self.doc.recompute()
|
|
# Gui.Selection.removeSelectionGate()
|
|
|
|
def moveMouse(self, info):
|
|
newPos = self.view.getPoint(*info["Position"])
|
|
self.createdLink.Placement.Base = newPos
|
|
|
|
def clickMouse(self, info):
|
|
if info["Button"] == "BUTTON1" and info["State"] == "DOWN":
|
|
if info["ShiftDown"]:
|
|
# Create a new link and moves this one now
|
|
currentPos = self.createdLink.Placement.Base
|
|
selectedPart = self.createdLink.LinkedObject
|
|
self.createdLink = self.assembly.newObject(
|
|
"App::Link", selectedPart.Name
|
|
)
|
|
self.createdLink.LinkedObject = selectedPart
|
|
self.createdLink.Placement.Base = currentPos
|
|
else:
|
|
self.endMove()
|
|
|
|
# 3D view keyboard handler
|
|
def KeyboardEvent(self, info):
|
|
if info["State"] == "UP" and info["Key"] == "ESCAPE":
|
|
self.endMove()
|
|
self.doc.removeObject(self.createdLink.Name)
|
|
|
|
# Taskbox keyboard event handler
|
|
def eventFilter(self, watched, event):
|
|
if watched == self.form and event.type() == QtCore.QEvent.KeyPress:
|
|
if event.key() == QtCore.Qt.Key_Escape and self.partMoving:
|
|
self.endMove()
|
|
self.doc.removeObject(self.createdLink.Name)
|
|
return True # Consume the event
|
|
return super().eventFilter(watched, event)
|
|
|
|
def getTranslationVec(self, part):
|
|
bb = part.Shape.BoundBox
|
|
if bb:
|
|
self.translation += (bb.XMax + bb.YMax + bb.ZMax) * 0.15
|
|
else:
|
|
self.translation += 10
|
|
return App.Vector(self.translation, self.translation, self.translation)
|
|
|
|
|
|
if App.GuiUp:
|
|
Gui.addCommand("Assembly_CreateAssembly", CommandCreateAssembly())
|
|
Gui.addCommand("Assembly_InsertLink", CommandInsertLink())
|