[Bf-extensions-cvs] [30f62c3] master: new addon: Export Paper Model
Addam Dominec
noreply at git.blender.org
Tue Feb 10 11:46:21 CET 2015
Commit: 30f62c33e474e8ccbf1b07bae722f5cd5c326160
Author: Addam Dominec
Date: Mon Feb 9 19:09:03 2015 +0100
Branches: master
https://developer.blender.org/rBAC30f62c33e474e8ccbf1b07bae722f5cd5c326160
new addon: Export Paper Model
This script generates a flat net of a given mesh.
It creates SVG files suitable for direct printing and paper modeling.
===================================================================
A io_export_paper_model.py
===================================================================
diff --git a/io_export_paper_model.py b/io_export_paper_model.py
new file mode 100644
index 0000000..5fd8719
--- /dev/null
+++ b/io_export_paper_model.py
@@ -0,0 +1,2152 @@
+# -*- coding: utf-8 -*-
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but without any warranty; without even the implied warranty of
+# merchantability or fitness for a particular purpose. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+# ##### END GPL LICENSE BLOCK #####
+
+#### 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
+# 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
+# Sticker.vertices should be of type Vector
+
+# check conflicts in island naming and either:
+# * append a number to the conflicting names or
+# * enumerate faces uniquely within all islands of the same name (requires a check that both label and abbr. equals)
+
+
+bl_info = {
+ "name": "Export Paper Model",
+ "author": "Addam Dominec",
+ "version": (0, 9),
+ "blender": (2, 70, 0),
+ "location": "File > Export > Paper Model",
+ "warning": "",
+ "description": "Export printable net of the active mesh",
+ "category": "Import-Export",
+ "wiki_url": "http://wiki.blender.org/index.php/Extensions:2.6/Py/"
+ "Scripts/Import-Export/Paper_Model",
+ "tracker_url": "https://developer.blender.org/T38441"
+}
+
+"""
+
+Additional links:
+ e-mail: adominec {at} gmail {dot} com
+
+"""
+import bpy
+import bl_operators
+import bgl
+import mathutils as M
+from re import compile as re_compile
+from itertools import chain
+from math import pi
+
+try:
+ import os.path as os_path
+except ImportError:
+ os_path = None
+
+try:
+ from blist import blist
+except ImportError:
+ blist = list
+
+default_priority_effect = {
+ 'CONVEX': 0.5,
+ 'CONCAVE': 1,
+ 'LENGTH': -0.05
+}
+
+
+def first_letters(text):
+ """Iterator over the first letter of each word"""
+ for match in first_letters.pattern.finditer(text):
+ yield text[match.start()]
+first_letters.pattern = re_compile("(?<!\w)[\w]")
+
+
+def pairs(sequence):
+ """Generate consecutive pairs throughout the given sequence; at last, it gives elements last, first."""
+ i = iter(sequence)
+ previous = first = next(i)
+ for this in i:
+ yield previous, this
+ previous = this
+ yield this, first
+
+
+def argmax_pair(array, key):
+ """Find an (unordered) pair of indices that maximize the given function"""
+ l = len(array)
+ mi, mj, m = None, None, None
+ for i in range(l):
+ for j in range(i+1, l):
+ k = key(array[i], array[j])
+ if not m or k > m:
+ mi, mj, m = i, j, k
+ return mi, mj
+
+
+def fitting_matrix(v1, v2):
+ """Get a matrix that rotates v1 to the same direction as v2"""
+ return (1 / v1.length_squared) * M.Matrix((
+ (v1.x*v2.x + v1.y*v2.y, v1.y*v2.x - v1.x*v2.y),
+ (v1.x*v2.y - v1.y*v2.x, v1.x*v2.x + v1.y*v2.y)))
+
+
+def z_up_matrix(n):
+ """Get a rotation matrix that aligns given vector upwards."""
+ b = n.xy.length
+ l = n.length
+ if b > 0:
+ return M.Matrix((
+ (n.x*n.z/(b*l), n.y*n.z/(b*l), -b/l),
+ (-n.y/b, n.x/b, 0),
+ (0, 0, 0)
+ ))
+ else:
+ # no need for rotation
+ return M.Matrix((
+ (1, 0, 0),
+ (0, (-1 if n.z < 0 else 1), 0),
+ (0, 0, 0)
+ ))
+
+
+def create_blank_image(image_name, dimensions, alpha=1):
+ """Create a new image and assign white color to all its pixels"""
+ image_name = image_name[:64]
+ 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. "
+ "Please report this on the BlenderArtists forum. Export failed.")
+ image.pixels = [1, 1, 1, alpha] * (width * height)
+ image.file_format = 'PNG'
+ return image
+
+
+class UnfoldError(ValueError):
+ pass
+
+
+class Unfolder:
+ def __init__(self, ob):
+ self.ob = ob
+ self.mesh = Mesh(ob.data, ob.matrix_world)
+ self.tex = None
+
+ def prepare(self, cage_size=None, create_uvmap=False, mark_seams=False, priority_effect=default_priority_effect, scale=1):
+ """Create the islands of the net"""
+ self.mesh.generate_cuts(cage_size / scale if cage_size else None, priority_effect)
+ self.mesh.finalize_islands()
+ self.mesh.enumerate_islands()
+ if create_uvmap:
+ self.tex = self.mesh.save_uv()
+ if mark_seams:
+ self.mesh.mark_cuts()
+
+ def copy_island_names(self, island_list):
+ """Copy island label and abbreviation from the best matching island in the list"""
+ orig_list = {(frozenset(face.id for face in item.faces), item.label, item.abbreviation) for item in island_list}
+ for island in self.mesh.islands:
+ islfaces = {uvface.face.index for uvface in island.faces}
+ match = max(orig_list, key=lambda item: islfaces.intersection(item[0]))
+ island.label = match[1]
+ island.abbreviation = match[2]
+
+ def save(self, properties):
+ """Export the document"""
+ # Note about scale: input is direcly in blender length
+ # Island.apply_scale multiplies everything by a user-defined ratio
+ # SVG object multiplies everything by ppm (output in pixels)
+ filepath = properties.filepath
+ if filepath.lower().endswith((".svg", ".png")):
+ filepath = filepath[0:-4]
+ # page size in meters
+ page_size = M.Vector((properties.output_size_x, properties.output_size_y))
+ # printable area size in meters
+ printable_size = page_size - 2 * properties.output_margin * M.Vector((1, 1))
+ unit_scale = bpy.context.scene.unit_settings.scale_length
+ ppm = properties.output_dpi * 100 / 2.54 # pixels per meter
+
+ # after this call, all dimensions will be in meters
+ self.mesh.scale_islands(unit_scale/properties.scale)
+ if properties.do_create_stickers:
+ self.mesh.generate_stickers(properties.sticker_width, properties.do_create_numbers)
+ elif properties.do_create_numbers:
+ self.mesh.generate_numbers_alone(properties.sticker_width)
+
+ text_height = properties.sticker_width if (properties.do_create_numbers and len(self.mesh.islands) > 1) else 0
+ aspect_ratio = printable_size.x / printable_size.y
+ # finalizing islands will scale everything so that the page height is 1
+ self.mesh.finalize_islands(title_height=text_height)
+ self.mesh.fit_islands(cage_size=printable_size)
+
+ if properties.output_type != 'NONE':
+ # bake an image and save it as a PNG to disk or into memory
+ use_separate_images = properties.image_packing in ('ISLAND_LINK', 'ISLAND_EMBED')
+ tex = self.mesh.save_uv(cage_size=printable_size, separate_image=use_separate_images, tex=self.tex)
+ if not tex:
+ raise UnfoldError("The mesh has no UV Map slots left. Either delete a UV Map or export the net without textures.")
+ rd = bpy.context.scene.render
+ recall = 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
+
+ rd.bake_type = 'TEXTURE' if properties.output_type == 'TEXTURE' else 'FULL'
+ rd.use_bake_selected_to_active = (properties.output_type == 'SELECTED_TO_ACTIVE')
+
+ 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
+ if properties.image_packing == 'PAGE_LINK':
+ self.mesh.save_image(tex, printable_size * ppm, filepath)
+ elif properties.image_packing == 'ISLAND_LINK':
+ self.mesh.save_separate_images(tex, printable_size.y * ppm, filepath)
+ elif properties.image_packing == 'ISLAND_EMBED':
+ self.mesh.save_separate_images(tex, printable_size.y * ppm, filepath, do_embed=True)
+
+ # revoke settings
+ 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
+ if not properties.do_create_uvmap:
+ tex.active = True
+ bpy.ops.mesh.uv_texture_remove()
+
+ svg = SVG(page_size, ppm, properties.style, (properties.output_type == 'NONE'))
+ svg.do_create_stickers = properties.do_create_stickers
+ svg.margin = properties.output_margin
+ svg.text_size = properties.sticker_width
+ svg.write(self.mesh, filepath)
+
+
+class Mesh:
+ """Wrapper for Bpy Mesh"""
+
+ def __init__(self, mesh, matrix):
+ self.verts = dict()
+ self.edges = dict()
+ self.edges_by_verts_indices = dict()
+ self.faces = dict()
+ self.islands = list()
+ self.data = mesh
+ self.pages = list()
+ for bpy_vertex in mesh.vertices:
+ self.verts[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
+ self.edges_by_verts_indices[(edge.va.index, edge.vb.index)] = edge
+ self.edges_by_verts_indices[(edge.vb.index, edge.va.index)] = edge
+ for bpy_face in mesh.polygons:
+ face = Face(bpy_face, self)
+ self.faces[bpy_face.index] = face
+ for edge in self.edges.values():
+ edge.choose_main_faces()
+ if edge.main_faces:
+ edge.calculate_angle()
+
+ def generate_cuts(self, page_size, priority_effect):
+ """Cut the mesh so that it can be unfolded to a flat net."""
+ # warning: this constructor modifies its parameter (face)
+ islands = {Island(face) for face in self.faces.values()}
+ # check for edges that are cut permanently
+ 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 edg
@@ Diff output truncated at 10240 characters. @@
More information about the Bf-extensions-cvs
mailing list