1 # -*- coding: utf-8 -*-
2 # ##### BEGIN GPL LICENSE BLOCK #####
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation, either version 2 of the License, or
7 # (at your option) any later version.
9 # This program is distributed in the hope that it will be useful,
10 # but without any warranty; without even the implied warranty of
11 # merchantability or fitness for a particular purpose. See the
12 # GNU General Public License for more details.
14 # You should have received a copy of the GNU General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
17 # ##### END GPL LICENSE BLOCK #####
20 "name": "Export Paper Model",
21 "author": "Addam Dominec",
23 "blender": (2, 73, 0),
24 "location": "File > Export > Paper Model",
26 "description": "Export printable net of the active mesh",
27 "category": "Import-Export",
28 "wiki_url": "http://wiki.blender.org/index.php/Extensions:2.6/Py/"
29 "Scripts/Import-Export/Paper_Model",
30 "tracker_url": "https://developer.blender.org/T38441"
34 # sanitize the constructors Edge, Face, UVFace so that they don't edit their parent object
35 # The Exporter classes should take parameters as a whole pack, and parse it themselves
36 # remember objects selected before baking (except selected to active)
37 # add 'estimated number of pages' to the export UI
38 # profile QuickSweepline vs. BruteSweepline with/without blist: for which nets is it faster?
39 # rotate islands to minimize area -- and change that only if necessary to fill the page size
40 # Sticker.vertices should be of type Vector
42 # check conflicts in island naming and either:
43 # * append a number to the conflicting names or
44 # * enumerate faces uniquely within all islands of the same name (requires a check that both label and abbr. equals)
50 e-mail: adominec {at} gmail {dot} com
57 from re
import compile as re_compile
58 from itertools
import chain
, repeat
59 from math
import pi
, ceil
62 import os
.path
as os_path
67 from blist
import blist
71 default_priority_effect
= {
77 global_paper_sizes
= [
78 ('USER', "User defined", "User defined paper size"),
79 ('A4', "A4", "International standard paper size"),
80 ('A3', "A3", "International standard paper size"),
81 ('US_LETTER', "Letter", "North American paper size"),
82 ('US_LEGAL', "Legal", "North American paper size")
86 def first_letters(text
):
87 """Iterator over the first letter of each word"""
88 for match
in first_letters
.pattern
.finditer(text
):
89 yield text
[match
.start()]
90 first_letters
.pattern
= re_compile("((?<!\w)\w)|\d")
93 def is_upsidedown_wrong(name
):
94 """Tell if the string would get a different meaning if written upside down"""
96 mistakable
= set("69NZMWpbqd")
97 rotatable
= set("80oOxXIl").union(mistakable
)
98 return chars
.issubset(rotatable
) and not chars
.isdisjoint(mistakable
)
102 """Generate consecutive pairs throughout the given sequence; at last, it gives elements last, first."""
104 previous
= first
= next(i
)
111 def argmax_pair(array
, key
):
112 """Find an (unordered) pair of indices that maximize the given function"""
114 mi
, mj
, m
= None, None, None
116 for j
in range(i
+1, n
):
117 k
= key(array
[i
], array
[j
])
123 def fitting_matrix(v1
, v2
):
124 """Get a matrix that rotates v1 to the same direction as v2"""
125 return (1 / v1
.length_squared
) * M
.Matrix((
126 (v1
.x
*v2
.x
+ v1
.y
*v2
.y
, v1
.y
*v2
.x
- v1
.x
*v2
.y
),
127 (v1
.x
*v2
.y
- v1
.y
*v2
.x
, v1
.x
*v2
.x
+ v1
.y
*v2
.y
)))
131 """Get a rotation matrix that aligns given vector upwards."""
136 (n
.x
*n
.z
/(b
*s
), n
.y
*n
.z
/(b
*s
), -b
/s
),
141 # no need for rotation
144 (0, (-1 if n
.z
< 0 else 1), 0),
149 def create_blank_image(image_name
, dimensions
, alpha
=1):
150 """Create a new image and assign white color to all its pixels"""
151 image_name
= image_name
[:64]
152 width
, height
= int(dimensions
.x
), int(dimensions
.y
)
153 image
= bpy
.data
.images
.new(image_name
, width
, height
, alpha
=True)
156 "There is something wrong with the material of the model. "
157 "Please report this on the BlenderArtists forum. Export failed.")
158 image
.pixels
= [1, 1, 1, alpha
] * (width
* height
)
159 image
.file_format
= 'PNG'
163 def bake(face_indices
, uvmap
, image
):
165 is_cycles
= (bpy
.context
.scene
.render
.engine
== 'CYCLES')
167 # please excuse the following mess. Cycles baking API does not seem to allow better.
168 ob
= bpy
.context
.active_object
170 # add a disconnected image node that defines the bake target
172 for mat
in me
.materials
:
174 img
= mat
.node_tree
.nodes
.new('ShaderNodeTexImage')
176 temp_nodes
[mat
] = img
177 mat
.node_tree
.nodes
.active
= img
179 # move all excess faces to negative numbers (that is the only way to disable them)
180 loop
= me
.uv_layers
[me
.uv_layers
.active_index
].data
181 face_indices
= set(face_indices
)
184 for face
in me
.polygons
if face
.index
not in face_indices
185 for i
, v
in enumerate(face
.vertices
)]
186 for vid
in ignored_uvs
:
188 bake_type
= bpy
.context
.scene
.cycles
.bake_type
189 sta
= bpy
.context
.scene
.render
.bake
.use_selected_to_active
191 bpy
.ops
.object.bake(type=bake_type
, margin
=0, use_selected_to_active
=sta
, cage_extrusion
=100, use_clear
=False)
192 except RuntimeError as e
:
193 raise UnfoldError(*e
.args
)
195 for mat
, node
in temp_nodes
.items():
196 mat
.node_tree
.nodes
.remove(node
)
197 for vid
in ignored_uvs
:
200 texfaces
= uvmap
.data
201 for fid
in face_indices
:
202 texfaces
[fid
].image
= image
203 bpy
.ops
.object.bake_image()
204 for fid
in face_indices
:
205 texfaces
[fid
].image
= None
208 class UnfoldError(ValueError):
213 def __init__(self
, ob
):
215 self
.mesh
= Mesh(ob
.data
, ob
.matrix_world
)
216 self
.mesh
.check_correct()
219 def prepare(self
, cage_size
=None, create_uvmap
=False, mark_seams
=False, priority_effect
=default_priority_effect
, scale
=1):
220 """Create the islands of the net"""
221 self
.mesh
.generate_cuts(cage_size
/ scale
if cage_size
else None, priority_effect
)
222 is_landscape
= cage_size
and cage_size
.x
> cage_size
.y
223 self
.mesh
.finalize_islands(is_landscape
)
224 self
.mesh
.enumerate_islands()
226 self
.tex
= self
.mesh
.save_uv()
228 self
.mesh
.mark_cuts()
230 def copy_island_names(self
, island_list
):
231 """Copy island label and abbreviation from the best matching island in the list"""
232 orig_islands
= [{face
.id for face
in item
.faces
} for item
in island_list
]
234 for i
, island
in enumerate(self
.mesh
.islands
):
235 islfaces
= {uvface
.face
.index
for uvface
in island
.faces
}
236 matching
.extend((len(islfaces
.intersection(item
)), i
, j
) for j
, item
in enumerate(orig_islands
))
237 matching
.sort(reverse
=True)
238 available_new
= [True for island
in self
.mesh
.islands
]
239 available_orig
= [True for item
in island_list
]
240 for face_count
, i
, j
in matching
:
241 if available_new
[i
] and available_orig
[j
]:
242 available_new
[i
] = available_orig
[j
] = False
243 self
.mesh
.islands
[i
].label
= island_list
[j
].label
244 self
.mesh
.islands
[i
].abbreviation
= island_list
[j
].abbreviation
246 def save(self
, properties
):
247 """Export the document"""
248 # Note about scale: input is directly in blender length
249 # Mesh.scale_islands multiplies everything by a user-defined ratio
250 # exporters (SVG or PDF) multiply everything by 1000 (output in millimeters)
251 Exporter
= SVG
if properties
.file_format
== 'SVG' else PDF
252 filepath
= properties
.filepath
253 extension
= properties
.file_format
.lower()
254 filepath
= bpy
.path
.ensure_ext(filepath
, "." + extension
)
255 # page size in meters
256 page_size
= M
.Vector((properties
.output_size_x
, properties
.output_size_y
))
257 # printable area size in meters
258 printable_size
= page_size
- 2 * properties
.output_margin
* M
.Vector((1, 1))
259 unit_scale
= bpy
.context
.scene
.unit_settings
.scale_length
260 ppm
= properties
.output_dpi
* 100 / 2.54 # pixels per meter
262 # after this call, all dimensions will be in meters
263 self
.mesh
.scale_islands(unit_scale
/properties
.scale
)
264 if properties
.do_create_stickers
:
265 self
.mesh
.generate_stickers(properties
.sticker_width
, properties
.do_create_numbers
)
266 elif properties
.do_create_numbers
:
267 self
.mesh
.generate_numbers_alone(properties
.sticker_width
)
269 text_height
= properties
.sticker_width
if (properties
.do_create_numbers
and len(self
.mesh
.islands
) > 1) else 0
270 aspect_ratio
= printable_size
.x
/ printable_size
.y
271 # title height must be somewhat larger that text size, glyphs go below the baseline
272 self
.mesh
.finalize_islands(is_landscape
=(printable_size
.x
> printable_size
.y
), title_height
=text_height
* 1.2)
273 self
.mesh
.fit_islands(cage_size
=printable_size
)
275 if properties
.output_type
!= 'NONE':
276 # bake an image and save it as a PNG to disk or into memory
277 image_packing
= properties
.image_packing
if properties
.file_format
== 'SVG' else 'ISLAND_EMBED'
278 use_separate_images
= image_packing
in ('ISLAND_LINK', 'ISLAND_EMBED')
279 tex
= self
.mesh
.save_uv(cage_size
=printable_size
, separate_image
=use_separate_images
, tex
=self
.tex
)
281 raise UnfoldError("The mesh has no UV Map slots left. Either delete a UV Map or export the net without textures.")
283 sce
= bpy
.context
.scene
286 if rd
.engine
== 'CYCLES':
287 recall
= sce
.cycles
.bake_type
, bk
.use_selected_to_active
, bk
.margin
, bk
.cage_extrusion
, bk
.use_cage
, bk
.use_clear
, bk
.use_pass_direct
, bk
.use_pass_indirect
289 lookup
= {'TEXTURE': 'DIFFUSE', 'AMBIENT_OCCLUSION': 'AO', 'RENDER': 'COMBINED', 'SELECTED_TO_ACTIVE': 'COMBINED'}
290 sce
.cycles
.bake_type
= lookup
[properties
.output_type
]
291 bk
.use_pass_direct
= bk
.use_pass_indirect
= (properties
.output_type
!= 'TEXTURE')
292 bk
.use_selected_to_active
= (properties
.output_type
== 'SELECTED_TO_ACTIVE')
293 bk
.margin
, bk
.cage_extrusion
, bk
.use_cage
, bk
.use_clear
= 0, 10, False, False
295 recall
= rd
.engine
, rd
.bake_type
, rd
.use_bake_to_vertex_color
, rd
.use_bake_selected_to_active
, rd
.bake_distance
, rd
.bake_bias
, rd
.bake_margin
, rd
.use_bake_clear
296 rd
.engine
= 'BLENDER_RENDER'
297 lookup
= {'TEXTURE': 'TEXTURE', 'AMBIENT_OCCLUSION': 'AO', 'RENDER': 'FULL', 'SELECTED_TO_ACTIVE': 'FULL'}
298 rd
.bake_type
= lookup
[properties
.output_type
]
299 rd
.use_bake_selected_to_active
= (properties
.output_type
== 'SELECTED_TO_ACTIVE')
300 rd
.bake_margin
, rd
.bake_distance
, rd
.bake_bias
, rd
.use_bake_to_vertex_color
, rd
.use_bake_clear
= 0, 0, 0.001, False, False
302 if image_packing
== 'PAGE_LINK':
303 self
.mesh
.save_image(tex
, printable_size
* ppm
, filepath
)
304 elif image_packing
== 'ISLAND_LINK':
305 image_dir
= filepath
[:filepath
.rfind(".")]
306 self
.mesh
.save_separate_images(tex
, ppm
, image_dir
)
307 elif image_packing
== 'ISLAND_EMBED':
308 self
.mesh
.save_separate_images(tex
, ppm
, filepath
, embed
=Exporter
.encode_image
)
311 if rd
.engine
== 'CYCLES':
312 sce
.cycles
.bake_type
, bk
.use_selected_to_active
, bk
.margin
, bk
.cage_extrusion
, bk
.use_cage
, bk
.use_clear
, bk
.use_pass_direct
, bk
.use_pass_indirect
= recall
314 rd
.engine
, rd
.bake_type
, rd
.use_bake_to_vertex_color
, rd
.use_bake_selected_to_active
, rd
.bake_distance
, rd
.bake_bias
, rd
.bake_margin
, rd
.use_bake_clear
= recall
315 if not properties
.do_create_uvmap
:
317 bpy
.ops
.mesh
.uv_texture_remove()
319 exporter
= Exporter(page_size
, properties
.style
, properties
.output_margin
, (properties
.output_type
== 'NONE'), properties
.angle_epsilon
)
320 exporter
.do_create_stickers
= properties
.do_create_stickers
321 exporter
.text_size
= properties
.sticker_width
322 exporter
.write(self
.mesh
, filepath
)
326 """Wrapper for Bpy Mesh"""
328 def __init__(self
, mesh
, matrix
):
329 self
.vertices
= dict()
331 self
.edges_by_verts_indices
= dict()
333 self
.islands
= list()
336 for bpy_vertex
in mesh
.vertices
:
337 self
.vertices
[bpy_vertex
.index
] = Vertex(bpy_vertex
, matrix
)
338 for bpy_edge
in mesh
.edges
:
339 edge
= Edge(bpy_edge
, self
, matrix
)
340 self
.edges
[bpy_edge
.index
] = edge
341 self
.edges_by_verts_indices
[(edge
.va
.index
, edge
.vb
.index
)] = edge
342 self
.edges_by_verts_indices
[(edge
.vb
.index
, edge
.va
.index
)] = edge
343 for bpy_face
in mesh
.polygons
:
344 face
= Face(bpy_face
, self
)
345 self
.faces
[bpy_face
.index
] = face
346 for edge
in self
.edges
.values():
347 edge
.choose_main_faces()
349 edge
.calculate_angle()
351 def check_correct(self
, epsilon
=1e-6):
352 """Check for invalid geometry"""
353 null_edges
= {i
for i
, e
in self
.edges
.items() if e
.vector
.length
< epsilon
and e
.faces
}
354 null_faces
= {i
for i
, f
in self
.faces
.items() if f
.normal
.length_squared
< epsilon
}
355 twisted_faces
= {i
for i
, f
in self
.faces
.items() if f
.is_twisted()}
356 if not (null_edges
or null_faces
or twisted_faces
):
358 bpy
.context
.tool_settings
.mesh_select_mode
= False, bool(null_edges
), bool(null_faces
or twisted_faces
)
359 for vertex
in self
.data
.vertices
:
360 vertex
.select
= False
361 for edge
in self
.data
.edges
:
362 edge
.select
= (edge
.index
in null_edges
)
363 for face
in self
.data
.polygons
:
364 face
.select
= (face
.index
in null_faces
or face
.index
in twisted_faces
)
365 cure
= ("Remove Doubles and Triangulate" if (null_edges
or null_faces
) and twisted_faces
366 else "Triangulate" if twisted_faces
367 else "Remove Doubles")
369 "The model contains:\n" +
370 (" {} zero-length edge(s)\n".format(len(null_edges
)) if null_edges
else "") +
371 (" {} zero-area face(s)\n".format(len(null_faces
)) if null_faces
else "") +
372 (" {} twisted polygon(s)\n".format(len(twisted_faces
)) if twisted_faces
else "") +
373 "The offenders are selected and you can use {} to fix them. Export failed.".format(cure
))
375 def generate_cuts(self
, page_size
, priority_effect
):
376 """Cut the mesh so that it can be unfolded to a flat net."""
377 # warning: this constructor modifies its parameter (face)
378 islands
= {Island(face
) for face
in self
.faces
.values()}
379 # check for edges that are cut permanently
380 edges
= [edge
for edge
in self
.edges
.values() if not edge
.force_cut
and len(edge
.faces
) > 1]
383 average_length
= sum(edge
.vector
.length
for edge
in edges
) / len(edges
)
385 edge
.generate_priority(priority_effect
, average_length
)
386 edges
.sort(reverse
=False, key
=lambda edge
: edge
.priority
)
388 if edge
.vector
.length_squared
== 0:
390 face_a
, face_b
= edge
.main_faces
391 island_a
, island_b
= face_a
.uvface
.island
, face_b
.uvface
.island
392 if island_a
is not island_b
:
393 if len(island_b
.faces
) > len(island_a
.faces
):
394 island_a
, island_b
= island_b
, island_a
395 if island_a
.join(island_b
, edge
, size_limit
=page_size
):
396 islands
.remove(island_b
)
398 self
.islands
= sorted(islands
, reverse
=True, key
=lambda island
: len(island
.faces
))
400 for edge
in self
.edges
.values():
401 # some edges did not know until now whether their angle is convex or concave
402 if edge
.main_faces
and (edge
.main_faces
[0].uvface
.flipped
or edge
.main_faces
[1].uvface
.flipped
):
403 edge
.calculate_angle()
404 # ensure that the order of faces corresponds to the order of uvedges
406 reordered
= [None, None]
407 for uvedge
in edge
.uvedges
:
409 index
= edge
.main_faces
.index(uvedge
.uvface
.face
)
410 reordered
[index
] = uvedge
412 reordered
.append(uvedge
)
413 edge
.uvedges
= reordered
415 for island
in self
.islands
:
416 # if the normals are ambiguous, flip them so that there are more convex edges than concave ones
417 if any(uvface
.flipped
for uvface
in island
.faces
):
418 island_edges
= {uvedge
.edge
for uvedge
in island
.edges
if not uvedge
.edge
.is_cut(uvedge
.uvface
.face
)}
419 balance
= sum((+1 if edge
.angle
> 0 else -1) for edge
in island_edges
)
421 island
.is_inside_out
= True
423 # construct a linked list from each island's boundary
424 # uvedge.neighbor_right is clockwise = forward = via uvedge.vb if not uvface.flipped
425 neighbor_lookup
, conflicts
= dict(), dict()
426 for uvedge
in island
.boundary
:
427 uvvertex
= uvedge
.va
if uvedge
.uvface
.flipped
else uvedge
.vb
428 if uvvertex
not in neighbor_lookup
:
429 neighbor_lookup
[uvvertex
] = uvedge
431 if uvvertex
not in conflicts
:
432 conflicts
[uvvertex
] = [neighbor_lookup
[uvvertex
], uvedge
]
434 conflicts
[uvvertex
].append(uvedge
)
436 for uvedge
in island
.boundary
:
437 uvvertex
= uvedge
.vb
if uvedge
.uvface
.flipped
else uvedge
.va
438 if uvvertex
not in conflicts
:
439 # using the 'get' method so as to handle single-connected vertices properly
440 uvedge
.neighbor_right
= neighbor_lookup
.get(uvvertex
, uvedge
)
441 uvedge
.neighbor_right
.neighbor_left
= uvedge
443 conflicts
[uvvertex
].append(uvedge
)
445 # resolve merged vertices with more boundaries crossing
446 def direction_to_float(vector
):
447 return (1 - vector
.x
/vector
.length
) if vector
.y
> 0 else (vector
.x
/vector
.length
- 1)
448 for uvvertex
, uvedges
in conflicts
.items():
449 def is_inwards(uvedge
):
450 return uvedge
.uvface
.flipped
== (uvedge
.va
is uvvertex
)
452 def uvedge_sortkey(uvedge
):
453 if is_inwards(uvedge
):
454 return direction_to_float(uvedge
.va
.co
- uvedge
.vb
.co
)
456 return direction_to_float(uvedge
.vb
.co
- uvedge
.va
.co
)
458 uvedges
.sort(key
=uvedge_sortkey
)
460 zip(uvedges
[:-1:2], uvedges
[1::2]) if is_inwards(uvedges
[0])
461 else zip([uvedges
[-1]] + uvedges
[1::2], uvedges
[:-1:2])):
462 left
.neighbor_right
= right
463 right
.neighbor_left
= left
467 """Mark cut edges in the original mesh so that the user can see"""
468 for bpy_edge
in self
.data
.edges
:
469 edge
= self
.edges
[bpy_edge
.index
]
470 bpy_edge
.use_seam
= len(edge
.uvedges
) > 1 and edge
.is_main_cut
472 def generate_stickers(self
, default_width
, do_create_numbers
=True):
473 """Add sticker faces where they are needed."""
474 def uvedge_priority(uvedge
):
475 """Returns whether it is a good idea to stick something on this edge's face"""
476 # TODO: it should take into account overlaps with faces and with other stickers
477 return uvedge
.uvface
.face
.area
/ sum((vb
.co
- va
.co
).length
for (va
, vb
) in pairs(uvedge
.uvface
.vertices
))
479 def add_sticker(uvedge
, index
, target_island
):
480 uvedge
.sticker
= Sticker(uvedge
, default_width
, index
, target_island
)
481 uvedge
.island
.add_marker(uvedge
.sticker
)
483 for edge
in self
.edges
.values():
484 if edge
.is_main_cut
and len(edge
.uvedges
) >= 2 and edge
.vector
.length_squared
> 0:
485 uvedge_a
, uvedge_b
= edge
.uvedges
[:2]
486 if uvedge_priority(uvedge_a
) < uvedge_priority(uvedge_b
):
487 uvedge_a
, uvedge_b
= uvedge_b
, uvedge_a
488 target_island
= uvedge_a
.island
489 left_edge
, right_edge
= uvedge_a
.neighbor_left
.edge
, uvedge_a
.neighbor_right
.edge
490 if do_create_numbers
:
491 for uvedge
in [uvedge_b
] + edge
.uvedges
[2:]:
492 if ((uvedge
.neighbor_left
.edge
is not right_edge
or uvedge
.neighbor_right
.edge
is not left_edge
) and
493 uvedge
not in (uvedge_a
.neighbor_left
, uvedge_a
.neighbor_right
)):
494 # it will not be clear to see that these uvedges should be sticked together
495 # So, create an arrow and put the index on all stickers
496 target_island
.sticker_numbering
+= 1
497 index
= str(target_island
.sticker_numbering
)
498 if is_upsidedown_wrong(index
):
500 target_island
.add_marker(Arrow(uvedge_a
, default_width
, index
))
503 # if all uvedges to be sticked are easy to see, create no numbers
507 add_sticker(uvedge_b
, index
, target_island
)
508 elif len(edge
.uvedges
) > 2:
510 target_island
= edge
.uvedges
[0].island
511 if len(edge
.uvedges
) > 2:
512 for uvedge
in edge
.uvedges
[2:]:
513 add_sticker(uvedge
, index
, target_island
)
515 def generate_numbers_alone(self
, size
):
517 for edge
in self
.edges
.values():
518 if edge
.is_main_cut
and len(edge
.uvedges
) >= 2:
519 global_numbering
+= 1
520 index
= str(global_numbering
)
521 if is_upsidedown_wrong(index
):
523 for uvedge
in edge
.uvedges
:
524 uvedge
.island
.add_marker(NumberAlone(uvedge
, index
, size
))
526 def enumerate_islands(self
):
527 for num
, island
in enumerate(self
.islands
, 1):
529 island
.generate_label()
531 def scale_islands(self
, scale
):
532 for island
in self
.islands
:
533 for point
in chain((vertex
.co
for vertex
in island
.vertices
), island
.fake_vertices
):
536 def finalize_islands(self
, is_landscape
=False, title_height
=0):
537 for island
in self
.islands
:
539 island
.title
= "[{}] {}".format(island
.abbreviation
, island
.label
)
540 points
= list(vertex
.co
for vertex
in island
.vertices
) + island
.fake_vertices
541 angle
= M
.geometry
.box_fit_2d(points
)
542 rot
= M
.Matrix
.Rotation(angle
, 2)
543 # ensure that the island matches page orientation (portrait/landscape)
544 dimensions
= M
.Vector(max(r
* v
for v
in points
) - min(r
* v
for v
in points
) for r
in rot
)
545 if dimensions
.x
> dimensions
.y
!= is_landscape
:
546 rot
= M
.Matrix
.Rotation(angle
+ pi
/ 2, 2)
548 # note: we need an in-place operation, and Vector.rotate() seems to work for 3d vectors only
549 point
[:] = rot
* point
550 for marker
in island
.markers
:
551 marker
.rot
= rot
* marker
.rot
552 bottom_left
= M
.Vector((min(v
.x
for v
in points
), min(v
.y
for v
in points
) - title_height
))
555 island
.bounding_box
= M
.Vector((max(v
.x
for v
in points
), max(v
.y
for v
in points
)))
557 def largest_island_ratio(self
, page_size
):
558 return max(i
/ p
for island
in self
.islands
for (i
, p
) in zip(island
.bounding_box
, page_size
))
560 def fit_islands(self
, cage_size
):
561 """Move islands so that they fit onto pages, based on their bounding boxes"""
563 def try_emplace(island
, page_islands
, cage_size
, stops_x
, stops_y
, occupied_cache
):
564 """Tries to put island to each pair from stops_x, stops_y
565 and checks if it overlaps with any islands present on the page.
566 Returns True and positions the given island on success."""
567 bbox_x
, bbox_y
= island
.bounding_box
.xy
569 if x
+ bbox_x
> cage_size
.x
:
572 if y
+ bbox_y
> cage_size
.y
or (x
, y
) in occupied_cache
:
574 for i
, obstacle
in enumerate(page_islands
):
575 # if this obstacle overlaps with the island, try another stop
576 if (x
+ bbox_x
> obstacle
.pos
.x
and
577 obstacle
.pos
.x
+ obstacle
.bounding_box
.x
> x
and
578 y
+ bbox_y
> obstacle
.pos
.y
and
579 obstacle
.pos
.y
+ obstacle
.bounding_box
.y
> y
):
580 if x
>= obstacle
.pos
.x
and y
>= obstacle
.pos
.y
:
581 occupied_cache
.add((x
, y
))
582 # just a stupid heuristic to make subsequent searches faster
584 page_islands
[1:i
+1] = page_islands
[:i
]
585 page_islands
[0] = obstacle
588 # if no obstacle called break, this position is okay
590 page_islands
.append(island
)
591 stops_x
.append(x
+ bbox_x
)
592 stops_y
.append(y
+ bbox_y
)
596 def drop_portion(stops
, border
, divisor
):
598 # distance from left neighbor to the right one, excluding the first stop
599 distances
= [right
- left
for left
, right
in zip(stops
, chain(stops
[2:], [border
]))]
600 quantile
= sorted(distances
)[len(distances
) // divisor
]
601 return [stop
for stop
, distance
in zip(stops
, chain([quantile
], distances
)) if distance
>= quantile
]
603 if any(island
.bounding_box
.x
> cage_size
.x
or island
.bounding_box
.y
> cage_size
.y
for island
in self
.islands
):
605 "An island is too big to fit onto page of the given size. "
606 "Either downscale the model or find and split that island manually.\n"
607 "Export failed, sorry.")
608 # sort islands by their diagonal... just a guess
609 remaining_islands
= sorted(self
.islands
, reverse
=True, key
=lambda island
: island
.bounding_box
.length_squared
)
612 while remaining_islands
:
613 # create a new page and try to fit as many islands onto it as possible
614 page
= Page(page_num
)
616 occupied_cache
= set()
617 stops_x
, stops_y
= [0], [0]
618 for island
in remaining_islands
:
619 try_emplace(island
, page
.islands
, cage_size
, stops_x
, stops_y
, occupied_cache
)
620 # if overwhelmed with stops, drop a quarter of them
621 if len(stops_x
)**2 > 4 * len(self
.islands
) + 100:
622 stops_x
= drop_portion(stops_x
, cage_size
.x
, 4)
623 stops_y
= drop_portion(stops_y
, cage_size
.y
, 4)
624 remaining_islands
= [island
for island
in remaining_islands
if island
not in page
.islands
]
625 self
.pages
.append(page
)
627 def save_uv(self
, cage_size
=M
.Vector((1, 1)), separate_image
=False, tex
=None):
628 # TODO: mode switching should be handled by higher-level code
629 bpy
.ops
.object.mode_set()
630 # note: assuming that the active object's data is self.mesh
632 tex
= self
.data
.uv_textures
.new()
635 tex
.name
= "Unfolded"
637 # TODO: this is somewhat dirty, but I do not see a nicer way in the API
638 loop
= self
.data
.uv_layers
[self
.data
.uv_layers
.active_index
]
640 for island
in self
.islands
:
641 island
.save_uv_separate(loop
)
643 for island
in self
.islands
:
644 island
.save_uv(loop
, cage_size
)
647 def save_image(self
, tex
, page_size_pixels
: M
.Vector
, filename
):
648 for page
in self
.pages
:
649 image
= create_blank_image("{} {} Unfolded".format(self
.data
.name
[:14], page
.name
), page_size_pixels
, alpha
=1)
650 image
.filepath_raw
= page
.image_path
= "{}_{}.png".format(filename
, page
.name
)
651 faces
= [uvface
.face
.index
for island
in page
.islands
for uvface
in island
.faces
]
652 bake(faces
, tex
, image
)
655 bpy
.data
.images
.remove(image
)
657 def save_separate_images(self
, tex
, scale
, filepath
, embed
=None):
658 # omitting this may cause a "Circular reference in texture stack" error
659 recall
= {texface
: texface
.image
for texface
in tex
.data
}
660 for texface
in tex
.data
:
662 for i
, island
in enumerate(self
.islands
, 1):
663 image_name
= "{} isl{}".format(self
.data
.name
[:15], i
)
664 image
= create_blank_image(image_name
, island
.bounding_box
* scale
, alpha
=0)
665 bake([uvface
.face
.index
for uvface
in island
.faces
], tex
, image
)
667 island
.embedded_image
= embed(image
)
669 from os
import makedirs
671 makedirs(image_dir
, exist_ok
=True)
672 image_path
= os_path
.join(image_dir
, "island{}.png".format(i
))
673 image
.filepath_raw
= image_path
675 island
.image_path
= image_path
677 bpy
.data
.images
.remove(image
)
678 for texface
, img
in recall
.items():
683 """BPy Vertex wrapper"""
684 __slots__
= ('index', 'co', 'edges', 'uvs')
686 def __init__(self
, bpy_vertex
, matrix
):
687 self
.index
= bpy_vertex
.index
688 self
.co
= matrix
* bpy_vertex
.co
693 return hash(self
.index
)
695 def __eq__(self
, other
):
696 return self
.index
== other
.index
700 """Wrapper for BPy Edge"""
701 __slots__
= ('va', 'vb', 'faces', 'main_faces', 'uvedges',
703 'is_main_cut', 'force_cut', 'priority', 'freestyle')
705 def __init__(self
, edge
, mesh
, matrix
=1):
706 self
.va
= mesh
.vertices
[edge
.vertices
[0]]
707 self
.vb
= mesh
.vertices
[edge
.vertices
[1]]
708 self
.vector
= self
.vb
.co
- self
.va
.co
710 # if self.main_faces is set, then self.uvedges[:2] must correspond to self.main_faces, in their order
711 # this constraint is assured at the time of finishing mesh.generate_cuts
712 self
.uvedges
= list()
714 self
.force_cut
= edge
.use_seam
# such edges will always be cut
715 self
.main_faces
= None # two faces that may be connected in the island
716 # is_main_cut defines whether the two main faces are connected
717 # all the others will be assumed to be cut
718 self
.is_main_cut
= True
721 self
.freestyle
= getattr(edge
, "use_freestyle_mark", False) # freestyle edges will be highlighted
722 self
.va
.edges
.append(self
) # FIXME: editing foreign attribute
723 self
.vb
.edges
.append(self
) # FIXME: editing foreign attribute
725 def choose_main_faces(self
):
726 """Choose two main faces that might get connected in an island"""
727 if len(self
.faces
) == 2:
728 self
.main_faces
= self
.faces
729 elif len(self
.faces
) > 2:
730 # find (with brute force) the pair of indices whose faces have the most similar normals
731 i
, j
= argmax_pair(self
.faces
, key
=lambda a
, b
: abs(a
.normal
.dot(b
.normal
)))
732 self
.main_faces
= [self
.faces
[i
], self
.faces
[j
]]
734 def calculate_angle(self
):
735 """Calculate the angle between the main faces"""
736 face_a
, face_b
= self
.main_faces
737 if face_a
.normal
.length_squared
== 0 or face_b
.normal
.length_squared
== 0:
738 self
.angle
= -3 # just a very sharp angle
740 # correction if normals are flipped
741 a_is_clockwise
= ((face_a
.vertices
.index(self
.va
) - face_a
.vertices
.index(self
.vb
)) % len(face_a
.vertices
) == 1)
742 b_is_clockwise
= ((face_b
.vertices
.index(self
.va
) - face_b
.vertices
.index(self
.vb
)) % len(face_b
.vertices
) == 1)
744 if face_a
.uvface
and face_b
.uvface
:
745 a_is_clockwise ^
= face_a
.uvface
.flipped
746 b_is_clockwise ^
= face_b
.uvface
.flipped
747 is_equal_flip
= (face_a
.uvface
.flipped
== face_b
.uvface
.flipped
)
748 # TODO: maybe this need not be true in _really_ ugly cases: assert(a_is_clockwise != b_is_clockwise)
749 if a_is_clockwise
!= b_is_clockwise
:
750 if (a_is_clockwise
== (face_b
.normal
.cross(face_a
.normal
).dot(self
.vector
) > 0)) == is_equal_flip
:
751 # the angle is convex
752 self
.angle
= face_a
.normal
.angle(face_b
.normal
)
754 # the angle is concave
755 self
.angle
= -face_a
.normal
.angle(face_b
.normal
)
757 # normals are flipped, so we know nothing
758 # so let us assume the angle be convex
759 self
.angle
= face_a
.normal
.angle(-face_b
.normal
)
761 def generate_priority(self
, priority_effect
, average_length
):
762 """Calculate the priority value for cutting"""
765 self
.priority
= priority_effect
['CONVEX'] * angle
/ pi
767 self
.priority
= priority_effect
['CONCAVE'] * (-angle
) / pi
768 self
.priority
+= (self
.vector
.length
/ average_length
) * priority_effect
['LENGTH']
770 def is_cut(self
, face
):
771 """Return False if this edge will the given face to another one in the resulting net
772 (useful for edges with more than two faces connected)"""
773 # Return whether there is a cut between the two main faces
774 if self
.main_faces
and face
in self
.main_faces
:
775 return self
.is_main_cut
776 # All other faces (third and more) are automatically treated as cut
780 def other_uvedge(self
, this
):
781 """Get an uvedge of this edge that is not the given one
782 causes an IndexError if case of less than two adjacent edges"""
783 return self
.uvedges
[1] if this
is self
.uvedges
[0] else self
.uvedges
[0]
787 """Wrapper for BPy Face"""
788 __slots__
= ('index', 'edges', 'vertices', 'uvface',
789 'loop_start', 'area', 'normal')
791 def __init__(self
, bpy_face
, mesh
):
792 self
.index
= bpy_face
.index
794 self
.vertices
= [mesh
.vertices
[i
] for i
in bpy_face
.vertices
]
795 self
.loop_start
= bpy_face
.loop_start
796 self
.area
= bpy_face
.area
798 self
.normal
= M
.geometry
.normal(v
.co
for v
in self
.vertices
)
799 for verts_indices
in bpy_face
.edge_keys
:
800 edge
= mesh
.edges_by_verts_indices
[verts_indices
]
801 self
.edges
.append(edge
)
802 edge
.faces
.append(self
) # FIXME: editing foreign attribute
804 def is_twisted(self
):
805 if len(self
.vertices
) > 3:
806 center
= sum((vertex
.co
for vertex
in self
.vertices
), M
.Vector((0, 0, 0))) / len(self
.vertices
)
807 plane_d
= center
.dot(self
.normal
)
808 diameter
= max((center
- vertex
.co
).length
for vertex
in self
.vertices
)
809 for vertex
in self
.vertices
:
811 if abs(vertex
.co
.dot(self
.normal
) - plane_d
) > diameter
* 0.01:
816 return hash(self
.index
)
820 """Part of the net to be exported"""
821 __slots__
= ('faces', 'edges', 'vertices', 'fake_vertices', 'uvverts_by_id', 'boundary', 'markers',
822 'pos', 'bounding_box',
823 'image_path', 'embedded_image',
824 'number', 'label', 'abbreviation', 'title',
825 'has_safe_geometry', 'is_inside_out',
828 def __init__(self
, face
):
829 """Create an Island from a single Face"""
832 self
.vertices
= set()
833 self
.fake_vertices
= list()
834 self
.markers
= list()
836 self
.abbreviation
= None
838 self
.pos
= M
.Vector((0, 0))
839 self
.image_path
= None
840 self
.embedded_image
= None
841 self
.is_inside_out
= False # swaps concave <-> convex edges
842 self
.has_safe_geometry
= True
843 self
.sticker_numbering
= 0
844 uvface
= UVFace(face
, self
)
845 self
.vertices
.update(uvface
.vertices
)
846 self
.edges
.update(uvface
.edges
)
847 self
.faces
.append(uvface
)
848 # speedup for Island.join
849 self
.uvverts_by_id
= {uvvertex
.vertex
.index
: [uvvertex
] for uvvertex
in self
.vertices
}
850 # UVEdges on the boundary
851 self
.boundary
= list(self
.edges
)
853 def join(self
, other
, edge
: Edge
, size_limit
=None, epsilon
=1e-6) -> bool:
855 Try to join other island on given edge
856 Returns False if they would overlap
859 class Intersection(Exception):
862 class GeometryError(Exception):
865 def is_below(self
, other
, correct_geometry
=True):
868 if self
.top
< other
.bottom
:
870 if other
.top
< self
.bottom
:
872 if self
.max.tup
<= other
.min.tup
:
874 if other
.max.tup
<= self
.min.tup
:
876 self_vector
= self
.max.co
- self
.min.co
877 min_to_min
= other
.min.co
- self
.min.co
878 cross_b1
= self_vector
.cross(min_to_min
)
879 cross_b2
= self_vector
.cross(other
.max.co
- self
.min.co
)
880 if cross_b2
< cross_b1
:
881 cross_b1
, cross_b2
= cross_b2
, cross_b1
882 if cross_b2
> 0 and (cross_b1
> 0 or (cross_b1
== 0 and not self
.is_uvface_upwards())):
884 if cross_b1
< 0 and (cross_b2
< 0 or (cross_b2
== 0 and self
.is_uvface_upwards())):
886 other_vector
= other
.max.co
- other
.min.co
887 cross_a1
= other_vector
.cross(-min_to_min
)
888 cross_a2
= other_vector
.cross(self
.max.co
- other
.min.co
)
889 if cross_a2
< cross_a1
:
890 cross_a1
, cross_a2
= cross_a2
, cross_a1
891 if cross_a2
> 0 and (cross_a1
> 0 or (cross_a1
== 0 and not other
.is_uvface_upwards())):
893 if cross_a1
< 0 and (cross_a2
< 0 or (cross_a2
== 0 and other
.is_uvface_upwards())):
895 if cross_a1
== cross_b1
== cross_a2
== cross_b2
== 0:
898 elif self
.is_uvface_upwards() == other
.is_uvface_upwards():
901 if self
.min.tup
== other
.min.tup
or self
.max.tup
== other
.max.tup
:
902 return cross_a2
> cross_b2
905 class QuickSweepline
:
906 """Efficient sweepline based on binary search, checking neighbors only"""
908 self
.children
= blist()
910 def add(self
, item
, cmp=is_below
):
911 low
, high
= 0, len(self
.children
)
913 mid
= (low
+ high
) // 2
914 if cmp(self
.children
[mid
], item
):
918 self
.children
.insert(low
, item
)
920 def remove(self
, item
, cmp=is_below
):
921 index
= self
.children
.index(item
)
922 self
.children
.pop(index
)
923 if index
> 0 and index
< len(self
.children
):
924 # check for intersection
925 if cmp(self
.children
[index
], self
.children
[index
-1]):
928 class BruteSweepline
:
929 """Safe sweepline which checks all its members pairwise"""
931 self
.children
= set()
932 self
.last_min
= None, []
933 self
.last_max
= None, []
935 def add(self
, item
, cmp=is_below
):
936 for child
in self
.children
:
937 if child
.min is not item
.min and child
.max is not item
.max:
938 cmp(item
, child
, False)
939 self
.children
.add(item
)
941 def remove(self
, item
):
942 self
.children
.remove(item
)
944 def sweep(sweepline
, segments
):
945 """Sweep across the segments and raise an exception if necessary"""
946 # careful, 'segments' may be a use-once iterator
947 events_add
= sorted(segments
, reverse
=True, key
=lambda uvedge
: uvedge
.min.tup
)
948 events_remove
= sorted(events_add
, reverse
=True, key
=lambda uvedge
: uvedge
.max.tup
)
950 while events_add
and events_add
[-1].min.tup
<= events_remove
[-1].max.tup
:
951 sweepline
.add(events_add
.pop())
952 sweepline
.remove(events_remove
.pop())
954 def root_find(value
, tree
):
955 """Find the root of a given value in a forest-like dictionary
956 also updates the dictionary using path compression"""
957 parent
, relink
= tree
.get(value
), list()
958 while parent
is not None:
960 value
, parent
= parent
, tree
.get(parent
)
961 tree
.update(dict.fromkeys(relink
, value
))
964 def slope_from(position
):
966 vec
= (uvedge
.vb
.co
- uvedge
.va
.co
) if uvedge
.va
.tup
== position
else (uvedge
.va
.co
- uvedge
.vb
.co
)
967 return (vec
.y
/ vec
.length
+ 1) if ((vec
.x
, vec
.y
) > (0, 0)) else (-1 - vec
.y
/ vec
.length
)
970 # find edge in other and in self
971 for uvedge
in edge
.uvedges
:
972 if uvedge
.uvface
.face
in uvedge
.edge
.main_faces
:
973 if uvedge
.uvface
.island
is self
and uvedge
in self
.boundary
:
975 elif uvedge
.uvface
.island
is other
and uvedge
in other
.boundary
:
980 # check if vertices and normals are aligned correctly
981 verts_flipped
= uvedge_b
.va
.vertex
is uvedge_a
.va
.vertex
982 flipped
= verts_flipped ^ uvedge_a
.uvface
.flipped ^ uvedge_b
.uvface
.flipped
984 # NOTE: if the edges differ in length, the matrix will involve uniform scaling.
985 # Such situation may occur in the case of twisted n-gons
986 first_b
, second_b
= (uvedge_b
.va
, uvedge_b
.vb
) if not verts_flipped
else (uvedge_b
.vb
, uvedge_b
.va
)
988 rot
= fitting_matrix(first_b
.co
- second_b
.co
, uvedge_a
.vb
.co
- uvedge_a
.va
.co
)
990 flip
= M
.Matrix(((-1, 0), (0, 1)))
991 rot
= fitting_matrix(flip
* (first_b
.co
- second_b
.co
), uvedge_a
.vb
.co
- uvedge_a
.va
.co
) * flip
992 trans
= uvedge_a
.vb
.co
- rot
* first_b
.co
993 # extract and transform island_b's boundary
994 phantoms
= {uvvertex
: UVVertex(rot
*uvvertex
.co
+ trans
, uvvertex
.vertex
) for uvvertex
in other
.vertices
}
996 # check the size of the resulting island
998 # first check: bounding box
999 left
= min(min(seg
.min.co
.x
for seg
in self
.boundary
), min(vertex
.co
.x
for vertex
in phantoms
))
1000 right
= max(max(seg
.max.co
.x
for seg
in self
.boundary
), max(vertex
.co
.x
for vertex
in phantoms
))
1001 bottom
= min(min(seg
.bottom
for seg
in self
.boundary
), min(vertex
.co
.y
for vertex
in phantoms
))
1002 top
= max(max(seg
.top
for seg
in self
.boundary
), max(vertex
.co
.y
for vertex
in phantoms
))
1003 bbox_width
= right
- left
1004 bbox_height
= top
- bottom
1005 if min(bbox_width
, bbox_height
)**2 > size_limit
.x
**2 + size_limit
.y
**2:
1007 if (bbox_width
> size_limit
.x
or bbox_height
> size_limit
.y
) and (bbox_height
> size_limit
.x
or bbox_width
> size_limit
.y
):
1008 # further checks (TODO!)
1009 # for the time being, just throw this piece away
1012 distance_limit
= edge
.vector
.length_squared
* epsilon
1013 # try and merge UVVertices closer than sqrt(distance_limit)
1014 merged_uvedges
= set()
1015 merged_uvedge_pairs
= list()
1017 # merge all uvvertices that are close enough using a union-find structure
1018 # uvvertices will be merged only in cases other->self and self->self
1019 # all resulting groups are merged together to a uvvertex of self
1020 is_merged_mine
= False
1021 shared_vertices
= self
.uvverts_by_id
.keys() & other
.uvverts_by_id
.keys()
1022 for vertex_id
in shared_vertices
:
1023 uvs
= self
.uvverts_by_id
[vertex_id
] + other
.uvverts_by_id
[vertex_id
]
1024 len_mine
= len(self
.uvverts_by_id
[vertex_id
])
1026 for i
, a
in enumerate(uvs
[:len_mine
]):
1027 i
= root_find(i
, merged
)
1028 for j
, b
in enumerate(uvs
[i
+1:], i
+1):
1029 b
= b
if j
< len_mine
else phantoms
[b
]
1030 j
= root_find(j
, merged
)
1033 i
, j
= (j
, i
) if j
< i
else (i
, j
)
1034 if (a
.co
- b
.co
).length_squared
< distance_limit
:
1036 for source
, target
in merged
.items():
1037 target
= root_find(target
, merged
)
1038 phantoms
[uvs
[source
]] = uvs
[target
]
1039 is_merged_mine |
= (source
< len_mine
) # remember that a vertex of this island has been merged
1041 for uvedge
in (chain(self
.boundary
, other
.boundary
) if is_merged_mine
else other
.boundary
):
1042 for partner
in uvedge
.edge
.uvedges
:
1043 if partner
is not uvedge
:
1044 paired_a
, paired_b
= phantoms
.get(partner
.vb
, partner
.vb
), phantoms
.get(partner
.va
, partner
.va
)
1045 if (partner
.uvface
.flipped ^ flipped
) != uvedge
.uvface
.flipped
:
1046 paired_a
, paired_b
= paired_b
, paired_a
1047 if phantoms
.get(uvedge
.va
, uvedge
.va
) is paired_a
and phantoms
.get(uvedge
.vb
, uvedge
.vb
) is paired_b
:
1048 # if these two edges will get merged, add them both to the set
1049 merged_uvedges
.update((uvedge
, partner
))
1050 merged_uvedge_pairs
.append((uvedge
, partner
))
1053 if uvedge_b
not in merged_uvedges
:
1054 raise UnfoldError("Export failed. Please report this error, including the model if you can.")
1057 PhantomUVEdge(phantoms
[uvedge
.va
], phantoms
[uvedge
.vb
], flipped ^ uvedge
.uvface
.flipped
)
1058 for uvedge
in other
.boundary
if uvedge
not in merged_uvedges
]
1059 # TODO: if is_merged_mine, it might make sense to create a similar list from self.boundary as well
1061 incidence
= {vertex
.tup
for vertex
in phantoms
.values()}.intersection(vertex
.tup
for vertex
in self
.vertices
)
1062 incidence
= {position
: list() for position
in incidence
} # from now on, 'incidence' is a dict
1063 for uvedge
in chain(boundary_other
, self
.boundary
):
1064 if uvedge
.va
.co
== uvedge
.vb
.co
:
1066 for vertex
in (uvedge
.va
, uvedge
.vb
):
1067 site
= incidence
.get(vertex
.tup
)
1068 if site
is not None:
1070 for position
, segments
in incidence
.items():
1071 if len(segments
) <= 2:
1073 segments
.sort(key
=slope_from(position
))
1074 for right
, left
in pairs(segments
):
1075 is_left_ccw
= left
.is_uvface_upwards() ^
(left
.max.tup
== position
)
1076 is_right_ccw
= right
.is_uvface_upwards() ^
(right
.max.tup
== position
)
1077 if is_right_ccw
and not is_left_ccw
and type(right
) is not type(left
) and right
not in merged_uvedges
and left
not in merged_uvedges
:
1079 if (not is_right_ccw
and right
not in merged_uvedges
) ^
(is_left_ccw
and left
not in merged_uvedges
):
1082 # check for self-intersections
1085 sweepline
= QuickSweepline() if self
.has_safe_geometry
and other
.has_safe_geometry
else BruteSweepline()
1086 sweep(sweepline
, (uvedge
for uvedge
in chain(boundary_other
, self
.boundary
)))
1087 self
.has_safe_geometry
&= other
.has_safe_geometry
1088 except GeometryError
:
1089 sweep(BruteSweepline(), (uvedge
for uvedge
in chain(boundary_other
, self
.boundary
)))
1090 self
.has_safe_geometry
= False
1091 except Intersection
:
1094 # mark all edges that connect the islands as not cut
1095 for uvedge
in merged_uvedges
:
1096 uvedge
.edge
.is_main_cut
= False
1098 # include all trasformed vertices as mine
1099 self
.vertices
.update(phantoms
.values())
1101 # update the uvverts_by_id dictionary
1102 for source
, target
in phantoms
.items():
1103 present
= self
.uvverts_by_id
.get(target
.vertex
.index
)
1105 self
.uvverts_by_id
[target
.vertex
.index
] = [target
]
1107 # emulation of set behavior... sorry, it is faster
1108 if source
in present
:
1109 present
.remove(source
)
1110 if target
not in present
:
1111 present
.append(target
)
1113 # re-link uvedges and uvfaces to their transformed locations
1114 for uvedge
in other
.edges
:
1115 uvedge
.island
= self
1116 uvedge
.va
= phantoms
[uvedge
.va
]
1117 uvedge
.vb
= phantoms
[uvedge
.vb
]
1120 for uvedge
in self
.edges
:
1121 uvedge
.va
= phantoms
.get(uvedge
.va
, uvedge
.va
)
1122 uvedge
.vb
= phantoms
.get(uvedge
.vb
, uvedge
.vb
)
1123 self
.edges
.update(other
.edges
)
1125 for uvface
in other
.faces
:
1126 uvface
.island
= self
1127 uvface
.vertices
= [phantoms
[uvvertex
] for uvvertex
in uvface
.vertices
]
1128 uvface
.uvvertex_by_id
= {
1129 index
: phantoms
[uvvertex
]
1130 for index
, uvvertex
in uvface
.uvvertex_by_id
.items()}
1131 uvface
.flipped ^
= flipped
1133 # there may be own uvvertices that need to be replaced by phantoms
1134 for uvface
in self
.faces
:
1135 if any(uvvertex
in phantoms
for uvvertex
in uvface
.vertices
):
1136 uvface
.vertices
= [phantoms
.get(uvvertex
, uvvertex
) for uvvertex
in uvface
.vertices
]
1137 uvface
.uvvertex_by_id
= {
1138 index
: phantoms
.get(uvvertex
, uvvertex
)
1139 for index
, uvvertex
in uvface
.uvvertex_by_id
.items()}
1140 self
.faces
.extend(other
.faces
)
1143 uvedge
for uvedge
in chain(self
.boundary
, other
.boundary
)
1144 if uvedge
not in merged_uvedges
]
1146 for uvedge
, partner
in merged_uvedge_pairs
:
1147 # make sure that main faces are the ones actually merged (this changes nothing in most cases)
1148 uvedge
.edge
.main_faces
[:] = uvedge
.uvface
.face
, partner
.uvface
.face
1150 # everything seems to be OK
1153 def add_marker(self
, marker
):
1154 self
.fake_vertices
.extend(marker
.bounds
)
1155 self
.markers
.append(marker
)
1157 def generate_label(self
, label
=None, abbreviation
=None):
1158 """Assign a name to this island automatically"""
1159 abbr
= abbreviation
or self
.abbreviation
or str(self
.number
)
1160 # TODO: dots should be added in the last instant when outputting any text
1161 if is_upsidedown_wrong(abbr
):
1163 self
.label
= label
or self
.label
or "Island {}".format(self
.number
)
1164 self
.abbreviation
= abbr
1166 def save_uv(self
, tex
, cage_size
):
1167 """Save UV Coordinates of all UVFaces to a given UV texture
1168 tex: UV Texture layer to use (BPy MeshUVLoopLayer struct)
1169 page_size: size of the page in pixels (vector)"""
1171 for uvface
in self
.faces
:
1172 for i
, uvvertex
in enumerate(uvface
.vertices
):
1173 uv
= uvvertex
.co
+ self
.pos
1174 texface
[uvface
.face
.loop_start
+ i
].uv
[0] = uv
.x
/ cage_size
.x
1175 texface
[uvface
.face
.loop_start
+ i
].uv
[1] = uv
.y
/ cage_size
.y
1177 def save_uv_separate(self
, tex
):
1178 """Save UV Coordinates of all UVFaces to a given UV texture, spanning from 0 to 1
1179 tex: UV Texture layer to use (BPy MeshUVLoopLayer struct)
1180 page_size: size of the page in pixels (vector)"""
1182 scale_x
, scale_y
= 1 / self
.bounding_box
.x
, 1 / self
.bounding_box
.y
1183 for uvface
in self
.faces
:
1184 for i
, uvvertex
in enumerate(uvface
.vertices
):
1185 texface
[uvface
.face
.loop_start
+ i
].uv
[0] = uvvertex
.co
.x
* scale_x
1186 texface
[uvface
.face
.loop_start
+ i
].uv
[1] = uvvertex
.co
.y
* scale_y
1190 """Container for several Islands"""
1191 __slots__
= ('islands', 'name', 'image_path')
1193 def __init__(self
, num
=1):
1194 self
.islands
= list()
1195 self
.name
= "page{}".format(num
)
1196 self
.image_path
= None
1201 __slots__
= ('co', 'vertex', 'tup')
1203 def __init__(self
, vector
, vertex
=None):
1205 self
.vertex
= vertex
1206 self
.tup
= tuple(self
.co
)
1210 return "UV {} [{:.3f}, {:.3f}]".format(self
.vertex
.index
, self
.co
.x
, self
.co
.y
)
1212 return "UV * [{:.3f}, {:.3f}]".format(self
.co
.x
, self
.co
.y
)
1217 # Every UVEdge is attached to only one UVFace
1218 # UVEdges are doubled as needed because they both have to point clockwise around their faces
1219 __slots__
= ('va', 'vb', 'island', 'uvface', 'edge',
1220 'min', 'max', 'bottom', 'top',
1221 'neighbor_left', 'neighbor_right', 'sticker')
1223 def __init__(self
, vertex1
: UVVertex
, vertex2
: UVVertex
, island
: Island
, uvface
, edge
):
1227 self
.island
= island
1228 self
.uvface
= uvface
1233 """Update data if UVVertices have moved"""
1234 self
.min, self
.max = (self
.va
, self
.vb
) if (self
.va
.tup
< self
.vb
.tup
) else (self
.vb
, self
.va
)
1235 y1
, y2
= self
.va
.co
.y
, self
.vb
.co
.y
1236 self
.bottom
, self
.top
= (y1
, y2
) if y1
< y2
else (y2
, y1
)
1238 def is_uvface_upwards(self
):
1239 return (self
.va
.tup
< self
.vb
.tup
) ^ self
.uvface
.flipped
1242 return "({0.va} - {0.vb})".format(self
)
1245 class PhantomUVEdge
:
1246 """Temporary 2D Segment for calculations"""
1247 __slots__
= ('va', 'vb', 'min', 'max', 'bottom', 'top')
1249 def __init__(self
, vertex1
: UVVertex
, vertex2
: UVVertex
, flip
):
1250 self
.va
, self
.vb
= (vertex2
, vertex1
) if flip
else (vertex1
, vertex2
)
1251 self
.min, self
.max = (self
.va
, self
.vb
) if (self
.va
.tup
< self
.vb
.tup
) else (self
.vb
, self
.va
)
1252 y1
, y2
= self
.va
.co
.y
, self
.vb
.co
.y
1253 self
.bottom
, self
.top
= (y1
, y2
) if y1
< y2
else (y2
, y1
)
1255 def is_uvface_upwards(self
):
1256 return self
.va
.tup
< self
.vb
.tup
1259 return "[{0.va} - {0.vb}]".format(self
)
1264 __slots__
= ('vertices', 'edges', 'face', 'island', 'flipped', 'uvvertex_by_id')
1266 def __init__(self
, face
: Face
, island
: Island
):
1267 """Creace an UVFace from a Face and a fixed edge.
1268 face: Face to take coordinates from
1269 island: Island to register itself in
1270 fixed_edge: Edge to connect to (that already has UV coordinates)"""
1271 self
.vertices
= list()
1274 self
.island
= island
1275 self
.flipped
= False # a flipped UVFace has edges clockwise
1277 rot
= z_up_matrix(face
.normal
)
1278 self
.uvvertex_by_id
= dict() # link vertex id -> UVVertex
1279 for vertex
in face
.vertices
:
1280 uvvertex
= UVVertex(rot
* vertex
.co
, vertex
)
1281 self
.vertices
.append(uvvertex
)
1282 self
.uvvertex_by_id
[vertex
.index
] = uvvertex
1284 edge_by_verts
= dict()
1285 for edge
in face
.edges
:
1286 edge_by_verts
[(edge
.va
.index
, edge
.vb
.index
)] = edge
1287 edge_by_verts
[(edge
.vb
.index
, edge
.va
.index
)] = edge
1288 for va
, vb
in pairs(self
.vertices
):
1289 edge
= edge_by_verts
[(va
.vertex
.index
, vb
.vertex
.index
)]
1290 uvedge
= UVEdge(va
, vb
, island
, self
, edge
)
1291 self
.edges
.append(uvedge
)
1292 edge
.uvedges
.append(uvedge
) # FIXME: editing foreign attribute
1296 """Mark in the document: an arrow denoting the number of the edge it points to"""
1297 __slots__
= ('bounds', 'center', 'rot', 'text', 'size')
1299 def __init__(self
, uvedge
, size
, index
):
1300 self
.text
= str(index
)
1301 edge
= (uvedge
.vb
.co
- uvedge
.va
.co
) if not uvedge
.uvface
.flipped
else (uvedge
.va
.co
- uvedge
.vb
.co
)
1302 self
.center
= (uvedge
.va
.co
+ uvedge
.vb
.co
) / 2
1304 tangent
= edge
.normalized()
1306 self
.rot
= M
.Matrix(((cos
, -sin
), (sin
, cos
)))
1307 normal
= M
.Vector((sin
, -cos
))
1308 self
.bounds
= [self
.center
, self
.center
+ (1.2*normal
+ tangent
)*size
, self
.center
+ (1.2*normal
- tangent
)*size
]
1312 """Mark in the document: sticker tab"""
1313 __slots__
= ('bounds', 'center', 'rot', 'text', 'width', 'vertices')
1315 def __init__(self
, uvedge
, default_width
=0.005, index
=None, target_island
=None):
1316 """Sticker is directly attached to the given UVEdge"""
1317 first_vertex
, second_vertex
= (uvedge
.va
, uvedge
.vb
) if not uvedge
.uvface
.flipped
else (uvedge
.vb
, uvedge
.va
)
1318 edge
= first_vertex
.co
- second_vertex
.co
1319 sticker_width
= min(default_width
, edge
.length
/ 2)
1320 other
= uvedge
.edge
.other_uvedge(uvedge
) # This is the other uvedge - the sticking target
1322 other_first
, other_second
= (other
.va
, other
.vb
) if not other
.uvface
.flipped
else (other
.vb
, other
.va
)
1323 other_edge
= other_second
.co
- other_first
.co
1325 # angle a is at vertex uvedge.va, b is at uvedge.vb
1327 sin_a
= sin_b
= 0.75**0.5
1328 # len_a is length of the side adjacent to vertex a, len_b likewise
1329 len_a
= len_b
= sticker_width
/ sin_a
1331 # fix overlaps with the most often neighbour - its sticking target
1332 if first_vertex
== other_second
:
1333 cos_a
= max(cos_a
, (edge
*other_edge
) / (edge
.length
**2)) # angles between pi/3 and 0
1334 elif second_vertex
== other_first
:
1335 cos_b
= max(cos_b
, (edge
*other_edge
) / (edge
.length
**2)) # angles between pi/3 and 0
1337 # Fix tabs for sticking targets with small angles
1338 # Index of other uvedge in its face (not in its island)
1339 other_idx
= other
.uvface
.edges
.index(other
)
1340 # Left and right neighbors in the face
1341 other_face_neighbor_left
= other
.uvface
.edges
[(other_idx
+1) % len(other
.uvface
.edges
)]
1342 other_face_neighbor_right
= other
.uvface
.edges
[(other_idx
-1) % len(other
.uvface
.edges
)]
1343 other_edge_neighbor_a
= other_face_neighbor_left
.vb
.co
- other
.vb
.co
1344 other_edge_neighbor_b
= other_face_neighbor_right
.va
.co
- other
.va
.co
1345 # Adjacent angles in the face
1346 cos_a
= max(cos_a
, (-other_edge
*other_edge_neighbor_a
) / (other_edge
.length
*other_edge_neighbor_a
.length
))
1347 cos_b
= max(cos_b
, (other_edge
*other_edge_neighbor_b
) / (other_edge
.length
*other_edge_neighbor_b
.length
))
1349 # Calculate the lengths of the glue tab edges using the possibly smaller angles
1350 sin_a
= abs(1 - cos_a
**2)**0.5
1351 len_b
= min(len_a
, (edge
.length
* sin_a
) / (sin_a
* cos_b
+ sin_b
* cos_a
))
1352 len_a
= 0 if sin_a
== 0 else min(sticker_width
/ sin_a
, (edge
.length
- len_b
*cos_b
) / cos_a
)
1354 sin_b
= abs(1 - cos_b
**2)**0.5
1355 len_a
= min(len_a
, (edge
.length
* sin_b
) / (sin_a
* cos_b
+ sin_b
* cos_a
))
1356 len_b
= 0 if sin_b
== 0 else min(sticker_width
/ sin_b
, (edge
.length
- len_a
* cos_a
) / cos_b
)
1358 v3
= UVVertex(second_vertex
.co
+ M
.Matrix(((cos_b
, -sin_b
), (sin_b
, cos_b
))) * edge
* len_b
/ edge
.length
)
1359 v4
= UVVertex(first_vertex
.co
+ M
.Matrix(((-cos_a
, -sin_a
), (sin_a
, -cos_a
))) * edge
* len_a
/ edge
.length
)
1361 self
.vertices
= [second_vertex
, v3
, v4
, first_vertex
]
1363 self
.vertices
= [second_vertex
, v3
, first_vertex
]
1365 sin
, cos
= edge
.y
/ edge
.length
, edge
.x
/ edge
.length
1366 self
.rot
= M
.Matrix(((cos
, -sin
), (sin
, cos
)))
1367 self
.width
= sticker_width
* 0.9
1368 if index
and target_island
is not uvedge
.island
:
1369 self
.text
= "{}:{}".format(target_island
.abbreviation
, index
)
1372 self
.center
= (uvedge
.va
.co
+ uvedge
.vb
.co
) / 2 + self
.rot
*M
.Vector((0, self
.width
*0.2))
1373 self
.bounds
= [v3
.co
, v4
.co
, self
.center
] if v3
.co
!= v4
.co
else [v3
.co
, self
.center
]
1377 """Mark in the document: numbering inside the island denoting edges to be sticked"""
1378 __slots__
= ('bounds', 'center', 'rot', 'text', 'size')
1380 def __init__(self
, uvedge
, index
, default_size
=0.005):
1381 """Sticker is directly attached to the given UVEdge"""
1382 edge
= (uvedge
.va
.co
- uvedge
.vb
.co
) if not uvedge
.uvface
.flipped
else (uvedge
.vb
.co
- uvedge
.va
.co
)
1384 self
.size
= default_size
1385 sin
, cos
= edge
.y
/ edge
.length
, edge
.x
/ edge
.length
1386 self
.rot
= M
.Matrix(((cos
, -sin
), (sin
, cos
)))
1388 self
.center
= (uvedge
.va
.co
+ uvedge
.vb
.co
) / 2 - self
.rot
*M
.Vector((0, self
.size
*1.2))
1389 self
.bounds
= [self
.center
]
1393 """Simple SVG exporter"""
1395 def __init__(self
, page_size
: M
.Vector
, style
, margin
, pure_net
=True, angle_epsilon
=0.01):
1396 """Initialize document settings.
1397 page_size: document dimensions in meters
1398 pure_net: if True, do not use image"""
1399 self
.page_size
= page_size
1400 self
.pure_net
= pure_net
1402 self
.margin
= margin
1404 self
.angle_epsilon
= angle_epsilon
1407 def encode_image(cls
, bpy_image
):
1410 with tempfile
.TemporaryDirectory() as directory
:
1411 filename
= directory
+ "/i.png"
1412 bpy_image
.filepath_raw
= filename
1414 return base64
.encodebytes(open(filename
, "rb").read()).decode('ascii')
1416 def format_vertex(self
, vector
, pos
=M
.Vector((0, 0))):
1417 """Return a string with both coordinates of the given vertex."""
1419 return "{:.6f} {:.6f}".format((x
+ self
.margin
) * 1000, (self
.page_size
.y
- y
- self
.margin
) * 1000)
1421 def write(self
, mesh
, filename
):
1422 """Write data to a file given by its name."""
1423 line_through
= " L ".join
# used for formatting of SVG path data
1426 dl
= ["{:.2f}".format(length
* self
.style
.line_width
* 1000) for length
in (2, 5, 10)]
1428 'SOLID': "none", 'DOT': "{0},{1}".format(*dl
), 'DASH': "{1},{2}".format(*dl
),
1429 'LONGDASH': "{2},{1}".format(*dl
), 'DASHDOT': "{2},{1},{0},{1}".format(*dl
)}
1431 def format_color(vec
):
1432 return "#{:02x}{:02x}{:02x}".format(round(vec
[0] * 255), round(vec
[1] * 255), round(vec
[2] * 255))
1434 def format_matrix(matrix
):
1435 return " ".join("{:.6f}".format(cell
) for column
in matrix
for cell
in column
)
1437 def path_convert(string
, relto
=os_path
.dirname(filename
)):
1438 assert(os_path
) # check the module was imported
1439 string
= os_path
.relpath(string
, relto
)
1440 if os_path
.sep
!= '/':
1441 string
= string
.replace(os_path
.sep
, '/')
1445 name
: format_color(getattr(self
.style
, name
)) for name
in (
1446 "outer_color", "outbg_color", "convex_color", "concave_color", "freestyle_color",
1447 "inbg_color", "sticker_fill", "text_color")}
1449 name
: format_style
[getattr(self
.style
, name
)] for name
in
1450 ("outer_style", "convex_style", "concave_style", "freestyle_style")})
1452 name
: getattr(self
.style
, attr
)[3] for name
, attr
in (
1453 ("outer_alpha", "outer_color"), ("outbg_alpha", "outbg_color"),
1454 ("convex_alpha", "convex_color"), ("concave_alpha", "concave_color"),
1455 ("freestyle_alpha", "freestyle_color"),
1456 ("inbg_alpha", "inbg_color"), ("sticker_alpha", "sticker_fill"),
1457 ("text_alpha", "text_color"))})
1459 name
: getattr(self
.style
, name
) * self
.style
.line_width
* 1000 for name
in
1460 ("outer_width", "convex_width", "concave_width", "freestyle_width", "outbg_width", "inbg_width")})
1461 for num
, page
in enumerate(mesh
.pages
):
1462 page_filename
= "{}_{}.svg".format(filename
[:filename
.rfind(".svg")], page
.name
) if len(mesh
.pages
) > 1 else filename
1463 with
open(page_filename
, 'w') as f
:
1464 print(self
.svg_base
.format(width
=self
.page_size
.x
*1000, height
=self
.page_size
.y
*1000), file=f
)
1465 print(self
.css_base
.format(**styleargs
), file=f
)
1468 self
.image_linked_tag
.format(
1469 pos
="{0:.6f} {0:.6f}".format(self
.margin
*1000),
1470 width
=(self
.page_size
.x
- 2 * self
.margin
)*1000,
1471 height
=(self
.page_size
.y
- 2 * self
.margin
)*1000,
1472 path
=path_convert(page
.image_path
)),
1474 if len(page
.islands
) > 1:
1475 print("<g>", file=f
)
1477 for island
in page
.islands
:
1478 print("<g>", file=f
)
1479 if island
.image_path
:
1481 self
.image_linked_tag
.format(
1482 pos
=self
.format_vertex(island
.pos
+ M
.Vector((0, island
.bounding_box
.y
))),
1483 width
=island
.bounding_box
.x
*1000,
1484 height
=island
.bounding_box
.y
*1000,
1485 path
=path_convert(island
.image_path
)),
1487 elif island
.embedded_image
:
1489 self
.image_embedded_tag
.format(
1490 pos
=self
.format_vertex(island
.pos
+ M
.Vector((0, island
.bounding_box
.y
))),
1491 width
=island
.bounding_box
.x
*1000,
1492 height
=island
.bounding_box
.y
*1000,
1493 path
=island
.image_path
),
1494 island
.embedded_image
, "'/>",
1498 self
.text_tag
.format(
1499 size
=1000 * self
.text_size
,
1500 x
=1000 * (island
.bounding_box
.x
*0.5 + island
.pos
.x
+ self
.margin
),
1501 y
=1000 * (self
.page_size
.y
- island
.pos
.y
- self
.margin
- 0.2 * self
.text_size
),
1502 label
=island
.title
),
1505 data_markers
, data_stickerfill
, data_outer
, data_convex
, data_concave
, data_freestyle
= (list() for i
in range(6))
1506 for marker
in island
.markers
:
1507 if isinstance(marker
, Sticker
):
1508 data_stickerfill
.append("M {} Z".format(
1509 line_through(self
.format_vertex(vertex
.co
, island
.pos
) for vertex
in marker
.vertices
)))
1511 data_markers
.append(self
.text_transformed_tag
.format(
1513 pos
=self
.format_vertex(marker
.center
, island
.pos
),
1514 mat
=format_matrix(marker
.rot
),
1515 size
=marker
.width
* 1000))
1516 elif isinstance(marker
, Arrow
):
1517 size
= marker
.size
* 1000
1518 position
= marker
.center
+ marker
.rot
*marker
.size
*M
.Vector((0, -0.9))
1519 data_markers
.append(self
.arrow_marker_tag
.format(
1521 arrow_pos
=self
.format_vertex(marker
.center
, island
.pos
),
1523 pos
=self
.format_vertex(position
, island
.pos
- marker
.size
*M
.Vector((0, 0.4))),
1524 mat
=format_matrix(size
* marker
.rot
)))
1525 elif isinstance(marker
, NumberAlone
):
1526 data_markers
.append(self
.text_transformed_tag
.format(
1528 pos
=self
.format_vertex(marker
.center
, island
.pos
),
1529 mat
=format_matrix(marker
.rot
),
1530 size
=marker
.size
* 1000))
1531 if data_stickerfill
and self
.style
.sticker_fill
[3] > 0:
1532 print("<path class='sticker' d='", rows(data_stickerfill
), "'/>", file=f
)
1534 outer_edges
= set(island
.boundary
)
1537 uvedge
= outer_edges
.pop()
1540 data_loop
.extend(self
.format_vertex(vertex
.co
, island
.pos
) for vertex
in uvedge
.sticker
.vertices
[1:])
1542 vertex
= uvedge
.vb
if uvedge
.uvface
.flipped
else uvedge
.va
1543 data_loop
.append(self
.format_vertex(vertex
.co
, island
.pos
))
1544 uvedge
= uvedge
.neighbor_right
1546 outer_edges
.remove(uvedge
)
1549 data_outer
.append("M {} Z".format(line_through(data_loop
)))
1551 for uvedge
in island
.edges
:
1553 if edge
.is_cut(uvedge
.uvface
.face
) and not uvedge
.sticker
:
1555 data_uvedge
= "M {}".format(
1556 line_through(self
.format_vertex(vertex
.co
, island
.pos
) for vertex
in (uvedge
.va
, uvedge
.vb
)))
1558 data_freestyle
.append(data_uvedge
)
1559 # each uvedge is in two opposite-oriented variants; we want to add each only once
1560 if uvedge
.sticker
or uvedge
.uvface
.flipped
!= (uvedge
.va
.vertex
.index
> uvedge
.vb
.vertex
.index
):
1561 if edge
.angle
> self
.angle_epsilon
:
1562 data_convex
.append(data_uvedge
)
1563 elif edge
.angle
< -self
.angle_epsilon
:
1564 data_concave
.append(data_uvedge
)
1565 if island
.is_inside_out
:
1566 data_convex
, data_concave
= data_concave
, data_convex
1569 print("<path class='freestyle' d='", rows(data_freestyle
), "'/>", file=f
)
1570 if (data_convex
or data_concave
) and not self
.pure_net
and self
.style
.use_inbg
:
1571 print("<path class='inner_background' d='", rows(data_convex
+ data_concave
), "'/>", file=f
)
1573 print("<path class='convex' d='", rows(data_convex
), "'/>", file=f
)
1575 print("<path class='concave' d='", rows(data_concave
), "'/>", file=f
)
1577 if not self
.pure_net
and self
.style
.use_outbg
:
1578 print("<path class='outer_background' d='", rows(data_outer
), "'/>", file=f
)
1579 print("<path class='outer' d='", rows(data_outer
), "'/>", file=f
)
1581 print(rows(data_markers
), file=f
)
1582 print("</g>", file=f
)
1584 if len(page
.islands
) > 1:
1585 print("</g>", file=f
)
1586 print("</svg>", file=f
)
1588 image_linked_tag
= "<image transform='translate({pos})' width='{width:.6f}' height='{height:.6f}' xlink:href='{path}'/>"
1589 image_embedded_tag
= "<image transform='translate({pos})' width='{width:.6f}' height='{height:.6f}' xlink:href='data:image/png;base64,"
1590 text_tag
= "<text transform='translate({x} {y})' style='font-size:{size:.2f}'><tspan>{label}</tspan></text>"
1591 text_transformed_tag
= "<text transform='matrix({mat} {pos})' style='font-size:{size:.2f}'><tspan>{label}</tspan></text>"
1592 arrow_marker_tag
= "<g><path transform='matrix({mat} {arrow_pos})' class='arrow' d='M 0 0 L 1 1 L 0 0.25 L -1 1 Z'/>" \
1593 "<text transform='translate({pos})' style='font-size:{scale:.2f}'><tspan>{index}</tspan></text></g>"
1595 svg_base
= """<?xml version='1.0' encoding='UTF-8' standalone='no'?>
1596 <svg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' version='1.1'
1597 width='{width:.2f}mm' height='{height:.2f}mm' viewBox='0 0 {width:.2f} {height:.2f}'>"""
1599 css_base
= """<style type="text/css">
1602 stroke-linecap: butt;
1603 stroke-linejoin: bevel;
1604 stroke-dasharray: none;
1607 stroke: {outer_color};
1608 stroke-dasharray: {outer_style};
1609 stroke-dashoffset: 0;
1610 stroke-width: {outer_width:.2};
1611 stroke-opacity: {outer_alpha:.2};
1614 stroke: {convex_color};
1615 stroke-dasharray: {convex_style};
1616 stroke-dashoffset:0;
1617 stroke-width:{convex_width:.2};
1618 stroke-opacity: {convex_alpha:.2}
1621 stroke: {concave_color};
1622 stroke-dasharray: {concave_style};
1623 stroke-dashoffset: 0;
1624 stroke-width: {concave_width:.2};
1625 stroke-opacity: {concave_alpha:.2}
1628 stroke: {freestyle_color};
1629 stroke-dasharray: {freestyle_style};
1630 stroke-dashoffset: 0;
1631 stroke-width: {freestyle_width:.2};
1632 stroke-opacity: {freestyle_alpha:.2}
1634 path.outer_background {{
1635 stroke: {outbg_color};
1636 stroke-opacity: {outbg_alpha};
1637 stroke-width: {outbg_width:.2}
1639 path.inner_background {{
1640 stroke: {inbg_color};
1641 stroke-opacity: {inbg_alpha};
1642 stroke-width: {inbg_width:.2}
1645 fill: {sticker_fill};
1647 fill-opacity: {sticker_alpha:.2};
1655 fill-opacity: {text_alpha:.2};
1665 """Simple PDF exporter"""
1667 mm_to_pt
= 72 / 25.4
1668 character_width_packed
= {
1669 191: "'", 222: 'ijl\x82\x91\x92', 278: '|¦\x00\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x0c\r\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f !,./:;I[\\]ft\xa0·ÌÍÎÏìíîï',
1670 333: '()-`r\x84\x88\x8b\x93\x94\x98\x9b¡¨\xad¯²³´¸¹{}', 350: '\x7f\x81\x8d\x8f\x90\x95\x9d', 365: '"ºª*°', 469: '^', 500: 'Jcksvxyz\x9a\x9eçýÿ', 584: '¶+<=>~¬±×÷', 611: 'FTZ\x8e¿ßø',
1671 667: '&ABEKPSVXY\x8a\x9fÀÁÂÃÄÅÈÉÊËÝÞ', 722: 'CDHNRUwÇÐÑÙÚÛÜ', 737: '©®', 778: 'GOQÒÓÔÕÖØ', 833: 'Mm¼½¾', 889: '%æ', 944: 'W\x9c', 1000: '\x85\x89\x8c\x97\x99Æ', 1015: '@', }
1672 character_width
= {c
: value
for (value
, chars
) in character_width_packed
.items() for c
in chars
}
1674 def __init__(self
, page_size
: M
.Vector
, style
, margin
, pure_net
=True, angle_epsilon
=0.01):
1675 self
.page_size
= page_size
1677 self
.margin
= M
.Vector((margin
, margin
))
1678 self
.pure_net
= pure_net
1679 self
.angle_epsilon
= angle_epsilon
1681 def text_width(self
, text
, scale
=None):
1682 return (scale
or self
.text_size
) * sum(self
.character_width
.get(c
, 556) for c
in text
) / 1000
1685 def encode_image(cls
, bpy_image
):
1686 data
= bytes(int(255 * px
) for (i
, px
) in enumerate(bpy_image
.pixels
) if i
% 4 != 3)
1688 "Type": "XObject", "Subtype": "Image", "Width": bpy_image
.size
[0], "Height": bpy_image
.size
[1],
1689 "ColorSpace": "DeviceRGB", "BitsPerComponent": 8, "Interpolate": True,
1690 "Filter": ["ASCII85Decode", "FlateDecode"], "stream": data
}
1693 def write(self
, mesh
, filename
):
1694 def format_dict(obj
, refs
=tuple()):
1695 return "<< " + "".join("/{} {}\n".format(key
, format_value(value
, refs
)) for (key
, value
) in obj
.items()) + ">>"
1697 def line_through(seq
):
1698 return "".join("{0.x:.6f} {0.y:.6f} {1} ".format(1000*v
.co
, c
) for (v
, c
) in zip(seq
, chain("m", repeat("l"))))
1700 def format_value(value
, refs
=tuple()):
1702 return "{} 0 R".format(refs
.index(value
) + 1)
1703 elif type(value
) is dict:
1704 return format_dict(value
, refs
)
1705 elif type(value
) in (list, tuple):
1706 return "[ " + " ".join(format_value(item
, refs
) for item
in value
) + " ]"
1707 elif type(value
) is int:
1709 elif type(value
) is float:
1710 return "{:.6f}".format(value
)
1711 elif type(value
) is bool:
1712 return "true" if value
else "false"
1714 return "/{}".format(value
) # this script can output only PDF names, no strings
1716 def write_object(index
, obj
, refs
, f
, stream
=None):
1717 byte_count
= f
.write("{} 0 obj\n".format(index
))
1718 if type(obj
) is not dict:
1719 stream
, obj
= obj
, dict()
1720 elif "stream" in obj
:
1721 stream
= obj
.pop("stream")
1723 if True or type(stream
) is bytes
:
1724 obj
["Filter"] = ["ASCII85Decode", "FlateDecode"]
1725 stream
= encode(stream
)
1726 obj
["Length"] = len(stream
)
1727 byte_count
+= f
.write(format_dict(obj
, refs
))
1729 byte_count
+= f
.write("\nstream\n")
1730 byte_count
+= f
.write(stream
)
1731 byte_count
+= f
.write("\nendstream")
1732 return byte_count
+ f
.write("\nendobj\n")
1735 from base64
import a85encode
1736 from zlib
import compress
1737 if hasattr(data
, "encode"):
1738 data
= data
.encode()
1739 return a85encode(compress(data
), adobe
=True, wrapcol
=250)[2:].decode()
1741 page_size_pt
= 1000 * self
.mm_to_pt
* self
.page_size
1742 root
= {"Type": "Pages", "MediaBox": [0, 0, page_size_pt
.x
, page_size_pt
.y
], "Kids": list()}
1743 catalog
= {"Type": "Catalog", "Pages": root
}
1745 "Type": "Font", "Subtype": "Type1", "Name": "F1",
1746 "BaseFont": "Helvetica", "Encoding": "MacRomanEncoding"}
1748 dl
= [length
* self
.style
.line_width
* 1000 for length
in (1, 4, 9)]
1750 'SOLID': list(), 'DOT': [dl
[0], dl
[1]], 'DASH': [dl
[1], dl
[2]],
1751 'LONGDASH': [dl
[2], dl
[1]], 'DASHDOT': [dl
[2], dl
[1], dl
[0], dl
[1]]}
1753 "Gtext": {"ca": self
.style
.text_color
[3], "Font": [font
, 1000 * self
.text_size
]},
1754 "Gsticker": {"ca": self
.style
.sticker_fill
[3]}}
1755 for name
in ("outer", "convex", "concave", "freestyle"):
1757 "LW": self
.style
.line_width
* 1000 * getattr(self
.style
, name
+ "_width"),
1758 "CA": getattr(self
.style
, name
+ "_color")[3],
1759 "D": [format_style
[getattr(self
.style
, name
+ "_style")], 0]}
1760 styles
["G" + name
] = gs
1761 for name
in ("outbg", "inbg"):
1763 "LW": self
.style
.line_width
* 1000 * getattr(self
.style
, name
+ "_width"),
1764 "CA": getattr(self
.style
, name
+ "_color")[3],
1765 "D": [format_style
['SOLID'], 0]}
1766 styles
["G" + name
] = gs
1768 objects
= [root
, catalog
, font
]
1769 objects
.extend(styles
.values())
1771 for page
in mesh
.pages
:
1772 commands
= ["{0:.6f} 0 0 {0:.6f} 0 0 cm".format(self
.mm_to_pt
)]
1773 resources
= {"Font": {"F1": font
}, "ExtGState": styles
, "XObject": dict()}
1774 for island
in page
.islands
:
1775 commands
.append("q 1 0 0 1 {0.x:.6f} {0.y:.6f} cm".format(1000*(self
.margin
+ island
.pos
)))
1776 if island
.embedded_image
:
1777 identifier
= "Im{}".format(len(resources
["XObject"]) + 1)
1778 commands
.append(self
.command_image
.format(1000 * island
.bounding_box
, identifier
))
1779 objects
.append(island
.embedded_image
)
1780 resources
["XObject"][identifier
] = island
.embedded_image
1783 commands
.append(self
.command_label
.format(
1784 size
=1000*self
.text_size
,
1785 x
=500 * (island
.bounding_box
.x
- self
.text_width(island
.title
)),
1786 y
=1000 * 0.2 * self
.text_size
,
1787 label
=island
.title
))
1789 data_markers
, data_stickerfill
, data_outer
, data_convex
, data_concave
, data_freestyle
= (list() for i
in range(6))
1790 for marker
in island
.markers
:
1791 if isinstance(marker
, Sticker
):
1792 data_stickerfill
.append(line_through(marker
.vertices
) + "f")
1794 data_markers
.append(self
.command_sticker
.format(
1796 pos
=1000*marker
.center
,
1798 align
=-500 * self
.text_width(marker
.text
, marker
.width
),
1799 size
=1000*marker
.width
))
1800 elif isinstance(marker
, Arrow
):
1801 size
= 1000 * marker
.size
1802 position
= 1000 * (marker
.center
+ marker
.rot
*marker
.size
*M
.Vector((0, -0.9)))
1803 data_markers
.append(self
.command_arrow
.format(
1805 arrow_pos
=1000 * marker
.center
,
1806 pos
=position
- 1000 * M
.Vector((0.5 * self
.text_width(marker
.text
), 0.4 * self
.text_size
)),
1807 mat
=size
* marker
.rot
,
1809 elif isinstance(marker
, NumberAlone
):
1810 data_markers
.append(self
.command_number
.format(
1812 pos
=1000*marker
.center
,
1814 size
=1000*marker
.size
))
1816 outer_edges
= set(island
.boundary
)
1819 uvedge
= outer_edges
.pop()
1822 data_loop
.extend(uvedge
.sticker
.vertices
[1:])
1824 vertex
= uvedge
.vb
if uvedge
.uvface
.flipped
else uvedge
.va
1825 data_loop
.append(vertex
)
1826 uvedge
= uvedge
.neighbor_right
1828 outer_edges
.remove(uvedge
)
1831 data_outer
.append(line_through(data_loop
) + "s")
1833 for uvedge
in island
.edges
:
1835 if edge
.is_cut(uvedge
.uvface
.face
) and not uvedge
.sticker
:
1837 data_uvedge
= line_through((uvedge
.va
, uvedge
.vb
)) + "S"
1839 data_freestyle
.append(data_uvedge
)
1840 # each uvedge is in two opposite-oriented variants; we want to add each only once
1841 if uvedge
.sticker
or uvedge
.uvface
.flipped
!= (uvedge
.va
.vertex
.index
> uvedge
.vb
.vertex
.index
):
1842 if edge
.angle
> self
.angle_epsilon
:
1843 data_convex
.append(data_uvedge
)
1844 elif edge
.angle
< -self
.angle_epsilon
:
1845 data_concave
.append(data_uvedge
)
1846 if island
.is_inside_out
:
1847 data_convex
, data_concave
= data_concave
, data_convex
1849 if data_stickerfill
and self
.style
.sticker_fill
[3] > 0:
1850 commands
.append("/Gsticker gs {0[0]:.3f} {0[1]:.3f} {0[2]:.3f} rg".format(self
.style
.sticker_fill
))
1851 commands
.extend(data_stickerfill
)
1853 commands
.append("/Gfreestyle gs {0[0]:.3f} {0[1]:.3f} {0[2]:.3f} RG".format(self
.style
.freestyle_color
))
1854 commands
.extend(data_freestyle
)
1855 if (data_convex
or data_concave
) and not self
.pure_net
and self
.style
.use_inbg
:
1856 commands
.append("/Ginbg gs {0[0]:.3f} {0[1]:.3f} {0[2]:.3f} RG".format(self
.style
.inbg_color
))
1857 commands
.extend(chain(data_convex
, data_concave
))
1859 commands
.append("/Gconvex gs {0[0]:.3f} {0[1]:.3f} {0[2]:.3f} RG".format(self
.style
.convex_color
))
1860 commands
.extend(data_convex
)
1862 commands
.append("/Gconcave gs {0[0]:.3f} {0[1]:.3f} {0[2]:.3f} RG".format(self
.style
.concave_color
))
1863 commands
.extend(data_concave
)
1865 if not self
.pure_net
and self
.style
.use_outbg
:
1866 commands
.append("/Goutbg gs {0[0]:.3f} {0[1]:.3f} {0[2]:.3f} RG".format(self
.style
.outbg_color
))
1867 commands
.extend(data_outer
)
1868 commands
.append("/Gouter gs {0[0]:.3f} {0[1]:.3f} {0[2]:.3f} RG".format(self
.style
.outer_color
))
1869 commands
.extend(data_outer
)
1870 commands
.append("/Gtext gs {0[0]:.3f} {0[1]:.3f} {0[2]:.3f} rg".format(self
.style
.text_color
))
1871 commands
.extend(data_markers
)
1872 commands
.append("Q")
1873 content
= "\n".join(commands
)
1874 page
= {"Type": "Page", "Parent": root
, "Contents": content
, "Resources": resources
}
1875 root
["Kids"].append(page
)
1876 objects
.extend((page
, content
))
1878 root
["Count"] = len(root
["Kids"])
1879 with
open(filename
, "w+") as f
:
1881 position
= f
.write("%PDF-1.4\n")
1882 for index
, obj
in enumerate(objects
, 1):
1883 xref_table
.append(position
)
1884 position
+= write_object(index
, obj
, objects
, f
)
1886 f
.write("xref_table\n0 {}\n".format(len(xref_table
) + 1))
1887 f
.write("{:010} {:05} f\n".format(0, 65536))
1888 for position
in xref_table
:
1889 f
.write("{:010} {:05} n\n".format(position
, 0))
1890 f
.write("trailer\n")
1891 f
.write(format_dict({"Size": len(xref_table
), "Root": catalog
}, objects
))
1892 f
.write("\nstartxref\n{}\n%%EOF\n".format(xref_pos
))
1894 command_label
= "/Gtext gs BT {x:.6f} {y:.6f} Td ({label}) Tj ET"
1895 command_image
= "q {0.x:.6f} 0 0 {0.y:.6f} 0 0 cm 1 0 0 -1 0 1 cm /{1} Do Q"
1896 command_sticker
= "q {mat[0][0]:.6f} {mat[1][0]:.6f} {mat[0][1]:.6f} {mat[1][1]:.6f} {pos.x:.6f} {pos.y:.6f} cm BT {align:.6f} 0 Td /F1 {size:.6f} Tf ({label}) Tj ET Q"
1897 command_arrow
= "q BT {pos.x:.6f} {pos.y:.6f} Td /F1 {size:.6f} Tf ({index}) Tj ET {mat[0][0]:.6f} {mat[1][0]:.6f} {mat[0][1]:.6f} {mat[1][1]:.6f} {arrow_pos.x:.6f} {arrow_pos.y:.6f} cm 0 0 m 1 -1 l 0 -0.25 l -1 -1 l f Q"
1898 command_number
= "q {mat[0][0]:.6f} {mat[1][0]:.6f} {mat[0][1]:.6f} {mat[1][1]:.6f} {pos.x:.6f} {pos.y:.6f} cm BT /F1 {size:.6f} Tf ({label}) Tj ET Q"
1901 class Unfold(bpy
.types
.Operator
):
1902 """Blender Operator: unfold the selected object."""
1904 bl_idname
= "mesh.unfold"
1906 bl_description
= "Mark seams so that the mesh can be exported as a paper model"
1907 bl_options
= {'REGISTER', 'UNDO'}
1908 edit
= bpy
.props
.BoolProperty(default
=False, options
={'HIDDEN'})
1909 priority_effect_convex
= bpy
.props
.FloatProperty(
1910 name
="Priority Convex", description
="Priority effect for edges in convex angles",
1911 default
=default_priority_effect
['CONVEX'], soft_min
=-1, soft_max
=10, subtype
='FACTOR')
1912 priority_effect_concave
= bpy
.props
.FloatProperty(
1913 name
="Priority Concave", description
="Priority effect for edges in concave angles",
1914 default
=default_priority_effect
['CONCAVE'], soft_min
=-1, soft_max
=10, subtype
='FACTOR')
1915 priority_effect_length
= bpy
.props
.FloatProperty(
1916 name
="Priority Length", description
="Priority effect of edge length",
1917 default
=default_priority_effect
['LENGTH'], soft_min
=-10, soft_max
=1, subtype
='FACTOR')
1918 do_create_uvmap
= bpy
.props
.BoolProperty(
1919 name
="Create UVMap", description
="Create a new UV Map showing the islands and page layout", default
=False)
1923 def poll(cls
, context
):
1924 return context
.active_object
and context
.active_object
.type == "MESH"
1926 def draw(self
, context
):
1927 layout
= self
.layout
1928 col
= layout
.column()
1929 col
.active
= not self
.object or len(self
.object.data
.uv_textures
) < 8
1930 col
.prop(self
.properties
, "do_create_uvmap")
1931 layout
.label(text
="Edge Cutting Factors:")
1932 col
= layout
.column(align
=True)
1933 col
.label(text
="Face Angle:")
1934 col
.prop(self
.properties
, "priority_effect_convex", text
="Convex")
1935 col
.prop(self
.properties
, "priority_effect_concave", text
="Concave")
1936 layout
.prop(self
.properties
, "priority_effect_length", text
="Edge Length")
1938 def execute(self
, context
):
1939 sce
= bpy
.context
.scene
1940 settings
= sce
.paper_model
1941 recall_mode
= context
.object.mode
1942 bpy
.ops
.object.mode_set(mode
='OBJECT')
1943 recall_display_islands
, sce
.paper_model
.display_islands
= sce
.paper_model
.display_islands
, False
1945 self
.object = context
.active_object
1946 mesh
= self
.object.data
1948 cage_size
= M
.Vector((settings
.output_size_x
, settings
.output_size_y
)) if settings
.limit_by_page
else None
1950 'CONVEX': self
.priority_effect_convex
,
1951 'CONCAVE': self
.priority_effect_concave
,
1952 'LENGTH': self
.priority_effect_length
}
1954 unfolder
= Unfolder(self
.object)
1956 cage_size
, self
.do_create_uvmap
, mark_seams
=True,
1957 priority_effect
=priority_effect
, scale
=sce
.unit_settings
.scale_length
/settings
.scale
)
1958 except UnfoldError
as error
:
1959 self
.report(type={'ERROR_INVALID_INPUT'}, message
=error
.args
[0])
1960 bpy
.ops
.object.mode_set(mode
=recall_mode
)
1961 sce
.paper_model
.display_islands
= recall_display_islands
1962 return {'CANCELLED'}
1963 if mesh
.paper_island_list
:
1964 unfolder
.copy_island_names(mesh
.paper_island_list
)
1966 island_list
= mesh
.paper_island_list
1967 attributes
= {item
.label
: (item
.abbreviation
, item
.auto_label
, item
.auto_abbrev
) for item
in island_list
}
1968 island_list
.clear() # remove previously defined islands
1969 for island
in unfolder
.mesh
.islands
:
1970 # add islands to UI list and set default descriptions
1971 list_item
= island_list
.add()
1972 # add faces' IDs to the island
1973 for uvface
in island
.faces
:
1974 lface
= list_item
.faces
.add()
1975 lface
.id = uvface
.face
.index
1977 list_item
["label"] = island
.label
1978 list_item
["abbreviation"], list_item
["auto_label"], list_item
["auto_abbrev"] = attributes
.get(
1980 (island
.abbreviation
, True, True))
1981 island_item_changed(list_item
, context
)
1983 mesh
.paper_island_index
= -1
1984 mesh
.show_edge_seams
= True
1986 bpy
.ops
.object.mode_set(mode
=recall_mode
)
1987 sce
.paper_model
.display_islands
= recall_display_islands
1991 class ClearAllSeams(bpy
.types
.Operator
):
1992 """Blender Operator: clear all seams of the active Mesh and all its unfold data"""
1994 bl_idname
= "mesh.clear_all_seams"
1995 bl_label
= "Clear All Seams"
1996 bl_description
= "Clear all the seams and unfolded islands of the active object"
1999 def poll(cls
, context
):
2000 return context
.active_object
and context
.active_object
.type == 'MESH'
2002 def execute(self
, context
):
2003 ob
= context
.active_object
2006 for edge
in mesh
.edges
:
2007 edge
.use_seam
= False
2008 mesh
.paper_island_list
.clear()
2013 def page_size_preset_changed(self
, context
):
2014 """Update the actual document size to correct values"""
2015 if hasattr(self
, "limit_by_page") and not self
.limit_by_page
:
2017 if self
.page_size_preset
== 'A4':
2018 self
.output_size_x
= 0.210
2019 self
.output_size_y
= 0.297
2020 elif self
.page_size_preset
== 'A3':
2021 self
.output_size_x
= 0.297
2022 self
.output_size_y
= 0.420
2023 elif self
.page_size_preset
== 'US_LETTER':
2024 self
.output_size_x
= 0.216
2025 self
.output_size_y
= 0.279
2026 elif self
.page_size_preset
== 'US_LEGAL':
2027 self
.output_size_x
= 0.216
2028 self
.output_size_y
= 0.356
2031 class PaperModelStyle(bpy
.types
.PropertyGroup
):
2033 ('SOLID', "Solid (----)", "Solid line"),
2034 ('DOT', "Dots (. . .)", "Dotted line"),
2035 ('DASH', "Short Dashes (- - -)", "Solid line"),
2036 ('LONGDASH', "Long Dashes (-- --)", "Solid line"),
2037 ('DASHDOT', "Dash-dotted (-- .)", "Solid line")
2039 outer_color
= bpy
.props
.FloatVectorProperty(
2040 name
="Outer Lines", description
="Color of net outline",
2041 default
=(0.0, 0.0, 0.0, 1.0), min=0, max=1, subtype
='COLOR', size
=4)
2042 outer_style
= bpy
.props
.EnumProperty(
2043 name
="Outer Lines Drawing Style", description
="Drawing style of net outline",
2044 default
='SOLID', items
=line_styles
)
2045 line_width
= bpy
.props
.FloatProperty(
2046 name
="Base Lines Thickness", description
="Base thickness of net lines, each actual value is a multiple of this length",
2047 default
=1e-4, min=0, soft_max
=5e-3, precision
=5, step
=1e-2, subtype
="UNSIGNED", unit
="LENGTH")
2048 outer_width
= bpy
.props
.FloatProperty(
2049 name
="Outer Lines Thickness", description
="Relative thickness of net outline",
2050 default
=3, min=0, soft_max
=10, precision
=1, step
=10, subtype
='FACTOR')
2051 use_outbg
= bpy
.props
.BoolProperty(
2052 name
="Highlight Outer Lines", description
="Add another line below every line to improve contrast",
2054 outbg_color
= bpy
.props
.FloatVectorProperty(
2055 name
="Outer Highlight", description
="Color of the highlight for outer lines",
2056 default
=(1.0, 1.0, 1.0, 1.0), min=0, max=1, subtype
='COLOR', size
=4)
2057 outbg_width
= bpy
.props
.FloatProperty(
2058 name
="Outer Highlight Thickness", description
="Relative thickness of the highlighting lines",
2059 default
=5, min=0, soft_max
=10, precision
=1, step
=10, subtype
='FACTOR')
2061 convex_color
= bpy
.props
.FloatVectorProperty(
2062 name
="Inner Convex Lines", description
="Color of lines to be folded to a convex angle",
2063 default
=(0.0, 0.0, 0.0, 1.0), min=0, max=1, subtype
='COLOR', size
=4)
2064 convex_style
= bpy
.props
.EnumProperty(
2065 name
="Convex Lines Drawing Style", description
="Drawing style of lines to be folded to a convex angle",
2066 default
='DASH', items
=line_styles
)
2067 convex_width
= bpy
.props
.FloatProperty(
2068 name
="Convex Lines Thickness", description
="Relative thickness of concave lines",
2069 default
=2, min=0, soft_max
=10, precision
=1, step
=10, subtype
='FACTOR')
2070 concave_color
= bpy
.props
.FloatVectorProperty(
2071 name
="Inner Concave Lines", description
="Color of lines to be folded to a concave angle",
2072 default
=(0.0, 0.0, 0.0, 1.0), min=0, max=1, subtype
='COLOR', size
=4)
2073 concave_style
= bpy
.props
.EnumProperty(
2074 name
="Concave Lines Drawing Style", description
="Drawing style of lines to be folded to a concave angle",
2075 default
='DASHDOT', items
=line_styles
)
2076 concave_width
= bpy
.props
.FloatProperty(
2077 name
="Concave Lines Thickness", description
="Relative thickness of concave lines",
2078 default
=2, min=0, soft_max
=10, precision
=1, step
=10, subtype
='FACTOR')
2079 freestyle_color
= bpy
.props
.FloatVectorProperty(
2080 name
="Freestyle Edges", description
="Color of lines marked as Freestyle Edge",
2081 default
=(0.0, 0.0, 0.0, 1.0), min=0, max=1, subtype
='COLOR', size
=4)
2082 freestyle_style
= bpy
.props
.EnumProperty(
2083 name
="Freestyle Edges Drawing Style", description
="Drawing style of Freestyle Edges",
2084 default
='SOLID', items
=line_styles
)
2085 freestyle_width
= bpy
.props
.FloatProperty(
2086 name
="Freestyle Edges Thickness", description
="Relative thickness of Freestyle edges",
2087 default
=2, min=0, soft_max
=10, precision
=1, step
=10, subtype
='FACTOR')
2088 use_inbg
= bpy
.props
.BoolProperty(
2089 name
="Highlight Inner Lines", description
="Add another line below every line to improve contrast",
2091 inbg_color
= bpy
.props
.FloatVectorProperty(
2092 name
="Inner Highlight", description
="Color of the highlight for inner lines",
2093 default
=(1.0, 1.0, 1.0, 1.0), min=0, max=1, subtype
='COLOR', size
=4)
2094 inbg_width
= bpy
.props
.FloatProperty(
2095 name
="Inner Highlight Thickness", description
="Relative thickness of the highlighting lines",
2096 default
=2, min=0, soft_max
=10, precision
=1, step
=10, subtype
='FACTOR')
2098 sticker_fill
= bpy
.props
.FloatVectorProperty(
2099 name
="Tabs Fill", description
="Fill color of sticking tabs",
2100 default
=(0.9, 0.9, 0.9, 1.0), min=0, max=1, subtype
='COLOR', size
=4)
2101 text_color
= bpy
.props
.FloatVectorProperty(
2102 name
="Text Color", description
="Color of all text used in the document",
2103 default
=(0.0, 0.0, 0.0, 1.0), min=0, max=1, subtype
='COLOR', size
=4)
2104 bpy
.utils
.register_class(PaperModelStyle
)
2107 class ExportPaperModel(bpy
.types
.Operator
):
2108 """Blender Operator: save the selected object's net and optionally bake its texture"""
2110 bl_idname
= "export_mesh.paper_model"
2111 bl_label
= "Export Paper Model"
2112 bl_description
= "Export the selected object's net and optionally bake its texture"
2113 filepath
= bpy
.props
.StringProperty(
2114 name
="File Path", description
="Target file to save the SVG", options
={'SKIP_SAVE'})
2115 filename
= bpy
.props
.StringProperty(
2116 name
="File Name", description
="Name of the file", options
={'SKIP_SAVE'})
2117 directory
= bpy
.props
.StringProperty(
2118 name
="Directory", description
="Directory of the file", options
={'SKIP_SAVE'})
2119 page_size_preset
= bpy
.props
.EnumProperty(
2120 name
="Page Size", description
="Size of the exported document",
2121 default
='A4', update
=page_size_preset_changed
, items
=global_paper_sizes
)
2122 output_size_x
= bpy
.props
.FloatProperty(
2123 name
="Page Width", description
="Width of the exported document",
2124 default
=0.210, soft_min
=0.105, soft_max
=0.841, subtype
="UNSIGNED", unit
="LENGTH")
2125 output_size_y
= bpy
.props
.FloatProperty(
2126 name
="Page Height", description
="Height of the exported document",
2127 default
=0.297, soft_min
=0.148, soft_max
=1.189, subtype
="UNSIGNED", unit
="LENGTH")
2128 output_margin
= bpy
.props
.FloatProperty(
2129 name
="Page Margin", description
="Distance from page borders to the printable area",
2130 default
=0.005, min=0, soft_max
=0.1, step
=0.1, subtype
="UNSIGNED", unit
="LENGTH")
2131 output_type
= bpy
.props
.EnumProperty(
2132 name
="Textures", description
="Source of a texture for the model",
2133 default
='NONE', items
=[
2134 ('NONE', "No Texture", "Export the net only"),
2135 ('TEXTURE', "From Materials", "Render the diffuse color and all painted textures"),
2136 ('AMBIENT_OCCLUSION', "Ambient Occlusion", "Render the Ambient Occlusion pass"),
2137 ('RENDER', "Full Render", "Render the material in actual scene illumination"),
2138 ('SELECTED_TO_ACTIVE', "Selected to Active", "Render all selected surrounding objects as a texture")
2140 do_create_stickers
= bpy
.props
.BoolProperty(
2141 name
="Create Tabs", description
="Create gluing tabs around the net (useful for paper)",
2143 do_create_numbers
= bpy
.props
.BoolProperty(
2144 name
="Create Numbers", description
="Enumerate edges to make it clear which edges should be sticked together",
2146 sticker_width
= bpy
.props
.FloatProperty(
2147 name
="Tabs and Text Size", description
="Width of gluing tabs and their numbers",
2148 default
=0.005, soft_min
=0, soft_max
=0.05, step
=0.1, subtype
="UNSIGNED", unit
="LENGTH")
2149 angle_epsilon
= bpy
.props
.FloatProperty(
2150 name
="Hidden Edge Angle", description
="Folds with angle below this limit will not be drawn",
2151 default
=pi
/360, min=0, soft_max
=pi
/4, step
=0.01, subtype
="ANGLE", unit
="ROTATION")
2152 output_dpi
= bpy
.props
.FloatProperty(
2153 name
="Resolution (DPI)", description
="Resolution of images in pixels per inch",
2154 default
=90, min=1, soft_min
=30, soft_max
=600, subtype
="UNSIGNED")
2155 file_format
= bpy
.props
.EnumProperty(
2156 name
="Document Format", description
="File format of the exported net",
2157 default
='PDF', items
=[
2158 ('PDF', "PDF", "Adobe Portable Document Format 1.4"),
2159 ('SVG', "SVG", "W3C Scalable Vector Graphics"),
2161 image_packing
= bpy
.props
.EnumProperty(
2162 name
="Image Packing Method", description
="Method of attaching baked image(s) to the SVG",
2163 default
='ISLAND_EMBED', items
=[
2164 ('PAGE_LINK', "Single Linked", "Bake one image per page of output and save it separately"),
2165 ('ISLAND_LINK', "Linked", "Bake images separately for each island and save them in a directory"),
2166 ('ISLAND_EMBED', "Embedded", "Bake images separately for each island and embed them into the SVG")
2168 scale
= bpy
.props
.FloatProperty(
2169 name
="Scale", description
="Divisor of all dimensions when exporting",
2170 default
=1, soft_min
=1.0, soft_max
=10000.0, step
=100, subtype
='UNSIGNED', precision
=1)
2171 do_create_uvmap
= bpy
.props
.BoolProperty(
2172 name
="Create UVMap", description
="Create a new UV Map showing the islands and page layout",
2173 default
=False, options
={'SKIP_SAVE'})
2174 ui_expanded_document
= bpy
.props
.BoolProperty(
2175 name
="Show Document Settings Expanded", description
="Shows the box 'Document Settings' expanded in user interface",
2176 default
=True, options
={'SKIP_SAVE'})
2177 ui_expanded_style
= bpy
.props
.BoolProperty(
2178 name
="Show Style Settings Expanded", description
="Shows the box 'Colors and Style' expanded in user interface",
2179 default
=False, options
={'SKIP_SAVE'})
2180 style
= bpy
.props
.PointerProperty(type=PaperModelStyle
)
2183 largest_island_ratio
= 0
2186 def poll(cls
, context
):
2187 return context
.active_object
and context
.active_object
.type == 'MESH'
2189 def execute(self
, context
):
2191 if self
.object.data
.paper_island_list
:
2192 self
.unfolder
.copy_island_names(self
.object.data
.paper_island_list
)
2193 self
.unfolder
.save(self
.properties
)
2194 self
.report({'INFO'}, "Saved a {}-page document".format(len(self
.unfolder
.mesh
.pages
)))
2196 except UnfoldError
as error
:
2197 self
.report(type={'ERROR_INVALID_INPUT'}, message
=error
.args
[0])
2198 return {'CANCELLED'}
2200 def get_scale_ratio(self
, sce
):
2201 margin
= self
.output_margin
+ self
.sticker_width
+ 1e-5
2202 if min(self
.output_size_x
, self
.output_size_y
) <= 2 * margin
:
2204 output_inner_size
= M
.Vector((self
.output_size_x
- 2*margin
, self
.output_size_y
- 2*margin
))
2205 ratio
= self
.unfolder
.mesh
.largest_island_ratio(output_inner_size
)
2206 return ratio
* sce
.unit_settings
.scale_length
/ self
.scale
2208 def invoke(self
, context
, event
):
2210 recall_mode
= context
.object.mode
2211 bpy
.ops
.object.mode_set(mode
='OBJECT')
2213 self
.scale
= sce
.paper_model
.scale
2214 self
.object = context
.active_object
2215 cage_size
= M
.Vector((sce
.paper_model
.output_size_x
, sce
.paper_model
.output_size_y
)) if sce
.paper_model
.limit_by_page
else None
2217 self
.unfolder
= Unfolder(self
.object)
2218 self
.unfolder
.prepare(
2219 cage_size
, create_uvmap
=self
.do_create_uvmap
,
2220 scale
=sce
.unit_settings
.scale_length
/self
.scale
)
2221 except UnfoldError
as error
:
2222 self
.report(type={'ERROR_INVALID_INPUT'}, message
=error
.args
[0])
2223 bpy
.ops
.object.mode_set(mode
=recall_mode
)
2224 return {'CANCELLED'}
2225 scale_ratio
= self
.get_scale_ratio(sce
)
2227 self
.scale
= ceil(self
.scale
* scale_ratio
)
2228 wm
= context
.window_manager
2229 wm
.fileselect_add(self
)
2231 bpy
.ops
.object.mode_set(mode
=recall_mode
)
2232 return {'RUNNING_MODAL'}
2234 def draw(self
, context
):
2235 layout
= self
.layout
2237 layout
.prop(self
.properties
, "do_create_uvmap")
2239 row
= layout
.row(align
=True)
2240 row
.menu("VIEW3D_MT_paper_model_presets", text
=bpy
.types
.VIEW3D_MT_paper_model_presets
.bl_label
)
2241 row
.operator("export_mesh.paper_model_preset_add", text
="", icon
='ADD')
2242 row
.operator("export_mesh.paper_model_preset_add", text
="", icon
='REMOVE').remove_active
= True
2244 # a little hack: this prints out something like "Scale: 1: 72"
2245 layout
.prop(self
.properties
, "scale", text
="Scale: 1")
2246 scale_ratio
= self
.get_scale_ratio(context
.scene
)
2249 text
="An island is roughly {:.1f}x bigger than page".format(scale_ratio
),
2251 elif scale_ratio
> 0:
2252 layout
.label(text
="Largest island is roughly 1/{:.1f} of page".format(1 / scale_ratio
))
2254 if context
.scene
.unit_settings
.scale_length
!= 1:
2256 text
="Unit scale {:.1f} makes page size etc. not display correctly".format(
2257 context
.scene
.unit_settings
.scale_length
), icon
="ERROR")
2259 row
= box
.row(align
=True)
2261 self
.properties
, "ui_expanded_document", text
="",
2262 icon
=('TRIA_DOWN' if self
.ui_expanded_document
else 'TRIA_RIGHT'), emboss
=False)
2263 row
.label(text
="Document Settings")
2265 if self
.ui_expanded_document
:
2266 box
.prop(self
.properties
, "file_format", text
="Format")
2267 box
.prop(self
.properties
, "page_size_preset")
2268 col
= box
.column(align
=True)
2269 col
.active
= self
.page_size_preset
== 'USER'
2270 col
.prop(self
.properties
, "output_size_x")
2271 col
.prop(self
.properties
, "output_size_y")
2272 box
.prop(self
.properties
, "output_margin")
2274 col
.prop(self
.properties
, "do_create_stickers")
2275 col
.prop(self
.properties
, "do_create_numbers")
2277 col
.active
= self
.do_create_stickers
or self
.do_create_numbers
2278 col
.prop(self
.properties
, "sticker_width")
2279 box
.prop(self
.properties
, "angle_epsilon")
2281 box
.prop(self
.properties
, "output_type")
2283 col
.active
= (self
.output_type
!= 'NONE')
2284 if len(self
.object.data
.uv_textures
) == 8:
2285 col
.label(text
="No UV slots left, No Texture is the only option.", icon
='ERROR')
2286 elif context
.scene
.render
.engine
not in ('BLENDER_RENDER', 'CYCLES') and self
.output_type
!= 'NONE':
2287 col
.label(text
="Blender Internal engine will be used for texture baking.", icon
='ERROR')
2288 col
.prop(self
.properties
, "output_dpi")
2290 row
.active
= self
.file_format
== 'SVG'
2291 row
.prop(self
.properties
, "image_packing", text
="Images")
2294 row
= box
.row(align
=True)
2296 self
.properties
, "ui_expanded_style", text
="",
2297 icon
=('TRIA_DOWN' if self
.ui_expanded_style
else 'TRIA_RIGHT'), emboss
=False)
2298 row
.label(text
="Colors and Style")
2300 if self
.ui_expanded_style
:
2301 box
.prop(self
.style
, "line_width", text
="Default line width")
2303 col
.prop(self
.style
, "outer_color")
2304 col
.prop(self
.style
, "outer_width", text
="Relative width")
2305 col
.prop(self
.style
, "outer_style", text
="Style")
2307 col
.active
= self
.output_type
!= 'NONE'
2308 col
.prop(self
.style
, "use_outbg", text
="Outer Lines Highlight:")
2310 sub
.active
= self
.output_type
!= 'NONE' and self
.style
.use_outbg
2311 sub
.prop(self
.style
, "outbg_color", text
="")
2312 sub
.prop(self
.style
, "outbg_width", text
="Relative width")
2314 col
.prop(self
.style
, "convex_color")
2315 col
.prop(self
.style
, "convex_width", text
="Relative width")
2316 col
.prop(self
.style
, "convex_style", text
="Style")
2318 col
.prop(self
.style
, "concave_color")
2319 col
.prop(self
.style
, "concave_width", text
="Relative width")
2320 col
.prop(self
.style
, "concave_style", text
="Style")
2322 col
.prop(self
.style
, "freestyle_color")
2323 col
.prop(self
.style
, "freestyle_width", text
="Relative width")
2324 col
.prop(self
.style
, "freestyle_style", text
="Style")
2326 col
.active
= self
.output_type
!= 'NONE'
2327 col
.prop(self
.style
, "use_inbg", text
="Inner Lines Highlight:")
2329 sub
.active
= self
.output_type
!= 'NONE' and self
.style
.use_inbg
2330 sub
.prop(self
.style
, "inbg_color", text
="")
2331 sub
.prop(self
.style
, "inbg_width", text
="Relative width")
2333 col
.active
= self
.do_create_stickers
2334 col
.prop(self
.style
, "sticker_fill")
2335 box
.prop(self
.style
, "text_color")
2338 def menu_func(self
, context
):
2339 self
.layout
.operator("export_mesh.paper_model", text
="Paper Model (.svg)")
2342 class VIEW3D_MT_paper_model_presets(bpy
.types
.Menu
):
2343 bl_label
= "Paper Model Presets"
2344 preset_subdir
= "export_mesh"
2345 preset_operator
= "script.execute_preset"
2346 draw
= bpy
.types
.Menu
.draw_preset
2349 class AddPresetPaperModel(bl_operators
.presets
.AddPresetBase
, bpy
.types
.Operator
):
2350 """Add or remove a Paper Model Preset"""
2351 bl_idname
= "export_mesh.paper_model_preset_add"
2352 bl_label
= "Add Paper Model Preset"
2353 preset_menu
= "VIEW3D_MT_paper_model_presets"
2354 preset_subdir
= "export_mesh"
2355 preset_defines
= ["op = bpy.context.active_operator"]
2358 def preset_values(self
):
2359 op
= bpy
.ops
.export_mesh
.paper_model
2360 properties
= op
.get_rna_type().properties
.items()
2361 blacklist
= bpy
.types
.Operator
.bl_rna
.properties
.keys()
2363 "op.{}".format(prop_id
) for (prop_id
, prop
) in properties
2364 if not (prop
.is_hidden
or prop
.is_skip_save
or prop_id
in blacklist
)]
2367 class VIEW3D_PT_paper_model_tools(bpy
.types
.Panel
):
2369 bl_space_type
= "VIEW_3D"
2370 bl_region_type
= "TOOLS"
2371 bl_category
= "Paper Model"
2373 def draw(self
, context
):
2374 layout
= self
.layout
2376 obj
= context
.active_object
2377 mesh
= obj
.data
if obj
and obj
.type == 'MESH' else None
2379 layout
.operator("export_mesh.paper_model")
2381 col
= layout
.column(align
=True)
2382 col
.label("Customization:")
2383 col
.operator("mesh.unfold")
2385 if context
.mode
== 'EDIT_MESH':
2386 row
= layout
.row(align
=True)
2387 row
.operator("mesh.mark_seam", text
="Mark Seam").clear
= False
2388 row
.operator("mesh.mark_seam", text
="Clear Seam").clear
= True
2390 layout
.operator("mesh.clear_all_seams")
2392 props
= sce
.paper_model
2393 layout
.prop(props
, "scale", text
="Model Scale: 1")
2395 layout
.prop(props
, "limit_by_page")
2396 col
= layout
.column()
2397 col
.active
= props
.limit_by_page
2398 col
.prop(props
, "page_size_preset")
2399 sub
= col
.column(align
=True)
2400 sub
.active
= props
.page_size_preset
== 'USER'
2401 sub
.prop(props
, "output_size_x")
2402 sub
.prop(props
, "output_size_y")
2405 class VIEW3D_PT_paper_model_islands(bpy
.types
.Panel
):
2406 bl_label
= "Islands"
2407 bl_space_type
= "VIEW_3D"
2408 bl_region_type
= "TOOLS"
2409 bl_category
= "Paper Model"
2411 def draw(self
, context
):
2412 layout
= self
.layout
2414 obj
= context
.active_object
2415 mesh
= obj
.data
if obj
and obj
.type == 'MESH' else None
2417 if mesh
and mesh
.paper_island_list
:
2419 text
="1 island:" if len(mesh
.paper_island_list
) == 1 else
2420 "{} islands:".format(len(mesh
.paper_island_list
)))
2421 layout
.template_list(
2422 'UI_UL_list', 'paper_model_island_list', mesh
,
2423 'paper_island_list', mesh
, 'paper_island_index', rows
=1, maxrows
=5)
2424 if mesh
.paper_island_index
>= 0:
2425 list_item
= mesh
.paper_island_list
[mesh
.paper_island_index
]
2426 sub
= layout
.column(align
=True)
2427 sub
.prop(list_item
, "auto_label")
2428 sub
.prop(list_item
, "label")
2429 sub
.prop(list_item
, "auto_abbrev")
2431 row
.active
= not list_item
.auto_abbrev
2432 row
.prop(list_item
, "abbreviation")
2434 layout
.label(text
="Not unfolded")
2435 layout
.box().label("Use the 'Unfold' tool")
2436 sub
= layout
.column(align
=True)
2437 sub
.active
= bool(mesh
and mesh
.paper_island_list
)
2438 sub
.prop(sce
.paper_model
, "display_islands", icon
='RESTRICT_VIEW_OFF')
2439 row
= sub
.row(align
=True)
2440 row
.active
= bool(sce
.paper_model
.display_islands
and mesh
and mesh
.paper_island_list
)
2441 row
.prop(sce
.paper_model
, "islands_alpha", slider
=True)
2444 def display_islands(self
, context
):
2445 # TODO: save the vertex positions and don't recalculate them always?
2446 ob
= context
.active_object
2447 if not ob
or ob
.type != 'MESH':
2450 if not mesh
.paper_island_list
or mesh
.paper_island_index
== -1:
2453 bgl
.glMatrixMode(bgl
.GL_PROJECTION
)
2454 perspMatrix
= context
.space_data
.region_3d
.perspective_matrix
2455 perspBuff
= bgl
.Buffer(bgl
.GL_FLOAT
, (4, 4), perspMatrix
.transposed())
2456 bgl
.glLoadMatrixf(perspBuff
)
2457 bgl
.glMatrixMode(bgl
.GL_MODELVIEW
)
2458 objectBuff
= bgl
.Buffer(bgl
.GL_FLOAT
, (4, 4), ob
.matrix_world
.transposed())
2459 bgl
.glLoadMatrixf(objectBuff
)
2460 bgl
.glEnable(bgl
.GL_BLEND
)
2461 bgl
.glBlendFunc(bgl
.GL_SRC_ALPHA
, bgl
.GL_ONE_MINUS_SRC_ALPHA
)
2462 bgl
.glEnable(bgl
.GL_POLYGON_OFFSET_FILL
)
2463 bgl
.glPolygonOffset(0, -10) # offset in Zbuffer to remove flicker
2464 bgl
.glPolygonMode(bgl
.GL_FRONT_AND_BACK
, bgl
.GL_FILL
)
2465 bgl
.glColor4f(1.0, 0.4, 0.0, self
.islands_alpha
)
2466 island
= mesh
.paper_island_list
[mesh
.paper_island_index
]
2467 for lface
in island
.faces
:
2468 face
= mesh
.polygons
[lface
.id]
2469 bgl
.glBegin(bgl
.GL_POLYGON
)
2470 for vertex_id
in face
.vertices
:
2471 vertex
= mesh
.vertices
[vertex_id
]
2472 bgl
.glVertex4f(*vertex
.co
.to_4d())
2474 bgl
.glPolygonOffset(0.0, 0.0)
2475 bgl
.glDisable(bgl
.GL_POLYGON_OFFSET_FILL
)
2476 bgl
.glLoadIdentity()
2477 display_islands
.handle
= None
2480 def display_islands_changed(self
, context
):
2481 """Switch highlighting islands on/off"""
2482 if self
.display_islands
:
2483 if not display_islands
.handle
:
2484 display_islands
.handle
= bpy
.types
.SpaceView3D
.draw_handler_add(
2485 display_islands
, (self
, context
), 'WINDOW', 'POST_VIEW')
2487 if display_islands
.handle
:
2488 bpy
.types
.SpaceView3D
.draw_handler_remove(display_islands
.handle
, 'WINDOW')
2489 display_islands
.handle
= None
2492 def label_changed(self
, context
):
2493 """The label of an island was changed"""
2494 # accessing properties via [..] to avoid a recursive call after the update
2495 self
["auto_label"] = not self
.label
or self
.label
.isspace()
2496 island_item_changed(self
, context
)
2499 def island_item_changed(self
, context
):
2500 """The labelling of an island was changed"""
2501 def increment(abbrev
, collisions
):
2502 letters
= "ABCDEFGHIJKLMNPQRSTUVWXYZ123456789"
2503 while abbrev
in collisions
:
2504 abbrev
= abbrev
.rstrip(letters
[-1])
2505 abbrev
= abbrev
[:2] + letters
[letters
.find(abbrev
[-1]) + 1 if len(abbrev
) == 3 else 0]
2508 # accessing properties via [..] to avoid a recursive call after the update
2509 island_list
= context
.active_object
.data
.paper_island_list
2511 self
["label"] = "" # avoid self-conflict
2513 while any(item
.label
== "Island {}".format(number
) for item
in island_list
):
2515 self
["label"] = "Island {}".format(number
)
2516 if self
.auto_abbrev
:
2517 self
["abbreviation"] = "" # avoid self-conflict
2518 abbrev
= "".join(first_letters(self
.label
))[:3].upper()
2519 self
["abbreviation"] = increment(abbrev
, {item
.abbreviation
for item
in island_list
})
2520 elif len(self
.abbreviation
) > 3:
2521 self
["abbreviation"] = self
.abbreviation
[:3]
2522 self
.name
= "[{}] {} ({} {})".format(
2523 self
.abbreviation
, self
.label
, len(self
.faces
), "faces" if len(self
.faces
) > 1 else "face")
2526 class FaceList(bpy
.types
.PropertyGroup
):
2527 id = bpy
.props
.IntProperty(name
="Face ID")
2528 bpy
.utils
.register_class(FaceList
)
2531 class IslandList(bpy
.types
.PropertyGroup
):
2532 faces
= bpy
.props
.CollectionProperty(
2533 name
="Faces", description
="Faces belonging to this island", type=FaceList
)
2534 label
= bpy
.props
.StringProperty(
2535 name
="Label", description
="Label on this island",
2536 default
="", update
=label_changed
)
2537 abbreviation
= bpy
.props
.StringProperty(
2538 name
="Abbreviation", description
="Three-letter label to use when there is not enough space",
2539 default
="", update
=island_item_changed
)
2540 auto_label
= bpy
.props
.BoolProperty(
2541 name
="Auto Label", description
="Generate the label automatically",
2542 default
=True, update
=island_item_changed
)
2543 auto_abbrev
= bpy
.props
.BoolProperty(
2544 name
="Auto Abbreviation", description
="Generate the abbreviation automatically",
2545 default
=True, update
=island_item_changed
)
2546 bpy
.utils
.register_class(IslandList
)
2549 class PaperModelSettings(bpy
.types
.PropertyGroup
):
2550 display_islands
= bpy
.props
.BoolProperty(
2551 name
="Highlight selected island", description
="Highlight faces corresponding to the selected island in the 3D View",
2552 options
={'SKIP_SAVE'}, update
=display_islands_changed
)
2553 islands_alpha
= bpy
.props
.FloatProperty(
2554 name
="Opacity", description
="Opacity of island highlighting",
2555 min=0.0, max=1.0, default
=0.3)
2556 limit_by_page
= bpy
.props
.BoolProperty(
2557 name
="Limit Island Size", description
="Do not create islands larger than given dimensions",
2558 default
=False, update
=page_size_preset_changed
)
2559 page_size_preset
= bpy
.props
.EnumProperty(
2560 name
="Page Size", description
="Maximal size of an island",
2561 default
='A4', update
=page_size_preset_changed
, items
=global_paper_sizes
)
2562 output_size_x
= bpy
.props
.FloatProperty(
2563 name
="Width", description
="Maximal width of an island",
2564 default
=0.2, soft_min
=0.105, soft_max
=0.841, subtype
="UNSIGNED", unit
="LENGTH")
2565 output_size_y
= bpy
.props
.FloatProperty(
2566 name
="Height", description
="Maximal height of an island",
2567 default
=0.29, soft_min
=0.148, soft_max
=1.189, subtype
="UNSIGNED", unit
="LENGTH")
2568 scale
= bpy
.props
.FloatProperty(
2569 name
="Scale", description
="Divisor of all dimensions when exporting",
2570 default
=1, soft_min
=1.0, soft_max
=10000.0, step
=100, subtype
='UNSIGNED', precision
=1)
2571 bpy
.utils
.register_class(PaperModelSettings
)
2575 bpy
.utils
.register_module(__name__
)
2577 bpy
.types
.Scene
.paper_model
= bpy
.props
.PointerProperty(
2578 name
="Paper Model", description
="Settings of the Export Paper Model script",
2579 type=PaperModelSettings
, options
={'SKIP_SAVE'})
2580 bpy
.types
.Mesh
.paper_island_list
= bpy
.props
.CollectionProperty(
2581 name
="Island List", type=IslandList
)
2582 bpy
.types
.Mesh
.paper_island_index
= bpy
.props
.IntProperty(
2583 name
="Island List Index",
2584 default
=-1, min=-1, max=100, options
={'SKIP_SAVE'})
2585 bpy
.types
.TOPBAR_MT_file_export
.append(menu_func
)
2589 bpy
.utils
.unregister_module(__name__
)
2590 bpy
.types
.TOPBAR_MT_file_export
.remove(menu_func
)
2591 if display_islands
.handle
:
2592 bpy
.types
.SpaceView3D
.draw_handler_remove(display_islands
.handle
, 'WINDOW')
2593 display_islands
.handle
= None
2596 if __name__
== "__main__":