Export UV Layout: Rewrite Export UV Layout addon
Differential Revision: https://developer.blender.org/D3715 Reviewer: brecht
This commit is contained in:
parent
ce871b0b50
commit
7017702897
|
@ -43,15 +43,20 @@ if "bpy" in locals():
|
|||
if "export_uv_svg" in locals():
|
||||
importlib.reload(export_uv_svg)
|
||||
|
||||
import os
|
||||
import bpy
|
||||
|
||||
from . import export_uv_eps
|
||||
from . import export_uv_png
|
||||
from . import export_uv_svg
|
||||
|
||||
from bpy.props import (
|
||||
StringProperty,
|
||||
BoolProperty,
|
||||
EnumProperty,
|
||||
IntVectorProperty,
|
||||
FloatProperty,
|
||||
)
|
||||
StringProperty,
|
||||
BoolProperty,
|
||||
EnumProperty,
|
||||
IntVectorProperty,
|
||||
FloatProperty,
|
||||
)
|
||||
|
||||
|
||||
class ExportUVLayout(bpy.types.Operator):
|
||||
|
@ -62,238 +67,162 @@ class ExportUVLayout(bpy.types.Operator):
|
|||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
filepath: StringProperty(
|
||||
subtype='FILE_PATH',
|
||||
)
|
||||
check_existing: BoolProperty(
|
||||
name="Check Existing",
|
||||
description="Check and warn on overwriting existing files",
|
||||
default=True,
|
||||
options={'HIDDEN'},
|
||||
)
|
||||
subtype='FILE_PATH',
|
||||
)
|
||||
export_all: BoolProperty(
|
||||
name="All UVs",
|
||||
description="Export all UVs in this mesh (not just visible ones)",
|
||||
default=False,
|
||||
)
|
||||
name="All UVs",
|
||||
description="Export all UVs in this mesh (not just visible ones)",
|
||||
default=False,
|
||||
)
|
||||
modified: BoolProperty(
|
||||
name="Modified",
|
||||
description="Exports UVs from the modified mesh",
|
||||
default=False,
|
||||
)
|
||||
name="Modified",
|
||||
description="Exports UVs from the modified mesh",
|
||||
default=False,
|
||||
)
|
||||
mode: EnumProperty(
|
||||
items=(('SVG', "Scalable Vector Graphic (.svg)",
|
||||
"Export the UV layout to a vector SVG file"),
|
||||
('EPS', "Encapsulate PostScript (.eps)",
|
||||
"Export the UV layout to a vector EPS file"),
|
||||
('PNG', "PNG Image (.png)",
|
||||
"Export the UV layout to a bitmap image"),
|
||||
),
|
||||
name="Format",
|
||||
description="File format to export the UV layout to",
|
||||
default='PNG',
|
||||
)
|
||||
items=(('SVG', "Scalable Vector Graphic (.svg)",
|
||||
"Export the UV layout to a vector SVG file"),
|
||||
('EPS', "Encapsulate PostScript (.eps)",
|
||||
"Export the UV layout to a vector EPS file"),
|
||||
('PNG', "PNG Image (.png)",
|
||||
"Export the UV layout to a bitmap image"),
|
||||
),
|
||||
name="Format",
|
||||
description="File format to export the UV layout to",
|
||||
default='PNG',
|
||||
)
|
||||
size: IntVectorProperty(
|
||||
name="Size",
|
||||
size=2,
|
||||
default=(1024, 1024),
|
||||
min=8, max=32768,
|
||||
description="Dimensions of the exported file",
|
||||
)
|
||||
size=2,
|
||||
default=(1024, 1024),
|
||||
min=8, max=32768,
|
||||
description="Dimensions of the exported file",
|
||||
)
|
||||
opacity: FloatProperty(
|
||||
name="Fill Opacity",
|
||||
min=0.0, max=1.0,
|
||||
default=0.25,
|
||||
description="Set amount of opacity for exported UV layout"
|
||||
)
|
||||
tessellated: BoolProperty(
|
||||
name="Tessellated UVs",
|
||||
description="Export tessellated UVs instead of polygons ones",
|
||||
default=False,
|
||||
options={'HIDDEN'}, # As not working currently :/
|
||||
)
|
||||
name="Fill Opacity",
|
||||
min=0.0, max=1.0,
|
||||
default=0.5,
|
||||
description="Set amount of opacity for exported UV layout"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
obj = context.active_object
|
||||
return (obj and obj.type == 'MESH' and obj.data.uv_layers)
|
||||
return obj is not None and obj.type == 'MESH' and len(obj.data.uv_layers) > 0
|
||||
|
||||
def _space_image(self, context):
|
||||
space_data = context.space_data
|
||||
if isinstance(space_data, bpy.types.SpaceImageEditor):
|
||||
return space_data
|
||||
else:
|
||||
return None
|
||||
def invoke(self, context, event):
|
||||
self.size = self.get_image_size(context)
|
||||
self.filepath = self.get_default_file_name(context) + "." + self.mode.lower()
|
||||
context.window_manager.fileselect_add(self)
|
||||
return {'RUNNING_MODAL'}
|
||||
|
||||
def _image_size(self, context, default_width=1024, default_height=1024):
|
||||
# fallback if not in image context.
|
||||
image_width, image_height = default_width, default_height
|
||||
def get_default_file_name(self, context):
|
||||
AMOUNT = 3
|
||||
objects = list(self.iter_objects_to_export(context))
|
||||
name = " ".join(sorted([obj.name for obj in objects[:AMOUNT]]))
|
||||
if len(objects) > AMOUNT:
|
||||
name += " and more"
|
||||
return name
|
||||
|
||||
space_data = self._space_image(context)
|
||||
if space_data:
|
||||
image = space_data.image
|
||||
if image:
|
||||
width, height = tuple(context.space_data.image.size)
|
||||
# in case no data is found.
|
||||
if width and height:
|
||||
image_width, image_height = width, height
|
||||
def check(self, context):
|
||||
if any(self.filepath.endswith(ext) for ext in (".png", ".eps", ".svg")):
|
||||
self.filepath = self.filepath[:-4]
|
||||
|
||||
return image_width, image_height
|
||||
|
||||
# Trying to be consistent with ED_object_get_active_image
|
||||
# from uvedit_ops.c so that what is exported are the uvs
|
||||
# that are seen in the UV Editor
|
||||
#
|
||||
# returns Image or None
|
||||
def _get_active_texture(self, mat):
|
||||
if mat is None or not mat.use_nodes:
|
||||
return None
|
||||
|
||||
node = self._get_active_texture_nodetree(mat.node_tree)
|
||||
|
||||
if node is not None and node.bl_rna.identifier in {'ShaderNodeTexImage', 'ShaderNodeTexEnvironment'}:
|
||||
return node.image
|
||||
|
||||
return None
|
||||
|
||||
# returns image node or None
|
||||
def _get_active_texture_nodetree(self, node_tree):
|
||||
active_tex_node = None
|
||||
active_group = None
|
||||
has_group = False
|
||||
inactive_node = None
|
||||
|
||||
for node in node_tree.nodes:
|
||||
if node.show_texture:
|
||||
active_tex_node = node
|
||||
if node.select:
|
||||
return node
|
||||
elif inactive_node is None and node.bl_rna.identifier in {'ShaderNodeTexImage', 'ShaderNodeTexEnvironment'}:
|
||||
inactive_node = node
|
||||
elif node.bl_rna.identifier == 'ShaderNodeGroup':
|
||||
if node.select:
|
||||
active_group = node
|
||||
else:
|
||||
has_group = True
|
||||
|
||||
# Not found a selected show_texture node
|
||||
# Try to find a selected show_texture node in the selected group
|
||||
if active_group is not None:
|
||||
node = self._get_active_texture_nodetree(active_group.node_tree)
|
||||
if node is not None:
|
||||
return node
|
||||
|
||||
if active_tex_node is not None:
|
||||
return active_tex_node
|
||||
|
||||
if has_group:
|
||||
for node in node_tree.nodes:
|
||||
if node.bl_rna.identifier == 'ShaderNodeGroup':
|
||||
n = self._get_active_texture_nodetree(node.node_tree)
|
||||
if n is not None and (n.show_texture or inactive_node is None):
|
||||
return n
|
||||
|
||||
return None
|
||||
|
||||
def _face_uv_iter(self, context, material_slots, mesh):
|
||||
uv_layer = mesh.uv_layers.active.data
|
||||
polys = mesh.polygons
|
||||
|
||||
if not self.export_all:
|
||||
local_image = None
|
||||
|
||||
if context.tool_settings.show_uv_local_view:
|
||||
space_data = self._space_image(context)
|
||||
if space_data:
|
||||
local_image = space_data.image
|
||||
has_active_texture = [
|
||||
self._get_active_texture(slot.material)
|
||||
is local_image for slot in material_slots]
|
||||
|
||||
for i, p in enumerate(polys):
|
||||
# context checks
|
||||
if (polys[i].select and (local_image is None or has_active_texture[polys[i].material_index])):
|
||||
start = p.loop_start
|
||||
end = start + p.loop_total
|
||||
uvs = tuple((uv.uv[0], uv.uv[1]) for uv in uv_layer[start:end])
|
||||
|
||||
# just write what we see.
|
||||
yield (i, uvs)
|
||||
else:
|
||||
# all, simple
|
||||
for i, p in enumerate(polys):
|
||||
start = p.loop_start
|
||||
end = start + p.loop_total
|
||||
uvs = tuple((uv.uv[0], uv.uv[1]) for uv in uv_layer[start:end])
|
||||
yield (i, uvs)
|
||||
ext = "." + self.mode.lower()
|
||||
self.filepath = bpy.path.ensure_ext(self.filepath, ext)
|
||||
return True
|
||||
|
||||
def execute(self, context):
|
||||
obj = context.active_object
|
||||
is_editmode = (obj.mode == 'EDIT')
|
||||
object = context.active_object
|
||||
is_editmode = (object.mode == 'EDIT')
|
||||
if is_editmode:
|
||||
bpy.ops.object.mode_set(mode='OBJECT', toggle=False)
|
||||
|
||||
mode = self.mode
|
||||
|
||||
filepath = self.filepath
|
||||
filepath = bpy.path.ensure_ext(filepath, "." + mode.lower())
|
||||
file = open(filepath, "w")
|
||||
fw = file.write
|
||||
filepath = bpy.path.ensure_ext(filepath, "." + self.mode.lower())
|
||||
|
||||
if mode == 'EPS':
|
||||
from . import export_uv_eps
|
||||
exportUV = export_uv_eps.Export_UV_EPS()
|
||||
elif mode == 'PNG':
|
||||
from . import export_uv_png
|
||||
exportUV = export_uv_png.Export_UV_PNG()
|
||||
elif mode == 'SVG':
|
||||
from . import export_uv_svg
|
||||
exportUV = export_uv_svg.Export_UV_SVG()
|
||||
meshes = list(self.iter_meshes_to_export(context))
|
||||
polygon_data = list(self.iter_polygon_data_to_draw(context, meshes))
|
||||
different_colors = set(color for _, color in polygon_data)
|
||||
if self.modified:
|
||||
self.free_meshes(meshes)
|
||||
|
||||
obList = [ob for ob in context.selected_objects if ob.type == 'MESH']
|
||||
|
||||
for obj in obList:
|
||||
obj.data.tag = False
|
||||
|
||||
exportUV.begin(fw, self.size, self.opacity)
|
||||
|
||||
for obj in obList:
|
||||
if (obj.data.tag):
|
||||
continue
|
||||
|
||||
obj.data.tag = True
|
||||
|
||||
if self.modified:
|
||||
mesh = obj.to_mesh(context.depsgraph, True)
|
||||
else:
|
||||
mesh = obj.data
|
||||
|
||||
exportUV.build(mesh, lambda: self._face_uv_iter(
|
||||
context, obj.material_slots, mesh))
|
||||
|
||||
exportUV.end()
|
||||
export = self.get_exporter()
|
||||
export(filepath, polygon_data, different_colors, self.size[0], self.size[1], self.opacity)
|
||||
|
||||
if is_editmode:
|
||||
bpy.ops.object.mode_set(mode='EDIT', toggle=False)
|
||||
|
||||
file.close()
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
def check(self, context):
|
||||
filepath = bpy.path.ensure_ext(self.filepath, "." + self.mode.lower())
|
||||
if filepath != self.filepath:
|
||||
self.filepath = filepath
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
def iter_meshes_to_export(self, context):
|
||||
for object in self.iter_objects_to_export(context):
|
||||
if self.modified:
|
||||
yield object.to_mesh(context.depsgraph, apply_modifiers=True)
|
||||
else:
|
||||
yield object.data
|
||||
|
||||
def invoke(self, context, event):
|
||||
import os
|
||||
self.size = self._image_size(context)
|
||||
self.filepath = os.path.splitext(bpy.data.filepath)[0]
|
||||
wm = context.window_manager
|
||||
wm.fileselect_add(self)
|
||||
return {'RUNNING_MODAL'}
|
||||
def iter_objects_to_export(self, context):
|
||||
for object in context.selected_objects:
|
||||
if object.type != "MESH":
|
||||
continue
|
||||
mesh = object.data
|
||||
if mesh.uv_layers.active is None:
|
||||
continue
|
||||
yield object
|
||||
|
||||
def free_meshes(self, meshes):
|
||||
for mesh in meshes:
|
||||
bpy.data.meshes.remove(mesh)
|
||||
|
||||
def currently_image_image_editor(self, context):
|
||||
return isinstance(context.space_data, bpy.types.SpaceImageEditor)
|
||||
|
||||
def get_currently_opened_image(self, context):
|
||||
if not self.currently_image_image_editor(context):
|
||||
return None
|
||||
return context.space_data.image
|
||||
|
||||
def get_image_size(self, context):
|
||||
# fallback if not in image context
|
||||
image_width = self.size[0]
|
||||
image_height = self.size[1]
|
||||
|
||||
# get size of "active" image if some exist
|
||||
image = self.get_currently_opened_image(context)
|
||||
if image is not None:
|
||||
width, height = image.size
|
||||
if width and height:
|
||||
image_width = width
|
||||
image_height = height
|
||||
|
||||
return image_width, image_height
|
||||
|
||||
def iter_polygon_data_to_draw(self, context, meshes):
|
||||
for mesh in meshes:
|
||||
uv_layer = mesh.uv_layers.active.data
|
||||
for polygon in mesh.polygons:
|
||||
if self.export_all or polygon.select:
|
||||
start = polygon.loop_start
|
||||
end = start + polygon.loop_total
|
||||
uvs = tuple(tuple(uv.uv) for uv in uv_layer[start:end])
|
||||
yield (uvs, self.get_polygon_color(mesh, polygon))
|
||||
|
||||
def get_polygon_color(self, mesh, polygon, default = (0.8, 0.8, 0.8)):
|
||||
if polygon.material_index < len(mesh.materials):
|
||||
material = mesh.materials[polygon.material_index]
|
||||
if material is not None:
|
||||
return tuple(material.diffuse_color)
|
||||
return default
|
||||
|
||||
def get_exporter(self):
|
||||
if self.mode == "PNG":
|
||||
return export_uv_png.export
|
||||
elif self.mode == "EPS":
|
||||
return export_uv_eps.export
|
||||
elif self.mode == "SVG":
|
||||
return export_uv_svg.export
|
||||
else:
|
||||
assert False
|
||||
|
||||
|
||||
def menu_func(self, context):
|
||||
|
@ -304,11 +233,9 @@ def register():
|
|||
bpy.utils.register_class(ExportUVLayout)
|
||||
bpy.types.IMAGE_MT_uvs.append(menu_func)
|
||||
|
||||
|
||||
def unregister():
|
||||
bpy.utils.unregister_class(ExportUVLayout)
|
||||
bpy.types.IMAGE_MT_uvs.remove(menu_func)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
register()
|
||||
|
|
|
@ -21,75 +21,72 @@
|
|||
import bpy
|
||||
|
||||
|
||||
class Export_UV_EPS:
|
||||
def begin(self, fw, image_size, opacity):
|
||||
def export(filepath, face_data, colors, width, height, opacity):
|
||||
with open(filepath, "w") as file:
|
||||
for text in get_file_parts(face_data, colors, width, height, opacity):
|
||||
file.write(text)
|
||||
|
||||
self.fw = fw
|
||||
self.image_width = image_size[0]
|
||||
self.image_height = image_size[1]
|
||||
self.opacity = opacity
|
||||
def get_file_parts(face_data, colors, width, height, opacity):
|
||||
yield from header(width, height)
|
||||
if opacity > 0.0:
|
||||
name_by_color = {}
|
||||
yield from prepare_colors(colors, name_by_color)
|
||||
yield from draw_colored_polygons(face_data, name_by_color, width, height)
|
||||
yield from draw_lines(face_data, width, height)
|
||||
yield from footer()
|
||||
|
||||
fw("%!PS-Adobe-3.0 EPSF-3.0\n")
|
||||
fw("%%%%Creator: Blender %s\n" % bpy.app.version_string)
|
||||
fw("%%Pages: 1\n")
|
||||
fw("%%Orientation: Portrait\n")
|
||||
fw("%%%%BoundingBox: 0 0 %d %d\n" % (self.image_width, self.image_height))
|
||||
fw("%%%%HiResBoundingBox: 0.0 0.0 %.4f %.4f\n" %
|
||||
(self.image_width, self.image_height))
|
||||
fw("%%EndComments\n")
|
||||
fw("%%Page: 1 1\n")
|
||||
fw("0 0 translate\n")
|
||||
fw("1.0 1.0 scale\n")
|
||||
fw("0 0 0 setrgbcolor\n")
|
||||
fw("[] 0 setdash\n")
|
||||
fw("1 setlinewidth\n")
|
||||
fw("1 setlinejoin\n")
|
||||
fw("1 setlinecap\n")
|
||||
|
||||
def build(self, mesh, face_iter_func):
|
||||
polys = mesh.polygons
|
||||
def header(width, height):
|
||||
yield "%!PS-Adobe-3.0 EPSF-3.0\n"
|
||||
yield f"%%Creator: Blender {bpy.app.version_string}\n"
|
||||
yield "%%Pages: 1\n"
|
||||
yield "%%Orientation: Portrait\n"
|
||||
yield f"%%BoundingBox: 0 0 {width} {height}\n"
|
||||
yield f"%%HiResBoundingBox: 0.0 0.0 {width:.4f} {height:.4f}\n"
|
||||
yield "%%EndComments\n"
|
||||
yield "%%Page: 1 1\n"
|
||||
yield "0 0 translate\n"
|
||||
yield "1.0 1.0 scale\n"
|
||||
yield "0 0 0 setrgbcolor\n"
|
||||
yield "[] 0 setdash\n"
|
||||
yield "1 setlinewidth\n"
|
||||
yield "1 setlinejoin\n"
|
||||
yield "1 setlinecap\n"
|
||||
|
||||
if self.opacity > 0.0:
|
||||
for i, mat in enumerate(mesh.materials if mesh.materials else [None]):
|
||||
self.fw("/DRAW_%d {" % i)
|
||||
self.fw("gsave\n")
|
||||
if mat:
|
||||
color = tuple((1.0 - ((1.0 - c) * self.opacity))
|
||||
for c in mat.diffuse_color)
|
||||
else:
|
||||
color = 1.0, 1.0, 1.0
|
||||
self.fw("%.3g %.3g %.3g setrgbcolor\n" % color)
|
||||
self.fw("fill\n")
|
||||
self.fw("grestore\n")
|
||||
self.fw("0 setgray\n")
|
||||
self.fw("} def\n")
|
||||
def prepare_colors(colors, out_name_by_color):
|
||||
for i, color in enumerate(colors):
|
||||
name = f"COLOR_{i}"
|
||||
yield "/%s {" % name
|
||||
out_name_by_color[color] = name
|
||||
|
||||
# fill
|
||||
for i, uvs in face_iter_func():
|
||||
self.fw("newpath\n")
|
||||
for j, uv in enumerate(uvs):
|
||||
uv_scale = (uv[0] * self.image_width, uv[1] * self.image_height)
|
||||
if j == 0:
|
||||
self.fw("%.5f %.5f moveto\n" % uv_scale)
|
||||
else:
|
||||
self.fw("%.5f %.5f lineto\n" % uv_scale)
|
||||
yield "gsave\n"
|
||||
yield "%.3g %.3g %.3g setrgbcolor\n" % color
|
||||
yield "fill\n"
|
||||
yield "grestore\n"
|
||||
yield "0 setgray\n"
|
||||
yield "} def\n"
|
||||
|
||||
self.fw("closepath\n")
|
||||
self.fw("DRAW_%d\n" % polys[i].material_index)
|
||||
def draw_colored_polygons(face_data, name_by_color, width, height):
|
||||
for uvs, color in face_data:
|
||||
yield from draw_polygon_path(uvs, width, height)
|
||||
yield "closepath\n"
|
||||
yield "%s\n" % name_by_color[color]
|
||||
|
||||
# stroke only
|
||||
for i, uvs in face_iter_func():
|
||||
self.fw("newpath\n")
|
||||
for j, uv in enumerate(uvs):
|
||||
uv_scale = (uv[0] * self.image_width, uv[1] * self.image_height)
|
||||
if j == 0:
|
||||
self.fw("%.5f %.5f moveto\n" % uv_scale)
|
||||
else:
|
||||
self.fw("%.5f %.5f lineto\n" % uv_scale)
|
||||
def draw_lines(face_data, width, height):
|
||||
for uvs, _ in face_data:
|
||||
yield from draw_polygon_path(uvs, width, height)
|
||||
yield "closepath\n"
|
||||
yield "stroke\n"
|
||||
|
||||
self.fw("closepath\n")
|
||||
self.fw("stroke\n")
|
||||
def draw_polygon_path(uvs, width, height):
|
||||
yield "newpath\n"
|
||||
for j, uv in enumerate(uvs):
|
||||
uv_scale = (uv[0] * width, uv[1] * height)
|
||||
if j == 0:
|
||||
yield "%.5f %.5f moveto\n" % uv_scale
|
||||
else:
|
||||
yield "%.5f %.5f lineto\n" % uv_scale
|
||||
|
||||
def end(self):
|
||||
self.fw("showpage\n")
|
||||
self.fw("%%EOF\n")
|
||||
def footer():
|
||||
yield "showpage\n"
|
||||
yield "%%EOF\n"
|
|
@ -20,163 +20,134 @@
|
|||
|
||||
import bpy
|
||||
|
||||
# maybe we could also just use the svg exporter, import it again
|
||||
# and render it. Unfortunately the svg importer does not work atm.
|
||||
def export(filepath, face_data, colors, width, height, opacity):
|
||||
aspect = width / height
|
||||
|
||||
class Export_UV_PNG:
|
||||
def begin(self, fw, image_size, opacity):
|
||||
self.filepath = fw.__self__.name
|
||||
fw.__self__.close()
|
||||
# curves for lines
|
||||
lines = curve_from_uvs(face_data, aspect, 1 / min(width, height))
|
||||
lines_object = bpy.data.objects.new("temp_lines_object", lines)
|
||||
black_material = make_colored_material((0, 0, 0))
|
||||
lines.materials.append(black_material)
|
||||
|
||||
self.scene = bpy.data.scenes.new("uv_temp")
|
||||
# background mesh
|
||||
background_mesh = background_mesh_from_uvs(face_data, colors, aspect, opacity)
|
||||
background_object = bpy.data.objects.new("temp_background_object", background_mesh)
|
||||
background_object.location = (0, 0, -1)
|
||||
|
||||
image_width = image_size[0]
|
||||
image_height = image_size[1]
|
||||
# camera
|
||||
camera = bpy.data.cameras.new("temp_camera")
|
||||
camera_object = bpy.data.objects.new("temp_camera_object", camera)
|
||||
camera.type = "ORTHO"
|
||||
camera.ortho_scale = max(1, aspect)
|
||||
camera_object.location = (aspect / 2, 0.5, 1)
|
||||
camera_object.rotation_euler = (0, 0, 0)
|
||||
|
||||
self.scene.render.resolution_x = image_width
|
||||
self.scene.render.resolution_y = image_height
|
||||
self.scene.render.resolution_percentage = 100
|
||||
# scene
|
||||
scene = bpy.data.scenes.new("temp_scene")
|
||||
scene.render.engine = "BLENDER_EEVEE"
|
||||
scene.render.resolution_x = width
|
||||
scene.render.resolution_y = height
|
||||
scene.render.image_settings.color_mode = "RGBA"
|
||||
scene.render.alpha_mode = "TRANSPARENT"
|
||||
scene.render.filepath = filepath
|
||||
|
||||
self.scene.render.alpha_mode = 'TRANSPARENT'
|
||||
# Link everything to the scene
|
||||
scene.collection.objects.link(lines_object)
|
||||
scene.collection.objects.link(camera_object)
|
||||
scene.collection.objects.link(background_object)
|
||||
scene.camera = camera_object
|
||||
|
||||
if image_width > image_height:
|
||||
self.scene.render.pixel_aspect_y = image_width / image_height
|
||||
elif image_width < image_height:
|
||||
self.scene.render.pixel_aspect_x = image_height / image_width
|
||||
# Render
|
||||
override = {"scene" : scene}
|
||||
bpy.ops.render.render(override, write_still=True)
|
||||
|
||||
self.base_material = bpy.data.materials.new("uv_temp_base")
|
||||
self.base_material.use_nodes = True
|
||||
self.base_material.node_tree.nodes.clear()
|
||||
output_node = self.base_material.node_tree.nodes.new(type="ShaderNodeOutputMaterial")
|
||||
emission_node = self.base_material.node_tree.nodes.new(type="ShaderNodeEmission")
|
||||
emission_node.inputs["Color"].default_value = (1.0, 1.0, 1.0, opacity)
|
||||
self.base_material.node_tree.links.new(
|
||||
output_node.inputs["Surface"],
|
||||
emission_node.outputs["Emission"])
|
||||
# Cleanup
|
||||
bpy.data.objects.remove(lines_object)
|
||||
bpy.data.objects.remove(camera_object)
|
||||
bpy.data.objects.remove(background_object)
|
||||
|
||||
self.material_wire = self.base_material.copy()
|
||||
self.material_wire.name = "Wire"
|
||||
self.material_wire.node_tree.nodes['Emission'].inputs["Color"].default_value = (0.0, 0.0, 0.0, 1.0)
|
||||
for material in background_mesh.materials:
|
||||
bpy.data.materials.remove(material)
|
||||
bpy.data.meshes.remove(background_mesh)
|
||||
|
||||
self.base_material.blend_method = "BLEND"
|
||||
bpy.data.cameras.remove(camera)
|
||||
bpy.data.curves.remove(lines)
|
||||
bpy.data.materials.remove(black_material)
|
||||
bpy.data.scenes.remove(scene)
|
||||
|
||||
self.material_solids_list = [] # list of lists
|
||||
self.material_solids_list.append([self.base_material,
|
||||
self.material_wire])
|
||||
def curve_from_uvs(face_data, aspect, thickness):
|
||||
lines = bpy.data.curves.new("temp_curve", "CURVE")
|
||||
lines.fill_mode = "BOTH"
|
||||
lines.bevel_depth = thickness
|
||||
lines.offset = -thickness / 2
|
||||
lines.dimensions = "3D"
|
||||
|
||||
self.mesh_list = []
|
||||
self.obj_list = []
|
||||
for uvs, _ in face_data:
|
||||
for i in range(len(uvs)):
|
||||
start = uvs[i]
|
||||
end = uvs[(i+1) % len(uvs)]
|
||||
|
||||
def build(self, mesh_source, face_iter_func):
|
||||
material_solids = [self.base_material.copy() for i in range(max(1, len(mesh_source.materials)))]
|
||||
spline = lines.splines.new("POLY")
|
||||
# one point is already there
|
||||
spline.points.add(count=1)
|
||||
points = spline.points
|
||||
|
||||
self.material_solids_list.append(material_solids)
|
||||
points[0].co.x = start[0] * aspect
|
||||
points[0].co.y = start[1]
|
||||
|
||||
mesh = bpy.data.meshes.new("uv_temp")
|
||||
self.mesh_list.append(mesh)
|
||||
points[1].co.x = end[0] * aspect
|
||||
points[1].co.y = end[1]
|
||||
|
||||
for mat_solid in material_solids:
|
||||
mesh.materials.append(mat_solid)
|
||||
return lines
|
||||
|
||||
# setup materials
|
||||
for i, mat_solid in enumerate(material_solids):
|
||||
if mesh_source.materials and mesh_source.materials[i]:
|
||||
mat_solid.node_tree.nodes['Emission'].\
|
||||
inputs["Color"].default_value[0:3]\
|
||||
= mesh_source.materials[i].diffuse_color
|
||||
def background_mesh_from_uvs(face_data, colors, aspect, opacity):
|
||||
mesh = bpy.data.meshes.new("temp_background")
|
||||
|
||||
# Add materials for wireframe modifier.
|
||||
for mat_solid in material_solids:
|
||||
mesh.materials.append(self.material_wire)
|
||||
vertices = []
|
||||
polygons = []
|
||||
for uvs, _ in face_data:
|
||||
polygon = []
|
||||
for uv in uvs:
|
||||
polygon.append(len(vertices))
|
||||
vertices.append((uv[0] * aspect, uv[1], 0))
|
||||
polygons.append(tuple(polygon))
|
||||
|
||||
polys_source = mesh_source.polygons
|
||||
mesh.from_pydata(vertices, [], polygons)
|
||||
|
||||
# get unique UV's in case there are many overlapping
|
||||
# which slow down filling.
|
||||
face_hash = {(uvs, polys_source[i].material_index)
|
||||
for i, uvs in face_iter_func()}
|
||||
materials, material_index_by_color = make_polygon_background_materials(colors, opacity)
|
||||
for material in materials:
|
||||
mesh.materials.append(material)
|
||||
|
||||
# now set the faces coords and locations
|
||||
# build mesh data
|
||||
mesh_new_vertices = []
|
||||
mesh_new_materials = []
|
||||
mesh_new_polys_startloop = []
|
||||
mesh_new_polys_totloop = []
|
||||
mesh_new_loops_vertices = []
|
||||
for generated_polygon, (_, color) in zip(mesh.polygons, face_data):
|
||||
generated_polygon.material_index = material_index_by_color[color]
|
||||
|
||||
current_vert = 0
|
||||
mesh.update()
|
||||
mesh.validate()
|
||||
|
||||
for uvs, mat_idx in face_hash:
|
||||
num_verts = len(uvs)
|
||||
# dummy = (0.0,) * num_verts
|
||||
for uv in uvs:
|
||||
mesh_new_vertices += (uv[0], uv[1], 0.0)
|
||||
mesh_new_polys_startloop.append(current_vert)
|
||||
mesh_new_polys_totloop.append(num_verts)
|
||||
mesh_new_loops_vertices += range(current_vert,
|
||||
current_vert + num_verts)
|
||||
mesh_new_materials.append(mat_idx)
|
||||
current_vert += num_verts
|
||||
return mesh
|
||||
|
||||
mesh.vertices.add(current_vert)
|
||||
mesh.loops.add(current_vert)
|
||||
mesh.polygons.add(len(mesh_new_polys_startloop))
|
||||
def make_polygon_background_materials(colors, opacity=1):
|
||||
materials = []
|
||||
material_index_by_color = {}
|
||||
for i, color in enumerate(colors):
|
||||
material = make_colored_material(color, opacity)
|
||||
materials.append(material)
|
||||
material_index_by_color[color] = i
|
||||
return materials, material_index_by_color
|
||||
|
||||
mesh.vertices.foreach_set("co", mesh_new_vertices)
|
||||
mesh.loops.foreach_set("vertex_index", mesh_new_loops_vertices)
|
||||
mesh.polygons.foreach_set("loop_start", mesh_new_polys_startloop)
|
||||
mesh.polygons.foreach_set("loop_total", mesh_new_polys_totloop)
|
||||
mesh.polygons.foreach_set("material_index", mesh_new_materials)
|
||||
def make_colored_material(color, opacity=1):
|
||||
material = bpy.data.materials.new("temp_material")
|
||||
material.use_nodes = True
|
||||
material.blend_method = "BLEND"
|
||||
tree = material.node_tree
|
||||
tree.nodes.clear()
|
||||
|
||||
mesh.update(calc_edges=True)
|
||||
output_node = tree.nodes.new("ShaderNodeOutputMaterial")
|
||||
emission_node = tree.nodes.new("ShaderNodeEmission")
|
||||
|
||||
obj_solid = bpy.data.objects.new("uv_temp_solid", mesh)
|
||||
emission_node.inputs["Color"].default_value = [color[0], color[1], color[2], opacity]
|
||||
tree.links.new(emission_node.outputs["Emission"], output_node.inputs["Surface"])
|
||||
|
||||
wire_mod = obj_solid.modifiers.new("wire_mod", 'WIREFRAME')
|
||||
wire_mod.use_replace = False
|
||||
wire_mod.use_relative_offset = True
|
||||
|
||||
wire_mod.material_offset = len(material_solids)
|
||||
|
||||
self.obj_list.append(obj_solid)
|
||||
self.scene.collection.objects.link(obj_solid)
|
||||
|
||||
def end(self):
|
||||
# setup the camera
|
||||
cam = bpy.data.cameras.new("uv_temp")
|
||||
cam.type = 'ORTHO'
|
||||
cam.ortho_scale = 1.0
|
||||
obj_cam = bpy.data.objects.new("uv_temp_cam", cam)
|
||||
obj_cam.location = 0.5, 0.5, 1.0
|
||||
self.scene.collection.objects.link(obj_cam)
|
||||
self.obj_list.append(obj_cam)
|
||||
self.scene.camera = obj_cam
|
||||
|
||||
# scene render settings
|
||||
self.scene.render.alpha_mode = 'TRANSPARENT'
|
||||
self.scene.render.image_settings.color_mode = 'RGBA'
|
||||
|
||||
self.scene.frame_start = 1
|
||||
self.scene.frame_end = 1
|
||||
|
||||
self.scene.render.image_settings.file_format = 'PNG'
|
||||
self.scene.render.filepath = self.filepath
|
||||
|
||||
self.scene.update()
|
||||
|
||||
data_context = {"blend_data": bpy.context.blend_data,
|
||||
"scene": self.scene}
|
||||
bpy.ops.render.render(data_context, write_still=True)
|
||||
|
||||
# cleanup
|
||||
bpy.data.scenes.remove(self.scene, do_unlink=True)
|
||||
|
||||
for obj in self.obj_list:
|
||||
bpy.data.objects.remove(obj, do_unlink=True)
|
||||
|
||||
bpy.data.cameras.remove(cam, do_unlink=True)
|
||||
|
||||
for mesh in self.mesh_list:
|
||||
bpy.data.meshes.remove(mesh, do_unlink=True)
|
||||
|
||||
for material_solids in self.material_solids_list:
|
||||
for mat_solid in material_solids:
|
||||
bpy.data.materials.remove(mat_solid, do_unlink=True)
|
||||
return material
|
||||
|
|
|
@ -19,65 +19,46 @@
|
|||
# <pep8-80 compliant>
|
||||
|
||||
import bpy
|
||||
|
||||
|
||||
from xml.sax.saxutils import escape
|
||||
from os.path import basename
|
||||
from xml.sax.saxutils import escape
|
||||
|
||||
def export(filepath, face_data, colors, width, height, opacity):
|
||||
with open(filepath, "w") as file:
|
||||
for text in get_file_parts(face_data, colors, width, height, opacity):
|
||||
file.write(text)
|
||||
|
||||
class Export_UV_SVG:
|
||||
def begin(self, fw, image_size, opacity):
|
||||
def get_file_parts(face_data, colors, width, height, opacity):
|
||||
yield from header(width, height)
|
||||
yield from draw_polygons(face_data, width, height, opacity)
|
||||
yield from footer()
|
||||
|
||||
self.fw = fw
|
||||
self.image_width = image_size[0]
|
||||
self.image_height = image_size[1]
|
||||
self.opacity = opacity
|
||||
def header(width, height):
|
||||
yield '<?xml version="1.0" standalone="no"?>\n'
|
||||
yield '<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" \n'
|
||||
yield ' "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">\n'
|
||||
yield f'<svg width="{width}" height="{height}" viewBox="0 0 {width} {height}"\n'
|
||||
yield ' xmlns="http://www.w3.org/2000/svg" version="1.1">\n'
|
||||
desc = f"{basename(bpy.data.filepath)}, (Blender {bpy.app.version_string})"
|
||||
yield f'<desc>{escape(desc)}</desc>\n'
|
||||
|
||||
fw('<?xml version="1.0" standalone="no"?>\n')
|
||||
fw('<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" \n')
|
||||
fw(' "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">\n')
|
||||
fw('<svg width="%d" height="%d" viewBox="0 0 %d %d"\n' %
|
||||
(self.image_width, self.image_height, self.image_width, self.image_height))
|
||||
fw(' xmlns="http://www.w3.org/2000/svg" version="1.1">\n')
|
||||
desc = ("%r, (Blender %s)" %
|
||||
(basename(bpy.data.filepath), bpy.app.version_string))
|
||||
fw('<desc>%s</desc>\n' % escape(desc))
|
||||
def draw_polygons(face_data, width, height, opacity):
|
||||
for uvs, color in face_data:
|
||||
fill = f'fill="{get_color_string(color)}"'
|
||||
|
||||
def build(self, mesh, face_iter_func):
|
||||
self.fw('<g>\n')
|
||||
desc = ("Mesh: %s" % (mesh.name))
|
||||
self.fw('<desc>%s</desc>\n' % escape(desc))
|
||||
yield '<polygon stroke="black" stroke-width="1"'
|
||||
yield f' {fill} fill-opacity="{opacity:.2g}"'
|
||||
|
||||
# svg colors
|
||||
fill_settings = []
|
||||
fill_default = 'fill="grey"'
|
||||
for mat in mesh.materials if mesh.materials else [None]:
|
||||
if mat:
|
||||
fill_settings.append('fill="rgb(%d, %d, %d)"' %
|
||||
tuple(int(c * 255) for c in mat.diffuse_color))
|
||||
else:
|
||||
fill_settings.append(fill_default)
|
||||
yield ' points="'
|
||||
|
||||
polys = mesh.polygons
|
||||
for i, uvs in face_iter_func():
|
||||
try: # rare cases material index is invalid.
|
||||
fill = fill_settings[polys[i].material_index]
|
||||
except IndexError:
|
||||
fill = fill_default
|
||||
for uv in uvs:
|
||||
x, y = uv[0], 1.0 - uv[1]
|
||||
yield f'{x*width:.3f},{y*height:.3f} '
|
||||
yield '" />\n'
|
||||
|
||||
self.fw('<polygon stroke="black" stroke-width="1"')
|
||||
if self.opacity > 0.0:
|
||||
self.fw(' %s fill-opacity="%.2g"' % (fill, self.opacity))
|
||||
def get_color_string(color):
|
||||
r, g, b = color
|
||||
return f"rgb({round(r*255)}, {round(g*255)}, {round(b*255)})"
|
||||
|
||||
self.fw(' points="')
|
||||
|
||||
for j, uv in enumerate(uvs):
|
||||
x, y = uv[0], 1.0 - uv[1]
|
||||
self.fw('%.3f,%.3f ' % (x * self.image_width, y * self.image_height))
|
||||
self.fw('" />\n')
|
||||
|
||||
self.fw('</g>\n')
|
||||
|
||||
def end(self):
|
||||
self.fw('\n')
|
||||
self.fw('</svg>\n')
|
||||
def footer():
|
||||
yield '\n'
|
||||
yield '</svg>\n'
|
Loading…
Reference in New Issue