[Bf-extensions-cvs] [2ba08390] master: Paper model exporter: update for Bl 2.8, upstream 414b27f
Adam Dominec
noreply at git.blender.org
Wed Jun 26 22:16:59 CEST 2019
Commit: 2ba08390b59e9874d505d8f2815467b7b726d7ed
Author: Adam Dominec
Date: Wed Jun 26 22:14:52 2019 +0200
Branches: master
https://developer.blender.org/rBA2ba08390b59e9874d505d8f2815467b7b726d7ed
Paper model exporter: update for Bl 2.8, upstream 414b27f
===================================================================
M io_export_paper_model.py
===================================================================
diff --git a/io_export_paper_model.py b/io_export_paper_model.py
index a5818339..39d6b508 100644
--- a/io_export_paper_model.py
+++ b/io_export_paper_model.py
@@ -1,41 +1,37 @@
# -*- 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 #####
+# This script is Free software. Please share and reuse.
+# ♡2010-2019 Adam Dominec <adominec at gmail.com>
+
+## Code structure
+# This file consists of several components, in this order:
+# * Unfolding and baking
+# * Export (SVG or PDF)
+# * User interface
+# During the unfold process, the mesh is mirrored into a 2D structure: UVFace, UVEdge, UVVertex.
bl_info = {
"name": "Export Paper Model",
"author": "Addam Dominec",
- "version": (0, 9),
- "blender": (2, 73, 0),
+ "version": (1, 0),
+ "blender": (2, 80, 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"
+ "Scripts/Import-Export/Paper_Model"
}
+# Task: split into four files (SVG and PDF separately)
+ # does any portion of baking belong into the export module?
+ # sketch out the code for GCODE and two-sided export
+
# 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?
+# QuickSweepline is very much broken -- it throws GeometryError for all nets > ~15 faces
# rotate islands to minimize area -- and change that only if necessary to fill the page size
# Sticker.vertices should be of type Vector
@@ -43,30 +39,14 @@ bl_info = {
# * 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)
-
-"""
-
-Additional links:
- e-mail: adominec {at} gmail {dot} com
-
-"""
import bpy
import bl_operators
-import bgl
+import bmesh
import mathutils as M
from re import compile as re_compile
-from itertools import chain, repeat
-from math import pi, ceil
-
-try:
- import os.path as os_path
-except ImportError:
- os_path = None
-
-try:
- from blist import blist
-except ImportError:
- blist = list
+from itertools import chain, repeat, product, combinations
+from math import pi, ceil, asin, atan2
+import os.path as os_path
default_priority_effect = {
'CONVEX': 0.5,
@@ -108,18 +88,6 @@ def pairs(sequence):
yield this, first
-def argmax_pair(array, key):
- """Find an (unordered) pair of indices that maximize the given function"""
- n = len(array)
- mi, mj, m = None, None, None
- 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
- 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((
@@ -146,6 +114,45 @@ def z_up_matrix(n):
))
+def cage_fit(points, aspect):
+ """Find rotation for a minimum bounding box with a given aspect ratio
+ returns a tuple: rotation angle, box height"""
+ def guesses(polygon):
+ """Yield all tentative extrema of the bounding box height wrt. polygon rotation"""
+ for a, b in pairs(polygon):
+ if a == b:
+ continue
+ direction = (b - a).normalized()
+ sinx, cosx = -direction.y, direction.x
+ rot = M.Matrix(((cosx, -sinx), (sinx, cosx)))
+ rot_polygon = [rot @ p for p in polygon]
+ left, right = [fn(rot_polygon, key=lambda p: p.to_tuple()) for fn in (min, max)]
+ bottom, top = [fn(rot_polygon, key=lambda p: p.yx.to_tuple()) for fn in (min, max)]
+ #print(f"{rot_polygon.index(left)}-{rot_polygon.index(right)}, {rot_polygon.index(bottom)}-{rot_polygon.index(top)}")
+ horz, vert = right - left, top - bottom
+ # solve (rot * a).y == (rot * b).y
+ yield max(aspect * horz.x, vert.y), sinx, cosx
+ # solve (rot * a).x == (rot * b).x
+ yield max(horz.x, aspect * vert.y), -cosx, sinx
+ # solve aspect * (rot * (right - left)).x == (rot * (top - bottom)).y
+ # using substitution t = tan(rot / 2)
+ q = aspect * horz.x - vert.y
+ r = vert.x + aspect * horz.y
+ t = ((r**2 + q**2)**0.5 - r) / q if q != 0 else 0
+ t = -1 / t if abs(t) > 1 else t # pick the positive solution
+ siny, cosy = 2 * t / (1 + t**2), (1 - t**2) / (1 + t**2)
+ rot = M.Matrix(((cosy, -siny), (siny, cosy)))
+ for p in rot_polygon:
+ p[:] = rot @ p # note: this also modifies left, right, bottom, top
+ #print(f"solve {aspect * (right - left).x} == {(top - bottom).y} with aspect = {aspect}")
+ if left.x < right.x and bottom.y < top.y and all(left.x <= p.x <= right.x and bottom.y <= p.y <= top.y for p in rot_polygon):
+ #print(f"yield {max(aspect * (right - left).x, (top - bottom).y)}")
+ yield max(aspect * (right - left).x, (top - bottom).y), sinx*cosy + cosx*siny, cosx*cosy - sinx*siny
+ polygon = [points[i] for i in M.geometry.convex_hull_2d(points)]
+ height, sinx, cosx = min(guesses(polygon))
+ return atan2(sinx, cosx), height
+
+
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]
@@ -160,79 +167,43 @@ def create_blank_image(image_name, dimensions, alpha=1):
return image
-def bake(face_indices, uvmap, image):
- import bpy
- is_cycles = (bpy.context.scene.render.engine == 'CYCLES')
- if is_cycles:
- # please excuse the following mess. Cycles baking API does not seem to allow better.
- ob = bpy.context.active_object
- me = ob.data
- # add a disconnected image node that defines the bake target
- temp_nodes = dict()
- for mat in me.materials:
- mat.use_nodes = True
- img = mat.node_tree.nodes.new('ShaderNodeTexImage')
- img.image = image
- temp_nodes[mat] = img
- mat.node_tree.nodes.active = img
- uvmap.active = True
- # move all excess faces to negative numbers (that is the only way to disable them)
- 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)]
- for vid in ignored_uvs:
- loop[vid].uv *= -1
- bake_type = bpy.context.scene.cycles.bake_type
- sta = bpy.context.scene.render.bake.use_selected_to_active
- try:
- bpy.ops.object.bake(type=bake_type, margin=0, use_selected_to_active=sta, cage_extrusion=100, use_clear=False)
- except RuntimeError as e:
- raise UnfoldError(*e.args)
- finally:
- for mat, node in temp_nodes.items():
- mat.node_tree.nodes.remove(node)
- for vid in ignored_uvs:
- loop[vid].uv *= -1
- else:
- texfaces = uvmap.data
- for fid in face_indices:
- texfaces[fid].image = image
- bpy.ops.object.bake_image()
- for fid in face_indices:
- texfaces[fid].image = None
-
-
class UnfoldError(ValueError):
- pass
+ def mesh_select(self):
+ if len(self.args) > 1:
+ elems, bm = self.args[1:3]
+ bpy.context.tool_settings.mesh_select_mode = [bool(elems[key]) for key in ("verts", "edges", "faces")]
+ for elem in chain(bm.verts, bm.edges, bm.faces):
+ elem.select = False
+ for elem in chain(*elems.values()):
+ elem.select_set(True)
+ bmesh.update_edit_mesh(bpy.context.object.data, False, False)
class Unfolder:
def __init__(self, ob):
- self.ob = ob
- self.mesh = Mesh(ob.data, ob.matrix_world)
+ self.do_create_uvmap = False
+ bm = bmesh.from_edit_mesh(ob.data)
+ self.mesh = Mesh(bm, ob.matrix_world)
+ self.mesh.copy_freestyle_marks(ob.data)
self.mesh.check_correct()
- self.tex = None
+
+ def __del__(self):
+ if not self.do_create_uvmap:
+ self.mesh.delete_uvmap()
- def prepare(self, cage_size=None, create_uvmap=False, mark_seams=False, priority_effect=default_priority_effect, scale=1):
+ def prepare(self, cage_size=None, priority_effect=default_priority_effect, scale=1, limit_by_page=False):
"""Create the islands of the net"""
- self.mesh.generate_cuts(cage_size / scale if cage_size else None, priority_effect)
- is_landscape = cage_size and cage_size.x > cage_size.y
- self.mesh.finalize_islands(is_landscape)
+ self.mesh.generate_cuts(cage_size / scale if limit_by_page and cage_size else None, priority_effect)
+ self.
@@ Diff output truncated at 10240 characters. @@
More information about the Bf-extensions-cvs
mailing list