Newer
Older
# -*- coding: utf-8 -*-
# This script is Free software. Please share and reuse.
# ♡2010-2020 Adam Dominec <adominec@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": (1, 2),
"blender": (2, 83, 0),
"location": "File > Export > Paper Model",
"warning": "",
"description": "Export printable net of the active mesh",
"doc_url": "{BLENDER_MANUAL_URL}/addons/import_export/paper_model.html",
"category": "Import-Export",
}
# 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
# 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
# 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)
import bpy
import bl_operators
import mathutils as M
from re import compile as re_compile
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,
'CONCAVE': 1,
'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"""
for match in first_letters.pattern.finditer(text):
yield text[match.start()]
first_letters.pattern = re_compile(r"((?<!\w)\w)|\d")
def is_upsidedown_wrong(name):
"""Tell if the string would get a different meaning if written upside down"""
chars = set(name)
mistakable = set("69NZMWpbqd")
rotatable = set("80oOxXIl").union(mistakable)
return chars.issubset(rotatable) and not chars.isdisjoint(mistakable)
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 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
if b > 0:
return M.Matrix((
(n.x*n.z/(b*s), n.y*n.z/(b*s), -b/s),
(-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)
))
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
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)]
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
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):
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]
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
def store_rna_properties(*datablocks):
return [{prop.identifier: getattr(data, prop.identifier) for prop in data.rna_type.properties if not prop.is_readonly} for data in datablocks]
def apply_rna_properties(memory, *datablocks):
for recall, data in zip(memory, datablocks):
for key, value in recall.items():
setattr(data, key, value)
class UnfoldError(ValueError):
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, loop_triangles=False, destructive=False)
class Unfolder:
def __init__(self, ob):
self.do_create_uvmap = False
bm = bmesh.from_edit_mesh(ob.data)
self.mesh = Mesh(bm, ob.matrix_world)
self.mesh.check_correct()
def __del__(self):
if not self.do_create_uvmap:
self.mesh.delete_uvmap()
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 limit_by_page and cage_size else None, priority_effect)
self.mesh.finalize_islands(cage_size or M.Vector((1, 1)))
self.mesh.enumerate_islands()
self.mesh.save_uv()
Loading
Loading full blame...