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