dsh-ppt-studio 1.1.5 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -1
- package/docs/00-/350/256/276/350/256/241/346/272/220.md +4 -0
- package/docs/02-/346/212/200/346/234/257/346/212/245/345/221/212.md +114 -1
- package/docs/03-/346/233/264/346/226/260/346/227/245/345/277/227.md +149 -0
- package/docs/05-/350/277/255/344/273/243/346/265/201/347/250/213.md +23 -0
- package/docs/06-/350/257/204/345/256/241/344/270/216/346/265/213/350/257/225.md +2 -1
- package/docs/07-/345/244/215/346/235/202/345/233/276/350/203/275/345/212/233/347/253/213/351/241/271/344/270/216/351/243/216/351/231/251/347/231/273/350/256/260.md +319 -0
- package/docs/08-/351/230/266/346/256/265A/350/256/276/350/256/241/350/257/264/346/230/216-/347/273/223/346/236/204/345/205/263/347/263/273/344/270/216/345/233/276/344/270/223/345/261/236/346/240/241/351/252/214.md +306 -0
- package/docs/09-/344/273/243/347/240/201/345/234/260/345/233/276/344/270/216/346/250/241/345/235/227/350/201/214/350/264/243.md +148 -0
- package/docs/10-/345/256/277/344/270/273/350/200/246/345/220/210/347/202/271/346/270/205/345/215/225.md +49 -0
- package/docs/11-/346/234/257/350/257/255/350/241/250.md +42 -0
- package/docs/12-/345/244/215/346/235/202/345/233/276/351/200/232/350/267/257/347/232/204/351/200/211/346/213/251/344/270/216/351/252/214/346/224/266.md +65 -0
- package/docs/13-/345/217/221/347/211/210/345/207/206/345/244/207/350/256/241/345/210/222.md +79 -0
- package/docs/14-/345/217/221/345/270/203/346/270/205/345/215/225/357/274/210/350/215/211/347/250/277/357/274/211.md +37 -0
- package/docs/17-/346/211/213/347/224/27313/351/241/265/350/255/246/345/221/212/351/200/220/346/235/241/345/244/204/347/275/256.md +102 -0
- package/examples/fx/fx-pro/preview/layout.json +12 -0
- package/examples/fx/fx-pro/preview/measured.json +1 -1
- package/examples/fx/fx-pro.pptx +0 -0
- package/examples/fx/seed.pptx +0 -0
- package/examples/fx/single.pptx +0 -0
- package/examples/fx/spliced.pptx +0 -0
- package/examples/smoke/imported/preview-rt/layout.json +3 -0
- package/examples/smoke/imported/source.pptx +0 -0
- package/examples/smoke/out-smoke.pptx +0 -0
- package/examples/smoke/preview/layout.json +3 -0
- package/lib/group-render.js +177 -0
- package/lib/msrender.js +27 -12
- package/lib/pptd/blocks.js +52 -0
- package/lib/pptd/export-pptx.js +266 -11
- package/lib/pptd/layout.js +54 -3
- package/lib/pptd/line-rules.js +259 -0
- package/lib/pptd/line-text-rule.js +90 -0
- package/lib/pptd/relations.js +615 -0
- package/lib/pptd/render-html.js +19 -1
- package/lib/pptd/route-geometry.js +120 -0
- package/lib/pptd/schema.js +146 -11
- package/lib/pptd/style-profile.js +133 -0
- package/lib/scaffold.js +54 -1
- package/lib/tools.js +20 -2
- package/lib/verify.js +69 -1
- package/package.json +20 -3
- package/scripts/audit-manual-facts.mjs +15 -3
- package/scripts/bench-diagram-scale.mjs +165 -0
- package/scripts/check-generators.mjs +22 -0
- package/scripts/check-lib-sync.mjs +33 -0
- package/scripts/check-line-text.mjs +30 -0
- package/scripts/check-package.mjs +60 -0
- package/scripts/check-removed-traces.mjs +37 -0
- package/scripts/check-syntax.mjs +44 -0
- package/scripts/check-unknown-keys.mjs +114 -0
- package/scripts/fixtures/handdrawn-baseline.json +177 -0
- package/scripts/fixtures/prompt-cost-baseline.json +178 -0
- package/scripts/handdrawn-option-probe.mjs +78 -0
- package/scripts/probe-prompt-cost.mjs +127 -0
- package/scripts/red-probe.mjs +75 -0
- package/scripts/render-group.mjs +72 -0
- package/scripts/smoke.mjs +183 -12
- package/scripts/test-fast.mjs +108 -0
- package/scripts/test-map.mjs +162 -0
- package/scripts/test-release.mjs +64 -0
- package/scripts/verify-diagram-relations.mjs +405 -0
- package/scripts/verify-handdrawn.mjs +115 -0
- package/scripts/verify-handwritten.mjs +203 -0
- package/scripts/verify-primitives.mjs +714 -0
- package/scripts/verify-products.mjs +37 -0
- package/scripts/verify-shape.mjs +84 -0
- package/scripts/visual-pair-batch.mjs +77 -0
- package/skills/diagram-drawing/SKILL.md +23 -0
- package/skills/diagram-drawing/capability-matrix.md +22 -0
- package/skills/diagram-drawing/composition.md +22 -0
- package/skills/diagram-drawing/mechanisms.md +141 -0
- package/skills/diagram-drawing/pitfalls.md +89 -0
- package/skills/diagram-drawing/routing.md +17 -0
- package/skills/diagram-drawing/text-and-spacing.md +16 -0
- package/skills/diagram-drawing/verify.md +50 -0
- package/skills/diagrams/compare.md +24 -0
- package/skills/diagrams/flow.md +24 -0
- package/skills/diagrams/funnel.md +25 -0
- package/skills/diagrams/layers.md +18 -0
- package/skills/diagrams/loop.md +25 -0
- package/skills/diagrams/matrix.md +23 -0
- package/skills/diagrams/sequence.md +17 -0
- package/skills/diagrams/state.md +34 -0
- package/skills/diagrams/steps-ring.md +24 -0
- package/skills/diagrams/swimlane.md +23 -0
- package/skills/diagrams/timeline.md +24 -0
- package/skills/diagrams/tree.md +24 -0
- package/skills/ppt-studio-manual/SKILL.md +2 -2
- package/templates/academic-white/preview.png +0 -0
- package/templates/business-blue/preview.png +0 -0
- package/templates/pitch-bold/preview.png +0 -0
- package/templates/tech-dark/preview.png +0 -0
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* 手写路径自证(阶段 A3,泛用性优先):`node scripts/verify-handwritten.mjs`
|
|
4
|
+
*
|
|
5
|
+
* 为什么要有这个脚本:族库再全,也总有"不属于任何族"的图;那时助手的唯一手段是**手写 elements**。
|
|
6
|
+
* 本脚本用**合成夹具**证明手写路径是一等公民,而且手写也有机械反馈:
|
|
7
|
+
* ① 一张合法的"手写复杂图"(分组底板 decoration + 容器 contains + >2 点折线 + attach + 标签 + 虚线反馈回路)
|
|
8
|
+
* ⇒ **0 错误**,且折线规则**不误报**,导出 parity 自证通过;
|
|
9
|
+
* ② 斜段连接线 ⇒ 报 `line-diagonal-segment`(提示怎么改);
|
|
10
|
+
* ③ 箭头端直段过短 ⇒ 报 `arrow-end-short-run`;
|
|
11
|
+
* ④ 刻意的斜线(环形/放射)用 `role: decoration` 声明 ⇒ **豁免、不再提示**。
|
|
12
|
+
* 全部断言 0 失败则退出 0。
|
|
13
|
+
*/
|
|
14
|
+
import { mkdirSync, writeFileSync, rmSync, readFileSync } from 'node:fs'
|
|
15
|
+
import { join } from 'node:path'
|
|
16
|
+
import { tmpdir } from 'node:os'
|
|
17
|
+
import { resolveDeck } from '../lib/pptd/schema.js'
|
|
18
|
+
import { renderDeck } from '../lib/pptd/render-html.js'
|
|
19
|
+
import { exportPptx } from '../lib/pptd/export-pptx.js'
|
|
20
|
+
import { verifyDeck } from '../lib/verify.js'
|
|
21
|
+
import { checkLineRules } from '../lib/pptd/line-rules.js'
|
|
22
|
+
import { renderGroupIsolated } from '../lib/group-render.js'
|
|
23
|
+
|
|
24
|
+
let pass = 0
|
|
25
|
+
let fail = 0
|
|
26
|
+
const failures = []
|
|
27
|
+
const ok = (name, cond, detail = '') => {
|
|
28
|
+
if (cond) { pass++; console.log(`✓ ${name}${detail ? ` — ${detail}` : ''}`) }
|
|
29
|
+
else { fail++; failures.push(name); console.log(`✗ ${name}${detail ? ` — ${detail}` : ''}`) }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const dir = join(tmpdir(), `pptd-handwritten-${Date.now()}`)
|
|
33
|
+
rmSync(dir, { recursive: true, force: true })
|
|
34
|
+
mkdirSync(join(dir, 'pages'), { recursive: true })
|
|
35
|
+
writeFileSync(join(dir, 'deck.yaml'), [
|
|
36
|
+
'version: 1', 'title: 手写自由发挥', 'size: [960, 540]', 'theme:',
|
|
37
|
+
// 暖色系(顺便验证"指定色系"这条泛用性诉求)
|
|
38
|
+
' colors: {primary: "#B45309", accent: "#EA580C", soft: "#FED7AA", text: "#431407", bg: "#FFFBEB"}',
|
|
39
|
+
' textStyles:', ' body: {fontSize: 14, color: "$text"}', ' line: {width: 2}',
|
|
40
|
+
' safeArea: {top: 40, bottom: 40, left: 40, right: 40}',
|
|
41
|
+
'pages:', ' - pages/01.yaml', ' - pages/02.yaml', ' - pages/03.yaml', ' - pages/04.yaml', ' - pages/05.yaml', '',
|
|
42
|
+
].join('\n'))
|
|
43
|
+
|
|
44
|
+
// ① 合法的手写复杂图(完全不依赖任何族)
|
|
45
|
+
writeFileSync(join(dir, 'pages', '01.yaml'), [
|
|
46
|
+
'pageType: content', 'elements:',
|
|
47
|
+
' - {elementId: band, elementType: shape, kind: roundRect, bounds: [60, 90, 840, 150], fill: "$soft", role: decoration, roleReason: 分组底板}',
|
|
48
|
+
' - {elementId: a, elementType: shape, kind: roundRect, bounds: [90, 120, 180, 90], fill: "$primary", contains: [a_lab]}',
|
|
49
|
+
' - {elementId: a_lab, elementType: text, bounds: [100, 150, 160, 30], content: {text: "采集", fontSize: 16, color: "$bg", align: center}}',
|
|
50
|
+
' - {elementId: b, elementType: shape, kind: roundRect, bounds: [390, 120, 180, 90], fill: "$primary", contains: [b_lab]}',
|
|
51
|
+
' - {elementId: b_lab, elementType: text, bounds: [400, 150, 160, 30], content: {text: "仿真", fontSize: 16, color: "$bg", align: center}}',
|
|
52
|
+
' - {elementId: c, elementType: shape, kind: roundRect, bounds: [690, 120, 180, 90], fill: "$accent", contains: [c_lab]}',
|
|
53
|
+
' - {elementId: c_lab, elementType: text, bounds: [700, 150, 160, 30], content: {text: "输出", fontSize: 16, color: "$bg", align: center}}',
|
|
54
|
+
' - elementId: e1', ' elementType: line',
|
|
55
|
+
' points: [[270, 165], [330, 165], [330, 165], [390, 165]]', ' arrow: true',
|
|
56
|
+
' attach: {from: {ref: a, side: right}, to: {ref: b, side: left}}',
|
|
57
|
+
' - elementId: e2', ' elementType: line',
|
|
58
|
+
' points: [[570, 165], [630, 165], [630, 165], [690, 165]]', ' arrow: true',
|
|
59
|
+
' attach: {from: {ref: b, side: right}, to: {ref: c, side: left}}',
|
|
60
|
+
' - elementId: fb', ' elementType: line',
|
|
61
|
+
' points: [[240, 210], [240, 300], [750, 300], [750, 210]]', ' arrow: true', ' line: {color: "$text", width: 1, dash: dash}',
|
|
62
|
+
' attach: {from: {ref: a, side: bottom}, to: {ref: c, side: bottom}}',
|
|
63
|
+
' - {elementId: fb_lab, elementType: text, bounds: [455, 305, 90, 20], content: {text: "迭代反馈", fontSize: 13, color: "$text", align: center}}',
|
|
64
|
+
'',
|
|
65
|
+
].join('\n'))
|
|
66
|
+
|
|
67
|
+
// ② 坏折线:斜段 + 箭头端过短
|
|
68
|
+
writeFileSync(join(dir, 'pages', '02.yaml'), [
|
|
69
|
+
'pageType: content', 'elements:',
|
|
70
|
+
' - {elementId: box, elementType: shape, kind: rect, bounds: [80, 120, 200, 90], fill: "$soft"}',
|
|
71
|
+
' - elementId: bad_diag', ' elementType: line', ' points: [[600, 120], [800, 260]]', ' arrow: true',
|
|
72
|
+
' - elementId: bad_short', ' elementType: line', ' points: [[300, 300], [515, 300], [520, 300]]', ' arrow: true',
|
|
73
|
+
'',
|
|
74
|
+
].join('\n'))
|
|
75
|
+
|
|
76
|
+
// ③ 刻意斜线(环形语义)用 decoration 声明 ⇒ 应豁免
|
|
77
|
+
writeFileSync(join(dir, 'pages', '03.yaml'), [
|
|
78
|
+
'pageType: content', 'elements:',
|
|
79
|
+
' - elementId: ring_a', ' elementType: line', ' points: [[200, 200], [400, 350]]', ' arrow: false',
|
|
80
|
+
' role: decoration', ' roleReason: 环形引导线的斜弦(设计意图)',
|
|
81
|
+
'',
|
|
82
|
+
].join('\n'))
|
|
83
|
+
|
|
84
|
+
// ④ 新判据的正例与反例(用户裁定的区分:共线重叠/压框 = 缺陷;共用端点的扇出 = 标准画法)
|
|
85
|
+
writeFileSync(join(dir, 'pages', '04.yaml'), [
|
|
86
|
+
'pageType: content', 'elements:',
|
|
87
|
+
// 正例①:互不相关的两条线走成共线重叠 ⇒ 必须报 line-collinear-overlap
|
|
88
|
+
' - elementId: ov1', ' elementType: line', ' points: [[300, 380], [600, 380]]', ' arrow: false',
|
|
89
|
+
' - elementId: ov2', ' elementType: line', ' points: [[400, 380], [700, 380]]', ' arrow: false',
|
|
90
|
+
// 正例②:线**压在盒子边框上** ⇒ 必须报 line-on-box-edge
|
|
91
|
+
' - {elementId: edgebox, elementType: shape, kind: rect, bounds: [200, 150, 300, 120], fill: "$soft"}',
|
|
92
|
+
' - elementId: onedge', ' elementType: line', ' points: [[200, 150], [500, 150]]', ' arrow: false',
|
|
93
|
+
// 反例:从**同一锚点**出发、共线且部分重合的一对线(扇出/树状干线)⇒ 不得报共线重叠
|
|
94
|
+
' - elementId: fan_long', ' elementType: line', ' points: [[700, 430], [800, 430]]', ' arrow: false',
|
|
95
|
+
// R3 正反例:箭头插进图形(该报);同样几何但 roleReason 声明(不该报)
|
|
96
|
+
' - elementId: ov_bad_in', ' elementType: line', ' points: [[340, 210], [390, 210]]', ' arrow: true',
|
|
97
|
+
' - elementId: ov_declared', ' elementType: line', ' points: [[340, 230], [390, 230]]', ' arrow: true', ' roleReason: 强调贯穿(刻意插进图形)',
|
|
98
|
+
// R3 缺口正例:端点离图形边 ~6px(该报)
|
|
99
|
+
' - elementId: ov_gap', ' elementType: line', ' points: [[560, 210], [572, 210]]', ' arrow: true',
|
|
100
|
+
' - {elementId: gapbox, elementType: shape, kind: rect, bounds: [578, 150, 200, 120], fill: "$soft"}',
|
|
101
|
+
// X2 正反例:折返尖刺(该报)与其声明版(不该报)
|
|
102
|
+
' - elementId: ov_spike', ' elementType: line', ' points: [[300, 470], [400, 470], [350, 470]]', ' arrow: false',
|
|
103
|
+
' - elementId: ov_spike_declared', ' elementType: line', ' points: [[300, 495], [400, 495], [350, 495]]', ' arrow: false', ' roleReason: 刻意的往返重叠',
|
|
104
|
+
// X2 正例:**无箭头**连线端点离盒子 ~6px(该报 line-end-off-edge)
|
|
105
|
+
' - elementId: ov_noarrow_gap', ' elementType: line', ' points: [[560, 230], [572, 230]]', ' arrow: false',
|
|
106
|
+
'',
|
|
107
|
+
].join('\n'))
|
|
108
|
+
|
|
109
|
+
// ⑤ 组级裁剪取景(C2 的核心不变量):容器与"容器内部的组标题"必须被纳入,
|
|
110
|
+
// 离得远的旁观元素必须被排除——这条规则第一版写错过(8 vs 10),用断言锁死。
|
|
111
|
+
writeFileSync(join(dir, 'pages', '05.yaml'), [
|
|
112
|
+
'pageType: content', 'groups:',
|
|
113
|
+
' - {id: g_card, label: 卡片组, members: [c_shape, c_lab]}',
|
|
114
|
+
'elements:',
|
|
115
|
+
' - {elementId: c_shape, elementType: shape, kind: roundRect, bounds: [120, 300, 200, 90], fill: "$primary", contains: [c_lab]}',
|
|
116
|
+
' - {elementId: c_lab, elementType: text, bounds: [130, 330, 180, 30], content: {text: "卡内文字", fontSize: 14, color: "$bg", align: center}}',
|
|
117
|
+
// 容器:contains 里有"几何上落在它内部"的标题(不在组 members 里)——必须靠 ③ 被纳入
|
|
118
|
+
' - {elementId: c_box, elementType: shape, kind: roundRect, bounds: [100, 280, 240, 140], fill: "$soft", role: decoration, roleReason: 卡片底板, contains: [c_shape, c_lab, c_title]}',
|
|
119
|
+
' - {elementId: c_title, elementType: text, bounds: [110, 288, 220, 22], content: {text: "卡片标题", fontSize: 13, color: "$text", align: center}}',
|
|
120
|
+
// 旁观者:离得远,不得被吞进取景
|
|
121
|
+
' - {elementId: far_away, elementType: shape, kind: rect, bounds: [700, 470, 160, 40], fill: "$accent"}',
|
|
122
|
+
'',
|
|
123
|
+
].join('\n'))
|
|
124
|
+
|
|
125
|
+
const ctx = await resolveDeck(dir)
|
|
126
|
+
await renderDeck(ctx, { out: 'preview' })
|
|
127
|
+
const layout = JSON.parse(readFileSync(join(dir, 'preview', 'layout.json'), 'utf8'))
|
|
128
|
+
const vOf = (i) => verifyDeck({ ...layout, pages: [layout.pages[i]] })
|
|
129
|
+
|
|
130
|
+
const v1 = vOf(0)
|
|
131
|
+
ok('手写复杂图(非任何族):**0 错误**(容器 contains + decoration 底板 + >2 点折线 + attach + 标签)',
|
|
132
|
+
v1.errors.length === 0, v1.errors.slice(0, 3).map((e) => `${e.code}:${e.id}`).join(' | ') || '无')
|
|
133
|
+
ok('手写复杂图:折线规则**不误报**(箭头端直段够长、全轴对齐)',
|
|
134
|
+
!v1.warns.some((w) => w.code === 'line-diagonal-segment' || w.code === 'arrow-end-short-run'),
|
|
135
|
+
v1.warns.map((w) => w.code).join(',') || '无相关警告')
|
|
136
|
+
|
|
137
|
+
const v2 = vOf(1)
|
|
138
|
+
ok('斜段连接线 ⇒ 报 `line-diagonal-segment`(带可操作的修法)',
|
|
139
|
+
v2.warns.some((w) => w.code === 'line-diagonal-segment' && /role: decoration/.test(w.message)),
|
|
140
|
+
v2.warns.map((w) => w.code).join(',') || '无')
|
|
141
|
+
ok('箭头端直段过短(20px 内的拐点)⇒ 报 `arrow-end-short-run`',
|
|
142
|
+
v2.warns.some((w) => w.code === 'arrow-end-short-run'), v2.warns.map((w) => w.code).join(',') || '无')
|
|
143
|
+
|
|
144
|
+
const v3 = vOf(2)
|
|
145
|
+
ok('刻意斜线用 `role: decoration` 声明 ⇒ **豁免**(不再提示,但仍是 warning 级、不阻断)',
|
|
146
|
+
!v3.warns.some((w) => w.code === 'line-diagonal-segment'), v3.warns.map((w) => w.code).join(',') || '无相关警告')
|
|
147
|
+
|
|
148
|
+
const v4 = vOf(3)
|
|
149
|
+
ok('互不相关的两条线**共线重叠** ⇒ 报 `line-collinear-overlap`(真缺陷判据)',
|
|
150
|
+
v4.warns.some((w) => w.code === 'line-collinear-overlap' && /ov1/.test(w.message) && /ov2/.test(w.message)),
|
|
151
|
+
v4.warns.map((w) => w.code).join(',') || '无')
|
|
152
|
+
ok('线**压在盒子边框上** ⇒ 报 `line-on-box-edge`(图 4 那类缺陷的机械判据)',
|
|
153
|
+
v4.warns.some((w) => w.code === 'line-on-box-edge'), v4.warns.map((w) => w.code).join(',') || '无')
|
|
154
|
+
ok('**共用端点**的扇出(标准树状干线)⇒ 不得报共线重叠(用户裁定:这样画更清晰)',
|
|
155
|
+
!v4.warns.some((w) => w.code === 'line-collinear-overlap' && /fan/.test(w.message)),
|
|
156
|
+
v4.warns.filter((w) => w.code === 'line-collinear-overlap').map((w) => w.message.slice(0, 46)).join(' | ') || '无')
|
|
157
|
+
|
|
158
|
+
// ⑥ 组级裁剪取景的不变量(C2)
|
|
159
|
+
const grp = await renderGroupIsolated(dir, { page: 5, group: 'g_card', out: join(dir, '.group-render') })
|
|
160
|
+
ok('组级取景:纳入容器与「容器内部的组标题」(C2 核心规则——第一版漏了这条,出 8 个元素而 CLI 出 10 个)',
|
|
161
|
+
['c_shape', 'c_lab', 'c_box', 'c_title'].every((id) => grp.members.includes(id)), grp.members.join(','))
|
|
162
|
+
ok('组级取景:不吞入离得远的旁观元素("闭包把整页拉进来"是这条规则要防的事)',
|
|
163
|
+
!grp.members.includes('far_away'), `${grp.members.length} 个元素`)
|
|
164
|
+
|
|
165
|
+
// R3:两条**只警告、可声明**的接合判据(用户明确要求:可能是意料之内的设计 ⇒ 绝不 error)
|
|
166
|
+
ok('箭头插进图形 ⇒ 报 `arrow-tip-inside-shape`(warning,不是错误)',
|
|
167
|
+
v4.warns.some((w) => w.code === 'arrow-tip-inside-shape' && /ov_bad_in/.test(w.message)),
|
|
168
|
+
v4.warns.filter((w) => w.code === 'arrow-tip-inside-shape').map((w) => w.message.slice(0, 30)).join(' | ') || '无')
|
|
169
|
+
ok('端点离目标 ~6px ⇒ 报 `arrow-end-gap`',
|
|
170
|
+
v4.warns.some((w) => w.code === 'arrow-end-gap' && /ov_gap/.test(w.message)),
|
|
171
|
+
v4.warns.filter((w) => w.code === 'arrow-end-gap').map((w) => w.message.slice(0, 30)).join(' | ') || '无')
|
|
172
|
+
ok('**声明刻意如此**(roleReason)⇒ 同样的几何**不报**(用户要求:意料之内不提示)',
|
|
173
|
+
!v4.warns.some((w) => w.code === 'arrow-tip-inside-shape' && /ov_declared/.test(w.message)),
|
|
174
|
+
v4.warns.map((w) => w.code).join(',') || '无警告')
|
|
175
|
+
|
|
176
|
+
// X2:尖刺与"没接上"两条门禁(只警告、可声明)
|
|
177
|
+
// 注意:管线在 normalizePage 末尾已**兜底修掉**折返尖刺 ⇒ 正常路径上页里看不到它,
|
|
178
|
+
// 所以这里直接测**规则函数**(保证"护栏本身有效";页级想验证请临时关掉兜底规整)。
|
|
179
|
+
ok('折返尖刺 ⇒ 规则函数报 `line-spike`(护栏有效性;页级已被兜底规整修掉)',
|
|
180
|
+
checkLineRules([{ elementId: 'sp', elementType: 'line', points: [[300, 470], [400, 470], [350, 470]], arrow: false }])
|
|
181
|
+
.some((w) => w.code === 'line-spike'), '')
|
|
182
|
+
ok('规则函数对**声明版**尖刺不报(可声明豁免)',
|
|
183
|
+
!checkLineRules([{ elementId: 'sp2', elementType: 'line', points: [[300, 470], [400, 470], [350, 470]], arrow: false, roleReason: '刻意往返重叠' }])
|
|
184
|
+
.some((w) => w.code === 'line-spike'), '')
|
|
185
|
+
ok('**声明刻意往返重叠** ⇒ 不报 line-spike',
|
|
186
|
+
!v4.warns.some((w) => w.code === 'line-spike' && /ov_spike_declared/.test(w.message)), '')
|
|
187
|
+
ok('**无箭头**连线端点离盒子 ~6px ⇒ 报 `line-end-off-edge`(与 arrow-end-gap 分工)',
|
|
188
|
+
v4.warns.some((w) => w.code === 'line-end-off-edge' && /ov_noarrow_gap/.test(w.message)),
|
|
189
|
+
v4.warns.filter((w) => w.code === 'line-end-off-edge').map((w) => w.message.slice(0, 30)).join(' | ') || '无')
|
|
190
|
+
|
|
191
|
+
// ⑥ 手写路径能出成品:导出 parity 自证
|
|
192
|
+
const r = await exportPptx(ctx, { out: join(dir, 'hand.pptx') })
|
|
193
|
+
ok('手写路径能正常出成品(导出 parity.ok=true,含虚线折线与附着线)',
|
|
194
|
+
r.parity?.ok === true, `attached ${r.parity?.attachedLinesOut}/${r.parity?.attachedLinesExp}|poly ${r.parity?.polyLinesOut}/${r.parity?.polyLinesExp}`)
|
|
195
|
+
|
|
196
|
+
// ⑤ 单元级:规则函数本身(DSL 原文与 preview 快照两种字段名都要认)
|
|
197
|
+
ok('规则函数:DSL 原文(elementType)与 preview 快照(kind)两条路径都认得出折线',
|
|
198
|
+
checkLineRules([{ elementId: 'x', elementType: 'line', points: [[0, 0], [100, 0], [105, 0]], arrow: true }]).some((w) => w.code === 'arrow-end-short-run')
|
|
199
|
+
&& checkLineRules([{ id: 'y', kind: 'line', points: [[0, 0], [100, 0], [105, 0]], arrow: true }]).some((w) => w.code === 'arrow-end-short-run'))
|
|
200
|
+
|
|
201
|
+
rmSync(dir, { recursive: true, force: true })
|
|
202
|
+
console.log(`\n==== verify-handwritten 结果:${pass} 通过 / ${fail} 失败 ====`)
|
|
203
|
+
if (fail) { console.log(`失败项:${failures.join(';')}`); process.exit(1) }
|