@topmindspace/tms-skills 0.3.6 → 0.4.1
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.
- package/CHANGELOG.md +153 -0
- package/README.en.md +12 -9
- package/README.md +12 -9
- package/bin/tms-skills.js +153 -13
- package/package.json +1 -1
- package/top-ppt-html/README.en.md +2 -2
- package/top-ppt-html/README.md +1 -1
- package/top-ppt-html/SKILL.md +8 -7
- package/top-ppt-html/assets/examples/2026-09-09-architecture-graphite-dark.html +58 -184
- package/top-ppt-html/assets/examples/2026-09-09-architecture-graphite-dark.model.json +13 -3
- package/top-ppt-html/assets/examples/2026-09-09-presentation-business-blue.html +58 -337
- package/top-ppt-html/assets/examples/2026-09-09-presentation-business-blue.model.json +15 -1
- package/top-ppt-html/assets/examples/2026-09-09-research-mckinsey.html +58 -930
- package/top-ppt-html/assets/examples/2026-09-09-research-mckinsey.model.json +15 -1
- package/top-ppt-html/assets/examples/2026-09-26-topmind-tms-skills-showcase.html +141 -31
- package/top-ppt-html/assets/icons/index.json +557 -0
- package/top-ppt-html/assets/icons//344/270/212/344/274/240.png +0 -0
- package/top-ppt-html/assets/icons//344/270/212/344/274/240.svg +1 -0
- package/top-ppt-html/assets/icons//344/270/213/350/275/275.png +0 -0
- package/top-ppt-html/assets/icons//344/270/213/350/275/275.svg +1 -0
- package/top-ppt-html/assets/icons//344/270/213/351/231/215.png +0 -0
- package/top-ppt-html/assets/icons//344/270/213/351/231/215.svg +1 -0
- package/top-ppt-html/assets/icons//344/271/246/347/255/276.png +0 -0
- package/top-ppt-html/assets/icons//344/271/246/347/255/276.svg +1 -0
- package/top-ppt-html/assets/icons//344/272/221.png +0 -0
- package/top-ppt-html/assets/icons//344/272/221.svg +1 -0
- package/top-ppt-html/assets/icons//344/275/215/347/275/256.png +0 -0
- package/top-ppt-html/assets/icons//344/275/215/347/275/256.svg +1 -0
- package/top-ppt-html/assets/icons//344/277/241/346/201/257.png +0 -0
- package/top-ppt-html/assets/icons//344/277/241/346/201/257.svg +1 -0
- package/top-ppt-html/assets/icons//345/210/206/344/272/253.png +0 -0
- package/top-ppt-html/assets/icons//345/210/206/344/272/253.svg +1 -0
- package/top-ppt-html/assets/icons//345/213/276/351/200/211.png +0 -0
- package/top-ppt-html/assets/icons//345/213/276/351/200/211.svg +1 -0
- package/top-ppt-html/assets/icons//345/215/240/346/257/224.png +0 -0
- package/top-ppt-html/assets/icons//345/215/240/346/257/224.svg +1 -0
- package/top-ppt-html/assets/icons//345/233/242/351/230/237.png +0 -0
- package/top-ppt-html/assets/icons//345/233/242/351/230/237.svg +1 -0
- package/top-ppt-html/assets/icons//345/233/276/350/241/250.png +0 -0
- package/top-ppt-html/assets/icons//345/233/276/350/241/250.svg +1 -0
- package/top-ppt-html/assets/icons//345/242/236/351/225/277.png +0 -0
- package/top-ppt-html/assets/icons//345/242/236/351/225/277.svg +1 -0
- package/top-ppt-html/assets/icons//345/256/211/345/205/250.png +0 -0
- package/top-ppt-html/assets/icons//345/256/211/345/205/250.svg +1 -0
- package/top-ppt-html/assets/icons//345/257/271/350/257/235.png +0 -0
- package/top-ppt-html/assets/icons//345/257/271/350/257/235.svg +1 -0
- package/top-ppt-html/assets/icons//345/267/245/345/205/267.png +0 -0
- package/top-ppt-html/assets/icons//345/267/245/345/205/267.svg +1 -0
- package/top-ppt-html/assets/icons//345/276/252/347/216/257.png +0 -0
- package/top-ppt-html/assets/icons//345/276/252/347/216/257.svg +1 -0
- package/top-ppt-html/assets/icons//346/210/220/346/236/234.png +0 -0
- package/top-ppt-html/assets/icons//346/210/220/346/236/234.svg +1 -0
- package/top-ppt-html/assets/icons//346/211/213/346/234/272.png +0 -0
- package/top-ppt-html/assets/icons//346/211/213/346/234/272.svg +1 -0
- package/top-ppt-html/assets/icons//346/225/210/347/216/207.png +0 -0
- package/top-ppt-html/assets/icons//346/225/210/347/216/207.svg +1 -0
- package/top-ppt-html/assets/icons//346/225/260/346/215/256.png +0 -0
- package/top-ppt-html/assets/icons//346/225/260/346/215/256.svg +1 -0
- package/top-ppt-html/assets/icons//346/226/207/344/273/266/345/244/271.png +0 -0
- package/top-ppt-html/assets/icons//346/226/207/344/273/266/345/244/271.svg +1 -0
- package/top-ppt-html/assets/icons//346/226/207/346/241/243.png +0 -0
- package/top-ppt-html/assets/icons//346/226/207/346/241/243.svg +1 -0
- package/top-ppt-html/assets/icons//346/227/227/345/270/234.png +0 -0
- package/top-ppt-html/assets/icons//346/227/227/345/270/234.svg +1 -0
- package/top-ppt-html/assets/icons//346/227/266/351/227/264.png +0 -0
- package/top-ppt-html/assets/icons//346/227/266/351/227/264.svg +1 -0
- package/top-ppt-html/assets/icons//346/230/237/346/240/207.png +0 -0
- package/top-ppt-html/assets/icons//346/230/237/346/240/207.svg +1 -0
- package/top-ppt-html/assets/icons//346/231/272/350/203/275.png +0 -0
- package/top-ppt-html/assets/icons//346/231/272/350/203/275.svg +1 -0
- package/top-ppt-html/assets/icons//346/235/203/351/231/220.png +0 -0
- package/top-ppt-html/assets/icons//346/235/203/351/231/220.svg +1 -0
- package/top-ppt-html/assets/icons//346/243/200/346/237/245.png +0 -0
- package/top-ppt-html/assets/icons//346/243/200/346/237/245.svg +1 -0
- package/top-ppt-html/assets/icons//346/264/236/345/257/237.png +0 -0
- package/top-ppt-html/assets/icons//346/264/236/345/257/237.svg +1 -0
- package/top-ppt-html/assets/icons//346/265/201/347/250/213.png +0 -0
- package/top-ppt-html/assets/icons//346/265/201/347/250/213.svg +1 -0
- package/top-ppt-html/assets/icons//346/270/205/345/215/225.png +0 -0
- package/top-ppt-html/assets/icons//346/270/205/345/215/225.svg +1 -0
- package/top-ppt-html/assets/icons//347/201/253/347/256/255.png +0 -0
- package/top-ppt-html/assets/icons//347/201/253/347/256/255.svg +1 -0
- package/top-ppt-html/assets/icons//347/201/257/346/263/241.png +0 -0
- package/top-ppt-html/assets/icons//347/201/257/346/263/241.svg +1 -0
- package/top-ppt-html/assets/icons//347/224/250/346/210/267.png +0 -0
- package/top-ppt-html/assets/icons//347/224/250/346/210/267.svg +1 -0
- package/top-ppt-html/assets/icons//347/233/256/346/240/207.png +0 -0
- package/top-ppt-html/assets/icons//347/233/256/346/240/207.svg +1 -0
- package/top-ppt-html/assets/icons//347/256/255/345/244/264.png +0 -0
- package/top-ppt-html/assets/icons//347/256/255/345/244/264.svg +1 -0
- package/top-ppt-html/assets/icons//347/273/204/347/273/207.png +0 -0
- package/top-ppt-html/assets/icons//347/273/204/347/273/207.svg +1 -0
- package/top-ppt-html/assets/icons//347/275/227/347/233/230.png +0 -0
- package/top-ppt-html/assets/icons//347/275/227/347/233/230.svg +1 -0
- package/top-ppt-html/assets/icons//350/241/250/346/240/274.png +0 -0
- package/top-ppt-html/assets/icons//350/241/250/346/240/274.svg +1 -0
- package/top-ppt-html/assets/icons//350/256/241/345/210/222.png +0 -0
- package/top-ppt-html/assets/icons//350/256/241/345/210/222.svg +1 -0
- package/top-ppt-html/assets/icons//350/256/276/347/275/256.png +0 -0
- package/top-ppt-html/assets/icons//350/256/276/347/275/256.svg +1 -0
- package/top-ppt-html/assets/icons//350/266/213/345/212/277.png +0 -0
- package/top-ppt-html/assets/icons//350/266/213/345/212/277.svg +1 -0
- package/top-ppt-html/assets/icons//351/202/256/344/273/266.png +0 -0
- package/top-ppt-html/assets/icons//351/202/256/344/273/266.svg +1 -0
- package/top-ppt-html/assets/icons//351/207/221/351/222/261.png +0 -0
- package/top-ppt-html/assets/icons//351/207/221/351/222/261.svg +1 -0
- package/top-ppt-html/assets/icons//351/223/276/346/216/245.png +0 -0
- package/top-ppt-html/assets/icons//351/223/276/346/216/245.svg +1 -0
- package/top-ppt-html/assets/icons//351/227/252/347/224/265.png +0 -0
- package/top-ppt-html/assets/icons//351/227/252/347/224/265.svg +1 -0
- package/top-ppt-html/assets/icons//351/243/216/351/231/251.png +0 -0
- package/top-ppt-html/assets/icons//351/243/216/351/231/251.svg +1 -0
- package/top-ppt-html/assets/pptx-export.js +44 -15
- package/top-ppt-html/assets/style-gallery.html +2 -2
- package/top-ppt-html/assets/templates/architecture.html +59 -17
- package/top-ppt-html/assets/templates/engine.css +12 -0
- package/top-ppt-html/assets/templates/presentation.html +59 -17
- package/top-ppt-html/assets/templates/research.html +59 -17
- package/top-ppt-html/package-lock.json +2 -2
- package/top-ppt-html/package.json +2 -2
- package/top-ppt-html/references/icons.md +22 -19
- package/top-ppt-html/references/layout-variants.md +69 -0
- package/top-ppt-html/references/style-pack.md +40 -0
- package/top-ppt-html/scripts/audit_css.py +1 -1
- package/top-ppt-html/scripts/build_pptx.js +228 -50
- package/top-ppt-html/scripts/check_icons.py +119 -0
- package/top-ppt-html/scripts/hydrate_charts.py +60 -5
- package/top-ppt-html/scripts/icon_lib.js +66 -37
- package/top-ppt-html/scripts/layout-constants.json +92 -5
- package/top-ppt-html/scripts/make_icon_pack.py +172 -0
- package/top-ppt-html/scripts/model-schema.json +4 -1
- package/top-ppt-html/scripts/regression.py +3 -3
- package/top-ppt-html/scripts/render_compare.py +1 -1
- package/top-ppt-html/scripts/render_from_model.py +116 -55
- package/top-ppt-html/scripts/scaffold_report.py +1 -1
- package/top-ppt-html/scripts/sync_runtime.py +14 -14
- package/top-ppt-html/scripts/validate_pptx.py +1 -0
- package/top-ppt-html/scripts/validate_report.py +157 -0
- package/topmind-cover/README.en.md +4 -4
- package/topmind-cover/README.md +4 -4
- package/topmind-cover/SKILL.md +36 -16
- package/topmind-cover/assets/examples/README.md +13 -7
- package/topmind-cover/assets/examples/bg-blur-alt.png +0 -0
- package/topmind-cover/assets/examples/bg-blur-wechat.png +0 -0
- package/topmind-cover/assets/examples/bg-blur.png +0 -0
- package/topmind-cover/assets/examples/big-type-wechat.png +0 -0
- package/topmind-cover/assets/examples/big-type.png +0 -0
- package/topmind-cover/assets/examples/brand-launch-wechat.png +0 -0
- package/topmind-cover/assets/examples/brand-launch.png +0 -0
- package/topmind-cover/assets/examples/gan-huo-wechat.png +0 -0
- package/topmind-cover/assets/examples/gan-huo.png +0 -0
- package/topmind-cover/assets/examples/ip-fun-wechat.png +0 -0
- package/topmind-cover/assets/examples/ip-fun.png +0 -0
- package/topmind-cover/assets/examples/magazine-wechat.png +0 -0
- package/topmind-cover/assets/examples/magazine.png +0 -0
- package/topmind-cover/assets/examples/minimal-wechat.png +0 -0
- package/topmind-cover/assets/examples/minimal.png +0 -0
- package/topmind-cover/assets/examples/news-flash-wechat.png +0 -0
- package/topmind-cover/assets/examples/news-flash.png +0 -0
- package/topmind-cover/assets/examples/overview.png +0 -0
- package/topmind-cover/assets/examples/paper-collage-alt.png +0 -0
- package/topmind-cover/assets/examples/paper-collage-wechat.png +0 -0
- package/topmind-cover/assets/examples/paper-collage.png +0 -0
- package/topmind-cover/assets/examples/tutorial-steps-wechat.png +0 -0
- package/topmind-cover/assets/examples/tutorial-steps.png +0 -0
- package/topmind-cover/assets/examples/white-clean-alt-wechat.png +0 -0
- package/topmind-cover/assets/examples/white-clean-alt.png +0 -0
- package/topmind-cover/assets/examples/white-clean-wechat.png +0 -0
- package/topmind-cover/assets/examples/white-clean.png +0 -0
- package/topmind-cover/package.json +2 -2
- package/topmind-cover/references/cover-styles.md +210 -35
- package/topmind-cover/references/impact-language.md +43 -2
- package/topmind-cover/references/ref-white-clean-1.png +0 -0
- package/topmind-cover/references/ref-white-clean-2.png +0 -0
- package/topmind-cover/scripts/crop-cover.py +19 -13
- package/topmind-wechat-post/README.en.md +2 -2
- package/topmind-wechat-post/README.md +2 -2
- package/topmind-wechat-post/SKILL.md +1 -1
- package/topmind-wechat-post/scripts/md2wechat.py +142 -30
- package/topmind-wechat-post/scripts/negative_tests.py +74 -0
- package/topmind-x-article/README.en.md +1 -1
- package/topmind-x-article/README.md +1 -1
- package/topmind-x-article/scripts/md2x.py +25 -5
- package/topmind-x-article/scripts/negative_tests.py +17 -0
|
@@ -159,15 +159,22 @@ function _normColor(c) {
|
|
|
159
159
|
if (/^[0-9a-f]{3}$/.test(v)) v = '#' + v;
|
|
160
160
|
return v;
|
|
161
161
|
}
|
|
162
|
-
/** 取图标 PNG data URI;无缓存时按名挑色兜底(accent → 首色 → null) */
|
|
162
|
+
/** 取图标 PNG data URI;无缓存时按名挑色兜底(accent → 首色 → 包内 512px PNG → null) */
|
|
163
163
|
function iconPng(name, colorHex) {
|
|
164
164
|
const n = name && ICON_ASSETS.icons[name] ? name : ICON_LIB.pickIconName(name, 0);
|
|
165
165
|
const bag = ICON_ASSETS.icons[n];
|
|
166
|
-
if (
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
166
|
+
if (bag) {
|
|
167
|
+
const want = _normColor(colorHex);
|
|
168
|
+
if (bag[want]) return bag[want];
|
|
169
|
+
const keys = Object.keys(bag);
|
|
170
|
+
if (keys.length) return bag[keys[0]];
|
|
171
|
+
}
|
|
172
|
+
// 无 sharp / 无预生成资产时的可靠回落:包内 512px 预渲染 PNG(python-pptx/pptxgenjs 均可直接嵌入)
|
|
173
|
+
try {
|
|
174
|
+
const p = ICON_LIB.iconPngPath(n);
|
|
175
|
+
if (p) return 'data:image/png;base64,' + fs.readFileSync(p).toString('base64');
|
|
176
|
+
} catch (e) { /* 继续回落 accent 方块 */ }
|
|
177
|
+
return null;
|
|
171
178
|
}
|
|
172
179
|
/** 在指定盒内嵌图标(真导出);无图标资产时回落 accent 小方块(可扫读路标) */
|
|
173
180
|
function addIcon(s, name, x, y, size, colorHex) {
|
|
@@ -275,6 +282,30 @@ const FS_POLICY = (function () {
|
|
|
275
282
|
}
|
|
276
283
|
})();
|
|
277
284
|
const FONT_LADDER = FS_POLICY.ladder;
|
|
285
|
+
/* 弹性文本单源(containers.adaptiveText · 0.2.0):长标题/长列表/长表格单元格的降级规则。
|
|
286
|
+
调用处只读 AT_* 与下列 helper,禁止手写字号/行距/阈值 magic number。 */
|
|
287
|
+
const AT = (LC.containers && LC.containers.adaptiveText) || {};
|
|
288
|
+
const AT_TITLE = AT.title || {}, AT_LIST = AT.list || {}, AT_TABLE = AT.table || {};
|
|
289
|
+
/* 标题字号上限:按字符数(CJK=1,拉丁/数字≈0.5,与 estLines 同口径)分档 */
|
|
290
|
+
function titleMaxFor(text) {
|
|
291
|
+
const s = String(text == null ? '' : text);
|
|
292
|
+
let units = 0;
|
|
293
|
+
for (const ch of s) units += /[\x00-\xff]/.test(ch) ? 0.5 : 1;
|
|
294
|
+
if (units >= (AT_TITLE.xlongChars != null ? AT_TITLE.xlongChars : 44)) return AT_TITLE.xlongMax || 20;
|
|
295
|
+
if (units >= (AT_TITLE.longChars != null ? AT_TITLE.longChars : 28)) return AT_TITLE.longMax || 24;
|
|
296
|
+
return AT_TITLE.base || 30;
|
|
297
|
+
}
|
|
298
|
+
/* 列表密集度:条目数达标 → 收紧字号上限/段间距/行距 */
|
|
299
|
+
function listFitOpts(n) {
|
|
300
|
+
const dense = n >= (AT_LIST.denseItems != null ? AT_LIST.denseItems : 6);
|
|
301
|
+
return {
|
|
302
|
+
dense,
|
|
303
|
+
max: dense ? (AT_LIST.denseMax || 12) : (AT_LIST.max || 14),
|
|
304
|
+
gapFactor: dense ? (AT_LIST.denseGapFactor != null ? AT_LIST.denseGapFactor : 0.25)
|
|
305
|
+
: (AT_LIST.gapFactor != null ? AT_LIST.gapFactor : 0.5),
|
|
306
|
+
lineFactor: dense ? (AT_LIST.denseLineFactor || 1.3) : (AT_LIST.lineFactor || 1.45),
|
|
307
|
+
};
|
|
308
|
+
}
|
|
278
309
|
/* 缩字号触底记录:触底 = 内容量超出该版式承载力,是「该拆页/换组合」的信号,不得无声无息 */
|
|
279
310
|
const SHRINK_FLOOR_HITS = [];
|
|
280
311
|
function modeFloorPt() {
|
|
@@ -335,6 +366,13 @@ function fitRowH(avail, n, pref, floor) {
|
|
|
335
366
|
return Math.max(lo, Math.min(pref, avail / Math.max(1, n)));
|
|
336
367
|
}
|
|
337
368
|
|
|
369
|
+
/* Exhibit 徽标文本:纯编号补 "Exhibit " 前缀(HTML 为 `Exhibit {no}` + CSS uppercase,视觉同 "EXHIBIT 1");
|
|
370
|
+
调用方已带前缀的不重复 */
|
|
371
|
+
function exhibitBadge(no) {
|
|
372
|
+
const t = String(no == null ? '' : no).trim();
|
|
373
|
+
if (!t) return t;
|
|
374
|
+
return /^exhibit[\s\-:]*/i.test(t) ? t.toUpperCase() : ('EXHIBIT ' + t.toUpperCase());
|
|
375
|
+
}
|
|
338
376
|
/* ══════════ 内容模型 ══════════
|
|
339
377
|
* 从 --model=<file.json> 读取(推荐:由 extract_model.py 从 HTML 报告抽取,
|
|
340
378
|
* 保证 PPTX 与页面同源);未提供时使用下方内嵌 CONTENT 示例。 */
|
|
@@ -463,9 +501,10 @@ function head(s, eyebrow, title, lead) {
|
|
|
463
501
|
};
|
|
464
502
|
if (eyebrow) s.addText(eyebrow, { x: H.x, y: H.y, w: H.w, h: 0.4, fontFace: STYLE.font,
|
|
465
503
|
fontSize: sz(13), bold: true, color: STYLE.accent, charSpacing: 2 });
|
|
466
|
-
/*
|
|
504
|
+
/* 主标题自适应(adaptiveText.title 单源):先按字符数分档定上限,再按高度验算降一档,避免压到分隔线 */
|
|
467
505
|
const titleH = eyebrow ? (H.ruleY - H.titleY - 0.06) : 1.6 - 0.6;
|
|
468
|
-
const
|
|
506
|
+
const tMax = titleMaxFor(title);
|
|
507
|
+
const tSize = sz(estTextH(title, CW, sz(tMax), 1.25) > titleH ? Math.min(tMax, AT_TITLE.step1 || 24) : tMax);
|
|
469
508
|
s.addText(title, { x: H.x, y: eyebrow ? H.titleY : 0.6, w: H.w, h: Math.max(0.6, titleH),
|
|
470
509
|
fontFace: STYLE.fontDisplay, fontSize: tSize, bold: true, color: STYLE.ink, fit: 'shrink' });
|
|
471
510
|
s.addShape('rect', { x: H.x, y: eyebrow ? H.ruleY : 1.6, w: 0.9, h: 0.045, fill: { color: STYLE.accent }, line: { type: 'none' } });
|
|
@@ -508,7 +547,7 @@ function isRec(v) { return !!v && typeof v === 'object' && !Array.isArray(v); }
|
|
|
508
547
|
pointKV: [k,v] | {k|t, v|d} | str(对齐 pts_list)
|
|
509
548
|
metricKV: [v,k] | {v|value, k}(对齐 metrics_row)
|
|
510
549
|
bulletKV: [k,a,t] | {k,a,t}(对齐 r_bullet;缺省保持 undefined 以便调用方判空)
|
|
511
|
-
phaseKV: [label,name,desc,state] | {label,name,d,s}(对齐 r_timeline
|
|
550
|
+
phaseKV: [label,name,desc,state,icon?] | {label,name,d,s,icon?}(对齐 r_timeline;icon 为变体 timeline 节点图标)
|
|
512
551
|
paraKV: [title,text] | str(对齐 r_twocol;str 为无加粗标题的正文段) */
|
|
513
552
|
function pointKV(p) {
|
|
514
553
|
if (Array.isArray(p)) return [String(p[0] == null ? '' : p[0]), String(p[1] == null ? '' : p[1])];
|
|
@@ -526,9 +565,10 @@ function bulletKV(it) {
|
|
|
526
565
|
return [String(it == null ? '' : it), undefined, undefined];
|
|
527
566
|
}
|
|
528
567
|
function phaseKV(ph) {
|
|
529
|
-
|
|
530
|
-
if (
|
|
531
|
-
|
|
568
|
+
// 第 5 元 = icon(变体 timeline 的节点图标;缺省 '',渲染回落圆点)
|
|
569
|
+
if (Array.isArray(ph)) return [ph[0] || '', ph[1] || '', ph[2] || '', ph[3] || '', ph[4] || ''];
|
|
570
|
+
if (isRec(ph)) return [ph.label || '', ph.name || '', ph.d || '', ph.s || '', ph.icon || ''];
|
|
571
|
+
return [String(ph == null ? '' : ph), '', '', '', ''];
|
|
532
572
|
}
|
|
533
573
|
function paraKV(p) {
|
|
534
574
|
if (Array.isArray(p)) return [p[0] || '', p[1] || ''];
|
|
@@ -835,13 +875,30 @@ function dataTableRows(c) {
|
|
|
835
875
|
function dataTableText(c) { return dataTableRows(c).map(r => r.join(' | ')).join('\n'); }
|
|
836
876
|
|
|
837
877
|
/* 原生图表基础选项(位置由调用点附加) */
|
|
878
|
+
/* 数据标签小数位:按模型数值实际小数位(上限 2 位)生成 format code。
|
|
879
|
+
原 '0"单位"' 会把 1.2 显示成 1(像素级数值失真);无单位且无小数时保持 General。 */
|
|
880
|
+
function chartDecimals(c) {
|
|
881
|
+
let d = 0;
|
|
882
|
+
const scan = (v) => {
|
|
883
|
+
if (typeof v === 'number' && isFinite(v)) {
|
|
884
|
+
const s = String(v), i = s.indexOf('.');
|
|
885
|
+
if (i >= 0) d = Math.max(d, Math.min(2, s.length - i - 1));
|
|
886
|
+
}
|
|
887
|
+
};
|
|
888
|
+
(c.values || []).forEach(scan);
|
|
889
|
+
(c.series || []).forEach(se => (se.values || []).forEach(scan));
|
|
890
|
+
(c.points || []).forEach(p => { if (Array.isArray(p)) p.forEach(scan); else scan(p); });
|
|
891
|
+
(c.start || []).forEach(scan); (c.target || []).forEach(scan);
|
|
892
|
+
return d;
|
|
893
|
+
}
|
|
838
894
|
function chartBaseOpts(c, colors, multi, kind) {
|
|
839
895
|
const isLine = (kind === 'line' || kind === 'area');
|
|
896
|
+
const _dec = chartDecimals(c), _ds = _dec ? '.' + '0'.repeat(_dec) : '';
|
|
840
897
|
const opts = {
|
|
841
898
|
showValue: !isLine && !multi,
|
|
842
899
|
dataLabelPosition: 'outEnd',
|
|
843
900
|
dataLabelColor: STYLE.body, dataLabelFontSize: sz(11.5), dataLabelFontFace: STYLE.font,
|
|
844
|
-
dataLabelFormatCode: c.unit ? '0"' + c.unit + '"' : 'General',
|
|
901
|
+
dataLabelFormatCode: c.unit ? '0' + _ds + '"' + c.unit + '"' : (_dec ? '0' + _ds : 'General'),
|
|
845
902
|
catAxisLabelColor: STYLE.faint, catAxisLabelFontSize: sz(10.5), catAxisLabelFontFace: STYLE.font,
|
|
846
903
|
valAxisHidden: true, valAxisLineShow: false, catAxisLineShow: false,
|
|
847
904
|
catGridLine: { style: 'none' }, valGridLine: { style: 'none' },
|
|
@@ -919,7 +976,19 @@ function nativeChart(s, c, x, y, w, h, dcols) {
|
|
|
919
976
|
}
|
|
920
977
|
const ctype = kind === 'line' ? pptx.ChartType.line
|
|
921
978
|
: kind === 'area' ? pptx.ChartType.area : pptx.ChartType.bar;
|
|
922
|
-
|
|
979
|
+
let cdata = chartSeries(c);
|
|
980
|
+
if (t === 'hbar') {
|
|
981
|
+
/* 条形图(barDir=bar)类目轴自下而上绘制:反转数据使首项居顶,
|
|
982
|
+
与 HTML svg-hbar 及 A 通道 hbarRows 同序(标签-数值配对不变) */
|
|
983
|
+
cdata = cdata.map(se => ({ name: se.name,
|
|
984
|
+
labels: (se.labels || []).slice().reverse(), values: (se.values || []).slice().reverse() }));
|
|
985
|
+
/* 单系列时颜色按数据点分配:颜色跟随数据走,与 A 通道 dcols[j%5] 同归属 */
|
|
986
|
+
if (!multi && colors && colors.length) {
|
|
987
|
+
const _n = Math.max(1, (cdata[0].values || []).length);
|
|
988
|
+
opts.chartColors = (cdata[0].values || []).map((_, i) => colors[(_n - 1 - i) % colors.length]);
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
chartTry(s, ctype, cdata, opts, c);
|
|
923
992
|
}
|
|
924
993
|
/* 散点(X/Y 双系列)/ 气泡({x,y,w} 单系列);labels 解析为 x,values 为 y */
|
|
925
994
|
function xyChart(s, c, pos, kind, colors) {
|
|
@@ -1440,10 +1509,14 @@ function ribbonRects(s, pts, color) {
|
|
|
1440
1509
|
const ca = (a[1] + a[2]) / 2, cb = (b[1] + b[2]) / 2;
|
|
1441
1510
|
const dx = b[0] - a[0], dy = cb - ca;
|
|
1442
1511
|
const len = Math.sqrt(dx * dx + dy * dy);
|
|
1443
|
-
|
|
1512
|
+
/* 厚度取绝对值:sankey 按 [x,上,下]、streamgraph 按 [x,下,上] 传参(y 向下),
|
|
1513
|
+
符号不应影响厚度(原 (a[2]-a[1]) 在 streamgraph 为负 → th 恒 0.02,流带变细线) */
|
|
1514
|
+
const th = Math.max(0.02, Math.abs(((a[1] - a[2]) + (b[1] - b[2])) / 2));
|
|
1444
1515
|
const ang = Math.atan2(dy, dx) * 180 / Math.PI;
|
|
1445
|
-
|
|
1446
|
-
|
|
1516
|
+
/* 相邻 quad 沿走向微叠 0.02in:消除拼接处的抗锯齿发丝缝(streamgraph/sankey 像素级瑕疵) */
|
|
1517
|
+
const ov = 0.02;
|
|
1518
|
+
s.addShape('rect', { x: (a[0] + b[0]) / 2 - len / 2 - ov, y: (ca + cb) / 2 - th / 2,
|
|
1519
|
+
w: Math.max(0.02, len) + ov * 2, h: th, rotate: Math.round(ang * 10) / 10,
|
|
1447
1520
|
fill: { color: color }, line: { type: 'none' } });
|
|
1448
1521
|
}
|
|
1449
1522
|
}
|
|
@@ -1461,7 +1534,8 @@ function infoTableRows(sec) {
|
|
|
1461
1534
|
if (t === 'network') return [['节点', '名称']].concat((sec.nodes || []).map(n => [String(n[0]), String(n[1])]));
|
|
1462
1535
|
if (t === 'marimekko') {
|
|
1463
1536
|
const head = ['列', '总量' + u].concat((sec.legend || []).map(String));
|
|
1464
|
-
return [head].concat((sec.cols || []).map((c, i) => [String(c[0]), String(c[1])]
|
|
1537
|
+
return [head].concat((sec.cols || []).map((c, i) => [String(c[0]), String(c[1])]
|
|
1538
|
+
.concat(marimekkoSegs((sec.cells || [])[i], 99).map(sg => sg.label != null ? sg.label + ' ' + sg.value : String(sg.value)))));
|
|
1465
1539
|
}
|
|
1466
1540
|
if (t === 'streamgraph') {
|
|
1467
1541
|
return [['系列'].concat((sec.labels || []).map(String))]
|
|
@@ -1627,7 +1701,11 @@ function infoBoxplot(s, sec, y0, y1) {
|
|
|
1627
1701
|
fontFace: STYLE.font, fontSize: sz(10.5), color: STYLE.body });
|
|
1628
1702
|
s.addText(String(md) + (sec.unit || ''), { x: cx - step / 2, y: yFor(md) - 0.42, w: step, h: 0.26, align: 'center',
|
|
1629
1703
|
fontFace: STYLE.font, fontSize: sz(10), bold: true, color: STYLE.accent });
|
|
1630
|
-
|
|
1704
|
+
/* min 标签默认在须线帽下方;mn 贴轴时会被组标签带(P.y1-0.42 起)压住 →
|
|
1705
|
+
翻到帽上方,同时避开箱体下沿(几何铁律②:禁叠印) */
|
|
1706
|
+
let mnY = yFor(mn) + 0.03;
|
|
1707
|
+
if (mnY + 0.24 > P.y1 - 0.42) mnY = Math.min(yFor(mn) - 0.28, yFor(q1) - 0.30);
|
|
1708
|
+
s.addText(String(mn) + (sec.unit || ''), { x: cx - step / 2, y: mnY, w: step, h: 0.24, align: 'center',
|
|
1631
1709
|
fontFace: STYLE.font, fontSize: sz(9.5), color: STYLE.faint });
|
|
1632
1710
|
s.addText(String(mx) + (sec.unit || ''), { x: cx - step / 2, y: yFor(mx) - 0.28, w: step, h: 0.24, align: 'center',
|
|
1633
1711
|
fontFace: STYLE.font, fontSize: sz(9.5), color: STYLE.faint });
|
|
@@ -1670,6 +1748,15 @@ function infoNetwork(s, sec, y0, y1) {
|
|
|
1670
1748
|
align: outward ? 'left' : 'right', valign: 'middle', fontFace: STYLE.font, fontSize: sz(10), color: STYLE.body });
|
|
1671
1749
|
});
|
|
1672
1750
|
}
|
|
1751
|
+
/* marimekko 单元格归一化:裸数值 [70,25,5] 或 [标签,数值] 对 [["<50ms",70],…]
|
|
1752
|
+
(与 cols/flows/groups 的 [标签,值] 惯例一致);非数值 → 0,零值段过滤 */
|
|
1753
|
+
function marimekkoSegs(arr, maxSegs) {
|
|
1754
|
+
return ((arr || []).slice(0, maxSegs == null ? 99 : maxSegs)).map(el => {
|
|
1755
|
+
const pair = Array.isArray(el);
|
|
1756
|
+
const v = Number(pair ? el[1] : el);
|
|
1757
|
+
return { label: pair ? String(el[0]) : null, value: isFinite(v) ? v : 0 };
|
|
1758
|
+
}).filter(sg => sg.value > 0);
|
|
1759
|
+
}
|
|
1673
1760
|
/* 马赛克图:列宽按列总量、列高按 100% 构成的双重编码 */
|
|
1674
1761
|
function infoMarimekko(s, sec, y0, y1) {
|
|
1675
1762
|
const G = PT.marimekko;
|
|
@@ -1690,14 +1777,17 @@ function infoMarimekko(s, sec, y0, y1) {
|
|
|
1690
1777
|
let cx0 = MX;
|
|
1691
1778
|
cols.forEach((c, i) => {
|
|
1692
1779
|
const cw = Math.max(0.2, (Number(c[1]) || 0) / total * availW);
|
|
1693
|
-
const
|
|
1694
|
-
const sum =
|
|
1780
|
+
const segs = marimekkoSegs(cells[i], G.maxSegs);
|
|
1781
|
+
const sum = segs.reduce((a, sg) => a + sg.value, 0) || 1;
|
|
1695
1782
|
let cy0 = P.y0;
|
|
1696
|
-
|
|
1783
|
+
segs.forEach((sg, k) => {
|
|
1784
|
+
const v = sg.value;
|
|
1697
1785
|
const sh = Math.max(0.02, v / sum * plotH);
|
|
1698
1786
|
s.addShape('rect', { x: cx0, y: cy0, w: cw, h: sh,
|
|
1699
1787
|
fill: { color: palette[k % palette.length] }, line: { color: STYLE.bg, width: 0.75 } });
|
|
1700
|
-
|
|
1788
|
+
/* 有标签用标签("<50ms"),裸数值沿用旧行为(值 + 单位) */
|
|
1789
|
+
const segLabel = sg.label != null ? sg.label : (String(v) + (sec.unit || ''));
|
|
1790
|
+
s.addText(segLabel, { x: cx0, y: cy0 + sh / 2 - 0.13, w: cw, h: 0.26, align: 'center',
|
|
1701
1791
|
fontFace: STYLE.font, fontSize: sz(10), bold: true, fit: 'shrink',
|
|
1702
1792
|
color: k === 0 ? STYLE.onAccent : STYLE.ink });
|
|
1703
1793
|
cy0 += sh;
|
|
@@ -1779,7 +1869,7 @@ function addTable(s, tbl, x, y, w, availH, opts) {
|
|
|
1779
1869
|
const minRowH = opts.minRowH != null ? opts.minRowH : PT.table.rowHMin;
|
|
1780
1870
|
const headRowH = opts.headRowH != null ? opts.headRowH : (PT.table.headRowH || minRowH);
|
|
1781
1871
|
/* 行数超过容量时优先压行高到 hardFloor,仍放不下则整体缩放到 availH(禁溢出) */
|
|
1782
|
-
const hardFloor = Math.min(minRowH, 0.22);
|
|
1872
|
+
const hardFloor = Math.min(minRowH, AT_TABLE.rowFloorIn != null ? AT_TABLE.rowFloorIn : 0.22);
|
|
1783
1873
|
const ideal = availH / Math.max(1, n);
|
|
1784
1874
|
let rowH = Math.min(maxRowH, Math.max(hardFloor, ideal));
|
|
1785
1875
|
if (n * rowH > availH + 0.01) {
|
|
@@ -1790,11 +1880,46 @@ function addTable(s, tbl, x, y, w, availH, opts) {
|
|
|
1790
1880
|
}
|
|
1791
1881
|
/* 行多则同步收字号(research 密排允许更小;句子单元格仍走语义字号门禁) */
|
|
1792
1882
|
const dense = rowH < 0.42;
|
|
1793
|
-
const headSz = sz(dense ? 11 : 13
|
|
1883
|
+
const headSz = sz(dense ? 11 : Math.min(13, AT_TABLE.headMax || 12));
|
|
1884
|
+
const bodySz = sz(dense ? 10 : Math.min(12.5, AT_TABLE.cellMax || 13));
|
|
1794
1885
|
const hRowH = Math.min(headRowH, Math.max(hardFloor, rowH * 1.15));
|
|
1886
|
+
/* 内容高度预估:逐格用 estTextH 估算,超出行高则全表收字号 ——
|
|
1887
|
+
禁依赖 fit:'shrink' 或渲染引擎自动撑高(PowerPoint/LibreOffice 会按内容增高行,压住脚注) */
|
|
1888
|
+
const ncolsT = Math.max(head.length, 1, ...rowsIn.map(r => r.length));
|
|
1889
|
+
const effColW = (Array.isArray(tbl.colW) && tbl.colW.length >= ncolsT) ? tbl.colW.slice(0, ncolsT)
|
|
1890
|
+
: Array.from({ length: ncolsT }, () => w / ncolsT);
|
|
1891
|
+
const cellPadIn = (dense ? 2 : 5) / 72;
|
|
1892
|
+
/* 长表格单元格适配(adaptiveText.table 单源):逐格估算,超出行高则全表收字号;
|
|
1893
|
+
floor 取模式下限与表格下限之大者——表格不得突破模式可读性底线 */
|
|
1894
|
+
const fitCellSz = (text, cw, availHh, maxSz) => fitFont([String(text == null ? '' : text)],
|
|
1895
|
+
Math.max(0.5, cw - cellPadIn * 2), Math.max(0.2, availHh - cellPadIn * 2),
|
|
1896
|
+
{ max: maxSz, maxShrinkSteps: 6, floor: Math.max(modeFloorPt(), AT_TABLE.cellFloor || 0) });
|
|
1897
|
+
let bodySzF = bodySz, headSzF = headSz;
|
|
1898
|
+
rowsIn.forEach(r => r.forEach((c, j) => {
|
|
1899
|
+
const f = fitCellSz(c, effColW[j] != null ? effColW[j] : effColW[0], rowH, bodySz);
|
|
1900
|
+
if (f < bodySzF) bodySzF = f;
|
|
1901
|
+
}));
|
|
1902
|
+
head.forEach((h, j) => {
|
|
1903
|
+
const f = fitCellSz(h, effColW[j] != null ? effColW[j] : effColW[0], hRowH, headSz);
|
|
1904
|
+
if (f < headSzF) headSzF = f;
|
|
1905
|
+
});
|
|
1906
|
+
/* 字号触底仍装不下 → 显式告警(内容超承载,建议拆页/精简;行高不再自动撑高) */
|
|
1907
|
+
const stillOver = (text, cw, availHh, f) =>
|
|
1908
|
+
estTextH(String(text == null ? '' : text), Math.max(0.5, cw - cellPadIn * 2), f, TM_LINE) >
|
|
1909
|
+
Math.max(0.2, availHh - cellPadIn * 2);
|
|
1910
|
+
let overCells = 0;
|
|
1911
|
+
rowsIn.forEach(r => r.forEach((c, j) => {
|
|
1912
|
+
if (stillOver(c, effColW[j] != null ? effColW[j] : effColW[0], rowH, bodySzF)) overCells++;
|
|
1913
|
+
}));
|
|
1914
|
+
head.forEach((h, j) => {
|
|
1915
|
+
if (stillOver(h, effColW[j] != null ? effColW[j] : effColW[0], hRowH, headSzF)) overCells++;
|
|
1916
|
+
});
|
|
1917
|
+
if (overCells > 0 && typeof console !== 'undefined')
|
|
1918
|
+
console.warn(`addTable: ${overCells} 个单元格在字号下限(正文 ${bodySzF}pt/表头 ${headSzF}pt)仍超出分配行高 —— ` +
|
|
1919
|
+
'内容超承载,建议拆页/精简文案(行高已锁定,防压脚注)');
|
|
1795
1920
|
const rows = [head.map(h => ({ text: h, options: { bold: true, color: STYLE.onAccent,
|
|
1796
|
-
fill: { color: STYLE.accent }, fontSize:
|
|
1797
|
-
.concat(rowsIn.map(r => r.map(c => ({ text: String(c), options: { fontSize:
|
|
1921
|
+
fill: { color: STYLE.accent }, fontSize: headSzF, fontFace: STYLE.font, valign: 'middle' } }))]
|
|
1922
|
+
.concat(rowsIn.map(r => r.map(c => ({ text: String(c), options: { fontSize: bodySzF,
|
|
1798
1923
|
color: STYLE.body, fontFace: STYLE.font, valign: 'middle', fill: { color: STYLE.bg } } }))));
|
|
1799
1924
|
/* rowH 数组:表头略高、正文均分剩余(pptxgenjs 支持逐行 rowH) */
|
|
1800
1925
|
const rowHeights = [hRowH].concat(rowsIn.map(() => rowH));
|
|
@@ -1885,7 +2010,7 @@ CONTENT.sections.forEach((sec) => {
|
|
|
1885
2010
|
|
|
1886
2011
|
/* 正文区起点 / 终点(含 lead 与 so-what / 来源行 / 待核实条占位) */
|
|
1887
2012
|
/* 非 exhibit 页型也可带 Exhibit 编号(帧头徽标;正文区下移让位,避免与主图重叠) */
|
|
1888
|
-
const exBadge = (type !== 'exhibit' && sec.exhibitNo) ?
|
|
2013
|
+
const exBadge = (type !== 'exhibit' && sec.exhibitNo) ? exhibitBadge(sec.exhibitNo) : null;
|
|
1889
2014
|
const bodyY0 = sec.lead ? PT.common.bodyYWithLead : CONTENT_TOP;
|
|
1890
2015
|
const bodyY = exBadge ? bodyY0 + 0.42 : bodyY0;
|
|
1891
2016
|
if (exBadge) {
|
|
@@ -1896,7 +2021,7 @@ CONTENT.sections.forEach((sec) => {
|
|
|
1896
2021
|
const _F = PT.flagbar || { hdH: 0.26, rowH: 0.24, maxRows: 3, gap: 0.12 };
|
|
1897
2022
|
const flagH = flagItems.length
|
|
1898
2023
|
? (_F.hdH + Math.min(_F.maxRows, flagItems.length) * _F.rowH + 0.06) : 0;
|
|
1899
|
-
let bodyBottom = chartBottom(!!sec.soWhat, !!sec.footnote);
|
|
2024
|
+
let bodyBottom = chartBottom(!!sec.soWhat, !!(sec.footnote || sec.note));
|
|
1900
2025
|
let flagY = 0;
|
|
1901
2026
|
if (flagH) {
|
|
1902
2027
|
/* 待核实条底对齐 contentBottom;与来源行 / so-what 同页时上移让位(不重叠) */
|
|
@@ -1943,7 +2068,7 @@ CONTENT.sections.forEach((sec) => {
|
|
|
1943
2068
|
/* R6:竖分隔线高度取原设计值与注释带感知下界的较小者——写死 3.1 会在
|
|
1944
2069
|
so-what/来源行并存时越过注释带顶(ANNOTATION_BAND_OVERLAP),而无注释时
|
|
1945
2070
|
不得比原设计更长(回归:smoke business-blue fixture slide 7)。 */
|
|
1946
|
-
const kpiBottom = chartBottom(!!sec.soWhat, !!sec.footnote) - 0.05;
|
|
2071
|
+
const kpiBottom = chartBottom(!!sec.soWhat, !!(sec.footnote || sec.note)) - 0.05;
|
|
1947
2072
|
s.addShape('rect', { x: kdivX, y: K.heroY + 0.15, w: 0.025,
|
|
1948
2073
|
h: Math.min(3.1, Math.max(0.6, kpiBottom - (K.heroY + 0.15))),
|
|
1949
2074
|
fill: { color: STYLE.line }, line: { type: 'none' } });
|
|
@@ -1952,9 +2077,15 @@ CONTENT.sections.forEach((sec) => {
|
|
|
1952
2077
|
{ x: kdivX + 0.35, y: K.metricY0, w: CW * (1 - K.dividerX) - 0.5, h: CONTENT_BOTTOM - K.metricY0 };
|
|
1953
2078
|
const kAvail = Math.max(0.8, kpiBottom - mreg.y);
|
|
1954
2079
|
const kmets = sec.metrics || [];
|
|
1955
|
-
|
|
2080
|
+
/* 容量按最小允许行高判断,行高按全部指标自适应压缩 ——
|
|
2081
|
+
原 kCap 用默认行高先截断,导致第 N+1 个指标被静默丢弃(禁静默丢数据) */
|
|
2082
|
+
const K_MINROWH = 0.42;
|
|
2083
|
+
const kCap = Math.max(1, Math.floor(kAvail / K_MINROWH));
|
|
1956
2084
|
const kRows = Math.min(kmets.length, kCap);
|
|
1957
|
-
|
|
2085
|
+
if (kRows < kmets.length)
|
|
2086
|
+
console.warn(`[build_pptx] 警告:KPI 页「${sec.title || ''}」支撑指标 ${kmets.length} 个超出容量(${kAvail.toFixed(2)}in),` +
|
|
2087
|
+
`仅渲染前 ${kRows} 个 —— 建议拆页`);
|
|
2088
|
+
const kRowH = fitRowH(kAvail, kRows, K.metricRowH, K_MINROWH);
|
|
1958
2089
|
kmets.slice(0, kRows).map(metricKV).forEach((m, i) => {
|
|
1959
2090
|
const ky = mreg.y + i * kRowH;
|
|
1960
2091
|
s.addText(m[0], { x: mreg.x, y: ky, w: mreg.w, h: Math.min(0.55, kRowH * 0.62),
|
|
@@ -1963,11 +2094,13 @@ CONTENT.sections.forEach((sec) => {
|
|
|
1963
2094
|
h: Math.max(0.24, kRowH - Math.min(0.55, kRowH * 0.62)), fontFace: STYLE.font,
|
|
1964
2095
|
fontSize: sz(10.5), color: STYLE.faint });
|
|
1965
2096
|
});
|
|
1966
|
-
/* 要点:指标行下方续排,k/v 双色(与 bar
|
|
2097
|
+
/* 要点:指标行下方续排,k/v 双色(与 bar 侧栏同口径);容量不足时显式告警(禁静默丢数据) */
|
|
1967
2098
|
const kpts = (sec.points || []).filter(Boolean);
|
|
1968
2099
|
const kpRowH = 0.5;
|
|
1969
2100
|
const kpY0 = mreg.y + kRows * kRowH + 0.12;
|
|
1970
2101
|
const kpCap = Math.max(0, Math.floor((kpiBottom - kpY0) / kpRowH));
|
|
2102
|
+
if (kpts.length > kpCap)
|
|
2103
|
+
console.warn(`[build_pptx] 警告:KPI 页「${sec.title || ''}」要点 ${kpts.length} 条超出容量,仅渲染前 ${kpCap} 条 —— 建议拆页`);
|
|
1971
2104
|
kpts.slice(0, kpCap).forEach((p, i) => {
|
|
1972
2105
|
const kv = pointKV(p), kk = kv[0], vv = kv[1];
|
|
1973
2106
|
const ky = kpY0 + i * kpRowH;
|
|
@@ -2225,10 +2358,24 @@ CONTENT.sections.forEach((sec) => {
|
|
|
2225
2358
|
s.addShape('roundRect', { x, y, w, h: rowH, rectRadius: 0.07,
|
|
2226
2359
|
fill: { color: acc ? STYLE.soft : STYLE.surface },
|
|
2227
2360
|
line: acc ? { type: 'none' } : { color: STYLE.line, width: 0.75 } });
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2361
|
+
/* 标题列宽按层内宽比例分配(labelFrac≈0.3);顶层窄时固定 3.2in 会把说明压成竖列叠印 →
|
|
2362
|
+
改为标题上、说明下的单框混排(与 HTML .pyr__t/.pyr__d 同语义) */
|
|
2363
|
+
const lwP = Math.min(P.labelW, (w - 0.44) * (P.labelFrac == null ? 0.32 : P.labelFrac));
|
|
2364
|
+
const descW = w - 0.44 - lwP;
|
|
2365
|
+
/* 窄层判定:说明列 < 1.2in,或标题在标题列内放不下单行(会中部换行)→
|
|
2366
|
+
标题上、说明下单框混排(与 HTML 窄屏 .pyr__lvl 单列同语义) */
|
|
2367
|
+
const narrowP = descW < 1.2 || (ld && estLines(lt, lwP, 13.5) > 1) || (!ld && estLines(lt, w - 0.44, 13.5) > 1);
|
|
2368
|
+
if (narrowP) {
|
|
2369
|
+
s.addText(lt, { x: x + 0.22, y: y + 0.03, w: w - 0.44, h: 0.30, valign: 'middle',
|
|
2370
|
+
fontFace: STYLE.font, fontSize: sz(12.5), bold: true, color: acc ? STYLE.accent : STYLE.ink, fit: 'shrink' });
|
|
2371
|
+
s.addText(ld, { x: x + 0.22, y: y + 0.33, w: w - 0.44, h: Math.max(0.2, rowH - 0.36), valign: 'middle',
|
|
2372
|
+
fontFace: STYLE.font, fontSize: sz(10.5), color: STYLE.faint, fit: 'shrink' });
|
|
2373
|
+
} else {
|
|
2374
|
+
s.addText(lt, { x: x + 0.22, y, w: lwP, h: rowH, valign: 'middle',
|
|
2375
|
+
fontFace: STYLE.font, fontSize: sz(13.5), bold: true, color: acc ? STYLE.accent : STYLE.ink, fit: 'shrink' });
|
|
2376
|
+
if (ld) s.addText(ld, { x: x + 0.22 + lwP, y, w: descW, h: rowH,
|
|
2377
|
+
valign: 'middle', fontFace: STYLE.font, fontSize: sz(11), color: STYLE.faint, fit: 'shrink' });
|
|
2378
|
+
}
|
|
2232
2379
|
});
|
|
2233
2380
|
} else if (type === 'steps') {
|
|
2234
2381
|
/* 步骤条:N 步横排 + 箭头;超 maxPerRow 折行;groups 分组标签 */
|
|
@@ -2297,8 +2444,14 @@ CONTENT.sections.forEach((sec) => {
|
|
|
2297
2444
|
ps.forEach((p, i) => {
|
|
2298
2445
|
const x = treg.x + 0.2 + i * stepW;
|
|
2299
2446
|
const on = p[3] === 'done' || p[3] === 'now';
|
|
2300
|
-
|
|
2301
|
-
|
|
2447
|
+
// 变体 timeline:节点图标(icon:<name>);无图标回落圆点
|
|
2448
|
+
const wantIcon = (sec.variant === 'timeline') && p[4];
|
|
2449
|
+
if (wantIcon) {
|
|
2450
|
+
addIcon(s, p[4], x - 0.13, T.dotY - 0.06, 0.26, on ? STYLE.accent : STYLE.body);
|
|
2451
|
+
} else {
|
|
2452
|
+
s.addShape('ellipse', { x: x - T.dotR / 2, y: T.dotY, w: T.dotR, h: T.dotR,
|
|
2453
|
+
fill: { color: on ? STYLE.accent : STYLE.line }, line: { type: 'none' } });
|
|
2454
|
+
}
|
|
2302
2455
|
s.addText(p[0], { x: x - 0.1, y: T.labelY, w: stepW - 0.2, h: 0.4, fontFace: STYLE.font,
|
|
2303
2456
|
fontSize: sz(12), bold: true, color: STYLE.accent, charSpacing: 1 });
|
|
2304
2457
|
s.addText(p[1], { x: x - 0.1, y: T.nameY, w: stepW - 0.2, h: 0.5, fontFace: STYLE.fontDisplay,
|
|
@@ -2354,11 +2507,25 @@ CONTENT.sections.forEach((sec) => {
|
|
|
2354
2507
|
const fz = fitFont(items.length ? items : ['x'.repeat(longest)], g.w, availH, { max: 13.5, gapFactor: 1.2 });
|
|
2355
2508
|
groups.forEach((col, ci) => {
|
|
2356
2509
|
if (!col.length) return;
|
|
2510
|
+
const colX = treg.x + ci * g.step;
|
|
2511
|
+
// 变体 2-col-feature:首栏特性栏(accent 左线 + 标题放大一档)
|
|
2512
|
+
const isFeature = (sec.variant === '2-col-feature') && ci === 0 && nCol === 2;
|
|
2513
|
+
const fx = isFeature ? colX + 0.14 : colX;
|
|
2514
|
+
const fw = isFeature ? g.w - 0.14 : g.w;
|
|
2515
|
+
if (isFeature) {
|
|
2516
|
+
// 特性栏 accent 左线:高度按首段实际文本估算(不用整栏 availH,避免无墨水区压进注释带触发 severe 门禁)
|
|
2517
|
+
const p0 = col[0] || ['', ''];
|
|
2518
|
+
const barH = Math.min(availH,
|
|
2519
|
+
estTextH(p0[0] + ' ', fw, sz(fz + 1), 1.5) +
|
|
2520
|
+
estTextH(p0[1] || '', fw, sz(fz - 1), 1.5) + sz(7) / 72 + 0.06);
|
|
2521
|
+
s.addShape('rect', { x: colX, y: treg.y, w: 0.045, h: Math.max(0.2, barH),
|
|
2522
|
+
fill: { color: STYLE.accent }, line: { type: 'none' }, objectName: 'feature:bar' });
|
|
2523
|
+
}
|
|
2357
2524
|
const runs = col.map(p => ([
|
|
2358
|
-
{ text: p[0] + ' ', options: { fontSize: sz(fz), bold: true, color: STYLE.ink } },
|
|
2525
|
+
{ text: p[0] + ' ', options: { fontSize: sz(isFeature ? fz + 1 : fz), bold: true, color: STYLE.ink } },
|
|
2359
2526
|
{ text: p[1] + '\n', options: { fontSize: sz(fz - 1), color: STYLE.body, breakLine: true } },
|
|
2360
2527
|
])).flat();
|
|
2361
|
-
s.addText(runs, { x:
|
|
2528
|
+
s.addText(runs, { x: fx, y: treg.y, w: fw, h: availH, fontFace: STYLE.font,
|
|
2362
2529
|
valign: 'top', lineSpacing: sz(fz * 1.5), paraSpaceBefore: sz(7) });
|
|
2363
2530
|
});
|
|
2364
2531
|
} else if (type === 'halftable') {
|
|
@@ -2452,8 +2619,10 @@ CONTENT.sections.forEach((sec) => {
|
|
|
2452
2619
|
: (Array.isArray(cd)
|
|
2453
2620
|
? { title: cd[0] || '', points: cd.length > 1 ? [['', cd[1]]] : [] }
|
|
2454
2621
|
: { title: String(cd == null ? '' : cd), points: [] }));
|
|
2455
|
-
const
|
|
2456
|
-
const
|
|
2622
|
+
const isBento = (sec.variant === 'bento-grid') && cds.length >= 3;
|
|
2623
|
+
const c = isBento ? 3 : Math.min(sec.columns || 3, cds.length || 1);
|
|
2624
|
+
// bento-grid:首卡占 2 个槽位(跨 2 列),其余顺排;槽位数 = 卡数 + 1
|
|
2625
|
+
const r = isBento ? Math.ceil((cds.length + 1) / c) : Math.ceil((cds.length || 1) / c);
|
|
2457
2626
|
const gw = (creg.w - (c - 1) * cdC.gap) / c;
|
|
2458
2627
|
const gh = fitRowH(creg.h - (r - 1) * cdC.gap, r, cdC.maxH, 0.6);
|
|
2459
2628
|
/* points 文本归一化(scaffold v9 契约 {title, points:[[k,v]]} 兼容三种形态) */
|
|
@@ -2478,21 +2647,28 @@ CONTENT.sections.forEach((sec) => {
|
|
|
2478
2647
|
{ max: 12, lineFactor: 1.5, gapFactor: 0 }))) : 12;
|
|
2479
2648
|
const ptsFz = sz(ptsFzB);
|
|
2480
2649
|
cds.forEach((cd, i) => {
|
|
2481
|
-
|
|
2650
|
+
let col = i % c, row = Math.floor(i / c), span = 1;
|
|
2651
|
+
if (isBento) {
|
|
2652
|
+
// 首卡跨 2 列:槽位 = i===0 ? [0,1] : i+1
|
|
2653
|
+
const slot = i === 0 ? 0 : i + 1;
|
|
2654
|
+
if (i === 0) span = 2;
|
|
2655
|
+
col = slot % c; row = Math.floor(slot / c);
|
|
2656
|
+
}
|
|
2657
|
+
const cw = span * gw + (span - 1) * cdC.gap;
|
|
2482
2658
|
const x = creg.x + col * (gw + cdC.gap), y = creg.y + row * (gh + cdC.gap);
|
|
2483
|
-
s.addShape('roundRect', { x, y, w:
|
|
2659
|
+
s.addShape('roundRect', { x, y, w: cw, h: gh, rectRadius: 0.08,
|
|
2484
2660
|
fill: { color: STYLE.surface }, line: { color: STYLE.line, width: 0.75 } });
|
|
2485
2661
|
/* 卡片头路标:真导出语义图标(SVG→PNG),与 HTML .card__ico 同源;无资产时回落 accent 方块 */
|
|
2486
2662
|
const ico = 0.18;
|
|
2487
2663
|
const icoName = cd.icon || ICON_LIB.pickIconName(cd.title || '', i);
|
|
2488
2664
|
addIcon(s, icoName, x + 0.18, y + 0.2, ico, STYLE.accent);
|
|
2489
|
-
s.addText(cd.title || '', { x: x + 0.18 + ico + 0.1, y: y + 0.14, w:
|
|
2665
|
+
s.addText(cd.title || '', { x: x + 0.18 + ico + 0.1, y: y + 0.14, w: cw - 0.36 - ico - 0.1, h: cdC.titleH, fontFace: STYLE.font,
|
|
2490
2666
|
fontSize: sz(15), bold: true, color: STYLE.ink, fit: 'shrink' });
|
|
2491
2667
|
/* points 兼容:[[k,v]…] | [str…] | [{t,d}…](scaffold v9 契约 {title, points:[[k,v]]}) */
|
|
2492
2668
|
const ptLines = (cd.points || []).map(cardPtText);
|
|
2493
2669
|
s.addText(ptLines.map(t => ({ text: t + '\n',
|
|
2494
2670
|
options: { fontSize: ptsFz, color: STYLE.body, fontFace: STYLE.font, breakLine: true } })),
|
|
2495
|
-
{ x: x + 0.18, y: y + 0.14 + cdC.titleH + 0.06, w:
|
|
2671
|
+
{ x: x + 0.18, y: y + 0.14 + cdC.titleH + 0.06, w: cw - 0.36, h: ptsBoxH,
|
|
2496
2672
|
fontFace: STYLE.font, valign: 'top', lineSpacing: ptsFz * 1.5 });
|
|
2497
2673
|
});
|
|
2498
2674
|
} else if (type === 'split') {
|
|
@@ -2644,12 +2820,12 @@ CONTENT.sections.forEach((sec) => {
|
|
|
2644
2820
|
const reg = REGIONS.regionOf('exhibit', 'primary', { withNote: !!(sec.soWhat || sec.footnote) })
|
|
2645
2821
|
|| { x: MX, y: PT.exhibit.chartY, w: CW, h: PT.exhibit.chartH };
|
|
2646
2822
|
if (sec.exhibitNo) {
|
|
2647
|
-
s.addText(
|
|
2823
|
+
s.addText(exhibitBadge(sec.exhibitNo), { x: MX, y: PT.exhibit.badgeY, w: CW, h: 0.35,
|
|
2648
2824
|
fontFace: STYLE.font, fontSize: sz(11.5), bold: true, color: STYLE.accent, charSpacing: 2 });
|
|
2649
2825
|
}
|
|
2650
2826
|
if (ec.labels && ec.values) {
|
|
2651
2827
|
const edcols = ec.colors || dataColors(styleArg);
|
|
2652
|
-
const h = Math.max(1.2, chartBottom(!!sec.soWhat, !!sec.footnote) - reg.y - 0.05);
|
|
2828
|
+
const h = Math.max(1.2, chartBottom(!!sec.soWhat, !!(sec.footnote || sec.note)) - reg.y - 0.05);
|
|
2653
2829
|
if (ec.type === 'hbar') {
|
|
2654
2830
|
chartBlock(s, ec, reg.x, reg.y, reg.w, h, edcols);
|
|
2655
2831
|
} else {
|
|
@@ -2681,8 +2857,10 @@ CONTENT.sections.forEach((sec) => {
|
|
|
2681
2857
|
: CW - 0.4;
|
|
2682
2858
|
const avail = bodyBottom - bodyY;
|
|
2683
2859
|
const rowH = fitRowH(avail, pts.length, P.rowH, 0.3);
|
|
2860
|
+
/* 长列表收紧(adaptiveText.list 单源):条目多时降字号上限/段间距/行距,仍装不下由 fitFont 阶梯下探 */
|
|
2861
|
+
const _lo = listFitOpts(pts.length);
|
|
2684
2862
|
const fzB = fitFont(pts.map(p => (p[0] || '') + (p[1] || '')), textW - 0.32, avail,
|
|
2685
|
-
{ max: 15, gapFactor:
|
|
2863
|
+
{ max: Math.min(15, _lo.max), gapFactor: _lo.gapFactor, lineFactor: _lo.lineFactor });
|
|
2686
2864
|
const fz = sz(fzB), fzSm = sz(snapFont(fzB - 1));
|
|
2687
2865
|
pts.forEach((p, i) => {
|
|
2688
2866
|
const y = bodyY + i * rowH;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""TopPPT HTML · 图标包校验(assets/icons/)
|
|
4
|
+
|
|
5
|
+
检查:
|
|
6
|
+
1. 每个 *.svg 合法 XML;根为 svg;viewBox 恰为 "0 0 24 24"
|
|
7
|
+
2. 描边风格统一:fill="none"、stroke="currentColor"、stroke-width="1.8"、
|
|
8
|
+
stroke-linecap/linejoin="round"(同家族铁律)
|
|
9
|
+
3. 只含允许元素(path/circle/rect/ellipse/line/polyline/polygon),无 <text>/<image>,
|
|
10
|
+
无 fill="#..." 彩色填充(禁三方彩色标),无外链
|
|
11
|
+
4. index.json 一致性:icons 键 ↔ <name>.svg 文件一一对应;defaults 5~8 个且都在包内;
|
|
12
|
+
keywords 非空且含自身名
|
|
13
|
+
5. 每个图标有同名 512px PNG(PPTX 通道预渲染)
|
|
14
|
+
|
|
15
|
+
用法: python scripts/check_icons.py [--strict]
|
|
16
|
+
退出码:0=通过;1=有 FAIL;2=仅 WARN(--strict 下也按 1 处理)
|
|
17
|
+
"""
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import json
|
|
21
|
+
import sys
|
|
22
|
+
import xml.etree.ElementTree as ET
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
|
|
25
|
+
ROOT = Path(__file__).resolve().parent.parent
|
|
26
|
+
ICON_DIR = ROOT / 'assets' / 'icons'
|
|
27
|
+
ALLOWED = {'path', 'circle', 'rect', 'ellipse', 'line', 'polyline', 'polygon'}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def main() -> int:
|
|
31
|
+
strict = '--strict' in sys.argv
|
|
32
|
+
fails: list[str] = []
|
|
33
|
+
warns: list[str] = []
|
|
34
|
+
|
|
35
|
+
idx_path = ICON_DIR / 'index.json'
|
|
36
|
+
if not idx_path.exists():
|
|
37
|
+
print('FAIL: assets/icons/index.json 缺失')
|
|
38
|
+
return 1
|
|
39
|
+
index = json.loads(idx_path.read_text(encoding='utf-8'))
|
|
40
|
+
icons_meta = index.get('icons') or {}
|
|
41
|
+
if index.get('count') != len(icons_meta):
|
|
42
|
+
fails.append(f"index.json count={index.get('count')} 与实际条目 {len(icons_meta)} 不一致")
|
|
43
|
+
|
|
44
|
+
svg_names = {p.stem for p in ICON_DIR.glob('*.svg')}
|
|
45
|
+
meta_names = set(icons_meta)
|
|
46
|
+
for n in sorted(meta_names - svg_names):
|
|
47
|
+
fails.append(f'index.json 有条目但缺文件: {n}.svg')
|
|
48
|
+
for n in sorted(svg_names - meta_names):
|
|
49
|
+
warns.append(f'有 SVG 文件但 index.json 未登记: {n}.svg')
|
|
50
|
+
|
|
51
|
+
defaults = index.get('defaults') or []
|
|
52
|
+
if not 5 <= len(defaults) <= 8:
|
|
53
|
+
fails.append(f'defaults 应 5~8 个,实际 {len(defaults)} 个')
|
|
54
|
+
for d in defaults:
|
|
55
|
+
if d not in icons_meta:
|
|
56
|
+
fails.append(f'default 图标不在包内: {d}')
|
|
57
|
+
|
|
58
|
+
for name, meta in icons_meta.items():
|
|
59
|
+
kws = meta.get('keywords') or []
|
|
60
|
+
if not kws:
|
|
61
|
+
warns.append(f'{name}: keywords 为空')
|
|
62
|
+
elif name not in kws:
|
|
63
|
+
warns.append(f'{name}: keywords 未含自身名')
|
|
64
|
+
png = ICON_DIR / f'{name}.png'
|
|
65
|
+
if not png.exists():
|
|
66
|
+
fails.append(f'缺预渲染 PNG: {name}.png')
|
|
67
|
+
elif png.stat().st_size < 1024:
|
|
68
|
+
warns.append(f'{name}.png 疑似过小({png.stat().st_size}B)')
|
|
69
|
+
|
|
70
|
+
for name in sorted(svg_names):
|
|
71
|
+
p = ICON_DIR / f'{name}.svg'
|
|
72
|
+
try:
|
|
73
|
+
root = ET.fromstring(p.read_text(encoding='utf-8'))
|
|
74
|
+
except ET.ParseError as e:
|
|
75
|
+
fails.append(f'{name}.svg XML 非法: {e}')
|
|
76
|
+
continue
|
|
77
|
+
tag = root.tag.split('}')[-1]
|
|
78
|
+
if tag != 'svg':
|
|
79
|
+
fails.append(f'{name}.svg 根元素不是 svg: {tag}')
|
|
80
|
+
continue
|
|
81
|
+
if root.get('viewBox') != '0 0 24 24':
|
|
82
|
+
fails.append(f"{name}.svg viewBox={root.get('viewBox')!r}(必须 '0 0 24 24')")
|
|
83
|
+
style = {
|
|
84
|
+
'fill': root.get('fill'), 'stroke': root.get('stroke'),
|
|
85
|
+
'stroke-width': root.get('stroke-width'),
|
|
86
|
+
'stroke-linecap': root.get('stroke-linecap'),
|
|
87
|
+
'stroke-linejoin': root.get('stroke-linejoin'),
|
|
88
|
+
}
|
|
89
|
+
want = {'fill': 'none', 'stroke': 'currentColor', 'stroke-width': '1.8',
|
|
90
|
+
'stroke-linecap': 'round', 'stroke-linejoin': 'round'}
|
|
91
|
+
if style != want:
|
|
92
|
+
fails.append(f'{name}.svg 风格属性偏离家族规范: {style}')
|
|
93
|
+
for el in root.iter():
|
|
94
|
+
t = el.tag.split('}')[-1]
|
|
95
|
+
if t == 'svg':
|
|
96
|
+
continue
|
|
97
|
+
if t not in ALLOWED:
|
|
98
|
+
fails.append(f'{name}.svg 含非法元素 <{t}>(禁 text/image/彩色填充)')
|
|
99
|
+
for k, v in el.attrib.items():
|
|
100
|
+
if k == 'fill' and v not in ('none',):
|
|
101
|
+
fails.append(f'{name}.svg <{t}> 有彩色填充 fill={v}')
|
|
102
|
+
if isinstance(v, str) and v.startswith('http'):
|
|
103
|
+
fails.append(f'{name}.svg 含外链: {v[:60]}')
|
|
104
|
+
|
|
105
|
+
for w in warns:
|
|
106
|
+
print(f'WARN: {w}')
|
|
107
|
+
for f in fails:
|
|
108
|
+
print(f'FAIL: {f}')
|
|
109
|
+
print(f'[check_icons] {len(svg_names)} svg / {len(icons_meta)} index / '
|
|
110
|
+
f'{len(defaults)} defaults: {"OK" if not fails else "FAIL"}')
|
|
111
|
+
if fails:
|
|
112
|
+
return 1
|
|
113
|
+
if warns and strict:
|
|
114
|
+
return 1
|
|
115
|
+
return 0
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
if __name__ == '__main__':
|
|
119
|
+
raise SystemExit(main())
|