Commit 20ec7b3a authored by Andrey Filippov's avatar Andrey Filippov

added Python 3 version

parent 57a926af
......@@ -4,6 +4,9 @@ by other mechanical CAD programs.
* Update: Here is the result of such conversion of 800+ parts and a few assemblies: http://wiki.elphel.com/index.php?title=Elphel_camera_assemblies
x3d_step_assy.py - original version for Python 2.7
x3d_step_assy3.py - version for Python 3
This macro converts assembly CAD model to X3D. It tries to recognize
individual parts (provided as STEP files) in the assembly model, converts
each part to X3D and then generates assembly X3D file that includes inline
......
'''
# Copyright (C) 2015, Elphel.inc.
# File: x3d_step_assy_color_match.py
# Generate x3d model from STEP parts models and STEP assembly
# by matching each solid in the assembly to the parts.
#
# Uses code from https://gist.github.com/hyOzd/2b38adff6a04e1613622
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.#
# This program 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http:#www.gnu.org/licenses/>.
@author: Andrey Filippov
@copyright: 2015 Elphel, Inc.
@license: GPLv3.0+
@contact: andrey@elphel.coml
@deffield updated: Updated
'''
#from email import Errors
__author__ = "Andrey Filippov"
__copyright__ = "Copyright 2015, Elphel, Inc."
__license__ = "GPL"
__version__ = "3.0+"
__maintainer__ = "Andrey Filippov"
__email__ = "andrey@elphel.com"
__status__ = "Development"
import FreeCAD
import FreeCADGui # just to update console output (change to threads later?) - does not seem to work
import Part
import os
import time
import pickle
import math
import xml.etree.ElementTree as et
from xml.dom import minidom
from FreeCAD import Base
from PySide import QtCore, QtGui
#from ConfigParser import SafeConfigParser #In Python 3, ConfigParser has been renamed to configparser for PEP 8 compliance.
#from configparser import SafeConfigParser #In Python 3, ConfigParser has been renamed to configparser for PEP 8 compliance.
from configparser import ConfigParser #In Python 3, ConfigParser has been renamed to configparser for PEP 8 compliance.
import sys
import traceback
CONFIG_PATH= "~/.FreeCAD/x3d_step_assy.ini"
ROOT_DIR = '~/parts/0393/export'
STEP_PARTS='~/parts/0393/export/step_parts'
#DIR_LIST = ["parts","subassy_flat"]
ASSEMBLY_PATH = ""
ASSEMBLY_SUFFIX = "-ASSY"
INFO_DIR = "info"
X3D_DIR = "x3d"
X3D_EXT = ".x3d"
INFO_EXT = ".pickle"
PRECISION = 0.0001
PRECISION_AREA = 0.001
PRECISION_VOLUME = 0.001
PRECISION_GYRATION = 0.001
PRECISION_INSIDE = 0.03
COLOR_PER_VERTEX = True
COMPONENTS = None # to hold data structure that is long to build so it will survive if the macro crashes
if CONFIG_PATH[0] == "~":
CONFIG_PATH = os.path.join(os.path.expanduser('~'),CONFIG_PATH[2:])
if ROOT_DIR[0] == "~":
ROOT_DIR = os.path.join(os.path.expanduser('~'),ROOT_DIR[2:])
if STEP_PARTS[0] == "~":
STEP_PARTS = os.path.join(os.path.expanduser('~'),STEP_PARTS[2:])
def get_step_list(dir_listdirs):
"""
@param dir_listdirs - a single directory path or a list of directories to scan for parts definitions as STEP files
@return a list of full paths of the STEP parts models
"""
if not isinstance(dir_listdirs,(list,tuple)):
dir_listdirs=[dir_listdirs]
return [os.path.join(root,f)
for dir_path in dir_listdirs if os.path.isdir(dir_path)
for root, _, files in os.walk(dir_path, topdown=True, onerror=None, followlinks = True)
for f in files if f.endswith((".step",".stp",".STP",".STEP"))]
def vector_to_tuple(v):
return((v.x,v.y,v.z))
def repair_solids_from_shells(shape):
"""
Some imported object from STEP files turned out to be open shells.
Convert them to solids with FreeCAD
@param shape - FreeCAD Shape
@return a list of FreeCAD solids
"""
solids = shape.Solids
new_solids = []
for sh in shape.Shells:
#find same shell in solids
for sld in solids:
if sh.isEqual(sld.Shells[0]):
new_solids.append(sld)
break
else:
new_solids.append(Part.Solid(sh))
return new_solids
#Find Vertex indices with maximal/minimal X,Y,Z to check orientation(Still does not check for holes - Add them somehow?
def verticesToCheck(solid):
"""
Create a list of 18 vertices having maximal/minimal X, Y, Z (and their +/- pairs).
These vertices will be later tested to be inside (with certain precision) the assembly part
@param solid - A Solid object
@return list of 18 3-tuples (x,y,z)
"""
l=[[],[],[],[],[],[],[],[],[]]
for v in solid.Vertexes:
l[0].append(v.X)
l[1].append(v.Y)
l[2].append(v.Z)
l[3].append(v.X + v.Y)
l[4].append(v.X - v.Y)
l[5].append(v.X + v.Z)
l[6].append(v.X - v.Z)
l[7].append(v.Y + v.Z)
l[8].append(v.Y - v.Z)
sind=set()
for lst in l:
sind.add(lst.index(min(lst)))
sind.add(lst.index(max(lst)))
lv=[]
for vi in sind:
v=solid.Vertexes[vi]
lv.append((v.X,v.Y,v.Z))
return lv
def getBoundBox(freecadObjects):
"""
Calculate BoundBox for all shapes in the document
@param freecadObjects - list of FreeCAD objects or all solids/shells
@return FreeCAD.BoundBox object for the whole document
"""
shells =[]
for o in freecadObjects:
if hasattr(o, "Shape"):
shape=o.Shape
for shell in shape.Shells: # solids and open shells
shells.append(shell)
elif hasattr(o, "BoundBox"):
shells.append(o)
bBox=None
for shell in shells:
thisBBox = shell.BoundBox
if not bBox:
bBox = FreeCAD.BoundBox(thisBBox.XMin,thisBBox.YMin,thisBBox.ZMin,
thisBBox.XMax,thisBBox.YMax,thisBBox.ZMax)
else:
bBox = FreeCAD.BoundBox(min(thisBBox.XMin,bBox.XMin),min(thisBBox.YMin,bBox.YMin),min(thisBBox.ZMin,bBox.ZMin),
max(thisBBox.XMax,bBox.XMax),max(thisBBox.YMax,bBox.YMax),max(thisBBox.ZMax,bBox.ZMax))
return bBox
def bBoxToX3d(bBox):
"""
Convert FreeCAD BoundBox to X3D representation (center, size)
@param bBox - FreeCAD BoundBox object
@return dictionary of {'center':(xc,yc,zc), size:(xs,ys,zs)}
"""
return {'center':((bBox.XMax + bBox.XMin)/2,(bBox.YMax + bBox.YMin)/2,(bBox.ZMax + bBox.ZMin)/2),
'size': ( bBox.XMax - bBox.XMin, bBox.YMax - bBox.YMin, bBox.ZMax - bBox.ZMin)}
#FreeCAD.BoundBox(0,0,0,0,0,0)
def create_file_info_nogui(shape, fname=""):
"""
A no-Gui version of the create_file_info, rather useless now as the color
is critical for the program. Using FreeCAD GGUI significantly slows down
the program and prevents it from running in a true batch mode. It seems
possible to hack Face.Tolerance property (unused so far) and import STEP
files saving colors in this property.
@param shape - FreeCAD Shape, containing one or more solids
@param fname - source file path
@return a pair of a list of info for each solid (as a dictionary) and a
list of solids in the shape
"""
objects = []
#repairing open shells
solids = shape.Solids
if len(solids) != len(shape.Shells):
print ("Repairing open shells that are not solids for %s"%(fname))
solids = repair_solids_from_shells(shape)
fromShell=True
else:
fromShell=False
for s in (solids):
pp=s.PrincipalProperties
objects.append({
"rpath": fname,
"shell": fromShell,
"volume": s.Volume,
"area": s.Area,
"center": vector_to_tuple(s.CenterOfMass),
"principal": {'RadiusOfGyration': pp['RadiusOfGyration'],
'FirstAxisOfInertia': vector_to_tuple(pp['FirstAxisOfInertia']),
'SecondAxisOfInertia': vector_to_tuple(pp['SecondAxisOfInertia']),
'ThirdAxisOfInertia': vector_to_tuple(pp['ThirdAxisOfInertia']),
'Moments': pp['Moments'],
'SymmetryPoint': pp['SymmetryPoint'],
'SymmetryAxis': pp['SymmetryAxis']},
"vertices": verticesToCheck(s)
})
# return objects
return (objects,solids)
def create_file_info(freecadObjects, fname=""):
"""
Collect information about each part/solid to be used for comparison between
assembly objects and parts
@param freecadObjects - list of FreeCAD objects
@param fname - source file path
@return a pair of a list of info for each solid (as a dictionary) and a
list of solids in the shape
"""
if not "Gui" in dir(FreeCAD):
return create_file_info_nogui(freecadObjects, fname)
# Count all shells in all objects
numShells = 0
for o in freecadObjects:
if hasattr(o, "Shape"):
numShells += len(o.Shape.Shells)
txt=""
if fname:
txt += " in "+fname
progress_bar = Base.ProgressIndicator()
progress_bar.start("Generating objects%s to export to X3D ..."%(txt), len(freecadObjects))
objects = []
allSolids=[]
for o in freecadObjects:
if hasattr(o, "Shape"):
shape=o.Shape
#repairing open shells
solids = shape.Solids
if len(solids) != len(shape.Shells):
print ("Repairing open shells that are not solids for %s"%(fname))
solids = repair_solids_from_shells(shape)
fromShell=True
else:
fromShell=False
# get all colors for faces in this object (normally just one Shell/Solid
color_set=set()
if o.ViewObject:
for clr in o.ViewObject.DiffuseColor: # colors are one per face
color_set.add(clr)
col_list = list(color_set)
col_dict={} # index for each color (reverse to list)
for i, clr in enumerate(col_list):
col_dict[clr] = i
#Calculate per-color centers for each object (normally each object has just one Solid/Shell
dc=o.ViewObject.DiffuseColor
if (len(dc) == 1) and (len(o.Shape.Faces)>1):
dc= dc * len(o.Shape.Faces)
colorCenters=[[0.0,0.0,0.0,0.0] for c in col_list] # SX,SY,SZ,S0
for clr,face in zip(dc, o.Shape.Faces):
clr_index = col_dict[clr]
m = face.Area
c = face.CenterOfMass # Vector
colorCenters[clr_index][0]+= c.x * m
colorCenters[clr_index][1]+= c.y * m
colorCenters[clr_index][2]+= c.z * m
colorCenters[clr_index][3]+= m
# print ("%s: cx=%f, cy=%f, cz=%f, m=%f"%(fname, c.x,c.y,c.z,m))
color_center_area={}
for clr in col_dict:
clr_index = col_dict[clr]
color_center_area[clr]={"center":(colorCenters[clr_index][0]/colorCenters[clr_index][3],
colorCenters[clr_index][1]/colorCenters[clr_index][3],
colorCenters[clr_index][2]/colorCenters[clr_index][3]),
"area": colorCenters[clr_index][3]}
# print ("color_center_area[%s] = %s"%(str(clr), str(color_center_area[clr])))
for i, s in enumerate(solids):
pp=s.PrincipalProperties
object={
"rpath": fname,
"shell": fromShell,
"volume": s.Volume,
"area": s.Area,
"center": vector_to_tuple(s.CenterOfMass),
"principal": {'RadiusOfGyration': pp['RadiusOfGyration'],
'FirstAxisOfInertia': vector_to_tuple(pp['FirstAxisOfInertia']),
'SecondAxisOfInertia': vector_to_tuple(pp['SecondAxisOfInertia']),
'ThirdAxisOfInertia': vector_to_tuple(pp['ThirdAxisOfInertia']),
'Moments': pp['Moments'],
'SymmetryPoint': pp['SymmetryPoint'],
'SymmetryAxis': pp['SymmetryAxis']},
"vertices": verticesToCheck(s)
}
if i == 0:
object["colorCenters"] = color_center_area
objects.append(object)
allSolids.append(s)
progress_bar.next() # True) # True - enable ESC to abort
progress_bar.stop()
return (objects,allSolids)
def get_info_files_nogui(dir_list = None):
"""
a no-gui version of get_info_files()
@param dir_list - list of directories (usually a single-element) to scan for
STEP part models (including subdirectories). Non-existing
directories in the list are OK, they will be silently skipped.
@return a dictionary with part names as keys and info parameters lists of
dictionaries created by create_file_info() as values
Each part usually has just one solid, but may have more than one, in that
case only the largest (by volume) is used for identification in the
assembly, and it is returned at index 0 in the result
"""
if dir_list is None:
dir_list = [STEP_PARTS]
start_time=time.time()
sl = get_step_list(dir_list = dir_list)
if not INFO_DIR in os.listdir(ROOT_DIR):
os.mkdir(os.path.join(ROOT_DIR,INFO_DIR))
todo_list = []
for f in sl: # now f is a full absolute path
fname,_ = os.path.splitext(os.path.basename(f))
info_path = os.path.join(ROOT_DIR,INFO_DIR,fname+INFO_EXT)
step_file = f # os.path.join(ROOT_DIR, f)
if (not os.path.isfile(info_path)) or (os.path.getmtime(step_file) > os.path.getmtime(info_path)): # no info or step is newer
todo_list.append(f)
for i, apath in enumerate(todo_list):
# apath = os.path.join(ROOT_DIR,f)
rslt_path = os.path.join(ROOT_DIR,INFO_DIR, os.path.splitext(os.path.basename(apath))[0] + INFO_EXT)
print("%d: Reading %s @%f"%(i,apath, time.time()-start_time), end="...")
shape = Part.read(apath)
print(" got %d solids @%f"%(len(shape.Solids), time.time()-start_time))
objects,_ = create_file_info_nogui(shape, apath)
print (objects)
pickle.dump(objects, open(rslt_path, "wb" ))
# Now read all pickled data:
info_dict = {}
for f in sl:
name = os.path.splitext(os.path.basename(f))[0]
info_path = os.path.join(ROOT_DIR,INFO_DIR, name + INFO_EXT)
info_dict[name] = pickle.load(open(info_path, "rb"))
#Put largest element as the [0] index
for k in info_dict:
if len(info_dict[k]) >1:
o = info_dict[k]
print (k,len(o),o)
vols = [s["volume"] for s in o]
mi = vols.index(max(vols))
print ("Largest solid is number %d"%(mi))
if mi>0:
o.insert(0,o.pop(mi))
return info_dict
def get_info_files(dir_list = None):
"""
Get information about each part collected with create_file_info() function
Generate this information for each part that does not have it or have
obsolete (older than STEP file) one.
@param dir_list - list of directories (usually a single-element) to scan for
STEP part models (including subdirectories). Non-existing
directories in the list are OK, they will be silently skipped.
@return a dictionary with part names as keys and info parameters lists of
dictionaries created by create_file_info() as values
Each part usually has just one solid, but may have more than one, in that
case only the largest (by volume) is used for identification in the
assembly, and it is returned at index 0 in the result
"""
if dir_list is None:
dir_list = [STEP_PARTS]
if not "Gui" in dir(FreeCAD):
return get_info_files_nogui(dir_list)
start_time=time.time()
sl = get_step_list(dir_listdirs = dir_list)
if not INFO_DIR in os.listdir(ROOT_DIR):
os.mkdir(os.path.join(ROOT_DIR,INFO_DIR))
todo_list = []
for f in sl: # now f is a full absolute path
print (f)
fname,_ = os.path.splitext(os.path.basename(f))
info_path = os.path.join(ROOT_DIR,INFO_DIR,fname+INFO_EXT)
step_file = f # os.path.join(ROOT_DIR, f)
if (not os.path.isfile(info_path)) or (os.path.getmtime(step_file) > os.path.getmtime(info_path)): # no info or step is newer
todo_list.append(f)
for i, apath in enumerate(todo_list):
# apath=os.path.join(ROOT_DIR,f)
rslt_path = os.path.join(ROOT_DIR,INFO_DIR, os.path.splitext(os.path.basename(apath))[0] + INFO_EXT)
print("%d: Reading %s @%f"%(i,apath, time.time()-start_time), end="...")
# Prepare data
FreeCAD.loadFile(apath)
doc = FreeCAD.activeDocument()
doc.Label = fname
print(" got %d objects @%f"%(len(doc.Objects), time.time()-start_time))
objects,_ = create_file_info(doc.Objects, apath)
FreeCAD.closeDocument(doc.Name)
FreeCADGui.updateGui()
# print (objects)
pickle.dump(objects, open(rslt_path, "wb" ))
# Now read all pickled data:
info_dict = {}
progress_bar = Base.ProgressIndicator()
progress_bar.start("Reading %d part info files ..."%(len(sl)), len(sl))
for f in sl:
name = os.path.splitext(os.path.basename(f))[0]
info_path = os.path.join(ROOT_DIR,INFO_DIR, name + INFO_EXT)
info_dict[name] = pickle.load(open(info_path, "rb"))
progress_bar.next()
progress_bar.stop()
# FreeCAD.Console.PrintMessage("get_info_files() - loaded"); #Rare FreeCAD crash?
#Put largest element of multi-solid parts as the [0] index.
for k in info_dict:
if len(info_dict[k]) >1:
o = info_dict[k]
print (k,len(o),o)
vols = [s["volume"] for s in o]
# vols = [s['principal']['RadiusOfGyration'][0] for s in o] # RadiusOfGyration[2] better characterizes the outer(larger) object?
# Maybe it is just outer thread?
mi = vols.index(max(vols))
print ("Largest solid is number %d"%(mi))
if mi>0:
o.insert(0,o.pop(mi))
# FreeCAD.Console.PrintMessage("get_info_files() - largest made first"); # rare FreeCAD crash?
return info_dict
def findPartsTransformations(solids, objects, candidates, info_dict, insidePrecision = PRECISION_INSIDE, precision = PRECISION):
"""
Find transformation (translation+rotation) matrices for each assembly part and each candidate part
@param solids - list of solids, they are used to check that the test part vertices are almost inside
Number of elements in solids, objects, candidates should match
@param objects - list of the solid properties (as dictionaries made by create_file_info()) of the assembly
elements.
@param candidates - list (per assembly element) of dictionaries indexed by part name (usually just one),
containing list of colors (tuples) for which there is an area match between assembly
element and a part. Build element part frame from colors first, then (if not enough)
use inertial directions.
@param info_dict - dictionary (part name as a key) of lists (first element is the largest by volume) of
part solid properties used for matching
@param inside_precision - precision for determining if the test points (available for each part) get inside
the assembly element. Currently as a fraction of the object bounding box diagonal,
but maybe it is better to use fraction of the translation distance plus diagonal?
@param precision - relative precision for matrix/vector calculations (determining co-linear/co-planar objects
@return a list (per assembly solid) of dictionaries part_name -> 4x4 transformation matrix (not yet tested
with multiple fits
"""
progress_bar = Base.ProgressIndicator()
progress_bar.start("Finding transformations for library parts to match assembly elements ...", len(objects))
transformations=[]
for i,s in enumerate(solids):
tolerance = insidePrecision * s.BoundBox.DiagonalLength # Or should it be fraction of the translation distance?
trans={}
print ("%d findPartsTransformations:"%(i))
for cand_name in candidates[i]:
co = info_dict[cand_name][0] # First solid in the candidate part file
try:
colorCenters = co['colorCenters']
except:
colorCenters = {}
matrix_part = ppToMatrix(co['principal'],co['center'], colorCenters, candidates[i][cand_name], 0, precision)
# Now try 4 orientations (until the first match).
# TODO - process parts with rotational axis (that allows certain, but not any rotation)
matrix_part_inverse = matrix_part.inverse()
# get color properties of a solid
try:
colorCenters = objects[i]['colorCenters']
except:
colorCenters = {}
for orient in range(4):
matrix_assy = ppToMatrix(s.PrincipalProperties,s.CenterOfMass,colorCenters, candidates[i][cand_name], orient, precision)
matrix_part_assy = matrix_assy.multiply(matrix_part_inverse)
for j, v in enumerate (co['vertices']):
if not s.isInside(matrix_part_assy.multiply(FreeCAD.Vector(v)),tolerance,True):
# print("%d: %s Failed on orientation %d vertice #%d (%f, %f,%f)"%(i,cand_name, orient, j, v[0],v[1],v[2]))
break
else:
print("%d: %s - got transformation with orientation %d"%(i,cand_name, orient))
# trans.append(matrix_part_assy)
trans[cand_name] = matrix_part_assy
break
else:
print("Could not find match for part %s, trying manually around that vertex"%(cand_name))
# Seems to be a bug FreeCAD does not recognize seemingly perfect match even with huge tolerance
# Will try manually around that point to find inside one
try_vectors= ((-1,-1,-1), (-1,-1, 0), (-1,-1, 1),
(-1, 0,-1), (-1, 0, 0), (-1, 0, 1),
(-1, 1,-1), (-1, 1, 0), (-1, 1, 1),
( 0,-1,-1), ( 0,-1, 0), ( 0,-1, 1),
( 0, 0,-1), ( 0, 0, 1),
( 0, 1,-1), ( 0, 1, 0), ( 0, 1, 1),
( 1,-1,-1), ( 1,-1, 0), ( 1,-1, 1),
( 1, 0,-1), ( 1, 0, 0), ( 1, 0, 1),
( 1, 1,-1), ( 1, 1, 0), ( 1, 1, 1))
for orient in range(4):
matrix_assy = ppToMatrix(s.PrincipalProperties,s.CenterOfMass,colorCenters, candidates[i][cand_name], orient, precision)
matrix_part_assy = matrix_assy.multiply(matrix_part_inverse)
for j, v in enumerate (co['vertices']):
if not s.isInside(matrix_part_assy.multiply(FreeCAD.Vector(v)),tolerance,True):
for tv in try_vectors:
mv= FreeCAD.Vector(v[0]+tv[0]*tolerance, v[1]+tv[1]*tolerance, v[2]+tv[2]*tolerance)
if s.isInside(matrix_part_assy.multiply(mv),tolerance,True):
break # got it!
else:
break # no luck
else:
print("%d: %s - finally got transformation with orientation %d"%(i,cand_name, orient))
# trans.append(matrix_part_assy)
trans[cand_name] = matrix_part_assy
break
else:
# trans.append(None) # so Transformations have same structure as candidates, and it is now dictionary
print("*** Could not find match for part %s"%(cand_name))
transformations.append(trans)
progress_bar.next() # True)
progress_bar.stop()
return transformations
def colorMatchCandidate(assy_object, candidates, info_dict, precision = PRECISION_AREA):
"""
Select colored features among the parts candidates by comparing total area per color
with the candidates so if some feature on the assembly object and the part have
different colors, the others can still be used for orientation identification.
Parts w/o matching color information can only be oriented by axes of gyration
@param assy_object - a dictionary of the parameters of the assembly object
@param candidates - a list of part name that fit this assembly object without color
properties
@param info_dict - dictionary of parameters for all parts (indexed by part names)
@return dictionary partName -> list of colors (as 3-tuples)
"""
colored_candidates={}
if candidates:
try:
assy_color_center_area = assy_object["colorCenters"]
except:
print ("colorMatchCandidate(), assy_object = ",assy_object)
assy_color_center_area = {}
cand_matches=[]
for candidate in candidates:
matched_colors=[]
info_cand= info_dict[candidate][0] # only first solid in a part
# print ("info_cand=",info_cand)
for color in assy_color_center_area:
assy_area= assy_color_center_area[color]["area"]
# print ("color: %s, assy_area = %f"%(str(color), assy_area))
try:
part_area = info_cand ["colorCenters"][color]["area"]
# print ("color: %s, part_area = %f"%(str(color), part_area))
if abs(part_area - assy_area) < precision * assy_area:
matched_colors.append(color)
except:
pass
cand_matches.append(matched_colors)
max_match = max([len(a) for a in cand_matches])
for candidate, colors in zip(candidates,cand_matches):
if len(colors) == max_match:
colored_candidates[candidate]=colors
return colored_candidates
def findComponents(assembly,
precision_area = PRECISION_AREA,
precision_volume = PRECISION_VOLUME,
precision_gyration = PRECISION_GYRATION,
precision_inside = PRECISION_INSIDE,
precision = PRECISION,
show_best = True):
"""
Match each assembly element with a part, provide the transformation matrix
@param assembly - may be file path (different treatment for Gui/no-Gui, Shape or doc.Objects or "" - will use ActiveDocument().Objects
@param precision_area = PRECISION_AREA - relative precision in surface area calculations
@param precision_volume = PRECISION_VOLUME - relative precision in volume calculations
@param precision_gyration = PRECISION_GYRATION - relative precision in radius of gyration calculations
@param precision_inside = PRECISION_INSIDE - relative precision in calculations of point inside/outside of a solid
@param precision = PRECISION - precision in vector calculation
@param show_best - calculate and show the best relative match for each parameter - can be used to fine-tune PRECISION* parameters
@return a dictionary with 4 fields (each list value has the same number of elements):
'solids' - a list of solids in the assembly
'objects' - a list of solid properties (as dictionaries) used for identification
'candidates' - a list of candidate parts dictionaries, containing lists of matched colors
'transformation' - a list of dictionaries of transformations, indexed by part names (normally just one element)
The same return dictionary is saved as a global variable COMPONENTS and is available as getComponents() method
"""
FreeCAD.Console.PrintMessage("findComponents(): Getting parts database, assembly = %s\n"%(assembly));
print("findComponents(): Getting parts database-\n");
global COMPONENTS
start_time=time.time()
print("Getting parts database-\n")
info_dict = get_info_files()
FreeCAD.Console.PrintMessage("findComponents(): Got parts database\n");
print("findComponents(): Got parts database, assembly=",assembly,"\n")
aname = ""
if not assembly: # including "" string
assembly = FreeCAD.activeDocument().Objects
FreeCAD.Console.PrintMessage("Using %d solids in the active document @%f"%(len(assembly), time.time()-start_time));
if isinstance (assembly, (bytes,str)):
assembly_path = assembly
aname,_ = os.path.splitext(os.path.basename(assembly_path))
if not "Gui" in dir(FreeCAD):
print("Reading assembly file %s @%f"%(assembly_path, time.time()-start_time), end="...")
assembly = Part.read(assembly_path)
print(" got %d solids @%f"%(len(assembly.Solids), time.time()-start_time))
else:
FreeCAD.Console.PrintMessage("Using STEP file assembly %s @%f"%(assembly_path, time.time()-start_time));
FreeCAD.loadFile(assembly_path)
doc = FreeCAD.activeDocument()
doc.Label = aname
print(" got %d objects @%f"%(len(doc.Objects), time.time()-start_time))
assembly = doc.Objects
FreeCAD.Console.PrintMessage(" got %d solids @%f\n"%(len(assembly), time.time()-start_time));
# assuming assembly is doc.Objects
if isinstance(assembly,Part.Shape):
FreeCAD.Console.PrintMessage("Using provided objects @%f"%(len(assembly.Solids), time.time()-start_time));
objects,solids = create_file_info_nogui(shape, aname)
# shape = assembly
else:
objects,solids = create_file_info(assembly, aname)
# print (objects)
progress_bar = Base.ProgressIndicator()
progress_bar.start("Looking for matching parts for each of the assembly element ...", len(objects))
FreeCAD.Console.PrintMessage("Looking for matching parts for each of the assembly element %d\n"%(len(objects)));
candidates=[]
for i,o in enumerate(objects):
# try:
# FreeCADGui.updateGui()
# except:
# pass
print (i,o)
this_candidates = []
list_errors=[]
rg=o['principal']['RadiusOfGyration']
rg_av = math.sqrt(rg[0]**2 + rg[1]**2 + rg[2]**2)
rgp = precision_gyration * rg_av
vp = o['volume']*precision_volume
ap = o['area']*precision_area
for n in info_dict:
co = info_dict[n][0]
errors = (abs(o['volume'] - co['volume']),
abs(o['area'] - co['area']),
abs(rg[0] - co['principal']['RadiusOfGyration'][0]),
abs(rg[1] - co['principal']['RadiusOfGyration'][1]),
abs(rg[2] - co['principal']['RadiusOfGyration'][2]),
)
if show_best:
list_errors.append(errors)
if ((errors[0] < vp) and
(errors[1] < ap) and
(errors[2] < rgp) and
(errors[3] < rgp) and
(errors[4] < rgp)):
this_candidates.append(n)
if show_best:
weighted_errors = [errors[0]/vp + errors[1]/ap + (errors[2] + errors[3] + errors[4])/rgp for errors in list_errors]
best_index = weighted_errors.index(min(weighted_errors))
errors = list_errors[best_index]
print ("Best match with %s, relative errors: dV=%f, dS=%f, dRG1=%f, dRG2=%f, dRG3=%f"%(
list(info_dict.keys())[best_index],
errors[0]/o['volume'],
errors[1]/o['area'],
errors[2]/rg_av,
errors[3]/rg_av,
errors[4]/rg_av))
# Filter candidates by number of color areas matched
colored_candidates=colorMatchCandidate(o, this_candidates, info_dict, precision_area)
try:
num_ass_obj_colors = len(o["colorCenters"])
except:
num_ass_obj_colors = 0
print ("%d :colors: %d candidates: %s, colored_candidates: %s"%(i,num_ass_obj_colors, str(this_candidates), str(colored_candidates)))
candidates.append(colored_candidates)
progress_bar.next() # True) # True - enable ESC to abort
progress_bar.stop()
transformations = findPartsTransformations(solids, objects, candidates, info_dict, precision_inside, precision)
#Each part can be in two orientations - check overlap after loading actual parts
COMPONENTS = {"solids":solids,"objects":objects,"candidates":candidates,"transformations":transformations}
FreeCAD.Console.PrintMessage("DONE looking for matching parts for each of the assembly element\n");