1 # ##### BEGIN GPL LICENSE BLOCK #####
3 # This program is free software; you can redistribute it and/or
4 # modify it under the terms of the GNU General Public License
5 # as published by the Free Software Foundation; either version 2
6 # of the License, or (at your option) any later version.
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
13 # You should have received a copy of the GNU General Public License
14 # along with this program; if not, write to the Free Software Foundation,
15 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 # ##### END GPL LICENSE BLOCK #####
19 """Manipulations of Models.
22 __author__
= "howard.trickey@gmail.com"
30 def PolyAreasToModel(polyareas
, bevel_amount
, bevel_pitch
, quadrangulate
):
31 """Convert a PolyAreas into a Model object.
33 Assumes polyareas are in xy plane.
36 polyareas: geom.PolyAreas
37 bevel_amount: float - if > 0, amount of bevel
38 bevel_pitch: float - if > 0, angle in radians of bevel
39 quadrangulate: bool - should n-gons be quadrangulated?
47 polyareas
.points
.AddZCoord(0.0)
48 m
.points
= polyareas
.points
49 for pa
in polyareas
.polyareas
:
50 PolyAreaToModel(m
, pa
, bevel_amount
, bevel_pitch
, quadrangulate
)
54 def PolyAreaToModel(m
, pa
, bevel_amount
, bevel_pitch
, quadrangulate
):
55 if bevel_amount
> 0.0:
56 BevelPolyAreaInModel(m
, pa
, bevel_amount
, bevel_pitch
, quadrangulate
,
61 qpa
= triquad
.QuadrangulateFaceWithHoles(pa
.poly
, pa
.holes
, pa
.points
)
63 m
.face_data
.extend([pa
.data
] * len(qpa
))
65 m
.faces
.append(pa
.poly
)
66 # TODO: just the first part of QuadrangulateFaceWithHoles, to join
68 m
.face_data
.append(pa
.data
)
71 def ExtrudePolyAreasInModel(mdl
, polyareas
, depth
, cap_back
):
72 """Extrude the boundaries given by polyareas by -depth in z.
74 Assumes polyareas are in xy plane.
77 mdl: geom.Model - where to do extrusion
78 polyareas: geom.Polyareas
80 cap_back: bool - if True, cap off the back
82 For all edges in polys in polyareas, make quads in Model
83 extending those edges by depth in the negative z direction.
84 The application data will be the data of the face that the edge
88 for pa
in polyareas
.polyareas
:
89 back_poly
= _ExtrudePoly(mdl
, pa
.poly
, depth
, pa
.data
, True)
92 back_holes
.append(_ExtrudePoly(mdl
, p
, depth
, pa
.data
, False))
94 qpa
= triquad
.QuadrangulateFaceWithHoles(back_poly
, back_holes
,
96 # need to reverse each poly to get normals pointing down
97 for i
, p
in enumerate(qpa
):
101 mdl
.faces
.extend(qpa
)
102 mdl
.face_data
.extend([pa
.data
] * len(qpa
))
105 def _ExtrudePoly(mdl
, poly
, depth
, data
, isccw
):
106 """Extrude the poly by -depth in z
109 mdl: geom.Model - where to do extrusion
110 poly: list of vertex indices
112 data: application data
113 isccw: True if counter-clockwise
115 For all edges in poly, make quads in Model
116 extending those edges by depth in the negative z direction.
117 The application data will be the data of the face that the edge
120 list of int - vertices for extruded poly
131 for i
, v
in enumerate(poly
):
132 vnext
= poly
[(i
+ incr
) % len(poly
)]
133 (x0
, y0
, z0
) = points
.pos
[v
]
134 (x1
, y1
, z1
) = points
.pos
[vnext
]
135 vextrude
= points
.AddPoint((x0
, y0
, z0
- depth
))
136 vnextextrude
= points
.AddPoint((x1
, y1
, z1
- depth
))
138 sideface
= [v
, vextrude
, vnextextrude
, vnext
]
140 sideface
= [v
, vnext
, vnextextrude
, vextrude
]
141 mdl
.faces
.append(sideface
)
142 mdl
.face_data
.append(data
)
143 extruded_poly
.append(vextrude
)
147 def BevelPolyAreaInModel(mdl
, polyarea
,
148 bevel_amount
, bevel_pitch
, quadrangulate
, as_percent
):
149 """Bevel the interior of polyarea in model.
151 This does smart beveling: advancing edges are merged
152 rather than doing an 'overlap'. Advancing edges that
153 hit an opposite edge result in a split into two beveled areas.
155 If the polyarea is not in the xy plane, do the work in a
156 transformed model, and then transfer the changes back.
159 mdl: geom.Model - where to do bevel
160 polyarea geom.PolyArea - area to bevel into
161 bevel_amount: float - if > 0, amount of bevel
162 bevel_pitch: float - if > 0, angle in radians of bevel
163 quadrangulate: bool - should n-gons be quadrangulated?
164 as_percent: bool - if True, interpret amount as percent of max
166 Faces and points are added to model to model the
167 bevel and the interior of the polyareas.
170 pa_norm
= polyarea
.Normal()
171 if pa_norm
== (0.0, 0.0, 1.0):
175 (pa_rot
, inv_rot
, inv_map
) = _RotatedPolyAreaToXY(polyarea
, pa_norm
)
176 # don't have to add the original faces into model, just their points.
178 m
.points
= pa_rot
.points
179 vspeed
= math
.tan(bevel_pitch
)
180 off
= offset
.Offset(pa_rot
, 0.0, vspeed
)
182 bevel_amount
= bevel_amount
* off
.MaxAmount() / 100.0
183 off
.Build(bevel_amount
)
184 inner_pas
= AddOffsetFacesToModel(m
, off
, polyarea
.data
)
185 for pa
in inner_pas
.polyareas
:
187 if len(pa
.poly
) == 0:
189 qpa
= triquad
.QuadrangulateFaceWithHoles(pa
.poly
, pa
.holes
,
192 m
.face_data
.extend([pa
.data
] * len(qpa
))
194 m
.faces
.append(pa
.poly
)
195 m
.face_data
.append(pa
.data
)
197 _AddTransformedPolysToModel(mdl
, m
.faces
, m
.points
, m
.face_data
,
201 def AddOffsetFacesToModel(mdl
, off
, data
=None):
202 """Add the faces due to an offset into model.
204 Returns the remaining interiors of the offset as a PolyAreas.
207 mdl: geom.Model - model to add offset faces into
209 data: any - application data to be copied to the faces
214 mdl
.points
= off
.polyarea
.points
215 assert(len(mdl
.points
.pos
) == 0 or len(mdl
.points
.pos
[0]) == 3)
220 for face
in o
.facespokes
:
222 for i
, spoke
in enumerate(face
):
223 nextspoke
= face
[(i
+ 1) % n
]
225 v1
= nextspoke
.origin
231 mface
= [v0
, v1
, v2
, v3
]
232 mdl
.faces
.append(mface
)
233 mdl
.face_data
.append(data
)
234 ostack
.extend(o
.inneroffsets
)
239 return off
.InnerPolyAreas()
242 def BevelSelectionInModel(mdl
, bevel_amount
, bevel_pitch
, quadrangulate
,
243 as_region
, as_percent
):
244 """Bevel all the faces in the model, perhaps as one region.
246 If as_region is False, each face is beveled individually,
247 otherwise regions of contiguous faces are merged into
248 PolyAreas and beveled as a whole.
250 TODO: something if extracted PolyAreas are not approximately
255 bevel_amount: float - amount to inset
256 bevel_pitch: float - angle of bevel side
257 quadrangulate: bool - should insides be quadrangulated?
258 as_region: bool - should faces be merged into regions?
259 as_percent: bool - should amount be interpreted as a percent
260 of the maximum amount (if True) or an absolute amount?
262 Beveling faces will be added to the model
267 pas
= RegionToPolyAreas(mdl
.faces
, mdl
.points
, mdl
.face_data
)
269 for f
, face
in enumerate(mdl
.faces
):
270 pas
.append(geom
.PolyArea(mdl
.points
, face
, [],
273 BevelPolyAreaInModel(mdl
, pa
,
274 bevel_amount
, bevel_pitch
, quadrangulate
, as_percent
)
277 def RegionToPolyAreas(faces
, points
, data
):
278 """Find polygonal outlines induced by union of faces.
280 Finds the polygons formed by boundary edges (those not
281 sharing an edge with another face in region_faces), and
282 turns those into PolyAreas.
283 In the general case, there will be holes inside.
284 We want to associate data with the region PolyAreas.
285 Just choose a representative element of data[] when
286 more than one face is combined into a PolyArea.
289 faces: list of list of int - each sublist is a face (indices into points)
290 points: geom.Points - gives coordinates for vertices
291 data: list of any - parallel to faces, app data to put in PolyAreas
293 list of geom.PolyArea
297 (edges
, vtoe
) = _GetEdgeData(faces
)
298 (face_adj
, is_interior_edge
) = _GetFaceGraph(faces
, edges
, vtoe
, points
)
299 (components
, ftoc
) = _FindFaceGraphComponents(faces
, face_adj
)
300 for c
in range(len(components
)):
301 boundary_edges
= set()
304 for e
, ((vs
, ve
), f
) in enumerate(edges
):
305 if ftoc
[f
] != c
or is_interior_edge
[e
]:
307 boundary_edges
.add(e
)
308 # vstobe[v] is set of edges leaving v
309 # (could be more than one if boundary touches itself at a vertex)
314 betodata
[(vs
, ve
)] = data
[f
]
317 while boundary_edges
:
318 e
= boundary_edges
.pop()
319 ((vstart
, ve
), face_i
) = edges
[e
]
321 datum
= betodata
[(vstart
, ve
)]
324 print("whoops, couldn't close boundary")
330 # find a next edge with face index face_i
331 # TODO: this is not guaranteed to work,
332 # as continuation edge may have been for a different
333 # face that is now combined with face_i by erasing
334 # interior edges. Find a better algorithm here.
336 for ne_cand
in nextes
:
337 if edges
[ne_cand
][1] == face_i
:
341 # case mentioned in TODO may have happened;
342 # just choose any nexte - may mess things up
344 ((_
, ve
), face_i
) = edges
[nexte
]
345 if nexte
not in boundary_edges
:
346 print("whoops, nexte not a boundary edge", nexte
)
348 boundary_edges
.remove(nexte
)
352 poly_data
.append(datum
)
354 # can happen if an entire closed polytope is given
355 # at least until we do an edge check
357 elif len(polys
) == 1:
358 ans
.append(geom
.PolyArea(points
, polys
[0], [], poly_data
[0]))
360 outerf
= _FindOuterPoly(polys
, points
, faces
)
361 pa
= geom
.PolyArea(points
, polys
[outerf
], [], poly_data
[outerf
])
362 pa
.holes
= [polys
[i
] for i
in range(len(polys
)) if i
!= outerf
]
367 def _GetEdgeData(faces
):
368 """Find edges from faces, and some lookup dictionaries.
371 faces: list of list of int - each a closed CCW polygon of vertex indices
373 (list of ((int, int), int), dict{ int->list of int}) -
374 list elements are ((startv, endv), face index)
375 dict maps vertices to edge indices
380 for findex
, f
in enumerate(faces
):
382 for i
, v
in enumerate(f
):
383 endv
= f
[(i
+ 1) % nf
]
384 edges
.append(((v
, endv
), findex
))
385 eindex
= len(edges
) - 1
387 vtoe
[v
].append(eindex
)
393 def _GetFaceGraph(faces
, edges
, vtoe
, points
):
394 """Find the face adjacency graph.
396 Faces are adjacent if they share an edge,
397 and the shared edge goes in the reverse direction,
398 and if the angle between them isn't too large.
401 faces: list of list of int
402 edges: list of ((int, int), int) - see _GetEdgeData
403 vtoe: dict{ int->list of int } - see _GetEdgeData
406 (list of list of int, list of bool) -
407 first list: each sublist is adjacent face indices for each face
408 second list: maps edge index to True if it separates adjacent faces
411 face_adj
= [[] for i
in range(len(faces
))]
412 is_interior_edge
= [False] * len(edges
)
413 for e
, ((vs
, ve
), f
) in enumerate(edges
):
414 for othere
in vtoe
[ve
]:
415 ((_
, we
), g
) = edges
[othere
]
417 # face g is adjacent to face f
419 if g
not in face_adj
[f
]:
420 face_adj
[f
].append(g
)
421 is_interior_edge
[e
] = True
422 # Don't bother with mirror relations, will catch later
423 return (face_adj
, is_interior_edge
)
426 def _FindFaceGraphComponents(faces
, face_adj
):
427 """Partition faces into connected components.
430 faces: list of list of int
431 face_adj: list of list of int - see _GetFaceGraph
433 (list of list of int, list of int) -
434 first list partitions face indices into separate lists,
436 second list maps face indices into their component index
442 ftoc
= [-1] * len(faces
)
443 for i
in range(len(faces
)):
445 compi
= len(components
)
447 _FFGCSearch(i
, faces
, face_adj
, ftoc
, compi
, comp
)
448 components
.append(comp
)
449 return (components
, ftoc
)
452 def _FFGCSearch(findex
, faces
, face_adj
, ftoc
, compi
, comp
):
453 """Depth first search helper function for _FindFaceGraphComponents
455 Searches recursively through all faces connected to findex, adding
456 each face found to comp and setting ftoc for that face to compi.
461 for otherf
in face_adj
[findex
]:
462 if ftoc
[otherf
] == -1:
463 _FFGCSearch(otherf
, faces
, face_adj
, ftoc
, compi
, comp
)
466 def _FindOuterPoly(polys
, points
, faces
):
467 """Assuming polys has one CCW-oriented face when looking
468 down average normal of faces, return that one.
470 Only one of the faces should have a normal whose dot product
471 with the average normal of faces is positive.
474 polys: list of list of int - list of polys given by vertex indices
476 faces: list of list of int - original selected region, used to find
479 int - the index in polys of the outermost one
484 fnorm
= (0.0, 0.0, 0.0)
487 fnorm
= geom
.VecAdd(fnorm
, geom
.Newell(face
, points
))
488 if fnorm
== (0.0, 0.0, 0.0):
490 # fnorm is really a multiple of the normal, but fine for test below
491 for i
, poly
in enumerate(polys
):
493 pnorm
= geom
.Newell(poly
, points
)
494 if geom
.VecDot(fnorm
, pnorm
) > 0:
496 print("whoops, couldn't find an outermost poly")
500 def _RotatedPolyAreaToXY(polyarea
, norm
):
501 """Return a PolyArea rotated to xy plane.
503 Only the points in polyarea will be transferred.
506 polyarea: geom.PolyArea
507 norm: the normal for polyarea
509 (geom.PolyArea, (float, ..., float), dict{ int -> int }) - new PolyArea,
510 4x3 inverse transform, dict mapping new verts to old ones
513 # find rotation matrix that takes norm to (0,0,1)
515 if abs(nx
) < abs(ny
) and abs(nx
) < abs(nz
):
516 v
= (vx
, vy
, vz
) = geom
.Norm3(0.0, nz
, - ny
)
517 elif abs(ny
) < abs(nz
):
518 v
= (vx
, vy
, vz
) = geom
.Norm3(nz
, 0.0, - nx
)
520 v
= (vx
, vy
, vz
) = geom
.Norm3(ny
, - nx
, 0.0)
521 (ux
, uy
, uz
) = geom
.Cross3(v
, norm
)
522 rotmat
= [ux
, vx
, nx
, uy
, vy
, ny
, uz
, vz
, nz
, 0.0, 0.0, 0.0]
523 # rotation matrices are orthogonal, so inverse is transpose
524 invrotmat
= [ux
, uy
, uz
, vx
, vy
, vz
, nx
, ny
, nz
, 0.0, 0.0, 0.0]
527 newpoints
= geom
.Points()
528 for poly
in [polyarea
.poly
] + polyarea
.holes
:
530 vcoords
= polyarea
.points
.pos
[v
]
531 newvcoords
= geom
.MulPoint3(vcoords
, rotmat
)
532 newv
= newpoints
.AddPoint(newvcoords
)
534 invpointmap
[newv
] = v
535 pa
= geom
.PolyArea(newpoints
)
536 pa
.poly
= [pointmap
[v
] for v
in polyarea
.poly
]
537 pa
.holes
= [[pointmap
[v
] for v
in hole
] for hole
in polyarea
.holes
]
538 pa
.data
= polyarea
.data
539 return (pa
, invrotmat
, invpointmap
)
542 def _AddTransformedPolysToModel(mdl
, polys
, points
, poly_data
,
543 transform
, pointmap
):
544 """Add (transformed) the points and faces to a model.
546 Add polys to mdl. The polys have coordinates given by indices
547 into points.pos; those need to be transformed by multiplying by
548 the transform matrix.
549 The vertices may already exist in mdl. Rather than relying on
550 AddPoint to detect the duplicate (transform rounding error makes
551 that dicey), the pointmap dictionar is used to map vertex indices
552 in polys into those in mdl - if they exist already.
555 mdl: geom.Model - where to put new vertices, faces
556 polys: list of list of int - each sublist a poly
557 points: geom.Points - coords for vertices in polys
558 poly_data: list of any - parallel to polys
559 transform: (float, ..., float) - 12-tuple, a 4x3 transform matrix
560 pointmap: dict { int -> int } - maps new vertex indices to old ones
562 The model gets new faces and vertices, based on those in polys.
563 We are allowed to modify pointmap, as it will be discarded after call.
566 for i
, coords
in enumerate(points
.pos
):
567 if i
not in pointmap
:
568 p
= geom
.MulPoint3(coords
, transform
)
569 pointmap
[i
] = mdl
.points
.AddPoint(p
)
570 for i
, poly
in enumerate(polys
):
571 mpoly
= [pointmap
[v
] for v
in poly
]
572 mdl
.faces
.append(mpoly
)
573 mdl
.face_data
.append(poly_data
[i
])