@topmindspace/tms-skills 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/LICENSE +21 -0
  3. package/README.md +102 -0
  4. package/bin/tms-skills.js +160 -0
  5. package/package.json +48 -0
  6. package/top-ppt-html/README.md +254 -0
  7. package/top-ppt-html/SKILL.md +111 -0
  8. package/top-ppt-html/assets/examples/2026-09-09-architecture-graphite-dark.html +3926 -0
  9. package/top-ppt-html/assets/examples/2026-09-09-architecture-graphite-dark.model.json +168 -0
  10. package/top-ppt-html/assets/examples/2026-09-09-architecture-spectrum.html +3926 -0
  11. package/top-ppt-html/assets/examples/2026-09-09-architecture-spectrum.model.json +168 -0
  12. package/top-ppt-html/assets/examples/2026-09-09-presentation-apple-mono.html +4325 -0
  13. package/top-ppt-html/assets/examples/2026-09-09-presentation-apple-mono.model.json +321 -0
  14. package/top-ppt-html/assets/examples/2026-09-09-presentation-brand-red.html +4325 -0
  15. package/top-ppt-html/assets/examples/2026-09-09-presentation-brand-red.model.json +321 -0
  16. package/top-ppt-html/assets/examples/2026-09-09-presentation-business-blue.html +4325 -0
  17. package/top-ppt-html/assets/examples/2026-09-09-presentation-business-blue.model.json +321 -0
  18. package/top-ppt-html/assets/examples/2026-09-09-research-deep-teal.html +5527 -0
  19. package/top-ppt-html/assets/examples/2026-09-09-research-deep-teal.model.json +914 -0
  20. package/top-ppt-html/assets/examples/2026-09-09-research-indigo-violet.html +5527 -0
  21. package/top-ppt-html/assets/examples/2026-09-09-research-indigo-violet.model.json +914 -0
  22. package/top-ppt-html/assets/examples/2026-09-09-research-mckinsey.html +5527 -0
  23. package/top-ppt-html/assets/examples/2026-09-09-research-mckinsey.model.json +914 -0
  24. package/top-ppt-html/assets/examples/2026-09-09-research-warm-sand.html +5527 -0
  25. package/top-ppt-html/assets/examples/2026-09-09-research-warm-sand.model.json +914 -0
  26. package/top-ppt-html/assets/pptx-export.js +1944 -0
  27. package/top-ppt-html/assets/style-gallery.html +589 -0
  28. package/top-ppt-html/assets/templates/architecture.html +3728 -0
  29. package/top-ppt-html/assets/templates/engine.css +840 -0
  30. package/top-ppt-html/assets/templates/presentation.html +3738 -0
  31. package/top-ppt-html/assets/templates/research.html +4017 -0
  32. package/top-ppt-html/assets/templates/ui.js +520 -0
  33. package/top-ppt-html/assets/theme-overview-architecture.png +0 -0
  34. package/top-ppt-html/assets/theme-overview-presentation.png +0 -0
  35. package/top-ppt-html/assets/theme-overview-research.png +0 -0
  36. package/top-ppt-html/assets/theme-overview.png +0 -0
  37. package/top-ppt-html/evals/prompts.csv +15 -0
  38. package/top-ppt-html/evals/rubric.schema.json +25 -0
  39. package/top-ppt-html/evals/run_evals.py +220 -0
  40. package/top-ppt-html/evals/trace.example.json +16 -0
  41. package/top-ppt-html/package.json +35 -0
  42. package/top-ppt-html/references/charts-basic.md +624 -0
  43. package/top-ppt-html/references/charts-discipline.md +108 -0
  44. package/top-ppt-html/references/charts-extended.md +482 -0
  45. package/top-ppt-html/references/charts.md +28 -0
  46. package/top-ppt-html/references/components-atoms.md +624 -0
  47. package/top-ppt-html/references/components.md +30 -0
  48. package/top-ppt-html/references/content-rules.md +490 -0
  49. package/top-ppt-html/references/design-system-engine.md +235 -0
  50. package/top-ppt-html/references/design-system.md +478 -0
  51. package/top-ppt-html/references/failure-modes.md +214 -0
  52. package/top-ppt-html/references/high-fidelity.md +127 -0
  53. package/top-ppt-html/references/icons.md +397 -0
  54. package/top-ppt-html/references/industry-benchmark.md +105 -0
  55. package/top-ppt-html/references/infographics-stats.md +308 -0
  56. package/top-ppt-html/references/infographics-structure.md +226 -0
  57. package/top-ppt-html/references/infographics.md +43 -0
  58. package/top-ppt-html/references/layout-grammar.md +315 -0
  59. package/top-ppt-html/references/layouts-architecture.md +108 -0
  60. package/top-ppt-html/references/layouts-combo.md +600 -0
  61. package/top-ppt-html/references/layouts-research.md +160 -0
  62. package/top-ppt-html/references/modes.md +254 -0
  63. package/top-ppt-html/references/outline-design.md +275 -0
  64. package/top-ppt-html/references/playbook.md +266 -0
  65. package/top-ppt-html/references/pptx-export.md +209 -0
  66. package/top-ppt-html/references/reform-plan.md +252 -0
  67. package/top-ppt-html/references/styles.md +370 -0
  68. package/top-ppt-html/references/tech-design.md +138 -0
  69. package/top-ppt-html/scripts/audit_css.py +109 -0
  70. package/top-ppt-html/scripts/audit_docs.py +176 -0
  71. package/top-ppt-html/scripts/audit_skill.py +220 -0
  72. package/top-ppt-html/scripts/audit_styles.py +351 -0
  73. package/top-ppt-html/scripts/build_examples.py +2276 -0
  74. package/top-ppt-html/scripts/build_pptx.js +2380 -0
  75. package/top-ppt-html/scripts/capture_theme_overview.js +78 -0
  76. package/top-ppt-html/scripts/checks_html.py +127 -0
  77. package/top-ppt-html/scripts/cross_verify.py +294 -0
  78. package/top-ppt-html/scripts/env_probe.py +158 -0
  79. package/top-ppt-html/scripts/extract_model.py +210 -0
  80. package/top-ppt-html/scripts/extract_snippet.py +374 -0
  81. package/top-ppt-html/scripts/gen_channel_a.js +214 -0
  82. package/top-ppt-html/scripts/layout-constants.json +3377 -0
  83. package/top-ppt-html/scripts/layout_slots.json +830 -0
  84. package/top-ppt-html/scripts/lib_layout_regions.js +412 -0
  85. package/top-ppt-html/scripts/measure_height.py +178 -0
  86. package/top-ppt-html/scripts/model-schema.json +547 -0
  87. package/top-ppt-html/scripts/negative_tests.py +307 -0
  88. package/top-ppt-html/scripts/package_skill.py +291 -0
  89. package/top-ppt-html/scripts/prepare_images.py +341 -0
  90. package/top-ppt-html/scripts/probe_image_export.py +188 -0
  91. package/top-ppt-html/scripts/quality_gate.py +301 -0
  92. package/top-ppt-html/scripts/regression.py +308 -0
  93. package/top-ppt-html/scripts/render_compare.py +275 -0
  94. package/top-ppt-html/scripts/render_from_model.py +698 -0
  95. package/top-ppt-html/scripts/scaffold_report.py +1054 -0
  96. package/top-ppt-html/scripts/section-file-map.json +105 -0
  97. package/top-ppt-html/scripts/sync_runtime.py +662 -0
  98. package/top-ppt-html/scripts/validate_pptx.py +1510 -0
  99. package/top-ppt-html/scripts/validate_report.py +1456 -0
@@ -0,0 +1,1944 @@
1
+ /* ═══════════════════════════════════════════════════════════════════════════
2
+ * TopPPT HTML· 浏览器端 PPTX 预览运行时
3
+ * 零依赖、随报告单文件分发;OOXML 序列化器(仅供预览,页面不导出文件)。
4
+ *
5
+ * 定位:HTML 是阅读与演示界面,PPTX 是协作与修改格式——两者来自同一内容模型
6
+ * (window.REPORT_MODEL)。页面只提供「预览 PPTX」(WYSIWYG 模态,与智能体精导通道
7
+ * 同一序列化引擎)与可复制 AI 提示词;正式 PPTX 一律由本地智能体走精导通道
8
+ * (build_pptx.js)生成并过 strict 硬门禁,浏览器端导出达不到该质量标准。
9
+ *
10
+ * API:
11
+ * TopPptHtml.slidesXml(model) → string[](每页 slide XML,预览模态渲染用)
12
+ * TopPptHtml.validateModel(model) → {ok, missing[], warnings[], pages}(schema 单源驱动)
13
+ * TopPptHtml.slidesOf / slideXml / relsXml / NS / PRESETS
14
+ * → 供 scripts/gen_channel_a.js 在 Node 侧组装完整
15
+ * PPTX 包(回归双裁判内部质量路径,页面不使用)
16
+ *
17
+ * model = window.REPORT_MODEL(JSON,与页面正文同源):
18
+ * { mode, style, theme(light|dark:PPTX 按此选亮/暗 token,默认 light),
19
+ * title, subtitle, meta, agenda:[[num,标题,说明]…](architecture 可省),
20
+ * sections:[{type: points|metrics|kpi|table|timeline|steps|bar|donut|heatmap|bullet|pyramid|
21
+ * image|cards|split|comparison|quote|diagram|exhibit|twocol|threecol|halftable|
22
+ * matrix|lane, soWhat?, footnote?, flags?, image?, …}],
23
+ * closing:{title,points} }
24
+ * 通用可选字段:soWhat(结论条)/ footnote(来源行)/ flags(待核实清单 → accent 强调色条)
25
+ * / image(素材图片或配图占位,{src|items|placeholder, layout, fit, caption, alt, hint},
26
+ * src 仅 data: 或相对路径;layout ∈ full/half/bleed/grid/compare/wall,见 layout-constants.json imageSpec)。
27
+ * 页型字段定义(29 种)与 chart.type 白名单(30 类)的唯一事实源:scripts/model-schema.json。
28
+ *
29
+ * ★ 单源机制(scripts/sync_runtime.py 注入,标记块内禁止手改):
30
+ * - 版式常量:scripts/layout-constants.json → __TOPPPT_CONSTANTS__ 块;
31
+ * 通道 B(build_pptx.js)require 同一份 JSON,两通道产出一致。
32
+ * - DSL schema:scripts/model-schema.json → __TOPPPT_SCHEMA__ 块;
33
+ * scripts/extract_model.py 读同一份文件,双端校验同源。
34
+ * ★ 三模式独立排版比例尺(MTS)——research 咨询密排 / architecture 全幅图页型;
35
+ * modeSize() 把调用点的 presentation 基准 pt 映射到当前模式比例尺。
36
+ * ★ 本运行时只做「预览与回归裁判」:图表为形状近似渲染(交付通道 B 用原生数据图表,
37
+ * 可在 PowerPoint 中"编辑数据");文本与版式几何两通道同源,cross_verify 逐页比对。
38
+ * ═══════════════════════════════════════════════════════════════════════════ */
39
+ (function (g) {
40
+ 'use strict';
41
+ /* 双形态载荷判定:[[t,d]…] 数组 vs [{t,d,accent}…] 记录。typeof [] === 'object'。 */
42
+ function isRec(v) { return !!v && typeof v === 'object' && !Array.isArray(v); }
43
+
44
+ /* __TOPPPT_CONSTANTS_START__ */
45
+ /* ── 由 scripts/sync_runtime.py 从 scripts/layout-constants.json 注入 · 禁止手改 ── */
46
+ /* MTS = 三模式独立排版比例尺(按模式取 pt;基准 TS_BASE = presentation 档) */
47
+ /* PRESETS_DARK = 风格 dark token(REPORT_MODEL.theme="dark" 时导出深色版)*/
48
+ /* GRID = 12 列网格;TYPO = 语义字阶 C0–T14;CONT = 容器内边距;REG = 图表登记四元组;DEEP = 深度模式配置 */
49
+ /* IMG = 素材图片规格与配图占位约定(版式/裁切/建议尺寸/占位标签,双引擎同源) */
50
+ /* STYLE_DATA_COLORS(_DARK) = 9 套风格各自的编码色板 c1–c5(数据系列;结构色纪律不变) */
51
+ /* LAYOUT_SLOTS + regionOf = 页型布局区域 IR(与 scripts/lib_layout_regions.js 同源) */
52
+ var PW = 13.333, PH = 7.5, MX = 0.6, CW = PW - 2 * MX, EMU = 914400;
53
+ var TS_BASE = {"coverTitle":44,"coverSub":18,"h1":30,"h2":17,"lead":14,"body":13,"caption":11,"micro":10};
54
+ var MTS = {"presentation":{"coverTitle":44,"coverSub":18,"h1":30,"h2":17,"lead":14,"body":13,"caption":11,"micro":10},"research":{"coverTitle":36,"coverSub":14,"h1":22,"h2":13.5,"lead":11.5,"body":10.5,"caption":9,"micro":8.5},"architecture":{"coverTitle":44,"coverSub":19,"h1":30,"h2":17,"lead":14,"body":13,"caption":11,"micro":10}};
55
+ var PT = {"layout":{"contentTop":2.35,"contentBottom":6.9,"contentBottomWithNote":6.4,"pageNumY":7.0},"common":{"headEyebrowY":0.48,"headTitleY":0.82,"headRuleY":1.72,"leadY":1.88,"bodyY":2.35,"bodyYWithLead":2.55,"pagerY":7.0},"cover":{"metaY":0.7,"titleY":2.4,"titleH":2.0,"subtitleY":4.5,"subtitleH":0.8},"closing":{"eyebrowY":0.7,"titleY":1.2,"titleH":1.2,"pointsY":3.0,"pointTitleH":0.6,"pointBodyH":1.2},"points":{"listY":2.5,"rowH":0.72,"rowHMin":0.5,"markSize":0.14,"metricY":4.7,"metricW":2.6,"metricValH":0.9,"metricCapY":5.6},"metrics":{"bandY":3.1,"cardH":2.3,"valY":3.45,"valH":1.1,"capY":4.55,"capH":0.7,"gap":0.3,"maxW":2.9},"table":{"y":2.5,"rowH":0.55,"rowHMin":0.32,"headRowH":0.5},"bar":{"hbarY0":2.6,"hbarY1":6.25,"chartX":0.6,"chartY":2.95,"chartW":1.2,"chartH":3.35,"noteY":6.55},"note":{"y":6.55,"h":0.4},"exhibit":{"badgeY":2.35,"chartY":2.75,"chartH":3.25,"soWhatY":6.05,"footnoteY":6.72},"twocol":{"colGap":0.5,"bodyH":3.6,"rowHMin":0.5},"hbar":{"labelW":1.9,"valW":1.0,"rowH":0.55,"barRatio":0.5},"diagram":{"layerBarW":1.35,"layerGap":0.15,"nodeGap":0.15,"bodyStartY":2.9,"maxLayerH":1.15,"nodeMaxW":2.2},"split":{"leftW":0.55,"rightW":0.415,"rightX":0.585,"pointsY":2.7,"rowH":0.78,"capY":2.55,"chartY":3.0,"chartH":3.7,"tableY":2.7,"tableRowH":0.5},"cards":{"startY":3.0,"gap":0.25,"maxH":2.0,"titleH":0.4},"timeline":{"labelY":2.95,"axisY":3.58,"dotY":3.51,"dotR":0.18,"nameY":3.9,"descY":4.45,"descH":1.2},"research":{"col3Gap":0.38,"matrixLabelW":1.5,"matrixCellH":0.85,"matrixY":2.2,"denseTableY":2.2,"denseRowH":0.42,"halfTableW":0.52,"halfChartX":0.57,"halfChartW":0.43},"arch":{"fullStartY":2.1,"fullEndY":6.9,"laneH":0.72,"laneHMin":0.5,"laneGap":0.14,"laneHeadW":1.3,"stepGap":0.12,"stepMaxW":1.9},"kpi":{"heroY":2.9,"heroW":0.52,"heroH":1.6,"dividerX":0.56,"metricY0":2.95,"metricRowH":0.85},"comparison":{"panelGap":0.4,"panelY":2.55,"panelH":3.4,"titleH":0.45,"rowH":0.62},"quote":{"markY":1.75,"textY":2.7,"textH":2.3,"authorY":5.25,"contextY":5.8},"donut":{"centerX":3.05,"centerY":4.6,"r":1.6,"holeR":0.95,"legendX":5.6,"legendRowH":0.55,"legendLabelW":3.4,"legendValW":1.3,"legendPctX":5.3,"legendPctW":1.3},"steps":{"y":3.05,"barH":1.35,"gap":0.12,"arrowW":0.3,"numH":0.3,"titleH":0.34,"pad":0.08,"maxPerRow":6},"heatmap":{"y":2.6,"labelW":1.7,"headH":0.42,"cellH":0.6,"cellGap":0.06,"legendW":1.6},"pyramid":{"y":2.35,"rowH":0.72,"gap":0.08,"minW":0.34,"labelW":3.2},"bullet":{"y":2.6,"rowH":0.8,"labelW":2.4,"valW":1.7,"barH":0.26},"image":{"y":2.3,"h":4.2,"halfW":0.56,"halfGap":0.06,"capY":6.6,"capH":0.3,"radius":0.09,"noteRowH":0.62,"gridGap":0.16,"gridCols":{"2":2,"3":3,"4":4,"6":3},"gridCapH":0.28,"compareGap":0.3,"compareTagH":0.26,"wallCellH":0.92,"wallGap":0.18,"wallMaxRows":2},"flagbar":{"hdH":0.26,"rowH":0.24,"maxRows":3,"gap":0.12},"sankey":{"y":2.45,"h":4.1,"nodeW":0.32,"nodeGap":0.14,"labelW":1.55,"minBandH":0.22,"maxNodes":12,"maxFlows":24},"treemap":{"y":2.45,"h":4.1,"gap":0.05,"labelMinH":0.34,"maxLeaves":16,"maxDepth":3},"boxplot":{"y":2.7,"h":3.7,"labelW":1.5,"groupGap":0.3,"boxMaxW":0.9,"whiskerCapW":0.34,"maxGroups":8},"network":{"y":2.4,"h":4.2,"nodeR":0.28,"nodeRMin":0.18,"labelMaxW":1.4,"maxNodes":18,"maxEdges":30},"marimekko":{"y":2.6,"h":3.75,"labelW":1.55,"colGap":0.06,"legendH":0.42,"maxCols":6,"maxSegs":4},"streamgraph":{"y":2.6,"h":3.75,"labelW":1.4,"bandMinH":0.12,"maxSeries":6}};
56
+ var PRESETS = {
57
+ 'business-blue': { accent:'1A73E8', ink:'202124', body:'5F6368', faint:'80868B', bg:'FFFFFF', surface:'F8F9FA', line:'DADCE0', soft:'E8F0FE', onAccent:'FFFFFF', font:'Microsoft YaHei', fontDisplay:'Microsoft YaHei' },
58
+ 'apple-mono': { accent:'1D1D1F', ink:'1D1D1F', body:'6E6E73', faint:'86868B', bg:'FFFFFF', surface:'F5F5F7', line:'D2D2D7', soft:'F5F5F7', onAccent:'FFFFFF', font:'Microsoft YaHei', fontDisplay:'Microsoft YaHei' },
59
+ 'mckinsey': { accent:'003A70', ink:'101828', body:'475467', faint:'667085', bg:'FFFFFF', surface:'F2F4F7', line:'D0D5DD', soft:'EEF4FB', onAccent:'FFFFFF', font:'Microsoft YaHei', fontDisplay:'Microsoft YaHei' },
60
+ 'brand-red': { accent:'D0021B', ink:'1F1A1B', body:'5D5456', faint:'8A8083', bg:'FFFFFF', surface:'FAF7F7', line:'E2D9DA', soft:'FBECEC', onAccent:'FFFFFF', font:'Microsoft YaHei', fontDisplay:'Microsoft YaHei' },
61
+ 'warm-sand': { accent:'96681F', ink:'2A2521', body:'6B5F52', faint:'93836F', bg:'FBF9F6', surface:'F6F2EC', line:'E0D7C9', soft:'F3ECDD', onAccent:'FFFFFF', font:'Microsoft YaHei', fontDisplay:'Microsoft YaHei' },
62
+ 'deep-teal': { accent:'0F6B5C', ink:'14201E', body:'4A5A57', faint:'73837F', bg:'FFFFFF', surface:'F2F6F5', line:'CFDEDC', soft:'E4F1EE', onAccent:'FFFFFF', font:'Microsoft YaHei', fontDisplay:'Microsoft YaHei' },
63
+ 'graphite-dark': { accent:'0369A1', ink:'1A1D21', body:'4B5158', faint:'767C84', bg:'F4F5F7', surface:'ECEEF1', line:'CED1D6', soft:'E2EFF7', onAccent:'FFFFFF', font:'Microsoft YaHei', fontDisplay:'Microsoft YaHei' },
64
+ 'indigo-violet': { accent:'5B5BD6', ink:'1B1A2E', body:'52506B', faint:'7A779A', bg:'FFFFFF', surface:'F4F4FB', line:'D7D7E8', soft:'ECECF9', onAccent:'FFFFFF', font:'Microsoft YaHei', fontDisplay:'Microsoft YaHei' },
65
+ 'spectrum': { accent:'4563EF', ink:'171A21', body:'4D5566', faint:'7B8494', bg:'FBFCFE', surface:'F4F6FA', line:'D6DCE6', soft:'ECEEFF', onAccent:'FFFFFF', font:'Microsoft YaHei', fontDisplay:'Microsoft YaHei' }
66
+ };
67
+ var PRESETS_DARK = {
68
+ 'business-blue': { accent:'8AB4F8', ink:'E8EAED', body:'ADB4BD', faint:'868D96', bg:'0D0F13', surface:'191D23', line:'2F353D', soft:'16243A', onAccent:'0D1B2E', font:'Microsoft YaHei', fontDisplay:'Microsoft YaHei' },
69
+ 'apple-mono': { accent:'F5F5F7', ink:'F5F5F7', body:'A1A1A6', faint:'86868B', bg:'000000', surface:'161617', line:'38383D', soft:'1D1D1F', onAccent:'000000', font:'Microsoft YaHei', fontDisplay:'Microsoft YaHei' },
70
+ 'mckinsey': { accent:'7FB2E5', ink:'E4E7EC', body:'AAB3C2', faint:'8A94A6', bg:'0C1116', surface:'12181F', line:'2A3342', soft:'12233A', onAccent:'0C1A2E', font:'Microsoft YaHei', fontDisplay:'Microsoft YaHei' },
71
+ 'brand-red': { accent:'FF5A66', ink:'F3ECEC', body:'C4B6B7', faint:'9A8D8E', bg:'130D0E', surface:'1A1213', line:'37272A', soft:'331518', onAccent:'2A0D10', font:'Microsoft YaHei', fontDisplay:'Microsoft YaHei' },
72
+ 'warm-sand': { accent:'D9B36A', ink:'ECE5DB', body:'C4B8A8', faint:'9B8D7A', bg:'15120E', surface:'1D1913', line:'3A3128', soft:'2E2417', onAccent:'2A1F10', font:'Microsoft YaHei', fontDisplay:'Microsoft YaHei' },
73
+ 'deep-teal': { accent:'4FC3B0', ink:'E2ECEA', body:'B3C2BF', faint:'86948F', bg:'0D1312', surface:'131B1A', line:'293834', soft:'12312C', onAccent:'0C2420', font:'Microsoft YaHei', fontDisplay:'Microsoft YaHei' },
74
+ 'graphite-dark': { accent:'4DA3FF', ink:'EEF2F6', body:'B9C2CD', faint:'85909D', bg:'0B0E12', surface:'171D24', line:'2B3540', soft:'132841', onAccent:'07182B', font:'Microsoft YaHei', fontDisplay:'Microsoft YaHei' },
75
+ 'indigo-violet': { accent:'A5A4F0', ink:'E6E5F5', body:'B6B4D4', faint:'8B89B0', bg:'100F1C', surface:'171629', line:'322F52', soft:'232146', onAccent:'171631', font:'Microsoft YaHei', fontDisplay:'Microsoft YaHei' },
76
+ 'spectrum': { accent:'8BA0F8', ink:'E8EBF2', body:'A7AFBF', faint:'7E8798', bg:'0E1116', surface:'171C26', line:'2B3342', soft:'1B2242', onAccent:'141A38', font:'Microsoft YaHei', fontDisplay:'Microsoft YaHei' }
77
+ };
78
+ var STYLE_DATA_COLORS = {"business-blue":["1a73e8","3526bb","21a199","b77148","99983d"],"apple-mono":["1d1d1f","0071e3","0f7b6c","c2410c","7c3aed"],"mckinsey":["003a70","2826bb","21a18e","b76848","99903d"],"brand-red":["d0021b","bb6326","d634ae","40a182","438aa8"],"warm-sand":["96681f","839324","cf3b3e","4d88b2","4a47a4"],"deep-teal":["0f6b5c","2683bb","22aa3e","b74870","a85c43"],"graphite-dark":["0369a1","2637bb","1f9977","b75948","a18a40"],"indigo-violet":["5b5bd6","7432ae","4197c8","a08f4b","7ca04b"],"spectrum":["4563ef","5b26bb","269ebb","af8746","83993d"]};
79
+ var STYLE_DATA_COLORS_DARK = {"business-blue":["8ab4f8","7966e0","88e7e5","d4ad91","cbce83"],"apple-mono":["f5f5f7","2997ff","2dd4bf","ff9f0a","bf5af2"],"mckinsey":["7fb2e5","6f6bdb","8ce3d8","d1a694","cbc586"],"brand-red":["ff5a66","e09e66","e788cb","91d4c2","83b4ce"],"warm-sand":["d9b36a","c2d571","df9391","97b5ce","8e89c7"],"deep-teal":["4fc3b0","7aaecd","97d8a4","c99cac","c29c8f"],"graphite-dark":["4da3ff","6c66e0","88e7dc","d4a691","cec983"],"indigo-violet":["a5a4f0","a967e0","89c3e7","d3c792","aece83"],"spectrum":["8ba0f8","9066e0","88d6e7","d4ba91","bdce83"]};
80
+ var GRID = {"columns":12,"gutter":0.16,"colW":0.8644,"x":[0.6,1.6244,2.6488,3.6732,4.6976,5.722,6.7464,7.7708,8.7952,9.8196,10.844,11.8684,12.733],"y":[2.35,3.27,4.19,5.11,6.03,6.9],"safe":{"top":0.4,"bottom":0.4,"left":0.6,"right":0.6}};
81
+ var TYPO = {"levels":[{"id":"C0","name":"封面/章节幕标题","role":"coverTitle","usage":"封面主标题、章节幕标题"},{"id":"T1","name":"页码/章节徽章","role":"caption","usage":"页码徽章、章节编号"},{"id":"T2","name":"页面主标题/结论标题","role":"h1","usage":"每页顶部结论句(research 行动标题)"},{"id":"T3","name":"副标题/语境说明","role":"lead","usage":"主标题下方导语"},{"id":"T4","name":"模块标题/图表标题","role":"h2","usage":"卡片标题、图表标题、面板标题"},{"id":"T5","name":"证据编号/轻量标签","role":"micro","usage":"Exhibit 编号、状态标签"},{"id":"T6","name":"证据块标题/小节标题","role":"h2","usage":"分栏小标题、证据块标题"},{"id":"T7","name":"正文解释段落","role":"body","usage":"证据解释、管理解读正文、表格正文"},{"id":"T8","name":"结论条文字","role":"body","usage":"so-what 结论条、核心结论框"},{"id":"T9","name":"SO WHAT 标签","role":"caption","usage":"so-what 标签行"},{"id":"T10","name":"SO WHAT 正文/业务含义","role":"body","usage":"so-what 正文、行动含义、表格行动项"},{"id":"T11","name":"图表轴/图例/刻度/微标签","role":"micro","usage":"坐标轴、图例、刻度、单位(禁用于表格正文与完整短句)"},{"id":"T12","name":"图表数据标签","role":"caption","usage":"折线点值、柱形标签、百分比标注"},{"id":"T13","name":"关键 KPI 大数字","role":"h1","usage":"KPI 大数字、hero 数值"},{"id":"T14","name":"注释/口径/来源/页脚","role":"micro","usage":"footnote、来源行、页脚、小页码"}],"tableSemanticRule":{"body":["T7","T10"],"microAllowed":["T11"],"microForbiddenFor":["表格正文","行动项","风险项","解释句","建议句","长项目符号","完整短句"]}};
82
+ var CONT = {"pad":{"card":0.18,"panel":0.2,"cell":0.08,"band":0.16,"soWhat":0.16,"kpi":0.18,"chart":0.1,"head":0.05,"footer":0.05,"node":0.06},"minPad":0.06,"textMetrics":{"lineFactor":1.45,"emAsciiRatio":0.52},"overflowRule":"溢出处置优先级:① 优化内容(列表化/精炼/合并/拆页/换组合版式)→ ② 调整容器尺寸与行宽 → ③ 有限度缩小字号(见 fontShrink)→ ④ 仍装不下则拆页。禁止无限缩字号硬塞,也禁止把「精简」当第一手段砍掉决策必需信息。","fontShrink":{"ladder":[15,14,13.5,13,12.5,12,11.5,11,10.5,10,9.5,9,8.5],"maxShrinkSteps":4,"floorPt":{"presentation":10,"research":9,"architecture":10},"preferContentFirst":true,"warnBelow":9.5},"continuousTextRule":"语义连续的一句话 / so-what 主句 / 结论句 / 表格单元格正文不得拆成多个独立文本框(避免异常空格、断句、基线漂移);局部加粗高亮必须用同一文本框内富文本。"};
83
+ var REG = {"bar":{"html":"svg-bars","pptx":"native","nativeType":"bar","dataTable":"notes","min":{"vbHeightMin":160}},"hbar":{"html":"svg-hbars","pptx":"native","nativeType":"bar","dataTable":"notes","min":{"vbHeightMin":100}},"stack":{"html":"svg-stack","pptx":"native","nativeType":"bar","dataTable":"notes","min":{"vbHeightMin":160},"nativeOpt":{"barGrouping":"stacked"}},"stackline":{"html":"svg-stackline","pptx":"native","nativeType":"bar","dataTable":"notes","min":{"vbHeightMin":40},"nativeOpt":{"barGrouping":"percentStacked"}},"line":{"html":"svg-line","pptx":"native","nativeType":"line","dataTable":"notes","min":{"vbHeightMin":160}},"dualline":{"html":"svg-dualline","pptx":"native","nativeType":"line","dataTable":"notes","min":{"vbHeightMin":160}},"area":{"html":"svg-area","pptx":"native","nativeType":"area","dataTable":"notes","min":{"vbHeightMin":160}},"donut":{"html":"svg-donut","pptx":"native","nativeType":"doughnut","dataTable":"notes","min":{"pxWidthMin":140}},"multidonut":{"html":"svg-mdonut","pptx":"native","nativeType":"doughnut","dataTable":"notes","min":{"pxWidthMin":140}},"pie":{"html":"svg-pie","pptx":"native","nativeType":"pie","dataTable":"notes","min":{"pxWidthMin":140}},"radar":{"html":"svg-radar","pptx":"native","nativeType":"radar","dataTable":"notes","min":{"pxWidthMin":220}},"scatter":{"html":"svg-scatter","pptx":"native","nativeType":"scatter","dataTable":"notes","min":{"vbHeightMin":180}},"bubble":{"html":"svg-bubble","pptx":"native","nativeType":"bubble","dataTable":"notes","min":{"vbHeightMin":180}},"waterfall":{"html":"svg-wf","pptx":"native","nativeType":"bar","dataTable":"notes","min":{"vbHeightMin":170},"nativeTrick":"stacked+hiddenBase+connector"},"gauge":{"html":"svg-gauge","pptx":"native","nativeType":"doughnut","dataTable":"notes","min":{"pxWidthMin":180},"nativeTrick":"doughnut+firstSliceAng+hiddenRemainder"},"pareto":{"html":"svg-pareto","pptx":"native","nativeType":"bar","dataTable":"notes","min":{"vbHeightMin":180},"nativeTrick":"multiType+bar+line"},"gantt":{"html":"svg-gantt","pptx":"shape","dataTable":"notes","min":{"vbHeightMin":130},"path":{"forbidPresetShapes":true}},"vsbar":{"html":"svg-vsbar","pptx":"shape","dataTable":"notes","min":{"vbHeightMin":100},"path":{"forbidPresetShapes":true}},"progress":{"html":"svg-progress","pptx":"shape","dataTable":"notes","min":{"vbHeightMin":100},"path":{"forbidPresetShapes":true}},"sparkline":{"html":"svg-spark","pptx":"shape","dataTable":"off","min":{"vbHeightMin":20},"path":{"forbidPresetShapes":true}},"funnel":{"html":"svg-funnel","pptx":"shape","dataTable":"notes","min":{"vbHeightMin":160},"path":{"minSamplePoints":8,"forbidPresetShapes":true}},"treemap":{"html":"svg-treemap","pptx":"shape","dataTable":"appendix","min":{"vbHeightMin":170},"path":{"forbidPresetShapes":true}},"marimekko":{"html":"svg-marimekko","pptx":"shape","dataTable":"appendix","min":{"vbHeightMin":160},"path":{"forbidPresetShapes":true}},"boxplot":{"html":"svg-boxplot","pptx":"shape","dataTable":"appendix","min":{"vbHeightMin":180},"path":{"forbidPresetShapes":true}},"network":{"html":"svg-network","pptx":"shape","dataTable":"appendix","min":{"vbHeightMin":200},"path":{"forbidPresetShapes":true}},"sankey":{"html":"svg-sankey","pptx":"shape","dataTable":"appendix","min":{"vbHeightMin":200},"path":{"minSamplePoints":16,"trackBothEdges":true,"forbidPresetShapes":true}},"streamgraph":{"html":"svg-stream","pptx":"shape","dataTable":"appendix","min":{"vbHeightMin":160},"path":{"minSamplePoints":24,"trackBothEdges":true,"forbidPresetShapes":true}},"slope":{"html":"svg-slope","pptx":"shape","dataTable":"notes","min":{"vbHeightMin":180},"path":{"forbidPresetShapes":true}},"dumbbell":{"html":"svg-dumbbell","pptx":"shape","dataTable":"notes","min":{"vbHeightMin":160},"path":{"forbidPresetShapes":true}},"lollipop":{"html":"svg-lollipop","pptx":"shape","dataTable":"notes","min":{"vbHeightMin":140},"path":{"forbidPresetShapes":true}},"dotplot":{"html":"svg-dotplot","pptx":"shape","dataTable":"notes","min":{"vbHeightMin":120},"path":{"forbidPresetShapes":true}},"bulletchart":{"html":"svg-bullet","pptx":"shape","dataTable":"notes","min":{"vbHeightMin":140},"path":{"forbidPresetShapes":true}},"waffle":{"html":"svg-waffle","pptx":"shape","dataTable":"notes","min":{"vbHeightMin":160},"path":{"forbidPresetShapes":true}},"radialbar":{"html":"svg-radialbar","pptx":"shape","dataTable":"notes","min":{"pxWidthMin":200},"path":{"minSamplePoints":16,"forbidPresetShapes":true}},"rose":{"html":"svg-rose","pptx":"shape","dataTable":"notes","min":{"pxWidthMin":180},"path":{"minSamplePoints":16,"forbidPresetShapes":true}},"candlestick":{"html":"svg-candle","pptx":"shape","dataTable":"appendix","min":{"vbHeightMin":180},"path":{"forbidPresetShapes":true}}};
84
+ var DEEP = {"triggers":["高保真","1:1","一比一","精确还原","按图还原","正式交付","不能偏移","像素级","严格对照","逐页验收"],"complexCharts":["sankey","streamgraph","treemap","marimekko","boxplot","network","rose","radialbar","candlestick","funnel"],"checks":{"anchors":true,"containerOverflow":true,"renderCompare":true,"manifest":true},"renderCompare":{"engine":"soffice","fallback":"powerpoint-com","skipIfMissing":true,"dpi":96}};
85
+ var IMG = {"layouts":["full","half","bleed","grid","compare","wall"],"multiLayouts":["grid","compare","wall"],"fitEnum":["cover","contain"],"fitDefault":"cover","formats":["png","jpeg","jpg","webp","svg"],"ratioDefault":{"full":"3:1","half":"4:3","bleed":"21:9","grid":"4:3","compare":"4:3","wall":"1:1"},"ratioCssClass":{"full":"media--r3-1","half":"media--r4-3","bleed":"media--r21-9","grid":"media--r4-3","compare":"media--r4-3","wall":"media--r1-1"},"recommendedSizePx":{"full":"2400×800","half":"1400×1050","bleed":"2560×1100","grid":"900×675","compare":"1400×1050","wall":"480×480"},"maxInlineBytes":1572864,"maxTotalInlineBytes":8388608,"maxPerPage":6,"placeholderLabel":"配图占位","placeholderHintPrefix":"建议 ","placeholderHintSuffix":"px","zeroExternal":true,"qualityRule":"位图优先提供 ≥2× 显示尺寸;截图用 16:9 或 4:3 锁定比例;数据图不要用截图(应用内联 SVG 图表,可随主题变色)。"};
86
+ var LAYOUT_SLOTS = {"version":"0.1","pageTypes":{"points":{"mode":["presentation","research","architecture"],"slots":[{"id":"head","role":"chrome","required":true,"html":".shead","pptx":"head()"},{"id":"primary","role":"primary","required":true,"html":".grid.g-2 / .card / .ul","pptx":"points columns","minUnits":1},{"id":"annotation","role":"annotation","required":false,"html":".sowhat / .note","pptx":"soWhatBar / footnoteLine"}]},"metrics":{"mode":["presentation","research"],"slots":[{"id":"head","role":"chrome","required":true,"html":".shead","pptx":"head()"},{"id":"primary","role":"primary","required":true,"html":".metric","pptx":"metric row","minUnits":4},{"id":"annotation","role":"annotation","required":false,"html":"口径行 / .sowhat","pptx":"footnoteLine / soWhatBar"}]},"kpi":{"mode":["presentation","research"],"slots":[{"id":"head","role":"chrome","required":true,"html":".shead","pptx":"head()"},{"id":"hero","role":"primary","required":true,"html":".t-metric hero","pptx":"hero number"},{"id":"metrics","role":"secondary","required":false,"html":".metric","pptx":"support metrics"},{"id":"annotation","role":"annotation","required":false,"html":".sowhat / .note","pptx":"soWhatBar"}]},"exhibit":{"mode":["research","presentation"],"slots":[{"id":"head","role":"chrome","required":true,"html":".shead / .exhibit__hd","pptx":"head()"},{"id":"primary","role":"primary","required":true,"html":"svg[data-chart] / .exhibit","pptx":"nativeChart | shapeChart","channel":"registry"},{"id":"secondary","role":"secondary","required":false,"html":"要点列表","pptx":"side bullets"},{"id":"annotation","role":"annotation","required":true,"html":".exhibit__src / .sowhat","pptx":"footnoteLine / soWhatBar / notes"}]},"split":{"mode":["presentation","research","architecture"],"slots":[{"id":"head","role":"chrome","required":true,"html":".shead","pptx":"head()"},{"id":"left","role":"primary","required":true,"html":"左区(points/table/image/chart)","pptx":"left zone"},{"id":"right","role":"secondary","required":true,"html":"右区(points/table/image/chart)","pptx":"right zone"},{"id":"annotation","role":"annotation","required":false,"html":".sowhat / .note","pptx":"soWhatBar"}]},"table":{"mode":["presentation","research"],"slots":[{"id":"head","role":"chrome","required":true,"html":".shead","pptx":"head()"},{"id":"primary","role":"primary","required":true,"html":".tbl-wrap table","pptx":"addTable","maxRows":{"presentation":8,"research":16}},{"id":"annotation","role":"annotation","required":false,"html":".sowhat / .note","pptx":"soWhatBar / footnoteLine"}]},"bar":{"mode":["presentation","research","architecture"],"slots":[{"id":"head","role":"chrome","required":true,"html":".shead","pptx":"head()"},{"id":"primary","role":"primary","required":true,"html":"svg[data-chart]","pptx":"nativeChart | shapeChart","channel":"registry"},{"id":"annotation","role":"annotation","required":true,"html":"note / dataTable","pptx":"footnoteLine / notes / inline table"}]},"donut":{"mode":["presentation","research"],"slots":[{"id":"head","role":"chrome","required":true,"html":".shead","pptx":"head()"},{"id":"primary","role":"primary","required":true,"html":"svg[data-chart=donut]","pptx":"addChart doughnut"},{"id":"annotation","role":"annotation","required":false,"html":"中心合计 / 图例 / .note","pptx":"center label / legend / notes"}]},"twocol":{"mode":["research"],"slots":[{"id":"head","role":"chrome","required":true,"html":".shead","pptx":"head()"},{"id":"primary","role":"primary","required":true,"html":".cols-2","pptx":"two text columns"},{"id":"annotation","role":"annotation","required":false,"html":".sowhat","pptx":"soWhatBar"}]},"threecol":{"mode":["research"],"slots":[{"id":"head","role":"chrome","required":true,"html":".shead","pptx":"head()"},{"id":"primary","role":"primary","required":true,"html":".cols-3","pptx":"three text columns"},{"id":"annotation","role":"annotation","required":false,"html":".sowhat","pptx":"soWhatBar"}]},"halftable":{"mode":["research"],"slots":[{"id":"head","role":"chrome","required":true,"html":".shead","pptx":"head()"},{"id":"primary","role":"primary","required":true,"html":"左表","pptx":"addTable left"},{"id":"secondary","role":"secondary","required":true,"html":"右图","pptx":"chart right"},{"id":"annotation","role":"annotation","required":false,"html":".sowhat","pptx":"soWhatBar"}]},"matrix":{"mode":["research"],"slots":[{"id":"head","role":"chrome","required":true,"html":".shead","pptx":"head()"},{"id":"primary","role":"primary","required":true,"html":".matrix","pptx":"matrix cells"},{"id":"annotation","role":"annotation","required":false,"html":".sowhat","pptx":"soWhatBar"}]},"comparison":{"mode":["presentation","research"],"slots":[{"id":"head","role":"chrome","required":true,"html":".shead","pptx":"head()"},{"id":"left","role":"primary","required":true,"html":"左面板","pptx":"left panel"},{"id":"right","role":"secondary","required":true,"html":"右面板","pptx":"right panel"},{"id":"annotation","role":"annotation","required":false,"html":"verdict / .sowhat","pptx":"verdict bar"}]},"quote":{"mode":["presentation","research"],"slots":[{"id":"head","role":"chrome","required":true,"html":".shead","pptx":"head()"},{"id":"primary","role":"primary","required":true,"html":"金句正文","pptx":"quote text"}]},"timeline":{"mode":["presentation","research"],"slots":[{"id":"head","role":"chrome","required":true,"html":".shead","pptx":"head()"},{"id":"primary","role":"primary","required":true,"html":".tl","pptx":"timeline axis"}]},"steps":{"mode":["presentation","architecture"],"slots":[{"id":"head","role":"chrome","required":true,"html":".shead","pptx":"head()"},{"id":"primary","role":"primary","required":true,"html":".steps","pptx":"step row"}]},"cards":{"mode":["presentation","research"],"slots":[{"id":"head","role":"chrome","required":true,"html":".shead","pptx":"head()"},{"id":"primary","role":"primary","required":true,"html":".card grid","pptx":"card grid"}]},"heatmap":{"mode":["presentation","research"],"slots":[{"id":"head","role":"chrome","required":true,"html":".shead","pptx":"head()"},{"id":"primary","role":"primary","required":true,"html":".heat","pptx":"heat cells"}]},"bullet":{"mode":["presentation","research"],"slots":[{"id":"head","role":"chrome","required":true,"html":".shead","pptx":"head()"},{"id":"primary","role":"primary","required":true,"html":".bul","pptx":"bullet rows"}]},"pyramid":{"mode":["presentation","research"],"slots":[{"id":"head","role":"chrome","required":true,"html":".shead","pptx":"head()"},{"id":"primary","role":"primary","required":true,"html":".pyr","pptx":"pyramid layers"}]},"image":{"mode":["presentation","research","architecture"],"slots":[{"id":"head","role":"chrome","required":true,"html":".shead","pptx":"head()"},{"id":"primary","role":"primary","required":true,"html":".media*","pptx":"imageLayoutShapes"}]},"diagram":{"mode":["architecture","research"],"slots":[{"id":"head","role":"chrome","required":true,"html":".shead","pptx":"head()"},{"id":"primary","role":"primary","required":true,"html":".arch / layers","pptx":"layered nodes + edges","maxNodes":24,"maxLayers":4},{"id":"annotation","role":"annotation","required":false,"html":".arch__legend / .note","pptx":"legend chips / notes"}]},"lane":{"mode":["architecture","research"],"slots":[{"id":"head","role":"chrome","required":true,"html":".shead","pptx":"head()"},{"id":"primary","role":"primary","required":true,"html":".lane","pptx":"lanes + steps"}]},"sankey":{"mode":["research","architecture"],"slots":[{"id":"head","role":"chrome","required":true,"html":".shead","pptx":"head()"},{"id":"primary","role":"primary","required":true,"html":"svg[data-chart=sankey]","pptx":"infoSankey","maxNodes":12},{"id":"annotation","role":"annotation","required":true,"html":"dataTable / .note","pptx":"notes"}]},"treemap":{"mode":["research","architecture"],"slots":[{"id":"head","role":"chrome","required":true,"html":".shead","pptx":"head()"},{"id":"primary","role":"primary","required":true,"html":"svg[data-chart=treemap]","pptx":"infoTreemap"},{"id":"annotation","role":"annotation","required":true,"html":"dataTable / .note","pptx":"notes"}]},"boxplot":{"mode":["research","architecture"],"slots":[{"id":"head","role":"chrome","required":true,"html":".shead","pptx":"head()"},{"id":"primary","role":"primary","required":true,"html":"svg[data-chart=boxplot]","pptx":"infoBoxplot"},{"id":"annotation","role":"annotation","required":true,"html":"dataTable / .note","pptx":"notes"}]},"network":{"mode":["research","architecture"],"slots":[{"id":"head","role":"chrome","required":true,"html":".shead","pptx":"head()"},{"id":"primary","role":"primary","required":true,"html":"svg[data-chart=network]","pptx":"infoNetwork"},{"id":"annotation","role":"annotation","required":true,"html":"dataTable / .note","pptx":"notes"}]},"marimekko":{"mode":["research","architecture"],"slots":[{"id":"head","role":"chrome","required":true,"html":".shead","pptx":"head()"},{"id":"primary","role":"primary","required":true,"html":"svg[data-chart=marimekko]","pptx":"infoMarimekko"},{"id":"annotation","role":"annotation","required":true,"html":"dataTable / .note","pptx":"notes"}]},"streamgraph":{"mode":["research","architecture"],"slots":[{"id":"head","role":"chrome","required":true,"html":".shead","pptx":"head()"},{"id":"primary","role":"primary","required":true,"html":"svg[data-chart=streamgraph]","pptx":"infoStreamgraph"},{"id":"annotation","role":"annotation","required":true,"html":"dataTable / .note","pptx":"notes"}]}}};
87
+
88
+ /* regionOf(pageType, slotId, opts) — 页型布局区域 IR(与 scripts/lib_layout_regions.js 同源) */
89
+ function regionOf(pageType, slotId, opts) {
90
+ var type = String(pageType || 'points'), slot = String(slotId || '');
91
+ opts = opts || {};
92
+ var L = PT.layout || {}, C = PT.common || {};
93
+ var topDefault = C.bodyY || 2.5;
94
+ var bottomDefault = opts.withNote ? (L.contentBottomWithNote || 6.4) : (L.contentBottom || 6.9);
95
+ if (slot === 'head' || slot === 'chrome') {
96
+ return { x: MX, y: C.headEyebrowY || 0.55, w: CW,
97
+ h: Math.max(0.4, (C.headRuleY || 1.95) - (C.headEyebrowY || 0.55)),
98
+ titleY: C.headTitleY, ruleY: C.headRuleY, leadY: C.leadY, bodyY: C.bodyY };
99
+ }
100
+ if (slot === 'annotation' || slot === 'soWhat') {
101
+ var sy = (PT.exhibit && PT.exhibit.soWhatY != null) ? PT.exhibit.soWhatY : 6.05;
102
+ return { x: MX, y: sy, w: CW, h: 0.62 };
103
+ }
104
+ if (slot === 'footnote') {
105
+ var fy = (PT.exhibit && PT.exhibit.footnoteY != null) ? PT.exhibit.footnoteY : ((PT.note && PT.note.y) || 6.55);
106
+ return { x: MX, y: fy, w: CW - 1.2, h: (PT.note && PT.note.h) || 0.32 };
107
+ }
108
+ var P = PT[type] || {};
109
+ if (type === 'exhibit' && slot === 'primary') {
110
+ var ey = P.chartY || 2.95;
111
+ return { x: MX, y: ey, w: CW, h: Math.max(1.2, (L.contentBottomWithNote || 6.4) - ey), badgeY: P.badgeY };
112
+ }
113
+ if (type === 'metrics' && slot === 'primary') {
114
+ return { x: MX, y: P.bandY || 3.1, w: CW, h: P.cardH || 2.3, gap: P.gap, maxW: P.maxW,
115
+ valY: P.valY, valH: P.valH, capY: P.capY, capH: P.capH };
116
+ }
117
+ if (type === 'table' && slot === 'primary') {
118
+ var ty = P.y || topDefault;
119
+ return { x: MX, y: ty, w: CW, h: Math.max(1, (opts.bottom != null ? opts.bottom : bottomDefault) - ty) };
120
+ }
121
+ if (type === 'bar' && slot === 'primary') {
122
+ var by = P.chartY || 2.95;
123
+ var bb = opts.bottom != null ? opts.bottom : bottomDefault;
124
+ return { x: MX + (P.chartX || 0), y: by, w: CW - (P.chartW || 0), h: Math.max(1.2, bb - by) };
125
+ }
126
+ if (type === 'bar' && slot === 'hbar') {
127
+ var y0 = P.hbarY0 || 2.6;
128
+ var y1 = opts.bottom != null ? opts.bottom : (P.hbarY1 || 6.25);
129
+ return { x: MX, y: y0, w: CW, h: Math.max(1.2, y1 - y0) };
130
+ }
131
+ if (type === 'points' && (slot === 'primary' || slot === 'list')) {
132
+ var ly = P.listY || topDefault;
133
+ return { x: MX, y: ly, w: CW, h: Math.max(0.8, (opts.bottom != null ? opts.bottom : bottomDefault) - ly) };
134
+ }
135
+ if (type === 'split' && (slot === 'left' || slot === 'right')) {
136
+ var st = P.chartY || topDefault;
137
+ var sb = opts.bottom != null ? opts.bottom : bottomDefault;
138
+ if (slot === 'left') return { x: MX, y: st, w: CW * (P.leftW || 0.55), h: Math.max(1.2, sb - st) };
139
+ return { x: MX + CW * (P.rightX != null ? P.rightX : 0.585), y: st, w: CW * (P.rightW || 0.415), h: Math.max(1.2, sb - st) };
140
+ }
141
+ if (type === 'twocol' && (slot === 'primary' || slot === 'left' || slot === 'right')) {
142
+ var gap = P.colGap || 0.5, colW = (CW - gap) / 2;
143
+ var tt = opts.top != null ? opts.top : topDefault;
144
+ var tb = opts.bottom != null ? opts.bottom : bottomDefault;
145
+ if (slot === 'right') return { x: MX + colW + gap, y: tt, w: colW, h: Math.max(1, tb - tt) };
146
+ return { x: MX, y: tt, w: colW, h: Math.max(1, tb - tt) };
147
+ }
148
+ if ((type === 'diagram' || type === 'lane') && slot === 'primary') {
149
+ var arch = PT.arch || {};
150
+ var dy = (opts.mode === 'architecture' && arch.fullStartY != null) ? arch.fullStartY : (P.bodyStartY || 2.9);
151
+ var db = (opts.mode === 'architecture' && arch.fullEndY != null) ? arch.fullEndY
152
+ : (opts.bottom != null ? opts.bottom : bottomDefault);
153
+ return { x: MX, y: dy, w: CW, h: Math.max(1.2, db - dy) };
154
+ }
155
+ if (type === 'kpi' && (slot === 'hero' || slot === 'primary')) {
156
+ return { x: MX, y: P.heroY || 2.9, w: CW * (P.heroW || 0.52), h: P.heroH || 1.6,
157
+ dividerX: P.dividerX, metricY0: P.metricY0, metricRowH: P.metricRowH };
158
+ }
159
+ if (type === 'comparison' && (slot === 'left' || slot === 'primary' || slot === 'right')) {
160
+ var cg = P.panelGap || 0.4, pw = (CW - cg) / 2;
161
+ if (slot === 'right') return { x: MX + pw + cg, y: P.panelY || 2.55, w: pw, h: P.panelH || 3.4 };
162
+ return { x: MX, y: P.panelY || 2.55, w: pw, h: P.panelH || 3.4 };
163
+ }
164
+ if (type === 'quote' && slot === 'primary') {
165
+ return { x: MX + 1.05, y: P.textY || 2.7, w: CW - 2.1, h: P.textH || 2.3 };
166
+ }
167
+ if (type === 'heatmap' && slot === 'primary') {
168
+ var hy = P.y || 2.6;
169
+ return { x: MX, y: hy, w: CW, h: Math.max(1, (opts.bottom != null ? opts.bottom : bottomDefault) - hy) };
170
+ }
171
+ if (type === 'bullet' && slot === 'primary') {
172
+ var uy = P.y || 2.6;
173
+ return { x: MX, y: uy, w: CW, h: Math.max(1, (opts.bottom != null ? opts.bottom : bottomDefault) - uy) };
174
+ }
175
+ if (type === 'pyramid' && slot === 'primary') {
176
+ var py2 = P.y || 2.35;
177
+ return { x: MX, y: py2, w: CW, h: Math.max(1, (opts.bottom != null ? opts.bottom : bottomDefault) - py2) };
178
+ }
179
+ if (type === 'steps' && slot === 'primary') {
180
+ return { x: MX, y: P.y || 3.05, w: CW, h: P.barH || 1.35 };
181
+ }
182
+ if (type === 'cards' && slot === 'primary') {
183
+ var cy2 = P.startY || 3.0;
184
+ return { x: MX, y: cy2, w: CW, h: Math.max(1, (opts.bottom != null ? opts.bottom : bottomDefault) - cy2) };
185
+ }
186
+ if (type === 'timeline' && slot === 'primary') {
187
+ return { x: MX, y: P.labelY || 2.95, w: CW, h: Math.max(1, bottomDefault - (P.labelY || 2.95)) };
188
+ }
189
+ if (type === 'image' && slot === 'primary') {
190
+ var iy2 = Math.max(P.y || 2.3, opts.top || 0);
191
+ return { x: MX, y: iy2, w: CW, h: Math.max(1.2, Math.min(P.h || 4.2, (opts.bottom != null ? opts.bottom : bottomDefault) - iy2)) };
192
+ }
193
+ if ((type === 'halftable') && (slot === 'left' || slot === 'primary')) {
194
+ var rs = PT.research || {};
195
+ return { x: MX, y: topDefault, w: CW * (rs.halfTableW || 0.52), h: Math.max(1, bottomDefault - topDefault) };
196
+ }
197
+ if ((type === 'halftable') && (slot === 'right' || slot === 'secondary')) {
198
+ var rs2 = PT.research || {};
199
+ return { x: MX + CW * (rs2.halfChartX || 0.57), y: topDefault, w: CW * (rs2.halfChartW || 0.43), h: Math.max(1, bottomDefault - topDefault) };
200
+ }
201
+ if (type === 'matrix' && slot === 'primary') {
202
+ var rm = PT.research || {};
203
+ var my = rm.matrixY || 2.3;
204
+ return { x: MX, y: my, w: CW, h: Math.max(1, (opts.bottom != null ? opts.bottom : bottomDefault) - my) };
205
+ }
206
+ var INFO = { sankey:1, treemap:1, boxplot:1, network:1, marimekko:1, streamgraph:1 };
207
+ if (INFO[type] && slot === 'primary') {
208
+ var iy3 = P.y || topDefault;
209
+ var ih3 = Math.max(1.2, Math.min(P.h || 4.1, (opts.bottom != null ? opts.bottom : bottomDefault) - iy3));
210
+ return { x: MX, y: iy3, w: CW, h: ih3 };
211
+ }
212
+ return { x: MX, y: topDefault, w: CW, h: Math.max(0.8, bottomDefault - topDefault) };
213
+ }
214
+
215
+ /* __TOPPPT_CONSTANTS_END__ */
216
+
217
+ /* __TOPPPT_SCHEMA_START__ */
218
+ /* ── 由 scripts/sync_runtime.py 从 scripts/model-schema.json 注入 · 禁止手改 ── */
219
+ /* DSL schema 单源:浏览器端 validateModel 与 scripts/extract_model.py 消费同一份定义 */
220
+ var MODEL_SCHEMA = {"version":"0.1","modes":["presentation","research","architecture"],"model":{"required":["title","sections:array","closing.title","closing.points:array"],"optional":["mode","style","theme","subtitle","meta","agenda:array"],"agendaMin":{"presentation":3,"research":3,"architecture":0},"sectionsMin":1,"sectionsRecommended":3,"agendaComfortMax":16},"commonSectionFields":["eyebrow","title","lead","soWhat","footnote","flags","image","exhibitNo","layoutPreset"],"flagsHint":"flags = 待核实/待二次修改条目字符串数组(如 '2027 增速 xx%(口径未定)');渲染为 accent 强调色标注行,提示用户核对。","imageHint":"image = 素材图片/配图占位对象。三选一必填:src(用户图,data: 内联或相对路径,零外链铁律禁 http(s))|items(多图版式:grid/compare/wall,元素 {src?, alt?, caption?, placeholder?})|placeholder:true(无图时出配图占位,原生形状渲染、pictures 不增)。其余可选:layout(full/half/bleed/grid/compare/wall,见 layout-constants.json imageSpec.layouts)、fit(cover 默认 / contain)、caption、alt、hint、ratio。","imageLayoutHint":"layout 与 HTML 版式对应:full=版心全宽图|half=左图右注(points 为右栏注解)|bleed=通栏出血|grid=多图网格(items 2/3/4/6 张)|compare=双图 A/B 对比(items 2 张)|wall=Logo 墙(items 3–12 张小图)。占位符标签文本由双引擎按 imageSpec 拼同一串,保证 A/B 通道逐页文本一致。","chartTypes":["bar","hbar","stack","stackline","line","dualline","area","donut","multidonut","pie","radar","scatter","bubble","waterfall","gauge","pareto","funnel","gantt","vsbar","progress","sparkline","slope","dumbbell","lollipop","dotplot","bulletchart","waffle","radialbar","rose","candlestick"],"chartTypesHint":"chart.type 可取值 = 本表(30 类:16 原生 + 14 形状还原)。其余 6 类复杂信息图(sankey/treemap/boxplot/network/marimekko/streamgraph)是**专属页型**而非 chart.type——它们要按列/段/节点等结构化载荷表达,故走 sections[].type。不变量:charts.registry.types == chartTypes ∪ 上述 6 类页型(sync_runtime.py 校验)。","chartDataTable":{"enum":["notes","inline","appendix","off"],"default":"notes","rule":"非原生图表(registry.pptx=shape)的 dataTable 不得为 off;缺失时按 registry 默认值补齐。"},"pageTypes":{"points":{"label":"要点列表页","modes":["presentation","research","architecture"],"required":["points:array"],"optional":["metrics:array"]},"metrics":{"label":"指标带页(4–6 个核心数字)","modes":["presentation","research"],"required":["metrics:array"]},"kpi":{"label":"大数指标页(hero 大数字 + 支撑指标行)","modes":["presentation","research"],"required":["hero:array"],"optional":["metrics:array"]},"table":{"label":"对比表页","modes":["presentation","research","architecture"],"required":["table.head:array","table.rows:array"],"optional":["table.colW:array"]},"timeline":{"label":"时间线 / 路线页","modes":["presentation","research"],"required":["phases:array"]},"steps":{"label":"步骤条页(N 步横排 + 箭头,可分组)","modes":["presentation","research","architecture"],"required":["steps:array"],"optional":["groups:array","note"]},"bar":{"label":"图表页(chart.type 见 chartTypes;native 通道走原生数据图表,shape 通道走高保真形状还原 + 数据表)","modes":["presentation","research"],"required":["chart.labels:array","chart.values:array"],"optional":["chart.type","chart.series:array","chart.points:array","chart.max","chart.unit","chart.colors:array","chart.start:array","chart.target:array","chart.dataTable","note"]},"donut":{"label":"环形图页(原生 pie 楔形 + 图例)","modes":["presentation","research"],"required":["chart.labels:array","chart.values:array"],"optional":["chart.unit","chart.centerLabel","chart.colors:array","chart.dataTable","note"]},"heatmap":{"label":"热力矩阵页(行 × 列 + 强度色阶)","modes":["presentation","research"],"required":["rowHeads:array","colHeads:array","cells:array"],"optional":["unit","scaleLabel:array","note"]},"bullet":{"label":"达成对比页(实际 vs 目标条)","modes":["presentation","research"],"required":["items:array"],"optional":["unit","max","note"]},"pyramid":{"label":"金字塔页(层级递进,顶层最窄)","modes":["presentation","research","architecture"],"required":["levels:array"],"optional":["note"]},"image":{"label":"素材图片页(full/half/bleed/grid/compare/wall,效果与位置锁定;支持配图占位)","modes":["presentation","research","architecture"],"required":["anyof:image.src:str|image.items:array|image.placeholder"],"optional":["image.caption","image.alt","image.layout","image.fit","image.ratio","image.hint","points:array","note"]},"cards":{"label":"卡片网格页(cards 元素须为 {title, points:[[k,v]…]},不接受元组)","modes":["presentation","research"],"required":["cards:array"],"optional":["columns"]},"split":{"label":"双区组合页(left / right 各可为 要点|图表|表格|图片:type = points(默认)| table | image | 图表类型名;图表/表格字段直接挂在对应侧)","modes":["presentation","research"],"required":["anyof:left.points:array|left.type:str"],"optional":["left.cap","left.image","left.labels:array","left.values:array","left.series:array","left.points:array","left.max","left.unit","left.colors:array","left.dataTable","left.head:array","left.rows:array","left.colW:array","right.type","right.cap","right.image","right.labels:array","right.values:array","right.series:array","right.points:array","right.max","right.unit","right.colors:array","right.dataTable","right.head:array","right.rows:array","right.colW:array"]},"comparison":{"label":"对比页(左右双栏 + 可选结论条)","modes":["presentation","research"],"required":["left.title","left.points:array","right.title","right.points:array"],"optional":["verdict"]},"quote":{"label":"引用 / 金句页(深色全幅)","modes":["presentation","research"],"required":["quote","author"],"optional":["context"]},"diagram":{"label":"分层架构页(architecture 模式走全幅几何)","modes":["presentation","research","architecture"],"required":["layers:array"],"optional":["legend:array"]},"exhibit":{"label":"Exhibit 编号图表页(research R2)","modes":["research"],"required":["chart.labels:array","chart.values:array"],"optional":["exhibitNo","chart.type","chart.series:array","chart.points:array","chart.max","chart.unit","chart.colors:array","chart.dataTable","note"]},"twocol":{"label":"双栏论证页(research R1)","modes":["research"],"required":["paragraphs:array"]},"threecol":{"label":"三栏证据页(research R6)","modes":["research"],"required":["paragraphs:array"]},"halftable":{"label":"半表半图页(research R7)","modes":["research"],"required":["table.head:array","table.rows:array","chart.labels:array","chart.values:array"],"optional":["table.colW:array","chart.type","chart.series:array","chart.points:array","chart.max","chart.unit","chart.colors:array","chart.dataTable","note"]},"matrix":{"label":"矩阵图页(research R8)","modes":["research"],"required":["rowHeads:array","colHeads:array","cells:array"]},"lane":{"label":"泳道页(architecture A2)","modes":["architecture"],"required":["lanes:array"]},"sankey":{"label":"桑基图页(节点-流带,流向与流量)","modes":["research","architecture"],"required":["flows:array"],"optional":["unit","chart.dataTable","note"]},"treemap":{"label":"树图页(面积编码的层级构成)","modes":["research","architecture"],"required":["items:array"],"optional":["unit","chart.dataTable","note"]},"boxplot":{"label":"箱线图页(分布对比:min/q1/median/q3/max)","modes":["research","architecture"],"required":["groups:array"],"optional":["unit","chart.dataTable","note"]},"network":{"label":"关系网络页(节点-边拓扑)","modes":["research","architecture"],"required":["nodes:array","edges:array"],"optional":["chart.dataTable","note"]},"marimekko":{"label":"马赛克图页(列宽 × 列高双重编码)","modes":["research","architecture"],"required":["cols:array","cells:array"],"optional":["unit","chart.dataTable","note"]},"streamgraph":{"label":"流带图页(时间上的构成演变)","modes":["research","architecture"],"required":["series:array"],"optional":["labels:array","chart.dataTable","note"]}}};
221
+ /* __TOPPPT_SCHEMA_END__ */
222
+
223
+ var _mode = 'presentation'; /* 当前模式(slidesOf 里按 model.mode 设置) */
224
+ /* 三模式独立排版比例尺:调用点给 presentation 基准 pt → 取最接近的 TS_BASE 角色 →
225
+ 查 MTS[mode][role]。research 是咨询密排比例尺(正文 10.5pt),不再是简单 ×0.8。
226
+ 未知模式回落 presentation 档(MTS 与 mode 取值同源于 model-schema.json 的 modes)。 */
227
+ function modeSize(base) {
228
+ var mts = (typeof MTS !== 'undefined' && MTS) ? (MTS[_mode] || MTS.presentation) : null;
229
+ if (!mts) return base;
230
+ var best = null, bd = 1e9;
231
+ for (var k in TS_BASE) {
232
+ if (!TS_BASE.hasOwnProperty(k)) continue;
233
+ var d = Math.abs(TS_BASE[k] - base);
234
+ if (d < bd) { bd = d; best = mts[k]; }
235
+ }
236
+ /* 钳制与取整必须与 build_pptx.js 的 sz() 逐字一致——同一张表配不同后处理 = 两通道字号漂移 */
237
+ return Math.max(8.5, Math.round((best == null ? base : best) * 2) / 2);
238
+ }
239
+ /* 编码色板:9 套风格**各有自己的** c1–c5(单源 styleDataColors / styleDataColorsDark)。
240
+ 与 HTML 侧的 .f-c1~c5 语义类同源;结构色纪律不受影响——本表只用于数据系列。 */
241
+ function dataColors(style, theme) {
242
+ var dark = (theme === 'dark');
243
+ var map = (dark && typeof STYLE_DATA_COLORS_DARK !== 'undefined' && STYLE_DATA_COLORS_DARK)
244
+ ? STYLE_DATA_COLORS_DARK
245
+ : (typeof STYLE_DATA_COLORS !== 'undefined' ? STYLE_DATA_COLORS : null);
246
+ if (!map) return null;
247
+ var c = map[style];
248
+ return (c && c.length) ? c : null; // null = 用 S 的中性色(在调用处映射)
249
+ }
250
+
251
+ /* ── 版式安全边界(与 build_pptx.js 同一常量单源) ── */
252
+ var LAY = PT.layout || {};
253
+ var CONTENT_TOP = (LAY.contentTop != null) ? LAY.contentTop : 2.5;
254
+ var CONTENT_BOTTOM = (LAY.contentBottom != null) ? LAY.contentBottom : 6.9;
255
+
256
+ /* ── 自适应排版工具(与 build_pptx.js 同算法,保证两通道字号/换行一致) ── */
257
+ function estLines(text, widthIn, fontPt) {
258
+ var usable = Math.max(24, widthIn * 72 - 8), w = 0, lines = 1;
259
+ var s = String(text == null ? '' : text);
260
+ for (var i = 0; i < s.length; i++) {
261
+ if (s[i] === '\n') { lines++; w = 0; continue; }
262
+ var cw = /[\x00-\xff]/.test(s[i]) ? fontPt * 0.52 : fontPt;
263
+ if (w + cw > usable) { lines++; w = cw; } else { w += cw; }
264
+ }
265
+ return lines;
266
+ }
267
+ function estTextH(text, widthIn, fontPt, lineFactor) {
268
+ return estLines(text, widthIn, fontPt) * fontPt * (lineFactor || 1.45) / 72;
269
+ }
270
+ var FONT_LADDER = [15, 14, 13.5, 13, 12.5, 12, 11.5, 11, 10.5, 10, 9.5, 9, 8.5];
271
+ /* 有限缩字号(v8.2,与 build_pptx.js 同策略):先优化内容/组合/拆页,最后才缩字号。
272
+ maxShrinkSteps 限制下探步数;floorPt 按模式绝对下限。 */
273
+ function modeFloorPtA(mode) {
274
+ var floors = (typeof CONT !== 'undefined' && CONT && CONT.fontShrink && CONT.fontShrink.floorPt) ||
275
+ { presentation: 10, research: 9, architecture: 10 };
276
+ var m = String(mode || 'presentation');
277
+ return (floors[m] != null) ? floors[m] : 10;
278
+ }
279
+ function fitFont(items, widthIn, availIn, opts) {
280
+ opts = opts || {};
281
+ var max = (opts.max == null) ? 99 : opts.max, lf = opts.lineFactor || 1.45;
282
+ var gapF = (opts.gapFactor == null) ? 0.5 : opts.gapFactor;
283
+ var mode = (typeof CONTENT !== 'undefined' && CONTENT && CONTENT.mode) ||
284
+ (typeof TOPPPT_MODE !== 'undefined' ? TOPPPT_MODE : 'presentation');
285
+ var floor = (opts.floor != null) ? opts.floor : modeFloorPtA(mode);
286
+ var maxSteps = (opts.maxShrinkSteps != null) ? opts.maxShrinkSteps
287
+ : ((typeof CONT !== 'undefined' && CONT && CONT.fontShrink && CONT.fontShrink.maxShrinkSteps != null)
288
+ ? CONT.fontShrink.maxShrinkSteps : 4);
289
+ var baseLadder = (typeof CONT !== 'undefined' && CONT && CONT.fontShrink && CONT.fontShrink.ladder) || FONT_LADDER;
290
+ var raw = baseLadder.filter(function (v) { return v <= max && v >= floor; });
291
+ var needH = function (f) {
292
+ var h = 0;
293
+ for (var i = 0; i < items.length; i++) h += estTextH(items[i], widthIn, f, lf) + f * gapF / 72;
294
+ return h + (opts.pad || 0);
295
+ };
296
+ for (var li = 0; li < raw.length; li++) {
297
+ if (needH(raw[li]) <= availIn) return raw[li];
298
+ }
299
+ var preferred = raw.length ? raw[0] : floor;
300
+ var prefIdx = baseLadder.indexOf(preferred);
301
+ var limited = baseLadder.slice(prefIdx, prefIdx + 1 + maxSteps)
302
+ .filter(function (v) { return v >= floor && v <= max; });
303
+ for (var lj = 0; lj < limited.length; lj++) {
304
+ if (needH(limited[lj]) <= availIn) return limited[lj];
305
+ }
306
+ if (limited.length) return limited[limited.length - 1];
307
+ return floor;
308
+ }
309
+ function cols(total, n, gap) {
310
+ var w = (total - (n - 1) * (gap || 0)) / Math.max(1, n);
311
+ return { w: w, step: w + (gap || 0) };
312
+ }
313
+ /* 行高收敛:与 build_pptx.js 同算法(宁可行高更小也不越界) */
314
+ function fitRowH(avail, n, pref, floor) {
315
+ var lo = (floor == null) ? 0.22 : floor;
316
+ return Math.max(lo, Math.min(pref, avail / Math.max(1, n)));
317
+ }
318
+ function chartBottom(hasSoWhat, hasFootnote) {
319
+ if (hasFootnote) return PT.exhibit.footnoteY - 0.12;
320
+ if (hasSoWhat) return PT.exhibit.soWhatY - 0.12;
321
+ return CONTENT_BOTTOM;
322
+ }
323
+
324
+ /* ── XML 工具 ── */
325
+ function esc(s) {
326
+ return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
327
+ .replace(/"/g, '&quot;').replace(/'/g, '&apos;');
328
+ }
329
+ function E(v) { return Math.round(v * EMU); }
330
+ function solid(hex) { return '<a:solidFill><a:srgbClr val="' + hex + '"/></a:solidFill>'; }
331
+
332
+ var _id = 10;
333
+ function nid() { return ++_id; }
334
+
335
+ /* 文本框:paras = [[{t,b,sz,col,font,spc}…]…](每个内层数组=一段,可混排多 run)
336
+ opts.lineSpacing / opts.spaceBefore 为基准 pt(与 build_pptx.js 的 lineSpacing/paraSpaceBefore 同源),
337
+ 序列化时统一过 modeSize 映射,保证两通道行距一致。 */
338
+ function txSp(x, y, w, h, paras, opts) {
339
+ opts = opts || {};
340
+ var pPr = '';
341
+ if (opts.lineSpacing) pPr += '<a:lnSpc><a:spcPts val="' + Math.round(modeSize(opts.lineSpacing) * 100) + '"/></a:lnSpc>';
342
+ if (opts.spaceBefore) pPr += '<a:spcBef><a:spcPts val="' + Math.round(modeSize(opts.spaceBefore) * 100) + '"/></a:spcBef>';
343
+ var pPrXml = (opts.align || pPr)
344
+ ? '<a:pPr' + (opts.align ? ' algn="' + opts.align + '"' : '') + '>' + pPr + '</a:pPr>'
345
+ : '';
346
+ var body = paras.map(function (runs) {
347
+ var rs = runs.map(function (r) {
348
+ var pr = '<a:rPr lang="zh-CN" sz="' + Math.round(modeSize(r.sz) * 100) + '"' +
349
+ (r.b ? ' b="1"' : '') + (r.spc ? ' spc="' + r.spc + '"' : '') + '>' +
350
+ solid(r.col) +
351
+ '<a:latin typeface="' + esc(r.font) + '"/><a:ea typeface="' + esc(r.font) + '"/></a:rPr>';
352
+ return '<a:r>' + pr + '<a:t>' + esc(r.t) + '</a:t></a:r>';
353
+ }).join('');
354
+ return '<a:p>' + pPrXml + rs + '</a:p>';
355
+ }).join('');
356
+ var fill = opts.fill ? solid(opts.fill) : '<a:noFill/>';
357
+ return '<p:sp><p:nvSpPr><p:cNvPr id="' + nid() + '" name="tx"/><p:cNvSpPr txBox="1"/><p:nvPr/></p:nvSpPr>' +
358
+ '<p:spPr><a:xfrm><a:off x="' + E(x) + '" y="' + E(y) + '"/><a:ext cx="' + E(w) + '" cy="' + E(h) + '"/></a:xfrm>' +
359
+ '<a:prstGeom prst="rect"><a:avLst/></a:prstGeom>' + fill + '<a:ln><a:noFill/></a:ln></p:spPr>' +
360
+ '<p:txBody><a:bodyPr wrap="square" anchor="' + (opts.anchor || 't') + '"><a:normAutofit/></a:bodyPr><a:lstStyle/>' +
361
+ body + '</p:txBody></p:sp>';
362
+ }
363
+
364
+ /* 形状:rect / roundRect / ellipse / pie(opts.av = avLst 原始内联 XML,供 donut 楔形角度) */
365
+ function shape(geom, x, y, w, h, fillHex, opts) {
366
+ opts = opts || {};
367
+ var av;
368
+ if (opts.av) av = '<a:avLst>' + opts.av + '</a:avLst>';
369
+ else if (geom === 'roundRect') av = '<a:avLst><a:gd name="adj" fmla="val ' + (opts.adj || 8000) + '"/></a:avLst>';
370
+ else av = '<a:avLst/>';
371
+ var ln = opts.line
372
+ ? '<a:ln w="9525">' + (opts.dash ? '<a:prstDash val="dash"/>' : '') + solid(opts.line) + '</a:ln>'
373
+ : '<a:ln><a:noFill/></a:ln>';
374
+ return '<p:sp><p:nvSpPr><p:cNvPr id="' + nid() + '" name="' + (opts.name || 'sh') + '"/><p:cNvSpPr/><p:nvPr/></p:nvSpPr>' +
375
+ '<p:spPr><a:xfrm><a:off x="' + E(x) + '" y="' + E(y) + '"/><a:ext cx="' + E(Math.max(w, 0.01)) + '" cy="' + E(Math.max(h, 0.01)) + '"/></a:xfrm>' +
376
+ '<a:prstGeom prst="' + geom + '">' + av + '</a:prstGeom>' + solid(fillHex) + ln + '</p:spPr>' +
377
+ '<p:txBody><a:bodyPr/><a:lstStyle/><a:p><a:endParaRPr lang="zh-CN"/></a:p></p:txBody></p:sp>';
378
+ }
379
+ /* pie 楔形 avLst:角度(度)× 60000;donut 页型两通道共用同一角度拆分规则 */
380
+ function pieAv(a1, a2) {
381
+ return '<a:gd name="adj1" fmla="val ' + Math.round(a1 * 60000) + '"/>' +
382
+ '<a:gd name="adj2" fmla="val ' + Math.round(a2 * 60000) + '"/>';
383
+ }
384
+ /* 环形图(donut 页型):原生 pie 楔形 + 背景色圆(挖孔)+ 中心文字 + 右侧图例。
385
+ 角度约定:12 点钟(270°)起顺时针;跨 360° 的楔形拆成两段(≤359.9 封顶)。 */
386
+ function donutWedges(labels, values) {
387
+ var total = 0;
388
+ (values || []).forEach(function (v) { total += Math.max(0, v); });
389
+ var segs = [], a = 270;
390
+ (values || []).forEach(function (v, i) {
391
+ var frac = total > 0 ? Math.max(0, v) / total : 0;
392
+ var b = a + frac * 360;
393
+ var s = a % 360, e = b % 360;
394
+ if (frac <= 0) { a = b; return; }
395
+ if (b - a >= 359.9) { segs.push([0, 359.9, i]); }
396
+ else if (b <= 360) { segs.push([a, Math.min(b, 359.9), i]); }
397
+ else { segs.push([a, 359.9, i]); if (e > 0.1) segs.push([0, e, i]); }
398
+ a = b;
399
+ });
400
+ return segs;
401
+ }
402
+ function donutChart(sh, ec, S, dcols) {
403
+ var d = PT.donut, labels = ec.labels || [], values = ec.values || [];
404
+ var segs = donutWedges(labels, values);
405
+ var bx = d.centerX - d.r, by = d.centerY - d.r, bw = d.r * 2;
406
+ segs.forEach(function (sg) {
407
+ sh.push(shape('pie', bx, by, bw, bw, dcols[sg[2] % dcols.length], { av: pieAv(sg[0], sg[1]) }));
408
+ });
409
+ var hb = d.holeR * 2;
410
+ sh.push(shape('ellipse', d.centerX - d.holeR, d.centerY - d.holeR, hb, hb, S.bg));
411
+ var total = 0; values.forEach(function (v) { total += Math.max(0, v); });
412
+ var totalStr = (Math.round(total * 10) / 10) + (ec.unit || '');
413
+ sh.push(txSp(d.centerX - d.holeR, d.centerY - 0.42, hb, 0.5,
414
+ [[{ t: totalStr, sz: 20, b: 1, col: S.ink, font: S.fontDisplay }]], { align: 'ctr', anchor: 'ctr' }));
415
+ sh.push(txSp(d.centerX - d.holeR, d.centerY + 0.08, hb, 0.35,
416
+ [[{ t: ec.centerLabel || '合计', sz: 9.5, col: S.faint, font: S.font }]], { align: 'ctr', anchor: 'ctr' }));
417
+ var n = Math.max(1, labels.length);
418
+ var rowH = Math.min(d.legendRowH, 3.4 / n);
419
+ var ly0 = d.centerY - (n * rowH) / 2 + 0.1;
420
+ var sum2 = total || 1;
421
+ labels.forEach(function (lb, i) {
422
+ var v = values[i] || 0;
423
+ var y = ly0 + i * rowH;
424
+ sh.push(shape('ellipse', d.legendX, y + 0.08, 0.16, 0.16, dcols[i % dcols.length]));
425
+ sh.push(txSp(d.legendX + 0.3, y, d.legendLabelW, rowH,
426
+ [[{ t: lb, sz: 12, b: 1, col: S.ink, font: S.font }]], { anchor: 'ctr' }));
427
+ sh.push(txSp(d.legendX + 3.8, y, d.legendValW, rowH,
428
+ [[{ t: String(v) + (ec.unit || ''), sz: 12, b: 1, col: S.body, font: S.font }]], { align: 'r', anchor: 'ctr' }));
429
+ sh.push(txSp(d.legendPctX, y, d.legendPctW, rowH,
430
+ [[{ t: (Math.round(v / sum2 * 1000) / 10) + '%', sz: 10.5, col: S.faint, font: S.font }]], { align: 'r', anchor: 'ctr' }));
431
+ });
432
+ }
433
+ function donutColors(style, S, theme) {
434
+ var d = dataColors(style, theme);
435
+ return d || [S.accent, S.faint, S.body, S.line];
436
+ }
437
+
438
+ /* 表格(graphicFrame + a:tbl);rows[0]=表头;行高按可用高度自适应(与 B 通道同规则) */
439
+ function table(x, y, w, colW, rows, S, opts) {
440
+ opts = opts || {};
441
+ var n = rows.length;
442
+ var maxRowH = (opts.maxRowH == null) ? PT.table.rowH : opts.maxRowH;
443
+ var minRowH = (opts.minRowH == null) ? PT.table.rowHMin : opts.minRowH;
444
+ var availH = (opts.availH == null) ? (CONTENT_BOTTOM - y) : opts.availH;
445
+ var rowH = Math.max(minRowH, Math.min(maxRowH, availH / Math.max(1, n)));
446
+ var dense = rowH < 0.42;
447
+ var total = colW.reduce(function (a, b) { return a + b; }, 0);
448
+ var grid = colW.map(function (cw) { return '<a:gridCol w="' + E(cw) + '"/>'; }).join('');
449
+ var trs = rows.map(function (row, ri) {
450
+ var tcs = row.map(function (cell) {
451
+ var isHead = ri === 0;
452
+ var fz = isHead ? (dense ? 11 : 13) : (dense ? 10 : 12.5);
453
+ return '<a:tc><a:txBody><a:bodyPr/><a:lstStyle/><a:p><a:r>' +
454
+ '<a:rPr lang="zh-CN" sz="' + Math.round(modeSize(fz) * 100) + '"' + (isHead ? ' b="1"' : '') + '>' +
455
+ solid(isHead ? S.onAccent : S.body) +
456
+ '<a:latin typeface="' + S.font + '"/><a:ea typeface="' + S.font + '"/></a:rPr>' +
457
+ '<a:t>' + esc(cell) + '</a:t></a:r></a:p></a:txBody>' +
458
+ '<a:tcPr marL="91440" marR="91440" marT="45720" marB="45720" anchor="ctr">' +
459
+ solid(isHead ? S.accent : S.bg) + '</a:tcPr></a:tc>';
460
+ }).join('');
461
+ return '<a:tr h="' + E(rowH) + '">' + tcs + '</a:tr>';
462
+ }).join('');
463
+ /* OOXML 规范:p:graphicFrame 的 graphic 子元素属 DrawingML 命名空间(a:graphic,与 pptxgenjs 一致) */
464
+ return '<p:graphicFrame><p:nvGraphicFramePr><p:cNvPr id="' + nid() + '" name="tbl"/>' +
465
+ '<p:cNvGraphicFramePr><a:graphicFrameLocks noGrp="1"/></p:cNvGraphicFramePr><p:nvPr/></p:nvGraphicFramePr>' +
466
+ '<p:xfrm><a:off x="' + E(x) + '" y="' + E(y) + '"/><a:ext cx="' + E(w) + '" cy="' + E(rowH * rows.length) + '"/></p:xfrm>' +
467
+ '<a:graphic><a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/table">' +
468
+ '<a:tbl><a:tblPr firstRow="1" bandRow="0"/><a:tblGrid>' + grid + '</a:tblGrid>' + trs + '</a:tbl>' +
469
+ '</a:graphicData></a:graphic></p:graphicFrame>';
470
+ }
471
+
472
+ /* so-what 结论条 / 页脚来源行(research 页通用可选件,与 HTML 同源版式;区域走布局 IR) */
473
+ function soWhatBar(sh, text, S) {
474
+ var R = (typeof regionOf === 'function') ? regionOf('exhibit', 'annotation')
475
+ : { x: MX, y: PT.exhibit.soWhatY, w: CW, h: 0.62 };
476
+ /* 与 B 通道同算法:长结论条有限缩字号,优先保容器不溢出 */
477
+ var bodyW = Math.max(2.5, R.w - 0.44 - 1.35);
478
+ var fzB = fitFont([String(text || '')], bodyW, R.h - 0.14, { max: 12.5, gapFactor: 0.2, maxShrinkSteps: 3 });
479
+ sh.push(shape('rect', R.x, R.y, R.w, R.h, S.soft));
480
+ sh.push(shape('rect', R.x, R.y, 0.06, R.h, S.accent));
481
+ sh.push(txSp(R.x + 0.22, R.y + 0.06, R.w - 0.44, R.h - 0.12,
482
+ [[{ t: 'SO WHAT ', sz: 11, b: 1, col: S.accent, font: S.font, spc: 150 },
483
+ { t: text, sz: fzB, col: S.ink, font: S.font }]], { anchor: 'ctr' }));
484
+ }
485
+ function footnoteLine(sh, text, S) {
486
+ var R = (typeof regionOf === 'function') ? regionOf('exhibit', 'footnote')
487
+ : { x: MX, y: PT.exhibit.footnoteY, w: CW - 1.2, h: 0.32 };
488
+ sh.push(txSp(R.x, R.y, R.w, R.h,
489
+ [[{ t: text, sz: 9.5, col: S.faint, font: S.font }]]));
490
+ }
491
+ /* 待核实清单条(accent 强调色标注 · 与 build_pptx.js / HTML .flagbar 同源) */
492
+ function flagBar(sh, items, S, yTop) {
493
+ var F = PT.flagbar || { hdH: 0.26, rowH: 0.24, maxRows: 3, gap: 0.12 };
494
+ var list = (items || []).slice(0, F.maxRows).map(String);
495
+ if (!list.length) return 0;
496
+ var h = F.hdH + list.length * F.rowH + 0.06;
497
+ var y = (yTop != null) ? yTop : (CONTENT_BOTTOM - h);
498
+ sh.push(shape('rect', MX, y, CW, h, S.soft));
499
+ sh.push(shape('rect', MX, y, 0.055, h, S.accent));
500
+ sh.push(txSp(MX + 0.22, y + 0.02, CW - 0.44, F.hdH,
501
+ [[{ t: '待核实 · 需二次确认', sz: 9.5, b: 1, col: S.accent, font: S.font, spc: 100 }]]));
502
+ list.forEach(function (t, i) {
503
+ sh.push(txSp(MX + 0.24, y + F.hdH + i * F.rowH, CW - 0.5, F.rowH,
504
+ [[{ t: '· ' + t, sz: 9.5, col: S.body, font: S.font }]], { anchor: 'ctr' }));
505
+ });
506
+ return h;
507
+ }
508
+ /* 素材图片:预览引擎只产出 slide XML(无 media part)→ 以中性图位块表示;
509
+ 配图占位(placeholder)额外发射标签文本——标签串由 imageSpec 常量拼接,与 B 通道**同串**
510
+ (cross_verify 逐页比对)。真实图片不发射任何文本,保证 A/B 文本集合一致。 */
511
+ function imgPlaceholderLabel(layout, multi) {
512
+ var IS = (typeof IMG !== 'undefined' && IMG) || {};
513
+ var label = IS.placeholderLabel || '配图占位';
514
+ if (multi) return label;
515
+ var size = (IS.recommendedSizePx || {})[layout];
516
+ if (!size) return label;
517
+ return label + ' · ' + (IS.placeholderHintPrefix || '建议 ') + size + (IS.placeholderHintSuffix || 'px');
518
+ }
519
+ function addImageEl(sh, src, S, x, y, w, h, opts) {
520
+ opts = opts || {};
521
+ var ph = !!opts.placeholder || !src;
522
+ /* name="img"/"imgph":预览模态据此把图位替换为真实图片(ui.js renderSlide),
523
+ 不影响文本集合(cross_verify 只比文本)。 */
524
+ sh.push(shape('roundRect', x, y, w, h, S.surface,
525
+ { line: S.line, dash: ph, adj: 4000, name: ph ? 'imgph' : 'img' }));
526
+ if (ph) {
527
+ sh.push(txSp(x + 0.08, y + h / 2 - 0.22, Math.max(0.4, w - 0.16), 0.44,
528
+ [[{ t: imgPlaceholderLabel(opts.layout || 'full', !!opts.multi), sz: 11, b: 1, col: S.faint, font: S.font }]],
529
+ { align: 'ctr', anchor: 'ctr' }));
530
+ }
531
+ }
532
+ /* 版式比例(imageSpec.ratioDefault 或 image.ratio,形如 "3:1")→ 数值宽高比。
533
+ 双引擎同源:HTML 用同比例的 .media--r* 锁定类,PPTX 用本函数算高度,版式才一致。 */
534
+ function imgRatio(layout, img) {
535
+ var IS = (typeof IMG !== 'undefined' && IMG) || {};
536
+ var raw = String((img && img.ratio) || (IS.ratioDefault || {})[layout] || '3:1');
537
+ var m = raw.match(/^\s*(\d+(?:\.\d+)?)\s*[:/×x]\s*(\d+(?:\.\d+)?)/);
538
+ if (!m) return 3;
539
+ var a = parseFloat(m[1]), b = parseFloat(m[2]);
540
+ return (a > 0 && b > 0) ? a / b : 3;
541
+ }
542
+ /* 图片版式族(与 HTML .media / .media-grid / .media-compare / .media-wall 同源):
543
+ full 版心全宽|half 左图右注|bleed 通栏出血|grid 多图网格|compare 双图对比|wall Logo 墙。
544
+ 每版式按 imageSpec.ratioDefault 锁定比例;空间不足时按可用高度收敛并**垂直居中**
545
+ (避免"图贴顶、下方大片留白")。返回图注基准 Y。占位符走原生形状,pictures 不增。 */
546
+ function imageLayoutShapes(sh, S, layout, img, items, points, isPh, fit, y0, y1) {
547
+ var IM = PT.image || {};
548
+ var gap = IM.gridGap || 0.16;
549
+ var iy = y0;
550
+ var ih = Math.max(1.0, y1 - iy);
551
+ var capY = iy + ih;
552
+ var ratio = imgRatio(layout, img);
553
+ function one(x, y, w, h, it, multi) {
554
+ var ph = isPh || !(it && it.src);
555
+ addImageEl(sh, (it && it.src) || '', S, x, y, w, h,
556
+ { placeholder: ph, layout: layout, multi: multi, fit: fit });
557
+ }
558
+ if (layout === 'grid' && items.length) {
559
+ var n = Math.min(items.length, (IMG && IMG.maxPerPage) || 6);
560
+ var colsN = (IM.gridCols || {})[String(n)] || Math.min(3, n);
561
+ var rowsN = Math.max(1, Math.ceil(n / colsN));
562
+ var capH = IM.gridCapH || 0.28;
563
+ var cellW = (CW - (colsN - 1) * gap) / colsN;
564
+ var capRows = rowsN * (capH + 0.04);
565
+ var cellH = Math.min(cellW / ratio, Math.max(0.5, (ih - (rowsN - 1) * gap - capRows) / rowsN));
566
+ var blockH = rowsN * cellH + (rowsN - 1) * gap + capRows;
567
+ var gy = iy + Math.max(0, (ih - blockH) / 2);
568
+ items.slice(0, n).forEach(function (it, i) {
569
+ var cx = MX + (i % colsN) * (cellW + gap);
570
+ var cy = gy + Math.floor(i / colsN) * (cellH + gap + capH + 0.04);
571
+ var cap = (it && it.caption) || '';
572
+ one(cx, cy, cellW, cellH, it, true);
573
+ if (cap) sh.push(txSp(cx, cy + cellH + 0.02, cellW, capH,
574
+ [[{ t: cap, sz: 10, col: S.faint, font: S.font }]], { align: 'ctr' }));
575
+ });
576
+ capY = gy + blockH;
577
+ } else if (layout === 'compare' && items.length >= 2) {
578
+ var cgap = IM.compareGap || 0.3;
579
+ var cw2 = (CW - cgap) / 2;
580
+ var capH2 = 0.34;
581
+ var ih2 = Math.min(cw2 / ratio, Math.max(0.5, ih - capH2));
582
+ var cy2 = iy + Math.max(0, (ih - ih2 - capH2) / 2);
583
+ [0, 1].forEach(function (i) {
584
+ var it = items[i] || {};
585
+ var cx2 = MX + i * (cw2 + cgap);
586
+ one(cx2, cy2, cw2, ih2, it, true);
587
+ var cap2 = it.caption || '';
588
+ if (cap2) sh.push(txSp(cx2, cy2 + ih2 + 0.02, cw2, 0.3,
589
+ [[{ t: cap2, sz: 10, col: S.faint, font: S.font }]], { align: 'ctr' }));
590
+ });
591
+ capY = cy2 + ih2 + capH2;
592
+ } else if (layout === 'wall' && items.length) {
593
+ var wgap = IM.wallGap || 0.18;
594
+ var wn = Math.min(items.length, 12);
595
+ var rowsW = Math.max(1, Math.min(IM.wallMaxRows || 2, Math.ceil(wn / 6)));
596
+ var perRow = Math.max(1, Math.ceil(wn / rowsW));
597
+ var cellW2 = (CW - (perRow - 1) * wgap) / perRow;
598
+ var cellH2 = Math.min(IM.wallCellH || 0.92, cellW2 / ratio,
599
+ Math.max(0.4, (ih - (rowsW - 1) * wgap) / rowsW));
600
+ var blockH2 = rowsW * cellH2 + (rowsW - 1) * wgap;
601
+ var wy = iy + Math.max(0, (ih - blockH2) / 2);
602
+ items.slice(0, wn).forEach(function (it, i) {
603
+ var cx3 = MX + (i % perRow) * (cellW2 + wgap);
604
+ var cy3 = wy + Math.floor(i / perRow) * (cellH2 + wgap);
605
+ one(cx3, cy3, cellW2, cellH2, it, true);
606
+ });
607
+ capY = wy + blockH2;
608
+ } else if (layout === 'half') {
609
+ var iw = CW * (IM.halfW || 0.56);
610
+ var ihh = Math.min(iw / ratio, ih);
611
+ one(MX, iy, iw, ihh, img, false);
612
+ var rxI = MX + iw + CW * (IM.halfGap || 0.06);
613
+ var rwI = CW - iw - CW * (IM.halfGap || 0.06);
614
+ var ptsI = points || [];
615
+ var rowHI = fitRowH(ih - 0.1, ptsI.length, IM.noteRowH || 0.62, 0.3);
616
+ var fzI = fitFont(ptsI.map(function (p) { return (p[0] || '') + (p[1] || ''); }), rwI - 0.3, ih,
617
+ { max: 14, gapFactor: 0.6 });
618
+ ptsI.forEach(function (pt, i) {
619
+ var lyI = iy + i * rowHI;
620
+ sh.push(shape('rect', rxI, lyI + rowHI / 2 - 0.06, 0.12, 0.12, S.accent));
621
+ sh.push(txSp(rxI + 0.26, lyI, rwI - 0.3, rowHI - 0.04,
622
+ [[{ t: (pt[0] || '') + ' ', sz: fzI, b: 1, col: S.ink, font: S.font },
623
+ { t: pt[1] || '', sz: fzI - 1, col: S.body, font: S.font }]], { anchor: 't' }));
624
+ });
625
+ capY = iy + Math.max(ihh, ih * 0.5);
626
+ } else if (layout === 'bleed') {
627
+ var bh = Math.min(ih, PW / ratio);
628
+ var by = iy + Math.max(0, (ih - bh) / 2);
629
+ one(0, by, PW, bh, img, false);
630
+ capY = by + bh;
631
+ } else {
632
+ var fh = Math.min(ih, CW / ratio);
633
+ var fy = iy + Math.max(0, (ih - fh) / 2);
634
+ one(MX, fy, CW, fh, img, false);
635
+ capY = fy + fh;
636
+ }
637
+ return capY;
638
+ }
639
+
640
+ /* ══ 图表形状渲染(预览/回归用近似版;交付通道 B 用原生可编辑数据图表) ══
641
+ A/B 两通道的「文本集合」必须一致(cross_verify 逐页比对):
642
+ 类别标签 + 数值标签都渲染为文本,数值标签由 cross_verify 的数值 token 规则过滤。 */
643
+ function valText(v, unit) { return String(v) + (unit || ''); }
644
+ /* 横向条形(hbar / halftable / exhibit hbar) */
645
+ function hbarRows(sh, ec, S, y0, y1, dcols, x0, w) {
646
+ x0 = (x0 == null) ? MX : x0;
647
+ w = (w == null) ? CW : w;
648
+ var labels = ec.labels || [], values = ec.values || [];
649
+ var n = Math.max(1, labels.length);
650
+ var rowH = Math.min(PT.hbar.rowH, (y1 - y0) / n);
651
+ var barH = Math.max(0.16, rowH * (PT.hbar.barRatio || 0.5));
652
+ var labelW = PT.hbar.labelW, valW = PT.hbar.valW;
653
+ var trackX = x0 + labelW + 0.1, trackW = w - labelW - valW - 0.2;
654
+ var vmax = ec.max || Math.max.apply(null, values.concat([1]));
655
+ var top = values.length ? Math.max.apply(null, values) : 0;
656
+ labels.forEach(function (lb, i) {
657
+ var v = values[i] || 0;
658
+ var y = y0 + i * rowH + (rowH - barH) / 2;
659
+ var hot = v === top;
660
+ var dc = dcols ? dcols[i % 5] : null;
661
+ sh.push(txSp(x0, y - 0.06, labelW - 0.05, barH + 0.12,
662
+ [[{ t: lb, sz: 11, col: S.body, font: S.font }]], { anchor: 'ctr' }));
663
+ sh.push(shape('rect', trackX, y, trackW, barH, S.surface, { line: S.line }));
664
+ var bw2 = Math.max(0.02, v / vmax * trackW);
665
+ sh.push(shape('rect', trackX, y, bw2, barH, dc || (hot ? S.accent : S.faint)));
666
+ sh.push(txSp(trackX + trackW + 0.08, y - 0.06, valW, barH + 0.12,
667
+ [[{ t: valText(v, ec.unit), sz: 11.5, b: 1, col: hot ? S.accent : S.body, font: S.font }]], { anchor: 'ctr' }));
668
+ });
669
+ }
670
+ /* 竖柱 / 堆叠柱 */
671
+ function vbarShapes(sh, ec, S, x, y, w, h, dcols) {
672
+ var labels = ec.labels || [], values = ec.values || [];
673
+ var nb = Math.max(1, labels.length);
674
+ var baseY = y + h, plotW = w, step = plotW / nb;
675
+ var bw = Math.min(1.1, step * 0.55);
676
+ var vmax = ec.max || Math.max.apply(null, values.concat([1]));
677
+ var top = values.length ? Math.max.apply(null, values) : 0;
678
+ sh.push(shape('rect', x, baseY, plotW, 0.02, S.line));
679
+ values.forEach(function (v, i) {
680
+ var hh = Math.max(0.05, v / vmax * (h - 0.5));
681
+ var bx = x + i * step + (step - bw) / 2;
682
+ var hot = v === top;
683
+ var dc = dcols ? dcols[i % 5] : null;
684
+ sh.push(shape('roundRect', bx, baseY - hh, bw, hh, dc || (hot ? S.accent : S.faint), { adj: 6000 }));
685
+ sh.push(txSp(bx - 0.3, baseY - hh - 0.4, bw + 0.6, 0.32,
686
+ [[{ t: valText(v, ec.unit), sz: 12.5, b: 1, col: hot ? S.accent : S.body, font: S.font }]], { align: 'ctr' }));
687
+ sh.push(txSp(bx - 0.3, baseY + 0.08, bw + 0.6, 0.32,
688
+ [[{ t: labels[i], sz: 10.5, col: S.faint, font: S.font }]], { align: 'ctr' }));
689
+ });
690
+ }
691
+ /* 折线 / 双折线 / 面积 */
692
+ function lineShapes(sh, ec, S, x, y, w, h, dcols) {
693
+ var labels = ec.labels || [];
694
+ var series = (ec.series && ec.series.length) ? ec.series : [{ name: '数值', values: ec.values || [] }];
695
+ var n = Math.max(1, labels.length);
696
+ var all = [];
697
+ series.forEach(function (se) { (se.values || []).forEach(function (v) { all.push(Number(v) || 0); }); });
698
+ var vmax = ec.max || Math.max.apply(null, all.concat([1]));
699
+ var plotH = h - 0.5, baseY = y + h;
700
+ sh.push(shape('rect', x, baseY, w, 0.02, S.line));
701
+ var stepX = n > 1 ? w / (n - 1) : 0;
702
+ series.forEach(function (se, si) {
703
+ var col = dcols ? dcols[si % 5] : (si === 0 ? S.accent : S.faint);
704
+ var pts = (se.values || []).map(function (v, i) {
705
+ var px = x + (n > 1 ? i * stepX : w / 2);
706
+ var py = baseY - Math.max(0.02, (Number(v) || 0) / vmax * plotH);
707
+ return [px, py];
708
+ });
709
+ if (!pts.length) return;
710
+ for (var i = 0; i < pts.length - 1; i++) {
711
+ sh.push(lineSeg(pts[i][0], pts[i][1], pts[i + 1][0], pts[i + 1][1], col, 2.5));
712
+ }
713
+ pts.forEach(function (p) {
714
+ sh.push(shape('ellipse', p[0] - 0.055, p[1] - 0.055, 0.11, 0.11, col));
715
+ });
716
+ });
717
+ labels.forEach(function (lb, i) {
718
+ var px = x + (n > 1 ? i * stepX : w / 2);
719
+ sh.push(txSp(px - 0.5, baseY + 0.08, 1.0, 0.32,
720
+ [[{ t: lb, sz: 10.5, col: S.faint, font: S.font }]], { align: 'ctr' }));
721
+ });
722
+ }
723
+ /* 直线段(用细长矩形近似,避免自定义几何——两通道渲染一致) */
724
+ function lineSeg(x1, y1, x2, y2, hex, thick) {
725
+ var dx = x2 - x1, dy = y2 - y1;
726
+ var len = Math.sqrt(dx * dx + dy * dy);
727
+ var ang = Math.atan2(dy, dx) * 180 / Math.PI;
728
+ var cx = (x1 + x2) / 2, cy = (y1 + y2) / 2;
729
+ var t = thick / 72;
730
+ var rot = '<a:xfrm rot="' + Math.round(ang * 60000) + '">';
731
+ return '<p:sp><p:nvSpPr><p:cNvPr id="' + nid() + '" name="ln"/><p:cNvSpPr/><p:nvPr/></p:nvSpPr>' +
732
+ '<p:spPr>' + rot + '<a:off x="' + E(cx - len / 2) + '" y="' + E(cy - t / 2) + '"/>' +
733
+ '<a:ext cx="' + E(len) + '" cy="' + E(t) + '"/></a:xfrm>' +
734
+ '<a:prstGeom prst="rect"><a:avLst/></a:prstGeom>' + solid(hex) + '<a:ln><a:noFill/></a:ln></p:spPr>' +
735
+ '<p:txBody><a:bodyPr/><a:lstStyle/><a:p/></p:txBody></p:sp>';
736
+ }
737
+ /* 散点 / 气泡(A 通道近似):只画点、不产文本——与 B 通道 XY 图表(类别为数值,过滤后为空)一致 */
738
+ function xyDots(sh, ec, S, x, y, w, h) {
739
+ var values = (ec.values || []).map(Number);
740
+ var pts = (ec.points && ec.points.length) ? ec.points : null;
741
+ var n = pts ? pts.length : Math.max(1, values.length);
742
+ var vmax = ec.max || Math.max.apply(null, values.concat([1]));
743
+ var baseY = y + h, plotH = Math.max(0.3, h - 0.3);
744
+ var stepX = n > 1 ? w / (n - 1) : 0;
745
+ sh.push(shape('rect', x, baseY, w, 0.02, S.line));
746
+ for (var i = 0; i < n; i++) {
747
+ var px = pts ? x + (Number(pts[i][0]) || 0) / vmax * w : x + i * stepX;
748
+ var py = pts ? baseY - (Number(pts[i][1]) || 0) / vmax * plotH : baseY - (values[i] || 0) / vmax * plotH;
749
+ sh.push(shape('ellipse', px - 0.07, py - 0.07, 0.14, 0.14, S.accent));
750
+ }
751
+ }
752
+ /* 仪表盘(A 通道近似):270° 弧段 + 数值(B 通道 gauge 的类别为数值,过滤后两端一致,故此处不产标签文本) */
753
+ function gaugeApprox(sh, ec, S, x, y, w, h) {
754
+ var v = Number((ec.values || [0])[0]) || 0;
755
+ var max = Number(ec.max) || 100;
756
+ var frac = Math.max(0, Math.min(1, v / max));
757
+ var r = Math.min(w, h) / 2 - 0.12;
758
+ var cx = x + w / 2, cy = y + h / 2;
759
+ var steps = 40, startA = Math.PI * 0.75, sweep = Math.PI * 1.5;
760
+ var px = null, py = null;
761
+ for (var i = 0; i <= steps; i++) {
762
+ var a = startA + sweep * (i / steps);
763
+ var qx = cx + r * Math.cos(a), qy = cy + r * Math.sin(a);
764
+ if (px !== null) sh.push(lineSeg(px, py, qx, qy, (i / steps) <= frac ? S.accent : S.line, 5));
765
+ px = qx; py = qy;
766
+ }
767
+ sh.push(txSp(x, cy - 0.2, w, 0.42,
768
+ [[{ t: String(v) + (ec.unit || ''), sz: 18, b: 1, col: S.accent, font: S.fontDisplay }]], { align: 'ctr', anchor: 'ctr' }));
769
+ }
770
+ /* 饼 / 环(A 通道近似):横向占比条 + 图例(类别标签为文本,数值由 cross_verify 过滤) */
771
+ function pieApprox(sh, ec, S, x, y, w, h, dcols) {
772
+ var labels = ec.labels || [], values = (ec.values || []).map(Number);
773
+ var total = values.reduce(function (a, b) { return a + b; }, 0) || 1;
774
+ var barH = Math.max(0.32, Math.min(0.62, h * 0.22));
775
+ var barY = y + h / 2 - barH / 2 - 0.2;
776
+ var cx0 = x;
777
+ labels.forEach(function (lb, i) {
778
+ var bw = (values[i] || 0) / total * w;
779
+ sh.push(shape('rect', cx0, barY, bw, barH,
780
+ dcols ? dcols[i % dcols.length] : (i === 0 ? S.accent : S.faint)));
781
+ cx0 += bw;
782
+ });
783
+ var lx = x;
784
+ labels.forEach(function (lb, i) {
785
+ sh.push(shape('ellipse', lx, barY + barH + 0.16, 0.1, 0.1,
786
+ dcols ? dcols[i % dcols.length] : (i === 0 ? S.accent : S.faint)));
787
+ sh.push(txSp(lx + 0.14, barY + barH + 0.12, 1.2, 0.24, [[{ t: lb, sz: 10.5, col: S.body, font: S.font }]]));
788
+ sh.push(txSp(lx + 1.36, barY + barH + 0.12, 0.8, 0.24,
789
+ [[{ t: String(values[i] != null ? values[i] : '') + (ec.unit || ''), sz: 10.5, b: 1, col: S.body, font: S.font }]]));
790
+ lx += 2.3;
791
+ });
792
+ }
793
+ /* 迷你折线(A 近似):无轴无标签,仅趋势线 + 末值(与 B 的 shapeSpark 同文本集) */
794
+ function sparkApprox(sh, ec, S, x, y, w, h) {
795
+ var vals = (ec.values || []).map(Number);
796
+ var vmax = ec.max || Math.max.apply(null, vals.concat([1]));
797
+ var n = vals.length;
798
+ var stepX = n > 1 ? w / (n - 1) : 0;
799
+ var baseY = y + h - 0.06, plotH = Math.max(0.2, h - 0.18);
800
+ var px = 0, py = 0;
801
+ vals.forEach(function (v, i) {
802
+ var cx = x + i * stepX, cy = baseY - Math.max(0.02, (v / vmax) * plotH);
803
+ if (i > 0) sh.push(lineSeg(px, py, cx, cy, S.accent, 2));
804
+ px = cx; py = cy;
805
+ });
806
+ sh.push(txSp(x, y, w, 0.3, [[{ t: String(vals[n - 1] != null ? vals[n - 1] : '') + (ec.unit || ''),
807
+ sz: 11.5, b: 1, col: S.accent, font: S.font }]], { align: 'r' }));
808
+ }
809
+ /* K 线(A 近似):实体 + 须,标签 + 收盘值(与 B 的 shapeCandlestick 同文本集) */
810
+ function candleApprox(sh, ec, S, x, y, w, h) {
811
+ var labels = ec.labels || [];
812
+ var rows = (ec.values || []).filter(function (r) { return r && r.length >= 4; })
813
+ .map(function (r) { return r.slice(0, 4).map(Number); });
814
+ var n = Math.max(1, rows.length);
815
+ var flat = [];
816
+ rows.forEach(function (r) { r.forEach(function (v) { if (!isNaN(v)) flat.push(v); }); });
817
+ var lo = flat.length ? Math.min.apply(null, flat) : 0;
818
+ var hi = flat.length ? Math.max.apply(null, flat) : 1;
819
+ var span = (hi - lo) || 1;
820
+ var axisH = 0.26, plotH = Math.max(0.5, h - axisH - 0.1);
821
+ var yFor = function (v) { return y + 0.06 + (1 - (v - lo) / span) * plotH; };
822
+ var step = w / n, bw = Math.min(0.42, step * 0.5);
823
+ sh.push(shape('rect', x, y + 0.06 + plotH, w, 0.015, S.line));
824
+ rows.forEach(function (r, i) {
825
+ var o = r[0], hiP = r[1], loP = r[2], cl = r[3];
826
+ var cxp = x + step * i + step / 2;
827
+ var up = cl >= o;
828
+ sh.push(shape('rect', cxp - 0.011, yFor(hiP), 0.022, Math.max(0.02, yFor(loP) - yFor(hiP)), S.faint));
829
+ sh.push(shape('rect', cxp - bw / 2, yFor(Math.max(o, cl)), bw,
830
+ Math.max(0.04, Math.abs(yFor(cl) - yFor(o))), up ? S.accent : S.line));
831
+ if (labels[i]) {
832
+ sh.push(txSp(cxp - step / 2, y + plotH + axisH - 0.06, step, 0.24,
833
+ [[{ t: String(labels[i]), sz: 10, col: S.faint, font: S.font }]], { align: 'ctr' }));
834
+ }
835
+ sh.push(txSp(cxp - step / 2, yFor(hiP) - 0.26, step, 0.24,
836
+ [[{ t: String(cl) + (ec.unit || ''), sz: 11.5, b: 1, col: up ? S.accent : S.body, font: S.font }]],
837
+ { align: 'r' }));
838
+ });
839
+ }
840
+ /* 通用形状近似(与 build_pptx.js 的 shapeProportion 同文本:标签 + 数值) */
841
+ function shapeProportion(sh, ec, S, x, y, w, h, dcols) {
842
+ var labels = ec.labels || [], values = (ec.values || []).map(Number);
843
+ var n = Math.max(1, labels.length);
844
+ var labelW = Math.min(2.4, w * 0.24), valW = 0.9;
845
+ var vmax = ec.max || Math.max.apply(null, values.concat([1]));
846
+ var top = values.length ? Math.max.apply(null, values) : 0;
847
+ var rowH = Math.max(0.22, Math.min(0.52, h / n));
848
+ var trackX = x + labelW + 0.08, trackW = w - labelW - valW - 0.2;
849
+ var barH = Math.max(0.12, rowH * 0.52);
850
+ labels.forEach(function (lb, i) {
851
+ var v = values[i] || 0, ry = y + i * rowH, hot = v === top;
852
+ sh.push(txSp(x, ry, labelW - 0.06, rowH, [[{ t: lb, sz: 11, col: S.body, font: S.font }]], { anchor: 'ctr' }));
853
+ sh.push(shape('rect', trackX, ry + (rowH - barH) / 2, trackW, barH, S.surface, { line: S.line }));
854
+ sh.push(shape('rect', trackX, ry + (rowH - barH) / 2, Math.max(0.02, Math.abs(v) / vmax * trackW), barH,
855
+ dcols ? dcols[i % dcols.length] : (hot ? S.accent : S.faint)));
856
+ sh.push(txSp(trackX + trackW + 0.06, ry, valW, rowH,
857
+ [[{ t: String(v) + (ec.unit || ''), sz: 11.5, b: 1, col: hot ? S.accent : S.body, font: S.font }]],
858
+ { align: 'right', anchor: 'ctr' }));
859
+ });
860
+ }
861
+ /* 数据表行(与 build_pptx.js 的 dataTableRows 同规则) */
862
+ function dataTableRows(ec) {
863
+ var labels = (ec.labels || []).map(String);
864
+ var unit = ec.unit ? '(' + ec.unit + ')' : '';
865
+ if (ec.series && ec.series.length) {
866
+ var out = [['项目'].concat(ec.series.map(function (se, i) { return se.name || ('系列' + (i + 1)); }))];
867
+ labels.forEach(function (lb, i) {
868
+ out.push([lb].concat(ec.series.map(function (se) {
869
+ var v = (se.values || [])[i]; return v == null ? '' : String(v);
870
+ })));
871
+ });
872
+ return out;
873
+ }
874
+ var out2 = [['项目', '数值' + unit]];
875
+ labels.forEach(function (lb, i) {
876
+ var v = (ec.values || [])[i];
877
+ out2.push([lb, v == null ? '' : String(v)]);
878
+ });
879
+ return out2;
880
+ }
881
+ /* 数据表策略(与 build_pptx.js 的 dataTableMode 同规则) */
882
+ function dataTableModeOf(ec) {
883
+ var t = (ec.type || 'bar').toLowerCase();
884
+ var reg = (typeof REG !== 'undefined' && REG && REG[t]) ? REG[t] : null;
885
+ var m = ec.dataTable || (reg && reg.dataTable) || 'notes';
886
+ return m === 'appendix' ? 'inline' : m;
887
+ }
888
+ /* 图表 + 数据表组合(与 build_pptx.js 的 chartBlock 同策略:inline 在图表下方附原生表格) */
889
+ function chartBlockShapes(sh, ec, S, x, y, w, h, dcols) {
890
+ var dm = dataTableModeOf(ec);
891
+ var chH = h;
892
+ if (dm === 'inline' && (ec.labels || []).length) {
893
+ var rowsData = dataTableRows(ec);
894
+ var n = rowsData.length;
895
+ var tH = Math.min(h * 0.44, Math.max(0.44, n * 0.22 + 0.06));
896
+ chH = Math.max(0.85, h - tH - 0.1);
897
+ var colW = rowsData[0].map(function () { return w / rowsData[0].length; });
898
+ sh.push(table(x, y + chH + 0.1, w, colW, rowsData, S, { availH: tH, maxRowH: 0.24, minRowH: 0.14 }));
899
+ }
900
+ chartShapes(sh, ec, S, x, y, w, chH, dcols);
901
+ }
902
+ /* ══ 复杂信息图页型(A 通道形状近似:文本集合与 B 通道严格一致,cross_verify 逐页比对)══ */
903
+ function infoTableRows(sec) {
904
+ var t = sec.type || '';
905
+ var u = sec.unit ? '(' + sec.unit + ')' : '';
906
+ if (t === 'sankey') return [['源', '汇', '流量' + u]].concat((sec.flows || []).map(function (f) { return [String(f[0]), String(f[1]), String(f[2])]; }));
907
+ if (t === 'treemap') return [['项目', '数值' + u]].concat((sec.items || []).map(function (it) { return [String(it[0]), String(it[1])]; }));
908
+ if (t === 'boxplot') return [['分组', '最小', 'Q1', '中位', 'Q3', '最大']].concat((sec.groups || []).map(function (g) { return g.map(String); }));
909
+ if (t === 'network') return [['节点', '名称']].concat((sec.nodes || []).map(function (n) { return [String(n[0]), String(n[1])]; }));
910
+ if (t === 'marimekko') {
911
+ var head = ['列', '总量' + u].concat((sec.legend || []).map(String));
912
+ return [head].concat((sec.cols || []).map(function (c, i) {
913
+ return [String(c[0]), String(c[1])].concat(((sec.cells || [])[i] || []).map(String));
914
+ }));
915
+ }
916
+ if (t === 'streamgraph') {
917
+ return [['系列'].concat((sec.labels || []).map(String))]
918
+ .concat((sec.series || []).map(function (se) {
919
+ return [String(se.name || '')].concat((se.values || []).map(String));
920
+ }));
921
+ }
922
+ return null;
923
+ }
924
+ function infoDataTableMode(sec) {
925
+ var m = String(((sec.chart || {}).dataTable) || 'notes').toLowerCase();
926
+ return m === 'appendix' ? 'inline' : m;
927
+ }
928
+ function infoPlotA(sec, y0, y1) {
929
+ var rows = infoTableRows(sec);
930
+ var inline = (infoDataTableMode(sec) === 'inline') && rows && rows.length;
931
+ if (!inline) return { y0: y0, y1: y1, rows: rows };
932
+ var tH = Math.min((y1 - y0) * 0.4, Math.max(0.44, rows.length * 0.22 + 0.06));
933
+ return { y0: y0, y1: y1 - tH - 0.1, rows: rows, table: { y: y1 - tH, h: tH } };
934
+ }
935
+ function tmSplitA(items, x, y, w, h, out) {
936
+ if (!items.length) return;
937
+ if (items.length === 1) { out.push({ it: items[0], x: x, y: y, w: w, h: h }); return; }
938
+ var total = items.reduce(function (a, i) { return a + i.v; }, 0) || 1;
939
+ var acc = 0, k = 0;
940
+ for (; k < items.length - 1; k++) { acc += items[k].v; if (acc >= total / 2) break; }
941
+ var first = items.slice(0, k + 1), second = items.slice(k + 1);
942
+ var r = first.reduce(function (a, i) { return a + i.v; }, 0) / total;
943
+ if (w >= h) {
944
+ tmSplitA(first, x, y, w * r, h, out);
945
+ tmSplitA(second, x + w * r, y, w * (1 - r), h, out);
946
+ } else {
947
+ tmSplitA(first, x, y, w, h * r, out);
948
+ tmSplitA(second, x, y + h * r, w, h * (1 - r), out);
949
+ }
950
+ }
951
+ /* 信息图近似渲染:按页型画形状 + 落同样的标签文本 */
952
+ function infoApprox(sh, sec, S, y0, y1) {
953
+ var t = sec.type || '';
954
+ var P = infoPlotA(sec, y0, y1);
955
+ if (P.table) {
956
+ var colW = P.rows[0].map(function () { return CW / P.rows[0].length; });
957
+ sh.push(table(MX, P.table.y, CW, colW, P.rows, S, { availH: P.table.h, maxRowH: 0.24, minRowH: 0.15 }));
958
+ }
959
+ var palette = [S.accent, S.faint, S.body, S.line];
960
+ if (t === 'sankey') {
961
+ var flows = (sec.flows || []).filter(function (f) { return f && f.length >= 3; });
962
+ var nodes = [], idx = {};
963
+ flows.forEach(function (f) {
964
+ [f[0], f[1]].forEach(function (n) { if (idx[n] == null) { idx[n] = nodes.length; nodes.push(String(n)); } });
965
+ });
966
+ var level = nodes.map(function () { return 0; });
967
+ for (var it2 = 0; it2 < nodes.length + 1; it2++) {
968
+ var ch = false;
969
+ flows.forEach(function (f) {
970
+ var a = idx[f[0]], b = idx[f[1]];
971
+ if (level[b] < level[a] + 1) { level[b] = level[a] + 1; ch = true; }
972
+ });
973
+ if (!ch) break;
974
+ }
975
+ var maxLv = Math.max.apply(null, level.concat([0]));
976
+ var colW2 = CW / (maxLv + 1);
977
+ var geo = {};
978
+ for (var l = 0; l <= maxLv; l++) {
979
+ var lv = nodes.filter(function (n, i) { return level[i] === l; });
980
+ var rowH2 = Math.max(0.24, (P.y1 - P.y0) / Math.max(1, lv.length));
981
+ lv.forEach(function (n, k2) { geo[n] = { x: MX + l * colW2 + colW2 * 0.5 - 0.16, y: P.y0 + k2 * rowH2, h: rowH2 * 0.7 }; });
982
+ }
983
+ flows.forEach(function (f) {
984
+ var a = geo[f[0]], b = geo[f[1]];
985
+ if (!a || !b) return;
986
+ sh.push(lineSeg(a.x + 0.32, a.y + a.h / 2, b.x, b.y + b.h / 2, S.soft, 8));
987
+ /* 流量标签:与 B 通道同文本(数值 + 单位,由 cross_verify 过滤) */
988
+ sh.push(txSp((a.x + b.x) / 2 - 0.5, (a.y + b.y) / 2 - 0.14, 1.0, 0.28,
989
+ [[{ t: String(f[2]) + (sec.unit || ''), sz: 9.5, col: S.faint, font: S.font }]], { align: 'ctr' }));
990
+ });
991
+ nodes.forEach(function (n) {
992
+ var g = geo[n];
993
+ sh.push(shape('rect', g.x, g.y, 0.32, g.h, S.accent));
994
+ sh.push(txSp(g.x + 0.38, g.y, Math.min(1.4, colW2 * 0.5), g.h,
995
+ [[{ t: n, sz: 10.5, b: 1, col: S.body, font: S.font }]], { anchor: 'ctr' }));
996
+ });
997
+ return;
998
+ }
999
+ if (t === 'treemap') {
1000
+ var items = (sec.items || []).map(function (it) { return { label: String(it[0]), v: Math.max(0, Number(it[1]) || 0) }; })
1001
+ .filter(function (i) { return i.v > 0; }).sort(function (a, b) { return b.v - a.v; });
1002
+ var out = [];
1003
+ tmSplitA(items, MX, P.y0, CW, P.y1 - P.y0, out);
1004
+ out.forEach(function (o) {
1005
+ sh.push(shape('rect', o.x + 0.03, o.y + 0.03, Math.max(0.05, o.w - 0.06), Math.max(0.05, o.h - 0.06), S.surface, { line: S.line }));
1006
+ sh.push(txSp(o.x + 0.1, o.y + 0.06, Math.max(0.2, o.w - 0.2), 0.28, [[{ t: o.it.label, sz: 10.5, b: 1, col: S.ink, font: S.font }]]));
1007
+ sh.push(txSp(o.x + 0.1, o.y + 0.32, Math.max(0.2, o.w - 0.2), 0.26,
1008
+ [[{ t: String(o.it.v) + (sec.unit || ''), sz: 10, col: S.body, font: S.font }]]));
1009
+ });
1010
+ return;
1011
+ }
1012
+ if (t === 'boxplot') {
1013
+ var groups = (sec.groups || []).filter(function (g) { return g && g.length >= 6; });
1014
+ var step = CW / Math.max(1, groups.length);
1015
+ var plotH = (P.y1 - P.y0) - 0.4;
1016
+ groups.forEach(function (g, i) {
1017
+ var cx = MX + step * i + step / 2;
1018
+ var bw = Math.min(0.9, step * 0.5);
1019
+ sh.push(shape('rect', cx - 0.012, P.y0, 0.024, plotH, S.faint));
1020
+ sh.push(shape('rect', cx - bw / 2, P.y0 + plotH * 0.2, bw, plotH * 0.6, S.soft, { line: S.accent }));
1021
+ sh.push(shape('rect', cx - bw / 2, P.y0 + plotH * 0.48, bw, 0.03, S.accent));
1022
+ sh.push(txSp(cx - step / 2, P.y1 - 0.34, step, 0.28,
1023
+ [[{ t: String(g[0]), sz: 10.5, col: S.body, font: S.font }]], { align: 'ctr' }));
1024
+ });
1025
+ return;
1026
+ }
1027
+ if (t === 'network') {
1028
+ var nds = sec.nodes || [];
1029
+ var n2 = Math.max(1, nds.length);
1030
+ var cx0 = MX + CW / 2, cy0 = (P.y0 + P.y1) / 2;
1031
+ var r = Math.min(CW / 2, (P.y1 - P.y0) / 2) - 1.0;
1032
+ var pos = {};
1033
+ nds.forEach(function (nd, i) {
1034
+ var a = -Math.PI / 2 + i * 2 * Math.PI / n2;
1035
+ pos[String(nd[0])] = { x: cx0 + r * Math.cos(a), y: cy0 + r * Math.sin(a) };
1036
+ });
1037
+ (sec.edges || []).forEach(function (e) {
1038
+ var a = pos[String(e[0])], b = pos[String(e[1])];
1039
+ if (!a || !b) return;
1040
+ sh.push(lineSeg(a.x, a.y, b.x, b.y, S.line, 1.5));
1041
+ });
1042
+ nds.forEach(function (nd) {
1043
+ var p = pos[String(nd[0])];
1044
+ sh.push(shape('ellipse', p.x - 0.26, p.y - 0.26, 0.52, 0.52, S.accent));
1045
+ var outward = p.x >= cx0;
1046
+ sh.push(txSp(outward ? p.x + 0.32 : p.x - 0.32 - 1.4, p.y - 0.14, 1.4, 0.28,
1047
+ [[{ t: String(nd[1] != null ? nd[1] : nd[0]), sz: 10, col: S.body, font: S.font }]],
1048
+ { align: outward ? 'l' : 'r', anchor: 'ctr' }));
1049
+ });
1050
+ return;
1051
+ }
1052
+ if (t === 'marimekko') {
1053
+ var cols = sec.cols || [];
1054
+ var legend = (sec.legend || []).map(String);
1055
+ var total = cols.reduce(function (a, c) { return a + (Number(c[1]) || 0); }, 0) || 1;
1056
+ var plotH2 = (P.y1 - P.y0) - 0.6;
1057
+ var availW = CW - 0.06 * Math.max(0, cols.length - 1);
1058
+ var cx1 = MX;
1059
+ cols.forEach(function (c, i) {
1060
+ var cw = Math.max(0.2, (Number(c[1]) || 0) / total * availW);
1061
+ var shares = ((sec.cells || [])[i] || []).map(Number);
1062
+ var sum = shares.reduce(function (a, b) { return a + b; }, 0) || 1;
1063
+ var cy = P.y0;
1064
+ shares.forEach(function (v, k) {
1065
+ var shh = Math.max(0.02, v / sum * plotH2);
1066
+ sh.push(shape('rect', cx1, cy, cw, shh, palette[k % palette.length]));
1067
+ cy += shh;
1068
+ });
1069
+ sh.push(txSp(cx1, P.y0 + plotH2 + 0.06, cw, 0.28,
1070
+ [[{ t: String(c[0]), sz: 10.5, b: 1, col: S.body, font: S.font }]], { align: 'ctr' }));
1071
+ cx1 += cw + 0.06;
1072
+ });
1073
+ var lx = MX;
1074
+ legend.forEach(function (lg, k) {
1075
+ sh.push(shape('rect', lx, P.y1 - 0.2, 0.12, 0.12, palette[k % palette.length]));
1076
+ sh.push(txSp(lx + 0.16, P.y1 - 0.26, 1.5, 0.26, [[{ t: lg, sz: 10, col: S.body, font: S.font }]], { anchor: 'ctr' }));
1077
+ lx += 1.75;
1078
+ });
1079
+ return;
1080
+ }
1081
+ if (t === 'streamgraph') {
1082
+ var series = sec.series || [];
1083
+ var labels = sec.labels || [];
1084
+ var nPts = labels.length || ((series[0] && series[0].values || []).length);
1085
+ if (nPts >= 2) {
1086
+ var values = series.map(function (se) {
1087
+ return (se.values || []).map(function (v) { return Math.max(0, Number(v) || 0); });
1088
+ });
1089
+ var totals = [];
1090
+ for (var i2 = 0; i2 < nPts; i2++) {
1091
+ var tt = 0;
1092
+ values.forEach(function (v) { tt += (v[i2] || 0); });
1093
+ totals.push(tt);
1094
+ }
1095
+ var maxTot = Math.max.apply(null, totals.concat([1]));
1096
+ var plotH3 = P.y1 - P.y0;
1097
+ var cyMid = (P.y0 + P.y1) / 2;
1098
+ var base = totals.map(function (tv) { return cyMid - tv / maxTot * plotH3 / 2; });
1099
+ series.forEach(function (se, si) {
1100
+ var upper = base.map(function (bv, i3) { return bv + (values[si][i3] || 0) / maxTot * plotH3 / 2; });
1101
+ var stepX = CW / (nPts - 1);
1102
+ for (var i4 = 0; i4 < nPts - 1; i4++) {
1103
+ var hgt = Math.max(0.04, ((upper[i4] - base[i4]) + (upper[i4 + 1] - base[i4 + 1])) / 2);
1104
+ sh.push(shape('rect', MX + i4 * stepX, (base[i4] + base[i4 + 1]) / 2, stepX, hgt, palette[si % palette.length]));
1105
+ }
1106
+ base = upper;
1107
+ sh.push(shape('rect', MX, P.y1 + 0.06 + si * 0.26, 0.12, 0.12, palette[si % palette.length]));
1108
+ sh.push(txSp(MX + 0.16, P.y1 + si * 0.26, 1.5, 0.26,
1109
+ [[{ t: String(se.name || ('系列' + (si + 1))), sz: 10, col: S.body, font: S.font }]], { anchor: 'ctr' }));
1110
+ });
1111
+ labels.forEach(function (lb, i5) {
1112
+ sh.push(txSp(MX + (nPts > 1 ? i5 / (nPts - 1) : 0.5) * CW - 0.4, P.y1 - 0.02, 0.8, 0.24,
1113
+ [[{ t: String(lb), sz: 9.5, col: S.faint, font: S.font }]], { align: 'ctr' }));
1114
+ });
1115
+ }
1116
+ return;
1117
+ }
1118
+ }
1119
+ /* 统一图表分发(与 scripts/layout-constants.json 的 charts.registry 同分派规则)
1120
+ * A 通道为形状近似:类别标签与数值都落为文本,数值 token 由 cross_verify 过滤;
1121
+ * 复杂信息图(sankey / treemap / boxplot / network / marimekko / streamgraph)请用专属页型承载。 */
1122
+ function chartShapes(sh, ec, S, x, y, w, h, dcols) {
1123
+ var t = (ec.type || 'bar').toLowerCase();
1124
+ var reg = (typeof REG !== 'undefined' && REG && REG[t]) ? REG[t] : null;
1125
+ if (t === 'hbar') { hbarRows(sh, ec, S, y, y + h, dcols, x, w); return; }
1126
+ if (t === 'line' || t === 'dualline' || t === 'area') { lineShapes(sh, ec, S, x, y, w, h, dcols); return; }
1127
+ if (t === 'scatter' || t === 'bubble') { xyDots(sh, ec, S, x, y, w, h); return; }
1128
+ if (t === 'gauge') { gaugeApprox(sh, ec, S, x, y, w, h); return; }
1129
+ if (t === 'pie' || t === 'donut' || t === 'multidonut') { pieApprox(sh, ec, S, x, y, w, h, dcols); return; }
1130
+ /* sparkline / candlestick 的文本集须与 B 通道专属渲染器一致(否则 cross_verify 误报) */
1131
+ if (t === 'sparkline') { sparkApprox(sh, ec, S, x, y, w, h); return; }
1132
+ if (t === 'candlestick') { candleApprox(sh, ec, S, x, y, w, h); return; }
1133
+ if (reg && reg.pptx === 'shape') { shapeProportion(sh, ec, S, x, y, w, h, dcols); return; }
1134
+ vbarShapes(sh, ec, S, x, y, w, h, dcols);
1135
+ }
1136
+
1137
+ /* 区域 IR 取用(与 scripts/lib_layout_regions.js 同源;未注入时回落 null) */
1138
+ function regOf(type, slot, opts) {
1139
+ return (typeof regionOf === 'function') ? regionOf(type, slot, opts) : null;
1140
+ }
1141
+
1142
+ /* ── 幻灯片生成(版式常量同 build_pptx.js · 同一 layout-constants.json) ── */
1143
+ function slidesOf(model, S) {
1144
+ var slides = [];
1145
+ _mode = model.mode || 'presentation';
1146
+ function head(sh, eyebrow, title, lead) {
1147
+ var H = (typeof regionOf === 'function') ? regionOf('head', 'head') : {
1148
+ x: MX, y: PT.common.headEyebrowY, w: CW,
1149
+ titleY: PT.common.headTitleY, ruleY: PT.common.headRuleY, leadY: PT.common.leadY,
1150
+ };
1151
+ if (eyebrow) sh.push(txSp(H.x, H.y, H.w, 0.4, [[{ t: eyebrow, sz: 13, b: 1, col: S.accent, font: S.font, spc: 200 }]]));
1152
+ var titleH = eyebrow ? (H.ruleY - H.titleY - 0.06) : 1.0;
1153
+ var tSize = estTextH(title, CW, 30, 1.25) > titleH ? 24 : 30;
1154
+ sh.push(txSp(H.x, eyebrow ? H.titleY : 0.6, H.w, Math.max(0.6, titleH), [[{ t: title, sz: tSize, b: 1, col: S.ink, font: S.fontDisplay }]]));
1155
+ sh.push(shape('rect', H.x, eyebrow ? H.ruleY : 1.6, 0.9, 0.045, S.accent));
1156
+ if (lead) sh.push(txSp(H.x, H.leadY || PT.common.leadY, H.w, 0.5, [[{ t: lead, sz: 14, col: S.body, font: S.font }]]));
1157
+ }
1158
+
1159
+ /* 封面 */
1160
+ var CV = PT.cover;
1161
+ var c = [shape('rect', 0, 0, PW, PH, S.accent)];
1162
+ c.push(txSp(MX, CV.metaY, CW, 0.4, [[{ t: model.meta || '', sz: 12, col: S.soft, font: S.font }]]));
1163
+ var cTSize = estTextH(model.title, CW - 1, 44, 1.25) > CV.titleH ? 36 : 44;
1164
+ c.push(txSp(MX, CV.titleY, CW - 1, CV.titleH, [[{ t: model.title, sz: cTSize, b: 1, col: S.onAccent, font: S.fontDisplay }]]));
1165
+ c.push(txSp(MX, CV.subtitleY, CW - 1, CV.subtitleH, [[{ t: model.subtitle || '', sz: 18, col: S.soft, font: S.font }]]));
1166
+ slides.push(c);
1167
+
1168
+ /* Agenda(>8 条自动双列,行高自适应防溢出;architecture 极简形态可省略 agenda) */
1169
+ var items = model.agenda || [];
1170
+ if (items.length) {
1171
+ var ag = [];
1172
+ head(ag, 'AGENDA', '报告大纲');
1173
+ var colN = items.length > 8 ? 2 : 1;
1174
+ var perCol = Math.ceil(items.length / colN) || 1;
1175
+ var rowH = Math.min(1.05, (CONTENT_BOTTOM - CONTENT_TOP) / perCol);
1176
+ var numSz = rowH >= 0.85 ? 30 : (rowH >= 0.65 ? 22 : 18);
1177
+ var tSz = rowH >= 0.85 ? 16 : 14, dSz = rowH >= 0.85 ? 12 : 11;
1178
+ items.forEach(function (it, i) {
1179
+ var col = Math.floor(i / perCol), row = i % perCol;
1180
+ var x = MX + col * (CW / colN), y = CONTENT_TOP + row * rowH;
1181
+ ag.push(txSp(x, y, 0.9, Math.min(0.9, rowH), [[{ t: it[0], sz: numSz, b: 1, col: S.accent, font: S.font }]]));
1182
+ ag.push(txSp(x + 1.0, y + 0.03, CW / colN - 1.1, Math.min(0.95, rowH),
1183
+ [[{ t: it[1], sz: tSz, b: 1, col: S.ink, font: S.font }],
1184
+ [{ t: it[2] || '', sz: dSz, col: S.body, font: S.font }]], { lineSpacing: tSz + 5 }));
1185
+ });
1186
+ slides.push(ag);
1187
+ }
1188
+
1189
+ /* 章节页 */
1190
+ (model.sections || []).forEach(function (sec) {
1191
+ var type = sec.type || 'points';
1192
+ var sh = [];
1193
+ if (type !== 'quote') head(sh, sec.eyebrow, sec.title, sec.lead);
1194
+ /* 非 exhibit 页型也可带 Exhibit 编号(帧头徽标;正文区下移让位,与 B 通道同规则) */
1195
+ var exBadge = (type !== 'exhibit' && sec.exhibitNo) ? String(sec.exhibitNo).toUpperCase() : null;
1196
+ var bodyY0 = sec.lead ? PT.common.bodyYWithLead : CONTENT_TOP;
1197
+ var bodyY = exBadge ? bodyY0 + 0.42 : bodyY0;
1198
+ if (exBadge) {
1199
+ sh.push(txSp(MX, CONTENT_TOP - 0.06, CW, 0.3,
1200
+ [[{ t: exBadge, sz: 11.5, b: 1, col: S.accent, font: S.font, spc: 200 }]]));
1201
+ }
1202
+ var flagItems = Array.isArray(sec.flags) ? sec.flags.filter(Boolean) : [];
1203
+ var _F = PT.flagbar || { hdH: 0.26, rowH: 0.24, maxRows: 3, gap: 0.12 };
1204
+ var flagH = flagItems.length
1205
+ ? (_F.hdH + Math.min(_F.maxRows, flagItems.length) * _F.rowH + 0.06) : 0;
1206
+ var bodyBottom = chartBottom(!!sec.soWhat, !!sec.footnote);
1207
+ var flagY = 0;
1208
+ if (flagH) {
1209
+ flagY = CONTENT_BOTTOM - flagH;
1210
+ if (sec.footnote) flagY = Math.min(flagY, PT.exhibit.footnoteY - flagH - 0.06);
1211
+ if (sec.soWhat) flagY = Math.min(flagY, PT.exhibit.soWhatY - flagH - 0.06);
1212
+ bodyBottom = Math.min(bodyBottom, flagY - (_F.gap || 0.12));
1213
+ }
1214
+
1215
+ if (type === 'metrics') {
1216
+ var M = PT.metrics, ms = sec.metrics || [], n = Math.max(1, ms.length);
1217
+ var mReg = regOf('metrics', 'primary') || { x: MX, y: M.bandY, w: CW, h: M.cardH };
1218
+ var g = cols(mReg.w, n, M.gap);
1219
+ var mw = Math.min(M.maxW, g.w);
1220
+ var gx = mReg.x + (mReg.w - n * mw - (n - 1) * M.gap) / 2;
1221
+ var cardH = Math.min(mReg.h, CONTENT_BOTTOM - mReg.y);
1222
+ ms.forEach(function (m, i) {
1223
+ var x = gx + i * (mw + M.gap);
1224
+ sh.push(shape('roundRect', x, mReg.y, mw, cardH, S.surface, { line: S.line, adj: 4000 }));
1225
+ var vSize = estTextH(m[0], mw - 0.2, 40, 1.1) > cardH * 0.5 ? 30 : 40;
1226
+ sh.push(txSp(x, M.valY, mw, M.valH, [[{ t: m[0], sz: vSize, b: 1, col: S.accent, font: S.fontDisplay }]], { align: 'ctr' }));
1227
+ sh.push(txSp(x + 0.15, M.capY, mw - 0.3, M.capH, [[{ t: m[1], sz: 11.5, col: S.body, font: S.font }]], { align: 'ctr' }));
1228
+ });
1229
+ } else if (type === 'kpi') {
1230
+ var K = PT.kpi, hro = sec.hero || [];
1231
+ var hReg = regOf('kpi', 'hero') || { x: MX, y: K.heroY, w: CW * K.heroW, h: K.heroH };
1232
+ sh.push(txSp(hReg.x, hReg.y, hReg.w, hReg.h, [[{ t: String(hro[0] || ''), sz: 64, b: 1, col: S.accent, font: S.fontDisplay }]]));
1233
+ sh.push(txSp(hReg.x + 0.05, hReg.y + 1.5, hReg.w - 0.4, 0.55, [[{ t: String(hro[1] || ''), sz: 15, b: 1, col: S.ink, font: S.font }]]));
1234
+ if (hro[2]) sh.push(txSp(hReg.x + 0.05, hReg.y + 2.1, hReg.w - 0.4, 0.4, [[{ t: '▲ ' + hro[2], sz: 12, b: 1, col: S.accent, font: S.font }]]));
1235
+ var kdivX = MX + CW * K.dividerX;
1236
+ sh.push(shape('rect', kdivX, K.heroY + 0.15, 0.025, 3.1, S.line));
1237
+ var mRegK = regOf('kpi', 'metrics') || { x: kdivX + 0.35, y: K.metricY0, w: CW * (1 - K.dividerX) - 0.5 };
1238
+ var kmets = sec.metrics || [];
1239
+ var kCap = Math.max(1, Math.floor((CONTENT_BOTTOM - mRegK.y) / K.metricRowH));
1240
+ var kRows = Math.min(kmets.length, kCap);
1241
+ var kRowH = fitRowH(CONTENT_BOTTOM - mRegK.y, kRows, K.metricRowH, 0.42);
1242
+ var kTopH = Math.min(0.55, kRowH * 0.62);
1243
+ kmets.slice(0, kRows).forEach(function (m, i) {
1244
+ var ky = mRegK.y + i * kRowH;
1245
+ sh.push(txSp(mRegK.x, ky, mRegK.w, kTopH, [[{ t: m[0], sz: 22, b: 1, col: S.accent, font: S.fontDisplay }]]));
1246
+ sh.push(txSp(mRegK.x, ky + kTopH, mRegK.w, Math.max(0.24, kRowH - kTopH), [[{ t: m[1], sz: 10.5, col: S.faint, font: S.font }]]));
1247
+ });
1248
+ } else if (type === 'comparison') {
1249
+ var C2 = PT.comparison;
1250
+ var lR = regOf('comparison', 'left') || { x: MX, y: C2.panelY, w: (CW - C2.panelGap) / 2, h: C2.panelH };
1251
+ var rR = regOf('comparison', 'right') || { x: MX + (CW + C2.panelGap) / 2, y: C2.panelY, w: (CW - C2.panelGap) / 2, h: C2.panelH };
1252
+ var cw2 = lR.w, cpy = lR.y, cph = lR.h, rx2 = rR.x;
1253
+ sh.push(shape('roundRect', lR.x, cpy, cw2, cph, S.surface, { adj: 4000, line: S.line }));
1254
+ sh.push(shape('roundRect', rx2, cpy, cw2, cph, S.soft, { adj: 4000 }));
1255
+ [{ x: lR.x, d: (sec.left || {}) }, { x: rx2, d: (sec.right || {}) }].forEach(function (sd) {
1256
+ var isRight = sd.x === rx2;
1257
+ var pts = sd.d.points || [];
1258
+ var avail = cph - C2.titleH - 0.34;
1259
+ var rowH2 = fitRowH(avail, pts.length, C2.rowH, 0.34);
1260
+ var fz = rowH2 < 0.5 ? 11.5 : 13;
1261
+ sh.push(txSp(sd.x + 0.22, cpy + 0.18, cw2 - 0.44, C2.titleH,
1262
+ [[{ t: sd.d.title || '', sz: 15, b: 1, col: isRight ? S.accent : S.ink, font: S.font }]]));
1263
+ pts.forEach(function (p, pi) {
1264
+ var py = cpy + 0.18 + C2.titleH + 0.16 + pi * rowH2;
1265
+ sh.push(shape('rect', sd.x + 0.24, py + rowH2 / 2 - 0.06, 0.12, 0.12, isRight ? S.accent : S.faint));
1266
+ sh.push(txSp(sd.x + 0.5, py, cw2 - 0.75, rowH2 - 0.04,
1267
+ [[{ t: p[0] + ' ', sz: fz, b: 1, col: S.ink, font: S.font },
1268
+ { t: p[1], sz: fz - 1, col: S.body, font: S.font }]]));
1269
+ });
1270
+ });
1271
+ if (sec.verdict) {
1272
+ var vR = regOf('exhibit', 'annotation') || { x: MX, y: PT.exhibit.soWhatY, w: CW, h: 0.62 };
1273
+ sh.push(shape('rect', vR.x, vR.y, vR.w, vR.h, S.accent));
1274
+ sh.push(txSp(vR.x + 0.22, vR.y + 0.06, vR.w - 0.44, vR.h - 0.12,
1275
+ [[{ t: '结论 ', sz: 11, b: 1, col: S.onAccent, font: S.font, spc: 150 },
1276
+ { t: sec.verdict, sz: 12.5, b: 1, col: S.onAccent, font: S.font }]], { anchor: 'ctr' }));
1277
+ }
1278
+ } else if (type === 'quote') {
1279
+ var Q = PT.quote;
1280
+ var qR = regOf('quote', 'primary') || { x: MX + 1.05, y: Q.textY, w: CW - 2.1, h: Q.textH };
1281
+ /* 金句页用主题一致强调带(accent-soft 底)——浅色主题下不再出现深色页 */
1282
+ sh.push(shape('rect', 0, 0, PW, PH, S.soft));
1283
+ if (sec.eyebrow) sh.push(txSp(MX, PT.common.headEyebrowY, CW, 0.4, [[{ t: sec.eyebrow, sz: 13, b: 1, col: S.accent, font: S.font, spc: 200 }]]));
1284
+ sh.push(txSp(MX, PT.common.headTitleY, CW, 1.0, [[{ t: sec.title, sz: 30, b: 1, col: S.ink, font: S.fontDisplay }]]));
1285
+ sh.push(shape('rect', MX, PT.common.headRuleY, 0.9, 0.045, S.accent));
1286
+ sh.push(txSp(MX + 0.05, Q.markY, 1.2, 1.1, [[{ t: '「', sz: 60, b: 1, col: S.accent, font: S.fontDisplay }]]));
1287
+ var qSize = estTextH(sec.quote, qR.w, 20, 1.4) > qR.h ? 16 : 20;
1288
+ sh.push(txSp(qR.x, qR.y, qR.w, qR.h, [[{ t: sec.quote, sz: qSize, b: 1, col: S.ink, font: S.fontDisplay }]], { anchor: 'ctr' }));
1289
+ sh.push(txSp(qR.x, Q.authorY, qR.w, 0.45, [[{ t: '—— ' + sec.author, sz: 13, b: 1, col: S.accent, font: S.font }]], { align: 'r' }));
1290
+ if (sec.context) sh.push(txSp(qR.x, Q.contextY, qR.w, 0.4, [[{ t: sec.context, sz: 11, col: S.faint, font: S.font }]], { align: 'r' }));
1291
+ } else if (type === 'image') {
1292
+ /* 素材图片页:full 版心全宽 / half 左图右注 / bleed 通栏出血 / grid 多图网格 /
1293
+ compare 双图对比 / wall Logo 墙;无图(image.placeholder)走原生占位块 + 标签文本 */
1294
+ var IM = PT.image, img = sec.image || {};
1295
+ var items = Array.isArray(img.items) ? img.items.filter(Boolean) : [];
1296
+ var layout = String(img.layout || (items.length > 1 ? 'grid' : 'full')).toLowerCase();
1297
+ var fit = String(img.fit || ((typeof IMG !== 'undefined' && IMG && IMG.fitDefault) || 'cover')).toLowerCase();
1298
+ var isPh = !!img.placeholder;
1299
+ var iR = regOf('image', 'primary', { top: bodyY, caption: !!img.caption }) ||
1300
+ { x: MX, y: Math.max(IM.y, bodyY), w: CW, h: IM.h };
1301
+ var iy = iR.y;
1302
+ var capH2 = img.caption ? 0.32 : 0;
1303
+ var ih = Math.max(1.2, iR.h);
1304
+ imageLayoutShapes(sh, S, layout, img, items, sec.points || [], isPh, fit, iy, iy + ih);
1305
+ if (img.caption) sh.push(txSp(MX, iy + ih + 0.04, CW, capH2, [[{ t: img.caption, sz: 10.5, col: S.faint, font: S.font }]]));
1306
+ } else if (type === 'donut') {
1307
+ var dcn = sec.chart || {};
1308
+ if (dcn.labels && dcn.values) donutChart(sh, dcn, S, dcn.colors || donutColors(model.style, S, model.theme));
1309
+ if (sec.note) sh.push(txSp(MX, PT.note.y, CW, PT.note.h, [[{ t: sec.note, sz: 11, col: S.faint, font: S.font }]]));
1310
+ } else if (type === 'heatmap') {
1311
+ var H = PT.heatmap;
1312
+ var hR = regOf('heatmap', 'primary', { bottom: bodyBottom }) || { x: MX, y: H.y, w: CW, h: bodyBottom - H.y };
1313
+ var rowsH = sec.rowHeads || [], colsH = sec.colHeads || [], cells = sec.cells || [];
1314
+ var nR = Math.max(1, rowsH.length), nC = Math.max(1, colsH.length);
1315
+ var availH = hR.h;
1316
+ var rowH3 = fitRowH(availH - H.headH, nR, H.cellH, 0.24);
1317
+ var gridW = hR.w - H.labelW;
1318
+ var cw3 = (gridW - (nC - 1) * H.cellGap) / nC;
1319
+ var flat = [], k2;
1320
+ for (k2 = 0; k2 < cells.length; k2++) for (var j2 = 0; j2 < (cells[k2] || []).length; j2++) { var nv = Number(cells[k2][j2]); if (!isNaN(nv)) flat.push(nv); }
1321
+ var vmax3 = flat.length ? Math.max.apply(null, flat) : 1;
1322
+ var vmin3 = flat.length ? Math.min.apply(null, flat) : 0;
1323
+ var levels = [S.surface, S.soft, S.line, S.accent];
1324
+ colsH.forEach(function (ch2, ci) {
1325
+ sh.push(txSp(hR.x + H.labelW + ci * (cw3 + H.cellGap), hR.y, cw3, H.headH,
1326
+ [[{ t: ch2, sz: 11, b: 1, col: S.body, font: S.font }]], { align: 'ctr', anchor: 'ctr' }));
1327
+ });
1328
+ var rowsUse = Math.min(nR, cells.length);
1329
+ for (var ri = 0; ri < rowsUse; ri++) {
1330
+ var yy = hR.y + H.headH + 0.08 + ri * (rowH3 + H.cellGap);
1331
+ sh.push(txSp(hR.x, yy, H.labelW - 0.15, rowH3, [[{ t: rowsH[ri] || '', sz: 11, b: 1, col: S.body, font: S.font }]], { align: 'r', anchor: 'ctr' }));
1332
+ for (var ci2 = 0; ci2 < nC; ci2++) {
1333
+ var v2 = Number((cells[ri] || [])[ci2]);
1334
+ var has2 = !isNaN(v2);
1335
+ var p2 = (has2 && vmax3 > vmin3) ? (v2 - vmin3) / (vmax3 - vmin3) : 0.5;
1336
+ var lvl = p2 >= 0.86 ? 3 : (p2 >= 0.55 ? 2 : (p2 >= 0.25 ? 1 : 0));
1337
+ var cx2 = hR.x + H.labelW + ci2 * (cw3 + H.cellGap);
1338
+ sh.push(shape('roundRect', cx2, yy, cw3, rowH3, levels[lvl],
1339
+ { adj: 3000, line: lvl === 0 ? S.line : undefined }));
1340
+ sh.push(txSp(cx2, yy, cw3, rowH3,
1341
+ [[{ t: has2 ? valText(v2, sec.unit) : '—', sz: 11.5, b: lvl >= 2 ? 1 : 0,
1342
+ col: lvl >= 3 ? S.onAccent : S.body, font: S.font }]], { align: 'ctr', anchor: 'ctr' }));
1343
+ }
1344
+ }
1345
+ var lg = sec.scaleLabel || ['低', '高'];
1346
+ var lgy = hR.y + H.headH + 0.08 + rowsUse * (rowH3 + H.cellGap) + 0.06;
1347
+ sh.push(txSp(hR.x, lgy, 0.6, 0.3, [[{ t: lg[0] || '低', sz: 9.5, col: S.faint, font: S.font }]]));
1348
+ for (var li2 = 0; li2 < 4; li2++) {
1349
+ sh.push(shape('rect', hR.x + 0.45 + li2 * 0.3, lgy + 0.04, 0.28, 0.16, levels[li2],
1350
+ { line: li2 === 0 ? S.line : undefined }));
1351
+ }
1352
+ sh.push(txSp(hR.x + 1.75, lgy, 0.6, 0.3, [[{ t: lg[1] || '高', sz: 9.5, col: S.faint, font: S.font }]]));
1353
+ } else if (type === 'bullet') {
1354
+ var B = PT.bullet, its = sec.items || [], nB = Math.max(1, its.length);
1355
+ var bR = regOf('bullet', 'primary', { bottom: bodyBottom }) || { x: MX, y: B.y, w: CW, h: bodyBottom - B.y };
1356
+ var availB = bR.h;
1357
+ var rowHB = fitRowH(availB, nB, B.rowH, 0.26);
1358
+ var trackX = bR.x + B.labelW + 0.15;
1359
+ var trackW = bR.w - B.labelW - B.valW - 0.3;
1360
+ var nums = [], q2;
1361
+ for (q2 = 0; q2 < its.length; q2++) { var nq = Number(its[q2][1]); if (!isNaN(nq)) nums.push(nq); }
1362
+ var vmaxB = sec.max || (nums.length ? Math.max.apply(null, nums) : 1);
1363
+ its.forEach(function (it, i) {
1364
+ var yy2 = bR.y + i * rowHB + rowHB / 2 - B.barH / 2;
1365
+ var v = Number(it[1]) || 0, tgt = Number(it[2]);
1366
+ var hit = !isNaN(tgt) && v >= tgt;
1367
+ sh.push(txSp(bR.x, bR.y + i * rowHB, B.labelW, rowHB, [[{ t: String(it[0]), sz: 12, b: 1, col: S.ink, font: S.font }]], { anchor: 'ctr' }));
1368
+ sh.push(shape('roundRect', trackX, yy2, trackW, B.barH, S.surface, { adj: 50000, line: S.line }));
1369
+ var fw = Math.max(0.02, Math.min(1, v / (vmaxB || 1)) * trackW);
1370
+ sh.push(shape('roundRect', trackX, yy2, fw, B.barH, hit ? S.accent : S.faint, { adj: 50000 }));
1371
+ if (!isNaN(tgt)) {
1372
+ var tx2 = trackX + Math.min(1, tgt / (vmaxB || 1)) * trackW;
1373
+ sh.push(shape('rect', tx2 - 0.01, yy2 - 0.07, 0.02, B.barH + 0.14, S.body));
1374
+ }
1375
+ sh.push(txSp(trackX + trackW + 0.12, bR.y + i * rowHB, B.valW, rowHB,
1376
+ [[{ t: String(it[1]) + (sec.unit || ''), sz: 12, b: 1, col: hit ? S.accent : S.body, font: S.font },
1377
+ { t: isNaN(tgt) ? '' : ' / ' + tgt + (sec.unit || ''), sz: 10, col: S.faint, font: S.font }]],
1378
+ { align: 'r', anchor: 'ctr' }));
1379
+ });
1380
+ } else if (type === 'pyramid') {
1381
+ var P = PT.pyramid, lv = sec.levels || [], nP = Math.max(1, lv.length);
1382
+ var pR = regOf('pyramid', 'primary', { bottom: bodyBottom }) || { x: MX, y: P.y, w: CW, h: bodyBottom - P.y };
1383
+ var availP = pR.h;
1384
+ var rowHP = fitRowH(availP - (nP - 1) * P.gap, nP, P.rowH, 0.26);
1385
+ lv.forEach(function (l, i) {
1386
+ var frac = P.minW + (1 - P.minW) * (nP === 1 ? 1 : i / (nP - 1));
1387
+ var w2 = pR.w * frac, x2 = pR.x + (pR.w - w2) / 2, y2 = pR.y + i * (rowHP + P.gap);
1388
+ var acc = isRec(l) && l.accent;
1389
+ var lt = isRec(l) ? (l.t || '') : String(l[0] || '');
1390
+ var ld = isRec(l) ? (l.d || '') : String(l[1] || '');
1391
+ sh.push(shape('roundRect', x2, y2, w2, rowHP, acc ? S.soft : S.surface, { adj: 5000, line: acc ? undefined : S.line }));
1392
+ var lwP = Math.min(P.labelW, w2 - 0.44);
1393
+ sh.push(txSp(x2 + 0.22, y2, lwP, rowHP, [[{ t: lt, sz: 13.5, b: 1, col: acc ? S.accent : S.ink, font: S.font }]], { anchor: 'ctr' }));
1394
+ if (ld) sh.push(txSp(x2 + 0.22 + lwP, y2, w2 - 0.44 - lwP, rowHP, [[{ t: ld, sz: 11, col: S.faint, font: S.font }]], { anchor: 'ctr' }));
1395
+ });
1396
+ } else if (type === 'steps') {
1397
+ var S2 = PT.steps, st = sec.steps || [], maxPer = S2.maxPerRow || 6, groups = sec.groups || [];
1398
+ var sR = regOf('steps', 'primary') || { x: MX, y: S2.y, w: CW, h: S2.barH };
1399
+ var chunks = [];
1400
+ if (groups.length) {
1401
+ var kk = 0;
1402
+ groups.forEach(function (gp) { var cnt = Number(gp[1]) || 0; chunks.push({ label: gp[0], items: st.slice(kk, kk + cnt) }); kk += cnt; });
1403
+ if (kk < st.length) chunks.push({ label: '', items: st.slice(kk) });
1404
+ } else {
1405
+ for (var i3 = 0; i3 < st.length; i3 += maxPer) chunks.push({ label: '', items: st.slice(i3, i3 + maxPer) });
1406
+ }
1407
+ var rowsN = Math.max(1, chunks.length);
1408
+ var availS = bodyBottom - sR.y;
1409
+ var rowHS = fitRowH(availS - (rowsN - 1) * 0.3, rowsN, sR.h || S2.barH, 0.3);
1410
+ var cyS = sR.y;
1411
+ chunks.forEach(function (ck) {
1412
+ if (ck.label) {
1413
+ sh.push(txSp(sR.x, cyS - 0.02, sR.w, 0.26, [[{ t: ck.label, sz: 10, b: 1, col: S.faint, font: S.font, spc: 100 }]]));
1414
+ cyS += 0.28;
1415
+ }
1416
+ var n4 = Math.max(1, ck.items.length);
1417
+ var g4 = cols(sR.w, n4, S2.gap + S2.arrowW);
1418
+ ck.items.forEach(function (it, i) {
1419
+ var acc = isRec(it) && it.accent;
1420
+ var t4 = isRec(it) ? (it.t || '') : String(it[0] || '');
1421
+ var d4 = isRec(it) ? (it.d || '') : String(it[1] || '');
1422
+ var x4 = sR.x + i * g4.step;
1423
+ sh.push(shape('roundRect', x4, cyS, g4.w, rowHS, acc ? S.soft : S.surface, { adj: 5000, line: acc ? undefined : S.line }));
1424
+ var num = String(i + 1); if (num.length < 2) num = '0' + num;
1425
+ var pd = S2.pad, numH = S2.numH, ttlH = S2.titleH;
1426
+ sh.push(txSp(x4 + 0.14, cyS + pd, g4.w - 0.28, numH, [[{ t: num, sz: 10, b: 1, col: S.accent, font: S.font }]]));
1427
+ sh.push(txSp(x4 + 0.14, cyS + pd + numH, g4.w - 0.28, ttlH, [[{ t: t4, sz: 13, b: 1, col: acc ? S.accent : S.ink, font: S.font }]]));
1428
+ if (d4) sh.push(txSp(x4 + 0.14, cyS + pd + numH + ttlH, g4.w - 0.28, Math.max(0.2, rowHS - pd * 2 - numH - ttlH), [[{ t: d4, sz: 10, col: S.faint, font: S.font }]]));
1429
+ if (i < n4 - 1) {
1430
+ sh.push(txSp(x4 + g4.w, cyS, S2.arrowW + S2.gap, rowHS, [[{ t: '→', sz: 12, col: S.faint, font: S.font }]], { align: 'ctr', anchor: 'ctr' }));
1431
+ }
1432
+ });
1433
+ cyS += rowHS + 0.3;
1434
+ });
1435
+ } else if (type === 'table') {
1436
+ var tR = regOf('table', 'primary', { bottom: bodyBottom }) || { x: MX, y: CONTENT_TOP, w: CW, h: bodyBottom - CONTENT_TOP };
1437
+ var headRow = sec.table.head;
1438
+ var colW = sec.table.colW || headRow.map(function () { return tR.w / headRow.length; });
1439
+ sh.push(table(tR.x, tR.y, tR.w, colW, [headRow].concat(sec.table.rows), S, { availH: tR.h }));
1440
+ } else if (type === 'timeline') {
1441
+ var T = PT.timeline, ps = sec.phases || [], np = Math.max(1, ps.length);
1442
+ var tLR = regOf('timeline', 'primary') || { x: MX, y: T.labelY, w: CW };
1443
+ var stepW = (tLR.w - 1.0) / np;
1444
+ sh.push(shape('rect', tLR.x + 0.2, T.axisY, tLR.w - 0.4, 0.02, S.line));
1445
+ ps.forEach(function (p, i) {
1446
+ var x5 = tLR.x + 0.2 + i * stepW;
1447
+ var on = p[3] === 'done' || p[3] === 'now';
1448
+ sh.push(shape('ellipse', x5 - T.dotR / 2, T.dotY, T.dotR, T.dotR, on ? S.accent : S.line));
1449
+ sh.push(txSp(x5 - 0.1, T.labelY, stepW - 0.2, 0.4, [[{ t: p[0], sz: 12, b: 1, col: S.accent, font: S.font, spc: 100 }]]));
1450
+ sh.push(txSp(x5 - 0.1, T.nameY, stepW - 0.2, 0.5, [[{ t: p[1], sz: 17, b: 1, col: p[3] === 'now' ? S.accent : S.ink, font: S.fontDisplay }]]));
1451
+ sh.push(txSp(x5 - 0.1, T.descY, stepW - 0.25, T.descH, [[{ t: p[2] || '', sz: 12, col: S.body, font: S.font }]], { lineSpacing: 18 }));
1452
+ });
1453
+ } else if (type === 'bar') {
1454
+ var ch = sec.chart || {};
1455
+ var dcols2 = (ch.colors || dataColors(model.style, model.theme));
1456
+ var botY = chartBottom(!!sec.soWhat, !!sec.footnote) - (sec.note ? 0.42 : 0.05);
1457
+ var isHBar = ch.type === 'hbar';
1458
+ var bRR = regOf('bar', isHBar ? 'hbar' : 'primary', { bottom: botY }) ||
1459
+ { x: isHBar ? MX : MX + PT.bar.chartX, y: isHBar ? PT.bar.hbarY0 : PT.bar.chartY,
1460
+ w: isHBar ? CW : CW - PT.bar.chartW, h: 3 };
1461
+ if (isHBar) {
1462
+ chartBlockShapes(sh, ch, S, bRR.x, bRR.y, bRR.w, bRR.h, dcols2);
1463
+ } else {
1464
+ chartBlockShapes(sh, ch, S, bRR.x, bRR.y, bRR.w, Math.max(1.5, bRR.h), dcols2);
1465
+ }
1466
+ if (sec.note) sh.push(txSp(MX, PT.note.y, CW, PT.note.h, [[{ t: sec.note, sz: 11, col: S.faint, font: S.font }]]));
1467
+ } else if (type === 'twocol' || type === 'threecol') {
1468
+ var ps2 = sec.paragraphs || [];
1469
+ var nCol = (type === 'threecol') ? 3 : 2;
1470
+ var gapC = (type === 'threecol') ? PT.research.col3Gap : PT.twocol.colGap;
1471
+ var tcR = regOf('twocol', 'primary', { top: bodyY, bottom: bodyBottom }) ||
1472
+ { x: MX, y: bodyY, w: CW, h: bodyBottom - bodyY };
1473
+ var gC = cols(tcR.w, nCol, gapC);
1474
+ var perC = Math.ceil(ps2.length / nCol);
1475
+ var availC = tcR.h;
1476
+ /* 探针与 build_pptx.js 同算法:按「各栏字数之和」的最大值估算(保证两通道字号一致) */
1477
+ var longest = 0, gi, pi2;
1478
+ for (gi = 0; gi < nCol; gi++) {
1479
+ var colG = ps2.slice(gi * perC, (gi + 1) * perC), ssum = 0;
1480
+ for (pi2 = 0; pi2 < colG.length; pi2++) ssum += String(colG[pi2][1] || '').length;
1481
+ if (ssum > longest) longest = ssum;
1482
+ }
1483
+ var probe = [], w4;
1484
+ for (w4 = 0; w4 < longest / 120; w4++) probe.push(new Array(121).join('x'));
1485
+ if (!probe.length) probe.push(new Array(1 + longest).join('x'));
1486
+ var fzC = fitFont(probe, gC.w, availC, { max: 13.5, gapFactor: 1.2 });
1487
+ for (var ci3 = 0; ci3 < nCol; ci3++) {
1488
+ var colC = ps2.slice(ci3 * perC, (ci3 + 1) * perC);
1489
+ if (!colC.length) continue;
1490
+ var runsC = [];
1491
+ colC.forEach(function (p) {
1492
+ runsC.push([{ t: p[0] + ' ', sz: fzC, b: 1, col: S.ink, font: S.font },
1493
+ { t: p[1], sz: fzC - 1, col: S.body, font: S.font }]);
1494
+ });
1495
+ sh.push(txSp(tcR.x + ci3 * gC.step, tcR.y, gC.w, availC, runsC,
1496
+ { lineSpacing: fzC * 1.5, spaceBefore: 7 }));
1497
+ }
1498
+ } else if (type === 'halftable') {
1499
+ var yH = sec.lead ? PT.common.bodyYWithLead : PT.research.denseTableY;
1500
+ var hLR = regOf('halftable', 'left', { top: yH, bottom: bodyBottom }) ||
1501
+ { x: MX, y: yH, w: CW * PT.research.halfTableW, h: bodyBottom - yH };
1502
+ var hRR = regOf('halftable', 'right', { top: yH, bottom: bodyBottom }) ||
1503
+ { x: MX + CW * PT.research.halfChartX, y: yH, w: CW * PT.research.halfChartW, h: bodyBottom - yH };
1504
+ var lwH = hLR.w;
1505
+ if (sec.table && sec.table.head) {
1506
+ var colWH = sec.table.colW || sec.table.head.map(function () { return lwH / sec.table.head.length; });
1507
+ sh.push(table(hLR.x, hLR.y, lwH, colWH, [sec.table.head].concat(sec.table.rows || []), S,
1508
+ { availH: hLR.h, maxRowH: PT.research.denseRowH + 0.1, minRowH: 0.28 }));
1509
+ }
1510
+ var ecH = sec.chart || {};
1511
+ if (ecH.labels && ecH.values) {
1512
+ chartBlockShapes(sh, ecH, S, hRR.x, hRR.y, hRR.w, Math.max(1.2, hRR.h),
1513
+ (ecH.colors || dataColors(model.style, model.theme)));
1514
+ }
1515
+ } else if (type === 'matrix') {
1516
+ var rhH = sec.rowHeads || [], chH = sec.colHeads || [];
1517
+ var mxR = regOf('matrix', 'primary', { bottom: bodyBottom }) ||
1518
+ { x: MX, y: PT.research.matrixY, w: CW, h: bodyBottom - PT.research.matrixY };
1519
+ var mY = mxR.y, mLW = PT.research.matrixLabelW;
1520
+ var rowsC = sec.cells || [], nR2 = Math.max(1, rowsC.length);
1521
+ var availM = mxR.h - 0.42;
1522
+ var mCH = fitRowH(availM, nR2, PT.research.matrixCellH, 0.3);
1523
+ var mW = (mxR.w - mLW) / Math.max(1, chH.length);
1524
+ chH.forEach(function (ch3, ci) {
1525
+ sh.push(txSp(mxR.x + mLW + ci * mW, mY, mW, 0.34, [[{ t: ch3, sz: 11, b: 1, col: S.body, font: S.font }]], { align: 'ctr', anchor: 'ctr' }));
1526
+ });
1527
+ rowsC.forEach(function (rowC, ri) {
1528
+ var y6 = mY + 0.42 + ri * mCH;
1529
+ sh.push(txSp(mxR.x, y6 + (mCH - 0.5) / 2, mLW - 0.15, 0.5, [[{ t: rhH[ri] || '', sz: 11, b: 1, col: S.body, font: S.font }]], { align: 'r', anchor: 'ctr' }));
1530
+ rowC.forEach(function (cell, ci) {
1531
+ var acc = isRec(cell) && cell.accent;
1532
+ var txt = isRec(cell) ? (cell.t || '') : String(cell);
1533
+ var cx4 = mxR.x + mLW + ci * mW;
1534
+ sh.push(shape('roundRect', cx4 + 0.04, y6, mW - 0.08, mCH - 0.1, acc ? S.soft : S.surface,
1535
+ { adj: 3000, line: acc ? undefined : S.line }));
1536
+ sh.push(txSp(cx4 + 0.14, y6 + 0.08, mW - 0.28, mCH - 0.26, [[{ t: txt, sz: 11, col: acc ? S.accent : S.body, font: S.font }]], { anchor: 'ctr' }));
1537
+ });
1538
+ });
1539
+ } else if (type === 'lane') {
1540
+ var lnS = sec.lanes || [], nL = Math.max(1, lnS.length);
1541
+ var startL = sec.lead ? PT.common.bodyYWithLead : PT.arch.fullStartY;
1542
+ var lnR = regOf('lane', 'primary', { mode: model.mode, bottom: bodyBottom }) ||
1543
+ { x: MX, y: startL, w: CW, h: bodyBottom - startL };
1544
+ var availL = lnR.h;
1545
+ var lnH = fitRowH(availL - (nL - 1) * PT.arch.laneGap, nL, PT.arch.laneH, 0.3);
1546
+ var lnGap = PT.arch.laneGap, lnHW = PT.arch.laneHeadW;
1547
+ lnS.forEach(function (ln, li) {
1548
+ var y7 = lnR.y + li * (lnH + lnGap);
1549
+ sh.push(shape('roundRect', lnR.x, y7, lnR.w, lnH, S.surface, { adj: 2500, line: S.line }));
1550
+ sh.push(shape('rect', lnR.x, y7, lnHW, lnH, S.surface, { line: S.line }));
1551
+ sh.push(txSp(lnR.x + 0.08, y7, lnHW - 0.16, lnH, [[{ t: ln[0], sz: 11, b: 1, col: S.body, font: S.font }]], { align: 'ctr', anchor: 'ctr' }));
1552
+ var steps = ln[1] || [];
1553
+ var stepW2 = Math.min(PT.arch.stepMaxW, (lnR.w - lnHW - 0.4 - (steps.length - 1) * PT.arch.stepGap) / Math.max(1, steps.length));
1554
+ steps.forEach(function (st, si) {
1555
+ var stAcc = isRec(st) && st.accent;
1556
+ var stT = isRec(st) ? st.t : st;
1557
+ var sx2 = lnR.x + lnHW + 0.2 + si * (stepW2 + PT.arch.stepGap);
1558
+ sh.push(shape('roundRect', sx2, y7 + 0.13, stepW2, lnH - 0.26, stAcc ? S.soft : S.bg,
1559
+ { adj: 4000, line: stAcc ? undefined : S.line }));
1560
+ sh.push(txSp(sx2 + 0.06, y7 + 0.13, stepW2 - 0.12, lnH - 0.26,
1561
+ [[{ t: stT, sz: 11, b: stAcc ? 1 : 0, col: stAcc ? S.accent : S.ink, font: S.font }]], { align: 'ctr', anchor: 'ctr' }));
1562
+ if (si < steps.length - 1) {
1563
+ sh.push(txSp(sx2 + stepW2, y7 + 0.13, PT.arch.stepGap, lnH - 0.26, [[{ t: '→', sz: 11, col: S.faint, font: S.font }]], { align: 'ctr', anchor: 'ctr' }));
1564
+ }
1565
+ });
1566
+ });
1567
+ } else if (type === 'cards') {
1568
+ var cdC = PT.cards, cds = sec.cards || [];
1569
+ var cR = regOf('cards', 'primary', { bottom: bodyBottom }) ||
1570
+ { x: MX, y: cdC.startY, w: CW, h: bodyBottom - cdC.startY };
1571
+ var cN = Math.min(sec.columns || 3, cds.length || 1);
1572
+ var rN = Math.ceil((cds.length || 1) / cN);
1573
+ var gw2 = (cR.w - (cN - 1) * cdC.gap) / cN;
1574
+ var gh2 = fitRowH(cR.h - (rN - 1) * cdC.gap, rN, cdC.maxH, 0.6);
1575
+ cds.forEach(function (cd, i) {
1576
+ var col2 = i % cN, row2 = Math.floor(i / cN);
1577
+ var x8 = cR.x + col2 * (gw2 + cdC.gap), y8 = cR.y + row2 * (gh2 + cdC.gap);
1578
+ sh.push(shape('roundRect', x8, y8, gw2, gh2, S.surface, { line: S.line, adj: 5000 }));
1579
+ /* 卡片头路标(与 B 通道同源:accent 小方块 = PPTX 侧图标等价物) */
1580
+ var ico2 = 0.18;
1581
+ sh.push(shape('roundRect', x8 + 0.18, y8 + 0.2, ico2, ico2, S.accent, { adj: 2500 }));
1582
+ sh.push(txSp(x8 + 0.18 + ico2 + 0.1, y8 + 0.14, gw2 - 0.36 - ico2 - 0.1, cdC.titleH,
1583
+ [[{ t: cd.title, sz: 15, b: 1, col: S.ink, font: S.font }]]));
1584
+ var paras2 = (cd.points || []).map(function (pt) {
1585
+ var line;
1586
+ if (pt && pt.length != null && typeof pt !== 'string') {
1587
+ var k = pt[0] == null ? '' : String(pt[0]);
1588
+ var v = pt[1] == null ? '' : String(pt[1]);
1589
+ line = (k && v) ? ('· ' + k + ' ' + v) : ('· ' + (k || v));
1590
+ } else if (pt && typeof pt === 'object') {
1591
+ line = '· ' + String(pt.t || '') + (pt.d ? ' ' + String(pt.d) : '');
1592
+ } else {
1593
+ line = '· ' + String(pt == null ? '' : pt);
1594
+ }
1595
+ return [{ t: line, sz: 12, col: S.body, font: S.font }];
1596
+ });
1597
+ sh.push(txSp(x8 + 0.18, y8 + 0.14 + cdC.titleH + 0.06, gw2 - 0.36, Math.max(0.4, gh2 - cdC.titleH - 0.34), paras2,
1598
+ { lineSpacing: 18 }));
1599
+ });
1600
+ } else if (type === 'split') {
1601
+ /* 双区自由组合页:左区与右区各可为 要点 / 图表 / 表格 / 图片(与 B 通道同语义)。 */
1602
+ var SP = PT.split;
1603
+ var lRs = regOf('split', 'left', { bottom: bodyBottom }) ||
1604
+ { x: MX, y: SP.chartY, w: CW * SP.leftW, h: bodyBottom - SP.chartY };
1605
+ var rRs = regOf('split', 'right', { bottom: bodyBottom }) ||
1606
+ { x: MX + CW * SP.rightX, y: SP.chartY, w: CW * SP.rightW, h: bodyBottom - SP.chartY };
1607
+ var lw2 = lRs.w, rw2 = rRs.w, rxL = rRs.x;
1608
+ var lt2 = (sec.left && sec.left.type) || 'points';
1609
+ /* 要点区渲染(左右两区共用;与 B 通道同算法同文本) */
1610
+ var drawPointsL = function (el, x, w) {
1611
+ var ptsP = (el && el.points) || [];
1612
+ var lyP = bodyY, availP = bodyBottom - lyP;
1613
+ var rowHP = fitRowH(availP, ptsP.length, SP.rowH, 0.3);
1614
+ var probeP = [], q5;
1615
+ for (q5 = 0; q5 < ptsP.length; q5++) probeP.push(String(ptsP[q5][0] || '') + String(ptsP[q5][1] || ''));
1616
+ var fzP = fitFont(probeP, w - 0.4, availP, { max: 14, gapFactor: 0.6 });
1617
+ ptsP.forEach(function (pt, i) {
1618
+ var ly2 = lyP + i * rowHP;
1619
+ sh.push(shape('rect', x, ly2 + rowHP / 2 - 0.06, 0.12, 0.12, S.accent));
1620
+ sh.push(txSp(x + 0.28, ly2, w - 0.4, rowHP - 0.04,
1621
+ [[{ t: pt[0] + ' ', sz: fzP, b: 1, col: S.ink, font: S.font },
1622
+ { t: pt[1], sz: fzP - 1, col: S.body, font: S.font }]]));
1623
+ });
1624
+ };
1625
+ if (lt2 === 'table') {
1626
+ var lhL = [sec.left.head].concat(sec.left.rows);
1627
+ var lcwL = sec.left.colW || sec.left.head.map(function () { return lw2 / sec.left.head.length; });
1628
+ sh.push(table(lRs.x, SP.tableY, lw2, lcwL, lhL, S, { availH: bodyBottom - SP.tableY, maxRowH: SP.tableRowH, minRowH: 0.3 }));
1629
+ } else if (lt2 === 'image') {
1630
+ var imL = (sec.left && sec.left.image) || {};
1631
+ addImageEl(sh, imL.src, S, lRs.x, SP.chartY, lw2, Math.max(1.4, bodyBottom - SP.chartY),
1632
+ { placeholder: !!imL.placeholder || !imL.src, layout: 'half', multi: false, fit: imL.fit });
1633
+ if (imL.caption) sh.push(txSp(lRs.x, SP.capY, lw2, 0.35, [[{ t: imL.caption, sz: 11, col: S.faint, font: S.font }]], { align: 'ctr' }));
1634
+ } else if (lt2 !== 'points') {
1635
+ var lc2 = sec.left;
1636
+ chartBlockShapes(sh, lc2, S, lRs.x, SP.chartY, lw2,
1637
+ Math.max(1.4, bodyBottom - SP.chartY - (lc2.cap ? 0.1 : 0)),
1638
+ lc2.colors || dataColors(model.style, model.theme));
1639
+ if (lc2.cap) sh.push(txSp(lRs.x, SP.capY, lw2, 0.35, [[{ t: lc2.cap, sz: 12, col: S.body, font: S.font }]], { align: 'ctr' }));
1640
+ } else {
1641
+ drawPointsL(sec.left, lRs.x, lw2);
1642
+ }
1643
+ var rt = (sec.right && sec.right.type) || 'bar';
1644
+ if (rt === 'table') {
1645
+ var rrL = [sec.right.head].concat(sec.right.rows);
1646
+ var rcwL = sec.right.colW || sec.right.head.map(function () { return rw2 / sec.right.head.length; });
1647
+ sh.push(table(rxL, SP.tableY, rw2, rcwL, rrL, S, { availH: bodyBottom - SP.tableY, maxRowH: SP.tableRowH, minRowH: 0.3 }));
1648
+ } else if (rt === 'image') {
1649
+ var im2 = (sec.right && sec.right.image) || {};
1650
+ addImageEl(sh, im2.src, S, rxL, SP.chartY, rw2, Math.max(1.4, bodyBottom - SP.chartY),
1651
+ { placeholder: !!im2.placeholder || !im2.src, layout: 'half', multi: false });
1652
+ if (im2.caption) sh.push(txSp(rxL, SP.capY, rw2, 0.35, [[{ t: im2.caption, sz: 11, col: S.faint, font: S.font }]], { align: 'ctr' }));
1653
+ } else if (rt === 'points') {
1654
+ drawPointsL(sec.right, rxL, rw2);
1655
+ } else {
1656
+ var bc2 = sec.right;
1657
+ var dcols3 = bc2.colors || dataColors(model.style, model.theme);
1658
+ chartBlockShapes(sh, bc2, S, rxL, SP.chartY, rw2, Math.max(1.4, bodyBottom - SP.chartY - (bc2.cap ? 0.1 : 0)), dcols3);
1659
+ if (bc2.cap) sh.push(txSp(rxL, SP.capY, rw2, 0.35, [[{ t: bc2.cap, sz: 12, col: S.body, font: S.font }]], { align: 'ctr' }));
1660
+ }
1661
+ } else if (type === 'diagram') {
1662
+ var D = PT.diagram, lys = sec.layers || [];
1663
+ var dgR = regOf('diagram', 'primary', { mode: _mode, bottom: bodyBottom }) ||
1664
+ { x: MX, y: D.bodyStartY, w: CW, h: bodyBottom - D.bodyStartY };
1665
+ var dStart = dgR.y;
1666
+ var dEnd = dgR.y + dgR.h;
1667
+ var nLay = Math.max(1, lys.length);
1668
+ var lh2 = Math.max(0.6, Math.min(D.maxLayerH, (dEnd - dStart) / nLay - D.layerGap));
1669
+ lys.forEach(function (lay, li) {
1670
+ var y9 = dStart + li * (lh2 + D.layerGap);
1671
+ var focus = lay[2] === 'focus';
1672
+ sh.push(shape('roundRect', dgR.x, y9, D.layerBarW, lh2, focus ? S.accent : S.surface,
1673
+ { adj: 3000, line: focus ? undefined : S.line }));
1674
+ sh.push(txSp(dgR.x + 0.1, y9 + 0.1, D.layerBarW - 0.2, lh2 - 0.2, [[{ t: lay[0], sz: 12, b: 1, col: focus ? S.onAccent : S.body, font: S.font }]], { align: 'ctr', anchor: 'ctr' }));
1675
+ var nodes = lay[1] || [], nn2 = Math.max(1, nodes.length);
1676
+ var availW = dgR.w - D.layerBarW - 0.2;
1677
+ var nw2 = Math.min(D.nodeMaxW, (availW - (nn2 - 1) * D.nodeGap) / nn2);
1678
+ nodes.forEach(function (nd, ni) {
1679
+ var x10 = dgR.x + D.layerBarW + 0.2 + ni * (nw2 + D.nodeGap);
1680
+ var ndacc = isRec(nd) ? nd.accent : false;
1681
+ var ntt = isRec(nd) ? nd.t : nd;
1682
+ var nds = isRec(nd) ? (nd.d || '') : '';
1683
+ sh.push(shape('roundRect', x10, y9, nw2, lh2, ndacc ? S.soft : S.surface, { adj: 4000, line: S.line }));
1684
+ sh.push(txSp(x10 + 0.12, y9 + (nds ? 0.12 : lh2 / 2 - 0.2), nw2 - 0.24, 0.38,
1685
+ [[{ t: ntt, sz: 12.5, b: 1, col: ndacc ? S.accent : S.ink, font: S.font }]], { anchor: nds ? 't' : 'ctr' }));
1686
+ if (nds) sh.push(txSp(x10 + 0.12, y9 + lh2 - 0.5, nw2 - 0.24, 0.4, [[{ t: nds, sz: 10, col: S.faint, font: S.font }]]));
1687
+ });
1688
+ if (li < lys.length - 1) {
1689
+ var cy2 = y9 + lh2 + D.layerGap / 2;
1690
+ var cxm2 = dgR.x + D.layerBarW / 2;
1691
+ sh.push(shape('rect', cxm2 - 0.01, cy2 - 0.1, 0.02, 0.14, focus ? S.accent : S.line));
1692
+ sh.push(shape('rect', cxm2 - 0.06, cy2 + 0.04, 0.12, 0.02, focus ? S.accent : S.line));
1693
+ }
1694
+ });
1695
+ if (sec.legend) {
1696
+ var lx2 = MX;
1697
+ sec.legend.forEach(function (lg2) {
1698
+ sh.push(txSp(lx2, PH - 0.62, 3.2, 0.3, [[{ t: '● ' + lg2, sz: 10, col: S.faint, font: S.font }]]));
1699
+ lx2 += 2.0;
1700
+ });
1701
+ }
1702
+ } else if (type === 'exhibit') {
1703
+ var eReg = regOf('exhibit', 'primary')
1704
+ || { x: MX, y: PT.exhibit.chartY, w: CW, badgeY: PT.exhibit.badgeY };
1705
+ if (sec.exhibitNo) {
1706
+ sh.push(txSp(MX, eReg.badgeY != null ? eReg.badgeY : PT.exhibit.badgeY, CW, 0.35, [[{ t: String(sec.exhibitNo).toUpperCase(), sz: 11.5, b: 1, col: S.accent, font: S.font, spc: 200 }]]));
1707
+ }
1708
+ var ec2 = sec.chart || {};
1709
+ if (ec2.labels && ec2.values) {
1710
+ var edcols2 = (ec2.colors || dataColors(model.style, model.theme));
1711
+ var hh2 = Math.max(1.2, chartBottom(!!sec.soWhat, !!sec.footnote) - eReg.y - 0.05);
1712
+ if (ec2.type === 'hbar') {
1713
+ chartBlockShapes(sh, ec2, S, eReg.x, eReg.y, eReg.w, hh2, edcols2);
1714
+ } else {
1715
+ chartBlockShapes(sh, ec2, S, eReg.x + 0.4, eReg.y - 0.1, eReg.w - 0.8, hh2 + 0.15, edcols2);
1716
+ }
1717
+ }
1718
+ } else if (type === 'sankey' || type === 'treemap' || type === 'boxplot'
1719
+ || type === 'network' || type === 'marimekko' || type === 'streamgraph') {
1720
+ var infoR = regOf(type, 'primary', { bottom: bodyBottom }) ||
1721
+ { x: MX, y: bodyY, w: CW, h: bodyBottom - bodyY };
1722
+ infoApprox(sh, sec, S, infoR.y, infoR.y + infoR.h);
1723
+ } else {
1724
+ /* points(默认):要点列表 + 可选右侧指标列 */
1725
+ var P2 = PT.points, pts3 = sec.points || [];
1726
+ var hasM = !!(sec.metrics && sec.metrics.length);
1727
+ var ptsR = regOf('points', 'primary', { top: bodyY, bottom: bodyBottom }) ||
1728
+ { x: MX, y: bodyY, w: CW, h: bodyBottom - bodyY };
1729
+ /* 指标列最多 3 个 + 文本区严格分区(与 build_pptx.js 同算法) */
1730
+ var nm2 = hasM ? Math.min(sec.metrics.length, 3) : 0;
1731
+ var mw2 = hasM ? Math.min(P2.metricW, Math.max(1.6, (CW - 4.8) / nm2)) : 0;
1732
+ var textW = hasM ? Math.max(4.2, (PW - MX - nm2 * mw2) - MX - 0.35) : CW - 0.4;
1733
+ var availPt = ptsR.h;
1734
+ var rowHPt = fitRowH(availPt, pts3.length, P2.rowH, 0.3);
1735
+ var probeP = [], q5;
1736
+ for (q5 = 0; q5 < pts3.length; q5++) probeP.push(String(pts3[q5][0] || '') + String(pts3[q5][1] || ''));
1737
+ var fzP = fitFont(probeP, textW - 0.32, availPt, { max: 15, gapFactor: 0.55 });
1738
+ pts3.forEach(function (p, i) {
1739
+ var y11 = ptsR.y + i * rowHPt;
1740
+ sh.push(shape('rect', ptsR.x, y11 + rowHPt / 2 - P2.markSize / 2, P2.markSize, P2.markSize, S.accent));
1741
+ sh.push(txSp(ptsR.x + 0.32, y11, textW, rowHPt - 0.04,
1742
+ [[{ t: p[0] + ' ', sz: fzP, b: 1, col: S.ink, font: S.font },
1743
+ { t: p[1], sz: fzP - 1, col: S.body, font: S.font }]]));
1744
+ });
1745
+ if (hasM) {
1746
+ var gx2 = PW - MX - nm2 * mw2;
1747
+ var vSz = nm2 >= 3 ? 28 : 34;
1748
+ sec.metrics.slice(0, nm2).forEach(function (m, i) {
1749
+ var x12 = gx2 + i * mw2;
1750
+ sh.push(txSp(x12, P2.metricY, mw2 - 0.2, P2.metricValH, [[{ t: m[0], sz: vSz, b: 1, col: S.accent, font: S.fontDisplay }]]));
1751
+ sh.push(txSp(x12, P2.metricCapY, mw2 - 0.2, 0.9, [[{ t: m[1], sz: 11, col: S.faint, font: S.font }]]));
1752
+ });
1753
+ }
1754
+ }
1755
+ /* research 通用可选件:so-what 结论条 + 页脚来源行 + 待核实条(共用同一槽位) */
1756
+ if (sec.soWhat) soWhatBar(sh, sec.soWhat, S);
1757
+ if (sec.footnote) footnoteLine(sh, sec.footnote, S);
1758
+ if (flagH) flagBar(sh, flagItems, S, flagY);
1759
+ slides.push(sh);
1760
+ });
1761
+
1762
+ /* 收尾(主题一致强调带——accent-soft 底 + 常规文字,浅色模式不再出深色页) */
1763
+ var CL = PT.closing;
1764
+ var cl = [shape('rect', 0, 0, PW, PH, S.soft)];
1765
+ cl.push(shape('rect', MX, CL.eyebrowY, 0.9, 0.045, S.accent));
1766
+ cl.push(txSp(MX, CL.eyebrowY + 0.16, CW, 0.4, [[{ t: '下一步', sz: 13, b: 1, col: S.accent, font: S.font, spc: 200 }]]));
1767
+ cl.push(txSp(MX, CL.titleY, CW - 1, CL.titleH, [[{ t: model.closing.title, sz: 30, b: 1, col: S.ink, font: S.fontDisplay }]]));
1768
+ var clPts = model.closing.points || [], clN = Math.max(1, clPts.length);
1769
+ var clW = (CW - (clN - 1) * 0.3) / clN;
1770
+ clPts.forEach(function (p, i) {
1771
+ var x13 = MX + i * (clW + 0.3);
1772
+ cl.push(shape('roundRect', x13, CL.pointsY - 0.2, clW, CL.pointTitleH + CL.pointBodyH + 0.32, S.surface, { line: S.line, adj: 4000 }));
1773
+ cl.push(txSp(x13 + 0.18, CL.pointsY, clW - 0.36, CL.pointTitleH, [[{ t: p[0], sz: 17, b: 1, col: S.accent, font: S.font }]]));
1774
+ cl.push(txSp(x13 + 0.18, CL.pointsY + CL.pointTitleH, clW - 0.36, CL.pointBodyH, [[{ t: p[1], sz: 12.5, col: S.body, font: S.font }]]));
1775
+ });
1776
+ slides.push(cl);
1777
+
1778
+ /* 页码 */
1779
+ var total = slides.length;
1780
+ slides.forEach(function (sh, i) {
1781
+ sh.push(txSp(PW - 1.4, (LAY.pageNumY != null ? LAY.pageNumY : PH - 0.5), 0.9, 0.3,
1782
+ [[{ t: (i + 1) + ' / ' + total, sz: 10, col: S.faint, font: S.font }]], { align: 'r' }));
1783
+ });
1784
+ return slides;
1785
+ }
1786
+
1787
+ /* ── 打包骨架 ── */
1788
+ var NS = {
1789
+ a: 'http://schemas.openxmlformats.org/drawingml/2006/main',
1790
+ p: 'http://schemas.openxmlformats.org/presentationml/2006/main',
1791
+ r: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships'
1792
+ };
1793
+ function slideXml(shapes) {
1794
+ return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' +
1795
+ '<p:sld xmlns:a="' + NS.a + '" xmlns:p="' + NS.p + '" xmlns:r="' + NS.r + '">' +
1796
+ '<p:cSld><p:spTree><p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr>' +
1797
+ '<p:grpSpPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="0" cy="0"/><a:chOff x="0" y="0"/><a:chExt cx="0" cy="0"/></a:xfrm></p:grpSpPr>' +
1798
+ shapes.join('') + '</p:spTree></p:cSld><p:clrMapOvr><a:overrideClrMapping bg1="lt1" tx1="dk1" bg2="lt2" tx2="dk2" accent1="accent1" accent2="accent2" accent3="accent3" accent4="accent4" accent5="accent5" accent6="accent6" hlink="hlink" folHlink="folHlink"/></p:clrMapOvr></p:sld>';
1799
+ }
1800
+ function relsXml(pairs) {
1801
+ var rs = pairs.map(function (p) {
1802
+ return '<Relationship Id="' + p[0] + '" Type="' + p[1] + '" Target="' + p[2] + '"/>';
1803
+ }).join('');
1804
+ return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' +
1805
+ '<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">' + rs + '</Relationships>';
1806
+ }
1807
+ var RT = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/';
1808
+
1809
+ /* 每页 slide XML(预览模态用:与导出同一引擎,所见即所得)
1810
+ theme="dark" 时用 PRESETS_DARK(与页面深色主题同源),默认 light */
1811
+ function themePresets(theme) {
1812
+ return (theme === 'dark' && typeof PRESETS_DARK !== 'undefined' &&
1813
+ PRESETS_DARK && Object.keys(PRESETS_DARK).length) ? PRESETS_DARK : PRESETS;
1814
+ }
1815
+ function slidesXml(model) {
1816
+ var S = themePresets(model && model.theme)[model.style] || PRESETS['business-blue'];
1817
+ return slidesOf(model, S).map(slideXml);
1818
+ }
1819
+
1820
+ /* ── 模型自检(schema 单源驱动:scripts/model-schema.json 经 sync_runtime 注入的 MODEL_SCHEMA;
1821
+ 与 scripts/extract_model.py 本地校验同一份定义,双端不漂移) ── */
1822
+ function schemaGet(obj, path) {
1823
+ var cur = obj, ks = path.split('.');
1824
+ for (var i = 0; i < ks.length; i++) {
1825
+ if (cur == null || typeof cur !== 'object') return undefined;
1826
+ cur = cur[ks[i]];
1827
+ }
1828
+ return cur;
1829
+ }
1830
+ function schemaFieldOk(sec, spec) {
1831
+ /* 'anyof:a|b:c'(任一满足,用于 image 页型 src/items/placeholder 三选一);
1832
+ 'a.b:array' / 'a.b:str' / 'a.b'(真值检查)——与 scripts/extract_model.py 同语义。 */
1833
+ if (spec.indexOf('anyof:') === 0) {
1834
+ return spec.slice(6).split('|').some(function (alt) { return alt && schemaFieldOk(sec, alt); });
1835
+ }
1836
+ var m = spec.match(/^(.*?)(?::(array|str))?$/), path = m[1], kind = m[2];
1837
+ var v = schemaGet(sec, path);
1838
+ if (kind === 'array') return Array.isArray(v) && v.length > 0;
1839
+ if (kind === 'str') return typeof v === 'string' && v.trim().length > 0;
1840
+ return !!v;
1841
+ }
1842
+ /* 图片模型软校验:版式 / 裁切策略 / 多图数量 / 外链 src——与 validate_pptx.py 硬门禁同口径,
1843
+ 浏览器预览与 extract_model.py 都能提前看到问题。 */
1844
+ function imageModelWarnings(sec, where, warnings) {
1845
+ var IS = (typeof IMG !== 'undefined' && IMG) || {};
1846
+ var holders = [{ img: sec.image, at: where + '.image' }];
1847
+ if (sec.right && sec.right.image) holders.push({ img: sec.right.image, at: where + '.right.image' });
1848
+ holders.forEach(function (h) {
1849
+ var img = h.img;
1850
+ if (!img || typeof img !== 'object') return;
1851
+ var layouts = IS.layouts || ['full', 'half', 'bleed', 'grid', 'compare', 'wall'];
1852
+ var fitEnum = IS.fitEnum || ['cover', 'contain'];
1853
+ var items = Array.isArray(img.items) ? img.items.filter(Boolean) : [];
1854
+ var layout = String(img.layout || (items.length > 1 ? 'grid' : 'full')).toLowerCase();
1855
+ if (layouts.indexOf(layout) < 0) {
1856
+ warnings.push(h.at + '.layout="' + layout + '" 未知(应为 ' + layouts.join('/') + ')');
1857
+ }
1858
+ if (img.fit && fitEnum.indexOf(String(img.fit).toLowerCase()) < 0) {
1859
+ warnings.push(h.at + '.fit="' + img.fit + '" 未知(应为 ' + fitEnum.join('/') + ')');
1860
+ }
1861
+ if (img.placeholder && (img.src || items.length)) {
1862
+ warnings.push(h.at + ' 同时声明 placeholder 与图片源(placeholder 优先,图片将被忽略)');
1863
+ }
1864
+ if (items.length > (IS.maxPerPage || 6)) {
1865
+ warnings.push(h.at + '.items 共 ' + items.length + ' 张,超出单页上限 ' + (IS.maxPerPage || 6));
1866
+ }
1867
+ if (layout === 'compare' && items.length && items.length !== 2) {
1868
+ warnings.push(h.at + '.items 为 compare 版式时应为 2 张(当前 ' + items.length + ')');
1869
+ }
1870
+ [img].concat(items).forEach(function (it) {
1871
+ if (it && typeof it === 'object' && typeof it.src === 'string' &&
1872
+ /^\s*(?:https?:)?\/\//.test(it.src)) {
1873
+ warnings.push(h.at + ' 含外链 src(只允许 data: 内联或相对路径)');
1874
+ }
1875
+ });
1876
+ });
1877
+ }
1878
+
1879
+ function validateModel(model) {
1880
+ var missing = [], warnings = [];
1881
+ var M = (typeof MODEL_SCHEMA !== 'undefined' && MODEL_SCHEMA) || {};
1882
+ var pageTypes = M.pageTypes || {};
1883
+ if (!model || typeof model !== 'object') {
1884
+ return { ok: false, missing: ['window.REPORT_MODEL(内容模型缺失)'], warnings: warnings, pages: 0 };
1885
+ }
1886
+ (M.model && M.model.required || ['title', 'sections:array', 'closing.title', 'closing.points:array'])
1887
+ .forEach(function (spec) {
1888
+ if (!schemaFieldOk(model, spec)) missing.push(spec.split(':')[0] + '(顶层必填)');
1889
+ });
1890
+ var mode = model.mode || 'presentation';
1891
+ var hasAgenda = Array.isArray(model.agenda) && model.agenda.length > 0;
1892
+ var agMin = (M.model && M.model.agendaMin && M.model.agendaMin[mode]) || 0;
1893
+ if (agMin > 0 && (!hasAgenda || model.agenda.length < agMin)) {
1894
+ missing.push('agenda(大纲 ≥' + agMin + ' 条)');
1895
+ }
1896
+ var secs = model.sections || [];
1897
+ if (!Array.isArray(secs) || secs.length < ((M.model && M.model.sectionsMin) || 1)) {
1898
+ missing.push('sections(章节页 ≥1)');
1899
+ }
1900
+ if (!model.closing || !model.closing.title) missing.push('closing(收尾页)');
1901
+ secs.forEach(function (sec, i) {
1902
+ var t = sec.type || 'points';
1903
+ var def = pageTypes[t];
1904
+ var where = 'sections[' + i + ']';
1905
+ if (!def) { missing.push(where + '.type="' + t + '"(未知页型)'); return; }
1906
+ if (!sec.title) missing.push(where + '.title(第 ' + (i + 1) + ' 章标题)');
1907
+ (def.required || []).forEach(function (spec) {
1908
+ if (!schemaFieldOk(sec, spec)) missing.push(where + '.' + spec + '(' + (def.label || t) + '必填)');
1909
+ });
1910
+ if (def.modes && def.modes.indexOf(mode) < 0) {
1911
+ warnings.push('sections[' + i + '] 页型 "' + t + '" 文档口径适用于 ' + def.modes.join('/') + ',当前 mode="' + mode + '"');
1912
+ }
1913
+ imageModelWarnings(sec, where, warnings);
1914
+ });
1915
+ if (M.modes && model.mode && M.modes.indexOf(model.mode) < 0) {
1916
+ warnings.push('mode="' + model.mode + '" 未知,按 presentation 处理');
1917
+ }
1918
+ if (model.style && !PRESETS[model.style]) warnings.push('style="' + model.style + '" 未知,回落商务蓝');
1919
+ if (model.theme && model.theme !== 'light' && model.theme !== 'dark') {
1920
+ warnings.push('theme="' + model.theme + '" 未知(应为 light/dark),按浅色导出');
1921
+ }
1922
+ if (secs.length < ((M.model && M.model.sectionsRecommended) || 3)) {
1923
+ warnings.push('章节页仅 ' + secs.length + ' 页,正式报告建议 ≥3');
1924
+ }
1925
+ var agMax = (M.model && M.model.agendaComfortMax) || 16;
1926
+ if ((model.agenda || []).length > agMax) {
1927
+ warnings.push('agenda ' + model.agenda.length + ' 条超出单页舒适上限 ' + agMax + ',建议拆分');
1928
+ }
1929
+ var pages = 1 + (hasAgenda ? 1 : 0) + secs.length + 1; /* 封面+大纲(可选)+章节+收尾 */
1930
+ return { ok: missing.length === 0, missing: missing, warnings: warnings, pages: pages };
1931
+ }
1932
+
1933
+ var api = {
1934
+ slidesXml: slidesXml,
1935
+ validateModel: validateModel,
1936
+ /* 以下供 scripts/gen_channel_a.js 在 Node 侧组装完整 PPTX 包(回归双裁判内部质量路径) */
1937
+ slidesOf: slidesOf, slideXml: slideXml, relsXml: relsXml, NS: NS, RT: RT, PRESETS: PRESETS,
1938
+ PRESETS_DARK: (typeof PRESETS_DARK !== 'undefined' ? PRESETS_DARK : null),
1939
+ dataColors: dataColors,
1940
+ esc: esc, solid: solid, E: E
1941
+ };
1942
+ if (typeof module !== 'undefined' && module.exports) module.exports = api; /* Node:gen_channel_a.js 用 */
1943
+ g.TopPptHtml = api;
1944
+ })(typeof window !== 'undefined' ? window : globalThis);