@topmindspace/tms-skills 0.1.0 → 0.1.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 (102) hide show
  1. package/CHANGELOG.md +76 -0
  2. package/README.md +9 -3
  3. package/bin/tms-skills.js +200 -160
  4. package/package.json +12 -9
  5. package/top-ppt-html/README.md +162 -254
  6. package/top-ppt-html/SKILL.md +126 -111
  7. package/top-ppt-html/assets/examples/2026-09-09-architecture-graphite-dark.html +3926 -3926
  8. package/top-ppt-html/assets/examples/2026-09-09-architecture-graphite-dark.model.json +167 -167
  9. package/top-ppt-html/assets/examples/2026-09-09-presentation-business-blue.html +4325 -4325
  10. package/top-ppt-html/assets/examples/2026-09-09-presentation-business-blue.model.json +320 -320
  11. package/top-ppt-html/assets/examples/2026-09-09-research-mckinsey.html +5527 -5527
  12. package/top-ppt-html/assets/examples/2026-09-09-research-mckinsey.model.json +913 -913
  13. package/top-ppt-html/assets/pptx-export.js +1944 -1944
  14. package/top-ppt-html/assets/style-gallery.html +589 -589
  15. package/top-ppt-html/assets/templates/architecture.html +3728 -3728
  16. package/top-ppt-html/assets/templates/engine.css +840 -840
  17. package/top-ppt-html/assets/templates/presentation.html +3738 -3738
  18. package/top-ppt-html/assets/templates/research.html +4017 -4017
  19. package/top-ppt-html/assets/templates/ui.js +520 -520
  20. package/top-ppt-html/assets/theme-overview-architecture.png +0 -0
  21. package/top-ppt-html/assets/theme-overview-research.png +0 -0
  22. package/top-ppt-html/assets/theme-overview.png +0 -0
  23. package/top-ppt-html/evals/prompts.csv +15 -15
  24. package/top-ppt-html/evals/rubric.schema.json +25 -25
  25. package/top-ppt-html/evals/run_evals.py +220 -220
  26. package/top-ppt-html/evals/trace.example.json +16 -16
  27. package/top-ppt-html/package-lock.json +186 -0
  28. package/top-ppt-html/package.json +11 -11
  29. package/top-ppt-html/references/charts-basic.md +624 -624
  30. package/top-ppt-html/references/charts-discipline.md +110 -108
  31. package/top-ppt-html/references/charts-extended.md +482 -482
  32. package/top-ppt-html/references/charts.md +37 -28
  33. package/top-ppt-html/references/components-atoms.md +624 -624
  34. package/top-ppt-html/references/components.md +30 -30
  35. package/top-ppt-html/references/content-rules.md +510 -490
  36. package/top-ppt-html/references/default-surface.md +61 -0
  37. package/top-ppt-html/references/design-system.md +478 -478
  38. package/top-ppt-html/references/failure-modes.md +227 -214
  39. package/top-ppt-html/references/high-fidelity.md +127 -127
  40. package/top-ppt-html/references/icons.md +176 -397
  41. package/top-ppt-html/references/infographics-stats.md +308 -308
  42. package/top-ppt-html/references/infographics-structure.md +226 -226
  43. package/top-ppt-html/references/infographics.md +43 -43
  44. package/top-ppt-html/references/layout-grammar.md +325 -315
  45. package/top-ppt-html/references/layouts-architecture.md +108 -108
  46. package/top-ppt-html/references/layouts-combo.md +600 -600
  47. package/top-ppt-html/references/layouts-research.md +160 -160
  48. package/top-ppt-html/references/modes.md +259 -254
  49. package/top-ppt-html/references/outline-design.md +275 -275
  50. package/top-ppt-html/references/playbook.md +284 -266
  51. package/top-ppt-html/references/pptx-export.md +227 -209
  52. package/top-ppt-html/references/presentation-craft.md +69 -0
  53. package/top-ppt-html/references/styles.md +370 -370
  54. package/top-ppt-html/references/tech-design.md +138 -138
  55. package/top-ppt-html/scripts/audit_css.py +109 -109
  56. package/top-ppt-html/scripts/audit_docs.py +176 -176
  57. package/top-ppt-html/scripts/audit_skill.py +231 -220
  58. package/top-ppt-html/scripts/audit_styles.py +351 -351
  59. package/top-ppt-html/scripts/build_examples.py +55 -2276
  60. package/top-ppt-html/scripts/build_pptx.js +2386 -2380
  61. package/top-ppt-html/scripts/capture_theme_overview.js +79 -78
  62. package/top-ppt-html/scripts/checks_html.py +166 -127
  63. package/top-ppt-html/scripts/cross_verify.py +298 -294
  64. package/top-ppt-html/scripts/env_probe.py +158 -158
  65. package/top-ppt-html/scripts/extract_model.py +210 -210
  66. package/top-ppt-html/scripts/extract_snippet.py +374 -374
  67. package/top-ppt-html/scripts/gen_channel_a.js +214 -214
  68. package/top-ppt-html/scripts/layout-constants.json +111 -18
  69. package/top-ppt-html/scripts/lib_layout_regions.js +410 -412
  70. package/top-ppt-html/scripts/measure_height.py +178 -178
  71. package/top-ppt-html/scripts/negative_tests.py +433 -307
  72. package/top-ppt-html/scripts/package_skill.py +294 -291
  73. package/top-ppt-html/scripts/prepare_images.py +341 -341
  74. package/top-ppt-html/scripts/probe_image_export.py +187 -188
  75. package/top-ppt-html/scripts/quality_gate.py +309 -301
  76. package/top-ppt-html/scripts/recommend_layout.py +384 -0
  77. package/top-ppt-html/scripts/regression.py +308 -308
  78. package/top-ppt-html/scripts/render_compare.py +275 -275
  79. package/top-ppt-html/scripts/render_from_model.py +698 -698
  80. package/top-ppt-html/scripts/scaffold_report.py +1054 -1054
  81. package/top-ppt-html/scripts/section-file-map.json +104 -104
  82. package/top-ppt-html/scripts/smoke_pptx.sh +30 -0
  83. package/top-ppt-html/scripts/sync_runtime.py +659 -662
  84. package/top-ppt-html/scripts/validate_pptx.py +1579 -1510
  85. package/top-ppt-html/scripts/validate_report.py +1755 -1456
  86. package/top-ppt-html/assets/examples/2026-09-09-architecture-spectrum.html +0 -3926
  87. package/top-ppt-html/assets/examples/2026-09-09-architecture-spectrum.model.json +0 -168
  88. package/top-ppt-html/assets/examples/2026-09-09-presentation-apple-mono.html +0 -4325
  89. package/top-ppt-html/assets/examples/2026-09-09-presentation-apple-mono.model.json +0 -321
  90. package/top-ppt-html/assets/examples/2026-09-09-presentation-brand-red.html +0 -4325
  91. package/top-ppt-html/assets/examples/2026-09-09-presentation-brand-red.model.json +0 -321
  92. package/top-ppt-html/assets/examples/2026-09-09-research-deep-teal.html +0 -5527
  93. package/top-ppt-html/assets/examples/2026-09-09-research-deep-teal.model.json +0 -914
  94. package/top-ppt-html/assets/examples/2026-09-09-research-indigo-violet.html +0 -5527
  95. package/top-ppt-html/assets/examples/2026-09-09-research-indigo-violet.model.json +0 -914
  96. package/top-ppt-html/assets/examples/2026-09-09-research-warm-sand.html +0 -5527
  97. package/top-ppt-html/assets/examples/2026-09-09-research-warm-sand.model.json +0 -914
  98. package/top-ppt-html/assets/theme-overview-presentation.png +0 -0
  99. package/top-ppt-html/references/design-system-engine.md +0 -235
  100. package/top-ppt-html/references/industry-benchmark.md +0 -105
  101. package/top-ppt-html/references/reform-plan.md +0 -252
  102. package/top-ppt-html/scripts/layout_slots.json +0 -830
@@ -1,412 +1,410 @@
1
- /**
2
- * TopPPT HTML · 页型布局区域 IR(双通道几何收敛起点)
3
- *
4
- * 从 layout-constants.json + layout_slots.json 派生「语义槽位 → 英寸矩形」。
5
- * B 通道(build_pptx.js)直接 require;A 通道消费同一 LC 常量(数字同源),
6
- * 本模块是把「槽位语义」从散落坐标收拢成可测接口的第一步。
7
- *
8
- * 纪律:改几何只改 layout-constants.json;改槽位只改 layout_slots.json。
9
- * 新增页型先扩 layout_slots.json,再在此补 regionOf 分支,最后写双引擎。
10
- */
11
- 'use strict';
12
-
13
- const fs = require('fs');
14
- const path = require('path');
15
-
16
- const ROOT = path.join(__dirname, '..');
17
- const LC = JSON.parse(fs.readFileSync(path.join(__dirname, 'layout-constants.json'), 'utf8'));
18
- // 布局 IR:优先 layout-constants.layoutSlots(v9 单源合并);否则回落 layout_slots.json
19
- let SLOTS = LC.layoutSlots;
20
- if (!SLOTS || !SLOTS.pageTypes) {
21
- SLOTS = JSON.parse(fs.readFileSync(path.join(__dirname, 'layout_slots.json'), 'utf8'));
22
- }
23
-
24
- const INFO_TYPES = {
25
- sankey: 1, treemap: 1, boxplot: 1, network: 1, marimekko: 1, streamgraph: 1,
26
- };
27
-
28
- function pageBox() {
29
- const p = LC.page;
30
- return { pw: p.pw, ph: p.ph, mx: p.mx, cw: p.pw - 2 * p.mx };
31
- }
32
-
33
- /** 内容区垂直范围(含注释层预留) */
34
- function contentBand(opts) {
35
- const L = LC.pageTypes.layout;
36
- const C = LC.pageTypes.common;
37
- const withNote = !!(opts && opts.withNote);
38
- const top = (opts && opts.top != null) ? opts.top : C.bodyY;
39
- const bottom = withNote ? L.contentBottomWithNote : L.contentBottom;
40
- return { top, bottom, h: Math.max(0, bottom - top) };
41
- }
42
-
43
- /**
44
- * 语义槽位 → {x,y,w,h}(英寸)
45
- * 未识别页型/槽位时返回 null(调用方回落自身逻辑,不中断)。
46
- */
47
- function regionOf(pageType, slotId, opts) {
48
- const type = String(pageType || 'points');
49
- const slot = String(slotId || '');
50
- const { mx, cw } = pageBox();
51
- const C = LC.pageTypes.common;
52
- const L = LC.pageTypes.layout;
53
- const PT = LC.pageTypes[type] || {};
54
-
55
- // ── chrome:页头 ──
56
- if (slot === 'head' || slot === 'chrome') {
57
- return {
58
- x: mx, y: C.headEyebrowY, w: cw,
59
- h: Math.max(0.4, (C.headRuleY || 1.95) - C.headEyebrowY),
60
- titleY: C.headTitleY, ruleY: C.headRuleY, leadY: C.leadY, bodyY: C.bodyY,
61
- };
62
- }
63
-
64
- // ── annotation:so-what / 来源 / note ──
65
- if (slot === 'annotation' || slot === 'soWhat') {
66
- const y = (LC.pageTypes.exhibit && LC.pageTypes.exhibit.soWhatY != null)
67
- ? LC.pageTypes.exhibit.soWhatY : 6.05;
68
- return { x: mx, y, w: cw, h: 0.62 };
69
- }
70
- if (slot === 'footnote') {
71
- const y = (LC.pageTypes.exhibit && LC.pageTypes.exhibit.footnoteY != null)
72
- ? LC.pageTypes.exhibit.footnoteY
73
- : ((LC.pageTypes.note && LC.pageTypes.note.y) || 6.55);
74
- return { x: mx, y, w: cw - 1.2, h: (LC.pageTypes.note && LC.pageTypes.note.h) || 0.32 };
75
- }
76
-
77
- // ── 页型主从区域 ──
78
- if (type === 'points') {
79
- const band = contentBand(opts);
80
- if (slot === 'primary' || slot === 'list') {
81
- return { x: mx, y: PT.listY || band.top, w: cw, h: Math.max(0.8, band.bottom - (PT.listY || band.top)) };
82
- }
83
- if (slot === 'metrics') {
84
- return { x: mx, y: PT.metricY || 4.7, w: cw, h: Math.max(0.6, L.contentBottom - (PT.metricY || 4.7)) };
85
- }
86
- }
87
-
88
- if (type === 'metrics') {
89
- if (slot === 'primary') {
90
- return {
91
- x: mx, y: PT.bandY || 3.1, w: cw, h: PT.cardH || 2.3,
92
- valY: PT.valY, valH: PT.valH, capY: PT.capY, capH: PT.capH,
93
- gap: PT.gap, maxW: PT.maxW,
94
- };
95
- }
96
- }
97
-
98
- if (type === 'table') {
99
- if (slot === 'primary') {
100
- const y = PT.y || L.contentTop;
101
- return {
102
- x: mx, y, w: cw,
103
- h: Math.max(1, ((opts && opts.bottom) || L.contentBottom) - y),
104
- rowH: PT.rowH, rowHMin: PT.rowHMin, headRowH: PT.headRowH,
105
- };
106
- }
107
- }
108
-
109
- if (type === 'bar') {
110
- if (slot === 'primary') {
111
- const y = PT.chartY || 2.95;
112
- const bottom = (opts && opts.bottom != null) ? opts.bottom
113
- : ((opts && opts.withNote) ? L.contentBottomWithNote : L.contentBottom);
114
- return { x: mx + (PT.chartX || 0), y, w: cw - (PT.chartW || 0), h: Math.max(1.2, bottom - y) };
115
- }
116
- if (slot === 'hbar') {
117
- const y0 = PT.hbarY0 || 2.6;
118
- const y1 = (opts && opts.bottom != null) ? opts.bottom : (PT.hbarY1 || 6.25);
119
- return { x: mx, y: y0, w: cw, h: Math.max(1.2, y1 - y0) };
120
- }
121
- }
122
-
123
- if (type === 'donut') {
124
- if (slot === 'primary') {
125
- const y = PT.chartY || 2.95;
126
- const bottom = (opts && opts.bottom != null) ? opts.bottom : L.contentBottom;
127
- return { x: mx, y, w: cw, h: Math.max(1.2, bottom - y) };
128
- }
129
- }
130
-
131
- if (type === 'exhibit') {
132
- if (slot === 'primary') {
133
- const y = PT.chartY || 2.95;
134
- const h = Math.max(1.2, (L.contentBottomWithNote - y));
135
- return { x: mx, y, w: cw, h, badgeY: PT.badgeY };
136
- }
137
- if (slot === 'secondary') {
138
- return { x: mx, y: PT.badgeY || L.contentTop, w: cw * 0.32, h: 0.4 };
139
- }
140
- }
141
-
142
- if (type === 'points') {
143
- const band = contentBand(opts);
144
- if (slot === 'primary' || slot === 'list') {
145
- return {
146
- x: mx, y: PT.listY || band.top, w: cw,
147
- h: Math.max(0.8, (opts && opts.bottom != null ? opts.bottom : band.bottom) - (PT.listY || band.top)),
148
- rowH: PT.rowH, rowHMin: PT.rowHMin, markSize: PT.markSize,
149
- };
150
- }
151
- if (slot === 'metrics') {
152
- return {
153
- x: mx, y: PT.metricY || 4.7, w: cw,
154
- h: Math.max(0.6, L.contentBottom - (PT.metricY || 4.7)),
155
- metricW: PT.metricW, metricValH: PT.metricValH, metricCapY: PT.metricCapY,
156
- };
157
- }
158
- }
159
-
160
- if (type === 'split') {
161
- const SP = PT;
162
- const top = SP.chartY || L.contentTop;
163
- const bottom = (opts && opts.bottom != null) ? opts.bottom : L.contentBottom;
164
- if (slot === 'left') {
165
- return {
166
- x: mx, y: top, w: cw * (SP.leftW || 0.55),
167
- h: Math.max(1.2, bottom - top),
168
- tableY: SP.tableY, tableRowH: SP.tableRowH, rowH: SP.rowH, capY: SP.capY,
169
- };
170
- }
171
- if (slot === 'right') {
172
- const rx = mx + cw * (SP.rightX != null ? SP.rightX : 0.585);
173
- return {
174
- x: rx, y: top, w: cw * (SP.rightW || 0.415),
175
- h: Math.max(1.2, bottom - top),
176
- tableY: SP.tableY, tableRowH: SP.tableRowH, rowH: SP.rowH, capY: SP.capY,
177
- };
178
- }
179
- }
180
-
181
- if (type === 'twocol') {
182
- const gap = PT.colGap || 0.5;
183
- const colW = (cw - gap) / 2;
184
- const top = (opts && opts.top != null) ? opts.top : L.contentTop;
185
- const bottom = (opts && opts.bottom != null) ? opts.bottom : L.contentBottom;
186
- if (slot === 'primary' || slot === 'left') {
187
- return { x: mx, y: top, w: colW, h: Math.max(1, bottom - top), gap, colW };
188
- }
189
- if (slot === 'secondary' || slot === 'right') {
190
- return { x: mx + colW + gap, y: top, w: colW, h: Math.max(1, bottom - top), gap, colW };
191
- }
192
- }
193
-
194
- if (type === 'diagram' || type === 'lane') {
195
- if (slot === 'primary') {
196
- const arch = LC.pageTypes.arch || {};
197
- const y = (opts && opts.mode === 'architecture' && arch.fullStartY != null)
198
- ? arch.fullStartY
199
- : (PT.bodyStartY || 2.9);
200
- const b = (opts && opts.mode === 'architecture' && arch.fullEndY != null)
201
- ? arch.fullEndY
202
- : ((opts && opts.bottom != null) ? opts.bottom : L.contentBottom);
203
- return {
204
- x: mx, y, w: cw, h: Math.max(1.2, b - y),
205
- layerBarW: PT.layerBarW, layerGap: PT.layerGap, maxLayerH: PT.maxLayerH,
206
- laneH: arch.laneH, laneHeadW: arch.laneHeadW, laneGap: arch.laneGap,
207
- stepGap: arch.stepGap, stepMaxW: arch.stepMaxW,
208
- };
209
- }
210
- }
211
-
212
- // ── 剩余高频页型 ──
213
- if (type === 'kpi') {
214
- if (slot === 'primary' || slot === 'hero') {
215
- return {
216
- x: mx, y: PT.heroY || 2.9, w: cw * (PT.heroW || 0.52), h: PT.heroH || 1.6,
217
- dividerX: PT.dividerX, metricY0: PT.metricY0, metricRowH: PT.metricRowH,
218
- };
219
- }
220
- if (slot === 'metrics') {
221
- const dx = PT.dividerX != null ? PT.dividerX : 0.56;
222
- return {
223
- x: mx + cw * dx + 0.35, y: PT.metricY0 || 2.95,
224
- w: cw * (1 - dx) - 0.5, h: Math.max(0.6, L.contentBottom - (PT.metricY0 || 2.95)),
225
- };
226
- }
227
- }
228
-
229
- if (type === 'comparison') {
230
- const gap = PT.panelGap || 0.4;
231
- const panelW = (cw - gap) / 2;
232
- const y = PT.panelY || 2.55;
233
- const h = PT.panelH || 3.4;
234
- if (slot === 'primary' || slot === 'left') {
235
- return { x: mx, y, w: panelW, h, titleH: PT.titleH, rowH: PT.rowH };
236
- }
237
- if (slot === 'right' || slot === 'secondary') {
238
- return { x: mx + panelW + gap, y, w: panelW, h, titleH: PT.titleH, rowH: PT.rowH };
239
- }
240
- }
241
-
242
- if (type === 'quote') {
243
- if (slot === 'primary') {
244
- return {
245
- x: mx + 1.05, y: PT.textY || 2.7, w: cw - 2.1, h: PT.textH || 2.3,
246
- markY: PT.markY, authorY: PT.authorY, contextY: PT.contextY,
247
- };
248
- }
249
- }
250
-
251
- if (type === 'heatmap') {
252
- if (slot === 'primary') {
253
- const y = PT.y || 2.6;
254
- const bottom = (opts && opts.bottom != null) ? opts.bottom : L.contentBottom;
255
- return {
256
- x: mx, y, w: cw, h: Math.max(1, bottom - y),
257
- labelW: PT.labelW, headH: PT.headH, cellH: PT.cellH, cellGap: PT.cellGap,
258
- };
259
- }
260
- }
261
-
262
- if (type === 'bullet') {
263
- if (slot === 'primary') {
264
- const y = PT.y || 2.6;
265
- const bottom = (opts && opts.bottom != null) ? opts.bottom : L.contentBottom;
266
- return {
267
- x: mx, y, w: cw, h: Math.max(1, bottom - y),
268
- rowH: PT.rowH, labelW: PT.labelW, valW: PT.valW, barH: PT.barH,
269
- };
270
- }
271
- }
272
-
273
- if (type === 'pyramid') {
274
- if (slot === 'primary') {
275
- const y = PT.y || 2.35;
276
- const bottom = (opts && opts.bottom != null) ? opts.bottom : L.contentBottom;
277
- return {
278
- x: mx, y, w: cw, h: Math.max(1, bottom - y),
279
- rowH: PT.rowH, gap: PT.gap, minW: PT.minW, labelW: PT.labelW,
280
- };
281
- }
282
- }
283
-
284
- if (type === 'steps') {
285
- if (slot === 'primary') {
286
- const y = PT.y || 3.05;
287
- return {
288
- x: mx, y, w: cw, h: PT.barH || 1.35,
289
- gap: PT.gap, arrowW: PT.arrowW, numH: PT.numH, titleH: PT.titleH,
290
- pad: PT.pad, maxPerRow: PT.maxPerRow,
291
- };
292
- }
293
- }
294
-
295
- if (type === 'cards') {
296
- if (slot === 'primary') {
297
- const y = PT.startY || 3.0;
298
- const bottom = (opts && opts.bottom != null) ? opts.bottom : L.contentBottom;
299
- return { x: mx, y, w: cw, h: Math.max(1, bottom - y), gap: PT.gap, maxH: PT.maxH, titleH: PT.titleH };
300
- }
301
- }
302
-
303
- if (type === 'timeline') {
304
- if (slot === 'primary') {
305
- return {
306
- x: mx, y: PT.labelY || 2.95, w: cw,
307
- h: Math.max(1, L.contentBottom - (PT.labelY || 2.95)),
308
- axisY: PT.axisY, dotY: PT.dotY, dotR: PT.dotR, nameY: PT.nameY,
309
- descY: PT.descY, descH: PT.descH,
310
- };
311
- }
312
- }
313
-
314
- if (type === 'image') {
315
- if (slot === 'primary') {
316
- const y = Math.max(PT.y || 2.3, (opts && opts.top) || 0);
317
- const bottom = (opts && opts.bottom != null) ? opts.bottom : L.contentBottom;
318
- const capH = (opts && opts.caption) ? 0.32 : 0;
319
- return {
320
- x: mx, y, w: cw, h: Math.max(1.2, Math.min(PT.h || 4.2, bottom - y - capH)),
321
- halfW: PT.halfW, halfGap: PT.halfGap, capY: PT.capY, capH: PT.capH || 0.3,
322
- radius: PT.radius, gridGap: PT.gridGap, gridCols: PT.gridCols,
323
- };
324
- }
325
- }
326
-
327
- if (type === 'halftable') {
328
- if (slot === 'primary' || slot === 'left') {
329
- const r = LC.pageTypes.research || {};
330
- const top = (opts && opts.top != null) ? opts.top : L.contentTop;
331
- const bottom = (opts && opts.bottom != null) ? opts.bottom : L.contentBottom;
332
- return { x: mx, y: top, w: cw * (r.halfTableW || 0.52), h: Math.max(1, bottom - top) };
333
- }
334
- if (slot === 'secondary' || slot === 'right') {
335
- const r = LC.pageTypes.research || {};
336
- const top = (opts && opts.top != null) ? opts.top : L.contentTop;
337
- const bottom = (opts && opts.bottom != null) ? opts.bottom : L.contentBottom;
338
- return {
339
- x: mx + cw * (r.halfChartX || 0.57), y: top,
340
- w: cw * (r.halfChartW || 0.43), h: Math.max(1, bottom - top),
341
- };
342
- }
343
- }
344
-
345
- if (type === 'matrix') {
346
- if (slot === 'primary') {
347
- const r = LC.pageTypes.research || {};
348
- const y = r.matrixY || 2.3;
349
- const bottom = (opts && opts.bottom != null) ? opts.bottom : L.contentBottom;
350
- return {
351
- x: mx, y, w: cw, h: Math.max(1, bottom - y),
352
- labelW: r.matrixLabelW, cellH: r.matrixCellH,
353
- };
354
- }
355
- }
356
-
357
- if (type === 'threecol') {
358
- const r = LC.pageTypes.research || {};
359
- const gap = r.col3Gap || 0.38;
360
- const colW = (cw - 2 * gap) / 3;
361
- const top = (opts && opts.top != null) ? opts.top : L.contentTop;
362
- const bottom = (opts && opts.bottom != null) ? opts.bottom : L.contentBottom;
363
- if (slot === 'primary' || slot === 'left') {
364
- return { x: mx, y: top, w: colW, h: Math.max(1, bottom - top), gap, colW, cols: 3 };
365
- }
366
- if (slot === 'middle') {
367
- return { x: mx + colW + gap, y: top, w: colW, h: Math.max(1, bottom - top), gap, colW, cols: 3 };
368
- }
369
- if (slot === 'right' || slot === 'secondary') {
370
- return { x: mx + 2 * (colW + gap), y: top, w: colW, h: Math.max(1, bottom - top), gap, colW, cols: 3 };
371
- }
372
- }
373
-
374
- // 信息图页型:内容区主图
375
- if (INFO_TYPES[type] && (slot === 'primary')) {
376
- const y = PT.y || L.contentTop;
377
- const h = PT.h || 4.1;
378
- const bottom = (opts && opts.bottom != null) ? opts.bottom : L.contentBottom;
379
- return { x: mx, y, w: cw, h: Math.max(1.2, Math.min(h, bottom - y)) };
380
- }
381
-
382
- // 默认:内容区全宽
383
- if (slot === 'primary' || slot === 'secondary' || slot === 'left' || slot === 'right') {
384
- const band = contentBand(opts);
385
- return { x: mx, y: band.top, w: cw, h: band.h };
386
- }
387
- return null;
388
- }
389
-
390
- /** 槽位是否在 layout_slots.json 中声明为 required */
391
- function slotRequired(pageType, slotId) {
392
- const def = (SLOTS.pageTypes || {})[pageType];
393
- if (!def) return false;
394
- const s = (def.slots || []).find(x => x.id === slotId);
395
- return !!(s && s.required);
396
- }
397
-
398
- /** 列出页型已声明槽位 id */
399
- function slotIds(pageType) {
400
- const def = (SLOTS.pageTypes || {})[pageType];
401
- return (def && def.slots) ? def.slots.map(s => s.id) : [];
402
- }
403
-
404
- module.exports = {
405
- LC,
406
- SLOTS,
407
- pageBox,
408
- contentBand,
409
- regionOf,
410
- slotRequired,
411
- slotIds,
412
- };
1
+ /**
2
+ * TopPPT HTML · 页型布局区域 IR(双通道几何收敛起点)
3
+ *
4
+ * 从 layout-constants.json(含 layoutSlots)派生「语义槽位 → 英寸矩形」。
5
+ * B 通道(build_pptx.js)直接 require;A 通道消费同一 LC 常量(数字同源)。
6
+ *
7
+ * 纪律:改几何与槽位只改 layout-constants.json(layoutSlots 已并入)。
8
+ * 新增页型先扩 LC.layoutSlots,再在此补 regionOf 分支,最后写双引擎。
9
+ */
10
+ 'use strict';
11
+
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+
15
+ const ROOT = path.join(__dirname, '..');
16
+ const LC = JSON.parse(fs.readFileSync(path.join(__dirname, 'layout-constants.json'), 'utf8'));
17
+ const SLOTS = LC.layoutSlots;
18
+ if (!SLOTS || !SLOTS.pageTypes) {
19
+ throw new Error('layout-constants.json 缺 layoutSlots.pageTypes(Batch 3 单源)');
20
+ }
21
+
22
+ const INFO_TYPES = {
23
+ sankey: 1, treemap: 1, boxplot: 1, network: 1, marimekko: 1, streamgraph: 1,
24
+ };
25
+
26
+ function pageBox() {
27
+ const p = LC.page;
28
+ return { pw: p.pw, ph: p.ph, mx: p.mx, cw: p.pw - 2 * p.mx };
29
+ }
30
+
31
+ /** 内容区垂直范围(含注释层预留) */
32
+ function contentBand(opts) {
33
+ const L = LC.pageTypes.layout;
34
+ const C = LC.pageTypes.common;
35
+ const withNote = !!(opts && opts.withNote);
36
+ const top = (opts && opts.top != null) ? opts.top : C.bodyY;
37
+ const bottom = withNote ? L.contentBottomWithNote : L.contentBottom;
38
+ return { top, bottom, h: Math.max(0, bottom - top) };
39
+ }
40
+
41
+ /**
42
+ * 语义槽位 → {x,y,w,h}(英寸)
43
+ * 未识别页型/槽位时返回 null(调用方回落自身逻辑,不中断)。
44
+ */
45
+ function regionOf(pageType, slotId, opts) {
46
+ const type = String(pageType || 'points');
47
+ const slot = String(slotId || '');
48
+ const { mx, cw } = pageBox();
49
+ const C = LC.pageTypes.common;
50
+ const L = LC.pageTypes.layout;
51
+ const PT = LC.pageTypes[type] || {};
52
+
53
+ // ── chrome:页头 ──
54
+ if (slot === 'head' || slot === 'chrome') {
55
+ return {
56
+ x: mx, y: C.headEyebrowY, w: cw,
57
+ h: Math.max(0.4, (C.headRuleY || 1.95) - C.headEyebrowY),
58
+ titleY: C.headTitleY, ruleY: C.headRuleY, leadY: C.leadY, bodyY: C.bodyY,
59
+ };
60
+ }
61
+
62
+ // ── annotation:so-what / 来源 / note ──
63
+ if (slot === 'annotation' || slot === 'soWhat') {
64
+ const y = (LC.pageTypes.exhibit && LC.pageTypes.exhibit.soWhatY != null)
65
+ ? LC.pageTypes.exhibit.soWhatY : 6.05;
66
+ return { x: mx, y, w: cw, h: 0.62 };
67
+ }
68
+ if (slot === 'footnote') {
69
+ const y = (LC.pageTypes.exhibit && LC.pageTypes.exhibit.footnoteY != null)
70
+ ? LC.pageTypes.exhibit.footnoteY
71
+ : ((LC.pageTypes.note && LC.pageTypes.note.y) || 6.55);
72
+ return { x: mx, y, w: cw - 1.2, h: (LC.pageTypes.note && LC.pageTypes.note.h) || 0.32 };
73
+ }
74
+
75
+ // ── 页型主从区域 ──
76
+ if (type === 'points') {
77
+ const band = contentBand(opts);
78
+ if (slot === 'primary' || slot === 'list') {
79
+ return { x: mx, y: PT.listY || band.top, w: cw, h: Math.max(0.8, band.bottom - (PT.listY || band.top)) };
80
+ }
81
+ if (slot === 'metrics') {
82
+ return { x: mx, y: PT.metricY || 4.7, w: cw, h: Math.max(0.6, L.contentBottom - (PT.metricY || 4.7)) };
83
+ }
84
+ }
85
+
86
+ if (type === 'metrics') {
87
+ if (slot === 'primary') {
88
+ return {
89
+ x: mx, y: PT.bandY || 3.1, w: cw, h: PT.cardH || 2.3,
90
+ valY: PT.valY, valH: PT.valH, capY: PT.capY, capH: PT.capH,
91
+ gap: PT.gap, maxW: PT.maxW,
92
+ };
93
+ }
94
+ }
95
+
96
+ if (type === 'table') {
97
+ if (slot === 'primary') {
98
+ const y = PT.y || L.contentTop;
99
+ return {
100
+ x: mx, y, w: cw,
101
+ h: Math.max(1, ((opts && opts.bottom) || L.contentBottom) - y),
102
+ rowH: PT.rowH, rowHMin: PT.rowHMin, headRowH: PT.headRowH,
103
+ };
104
+ }
105
+ }
106
+
107
+ if (type === 'bar') {
108
+ if (slot === 'primary') {
109
+ const y = PT.chartY || 2.95;
110
+ const bottom = (opts && opts.bottom != null) ? opts.bottom
111
+ : ((opts && opts.withNote) ? L.contentBottomWithNote : L.contentBottom);
112
+ return { x: mx + (PT.chartX || 0), y, w: cw - (PT.chartW || 0), h: Math.max(1.2, bottom - y) };
113
+ }
114
+ if (slot === 'hbar') {
115
+ const y0 = PT.hbarY0 || 2.6;
116
+ const y1 = (opts && opts.bottom != null) ? opts.bottom : (PT.hbarY1 || 6.25);
117
+ return { x: mx, y: y0, w: cw, h: Math.max(1.2, y1 - y0) };
118
+ }
119
+ }
120
+
121
+ if (type === 'donut') {
122
+ if (slot === 'primary') {
123
+ const y = PT.chartY || 2.95;
124
+ const bottom = (opts && opts.bottom != null) ? opts.bottom : L.contentBottom;
125
+ return { x: mx, y, w: cw, h: Math.max(1.2, bottom - y) };
126
+ }
127
+ }
128
+
129
+ if (type === 'exhibit') {
130
+ if (slot === 'primary') {
131
+ const y = PT.chartY || 2.95;
132
+ const h = Math.max(1.2, (L.contentBottomWithNote - y));
133
+ return { x: mx, y, w: cw, h, badgeY: PT.badgeY };
134
+ }
135
+ if (slot === 'secondary') {
136
+ return { x: mx, y: PT.badgeY || L.contentTop, w: cw * 0.32, h: 0.4 };
137
+ }
138
+ }
139
+
140
+ if (type === 'points') {
141
+ const band = contentBand(opts);
142
+ if (slot === 'primary' || slot === 'list') {
143
+ return {
144
+ x: mx, y: PT.listY || band.top, w: cw,
145
+ h: Math.max(0.8, (opts && opts.bottom != null ? opts.bottom : band.bottom) - (PT.listY || band.top)),
146
+ rowH: PT.rowH, rowHMin: PT.rowHMin, markSize: PT.markSize,
147
+ };
148
+ }
149
+ if (slot === 'metrics') {
150
+ return {
151
+ x: mx, y: PT.metricY || 4.7, w: cw,
152
+ h: Math.max(0.6, L.contentBottom - (PT.metricY || 4.7)),
153
+ metricW: PT.metricW, metricValH: PT.metricValH, metricCapY: PT.metricCapY,
154
+ };
155
+ }
156
+ }
157
+
158
+ if (type === 'split') {
159
+ const SP = PT;
160
+ const top = SP.chartY || L.contentTop;
161
+ const bottom = (opts && opts.bottom != null) ? opts.bottom : L.contentBottom;
162
+ if (slot === 'left') {
163
+ return {
164
+ x: mx, y: top, w: cw * (SP.leftW || 0.55),
165
+ h: Math.max(1.2, bottom - top),
166
+ tableY: SP.tableY, tableRowH: SP.tableRowH, rowH: SP.rowH, capY: SP.capY,
167
+ };
168
+ }
169
+ if (slot === 'right') {
170
+ const rx = mx + cw * (SP.rightX != null ? SP.rightX : 0.585);
171
+ return {
172
+ x: rx, y: top, w: cw * (SP.rightW || 0.415),
173
+ h: Math.max(1.2, bottom - top),
174
+ tableY: SP.tableY, tableRowH: SP.tableRowH, rowH: SP.rowH, capY: SP.capY,
175
+ };
176
+ }
177
+ }
178
+
179
+ if (type === 'twocol') {
180
+ const gap = PT.colGap || 0.5;
181
+ const colW = (cw - gap) / 2;
182
+ const top = (opts && opts.top != null) ? opts.top : L.contentTop;
183
+ const bottom = (opts && opts.bottom != null) ? opts.bottom : L.contentBottom;
184
+ if (slot === 'primary' || slot === 'left') {
185
+ return { x: mx, y: top, w: colW, h: Math.max(1, bottom - top), gap, colW };
186
+ }
187
+ if (slot === 'secondary' || slot === 'right') {
188
+ return { x: mx + colW + gap, y: top, w: colW, h: Math.max(1, bottom - top), gap, colW };
189
+ }
190
+ }
191
+
192
+ if (type === 'diagram' || type === 'lane') {
193
+ if (slot === 'primary') {
194
+ const arch = LC.pageTypes.arch || {};
195
+ const y = (opts && opts.mode === 'architecture' && arch.fullStartY != null)
196
+ ? arch.fullStartY
197
+ : (PT.bodyStartY || 2.9);
198
+ const b = (opts && opts.mode === 'architecture' && arch.fullEndY != null)
199
+ ? arch.fullEndY
200
+ : ((opts && opts.bottom != null) ? opts.bottom : L.contentBottom);
201
+ return {
202
+ x: mx, y, w: cw, h: Math.max(1.2, b - y),
203
+ layerBarW: PT.layerBarW, layerGap: PT.layerGap, maxLayerH: PT.maxLayerH,
204
+ laneH: arch.laneH, laneHeadW: arch.laneHeadW, laneGap: arch.laneGap,
205
+ stepGap: arch.stepGap, stepMaxW: arch.stepMaxW,
206
+ };
207
+ }
208
+ }
209
+
210
+ // ── 剩余高频页型 ──
211
+ if (type === 'kpi') {
212
+ if (slot === 'primary' || slot === 'hero') {
213
+ return {
214
+ x: mx, y: PT.heroY || 2.9, w: cw * (PT.heroW || 0.52), h: PT.heroH || 1.6,
215
+ dividerX: PT.dividerX, metricY0: PT.metricY0, metricRowH: PT.metricRowH,
216
+ };
217
+ }
218
+ if (slot === 'metrics') {
219
+ const dx = PT.dividerX != null ? PT.dividerX : 0.56;
220
+ return {
221
+ x: mx + cw * dx + 0.35, y: PT.metricY0 || 2.95,
222
+ w: cw * (1 - dx) - 0.5, h: Math.max(0.6, L.contentBottom - (PT.metricY0 || 2.95)),
223
+ };
224
+ }
225
+ }
226
+
227
+ if (type === 'comparison') {
228
+ const gap = PT.panelGap || 0.4;
229
+ const panelW = (cw - gap) / 2;
230
+ const y = PT.panelY || 2.55;
231
+ const h = PT.panelH || 3.4;
232
+ if (slot === 'primary' || slot === 'left') {
233
+ return { x: mx, y, w: panelW, h, titleH: PT.titleH, rowH: PT.rowH };
234
+ }
235
+ if (slot === 'right' || slot === 'secondary') {
236
+ return { x: mx + panelW + gap, y, w: panelW, h, titleH: PT.titleH, rowH: PT.rowH };
237
+ }
238
+ }
239
+
240
+ if (type === 'quote') {
241
+ if (slot === 'primary') {
242
+ return {
243
+ x: mx + 1.05, y: PT.textY || 2.7, w: cw - 2.1, h: PT.textH || 2.3,
244
+ markY: PT.markY, authorY: PT.authorY, contextY: PT.contextY,
245
+ };
246
+ }
247
+ }
248
+
249
+ if (type === 'heatmap') {
250
+ if (slot === 'primary') {
251
+ const y = PT.y || 2.6;
252
+ const bottom = (opts && opts.bottom != null) ? opts.bottom : L.contentBottom;
253
+ return {
254
+ x: mx, y, w: cw, h: Math.max(1, bottom - y),
255
+ labelW: PT.labelW, headH: PT.headH, cellH: PT.cellH, cellGap: PT.cellGap,
256
+ };
257
+ }
258
+ }
259
+
260
+ if (type === 'bullet') {
261
+ if (slot === 'primary') {
262
+ const y = PT.y || 2.6;
263
+ const bottom = (opts && opts.bottom != null) ? opts.bottom : L.contentBottom;
264
+ return {
265
+ x: mx, y, w: cw, h: Math.max(1, bottom - y),
266
+ rowH: PT.rowH, labelW: PT.labelW, valW: PT.valW, barH: PT.barH,
267
+ };
268
+ }
269
+ }
270
+
271
+ if (type === 'pyramid') {
272
+ if (slot === 'primary') {
273
+ const y = PT.y || 2.35;
274
+ const bottom = (opts && opts.bottom != null) ? opts.bottom : L.contentBottom;
275
+ return {
276
+ x: mx, y, w: cw, h: Math.max(1, bottom - y),
277
+ rowH: PT.rowH, gap: PT.gap, minW: PT.minW, labelW: PT.labelW,
278
+ };
279
+ }
280
+ }
281
+
282
+ if (type === 'steps') {
283
+ if (slot === 'primary') {
284
+ const y = PT.y || 3.05;
285
+ return {
286
+ x: mx, y, w: cw, h: PT.barH || 1.35,
287
+ gap: PT.gap, arrowW: PT.arrowW, numH: PT.numH, titleH: PT.titleH,
288
+ pad: PT.pad, maxPerRow: PT.maxPerRow,
289
+ };
290
+ }
291
+ }
292
+
293
+ if (type === 'cards') {
294
+ if (slot === 'primary') {
295
+ const y = PT.startY || 3.0;
296
+ const bottom = (opts && opts.bottom != null) ? opts.bottom : L.contentBottom;
297
+ return { x: mx, y, w: cw, h: Math.max(1, bottom - y), gap: PT.gap, maxH: PT.maxH, titleH: PT.titleH };
298
+ }
299
+ }
300
+
301
+ if (type === 'timeline') {
302
+ if (slot === 'primary') {
303
+ return {
304
+ x: mx, y: PT.labelY || 2.95, w: cw,
305
+ h: Math.max(1, L.contentBottom - (PT.labelY || 2.95)),
306
+ axisY: PT.axisY, dotY: PT.dotY, dotR: PT.dotR, nameY: PT.nameY,
307
+ descY: PT.descY, descH: PT.descH,
308
+ };
309
+ }
310
+ }
311
+
312
+ if (type === 'image') {
313
+ if (slot === 'primary') {
314
+ const y = Math.max(PT.y || 2.3, (opts && opts.top) || 0);
315
+ const bottom = (opts && opts.bottom != null) ? opts.bottom : L.contentBottom;
316
+ const capH = (opts && opts.caption) ? 0.32 : 0;
317
+ return {
318
+ x: mx, y, w: cw, h: Math.max(1.2, Math.min(PT.h || 4.2, bottom - y - capH)),
319
+ halfW: PT.halfW, halfGap: PT.halfGap, capY: PT.capY, capH: PT.capH || 0.3,
320
+ radius: PT.radius, gridGap: PT.gridGap, gridCols: PT.gridCols,
321
+ };
322
+ }
323
+ }
324
+
325
+ if (type === 'halftable') {
326
+ if (slot === 'primary' || slot === 'left') {
327
+ const r = LC.pageTypes.research || {};
328
+ const top = (opts && opts.top != null) ? opts.top : L.contentTop;
329
+ const bottom = (opts && opts.bottom != null) ? opts.bottom : L.contentBottom;
330
+ return { x: mx, y: top, w: cw * (r.halfTableW || 0.52), h: Math.max(1, bottom - top) };
331
+ }
332
+ if (slot === 'secondary' || slot === 'right') {
333
+ const r = LC.pageTypes.research || {};
334
+ const top = (opts && opts.top != null) ? opts.top : L.contentTop;
335
+ const bottom = (opts && opts.bottom != null) ? opts.bottom : L.contentBottom;
336
+ return {
337
+ x: mx + cw * (r.halfChartX || 0.57), y: top,
338
+ w: cw * (r.halfChartW || 0.43), h: Math.max(1, bottom - top),
339
+ };
340
+ }
341
+ }
342
+
343
+ if (type === 'matrix') {
344
+ if (slot === 'primary') {
345
+ const r = LC.pageTypes.research || {};
346
+ const y = r.matrixY || 2.3;
347
+ const bottom = (opts && opts.bottom != null) ? opts.bottom : L.contentBottom;
348
+ return {
349
+ x: mx, y, w: cw, h: Math.max(1, bottom - y),
350
+ labelW: r.matrixLabelW, cellH: r.matrixCellH,
351
+ };
352
+ }
353
+ }
354
+
355
+ if (type === 'threecol') {
356
+ const r = LC.pageTypes.research || {};
357
+ const gap = r.col3Gap || 0.38;
358
+ const colW = (cw - 2 * gap) / 3;
359
+ const top = (opts && opts.top != null) ? opts.top : L.contentTop;
360
+ const bottom = (opts && opts.bottom != null) ? opts.bottom : L.contentBottom;
361
+ if (slot === 'primary' || slot === 'left') {
362
+ return { x: mx, y: top, w: colW, h: Math.max(1, bottom - top), gap, colW, cols: 3 };
363
+ }
364
+ if (slot === 'middle') {
365
+ return { x: mx + colW + gap, y: top, w: colW, h: Math.max(1, bottom - top), gap, colW, cols: 3 };
366
+ }
367
+ if (slot === 'right' || slot === 'secondary') {
368
+ return { x: mx + 2 * (colW + gap), y: top, w: colW, h: Math.max(1, bottom - top), gap, colW, cols: 3 };
369
+ }
370
+ }
371
+
372
+ // 信息图页型:内容区主图
373
+ if (INFO_TYPES[type] && (slot === 'primary')) {
374
+ const y = PT.y || L.contentTop;
375
+ const h = PT.h || 4.1;
376
+ const bottom = (opts && opts.bottom != null) ? opts.bottom : L.contentBottom;
377
+ return { x: mx, y, w: cw, h: Math.max(1.2, Math.min(h, bottom - y)) };
378
+ }
379
+
380
+ // 默认:内容区全宽
381
+ if (slot === 'primary' || slot === 'secondary' || slot === 'left' || slot === 'right') {
382
+ const band = contentBand(opts);
383
+ return { x: mx, y: band.top, w: cw, h: band.h };
384
+ }
385
+ return null;
386
+ }
387
+
388
+ /** 槽位是否在 LC.layoutSlots 中声明为 required */
389
+ function slotRequired(pageType, slotId) {
390
+ const def = (SLOTS.pageTypes || {})[pageType];
391
+ if (!def) return false;
392
+ const s = (def.slots || []).find(x => x.id === slotId);
393
+ return !!(s && s.required);
394
+ }
395
+
396
+ /** 列出页型已声明槽位 id */
397
+ function slotIds(pageType) {
398
+ const def = (SLOTS.pageTypes || {})[pageType];
399
+ return (def && def.slots) ? def.slots.map(s => s.id) : [];
400
+ }
401
+
402
+ module.exports = {
403
+ LC,
404
+ SLOTS,
405
+ pageBox,
406
+ contentBand,
407
+ regionOf,
408
+ slotRequired,
409
+ slotIds,
410
+ };