@songsid/agend 2.3.1 → 2.3.2-beta.10

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 (338) hide show
  1. package/THIRD_PARTY_NOTICES.md +40 -0
  2. package/dist/agent-cli.js +21 -2
  3. package/dist/agent-cli.js.map +1 -1
  4. package/dist/agent-endpoint.js +5 -0
  5. package/dist/agent-endpoint.js.map +1 -1
  6. package/dist/antigravity-usage-scan.d.ts +58 -0
  7. package/dist/antigravity-usage-scan.js +257 -0
  8. package/dist/antigravity-usage-scan.js.map +1 -0
  9. package/dist/antigravity-usage-worker.d.ts +1 -0
  10. package/dist/antigravity-usage-worker.js +10 -0
  11. package/dist/antigravity-usage-worker.js.map +1 -0
  12. package/dist/auth-api.js +4 -0
  13. package/dist/auth-api.js.map +1 -1
  14. package/dist/authority/authority-state.d.ts +4 -0
  15. package/dist/authority/authority-state.js +16 -10
  16. package/dist/authority/authority-state.js.map +1 -1
  17. package/dist/authority/session-authorizations.d.ts +7 -1
  18. package/dist/authority/session-authorizations.js +6 -4
  19. package/dist/authority/session-authorizations.js.map +1 -1
  20. package/dist/authority/web-authority.d.ts +19 -6
  21. package/dist/authority/web-authority.js +22 -8
  22. package/dist/authority/web-authority.js.map +1 -1
  23. package/dist/backend/kiro-identity.d.ts +64 -0
  24. package/dist/backend/kiro-identity.js +101 -0
  25. package/dist/backend/kiro-identity.js.map +1 -1
  26. package/dist/backend/kiro-retired-model.d.ts +62 -0
  27. package/dist/backend/kiro-retired-model.js +160 -0
  28. package/dist/backend/kiro-retired-model.js.map +1 -0
  29. package/dist/backend/kiro.d.ts +71 -2
  30. package/dist/backend/kiro.js +352 -11
  31. package/dist/backend/kiro.js.map +1 -1
  32. package/dist/backend/types.d.ts +59 -0
  33. package/dist/backend/types.js.map +1 -1
  34. package/dist/backend-signin-guide.d.ts +1 -0
  35. package/dist/backend-signin-guide.js +9 -0
  36. package/dist/backend-signin-guide.js.map +1 -0
  37. package/dist/backend-signin-pending.d.ts +10 -0
  38. package/dist/backend-signin-pending.js +44 -0
  39. package/dist/backend-signin-pending.js.map +1 -0
  40. package/dist/browser-backend-signins.d.ts +56 -0
  41. package/dist/browser-backend-signins.js +189 -0
  42. package/dist/browser-backend-signins.js.map +1 -0
  43. package/dist/cache-ledger.d.ts +38 -2
  44. package/dist/cache-ledger.js +133 -8
  45. package/dist/cache-ledger.js.map +1 -1
  46. package/dist/cache-prices.d.ts +2 -1
  47. package/dist/cache-prices.js +5 -2
  48. package/dist/cache-prices.js.map +1 -1
  49. package/dist/cache-service.d.ts +68 -0
  50. package/dist/cache-service.js +355 -13
  51. package/dist/cache-service.js.map +1 -1
  52. package/dist/channel/adapters/discord.d.ts +13 -0
  53. package/dist/channel/adapters/discord.js +103 -16
  54. package/dist/channel/adapters/discord.js.map +1 -1
  55. package/dist/channel/mcp-tools.js +34 -7
  56. package/dist/channel/mcp-tools.js.map +1 -1
  57. package/dist/channel/tool-router.js +2 -1
  58. package/dist/channel/tool-router.js.map +1 -1
  59. package/dist/channel/types.d.ts +11 -0
  60. package/dist/classic-channel-manager.d.ts +11 -0
  61. package/dist/classic-channel-manager.js +39 -1
  62. package/dist/classic-channel-manager.js.map +1 -1
  63. package/dist/cli.js +21 -2
  64. package/dist/cli.js.map +1 -1
  65. package/dist/config-validator.d.ts +7 -1
  66. package/dist/config-validator.js +92 -12
  67. package/dist/config-validator.js.map +1 -1
  68. package/dist/config.d.ts +19 -0
  69. package/dist/config.js +31 -1
  70. package/dist/config.js.map +1 -1
  71. package/dist/cpu-quota.d.ts +23 -0
  72. package/dist/cpu-quota.js +86 -0
  73. package/dist/cpu-quota.js.map +1 -0
  74. package/dist/daemon.d.ts +57 -3
  75. package/dist/daemon.js +330 -50
  76. package/dist/daemon.js.map +1 -1
  77. package/dist/dashboard-permissions.d.ts +20 -6
  78. package/dist/dashboard-permissions.js +58 -9
  79. package/dist/dashboard-permissions.js.map +1 -1
  80. package/dist/delivery-outbox.d.ts +12 -1
  81. package/dist/delivery-outbox.js +67 -15
  82. package/dist/delivery-outbox.js.map +1 -1
  83. package/dist/doctor.d.ts +3 -0
  84. package/dist/doctor.js +11 -1
  85. package/dist/doctor.js.map +1 -1
  86. package/dist/event-log.d.ts +5 -0
  87. package/dist/event-log.js +13 -0
  88. package/dist/event-log.js.map +1 -1
  89. package/dist/fleet-manager.d.ts +212 -0
  90. package/dist/fleet-manager.js +914 -111
  91. package/dist/fleet-manager.js.map +1 -1
  92. package/dist/fleet-process-handlers.d.ts +1 -0
  93. package/dist/fleet-process-handlers.js +9 -1
  94. package/dist/fleet-process-handlers.js.map +1 -1
  95. package/dist/forged-envelope-scan.d.ts +9 -0
  96. package/dist/forged-envelope-scan.js +30 -5
  97. package/dist/forged-envelope-scan.js.map +1 -1
  98. package/dist/general-knowledge/skills/credential-profiles/SKILL.md +5 -2
  99. package/dist/general-knowledge/skills/kiro-engine-migration/SKILL.md +14 -1
  100. package/dist/general-knowledge/skills/web-appearance/SKILL.canvas-worker-off.md +28 -5
  101. package/dist/general-knowledge/skills/web-appearance/SKILL.md +31 -8
  102. package/dist/general-knowledge/skills/web-dashboard/SKILL.canvas-worker-off.md +138 -59
  103. package/dist/general-knowledge/skills/web-dashboard/SKILL.md +145 -61
  104. package/dist/guest-seen-people.d.ts +38 -0
  105. package/dist/guest-seen-people.js +78 -0
  106. package/dist/guest-seen-people.js.map +1 -0
  107. package/dist/instance-config-impact.js +14 -6
  108. package/dist/instance-config-impact.js.map +1 -1
  109. package/dist/instance-lifecycle.d.ts +20 -0
  110. package/dist/instance-lifecycle.js +71 -13
  111. package/dist/instance-lifecycle.js.map +1 -1
  112. package/dist/kiro-db-reader.d.ts +5 -0
  113. package/dist/kiro-db-reader.js +18 -0
  114. package/dist/kiro-db-reader.js.map +1 -1
  115. package/dist/kiro-engine-status.d.ts +2 -0
  116. package/dist/kiro-engine-status.js +9 -0
  117. package/dist/kiro-engine-status.js.map +1 -1
  118. package/dist/kiro-migrate.d.ts +49 -0
  119. package/dist/kiro-migrate.js +105 -0
  120. package/dist/kiro-migrate.js.map +1 -0
  121. package/dist/kiro-transcript-lane.d.ts +2 -0
  122. package/dist/kiro-transcript-lane.js.map +1 -1
  123. package/dist/kiro-transcript-worker.js +1 -0
  124. package/dist/kiro-transcript-worker.js.map +1 -1
  125. package/dist/kiro-tui-forensic.d.ts +7 -0
  126. package/dist/kiro-tui-forensic.js +110 -0
  127. package/dist/kiro-tui-forensic.js.map +1 -0
  128. package/dist/kiro-usage-run.d.ts +7 -0
  129. package/dist/kiro-usage-run.js +39 -0
  130. package/dist/kiro-usage-run.js.map +1 -0
  131. package/dist/kiro-usage-scan.d.ts +49 -0
  132. package/dist/kiro-usage-scan.js +223 -0
  133. package/dist/kiro-usage-scan.js.map +1 -0
  134. package/dist/kiro-usage-worker.d.ts +1 -0
  135. package/dist/kiro-usage-worker.js +10 -0
  136. package/dist/kiro-usage-worker.js.map +1 -0
  137. package/dist/kiro-usage.d.ts +58 -0
  138. package/dist/kiro-usage.js +310 -0
  139. package/dist/kiro-usage.js.map +1 -0
  140. package/dist/locale.js +120 -12
  141. package/dist/locale.js.map +1 -1
  142. package/dist/log-service.js +1 -1
  143. package/dist/log-service.js.map +1 -1
  144. package/dist/login-url.d.ts +4 -0
  145. package/dist/login-url.js +22 -0
  146. package/dist/login-url.js.map +1 -0
  147. package/dist/mac-auto-restore.d.ts +117 -0
  148. package/dist/mac-auto-restore.js +294 -0
  149. package/dist/mac-auto-restore.js.map +1 -0
  150. package/dist/needs-you-hub.d.ts +2 -1
  151. package/dist/needs-you-hub.js +1 -1
  152. package/dist/needs-you-hub.js.map +1 -1
  153. package/dist/needs-you.d.ts +8 -2
  154. package/dist/needs-you.js +6 -0
  155. package/dist/needs-you.js.map +1 -1
  156. package/dist/notice-delivery.d.ts +16 -0
  157. package/dist/notice-delivery.js +35 -0
  158. package/dist/notice-delivery.js.map +1 -0
  159. package/dist/office-share-page.d.ts +16 -0
  160. package/dist/office-share-page.js +85 -0
  161. package/dist/office-share-page.js.map +1 -0
  162. package/dist/office-share-service.d.ts +91 -0
  163. package/dist/office-share-service.js +176 -0
  164. package/dist/office-share-service.js.map +1 -0
  165. package/dist/office-share.d.ts +258 -0
  166. package/dist/office-share.js +549 -0
  167. package/dist/office-share.js.map +1 -0
  168. package/dist/opencode-usage-scan.d.ts +28 -0
  169. package/dist/opencode-usage-scan.js +92 -0
  170. package/dist/opencode-usage-scan.js.map +1 -0
  171. package/dist/opencode-usage-worker.d.ts +1 -0
  172. package/dist/opencode-usage-worker.js +10 -0
  173. package/dist/opencode-usage-worker.js.map +1 -0
  174. package/dist/outbound-handlers.js +22 -2
  175. package/dist/outbound-handlers.js.map +1 -1
  176. package/dist/outbound-schemas.d.ts +28 -2
  177. package/dist/outbound-schemas.js +29 -2
  178. package/dist/outbound-schemas.js.map +1 -1
  179. package/dist/public-web-gateway.js +16 -2
  180. package/dist/public-web-gateway.js.map +1 -1
  181. package/dist/quickstart-api.js +13 -2
  182. package/dist/quickstart-api.js.map +1 -1
  183. package/dist/quickstart.js +2 -1
  184. package/dist/quickstart.js.map +1 -1
  185. package/dist/remote-card-answer.d.ts +83 -0
  186. package/dist/remote-card-answer.js +208 -0
  187. package/dist/remote-card-answer.js.map +1 -0
  188. package/dist/reply-triggers.d.ts +2 -0
  189. package/dist/reply-triggers.js +3 -0
  190. package/dist/reply-triggers.js.map +1 -0
  191. package/dist/settings-api.d.ts +28 -0
  192. package/dist/settings-api.js +148 -17
  193. package/dist/settings-api.js.map +1 -1
  194. package/dist/settings-change.js +54 -17
  195. package/dist/settings-change.js.map +1 -1
  196. package/dist/settings-effect.js +12 -3
  197. package/dist/settings-effect.js.map +1 -1
  198. package/dist/settings-instance-patch.js +4 -1
  199. package/dist/settings-instance-patch.js.map +1 -1
  200. package/dist/setup-form.d.ts +1 -1
  201. package/dist/setup-form.js +55 -6
  202. package/dist/setup-form.js.map +1 -1
  203. package/dist/setup-host.d.ts +9 -0
  204. package/dist/setup-host.js +127 -6
  205. package/dist/setup-host.js.map +1 -1
  206. package/dist/setup-signin-assets.d.ts +2 -0
  207. package/dist/setup-signin-assets.js +6 -0
  208. package/dist/setup-signin-assets.js.map +1 -0
  209. package/dist/setup-wizard.js +2 -0
  210. package/dist/setup-wizard.js.map +1 -1
  211. package/dist/start-attempt.d.ts +9 -5
  212. package/dist/start-attempt.js +27 -8
  213. package/dist/start-attempt.js.map +1 -1
  214. package/dist/start-failure-chat.d.ts +4 -0
  215. package/dist/start-failure-chat.js +23 -6
  216. package/dist/start-failure-chat.js.map +1 -1
  217. package/dist/start-failure-notice.d.ts +4 -0
  218. package/dist/start-failure-notice.js +8 -0
  219. package/dist/start-failure-notice.js.map +1 -1
  220. package/dist/start-failure.d.ts +25 -0
  221. package/dist/start-failure.js +75 -2
  222. package/dist/start-failure.js.map +1 -1
  223. package/dist/structured-signin-transport.d.ts +8 -0
  224. package/dist/structured-signin-transport.js +79 -0
  225. package/dist/structured-signin-transport.js.map +1 -0
  226. package/dist/structured-signin.d.ts +131 -0
  227. package/dist/structured-signin.js +388 -0
  228. package/dist/structured-signin.js.map +1 -0
  229. package/dist/tool-permissions.js +12 -0
  230. package/dist/tool-permissions.js.map +1 -1
  231. package/dist/topic-commands.js +6 -2
  232. package/dist/topic-commands.js.map +1 -1
  233. package/dist/transcript-sources.d.ts +26 -2
  234. package/dist/transcript-sources.js +103 -40
  235. package/dist/transcript-sources.js.map +1 -1
  236. package/dist/turn-reply-guard.d.ts +25 -0
  237. package/dist/turn-reply-guard.js +111 -12
  238. package/dist/turn-reply-guard.js.map +1 -1
  239. package/dist/types.d.ts +46 -9
  240. package/dist/ui/backend-signin.js +68 -12
  241. package/dist/ui/chat-ambient.js +1 -1
  242. package/dist/ui/chat-effects.js +4 -4
  243. package/dist/ui/chat-render.js +5 -2
  244. package/dist/ui/chat-strings.js +4 -4
  245. package/dist/ui/chat-thread.js +2 -2
  246. package/dist/ui/first-run.js +2 -2
  247. package/dist/ui/fleet-canvas-view.js +114 -0
  248. package/dist/ui/fleet-canvas.js +15 -77
  249. package/dist/ui/fleet-office-editor.js +94 -14
  250. package/dist/ui/fleet-office-layout.js +235 -0
  251. package/dist/ui/fleet-office-model.js +12 -6
  252. package/dist/ui/fleet-office-overview.js +167 -0
  253. package/dist/ui/fleet-office-pack.d.ts +44 -0
  254. package/dist/ui/fleet-office-pack.js +352 -37
  255. package/dist/ui/fleet-office-skin.js +220 -36
  256. package/dist/ui/fleet-office-store.js +112 -7
  257. package/dist/ui/fleet-office-view.js +155 -0
  258. package/dist/ui/fleet-office-zones.js +264 -42
  259. package/dist/ui/fleet-office.js +957 -160
  260. package/dist/ui/fleet-usage.js +26 -21
  261. package/dist/ui/office-share-dialog.js +145 -0
  262. package/dist/ui/office-share-page.js +164 -0
  263. package/dist/ui/panel-chat.js +9 -2
  264. package/dist/ui/panel-details.js +2 -2
  265. package/dist/ui/panel-fleet.js +11 -77
  266. package/dist/ui/panel-log.js +4 -4
  267. package/dist/ui/panel-needs.js +4 -4
  268. package/dist/ui/panel-settings.js +608 -167
  269. package/dist/ui/search-dialog.js +26 -7
  270. package/dist/ui/settings-appearance.js +82 -29
  271. package/dist/ui/settings-apply.js +42 -7
  272. package/dist/ui/settings-dialogs.js +76 -23
  273. package/dist/ui/settings-draft.js +51 -0
  274. package/dist/ui/settings-guests.js +255 -79
  275. package/dist/ui/settings-learn.js +2 -2
  276. package/dist/ui/settings-model.js +16 -6
  277. package/dist/ui/settings-registry.js +146 -0
  278. package/dist/ui/settings-strings.js +116 -44
  279. package/dist/ui/settings-web-authority.js +34 -23
  280. package/dist/ui/shared/app-i18n.js +18 -2
  281. package/dist/ui/shared/app-nav.js +10 -5
  282. package/dist/ui/shared/app-prefs.js +80 -2
  283. package/dist/ui/shared/app-route.js +39 -5
  284. package/dist/ui/shared/app-shell.js +73 -10
  285. package/dist/ui/shared/app-stream.js +3 -1
  286. package/dist/ui/shared/app.css +307 -17
  287. package/dist/ui/shared/app.js +3 -0
  288. package/dist/ui/shared/effect-stock-trust.js +1 -1
  289. package/dist/ui/shared/identity-color.js +4 -4
  290. package/dist/ui/shared/office/cozy.png +0 -0
  291. package/dist/ui/shared/side-panel.js +95 -0
  292. package/dist/ui/shared/signin.js +2 -2
  293. package/dist/ui/shared/ui-dialog.js +6 -1
  294. package/dist/ui/shared/ui-icons.js +2 -0
  295. package/dist/ui/shared/web-authorize.js +13 -12
  296. package/dist/ui/structured-signin.js +115 -0
  297. package/dist/usage-report.d.ts +20 -0
  298. package/dist/usage-report.js +54 -17
  299. package/dist/usage-report.js.map +1 -1
  300. package/dist/usage-rollup.d.ts +9 -1
  301. package/dist/usage-rollup.js +20 -4
  302. package/dist/usage-rollup.js.map +1 -1
  303. package/dist/web-api.d.ts +26 -2
  304. package/dist/web-api.js +361 -5
  305. package/dist/web-api.js.map +1 -1
  306. package/dist/web-auth.js +8 -2
  307. package/dist/web-auth.js.map +1 -1
  308. package/dist/web-backend-login.d.ts +1 -7
  309. package/dist/web-backend-login.js +2 -24
  310. package/dist/web-backend-login.js.map +1 -1
  311. package/dist/web-experimental.d.ts +1 -1
  312. package/dist/web-experimental.js +1 -1
  313. package/dist/web-experimental.js.map +1 -1
  314. package/dist/web-mod-agent.js +2 -2
  315. package/dist/web-mod-agent.js.map +1 -1
  316. package/dist/web-office-acts.d.ts +173 -0
  317. package/dist/web-office-acts.js +285 -0
  318. package/dist/web-office-acts.js.map +1 -0
  319. package/dist/web-office-skins.d.ts +109 -0
  320. package/dist/web-office-skins.js +299 -0
  321. package/dist/web-office-skins.js.map +1 -0
  322. package/dist/web-office-store.d.ts +8 -2
  323. package/dist/web-office-store.js +77 -14
  324. package/dist/web-office-store.js.map +1 -1
  325. package/dist/web-settings-patch.d.ts +8 -1
  326. package/dist/web-settings-patch.js +65 -5
  327. package/dist/web-settings-patch.js.map +1 -1
  328. package/dist/web-shell-routes.d.ts +4 -2
  329. package/dist/web-shell-routes.js +17 -7
  330. package/dist/web-shell-routes.js.map +1 -1
  331. package/dist/working-recovery.d.ts +104 -0
  332. package/dist/working-recovery.js +298 -0
  333. package/dist/working-recovery.js.map +1 -0
  334. package/dist/working-state-journal.d.ts +5 -1
  335. package/dist/working-state-journal.js +31 -3
  336. package/dist/working-state-journal.js.map +1 -1
  337. package/package.json +3 -2
  338. package/templates/windows-shortcuts.ps1 +155 -140
@@ -0,0 +1,155 @@
1
+ // #2540: the Office view's canvas controls — the pure part (fleet-office.js applies it). The office fills the Fleet
2
+ // panel; its scene (the zones' 1280×720 logical canvas, or the desks layout) is panned and zoomed like the org chart's
3
+ // canvas (fleet-canvas-view.js: the same view { k, x, y }, gestures, wheel and keys), with these of its own:
4
+ //
5
+ // - Pixel levels for the steps. + and − step to the next pixel level (one logical unit a whole number of device px:
6
+ // k·dpr = 1, 2, 3, …; below one device px a unit by 1.25); the canvas always draws unsmoothed (nearest neighbour).
7
+ // The wheel and a pinch zoom continuously, and Fit takes whatever scale fills the panel.
8
+ // - Fit: two modes, kept per office (fleet-office-store.js). "contain" (the default): the largest scale at which the
9
+ // whole scene shows — non-integer, so the scene fills the panel's width or height. "cover" (Fill): the smallest
10
+ // scale that fills both; what overflows is cropped and stays a pan away. Either is centred.
11
+ // - The view never loses the scene (clampPan): along an axis where the scene is larger than the panel it covers the
12
+ // panel (no gap at an edge); where it is smaller it is centred on that axis (the user: the canvas's middle is the
13
+ // scene's), on opening, Fit, any zoom and a resize alike.
14
+ //
15
+ // Around the scene is a matte: the pack's `matte` (fleet-office-pack.js), else the colour of the background's edge
16
+ // pixels (sampled once per style: edgeMatte), else the skin's own (Studio's hall floor; Desks': the dashboard's
17
+ // background). It paints the whole canvas outside the scene (and the stage behind it), never the page's grey.
18
+ import { clampK, pinchTo, zoomTo } from "./fleet-canvas-view.js";
19
+ import { CANVAS_MAX } from "./fleet-office-model.js";
20
+
21
+ /** The zoom's range, in CSS px per logical unit. */
22
+ export const OK_MIN = 0.1, OK_MAX = 8;
23
+ const EPS = 1e-6;
24
+ const clampO = (k) => clampK(k, OK_MIN, OK_MAX);
25
+ /** The highest pixel level within the range at this ratio (at least 1). */
26
+ const topLevel = (dpr) => Math.max(1, Math.floor(OK_MAX * dpr + EPS));
27
+
28
+ /** `k` on its pixel level: k·dpr rounded to a whole number when it is 1 or more; as it is (in range) below that. */
29
+ export function snapK(k, dpr = 1) {
30
+ const c = clampO(k), n = c * dpr;
31
+ if (n < 1 - EPS) return c;
32
+ return Math.min(topLevel(dpr), Math.max(1, Math.round(n))) / dpr;
33
+ }
34
+ /** Whether `k` is a pixel level (a whole number of device px per logical unit). */
35
+ export const isPixelK = (k, dpr = 1) => k * dpr >= 1 - EPS && Math.abs(k * dpr - Math.round(k * dpr)) < 1e-6;
36
+ /** One step in (`dir` > 0) or out: the next pixel level; below one device pixel a unit, by 1.25. */
37
+ export function stepK(k, dir, dpr = 1) {
38
+ const n = k * dpr;
39
+ if (dir > 0) {
40
+ if (n >= 1 - EPS) return Math.min(topLevel(dpr), Math.floor(n + EPS) + 1) / dpr;
41
+ return n * 1.25 >= 1 - EPS ? 1 / dpr : clampO(k * 1.25);
42
+ }
43
+ if (n > 1 + EPS) return Math.max(1, Math.ceil(n - EPS) - 1) / dpr;
44
+ return clampO(k * 0.8);
45
+ }
46
+ /** The view at scale `k` (in range, continuous: the wheel's) about the viewport point (cx, cy). */
47
+ export const zoomOffice = (v, k, cx, cy) => zoomTo(v, clampO(k), cx, cy);
48
+ /** A pinch step: `raw` the zoom the pinch has reached (continuous); the world point under the old midpoint follows. */
49
+ export const pinchOffice = (v, from, to, raw) => pinchTo(v, from, to, clampO(raw));
50
+
51
+ /** The fit modes: "contain" (the whole scene shows) and "cover" (the panel is filled, the overflow cropped). */
52
+ export const FIT_MODES = ["contain", "cover"];
53
+ export const fitModeOk = (m) => (m === "cover" ? "cover" : "contain");
54
+ /** The scale at which a `w`×`h` scene fits a `size` viewport: contain (the whole scene) or cover (the whole panel). */
55
+ export function fitK(w, h, size, mode = "contain", margin = 0) {
56
+ const sx = (size.w - 2 * margin) / Math.max(1, w), sy = (size.h - 2 * margin) / Math.max(1, h);
57
+ return clampO(fitModeOk(mode) === "cover" ? Math.max(sx, sy) : Math.min(sx, sy));
58
+ }
59
+ /** The scene at fitK, centred. */
60
+ export function fitScene(w, h, size, mode = "contain", margin = 0) {
61
+ const k = fitK(w, h, size, mode, margin);
62
+ return { k, x: (size.w - w * k) / 2, y: (size.h - h * k) / 2 };
63
+ }
64
+ /**
65
+ * The desks layout's scale when it opens: the pixel level nearest one CSS px a unit (1 on an ordinary or a 2× screen,
66
+ * 4/3 at 150 %, 0.8 at 125 %); the layout wraps at the panel's width over it, so it opens as wide as the panel.
67
+ */
68
+ export const desksK = (dpr = 1) => Math.max(1, Math.round(dpr)) / dpr;
69
+ /** The view a scene opens on with nothing kept: zones fitted (the office's mode), desks from the top left at desksK (centred by clampPan on an axis where it is smaller). */
70
+ export const openingView = (w, h, size, dpr, zones, mode = "contain") => (zones ? fitScene(w, h, size, mode) : { k: desksK(dpr), x: 0, y: 0 });
71
+ /** One axis of clampPan: offset `o` of a scene `s` long in a viewport `n` long. */
72
+ const clampAxis = (o, s, n) => (s >= n ? Math.min(0, Math.max(n - s, o)) : (n - s) / 2);
73
+ /**
74
+ * The view kept on the scene: along an axis where the scene is at least the viewport it covers the viewport (no gap at
75
+ * either edge); where it is smaller it is centred on that axis. Nothing can be scrolled off-screen.
76
+ */
77
+ export function clampPan(v, w, h, size) {
78
+ return { k: v.k, x: clampAxis(v.x, w * v.k, size.w), y: clampAxis(v.y, h * v.k, size.h) };
79
+ }
80
+ /** The zoom as a person reads it: "150%". */
81
+ export const zoomLabel = (k) => `${Math.round(k * 100)}%`;
82
+ /**
83
+ * The overview's box in a `size` stage: the scene's aspect, about a seventh of the stage's width (120–280 px, at most
84
+ * a third of its height), `margin` px in from the bottom right corner — always inside the stage.
85
+ */
86
+ export function miniBox(size, w, h, margin = 12) {
87
+ const ar = Math.max(1, w) / Math.max(1, h), roomW = Math.max(0, size.w - 2 * margin), roomH = Math.max(0, size.h - 2 * margin);
88
+ let bw = Math.min(280, Math.max(120, Math.round(size.w / 7)), roomW), bh = bw / ar;
89
+ const maxH = Math.min(roomH, Math.max(60, size.h / 3));
90
+ if (bh > maxH) { bh = maxH; bw = bh * ar; }
91
+ bw = Math.floor(bw); bh = Math.floor(bh);
92
+ return { x: size.w - margin - bw, y: size.h - margin - bh, w: bw, h: bh };
93
+ }
94
+ /** The view's frame on the overview (logical units): the part of the scene in view (never outside it). */
95
+ export function miniFrame(v, size, w, h) {
96
+ const x0 = Math.max(0, -v.x / v.k), y0 = Math.max(0, -v.y / v.k);
97
+ const x1 = Math.min(w, (size.w - v.x) / v.k), y1 = Math.min(h, (size.h - v.y) / v.k);
98
+ return { x: x0, y: y0, width: Math.max(0, x1 - x0), height: Math.max(0, y1 - y0) };
99
+ }
100
+
101
+ // ── Kept per device, per office: the zoom and the logical point in the middle (the viewport's size may differ) ──
102
+
103
+ /** What is kept of a view: { k, cx, cy }, and `fit: true` when it is a fit (it is fitted again, to the size then). */
104
+ export const savedOf = (v, size, fit = false) => ({ k: v.k, cx: (size.w / 2 - v.x) / v.k, cy: (size.h / 2 - v.y) / v.k, ...(fit ? { fit: true } : {}) });
105
+ /** A kept view, checked: { k, cx, cy } with finite numbers (k in range), else null. */
106
+ export function savedOk(x) {
107
+ if (!x || typeof x !== "object") return null;
108
+ const { k, cx, cy } = x;
109
+ if (![k, cx, cy].every((n) => typeof n === "number" && Number.isFinite(n)) || k <= 0) return null;
110
+ if (Math.abs(cx) > 1e6 || Math.abs(cy) > 1e6) return null;
111
+ return { k: clampO(k), cx, cy, ...(x.fit === true ? { fit: true } : {}) };
112
+ }
113
+ /** The view a kept one gives in a `size` viewport: the same zoom, the same logical point in the middle. */
114
+ export function viewFromSaved(s, size) {
115
+ const k = clampO(s.k);
116
+ return { k, x: size.w / 2 - s.cx * k, y: size.h / 2 - s.cy * k };
117
+ }
118
+
119
+ // ── Drawing ──
120
+
121
+ /** The cached layers' budget (device px): beyond it they are drawn coarser and scaled up by a whole number. */
122
+ export const LAYER_MAX = { side: CANVAS_MAX.side, area: 8 * 1024 * 1024 };
123
+ /**
124
+ * The layers' resolution (layer px per logical unit) for a view at `a` device px a unit and a `w`×`h` scene: `a`
125
+ * itself (the layer is blitted 1:1), or — over the budget — `a` divided by the smallest whole number that fits it,
126
+ * so the blit is still an integer scale-up (nearest neighbour: crisp).
127
+ */
128
+ export function layerScale(a, w, h, max = LAYER_MAX) {
129
+ const cap = Math.min(max.side / Math.max(1, w), max.side / Math.max(1, h), Math.sqrt(max.area / Math.max(1, w * h)));
130
+ if (a <= cap) return a;
131
+ return a / Math.ceil(a / cap - EPS);
132
+ }
133
+ /**
134
+ * The matte from a background's edge pixels: `strips` are RGBA pixel arrays (the top and bottom rows, the left and
135
+ * right columns); the mean of their opaque pixels as #rrggbb, or null when none is opaque.
136
+ */
137
+ export function edgeMatte(strips) {
138
+ let r = 0, g = 0, b = 0, n = 0;
139
+ for (const s of strips) {
140
+ if (!s || typeof s.length !== "number") continue;
141
+ for (let i = 0; i + 3 < s.length; i += 4) { if (s[i + 3] < 128) continue; r += s[i]; g += s[i + 1]; b += s[i + 2]; n++; }
142
+ }
143
+ if (!n) return null;
144
+ const hx = (v) => Math.round(v / n).toString(16).padStart(2, "0");
145
+ return `#${hx(r)}${hx(g)}${hx(b)}`;
146
+ }
147
+ /** The edge strips of a canvas (its 2d context's pixels), for edgeMatte; null when they cannot be read. */
148
+ export function edgeStrips(c) {
149
+ try {
150
+ const g = c && typeof c.getContext === "function" ? c.getContext("2d") : null;
151
+ if (!g || typeof g.getImageData !== "function" || !(c.width > 0) || !(c.height > 0)) return null;
152
+ const w = c.width, h = c.height;
153
+ return [g.getImageData(0, 0, w, 1).data, g.getImageData(0, h - 1, w, 1).data, g.getImageData(0, 0, 1, h).data, g.getImageData(w - 1, 0, 1, h).data];
154
+ } catch { return null; } // a tainted or lost canvas: the skin's matte
155
+ }
@@ -2,27 +2,40 @@
2
2
  // fleet-office-pack.js). Each instance goes to the area for its state now and takes a seat slot there; when its state
3
3
  // changes it walks (along the pack's waypoints) to the new area. Pure — fleet-office.js draws it. Also here: what a
4
4
  // character says in its speech bubble, and which furniture is active.
5
- import { LOGICAL } from "./fleet-office-pack.js";
6
- import { nameHash, pathLength, along, routeVia } from "./fleet-office-model.js";
5
+ import { LOGICAL, own } from "./fleet-office-pack.js";
6
+ import { FLIGHT, nameHash, pathLength, along, routeVia } from "./fleet-office-model.js";
7
+ import { hue } from "./fleet-traffic.js";
7
8
 
8
9
  /** The area for a state: the pack's own, else its "default", else its first area. */
9
10
  export function zoneOf(pack, state) {
10
11
  const z = pack.stateZones || {};
11
- return z[state] || z.default || Object.keys(pack.areas)[0];
12
+ return own(z, state) || own(z, "default") || Object.keys(pack.areas)[0];
12
13
  }
13
14
 
14
- /** How far apart overflow spots are, and how far they stay from the area's seats (logical units). */
15
- export const OVERFLOW = { STEP: 56, CLEAR: 40, EDGE: 24 };
16
- /** An area's overflow spots, for when its seats are taken: a grid inside its rect, clear of its seats, in reading order. */
17
- export function overflowSpots(area) {
18
- const out = [], { x, y, w, h } = area.rect;
19
- for (let sy = y + OVERFLOW.EDGE + 30; sy <= y + h - OVERFLOW.EDGE; sy += OVERFLOW.STEP) {
20
- for (let sx = x + OVERFLOW.EDGE; sx <= x + w - OVERFLOW.EDGE; sx += OVERFLOW.STEP) {
21
- if (area.seats.every((s) => Math.hypot(s.x - sx, s.y - sy) >= OVERFLOW.CLEAR)) out.push({ x: sx, y: sy });
15
+ /**
16
+ * How far apart overflow spots are, and how far they stay from the area's seats (logical units); TITLE: the band at
17
+ * the top of an area its name is written in, kept clear. A crowded area packs tighter (STEPS, #2513: 70 instances, 42
18
+ * idle, piled up in the lounge) before anyone is left to the "+N" count.
19
+ */
20
+ export const OVERFLOW = { STEP: 56, CLEAR: 40, EDGE: 24, TITLE: 30, STEPS: [56, 46, 38, 32] };
21
+ function spotsAt(area, step) {
22
+ const out = [], { x, y, w, h } = area.rect, clear = step === OVERFLOW.STEP ? OVERFLOW.CLEAR : Math.round(step * 0.72);
23
+ for (let sy = y + OVERFLOW.EDGE + OVERFLOW.TITLE; sy <= y + h - OVERFLOW.EDGE; sy += step) {
24
+ for (let sx = x + OVERFLOW.EDGE; sx <= x + w - OVERFLOW.EDGE; sx += step) {
25
+ if (area.seats.every((s) => Math.hypot(s.x - sx, s.y - sy) >= clear)) out.push({ x: sx, y: sy });
22
26
  }
23
27
  }
24
28
  return out;
25
29
  }
30
+ /**
31
+ * An area's overflow spots, for when its seats are taken: a tidy grid inside its rect, clear of its seats and its
32
+ * title, in reading order. `need`: how many are wanted — the widest spacing that fits them all, else the tightest.
33
+ */
34
+ export function overflowSpots(area, need = 0) {
35
+ let spots = [];
36
+ for (const step of OVERFLOW.STEPS) { spots = spotsAt(area, step); if (spots.length >= need) return spots; }
37
+ return spots;
38
+ }
26
39
 
27
40
  /**
28
41
  * Seats for everyone: `wants` [{ name, area }] in the fleet's order; `prev` what the last call gave (name → { area,
@@ -31,51 +44,224 @@ export function overflowSpots(area) {
31
44
  * Returns { seats: Map name → { area, slot, x, y }, crowd: Map }.
32
45
  */
33
46
  export function assignSeats(pack, wants, prev = new Map()) {
34
- const seats = new Map(), crowd = new Map(), taken = new Map(), spill = new Map();
35
- const spotsOf = (area) => { if (!spill.has(area)) spill.set(area, overflowSpots(pack.areas[area])); return spill.get(area); };
36
- const pos = (area, slot) => (slot >= 0 ? pack.areas[area].seats[slot] : spotsOf(area)[-slot - 1]);
47
+ const seats = new Map(), crowd = new Map(), taken = new Map(), spill = new Map(), demand = new Map();
48
+ const areaOf = (a) => own(pack.areas, a);
49
+ for (const w of wants) demand.set(w.area, (demand.get(w.area) || 0) + 1);
50
+ const spotsOf = (area) => {
51
+ if (!spill.has(area)) spill.set(area, overflowSpots(areaOf(area), Math.max(0, (demand.get(area) || 0) - areaOf(area).seats.length)));
52
+ return spill.get(area);
53
+ };
54
+ const pos = (area, slot) => (slot >= 0 ? areaOf(area).seats[slot] : spotsOf(area)[-slot - 1]);
37
55
  const usedIn = (area) => { if (!taken.has(area)) taken.set(area, new Set()); return taken.get(area); };
38
56
  const give = (name, area, slot) => { const p = pos(area, slot); usedIn(area).add(slot); seats.set(name, { area, slot, x: p.x, y: p.y }); };
39
57
  for (const w of wants) {
40
58
  const p = prev.get(w.name);
41
- if (!p || p.area !== w.area || !pack.areas[w.area] || !pos(w.area, p.slot) || usedIn(w.area).has(p.slot)) continue;
59
+ if (!p || p.area !== w.area || !areaOf(w.area) || !pos(w.area, p.slot) || usedIn(w.area).has(p.slot)) continue;
42
60
  give(w.name, w.area, p.slot);
43
61
  }
44
62
  for (const w of wants) {
45
- if (seats.has(w.name) || !pack.areas[w.area]) continue;
46
- const used = usedIn(w.area), area = pack.areas[w.area];
63
+ if (seats.has(w.name) || !areaOf(w.area)) continue;
64
+ const used = usedIn(w.area), area = areaOf(w.area);
47
65
  let slot = area.seats.findIndex((_, i) => !used.has(i));
48
66
  if (slot < 0) { const j = spotsOf(w.area).findIndex((_, i) => !used.has(-i - 1)); slot = j < 0 ? null : -j - 1; }
49
67
  if (slot === null) crowd.set(w.area, (crowd.get(w.area) || 0) + 1);
50
68
  else give(w.name, w.area, slot);
51
69
  }
52
- return { seats, crowd };
70
+ // Over capacity: more people want the area than it has seats (their bubbles and tags are thinned, fleet-office.js).
71
+ const over = new Set([...demand].filter(([a, n]) => areaOf(a) && n > areaOf(a).seats.length).map(([a]) => a));
72
+ return { seats, crowd, over };
53
73
  }
54
74
 
55
75
  /**
56
76
  * The zones layout for `nodes` (buildOrgView's) on a pack: everyone but the missing at their seat. The same shape as
57
- * the desks layout (rooms → desks with a seat), in logical units, plus `mode: "zones"`, the pack's areas and graph, the
58
- * seats given (`seats`, the next call's `prev`) and `crowd`.
77
+ * the desks layout (rooms → desks with a seat), in logical units, plus `mode: "zones"`, the pack's areas and graph (and
78
+ * its `straight`, #2634), the seats given (`seats`, the next call's `prev`), `crowd` and `over` (the areas over capacity).
59
79
  */
60
- export function zonesLayout(pack, nodes, prev = new Map()) {
61
- const wants = nodes.filter((n) => n.state !== "missing").map((n) => ({ name: n.name, area: zoneOf(pack, n.state) }));
62
- const { seats, crowd } = assignSeats(pack, wants, prev);
80
+ export function zonesLayout(pack, nodes, prev = new Map(), { night = false } = {}) {
81
+ // At night (#2568) the idle go to bed: the area asleep people go to while it has a free seat; the rest doze where they are.
82
+ const bed = zoneOf(pack, "paused"), beds = (own(pack.areas, bed)?.seats || []).length, sleepers = new Set(), inBed = new Set();
83
+ let used = nodes.filter((n) => n.state !== "idle" && zoneOf(pack, n.state) === bed).length;
84
+ if (night) for (const n of nodes) if (n.state === "idle") { sleepers.add(n.name); if (used < beds) { used++; inBed.add(n.name); } }
85
+ const wants = nodes.filter((n) => n.state !== "missing").map((n) => ({ name: n.name, area: inBed.has(n.name) ? bed : zoneOf(pack, n.state) }));
86
+ const { seats, crowd, over } = assignSeats(pack, wants, prev);
63
87
  const desks = nodes.filter((n) => seats.has(n.name)).map((n) => {
64
88
  const s = seats.get(n.name);
65
- return { key: `zone/${n.name}`, node: n, x: s.x - 24, y: s.y - 52, w: 48, h: 66, seat: { x: s.x, y: s.y }, wp: null, area: s.area, slot: s.slot };
89
+ return { key: `zone/${n.name}`, node: n, x: s.x - 24, y: s.y - 52, w: 48, h: 66, seat: { x: s.x, y: s.y }, wp: null, area: s.area, slot: s.slot, night: sleepers.has(n.name) };
66
90
  });
67
91
  return { mode: "zones", rooms: [{ key: "\u0000zones", name: null, lobby: false, x: 0, y: 0, w: LOGICAL.w, h: LOGICAL.h, desks }], areas: pack.areas,
68
- waypoints: pack.waypoints || [], edges: pack.edges || [], door: pack.door, width: LOGICAL.w, height: LOGICAL.h, seats, crowd };
92
+ waypoints: pack.waypoints || [], edges: pack.edges || [], straight: pack.straight === true, door: pack.door, width: LOGICAL.w, height: LOGICAL.h, seats, crowd, over };
69
93
  }
70
94
 
71
95
  // ── Walking ──
72
96
 
73
97
  /** Logical units per ms (crossing the canvas takes about six seconds). */
74
98
  export const WALK = { SPEED: 0.2 };
99
+ /** The step of a polyline a fraction `t` (0–1) of the way along it: { dx, dy } (its direction), or null (no length). */
100
+ export function headingAt(pts, t) {
101
+ const total = pathLength(pts);
102
+ if (!total) return null;
103
+ let left = Math.max(0, Math.min(1, t)) * total;
104
+ for (let i = 1; i < pts.length; i++) {
105
+ const dx = pts[i].x - pts[i - 1].x, dy = pts[i].y - pts[i - 1].y, l = Math.hypot(dx, dy);
106
+ if (l > 0 && (left <= l || i === pts.length - 1)) return { dx, dy };
107
+ left -= l;
108
+ }
109
+ return null;
110
+ }
111
+ /**
112
+ * #2634: a polyline with each diagonal leg made an L — across first, then up or down — so a character set's three
113
+ * walking strips always fit; legs already across or up/down are kept, and no point repeats (no zero-length leg).
114
+ */
115
+ export function straighten(pts) {
116
+ const out = [];
117
+ const add = (x, y) => { const l = out[out.length - 1]; if (!l || l.x !== x || l.y !== y) out.push({ x, y }); };
118
+ for (const p of pts) { const l = out[out.length - 1]; if (l && l.x !== p.x && l.y !== p.y) add(p.x, l.y); add(p.x, p.y); }
119
+ return out;
120
+ }
121
+ // ── Night (#2568) ──
122
+
123
+ /** Bedtime on the viewer's clock: from FROM o'clock to TO o'clock the idle sleep (anyone with work still gets up for it). */
124
+ export const NIGHT = { FROM: 0, TO: 6, TICK: 60000 };
125
+ export const isNight = (d = new Date()) => { const h = d.getHours(); return NIGHT.FROM <= NIGHT.TO ? h >= NIGHT.FROM && h < NIGHT.TO : h >= NIGHT.FROM || h < NIGHT.TO; };
126
+
127
+ // ── Wandering (#2560) ──
128
+
129
+ /**
130
+ * An idle person now and then strolls to a spot near their seat and back, so a quiet room is not a row of statues:
131
+ * every PERIOD ms (each person's own phase, from their name) they are AWAY ms at a waypoint within REACH of the seat —
132
+ * one no seat of the office is on (taken or free), picked anew each time — then walk back. Pure: the page asks every TICK ms and walks the change.
133
+ */
134
+ export const WANDER = { PERIOD: 40000, AWAY: 8000, REACH: 220, TICK: 1000 };
135
+ /** Where `name`, seated at `seat`, should be at `t` (ms) while idle: a spot near the seat, or the seat. */
136
+ export function wanderTarget(name, seat, graph, t, seats = []) {
137
+ const near = (graph.waypoints || []).filter((w) => Math.hypot(w.x - seat.x, w.y - seat.y) <= WANDER.REACH
138
+ && !seats.some((s) => Math.hypot(w.x - s.x, w.y - s.y) < 4));
139
+ if (!near.length) return seat;
140
+ const h = nameHash(name), k = t + (h % WANDER.PERIOD), turn = Math.floor(k / WANDER.PERIOD);
141
+ if (k - turn * WANDER.PERIOD >= WANDER.AWAY) return seat;
142
+ const w = near[nameHash(`${name}\u0000${turn}`) % near.length];
143
+ return { x: w.x, y: w.y };
144
+ }
145
+ /** An NPC drawn as furniture (#2596: an item with `walk`): its walker's name — never a person's, and its own phase. */
146
+ export const npcKey = (f) => `furniture/${f.id}`;
147
+ /** Everyone's walk target: their seat, or — idle and `wander` on — wanderTarget's spot (one person a spot: whoever
148
+ * would share it stays seated this time). And, `wander` on, each item of `furniture` with a `walk` (#2596): its spot
149
+ * (x, y), or a stroll near it as an idle person's (by npcKey) — none at `night` for one with a night place (asleep there). */
150
+ export function walkTargets(desks, graph, t, wander = true, furniture = [], night = false) {
151
+ // Every seat of the office (anyone's, taken or not: someone may sit down on it any moment), not only the ones in use.
152
+ // And each walking NPC's spot (#2596 review): its home, where it stands outside its stroll and goes back to whenever
153
+ // its stroll would collide, so nobody else's stroll may end there — no two walkers ever share a target.
154
+ const homes = wander ? furniture.filter((f) => f.walk && !(night && f.night)).map((f) => ({ x: f.x, y: f.y })) : [];
155
+ const seats = [...Object.values(graph.areas || {}).flatMap((a) => a.seats || []), ...desks.map((d) => d.seat), ...homes];
156
+ const used = new Set(homes.map((h) => `${h.x},${h.y}`));
157
+ const stroll = (name, home) => {
158
+ const p = wanderTarget(name, home, graph, t, seats), k = `${p.x},${p.y}`;
159
+ if (p === home || used.has(k)) return home;
160
+ used.add(k);
161
+ return p;
162
+ };
163
+ const out = new Map(desks.map((d) => [d.node.name, !wander || d.node.state !== "idle" || d.night ? d.seat : stroll(d.node.name, d.seat)]));
164
+ if (wander) for (const f of furniture) if (f.walk && !(night && f.night)) out.set(npcKey(f), stroll(npcKey(f), { x: f.x, y: f.y }));
165
+ return out;
166
+ }
167
+
168
+ // ── Mail from another office (#2564) ──
169
+
170
+ /**
171
+ * Something sent to a person here by someone who is not (in another office, or in none): it flies in at the door and
172
+ * lands on the floor near the middle of the room; the person walks over, picks it up and walks back to their seat.
173
+ * MAX_WAIT: a letter no one picks up is gone after it; GAP: letters waiting side by side; SEARCH: how far from the
174
+ * middle a landing spot is looked for when no waypoint will do; ASLEEP: who does not get up
175
+ * for it (it waits for them to wake, up to MAX_WAIT).
176
+ */
177
+ export const MAIL = { MAX_WAIT: 60000, GAP: 16, SEARCH: 320, ASLEEP: new Set(["paused", "stopped"]), MAX: 20 };
178
+ /**
179
+ * Where the `n`th waiting letter lands: `n` GAPs along from the waypoint nearest the room's middle — the nearest whose
180
+ * spot (offset included) no seat of the office is on, taken or free (someone may sit there any moment; #2561 likewise);
181
+ * with none, the nearest point round the middle (rings GAP·2 apart, inside the room) that no seat is on; with none of
182
+ * those either, null — the letter is not delivered here (never on a seat).
183
+ */
184
+ export function dropPoint(L, n = 0) {
185
+ const mid = { x: L.width / 2, y: L.height * 0.55 }, d = (p, q) => Math.hypot(p.x - q.x, p.y - q.y);
186
+ const seats = [...Object.values(L.areas || {}).flatMap((a) => a.seats || []), ...desksIn(L).map((q) => q.seat)];
187
+ const onSeat = (p) => seats.some((q) => d(p, q) < 4);
188
+ const spots = (L.waypoints || []).map((w) => ({ x: Math.round(w.x + n * MAIL.GAP), y: Math.round(w.y), from: w }))
189
+ .filter((p) => !onSeat(p.from) && !onSeat(p)).sort((p, q) => d(p.from, mid) - d(q.from, mid));
190
+ if (spots.length) return { x: spots[0].x, y: spots[0].y };
191
+ const inRoom = (p) => p.x >= 0 && p.y >= 0 && p.x <= L.width && p.y <= L.height;
192
+ for (let r = 0; r <= MAIL.SEARCH; r += 2 * MAIL.GAP) {
193
+ for (let k = 0, m = r ? 16 : 1; k < m; k++) {
194
+ const a = (2 * Math.PI * k) / m, p = { x: Math.round(mid.x + r * Math.cos(a) + n * MAIL.GAP), y: Math.round(mid.y + r * Math.sin(a)) };
195
+ if (inRoom(p) && !onSeat(p)) return p;
196
+ }
197
+ }
198
+ return null;
199
+ }
200
+ const desksIn = (L) => (L.rooms || []).flatMap((r) => r.desks || []).filter((d) => d.node);
201
+ /**
202
+ * The letters of one office. `sync(items, now, L, still)`: each new traffic item to someone drawn here from someone
203
+ * who is not becomes a letter (none with reduced motion, nor for a stale delegation; a zones office only).
204
+ * `frame(now, L, walkers)` → what to draw, { id, hue, kind, from, x, y, t } (t: its flight, 0 on the floor), picking
205
+ * up the letters whose person stands on them, and dropping those gone too long or whose person left. `targets(L)`:
206
+ * name → where to walk to pick one up (the first waiting for them; none while they are asleep). `prime(items)`: mark as
207
+ * seen what was in the air before this office was shown (a new office's or style's mail starts empty). `size()`.
208
+ */
209
+ export function createMail({ max = MAIL.MAX } = {}) {
210
+ const seen = new Set();
211
+ let letters = [];
212
+ const here = (L, name) => desksIn(L).find((d) => d.node.name === name) || null;
213
+ return {
214
+ sync(items, now, L, still = false) {
215
+ let started = 0;
216
+ for (const it of items || []) {
217
+ if (seen.has(it.id)) continue;
218
+ seen.add(it.id);
219
+ if (seen.size > 2000) seen.delete(seen.values().next().value);
220
+ if (still || it.phase === "stale" || !L || L.mode !== "zones" || !here(L, it.to) || here(L, it.from)) continue;
221
+ const drop = dropPoint(L, letters.filter((l) => l.to !== it.to).length % 4);
222
+ if (!drop) continue; // nowhere off the seats: not delivered here
223
+ const pts = [{ x: L.door.x, y: L.door.y }, drop];
224
+ letters.push({ id: it.id, from: it.from, to: it.to, kind: it.kind, hue: hue(it.kind), start: now, drop, pts,
225
+ ms: Math.min(FLIGHT.MAX_MS, Math.max(FLIGHT.MIN_MS, pathLength(pts) / FLIGHT.SPEED)) });
226
+ started++;
227
+ }
228
+ if (letters.length > max) letters = letters.slice(letters.length - max);
229
+ return started;
230
+ },
231
+ frame(now, L, walkers) {
232
+ const out = [];
233
+ letters = letters.filter((l) => {
234
+ if (!here(L, l.to)) return false;
235
+ const t = (now - l.start) / l.ms;
236
+ if (t < 1) { if (t >= 0) { const p = along(l.pts, t); out.push({ id: l.id, hue: l.hue, kind: l.kind, from: l.from, x: p.x, y: p.y, t }); } return true; }
237
+ if (now - l.start - l.ms > MAIL.MAX_WAIT) return false;
238
+ const w = walkers && walkers.at(l.to, now);
239
+ if (w && !w.walking && Math.hypot(w.x - l.drop.x, w.y - l.drop.y) < 4) return false; // picked up
240
+ out.push({ id: l.id, hue: l.hue, kind: l.kind, from: l.from, x: l.drop.x, y: l.drop.y, t: 0 });
241
+ return true;
242
+ });
243
+ return out;
244
+ },
245
+ targets(L, now) {
246
+ const out = new Map();
247
+ for (const l of letters) {
248
+ if (out.has(l.to) || now - l.start < l.ms) continue;
249
+ const d = here(L, l.to);
250
+ if (d && !MAIL.ASLEEP.has(d.node.state) && !d.night) out.set(l.to, l.drop); // asleep, or in bed for the night (#2568)
251
+ }
252
+ return out;
253
+ },
254
+ /** What is already in the traffic when this office's mail starts (#2564): seen, so it is not delivered again here. */
255
+ prime(items) { for (const it of items || []) seen.add(it.id); },
256
+ size: () => letters.length,
257
+ };
258
+ }
259
+
75
260
  /**
76
261
  * Where each character is. `sync(targets, now, graph, still)`: targets name → { x, y }; someone new is already there;
77
- * someone whose target moved walks from where they are now along the graph (routeVia) — or is there at once when
78
- * `still` (reduced motion); someone gone is forgotten. `at(name, now)` → { x, y, walking }; `moving(now)`.
262
+ * someone whose target moved walks from where they are now along the graph (routeVia; each diagonal leg an L when the
263
+ * graph is `straight`, #2634) — or is there at once when `still` (reduced motion); someone gone is forgotten. `at(name, now)` → { x, y, walking } and, while walking, `heading`: the
264
+ * current step's { dx, dy }, which picks a pack's walkDown / walkUp / walkSide strip; `moving(now)`.
79
265
  */
80
266
  export function createWalkers() {
81
267
  const by = new Map();
@@ -84,7 +270,7 @@ export function createWalkers() {
84
270
  const t = (now - w.start) / w.ms;
85
271
  if (t >= 1) return { x: w.to.x, y: w.to.y, walking: false };
86
272
  const p = along(w.pts, Math.max(0, t));
87
- return { x: p.x, y: p.y, walking: true };
273
+ return { x: p.x, y: p.y, walking: true, heading: headingAt(w.pts, Math.max(0, t)) };
88
274
  };
89
275
  return {
90
276
  sync(targets, now, graph, still = false) {
@@ -93,7 +279,8 @@ export function createWalkers() {
93
279
  const w = by.get(name);
94
280
  if (!w || still) { if (!w || w.to.x !== to.x || w.to.y !== to.y) by.set(name, { pts: null, to }); continue; }
95
281
  if (w.to.x === to.x && w.to.y === to.y) continue;
96
- const here = posAt(w, now), pts = routeVia(graph, { x: here.x, y: here.y }, { x: to.x, y: to.y });
282
+ const here = posAt(w, now), via = routeVia(graph, { x: here.x, y: here.y }, { x: to.x, y: to.y });
283
+ const pts = graph.straight ? straighten(via) : via; // #2634: timed along the longer way
97
284
  by.set(name, { pts, start: now, ms: Math.max(1, pathLength(pts) / WALK.SPEED), to });
98
285
  }
99
286
  },
@@ -104,14 +291,17 @@ export function createWalkers() {
104
291
 
105
292
  /**
106
293
  * The person at (x, y) in layout px as the scene draws them (#2431 review): each desk's box moved to where its person is
107
- * now (`posOf(desk)`: a walker's position; null: at the seat), the one drawn last (the nearest) tried first.
294
+ * now (`posOf(desk)`: a walker's position; null: at the seat), the one drawn last (the nearest) tried first — the same
295
+ * order the scene draws them in. `boxOf(desk, at)` (#2513 review): the box the skin draws that person in there (an
296
+ * uploaded character's strip, anchor and size, in its current pose and walking direction); null: the desk's own box.
108
297
  */
109
- export function personAt(desks, x, y, posOf) {
110
- const people = desks.filter((d) => d.node).map((d) => { const at = posOf(d) || d.seat; return { d, at, dx: at.x - d.seat.x, dy: at.y - d.seat.y }; })
298
+ export function personAt(desks, x, y, posOf, boxOf = null) {
299
+ const people = desks.filter((d) => d.node).map((d) => ({ d, at: posOf(d) || { ...d.seat, walking: false, heading: null } }))
111
300
  .sort((a, b) => a.at.y - b.at.y);
112
301
  for (let i = people.length - 1; i >= 0; i--) {
113
- const { d, dx, dy } = people[i];
114
- if (x >= d.x + dx && x < d.x + dx + d.w && y >= d.y + dy && y < d.y + dy + d.h) return d;
302
+ const { d, at } = people[i];
303
+ const b = (boxOf && boxOf(d, at)) || { x: d.x + at.x - d.seat.x, y: d.y + at.y - d.seat.y, w: d.w, h: d.h };
304
+ if (x >= b.x && x < b.x + b.w && y >= b.y && y < b.y + b.h) return d;
115
305
  }
116
306
  return null;
117
307
  }
@@ -143,11 +333,13 @@ export function bubbleAt(name, state, ms, lines) {
143
333
  * { key, calling, shown, box, tail? } with `box` the bubble's whole box ({ x, y, w, h }, for its whole line, so it never
144
334
  * moves while typing out) and `tail` the point its tail reaches: the strip from the box down to it is kept clear too. The ones calling for the person go first (they always show), then the one that started earliest
145
335
  * (the most typed out) — one already showing keeps its place. Each takes its spot, or one row up (`lift`) when that is
146
- * free; else it waits for another round. Returns the placed ones, each with its `dy` (0 or -lift).
336
+ * free; else it waits for another round. Returns the placed ones, each with its `dy` (0 or -lift). `rank` (optional,
337
+ * bubbleRank): a lower one goes first (else: calling 0, the rest 2); `blocked`: boxes no bubble may cover (name tags).
147
338
  */
148
- export function placeBubbles(items, lift = 22) {
149
- const order = [...items].sort((a, b) => (Number(b.calling) - Number(a.calling)) || (b.shown - a.shown) || (a.key < b.key ? -1 : a.key > b.key ? 1 : 0));
150
- const taken = [], out = [];
339
+ export function placeBubbles(items, lift = 22, blocked = []) {
340
+ const rank = (it) => (it.rank ?? (it.calling ? 0 : 2));
341
+ const order = [...items].sort((a, b) => (rank(a) - rank(b)) || (b.shown - a.shown) || (a.key < b.key ? -1 : a.key > b.key ? 1 : 0));
342
+ const taken = [...blocked], out = [];
151
343
  const hits = (r) => taken.some((t) => r.x < t.x + t.w && t.x < r.x + r.w && r.y < t.y + t.h && t.y < r.y + r.h);
152
344
  for (const it of order) {
153
345
  for (const dy of [0, -lift]) {
@@ -161,17 +353,47 @@ export function placeBubbles(items, lift = 22) {
161
353
  return out;
162
354
  }
163
355
 
356
+ /** Who speaks first when bubbles compete: calling for the person, then working, then everyone else. */
357
+ export const bubbleRank = (state) => (CALLING.has(state) ? 0 : state === "working" ? 1 : 2);
358
+ /** At most this many bubbles in an area over capacity (#2513). */
359
+ export const CROWD_BUBBLES = 3;
360
+ /** States whose bubbles are left out in an area over capacity (the chatter; a call or work still speaks). */
361
+ const QUIET_IN_CROWD = new Set(["idle", "paused", "stopped", "starting", "queued"]);
362
+ /**
363
+ * The bubbles a frame may show (#2513): in an area over capacity (`over`), none for idle or asleep people and at most
364
+ * CROWD_BUBBLES, the highest ranked first (bubbleRank); elsewhere all of them. `items`: { area, state, shown, key, … }.
365
+ */
366
+ export function thinBubbles(items, over) {
367
+ const byArea = new Map(), out = [];
368
+ for (const it of items) {
369
+ if (!over || !over.has(it.area)) { out.push(it); continue; }
370
+ if (QUIET_IN_CROWD.has(it.state)) continue;
371
+ if (!byArea.has(it.area)) byArea.set(it.area, []);
372
+ byArea.get(it.area).push(it);
373
+ }
374
+ for (const list of byArea.values()) {
375
+ list.sort((a, b) => (bubbleRank(a.state) - bubbleRank(b.state)) || (b.shown - a.shown) || (a.key < b.key ? -1 : a.key > b.key ? 1 : 0));
376
+ out.push(...list.slice(0, CROWD_BUBBLES));
377
+ }
378
+ return out;
379
+ }
380
+
164
381
  /** The lines for a state: the skin's (in the page's language), else the page's own. */
165
382
  export function linesFor(state, lang, pack, defaults) {
166
- const own = pack && pack.bubbles && pack.bubbles[lang] && pack.bubbles[lang][state];
167
- return own && own.length ? own : defaults[state] || [];
383
+ const mine = pack && own(own(pack.bubbles, lang), state);
384
+ return Array.isArray(mine) && mine.length ? mine : own(defaults, state) || [];
168
385
  }
169
386
 
170
387
  // ── Furniture ──
171
388
 
172
389
  /** The furniture active now: an item whose `activeWhen` names a state someone shown is in. */
173
- export function activeFurniture(pack, states) {
390
+ export function activeFurniture(pack, states, byName = new Map()) {
174
391
  const on = new Set();
175
- for (const f of (pack && pack.furniture) || []) if (f.activeWhen && f.activeWhen.some((s) => states.has(s))) on.add(f.id);
392
+ for (const f of (pack && pack.furniture) || []) {
393
+ if (!f.activeWhen) continue;
394
+ // activeFor (#2570): only those people's states count; else anyone's shown.
395
+ const hit = f.activeFor ? f.activeFor.some((n) => byName.has(n) && f.activeWhen.includes(byName.get(n))) : f.activeWhen.some((s) => states.has(s));
396
+ if (hit) on.add(f.id);
397
+ }
176
398
  return on;
177
399
  }