@topmindspace/tms-skills 0.1.16 → 0.1.18
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 +47 -0
- package/README.en.md +3 -3
- package/README.md +3 -3
- 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 +2 -2
- package/top-ppt-html/assets/examples/2026-09-09-architecture-graphite-dark.html +122 -48
- package/top-ppt-html/assets/examples/2026-09-09-presentation-business-blue.html +122 -48
- package/top-ppt-html/assets/examples/2026-09-09-research-mckinsey.html +122 -48
- package/top-ppt-html/assets/examples/2026-09-26-topmind-tms-skills-showcase.html +126 -52
- package/top-ppt-html/assets/examples/2026-09-26-topmind-tms-skills-showcase.model.json +3 -3
- package/top-ppt-html/assets/pptx-export.js +118 -45
- package/top-ppt-html/assets/style-gallery.html +19 -3
- package/top-ppt-html/assets/templates/architecture.html +122 -48
- package/top-ppt-html/assets/templates/engine.css +3 -2
- package/top-ppt-html/assets/templates/presentation.html +122 -48
- package/top-ppt-html/assets/templates/research.html +122 -48
- package/top-ppt-html/package-lock.json +2 -2
- package/top-ppt-html/package.json +1 -1
- package/top-ppt-html/references/chart-decision-tree.md +26 -25
- package/top-ppt-html/references/components-atoms.md +5 -4
- package/top-ppt-html/references/content-rules.md +3 -1
- package/top-ppt-html/references/failure-modes.md +29 -3
- package/top-ppt-html/references/layouts-combo.md +1 -1
- package/top-ppt-html/references/modes.md +1 -1
- package/top-ppt-html/references/page-type-matrix.md +10 -6
- package/top-ppt-html/references/pptx-export.md +3 -3
- package/top-ppt-html/scripts/build_pptx.js +101 -42
- package/top-ppt-html/scripts/cross_verify.py +84 -1
- package/top-ppt-html/scripts/extract_model.py +5 -0
- package/top-ppt-html/scripts/layout-constants.json +42 -2
- package/top-ppt-html/scripts/lib_layout_regions.js +6 -3
- package/top-ppt-html/scripts/model-schema.json +1 -0
- package/top-ppt-html/scripts/render_from_model.py +133 -17
- package/top-ppt-html/scripts/sync_runtime.py +19 -3
- package/top-ppt-html/scripts/test_feedback_gates.py +73 -0
- package/top-ppt-html/scripts/validate_pptx.py +148 -0
- package/top-ppt-html/scripts/validate_report.py +24 -0
|
@@ -10,15 +10,15 @@
|
|
|
10
10
|
| 现状基线 / 指标快照 | `metrics` | `kpi` | 4–6 个;单数压场用 `kpi` |
|
|
11
11
|
| 单个核心数字压场 | `kpi` | `metrics` | hero 一数一结论 |
|
|
12
12
|
| 多维对标清单 | `table` | `halftable` | ≤8 行(research ≤16) |
|
|
13
|
-
| 时间趋势 / 爬坡 | `bar`(chart.type=line/area/dualline) | `exhibit` | 时间点 >8
|
|
13
|
+
| 时间趋势 / 爬坡 | `bar`(chart.type=line/area/dualline) | `exhibit` | 时间点 >8 改折线;**area 还原度 low,PPTX 优先 line** |
|
|
14
14
|
| 构成占比 | `donut`(pie/multidonut/waffle) | `halftable` / **V3 KPI** | 扇区 ≤5,最小扇区 ≥8%;**极偏(min<5% 或 max/min>20)禁 donut→V3** |
|
|
15
15
|
| 排名 / 大小对比 | `bar`(chart.type=hbar/lollipop/pareto) | `table` | 类别 >8 取 Top 7 + 附录 |
|
|
16
|
-
| 现状 vs 目标 | `bullet` / `comparison` | `bar`(vsbar/bulletchart) |
|
|
16
|
+
| 现状 vs 目标 | `bullet` / `comparison` | `bar`(vsbar/bulletchart) | 目标线与实际条同量纲;comparison 的 `verdict`/`soWhat` **二选一** |
|
|
17
17
|
| 两期升降(谁升谁降) | `bar`(slope/dumbbell) | `table` | 两端都要标数值 |
|
|
18
18
|
| 双维分布 / 优先级 | `bar`(scatter/bubble) | `matrix` | 气泡半径 ∝ √值 |
|
|
19
|
-
| 分布 / 离散度 | `boxplot` | `bar`(dotplot) | 组数 ≤8
|
|
19
|
+
| 分布 / 离散度 | `boxplot` | `bar`(dotplot) | 组数 ≤8;箱线四件齐备;**boxplot 还原度 low,PPTX 改 dotplot** |
|
|
20
20
|
| 增减归因(A→B) | `bar`(waterfall) | `table` | 每段标变化量 + 合计收口 |
|
|
21
|
-
| 转化 / 筛选漏斗 | `bar`(funnel) | `sankey` | 层数 ≤5 |
|
|
21
|
+
| 转化 / 筛选漏斗 | `bar`(funnel) | `sankey` | 层数 ≤5;**sankey 还原度 low,PPTX 改 table** |
|
|
22
22
|
| 计划排期(并行) | `bar`(gantt) | `timeline` | 里程碑叙事用 `timeline`,勿混 |
|
|
23
23
|
| 里程碑 / 路线叙事 | `timeline` | `steps` | 3–5 个,每个 ≤2 行 |
|
|
24
24
|
| 步骤 / 实施节奏 | `steps` | `timeline` | 3–6 步;高亮步 ≤2 处 |
|
|
@@ -28,8 +28,12 @@
|
|
|
28
28
|
| 关键判断 / 金句休止 | `quote` | `points` | 全文 1–2 处,多了廉价 |
|
|
29
29
|
| 完整论述(发现→证据→含义) | `twocol`(research R1) | `threecol` | 段 ≤200 字;标题必须结论句 |
|
|
30
30
|
| 数据证据页(主力) | `exhibit`(R2) | `split` | 全篇 Exhibit 连续编号 |
|
|
31
|
-
| 三栏并列论点 | `threecol`(R6) | `cards` | 每栏 ≤150
|
|
32
|
-
| 系统分层架构 | `diagram` | `bar`(chart.type=network) | 节点 ≤24;层 ≤4 |
|
|
31
|
+
| 三栏并列论点 | `threecol`(R6) | `cards` | 每栏 ≤150 字;栏宽按版心全宽推导 |
|
|
32
|
+
| 系统分层架构 | `diagram` | `bar`(chart.type=network) | 节点 ≤24;层 ≤4;**network 还原度 low** |
|
|
33
33
|
| 流程 / 职责协作 | `lane` | `diagram` | 每行 ≤6 步;正交折线优先 |
|
|
34
34
|
| 素材图片(实拍/截图) | `image` | `split` | 六版式;无图用 `image.placeholder` |
|
|
35
35
|
| 待核实项收口 | `flags` 字段 | `table` | 标色必须配说明 |
|
|
36
|
+
| 大纲 / 议程 | `agenda`(结构页) | — | ≤8 条单列;>8 双列;>12 自动分页;标题 ≤36 字 |
|
|
37
|
+
|
|
38
|
+
> **容量契约**(超限请拆页/换形态,禁静默截断):agenda ≤16 条(`splitMax`)、单条标题 ≤36 字(`titleMaxChars`);metrics ≤6/行、值 ≤6 字;diagram ≤3 层 × 每层 ≤3 节点、层名 ≤7 字、注解 ≤10 字;timeline ≤5 段、每段说明 ≤22 字;comparison `verdict`/`soWhat` 二选一;threecol/twocol 每栏 ≥120 字、段数 <4 时用三栏;表格单元格 ≤14 字、行 ≤8(research ≤16)。
|
|
39
|
+
> **PPTX 还原度**(`charts.fidelityMap`):`high` = bar/hbar/line/donut/stack/waterfall/gauge/pareto/progress 等;`low` = area/radar/treemap/sankey/streamgraph/marimekko/boxplot/network——PPTX 会退化,优先改 high 图型或 `dataTable=inline`。完整分级见 `chart-decision-tree.md`。
|
|
@@ -108,7 +108,7 @@ HTML 侧仍用 MD3 clamp;双端都以「最后一排可读」为准(`present
|
|
|
108
108
|
|
|
109
109
|
> 几何常量见 `layout-constants.json` `pageTypes.{sankey,treemap,boxplot,network,marimekko,streamgraph}`(含节点/流带上限、采样点下限、双边界追踪约束);版式与生成规则详见 `references/infographics.md`。
|
|
110
110
|
|
|
111
|
-
**通用可选字段(全部页型)**:`soWhat`(结论条,MD3 tonal soft 满铺、**无左轨**;**不绘制「SO WHAT」标签**)、`footnote`(页脚来源行)、`flags`(**待核实标注清单** `["…"]` → accent 强调色清单条,提示用户二次确认,并写入演讲者备注)、`image`(素材图片 / 配图占位,见 `image` 页型)、`exhibitNo`(非 exhibit 页型也可带 Exhibit 徽标)。research 模式 `soWhat/footnote` **只在关键论证页填写**(克制条款;结论条无 So what
|
|
111
|
+
**通用可选字段(全部页型)**:`soWhat`(结论条,MD3 tonal soft 满铺、**无左轨**;**不绘制「SO WHAT」标签**)、`footnote`(页脚来源行)、`note`(口径注,与 `footnote` 共用注释带末行 `footnoteY=6.72`,同页二者自动合并为一行)、`flags`(**待核实标注清单** `["…"]` → accent 强调色清单条,提示用户二次确认,并写入演讲者备注)、`image`(素材图片 / 配图占位,见 `image` 页型)、`exhibitNo`(非 exhibit 页型也可带 Exhibit 徽标)。research 模式 `soWhat/footnote` **只在关键论证页填写**(克制条款;结论条无 So what 字样)。**`verdict` 与 `soWhat` 互斥**(共用 annotation 结论条槽位,同页双填必叠印)——comparison 页填 `verdict`(accent 实底结论条)时不要再填 `soWhat`;渲染器自动二选一(verdict 优先),`extract_model` 对同页双填发 WARN。
|
|
112
112
|
|
|
113
113
|
> **素材图片(单源 `layout-constants.json` 的 `imageSpec`)**:`image` 三选一必填——`src`(用户图,data: 内联或相对路径)|`items:[{src,alt?,caption?,placeholder?}]`(多图版式)|`placeholder:true`(**配图占位**,无素材时锁版式用)。
|
|
114
114
|
> - **六版式**(`layout`):`full` 版心全宽 3:1|`half` 左图右注 4:3|`bleed` 通栏出血 21:9|`grid` 多图网格 4:3(2/3/4/6 张)|`compare` 双图 A/B 4:3|`wall` Logo 墙 1:1。比例写在 `imageSpec.ratioDefault`,**HTML 用同比例锁定类、PPTX 用同一比例算高度并垂直居中**——两通道版式一致。
|
|
@@ -118,7 +118,7 @@ HTML 侧仍用 MD3 clamp;双端都以「最后一排可读」为准(`present
|
|
|
118
118
|
> - **准入门**:`imageAdmission`(`forbidden` 禁图片化区域 / `allowed` 可保留的复杂视觉资产 / `maxPageAreaPct` 40% / `fullSlideRiskPct` 90%)。
|
|
119
119
|
> - **素材准备**:`python scripts/prepare_images.py <图片|目录> --layout full|half|grid…` → 产出可粘贴的 `.media` HTML 片段与 `image` 模型对象(自动缩放/压缩/内联,体积超限自动转相对路径)。
|
|
120
120
|
|
|
121
|
-
> **图表双通道**:图表类型与通道归属以 `scripts/layout-constants.json` 的 `charts.registry` 为唯一事实源(四元组:`html` 实现 / `pptx` 通道 / `nativeType` 或 `path` 几何约束 / `dataTable`
|
|
121
|
+
> **图表双通道**:图表类型与通道归属以 `scripts/layout-constants.json` 的 `charts.registry` 为唯一事实源(四元组:`html` 实现 / `pptx` 通道 / `nativeType` 或 `path` 几何约束 / `dataTable` 策略)。**PPTX 还原度分级**(`charts.fidelityMap`):`high` 双通道等价|`medium` 形状可读但视觉降级|`low` PPTX 显著退化(area/radar/treemap/sankey/streamgraph/marimekko/boxplot/network)。**交付 PPTX 时优先 high 图型**(`bar`/`hbar`/`line`/`donut`),或对 low 图型强制 `dataTable=inline` 补数据表——详见 `chart-decision-tree.md`。
|
|
122
122
|
> - **原生通道(16 类,`pptx:"native"`)**:`bar / hbar / stack / stackline / line / dualline / area / donut / multidonut / pie / radar / scatter / bubble` + 3 类原生技巧 `waterfall`(堆叠柱 + 隐藏基底 + 累计连接线)、`gauge`(doughnut + firstSliceAng 270° 扇形)、`pareto`(多类型组合:柱 + 累计折线)——一律走 `pptxgenjs addChart`,产出真 chart part + 内嵌 Excel 工作簿,PowerPoint/WPS 中双击即可"编辑数据"。`chart.series` 传多系列(`[{name, values}…]`);`chart.points` 传 XY 点(scatter `[[x,y]…]` / bubble `[[x,y,size]…]`)。
|
|
123
123
|
> - **形状通道(`pptx:"shape"`,20 类)**:全部走 OOXML 原生形状高保真还原(`charts.registry[type].path` 约束采样点下限、双边界追踪、禁预设形状替代)。按**载荷形态**分两组:
|
|
124
124
|
> - **可作 `chart.type` 的 14 类**(载荷 = `chart:{labels, values, series?, start?, target?, max?, unit?}`):`funnel / gantt / vsbar / progress / sparkline / slope / dumbbell / lollipop / dotplot / bulletchart / waffle / radialbar / rose / candlestick`——由 `bar` / `exhibit` / `halftable` / `split` 页型承载。
|
|
@@ -129,7 +129,7 @@ HTML 侧仍用 MD3 clamp;双端都以「最后一排可读」为准(`present
|
|
|
129
129
|
> - **A/B 双通道语义一致**:A 通道(浏览器预览 `pptx-export.js`)对所有类型做形状近似(类别标签与数值都落为文本),B 通道交付走原生图表;`cross_verify.py` 对两侧文本做归一化(chart categories 并入 + `NUMERIC_TOKEN` 纯数值过滤 + `sorted(set())` 去重),并额外核对 **B 通道原生图表数值 ↔ 模型数值**(类别标签一致不代表数值一致)。
|
|
130
130
|
> - **通道能力边界(演讲者备注)**:A 预览通道**不产 notesSlide**(浏览器预览也看不到备注),故 `MODEL_CHART_NOTES_MISSING` 与 `MODEL_CHART_COUNT` 同走 `--allow-shape-charts` 豁免;**交付通道(B)不豁免**——`dataTable=notes` 的图表必须在备注里真的有数据表。
|
|
131
131
|
>
|
|
132
|
-
> **自适应排版**:所有文本块按可用高度自适应字号/行距(`fitFont`),表格行高按可用高度计算,列表/泳道/矩阵/卡片行高随容器收敛——保证不越界、不重叠(`validate_pptx.py`
|
|
132
|
+
> **自适应排版**:所有文本块按可用高度自适应字号/行距(`fitFont`),表格行高按可用高度计算,列表/泳道/矩阵/卡片行高随容器收敛——保证不越界、不重叠(`validate_pptx.py` 硬拦越界与文本溢出)。**几何铁律**:① 架构节点标题/注解按实际节点高度钳制(注解不得「浮回」标题上,空间不足降级单框混排);② 图表类目标签含在图高 `h` 内(禁画到 `y+h` 外压 so-what);③ 多栏均分用**版心全宽**推导(`(CW-(n-1)×gap)/n`,禁用单栏宽当总宽);④ 图例落图区底部(`bodyBottom-0.28`),禁用页面底部固定偏移;⑤ KPI 支撑指标列在 `dividerX` 右侧(禁全宽默认返回压住 hero);⑥ 有图注时图片下界收到 `contentBottomWithNote`。Agenda:>8 条自动双列、>12 条自动分页(Agenda I/II)、长标题截断全称沉 notes(`titleMaxChars=36`)、两通道列优先一致。
|
|
133
133
|
|
|
134
134
|
> 保真机制:每种子页型对应 HTML 的一种锁定版式(`components.md` §36d/§37–38b/§39–42 ↔ 页型表),坐标/字号按同一套常量映射(`layout-constants.json` 的 `pageTypes` + `modeTypeScale`)。**三模式独立比例尺**:research 密排(正文 10.5pt、h1 22pt);architecture 与 presentation 同基但 diagram 走全幅几何。图表颜色:**9 套风格各有自己的编码色板 c1–c5**(单源 `styleDataColors` / `styleDataColorsDark`,按 `model.theme` 选择),多系列图表自动取用(`dataColors(style, theme)`);未登记风格时回落「accent / faint / body / line」四阶单色系。**页型字段与必填约束以 `model-schema.json` 为唯一事实源**(`:array` = 非空数组、`:str` = 非空字符串),浏览器端 `validateModel` 与 `extract_model.py` 校验同一份定义。
|
|
135
135
|
|
|
@@ -381,6 +381,15 @@ function footnoteLine(s, text) {
|
|
|
381
381
|
s.addText(text, { x: R.x, y: R.y, w: R.w, h: R.h,
|
|
382
382
|
fontFace: STYLE.font, fontSize: sz(9.5), color: STYLE.faint, objectName: trName('footnote') });
|
|
383
383
|
}
|
|
384
|
+
/* R2:sec.note 与 sec.footnote 共用注释带末行(footnoteY=6.72)。
|
|
385
|
+
note.y=6.55 会压进 so-what 带——同页二者合并为一行。 */
|
|
386
|
+
function noteOrFootnote(s, sec) {
|
|
387
|
+
const parts = [];
|
|
388
|
+
if (sec && sec.note) parts.push(String(sec.note));
|
|
389
|
+
if (sec && sec.footnote) parts.push(String(sec.footnote));
|
|
390
|
+
if (!parts.length) return;
|
|
391
|
+
footnoteLine(s, parts.join(' · '));
|
|
392
|
+
}
|
|
384
393
|
/* 待核实清单条(accent 强调色标注 · 与 HTML .flagbar 同源)
|
|
385
394
|
* 底对齐 contentBottom,正文区在调用点按 flagH 让位。 */
|
|
386
395
|
function flagBar(s, items, yTop) {
|
|
@@ -1584,30 +1593,55 @@ function addTable(s, tbl, x, y, w, availH, opts) {
|
|
|
1584
1593
|
s.addNotes([CONTENT.subtitle, CONTENT.meta].filter(Boolean).join('\n'));
|
|
1585
1594
|
})();
|
|
1586
1595
|
|
|
1587
|
-
/* Agenda(architecture 极简形态可省略 agenda →
|
|
1596
|
+
/* Agenda(architecture 极简形态可省略 agenda → 不产出大纲页;
|
|
1597
|
+
>8 条自动双列;>12 条自动分页 Agenda I/II;长标题截断+全称入 notes;
|
|
1598
|
+
行高自适应防溢出;列填充与 HTML 统一为列优先) */
|
|
1588
1599
|
(function agenda() {
|
|
1589
1600
|
if (!Array.isArray(CONTENT.agenda) || !CONTENT.agenda.length) return;
|
|
1590
|
-
const s = base();
|
|
1591
|
-
head(s, 'AGENDA', '报告大纲');
|
|
1592
1601
|
const items = CONTENT.agenda;
|
|
1593
|
-
|
|
1594
|
-
const
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
const
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1602
|
+
/* R4:单页容量 = 双列 × 每列最多 6 行(行高 ≥0.55in 可读) */
|
|
1603
|
+
const ROWS_PER_PAGE = 12;
|
|
1604
|
+
const pages = [];
|
|
1605
|
+
for (let i = 0; i < items.length; i += ROWS_PER_PAGE) pages.push(items.slice(i, i + ROWS_PER_PAGE));
|
|
1606
|
+
pages.forEach((pageItems, pi) => {
|
|
1607
|
+
const s = base();
|
|
1608
|
+
const sub = pages.length > 1 ? `(${pi + 1}/${pages.length})` : '';
|
|
1609
|
+
head(s, 'AGENDA', '报告大纲' + sub);
|
|
1610
|
+
const colN = pageItems.length > 8 ? 2 : 1;
|
|
1611
|
+
const perCol = Math.ceil(pageItems.length / colN);
|
|
1612
|
+
/* 预留 0.25in,避免末行底边落入 severe-overlap 安全网 */
|
|
1613
|
+
const availH = CONTENT_BOTTOM - CONTENT_TOP - 0.25;
|
|
1614
|
+
const rowH = Math.min(1.05, availH / Math.max(1, perCol));
|
|
1615
|
+
const numSize = rowH >= 0.85 ? sz(30) : (rowH >= 0.65 ? sz(22) : sz(18));
|
|
1616
|
+
const tSize = rowH >= 0.85 ? sz(16) : sz(14);
|
|
1617
|
+
const dSize = rowH >= 0.85 ? sz(12) : sz(11);
|
|
1618
|
+
const notes = [];
|
|
1619
|
+
pageItems.forEach((it, i) => {
|
|
1620
|
+
/* 列优先:先填左列再填右列(与 HTML grid-auto-flow:column 一致) */
|
|
1621
|
+
const col = Math.floor(i / perCol), row = i % perCol;
|
|
1622
|
+
const x = MX + col * (CW / colN), y = CONTENT_TOP + row * rowH;
|
|
1623
|
+
/* R4:标题按盒宽估算折行,超 1 行则截断并把全称沉 notes */
|
|
1624
|
+
const titleBoxW = CW / colN - 1.15;
|
|
1625
|
+
const rawTitle = String(it[1] || '');
|
|
1626
|
+
const titleEstW = estTextH(rawTitle, titleBoxW, tSize, 1.2);
|
|
1627
|
+
const maxTitleH = rowH * 0.55;
|
|
1628
|
+
let title = rawTitle;
|
|
1629
|
+
if (titleEstW > maxTitleH) {
|
|
1630
|
+
const maxChars = Math.max(10, Math.floor(rawTitle.length * (maxTitleH / titleEstW)));
|
|
1631
|
+
if (rawTitle.length > maxChars) {
|
|
1632
|
+
title = rawTitle.slice(0, maxChars - 1) + '…';
|
|
1633
|
+
notes.push(`条目 ${it[0]} 全称:${rawTitle}`);
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
s.addText(it[0], { x, y, w: 0.9, h: rowH - 0.05, fontFace: STYLE.font, fontSize: numSize,
|
|
1637
|
+
bold: true, color: STYLE.accent, valign: 'top' });
|
|
1638
|
+
s.addText([
|
|
1639
|
+
{ text: title + '\n', options: { fontSize: tSize, bold: true, color: STYLE.ink } },
|
|
1640
|
+
{ text: it[2] || '', options: { fontSize: dSize, color: STYLE.body } },
|
|
1641
|
+
], { x: x + 1.0, y: y + 0.03, w: titleBoxW, h: rowH - 0.08, fontFace: STYLE.font,
|
|
1642
|
+
valign: 'top', lineSpacing: tSize + 5 });
|
|
1643
|
+
});
|
|
1644
|
+
if (notes.length) s.addNotes(notes.join('\n'));
|
|
1611
1645
|
});
|
|
1612
1646
|
})();
|
|
1613
1647
|
|
|
@@ -1729,15 +1763,18 @@ CONTENT.sections.forEach((sec) => {
|
|
|
1729
1763
|
], { x: sd.x + 0.5, y: py, w: cw2 - 0.75, h: rowH - 0.04, fontFace: STYLE.font, valign: 'top' });
|
|
1730
1764
|
});
|
|
1731
1765
|
});
|
|
1766
|
+
/* R2:verdict 与 soWhat 共用 annotation 槽位——只画一个(verdict 优先,语义更具体)。
|
|
1767
|
+
两者同页必须互斥,否则结论条整条叠印。 */
|
|
1732
1768
|
if (sec.verdict) {
|
|
1733
1769
|
const vreg = REGIONS.regionOf('exhibit', 'annotation') ||
|
|
1734
1770
|
{ x: MX, y: PT.exhibit.soWhatY, w: CW, h: 0.62 };
|
|
1735
1771
|
s.addShape('rect', { x: vreg.x, y: vreg.y, w: vreg.w, h: vreg.h,
|
|
1736
|
-
fill: { color: STYLE.accent }, line: { type: 'none' } });
|
|
1772
|
+
fill: { color: STYLE.accent }, line: { type: 'none' }, objectName: trName('soWhat') });
|
|
1737
1773
|
s.addText([
|
|
1738
1774
|
{ text: '结论 ', options: { fontSize: sz(11), bold: true, color: STYLE.onAccent, charSpacing: 1.5 } },
|
|
1739
1775
|
{ text: sec.verdict, options: { fontSize: sz(12.5), bold: true, color: STYLE.onAccent } },
|
|
1740
|
-
], { x: vreg.x + 0.22, y: vreg.y + 0.06, w: vreg.w - 0.44, h: vreg.h - 0.12, fontFace: STYLE.font,
|
|
1776
|
+
], { x: vreg.x + 0.22, y: vreg.y + 0.06, w: vreg.w - 0.44, h: vreg.h - 0.12, fontFace: STYLE.font,
|
|
1777
|
+
valign: 'middle', objectName: trName('soWhat') });
|
|
1741
1778
|
}
|
|
1742
1779
|
} else if (type === 'quote') {
|
|
1743
1780
|
const Q = PT.quote;
|
|
@@ -1808,8 +1845,6 @@ CONTENT.sections.forEach((sec) => {
|
|
|
1808
1845
|
align: 'right', valign: 'middle', fontFace: STYLE.font, fontSize: sz(10.5), color: STYLE.faint });
|
|
1809
1846
|
});
|
|
1810
1847
|
}
|
|
1811
|
-
if (sec.note) s.addText(sec.note, { x: MX, y: PT.note.y, w: CW, h: PT.note.h,
|
|
1812
|
-
fontFace: STYLE.font, fontSize: sz(11), color: STYLE.faint });
|
|
1813
1848
|
} else if (type === 'heatmap') {
|
|
1814
1849
|
/* 热力矩阵:行 × 列 + 4 级色阶(surface-1 → accent-soft → accent-soft-2 → accent) */
|
|
1815
1850
|
const H = PT.heatmap;
|
|
@@ -1993,7 +2028,7 @@ CONTENT.sections.forEach((sec) => {
|
|
|
1993
2028
|
} else if (type === 'bar') {
|
|
1994
2029
|
const c = sec.chart || {};
|
|
1995
2030
|
const dcols = c.colors || dataColors(styleArg);
|
|
1996
|
-
const bot = chartBottom(!!sec.soWhat, !!sec.footnote
|
|
2031
|
+
const bot = chartBottom(!!sec.soWhat, !!(sec.footnote || sec.note)) - 0.05;
|
|
1997
2032
|
const isH = c.type === 'hbar';
|
|
1998
2033
|
const reg = REGIONS.regionOf('bar', isH ? 'hbar' : 'primary', { bottom: bot }) ||
|
|
1999
2034
|
{ x: MX, y: isH ? PT.bar.hbarY0 : PT.bar.chartY, w: CW, h: 3 };
|
|
@@ -2022,15 +2057,13 @@ CONTENT.sections.forEach((sec) => {
|
|
|
2022
2057
|
} else {
|
|
2023
2058
|
chartBlock(s, c, cx, reg.y, cw, Math.max(1.5, reg.h), dcols);
|
|
2024
2059
|
}
|
|
2025
|
-
if (sec.note) s.addText(sec.note, { x: MX, y: PT.note.y, w: CW, h: PT.note.h, fontFace: STYLE.font,
|
|
2026
|
-
fontSize: sz(11), color: STYLE.faint });
|
|
2027
2060
|
} else if (type === 'twocol' || type === 'threecol') {
|
|
2028
2061
|
const ps = sec.paragraphs || [];
|
|
2029
2062
|
const nCol = (type === 'threecol') ? 3 : 2;
|
|
2030
2063
|
const gap = (type === 'threecol') ? PT.research.col3Gap : PT.twocol.colGap;
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2064
|
+
/* R3:多栏均分必须用版心全宽。regionOf('twocol','primary') 返回的是「单栏宽」,
|
|
2065
|
+
直接当总宽再 cols() 会把三栏压成 1.69in、右侧 55% 全空。 */
|
|
2066
|
+
const treg = { x: MX, y: bodyY, w: CW, h: bodyBottom - bodyY };
|
|
2034
2067
|
const g = cols(treg.w, nCol, gap);
|
|
2035
2068
|
const per = Math.ceil(ps.length / nCol);
|
|
2036
2069
|
const availH = treg.h;
|
|
@@ -2258,11 +2291,30 @@ CONTENT.sections.forEach((sec) => {
|
|
|
2258
2291
|
const nds = isRec(nd) ? (nd.d || '') : '';
|
|
2259
2292
|
s.addShape('roundRect', { x, y, w: nw, h: lh, rectRadius: 0.06,
|
|
2260
2293
|
fill: { color: acc ? STYLE.soft : STYLE.surface }, line: { color: STYLE.line, width: 0.75 } });
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2294
|
+
/* 节点内标题/注解:注解底对齐会「浮回」标题上(R1)。改为标题顶对齐 + 注解钳在标题下边;
|
|
2295
|
+
空间不足时降级为单框混排(标题粗体 + 注解小字),禁止叠印。 */
|
|
2296
|
+
const padX = 0.12, padY = 0.10;
|
|
2297
|
+
const innerW = nw - padX * 2;
|
|
2298
|
+
const titleH = 0.32, noteH = 0.34, gapTN = 0.04;
|
|
2299
|
+
const needH = titleH + gapTN + (nds ? noteH : 0) + padY * 2;
|
|
2300
|
+
if (!nds || lh >= needH) {
|
|
2301
|
+
const tY = y + padY;
|
|
2302
|
+
s.addText(ntt, { x: x + padX, y: nds ? tY : y + lh / 2 - 0.18, w: innerW, h: titleH,
|
|
2303
|
+
fontFace: STYLE.font, fontSize: sz(nds ? 12 : 12.5), bold: true,
|
|
2304
|
+
color: acc ? STYLE.accent : STYLE.ink, valign: 'middle' });
|
|
2305
|
+
if (nds) {
|
|
2306
|
+
const nY = Math.max(tY + titleH + gapTN, y + lh - padY - noteH);
|
|
2307
|
+
s.addText(nds, { x: x + padX, y: nY, w: innerW, h: Math.min(noteH, y + lh - padY - nY),
|
|
2308
|
+
fontFace: STYLE.font, fontSize: sz(10), color: STYLE.faint, valign: 'top' });
|
|
2309
|
+
}
|
|
2310
|
+
} else {
|
|
2311
|
+
/* 空间不足:单框混排(同一文本框内富文本,禁拆两框叠印) */
|
|
2312
|
+
s.addText([
|
|
2313
|
+
{ text: ntt, options: { fontSize: sz(11), bold: true, color: acc ? STYLE.accent : STYLE.ink, breakLine: true } },
|
|
2314
|
+
{ text: nds, options: { fontSize: sz(9), color: STYLE.faint } },
|
|
2315
|
+
], { x: x + padX, y: y + padY * 0.5, w: innerW, h: lh - padY,
|
|
2316
|
+
fontFace: STYLE.font, valign: 'top', lineSpacing: sz(11 * 1.25) });
|
|
2317
|
+
}
|
|
2266
2318
|
});
|
|
2267
2319
|
if (li < lys.length - 1) {
|
|
2268
2320
|
const cy = y + lh + D.layerGap / 2;
|
|
@@ -2275,10 +2327,15 @@ CONTENT.sections.forEach((sec) => {
|
|
|
2275
2327
|
}
|
|
2276
2328
|
});
|
|
2277
2329
|
if (sec.legend) {
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2330
|
+
/* R2:图例进图区底部(与 HTML .arch__legend 同源),禁用 PH-0.62 固定偏移——
|
|
2331
|
+
那会压到来源行(6.72)与页码(7.0)。图例条自适应行内均分,右对齐。 */
|
|
2332
|
+
const lgH = 0.28;
|
|
2333
|
+
const lgY = Math.max(dStart, bodyBottom - lgH - 0.02);
|
|
2334
|
+
const nLg = Math.max(1, sec.legend.length);
|
|
2335
|
+
const step = Math.min(2.4, CW / nLg);
|
|
2336
|
+
sec.legend.forEach((lg, li) => {
|
|
2337
|
+
s.addText('● ' + lg, { x: MX + CW - nLg * step + li * step, y: lgY, w: step - 0.05, h: lgH,
|
|
2338
|
+
fontFace: STYLE.font, fontSize: sz(10), color: STYLE.faint, align: 'left' });
|
|
2282
2339
|
});
|
|
2283
2340
|
}
|
|
2284
2341
|
} else if (type === 'exhibit') {
|
|
@@ -2348,8 +2405,10 @@ CONTENT.sections.forEach((sec) => {
|
|
|
2348
2405
|
}
|
|
2349
2406
|
}
|
|
2350
2407
|
/* research 通用可选件:so-what 结论条 + 页脚来源行 + 待核实条(所有页型共用同一槽位) */
|
|
2351
|
-
|
|
2352
|
-
if (sec.
|
|
2408
|
+
/* R2:verdict 已占 annotation 槽位时跳过 soWhat,禁止同槽双条叠印 */
|
|
2409
|
+
if (sec.soWhat && !sec.verdict) soWhatBar(s, sec.soWhat);
|
|
2410
|
+
/* R2:note 与 footnote 共用注释带末行(禁 note.y=6.55 压 so-what) */
|
|
2411
|
+
noteOrFootnote(s, sec);
|
|
2353
2412
|
if (flagH) flagBar(s, flagItems, flagY);
|
|
2354
2413
|
/* 演讲者备注(口径/含义/来源沉 notes,不堆版面) */
|
|
2355
2414
|
const _notes = [];
|
|
@@ -58,6 +58,7 @@ def slide_texts(prs):
|
|
|
58
58
|
for shape in slide.shapes:
|
|
59
59
|
if shape.has_text_frame:
|
|
60
60
|
for line in shape.text_frame.text.splitlines():
|
|
61
|
+
|
|
61
62
|
t = line.strip()
|
|
62
63
|
if t and not NUMERIC_TOKEN.match(t):
|
|
63
64
|
texts.append(t)
|
|
@@ -193,6 +194,58 @@ def overflow_estimate(prs, name):
|
|
|
193
194
|
return issues
|
|
194
195
|
|
|
195
196
|
|
|
197
|
+
# ── R6 跨通道一致性(Agenda 阅读顺序 / 元素数量 / 图表数据标签) ──
|
|
198
|
+
|
|
199
|
+
def agenda_order(prs) -> list[str]:
|
|
200
|
+
"""大纲页条目阅读顺序(第 2 页,按 y 再 x 排序的短文本序列)。"""
|
|
201
|
+
if len(prs.slides) < 2:
|
|
202
|
+
return []
|
|
203
|
+
items = []
|
|
204
|
+
for shape in prs.slides[1].shapes:
|
|
205
|
+
if not shape.has_text_frame:
|
|
206
|
+
continue
|
|
207
|
+
t = shape.text_frame.text.strip()
|
|
208
|
+
if not t or len(t) > 60:
|
|
209
|
+
continue
|
|
210
|
+
# 序号行(01/02…)或短标题——取前 20 个稳定 token 作顺序指纹
|
|
211
|
+
items.append((round(shape.top / 914400, 1), round(shape.left / 914400, 1), t[:24]))
|
|
212
|
+
items.sort()
|
|
213
|
+
return [t for _, _, t in items[:20]]
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def element_counts(prs) -> list[tuple[int, int, int]]:
|
|
217
|
+
"""每页 (shapes, pictures, charts) 数量——跨通道应对齐(A 形状近似 vs B 原生图表可有 1 图差)。"""
|
|
218
|
+
out = []
|
|
219
|
+
for slide in prs.slides:
|
|
220
|
+
shapes = sum(1 for s in slide.shapes if s.shape_type is not None)
|
|
221
|
+
pics = sum(1 for s in slide.shapes if s.shape_type == 13) # PICTURE
|
|
222
|
+
charts = sum(1 for s in slide.shapes if getattr(s, 'has_chart', False))
|
|
223
|
+
out.append((shapes, pics, charts))
|
|
224
|
+
return out
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def chart_label_presence(prs) -> list[int]:
|
|
228
|
+
"""含图表的页是否有数据标签/数值文本(防「图表无标签」静默退化)。"""
|
|
229
|
+
flagged = []
|
|
230
|
+
for i, slide in enumerate(prs.slides, 1):
|
|
231
|
+
has_chart = any(getattr(s, 'has_chart', False) for s in slide.shapes)
|
|
232
|
+
if not has_chart:
|
|
233
|
+
continue
|
|
234
|
+
has_numeric = False
|
|
235
|
+
for s in slide.shapes:
|
|
236
|
+
if s.has_text_frame:
|
|
237
|
+
for line in s.text_frame.text.splitlines():
|
|
238
|
+
if NUMERIC_TOKEN.match(line.strip()):
|
|
239
|
+
has_numeric = True
|
|
240
|
+
break
|
|
241
|
+
if has_numeric:
|
|
242
|
+
break
|
|
243
|
+
# 原生 chart part 的数据在 chart XML 里(不进 text_frame)——只对形状通道严格
|
|
244
|
+
if not has_numeric and not any(getattr(s, 'has_chart', False) for s in slide.shapes):
|
|
245
|
+
flagged.append(i)
|
|
246
|
+
return flagged
|
|
247
|
+
|
|
248
|
+
|
|
196
249
|
def main() -> int:
|
|
197
250
|
if Presentation is None:
|
|
198
251
|
print('python-pptx 未安装')
|
|
@@ -270,7 +323,37 @@ def main() -> int:
|
|
|
270
323
|
fails.append(f'{name}: B 通道出现 A 没有的字号 {alien}'
|
|
271
324
|
f'(两通道应同走 modeTypeScale;modeSize() 与 sz() 的钳制/取整须一致)')
|
|
272
325
|
sz_note = ' 字号比例尺=' + ('PASS' if not alien else 'DRIFT')
|
|
273
|
-
|
|
326
|
+
# ⑥ 跨通道一致性:Agenda 阅读顺序 / 元素数量 / 图表数据标签
|
|
327
|
+
ao_a, ao_b = agenda_order(prs_a), agenda_order(prs_b)
|
|
328
|
+
ag_note = ''
|
|
329
|
+
if ao_a and ao_b and ao_a != ao_b:
|
|
330
|
+
fails.append(f'{name}: Agenda 阅读顺序不一致 A={ao_a[:4]}… B={ao_b[:4]}…')
|
|
331
|
+
ag_note = ' Agenda序=DRIFT'
|
|
332
|
+
elif ao_a and ao_b:
|
|
333
|
+
ag_note = ' Agenda序=PASS'
|
|
334
|
+
ec_a, ec_b = element_counts(prs_a), element_counts(prs_b)
|
|
335
|
+
# A/B 图表实现路径不同(形状近似 vs 原生/形状),形状总数不可比。
|
|
336
|
+
# 只对图片数(应精确一致)做硬比对——图片是同一份素材,数量错=内容丢失。
|
|
337
|
+
cnt_diff = []
|
|
338
|
+
for i, ((sa, pa, ca), (sb, pb, cb)) in enumerate(zip(ec_a, ec_b), 1):
|
|
339
|
+
if pa != pb:
|
|
340
|
+
cnt_diff.append((i, f'pics A={pa} B={pb}'))
|
|
341
|
+
cnt_note = ''
|
|
342
|
+
if cnt_diff:
|
|
343
|
+
fails.append(f'{name}: 图片数量不一致 {cnt_diff[:3]}(同一模型应同数)')
|
|
344
|
+
cnt_note = ' 图片数=DRIFT'
|
|
345
|
+
else:
|
|
346
|
+
cnt_note = ' 图片数=PASS'
|
|
347
|
+
lab_a = chart_label_presence(prs_a)
|
|
348
|
+
lab_b = chart_label_presence(prs_b)
|
|
349
|
+
lab_note = ''
|
|
350
|
+
if lab_a or lab_b:
|
|
351
|
+
fails.append(f'{name}: 图表缺数据标签 A页{lab_a} B页{lab_b}(形状通道须附数值)')
|
|
352
|
+
lab_note = ' 图表标签=MISS'
|
|
353
|
+
else:
|
|
354
|
+
lab_note = ' 图表标签=PASS'
|
|
355
|
+
print(f'{name}: A可解析✓ B可解析✓ 页数={len(ta)} 逐页文本={status}{chart_note}{sz_note}'
|
|
356
|
+
f'{ag_note}{cnt_note}{lab_note}')
|
|
274
357
|
|
|
275
358
|
print('\n── 溢出启发式(check-overflow · 18% 容差)──')
|
|
276
359
|
if all_overflow:
|
|
@@ -95,6 +95,11 @@ def validate_against_schema(model, schema):
|
|
|
95
95
|
warnings.append(f"章节页仅 {len(secs)} 页,正式报告建议 ≥3")
|
|
96
96
|
if len(ag) > ms.get('agendaComfortMax', 16):
|
|
97
97
|
warnings.append(f"agenda {len(ag)} 条超出单页舒适上限,建议拆分")
|
|
98
|
+
# R2:verdict 与 soWhat 共用 annotation 槽位——同页双填必叠印
|
|
99
|
+
for i, sec in enumerate(secs if isinstance(secs, list) else []):
|
|
100
|
+
if isinstance(sec, dict) and sec.get('verdict') and sec.get('soWhat'):
|
|
101
|
+
warnings.append(
|
|
102
|
+
f"sections[{i}] 同时填写 verdict 与 soWhat(共用结论条槽位,渲染会叠印)——请二选一")
|
|
98
103
|
return missing, warnings
|
|
99
104
|
|
|
100
105
|
|
|
@@ -835,8 +835,47 @@
|
|
|
835
835
|
"preferCoreFirst": true,
|
|
836
836
|
"corePreferMin": 3
|
|
837
837
|
},
|
|
838
|
+
"fidelityMap": {
|
|
839
|
+
"$comment": "PPTX 还原度分级:high=双通道等价(原生图表或简单形状);medium=形状还原可读但视觉降级;low=复杂信息图在 PPTX 显著退化(面积图变实心楔形、桑基/树图/流带近似),建议改 bar/line/hbar 或强制 dataTable=inline。validate_report 对 low 图型发 WARN。",
|
|
840
|
+
"bar": "high",
|
|
841
|
+
"hbar": "high",
|
|
842
|
+
"stack": "high",
|
|
843
|
+
"stackline": "high",
|
|
844
|
+
"line": "high",
|
|
845
|
+
"dualline": "high",
|
|
846
|
+
"donut": "high",
|
|
847
|
+
"multidonut": "high",
|
|
848
|
+
"pie": "high",
|
|
849
|
+
"scatter": "high",
|
|
850
|
+
"bubble": "high",
|
|
851
|
+
"waterfall": "high",
|
|
852
|
+
"gauge": "high",
|
|
853
|
+
"pareto": "high",
|
|
854
|
+
"progress": "high",
|
|
855
|
+
"vsbar": "high",
|
|
856
|
+
"sparkline": "high",
|
|
857
|
+
"funnel": "medium",
|
|
858
|
+
"slope": "medium",
|
|
859
|
+
"dumbbell": "medium",
|
|
860
|
+
"lollipop": "medium",
|
|
861
|
+
"dotplot": "medium",
|
|
862
|
+
"bulletchart": "medium",
|
|
863
|
+
"waffle": "medium",
|
|
864
|
+
"radialbar": "medium",
|
|
865
|
+
"rose": "medium",
|
|
866
|
+
"gantt": "medium",
|
|
867
|
+
"candlestick": "medium",
|
|
868
|
+
"area": "low",
|
|
869
|
+
"radar": "low",
|
|
870
|
+
"treemap": "low",
|
|
871
|
+
"sankey": "low",
|
|
872
|
+
"streamgraph": "low",
|
|
873
|
+
"marimekko": "low",
|
|
874
|
+
"boxplot": "low",
|
|
875
|
+
"network": "low"
|
|
876
|
+
},
|
|
838
877
|
"registry": {
|
|
839
|
-
"$comment": "图表登记四元组:pptx=native 走 addChart(真 chart part + 内嵌 Excel,双击可编辑数据);pptx=shape 走 OOXML 原生形状/SVG path 高保真还原,且**必须**按 dataTable 策略附数据表保证数据可追溯;nativeTrick = 用原生图表技巧实现的类型;path = 形状还原的几何约束(采样点下限 / 双边界追踪 / 禁预设形状)。build_pptx.js 与 pptx-export.js 双引擎按本表分派,validate_pptx.py 按本表断言。",
|
|
878
|
+
"$comment": "图表登记四元组:pptx=native 走 addChart(真 chart part + 内嵌 Excel,双击可编辑数据);pptx=shape 走 OOXML 原生形状/SVG path 高保真还原,且**必须**按 dataTable 策略附数据表保证数据可追溯;nativeTrick = 用原生图表技巧实现的类型;path = 形状还原的几何约束(采样点下限 / 双边界追踪 / 禁预设形状)。build_pptx.js 与 pptx-export.js 双引擎按本表分派,validate_pptx.py 按本表断言。PPTX 还原度分级见 charts.fidelityMap。",
|
|
840
879
|
"bar": {
|
|
841
880
|
"html": "svg-bars",
|
|
842
881
|
"pptx": "native",
|
|
@@ -1307,9 +1346,10 @@
|
|
|
1307
1346
|
"judgmentPattern": "\\d|[二两三四五六七八九十半]|一(?!览|体|致|直|些|般|系列|同|起|方面)|是(?!否)|应(?!用|急|对|届)|需(?!求)|将(?!来|近)|增|降|优先|瓶颈|关键|高于|低于|快于|慢于|第一|唯一|从.{1,12}到|翻倍|过半|不足|领先|落后|卡在|分化|出现|成为|仍|并非|必须|才能|才是|最大|最浅|最深|马当先|明显|尚在|已(?!知)|提升|贡献|占比|领跑|先行|滞后|分野|显著|差异|穿越|兑现|放量|[::—–]"
|
|
1308
1347
|
},
|
|
1309
1348
|
"agenda": {
|
|
1310
|
-
"$comment": "大纲条目数阈值:≤singleMax 单列;>singleMax 须 .agenda--2col(两列/两排);>splitMax 拆上篇/下篇或只列一级章节。scaffold / validate_report 消费。",
|
|
1349
|
+
"$comment": "大纲条目数阈值:≤singleMax 单列;>singleMax 须 .agenda--2col(两列/两排);>splitMax 拆上篇/下篇或只列一级章节。titleMaxChars = 单条标题字数上限(超长截断+全称沉 notes)。scaffold / validate_report 消费。",
|
|
1311
1350
|
"singleMax": 8,
|
|
1312
1351
|
"splitMax": 16,
|
|
1352
|
+
"titleMaxChars": 36,
|
|
1313
1353
|
"twoColClass": "agenda--2col"
|
|
1314
1354
|
},
|
|
1315
1355
|
"pageHeight": {
|
|
@@ -312,10 +312,13 @@ function regionOf(pageType, slotId, opts) {
|
|
|
312
312
|
if (type === 'image') {
|
|
313
313
|
if (slot === 'primary') {
|
|
314
314
|
const y = Math.max(PT.y || 2.3, (opts && opts.top) || 0);
|
|
315
|
-
|
|
316
|
-
|
|
315
|
+
/* R2:有图注时图片不得越过 contentBottomWithNote(图注本身落在注释带,属 annotation self)。
|
|
316
|
+
否则图片底边(如 6.58)会被判「主内容侵入注释带」。 */
|
|
317
|
+
const hasCap = !!(opts && opts.caption);
|
|
318
|
+
const bottom = (opts && opts.bottom != null) ? opts.bottom
|
|
319
|
+
: (hasCap ? L.contentBottomWithNote : L.contentBottom);
|
|
317
320
|
return {
|
|
318
|
-
x: mx, y, w: cw, h: Math.max(1.2, Math.min(PT.h || 4.2, bottom - y
|
|
321
|
+
x: mx, y, w: cw, h: Math.max(1.2, Math.min(PT.h || 4.2, bottom - y)),
|
|
319
322
|
halfW: PT.halfW, halfGap: PT.halfGap, capY: PT.capY, capH: PT.capH || 0.3,
|
|
320
323
|
radius: PT.radius, gridGap: PT.gridGap, gridCols: PT.gridCols,
|
|
321
324
|
};
|
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
"exhibitNo",
|
|
42
42
|
"layoutPreset"
|
|
43
43
|
],
|
|
44
|
+
"mutualExclusionHint": "verdict 与 soWhat 互斥:二者共用 annotation 结论条槽位,同页双填必叠印。comparison 页填 verdict 时不要再填 soWhat(渲染器已自动二选一,但模型侧应只给一个)。",
|
|
44
45
|
"flagsHint": "flags = 待核实/待二次修改条目字符串数组(如 '2027 增速 xx%(口径未定)');渲染为 accent 强调色标注行,提示用户核对。",
|
|
45
46
|
"imageHint": "image = 素材图片/配图占位对象。三选一必填:src(用户图,data: 内联或相对路径,零外链铁律禁 http(s))|items(多图版式:grid/compare/wall,元素 {src?, alt?, caption?, placeholder?})|placeholder:true(无图时出配图占位,原生形状渲染、pictures 不增)。其余可选:layout(full/half/bleed/grid/compare/wall,见 layout-constants.json imageSpec.layouts)、fit(cover 默认 / contain)、caption、alt、hint、ratio。",
|
|
46
47
|
"imageLayoutHint": "layout 与 HTML 版式对应:full=版心全宽图|half=左图右注(points 为右栏注解)|bleed=通栏出血|grid=多图网格(items 2/3/4/6 张)|compare=双图 A/B 对比(items 2 张)|wall=Logo 墙(items 3–12 张小图)。占位符标签文本由双引擎按 imageSpec 拼同一串,保证 A/B 通道逐页文本一致。",
|