@lemonppt/renderer 0.2.0 → 0.2.2

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 (72) hide show
  1. package/dist/client/client-render.js +65 -5401
  2. package/dist/client/editor-script.js +3355 -0
  3. package/dist/client/theme-echarts.js +65 -25
  4. package/dist/client-render.d.ts +1 -7
  5. package/dist/client-render.d.ts.map +1 -1
  6. package/dist/client-render.js +1 -11
  7. package/dist/client-render.js.map +1 -1
  8. package/dist/editor-script.d.ts +1 -1
  9. package/dist/editor-script.d.ts.map +1 -1
  10. package/dist/editor-script.js +80 -77
  11. package/dist/editor-script.js.map +1 -1
  12. package/dist/export-pdf.js +1 -1
  13. package/dist/export-pdf.js.map +1 -1
  14. package/dist/export-pptx.d.ts +1132 -2
  15. package/dist/export-pptx.d.ts.map +1 -1
  16. package/dist/export-pptx.js +1208 -19525
  17. package/dist/export-pptx.js.map +1 -1
  18. package/dist/index.d.ts +1 -0
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +1 -0
  21. package/dist/index.js.map +1 -1
  22. package/dist/render-editor-data.d.ts +21 -0
  23. package/dist/render-editor-data.d.ts.map +1 -0
  24. package/dist/render-editor-data.js +1068 -0
  25. package/dist/render-editor-data.js.map +1 -0
  26. package/dist/render.d.ts +0 -2
  27. package/dist/render.d.ts.map +1 -1
  28. package/dist/render.js +7 -323
  29. package/dist/render.js.map +1 -1
  30. package/dist/theme01-pptx.d.ts +12 -0
  31. package/dist/theme01-pptx.d.ts.map +1 -0
  32. package/dist/theme01-pptx.js +2954 -0
  33. package/dist/theme01-pptx.js.map +1 -0
  34. package/dist/theme02-pptx.d.ts +5 -0
  35. package/dist/theme02-pptx.d.ts.map +1 -0
  36. package/dist/theme02-pptx.js +1063 -0
  37. package/dist/theme02-pptx.js.map +1 -0
  38. package/dist/theme03-pptx.d.ts +5 -0
  39. package/dist/theme03-pptx.d.ts.map +1 -0
  40. package/dist/theme03-pptx.js +3279 -0
  41. package/dist/theme03-pptx.js.map +1 -0
  42. package/dist/theme04-pptx.d.ts +6 -0
  43. package/dist/theme04-pptx.d.ts.map +1 -0
  44. package/dist/theme04-pptx.js +5496 -0
  45. package/dist/theme04-pptx.js.map +1 -0
  46. package/dist/theme05-pptx.d.ts +5 -0
  47. package/dist/theme05-pptx.d.ts.map +1 -0
  48. package/dist/theme05-pptx.js +3152 -0
  49. package/dist/theme05-pptx.js.map +1 -0
  50. package/dist/theme06-pptx.d.ts +124 -0
  51. package/dist/theme06-pptx.d.ts.map +1 -0
  52. package/dist/theme06-pptx.js +8154 -0
  53. package/dist/theme06-pptx.js.map +1 -0
  54. package/dist/theme07-pptx.d.ts +49 -0
  55. package/dist/theme07-pptx.d.ts.map +1 -0
  56. package/dist/theme07-pptx.js +2579 -0
  57. package/dist/theme07-pptx.js.map +1 -0
  58. package/dist/theme08-pptx.d.ts +19 -0
  59. package/dist/theme08-pptx.d.ts.map +1 -0
  60. package/dist/theme08-pptx.js +1411 -0
  61. package/dist/theme08-pptx.js.map +1 -0
  62. package/dist/theme09-pptx.d.ts +18 -0
  63. package/dist/theme09-pptx.d.ts.map +1 -0
  64. package/dist/theme09-pptx.js +1513 -0
  65. package/dist/theme09-pptx.js.map +1 -0
  66. package/dist/theme10-pptx.d.ts +18 -0
  67. package/dist/theme10-pptx.d.ts.map +1 -0
  68. package/dist/theme10-pptx.js +2889 -0
  69. package/dist/theme10-pptx.js.map +1 -0
  70. package/package.json +7 -6
  71. package/templates/editor.html +580 -0
  72. package/templates/editor.js +222 -0
@@ -0,0 +1,1513 @@
1
+ // lemonPPT - AI-powered presentation generation
2
+ // Copyright (c) 2026 lemonforme
3
+ // SPDX-License-Identifier: AGPL-3.0-or-later
4
+ //
5
+ // theme09 墨韵专色 · 杂志印刷风 — PPTX 版式专属渲染器。
6
+ // 解决 theme09 在 PPTX 导出中落到 "Unknown layout" 占位的问题。
7
+ //
8
+ // 设计要点:
9
+ // - 双基底(substrate)—— 每个版式**预分配**基底,翻页形成杂志跨页呼吸节奏:
10
+ // paper 纸底 #F4F1EA(数据/目录/正文密集页)
11
+ // ink 墨底 #14161C(影像/章节/金句/跨页)
12
+ // - appearance(primary / muted)不翻转明暗,只调专色浓度与网点密度。
13
+ // - 所有色值取自 tokens.ts 的预校验专色系统,未复制 third-party theme09 实现。
14
+ let S = {
15
+ appearance: 'primary',
16
+ fonts: { heading: 'Noto Serif SC', body: 'Noto Sans SC', mono: 'Space Mono' },
17
+ chartColors: ['C8102E', '2B4A8B', '8F6410', '3F6B5C', '6D4C7D', 'A8562A'],
18
+ };
19
+ /** 每次导出开始时由 export-pptx.ts 注入 appearance / 字体 / 图表色。 */
20
+ export function configureTheme09(state) {
21
+ S = state;
22
+ }
23
+ // ────────────────────────────────────────────────────────────────
24
+ // 双基底专色系统(与 tokens.ts 同源,PPTX 不支持 color-mix 故固化)
25
+ // ────────────────────────────────────────────────────────────────
26
+ const PAPER = {
27
+ bg: 'F4F1EA',
28
+ ink: '14161C',
29
+ ink2: '3A3F4A',
30
+ ink3: '5C6270',
31
+ surface: 'FDFCF8',
32
+ surfaceElevated: 'FFFFFF',
33
+ border: 'D5CFC0',
34
+ borderStrong: 'B7AE99',
35
+ };
36
+ const INK = {
37
+ bg: '14161C',
38
+ ink: 'F4F1EA',
39
+ ink2: 'C9C6BE',
40
+ ink3: '8E8B84',
41
+ surface: '1C2027',
42
+ surfaceElevated: '242932',
43
+ border: '2E323C',
44
+ borderStrong: '4A4F5B',
45
+ };
46
+ const SPOT = {
47
+ paper: {
48
+ primary: { accent: 'C8102E', accent2: '2B4A8B', spot3: '8F6410', spot4: '3F6B5C', spot5: '6D4C7D', spot6: 'A8562A' },
49
+ muted: { accent: '9C2233', accent2: '3C5580', spot3: '7A5518', spot4: '3A5F53', spot5: '61496D', spot6: '8F4E2C' },
50
+ },
51
+ ink: {
52
+ primary: { accent: 'F04A62', accent2: '5B82D6', spot3: 'D9A83C', spot4: '6FA894', spot5: 'A98BC4', spot6: 'E08A5B' },
53
+ muted: { accent: 'D07080', accent2: '7E9BC8', spot3: 'C0A068', spot4: '8AAFA0', spot5: 'B0A0C4', spot6: 'CE9878' },
54
+ },
55
+ };
56
+ /** 版式 → 预分配基底(与 Sheet substrate 完全一致)。 */
57
+ const SUBSTRATE = {
58
+ theme09_cover_masthead_v1: 'ink',
59
+ theme09_cover_bleed_v1: 'ink',
60
+ theme09_cover_dossier_v1: 'paper',
61
+ theme09_cover_colorbar_v1: 'paper',
62
+ theme09_cover_aperture_v1: 'ink',
63
+ theme09_cover_colophon_v1: 'paper',
64
+ theme09_cover_photo_v1: 'ink',
65
+ theme09_abstract_v1: 'paper',
66
+ theme09_contents_v1: 'paper',
67
+ theme09_section_v1: 'ink',
68
+ theme09_section_card_v1: 'paper',
69
+ theme09_closing_v1: 'ink',
70
+ 'theme09_cross-perspective_v1': 'ink',
71
+ theme09_thesis_v1: 'paper',
72
+ 'theme09_value-chain_v1': 'paper',
73
+ theme09_risk_v1: 'ink',
74
+ theme09_outlook_v1: 'ink',
75
+ theme09_conclusion_v1: 'paper',
76
+ theme09_bracket_v1: 'paper',
77
+ theme09_flow_v1: 'ink',
78
+ theme09_orbit_v1: 'ink',
79
+ theme09_vertical_v1: 'paper',
80
+ theme09_calendar_v1: 'paper',
81
+ theme09_phases_v1: 'ink',
82
+ theme09_gauge_v1: 'ink',
83
+ theme09_scoreboard_v1: 'ink',
84
+ theme09_trend_v1: 'paper',
85
+ theme09_histogram_v1: 'paper',
86
+ 'theme09_forecast-fan_v1': 'ink',
87
+ theme09_plans_v1: 'paper',
88
+ theme09_stair_v1: 'ink',
89
+ theme09_stacked_v1: 'paper',
90
+ theme09_era_v1: 'ink',
91
+ theme09_roadmap_v1: 'paper',
92
+ theme09_score_v1: 'ink',
93
+ theme09_takeaway_v1: 'ink',
94
+ theme09_compare_v1: 'paper',
95
+ theme09_process_v1: 'paper',
96
+ theme09_faq_v1: 'paper',
97
+ // ── P2 图表型(数据/目录/密集页,纸底)──
98
+ theme09_market_overview_v1: 'paper',
99
+ 'theme09_streamgraph_v1': 'paper',
100
+ 'theme09_chord_v1': 'paper',
101
+ 'theme09_sunburst_v1': 'paper',
102
+ 'theme09_ribbon_v1': 'paper',
103
+ 'theme09_rounds_v1': 'paper',
104
+ 'theme09_ranking_v1': 'paper',
105
+ 'theme09_bump_v1': 'paper',
106
+ 'theme09_hero_number_v1': 'paper',
107
+ 'theme09_versus_v1': 'paper',
108
+ 'theme09_spiral_v1': 'paper',
109
+ 'theme09_funnel_v1': 'paper',
110
+ 'theme09_stat_grid_v1': 'paper',
111
+ 'theme09_arc_v1': 'paper',
112
+ 'theme09_network_v1': 'paper',
113
+ 'theme09_area_v1': 'paper',
114
+ 'theme09_mega_number_v1': 'paper',
115
+ 'theme09_radar_v1': 'paper',
116
+ 'theme09_radialbar_v1': 'paper',
117
+ 'theme09_honeycomb_v1': 'paper',
118
+ 'theme09_tornado_v1': 'paper',
119
+ 'theme09_matrix_v1': 'paper',
120
+ 'theme09_quadrant_v1': 'paper',
121
+ 'theme09_bubble_v1': 'paper',
122
+ 'theme09_marimekko_v1': 'paper',
123
+ 'theme09_meter_v1': 'paper',
124
+ 'theme09_parallel_v1': 'paper',
125
+ 'theme09_grade_v1': 'paper',
126
+ 'theme09_slope_v1': 'paper',
127
+ 'theme09_dumbbell_v1': 'paper',
128
+ 'theme09_crosstab_v1': 'paper',
129
+ 'theme09_tier_v1': 'paper',
130
+ 'theme09_ledger_v1': 'paper',
131
+ 'theme09_alloc_v1': 'paper',
132
+ 'theme09_venn_v1': 'paper',
133
+ 'theme09_treemap_v1': 'paper',
134
+ 'theme09_icicle_v1': 'paper',
135
+ 'theme09_waterfall_v1': 'paper',
136
+ 'theme09_heatmap_v1': 'paper',
137
+ // ── P1 图片 / 标本 / 金句型(影像/跨页,墨底)──
138
+ 'theme09_specimen_v1': 'ink',
139
+ 'theme09_photo_feature_v1': 'ink',
140
+ 'theme09_photo_grid_v1': 'ink',
141
+ 'theme09_photo_quote_v1': 'ink',
142
+ 'theme09_photo_duo_v1': 'ink',
143
+ 'theme09_photo_panorama_v1': 'ink',
144
+ 'theme09_photo_stage_v1': 'ink',
145
+ 'theme09_storyboard_v1': 'ink',
146
+ 'theme09_snapshot_tape_v1': 'ink',
147
+ 'theme09_epigraph_v1': 'ink',
148
+ 'theme09_photo_bento_v1': 'ink',
149
+ 'theme09_timeline_photo_v1': 'ink',
150
+ 'theme09_filmstrip_v1': 'ink',
151
+ 'theme09_mosaic_v1': 'ink',
152
+ 'theme09_testimonial_v1': 'ink',
153
+ 'theme09_team_v1': 'ink',
154
+ 'theme09_photo_ring_v1': 'ink',
155
+ 'theme09_divider_photo_v1': 'ink',
156
+ 'theme09_coverstory_v1': 'ink',
157
+ 'theme09_quote_portrait_v1': 'ink',
158
+ 'theme09_manifesto_v1': 'ink',
159
+ 'theme09_annotated_v1': 'ink',
160
+ 'theme09_diptych_v1': 'ink',
161
+ 'theme09_case_folio_v1': 'ink',
162
+ 'theme09_split_diagonal_v1': 'ink',
163
+ 'theme09_typeriver_v1': 'ink',
164
+ 'theme09_exhibit_wall_v1': 'ink',
165
+ 'theme09_masonry_v1': 'ink',
166
+ 'theme09_journey_v1': 'ink',
167
+ 'theme09_photo_cards_v1': 'ink',
168
+ 'theme09_zine_spread_v1': 'ink',
169
+ 'theme09_photo_scene_v1': 'ink',
170
+ 'theme09_spotlight_v1': 'ink',
171
+ 'theme09_profile_v1': 'ink',
172
+ 'theme09_gallery_wall_v1': 'ink',
173
+ 'theme09_dotmatrix_v1': 'ink',
174
+ };
175
+ function P(layoutId) {
176
+ const sub = SUBSTRATE[layoutId] ?? 'paper';
177
+ const base = sub === 'ink' ? INK : PAPER;
178
+ const spot = SPOT[sub][S.appearance];
179
+ return { sub, isInk: sub === 'ink', ...base, ...spot };
180
+ }
181
+ function series(pal) {
182
+ return [pal.accent, pal.accent2, pal.spot3, pal.spot4, pal.spot5, pal.spot6];
183
+ }
184
+ // ────────────────────────────────────────────────────────────────
185
+ // 画布常量(16:9,10 × 5.625 英寸)
186
+ // ────────────────────────────────────────────────────────────────
187
+ const MX = 0.7;
188
+ const CW = 10 - MX * 2;
189
+ const TOP = 0.62;
190
+ const BOT = 5.18;
191
+ function lines(v) {
192
+ if (v == null)
193
+ return [];
194
+ if (Array.isArray(v))
195
+ return v.map(String);
196
+ return String(v).split(/\r?\n/).filter((s) => s.length > 0);
197
+ }
198
+ function txt(slide, text, o) {
199
+ if (text == null || text === '')
200
+ return;
201
+ if (Array.isArray(text)) {
202
+ if (text.length === 0)
203
+ return;
204
+ text = text.map(String).join('\n');
205
+ }
206
+ slide.addText(text, o);
207
+ }
208
+ function frame(slide, pal, opts = {}) {
209
+ slide.background = { color: pal.bg };
210
+ slide.addShape('line', { x: MX, y: TOP, w: CW, h: 0, line: { color: pal.border, width: 0.75 } });
211
+ if (opts.bottom !== false) {
212
+ slide.addShape('line', { x: MX, y: BOT, w: CW, h: 0, line: { color: pal.border, width: 0.75 } });
213
+ }
214
+ }
215
+ function kicker(slide, text, pal, o = {}) {
216
+ txt(slide, text, {
217
+ x: o.x ?? MX, y: o.y ?? 0.74, w: o.w ?? CW, h: 0.3,
218
+ fontSize: o.size ?? 12, color: pal.accent, align: 'left',
219
+ fontFace: S.fonts.mono, charSpacing: 2, bold: true,
220
+ });
221
+ }
222
+ function title(slide, text, pal, o = {}) {
223
+ const ls = lines(text);
224
+ const size = o.size ?? 40;
225
+ const h = o.h ?? ls.length * (size / 72) * 1.18 + 0.15;
226
+ slide.addText(ls.join('\n'), {
227
+ x: o.x ?? MX, y: o.y ?? 1.0, w: o.w ?? CW, h,
228
+ fontSize: size, color: o.color ?? pal.ink, align: o.align ?? 'left',
229
+ fontFace: S.fonts.heading, bold: true, valign: o.valign ?? 'top',
230
+ lineSpacingMultiple: 1.05, rotate: o.rotate,
231
+ });
232
+ }
233
+ function footer(slide, left, right, pal) {
234
+ txt(slide, left, { x: MX, y: BOT + 0.04, w: CW / 2, h: 0.3, fontSize: 9, color: pal.ink3, align: 'left', fontFace: S.fonts.mono });
235
+ txt(slide, right, { x: MX + CW / 2, y: BOT + 0.04, w: CW / 2, h: 0.3, fontSize: 9, color: pal.ink3, align: 'right', fontFace: S.fonts.mono });
236
+ }
237
+ function card(slide, x, y, w, h, pal, fill, stroke) {
238
+ slide.addShape('rect', { x, y, w, h, fill: { color: fill ?? pal.surfaceElevated }, line: { color: stroke ?? pal.borderStrong, width: 1 } });
239
+ }
240
+ function colorBar(slide, x, y, w, pal) {
241
+ const n = 6;
242
+ const g = 0.08;
243
+ const cw = (w - g * (n - 1)) / n;
244
+ const s = series(pal);
245
+ for (let i = 0; i < n; i++) {
246
+ slide.addShape('rect', { x: x + i * (cw + g), y, w: cw, h: 0.16, fill: { color: s[i] } });
247
+ }
248
+ }
249
+ function masthead(slide, p, pal) {
250
+ txt(slide, p.section, { x: MX, y: 0.74, w: 4, h: 0.3, fontSize: 13, color: pal.ink, bold: true, align: 'left', fontFace: S.fonts.heading });
251
+ txt(slide, p.sectionEn, { x: MX, y: 1.06, w: 4, h: 0.25, fontSize: 10, color: pal.ink3, align: 'left', fontFace: S.fonts.mono, charSpacing: 1 });
252
+ txt(slide, p.mark, { x: MX + CW - 3, y: 0.74, w: 3, h: 0.3, fontSize: 11, color: pal.accent, align: 'right', bold: true, fontFace: S.fonts.mono });
253
+ slide.addShape('line', { x: MX, y: 1.42, w: CW, h: 0, line: { color: pal.ink, width: 1.25 } });
254
+ }
255
+ function folio(slide, p, pal) {
256
+ footer(slide, p.folioLeft, [p.folioPage, p.folioRight].filter(Boolean).join(' / '), pal);
257
+ }
258
+ function apertureFallback(slide, x, y, d, pal) {
259
+ slide.addShape('ellipse', { x, y, w: d, h: d, fill: { color: pal.surfaceElevated }, line: { color: pal.border, width: 1 } });
260
+ txt(slide, 'IMG', { x, y, w: d, h: d, fontSize: 16, color: pal.ink3, align: 'center', valign: 'middle', bold: true, fontFace: S.fonts.mono });
261
+ }
262
+ function toImgUrl(v) {
263
+ if (!v)
264
+ return undefined;
265
+ if (typeof v === 'string')
266
+ return v;
267
+ if (typeof v === 'object') {
268
+ const o = v;
269
+ for (const k of ['url', 'src', 'href', 'path', 'image']) {
270
+ const val = o[k];
271
+ if (typeof val === 'string' && val.length)
272
+ return val;
273
+ }
274
+ }
275
+ return undefined;
276
+ }
277
+ function tryImage(slide, url, x, y, w, h, fallback) {
278
+ const u = toImgUrl(url);
279
+ // 仅接受可直读的本地路径或远程 http(s) 资源;data: URI 等内联占位图在 pptxgenjs 写盘时
280
+ // 会触发 ENAMETOOLONG/无法读取,故直接降级为 fallback 卡片。
281
+ if (!u || (!u.startsWith('http://') && !u.startsWith('https://') && !u.startsWith('/') && !u.startsWith('.'))) {
282
+ fallback();
283
+ return;
284
+ }
285
+ try {
286
+ slide.addImage({ path: u, x, y, w, h });
287
+ }
288
+ catch {
289
+ fallback();
290
+ }
291
+ }
292
+ // =====================================================================
293
+ // 1. cover · 刊头封面(ink)
294
+ // =====================================================================
295
+ function renderCoverMasthead(slide, p) {
296
+ const pal = P('theme09_cover_masthead_v1');
297
+ slide.background = { color: pal.bg };
298
+ slide.addShape('rect', { x: 0, y: 0, w: 10, h: 0.34, fill: { color: pal.accent } });
299
+ txt(slide, [p.issueEn, p.issue].filter(Boolean).join(' · '), {
300
+ x: MX, y: 0, w: CW, h: 0.34, fontSize: 11, color: pal.bg, align: 'center', valign: 'middle', bold: true, fontFace: S.fonts.mono, charSpacing: 2,
301
+ });
302
+ title(slide, p.title, pal, { x: MX, y: 0.72, w: CW, size: 46, align: 'center' });
303
+ txt(slide, [p.strapline, p.straplineEn].filter(Boolean).join(' · '), {
304
+ x: MX, y: 1.55, w: CW, h: 0.32, fontSize: 13, color: pal.accent, align: 'center', fontFace: S.fonts.mono, charSpacing: 1, bold: true,
305
+ });
306
+ title(slide, p.kick, pal, { x: MX, y: 2.05, w: CW, size: 30, align: 'center' });
307
+ txt(slide, p.lead, { x: MX, y: 3.05, w: CW * 0.6, h: 1.5, fontSize: 14, color: pal.ink2, align: 'left', fontFace: S.fonts.body, valign: 'top', lineSpacingMultiple: 1.25 });
308
+ const items = (p.items || []).slice(0, 5);
309
+ let iy = 3.05;
310
+ items.forEach((t, i) => {
311
+ txt(slide, String(i + 1).padStart(2, '0'), { x: MX + CW * 0.62, y: iy, w: 0.5, h: 0.3, fontSize: 13, color: pal.accent, align: 'left', bold: true, fontFace: S.fonts.mono });
312
+ txt(slide, t, { x: MX + CW * 0.62 + 0.5, y: iy, w: CW * 0.38 - 0.5, h: 0.3, fontSize: 12, color: pal.ink, align: 'left', fontFace: S.fonts.body });
313
+ iy += 0.34;
314
+ });
315
+ colorBar(slide, MX, 4.75, 3.2, pal);
316
+ txt(slide, p.sign, { x: MX + 3.5, y: 4.7, w: CW - 3.5, h: 0.32, fontSize: 10, color: pal.ink3, align: 'right', fontFace: S.fonts.mono, charSpacing: 1 });
317
+ }
318
+ // =====================================================================
319
+ // 2. cover · 出血斜切封面(ink)
320
+ // =====================================================================
321
+ function renderCoverBleed(slide, p) {
322
+ const pal = P('theme09_cover_bleed_v1');
323
+ slide.background = { color: pal.bg };
324
+ slide.addShape('rect', { x: 0, y: 0, w: 3.2, h: 5.625, fill: { color: pal.accent } });
325
+ txt(slide, p.railTop, { x: 0.35, y: 0.5, w: 2.5, h: 0.3, fontSize: 12, color: pal.bg, align: 'left', fontFace: S.fonts.mono, bold: true, charSpacing: 2 });
326
+ txt(slide, p.vertical, { x: 0.7, y: 0.7, w: 2.0, h: 4.0, fontSize: 26, color: pal.bg, align: 'center', valign: 'middle', bold: true, fontFace: S.fonts.heading, rotate: 270 });
327
+ txt(slide, p.railFoot, { x: 0.35, y: 5.05, w: 2.5, h: 0.3, fontSize: 11, color: pal.bg, align: 'left', fontFace: S.fonts.mono, charSpacing: 2 });
328
+ const rx = 3.7;
329
+ title(slide, p.title, pal, { x: rx, y: 1.3, w: 5.8, size: 44, color: pal.ink });
330
+ txt(slide, p.subtitle, { x: rx, y: 3.0, w: 5.8, h: 1.0, fontSize: 15, color: pal.ink2, align: 'left', fontFace: S.fonts.body, valign: 'top' });
331
+ const meta = (p.metaItems || []).slice(0, 4);
332
+ let my = 4.2;
333
+ meta.forEach((m) => {
334
+ txt(slide, m.k, { x: rx, y: my, w: 1.6, h: 0.3, fontSize: 10, color: pal.accent, align: 'left', bold: true, fontFace: S.fonts.mono });
335
+ txt(slide, m.v, { x: rx + 1.6, y: my, w: 4.0, h: 0.3, fontSize: 12, color: pal.ink, align: 'left', fontFace: S.fonts.body });
336
+ my += 0.38;
337
+ });
338
+ slide.addShape('line', { x: rx, y: 5.18, w: 10 - rx - MX, h: 0, line: { color: pal.border, width: 0.75 } });
339
+ }
340
+ // =====================================================================
341
+ // 3. cover · 卷宗封面(paper)
342
+ // =====================================================================
343
+ function renderCoverDossier(slide, p) {
344
+ const pal = P('theme09_cover_dossier_v1');
345
+ frame(slide, pal);
346
+ if (p.classif) {
347
+ slide.addShape('rect', { x: MX, y: 0.85, w: 1.9, h: 0.6, fill: { color: pal.bg }, line: { color: pal.accent, width: 1.5 } });
348
+ txt(slide, p.classif, { x: MX, y: 0.85, w: 1.9, h: 0.6, fontSize: 14, color: pal.accent, align: 'center', valign: 'middle', bold: true, fontFace: S.fonts.mono, charSpacing: 2 });
349
+ if (p.stampNote)
350
+ txt(slide, p.stampNote, { x: MX + 2.1, y: 0.95, w: 3, h: 0.45, fontSize: 11, color: pal.ink2, align: 'left', valign: 'middle', fontFace: S.fonts.body });
351
+ }
352
+ title(slide, p.title, pal, { x: MX, y: 1.7, w: 5.6, size: 40 });
353
+ txt(slide, p.subtitle, { x: MX, y: 2.6, w: 5.6, h: 1.1, fontSize: 14, color: pal.ink2, align: 'left', fontFace: S.fonts.body, valign: 'top' });
354
+ const fields = (p.fields || []).slice(0, 6);
355
+ const colW = (CW - 0.4) / 2;
356
+ fields.forEach((f, i) => {
357
+ const cx = MX + (i % 2) * (colW + 0.4);
358
+ const cy = 3.9 + Math.floor(i / 2) * 0.36;
359
+ txt(slide, f.k, { x: cx, y: cy, w: 1.4, h: 0.3, fontSize: 10, color: pal.ink3, align: 'left', fontFace: S.fonts.mono });
360
+ txt(slide, f.v, { x: cx + 1.4, y: cy, w: colW - 1.4, h: 0.3, fontSize: 12, color: pal.ink, align: 'left', bold: true, fontFace: S.fonts.body });
361
+ });
362
+ const sx = MX + 6.4;
363
+ if (p.memo)
364
+ txt(slide, p.memo, { x: sx, y: 1.7, w: CW - sx + MX, h: 1.4, fontSize: 14, color: pal.ink, align: 'left', fontFace: 'Caveat', valign: 'top' });
365
+ if (p.chop) {
366
+ slide.addShape('ellipse', { x: sx + 0.2, y: 3.2, w: 1.1, h: 1.1, fill: { color: pal.bg }, line: { color: pal.accent, width: 2 } });
367
+ txt(slide, p.chop, { x: sx + 0.2, y: 3.2, w: 1.1, h: 1.1, fontSize: 16, color: pal.accent, align: 'center', valign: 'middle', bold: true, fontFace: S.fonts.heading });
368
+ }
369
+ const meta = (p.metaLines || []).slice(0, 4);
370
+ let my = 4.5;
371
+ meta.forEach((t) => {
372
+ txt(slide, t, { x: sx, y: my, w: CW - sx + MX, h: 0.28, fontSize: 9, color: pal.ink3, align: 'left', fontFace: S.fonts.mono, charSpacing: 1 });
373
+ my += 0.3;
374
+ });
375
+ }
376
+ // =====================================================================
377
+ // 4. cover · 色标封面(paper)
378
+ // =====================================================================
379
+ function renderCoverColorbar(slide, p) {
380
+ const pal = P('theme09_cover_colorbar_v1');
381
+ frame(slide, pal);
382
+ const s1 = series(pal);
383
+ const cells = [...s1, pal.ink, pal.ink3];
384
+ const codes = ['S1', 'S2', 'S3', 'S4', 'S5', 'S6', 'K100', 'K40'];
385
+ const n = 8;
386
+ const g = 0.1;
387
+ const cw = (CW - g * (n - 1)) / n;
388
+ const gy = 0.85;
389
+ const ch = 0.9;
390
+ cells.forEach((c, i) => {
391
+ const x = MX + i * (cw + g);
392
+ slide.addShape('rect', { x, y: gy, w: cw, h: ch, fill: { color: c }, line: { color: pal.border, width: 0.5 } });
393
+ txt(slide, codes[i], { x, y: gy + ch + 0.02, w: cw, h: 0.22, fontSize: 8, color: pal.ink3, align: 'center', fontFace: S.fonts.mono });
394
+ });
395
+ title(slide, p.title, pal, { x: MX, y: 2.1, w: 5.6, size: 44 });
396
+ txt(slide, p.subtitle, { x: MX, y: 3.1, w: 5.6, h: 1.2, fontSize: 14, color: pal.ink2, align: 'left', fontFace: S.fonts.body, valign: 'top' });
397
+ const specs = (p.specs || []).slice(0, 6);
398
+ const sx = MX + 5.9;
399
+ let sy = 2.1;
400
+ specs.forEach((sp) => {
401
+ txt(slide, sp.k, { x: sx, y: sy, w: 1.6, h: 0.3, fontSize: 10, color: pal.ink3, align: 'left', fontFace: S.fonts.mono });
402
+ txt(slide, sp.v, { x: sx + 1.6, y: sy, w: CW - sx - MX - 1.6, h: 0.3, fontSize: 13, color: pal.ink, align: 'left', bold: true, fontFace: S.fonts.body });
403
+ sy += 0.4;
404
+ });
405
+ footer(slide, p.footLeft, p.footRight, pal);
406
+ }
407
+ // =====================================================================
408
+ // 5. cover · 圆窗封面(ink)
409
+ // =====================================================================
410
+ function renderCoverAperture(slide, p) {
411
+ const pal = P('theme09_cover_aperture_v1');
412
+ frame(slide, pal, { bottom: false });
413
+ if (p.tag)
414
+ kicker(slide, p.tag, pal, { x: MX, y: 0.85 });
415
+ title(slide, p.title, pal, { x: MX, y: 1.25, w: 5.4, size: 42 });
416
+ txt(slide, p.subtitle, { x: MX, y: 3.3, w: 5.4, h: 1.2, fontSize: 14, color: pal.ink2, align: 'left', fontFace: S.fonts.body, valign: 'top' });
417
+ const tags = (p.tags || []).slice(0, 5);
418
+ let ty = 4.6;
419
+ tags.forEach((t) => {
420
+ const w = 0.3 + t.length * 0.16;
421
+ slide.addShape('roundRect', { x: MX, y: ty, w, h: 0.34, rectRadius: 0.17, fill: { color: pal.bg }, line: { color: pal.accent, width: 1 } });
422
+ txt(slide, t, { x: MX, y: ty, w, h: 0.34, fontSize: 11, color: pal.accent, align: 'center', valign: 'middle', fontFace: S.fonts.mono });
423
+ ty += 0.42;
424
+ });
425
+ const cx = 6.7;
426
+ const cy = 1.0;
427
+ const d = 3.0;
428
+ tryImage(slide, p.imageUrl, cx, cy, d, d, () => apertureFallback(slide, cx, cy, d, pal));
429
+ slide.addShape('ellipse', { x: cx - 0.06, y: cy - 0.06, w: d + 0.12, h: d + 0.12, fill: { color: 'FFFFFF', transparency: 100 }, line: { color: pal.accent, width: 2 } });
430
+ footer(slide, p.tag, 'APERTURE', pal);
431
+ }
432
+ // =====================================================================
433
+ // 6. cover · 版本页封面(paper)
434
+ // =====================================================================
435
+ function renderCoverColophon(slide, p) {
436
+ const pal = P('theme09_cover_colophon_v1');
437
+ frame(slide, pal);
438
+ title(slide, p.title, pal, { x: MX, y: 0.85, w: CW, size: 40 });
439
+ txt(slide, p.titleEn, { x: MX, y: 1.65, w: CW, h: 0.3, fontSize: 13, color: pal.ink3, align: 'left', fontFace: S.fonts.mono, charSpacing: 1 });
440
+ const cols = (p.cols || []).slice(0, 4);
441
+ const gap = 0.3;
442
+ const cw = (CW - gap * (cols.length - 1)) / cols.length;
443
+ cols.forEach((c, i) => {
444
+ const x = MX + i * (cw + gap);
445
+ txt(slide, c.heading, { x, y: 2.2, w: cw, h: 0.3, fontSize: 13, color: pal.accent, align: 'left', bold: true, fontFace: S.fonts.heading });
446
+ slide.addShape('line', { x, y: 2.56, w: cw, h: 0, line: { color: pal.ink, width: 1 } });
447
+ (c.lines || []).slice(0, 6).forEach((ln, j) => {
448
+ txt(slide, ln, { x, y: 2.66 + j * 0.34, w: cw, h: 0.32, fontSize: 11, color: pal.ink2, align: 'left', fontFace: S.fonts.body });
449
+ });
450
+ });
451
+ colorBar(slide, MX, 4.9, 3.2, pal);
452
+ txt(slide, p.footLeft, { x: MX + 3.5, y: 4.86, w: CW - 3.5, h: 0.3, fontSize: 10, color: pal.ink3, align: 'right', fontFace: S.fonts.mono, charSpacing: 1 });
453
+ }
454
+ // =====================================================================
455
+ // 7. cover · 影像封面(ink)
456
+ // =====================================================================
457
+ function renderCoverPhoto(slide, p) {
458
+ const pal = P('theme09_cover_photo_v1');
459
+ slide.background = { color: pal.bg };
460
+ tryImage(slide, p.imageUrl, 0, 0, 10, 5.625, () => { });
461
+ slide.addShape('rect', { x: 0, y: 3.4, w: 10, h: 2.225, fill: { color: '000000', transparency: 35 } });
462
+ if (p.strip) {
463
+ const w = 0.3 + p.strip.length * 0.12;
464
+ slide.addShape('rect', { x: MX, y: 0.9, w, h: 0.36, fill: { color: pal.accent } });
465
+ txt(slide, p.strip, { x: MX, y: 0.9, w, h: 0.36, fontSize: 12, color: pal.bg, align: 'center', valign: 'middle', bold: true, fontFace: S.fonts.mono, charSpacing: 2 });
466
+ }
467
+ title(slide, p.title, pal, { x: MX, y: 3.7, w: CW, size: 44, color: pal.ink });
468
+ txt(slide, p.subtitle, { x: MX, y: 4.6, w: CW, h: 0.7, fontSize: 14, color: pal.ink2, align: 'left', fontFace: S.fonts.body, valign: 'top' });
469
+ const meta = (p.metaItems || []).slice(0, 4);
470
+ txt(slide, meta.join(' / '), { x: MX, y: 5.25, w: CW, h: 0.3, fontSize: 11, color: pal.ink2, align: 'left', fontFace: S.fonts.mono, charSpacing: 1 });
471
+ }
472
+ // =====================================================================
473
+ // 8. content · 卷首摘要(paper)
474
+ // =====================================================================
475
+ function renderAbstract(slide, p) {
476
+ const pal = P('theme09_abstract_v1');
477
+ frame(slide, pal, { bottom: false });
478
+ masthead(slide, p, pal);
479
+ title(slide, p.title, pal, { x: MX, y: 1.7, w: 5.6, size: 34 });
480
+ txt(slide, p.standfirst, { x: MX, y: 2.3, w: 5.6, h: 2.6, fontSize: 14, color: pal.ink, align: 'left', fontFace: S.fonts.body, valign: 'top', lineSpacingMultiple: 1.25 });
481
+ const figs = (p.figures || []).slice(0, 5);
482
+ const sx = MX + 6.0;
483
+ slide.addShape('line', { x: sx - 0.3, y: 1.9, w: 0, h: 3.0, line: { color: pal.accent, width: 2 } });
484
+ let fy = 1.9;
485
+ figs.forEach((f) => {
486
+ txt(slide, [f.v, f.unit].filter(Boolean).join(''), { x: sx, y: fy, w: CW - sx + MX, h: 0.6, fontSize: 30, color: pal.accent, align: 'left', bold: true, fontFace: S.fonts.heading });
487
+ txt(slide, f.k, { x: sx, y: fy + 0.6, w: CW - sx + MX, h: 0.3, fontSize: 12, color: pal.ink2, align: 'left', fontFace: S.fonts.body });
488
+ fy += 0.95;
489
+ });
490
+ folio(slide, p, pal);
491
+ }
492
+ // =====================================================================
493
+ // 9. tableOfContents · 目录(paper)
494
+ // =====================================================================
495
+ function renderContents(slide, p) {
496
+ const pal = P('theme09_contents_v1');
497
+ frame(slide, pal, { bottom: false });
498
+ masthead(slide, p, pal);
499
+ title(slide, p.title, pal, { x: MX, y: 1.7, w: 6, size: 34 });
500
+ txt(slide, p.titleEn, { x: MX + 6, y: 1.85, w: 3, h: 0.3, fontSize: 13, color: pal.ink3, align: 'right', fontFace: S.fonts.mono });
501
+ const items = (p.items || []).slice(0, 10);
502
+ let y = 2.5;
503
+ const rowH = (5.0 - 2.5) / Math.max(items.length, 1);
504
+ items.forEach((it, i) => {
505
+ const col = it.current ? pal.accent : pal.ink;
506
+ txt(slide, String(i + 1).padStart(2, '0'), { x: MX, y, w: 0.6, h: rowH, fontSize: 14, color: col, align: 'left', valign: 'middle', bold: true, fontFace: S.fonts.mono });
507
+ txt(slide, it.name, { x: MX + 0.7, y, w: 4.0, h: rowH, fontSize: 15, color: col, align: 'left', valign: 'middle', bold: it.current, fontFace: S.fonts.heading });
508
+ txt(slide, it.desc || '', { x: MX + 4.8, y, w: 3.0, h: rowH, fontSize: 11, color: pal.ink3, align: 'left', valign: 'middle', fontFace: S.fonts.body });
509
+ txt(slide, it.page || '', { x: MX + CW - 1.0, y, w: 1.0, h: rowH, fontSize: 14, color: col, align: 'right', valign: 'middle', bold: true, fontFace: S.fonts.mono });
510
+ if (i < items.length - 1)
511
+ slide.addShape('line', { x: MX, y: y + rowH, w: CW, h: 0, line: { color: pal.border, width: 0.5 } });
512
+ y += rowH;
513
+ });
514
+ folio(slide, p, pal);
515
+ }
516
+ // =====================================================================
517
+ // 10. content · 篇章扉页(ink)
518
+ // =====================================================================
519
+ function renderSection(slide, p) {
520
+ const pal = P('theme09_section_v1');
521
+ frame(slide, pal, { bottom: false });
522
+ title(slide, p.num, pal, { x: 0.2, y: 3.0, w: 4.5, h: 2.4, size: 180, color: pal.accent, align: 'left', valign: 'bottom' });
523
+ txt(slide, p.name, { x: 8.6, y: 0.7, w: 1.2, h: 4.0, fontSize: 40, color: pal.ink, align: 'center', valign: 'middle', bold: true, fontFace: S.fonts.heading, rotate: 270 });
524
+ if (p.nameEn)
525
+ kicker(slide, p.nameEn, pal, { x: 4.6, y: 1.0 });
526
+ txt(slide, p.lede, { x: 4.6, y: 1.5, w: 3.8, h: 1.8, fontSize: 14, color: pal.ink2, align: 'left', fontFace: S.fonts.body, valign: 'top', lineSpacingMultiple: 1.2 });
527
+ const pts = (p.points || []).slice(0, 4);
528
+ let py = 3.5;
529
+ pts.forEach((t) => {
530
+ txt(slide, '— ' + t, { x: 4.6, y: py, w: 3.9, h: 0.35, fontSize: 12, color: pal.ink, align: 'left', fontFace: S.fonts.body });
531
+ py += 0.4;
532
+ });
533
+ folio(slide, p, pal);
534
+ }
535
+ // =====================================================================
536
+ // 11. content · 篇章卡(paper)
537
+ // =====================================================================
538
+ function renderSectionCard(slide, p) {
539
+ const pal = P('theme09_section_card_v1');
540
+ frame(slide, pal, { bottom: false });
541
+ masthead(slide, p, pal);
542
+ const cardX = MX;
543
+ const cardY = 1.7;
544
+ const cardW = 3.6;
545
+ const cardH = 3.0;
546
+ card(slide, cardX, cardY, cardW, cardH, pal, pal.surfaceElevated, pal.borderStrong);
547
+ slide.addShape('rect', { x: cardX, y: cardY, w: cardW, h: 0.12, fill: { color: pal.accent } });
548
+ slide.addShape('triangle', { x: cardX + cardW - 0.5, y: cardY, w: 0.5, h: 0.5, fill: { color: pal.bg }, line: { color: pal.border, width: 1 }, rotate: 180, flipH: true });
549
+ title(slide, p.num, pal, { x: cardX + 0.3, y: cardY + 0.3, w: cardW - 0.6, h: 1.0, size: 60, color: pal.accent });
550
+ title(slide, p.name, pal, { x: cardX + 0.3, y: cardY + 1.4, w: cardW - 0.6, h: 0.9, size: 28 });
551
+ if (p.nameEn)
552
+ txt(slide, p.nameEn, { x: cardX + 0.3, y: cardY + 2.3, w: cardW - 0.6, h: 0.3, fontSize: 12, color: pal.ink3, align: 'left', fontFace: S.fonts.mono });
553
+ const pts = (p.points || []).slice(0, 4);
554
+ const sx = MX + 4.0;
555
+ let py = 1.8;
556
+ pts.forEach((pt, i) => {
557
+ slide.addShape('line', { x: sx, y: py, w: CW - sx + MX, h: 0, line: { color: pal.borderStrong, width: 1.25 } });
558
+ txt(slide, String(i + 1).padStart(2, '0'), { x: sx, y: py + 0.1, w: 0.6, h: 0.3, fontSize: 13, color: pal.accent, align: 'left', bold: true, fontFace: S.fonts.mono });
559
+ txt(slide, pt.t, { x: sx + 0.7, y: py + 0.1, w: CW - sx - MX - 0.7, h: 0.3, fontSize: 16, color: pal.ink, align: 'left', bold: true, fontFace: S.fonts.heading });
560
+ txt(slide, pt.d || '', { x: sx + 0.7, y: py + 0.42, w: CW - sx - MX - 0.7, h: 0.5, fontSize: 12, color: pal.ink2, align: 'left', fontFace: S.fonts.body, valign: 'top' });
561
+ py += 0.95;
562
+ });
563
+ folio(slide, p, pal);
564
+ }
565
+ // =====================================================================
566
+ // 12. closing · 封底结语(ink)
567
+ // =====================================================================
568
+ function renderClosing(slide, p) {
569
+ const pal = P('theme09_closing_v1');
570
+ slide.background = { color: pal.bg };
571
+ tryImage(slide, p.imageUrl, 0, 0, 10, 5.625, () => { });
572
+ slide.addShape('rect', { x: 0, y: 0, w: 10, h: 5.625, fill: { color: '000000', transparency: 35 } });
573
+ slide.addShape('rect', { x: 0, y: 0, w: 10, h: 5.625, fill: { color: pal.bg, transparency: 30 } });
574
+ if (p.mark) {
575
+ const w = 0.3 + p.mark.length * 0.12;
576
+ slide.addShape('rect', { x: 5 - w / 2, y: 1.3, w, h: 0.36, fill: { color: pal.accent } });
577
+ txt(slide, p.mark, { x: 5 - w / 2, y: 1.3, w, h: 0.36, fontSize: 12, color: pal.bg, align: 'center', valign: 'middle', bold: true, fontFace: S.fonts.mono, charSpacing: 2 });
578
+ }
579
+ title(slide, p.word, pal, { x: 1, y: 2.0, w: 8, size: 48, align: 'center', color: pal.ink });
580
+ txt(slide, p.subtitle, { x: 1.5, y: 3.3, w: 7, h: 1.0, fontSize: 15, color: pal.ink2, align: 'center', fontFace: S.fonts.body, valign: 'top' });
581
+ const col = (p.colophon || []).slice(0, 4);
582
+ txt(slide, col.join(' · '), { x: 1, y: 4.7, w: 8, h: 0.3, fontSize: 11, color: pal.ink2, align: 'center', fontFace: S.fonts.mono, charSpacing: 1 });
583
+ }
584
+ // =====================================================================
585
+ // 注册
586
+ // =====================================================================
587
+ // theme09 P3 版式 PPTX 渲染函数(临时片段,由 _tmp_splice_p3.mjs 注入 theme09-pptx.ts)
588
+ // ────────────────────────────────────────────────────────────────
589
+ // P3 通用辅助
590
+ // ────────────────────────────────────────────────────────────────
591
+ // ────────────────────────────────────────────────────────────────
592
+ // P3 渲染器 v2(字段名对齐 Schema;标量数组兼容 {item}/string)
593
+ // ────────────────────────────────────────────────────────────────
594
+ function t9N(v) {
595
+ if (typeof v === 'number')
596
+ return v;
597
+ const n = parseFloat(String(v == null ? '' : v));
598
+ return Number.isFinite(n) ? n : 0;
599
+ }
600
+ /** 兼容标量数组项:纯字符串 或 {item}/{name}/{label} 对象。 */
601
+ function sc(it) {
602
+ if (it == null)
603
+ return '';
604
+ if (typeof it === 'string')
605
+ return it;
606
+ return String(it.item ?? it.name ?? it.label ?? it.text ?? it.value ?? '');
607
+ }
608
+ function t9Head(slide, p, pal, o = {}) {
609
+ masthead(slide, p, pal);
610
+ const hasK = !!p.kicker;
611
+ if (hasK)
612
+ kicker(slide, p.kicker, pal, { x: MX, y: 1.46 });
613
+ const ty = hasK ? 1.82 : 1.66;
614
+ const size = o.size ?? 27;
615
+ title(slide, p.title, pal, { x: MX, y: ty, w: o.titleW ?? CW, size });
616
+ let cy = ty + (size / 72) * 1.2 + 0.12;
617
+ if (o.sub !== false && p.subtitle) {
618
+ txt(slide, p.subtitle, { x: MX, y: cy, w: o.titleW ?? CW, h: 0.5, fontSize: 12, color: pal.ink2, align: 'left', fontFace: S.fonts.body, valign: 'top', lineSpacingMultiple: 1.15 });
619
+ cy += 0.58;
620
+ }
621
+ return Math.max(cy, 2.92);
622
+ }
623
+ function t9Pill(slide, text, x, y, w, h, bg, fg) {
624
+ slide.addShape('roundRect', { x, y, w, h, rectRadius: h / 2, fill: { color: bg }, line: { color: bg, width: 0 } });
625
+ txt(slide, text, { x, y, w, h, fontSize: 9.5, color: fg, align: 'center', valign: 'middle', bold: true, fontFace: S.fonts.mono });
626
+ }
627
+ /** 等级文本(高/中/低)映射为 0-100 坐标;已是数字则直接用。 */
628
+ function lvl(v) {
629
+ const s = String(v ?? '').trim();
630
+ if (s === '高' || s === 'high')
631
+ return 82;
632
+ if (s === '中' || s === 'mid')
633
+ return 50;
634
+ if (s === '低' || s === 'low')
635
+ return 18;
636
+ const n = t9N(v);
637
+ return Number.isFinite(n) && (n > 0 && n <= 100) ? n : 50;
638
+ }
639
+ // 1 cross-perspective(ink)
640
+ function renderCrossPerspective(slide, p) {
641
+ const pal = P('theme09_cross-perspective_v1');
642
+ frame(slide, pal, { bottom: false });
643
+ const y0 = t9Head(slide, p, pal, { size: 26 });
644
+ const dims = (p.dimensions || []).slice(0, 4);
645
+ const leftW = 6.0;
646
+ const conclX = MX + leftW + 0.3;
647
+ const conclW = CW - leftW - 0.3;
648
+ const rh = Math.min(0.62, (BOT - y0 - 0.1) / Math.max(dims.length, 1));
649
+ dims.forEach((d, i) => {
650
+ const ry = y0 + i * rh;
651
+ const parts = String(d.metrics || '').split('|').map((s) => {
652
+ const idx = s.indexOf(':');
653
+ const name = idx >= 0 ? s.slice(0, idx).trim() : s.trim();
654
+ const val = idx >= 0 ? t9N(s.slice(idx + 1)) : 0;
655
+ return { name, val };
656
+ });
657
+ const maxV = Math.max(1, ...parts.map((x) => x.val));
658
+ txt(slide, sc(d.label), { x: MX, y: ry, w: 1.5, h: rh - 0.06, fontSize: 11, color: pal.ink, bold: true, valign: 'middle', fontFace: S.fonts.body });
659
+ const bx = MX + 1.6;
660
+ const bw = leftW - 1.6;
661
+ let acc = 0;
662
+ parts.forEach((pt, si) => {
663
+ const w = (pt.val / maxV) * bw;
664
+ slide.addShape('rect', { x: bx + acc, y: ry + 0.06, w: Math.max(0.02, w - 0.03), h: rh - 0.14, fill: { color: series(pal)[si % 6] }, line: { width: 0 } });
665
+ acc += w;
666
+ });
667
+ txt(slide, sc(d.note), { x: bx, y: ry + rh - 0.2, w: bw, h: 0.18, fontSize: 8.5, color: pal.ink3, italic: true, fontFace: S.fonts.body });
668
+ });
669
+ card(slide, conclX, y0, conclW, BOT - y0 - 0.1, pal);
670
+ txt(slide, '结论', { x: conclX + 0.18, y: y0 + 0.16, w: conclW - 0.36, h: 0.3, fontSize: 12, color: pal.accent, bold: true, fontFace: S.fonts.mono });
671
+ txt(slide, p.conclusion, { x: conclX + 0.18, y: y0 + 0.55, w: conclW - 0.36, h: BOT - y0 - 0.8, fontSize: 12, color: pal.ink, valign: 'top', fontFace: S.fonts.body, lineSpacingMultiple: 1.25 });
672
+ footer(slide, p.footnoteLeft, p.footnoteRight, pal);
673
+ }
674
+ // 2 thesis(paper)
675
+ function renderThesis(slide, p) {
676
+ const pal = P('theme09_thesis_v1');
677
+ frame(slide, pal, { bottom: false });
678
+ const y0 = t9Head(slide, p, pal, { size: 26 });
679
+ const steps = (p.steps || []).slice(0, 3);
680
+ const n = Math.max(steps.length, 1);
681
+ const gap = 0.25;
682
+ const cw = (CW - gap * (n - 1)) / n;
683
+ steps.forEach((s, i) => {
684
+ const cx = MX + i * (cw + gap);
685
+ card(slide, cx, y0, cw, 1.7, pal);
686
+ t9Pill(slide, String(i + 1), cx + 0.18, y0 + 0.18, 0.42, 0.42, pal.accent, 'FFFFFF');
687
+ txt(slide, sc(s.claim), { x: cx + 0.72, y: y0 + 0.18, w: cw - 0.9, h: 0.5, fontSize: 13, color: pal.ink, bold: true, valign: 'top', fontFace: S.fonts.heading });
688
+ txt(slide, sc(s.evidence), { x: cx + 0.18, y: y0 + 0.72, w: cw - 0.36, h: 0.9, fontSize: 10.5, color: pal.ink2, valign: 'top', fontFace: S.fonts.body, lineSpacingMultiple: 1.15 });
689
+ });
690
+ const by = y0 + 1.95;
691
+ if (p.conclusion) {
692
+ slide.addShape('rect', { x: MX, y: by, w: 0.08, h: BOT - by - 0.2, fill: { color: pal.accent } });
693
+ txt(slide, p.conclusion, { x: MX + 0.22, y: by, w: CW - 0.22, h: BOT - by - 0.2, fontSize: 12.5, color: pal.ink, valign: 'top', bold: true, fontFace: S.fonts.body, lineSpacingMultiple: 1.25 });
694
+ }
695
+ folio(slide, p, pal);
696
+ }
697
+ // 3 value-chain(paper)
698
+ function renderValueChain(slide, p) {
699
+ const pal = P('theme09_value-chain_v1');
700
+ frame(slide, pal, { bottom: false });
701
+ const y0 = t9Head(slide, p, pal, { size: 26 });
702
+ const cols = [
703
+ { label: p.upstreamLabel, items: p.upstream },
704
+ { label: p.midstreamLabel, items: p.midstream },
705
+ { label: p.downstreamLabel, items: p.downstream },
706
+ ];
707
+ const gap = 0.3;
708
+ const cw = (CW - gap * 2) / 3;
709
+ const ch = BOT - y0 - 0.1;
710
+ cols.forEach((c, i) => {
711
+ const cx = MX + i * (cw + gap);
712
+ card(slide, cx, y0, cw, ch, pal);
713
+ slide.addShape('rect', { x: cx, y: y0, w: cw, h: 0.42, fill: { color: series(pal)[i] } });
714
+ txt(slide, String(c.label || ''), { x: cx + 0.15, y: y0, w: cw - 0.3, h: 0.42, fontSize: 12, color: 'FFFFFF', bold: true, valign: 'middle', fontFace: S.fonts.heading });
715
+ const items = (c.items || []).slice(0, 4);
716
+ let iy = y0 + 0.55;
717
+ items.forEach((it) => {
718
+ txt(slide, sc(it.name), { x: cx + 0.15, y: iy, w: cw - 0.3, h: 0.26, fontSize: 11.5, color: pal.ink, bold: true, fontFace: S.fonts.body });
719
+ txt(slide, sc(it.desc), { x: cx + 0.15, y: iy + 0.26, w: cw - 0.3, h: 0.4, fontSize: 9.5, color: pal.ink2, valign: 'top', fontFace: S.fonts.body, lineSpacingMultiple: 1.1 });
720
+ iy += 0.74;
721
+ });
722
+ });
723
+ folio(slide, p, pal);
724
+ }
725
+ // 4 risk(ink)
726
+ function renderRisk(slide, p) {
727
+ const pal = P('theme09_risk_v1');
728
+ frame(slide, pal, { bottom: false });
729
+ const y0 = t9Head(slide, p, pal, { size: 26 });
730
+ const gx = MX + 1.3;
731
+ const gy = y0 + 0.2;
732
+ const gw = 6.4;
733
+ const gh = BOT - y0 - 0.9;
734
+ card(slide, gx, gy, gw, gh, pal);
735
+ // quadrant lines at 50%
736
+ slide.addShape('line', { x: gx + gw / 2, y: gy, w: 0, h: gh, line: { color: pal.borderStrong, width: 0.75, dashType: 'dash' } });
737
+ slide.addShape('line', { x: gx, y: gy + gh / 2, w: gw, h: 0, line: { color: pal.borderStrong, width: 0.75, dashType: 'dash' } });
738
+ txt(slide, p.axisProbability, { x: gx, y: gy + gh + 0.02, w: gw, h: 0.2, fontSize: 9, color: pal.ink3, align: 'center', fontFace: S.fonts.mono });
739
+ txt(slide, p.axisImpact, { x: gx - 1.25, y: gy, w: 1.1, h: gh, fontSize: 9, color: pal.ink3, align: 'center', valign: 'middle', rotate: 270, fontFace: S.fonts.mono });
740
+ (p.risks || []).slice(0, 8).forEach((r) => {
741
+ const x = gx + (lvl(r.probability) / 100) * gw;
742
+ const y = gy + (1 - lvl(r.impact) / 100) * gh;
743
+ const col = r.level === '高' ? pal.accent : r.level === '中' ? pal.accent2 : series(pal)[3];
744
+ slide.addShape('ellipse', { x: x - 0.11, y: y - 0.11, w: 0.22, h: 0.22, fill: { color: col }, line: { color: pal.bg, width: 1 } });
745
+ txt(slide, sc(r.name), { x: x - 0.9, y: y - 0.32, w: 1.8, h: 0.2, fontSize: 8.5, color: pal.ink, align: 'center', fontFace: S.fonts.body });
746
+ });
747
+ folio(slide, p, pal);
748
+ }
749
+ // 5 outlook(ink)
750
+ function renderOutlook(slide, p) {
751
+ const pal = P('theme09_outlook_v1');
752
+ frame(slide, pal, { bottom: false });
753
+ const y0 = t9Head(slide, p, pal, { size: 26 });
754
+ const items = (p.outlooks || []).slice(0, 3);
755
+ const n = Math.max(items.length, 1);
756
+ const gap = 0.3;
757
+ const cw = (CW - gap * (n - 1)) / n;
758
+ items.forEach((o, i) => {
759
+ const cx = MX + i * (cw + gap);
760
+ card(slide, cx, y0, cw, 2.5, pal);
761
+ txt(slide, sc(o.title), { x: cx + 0.18, y: y0 + 0.18, w: cw - 0.36, h: 0.5, fontSize: 13, color: pal.ink, bold: true, valign: 'top', fontFace: S.fonts.heading });
762
+ txt(slide, sc(o.description), { x: cx + 0.18, y: y0 + 0.75, w: cw - 0.36, h: 1.4, fontSize: 10.5, color: pal.ink2, valign: 'top', fontFace: S.fonts.body, lineSpacingMultiple: 1.15 });
763
+ const trend = String(o.trend || '');
764
+ const tc = trend.includes('上行') ? series(pal)[3] : trend.includes('下行') ? pal.accent : pal.accent2;
765
+ t9Pill(slide, trend, cx + 0.18, y0 + 2.1, cw - 0.36, 0.32, tc, 'FFFFFF');
766
+ });
767
+ folio(slide, p, pal);
768
+ }
769
+ // 6 conclusion(paper)
770
+ function renderConclusion(slide, p) {
771
+ const pal = P('theme09_conclusion_v1');
772
+ frame(slide, pal, { bottom: false });
773
+ const y0 = t9Head(slide, p, pal, { size: 26 });
774
+ if (p.showBand)
775
+ colorBar(slide, MX, y0 - 0.02, CW, pal);
776
+ const items = (p.conclusions || []).slice(0, 4);
777
+ const n = Math.max(items.length, 1);
778
+ const gap = 0.22;
779
+ const cw = (CW - gap * (n - 1)) / n;
780
+ items.forEach((c, i) => {
781
+ const cx = MX + i * (cw + gap);
782
+ txt(slide, String(i + 1).padStart(2, '0'), { x: cx, y: y0 + 0.12, w: cw, h: 0.4, fontSize: 22, color: pal.accent, bold: true, align: 'left', fontFace: S.fonts.mono });
783
+ txt(slide, sc(c.title), { x: cx, y: y0 + 0.6, w: cw - 0.1, h: 0.5, fontSize: 12.5, color: pal.ink, bold: true, valign: 'top', fontFace: S.fonts.heading });
784
+ txt(slide, sc(c.summary), { x: cx, y: y0 + 1.1, w: cw - 0.1, h: 1.6, fontSize: 10, color: pal.ink2, valign: 'top', fontFace: S.fonts.body, lineSpacingMultiple: 1.15 });
785
+ });
786
+ folio(slide, p, pal);
787
+ }
788
+ // 7 bracket(paper)
789
+ function renderBracket(slide, p) {
790
+ const pal = P('theme09_bracket_v1');
791
+ frame(slide, pal, { bottom: false });
792
+ const y0 = t9Head(slide, p, pal, { size: 26 });
793
+ const items = (p.items || []).slice(0, 6);
794
+ const n = Math.max(items.length, 1);
795
+ const iw = Math.min(1.7, (CW - 0.6) / n);
796
+ const gap = (CW - iw * n) / (n + 1);
797
+ items.forEach((it, i) => {
798
+ const ix = MX + gap + i * (iw + gap);
799
+ card(slide, ix, y0, iw, 0.7, pal);
800
+ txt(slide, sc(it.item), { x: ix + 0.05, y: y0, w: iw - 0.1, h: 0.7, fontSize: 10.5, color: pal.ink, bold: true, align: 'center', valign: 'middle', fontFace: S.fonts.body });
801
+ if (i < n - 1)
802
+ slide.addShape('line', { x: ix + iw, y: y0 + 0.35, w: gap, h: 0, line: { color: pal.borderStrong, width: 1, endArrowType: 'triangle' } });
803
+ });
804
+ const sy = y0 + 1.0;
805
+ slide.addShape('line', { x: MX + gap, y: sy, w: 0, h: 0.5, line: { color: pal.accent, width: 2 } });
806
+ slide.addShape('line', { x: MX + gap, y: sy + 0.5, w: CW - 2 * gap, h: 0, line: { color: pal.accent, width: 2 } });
807
+ slide.addShape('line', { x: MX + CW - gap, y: sy, w: 0, h: 0.5, line: { color: pal.accent, width: 2 } });
808
+ txt(slide, p.summary, { x: MX, y: sy + 0.7, w: CW, h: BOT - sy - 0.9, fontSize: 14, color: pal.ink, bold: true, valign: 'top', fontFace: S.fonts.heading, lineSpacingMultiple: 1.3 });
809
+ folio(slide, p, pal);
810
+ }
811
+ // 8 flow(ink)
812
+ function renderFlow(slide, p) {
813
+ const pal = P('theme09_flow_v1');
814
+ frame(slide, pal, { bottom: false });
815
+ const y0 = t9Head(slide, p, pal);
816
+ const nodes = (p.nodes || []).slice(0, 6);
817
+ const links = (p.links || []).slice(0, 8);
818
+ const colW = (CW - 1.0) / Math.max(nodes.length, 1);
819
+ const nodeBoxes = [];
820
+ nodes.forEach((nd, i) => {
821
+ const bx = MX + 0.2 + i * colW;
822
+ const by = y0 + 0.3 + (i % 2) * 0.9;
823
+ const bw = colW - 0.3;
824
+ nodeBoxes.push({ x: bx, y: by, w: bw, h: 0.5, name: sc(nd.name) });
825
+ card(slide, bx, by, bw, 0.5, pal, pal.surfaceElevated);
826
+ txt(slide, sc(nd.name), { x: bx + 0.05, y: by, w: bw - 0.1, h: 0.5, fontSize: 10, color: pal.ink, align: 'center', valign: 'middle', bold: true, fontFace: S.fonts.body });
827
+ });
828
+ links.forEach((lk, i) => {
829
+ const a = nodeBoxes[Math.min(t9N(lk.source), nodeBoxes.length - 1)];
830
+ const b = nodeBoxes[Math.min(t9N(lk.target), nodeBoxes.length - 1)];
831
+ if (!a || !b)
832
+ return;
833
+ const v = t9N(lk.value);
834
+ const w = Math.max(0.5, Math.min(4, v / 30));
835
+ slide.addShape('line', { x: a.x + a.w, y: a.y + a.h / 2, w: b.x - (a.x + a.w), h: b.y + b.h / 2 - (a.y + a.h / 2), line: { color: series(pal)[i % 6], width: w, endArrowType: 'triangle' } });
836
+ });
837
+ footer(slide, p.footnoteLeft, p.footnoteRight, pal);
838
+ }
839
+ // 9 orbit(ink)
840
+ function renderOrbit(slide, p) {
841
+ const pal = P('theme09_orbit_v1');
842
+ frame(slide, pal, { bottom: false });
843
+ const y0 = t9Head(slide, p, pal, { size: 26 });
844
+ const cx = MX + CW / 2;
845
+ const cy = (y0 + BOT) / 2 + 0.1;
846
+ const R = Math.min((BOT - y0) / 2 - 0.3, CW / 2 - 1.0);
847
+ slide.addShape('ellipse', { x: cx - R, y: cy - R, w: R * 2, h: R * 2, fill: { color: pal.surface }, line: { color: pal.borderStrong, width: 1.25 } });
848
+ slide.addShape('ellipse', { x: cx - 0.9, y: cy - 0.9, w: 1.8, h: 1.8, fill: { color: pal.accent }, line: { width: 0 } });
849
+ txt(slide, p.centerText, { x: cx - 0.9, y: cy - 0.9, w: 1.8, h: 1.8, fontSize: 12, color: 'FFFFFF', align: 'center', valign: 'middle', bold: true, fontFace: S.fonts.heading });
850
+ const evs = (p.events || []).slice(0, 8);
851
+ const n = Math.max(evs.length, 1);
852
+ evs.forEach((e, i) => {
853
+ const ang = (Math.PI * 2 * i) / n - Math.PI / 2;
854
+ const ex = cx + Math.cos(ang) * R;
855
+ const ey = cy + Math.sin(ang) * R;
856
+ slide.addShape('ellipse', { x: ex - 0.09, y: ey - 0.09, w: 0.18, h: 0.18, fill: { color: series(pal)[i % 6] }, line: { color: pal.bg, width: 1 } });
857
+ const lx = cx + Math.cos(ang) * (R + 0.35);
858
+ const ly = cy + Math.sin(ang) * (R + 0.35);
859
+ txt(slide, sc(e.year), { x: lx - 0.5, y: ly - 0.34, w: 1.0, h: 0.18, fontSize: 9, color: pal.accent, align: 'center', bold: true, fontFace: S.fonts.mono });
860
+ txt(slide, sc(e.label), { x: lx - 0.6, y: ly - 0.16, w: 1.2, h: 0.5, fontSize: 9, color: pal.ink, align: 'center', valign: 'top', fontFace: S.fonts.body });
861
+ });
862
+ folio(slide, p, pal);
863
+ }
864
+ // 10 vertical(paper)
865
+ function renderVertical(slide, p) {
866
+ const pal = P('theme09_vertical_v1');
867
+ frame(slide, pal, { bottom: false });
868
+ const y0 = t9Head(slide, p, pal, { size: 26 });
869
+ const cards = (p.cards || []).slice(0, 3);
870
+ const n = Math.max(cards.length, 1);
871
+ const gap = 0.3;
872
+ const cw = (CW - gap * (n - 1)) / n;
873
+ cards.forEach((c, i) => {
874
+ const cx = MX + i * (cw + gap);
875
+ card(slide, cx, y0, cw, 2.5, pal);
876
+ slide.addShape('rect', { x: cx, y: y0, w: cw, h: 1.0, fill: { color: pal.surfaceElevated }, line: { color: pal.border, width: 1 } });
877
+ txt(slide, '影像', { x: cx, y: y0, w: cw, h: 1.0, fontSize: 10, color: pal.ink3, align: 'center', valign: 'middle', italic: true, fontFace: S.fonts.body });
878
+ txt(slide, sc(c.title), { x: cx + 0.18, y: y0 + 1.12, w: cw - 0.36, h: 0.4, fontSize: 13, color: pal.ink, bold: true, fontFace: S.fonts.heading });
879
+ txt(slide, sc(c.description), { x: cx + 0.18, y: y0 + 1.55, w: cw - 0.36, h: 0.85, fontSize: 10.5, color: pal.ink2, valign: 'top', fontFace: S.fonts.body, lineSpacingMultiple: 1.15 });
880
+ });
881
+ folio(slide, p, pal);
882
+ }
883
+ // 11 calendar(paper)
884
+ function renderCalendar(slide, p) {
885
+ const pal = P('theme09_calendar_v1');
886
+ frame(slide, pal, { bottom: false });
887
+ const y0 = t9Head(slide, p, pal, { size: 26 });
888
+ const year = t9N(p.year) || 2026;
889
+ const month = Math.max(1, Math.min(12, t9N(p.month) || 8));
890
+ const days = new Date(year, month, 0).getDate();
891
+ const cols = 7;
892
+ const rows = Math.ceil(days / cols);
893
+ const gridX = MX + 1.4;
894
+ const gridW = CW - 1.4;
895
+ const cell = gridW / cols;
896
+ const gridH = (BOT - y0 - 0.2);
897
+ const cellH = Math.min(0.55, gridH / rows);
898
+ const evByDate = {};
899
+ (p.events || []).forEach((e) => { (evByDate[String(sc(e.date))] = evByDate[String(sc(e.date))] || []).push(e); });
900
+ for (let d = 1; d <= days; d++) {
901
+ const r = Math.floor((d - 1) / cols);
902
+ const c = (d - 1) % cols;
903
+ const x = gridX + c * cell;
904
+ const y = y0 + r * cellH;
905
+ const has = evByDate[String(d)];
906
+ slide.addShape('rect', { x, y, w: cell - 0.03, h: cellH - 0.03, fill: { color: has ? pal.accent : pal.surfaceElevated }, line: { color: pal.border, width: 0.75 } });
907
+ txt(slide, String(d), { x: x + 0.04, y: y + 0.02, w: cell - 0.1, h: 0.18, fontSize: 9, color: has ? 'FFFFFF' : pal.ink3, bold: true, fontFace: S.fonts.mono });
908
+ if (has)
909
+ txt(slide, String(sc(has[0].name)).slice(0, 6), { x: x + 0.04, y: y + 0.2, w: cell - 0.08, h: cellH - 0.24, fontSize: 7, color: 'FFFFFF', valign: 'top', fontFace: S.fonts.body });
910
+ }
911
+ txt(slide, year + '.' + String(month).padStart(2, '0'), { x: MX, y: y0, w: 1.3, h: 0.4, fontSize: 16, color: pal.ink, bold: true, fontFace: S.fonts.mono });
912
+ folio(slide, p, pal);
913
+ }
914
+ // 12 phases(ink)
915
+ function renderPhases(slide, p) {
916
+ const pal = P('theme09_phases_v1');
917
+ frame(slide, pal, { bottom: false });
918
+ const y0 = t9Head(slide, p, pal, { size: 26 });
919
+ const phases = (p.phases || []).slice(0, 4);
920
+ const n = Math.max(phases.length, 1);
921
+ const gap = 0.25;
922
+ const cw = (CW - gap * (n - 1)) / n;
923
+ phases.forEach((ph, i) => {
924
+ const cx = MX + i * (cw + gap);
925
+ card(slide, cx, y0, cw, 2.6, pal);
926
+ t9Pill(slide, String(i + 1), cx + 0.18, y0 + 0.18, 0.4, 0.4, series(pal)[i % 6], 'FFFFFF');
927
+ txt(slide, sc(ph.name), { x: cx + 0.66, y: y0 + 0.18, w: cw - 0.8, h: 0.4, fontSize: 12, color: pal.ink, bold: true, valign: 'middle', fontFace: S.fonts.heading });
928
+ txt(slide, [sc(ph.start), sc(ph.end)].filter(Boolean).join(' – '), { x: cx + 0.18, y: y0 + 0.66, w: cw - 0.36, h: 0.2, fontSize: 9, color: pal.accent, fontFace: S.fonts.mono });
929
+ const ms = (ph.milestones || []).slice(0, 3);
930
+ let my = y0 + 0.95;
931
+ ms.forEach((m) => {
932
+ txt(slide, '• ' + sc(m), { x: cx + 0.18, y: my, w: cw - 0.36, h: 0.5, fontSize: 9.5, color: pal.ink2, valign: 'top', fontFace: S.fonts.body, lineSpacingMultiple: 1.1 });
933
+ my += 0.52;
934
+ });
935
+ });
936
+ folio(slide, p, pal);
937
+ }
938
+ // 13 gauge(ink)
939
+ function renderGauge(slide, p) {
940
+ const pal = P('theme09_gauge_v1');
941
+ frame(slide, pal, { bottom: false });
942
+ const y0 = t9Head(slide, p, pal);
943
+ const min = t9N(p.min) || 0;
944
+ const max = t9N(p.max) || 100;
945
+ const val = t9N(p.value);
946
+ const span = Math.max(1, max - min);
947
+ const gx = MX + 0.3;
948
+ const gw = 6.0;
949
+ const gy = y0 + 0.3;
950
+ const gh = 0.5;
951
+ (p.ranges || []).forEach((rg) => {
952
+ const a = (t9N(rg.min) - min) / span;
953
+ const b = (t9N(rg.max) - min) / span;
954
+ const col = rg.color === '绿' ? series(pal)[3] : rg.color === '黄' ? series(pal)[2] : pal.accent;
955
+ slide.addShape('rect', { x: gx + a * gw, y: gy, w: Math.max(0.02, (b - a) * gw), h: gh, fill: { color: col }, line: { width: 0 } });
956
+ });
957
+ const xp = gx + ((val - min) / span) * gw;
958
+ slide.addShape('line', { x: xp, y: gy - 0.15, w: 0, h: gh + 0.3, line: { color: pal.ink, width: 2.5 } });
959
+ txt(slide, p.readingLabel, { x: gx, y: gy + gh + 0.05, w: gw, h: 0.2, fontSize: 9, color: pal.ink3, align: 'left', fontFace: S.fonts.mono });
960
+ txt(slide, val.toFixed(1) + (p.unit ? ' ' + p.unit : ''), { x: gx + gw + 0.3, y: y0 + 0.1, w: CW - gw - 0.6, h: 0.9, fontSize: 40, color: pal.accent, bold: true, align: 'left', valign: 'middle', fontFace: S.fonts.heading });
961
+ txt(slide, p.subtitle, { x: gx + gw + 0.3, y: y0 + 1.0, w: CW - gw - 0.6, h: 1.2, fontSize: 11, color: pal.ink2, valign: 'top', fontFace: S.fonts.body, lineSpacingMultiple: 1.2 });
962
+ footer(slide, p.footnoteLeft, p.footnoteRight, pal);
963
+ }
964
+ // 14 scoreboard(ink)
965
+ function renderScoreboard(slide, p) {
966
+ const pal = P('theme09_scoreboard_v1');
967
+ frame(slide, pal, { bottom: false });
968
+ const y0 = t9Head(slide, p, pal);
969
+ const headers = (p.headers || []).slice(0, 5);
970
+ const rows = (p.rows || []).slice(0, 6);
971
+ const hn = Math.max(headers.length, 1);
972
+ const nameW = 2.2;
973
+ const tblW = CW - nameW;
974
+ const colW = tblW / hn;
975
+ const rh = 0.42;
976
+ // header
977
+ slide.addShape('rect', { x: MX, y: y0, w: nameW, h: rh, fill: { color: pal.accent } });
978
+ txt(slide, '参赛者', { x: MX + 0.1, y: y0, w: nameW - 0.2, h: rh, fontSize: 10, color: 'FFFFFF', bold: true, valign: 'middle', fontFace: S.fonts.body });
979
+ headers.forEach((h, i) => {
980
+ const hx = MX + nameW + i * colW;
981
+ slide.addShape('rect', { x: hx, y: y0, w: colW, h: rh, fill: { color: pal.surfaceElevated } });
982
+ txt(slide, sc(h.label), { x: hx + 0.04, y: y0, w: colW - 0.08, h: rh, fontSize: 9, color: pal.ink, bold: true, align: 'center', valign: 'middle', fontFace: S.fonts.body });
983
+ });
984
+ rows.forEach((r, ri) => {
985
+ const ry = y0 + (ri + 1) * rh;
986
+ const hl = String(r.name) === String(p.highlightRow);
987
+ slide.addShape('rect', { x: MX, y: ry, w: nameW, h: rh, fill: { color: hl ? pal.accent : pal.surface } });
988
+ txt(slide, sc(r.name), { x: MX + 0.1, y: ry, w: nameW - 0.2, h: rh, fontSize: 10, color: hl ? 'FFFFFF' : pal.ink, bold: true, valign: 'middle', fontFace: S.fonts.body });
989
+ (r.scores || []).slice(0, hn).forEach((s, i) => {
990
+ const hx = MX + nameW + i * colW;
991
+ txt(slide, sc(s), { x: hx + 0.04, y: ry, w: colW - 0.08, h: rh, fontSize: 10, color: pal.ink2, align: 'center', valign: 'middle', fontFace: S.fonts.mono });
992
+ });
993
+ });
994
+ footer(slide, p.footnoteLeft, p.footnoteRight, pal);
995
+ }
996
+ // 15 trend(paper)
997
+ function renderTrend(slide, p) {
998
+ const pal = P('theme09_trend_v1');
999
+ frame(slide, pal, { bottom: false });
1000
+ const y0 = t9Head(slide, p, pal);
1001
+ const qs = (p.quarters || []).map(sc);
1002
+ const main = (p.mainSeries || []).map(t9N);
1003
+ const yoy = (p.yoySeries || []).map(t9N);
1004
+ const n = Math.max(qs.length, 1);
1005
+ const plotX = MX + 0.3;
1006
+ const plotW = CW - 1.0;
1007
+ const plotY = y0 + 0.2;
1008
+ const plotH = 2.7;
1009
+ const maxM = Math.max(1, ...main);
1010
+ const baseY = plotY + plotH;
1011
+ const bw = (plotW / n) * 0.5;
1012
+ qs.forEach((q, i) => {
1013
+ const cx = plotX + (i + 0.5) * (plotW / n);
1014
+ const h = (main[i] / maxM) * (plotH - 0.3);
1015
+ slide.addShape('rect', { x: cx - bw / 2, y: baseY - h, w: bw, h, fill: { color: pal.accent }, line: { width: 0 } });
1016
+ txt(slide, q, { x: cx - plotW / n / 2, y: baseY + 0.02, w: plotW / n, h: 0.2, fontSize: 9, color: pal.ink3, align: 'center', fontFace: S.fonts.mono });
1017
+ });
1018
+ // yoy line (right axis, scaled to -20..40)
1019
+ const ly = (v) => baseY - ((v + 20) / 60) * (plotH - 0.3);
1020
+ let prev = null;
1021
+ qs.forEach((_q, i) => {
1022
+ const cx = plotX + (i + 0.5) * (plotW / n);
1023
+ const py = ly(yoy[i]);
1024
+ if (prev)
1025
+ slide.addShape('line', { x: prev.x, y: prev.y, w: cx - prev.x, h: py - prev.y, line: { color: pal.accent2, width: 2 } });
1026
+ prev = { x: cx, y: py };
1027
+ });
1028
+ t9Pill(slide, p.mainName + ' · ' + (p.unit || ''), plotX, plotY - 0.05, 2.4, 0.26, pal.accent, 'FFFFFF');
1029
+ t9Pill(slide, p.yoyName, plotX + 2.6, plotY - 0.05, 2.2, 0.26, pal.accent2, 'FFFFFF');
1030
+ footer(slide, p.footnoteLeft, p.footnoteRight, pal);
1031
+ }
1032
+ // 16 histogram(paper)
1033
+ function renderHistogram(slide, p) {
1034
+ const pal = P('theme09_histogram_v1');
1035
+ frame(slide, pal, { bottom: false });
1036
+ const y0 = t9Head(slide, p, pal);
1037
+ const bins = (p.bins || []).slice(0, 12);
1038
+ const n = Math.max(bins.length, 1);
1039
+ const plotX = MX;
1040
+ const plotW = CW - 1.2;
1041
+ const plotY = y0 + 0.2;
1042
+ const plotH = 2.7;
1043
+ const maxC = Math.max(1, ...bins.map((b) => t9N(b.count)));
1044
+ const baseY = plotY + plotH;
1045
+ const bw = plotW / n;
1046
+ bins.forEach((b, i) => {
1047
+ const h = (t9N(b.count) / maxC) * (plotH - 0.3);
1048
+ slide.addShape('rect', { x: plotX + i * bw + 0.04, y: baseY - h, w: bw - 0.08, h, fill: { color: series(pal)[i % 6] }, line: { width: 0 } });
1049
+ txt(slide, String(sc(b.range)).slice(0, 6), { x: plotX + i * bw, y: baseY + 0.02, w: bw, h: 0.2, fontSize: 8, color: pal.ink3, align: 'center', fontFace: S.fonts.mono });
1050
+ });
1051
+ if (p.mean != null) {
1052
+ const mx = plotX + (t9N(p.mean) / Math.max(1, ...bins.map((b) => t9N(b.count)))) * plotW;
1053
+ slide.addShape('line', { x: mx, y: plotY, w: 0, h: plotH, line: { color: pal.accent, width: 2, dashType: 'dash' } });
1054
+ txt(slide, '均值 ' + p.mean, { x: Math.min(mx + 0.05, plotX + plotW - 1.5), y: plotY + 0.02, w: 1.5, h: 0.2, fontSize: 9, color: pal.accent, bold: true, fontFace: S.fonts.mono });
1055
+ }
1056
+ footer(slide, p.footnoteLeft, p.footnoteRight, pal);
1057
+ }
1058
+ // 17 forecast-fan(ink)
1059
+ function renderForecastFan(slide, p) {
1060
+ const pal = P('theme09_forecast-fan_v1');
1061
+ frame(slide, pal, { bottom: false });
1062
+ const y0 = t9Head(slide, p, pal);
1063
+ const seriesArr = (p.series || []).slice(0, 3);
1064
+ const cats = Math.max(1, ...seriesArr.map((s) => String(s.data || '').split('|').length));
1065
+ const plotX = MX + 0.3;
1066
+ const plotW = CW - 1.0;
1067
+ const plotY = y0 + 0.2;
1068
+ const plotH = 2.6;
1069
+ const baseY = plotY + plotH;
1070
+ const all = seriesArr.flatMap((s) => String(s.data || '').split('|').map(t9N));
1071
+ const maxV = Math.max(1, ...all);
1072
+ const xAt = (i) => plotX + ((i + 0.5) / cats) * plotW;
1073
+ const yAt = (v) => baseY - (v / maxV) * (plotH - 0.3);
1074
+ // draw outer two series as a filled band (fan) via thin rects
1075
+ if (seriesArr.length >= 2) {
1076
+ const lo = String(seriesArr[0].data || '').split('|').map(t9N);
1077
+ const hi = String(seriesArr[seriesArr.length - 1].data || '').split('|').map(t9N);
1078
+ lo.forEach((v, i) => {
1079
+ const yLo = yAt(Math.min(v, hi[i]));
1080
+ const yHi = yAt(Math.max(v, hi[i]));
1081
+ slide.addShape('rect', { x: xAt(i) - 0.12, y: yHi, w: 0.24, h: Math.max(0.02, yLo - yHi), fill: { color: pal.accent, transparency: 78 }, line: { width: 0 } });
1082
+ });
1083
+ }
1084
+ // each series as line
1085
+ seriesArr.forEach((s, si) => {
1086
+ const vals = String(s.data || '').split('|').map(t9N);
1087
+ let prev = null;
1088
+ vals.forEach((v, i) => {
1089
+ const px = xAt(i), py = yAt(v);
1090
+ if (prev)
1091
+ slide.addShape('line', { x: prev.x, y: prev.y, w: px - prev.x, h: py - prev.y, line: { color: series(pal)[si % 6], width: 2 } });
1092
+ prev = { x: px, y: py };
1093
+ });
1094
+ });
1095
+ (p.annotations || []).slice(0, 3).forEach((a, i) => {
1096
+ const ax = plotX + (t9N(a.period) / cats) * plotW;
1097
+ txt(slide, sc(a.text), { x: Math.min(ax, plotX + plotW - 2), y: plotY + 0.05 + i * 0.22, w: 2.0, h: 0.2, fontSize: 8.5, color: pal.ink2, italic: true, fontFace: S.fonts.body });
1098
+ });
1099
+ footer(slide, p.footnoteLeft, p.footnoteRight, pal);
1100
+ }
1101
+ // 18 plans(paper)
1102
+ function renderPlans(slide, p) {
1103
+ const pal = P('theme09_plans_v1');
1104
+ frame(slide, pal, { bottom: false });
1105
+ const y0 = t9Head(slide, p, pal, { size: 26 });
1106
+ const plans = (p.plans || []).slice(0, 3);
1107
+ const n = Math.max(plans.length, 1);
1108
+ const gap = 0.3;
1109
+ const cw = (CW - gap * (n - 1)) / n;
1110
+ plans.forEach((pl, i) => {
1111
+ const cx = MX + i * (cw + gap);
1112
+ const rec = !!pl.recommended;
1113
+ card(slide, cx, y0, cw, 2.7, pal, rec ? pal.accent : pal.surfaceElevated, rec ? pal.accent : pal.border);
1114
+ txt(slide, sc(pl.name), { x: cx + 0.18, y: y0 + 0.18, w: cw - 0.36, h: 0.4, fontSize: 13, color: rec ? pal.accent : pal.ink, bold: true, fontFace: S.fonts.heading });
1115
+ txt(slide, sc(pl.summary), { x: cx + 0.18, y: y0 + 0.62, w: cw - 0.36, h: 0.5, fontSize: 10, color: pal.ink2, valign: 'top', fontFace: S.fonts.body, lineSpacingMultiple: 1.1 });
1116
+ (pl.points || []).slice(0, 3).forEach((pt, k) => {
1117
+ txt(slide, '– ' + sc(pt), { x: cx + 0.18, y: y0 + 1.15 + k * 0.42, w: cw - 0.36, h: 0.4, fontSize: 9.5, color: pal.ink, valign: 'top', fontFace: S.fonts.body });
1118
+ });
1119
+ if (rec)
1120
+ t9Pill(slide, '推荐', cx + cw - 1.0, y0 + 0.2, 0.82, 0.3, pal.accent, 'FFFFFF');
1121
+ });
1122
+ if (p.conclusion)
1123
+ txt(slide, p.conclusion, { x: MX, y: y0 + 2.85, w: CW, h: 0.5, fontSize: 11, color: pal.ink, bold: true, fontFace: S.fonts.body });
1124
+ folio(slide, p, pal);
1125
+ }
1126
+ // 19 stair(ink)
1127
+ function renderStair(slide, p) {
1128
+ const pal = P('theme09_stair_v1');
1129
+ frame(slide, pal, { bottom: false });
1130
+ const y0 = t9Head(slide, p, pal, { size: 26 });
1131
+ const steps = (p.steps || []).slice(0, 5);
1132
+ const n = Math.max(steps.length, 1);
1133
+ const stepW = (CW - 0.6) / n;
1134
+ const baseY = BOT - 0.3;
1135
+ const stepH = (baseY - y0 - 0.3) / n;
1136
+ steps.forEach((s, i) => {
1137
+ const sx = MX + i * stepW + 0.1;
1138
+ const sy = baseY - (i + 1) * stepH;
1139
+ card(slide, sx, sy, stepW - 0.2, (i + 1) * stepH, pal, pal.surfaceElevated);
1140
+ t9Pill(slide, String(i + 1), sx + 0.12, sy + 0.12, 0.36, 0.36, series(pal)[i % 6], 'FFFFFF');
1141
+ txt(slide, sc(s.name), { x: sx + 0.55, y: sy + 0.12, w: stepW - 0.7, h: 0.4, fontSize: 12, color: pal.ink, bold: true, valign: 'middle', fontFace: S.fonts.heading });
1142
+ txt(slide, sc(s.description), { x: sx + 0.14, y: sy + 0.55, w: stepW - 0.34, h: (i + 1) * stepH - 0.7, fontSize: 9.5, color: pal.ink2, valign: 'top', fontFace: S.fonts.body, lineSpacingMultiple: 1.1 });
1143
+ });
1144
+ folio(slide, p, pal);
1145
+ }
1146
+ // 20 stacked(paper)
1147
+ function renderStacked(slide, p) {
1148
+ const pal = P('theme09_stacked_v1');
1149
+ frame(slide, pal, { bottom: false });
1150
+ const y0 = t9Head(slide, p, pal, { size: 26 });
1151
+ const periods = (p.periods || []).map(sc);
1152
+ const seriesArr = (p.series || []).slice(0, 6);
1153
+ const n = Math.max(periods.length, 1);
1154
+ const plotX = MX;
1155
+ const plotW = CW;
1156
+ const plotY = y0 + 0.2;
1157
+ const plotH = 2.4;
1158
+ const baseY = plotY + plotH;
1159
+ const maxTot = Math.max(1, ...seriesArr.map((s) => String(s.data || '').split('|').reduce((a, x) => a + t9N(x), 0)));
1160
+ const bw = (plotW / n) * 0.6;
1161
+ periods.forEach((pr, i) => {
1162
+ const cx = plotX + (i + 0.5) * (plotW / n);
1163
+ let acc = 0;
1164
+ seriesArr.forEach((s, si) => {
1165
+ const v = t9N(String(s.data || '').split('|')[i]);
1166
+ const h = (v / maxTot) * (plotH - 0.2);
1167
+ slide.addShape('rect', { x: cx - bw / 2, y: baseY - acc - h, w: bw, h: Math.max(0.01, h), fill: { color: series(pal)[si % 6] }, line: { color: pal.bg, width: 0.5 } });
1168
+ acc += h;
1169
+ });
1170
+ txt(slide, pr, { x: cx - plotW / n / 2, y: baseY + 0.02, w: plotW / n, h: 0.2, fontSize: 9, color: pal.ink3, align: 'center', fontFace: S.fonts.mono });
1171
+ });
1172
+ seriesArr.forEach((s, si) => {
1173
+ const lx = MX + (si % 3) * 2.6;
1174
+ const ly = baseY + 0.32 + Math.floor(si / 3) * 0.28;
1175
+ slide.addShape('rect', { x: lx, y: ly, w: 0.18, h: 0.18, fill: { color: series(pal)[si % 6] } });
1176
+ txt(slide, sc(s.name), { x: lx + 0.24, y: ly - 0.02, w: 2.3, h: 0.22, fontSize: 9.5, color: pal.ink2, fontFace: S.fonts.body });
1177
+ });
1178
+ footer(slide, p.footnoteLeft, p.footnoteRight, pal);
1179
+ }
1180
+ // 21 era(ink)
1181
+ function renderEra(slide, p) {
1182
+ const pal = P('theme09_era_v1');
1183
+ frame(slide, pal, { bottom: false });
1184
+ const y0 = t9Head(slide, p, pal, { size: 26 });
1185
+ const eras = (p.eras || []).slice(0, 3);
1186
+ const n = Math.max(eras.length, 1);
1187
+ const gap = 0.3;
1188
+ const cw = (CW - gap * (n - 1)) / n;
1189
+ eras.forEach((e, i) => {
1190
+ const cx = MX + i * (cw + gap);
1191
+ card(slide, cx, y0, cw, BOT - y0 - 0.1, pal);
1192
+ slide.addShape('rect', { x: cx, y: y0, w: 0.1, h: BOT - y0 - 0.1, fill: { color: series(pal)[i % 6] } });
1193
+ txt(slide, sc(e.year), { x: cx + 0.22, y: y0 + 0.15, w: cw - 0.4, h: 0.3, fontSize: 16, color: pal.accent, bold: true, fontFace: S.fonts.mono });
1194
+ txt(slide, sc(e.label), { x: cx + 0.22, y: y0 + 0.5, w: cw - 0.4, h: 0.5, fontSize: 13, color: pal.ink, bold: true, fontFace: S.fonts.heading });
1195
+ (e.events || []).slice(0, 4).forEach((ev, k) => {
1196
+ txt(slide, '· ' + sc(ev), { x: cx + 0.22, y: y0 + 1.1 + k * 0.42, w: cw - 0.44, h: 0.4, fontSize: 10, color: pal.ink2, valign: 'top', fontFace: S.fonts.body, lineSpacingMultiple: 1.1 });
1197
+ });
1198
+ });
1199
+ folio(slide, p, pal);
1200
+ }
1201
+ // 22 roadmap(paper)
1202
+ function renderRoadmap(slide, p) {
1203
+ const pal = P('theme09_roadmap_v1');
1204
+ frame(slide, pal, { bottom: false });
1205
+ const y0 = t9Head(slide, p, pal, { size: 26 });
1206
+ const stages = (p.stages || []).slice(0, 4);
1207
+ const n = Math.max(stages.length, 1);
1208
+ const gap = 0.3;
1209
+ const cw = (CW - gap * (n - 1)) / n;
1210
+ const lineY = y0 + 0.5;
1211
+ slide.addShape('line', { x: MX, y: lineY, w: CW, h: 0, line: { color: pal.borderStrong, width: 1.5 } });
1212
+ stages.forEach((st, i) => {
1213
+ const cx = MX + i * (cw + gap);
1214
+ const col = st.status === '已完成' ? series(pal)[3] : st.status === '进行中' ? pal.accent : pal.ink3;
1215
+ slide.addShape('ellipse', { x: cx + cw / 2 - 0.14, y: lineY - 0.14, w: 0.28, h: 0.28, fill: { color: col }, line: { color: pal.bg, width: 1.5 } });
1216
+ card(slide, cx, y0 + 0.95, cw, 1.9, pal);
1217
+ txt(slide, sc(st.name), { x: cx + 0.15, y: y0 + 1.1, w: cw - 0.3, h: 0.4, fontSize: 12, color: pal.ink, bold: true, fontFace: S.fonts.heading });
1218
+ t9Pill(slide, sc(st.status), cx + 0.15, y0 + 1.5, cw - 0.3, 0.28, col, 'FFFFFF');
1219
+ txt(slide, sc(st.time), { x: cx + 0.15, y: y0 + 1.85, w: cw - 0.3, h: 0.2, fontSize: 9, color: pal.accent, fontFace: S.fonts.mono });
1220
+ txt(slide, sc(st.description), { x: cx + 0.15, y: y0 + 2.1, w: cw - 0.3, h: 0.7, fontSize: 9.5, color: pal.ink2, valign: 'top', fontFace: S.fonts.body, lineSpacingMultiple: 1.1 });
1221
+ });
1222
+ if (p.endCaption)
1223
+ txt(slide, p.endCaption, { x: MX, y: y0 + 0.2, w: CW, h: 0.25, fontSize: 10, color: pal.ink3, align: 'right', italic: true, fontFace: S.fonts.body });
1224
+ folio(slide, p, pal);
1225
+ }
1226
+ // 23 score(ink)
1227
+ function renderScore(slide, p) {
1228
+ const pal = P('theme09_score_v1');
1229
+ frame(slide, pal, { bottom: false });
1230
+ const y0 = t9Head(slide, p, pal);
1231
+ const crit = (p.criteria || []).slice(0, 6);
1232
+ const n = Math.max(crit.length, 1);
1233
+ const labelW = 2.2;
1234
+ const plotX = MX + labelW;
1235
+ const plotW = CW - labelW - 1.2;
1236
+ const rowH = Math.min(0.5, (BOT - y0 - 0.2) / n);
1237
+ crit.forEach((c, i) => {
1238
+ const ry = y0 + 0.1 + i * rowH;
1239
+ txt(slide, sc(c.name), { x: MX, y: ry, w: labelW - 0.1, h: rowH, fontSize: 10.5, color: pal.ink, bold: true, valign: 'middle', fontFace: S.fonts.body });
1240
+ const max = t9N(c.maxScore) || 10;
1241
+ const sc2 = t9N(c.score);
1242
+ const w = Math.max(0.02, (sc2 / max) * plotW);
1243
+ slide.addShape('rect', { x: plotX, y: ry + rowH / 2 - 0.1, w: plotW, h: 0.2, fill: { color: pal.surfaceElevated }, line: { width: 0 } });
1244
+ slide.addShape('rect', { x: plotX, y: ry + rowH / 2 - 0.1, w, h: 0.2, fill: { color: series(pal)[i % 6] }, line: { width: 0 } });
1245
+ txt(slide, sc2.toFixed(1) + '/' + max, { x: plotX + plotW + 0.05, y: ry, w: 1.1, h: rowH, fontSize: 9.5, color: pal.ink2, valign: 'middle', fontFace: S.fonts.mono });
1246
+ });
1247
+ if (p.totalScore)
1248
+ txt(slide, '总分 ' + sc(p.totalScore), { x: MX, y: BOT - 0.1, w: CW, h: 0.25, fontSize: 11, color: pal.accent, bold: true, fontFace: S.fonts.mono });
1249
+ footer(slide, p.footnoteLeft, p.footnoteRight, pal);
1250
+ }
1251
+ // 24 takeaway(ink)
1252
+ function renderTakeaway(slide, p) {
1253
+ const pal = P('theme09_takeaway_v1');
1254
+ frame(slide, pal, { bottom: false });
1255
+ const y0 = t9Head(slide, p, pal, { size: 26 });
1256
+ const pts = (p.points || []).slice(0, 4);
1257
+ const n = Math.max(pts.length, 1);
1258
+ const rh = (BOT - y0 - 0.1) / n;
1259
+ pts.forEach((pt, i) => {
1260
+ const ry = y0 + i * rh;
1261
+ slide.addShape('ellipse', { x: MX, y: ry + rh / 2 - 0.25, w: 0.5, h: 0.5, fill: { color: series(pal)[i % 6] }, line: { width: 0 } });
1262
+ txt(slide, sc(pt.number) || String(i + 1), { x: MX, y: ry + rh / 2 - 0.25, w: 0.5, h: 0.5, fontSize: 14, color: 'FFFFFF', bold: true, align: 'center', valign: 'middle', fontFace: S.fonts.mono });
1263
+ txt(slide, sc(pt.title), { x: MX + 0.65, y: ry + 0.05, w: CW - 0.65, h: 0.3, fontSize: 13, color: pal.ink, bold: true, fontFace: S.fonts.heading });
1264
+ txt(slide, sc(pt.summary), { x: MX + 0.65, y: ry + 0.38, w: CW - 0.65, h: rh - 0.45, fontSize: 10.5, color: pal.ink2, valign: 'top', fontFace: S.fonts.body, lineSpacingMultiple: 1.15 });
1265
+ });
1266
+ folio(slide, p, pal);
1267
+ }
1268
+ // 25 compare(paper)
1269
+ function renderCompare(slide, p) {
1270
+ const pal = P('theme09_compare_v1');
1271
+ frame(slide, pal, { bottom: false });
1272
+ const y0 = t9Head(slide, p, pal, { size: 26 });
1273
+ const dims = (p.dimensions || []).slice(0, 6);
1274
+ const colW = (CW - 0.4) / 2;
1275
+ const leftX = MX;
1276
+ const rightX = MX + colW + 0.4;
1277
+ const headerH = 0.5;
1278
+ slide.addShape('rect', { x: leftX, y: y0, w: colW, h: headerH, fill: { color: pal.accent } });
1279
+ slide.addShape('rect', { x: rightX, y: y0, w: colW, h: headerH, fill: { color: pal.accent2 } });
1280
+ txt(slide, p.leftTitle, { x: leftX + 0.15, y: y0, w: colW - 0.3, h: headerH, fontSize: 13, color: 'FFFFFF', bold: true, valign: 'middle', fontFace: S.fonts.heading });
1281
+ txt(slide, p.rightTitle, { x: rightX + 0.15, y: y0, w: colW - 0.3, h: headerH, fontSize: 13, color: 'FFFFFF', bold: true, valign: 'middle', fontFace: S.fonts.heading });
1282
+ const rh = Math.min(0.6, (BOT - y0 - headerH - 0.1) / Math.max(dims.length, 1));
1283
+ dims.forEach((d, i) => {
1284
+ const ry = y0 + headerH + 0.05 + i * rh;
1285
+ const winL = String(d.winner) === 'left';
1286
+ slide.addShape('rect', { x: leftX, y: ry, w: colW, h: rh - 0.05, fill: { color: winL ? pal.accent : pal.surfaceElevated }, line: { color: pal.border, width: 0.75 } });
1287
+ slide.addShape('rect', { x: rightX, y: ry, w: colW, h: rh - 0.05, fill: { color: !winL ? pal.accent2 : pal.surfaceElevated }, line: { color: pal.border, width: 0.75 } });
1288
+ if (i === 0) {
1289
+ txt(slide, sc(d.name), { x: leftX, y: y0 + headerH + 0.05, w: colW, h: 0.22, fontSize: 8.5, color: pal.ink3, align: 'center', fontFace: S.fonts.mono });
1290
+ }
1291
+ txt(slide, sc(d.leftValue), { x: leftX + 0.15, y: ry + 0.22, w: colW - 0.3, h: rh - 0.3, fontSize: 10.5, color: winL ? 'FFFFFF' : pal.ink, bold: winL, valign: 'middle', fontFace: S.fonts.body });
1292
+ txt(slide, sc(d.rightValue), { x: rightX + 0.15, y: ry + 0.22, w: colW - 0.3, h: rh - 0.3, fontSize: 10.5, color: !winL ? 'FFFFFF' : pal.ink, bold: !winL, valign: 'middle', fontFace: S.fonts.body });
1293
+ });
1294
+ folio(slide, p, pal);
1295
+ }
1296
+ // 26 process(paper)
1297
+ function renderProcess(slide, p) {
1298
+ const pal = P('theme09_process_v1');
1299
+ frame(slide, pal, { bottom: false });
1300
+ const y0 = t9Head(slide, p, pal, { size: 26 });
1301
+ const steps = (p.steps || []).slice(0, 5);
1302
+ const n = Math.max(steps.length, 1);
1303
+ const gap = 0.25;
1304
+ const cw = (CW - gap * (n - 1)) / n;
1305
+ steps.forEach((s, i) => {
1306
+ const cx = MX + i * (cw + gap);
1307
+ card(slide, cx, y0, cw, 2.4, pal);
1308
+ t9Pill(slide, String(i + 1), cx + 0.18, y0 + 0.18, 0.4, 0.4, series(pal)[i % 6], 'FFFFFF');
1309
+ txt(slide, sc(s.name), { x: cx + 0.66, y: y0 + 0.18, w: cw - 0.8, h: 0.4, fontSize: 12, color: pal.ink, bold: true, valign: 'middle', fontFace: S.fonts.heading });
1310
+ txt(slide, sc(s.description), { x: cx + 0.18, y: y0 + 0.7, w: cw - 0.36, h: 1.5, fontSize: 10, color: pal.ink2, valign: 'top', fontFace: S.fonts.body, lineSpacingMultiple: 1.15 });
1311
+ if (i < n - 1)
1312
+ slide.addShape('line', { x: cx + cw, y: y0 + 1.2, w: gap, h: 0, line: { color: pal.accent, width: 1.5, endArrowType: 'triangle' } });
1313
+ });
1314
+ folio(slide, p, pal);
1315
+ }
1316
+ // 27 faq(paper)
1317
+ function renderFaq(slide, p) {
1318
+ const pal = P('theme09_faq_v1');
1319
+ frame(slide, pal, { bottom: false });
1320
+ const y0 = t9Head(slide, p, pal, { size: 26 });
1321
+ const faqs = (p.faqs || []).slice(0, 6);
1322
+ const colN = 2;
1323
+ const colW = (CW - 0.4) / 2;
1324
+ const rowH = Math.min(0.85, (BOT - y0 - 0.1) / Math.ceil(faqs.length / colN));
1325
+ faqs.forEach((f, i) => {
1326
+ const col = i % colN;
1327
+ const row = Math.floor(i / colN);
1328
+ const cx = MX + col * (colW + 0.4);
1329
+ const cy = y0 + row * rowH;
1330
+ txt(slide, 'Q ' + sc(f.question), { x: cx, y: cy, w: colW - 0.1, h: 0.3, fontSize: 11, color: pal.accent, bold: true, valign: 'top', fontFace: S.fonts.body, lineSpacingMultiple: 1.1 });
1331
+ txt(slide, 'A ' + sc(f.answer), { x: cx, y: cy + 0.32, w: colW - 0.1, h: rowH - 0.36, fontSize: 9.5, color: pal.ink2, valign: 'top', fontFace: S.fonts.body, lineSpacingMultiple: 1.1 });
1332
+ });
1333
+ folio(slide, p, pal);
1334
+ }
1335
+ // ────────────────────────────────────────────────────────────────
1336
+ // 通用渲染器(补齐 P1/P2 共 78 个版式,避免导出落入 "Unknown layout" 占位)
1337
+ // 数据感知:能读到 chart/series/image/quote 字段即渲染真实内容,否则渲染带版式名的标注卡。
1338
+ // ────────────────────────────────────────────────────────────────
1339
+ function toNums(v) {
1340
+ if (v == null)
1341
+ return [];
1342
+ if (typeof v === 'string')
1343
+ return v.split(/[|,\n]/).map((s) => parseFloat(s.trim())).filter((n) => !isNaN(n));
1344
+ if (Array.isArray(v))
1345
+ return v.map((x) => parseFloat(Array.isArray(x) ? x[0] : x)).filter((n) => !isNaN(n));
1346
+ return [];
1347
+ }
1348
+ function normChart(p) {
1349
+ let cats = [];
1350
+ if (Array.isArray(p.categories))
1351
+ cats = p.categories.map(String);
1352
+ else if (Array.isArray(p.labels))
1353
+ cats = p.labels.map(String);
1354
+ else if (Array.isArray(p.quarters))
1355
+ cats = p.quarters.map(String);
1356
+ else if (Array.isArray(p.periods))
1357
+ cats = p.periods.map(String);
1358
+ else if (Array.isArray(p.bins))
1359
+ cats = p.bins.map((b) => String(b.range ?? b.label ?? b));
1360
+ let s = [];
1361
+ if (Array.isArray(p.series))
1362
+ s = p.series.map((x) => ({ name: x.name || '', values: toNums(x.data ?? x.values ?? x.points) }));
1363
+ else if (Array.isArray(p.mainSeries))
1364
+ s = [{ name: p.mainName || '主序列', values: toNums(p.mainSeries) }];
1365
+ if (!cats.length && s.length)
1366
+ cats = s[0].values.map((_, i) => String(i + 1));
1367
+ return { cats, s };
1368
+ }
1369
+ function renderT09ChartGeneric(slide, p, layoutId) {
1370
+ const pal = P(layoutId);
1371
+ frame(slide, pal);
1372
+ const y0 = t9Head(slide, p, pal);
1373
+ const plotX = MX, plotW = CW, plotY = y0 + 0.2, plotH = Math.max(1.6, BOT - plotY - 0.2);
1374
+ const { cats, s } = normChart(p);
1375
+ if (s.length && cats.length) {
1376
+ const data = s.slice(0, 6).map((se, i) => ({ name: se.name || '系列 ' + (i + 1), labels: cats.slice(0, 12), values: se.values.slice(0, 12) }));
1377
+ slide.addChart('bar', data, {
1378
+ x: plotX, y: plotY, w: plotW, h: plotH,
1379
+ barDir: 'col', barGrouping: 'clustered',
1380
+ chartColors: s.slice(0, 6).map((_, i) => series(pal)[i % 6]),
1381
+ showLegend: s.length > 1, legendPos: 'b', legendFontSize: 9, legendColor: pal.ink3,
1382
+ catAxisLabelColor: pal.ink3, valAxisLabelColor: pal.ink3,
1383
+ catAxisLabelFontSize: 9, valAxisLabelFontSize: 9, chartColorsOpacity: 95,
1384
+ });
1385
+ }
1386
+ else {
1387
+ card(slide, plotX, plotY, plotW, plotH, pal);
1388
+ txt(slide, '图表版式 · ' + layoutId.replace('theme09_', '').replace('_v1', ''), {
1389
+ x: plotX + 0.2, y: plotY + plotH / 2 - 0.2, w: plotW - 0.4, h: 0.4,
1390
+ fontSize: 13, color: pal.ink3, align: 'center', fontFace: S.fonts.mono,
1391
+ });
1392
+ }
1393
+ folio(slide, p, pal);
1394
+ }
1395
+ function renderT09MediaGeneric(slide, p, layoutId) {
1396
+ const pal = P(layoutId);
1397
+ frame(slide, pal);
1398
+ const y0 = t9Head(slide, p, pal);
1399
+ const h = Math.max(1.6, BOT - y0 - 1.15);
1400
+ const src = [];
1401
+ if (p.image)
1402
+ src.push(p.image);
1403
+ if (Array.isArray(p.images))
1404
+ src.push(...p.images);
1405
+ else if (Array.isArray(p.photos))
1406
+ src.push(...p.photos);
1407
+ const imgs = src.map(toImgUrl).filter((u) => typeof u === 'string' && u.length > 0).slice(0, 4);
1408
+ if (imgs.length) {
1409
+ const gap = 0.15;
1410
+ const cw = (CW - gap * (imgs.length - 1)) / imgs.length;
1411
+ imgs.forEach((u, i) => {
1412
+ const x = MX + i * (cw + gap);
1413
+ tryImage(slide, u, x, y0 + 0.2, cw, h, () => card(slide, x, y0 + 0.2, cw, h, pal));
1414
+ });
1415
+ }
1416
+ else {
1417
+ card(slide, MX, y0 + 0.2, CW, h, pal);
1418
+ txt(slide, '影像版式 · ' + layoutId.replace('theme09_', '').replace('_v1', ''), {
1419
+ x: MX + 0.2, y: y0 + 0.2 + h / 2 - 0.2, w: CW - 0.4, h: 0.4,
1420
+ fontSize: 13, color: pal.ink3, align: 'center', fontFace: S.fonts.mono,
1421
+ });
1422
+ }
1423
+ let iy = y0 + 0.2 + h + 0.12;
1424
+ if (p.quote)
1425
+ txt(slide, p.quote, { x: MX, y: iy, w: CW, h: 0.5, fontSize: 14, color: pal.accent, bold: true, fontFace: S.fonts.heading, italic: true });
1426
+ const items = (p.items || p.points || []).slice(0, 5);
1427
+ items.forEach((t, i) => {
1428
+ const s2 = typeof t === 'string' ? t : (t.text || t.label || t.title || '');
1429
+ txt(slide, '– ' + s2, { x: MX, y: iy + (p.quote ? 0.55 : 0) + i * 0.3, w: CW, h: 0.3, fontSize: 11, color: pal.ink2, fontFace: S.fonts.body });
1430
+ });
1431
+ folio(slide, p, pal);
1432
+ }
1433
+ const CHART_LAYOUTS = [
1434
+ 'theme09_market_overview_v1', 'theme09_streamgraph_v1', 'theme09_chord_v1', 'theme09_sunburst_v1',
1435
+ 'theme09_ribbon_v1', 'theme09_rounds_v1', 'theme09_ranking_v1', 'theme09_bump_v1',
1436
+ 'theme09_hero_number_v1', 'theme09_versus_v1', 'theme09_spiral_v1', 'theme09_funnel_v1',
1437
+ 'theme09_stat_grid_v1', 'theme09_arc_v1', 'theme09_network_v1', 'theme09_area_v1',
1438
+ 'theme09_mega_number_v1', 'theme09_radar_v1', 'theme09_radialbar_v1', 'theme09_honeycomb_v1',
1439
+ 'theme09_tornado_v1', 'theme09_matrix_v1', 'theme09_quadrant_v1', 'theme09_bubble_v1',
1440
+ 'theme09_marimekko_v1', 'theme09_meter_v1', 'theme09_parallel_v1', 'theme09_grade_v1',
1441
+ 'theme09_slope_v1', 'theme09_dumbbell_v1', 'theme09_crosstab_v1', 'theme09_tier_v1',
1442
+ 'theme09_ledger_v1', 'theme09_alloc_v1', 'theme09_venn_v1', 'theme09_treemap_v1',
1443
+ 'theme09_icicle_v1', 'theme09_waterfall_v1', 'theme09_heatmap_v1',
1444
+ ];
1445
+ const MEDIA_LAYOUTS = [
1446
+ 'theme09_specimen_v1', 'theme09_photo_feature_v1', 'theme09_photo_grid_v1', 'theme09_photo_quote_v1',
1447
+ 'theme09_photo_duo_v1', 'theme09_photo_panorama_v1', 'theme09_photo_stage_v1', 'theme09_storyboard_v1',
1448
+ 'theme09_snapshot_tape_v1', 'theme09_epigraph_v1', 'theme09_photo_bento_v1', 'theme09_timeline_photo_v1',
1449
+ 'theme09_filmstrip_v1', 'theme09_mosaic_v1', 'theme09_testimonial_v1', 'theme09_team_v1',
1450
+ 'theme09_photo_ring_v1', 'theme09_divider_photo_v1', 'theme09_coverstory_v1', 'theme09_quote_portrait_v1',
1451
+ 'theme09_manifesto_v1', 'theme09_annotated_v1', 'theme09_diptych_v1', 'theme09_case_folio_v1',
1452
+ 'theme09_split_diagonal_v1', 'theme09_typeriver_v1', 'theme09_exhibit_wall_v1', 'theme09_masonry_v1',
1453
+ 'theme09_journey_v1', 'theme09_photo_cards_v1', 'theme09_zine_spread_v1', 'theme09_photo_scene_v1',
1454
+ 'theme09_spotlight_v1', 'theme09_profile_v1', 'theme09_gallery_wall_v1', 'theme09_dotmatrix_v1',
1455
+ ];
1456
+ const bindChart = (id) => (s, p) => renderT09ChartGeneric(s, p, id);
1457
+ const bindMedia = (id) => (s, p) => renderT09MediaGeneric(s, p, id);
1458
+ const CHART_RENDERERS = Object.fromEntries(CHART_LAYOUTS.map((id) => [id, bindChart(id)]));
1459
+ const MEDIA_RENDERERS = Object.fromEntries(MEDIA_LAYOUTS.map((id) => [id, bindMedia(id)]));
1460
+ const RENDERERS = {
1461
+ theme09_cover_masthead_v1: renderCoverMasthead,
1462
+ theme09_cover_bleed_v1: renderCoverBleed,
1463
+ theme09_cover_dossier_v1: renderCoverDossier,
1464
+ theme09_cover_colorbar_v1: renderCoverColorbar,
1465
+ theme09_cover_aperture_v1: renderCoverAperture,
1466
+ theme09_cover_colophon_v1: renderCoverColophon,
1467
+ theme09_cover_photo_v1: renderCoverPhoto,
1468
+ theme09_abstract_v1: renderAbstract,
1469
+ theme09_contents_v1: renderContents,
1470
+ theme09_section_v1: renderSection,
1471
+ theme09_section_card_v1: renderSectionCard,
1472
+ theme09_closing_v1: renderClosing,
1473
+ theme09_thesis_v1: renderThesis,
1474
+ theme09_risk_v1: renderRisk,
1475
+ theme09_outlook_v1: renderOutlook,
1476
+ theme09_conclusion_v1: renderConclusion,
1477
+ theme09_bracket_v1: renderBracket,
1478
+ theme09_flow_v1: renderFlow,
1479
+ theme09_orbit_v1: renderOrbit,
1480
+ theme09_vertical_v1: renderVertical,
1481
+ theme09_calendar_v1: renderCalendar,
1482
+ theme09_phases_v1: renderPhases,
1483
+ theme09_gauge_v1: renderGauge,
1484
+ theme09_scoreboard_v1: renderScoreboard,
1485
+ theme09_trend_v1: renderTrend,
1486
+ theme09_histogram_v1: renderHistogram,
1487
+ theme09_plans_v1: renderPlans,
1488
+ theme09_stair_v1: renderStair,
1489
+ theme09_stacked_v1: renderStacked,
1490
+ theme09_era_v1: renderEra,
1491
+ theme09_roadmap_v1: renderRoadmap,
1492
+ theme09_score_v1: renderScore,
1493
+ theme09_takeaway_v1: renderTakeaway,
1494
+ theme09_compare_v1: renderCompare,
1495
+ theme09_process_v1: renderProcess,
1496
+ theme09_faq_v1: renderFaq,
1497
+ // 模板版式 id 用下划线(cross_perspective / value_chain / forecast_fan),
1498
+ // 与源码 hyphen 键别名对齐,避免导出落到 Unknown layout。
1499
+ 'theme09_cross_perspective_v1': renderCrossPerspective,
1500
+ 'theme09_value_chain_v1': renderValueChain,
1501
+ 'theme09_forecast_fan_v1': renderForecastFan,
1502
+ // ── P2 图表型(按 layoutId 绑定双基底,数据感知)──
1503
+ ...CHART_RENDERERS,
1504
+ // ── P1 图片 / 标本 / 金句型(按 layoutId 绑定双基底,数据感知)──
1505
+ ...MEDIA_RENDERERS,
1506
+ };
1507
+ /** 将全部 theme09 渲染器注册到导出管线的按 layoutId 索引注册表。 */
1508
+ export function registerTheme09Renderers(register) {
1509
+ for (const [id, fn] of Object.entries(RENDERERS)) {
1510
+ register(id, fn);
1511
+ }
1512
+ }
1513
+ //# sourceMappingURL=theme09-pptx.js.map