-
Notifications
You must be signed in to change notification settings - Fork 9
/
lattice2Subsequencer.py
393 lines (324 loc) · 17.6 KB
/
lattice2Subsequencer.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
#***************************************************************************
#* *
#* Copyright (c) 2017 - Victor Titov (DeepSOIC) *
#* <[email protected]> *
#* *
#* This program is free software; you can redistribute it and/or modify *
#* it under the terms of the GNU Lesser General Public License (LGPL) *
#* as published by the Free Software Foundation; either version 2 of *
#* the License, or (at your option) any later version. *
#* for detail see the LICENCE text file. *
#* *
#* 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 Library General Public License for more details. *
#* *
#* You should have received a copy of the GNU Library General Public *
#* License along with this program; if not, write to the Free Software *
#* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
#* USA *
#* *
#***************************************************************************
__title__="Subsequencer: module with code to extract chains of similar elements from compounds"
__author__ = "DeepSOIC"
__url__ = ""
# -----------------------<CONSTANTS>-------------------------
TRAVERSAL_MODES = ['Compund children','Compound leaves']
LOOP_MODES = ['Till end', 'All around', 'All from first']
# -----------------------</CONSTANTS>------------------------
# -----------------------<EXCEPTIONS>------------------------
class TraversalError(TypeError):
"Error to raise when extracting children from compound fails"
pass
class SubsequencingError(ValueError):
"General error, that subsequencing is impossible or failed"
pass
class SubsequencingError_LinkType(SubsequencingError):
"Subsequencing can't be applied to this link type (e.g. App::PropertyLink)."
pass
class SubsequencingError_LinkValue(SubsequencingError):
"Type of links to subsequence are right, but they happen to be links to whole objects, or otherwise impossible to iterate"
pass
# -----------------------</EXCEPTIONS>------------------------
# -----------------------<HELPER STUFF>-----------------------
try:
basestring = basestring
except Exception:
basestring = str #Py3 compatibility
class HashableShape(object):
"Decorator for Part.Shape, that can be used as key in dicts. Based on isSame method."
def __init__(self, shape):
self.Shape = shape
self.hash = shape.hashCode()
def __eq__(self, other):
return self.Shape.isSame(other.Shape)
def __hash__(self):
return self.hash
def traverseCompound(compound, traversal):
if traversal == 'Compund children':
if compound.ShapeType != 'Compound':
raise TraversalError("Shape is not compound; can't traverse it in direct children mode.")
return compound.childShapes()
elif traversal == 'Compound leaves':
import lattice2CompoundExplorer as LCE
return LCE.allLeaves(compound)
element_extractors = {
"Vertex": (lambda sh: sh.Vertexes),
"Edge": (lambda sh: sh.Edges),
"Wire": (lambda sh: sh.Wires),
"Face": (lambda sh: sh.Faces),
"Shell": (lambda sh: sh.Shells),
"Solid": (lambda sh: sh.Solids),
"CompSolid": (lambda sh: sh.CompSolids),
"Compound": (lambda sh: sh.Compounds),
}
def getIndexesIntoList(element, list_of_shapes):
"""Finds this element in shapes in list_of_shapes. This is a generator function (to be
used if there are multiple occurrences of the element).
[(index_into_list, element_type_string, subelement_index), ...]"""
element_type_string = element.ShapeType
element_extractor = element_extractors[element_type_string]
ret = []
for i_sh in range(len(list_of_shapes)):
elements = element_extractor(list_of_shapes[i_sh])
for i_el in range(len(elements)):
if elements[i_el].isEqual(element):
# to make link more robust, use negative index if one is closer to the end
if i_el * 2 > len(elements):
i_el = i_el - len(elements)
yield (i_sh, element_type_string, i_el)
# -----------------------</HELPER STUFF>-----------------------
# -------------------<LINK TYPE CONVERSION>--------------------
def linkSubList_convertToOldStyle(references):
("input: [(obj1, (sub1, sub2)), (obj2, (sub1, sub2))]\n"
"output: [(obj1, sub1), (obj1, sub2), (obj2, sub1), (obj2, sub2)]")
result = []
for tup in references:
if type(tup[1]) is tuple or type(tup[1]) is list:
for subname in tup[1]:
result.append((tup[0], subname))
if len(tup[1]) == 0:
result.append((tup[0], ''))
elif isinstance(tup[1],basestring):
# old style references, no conversion required
result.append(tup)
return result
def toLinkSubList(linksub):
if linksub is None:
return []
object, subs = linksub
if not hasattr(subs, '__iter__'):
subs = [subs]
return [(object, sub) for sub in subs]
def toLinkSub(linksublist):
if not linksublist:
return None
linksublist = linkSubList_convertToOldStyle(linksublist)
subs = []
object = linksublist[0][0]
for itobj, sub in linksublist:
if itobj is not object:
raise ValueError("LinkSubList can't be converted into LinkSub because it links to more than one object.")
subs.append(sub)
return (object, subs)
# -------------------</LINK TYPE CONVERSION>--------------------
# ---------------------------<API>------------------------------
def Subsequence_basic(link, traversal, loop):
"""Subsequence_basic(link, traversal, loop): low-level function powering Subsequencing. Transforms
a single sub-link into a list of sub-links to every child of the linked compound.
link: tuple (object, subelement), where object is a document object, and subelement is
a string like 'Edge3'.
traversal: either of values from TRAVERSAL_MODES, that sets how to enumerate children
loop: either of values from LOOP_MODES. Sets which children to use.
returns: list of links [(object,subelement), ....]"""
# extract shapes of the array
compound = link[0].Shape
children = traverseCompound(compound, traversal)
# parse link string. Input: element_string. Output: element_shape, element_type_string
element_string = link[1]
if not issubclass(type(element_string), basestring):
raise TypeError("Second element of link tuple must be a string, not {typ}".format(typ= type(element_string).__name__))
element_type_string = None
for key in element_extractors.keys():
if element_string.startswith(key):
element_type_string = key
break
if element_type_string is None:
# raise ValueError("Subelement string format not recognized")
# resort to generic FreeCAD method
element_shape = compound.getElement(element_string)
else:
# use negative-index-aware method
#extract index from string:
index = -1 + int( element_string[ len(element_type_string) : ] )
element_shape = element_extractors[element_type_string](compound)[index]
# convert global element index to index in child
i_first_child, element_type_string, i_in_child = next(getIndexesIntoList(element_shape, children))
if loop == 'All from first':
i_first_child = 0
elements = element_extractors[element_type_string](compound)
index_dict = dict([(HashableShape(elements[i]), i) for i in range(len(elements))])
# find the element in each child, find out its global index, and output result in a form of a string for a link
ret = [] #list of tuples (object, subelement_string)
for i in range( len(children) if loop != 'Till end' else len(children) - i_first_child ):
i_child = (i + i_first_child) % len(children)
element = element_extractors[element_type_string](children[i_child])[i_in_child]
ret.append((
link[0],
element_type_string + str(index_dict[HashableShape(element)]+1)
))
return ret
def Subsequence_LinkSubList(linksublist, traversal = TRAVERSAL_MODES[0], loop = LOOP_MODES[0], object_filter = None, index_filter = None):
"""Subsequence_LinkSubList(linksublist, traversal = TRAVERSAL_MODES[0], loop = 'Till end', object_filter = None):
form a list of values for iterating over elements in App::PropertyLinkSubList.
linksublist: the value of property (either of type [(object, sub), ..], or
[(object, [sub1, sub2, ...]), ...]
traversal: how to unpack compounds
loop: sets which children to process.
object_filter: list or set of objects that should be considered being arrays. If
omitted, all links to subelements are attempted to be enumerated as arrays.
index_list: list or set of ints, that sets which parts of link are to be subsequenced.
If None, all elements are attempted to be subsequenced, and only if none can be, an error
is raised. If index_list is specified, it is treated as strict, and if any
corresponding sublink can't be subsequenced, an error is raised.
return: list of values that can be assigned to the link in a loop.
"""
linksublist = linkSubList_convertToOldStyle(linksublist)
if object_filter is None:
object_filter = [obj for (obj,sub) in linksublist] #number of links is likely quite low, so using sets might give more of a penalty than gain
loops = [] #list to receive subsequences for made for pieces of the link
n_seq = None
i = -1
for object,sub in linksublist:
i += 1
if (sub
and (object in object_filter)
and (index_filter is None or i in index_filter)
and hasattr(object, "Shape") ):
try:
seq = Subsequence_basic((object, sub), traversal, loop)
if len(seq) < 2:
from lattice2Executer import warning
warning(None, u"Subsequencing link index {i} to {sub} of '{obj}' yielded only one item."
.format(i= i, obj= object.Label, sub= sub))
except TraversalError:
if index_filter is not None:
raise # re-raise. When index list is given, treat it as that it must be subsequenced.
loops.append((object,sub))
continue
loops.append(seq)
if n_seq is None:
n_seq = len(seq)
else:
n_seq = min(n_seq, len(seq))
else:
if index_filter and i in index_filter:
if not sub:
raise SubsequencingError_LinkValue("Sublink part {index} can't be subsequenced, because it's a link to whole object, not to subelement.".format(index= i))
loops.append((object,sub))
assert(len(loops) == len(linksublist))
if n_seq is None:
raise SubsequencingError_LinkValue("In supplied link, nothing to loop over compounds was found.")
# expand non-subsequenced parts of linksublist
for i_loop in range(len(loops)):
if type(loops[i_loop]) is not list:
loops[i_loop] = [loops[i_loop]]*n_seq
# form the result
ret = []
for i_seq in range(n_seq):
ret.append(
[loop[i_seq] for loop in loops]
)
return ret
def Subsequence_LinkSub(linksub, traversal = TRAVERSAL_MODES[0], loop = LOOP_MODES[0], object_filter = None, index_filter = None):
"""Subsequence_LinkSub(linksub, traversal = TRAVERSAL_MODES[0], loop = LOOP_MODES[0], object_filter = None):
form a list of values for iterating over elements in App::PropertyLinkSub.
linksub: the value of property, like (object, ["Edge1", "Edge3"...])
traversal: how to unpack compounds
loop: sets which children to process.
object_filter: list or set of objects that should be considered being arrays. If
omitted, all links to subelements are attempted to be enumerated as arrays.
index_list: list or set of ints, that sets which parts of link are to be subsequenced.
If None, all elements are attempted to be subsequenced, and only if none can be, an error
is raised. If index_list is specified, it is treated as strict, and if any
corresponding sublink can't be subsequenced, an error is raised.
return: list of values that can be assigned to the link in a loop.
"""
object, subs = linksub
if not hasattr(subs, '__iter__'):
subs = [subs]
linksublist = toLinkSubList(linksub)
vals = Subsequence_LinkSubList(linksublist, traversal, loop, object_filter, index_filter)
return [toLinkSub(v) for v in vals]
def Subsequence_auto(link, traversal = TRAVERSAL_MODES[0], loop = LOOP_MODES[0], object_filter = None, index_filter = None):
"""Subsequence_auto(link, traversal = TRAVERSAL_MODES[0], loop = LOOP_MODES[0], object_filter = None, index_filter = None):
form a list of values for iterating over elements in App::PropertyLinkSub or
App::PropertyLinkSubList.
link: the value of property
traversal: how to unpack compounds
loop: sets which children to process.
object_filter: list or set of objects that should be considered being arrays. If
omitted, all links to subelements are attempted to be enumerated as arrays.
index_list: list or set of ints, that sets which parts of link are to be subsequenced.
If None, all elements are attempted to be subsequenced, and only if none can be, an error
is raised. If index_list is specified, it is treated as strict, and if any
corresponding sublink can't be subsequenced, an error is raised.
return: list of values that can be assigned to the link in a loop.
"""
if type(link) is list:
# linkSubList or linkList!
if len(link) == 0:
raise SubsequencingError_LinkValue("Cannot subsequence empty link (link is empty list).")
if type(link[0]) is not tuple:
raise SubsequencingError_LinkType("LinkList cannot be subsequenced (only LinkSub or LinkSubList can).")
return Subsequence_LinkSubList(link, traversal, loop, object_filter, index_filter)
elif type(link) is tuple:
# linkSub!
return Subsequence_LinkSub(link, traversal, loop, object_filter, index_filter)
elif link is None:
raise SubsequencingError_LinkValue("Cannot subsequence empty link (link is None).")
else:
raise SubsequencingError_LinkType("Link type not recognized.")
def Subsequence_LinkDict(link_dict, traversal = TRAVERSAL_MODES[0], loop = LOOP_MODES[0], object_filter = None, index_filter = None):
"""Subsequence_LinkDict(link_dict, traversal = TRAVERSAL_MODES[0], loop = LOOP_MODES[0], object_filter = None, index_filter = None):
subsequence a heterogeneous list of links supplied as a dict.
link_dict: dictionary. Key is any, value is the value of some link property. It can be
SubLink, SubList, and plain Link, and LinkList (last two are skipped).
traversal: how to unpack compounds
loop: sets which children to process.
object_filter: list or set of objects that should be considered being arrays. If
omitted, all links to subelements are attempted to be enumerated as arrays.
index_list: list or set of ints, that sets which parts of link are to be subsequenced.
If None, all elements are attempted to be subsequenced, and only if none can be, an error
is raised. If index_list is specified, it is treated as strict, and if any
corresponding sublink can't be subsequenced, an error is raised.
return: tuple (int, dict). Int is the length of the subsequence. Dict contains the
subsequences. It is similar to input dikt, but link values were replaced with lists of
link values. If a link failed to be subsequenced, the key/value pair is not added to
return dict at all."""
ret_dict = {} #ret_dict is the dict to receive result
err_dict = {}
# subsequence all links
n_seq = None # length of shortest subsequence; None means no subsequences were made yet.
for key in link_dict.keys():
link = link_dict[key]
try:
link_seq = Subsequence_auto(link, traversal, loop, object_filter, index_filter)
n_seq = len(link_seq) if n_seq is None else min(n_seq, len(link_seq))
ret_dict[key] = link_seq
# silently absorb non-subsequencable links; fail only if everything fails
except SubsequencingError_LinkType as err:
err_dict[key] = err
except SubsequencingError_LinkValue as err:
err_dict[key] = err
# trim them to shortest subsequence
if n_seq is None:
raise SubsequencingError_LinkValue("In supplied links, nothing to loop over compounds was found. Key-by-key errors follow: {err_list}"
.format(err_list= "\n".join([repr(key)+": "+str(err_dict[key]) for key in err_dict])))
for key in ret_dict.keys():
seq = ret_dict[key]
seq = seq[0:n_seq]
ret_dict[key] = seq
# done!
return (n_seq, ret_dict)