@xiaobuyu/nodesign 0.0.6 → 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.
Files changed (383) hide show
  1. package/README.md +266 -79
  2. package/README.zh-CN.md +254 -67
  3. package/package.json +2 -2
  4. package/server/api/admin.js +7 -0
  5. package/server/api/assets/docx-page.js +5 -4
  6. package/server/api/assets/helpers.js +26 -0
  7. package/server/api/assets/notes.js +50 -0
  8. package/server/api/assets.js +31 -38
  9. package/server/api/board.js +29 -1
  10. package/server/api/browse.js +3 -2
  11. package/server/api/chatai.js +9 -8
  12. package/server/api/exports/cards.js +7 -6
  13. package/server/api/exports/docx-pdf.js +4 -1
  14. package/server/api/instruction.js +3 -3
  15. package/server/api/local.js +10 -8
  16. package/server/api/projects.js +19 -3
  17. package/server/api/publish.js +3 -2
  18. package/server/api/sessions-rewind.js +220 -0
  19. package/server/api/sessions.js +58 -194
  20. package/server/api/stage.js +35 -0
  21. package/server/api/turn-model-switch.js +109 -0
  22. package/server/api/turn.js +52 -88
  23. package/server/auth/middleware.js +29 -6
  24. package/server/auth/tier.js +27 -7
  25. package/server/auth/users-store.js +14 -1
  26. package/server/engine/agent/actor-trail.js +110 -0
  27. package/server/engine/agent/agent-shared.js +22 -52
  28. package/server/engine/agent/beat-state.js +61 -0
  29. package/server/engine/agent/cast.js +322 -0
  30. package/server/engine/agent/events.js +18 -1
  31. package/server/engine/agent/hooks/beat-gate.js +73 -0
  32. package/server/engine/agent/hooks/failure.js +11 -1
  33. package/server/engine/agent/hooks/lifecycle.js +16 -41
  34. package/server/engine/agent/hooks/post-guidance.js +4 -38
  35. package/server/engine/agent/hooks/post-trim.js +1 -0
  36. package/server/engine/agent/hooks/pre-board-dirty.js +29 -0
  37. package/server/engine/agent/hooks/pre-defaults.js +206 -2
  38. package/server/engine/agent/hooks/pre-injectors.js +19 -2
  39. package/server/engine/agent/hooks/pre-peer-guard.js +88 -0
  40. package/server/engine/agent/hooks/pre-unknown-params.js +82 -0
  41. package/server/engine/agent/hooks/pre-workspace-scope-guard.js +33 -0
  42. package/server/engine/agent/hooks/resident-role-lifecycle.js +100 -0
  43. package/server/engine/agent/hooks/slot-alias.js +30 -0
  44. package/server/engine/agent/hooks/user-prompt-submit.js +208 -47
  45. package/server/engine/agent/hooks.js +80 -14
  46. package/server/engine/agent/isolation.js +25 -2
  47. package/server/engine/agent/memory-config.js +87 -0
  48. package/server/engine/agent/model-context.js +102 -18
  49. package/server/engine/agent/model-table.js +361 -58
  50. package/server/engine/agent/prompts/nodesign-prelude.md +579 -70
  51. package/server/engine/agent/prompts/tools/direct-edit-protocol.md +1 -1
  52. package/server/engine/agent/prompts/tools/generate-image-cookbook.md +26 -9
  53. package/server/engine/agent/prompts/tools/paint-still-cookbook.md +1 -1
  54. package/server/engine/agent/prompts/tools/site-reference.md +15 -4
  55. package/server/engine/agent/role-card.js +175 -0
  56. package/server/engine/agent/rp-mode.js +17 -0
  57. package/server/engine/agent/session-loop.js +55 -32
  58. package/server/engine/agent/stage-status.js +73 -0
  59. package/server/engine/agent/subagent-flight.js +73 -0
  60. package/server/engine/agent/system-prompts.js +41 -0
  61. package/server/engine/agent/tool-input-stream.js +165 -0
  62. package/server/engine/agents/explorer.md +1 -1
  63. package/server/engine/agents/index.js +1 -1
  64. package/server/engine/mcp/actor.js +59 -0
  65. package/server/engine/mcp/capability-gate.js +21 -2
  66. package/server/engine/mcp/index.js +118 -33
  67. package/server/engine/mcp/mode-profile.js +139 -0
  68. package/server/engine/mcp/server-name.js +19 -0
  69. package/server/engine/mcp/tools/artifact-session.js +2 -1
  70. package/server/engine/mcp/tools/browse-computer.js +2 -1
  71. package/server/engine/mcp/tools/browse-find-batch.js +95 -16
  72. package/server/engine/mcp/tools/browse.js +25 -3
  73. package/server/engine/mcp/tools/cast-role.js +167 -0
  74. package/server/engine/mcp/tools/crystallize-skill.js +1 -1
  75. package/server/engine/mcp/tools/draw-trend.js +104 -0
  76. package/server/engine/mcp/tools/edit-board-schema.js +56 -0
  77. package/server/engine/mcp/tools/edit-board.js +569 -0
  78. package/server/engine/mcp/tools/generate-image.js +50 -70
  79. package/server/engine/mcp/tools/helpers/codex-imagegen.js +139 -0
  80. package/server/engine/mcp/tools/helpers/rembg.js +7 -2
  81. package/server/engine/mcp/tools/helpers/shot-pipeline.js +109 -0
  82. package/server/engine/mcp/tools/list-pages.js +11 -5
  83. package/server/engine/mcp/tools/look-at-board.js +4 -3
  84. package/server/engine/mcp/tools/open-sheet.js +242 -0
  85. package/server/engine/mcp/tools/organize-board.js +45 -6
  86. package/server/engine/mcp/tools/paint-still.js +1 -1
  87. package/server/engine/mcp/tools/pin-to-board.js +103 -19
  88. package/server/engine/mcp/tools/read-board.js +123 -16
  89. package/server/engine/mcp/tools/read-tavern-json.js +87 -19
  90. package/server/engine/mcp/tools/report-issue.js +15 -7
  91. package/server/engine/mcp/tools/role-memory.js +83 -0
  92. package/server/engine/mcp/tools/roll-dice.js +82 -0
  93. package/server/engine/mcp/tools/roll-film.js +1 -1
  94. package/server/engine/mcp/tools/screenshot.js +19 -9
  95. package/server/engine/mcp/tools/set-vars.js +150 -0
  96. package/server/engine/mcp/tools/sheet-replan.js +46 -0
  97. package/server/engine/mcp/tools/web-search.js +12 -16
  98. package/server/engine/mcp/tools/write-on-board-flow.js +108 -0
  99. package/server/engine/mcp/tools/write-on-board-place.js +211 -0
  100. package/server/engine/mcp/tools/write-on-board-role-anchor.js +48 -0
  101. package/server/engine/mcp/tools/write-on-board-schema.js +125 -0
  102. package/server/engine/mcp/tools/write-on-board.js +568 -99
  103. package/server/engine/plugins/nodesign/skills/blackboard-rp/SKILL.md +215 -0
  104. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/SKILL.md +14 -7
  105. package/server/engine/plugins/nodesign/skills/docx-craft/SKILL.md +44 -5
  106. package/server/engine/plugins/nodesign/skills/docx-craft/references/token-schema.md +16 -0
  107. package/server/engine/plugins/nodesign/skills/site-craft/SKILL.md +24 -8
  108. package/server/engine/plugins/nodesign/skills/story-craft/SKILL.md +94 -0
  109. package/server/engine/plugins/nodesign/skills/story-import/SKILL.md +112 -0
  110. package/server/engine/plugins/nodesign/skills/story-intimacy/SKILL.md +72 -0
  111. package/server/engine/plugins/nodesign/skills/story-voice/SKILL.md +66 -0
  112. package/server/engine/plugins/nodesign/skills/story-voice/voices//345/206/267/347/241/254.md +33 -0
  113. package/server/engine/plugins/nodesign/skills/story-voice/voices//346/203/205/346/204/237/347/233/264/347/273/231.md +42 -0
  114. package/server/engine/plugins/nodesign/skills/story-voice/voices//346/204/217/350/257/206/346/265/201.md +42 -0
  115. package/server/engine/plugins/nodesign/skills/story-voice/voices//346/255/246/344/276/240/347/231/275/350/257/235.md +34 -0
  116. package/server/engine/plugins/nodesign/skills/story-voice/voices//347/224/237/346/264/273/346/265/201.md +35 -0
  117. package/server/engine/plugins/nodesign/skills/story-voice/voices//347/231/275/346/217/217.md +37 -0
  118. package/server/engine/plugins/nodesign/skills/story-voice/voices//350/210/236/345/217/260/345/211/247.md +32 -0
  119. package/server/engine/plugins/nodesign/skills/story-voice/voices//351/230/266/346/256/265/345/210/207/346/215/242.md +42 -0
  120. package/server/engine/runs/board-seater.js +272 -0
  121. package/server/engine/runs/board-tasklist.js +90 -48
  122. package/server/engine/runs/turn-relay.js +24 -2
  123. package/server/engine/runs/user-chalk-echo.js +44 -0
  124. package/server/index.js +2 -2
  125. package/server/lib/artifact-target.js +31 -14
  126. package/server/lib/board-anchor.js +49 -0
  127. package/server/lib/board-dirty.js +93 -0
  128. package/server/lib/board-excerpt.js +80 -0
  129. package/server/lib/board-follow.js +111 -0
  130. package/server/lib/board-groups.js +1 -5
  131. package/server/lib/board-hero.js +3 -1
  132. package/server/lib/board-kind-sizes.js +38 -3
  133. package/server/lib/board-lanes.js +58 -0
  134. package/server/lib/board-obstacles.js +80 -0
  135. package/server/lib/board-relations.js +5 -1
  136. package/server/lib/board-sheets.js +467 -0
  137. package/server/lib/board-shelf.js +86 -0
  138. package/server/lib/board-transform.js +62 -0
  139. package/server/lib/canvas-id.js +46 -4
  140. package/server/lib/chalk-flow.js +88 -0
  141. package/server/lib/chalk-rewrite.js +63 -0
  142. package/server/lib/chalk-size-pref.js +48 -0
  143. package/server/lib/chalk.js +42 -3
  144. package/server/lib/export-collect.js +3 -1
  145. package/server/lib/ingress/forward-openai-chat.js +16 -4
  146. package/server/lib/ingress/openai-chat.js +23 -12
  147. package/server/lib/ingress/session-routes.js +3 -3
  148. package/server/lib/ingress/stream-watchdog.js +53 -0
  149. package/server/lib/ingress/upstream-billing.js +15 -0
  150. package/server/lib/ingress/upstream-error-hints.js +36 -0
  151. package/server/lib/issues-store.js +3 -1
  152. package/server/lib/json-preview.js +58 -0
  153. package/server/lib/kinds/docx.js +11 -4
  154. package/server/lib/kinds/index.js +6 -16
  155. package/server/lib/kinds/site.js +40 -7
  156. package/server/lib/model-ingress.js +26 -1
  157. package/server/lib/moderation.js +10 -5
  158. package/server/lib/quota.js +26 -6
  159. package/server/lib/rect.js +50 -0
  160. package/server/lib/rewrite-refs.js +110 -0
  161. package/server/lib/screen.js +36 -0
  162. package/server/lib/sheet-state.js +19 -0
  163. package/server/lib/sketch-array.js +118 -0
  164. package/server/lib/sketch-hatch.js +99 -0
  165. package/server/lib/sketch-layout.js +269 -69
  166. package/server/lib/sketch-shapes.js +152 -0
  167. package/server/lib/sketch-stencils.js +170 -0
  168. package/server/lib/ssrf-guard.js +9 -0
  169. package/server/lib/state-table.js +237 -0
  170. package/server/lib/state-trend.js +94 -0
  171. package/server/lib/state-triggers.js +199 -0
  172. package/server/lib/task-scan.js +18 -0
  173. package/server/lib/tavern-json.js +48 -0
  174. package/server/lib/user-uploads.js +88 -0
  175. package/server/projects/board-limits.js +5 -3
  176. package/server/projects/board-sanitize.js +220 -10
  177. package/server/projects/board-store.js +137 -7
  178. package/server/projects/memory-migration.js +85 -0
  179. package/server/projects/move-entry.js +11 -1
  180. package/server/projects/session-jsonl.js +110 -0
  181. package/server/projects/store.js +56 -3
  182. package/server/projects/ui-config.js +2 -1
  183. package/server/projects/viewpoint-store.js +35 -3
  184. package/server/projects/workspace-slots.js +30 -0
  185. package/server/projects/workspace-templates.js +42 -10
  186. package/server/projects/workspace.js +25 -26
  187. package/server/runtime/capabilities.js +6 -2
  188. package/server/services/rembg-launcher.js +5 -2
  189. package/server/shared/locales.js +70 -0
  190. package/server/shared/messages-en.js +69 -0
  191. package/server/shared/messages.js +65 -0
  192. package/server/ws/broker.js +3 -0
  193. package/server/ws/index.js +43 -3
  194. package/web/README.md +1 -1
  195. package/web/dist/assets/{BrowserWindow-P_vIdh-G.js → BrowserWindow-CcaGRWz5.js} +1 -1
  196. package/web/dist/assets/DeckWindow-BIr2KGFT.js +6 -0
  197. package/web/dist/assets/{DocxWindow-DSAU1-tD.js → DocxWindow-B9KZmU5z.js} +2 -2
  198. package/web/dist/assets/{MermaidBlock-DR1GikCv.js → MermaidBlock-DrfRnz8E.js} +43 -43
  199. package/web/dist/assets/{SiteWindow-92uBuRRR.js → SiteWindow-CJOjCF7d.js} +6 -6
  200. package/web/dist/assets/{abnfDiagram-VCTEODGH-uA8Esix4.js → abnfDiagram-VCTEODGH-CUZbQGIg.js} +1 -1
  201. package/web/dist/assets/{arc-D_khH_xN.js → arc-C1-HSUny.js} +1 -1
  202. package/web/dist/assets/{architectureDiagram-5GKGNRK7-DydJHqOt.js → architectureDiagram-5GKGNRK7-B5nwvu60.js} +2 -2
  203. package/web/dist/assets/{blockDiagram-NRAW4CY4-B7wBbKiW.js → blockDiagram-NRAW4CY4-uPPuFLwR.js} +1 -1
  204. package/web/dist/assets/{c4Diagram-UCG6FXSJ-6mrH0x7Y.js → c4Diagram-UCG6FXSJ-C0mKBFlz.js} +1 -1
  205. package/web/dist/assets/channel-BxpWNOGU.js +1 -0
  206. package/web/dist/assets/{chunk-2Q5K7J3B-Cuikho5Q.js → chunk-2Q5K7J3B-Cs0rlZ4q.js} +1 -1
  207. package/web/dist/assets/{chunk-5VM5RSS4-3-KZ5AUz.js → chunk-5VM5RSS4-DtR63oYb.js} +1 -1
  208. package/web/dist/assets/{chunk-F27PBJKO-Bkwq2gGU.js → chunk-F27PBJKO-B3MRdezJ.js} +1 -1
  209. package/web/dist/assets/{chunk-G27WJ6UU-BLyQzdGE.js → chunk-G27WJ6UU-BtwntwpS.js} +1 -1
  210. package/web/dist/assets/{chunk-JWPE2WC7-B7HxxQds.js → chunk-JWPE2WC7-BHsTe5PD.js} +1 -1
  211. package/web/dist/assets/{chunk-LCL6LL3I-CtXrnUwF.js → chunk-LCL6LL3I-knY15Qdm.js} +1 -1
  212. package/web/dist/assets/{chunk-POPQ4Y6H-dxddZlXt.js → chunk-POPQ4Y6H-C51tgK_I.js} +1 -1
  213. package/web/dist/assets/{chunk-SVP7TREG-DpkmrgHn.js → chunk-SVP7TREG-CnwRZmF_.js} +1 -1
  214. package/web/dist/assets/{chunk-XXDRQBXY-BwZ7RbpQ.js → chunk-XXDRQBXY-BtXzVF-_.js} +1 -1
  215. package/web/dist/assets/classDiagram-DTDB5LWJ-DUEKmDfo.js +1 -0
  216. package/web/dist/assets/classDiagram-v2-JRS7N3AN-DUEKmDfo.js +1 -0
  217. package/web/dist/assets/{cose-bilkent-JH36ORCC-DGFOsMUt.js → cose-bilkent-JH36ORCC-CzWlGP6i.js} +1 -1
  218. package/web/dist/assets/{cynefin-OW5HDTMX-DUwn3ehS.js → cynefin-OW5HDTMX-vK64EGSv.js} +1 -1
  219. package/web/dist/assets/{cynefinDiagram-5FMLGOSQ-DlLAOZ9x.js → cynefinDiagram-5FMLGOSQ-Bs3g1Gvh.js} +1 -1
  220. package/web/dist/assets/{dagre-3AP2YEHR-CyourLfz.js → dagre-3AP2YEHR-BhgkYe8V.js} +1 -1
  221. package/web/dist/assets/{diagram-S7CK7UJ4-BTMbx2np.js → diagram-S7CK7UJ4-C_6mvD2V.js} +1 -1
  222. package/web/dist/assets/{diagram-UQ7AKVKN-UK94_1-P.js → diagram-UQ7AKVKN-qW7tssUW.js} +1 -1
  223. package/web/dist/assets/{diagram-VSXAHHWV-C7zS-v9v.js → diagram-VSXAHHWV-txJPHpvG.js} +1 -1
  224. package/web/dist/assets/{diagram-VX7I27RA-DKJRa6CK.js → diagram-VX7I27RA-CQTMsCnL.js} +1 -1
  225. package/web/dist/assets/{diagram-Z3DM3KII-kUbAfGAp.js → diagram-Z3DM3KII--cb1hoE6.js} +1 -1
  226. package/web/dist/assets/{ebnfDiagram-PWID7BFC-CVW16_vb.js → ebnfDiagram-PWID7BFC-Da6lDU77.js} +1 -1
  227. package/web/dist/assets/{erDiagram-SSCWMZ5O-BRQMFQ5y.js → erDiagram-SSCWMZ5O-DGCMrsQn.js} +1 -1
  228. package/web/dist/assets/{flowDiagram-A5DVABFB-S34LpLtK.js → flowDiagram-A5DVABFB-GdOL_qno.js} +1 -1
  229. package/web/dist/assets/{ganttDiagram-EL5Y4UJY-BFiqk1-G.js → ganttDiagram-EL5Y4UJY-Cx1F_jBK.js} +4 -4
  230. package/web/dist/assets/{gitGraphDiagram-WWUBYQGX-LZsrdyyv.js → gitGraphDiagram-WWUBYQGX-B5_6JhR3.js} +1 -1
  231. package/web/dist/assets/hand-CuvZZQGW.webp +0 -0
  232. package/web/dist/assets/index-BYKfMnmv.js +2759 -0
  233. package/web/dist/assets/index-CW7VsrVg.css +1 -0
  234. package/web/dist/assets/{infoDiagram-RXCK75RN-0DvN6KIp.js → infoDiagram-RXCK75RN-CjvKpSPA.js} +1 -1
  235. package/web/dist/assets/{ishikawaDiagram-5VMMS53U-CaY3A-Bx.js → ishikawaDiagram-5VMMS53U-CxLoAPhx.js} +1 -1
  236. package/web/dist/assets/{journeyDiagram-EYS64GPL-BBeiaCrj.js → journeyDiagram-EYS64GPL-BfvS1upJ.js} +1 -1
  237. package/web/dist/assets/{kanban-definition-3QL26DDD-ux42DRXQ.js → kanban-definition-3QL26DDD-DKPPu0w4.js} +1 -1
  238. package/web/dist/assets/{layout-CnXiF_NB.js → layout-Dhgck-Zo.js} +1 -1
  239. package/web/dist/assets/{linear-BMW0u6G6.js → linear-DMSQ-aUw.js} +1 -1
  240. package/web/dist/assets/lxgw-nd-app-bold-D1pKA1bR.woff2 +0 -0
  241. package/web/dist/assets/lxgw-nd-app-regular-BDe3d26v.woff2 +0 -0
  242. package/web/dist/assets/lxgw-nd-bold-Bufvlg03.woff2 +0 -0
  243. package/web/dist/assets/lxgw-nd-regular-DQwoI4OP.woff2 +0 -0
  244. package/web/dist/assets/lxgwwenkaiscreen-subset-100-DLPe7tEt.woff2 +0 -0
  245. package/web/dist/assets/lxgwwenkaiscreen-subset-101-d3VsZKWT.woff2 +0 -0
  246. package/web/dist/assets/lxgwwenkaiscreen-subset-102-ClzJuLq6.woff2 +0 -0
  247. package/web/dist/assets/lxgwwenkaiscreen-subset-103-DuDnQ6K-.woff2 +0 -0
  248. package/web/dist/assets/lxgwwenkaiscreen-subset-104-kPGW3Eem.woff2 +0 -0
  249. package/web/dist/assets/lxgwwenkaiscreen-subset-105-BzD54h-0.woff2 +0 -0
  250. package/web/dist/assets/lxgwwenkaiscreen-subset-106-BQ7fIzlx.woff2 +0 -0
  251. package/web/dist/assets/lxgwwenkaiscreen-subset-107-D8tDfFHI.woff2 +0 -0
  252. package/web/dist/assets/lxgwwenkaiscreen-subset-108-tGhkpwep.woff2 +0 -0
  253. package/web/dist/assets/lxgwwenkaiscreen-subset-109-7SMVSrly.woff2 +0 -0
  254. package/web/dist/assets/lxgwwenkaiscreen-subset-110-_uDKrFed.woff2 +0 -0
  255. package/web/dist/assets/lxgwwenkaiscreen-subset-111-Bio825RE.woff2 +0 -0
  256. package/web/dist/assets/lxgwwenkaiscreen-subset-112-D2ee0i-C.woff2 +0 -0
  257. package/web/dist/assets/lxgwwenkaiscreen-subset-113-JPFVDrga.woff2 +0 -0
  258. package/web/dist/assets/lxgwwenkaiscreen-subset-114-BcfxYA4w.woff2 +0 -0
  259. package/web/dist/assets/lxgwwenkaiscreen-subset-115-B1HeeOsd.woff2 +0 -0
  260. package/web/dist/assets/lxgwwenkaiscreen-subset-116-hY12tXpq.woff2 +0 -0
  261. package/web/dist/assets/lxgwwenkaiscreen-subset-117-BvuZwmre.woff2 +0 -0
  262. package/web/dist/assets/lxgwwenkaiscreen-subset-118-tzvs_H7j.woff2 +0 -0
  263. package/web/dist/assets/lxgwwenkaiscreen-subset-119-J2DWuYDN.woff2 +0 -0
  264. package/web/dist/assets/lxgwwenkaiscreen-subset-21-pDubCJV5.woff2 +0 -0
  265. package/web/dist/assets/lxgwwenkaiscreen-subset-22-ChAPMpqp.woff2 +0 -0
  266. package/web/dist/assets/lxgwwenkaiscreen-subset-23-BBx3oNpT.woff2 +0 -0
  267. package/web/dist/assets/lxgwwenkaiscreen-subset-24-CYt-Oagk.woff2 +0 -0
  268. package/web/dist/assets/lxgwwenkaiscreen-subset-25-Cx3j-ZUv.woff2 +0 -0
  269. package/web/dist/assets/lxgwwenkaiscreen-subset-26-B9AfZ3LL.woff2 +0 -0
  270. package/web/dist/assets/lxgwwenkaiscreen-subset-27-Bh2s9MNE.woff2 +0 -0
  271. package/web/dist/assets/lxgwwenkaiscreen-subset-28-C02JPoKP.woff2 +0 -0
  272. package/web/dist/assets/lxgwwenkaiscreen-subset-29-T_tbUGhk.woff2 +0 -0
  273. package/web/dist/assets/lxgwwenkaiscreen-subset-30-DMfyNzhz.woff2 +0 -0
  274. package/web/dist/assets/lxgwwenkaiscreen-subset-31-DFfdzbkg.woff2 +0 -0
  275. package/web/dist/assets/lxgwwenkaiscreen-subset-32-Bs1fz2iW.woff2 +0 -0
  276. package/web/dist/assets/lxgwwenkaiscreen-subset-33-cpB96OkZ.woff2 +0 -0
  277. package/web/dist/assets/lxgwwenkaiscreen-subset-34-BhinPYWX.woff2 +0 -0
  278. package/web/dist/assets/lxgwwenkaiscreen-subset-35-D_njRQqA.woff2 +0 -0
  279. package/web/dist/assets/lxgwwenkaiscreen-subset-36-DqXlLE3t.woff2 +0 -0
  280. package/web/dist/assets/lxgwwenkaiscreen-subset-37-j5LbvMph.woff2 +0 -0
  281. package/web/dist/assets/lxgwwenkaiscreen-subset-38-oO4jWdKm.woff2 +0 -0
  282. package/web/dist/assets/lxgwwenkaiscreen-subset-39-CabsX4rw.woff2 +0 -0
  283. package/web/dist/assets/lxgwwenkaiscreen-subset-4-PatS3YHh.woff2 +0 -0
  284. package/web/dist/assets/lxgwwenkaiscreen-subset-40-yrLg3C3e.woff2 +0 -0
  285. package/web/dist/assets/lxgwwenkaiscreen-subset-41-DBXmCQhc.woff2 +0 -0
  286. package/web/dist/assets/lxgwwenkaiscreen-subset-42-Bqij_UiQ.woff2 +0 -0
  287. package/web/dist/assets/lxgwwenkaiscreen-subset-43-D3kp0Ntc.woff2 +0 -0
  288. package/web/dist/assets/lxgwwenkaiscreen-subset-44-B_BNGvK4.woff2 +0 -0
  289. package/web/dist/assets/lxgwwenkaiscreen-subset-45-CZc0cQWs.woff2 +0 -0
  290. package/web/dist/assets/lxgwwenkaiscreen-subset-46-CHQX5Kd9.woff2 +0 -0
  291. package/web/dist/assets/lxgwwenkaiscreen-subset-47-Ci8gLrHA.woff2 +0 -0
  292. package/web/dist/assets/lxgwwenkaiscreen-subset-48-run3ugD3.woff2 +0 -0
  293. package/web/dist/assets/lxgwwenkaiscreen-subset-49-DgfiUb6u.woff2 +0 -0
  294. package/web/dist/assets/lxgwwenkaiscreen-subset-5-B0CtGbX_.woff2 +0 -0
  295. package/web/dist/assets/lxgwwenkaiscreen-subset-50-CdCG-POr.woff2 +0 -0
  296. package/web/dist/assets/lxgwwenkaiscreen-subset-51-L2okeQZ2.woff2 +0 -0
  297. package/web/dist/assets/lxgwwenkaiscreen-subset-52-BZAqyGJW.woff2 +0 -0
  298. package/web/dist/assets/lxgwwenkaiscreen-subset-53-B0xWqLeU.woff2 +0 -0
  299. package/web/dist/assets/lxgwwenkaiscreen-subset-54-BZVQX-EA.woff2 +0 -0
  300. package/web/dist/assets/lxgwwenkaiscreen-subset-55-093mkIUJ.woff2 +0 -0
  301. package/web/dist/assets/lxgwwenkaiscreen-subset-56-CbhWG8G-.woff2 +0 -0
  302. package/web/dist/assets/lxgwwenkaiscreen-subset-57-pLQuvo6c.woff2 +0 -0
  303. package/web/dist/assets/lxgwwenkaiscreen-subset-58-BHhoY50x.woff2 +0 -0
  304. package/web/dist/assets/lxgwwenkaiscreen-subset-59-D1_6DuIQ.woff2 +0 -0
  305. package/web/dist/assets/lxgwwenkaiscreen-subset-6-BnAelmYU.woff2 +0 -0
  306. package/web/dist/assets/lxgwwenkaiscreen-subset-60-HDBIuLTZ.woff2 +0 -0
  307. package/web/dist/assets/lxgwwenkaiscreen-subset-61-C-c13SeU.woff2 +0 -0
  308. package/web/dist/assets/lxgwwenkaiscreen-subset-62-kHRVOhZ2.woff2 +0 -0
  309. package/web/dist/assets/lxgwwenkaiscreen-subset-63-C0MCsvFv.woff2 +0 -0
  310. package/web/dist/assets/lxgwwenkaiscreen-subset-64-DZekguCj.woff2 +0 -0
  311. package/web/dist/assets/lxgwwenkaiscreen-subset-65-CtP6c3IU.woff2 +0 -0
  312. package/web/dist/assets/lxgwwenkaiscreen-subset-66-DLVWoMSf.woff2 +0 -0
  313. package/web/dist/assets/lxgwwenkaiscreen-subset-67-BVuOnayH.woff2 +0 -0
  314. package/web/dist/assets/lxgwwenkaiscreen-subset-68-DpxQ6Mnx.woff2 +0 -0
  315. package/web/dist/assets/lxgwwenkaiscreen-subset-69-qesBxLmC.woff2 +0 -0
  316. package/web/dist/assets/lxgwwenkaiscreen-subset-70-BBt1xN7h.woff2 +0 -0
  317. package/web/dist/assets/lxgwwenkaiscreen-subset-71-BWqZacwB.woff2 +0 -0
  318. package/web/dist/assets/lxgwwenkaiscreen-subset-72-B1wP_Yst.woff2 +0 -0
  319. package/web/dist/assets/lxgwwenkaiscreen-subset-73-C99T7jNZ.woff2 +0 -0
  320. package/web/dist/assets/lxgwwenkaiscreen-subset-74-DrUTsWBe.woff2 +0 -0
  321. package/web/dist/assets/lxgwwenkaiscreen-subset-75-C4M5XXQe.woff2 +0 -0
  322. package/web/dist/assets/lxgwwenkaiscreen-subset-76-Bq8spVWF.woff2 +0 -0
  323. package/web/dist/assets/lxgwwenkaiscreen-subset-77-D9vNupE2.woff2 +0 -0
  324. package/web/dist/assets/lxgwwenkaiscreen-subset-78-D6th7Gnu.woff2 +0 -0
  325. package/web/dist/assets/lxgwwenkaiscreen-subset-79-bezM85cS.woff2 +0 -0
  326. package/web/dist/assets/lxgwwenkaiscreen-subset-80-DD8pnhG2.woff2 +0 -0
  327. package/web/dist/assets/lxgwwenkaiscreen-subset-81-N6jtSyBs.woff2 +0 -0
  328. package/web/dist/assets/lxgwwenkaiscreen-subset-82-D6c180bZ.woff2 +0 -0
  329. package/web/dist/assets/lxgwwenkaiscreen-subset-83-em17AadM.woff2 +0 -0
  330. package/web/dist/assets/lxgwwenkaiscreen-subset-84-CJ8gdrMK.woff2 +0 -0
  331. package/web/dist/assets/lxgwwenkaiscreen-subset-85-9PPGO1eB.woff2 +0 -0
  332. package/web/dist/assets/lxgwwenkaiscreen-subset-86-CjWPf-u5.woff2 +0 -0
  333. package/web/dist/assets/lxgwwenkaiscreen-subset-87-8D_X_yOC.woff2 +0 -0
  334. package/web/dist/assets/lxgwwenkaiscreen-subset-88-9vtKFPBx.woff2 +0 -0
  335. package/web/dist/assets/lxgwwenkaiscreen-subset-89-BhE5jAKX.woff2 +0 -0
  336. package/web/dist/assets/lxgwwenkaiscreen-subset-90-D2Npfm5E.woff2 +0 -0
  337. package/web/dist/assets/lxgwwenkaiscreen-subset-91-C6Ft1-Eu.woff2 +0 -0
  338. package/web/dist/assets/lxgwwenkaiscreen-subset-97-CaExyQP9.woff2 +0 -0
  339. package/web/dist/assets/lxgwwenkaiscreen-subset-98-CPoVbsUd.woff2 +0 -0
  340. package/web/dist/assets/lxgwwenkaiscreen-subset-99-ZUQZGVgH.woff2 +0 -0
  341. package/web/dist/assets/{mindmap-definition-FBJOCRG2-hUxSpdKn.js → mindmap-definition-FBJOCRG2-CalfL_Kn.js} +1 -1
  342. package/web/dist/assets/{pegDiagram-XKGWAZYB-aCaQPH0w.js → pegDiagram-XKGWAZYB-D9mVWz7l.js} +1 -1
  343. package/web/dist/assets/pending-edit-apply-kwMFWsUe.js +31 -0
  344. package/web/dist/assets/{pieDiagram-E7YTZNPT-CmOEZ8zg.js → pieDiagram-E7YTZNPT-DELzbZNS.js} +1 -1
  345. package/web/dist/assets/{quadrantDiagram-AXDQQJYC-VW6fAhsV.js → quadrantDiagram-AXDQQJYC-TZqp4mfA.js} +1 -1
  346. package/web/dist/assets/{railroadDiagram-O6MQD6OU-CKJ7Ba5K.js → railroadDiagram-O6MQD6OU-DxX59f0i.js} +1 -1
  347. package/web/dist/assets/{requirementDiagram-EFPCY7ZU-pNGkq9Vw.js → requirementDiagram-EFPCY7ZU-DFdsJLIe.js} +1 -1
  348. package/web/dist/assets/{rotate-cw-KAfChbGV.js → rotate-cw-62liz7LX.js} +1 -1
  349. package/web/dist/assets/{sankeyDiagram-P5KCCOFB-BIMmDTfZ.js → sankeyDiagram-P5KCCOFB-ByYcd3fG.js} +1 -1
  350. package/web/dist/assets/{sequenceDiagram-WJ2MYXX4-BBW7wzKY.js → sequenceDiagram-WJ2MYXX4-Bj-ml1jS.js} +1 -1
  351. package/web/dist/assets/{sizeCapture-X5ZJPWSS-DdkXmLYR.js → sizeCapture-X5ZJPWSS-KhsXThyD.js} +1 -1
  352. package/web/dist/assets/{square-dashed-mouse-pointer-CDUlhUP7.js → square-dashed-mouse-pointer-D_VrqVpy.js} +1 -1
  353. package/web/dist/assets/{stateDiagram-HBIQ2CUA-W313Uz16.js → stateDiagram-HBIQ2CUA-BJlpHfWU.js} +1 -1
  354. package/web/dist/assets/stateDiagram-v2-4QOOHH4V-CR2kYAH2.js +1 -0
  355. package/web/dist/assets/{swimlanes-XN3QIQJK-C29TL26K.js → swimlanes-XN3QIQJK-DUB9OJVs.js} +1 -1
  356. package/web/dist/assets/swimlanesDiagram-VK2B7HYN-BitOxnG2.js +8 -0
  357. package/web/dist/assets/{timeline-definition-24CTP7MA-CF0JMObX.js → timeline-definition-24CTP7MA-C03NX82A.js} +1 -1
  358. package/web/dist/assets/{vennDiagram-4TSXK5OY-DrDFsaH3.js → vennDiagram-4TSXK5OY-B9pjzngp.js} +1 -1
  359. package/web/dist/assets/{wardleyDiagram-VM6X3IG4-CVrnRIS_.js → wardleyDiagram-VM6X3IG4-UqWq_mcT.js} +1 -1
  360. package/web/dist/assets/{xychartDiagram-S5SC5T6Z-BvkN1zsT.js → xychartDiagram-S5SC5T6Z-D2pDKtOv.js} +1 -1
  361. package/web/dist/index.html +2 -2
  362. package/server/api/memory.js +0 -185
  363. package/server/engine/agent/prompts/tools/generate-image-cookbook.gemini-gateway.md +0 -583
  364. package/server/engine/mcp/tools/arrange-on-board.js +0 -124
  365. package/server/engine/mcp/tools/create-on-board.js +0 -152
  366. package/server/engine/mcp/tools/edit-sketch.js +0 -167
  367. package/server/engine/mcp/tools/record-decision.js +0 -140
  368. package/server/engine/mcp/tools/relate-on-board.js +0 -112
  369. package/server/engine/mcp/tools/sketch-on-board.js +0 -337
  370. package/server/engine/plugins/nodesign/skills/rp-craft/SKILL.md +0 -235
  371. 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
  372. package/server/engine/plugins/nodesign/skills/rp-craft//346/274/224/345/207/272.template.js +0 -268
  373. package/web/dist/assets/DeckWindow-BpLv5082.js +0 -16
  374. package/web/dist/assets/channel-DGkccMO1.js +0 -1
  375. package/web/dist/assets/classDiagram-DTDB5LWJ-BGscw3OD.js +0 -1
  376. package/web/dist/assets/classDiagram-v2-JRS7N3AN-BGscw3OD.js +0 -1
  377. package/web/dist/assets/index-C0KUf6Wd.css +0 -1
  378. package/web/dist/assets/index-GAamh1p4.js +0 -2152
  379. package/web/dist/assets/lxgw-nd-bold-DbRfiTgo.woff2 +0 -0
  380. package/web/dist/assets/lxgw-nd-regular-CpMw9Ehg.woff2 +0 -0
  381. package/web/dist/assets/pending-edit-apply-AcdOQ9Ij.js +0 -31
  382. package/web/dist/assets/stateDiagram-v2-4QOOHH4V-DDhJBZzf.js +0 -1
  383. package/web/dist/assets/swimlanesDiagram-VK2B7HYN-BfN-Xdmu.js +0 -8
@@ -0,0 +1,118 @@
1
+ /**
2
+ * server/lib/sketch-array.js —— 阵列 / 环列 / 镜像 / 散布(2026-08-30 画图能力线刀④)
3
+ *
4
+ * 复杂图案大多是简单元素的规律重复,而**等距和对称恰是模型最不擅长的算术**
5
+ * (画一圈椅子 = 12 次三角函数)。这四个算子把规律归机器、元素归 agent:
6
+ *
7
+ * repeat {n, dx, dy} 沿直线摆 n 份(栅栏、窗、台阶)
8
+ * ring {n, cx, cy} 绕圆心转 n 份(圆桌椅子、钟面、花瓣)
9
+ * mirror {axis, at} 对称补一份(画半只蝴蝶得整只)
10
+ * scatter {n, in:{…}} 区域内播撒 n 份、带大小抖动(星空、草地、碎石)
11
+ *
12
+ * 每一份都用**自己的种子**重生成笔迹 —— 抖动各不相同,看起来是手画了 n 遍,
13
+ * 不是图章盖了 n 下。scatter 的随机也是种子确定的:同一次调用重放结果一致。
14
+ */
15
+
16
+ import { UNIT, transformD } from './sketch-layout.js';
17
+
18
+ /** 与 sketch-layout 同款确定性随机(那边的没导出,够小就地一份) */
19
+ function rng(seedStr) {
20
+ let h = 0x811c9dc5;
21
+ for (let i = 0; i < seedStr.length; i += 1) { h ^= seedStr.charCodeAt(i); h = Math.imul(h, 0x01000193) >>> 0; }
22
+ let a = h >>> 0;
23
+ return () => {
24
+ a = (a + 0x6D2B79F5) >>> 0;
25
+ let t = a;
26
+ t = Math.imul(t ^ (t >>> 15), t | 1);
27
+ t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
28
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
29
+ };
30
+ }
31
+
32
+ /** 把 {rect,d}(d 局部于 rect)过一个**绝对坐标**变换,返回重建包围盒后的 {rect,d} */
33
+ function applyAbs(base, fn) {
34
+ let minX = Infinity; let minY = Infinity; let maxX = -Infinity; let maxY = -Infinity;
35
+ const dAbs = transformD(base.d, (p) => {
36
+ const q = fn({ x: base.rect.x + p.x, y: base.rect.y + p.y });
37
+ if (q.x < minX) minX = q.x; if (q.x > maxX) maxX = q.x;
38
+ if (q.y < minY) minY = q.y; if (q.y > maxY) maxY = q.y;
39
+ return q;
40
+ });
41
+ if (!Number.isFinite(minX)) return base;
42
+ const P = 6; // 与全族同款 pad(抖动和线宽的呼吸位;不补的话镜像副本比本尊瘦一圈)
43
+ const d = transformD(dAbs, (p) => ({ x: p.x - minX + P, y: p.y - minY + P }));
44
+ return { rect: { x: Math.round(minX - P), y: Math.round(minY - P), w: Math.max(4, Math.round(maxX - minX + P * 2)), h: Math.max(4, Math.round(maxY - minY + P * 2)) }, d };
45
+ }
46
+
47
+ /**
48
+ * 展开一个形状的算子。
49
+ * @param s 入参形状(可能带 repeat/ring/mirror/scatter;坐标全是格)
50
+ * @param makeOne (seedSuffix) => {rect, d} 重新生成一份本尊(各份各自的抖)
51
+ * @param key 形状名(副本叫 key-2, key-3…)
52
+ * @returns {Array<{key,rect,d}>} 或 { error }
53
+ */
54
+ export function expandModifiers(s, makeOne, key) {
55
+ const mods = ['repeat', 'ring', 'mirror', 'scatter'].filter((k) => s[k]);
56
+ if (!mods.length) return [{ key, ...makeOne('') }];
57
+ if (mods.length > 1) return { error: `形状 ${key}:repeat/ring/mirror/scatter 一次只能挂一个` };
58
+ const out = [];
59
+ const push = (i, piece) => out.push({ key: i === 0 ? key : `${key}-${i + 1}`, ...piece });
60
+
61
+ if (s.repeat) {
62
+ const { n, dx = 0, dy = 0 } = s.repeat;
63
+ if (!dx && !dy) return { error: `形状 ${key}:repeat 要给步长 dx/dy(格)——步长 0 是 n 份叠在原地` };
64
+ for (let i = 0; i < n; i += 1) {
65
+ const b = makeOne(`:${i}`);
66
+ push(i, { rect: { ...b.rect, x: Math.round(b.rect.x + dx * UNIT * i), y: Math.round(b.rect.y + dy * UNIT * i) }, d: b.d });
67
+ }
68
+ return out;
69
+ }
70
+ if (s.ring) {
71
+ const { n, cx, cy, upright = false } = s.ring;
72
+ const C = { x: cx * UNIT, y: cy * UNIT };
73
+ for (let i = 0; i < n; i += 1) {
74
+ const th = (Math.PI * 2 * i) / n;
75
+ const cos = Math.cos(th); const sin = Math.sin(th);
76
+ const b = makeOne(`:${i}`);
77
+ if (i === 0) { push(i, b); continue; }
78
+ if (upright) {
79
+ // 不转身,只沿圆周换座(侧视图里有重力的东西:帐篷、篝火边的人)
80
+ const bc = { x: b.rect.x + b.rect.w / 2, y: b.rect.y + b.rect.h / 2 };
81
+ const nc = {
82
+ x: C.x + (bc.x - C.x) * cos - (bc.y - C.y) * sin,
83
+ y: C.y + (bc.x - C.x) * sin + (bc.y - C.y) * cos,
84
+ };
85
+ push(i, { rect: { ...b.rect, x: Math.round(nc.x - b.rect.w / 2), y: Math.round(nc.y - b.rect.h / 2) }, d: b.d });
86
+ continue;
87
+ }
88
+ push(i, applyAbs(b, (p) => ({
89
+ x: C.x + (p.x - C.x) * cos - (p.y - C.y) * sin,
90
+ y: C.y + (p.x - C.x) * sin + (p.y - C.y) * cos,
91
+ })));
92
+ }
93
+ return out;
94
+ }
95
+ if (s.mirror) {
96
+ const { axis, at } = s.mirror;
97
+ const ax = at * UNIT;
98
+ push(0, makeOne(''));
99
+ push(1, applyAbs(makeOne(':m'), (p) => (axis === 'x' ? { x: 2 * ax - p.x, y: p.y } : { x: p.x, y: 2 * ax - p.y })));
100
+ return out;
101
+ }
102
+ if (s.scatter) {
103
+ const { n } = s.scatter;
104
+ const zone = s.scatter.in;
105
+ const R = { x: zone.x * UNIT, y: zone.y * UNIT, w: zone.w * UNIT, h: zone.h * UNIT };
106
+ const rand = rng(`scatter:${key}:${n}`);
107
+ for (let i = 0; i < n; i += 1) {
108
+ const b = makeOne(`:${i}`);
109
+ const k = 0.7 + rand() * 0.45; // 大小抖动 0.7~1.15
110
+ const scaled = applyAbs(b, (p) => ({ x: b.rect.x + (p.x - b.rect.x) * k, y: b.rect.y + (p.y - b.rect.y) * k }));
111
+ const x = R.x + rand() * Math.max(1, R.w - scaled.rect.w);
112
+ const y = R.y + rand() * Math.max(1, R.h - scaled.rect.h);
113
+ push(i, { rect: { ...scaled.rect, x: Math.round(x), y: Math.round(y) }, d: scaled.d });
114
+ }
115
+ return out;
116
+ }
117
+ return [{ key, ...makeOne('') }];
118
+ }
@@ -0,0 +1,99 @@
1
+ /**
2
+ * server/lib/sketch-hatch.js —— 排线填充(2026-08-30 画图能力线)
3
+ *
4
+ * 线稿世界里唯一体面的"上色":45° 手排线。闭合轮廓 → 一组扫描线段(偶奇规则),
5
+ * 端点带一点抖 —— 像人一笔一笔排出来的,不是激光刻的。
6
+ *
7
+ * 只对**闭合轮廓**生效(Z 收尾的子路径):往开放折线里灌填充没有定义良好的
8
+ * 内部,硬灌就是把问题藏起来 —— 调用方大声拒。
9
+ */
10
+
11
+ const f1 = (n) => Math.round(n * 10) / 10;
12
+
13
+ function rng(seedStr) {
14
+ let h = 0x811c9dc5;
15
+ for (let i = 0; i < seedStr.length; i += 1) { h ^= seedStr.charCodeAt(i); h = Math.imul(h, 0x01000193) >>> 0; }
16
+ let a = h >>> 0;
17
+ return () => {
18
+ a = (a + 0x6D2B79F5) >>> 0;
19
+ let t = a;
20
+ t = Math.imul(t ^ (t >>> 15), t | 1);
21
+ t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
22
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
23
+ };
24
+ }
25
+
26
+ /** 大写绝对 M/L/Q/C/Z → 多边形组(曲线采样 8 段;只收 Z 闭合的子路径) */
27
+ export function flattenClosed(d) {
28
+ const tokens = String(d).match(/[MLQCZ]|-?\d*\.?\d+(?:[eE][-+]?\d+)?/g) || [];
29
+ const polys = [];
30
+ let pts = []; let i = 0; let closedAny = false;
31
+ const num = () => Number(tokens[i++]);
32
+ const flush = (closed) => { if (closed && pts.length >= 3) { polys.push(pts); closedAny = true; } pts = []; };
33
+ while (i < tokens.length) {
34
+ const t = tokens[i++];
35
+ if (t === 'M') { flush(false); pts = [{ x: num(), y: num() }]; continue; }
36
+ if (t === 'L') { pts.push({ x: num(), y: num() }); continue; }
37
+ if (t === 'Q' || t === 'C') {
38
+ const cps = t === 'Q' ? [{ x: num(), y: num() }] : [{ x: num(), y: num() }, { x: num(), y: num() }];
39
+ const end = { x: num(), y: num() };
40
+ const a = pts[pts.length - 1] || end;
41
+ for (let k = 1; k <= 8; k += 1) {
42
+ const u = k / 8;
43
+ let p;
44
+ if (t === 'Q') {
45
+ const v = 1 - u;
46
+ p = { x: v * v * a.x + 2 * v * u * cps[0].x + u * u * end.x, y: v * v * a.y + 2 * v * u * cps[0].y + u * u * end.y };
47
+ } else {
48
+ const v = 1 - u;
49
+ p = {
50
+ x: v ** 3 * a.x + 3 * v * v * u * cps[0].x + 3 * v * u * u * cps[1].x + u ** 3 * end.x,
51
+ y: v ** 3 * a.y + 3 * v * v * u * cps[0].y + 3 * v * u * u * cps[1].y + u ** 3 * end.y,
52
+ };
53
+ }
54
+ pts.push(p);
55
+ }
56
+ continue;
57
+ }
58
+ if (t === 'Z') { flush(true); continue; }
59
+ }
60
+ flush(false);
61
+ return { polys, closedAny };
62
+ }
63
+
64
+ /**
65
+ * 多边形组 → 45° 排线 d。偶奇规则(洞是真洞:月牙的内弧不会被排穿)。
66
+ * @returns {string} 空串 = 没有可填的面积
67
+ */
68
+ export function hatchD(polys, seed = 'hatch', { spacing = 11, angle = Math.PI / 4 } = {}) {
69
+ if (!polys?.length) return '';
70
+ const cos = Math.cos(-angle); const sin = Math.sin(-angle);
71
+ const rot = polys.map((p) => p.map(({ x, y }) => ({ x: x * cos - y * sin, y: x * sin + y * cos })));
72
+ let minY = Infinity; let maxY = -Infinity;
73
+ for (const p of rot) for (const q of p) { if (q.y < minY) minY = q.y; if (q.y > maxY) maxY = q.y; }
74
+ if (!Number.isFinite(minY)) return '';
75
+ const rand = rng(seed);
76
+ const back = (x, y) => ({ x: x * Math.cos(angle) - y * Math.sin(angle), y: x * Math.sin(angle) + y * Math.cos(angle) });
77
+ const segs = [];
78
+ for (let y = minY + spacing * 0.6; y < maxY; y += spacing) {
79
+ const xs = [];
80
+ for (const p of rot) {
81
+ for (let k = 0; k < p.length; k += 1) {
82
+ const a = p[k]; const b = p[(k + 1) % p.length];
83
+ if ((a.y <= y && b.y > y) || (b.y <= y && a.y > y)) {
84
+ xs.push(a.x + ((y - a.y) / (b.y - a.y)) * (b.x - a.x));
85
+ }
86
+ }
87
+ }
88
+ xs.sort((m, n) => m - n);
89
+ for (let k = 0; k + 1 < xs.length; k += 2) {
90
+ if (xs[k + 1] - xs[k] < 3) continue; // 太窄的缝不排
91
+ const inset = 1.5; // 别顶到轮廓线
92
+ const j = () => (rand() * 2 - 1) * 0.9;
93
+ const a = back(xs[k] + inset + j(), y + j());
94
+ const b = back(xs[k + 1] - inset + j(), y + j());
95
+ segs.push(`M ${f1(a.x)} ${f1(a.y)} L ${f1(b.x)} ${f1(b.y)}`);
96
+ }
97
+ }
98
+ return segs.join(' ');
99
+ }
@@ -14,19 +14,24 @@
14
14
  * 全是纯函数;尺寸估算与 create-on-board 同一套公式。
15
15
  */
16
16
 
17
- export const UNIT = 24; // 1 网格 = 24 世界像素
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,用户定):黑板上的字要在 80%~100% 缩放下清晰可读。
20
- * 手写/md 正文 16px 世界像素在 0.8 倍下是 12.8 屏幕像素 —— 这是底线,所以节点
21
- * 字号不低于 md;一张图的尺寸要能在 0.8 倍下整张进一个普通视口(1400×900 屏
22
- * 1750×1125 世界像素)。超过 SKETCH_MAX 直接拒:一张图说一件事,大了就拆。
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 = { w: 1700, h: 1100 }; // 推荐上限(0.8 倍一屏)
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
- /** 手写字身位估算(抄 create-on-board / 前端 handleCreateText 同款) */
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
- const cols = wUnits ? Math.max(4, Math.floor((wUnits * UNIT) / (px * 1.05))) : Math.min(26, Math.max(6, t.length));
56
- const lines = Math.ceil(t.length / cols) + (t.match(/\n/g)?.length || 0);
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
- export function layoutNodes(nodes, { template = 'auto', cols = null } = {}) {
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
- let tpl = template;
157
- if (tpl === 'auto') tpl = nodes.some(n => n.at) ? 'free' : (nodes.length <= 4 ? 'column' : 'grid');
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
- const [center, ...rest] = nodes;
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
- export function bboxOf(rects) {
217
- let x0 = Infinity; let y0 = Infinity; let x1 = -Infinity; let y1 = -Infinity;
218
- for (const r of rects) {
219
- x0 = Math.min(x0, r.x); y0 = Math.min(y0, r.y); x1 = Math.max(x1, r.x + r.w); y1 = Math.max(y1, r.y + r.h);
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
- * 宏观落位:在 obstacles(同层已摆的矩形)中给一块 w×h 找地方。
226
- * - near:锚右侧(撞了往下让),让不开就锚下方
227
- * - 否则:内容最低边下面(与入座"新东西排底下"同一条起排线精神)
228
- */
229
- export function findSpot({ w, h, near = null, obstacles = [], contentBottom = 0, viewport = null }) {
230
- const hits = (x, y) => obstacles.some(o => !(x + w <= o.x || o.x + o.w <= x || y + h <= o.y || o.y + o.h <= y));
231
- // 用户视口里有空地就落在视口里(黑板是主窗口时,画在他眼前而不是让他去找)。
232
- // 阅读顺序纪律(08-23 真踩:第二张图落到第一张的**左边**):先挑不在已有内容左侧/
233
- // 上方的位置(顺着先左后右、先上后下长),实在没有才退而求其次。
234
- if (!near && viewport && viewport.w >= w + 24 && viewport.h >= h + 24) {
235
- const content = obstacles.length ? {
236
- x: Math.min(...obstacles.map(o => o.x)), y: Math.min(...obstacles.map(o => o.y)),
237
- } : null;
238
- const sx = Math.max(24, Math.round(viewport.w / 8)); const sy = Math.max(24, Math.round(viewport.h / 8));
239
- for (const strict of [true, false]) {
240
- for (let y = viewport.y + 12; y + h <= viewport.y + viewport.h - 12; y += sy) {
241
- for (let x = viewport.x + 12; x + w <= viewport.x + viewport.w - 12; x += sx) {
242
- if (strict && content && (x < content.x - 24 || y < content.y - 24)) continue;
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
- if (vp?.camera?.w && vp?.zoom) {
273
- const sw = vp.camera.w * vp.zoom; const sh = vp.camera.h * vp.zoom;
274
- return { w: Math.round(sw / 0.8), h: Math.round(sh / 0.8), screen: { w: Math.round(sw), h: Math.round(sh) } };
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 { ...SKETCH_FIT, screen: null };
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
  }