@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
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* server/lib/board-sheets.js —— 纸(sheet)的分配与符号地图(2026-08-29 纸范式刀 1)
|
|
3
|
+
*
|
|
4
|
+
* ## 范式总纲(站主拍板)
|
|
5
|
+
*
|
|
6
|
+
* 落位的旧回路是「模糊锚点 + 机器启发式找洞」,㉚ 五刀证明那一层的坑修不完。
|
|
7
|
+
* 新回路:**开工先铺一张纸(0.75 倍缩放下等于用户一屏的矩形),agent 在纸内用
|
|
8
|
+
* 局部绝对坐标精确摆放,写满翻下一张**。机器只剩两件事:
|
|
9
|
+
* 1. **纸的分配**(下一张纸放哪 —— 缺省当前纸正下方,铺第一张对准用户视口)
|
|
10
|
+
* 2. **局部 → 世界的平移**(纸的原点加上去就完了)
|
|
11
|
+
*
|
|
12
|
+
* 纸是**分配纪律不是本体容器**(08-23 否 frame 的判决不翻案):成员按几何派生
|
|
13
|
+
* (物件中心落在纸内),组照旧由 tag/线派生。用户的手不受纸约束 —— 拖出纸边界
|
|
14
|
+
* 完全合法。
|
|
15
|
+
*
|
|
16
|
+
* 纸名一律 ASCII(p1/p2/…):它是模型要写进工具参数的值(中文参数值会让部分
|
|
17
|
+
* 模型静默结束回合 —— feedback-ascii-tool-params)。
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { UNIT, overlaps, bboxOf, pointIn } from './rect.js';
|
|
21
|
+
import { ONE_SCREEN, ZOOM_BASIS } from './screen.js';
|
|
22
|
+
import { estimateSizeOn, zoneRects } from './board-kind-sizes.js';
|
|
23
|
+
import { DEFAULT_CHALK_W } from './sketch-layout.js';
|
|
24
|
+
import { ROLE_SLUG_RE } from '../engine/agent/cast.js';
|
|
25
|
+
import { layerOf } from './canvas-id.js';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 根层物件(纸只存在于根层桌面,纸面账目只许数根层的东西)。
|
|
29
|
+
*
|
|
30
|
+
* ⛔ 2026-08-30 跨层幻影案(proj_mtfpehm3 首拍连败 4 发):文件夹里的文件卡
|
|
31
|
+
* 存的是**文件夹层内坐标**,数值恰好落在根层第一张纸的范围里;这儿原来不分层
|
|
32
|
+
* 直接扫全部 objects,前端根本不渲染的卡在服务端把纸「占满」了 —— 版位报
|
|
33
|
+
* 剩 0 行、整张纸报 full,全是假账。判据只有一条:layerOf 说它在根层才算。
|
|
34
|
+
*/
|
|
35
|
+
function rootObjects(board) {
|
|
36
|
+
const objs = board?.objects || {};
|
|
37
|
+
const known = new Set(Object.keys(board?.zones || {}));
|
|
38
|
+
const out = [];
|
|
39
|
+
for (const [id, e] of Object.entries(objs)) {
|
|
40
|
+
if (!Number.isFinite(e?.x) || !Number.isFinite(e?.y)) continue;
|
|
41
|
+
if (layerOf(id, e, known) !== '') continue;
|
|
42
|
+
out.push([id, e]);
|
|
43
|
+
}
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** 纸与纸之间的沟(格子感放在纸与纸之间 —— 登录墙定格动画同一条经验) */
|
|
48
|
+
export const SHEET_GAP = 2 * UNIT; // 48
|
|
49
|
+
/** 纸的版心边距:纸内可用区四周留白 */
|
|
50
|
+
export const SHEET_MARGIN = UNIT; // 24
|
|
51
|
+
|
|
52
|
+
const snap = (v) => Math.round(v / UNIT) * UNIT;
|
|
53
|
+
|
|
54
|
+
/** 纸尺寸:铺纸那一刻按设备档算(fitFor 的产出直接喂进来),没有就兜底一屏 */
|
|
55
|
+
export function sheetSizeFor(fit) {
|
|
56
|
+
if (fit && Number.isFinite(fit.w) && Number.isFinite(fit.h)) {
|
|
57
|
+
return { w: Math.round(fit.w), h: Math.round(fit.h) };
|
|
58
|
+
}
|
|
59
|
+
return { ...ONE_SCREEN };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** 注册表 → 阅读序矩形列表(先上后下、同带先左后右) */
|
|
63
|
+
export function sheetRects(board) {
|
|
64
|
+
return Object.entries(board?.sheets || {})
|
|
65
|
+
.map(([id, s]) => ({ id, x: s.x, y: s.y, w: s.w, h: s.h, at: s.at || '', by: s.by || null, title: s.title || null }))
|
|
66
|
+
.sort((a, b) => (a.y - b.y) || (a.x - b.x));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** 下一个纸名:p1/p2/…(取最大序号 +1,撕掉的号不复用 —— 名字要能当历史讲) */
|
|
70
|
+
export function nextSheetName(board) {
|
|
71
|
+
let max = 0;
|
|
72
|
+
for (const id of Object.keys(board?.sheets || {})) {
|
|
73
|
+
const m = /^p(\d{1,5})$/.exec(id);
|
|
74
|
+
if (m) max = Math.max(max, Number(m[1]));
|
|
75
|
+
}
|
|
76
|
+
return `p${max + 1}`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** 点落在哪张纸上(重叠时取登记时间最新的 —— 正常分配不产生重叠,这是兜底) */
|
|
80
|
+
export function sheetOfPoint(board, pt) {
|
|
81
|
+
let hit = null;
|
|
82
|
+
for (const s of sheetRects(board)) {
|
|
83
|
+
if (!pointIn(pt, s)) continue;
|
|
84
|
+
if (!hit || String(s.at) >= String(hit.at)) hit = s;
|
|
85
|
+
}
|
|
86
|
+
return hit;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* 一张纸的成员(几何派生:物件中心在纸内;staging 也算 —— 草稿也占纸面)。
|
|
91
|
+
* 文件夹卡(zones)也是成员:它实打实占着那块地,往下接排必须从它底下起
|
|
92
|
+
* (2026-08-29 占位契约刀 A —— 在这之前它对落位系统整个是隐形的)。
|
|
93
|
+
*/
|
|
94
|
+
export function sheetMembers(board, sheetId) {
|
|
95
|
+
const s = board?.sheets?.[sheetId];
|
|
96
|
+
if (!s) return [];
|
|
97
|
+
const out = [];
|
|
98
|
+
for (const [id, e] of rootObjects(board)) {
|
|
99
|
+
const sz = estimateSizeOn(board, id, e);
|
|
100
|
+
const c = { x: e.x + sz.w / 2, y: e.y + sz.h / 2 };
|
|
101
|
+
if (pointIn(c, s)) out.push({ id, x: e.x, y: e.y, w: sz.w, h: sz.h, entry: e });
|
|
102
|
+
}
|
|
103
|
+
for (const z of zoneRects(board)) {
|
|
104
|
+
const c = { x: z.x + z.w / 2, y: z.y + z.h / 2 };
|
|
105
|
+
if (pointIn(c, s)) out.push({ id: z.id, x: z.x, y: z.y, w: z.w, h: z.h, entry: null, folder: true });
|
|
106
|
+
}
|
|
107
|
+
return out.sort((a, b) => (a.y - b.y) || (a.x - b.x));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** 纸内可用区(世界坐标) */
|
|
111
|
+
export function innerRect(s, margin = SHEET_MARGIN) {
|
|
112
|
+
return { x: s.x + margin, y: s.y + margin, w: s.w - margin * 2, h: s.h - margin * 2 };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** 局部坐标(纸内像素,原点=版心左上角)→ 世界坐标 */
|
|
116
|
+
export function toWorld(s, local, margin = SHEET_MARGIN) {
|
|
117
|
+
return { x: s.x + margin + local.x, y: s.y + margin + local.y };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** 世界坐标 → 纸内局部坐标 */
|
|
121
|
+
export function toLocal(s, world, margin = SHEET_MARGIN) {
|
|
122
|
+
return { x: world.x - s.x - margin, y: world.y - s.y - margin };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* 最新那张纸的名字 = 登记时间(at)最大的那张。**「最新」只有这一份算法**。
|
|
127
|
+
*
|
|
128
|
+
* ⚠️ 2026-08-30:这之前有两份。落位走 currentSheet(按 at),而每回合状态块和
|
|
129
|
+
* read_board 取的是 `sheetRects(board)` 的**最后一项** —— 那个数组按 y 排序,
|
|
130
|
+
* 拿到的是「最下面那张」。平时新纸往下叠、两者恰好重合,`open_sheet{where:
|
|
131
|
+
* 'viewport'}` 把纸铺在上方时就分叉:免费账本会去报一张 agent 根本没在写的纸。
|
|
132
|
+
* 「同一件事各执一词」先想收成一份。
|
|
133
|
+
*/
|
|
134
|
+
export function latestSheetId(board) {
|
|
135
|
+
let best = null; let bestAt = '';
|
|
136
|
+
for (const [id, s] of Object.entries(board?.sheets || {})) {
|
|
137
|
+
if (!best || String(s.at || '') > bestAt) { best = id; bestAt = String(s.at || ''); }
|
|
138
|
+
}
|
|
139
|
+
return best;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* 当前纸:会话指过的那张(还在就认),否则登记时间最新的一张。
|
|
144
|
+
* @param {string|null} preferred 会话层记着的纸名
|
|
145
|
+
*/
|
|
146
|
+
export function currentSheet(board, preferred = null) {
|
|
147
|
+
const sheets = board?.sheets || {};
|
|
148
|
+
if (preferred && sheets[preferred]) {
|
|
149
|
+
return { id: preferred, ...sheets[preferred] };
|
|
150
|
+
}
|
|
151
|
+
const id = latestSheetId(board);
|
|
152
|
+
return id ? { id, ...sheets[id] } : null;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* 给新纸找落点。纪律:
|
|
157
|
+
* - 铺第一张(或显式要求)对准**用户视口**(agent 在用户眼皮底下开工)
|
|
158
|
+
* - 续铺缺省落在锚定纸(通常是当前纸)**正下方**(竖着长 = 阅读序,跟章节竖列同构)
|
|
159
|
+
* - 纸与纸绝不重叠(硬约束,沿轴滑开);散落的旧物件尽量避开,避不开就压上
|
|
160
|
+
* (纸画在物件层之下,旧东西看起来就是「摆在这张纸上」—— 如实报,不硬拒)
|
|
161
|
+
*
|
|
162
|
+
* 契约与 resolvePlacement 同款:**没有失败分支**,永远返回一个矩形。
|
|
163
|
+
*
|
|
164
|
+
* @returns {{x,y,w,h, basis:'viewport'|'below-sheet'|'below-content', overlapsLoose:boolean}}
|
|
165
|
+
*/
|
|
166
|
+
export function allocateSheetRect({ board, size, viewport = null, nearSheet = null, obstacles = [] }) {
|
|
167
|
+
const sz = { w: Math.max(240, Math.round(size?.w || ONE_SCREEN.w)), h: Math.max(240, Math.round(size?.h || ONE_SCREEN.h)) };
|
|
168
|
+
const sheets = sheetRects(board);
|
|
169
|
+
const anchor = nearSheet ? sheets.find((s) => s.id === nearSheet) || null : null;
|
|
170
|
+
|
|
171
|
+
let ideal; let basis;
|
|
172
|
+
if (anchor) {
|
|
173
|
+
ideal = { x: anchor.x, y: anchor.y + anchor.h + SHEET_GAP };
|
|
174
|
+
basis = 'below-sheet';
|
|
175
|
+
} else if (viewport && Number.isFinite(viewport.x)) {
|
|
176
|
+
// 对准视口:纸的左上贴视口左上(snap 到格)。纸比视口大(0.75 基准 vs 用户
|
|
177
|
+
// 此刻缩放)没关系 —— 铺纸之后镜头会去框纸。
|
|
178
|
+
ideal = { x: snap(viewport.x), y: snap(viewport.y) };
|
|
179
|
+
basis = 'viewport';
|
|
180
|
+
} else {
|
|
181
|
+
const content = bboxOf([...sheets, ...obstacles]);
|
|
182
|
+
ideal = content
|
|
183
|
+
? { x: snap(content.x), y: snap(content.y + content.h) + SHEET_GAP }
|
|
184
|
+
: { x: 0, y: 0 };
|
|
185
|
+
basis = 'below-content';
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const hitsSheet = (r) => sheets.some((s) => overlaps(r, s, SHEET_GAP / 2));
|
|
189
|
+
const hitsLoose = (r) => obstacles.some((o) => overlaps(r, o));
|
|
190
|
+
|
|
191
|
+
// 先找「纸不撞纸、也不压散件」的位置;往下滑最多 4 屏,找不到就接受压散件,
|
|
192
|
+
// 只保硬约束(纸不撞纸)。滑动只沿 y 轴 —— 纸的阅读序是竖着长的。
|
|
193
|
+
const MAX_SLIDE = sz.h * 4;
|
|
194
|
+
let clean = null; let sheetClear = null;
|
|
195
|
+
for (let dy = 0; dy <= MAX_SLIDE; dy += UNIT * 2) {
|
|
196
|
+
const r = { x: ideal.x, y: ideal.y + dy, w: sz.w, h: sz.h };
|
|
197
|
+
if (hitsSheet(r)) continue;
|
|
198
|
+
if (sheetClear === null) sheetClear = r;
|
|
199
|
+
if (!hitsLoose(r)) { clean = r; break; }
|
|
200
|
+
}
|
|
201
|
+
if (clean) return { ...clean, basis, overlapsLoose: false };
|
|
202
|
+
if (sheetClear) return { ...sheetClear, basis, overlapsLoose: true };
|
|
203
|
+
// 连纸不撞纸的位置都没有(理想列被纸占满 4 屏):落到全部纸的最底下
|
|
204
|
+
const all = bboxOf(sheets) || { x: ideal.x, y: ideal.y, w: 0, h: 0 };
|
|
205
|
+
const r = { x: ideal.x, y: snap(all.y + all.h) + SHEET_GAP, w: sz.w, h: sz.h };
|
|
206
|
+
return { ...r, basis, overlapsLoose: hitsLoose(r) };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* 纸内自动落位(给没带坐标的写入用:产物入座、步骤清单、agent 偷懒不给 at)。
|
|
211
|
+
*
|
|
212
|
+
* **跟着设备的形状走**(2026-08-29 站主拍板):
|
|
213
|
+
* - 横纸(电脑:2048×973 那种)→ **报纸分栏**:竖着填满一栏,再到右边下一栏顶上,
|
|
214
|
+
* 整页填满才翻页。原来只会往下排一列 —— 一张两个屏幕宽的纸,四分之三是空的。
|
|
215
|
+
* - 竖纸(手机:屏宽 × 1.6 屏高)→ 只往下。竖屏上分栏等于把每栏挤成一指宽。
|
|
216
|
+
*
|
|
217
|
+
* 栏宽固定 DEFAULT_CHALK_W(不随内容变),否则每写一件栏边界就漂一次,读起来
|
|
218
|
+
* 是锯齿。比一栏宽的东西(产物卡 640)自然占掉它压住的所有栏 —— 算某栏的底部时
|
|
219
|
+
* 认**水平重叠**而不是中心点,宽物件才挡得住它真正盖住的那几栏。
|
|
220
|
+
*
|
|
221
|
+
* 装不下返回 null(调用方翻新纸)。仍然不是启发式引擎:规则只有"竖着填、填满换栏"。
|
|
222
|
+
*
|
|
223
|
+
* @returns {{x,y,col?}|null} 世界坐标;null = 这张纸满了
|
|
224
|
+
*/
|
|
225
|
+
export function nextSpotInSheet(board, sheetId, box, { gap = UNIT } = {}) {
|
|
226
|
+
const s = board?.sheets?.[sheetId];
|
|
227
|
+
if (!s) return null;
|
|
228
|
+
const inner = innerRect(s);
|
|
229
|
+
if (box.w > inner.w) return null; // 比纸还宽的东西没资格进这张纸
|
|
230
|
+
const members = sheetMembers(board, sheetId);
|
|
231
|
+
const floor = inner.y + inner.h;
|
|
232
|
+
|
|
233
|
+
// 竖纸:只往下(原规则)
|
|
234
|
+
if (inner.w <= inner.h) {
|
|
235
|
+
const bottom = members.length ? Math.max(...members.map((m) => m.y + m.h)) : inner.y - gap;
|
|
236
|
+
const y = Math.round(bottom + gap);
|
|
237
|
+
if (y + box.h > floor) return null;
|
|
238
|
+
return { x: Math.round(inner.x), y };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// 横纸:先往下接;这一列到底了,就在纸上往右找第一块放得下的地方。
|
|
242
|
+
// 步长跟这件东西的宽度走,不是切死的栏 —— 机器只是帮忙找地方,版面怎么切
|
|
243
|
+
// 是 agent 自己的事(站主:有限制有规划的自由)。
|
|
244
|
+
const step = Math.max(UNIT * 4, Math.round(box.w) + gap);
|
|
245
|
+
for (let x = inner.x; x + box.w <= inner.x + inner.w + 1; x += step) {
|
|
246
|
+
const hit = members.filter((m) => m.x < x + box.w && m.x + m.w > x);
|
|
247
|
+
const bottom = hit.length ? Math.max(...hit.map((m) => m.y + m.h)) : inner.y - gap;
|
|
248
|
+
const y = Math.round(bottom + gap);
|
|
249
|
+
if (y + box.h <= floor) return { x: Math.round(x), y, moved: x > inner.x };
|
|
250
|
+
}
|
|
251
|
+
return null; // 纸上哪儿都放不下了 —— 这才叫满
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* 这张纸还剩哪些空地(2026-08-29 刀 F,站主要的"有规划的自由"的依据)。
|
|
256
|
+
*
|
|
257
|
+
* agent 要自己判断"还放不放得下、要不要开新一页",就得知道纸上哪里还空着 ——
|
|
258
|
+
* 只报"最后一件下面还剩多少"是不够的:那一列到底了不等于这张纸满了,右边可能
|
|
259
|
+
* 整片空着。按内容列宽扫一遍,报每一列的剩余高度。
|
|
260
|
+
*
|
|
261
|
+
* @returns {Array<{x:number, freeH:number}>} x = 纸内局部像素
|
|
262
|
+
*/
|
|
263
|
+
export function freeColumnsInSheet(board, sheetId, colW = DEFAULT_CHALK_W, gap = UNIT) {
|
|
264
|
+
const s = board?.sheets?.[sheetId];
|
|
265
|
+
if (!s) return [];
|
|
266
|
+
const inner = innerRect(s);
|
|
267
|
+
const members = sheetMembers(board, sheetId);
|
|
268
|
+
const out = [];
|
|
269
|
+
for (let x = inner.x; x + colW <= inner.x + inner.w + 1; x += colW + gap) {
|
|
270
|
+
const hit = members.filter((m) => m.x < x + colW && m.x + m.w > x);
|
|
271
|
+
const bottom = hit.length ? Math.max(...hit.map((m) => m.y + m.h)) : inner.y - gap;
|
|
272
|
+
out.push({ x: Math.round(x - inner.x), freeH: Math.max(0, Math.round(inner.y + inner.h - (bottom + gap))) });
|
|
273
|
+
}
|
|
274
|
+
return out;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/* ── 版位(slot,2026-08-29 占位契约刀 E)────────────────────────────────
|
|
278
|
+
*
|
|
279
|
+
* 站主拍板:**agent 应当提前规划所有落位,然后再开始生成**。所以开工那一步不是
|
|
280
|
+
* 「写第一条」而是「把这一屏切成几块地」(open_sheet 的 plan),之后每条内容
|
|
281
|
+
* 点名往哪块地里放。
|
|
282
|
+
*
|
|
283
|
+
* 为什么这比「一条一条自己给 at」强:一张纸是横的(约 2.1:1),一条一条往下写
|
|
284
|
+
* 只会用掉最左边一栏、剩下大半空着;而且写到第五条才发现装不下时,前四条已经
|
|
285
|
+
* 落盘了。先规划=**版面在内容之前就定死**,装不下当场就知道。
|
|
286
|
+
*
|
|
287
|
+
* 装不下的处理是**拒收**不是挤进去(站主原话:提示 agent 让她分块内容、重新
|
|
288
|
+
* 布置)—— 折叠、裁切、自动缩排都是替它把问题藏起来。
|
|
289
|
+
*/
|
|
290
|
+
|
|
291
|
+
/** 版位矩形(世界坐标)。名字不存在返回 null */
|
|
292
|
+
export function slotRectOf(sheet, name, margin = SHEET_MARGIN) {
|
|
293
|
+
const sl = sheet?.slots?.[name];
|
|
294
|
+
if (!sl) return null;
|
|
295
|
+
return {
|
|
296
|
+
x: sheet.x + margin + sl.x, y: sheet.y + margin + sl.y,
|
|
297
|
+
w: sl.w, h: sl.h, about: sl.about || null,
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** 落在某块地里的物件(中心点判据,同 sheetMembers;只数根层 —— 版位在纸上,
|
|
302
|
+
* 纸在根层。顶层文件夹卡真占着地也算成员:它就摆在那儿)。 */
|
|
303
|
+
export function membersInRect(board, rect) {
|
|
304
|
+
const out = [];
|
|
305
|
+
for (const [id, e] of rootObjects(board)) {
|
|
306
|
+
const sz = estimateSizeOn(board, id, e);
|
|
307
|
+
const c = { x: e.x + sz.w / 2, y: e.y + sz.h / 2 };
|
|
308
|
+
if (pointIn(c, rect)) out.push({ id, x: e.x, y: e.y, w: sz.w, h: sz.h });
|
|
309
|
+
}
|
|
310
|
+
for (const z of zoneRects(board)) {
|
|
311
|
+
const c = { x: z.x + z.w / 2, y: z.y + z.h / 2 };
|
|
312
|
+
if (pointIn(c, rect)) out.push({ id: z.id, x: z.x, y: z.y, w: z.w, h: z.h, folder: true });
|
|
313
|
+
}
|
|
314
|
+
return out.sort((a, b) => (a.y - b.y) || (a.x - b.x));
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* 版位内往下接排。装不下**不返回兜底位置** —— 返回 full + 还剩多少,
|
|
319
|
+
* 调用方据此拒收并把这个数报给 agent。
|
|
320
|
+
*
|
|
321
|
+
* @returns {{x,y}|{full:true, freeH:number, needH:number}}
|
|
322
|
+
*/
|
|
323
|
+
export function nextSpotInSlot(board, rect, box, { gap = UNIT } = {}) {
|
|
324
|
+
const members = membersInRect(board, rect);
|
|
325
|
+
const bottom = members.length ? Math.max(...members.map((m) => m.y + m.h)) : rect.y - gap;
|
|
326
|
+
const y = Math.round(bottom + gap);
|
|
327
|
+
const freeH = Math.max(0, Math.round(rect.y + rect.h - y));
|
|
328
|
+
if (box.h > freeH) return { full: true, freeH, needH: box.h, taken: members.length };
|
|
329
|
+
return { x: Math.round(rect.x), y };
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* 符号地图:板上现在有哪些纸(read_board / 注入 用)。
|
|
334
|
+
* 每张:名字、矩形、件数、剩余高度(版心内从成员最低边到纸底)。
|
|
335
|
+
*/
|
|
336
|
+
export function sheetSummaries(board) {
|
|
337
|
+
return sheetRects(board).map((s) => {
|
|
338
|
+
const members = sheetMembers(board, s.id);
|
|
339
|
+
const inner = innerRect(s);
|
|
340
|
+
const bottom = members.length ? Math.max(...members.map((m) => m.y + m.h)) : inner.y;
|
|
341
|
+
const full = board?.sheets?.[s.id] || {};
|
|
342
|
+
// 版位余量(2026-08-30):agent 每回合免费拿到的空间账本此前只有整张纸的
|
|
343
|
+
// freeH —— 而它写的时候是往**块**里写。报纸不报块,等于让它对着错的数做判断。
|
|
344
|
+
const slots = Object.entries(full.slots || {}).map(([name, sl]) => {
|
|
345
|
+
const rect = slotRectOf({ ...full, id: s.id }, name);
|
|
346
|
+
const spot = rect ? nextSpotInSlot(board, rect, { w: 1, h: 1 }) : { full: true };
|
|
347
|
+
const freeH = (rect && !spot.full) ? Math.max(0, Math.round(rect.y + rect.h - spot.y)) : 0;
|
|
348
|
+
return { name, about: sl.about || null, freeH, freeLines: Math.max(0, Math.floor((freeH - 8) / 26)) };
|
|
349
|
+
});
|
|
350
|
+
return {
|
|
351
|
+
id: s.id, x: s.x, y: s.y, w: s.w, h: s.h,
|
|
352
|
+
title: s.title, by: s.by, at: s.at,
|
|
353
|
+
count: members.length,
|
|
354
|
+
lastId: members.length ? members[members.length - 1].id : null,
|
|
355
|
+
freeH: Math.max(0, Math.round(inner.y + inner.h - bottom)),
|
|
356
|
+
innerW: inner.w,
|
|
357
|
+
slots,
|
|
358
|
+
};
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/* ── 落位三动词(2026-08-29 刀 2:启发式引擎退役后仅存的三条几何规则)──
|
|
363
|
+
*
|
|
364
|
+
* 旧引擎(环搜/挑侧/半平面/走廊/兜底左缘)整层退役 —— ㉚ 五刀证明那一层的坑
|
|
365
|
+
* 修不完,且 agent 对「纸内绝对坐标」的适应性远好于「模糊锚点」。留下的规则
|
|
366
|
+
* 每条都短到不需要启发式:
|
|
367
|
+
* placeAtOnSheet agent 说了算(钳进版心,钳了如实报)
|
|
368
|
+
* placeThread 接楼只有一个方向:正下方,压住就跳到那件底下
|
|
369
|
+
* placeBeside 贴放是精确几何不是搜索(压上如实报,不代找洞)
|
|
370
|
+
*/
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* 纸内定点:局部坐标 → 世界坐标,钳进版心。钳过要如实报(越界钳住但要说)。
|
|
374
|
+
*
|
|
375
|
+
* `overflowY` = 从 agent 要的那个 y 往下**真的不够高**还差多少(2026-08-29 刀 C)。
|
|
376
|
+
* 站主定的换纸判据第一条就是"当前这块地放不下剩下一整块内容"—— 光说"钳住了"
|
|
377
|
+
* 不够,钳住的结果是那条被压到贴着纸底、跟上一条挤在一起,而 agent 完全不知道
|
|
378
|
+
* 该翻页了。
|
|
379
|
+
*/
|
|
380
|
+
export function placeAtOnSheet(s, at, box) {
|
|
381
|
+
const inner = innerRect(s);
|
|
382
|
+
const ideal = toWorld(s, { x: Math.round(at.x), y: Math.round(at.y) });
|
|
383
|
+
const x = Math.min(Math.max(ideal.x, inner.x), Math.max(inner.x, inner.x + inner.w - box.w));
|
|
384
|
+
const y = Math.min(Math.max(ideal.y, inner.y), Math.max(inner.y, inner.y + inner.h - box.h));
|
|
385
|
+
return {
|
|
386
|
+
x: Math.round(x), y: Math.round(y),
|
|
387
|
+
clamped: x !== ideal.x || y !== ideal.y,
|
|
388
|
+
overflowY: Math.max(0, Math.round(ideal.y + box.h - (inner.y + inner.h))),
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* 接楼(线程):被回应那条的正下方;位置被占就跳到占位者底下接着试。
|
|
394
|
+
* 只往下 —— 读序即方向。落点出了所在纸的版心底 → 报 sheetFull(调用方翻纸)。
|
|
395
|
+
* 纸外(文件夹层/散地)没有纸界,滑到空为止。
|
|
396
|
+
*/
|
|
397
|
+
export function placeThread(board, replyRect, box, { obstacles = [], gap = UNIT } = {}) {
|
|
398
|
+
const sheet = sheetOfPoint(board, { x: replyRect.x + replyRect.w / 2, y: replyRect.y + replyRect.h / 2 });
|
|
399
|
+
const floor = sheet ? innerRect(sheet).y + innerRect(sheet).h : Infinity;
|
|
400
|
+
const x = Math.round(replyRect.x);
|
|
401
|
+
let y = replyRect.y + replyRect.h + gap;
|
|
402
|
+
for (let i = 0; i < 500; i += 1) {
|
|
403
|
+
if (y + box.h > floor) return { sheetFull: sheet.id };
|
|
404
|
+
const r = { x, y, w: box.w, h: box.h };
|
|
405
|
+
const hit = obstacles.find((o) => overlaps(r, o));
|
|
406
|
+
if (!hit) return { x, y: Math.round(y), sheetId: sheet?.id || null };
|
|
407
|
+
y = hit.y + hit.h + gap;
|
|
408
|
+
}
|
|
409
|
+
return { x, y: Math.round(y), sheetId: sheet?.id || null };
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/** 贴放:锚点某一侧的精确位置(gap 像素)。不搜索 —— 压上由调用方如实报。 */
|
|
413
|
+
export function placeBeside(anchor, box, side = 'right', gap = UNIT) {
|
|
414
|
+
const p = side === 'right' ? { x: anchor.x + anchor.w + gap, y: anchor.y }
|
|
415
|
+
: side === 'left' ? { x: anchor.x - gap - box.w, y: anchor.y }
|
|
416
|
+
: side === 'below' ? { x: anchor.x, y: anchor.y + anchor.h + gap }
|
|
417
|
+
: { x: anchor.x, y: anchor.y - gap - box.h };
|
|
418
|
+
return { x: Math.round(p.x), y: Math.round(p.y) };
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/** 压上了谁(如实报的判据;调用方拿去写返回文案,不据此挪位置) */
|
|
422
|
+
export function overlapIds(rect, obstacles, { pad = 0, limit = 6 } = {}) {
|
|
423
|
+
const out = [];
|
|
424
|
+
for (const o of obstacles) {
|
|
425
|
+
if (!overlaps(rect, o, pad)) continue;
|
|
426
|
+
if (o.id) out.push(o.id);
|
|
427
|
+
if (out.length >= limit) break;
|
|
428
|
+
}
|
|
429
|
+
return out;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* 精灵身位(从 board-place.js 搬来,2026-08-27 建)。角色精灵在客户端贴着该角色
|
|
434
|
+
* 最新一条板书摆,服务端只能把那条当带身位的障碍 —— 四周外扩 pad,新东西的
|
|
435
|
+
* 压上判定就不会压在精灵脸上。「最新」按 objects 插入序。
|
|
436
|
+
*/
|
|
437
|
+
export function inflateSpriteSeats(obstacles, objects, pad = 60) {
|
|
438
|
+
const last = new Map();
|
|
439
|
+
for (const [id, e] of Object.entries(objects || {})) {
|
|
440
|
+
if (typeof e?.by === 'string' && ROLE_SLUG_RE.test(e.by)) last.set(e.by, id);
|
|
441
|
+
}
|
|
442
|
+
if (!last.size) return obstacles;
|
|
443
|
+
const ids = new Set(last.values());
|
|
444
|
+
return obstacles.map((o) => (ids.has(o.id)
|
|
445
|
+
? { ...o, x: o.x - pad, y: o.y - pad, w: o.w + pad * 2, h: o.h + pad * 2 }
|
|
446
|
+
: o));
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* 卷卡(RollLayer)占位估算(2026-08-29 刀 3):卷卡是前端合成物不进 objects,
|
|
451
|
+
* 但它真占桌面一角 —— agent 要知道那儿有张卡。位置 = 成员左上角(前端同款),
|
|
452
|
+
* 尺寸按标签字数估(chip:padding + 文字)。成员座位本来就保留当障碍,这只是
|
|
453
|
+
* 让 read_board 能把「那张卷卡」点出来。
|
|
454
|
+
*/
|
|
455
|
+
export function rollCardRect(board, tag) {
|
|
456
|
+
const roll = board?.rolls?.[tag];
|
|
457
|
+
if (!roll) return null;
|
|
458
|
+
const members = Object.entries(board?.objects || {}).filter(([, e]) => e?.tag === tag && Number.isFinite(e?.x));
|
|
459
|
+
if (!members.length) return null;
|
|
460
|
+
const x = Math.min(...members.map(([, e]) => e.x));
|
|
461
|
+
const y = Math.min(...members.map(([, e]) => e.y));
|
|
462
|
+
const label = String(roll.label || tag);
|
|
463
|
+
const em = [...label].reduce((n, c) => n + (/[ -鿿-]/.test(c) ? 1 : 0.62), 0);
|
|
464
|
+
return { x: Math.round(x), y: Math.round(y), w: Math.round(48 + em * 15), h: 40 };
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
export { ZOOM_BASIS };
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* server/lib/board-shelf.js —— 暂存架(2026-08-30)
|
|
3
|
+
*
|
|
4
|
+
* 机器的手从此只够得到这条架:入座器/步骤镜像找不到 agent 规划的地
|
|
5
|
+
* (for:'artifacts' 版位、chalk 显式锚点)时,一律把东西码在这条竖带上 ——
|
|
6
|
+
* **绝不再铺纸,也绝不再往 agent 的纸面上顺排**。真案 proj_mtfz7n8p:web_search
|
|
7
|
+
* 采回的参考图撞进入座器的「开工例外」,第一张纸成了机器铺的,agent 从头到尾
|
|
8
|
+
* 不知道那张纸存在,自己的板书只能在缝里流。
|
|
9
|
+
*
|
|
10
|
+
* 架上的东西 seat:'shelf'。离架不靠第二份成员表:pin_to_board 写 seat:'agent'、
|
|
11
|
+
* edit_board move 写 seat:'agent'、用户拖拽写 seat:'user' —— 任何一只手一挪,
|
|
12
|
+
* seat 被改写,它就自然不在架上了(单一真相 = 座位出处字段本身)。
|
|
13
|
+
*
|
|
14
|
+
* 架的原点存 board.shelf {x,y},立了就不挪;唯一的例外是**架带跟后来铺的纸
|
|
15
|
+
* 横向撞上**(agent 有权把纸开在任何地方),这时搬去纸群左侧重立 —— 判据是
|
|
16
|
+
* 整条竖带不是原点那一个点,见 bandHitsSheet。
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
export const SHELF_W = 360;
|
|
20
|
+
export const SHELF_GAP = 24;
|
|
21
|
+
// 文件夹卡在架上的占地(数字对齐 web/src/lib/board-geometry.js 的 FOLDER_CARD ——
|
|
22
|
+
// zones 存档只有坐标,尺寸恒为前端那份;这里只用来算架上的避让)
|
|
23
|
+
export const FOLDER_BOX = { w: 288, h: 240 };
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* 架带跟这张纸撞不撞。**架不是原点那一个点,是从原点往下的一条竖带**
|
|
27
|
+
* (宽 SHELF_W,向下不封口)。
|
|
28
|
+
*
|
|
29
|
+
* 原来这儿测的是「原点这个点落没落进纸里」,于是架立在纸群**正上方**时
|
|
30
|
+
* 永远判不出冲突,然后顺着这条带一路往下长穿每一张纸。真案 proj_mtg61or1:
|
|
31
|
+
* 架原点 (24,24) 在 p1 上方 1464px,四张纸一张都没"压住"它,但架带 x[24,384)
|
|
32
|
+
* 跟四张纸的横向重叠率是 100% —— agent 写的板书全从 x=24 起排,42 个根层
|
|
33
|
+
* 对象里 38 个压在架带上,架被自己码的货一路顶到 y=8322(板子才高 2600);
|
|
34
|
+
* 反过来铺新纸又要躲开架上的散件,纸也被顶着往下走(p3 底 3805 → p4 顶 4621)。
|
|
35
|
+
* 两边互相顶,板子只会单向长高。
|
|
36
|
+
*/
|
|
37
|
+
const bandHitsSheet = (s, x, y) => s.x < x + SHELF_W && s.x + s.w > x && s.y + s.h > y;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 架的原点。已立的沿用(除非架带跟纸撞上);没立过的:有纸靠纸群左上侧,
|
|
41
|
+
* 没纸落在用户视口左上(到货要让用户看得见)。
|
|
42
|
+
* @param {object} board
|
|
43
|
+
* @param {{x,y,w,h}|null} viewport 用户相机(根层才传)
|
|
44
|
+
* @returns {{x:number,y:number,changed:boolean}}
|
|
45
|
+
*/
|
|
46
|
+
export function resolveShelfOrigin(board, viewport = null) {
|
|
47
|
+
const sheets = Object.values(board.sheets || {})
|
|
48
|
+
.filter(s => Number.isFinite(s?.x) && Number.isFinite(s?.y) && Number.isFinite(s?.w) && Number.isFinite(s?.h));
|
|
49
|
+
const cur = board.shelf;
|
|
50
|
+
if (cur && Number.isFinite(cur.x) && Number.isFinite(cur.y) && !sheets.some(s => bandHitsSheet(s, cur.x, cur.y))) {
|
|
51
|
+
return { x: cur.x, y: cur.y, changed: false };
|
|
52
|
+
}
|
|
53
|
+
if (sheets.length) {
|
|
54
|
+
const minX = Math.min(...sheets.map(s => s.x));
|
|
55
|
+
const minY = Math.min(...sheets.map(s => s.y));
|
|
56
|
+
return { x: Math.round(minX - SHELF_W - SHELF_GAP), y: Math.round(minY), changed: true };
|
|
57
|
+
}
|
|
58
|
+
if (viewport && Number.isFinite(viewport.x) && Number.isFinite(viewport.y)) {
|
|
59
|
+
return { x: Math.round(viewport.x + SHELF_GAP), y: Math.round(viewport.y + SHELF_GAP), changed: true };
|
|
60
|
+
}
|
|
61
|
+
return { x: SHELF_GAP, y: SHELF_GAP, changed: true };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* 架上下一个空位:从原点往下码。占位判据是「矩形横向压进架带」,不看 seat ——
|
|
66
|
+
* 用户拖了张卡堵在架上,新到的也得往下让,压上去才是数据损坏。
|
|
67
|
+
* @param {{x,y}} origin
|
|
68
|
+
* @param {Array<{x,y,w,h}>} obstacles 同层全部已摆矩形
|
|
69
|
+
*/
|
|
70
|
+
export function nextShelfSpot(origin, obstacles) {
|
|
71
|
+
let bottom = origin.y;
|
|
72
|
+
for (const o of obstacles || []) {
|
|
73
|
+
if (!Number.isFinite(o?.x) || !Number.isFinite(o?.y)) continue;
|
|
74
|
+
if (o.x + (o.w || 0) <= origin.x || o.x >= origin.x + SHELF_W) continue;
|
|
75
|
+
if (o.y + (o.h || 0) <= origin.y) continue;
|
|
76
|
+
bottom = Math.max(bottom, o.y + (o.h || 0) + SHELF_GAP);
|
|
77
|
+
}
|
|
78
|
+
return { x: origin.x, y: Math.round(bottom) };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** 架上现有的东西(根层、seat:'shelf' 的画布 id 列表,登记序) */
|
|
82
|
+
export function shelfItems(board) {
|
|
83
|
+
return Object.entries(board.objects || {})
|
|
84
|
+
.filter(([, e]) => e?.seat === 'shelf' && !e.zone && Number.isFinite(e.x))
|
|
85
|
+
.map(([id]) => id);
|
|
86
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* server/lib/board-transform.js —— 整组缩放/旋转(2026-08-30 画图能力线)
|
|
3
|
+
*
|
|
4
|
+
* 画完一幅想整体缩小/转个角度,此前只能逐笔挪。以组(tag)为单位绕组心变换:
|
|
5
|
+
* - 涂鸦(scribble)**真变形**:d 逐点过矩阵、包围盒重建 —— 线条真的转过去
|
|
6
|
+
* - 其他成员(text/板书/产物卡)**只换座不变形**:字号和卡面没有"转 30°"的
|
|
7
|
+
* 渲染语义,硬转是把问题藏起来 —— 中心点跟着矩阵走、如实报"这几件只挪了位"
|
|
8
|
+
*
|
|
9
|
+
* 纯函数:算出 objects patch 交回,落盘归调用方(edit-board)。
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { transformD } from './sketch-layout.js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @param members [{id, entry, w, h}](w/h 是估好的真尺寸)
|
|
16
|
+
* @param opts { scale?: 0.3..3, rotate?: 度, about?: {x,y} 世界像素(缺省组心)}
|
|
17
|
+
* @returns {{ patch: Record<string, object>, inked: number, seated: number, center: {x,y} }}
|
|
18
|
+
*/
|
|
19
|
+
export function transformGroup(members, { scale = 1, rotate = 0, about = null } = {}) {
|
|
20
|
+
const minX = Math.min(...members.map((m) => m.entry.x));
|
|
21
|
+
const minY = Math.min(...members.map((m) => m.entry.y));
|
|
22
|
+
const maxX = Math.max(...members.map((m) => m.entry.x + m.w));
|
|
23
|
+
const maxY = Math.max(...members.map((m) => m.entry.y + m.h));
|
|
24
|
+
const C = about || { x: (minX + maxX) / 2, y: (minY + maxY) / 2 };
|
|
25
|
+
const th = (rotate * Math.PI) / 180;
|
|
26
|
+
const cos = Math.cos(th); const sin = Math.sin(th);
|
|
27
|
+
const T = (p) => ({
|
|
28
|
+
x: C.x + ((p.x - C.x) * cos - (p.y - C.y) * sin) * scale,
|
|
29
|
+
y: C.y + ((p.x - C.x) * sin + (p.y - C.y) * cos) * scale,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const patch = {};
|
|
33
|
+
let inked = 0; let seated = 0;
|
|
34
|
+
for (const m of members) {
|
|
35
|
+
const e = m.entry;
|
|
36
|
+
if (m.id.startsWith('scribble:') && typeof e?.data?.d === 'string') {
|
|
37
|
+
let mnX = Infinity; let mnY = Infinity; let mxX = -Infinity; let mxY = -Infinity;
|
|
38
|
+
const dAbs = transformD(e.data.d, (p) => {
|
|
39
|
+
const q = T({ x: e.x + p.x, y: e.y + p.y });
|
|
40
|
+
if (q.x < mnX) mnX = q.x; if (q.x > mxX) mxX = q.x;
|
|
41
|
+
if (q.y < mnY) mnY = q.y; if (q.y > mxY) mxY = q.y;
|
|
42
|
+
return q;
|
|
43
|
+
});
|
|
44
|
+
if (!Number.isFinite(mnX)) { seated += 1; continue; }
|
|
45
|
+
const P = 6;
|
|
46
|
+
const d = transformD(dAbs, (p) => ({ x: p.x - mnX + P, y: p.y - mnY + P }));
|
|
47
|
+
patch[m.id] = {
|
|
48
|
+
...e, x: Math.round(mnX - P), y: Math.round(mnY - P),
|
|
49
|
+
w: Math.max(4, Math.round(mxX - mnX + P * 2)), h: Math.max(4, Math.round(mxY - mnY + P * 2)),
|
|
50
|
+
data: { ...e.data, d },
|
|
51
|
+
};
|
|
52
|
+
inked += 1;
|
|
53
|
+
} else {
|
|
54
|
+
// 只换座:中心过矩阵,尺寸不动
|
|
55
|
+
const c0 = { x: e.x + m.w / 2, y: e.y + m.h / 2 };
|
|
56
|
+
const c1 = T(c0);
|
|
57
|
+
patch[m.id] = { ...e, x: Math.round(c1.x - m.w / 2), y: Math.round(c1.y - m.h / 2) };
|
|
58
|
+
seated += 1;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return { patch, inked, seated, center: { x: Math.round(C.x), y: Math.round(C.y) } };
|
|
62
|
+
}
|
package/server/lib/canvas-id.js
CHANGED
|
@@ -22,7 +22,24 @@ export function normalizeCanvasId(raw) {
|
|
|
22
22
|
* 返回 { x, y, w, h, anchorId, ids } 或 null;anchorId = 最右那个(贴右侧摆用)。
|
|
23
23
|
* sizeOf(id, entry) 由调用方给(estimateSizeOn 绑着 board,不在这层 import)。
|
|
24
24
|
*/
|
|
25
|
-
|
|
25
|
+
/**
|
|
26
|
+
* 查 tag 时的归一:剥掉前导 `#`(2026-08-30)。
|
|
27
|
+
*
|
|
28
|
+
* ⛔ 我们在 write_on_board 的 `near`、read_board 的 `tag` 等好几处描述里明写
|
|
29
|
+
* 「Canvas id or **#tag**」,而查询侧一直是 `e.tag === raw` 精确比对,从来没剥过
|
|
30
|
+
* 这个井号 —— 全库统计:带 # 的写法 5 处,5 处全废;不带 # 的 2773 处全通。
|
|
31
|
+
* 「注释/描述声称做了、代码没做」的第三例。
|
|
32
|
+
*
|
|
33
|
+
* 只用在**查**的一侧。写的一侧(write_on_board 的 tag 参数)走 TAG_RE,本来就
|
|
34
|
+
* 收不下 # —— 那是 schema 报错,是响的,不用管。
|
|
35
|
+
*/
|
|
36
|
+
export function bareTag(raw) {
|
|
37
|
+
return String(raw ?? '').trim().replace(/^#+/, '');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function tagEnvelope(board, rawTag, sizeOf) {
|
|
41
|
+
const tag = bareTag(rawTag);
|
|
42
|
+
if (!tag) return null;
|
|
26
43
|
const hits = Object.entries(board?.objects || {})
|
|
27
44
|
.filter(([, e]) => e?.tag === tag && Number.isFinite(e?.x));
|
|
28
45
|
if (!hits.length) return null;
|