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.
- package/README.md +46 -5
- package/dist/cli.js +51 -1
- package/dist/config.js +51 -16
- package/dist/core.js +2 -2
- package/dist/init.js +425 -0
- package/dist/preview-editor/assets/index-CHuGGIR3.js +276 -0
- package/dist/preview-editor/assets/{index-B4VZgNSK.css → index-CQScMhyg.css} +1 -1
- package/dist/preview-editor/index.html +2 -2
- package/dist/standalone.js +8 -0
- package/dist/vendored/mcp/client.mjs +537 -0
- package/dist/vendored/mcp/tools.mjs +268 -0
- package/dist/vendored/preview/assets/default-image.png +0 -0
- package/dist/{server-core → vendored/server/core}/ai-chat.js +82 -38
- package/dist/{server-core → vendored/server/core}/browser.js +1 -1
- package/dist/{server-core → vendored/server/core}/cardSave.js +8 -3
- package/dist/{server-core → vendored/server/core}/cardSync.js +6 -14
- package/dist/{server-core → vendored/server/core}/cardValidate.js +11 -8
- package/dist/{server-core → vendored/server/core}/index.js +5 -3
- package/dist/vendored/server/core/skillPaths.js +124 -0
- package/dist/{server-core → vendored/server/core}/sliceUpload.js +5 -8
- package/dist/vendored/skills/README.md +93 -0
- package/dist/vendored/skills/card-doctor/SKILL.md +52 -0
- package/dist/vendored/skills/card-skill-trainer/SKILL.md +312 -0
- package/dist/vendored/skills/card-skill-trainer/baselines/v3-r038n.json +43 -0
- package/dist/vendored/skills/card-skill-trainer/baselines/v3-r039n.json +47 -0
- package/dist/vendored/skills/card-skill-trainer/baselines/v3-r050n.json +48 -0
- package/dist/vendored/skills/card-skill-trainer/scripts/build_dataset.py +472 -0
- package/dist/vendored/skills/card-skill-trainer/scripts/build_sample.mjs +162 -0
- package/dist/vendored/skills/card-skill-trainer/scripts/diagnose_case.mjs +241 -0
- package/dist/vendored/skills/card-skill-trainer/scripts/lib/scoring.mjs +525 -0
- package/dist/vendored/skills/card-skill-trainer/scripts/round_diff.mjs +133 -0
- package/dist/vendored/skills/card-skill-trainer/scripts/run_round.mjs +1043 -0
- package/dist/vendored/skills/card-skill-trainer/scripts/score_round.mjs +195 -0
- package/dist/vendored/skills/card-skill-trainer/scripts/test_missing_slice.py +122 -0
- package/dist/vendored/skills/dsl-card-ai/SKILL.md +191 -0
- package/dist/vendored/skills/dsl-card-generator/SKILL.md +480 -0
- package/dist/vendored/skills/dsl-card-generator/config/render-contract.json +11 -0
- package/dist/vendored/skills/dsl-card-generator/references/amount_formatting.md +138 -0
- package/dist/vendored/skills/dsl-card-generator/references/card_index.json +1597 -0
- package/dist/vendored/skills/dsl-card-generator/references/card_templates.json +384 -0
- package/dist/vendored/skills/dsl-card-generator/references/card_to_summary.md +56 -0
- package/dist/vendored/skills/dsl-card-generator/references/component_rules.md +76 -0
- package/dist/vendored/skills/dsl-card-generator/references/component_specs.md +393 -0
- package/dist/vendored/skills/dsl-card-generator/references/dsl_structure.md +208 -0
- package/dist/vendored/skills/dsl-card-generator/references/event_patterns.md +287 -0
- package/dist/vendored/skills/dsl-card-generator/references/expression_engine.md +121 -0
- package/dist/vendored/skills/dsl-card-generator/references/field_name_dict.json +34 -0
- package/dist/vendored/skills/dsl-card-generator/references/field_naming.md +101 -0
- package/dist/vendored/skills/dsl-card-generator/references/generation_pitfalls.md +928 -0
- package/dist/vendored/skills/dsl-card-generator/references/handcrafted/README.md +69 -0
- package/dist/vendored/skills/dsl-card-generator/references/handcrafted/examples_index.json +2967 -0
- package/dist/vendored/skills/dsl-card-generator/references/handcrafted/patterns.json +2835 -0
- package/dist/vendored/skills/dsl-card-generator/references/handcrafted/recipes_v2.json +648 -0
- package/dist/vendored/skills/dsl-card-generator/references/handcrafted/recipes_v2_schema.md +295 -0
- package/dist/vendored/skills/dsl-card-generator/references/image_diff.md +48 -0
- package/dist/vendored/skills/dsl-card-generator/references/image_to_card.md +129 -0
- package/dist/vendored/skills/dsl-card-generator/references/ir-format.md +212 -0
- package/dist/vendored/skills/dsl-card-generator/references/message-center-pattern.md +149 -0
- package/dist/vendored/skills/dsl-card-generator/references/platform_jsapi.md +196 -0
- package/dist/vendored/skills/dsl-card-generator/references/previewer_contract.md +54 -0
- package/dist/vendored/skills/dsl-card-generator/references/quickref.md +197 -0
- package/dist/vendored/skills/dsl-card-generator/references/recipe_snippets/absolute-badge.json +84 -0
- package/dist/vendored/skills/dsl-card-generator/references/recipe_snippets/accordion-group-toggle.json +18 -0
- package/dist/vendored/skills/dsl-card-generator/references/recipe_snippets/alertCard-pair.json +12 -0
- package/dist/vendored/skills/dsl-card-generator/references/recipe_snippets/alertDialog-call.json +12 -0
- package/dist/vendored/skills/dsl-card-generator/references/recipe_snippets/canvas-countdown.json +15 -0
- package/dist/vendored/skills/dsl-card-generator/references/recipe_snippets/canvas-donut.json +15 -0
- package/dist/vendored/skills/dsl-card-generator/references/recipe_snippets/canvas-gauge-score.json +11 -0
- package/dist/vendored/skills/dsl-card-generator/references/recipe_snippets/canvas-radar.json +10 -0
- package/dist/vendored/skills/dsl-card-generator/references/recipe_snippets/canvas-title-head.json +13 -0
- package/dist/vendored/skills/dsl-card-generator/references/recipe_snippets/globalSet.json +12 -0
- package/dist/vendored/skills/dsl-card-generator/references/recipe_snippets/label-col-modes.json +21 -0
- package/dist/vendored/skills/dsl-card-generator/references/recipe_snippets/list-expand-collapse.json +17 -0
- package/dist/vendored/skills/dsl-card-generator/references/recipe_snippets/money-format.json +11 -0
- package/dist/vendored/skills/dsl-card-generator/references/recipe_snippets/opacity-switch.json +130 -0
- package/dist/vendored/skills/dsl-card-generator/references/recipe_snippets/progress-bar.json +25 -0
- package/dist/vendored/skills/dsl-card-generator/references/recipe_snippets/tab-billMode.json +15 -0
- package/dist/vendored/skills/dsl-card-generator/references/recipe_snippets/title-underline-overlay.json +21 -0
- package/dist/vendored/skills/dsl-card-generator/references/recipes.md +797 -0
- package/dist/vendored/skills/dsl-card-generator/references/rules.md +235 -0
- package/dist/vendored/skills/dsl-card-generator/references/slices_convention.md +177 -0
- package/dist/vendored/skills/dsl-card-generator/references/spec_to_schema_patterns.md +609 -0
- package/dist/vendored/skills/dsl-card-generator/references/ui_fidelity_rules.md +131 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/add_component.mjs +118 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/analyze_component_usage.mjs +153 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/apply_opacity_pattern.mjs +343 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/build_card_index.mjs +108 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/build_card_templates.mjs +190 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/check_fidelity.mjs +257 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/check_magic_values.mjs +79 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/crop_slices.mjs +98 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/describe_card.mjs +265 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/diff_bands.mjs +257 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/diff_card.mjs +316 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/expand_ir.mjs +873 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/extract_handcrafted.py +1199 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/extract_subtree.mjs +191 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/generate_cards.mjs +389 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/inventory_schemas.py +160 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/iterate.mjs +700 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/design_image.mjs +56 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/design_sample.mjs +328 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/layout-tokens.mjs +79 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/paths.mjs +103 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/scaffold.mjs +575 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/slicePath.mjs +81 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/slice_dedupe.mjs +147 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/lint_autofix.mjs +363 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/lint_card.mjs +1399 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/preflight.mjs +249 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/register_card_template.mjs +208 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/render_baseline.mjs +231 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/render_card.mjs +801 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/render_diff.mjs +239 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/resolve_card_template.mjs +73 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/route_input.mjs +447 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/scaffold_card.mjs +81 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/slices_normalize.mjs +316 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/spec_audit.mjs +462 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/spec_to_schema_v3.mjs +7735 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/spec_tree.mjs +355 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/validate_all_schemas.py +71 -0
- package/dist/vendored/skills/dsl-card-generator/vendor/preview/assets/default-image-wzZYQ86K.png +0 -0
- package/dist/vendored/skills/dsl-card-generator/vendor/preview/assets/standalone-C9k-07y7.css +1 -0
- package/dist/vendored/skills/dsl-card-generator/vendor/preview/assets/standalone-oFRM4f4V.js +55 -0
- package/dist/vendored/skills/dsl-card-generator/vendor/preview/assets/svg-symbols-B2exBrAr.svg +118 -0
- package/dist/vendored/skills/dsl-card-generator/vendor/preview/meta/component-meta.json +2714 -0
- package/dist/vendored/skills/dsl-card-generator/vendor/preview/meta/directive-meta.json +40 -0
- package/dist/vendored/skills/dsl-card-generator/vendor/preview/meta/style-meta.json +1086 -0
- package/dist/vendored/{standalone.html → skills/dsl-card-generator/vendor/preview/standalone.html} +2 -1
- package/dist/vendored/skills/finmall-card-sync/SKILL.md +262 -0
- package/dist/vendored/skills/finmall-session-navigator/SKILL.md +169 -0
- package/dist/vendored/skills/finmall-session-navigator/scripts/finmall_login.mjs +882 -0
- package/dist/vendored/skills/finmall-session-navigator/scripts/package-lock.json +59 -0
- package/dist/vendored/skills/finmall-session-navigator/scripts/package.json +10 -0
- package/dist/vendored/skills/finmall-session-navigator/scripts/session-manager.mjs +96 -0
- package/dist/vendored/skills/shared/README.md +55 -0
- package/dist/vendored/skills/shared/auth-model.md +79 -0
- package/dist/vendored/skills/sketch-spec-to-card/SKILL.md +329 -0
- package/dist/vendored/skills/sketch-spec-to-card/references/boundary.json +14 -0
- package/dist/vendored/skills/sketch-spec-to-card/references/card_refs.json +13 -0
- package/dist/vendored/skills/sketch-spec-to-card/references/spec-schema.md +103 -0
- package/dist/vendored/skills/sketch-spec-to-card/scripts/build_prompt.py +797 -0
- package/dist/vendored/skills/sketch-spec-to-card/scripts/card_refs.py +94 -0
- package/dist/vendored/skills/sketch-spec-to-card/scripts/cluster_states.py +706 -0
- package/dist/vendored/skills/sketch-spec-to-card/scripts/parse_meaxure.py +1781 -0
- package/dist/vendored/skills/sketch-spec-to-card/scripts/png_utils.py +318 -0
- package/dist/vendored/skills/sketch-spec-to-card/scripts/vision.py +1118 -0
- package/dist/vendored/templates/helloCard/data.json +10 -0
- package/dist/vendored/templates/helloCard/schema.json +238 -0
- package/dist/vendored/tools/link-skills.mjs +485 -0
- package/package.json +2 -1
- package/dist/preview-editor/assets/index-jl4YJzKv.js +0 -180
- package/dist/vendored/App.vue +0 -2858
- package/dist/vendored/ansi.js +0 -185
- package/dist/vendored/assets/apple-touch-icon.png +0 -0
- package/dist/vendored/assets/default-image.png +0 -0
- package/dist/vendored/assets/favicon-16.png +0 -0
- package/dist/vendored/assets/favicon-32.png +0 -0
- package/dist/vendored/assets/site-icon.png +0 -0
- package/dist/vendored/assets/svgIcons/icons.svg +0 -87
- package/dist/vendored/assets/svgIcons/index.js +0 -39
- package/dist/vendored/components/AiSettings.vue +0 -392
- package/dist/vendored/components/AlertCardInstance.vue +0 -58
- package/dist/vendored/components/AlertCardLayer.vue +0 -59
- package/dist/vendored/components/ApiCallPanel.vue +0 -127
- package/dist/vendored/components/ApiPayloadTree.vue +0 -201
- package/dist/vendored/components/AppToast.vue +0 -37
- package/dist/vendored/components/CardGallery.vue +0 -291
- package/dist/vendored/components/CardPreview.vue +0 -72
- package/dist/vendored/components/CardSelect.vue +0 -206
- package/dist/vendored/components/ChatComposer.vue +0 -413
- package/dist/vendored/components/ChatMessage.vue +0 -383
- package/dist/vendored/components/ChatPanel.vue +0 -937
- package/dist/vendored/components/ComponentPalette.vue +0 -301
- package/dist/vendored/components/ConfirmDialog.vue +0 -54
- package/dist/vendored/components/DatePickerOverlay.vue +0 -345
- package/dist/vendored/components/DebugPanel.vue +0 -1734
- package/dist/vendored/components/DeviceOverlay.vue +0 -70
- package/dist/vendored/components/Dropdown.vue +0 -193
- package/dist/vendored/components/EventEditor.vue +0 -273
- package/dist/vendored/components/HoverGuides.vue +0 -122
- package/dist/vendored/components/JsonTree.vue +0 -547
- package/dist/vendored/components/LogValue.vue +0 -52
- package/dist/vendored/components/MarkdownBody.vue +0 -64
- package/dist/vendored/components/NewCardDialog.vue +0 -132
- package/dist/vendored/components/NodeInspector.vue +0 -295
- package/dist/vendored/components/NumberStepper.vue +0 -125
- package/dist/vendored/components/PickerOverlay.vue +0 -131
- package/dist/vendored/components/PropControl.vue +0 -245
- package/dist/vendored/components/PropTree.vue +0 -128
- package/dist/vendored/components/RemotePreview.vue +0 -173
- package/dist/vendored/components/SelectionOverlay.vue +0 -105
- package/dist/vendored/components/StructureTree.vue +0 -1002
- package/dist/vendored/components/Switch.vue +0 -38
- package/dist/vendored/components/setters/SetBgPosition.vue +0 -64
- package/dist/vendored/components/setters/SetBgSize.vue +0 -90
- package/dist/vendored/components/setters/SetCode.vue +0 -198
- package/dist/vendored/components/setters/SetColor.vue +0 -244
- package/dist/vendored/components/setters/SetCustomSelect.vue +0 -58
- package/dist/vendored/components/setters/SetExpr.vue +0 -107
- package/dist/vendored/components/setters/SetGradient.vue +0 -125
- package/dist/vendored/components/setters/SetGrid9.vue +0 -68
- package/dist/vendored/components/setters/SetIconSelect.vue +0 -67
- package/dist/vendored/components/setters/SetIfForm.vue +0 -61
- package/dist/vendored/components/setters/SetImage.vue +0 -43
- package/dist/vendored/components/setters/SetList.vue +0 -139
- package/dist/vendored/components/setters/SetNumber.vue +0 -118
- package/dist/vendored/components/setters/SetNumberStyle.vue +0 -113
- package/dist/vendored/components/setters/SetPadding.vue +0 -77
- package/dist/vendored/components/setters/SetSegment.vue +0 -160
- package/dist/vendored/components/setters/SetSelect.vue +0 -51
- package/dist/vendored/components/setters/SetSwitch.vue +0 -49
- package/dist/vendored/components/setters/SetText.vue +0 -52
- package/dist/vendored/components/setters/props.js +0 -23
- package/dist/vendored/fidelity/baseline-messageOverview.json +0 -87
- package/dist/vendored/fidelity/calibration.json +0 -29
- package/dist/vendored/fidelity/fidelity-messageOverview-after.html +0 -114
- package/dist/vendored/fidelity/fidelity-messageOverview-after.json +0 -272
- package/dist/vendored/fidelity/fidelity-messageOverview.html +0 -185
- package/dist/vendored/fidelity/fidelity-messageOverview.json +0 -336
- package/dist/vendored/fidelity/fidelity-messageOverview_v2.html +0 -262
- package/dist/vendored/fidelity/fidelity-messageOverview_v2.json +0 -502
- package/dist/vendored/fidelity/fidelity-messageOverview_v3.html +0 -201
- package/dist/vendored/fidelity/fidelity-messageOverview_v3.json +0 -417
- package/dist/vendored/fidelity/fidelity-messageOverview_visual.html +0 -125
- package/dist/vendored/fidelity/fidelity-messageOverview_visual.json +0 -338
- package/dist/vendored/index.html +0 -16
- package/dist/vendored/main.js +0 -37
- package/dist/vendored/runtime/ai-api.js +0 -165
- package/dist/vendored/runtime/alert-card.js +0 -125
- package/dist/vendored/runtime/app-toast.js +0 -33
- package/dist/vendored/runtime/clipboard-node.js +0 -78
- package/dist/vendored/runtime/clipboard.js +0 -29
- package/dist/vendored/runtime/event-model.js +0 -300
- package/dist/vendored/runtime/hoverGuide.js +0 -31
- package/dist/vendored/runtime/jsapi-mock.js +0 -256
- package/dist/vendored/runtime/markdown.js +0 -140
- package/dist/vendored/runtime/node-model.js +0 -217
- package/dist/vendored/skills/.manifest.json +0 -1101
- package/dist/vendored/standalone-main.js +0 -36
- package/dist/vendored/styles/ai-chat.css +0 -1850
- package/dist/vendored/styles/app.css +0 -3648
- package/dist/vendored/styles/card-gallery.css +0 -285
- package/dist/vendored/styles/card-select.css +0 -288
- package/dist/vendored/styles/components.css +0 -383
- package/dist/vendored/styles/dropdown.css +0 -324
- package/dist/vendored/styles/json-tree.css +0 -308
- package/dist/vendored/styles/node-inspector.css +0 -800
- package/dist/vendored/styles/setters.css +0 -1050
- package/dist/vendored/styles/standalone.css +0 -27
- package/dist/vendored/styles/structure-tree.css +0 -460
- /package/dist/vendored/{api.js → preview/api.js} +0 -0
- /package/dist/vendored/{assets → preview/assets}/svg-symbols.svg +0 -0
- /package/dist/vendored/{components → preview/components}/AbsoluteLayout.vue +0 -0
- /package/dist/vendored/{components → preview/components}/Button.vue +0 -0
- /package/dist/vendored/{components → preview/components}/Canvas.vue +0 -0
- /package/dist/vendored/{components → preview/components}/CheckGroup.vue +0 -0
- /package/dist/vendored/{components → preview/components}/Column.vue +0 -0
- /package/dist/vendored/{components → preview/components}/CustomPlaceholder.vue +0 -0
- /package/dist/vendored/{components → preview/components}/Divider.vue +0 -0
- /package/dist/vendored/{components → preview/components}/GridLayout.vue +0 -0
- /package/dist/vendored/{components → preview/components}/Icon.vue +0 -0
- /package/dist/vendored/{components → preview/components}/Image.vue +0 -0
- /package/dist/vendored/{components → preview/components}/Input.vue +0 -0
- /package/dist/vendored/{components → preview/components}/ListLayout.vue +0 -0
- /package/dist/vendored/{components → preview/components}/Map.vue +0 -0
- /package/dist/vendored/{components → preview/components}/MultiColumn.vue +0 -0
- /package/dist/vendored/{components → preview/components}/MultiText.vue +0 -0
- /package/dist/vendored/{components → preview/components}/ProgressSlider.vue +0 -0
- /package/dist/vendored/{components → preview/components}/RadioButton.vue +0 -0
- /package/dist/vendored/{components → preview/components}/RenderTree.vue +0 -0
- /package/dist/vendored/{components → preview/components}/Root.vue +0 -0
- /package/dist/vendored/{components → preview/components}/Row.vue +0 -0
- /package/dist/vendored/{components → preview/components}/SvgCode.vue +0 -0
- /package/dist/vendored/{components → preview/components}/SvgSymbols.vue +0 -0
- /package/dist/vendored/{components → preview/components}/Text.vue +0 -0
- /package/dist/vendored/{components → preview/components}/Video.vue +0 -0
- /package/dist/vendored/{components → preview/components}/ViewContainer.vue +0 -0
- /package/dist/vendored/{components → preview/components}/base.js +0 -0
- /package/dist/vendored/{components → preview/components}/icons.js +0 -0
- /package/dist/vendored/{components → preview/components}/index.js +0 -0
- /package/dist/vendored/{meta → preview/meta}/component-meta.json +0 -0
- /package/dist/vendored/{meta → preview/meta}/directive-meta.json +0 -0
- /package/dist/vendored/{meta → preview/meta}/style-meta.json +0 -0
- /package/dist/vendored/{runtime → preview/runtime}/StyleParser.js +0 -0
- /package/dist/vendored/{runtime → preview/runtime}/adapter.js +0 -0
- /package/dist/vendored/{runtime → preview/runtime}/assets.js +0 -0
- /package/dist/vendored/{runtime → preview/runtime}/canvas-context.js +0 -0
- /package/dist/vendored/{runtime → preview/runtime}/card-runtime.js +0 -0
- /package/dist/vendored/{runtime → preview/runtime}/evaluate.js +0 -0
- /package/dist/vendored/{runtime → preview/runtime}/property.js +0 -0
- /package/dist/vendored/{runtime → preview/runtime}/selectionOverlay.js +0 -0
- /package/dist/vendored/{runtime → preview/runtime}/toPreview.js +0 -0
- /package/dist/{server-core → vendored/server/core}/assetProxy.js +0 -0
- /package/dist/{server-core → vendored/server/core}/bbox.js +0 -0
- /package/dist/{server-core → vendored/server/core}/buildDataFromSchema.js +0 -0
- /package/dist/{server-core → vendored/server/core}/cardFiles.js +0 -0
- /package/dist/{server-core → vendored/server/core}/cardHistory.js +0 -0
- /package/dist/{server-core → vendored/server/core}/cardImage.js +0 -0
- /package/dist/{server-core → vendored/server/core}/cardImportMeta.js +0 -0
- /package/dist/{server-core → vendored/server/core}/cardListCache.js +0 -0
- /package/dist/{server-core → vendored/server/core}/cardOpenFolder.js +0 -0
- /package/dist/{server-core → vendored/server/core}/cardSidecar.js +0 -0
- /package/dist/{server-core → vendored/server/core}/codeBridge.js +0 -0
- /package/dist/{server-core → vendored/server/core}/logStream.js +0 -0
- /package/dist/{server-core → vendored/server/core}/logger.js +0 -0
- /package/dist/{server-core → vendored/server/core}/nodeModels.js +0 -0
- /package/dist/{server-core → vendored/server/core}/renderStatus.js +0 -0
- /package/dist/{server-core → vendored/server/core}/screenshot.js +0 -0
- /package/dist/{server-core → vendored/server/core}/sliceStatic.js +0 -0
- /package/dist/vendored/{meta → skills/dsl-card-generator/vendor/preview/meta}/action-meta.json +0 -0
- /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
|
+
}
|