Skip to content
Snippets Groups Projects
io_export_paper_model.py 122 KiB
Newer Older
  • Learn to ignore specific revisions
  • 
        def execute(self, context):
            ob = context.active_object
            mesh = ob.data
    
            for edge in mesh.edges:
                edge.use_seam = False
            mesh.paper_island_list.clear()
    
            return {'FINISHED'}
    
    
    def page_size_preset_changed(self, context):
        """Update the actual document size to correct values"""
    
        if hasattr(self, "limit_by_page") and not self.limit_by_page:
            return
    
        if self.page_size_preset == 'A4':
            self.output_size_x = 0.210
            self.output_size_y = 0.297
        elif self.page_size_preset == 'A3':
            self.output_size_x = 0.297
            self.output_size_y = 0.420
        elif self.page_size_preset == 'US_LETTER':
            self.output_size_x = 0.216
            self.output_size_y = 0.279
        elif self.page_size_preset == 'US_LEGAL':
            self.output_size_x = 0.216
            self.output_size_y = 0.356
    
    
    class PaperModelStyle(bpy.types.PropertyGroup):
        line_styles = [
            ('SOLID', "Solid (----)", "Solid line"),
            ('DOT', "Dots (. . .)", "Dotted line"),
            ('DASH', "Short Dashes (- - -)", "Solid line"),
            ('LONGDASH', "Long Dashes (-- --)", "Solid line"),
            ('DASHDOT', "Dash-dotted (-- .)", "Solid line")
        ]
    
        outer_color: bpy.props.FloatVectorProperty(
    
            name="Outer Lines", description="Color of net outline",
    
            default=(0.0, 0.0, 0.0, 1.0), min=0, max=1, subtype='COLOR', size=4)
    
        outer_style: bpy.props.EnumProperty(
    
            name="Outer Lines Drawing Style", description="Drawing style of net outline",
    
            default='SOLID', items=line_styles)
    
        line_width: bpy.props.FloatProperty(
    
            name="Base Lines Thickness", description="Base thickness of net lines, each actual value is a multiple of this length",
    
            default=1e-4, min=0, soft_max=5e-3, precision=5, step=1e-2, subtype="UNSIGNED", unit="LENGTH")
    
        outer_width: bpy.props.FloatProperty(
    
            name="Outer Lines Thickness", description="Relative thickness of net outline",
    
            default=3, min=0, soft_max=10, precision=1, step=10, subtype='FACTOR')
    
        use_outbg: bpy.props.BoolProperty(
    
            name="Highlight Outer Lines", description="Add another line below every line to improve contrast",
    
        outbg_color: bpy.props.FloatVectorProperty(
    
            name="Outer Highlight", description="Color of the highlight for outer lines",
    
            default=(1.0, 1.0, 1.0, 1.0), min=0, max=1, subtype='COLOR', size=4)
    
        outbg_width: bpy.props.FloatProperty(
    
            name="Outer Highlight Thickness", description="Relative thickness of the highlighting lines",
    
            default=5, min=0, soft_max=10, precision=1, step=10, subtype='FACTOR')
    
    
        convex_color: bpy.props.FloatVectorProperty(
    
            name="Inner Convex Lines", description="Color of lines to be folded to a convex angle",
    
            default=(0.0, 0.0, 0.0, 1.0), min=0, max=1, subtype='COLOR', size=4)
    
        convex_style: bpy.props.EnumProperty(
    
            name="Convex Lines Drawing Style", description="Drawing style of lines to be folded to a convex angle",
    
            default='DASH', items=line_styles)
    
        convex_width: bpy.props.FloatProperty(
    
            name="Convex Lines Thickness", description="Relative thickness of concave lines",
    
            default=2, min=0, soft_max=10, precision=1, step=10, subtype='FACTOR')
    
        concave_color: bpy.props.FloatVectorProperty(
    
            name="Inner Concave Lines", description="Color of lines to be folded to a concave angle",
    
            default=(0.0, 0.0, 0.0, 1.0), min=0, max=1, subtype='COLOR', size=4)
    
        concave_style: bpy.props.EnumProperty(
    
            name="Concave Lines Drawing Style", description="Drawing style of lines to be folded to a concave angle",
    
            default='DASHDOT', items=line_styles)
    
        concave_width: bpy.props.FloatProperty(
    
            name="Concave Lines Thickness", description="Relative thickness of concave lines",
    
            default=2, min=0, soft_max=10, precision=1, step=10, subtype='FACTOR')
    
        freestyle_color: bpy.props.FloatVectorProperty(
    
            name="Freestyle Edges", description="Color of lines marked as Freestyle Edge",
    
            default=(0.0, 0.0, 0.0, 1.0), min=0, max=1, subtype='COLOR', size=4)
    
        freestyle_style: bpy.props.EnumProperty(
    
            name="Freestyle Edges Drawing Style", description="Drawing style of Freestyle Edges",
    
            default='SOLID', items=line_styles)
    
        freestyle_width: bpy.props.FloatProperty(
    
            name="Freestyle Edges Thickness", description="Relative thickness of Freestyle edges",
    
            default=2, min=0, soft_max=10, precision=1, step=10, subtype='FACTOR')
    
        use_inbg: bpy.props.BoolProperty(
    
            name="Highlight Inner Lines", description="Add another line below every line to improve contrast",
    
        inbg_color: bpy.props.FloatVectorProperty(
    
            name="Inner Highlight", description="Color of the highlight for inner lines",
    
            default=(1.0, 1.0, 1.0, 1.0), min=0, max=1, subtype='COLOR', size=4)
    
        inbg_width: bpy.props.FloatProperty(
    
            name="Inner Highlight Thickness", description="Relative thickness of the highlighting lines",
    
            default=2, min=0, soft_max=10, precision=1, step=10, subtype='FACTOR')
    
    
        sticker_fill: bpy.props.FloatVectorProperty(
    
            name="Tabs Fill", description="Fill color of sticking tabs",
    
            default=(0.9, 0.9, 0.9, 1.0), min=0, max=1, subtype='COLOR', size=4)
    
        text_color: bpy.props.FloatVectorProperty(
    
            name="Text Color", description="Color of all text used in the document",
    
            default=(0.0, 0.0, 0.0, 1.0), min=0, max=1, subtype='COLOR', size=4)
    bpy.utils.register_class(PaperModelStyle)
    
    
    class ExportPaperModel(bpy.types.Operator):
        """Blender Operator: save the selected object's net and optionally bake its texture"""
    
        bl_idname = "export_mesh.paper_model"
        bl_label = "Export Paper Model"
        bl_description = "Export the selected object's net and optionally bake its texture"
    
        filepath: bpy.props.StringProperty(
    
            name="File Path", description="Target file to save the SVG", options={'SKIP_SAVE'})
    
        filename: bpy.props.StringProperty(
    
            name="File Name", description="Name of the file", options={'SKIP_SAVE'})
    
        directory: bpy.props.StringProperty(
    
            name="Directory", description="Directory of the file", options={'SKIP_SAVE'})
    
        page_size_preset: bpy.props.EnumProperty(
    
            name="Page Size", description="Size of the exported document",
            default='A4', update=page_size_preset_changed, items=global_paper_sizes)
    
        output_size_x: bpy.props.FloatProperty(
    
            name="Page Width", description="Width of the exported document",
    
            default=0.210, soft_min=0.105, soft_max=0.841, subtype="UNSIGNED", unit="LENGTH")
    
        output_size_y: bpy.props.FloatProperty(
    
            name="Page Height", description="Height of the exported document",
    
            default=0.297, soft_min=0.148, soft_max=1.189, subtype="UNSIGNED", unit="LENGTH")
    
        output_margin: bpy.props.FloatProperty(
    
            name="Page Margin", description="Distance from page borders to the printable area",
            default=0.005, min=0, soft_max=0.1, step=0.1, subtype="UNSIGNED", unit="LENGTH")
    
        output_type: bpy.props.EnumProperty(
    
            name="Textures", description="Source of a texture for the model",
    
            default='NONE', items=[
                ('NONE', "No Texture", "Export the net only"),
                ('TEXTURE', "From Materials", "Render the diffuse color and all painted textures"),
                ('AMBIENT_OCCLUSION', "Ambient Occlusion", "Render the Ambient Occlusion pass"),
                ('RENDER', "Full Render", "Render the material in actual scene illumination"),
                ('SELECTED_TO_ACTIVE', "Selected to Active", "Render all selected surrounding objects as a texture")
            ])
    
        do_create_stickers: bpy.props.BoolProperty(
    
            name="Create Tabs", description="Create gluing tabs around the net (useful for paper)",
    
        do_create_numbers: bpy.props.BoolProperty(
    
            name="Create Numbers", description="Enumerate edges to make it clear which edges should be sticked together",
    
        sticker_width: bpy.props.FloatProperty(
    
            name="Tabs and Text Size", description="Width of gluing tabs and their numbers",
    
            default=0.005, soft_min=0, soft_max=0.05, step=0.1, subtype="UNSIGNED", unit="LENGTH")
    
        angle_epsilon: bpy.props.FloatProperty(
    
            name="Hidden Edge Angle", description="Folds with angle below this limit will not be drawn",
    
            default=pi/360, min=0, soft_max=pi/4, step=0.01, subtype="ANGLE", unit="ROTATION")
    
        output_dpi: bpy.props.FloatProperty(
    
            name="Resolution (DPI)", description="Resolution of images in pixels per inch",
    
            default=90, min=1, soft_min=30, soft_max=600, subtype="UNSIGNED")
    
        file_format: bpy.props.EnumProperty(
    
            name="Document Format", description="File format of the exported net",
    
            default='PDF', items=[
                ('PDF', "PDF", "Adobe Portable Document Format 1.4"),
                ('SVG', "SVG", "W3C Scalable Vector Graphics"),
            ])
    
        image_packing: bpy.props.EnumProperty(
    
            name="Image Packing Method", description="Method of attaching baked image(s) to the SVG",
    
            default='ISLAND_EMBED', items=[
                ('PAGE_LINK', "Single Linked", "Bake one image per page of output and save it separately"),
                ('ISLAND_LINK', "Linked", "Bake images separately for each island and save them in a directory"),
                ('ISLAND_EMBED', "Embedded", "Bake images separately for each island and embed them into the SVG")
            ])
    
        scale: bpy.props.FloatProperty(
    
            name="Scale", description="Divisor of all dimensions when exporting",
    
            default=1, soft_min=1.0, soft_max=10000.0, step=100, subtype='UNSIGNED', precision=1)
    
        do_create_uvmap: bpy.props.BoolProperty(
    
            name="Create UVMap", description="Create a new UV Map showing the islands and page layout",
    
            default=False, options={'SKIP_SAVE'})
    
        ui_expanded_document: bpy.props.BoolProperty(
    
            name="Show Document Settings Expanded", description="Shows the box 'Document Settings' expanded in user interface",
    
            default=True, options={'SKIP_SAVE'})
    
        ui_expanded_style: bpy.props.BoolProperty(
    
            name="Show Style Settings Expanded", description="Shows the box 'Colors and Style' expanded in user interface",
    
            default=False, options={'SKIP_SAVE'})
    
        style: bpy.props.PointerProperty(type=PaperModelStyle)
    
    
        unfolder = None
        largest_island_ratio = 0
    
        @classmethod
        def poll(cls, context):
            return context.active_object and context.active_object.type == 'MESH'
    
        def execute(self, context):
            try:
                if self.object.data.paper_island_list:
                    self.unfolder.copy_island_names(self.object.data.paper_island_list)
                self.unfolder.save(self.properties)
                self.report({'INFO'}, "Saved a {}-page document".format(len(self.unfolder.mesh.pages)))
                return {'FINISHED'}
            except UnfoldError as error:
                self.report(type={'ERROR_INVALID_INPUT'}, message=error.args[0])
                return {'CANCELLED'}
    
        def get_scale_ratio(self, sce):
            margin = self.output_margin + self.sticker_width + 1e-5
            if min(self.output_size_x, self.output_size_y) <= 2 * margin:
                return False
            output_inner_size = M.Vector((self.output_size_x - 2*margin, self.output_size_y - 2*margin))
            ratio = self.unfolder.mesh.largest_island_ratio(output_inner_size)
            return ratio * sce.unit_settings.scale_length / self.scale
    
        def invoke(self, context, event):
            sce = context.scene
            recall_mode = context.object.mode
            bpy.ops.object.mode_set(mode='OBJECT')
    
            self.scale = sce.paper_model.scale
            self.object = context.active_object
            cage_size = M.Vector((sce.paper_model.output_size_x, sce.paper_model.output_size_y)) if sce.paper_model.limit_by_page else None
            try:
                self.unfolder = Unfolder(self.object)
    
                self.unfolder.prepare(
                    cage_size, create_uvmap=self.do_create_uvmap,
                    scale=sce.unit_settings.scale_length/self.scale)
    
            except UnfoldError as error:
                self.report(type={'ERROR_INVALID_INPUT'}, message=error.args[0])
                bpy.ops.object.mode_set(mode=recall_mode)
                return {'CANCELLED'}
            scale_ratio = self.get_scale_ratio(sce)
            if scale_ratio > 1:
                self.scale = ceil(self.scale * scale_ratio)
            wm = context.window_manager
            wm.fileselect_add(self)
    
            bpy.ops.object.mode_set(mode=recall_mode)
            return {'RUNNING_MODAL'}
    
        def draw(self, context):
            layout = self.layout
    
            layout.prop(self.properties, "do_create_uvmap")
    
            row = layout.row(align=True)
            row.menu("VIEW3D_MT_paper_model_presets", text=bpy.types.VIEW3D_MT_paper_model_presets.bl_label)
    
            row.operator("export_mesh.paper_model_preset_add", text="", icon='ADD')
            row.operator("export_mesh.paper_model_preset_add", text="", icon='REMOVE').remove_active = True
    
    
            # a little hack: this prints out something like "Scale: 1: 72"
            layout.prop(self.properties, "scale", text="Scale: 1")
            scale_ratio = self.get_scale_ratio(context.scene)
            if scale_ratio > 1:
    
                layout.label(
                    text="An island is roughly {:.1f}x bigger than page".format(scale_ratio),
                    icon="ERROR")
    
            elif scale_ratio > 0:
                layout.label(text="Largest island is roughly 1/{:.1f} of page".format(1 / scale_ratio))
    
    
            if context.scene.unit_settings.scale_length != 1:
                layout.label(
                    text="Unit scale {:.1f} makes page size etc. not display correctly".format(
                        context.scene.unit_settings.scale_length), icon="ERROR")
    
            box = layout.box()
            row = box.row(align=True)
    
            row.prop(
                self.properties, "ui_expanded_document", text="",
    
                icon=('TRIA_DOWN' if self.ui_expanded_document else 'TRIA_RIGHT'), emboss=False)
            row.label(text="Document Settings")
    
            if self.ui_expanded_document:
                box.prop(self.properties, "file_format", text="Format")
                box.prop(self.properties, "page_size_preset")
                col = box.column(align=True)
                col.active = self.page_size_preset == 'USER'
                col.prop(self.properties, "output_size_x")
                col.prop(self.properties, "output_size_y")
                box.prop(self.properties, "output_margin")
                col = box.column()
                col.prop(self.properties, "do_create_stickers")
                col.prop(self.properties, "do_create_numbers")
                col = box.column()
                col.active = self.do_create_stickers or self.do_create_numbers
                col.prop(self.properties, "sticker_width")
                box.prop(self.properties, "angle_epsilon")
    
                box.prop(self.properties, "output_type")
                col = box.column()
                col.active = (self.output_type != 'NONE')
                if len(self.object.data.uv_textures) == 8:
                    col.label(text="No UV slots left, No Texture is the only option.", icon='ERROR')
                elif context.scene.render.engine not in ('BLENDER_RENDER', 'CYCLES') and self.output_type != 'NONE':
                    col.label(text="Blender Internal engine will be used for texture baking.", icon='ERROR')
                col.prop(self.properties, "output_dpi")
                row = col.row()
                row.active = self.file_format == 'SVG'
                row.prop(self.properties, "image_packing", text="Images")
    
            box = layout.box()
            row = box.row(align=True)
    
            row.prop(
                self.properties, "ui_expanded_style", text="",
    
                icon=('TRIA_DOWN' if self.ui_expanded_style else 'TRIA_RIGHT'), emboss=False)
            row.label(text="Colors and Style")
    
            if self.ui_expanded_style:
                box.prop(self.style, "line_width", text="Default line width")
                col = box.column()
                col.prop(self.style, "outer_color")
                col.prop(self.style, "outer_width", text="Relative width")
                col.prop(self.style, "outer_style", text="Style")
                col = box.column()
                col.active = self.output_type != 'NONE'
                col.prop(self.style, "use_outbg", text="Outer Lines Highlight:")
                sub = col.column()
                sub.active = self.output_type != 'NONE' and self.style.use_outbg
                sub.prop(self.style, "outbg_color", text="")
                sub.prop(self.style, "outbg_width", text="Relative width")
                col = box.column()
                col.prop(self.style, "convex_color")
                col.prop(self.style, "convex_width", text="Relative width")
                col.prop(self.style, "convex_style", text="Style")
                col = box.column()
                col.prop(self.style, "concave_color")
                col.prop(self.style, "concave_width", text="Relative width")
                col.prop(self.style, "concave_style", text="Style")
                col = box.column()
                col.prop(self.style, "freestyle_color")
                col.prop(self.style, "freestyle_width", text="Relative width")
                col.prop(self.style, "freestyle_style", text="Style")
                col = box.column()
                col.active = self.output_type != 'NONE'
                col.prop(self.style, "use_inbg", text="Inner Lines Highlight:")
                sub = col.column()
                sub.active = self.output_type != 'NONE' and self.style.use_inbg
                sub.prop(self.style, "inbg_color", text="")
                sub.prop(self.style, "inbg_width", text="Relative width")
                col = box.column()
                col.active = self.do_create_stickers
                col.prop(self.style, "sticker_fill")
                box.prop(self.style, "text_color")
    
    
    def menu_func(self, context):
        self.layout.operator("export_mesh.paper_model", text="Paper Model (.svg)")
    
    
    class VIEW3D_MT_paper_model_presets(bpy.types.Menu):
        bl_label = "Paper Model Presets"
        preset_subdir = "export_mesh"
        preset_operator = "script.execute_preset"
        draw = bpy.types.Menu.draw_preset
    
    
    class AddPresetPaperModel(bl_operators.presets.AddPresetBase, bpy.types.Operator):
        """Add or remove a Paper Model Preset"""
        bl_idname = "export_mesh.paper_model_preset_add"
        bl_label = "Add Paper Model Preset"
        preset_menu = "VIEW3D_MT_paper_model_presets"
        preset_subdir = "export_mesh"
        preset_defines = ["op = bpy.context.active_operator"]
    
        @property
        def preset_values(self):
            op = bpy.ops.export_mesh.paper_model
    
            properties = op.get_rna_type().properties.items()
    
            blacklist = bpy.types.Operator.bl_rna.properties.keys()
    
            return [
                "op.{}".format(prop_id) for (prop_id, prop) in properties
    
                if not (prop.is_hidden or prop.is_skip_save or prop_id in blacklist)]
    
    
    class VIEW3D_PT_paper_model_tools(bpy.types.Panel):
        bl_label = "Tools"
        bl_space_type = "VIEW_3D"
        bl_region_type = "TOOLS"
        bl_category = "Paper Model"
    
        def draw(self, context):
            layout = self.layout
            sce = context.scene
            obj = context.active_object
            mesh = obj.data if obj and obj.type == 'MESH' else None
    
            layout.operator("export_mesh.paper_model")
    
            col = layout.column(align=True)
    
            col.label(text="Customization:")
    
            col.operator("mesh.unfold")
    
            if context.mode == 'EDIT_MESH':
                row = layout.row(align=True)
                row.operator("mesh.mark_seam", text="Mark Seam").clear = False
                row.operator("mesh.mark_seam", text="Clear Seam").clear = True
            else:
                layout.operator("mesh.clear_all_seams")
    
    
            props = sce.paper_model
            layout.prop(props, "scale", text="Model Scale: 1")
    
            layout.prop(props, "limit_by_page")
            col = layout.column()
            col.active = props.limit_by_page
            col.prop(props, "page_size_preset")
    
            sub = col.column(align=True)
    
            sub.active = props.page_size_preset == 'USER'
            sub.prop(props, "output_size_x")
            sub.prop(props, "output_size_y")
    
    
    
    class VIEW3D_PT_paper_model_islands(bpy.types.Panel):
        bl_label = "Islands"
        bl_space_type = "VIEW_3D"
        bl_region_type = "TOOLS"
        bl_category = "Paper Model"
    
        def draw(self, context):
            layout = self.layout
            sce = context.scene
            obj = context.active_object
            mesh = obj.data if obj and obj.type == 'MESH' else None
    
            if mesh and mesh.paper_island_list:
    
                layout.label(
                    text="1 island:" if len(mesh.paper_island_list) == 1 else
    
                    "{} islands:".format(len(mesh.paper_island_list)))
    
                layout.template_list(
                    'UI_UL_list', 'paper_model_island_list', mesh,
    
                    'paper_island_list', mesh, 'paper_island_index', rows=1, maxrows=5)
                if mesh.paper_island_index >= 0:
                    list_item = mesh.paper_island_list[mesh.paper_island_index]
                    sub = layout.column(align=True)
                    sub.prop(list_item, "auto_label")
                    sub.prop(list_item, "label")
                    sub.prop(list_item, "auto_abbrev")
                    row = sub.row()
                    row.active = not list_item.auto_abbrev
                    row.prop(list_item, "abbreviation")
            else:
                layout.label(text="Not unfolded")
    
                layout.box().label(text="Use the 'Unfold' tool")
    
            sub = layout.column(align=True)
            sub.active = bool(mesh and mesh.paper_island_list)
            sub.prop(sce.paper_model, "display_islands", icon='RESTRICT_VIEW_OFF')
            row = sub.row(align=True)
            row.active = bool(sce.paper_model.display_islands and mesh and mesh.paper_island_list)
            row.prop(sce.paper_model, "islands_alpha", slider=True)
    
    
    def display_islands(self, context):
        # TODO: save the vertex positions and don't recalculate them always?
        ob = context.active_object
        if not ob or ob.type != 'MESH':
            return
        mesh = ob.data
        if not mesh.paper_island_list or mesh.paper_island_index == -1:
            return
    
        bgl.glMatrixMode(bgl.GL_PROJECTION)
        perspMatrix = context.space_data.region_3d.perspective_matrix
        perspBuff = bgl.Buffer(bgl.GL_FLOAT, (4, 4), perspMatrix.transposed())
        bgl.glLoadMatrixf(perspBuff)
        bgl.glMatrixMode(bgl.GL_MODELVIEW)
        objectBuff = bgl.Buffer(bgl.GL_FLOAT, (4, 4), ob.matrix_world.transposed())
        bgl.glLoadMatrixf(objectBuff)
        bgl.glEnable(bgl.GL_BLEND)
        bgl.glBlendFunc(bgl.GL_SRC_ALPHA, bgl.GL_ONE_MINUS_SRC_ALPHA)
        bgl.glEnable(bgl.GL_POLYGON_OFFSET_FILL)
        bgl.glPolygonOffset(0, -10)  # offset in Zbuffer to remove flicker
        bgl.glPolygonMode(bgl.GL_FRONT_AND_BACK, bgl.GL_FILL)
        bgl.glColor4f(1.0, 0.4, 0.0, self.islands_alpha)
        island = mesh.paper_island_list[mesh.paper_island_index]
        for lface in island.faces:
            face = mesh.polygons[lface.id]
            bgl.glBegin(bgl.GL_POLYGON)
            for vertex_id in face.vertices:
                vertex = mesh.vertices[vertex_id]
                bgl.glVertex4f(*vertex.co.to_4d())
            bgl.glEnd()
        bgl.glPolygonOffset(0.0, 0.0)
        bgl.glDisable(bgl.GL_POLYGON_OFFSET_FILL)
        bgl.glLoadIdentity()
    display_islands.handle = None
    
    
    def display_islands_changed(self, context):
        """Switch highlighting islands on/off"""
        if self.display_islands:
            if not display_islands.handle:
    
                display_islands.handle = bpy.types.SpaceView3D.draw_handler_add(
                    display_islands, (self, context), 'WINDOW', 'POST_VIEW')
    
        else:
            if display_islands.handle:
                bpy.types.SpaceView3D.draw_handler_remove(display_islands.handle, 'WINDOW')
                display_islands.handle = None
    
    
    def label_changed(self, context):
        """The label of an island was changed"""
        # accessing properties via [..] to avoid a recursive call after the update
        self["auto_label"] = not self.label or self.label.isspace()
        island_item_changed(self, context)
    
    
    def island_item_changed(self, context):
        """The labelling of an island was changed"""
    
        def increment(abbrev, collisions):
            letters = "ABCDEFGHIJKLMNPQRSTUVWXYZ123456789"
            while abbrev in collisions:
                abbrev = abbrev.rstrip(letters[-1])
                abbrev = abbrev[:2] + letters[letters.find(abbrev[-1]) + 1 if len(abbrev) == 3 else 0]
            return abbrev
    
    
        # accessing properties via [..] to avoid a recursive call after the update
        island_list = context.active_object.data.paper_island_list
        if self.auto_label:
            self["label"] = ""  # avoid self-conflict
            number = 1
            while any(item.label == "Island {}".format(number) for item in island_list):
                number += 1
            self["label"] = "Island {}".format(number)
        if self.auto_abbrev:
    
            self["abbreviation"] = ""  # avoid self-conflict
            abbrev = "".join(first_letters(self.label))[:3].upper()
            self["abbreviation"] = increment(abbrev, {item.abbreviation for item in island_list})
    
        elif len(self.abbreviation) > 3:
            self["abbreviation"] = self.abbreviation[:3]
    
        self.name = "[{}] {} ({} {})".format(
            self.abbreviation, self.label, len(self.faces), "faces" if len(self.faces) > 1 else "face")
    
    
    
    class FaceList(bpy.types.PropertyGroup):
    
        id: bpy.props.IntProperty(name="Face ID")
    
    bpy.utils.register_class(FaceList)
    
    
    
    class IslandList(bpy.types.PropertyGroup):
    
        faces: bpy.props.CollectionProperty(
    
            name="Faces", description="Faces belonging to this island", type=FaceList)
    
        label: bpy.props.StringProperty(
    
            name="Label", description="Label on this island",
    
            default="", update=label_changed)
    
        abbreviation: bpy.props.StringProperty(
    
            name="Abbreviation", description="Three-letter label to use when there is not enough space",
    
            default="", update=island_item_changed)
    
        auto_label: bpy.props.BoolProperty(
    
            name="Auto Label", description="Generate the label automatically",
    
            default=True, update=island_item_changed)
    
        auto_abbrev: bpy.props.BoolProperty(
    
            name="Auto Abbreviation", description="Generate the abbreviation automatically",
    
            default=True, update=island_item_changed)
    bpy.utils.register_class(IslandList)
    
    
    class PaperModelSettings(bpy.types.PropertyGroup):
    
        display_islands: bpy.props.BoolProperty(
    
            name="Highlight selected island", description="Highlight faces corresponding to the selected island in the 3D View",
    
            options={'SKIP_SAVE'}, update=display_islands_changed)
    
        islands_alpha: bpy.props.FloatProperty(
    
            name="Opacity", description="Opacity of island highlighting",
            min=0.0, max=1.0, default=0.3)
    
        limit_by_page: bpy.props.BoolProperty(
    
            name="Limit Island Size", description="Do not create islands larger than given dimensions",
            default=False, update=page_size_preset_changed)
    
        page_size_preset: bpy.props.EnumProperty(
    
            name="Page Size", description="Maximal size of an island",
            default='A4', update=page_size_preset_changed, items=global_paper_sizes)
    
        output_size_x: bpy.props.FloatProperty(
    
            name="Width", description="Maximal width of an island",
    
            default=0.2, soft_min=0.105, soft_max=0.841, subtype="UNSIGNED", unit="LENGTH")
    
        output_size_y: bpy.props.FloatProperty(
    
            name="Height", description="Maximal height of an island",
    
            default=0.29, soft_min=0.148, soft_max=1.189, subtype="UNSIGNED", unit="LENGTH")
    
        scale: bpy.props.FloatProperty(
    
            name="Scale", description="Divisor of all dimensions when exporting",
    
            default=1, soft_min=1.0, soft_max=10000.0, step=100, subtype='UNSIGNED', precision=1)
    bpy.utils.register_class(PaperModelSettings)
    
    
    def register():
        bpy.utils.register_module(__name__)
    
    
        bpy.types.Scene.paper_model = bpy.props.PointerProperty(
            name="Paper Model", description="Settings of the Export Paper Model script",
            type=PaperModelSettings, options={'SKIP_SAVE'})
        bpy.types.Mesh.paper_island_list = bpy.props.CollectionProperty(
            name="Island List", type=IslandList)
        bpy.types.Mesh.paper_island_index = bpy.props.IntProperty(
            name="Island List Index",
    
            default=-1, min=-1, max=100, options={'SKIP_SAVE'})
    
        bpy.types.TOPBAR_MT_file_export.append(menu_func)
    
    
    
    def unregister():
        bpy.utils.unregister_module(__name__)
    
        bpy.types.TOPBAR_MT_file_export.remove(menu_func)
    
        if display_islands.handle:
            bpy.types.SpaceView3D.draw_handler_remove(display_islands.handle, 'WINDOW')
            display_islands.handle = None
    
    
    if __name__ == "__main__":
        register()