247 lines
8.3 KiB
Python
247 lines
8.3 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 FreeCAD as App
|
|
|
|
if App.GuiUp:
|
|
import FreeCADGui as Gui
|
|
|
|
# translate = App.Qt.translate
|
|
|
|
__title__ = "Assembly utilitary functions"
|
|
__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:
|
|
temp = doc.Temporary
|
|
except AttributeError:
|
|
temp = False
|
|
return temp
|
|
|
|
|
|
def getObject(full_name):
|
|
# full_name is "Assembly.Assembly1.Assembly2.Assembly3.Box.Edge16"
|
|
# or "Assembly.Assembly1.Assembly2.Assembly3.Body.pad.Edge16"
|
|
# We want either Body or Box.
|
|
parts = full_name.split(".")
|
|
doc = App.ActiveDocument
|
|
if len(parts) < 3:
|
|
App.Console.PrintError(
|
|
"getObject() in UtilsAssembly.py the object name is too short, at minimum it should be something like 'Assembly.Box.edge16'. It shouldn't be shorter"
|
|
)
|
|
return None
|
|
|
|
obj = doc.getObject(parts[-3]) # So either 'Body', or 'Assembly'
|
|
|
|
if not obj:
|
|
return None
|
|
|
|
if obj.TypeId == "PartDesign::Body":
|
|
return obj
|
|
elif obj.TypeId == "App::Link":
|
|
linked_obj = obj.getLinkedObject()
|
|
if linked_obj.TypeId == "PartDesign::Body":
|
|
return obj
|
|
|
|
else: # primitive, fastener, gear ... or link to primitive, fastener, gear...
|
|
return doc.getObject(parts[-2])
|
|
|
|
|
|
def getElementName(full_name):
|
|
# full_name is "Assembly.Assembly1.Assembly2.Assembly3.Box.Edge16"
|
|
# We want either Edge16.
|
|
parts = full_name.split(".")
|
|
|
|
if len(parts) < 3:
|
|
# At minimum "Assembly.Box.edge16". It shouldn't be shorter
|
|
return ""
|
|
|
|
return parts[-1]
|
|
|
|
|
|
def getObjsNamesAndElement(obj_name, sub_name):
|
|
# if obj_name = "Assembly" and sub_name = "Assembly1.Assembly2.Assembly3.Box.Edge16"
|
|
# this will return ["Assembly","Assembly1","Assembly2","Assembly3","Box"] and "Edge16"
|
|
|
|
parts = sub_name.split(".")
|
|
|
|
# The last part is always the element name even if empty
|
|
element_name = parts[-1]
|
|
|
|
# The remaining parts are object names
|
|
obj_names = parts[:-1]
|
|
obj_names.insert(0, obj_name)
|
|
|
|
return obj_names, element_name
|
|
|
|
|
|
def getFullObjName(obj_name, sub_name):
|
|
# if obj_name = "Assembly" and sub_name = "Assembly1.Assembly2.Assembly3.Box.Edge16"
|
|
# this will return "Assembly.Assembly1.Assembly2.Assembly3.Box"
|
|
objs_names, element_name = getObjsNamesAndElement(obj_name, sub_name)
|
|
return ".".join(objs_names)
|
|
|
|
|
|
def getFullElementName(obj_name, sub_name):
|
|
# if obj_name = "Assembly" and sub_name = "Assembly1.Assembly2.Assembly3.Box.Edge16"
|
|
# this will return "Assembly.Assembly1.Assembly2.Assembly3.Box.Edge16"
|
|
return obj_name + "." + sub_name
|
|
|
|
|
|
def extract_type_and_number(element_name):
|
|
element_type = ""
|
|
element_number = ""
|
|
|
|
for char in element_name:
|
|
if char.isalpha():
|
|
# If the character is a letter, it's part of the type
|
|
element_type += char
|
|
elif char.isdigit():
|
|
# If the character is a digit, it's part of the number
|
|
element_number += char
|
|
else:
|
|
break
|
|
|
|
if element_type and element_number:
|
|
element_number = int(element_number)
|
|
return element_type, element_number
|
|
else:
|
|
return None, None
|
|
|
|
|
|
def findElementClosestVertex(selection_dict):
|
|
elt_type, elt_index = extract_type_and_number(selection_dict["element_name"])
|
|
|
|
if elt_type == "Vertex":
|
|
return selection_dict["element_name"]
|
|
|
|
elif elt_type == "Edge":
|
|
edge = selection_dict["object"].Shape.Edges[elt_index - 1]
|
|
|
|
curve = edge.Curve
|
|
if curve.TypeId == "Part::GeomCircle":
|
|
# For centers, as they are not shape vertexes, we return the element name.
|
|
# For now we only allow selecting the center of arcs / circles.
|
|
return selection_dict["element_name"]
|
|
|
|
edge_points = getPointsFromVertexes(edge.Vertexes)
|
|
closest_vertex_index, _ = findClosestPointToMousePos(
|
|
edge_points, selection_dict["mouse_pos"]
|
|
)
|
|
vertex_name = findVertexNameInObject(
|
|
edge.Vertexes[closest_vertex_index], selection_dict["object"]
|
|
)
|
|
|
|
return vertex_name
|
|
|
|
elif elt_type == "Face":
|
|
face = selection_dict["object"].Shape.Faces[elt_index - 1]
|
|
|
|
# Handle the circle/arc edges for their centers
|
|
center_points = []
|
|
center_points_edge_indexes = []
|
|
edges = face.Edges
|
|
|
|
for i, edge in enumerate(edges):
|
|
curve = edge.Curve
|
|
if curve.TypeId == "Part::GeomCircle":
|
|
center_points.append(curve.Location)
|
|
center_points_edge_indexes.append(i)
|
|
|
|
if len(center_points) > 0:
|
|
closest_center_index, closest_center_distance = findClosestPointToMousePos(
|
|
center_points, selection_dict["mouse_pos"]
|
|
)
|
|
|
|
# Hendle the face vertexes
|
|
face_points = getPointsFromVertexes(face.Vertexes)
|
|
closest_vertex_index, closest_vertex_distance = findClosestPointToMousePos(
|
|
face_points, selection_dict["mouse_pos"]
|
|
)
|
|
|
|
if len(center_points) > 0:
|
|
if closest_center_distance < closest_vertex_distance:
|
|
# Note the index here is the index within the face! Not the object.
|
|
index = center_points_edge_indexes[closest_center_index] + 1
|
|
return "Edge" + str(index)
|
|
|
|
vertex_name = findVertexNameInObject(
|
|
face.Vertexes[closest_vertex_index], selection_dict["object"]
|
|
)
|
|
|
|
return vertex_name
|
|
|
|
return ""
|
|
|
|
|
|
def getPointsFromVertexes(vertexes):
|
|
points = []
|
|
for vtx in vertexes:
|
|
points.append(vtx.Point)
|
|
return points
|
|
|
|
|
|
def findClosestPointToMousePos(candidates_points, mousePos):
|
|
closest_point_index = None
|
|
point_min_length = None
|
|
|
|
for i, point in enumerate(candidates_points):
|
|
length = (mousePos - point).Length
|
|
if closest_point_index is None or length < point_min_length:
|
|
closest_point_index = i
|
|
point_min_length = length
|
|
|
|
return closest_point_index, point_min_length
|
|
|
|
|
|
def findVertexNameInObject(vertex, obj):
|
|
for i, vtx in enumerate(obj.Shape.Vertexes):
|
|
if vtx.Point == vertex.Point:
|
|
return "Vertex" + str(i + 1)
|
|
return ""
|
|
|
|
|
|
def color_from_unsigned(c):
|
|
return [
|
|
float(int((c >> 24) & 0xFF) / 255),
|
|
float(int((c >> 16) & 0xFF) / 255),
|
|
float(int((c >> 8) & 0xFF) / 255),
|
|
]
|