@lark-apaas/coding-steering 0.1.18-dev.4e64c13 → 0.1.18-dev.61b3ece
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/charts/SKILL.md +4 -0
- package/steering/design-html/skills/pptx-style-extract/SKILL.md +11 -8
- 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 +526 -110
- package/steering/design-html/skills/pptx-style-extract/scripts/extract.py +226 -6
- package/steering/design-html/skills/pptx-style-extract/scripts/ooxml.py +18 -1
- package/steering/design-html/skills/pptx-style-extract/scripts/package.py +167 -28
- package/steering/design-html/skills/pptx-style-extract/scripts/parts.py +3 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/query.py +3 -8
- package/steering/design-html/skills/pptx-style-extract/scripts/test_background_composite.py +57 -0
- 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 +127 -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 +208 -0
- package/steering/design-html/skills/pptx-style-extract/v2-format-spec.md +14 -7
- 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
- package/steering/nestjs-react-fullstack/skills/plugin-guide/SKILL.md +5 -3
- package/steering/nestjs-react-fullstack/skills_local/plugin-guide/SKILL.md +4 -0
- package/steering/vite-react/skills/plugin-guide/SKILL.md +3 -1
- package/steering/vite-react/skills/react-three-fiber/SKILL.md +4 -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
|
|
@@ -28,6 +29,7 @@ OPAQUE_ENOUGH = 128 # 能当背景的最低不透明度:低于半透明就
|
|
|
28
29
|
# 那是叠加装饰不是背景
|
|
29
30
|
FILL_MANY = 5 # 「被大量当填充铺开」的次数下限,用于区分卡片底与偶发用色
|
|
30
31
|
BG_CONTENT_CAP = 5 # 内容页背景收几张:再多消费端也挑不过来,超出的写进 TODO 交人取舍
|
|
32
|
+
SHEET_CAP = 12 # 联系表展示上限;进包的资产不受它约束,一张都不截
|
|
31
33
|
|
|
32
34
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
33
35
|
SKILL_ROOT = os.path.dirname(HERE)
|
|
@@ -263,9 +265,9 @@ OFFICE_DEFAULT_FONTS_NORM = {norm(x) for x in OFFICE_DEFAULT_FONTS}
|
|
|
263
265
|
|
|
264
266
|
|
|
265
267
|
def cover_slot_colors(tokens, archetypes, rows, cusage):
|
|
266
|
-
"""slot 里出现的每个色值都必须在色板里有名字。
|
|
268
|
+
"""slot CSS 里出现的每个色值都必须在色板里有名字。
|
|
267
269
|
|
|
268
|
-
Hard Rules 写「颜色只用 colors 里的 token」,而 slot 的 color 是从模板直读的,
|
|
270
|
+
Hard Rules 写「颜色只用 colors 里的 token」,而 slot CSS 的 color 是从模板直读的,
|
|
269
271
|
两者不对齐就等于产物自己违反自己的规则——slot 的色值直读自模板,未必都已进
|
|
270
272
|
色板。这里把缺的补进色板,按用法归族命名。
|
|
271
273
|
"""
|
|
@@ -285,7 +287,7 @@ def cover_slot_colors(tokens, archetypes, rows, cusage):
|
|
|
285
287
|
added = []
|
|
286
288
|
for a in archetypes:
|
|
287
289
|
for s in a['slots']:
|
|
288
|
-
h = (s.get('
|
|
290
|
+
h = (s.get('_color') or '').upper()
|
|
289
291
|
if not h.startswith('#') or h in have:
|
|
290
292
|
continue
|
|
291
293
|
have.add(h)
|
|
@@ -499,7 +501,8 @@ def draft_anchors(d, tokens, fonts, roles, assets, archetypes):
|
|
|
499
501
|
# 6. 层级:字号跨度 + 字重是否单一(字重真单一才敢说「不靠字重」)
|
|
500
502
|
disp, body = roles.get('display'), roles.get('body')
|
|
501
503
|
if disp and body and disp['sz_px'] > body['sz_px']:
|
|
502
|
-
ws = {s.get('
|
|
504
|
+
ws = {s.get('_font_weight') for a in archetypes for s in a['slots']
|
|
505
|
+
if s.get('_font_weight')}
|
|
503
506
|
tail = (',字重只用 %s 一档' % list(ws)[0]) if len(ws) == 1 else ''
|
|
504
507
|
A.append(('size-driven-hierarchy', 'pattern',
|
|
505
508
|
'最大字号档与正文档相差 %.1f 倍(见 typography)%s'
|
|
@@ -894,11 +897,58 @@ def clean_layout_name(name):
|
|
|
894
897
|
return re.sub(r'^\d+[_\-\s]*', '', (name or '').strip()) or '未命名版式'
|
|
895
898
|
|
|
896
899
|
|
|
900
|
+
def is_bleed(s):
|
|
901
|
+
return (s.get('kind') == 'pic' and (s.get('w_pct') or 0) >= 95
|
|
902
|
+
and (s.get('h_pct') or 0) >= 95)
|
|
903
|
+
|
|
904
|
+
|
|
905
|
+
def top_bleed_media(shapes):
|
|
906
|
+
"""一串形状里最上层的满屏图。
|
|
907
|
+
|
|
908
|
+
OOXML 的 spTree 是绘制序,靠后的画在上面。一个版式常叠两张满屏图——通用底纹在
|
|
909
|
+
下、这一页的主视觉在上——所以看得见的是最后那张。取第一张会拿到底纹,实测让
|
|
910
|
+
章节页的深蓝主视觉被换成了另一张鲜蓝底纹,成品与原稿完全不是一个颜色。
|
|
911
|
+
"""
|
|
912
|
+
out = None
|
|
913
|
+
for s in shapes:
|
|
914
|
+
if is_bleed(s) and s.get('media'):
|
|
915
|
+
out = s['media']
|
|
916
|
+
return out
|
|
917
|
+
|
|
918
|
+
|
|
919
|
+
def slot_overlaps(slots):
|
|
920
|
+
"""同一页型里坐标互相重叠的槽对。只报事实,不改坐标——坐标是从模板量的。"""
|
|
921
|
+
out = []
|
|
922
|
+
for i in range(len(slots)):
|
|
923
|
+
for j in range(i + 1, len(slots)):
|
|
924
|
+
a, b = slots[i].get('box'), slots[j].get('box')
|
|
925
|
+
if not (a and b):
|
|
926
|
+
continue
|
|
927
|
+
ox = min(a[0] + a[2], b[0] + b[2]) - max(a[0], b[0])
|
|
928
|
+
oy = min(a[1] + a[3], b[1] + b[3]) - max(a[1], b[1])
|
|
929
|
+
if ox > 0 and oy > 0:
|
|
930
|
+
out.append('%s×%s 叠 %dx%d' % (slots[i].get('role'), slots[j].get('role'),
|
|
931
|
+
round(ox), round(oy)))
|
|
932
|
+
return out
|
|
933
|
+
|
|
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
|
+
|
|
897
943
|
def slot_style(s):
|
|
898
|
-
"""
|
|
944
|
+
"""占位符自带的排版样式,统一转成可直接写进 HTML style 的 CSS 声明串。
|
|
899
945
|
|
|
900
946
|
样式可能在三层:lstStyle.lvl1pPr(版式占位符常用)、段落 defRPr(Mac Office
|
|
901
947
|
导出把大量属性写在这一层)、段落 pPr(对齐)。逐层兜底,缺一层就往下取。
|
|
948
|
+
|
|
949
|
+
`box` 是布局几何,继续由 slot 独立承载;其余渲染属性不再泄漏成 size / color /
|
|
950
|
+
align / insets_px 等 PPTX 中间字段。下划线开头的键仅供 draft 内部统计,emit_layouts
|
|
951
|
+
不会写进消费者产物。
|
|
902
952
|
"""
|
|
903
953
|
txt = s.get('text') or {}
|
|
904
954
|
ls = dict((txt.get('lstStyle') or {}).get('lvl1pPr') or {})
|
|
@@ -919,25 +969,84 @@ def slot_style(s):
|
|
|
919
969
|
anysz = shape_sz(s)
|
|
920
970
|
if anysz:
|
|
921
971
|
ls['sz_px'] = anysz
|
|
972
|
+
body = txt.get('bodyPr') or {}
|
|
973
|
+
css = []
|
|
922
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
|
+
))
|
|
923
984
|
if ls.get('sz_px'):
|
|
924
|
-
|
|
985
|
+
# normAutofit 的 fontScale 是模板让大字装进小框的手段——不乘它,消费端拿到的是
|
|
986
|
+
# 未缩放字号,字比框高,渐变裁切会把溢出的底部切成透明。缺省 1.0(无 autofit / 无缩放)。
|
|
987
|
+
scale = body.get('font_scale')
|
|
988
|
+
raw = ls['sz_px'] * scale if scale else ls['sz_px']
|
|
989
|
+
size = round(raw)
|
|
990
|
+
css.append('font-size: %dpx' % size)
|
|
991
|
+
out['_font_size'] = size
|
|
992
|
+
weight = ls.get('weight') or (700 if ls.get('bold') else None)
|
|
993
|
+
if weight:
|
|
994
|
+
css.append('font-weight: %s' % weight)
|
|
995
|
+
out['_font_weight'] = weight
|
|
996
|
+
if ls.get('italic'):
|
|
997
|
+
css.append('font-style: italic')
|
|
998
|
+
decorations = []
|
|
999
|
+
if ls.get('underline'):
|
|
1000
|
+
decorations.append('underline')
|
|
1001
|
+
if ls.get('strike'):
|
|
1002
|
+
decorations.append('line-through')
|
|
1003
|
+
if decorations:
|
|
1004
|
+
css.append('text-decoration: %s' % ' '.join(decorations))
|
|
1005
|
+
if ls.get('spc_px') is not None:
|
|
1006
|
+
css.append('letter-spacing: %spx' % css_number(ls['spc_px']))
|
|
925
1007
|
col = (ls.get('color') or {}).get('resolved')
|
|
926
1008
|
if col:
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
1009
|
+
css.append('color: %s' % col)
|
|
1010
|
+
out['_color'] = col
|
|
1011
|
+
else:
|
|
1012
|
+
# 占位符的字色也可以是 gradFill(章节页的大号序号常这么做)。解析层已经把
|
|
1013
|
+
# stops 和角度记全了,这里只取单色就会整条丢掉,消费端只能自己编一个平色。
|
|
1014
|
+
# 与 decor 同一约定:css 是可直接写进 style 的声明串。
|
|
1015
|
+
f = ls.get('fill') or {}
|
|
1016
|
+
if f.get('type') == 'gradient':
|
|
1017
|
+
g = _load_query()._css_gradient(f)
|
|
1018
|
+
if g:
|
|
1019
|
+
css += ['background-image: %s' % g, '-webkit-background-clip: text',
|
|
1020
|
+
'background-clip: text', 'color: transparent']
|
|
1021
|
+
align = ls.get('algn')
|
|
1022
|
+
if align:
|
|
1023
|
+
css.append('text-align: %s' % {
|
|
1024
|
+
'l': 'left', 'ctr': 'center', 'r': 'right', 'just': 'justify',
|
|
1025
|
+
}.get(align, align))
|
|
1026
|
+
line_spacing = ls.get('lnSpc') or {}
|
|
1027
|
+
# normAutofit 的 lnSpcReduction 与 fontScale 同时把行距压缩,一起缩才装得进原框。
|
|
1028
|
+
reduction = body.get('ln_spc_reduction') or 0
|
|
1029
|
+
if line_spacing.get('mult'):
|
|
1030
|
+
mult = line_spacing['mult'] * 1.2 * (1 - reduction)
|
|
1031
|
+
css.append('line-height: %s' % css_number(mult))
|
|
1032
|
+
elif line_spacing.get('px'):
|
|
1033
|
+
css.append('line-height: %spx' % css_number(line_spacing['px'] * (1 - reduction)))
|
|
1034
|
+
anchor = body.get('anchor')
|
|
935
1035
|
if anchor in ('ctr', 'b'):
|
|
936
|
-
|
|
1036
|
+
css += ['display: flex', 'flex-direction: column',
|
|
1037
|
+
'justify-content: %s' % {'ctr': 'center', 'b': 'flex-end'}[anchor]]
|
|
1038
|
+
if body.get('rot'):
|
|
1039
|
+
try:
|
|
1040
|
+
degrees = float(body['rot']) / 60000.0
|
|
1041
|
+
css.append('rotate: %sdeg' % css_number(degrees))
|
|
1042
|
+
except (TypeError, ValueError):
|
|
1043
|
+
pass
|
|
1044
|
+
if css:
|
|
1045
|
+
out['css'] = '; '.join(css)
|
|
937
1046
|
return out
|
|
938
1047
|
|
|
939
1048
|
|
|
940
|
-
def instance_override(shapes, slide_part, slots, bgm, cW, cH):
|
|
1049
|
+
def instance_override(shapes, slide_part, slots, bgm, cW, cH, composites=None):
|
|
941
1050
|
"""实例页覆盖版式:版式是骨架,实例页才是设计师最终摆定的样子。
|
|
942
1051
|
|
|
943
1052
|
版式底图常是多个版式共用的通用底纹,实例页可能另铺主视觉大图;标题占位符的框高
|
|
@@ -947,11 +1056,7 @@ def instance_override(shapes, slide_part, slots, bgm, cW, cH):
|
|
|
947
1056
|
ins = [s for s in shapes if s.get('part') == slide_part]
|
|
948
1057
|
if not ins:
|
|
949
1058
|
return slots, bgm
|
|
950
|
-
|
|
951
|
-
if (s.get('kind') == 'pic' and s.get('media')
|
|
952
|
-
and s.get('w_pct', 0) >= 95 and s.get('h_pct', 0) >= 95):
|
|
953
|
-
bgm = s['media']
|
|
954
|
-
break
|
|
1059
|
+
bgm = (composites or {}).get(slide_part) or top_bleed_media(ins) or bgm
|
|
955
1060
|
texts = []
|
|
956
1061
|
for s in ins:
|
|
957
1062
|
b = s.get('box') or {}
|
|
@@ -979,13 +1084,14 @@ def layouts_from_template(d, shapes, cW, cH):
|
|
|
979
1084
|
"""
|
|
980
1085
|
by_part = defaultdict(list)
|
|
981
1086
|
for s in shapes:
|
|
982
|
-
if s.get('layer') == 'layout' and s.get('
|
|
1087
|
+
if (s.get('layer') == 'layout' and s.get('kind') == 'sp'
|
|
1088
|
+
and (s.get('box') or {}).get('w') and (s.get('ph') or shape_text(s))):
|
|
983
1089
|
by_part[s['part']].append(s)
|
|
984
1090
|
bg_of_layout = {}
|
|
1091
|
+
composites = d.get('background_composites') or {}
|
|
985
1092
|
for s in shapes:
|
|
986
|
-
if
|
|
987
|
-
|
|
988
|
-
bg_of_layout.setdefault(s['part'], s.get('media'))
|
|
1093
|
+
if s.get('layer') == 'layout' and is_bleed(s) and s.get('media'):
|
|
1094
|
+
bg_of_layout[s['part']] = s['media'] # 靠后者在上层,最后一张才是看得见的
|
|
989
1095
|
topo = d.get('theme_topology') or {}
|
|
990
1096
|
theme_of_master = {m['master']: m.get('theme_label')
|
|
991
1097
|
for m in (topo.get('per_master') or [])}
|
|
@@ -1007,11 +1113,11 @@ def layouts_from_template(d, shapes, cW, cH):
|
|
|
1007
1113
|
phs.sort(key=lambda s: ((s['box'].get('y') or 0), (s['box'].get('x') or 0)))
|
|
1008
1114
|
slots, seen_kind = [], set()
|
|
1009
1115
|
for s in phs:
|
|
1010
|
-
t = PH_TO_TYPE.get((s
|
|
1011
|
-
if t in ('slide-number', 'footer'):
|
|
1012
|
-
continue #
|
|
1116
|
+
t = PH_TO_TYPE.get((s.get('ph') or {}).get('type'), 'body')
|
|
1117
|
+
if t in ('slide-number', 'footer') and not shape_text(s):
|
|
1118
|
+
continue # 空 chrome 占位符不是实际元素
|
|
1013
1119
|
b = s['box']
|
|
1014
|
-
role = t if t in ('title', 'subtitle') else 'body'
|
|
1120
|
+
role = t if t in ('title', 'subtitle', 'footer', 'slide-number') else 'body'
|
|
1015
1121
|
if t == 'title' and 'title' in seen_kind:
|
|
1016
1122
|
role, t = 'subtitle', 'subtitle'
|
|
1017
1123
|
seen_kind.add(t)
|
|
@@ -1020,10 +1126,18 @@ def layouts_from_template(d, shapes, cW, cH):
|
|
|
1020
1126
|
round(b.get('w', 0)), round(b.get('h', 0))],
|
|
1021
1127
|
'txt': shape_text(s) or (s.get('name') or '')[:24]}
|
|
1022
1128
|
row.update(slot_style(s))
|
|
1129
|
+
if t == 'body':
|
|
1130
|
+
ph = s.get('ph') or {}
|
|
1131
|
+
row.update({
|
|
1132
|
+
'_needs_role': True,
|
|
1133
|
+
'_source_layer': 'layout',
|
|
1134
|
+
'_placeholder': '%s/%s' % (
|
|
1135
|
+
ph.get('type') or '-', ph.get('idx') or '-'),
|
|
1136
|
+
})
|
|
1023
1137
|
slots.append(row)
|
|
1024
1138
|
# 非满屏的图片元素(logo / 联名标 / 装饰)——它们逐版式换位置换尺寸,
|
|
1025
1139
|
# 必须按版式落进 slots,压成一条全局「固定位」规则就会撞标题。
|
|
1026
|
-
bgm = bg_of_layout.get(l['part'])
|
|
1140
|
+
bgm = composites.get(l['part']) or bg_of_layout.get(l['part'])
|
|
1027
1141
|
for s in shapes:
|
|
1028
1142
|
if s['part'] != l['part'] or s.get('kind') != 'pic' or not s.get('media'):
|
|
1029
1143
|
continue
|
|
@@ -1040,7 +1154,8 @@ def layouts_from_template(d, shapes, cW, cH):
|
|
|
1040
1154
|
continue
|
|
1041
1155
|
inst = slide_of_layout.get(l['part'])
|
|
1042
1156
|
if inst:
|
|
1043
|
-
slots, bgm = instance_override(
|
|
1157
|
+
slots, bgm = instance_override(
|
|
1158
|
+
shapes, inst, slots, bgm, cW, cH, composites)
|
|
1044
1159
|
taken = {tuple(s['box']) for s in slots}
|
|
1045
1160
|
decor = collect_decor(shapes, inst or l['part'], taken, (cW, cH))
|
|
1046
1161
|
named_role = role_of_name(l.get('name'))
|
|
@@ -1138,17 +1253,76 @@ def collect_decor(shapes, part, taken_boxes, canvas, limit=10):
|
|
|
1138
1253
|
return out[:limit] # 同款不同位置都要留,位置本身是版式信息
|
|
1139
1254
|
|
|
1140
1255
|
|
|
1256
|
+
def placeholder_key(shape):
|
|
1257
|
+
ph = shape.get('ph') or {}
|
|
1258
|
+
if not ph:
|
|
1259
|
+
return None
|
|
1260
|
+
return (ph.get('type') or 'body', str(ph.get('idx') or ''))
|
|
1261
|
+
|
|
1262
|
+
|
|
1263
|
+
def merge_dict(base, override):
|
|
1264
|
+
"""把实例页的非空声明叠到版式声明上;空实例占位符继续继承版式事实。"""
|
|
1265
|
+
out = copy.deepcopy(base or {})
|
|
1266
|
+
for key, value in (override or {}).items():
|
|
1267
|
+
if value is None or value == []:
|
|
1268
|
+
continue
|
|
1269
|
+
if isinstance(value, dict) and isinstance(out.get(key), dict):
|
|
1270
|
+
out[key] = merge_dict(out[key], value)
|
|
1271
|
+
else:
|
|
1272
|
+
out[key] = copy.deepcopy(value)
|
|
1273
|
+
return out
|
|
1274
|
+
|
|
1275
|
+
|
|
1276
|
+
def inherited_text_shapes(layout_shapes, slide_shapes):
|
|
1277
|
+
"""返回实例页可用的文字形状,并补齐其引用版式中的占位符几何与样式。"""
|
|
1278
|
+
layout_text = []
|
|
1279
|
+
for shape in layout_shapes:
|
|
1280
|
+
if shape.get('kind') != 'sp' or not (shape.get('box') or {}).get('w'):
|
|
1281
|
+
continue
|
|
1282
|
+
ph = shape.get('ph') or {}
|
|
1283
|
+
ph_type = ph.get('type')
|
|
1284
|
+
if shape_text(shape) or (ph and ph_type not in ('ftr', 'dt', 'sldNum')):
|
|
1285
|
+
layout_text.append(shape)
|
|
1286
|
+
by_placeholder = {placeholder_key(s): s for s in layout_text if placeholder_key(s)}
|
|
1287
|
+
used = set()
|
|
1288
|
+
out = []
|
|
1289
|
+
for shape in slide_shapes:
|
|
1290
|
+
if shape.get('kind') != 'sp':
|
|
1291
|
+
continue
|
|
1292
|
+
key = placeholder_key(shape)
|
|
1293
|
+
base = by_placeholder.get(key)
|
|
1294
|
+
if base:
|
|
1295
|
+
merged = merge_dict(base, shape)
|
|
1296
|
+
merged['text'] = merge_dict(base.get('text'), shape.get('text'))
|
|
1297
|
+
if not shape_text(shape):
|
|
1298
|
+
merged['text']['paragraphs'] = copy.deepcopy(
|
|
1299
|
+
(base.get('text') or {}).get('paragraphs') or [])
|
|
1300
|
+
used.add(key)
|
|
1301
|
+
out.append((merged, 'slide+layout'))
|
|
1302
|
+
elif (shape.get('box') or {}).get('w') and shape_text(shape):
|
|
1303
|
+
out.append((shape, 'slide'))
|
|
1304
|
+
for shape in layout_text:
|
|
1305
|
+
key = placeholder_key(shape)
|
|
1306
|
+
if key not in used:
|
|
1307
|
+
out.append((shape, 'layout'))
|
|
1308
|
+
return out
|
|
1309
|
+
|
|
1310
|
+
|
|
1141
1311
|
def draft_layouts(d, outdir):
|
|
1142
|
-
|
|
1312
|
+
with open(os.path.join(outdir, 'ref', 'shapes.json'), encoding='utf-8') as stream:
|
|
1313
|
+
shapes = json.load(stream)['shapes']
|
|
1143
1314
|
cW, cH = d['canvas']['px']
|
|
1144
1315
|
if (d.get('form_hint') or {}).get('form') == 3:
|
|
1145
1316
|
arch = layouts_from_template(d, shapes, cW, cH)
|
|
1146
1317
|
if len(arch) >= 3:
|
|
1147
1318
|
return arch, [], []
|
|
1148
1319
|
by_slide = defaultdict(list)
|
|
1320
|
+
by_layout = defaultdict(list)
|
|
1149
1321
|
for s in shapes:
|
|
1150
1322
|
if s.get('layer') == 'slide':
|
|
1151
1323
|
by_slide[s['part']].append(s)
|
|
1324
|
+
elif s.get('layer') == 'layout':
|
|
1325
|
+
by_layout[s['part']].append(s)
|
|
1152
1326
|
|
|
1153
1327
|
bg_of_slide, layout_of_slide = {}, {}
|
|
1154
1328
|
for s in d.get('slides', []):
|
|
@@ -1156,40 +1330,51 @@ def draft_layouts(d, outdir):
|
|
|
1156
1330
|
bg_of_slide[s['part']] = json.dumps(bg, sort_keys=True) if isinstance(bg, dict) else bg
|
|
1157
1331
|
layout_of_slide[s['part']] = s.get('layout')
|
|
1158
1332
|
# 版式层的满屏底图(form=2 常态:底图挂在 layout 上)
|
|
1333
|
+
composites = d.get('background_composites') or {}
|
|
1159
1334
|
bg_of_layout = {}
|
|
1160
1335
|
for s in shapes:
|
|
1161
|
-
if
|
|
1162
|
-
|
|
1163
|
-
bg_of_layout.setdefault(s['part'], s.get('media'))
|
|
1336
|
+
if s.get('layer') == 'layout' and is_bleed(s) and s.get('media'):
|
|
1337
|
+
bg_of_layout[s['part']] = s['media']
|
|
1164
1338
|
|
|
1165
1339
|
pages = []
|
|
1166
1340
|
for part, sh in sorted(by_slide.items(), key=lambda kv: slide_no(kv[0])):
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
bg_media = s.get('media')
|
|
1171
|
-
break
|
|
1341
|
+
layout_part = layout_of_slide.get(part)
|
|
1342
|
+
layout_shapes = by_layout.get(layout_part) or []
|
|
1343
|
+
bg_media = top_bleed_media(sh)
|
|
1172
1344
|
if bg_media is None:
|
|
1173
|
-
bg_media = bg_of_layout.get(
|
|
1345
|
+
bg_media = bg_of_layout.get(layout_part)
|
|
1346
|
+
rendered_bg = (composites.get(part)
|
|
1347
|
+
or composites.get(layout_part)
|
|
1348
|
+
or bg_media)
|
|
1174
1349
|
texts = []
|
|
1175
|
-
for s in sh:
|
|
1176
|
-
|
|
1177
|
-
continue
|
|
1178
|
-
txt = shape_text(s)
|
|
1179
|
-
if not txt:
|
|
1180
|
-
continue
|
|
1350
|
+
for s, source_layer in inherited_text_shapes(layout_shapes, sh):
|
|
1351
|
+
txt = shape_text(s) or (s.get('name') or '')[:24]
|
|
1181
1352
|
b = s.get('box') or {}
|
|
1182
1353
|
if b.get('w', 0) < DECOR_MIN or b.get('h', 0) < 16:
|
|
1183
1354
|
continue
|
|
1184
|
-
|
|
1355
|
+
ph = s.get('ph') or {}
|
|
1356
|
+
ph_type = ph.get('type')
|
|
1357
|
+
direct_type = PH_TO_TYPE.get(ph_type, 'body')
|
|
1358
|
+
texts.append({
|
|
1359
|
+
'sz': shape_sz(s),
|
|
1360
|
+
'box': b,
|
|
1361
|
+
'txt': txt,
|
|
1362
|
+
'style': slot_style(s),
|
|
1363
|
+
'direct_type': direct_type,
|
|
1364
|
+
'needs_role': direct_type == 'body',
|
|
1365
|
+
'source_layer': source_layer,
|
|
1366
|
+
'placeholder': '%s/%s' % (ph_type or '-', ph.get('idx') or '-'),
|
|
1367
|
+
})
|
|
1185
1368
|
texts.sort(key=lambda t: (-t['sz'], t['box'].get('y', 0)))
|
|
1186
|
-
|
|
1369
|
+
visible_shapes = layout_shapes + sh
|
|
1370
|
+
pics = [s for s in visible_shapes if s.get('kind') == 'pic' and s.get('w_pct', 0) < 95]
|
|
1187
1371
|
# 小图元素(logo / 角标 / 装饰)逐页记位置,供 archetype 落 slots
|
|
1188
1372
|
marks = [{'media': s['media'], 'box': s['box']} for s in pics
|
|
1189
1373
|
if s.get('media') and (s.get('box') or {}).get('w') and s.get('w_pct', 0) < 30]
|
|
1190
1374
|
pages.append({'part': part, 'no': slide_no(part), 'bg_media': bg_media,
|
|
1375
|
+
'rendered_bg': rendered_bg,
|
|
1191
1376
|
'bg_color': bg_of_slide.get(part), 'texts': texts, 'pic_n': len(pics),
|
|
1192
|
-
'marks': marks, 'shape_n': len(
|
|
1377
|
+
'marks': marks, 'shape_n': len(visible_shapes), 'layout': layout_part})
|
|
1193
1378
|
|
|
1194
1379
|
# 页型的**角色**(封面 / 章节页 / 内容页……)不在这里判:那是看图才能下的结论,
|
|
1195
1380
|
# 交给读得到重建图的模型。脚本只做客观归并——同一张底图 + 文字块数量相近的页
|
|
@@ -1227,6 +1412,9 @@ def draft_layouts(d, outdir):
|
|
|
1227
1412
|
rep = max(ps, key=lambda p: len(p['texts']))
|
|
1228
1413
|
if bg_raw == '__first__':
|
|
1229
1414
|
bg_raw = rep['bg_media'] or rep['bg_color'] or 'none'
|
|
1415
|
+
rendered_bg = rep.get('rendered_bg')
|
|
1416
|
+
if rendered_bg:
|
|
1417
|
+
bg_raw = rendered_bg
|
|
1230
1418
|
name = 'layout-%d' % gi
|
|
1231
1419
|
# 标题按「位置 + 跨度」认,不按字号——big-number 类的巨号数值常比标题还大
|
|
1232
1420
|
# 标题 = 该页最靠上的那批文本里最宽的一块。不按「画布前 28%」这类固定比例切:
|
|
@@ -1240,12 +1428,13 @@ def draft_layouts(d, outdir):
|
|
|
1240
1428
|
rest.sort(key=lambda t: (t['box'].get('y', 0), t['box'].get('x', 0)))
|
|
1241
1429
|
ordered = ([title] if title else []) + rest
|
|
1242
1430
|
slots = []
|
|
1243
|
-
|
|
1244
|
-
'需要更多同类槽时,按已有同类槽的间距等距延续,不要另起一套网格'
|
|
1245
|
-
'——包里的坐标是模板量出来的,自创网格等于放弃这套版式', name)
|
|
1246
|
-
for i, t in enumerate(ordered[:6]):
|
|
1431
|
+
for i, t in enumerate(ordered):
|
|
1247
1432
|
b = t['box']
|
|
1248
|
-
if t
|
|
1433
|
+
if t.get('needs_role'):
|
|
1434
|
+
role = typ = 'body'
|
|
1435
|
+
elif t.get('direct_type') in ('title', 'subtitle', 'footer', 'slide-number'):
|
|
1436
|
+
role = typ = t['direct_type']
|
|
1437
|
+
elif t is title:
|
|
1249
1438
|
role = typ = 'title'
|
|
1250
1439
|
elif (title and i == 1
|
|
1251
1440
|
# 副标题 = 紧跟在标题下方、与标题左对齐的那一块。三个量都相对标题
|
|
@@ -1261,6 +1450,12 @@ def draft_layouts(d, outdir):
|
|
|
1261
1450
|
round(b.get('w', 0)), round(b.get('h', 0))],
|
|
1262
1451
|
'type': typ, 'sz': t['sz'], 'txt': t['txt']}
|
|
1263
1452
|
row.update(t.get('style') or {})
|
|
1453
|
+
if t.get('needs_role'):
|
|
1454
|
+
row.update({
|
|
1455
|
+
'_needs_role': True,
|
|
1456
|
+
'_source_layer': t.get('source_layer'),
|
|
1457
|
+
'_placeholder': t.get('placeholder'),
|
|
1458
|
+
})
|
|
1264
1459
|
slots.append(row)
|
|
1265
1460
|
# 代表页上的小图元素按位置去重后落 slots(同一 logo 在不同页型位置不同)
|
|
1266
1461
|
seen_mark = set()
|
|
@@ -1274,7 +1469,15 @@ def draft_layouts(d, outdir):
|
|
|
1274
1469
|
'media': mk['media'],
|
|
1275
1470
|
'box': [round(b.get('x', 0)), round(b.get('y', 0)),
|
|
1276
1471
|
round(b.get('w', 0)), round(b.get('h', 0))]})
|
|
1277
|
-
|
|
1472
|
+
taken = {tuple(s['box']) for s in slots}
|
|
1473
|
+
decor = []
|
|
1474
|
+
seen_decor = set()
|
|
1475
|
+
for source_part in (rep.get('layout'), rep['part']):
|
|
1476
|
+
for item in collect_decor(shapes, source_part, taken, (cW, cH)):
|
|
1477
|
+
key = (tuple(item['box']), item['geom'], item['css'])
|
|
1478
|
+
if key not in seen_decor:
|
|
1479
|
+
seen_decor.add(key)
|
|
1480
|
+
decor.append(item)
|
|
1278
1481
|
archetypes.append({'name': name, 'bg': None, 'bg_raw': bg_raw, 'slots': slots,
|
|
1279
1482
|
'decor': decor,
|
|
1280
1483
|
'pages': sorted(p['no'] for p in ps), 'rep': rep['no'],
|
|
@@ -1337,7 +1540,7 @@ def contact_sheet(outdir, cands, path):
|
|
|
1337
1540
|
except Exception:
|
|
1338
1541
|
return None
|
|
1339
1542
|
cell, pad, cols = 220, 20, 4
|
|
1340
|
-
items = cands
|
|
1543
|
+
items = cands # 上限由调用方定,编号与 BRIEF 表格一一对应
|
|
1341
1544
|
if not items:
|
|
1342
1545
|
return None
|
|
1343
1546
|
rows = (len(items) + cols - 1) // cols
|
|
@@ -1424,10 +1627,11 @@ def emit_frontmatter(d, tokens, fonts, roles, anchors, gaps, ldir):
|
|
|
1424
1627
|
L.append('spacing:')
|
|
1425
1628
|
L.append(' page-padding: {top: %s, right: %s, bottom: %s, left: %s}'
|
|
1426
1629
|
% (edge['top'], edge['right'], edge['bottom'], edge['left']))
|
|
1427
|
-
#
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1630
|
+
# rounded.card 是全局 token,只能表达全档共同的一档圆角。多个非零档位或零/非零
|
|
1631
|
+
# 混用时,圆角属于 layouts.md 里的局部形状事实,压成一个值会把直角容器也圆角化。
|
|
1632
|
+
radii = d.get('radii_census') or []
|
|
1633
|
+
if len(radii) == 1 and radii[0]['px'] >= 1:
|
|
1634
|
+
top = radii[0]
|
|
1431
1635
|
L.append('rounded:')
|
|
1432
1636
|
L.append(' card: %dpx' % round(top['px']))
|
|
1433
1637
|
if edges_full:
|
|
@@ -1463,9 +1667,13 @@ def draft_flow(a, facts, canvas):
|
|
|
1463
1667
|
cW, cH = canvas
|
|
1464
1668
|
# 装饰件也算进来:很多模板的版式层只有几个占位符,真正撑起版面的是卡片容器
|
|
1465
1669
|
# (在 decor 里)。只看 slots 会把一页的主体结构整个漏掉。
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1670
|
+
slots = [s for s in a['slots'] if s.get('box')]
|
|
1671
|
+
fixed_roles = {'logo', 'slide-number', 'page-number', 'header', 'footer'}
|
|
1672
|
+
fixed = [s for s in slots if s.get('role') in fixed_roles]
|
|
1673
|
+
content_slots = [s for s in slots if s.get('role') not in fixed_roles]
|
|
1674
|
+
containers = [{'role': 'container', 'type': 'decor', 'box': dcr['box'],
|
|
1675
|
+
'css': dcr.get('css')} for dcr in (a.get('decor') or [])]
|
|
1676
|
+
items = group_flow_cards(content_slots, containers)
|
|
1469
1677
|
if len(items) < 2:
|
|
1470
1678
|
return None
|
|
1471
1679
|
items.sort(key=lambda s: (s['box'][1], s['box'][0]))
|
|
@@ -1476,7 +1684,7 @@ def draft_flow(a, facts, canvas):
|
|
|
1476
1684
|
return None
|
|
1477
1685
|
# 区带边界 = 间距分布里的最大空档。同一区带内部的间距(网格行距之类)总是明显
|
|
1478
1686
|
# 小于区带之间的间距,用本页自己的分布切,不设固定阈值。
|
|
1479
|
-
cut = _gap_cut(pos, min(pos), max(pos)) if len(pos) > 1 else
|
|
1687
|
+
cut = _gap_cut(pos, min(pos), max(pos)) if len(pos) > 1 else pos[0]
|
|
1480
1688
|
regions, cur = [], [items[0]]
|
|
1481
1689
|
for i, g in enumerate(gaps):
|
|
1482
1690
|
if g >= cut:
|
|
@@ -1485,6 +1693,11 @@ def draft_flow(a, facts, canvas):
|
|
|
1485
1693
|
cur.append(items[i + 1])
|
|
1486
1694
|
regions.append(cur)
|
|
1487
1695
|
|
|
1696
|
+
# 整页左右边距 = 所有内容的横向外包络,作为各区带的缺省。
|
|
1697
|
+
lefts = [s['box'][0] for s in items]
|
|
1698
|
+
rights = [s['box'][0] + s['box'][2] for s in items]
|
|
1699
|
+
page_margin = [min(lefts), cW - max(rights)]
|
|
1700
|
+
|
|
1488
1701
|
out = []
|
|
1489
1702
|
for reg in regions:
|
|
1490
1703
|
if not reg:
|
|
@@ -1507,8 +1720,17 @@ def draft_flow(a, facts, canvas):
|
|
|
1507
1720
|
if len(rows) > 1:
|
|
1508
1721
|
row_gap = round(rows[1][0]['box'][1]
|
|
1509
1722
|
- (rows[0][0]['box'][1] + rows[0][0]['box'][3]))
|
|
1510
|
-
|
|
1511
|
-
|
|
1723
|
+
region = {'kind': 'grid', 'cols': cols, 'gap': [max(col_gap, 0), max(row_gap, 0)],
|
|
1724
|
+
'items': rows[0]}
|
|
1725
|
+
# 卡片组的横向范围常和整页不同(标题贴左、卡片居中)。整页边距是所有元素的
|
|
1726
|
+
# 外包络,直接套给居中卡片组会把它拉偏成左对齐。区带范围和整页明显不一致时,
|
|
1727
|
+
# 落这个区带自己的左右边距,消费端把网格放进它再填 1fr。按落盘的整数比较,
|
|
1728
|
+
# 亚像素噪声不触发多余的区带边距。
|
|
1729
|
+
reg_margin = [min(s['box'][0] for s in rows[0]),
|
|
1730
|
+
cW - max(s['box'][0] + s['box'][2] for s in rows[0])]
|
|
1731
|
+
if [int(reg_margin[0]), int(reg_margin[1])] != [int(page_margin[0]), int(page_margin[1])]:
|
|
1732
|
+
region['margin'] = reg_margin
|
|
1733
|
+
out.append(region)
|
|
1512
1734
|
elif len(rows) == len(reg):
|
|
1513
1735
|
# 每行一个元素 = 真的竖着排
|
|
1514
1736
|
inner = 0
|
|
@@ -1519,14 +1741,91 @@ def draft_flow(a, facts, canvas):
|
|
|
1519
1741
|
# 每行元素数不一致(比如左列两张、右列一张跨两行)。硬说成 stack 会让消费端
|
|
1520
1742
|
# 以为它们是竖排的,比不给还糟。如实说这块推不出规整结构,按坐标摆。
|
|
1521
1743
|
out.append({'kind': 'free', 'items': reg})
|
|
1744
|
+
if fixed:
|
|
1745
|
+
out.append({'kind': 'free', 'items': fixed})
|
|
1522
1746
|
if len(out) < 2:
|
|
1523
1747
|
return None
|
|
1524
|
-
|
|
1525
|
-
rights = [s['box'][0] + s['box'][2] for s in items]
|
|
1526
|
-
return {'top': items[0]['box'][1], 'margin': [min(lefts), cW - max(rights)],
|
|
1748
|
+
return {'top': items[0]['box'][1], 'margin': page_margin,
|
|
1527
1749
|
'gap': round(cut), 'regions': out}
|
|
1528
1750
|
|
|
1529
1751
|
|
|
1752
|
+
def box_contains(outer, inner):
|
|
1753
|
+
return (outer[0] <= inner[0] and outer[1] <= inner[1]
|
|
1754
|
+
and outer[0] + outer[2] >= inner[0] + inner[2]
|
|
1755
|
+
and outer[1] + outer[3] >= inner[1] + inner[3])
|
|
1756
|
+
|
|
1757
|
+
|
|
1758
|
+
def boxes_overlap(a, b):
|
|
1759
|
+
return (min(a[0] + a[2], b[0] + b[2]) > max(a[0], b[0])
|
|
1760
|
+
and min(a[1] + a[3], b[1] + b[3]) > max(a[1], b[1]))
|
|
1761
|
+
|
|
1762
|
+
|
|
1763
|
+
def overlap_ratio(outer, inner):
|
|
1764
|
+
width = min(outer[0] + outer[2], inner[0] + inner[2]) - max(outer[0], inner[0])
|
|
1765
|
+
height = min(outer[1] + outer[3], inner[1] + inner[3]) - max(outer[1], inner[1])
|
|
1766
|
+
if width <= 0 or height <= 0 or inner[2] <= 0 or inner[3] <= 0:
|
|
1767
|
+
return 0
|
|
1768
|
+
return width * height / (inner[2] * inner[3])
|
|
1769
|
+
|
|
1770
|
+
|
|
1771
|
+
def group_flow_cards(slots, containers):
|
|
1772
|
+
"""把并列卡片容器及其文字组成一层 group,避免拍平成多列元素。"""
|
|
1773
|
+
candidates = []
|
|
1774
|
+
for container in containers:
|
|
1775
|
+
children = [slot for slot in slots if box_contains(container['box'], slot['box'])]
|
|
1776
|
+
if len(children) >= 2:
|
|
1777
|
+
candidates.append((container, children))
|
|
1778
|
+
selected = []
|
|
1779
|
+
for container, children in sorted(
|
|
1780
|
+
candidates, key=lambda pair: pair[0]['box'][2] * pair[0]['box'][3]):
|
|
1781
|
+
if not any(boxes_overlap(container['box'], other['box']) for other, _ in selected):
|
|
1782
|
+
selected.append((container, children))
|
|
1783
|
+
if len(selected) < 2:
|
|
1784
|
+
return slots + containers
|
|
1785
|
+
|
|
1786
|
+
grouped_slots = {id(slot) for _, children in selected for slot in children}
|
|
1787
|
+
nested_by_container = {}
|
|
1788
|
+
for container, _ in selected:
|
|
1789
|
+
nested_by_container[id(container)] = [
|
|
1790
|
+
other for other in containers
|
|
1791
|
+
if other is not container and overlap_ratio(container['box'], other['box']) >= 0.9
|
|
1792
|
+
]
|
|
1793
|
+
grouped_containers = {
|
|
1794
|
+
id(container)
|
|
1795
|
+
for container, _ in selected
|
|
1796
|
+
for container in [container] + nested_by_container[id(container)]
|
|
1797
|
+
}
|
|
1798
|
+
out = [slot for slot in slots if id(slot) not in grouped_slots]
|
|
1799
|
+
out += [container for container in containers if id(container) not in grouped_containers]
|
|
1800
|
+
for container, children in selected:
|
|
1801
|
+
children = children + nested_by_container[id(container)]
|
|
1802
|
+
children = sorted(children, key=lambda slot: (slot['box'][1], slot['box'][0]))
|
|
1803
|
+
gaps = [children[i + 1]['box'][1]
|
|
1804
|
+
- (children[i]['box'][1] + children[i]['box'][3])
|
|
1805
|
+
for i in range(len(children) - 1)]
|
|
1806
|
+
outer = container['box']
|
|
1807
|
+
insets = [
|
|
1808
|
+
min(child['box'][1] - outer[1] for child in children),
|
|
1809
|
+
min(outer[0] + outer[2] - child['box'][0] - child['box'][2] for child in children),
|
|
1810
|
+
min(outer[1] + outer[3] - child['box'][1] - child['box'][3] for child in children),
|
|
1811
|
+
min(child['box'][0] - outer[0] for child in children),
|
|
1812
|
+
]
|
|
1813
|
+
padding = max(0, round(min(insets)))
|
|
1814
|
+
css = container.get('css') or ''
|
|
1815
|
+
if padding:
|
|
1816
|
+
css = '; '.join(part for part in (
|
|
1817
|
+
css.rstrip('; '), 'box-sizing: border-box', 'padding: %dpx' % padding) if part)
|
|
1818
|
+
out.append({
|
|
1819
|
+
'role': 'group',
|
|
1820
|
+
'type': 'group',
|
|
1821
|
+
'box': outer,
|
|
1822
|
+
'css': css,
|
|
1823
|
+
'gap': max(0, round(min(gaps))) if gaps else 0,
|
|
1824
|
+
'items': children,
|
|
1825
|
+
})
|
|
1826
|
+
return out
|
|
1827
|
+
|
|
1828
|
+
|
|
1530
1829
|
def structure_facts(archetypes, d, shapes):
|
|
1531
1830
|
"""每个页型的**结构事实**:栅格、垂直间距序列、容器样式配方、样张里的实际字数。
|
|
1532
1831
|
|
|
@@ -1555,8 +1854,7 @@ def structure_facts(archetypes, d, shapes):
|
|
|
1555
1854
|
'fill': fill, 'line': line, 'fx': fx, 'shapes': set()})
|
|
1556
1855
|
g['n'] += 1
|
|
1557
1856
|
g['parts'].add(s.get('part'))
|
|
1558
|
-
|
|
1559
|
-
g['radii'].append(s['radius_px'])
|
|
1857
|
+
g['radii'].append(s.get('radius_px') or 0)
|
|
1560
1858
|
g['shapes'].add(id(s))
|
|
1561
1859
|
ranked = sorted(groups.values(), key=lambda g: -g['n'])
|
|
1562
1860
|
recipe_id = {}
|
|
@@ -1598,7 +1896,9 @@ def structure_facts(archetypes, d, shapes):
|
|
|
1598
1896
|
|
|
1599
1897
|
def emit_layouts(archetypes, ldir, busy_hints=None, facts=None, recipes=None):
|
|
1600
1898
|
prefilled = sum(1 for a in archetypes if a.get('zh'))
|
|
1601
|
-
L = ['#
|
|
1899
|
+
L = ['# 判断单草案 —— package.py 读它产出 layouts.md,deck 的版式坐标从 layouts.md 读。',
|
|
1900
|
+
'# 只改 names / roles / text_roles / layout_modes / bg_rules 五段(都是扁平键值,'
|
|
1901
|
+
'改完 package.py 自动并回各页型)。',
|
|
1602
1902
|
'# 下面 layouts 段是普查数值,一个字都不要动——改它容易连带删掉 slots/confidence。']
|
|
1603
1903
|
if prefilled:
|
|
1604
1904
|
L.append('# names 已按模板自带的版式名填好 %d 条,读一遍确认表意即可,通常不用改。' % prefilled)
|
|
@@ -1627,6 +1927,32 @@ def emit_layouts(archetypes, ldir, busy_hints=None, facts=None, recipes=None):
|
|
|
1627
1927
|
len([s for s in a['slots'] if not s.get('asset')]),
|
|
1628
1928
|
'/'.join(str(x) for x in szs[:5]) or '未声明',
|
|
1629
1929
|
a.get('pic_n') or 0, ';有满屏底图' if a.get('bg_raw') else ''))
|
|
1930
|
+
text_role_ids = {}
|
|
1931
|
+
for a in archetypes:
|
|
1932
|
+
index = 0
|
|
1933
|
+
for slot in a.get('slots') or []:
|
|
1934
|
+
if not slot.get('_needs_role'):
|
|
1935
|
+
continue
|
|
1936
|
+
index += 1
|
|
1937
|
+
text_role_ids[id(slot)] = '%s-text-%d' % (a['name'], index)
|
|
1938
|
+
if text_role_ids:
|
|
1939
|
+
L.append('text_roles: # 取值 title|subtitle|header|footer|body;只改角色,不删槽')
|
|
1940
|
+
for a in archetypes:
|
|
1941
|
+
for slot in a.get('slots') or []:
|
|
1942
|
+
role_id = text_role_ids.get(id(slot))
|
|
1943
|
+
if not role_id:
|
|
1944
|
+
continue
|
|
1945
|
+
L.append(' %s: TODO文本角色 # 来源 %s;占位符 %s;样例 %s;'
|
|
1946
|
+
'box %s;字号 %s;css %s'
|
|
1947
|
+
% (role_id, slot.get('_source_layer') or '-',
|
|
1948
|
+
slot.get('_placeholder') or '-', q(slot.get('txt') or ''),
|
|
1949
|
+
slot.get('box'), round(slot.get('sz') or 0),
|
|
1950
|
+
q(slot.get('css') or '未声明')))
|
|
1951
|
+
flow_archetypes = [a for a in archetypes if a.get('flow')]
|
|
1952
|
+
if flow_archetypes:
|
|
1953
|
+
L.append('layout_modes: # 取值 flow|slots;内容会变的内容页优先 flow,固定构图页用 slots')
|
|
1954
|
+
for a in flow_archetypes:
|
|
1955
|
+
L.append(' %s: TODO布局模式 # 依据见 layouts 段该页型上方的结构事实' % a['name'])
|
|
1630
1956
|
# 禁放区是**背景图**的属性,不是页型的属性——按背景资产分组,页型再多也不涨
|
|
1631
1957
|
bgs = []
|
|
1632
1958
|
for a in archetypes:
|
|
@@ -1679,6 +2005,13 @@ def emit_layouts(archetypes, ldir, busy_hints=None, facts=None, recipes=None):
|
|
|
1679
2005
|
% '、'.join('%s=%d字' % (b, n) for b, n in fx['chars'][:6]))
|
|
1680
2006
|
if fx.get('recipes'):
|
|
1681
2007
|
L.append(' # 命中配方:%s' % '、'.join(fx['recipes'][:4]))
|
|
2008
|
+
# 槽与槽在坐标上重叠:PPT 里占位符互相压是常态(文字 valign 居中、样张只有一行,
|
|
2009
|
+
# 看不出来),照抄坐标做成 HTML 后内容一变长就撞。实测封面 title 框比 subtitle
|
|
2010
|
+
# 的顶还低 41px,两行标题直接压在副标题上。这里只报事实,怎么让开由你定。
|
|
2011
|
+
ov = slot_overlaps(a.get('slots') or [])
|
|
2012
|
+
if ov:
|
|
2013
|
+
L.append(' # 槽位重叠:%s(模板里靠文字居中不显形,内容变长会撞)'
|
|
2014
|
+
% '、'.join(ov[:3]))
|
|
1682
2015
|
L.append(' %s:' % a['name'])
|
|
1683
2016
|
if a.get('role'):
|
|
1684
2017
|
L.append(' role: %s' % a['role'])
|
|
@@ -1686,8 +2019,6 @@ def emit_layouts(archetypes, ldir, busy_hints=None, facts=None, recipes=None):
|
|
|
1686
2019
|
L.append(' background: %s' % a['bg'])
|
|
1687
2020
|
fl = a.get('flow')
|
|
1688
2021
|
if fl:
|
|
1689
|
-
L.append(' # ↓ flow 与 slots 二选一:内容长度会变的页用 flow(区带依次排、'
|
|
1690
|
-
'高度由内容定、下面的自动被推下去),构图固定的页用 slots。删掉不要的那个。')
|
|
1691
2022
|
L.append(' flow:')
|
|
1692
2023
|
L.append(' top: %d' % fl['top'])
|
|
1693
2024
|
L.append(' margin: [%d, %d]' % tuple(fl['margin']))
|
|
@@ -1698,6 +2029,10 @@ def emit_layouts(archetypes, ldir, busy_hints=None, facts=None, recipes=None):
|
|
|
1698
2029
|
L.append(' - kind: grid')
|
|
1699
2030
|
L.append(' cols: %d' % r['cols'])
|
|
1700
2031
|
L.append(' gap: [%d, %d]' % tuple(r['gap']))
|
|
2032
|
+
if r.get('margin'):
|
|
2033
|
+
L.append(' margin: [%d, %d] # 本区带自己的左右边距,'
|
|
2034
|
+
'和整页 margin 不同(居中卡片组不跟标题的左边距)'
|
|
2035
|
+
% tuple(r['margin']))
|
|
1701
2036
|
elif r['kind'] == 'free':
|
|
1702
2037
|
L.append(' - kind: free # 推不出规整结构,按 slots 的坐标摆')
|
|
1703
2038
|
else:
|
|
@@ -1705,6 +2040,32 @@ def emit_layouts(archetypes, ldir, busy_hints=None, facts=None, recipes=None):
|
|
|
1705
2040
|
L.append(' gap: %d' % r['gap'])
|
|
1706
2041
|
L.append(' items:')
|
|
1707
2042
|
for s in r['items']:
|
|
2043
|
+
if s.get('type') == 'group':
|
|
2044
|
+
L.append(' - role: group')
|
|
2045
|
+
L.append(' gap: %d' % s['gap'])
|
|
2046
|
+
if s.get('css'):
|
|
2047
|
+
L.append(' css: "%s"'
|
|
2048
|
+
% str(s['css']).replace('"', "'"))
|
|
2049
|
+
L.append(' items:')
|
|
2050
|
+
for child in s['items']:
|
|
2051
|
+
role_id = text_role_ids.get(id(child))
|
|
2052
|
+
if role_id:
|
|
2053
|
+
L.append(' # text-role: %s' % role_id)
|
|
2054
|
+
if child.get('type') == 'decor':
|
|
2055
|
+
L.append(' - {role: container, css: "%s"}'
|
|
2056
|
+
% str(child.get('css') or '').replace('"', "'"))
|
|
2057
|
+
continue
|
|
2058
|
+
extra = ''
|
|
2059
|
+
if child.get('css') is not None:
|
|
2060
|
+
extra += ', css: "%s"' % str(child['css']).replace('"', "'")
|
|
2061
|
+
if child.get('asset'):
|
|
2062
|
+
extra += ', asset: %s' % child['asset']
|
|
2063
|
+
L.append(' - {role: %s, type: %s%s}'
|
|
2064
|
+
% (child['role'], child['type'], extra))
|
|
2065
|
+
continue
|
|
2066
|
+
role_id = text_role_ids.get(id(s))
|
|
2067
|
+
if role_id:
|
|
2068
|
+
L.append(' # text-role: %s' % role_id)
|
|
1708
2069
|
# free 区带按坐标摆,而 slots 会被删掉,所以坐标必须写在这里
|
|
1709
2070
|
bx = ', box: %s' % s['box'] if r['kind'] == 'free' else ''
|
|
1710
2071
|
if s.get('type') == 'decor':
|
|
@@ -1712,22 +2073,22 @@ def emit_layouts(archetypes, ldir, busy_hints=None, facts=None, recipes=None):
|
|
|
1712
2073
|
% (bx, (s.get('css') or '').replace('"', "'")))
|
|
1713
2074
|
continue
|
|
1714
2075
|
extra = bx
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
extra += ', %s: %s' % (k, '"%s"' % s[k] if k == 'color' else s[k])
|
|
2076
|
+
if s.get('css') is not None:
|
|
2077
|
+
# CSS 串一律加引号:里面的逗号/冒号在 flow map 里是分隔符
|
|
2078
|
+
extra += ', css: "%s"' % str(s['css']).replace('"', "'")
|
|
1719
2079
|
if s.get('asset'):
|
|
1720
2080
|
extra += ', asset: %s' % s['asset']
|
|
1721
2081
|
L.append(' - {role: %s, type: %s%s}' % (s['role'], s['type'], extra))
|
|
1722
2082
|
L.append(' slots:')
|
|
1723
2083
|
for s in a['slots']:
|
|
2084
|
+
role_id = text_role_ids.get(id(s))
|
|
2085
|
+
if role_id:
|
|
2086
|
+
L.append(' # text-role: %s' % role_id)
|
|
1724
2087
|
extra = ''
|
|
1725
2088
|
if s.get('asset'):
|
|
1726
2089
|
extra += ', asset: %s' % s['asset']
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
v = s[k]
|
|
1730
|
-
extra += ', %s: %s' % (k, '"%s"' % v if k == 'color' else v)
|
|
2090
|
+
if s.get('css') is not None:
|
|
2091
|
+
extra += ', css: "%s"' % str(s['css']).replace('"', "'")
|
|
1731
2092
|
L.append(' - {role: %s, box: %s, type: %s%s}'
|
|
1732
2093
|
% (s['role'], s['box'], s['type'], extra))
|
|
1733
2094
|
if a.get('decor'):
|
|
@@ -1754,7 +2115,7 @@ def emit_body(d, tokens, fonts, roles, assets, archetypes, exceptions, cusage, l
|
|
|
1754
2115
|
|
|
1755
2116
|
L = ['## Overview', '',
|
|
1756
2117
|
'TODO: 两三句话讲清这套模板的性格与适用场景——看过联系表和页面重建图之后再写。', '']
|
|
1757
|
-
L.append(('模板自带 %d
|
|
2118
|
+
L.append(('模板自带 %d 种版式,页型、坐标和 CSS 样式都直读自版式层。'
|
|
1758
2119
|
% len(archetypes)) if (d.get('form_hint') or {}).get('form') == 3 else
|
|
1759
2120
|
('%d 页样张归纳出 %d 种页型。' % (d['counts']['slides'], len(archetypes))))
|
|
1760
2121
|
L += ['', '## Usage', '',
|
|
@@ -1769,27 +2130,43 @@ def emit_body(d, tokens, fonts, roles, assets, archetypes, exceptions, cusage, l
|
|
|
1769
2130
|
% sidecar,
|
|
1770
2131
|
'3. **按页型给的形态落元素** —— 页型给 `flow` 就用流式,给 `slots` 就用绝对,'
|
|
1771
2132
|
'两者只会出现一个。'
|
|
1772
|
-
'**flow**:整块用一个纵向 flex 容器,`top` 是它的起始 y,`margin`
|
|
2133
|
+
'**flow**:整块用一个纵向 flex 容器,`top` 是它的起始 y,`margin` 是整块的左右边距,'
|
|
1773
2134
|
'`gap` 是区带之间的间距;`regions` 从上往下依次排,**每个区带的高度由它自己的'
|
|
1774
2135
|
'内容决定,不要写死高度**——上面的区带内容变多时,下面的自然被推下去,这正是'
|
|
1775
2136
|
'这套表达要解决的事。区带内部:`kind: grid` 用 `grid-template-columns: repeat(cols, 1fr)` '
|
|
1776
2137
|
'配 `gap: [行间距, 列间距]`;`kind: stack` 用纵向 flex 配 `gap`;`kind: free` '
|
|
1777
|
-
'
|
|
1778
|
-
'
|
|
2138
|
+
'按 item 自带的 `box` 绝对定位。区带自带 `margin: [左, 右]` 时用它的、'
|
|
2139
|
+
'覆盖整块的 `margin`(模板里居中的卡片组和贴左的标题横向范围本就不同);'
|
|
2140
|
+
'没带就用整块的 `margin`。`grid` 在自己这份左右边距里再 `repeat(cols, 1fr)`。'
|
|
2141
|
+
'`grid` 里的 `role: group` 是一张卡片:'
|
|
2142
|
+
'group 的 `css` 用于外层容器,内部 `items` 按顺序纵向排布并使用 group 的 `gap`。'
|
|
2143
|
+
'每个 `role: container` 的项是容器,把它的 `css` 逐项原样写进 style,内容放进去;'
|
|
2144
|
+
'其中没有 `border-radius` 就按 `0`,不得自行补圆角。',
|
|
1779
2145
|
'4. **按 slot 落元素(页型给的是 slots 时)** —— 每个 slot 渲染成一个绝对定位元素:`box` 是 '
|
|
1780
|
-
'`[x, y, w, h]`(%dx%d
|
|
1781
|
-
'
|
|
2146
|
+
'`[x, y, w, h]`(%dx%d 画布上的绝对像素),机械展开成 `left/top/width/height`;'
|
|
2147
|
+
'slot 的 `css` 是模板排版属性已转译好的声明串,原样写进 style,不要另选字号、'
|
|
2148
|
+
'内边距、颜色或对齐。'
|
|
1782
2149
|
'带 `asset` 的 slot 是图片元素(logo、角标),把该资产放在它自己的 `box` 里;'
|
|
1783
2150
|
'这个页型没有 `asset` 槽,这一页就不出现该资产。' % (canvas[0], canvas[1]),
|
|
1784
2151
|
'5. **铺装饰几何** —— 页型的 `decor` 是这一页的图形骨架(图标托底的圆、'
|
|
1785
|
-
'卡片、分隔线):每条渲染成一个绝对定位空元素,`box` 给位置,`css`
|
|
1786
|
-
'`geom: ellipse` 另加
|
|
2152
|
+
'卡片、分隔线):每条渲染成一个绝对定位空元素,`box` 给位置,`css` 逐项原样写进 '
|
|
2153
|
+
'style;没有 `border-radius` 就按 `0`。只有 `geom: ellipse` 另加 '
|
|
2154
|
+
'`border-radius: 50%`。它们压在背景之上、slot 之下,'
|
|
1787
2155
|
'落在 slot 上的图标正是靠它们托住。',
|
|
1788
|
-
'6.
|
|
2156
|
+
'6. **落实全局设计** —— `design.md` frontmatter 的 `colors`、`typography`、'
|
|
2157
|
+
'`spacing`、`rounded`、`components` 是全局 token;用 CSS variables、类名或内联'
|
|
2158
|
+
'样式承载。局部 slot / decor 的 `css` 优先,不能再解释成另一套视觉系统。'
|
|
2159
|
+
'字体使用 Typography 的完整栈与降级,不在运行时安装字体或依赖。',
|
|
2160
|
+
'7. **保持标题结构** —— 有合适页型可参考时,沿用该页型已有的标题层级与局部 '
|
|
2161
|
+
'`css`;只渲染该页型已有的文字槽,背景中已经可见的固定标题不再创建文本,'
|
|
2162
|
+
'页型没有 `subtitle` 槽就不新增副标题。没有合适参考时,按本包整体视觉组织标题。']
|
|
1789
2163
|
if assets:
|
|
1790
2164
|
L += ['', '资产文件(背景由页型的 `background` 字段指定,'
|
|
1791
2165
|
'图片资产的位置由该页型 `slots` 里带 `asset` 的槽给出):', '',
|
|
1792
|
-
'{{ASSET_TABLE}}'
|
|
2166
|
+
'{{ASSET_TABLE}}', '',
|
|
2167
|
+
'将包内 `assets/` 复制到项目内相对目录,再引用复制后的路径;最终 HTML 不引用'
|
|
2168
|
+
'抽取工作目录或本机绝对路径。附件只提供 `assetRoot` / `assetPaths` 时,把'
|
|
2169
|
+
'`assetRoot` 当作不透明前缀,只拼接清单中声明的相对路径。']
|
|
1793
2170
|
L += ['', '文字与容器的外接矩形落在该页型 `background` 对应的 `text_safe` 内,'
|
|
1794
2171
|
'避开 `avoid` 列出的区域(两者都在 %s 的 `backgrounds` 段)。内容装不下时换页型或拆页。'
|
|
1795
2172
|
% sidecar, '',
|
|
@@ -1804,7 +2181,8 @@ def emit_body(d, tokens, fonts, roles, assets, archetypes, exceptions, cusage, l
|
|
|
1804
2181
|
',源为商业/内部字体无 web 分发源,按气质降级到 %s' % f['stack'][1]
|
|
1805
2182
|
if len(f['stack']) > 1 else ''))
|
|
1806
2183
|
L += ['', '字号轴:' + '、'.join('%s %dpx' % (k, round(v['sz_px'])) for k, v in roles.items())
|
|
1807
|
-
+ '。slot 自带 `size`
|
|
2184
|
+
+ '。slot 自带 `css` 时以其中的 `font-size` 为准;没有 slot CSS 的新增层级,'
|
|
2185
|
+
'复用轴上最接近的一档。', '',
|
|
1808
2186
|
'字体加载(**HARD REQUIREMENT:下面这行 @import 原样写入全局样式首行,禁止替换为 '
|
|
1809
2187
|
'fonts.googleapis.com 或其他域**):', '', '```', imp, '```', '',
|
|
1810
2188
|
'镜像只保证 wght 400 一档,更粗的字重由浏览器合成,字重不能作为唯一区分手段;'
|
|
@@ -1827,7 +2205,15 @@ def emit_body(d, tokens, fonts, roles, assets, archetypes, exceptions, cusage, l
|
|
|
1827
2205
|
% (logo['id'], '、'.join('`%s`' % x for x in with_logo) or '(无)'))
|
|
1828
2206
|
L += ['- 坐标、字号、色值、资产位置以 %s 为准;本文件的 Colors / Typography 是可用值的清单。'
|
|
1829
2207
|
% sidecar,
|
|
1830
|
-
'-
|
|
2208
|
+
'- 强调色族以 Colors 和 %s 的 slot CSS 为主;必要时可以使用 Colors 之外的颜色,'
|
|
2209
|
+
'但不能形成与模板主色竞争的第二强调色。' % sidecar,
|
|
2210
|
+
'- 新增颜色应与模板整体的色相、明度和饱和度关系协调。允许新增中性色、低彩度辅助色'
|
|
2211
|
+
'或局部语义色表达正负、风险、警告、状态、图表序列,但保持辅助层级;'
|
|
2212
|
+
'只要新色通过高饱和、高对比、大面积或跨页重复获得主视觉权重,'
|
|
2213
|
+
'或被用于标题、关键数字、图表主序列、卡片底色或渐变,就属于新的强调色,改用模板'
|
|
2214
|
+
'强调色族的深浅、透明度,或改用线型、纹理、标签区分。',
|
|
2215
|
+
'- 交付前逐页检查:色板、字体、版式、背景、资产和本段规则均来自本风格包;'
|
|
2216
|
+
'页面无资源加载失败、内容溢出或画幅裁切。',
|
|
1831
2217
|
'- 本包里的数值就是普查结果,照用即可,无需重新统计颜色、字体或版式。',
|
|
1832
2218
|
'- 风格包以文本形式(zip 摘要等)到手时,直接用摘要里 design.md / layouts.md 的文本。',
|
|
1833
2219
|
'', '## Exceptions', '']
|
|
@@ -1841,9 +2227,9 @@ def emit_body(d, tokens, fonts, roles, assets, archetypes, exceptions, cusage, l
|
|
|
1841
2227
|
|
|
1842
2228
|
def emit_brief(d, ctx, ldir):
|
|
1843
2229
|
(tokens, rest, fonts, roles, assets, rejected, todos, archetypes, cands, sheet,
|
|
1844
|
-
leftover, lsheet) = ctx
|
|
2230
|
+
leftover, lsheet, sheet_n) = ctx
|
|
1845
2231
|
canvas = d['canvas']['px']
|
|
1846
|
-
L = ['#
|
|
2232
|
+
L = ['# 抽取简报(第 1/3 步产物;改完草案跑 package.py 出包)', '',
|
|
1847
2233
|
'源:`%s` 画布 %dx%d %d 页 / %d 版式 主题 %s form=%s'
|
|
1848
2234
|
% (d['source']['filename'], canvas[0], canvas[1], d['counts']['slides'],
|
|
1849
2235
|
d['counts']['layouts'], d['theme_topology']['themes'],
|
|
@@ -1878,16 +2264,20 @@ def emit_brief(d, ctx, ldir):
|
|
|
1878
2264
|
for t in todos:
|
|
1879
2265
|
L.append('- ' + t)
|
|
1880
2266
|
L += ['', '## 联系表(一次看完所有候选图)', '',
|
|
1881
|
-
'`l-out/contact-sheet.png` ——
|
|
2267
|
+
'`l-out/contact-sheet.png` —— 图格编号对应下表前几行;看完再决定 logo / 封面归属。' if sheet
|
|
1882
2268
|
else '(Pillow 不可用,未生成联系表;逐张看 `media-out/`)', '',
|
|
1883
2269
|
'| # | 文件 | 尺寸 | 出现 | 满屏 | 页 | 草案判定 |', '|---|---|---|---|---|---|---|']
|
|
1884
2270
|
decided = {a['src']['file']: a['id'] for a in assets}
|
|
1885
2271
|
why = {c['file']: r for c, r in rejected}
|
|
1886
|
-
for i, c in enumerate(cands
|
|
2272
|
+
for i, c in enumerate(cands, 1):
|
|
1887
2273
|
L.append('| %d | `%s` | %sx%s | %d | %s | %s | %s |' % (
|
|
1888
2274
|
i, c['file'], c['probe'].get('w') or '?', c['probe'].get('h') or '?', c['n'],
|
|
1889
2275
|
'Y' if c['fullscreen'] else '', ','.join(map(str, c['slides'][:6])) or 'layout',
|
|
1890
2276
|
decided.get(c['file']) or ('✗ ' + why.get(c['file'], '未采纳'))))
|
|
2277
|
+
if len(cands) > sheet_n:
|
|
2278
|
+
L.append('')
|
|
2279
|
+
L.append('拼版图只含前 %d 张(第 %d 行之后的没有图格)。要看后面某张,'
|
|
2280
|
+
'按文件名直接看 `media-out/`。' % (sheet_n, sheet_n))
|
|
1891
2281
|
L += ['', '## 颜色(草案 token 已写进 frontmatter.yaml)', '',
|
|
1892
2282
|
'| token | hex | 出现 |', '|---|---|---|']
|
|
1893
2283
|
for name, r in tokens:
|
|
@@ -1913,7 +2303,7 @@ def emit_brief(d, ctx, ldir):
|
|
|
1913
2303
|
if leftover:
|
|
1914
2304
|
L += ['', '未归入 archetype 的页:%s —— 都是单页孤例,需要就自己补一个 archetype。'
|
|
1915
2305
|
% ', '.join(map(str, leftover))]
|
|
1916
|
-
L += ['', '各 archetype 的 slot
|
|
2306
|
+
L += ['', '各 archetype 的 slot 原文(据此起中文页型名,并在 text_roles 判断文本角色):', '']
|
|
1917
2307
|
for a in archetypes:
|
|
1918
2308
|
L.append('- `%s`(第 %s 页,覆盖 %s)' % (a['name'], a['rep'], a['pages']))
|
|
1919
2309
|
for s in a['slots']:
|
|
@@ -1939,9 +2329,18 @@ def main(argv=None):
|
|
|
1939
2329
|
tokens, rest, rows = draft_colors(d, cusage)
|
|
1940
2330
|
fonts = draft_fonts(d)
|
|
1941
2331
|
archetypes, pages, leftover = draft_layouts(d, outdir)
|
|
2332
|
+
# 封面底图:form=3 的页型键就是角色名(cover/section/...),直接按名字取。
|
|
2333
|
+
# form=2 按样张聚类,键是 layout-1..N,永远匹配不上 'cover'——实测 vo-lite 因此
|
|
2334
|
+
# 一张 role: cover 都没有,封面主视觉被标成 bg-content-1,消费端拿不到封面资产,
|
|
2335
|
+
# design.md 的「封面底图必用 cover 资产」这条硬规则无从满足。回退到覆盖第 1 页的
|
|
2336
|
+
# 那个页型:deck 的第 1 页就是封面,这是版式无关的事实。
|
|
1942
2337
|
cover_media = next((a['bg_raw'] for a in archetypes if a['name'] == 'cover'), None)
|
|
1943
|
-
|
|
1944
|
-
|
|
2338
|
+
if not cover_media:
|
|
2339
|
+
cover_media = next((a['bg_raw'] for a in archetypes
|
|
2340
|
+
if 1 in (a.get('pages') or ())), None)
|
|
2341
|
+
exported_media = {m['media'] for m in d.get('media', []) if m.get('exported')}
|
|
2342
|
+
bg_needed = {a['bg_raw'] for a in archetypes if a['bg_raw'] in exported_media}
|
|
2343
|
+
bg_under = {p['no']: p.get('rendered_bg') or p['bg_media'] for p in pages}
|
|
1945
2344
|
assets, rejected, todos, alias, pool = draft_assets(d, outdir, bg_needed, cover_media, bg_under)
|
|
1946
2345
|
media_to_asset = {a['src']['media']: a['id'] for a in assets}
|
|
1947
2346
|
for m, w in (alias or {}).items():
|
|
@@ -1995,9 +2394,23 @@ def main(argv=None):
|
|
|
1995
2394
|
a['slots'] = keep
|
|
1996
2395
|
roles = draft_scale(d, archetypes)
|
|
1997
2396
|
slot_added = cover_slot_colors(tokens, archetypes, rows, cusage)
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2397
|
+
# 进包的资产必须全部上联系表。BRIEF 让 L 层「看联系表确认 logo / 封面归属」,
|
|
2398
|
+
# 表上没有的东西它只会从表里另挑一张顶上去。封面主视觉按定义只出现在封面那一页
|
|
2399
|
+
# (n=1),按出现次数排序时排在最末——实测被 cands[:12] 截掉,模型于是把 bg-cover
|
|
2400
|
+
# 换成了已经在用的内容页背景,封面与内容页字节相同,封面主视觉整个丢失。
|
|
2401
|
+
decided_c = sorted([a['src'] for a in assets], key=lambda c: (-c['n'], c['file']))
|
|
2402
|
+
other_c = sorted([c for c, _ in rejected], key=lambda c: (-c['n'], c['file']))
|
|
2403
|
+
cands, seen_file = [], set()
|
|
2404
|
+
for c in decided_c + other_c: # 同一张图可能有多条候选记录(不同位置各一条)
|
|
2405
|
+
if c['file'] not in seen_file:
|
|
2406
|
+
seen_file.add(c['file'])
|
|
2407
|
+
cands.append(c)
|
|
2408
|
+
# 表列全部候选,拼版图只拼前几张:两者成本差着数量级。表是文字,60 行也几乎不占
|
|
2409
|
+
# 上下文,却是模型唯一能知道「存在这张图」的地方——名额砍在这里,被误判成未采纳的
|
|
2410
|
+
# 图连翻案的机会都没有。拼版图是要「看」的,60 格就是 4 列×15 行、降采样后每格
|
|
2411
|
+
# 糊成一团,那个上限才有意义。
|
|
2412
|
+
sheet_items = cands[:max(SHEET_CAP, len(decided_c))]
|
|
2413
|
+
sheet = contact_sheet(outdir, sheet_items, os.path.join(ldir, 'contact-sheet.png'))
|
|
2001
2414
|
lsheet = layout_sheet(outdir, archetypes, os.path.join(ldir, 'layout-sheet.png'))
|
|
2002
2415
|
|
|
2003
2416
|
anchors = draft_anchors(d, tokens, fonts, roles, assets, archetypes)
|
|
@@ -2040,7 +2453,7 @@ def main(argv=None):
|
|
|
2040
2453
|
gaps.append('源字体 %s 不在 font-fallback 表里,字体栈只有原名,消费端很可能装不上;'
|
|
2041
2454
|
'按气质挑一个有 web 分发源的近似体补进栈,不要照抄原名。' % f['names'][0])
|
|
2042
2455
|
nosize = [(a['name'], s['box']) for a in archetypes for s in a['slots']
|
|
2043
|
-
if not s.get('asset') and not s.get('
|
|
2456
|
+
if not s.get('asset') and not s.get('_font_size')]
|
|
2044
2457
|
if nosize:
|
|
2045
2458
|
gaps.append('这些文字槽在源文件任何层级都没有字号声明(都不是占位符,是普通文本框,'
|
|
2046
2459
|
'继承源是 presentation.xml 的 defaultTextStyle,本抽取按约定不解继承链):'
|
|
@@ -2068,12 +2481,15 @@ def main(argv=None):
|
|
|
2068
2481
|
emit_layouts(archetypes, ldir, busy_hints, facts, recipes)
|
|
2069
2482
|
emit_body(d, tokens, fonts, roles, assets, archetypes, exceptions, cusage, ldir)
|
|
2070
2483
|
emit_brief(d, (tokens, rest, fonts, roles, assets, rejected, todos, archetypes, cands, sheet,
|
|
2071
|
-
leftover, lsheet), ldir)
|
|
2484
|
+
leftover, lsheet, len(sheet_items)), ldir)
|
|
2072
2485
|
|
|
2073
|
-
|
|
2074
|
-
|
|
2486
|
+
# 这几行落在模型判断「skill 是不是做完了」的那一刻。只报数就会被读成「包已生成」,
|
|
2487
|
+
# 于是判断和打包整段被跳过,deck 拿不到任何版式坐标。所以这里报进度与下一条命令。
|
|
2488
|
+
print('第 1/3 步完成,判断单草案 -> %s' % ldir)
|
|
2489
|
+
print(' 待你确认:资产 %d(%s) 版式 %d 色 %d 字体 %d'
|
|
2075
2490
|
% (len(assets), ', '.join(x['id'] for x in assets), len(archetypes), len(tokens), len(fonts)))
|
|
2076
|
-
print('
|
|
2491
|
+
print(' 第 2 步 读 l-out/BRIEF.md 与 contact-sheet.png,改掉草案里的 TODO')
|
|
2492
|
+
print(' 第 3 步 package.py 产出 design.md + layouts.md —— deck 的版式坐标只从这两份读')
|
|
2077
2493
|
sys.stdout.flush()
|
|
2078
2494
|
return 0
|
|
2079
2495
|
|