Grease pencil

https://www.youtube.com/watch?v=fis2lTuXDFU&list=PLrrSLz9DKjHV9rXxRYiuYrX9PZAVX25n7&index=4

# =========================================================
# アドオン宣言 (必ずファイルの先頭に配置)
# =========================================================
bl_info = {
    "name": "GP Easy Paint",
    "author": "AI",
    "version": (1, 5, 0),
    "blender": (5, 0, 0),
    "location": "View3D > Sidebar > GP Easy Paint",
    "description": "GPオブジェクト作成 → ツール/太さ/透明度/色 → レイヤー → エフェクト → 完成",
    "category": "GP Easy Paint",
}

import bpy
import os
from bpy.types import Operator, Panel, PropertyGroup
from bpy.props import StringProperty, PointerProperty, FloatVectorProperty, IntProperty, FloatProperty

# =========================================================
# 1. 共通定義
# =========================================================
ADDON_TITLE = "GP Easy Paint"
PREFIX_VAL = "GPEASY"
PREFIX = PREFIX_VAL.lower()
ADDON_CATEGORY = "GP Easy Paint"

DEFAULT_COL_NAME = "GreaseObjects"
DEFAULT_SUB_COL_NAME = "SubObjects"
DEFAULT_BASE_NAME = "GP_"

# ===================================================================
# 2. リンク定義
# ===================================================================
LINK_NAME_1 = "カスタムリンク"
LINK_URL_1 = "<https://example.com>"
LINK_NAME_2 = "コンテナ builder"
LINK_URL_2 = "<https://note.com/zionad2017/n/ndb6dbdbc844a>"
LINK_NAME_3 = "posfie"
LINK_URL_3 = "<https://posfie.com/@timekagura/t/zionad2022?sort=0>"
LINK_NAME_4 = "zionadchat"
LINK_URL_4 = "<https://x.com/zionadchat>"

# =========================================================
# 3. プロパティグループ
# =========================================================
class GPEASY_Properties(PropertyGroup):
    col_name: StringProperty(name="主コレクション", default=DEFAULT_COL_NAME)
    sub_col_name: StringProperty(name="サブコレクション", default=DEFAULT_SUB_COL_NAME)
    obj_base_name: StringProperty(name="ベース名", default=DEFAULT_BASE_NAME)

    link_1_name: StringProperty(name="リンク1名", default=LINK_NAME_1)
    link_1_url: StringProperty(name="リンク1URL", default=LINK_URL_1)
    
    col_01_name: StringProperty(default="黒"); col_01_stroke: FloatVectorProperty(subtype='COLOR', size=4, default=(0.050, 0.050, 0.050, 1.000), min=0.0, max=1.0); col_01_fill: FloatVectorProperty(subtype='COLOR', size=4, default=(0.300, 0.300, 0.300, 0.500), min=0.0, max=1.0)
    col_02_name: StringProperty(default="白"); col_02_stroke: FloatVectorProperty(subtype='COLOR', size=4, default=(1.000, 1.000, 1.000, 1.000), min=0.0, max=1.0); col_02_fill: FloatVectorProperty(subtype='COLOR', size=4, default=(0.900, 0.900, 0.900, 0.500), min=0.0, max=1.0)
    col_03_name: StringProperty(default="灰"); col_03_stroke: FloatVectorProperty(subtype='COLOR', size=4, default=(0.400, 0.400, 0.400, 1.000), min=0.0, max=1.0); col_03_fill: FloatVectorProperty(subtype='COLOR', size=4, default=(0.600, 0.600, 0.600, 0.500), min=0.0, max=1.0)
    col_04_name: StringProperty(default="赤"); col_04_stroke: FloatVectorProperty(subtype='COLOR', size=4, default=(0.800, 0.100, 0.100, 1.000), min=0.0, max=1.0); col_04_fill: FloatVectorProperty(subtype='COLOR', size=4, default=(0.900, 0.400, 0.400, 0.500), min=0.0, max=1.0)
    col_05_name: StringProperty(default="青"); col_05_stroke: FloatVectorProperty(subtype='COLOR', size=4, default=(0.100, 0.200, 0.800, 1.000), min=0.0, max=1.0); col_05_fill: FloatVectorProperty(subtype='COLOR', size=4, default=(0.400, 0.500, 0.900, 0.500), min=0.0, max=1.0)
    col_06_name: StringProperty(default="緑"); col_06_stroke: FloatVectorProperty(subtype='COLOR', size=4, default=(0.100, 0.700, 0.200, 1.000), min=0.0, max=1.0); col_06_fill: FloatVectorProperty(subtype='COLOR', size=4, default=(0.400, 0.900, 0.500, 0.500), min=0.0, max=1.0)
    col_07_name: StringProperty(default="黄"); col_07_stroke: FloatVectorProperty(subtype='COLOR', size=4, default=(0.900, 0.800, 0.100, 1.000), min=0.0, max=1.0); col_07_fill: FloatVectorProperty(subtype='COLOR', size=4, default=(0.900, 0.900, 0.500, 0.500), min=0.0, max=1.0)
    col_08_name: StringProperty(default="水"); col_08_stroke: FloatVectorProperty(subtype='COLOR', size=4, default=(0.100, 0.800, 0.900, 1.000), min=0.0, max=1.0); col_08_fill: FloatVectorProperty(subtype='COLOR', size=4, default=(0.500, 0.900, 0.900, 0.500), min=0.0, max=1.0)
    col_09_name: StringProperty(default="紫"); col_09_stroke: FloatVectorProperty(subtype='COLOR', size=4, default=(0.600, 0.100, 0.800, 1.000), min=0.0, max=1.0); col_09_fill: FloatVectorProperty(subtype='COLOR', size=4, default=(0.800, 0.500, 0.900, 0.500), min=0.0, max=1.0)
    col_10_name: StringProperty(default="橙"); col_10_stroke: FloatVectorProperty(subtype='COLOR', size=4, default=(0.900, 0.400, 0.100, 1.000), min=0.0, max=1.0); col_10_fill: FloatVectorProperty(subtype='COLOR', size=4, default=(0.900, 0.600, 0.400, 0.500), min=0.0, max=1.0)
    col_11_name: StringProperty(default="茶"); col_11_stroke: FloatVectorProperty(subtype='COLOR', size=4, default=(0.400, 0.200, 0.100, 1.000), min=0.0, max=1.0); col_11_fill: FloatVectorProperty(subtype='COLOR', size=4, default=(0.600, 0.400, 0.300, 0.500), min=0.0, max=1.0)
    col_12_name: StringProperty(default="桃"); col_12_stroke: FloatVectorProperty(subtype='COLOR', size=4, default=(0.900, 0.009, 0.000, 1.000), min=0.0, max=1.0); col_12_fill: FloatVectorProperty(subtype='COLOR', size=4, default=(0.900, 0.600, 0.800, 0.500), min=0.0, max=1.0)
    col_13_name: StringProperty(default="黄緑"); col_13_stroke: FloatVectorProperty(subtype='COLOR', size=4, default=(0.500, 0.900, 0.100, 1.000), min=0.0, max=1.0); col_13_fill: FloatVectorProperty(subtype='COLOR', size=4, default=(0.700, 0.900, 0.300, 0.500), min=0.0, max=1.0)
    col_14_name: StringProperty(default="深緑"); col_14_stroke: FloatVectorProperty(subtype='COLOR', size=4, default=(0.000, 0.300, 0.100, 1.000), min=0.0, max=1.0); col_14_fill: FloatVectorProperty(subtype='COLOR', size=4, default=(0.100, 0.500, 0.200, 0.500), min=0.0, max=1.0)
    col_15_name: StringProperty(default="紺"); col_15_stroke: FloatVectorProperty(subtype='COLOR', size=4, default=(0.000, 0.100, 0.400, 1.000), min=0.0, max=1.0); col_15_fill: FloatVectorProperty(subtype='COLOR', size=4, default=(0.100, 0.200, 0.500, 0.500), min=0.0, max=1.0)
    col_16_name: StringProperty(default="青紫"); col_16_stroke: FloatVectorProperty(subtype='COLOR', size=4, default=(0.300, 0.100, 0.700, 1.000), min=0.0, max=1.0); col_16_fill: FloatVectorProperty(subtype='COLOR', size=4, default=(0.500, 0.300, 0.800, 0.500), min=0.0, max=1.0)
    col_17_name: StringProperty(default="赤紫"); col_17_stroke: FloatVectorProperty(subtype='COLOR', size=4, default=(0.800, 0.100, 0.500, 1.000), min=0.0, max=1.0); col_17_fill: FloatVectorProperty(subtype='COLOR', size=4, default=(0.900, 0.400, 0.600, 0.500), min=0.0, max=1.0)
    col_18_name: StringProperty(default="朱"); col_18_stroke: FloatVectorProperty(subtype='COLOR', size=4, default=(0.900, 0.200, 0.100, 1.000), min=0.0, max=1.0); col_18_fill: FloatVectorProperty(subtype='COLOR', size=4, default=(0.900, 0.400, 0.300, 0.500), min=0.0, max=1.0)
    col_19_name: StringProperty(default="黄土"); col_19_stroke: FloatVectorProperty(subtype='COLOR', size=4, default=(0.700, 0.500, 0.200, 1.000), min=0.0, max=1.0); col_19_fill: FloatVectorProperty(subtype='COLOR', size=4, default=(0.800, 0.600, 0.400, 0.500), min=0.0, max=1.0)
    col_20_name: StringProperty(default="焦茶"); col_20_stroke: FloatVectorProperty(subtype='COLOR', size=4, default=(1.000, 0.000, 0.468, 1.000), min=0.0, max=1.0); col_20_fill: FloatVectorProperty(subtype='COLOR', size=4, default=(0.400, 0.200, 0.100, 0.500), min=0.0, max=1.0)
    col_21_name: StringProperty(default="薄灰"); col_21_stroke: FloatVectorProperty(subtype='COLOR', size=4, default=(0.700, 0.700, 0.700, 1.000), min=0.0, max=1.0); col_21_fill: FloatVectorProperty(subtype='COLOR', size=4, default=(0.800, 0.800, 0.800, 0.500), min=0.0, max=1.0)
    col_22_name: StringProperty(default="暗灰"); col_22_stroke: FloatVectorProperty(subtype='COLOR', size=4, default=(0.200, 0.200, 0.200, 1.000), min=0.0, max=1.0); col_22_fill: FloatVectorProperty(subtype='COLOR', size=4, default=(0.300, 0.300, 0.300, 0.500), min=0.0, max=1.0)
    col_23_name: StringProperty(default="肌"); col_23_stroke: FloatVectorProperty(subtype='COLOR', size=4, default=(1.000, 0.800, 0.700, 1.000), min=0.0, max=1.0); col_23_fill: FloatVectorProperty(subtype='COLOR', size=4, default=(1.000, 0.900, 0.800, 0.500), min=0.0, max=1.0)
    col_24_name: StringProperty(default="青緑"); col_24_stroke: FloatVectorProperty(subtype='COLOR', size=4, default=(0.100, 0.600, 0.500, 1.000), min=0.0, max=1.0); col_24_fill: FloatVectorProperty(subtype='COLOR', size=4, default=(0.300, 0.700, 0.600, 0.500), min=0.0, max=1.0)

# =========================================================
# 4. ユーティリティ
# =========================================================
def is_gp_object(obj):
    return obj and obj.type in {'GPENCIL', 'GREASEPENCIL'}

def get_next_name(base_name):
    max_num = 0
    for obj in bpy.data.objects:
        if is_gp_object(obj) and obj.name.startswith(base_name):
            try: max_num = max(max_num, int(obj.name.replace(base_name, "")))
            except ValueError: pass
    return f"{base_name}{max_num + 1:03d}"

def get_or_create_gp_material(index, stroke_col, fill_col):
    s_hex = f"{int(stroke_col[0]*255):02X}{int(stroke_col[1]*255):02X}{int(stroke_col[2]*255):02X}{int(stroke_col[3]*255):02X}"
    f_hex = f"{int(fill_col[0]*255):02X}{int(fill_col[1]*255):02X}{int(fill_col[2]*255):02X}{int(fill_col[3]*255):02X}"
    mat_name = f"GP_Color_{index:02d}_{s_hex}_{f_hex}"
    
    mat = bpy.data.materials.get(mat_name)
    if not mat:
        mat = bpy.data.materials.new(name=mat_name)
        try:
            bpy.data.materials.create_gpencil_data(mat)
        except Exception as e:
            pass
        
        if hasattr(mat, "grease_pencil") and mat.grease_pencil:
            mat.grease_pencil.color = stroke_col
            mat.grease_pencil.fill_color = fill_col
            mat.grease_pencil.show_stroke = (stroke_col[3] > 0.0)
            mat.grease_pencil.show_fill = (fill_col[3] > 0.0)
    mat.diffuse_color = stroke_col[:3] + (1.0,)
    return mat

def create_noise_node_group():
    name = "GP_Boiling_Noise_FX"
    if name in bpy.data.node_groups: return bpy.data.node_groups[name]
    ng = bpy.data.node_groups.new(name, 'GeometryNodeTree')
    ng.interface.new_socket("Geometry", in_out='INPUT', socket_type='NodeSocketGeometry')
    sock_fac = ng.interface.new_socket("Factor", in_out='INPUT', socket_type='NodeSocketFloat')
    sock_fac.default_value = 0.05; sock_fac.min_value = -10.0; sock_fac.max_value = 10.0   
    ng.interface.new_socket("Geometry", in_out='OUTPUT', socket_type='NodeSocketGeometry')
    
    node_in = ng.nodes.new('NodeGroupInput'); node_out = ng.nodes.new('NodeGroupOutput')
    noise = ng.nodes.new('ShaderNodeTexNoise'); noise.noise_dimensions = '4D'
    time = ng.nodes.new('GeometryNodeInputSceneTime')
    math_mul = ng.nodes.new('ShaderNodeMath'); math_mul.operation = 'MULTIPLY'; math_mul.inputs[1].default_value = 15.0
    vmath_sub = ng.nodes.new('ShaderNodeVectorMath'); vmath_sub.operation = 'SUBTRACT'; vmath_sub.inputs[1].default_value = (0.5, 0.5, 0.5)
    vmath_scale = ng.nodes.new('ShaderNodeVectorMath'); vmath_scale.operation = 'SCALE'
    set_pos = ng.nodes.new('GeometryNodeSetPosition')
    
    ng.links.new(time.outputs['Seconds'], math_mul.inputs[0])
    ng.links.new(math_mul.outputs['Value'], noise.inputs['W'])
    ng.links.new(noise.outputs['Color'], vmath_sub.inputs[0])
    ng.links.new(vmath_sub.outputs['Vector'], vmath_scale.inputs[0])
    ng.links.new(node_in.outputs['Factor'], vmath_scale.inputs['Scale'])
    ng.links.new(node_in.outputs['Geometry'], set_pos.inputs['Geometry'])
    ng.links.new(vmath_scale.outputs['Vector'], set_pos.inputs['Offset'])
    ng.links.new(set_pos.outputs['Geometry'], node_out.inputs['Geometry'])
    return ng

def create_thickness_node_group():
    name = "GP_Outline_Thickness_FX"
    if name in bpy.data.node_groups: return bpy.data.node_groups[name]
    ng = bpy.data.node_groups.new(name, 'GeometryNodeTree')
    ng.interface.new_socket("Geometry", in_out='INPUT', socket_type='NodeSocketGeometry')
    sock_thick = ng.interface.new_socket("Add_Thickness", in_out='INPUT', socket_type='NodeSocketFloat')
    sock_thick.default_value = 0.02; sock_thick.min_value = 0.0; sock_thick.max_value = 10.0  
    ng.interface.new_socket("Geometry", in_out='OUTPUT', socket_type='NodeSocketGeometry')
    
    node_in = ng.nodes.new('NodeGroupInput'); node_out = ng.nodes.new('NodeGroupOutput')
    rad_in = ng.nodes.new('GeometryNodeInputRadius')
    math_add = ng.nodes.new('ShaderNodeMath'); math_add.operation = 'ADD'
    set_rad = ng.nodes.new('GeometryNodeSetCurveRadius')
    
    ng.links.new(rad_in.outputs['Radius'], math_add.inputs[0])
    ng.links.new(node_in.outputs['Add_Thickness'], math_add.inputs[1])
    ng.links.new(node_in.outputs['Geometry'], set_rad.inputs['Curve'])
    ng.links.new(math_add.outputs['Value'], set_rad.inputs['Radius'])
    ng.links.new(set_rad.outputs['Curve'], node_out.inputs['Geometry'])
    return ng

# =========================================================
# 5. オペレーター群
# =========================================================
class GPEASY_OT_CreateGP(Operator):
    bl_idname = f"{PREFIX}.create_gp"
    bl_label = "① + 新規 GPオブジェクトを作成"
    bl_options = {'REGISTER', 'UNDO'}
    
    def execute(self, context):
        props = getattr(context.scene, f"{PREFIX}_props")
        
        if context.mode != 'OBJECT':
            try:
                bpy.ops.object.mode_set(mode='OBJECT')
            except Exception as e:
                self.report({'ERROR'}, f"モード変更失敗: {e}")
                return {'CANCELLED'}
            
        col = bpy.data.collections.get(props.col_name)
        if not col:
            col = bpy.data.collections.new(props.col_name)
            context.scene.collection.children.link(col)
            
        sub_col = bpy.data.collections.get(props.sub_col_name)
        if not sub_col:
            sub_col = bpy.data.collections.new(props.sub_col_name)
            col.children.link(sub_col)

        # ------------------------------------------
        # APIでGPデータを直接生成(エラー回避)
        # ------------------------------------------
        try:
            bpy.ops.object.select_all(action='DESELECT')
            
            gp_name = get_next_name(props.obj_base_name)
            gp_data = bpy.data.grease_pencils.new(name=gp_name + "_Data")
            gp = bpy.data.objects.new(gp_name, gp_data)
            
            sub_col.objects.link(gp)
            
            context.view_layer.objects.active = gp
            gp.select_set(True)
        except Exception as e:
            self.report({'ERROR'}, f"GP作成失敗: {e}")
            return {'CANCELLED'}

        gp_data = gp.data
        if hasattr(gp_data, "layers"):
            try:
                if len(gp_data.layers) == 0:
                    gp_data.layers.new(name="Fill")
                    layer_line = gp_data.layers.new(name="Line")
                    if hasattr(gp_data.layers, "active"):
                        gp_data.layers.active = layer_line
            except Exception as e:
                pass
            
        stroke_col = props.col_01_stroke
        fill_col = props.col_01_fill
        mat = get_or_create_gp_material(1, stroke_col, fill_col)

        found_mat = False
        for i, m in enumerate(gp.data.materials):
            if m == mat:
                gp.active_material_index = i
                found_mat = True
                break
        if not found_mat:
            gp.data.materials.append(mat)
            gp.active_material_index = len(gp.data.materials) - 1
            
        try:
            gp.active_material = mat
        except:
            pass
        
        try:
            bpy.ops.object.mode_set(mode='PAINT_GREASE_PENCIL')
            bpy.ops.grease_pencil.insert_blank_frame()
        except Exception as e:
            pass

        context.view_layer.update()
        for area in context.screen.areas:
            if area.type == 'VIEW_3D': area.tag_redraw()

        self.report({'INFO'}, f"'{gp.name}' を作成しました。")
        return {'FINISHED'}

class GPEASY_OT_CopyDefaults(Operator):
    bl_idname = f"{PREFIX}.copy_defaults"
    bl_label = "現在の色数値をコードとしてコピー"
    
    def execute(self, context):
        props = getattr(context.scene, f"{PREFIX}_props")
        out_lines = []
        for i in range(1, 25):
            name = getattr(props, f"col_{i:02d}_name")
            stroke = getattr(props, f"col_{i:02d}_stroke")
            fill = getattr(props, f"col_{i:02d}_fill")
            s_v = f"({stroke[0]:.3f}, {stroke[1]:.3f}, {stroke[2]:.3f}, {stroke[3]:.3f})"
            f_v = f"({fill[0]:.3f}, {fill[1]:.3f}, {fill[2]:.3f}, {fill[3]:.3f})"
            line_str = (
                f'    col_{i:02d}_name: StringProperty(default="{name}"); '
                f'col_{i:02d}_stroke: FloatVectorProperty(subtype=\'COLOR\', size=4, default={s_v}, min=0.0, max=1.0); '
                f'col_{i:02d}_fill: FloatVectorProperty(subtype=\'COLOR\', size=4, default={f_v}, min=0.0, max=1.0)'
            )
            out_lines.append(line_str)
            
        context.window_manager.clipboard = "\n".join(out_lines)
        self.report({'INFO'}, "コピーしました!")
        return {'FINISHED'}

class GPEASY_OT_SetBrushTool(Operator):
    bl_idname = f"{PREFIX}.set_brush_tool"
    bl_label = "ツール変更"
    tool_type: StringProperty()
    
    @classmethod
    def poll(cls, context):
        return context.area and context.area.type == 'VIEW_3D'
    
    def execute(self, context):
        if context.mode not in {'PAINT_GPENCIL', 'PAINT_GREASE_PENCIL'}:
            try: bpy.ops.object.mode_set(mode='PAINT_GREASE_PENCIL')
            except: pass

        override = context.copy() if hasattr(context, "copy") else {}
        for area in context.screen.areas:
            if area.type == 'VIEW_3D':
                override['area'] = area
                override['region'] = area.regions[-1]
                break

        if self.tool_type == 'ERASER':
            tool_ids = ["builtin.grease_pencil_eraser", "builtin.grease_pencil_erase", "builtin.eraser", "builtin_brush.Erase"]
            for t in tool_ids:
                try:
                    with context.temp_override(**override): bpy.ops.wm.tool_set_by_id(name=t)
                    break
                except: pass
        else:
            tool_ids = ["builtin.grease_pencil_draw", "builtin.brush", "builtin_brush.Draw"]
            for t in tool_ids:
                try:
                    with context.temp_override(**override): bpy.ops.wm.tool_set_by_id(name=t)
                    break
                except: pass

            t_name = {"PEN":"Pen", "PENCIL":"Pencil", "MARKER":"Marker"}.get(self.tool_type, "Pen")
            for ast in [t_name, f"GP {t_name}", t_name.lower()]:
                try: bpy.ops.brush.asset_activate(asset_id=ast)
                except: pass

        for area in context.screen.areas:
            if area.type == 'VIEW_3D': area.tag_redraw()
            
        return {'FINISHED'}

class GPEASY_OT_ToggleCanvas(Operator):
    bl_idname = f"{PREFIX}.toggle_canvas"
    bl_label = "グリッド表示"
    def execute(self, context):
        for area in context.screen.areas:
            if area.type == 'VIEW_3D':
                for space in area.spaces:
                    if space.type == 'VIEW_3D':
                        state = space.overlay.show_floor
                        space.overlay.show_floor = not state
                        space.overlay.show_ortho_grid = not state
        return {'FINISHED'}

class GPEASY_OT_SetBrushSize(Operator):
    bl_idname = f"{PREFIX}.set_brush_size"
    bl_label = "太さ"
    size: IntProperty(default=10)
    
    def execute(self, context):
        self.size = int(self.size)
        try: 
            context.scene.tool_settings.unified_paint_settings.use_unified_size = True
            context.scene.tool_settings.unified_paint_settings.size = self.size
        except: pass
        try:
            for b in bpy.data.brushes:
                if hasattr(b, "gpencil_tool") and b.gpencil_tool == 'ERASE': continue
                b.size = self.size
        except: pass
        return {'FINISHED'}

class GPEASY_OT_SetBrushOpacity(Operator):
    bl_idname = f"{PREFIX}.set_brush_opacity"
    bl_label = "透明度"
    opacity: FloatProperty(default=1.0)
    
    def execute(self, context):
        try: 
            context.scene.tool_settings.unified_paint_settings.use_unified_weight = True
            context.scene.tool_settings.unified_paint_settings.weight = self.opacity
            context.scene.tool_settings.unified_paint_settings.use_unified_strength = True
            context.scene.tool_settings.unified_paint_settings.strength = self.opacity
        except: pass
        try:
            for b in bpy.data.brushes:
                if hasattr(b, "gpencil_tool") and b.gpencil_tool == 'ERASE': continue
                if hasattr(b, "strength"): b.strength = self.opacity
                if hasattr(b, "weight"): b.weight = self.opacity
        except: pass
        
        for area in context.screen.areas:
            if area.type == 'VIEW_3D': area.tag_redraw()
        return {'FINISHED'}

class GPEASY_OT_ApplyColor(Operator):
    bl_idname = f"{PREFIX}.apply_color"
    bl_label = "色"
    color_index: IntProperty(default=1)
    
    def execute(self, context):
        obj = context.active_object
        if not is_gp_object(obj): return {'CANCELLED'}
        props = getattr(context.scene, f"{PREFIX}_props")
        
        stroke_col = getattr(props, f"col_{self.color_index:02d}_stroke")
        fill_col = getattr(props, f"col_{self.color_index:02d}_fill")
        
        mat = get_or_create_gp_material(self.color_index, stroke_col, fill_col)
        
        found = False
        for i, slot_mat in enumerate(obj.data.materials):
            if slot_mat == mat:
                obj.active_material_index = i
                found = True
                break
        if not found:
            obj.data.materials.append(mat)
            obj.active_material_index = len(obj.data.materials) - 1
            
        try: obj.active_material = mat
        except: pass
        for area in context.screen.areas:
            if area.type == 'VIEW_3D': area.tag_redraw()
        return {'FINISHED'}

class GPEASY_OT_AddLayer(Operator):
    bl_idname = f"{PREFIX}.add_layer"
    bl_label = "新規レイヤー"
    
    def execute(self, context):
        obj = context.active_object
        if not is_gp_object(obj): return {'CANCELLED'}
            
        if hasattr(obj.data, "layers"):
            try:
                base_name = "Layer"
                new_name = base_name
                count = 1
                while obj.data.layers.get(new_name):
                    new_name = f"{base_name} {count}"
                    count += 1
                layer = obj.data.layers.new(name=new_name)
                obj.data.layers.active = layer
                if hasattr(layer, "frames"):
                    layer.frames.new(context.scene.frame_current)
                return {'FINISHED'}
            except: pass
        for op_name in ['layer_add', 'gpencil_layer_add']:
            try:
                if hasattr(bpy.ops.grease_pencil, op_name):
                    getattr(bpy.ops.grease_pencil, op_name)()
                    return {'FINISHED'}
            except: pass
        return {'FINISHED'}

class GPEASY_OT_SetLayerActive(Operator):
    bl_idname = f"{PREFIX}.set_layer_active"
    bl_label = "レイヤー選択"
    layer_name: StringProperty()
    
    def execute(self, context):
        obj = context.active_object
        if is_gp_object(obj) and hasattr(obj.data, "layers"):
            layer = obj.data.layers.get(self.layer_name)
            if layer:
                try: obj.data.layers.active = layer
                except: pass
        return {'FINISHED'}

class GPEASY_OT_AddModifier(Operator):
    bl_idname = f"{PREFIX}.add_modifier"
    bl_label = "エフェクト追加"
    mod_type: StringProperty()
    def execute(self, context):
        obj = context.active_object
        if not is_gp_object(obj): return {'CANCELLED'}
        mods = getattr(obj, "modifiers", [])
        if self.mod_type == 'NOISE':
            m_name = "Boiling_Noise"
            if m_name not in mods:
                mod = mods.new(name=m_name, type='NODES')
                mod.node_group = create_noise_node_group()
        elif self.mod_type == 'THICKNESS':
            m_name = "Outline_Thickness"
            if m_name not in mods:
                mod = mods.new(name=m_name, type='NODES')
                mod.node_group = create_thickness_node_group()
        return {'FINISHED'}

class GPEASY_OT_ResetModifiers(Operator):
    bl_idname = f"{PREFIX}.reset_modifiers"
    bl_label = "設定を初期値に戻す"
    bl_options = {'REGISTER', 'UNDO'}
    def execute(self, context):
        obj = context.active_object
        if not is_gp_object(obj): return {'CANCELLED'}
        mods = getattr(obj, "modifiers", [])
        for m in mods:
            if m.type == 'NODES' and m.node_group:
                try:
                    if m.name == "Boiling_Noise":
                        id_name = [i.identifier for i in m.node_group.interface.items_tree if i.name == "Factor"][0]
                        m[id_name] = 0.05
                    elif m.name == "Outline_Thickness":
                        id_name = [i.identifier for i in m.node_group.interface.items_tree if i.name == "Add_Thickness"][0]
                        m[id_name] = 0.02
                except: pass
        self.report({'INFO'}, "初期化しました")
        return {'FINISHED'}

class GPEASY_OT_FinishDrawing(Operator):
    bl_idname = f"{PREFIX}.finish_drawing"
    bl_label = "⑦ ✓ オブジェクト完成"
    def execute(self, context):
        if not is_gp_object(context.active_object): return {'CANCELLED'}
        try: bpy.ops.object.mode_set(mode='OBJECT')
        except: pass
        try: bpy.ops.gpeasy.create_gp()
        except: pass
        return {'FINISHED'}

class GPEASY_OT_EditPalette(Operator):
    bl_idname = f"{PREFIX}.edit_palette"
    bl_label = "🎨 パレットの色を編集"
    def invoke(self, context, event): return context.window_manager.invoke_props_dialog(self, width=400)
    def draw(self, context):
        layout = self.layout
        props = getattr(context.scene, f"{PREFIX}_props")
        layout.label(text="名前 / Stroke(線) / Fill(塗り)", icon='COLOR')
        for i in range(1, 25):
            row = layout.row(align=True)
            row.prop(props, f"col_{i:02d}_name", text="")
            row.prop(props, f"col_{i:02d}_stroke", text="")
            row.prop(props, f"col_{i:02d}_fill", text="")
    def execute(self, context): return {'FINISHED'}

class GPEASY_OT_RemoveAllPanels(Operator):
    bl_idname = f"{PREFIX}.remove_all_panels"
    bl_label = "Unregister Addon"
    def execute(self, context): unregister(); return {'FINISHED'}

# =========================================================
# 6. パネル UI
# =========================================================
class GPEASY_PT_Workflow(Panel):
    bl_label = "1. 描画ワークフロー"
    bl_idname = f"{PREFIX_VAL}_PT_workflow"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = ADDON_TITLE
    bl_order = 0

    def draw(self, context):
        layout = self.layout
        props = getattr(context.scene, f"{PREFIX}_props")
        
        col = layout.column(align=True)
        col.scale_y = 1.5
        col.operator(GPEASY_OT_CreateGP.bl_idname, icon='ADD')
        layout.separator()
        
        box_tool = layout.box()
        box_tool.label(text="② ツール & グリッド:", icon='TOOL_SETTINGS')
        row_tool = box_tool.row(align=True)
        row_tool.operator(GPEASY_OT_SetBrushTool.bl_idname, text="ペン", icon='GREASEPENCIL').tool_type = 'PEN'
        row_tool.operator(GPEASY_OT_SetBrushTool.bl_idname, text="鉛筆", icon='EDITMODE_HLT').tool_type = 'PENCIL'
        row_tool.operator(GPEASY_OT_SetBrushTool.bl_idname, text="マーカー", icon='BRUSH_DATA').tool_type = 'MARKER'
        row_tool.operator(GPEASY_OT_SetBrushTool.bl_idname, text="消しゴム", icon='TRASH').tool_type = 'ERASER'
        box_tool.operator(GPEASY_OT_ToggleCanvas.bl_idname, text="マス目ガイド ON/OFF", icon='GRID')
        
        box_size = layout.box()
        box_size.label(text="③ ペンの太さ:", icon='BRUSH_DATA')
        r1 = box_size.row(align=True)
        r1.operator(GPEASY_OT_SetBrushSize.bl_idname, text="極細 (3)").size = 3
        r1.operator(GPEASY_OT_SetBrushSize.bl_idname, text="細 (10)").size = 10
        r1.operator(GPEASY_OT_SetBrushSize.bl_idname, text="中 (20)").size = 20
        r2 = box_size.row(align=True)
        r2.operator(GPEASY_OT_SetBrushSize.bl_idname, text="太 (30)").size = 30
        r2.operator(GPEASY_OT_SetBrushSize.bl_idname, text="極太 (50)").size = 50
        r2.operator(GPEASY_OT_SetBrushSize.bl_idname, text="特大 (100)").size = 100
        
        box_opacity = layout.box()
        box_opacity.label(text="④ ペンの透明度:", icon='IMAGE_ALPHA')
        r3 = box_opacity.row(align=True)
        r3.operator(GPEASY_OT_SetBrushOpacity.bl_idname, text="10% (極薄)").opacity = 0.1
        r3.operator(GPEASY_OT_SetBrushOpacity.bl_idname, text="30%").opacity = 0.3
        r3.operator(GPEASY_OT_SetBrushOpacity.bl_idname, text="50%").opacity = 0.5
        r3.operator(GPEASY_OT_SetBrushOpacity.bl_idname, text="80%").opacity = 0.8
        r3.operator(GPEASY_OT_SetBrushOpacity.bl_idname, text="100% (濃)").opacity = 1.0

        box_col = layout.box()
        r_col = box_col.row()
        r_col.label(text="⑤ 色を選ぶ:", icon='COLOR')
        r_col.operator(GPEASY_OT_EditPalette.bl_idname, text="", icon='PREFERENCES')
        box_col.operator(GPEASY_OT_CopyDefaults.bl_idname, text="色設定のコードをコピー", icon='COPYDOWN')
        box_col.separator()
        
        grid = box_col.grid_flow(columns=3, align=True)
        for i in range(1, 25):
            name = getattr(props, f"col_{i:02d}_name")
            c = grid.column(align=True)
            r = c.row(align=True)
            r.scale_y = 1.5 
            r.prop(props, f"col_{i:02d}_stroke", text="")
            r.prop(props, f"col_{i:02d}_fill", text="")
            r.operator(GPEASY_OT_ApplyColor.bl_idname, text=name).color_index = i

        obj = context.active_object
        if is_gp_object(obj):
            box_layer = layout.box()
            row_head = box_layer.row()
            row_head.label(text="⑥ レイヤー:", icon='GROUP')
            row_head.operator(GPEASY_OT_AddLayer.bl_idname, text="追加", icon='ADD')
            
            if hasattr(obj.data, "layers") and obj.data.layers:
                layers_list = list(obj.data.layers) if hasattr(obj.data.layers, "__iter__") else []
                for layer in reversed(layers_list):
                    row_layer = box_layer.row(align=True)
                    is_active = False
                    try: 
                        if hasattr(obj.data.layers, "active") and obj.data.layers.active:
                            is_active = (obj.data.layers.active.name == layer.name)
                    except: pass
                    
                    icon = 'RADIOBUT_ON' if is_active else 'RADIOBUT_OFF'
                    op_act = row_layer.operator(GPEASY_OT_SetLayerActive.bl_idname, text=getattr(layer, "name", "Layer"), icon=icon)
                    op_act.layer_name = getattr(layer, "name", "")
                    
                    if hasattr(layer, "hide"): 
                        row_layer.prop(layer, "hide", text="", icon='HIDE_ON' if getattr(layer, "hide", False) else 'HIDE_OFF')
                    elif hasattr(layer, "hide_viewport"): 
                        row_layer.prop(layer, "hide_viewport", text="", icon='HIDE_ON' if getattr(layer, "hide_viewport", False) else 'HIDE_OFF')
                    if hasattr(layer, "lock"): 
                        row_layer.prop(layer, "lock", text="", icon='LOCKED' if getattr(layer, "lock", False) else 'UNLOCKED')
                    if hasattr(layer, "opacity"): 
                        row_layer.prop(layer, "opacity", text="", slider=True)
            else:
                box_layer.label(text="レイヤーなし")

            box_fx = layout.box()
            box_fx.label(text="★ エフェクト追加 (再生で動きます):", icon='MODIFIER')
            row_fx = box_fx.row(align=True)
            row_fx.operator(GPEASY_OT_AddModifier.bl_idname, text="ボイリング").mod_type = 'NOISE'
            row_fx.operator(GPEASY_OT_AddModifier.bl_idname, text="線を太くする").mod_type = 'THICKNESS'
            
            mods = getattr(obj, "modifiers", [])
            has_effect = False
            for m in mods:
                if m.type == 'NODES' and m.node_group:
                    if m.name in ["Boiling_Noise", "Outline_Thickness"]: has_effect = True
                    try:
                        if m.name == "Boiling_Noise":
                            id_name = [i.identifier for i in m.node_group.interface.items_tree if i.name == "Factor"][0]
                            if id_name not in m: m[id_name] = 0.05
                            box_fx.prop(m, f'["{id_name}"]', text="震えの強さ", slider=True)
                        elif m.name == "Outline_Thickness":
                            id_name = [i.identifier for i in m.node_group.interface.items_tree if i.name == "Add_Thickness"][0]
                            if id_name not in m: m[id_name] = 0.02
                            box_fx.prop(m, f'["{id_name}"]', text="追加する太さ", slider=True)
                    except: pass
            
            if has_effect:
                box_fx.operator(GPEASY_OT_ResetModifiers.bl_idname, text="初期値に戻す", icon='FILE_REFRESH')

        layout.separator()
        col = layout.column(align=True)
        col.scale_y = 1.5
        col.operator(GPEASY_OT_FinishDrawing.bl_idname, icon='CHECKMARK')

class GPEASY_PT_Manager(Panel):
    bl_label = "2. オブジェクト管理"
    bl_idname = f"{PREFIX_VAL}_PT_manager"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = ADDON_TITLE
    bl_order = 1
    bl_options = {'DEFAULT_CLOSED'}

    def draw(self, context):
        layout = self.layout
        box = layout.box()
        props = getattr(context.scene, f"{PREFIX}_props")
        
        col = bpy.data.collections.get(props.col_name)
        if col:
            sub_col = bpy.data.collections.get(props.sub_col_name)
            target_col = sub_col if sub_col else col
            for obj in target_col.objects:
                if is_gp_object(obj):
                    row = box.row(align=True)
                    icon = 'RADIOBUT_ON' if context.active_object == obj else 'RADIOBUT_OFF'
                    row.prop(obj, "name", text="", icon=icon)
                    row.prop(obj, "hide_viewport", text="", icon='HIDE_ON' if obj.hide_viewport else 'HIDE_OFF')
                    row.prop(obj, "hide_select", text="", icon='RESTRICT_SELECT_ON' if obj.hide_select else 'UNLOCKED')
        else: box.label(text="まだ描画がありません")

class GPEASY_PT_Settings(Panel):
    bl_label = "3. 設定 & リンク"
    bl_idname = f"{PREFIX_VAL}_PT_settings"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = ADDON_TITLE
    bl_order = 2
    bl_options = {'DEFAULT_CLOSED'}

    def draw(self, context):
        layout = self.layout
        props = getattr(context.scene, f"{PREFIX}_props")

        box = layout.box()
        box.label(text="▼ コレクション設定", icon='PREFERENCES')
        box.prop(props, "col_name")
        box.prop(props, "sub_col_name")
        box.prop(props, "obj_base_name")

        box_link = layout.box()
        box_link.label(text="▼ リンクを開く", icon='WORLD_DATA')
        if props.link_1_url:
            box_links = box_link
            box_links.operator("wm.url_open", text=props.link_1_name, icon='URL').url = props.link_1_url
        box_link.operator("wm.url_open", text=LINK_NAME_2, icon='URL').url = LINK_URL_2
        box_link.operator("wm.url_open", text=LINK_NAME_3, icon='URL').url = LINK_URL_3
        box_link.operator("wm.url_open", text=LINK_NAME_4, icon='URL').url = LINK_URL_4

class GPEASY_PT_RemovePanel(Panel):
    bl_label = "4. アドオン削除"
    bl_idname = f"{PREFIX_VAL}_PT_remove"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = ADDON_TITLE
    bl_order = 3
    bl_options = {'DEFAULT_CLOSED'}
    def draw(self, context):
        layout = self.layout
        layout.operator(GPEASY_OT_RemoveAllPanels.bl_idname, icon='CANCEL')

# =========================================================
# 7. 登録・解除
# =========================================================
classes = (
    GPEASY_Properties,
    GPEASY_OT_CreateGP, GPEASY_OT_CopyDefaults,
    GPEASY_OT_SetBrushTool, GPEASY_OT_ToggleCanvas, GPEASY_OT_SetBrushSize, GPEASY_OT_SetBrushOpacity, 
    GPEASY_OT_ApplyColor, GPEASY_OT_AddModifier, GPEASY_OT_ResetModifiers, 
    GPEASY_OT_AddLayer, GPEASY_OT_SetLayerActive, GPEASY_OT_FinishDrawing, 
    GPEASY_OT_EditPalette, GPEASY_OT_RemoveAllPanels,
    GPEASY_PT_Workflow, GPEASY_PT_Manager, GPEASY_PT_Settings, GPEASY_PT_RemovePanel,
)

def register():
    for cls in classes:
        try: bpy.utils.register_class(cls)
        except Exception as e: print(f"Error registering {cls.__name__}: {e}")
            
    setattr(bpy.types.Scene, f"{PREFIX}_props", PointerProperty(type=GPEASY_Properties))

def unregister():
    if hasattr(bpy.types.Scene, f"{PREFIX}_props"):
        delattr(bpy.types.Scene, f"{PREFIX}_props")
        
    for cls in reversed(classes):
        try: bpy.utils.unregister_class(cls)
        except: pass

if __name__ == "__main__":
    register()
# =========================================================
# アドオン宣言 (必ずファイルの先頭に配置)
# =========================================================
bl_info = {
    "name": "GP Easy Paint",
    "author": "AI",
    "version": (1, 5, 0),
    "blender": (5, 0, 0),
    "location": "View3D > Sidebar > GP Easy Paint",
    "description": "GPオブジェクト作成 → ツール/太さ/透明度/色 → レイヤー → エフェクト → 完成",
    "category": "GP Easy Paint",
}

import bpy
import os
from bpy.types import Operator, Panel, PropertyGroup
from bpy.props import StringProperty, PointerProperty, FloatVectorProperty, IntProperty, FloatProperty

# =========================================================
# 1. 共通定義
# =========================================================
ADDON_TITLE = "GP Easy Paint"
PREFIX_VAL = "GPEASY"
PREFIX = PREFIX_VAL.lower()
ADDON_CATEGORY = "GP Easy Paint"

DEFAULT_COL_NAME = "GreaseObjects"
DEFAULT_SUB_COL_NAME = "SubObjects"
DEFAULT_BASE_NAME = "GP_"

# ===================================================================
# 2. リンク定義
# ===================================================================
LINK_NAME_1 = "カスタムリンク"
LINK_URL_1 = "<https://example.com>"

LINK_NAME_2 = "コンテナ builder"
LINK_URL_2 = "<https://note.com/zionad2017/n/ndb6dbdbc844a>"

LINK_NAME_3 = "posfie"
LINK_URL_3 = "<https://posfie.com/@timekagura/t/zionad2022?sort=0>"

LINK_NAME_4 = "zionadchat"
LINK_URL_4 = "<https://x.com/zionadchat>"

# =========================================================
# 3. プロパティグループ
# =========================================================
class GPEASY_Properties(PropertyGroup):
    col_name: StringProperty(name="主コレクション", default=DEFAULT_COL_NAME)
    sub_col_name: StringProperty(name="サブコレクション", default=DEFAULT_SUB_COL_NAME)
    obj_base_name: StringProperty(name="ベース名", default=DEFAULT_BASE_NAME)

    link_1_name: StringProperty(name="リンク1名", default=LINK_NAME_1)
    link_1_url: StringProperty(name="リンク1URL", default=LINK_URL_1)
    
    # ▼ ここから下のコードをコピー機能でペースト上書きできます ▼
    col_01_name: StringProperty(default="黒"); col_01_stroke: FloatVectorProperty(subtype='COLOR', size=4, default=(0.050, 0.050, 0.050, 1.000), min=0.0, max=1.0); col_01_fill: FloatVectorProperty(subtype='COLOR', size=4, default=(0.300, 0.300, 0.300, 0.500), min=0.0, max=1.0)
    col_02_name: StringProperty(default="白"); col_02_stroke: FloatVectorProperty(subtype='COLOR', size=4, default=(1.000, 1.000, 1.000, 1.000), min=0.0, max=1.0); col_02_fill: FloatVectorProperty(subtype='COLOR', size=4, default=(0.900, 0.900, 0.900, 0.500), min=0.0, max=1.0)
    col_03_name: StringProperty(default="灰"); col_03_stroke: FloatVectorProperty(subtype='COLOR', size=4, default=(0.400, 0.400, 0.400, 1.000), min=0.0, max=1.0); col_03_fill: FloatVectorProperty(subtype='COLOR', size=4, default=(0.600, 0.600, 0.600, 0.500), min=0.0, max=1.0)
    col_04_name: StringProperty(default="赤"); col_04_stroke: FloatVectorProperty(subtype='COLOR', size=4, default=(0.800, 0.100, 0.100, 1.000), min=0.0, max=1.0); col_04_fill: FloatVectorProperty(subtype='COLOR', size=4, default=(0.900, 0.400, 0.400, 0.500), min=0.0, max=1.0)
    col_05_name: StringProperty(default="青"); col_05_stroke: FloatVectorProperty(subtype='COLOR', size=4, default=(0.100, 0.200, 0.800, 1.000), min=0.0, max=1.0); col_05_fill: FloatVectorProperty(subtype='COLOR', size=4, default=(0.400, 0.500, 0.900, 0.500), min=0.0, max=1.0)
    col_06_name: StringProperty(default="緑"); col_06_stroke: FloatVectorProperty(subtype='COLOR', size=4, default=(0.100, 0.700, 0.200, 1.000), min=0.0, max=1.0); col_06_fill: FloatVectorProperty(subtype='COLOR', size=4, default=(0.400, 0.900, 0.500, 0.500), min=0.0, max=1.0)
    col_07_name: StringProperty(default="黄"); col_07_stroke: FloatVectorProperty(subtype='COLOR', size=4, default=(0.900, 0.800, 0.100, 1.000), min=0.0, max=1.0); col_07_fill: FloatVectorProperty(subtype='COLOR', size=4, default=(0.900, 0.900, 0.500, 0.500), min=0.0, max=1.0)
    col_08_name: StringProperty(default="水"); col_08_stroke: FloatVectorProperty(subtype='COLOR', size=4, default=(0.100, 0.800, 0.900, 1.000), min=0.0, max=1.0); col_08_fill: FloatVectorProperty(subtype='COLOR', size=4, default=(0.500, 0.900, 0.900, 0.500), min=0.0, max=1.0)
    col_09_name: StringProperty(default="紫"); col_09_stroke: FloatVectorProperty(subtype='COLOR', size=4, default=(0.600, 0.100, 0.800, 1.000), min=0.0, max=1.0); col_09_fill: FloatVectorProperty(subtype='COLOR', size=4, default=(0.800, 0.500, 0.900, 0.500), min=0.0, max=1.0)
    col_10_name: StringProperty(default="橙"); col_10_stroke: FloatVectorProperty(subtype='COLOR', size=4, default=(0.900, 0.400, 0.100, 1.000), min=0.0, max=1.0); col_10_fill: FloatVectorProperty(subtype='COLOR', size=4, default=(0.900, 0.600, 0.400, 0.500), min=0.0, max=1.0)
    col_11_name: StringProperty(default="茶"); col_11_stroke: FloatVectorProperty(subtype='COLOR', size=4, default=(0.400, 0.200, 0.100, 1.000), min=0.0, max=1.0); col_11_fill: FloatVectorProperty(subtype='COLOR', size=4, default=(0.600, 0.400, 0.300, 0.500), min=0.0, max=1.0)
    col_12_name: StringProperty(default="桃"); col_12_stroke: FloatVectorProperty(subtype='COLOR', size=4, default=(0.900, 0.009, 0.000, 1.000), min=0.0, max=1.0); col_12_fill: FloatVectorProperty(subtype='COLOR', size=4, default=(0.900, 0.600, 0.800, 0.500), min=0.0, max=1.0)
    col_13_name: StringProperty(default="黄緑"); col_13_stroke: FloatVectorProperty(subtype='COLOR', size=4, default=(0.500, 0.900, 0.100, 1.000), min=0.0, max=1.0); col_13_fill: FloatVectorProperty(subtype='COLOR', size=4, default=(0.700, 0.900, 0.300, 0.500), min=0.0, max=1.0)
    col_14_name: StringProperty(default="深緑"); col_14_stroke: FloatVectorProperty(subtype='COLOR', size=4, default=(0.000, 0.300, 0.100, 1.000), min=0.0, max=1.0); col_14_fill: FloatVectorProperty(subtype='COLOR', size=4, default=(0.100, 0.500, 0.200, 0.500), min=0.0, max=1.0)
    col_15_name: StringProperty(default="紺"); col_15_stroke: FloatVectorProperty(subtype='COLOR', size=4, default=(0.000, 0.100, 0.400, 1.000), min=0.0, max=1.0); col_15_fill: FloatVectorProperty(subtype='COLOR', size=4, default=(0.100, 0.200, 0.500, 0.500), min=0.0, max=1.0)
    col_16_name: StringProperty(default="青紫"); col_16_stroke: FloatVectorProperty(subtype='COLOR', size=4, default=(0.300, 0.100, 0.700, 1.000), min=0.0, max=1.0); col_16_fill: FloatVectorProperty(subtype='COLOR', size=4, default=(0.500, 0.300, 0.800, 0.500), min=0.0, max=1.0)
    col_17_name: StringProperty(default="赤紫"); col_17_stroke: FloatVectorProperty(subtype='COLOR', size=4, default=(0.800, 0.100, 0.500, 1.000), min=0.0, max=1.0); col_17_fill: FloatVectorProperty(subtype='COLOR', size=4, default=(0.900, 0.400, 0.600, 0.500), min=0.0, max=1.0)
    col_18_name: StringProperty(default="朱"); col_18_stroke: FloatVectorProperty(subtype='COLOR', size=4, default=(0.900, 0.200, 0.100, 1.000), min=0.0, max=1.0); col_18_fill: FloatVectorProperty(subtype='COLOR', size=4, default=(0.900, 0.400, 0.300, 0.500), min=0.0, max=1.0)
    col_19_name: StringProperty(default="黄土"); col_19_stroke: FloatVectorProperty(subtype='COLOR', size=4, default=(0.700, 0.500, 0.200, 1.000), min=0.0, max=1.0); col_19_fill: FloatVectorProperty(subtype='COLOR', size=4, default=(0.800, 0.600, 0.400, 0.500), min=0.0, max=1.0)
    col_20_name: StringProperty(default="焦茶"); col_20_stroke: FloatVectorProperty(subtype='COLOR', size=4, default=(1.000, 0.000, 0.468, 1.000), min=0.0, max=1.0); col_20_fill: FloatVectorProperty(subtype='COLOR', size=4, default=(0.400, 0.200, 0.100, 0.500), min=0.0, max=1.0)
    col_21_name: StringProperty(default="薄灰"); col_21_stroke: FloatVectorProperty(subtype='COLOR', size=4, default=(0.700, 0.700, 0.700, 1.000), min=0.0, max=1.0); col_21_fill: FloatVectorProperty(subtype='COLOR', size=4, default=(0.800, 0.800, 0.800, 0.500), min=0.0, max=1.0)
    col_22_name: StringProperty(default="暗灰"); col_22_stroke: FloatVectorProperty(subtype='COLOR', size=4, default=(0.200, 0.200, 0.200, 1.000), min=0.0, max=1.0); col_22_fill: FloatVectorProperty(subtype='COLOR', size=4, default=(0.300, 0.300, 0.300, 0.500), min=0.0, max=1.0)
    col_23_name: StringProperty(default="肌"); col_23_stroke: FloatVectorProperty(subtype='COLOR', size=4, default=(1.000, 0.800, 0.700, 1.000), min=0.0, max=1.0); col_23_fill: FloatVectorProperty(subtype='COLOR', size=4, default=(1.000, 0.900, 0.800, 0.500), min=0.0, max=1.0)
    col_24_name: StringProperty(default="青緑"); col_24_stroke: FloatVectorProperty(subtype='COLOR', size=4, default=(0.100, 0.600, 0.500, 1.000), min=0.0, max=1.0); col_24_fill: FloatVectorProperty(subtype='COLOR', size=4, default=(0.300, 0.700, 0.600, 0.500), min=0.0, max=1.0)
    # ▲ ここまで ▲

# =========================================================
# 4. ユーティリティ
# =========================================================
def is_gp_object(obj):
    return obj and obj.type in {'GPENCIL', 'GREASEPENCIL'}

def get_next_name(base_name):
    max_num = 0
    for obj in bpy.data.objects:
        if is_gp_object(obj) and obj.name.startswith(base_name):
            try: max_num = max(max_num, int(obj.name.replace(base_name, "")))
            except: pass
    return f"{base_name}{max_num + 1:03d}"

def get_or_create_gp_material(index, stroke_col, fill_col):
    s_hex = f"{int(stroke_col[0]*255):02X}{int(stroke_col[1]*255):02X}{int(stroke_col[2]*255):02X}{int(stroke_col[3]*255):02X}"
    f_hex = f"{int(fill_col[0]*255):02X}{int(fill_col[1]*255):02X}{int(fill_col[2]*255):02X}{int(fill_col[3]*255):02X}"
    mat_name = f"GP_Color_{index:02d}_{s_hex}_{f_hex}"
    
    mat = bpy.data.materials.get(mat_name)
    if not mat:
        mat = bpy.data.materials.new(name=mat_name)
        try: bpy.data.materials.create_gpencil_data(mat)
        except: pass
        
        if hasattr(mat, "grease_pencil") and mat.grease_pencil:
            mat.grease_pencil.color = stroke_col
            mat.grease_pencil.fill_color = fill_col
            mat.grease_pencil.show_stroke = (stroke_col[3] > 0.0)
            mat.grease_pencil.show_fill = (fill_col[3] > 0.0)
    mat.diffuse_color = stroke_col[:3] + (1.0,)
    return mat

def create_noise_node_group():
    name = "GP_Boiling_Noise_FX"
    if name in bpy.data.node_groups: return bpy.data.node_groups[name]
    ng = bpy.data.node_groups.new(name, 'GeometryNodeTree')
    ng.interface.new_socket("Geometry", in_out='INPUT', socket_type='NodeSocketGeometry')
    sock_fac = ng.interface.new_socket("Factor", in_out='INPUT', socket_type='NodeSocketFloat')
    sock_fac.default_value = 0.05; sock_fac.min_value = -10.0; sock_fac.max_value = 10.0   
    ng.interface.new_socket("Geometry", in_out='OUTPUT', socket_type='NodeSocketGeometry')
    
    node_in = ng.nodes.new('NodeGroupInput'); node_out = ng.nodes.new('NodeGroupOutput')
    noise = ng.nodes.new('ShaderNodeTexNoise'); noise.noise_dimensions = '4D'
    time = ng.nodes.new('GeometryNodeInputSceneTime')
    math_mul = ng.nodes.new('ShaderNodeMath'); math_mul.operation = 'MULTIPLY'; math_mul.inputs[1].default_value = 15.0
    vmath_sub = ng.nodes.new('ShaderNodeVectorMath'); vmath_sub.operation = 'SUBTRACT'; vmath_sub.inputs[1].default_value = (0.5, 0.5, 0.5)
    vmath_scale = ng.nodes.new('ShaderNodeVectorMath'); vmath_scale.operation = 'SCALE'
    set_pos = ng.nodes.new('GeometryNodeSetPosition')
    
    ng.links.new(time.outputs['Seconds'], math_mul.inputs[0])
    ng.links.new(math_mul.outputs['Value'], noise.inputs['W'])
    ng.links.new(noise.outputs['Color'], vmath_sub.inputs[0])
    ng.links.new(vmath_sub.outputs['Vector'], vmath_scale.inputs[0])
    ng.links.new(node_in.outputs['Factor'], vmath_scale.inputs['Scale'])
    ng.links.new(node_in.outputs['Geometry'], set_pos.inputs['Geometry'])
    ng.links.new(vmath_scale.outputs['Vector'], set_pos.inputs['Offset'])
    ng.links.new(set_pos.outputs['Geometry'], node_out.inputs['Geometry'])
    return ng

def create_thickness_node_group():
    name = "GP_Outline_Thickness_FX"
    if name in bpy.data.node_groups: return bpy.data.node_groups[name]
    ng = bpy.data.node_groups.new(name, 'GeometryNodeTree')
    ng.interface.new_socket("Geometry", in_out='INPUT', socket_type='NodeSocketGeometry')
    sock_thick = ng.interface.new_socket("Add_Thickness", in_out='INPUT', socket_type='NodeSocketFloat')
    sock_thick.default_value = 0.02; sock_thick.min_value = 0.0; sock_thick.max_value = 10.0  
    ng.interface.new_socket("Geometry", in_out='OUTPUT', socket_type='NodeSocketGeometry')
    
    node_in = ng.nodes.new('NodeGroupInput'); node_out = ng.nodes.new('NodeGroupOutput')
    rad_in = ng.nodes.new('GeometryNodeInputRadius')
    math_add = ng.nodes.new('ShaderNodeMath'); math_add.operation = 'ADD'
    set_rad = ng.nodes.new('GeometryNodeSetCurveRadius')
    
    ng.links.new(rad_in.outputs['Radius'], math_add.inputs[0])
    ng.links.new(node_in.outputs['Add_Thickness'], math_add.inputs[1])
    ng.links.new(node_in.outputs['Geometry'], set_rad.inputs['Curve'])
    ng.links.new(math_add.outputs['Value'], set_rad.inputs['Radius'])
    ng.links.new(set_rad.outputs['Curve'], node_out.inputs['Geometry'])
    return ng

# =========================================================
# 5. オペレーター群
# =========================================================
class GPEASY_OT_CreateGP(Operator):
    bl_idname = f"{PREFIX}.create_gp"
    bl_label = "① + 新規 GPオブジェクトを作成"
    bl_options = {'REGISTER', 'UNDO'}
    
    def execute(self, context):
        props = getattr(context.scene, f"{PREFIX}_props")
        if context.mode != 'OBJECT':
            try: bpy.ops.object.mode_set(mode='OBJECT')
            except: pass
            
        col = bpy.data.collections.get(props.col_name)
        if not col:
            col = bpy.data.collections.new(props.col_name)
            context.scene.collection.children.link(col)
            
        sub_col = bpy.data.collections.get(props.sub_col_name)
        if not sub_col:
            sub_col = bpy.data.collections.new(props.sub_col_name)
            col.children.link(sub_col)

        try: bpy.ops.object.grease_pencil_add(type='EMPTY')
        except: pass
        
        gp = context.active_object
        if not is_gp_object(gp): return {'CANCELLED'}
        
        gp.name = get_next_name(props.obj_base_name)
        
        for c in gp.users_collection: c.objects.unlink(gp)
        sub_col.objects.link(gp)

        gp_data = gp.data
        if hasattr(gp_data, "layers"):
            try:
                if len(gp_data.layers) == 0:
                    layer_fill = gp_data.layers.new(name="Fill")
                    layer_line = gp_data.layers.new(name="Line")
                    try: gp_data.layers.active = layer_line
                    except: pass
            except: pass
            
        stroke_col = props.col_01_stroke
        fill_col = props.col_01_fill
        mat = get_or_create_gp_material(1, stroke_col, fill_col)

        found_mat = False
        for i, m in enumerate(gp.data.materials):
            if m == mat:
                gp.active_material_index = i
                found_mat = True
                break
        if not found_mat:
            gp.data.materials.append(mat)
            gp.active_material_index = len(gp.data.materials) - 1
            
        try: gp.active_material = mat
        except: pass
        
        try: bpy.ops.object.mode_set(mode='PAINT_GREASE_PENCIL')
        except: pass
        try: bpy.ops.grease_pencil.insert_blank_frame()
        except: pass

        try: context.view_layer.update()
        except: pass
        try: bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1)
        except: pass

        self.report({'INFO'}, f"'{gp.name}' を作成しました。")
        return {'FINISHED'}

class GPEASY_OT_CopyDefaults(Operator):
    bl_idname = f"{PREFIX}.copy_defaults"
    bl_label = "現在の色数値をコードとしてコピー"
    bl_description = "現在の色設定をスクリプト形式でクリップボードにコピーします。"

    def execute(self, context):
        props = getattr(context.scene, f"{PREFIX}_props")
        out_lines = []
        
        for i in range(1, 25):
            name = getattr(props, f"col_{i:02d}_name")
            stroke = getattr(props, f"col_{i:02d}_stroke")
            fill = getattr(props, f"col_{i:02d}_fill")
            
            s_v = f"({stroke[0]:.3f}, {stroke[1]:.3f}, {stroke[2]:.3f}, {stroke[3]:.3f})"
            f_v = f"({fill[0]:.3f}, {fill[1]:.3f}, {fill[2]:.3f}, {fill[3]:.3f})"
            
            line_str = (
                f'    col_{i:02d}_name: StringProperty(default="{name}"); '
                f'col_{i:02d}_stroke: FloatVectorProperty(subtype=\'COLOR\', size=4, default={s_v}, min=0.0, max=1.0); '
                f'col_{i:02d}_fill: FloatVectorProperty(subtype=\'COLOR\', size=4, default={f_v}, min=0.0, max=1.0)'
            )
            out_lines.append(line_str)
            
        c_board = "\n".join(out_lines)
        context.window_manager.clipboard = c_board
        self.report({'INFO'}, "色の数値をクリップボードにコピーしました!")
        return {'FINISHED'}

class GPEASY_OT_SetBrushTool(Operator):
    bl_idname = f"{PREFIX}.set_brush_tool"
    bl_label = "ツール変更"
    tool_type: StringProperty()
    
    def execute(self, context):
        if context.mode not in {'PAINT_GPENCIL', 'PAINT_GREASE_PENCIL'}:
            try: bpy.ops.object.mode_set(mode='PAINT_GREASE_PENCIL')
            except: pass

        if self.tool_type == 'ERASER':
            tool_ids = ["builtin.grease_pencil_eraser", "builtin.grease_pencil_erase", "builtin.eraser", "builtin_brush.Erase"]
            for t in tool_ids:
                try: bpy.ops.wm.tool_set_by_id(name=t)
                except: pass
            
            try:
                tools = context.workspace.tools.from_space_view3d_mode(context.mode)
                for item in tools:
                    if getattr(item, "idname", "") and 'erase' in item.idname.lower():
                        bpy.ops.wm.tool_set_by_id(name=item.idname)
                        break
            except: pass
            
            for ast in ["Hard Eraser", "Soft Eraser", "Stroke Eraser", "Point Eraser", "Eraser", "Erase"]:
                try: bpy.ops.brush.asset_activate(asset_id=ast)
                except: pass
                
            try:
                paint = getattr(context.scene.tool_settings, "grease_pencil_paint", getattr(context.scene.tool_settings, "gpencil_paint", None))
                if paint and paint.brush: paint.brush.gpencil_tool = 'ERASE'
            except: pass
            
        else:
            tool_ids = ["builtin.grease_pencil_draw", "builtin.brush", "builtin_brush.Draw"]
            for t in tool_ids:
                try: bpy.ops.wm.tool_set_by_id(name=t)
                except: pass

            try:
                tools = context.workspace.tools.from_space_view3d_mode(context.mode)
                for item in tools:
                    if getattr(item, "idname", ""):
                        idname_low = item.idname.lower()
                        if ('draw' in idname_low or 'brush' in idname_low) and 'erase' not in idname_low:
                            bpy.ops.wm.tool_set_by_id(name=item.idname)
                            break
            except: pass

            t_name = {"PEN":"Pen", "PENCIL":"Pencil", "MARKER":"Marker"}.get(self.tool_type, "Pen")
            for ast in [t_name, f"GP {t_name}", t_name.lower()]:
                try: bpy.ops.brush.asset_activate(asset_id=ast)
                except: pass

            try:
                paint = getattr(context.scene.tool_settings, "grease_pencil_paint", getattr(context.scene.tool_settings, "gpencil_paint", None))
                if paint and paint.brush:
                    if hasattr(paint.brush, "gpencil_tool") and paint.brush.gpencil_tool == 'ERASE':
                        paint.brush.gpencil_tool = 'DRAW'
                    try: paint.brush.use_material_pin = True
                    except: pass
            except: pass
            
        for area in context.screen.areas:
            if area.type == 'VIEW_3D': area.tag_redraw()
            
        return {'FINISHED'}

class GPEASY_OT_ToggleCanvas(Operator):
    bl_idname = f"{PREFIX}.toggle_canvas"
    bl_label = "グリッド表示"
    def execute(self, context):
        for area in context.screen.areas:
            if area.type == 'VIEW_3D':
                for space in area.spaces:
                    if space.type == 'VIEW_3D':
                        state = space.overlay.show_floor
                        space.overlay.show_floor = not state
                        space.overlay.show_ortho_grid = not state
        return {'FINISHED'}

class GPEASY_OT_SetBrushSize(Operator):
    bl_idname = f"{PREFIX}.set_brush_size"
    bl_label = "太さ"
    size: IntProperty(default=10)
    
    def execute(self, context):
        self.size = int(self.size)
        try: 
            context.scene.tool_settings.unified_paint_settings.use_unified_size = True
            context.scene.tool_settings.unified_paint_settings.size = self.size
        except: pass
        
        try:
            paint = getattr(context.scene.tool_settings, "grease_pencil_paint", getattr(context.scene.tool_settings, "gpencil_paint", None))
            if paint and paint.brush:
                paint.brush.size = self.size
        except: pass
        
        try:
            for b in bpy.data.brushes:
                if hasattr(b, "gpencil_tool") and b.gpencil_tool == 'ERASE': continue
                b.size = self.size
        except: pass
        
        return {'FINISHED'}

# -------------------------------------------------------------
# 安定化案: 透明度設定機能 (ブラシに不透明度を強制適用)
# -------------------------------------------------------------
class GPEASY_OT_SetBrushOpacity(Operator):
    bl_idname = f"{PREFIX}.set_brush_opacity"
    bl_label = "透明度"
    opacity: FloatProperty(default=1.0)
    
    def execute(self, context):
        # 1. 統合設定の強制
        try: 
            context.scene.tool_settings.unified_paint_settings.use_unified_weight = True
            context.scene.tool_settings.unified_paint_settings.weight = self.opacity
        except: pass

        try:
            context.scene.tool_settings.unified_paint_settings.use_unified_strength = True
            context.scene.tool_settings.unified_paint_settings.strength = self.opacity
        except: pass
        
        # 2. 現在のペイントブラシに直接設定
        try:
            paint = getattr(context.scene.tool_settings, "grease_pencil_paint", getattr(context.scene.tool_settings, "gpencil_paint", None))
            if paint and paint.brush:
                if hasattr(paint.brush, "strength"): paint.brush.strength = self.opacity
                if hasattr(paint.brush, "weight"): paint.brush.weight = self.opacity
        except: pass
        
        # 3. データ上の全描画ブラシを一括上書き (最強のフェイルセーフ)
        try:
            for b in bpy.data.brushes:
                if hasattr(b, "gpencil_tool") and b.gpencil_tool == 'ERASE': continue
                if hasattr(b, "strength"): b.strength = self.opacity
                if hasattr(b, "weight"): b.weight = self.opacity
        except: pass
        
        for area in context.screen.areas:
            if area.type == 'VIEW_3D': area.tag_redraw()
            
        return {'FINISHED'}

class GPEASY_OT_ApplyColor(Operator):
    bl_idname = f"{PREFIX}.apply_color"
    bl_label = "色"
    color_index: IntProperty(default=1)
    
    def execute(self, context):
        obj = context.active_object
        if not is_gp_object(obj): return {'CANCELLED'}
        props = getattr(context.scene, f"{PREFIX}_props")
        
        stroke_col = getattr(props, f"col_{self.color_index:02d}_stroke")
        fill_col = getattr(props, f"col_{self.color_index:02d}_fill")
        
        mat = get_or_create_gp_material(self.color_index, stroke_col, fill_col)
        
        found = False
        for i, slot_mat in enumerate(obj.data.materials):
            if slot_mat == mat:
                obj.active_material_index = i
                found = True
                break
        if not found:
            obj.data.materials.append(mat)
            obj.active_material_index = len(obj.data.materials) - 1
            
        try: obj.active_material = mat
        except: pass

        try:
            context.scene.tool_settings.unified_paint_settings.use_unified_color = True
            context.scene.tool_settings.unified_paint_settings.color = stroke_col[:3]
        except: pass

        try:
            for b in bpy.data.brushes:
                if hasattr(b, "gpencil_tool") and b.gpencil_tool == 'ERASE': continue
                
                if hasattr(b, "color"): b.color = stroke_col[:3]
                if hasattr(b, "secondary_color"): b.secondary_color = fill_col[:3]
                
                try: b.use_material_pin = True
                except: pass
                if hasattr(b, "gpencil_settings"):
                    try: b.gpencil_settings.pen_color = stroke_col[:3]
                    except: pass
                    try: b.gpencil_settings.use_material_pin = True
                    except: pass
        except: pass

        for area in context.screen.areas:
            if area.type == 'VIEW_3D': area.tag_redraw()
            
        return {'FINISHED'}

class GPEASY_OT_AddLayer(Operator):
    bl_idname = f"{PREFIX}.add_layer"
    bl_label = "新規レイヤー"
    
    def execute(self, context):
        obj = context.active_object
        if not is_gp_object(obj) or not hasattr(obj.data, "layers"): 
            return {'CANCELLED'}
        
        try:
            base_name = "Layer"
            new_name = base_name
            count = 1
            while obj.data.layers.get(new_name):
                new_name = f"{base_name} {count}"
                count += 1
                
            layer = obj.data.layers.new(name=new_name)
            obj.data.layers.active = layer
            if hasattr(layer, "frames"):
                layer.frames.new(context.scene.frame_current)
            return {'FINISHED'}
        except: pass
        
        try:
            bpy.ops.grease_pencil.layer_add()
            return {'FINISHED'}
        except: pass
        
        try:
            bpy.ops.gpencil.layer_add()
            return {'FINISHED'}
        except: pass
        
        return {'FINISHED'}

class GPEASY_OT_SetLayerActive(Operator):
    bl_idname = f"{PREFIX}.set_layer_active"
    bl_label = "レイヤー選択"
    layer_name: StringProperty()
    
    def execute(self, context):
        obj = context.active_object
        if is_gp_object(obj) and hasattr(obj.data, "layers"):
            layer = obj.data.layers.get(self.layer_name)
            if layer:
                try: obj.data.layers.active = layer
                except: pass
        return {'FINISHED'}

class GPEASY_OT_AddModifier(Operator):
    bl_idname = f"{PREFIX}.add_modifier"
    bl_label = "エフェクト追加"
    mod_type: StringProperty()
    def execute(self, context):
        obj = context.active_object
        if not is_gp_object(obj): return {'CANCELLED'}
        mods = getattr(obj, "modifiers", [])
        if self.mod_type == 'NOISE':
            m_name = "Boiling_Noise"
            if m_name not in mods:
                mod = mods.new(name=m_name, type='NODES')
                mod.node_group = create_noise_node_group()
        elif self.mod_type == 'THICKNESS':
            m_name = "Outline_Thickness"
            if m_name not in mods:
                mod = mods.new(name=m_name, type='NODES')
                mod.node_group = create_thickness_node_group()
        return {'FINISHED'}

class GPEASY_OT_ResetModifiers(Operator):
    bl_idname = f"{PREFIX}.reset_modifiers"
    bl_label = "設定を初期値に戻す"
    bl_options = {'REGISTER', 'UNDO'}
    def execute(self, context):
        obj = context.active_object
        if not is_gp_object(obj): return {'CANCELLED'}
        mods = getattr(obj, "modifiers", [])
        for m in mods:
            if m.type == 'NODES' and m.node_group:
                try:
                    if m.name == "Boiling_Noise":
                        id_name = [i.identifier for i in m.node_group.interface.items_tree if i.name == "Factor"][0]
                        m[id_name] = 0.05
                    elif m.name == "Outline_Thickness":
                        id_name = [i.identifier for i in m.node_group.interface.items_tree if i.name == "Add_Thickness"][0]
                        m[id_name] = 0.02
                except: pass
        self.report({'INFO'}, "エフェクトの数値を初期化しました")
        return {'FINISHED'}

class GPEASY_OT_FinishDrawing(Operator):
    bl_idname = f"{PREFIX}.finish_drawing"
    bl_label = "⑦ ✓ オブジェクト完成"
    def execute(self, context):
        if not is_gp_object(context.active_object): return {'CANCELLED'}
        try: bpy.ops.object.mode_set(mode='OBJECT')
        except: pass
        try: getattr(bpy.ops, PREFIX).create_gp()
        except: pass
        return {'FINISHED'}

class GPEASY_OT_EditPalette(Operator):
    bl_idname = f"{PREFIX}.edit_palette"
    bl_label = "🎨 パレットの色を編集"
    def invoke(self, context, event): return context.window_manager.invoke_props_dialog(self, width=400)
    def draw(self, context):
        layout = self.layout
        props = getattr(context.scene, f"{PREFIX}_props")
        layout.label(text="名前 / Stroke(線) / Fill(塗り)", icon='COLOR')
        for i in range(1, 25):
            row = layout.row(align=True)
            row.prop(props, f"col_{i:02d}_name", text="")
            row.prop(props, f"col_{i:02d}_stroke", text="")
            row.prop(props, f"col_{i:02d}_fill", text="")
    def execute(self, context): return {'FINISHED'}

class GPEASY_OT_RemoveAllPanels(Operator):
    bl_idname = f"{PREFIX}.remove_all_panels"
    bl_label = "Unregister Addon"
    def execute(self, context): unregister(); return {'FINISHED'}

# =========================================================
# 6. パネル UI
# =========================================================
class GPEASY_PT_Workflow(Panel):
    bl_label = "1. 描画ワークフロー"
    bl_idname = f"{PREFIX_VAL}_PT_workflow"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = ADDON_TITLE
    bl_order = 0

    def draw(self, context):
        layout = self.layout
        props = getattr(context.scene, f"{PREFIX}_props")
        
        col = layout.column(align=True)
        col.scale_y = 1.5
        col.operator(GPEASY_OT_CreateGP.bl_idname, icon='ADD')
        layout.separator()
        
        box_tool = layout.box()
        box_tool.label(text="② ツール & グリッド:", icon='TOOL_SETTINGS')
        row_tool = box_tool.row(align=True)
        row_tool.operator(GPEASY_OT_SetBrushTool.bl_idname, text="ペン", icon='GREASEPENCIL').tool_type = 'PEN'
        row_tool.operator(GPEASY_OT_SetBrushTool.bl_idname, text="鉛筆", icon='EDITMODE_HLT').tool_type = 'PENCIL'
        row_tool.operator(GPEASY_OT_SetBrushTool.bl_idname, text="マーカー", icon='BRUSH_DATA').tool_type = 'MARKER'
        row_tool.operator(GPEASY_OT_SetBrushTool.bl_idname, text="消しゴム", icon='TRASH').tool_type = 'ERASER'
        box_tool.operator(GPEASY_OT_ToggleCanvas.bl_idname, text="マス目ガイド ON/OFF", icon='GRID')
        
        box_size = layout.box()
        box_size.label(text="③ ペンの太さ:", icon='BRUSH_DATA')
        r1 = box_size.row(align=True)
        r1.operator(GPEASY_OT_SetBrushSize.bl_idname, text="極細 (3)").size = 3
        r1.operator(GPEASY_OT_SetBrushSize.bl_idname, text="細 (10)").size = 10
        r1.operator(GPEASY_OT_SetBrushSize.bl_idname, text="中 (20)").size = 20
        r2 = box_size.row(align=True)
        r2.operator(GPEASY_OT_SetBrushSize.bl_idname, text="太 (30)").size = 30
        r2.operator(GPEASY_OT_SetBrushSize.bl_idname, text="極太 (50)").size = 50
        r2.operator(GPEASY_OT_SetBrushSize.bl_idname, text="特大 (100)").size = 100
        
        # ★ 追加された透明度エリア
        box_opacity = layout.box()
        box_opacity.label(text="④ ペンの透明度:", icon='IMAGE_ALPHA')
        r3 = box_opacity.row(align=True)
        r3.operator(GPEASY_OT_SetBrushOpacity.bl_idname, text="10% (極薄)").opacity = 0.1
        r3.operator(GPEASY_OT_SetBrushOpacity.bl_idname, text="30%").opacity = 0.3
        r3.operator(GPEASY_OT_SetBrushOpacity.bl_idname, text="50%").opacity = 0.5
        r3.operator(GPEASY_OT_SetBrushOpacity.bl_idname, text="80%").opacity = 0.8
        r3.operator(GPEASY_OT_SetBrushOpacity.bl_idname, text="100% (濃)").opacity = 1.0

        box_col = layout.box()
        r_col = box_col.row()
        r_col.label(text="⑤ 色を選ぶ:", icon='COLOR')
        r_col.operator(GPEASY_OT_EditPalette.bl_idname, text="", icon='PREFERENCES')
        
        box_col.operator(GPEASY_OT_CopyDefaults.bl_idname, text="色設定のコードをコピー", icon='COPYDOWN')
        box_col.separator()
        
        grid = box_col.grid_flow(columns=3, align=True)
        for i in range(1, 25):
            name = getattr(props, f"col_{i:02d}_name")
            c = grid.column(align=True)
            r = c.row(align=True)
            r.scale_y = 1.5 
            r.prop(props, f"col_{i:02d}_stroke", text="")
            r.prop(props, f"col_{i:02d}_fill", text="")
            op = r.operator(GPEASY_OT_ApplyColor.bl_idname, text=name)
            op.color_index = i

        obj = context.active_object
        if is_gp_object(obj):
            box_layer = layout.box()
            row_head = box_layer.row()
            row_head.label(text="⑥ レイヤー:", icon='GROUP')
            row_head.operator(GPEASY_OT_AddLayer.bl_idname, text="追加", icon='ADD')
            
            if hasattr(obj.data, "layers") and obj.data.layers:
                layers_list = []
                try: layers_list = list(obj.data.layers)
                except: pass
                
                for layer in reversed(layers_list):
                    row_layer = box_layer.row(align=True)
                    
                    is_active = False
                    try: 
                        if hasattr(obj.data.layers, "active") and obj.data.layers.active:
                            is_active = (obj.data.layers.active.name == layer.name)
                    except: pass
                    
                    icon = 'RADIOBUT_ON' if is_active else 'RADIOBUT_OFF'
                    op_act = row_layer.operator(GPEASY_OT_SetLayerActive.bl_idname, text=getattr(layer, "name", "Layer"), icon=icon)
                    op_act.layer_name = getattr(layer, "name", "")
                    
                    if hasattr(layer, "hide"): 
                        row_layer.prop(layer, "hide", text="", icon='HIDE_ON' if getattr(layer, "hide", False) else 'HIDE_OFF')
                    elif hasattr(layer, "hide_viewport"): 
                        row_layer.prop(layer, "hide_viewport", text="", icon='HIDE_ON' if getattr(layer, "hide_viewport", False) else 'HIDE_OFF')
                        
                    if hasattr(layer, "lock"): 
                        row_layer.prop(layer, "lock", text="", icon='LOCKED' if getattr(layer, "lock", False) else 'UNLOCKED')
                        
                    if hasattr(layer, "opacity"): 
                        row_layer.prop(layer, "opacity", text="", slider=True)
            else:
                box_layer.label(text="レイヤーなし")

            box_fx = layout.box()
            box_fx.label(text="★ エフェクト追加 (再生で動きます):", icon='MODIFIER')
            row_fx = box_fx.row(align=True)
            row_fx.operator(GPEASY_OT_AddModifier.bl_idname, text="ボイリング(震え)").mod_type = 'NOISE'
            row_fx.operator(GPEASY_OT_AddModifier.bl_idname, text="線を太くする").mod_type = 'THICKNESS'
            
            mods = getattr(obj, "modifiers", [])
            has_effect = False
            for m in mods:
                if m.type == 'NODES' and m.node_group:
                    if m.name in ["Boiling_Noise", "Outline_Thickness"]: has_effect = True
                    try:
                        if m.name == "Boiling_Noise":
                            id_name = [i.identifier for i in m.node_group.interface.items_tree if i.name == "Factor"][0]
                            if id_name not in m: m[id_name] = 0.05
                            box_fx.prop(m, f'["{id_name}"]', text="震えの強さ (-10〜10)", slider=True)
                        elif m.name == "Outline_Thickness":
                            id_name = [i.identifier for i in m.node_group.interface.items_tree if i.name == "Add_Thickness"][0]
                            if id_name not in m: m[id_name] = 0.02
                            box_fx.prop(m, f'["{id_name}"]', text="追加する太さ (0〜10)", slider=True)
                    except: pass
            
            if has_effect:
                box_fx.operator(GPEASY_OT_ResetModifiers.bl_idname, text="設定を初期値に戻す", icon='FILE_REFRESH')

        layout.separator()
        col = layout.column(align=True)
        col.scale_y = 1.5
        col.operator(GPEASY_OT_FinishDrawing.bl_idname, icon='CHECKMARK')

class GPEASY_PT_Manager(Panel):
    bl_label = "2. オブジェクト管理"
    bl_idname = f"{PREFIX_VAL}_PT_manager"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = ADDON_TITLE
    bl_order = 1
    bl_options = {'DEFAULT_CLOSED'}

    def draw(self, context):
        layout = self.layout
        box = layout.box()
        props = getattr(context.scene, f"{PREFIX}_props")
        
        col = bpy.data.collections.get(props.col_name)
        if col:
            sub_col = bpy.data.collections.get(props.sub_col_name)
            target_col = sub_col if sub_col else col
            for obj in target_col.objects:
                if is_gp_object(obj):
                    row = box.row(align=True)
                    icon = 'RADIOBUT_ON' if context.active_object == obj else 'RADIOBUT_OFF'
                    row.prop(obj, "name", text="", icon=icon)
                    row.prop(obj, "hide_viewport", text="", icon='HIDE_ON' if obj.hide_viewport else 'HIDE_OFF')
                    row.prop(obj, "hide_select", text="", icon='RESTRICT_SELECT_ON' if obj.hide_select else 'UNLOCKED')
        else: box.label(text="まだ描画がありません")

class GPEASY_PT_Settings(Panel):
    bl_label = "3. 設定 & リンク"
    bl_idname = f"{PREFIX_VAL}_PT_settings"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = ADDON_TITLE
    bl_order = 2
    bl_options = {'DEFAULT_CLOSED'}

    def draw(self, context):
        layout = self.layout
        props = getattr(context.scene, f"{PREFIX}_props")

        box = layout.box()
        box.label(text="▼ コレクション&名前設定", icon='PREFERENCES')
        box.prop(props, "col_name")
        box.prop(props, "sub_col_name")
        box.prop(props, "obj_base_name")

        box_link = layout.box()
        box_link.label(text="▼ カスタムリンク (1番)", icon='URL')
        box_link.prop(props, "link_1_name", text="名前")
        box_link.prop(props, "link_1_url", text="URL")

        box_links = layout.box()
        box_links.label(text="▼ リンクを開く", icon='WORLD_DATA')
        if props.link_1_url:
            box_links.operator("wm.url_open", text=props.link_1_name, icon='URL').url = props.link_1_url
        box_links.operator("wm.url_open", text=LINK_NAME_2, icon='URL').url = LINK_URL_2
        box_links.operator("wm.url_open", text=LINK_NAME_3, icon='URL').url = LINK_URL_3
        box_links.operator("wm.url_open", text=LINK_NAME_4, icon='URL').url = LINK_URL_4

class GPEASY_PT_RemovePanel(Panel):
    bl_label = "4. アドオン削除"
    bl_idname = f"{PREFIX_VAL}_PT_remove"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = ADDON_TITLE
    bl_order = 3
    bl_options = {'DEFAULT_CLOSED'}
    def draw(self, context):
        layout = self.layout
        layout.operator(GPEASY_OT_RemoveAllPanels.bl_idname, icon='CANCEL')

# =========================================================
# 7. 登録・解除
# =========================================================
classes = (
    GPEASY_Properties,
    GPEASY_OT_CreateGP, GPEASY_OT_CopyDefaults,
    GPEASY_OT_SetBrushTool, GPEASY_OT_ToggleCanvas, GPEASY_OT_SetBrushSize, GPEASY_OT_SetBrushOpacity, 
    GPEASY_OT_ApplyColor, GPEASY_OT_AddModifier, GPEASY_OT_ResetModifiers, 
    GPEASY_OT_AddLayer, GPEASY_OT_SetLayerActive, GPEASY_OT_FinishDrawing, 
    GPEASY_OT_EditPalette, GPEASY_OT_RemoveAllPanels,
    GPEASY_PT_Workflow, GPEASY_PT_Manager, GPEASY_PT_Settings, GPEASY_PT_RemovePanel,
)

def register():
    for cls in classes: bpy.utils.register_class(cls)
    setattr(bpy.types.Scene, f"{PREFIX}_props", PointerProperty(type=GPEASY_Properties))

def unregister():
    if hasattr(bpy.types.Scene, f"{PREFIX}_props"):
        delattr(bpy.types.Scene, f"{PREFIX}_props")
    for cls in reversed(classes):
        try: bpy.utils.unregister_class(cls)
        except: pass

if __name__ == "__main__": register()