Newer
Older
('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_numbers: bpy.props.BoolProperty(
name="Create Numbers", description="Enumerate edges to make it clear which edges should be sticked together",
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",
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",
default=90, min=1, soft_min=30, soft_max=600, subtype="UNSIGNED")
bake_samples: bpy.props.IntProperty(
name="Samples", description="Number of samples to render for each pixel",
default=64, min=1, subtype="UNSIGNED")
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",
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")
])
name="Scale", description="Divisor of all dimensions when exporting",
default=1, soft_min=1.0, soft_max=100.0, subtype='FACTOR', precision=1)
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",
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",
default=False, options={'SKIP_SAVE'})
style: bpy.props.PointerProperty(type=PaperModelStyle)
unfolder = None
@classmethod
def poll(cls, context):
return context.active_object and context.active_object.type == 'MESH'
2053
2054
2055
2056
2057
2058
2059
2060
2061
2062
2063
2064
2065
2066
2067
2068
2069
2070
2071
2072
2073
2074
2075
2076
2077
2078
2079
2080
2081
2082
def prepare(self, context):
sce = context.scene
self.recall_mode = context.object.mode
bpy.ops.object.mode_set(mode='EDIT')
self.object = context.active_object
self.unfolder = Unfolder(self.object)
cage_size = M.Vector((sce.paper_model.output_size_x, sce.paper_model.output_size_y))
self.unfolder.prepare(cage_size, scale=sce.unit_settings.scale_length/self.scale, limit_by_page=sce.paper_model.limit_by_page)
if self.scale == 1:
self.scale = ceil(self.get_scale_ratio(sce))
def recall(self):
if self.unfolder:
del self.unfolder
bpy.ops.object.mode_set(mode=self.recall_mode)
def invoke(self, context, event):
self.scale = context.scene.paper_model.scale
try:
self.prepare(context)
except UnfoldError as error:
self.report(type={'ERROR_INVALID_INPUT'}, message=error.args[0])
error.mesh_select()
self.recall()
return {'CANCELLED'}
wm = context.window_manager
wm.fileselect_add(self)
return {'RUNNING_MODAL'}
def execute(self, context):
if not self.unfolder:
self.prepare(context)
self.unfolder.do_create_uvmap = self.do_create_uvmap
try:
if self.object.data.paper_island_list:
self.unfolder.copy_island_names(self.object.data.paper_island_list)
self.unfolder.save(self.properties)
self.report({'INFO'}, "Saved a {}-page document".format(len(self.unfolder.mesh.pages)))
return {'FINISHED'}
except UnfoldError as error:
self.report(type={'ERROR_INVALID_INPUT'}, message=error.args[0])
return {'CANCELLED'}
finally:
self.recall()
def get_scale_ratio(self, sce):
margin = self.output_margin + self.sticker_width
if min(self.output_size_x, self.output_size_y) <= 2 * margin:
return False
output_inner_size = M.Vector((self.output_size_x - 2*margin, self.output_size_y - 2*margin))
ratio = self.unfolder.mesh.largest_island_ratio(output_inner_size)
return ratio * sce.unit_settings.scale_length / self.scale
def draw(self, context):
layout = self.layout
layout.prop(self.properties, "do_create_uvmap")
row = layout.row(align=True)
row.menu("VIEW3D_MT_paper_model_presets", text=bpy.types.VIEW3D_MT_paper_model_presets.bl_label)
row.operator("export_mesh.paper_model_preset_add", text="", icon='ADD')
row.operator("export_mesh.paper_model_preset_add", text="", icon='REMOVE').remove_active = True
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")
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="",
2135
2136
2137
2138
2139
2140
2141
2142
2143
2144
2145
2146
2147
2148
2149
2150
2151
2152
2153
2154
2155
2156
icon=('TRIA_DOWN' if self.ui_expanded_document else 'TRIA_RIGHT'), emboss=False)
row.label(text="Document Settings")
if self.ui_expanded_document:
box.prop(self.properties, "file_format", text="Format")
box.prop(self.properties, "page_size_preset")
col = box.column(align=True)
col.active = self.page_size_preset == 'USER'
col.prop(self.properties, "output_size_x")
col.prop(self.properties, "output_size_y")
box.prop(self.properties, "output_margin")
col = box.column()
col.prop(self.properties, "do_create_stickers")
col.prop(self.properties, "do_create_numbers")
col = box.column()
col.active = self.do_create_stickers or self.do_create_numbers
col.prop(self.properties, "sticker_width")
box.prop(self.properties, "angle_epsilon")
box.prop(self.properties, "output_type")
col = box.column()
col.active = (self.output_type != 'NONE')
if len(self.object.data.uv_layers) == 8:
col.label(text="No UV slots left, No Texture is the only option.", icon='ERROR')
elif context.scene.render.engine != 'CYCLES' and self.output_type != 'NONE':
col.label(text="Cycles will be used for texture baking.", icon='ERROR')
row = col.row()
row.active = self.output_type in ('AMBIENT_OCCLUSION', 'RENDER', 'SELECTED_TO_ACTIVE')
row.prop(self.properties, "bake_samples")
col.prop(self.properties, "output_dpi")
row = col.row()
row.active = self.file_format == 'SVG'
row.prop(self.properties, "image_packing", text="Images")
box = layout.box()
row = box.row(align=True)
row.prop(
self.properties, "ui_expanded_style", text="",
2173
2174
2175
2176
2177
2178
2179
2180
2181
2182
2183
2184
2185
2186
2187
2188
2189
2190
2191
2192
2193
2194
2195
2196
2197
2198
2199
2200
2201
2202
2203
2204
2205
2206
2207
2208
2209
2210
2211
2212
2213
icon=('TRIA_DOWN' if self.ui_expanded_style else 'TRIA_RIGHT'), emboss=False)
row.label(text="Colors and Style")
if self.ui_expanded_style:
box.prop(self.style, "line_width", text="Default line width")
col = box.column()
col.prop(self.style, "outer_color")
col.prop(self.style, "outer_width", text="Relative width")
col.prop(self.style, "outer_style", text="Style")
col = box.column()
col.active = self.output_type != 'NONE'
col.prop(self.style, "use_outbg", text="Outer Lines Highlight:")
sub = col.column()
sub.active = self.output_type != 'NONE' and self.style.use_outbg
sub.prop(self.style, "outbg_color", text="")
sub.prop(self.style, "outbg_width", text="Relative width")
col = box.column()
col.prop(self.style, "convex_color")
col.prop(self.style, "convex_width", text="Relative width")
col.prop(self.style, "convex_style", text="Style")
col = box.column()
col.prop(self.style, "concave_color")
col.prop(self.style, "concave_width", text="Relative width")
col.prop(self.style, "concave_style", text="Style")
col = box.column()
col.prop(self.style, "freestyle_color")
col.prop(self.style, "freestyle_width", text="Relative width")
col.prop(self.style, "freestyle_style", text="Style")
col = box.column()
col.active = self.output_type != 'NONE'
col.prop(self.style, "use_inbg", text="Inner Lines Highlight:")
sub = col.column()
sub.active = self.output_type != 'NONE' and self.style.use_inbg
sub.prop(self.style, "inbg_color", text="")
sub.prop(self.style, "inbg_width", text="Relative width")
col = box.column()
col.active = self.do_create_stickers
col.prop(self.style, "sticker_fill")
box.prop(self.style, "text_color")
2214
2215
2216
2217
2218
2219
2220
2221
2222
2223
2224
2225
2226
2227
2228
2229
2230
2231
2232
2233
2234
2235
2236
2237
2238
2239
2240
2241
2242
2243
2244
2245
2246
2247
2248
2249
2250
2251
2252
2253
2254
2255
2256
2257
2258
2259
2260
2261
2262
2263
2264
2265
2266
2267
2268
2269
2270
2271
2272
def menu_func_export(self, context):
self.layout.operator("export_mesh.paper_model", text="Paper Model (.pdf/.svg)")
def menu_func_unfold(self, context):
self.layout.operator("mesh.unfold", text="Unfold")
class SelectIsland(bpy.types.Operator):
"""Blender Operator: select all faces of the active island"""
bl_idname = "mesh.select_paper_island"
bl_label = "Select Island"
bl_description = "Select an island of the paper model net"
operation: bpy.props.EnumProperty(
name="Operation", description="Operation with the current selection",
default='ADD', items=[
('ADD', "Add", "Add to current selection"),
('REMOVE', "Remove", "Remove from selection"),
('REPLACE', "Replace", "Select only the ")
])
@classmethod
def poll(cls, context):
return context.active_object and context.active_object.type == 'MESH' and context.mode == 'EDIT_MESH'
def execute(self, context):
ob = context.active_object
me = ob.data
bm = bmesh.from_edit_mesh(me)
island = me.paper_island_list[me.paper_island_index]
faces = {face.id for face in island.faces}
edges = set()
verts = set()
if self.operation == 'REPLACE':
for face in bm.faces:
selected = face.index in faces
face.select = selected
if selected:
edges.update(face.edges)
verts.update(face.verts)
for edge in bm.edges:
edge.select = edge in edges
for vert in bm.verts:
vert.select = vert in verts
else:
selected = (self.operation == 'ADD')
for index in faces:
face = bm.faces[index]
face.select = selected
edges.update(face.edges)
verts.update(face.verts)
for edge in edges:
edge.select = any(face.select for face in edge.link_faces)
for vert in verts:
vert.select = any(edge.select for edge in vert.link_edges)
bmesh.update_edit_mesh(me, False, False)
return {'FINISHED'}
class VIEW3D_MT_paper_model_presets(bpy.types.Menu):
bl_label = "Paper Model Presets"
preset_subdir = "export_mesh"
preset_operator = "script.execute_preset"
draw = bpy.types.Menu.draw_preset
class AddPresetPaperModel(bl_operators.presets.AddPresetBase, bpy.types.Operator):
"""Add or remove a Paper Model Preset"""
bl_idname = "export_mesh.paper_model_preset_add"
bl_label = "Add Paper Model Preset"
preset_menu = "VIEW3D_MT_paper_model_presets"
preset_subdir = "export_mesh"
preset_defines = ["op = bpy.context.active_operator"]
@property
def preset_values(self):
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
if not (prop.is_hidden or prop.is_skip_save or prop_id in blacklist)]
class VIEW3D_PT_paper_model_tools(bpy.types.Panel):
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = 'Paper'
bl_label = "Unfold"
def draw(self, context):
layout = self.layout
sce = context.scene
obj = context.active_object
mesh = obj.data if obj and obj.type == 'MESH' else None
layout.operator("mesh.unfold")
if context.mode == 'EDIT_MESH':
row = layout.row(align=True)
row.operator("mesh.mark_seam", text="Mark Seam").clear = False
row.operator("mesh.mark_seam", text="Clear Seam").clear = True
else:
layout.operator("mesh.clear_all_seams")
class VIEW3D_PT_paper_model_settings(bpy.types.Panel):
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = 'Paper'
bl_label = "Export"
def draw(self, context):
layout = self.layout
sce = context.scene
obj = context.active_object
mesh = obj.data if obj and obj.type == 'MESH' else None
layout.operator("export_mesh.paper_model")
props = sce.paper_model
layout.prop(props, "scale", text="Model Scale: 1/")
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 = props.page_size_preset == 'USER'
sub.prop(props, "output_size_x")
sub.prop(props, "output_size_y")
class DATA_PT_paper_model_islands(bpy.types.Panel):
bl_space_type = 'PROPERTIES'
bl_region_type = 'WINDOW'
bl_context = "data"
bl_label = "Paper Model Islands"
COMPAT_ENGINES = {'BLENDER_RENDER', 'BLENDER_EEVEE', 'BLENDER_WORKBENCH'}
def draw(self, context):
layout = self.layout
sce = context.scene
obj = context.active_object
mesh = obj.data if obj and obj.type == 'MESH' else None
layout.operator("mesh.unfold", icon='FILE_REFRESH')
if mesh and mesh.paper_island_list:
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,
'paper_island_list', mesh, 'paper_island_index', rows=1, maxrows=5)
sub = layout.split(align=True)
sub.operator("mesh.select_paper_island", text="Select").operation = 'ADD'
sub.operator("mesh.select_paper_island", text="Deselect").operation = 'REMOVE'
sub.prop(sce.paper_model, "sync_island", icon='UV_SYNC_SELECT', toggle=True)
if mesh.paper_island_index >= 0:
list_item = mesh.paper_island_list[mesh.paper_island_index]
sub = layout.column(align=True)
sub.prop(list_item, "auto_label")
sub.prop(list_item, "label")
sub.prop(list_item, "auto_abbrev")
row = sub.row()
row.active = not list_item.auto_abbrev
row.prop(list_item, "abbreviation")
else:
layout.box().label(text="Not unfolded")
def label_changed(self, context):
"""The label of an island was changed"""
# accessing properties via [..] to avoid a recursive call after the update
self["auto_label"] = not self.label or self.label.isspace()
island_item_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:
self["label"] = "" # avoid self-conflict
number = 1
while any(item.label == "Island {}".format(number) for item in island_list):
number += 1
self["label"] = "Island {}".format(number)
if self.auto_abbrev:
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")
def island_index_changed(self, context):
"""The active island was changed"""
if context.scene.paper_model.sync_island and SelectIsland.poll(context):
bpy.ops.mesh.select_paper_island(operation='REPLACE')
class FaceList(bpy.types.PropertyGroup):
id: bpy.props.IntProperty(name="Face ID")
class IslandList(bpy.types.PropertyGroup):
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",
default="", update=island_item_changed)
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",
default=True, update=island_item_changed)
class PaperModelSettings(bpy.types.PropertyGroup):
sync_island: bpy.props.BoolProperty(
name="Sync", description="Keep faces of the active island selected",
default=False, update=island_index_changed)
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",
default=0.29, soft_min=0.148, soft_max=1.189, subtype="UNSIGNED", unit="LENGTH")
name="Scale", description="Divisor of all dimensions when exporting",
default=1, soft_min=1.0, soft_max=100.0, subtype='FACTOR', precision=1)
module_classes = (
Unfold,
ExportPaperModel,
ClearAllSeams,
SelectIsland,
AddPresetPaperModel,
FaceList,
IslandList,
PaperModelSettings,
VIEW3D_MT_paper_model_presets,
DATA_PT_paper_model_islands,
VIEW3D_PT_paper_model_tools,
VIEW3D_PT_paper_model_settings,
def register():
for cls in module_classes:
bpy.utils.register_class(cls)
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'}, update=island_index_changed)
bpy.types.TOPBAR_MT_file_export.append(menu_func_export)
bpy.types.VIEW3D_MT_edit_mesh.prepend(menu_func_unfold)
def unregister():
bpy.types.TOPBAR_MT_file_export.remove(menu_func_export)
bpy.types.VIEW3D_MT_edit_mesh.remove(menu_func_unfold)
for cls in reversed(module_classes):
bpy.utils.unregister_class(cls)
if __name__ == "__main__":
register()