blender Million 2026













# Copied: 15:00:01
import bpy
import bmesh
import math
import webbrowser
from bpy.props import FloatVectorProperty, FloatProperty, BoolProperty, PointerProperty, StringProperty
from bpy.types import Operator, Panel, PropertyGroup
from mathutils import Vector
from datetime import datetime

# ==============================================================================
#  設定エリア & ID管理
# ==============================================================================

PREFIX = "GeoSplit20260308"
TAB_NAME = "   [ Geo Split ]   "

# ### ZIONAD_SOURCE_ID: GEO_SPLIT_2026_03_08_COLOR ###

bl_info = {
    "name": f"zionad 520 [ Geo Split ] {PREFIX}",
    "author": "zionadchat",
    "version": (3, 3, 0),
    "blender": (3, 0, 0),
    "location": "3D View > Sidebar",
    "description": "Earth Splitter & Cone Generator with Multi-Color",
    "category": "3D View",
}

OP_PREFIX = PREFIX.lower()
PROPS_NAME = f"{PREFIX}_props"
SOURCE_ID_TAG = "### ZIONAD_SOURCE_ID: GEO_SPLIT_2026_03_08_COLOR ###"

ADDON_LINKS = (
    {"label": "Code Copy Template", "url": "<https://www.notion.so/Code-copy-20260221>"},
    {"label": "Theory Background", "url": "<https://www.notion.so/Einstein-from-20260119>"},
)

# ==============================================================================
#  デフォルト値設定 (コピー機能で書き換わるエリア)
# ==============================================================================
# <BEGIN_DICT>
CURRENT_DEFAULTS = {
    "show_preview": True,
    "color_upper": (0.05, 0.40, 0.80, 0.80),     # 青系
    "color_lower": (0.10, 0.60, 0.20, 0.80),     # 緑系
    "color_cone_lat": (0.90, 0.80, 0.10, 0.80),  # 黄色系
    "color_cone_polar": (0.90, 0.20, 0.20, 0.80),# 赤系
    "sphere_loc": (0.0000, 0.0000, 0.0000),
    "sphere_radius": 5.0000,
    "split_latitude": 60.0000,
    "show_upper": True,
    "show_lower": True,
    "show_cone_lat": True,
    "show_cone_polar": True,
}
# <END_DICT>

# ==============================================================================
#  ジオメトリ生成ロジック (BMesh)
# ==============================================================================

def build_upper_sphere(radius, lat_deg):
    bm = bmesh.new()
    bmesh.ops.create_uvsphere(bm, u_segments=64, v_segments=32, radius=radius)
    z_cut = radius * math.sin(math.radians(lat_deg))
    geom = bm.verts[:] + bm.edges[:] + bm.faces[:]
    res = bmesh.ops.bisect_plane(bm, geom=geom, plane_co=(0,0,z_cut), plane_no=(0,0,1), clear_inner=True, clear_outer=False)
    cut_edges = [e for e in res['geom_cut'] if isinstance(e, bmesh.types.BMEdge)]
    if cut_edges:
        try: bmesh.ops.holes_fill(bm, edges=cut_edges)
        except: pass
    return bm

def build_lower_sphere(radius, lat_deg):
    bm = bmesh.new()
    bmesh.ops.create_uvsphere(bm, u_segments=64, v_segments=32, radius=radius)
    z_cut = radius * math.sin(math.radians(lat_deg))
    geom = bm.verts[:] + bm.edges[:] + bm.faces[:]
    res = bmesh.ops.bisect_plane(bm, geom=geom, plane_co=(0,0,z_cut), plane_no=(0,0,1), clear_inner=False, clear_outer=True)
    cut_edges = [e for e in res['geom_cut'] if isinstance(e, bmesh.types.BMEdge)]
    if cut_edges:
        try: bmesh.ops.holes_fill(bm, edges=cut_edges)
        except: pass
    return bm

def build_cone_lat(radius, lat_deg):
    bm = bmesh.new()
    lat_rad = math.radians(lat_deg)
    z_cut = radius * math.sin(lat_rad)
    r_base = radius * math.cos(lat_rad)
    depth = abs(z_cut)
    if depth < 0.0001: depth = 0.0001
        
    if z_cut >= 0:
        bmesh.ops.create_cone(bm, segments=64, radius1=0, radius2=r_base, depth=depth)
        bmesh.ops.translate(bm, vec=(0,0, depth/2), verts=bm.verts)
    else:
        bmesh.ops.create_cone(bm, segments=64, radius1=r_base, radius2=0, depth=depth)
        bmesh.ops.translate(bm, vec=(0,0, -depth/2), verts=bm.verts)
    return bm

def build_cone_polar(radius):
    bm = bmesh.new()
    bmesh.ops.create_cone(bm, segments=64, radius1=radius, radius2=0, depth=radius)
    bmesh.ops.translate(bm, vec=(0,0, radius/2), verts=bm.verts)
    return bm

def create_unique_material(color, name_prefix="Mat"):
    timestamp = datetime.now().strftime('%M%S%f')[:5] 
    mat_name = f"{name_prefix}_{timestamp}"
    mat = bpy.data.materials.new(name=mat_name)
    mat.use_nodes = True
    mat.blend_method = 'BLEND'
    if mat.use_nodes:
        tree = mat.node_tree
        tree.nodes.clear()
        bsdf = tree.nodes.new("ShaderNodeBsdfPrincipled")
        bsdf.location = (0, 0)
        out = tree.nodes.new("ShaderNodeOutputMaterial")
        out.location = (300, 0)
        tree.links.new(bsdf.outputs[0], out.inputs[0])
        if "Base Color" in bsdf.inputs: bsdf.inputs['Base Color'].default_value = color
        if "Alpha" in bsdf.inputs: bsdf.inputs['Alpha'].default_value = color[3]
    mat.diffuse_color = color
    return mat

# ==============================================================================
#  プレビュー用ロジック
# ==============================================================================
PREVIEW_COL_NAME = f"{PREFIX}_Preview_Zone"
PREVIEW_TAG = f"{PREFIX}_preview_tag"

def update_preview_geometry(context):
    props = getattr(context.scene, PROPS_NAME, None)
    if not props: return

    col = bpy.data.collections.get(PREVIEW_COL_NAME)
    if not col:
        col = bpy.data.collections.new(PREVIEW_COL_NAME)
        context.scene.collection.children.link(col)
    
    for o in[o for o in col.objects if o.get(PREVIEW_TAG)]:
        bpy.data.objects.remove(o, do_unlink=True)

    if not props.show_preview: return

    # オブジェクトと個別の色を一緒にリストにまとめる
    parts =[]
    if props.show_upper: 
        parts.append(("UpperSphere", build_upper_sphere(props.sphere_radius, props.split_latitude), props.color_upper))
    if props.show_lower: 
        parts.append(("LowerSphere", build_lower_sphere(props.sphere_radius, props.split_latitude), props.color_lower))
    if props.show_cone_lat: 
        parts.append(("ConeCenterLat", build_cone_lat(props.sphere_radius, props.split_latitude), props.color_cone_lat))
    if props.show_cone_polar: 
        parts.append(("ConePolarEq", build_cone_polar(props.sphere_radius), props.color_cone_polar))

    for name, bm, color in parts:
        bmesh.ops.translate(bm, vec=Vector(props.sphere_loc), verts=bm.verts)
        mesh = bpy.data.meshes.new(f"PreviewMesh_{name}")
        bm.to_mesh(mesh)
        bm.free()
        
        obj = bpy.data.objects.new(f"[Preview] {name}", mesh)
        obj[PREVIEW_TAG] = True
        col.objects.link(obj)
        
        # 個別カラーでマテリアル生成
        mat = create_unique_material(color, f"Mat_Prev_{name}")
        obj.data.materials.append(mat)
        
        obj.show_wire = True
        obj.show_all_edges = True
        obj.select_set(False)

_timer = None
def delayed_update():
    global _timer
    _timer = None
    if bpy.context and bpy.context.scene:
        update_preview_geometry(bpy.context)
    return None

def on_update(self, context):
    global _timer
    if _timer: 
        try: bpy.app.timers.unregister(_timer)
        except: pass
    _timer = bpy.app.timers.register(delayed_update, first_interval=0.05)

# ==============================================================================
#  PROPERTIES
# ==============================================================================
class PG_SphereProps(PropertyGroup):
    show_preview: BoolProperty(name="Show Preview", default=CURRENT_DEFAULTS['show_preview'], update=on_update)
    sphere_loc: FloatVectorProperty(name="Location", size=3, default=CURRENT_DEFAULTS['sphere_loc'], update=on_update)
    sphere_radius: FloatProperty(name="Radius", default=CURRENT_DEFAULTS['sphere_radius'], min=0.01, update=on_update)
    split_latitude: FloatProperty(name="Split Latitude", default=CURRENT_DEFAULTS['split_latitude'], min=-90.0, max=90.0, update=on_update)
    
    # 個別カラープロパティ
    color_upper: FloatVectorProperty(name="Upper Color", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color_upper'], update=on_update)
    color_lower: FloatVectorProperty(name="Lower Color", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color_lower'], update=on_update)
    color_cone_lat: FloatVectorProperty(name="Cone (Lat) Color", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color_cone_lat'], update=on_update)
    color_cone_polar: FloatVectorProperty(name="Cone (Polar) Color", subtype='COLOR', size=4, min=0, max=1, default=CURRENT_DEFAULTS['color_cone_polar'], update=on_update)

    show_upper: BoolProperty(name="Show Upper Sphere", default=CURRENT_DEFAULTS['show_upper'], update=on_update)
    show_lower: BoolProperty(name="Show Lower Sphere", default=CURRENT_DEFAULTS['show_lower'], update=on_update)
    show_cone_lat: BoolProperty(name="Show Cone (Lat)", default=CURRENT_DEFAULTS['show_cone_lat'], update=on_update)
    show_cone_polar: BoolProperty(name="Show Cone (Polar)", default=CURRENT_DEFAULTS['show_cone_polar'], update=on_update)

# ==============================================================================
#  OPERATORS
# ==============================================================================
class OT_CreateSphere(Operator):
    bl_idname = f"{OP_PREFIX}.create_sphere"
    bl_label = "Create Objects"
    bl_options = {'REGISTER', 'UNDO'}
    
    def execute(self, context):
        props = getattr(context.scene, PROPS_NAME, None)
        
        parts =[]
        if props.show_upper: 
            parts.append(("UpperSphere", build_upper_sphere(props.sphere_radius, props.split_latitude), props.color_upper))
        if props.show_lower: 
            parts.append(("LowerSphere", build_lower_sphere(props.sphere_radius, props.split_latitude), props.color_lower))
        if props.show_cone_lat: 
            parts.append(("ConeCenterLat", build_cone_lat(props.sphere_radius, props.split_latitude), props.color_cone_lat))
        if props.show_cone_polar: 
            parts.append(("ConePolarEq", build_cone_polar(props.sphere_radius), props.color_cone_polar))

        if not parts:
            self.report({'WARNING'}, "No visibility options selected.")
            return {'CANCELLED'}

        created_objs =[]
        timestamp = datetime.now().strftime('%H%M%S')
        
        for name, bm, color in parts:
            bmesh.ops.translate(bm, vec=Vector(props.sphere_loc), verts=bm.verts)
            mesh = bpy.data.meshes.new(f"{name}_Mesh")
            bm.to_mesh(mesh)
            bm.free()
            
            obj = bpy.data.objects.new(f"{name}_{timestamp}", mesh)
            
            if context.collection: context.collection.objects.link(obj)
            else: context.scene.collection.objects.link(obj)
                
            # 個別カラーのマテリアルを適用
            unique_mat = create_unique_material(color, f"Mat_{name}")
            obj.data.materials.append(unique_mat)
            created_objs.append(obj)
            
        bpy.ops.object.select_all(action='DESELECT')
        for obj in created_objs:
            obj.select_set(True)
        if created_objs:
            context.view_layer.objects.active = created_objs[-1]
        
        self.report({'INFO'}, f"Created {len(created_objs)} Parts Successfully")
        return {'FINISHED'}

class OT_CopyFullScript(Operator):
    bl_idname = f"{OP_PREFIX}.copy_script"
    bl_label = "Copy Script"
    def execute(self, context):
        props = getattr(context.scene, PROPS_NAME, None)
        target_text = None
        for t in bpy.data.texts:
            if SOURCE_ID_TAG in t.as_string():
                target_text = t; break
        if not target_text:
            self.report({'ERROR'}, "Script source not found.")
            return {'CANCELLED'}

        code = target_text.as_string()
        l = props.sphere_loc
        cu = props.color_upper
        cl = props.color_lower
        ccl = props.color_cone_lat
        ccp = props.color_cone_polar
        
        new_dict = "CURRENT_DEFAULTS = {\n"
        new_dict += f'    "show_preview": {props.show_preview},\n'
        new_dict += f'    "color_upper": ({cu[0]:.4f}, {cu[1]:.4f}, {cu[2]:.4f}, {cu[3]:.4f}),\n'
        new_dict += f'    "color_lower": ({cl[0]:.4f}, {cl[1]:.4f}, {cl[2]:.4f}, {cl[3]:.4f}),\n'
        new_dict += f'    "color_cone_lat": ({ccl[0]:.4f}, {ccl[1]:.4f}, {ccl[2]:.4f}, {ccl[3]:.4f}),\n'
        new_dict += f'    "color_cone_polar": ({ccp[0]:.4f}, {ccp[1]:.4f}, {ccp[2]:.4f}, {ccp[3]:.4f}),\n'
        new_dict += f'    "sphere_loc": ({l[0]:.4f}, {l[1]:.4f}, {l[2]:.4f}),\n'
        new_dict += f'    "sphere_radius": {props.sphere_radius:.4f},\n'
        new_dict += f'    "split_latitude": {props.split_latitude:.4f},\n'
        new_dict += f'    "show_upper": {props.show_upper},\n'
        new_dict += f'    "show_lower": {props.show_lower},\n'
        new_dict += f'    "show_cone_lat": {props.show_cone_lat},\n'
        new_dict += f'    "show_cone_polar": {props.show_cone_polar},\n'
        new_dict += "}\n"

        try:
            start, end = "# <BEGIN" + "_DICT>", "# <END" + "_DICT>"
            pre, post = code.split(start)[0], code.split(end)[1]
            final = f"# Copied: {datetime.now().strftime('%H:%M:%S')}\n" + pre + start + "\n" + new_dict + end + post
            context.window_manager.clipboard = final
            self.report({'INFO'}, "Code copied!")
        except: return {'CANCELLED'}
        return {'FINISHED'}

class OT_Reset(Operator):
    bl_idname = f"{OP_PREFIX}.reset"
    bl_label = "Reset"
    def execute(self, context):
        p = getattr(context.scene, PROPS_NAME)
        p.sphere_loc = (0,0,0)
        p.sphere_radius = 5.0
        p.split_latitude = 60.0
        p.show_upper = True; p.show_lower = True
        p.show_cone_lat = True; p.show_cone_polar = True
        return {'FINISHED'}

class OT_OpenUrl(Operator):
    bl_idname = f"{OP_PREFIX}.open_url"; bl_label = "Open URL"; url: StringProperty()
    def execute(self, context): webbrowser.open(self.url); return {'FINISHED'}

class OT_RemoveAddon(Operator):
    bl_idname = f"{OP_PREFIX}.remove_addon"; bl_label = "Remove Addon"
    def execute(self, context):
        bpy.app.timers.register(lambda: unregister(), first_interval=0.1)
        return {'FINISHED'}

# ==============================================================================
#  PANELS
# ==============================================================================
class PT_MainPanel(Panel):
    bl_label = "Earth / Geo Splitter"
    bl_idname = f"{PREFIX}_PT_main"
    bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = TAB_NAME

    def draw(self, context):
        layout = self.layout
        props = getattr(context.scene, PROPS_NAME, None)
        if not props: layout.label(text="Reload Script"); return

        row = layout.row()
        row.scale_y = 1.2
        row.operator(OT_CopyFullScript.bl_idname, icon='COPY_ID', text="Copy Code with Values")
        layout.separator()

        layout.prop(props, "show_preview", icon='RESTRICT_VIEW_OFF' if props.show_preview else 'RESTRICT_VIEW_ON')
        
        box = layout.box()
        if not props.show_preview:
            box.label(text="Preview is Hidden", icon='INFO')
            
        box.prop(props, "sphere_loc")
        box.prop(props, "sphere_radius")
        box.prop(props, "split_latitude", text="Latitude (Degree)")
        
        box.label(text="Visibility & Colors:")
        
        # Upper Sphere
        row = box.row(align=True)
        row.prop(props, "show_upper", text="Upper Sphere (North)", icon='HIDE_OFF' if props.show_upper else 'HIDE_ON')
        if props.show_upper: row.prop(props, "color_upper", text="")
            
        # Lower Sphere
        row = box.row(align=True)
        row.prop(props, "show_lower", text="Lower Sphere (South)", icon='HIDE_OFF' if props.show_lower else 'HIDE_ON')
        if props.show_lower: row.prop(props, "color_lower", text="")
            
        # Cone Center to Lat
        row = box.row(align=True)
        row.prop(props, "show_cone_lat", text="Cone (Center to Lat)", icon='HIDE_OFF' if props.show_cone_lat else 'HIDE_ON')
        if props.show_cone_lat: row.prop(props, "color_cone_lat", text="")
            
        # Cone Pole to Eq
        row = box.row(align=True)
        row.prop(props, "show_cone_polar", text="Cone (Pole to Eq)", icon='HIDE_OFF' if props.show_cone_polar else 'HIDE_ON')
        if props.show_cone_polar: row.prop(props, "color_cone_polar", text="")

        box.operator(OT_Reset.bl_idname, icon='LOOP_BACK', text="Reset Params")

        layout.separator()
        col = layout.column()
        col.scale_y = 1.5
        col.operator(OT_CreateSphere.bl_idname, icon='MESH_UVSPHERE', text="Create Objects")

class PT_LinksPanel(Panel):
    bl_label = "Links"; bl_idname = f"{PREFIX}_PT_links"; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = TAB_NAME; bl_options = {'DEFAULT_CLOSED'}
    def draw(self, context):
        for l in ADDON_LINKS: self.layout.operator(OT_OpenUrl.bl_idname, text=l["label"]).url = l["url"]

class PT_RemovePanel(Panel):
    bl_label = "System"; bl_idname = f"{PREFIX}_PT_remove"; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = TAB_NAME; bl_options = {'DEFAULT_CLOSED'}
    def draw(self, context): self.layout.operator(OT_RemoveAddon.bl_idname, icon='CANCEL', text="Remove Addon")

# ==============================================================================
#  REGISTER
# ==============================================================================
classes = (PG_SphereProps, OT_CreateSphere, OT_CopyFullScript, OT_Reset, OT_OpenUrl, OT_RemoveAddon, PT_MainPanel, PT_LinksPanel, PT_RemovePanel)

def register():
    for c in classes: bpy.utils.register_class(c)
    setattr(bpy.types.Scene, PROPS_NAME, PointerProperty(type=PG_SphereProps))

def unregister():
    if hasattr(bpy.types.Scene, PROPS_NAME): delattr(bpy.types.Scene, PROPS_NAME)
    for c in reversed(classes): bpy.utils.unregister_class(c)

if __name__ == "__main__": register()