Skip to content
Snippets Groups Projects
io_export_paper_model.py 122 KiB
Newer Older
  • Learn to ignore specific revisions
  •             bottom = min(min(seg.bottom for seg in self.boundary), min(vertex.co.y for vertex in phantoms))
                top = max(max(seg.top for seg in self.boundary), max(vertex.co.y for vertex in phantoms))
                bbox_width = right - left
                bbox_height = top - bottom
    
                if min(bbox_width, bbox_height)**2 > size_limit.x**2 + size_limit.y**2:
                    return False
                if (bbox_width > size_limit.x or bbox_height > size_limit.y) and (bbox_height > size_limit.x or bbox_width > size_limit.y):
                    # further checks (TODO!)
                    # for the time being, just throw this piece away
                    return False
    
    
            distance_limit = edge.vector.length_squared * epsilon
    
            # try and merge UVVertices closer than sqrt(distance_limit)
            merged_uvedges = set()
            merged_uvedge_pairs = list()
    
            # merge all uvvertices that are close enough using a union-find structure
            # uvvertices will be merged only in cases other->self and self->self
            # all resulting groups are merged together to a uvvertex of self
            is_merged_mine = False
            shared_vertices = self.uvverts_by_id.keys() & other.uvverts_by_id.keys()
            for vertex_id in shared_vertices:
                uvs = self.uvverts_by_id[vertex_id] + other.uvverts_by_id[vertex_id]
                len_mine = len(self.uvverts_by_id[vertex_id])
                merged = dict()
                for i, a in enumerate(uvs[:len_mine]):
                    i = root_find(i, merged)
                    for j, b in enumerate(uvs[i+1:], i+1):
                        b = b if j < len_mine else phantoms[b]
                        j = root_find(j, merged)
                        if i == j:
                            continue
                        i, j = (j, i) if j < i else (i, j)
                        if (a.co - b.co).length_squared < distance_limit:
                            merged[j] = i
                for source, target in merged.items():
                    target = root_find(target, merged)
                    phantoms[uvs[source]] = uvs[target]
                    is_merged_mine |= (source < len_mine)  # remember that a vertex of this island has been merged
    
            for uvedge in (chain(self.boundary, other.boundary) if is_merged_mine else other.boundary):
                for partner in uvedge.edge.uvedges:
                    if partner is not uvedge:
                        paired_a, paired_b = phantoms.get(partner.vb, partner.vb), phantoms.get(partner.va, partner.va)
                        if (partner.uvface.flipped ^ flipped) != uvedge.uvface.flipped:
                            paired_a, paired_b = paired_b, paired_a
                        if phantoms.get(uvedge.va, uvedge.va) is paired_a and phantoms.get(uvedge.vb, uvedge.vb) is paired_b:
                            # if these two edges will get merged, add them both to the set
                            merged_uvedges.update((uvedge, partner))
                            merged_uvedge_pairs.append((uvedge, partner))
                            break
    
            if uvedge_b not in merged_uvedges:
                raise UnfoldError("Export failed. Please report this error, including the model if you can.")
    
    
            boundary_other = [
                PhantomUVEdge(phantoms[uvedge.va], phantoms[uvedge.vb], flipped ^ uvedge.uvface.flipped)
    
                for uvedge in other.boundary if uvedge not in merged_uvedges]
            # TODO: if is_merged_mine, it might make sense to create a similar list from self.boundary as well
    
    
            incidence = {vertex.tup for vertex in phantoms.values()}.intersection(vertex.tup for vertex in self.vertices)
    
            incidence = {position: list() for position in incidence}  # from now on, 'incidence' is a dict
            for uvedge in chain(boundary_other, self.boundary):
                if uvedge.va.co == uvedge.vb.co:
                    continue
                for vertex in (uvedge.va, uvedge.vb):
                    site = incidence.get(vertex.tup)
                    if site is not None:
                        site.append(uvedge)
            for position, segments in incidence.items():
                if len(segments) <= 2:
                    continue
                segments.sort(key=slope_from(position))
                for right, left in pairs(segments):
                    is_left_ccw = left.is_uvface_upwards() ^ (left.max.tup == position)
                    is_right_ccw = right.is_uvface_upwards() ^ (right.max.tup == position)
                    if is_right_ccw and not is_left_ccw and type(right) is not type(left) and right not in merged_uvedges and left not in merged_uvedges:
                        return False
                    if (not is_right_ccw and right not in merged_uvedges) ^ (is_left_ccw and left not in merged_uvedges):
                        return False
    
            # check for self-intersections
            try:
                try:
                    sweepline = QuickSweepline() if self.has_safe_geometry and other.has_safe_geometry else BruteSweepline()
                    sweep(sweepline, (uvedge for uvedge in chain(boundary_other, self.boundary)))
                    self.has_safe_geometry &= other.has_safe_geometry
                except GeometryError:
                    sweep(BruteSweepline(), (uvedge for uvedge in chain(boundary_other, self.boundary)))
                    self.has_safe_geometry = False
            except Intersection:
                return False
    
            # mark all edges that connect the islands as not cut
            for uvedge in merged_uvedges:
                uvedge.edge.is_main_cut = False
    
            # include all trasformed vertices as mine
    
            self.vertices.update(phantoms.values())
    
    
            # update the uvverts_by_id dictionary
            for source, target in phantoms.items():
                present = self.uvverts_by_id.get(target.vertex.index)
                if not present:
                    self.uvverts_by_id[target.vertex.index] = [target]
                else:
                    # emulation of set behavior... sorry, it is faster
                    if source in present:
                        present.remove(source)
                    if target not in present:
                        present.append(target)
    
            # re-link uvedges and uvfaces to their transformed locations
            for uvedge in other.edges:
                uvedge.island = self
                uvedge.va = phantoms[uvedge.va]
                uvedge.vb = phantoms[uvedge.vb]
                uvedge.update()
            if is_merged_mine:
                for uvedge in self.edges:
                    uvedge.va = phantoms.get(uvedge.va, uvedge.va)
                    uvedge.vb = phantoms.get(uvedge.vb, uvedge.vb)
            self.edges.update(other.edges)
    
            for uvface in other.faces:
                uvface.island = self
    
                uvface.vertices = [phantoms[uvvertex] for uvvertex in uvface.vertices]
                uvface.uvvertex_by_id = {
                    index: phantoms[uvvertex]
    
                    for index, uvvertex in uvface.uvvertex_by_id.items()}
                uvface.flipped ^= flipped
            if is_merged_mine:
                # there may be own uvvertices that need to be replaced by phantoms
                for uvface in self.faces:
    
                    if any(uvvertex in phantoms for uvvertex in uvface.vertices):
                        uvface.vertices = [phantoms.get(uvvertex, uvvertex) for uvvertex in uvface.vertices]
                        uvface.uvvertex_by_id = {
                            index: phantoms.get(uvvertex, uvvertex)
    
                            for index, uvvertex in uvface.uvvertex_by_id.items()}
            self.faces.extend(other.faces)
    
    
            self.boundary = [
                uvedge for uvedge in chain(self.boundary, other.boundary)
                if uvedge not in merged_uvedges]
    
    
            for uvedge, partner in merged_uvedge_pairs:
                # make sure that main faces are the ones actually merged (this changes nothing in most cases)
                uvedge.edge.main_faces[:] = uvedge.uvface.face, partner.uvface.face
    
            # everything seems to be OK
            return True
    
        def add_marker(self, marker):
    
            self.fake_vertices.extend(marker.bounds)
    
            self.markers.append(marker)
    
        def generate_label(self, label=None, abbreviation=None):
            """Assign a name to this island automatically"""
            abbr = abbreviation or self.abbreviation or str(self.number)
            # TODO: dots should be added in the last instant when outputting any text
            if is_upsidedown_wrong(abbr):
                abbr += "."
            self.label = label or self.label or "Island {}".format(self.number)
            self.abbreviation = abbr
    
        def save_uv(self, tex, cage_size):
            """Save UV Coordinates of all UVFaces to a given UV texture
            tex: UV Texture layer to use (BPy MeshUVLoopLayer struct)
            page_size: size of the page in pixels (vector)"""
            texface = tex.data
            for uvface in self.faces:
    
                for i, uvvertex in enumerate(uvface.vertices):
    
                    uv = uvvertex.co + self.pos
                    texface[uvface.face.loop_start + i].uv[0] = uv.x / cage_size.x
                    texface[uvface.face.loop_start + i].uv[1] = uv.y / cage_size.y
    
        def save_uv_separate(self, tex):
            """Save UV Coordinates of all UVFaces to a given UV texture, spanning from 0 to 1
            tex: UV Texture layer to use (BPy MeshUVLoopLayer struct)
            page_size: size of the page in pixels (vector)"""
            texface = tex.data
            scale_x, scale_y = 1 / self.bounding_box.x, 1 / self.bounding_box.y
            for uvface in self.faces:
    
                for i, uvvertex in enumerate(uvface.vertices):
    
                    texface[uvface.face.loop_start + i].uv[0] = uvvertex.co.x * scale_x
                    texface[uvface.face.loop_start + i].uv[1] = uvvertex.co.y * scale_y
    
    
    class Page:
        """Container for several Islands"""
        __slots__ = ('islands', 'name', 'image_path')
    
        def __init__(self, num=1):
            self.islands = list()
            self.name = "page{}".format(num)
            self.image_path = None
    
    
    class UVVertex:
        """Vertex in 2D"""
        __slots__ = ('co', 'vertex', 'tup')
    
        def __init__(self, vector, vertex=None):
            self.co = vector.xy
            self.vertex = vertex
            self.tup = tuple(self.co)
    
        def __repr__(self):
            if self.vertex:
                return "UV {} [{:.3f}, {:.3f}]".format(self.vertex.index, self.co.x, self.co.y)
            else:
                return "UV * [{:.3f}, {:.3f}]".format(self.co.x, self.co.y)
    
    
    class UVEdge:
        """Edge in 2D"""
        # Every UVEdge is attached to only one UVFace
        # UVEdges are doubled as needed because they both have to point clockwise around their faces
        __slots__ = ('va', 'vb', 'island', 'uvface', 'edge',
            'min', 'max', 'bottom', 'top',
            'neighbor_left', 'neighbor_right', 'sticker')
    
        def __init__(self, vertex1: UVVertex, vertex2: UVVertex, island: Island, uvface, edge):
            self.va = vertex1
            self.vb = vertex2
            self.update()
            self.island = island
            self.uvface = uvface
            self.sticker = None
            self.edge = edge
    
        def update(self):
            """Update data if UVVertices have moved"""
            self.min, self.max = (self.va, self.vb) if (self.va.tup < self.vb.tup) else (self.vb, self.va)
            y1, y2 = self.va.co.y, self.vb.co.y
            self.bottom, self.top = (y1, y2) if y1 < y2 else (y2, y1)
    
        def is_uvface_upwards(self):
            return (self.va.tup < self.vb.tup) ^ self.uvface.flipped
    
        def __repr__(self):
            return "({0.va} - {0.vb})".format(self)
    
    
    class PhantomUVEdge:
        """Temporary 2D Segment for calculations"""
        __slots__ = ('va', 'vb', 'min', 'max', 'bottom', 'top')
    
        def __init__(self, vertex1: UVVertex, vertex2: UVVertex, flip):
            self.va, self.vb = (vertex2, vertex1) if flip else (vertex1, vertex2)
            self.min, self.max = (self.va, self.vb) if (self.va.tup < self.vb.tup) else (self.vb, self.va)
            y1, y2 = self.va.co.y, self.vb.co.y
            self.bottom, self.top = (y1, y2) if y1 < y2 else (y2, y1)
    
        def is_uvface_upwards(self):
            return self.va.tup < self.vb.tup
    
        def __repr__(self):
            return "[{0.va} - {0.vb}]".format(self)
    
    
    class UVFace:
        """Face in 2D"""
    
        __slots__ = ('vertices', 'edges', 'face', 'island', 'flipped', 'uvvertex_by_id')
    
    
        def __init__(self, face: Face, island: Island):
            """Creace an UVFace from a Face and a fixed edge.
            face: Face to take coordinates from
            island: Island to register itself in
            fixed_edge: Edge to connect to (that already has UV coordinates)"""
    
            self.vertices = list()
    
            self.face = face
            face.uvface = self
            self.island = island
            self.flipped = False  # a flipped UVFace has edges clockwise
    
            rot = z_up_matrix(face.normal)
    
            self.uvvertex_by_id = dict()  # link vertex id -> UVVertex
            for vertex in face.vertices:
                uvvertex = UVVertex(rot * vertex.co, vertex)
                self.vertices.append(uvvertex)
                self.uvvertex_by_id[vertex.index] = uvvertex
    
            self.edges = list()
            edge_by_verts = dict()
            for edge in face.edges:
                edge_by_verts[(edge.va.index, edge.vb.index)] = edge
                edge_by_verts[(edge.vb.index, edge.va.index)] = edge
    
            for va, vb in pairs(self.vertices):
    
                edge = edge_by_verts[(va.vertex.index, vb.vertex.index)]
                uvedge = UVEdge(va, vb, island, self, edge)
                self.edges.append(uvedge)
    
                edge.uvedges.append(uvedge)  # FIXME: editing foreign attribute
    
    
    
    class Arrow:
        """Mark in the document: an arrow denoting the number of the edge it points to"""
        __slots__ = ('bounds', 'center', 'rot', 'text', 'size')
    
        def __init__(self, uvedge, size, index):
            self.text = str(index)
            edge = (uvedge.vb.co - uvedge.va.co) if not uvedge.uvface.flipped else (uvedge.va.co - uvedge.vb.co)
            self.center = (uvedge.va.co + uvedge.vb.co) / 2
            self.size = size
            tangent = edge.normalized()
    
            cos, sin = tangent
            self.rot = M.Matrix(((cos, -sin), (sin, cos)))
            normal = M.Vector((sin, -cos))
    
            self.bounds = [self.center, self.center + (1.2*normal + tangent)*size, self.center + (1.2*normal - tangent)*size]
    
    
    class Sticker:
        """Mark in the document: sticker tab"""
        __slots__ = ('bounds', 'center', 'rot', 'text', 'width', 'vertices')
    
        def __init__(self, uvedge, default_width=0.005, index=None, target_island=None):
            """Sticker is directly attached to the given UVEdge"""
            first_vertex, second_vertex = (uvedge.va, uvedge.vb) if not uvedge.uvface.flipped else (uvedge.vb, uvedge.va)
            edge = first_vertex.co - second_vertex.co
            sticker_width = min(default_width, edge.length / 2)
            other = uvedge.edge.other_uvedge(uvedge)  # This is the other uvedge - the sticking target
    
            other_first, other_second = (other.va, other.vb) if not other.uvface.flipped else (other.vb, other.va)
            other_edge = other_second.co - other_first.co
    
            # angle a is at vertex uvedge.va, b is at uvedge.vb
            cos_a = cos_b = 0.5
            sin_a = sin_b = 0.75**0.5
            # len_a is length of the side adjacent to vertex a, len_b likewise
            len_a = len_b = sticker_width / sin_a
    
            # fix overlaps with the most often neighbour - its sticking target
            if first_vertex == other_second:
                cos_a = max(cos_a, (edge*other_edge) / (edge.length**2))  # angles between pi/3 and 0
            elif second_vertex == other_first:
                cos_b = max(cos_b, (edge*other_edge) / (edge.length**2))  # angles between pi/3 and 0
    
            # Fix tabs for sticking targets with small angles
            # Index of other uvedge in its face (not in its island)
            other_idx = other.uvface.edges.index(other)
            # Left and right neighbors in the face
            other_face_neighbor_left = other.uvface.edges[(other_idx+1) % len(other.uvface.edges)]
            other_face_neighbor_right = other.uvface.edges[(other_idx-1) % len(other.uvface.edges)]
            other_edge_neighbor_a = other_face_neighbor_left.vb.co - other.vb.co
            other_edge_neighbor_b = other_face_neighbor_right.va.co - other.va.co
            # Adjacent angles in the face
            cos_a = max(cos_a, (-other_edge*other_edge_neighbor_a) / (other_edge.length*other_edge_neighbor_a.length))
            cos_b = max(cos_b, (other_edge*other_edge_neighbor_b) / (other_edge.length*other_edge_neighbor_b.length))
    
            # Calculate the lengths of the glue tab edges using the possibly smaller angles
            sin_a = abs(1 - cos_a**2)**0.5
    
            len_b = min(len_a, (edge.length * sin_a) / (sin_a * cos_b + sin_b * cos_a))
    
            len_a = 0 if sin_a == 0 else min(sticker_width / sin_a, (edge.length - len_b*cos_b) / cos_a)
    
            sin_b = abs(1 - cos_b**2)**0.5
    
            len_a = min(len_a, (edge.length * sin_b) / (sin_a * cos_b + sin_b * cos_a))
            len_b = 0 if sin_b == 0 else min(sticker_width / sin_b, (edge.length - len_a * cos_a) / cos_b)
    
            v3 = UVVertex(second_vertex.co + M.Matrix(((cos_b, -sin_b), (sin_b, cos_b))) * edge * len_b / edge.length)
    
            v4 = UVVertex(first_vertex.co + M.Matrix(((-cos_a, -sin_a), (sin_a, -cos_a))) * edge * len_a / edge.length)
            if v3.co != v4.co:
                self.vertices = [second_vertex, v3, v4, first_vertex]
            else:
                self.vertices = [second_vertex, v3, first_vertex]
    
            sin, cos = edge.y / edge.length, edge.x / edge.length
            self.rot = M.Matrix(((cos, -sin), (sin, cos)))
            self.width = sticker_width * 0.9
            if index and target_island is not uvedge.island:
                self.text = "{}:{}".format(target_island.abbreviation, index)
            else:
                self.text = index
            self.center = (uvedge.va.co + uvedge.vb.co) / 2 + self.rot*M.Vector((0, self.width*0.2))
            self.bounds = [v3.co, v4.co, self.center] if v3.co != v4.co else [v3.co, self.center]
    
    
    class NumberAlone:
        """Mark in the document: numbering inside the island denoting edges to be sticked"""
        __slots__ = ('bounds', 'center', 'rot', 'text', 'size')
    
        def __init__(self, uvedge, index, default_size=0.005):
            """Sticker is directly attached to the given UVEdge"""
            edge = (uvedge.va.co - uvedge.vb.co) if not uvedge.uvface.flipped else (uvedge.vb.co - uvedge.va.co)
    
            self.size = default_size
            sin, cos = edge.y / edge.length, edge.x / edge.length
            self.rot = M.Matrix(((cos, -sin), (sin, cos)))
            self.text = index
            self.center = (uvedge.va.co + uvedge.vb.co) / 2 - self.rot*M.Vector((0, self.size*1.2))
            self.bounds = [self.center]
    
    
    class SVG:
        """Simple SVG exporter"""
    
        def __init__(self, page_size: M.Vector, style, margin, pure_net=True, angle_epsilon=0.01):
            """Initialize document settings.
            page_size: document dimensions in meters
            pure_net: if True, do not use image"""
            self.page_size = page_size
            self.pure_net = pure_net
            self.style = style
            self.margin = margin
            self.text_size = 12
            self.angle_epsilon = angle_epsilon
    
        @classmethod
        def encode_image(cls, bpy_image):
            import tempfile
            import base64
            with tempfile.TemporaryDirectory() as directory:
                filename = directory + "/i.png"
                bpy_image.filepath_raw = filename
                bpy_image.save()
                return base64.encodebytes(open(filename, "rb").read()).decode('ascii')
    
        def format_vertex(self, vector, pos=M.Vector((0, 0))):
            """Return a string with both coordinates of the given vertex."""
            x, y = vector + pos
            return "{:.6f} {:.6f}".format((x + self.margin) * 1000, (self.page_size.y - y - self.margin) * 1000)
    
        def write(self, mesh, filename):
            """Write data to a file given by its name."""
            line_through = " L ".join  # used for formatting of SVG path data
            rows = "\n".join
    
            dl = ["{:.2f}".format(length * self.style.line_width * 1000) for length in (2, 5, 10)]
    
            format_style = {
                'SOLID': "none", 'DOT': "{0},{1}".format(*dl), 'DASH': "{1},{2}".format(*dl),
                'LONGDASH': "{2},{1}".format(*dl), 'DASHDOT': "{2},{1},{0},{1}".format(*dl)}
    
    
            def format_color(vec):
                return "#{:02x}{:02x}{:02x}".format(round(vec[0] * 255), round(vec[1] * 255), round(vec[2] * 255))
    
            def format_matrix(matrix):
                return " ".join("{:.6f}".format(cell) for column in matrix for cell in column)
    
            def path_convert(string, relto=os_path.dirname(filename)):
                assert(os_path)  # check the module was imported
                string = os_path.relpath(string, relto)
                if os_path.sep != '/':
                    string = string.replace(os_path.sep, '/')
                return string
    
    
            styleargs = {
                name: format_color(getattr(self.style, name)) for name in (
                    "outer_color", "outbg_color", "convex_color", "concave_color", "freestyle_color",
                    "inbg_color", "sticker_fill", "text_color")}
            styleargs.update({
                name: format_style[getattr(self.style, name)] for name in
    
                ("outer_style", "convex_style", "concave_style", "freestyle_style")})
    
            styleargs.update({
                name: getattr(self.style, attr)[3] for name, attr in (
                    ("outer_alpha", "outer_color"), ("outbg_alpha", "outbg_color"),
                    ("convex_alpha", "convex_color"), ("concave_alpha", "concave_color"),
                    ("freestyle_alpha", "freestyle_color"),
                    ("inbg_alpha", "inbg_color"), ("sticker_alpha", "sticker_fill"),
                    ("text_alpha", "text_color"))})
            styleargs.update({
                name: getattr(self.style, name) * self.style.line_width * 1000 for name in
    
                ("outer_width", "convex_width", "concave_width", "freestyle_width", "outbg_width", "inbg_width")})
            for num, page in enumerate(mesh.pages):
                page_filename = "{}_{}.svg".format(filename[:filename.rfind(".svg")], page.name) if len(mesh.pages) > 1 else filename
                with open(page_filename, 'w') as f:
                    print(self.svg_base.format(width=self.page_size.x*1000, height=self.page_size.y*1000), file=f)
                    print(self.css_base.format(**styleargs), file=f)
                    if page.image_path:
    
                        print(
                            self.image_linked_tag.format(
                                pos="{0:.6f} {0:.6f}".format(self.margin*1000),
                                width=(self.page_size.x - 2 * self.margin)*1000,
                                height=(self.page_size.y - 2 * self.margin)*1000,
                                path=path_convert(page.image_path)),
    
                            file=f)
                    if len(page.islands) > 1:
                        print("<g>", file=f)
    
                    for island in page.islands:
                        print("<g>", file=f)
                        if island.image_path:
    
                            print(
                                self.image_linked_tag.format(
                                    pos=self.format_vertex(island.pos + M.Vector((0, island.bounding_box.y))),
                                    width=island.bounding_box.x*1000,
                                    height=island.bounding_box.y*1000,
                                    path=path_convert(island.image_path)),
    
                                file=f)
                        elif island.embedded_image:
    
                            print(
                                self.image_embedded_tag.format(
    
                                    pos=self.format_vertex(island.pos + M.Vector((0, island.bounding_box.y))),
                                    width=island.bounding_box.x*1000,
                                    height=island.bounding_box.y*1000,
                                    path=island.image_path),
                                island.embedded_image, "'/>",
                                file=f, sep="")
                        if island.title:
    
                            print(
                                self.text_tag.format(
                                    size=1000 * self.text_size,
                                    x=1000 * (island.bounding_box.x*0.5 + island.pos.x + self.margin),
                                    y=1000 * (self.page_size.y - island.pos.y - self.margin - 0.2 * self.text_size),
                                    label=island.title),
                                file=f)
    
    
                        data_markers, data_stickerfill, data_outer, data_convex, data_concave, data_freestyle = (list() for i in range(6))
                        for marker in island.markers:
                            if isinstance(marker, Sticker):
                                data_stickerfill.append("M {} Z".format(
                                    line_through(self.format_vertex(vertex.co, island.pos) for vertex in marker.vertices)))
                                if marker.text:
                                    data_markers.append(self.text_transformed_tag.format(
                                        label=marker.text,
                                        pos=self.format_vertex(marker.center, island.pos),
                                        mat=format_matrix(marker.rot),
                                        size=marker.width * 1000))
                            elif isinstance(marker, Arrow):
                                size = marker.size * 1000
                                position = marker.center + marker.rot*marker.size*M.Vector((0, -0.9))
                                data_markers.append(self.arrow_marker_tag.format(
                                    index=marker.text,
                                    arrow_pos=self.format_vertex(marker.center, island.pos),
                                    scale=size,
                                    pos=self.format_vertex(position, island.pos - marker.size*M.Vector((0, 0.4))),
                                    mat=format_matrix(size * marker.rot)))
                            elif isinstance(marker, NumberAlone):
                                data_markers.append(self.text_transformed_tag.format(
                                    label=marker.text,
                                    pos=self.format_vertex(marker.center, island.pos),
                                    mat=format_matrix(marker.rot),
                                    size=marker.size * 1000))
                        if data_stickerfill and self.style.sticker_fill[3] > 0:
                            print("<path class='sticker' d='", rows(data_stickerfill), "'/>", file=f)
    
                        outer_edges = set(island.boundary)
                        while outer_edges:
                            data_loop = list()
                            uvedge = outer_edges.pop()
                            while 1:
                                if uvedge.sticker:
                                    data_loop.extend(self.format_vertex(vertex.co, island.pos) for vertex in uvedge.sticker.vertices[1:])
                                else:
                                    vertex = uvedge.vb if uvedge.uvface.flipped else uvedge.va
                                    data_loop.append(self.format_vertex(vertex.co, island.pos))
                                uvedge = uvedge.neighbor_right
                                try:
                                    outer_edges.remove(uvedge)
                                except KeyError:
                                    break
                            data_outer.append("M {} Z".format(line_through(data_loop)))
    
                        for uvedge in island.edges:
                            edge = uvedge.edge
                            if edge.is_cut(uvedge.uvface.face) and not uvedge.sticker:
                                continue
                            data_uvedge = "M {}".format(
                                line_through(self.format_vertex(vertex.co, island.pos) for vertex in (uvedge.va, uvedge.vb)))
                            if edge.freestyle:
                                data_freestyle.append(data_uvedge)
                            # each uvedge is in two opposite-oriented variants; we want to add each only once
                            if uvedge.sticker or uvedge.uvface.flipped != (uvedge.va.vertex.index > uvedge.vb.vertex.index):
                                if edge.angle > self.angle_epsilon:
                                    data_convex.append(data_uvedge)
                                elif edge.angle < -self.angle_epsilon:
                                    data_concave.append(data_uvedge)
                        if island.is_inside_out:
                            data_convex, data_concave = data_concave, data_convex
    
                        if data_freestyle:
                            print("<path class='freestyle' d='", rows(data_freestyle), "'/>", file=f)
                        if (data_convex or data_concave) and not self.pure_net and self.style.use_inbg:
                            print("<path class='inner_background' d='", rows(data_convex + data_concave), "'/>", file=f)
                        if data_convex:
                            print("<path class='convex' d='", rows(data_convex), "'/>", file=f)
                        if data_concave:
                            print("<path class='concave' d='", rows(data_concave), "'/>", file=f)
                        if data_outer:
                            if not self.pure_net and self.style.use_outbg:
                                print("<path class='outer_background' d='", rows(data_outer), "'/>", file=f)
                            print("<path class='outer' d='", rows(data_outer), "'/>", file=f)
                        if data_markers:
                            print(rows(data_markers), file=f)
                        print("</g>", file=f)
    
                    if len(page.islands) > 1:
                        print("</g>", file=f)
                    print("</svg>", file=f)
    
        image_linked_tag = "<image transform='translate({pos})' width='{width:.6f}' height='{height:.6f}' xlink:href='{path}'/>"
        image_embedded_tag = "<image transform='translate({pos})' width='{width:.6f}' height='{height:.6f}' xlink:href='data:image/png;base64,"
        text_tag = "<text transform='translate({x} {y})' style='font-size:{size:.2f}'><tspan>{label}</tspan></text>"
        text_transformed_tag = "<text transform='matrix({mat} {pos})' style='font-size:{size:.2f}'><tspan>{label}</tspan></text>"
        arrow_marker_tag = "<g><path transform='matrix({mat} {arrow_pos})' class='arrow' d='M 0 0 L 1 1 L 0 0.25 L -1 1 Z'/>" \
            "<text transform='translate({pos})' style='font-size:{scale:.2f}'><tspan>{index}</tspan></text></g>"
    
        svg_base = """<?xml version='1.0' encoding='UTF-8' standalone='no'?>
        <svg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' version='1.1'
        width='{width:.2f}mm' height='{height:.2f}mm' viewBox='0 0 {width:.2f} {height:.2f}'>"""
    
        css_base = """<style type="text/css">
        path {{
            fill: none;
            stroke-linecap: butt;
            stroke-linejoin: bevel;
            stroke-dasharray: none;
        }}
        path.outer {{
            stroke: {outer_color};
            stroke-dasharray: {outer_style};
            stroke-dashoffset: 0;
            stroke-width: {outer_width:.2};
            stroke-opacity: {outer_alpha:.2};
        }}
        path.convex {{
            stroke: {convex_color};
            stroke-dasharray: {convex_style};
            stroke-dashoffset:0;
            stroke-width:{convex_width:.2};
            stroke-opacity: {convex_alpha:.2}
        }}
        path.concave {{
            stroke: {concave_color};
            stroke-dasharray: {concave_style};
            stroke-dashoffset: 0;
            stroke-width: {concave_width:.2};
            stroke-opacity: {concave_alpha:.2}
        }}
        path.freestyle {{
            stroke: {freestyle_color};
            stroke-dasharray: {freestyle_style};
            stroke-dashoffset: 0;
            stroke-width: {freestyle_width:.2};
            stroke-opacity: {freestyle_alpha:.2}
        }}
        path.outer_background {{
            stroke: {outbg_color};
            stroke-opacity: {outbg_alpha};
            stroke-width: {outbg_width:.2}
        }}
        path.inner_background {{
            stroke: {inbg_color};
            stroke-opacity: {inbg_alpha};
            stroke-width: {inbg_width:.2}
        }}
        path.sticker {{
            fill: {sticker_fill};
            stroke: none;
            fill-opacity: {sticker_alpha:.2};
        }}
        path.arrow {{
            fill: #000;
        }}
        text {{
            font-style: normal;
            fill: {text_color};
            fill-opacity: {text_alpha:.2};
            stroke: none;
        }}
        text, tspan {{
            text-anchor:middle;
        }}
        </style>"""
    
    
    class PDF:
        """Simple PDF exporter"""
    
        mm_to_pt = 72 / 25.4
    
        character_width_packed = {
            191: "'", 222: 'ijl\x82\x91\x92', 278: '\x00\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x0c\r\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f !,./:;I[\\]ft\xa0·ÌÍÎÏìíîï',
            333: '()-`r\x84\x88\x8b\x93\x94\x98\x9b¡¨\xad¯²³´¸¹{}', 350: '\x7f\x81\x8d\x8f\x90\x95\x9d', 365: '"ºª*°', 469: '^', 500: 'Jcksvxyz\x9a\x9eçýÿ', 584: '¶+<=>~¬±×÷', 611: 'FTZ\x8e¿ßø',
            667: '&ABEKPSVXY\x8a\x9fÀÁÂÃÄÅÈÉÊËÝÞ', 722: 'CDHNRUwÇÐÑÙÚÛÜ', 737: '©®', 778: 'GOQÒÓÔÕÖØ', 833: 'Mm¼½¾', 889: '', 944: 'W\x9c', 1000: '\x85\x89\x8c\x97\x99Æ', 1015: '@', }
        character_width = {c: value for (value, chars) in character_width_packed.items() for c in chars}
    
    
        def __init__(self, page_size: M.Vector, style, margin, pure_net=True, angle_epsilon=0.01):
            self.page_size = page_size
            self.style = style
            self.margin = M.Vector((margin, margin))
            self.pure_net = pure_net
            self.angle_epsilon = angle_epsilon
    
        def text_width(self, text, scale=None):
            return (scale or self.text_size) * sum(self.character_width.get(c, 556) for c in text) / 1000
    
        @classmethod
        def encode_image(cls, bpy_image):
            data = bytes(int(255 * px) for (i, px) in enumerate(bpy_image.pixels) if i % 4 != 3)
    
            image = {
                "Type": "XObject", "Subtype": "Image", "Width": bpy_image.size[0], "Height": bpy_image.size[1],
                "ColorSpace": "DeviceRGB", "BitsPerComponent": 8, "Interpolate": True,
                "Filter": ["ASCII85Decode", "FlateDecode"], "stream": data}
    
            return image
    
        def write(self, mesh, filename):
            def format_dict(obj, refs=tuple()):
                return "<< " + "".join("/{} {}\n".format(key, format_value(value, refs)) for (key, value) in obj.items()) + ">>"
    
            def line_through(seq):
                return "".join("{0.x:.6f} {0.y:.6f} {1} ".format(1000*v.co, c) for (v, c) in zip(seq, chain("m", repeat("l"))))
    
            def format_value(value, refs=tuple()):
                if value in refs:
                    return "{} 0 R".format(refs.index(value) + 1)
                elif type(value) is dict:
                    return format_dict(value, refs)
                elif type(value) in (list, tuple):
                    return "[ " + " ".join(format_value(item, refs) for item in value) + " ]"
                elif type(value) is int:
                    return str(value)
                elif type(value) is float:
                    return "{:.6f}".format(value)
                elif type(value) is bool:
                    return "true" if value else "false"
                else:
                    return "/{}".format(value)  # this script can output only PDF names, no strings
    
            def write_object(index, obj, refs, f, stream=None):
                byte_count = f.write("{} 0 obj\n".format(index))
                if type(obj) is not dict:
                    stream, obj = obj, dict()
                elif "stream" in obj:
                    stream = obj.pop("stream")
                if stream:
                    if True or type(stream) is bytes:
                        obj["Filter"] = ["ASCII85Decode", "FlateDecode"]
                        stream = encode(stream)
                    obj["Length"] = len(stream)
                byte_count += f.write(format_dict(obj, refs))
                if stream:
                    byte_count += f.write("\nstream\n")
                    byte_count += f.write(stream)
                    byte_count += f.write("\nendstream")
                return byte_count + f.write("\nendobj\n")
    
            def encode(data):
                from base64 import a85encode
                from zlib import compress
                if hasattr(data, "encode"):
                    data = data.encode()
                return a85encode(compress(data), adobe=True, wrapcol=250)[2:].decode()
    
            page_size_pt = 1000 * self.mm_to_pt * self.page_size
            root = {"Type": "Pages", "MediaBox": [0, 0, page_size_pt.x, page_size_pt.y], "Kids": list()}
            catalog = {"Type": "Catalog", "Pages": root}
    
            font = {
                "Type": "Font", "Subtype": "Type1", "Name": "F1",
                "BaseFont": "Helvetica", "Encoding": "MacRomanEncoding"}
    
    
            dl = [length * self.style.line_width * 1000 for length in (1, 4, 9)]
    
            format_style = {
                'SOLID': list(), 'DOT': [dl[0], dl[1]], 'DASH': [dl[1], dl[2]],
                'LONGDASH': [dl[2], dl[1]], 'DASHDOT': [dl[2], dl[1], dl[0], dl[1]]}
    
            styles = {
                "Gtext": {"ca": self.style.text_color[3], "Font": [font, 1000 * self.text_size]},
                "Gsticker": {"ca": self.style.sticker_fill[3]}}
            for name in ("outer", "convex", "concave", "freestyle"):
                gs = {
                    "LW": self.style.line_width * 1000 * getattr(self.style, name + "_width"),
                    "CA": getattr(self.style, name + "_color")[3],
                    "D": [format_style[getattr(self.style, name + "_style")], 0]}
                styles["G" + name] = gs
            for name in ("outbg", "inbg"):
                gs = {
                    "LW": self.style.line_width * 1000 * getattr(self.style, name + "_width"),
                    "CA": getattr(self.style, name + "_color")[3],
                    "D": [format_style['SOLID'], 0]}
                styles["G" + name] = gs
    
            objects = [root, catalog, font]
            objects.extend(styles.values())
    
            for page in mesh.pages:
                commands = ["{0:.6f} 0 0 {0:.6f} 0 0 cm".format(self.mm_to_pt)]
                resources = {"Font": {"F1": font}, "ExtGState": styles, "XObject": dict()}
                for island in page.islands:
                    commands.append("q 1 0 0 1 {0.x:.6f} {0.y:.6f} cm".format(1000*(self.margin + island.pos)))
                    if island.embedded_image:
                        identifier = "Im{}".format(len(resources["XObject"]) + 1)
    
                        commands.append(self.command_image.format(1000 * island.bounding_box, identifier))
    
                        objects.append(island.embedded_image)
                        resources["XObject"][identifier] = island.embedded_image
    
                    if island.title:
    
                        commands.append(self.command_label.format(
    
                            size=1000*self.text_size,
                            x=500 * (island.bounding_box.x - self.text_width(island.title)),
                            y=1000 * 0.2 * self.text_size,
                            label=island.title))
    
                    data_markers, data_stickerfill, data_outer, data_convex, data_concave, data_freestyle = (list() for i in range(6))
                    for marker in island.markers:
                        if isinstance(marker, Sticker):
                            data_stickerfill.append(line_through(marker.vertices) + "f")
                            if marker.text:
    
                                data_markers.append(self.command_sticker.format(
    
                                    label=marker.text,
                                    pos=1000*marker.center,
                                    mat=marker.rot,
                                    align=-500 * self.text_width(marker.text, marker.width),
                                    size=1000*marker.width))
                        elif isinstance(marker, Arrow):
                            size = 1000 * marker.size
                            position = 1000 * (marker.center + marker.rot*marker.size*M.Vector((0, -0.9)))
    
                            data_markers.append(self.command_arrow.format(
    
                                index=marker.text,
                                arrow_pos=1000 * marker.center,
                                pos=position - 1000 * M.Vector((0.5 * self.text_width(marker.text), 0.4 * self.text_size)),
                                mat=size * marker.rot,
                                size=size))
                        elif isinstance(marker, NumberAlone):
    
                            data_markers.append(self.command_number.format(
    
                                label=marker.text,
                                pos=1000*marker.center,
    
                                size=1000*marker.size))
    
                    outer_edges = set(island.boundary)
                    while outer_edges:
                        data_loop = list()
                        uvedge = outer_edges.pop()
                        while 1:
                            if uvedge.sticker:
                                data_loop.extend(uvedge.sticker.vertices[1:])
                            else:
                                vertex = uvedge.vb if uvedge.uvface.flipped else uvedge.va
                                data_loop.append(vertex)
                            uvedge = uvedge.neighbor_right
                            try:
                                outer_edges.remove(uvedge)
                            except KeyError:
                                break
                        data_outer.append(line_through(data_loop) + "s")
    
                    for uvedge in island.edges:
                        edge = uvedge.edge
                        if edge.is_cut(uvedge.uvface.face) and not uvedge.sticker:
                            continue
                        data_uvedge = line_through((uvedge.va, uvedge.vb)) + "S"
                        if edge.freestyle:
                            data_freestyle.append(data_uvedge)
                        # each uvedge is in two opposite-oriented variants; we want to add each only once
                        if uvedge.sticker or uvedge.uvface.flipped != (uvedge.va.vertex.index > uvedge.vb.vertex.index):
                            if edge.angle > self.angle_epsilon:
                                data_convex.append(data_uvedge)
                            elif edge.angle < -self.angle_epsilon:
                                data_concave.append(data_uvedge)
                    if island.is_inside_out:
                        data_convex, data_concave = data_concave, data_convex
    
                    if data_stickerfill and self.style.sticker_fill[3] > 0:
                        commands.append("/Gsticker gs {0[0]:.3f} {0[1]:.3f} {0[2]:.3f} rg".format(self.style.sticker_fill))
                        commands.extend(data_stickerfill)
                    if data_freestyle:
                        commands.append("/Gfreestyle gs {0[0]:.3f} {0[1]:.3f} {0[2]:.3f} RG".format(self.style.freestyle_color))
                        commands.extend(data_freestyle)
                    if (data_convex or data_concave) and not self.pure_net and self.style.use_inbg:
                        commands.append("/Ginbg gs {0[0]:.3f} {0[1]:.3f} {0[2]:.3f} RG".format(self.style.inbg_color))
                        commands.extend(chain(data_convex, data_concave))
                    if data_convex:
                        commands.append("/Gconvex gs {0[0]:.3f} {0[1]:.3f} {0[2]:.3f} RG".format(self.style.convex_color))
                        commands.extend(data_convex)
                    if data_concave:
                        commands.append("/Gconcave gs {0[0]:.3f} {0[1]:.3f} {0[2]:.3f} RG".format(self.style.concave_color))
                        commands.extend(data_concave)
                    if data_outer:
                        if not self.pure_net and self.style.use_outbg:
                            commands.append("/Goutbg gs {0[0]:.3f} {0[1]:.3f} {0[2]:.3f} RG".format(self.style.outbg_color))
                            commands.extend(data_outer)
                        commands.append("/Gouter gs {0[0]:.3f} {0[1]:.3f} {0[2]:.3f} RG".format(self.style.outer_color))
                        commands.extend(data_outer)
                    commands.append("/Gtext gs {0[0]:.3f} {0[1]:.3f} {0[2]:.3f} rg".format(self.style.text_color))
                    commands.extend(data_markers)
                    commands.append("Q")
                content = "\n".join(commands)
                page = {"Type": "Page", "Parent": root, "Contents": content, "Resources": resources}
                root["Kids"].append(page)
                objects.extend((page, content))
    
            root["Count"] = len(root["Kids"])
            with open(filename, "w+") as f:
                xref_table = list()
                position = f.write("%PDF-1.4\n")
                for index, obj in enumerate(objects, 1):
                    xref_table.append(position)
                    position += write_object(index, obj, objects, f)
                xref_pos = position
                f.write("xref_table\n0 {}\n".format(len(xref_table) + 1))
                f.write("{:010} {:05} f\n".format(0, 65536))
                for position in xref_table:
                    f.write("{:010} {:05} n\n".format(position, 0))
                f.write("trailer\n")
                f.write(format_dict({"Size": len(xref_table), "Root": catalog}, objects))
                f.write("\nstartxref\n{}\n%%EOF\n".format(xref_pos))
    
    
        command_label = "/Gtext gs BT {x:.6f} {y:.6f} Td ({label}) Tj ET"
        command_image = "q {0.x:.6f} 0 0 {0.y:.6f} 0 0 cm 1 0 0 -1 0 1 cm /{1} Do Q"
        command_sticker = "q {mat[0][0]:.6f} {mat[1][0]:.6f} {mat[0][1]:.6f} {mat[1][1]:.6f} {pos.x:.6f} {pos.y:.6f} cm BT {align:.6f} 0 Td /F1 {size:.6f} Tf ({label}) Tj ET Q"
        command_arrow = "q BT {pos.x:.6f} {pos.y:.6f} Td /F1 {size:.6f} Tf ({index}) Tj ET {mat[0][0]:.6f} {mat[1][0]:.6f} {mat[0][1]:.6f} {mat[1][1]:.6f} {arrow_pos.x:.6f} {arrow_pos.y:.6f} cm 0 0 m 1 -1 l 0 -0.25 l -1 -1 l f Q"
        command_number = "q {mat[0][0]:.6f} {mat[1][0]:.6f} {mat[0][1]:.6f} {mat[1][1]:.6f} {pos.x:.6f} {pos.y:.6f} cm BT /F1 {size:.6f} Tf ({label}) Tj ET Q"
    
    
    
    class Unfold(bpy.types.Operator):
        """Blender Operator: unfold the selected object."""
    
        bl_idname = "mesh.unfold"
        bl_label = "Unfold"
        bl_description = "Mark seams so that the mesh can be exported as a paper model"
        bl_options = {'REGISTER', 'UNDO'}
    
        edit: bpy.props.BoolProperty(default=False, options={'HIDDEN'})
        priority_effect_convex: bpy.props.FloatProperty(
    
            name="Priority Convex", description="Priority effect for edges in convex angles",
    
            default=default_priority_effect['CONVEX'], soft_min=-1, soft_max=10, subtype='FACTOR')
    
        priority_effect_concave: bpy.props.FloatProperty(
    
            name="Priority Concave", description="Priority effect for edges in concave angles",
    
            default=default_priority_effect['CONCAVE'], soft_min=-1, soft_max=10, subtype='FACTOR')
    
        priority_effect_length: bpy.props.FloatProperty(
    
            name="Priority Length", description="Priority effect of edge length",
    
            default=default_priority_effect['LENGTH'], soft_min=-10, soft_max=1, subtype='FACTOR')
    
        do_create_uvmap: bpy.props.BoolProperty(
    
            name="Create UVMap", description="Create a new UV Map showing the islands and page layout", default=False)
    
        object = None
    
        @classmethod
        def poll(cls, context):
            return context.active_object and context.active_object.type == "MESH"
    
        def draw(self, context):
            layout = self.layout
            col = layout.column()
            col.active = not self.object or len(self.object.data.uv_textures) < 8
            col.prop(self.properties, "do_create_uvmap")
            layout.label(text="Edge Cutting Factors:")
            col = layout.column(align=True)
            col.label(text="Face Angle:")
            col.prop(self.properties, "priority_effect_convex", text="Convex")
            col.prop(self.properties, "priority_effect_concave", text="Concave")
            layout.prop(self.properties, "priority_effect_length", text="Edge Length")
    
        def execute(self, context):
            sce = bpy.context.scene
            settings = sce.paper_model
            recall_mode = context.object.mode
            bpy.ops.object.mode_set(mode='OBJECT')
            recall_display_islands, sce.paper_model.display_islands = sce.paper_model.display_islands, False
    
            self.object = context.active_object
            mesh = self.object.data
    
            cage_size = M.Vector((settings.output_size_x, settings.output_size_y)) if settings.limit_by_page else None
    
            priority_effect = {
                'CONVEX': self.priority_effect_convex,
                'CONCAVE': self.priority_effect_concave,
                'LENGTH': self.priority_effect_length}
    
            try:
                unfolder = Unfolder(self.object)
    
                unfolder.prepare(
                    cage_size, self.do_create_uvmap, mark_seams=True,
                    priority_effect=priority_effect, scale=sce.unit_settings.scale_length/settings.scale)
    
            except UnfoldError as error:
                self.report(type={'ERROR_INVALID_INPUT'}, message=error.args[0])
                bpy.ops.object.mode_set(mode=recall_mode)
                sce.paper_model.display_islands = recall_display_islands
                return {'CANCELLED'}
            if mesh.paper_island_list:
                unfolder.copy_island_names(mesh.paper_island_list)
    
            island_list = mesh.paper_island_list
            attributes = {item.label: (item.abbreviation, item.auto_label, item.auto_abbrev) for item in island_list}
            island_list.clear()  # remove previously defined islands
            for island in unfolder.mesh.islands:
                # add islands to UI list and set default descriptions
                list_item = island_list.add()
                # add faces' IDs to the island
                for uvface in island.faces:
                    lface = list_item.faces.add()
                    lface.id = uvface.face.index
    
                list_item["label"] = island.label
    
                list_item["abbreviation"], list_item["auto_label"], list_item["auto_abbrev"] = attributes.get(
                    island.label,
                    (island.abbreviation, True, True))
    
                island_item_changed(list_item, context)
    
            mesh.paper_island_index = -1
            mesh.show_edge_seams = True
    
            bpy.ops.object.mode_set(mode=recall_mode)
            sce.paper_model.display_islands = recall_display_islands
            return {'FINISHED'}
    
    
    class ClearAllSeams(bpy.types.Operator):
        """Blender Operator: clear all seams of the active Mesh and all its unfold data"""
    
        bl_idname = "mesh.clear_all_seams"
        bl_label = "Clear All Seams"
        bl_description = "Clear all the seams and unfolded islands of the active object"
    
        @classmethod
        def poll(cls, context):
            return context.active_object and context.active_object.type == 'MESH'