FreeCAD

Форк
0
/
CommandInsertLink.py 
446 строк · 16.9 Кб
1
# SPDX-License-Identifier: LGPL-2.1-or-later
2
# /**************************************************************************
3
#                                                                           *
4
#    Copyright (c) 2023 Ondsel <development@ondsel.com>                     *
5
#                                                                           *
6
#    This file is part of FreeCAD.                                          *
7
#                                                                           *
8
#    FreeCAD is free software: you can redistribute it and/or modify it     *
9
#    under the terms of the GNU Lesser General Public License as            *
10
#    published by the Free Software Foundation, either version 2.1 of the   *
11
#    License, or (at your option) any later version.                        *
12
#                                                                           *
13
#    FreeCAD is distributed in the hope that it will be useful, but         *
14
#    WITHOUT ANY WARRANTY; without even the implied warranty of             *
15
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU       *
16
#    Lesser General Public License for more details.                        *
17
#                                                                           *
18
#    You should have received a copy of the GNU Lesser General Public       *
19
#    License along with FreeCAD. If not, see                                *
20
#    <https://www.gnu.org/licenses/>.                                       *
21
#                                                                           *
22
# **************************************************************************/
23

24
import re
25
import os
26
import FreeCAD as App
27

28
from PySide.QtCore import QT_TRANSLATE_NOOP
29

30
if App.GuiUp:
31
    import FreeCADGui as Gui
32
    from PySide import QtCore, QtGui, QtWidgets
33

34
import UtilsAssembly
35
import Preferences
36
import CommandCreateJoint
37

38
# translate = App.Qt.translate
39

40
__title__ = "Assembly Command Insert Link"
41
__author__ = "Ondsel"
42
__url__ = "https://www.freecad.org"
43

44

45
class CommandInsertLink:
46
    def __init__(self):
47
        pass
48

49
    def GetResources(self):
50
        return {
51
            "Pixmap": "Assembly_InsertLink",
52
            "MenuText": QT_TRANSLATE_NOOP("Assembly_InsertLink", "Insert Link"),
53
            "Accel": "I",
54
            "ToolTip": "<p>"
55
            + QT_TRANSLATE_NOOP(
56
                "Assembly_InsertLink",
57
                "Insert a Link into the currently active assembly. This will create dynamic links to parts/bodies/primitives/assemblies. To insert external objects, make sure that the file is <b>open in the current session</b>",
58
            )
59
            + "</p><p><ul><li>"
60
            + QT_TRANSLATE_NOOP("Assembly_InsertLink", "Insert by left clicking items in the list.")
61
            + "</li><li>"
62
            + QT_TRANSLATE_NOOP(
63
                "Assembly_InsertLink", "Remove by right clicking items in the list."
64
            )
65
            + "</li><li>"
66
            + QT_TRANSLATE_NOOP(
67
                "Assembly_InsertLink",
68
                "Press shift to add several links while clicking on the view.",
69
            )
70
            + "</li></ul></p>",
71
            "CmdType": "ForEdit",
72
        }
73

74
    def IsActive(self):
75
        return UtilsAssembly.isAssemblyCommandActive()
76

77
    def Activated(self):
78
        assembly = UtilsAssembly.activeAssembly()
79
        if not assembly:
80
            return
81
        view = Gui.activeDocument().activeView()
82

83
        self.panel = TaskAssemblyInsertLink(assembly, view)
84
        Gui.Control.showDialog(self.panel)
85

86

87
class TaskAssemblyInsertLink(QtCore.QObject):
88
    def __init__(self, assembly, view):
89
        super().__init__()
90

91
        self.assembly = assembly
92
        self.view = view
93
        self.doc = App.ActiveDocument
94

95
        self.form = Gui.PySideUic.loadUi(":/panels/TaskAssemblyInsertLink.ui")
96
        self.form.installEventFilter(self)
97
        self.form.partList.installEventFilter(self)
98

99
        pref = Preferences.preferences()
100
        self.form.CheckBox_InsertInParts.setChecked(pref.GetBool("InsertInParts", True))
101

102
        # Actions
103
        self.form.openFileButton.clicked.connect(self.openFiles)
104
        self.form.partList.itemClicked.connect(self.onItemClicked)
105
        self.form.filterPartList.textChanged.connect(self.onFilterChange)
106

107
        self.allParts = []
108
        self.partsDoc = []
109
        self.translation = 0
110
        self.partMoving = False
111
        self.totalTranslation = App.Vector()
112
        self.groundedObj = None
113

114
        self.insertionStack = []  # used to handle cancellation of insertions.
115

116
        self.buildPartList()
117

118
        App.setActiveTransaction("Insert Link")
119

120
    def accept(self):
121
        self.deactivated()
122

123
        if self.partMoving:
124
            self.endMove()
125

126
        App.closeActiveTransaction()
127
        return True
128

129
    def reject(self):
130
        self.deactivated()
131

132
        if self.partMoving:
133
            self.dismissPart()
134

135
        App.closeActiveTransaction(True)
136
        return True
137

138
    def deactivated(self):
139
        pref = Preferences.preferences()
140
        pref.SetBool("InsertInParts", self.form.CheckBox_InsertInParts.isChecked())
141

142
    def buildPartList(self):
143
        self.allParts.clear()
144
        self.partsDoc.clear()
145

146
        docList = App.listDocuments().values()
147

148
        for doc in docList:
149
            if UtilsAssembly.isDocTemporary(doc):
150
                continue
151

152
            # Build list of current assembly's parents, including the current assembly itself
153
            parents = self.assembly.Parents
154
            if parents:
155
                root_parent, sub = parents[0]
156
                parents_names, _ = UtilsAssembly.getObjsNamesAndElement(root_parent.Name, sub)
157
            else:
158
                parents_names = [self.assembly.Name]
159

160
            for obj in doc.findObjects("App::Part"):
161
                # we don't want to link to itself or parents.
162
                if obj.Name not in parents_names:
163
                    self.allParts.append(obj)
164
                    self.partsDoc.append(doc)
165

166
            for obj in doc.findObjects("Part::Feature"):
167
                # but only those at top level (not nested inside other containers)
168
                if obj.getParentGeoFeatureGroup() is None:
169
                    self.allParts.append(obj)
170
                    self.partsDoc.append(doc)
171

172
        self.form.partList.clear()
173
        for part in self.allParts:
174
            newItem = QtGui.QListWidgetItem()
175
            newItem.setText(part.Label + " (" + part.Document.Name + ".FCStd)")
176
            newItem.setIcon(part.ViewObject.Icon)
177
            self.form.partList.addItem(newItem)
178

179
    def onFilterChange(self):
180
        filter_str = self.form.filterPartList.text().strip().lower()
181

182
        for i in range(self.form.partList.count()):
183
            item = self.form.partList.item(i)
184
            item_text = item.text().lower()
185

186
            # Check if the item's text contains the filter string
187
            is_visible = filter_str in item_text if filter_str else True
188

189
            item.setHidden(not is_visible)
190

191
    def openFiles(self):
192
        selected_files, _ = QtGui.QFileDialog.getOpenFileNames(
193
            None,
194
            "Select FreeCAD documents to import parts from",
195
            "",
196
            "Supported Formats (*.FCStd *.fcstd);;All files (*)",
197
        )
198

199
        for filename in selected_files:
200
            requested_file = os.path.split(filename)[1]
201
            import_doc_is_open = any(
202
                requested_file == os.path.split(doc.FileName)[1]
203
                for doc in App.listDocuments().values()
204
            )
205

206
            if not import_doc_is_open:
207
                if filename.lower().endswith(".fcstd"):
208
                    App.openDocument(filename)
209
                    App.setActiveDocument(self.doc.Name)
210
                    self.buildPartList()
211

212
    def onItemClicked(self, item):
213
        for selected in self.form.partList.selectedIndexes():
214
            selectedPart = self.allParts[selected.row()]
215
        if not selectedPart:
216
            return
217

218
        if self.partMoving:
219
            self.endMove()
220

221
        # check that the current document had been saved or that it's the same document as that of the selected part
222
        if not self.doc.FileName != "" and not self.doc == selectedPart.Document:
223
            msgBox = QtWidgets.QMessageBox()
224
            msgBox.setIcon(QtWidgets.QMessageBox.Warning)
225
            msgBox.setText("The current document must be saved before inserting external parts.")
226
            msgBox.setWindowTitle("Save Document")
227
            saveButton = msgBox.addButton("Save", QtWidgets.QMessageBox.AcceptRole)
228
            cancelButton = msgBox.addButton("Cancel", QtWidgets.QMessageBox.RejectRole)
229

230
            msgBox.exec_()
231

232
            if not (msgBox.clickedButton() == saveButton and Gui.ActiveDocument.saveAs()):
233
                return
234

235
        objectWhereToInsert = self.assembly
236

237
        if self.form.CheckBox_InsertInParts.isChecked() and selectedPart.TypeId != "App::Part":
238
            objectWhereToInsert = self.assembly.newObject("App::Part", "Part_" + selectedPart.Label)
239

240
        createdLink = objectWhereToInsert.newObject("App::Link", selectedPart.Label)
241
        createdLink.LinkedObject = selectedPart
242
        createdLink.recompute()
243

244
        addedObject = createdLink
245
        if self.form.CheckBox_InsertInParts.isChecked() and selectedPart.TypeId != "App::Part":
246
            addedObject = objectWhereToInsert
247

248
        insertionDict = {}
249
        insertionDict["item"] = item
250
        insertionDict["addedObject"] = addedObject
251
        self.insertionStack.append(insertionDict)
252
        self.increment_counter(item)
253

254
        translation = self.getTranslationVec(addedObject)
255
        insertionDict["translation"] = translation
256
        self.totalTranslation += translation
257
        addedObject.Placement.Base = self.totalTranslation
258

259
        # highlight the link
260
        Gui.Selection.clearSelection()
261
        Gui.Selection.addSelection(self.doc.Name, addedObject.Name, "")
262

263
        # Start moving the part if user brings mouse on view
264
        self.initMove()
265

266
        self.form.partList.setItemSelected(item, False)
267

268
        if len(self.insertionStack) == 1 and not UtilsAssembly.isAssemblyGrounded():
269
            self.handleFirstInsertion()
270

271
    def handleFirstInsertion(self):
272
        pref = Preferences.preferences()
273
        fixPart = False
274
        fixPartPref = pref.GetInt("GroundFirstPart", 0)
275
        if fixPartPref == 0:  # unset
276
            msgBox = QtWidgets.QMessageBox()
277
            msgBox.setWindowTitle("Ground Part?")
278
            msgBox.setText(
279
                "Do you want to ground the first inserted part automatically?\nYou need at least one grounded part in your assembly."
280
            )
281
            msgBox.setIcon(QtWidgets.QMessageBox.Question)
282

283
            yesButton = msgBox.addButton("Yes", QtWidgets.QMessageBox.YesRole)
284
            noButton = msgBox.addButton("No", QtWidgets.QMessageBox.NoRole)
285
            yesAlwaysButton = msgBox.addButton("Always", QtWidgets.QMessageBox.YesRole)
286
            noAlwaysButton = msgBox.addButton("Never", QtWidgets.QMessageBox.NoRole)
287

288
            msgBox.exec_()
289

290
            clickedButton = msgBox.clickedButton()
291
            if clickedButton == yesButton:
292
                fixPart = True
293
            elif clickedButton == yesAlwaysButton:
294
                fixPart = True
295
                pref.SetInt("GroundFirstPart", 1)
296
            elif clickedButton == noAlwaysButton:
297
                pref.SetInt("GroundFirstPart", 2)
298

299
        elif fixPartPref == 1:  # Yes always
300
            fixPart = True
301

302
        if fixPart:
303
            # Create groundedJoint.
304
            if len(self.insertionStack) != 1:
305
                return
306

307
            self.groundedObj = self.insertionStack[0]["addedObject"]
308
            self.groundedJoint = CommandCreateJoint.createGroundedJoint(self.groundedObj)
309
            self.endMove()
310

311
    def increment_counter(self, item):
312
        text = item.text()
313
        match = re.search(r"(\d+) inserted$", text)
314

315
        if match:
316
            # Counter exists, increment it
317
            counter = int(match.group(1)) + 1
318
            new_text = re.sub(r"\d+ inserted$", f"{counter} inserted", text)
319
        else:
320
            # Counter does not exist, add it
321
            new_text = f"{text} : 1 inserted"
322

323
        item.setText(new_text)
324

325
    def decrement_counter(self, item):
326
        text = item.text()
327
        match = re.search(r"(\d+) inserted$", text)
328

329
        if match:
330
            counter = int(match.group(1)) - 1
331
            if counter > 0:
332
                # Update the counter
333
                new_text = re.sub(r"\d+ inserted$", f"{counter} inserted", text)
334
            elif counter == 0:
335
                # Remove the counter part from the text
336
                new_text = re.sub(r" : \d+ inserted$", "", text)
337
            else:
338
                return
339

340
            item.setText(new_text)
341

342
    def initMove(self):
343
        self.callbackMove = self.view.addEventCallback("SoLocation2Event", self.moveMouse)
344
        self.callbackClick = self.view.addEventCallback("SoMouseButtonEvent", self.clickMouse)
345
        self.callbackKey = self.view.addEventCallback("SoKeyboardEvent", self.KeyboardEvent)
346
        self.partMoving = True
347

348
        # Selection filter to avoid selecting the part while it's moving
349
        # filter = Gui.Selection.Filter('SELECT ???')
350
        # Gui.Selection.addSelectionGate(filter)
351

352
    def endMove(self):
353
        self.view.removeEventCallback("SoLocation2Event", self.callbackMove)
354
        self.view.removeEventCallback("SoMouseButtonEvent", self.callbackClick)
355
        self.view.removeEventCallback("SoKeyboardEvent", self.callbackKey)
356
        self.partMoving = False
357
        self.doc.recompute()
358
        # Gui.Selection.removeSelectionGate()
359

360
    def moveMouse(self, info):
361
        newPos = self.view.getPoint(*info["Position"])
362
        self.insertionStack[-1]["addedObject"].Placement.Base = newPos
363

364
    def clickMouse(self, info):
365
        if info["Button"] == "BUTTON1" and info["State"] == "DOWN":
366
            Gui.Selection.clearSelection()
367
            if info["ShiftDown"]:
368
                # Create a new link and moves this one now
369
                addedObject = self.insertionStack[-1]["addedObject"]
370
                currentPos = addedObject.Placement.Base
371
                selectedPart = addedObject
372
                if addedObject.TypeId == "App::Link":
373
                    selectedPart = addedObject.LinkedObject
374

375
                addedObject = self.assembly.newObject("App::Link", selectedPart.Label)
376
                addedObject.LinkedObject = selectedPart
377
                addedObject.Placement.Base = currentPos
378

379
                insertionDict = {}
380
                insertionDict["translation"] = App.Vector()
381
                insertionDict["item"] = self.insertionStack[-1]["item"]
382
                insertionDict["addedObject"] = addedObject
383
                self.insertionStack.append(insertionDict)
384

385
            else:
386
                self.endMove()
387

388
        elif info["Button"] == "BUTTON2" and info["State"] == "DOWN":
389
            self.dismissPart()
390

391
    # 3D view keyboard handler
392
    def KeyboardEvent(self, info):
393
        if info["State"] == "UP" and info["Key"] == "ESCAPE":
394
            self.dismissPart()
395

396
    def dismissPart(self):
397
        self.endMove()
398
        stack_item = self.insertionStack.pop()
399
        self.totalTranslation -= stack_item["translation"]
400
        UtilsAssembly.removeObjAndChilds(stack_item["addedObject"])
401
        self.decrement_counter(stack_item["item"])
402

403
    # Taskbox keyboard event handler
404
    def eventFilter(self, watched, event):
405
        if watched == self.form and event.type() == QtCore.QEvent.KeyPress:
406
            if event.key() == QtCore.Qt.Key_Escape and self.partMoving:
407
                self.dismissPart()
408
                return True  # Consume the event
409

410
        if event.type() == QtCore.QEvent.ContextMenu and watched is self.form.partList:
411
            item = watched.itemAt(event.pos())
412

413
            if item:
414
                # Iterate through the insertionStack in reverse
415
                for i in reversed(range(len(self.insertionStack))):
416
                    stack_item = self.insertionStack[i]
417

418
                    if stack_item["item"] == item:
419
                        if self.partMoving:
420
                            self.endMove()
421

422
                        self.totalTranslation -= stack_item["translation"]
423
                        obj = stack_item["addedObject"]
424
                        if self.groundedObj == obj:
425
                            self.groundedJoint.Document.removeObject(self.groundedJoint.Name)
426
                        UtilsAssembly.removeObjAndChilds(obj)
427

428
                        self.decrement_counter(item)
429
                        del self.insertionStack[i]
430
                        self.form.partList.setItemSelected(item, False)
431

432
                        return True
433

434
        return super().eventFilter(watched, event)
435

436
    def getTranslationVec(self, part):
437
        bb = part.Shape.BoundBox
438
        if bb:
439
            translation = (bb.XMax + bb.YMax + bb.ZMax) * 0.15
440
        else:
441
            translation = 10
442
        return App.Vector(translation, translation, translation)
443

444

445
if App.GuiUp:
446
    Gui.addCommand("Assembly_InsertLink", CommandInsertLink())
447

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

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

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

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