From c9605694fa2abb23f51dedafaa03eadd34f3aab5 Mon Sep 17 00:00:00 2001 From: Adam Dominec Date: Sun, 6 Aug 2017 10:12:08 +0200 Subject: io_export_paper_model: update to upstream 932e32 --- io_export_paper_model.py | 654 ++++++++++++++++++++++++++--------------------- 1 file changed, 366 insertions(+), 288 deletions(-) diff --git a/io_export_paper_model.py b/io_export_paper_model.py index 508b953f..1203d18f 100644 --- a/io_export_paper_model.py +++ b/io_export_paper_model.py @@ -20,7 +20,7 @@ bl_info = { "name": "Export Paper Model", "author": "Addam Dominec", "version": (0, 9), - "blender": (2, 70, 0), + "blender": (2, 73, 0), "location": "File > Export > Paper Model", "warning": "", "description": "Export printable net of the active mesh", @@ -30,12 +30,10 @@ bl_info = { "tracker_url": "https://developer.blender.org/T38441" } -#### TODO: -# sanitize the constructors so that they don't edit their parent object -# rename verts -> vertices, edge.vect -> edge.vector -# SVG object doesn't need a 'pure_net' argument in constructor -# remember selected objects before baking, except selected to active -# islands with default names should be excluded while matching +# TODO: +# sanitize the constructors Edge, Face, UVFace so that they don't edit their parent object +# The Exporter classes should take parameters as a whole pack, and parse it themselves +# remember objects selected before baking (except selected to active) # add 'estimated number of pages' to the export UI # profile QuickSweepline vs. BruteSweepline with/without blist: for which nets is it faster? # rotate islands to minimize area -- and change that only if necessary to fill the page size @@ -76,6 +74,14 @@ default_priority_effect = { 'LENGTH': -0.05 } +global_paper_sizes = [ + ('USER', "User defined", "User defined paper size"), + ('A4', "A4", "International standard paper size"), + ('A3', "A3", "International standard paper size"), + ('US_LETTER', "Letter", "North American paper size"), + ('US_LEGAL', "Legal", "North American paper size") +] + def first_letters(text): """Iterator over the first letter of each word""" @@ -104,10 +110,10 @@ def pairs(sequence): def argmax_pair(array, key): """Find an (unordered) pair of indices that maximize the given function""" - l = len(array) + n = len(array) mi, mj, m = None, None, None - for i in range(l): - for j in range(i+1, l): + for i in range(n): + for j in range(i+1, n): k = key(array[i], array[j]) if not m or k > m: mi, mj, m = i, j, k @@ -124,10 +130,10 @@ def fitting_matrix(v1, v2): def z_up_matrix(n): """Get a rotation matrix that aligns given vector upwards.""" b = n.xy.length - l = n.length + s = n.length if b > 0: return M.Matrix(( - (n.x*n.z/(b*l), n.y*n.z/(b*l), -b/l), + (n.x*n.z/(b*s), n.y*n.z/(b*s), -b/s), (-n.y/b, n.x/b, 0), (0, 0, 0) )) @@ -146,7 +152,8 @@ def create_blank_image(image_name, dimensions, alpha=1): width, height = int(dimensions.x), int(dimensions.y) image = bpy.data.images.new(image_name, width, height, alpha=True) if image.users > 0: - raise UnfoldError("There is something wrong with the material of the model. " + raise UnfoldError( + "There is something wrong with the material of the model. " "Please report this on the BlenderArtists forum. Export failed.") image.pixels = [1, 1, 1, alpha] * (width * height) image.file_format = 'PNG' @@ -175,7 +182,10 @@ def bake(face_indices, uvmap, image): me.materials.append(mat) loop = me.uv_layers[me.uv_layers.active_index].data face_indices = set(face_indices) - ignored_uvs = [face.loop_start + i for face in me.polygons if face.index not in face_indices for i, v in enumerate(face.vertices)] + ignored_uvs = [ + face.loop_start + i + for face in me.polygons if face.index not in face_indices + for i, v in enumerate(face.vertices)] for vid in ignored_uvs: loop[vid].uv[0] *= -1 loop[vid].uv[1] *= -1 @@ -284,7 +294,7 @@ class Unfolder: bk = rd.bake if rd.engine == 'CYCLES': recall = sce.cycles.bake_type, bk.use_selected_to_active, bk.margin, bk.cage_extrusion, bk.use_cage, bk.use_clear - lookup = {'TEXTURE': 'DIFFUSE_COLOR', 'AMBIENT_OCCLUSION': 'AO', 'RENDER': 'COMBINED', 'SELECTED_TO_ACTIVE': 'COMBINED'} + lookup = {'TEXTURE': 'DIFFUSE', 'AMBIENT_OCCLUSION': 'AO', 'RENDER': 'COMBINED', 'SELECTED_TO_ACTIVE': 'COMBINED'} sce.cycles.bake_type = lookup[properties.output_type] bk.use_selected_to_active = (properties.output_type == 'SELECTED_TO_ACTIVE') bk.margin, bk.cage_extrusion, bk.use_cage, bk.use_clear = 0, 10, False, False @@ -299,7 +309,8 @@ class Unfolder: if image_packing == 'PAGE_LINK': self.mesh.save_image(tex, printable_size * ppm, filepath) elif image_packing == 'ISLAND_LINK': - self.mesh.save_separate_images(tex, ppm, filepath) + image_dir = filepath[:filepath.rfind(".")] + self.mesh.save_separate_images(tex, ppm, image_dir) elif image_packing == 'ISLAND_EMBED': self.mesh.save_separate_images(tex, ppm, filepath, embed=Exporter.encode_image) @@ -322,7 +333,7 @@ class Mesh: """Wrapper for Bpy Mesh""" def __init__(self, mesh, matrix): - self.verts = dict() + self.vertices = dict() self.edges = dict() self.edges_by_verts_indices = dict() self.faces = dict() @@ -330,7 +341,7 @@ class Mesh: self.data = mesh self.pages = list() for bpy_vertex in mesh.vertices: - self.verts[bpy_vertex.index] = Vertex(bpy_vertex, matrix) + self.vertices[bpy_vertex.index] = Vertex(bpy_vertex, matrix) for bpy_edge in mesh.edges: edge = Edge(bpy_edge, self, matrix) self.edges[bpy_edge.index] = edge @@ -346,7 +357,7 @@ class Mesh: def check_correct(self, epsilon=1e-6): """Check for invalid geometry""" - null_edges = {i for i, e in self.edges.items() if e.length < epsilon and e.faces} + null_edges = {i for i, e in self.edges.items() if e.vector.length < epsilon and e.faces} null_faces = {i for i, f in self.faces.items() if f.normal.length_squared < epsilon} twisted_faces = {i for i, f in self.faces.items() if f.is_twisted()} if not (null_edges or null_faces or twisted_faces): @@ -358,8 +369,11 @@ class Mesh: edge.select = (edge.index in null_edges) for face in self.data.polygons: face.select = (face.index in null_faces or face.index in twisted_faces) - cure = "Remove Doubles and Triangulate" if (null_edges or null_faces) and twisted_faces else "Triangulate" if twisted_faces else "Remove Doubles" - raise UnfoldError("The model contains:\n" + + cure = ("Remove Doubles and Triangulate" if (null_edges or null_faces) and twisted_faces + else "Triangulate" if twisted_faces + else"Remove Doubles") + raise UnfoldError( + "The model contains:\n" + (" {} zero-length edge(s)\n".format(len(null_edges)) if null_edges else "") + (" {} zero-area face(s)\n".format(len(null_faces)) if null_faces else "") + (" {} twisted polygon(s)\n".format(len(twisted_faces)) if twisted_faces else "") + @@ -373,12 +387,12 @@ class Mesh: edges = [edge for edge in self.edges.values() if not edge.force_cut and len(edge.faces) > 1] if edges: - average_length = sum(edge.length for edge in edges) / len(edges) + average_length = sum(edge.vector.length for edge in edges) / len(edges) for edge in edges: edge.generate_priority(priority_effect, average_length) edges.sort(reverse=False, key=lambda edge: edge.priority) for edge in edges: - if edge.length == 0: + if edge.vector.length_squared == 0: continue face_a, face_b = edge.main_faces island_a, island_b = face_a.uvface.island, face_b.uvface.island @@ -449,7 +463,9 @@ class Mesh: return direction_to_float(uvedge.vb.co - uvedge.va.co) uvedges.sort(key=uvedge_sortkey) - for right, left in zip(uvedges[:-1:2], uvedges[1::2]) if is_inwards(uvedges[0]) else zip([uvedges[-1]] + uvedges[1::2], uvedges[:-1:2]): + for right, left in ( + zip(uvedges[:-1:2], uvedges[1::2]) if is_inwards(uvedges[0]) + else zip([uvedges[-1]] + uvedges[1::2], uvedges[:-1:2])): left.neighbor_right = right right.neighbor_left = left return True @@ -465,14 +481,14 @@ class Mesh: def uvedge_priority(uvedge): """Retuns whether it is a good idea to stick something on this edge's face""" # TODO: it should take into account overlaps with faces and with other stickers - return uvedge.uvface.face.area / sum((vb.co - va.co).length for (va, vb) in pairs(uvedge.uvface.verts)) + return uvedge.uvface.face.area / sum((vb.co - va.co).length for (va, vb) in pairs(uvedge.uvface.vertices)) def add_sticker(uvedge, index, target_island): uvedge.sticker = Sticker(uvedge, default_width, index, target_island) uvedge.island.add_marker(uvedge.sticker) for edge in self.edges.values(): - if edge.is_main_cut and len(edge.uvedges) >= 2 and edge.vect.length_squared > 0: + if edge.is_main_cut and len(edge.uvedges) >= 2 and edge.vector.length_squared > 0: uvedge_a, uvedge_b = edge.uvedges[:2] if uvedge_priority(uvedge_a) < uvedge_priority(uvedge_b): uvedge_a, uvedge_b = uvedge_b, uvedge_a @@ -521,14 +537,14 @@ class Mesh: def scale_islands(self, scale): for island in self.islands: - for point in chain((vertex.co for vertex in island.verts), island.fake_verts): + for point in chain((vertex.co for vertex in island.vertices), island.fake_vertices): point *= scale def finalize_islands(self, is_landscape=False, title_height=0): for island in self.islands: if title_height: island.title = "[{}] {}".format(island.abbreviation, island.label) - points = list(vertex.co for vertex in island.verts) + island.fake_verts + points = list(vertex.co for vertex in island.vertices) + island.fake_vertices angle = M.geometry.box_fit_2d(points) rot = M.Matrix.Rotation(angle, 2) # ensure that the island matches page orientation (portrait/landscape) @@ -592,7 +608,8 @@ class Mesh: return [stop for stop, distance in zip(stops, chain([quantile], distances)) if distance >= quantile] if any(island.bounding_box.x > cage_size.x or island.bounding_box.y > cage_size.y for island in self.islands): - raise UnfoldError("An island is too big to fit onto page of the given size. " + raise UnfoldError( + "An island is too big to fit onto page of the given size. " "Either downscale the model or find and split that island manually.\n" "Export failed, sorry.") # sort islands by their diagonal... just a guess @@ -658,7 +675,7 @@ class Mesh: image_path = os_path.join(image_dir, "island{}.png".format(i)) image.filepath_raw = image_path image.save() - island.image_path = image.path + island.image_path = image_path image.user_clear() bpy.data.images.remove(image) @@ -683,14 +700,13 @@ class Vertex: class Edge: """Wrapper for BPy Edge""" __slots__ = ('va', 'vb', 'faces', 'main_faces', 'uvedges', - 'vect', 'length', 'angle', + 'vector', 'angle', 'is_main_cut', 'force_cut', 'priority', 'freestyle') def __init__(self, edge, mesh, matrix=1): - self.va = mesh.verts[edge.vertices[0]] - self.vb = mesh.verts[edge.vertices[1]] - self.vect = self.vb.co - self.va.co - self.length = self.vect.length + self.va = mesh.vertices[edge.vertices[0]] + self.vb = mesh.vertices[edge.vertices[1]] + self.vector = self.vb.co - self.va.co self.faces = list() # if self.main_faces is set, then self.uvedges[:2] must correspond to self.main_faces, in their order # this constraint is assured at the time of finishing mesh.generate_cuts @@ -703,9 +719,9 @@ class Edge: self.is_main_cut = True self.priority = None self.angle = None - self.freestyle = getattr(edge, "use_freestyle_mark", False) # freestyle edges will be highlighted - self.va.edges.append(self) #FIXME: editing foreign attribute - self.vb.edges.append(self) #FIXME: editing foreign attribute + self.freestyle = getattr(edge, "use_freestyle_mark", False) # freestyle edges will be highlighted + self.va.edges.append(self) # FIXME: editing foreign attribute + self.vb.edges.append(self) # FIXME: editing foreign attribute def choose_main_faces(self): """Choose two main faces that might get connected in an island""" @@ -720,11 +736,11 @@ class Edge: """Calculate the angle between the main faces""" face_a, face_b = self.main_faces if face_a.normal.length_squared == 0 or face_b.normal.length_squared == 0: - self.angle = -3 # just a very sharp angle + self.angle = -3 # just a very sharp angle return # correction if normals are flipped - a_is_clockwise = ((face_a.verts.index(self.va) - face_a.verts.index(self.vb)) % len(face_a.verts) == 1) - b_is_clockwise = ((face_b.verts.index(self.va) - face_b.verts.index(self.vb)) % len(face_b.verts) == 1) + a_is_clockwise = ((face_a.vertices.index(self.va) - face_a.vertices.index(self.vb)) % len(face_a.vertices) == 1) + b_is_clockwise = ((face_b.vertices.index(self.va) - face_b.vertices.index(self.vb)) % len(face_b.vertices) == 1) is_equal_flip = True if face_a.uvface and face_b.uvface: a_is_clockwise ^= face_a.uvface.flipped @@ -732,7 +748,7 @@ class Edge: is_equal_flip = (face_a.uvface.flipped == face_b.uvface.flipped) # TODO: maybe this need not be true in _really_ ugly cases: assert(a_is_clockwise != b_is_clockwise) if a_is_clockwise != b_is_clockwise: - if (a_is_clockwise == (face_b.normal.cross(face_a.normal).dot(self.vect) > 0)) == is_equal_flip: + if (a_is_clockwise == (face_b.normal.cross(face_a.normal).dot(self.vector) > 0)) == is_equal_flip: # the angle is convex self.angle = face_a.normal.angle(face_b.normal) else: @@ -750,7 +766,7 @@ class Edge: self.priority = priority_effect['CONVEX'] * angle / pi else: self.priority = priority_effect['CONCAVE'] * (-angle) / pi - self.priority += (self.length / average_length) * priority_effect['LENGTH'] + self.priority += (self.vector.length / average_length) * priority_effect['LENGTH'] def is_cut(self, face): """Return False if this edge will the given face to another one in the resulting net @@ -770,28 +786,28 @@ class Edge: class Face: """Wrapper for BPy Face""" - __slots__ = ('index', 'edges', 'verts', 'uvface', + __slots__ = ('index', 'edges', 'vertices', 'uvface', 'loop_start', 'area', 'normal') def __init__(self, bpy_face, mesh): self.index = bpy_face.index self.edges = list() - self.verts = [mesh.verts[i] for i in bpy_face.vertices] + self.vertices = [mesh.vertices[i] for i in bpy_face.vertices] self.loop_start = bpy_face.loop_start self.area = bpy_face.area self.uvface = None - self.normal = M.geometry.normal(v.co for v in self.verts) + self.normal = M.geometry.normal(v.co for v in self.vertices) for verts_indices in bpy_face.edge_keys: edge = mesh.edges_by_verts_indices[verts_indices] self.edges.append(edge) - edge.faces.append(self) #FIXME: editing foreign attribute + edge.faces.append(self) # FIXME: editing foreign attribute def is_twisted(self): - if len(self.verts) > 3: - center = sum((vertex.co for vertex in self.verts), M.Vector((0, 0, 0))) / len(self.verts) + if len(self.vertices) > 3: + center = sum((vertex.co for vertex in self.vertices), M.Vector((0, 0, 0))) / len(self.vertices) plane_d = center.dot(self.normal) - diameter = max((center - vertex.co).length for vertex in self.verts) - for vertex in self.verts: + diameter = max((center - vertex.co).length for vertex in self.vertices) + for vertex in self.vertices: # check coplanarity if abs(vertex.co.dot(self.normal) - plane_d) > diameter * 0.01: return True @@ -803,19 +819,19 @@ class Face: class Island: """Part of the net to be exported""" - __slots__ = ('faces', 'edges', 'verts', 'fake_verts', 'uvverts_by_id', 'boundary', 'markers', + __slots__ = ('faces', 'edges', 'vertices', 'fake_vertices', 'uvverts_by_id', 'boundary', 'markers', 'pos', 'bounding_box', 'image_path', 'embedded_image', 'number', 'label', 'abbreviation', 'title', 'has_safe_geometry', 'is_inside_out', 'sticker_numbering') - def __init__(self, face=None): + def __init__(self, face): """Create an Island from a single Face""" self.faces = list() self.edges = set() - self.verts = set() - self.fake_verts = list() + self.vertices = set() + self.fake_vertices = list() self.markers = list() self.label = None self.abbreviation = None @@ -826,14 +842,12 @@ class Island: self.is_inside_out = False # swaps concave <-> convex edges self.has_safe_geometry = True self.sticker_numbering = 0 - - if face: - uvface = UVFace(face, self) - self.verts.update(uvface.verts) - self.edges.update(uvface.edges) - self.faces.append(uvface) + uvface = UVFace(face, self) + self.vertices.update(uvface.vertices) + self.edges.update(uvface.edges) + self.faces.append(uvface) # speedup for Island.join - self.uvverts_by_id = {uvvertex.vertex.index: [uvvertex] for uvvertex in self.verts} + self.uvverts_by_id = {uvvertex.vertex.index: [uvvertex] for uvvertex in self.vertices} # UVEdges on the boundary self.boundary = list(self.edges) @@ -978,13 +992,17 @@ class Island: rot = fitting_matrix(flip * (first_b.co - second_b.co), uvedge_a.vb.co - uvedge_a.va.co) * flip trans = uvedge_a.vb.co - rot * first_b.co # extract and transform island_b's boundary - phantoms = {uvvertex: UVVertex(rot*uvvertex.co + trans, uvvertex.vertex) for uvvertex in other.verts} + phantoms = {uvvertex: UVVertex(rot*uvvertex.co + trans, uvvertex.vertex) for uvvertex in other.vertices} # check the size of the resulting island if size_limit: # first check: bounding box - bbox_width = max(max(seg.max.co.x for seg in self.boundary), max(vertex.co.x for vertex in phantoms)) - min(min(seg.min.co.x for seg in self.boundary), min(vertex.co.x for vertex in phantoms)) - bbox_height = max(max(seg.top for seg in self.boundary), max(vertex.co.y for vertex in phantoms)) - min(min(seg.bottom for seg in self.boundary), min(vertex.co.y for vertex in phantoms)) + left = min(min(seg.min.co.x for seg in self.boundary), min(vertex.co.x for vertex in phantoms)) + right = max(max(seg.max.co.x for seg in self.boundary), max(vertex.co.x for vertex in phantoms)) + bottom = min(min(seg.bottom for seg in self.boundary), min(vertex.co.y for vertex in phantoms)) + top = max(max(seg.top for seg in self.boundary), max(vertex.co.y for vertex in phantoms)) + bbox_width = right - left + bbox_height = top - bottom if min(bbox_width, bbox_height)**2 > size_limit.x**2 + size_limit.y**2: return False if (bbox_width > size_limit.x or bbox_height > size_limit.y) and (bbox_height > size_limit.x or bbox_width > size_limit.y): @@ -992,7 +1010,7 @@ class Island: # for the time being, just throw this piece away return False - distance_limit = edge.vect.length_squared * epsilon + distance_limit = edge.vector.length_squared * epsilon # try and merge UVVertices closer than sqrt(distance_limit) merged_uvedges = set() merged_uvedge_pairs = list() @@ -1036,11 +1054,12 @@ class Island: if uvedge_b not in merged_uvedges: raise UnfoldError("Export failed. Please report this error, including the model if you can.") - boundary_other = [PhantomUVEdge(phantoms[uvedge.va], phantoms[uvedge.vb], flipped ^ uvedge.uvface.flipped) + boundary_other = [ + PhantomUVEdge(phantoms[uvedge.va], phantoms[uvedge.vb], flipped ^ uvedge.uvface.flipped) for uvedge in other.boundary if uvedge not in merged_uvedges] # TODO: if is_merged_mine, it might make sense to create a similar list from self.boundary as well - incidence = {vertex.tup for vertex in phantoms.values()}.intersection(vertex.tup for vertex in self.verts) + incidence = {vertex.tup for vertex in phantoms.values()}.intersection(vertex.tup for vertex in self.vertices) incidence = {position: list() for position in incidence} # from now on, 'incidence' is a dict for uvedge in chain(boundary_other, self.boundary): if uvedge.va.co == uvedge.vb.co: @@ -1078,7 +1097,7 @@ class Island: uvedge.edge.is_main_cut = False # include all trasformed vertices as mine - self.verts.update(phantoms.values()) + self.vertices.update(phantoms.values()) # update the uvverts_by_id dictionary for source, target in phantoms.items(): @@ -1106,21 +1125,24 @@ class Island: for uvface in other.faces: uvface.island = self - uvface.verts = [phantoms[uvvertex] for uvvertex in uvface.verts] - uvface.uvvertex_by_id = {index: phantoms[uvvertex] + uvface.vertices = [phantoms[uvvertex] for uvvertex in uvface.vertices] + uvface.uvvertex_by_id = { + index: phantoms[uvvertex] for index, uvvertex in uvface.uvvertex_by_id.items()} uvface.flipped ^= flipped if is_merged_mine: # there may be own uvvertices that need to be replaced by phantoms for uvface in self.faces: - if any(uvvertex in phantoms for uvvertex in uvface.verts): - uvface.verts = [phantoms.get(uvvertex, uvvertex) for uvvertex in uvface.verts] - uvface.uvvertex_by_id = {index: phantoms.get(uvvertex, uvvertex) + if any(uvvertex in phantoms for uvvertex in uvface.vertices): + uvface.vertices = [phantoms.get(uvvertex, uvvertex) for uvvertex in uvface.vertices] + uvface.uvvertex_by_id = { + index: phantoms.get(uvvertex, uvvertex) for index, uvvertex in uvface.uvvertex_by_id.items()} self.faces.extend(other.faces) - self.boundary = [uvedge for uvedge in - chain(self.boundary, other.boundary) if uvedge not in merged_uvedges] + self.boundary = [ + uvedge for uvedge in chain(self.boundary, other.boundary) + if uvedge not in merged_uvedges] for uvedge, partner in merged_uvedge_pairs: # make sure that main faces are the ones actually merged (this changes nothing in most cases) @@ -1130,7 +1152,7 @@ class Island: return True def add_marker(self, marker): - self.fake_verts.extend(marker.bounds) + self.fake_vertices.extend(marker.bounds) self.markers.append(marker) def generate_label(self, label=None, abbreviation=None): @@ -1148,7 +1170,7 @@ class Island: page_size: size of the page in pixels (vector)""" texface = tex.data for uvface in self.faces: - for i, uvvertex in enumerate(uvface.verts): + for i, uvvertex in enumerate(uvface.vertices): uv = uvvertex.co + self.pos texface[uvface.face.loop_start + i].uv[0] = uv.x / cage_size.x texface[uvface.face.loop_start + i].uv[1] = uv.y / cage_size.y @@ -1160,7 +1182,7 @@ class Island: texface = tex.data scale_x, scale_y = 1 / self.bounding_box.x, 1 / self.bounding_box.y for uvface in self.faces: - for i, uvvertex in enumerate(uvface.verts): + for i, uvvertex in enumerate(uvface.vertices): texface[uvface.face.loop_start + i].uv[0] = uvvertex.co.x * scale_x texface[uvface.face.loop_start + i].uv[1] = uvvertex.co.y * scale_y @@ -1240,32 +1262,35 @@ class PhantomUVEdge: class UVFace: """Face in 2D""" - __slots__ = ('verts', 'edges', 'face', 'island', 'flipped', 'uvvertex_by_id') + __slots__ = ('vertices', 'edges', 'face', 'island', 'flipped', 'uvvertex_by_id') def __init__(self, face: Face, island: Island): """Creace an UVFace from a Face and a fixed edge. face: Face to take coordinates from island: Island to register itself in fixed_edge: Edge to connect to (that already has UV coordinates)""" - self.verts = list() + self.vertices = list() self.face = face face.uvface = self self.island = island self.flipped = False # a flipped UVFace has edges clockwise rot = z_up_matrix(face.normal) - self.uvvertex_by_id = {vertex.index: UVVertex(rot * vertex.co, vertex) for vertex in face.verts} - self.verts = [self.uvvertex_by_id[vertex.index] for vertex in face.verts] + self.uvvertex_by_id = dict() # link vertex id -> UVVertex + for vertex in face.vertices: + uvvertex = UVVertex(rot * vertex.co, vertex) + self.vertices.append(uvvertex) + self.uvvertex_by_id[vertex.index] = uvvertex self.edges = list() edge_by_verts = dict() for edge in face.edges: edge_by_verts[(edge.va.index, edge.vb.index)] = edge edge_by_verts[(edge.vb.index, edge.va.index)] = edge - for va, vb in pairs(self.verts): + for va, vb in pairs(self.vertices): edge = edge_by_verts[(va.vertex.index, vb.vertex.index)] uvedge = UVEdge(va, vb, island, self, edge) self.edges.append(uvedge) - edge.uvedges.append(uvedge) #FIXME: editing foreign attribute + edge.uvedges.append(uvedge) # FIXME: editing foreign attribute class Arrow: @@ -1277,10 +1302,10 @@ class Arrow: edge = (uvedge.vb.co - uvedge.va.co) if not uvedge.uvface.flipped else (uvedge.va.co - uvedge.vb.co) self.center = (uvedge.va.co + uvedge.vb.co) / 2 self.size = size - sin, cos = edge.y / edge.length, edge.x / edge.length - self.rot = M.Matrix(((cos, -sin), (sin, cos))) tangent = edge.normalized() - normal = M.Vector((tangent.y, -tangent.x)) + cos, sin = tangent + self.rot = M.Matrix(((cos, -sin), (sin, cos))) + normal = M.Vector((sin, -cos)) self.bounds = [self.center, self.center + (1.2*normal + tangent)*size, self.center + (1.2*normal - tangent)*size] @@ -1324,14 +1349,14 @@ class Sticker: # Calculate the lengths of the glue tab edges using the possibly smaller angles sin_a = abs(1 - cos_a**2)**0.5 - len_b = min(len_a, (edge.length*sin_a) / (sin_a*cos_b + sin_b*cos_a)) + len_b = min(len_a, (edge.length * sin_a) / (sin_a * cos_b + sin_b * cos_a)) len_a = 0 if sin_a == 0 else min(sticker_width / sin_a, (edge.length - len_b*cos_b) / cos_a) sin_b = abs(1 - cos_b**2)**0.5 - len_a = min(len_a, (edge.length*sin_b) / (sin_a*cos_b + sin_b*cos_a)) - len_b = 0 if sin_b == 0 else min(sticker_width / sin_b, (edge.length - len_a*cos_a) / cos_b) + len_a = min(len_a, (edge.length * sin_b) / (sin_a * cos_b + sin_b * cos_a)) + len_b = 0 if sin_b == 0 else min(sticker_width / sin_b, (edge.length - len_a * cos_a) / cos_b) - v3 = UVVertex(second_vertex.co + M.Matrix(((cos_b, -sin_b), (sin_b, cos_b))) * edge *len_b / edge.length) + v3 = UVVertex(second_vertex.co + M.Matrix(((cos_b, -sin_b), (sin_b, cos_b))) * edge * len_b / edge.length) v4 = UVVertex(first_vertex.co + M.Matrix(((-cos_a, -sin_a), (sin_a, -cos_a))) * edge * len_a / edge.length) if v3.co != v4.co: self.vertices = [second_vertex, v3, v4, first_vertex] @@ -1400,7 +1425,9 @@ class SVG: rows = "\n".join dl = ["{:.2f}".format(length * self.style.line_width * 1000) for length in (2, 5, 10)] - format_style = {'SOLID': "none", 'DOT': "{0},{1}".format(*dl), 'DASH': "{1},{2}".format(*dl), 'LONGDASH': "{2},{1}".format(*dl), 'DASHDOT': "{2},{1},{0},{1}".format(*dl)} + format_style = { + 'SOLID': "none", 'DOT': "{0},{1}".format(*dl), 'DASH': "{1},{2}".format(*dl), + 'LONGDASH': "{2},{1}".format(*dl), 'DASHDOT': "{2},{1},{0},{1}".format(*dl)} def format_color(vec): return "#{:02x}{:02x}{:02x}".format(round(vec[0] * 255), round(vec[1] * 255), round(vec[2] * 255)) @@ -1415,18 +1442,22 @@ class SVG: string = string.replace(os_path.sep, '/') return string - styleargs = {name: format_color(getattr(self.style, name)) for name in - ("outer_color", "outbg_color", "convex_color", "concave_color", "freestyle_color", - "inbg_color", "sticker_fill", "text_color")} - styleargs.update({name: format_style[getattr(self.style, name)] for name in + styleargs = { + name: format_color(getattr(self.style, name)) for name in ( + "outer_color", "outbg_color", "convex_color", "concave_color", "freestyle_color", + "inbg_color", "sticker_fill", "text_color")} + styleargs.update({ + name: format_style[getattr(self.style, name)] for name in ("outer_style", "convex_style", "concave_style", "freestyle_style")}) - styleargs.update({name: getattr(self.style, attr)[3] for name, attr in - (("outer_alpha", "outer_color"), ("outbg_alpha", "outbg_color"), - ("convex_alpha", "convex_color"), ("concave_alpha", "concave_color"), - ("freestyle_alpha", "freestyle_color"), - ("inbg_alpha", "inbg_color"), ("sticker_alpha", "sticker_fill"), - ("text_alpha", "text_color"))}) - styleargs.update({name: getattr(self.style, name) * self.style.line_width * 1000 for name in + styleargs.update({ + name: getattr(self.style, attr)[3] for name, attr in ( + ("outer_alpha", "outer_color"), ("outbg_alpha", "outbg_color"), + ("convex_alpha", "convex_color"), ("concave_alpha", "concave_color"), + ("freestyle_alpha", "freestyle_color"), + ("inbg_alpha", "inbg_color"), ("sticker_alpha", "sticker_fill"), + ("text_alpha", "text_color"))}) + styleargs.update({ + name: getattr(self.style, name) * self.style.line_width * 1000 for name in ("outer_width", "convex_width", "concave_width", "freestyle_width", "outbg_width", "inbg_width")}) for num, page in enumerate(mesh.pages): page_filename = "{}_{}.svg".format(filename[:filename.rfind(".svg")], page.name) if len(mesh.pages) > 1 else filename @@ -1434,11 +1465,12 @@ class SVG: print(self.svg_base.format(width=self.page_size.x*1000, height=self.page_size.y*1000), file=f) print(self.css_base.format(**styleargs), file=f) if page.image_path: - print(self.image_linked_tag.format( - pos="{0:.6f} {0:.6f}".format(self.margin*1000), - width=(self.page_size.x - 2 * self.margin)*1000, - height=(self.page_size.y - 2 * self.margin)*1000, - path=path_convert(page.image_path)), + print( + self.image_linked_tag.format( + pos="{0:.6f} {0:.6f}".format(self.margin*1000), + width=(self.page_size.x - 2 * self.margin)*1000, + height=(self.page_size.y - 2 * self.margin)*1000, + path=path_convert(page.image_path)), file=f) if len(page.islands) > 1: print("", file=f) @@ -1446,14 +1478,16 @@ class SVG: for island in page.islands: print("", file=f) if island.image_path: - print(self.image_linked_tag.format( - pos=self.format_vertex(island.pos + M.Vector((0, island.bounding_box.y))), - width=island.bounding_box.x*1000, - height=island.bounding_box.y*1000, - path=path_convert(island.image_path)), + print( + self.image_linked_tag.format( + pos=self.format_vertex(island.pos + M.Vector((0, island.bounding_box.y))), + width=island.bounding_box.x*1000, + height=island.bounding_box.y*1000, + path=path_convert(island.image_path)), file=f) elif island.embedded_image: - print(self.image_embedded_tag.format( + print( + self.image_embedded_tag.format( pos=self.format_vertex(island.pos + M.Vector((0, island.bounding_box.y))), width=island.bounding_box.x*1000, height=island.bounding_box.y*1000, @@ -1461,11 +1495,13 @@ class SVG: island.embedded_image, "'/>", file=f, sep="") if island.title: - print(self.text_tag.format( - size=1000 * self.text_size, - x=1000 * (island.bounding_box.x*0.5 + island.pos.x + self.margin), - y=1000 * (self.page_size.y - island.pos.y - self.margin - 0.2 * self.text_size), - label=island.title), file=f) + print( + self.text_tag.format( + size=1000 * self.text_size, + x=1000 * (island.bounding_box.x*0.5 + island.pos.x + self.margin), + y=1000 * (self.page_size.y - island.pos.y - self.margin - 0.2 * self.text_size), + label=island.title), + file=f) data_markers, data_stickerfill, data_outer, data_convex, data_concave, data_freestyle = (list() for i in range(6)) for marker in island.markers: @@ -1630,6 +1666,12 @@ class PDF: """Simple PDF exporter""" mm_to_pt = 72 / 25.4 + character_width_packed = { + 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·ÌÍÎÏìíîï', + 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¿ßø', + 667: '&ABEKPSVXY\x8a\x9fÀÁÂÃÄÅÈÉÊËÝÞ', 722: 'CDHNRUwÇÐÑÙÚÛÜ', 737: '©®', 778: 'GOQÒÓÔÕÖØ', 833: 'Mm¼½¾', 889: '%æ', 944: 'W\x9c', 1000: '\x85\x89\x8c\x97\x99Æ', 1015: '@', } + character_width = {c: value for (value, chars) in character_width_packed.items() for c in chars} + def __init__(self, page_size: M.Vector, style, margin, pure_net=True, angle_epsilon=0.01): self.page_size = page_size self.style = style @@ -1637,18 +1679,18 @@ class PDF: self.pure_net = pure_net self.angle_epsilon = angle_epsilon - character_width_packed = {833: 'mM', 834: '¼½¾', 260: '¦|', 389: '*', 584: '>~+¬±<×÷=', 778: 'ÒGÖÕQÔØÓO', 333: '¹\xad\x98\x84²¨\x94\x9b¯¡´()\x8b\x93¸³-\x88`r', 334: '{}', 400: '°', 722: 'DÛÚUÑwRÐÜCÇNÙH', 611: '¿øTßZF\x8e', 469: '^', 278: 'ì\x05\x06 ;\x01/\x08I\x07,\x13\x11\x04\\.![\x15\r\x10:\x18]\x0c\x00\x1bÍf\xa0\x14\x1c\n\t\x1e\x1dïí\x12·\x16\x0bî\x0e\x03tÏ\x17\x1fÎ\x19\x0f\x02Ì\x1a', 537: '¶', 667: 'ÄË\x8aÃÀBÊVX&AKSÈÞPÁYÉ\x9fÝEÅÂ', 222: 'jl\x92\x91i\x82', 737: '©®', 355: '"', 1000: '\x89\x97\x8c\x99\x85Æ', 556: 'éhòúd»§ùþ5\x803õ¢åëûa64_ã\x83ñ¤8n?g2e#9«oqL$âö1päuð\x86¥µ\x967üóê\x87bá0àèô£', 365: 'º', 944: '\x9cW', 370: 'ª', 500: 'Js\x9eçyÿ\x9aývckzx', 350: '\x90\x8d\x81\x8f\x95\x7f\x9d', 1015: '@', 889: 'æ%', 191: "'"} - character_width = {c: value for (value, chars) in character_width_packed.items() for c in chars} def text_width(self, text, scale=None): return (scale or self.text_size) * sum(self.character_width.get(c, 556) for c in text) / 1000 @classmethod def encode_image(cls, bpy_image): data = bytes(int(255 * px) for (i, px) in enumerate(bpy_image.pixels) if i % 4 != 3) - image = {"Type": "XObject", "Subtype": "Image", "Width": bpy_image.size[0], "Height": bpy_image.size[1], "ColorSpace": "DeviceRGB", "BitsPerComponent": 8, "Interpolate": True, "Filter": ["ASCII85Decode", "FlateDecode"], "stream": data} + image = { + "Type": "XObject", "Subtype": "Image", "Width": bpy_image.size[0], "Height": bpy_image.size[1], + "ColorSpace": "DeviceRGB", "BitsPerComponent": 8, "Interpolate": True, + "Filter": ["ASCII85Decode", "FlateDecode"], "stream": data} return image - def write(self, mesh, filename): def format_dict(obj, refs=tuple()): return "<< " + "".join("/{} {}\n".format(key, format_value(value, refs)) for (key, value) in obj.items()) + ">>" @@ -1700,10 +1742,14 @@ class PDF: page_size_pt = 1000 * self.mm_to_pt * self.page_size root = {"Type": "Pages", "MediaBox": [0, 0, page_size_pt.x, page_size_pt.y], "Kids": list()} catalog = {"Type": "Catalog", "Pages": root} - font = {"Type": "Font", "Subtype": "Type1", "Name": "F1", "BaseFont": "Helvetica", "Encoding": "MacRomanEncoding"} + font = { + "Type": "Font", "Subtype": "Type1", "Name": "F1", + "BaseFont": "Helvetica", "Encoding": "MacRomanEncoding"} dl = [length * self.style.line_width * 1000 for length in (1, 4, 9)] - format_style = {'SOLID': list(), 'DOT': [dl[0], dl[1]], 'DASH': [dl[1], dl[2]], 'LONGDASH': [dl[2], dl[1]], 'DASHDOT': [dl[2], dl[1], dl[0], dl[1]]} + format_style = { + 'SOLID': list(), 'DOT': [dl[0], dl[1]], 'DASH': [dl[1], dl[2]], + 'LONGDASH': [dl[2], dl[1]], 'DASHDOT': [dl[2], dl[1], dl[0], dl[1]]} styles = { "Gtext": {"ca": self.style.text_color[3], "Font": [font, 1000 * self.text_size]}, "Gsticker": {"ca": self.style.sticker_fill[3]}} @@ -1730,12 +1776,12 @@ class PDF: commands.append("q 1 0 0 1 {0.x:.6f} {0.y:.6f} cm".format(1000*(self.margin + island.pos))) if island.embedded_image: identifier = "Im{}".format(len(resources["XObject"]) + 1) - commands.append("q {0.x:.6f} 0 0 {0.y:.6f} 0 0 cm 1 0 0 -1 0 1 cm /{1} Do Q".format(1000 * island.bounding_box, identifier)) + commands.append(self.command_image.format(1000 * island.bounding_box, identifier)) objects.append(island.embedded_image) resources["XObject"][identifier] = island.embedded_image if island.title: - commands.append("/Gtext gs BT {x:.6f} {y:.6f} Td ({label}) Tj ET".format( + commands.append(self.command_label.format( size=1000*self.text_size, x=500 * (island.bounding_box.x - self.text_width(island.title)), y=1000 * 0.2 * self.text_size, @@ -1746,7 +1792,7 @@ class PDF: if isinstance(marker, Sticker): data_stickerfill.append(line_through(marker.vertices) + "f") if marker.text: - data_markers.append("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".format( + data_markers.append(self.command_sticker.format( label=marker.text, pos=1000*marker.center, mat=marker.rot, @@ -1755,17 +1801,17 @@ class PDF: elif isinstance(marker, Arrow): size = 1000 * marker.size position = 1000 * (marker.center + marker.rot*marker.size*M.Vector((0, -0.9))) - data_markers.append("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".format( + data_markers.append(self.command_arrow.format( index=marker.text, arrow_pos=1000 * marker.center, pos=position - 1000 * M.Vector((0.5 * self.text_width(marker.text), 0.4 * self.text_size)), mat=size * marker.rot, size=size)) elif isinstance(marker, NumberAlone): - data_markers.append("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".format( + data_markers.append(self.command_number.format( label=marker.text, pos=1000*marker.center, - mat = marker.rot, + mat=marker.rot, size=1000*marker.size)) outer_edges = set(island.boundary) @@ -1846,6 +1892,12 @@ class PDF: f.write(format_dict({"Size": len(xref_table), "Root": catalog}, objects)) f.write("\nstartxref\n{}\n%%EOF\n".format(xref_pos)) + command_label = "/Gtext gs BT {x:.6f} {y:.6f} Td ({label}) Tj ET" + command_image = "q {0.x:.6f} 0 0 {0.y:.6f} 0 0 cm 1 0 0 -1 0 1 cm /{1} Do Q" + 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" + 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" + 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" + class Unfold(bpy.types.Operator): """Blender Operator: unfold the selected object.""" @@ -1854,18 +1906,18 @@ class Unfold(bpy.types.Operator): bl_label = "Unfold" bl_description = "Mark seams so that the mesh can be exported as a paper model" bl_options = {'REGISTER', 'UNDO'} - edit = bpy.props.BoolProperty(name="", description="", default=False, options={'HIDDEN'}) - priority_effect_convex = bpy.props.FloatProperty(name="Priority Convex", - description="Priority effect for edges in convex angles", + edit = bpy.props.BoolProperty(default=False, options={'HIDDEN'}) + priority_effect_convex = bpy.props.FloatProperty( + name="Priority Convex", description="Priority effect for edges in convex angles", default=default_priority_effect['CONVEX'], soft_min=-1, soft_max=10, subtype='FACTOR') - priority_effect_concave = bpy.props.FloatProperty(name="Priority Concave", - description="Priority effect for edges in concave angles", + priority_effect_concave = bpy.props.FloatProperty( + name="Priority Concave", description="Priority effect for edges in concave angles", default=default_priority_effect['CONCAVE'], soft_min=-1, soft_max=10, subtype='FACTOR') - priority_effect_length = bpy.props.FloatProperty(name="Priority Length", - description="Priority effect of edge length", + priority_effect_length = bpy.props.FloatProperty( + name="Priority Length", description="Priority effect of edge length", default=default_priority_effect['LENGTH'], soft_min=-10, soft_max=1, subtype='FACTOR') - do_create_uvmap = bpy.props.BoolProperty(name="Create UVMap", - description="Create a new UV Map showing the islands and page layout", default=False) + do_create_uvmap = bpy.props.BoolProperty( + name="Create UVMap", description="Create a new UV Map showing the islands and page layout", default=False) object = None @classmethod @@ -1895,10 +1947,15 @@ class Unfold(bpy.types.Operator): mesh = self.object.data cage_size = M.Vector((settings.output_size_x, settings.output_size_y)) if settings.limit_by_page else None - priority_effect = {'CONVEX': self.priority_effect_convex, 'CONCAVE': self.priority_effect_concave, 'LENGTH': self.priority_effect_length} + priority_effect = { + 'CONVEX': self.priority_effect_convex, + 'CONCAVE': self.priority_effect_concave, + 'LENGTH': self.priority_effect_length} try: unfolder = Unfolder(self.object) - unfolder.prepare(cage_size, self.do_create_uvmap, mark_seams=True, priority_effect=priority_effect, scale=sce.unit_settings.scale_length/settings.scale) + unfolder.prepare( + cage_size, self.do_create_uvmap, mark_seams=True, + priority_effect=priority_effect, scale=sce.unit_settings.scale_length/settings.scale) except UnfoldError as error: self.report(type={'ERROR_INVALID_INPUT'}, message=error.args[0]) bpy.ops.object.mode_set(mode=recall_mode) @@ -1919,7 +1976,9 @@ class Unfold(bpy.types.Operator): lface.id = uvface.face.index list_item["label"] = island.label - list_item["abbreviation"], list_item["auto_label"], list_item["auto_abbrev"] = attributes.get(island.label, (island.abbreviation, True, True)) + list_item["abbreviation"], list_item["auto_label"], list_item["auto_abbrev"] = attributes.get( + island.label, + (island.abbreviation, True, True)) island_item_changed(list_item, context) mesh.paper_island_index = -1 @@ -1954,6 +2013,8 @@ class ClearAllSeams(bpy.types.Operator): def page_size_preset_changed(self, context): """Update the actual document size to correct values""" + if hasattr(self, "limit_by_page") and not self.limit_by_page: + return if self.page_size_preset == 'A4': self.output_size_x = 0.210 self.output_size_y = 0.297 @@ -1976,70 +2037,70 @@ class PaperModelStyle(bpy.types.PropertyGroup): ('LONGDASH', "Long Dashes (-- --)", "Solid line"), ('DASHDOT', "Dash-dotted (-- .)", "Solid line") ] - outer_color = bpy.props.FloatVectorProperty(name="Outer Lines", - description="Color of net outline", + outer_color = bpy.props.FloatVectorProperty( + name="Outer Lines", description="Color of net outline", default=(0.0, 0.0, 0.0, 1.0), min=0, max=1, subtype='COLOR', size=4) - outer_style = bpy.props.EnumProperty(name="Outer Lines Drawing Style", - description="Drawing style of net outline", + outer_style = bpy.props.EnumProperty( + name="Outer Lines Drawing Style", description="Drawing style of net outline", default='SOLID', items=line_styles) - line_width = bpy.props.FloatProperty(name="Base Lines Thickness", - description="Base thickness of net lines, each actual value is a multiple of this length", + line_width = bpy.props.FloatProperty( + name="Base Lines Thickness", description="Base thickness of net lines, each actual value is a multiple of this length", default=1e-4, min=0, soft_max=5e-3, precision=5, step=1e-2, subtype="UNSIGNED", unit="LENGTH") - outer_width = bpy.props.FloatProperty(name="Outer Lines Thickness", - description="Relative thickness of net outline", + outer_width = bpy.props.FloatProperty( + name="Outer Lines Thickness", description="Relative thickness of net outline", default=3, min=0, soft_max=10, precision=1, step=10, subtype='FACTOR') - use_outbg = bpy.props.BoolProperty(name="Highlight Outer Lines", - description="Add another line below every line to improve contrast", + use_outbg = bpy.props.BoolProperty( + name="Highlight Outer Lines", description="Add another line below every line to improve contrast", default=True) - outbg_color = bpy.props.FloatVectorProperty(name="Outer Highlight", - description="Color of the highlight for outer lines", + outbg_color = bpy.props.FloatVectorProperty( + name="Outer Highlight", description="Color of the highlight for outer lines", default=(1.0, 1.0, 1.0, 1.0), min=0, max=1, subtype='COLOR', size=4) - outbg_width = bpy.props.FloatProperty(name="Outer Highlight Thickness", - description="Relative thickness of the highlighting lines", + outbg_width = bpy.props.FloatProperty( + name="Outer Highlight Thickness", description="Relative thickness of the highlighting lines", default=5, min=0, soft_max=10, precision=1, step=10, subtype='FACTOR') - convex_color = bpy.props.FloatVectorProperty(name="Inner Convex Lines", - description="Color of lines to be folded to a convex angle", + convex_color = bpy.props.FloatVectorProperty( + name="Inner Convex Lines", description="Color of lines to be folded to a convex angle", default=(0.0, 0.0, 0.0, 1.0), min=0, max=1, subtype='COLOR', size=4) - convex_style = bpy.props.EnumProperty(name="Convex Lines Drawing Style", - description="Drawing style of lines to be folded to a convex angle", + convex_style = bpy.props.EnumProperty( + name="Convex Lines Drawing Style", description="Drawing style of lines to be folded to a convex angle", default='DASH', items=line_styles) - convex_width = bpy.props.FloatProperty(name="Convex Lines Thickness", - description="Relative thickness of concave lines", + convex_width = bpy.props.FloatProperty( + name="Convex Lines Thickness", description="Relative thickness of concave lines", default=2, min=0, soft_max=10, precision=1, step=10, subtype='FACTOR') - concave_color = bpy.props.FloatVectorProperty(name="Inner Concave Lines", - description="Color of lines to be folded to a concave angle", + concave_color = bpy.props.FloatVectorProperty( + name="Inner Concave Lines", description="Color of lines to be folded to a concave angle", default=(0.0, 0.0, 0.0, 1.0), min=0, max=1, subtype='COLOR', size=4) - concave_style = bpy.props.EnumProperty(name="Concave Lines Drawing Style", - description="Drawing style of lines to be folded to a concave angle", + concave_style = bpy.props.EnumProperty( + name="Concave Lines Drawing Style", description="Drawing style of lines to be folded to a concave angle", default='DASHDOT', items=line_styles) - concave_width = bpy.props.FloatProperty(name="Concave Lines Thickness", - description="Relative thickness of concave lines", + concave_width = bpy.props.FloatProperty( + name="Concave Lines Thickness", description="Relative thickness of concave lines", default=2, min=0, soft_max=10, precision=1, step=10, subtype='FACTOR') - freestyle_color = bpy.props.FloatVectorProperty(name="Freestyle Edges", - description="Color of lines marked as Freestyle Edge", + freestyle_color = bpy.props.FloatVectorProperty( + name="Freestyle Edges", description="Color of lines marked as Freestyle Edge", default=(0.0, 0.0, 0.0, 1.0), min=0, max=1, subtype='COLOR', size=4) - freestyle_style = bpy.props.EnumProperty(name="Freestyle Edges Drawing Style", - description="Drawing style of Freestyle Edges", + freestyle_style = bpy.props.EnumProperty( + name="Freestyle Edges Drawing Style", description="Drawing style of Freestyle Edges", default='SOLID', items=line_styles) - freestyle_width = bpy.props.FloatProperty(name="Freestyle Edges Thickness", - description="Relative thickness of Freestyle edges", + freestyle_width = bpy.props.FloatProperty( + name="Freestyle Edges Thickness", description="Relative thickness of Freestyle edges", default=2, min=0, soft_max=10, precision=1, step=10, subtype='FACTOR') - use_inbg = bpy.props.BoolProperty(name="Highlight Inner Lines", - description="Add another line below every line to improve contrast", + use_inbg = bpy.props.BoolProperty( + name="Highlight Inner Lines", description="Add another line below every line to improve contrast", default=True) - inbg_color = bpy.props.FloatVectorProperty(name="Inner Highlight", - description="Color of the highlight for inner lines", + inbg_color = bpy.props.FloatVectorProperty( + name="Inner Highlight", description="Color of the highlight for inner lines", default=(1.0, 1.0, 1.0, 1.0), min=0, max=1, subtype='COLOR', size=4) - inbg_width = bpy.props.FloatProperty(name="Inner Highlight Thickness", - description="Relative thickness of the highlighting lines", + inbg_width = bpy.props.FloatProperty( + name="Inner Highlight Thickness", description="Relative thickness of the highlighting lines", default=2, min=0, soft_max=10, precision=1, step=10, subtype='FACTOR') - sticker_fill = bpy.props.FloatVectorProperty(name="Tabs Fill", - description="Fill color of sticking tabs", + sticker_fill = bpy.props.FloatVectorProperty( + name="Tabs Fill", description="Fill color of sticking tabs", default=(0.9, 0.9, 0.9, 1.0), min=0, max=1, subtype='COLOR', size=4) - text_color = bpy.props.FloatVectorProperty(name="Text Color", - description="Color of all text used in the document", + text_color = bpy.props.FloatVectorProperty( + name="Text Color", description="Color of all text used in the document", default=(0.0, 0.0, 0.0, 1.0), min=0, max=1, subtype='COLOR', size=4) bpy.utils.register_class(PaperModelStyle) @@ -2047,43 +2108,29 @@ bpy.utils.register_class(PaperModelStyle) class ExportPaperModel(bpy.types.Operator): """Blender Operator: save the selected object's net and optionally bake its texture""" - def scaled_getter(name): - return lambda self: self[name] / bpy.context.scene.unit_settings.scale_length - - def scaled_setter(name): - def setter(self, value): - self[name] = value * bpy.context.scene.unit_settings.scale_length - return setter - bl_idname = "export_mesh.paper_model" bl_label = "Export Paper Model" bl_description = "Export the selected object's net and optionally bake its texture" - filepath = bpy.props.StringProperty(name="File Path", - description="Target file to save the SVG", options={'SKIP_SAVE'}) - filename = bpy.props.StringProperty(name="File Name", - description="Name of the file", options={'SKIP_SAVE'}) - directory = bpy.props.StringProperty(name="Directory", - description="Directory of the file", options={'SKIP_SAVE'}) - page_size_preset = bpy.props.EnumProperty(name="Page Size", - description="Size of the exported document", - default='A4', update=page_size_preset_changed, items=[ - ('USER', "User defined", "User defined paper size"), - ('A4', "A4", "International standard paper size"), - ('A3', "A3", "International standard paper size"), - ('US_LETTER', "Letter", "North American paper size"), - ('US_LEGAL', "Legal", "North American paper size") - ]) - output_size_x = bpy.props.FloatProperty(name="Page Width", - description="Width of the exported document", + filepath = bpy.props.StringProperty( + name="File Path", description="Target file to save the SVG", options={'SKIP_SAVE'}) + filename = bpy.props.StringProperty( + name="File Name", description="Name of the file", options={'SKIP_SAVE'}) + directory = bpy.props.StringProperty( + name="Directory", description="Directory of the file", options={'SKIP_SAVE'}) + page_size_preset = bpy.props.EnumProperty( + name="Page Size", description="Size of the exported document", + default='A4', update=page_size_preset_changed, items=global_paper_sizes) + output_size_x = bpy.props.FloatProperty( + name="Page Width", description="Width of the exported document", default=0.210, soft_min=0.105, soft_max=0.841, subtype="UNSIGNED", unit="LENGTH") - output_size_y = bpy.props.FloatProperty(name="Page Height", - description="Height of the exported document", + output_size_y = bpy.props.FloatProperty( + name="Page Height", description="Height of the exported document", default=0.297, soft_min=0.148, soft_max=1.189, subtype="UNSIGNED", unit="LENGTH") - output_margin = bpy.props.FloatProperty(name="Page Margin", - description="Distance from page borders to the printable area", - default=0.005, min=0, soft_max=0.1, step=0.1, subtype="DISTANCE", unit="LENGTH", get=scaled_getter("output_margin"), set=scaled_setter("output_margin")) - output_type = bpy.props.EnumProperty(name="Textures", - description="Source of a texture for the model", + output_margin = bpy.props.FloatProperty( + name="Page Margin", description="Distance from page borders to the printable area", + default=0.005, min=0, soft_max=0.1, step=0.1, subtype="UNSIGNED", unit="LENGTH") + output_type = bpy.props.EnumProperty( + name="Textures", description="Source of a texture for the model", default='NONE', items=[ ('NONE', "No Texture", "Export the net only"), ('TEXTURE', "From Materials", "Render the diffuse color and all painted textures"), @@ -2091,45 +2138,45 @@ class ExportPaperModel(bpy.types.Operator): ('RENDER', "Full Render", "Render the material in actual scene illumination"), ('SELECTED_TO_ACTIVE', "Selected to Active", "Render all selected surrounding objects as a texture") ]) - do_create_stickers = bpy.props.BoolProperty(name="Create Tabs", - description="Create gluing tabs around the net (useful for paper)", + do_create_stickers = bpy.props.BoolProperty( + name="Create Tabs", description="Create gluing tabs around the net (useful for paper)", default=True) - do_create_numbers = bpy.props.BoolProperty(name="Create Numbers", - description="Enumerate edges to make it clear which edges should be sticked together", + do_create_numbers = bpy.props.BoolProperty( + name="Create Numbers", description="Enumerate edges to make it clear which edges should be sticked together", default=True) - sticker_width = bpy.props.FloatProperty(name="Tabs and Text Size", - description="Width of gluing tabs and their numbers", + sticker_width = bpy.props.FloatProperty( + name="Tabs and Text Size", description="Width of gluing tabs and their numbers", default=0.005, soft_min=0, soft_max=0.05, step=0.1, subtype="UNSIGNED", unit="LENGTH") - angle_epsilon = bpy.props.FloatProperty(name="Hidden Edge Angle", - description="Folds with angle below this limit will not be drawn", + angle_epsilon = bpy.props.FloatProperty( + name="Hidden Edge Angle", description="Folds with angle below this limit will not be drawn", default=pi/360, min=0, soft_max=pi/4, step=0.01, subtype="ANGLE", unit="ROTATION") - output_dpi = bpy.props.FloatProperty(name="Resolution (DPI)", - description="Resolution of images in pixels per inch", + output_dpi = bpy.props.FloatProperty( + name="Resolution (DPI)", description="Resolution of images in pixels per inch", default=90, min=1, soft_min=30, soft_max=600, subtype="UNSIGNED") - file_format = bpy.props.EnumProperty(name="Document Format", - description="File format of the exported net", + file_format = bpy.props.EnumProperty( + name="Document Format", description="File format of the exported net", default='PDF', items=[ ('PDF', "PDF", "Adobe Portable Document Format 1.4"), ('SVG', "SVG", "W3C Scalable Vector Graphics"), ]) - image_packing = bpy.props.EnumProperty(name="Image Packing Method", - description="Method of attaching baked image(s) to the SVG", + image_packing = bpy.props.EnumProperty( + name="Image Packing Method", description="Method of attaching baked image(s) to the SVG", default='ISLAND_EMBED', items=[ ('PAGE_LINK', "Single Linked", "Bake one image per page of output and save it separately"), ('ISLAND_LINK', "Linked", "Bake images separately for each island and save them in a directory"), ('ISLAND_EMBED', "Embedded", "Bake images separately for each island and embed them into the SVG") ]) - scale = bpy.props.FloatProperty(name="Scale", - description="Divisor of all dimensions when exporting", + scale = bpy.props.FloatProperty( + name="Scale", description="Divisor of all dimensions when exporting", default=1, soft_min=1.0, soft_max=10000.0, step=100, subtype='UNSIGNED', precision=1) - do_create_uvmap = bpy.props.BoolProperty(name="Create UVMap", - description="Create a new UV Map showing the islands and page layout", + do_create_uvmap = bpy.props.BoolProperty( + name="Create UVMap", description="Create a new UV Map showing the islands and page layout", default=False, options={'SKIP_SAVE'}) - ui_expanded_document = bpy.props.BoolProperty(name="Show Document Settings Expanded", - description="Shows the box 'Document Settings' expanded in user interface", + ui_expanded_document = bpy.props.BoolProperty( + name="Show Document Settings Expanded", description="Shows the box 'Document Settings' expanded in user interface", default=True, options={'SKIP_SAVE'}) - ui_expanded_style = bpy.props.BoolProperty(name="Show Style Settings Expanded", - description="Shows the box 'Colors and Style' expanded in user interface", + ui_expanded_style = bpy.props.BoolProperty( + name="Show Style Settings Expanded", description="Shows the box 'Colors and Style' expanded in user interface", default=False, options={'SKIP_SAVE'}) style = bpy.props.PointerProperty(type=PaperModelStyle) @@ -2150,8 +2197,6 @@ class ExportPaperModel(bpy.types.Operator): except UnfoldError as error: self.report(type={'ERROR_INVALID_INPUT'}, message=error.args[0]) return {'CANCELLED'} - except: - raise def get_scale_ratio(self, sce): margin = self.output_margin + self.sticker_width + 1e-5 @@ -2171,7 +2216,9 @@ class ExportPaperModel(bpy.types.Operator): 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 try: self.unfolder = Unfolder(self.object) - self.unfolder.prepare(cage_size, create_uvmap=self.do_create_uvmap, scale=sce.unit_settings.scale_length/self.scale) + self.unfolder.prepare( + cage_size, create_uvmap=self.do_create_uvmap, + scale=sce.unit_settings.scale_length/self.scale) except UnfoldError as error: self.report(type={'ERROR_INVALID_INPUT'}, message=error.args[0]) bpy.ops.object.mode_set(mode=recall_mode) @@ -2199,13 +2246,20 @@ class ExportPaperModel(bpy.types.Operator): layout.prop(self.properties, "scale", text="Scale: 1") scale_ratio = self.get_scale_ratio(context.scene) if scale_ratio > 1: - layout.label(text="An island is roughly {:.1f}x bigger than page".format(scale_ratio), icon="ERROR") + layout.label( + text="An island is roughly {:.1f}x bigger than page".format(scale_ratio), + icon="ERROR") elif scale_ratio > 0: layout.label(text="Largest island is roughly 1/{:.1f} of page".format(1 / scale_ratio)) + if context.scene.unit_settings.scale_length != 1: + layout.label( + text="Unit scale {:.1f} makes page size etc. not display correctly".format( + context.scene.unit_settings.scale_length), icon="ERROR") box = layout.box() row = box.row(align=True) - row.prop(self.properties, "ui_expanded_document", text="", + row.prop( + self.properties, "ui_expanded_document", text="", icon=('TRIA_DOWN' if self.ui_expanded_document else 'TRIA_RIGHT'), emboss=False) row.label(text="Document Settings") @@ -2239,7 +2293,8 @@ class ExportPaperModel(bpy.types.Operator): box = layout.box() row = box.row(align=True) - row.prop(self.properties, "ui_expanded_style", text="", + row.prop( + self.properties, "ui_expanded_style", text="", icon=('TRIA_DOWN' if self.ui_expanded_style else 'TRIA_RIGHT'), emboss=False) row.label(text="Colors and Style") @@ -2305,7 +2360,8 @@ class AddPresetPaperModel(bl_operators.presets.AddPresetBase, bpy.types.Operator op = bpy.ops.export_mesh.paper_model properties = op.get_rna().bl_rna.properties.items() blacklist = bpy.types.Operator.bl_rna.properties.keys() - return ["op.{}".format(prop_id) for (prop_id, prop) in properties + return [ + "op.{}".format(prop_id) for (prop_id, prop) in properties if not (prop.is_hidden or prop.is_skip_save or prop_id in blacklist)] @@ -2334,14 +2390,17 @@ class VIEW3D_PT_paper_model_tools(bpy.types.Panel): else: layout.operator("mesh.clear_all_seams") - layout.prop(sce.paper_model, "scale", text="Model Scale: 1") + props = sce.paper_model + layout.prop(props, "scale", text="Model Scale: 1") - col = layout.column(align=True) - col.prop(sce.paper_model, "limit_by_page") + layout.prop(props, "limit_by_page") + col = layout.column() + col.active = props.limit_by_page + col.prop(props, "page_size_preset") sub = col.column(align=True) - sub.active = sce.paper_model.limit_by_page - sub.prop(sce.paper_model, "output_size_x") - sub.prop(sce.paper_model, "output_size_y") + sub.active = props.page_size_preset == 'USER' + sub.prop(props, "output_size_x") + sub.prop(props, "output_size_y") class VIEW3D_PT_paper_model_islands(bpy.types.Panel): @@ -2357,9 +2416,11 @@ class VIEW3D_PT_paper_model_islands(bpy.types.Panel): mesh = obj.data if obj and obj.type == 'MESH' else None if mesh and mesh.paper_island_list: - layout.label(text="1 island:" if len(mesh.paper_island_list) == 1 else + layout.label( + text="1 island:" if len(mesh.paper_island_list) == 1 else "{} islands:".format(len(mesh.paper_island_list))) - layout.template_list('UI_UL_list', 'paper_model_island_list', mesh, + layout.template_list( + 'UI_UL_list', 'paper_model_island_list', mesh, 'paper_island_list', mesh, 'paper_island_index', rows=1, maxrows=5) if mesh.paper_island_index >= 0: list_item = mesh.paper_island_list[mesh.paper_island_index] @@ -2421,7 +2482,8 @@ def display_islands_changed(self, context): """Switch highlighting islands on/off""" if self.display_islands: if not display_islands.handle: - display_islands.handle = bpy.types.SpaceView3D.draw_handler_add(display_islands, (self, context), 'WINDOW', 'POST_VIEW') + display_islands.handle = bpy.types.SpaceView3D.draw_handler_add( + display_islands, (self, context), 'WINDOW', 'POST_VIEW') else: if display_islands.handle: bpy.types.SpaceView3D.draw_handler_remove(display_islands.handle, 'WINDOW') @@ -2437,6 +2499,13 @@ def label_changed(self, context): def island_item_changed(self, context): """The labelling of an island was changed""" + def increment(abbrev, collisions): + letters = "ABCDEFGHIJKLMNPQRSTUVWXYZ123456789" + while abbrev in collisions: + abbrev = abbrev.rstrip(letters[-1]) + abbrev = abbrev[:2] + letters[letters.find(abbrev[-1]) + 1 if len(abbrev) == 3 else 0] + return abbrev + # accessing properties via [..] to avoid a recursive call after the update island_list = context.active_object.data.paper_island_list if self.auto_label: @@ -2446,51 +2515,59 @@ def island_item_changed(self, context): number += 1 self["label"] = "Island {}".format(number) if self.auto_abbrev: - self["abbreviation"] = "".join(first_letters(self.label))[:3].upper() + self["abbreviation"] = "" # avoid self-conflict + abbrev = "".join(first_letters(self.label))[:3].upper() + self["abbreviation"] = increment(abbrev, {item.abbreviation for item in island_list}) elif len(self.abbreviation) > 3: self["abbreviation"] = self.abbreviation[:3] - self.name = "[{}] {} ({} {})".format(self.abbreviation, self.label, len(self.faces), "faces" if len(self.faces) > 1 else "face") + self.name = "[{}] {} ({} {})".format( + self.abbreviation, self.label, len(self.faces), "faces" if len(self.faces) > 1 else "face") class FaceList(bpy.types.PropertyGroup): id = bpy.props.IntProperty(name="Face ID") +bpy.utils.register_class(FaceList) class IslandList(bpy.types.PropertyGroup): - faces = bpy.props.CollectionProperty(type=FaceList, name="Faces", - description="Faces belonging to this island") - label = bpy.props.StringProperty(name="Label", - description="Label on this island", + faces = bpy.props.CollectionProperty( + name="Faces", description="Faces belonging to this island", type=FaceList) + label = bpy.props.StringProperty( + name="Label", description="Label on this island", default="", update=label_changed) - abbreviation = bpy.props.StringProperty(name="Abbreviation", - description="Three-letter label to use when there is not enough space", + abbreviation = bpy.props.StringProperty( + name="Abbreviation", description="Three-letter label to use when there is not enough space", default="", update=island_item_changed) - auto_label = bpy.props.BoolProperty(name="Auto Label", - description="Generate the label automatically", + auto_label = bpy.props.BoolProperty( + name="Auto Label", description="Generate the label automatically", default=True, update=island_item_changed) - auto_abbrev = bpy.props.BoolProperty(name="Auto Abbreviation", - description="Generate the abbreviation automatically", + auto_abbrev = bpy.props.BoolProperty( + name="Auto Abbreviation", description="Generate the abbreviation automatically", default=True, update=island_item_changed) -bpy.utils.register_class(FaceList) bpy.utils.register_class(IslandList) class PaperModelSettings(bpy.types.PropertyGroup): - display_islands = bpy.props.BoolProperty(name="Highlight selected island", - description="Highlight faces corresponding to the selected island in the 3D View", + display_islands = bpy.props.BoolProperty( + name="Highlight selected island", description="Highlight faces corresponding to the selected island in the 3D View", options={'SKIP_SAVE'}, update=display_islands_changed) - islands_alpha = bpy.props.FloatProperty(name="Opacity", - description="Opacity of island highlighting", min=0.0, max=1.0, default=0.3) - limit_by_page = bpy.props.BoolProperty(name="Limit Island Size", - description="Do not create islands larger than given dimensions", default=False) - output_size_x = bpy.props.FloatProperty(name="Width", - description="Maximal width of an island", + islands_alpha = bpy.props.FloatProperty( + name="Opacity", description="Opacity of island highlighting", + min=0.0, max=1.0, default=0.3) + limit_by_page = bpy.props.BoolProperty( + name="Limit Island Size", description="Do not create islands larger than given dimensions", + default=False, update=page_size_preset_changed) + page_size_preset = bpy.props.EnumProperty( + name="Page Size", description="Maximal size of an island", + default='A4', update=page_size_preset_changed, items=global_paper_sizes) + output_size_x = bpy.props.FloatProperty( + name="Width", description="Maximal width of an island", default=0.2, soft_min=0.105, soft_max=0.841, subtype="UNSIGNED", unit="LENGTH") - output_size_y = bpy.props.FloatProperty(name="Height", - description="Maximal height of an island", + output_size_y = bpy.props.FloatProperty( + name="Height", description="Maximal height of an island", default=0.29, soft_min=0.148, soft_max=1.189, subtype="UNSIGNED", unit="LENGTH") - scale = bpy.props.FloatProperty(name="Scale", - description="Divisor of all dimensions when exporting", + scale = bpy.props.FloatProperty( + name="Scale", description="Divisor of all dimensions when exporting", default=1, soft_min=1.0, soft_max=10000.0, step=100, subtype='UNSIGNED', precision=1) bpy.utils.register_class(PaperModelSettings) @@ -2498,13 +2575,13 @@ bpy.utils.register_class(PaperModelSettings) def register(): bpy.utils.register_module(__name__) - bpy.types.Scene.paper_model = bpy.props.PointerProperty(type=PaperModelSettings, - name="Paper Model", - description="Settings of the Export Paper Model script", - options={'SKIP_SAVE'}) - bpy.types.Mesh.paper_island_list = bpy.props.CollectionProperty(type=IslandList, - name="Island List", description="") - bpy.types.Mesh.paper_island_index = bpy.props.IntProperty(name="Island List Index", + bpy.types.Scene.paper_model = bpy.props.PointerProperty( + name="Paper Model", description="Settings of the Export Paper Model script", + type=PaperModelSettings, options={'SKIP_SAVE'}) + bpy.types.Mesh.paper_island_list = bpy.props.CollectionProperty( + name="Island List", type=IslandList) + bpy.types.Mesh.paper_island_index = bpy.props.IntProperty( + name="Island List Index", default=-1, min=-1, max=100, options={'SKIP_SAVE'}) bpy.types.INFO_MT_file_export.append(menu_func) @@ -2516,5 +2593,6 @@ def unregister(): bpy.types.SpaceView3D.draw_handler_remove(display_islands.handle, 'WINDOW') display_islands.handle = None + if __name__ == "__main__": register() -- cgit v1.2.3