Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
F
freecad_x3d
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Wiki
Wiki
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Commits
Issue Boards
Open sidebar
Elphel
freecad_x3d
Commits
9210b2e5
Commit
9210b2e5
authored
Dec 13, 2015
by
Andrey Filippov
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
more testing, added dialog to setup paths
parent
0b0cddbf
Changes
1
Show whitespace changes
Inline
Side-by-side
Showing
1 changed file
with
784 additions
and
119 deletions
+784
-119
x3d_step_assy.py
x3d_step_assy.py
+784
-119
No files found.
x3d_step_assy.py
View file @
9210b2e5
...
...
@@ -2,10 +2,9 @@ from __future__ import division
from
__future__
import
print_function
'''
# Copyright (C) 2015, Elphel.inc.
# File: x3d_step_assy.py
# 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.
# Work in progress, not yet handles parts with symmetries
#
# Uses code from https://gist.github.com/hyOzd/2b38adff6a04e1613622
#
...
...
@@ -28,6 +27,7 @@ from __future__ import print_function
@contact: andrey@elphel.coml
@deffield updated: Updated
'''
from
email
import
Errors
__author__
=
"Andrey Filippov"
__copyright__
=
"Copyright 2015, Elphel, Inc."
__license__
=
"GPL"
...
...
@@ -37,34 +37,69 @@ __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
import
sys
import
traceback
ROOT_DIR
=
'~/parts/0393/export'
DIR_LIST
=
[
"parts"
,
"subassy_flat"
]
ROOT_DIR
=
'~/parts/0393/export1'
STEP_PARTS
=
'~/parts/0393/export1/step_parts'
#DIR_LIST = ["parts","subassy_flat"]
ASSEMBLY_PATH
=
None
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
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 get_step_list(dir_list):
step_files = []
for rpath in dir_list:
apath = os.path.join(ROOT_DIR,rpath)
try:
step_files += [os.path.join(rpath, f) for f in os.listdir(apath) if os.path.isfile(os.path.join(apath, f)) and f.endswith((".step",".stp"))]
except:
print ("Failed to add files from
%
s"
%
apath)
return step_files
"""
def
vector_to_tuple
(
v
):
return
((
v
.
x
,
v
.
y
,
v
.
z
))
...
...
@@ -104,7 +139,7 @@ def verticesToCheck(solid):
lv
.
append
((
v
.
X
,
v
.
Y
,
v
.
Z
))
return
lv
def
create_file_info
(
shape
,
fname
=
""
):
def
create_file_info
_nogui
(
shape
,
fname
=
""
):
objects
=
[]
#repairing open shells
solids
=
shape
.
Solids
...
...
@@ -131,33 +166,122 @@ def create_file_info(shape, fname=""):
'SymmetryAxis'
:
pp
[
'SymmetryAxis'
]},
"vertices"
:
verticesToCheck
(
s
)
})
return
objects
# return objects
return
(
objects
,
solids
)
def
create_file_info
(
freecadObjects
,
fname
=
""
):
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(o.ViewObject.DiffuseColor, o.Shape.Faces):
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
def
get_info_files
(
dir_list
=
DIR_LIST
):
progress_bar
.
stop
()
return
(
objects
,
allSolids
)
def
get_info_files_nogui
(
dir_list
=
None
):
if
dir_list
is
None
:
dir_list
=
[
STEP_PARTS
]
start_time
=
time
.
time
()
sl
=
get_step_list
(
dir_list
=
dir_list
)
# print ("Step files:")
# for i,f in enumerate (sl):
# print("%d: %s"%(i,f))
if
not
INFO_DIR
in
os
.
listdir
(
ROOT_DIR
):
os
.
mkdir
(
os
.
path
.
join
(
ROOT_DIR
,
INFO_DIR
))
todo_list
=
[]
for
f
in
sl
:
for
f
in
sl
:
# now f is a full absolute path
fname
,
_
=
os
.
path
.
splitext
(
os
.
path
.
basename
(
f
))
# print("%s -> %s"%(f,fname))
if
not
os
.
path
.
isfile
(
os
.
path
.
join
(
ROOT_DIR
,
INFO_DIR
,
fname
+
INFO_EXT
)):
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,f in enumerate (todo_list):
# print("%d: %s"%(i,f))
for
i
,
f
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
(
f
))[
0
]
+
INFO_EXT
)
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
(
shape
,
f
)
objects
,
_
=
create_file_info_nogui
(
shape
,
apath
)
print
(
objects
)
pickle
.
dump
(
objects
,
open
(
rslt_path
,
"wb"
))
...
...
@@ -179,19 +303,99 @@ def get_info_files(dir_list = DIR_LIST):
o
.
insert
(
0
,
o
.
pop
(
mi
))
return
info_dict
def
findPartsTransformations
(
solids
,
objects
,
candidates
,
info_dict
,
precision
=
0.01
):
def
get_info_files
(
dir_list
=
None
):
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"
);
#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"
);
return
info_dict
def
findPartsTransformations
(
solids
,
objects
,
candidates
,
info_dict
,
insidePrecision
=
PRECISION_INSIDE
,
precision
=
PRECISION
):
"""
@param candidates - list (per assembly element) of dictionaries indexed by part name (usually just one) 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
"""
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
=
p
recision
*
s
.
BoundBox
.
DiagonalLength
# Or should it be fraction of the translation distance?
tolerance
=
insideP
recision
*
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
]
matrix_part
=
ppToMatrix
(
co
[
'principal'
],
co
[
'center'
])
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
,
orient
)
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
):
...
...
@@ -215,7 +419,7 @@ def findPartsTransformations(solids, objects, candidates, info_dict, precision=0
(
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
,
orient
)
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
):
...
...
@@ -233,42 +437,163 @@ def findPartsTransformations(solids, objects, candidates, info_dict, precision=0
trans
.
append
(
None
)
# so Transformations have same structure as candidates
print
(
"*** Could not find match for part
%
s"
%
(
cand_name
))
transformations
.
append
(
trans
)
progress_bar
.
next
()
# True) # True - enable ESC to abort
progress_bar
.
stop
()
return
transformations
#components=scan_step_parts.findComponents("/home/andrey/parts/0393/export/nc393_05_flat_noassy.stp")
def
findComponents
(
assembly_path
,
precision_inside
=
PRECISION_INSIDE
,
precision
=
PRECISION
):
def
colorMatchCandidate
(
assy_object
,
candidates
,
info_dict
,
precision
=
PRECISION_AREA
):
"""
@return dictionary partName -> list of colors (as 3-tuples)
"""
colored_candidates
=
{}
if
candidates
:
assy_color_center_area
=
assy_object
[
"colorCenters"
]
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
"""
PRECISION_AREA = 0.02
PRECISION_VOLUME = 0.03
PRECISION_INSIDE = 0.03
"""
#components=scan_step_parts.findComponents("/home/andrey/parts/0393/export/nc393_07_flat_noassy.stp")
def
findComponents
(
assembly
,
precision_area
=
PRECISION_AREA
,
precision_volume
=
PRECISION_VOLUME
,
precision_gyration
=
PRECISION_GYRATION
,
precision_inside
=
PRECISION_INSIDE
,
precision
=
PRECISION
,
show_best
=
True
):
"""
@param assembly - may be file path (different treatment for Gui/no-Gui, Shape or doc.Objects or None - 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
"""
FreeCAD
.
Console
.
PrintMessage
(
"findComponents(): Getting parts database"
);
global
COMPONENTS
start_time
=
time
.
time
()
print
(
"Getting parts database"
)
info_dict
=
get_info_files
()
FreeCAD
.
Console
.
PrintMessage
(
"findComponents(): Got parts database"
);
aname
=
""
if
not
assembly
:
assembly
=
FreeCAD
.
activeDocument
()
.
Objects
FreeCAD
.
Console
.
PrintMessage
(
"Using
%
d solids in the active document @
%
f"
%
(
len
(
assembly
),
time
.
time
()
-
start_time
));
if
isinstance
(
assembly
,
(
str
,
unicode
)):
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
=
"..."
)
shape
=
Part
.
read
(
assembly_path
)
print
(
" got
%
d solids @
%
f"
%
(
len
(
shape
.
Solids
),
time
.
time
()
-
start_time
))
objects
=
create_file_info
(
shape
,
assembly_path
)
print
(
objects
)
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"
%
(
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
))
candidates
=
[]
for
i
,
o
in
enumerate
(
objects
):
# try:
# FreeCADGui.updateGui()
# except:
# pass
print
(
i
,
o
)
this_candidates
=
[]
list_errors
=
[]
rg
=
o
[
'principal'
][
'RadiusOfGyration'
]
rgp
=
precision
*
math
.
sqrt
(
rg
[
0
]
**
2
+
rg
[
1
]
**
2
+
rg
[
2
]
**
2
)
vp
=
o
[
'volume'
]
*
precision
ap
=
o
[
'area'
]
*
precision
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
]
if
((
abs
(
o
[
'volume'
]
-
co
[
'volume'
])
<
vp
)
and
(
abs
(
o
[
'area'
]
-
co
[
'area'
])
<
ap
)
and
(
abs
(
rg
[
0
]
-
co
[
'principal'
][
'RadiusOfGyration'
][
0
])
<
ap
)
and
(
abs
(
rg
[
1
]
-
co
[
'principal'
][
'RadiusOfGyration'
][
1
])
<
ap
)
and
(
abs
(
rg
[
2
]
-
co
[
'principal'
][
'RadiusOfGyration'
][
2
])
<
ap
)):
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 ((abs(o['volume'] - co['volume']) < vp) and
# (abs(o['area'] - co['area']) < ap) and
# (abs(rg[0] - co['principal']['RadiusOfGyration'][0]) < rgp) and
# (abs(rg[1] - co['principal']['RadiusOfGyration'][1]) < rgp) and
# (abs(rg[2] - co['principal']['RadiusOfGyration'][2]) < rgp)):
if
((
errors
[
0
]
<
vp
)
and
(
errors
[
1
]
<
ap
)
and
(
errors
[
2
]
<
rgp
)
and
(
errors
[
3
]
<
rgp
)
and
(
errors
[
4
]
<
rgp
)):
this_candidates
.
append
(
n
)
candidates
.
append
(
this_candidates
)
solids
=
shape
.
Solids
if
len
(
solids
)
!=
len
(
shape
.
Shells
):
print
(
"Repairing open shells that are not solids for
%
s"
%
(
assembly_path
))
solids
=
repair_solids_from_shells
(
shape
)
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"
%
(
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
transformations
=
findPartsTransformations
(
solids
,
objects
,
candidates
,
info_dict
,
precision_inside
)
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
return
{
"shape"
:
shape
,
"objects"
:
objects
,
"candidates"
:
candidates
,
"transformations"
:
transformations
}
COMPONENTS
=
{
"solids"
:
solids
,
"objects"
:
objects
,
"candidates"
:
candidates
,
"transformations"
:
transformations
}
return
COMPONENTS
# return {"solids":solids,"objects":objects,"candidates":candidates,"transformations":transformations}
def
getComponents
():
return
COMPONENTS
def
ortho3
(
v0
,
v1
):
v0
.
normalize
()
...
...
@@ -281,30 +606,106 @@ def ortho3(v0,v1):
def
ppToMatrix
(
pp
,
center
=
(
0
,
0
,
0
),
orient
=
0
):
#Both Vectors and lists/tuples are OK here
v0
=
FreeCAD
.
Vector
(
pp
[
"FirstAxisOfInertia"
])
v1
=
FreeCAD
.
Vector
(
pp
[
"SecondAxisOfInertia"
])
v2
=
FreeCAD
.
Vector
(
pp
[
"ThirdAxisOfInertia"
])
def
ppToMatrix
(
pp
,
center
=
(
0
,
0
,
0
),
colorCenters
=
{},
# should have all the colors in a colors list "by design"
colors
=
[],
orient
=
0
,
precision
=
PRECISION
):
"""
@param pp - PrincipalProperties
@param center - Center of mass
@param colorCenters - dictionary indexed by colors, having center of color and area of each color (not used here)
@param colors - list of matched colors (tuples)
@param orient - 2-bit modifier for first and second axis of inertia (bit 0 - sign of the first axis, bit 1 - sign of the second)
orient will be overridden if there are some color vectors that define orientation
@param precision - multiplier for the radius of gyration to compare with color vectors
@return 4-matrix
"""
rg
=
pp
[
'RadiusOfGyration'
]
eps
=
math
.
sqrt
(
rg
[
0
]
**
2
+
rg
[
1
]
**
2
+
rg
[
2
]
**
2
)
*
precision
color_vectors
=
[]
t
=
FreeCAD
.
Vector
(
center
)
vectors
=
[]
for
color
in
colors
:
## print ("colorCenters=", colorCenters[color]['center']," area=",colorCenters[color]['area'])
color_vectors
.
append
(
FreeCAD
.
Vector
(
colorCenters
[
color
][
'center'
])
-
t
)
print
(
"color_vectors="
,
color_vectors
)
## print ("color_vectors=",color_vectors, "t=",t)
if
color_vectors
:
# find the longest one
lengths
=
[
v
.
Length
for
v
in
color_vectors
]
l
=
max
(
lengths
)
v
=
color_vectors
.
pop
(
lengths
.
index
(
l
))
if
l
>
eps
:
vectors
.
append
(
v
.
normalize
())
if
vectors
and
color_vectors
:
# now find the vector having maximal orthogonal component to v[0]
lengths
=
[
v
.
cross
(
vectors
[
0
])
.
Length
for
v
in
color_vectors
]
l
=
max
(
lengths
)
v
=
color_vectors
.
pop
(
lengths
.
index
(
l
))
if
l
>
eps
:
vectors
.
append
(
v
.
normalize
())
# print ("vectors=",vectors)
#use gyro axis (or two of them)
if
len
(
vectors
)
<
3
:
#insufficient color vectors
vgyro
=
[
FreeCAD
.
Vector
(
pp
[
"FirstAxisOfInertia"
]),
FreeCAD
.
Vector
(
pp
[
"SecondAxisOfInertia"
]),
FreeCAD
.
Vector
(
pp
[
"ThirdAxisOfInertia"
])]
if
(
orient
&
1
)
:
v0
.
multiply
(
-
1.0
)
vgyro
[
0
]
.
multiply
(
-
1.0
)
if
(
orient
&
2
)
:
v1
.
multiply
(
-
1.0
)
vgyro
[
1
]
.
multiply
(
-
1.0
)
#v0,v1,v2 = ortho3(v0,v1)
if
v2
.
dot
(
v0
.
cross
(
v1
))
<
0
:
v2
.
multiply
(
-
1.0
)
return
FreeCAD
.
Matrix
(
v0
.
x
,
v1
.
x
,
v2
.
x
,
t
.
x
,
v0
.
y
,
v1
.
y
,
v2
.
y
,
t
.
y
,
v0
.
z
,
v1
.
z
,
v2
.
z
,
t
.
z
,
0.0
,
0.0
,
0.0
,
1.0
)
if
vgyro
[
2
]
.
dot
(
vgyro
[
0
]
.
cross
(
vgyro
[
1
]))
<
0
:
vgyro
[
2
]
.
multiply
(
-
1.0
)
## print ("vgyro=", vgyro)
if
not
vectors
:
vectors
=
[
vgyro
[
0
],
vgyro
[
1
],
vgyro
[
2
]]
else
:
# at least one vector is defined from colors, need one more
new_directions
=
[
False
,
False
,
False
]
new_length
=
len
(
vectors
)
if
len
(
vectors
)
<
2
:
# == 1, need one more
## print ("vgyro=",vgyro)
for
i
in
range
(
3
):
# filter parallel to existing
for
v
in
vectors
:
if
v
.
cross
(
vgyro
[
i
])
.
Length
<
eps
:
break
else
:
new_directions
[
i
]
=
True
new_length
+=
1
## print ("new_directions=",new_directions," new_length=",new_length)
if
new_length
>
2
:
# extras, filter more (perpendicular to axis of symmetry)
if
(
new_directions
[
0
]
or
new_directions
[
1
])
and
((
rg
[
0
]
-
rg
[
1
])
<
eps
):
if
new_directions
[
1
]:
new_directions
[
1
]
=
False
new_length
-=
1
if
new_directions
[
0
]
and
(
new_length
>
2
):
new_directions
[
0
]
=
False
new_length
-=
1
if
(
new_length
>
2
)
and
(
new_directions
[
1
]
or
new_directions
[
2
])
and
((
rg
[
1
]
-
rg
[
2
])
<
eps
):
if
new_directions
[
1
]:
new_directions
[
1
]
=
False
new_length
-=
1
if
new_directions
[
2
]
and
(
new_length
>
3
):
new_directions
[
2
]
=
False
new_length
-=
1
## print ("new_directions=",new_directions," new_length=",new_length)
def
traslateToMatrix
(
center
=
(
0
,
0
,
0
)):
#Both Vectors and lists/tuples are OK here
t
=
FreeCAD
.
Vector
(
center
)
return
FreeCAD
.
Matrix
(
1.0
,
0.0
,
0.0
,
t
.
x
,
0.0
,
1.0
,
0.0
,
t
.
y
,
0.0
,
0.0
,
1.0
,
t
.
z
,
0.0
,
0.0
,
0.0
,
1.0
)
# All good, add 1,2,3-rd and make ortho-normal
if
len
(
vectors
)
<
2
:
i
=
new_directions
.
index
(
True
)
vectors
.
append
((
vgyro
[
i
]
-
vectors
[
0
]
*
vectors
[
0
]
.
dot
(
vgyro
[
i
]))
.
normalize
())
# here we have 2 vectors, make a third
vectors
=
[
vectors
[
0
],
vectors
[
1
],
vectors
[
0
]
.
cross
(
vectors
[
1
])
.
normalize
()]
if
vectors
[
2
]
.
dot
(
vectors
[
0
]
.
cross
(
vectors
[
1
]))
<
0
:
vectors
[
2
]
.
multiply
(
-
1.0
)
# print ("Final vectors=",vectors)
return
FreeCAD
.
Matrix
(
vectors
[
0
]
.
x
,
vectors
[
1
]
.
x
,
vectors
[
2
]
.
x
,
t
.
x
,
vectors
[
0
]
.
y
,
vectors
[
1
]
.
y
,
vectors
[
2
]
.
y
,
t
.
y
,
vectors
[
0
]
.
z
,
vectors
[
1
]
.
z
,
vectors
[
2
]
.
z
,
t
.
z
,
0.0
,
0.0
,
0.0
,
1.0
)
def
list_parts_offsets
():
info_files
=
get_info_files
()
...
...
@@ -312,11 +713,16 @@ def list_parts_offsets():
for
j
,
o
in
enumerate
(
info_files
[
name
]):
d
=
math
.
sqrt
(
o
[
"center"
][
0
]
**
2
+
o
[
"center"
][
1
]
**
2
+
o
[
"center"
][
2
]
**
2
)
if
j
==
0
:
print
(
"
%
3
i
:"
%
(
i
),
end
=
""
)
print
(
"
%
4
d
:"
%
(
i
),
end
=
""
)
else
:
print
(
" "
,
end
=
""
)
print
(
"
%
s offset =
%6.1
f"
%
(
name
,
d
))
def
list_parts
():
info_files
=
get_info_files
()
for
i
,
name
in
enumerate
(
info_files
):
print
(
"
%4
d '
%
s':
%
d solids:
%
s"
%
(
i
,
name
,
len
(
info_files
[
name
]),
str
(
info_files
[
name
])))
# X3D Export
def
getShapeNode
(
vertices
,
faces
,
diffuseColor
=
None
,
main_color_index
=
0
,
colorPerVertex
=
True
):
...
...
@@ -349,7 +755,7 @@ def getShapeNode(vertices, faces, diffuseColor = None, main_color_index = 0, col
materialNode
.
set
(
'diffuseColor'
,
"
%
f
%
f
%
f"
%
tuple
(
diffuseColor
[
main_color_index
*
3
:
main_color_index
*
3
+
3
]))
return
shapeNode
def
exportX3D
(
objects
,
filepath
,
colorPerVertex
):
def
exportX3D
(
objects
,
filepath
,
id
=
"part"
,
colorPerVertex
=
False
):
"""Export given list of objects to a X3D file.
Each object is a dictionary in this form:
...
...
@@ -359,17 +765,19 @@ def exportX3D(objects, filepath, colorPerVertex):
color : [R, G, B,...] # number range is 0-1.0, exactly 3 elements for a single color, 3*N for per-vertex colors
}
"""
progress_bar
=
Base
.
ProgressIndicator
()
progress_bar
.
start
(
"Saving objects to X3D file
%
s ..."
%
(
filepath
),
len
(
objects
))
x3dNode
=
et
.
Element
(
'x3d'
)
x3dNode
.
set
(
'profile'
,
'Interchange'
)
x3dNode
.
set
(
'version'
,
'3.3'
)
sceneNode
=
et
.
SubElement
(
x3dNode
,
'Scene'
)
progress_bar
=
Base
.
ProgressIndicator
(
)
progress_bar
.
start
(
"Saving objects to X3D file
%
s ..."
%
(
filepath
),
len
(
objects
)
)
groupNode
=
et
.
SubElement
(
sceneNode
,
'Group'
)
groupNode
.
set
(
'id'
,
id
)
for
o
in
objects
:
shapeNode
=
getShapeNode
(
o
[
"points"
],
o
[
"faces"
],
o
[
"color"
],
o
[
"main_color_index"
],
colorPerVertex
)
scene
Node
.
append
(
shapeNode
)
group
Node
.
append
(
shapeNode
)
progress_bar
.
next
()
# True) # True - enable ESC to abort
oneliner
=
et
.
tostring
(
x3dNode
)
...
...
@@ -410,7 +818,7 @@ def prepareX3dExport(freecadObjects, fname=""):
mesh
=
f
.
tessellate
(
1
)
if
(
not
mesh
[
0
])
or
(
not
mesh
[
1
]):
continue
# some objects (such as Part:Circle)
color_index
=
col_dict
[
o
.
ViewObject
.
DiffuseColor
[
i
]]
color_index
=
col_dict
[
o
.
ViewObject
.
DiffuseColor
[
i
]]
#sometimes len(o.ViewObject.DiffuseColor[i]) ==1, but it will not get here
color_areas
[
color_index
]
+=
f
.
Area
delta
=
len
(
points
)
new_indices
=
[]
...
...
@@ -445,23 +853,27 @@ def prepareX3dExport(freecadObjects, fname=""):
progress_bar
.
stop
()
return
objects
def
generatePartsX3d
(
dir_list
=
DIR_LIST
,
colorPerVertex
=
COLOR_PER_VERTEX
):
def
generatePartsX3d
(
dir_list
=
[
STEP_PARTS
],
colorPerVertex
=
COLOR_PER_VERTEX
):
start_time
=
time
.
time
()
info_dict
=
get_info_files
(
dir_list
)
# Will (re-) build info files if missing
step_list
=
get_step_list
(
dir_list
)
#relative to ROOT_DIR
step_list
=
get_step_list
(
dir_list
)
#
now absolute, not
relative to ROOT_DIR
if
not
X3D_DIR
in
os
.
listdir
(
ROOT_DIR
):
os
.
mkdir
(
os
.
path
.
join
(
ROOT_DIR
,
X3D_DIR
))
numExported
=
0
for
step_file
in
step_list
:
partName
,
_
=
os
.
path
.
splitext
(
os
.
path
.
basename
(
step_file
))
x3dFile
=
os
.
path
.
join
(
ROOT_DIR
,
X3D_DIR
,
partName
+
X3D_EXT
)
if
not
os
.
path
.
isfile
(
x3dFile
):
if
(
not
os
.
path
.
isfile
(
x3dFile
))
or
(
os
.
path
.
getmtime
(
step_file
)
>
os
.
path
.
getmtime
(
x3dFile
)
):
# Prepare data
FreeCAD
.
loadFile
(
os
.
path
.
join
(
ROOT_DIR
,
step_file
))
FreeCAD
.
loadFile
(
step_file
)
#
os.path.join(ROOT_DIR,step_file))
doc
=
FreeCAD
.
activeDocument
()
doc
.
Label
=
partName
x3d_objects
=
prepareX3dExport
(
doc
.
Objects
,
step_file
)
# step_file needed just for progress bar
exportX3D
(
x3d_objects
,
x3dFile
,
colorPerVertex
)
exportX3D
(
x3d_objects
,
x3dFile
,
id
=
"part_"
+
partName
,
colorPerVertex
=
colorPerVertex
)
FreeCAD
.
closeDocument
(
doc
.
Name
)
FreeCADGui
.
updateGui
()
numExported
+=
1
print
(
"Exported
%
d files as X3D in @
%
f seconds, "
%
(
numExported
,
time
.
time
()
-
start_time
))
def
matrix4ToX3D
(
m
,
eps
=
0.000001
):
#assuming 3x3 matrix is pure rotational
axis
=
FreeCAD
.
Vector
(
m
.
A32
-
m
.
A23
,
m
.
A13
-
m
.
A31
,
m
.
A21
-
m
.
A12
)
r
=
axis
.
Length
# math.sqrt(axis.X**2 + axis.Y**2 + axis.Z**2)
...
...
@@ -506,12 +918,35 @@ def matrix4ToX3D(m, eps=0.000001): #assuming 3x3 matrix is pure rotational
def
generateAssemblyX3d
(
assembly_path
,
components
=
None
,
dir_list
=
DIR_LIST
,
colorPerVertex
=
COLOR_PER_VERTEX
):
def
generateAssemblyX3d
(
assembly_path
,
components
=
None
,
dir_list
=
[
STEP_PARTS
],
colorPerVertex
=
COLOR_PER_VERTEX
,
precision_area
=
PRECISION_AREA
,
precision_volume
=
PRECISION_VOLUME
,
precision_gyration
=
PRECISION_GYRATION
,
precision_inside
=
PRECISION_INSIDE
,
precision
=
PRECISION
):
start_time
=
time
.
time
()
info_dict
=
get_info_files
(
dir_list
)
# Will (re-) build info files if missing
generatePartsX3d
(
dir_list
=
DIR_LIST
,
colorPerVertex
=
COLOR_PER_VERTEX
)
# Will only run if files are not there yet
FreeCAD
.
Console
.
PrintMessage
(
"generateAssemblyX3d()"
);
generatePartsX3d
(
dir_list
=
[
STEP_PARTS
],
colorPerVertex
=
COLOR_PER_VERTEX
)
# Will only run if files are not there yet
FreeCAD
.
Console
.
PrintMessage
(
"generatePartsX3d() Done"
);
if
not
components
:
components
=
findComponents
(
assembly_path
,
precision_inside
=
PRECISION_INSIDE
,
precision
=
PRECISION
)
components
=
COMPONENTS
# try to use global ones
if
not
components
:
# COMPONETS do not exist either - rebuild them
components
=
findComponents
(
assembly_path
,
# None is OK here
precision_area
=
precision_area
,
precision_volume
=
precision_volume
,
precision_gyration
=
precision_gyration
,
precision_inside
=
precision_inside
,
precision
=
precision
,
show_best
=
False
)
if
assembly_path
:
assName
,
_
=
os
.
path
.
splitext
(
os
.
path
.
basename
(
assembly_path
))
else
:
assName
=
FreeCAD
.
activeDocument
()
.
Objects
[
0
]
.
Label
x3dFile
=
os
.
path
.
join
(
ROOT_DIR
,
X3D_DIR
,
assName
+
X3D_EXT
)
# currently in the same directory as parts
x3dNode
=
et
.
Element
(
'x3d'
)
x3dNode
.
set
(
'profile'
,
'Interchange'
)
...
...
@@ -538,7 +973,8 @@ def generateAssemblyX3d(assembly_path, components = None, dir_list = DIR_LIST, c
else
:
print
(
"Component
%
d does not have any matches, ignoring. Candidates:
%
s"
%
(
i
,
str
(
parts
)))
continue
bbox
=
components
[
'shape'
]
.
Shells
[
i
]
.
BoundBox
# bbox=components['shape'].Shells[i].BoundBox
bbox
=
components
[
'solids'
][
i
]
.
BoundBox
bboxCenter
=
((
bbox
.
XMax
+
bbox
.
XMin
)
/
2
,(
bbox
.
YMax
+
bbox
.
YMin
)
/
2
,(
bbox
.
ZMax
+
bbox
.
ZMin
)
/
2
)
bboxSize
=
(
bbox
.
XMax
-
bbox
.
XMin
,
bbox
.
YMax
-
bbox
.
YMin
,
bbox
.
ZMax
-
bbox
.
ZMin
)
...
...
@@ -580,12 +1016,241 @@ def generateAssemblyX3d(assembly_path, components = None, dir_list = DIR_LIST, c
print
(
"Writing assembly to
%
s"
%
(
x3dFile
))
with
open
(
x3dFile
,
"wr"
)
as
f
:
f
.
write
(
reparsed
.
toprettyxml
(
indent
=
" "
))
print
(
"Assembly
%
s exported as X3D file in @
%
f seconds, "
%
(
x3dFile
,
time
.
time
()
-
start_time
))
FreeCAD
.
Console
.
PrintMessage
(
"Assembly
%
s exported as X3D file in @
%
f seconds, "
%
(
x3dFile
,
time
.
time
()
-
start_time
));
return
components
def
showFailedComponents
(
components
=
COMPONENTS
):
if
components
is
None
:
components
=
COMPONENTS
FreeCADGui
.
SendMsgToActiveView
(
"ViewFit"
)
doc
=
FreeCAD
.
activeDocument
()
for
o
in
doc
.
Objects
:
o
.
ViewObject
.
Visibility
=
False
# turn off normal objects
print
(
components
)
print
(
COMPONENTS
)
for
i
,
s
in
enumerate
(
components
[
'solids'
]):
if
not
components
[
'transformations'
][
i
]:
doc
.
addObject
(
"Part::Feature"
,
"missing_
%
d"
%
i
)
.
Shape
=
s
def
run
():
# form = X3dStepAssyDialog() # FreeCADGui.getMainWindow())
# form.show()
get_info_files
()
#X3dStepAssyDialog
########################################################################
class
X3dStepAssyDialog
(
QtGui
.
QWidget
):
""""""
assembly_path
=
None
x3d_root_path
=
None
step_parts_path
=
None
log_file
=
None
#----------------------------------------------------------------------
def
get_path_text
(
self
,
path
,
mode
=
None
):
if
path
:
return
path
if
mode
==
"assy"
:
return
"Active document"
elif
mode
==
"log"
:
return
"stdout"
else
:
return
"not set"
def
__init__
(
self
,
assembly_path
=
None
,
x3d_root_path
=
None
,
step_parts_path
=
None
):
self
.
assembly_path
=
assembly_path
self
.
x3d_root_path
=
x3d_root_path
self
.
step_parts_path
=
step_parts_path
"""Constructor"""
QtGui
.
QWidget
.
__init__
(
self
)
# ,parent=parent)
self
.
label
=
QtGui
.
QLabel
(
"Python rules!"
)
# create the buttons
label_log_file
=
QtGui
.
QLabel
(
"Log file"
)
self
.
log_file_btn
=
QtGui
.
QPushButton
(
self
.
get_path_text
(
self
.
log_file
,
"log"
))
self
.
log_file_btn
.
setToolTip
(
"Select log file for 'print' operators. Will be reset when starting macro execution"
)
label_assembly
=
QtGui
.
QLabel
(
"Assembly to process"
)
self
.
assembly_btn
=
QtGui
.
QPushButton
(
self
.
get_path_text
(
self
.
assembly_path
,
"assy"
))
self
.
assembly_btn
.
setToolTip
(
"Select assembly STEP model, if none is selected the active FreeCAD document will be used"
)
label_x3d_root_btn
=
QtGui
.
QLabel
(
"Working directory"
)
self
.
x3d_root_btn
=
QtGui
.
QPushButton
(
self
.
get_path_text
(
self
.
x3d_root_path
))
self
.
x3d_root_btn
.
setToolTip
(
"'info' and 'x3d' directories will be created/updated under the selected directory"
)
label_step_parts
=
QtGui
.
QLabel
(
"Step parts directory"
)
self
.
step_parts_btn
=
QtGui
.
QPushButton
(
self
.
get_path_text
(
self
.
step_parts_path
))
self
.
step_parts_btn
.
setToolTip
(
"Select directory containing all the parts STEP models. Will scan sub-directories"
)
self
.
help_btn
=
QtGui
.
QPushButton
(
"?"
)
self
.
execute_btn
=
QtGui
.
QPushButton
(
"Execute macro (may take hours!)"
)
self
.
log_file_btn
.
clicked
.
connect
(
self
.
selectLogFile
)
self
.
assembly_btn
.
clicked
.
connect
(
self
.
selectAssembly
)
self
.
x3d_root_btn
.
clicked
.
connect
(
self
.
selectX3dRoot
)
self
.
step_parts_btn
.
clicked
.
connect
(
self
.
selectStepParts
)
self
.
help_btn
.
clicked
.
connect
(
self
.
showHelp
)
self
.
execute_btn
.
clicked
.
connect
(
self
.
executeMacro
)
# layout widgets
layout
=
QtGui
.
QGridLayout
()
# parent=parent)
layout
.
setColumnStretch
(
1
,
1
)
layout
.
addWidget
(
label_assembly
,
0
,
0
)
layout
.
addWidget
(
self
.
assembly_btn
,
0
,
1
)
layout
.
addWidget
(
label_x3d_root_btn
,
1
,
0
)
layout
.
addWidget
(
self
.
x3d_root_btn
,
1
,
1
)
layout
.
addWidget
(
label_step_parts
,
2
,
0
)
layout
.
addWidget
(
self
.
step_parts_btn
,
2
,
1
)
layout
.
addWidget
(
label_log_file
,
3
,
0
)
layout
.
addWidget
(
self
.
log_file_btn
,
3
,
1
)
layout
.
addWidget
(
self
.
help_btn
,
4
,
0
)
layout
.
addWidget
(
self
.
execute_btn
,
4
,
1
)
self
.
setLayout
(
layout
)
# set the position and size of the window
self
.
setGeometry
(
100
,
100
,
300
,
100
)
self
.
setWindowTitle
(
"STEP assembly to X3D converter"
)
#----------------------------------------------------------------------
def
selectLogFile
(
self
):
prompt_file
=
self
.
log_file
if
not
prompt_file
:
prompt_file
=
self
.
x3d_root_path
self
.
log_file
,
_
=
QtGui
.
QFileDialog
.
getSaveFileName
(
self
,
"Select log file (or cancel to use stdout)"
,
prompt_file
)
self
.
log_file_btn
.
setText
(
self
.
get_path_text
(
self
.
log_file
,
"log"
))
def
selectAssembly
(
self
):
self
.
assembly_path
,
_
=
QtGui
.
QFileDialog
.
getOpenFileName
(
self
,
"Select assembly file (cancel to use loaded to FreeCAD)"
,
self
.
assembly_path
,
"STEP files (*.step *.stp *.STEP *.STP)"
)
self
.
assembly_btn
.
setText
(
self
.
get_path_text
(
self
.
assembly_path
,
True
))
def
selectX3dRoot
(
self
):
self
.
x3d_root_path
=
QtGui
.
QFileDialog
.
getExistingDirectory
(
self
,
"Select working directory for STEP->x3d conversion"
,
self
.
x3d_root_path
)
self
.
x3d_root_btn
.
setText
(
self
.
get_path_text
(
self
.
x3d_root_path
))
def
selectStepParts
(
self
):
self
.
step_parts_path
=
QtGui
.
QFileDialog
.
getExistingDirectory
(
self
,
"Select working directory for STEP->x3d conversion"
,
self
.
step_parts_path
)
self
.
step_parts_btn
.
setText
(
self
.
get_path_text
(
self
.
step_parts_path
))
def
showHelp
(
self
):
msg
=
(
"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 "
"references to the recognized part files, applying appropriate transformations "
"(rotations and translations).
\n\n
"
"First thing the program does is it scans all the STEP models under the "
"specified directory and collects general properties of each file, including "
"volume, surface area, center of mass, gyration radii and axes, as well as "
"per-color centers. Normally each part should contain just one solid, but if "
"there are more than one only the largest (by volume) will be used for "
"identification in the assembly (in that case assembly may show multiple not "
"matched solids that will still be correctly rendered in the final model with "
"each part).
\n\n
"
"This information will be saved in 'info' directory under the specified working "
"directory, same file name as the original STEP model but with extension "
"'.pickle' (and yes, they are just Python pickle files). These files are "
"saved in one directory, so each original part file have to have unique name, "
"even when stored in different directories. This file basename (last segment "
"of the OS path without the extension) will be used as a part name and used "
"in 'id' and 'class' properties of the result x3d files. The program only "
"processes the part files if the corresponding info file does not exist or "
"has the modification timestamp earlier than the STEP model.
\n\n
"
"During the next step the assembly object is analyzed and the same properties "
"are extracted for each solid, then the each is compared to the library part "
"and the parts with the same values (to the specified precision) are selected "
"as potential candidates. Parts material is not used, so distinguish between "
"similar screws that have the same geometry the color may be used.
\n\n
"
"This allows to find the position of the center of volume of the part in the "
"assembly, but getting the correct orientation is trickier. For the asymmetrical "
"(having all 3 different radii of gyration) it is rather easy (only 4 variants "
"to check as the gyration axes can have opposite direction), it also works for "
"the parts with full cylindrical or spherical symmetry where the axes match is not "
"required, but it is more difficult to deal with the discrete rotational symmetry. "
"When resolving such cases the program relies on colored faces of the parts. "
"Coloring just a single hole (not on the axis of the symmetry) in the part "
"(and then using it in the assembly) breaks ambiguity. Parts that do not have "
"faces that can be easily colored can be modified with boolean operations that "
"preserves the shape but add color asymmetry
\n\n
"
"When the solids are matched, the program generates missing/old (by timestamp) "
"x3d files of the individual parts and assembly in the 'x3d' subdirectory of the "
"working directory. It also generates and shows the parts that are not recognized "
"(they might be 'other' solids of the part files and so will be available in the "
" generated model).
\n\n
"
"This method can work with most modern CAD systems, and does not require special "
"export - the colored STEP files are still good for production. In some systems "
"the assembly model should be flattened (removed assembly status) before STEP "
"export, it is also advised to import individual parts that are provided to you "
"as STEP models to the CAD that is used for the assembly and re-exporting to STEP "
"so both part and assembly STEP files will be generated by the same software.
\n\n
"
)
msgBox
=
QtGui
.
QMessageBox
(
QtGui
.
QMessageBox
.
Question
,
"About STEP->X3D Assembly converter"
,
msg
)
msgBox
.
exec_
()
# msgBox = QtGui.QMessageBox.about(self,"About STEP->X3D Assembly converter",msg )
# msgBox.setText("The document has been modified.")
# msgBox.exec_()
def
executeMacro
(
self
):
global
ROOT_DIR
,
ASSEMBLY_PATH
,
STEP_PARTS
,
COMPONENTS
COMPONENTS
=
None
# Start with new ones
# print ("ROOT_DIR=%s"%(ROOT_DIR))
# msgBox = QtGui.QMessageBox(QtGui.QMessageBox.Question, "About STEP->X3D Assembly converter", "ROOT_DIR=%s"%(ROOT_DIR))
# msgBox.exec_()
FreeCAD
.
Console
.
PrintMessage
(
"Starting execution..."
);
ASSEMBLY_PATH
=
self
.
assembly_path
ROOT_DIR
=
self
.
x3d_root_path
STEP_PARTS
=
self
.
step_parts_path
if
self
.
log_file
:
sys
.
stdout
=
open
(
self
.
log_file
,
"w"
)
else
:
sys
.
stdout
=
sys
.
__stdout__
try
:
# does not work
components
=
generateAssemblyX3d
(
self
.
assembly_path
)
# If None - will use ActiveDocument().Objects
except
:
self
.
errorDialog
(
traceback
.
format_exc
())
showFailedComponents
(
components
)
sys
.
stdout
.
close
()
sys
.
stdout
=
sys
.
__stdout__
def
errorDialog
(
msg
):
# Create a simple dialog QMessageBox
# The first argument indicates the icon used: one of QtGui.QMessageBox.{NoIcon, Information, Warning, Critical, Question}
diag
=
QtGui
.
QMessageBox
(
QtGui
.
QMessageBox
.
Error
,
'Error in macro'
,
msg
)
diag
.
setWindowModality
(
QtCore
.
Qt
.
ApplicationModal
)
diag
.
exec_
()
#----------------------------------------------------------------------
if
__name__
==
"__main__"
:
run
()
form
=
X3dStepAssyDialog
(
assembly_path
=
ASSEMBLY_PATH
,
x3d_root_path
=
ROOT_DIR
,
step_parts_path
=
STEP_PARTS
)
# FreeCADGui.getMainWindow())
form
.
show
()
# run()
# def __init__(self, assembly_path=None, x3d_root_path=None, step_parts_path = None):
"""
reload (x3d_step_assy)
form = x3d_step_assy.X3dStepAssyDialog(assembly_path= x3d_step_assy.ASSEMBLY_PATH, x3d_root_path = x3d_step_assy.ROOT_DIR, step_parts_path = x3d_step_assy.STEP_PARTS)
form.show()
components= x3d_step_assy.generateAssemblyX3d(x3d_step_assy.ASSEMBLY_PATH)
>>> Gui.getDocument("Unnamed").getObject("Part__Feature").Visibility=False
"""
\ No newline at end of file
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment