cardbuddy 0.11.0 → 0.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (313) hide show
  1. package/README.md +46 -5
  2. package/dist/cli.js +51 -1
  3. package/dist/config.js +51 -16
  4. package/dist/core.js +2 -2
  5. package/dist/init.js +425 -0
  6. package/dist/preview-editor/assets/index-CHuGGIR3.js +276 -0
  7. package/dist/preview-editor/assets/{index-B4VZgNSK.css → index-CQScMhyg.css} +1 -1
  8. package/dist/preview-editor/index.html +2 -2
  9. package/dist/standalone.js +8 -0
  10. package/dist/vendored/mcp/client.mjs +537 -0
  11. package/dist/vendored/mcp/tools.mjs +268 -0
  12. package/dist/vendored/preview/assets/default-image.png +0 -0
  13. package/dist/{server-core → vendored/server/core}/ai-chat.js +82 -38
  14. package/dist/{server-core → vendored/server/core}/browser.js +1 -1
  15. package/dist/{server-core → vendored/server/core}/cardSave.js +8 -3
  16. package/dist/{server-core → vendored/server/core}/cardSync.js +6 -14
  17. package/dist/{server-core → vendored/server/core}/cardValidate.js +11 -8
  18. package/dist/{server-core → vendored/server/core}/index.js +5 -3
  19. package/dist/vendored/server/core/skillPaths.js +124 -0
  20. package/dist/{server-core → vendored/server/core}/sliceUpload.js +5 -8
  21. package/dist/vendored/skills/README.md +93 -0
  22. package/dist/vendored/skills/card-doctor/SKILL.md +52 -0
  23. package/dist/vendored/skills/card-skill-trainer/SKILL.md +312 -0
  24. package/dist/vendored/skills/card-skill-trainer/baselines/v3-r038n.json +43 -0
  25. package/dist/vendored/skills/card-skill-trainer/baselines/v3-r039n.json +47 -0
  26. package/dist/vendored/skills/card-skill-trainer/baselines/v3-r050n.json +48 -0
  27. package/dist/vendored/skills/card-skill-trainer/scripts/build_dataset.py +472 -0
  28. package/dist/vendored/skills/card-skill-trainer/scripts/build_sample.mjs +162 -0
  29. package/dist/vendored/skills/card-skill-trainer/scripts/diagnose_case.mjs +241 -0
  30. package/dist/vendored/skills/card-skill-trainer/scripts/lib/scoring.mjs +525 -0
  31. package/dist/vendored/skills/card-skill-trainer/scripts/round_diff.mjs +133 -0
  32. package/dist/vendored/skills/card-skill-trainer/scripts/run_round.mjs +1043 -0
  33. package/dist/vendored/skills/card-skill-trainer/scripts/score_round.mjs +195 -0
  34. package/dist/vendored/skills/card-skill-trainer/scripts/test_missing_slice.py +122 -0
  35. package/dist/vendored/skills/dsl-card-ai/SKILL.md +191 -0
  36. package/dist/vendored/skills/dsl-card-generator/SKILL.md +480 -0
  37. package/dist/vendored/skills/dsl-card-generator/config/render-contract.json +11 -0
  38. package/dist/vendored/skills/dsl-card-generator/references/amount_formatting.md +138 -0
  39. package/dist/vendored/skills/dsl-card-generator/references/card_index.json +1597 -0
  40. package/dist/vendored/skills/dsl-card-generator/references/card_templates.json +384 -0
  41. package/dist/vendored/skills/dsl-card-generator/references/card_to_summary.md +56 -0
  42. package/dist/vendored/skills/dsl-card-generator/references/component_rules.md +76 -0
  43. package/dist/vendored/skills/dsl-card-generator/references/component_specs.md +393 -0
  44. package/dist/vendored/skills/dsl-card-generator/references/dsl_structure.md +208 -0
  45. package/dist/vendored/skills/dsl-card-generator/references/event_patterns.md +287 -0
  46. package/dist/vendored/skills/dsl-card-generator/references/expression_engine.md +121 -0
  47. package/dist/vendored/skills/dsl-card-generator/references/field_name_dict.json +34 -0
  48. package/dist/vendored/skills/dsl-card-generator/references/field_naming.md +101 -0
  49. package/dist/vendored/skills/dsl-card-generator/references/generation_pitfalls.md +928 -0
  50. package/dist/vendored/skills/dsl-card-generator/references/handcrafted/README.md +69 -0
  51. package/dist/vendored/skills/dsl-card-generator/references/handcrafted/examples_index.json +2967 -0
  52. package/dist/vendored/skills/dsl-card-generator/references/handcrafted/patterns.json +2835 -0
  53. package/dist/vendored/skills/dsl-card-generator/references/handcrafted/recipes_v2.json +648 -0
  54. package/dist/vendored/skills/dsl-card-generator/references/handcrafted/recipes_v2_schema.md +295 -0
  55. package/dist/vendored/skills/dsl-card-generator/references/image_diff.md +48 -0
  56. package/dist/vendored/skills/dsl-card-generator/references/image_to_card.md +129 -0
  57. package/dist/vendored/skills/dsl-card-generator/references/ir-format.md +212 -0
  58. package/dist/vendored/skills/dsl-card-generator/references/message-center-pattern.md +149 -0
  59. package/dist/vendored/skills/dsl-card-generator/references/platform_jsapi.md +196 -0
  60. package/dist/vendored/skills/dsl-card-generator/references/previewer_contract.md +54 -0
  61. package/dist/vendored/skills/dsl-card-generator/references/quickref.md +197 -0
  62. package/dist/vendored/skills/dsl-card-generator/references/recipe_snippets/absolute-badge.json +84 -0
  63. package/dist/vendored/skills/dsl-card-generator/references/recipe_snippets/accordion-group-toggle.json +18 -0
  64. package/dist/vendored/skills/dsl-card-generator/references/recipe_snippets/alertCard-pair.json +12 -0
  65. package/dist/vendored/skills/dsl-card-generator/references/recipe_snippets/alertDialog-call.json +12 -0
  66. package/dist/vendored/skills/dsl-card-generator/references/recipe_snippets/canvas-countdown.json +15 -0
  67. package/dist/vendored/skills/dsl-card-generator/references/recipe_snippets/canvas-donut.json +15 -0
  68. package/dist/vendored/skills/dsl-card-generator/references/recipe_snippets/canvas-gauge-score.json +11 -0
  69. package/dist/vendored/skills/dsl-card-generator/references/recipe_snippets/canvas-radar.json +10 -0
  70. package/dist/vendored/skills/dsl-card-generator/references/recipe_snippets/canvas-title-head.json +13 -0
  71. package/dist/vendored/skills/dsl-card-generator/references/recipe_snippets/globalSet.json +12 -0
  72. package/dist/vendored/skills/dsl-card-generator/references/recipe_snippets/label-col-modes.json +21 -0
  73. package/dist/vendored/skills/dsl-card-generator/references/recipe_snippets/list-expand-collapse.json +17 -0
  74. package/dist/vendored/skills/dsl-card-generator/references/recipe_snippets/money-format.json +11 -0
  75. package/dist/vendored/skills/dsl-card-generator/references/recipe_snippets/opacity-switch.json +130 -0
  76. package/dist/vendored/skills/dsl-card-generator/references/recipe_snippets/progress-bar.json +25 -0
  77. package/dist/vendored/skills/dsl-card-generator/references/recipe_snippets/tab-billMode.json +15 -0
  78. package/dist/vendored/skills/dsl-card-generator/references/recipe_snippets/title-underline-overlay.json +21 -0
  79. package/dist/vendored/skills/dsl-card-generator/references/recipes.md +797 -0
  80. package/dist/vendored/skills/dsl-card-generator/references/rules.md +235 -0
  81. package/dist/vendored/skills/dsl-card-generator/references/slices_convention.md +177 -0
  82. package/dist/vendored/skills/dsl-card-generator/references/spec_to_schema_patterns.md +609 -0
  83. package/dist/vendored/skills/dsl-card-generator/references/ui_fidelity_rules.md +131 -0
  84. package/dist/vendored/skills/dsl-card-generator/scripts/add_component.mjs +118 -0
  85. package/dist/vendored/skills/dsl-card-generator/scripts/analyze_component_usage.mjs +153 -0
  86. package/dist/vendored/skills/dsl-card-generator/scripts/apply_opacity_pattern.mjs +343 -0
  87. package/dist/vendored/skills/dsl-card-generator/scripts/build_card_index.mjs +108 -0
  88. package/dist/vendored/skills/dsl-card-generator/scripts/build_card_templates.mjs +190 -0
  89. package/dist/vendored/skills/dsl-card-generator/scripts/check_fidelity.mjs +257 -0
  90. package/dist/vendored/skills/dsl-card-generator/scripts/check_magic_values.mjs +79 -0
  91. package/dist/vendored/skills/dsl-card-generator/scripts/crop_slices.mjs +98 -0
  92. package/dist/vendored/skills/dsl-card-generator/scripts/describe_card.mjs +265 -0
  93. package/dist/vendored/skills/dsl-card-generator/scripts/diff_bands.mjs +257 -0
  94. package/dist/vendored/skills/dsl-card-generator/scripts/diff_card.mjs +316 -0
  95. package/dist/vendored/skills/dsl-card-generator/scripts/expand_ir.mjs +873 -0
  96. package/dist/vendored/skills/dsl-card-generator/scripts/extract_handcrafted.py +1199 -0
  97. package/dist/vendored/skills/dsl-card-generator/scripts/extract_subtree.mjs +191 -0
  98. package/dist/vendored/skills/dsl-card-generator/scripts/generate_cards.mjs +389 -0
  99. package/dist/vendored/skills/dsl-card-generator/scripts/inventory_schemas.py +160 -0
  100. package/dist/vendored/skills/dsl-card-generator/scripts/iterate.mjs +700 -0
  101. package/dist/vendored/skills/dsl-card-generator/scripts/lib/design_image.mjs +56 -0
  102. package/dist/vendored/skills/dsl-card-generator/scripts/lib/design_sample.mjs +328 -0
  103. package/dist/vendored/skills/dsl-card-generator/scripts/lib/layout-tokens.mjs +79 -0
  104. package/dist/vendored/skills/dsl-card-generator/scripts/lib/paths.mjs +103 -0
  105. package/dist/vendored/skills/dsl-card-generator/scripts/lib/scaffold.mjs +575 -0
  106. package/dist/vendored/skills/dsl-card-generator/scripts/lib/slicePath.mjs +81 -0
  107. package/dist/vendored/skills/dsl-card-generator/scripts/lib/slice_dedupe.mjs +147 -0
  108. package/dist/vendored/skills/dsl-card-generator/scripts/lint_autofix.mjs +363 -0
  109. package/dist/vendored/skills/dsl-card-generator/scripts/lint_card.mjs +1399 -0
  110. package/dist/vendored/skills/dsl-card-generator/scripts/preflight.mjs +249 -0
  111. package/dist/vendored/skills/dsl-card-generator/scripts/register_card_template.mjs +208 -0
  112. package/dist/vendored/skills/dsl-card-generator/scripts/render_baseline.mjs +231 -0
  113. package/dist/vendored/skills/dsl-card-generator/scripts/render_card.mjs +801 -0
  114. package/dist/vendored/skills/dsl-card-generator/scripts/render_diff.mjs +239 -0
  115. package/dist/vendored/skills/dsl-card-generator/scripts/resolve_card_template.mjs +73 -0
  116. package/dist/vendored/skills/dsl-card-generator/scripts/route_input.mjs +447 -0
  117. package/dist/vendored/skills/dsl-card-generator/scripts/scaffold_card.mjs +81 -0
  118. package/dist/vendored/skills/dsl-card-generator/scripts/slices_normalize.mjs +316 -0
  119. package/dist/vendored/skills/dsl-card-generator/scripts/spec_audit.mjs +462 -0
  120. package/dist/vendored/skills/dsl-card-generator/scripts/spec_to_schema_v3.mjs +7735 -0
  121. package/dist/vendored/skills/dsl-card-generator/scripts/spec_tree.mjs +355 -0
  122. package/dist/vendored/skills/dsl-card-generator/scripts/validate_all_schemas.py +71 -0
  123. package/dist/vendored/skills/dsl-card-generator/vendor/preview/assets/default-image-wzZYQ86K.png +0 -0
  124. package/dist/vendored/skills/dsl-card-generator/vendor/preview/assets/standalone-C9k-07y7.css +1 -0
  125. package/dist/vendored/skills/dsl-card-generator/vendor/preview/assets/standalone-oFRM4f4V.js +55 -0
  126. package/dist/vendored/skills/dsl-card-generator/vendor/preview/assets/svg-symbols-B2exBrAr.svg +118 -0
  127. package/dist/vendored/skills/dsl-card-generator/vendor/preview/meta/component-meta.json +2714 -0
  128. package/dist/vendored/skills/dsl-card-generator/vendor/preview/meta/directive-meta.json +40 -0
  129. package/dist/vendored/skills/dsl-card-generator/vendor/preview/meta/style-meta.json +1086 -0
  130. package/dist/vendored/{standalone.html → skills/dsl-card-generator/vendor/preview/standalone.html} +2 -1
  131. package/dist/vendored/skills/finmall-card-sync/SKILL.md +262 -0
  132. package/dist/vendored/skills/finmall-session-navigator/SKILL.md +169 -0
  133. package/dist/vendored/skills/finmall-session-navigator/scripts/finmall_login.mjs +882 -0
  134. package/dist/vendored/skills/finmall-session-navigator/scripts/package-lock.json +59 -0
  135. package/dist/vendored/skills/finmall-session-navigator/scripts/package.json +10 -0
  136. package/dist/vendored/skills/finmall-session-navigator/scripts/session-manager.mjs +96 -0
  137. package/dist/vendored/skills/shared/README.md +55 -0
  138. package/dist/vendored/skills/shared/auth-model.md +79 -0
  139. package/dist/vendored/skills/sketch-spec-to-card/SKILL.md +329 -0
  140. package/dist/vendored/skills/sketch-spec-to-card/references/boundary.json +14 -0
  141. package/dist/vendored/skills/sketch-spec-to-card/references/card_refs.json +13 -0
  142. package/dist/vendored/skills/sketch-spec-to-card/references/spec-schema.md +103 -0
  143. package/dist/vendored/skills/sketch-spec-to-card/scripts/build_prompt.py +797 -0
  144. package/dist/vendored/skills/sketch-spec-to-card/scripts/card_refs.py +94 -0
  145. package/dist/vendored/skills/sketch-spec-to-card/scripts/cluster_states.py +706 -0
  146. package/dist/vendored/skills/sketch-spec-to-card/scripts/parse_meaxure.py +1781 -0
  147. package/dist/vendored/skills/sketch-spec-to-card/scripts/png_utils.py +318 -0
  148. package/dist/vendored/skills/sketch-spec-to-card/scripts/vision.py +1118 -0
  149. package/dist/vendored/templates/helloCard/data.json +10 -0
  150. package/dist/vendored/templates/helloCard/schema.json +238 -0
  151. package/dist/vendored/tools/link-skills.mjs +485 -0
  152. package/package.json +2 -1
  153. package/dist/preview-editor/assets/index-jl4YJzKv.js +0 -180
  154. package/dist/vendored/App.vue +0 -2858
  155. package/dist/vendored/ansi.js +0 -185
  156. package/dist/vendored/assets/apple-touch-icon.png +0 -0
  157. package/dist/vendored/assets/default-image.png +0 -0
  158. package/dist/vendored/assets/favicon-16.png +0 -0
  159. package/dist/vendored/assets/favicon-32.png +0 -0
  160. package/dist/vendored/assets/site-icon.png +0 -0
  161. package/dist/vendored/assets/svgIcons/icons.svg +0 -87
  162. package/dist/vendored/assets/svgIcons/index.js +0 -39
  163. package/dist/vendored/components/AiSettings.vue +0 -392
  164. package/dist/vendored/components/AlertCardInstance.vue +0 -58
  165. package/dist/vendored/components/AlertCardLayer.vue +0 -59
  166. package/dist/vendored/components/ApiCallPanel.vue +0 -127
  167. package/dist/vendored/components/ApiPayloadTree.vue +0 -201
  168. package/dist/vendored/components/AppToast.vue +0 -37
  169. package/dist/vendored/components/CardGallery.vue +0 -291
  170. package/dist/vendored/components/CardPreview.vue +0 -72
  171. package/dist/vendored/components/CardSelect.vue +0 -206
  172. package/dist/vendored/components/ChatComposer.vue +0 -413
  173. package/dist/vendored/components/ChatMessage.vue +0 -383
  174. package/dist/vendored/components/ChatPanel.vue +0 -937
  175. package/dist/vendored/components/ComponentPalette.vue +0 -301
  176. package/dist/vendored/components/ConfirmDialog.vue +0 -54
  177. package/dist/vendored/components/DatePickerOverlay.vue +0 -345
  178. package/dist/vendored/components/DebugPanel.vue +0 -1734
  179. package/dist/vendored/components/DeviceOverlay.vue +0 -70
  180. package/dist/vendored/components/Dropdown.vue +0 -193
  181. package/dist/vendored/components/EventEditor.vue +0 -273
  182. package/dist/vendored/components/HoverGuides.vue +0 -122
  183. package/dist/vendored/components/JsonTree.vue +0 -547
  184. package/dist/vendored/components/LogValue.vue +0 -52
  185. package/dist/vendored/components/MarkdownBody.vue +0 -64
  186. package/dist/vendored/components/NewCardDialog.vue +0 -132
  187. package/dist/vendored/components/NodeInspector.vue +0 -295
  188. package/dist/vendored/components/NumberStepper.vue +0 -125
  189. package/dist/vendored/components/PickerOverlay.vue +0 -131
  190. package/dist/vendored/components/PropControl.vue +0 -245
  191. package/dist/vendored/components/PropTree.vue +0 -128
  192. package/dist/vendored/components/RemotePreview.vue +0 -173
  193. package/dist/vendored/components/SelectionOverlay.vue +0 -105
  194. package/dist/vendored/components/StructureTree.vue +0 -1002
  195. package/dist/vendored/components/Switch.vue +0 -38
  196. package/dist/vendored/components/setters/SetBgPosition.vue +0 -64
  197. package/dist/vendored/components/setters/SetBgSize.vue +0 -90
  198. package/dist/vendored/components/setters/SetCode.vue +0 -198
  199. package/dist/vendored/components/setters/SetColor.vue +0 -244
  200. package/dist/vendored/components/setters/SetCustomSelect.vue +0 -58
  201. package/dist/vendored/components/setters/SetExpr.vue +0 -107
  202. package/dist/vendored/components/setters/SetGradient.vue +0 -125
  203. package/dist/vendored/components/setters/SetGrid9.vue +0 -68
  204. package/dist/vendored/components/setters/SetIconSelect.vue +0 -67
  205. package/dist/vendored/components/setters/SetIfForm.vue +0 -61
  206. package/dist/vendored/components/setters/SetImage.vue +0 -43
  207. package/dist/vendored/components/setters/SetList.vue +0 -139
  208. package/dist/vendored/components/setters/SetNumber.vue +0 -118
  209. package/dist/vendored/components/setters/SetNumberStyle.vue +0 -113
  210. package/dist/vendored/components/setters/SetPadding.vue +0 -77
  211. package/dist/vendored/components/setters/SetSegment.vue +0 -160
  212. package/dist/vendored/components/setters/SetSelect.vue +0 -51
  213. package/dist/vendored/components/setters/SetSwitch.vue +0 -49
  214. package/dist/vendored/components/setters/SetText.vue +0 -52
  215. package/dist/vendored/components/setters/props.js +0 -23
  216. package/dist/vendored/fidelity/baseline-messageOverview.json +0 -87
  217. package/dist/vendored/fidelity/calibration.json +0 -29
  218. package/dist/vendored/fidelity/fidelity-messageOverview-after.html +0 -114
  219. package/dist/vendored/fidelity/fidelity-messageOverview-after.json +0 -272
  220. package/dist/vendored/fidelity/fidelity-messageOverview.html +0 -185
  221. package/dist/vendored/fidelity/fidelity-messageOverview.json +0 -336
  222. package/dist/vendored/fidelity/fidelity-messageOverview_v2.html +0 -262
  223. package/dist/vendored/fidelity/fidelity-messageOverview_v2.json +0 -502
  224. package/dist/vendored/fidelity/fidelity-messageOverview_v3.html +0 -201
  225. package/dist/vendored/fidelity/fidelity-messageOverview_v3.json +0 -417
  226. package/dist/vendored/fidelity/fidelity-messageOverview_visual.html +0 -125
  227. package/dist/vendored/fidelity/fidelity-messageOverview_visual.json +0 -338
  228. package/dist/vendored/index.html +0 -16
  229. package/dist/vendored/main.js +0 -37
  230. package/dist/vendored/runtime/ai-api.js +0 -165
  231. package/dist/vendored/runtime/alert-card.js +0 -125
  232. package/dist/vendored/runtime/app-toast.js +0 -33
  233. package/dist/vendored/runtime/clipboard-node.js +0 -78
  234. package/dist/vendored/runtime/clipboard.js +0 -29
  235. package/dist/vendored/runtime/event-model.js +0 -300
  236. package/dist/vendored/runtime/hoverGuide.js +0 -31
  237. package/dist/vendored/runtime/jsapi-mock.js +0 -256
  238. package/dist/vendored/runtime/markdown.js +0 -140
  239. package/dist/vendored/runtime/node-model.js +0 -217
  240. package/dist/vendored/skills/.manifest.json +0 -1101
  241. package/dist/vendored/standalone-main.js +0 -36
  242. package/dist/vendored/styles/ai-chat.css +0 -1850
  243. package/dist/vendored/styles/app.css +0 -3648
  244. package/dist/vendored/styles/card-gallery.css +0 -285
  245. package/dist/vendored/styles/card-select.css +0 -288
  246. package/dist/vendored/styles/components.css +0 -383
  247. package/dist/vendored/styles/dropdown.css +0 -324
  248. package/dist/vendored/styles/json-tree.css +0 -308
  249. package/dist/vendored/styles/node-inspector.css +0 -800
  250. package/dist/vendored/styles/setters.css +0 -1050
  251. package/dist/vendored/styles/standalone.css +0 -27
  252. package/dist/vendored/styles/structure-tree.css +0 -460
  253. /package/dist/vendored/{api.js → preview/api.js} +0 -0
  254. /package/dist/vendored/{assets → preview/assets}/svg-symbols.svg +0 -0
  255. /package/dist/vendored/{components → preview/components}/AbsoluteLayout.vue +0 -0
  256. /package/dist/vendored/{components → preview/components}/Button.vue +0 -0
  257. /package/dist/vendored/{components → preview/components}/Canvas.vue +0 -0
  258. /package/dist/vendored/{components → preview/components}/CheckGroup.vue +0 -0
  259. /package/dist/vendored/{components → preview/components}/Column.vue +0 -0
  260. /package/dist/vendored/{components → preview/components}/CustomPlaceholder.vue +0 -0
  261. /package/dist/vendored/{components → preview/components}/Divider.vue +0 -0
  262. /package/dist/vendored/{components → preview/components}/GridLayout.vue +0 -0
  263. /package/dist/vendored/{components → preview/components}/Icon.vue +0 -0
  264. /package/dist/vendored/{components → preview/components}/Image.vue +0 -0
  265. /package/dist/vendored/{components → preview/components}/Input.vue +0 -0
  266. /package/dist/vendored/{components → preview/components}/ListLayout.vue +0 -0
  267. /package/dist/vendored/{components → preview/components}/Map.vue +0 -0
  268. /package/dist/vendored/{components → preview/components}/MultiColumn.vue +0 -0
  269. /package/dist/vendored/{components → preview/components}/MultiText.vue +0 -0
  270. /package/dist/vendored/{components → preview/components}/ProgressSlider.vue +0 -0
  271. /package/dist/vendored/{components → preview/components}/RadioButton.vue +0 -0
  272. /package/dist/vendored/{components → preview/components}/RenderTree.vue +0 -0
  273. /package/dist/vendored/{components → preview/components}/Root.vue +0 -0
  274. /package/dist/vendored/{components → preview/components}/Row.vue +0 -0
  275. /package/dist/vendored/{components → preview/components}/SvgCode.vue +0 -0
  276. /package/dist/vendored/{components → preview/components}/SvgSymbols.vue +0 -0
  277. /package/dist/vendored/{components → preview/components}/Text.vue +0 -0
  278. /package/dist/vendored/{components → preview/components}/Video.vue +0 -0
  279. /package/dist/vendored/{components → preview/components}/ViewContainer.vue +0 -0
  280. /package/dist/vendored/{components → preview/components}/base.js +0 -0
  281. /package/dist/vendored/{components → preview/components}/icons.js +0 -0
  282. /package/dist/vendored/{components → preview/components}/index.js +0 -0
  283. /package/dist/vendored/{meta → preview/meta}/component-meta.json +0 -0
  284. /package/dist/vendored/{meta → preview/meta}/directive-meta.json +0 -0
  285. /package/dist/vendored/{meta → preview/meta}/style-meta.json +0 -0
  286. /package/dist/vendored/{runtime → preview/runtime}/StyleParser.js +0 -0
  287. /package/dist/vendored/{runtime → preview/runtime}/adapter.js +0 -0
  288. /package/dist/vendored/{runtime → preview/runtime}/assets.js +0 -0
  289. /package/dist/vendored/{runtime → preview/runtime}/canvas-context.js +0 -0
  290. /package/dist/vendored/{runtime → preview/runtime}/card-runtime.js +0 -0
  291. /package/dist/vendored/{runtime → preview/runtime}/evaluate.js +0 -0
  292. /package/dist/vendored/{runtime → preview/runtime}/property.js +0 -0
  293. /package/dist/vendored/{runtime → preview/runtime}/selectionOverlay.js +0 -0
  294. /package/dist/vendored/{runtime → preview/runtime}/toPreview.js +0 -0
  295. /package/dist/{server-core → vendored/server/core}/assetProxy.js +0 -0
  296. /package/dist/{server-core → vendored/server/core}/bbox.js +0 -0
  297. /package/dist/{server-core → vendored/server/core}/buildDataFromSchema.js +0 -0
  298. /package/dist/{server-core → vendored/server/core}/cardFiles.js +0 -0
  299. /package/dist/{server-core → vendored/server/core}/cardHistory.js +0 -0
  300. /package/dist/{server-core → vendored/server/core}/cardImage.js +0 -0
  301. /package/dist/{server-core → vendored/server/core}/cardImportMeta.js +0 -0
  302. /package/dist/{server-core → vendored/server/core}/cardListCache.js +0 -0
  303. /package/dist/{server-core → vendored/server/core}/cardOpenFolder.js +0 -0
  304. /package/dist/{server-core → vendored/server/core}/cardSidecar.js +0 -0
  305. /package/dist/{server-core → vendored/server/core}/codeBridge.js +0 -0
  306. /package/dist/{server-core → vendored/server/core}/logStream.js +0 -0
  307. /package/dist/{server-core → vendored/server/core}/logger.js +0 -0
  308. /package/dist/{server-core → vendored/server/core}/nodeModels.js +0 -0
  309. /package/dist/{server-core → vendored/server/core}/renderStatus.js +0 -0
  310. /package/dist/{server-core → vendored/server/core}/screenshot.js +0 -0
  311. /package/dist/{server-core → vendored/server/core}/sliceStatic.js +0 -0
  312. /package/dist/vendored/{meta → skills/dsl-card-generator/vendor/preview/meta}/action-meta.json +0 -0
  313. /package/dist/{server-core/_vendor → vendored/tools}/finmall-env.mjs +0 -0
@@ -0,0 +1,1118 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """vision.py — 纯视觉驱动的卡片区域检测(无业务词、无文本依赖)。
4
+
5
+ 设计原则(用户 2026-08-14 确认):
6
+ - 判定依据全部来自视觉几何:大块、独立、卡片状(有填充/圆角/阴影/描边)的元素即卡片。
7
+ - 文本/名称只是附属信息,不参与「保留/剔除」判定(设计稿文本多为示意)。
8
+ - 一个画板可出现 1..N 张卡片:空间上分离的视觉块各自成卡。
9
+ - 卡片以外的一切(全屏背景、系统区、图标、装饰)在聚焦时全部剔除。
10
+ """
11
+ import math
12
+ import re
13
+
14
+
15
+ # ---------- 几何常量(375x812 移动端设计稿;按画板尺寸比例自适应) ----------
16
+ TOP_SYSTEM_RATIO = 0.12 # 顶部状态栏+导航占画板高度比例(约 88-90px @812)
17
+ BOTTOM_SYSTEM_RATIO = 0.13 # 底部导航+AI输入框+Home Indicator 高度比例(约 100px @812)
18
+ FULLSCREEN_BG_RATIO = 0.85 # 面积 >= 画板 85% 视为全屏背景(蒙版/背景)
19
+ CARD_MIN_AREA_RATIO = 0.045 # 卡片最小面积 = 画板面积的 4.5%(375*812≈305k → ~13.7k px²)
20
+ CARD_MAX_AREA_RATIO = 0.75 # 卡片最大面积 < 画板 75%(排除整屏)
21
+ EDGE_DECOR_RATIO = 0.45 # 与画板顶/底边粘连且高度 < 45%H 的块视为装饰(椭圆/弥散背景)
22
+ MERGE_GAP_RATIO = 0.02 # 两块之间间隙小于画板宽 2% 且高度相似时合并
23
+ OVERLAY_MIN_WIDTH_RATIO = 0.55 # 居中对话框通常明显窄于画板
24
+ OVERLAY_MAX_WIDTH_RATIO = 0.90
25
+ OVERLAY_MIN_HEIGHT_RATIO = 0.16
26
+ OVERLAY_MAX_HEIGHT_RATIO = 0.70
27
+ OVERLAY_CENTER_TOLERANCE = 0.10 # 对话框水平中心容差
28
+ # 相对外层卡壳的四周留白,用于区分嵌套内容和模态浮层。
29
+ # 2026-09-06 由 0.015 降到 0.008:银行 alert 对话框常近全卡宽(综合推荐
30
+ # 「总资产提示」335/343 宽,横向 inset 仅 4px <0.015W=5.6px 漏检);模态语义
31
+ # 已由前置 scrim 闸保证,inset 只需排除「内壳与外壳同宽对齐」的普通内容列。
32
+ # 全 24 稿回归:新增 8 处检出均为真弹框(总资产提示/网点报错提示×4/交易
33
+ # 密码框×2/转账计划时间修正提示),0 丢失。
34
+ OVERLAY_MIN_INSET_X_RATIO = 0.008
35
+ OVERLAY_MIN_INSET_Y_RATIO = 0.035
36
+
37
+
38
+ class VisualElement:
39
+ """一个视觉元素(归一化后):几何 + 视觉特征 + 附属信息。"""
40
+ __slots__ = ("name", "type", "rect", "fills", "radius", "shadows",
41
+ "opacity", "border", "rotation", "texts", "children_count", "raw",
42
+ "is_text", "is_slice",
43
+ # 层级树信息(_flatten_with_tree 填充)
44
+ "id", "parentId", "childIds", "depth")
45
+
46
+ def __init__(self, raw):
47
+ r = raw.get("rect") or {}
48
+ self.name = raw.get("name", "")
49
+ self.type = raw.get("type", "")
50
+ self.rect = (r.get("x", 0), r.get("y", 0), r.get("width", 0), r.get("height", 0))
51
+ self.fills = raw.get("fills") or []
52
+ self.radius = raw.get("radius") or []
53
+ self.shadows = raw.get("shadows") or []
54
+ self.opacity = raw.get("opacity", 1)
55
+ self.border = raw.get("borders") or []
56
+ self.rotation = raw.get("rotation", 0)
57
+ self.is_text = raw.get("type") == "text"
58
+ self.is_slice = raw.get("type") == "slice"
59
+ self.raw = raw
60
+ self.texts = []
61
+ self.children_count = 0
62
+ # 层级树信息(默认值,_flatten_with_tree 会覆写)
63
+ self.id = -1
64
+ self.parentId = None
65
+ self.childIds = []
66
+ self.depth = 0
67
+
68
+ @property
69
+ def area(self):
70
+ x, y, w, h = self.rect
71
+ return max(w, 0) * max(h, 0)
72
+
73
+ @property
74
+ def has_visual(self):
75
+ if self.fills:
76
+ return True
77
+ if any(x and x > 0 for x in self.radius):
78
+ return True
79
+ if self.shadows:
80
+ return True
81
+ if self.border:
82
+ return True
83
+ # Symbol:Sketch/Figma 可复用视觉组件。MeaXure 导出常为不展开子层的叶子节点
84
+ # (fills/shadows/borders 均为空数组),但符号本身承载渲染内容。
85
+ # 若误判 no_visual,大尺寸 symbol 卡片容器会从候选中丢失,
86
+ # 导致 detect_cards 区域收缩、聚焦裁切缺失卡片头部。issue-focus-001。
87
+ if self.type == "symbol":
88
+ return True
89
+ return False
90
+
91
+ def gradient_info(self):
92
+ out = []
93
+ for f in self.fills:
94
+ if not isinstance(f, dict):
95
+ continue
96
+ ft = f.get("fillType") or f.get("type") or ""
97
+ if "grad" not in str(ft).lower():
98
+ continue
99
+ g = f.get("gradient") or {}
100
+ gtype = g.get("type", "Linear")
101
+ stops = g.get("colorStops") or []
102
+ css_stops = []
103
+ for s in stops:
104
+ c = s.get("color") or {}
105
+ css = c.get("css-rgba") or c.get("css-hsla") or ""
106
+ pos = s.get("position")
107
+ if css and pos is not None:
108
+ css_stops.append(f"{css} {round(pos*100)}%")
109
+ if css_stops:
110
+ if str(gtype).lower().startswith("radial"):
111
+ out.append("radial-gradient(" + ", ".join(css_stops) + ")")
112
+ else:
113
+ frm, to = g.get("from", {}), g.get("to", {})
114
+ fx, fy = frm.get("x", 0.5), frm.get("y", 0)
115
+ tx, ty = to.get("x", 0.5), to.get("y", 1)
116
+ angle = "to bottom" if abs(ty - fy) >= abs(tx - fx) else "to right"
117
+ out.append(f"linear-gradient({angle}, " + ", ".join(css_stops) + ")")
118
+ return out
119
+
120
+
121
+ def _hex_of(color_obj):
122
+ if not isinstance(color_obj, dict):
123
+ return None
124
+ for key in ("color-hex", "argb-hex", "rgba-hex"):
125
+ v = color_obj.get(key)
126
+ if isinstance(v, str) and v.startswith("#"):
127
+ return v.split()[0][:7] if " " not in v else v[:7]
128
+ return None
129
+
130
+
131
+ def _fill_hex(f):
132
+ if isinstance(f, dict) and f.get("color"):
133
+ return _hex_of(f["color"])
134
+ return None
135
+
136
+
137
+ def _flatten_raw(raw_layers):
138
+ out = []
139
+ def walk(ls, parent_count=0):
140
+ for L in ls:
141
+ ve = VisualElement(L)
142
+ ve.children_count = parent_count
143
+ if L.get("layers"):
144
+ n = sum(1 for c in L["layers"])
145
+ ve.children_count += n
146
+ walk(L["layers"], n)
147
+ out.append(ve)
148
+ walk(raw_layers)
149
+ return out
150
+
151
+
152
+ def _flatten_with_tree(raw_layers):
153
+ """保留层级关系的扁平化:每个 VisualElement 带 id / parentId / childIds / depth。
154
+
155
+ 与 _flatten_raw 的区别:不丢掉树结构信息。返回的列表顺序仍为先序遍历
156
+ (与 _flatten_raw 一致,保证向后兼容),但每个节点可通过 parentId / childIds
157
+ 重建原始嵌套关系。id 为列表下标(从 0 开始),稳定且唯一。
158
+ """
159
+ nodes = []
160
+ def walk(ls, parent_id, depth):
161
+ for L in ls:
162
+ node_id = len(nodes)
163
+ ve = VisualElement(L)
164
+ ve.id = node_id
165
+ ve.parentId = parent_id
166
+ ve.childIds = []
167
+ ve.depth = depth
168
+ ve.children_count = len(L.get("layers") or [])
169
+ nodes.append(ve)
170
+ if L.get("layers"):
171
+ child_start = len(nodes)
172
+ walk(L["layers"], node_id, depth + 1)
173
+ ve.childIds = list(range(child_start, len(nodes)))
174
+ walk(raw_layers, None, 0)
175
+ return nodes
176
+
177
+
178
+ def _is_fullscreen(ve, W, H):
179
+ return ve.area >= FULLSCREEN_BG_RATIO * W * H
180
+
181
+
182
+ def _in_system_zone(ve, W, H):
183
+ """系统 chrome(状态栏/底部导航/AI 输入框/Home Indicator)判定。
184
+ 只看 y 区域会误杀「卡片底部内容」(如还款成功卡的字段区 y 落在底部 13% 但宽=卡宽非全宽导航)。
185
+ 系统 chrome 特征 = 位于顶/底系统区 **且近全宽(≥0.95W)**:窄于画布宽的元素是卡片内容,不判系统区。"""
186
+ x, y, w, h = ve.rect
187
+ top = TOP_SYSTEM_RATIO * H
188
+ bottom = H - BOTTOM_SYSTEM_RATIO * H
189
+ if w < 0.95 * W:
190
+ return False # 非全宽(卡片内容/字段区),即使落在系统区 y 也放行
191
+ return (y + h <= top) or (y >= bottom)
192
+
193
+
194
+ def _is_edge_decor(ve, W, H):
195
+ """与画板顶/底边粘连的装饰块(椭圆/弥散背景/渐变条),非卡片。"""
196
+ x, y, w, h = ve.rect
197
+ if h <= 0:
198
+ return False
199
+ if y <= 0 and h < EDGE_DECOR_RATIO * H:
200
+ return True
201
+ if y + h >= H and h < EDGE_DECOR_RATIO * H:
202
+ return True
203
+ return False
204
+
205
+
206
+ def _touch_screen_edges(ve, W):
207
+ x, y, w, h = ve.rect
208
+ return (x <= 0 and w >= 0.5 * W) or (x + w >= W and w >= 0.5 * W)
209
+
210
+
211
+ def _overlap(a, b):
212
+ ax, ay, aw, ah = a
213
+ bx, by, bw, bh = b
214
+ ix = max(0, min(ax + aw, bx + bw) - max(ax, bx))
215
+ iy = max(0, min(ay + ah, by + bh) - max(ay, by))
216
+ return ix * iy
217
+
218
+
219
+ def _union(a, b):
220
+ ax, ay, aw, ah = a
221
+ bx, by, bw, bh = b
222
+ x0 = min(ax, bx); y0 = min(ay, by)
223
+ x1 = max(ax + aw, bx + bw); y1 = max(ay + ah, by + bh)
224
+ return (x0, y0, x1 - x0, y1 - y0)
225
+
226
+
227
+ def _intersect_ratio(a, b):
228
+ """a 与 b 的重叠面积占 a 面积比例。"""
229
+ if a[2] * a[3] <= 0:
230
+ return 0.0
231
+ return _overlap(a, b) / (a[2] * a[3])
232
+
233
+
234
+
235
+ def _bottom_sheet_rect(raw_layers, W, H):
236
+ """贴底通栏 + 顶圆角底直角的底部抽屉面板矩形(无 fills 的 symbol 也由子层生效)。
237
+
238
+ 返回 (x, y, w, h, radii),radii=[top_r, top_r, 0, 0];未找到返回 None。
239
+ """
240
+ best = None
241
+ def walk(ls):
242
+ nonlocal best
243
+ for L in ls:
244
+ ty = L.get("type", "")
245
+ r = L.get("rect") or {}
246
+ w, h = r.get("width", 0), r.get("height", 0)
247
+ x, y = r.get("x", 0), r.get("y", 0)
248
+ if ty in ("shape", "symbol", "group") and w >= 0.95 * W and h >= 0.2 * H \
249
+ and (y + h) >= 0.98 * H:
250
+ rad = L.get("radius") or []
251
+ if rad:
252
+ if len(rad) == 1:
253
+ top_r, bottom_r = rad[0], rad[0]
254
+ elif len(rad) >= 4:
255
+ top_r, bottom_r = rad[0], rad[2]
256
+ else:
257
+ top_r, bottom_r = rad[0], (rad[1] if len(rad) >= 2 else 0)
258
+ if isinstance(top_r, (int, float)) and top_r > 8 \
259
+ and isinstance(bottom_r, (int, float)) and bottom_r == 0:
260
+ if best is None or h > best[3]:
261
+ best = (x, y, w, h, [top_r, top_r, 0, 0])
262
+ if L.get("layers"):
263
+ walk(L["layers"])
264
+ walk(raw_layers)
265
+ return best
266
+
267
+
268
+ def _merge_rects(rects, W, H):
269
+ rects = [r for r in rects if r[2] > 0 and r[3] > 0]
270
+ merged = True
271
+ while merged:
272
+ merged = False
273
+ for i in range(len(rects)):
274
+ for j in range(i + 1, len(rects)):
275
+ a, b = rects[i], rects[j]
276
+ ov = _overlap(a, b)
277
+ if ov > 0:
278
+ rects[i] = _union(a, b)
279
+ rects.pop(j)
280
+ merged = True
281
+ break
282
+ ax, ay, aw, ah = a
283
+ bx, by, bw, bh = b
284
+ gap_x = max(bx - (ax + aw), ax - (bx + bw))
285
+ gap_y = max(by - (ay + ah), ay - (by + bh))
286
+ # 两级合并策略:
287
+ # 1) 像素级无缝相邻(gap≤0.5px)→ 无条件合并(同一视觉体的取整误差)
288
+ # 2) 小间隙(≤MERGE_GAP_RATIO)且高度相似(差≤30%)→ 合并
289
+ # 典型:卡片背景层与内容层之间有 2-8px 间隙但视觉上连续;
290
+ # 列表项之间有均匀间距但属于同一卡片。
291
+ # 高度相似约束防止把矮气泡并入高业务卡。
292
+ if gap_x <= 0.5 and gap_y <= 0.5:
293
+ rects[i] = _union(a, b)
294
+ rects.pop(j)
295
+ merged = True
296
+ break
297
+ if (gap_x <= MERGE_GAP_RATIO * W and gap_y <= MERGE_GAP_RATIO * H and
298
+ abs(ah - bh) <= 0.3 * max(ah, bh, 1)):
299
+ rects[i] = _union(a, b)
300
+ rects.pop(j)
301
+ merged = True
302
+ break
303
+ if merged:
304
+ break
305
+ return rects
306
+
307
+
308
+ def _rect_inside(inner, outer, tol=0.5):
309
+ """inner 是否落在 outer 内(含浮点/取整容差)。"""
310
+ ix, iy, iw, ih = inner
311
+ ox, oy, ow, oh = outer
312
+ return (ix >= ox - tol and iy >= oy - tol and
313
+ ix + iw <= ox + ow + tol and iy + ih <= oy + oh + tol)
314
+
315
+
316
+ def _norm_rect_key(rect):
317
+ """rect 取整到 0.1px,用于合并同 rect 的多层外壳(如无圆角背景层 + 有圆角卡面)。"""
318
+ return tuple(round(float(v), 1) for v in rect)
319
+
320
+
321
+ def _radii_score(radii):
322
+ """圆角信息强度:有圆角 > 无圆角;圆角角数越多、半径越大越像外壳。"""
323
+ vals = [max(0.0, float(v)) for v in (radii or [])]
324
+ pos = sum(1 for v in vals if v > 0)
325
+ max_r = max(vals) if vals else 0.0
326
+ return (1 if pos else 0, pos, max_r)
327
+
328
+
329
+ def _radius_values(radii):
330
+ values = []
331
+ for value in (radii or []):
332
+ if isinstance(value, (int, float)) and value > 0:
333
+ values.append(float(value))
334
+ return values
335
+
336
+
337
+ def _candidate_visuals(elements, W, H):
338
+ candidates = []
339
+ for ve in elements:
340
+ if ve.is_text or ve.is_slice or ve.area <= 0:
341
+ continue
342
+ if _is_fullscreen(ve, W, H) or _in_system_zone(ve, W, H) or _is_edge_decor(ve, W, H):
343
+ continue
344
+ if not ve.has_visual:
345
+ continue
346
+ x, y, w, h = ve.rect
347
+ if ve.area < CARD_MIN_AREA_RATIO * W * H:
348
+ continue
349
+ if h > 3 * w and w < 0.5 * W:
350
+ continue
351
+ if w > 2.5 * h and h < 0.05 * H:
352
+ continue
353
+ candidates.append(ve)
354
+ return candidates
355
+
356
+
357
+ def _overlay_shell_metrics(ve, W, H):
358
+ x, y, w, h = ve.rect
359
+ width_ratio = w / W
360
+ height_ratio = h / H
361
+ if not (OVERLAY_MIN_WIDTH_RATIO <= width_ratio <= OVERLAY_MAX_WIDTH_RATIO):
362
+ return None
363
+ if not (OVERLAY_MIN_HEIGHT_RATIO <= height_ratio <= OVERLAY_MAX_HEIGHT_RATIO):
364
+ return None
365
+ center_dx = abs((x + w / 2) - W / 2)
366
+ if center_dx > OVERLAY_CENTER_TOLERANCE * W:
367
+ return None
368
+ if center_dy_ratio := abs((y + h / 2) / H - 0.5) > 0.38:
369
+ return None
370
+ radii = _radius_values(ve.radius)
371
+ if not (radii or ve.shadows or ve.border):
372
+ return None
373
+ return {"centerDxRatio": center_dx / W, "widthRatio": width_ratio, "heightRatio": height_ratio}
374
+
375
+
376
+ def _has_modal_scrim(elements, W, H):
377
+ """模态遮罩(scrim):近全屏(w≥90%W 且 h≥90%H)且半透明(opacity<0.95)的层。
378
+
379
+ 与 parse_meaxure.detect_modal_layers 的 scrim 口径一致(契约:无 scrim 一律判 card)。
380
+ """
381
+ for ve in elements:
382
+ if ve.is_text:
383
+ continue
384
+ _, _, w, h = ve.rect
385
+ if w < 0.9 * W or h < 0.9 * H:
386
+ continue
387
+ try:
388
+ op = float(ve.opacity)
389
+ except (TypeError, ValueError):
390
+ op = 1.0
391
+ if op < 0.95:
392
+ return True
393
+ return False
394
+
395
+
396
+ def detect_overlay_dialog(elements, width, height):
397
+ """通过嵌套几何识别居中模态对话框。
398
+
399
+ 真实卡片可以嵌套子块;模态对话框的通用特征是一个有圆角/阴影的居中
400
+ 中型壳体,完整内缩于更大的底层卡壳,并且同时包含文本与图形控件。
401
+ 该判定不读取业务文案,也不依赖固定颜色。
402
+ """
403
+ W, H = float(width), float(height)
404
+ # 契约(SKILL §4.5):无 scrim 一律判 card。居中嵌套壳体只是几何必要信号——
405
+ # 业务卡内嵌的空态/提示面板(如 01基金_基金推荐 系 8 个画板)同样满足嵌套几何,
406
+ # 必须有模态遮罩佐证才判 overlay_dialog,否则整板内容被误跳过。
407
+ if not _has_modal_scrim(elements, W, H):
408
+ return None
409
+ visuals = _candidate_visuals(elements, W, H)
410
+ for inner in visuals:
411
+ metrics = _overlay_shell_metrics(inner, W, H)
412
+ if not metrics:
413
+ continue
414
+ inner_text_count = 0
415
+ inner_rich_count = 0
416
+ for ve in elements:
417
+ if _intersect_ratio(ve.rect, inner.rect) < 0.7:
418
+ continue
419
+ if ve.is_text and ve.area > 0:
420
+ inner_text_count += 1
421
+ elif not ve.is_text and ve.area < 0.5 * inner.area and not _is_divider(ve):
422
+ inner_rich_count += 1
423
+ if inner_text_count < 1 or inner_rich_count < 1:
424
+ continue
425
+ for outer in visuals:
426
+ if outer is inner or outer.area <= 1.5 * inner.area:
427
+ continue
428
+ ix, iy, iw, ih = inner.rect
429
+ ox, oy, ow, oh = outer.rect
430
+ inset_x = min(ix - ox, (ox + ow) - (ix + iw))
431
+ inset_y = min(iy - oy, (oy + oh) - (iy + ih))
432
+ if (not _rect_inside(inner.rect, outer.rect, tol=0.015 * H) or
433
+ inset_x < OVERLAY_MIN_INSET_X_RATIO * W or
434
+ inset_y < -0.012 * H or oh < 1.5 * ih):
435
+ continue
436
+ metrics["outerRect"] = outer.rect
437
+ metrics["inset"] = {"x": inset_x, "y": inset_y}
438
+ return metrics
439
+ return None
440
+
441
+
442
+ def _count_controls_near(elements, rect, width, height):
443
+ """统计与输入坞同一横带的独立小控件数量。"""
444
+ _, dock_y, _, dock_h = rect
445
+ count = 0
446
+ for ve in elements:
447
+ if ve.is_text or ve.area <= 0:
448
+ continue
449
+ x, y, w, h = ve.rect
450
+ if w > 0.22 * width or h > 0.14 * height:
451
+ continue
452
+ overlap = min(y + h, dock_y + dock_h) - max(y, dock_y)
453
+ if overlap < 0.35 * min(h, dock_h):
454
+ continue
455
+ if _intersect_ratio(ve.rect, rect) > 0.55:
456
+ continue
457
+ count += 1
458
+ return count
459
+
460
+
461
+ def _detect_keyboard(elements, width, height):
462
+ """识别贴底大面积键盘:通栏壳体 + 大量小子控件。
463
+
464
+ 键盘/底部选择器必然停靠屏幕底沿(真实样本底沿均 ≥0.94H);
465
+ 非贴底区域只有控件密度足够高(≥12,数字键盘 12 键起步)才可采信,
466
+ 否则账户列表/商品列表这类「通栏壳 + 若干图标」会被误判成键盘。
467
+
468
+ 控件计数按 rect 去重:Sketch 常把同一按钮导出为同 rect 的叠层
469
+ (阴影层 + 填充层),不去重会让计数翻倍——基金产品列表的 6~7 个
470
+ 真实按钮被计成 12 个,恰好越过「12 键起步」门槛误判为键盘
471
+ (基金1128 #5/#6/#14/#15/#16)。真实数字键盘按键 rect 各不相同,
472
+ 去重后仍 ≥16(收款人名册#20、我的账户#13、购汇#22 回归验证)。
473
+
474
+ 按键网格闸(2026-09-06):控件数去重后仍不够——资产配置雷达卡的
475
+ 同心环 + 胶囊 + 标签共 25 个异 rect 控件,计数与真实键盘相当
476
+ (综合推荐1222 #3-#47 全族误检)。键盘的本质特征是「≥2 行同尺寸
477
+ 按键行」(数字键盘 3-4 列 × 多行,行内 ≥75% 成员宽高落在行中位数
478
+ 0.8~1.25 倍内);雷达同心环尺寸递进、表单控件行尺寸参差,同尺寸
479
+ 按键行数为 0-1。真密码键盘全部 ≥2(购汇#22=3、转账计划#21=5、
480
+ 收款人名册#20=2、我的账户#13=2、信用卡还款#9=2)。
481
+ """
482
+ W, H = float(width), float(height)
483
+ for ve in elements:
484
+ if ve.is_text or ve.is_slice or ve.area <= 0:
485
+ continue
486
+ x, y, w, h = ve.rect
487
+ if (w < 0.82 * W or h < 0.18 * H or h > 0.78 * H or
488
+ y < 0.28 * H or (y + h) < 0.78 * H):
489
+ continue
490
+ small_controls = 0
491
+ seen_control_rects = set()
492
+ control_rects = []
493
+ for child in elements:
494
+ if child is ve or child.is_text or child.area <= 0:
495
+ continue
496
+ if _intersect_ratio(child.rect, ve.rect) < 0.55:
497
+ continue
498
+ if child.area < 0.001 * W * H or child.area > 0.02 * W * H:
499
+ continue
500
+ rect_key = _norm_rect_key(child.rect)
501
+ if rect_key in seen_control_rects:
502
+ continue
503
+ seen_control_rects.add(rect_key)
504
+ control_rects.append(child.rect)
505
+ small_controls += 1
506
+ bottom_docked = (y + h) >= 0.90 * H
507
+ if (small_controls >= 8 and (bottom_docked or small_controls >= 12)
508
+ and _uniform_key_rows(control_rects) >= 2):
509
+ return {
510
+ "rect": ve.rect,
511
+ "smallControlCount": small_controls,
512
+ "heightRatio": h / H,
513
+ }
514
+ return None
515
+
516
+
517
+ def _uniform_key_rows(control_rects):
518
+ """统计「同尺寸按键行」数量:≥3 个控件 y 中心对齐成行,且行内
519
+ ≥75% 成员宽高都落在行中位数的 0.8~1.25 倍内。
520
+
521
+ 数字键盘的按键行天然满足(同尺寸键 3-4 列 × 多行);雷达图同心环、
522
+ 表单控件行、商品按钮行的尺寸参差或不足 3 个一行,返回 0-1。
523
+ """
524
+ items = sorted(control_rects, key=lambda r: r[1] + r[3] / 2)
525
+ rows = []
526
+ for rect in items:
527
+ cy = rect[1] + rect[3] / 2
528
+ for row in rows:
529
+ if abs(row["cy"] - cy) <= max(8.0, 0.5 * rect[3]):
530
+ row["rects"].append(rect)
531
+ row["cy"] = (row["cy"] * (len(row["rects"]) - 1) + cy) / len(row["rects"])
532
+ break
533
+ else:
534
+ rows.append({"cy": cy, "rects": [rect]})
535
+ uniform = 0
536
+ for row in rows:
537
+ members = row["rects"]
538
+ if len(members) < 3:
539
+ continue
540
+ widths = sorted(r[2] for r in members)
541
+ heights = sorted(r[3] for r in members)
542
+ median_w = widths[len(widths) // 2]
543
+ median_h = heights[len(heights) // 2]
544
+ same = sum(
545
+ 1 for r in members
546
+ if 0.8 * median_w <= r[2] <= 1.25 * median_w
547
+ and 0.8 * median_h <= r[3] <= 1.25 * median_h
548
+ )
549
+ if same >= 0.75 * len(members):
550
+ uniform += 1
551
+ return uniform
552
+
553
+
554
+ def _keyboard_below_dock(elements, dock, width, height):
555
+ """识别输入坞下方的大面积贴底交互层,兼容整图导出的系统键盘。"""
556
+ dx, dy, dw, dh = dock["rect"]
557
+ dock_bottom = dy + dh
558
+ W, H = float(width), float(height)
559
+ for ve in elements:
560
+ if ve.is_text or ve.is_slice or ve.area <= 0:
561
+ continue
562
+ x, y, w, h = ve.rect
563
+ if w < 0.82 * W or h < 0.20 * H or y < dock_bottom - 2:
564
+ continue
565
+ if y - dock_bottom > 0.12 * H or (y + h) < 0.82 * H:
566
+ continue
567
+ return {
568
+ "rect": ve.rect,
569
+ "heightRatio": h / H,
570
+ "detection": "below-input-dock",
571
+ }
572
+ return None
573
+
574
+
575
+ def detect_conversation_flow(elements, width, height):
576
+ """识别会话流画板:输入坞/键盘与气泡壳互相印证。
577
+
578
+ 会话页中的气泡、推荐列表、评分条都是交互流片段,不是最终落地的独立
579
+ 业务卡片。这里只使用横向输入控件、贴底键盘、圆角气泡壳与纵向堆叠等
580
+ 几何信号,不读取业务文案,也不依赖固定颜色。
581
+ """
582
+ W, H = float(width), float(height)
583
+ input_dock = None
584
+ dock_candidates = []
585
+ for ve in elements:
586
+ if ve.is_text or ve.is_slice or ve.area <= 0:
587
+ continue
588
+ x, y, w, h = ve.rect
589
+ width_ratio = w / W
590
+ height_ratio = h / H
591
+ if not (0.38 <= width_ratio <= 0.96 and 0.025 <= height_ratio <= 0.13):
592
+ continue
593
+ if w / max(h, 1) < 2.2:
594
+ continue
595
+ center_dx = abs((x + w / 2) - W / 2)
596
+ if center_dx > 0.18 * W:
597
+ continue
598
+ if (y + h) < 0.45 * H:
599
+ continue
600
+ controls = _count_controls_near(elements, ve.rect, W, H)
601
+ text_count = sum(
602
+ 1 for child in elements
603
+ if child.is_text and _intersect_ratio(child.rect, ve.rect) >= 0.55
604
+ )
605
+ if controls >= 2 and text_count >= 1:
606
+ dock_candidates.append((controls, ve))
607
+
608
+ if dock_candidates:
609
+ dock_candidates.sort(key=lambda item: (item[0], item[1].area), reverse=True)
610
+ dock = dock_candidates[0][1]
611
+ input_dock = {
612
+ "rect": dock.rect,
613
+ "nearbyControlCount": dock_candidates[0][0],
614
+ }
615
+
616
+ keyboard = _detect_keyboard(elements, W, H)
617
+ if input_dock and not keyboard:
618
+ keyboard = _keyboard_below_dock(elements, input_dock, W, H)
619
+ if not input_dock and not keyboard:
620
+ return None
621
+
622
+ bubbles = []
623
+ for ve in _candidate_visuals(elements, W, H):
624
+ x, y, w, h = ve.rect
625
+ if not (0.42 <= w / W <= 0.98 and 0.035 <= h / H <= 0.48):
626
+ continue
627
+ text_count = sum(
628
+ 1 for child in elements
629
+ if child.is_text and _intersect_ratio(child.rect, ve.rect) >= 0.55
630
+ )
631
+ if text_count < 1:
632
+ continue
633
+ bubbles.append({"rect": ve.rect, "textCount": text_count})
634
+
635
+ stacked_bubbles = 0
636
+ ordered = sorted(bubbles, key=lambda item: item["rect"][1])
637
+ maximal_bubbles = []
638
+ for index, bubble in enumerate(ordered):
639
+ rect = bubble["rect"]
640
+ contained = any(
641
+ other is not bubble and _rect_inside(rect, other["rect"], tol=1.0)
642
+ for other in ordered
643
+ )
644
+ if not contained:
645
+ maximal_bubbles.append(bubble)
646
+ for index in range(1, len(maximal_bubbles)):
647
+ previous = maximal_bubbles[index - 1]
648
+ current = maximal_bubbles[index]
649
+ gap = current["rect"][1] - (previous["rect"][1] + previous["rect"][3])
650
+ if 0 <= gap <= 0.16 * H:
651
+ stacked_bubbles += 1
652
+
653
+ if keyboard and input_dock:
654
+ pass
655
+ elif not bubbles or (len(bubbles) < 2 and not keyboard):
656
+ return None
657
+ elif not keyboard and stacked_bubbles == 0:
658
+ return None
659
+
660
+ return {
661
+ "inputDock": input_dock,
662
+ "keyboard": keyboard,
663
+ "bubbleCount": len(bubbles),
664
+ "stackedBubblePairs": stacked_bubbles,
665
+ }
666
+
667
+
668
+ def _is_repeated_control_bar(elements, rect):
669
+ """识别横向重复图标/选项控件条,例如评分、开关组或快捷操作条。"""
670
+ x, y, w, h = rect
671
+ card_area = max(w * h, 1)
672
+ clusters = []
673
+ for ve in elements:
674
+ if ve.is_text or ve.area <= 0:
675
+ continue
676
+ vx, vy, vw, vh = ve.rect
677
+ width_ratio = vw / max(w, 1)
678
+ height_ratio = vh / max(h, 1)
679
+ if not (0.07 <= width_ratio <= 0.32 and 0.18 <= height_ratio <= 0.92):
680
+ continue
681
+ if not (0.45 <= vw / max(vh, 1) <= 2.2):
682
+ continue
683
+ if _intersect_ratio(ve.rect, rect) < 0.55:
684
+ continue
685
+ center_x = vx + vw / 2
686
+ center_y = vy + vh / 2
687
+ target = None
688
+ for cluster in clusters:
689
+ _, sample_x, sample_y, sample_w, sample_h, _ = cluster
690
+ if (abs(center_x - sample_x) <= 0.015 * w and
691
+ abs(center_y - sample_y) <= 0.06 * h):
692
+ target = cluster
693
+ break
694
+ if (abs(center_x - sample_x) <= 0.05 * w and
695
+ abs(center_y - sample_y) <= 0.16 * h and
696
+ 0.55 <= vw / max(sample_w, 1) <= 1.8 and
697
+ 0.55 <= vh / max(sample_h, 1) <= 1.8):
698
+ target = cluster
699
+ break
700
+ if target is None:
701
+ clusters.append([ve, center_x, center_y, vw, vh, ve.area])
702
+ elif ve.area > target[5]:
703
+ target[:] = [ve, center_x, center_y, vw, vh, ve.area]
704
+
705
+ if len(clusters) < 3:
706
+ return False
707
+ clusters.sort(key=lambda item: item[1])
708
+ gaps = [
709
+ clusters[index + 1][1] - clusters[index][1]
710
+ for index in range(len(clusters) - 1)
711
+ ]
712
+ span = clusters[-1][1] - clusters[0][1]
713
+ # 支配性守卫(2026-09-06,01基金_基金推荐 误杀案例):真控件条(评分/开关组/
714
+ # 快捷操作条)整卡几乎只有这条横排,纵向应占据卡片大部分;业务卡底部的
715
+ # 同构子卡行(3 列收益卡/入口宫格)只覆盖卡高一部分,标题/列表等主内容在上。
716
+ # 用簇的纵向并集覆盖率区分两者,避免把业务卡误判为控件条。
717
+ band_top = min(item[2] - item[4] / 2 for item in clusters)
718
+ band_bottom = max(item[2] + item[4] / 2 for item in clusters)
719
+ band_cover = (band_bottom - band_top) / max(h, 1)
720
+ if band_cover < 0.5:
721
+ return False
722
+ return span >= 0.48 * w and max(gaps) - min(gaps) <= 0.38 * max(gaps)
723
+
724
+
725
+ def _is_chat_bubble_shell(elements, rect, width, height):
726
+ """识别左右锚定的中型气泡/推荐壳,不把会话流片段当独立卡片。
727
+
728
+ 关键区分:含图片/切图/多形状等富视觉内容的壳体是商品卡/内容卡,
729
+ 不是简单对话气泡 → 返回 False。
730
+ """
731
+ x, y, w, h = rect
732
+ width_ratio = w / max(width, 1)
733
+ left_inset = x / max(width, 1)
734
+ right_inset = (width - x - w) / max(width, 1)
735
+ if not (0.38 <= width_ratio <= 0.88):
736
+ return False
737
+ if min(left_inset, right_inset) < 0.035 or max(left_inset, right_inset) < 0.07:
738
+ return False
739
+ text_count = sum(
740
+ 1 for ve in elements
741
+ if ve.is_text and _intersect_ratio(ve.rect, rect) >= 0.55
742
+ )
743
+ # 富视觉内容检测:含切图/图片/多形状的壳体是内容卡,不是气泡
744
+ rich_count = sum(
745
+ 1 for ve in elements
746
+ if not ve.is_text and _intersect_ratio(ve.rect, rect) >= 0.55
747
+ and not _is_divider(ve) and ve.area > 0
748
+ and (ve.is_slice or ve.type == "symbol"
749
+ or (ve.type == "shape" and (ve.fills or ve.border
750
+ or any(v > 0 for v in _radius_values(ve.radius)))))
751
+ )
752
+ if rich_count >= 2:
753
+ return False
754
+ return text_count >= 1
755
+
756
+
757
+ def _is_prose_paragraph_region(elements, rect):
758
+ """识别散文段落区域(AI 回复的纯文字说明块)——不是卡片。
759
+
760
+ 几何签名(与列表/表单卡区分):
761
+ - 文本按行堆叠,行距 ≈ 行高(pitch/height <= 1.6,列表行通常 >= 2);
762
+ - 行高几乎一致(±4px),行首 x 对齐(±6px);
763
+ - 区域内有 >= 3 行。
764
+ 注意 MeaXure 会把同行不同字重的片段(如加粗金额)导出为多个文本,
765
+ 因此行首对齐必须按「行聚类」后的行起始 x 判定,而非逐文本判定。
766
+ 列表卡即使纯文本也有行壳间距与多列 x 分布,不会同时满足以上三条。
767
+ """
768
+ x, y, w, h = rect
769
+ texts = [
770
+ ve for ve in elements
771
+ if ve.is_text and ve.area > 0 and _intersect_ratio(ve.rect, rect) >= 0.55
772
+ ]
773
+ if len(texts) < 3:
774
+ return False
775
+ # 剔除合并容器层(MeaXure 常同时导出整段合并层 + 行碎片):高度远超中位行高者
776
+ heights = sorted(ve.rect[3] for ve in texts)
777
+ median_h = heights[len(heights) // 2] or 1
778
+ line_texts = [ve for ve in texts if ve.rect[3] <= median_h * 1.8]
779
+ if len(line_texts) < 3:
780
+ return False
781
+ # 行高一致性:>= 2/3 文本行高在中位行高 ±4 内
782
+ uniform = sum(1 for ve in line_texts if abs(ve.rect[3] - median_h) <= 4)
783
+ if uniform < len(line_texts) * 2 / 3:
784
+ return False
785
+ # 行聚类:y 差 <= 行高一半 的文本属同一行(同行可能有多个不同字重碎片)
786
+ line_texts = sorted(line_texts, key=lambda ve: (ve.rect[1], ve.rect[0]))
787
+ lines = [] # [(line_y, line_x0)]
788
+ for ve in line_texts:
789
+ vy, vx = ve.rect[1], ve.rect[0]
790
+ if lines and vy - lines[-1][0] <= median_h * 0.5:
791
+ prev_y, prev_x0 = lines[-1]
792
+ lines[-1] = (prev_y, min(prev_x0, vx))
793
+ else:
794
+ lines.append((vy, vx))
795
+ if len(lines) < 3:
796
+ return False
797
+ # 行首 x 对齐:>= 2/3 行的起始 x 在最小行首 x ±6 内(换行行首)
798
+ min_line_x = min(lx for _, lx in lines)
799
+ left_aligned = sum(1 for _, lx in lines if abs(lx - min_line_x) <= 6)
800
+ if left_aligned < len(lines) * 2 / 3:
801
+ return False
802
+ # 行距/行高比:相邻行距应 ≈ 行高
803
+ pitches = sorted(lines[i + 1][0] - lines[i][0] for i in range(len(lines) - 1))
804
+ median_pitch = pitches[len(pitches) // 2] or 1
805
+ return median_pitch <= median_h * 1.6
806
+
807
+
808
+ def _is_stacked_text_bubble_union(elements, rect, width, height):
809
+ """识别两个以上同宽同高文本气泡合并出的全宽区域。
810
+
811
+ 关键区分:如果"气泡"被一个有视觉属性(圆角/填充/阴影/描边)的大壳体
812
+ 包裹,它们是卡片的列表子项(如积分明细行、优惠券条目),不是独立对话
813
+ 气泡 → 返回 False。
814
+
815
+ 第二重区分(2026-09-06):配对壳体必须内部无图形控件才算文本气泡。
816
+ 业务卡的相邻区块(如基金对比卡的 VS 段与产品段)几何上同样「同宽、
817
+ 相邻、高度差≤35%」,但内部含按钮/图表等控件——卡片证据优先直接放行。
818
+ 误杀案例:基金1128 #14/#15/#16(产品比较卡)、理财1218 #24、
819
+ 网点查询1219 #8/#13;真气泡(转账二批次 #25-27、收款人名册 #9-13)
820
+ 内部控件数为 0,不受影响。
821
+ """
822
+ _, _, w, h = rect
823
+ if w < 0.82 * width or h < 0.08 * height:
824
+ return False
825
+ shells = []
826
+ for ve in _candidate_visuals(elements, width, height):
827
+ if _intersect_ratio(ve.rect, rect) < 0.55:
828
+ continue
829
+ text_count = sum(
830
+ 1 for child in elements
831
+ if child.is_text and _intersect_ratio(child.rect, ve.rect) >= 0.55
832
+ )
833
+ if text_count >= 1:
834
+ shells.append(ve)
835
+ # 卡片壳体包含检测:存在有视觉属性的大壳体包裹 ≥50% 子壳 → 是卡片列表
836
+ if len(shells) >= 3:
837
+ for ve in shells:
838
+ has_shell_visual = (
839
+ any(v > 0 for v in _radius_values(ve.radius))
840
+ or ve.fills or ve.shadows or ve.border
841
+ )
842
+ if not has_shell_visual:
843
+ continue
844
+ contained = sum(
845
+ 1 for other in shells
846
+ if other is not ve and _rect_inside(other.rect, ve.rect, tol=2.0)
847
+ )
848
+ if contained >= max(2, len(shells) * 0.5):
849
+ return False
850
+ found_bubble_pair = False
851
+ for index, left in enumerate(shells):
852
+ for right in shells[index + 1:]:
853
+ lx, ly, lw, lh = left.rect
854
+ rx, ry, rw, rh = right.rect
855
+ if (abs(lw - rw) <= 0.18 * max(lw, rw) and
856
+ abs(lh - rh) <= 0.35 * max(lh, rh)):
857
+ gap = ry - (ly + lh)
858
+ if 0 <= gap <= 0.16 * height:
859
+ if (_inner_visual_count(left, elements) <= 1 and
860
+ _inner_visual_count(right, elements) <= 1):
861
+ found_bubble_pair = True
862
+ else:
863
+ return False
864
+ return found_bubble_pair
865
+
866
+
867
+ def _inner_visual_count(shell, elements):
868
+ """壳体内部的图形控件数(剔除文本、分隔线、同 rect 叠层与背景层)。
869
+
870
+ 用于区分「文本气泡」(内部仅文字,控件数≈0)与「卡片区块」
871
+ (内部有按钮/图表等控件)。
872
+ """
873
+ count = 0
874
+ shell_key = _norm_rect_key(shell.rect)
875
+ for child in elements:
876
+ if child.is_text or child.area <= 0 or _is_divider(child):
877
+ continue
878
+ if _norm_rect_key(child.rect) == shell_key:
879
+ continue
880
+ if child.area >= 0.5 * shell.area:
881
+ continue
882
+ if _intersect_ratio(child.rect, shell.rect) < 0.55:
883
+ continue
884
+ count += 1
885
+ return count
886
+
887
+
888
+ def _is_divider(ve):
889
+ _, _, width, height = ve.rect
890
+ return width <= 2 or height <= 2
891
+
892
+
893
+ def card_focus_metrics(elements, rect, width, height):
894
+ """量化候选区域的结构复杂度,供聚焦提取统一过滤。"""
895
+ x, y, w, h = rect
896
+ card_area = max(w * h, 1)
897
+ text_count = 0
898
+ rich_count = 0
899
+ slice_count = 0
900
+ symbol_count = 0
901
+ shape_count = 0
902
+ for ve in elements:
903
+ if _intersect_ratio(ve.rect, rect) < 0.35:
904
+ continue
905
+ if ve.is_text:
906
+ if ve.area > 0:
907
+ text_count += 1
908
+ continue
909
+ if _is_divider(ve) or ve.area >= 0.55 * card_area:
910
+ continue
911
+ if ve.is_slice:
912
+ slice_count += 1
913
+ rich_count += 1
914
+ elif ve.type == "symbol":
915
+ symbol_count += 1
916
+ rich_count += 1
917
+ elif ve.type == "shape" and (ve.fills or ve.border or any(v > 0 for v in _radius_values(ve.radius))):
918
+ shape_count += 1
919
+ rich_count += 1
920
+ return {
921
+ "areaRatio": card_area / max(width * height, 1),
922
+ "aspectRatio": w / max(h, 1),
923
+ "heightRatio": h / max(height, 1),
924
+ "textCount": text_count,
925
+ "richComponentCount": rich_count,
926
+ "sliceCount": slice_count,
927
+ "symbolCount": symbol_count,
928
+ "shapeCount": shape_count,
929
+ }
930
+
931
+
932
+ def classify_card_focus(elements, rect, width, height, interaction_flow=None):
933
+ """判定候选区域是否是可落地的独立卡片。
934
+
935
+ 只使用几何与结构复杂度:对话/纯文本容器缺少有效图形控件;按钮/工具条
936
+ 是扁长低复杂度壳体;两者都不应进入下游训练。
937
+ """
938
+ x, y, w, h = rect
939
+ metrics = card_focus_metrics(elements, rect, width, height)
940
+ if interaction_flow:
941
+ for signal_name in ("inputDock", "keyboard"):
942
+ signal = interaction_flow.get(signal_name)
943
+ signal_rect = signal.get("rect") if signal else None
944
+ if not signal_rect:
945
+ continue
946
+ sx, sy, sw, sh = signal_rect
947
+ signal_area = max(sw * sh, 1)
948
+ ix = max(0, min(x + w, sx + sw) - max(x, sx))
949
+ iy = max(0, min(y + h, sy + sh) - max(y, sy))
950
+ if (ix * iy) / signal_area >= 0.30:
951
+ return False, f"conversation_{signal_name}_region", metrics
952
+ if metrics["richComponentCount"] == 0 and metrics["textCount"] == 0:
953
+ return False, "empty_region", metrics
954
+ # 低结构判定:richComponent < 2 且文本也不足 → 才拒绝。
955
+ # 文本丰富的卡片(如积分明细列表,textCount≥4)即使 rich 少也应保留,
956
+ # 下游可从纯文本+几何生成合法卡片。
957
+ if metrics["richComponentCount"] < 2 and metrics["textCount"] < 4:
958
+ return False, "text_only_or_low_structure_region", metrics
959
+ # 纯文本散文段落(AI 回复说明文字):无论文本多丰富都不是卡片。
960
+ # 用户规则:对话气泡/纯文本回复不参与卡片训练。
961
+ if metrics["richComponentCount"] == 0 and _is_prose_paragraph_region(elements, rect):
962
+ return False, "prose_paragraph_region", metrics
963
+ if _is_repeated_control_bar(elements, rect):
964
+ return False, "repeated_control_bar_region", metrics
965
+ if _is_chat_bubble_shell(elements, rect, width, height):
966
+ return False, "conversation_bubble_region", metrics
967
+ if _is_stacked_text_bubble_union(elements, rect, width, height):
968
+ return False, "stacked_conversation_bubbles_region", metrics
969
+ if (metrics["aspectRatio"] >= 2.2 and metrics["heightRatio"] <= 0.15 and
970
+ metrics["richComponentCount"] <= 2):
971
+ return False, "single_button_or_bar_region", metrics
972
+ return True, "", metrics
973
+
974
+
975
+ def detect_cards(raw_layers, width, height, interaction_flow=None):
976
+ """核心:从图层原始数据检测 1..N 个卡片区域。
977
+
978
+ 返回 [(x, y, w, h, elements, shapes)]:
979
+ - elements:归属于该卡片的 VisualElement 列表
980
+ - shapes:[((sx,sy,sw,sh), radii), ...],卡片外壳几何(仅 rect+radius,
981
+ 用于生成透明遮罩,**不依赖背景颜色**)
982
+ """
983
+ W, H = float(width), float(height)
984
+ elements = _flatten_with_tree(raw_layers)
985
+ bs = _bottom_sheet_rect(raw_layers, W, H)
986
+ if detect_overlay_dialog(elements, W, H) or bs:
987
+ return []
988
+
989
+ candidates = []
990
+ candidate_shapes = []
991
+ for ve in _candidate_visuals(elements, W, H):
992
+ x, y, w, h = ve.rect
993
+ candidates.append(ve.rect)
994
+ candidate_shapes.append((ve.rect, list(ve.radius or [])))
995
+
996
+ cards = _merge_rects(candidates, W, H)
997
+
998
+ result = []
999
+ for c in cards:
1000
+ if interaction_flow:
1001
+ cx, cy, cw, ch = c
1002
+ candidate_area = max(cw * ch, 1)
1003
+ rejected = False
1004
+ for signal_name in ("inputDock", "keyboard"):
1005
+ signal = interaction_flow.get(signal_name)
1006
+ signal_rect = signal.get("rect") if signal else None
1007
+ if not signal_rect:
1008
+ continue
1009
+ sx, sy, sw, sh = signal_rect
1010
+ ix = max(0, min(cx + cw, sx + sw) - max(cx, sx))
1011
+ iy = max(0, min(cy + ch, sy + sh) - max(cy, sy))
1012
+ if (ix * iy) / candidate_area >= 0.30:
1013
+ rejected = True
1014
+ break
1015
+ if rejected:
1016
+ continue
1017
+ elements_in = []
1018
+ for ve in elements:
1019
+ if _point_in_rect(ve_center(ve), c) or _overlap(ve.rect, c) > 0.3 * ve.area:
1020
+ elements_in.append(ve)
1021
+ # 同 rect 的外壳层去重:优先保留有圆角的卡面,剔除同 rect 的
1022
+ # 无圆角背景层。否则无圆角矩形会把圆角外背景重新遮成不透明。
1023
+ by_rect = {}
1024
+ for r, rad in candidate_shapes:
1025
+ if not _rect_inside(r, c):
1026
+ continue
1027
+ key = _norm_rect_key(r)
1028
+ cur = by_rect.get(key)
1029
+ if cur is None or _radii_score(rad) > _radii_score(cur[1]):
1030
+ by_rect[key] = (r, rad)
1031
+ shapes = sorted(by_rect.values(), key=lambda item: (item[0][1], item[0][0]))
1032
+ result.append((c[0], c[1], c[2], c[3], elements_in, shapes))
1033
+
1034
+ result.sort(key=lambda t: (t[1], t[0]))
1035
+ return result
1036
+
1037
+
1038
+ def ve_center(ve):
1039
+ x, y, w, h = ve.rect
1040
+ return (x + w / 2, y + h / 2)
1041
+
1042
+
1043
+ def _point_in_rect(pt, r):
1044
+ px, py = pt
1045
+ x, y, w, h = r
1046
+ return x <= px <= x + w and y <= py <= y + h
1047
+
1048
+
1049
+ # ---------- Flex 布局推断(纯几何,对齐平台节点 flex 属性) ----------
1050
+ def infer_flex_layout(container_rect, child_rects):
1051
+ """从容器与其子元素几何推断 flex 布局参数(纯视觉,无文本依赖)。
1052
+
1053
+ MeaXure 导出的是绝对定位元素(Sketch 模型),无显式 flex 属性。
1054
+ 本函数从子元素的排列方向与对齐方式反推平台节点可用的 flex 参数:
1055
+ - direction : row(横向排列) / column(纵向排列)
1056
+ - justify : 主轴对齐(flex-start/center/flex-end/space-between)
1057
+ - align : 交叉轴对齐(flex-start/center/flex-end/stretch)
1058
+
1059
+ 参数:
1060
+ container_rect : (x, y, w, h) 容器矩形
1061
+ child_rects : [(x, y, w, h), ...] 直接子元素矩形(已剔除容器自身)
1062
+ 返回 dict 或 None(子元素 <2 无法判定对齐)。
1063
+ """
1064
+ if len(child_rects) < 2:
1065
+ return None
1066
+ cx, cy, cw, ch = container_rect
1067
+ kids = [r for r in child_rects if r[2] > 0 and r[3] > 0]
1068
+ if len(kids) < 2:
1069
+ return None
1070
+ # 方向判定:子元素中心在 x 上方差 vs y 上方差,大者为排列主轴
1071
+ xs = [r[0] + r[2] / 2 for r in kids]
1072
+ ys = [r[1] + r[3] / 2 for r in kids]
1073
+ spread_x = max(xs) - min(xs)
1074
+ spread_y = max(ys) - min(ys)
1075
+ direction = "row" if spread_x >= spread_y else "column"
1076
+ if direction == "row":
1077
+ main_centers, cross_centers = xs, ys
1078
+ main_size, cross_size = cw, ch
1079
+ kid_main_size = [r[2] for r in kids]
1080
+ else:
1081
+ main_centers, cross_centers = ys, xs
1082
+ main_size, cross_size = ch, cw
1083
+ kid_main_size = [r[3] for r in kids]
1084
+ main_start = (cx if direction == "row" else cy)
1085
+ # 主轴对齐:子元素中心相对容器主轴的分布
1086
+ main_extent = max(main_centers) - min(main_centers)
1087
+ main_min_off = min(main_centers) - main_start
1088
+ main_max_off = (main_start + main_size) - max(main_centers)
1089
+ total_kid_main = sum(kid_main_size)
1090
+ slack = main_size - total_kid_main
1091
+ justify = "flex-start"
1092
+ if slack > 0:
1093
+ # 间距均匀且两侧留白相近 → space-between 或 center
1094
+ if len(kids) >= 3 and main_extent > 0:
1095
+ gaps = [main_centers[i + 1] - main_centers[i] for i in range(len(main_centers) - 1)]
1096
+ if gaps and max(gaps) - min(gaps) <= 0.3 * max(gaps):
1097
+ justify = "space-between" if (abs(main_min_off - main_max_off) < 0.2 * slack) else "space-between"
1098
+ if justify == "flex-start":
1099
+ if abs(main_min_off - main_max_off) <= 0.2 * slack:
1100
+ justify = "center"
1101
+ elif main_max_off < main_min_off:
1102
+ justify = "flex-end"
1103
+ # 交叉轴对齐:子元素中心相对容器交叉轴居中度
1104
+ cross_start = (cy if direction == "row" else cx)
1105
+ cross_offs = [c - (cross_start + cross_size / 2) for c in cross_centers]
1106
+ avg_cross_off = sum(cross_offs) / len(cross_offs)
1107
+ if abs(avg_cross_off) <= 0.1 * cross_size:
1108
+ align = "center"
1109
+ elif avg_cross_off < 0:
1110
+ align = "flex-start"
1111
+ else:
1112
+ align = "flex-end"
1113
+ return {
1114
+ "direction": direction,
1115
+ "justifyContent": justify,
1116
+ "alignItems": align,
1117
+ "childCount": len(kids),
1118
+ }