@lark-apaas/coding-steering 0.1.18-dev.e6a2787 → 0.1.18-dev.ec7b8c1
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/package.json +1 -1
- package/steering/design-html/skills/pptx-style-extract/SKILL.md +4 -3
- package/steering/design-html/skills/pptx-style-extract/scripts/check_v2.py +33 -3
- package/steering/design-html/skills/pptx-style-extract/scripts/draft.py +404 -82
- package/steering/design-html/skills/pptx-style-extract/scripts/package.py +146 -24
- package/steering/design-html/skills/pptx-style-extract/scripts/query.py +3 -8
- package/steering/design-html/skills/pptx-style-extract/scripts/test_color_contract.py +60 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/test_design_consumer_contract.py +63 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/test_flow_layout_contract.py +468 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/test_layout_css.py +98 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/test_rounded_contract.py +112 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/test_text_role_contract.py +168 -0
- package/steering/design-html/skills/pptx-style-extract/v2-format-spec.md +13 -6
- package/steering/design-html/skills/slide-deck/SKILL.md +15 -20
- package/steering/design-html/skills/slide-deck/scripts/check_local_references.py +179 -0
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
(package.py 见 TODO 即 FAIL),由 L 层改掉。
|
|
13
13
|
"""
|
|
14
14
|
import argparse
|
|
15
|
+
import copy
|
|
15
16
|
import json
|
|
16
17
|
import os
|
|
17
18
|
import re
|
|
@@ -264,9 +265,9 @@ OFFICE_DEFAULT_FONTS_NORM = {norm(x) for x in OFFICE_DEFAULT_FONTS}
|
|
|
264
265
|
|
|
265
266
|
|
|
266
267
|
def cover_slot_colors(tokens, archetypes, rows, cusage):
|
|
267
|
-
"""slot 里出现的每个色值都必须在色板里有名字。
|
|
268
|
+
"""slot CSS 里出现的每个色值都必须在色板里有名字。
|
|
268
269
|
|
|
269
|
-
Hard Rules 写「颜色只用 colors 里的 token」,而 slot 的 color 是从模板直读的,
|
|
270
|
+
Hard Rules 写「颜色只用 colors 里的 token」,而 slot CSS 的 color 是从模板直读的,
|
|
270
271
|
两者不对齐就等于产物自己违反自己的规则——slot 的色值直读自模板,未必都已进
|
|
271
272
|
色板。这里把缺的补进色板,按用法归族命名。
|
|
272
273
|
"""
|
|
@@ -286,7 +287,7 @@ def cover_slot_colors(tokens, archetypes, rows, cusage):
|
|
|
286
287
|
added = []
|
|
287
288
|
for a in archetypes:
|
|
288
289
|
for s in a['slots']:
|
|
289
|
-
h = (s.get('
|
|
290
|
+
h = (s.get('_color') or '').upper()
|
|
290
291
|
if not h.startswith('#') or h in have:
|
|
291
292
|
continue
|
|
292
293
|
have.add(h)
|
|
@@ -500,7 +501,8 @@ def draft_anchors(d, tokens, fonts, roles, assets, archetypes):
|
|
|
500
501
|
# 6. 层级:字号跨度 + 字重是否单一(字重真单一才敢说「不靠字重」)
|
|
501
502
|
disp, body = roles.get('display'), roles.get('body')
|
|
502
503
|
if disp and body and disp['sz_px'] > body['sz_px']:
|
|
503
|
-
ws = {s.get('
|
|
504
|
+
ws = {s.get('_font_weight') for a in archetypes for s in a['slots']
|
|
505
|
+
if s.get('_font_weight')}
|
|
504
506
|
tail = (',字重只用 %s 一档' % list(ws)[0]) if len(ws) == 1 else ''
|
|
505
507
|
A.append(('size-driven-hierarchy', 'pattern',
|
|
506
508
|
'最大字号档与正文档相差 %.1f 倍(见 typography)%s'
|
|
@@ -930,11 +932,23 @@ def slot_overlaps(slots):
|
|
|
930
932
|
return out
|
|
931
933
|
|
|
932
934
|
|
|
935
|
+
def css_number(value, digits=3):
|
|
936
|
+
"""CSS 数值稳定格式:整数不带小数,其余去掉无意义尾零。"""
|
|
937
|
+
number = round(float(value), digits)
|
|
938
|
+
if number == int(number):
|
|
939
|
+
return str(int(number))
|
|
940
|
+
return ('%.*f' % (digits, number)).rstrip('0').rstrip('.')
|
|
941
|
+
|
|
942
|
+
|
|
933
943
|
def slot_style(s):
|
|
934
|
-
"""
|
|
944
|
+
"""占位符自带的排版样式,统一转成可直接写进 HTML style 的 CSS 声明串。
|
|
935
945
|
|
|
936
946
|
样式可能在三层:lstStyle.lvl1pPr(版式占位符常用)、段落 defRPr(Mac Office
|
|
937
947
|
导出把大量属性写在这一层)、段落 pPr(对齐)。逐层兜底,缺一层就往下取。
|
|
948
|
+
|
|
949
|
+
`box` 是布局几何,继续由 slot 独立承载;其余渲染属性不再泄漏成 size / color /
|
|
950
|
+
align / insets_px 等 PPTX 中间字段。下划线开头的键仅供 draft 内部统计,emit_layouts
|
|
951
|
+
不会写进消费者产物。
|
|
938
952
|
"""
|
|
939
953
|
txt = s.get('text') or {}
|
|
940
954
|
ls = dict((txt.get('lstStyle') or {}).get('lvl1pPr') or {})
|
|
@@ -955,12 +969,41 @@ def slot_style(s):
|
|
|
955
969
|
anysz = shape_sz(s)
|
|
956
970
|
if anysz:
|
|
957
971
|
ls['sz_px'] = anysz
|
|
972
|
+
body = txt.get('bodyPr') or {}
|
|
973
|
+
css = []
|
|
958
974
|
out = {}
|
|
975
|
+
insets = body.get('insets_px') or {}
|
|
976
|
+
if insets:
|
|
977
|
+
css.append('box-sizing: border-box')
|
|
978
|
+
css.append('padding: %spx %spx %spx %spx' % (
|
|
979
|
+
css_number(insets.get('tIns', 0) or 0),
|
|
980
|
+
css_number(insets.get('rIns', 0) or 0),
|
|
981
|
+
css_number(insets.get('bIns', 0) or 0),
|
|
982
|
+
css_number(insets.get('lIns', 0) or 0),
|
|
983
|
+
))
|
|
959
984
|
if ls.get('sz_px'):
|
|
960
|
-
|
|
985
|
+
size = round(ls['sz_px'])
|
|
986
|
+
css.append('font-size: %dpx' % size)
|
|
987
|
+
out['_font_size'] = size
|
|
988
|
+
weight = ls.get('weight') or (700 if ls.get('bold') else None)
|
|
989
|
+
if weight:
|
|
990
|
+
css.append('font-weight: %s' % weight)
|
|
991
|
+
out['_font_weight'] = weight
|
|
992
|
+
if ls.get('italic'):
|
|
993
|
+
css.append('font-style: italic')
|
|
994
|
+
decorations = []
|
|
995
|
+
if ls.get('underline'):
|
|
996
|
+
decorations.append('underline')
|
|
997
|
+
if ls.get('strike'):
|
|
998
|
+
decorations.append('line-through')
|
|
999
|
+
if decorations:
|
|
1000
|
+
css.append('text-decoration: %s' % ' '.join(decorations))
|
|
1001
|
+
if ls.get('spc_px') is not None:
|
|
1002
|
+
css.append('letter-spacing: %spx' % css_number(ls['spc_px']))
|
|
961
1003
|
col = (ls.get('color') or {}).get('resolved')
|
|
962
1004
|
if col:
|
|
963
|
-
|
|
1005
|
+
css.append('color: %s' % col)
|
|
1006
|
+
out['_color'] = col
|
|
964
1007
|
else:
|
|
965
1008
|
# 占位符的字色也可以是 gradFill(章节页的大号序号常这么做)。解析层已经把
|
|
966
1009
|
# stops 和角度记全了,这里只取单色就会整条丢掉,消费端只能自己编一个平色。
|
|
@@ -969,17 +1012,30 @@ def slot_style(s):
|
|
|
969
1012
|
if f.get('type') == 'gradient':
|
|
970
1013
|
g = _load_query()._css_gradient(f)
|
|
971
1014
|
if g:
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
1015
|
+
css += ['background-image: %s' % g, '-webkit-background-clip: text',
|
|
1016
|
+
'background-clip: text', 'color: transparent']
|
|
1017
|
+
align = ls.get('algn')
|
|
1018
|
+
if align:
|
|
1019
|
+
css.append('text-align: %s' % {
|
|
1020
|
+
'l': 'left', 'ctr': 'center', 'r': 'right', 'just': 'justify',
|
|
1021
|
+
}.get(align, align))
|
|
1022
|
+
line_spacing = ls.get('lnSpc') or {}
|
|
1023
|
+
if line_spacing.get('mult'):
|
|
1024
|
+
css.append('line-height: %s' % css_number(line_spacing['mult'] * 1.2))
|
|
1025
|
+
elif line_spacing.get('px'):
|
|
1026
|
+
css.append('line-height: %spx' % css_number(line_spacing['px']))
|
|
1027
|
+
anchor = body.get('anchor')
|
|
981
1028
|
if anchor in ('ctr', 'b'):
|
|
982
|
-
|
|
1029
|
+
css += ['display: flex', 'flex-direction: column',
|
|
1030
|
+
'justify-content: %s' % {'ctr': 'center', 'b': 'flex-end'}[anchor]]
|
|
1031
|
+
if body.get('rot'):
|
|
1032
|
+
try:
|
|
1033
|
+
degrees = float(body['rot']) / 60000.0
|
|
1034
|
+
css.append('rotate: %sdeg' % css_number(degrees))
|
|
1035
|
+
except (TypeError, ValueError):
|
|
1036
|
+
pass
|
|
1037
|
+
if css:
|
|
1038
|
+
out['css'] = '; '.join(css)
|
|
983
1039
|
return out
|
|
984
1040
|
|
|
985
1041
|
|
|
@@ -1021,7 +1077,8 @@ def layouts_from_template(d, shapes, cW, cH):
|
|
|
1021
1077
|
"""
|
|
1022
1078
|
by_part = defaultdict(list)
|
|
1023
1079
|
for s in shapes:
|
|
1024
|
-
if s.get('layer') == 'layout' and s.get('
|
|
1080
|
+
if (s.get('layer') == 'layout' and s.get('kind') == 'sp'
|
|
1081
|
+
and (s.get('box') or {}).get('w') and (s.get('ph') or shape_text(s))):
|
|
1025
1082
|
by_part[s['part']].append(s)
|
|
1026
1083
|
bg_of_layout = {}
|
|
1027
1084
|
composites = d.get('background_composites') or {}
|
|
@@ -1050,10 +1107,10 @@ def layouts_from_template(d, shapes, cW, cH):
|
|
|
1050
1107
|
slots, seen_kind = [], set()
|
|
1051
1108
|
for s in phs:
|
|
1052
1109
|
t = PH_TO_TYPE.get((s['ph'] or {}).get('type'), 'body')
|
|
1053
|
-
if t in ('slide-number', 'footer'):
|
|
1054
|
-
continue #
|
|
1110
|
+
if t in ('slide-number', 'footer') and not shape_text(s):
|
|
1111
|
+
continue # 空 chrome 占位符不是实际元素
|
|
1055
1112
|
b = s['box']
|
|
1056
|
-
role = t if t in ('title', 'subtitle') else 'body'
|
|
1113
|
+
role = t if t in ('title', 'subtitle', 'footer', 'slide-number') else 'body'
|
|
1057
1114
|
if t == 'title' and 'title' in seen_kind:
|
|
1058
1115
|
role, t = 'subtitle', 'subtitle'
|
|
1059
1116
|
seen_kind.add(t)
|
|
@@ -1062,6 +1119,14 @@ def layouts_from_template(d, shapes, cW, cH):
|
|
|
1062
1119
|
round(b.get('w', 0)), round(b.get('h', 0))],
|
|
1063
1120
|
'txt': shape_text(s) or (s.get('name') or '')[:24]}
|
|
1064
1121
|
row.update(slot_style(s))
|
|
1122
|
+
if t == 'body':
|
|
1123
|
+
ph = s.get('ph') or {}
|
|
1124
|
+
row.update({
|
|
1125
|
+
'_needs_role': True,
|
|
1126
|
+
'_source_layer': 'layout',
|
|
1127
|
+
'_placeholder': '%s/%s' % (
|
|
1128
|
+
ph.get('type') or '-', ph.get('idx') or '-'),
|
|
1129
|
+
})
|
|
1065
1130
|
slots.append(row)
|
|
1066
1131
|
# 非满屏的图片元素(logo / 联名标 / 装饰)——它们逐版式换位置换尺寸,
|
|
1067
1132
|
# 必须按版式落进 slots,压成一条全局「固定位」规则就会撞标题。
|
|
@@ -1181,17 +1246,76 @@ def collect_decor(shapes, part, taken_boxes, canvas, limit=10):
|
|
|
1181
1246
|
return out[:limit] # 同款不同位置都要留,位置本身是版式信息
|
|
1182
1247
|
|
|
1183
1248
|
|
|
1249
|
+
def placeholder_key(shape):
|
|
1250
|
+
ph = shape.get('ph') or {}
|
|
1251
|
+
if not ph:
|
|
1252
|
+
return None
|
|
1253
|
+
return (ph.get('type') or 'body', str(ph.get('idx') or ''))
|
|
1254
|
+
|
|
1255
|
+
|
|
1256
|
+
def merge_dict(base, override):
|
|
1257
|
+
"""把实例页的非空声明叠到版式声明上;空实例占位符继续继承版式事实。"""
|
|
1258
|
+
out = copy.deepcopy(base or {})
|
|
1259
|
+
for key, value in (override or {}).items():
|
|
1260
|
+
if value is None or value == []:
|
|
1261
|
+
continue
|
|
1262
|
+
if isinstance(value, dict) and isinstance(out.get(key), dict):
|
|
1263
|
+
out[key] = merge_dict(out[key], value)
|
|
1264
|
+
else:
|
|
1265
|
+
out[key] = copy.deepcopy(value)
|
|
1266
|
+
return out
|
|
1267
|
+
|
|
1268
|
+
|
|
1269
|
+
def inherited_text_shapes(layout_shapes, slide_shapes):
|
|
1270
|
+
"""返回实例页可用的文字形状,并补齐其引用版式中的占位符几何与样式。"""
|
|
1271
|
+
layout_text = []
|
|
1272
|
+
for shape in layout_shapes:
|
|
1273
|
+
if shape.get('kind') != 'sp' or not (shape.get('box') or {}).get('w'):
|
|
1274
|
+
continue
|
|
1275
|
+
ph = shape.get('ph') or {}
|
|
1276
|
+
ph_type = ph.get('type')
|
|
1277
|
+
if shape_text(shape) or (ph and ph_type not in ('ftr', 'dt', 'sldNum')):
|
|
1278
|
+
layout_text.append(shape)
|
|
1279
|
+
by_placeholder = {placeholder_key(s): s for s in layout_text if placeholder_key(s)}
|
|
1280
|
+
used = set()
|
|
1281
|
+
out = []
|
|
1282
|
+
for shape in slide_shapes:
|
|
1283
|
+
if shape.get('kind') != 'sp':
|
|
1284
|
+
continue
|
|
1285
|
+
key = placeholder_key(shape)
|
|
1286
|
+
base = by_placeholder.get(key)
|
|
1287
|
+
if base:
|
|
1288
|
+
merged = merge_dict(base, shape)
|
|
1289
|
+
merged['text'] = merge_dict(base.get('text'), shape.get('text'))
|
|
1290
|
+
if not shape_text(shape):
|
|
1291
|
+
merged['text']['paragraphs'] = copy.deepcopy(
|
|
1292
|
+
(base.get('text') or {}).get('paragraphs') or [])
|
|
1293
|
+
used.add(key)
|
|
1294
|
+
out.append((merged, 'slide+layout'))
|
|
1295
|
+
elif (shape.get('box') or {}).get('w') and shape_text(shape):
|
|
1296
|
+
out.append((shape, 'slide'))
|
|
1297
|
+
for shape in layout_text:
|
|
1298
|
+
key = placeholder_key(shape)
|
|
1299
|
+
if key not in used:
|
|
1300
|
+
out.append((shape, 'layout'))
|
|
1301
|
+
return out
|
|
1302
|
+
|
|
1303
|
+
|
|
1184
1304
|
def draft_layouts(d, outdir):
|
|
1185
|
-
|
|
1305
|
+
with open(os.path.join(outdir, 'ref', 'shapes.json'), encoding='utf-8') as stream:
|
|
1306
|
+
shapes = json.load(stream)['shapes']
|
|
1186
1307
|
cW, cH = d['canvas']['px']
|
|
1187
1308
|
if (d.get('form_hint') or {}).get('form') == 3:
|
|
1188
1309
|
arch = layouts_from_template(d, shapes, cW, cH)
|
|
1189
1310
|
if len(arch) >= 3:
|
|
1190
1311
|
return arch, [], []
|
|
1191
1312
|
by_slide = defaultdict(list)
|
|
1313
|
+
by_layout = defaultdict(list)
|
|
1192
1314
|
for s in shapes:
|
|
1193
1315
|
if s.get('layer') == 'slide':
|
|
1194
1316
|
by_slide[s['part']].append(s)
|
|
1317
|
+
elif s.get('layer') == 'layout':
|
|
1318
|
+
by_layout[s['part']].append(s)
|
|
1195
1319
|
|
|
1196
1320
|
bg_of_slide, layout_of_slide = {}, {}
|
|
1197
1321
|
for s in d.get('slides', []):
|
|
@@ -1207,32 +1331,43 @@ def draft_layouts(d, outdir):
|
|
|
1207
1331
|
|
|
1208
1332
|
pages = []
|
|
1209
1333
|
for part, sh in sorted(by_slide.items(), key=lambda kv: slide_no(kv[0])):
|
|
1334
|
+
layout_part = layout_of_slide.get(part)
|
|
1335
|
+
layout_shapes = by_layout.get(layout_part) or []
|
|
1210
1336
|
bg_media = top_bleed_media(sh)
|
|
1211
1337
|
if bg_media is None:
|
|
1212
|
-
bg_media = bg_of_layout.get(
|
|
1338
|
+
bg_media = bg_of_layout.get(layout_part)
|
|
1213
1339
|
rendered_bg = (composites.get(part)
|
|
1214
|
-
or composites.get(
|
|
1340
|
+
or composites.get(layout_part)
|
|
1215
1341
|
or bg_media)
|
|
1216
1342
|
texts = []
|
|
1217
|
-
for s in sh:
|
|
1218
|
-
|
|
1219
|
-
continue
|
|
1220
|
-
txt = shape_text(s)
|
|
1221
|
-
if not txt:
|
|
1222
|
-
continue
|
|
1343
|
+
for s, source_layer in inherited_text_shapes(layout_shapes, sh):
|
|
1344
|
+
txt = shape_text(s) or (s.get('name') or '')[:24]
|
|
1223
1345
|
b = s.get('box') or {}
|
|
1224
1346
|
if b.get('w', 0) < DECOR_MIN or b.get('h', 0) < 16:
|
|
1225
1347
|
continue
|
|
1226
|
-
|
|
1348
|
+
ph = s.get('ph') or {}
|
|
1349
|
+
ph_type = ph.get('type')
|
|
1350
|
+
direct_type = PH_TO_TYPE.get(ph_type, 'body')
|
|
1351
|
+
texts.append({
|
|
1352
|
+
'sz': shape_sz(s),
|
|
1353
|
+
'box': b,
|
|
1354
|
+
'txt': txt,
|
|
1355
|
+
'style': slot_style(s),
|
|
1356
|
+
'direct_type': direct_type,
|
|
1357
|
+
'needs_role': direct_type == 'body',
|
|
1358
|
+
'source_layer': source_layer,
|
|
1359
|
+
'placeholder': '%s/%s' % (ph_type or '-', ph.get('idx') or '-'),
|
|
1360
|
+
})
|
|
1227
1361
|
texts.sort(key=lambda t: (-t['sz'], t['box'].get('y', 0)))
|
|
1228
|
-
|
|
1362
|
+
visible_shapes = layout_shapes + sh
|
|
1363
|
+
pics = [s for s in visible_shapes if s.get('kind') == 'pic' and s.get('w_pct', 0) < 95]
|
|
1229
1364
|
# 小图元素(logo / 角标 / 装饰)逐页记位置,供 archetype 落 slots
|
|
1230
1365
|
marks = [{'media': s['media'], 'box': s['box']} for s in pics
|
|
1231
1366
|
if s.get('media') and (s.get('box') or {}).get('w') and s.get('w_pct', 0) < 30]
|
|
1232
1367
|
pages.append({'part': part, 'no': slide_no(part), 'bg_media': bg_media,
|
|
1233
1368
|
'rendered_bg': rendered_bg,
|
|
1234
1369
|
'bg_color': bg_of_slide.get(part), 'texts': texts, 'pic_n': len(pics),
|
|
1235
|
-
'marks': marks, 'shape_n': len(
|
|
1370
|
+
'marks': marks, 'shape_n': len(visible_shapes), 'layout': layout_part})
|
|
1236
1371
|
|
|
1237
1372
|
# 页型的**角色**(封面 / 章节页 / 内容页……)不在这里判:那是看图才能下的结论,
|
|
1238
1373
|
# 交给读得到重建图的模型。脚本只做客观归并——同一张底图 + 文字块数量相近的页
|
|
@@ -1286,12 +1421,13 @@ def draft_layouts(d, outdir):
|
|
|
1286
1421
|
rest.sort(key=lambda t: (t['box'].get('y', 0), t['box'].get('x', 0)))
|
|
1287
1422
|
ordered = ([title] if title else []) + rest
|
|
1288
1423
|
slots = []
|
|
1289
|
-
|
|
1290
|
-
'需要更多同类槽时,按已有同类槽的间距等距延续,不要另起一套网格'
|
|
1291
|
-
'——包里的坐标是模板量出来的,自创网格等于放弃这套版式', name)
|
|
1292
|
-
for i, t in enumerate(ordered[:6]):
|
|
1424
|
+
for i, t in enumerate(ordered):
|
|
1293
1425
|
b = t['box']
|
|
1294
|
-
if t
|
|
1426
|
+
if t.get('needs_role'):
|
|
1427
|
+
role = typ = 'body'
|
|
1428
|
+
elif t.get('direct_type') in ('title', 'subtitle', 'footer', 'slide-number'):
|
|
1429
|
+
role = typ = t['direct_type']
|
|
1430
|
+
elif t is title:
|
|
1295
1431
|
role = typ = 'title'
|
|
1296
1432
|
elif (title and i == 1
|
|
1297
1433
|
# 副标题 = 紧跟在标题下方、与标题左对齐的那一块。三个量都相对标题
|
|
@@ -1307,6 +1443,12 @@ def draft_layouts(d, outdir):
|
|
|
1307
1443
|
round(b.get('w', 0)), round(b.get('h', 0))],
|
|
1308
1444
|
'type': typ, 'sz': t['sz'], 'txt': t['txt']}
|
|
1309
1445
|
row.update(t.get('style') or {})
|
|
1446
|
+
if t.get('needs_role'):
|
|
1447
|
+
row.update({
|
|
1448
|
+
'_needs_role': True,
|
|
1449
|
+
'_source_layer': t.get('source_layer'),
|
|
1450
|
+
'_placeholder': t.get('placeholder'),
|
|
1451
|
+
})
|
|
1310
1452
|
slots.append(row)
|
|
1311
1453
|
# 代表页上的小图元素按位置去重后落 slots(同一 logo 在不同页型位置不同)
|
|
1312
1454
|
seen_mark = set()
|
|
@@ -1320,7 +1462,15 @@ def draft_layouts(d, outdir):
|
|
|
1320
1462
|
'media': mk['media'],
|
|
1321
1463
|
'box': [round(b.get('x', 0)), round(b.get('y', 0)),
|
|
1322
1464
|
round(b.get('w', 0)), round(b.get('h', 0))]})
|
|
1323
|
-
|
|
1465
|
+
taken = {tuple(s['box']) for s in slots}
|
|
1466
|
+
decor = []
|
|
1467
|
+
seen_decor = set()
|
|
1468
|
+
for source_part in (rep.get('layout'), rep['part']):
|
|
1469
|
+
for item in collect_decor(shapes, source_part, taken, (cW, cH)):
|
|
1470
|
+
key = (tuple(item['box']), item['geom'], item['css'])
|
|
1471
|
+
if key not in seen_decor:
|
|
1472
|
+
seen_decor.add(key)
|
|
1473
|
+
decor.append(item)
|
|
1324
1474
|
archetypes.append({'name': name, 'bg': None, 'bg_raw': bg_raw, 'slots': slots,
|
|
1325
1475
|
'decor': decor,
|
|
1326
1476
|
'pages': sorted(p['no'] for p in ps), 'rep': rep['no'],
|
|
@@ -1470,10 +1620,11 @@ def emit_frontmatter(d, tokens, fonts, roles, anchors, gaps, ldir):
|
|
|
1470
1620
|
L.append('spacing:')
|
|
1471
1621
|
L.append(' page-padding: {top: %s, right: %s, bottom: %s, left: %s}'
|
|
1472
1622
|
% (edge['top'], edge['right'], edge['bottom'], edge['left']))
|
|
1473
|
-
#
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1623
|
+
# rounded.card 是全局 token,只能表达全档共同的一档圆角。多个非零档位或零/非零
|
|
1624
|
+
# 混用时,圆角属于 layouts.md 里的局部形状事实,压成一个值会把直角容器也圆角化。
|
|
1625
|
+
radii = d.get('radii_census') or []
|
|
1626
|
+
if len(radii) == 1 and radii[0]['px'] >= 1:
|
|
1627
|
+
top = radii[0]
|
|
1477
1628
|
L.append('rounded:')
|
|
1478
1629
|
L.append(' card: %dpx' % round(top['px']))
|
|
1479
1630
|
if edges_full:
|
|
@@ -1509,9 +1660,13 @@ def draft_flow(a, facts, canvas):
|
|
|
1509
1660
|
cW, cH = canvas
|
|
1510
1661
|
# 装饰件也算进来:很多模板的版式层只有几个占位符,真正撑起版面的是卡片容器
|
|
1511
1662
|
# (在 decor 里)。只看 slots 会把一页的主体结构整个漏掉。
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1663
|
+
slots = [s for s in a['slots'] if s.get('box')]
|
|
1664
|
+
fixed_roles = {'logo', 'slide-number', 'page-number', 'header', 'footer'}
|
|
1665
|
+
fixed = [s for s in slots if s.get('role') in fixed_roles]
|
|
1666
|
+
content_slots = [s for s in slots if s.get('role') not in fixed_roles]
|
|
1667
|
+
containers = [{'role': 'container', 'type': 'decor', 'box': dcr['box'],
|
|
1668
|
+
'css': dcr.get('css')} for dcr in (a.get('decor') or [])]
|
|
1669
|
+
items = group_flow_cards(content_slots, containers)
|
|
1515
1670
|
if len(items) < 2:
|
|
1516
1671
|
return None
|
|
1517
1672
|
items.sort(key=lambda s: (s['box'][1], s['box'][0]))
|
|
@@ -1522,7 +1677,7 @@ def draft_flow(a, facts, canvas):
|
|
|
1522
1677
|
return None
|
|
1523
1678
|
# 区带边界 = 间距分布里的最大空档。同一区带内部的间距(网格行距之类)总是明显
|
|
1524
1679
|
# 小于区带之间的间距,用本页自己的分布切,不设固定阈值。
|
|
1525
|
-
cut = _gap_cut(pos, min(pos), max(pos)) if len(pos) > 1 else
|
|
1680
|
+
cut = _gap_cut(pos, min(pos), max(pos)) if len(pos) > 1 else pos[0]
|
|
1526
1681
|
regions, cur = [], [items[0]]
|
|
1527
1682
|
for i, g in enumerate(gaps):
|
|
1528
1683
|
if g >= cut:
|
|
@@ -1531,6 +1686,11 @@ def draft_flow(a, facts, canvas):
|
|
|
1531
1686
|
cur.append(items[i + 1])
|
|
1532
1687
|
regions.append(cur)
|
|
1533
1688
|
|
|
1689
|
+
# 整页左右边距 = 所有内容的横向外包络,作为各区带的缺省。
|
|
1690
|
+
lefts = [s['box'][0] for s in items]
|
|
1691
|
+
rights = [s['box'][0] + s['box'][2] for s in items]
|
|
1692
|
+
page_margin = [min(lefts), cW - max(rights)]
|
|
1693
|
+
|
|
1534
1694
|
out = []
|
|
1535
1695
|
for reg in regions:
|
|
1536
1696
|
if not reg:
|
|
@@ -1553,8 +1713,17 @@ def draft_flow(a, facts, canvas):
|
|
|
1553
1713
|
if len(rows) > 1:
|
|
1554
1714
|
row_gap = round(rows[1][0]['box'][1]
|
|
1555
1715
|
- (rows[0][0]['box'][1] + rows[0][0]['box'][3]))
|
|
1556
|
-
|
|
1557
|
-
|
|
1716
|
+
region = {'kind': 'grid', 'cols': cols, 'gap': [max(col_gap, 0), max(row_gap, 0)],
|
|
1717
|
+
'items': rows[0]}
|
|
1718
|
+
# 卡片组的横向范围常和整页不同(标题贴左、卡片居中)。整页边距是所有元素的
|
|
1719
|
+
# 外包络,直接套给居中卡片组会把它拉偏成左对齐。区带范围和整页明显不一致时,
|
|
1720
|
+
# 落这个区带自己的左右边距,消费端把网格放进它再填 1fr。按落盘的整数比较,
|
|
1721
|
+
# 亚像素噪声不触发多余的区带边距。
|
|
1722
|
+
reg_margin = [min(s['box'][0] for s in rows[0]),
|
|
1723
|
+
cW - max(s['box'][0] + s['box'][2] for s in rows[0])]
|
|
1724
|
+
if [int(reg_margin[0]), int(reg_margin[1])] != [int(page_margin[0]), int(page_margin[1])]:
|
|
1725
|
+
region['margin'] = reg_margin
|
|
1726
|
+
out.append(region)
|
|
1558
1727
|
elif len(rows) == len(reg):
|
|
1559
1728
|
# 每行一个元素 = 真的竖着排
|
|
1560
1729
|
inner = 0
|
|
@@ -1565,14 +1734,91 @@ def draft_flow(a, facts, canvas):
|
|
|
1565
1734
|
# 每行元素数不一致(比如左列两张、右列一张跨两行)。硬说成 stack 会让消费端
|
|
1566
1735
|
# 以为它们是竖排的,比不给还糟。如实说这块推不出规整结构,按坐标摆。
|
|
1567
1736
|
out.append({'kind': 'free', 'items': reg})
|
|
1737
|
+
if fixed:
|
|
1738
|
+
out.append({'kind': 'free', 'items': fixed})
|
|
1568
1739
|
if len(out) < 2:
|
|
1569
1740
|
return None
|
|
1570
|
-
|
|
1571
|
-
rights = [s['box'][0] + s['box'][2] for s in items]
|
|
1572
|
-
return {'top': items[0]['box'][1], 'margin': [min(lefts), cW - max(rights)],
|
|
1741
|
+
return {'top': items[0]['box'][1], 'margin': page_margin,
|
|
1573
1742
|
'gap': round(cut), 'regions': out}
|
|
1574
1743
|
|
|
1575
1744
|
|
|
1745
|
+
def box_contains(outer, inner):
|
|
1746
|
+
return (outer[0] <= inner[0] and outer[1] <= inner[1]
|
|
1747
|
+
and outer[0] + outer[2] >= inner[0] + inner[2]
|
|
1748
|
+
and outer[1] + outer[3] >= inner[1] + inner[3])
|
|
1749
|
+
|
|
1750
|
+
|
|
1751
|
+
def boxes_overlap(a, b):
|
|
1752
|
+
return (min(a[0] + a[2], b[0] + b[2]) > max(a[0], b[0])
|
|
1753
|
+
and min(a[1] + a[3], b[1] + b[3]) > max(a[1], b[1]))
|
|
1754
|
+
|
|
1755
|
+
|
|
1756
|
+
def overlap_ratio(outer, inner):
|
|
1757
|
+
width = min(outer[0] + outer[2], inner[0] + inner[2]) - max(outer[0], inner[0])
|
|
1758
|
+
height = min(outer[1] + outer[3], inner[1] + inner[3]) - max(outer[1], inner[1])
|
|
1759
|
+
if width <= 0 or height <= 0 or inner[2] <= 0 or inner[3] <= 0:
|
|
1760
|
+
return 0
|
|
1761
|
+
return width * height / (inner[2] * inner[3])
|
|
1762
|
+
|
|
1763
|
+
|
|
1764
|
+
def group_flow_cards(slots, containers):
|
|
1765
|
+
"""把并列卡片容器及其文字组成一层 group,避免拍平成多列元素。"""
|
|
1766
|
+
candidates = []
|
|
1767
|
+
for container in containers:
|
|
1768
|
+
children = [slot for slot in slots if box_contains(container['box'], slot['box'])]
|
|
1769
|
+
if len(children) >= 2:
|
|
1770
|
+
candidates.append((container, children))
|
|
1771
|
+
selected = []
|
|
1772
|
+
for container, children in sorted(
|
|
1773
|
+
candidates, key=lambda pair: pair[0]['box'][2] * pair[0]['box'][3]):
|
|
1774
|
+
if not any(boxes_overlap(container['box'], other['box']) for other, _ in selected):
|
|
1775
|
+
selected.append((container, children))
|
|
1776
|
+
if len(selected) < 2:
|
|
1777
|
+
return slots + containers
|
|
1778
|
+
|
|
1779
|
+
grouped_slots = {id(slot) for _, children in selected for slot in children}
|
|
1780
|
+
nested_by_container = {}
|
|
1781
|
+
for container, _ in selected:
|
|
1782
|
+
nested_by_container[id(container)] = [
|
|
1783
|
+
other for other in containers
|
|
1784
|
+
if other is not container and overlap_ratio(container['box'], other['box']) >= 0.9
|
|
1785
|
+
]
|
|
1786
|
+
grouped_containers = {
|
|
1787
|
+
id(container)
|
|
1788
|
+
for container, _ in selected
|
|
1789
|
+
for container in [container] + nested_by_container[id(container)]
|
|
1790
|
+
}
|
|
1791
|
+
out = [slot for slot in slots if id(slot) not in grouped_slots]
|
|
1792
|
+
out += [container for container in containers if id(container) not in grouped_containers]
|
|
1793
|
+
for container, children in selected:
|
|
1794
|
+
children = children + nested_by_container[id(container)]
|
|
1795
|
+
children = sorted(children, key=lambda slot: (slot['box'][1], slot['box'][0]))
|
|
1796
|
+
gaps = [children[i + 1]['box'][1]
|
|
1797
|
+
- (children[i]['box'][1] + children[i]['box'][3])
|
|
1798
|
+
for i in range(len(children) - 1)]
|
|
1799
|
+
outer = container['box']
|
|
1800
|
+
insets = [
|
|
1801
|
+
min(child['box'][1] - outer[1] for child in children),
|
|
1802
|
+
min(outer[0] + outer[2] - child['box'][0] - child['box'][2] for child in children),
|
|
1803
|
+
min(outer[1] + outer[3] - child['box'][1] - child['box'][3] for child in children),
|
|
1804
|
+
min(child['box'][0] - outer[0] for child in children),
|
|
1805
|
+
]
|
|
1806
|
+
padding = max(0, round(min(insets)))
|
|
1807
|
+
css = container.get('css') or ''
|
|
1808
|
+
if padding:
|
|
1809
|
+
css = '; '.join(part for part in (
|
|
1810
|
+
css.rstrip('; '), 'box-sizing: border-box', 'padding: %dpx' % padding) if part)
|
|
1811
|
+
out.append({
|
|
1812
|
+
'role': 'group',
|
|
1813
|
+
'type': 'group',
|
|
1814
|
+
'box': outer,
|
|
1815
|
+
'css': css,
|
|
1816
|
+
'gap': max(0, round(min(gaps))) if gaps else 0,
|
|
1817
|
+
'items': children,
|
|
1818
|
+
})
|
|
1819
|
+
return out
|
|
1820
|
+
|
|
1821
|
+
|
|
1576
1822
|
def structure_facts(archetypes, d, shapes):
|
|
1577
1823
|
"""每个页型的**结构事实**:栅格、垂直间距序列、容器样式配方、样张里的实际字数。
|
|
1578
1824
|
|
|
@@ -1601,8 +1847,7 @@ def structure_facts(archetypes, d, shapes):
|
|
|
1601
1847
|
'fill': fill, 'line': line, 'fx': fx, 'shapes': set()})
|
|
1602
1848
|
g['n'] += 1
|
|
1603
1849
|
g['parts'].add(s.get('part'))
|
|
1604
|
-
|
|
1605
|
-
g['radii'].append(s['radius_px'])
|
|
1850
|
+
g['radii'].append(s.get('radius_px') or 0)
|
|
1606
1851
|
g['shapes'].add(id(s))
|
|
1607
1852
|
ranked = sorted(groups.values(), key=lambda g: -g['n'])
|
|
1608
1853
|
recipe_id = {}
|
|
@@ -1645,7 +1890,8 @@ def structure_facts(archetypes, d, shapes):
|
|
|
1645
1890
|
def emit_layouts(archetypes, ldir, busy_hints=None, facts=None, recipes=None):
|
|
1646
1891
|
prefilled = sum(1 for a in archetypes if a.get('zh'))
|
|
1647
1892
|
L = ['# 判断单草案 —— package.py 读它产出 layouts.md,deck 的版式坐标从 layouts.md 读。',
|
|
1648
|
-
'# 只改 names / roles /
|
|
1893
|
+
'# 只改 names / roles / text_roles / layout_modes / bg_rules 五段(都是扁平键值,'
|
|
1894
|
+
'改完 package.py 自动并回各页型)。',
|
|
1649
1895
|
'# 下面 layouts 段是普查数值,一个字都不要动——改它容易连带删掉 slots/confidence。']
|
|
1650
1896
|
if prefilled:
|
|
1651
1897
|
L.append('# names 已按模板自带的版式名填好 %d 条,读一遍确认表意即可,通常不用改。' % prefilled)
|
|
@@ -1674,6 +1920,32 @@ def emit_layouts(archetypes, ldir, busy_hints=None, facts=None, recipes=None):
|
|
|
1674
1920
|
len([s for s in a['slots'] if not s.get('asset')]),
|
|
1675
1921
|
'/'.join(str(x) for x in szs[:5]) or '未声明',
|
|
1676
1922
|
a.get('pic_n') or 0, ';有满屏底图' if a.get('bg_raw') else ''))
|
|
1923
|
+
text_role_ids = {}
|
|
1924
|
+
for a in archetypes:
|
|
1925
|
+
index = 0
|
|
1926
|
+
for slot in a.get('slots') or []:
|
|
1927
|
+
if not slot.get('_needs_role'):
|
|
1928
|
+
continue
|
|
1929
|
+
index += 1
|
|
1930
|
+
text_role_ids[id(slot)] = '%s-text-%d' % (a['name'], index)
|
|
1931
|
+
if text_role_ids:
|
|
1932
|
+
L.append('text_roles: # 取值 title|subtitle|header|footer|body;只改角色,不删槽')
|
|
1933
|
+
for a in archetypes:
|
|
1934
|
+
for slot in a.get('slots') or []:
|
|
1935
|
+
role_id = text_role_ids.get(id(slot))
|
|
1936
|
+
if not role_id:
|
|
1937
|
+
continue
|
|
1938
|
+
L.append(' %s: TODO文本角色 # 来源 %s;占位符 %s;样例 %s;'
|
|
1939
|
+
'box %s;字号 %s;css %s'
|
|
1940
|
+
% (role_id, slot.get('_source_layer') or '-',
|
|
1941
|
+
slot.get('_placeholder') or '-', q(slot.get('txt') or ''),
|
|
1942
|
+
slot.get('box'), round(slot.get('sz') or 0),
|
|
1943
|
+
q(slot.get('css') or '未声明')))
|
|
1944
|
+
flow_archetypes = [a for a in archetypes if a.get('flow')]
|
|
1945
|
+
if flow_archetypes:
|
|
1946
|
+
L.append('layout_modes: # 取值 flow|slots;内容会变的内容页优先 flow,固定构图页用 slots')
|
|
1947
|
+
for a in flow_archetypes:
|
|
1948
|
+
L.append(' %s: TODO布局模式 # 依据见 layouts 段该页型上方的结构事实' % a['name'])
|
|
1677
1949
|
# 禁放区是**背景图**的属性,不是页型的属性——按背景资产分组,页型再多也不涨
|
|
1678
1950
|
bgs = []
|
|
1679
1951
|
for a in archetypes:
|
|
@@ -1740,8 +2012,6 @@ def emit_layouts(archetypes, ldir, busy_hints=None, facts=None, recipes=None):
|
|
|
1740
2012
|
L.append(' background: %s' % a['bg'])
|
|
1741
2013
|
fl = a.get('flow')
|
|
1742
2014
|
if fl:
|
|
1743
|
-
L.append(' # ↓ flow 与 slots 二选一:内容长度会变的页用 flow(区带依次排、'
|
|
1744
|
-
'高度由内容定、下面的自动被推下去),构图固定的页用 slots。删掉不要的那个。')
|
|
1745
2015
|
L.append(' flow:')
|
|
1746
2016
|
L.append(' top: %d' % fl['top'])
|
|
1747
2017
|
L.append(' margin: [%d, %d]' % tuple(fl['margin']))
|
|
@@ -1752,6 +2022,10 @@ def emit_layouts(archetypes, ldir, busy_hints=None, facts=None, recipes=None):
|
|
|
1752
2022
|
L.append(' - kind: grid')
|
|
1753
2023
|
L.append(' cols: %d' % r['cols'])
|
|
1754
2024
|
L.append(' gap: [%d, %d]' % tuple(r['gap']))
|
|
2025
|
+
if r.get('margin'):
|
|
2026
|
+
L.append(' margin: [%d, %d] # 本区带自己的左右边距,'
|
|
2027
|
+
'和整页 margin 不同(居中卡片组不跟标题的左边距)'
|
|
2028
|
+
% tuple(r['margin']))
|
|
1755
2029
|
elif r['kind'] == 'free':
|
|
1756
2030
|
L.append(' - kind: free # 推不出规整结构,按 slots 的坐标摆')
|
|
1757
2031
|
else:
|
|
@@ -1759,6 +2033,32 @@ def emit_layouts(archetypes, ldir, busy_hints=None, facts=None, recipes=None):
|
|
|
1759
2033
|
L.append(' gap: %d' % r['gap'])
|
|
1760
2034
|
L.append(' items:')
|
|
1761
2035
|
for s in r['items']:
|
|
2036
|
+
if s.get('type') == 'group':
|
|
2037
|
+
L.append(' - role: group')
|
|
2038
|
+
L.append(' gap: %d' % s['gap'])
|
|
2039
|
+
if s.get('css'):
|
|
2040
|
+
L.append(' css: "%s"'
|
|
2041
|
+
% str(s['css']).replace('"', "'"))
|
|
2042
|
+
L.append(' items:')
|
|
2043
|
+
for child in s['items']:
|
|
2044
|
+
role_id = text_role_ids.get(id(child))
|
|
2045
|
+
if role_id:
|
|
2046
|
+
L.append(' # text-role: %s' % role_id)
|
|
2047
|
+
if child.get('type') == 'decor':
|
|
2048
|
+
L.append(' - {role: container, css: "%s"}'
|
|
2049
|
+
% str(child.get('css') or '').replace('"', "'"))
|
|
2050
|
+
continue
|
|
2051
|
+
extra = ''
|
|
2052
|
+
if child.get('css') is not None:
|
|
2053
|
+
extra += ', css: "%s"' % str(child['css']).replace('"', "'")
|
|
2054
|
+
if child.get('asset'):
|
|
2055
|
+
extra += ', asset: %s' % child['asset']
|
|
2056
|
+
L.append(' - {role: %s, type: %s%s}'
|
|
2057
|
+
% (child['role'], child['type'], extra))
|
|
2058
|
+
continue
|
|
2059
|
+
role_id = text_role_ids.get(id(s))
|
|
2060
|
+
if role_id:
|
|
2061
|
+
L.append(' # text-role: %s' % role_id)
|
|
1762
2062
|
# free 区带按坐标摆,而 slots 会被删掉,所以坐标必须写在这里
|
|
1763
2063
|
bx = ', box: %s' % s['box'] if r['kind'] == 'free' else ''
|
|
1764
2064
|
if s.get('type') == 'decor':
|
|
@@ -1766,25 +2066,22 @@ def emit_layouts(archetypes, ldir, busy_hints=None, facts=None, recipes=None):
|
|
|
1766
2066
|
% (bx, (s.get('css') or '').replace('"', "'")))
|
|
1767
2067
|
continue
|
|
1768
2068
|
extra = bx
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
extra += ', %s: %s' % (
|
|
1773
|
-
k, '"%s"' % str(s[k]).replace('"', "'")
|
|
1774
|
-
if k in ('color', 'css') else s[k])
|
|
2069
|
+
if s.get('css') is not None:
|
|
2070
|
+
# CSS 串一律加引号:里面的逗号/冒号在 flow map 里是分隔符
|
|
2071
|
+
extra += ', css: "%s"' % str(s['css']).replace('"', "'")
|
|
1775
2072
|
if s.get('asset'):
|
|
1776
2073
|
extra += ', asset: %s' % s['asset']
|
|
1777
2074
|
L.append(' - {role: %s, type: %s%s}' % (s['role'], s['type'], extra))
|
|
1778
2075
|
L.append(' slots:')
|
|
1779
2076
|
for s in a['slots']:
|
|
2077
|
+
role_id = text_role_ids.get(id(s))
|
|
2078
|
+
if role_id:
|
|
2079
|
+
L.append(' # text-role: %s' % role_id)
|
|
1780
2080
|
extra = ''
|
|
1781
2081
|
if s.get('asset'):
|
|
1782
2082
|
extra += ', asset: %s' % s['asset']
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
v = s[k]
|
|
1786
|
-
extra += ', %s: %s' % (
|
|
1787
|
-
k, '"%s"' % str(v).replace('"', "'") if k in ('color', 'css') else v)
|
|
2083
|
+
if s.get('css') is not None:
|
|
2084
|
+
extra += ', css: "%s"' % str(s['css']).replace('"', "'")
|
|
1788
2085
|
L.append(' - {role: %s, box: %s, type: %s%s}'
|
|
1789
2086
|
% (s['role'], s['box'], s['type'], extra))
|
|
1790
2087
|
if a.get('decor'):
|
|
@@ -1811,7 +2108,7 @@ def emit_body(d, tokens, fonts, roles, assets, archetypes, exceptions, cusage, l
|
|
|
1811
2108
|
|
|
1812
2109
|
L = ['## Overview', '',
|
|
1813
2110
|
'TODO: 两三句话讲清这套模板的性格与适用场景——看过联系表和页面重建图之后再写。', '']
|
|
1814
|
-
L.append(('模板自带 %d
|
|
2111
|
+
L.append(('模板自带 %d 种版式,页型、坐标和 CSS 样式都直读自版式层。'
|
|
1815
2112
|
% len(archetypes)) if (d.get('form_hint') or {}).get('form') == 3 else
|
|
1816
2113
|
('%d 页样张归纳出 %d 种页型。' % (d['counts']['slides'], len(archetypes))))
|
|
1817
2114
|
L += ['', '## Usage', '',
|
|
@@ -1826,27 +2123,43 @@ def emit_body(d, tokens, fonts, roles, assets, archetypes, exceptions, cusage, l
|
|
|
1826
2123
|
% sidecar,
|
|
1827
2124
|
'3. **按页型给的形态落元素** —— 页型给 `flow` 就用流式,给 `slots` 就用绝对,'
|
|
1828
2125
|
'两者只会出现一个。'
|
|
1829
|
-
'**flow**:整块用一个纵向 flex 容器,`top` 是它的起始 y,`margin`
|
|
2126
|
+
'**flow**:整块用一个纵向 flex 容器,`top` 是它的起始 y,`margin` 是整块的左右边距,'
|
|
1830
2127
|
'`gap` 是区带之间的间距;`regions` 从上往下依次排,**每个区带的高度由它自己的'
|
|
1831
2128
|
'内容决定,不要写死高度**——上面的区带内容变多时,下面的自然被推下去,这正是'
|
|
1832
2129
|
'这套表达要解决的事。区带内部:`kind: grid` 用 `grid-template-columns: repeat(cols, 1fr)` '
|
|
1833
2130
|
'配 `gap: [行间距, 列间距]`;`kind: stack` 用纵向 flex 配 `gap`;`kind: free` '
|
|
1834
|
-
'
|
|
1835
|
-
'
|
|
2131
|
+
'按 item 自带的 `box` 绝对定位。区带自带 `margin: [左, 右]` 时用它的、'
|
|
2132
|
+
'覆盖整块的 `margin`(模板里居中的卡片组和贴左的标题横向范围本就不同);'
|
|
2133
|
+
'没带就用整块的 `margin`。`grid` 在自己这份左右边距里再 `repeat(cols, 1fr)`。'
|
|
2134
|
+
'`grid` 里的 `role: group` 是一张卡片:'
|
|
2135
|
+
'group 的 `css` 用于外层容器,内部 `items` 按顺序纵向排布并使用 group 的 `gap`。'
|
|
2136
|
+
'每个 `role: container` 的项是容器,把它的 `css` 逐项原样写进 style,内容放进去;'
|
|
2137
|
+
'其中没有 `border-radius` 就按 `0`,不得自行补圆角。',
|
|
1836
2138
|
'4. **按 slot 落元素(页型给的是 slots 时)** —— 每个 slot 渲染成一个绝对定位元素:`box` 是 '
|
|
1837
|
-
'`[x, y, w, h]`(%dx%d
|
|
1838
|
-
'
|
|
2139
|
+
'`[x, y, w, h]`(%dx%d 画布上的绝对像素),机械展开成 `left/top/width/height`;'
|
|
2140
|
+
'slot 的 `css` 是模板排版属性已转译好的声明串,原样写进 style,不要另选字号、'
|
|
2141
|
+
'内边距、颜色或对齐。'
|
|
1839
2142
|
'带 `asset` 的 slot 是图片元素(logo、角标),把该资产放在它自己的 `box` 里;'
|
|
1840
2143
|
'这个页型没有 `asset` 槽,这一页就不出现该资产。' % (canvas[0], canvas[1]),
|
|
1841
2144
|
'5. **铺装饰几何** —— 页型的 `decor` 是这一页的图形骨架(图标托底的圆、'
|
|
1842
|
-
'卡片、分隔线):每条渲染成一个绝对定位空元素,`box` 给位置,`css`
|
|
1843
|
-
'`geom: ellipse` 另加
|
|
2145
|
+
'卡片、分隔线):每条渲染成一个绝对定位空元素,`box` 给位置,`css` 逐项原样写进 '
|
|
2146
|
+
'style;没有 `border-radius` 就按 `0`。只有 `geom: ellipse` 另加 '
|
|
2147
|
+
'`border-radius: 50%`。它们压在背景之上、slot 之下,'
|
|
1844
2148
|
'落在 slot 上的图标正是靠它们托住。',
|
|
1845
|
-
'6.
|
|
2149
|
+
'6. **落实全局设计** —— `design.md` frontmatter 的 `colors`、`typography`、'
|
|
2150
|
+
'`spacing`、`rounded`、`components` 是全局 token;用 CSS variables、类名或内联'
|
|
2151
|
+
'样式承载。局部 slot / decor 的 `css` 优先,不能再解释成另一套视觉系统。'
|
|
2152
|
+
'字体使用 Typography 的完整栈与降级,不在运行时安装字体或依赖。',
|
|
2153
|
+
'7. **保持标题结构** —— 有合适页型可参考时,沿用该页型已有的标题层级与局部 '
|
|
2154
|
+
'`css`;只渲染该页型已有的文字槽,背景中已经可见的固定标题不再创建文本,'
|
|
2155
|
+
'页型没有 `subtitle` 槽就不新增副标题。没有合适参考时,按本包整体视觉组织标题。']
|
|
1846
2156
|
if assets:
|
|
1847
2157
|
L += ['', '资产文件(背景由页型的 `background` 字段指定,'
|
|
1848
2158
|
'图片资产的位置由该页型 `slots` 里带 `asset` 的槽给出):', '',
|
|
1849
|
-
'{{ASSET_TABLE}}'
|
|
2159
|
+
'{{ASSET_TABLE}}', '',
|
|
2160
|
+
'将包内 `assets/` 复制到项目内相对目录,再引用复制后的路径;最终 HTML 不引用'
|
|
2161
|
+
'抽取工作目录或本机绝对路径。附件只提供 `assetRoot` / `assetPaths` 时,把'
|
|
2162
|
+
'`assetRoot` 当作不透明前缀,只拼接清单中声明的相对路径。']
|
|
1850
2163
|
L += ['', '文字与容器的外接矩形落在该页型 `background` 对应的 `text_safe` 内,'
|
|
1851
2164
|
'避开 `avoid` 列出的区域(两者都在 %s 的 `backgrounds` 段)。内容装不下时换页型或拆页。'
|
|
1852
2165
|
% sidecar, '',
|
|
@@ -1861,7 +2174,8 @@ def emit_body(d, tokens, fonts, roles, assets, archetypes, exceptions, cusage, l
|
|
|
1861
2174
|
',源为商业/内部字体无 web 分发源,按气质降级到 %s' % f['stack'][1]
|
|
1862
2175
|
if len(f['stack']) > 1 else ''))
|
|
1863
2176
|
L += ['', '字号轴:' + '、'.join('%s %dpx' % (k, round(v['sz_px'])) for k, v in roles.items())
|
|
1864
|
-
+ '。slot 自带 `size`
|
|
2177
|
+
+ '。slot 自带 `css` 时以其中的 `font-size` 为准;没有 slot CSS 的新增层级,'
|
|
2178
|
+
'复用轴上最接近的一档。', '',
|
|
1865
2179
|
'字体加载(**HARD REQUIREMENT:下面这行 @import 原样写入全局样式首行,禁止替换为 '
|
|
1866
2180
|
'fonts.googleapis.com 或其他域**):', '', '```', imp, '```', '',
|
|
1867
2181
|
'镜像只保证 wght 400 一档,更粗的字重由浏览器合成,字重不能作为唯一区分手段;'
|
|
@@ -1884,7 +2198,15 @@ def emit_body(d, tokens, fonts, roles, assets, archetypes, exceptions, cusage, l
|
|
|
1884
2198
|
% (logo['id'], '、'.join('`%s`' % x for x in with_logo) or '(无)'))
|
|
1885
2199
|
L += ['- 坐标、字号、色值、资产位置以 %s 为准;本文件的 Colors / Typography 是可用值的清单。'
|
|
1886
2200
|
% sidecar,
|
|
1887
|
-
'-
|
|
2201
|
+
'- 强调色族以 Colors 和 %s 的 slot CSS 为主;必要时可以使用 Colors 之外的颜色,'
|
|
2202
|
+
'但不能形成与模板主色竞争的第二强调色。' % sidecar,
|
|
2203
|
+
'- 新增颜色应与模板整体的色相、明度和饱和度关系协调。允许新增中性色、低彩度辅助色'
|
|
2204
|
+
'或局部语义色表达正负、风险、警告、状态、图表序列,但保持辅助层级;'
|
|
2205
|
+
'只要新色通过高饱和、高对比、大面积或跨页重复获得主视觉权重,'
|
|
2206
|
+
'或被用于标题、关键数字、图表主序列、卡片底色或渐变,就属于新的强调色,改用模板'
|
|
2207
|
+
'强调色族的深浅、透明度,或改用线型、纹理、标签区分。',
|
|
2208
|
+
'- 交付前逐页检查:色板、字体、版式、背景、资产和本段规则均来自本风格包;'
|
|
2209
|
+
'页面无资源加载失败、内容溢出或画幅裁切。',
|
|
1888
2210
|
'- 本包里的数值就是普查结果,照用即可,无需重新统计颜色、字体或版式。',
|
|
1889
2211
|
'- 风格包以文本形式(zip 摘要等)到手时,直接用摘要里 design.md / layouts.md 的文本。',
|
|
1890
2212
|
'', '## Exceptions', '']
|
|
@@ -1974,7 +2296,7 @@ def emit_brief(d, ctx, ldir):
|
|
|
1974
2296
|
if leftover:
|
|
1975
2297
|
L += ['', '未归入 archetype 的页:%s —— 都是单页孤例,需要就自己补一个 archetype。'
|
|
1976
2298
|
% ', '.join(map(str, leftover))]
|
|
1977
|
-
L += ['', '各 archetype 的 slot
|
|
2299
|
+
L += ['', '各 archetype 的 slot 原文(据此起中文页型名,并在 text_roles 判断文本角色):', '']
|
|
1978
2300
|
for a in archetypes:
|
|
1979
2301
|
L.append('- `%s`(第 %s 页,覆盖 %s)' % (a['name'], a['rep'], a['pages']))
|
|
1980
2302
|
for s in a['slots']:
|
|
@@ -2124,7 +2446,7 @@ def main(argv=None):
|
|
|
2124
2446
|
gaps.append('源字体 %s 不在 font-fallback 表里,字体栈只有原名,消费端很可能装不上;'
|
|
2125
2447
|
'按气质挑一个有 web 分发源的近似体补进栈,不要照抄原名。' % f['names'][0])
|
|
2126
2448
|
nosize = [(a['name'], s['box']) for a in archetypes for s in a['slots']
|
|
2127
|
-
if not s.get('asset') and not s.get('
|
|
2449
|
+
if not s.get('asset') and not s.get('_font_size')]
|
|
2128
2450
|
if nosize:
|
|
2129
2451
|
gaps.append('这些文字槽在源文件任何层级都没有字号声明(都不是占位符,是普通文本框,'
|
|
2130
2452
|
'继承源是 presentation.xml 的 defaultTextStyle,本抽取按约定不解继承链):'
|