bl_info = {
    "name": "Sculpt Along Curve (standalone)",
    "author": "GG & ChatGPT & DeepSeek",
    "version": (1, 0),
    "blender": (4, 5, 0),
    "location": "View3D > Sidebar > Edit > Sculpt Along Curve",
    "description": "Sculpt along curves and create/edit pattern curves, standalone version",
    "category": "Sculpt",
}

import bpy
import bmesh
import re
from mathutils import Vector


# --- Helpers -------------------------------------------------------

def update_curve_resolution(context):
    """Update curve resolution when UI property changes"""
    scene = context.scene
    idx = scene.curve_list_index
    if idx < 0 or idx >= len(scene.curve_list):
        return
    curve_item = scene.curve_list[idx].obj_ref
    if curve_item and curve_item.type == 'CURVE':
        curve_item.data.resolution_u = scene.curve_resolution_u


VALID_SCULPT_TOOLS = {"mask", "brush", "paint", "draw_face_sets"}

def get_active_sculpt_tool_name(context=None):
    """Return the normalized name of the active sculpt tool"""
    context = context or bpy.context
    obj = context.object
    workspace = context.workspace

    if not obj or obj.type != 'MESH' or context.mode != 'SCULPT':
        return None

    try:
        tool = workspace.tools.from_space_view3d_mode(context.mode)
        name = getattr(tool, "idname", str(tool))
        name = name.lower().split('.')[-1]
        return name
    except Exception:
        return None


def is_active_sculpt_tool_valid_multires(context=None):
    """
    Returns True if the active sculpt tool is valid, taking into account
    whether the active mesh has a Multires modifier.
    """
    context = context or bpy.context
    obj = context.object

    if not obj or obj.type != 'MESH' or context.mode != 'SCULPT':
        return False

    has_multires = any(mod.type == 'MULTIRES' for mod in obj.modifiers)
    if has_multires:
        allowed_tools = {"mask", "brush", "draw_face_sets"}
    else:
        allowed_tools = {"mask", "brush", "paint", "draw_face_sets"}

    tool_name = get_active_sculpt_tool_name(context)
    if not tool_name:
        return False

    return tool_name in allowed_tools


def find_layer_collection_path(layer_collection, target_collection, path=None):
    """Find path to target collection in layer collection hierarchy"""
    if path is None:
        path = [layer_collection]
    if layer_collection.collection == target_collection:
        return path
    for child in layer_collection.children:
        res = find_layer_collection_path(child, target_collection, path + [child])
        if res:
            return res
    return None


def unhide_collection_in_view_layer(view_layer, collection):
    """Ensure collection is visible in the given view layer"""
    root = view_layer.layer_collection
    path = find_layer_collection_path(root, collection)
    if not path:
        return False
    for lc in path:
        try:
            lc.hide_viewport = False
        except Exception:
            pass
        try:
            lc.exclude = False
        except Exception:
            pass
        try:
            lc.hide_render = False
        except Exception:
            pass
    return True


def get_curves_for_active_mesh(scene, active_mesh):
    """Return all curves in the collection 'Curves for {MeshName}' for the given mesh"""
    curves = []
    if active_mesh and active_mesh.type == 'MESH':
        top_col_name = "Stroke Along Curves"
        sub_col_name = f"Curves for {active_mesh.name}"
        if top_col_name in bpy.data.collections:
            top_col = bpy.data.collections[top_col_name]
            if sub_col_name in top_col.children:
                sub_col = top_col.children[sub_col_name]
                for o in sub_col.objects:
                    if o.type == 'CURVE':
                        curves.append(o)
    return curves


def refresh_curve_list(scene):
    """Populate the curve list based on active mesh or active curve"""
    active_obj = bpy.context.object
    target_mesh = None

    # Sculpt mode: use the active mesh
    if active_obj and active_obj.type == 'MESH':
        target_mesh = active_obj

    # Edit Curve mode: find the mesh linked to the active curve
    elif active_obj and active_obj.type == 'CURVE':
        for col in active_obj.users_collection:
            if col.name.startswith("Curves for "):
                mesh_name = col.name[len("Curves for "):]
                mesh_obj = bpy.data.objects.get(mesh_name)
                if mesh_obj and mesh_obj.type == 'MESH':
                    target_mesh = mesh_obj
                    break

    # Clear list if no mesh found
    if not target_mesh:
        scene.curve_list.clear()
        scene.curve_list_index = 0
        return

    # Populate the list
    index = getattr(scene, "curve_list_index", 0)
    scene.curve_list.clear()
    curves = get_curves_for_active_mesh(scene, target_mesh)
    for c in curves:
        item = scene.curve_list.add()
        item.obj_ref = c

    scene.curve_list_index = 0 if not scene.curve_list else min(index, len(scene.curve_list) - 1)

    # Update curve_resolution_u from the selected curve
    idx = scene.curve_list_index
    if 0 <= idx < len(scene.curve_list):
        selected_curve = scene.curve_list[idx].obj_ref
        if selected_curve and selected_curve.type == 'CURVE':
            scene.curve_resolution_u = selected_curve.data.resolution_u


# --- Handlers --------------------------------------------------------

@bpy.app.handlers.persistent
def refresh_curve_list_handler(scene):
    """Optimized: refresh curve list only when count changes"""
    current_curve_count = len([obj for obj in bpy.data.objects if obj.type == 'CURVE'])
    
    if not hasattr(scene, "_curve_count_cache"):
        scene["_curve_count_cache"] = current_curve_count
        refresh_curve_list(scene)
        return

    if scene["_curve_count_cache"] != current_curve_count:
        scene["_curve_count_cache"] = current_curve_count
        refresh_curve_list(scene)


# --- Property container --------------------------------------------

class CurveItem(bpy.types.PropertyGroup):
    obj_ref: bpy.props.PointerProperty(type=bpy.types.Object)


# --- Automatic Edit Mode switch on curve selection ------------------

updating_curve_selection = False

def update_curve_selection(self, context):
    """Callback when user changes the curve_list_index"""
    global updating_curve_selection
    if updating_curve_selection:
        return

    updating_curve_selection = True
    try:
        scene = context.scene
        idx = scene.curve_list_index

        if idx >= len(scene.curve_list):
            return

        new_curve = scene.curve_list[idx].obj_ref
        if not new_curve:
            return

        # Only switch if we're in Curve Edit Mode
        if context.mode == 'EDIT_CURVE' and new_curve != context.object:
            try:
                # Unhide the curve itself
                new_curve.hide_set(False)
                new_curve.hide_viewport = False
                new_curve.hide_render = False

                # Unhide all collections containing the curve
                for col in new_curve.users_collection:
                    col.hide_viewport = False
                    col.hide_render = False
                    try:
                        unhide_collection_in_view_layer(context.view_layer, col)
                    except:
                        pass

                # Switch active curve
                bpy.ops.object.mode_set(mode='OBJECT')
                bpy.ops.object.select_all(action='DESELECT')
                new_curve.select_set(True)
                context.view_layer.objects.active = new_curve
                bpy.ops.object.mode_set(mode='EDIT')

            except Exception as e:
                print(f"Failed to switch to curve {new_curve.name}: {e}")
                
        # Auto-refresh curve resolution
        if new_curve.type == 'CURVE':
            new_curve.data.resolution_u = scene.curve_resolution_u
                
    finally:
        updating_curve_selection = False


# --- Operators ------------------------------------------------------

class MESH_OT_loose_edge_info(bpy.types.Operator):
    """Analyze loose edges as connected chains and detect loops"""
    bl_idname = "mesh.loose_edge_info"
    bl_label = "Loose Edge Info"
    bl_options = {'REGISTER', 'UNDO'}

    mesh_name: bpy.props.StringProperty(name="Mesh Name")

    def execute(self, context):
        obj = bpy.data.objects.get(self.mesh_name)
        if not obj or obj.type != 'MESH':
            self.report({'ERROR'}, "Mesh not found")
            return {'CANCELLED'}

        bm = bmesh.new()
        bm.from_mesh(obj.data)

        # Find loose edges
        loose_edges = {e for e in bm.edges if len(e.link_faces) == 0}
        visited_edges = set()
        loose_chains = []

        def collect_chain(start_edge):
            """Return a set of connected loose edges starting from start_edge"""
            stack = [start_edge]
            chain_edges = set()
            while stack:
                e = stack.pop()
                if e in chain_edges:
                    continue
                chain_edges.add(e)
                for vert in e.verts:
                    for le in vert.link_edges:
                        if le in loose_edges and le not in chain_edges:
                            stack.append(le)
            return chain_edges

        def is_loop(chain_edges):
            """Determine if a set of connected edges forms a closed loop"""
            vertices = set()
            for e in chain_edges:
                vertices.update(e.verts)
            for v in vertices:
                linked_loose = [le for le in v.link_edges if le in chain_edges]
                if len(linked_loose) != 2:
                    return False
            return True

        # Build loose edge chains
        for e in loose_edges:
            if e in visited_edges:
                continue
            chain_edges = collect_chain(e)
            visited_edges.update(chain_edges)
            loop_status = is_loop(chain_edges)
            loose_chains.append((chain_edges, loop_status))

        # Print results
        print(f"Total loose edge chains: {len(loose_chains)}")
        for i, (chain_edges, loop_status) in enumerate(loose_chains, start=1):
            verts_indices = sorted({v.index for e in chain_edges for v in e.verts})
            print(f"Loose Edge {i}: verts {verts_indices} | Loop: {loop_status}")

        bm.free()
        return {'FINISHED'}


class SCULPT_OT_stroke_action(bpy.types.Operator):
    """Generate and apply sculpt stroke along loose chains of a curve"""
    bl_idname = "sculpt.stroke_action"
    bl_label = "Stroke Action"
    bl_options = {'INTERNAL'}

    mesh_name: bpy.props.StringProperty()
    curve_name: bpy.props.StringProperty()

    def execute(self, context):
        # Get objects
        obj = bpy.data.objects.get(self.mesh_name)
        curve_obj = bpy.data.objects.get(self.curve_name)
        if not obj or not curve_obj:
            self.report({'ERROR'}, "Mesh or curve not found")
            return {'CANCELLED'}

        # Build evaluated curve mesh
        depsgraph = context.evaluated_depsgraph_get()
        curve_eval = curve_obj.evaluated_get(depsgraph)
        mesh_eval = curve_eval.to_mesh()
        if not mesh_eval or not mesh_eval.vertices:
            if mesh_eval:
                curve_eval.to_mesh_clear()
            self.report({'ERROR'}, "Curve mesh has no vertices")
            return {'CANCELLED'}

        # Build bmesh to detect chains
        bm = bmesh.new()
        bm.from_mesh(mesh_eval)
        curve_eval.to_mesh_clear()

        loose_edges = set(bm.edges)
        visited_edges = set()
        chains = []

        def collect_chain(start_edge):
            stack = [start_edge]
            chain_edges = set()
            while stack:
                e = stack.pop()
                if e in chain_edges:
                    continue
                chain_edges.add(e)
                for vert in e.verts:
                    for le in vert.link_edges:
                        if le in loose_edges and le not in chain_edges:
                            stack.append(le)
            return chain_edges

        def is_loop(chain_edges):
            vertices = set()
            for e in chain_edges:
                vertices.update(e.verts)
            for v in vertices:
                linked = [le for le in v.link_edges if le in chain_edges]
                if len(linked) != 2:
                    return False
            return True

        # Build chains
        for e in loose_edges:
            if e in visited_edges:
                continue
            chain_edges = collect_chain(e)
            visited_edges.update(chain_edges)
            loop_status = is_loop(chain_edges)
            chains.append((chain_edges, loop_status))

        # Apply stroke along each chain
        sculpt = context.tool_settings.sculpt
        brush = sculpt.brush
        if hasattr(brush, "use_frontface"):
            brush.use_frontface = False

        for chain_edges, loop_status in chains:
            # Order vertices along chain
            edges = list(chain_edges)
            verts_ordered = [edges[0].verts[0], edges[0].verts[1]]
            remaining_edges = set(edges[1:])
            while remaining_edges:
                extended = False
                for e in list(remaining_edges):
                    if verts_ordered[-1] in e.verts:
                        other_vert = e.verts[0] if e.verts[1] == verts_ordered[-1] else e.verts[1]
                        verts_ordered.append(other_vert)
                        remaining_edges.remove(e)
                        extended = True
                        break
                    elif verts_ordered[0] in e.verts:
                        other_vert = e.verts[0] if e.verts[1] == verts_ordered[0] else e.verts[1]
                        verts_ordered.insert(0, other_vert)
                        remaining_edges.remove(e)
                        extended = True
                        break
                if not extended:
                    break

            # Transform world → mesh local
            points = [curve_obj.matrix_world @ v.co for v in verts_ordered]
            points = [obj.matrix_world.inverted() @ p for p in points]

            # Interpolate points along edges
            dense_points = []
            num_verts = len(points)
            is_closed = loop_status

            if is_closed:
                # Closed loop
                for i in range(num_verts):
                    p0 = points[i]
                    p1 = points[(i + 1) % num_verts]
                    for j in range(2):  # n_points_between = 1
                        t = j / 2
                        dense_points.append(p0.lerp(p1, t))
                if len(dense_points) > 1:
                    dense_points = dense_points[1:-1]
            else:
                # Open chain
                for i in range(num_verts - 1):
                    p0 = points[i]
                    p1 = points[i + 1]
                    for j in range(2):  # n_points_between = 1
                        t = j / 2
                        dense_points.append(p0.lerp(p1, t))
                dense_points.append(points[-1])

            # Build stroke data
            strokes = []
            for i, p in enumerate(dense_points):
                strokes.append({
                    "name": "stroke",
                    "location": p,
                    "mouse": (0.0, 0.0),
                    "mouse_event": (0.0, 0.0),
                    "pressure": 1.0,
                    "size": brush.size,
                    "time": i * 0.01,
                    "is_start": i == 0,
                    "x_tilt": 0.0,
                    "y_tilt": 0.0,
                })

            # Apply stroke
            applied = False
            for area in context.screen.areas:
                if area.type == "VIEW_3D":
                    for region in area.regions:
                        if region.type == "WINDOW":
                            with context.temp_override(area=area, region=region, object=obj):
                                bpy.ops.sculpt.brush_stroke(stroke=strokes)
                            applied = True
                            break
                    if applied:
                        break

        bm.free()
        return {'FINISHED'}


class SCULPT_OT_create_path_curve(bpy.types.Operator):
    """Create a new Path curve and switch to Edit Mode"""
    bl_idname = "sculpt.create_path_curve"
    bl_label = "Add Path Curve"
    bl_options = {'REGISTER', 'UNDO'}

    def execute(self, context):
        # Determine target mesh
        target_mesh = None
        if context.mode == 'EDIT_CURVE':
            active_curve = context.object
            if not active_curve or active_curve.type != 'CURVE':
                self.report({'ERROR'}, "Active object must be a curve")
                return {'CANCELLED'}
            for col in active_curve.users_collection:
                if col.name.startswith("Curves for "):
                    mesh_name = col.name[len("Curves for "):]
                    candidate = bpy.data.objects.get(mesh_name)
                    if candidate and candidate.type == 'MESH':
                        target_mesh = candidate
                        break
            if not target_mesh:
                self.report({'ERROR'}, "Could not find mesh linked to this curve")
                return {'CANCELLED'}
        else:
            active_obj = context.object
            if not active_obj or active_obj.type != 'MESH':
                self.report({'ERROR'}, "Active object must be a mesh")
                return {'CANCELLED'}
            target_mesh = active_obj
            mesh_name = target_mesh.name

        # Switch to OBJECT mode
        if context.mode != 'OBJECT':
            bpy.ops.object.mode_set(mode='OBJECT')

        # Store mesh reference
        context.scene["active_mesh_name"] = mesh_name

        # Add NURBS path curve
        bpy.ops.curve.primitive_nurbs_path_add()
        new_curve = context.active_object
        
        new_curve.data.use_stretch = True
        new_curve.data.use_deform_bounds = True
        
        # Add Shrinkwrap modifier
        bpy.ops.object.modifier_add(type='SHRINKWRAP')
        new_curve.modifiers["Shrinkwrap"].target = target_mesh
        new_curve.modifiers["Shrinkwrap"].wrap_mode = 'ABOVE_SURFACE'
        
        # Add Copy Transforms constraint
        bpy.ops.object.constraint_add(type='COPY_TRANSFORMS')
        new_curve.constraints["Copy Transforms"].target = target_mesh
        
        bpy.context.object.data.resolution_u = 15

        # Generate unique name
        base_name = "Path"
        i = 1
        while f"{base_name}_{i:02d}" in bpy.data.objects:
            i += 1
        new_curve.name = f"{base_name}_{i:02d}"

        # Ensure proper collections exist
        top_col_name = "Stroke Along Curves"
        if top_col_name not in bpy.data.collections:
            top_col = bpy.data.collections.new(top_col_name)
            top_col.color_tag = 'COLOR_03'
            context.scene.collection.children.link(top_col)
        else:
            top_col = bpy.data.collections[top_col_name]

        sub_col_name = f"Curves for {mesh_name}"
        if sub_col_name not in top_col.children:
            sub_col = bpy.data.collections.new(sub_col_name)
            sub_col.color_tag = 'COLOR_03'
            top_col.children.link(sub_col)
        else:
            sub_col = top_col.children[sub_col_name]

        # Link curve only to its sub-collection
        for col in new_curve.users_collection:
            col.objects.unlink(new_curve)
        sub_col.objects.link(new_curve)

        # Activate and enter Edit mode
        context.view_layer.objects.active = new_curve
        new_curve.select_set(True)
        bpy.ops.object.mode_set(mode='EDIT')
        
        # Refresh the curve list in UI
        refresh_curve_list(context.scene)

        # Select the newly created curve in the panel
        for i, item in enumerate(context.scene.curve_list):
            if item.obj_ref == new_curve:
                context.scene.curve_list_index = i
                break
            
        # Delete Vertices and setup curve draw tool
        bpy.ops.curve.select_all(action='SELECT')
        bpy.ops.curve.delete(type='SEGMENT')
        bpy.ops.wm.tool_set_by_id(name="builtin.draw")
        ts = bpy.context.scene.tool_settings
        cps = ts.curve_paint_settings
        cps.use_project_only_selected = False
        cps.depth_mode = 'SURFACE'

        self.report({'INFO'}, f"Created curve '{new_curve.name}' for {mesh_name}")
        return {'FINISHED'}


class SCULPT_OT_edit_curve(bpy.types.Operator):
    """Select only the chosen curve and enter Edit mode"""
    bl_idname = "sculpt.edit_curve"
    bl_label = "Edit Curve"
    bl_options = {'REGISTER', 'UNDO'}

    def execute(self, context):
        scene = context.scene
        idx = scene.curve_list_index
        if idx >= len(scene.curve_list):
            self.report({'ERROR'}, "No curve selected")
            return {'CANCELLED'}

        curve_obj = scene.curve_list[idx].obj_ref
        if not curve_obj:
            self.report({'ERROR'}, "Curve reference invalid")
            return {'CANCELLED'}
        
        # Unhide selected curve
        try:
            curve_obj.hide_set(False)
        except:
            pass

        curve_obj.hide_viewport = False
        curve_obj.hide_render = False
                
        # Make sure the collection containing the curve is visible
        for col in curve_obj.users_collection:
            col.hide_viewport = False
            col.hide_render = False
            unhid = unhide_collection_in_view_layer(context.view_layer, col)
            if not unhid:
                for vl in context.scene.view_layers:
                    if unhide_collection_in_view_layer(vl, col):
                        break

        if context.mode != 'OBJECT':
            bpy.ops.object.mode_set(mode='OBJECT')

        bpy.ops.object.select_all(action='DESELECT')
        curve_obj.select_set(True)
        context.view_layer.objects.active = curve_obj
        bpy.ops.object.mode_set(mode='EDIT')
        self.report({'INFO'}, f"Editing curve: {curve_obj.name}")
        return {'FINISHED'}


class SCULPT_OT_return_to_sculpt(bpy.types.Operator):
    """Return to Sculpt mode and select linked curve in Sculpt panel"""
    bl_idname = "sculpt.return_to_sculpt"
    bl_label = "Return to Sculpt"
    bl_options = {'REGISTER', 'UNDO'}

    def execute(self, context):
        scene = context.scene
        obj = context.object

        if not obj or obj.type != 'CURVE':
            self.report({'ERROR'}, "Active object is not a curve")
            return {'CANCELLED'}

        curve_obj = obj
        curve_name = curve_obj.name
        target_mesh = None

        # Find the linked mesh
        for col in curve_obj.users_collection:
            if col.name.startswith("Curves for "):
                mesh_name = col.name[len("Curves for "):]
                mesh_obj = bpy.data.objects.get(mesh_name)
                if mesh_obj and mesh_obj.type == 'MESH':
                    target_mesh = mesh_obj
                    break

        if not target_mesh:
            self.report({'ERROR'}, f"No linked mesh found for curve '{curve_name}'")
            return {'CANCELLED'}

        # Switch to Sculpt Mode on target mesh
        bpy.ops.object.mode_set(mode='OBJECT')
        bpy.ops.object.select_all(action='DESELECT')
        target_mesh.select_set(True)
        context.view_layer.objects.active = target_mesh
        bpy.ops.object.mode_set(mode='SCULPT')

        # Select the correct curve in UI
        for i, item in enumerate(scene.curve_list):
            if item.obj_ref == curve_obj:
                scene.curve_list_index = i
                break

        self.report({'INFO'}, f"Returned to Sculpt mode for '{target_mesh.name}' and selected curve '{curve_name}'")
        return {'FINISHED'}


class SCULPT_OT_apply_curve(bpy.types.Operator):
    """Convert curve to applied mesh and move into Applied_Curves collection"""
    bl_idname = "sculpt.apply_curve"
    bl_label = "Apply Curve"
    bl_options = {'REGISTER', 'UNDO'}

    curve_name: bpy.props.StringProperty()

    def execute(self, context):
        # Store previous state
        prev_mode = context.mode
        prev_active_obj = context.view_layer.objects.active

        # Get curve object
        curve_obj = bpy.data.objects.get(self.curve_name)
        if not curve_obj or curve_obj.type != 'CURVE':
            self.report({'ERROR'}, "Invalid or missing curve object")
            return {'CANCELLED'}

        # Make curve visible
        curve_obj.hide_set(False)
        curve_obj.hide_viewport = False
        curve_obj.hide_render = False
        for col in curve_obj.users_collection:
            col.hide_viewport = False
            unhid = unhide_collection_in_view_layer(context.view_layer, col)
            if not unhid:
                for vl in context.scene.view_layers:
                    if unhide_collection_in_view_layer(vl, col):
                        break

        # Select the curve
        if context.mode != 'OBJECT':
            bpy.ops.object.mode_set(mode='OBJECT')
        bpy.ops.object.select_all(action='DESELECT')
        curve_obj.select_set(True)
        context.view_layer.objects.active = curve_obj

        # Prepare Applied_Curves collection
        top_col_name = "Stroke Along Curves"
        applied_col_name = "Applied_Curves"

        top_col = bpy.data.collections.get(top_col_name)
        if not top_col:
            top_col = bpy.data.collections.new(top_col_name)
            context.scene.collection.children.link(top_col)
        else:
            top_col.hide_viewport = False
            top_col.hide_render = False
            unhide_collection_in_view_layer(context.view_layer, top_col)

        applied_col = top_col.children.get(applied_col_name)
        if not applied_col:
            applied_col = bpy.data.collections.new(applied_col_name)
            top_col.children.link(applied_col)
        else:
            applied_col.hide_viewport = False
            applied_col.hide_render = False
            unhide_collection_in_view_layer(context.view_layer, applied_col)

        # Delete any existing applied mesh
        applied_name = f"{curve_obj.name}_applied"
        old_mesh = applied_col.objects.get(applied_name)
        if old_mesh:
            bpy.data.objects.remove(old_mesh, do_unlink=True)

        # Duplicate Curve
        bpy.ops.object.select_all(action='DESELECT')
        curve_obj.select_set(True)
        context.view_layer.objects.active = curve_obj
        bpy.ops.object.duplicate_move(OBJECT_OT_duplicate={"linked": False, "mode": 'TRANSLATION'})

        applied_obj = context.object
        applied_name = f"{curve_obj.name}_applied"
        applied_obj.name = applied_name

        # Move to applied_curves collection
        for col in list(applied_obj.users_collection):
            col.objects.unlink(applied_obj)
        applied_col.objects.link(applied_obj)
        
        # Convert spline to NURBS
        bpy.ops.object.mode_set(mode='EDIT')
        bpy.ops.curve.select_all(action='SELECT')
        bpy.ops.curve.spline_type_set(type='NURBS', use_handles=True)
        bpy.ops.object.mode_set(mode='OBJECT')

        # Remove all modifiers on the duplicate curve
        for mod in list(applied_obj.modifiers):
            applied_obj.modifiers.remove(mod)

        # Convert duplicate curve → mesh
        old_name = applied_name
        bpy.ops.object.convert(target='MESH', keep_original=False)

        applied_obj = context.object
        applied_obj.name = old_name

        # Select both original curve & applied mesh
        bpy.ops.object.select_all(action='DESELECT')
        curve_obj.select_set(True)
        applied_obj.select_set(True)
        context.view_layer.objects.active = curve_obj

        # Copy modifiers from original curve to applied mesh
        bpy.ops.object.make_links_data(type='MODIFIERS')

        # Deselect all
        bpy.ops.object.select_all(action='DESELECT')
        
        # Ensure applied object is active and selected
        applied_obj.select_set(True)
        context.view_layer.objects.active = applied_obj

        # Apply all modifiers
        for mod in list(applied_obj.modifiers):
            try:
                bpy.ops.object.modifier_apply(modifier=mod.name)
            except Exception as e:
                print(f"⚠️ Failed to apply modifier {mod.name}: {e}")
        
        # Space vertices
        bpy.ops.object.mode_set(mode='EDIT')
        bpy.ops.mesh.select_all(action='SELECT')
        bpy.ops.mesh.looptools_space(
            influence=100,
            input='selected',
            interpolation='linear'
        )
        bpy.ops.object.mode_set(mode='OBJECT')

        # Restore previous active object
        if prev_active_obj and prev_active_obj.name in bpy.data.objects:
            bpy.ops.object.select_all(action='DESELECT')
            prev_active_obj.select_set(True)
            context.view_layer.objects.active = prev_active_obj

        # Restore previous mode
        try:
            if prev_mode != 'OBJECT':
                bpy.ops.object.mode_set(mode=prev_mode)
        except:
            pass

        # Hide Applied_Curves collection
        applied_col.hide_viewport = True
        applied_col.hide_render = True

        return {'FINISHED'}


class MESH_OT_space_vertices(bpy.types.Operator):
    """Select a mesh, enter edit mode, space vertices with LoopTools"""
    bl_idname = "mesh.space_vertices"
    bl_label = "Space Vertices"
    bl_options = {'REGISTER', 'UNDO'}

    mesh_name: bpy.props.StringProperty(
        name="Mesh Name",
        description="Name of the mesh to space vertices on",
        default=""
    )

    def execute(self, context):
        if context.mode != 'OBJECT':
            bpy.ops.object.mode_set(mode='OBJECT')

        if not self.mesh_name:
            mesh_obj = context.active_object
            if not mesh_obj:
                self.report({'ERROR'}, "No active object found")
                return {'CANCELLED'}
            self.mesh_name = mesh_obj.name
        else:
            mesh_obj = bpy.data.objects.get(self.mesh_name)

        if not mesh_obj or mesh_obj.type != 'MESH':
            self.report({'ERROR'}, f"Mesh '{self.mesh_name}' not found or not a mesh")
            return {'CANCELLED'}

        for obj in context.view_layer.objects:
            obj.select_set(False)

        mesh_obj.select_set(True)
        context.view_layer.objects.active = mesh_obj

        bpy.ops.object.mode_set(mode='EDIT')
        bpy.ops.mesh.select_all(action='SELECT')

        try:
            bpy.ops.mesh.looptools_space(
                influence=100,
                input='selected',
                interpolation='linear'
            )
        except Exception as e:
            self.report({'WARNING'}, f"LoopTools failed: {e}")

        bpy.ops.object.mode_set(mode='OBJECT')
        return {'FINISHED'}


class SCULPT_OT_along_curve(bpy.types.Operator):
    """Sculpt Along Curve using applied mesh"""
    bl_idname = "sculpt.along_curve"
    bl_label = "Sculpt Along Curve"
    bl_options = {'REGISTER', 'UNDO'}

    def execute(self, context):
        scene = context.scene
        obj = context.object

        # Ensure Sculpt Mode for undo tracking
        prev_mode = context.mode
        if prev_mode != 'SCULPT' and obj:
            bpy.ops.object.mode_set(mode='SCULPT')

        try:
            # Tool validation
            if not is_active_sculpt_tool_valid_multires(context):
                self.report({'WARNING'}, "Active tool must be Mask, Brush, Paint (Except in Multires), or Draw Face Sets")
                return {'CANCELLED'}

            # Determine curve object
            if obj and obj.type == 'CURVE':
                curve_obj = obj
            else:
                idx = scene.curve_list_index
                if idx >= len(scene.curve_list):
                    self.report({'ERROR'}, "No curve selected")
                    return {'CANCELLED'}
                curve_obj = scene.curve_list[idx].obj_ref
                if not curve_obj:
                    self.report({'ERROR'}, "Curve reference invalid")
                    return {'CANCELLED'}

            # Determine linked mesh
            target_mesh = None
            for col in curve_obj.users_collection:
                if col.name.startswith("Curves for "):
                    mesh_name = col.name[len("Curves for "):]
                    mesh_obj = bpy.data.objects.get(mesh_name)
                    if mesh_obj and mesh_obj.type == 'MESH':
                        target_mesh = mesh_obj
                        break

            if not target_mesh:
                self.report({'ERROR'}, "No mesh linked to this curve")
                return {'CANCELLED'}

            # Apply the curve
            bpy.ops.sculpt.apply_curve(curve_name=curve_obj.name)

            applied_mesh_name = f"{curve_obj.name}_applied"
            applied_mesh = bpy.data.objects.get(applied_mesh_name)
            if not applied_mesh:
                self.report({'ERROR'}, "Applied mesh not found")
                return {'CANCELLED'}

            # Run stroke_action using applied mesh
            bpy.ops.sculpt.stroke_action(
                curve_name=applied_mesh.name,
                mesh_name=target_mesh.name
            )

            # Re-select curve in UI list
            for i, item in enumerate(scene.curve_list):
                if item.obj_ref == curve_obj:
                    scene.curve_list_index = i
                    break

            # Push undo manually
            bpy.ops.ed.undo_push(message="Sculpt Along Curve")

        finally:
            # Restore previous mode
            if prev_mode != context.mode:
                bpy.ops.object.mode_set(mode=prev_mode)

        return {'FINISHED'}


class SCULPT_OT_along_curve_editmode(bpy.types.Operator):
    """Sculpt Along Curve (Edit Mode)"""
    bl_idname = "sculpt.along_curve_editmode"
    bl_label = "Sculpt Along Curve (Edit Mode)"
    bl_options = {'REGISTER', 'UNDO'}

    _timer = None

    def execute(self, context):
        obj = context.object
        if not obj or obj.type != 'CURVE' or context.mode != 'EDIT_CURVE':
            self.report({'ERROR'}, "This operator can only be run in Curve Edit Mode")
            return {'CANCELLED'}

        self.curve_obj = obj

        # Return to Sculpt
        try:
            bpy.ops.sculpt.return_to_sculpt()
        except Exception as e:
            self.report({'ERROR'}, f"Failed to return to sculpt: {e}")
            return {'CANCELLED'}

        # Run Sculpt Along Curve logic
        try:
            bpy.ops.sculpt.along_curve('INVOKE_DEFAULT')
        except Exception as e:
            self.report({'ERROR'}, f"Failed sculpt along curve logic: {e}")
            return {'CANCELLED'}

        # Start adaptive timer to return to Edit Curve
        self._timer = bpy.app.timers.register(self.check_sculpt_mode, first_interval=0.05)
        return {'FINISHED'}

    def check_sculpt_mode(self):
        """Check if the active object is in Sculpt Mode, then return to Edit Curve"""
        if bpy.context.object and bpy.context.object.mode == 'SCULPT':
            try:
                bpy.ops.sculpt.edit_curve()
                self.report({'INFO'}, f"Returned to edit curve '{self.curve_obj.name}'")
            except Exception as e:
                self.report({'WARNING'}, f"Could not re-enter curve edit mode: {e}")
            return None  # stop timer
        else:
            return 0.05  # check again


class SCULPT_OT_add_curve_layer(bpy.types.Operator):
    """Add a brush stroke along the curve"""
    bl_idname = "sculpt.add_curve_layer"
    bl_label = "Add Curve Stroke"
    bl_options = {'REGISTER', 'UNDO'}

    def execute(self, context):
        scene = context.scene
        obj = context.object
        
        if not is_active_sculpt_tool_valid_multires(context):
            self.report({'WARNING'}, "Active tool must be Mask, Brush, Paint (Except in Multires), or Draw Face Sets")
            return {'CANCELLED'}

        # Determine selected curve from UI
        idx = scene.curve_list_index
        if idx >= len(scene.curve_list):
            self.report({'ERROR'}, "No curve selected")
            return {'CANCELLED'}

        curve_obj = scene.curve_list[idx].obj_ref
        if not curve_obj:
            self.report({'ERROR'}, "Curve reference invalid")
            return {'CANCELLED'}

        curve_name = curve_obj.name
        applied_name = f"{curve_name}_applied"

        # Find corresponding mesh
        target_mesh = None
        for col in curve_obj.users_collection:
            if col.name.startswith("Curves for "):
                mesh_name = col.name[len("Curves for "):]
                mesh_obj = bpy.data.objects.get(mesh_name)
                if mesh_obj and mesh_obj.type == 'MESH':
                    target_mesh = mesh_obj
                    break

        if not target_mesh:
            self.report({'ERROR'}, "No mesh linked to selected curve")
            return {'CANCELLED'}

        # Check Applied_Curves collection & find applied mesh
        top_col = bpy.data.collections.get("Stroke Along Curves")
        if not top_col:
            bpy.ops.sculpt.along_curve()
            return {'FINISHED'}

        applied_col = top_col.children.get("Applied_Curves")
        if not applied_col:
            bpy.ops.sculpt.along_curve()
            return {'FINISHED'}

        applied_mesh = applied_col.objects.get(applied_name)
        if not applied_mesh:
            bpy.ops.sculpt.along_curve()
            return {'FINISHED'}

        # Add stroke using the applied mesh
        bpy.ops.sculpt.stroke_action(
            curve_name=applied_mesh.name,
            mesh_name=target_mesh.name
        )

        bpy.ops.mesh.loose_edge_info(mesh_name=applied_mesh.name)
        self.report({'INFO'}, f"Added stroke for curve '{curve_name}'")
        return {'FINISHED'}


class SCULPT_OT_add_curve_layer_editmode(bpy.types.Operator):
    """Add a brush stroke along the curve (edit mode)"""
    bl_idname = "sculpt.add_curve_layer_editmode"
    bl_label = "Add Curve Stroke (Edit Mode)"
    bl_options = {'REGISTER', 'UNDO'}
    
    _timer = None

    def execute(self, context):
        obj = context.object
        if not obj or obj.type != 'CURVE' or context.mode != 'EDIT_CURVE':
            self.report({'ERROR'}, "This operator can only be run in Curve Edit Mode")
            return {'CANCELLED'}

        self.curve_obj = obj

        # Return to Sculpt
        try:
            bpy.ops.sculpt.return_to_sculpt()
        except Exception as e:
            self.report({'ERROR'}, f"Failed to return to sculpt: {e}")
            return {'CANCELLED'}

        # Run Add Curve logic
        try:
            bpy.ops.sculpt.add_curve_layer('INVOKE_DEFAULT')
        except Exception as e:
            self.report({'ERROR'}, f"Failed sculpt along curve logic: {e}")
            return {'CANCELLED'}

        # Start adaptive timer to return to Edit Curve
        self._timer = bpy.app.timers.register(self.check_sculpt_mode, first_interval=0.05)
        return {'FINISHED'}

    def check_sculpt_mode(self):
        """Check if the active object is in Sculpt Mode, then return to Edit Curve"""
        if bpy.context.object and bpy.context.object.mode == 'SCULPT':
            try:
                bpy.ops.sculpt.edit_curve()
                self.report({'INFO'}, f"Returned to edit curve '{self.curve_obj.name}'")
            except Exception as e:
                self.report({'WARNING'}, f"Could not re-enter curve edit mode: {e}")
            return None
        else:
            return 0.05


class SCULPT_OT_create_pattern(bpy.types.Operator):
    """Create a Pattern Curve based on the selected Sculpt Curve"""
    bl_idname = "sculpt.create_pattern"
    bl_label = "Create Pattern"
    bl_options = {'REGISTER', 'UNDO'}

    def execute(self, context):
        scene = context.scene

        # Get selected curve from UI list
        idx = scene.curve_list_index
        if idx < 0 or idx >= len(scene.curve_list):
            self.report({'ERROR'}, "No curve selected")
            return {'CANCELLED'}

        original_curve = scene.curve_list[idx].obj_ref
        if not original_curve:
            self.report({'ERROR'}, "Selected curve reference missing")
            return {'CANCELLED'}

        curve_name = original_curve.name
        
        # Unhide curve + collections
        view_layer = context.view_layer
        original_curve.hide_set(False)
        original_curve.hide_viewport = False
        original_curve.hide_render = False

        for col in original_curve.users_collection:
            unhide_collection_in_view_layer(view_layer, col)
            col.hide_viewport = False
            col.hide_render = False

        # Find linked mesh
        mesh_name = None
        for col in original_curve.users_collection:
            if col.name.startswith("Curves for "):
                mesh_name = col.name[len("Curves for "):]
                break

        if not mesh_name:
            self.report({'ERROR'}, "Mesh linked to this curve not found")
            return {'CANCELLED'}

        # Ensure Object Mode
        if context.mode != 'OBJECT':
            bpy.ops.object.mode_set(mode='OBJECT')

        bpy.ops.object.select_all(action='DESELECT')

        # Create a new Bezier Circle
        bpy.ops.curve.primitive_bezier_circle_add()
        new_curve_pattern = context.object
        new_curve_pattern.data.resolution_u = 15

        # Rename with automatic numbering
        base = f"{curve_name}_Pattern"
        i = 1
        while True:
            candidate = f"{base}{i:02d}"
            if candidate not in bpy.data.objects:
                new_curve_pattern.name = candidate
                break
            i += 1

        # Move to the same collections as original curve
        for col in new_curve_pattern.users_collection:
            col.objects.unlink(new_curve_pattern)

        for col in original_curve.users_collection:
            col.objects.link(new_curve_pattern)
            
        # Add Array Modifier
        array_mod = new_curve_pattern.modifiers.new("PatternArray", 'ARRAY')
        array_mod.fit_type = 'FIT_CURVE'
        array_mod.curve = original_curve
        array_mod.use_relative_offset = True
        array_mod.relative_offset_displace[0] = 0.0
        array_mod.relative_offset_displace[1] = 1.0
        array_mod.use_merge_vertices = True
        array_mod.use_merge_vertices_cap = True

        # Add Curve Modifier
        curve_mod = new_curve_pattern.modifiers.new("PatternCurve", 'CURVE')
        curve_mod.object = original_curve
        curve_mod.deform_axis = 'POS_Y'
        curve_mod.show_in_editmode = True
        
        # Add Shrinkwrap modifier
        target_mesh_obj = bpy.data.objects.get(mesh_name)
        if target_mesh_obj:
            shrinkwrap_mod = new_curve_pattern.modifiers.new("Shrinkwrap", 'SHRINKWRAP')
            shrinkwrap_mod.target = target_mesh_obj
            shrinkwrap_mod.wrap_method = 'NEAREST_SURFACEPOINT'
            shrinkwrap_mod.wrap_mode = 'ABOVE_SURFACE'
                
        # Add Smooth modifier
        smooth_mod = new_curve_pattern.modifiers.new("PatternSmooth", 'SMOOTH')
        smooth_mod.iterations = 30
        
        # Add Copy Transforms Constraint
        copy_trans = new_curve_pattern.constraints.new(type='COPY_TRANSFORMS')
        copy_trans.target = original_curve
        
        # Enter Edit Pattern mode
        if context.mode != 'OBJECT':
            bpy.ops.object.mode_set(mode='OBJECT')

        bpy.ops.object.select_all(action='DESELECT')
        new_curve_pattern.select_set(True)
        context.view_layer.objects.active = new_curve_pattern

        # Update UI list selection
        scene.curve_list_index = 0
        for i, item in enumerate(scene.curve_list):
            if item.obj_ref == new_curve_pattern:
                scene.curve_list_index = i
                break

        # Enter Edit Mode and call edit pattern operator
        bpy.ops.object.editmode_toggle()
        bpy.ops.sculpt.edit_pattern()
        
        self.report({'INFO'}, f"Created pattern curve: {new_curve_pattern.name}")
        return {'FINISHED'}


class SCULPT_OT_edit_pattern(bpy.types.Operator):
    """Edit the selected Pattern Curve"""
    bl_idname = "sculpt.edit_pattern"
    bl_label = "Edit Pattern"
    bl_options = {'REGISTER', 'UNDO'}

    def execute(self, context):
        obj = context.object
        if not obj or obj.type != 'CURVE':
            self.report({'ERROR'}, "Please select a curve pattern")
            return {'CANCELLED'}
        
        # Set the flag
        context.scene.is_edit_pattern_mode = True

        # Check Local View state
        space = context.space_data
        already_local = False

        if space and space.type == 'VIEW_3D':
            already_local = space.local_view is not None

        # Go into Local View (only if not already)
        if not already_local:
            bpy.ops.view3d.localview()
            
        # Disable visibility for all Constraints
        for constraint in obj.constraints:
            constraint.mute = True

        # Disable visibility for Shrinkwrap & Curve modifiers
        for mod in obj.modifiers:
            if mod.type in {'CURVE', 'SHRINKWRAP'}:
                mod.show_viewport = False

        # Frame selected
        bpy.ops.view3d.view_selected()
        bpy.ops.view3d.view_axis(type='TOP')

        self.report({'INFO'}, f"Editing pattern: {obj.name}")
        return {'FINISHED'}


class SCULPT_OT_edit_pattern_inverse(bpy.types.Operator):
    """Exit pattern edit and restore constraints & modifiers"""
    bl_idname = "sculpt.edit_pattern_inverse"
    bl_label = "Exit Pattern Edit"
    bl_options = {'REGISTER', 'UNDO'}

    def execute(self, context):
        obj = context.object
        if not obj or obj.type != 'CURVE':
            self.report({'ERROR'}, "Please select a pattern curve")
            return {'CANCELLED'}
        
        # Reset the flag
        context.scene.is_edit_pattern_mode = False

        # Check Local View state
        space = context.space_data
        already_local = False

        if space and space.type == 'VIEW_3D':
            already_local = space.local_view is not None

        # Exit Local View (only if already in it)
        if already_local:
            bpy.ops.view3d.localview()
            
        # Enable visibility for all Constraints
        for constraint in obj.constraints:
            constraint.mute = False

        # Enable visibility for Shrinkwrap & Curve modifiers
        for mod in obj.modifiers:
            if mod.type in {'CURVE', 'SHRINKWRAP'}:
                mod.show_viewport = True

        self.report({'INFO'}, f"Restored pattern: {obj.name}")
        return {'FINISHED'}


class SCULPT_OT_toggle_curve_visibility(bpy.types.Operator):
    bl_idname = "sculpt.toggle_curve_visibility"
    bl_label = "Toggle Curve Visibility"
    bl_options = {'INTERNAL'}

    curve_name: bpy.props.StringProperty()

    def execute(self, context):
        curve = bpy.data.objects.get(self.curve_name)
        if curve and curve.type == 'CURVE':
            curve.hide_set(not curve.hide_get())
        return {'FINISHED'}


class SCULPT_OT_delete_curve(bpy.types.Operator):
    """Delete a curve from the scene and UI list"""
    bl_idname = "sculpt.delete_curve"
    bl_label = "Delete Curve"
    bl_options = {'REGISTER', 'UNDO'}

    index: bpy.props.IntProperty()

    def safe_mode_switch(self, context, obj, mode):
        if obj and obj.name in bpy.data.objects:
            context.view_layer.objects.active = obj
            try:
                bpy.ops.object.mode_set(mode=mode)
            except RuntimeError:
                pass

    def execute(self, context):
        scene = context.scene

        if self.index < 0 or self.index >= len(scene.curve_list):
            return {'CANCELLED'}

        # Curve being deleted
        item_to_delete = scene.curve_list[self.index]
        obj_to_delete = item_to_delete.obj_ref

        # UI-selected curve
        selected_idx = scene.curve_list_index
        selected_item = scene.curve_list[selected_idx] if 0 <= selected_idx < len(scene.curve_list) else None
        selected_curve = selected_item.obj_ref if selected_item else None

        # If user is in EDIT mode and is deleting the selected curve,
        # switch selection to a different curve BEFORE deleting.
        in_edit_curve_mode = (
            context.object == obj_to_delete and 
            context.mode == 'EDIT_CURVE'
        )

        if in_edit_curve_mode:
            if len(scene.curve_list) > 1:
                if self.index + 1 < len(scene.curve_list):
                    scene.curve_list_index = self.index + 1
                else:
                    scene.curve_list_index = self.index - 1

                # Switch edit mode to the new curve safely
                new_item = scene.curve_list[scene.curve_list_index]
                new_obj = new_item.obj_ref
                self.safe_mode_switch(context, new_obj, 'EDIT')
            else:
                bpy.ops.object.mode_set(mode='OBJECT')

        # Delete the curve object
        if obj_to_delete and obj_to_delete.name in bpy.data.objects:
            bpy.data.objects.remove(obj_to_delete, do_unlink=True)

        # Remove from UI list
        scene.curve_list.remove(self.index)

        # Maintain / adjust UI list selection
        if obj_to_delete == selected_curve:
            if len(scene.curve_list) > 0:
                scene.curve_list_index = min(scene.curve_list_index, len(scene.curve_list) - 1)
            else:
                scene.curve_list_index = -1

        elif self.index < selected_idx:
            scene.curve_list_index -= 1

        return {'FINISHED'}


# --- UI List --------------------------------------------------------

class SCULPT_UL_curve_list(bpy.types.UIList):
    bl_idname = "SCULPT_UL_curve_list"

    def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index):
        if not item.obj_ref:
            layout.label(text="(missing)", icon='ERROR')
            return

        row = layout.row(align=True)
        # Eye icon toggle
        eye_icon = 'HIDE_OFF' if not item.obj_ref.hide_get() else 'HIDE_ON'
        op = row.operator("sculpt.toggle_curve_visibility", text="", icon=eye_icon)
        op.curve_name = item.obj_ref.name

        # Curve name label
        row.label(text=item.obj_ref.name, icon='CURVE_PATH')

        # Delete button
        op = row.operator("sculpt.delete_curve", text="", icon="X", emboss=False)
        op.index = index


# --- Panel ----------------------------------------------------------

class SCULPT_PT_along_curve_panel(bpy.types.Panel):
    bl_label = "Sculpt Along Curve"
    bl_idname = "SCULPT_PT_along_curve_panel"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = 'Edit'  # Теперь панель будет во вкладке Edit

    @classmethod
    def poll(cls, context):
        obj = context.object
        if not obj:
            return False
        # Panel appears in Sculpt mode on meshes, and Edit Curve mode on curves
        return (
            (obj.type == 'MESH' and context.mode == 'SCULPT') or
            (obj.type == 'CURVE' and context.mode == 'EDIT_CURVE')
        )

    def draw(self, context):
        layout = self.layout
        scene = context.scene
        obj = context.object
        curves_exist = len(scene.curve_list) > 0
        is_mesh = (obj.type == 'MESH' and context.mode == 'SCULPT')
        is_curve = (obj.type == 'CURVE' and context.mode == 'EDIT_CURVE')

        # ----------------------------
        # Normal UI
        # ----------------------------
        col = layout.column()

        # Curve list
        if curves_exist and (is_mesh or is_curve):
            col.template_list(
                "SCULPT_UL_curve_list", "",
                scene, "curve_list",
                scene, "curve_list_index",
                rows=4
            )

        # ----------------------------
        # Hide everything except 'Edit Pattern Inverse' in edit pattern mode
        # ----------------------------
        if getattr(scene, "is_edit_pattern_mode", False):
            row = col.row()
            row.operator("sculpt.edit_pattern_inverse", text="Exit Pattern Edit")
            return  # Stop drawing other buttons

        # ----------------------------
        # Determine selected curve
        # ----------------------------
        selected_curve = None
        idx = scene.curve_list_index
        if idx >= 0 and idx < len(scene.curve_list):
            selected_curve = scene.curve_list[idx].obj_ref

        # ----------------------------
        # Add Path Curve
        # ----------------------------
        col.operator("sculpt.create_path_curve", text="Add Path Curve")

        # ----------------------------
        # Sculpt Mode buttons (mesh)
        # ----------------------------
        if is_mesh and curves_exist:
            row = col.row(align=True)
            row.operator("sculpt.along_curve", text="Sculpt Along Curve")
            row.operator("sculpt.add_curve_layer", text="", icon="PLUS")
            col.operator("sculpt.edit_curve", text="Edit Curve")

            # Show "Create Pattern" only if selected curve is NOT a pattern
            show_create_pattern = True
            if selected_curve and re.search(r"_Pattern\d+$", selected_curve.name):
                show_create_pattern = False
            if show_create_pattern:
                col.operator("sculpt.create_pattern", text="Create Pattern")

        # ----------------------------
        # Curve Edit Mode buttons
        # ----------------------------
        if is_curve:
            row = col.row(align=True)
            row.operator("sculpt.along_curve_editmode", text="Sculpt Along Curve")
            row.operator("sculpt.add_curve_layer_editmode", text="", icon="PLUS")
            col.operator("sculpt.return_to_sculpt", text="Return to Sculpt")

            # Only show "Edit Pattern" if selected curve is a pattern
            if selected_curve and re.search(r"_Pattern\d+$", selected_curve.name):
                col.operator("sculpt.edit_pattern", text="Edit Pattern")

        # Display curve resolution property
        if selected_curve and selected_curve.type == 'CURVE':
            layout.prop(scene, "curve_resolution_u")


# --- Registration ---------------------------------------------------

classes = [
    CurveItem,
    SCULPT_OT_create_path_curve,
    SCULPT_OT_edit_curve,
    SCULPT_OT_return_to_sculpt,
    SCULPT_OT_along_curve,
    SCULPT_OT_along_curve_editmode,
    SCULPT_OT_add_curve_layer_editmode,
    SCULPT_UL_curve_list,
    SCULPT_PT_along_curve_panel,
    SCULPT_OT_add_curve_layer,
    SCULPT_OT_stroke_action,
    MESH_OT_space_vertices,
    SCULPT_OT_apply_curve,
    SCULPT_OT_create_pattern,
    SCULPT_OT_edit_pattern,
    SCULPT_OT_edit_pattern_inverse,
    SCULPT_OT_toggle_curve_visibility,
    MESH_OT_loose_edge_info,
    SCULPT_OT_delete_curve,
]

_refresh_handler_added = False

def register():
    global _refresh_handler_added

    # Register all classes
    for cls in classes:
        bpy.utils.register_class(cls)

    # Register Scene properties
    bpy.types.Scene.curve_list = bpy.props.CollectionProperty(type=CurveItem)
    bpy.types.Scene.curve_list_index = bpy.props.IntProperty(
        default=0,
        update=update_curve_selection
    )
    bpy.types.Scene.is_edit_pattern_mode = bpy.props.BoolProperty(
        name="Edit Pattern Mode",
        description="True when user is editing a pattern",
        default=False
    )
    bpy.types.Scene.curve_resolution_u = bpy.props.IntProperty(
        name="Curve Resolution U",
        description="Resolution U of the selected curve",
        default=15,
        min=1,
        max=100,
        update=lambda self, context: update_curve_resolution(context)
    )

    # Refresh curve list immediately
    try:
        refresh_curve_list(bpy.context.scene)
    except Exception as e:
        print(f"⚠️ Failed to refresh curve list on register: {e}")

    # Add persistent handler
    if refresh_curve_list_handler not in bpy.app.handlers.depsgraph_update_post:
        bpy.app.handlers.depsgraph_update_post.append(refresh_curve_list_handler)
        _refresh_handler_added = True


def unregister():
    global _refresh_handler_added

    # Remove scene properties
    for prop in ["curve_list", "curve_list_index", "is_edit_pattern_mode", "curve_resolution_u"]:
        try:
            delattr(bpy.types.Scene, prop)
        except Exception:
            pass

    # Remove depsgraph handler safely
    if _refresh_handler_added:
        bpy.app.handlers.depsgraph_update_post[:] = [
            h for h in bpy.app.handlers.depsgraph_update_post
            if h != refresh_curve_list_handler
        ]
        _refresh_handler_added = False

    # Unregister classes in reverse order
    for cls in reversed(classes):
        bpy.utils.unregister_class(cls)
        

if __name__ == "__main__":
    register()