@xiaobuyu/nodesign 0.0.7 → 0.0.8
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 +266 -79
- package/README.zh-CN.md +254 -67
- package/package.json +2 -2
- package/server/api/admin.js +7 -0
- package/server/api/assets/docx-page.js +5 -4
- package/server/api/assets/helpers.js +8 -0
- package/server/api/assets.js +21 -33
- package/server/api/board.js +29 -1
- package/server/api/browse.js +3 -2
- package/server/api/chatai.js +9 -8
- package/server/api/exports/cards.js +7 -6
- package/server/api/exports/docx-pdf.js +4 -1
- package/server/api/local.js +10 -8
- package/server/api/projects.js +19 -3
- package/server/api/publish.js +3 -2
- package/server/api/sessions-rewind.js +220 -0
- package/server/api/sessions.js +58 -194
- package/server/api/stage.js +35 -0
- package/server/api/turn-model-switch.js +109 -0
- package/server/api/turn.js +52 -88
- package/server/auth/middleware.js +29 -6
- package/server/auth/tier.js +27 -7
- package/server/auth/users-store.js +14 -1
- package/server/engine/agent/actor-trail.js +110 -0
- package/server/engine/agent/agent-shared.js +22 -52
- package/server/engine/agent/beat-state.js +61 -0
- package/server/engine/agent/cast.js +322 -0
- package/server/engine/agent/events.js +9 -2
- package/server/engine/agent/hooks/beat-gate.js +73 -0
- package/server/engine/agent/hooks/lifecycle.js +6 -2
- package/server/engine/agent/hooks/post-trim.js +1 -0
- package/server/engine/agent/hooks/pre-board-dirty.js +29 -0
- package/server/engine/agent/hooks/pre-defaults.js +206 -2
- package/server/engine/agent/hooks/pre-injectors.js +19 -2
- package/server/engine/agent/hooks/pre-peer-guard.js +88 -0
- package/server/engine/agent/hooks/pre-unknown-params.js +82 -0
- package/server/engine/agent/hooks/pre-workspace-scope-guard.js +33 -0
- package/server/engine/agent/hooks/resident-role-lifecycle.js +100 -0
- package/server/engine/agent/hooks/slot-alias.js +30 -0
- package/server/engine/agent/hooks/user-prompt-submit.js +197 -7
- package/server/engine/agent/hooks.js +76 -6
- package/server/engine/agent/isolation.js +25 -2
- package/server/engine/agent/memory-config.js +27 -1
- package/server/engine/agent/model-context.js +102 -18
- package/server/engine/agent/model-table.js +361 -58
- package/server/engine/agent/prompts/nodesign-prelude.md +566 -59
- package/server/engine/agent/prompts/tools/direct-edit-protocol.md +1 -1
- package/server/engine/agent/prompts/tools/generate-image-cookbook.md +2 -2
- package/server/engine/agent/prompts/tools/paint-still-cookbook.md +1 -1
- package/server/engine/agent/prompts/tools/site-reference.md +2 -1
- package/server/engine/agent/role-card.js +175 -0
- package/server/engine/agent/rp-mode.js +17 -0
- package/server/engine/agent/session-loop.js +26 -14
- package/server/engine/agent/stage-status.js +73 -0
- package/server/engine/agent/subagent-flight.js +73 -0
- package/server/engine/agent/system-prompts.js +41 -0
- package/server/engine/agent/tool-input-stream.js +165 -0
- package/server/engine/mcp/actor.js +59 -0
- package/server/engine/mcp/capability-gate.js +21 -2
- package/server/engine/mcp/index.js +116 -24
- package/server/engine/mcp/mode-profile.js +139 -0
- package/server/engine/mcp/server-name.js +19 -0
- package/server/engine/mcp/tools/artifact-session.js +2 -1
- package/server/engine/mcp/tools/browse-find-batch.js +76 -14
- package/server/engine/mcp/tools/browse.js +1 -1
- package/server/engine/mcp/tools/cast-role.js +167 -0
- package/server/engine/mcp/tools/draw-trend.js +104 -0
- package/server/engine/mcp/tools/edit-board-schema.js +56 -0
- package/server/engine/mcp/tools/edit-board.js +569 -0
- package/server/engine/mcp/tools/look-at-board.js +4 -3
- package/server/engine/mcp/tools/open-sheet.js +242 -0
- package/server/engine/mcp/tools/organize-board.js +19 -5
- package/server/engine/mcp/tools/paint-still.js +1 -1
- package/server/engine/mcp/tools/pin-to-board.js +78 -3
- package/server/engine/mcp/tools/read-board.js +109 -16
- package/server/engine/mcp/tools/read-tavern-json.js +87 -19
- package/server/engine/mcp/tools/report-issue.js +1 -0
- package/server/engine/mcp/tools/role-memory.js +83 -0
- package/server/engine/mcp/tools/roll-dice.js +82 -0
- package/server/engine/mcp/tools/roll-film.js +1 -1
- package/server/engine/mcp/tools/screenshot.js +2 -2
- package/server/engine/mcp/tools/set-vars.js +150 -0
- package/server/engine/mcp/tools/sheet-replan.js +46 -0
- package/server/engine/mcp/tools/web-search.js +1 -1
- package/server/engine/mcp/tools/write-on-board-flow.js +108 -0
- package/server/engine/mcp/tools/write-on-board-place.js +211 -0
- package/server/engine/mcp/tools/write-on-board-role-anchor.js +48 -0
- package/server/engine/mcp/tools/write-on-board-schema.js +125 -0
- package/server/engine/mcp/tools/write-on-board.js +567 -107
- package/server/engine/plugins/nodesign/skills/blackboard-rp/SKILL.md +215 -0
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/SKILL.md +11 -3
- package/server/engine/plugins/nodesign/skills/docx-craft/SKILL.md +34 -4
- package/server/engine/plugins/nodesign/skills/site-craft/SKILL.md +20 -4
- package/server/engine/plugins/nodesign/skills/story-craft/SKILL.md +94 -0
- package/server/engine/plugins/nodesign/skills/story-import/SKILL.md +112 -0
- package/server/engine/plugins/nodesign/skills/story-intimacy/SKILL.md +72 -0
- package/server/engine/plugins/nodesign/skills/story-voice/SKILL.md +66 -0
- package/server/engine/plugins/nodesign/skills/story-voice/voices//345/206/267/347/241/254.md +33 -0
- package/server/engine/plugins/nodesign/skills/story-voice/voices//346/203/205/346/204/237/347/233/264/347/273/231.md +42 -0
- package/server/engine/plugins/nodesign/skills/story-voice/voices//346/204/217/350/257/206/346/265/201.md +42 -0
- package/server/engine/plugins/nodesign/skills/story-voice/voices//346/255/246/344/276/240/347/231/275/350/257/235.md +34 -0
- package/server/engine/plugins/nodesign/skills/story-voice/voices//347/224/237/346/264/273/346/265/201.md +35 -0
- package/server/engine/plugins/nodesign/skills/story-voice/voices//347/231/275/346/217/217.md +37 -0
- package/server/engine/plugins/nodesign/skills/story-voice/voices//350/210/236/345/217/260/345/211/247.md +32 -0
- package/server/engine/plugins/nodesign/skills/story-voice/voices//351/230/266/346/256/265/345/210/207/346/215/242.md +42 -0
- package/server/engine/runs/board-seater.js +272 -0
- package/server/engine/runs/board-tasklist.js +90 -48
- package/server/engine/runs/turn-relay.js +24 -2
- package/server/engine/runs/user-chalk-echo.js +44 -0
- package/server/index.js +2 -0
- package/server/lib/board-anchor.js +49 -0
- package/server/lib/board-dirty.js +93 -0
- package/server/lib/board-excerpt.js +80 -0
- package/server/lib/board-follow.js +111 -0
- package/server/lib/board-groups.js +1 -5
- package/server/lib/board-hero.js +3 -1
- package/server/lib/board-kind-sizes.js +36 -0
- package/server/lib/board-lanes.js +58 -0
- package/server/lib/board-obstacles.js +80 -0
- package/server/lib/board-relations.js +5 -1
- package/server/lib/board-sheets.js +467 -0
- package/server/lib/board-shelf.js +86 -0
- package/server/lib/board-transform.js +62 -0
- package/server/lib/canvas-id.js +18 -1
- package/server/lib/chalk-flow.js +88 -0
- package/server/lib/chalk-rewrite.js +63 -0
- package/server/lib/chalk-size-pref.js +48 -0
- package/server/lib/chalk.js +42 -3
- package/server/lib/ingress/forward-openai-chat.js +16 -4
- package/server/lib/ingress/openai-chat.js +23 -12
- package/server/lib/ingress/session-routes.js +3 -3
- package/server/lib/ingress/stream-watchdog.js +53 -0
- package/server/lib/ingress/upstream-billing.js +15 -0
- package/server/lib/ingress/upstream-error-hints.js +36 -0
- package/server/lib/json-preview.js +58 -0
- package/server/lib/model-ingress.js +26 -1
- package/server/lib/moderation.js +10 -5
- package/server/lib/quota.js +26 -6
- package/server/lib/rect.js +50 -0
- package/server/lib/screen.js +36 -0
- package/server/lib/sheet-state.js +19 -0
- package/server/lib/sketch-array.js +118 -0
- package/server/lib/sketch-hatch.js +99 -0
- package/server/lib/sketch-layout.js +269 -69
- package/server/lib/sketch-shapes.js +152 -0
- package/server/lib/sketch-stencils.js +170 -0
- package/server/lib/state-table.js +237 -0
- package/server/lib/state-trend.js +94 -0
- package/server/lib/state-triggers.js +199 -0
- package/server/lib/tavern-json.js +48 -0
- package/server/lib/user-uploads.js +88 -0
- package/server/projects/board-limits.js +5 -3
- package/server/projects/board-sanitize.js +220 -10
- package/server/projects/board-store.js +95 -6
- package/server/projects/session-jsonl.js +110 -0
- package/server/projects/store.js +56 -3
- package/server/projects/viewpoint-store.js +35 -3
- package/server/projects/workspace-slots.js +30 -0
- package/server/projects/workspace-templates.js +29 -0
- package/server/projects/workspace.js +16 -17
- package/server/runtime/capabilities.js +6 -2
- package/server/shared/locales.js +70 -0
- package/server/shared/messages-en.js +69 -0
- package/server/shared/messages.js +65 -0
- package/server/ws/broker.js +3 -0
- package/server/ws/index.js +43 -3
- package/web/README.md +1 -1
- package/web/dist/assets/{BrowserWindow-BoOBaIyz.js → BrowserWindow-CcaGRWz5.js} +1 -1
- package/web/dist/assets/DeckWindow-BIr2KGFT.js +6 -0
- package/web/dist/assets/{DocxWindow-B_u6aX9o.js → DocxWindow-B9KZmU5z.js} +2 -2
- package/web/dist/assets/{MermaidBlock-BO-46Du5.js → MermaidBlock-DrfRnz8E.js} +43 -43
- package/web/dist/assets/{SiteWindow-ConPmzdM.js → SiteWindow-CJOjCF7d.js} +6 -6
- package/web/dist/assets/{abnfDiagram-VCTEODGH-FCO9yf-n.js → abnfDiagram-VCTEODGH-CUZbQGIg.js} +1 -1
- package/web/dist/assets/{arc-BLcEOSRX.js → arc-C1-HSUny.js} +1 -1
- package/web/dist/assets/{architectureDiagram-5GKGNRK7-kxiX4TeN.js → architectureDiagram-5GKGNRK7-B5nwvu60.js} +2 -2
- package/web/dist/assets/{blockDiagram-NRAW4CY4-CptFksix.js → blockDiagram-NRAW4CY4-uPPuFLwR.js} +1 -1
- package/web/dist/assets/{c4Diagram-UCG6FXSJ-Dxmxj34F.js → c4Diagram-UCG6FXSJ-C0mKBFlz.js} +1 -1
- package/web/dist/assets/channel-BxpWNOGU.js +1 -0
- package/web/dist/assets/{chunk-2Q5K7J3B-D9inx5oo.js → chunk-2Q5K7J3B-Cs0rlZ4q.js} +1 -1
- package/web/dist/assets/{chunk-5VM5RSS4-DydfHlEc.js → chunk-5VM5RSS4-DtR63oYb.js} +1 -1
- package/web/dist/assets/{chunk-F27PBJKO-BO7GxhSv.js → chunk-F27PBJKO-B3MRdezJ.js} +1 -1
- package/web/dist/assets/{chunk-G27WJ6UU-BzP6YiQ_.js → chunk-G27WJ6UU-BtwntwpS.js} +1 -1
- package/web/dist/assets/{chunk-JWPE2WC7-CHcgcd0s.js → chunk-JWPE2WC7-BHsTe5PD.js} +1 -1
- package/web/dist/assets/{chunk-LCL6LL3I-D2Wg5JtX.js → chunk-LCL6LL3I-knY15Qdm.js} +1 -1
- package/web/dist/assets/{chunk-POPQ4Y6H-B2fCRfpM.js → chunk-POPQ4Y6H-C51tgK_I.js} +1 -1
- package/web/dist/assets/{chunk-SVP7TREG-JSLcsRlZ.js → chunk-SVP7TREG-CnwRZmF_.js} +1 -1
- package/web/dist/assets/{chunk-XXDRQBXY-BCjvaeHm.js → chunk-XXDRQBXY-BtXzVF-_.js} +1 -1
- package/web/dist/assets/classDiagram-DTDB5LWJ-DUEKmDfo.js +1 -0
- package/web/dist/assets/classDiagram-v2-JRS7N3AN-DUEKmDfo.js +1 -0
- package/web/dist/assets/{cose-bilkent-JH36ORCC-0uKv6oJ7.js → cose-bilkent-JH36ORCC-CzWlGP6i.js} +1 -1
- package/web/dist/assets/{cynefin-OW5HDTMX-Bvt-gEmA.js → cynefin-OW5HDTMX-vK64EGSv.js} +1 -1
- package/web/dist/assets/{cynefinDiagram-5FMLGOSQ-C9Ht02qC.js → cynefinDiagram-5FMLGOSQ-Bs3g1Gvh.js} +1 -1
- package/web/dist/assets/{dagre-3AP2YEHR-BfxZny4s.js → dagre-3AP2YEHR-BhgkYe8V.js} +1 -1
- package/web/dist/assets/{diagram-S7CK7UJ4-CrE0ZETE.js → diagram-S7CK7UJ4-C_6mvD2V.js} +1 -1
- package/web/dist/assets/{diagram-UQ7AKVKN-BO8VQ4Fx.js → diagram-UQ7AKVKN-qW7tssUW.js} +1 -1
- package/web/dist/assets/{diagram-VSXAHHWV-CHUZnR9F.js → diagram-VSXAHHWV-txJPHpvG.js} +1 -1
- package/web/dist/assets/{diagram-VX7I27RA-DYQAz1Vu.js → diagram-VX7I27RA-CQTMsCnL.js} +1 -1
- package/web/dist/assets/{diagram-Z3DM3KII-BDjivgLr.js → diagram-Z3DM3KII--cb1hoE6.js} +1 -1
- package/web/dist/assets/{ebnfDiagram-PWID7BFC-Bpe_QVbH.js → ebnfDiagram-PWID7BFC-Da6lDU77.js} +1 -1
- package/web/dist/assets/{erDiagram-SSCWMZ5O-BxhwuAu1.js → erDiagram-SSCWMZ5O-DGCMrsQn.js} +1 -1
- package/web/dist/assets/{flowDiagram-A5DVABFB-CCw2H08-.js → flowDiagram-A5DVABFB-GdOL_qno.js} +1 -1
- package/web/dist/assets/{ganttDiagram-EL5Y4UJY-DtTc7MyI.js → ganttDiagram-EL5Y4UJY-Cx1F_jBK.js} +4 -4
- package/web/dist/assets/{gitGraphDiagram-WWUBYQGX-BvORxMd2.js → gitGraphDiagram-WWUBYQGX-B5_6JhR3.js} +1 -1
- package/web/dist/assets/hand-CuvZZQGW.webp +0 -0
- package/web/dist/assets/index-BYKfMnmv.js +2759 -0
- package/web/dist/assets/index-CW7VsrVg.css +1 -0
- package/web/dist/assets/{infoDiagram-RXCK75RN-DMcrkypD.js → infoDiagram-RXCK75RN-CjvKpSPA.js} +1 -1
- package/web/dist/assets/{ishikawaDiagram-5VMMS53U-CZkTIbe-.js → ishikawaDiagram-5VMMS53U-CxLoAPhx.js} +1 -1
- package/web/dist/assets/{journeyDiagram-EYS64GPL-DR0XazdE.js → journeyDiagram-EYS64GPL-BfvS1upJ.js} +1 -1
- package/web/dist/assets/{kanban-definition-3QL26DDD-DN8VbwVu.js → kanban-definition-3QL26DDD-DKPPu0w4.js} +1 -1
- package/web/dist/assets/{layout-9_paCbyt.js → layout-Dhgck-Zo.js} +1 -1
- package/web/dist/assets/{linear-g6Ct012e.js → linear-DMSQ-aUw.js} +1 -1
- package/web/dist/assets/lxgw-nd-app-bold-D1pKA1bR.woff2 +0 -0
- package/web/dist/assets/lxgw-nd-app-regular-BDe3d26v.woff2 +0 -0
- package/web/dist/assets/lxgw-nd-bold-Bufvlg03.woff2 +0 -0
- package/web/dist/assets/lxgw-nd-regular-DQwoI4OP.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-100-DLPe7tEt.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-101-d3VsZKWT.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-102-ClzJuLq6.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-103-DuDnQ6K-.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-104-kPGW3Eem.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-105-BzD54h-0.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-106-BQ7fIzlx.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-107-D8tDfFHI.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-108-tGhkpwep.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-109-7SMVSrly.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-110-_uDKrFed.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-111-Bio825RE.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-112-D2ee0i-C.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-113-JPFVDrga.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-114-BcfxYA4w.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-115-B1HeeOsd.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-116-hY12tXpq.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-117-BvuZwmre.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-118-tzvs_H7j.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-119-J2DWuYDN.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-21-pDubCJV5.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-22-ChAPMpqp.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-23-BBx3oNpT.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-24-CYt-Oagk.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-25-Cx3j-ZUv.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-26-B9AfZ3LL.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-27-Bh2s9MNE.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-28-C02JPoKP.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-29-T_tbUGhk.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-30-DMfyNzhz.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-31-DFfdzbkg.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-32-Bs1fz2iW.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-33-cpB96OkZ.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-34-BhinPYWX.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-35-D_njRQqA.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-36-DqXlLE3t.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-37-j5LbvMph.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-38-oO4jWdKm.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-39-CabsX4rw.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-4-PatS3YHh.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-40-yrLg3C3e.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-41-DBXmCQhc.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-42-Bqij_UiQ.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-43-D3kp0Ntc.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-44-B_BNGvK4.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-45-CZc0cQWs.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-46-CHQX5Kd9.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-47-Ci8gLrHA.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-48-run3ugD3.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-49-DgfiUb6u.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-5-B0CtGbX_.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-50-CdCG-POr.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-51-L2okeQZ2.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-52-BZAqyGJW.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-53-B0xWqLeU.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-54-BZVQX-EA.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-55-093mkIUJ.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-56-CbhWG8G-.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-57-pLQuvo6c.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-58-BHhoY50x.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-59-D1_6DuIQ.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-6-BnAelmYU.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-60-HDBIuLTZ.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-61-C-c13SeU.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-62-kHRVOhZ2.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-63-C0MCsvFv.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-64-DZekguCj.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-65-CtP6c3IU.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-66-DLVWoMSf.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-67-BVuOnayH.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-68-DpxQ6Mnx.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-69-qesBxLmC.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-70-BBt1xN7h.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-71-BWqZacwB.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-72-B1wP_Yst.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-73-C99T7jNZ.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-74-DrUTsWBe.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-75-C4M5XXQe.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-76-Bq8spVWF.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-77-D9vNupE2.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-78-D6th7Gnu.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-79-bezM85cS.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-80-DD8pnhG2.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-81-N6jtSyBs.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-82-D6c180bZ.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-83-em17AadM.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-84-CJ8gdrMK.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-85-9PPGO1eB.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-86-CjWPf-u5.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-87-8D_X_yOC.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-88-9vtKFPBx.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-89-BhE5jAKX.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-90-D2Npfm5E.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-91-C6Ft1-Eu.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-97-CaExyQP9.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-98-CPoVbsUd.woff2 +0 -0
- package/web/dist/assets/lxgwwenkaiscreen-subset-99-ZUQZGVgH.woff2 +0 -0
- package/web/dist/assets/{mindmap-definition-FBJOCRG2-BGt4omYC.js → mindmap-definition-FBJOCRG2-CalfL_Kn.js} +1 -1
- package/web/dist/assets/{pegDiagram-XKGWAZYB-uSfS-PBE.js → pegDiagram-XKGWAZYB-D9mVWz7l.js} +1 -1
- package/web/dist/assets/pending-edit-apply-kwMFWsUe.js +31 -0
- package/web/dist/assets/{pieDiagram-E7YTZNPT-DpeI935H.js → pieDiagram-E7YTZNPT-DELzbZNS.js} +1 -1
- package/web/dist/assets/{quadrantDiagram-AXDQQJYC-B7IuNvWa.js → quadrantDiagram-AXDQQJYC-TZqp4mfA.js} +1 -1
- package/web/dist/assets/{railroadDiagram-O6MQD6OU-ARzYpPQI.js → railroadDiagram-O6MQD6OU-DxX59f0i.js} +1 -1
- package/web/dist/assets/{requirementDiagram-EFPCY7ZU-Cj4upkAo.js → requirementDiagram-EFPCY7ZU-DFdsJLIe.js} +1 -1
- package/web/dist/assets/{rotate-cw-CXJcyHWk.js → rotate-cw-62liz7LX.js} +1 -1
- package/web/dist/assets/{sankeyDiagram-P5KCCOFB-OaVwSnLP.js → sankeyDiagram-P5KCCOFB-ByYcd3fG.js} +1 -1
- package/web/dist/assets/{sequenceDiagram-WJ2MYXX4-1yrUbq4t.js → sequenceDiagram-WJ2MYXX4-Bj-ml1jS.js} +1 -1
- package/web/dist/assets/{sizeCapture-X5ZJPWSS-nYH5p9D6.js → sizeCapture-X5ZJPWSS-KhsXThyD.js} +1 -1
- package/web/dist/assets/{square-dashed-mouse-pointer-CTtTVKoH.js → square-dashed-mouse-pointer-D_VrqVpy.js} +1 -1
- package/web/dist/assets/{stateDiagram-HBIQ2CUA-Dj5HvyNq.js → stateDiagram-HBIQ2CUA-BJlpHfWU.js} +1 -1
- package/web/dist/assets/stateDiagram-v2-4QOOHH4V-CR2kYAH2.js +1 -0
- package/web/dist/assets/{swimlanes-XN3QIQJK-umeT0HpB.js → swimlanes-XN3QIQJK-DUB9OJVs.js} +1 -1
- package/web/dist/assets/swimlanesDiagram-VK2B7HYN-BitOxnG2.js +8 -0
- package/web/dist/assets/{timeline-definition-24CTP7MA-MqnQfHKB.js → timeline-definition-24CTP7MA-C03NX82A.js} +1 -1
- package/web/dist/assets/{vennDiagram-4TSXK5OY-b2liyZra.js → vennDiagram-4TSXK5OY-B9pjzngp.js} +1 -1
- package/web/dist/assets/{wardleyDiagram-VM6X3IG4-QMK-zNh1.js → wardleyDiagram-VM6X3IG4-UqWq_mcT.js} +1 -1
- package/web/dist/assets/{xychartDiagram-S5SC5T6Z-D5Mc6tfA.js → xychartDiagram-S5SC5T6Z-D2pDKtOv.js} +1 -1
- package/web/dist/index.html +2 -2
- package/server/engine/mcp/tools/arrange-on-board.js +0 -126
- package/server/engine/mcp/tools/create-on-board.js +0 -152
- package/server/engine/mcp/tools/edit-sketch.js +0 -167
- package/server/engine/mcp/tools/relate-on-board.js +0 -150
- package/server/engine/mcp/tools/sketch-on-board.js +0 -345
- package/server/engine/plugins/nodesign/skills/rp-craft/SKILL.md +0 -235
- package/server/engine/plugins/nodesign/skills/rp-craft/patterns//346/274/224/345/207/272/351/241/265-/346/234/200/345/260/217/345/256/236/347/216/260.html +0 -78
- package/server/engine/plugins/nodesign/skills/rp-craft//346/274/224/345/207/272.template.js +0 -268
- package/web/dist/assets/DeckWindow-Bg3rgkvi.js +0 -11
- package/web/dist/assets/channel-DFyiuGL0.js +0 -1
- package/web/dist/assets/classDiagram-DTDB5LWJ-Aw1f8z-b.js +0 -1
- package/web/dist/assets/classDiagram-v2-JRS7N3AN-Aw1f8z-b.js +0 -1
- package/web/dist/assets/index-C0KUf6Wd.css +0 -1
- package/web/dist/assets/index-DGG5wIZs.js +0 -2118
- package/web/dist/assets/lxgw-nd-bold-DbRfiTgo.woff2 +0 -0
- package/web/dist/assets/lxgw-nd-regular-CpMw9Ehg.woff2 +0 -0
- package/web/dist/assets/pending-edit-apply-uP4uvrOD.js +0 -31
- package/web/dist/assets/stateDiagram-v2-4QOOHH4V-kJ1m_zJT.js +0 -1
- package/web/dist/assets/swimlanesDiagram-VK2B7HYN-DPSkMbbl.js +0 -8
|
@@ -14,19 +14,24 @@
|
|
|
14
14
|
* 全是纯函数;尺寸估算与 create-on-board 同一套公式。
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
import { UNIT, overlaps, bboxOf as rectBbox } from './rect.js';
|
|
18
|
+
import { ZOOM_BASIS, ONE_SCREEN } from './screen.js';
|
|
19
|
+
export { UNIT }; // 兼容出口(真身在 rect.js)
|
|
18
20
|
/**
|
|
19
|
-
* 可读性规范(2026-08-23
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
21
|
+
* 可读性规范(2026-08-23,用户定;2026-08-29 纸范式改 0.75 基准):黑板上的字要在
|
|
22
|
+
* 75%~100% 缩放下清晰可读。手写/md 正文 16px 世界像素在 0.75 倍下是 12 屏幕像素 ——
|
|
23
|
+
* 这是底线,所以节点字号不低于 md;一张图的尺寸要能在 0.75 倍下整张进一个普通视口。
|
|
24
|
+
* 超过 SKETCH_MAX 直接拒:一张图说一件事,大了就拆。
|
|
23
25
|
*/
|
|
24
|
-
export const SKETCH_FIT =
|
|
26
|
+
export const SKETCH_FIT = ONE_SCREEN; // 推荐上限 = 一屏(唯一基准在 lib/screen.js)
|
|
25
27
|
export const SKETCH_MAX = { w: 2600, h: 1700 }; // 硬上限(再大就拆成两张)
|
|
26
28
|
export const GAP = 16; // 模板排布的节点间距
|
|
27
29
|
const SIZE_PX = { sm: 13, md: 16, lg: 22, xl: 30 };
|
|
28
30
|
|
|
29
|
-
/**
|
|
31
|
+
/** 字宽 em 估(CJK/全角 1em,其余 0.62em)—— plain/md 两分支共用 */
|
|
32
|
+
const em2 = (l) => [...l].reduce((n, c) => n + (/[ -鿿-]/.test(c) ? 1 : 0.62), 0);
|
|
33
|
+
|
|
34
|
+
/** 手写字身位估算(与前端 text-fonts.js estimateTextBox 同公式,parity 测试钉着) */
|
|
30
35
|
export function textBox(t, sizeKey, { md = false, wUnits = null } = {}) {
|
|
31
36
|
const px = SIZE_PX[sizeKey] || 16;
|
|
32
37
|
if (md) {
|
|
@@ -52,11 +57,42 @@ export function textBox(t, sizeKey, { md = false, wUnits = null } = {}) {
|
|
|
52
57
|
}
|
|
53
58
|
return { w, h: Math.round(n * px * 1.6) + 8 + paras * 8 };
|
|
54
59
|
}
|
|
55
|
-
|
|
56
|
-
|
|
60
|
+
// plain 也按 em 估(2026-08-29 刀 3:原来按 t.length,CJK 全错 —— 与前端
|
|
61
|
+
// text-fonts.js estimateTextBox 同公式,parity 测试钉着,改一边看另一边)
|
|
62
|
+
const rows = String(t).split('\n');
|
|
63
|
+
const longest0 = Math.min(26, Math.max(4, ...rows.map(em2)));
|
|
64
|
+
const cols = wUnits ? Math.max(4, (wUnits * UNIT) / (px * 1.05)) : longest0;
|
|
65
|
+
const lines = rows.reduce((n, l) => n + Math.max(1, Math.ceil(em2(l) / cols)), 0);
|
|
57
66
|
return { w: wUnits ? wUnits * UNIT : Math.round(cols * px * 1.05) + 12, h: Math.round(lines * px * 1.6) + 10 };
|
|
58
67
|
}
|
|
59
68
|
|
|
69
|
+
/**
|
|
70
|
+
* 一块地方大概能写多少字(2026-08-29 占位契约刀 D,站主点名「给 agent 一个
|
|
71
|
+
* 每个中英文字符占多大的参考」)。
|
|
72
|
+
*
|
|
73
|
+
* 公式就是 textBox 那一套的逆用,不是另立一把尺:CJK 记 1em、拉丁记 0.62em,
|
|
74
|
+
* 行高 = 字号×1.6。板书渲染字号恒定 16px(BoardObject 写死,不读 size 参数),
|
|
75
|
+
* 所以这里也钉 16。
|
|
76
|
+
*
|
|
77
|
+
* 为什么要有它:在这之前给 agent 的护栏**全是像素**("硬上限 2600×1700"、
|
|
78
|
+
* "剩 ~412px 高"),而 agent 手里的东西是字。让它自己把字数换算成像素,等于
|
|
79
|
+
* 每次落笔前做一道它做不准的算术 —— 结果就是写完才发现装不下。
|
|
80
|
+
*
|
|
81
|
+
* @param {number} w 可写宽度(像素)
|
|
82
|
+
* @param {number} h 可写高度(像素)
|
|
83
|
+
* @returns {{lines:number, cjk:number, latin:number, perLine:number}}
|
|
84
|
+
* lines=能排几行,perLine=一行几个汉字,cjk/latin=总字数(纯中文 / 纯拉丁)
|
|
85
|
+
*/
|
|
86
|
+
/** 一条板书的默认版心宽(18 格)—— 自动宽度那三档的上档,也是 prelude 教的列宽 */
|
|
87
|
+
export const DEFAULT_CHALK_W = 18 * UNIT;
|
|
88
|
+
|
|
89
|
+
export function capacityOf(w, h) {
|
|
90
|
+
const px = 16;
|
|
91
|
+
const perLine = Math.max(1, Math.floor((w - 12) / px));
|
|
92
|
+
const lines = Math.max(0, Math.floor((h - 8) / (px * 1.6)));
|
|
93
|
+
return { lines, perLine, cjk: lines * perLine, latin: Math.round(lines * perLine / 0.62) };
|
|
94
|
+
}
|
|
95
|
+
|
|
60
96
|
/* ── 确定性随机(与前端 board-bindings 同款 FNV + mulberry32)── */
|
|
61
97
|
function hashSeed(s) {
|
|
62
98
|
let h = 0x811c9dc5;
|
|
@@ -145,16 +181,146 @@ export function shapePath(kind, { w = 0, h = 0, to = null, d = null } = {}, seed
|
|
|
145
181
|
return null;
|
|
146
182
|
}
|
|
147
183
|
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* 自由 path 的手绘化(2026-08-30 画图探针案):别的图元都过 wobblySeg,唯独
|
|
187
|
+
* path 原文直通 —— 同一幅画里一半抖一半死直,风格劈叉。这里把 **L 段**换成
|
|
188
|
+
* 微抖折线(M/Q/C 原样保留:曲线本身已经"活",再抖是画蛇添足;Z 换成抖回
|
|
189
|
+
* 起点的显式线段,SVG 的 Z 是死直线)。端点一个不动 —— agent 的几何意图神圣。
|
|
190
|
+
*
|
|
191
|
+
* @param {string} dIn 已换算成像素的绝对坐标 path(大写 M/L/Q/C/Z)
|
|
192
|
+
*/
|
|
193
|
+
export function roughFreePath(dIn, seed = 'freepath') {
|
|
194
|
+
const rand = rng(hashSeed(seed));
|
|
195
|
+
const tokens = String(dIn).match(/[MLQCZ]|-?\d*\.?\d+(?:[eE][-+]?\d+)?/g) || [];
|
|
196
|
+
let out = ''; let i = 0;
|
|
197
|
+
let cur = null; let start = null;
|
|
198
|
+
const num = () => Number(tokens[i++]);
|
|
199
|
+
while (i < tokens.length) {
|
|
200
|
+
const t = tokens[i++];
|
|
201
|
+
if (t === 'M') { cur = { x: num(), y: num() }; start = { ...cur }; out += `M ${f1(cur.x)} ${f1(cur.y)}`; continue; }
|
|
202
|
+
if (t === 'L') {
|
|
203
|
+
const p = { x: num(), y: num() };
|
|
204
|
+
out += cur ? wobblySeg(rand, cur, p) : ` L ${f1(p.x)} ${f1(p.y)}`;
|
|
205
|
+
cur = p; continue;
|
|
206
|
+
}
|
|
207
|
+
if (t === 'Q') { const c = { x: num(), y: num() }; const p = { x: num(), y: num() }; out += ` Q ${f1(c.x)} ${f1(c.y)} ${f1(p.x)} ${f1(p.y)}`; cur = p; continue; }
|
|
208
|
+
if (t === 'C') { const a = { x: num(), y: num() }; const b = { x: num(), y: num() }; const p = { x: num(), y: num() }; out += ` C ${f1(a.x)} ${f1(a.y)} ${f1(b.x)} ${f1(b.y)} ${f1(p.x)} ${f1(p.y)}`; cur = p; continue; }
|
|
209
|
+
if (t === 'Z') { if (cur && start) { out += wobblySeg(rand, cur, start); cur = { ...start }; } continue; }
|
|
210
|
+
}
|
|
211
|
+
return out.trim();
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* 逐坐标对变换一条 path(大写绝对 M/L/Q/C/Z)。旋转/镜像/缩放全走它 ——
|
|
216
|
+
* 数字成对喂给 fn,命令字原样保留。
|
|
217
|
+
*/
|
|
218
|
+
export function transformD(d, fn) {
|
|
219
|
+
const tokens = String(d).match(/[MLQCZ]|-?\d*\.?\d+(?:[eE][-+]?\d+)?/g) || [];
|
|
220
|
+
const out = [];
|
|
221
|
+
let i = 0;
|
|
222
|
+
while (i < tokens.length) {
|
|
223
|
+
const t = tokens[i];
|
|
224
|
+
if (/^[MLQCZ]$/.test(t)) { out.push(t); i += 1; continue; }
|
|
225
|
+
const p = fn({ x: Number(tokens[i]), y: Number(tokens[i + 1]) });
|
|
226
|
+
i += 2;
|
|
227
|
+
out.push(f1(p.x), f1(p.y));
|
|
228
|
+
}
|
|
229
|
+
return out.join(' ');
|
|
230
|
+
}
|
|
231
|
+
|
|
148
232
|
/**
|
|
149
233
|
* 模板排布。nodes: [{ key, w, h, at?: {x,y}(网格) }] → Map key → {x,y}(局部像素)
|
|
150
234
|
* - free:有 at 的按网格落,没 at 的排在 free 区域下面一列
|
|
151
235
|
* - column / row / grid(cols) / mindmap(第一个是中心,其余环绕)
|
|
152
236
|
*/
|
|
153
|
-
|
|
237
|
+
/**
|
|
238
|
+
* 模板解析(08-27 抽出,write-on-board 与 layoutNodes 共用一份 auto 规则)。
|
|
239
|
+
* ⭐ auto 现在**认线**:图内边 ≥1 且节点 ≥3 → flow(按结构分层)。在这之前
|
|
240
|
+
* 布局引擎对 edges 全盲 —— 给了线也按 column/grid 堆,「摊一堆字」的机器根源。
|
|
241
|
+
*/
|
|
242
|
+
export function resolveTemplate(nodes, { template = 'auto', edges = [], column = false } = {}) {
|
|
243
|
+
/**
|
|
244
|
+
* 车道约束(2026-08-28 移动端第二轮):手机/平板一律竖排一列,**连模型点名的
|
|
245
|
+
* 模板也盖掉**。
|
|
246
|
+
*
|
|
247
|
+
* ⭐ 这条是真会话逼出来的。模型收到「一件 ≤342 宽」之后**试了三次**:第一版
|
|
248
|
+
* flow 排成 549 宽被警告 → 它改传 `w:14` 想收窄节点,结果整体变成 **576,更宽**
|
|
249
|
+
* (节点是窄了,但 flow 把它们横着摊成一排)→ 只好 erase_group 重来、再手工
|
|
250
|
+
* 一件件 move 成一列。擦两次、多花七八个工具调用,才得到本该一次给它的东西。
|
|
251
|
+
*
|
|
252
|
+
* ⛔ 病根不是「模型不配合」,是**它够着的那根杠杆是坏的**:唯一能改宽度的入参
|
|
253
|
+
* 改的是单节点,而决定整体宽度的是布局模板,那个它没法按屏幕挑。所以约束下沉
|
|
254
|
+
* 到这儿 —— 布局引擎知道屏幕多宽,模型不需要知道。
|
|
255
|
+
*/
|
|
256
|
+
if (column) return 'column';
|
|
257
|
+
if (template !== 'auto') return template;
|
|
258
|
+
if (nodes.some(n => n.at)) return 'free';
|
|
259
|
+
const keys = new Set(nodes.map(n => n.key));
|
|
260
|
+
const inner = edges.filter(e => keys.has(e.from) && keys.has(e.to) && e.from !== e.to);
|
|
261
|
+
if (inner.length && nodes.length >= 3) return 'flow';
|
|
262
|
+
return nodes.length <= 4 ? 'column' : 'grid';
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** 分层流式(Sugiyama 简版):层号=最长入路径(模型画的图可能带环,深度截断兜住),
|
|
266
|
+
* 层内按父级平均横位排(减少交叉),每层居中。线因此顺着重力方向读。 */
|
|
267
|
+
function layoutFlow(nodes, edges, pull = new Map()) {
|
|
268
|
+
const pos = new Map();
|
|
269
|
+
const idx = new Map(nodes.map((n, i) => [n.key, i]));
|
|
270
|
+
const level = new Map();
|
|
271
|
+
const depth = (k, seen) => {
|
|
272
|
+
if (level.has(k)) return level.get(k);
|
|
273
|
+
if (seen.has(k)) return 0; // 环:断在回边上
|
|
274
|
+
seen.add(k);
|
|
275
|
+
let best = 0;
|
|
276
|
+
for (const e of edges) if (e.to === k) best = Math.max(best, depth(e.from, seen) + 1);
|
|
277
|
+
seen.delete(k);
|
|
278
|
+
level.set(k, best);
|
|
279
|
+
return best;
|
|
280
|
+
};
|
|
281
|
+
for (const n of nodes) depth(n.key, new Set());
|
|
282
|
+
const layers = [];
|
|
283
|
+
for (const n of nodes) { const l = level.get(n.key) || 0; (layers[l] ||= []).push(n); }
|
|
284
|
+
const centerX = new Map();
|
|
285
|
+
let y = 0;
|
|
286
|
+
for (const layer of layers) {
|
|
287
|
+
if (!layer) continue;
|
|
288
|
+
const parentAvg = (k) => {
|
|
289
|
+
const ps = edges.filter(e => e.to === k && centerX.has(e.from)).map(e => centerX.get(e.from));
|
|
290
|
+
return ps.length ? ps.reduce((a, b) => a + b, 0) / ps.length : idx.get(k) * 10;
|
|
291
|
+
};
|
|
292
|
+
// 节点级拉力(08-27 产物锚 v2):连着外部产物的节点排向产物那一侧。
|
|
293
|
+
// 外部锚是世界坐标、parentAvg 是局部坐标,不同尺度 —— 各自归一到 [0,1] 再比。
|
|
294
|
+
const norm = (vals) => {
|
|
295
|
+
const lo = Math.min(...vals); const hi = Math.max(...vals);
|
|
296
|
+
return (v) => (hi > lo ? (v - lo) / (hi - lo) : 0.5);
|
|
297
|
+
};
|
|
298
|
+
const pulled = layer.filter((n) => pull.has(n.key));
|
|
299
|
+
const nExt = pulled.length ? norm(pulled.map((n) => pull.get(n.key).x)) : null;
|
|
300
|
+
const paVals = layer.filter((n) => !pull.has(n.key)).map((n) => parentAvg(n.key));
|
|
301
|
+
const nPa = paVals.length ? norm(paVals) : null;
|
|
302
|
+
const score = (n) => (pull.has(n.key) ? nExt(pull.get(n.key).x) : (nPa ? nPa(parentAvg(n.key)) : 0.5));
|
|
303
|
+
layer.sort((a, b) => (score(a) - score(b)) || (idx.get(a.key) - idx.get(b.key)));
|
|
304
|
+
const totalW = layer.reduce((s, n) => s + n.w, 0) + (layer.length - 1) * (GAP * 2 + 8);
|
|
305
|
+
let x = Math.round(-totalW / 2);
|
|
306
|
+
let rowH = 0;
|
|
307
|
+
for (const n of layer) {
|
|
308
|
+
pos.set(n.key, { x, y });
|
|
309
|
+
centerX.set(n.key, x + n.w / 2);
|
|
310
|
+
x += n.w + GAP * 2 + 8; rowH = Math.max(rowH, n.h);
|
|
311
|
+
}
|
|
312
|
+
y += rowH + GAP * 3; // 层间留出画箭头的呼吸
|
|
313
|
+
}
|
|
314
|
+
return pos;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
export function layoutNodes(nodes, { template = 'auto', cols = null, edges = [], pull = new Map(), pullOrigin = null } = {}) {
|
|
154
318
|
const pos = new Map();
|
|
155
319
|
if (!nodes.length) return pos;
|
|
156
|
-
|
|
157
|
-
|
|
320
|
+
const keys = new Set(nodes.map(n => n.key));
|
|
321
|
+
const inner = edges.filter(e => keys.has(e.from) && keys.has(e.to) && e.from !== e.to);
|
|
322
|
+
const tpl = resolveTemplate(nodes, { template, edges });
|
|
323
|
+
if (tpl === 'flow') return layoutFlow(nodes, inner, pull);
|
|
158
324
|
|
|
159
325
|
if (tpl === 'free') {
|
|
160
326
|
let bottom = 0;
|
|
@@ -180,7 +346,58 @@ export function layoutNodes(nodes, { template = 'auto', cols = null } = {}) {
|
|
|
180
346
|
return pos;
|
|
181
347
|
}
|
|
182
348
|
if (tpl === 'mindmap') {
|
|
183
|
-
|
|
349
|
+
// 枢纽按度数选、环序按 BFS 排(08-27):在这之前中心=第一个节点、环上按入参
|
|
350
|
+
// 顺序 —— 结构全被扔掉,同父的孩子散在环两端,线横穿整张图
|
|
351
|
+
let order = nodes;
|
|
352
|
+
if (inner.length) {
|
|
353
|
+
const deg = new Map(nodes.map(n => [n.key, 0]));
|
|
354
|
+
const adj = new Map(nodes.map(n => [n.key, []]));
|
|
355
|
+
for (const e of inner) {
|
|
356
|
+
deg.set(e.from, deg.get(e.from) + 1); deg.set(e.to, deg.get(e.to) + 1);
|
|
357
|
+
adj.get(e.from).push(e.to); adj.get(e.to).push(e.from);
|
|
358
|
+
}
|
|
359
|
+
const hub = [...nodes].sort((a, b) => deg.get(b.key) - deg.get(a.key))[0];
|
|
360
|
+
const seen = new Set([hub.key]); const q = [hub.key]; const bfs = [];
|
|
361
|
+
while (q.length) {
|
|
362
|
+
const k = q.shift();
|
|
363
|
+
for (const nk of adj.get(k) || []) if (!seen.has(nk)) { seen.add(nk); bfs.push(nk); q.push(nk); }
|
|
364
|
+
}
|
|
365
|
+
const byKey = new Map(nodes.map(n => [n.key, n]));
|
|
366
|
+
order = [hub, ...bfs.map(k => byKey.get(k)), ...nodes.filter(n => !seen.has(n.key))];
|
|
367
|
+
}
|
|
368
|
+
let [center, ...rest] = order;
|
|
369
|
+
// 节点级拉力(08-27 产物锚 v2):连着外部产物的叶子占朝着那个产物的环位 ——
|
|
370
|
+
// 立绘在上方,评它的节点就在环的上侧,线不再绕半圈。质心=全部外部锚的平均
|
|
371
|
+
// (图会落在它们旁边,质心是图心的够用近似)。
|
|
372
|
+
// 方位原点:pullOrigin = 图心的世界坐标(落位后由调用方二次布局传入 ——
|
|
373
|
+
// 环形 bbox 不随槽位变,二次布局不动落位)。没有它就退回锚质心;
|
|
374
|
+
// 锚全挤在一点时质心=锚本身、方向向量退化 → 放弃重排(bfs 序保底)。
|
|
375
|
+
let origin = pullOrigin;
|
|
376
|
+
if (!origin) {
|
|
377
|
+
const pts = [...pull.values()];
|
|
378
|
+
const cx0 = pts.reduce((s, p) => s + p.x, 0) / pts.length;
|
|
379
|
+
const cy0 = pts.reduce((s, p) => s + p.y, 0) / pts.length;
|
|
380
|
+
const spread = Math.max(...pts.map((p) => Math.hypot(p.x - cx0, p.y - cy0)));
|
|
381
|
+
origin = spread > 1 ? { x: cx0, y: cy0 } : null;
|
|
382
|
+
}
|
|
383
|
+
if (origin && pull.size && rest.some((n) => pull.has(n.key))) {
|
|
384
|
+
const slotAng = (i) => (i / rest.length) * Math.PI * 2 - Math.PI / 2;
|
|
385
|
+
const free = new Set(rest.map((_, i) => i));
|
|
386
|
+
const assigned = new Array(rest.length).fill(null);
|
|
387
|
+
const angDist = (a, b) => { const d = Math.abs(a - b) % (Math.PI * 2); return Math.min(d, Math.PI * 2 - d); };
|
|
388
|
+
for (const n of rest) {
|
|
389
|
+
if (!pull.has(n.key)) continue;
|
|
390
|
+
const p = pull.get(n.key);
|
|
391
|
+
const want = Math.atan2(p.y - origin.y, p.x - origin.x);
|
|
392
|
+
let best = null;
|
|
393
|
+
for (const i of free) if (best === null || angDist(slotAng(i), want) < angDist(slotAng(best), want)) best = i;
|
|
394
|
+
assigned[best] = n; free.delete(best);
|
|
395
|
+
}
|
|
396
|
+
const others = rest.filter((n) => !pull.has(n.key));
|
|
397
|
+
let j = 0;
|
|
398
|
+
for (const i of [...free].sort((a, b) => a - b)) { assigned[i] = others[j++]; }
|
|
399
|
+
rest = assigned;
|
|
400
|
+
}
|
|
184
401
|
const cx = 0; const cy = 0;
|
|
185
402
|
pos.set(center.key, { x: cx - center.w / 2, y: cy - center.h / 2 });
|
|
186
403
|
if (!rest.length) return pos;
|
|
@@ -212,66 +429,49 @@ export function layoutNodes(nodes, { template = 'auto', cols = null } = {}) {
|
|
|
212
429
|
return pos;
|
|
213
430
|
}
|
|
214
431
|
|
|
215
|
-
/**
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
}
|
|
221
|
-
return Number.isFinite(x0) ? { x: x0, y: y0, w: x1 - x0, h: y1 - y0 } : { x: 0, y: 0, w: 0, h: 0 };
|
|
432
|
+
/** 一组矩形的包围盒,空集回**零框**(写方直接解构 .x/.w)。真身在 rect.js(那边
|
|
433
|
+
* 空集 null)。08-28 从 bboxOf 改名 —— 同名两种空集语义在同一个调用文件里混用,
|
|
434
|
+
* 读代码必看错;名字把契约说出来,谁也不用再翻两处注释对齐。 */
|
|
435
|
+
export function bboxOrZero(rects) {
|
|
436
|
+
return rectBbox(rects) || { x: 0, y: 0, w: 0, h: 0 };
|
|
222
437
|
}
|
|
223
438
|
|
|
224
439
|
/**
|
|
225
|
-
*
|
|
226
|
-
*
|
|
227
|
-
*
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
if (!hits(x, y)) return { x: Math.round(x), y: Math.round(y), side: 'viewport' };
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
if (near) {
|
|
249
|
-
let x = near.x + near.w + 32; let y = near.y;
|
|
250
|
-
for (let g = 0; g < 30; g += 1) {
|
|
251
|
-
if (!hits(x, y)) return { x: Math.round(x), y: Math.round(y), side: 'right' };
|
|
252
|
-
const blocker = obstacles.find(o => !(x + w <= o.x || o.x + o.w <= x || y + h <= o.y || o.y + o.h <= y));
|
|
253
|
-
y = blocker.y + blocker.h + GAP;
|
|
254
|
-
}
|
|
255
|
-
x = near.x; y = near.y + near.h + 32;
|
|
256
|
-
for (let g = 0; g < 30; g += 1) {
|
|
257
|
-
if (!hits(x, y)) return { x: Math.round(x), y: Math.round(y), side: 'below' };
|
|
258
|
-
const blocker = obstacles.find(o => !(x + w <= o.x || o.x + o.w <= x || y + h <= o.y || o.y + o.h <= y));
|
|
259
|
-
y = blocker.y + blocker.h + GAP;
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
// 排在内容底下:左边缘对齐已有内容(不是写死的 10),读起来像续在同一栏
|
|
263
|
-
const left = obstacles.length ? Math.min(...obstacles.map(o => o.x)) : 10;
|
|
264
|
-
return { x: Math.round(left), y: Math.round(contentBottom) + 40, side: 'bottom' };
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
/**
|
|
268
|
-
* 按用户屏幕算「一屏」:视点里有相机矩形和缩放 → 屏幕像素 = 世界 × 缩放;
|
|
269
|
-
* 0.8 倍可读 → 一屏世界像素 = 屏幕 / 0.8。没视点就用 SKETCH_FIT 的缺省。
|
|
440
|
+
* 按用户屏幕算「一屏」,并说清这台机器该拿哪一套版式(2026-08-28 移动端第二轮)。
|
|
441
|
+
*
|
|
442
|
+
* 屏幕像素:优先读上报的 `device.w/h`(浏览器直接量的),没有就退回
|
|
443
|
+
* 「相机矩形 × 缩放」(08-23 起就在这么算,缩放怎么变都得同一个数)。
|
|
444
|
+
*
|
|
445
|
+
* ## 两套版式
|
|
446
|
+
*
|
|
447
|
+
* 桌面 一屏世界像素 = 屏幕 / 0.75(纸范式基准,见 lib/screen.js —— 用户在
|
|
448
|
+
* 0.75 倍上读得动,所以内容可以比屏幕大一点)
|
|
449
|
+
* 触屏 **一件 = 一屏**:宽 = 屏宽 − 48(两边各留 24 呼吸),纵向单列
|
|
450
|
+
*
|
|
451
|
+
* ⭐ 触屏这条不是"把桌面的数按比例缩小"。桌面那 1.25 倍富余的前提是**用户会缩小
|
|
452
|
+
* 了看全貌再放大读细节**,而手机上缩小到 0.19 倍就什么都读不了(真板量的:1766x2018
|
|
453
|
+
* 的内容在 390 宽的屏上全内容 fit = 19%)。手机上唯一可用的姿势是一件占满一屏、
|
|
454
|
+
* 竖着翻,所以内容必须**按屏幕生产**,而不是生产完再想办法塞进去。
|
|
455
|
+
*
|
|
456
|
+
* ⚠️ 高度给到 1.6 屏而不是 1 屏:竖着滚是手机上读长内容的天然姿势,硬压到一屏
|
|
457
|
+
* 会逼出一堆"为了短而短"的碎块。宽度才是硬约束(横向滑动没人受得了)。
|
|
270
458
|
*/
|
|
271
459
|
export function fitFor(vp) {
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
460
|
+
const cls = vp?.device?.class;
|
|
461
|
+
const lane = (cls === 'phone' || cls === 'tablet') ? cls : 'desktop';
|
|
462
|
+
let sw = Number(vp?.device?.w) || 0;
|
|
463
|
+
let sh = Number(vp?.device?.h) || 0;
|
|
464
|
+
if (!(sw > 0) && vp?.camera?.w && vp?.zoom) {
|
|
465
|
+
sw = vp.camera.w * vp.zoom; sh = vp.camera.h * vp.zoom;
|
|
466
|
+
}
|
|
467
|
+
if (!(sw > 0)) return { ...SKETCH_FIT, screen: null, lane, column: lane !== 'desktop' };
|
|
468
|
+
const screen = { w: Math.round(sw), h: Math.round(sh) };
|
|
469
|
+
if (lane === 'desktop') {
|
|
470
|
+
return { w: Math.round(sw / ZOOM_BASIS), h: Math.round(sh / ZOOM_BASIS), screen, lane, column: false };
|
|
275
471
|
}
|
|
276
|
-
return {
|
|
472
|
+
return {
|
|
473
|
+
w: Math.max(240, Math.round(sw - 48)),
|
|
474
|
+
h: Math.max(320, Math.round(sh * 1.6)),
|
|
475
|
+
screen, lane, column: true,
|
|
476
|
+
};
|
|
277
477
|
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* server/lib/sketch-shapes.js —— sketch 形状构建(2026-08-27 行数棘轮拆件)
|
|
3
|
+
*
|
|
4
|
+
* 从 mcp/tools/write-on-board.js **原样**搬出:入参形状声明 → 局部像素矩形 +
|
|
5
|
+
* 手绘 path + hug 标记。纯计算、不碰磁盘;语义一字未动,搬家原因只有一个 ——
|
|
6
|
+
* write-on-board 顶到 600 行棘轮(642),拆最独立的这一块。
|
|
7
|
+
*
|
|
8
|
+
* 返回 { shapes } 或 { error }(原来的 err() 出口改成 error 字符串,调用方
|
|
9
|
+
* 自己包成工具错误 —— 这层不该知道 MCP 的返回形状)。
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { UNIT, shapePath, roughFreePath } from './sketch-layout.js';
|
|
13
|
+
import { stencilStrokes, STENCIL_NAMES } from './sketch-stencils.js';
|
|
14
|
+
import { expandModifiers } from './sketch-array.js';
|
|
15
|
+
import { flattenClosed, hatchD } from './sketch-hatch.js';
|
|
16
|
+
|
|
17
|
+
/** 板书墨色表(zod 不硬拒,认不出落 ink —— 风格不用 -32602 管) */
|
|
18
|
+
export const SKETCH_COLORS = ['ink', 'red', 'pencil', 'brass'];
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @param {Array} shapesIn 工具入参的 shapes
|
|
22
|
+
* @param {object} deps
|
|
23
|
+
* rectOfNode(key) → {x,y,w,h}|null 已落位节点的局部矩形
|
|
24
|
+
* isTaken(id) → boolean id 是否已被节点/形状占用
|
|
25
|
+
* tag 本张图的 tag(手绘抖动的种子)
|
|
26
|
+
* @returns {{shapes: Array<{key,rect,d,color,width,hug}>}|{error: string}}
|
|
27
|
+
*/
|
|
28
|
+
export function buildSketchShapes(shapesIn, { rectOfNode, isTaken, tag }) {
|
|
29
|
+
const shapes = [];
|
|
30
|
+
for (let i = 0; i < shapesIn.length; i += 1) {
|
|
31
|
+
const s = shapesIn[i];
|
|
32
|
+
const sid = s.id || `s${i + 1}`;
|
|
33
|
+
if (isTaken(sid) || shapes.some(x => x.key === sid)) return { error: `形状 id「${sid}」跟节点/别的形状重名(形状缺省叫 s1,s2…,节点别用这类名)` };
|
|
34
|
+
const seed = `${tag || 'solo'}:${sid}`;
|
|
35
|
+
// 缺省一律 ink。曾有个 'ink2'(箭头用更深墨色)的死分支:色板里没这个色,
|
|
36
|
+
// 落盘前的白名单每次都把它打回 'ink',意图从未生效 —— 08-28 勘查后铲平。
|
|
37
|
+
const color = SKETCH_COLORS.includes(s.color) ? s.color : 'ink';
|
|
38
|
+
const width = s.width || 2;
|
|
39
|
+
// makeOne:按种子重新生成一份本尊 —— 算子展开的每一份各自的抖(看起来是
|
|
40
|
+
// 手画了 n 遍,不是图章盖了 n 下)。返回 {rect, d},d 局部于 rect。
|
|
41
|
+
let makeOne;
|
|
42
|
+
if (s.kind === 'path') {
|
|
43
|
+
// 单位单义(2026-08-30 画图探针案):d 的坐标与 at/w/h **同一套格**(1 格 =
|
|
44
|
+
// 24px,小数随意)。此前 schema 写着"d 是像素"而 at 是格 —— 同一个形状两套
|
|
45
|
+
// 单位,真会话里整幅画的 path 墨迹只有别的图元 1/24 大,agent 误诊成
|
|
46
|
+
// 「path 不渲染」,从此只敢用 rect/circle 拼图,曲线全灭。
|
|
47
|
+
if (!s.d || !/^[\dMLQCZ ,.\-eE]+$/.test(s.d)) return { error: `形状 ${sid}:path 只收大写 M/L/Q/C/Z 与数字(绝对坐标,单位=格)` };
|
|
48
|
+
const nums = s.d.match(/-?\d*\.?\d+(?:[eE][-+]?\d+)?/g)?.map(Number) || [];
|
|
49
|
+
if (!nums.length) return { error: `形状 ${sid}:path 里没有坐标` };
|
|
50
|
+
const xs = nums.filter((_, k) => k % 2 === 0); const ys = nums.filter((_, k) => k % 2 === 1);
|
|
51
|
+
const P = 6;
|
|
52
|
+
const minX = Math.min(...xs) * UNIT; const minY = Math.min(...ys) * UNIT;
|
|
53
|
+
const w = Math.max(4, (Math.max(...xs) - Math.min(...xs)) * UNIT);
|
|
54
|
+
const h = Math.max(4, (Math.max(...ys) - Math.min(...ys)) * UNIT);
|
|
55
|
+
// ×UNIT 并平移到墨迹包围盒(pad 6)—— rect 跟墨走,选中框不再套住一片空气
|
|
56
|
+
let k = 0;
|
|
57
|
+
const local = s.d.replace(/-?\d*\.?\d+(?:[eE][-+]?\d+)?/g, (n) => {
|
|
58
|
+
const v = Number(n) * UNIT - ((k++ % 2 === 0) ? minX : minY) + P;
|
|
59
|
+
return String(Math.round(v * 10) / 10);
|
|
60
|
+
});
|
|
61
|
+
const rect0 = { x: (s.at?.x || 0) * UNIT + minX - P, y: (s.at?.y || 0) * UNIT + minY - P, w: w + P * 2, h: h + P * 2 };
|
|
62
|
+
let fillPolys = null;
|
|
63
|
+
if (s.fill) {
|
|
64
|
+
const fc = flattenClosed(local);
|
|
65
|
+
if (!fc.closedAny) return { error: `形状 ${sid}:fill 只对闭合轮廓生效(Z 收尾)—— 这条 path 没有闭合的子路径` };
|
|
66
|
+
fillPolys = fc.polys;
|
|
67
|
+
}
|
|
68
|
+
makeOne = (sx) => ({ rect: { ...rect0 }, d: roughFreePath(local, seed + sx) + (fillPolys ? ' ' + hatchD(fillPolys, seed + sx) : '') });
|
|
69
|
+
} else if (s.kind === 'stencil') {
|
|
70
|
+
// 词汇表(刀①):参数化简笔画 —— 名字 + 位置 + 尺寸,一笔一画归机器
|
|
71
|
+
if (!STENCIL_NAMES.includes(s.name)) return { error: `形状 ${sid}:stencil 认识这些名字 —— ${STENCIL_NAMES.join(', ')}` };
|
|
72
|
+
if (!s.at || !s.w) return { error: `形状 ${sid}:stencil 要 at + w(格;h 可省 = 按图形比例)` };
|
|
73
|
+
const st = stencilStrokes(s.name, s.w * UNIT, s.h ? s.h * UNIT : null);
|
|
74
|
+
const flipped = s.flip
|
|
75
|
+
? st.strokes.map((d) => { let k2 = 0; return d.replace(/-?\d*\.?\d+/g, (n) => ((k2++ % 2 === 0) ? String(Math.round((st.w - Number(n)) * 10) / 10) : n)); })
|
|
76
|
+
: st.strokes;
|
|
77
|
+
const rect0 = { x: s.at.x * UNIT - 6, y: s.at.y * UNIT - 6, w: st.w + 12, h: st.h + 12 };
|
|
78
|
+
let stFill = null;
|
|
79
|
+
if (s.fill) {
|
|
80
|
+
const fc = flattenClosed(flipped.join(' '));
|
|
81
|
+
if (!fc.closedAny) return { error: `形状 ${sid}:这个 stencil 没有闭合轮廓,fill 排不了线` };
|
|
82
|
+
stFill = fc.polys;
|
|
83
|
+
}
|
|
84
|
+
makeOne = (sx) => ({
|
|
85
|
+
rect: { ...rect0 },
|
|
86
|
+
d: flipped.map((d2, j) => roughFreePath(d2, `${seed}${sx}:${j}`)).join(' ')
|
|
87
|
+
+ (stFill ? ' ' + hatchD(stFill, seed + sx) : ''),
|
|
88
|
+
});
|
|
89
|
+
} else if (s.kind === 'line' || s.kind === 'arrow' || s.kind === 'underline') {
|
|
90
|
+
if (s.fill) return { error: `形状 ${sid}:${s.kind} 是一笔线,没有内部可填 —— fill 只给闭合形状` };
|
|
91
|
+
let a; let b;
|
|
92
|
+
if (s.kind === 'underline' && s.around) {
|
|
93
|
+
const r = rectOfNode(s.around); if (!r) return { error: `形状 ${sid}:around 指向不存在的节点 ${s.around}` };
|
|
94
|
+
a = { x: r.x + 2, y: r.y + r.h - 2 }; b = { x: r.x + r.w - 2, y: r.y + r.h - 2 };
|
|
95
|
+
} else {
|
|
96
|
+
if (!s.at) return { error: `形状 ${sid}:${s.kind} 要 at 起点` };
|
|
97
|
+
a = { x: s.at.x * UNIT, y: s.at.y * UNIT };
|
|
98
|
+
if (s.toNode) {
|
|
99
|
+
const r = rectOfNode(s.toNode); if (!r) return { error: `形状 ${sid}:toNode 指向不存在的节点 ${s.toNode}` };
|
|
100
|
+
b = { x: r.x + r.w / 2, y: r.y + r.h / 2 };
|
|
101
|
+
} else if (s.to) b = { x: s.to.x * UNIT, y: s.to.y * UNIT };
|
|
102
|
+
else if (s.kind === 'underline') b = { x: a.x + (s.w || 4) * UNIT, y: a.y };
|
|
103
|
+
else return { error: `形状 ${sid}:${s.kind} 要 to 或 toNode` };
|
|
104
|
+
}
|
|
105
|
+
makeOne = (sx) => {
|
|
106
|
+
const sp = shapePath(s.kind, { to: { x: b.x - a.x, y: b.y - a.y } }, seed + sx);
|
|
107
|
+
return { rect: { x: Math.min(a.x, b.x) - 6, y: Math.min(a.y, b.y) - 6, w: sp.w, h: sp.h }, d: sp.d };
|
|
108
|
+
};
|
|
109
|
+
} else {
|
|
110
|
+
let box;
|
|
111
|
+
if (s.around) {
|
|
112
|
+
const r = rectOfNode(s.around); if (!r) return { error: `形状 ${sid}:around 指向不存在的节点 ${s.around}` };
|
|
113
|
+
const pad = s.kind === 'rect' ? 8 : 14;
|
|
114
|
+
box = { x: r.x - pad, y: r.y - pad, w: r.w + pad * 2, h: r.h + pad * 2 };
|
|
115
|
+
if (s.kind === 'circle') { const dmax = Math.max(box.w, box.h); box = { x: box.x + (box.w - dmax) / 2, y: box.y + (box.h - dmax) / 2, w: dmax, h: dmax }; }
|
|
116
|
+
} else {
|
|
117
|
+
if (!s.at || !s.w) return { error: `形状 ${sid}:${s.kind} 要 at + w(+h)或 around` };
|
|
118
|
+
box = { x: s.at.x * UNIT, y: s.at.y * UNIT, w: s.w * UNIT, h: (s.h || s.w) * UNIT };
|
|
119
|
+
}
|
|
120
|
+
let boxFill = null;
|
|
121
|
+
if (s.fill) {
|
|
122
|
+
const P2 = 6;
|
|
123
|
+
if (s.kind === 'rect') {
|
|
124
|
+
boxFill = [[{ x: P2, y: P2 }, { x: P2 + box.w, y: P2 }, { x: P2 + box.w, y: P2 + box.h }, { x: P2, y: P2 + box.h }]];
|
|
125
|
+
} else {
|
|
126
|
+
const rx = box.w / 2; const ry = (s.kind === 'circle' ? box.w : box.h) / 2;
|
|
127
|
+
const pts = [];
|
|
128
|
+
for (let a2 = 0; a2 < 24; a2 += 1) {
|
|
129
|
+
const t2 = (a2 / 24) * Math.PI * 2;
|
|
130
|
+
pts.push({ x: P2 + rx + Math.cos(t2) * rx, y: P2 + ry + Math.sin(t2) * ry });
|
|
131
|
+
}
|
|
132
|
+
boxFill = [pts];
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
makeOne = (sx) => {
|
|
136
|
+
const sp = shapePath(s.kind, { w: box.w, h: box.h }, seed + sx);
|
|
137
|
+
return { rect: { x: box.x - 6, y: box.y - 6, w: sp.w, h: sp.h }, d: sp.d + (boxFill ? ' ' + hatchD(boxFill, seed + sx) : '') };
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
// 算子展开(刀④):repeat/ring/mirror/scatter —— 等距和对称归机器
|
|
141
|
+
const pieces = expandModifiers(s, makeOne, sid);
|
|
142
|
+
if (pieces.error) return { error: pieces.error };
|
|
143
|
+
// 包着节点的记号记 hug:落盘后编辑侧挪节点它跟着走(散架病的另一半在 edit-board)
|
|
144
|
+
const hugKey = (s.around && ['rect', 'ellipse', 'circle', 'underline'].includes(s.kind)) ? s.around : null;
|
|
145
|
+
for (const pc of pieces) {
|
|
146
|
+
if (isTaken(pc.key) || shapes.some((x) => x.key === pc.key)) return { error: `形状 id「${pc.key}」撞名(算子副本叫 <id>-2, <id>-3…,别的形状别占这些名)` };
|
|
147
|
+
shapes.push({ key: pc.key, rect: pc.rect, d: pc.d, color, width, hug: hugKey });
|
|
148
|
+
}
|
|
149
|
+
if (shapes.length > 120) return { error: `形状展开后超过 120 件(第 ${i + 1} 个展开完是 ${shapes.length})—— 阵列收敛一点,或拆成几张图` };
|
|
150
|
+
}
|
|
151
|
+
return { shapes };
|
|
152
|
+
}
|