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()