# -*- coding: utf-8 -*-
"""
生成「物理模块 + 神经网络层 深度耦合」架构图的可编辑 PPT 素材。
每个子元素单独一页，文字均为可编辑文本框；另含一页完整拼装参考。

运行: python generate_physics_nn_diagram.py
"""
from __future__ import annotations

import math
import random
from pathlib import Path

from pptx import Presentation
from pptx.dml.color import RGBColor
from pptx.enum.dml import MSO_LINE_DASH_STYLE
from pptx.enum.shapes import MSO_CONNECTOR, MSO_SHAPE
from pptx.enum.text import MSO_ANCHOR, PP_ALIGN
from pptx.util import Inches, Pt

OUT_DIR = Path(__file__).resolve().parent
OUT_PPTX = OUT_DIR / "物理神经网络耦合图_可编辑素材.pptx"

# 配色（贴近原图）
C_BG = RGBColor(0x0A, 0x0A, 0x0A)
C_WHITE = RGBColor(0xFF, 0xFF, 0xFF)
C_BLACK = RGBColor(0x00, 0x00, 0x00)
C_BLUE = RGBColor(0x4A, 0x86, 0xC8)
C_BLUE_LT = RGBColor(0x6D, 0xA3, 0xE8)
C_TEAL = RGBColor(0x3D, 0xA8, 0x9A)
C_GREY = RGBColor(0xB0, 0xB0, 0xB0)
C_GREY_DK = RGBColor(0x55, 0x55, 0x55)
C_GREY_LT = RGBColor(0xD9, 0xD9, 0xD9)
C_LINE = RGBColor(0x88, 0x88, 0x88)

FONT = "Microsoft YaHei"
SLIDE_W = Inches(13.333)
SLIDE_H = Inches(7.5)


def new_prs() -> Presentation:
    prs = Presentation()
    prs.slide_width = SLIDE_W
    prs.slide_height = SLIDE_H
    return prs


def blank_slide(prs: Presentation, dark: bool = False):
    slide = prs.slides.add_slide(prs.slide_layouts[6])
    if dark:
        fill = slide.background.fill
        fill.solid()
        fill.fore_color.rgb = C_BG
    return slide


def add_label(slide, left, top, width, height, text, size=14, bold=False,
              color=C_BLACK, align=PP_ALIGN.CENTER, wrap=True):
    box = slide.shapes.add_textbox(left, top, width, height)
    tf = box.text_frame
    tf.word_wrap = wrap
    tf.vertical_anchor = MSO_ANCHOR.MIDDLE
    p = tf.paragraphs[0]
    p.text = text
    p.alignment = align
    p.font.name = FONT
    p.font.size = Pt(size)
    p.font.bold = bold
    p.font.color.rgb = color
    return box


def add_round_box(slide, left, top, width, height, fill=C_WHITE, line=C_BLACK,
                  line_w=1.0, radius_hint=0.08):
    shape = slide.shapes.add_shape(
        MSO_SHAPE.ROUNDED_RECTANGLE, left, top, width, height
    )
    shape.fill.solid()
    shape.fill.fore_color.rgb = fill
    shape.line.color.rgb = line
    shape.line.width = Pt(line_w)
    # 圆角
    if hasattr(shape, "adjustments") and shape.adjustments:
        shape.adjustments[0] = radius_hint
    return shape


def add_oval(slide, left, top, size, fill, line=None, line_w=1.0):
    shape = slide.shapes.add_shape(MSO_SHAPE.OVAL, left, top, size, size)
    shape.fill.solid()
    shape.fill.fore_color.rgb = fill
    if line:
        shape.line.color.rgb = line
        shape.line.width = Pt(line_w)
    else:
        shape.line.fill.background()
    return shape


def add_line(slide, x1, y1, x2, y2, color=C_LINE, width=0.75, dash=None):
    conn = slide.shapes.add_connector(MSO_CONNECTOR.STRAIGHT, x1, y1, x2, y2)
    conn.line.color.rgb = color
    conn.line.width = Pt(width)
    if dash:
        conn.line.dash_style = dash
    return conn


def add_arrow_down(slide, cx, y1, y2, color=C_WHITE, dash=False):
    add_line(slide, cx, y1, cx, y2 - Inches(0.08), color, 1.2,
             MSO_LINE_DASH_STYLE.DASH if dash else None)
    tri = slide.shapes.add_shape(
        MSO_SHAPE.ISOSCELES_TRIANGLE,
        cx - Inches(0.06), y2 - Inches(0.12), Inches(0.12), Inches(0.12)
    )
    tri.rotation = 180
    tri.fill.solid()
    tri.fill.fore_color.rgb = color
    tri.line.fill.background()
    return tri


def add_arrow_up(slide, cx, y1, y2, color=C_WHITE):
    add_line(slide, cx, y1 + Inches(0.08), cx, y2, color, 1.2)
    tri = slide.shapes.add_shape(
        MSO_SHAPE.ISOSCELES_TRIANGLE,
        cx - Inches(0.06), y1, Inches(0.12), Inches(0.12)
    )
    tri.fill.solid()
    tri.fill.fore_color.rgb = color
    tri.line.fill.background()


def add_arrow_right(slide, x1, y, x2, color=C_WHITE):
    add_line(slide, x1, y, x2 - Inches(0.08), y, color, 1.5)
    tri = slide.shapes.add_shape(
        MSO_SHAPE.ISOSCELES_TRIANGLE,
        x2 - Inches(0.12), y - Inches(0.06), Inches(0.12), Inches(0.12)
    )
    tri.rotation = 90
    tri.fill.solid()
    tri.fill.fore_color.rgb = color
    tri.line.fill.background()


def add_double_arrow_h(slide, x1, y1, x2, y2=None, color=C_WHITE):
    if y2 is None:
        y2 = y1
    add_line(slide, x1 + Inches(0.1), y1, x2 - Inches(0.1), y2, color, 1.5)
    for x, rot in ((x1, 270), (x2 - Inches(0.12), 90)):
        tri = slide.shapes.add_shape(
            MSO_SHAPE.ISOSCELES_TRIANGLE,
            x, y1 - Inches(0.06), Inches(0.12), Inches(0.12)
        )
        tri.rotation = rot
        tri.fill.solid()
        tri.fill.fore_color.rgb = color
        tri.line.fill.background()


def draw_mesh_icon(slide, cx, cy, w, h, color=C_GREY):
    """简化 3D 网格图标"""
    pts = [
        (cx - w * 0.35, cy + h * 0.2),
        (cx, cy - h * 0.35),
        (cx + w * 0.35, cy + h * 0.2),
        (cx, cy + h * 0.35),
    ]
    for i in range(4):
        x1, y1 = pts[i]
        x2, y2 = pts[(i + 1) % 4]
        add_line(slide, x1, y1, x2, y2, color, 1.0)
    add_line(slide, pts[0][0], pts[0][1], pts[2][0], pts[2][1], color, 0.8)
    add_line(slide, pts[1][0], pts[1][1], pts[3][0], pts[3][1], color, 0.8)
    for i in range(3):
        t = (i + 1) / 4
        mx = pts[0][0] + t * (pts[2][0] - pts[0][0])
        my = pts[0][1] + t * (pts[2][1] - pts[0][1])
        add_line(slide, mx, my, pts[1][0], pts[1][1], color, 0.6)


def draw_radar_icon(slide, cx, cy, size, color=C_GREY):
    base = slide.shapes.add_shape(
        MSO_SHAPE.OVAL, cx - size * 0.15, cy + size * 0.05,
        size * 0.3, size * 0.15
    )
    base.fill.solid()
    base.fill.fore_color.rgb = color
    base.line.fill.background()
    dish = slide.shapes.add_shape(
        MSO_SHAPE.OVAL, cx - size * 0.35, cy - size * 0.25,
        size * 0.7, size * 0.35
    )
    dish.fill.background()
    dish.line.color.rgb = color
    dish.line.width = Pt(1.2)
    for ang in (-25, 0, 25):
        rad = math.radians(ang - 90)
        add_line(
            slide, cx, cy,
            cx + math.cos(rad) * size * 0.55,
            cy + math.sin(rad) * size * 0.55,
            color, 0.8, MSO_LINE_DASH_STYLE.DASH
        )


def draw_field_icon(slide, cx, cy, w, h, color=C_GREY):
    rect = slide.shapes.add_shape(
        MSO_SHAPE.RECTANGLE, cx - w / 2, cy - h / 2, w, h
    )
    rect.fill.background()
    rect.line.color.rgb = color
    rect.line.width = Pt(1.0)
    for i in range(4):
        y = cy - h / 2 + h * (i + 1) / 5
        add_line(slide, cx - w / 2, y, cx + w / 2, y, color, 0.5)
    for i in range(4):
        x = cx - w / 2 + w * (i + 1) / 5
        add_line(slide, x, cy - h / 2, x, cy + h / 2, color, 0.5)
    peak = add_oval(slide, cx - Inches(0.08), cy - Inches(0.08), Inches(0.16), C_GREY_DK)


def draw_wave_icon(slide, cx, cy, w, color=C_GREY):
    for i, phase in enumerate((0, 0.4, 0.8)):
        pts = []
        steps = 20
        for s in range(steps + 1):
            t = s / steps
            x = cx - w / 2 + w * t
            y = cy + math.sin(t * math.pi * 2 + phase) * Inches(0.12)
            pts.append((x, y))
        for j in range(len(pts) - 1):
            add_line(slide, pts[j][0], pts[j][1], pts[j + 1][0], pts[j + 1][1],
                     color, 1.0 if i == 1 else 0.7)


def draw_beam_icon(slide, cx, cy, size, color=C_GREY):
    add_line(slide, cx - size * 0.3, cy, cx, cy, color, 1.2)
    for ang in (-35, -15, 15, 35):
        rad = math.radians(ang)
        add_line(slide, cx, cy, cx + math.cos(rad) * size * 0.45,
                 cy + math.sin(rad) * size * 0.45, color, 1.0)


def draw_boundary_icon(slide, cx, cy, size, color=C_GREY):
    add_line(slide, cx, cy - size * 0.35, cx, cy + size * 0.35, color, 1.2,
             MSO_LINE_DASH_STYLE.DASH)
    add_line(slide, cx - size * 0.35, cy, cx + size * 0.1, cy - size * 0.15, color, 1.2)
    tri = slide.shapes.add_shape(
        MSO_SHAPE.ISOSCELES_TRIANGLE,
        cx + size * 0.05, cy - size * 0.22, Inches(0.1), Inches(0.1)
    )
    tri.rotation = 120
    tri.fill.solid()
    tri.fill.fore_color.rgb = color
    tri.line.fill.background()


def draw_scale_icon(slide, cx, cy, w, color=C_GREY):
    add_line(slide, cx, cy + Inches(0.15), cx, cy - Inches(0.25), color, 1.5)
    add_line(slide, cx - w / 2, cy - Inches(0.25), cx + w / 2, cy - Inches(0.25), color, 1.5)
    for dx in (-w / 2, w / 2):
        pan = slide.shapes.add_shape(
            MSO_SHAPE.OVAL, cx + dx - Inches(0.12), cy - Inches(0.05),
            Inches(0.24), Inches(0.12)
        )
        pan.fill.background()
        pan.line.color.rgb = color
        pan.line.width = Pt(1.0)


def slide_title_bar(slide, title: str):
    add_label(slide, Inches(0.4), Inches(0.15), Inches(12.5), Inches(0.45),
              title, size=18, bold=True, color=C_GREY_DK, align=PP_ALIGN.LEFT)


# ---------- 各子元素 ----------

def make_top_box_1(slide):
    slide_title_bar(slide, "01 · 顶部算子框 — 连续方程算子")
    w, h = Inches(2.6), Inches(1.35)
    x, y = Inches(5.4), Inches(2.8)
    add_round_box(slide, x, y, w, h)
    add_label(slide, x, y + Inches(0.08), w, Inches(0.4),
              "连续方程算子", size=16, bold=True)
    add_label(slide, x, y + Inches(0.45), w, Inches(0.75),
              "∇·(ρv) + ∂ρ/∂t = 0", size=13, color=C_GREY_DK)
    add_arrow_down(slide, x + w / 2, y + h, y + h + Inches(0.55), dash=True)


def make_top_box_2(slide):
    slide_title_bar(slide, "02 · 顶部算子框 — 边界条件")
    w, h = Inches(2.6), Inches(1.35)
    x, y = Inches(5.4), Inches(2.8)
    add_round_box(slide, x, y, w, h)
    add_label(slide, x, y + Inches(0.08), w, Inches(0.4),
              "边界条件", size=16, bold=True)
    add_label(slide, x, y + Inches(0.45), w, Inches(0.75),
              "B(u) = g  或  n×E = 0", size=13, color=C_GREY_DK)
    add_arrow_down(slide, x + w / 2, y + h, y + h + Inches(0.55), dash=True)


def make_top_box_3(slide):
    slide_title_bar(slide, "03 · 顶部算子框 — 传播/散射算子")
    w, h = Inches(2.6), Inches(1.35)
    x, y = Inches(5.4), Inches(2.8)
    add_round_box(slide, x, y, w, h)
    add_label(slide, x, y + Inches(0.08), w, Inches(0.4),
              "传播/散射算子", size=16, bold=True)
    add_label(slide, x, y + Inches(0.45), w, Inches(0.75),
              "∇²E + k²E = 0", size=13, color=C_GREY_DK)
    add_arrow_down(slide, x + w / 2, y + h, y + h + Inches(0.55), dash=True)


def make_top_box_4(slide):
    slide_title_bar(slide, "04 · 顶部算子框 — 守恒约束")
    w, h = Inches(2.6), Inches(1.35)
    x, y = Inches(5.4), Inches(2.8)
    add_round_box(slide, x, y, w, h)
    add_label(slide, x, y + Inches(0.08), w, Inches(0.4),
              "守恒约束", size=16, bold=True)
    add_label(slide, x, y + Inches(0.45), w, Inches(0.75),
              "能量 / 动量 / 质量守恒", size=13, color=C_GREY_DK)
    add_arrow_down(slide, x + w / 2, y + h, y + h + Inches(0.55), dash=True)


def make_left_geometry(slide):
    slide_title_bar(slide, "05 · 左侧 — 几何网格 / 材料分布")
    w, h = Inches(1.55), Inches(3.2)
    x, y = Inches(5.0), Inches(2.0)
    add_round_box(slide, x, y, w, h)
    add_label(slide, x, y + Inches(0.08), w, Inches(0.35),
              "几何规则约束", size=11, bold=True)
    draw_mesh_icon(slide, x + w / 2, y + h * 0.45, w, h * 0.55)
    add_label(slide, x, y + h - Inches(0.55), w, Inches(0.45),
              "几何网格\n材料分布", size=12, bold=True)
    add_double_arrow_h(slide, x + w, y + h / 2, x + w + Inches(0.55), y + h / 2)


def make_neural_network(slide):
    slide_title_bar(slide, "06 · 中部 — 神经网络层（6层）")
    layer_colors = [C_BLUE, C_GREY_DK, C_TEAL, C_WHITE, C_GREY_DK, C_BLUE_LT]
    neuron_colors = [C_BLUE, C_GREY, C_TEAL, C_GREY_LT, C_GREY, C_BLUE_LT]
    n_layers = 6
    n_neurons = 7
    layer_w = Inches(0.55)
    gap = Inches(0.18)
    total_w = n_layers * layer_w + (n_layers - 1) * gap
    x0 = (SLIDE_W - total_w) / 2
    y0 = Inches(1.8)
    layer_h = Inches(3.6)
    neuron_r = Inches(0.11)
    neuron_gap = (layer_h - Inches(0.5)) / (n_neurons - 1)

    layer_centers = []
    neuron_positions = []

    for li in range(n_layers):
        lx = x0 + li * (layer_w + gap)
        add_round_box(slide, lx, y0, layer_w, layer_h,
                      fill=RGBColor(0xFF, 0xFF, 0xFF), line=layer_colors[li], line_w=1.5)
        cx = lx + layer_w / 2
        layer_centers.append(cx)
        positions = []
        ny = y0 + Inches(0.25)
        for ni in range(n_neurons):
            if ni == n_neurons // 2:
                add_label(slide, cx - Inches(0.08), ny - Inches(0.05),
                          Inches(0.16), Inches(0.2), "⋮", size=14, color=C_GREY_DK)
                ny += neuron_gap
                continue
            add_oval(slide, cx - neuron_r, ny - neuron_r, neuron_r * 2,
                     neuron_colors[li], line=layer_colors[li], line_w=0.8)
            positions.append((cx, ny))
            ny += neuron_gap
        neuron_positions.append(positions)

    random.seed(42)
    for li in range(n_layers - 1):
        for (x1, y1) in neuron_positions[li]:
            for (x2, y2) in neuron_positions[li + 1]:
                if random.random() < 0.55:
                    add_line(slide, x1, y1, x2, y2, C_LINE, 0.4)


def make_right_decoder(slide):
    slide_title_bar(slide, "07 · 右侧 — 物理解码器（算子输出）")
    w, h = Inches(1.55), Inches(3.2)
    x, y = Inches(6.8), Inches(2.0)
    add_round_box(slide, x, y, w, h)
    add_label(slide, x, y + Inches(0.06), w, Inches(0.55),
              "物理解码器\n(算子输出)", size=11, bold=True)
    draw_radar_icon(slide, x + w / 2, y + Inches(1.05), Inches(0.9))
    draw_field_icon(slide, x + w / 2, y + Inches(2.0), Inches(1.0), Inches(0.75))
    add_label(slide, x, y + h - Inches(0.45), w, Inches(0.35),
              "... 多源输出", size=11, color=C_GREY_DK)
    add_arrow_right(slide, x - Inches(0.55), y + h / 2, x)


def make_bottom_box_1(slide):
    slide_title_bar(slide, "08 · 底部算子 — 载荷频率算子")
    w, h = Inches(1.7), Inches(1.15)
    x, y = Inches(5.8), Inches(3.0)
    add_round_box(slide, x, y, w, h)
    draw_beam_icon(slide, x + w / 2, y + Inches(0.35), Inches(0.7))
    add_label(slide, x, y + Inches(0.55), w, Inches(0.5),
              "载荷频率\n算子", size=12, bold=True)
    add_arrow_up(slide, x + w / 2, y, y - Inches(0.5))


def make_bottom_box_2(slide):
    slide_title_bar(slide, "09 · 底部算子 — 边界条件")
    w, h = Inches(1.7), Inches(1.15)
    x, y = Inches(5.8), Inches(3.0)
    add_round_box(slide, x, y, w, h)
    draw_boundary_icon(slide, x + w / 2, y + Inches(0.35), Inches(0.75))
    add_label(slide, x, y + Inches(0.55), w, Inches(0.5),
              "边界条件", size=12, bold=True)
    add_arrow_up(slide, x + w / 2, y, y - Inches(0.5))


def make_bottom_box_3(slide):
    slide_title_bar(slide, "10 · 底部算子 — 传播/散射算子")
    w, h = Inches(1.7), Inches(1.15)
    x, y = Inches(5.8), Inches(3.0)
    add_round_box(slide, x, y, w, h)
    draw_wave_icon(slide, x + w / 2, y + Inches(0.35), Inches(0.85))
    add_label(slide, x, y + Inches(0.55), w, Inches(0.5),
              "传播/散射\n算子", size=12, bold=True)
    add_arrow_up(slide, x + w / 2, y, y - Inches(0.5))


def make_bottom_box_4(slide):
    slide_title_bar(slide, "11 · 底部算子 — 守恒约束")
    w, h = Inches(1.7), Inches(1.15)
    x, y = Inches(5.8), Inches(3.0)
    add_round_box(slide, x, y, w, h)
    draw_scale_icon(slide, x + w / 2, y + Inches(0.38), Inches(0.75))
    add_label(slide, x, y + Inches(0.55), w, Inches(0.5),
              "守恒约束", size=12, bold=True)
    add_arrow_up(slide, x + w / 2, y, y - Inches(0.5))


def make_footer_title(slide):
    slide_title_bar(slide, "12 · 底部标题条")
    w, h = Inches(8.5), Inches(0.75)
    x, y = Inches(2.4), Inches(3.2)
    box = add_round_box(slide, x, y, w, h, fill=C_WHITE, line=C_TEAL, line_w=2.0)
    add_label(slide, x, y, w, h,
              "物理模块 + 神经网络层 深度耦合", size=22, bold=True, color=C_TEAL)


def make_connection_lines(slide):
    slide_title_bar(slide, "13 · 连接线与反馈回路")
    y = Inches(3.5)
    add_line(slide, Inches(1.5), y, Inches(11.8), y, C_WHITE, 1.2,
             MSO_LINE_DASH_STYLE.DASH)
    add_arrow_down(slide, Inches(3.5), Inches(2.5), Inches(3.5), dash=False)
    add_arrow_down(slide, Inches(6.5), Inches(2.5), Inches(6.5), dash=False)
    add_arrow_down(slide, Inches(9.5), Inches(2.5), Inches(9.5), dash=False)
    add_label(slide, Inches(1.0), Inches(4.0), Inches(11.0), Inches(0.5),
              "（虚线底栏 + 各层虚线箭头，可按需复制到拼装页）",
              size=12, color=C_GREY_DK)


def make_full_assembly(slide):
    """完整拼装参考页（深色背景）"""
    fill = slide.background.fill
    fill.solid()
    fill.fore_color.rgb = C_BG

    add_label(slide, Inches(0.3), Inches(0.1), Inches(12.7), Inches(0.4),
              "完整拼装参考（所有元素可从此页复制，或从前页分别插入）",
              size=14, bold=True, color=C_WHITE, align=PP_ALIGN.LEFT)

    # 顶部四框
    top_y = Inches(0.55)
    top_specs = [
        ("连续方程算子", "∇·(ρv)+∂ρ/∂t=0"),
        ("边界条件", "B(u)=g"),
        ("传播/散射算子", "∇²E+k²E=0"),
        ("守恒约束", "能量/动量/质量守恒"),
    ]
    tw, th = Inches(2.35), Inches(0.95)
    tx0 = Inches(0.55)
    tg = Inches(0.28)
    top_xs = []
    for i, (title, formula) in enumerate(top_specs):
        tx = tx0 + i * (tw + tg)
        top_xs.append(tx + tw / 2)
        add_round_box(slide, tx, top_y, tw, th, fill=C_WHITE)
        add_label(slide, tx, top_y + Inches(0.04), tw, Inches(0.32),
                  title, size=10, bold=True, color=C_BLACK)
        add_label(slide, tx, top_y + Inches(0.36), tw, Inches(0.5),
                  formula, size=9, color=C_GREY_DK)

    # 神经网络区域
    nn_y0 = Inches(1.65)
    nn_h = Inches(2.55)
    layer_colors = [C_BLUE, C_GREY_DK, C_TEAL, C_WHITE, C_GREY_DK, C_BLUE_LT]
    neuron_colors = [C_BLUE, C_GREY, C_TEAL, C_GREY_LT, C_GREY, C_BLUE_LT]
    n_layers, n_neurons = 6, 5
    layer_w = Inches(0.42)
    gap = Inches(0.12)
    nn_x0 = Inches(3.05)
    neuron_positions = []
    layer_centers = []

    for li in range(n_layers):
        lx = nn_x0 + li * (layer_w + gap)
        add_round_box(slide, lx, nn_y0, layer_w, nn_h,
                      fill=RGBColor(0x18, 0x18, 0x18), line=layer_colors[li], line_w=1.2)
        cx = lx + layer_w / 2
        layer_centers.append(cx)
        positions = []
        ny = nn_y0 + Inches(0.18)
        ngap = (nn_h - Inches(0.36)) / (n_neurons - 1)
        nr = Inches(0.07)
        for ni in range(n_neurons):
            if ni == n_neurons // 2:
                add_label(slide, cx - Inches(0.06), ny - Inches(0.04),
                          Inches(0.12), Inches(0.15), "⋮", size=10, color=C_GREY)
                ny += ngap
                continue
            add_oval(slide, cx - nr, ny - nr, nr * 2, neuron_colors[li],
                     line=layer_colors[li], line_w=0.6)
            positions.append((cx, ny))
            ny += ngap
        neuron_positions.append(positions)

    random.seed(7)
    for li in range(n_layers - 1):
        for p1 in neuron_positions[li]:
            for p2 in neuron_positions[li + 1]:
                if random.random() < 0.5:
                    add_line(slide, p1[0], p1[1], p2[0], p2[1], C_LINE, 0.35)

    # 顶部虚线箭头
    for i, cx in enumerate([layer_centers[0], layer_centers[1],
                            layer_centers[3], layer_centers[5]]):
        add_arrow_down(slide, cx, top_y + th, nn_y0, color=C_WHITE, dash=True)

    # 左侧
    lw, lh = Inches(1.15), Inches(2.55)
    lx, ly = Inches(1.55), nn_y0
    add_round_box(slide, lx, ly, lw, lh, fill=RGBColor(0x18, 0x18, 0x18), line=C_WHITE)
    add_label(slide, lx, ly + Inches(0.04), lw, Inches(0.3),
              "几何规则约束", size=8, bold=True, color=C_WHITE)
    draw_mesh_icon(slide, lx + lw / 2, ly + lh * 0.45, lw, lh * 0.5, C_GREY)
    add_label(slide, lx, ly + lh - Inches(0.42), lw, Inches(0.38),
              "几何网格\n材料分布", size=9, bold=True, color=C_WHITE)
    add_double_arrow_h(slide, lx + lw, ly + lh / 2, nn_x0, ly + lh / 2)

    # 右侧
    rx = Inches(9.65)
    add_round_box(slide, rx, ly, lw, lh, fill=RGBColor(0x18, 0x18, 0x18), line=C_WHITE)
    add_label(slide, rx, ly + Inches(0.04), lw, Inches(0.42),
              "物理解码器\n(算子输出)", size=8, bold=True, color=C_WHITE)
    draw_radar_icon(slide, rx + lw / 2, ly + Inches(0.95), Inches(0.65), C_GREY)
    draw_field_icon(slide, rx + lw / 2, ly + Inches(1.75), Inches(0.7), Inches(0.5), C_GREY)
    add_label(slide, rx, ly + lh - Inches(0.32), lw, Inches(0.28),
              "... 多源输出", size=8, color=C_GREY)
    add_arrow_right(slide, layer_centers[-1] + layer_w / 2 + Inches(0.05),
                    ly + lh / 2, rx)

    # 底部四框
    bot_y = Inches(4.55)
    bw, bh = Inches(1.45), Inches(0.85)
    bot_specs = [
        ("载荷频率\n算子", draw_beam_icon),
        ("边界条件", draw_boundary_icon),
        ("传播/散射\n算子", draw_wave_icon),
        ("守恒约束", draw_scale_icon),
    ]
    bx0 = Inches(2.15)
    bg = Inches(0.22)
    bot_centers = []
    for i, (txt, icon_fn) in enumerate(bot_specs):
        bx = bx0 + i * (bw + bg)
        bot_centers.append(bx + bw / 2)
        add_round_box(slide, bx, bot_y, bw, bh, fill=C_WHITE)
        if icon_fn == draw_wave_icon:
            icon_fn(slide, bx + bw / 2, bot_y + Inches(0.28), Inches(0.65))
        elif icon_fn == draw_scale_icon:
            icon_fn(slide, bx + bw / 2, bot_y + Inches(0.3), Inches(0.6))
        else:
            icon_fn(slide, bx + bw / 2, bot_y + Inches(0.28), Inches(0.6))
        add_label(slide, bx, bot_y + Inches(0.42), bw, Inches(0.4),
                  txt, size=9, bold=True, color=C_BLACK)

    for cx, li in zip(bot_centers, [0, 1, 3, 5]):
        add_arrow_up(slide, cx, bot_y, nn_y0 + nn_h, color=C_WHITE)

    # 底栏虚线
    dash_y = Inches(5.55)
    add_line(slide, Inches(1.3), dash_y, Inches(12.0), dash_y, C_WHITE, 1.0,
             MSO_LINE_DASH_STYLE.DASH)

    # 底部标题
    fw, fh = Inches(7.5), Inches(0.6)
    fx = (SLIDE_W - fw) / 2
    fy = Inches(6.15)
    add_round_box(slide, fx, fy, fw, fh, fill=RGBColor(0x18, 0x18, 0x18),
                  line=C_TEAL, line_w=2.0)
    add_label(slide, fx, fy, fw, fh,
              "物理模块 + 神经网络层 深度耦合", size=18, bold=True, color=C_TEAL)


def make_usage_slide(slide):
    slide_title_bar(slide, "使用说明")
    items = [
        "本文件共 15 页：第 1–13 页为独立子元素，第 14 页为完整拼装参考，第 15 页为说明。",
        "所有中文、公式均为可编辑文本框，双击即可修改字体与内容。",
        "建议做法：打开对应页 → Ctrl+A 全选 → 复制 → 粘贴到目标 PPT。",
        "图标由 PPT 基本形状组合而成，可在「选择窗格」中单独调整。",
        "若需公式编辑器格式，可选中文本后 插入 → 公式 重新录入。",
        "神经网络连线为独立线条，可在选择窗格中批量删除或重画。",
    ]
    y = Inches(1.2)
    for item in items:
        add_label(slide, Inches(0.8), y, Inches(11.8), Inches(0.55),
                  "• " + item, size=14, color=C_GREY_DK, align=PP_ALIGN.LEFT)
        y += Inches(0.62)


def build():
    prs = new_prs()
    makers = [
        make_top_box_1,
        make_top_box_2,
        make_top_box_3,
        make_top_box_4,
        make_left_geometry,
        make_neural_network,
        make_right_decoder,
        make_bottom_box_1,
        make_bottom_box_2,
        make_bottom_box_3,
        make_bottom_box_4,
        make_footer_title,
        make_connection_lines,
    ]
    for fn in makers:
        slide = blank_slide(prs, dark=False)
        fn(slide)

    full = blank_slide(prs, dark=True)
    make_full_assembly(full)

    usage = blank_slide(prs, dark=False)
    make_usage_slide(usage)

    prs.save(str(OUT_PPTX))
    print(f"已生成: {OUT_PPTX}")
    print(f"共 {len(prs.slides)} 页幻灯片")


if __name__ == "__main__":
    build()
