diff options
-rw-r--r-- | io_mesh_uv_layout/__init__.py | 341 | ||||
-rw-r--r-- | io_mesh_uv_layout/export_uv_eps.py | 123 | ||||
-rw-r--r-- | io_mesh_uv_layout/export_uv_png.py | 291 | ||||
-rw-r--r-- | io_mesh_uv_layout/export_uv_svg.py | 101 |
4 files changed, 366 insertions, 490 deletions
diff --git a/io_mesh_uv_layout/__init__.py b/io_mesh_uv_layout/__init__.py index 4f801a41..5d8d27da 100644 --- a/io_mesh_uv_layout/__init__.py +++ b/io_mesh_uv_layout/__init__.py @@ -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) - - def _space_image(self, context): - space_data = context.space_data - if isinstance(space_data, bpy.types.SpaceImageEditor): - return space_data - else: - return None - - 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 + return obj is not None and obj.type == 'MESH' and len(obj.data.uv_layers) > 0 - 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 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'} - return image_width, image_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 - # 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 + def check(self, context): + if any(self.filepath.endswith(ext) for ext in (".png", ".eps", ".svg")): + self.filepath = self.filepath[:-4] - 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'] + export = self.get_exporter() + export(filepath, polygon_data, different_colors, self.size[0], self.size[1], self.opacity) - for obj in obList: - obj.data.tag = False - - exportUV.begin(fw, self.size, self.opacity) - - for obj in obList: - if (obj.data.tag): - continue + if is_editmode: + bpy.ops.object.mode_set(mode='EDIT', toggle=False) - obj.data.tag = True + return {'FINISHED'} + def iter_meshes_to_export(self, context): + for object in self.iter_objects_to_export(context): if self.modified: - mesh = obj.to_mesh(context.depsgraph, True) + yield object.to_mesh(context.depsgraph, apply_modifiers=True) else: - mesh = obj.data + yield object.data - exportUV.build(mesh, lambda: self._face_uv_iter( - context, obj.material_slots, mesh)) + 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 - exportUV.end() + def free_meshes(self, meshes): + for mesh in meshes: + bpy.data.meshes.remove(mesh) - if is_editmode: - bpy.ops.object.mode_set(mode='EDIT', toggle=False) + def currently_image_image_editor(self, context): + return isinstance(context.space_data, bpy.types.SpaceImageEditor) - file.close() + def get_currently_opened_image(self, context): + if not self.currently_image_image_editor(context): + return None + return context.space_data.image - return {'FINISHED'} + def get_image_size(self, context): + # fallback if not in image context + image_width = self.size[0] + image_height = self.size[1] - 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 + # 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 - 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'} + 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() diff --git a/io_mesh_uv_layout/export_uv_eps.py b/io_mesh_uv_layout/export_uv_eps.py index d00e998a..3280cefa 100644 --- a/io_mesh_uv_layout/export_uv_eps.py +++ b/io_mesh_uv_layout/export_uv_eps.py @@ -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"
\ No newline at end of file diff --git a/io_mesh_uv_layout/export_uv_png.py b/io_mesh_uv_layout/export_uv_png.py index be03ed0b..74b820b8 100644 --- a/io_mesh_uv_layout/export_uv_png.py +++ b/io_mesh_uv_layout/export_uv_png.py @@ -20,163 +20,134 @@ import bpy - -class Export_UV_PNG: - def begin(self, fw, image_size, opacity): - self.filepath = fw.__self__.name - fw.__self__.close() - - self.scene = bpy.data.scenes.new("uv_temp") - - image_width = image_size[0] - image_height = image_size[1] - - self.scene.render.resolution_x = image_width - self.scene.render.resolution_y = image_height - self.scene.render.resolution_percentage = 100 - - self.scene.render.alpha_mode = 'TRANSPARENT' - - 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 - - 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"]) - - 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) - - self.base_material.blend_method = "BLEND" - - self.material_solids_list = [] # list of lists - self.material_solids_list.append([self.base_material, - self.material_wire]) - - self.mesh_list = [] - self.obj_list = [] - - def build(self, mesh_source, face_iter_func): - material_solids = [self.base_material.copy() for i in range(max(1, len(mesh_source.materials)))] - - self.material_solids_list.append(material_solids) - - mesh = bpy.data.meshes.new("uv_temp") - self.mesh_list.append(mesh) - - for mat_solid in material_solids: - mesh.materials.append(mat_solid) - - # 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 - - # Add materials for wireframe modifier. - for mat_solid in material_solids: - mesh.materials.append(self.material_wire) - - polys_source = mesh_source.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()} - - # 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 = [] - - current_vert = 0 - - 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 - - mesh.vertices.add(current_vert) - mesh.loops.add(current_vert) - mesh.polygons.add(len(mesh_new_polys_startloop)) - - 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) - - mesh.update(calc_edges=True) - - obj_solid = bpy.data.objects.new("uv_temp_solid", mesh) - - 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) +# 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 + + # 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) + + # 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) + + # 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) + + # 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 + + # 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 + + # Render + override = {"scene" : scene} + bpy.ops.render.render(override, write_still=True) + + # Cleanup + bpy.data.objects.remove(lines_object) + bpy.data.objects.remove(camera_object) + bpy.data.objects.remove(background_object) + + for material in background_mesh.materials: + bpy.data.materials.remove(material) + bpy.data.meshes.remove(background_mesh) + + bpy.data.cameras.remove(camera) + bpy.data.curves.remove(lines) + bpy.data.materials.remove(black_material) + bpy.data.scenes.remove(scene) + +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" + + for uvs, _ in face_data: + for i in range(len(uvs)): + start = uvs[i] + end = uvs[(i+1) % len(uvs)] + + spline = lines.splines.new("POLY") + # one point is already there + spline.points.add(count=1) + points = spline.points + + points[0].co.x = start[0] * aspect + points[0].co.y = start[1] + + points[1].co.x = end[0] * aspect + points[1].co.y = end[1] + + return lines + +def background_mesh_from_uvs(face_data, colors, aspect, opacity): + mesh = bpy.data.meshes.new("temp_background") + + 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)) + + mesh.from_pydata(vertices, [], polygons) + + materials, material_index_by_color = make_polygon_background_materials(colors, opacity) + for material in materials: + mesh.materials.append(material) + + for generated_polygon, (_, color) in zip(mesh.polygons, face_data): + generated_polygon.material_index = material_index_by_color[color] + + mesh.update() + mesh.validate() + + return mesh + +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 + +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() + + output_node = tree.nodes.new("ShaderNodeOutputMaterial") + emission_node = tree.nodes.new("ShaderNodeEmission") + + emission_node.inputs["Color"].default_value = [color[0], color[1], color[2], opacity] + tree.links.new(emission_node.outputs["Emission"], output_node.inputs["Surface"]) + + return material diff --git a/io_mesh_uv_layout/export_uv_svg.py b/io_mesh_uv_layout/export_uv_svg.py index fe727d09..d00f9402 100644 --- a/io_mesh_uv_layout/export_uv_svg.py +++ b/io_mesh_uv_layout/export_uv_svg.py @@ -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 - -class Export_UV_SVG: - def begin(self, fw, image_size, opacity): - - self.fw = fw - self.image_width = image_size[0] - self.image_height = image_size[1] - self.opacity = opacity - - 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 build(self, mesh, face_iter_func): - self.fw('<g>\n') - desc = ("Mesh: %s" % (mesh.name)) - self.fw('<desc>%s</desc>\n' % escape(desc)) - - # 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) - - 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 - - self.fw('<polygon stroke="black" stroke-width="1"') - if self.opacity > 0.0: - self.fw(' %s fill-opacity="%.2g"' % (fill, self.opacity)) - - 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 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) + +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() + +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' + +def draw_polygons(face_data, width, height, opacity): + for uvs, color in face_data: + fill = f'fill="{get_color_string(color)}"' + + yield '<polygon stroke="black" stroke-width="1"' + yield f' {fill} fill-opacity="{opacity:.2g}"' + + yield ' points="' + + for uv in uvs: + x, y = uv[0], 1.0 - uv[1] + yield f'{x*width:.3f},{y*height:.3f} ' + yield '" />\n' + +def get_color_string(color): + r, g, b = color + return f"rgb({round(r*255)}, {round(g*255)}, {round(b*255)})" + +def footer(): + yield '\n' + yield '</svg>\n'
\ No newline at end of file |