diff --git a/lasercut/autodetect.py b/lasercut/autodetect.py new file mode 100644 index 0000000..3463144 --- /dev/null +++ b/lasercut/autodetect.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python + +# *************************************************************************** +# * * +# * Copyright (c) 2016 execuc * +# * * +# * This file is part of LCInterlocking module. * +# * LCInterlocking module 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. * +# * * +# * This module 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 this library; if not, write to the Free Software * +# * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * +# * MA 02110-1301 USA * +# * * +# *************************************************************************** + +from lasercut import helper +from lasercut.tabproperties import TabProperties + +# Extra depth added past the neighboring part's own thickness when probing for +# a physical intersection, to absorb tiny numerical/zero-gap edge cases. +PROBE_MARGIN = 0.2 +MIN_INTERSECT_VOLUME = 0.001 + + +class CandidateFace: + def __init__(self, freecad_obj, face_index, face, y_length, thickness): + self.freecad_obj = freecad_obj + self.face_index = face_index + self.face = face + self.face_name = "Face%d" % (face_index + 1) + self.y_length = y_length + self.thickness = thickness + + +def collect_candidate_faces(freecad_objects): + candidates = [] + for obj in freecad_objects: + for index, face in enumerate(obj.Shape.Faces): + try: + normal, y_local, z_local = helper.get_local_axis(face) + except Exception: + # get_local_axis assumes a planar quad face; skip anything else + # (curved faces, fillets, holes, ...) rather than crashing the scan. + continue + if normal is None: + continue + candidates.append(CandidateFace(obj, index, face, y_local.Length, z_local.Length)) + return candidates + + +def _intersecting_parts(candidate, freecad_objects, thickness_by_name): + normal = candidate.face.normalAt(0, 0).normalize() + matches = [] + for obj in freecad_objects: + if obj is candidate.freecad_obj: + continue + depth = thickness_by_name.get(obj.Name, candidate.thickness) + PROBE_MARGIN + try: + probe = candidate.face.extrude(normal * depth) + volume = obj.Shape.common(probe).Volume + except Exception: + continue + if volume > MIN_INTERSECT_VOLUME: + matches.append(obj) + return matches + + +def find_connections(freecad_objects, thickness_by_name): + candidates = collect_candidate_faces(freecad_objects) + + ambiguous = [] + unmatched = [] + best_by_pair = {} + for candidate in candidates: + matches = _intersecting_parts(candidate, freecad_objects, thickness_by_name) + if len(matches) == 0: + unmatched.append(candidate) + elif len(matches) > 1: + ambiguous.append(candidate) + else: + target = matches[0] + pair_key = frozenset([candidate.freecad_obj.Name, target.Name]) + existing = best_by_pair.get(pair_key) + # Both sides of a T-joint pass the test; keep the smaller face as the tab face. + if existing is None or candidate.face.Area < existing[0].face.Area: + best_by_pair[pair_key] = (candidate, target) + connections = list(best_by_pair.values()) + return connections, ambiguous, unmatched + + +def compute_tab_sizing(y_length, desired_tab_width, tab_type): + if tab_type == TabProperties.TYPE_CONTINUOUS: + # tabs_number counts every alternating segment here, tab and gap alike. + tabs_number = max(2, int(round(y_length / desired_tab_width))) + return tabs_number, desired_tab_width + + # tabs_number counts only the solid tabs, so double the period to leave a gap of roughly the same size. + tabs_number = max(1, int(round(y_length / (2. * desired_tab_width)))) + tabs_width = desired_tab_width + while tabs_number > 1 and tabs_number * tabs_width > y_length: + tabs_number -= 1 + return tabs_number, tabs_width diff --git a/lasercut/helper.py b/lasercut/helper.py index 5a58f21..778e2b9 100644 --- a/lasercut/helper.py +++ b/lasercut/helper.py @@ -258,6 +258,8 @@ def sort_quad_vertex(list_edges, reverse): def biggest_area_faces(freecad_shape): sorted_list = sort_area_shape_faces(freecad_shape) + if not sorted_list: + raise ValueError("Shape has no usable faces (check the shape is valid and not empty)") biggest_area_face = sorted_list[-1] # contains : 0:normal, 1:area mm2, 2; list of faces return biggest_area_face @@ -265,6 +267,8 @@ def biggest_area_faces(freecad_shape): def smallest_area_faces(freecad_shape): sorted_list = sort_area_shape_faces(freecad_shape) + if not sorted_list: + raise ValueError("Shape has no usable faces (check the shape is valid and not empty)") smallest_area_face = sorted_list[0] # contains : 0:normal, 1:area mm2, 2; list of faces return smallest_area_face diff --git a/lasercut/material.py b/lasercut/material.py index 5c76142..1b9379c 100644 --- a/lasercut/material.py +++ b/lasercut/material.py @@ -75,9 +75,14 @@ def __init__(self, **kwargs): def recomputeInit(self, freecad_obj): self.freecad_object = freecad_obj - thickness = retrieve_thickness_from_biggest_face(freecad_obj) + try: + thickness = retrieve_thickness_from_biggest_face(freecad_obj) + except ValueError as e: + FreeCAD.Console.PrintError(e) + return if compare_value(thickness, self.thickness) is False: - FreeCAD.Console.PrintError("Recomputed thickness for %s is different (%f != %f)\n" % (self.name, thickness, self.thickness)) + FreeCAD.Console.PrintMessage("Thickness for %s updated (%f -> %f)\n" % (self.name, self.thickness, thickness)) + self.thickness = thickness # Prendre la normal la plus présente en terme de surface (biggest_area_faces) diff --git a/panel/crosspiece.py b/panel/crosspiece.py index 20ad1c8..eb2ed42 100644 --- a/panel/crosspiece.py +++ b/panel/crosspiece.py @@ -27,7 +27,7 @@ from FreeCAD import Gui, Matrix import os from lasercut.crosspart import make_cross_parts -from panel.treepanel import TreePanel, PREVIEW_NONE, PREVIEW_NORMAL, PREVIEW_FAST +from panel.treepanel import TreePanel, PREVIEW_NONE, PREVIEW_NORMAL, PREVIEW_FAST, OriginalPartsGroup, OriginalPartsGroupViewProvider from panel.propertieslist import PropertiesList import json import copy @@ -44,6 +44,7 @@ def __init__(self, obj): obj.addProperty('App::PropertyPythonObject', 'preview').preview = PREVIEW_NONE obj.addProperty('App::PropertyLinkList', 'generatedParts').generatedParts = [] obj.addProperty('App::PropertyLinkList', 'fromParts').fromParts = [] + obj.addProperty('App::PropertyLink', 'originFolder').originFolder = None obj.addProperty('App::PropertyPythonObject', 'edit').edit = False obj.addProperty('App::PropertyPythonObject', 'namesMapping').namesMapping = {} obj.Proxy = self @@ -110,28 +111,28 @@ def execute(self, fp): fp.need_recompute = False document = fp.Document - if len(fp.fromParts) > 0: - groupObj = fp.fromParts[0] - else: - groupObj = document.addObject("App::DocumentObjectGroup", str(fp.Name) + "_origin_parts") - - subObjectList = groupObj.Group - for subObj in subObjectList: - groupObj.removeObject(subObj) - fp.fromParts = [] parts = [] freedac_origin_obj = [] - freedac_origin_obj.append(groupObj) for part in fp.parts.lst: cp_part = copy.deepcopy(part) freecad_obj = document.getObject(cp_part.name) freedac_origin_obj.append(freecad_obj) cp_part.recomputeInit(freecad_obj) - groupObj.addObject(freecad_obj) + part.thickness = cp_part.thickness parts.append(cp_part) fp.fromParts = freedac_origin_obj + + if not hasattr(fp, "originFolder"): + fp.addProperty('App::PropertyLink', 'originFolder').originFolder = None + if fp.originFolder is None: + origin_folder = document.addObject("App::FeaturePython", str(fp.Name) + "_origin_parts") + OriginalPartsGroup(origin_folder) + OriginalPartsGroupViewProvider(origin_folder.ViewObject) + fp.originFolder = origin_folder + fp.originFolder.parts = freedac_origin_obj + computed_parts = make_cross_parts(parts) previous_nameMapping = copy.copy(fp.namesMapping) @@ -198,10 +199,20 @@ def attach(self, vobj): self.Object = vobj.Object def claimChildren(self): - if len(self.Object.fromParts) > 0: - return [self.Object.fromParts[0]] + self.Object.generatedParts - else: - return [] + children = [] + if hasattr(self.Object, "originFolder") and self.Object.originFolder is not None: + children.append(self.Object.originFolder) + return children + list(self.Object.generatedParts) + + def onDelete(self, *args): + document = self.Object.Document + for obj in self.Object.fromParts: + obj.ViewObject.show() + for obj in list(self.Object.generatedParts): + document.removeObject(obj.Name) + if hasattr(self.Object, "originFolder") and self.Object.originFolder is not None: + document.removeObject(self.Object.originFolder.Name) + return True class CrossPiece(TreePanel): diff --git a/panel/multiplejoins.py b/panel/multiplejoins.py index 287f2ff..9763896 100644 --- a/panel/multiplejoins.py +++ b/panel/multiplejoins.py @@ -27,7 +27,7 @@ from FreeCAD import Gui, Matrix import os from lasercut.join import make_tabs_joins -from panel.treepanel import TreePanel, PREVIEW_NONE, PREVIEW_NORMAL, PREVIEW_FAST +from panel.treepanel import TreePanel, PREVIEW_NONE, PREVIEW_NORMAL, PREVIEW_FAST, OriginalPartsGroup, OriginalPartsGroupViewProvider from panel.propertieslist import PropertiesList import json import copy @@ -44,6 +44,7 @@ def __init__(self, obj): obj.addProperty('App::PropertyPythonObject', 'preview').preview = PREVIEW_NONE obj.addProperty('App::PropertyLinkList', 'generatedParts').generatedParts = [] obj.addProperty('App::PropertyLinkList', 'fromParts').fromParts = [] + obj.addProperty('App::PropertyLink', 'originFolder').originFolder = None obj.addProperty('App::PropertyPythonObject', 'edit').edit = False obj.addProperty('App::PropertyPythonObject', 'namesMapping').namesMapping = {} obj.Proxy = self @@ -120,29 +121,28 @@ def execute(self, fp): fp.need_recompute = False document = fp.Document - if len(fp.fromParts) > 0: - groupObj = fp.fromParts[0] - else: - groupObj = document.addObject("App::DocumentObjectGroup", str(fp.Name) + "_origin_parts") - - subObjectList = groupObj.Group - for subObj in subObjectList: - groupObj.removeObject(subObj) - fp.fromParts = [] parts = [] freedac_origin_obj = [] - freedac_origin_obj.append(groupObj) for part in fp.parts.lst: cp_part = copy.deepcopy(part) freecad_obj = document.getObject(cp_part.name) freedac_origin_obj.append(freecad_obj) cp_part.recomputeInit(freecad_obj) - groupObj.addObject(freecad_obj) + part.thickness = cp_part.thickness parts.append(cp_part) fp.fromParts = freedac_origin_obj + if not hasattr(fp, "originFolder"): + fp.addProperty('App::PropertyLink', 'originFolder').originFolder = None + if fp.originFolder is None: + origin_folder = document.addObject("App::FeaturePython", str(fp.Name) + "_origin_parts") + OriginalPartsGroup(origin_folder) + OriginalPartsGroupViewProvider(origin_folder.ViewObject) + fp.originFolder = origin_folder + fp.originFolder.parts = freedac_origin_obj + tabs = [] for tab in fp.faces.lst: cp_tab = copy.deepcopy(tab) @@ -217,10 +217,20 @@ def attach(self, vobj): self.Object = vobj.Object def claimChildren(self): - if len(self.Object.fromParts) > 0: - return [self.Object.fromParts[0]] + self.Object.generatedParts - else: - return [] + children = [] + if hasattr(self.Object, "originFolder") and self.Object.originFolder is not None: + children.append(self.Object.originFolder) + return children + list(self.Object.generatedParts) + + def onDelete(self, *args): + document = self.Object.Document + for obj in self.Object.fromParts: + obj.ViewObject.show() + for obj in list(self.Object.generatedParts): + document.removeObject(obj.Name) + if hasattr(self.Object, "originFolder") and self.Object.originFolder is not None: + document.removeObject(self.Object.originFolder.Name) + return True class MultipleJoins(TreePanel): diff --git a/panel/partmat.py b/panel/partmat.py index 1ae7b15..5735b00 100644 --- a/panel/partmat.py +++ b/panel/partmat.py @@ -124,13 +124,9 @@ def append_link(self, freecad_object, freecad_object_src): return self.part_list[-1] - def remove(self, name): + def remove(self, name, batch_names=frozenset()): + self._promote_link(name, batch_names) found_index = None - linked_parts = self.get_linked_parts(name) - if len(linked_parts) > 0: - FreeCAD.Console.PrintError('Some parts are linked to this part %s\n' % name) - raise ValueError('Some parts are linked to this part %s' % name) - for index in range(len(self.part_list)): if self.part_list[index].name == name: found_index = index @@ -138,6 +134,33 @@ def remove(self, name): if found_index is not None: self.part_list.pop(found_index) + def _merged_from_origin(self, origin, link_part): + new_part = copy.deepcopy(origin) + new_part.name = link_part.name + new_part.label = link_part.label + new_part.new_name = link_part.new_name + new_part.link_name = link_part.link_name + return new_part + + def _promote_link(self, name, batch_names): + linked_names = [n for n in self.get_linked_parts(name) if n not in batch_names] + if len(linked_names) == 0: + return + origin, widget = self.get(name) + promoted_name = linked_names[0] + for index in range(len(self.part_list.lst)): + if self.part_list.lst[index].name == promoted_name: + promoted = self._merged_from_origin(origin, self.part_list.lst[index]) + promoted.link_name = "" + self.part_list.lst[index] = promoted + break + for other_name in linked_names[1:]: + for index in range(len(self.part_list.lst)): + if self.part_list.lst[index].name == other_name: + self.part_list.lst[index].link_name = promoted_name + break + FreeCAD.Console.PrintMessage("%s promoted to origin of the linked parts group\n" % promoted_name) + def get_linked_parts(self, name): el_list = [] part_lst = self.part_list.lst @@ -172,12 +195,7 @@ def get_parts_properties(self): for part in self.part_list.lst: if part.link_name: part_link, widget = self.get(part.link_name) - new_part = copy.deepcopy(part_link) - new_part.new_name = part.new_name - new_part.name = part.name - new_part.link_name = part.link_name - - part_properties.append(new_part) + part_properties.append(self._merged_from_origin(part_link, part)) else: part_properties.append(copy.deepcopy(part)) diff --git a/panel/selection.py b/panel/selection.py index a73dc7a..63123aa 100644 --- a/panel/selection.py +++ b/panel/selection.py @@ -25,19 +25,30 @@ import FreeCADGui +def resolve_canonical_object(obj): + """Resolve a PartDesign Tip/sub-feature to its owning Body.""" + if obj is None or obj.TypeId == "PartDesign::Body": + return obj + for parent in obj.InList: + if parent.TypeId == "PartDesign::Body" and obj in parent.Group: + return parent + return obj + + def get_freecad_objects_list(): objs_sel = [] for selection in FreeCADGui.Selection.getSelectionEx(): - objs_sel.append(selection.Object) + objs_sel.append(resolve_canonical_object(selection.Object)) return objs_sel def get_freecad_faces_objects_list(): face_obj_list = [] for selection_obj in FreeCADGui.Selection.getSelectionEx(): + canonical_obj = resolve_canonical_object(selection_obj.Object) index = 0 for face in selection_obj.SubObjects: - face_obj_list.append({'freecad_object': selection_obj.Object, 'face': face, + face_obj_list.append({'freecad_object': canonical_obj, 'face': face, 'name': selection_obj.SubElementNames[index]}) index += 1 return face_obj_list diff --git a/panel/tab.py b/panel/tab.py index 723b063..198debb 100644 --- a/panel/tab.py +++ b/panel/tab.py @@ -192,12 +192,9 @@ def append_link(self, face, src_tab_name): #self.faces_widget_list.append(TabLink(tab_properties)) return self.faces[-1]#, self.faces_widget_list[-1] - def remove(self, name): + def remove(self, name, batch_names=frozenset()): + self._promote_link(name, batch_names) found_index = None - linked_tabs = self.get_linked_tabs(name) - if len(linked_tabs) > 0: - raise ValueError('Some tabs are linked to this part %s' % name) - for index in range(len(self.faces)): if self.faces[index].tab_name == name: found_index = index @@ -206,6 +203,38 @@ def remove(self, name): if found_index is not None: self.faces.pop(found_index) + def _merged_from_origin(self, origin, link_tab): + new_tab = copy.deepcopy(origin) + new_tab.link_name = link_tab.link_name + new_tab.tab_name = link_tab.tab_name + new_tab.face_name = link_tab.face_name + new_tab.description = link_tab.description + new_tab.freecad_obj_name = link_tab.freecad_obj_name + new_tab.y_invert = link_tab.y_invert + new_tab.transform_matrix = link_tab.transform_matrix + new_tab.thickness = link_tab.thickness + new_tab.y_length = link_tab.y_length + return new_tab + + def _promote_link(self, name, batch_names): + linked = [tab for tab in self.get_linked_tabs(name) if tab.tab_name not in batch_names] + if len(linked) == 0: + return + origin, widget = self.get(name) + promoted_tab_name = linked[0].tab_name + for index in range(len(self.faces.lst)): + if self.faces.lst[index].tab_name == promoted_tab_name: + promoted = self._merged_from_origin(origin, self.faces.lst[index]) + promoted.link_name = "" + self.faces.lst[index] = promoted + break + for other in linked[1:]: + for index in range(len(self.faces.lst)): + if self.faces.lst[index].tab_name == other.tab_name: + self.faces.lst[index].link_name = promoted_tab_name + break + FreeCAD.Console.PrintMessage("%s promoted to origin of the linked faces group\n" % promoted_tab_name) + def exist(self, name): for part in self.faces: if part.tab_name == name: @@ -230,7 +259,7 @@ def get(self, name): def get_linked_tabs(self, name): el_list = [] for tab in self.faces: - if isinstance(tab, TabLink) and tab.link_name == name: + if tab.link_name == name: el_list.append(tab) return el_list @@ -242,18 +271,7 @@ def get_tabs_properties(self): for tab in self.faces.lst: if tab.link_name: tab_link, widget = self.get(tab.link_name) - new_tab = copy.deepcopy(tab_link) - new_tab.link_name = tab.link_name - new_tab.tab_name = tab.tab_name - new_tab.face_name = tab.face_name - new_tab.description = tab.description - new_tab.freecad_obj_name = tab.freecad_obj_name - new_tab.y_invert = tab.y_invert - new_tab.transform_matrix = tab.transform_matrix - new_tab.thickness = tab.thickness - new_tab.y_length = tab.y_length - - tabs_properties.append(new_tab) + tabs_properties.append(self._merged_from_origin(tab_link, tab)) else: tabs_properties.append(copy.deepcopy(tab)) diff --git a/panel/treepanel.py b/panel/treepanel.py index c9c0121..3b3cee5 100644 --- a/panel/treepanel.py +++ b/panel/treepanel.py @@ -38,6 +38,7 @@ from lasercut.tabproperties import TabProperties from panel.treeview import TreeModel, TreeItem from panel.propertieslist import PropertiesList +from lasercut import autodetect PREVIEW_NONE = 0 @@ -90,14 +91,17 @@ def __init__(self, title, obj_join = None): #none to be removed self.other_object_list = [] self.save_initial_objects() + self.rebuild_tree() + + def rebuild_tree(self): + self.model.clear() for item in self.parts: self.model.append_part(item.name, item.label, bool(item.link_name)) - for item in self.faces: self.model.append_tab(item.freecad_obj_name, item.tab_name, item.face_name, bool(item.link_name)) def getStandardButtons(self): - return int(QtGui.QDialogButtonBox.Ok | QtGui.QDialogButtonBox.Cancel) + return QtGui.QDialogButtonBox.Ok | QtGui.QDialogButtonBox.Cancel def accept(self): raise ValueError("Must overloaded") @@ -144,6 +148,18 @@ def init_tree_widget(self): h_box.addWidget(add_faces_button) h_box.addWidget(add_same_faces_button) self.tree_vbox.addLayout(h_box) + # Auto button + h_box = QtGui.QHBoxLayout() + h_box.addWidget(QtGui.QLabel('Tab width:', self.tree_widget)) + self.auto_tab_width_box = QtGui.QDoubleSpinBox(self.tree_widget) + self.auto_tab_width_box.setRange(1., 300.) + self.auto_tab_width_box.setDecimals(2) + self.auto_tab_width_box.setValue(10.) + h_box.addWidget(self.auto_tab_width_box) + auto_button = QtGui.QPushButton('Auto-add faces', self.tree_widget) + auto_button.clicked.connect(self.auto_configure) + h_box.addWidget(auto_button) + self.tree_vbox.addLayout(h_box) # tree self.selection_model = self.tree_view_widget.selectionModel() self.selection_model.selectionChanged.connect(self.selection_changed) @@ -204,35 +220,46 @@ def remove_items(self): if len(indexes) == 0: FreeCAD.Console.PrintWarning("Nothing to remove\n") return - parent_test_name = indexes[0].internalPointer().parent().get_name() - for index in indexes:#[1:]: - if index.internalPointer().parent().get_name() != parent_test_name: - FreeCAD.Console.PrintError("No same level delete") - return False - elif index.internalPointer().child_count() > 0: - FreeCAD.Console.PrintError("%s has children" % index.internalPointer().get_name()) - return False - for index in indexes: - item = index.internalPointer() - if item.type == TreeItem.PART and len(self.partsList.get_linked_parts(item.get_name())) > 0: - FreeCAD.Console.PrintError('Some parts are linked to this part %s\n' % item.get_name()) - return False - elif item.type == TreeItem.TAB and len(self.tabsList.get_linked_tabs(item.get_name())) > 0: - FreeCAD.Console.PrintError('Some tabs are linked to this tab %s\n' % item.get_name()) - return False - for index in indexes: - item = index.internalPointer() - if item.type == TreeItem.PART or item.type == TreeItem.PART_LINK: - self.partsList.remove(item.get_name()) - elif item.type == TreeItem.TAB or item.type == TreeItem.TAB_LINK: - self.tabsList.remove(item.get_name()) - else: - FreeCAD.Console.PrintError("Unknown deleter item") - rows = sorted(set(index.row() for index in indexes)) - for row in reversed(rows): - self.model.removeRow(row, indexes[0].parent()) + # Cascading delete: removing a part also removes its child faces. + items_to_remove = [] + seen_ids = set() + def collect(tree_item): + if id(tree_item) in seen_ids: + return + seen_ids.add(id(tree_item)) + items_to_remove.append(tree_item) + for child in list(tree_item.childItems): + collect(child) + + for index in indexes: + collect(index.internalPointer()) + + # An origin still linked from outside this batch is promoted to a new + # origin (PartsList/TabsList.remove) rather than blocking the removal. + names_to_remove = set(item.get_name() for item in items_to_remove) + + remaining = items_to_remove + while remaining: + still_remaining = [] + for item in remaining: + try: + if item.type == TreeItem.PART or item.type == TreeItem.PART_LINK: + self.partsList.remove(item.get_name(), names_to_remove) + elif item.type == TreeItem.TAB or item.type == TreeItem.TAB_LINK: + self.tabsList.remove(item.get_name(), names_to_remove) + else: + FreeCAD.Console.PrintError("Unknown deleter item") + except ValueError: + still_remaining.append(item) + if len(still_remaining) == len(remaining): + for item in still_remaining: + FreeCAD.Console.PrintError("Could not remove %s\n" % item.get_name()) + break + remaining = still_remaining + + self.rebuild_tree() return def check_faces(self, faces): @@ -281,7 +308,83 @@ def add_same_tabs(self): self.force_selection(index) return + def auto_configure(self): + self.check_is_in_active_view() + freecad_objects = [] + thickness_by_name = {} + for material in self.partsList: + freecad_obj = self.active_document.getObject(material.name) + if freecad_obj is None: + FreeCAD.Console.PrintWarning("Part %s no longer exists in the document\n" % material.name) + continue + freecad_objects.append(freecad_obj) + thickness_by_name[freecad_obj.Name] = material.thickness + if len(freecad_objects) == 0: + FreeCAD.Console.PrintWarning("No parts added yet\n") + return + + desired_width = self.auto_tab_width_box.value() + tab_type = self.tab_type_box.currentText() + connections, ambiguous, unmatched = autodetect.find_connections(freecad_objects, thickness_by_name) + + # Skip connections where the face is already configured, so a repeated + # Auto run never disturbs faces added or edited manually before it. + filtered = [] + for candidate, target in connections: + tab_name = "%s.%s" % (candidate.freecad_obj.Name, candidate.face_name) + if self.tabsList.exist(tab_name): + continue + tabs_number, tabs_width = autodetect.compute_tab_sizing(candidate.y_length, desired_width, tab_type) + filtered.append((candidate, target, tabs_number, tabs_width)) + + # Group by size so same-size connections are linked (edit one, edit all). + groups = {} + for candidate, target, tabs_number, tabs_width in filtered: + key = (round(candidate.y_length, 1), round(candidate.thickness, 1)) + groups.setdefault(key, []).append((candidate, target, tabs_number, tabs_width)) + + added_count = 0 + linked_group_count = 0 + last_index = None + for key, entries in groups.items(): + origin_candidate, origin_target, tabs_number, tabs_width = entries[0] + face_dict = {'freecad_object': origin_candidate.freecad_obj, + 'face': origin_candidate.face, + 'name': origin_candidate.face_name} + try: + item = self.tabsList.append(face_dict, tab_type) + except ValueError as e: + FreeCAD.Console.PrintError(e) + continue + item.tabs_number = tabs_number + item.tabs_width = tabs_width + last_index = self.model.append_tab(item.freecad_obj_name, item.tab_name, item.face_name) + added_count += 1 + + if len(entries) > 1: + linked_group_count += 1 + for link_candidate, _, _, _ in entries[1:]: + link_dict = {'freecad_object': link_candidate.freecad_obj, + 'face': link_candidate.face, + 'name': link_candidate.face_name} + try: + sub_item = self.tabsList.append_link(link_dict, item.tab_name) + except ValueError as e: + FreeCAD.Console.PrintError(e) + continue + self.model.append_tab(sub_item.freecad_obj_name, sub_item.tab_name, sub_item.face_name, True) + added_count += 1 + + FreeCAD.Console.PrintMessage( + "Auto: added %d connection(s) in %d group(s) (%d linked), %d unmatched, %d ambiguous face(s)\n" + % (added_count, len(groups), linked_group_count, len(unmatched), len(ambiguous))) + if last_index is not None: + self.force_selection(last_index) + return + def force_selection(self, index): + if index is None or not index.isValid(): + return self.selection_model.clearSelection() self.selection_model.select(index, QItemSelectionModel.ClearAndSelect | QItemSelectionModel.Rows) @@ -314,9 +417,11 @@ def selection_changed(self, selected, deselected): item = index.internalPointer() tab, widget = self.tabsList.get(item.get_name()) if tab is None: - raise ValueError("No tab named %s", item.get_name()) + FreeCAD.Console.PrintWarning("No tab named %s\n" % item.get_name()) + continue if widget is None: - raise ValueError("No widget named %s", item.get_name()) + FreeCAD.Console.PrintWarning("No widget named %s\n" % item.get_name()) + continue fobj = self.active_document.getObject(tab.freecad_obj_name) FreeCADGui.Selection.addSelection(fobj, tab.face_name) @@ -420,3 +525,38 @@ def check_is_in_active_view(self): def save_link_properties(self): self.partsList.get_parts_properties() self.tabsList.get_tabs_properties() + + +class OriginalPartsGroup: + """Presentational-only folder: claims parts via its own link list, never App::DocumentObjectGroup's Group, so it never reparents them.""" + + def __init__(self, obj): + obj.addProperty('App::PropertyLinkList', 'parts').parts = [] + obj.Proxy = self + + def execute(self, fp): + pass + + +class OriginalPartsGroupViewProvider: + def __init__(self, vobj): + vobj.Proxy = self + + def attach(self, vobj): + self.ViewObject = vobj + self.Object = vobj.Object + + def claimChildren(self): + return list(self.Object.parts) + + def getIcon(self): + return ":/icons/Group.svg" + + def onChanged(self, vp, prop): + pass + + def __getstate__(self): + return None + + def __setstate__(self, state): + return None diff --git a/panel/treeview.py b/panel/treeview.py index 1c75094..b0c31c2 100644 --- a/panel/treeview.py +++ b/panel/treeview.py @@ -86,6 +86,11 @@ def __init__(self, parent=None): super(TreeModel, self).__init__(parent) self.rootItem = TreeItem(TreeItem.ROOT, ["Name", "Label"]) + def clear(self): + self.beginResetModel() + self.rootItem = TreeItem(TreeItem.ROOT, ["Name", "Label"]) + self.endResetModel() + def append_part(self, name, label, is_link=False): row = self.rootItem.child_count() - 1 self.beginInsertRows(QtCore.QModelIndex(), row, row)