[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