Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 111 additions & 0 deletions lasercut/autodetect.py
Original file line number Diff line number Diff line change
@@ -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
4 changes: 4 additions & 0 deletions lasercut/helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -258,13 +258,17 @@ 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


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
Expand Down
9 changes: 7 additions & 2 deletions lasercut/material.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
43 changes: 27 additions & 16 deletions panel/crosspiece.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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):
Expand Down
42 changes: 26 additions & 16 deletions panel/multiplejoins.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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):
Expand Down
42 changes: 30 additions & 12 deletions panel/partmat.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,20 +124,43 @@ 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
break
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
Expand Down Expand Up @@ -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))

Expand Down
Loading