@songsid/agend 2.3.1 → 2.3.2-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/authority/authority-state.d.ts +4 -0
- package/dist/authority/authority-state.js +16 -10
- package/dist/authority/authority-state.js.map +1 -1
- package/dist/authority/session-authorizations.d.ts +7 -1
- package/dist/authority/session-authorizations.js +6 -4
- package/dist/authority/session-authorizations.js.map +1 -1
- package/dist/authority/web-authority.d.ts +19 -6
- package/dist/authority/web-authority.js +22 -8
- package/dist/authority/web-authority.js.map +1 -1
- package/dist/channel/adapters/discord.d.ts +2 -0
- package/dist/channel/adapters/discord.js +33 -3
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/channel/mcp-tools.js +5 -5
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/channel/types.d.ts +11 -0
- package/dist/config-validator.js +40 -3
- package/dist/config-validator.js.map +1 -1
- package/dist/daemon.d.ts +34 -3
- package/dist/daemon.js +227 -46
- package/dist/daemon.js.map +1 -1
- package/dist/delivery-outbox.d.ts +12 -1
- package/dist/delivery-outbox.js +67 -15
- package/dist/delivery-outbox.js.map +1 -1
- package/dist/doctor.d.ts +3 -0
- package/dist/doctor.js +9 -0
- package/dist/doctor.js.map +1 -1
- package/dist/event-log.d.ts +5 -0
- package/dist/event-log.js +13 -0
- package/dist/event-log.js.map +1 -1
- package/dist/fleet-manager.d.ts +140 -0
- package/dist/fleet-manager.js +414 -29
- package/dist/fleet-manager.js.map +1 -1
- package/dist/fleet-process-handlers.d.ts +1 -0
- package/dist/fleet-process-handlers.js +9 -1
- package/dist/fleet-process-handlers.js.map +1 -1
- package/dist/general-knowledge/skills/web-appearance/SKILL.canvas-worker-off.md +4 -4
- package/dist/general-knowledge/skills/web-appearance/SKILL.md +7 -7
- package/dist/general-knowledge/skills/web-dashboard/SKILL.canvas-worker-off.md +113 -55
- package/dist/general-knowledge/skills/web-dashboard/SKILL.md +114 -56
- package/dist/guest-seen-people.d.ts +38 -0
- package/dist/guest-seen-people.js +78 -0
- package/dist/guest-seen-people.js.map +1 -0
- package/dist/instance-config-impact.js +4 -1
- package/dist/instance-config-impact.js.map +1 -1
- package/dist/instance-lifecycle.d.ts +1 -0
- package/dist/instance-lifecycle.js +16 -1
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/locale.js +38 -10
- package/dist/locale.js.map +1 -1
- package/dist/mac-auto-restore.d.ts +117 -0
- package/dist/mac-auto-restore.js +294 -0
- package/dist/mac-auto-restore.js.map +1 -0
- package/dist/outbound-schemas.d.ts +2 -0
- package/dist/outbound-schemas.js +2 -0
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/public-web-gateway.js +9 -1
- package/dist/public-web-gateway.js.map +1 -1
- package/dist/settings-api.d.ts +26 -0
- package/dist/settings-api.js +107 -1
- package/dist/settings-api.js.map +1 -1
- package/dist/settings-change.js +37 -3
- package/dist/settings-change.js.map +1 -1
- package/dist/settings-effect.js +5 -2
- package/dist/settings-effect.js.map +1 -1
- package/dist/start-attempt.d.ts +9 -5
- package/dist/start-attempt.js +27 -8
- package/dist/start-attempt.js.map +1 -1
- package/dist/start-failure-chat.d.ts +4 -0
- package/dist/start-failure-chat.js +23 -6
- package/dist/start-failure-chat.js.map +1 -1
- package/dist/start-failure-notice.d.ts +4 -0
- package/dist/start-failure-notice.js +8 -0
- package/dist/start-failure-notice.js.map +1 -1
- package/dist/start-failure.d.ts +25 -0
- package/dist/start-failure.js +75 -2
- package/dist/start-failure.js.map +1 -1
- package/dist/turn-reply-guard.d.ts +25 -0
- package/dist/turn-reply-guard.js +111 -12
- package/dist/turn-reply-guard.js.map +1 -1
- package/dist/types.d.ts +18 -1
- package/dist/ui/backend-signin.js +5 -2
- package/dist/ui/chat-ambient.js +1 -1
- package/dist/ui/chat-effects.js +4 -4
- package/dist/ui/chat-render.js +5 -2
- package/dist/ui/chat-strings.js +2 -2
- package/dist/ui/chat-thread.js +2 -2
- package/dist/ui/first-run.js +2 -2
- package/dist/ui/fleet-canvas-view.js +114 -0
- package/dist/ui/fleet-canvas.js +15 -77
- package/dist/ui/fleet-office-editor.js +80 -5
- package/dist/ui/fleet-office-layout.js +197 -0
- package/dist/ui/fleet-office-model.js +6 -3
- package/dist/ui/fleet-office-overview.js +167 -0
- package/dist/ui/fleet-office-pack.d.ts +39 -0
- package/dist/ui/fleet-office-pack.js +240 -29
- package/dist/ui/fleet-office-skin.js +140 -29
- package/dist/ui/fleet-office-store.js +84 -5
- package/dist/ui/fleet-office-view.js +154 -0
- package/dist/ui/fleet-office-zones.js +95 -33
- package/dist/ui/fleet-office.js +779 -140
- package/dist/ui/fleet-usage.js +7 -6
- package/dist/ui/panel-chat.js +1 -1
- package/dist/ui/panel-details.js +2 -2
- package/dist/ui/panel-fleet.js +11 -77
- package/dist/ui/panel-log.js +2 -2
- package/dist/ui/panel-settings.js +578 -163
- package/dist/ui/search-dialog.js +26 -7
- package/dist/ui/settings-appearance.js +82 -29
- package/dist/ui/settings-apply.js +42 -7
- package/dist/ui/settings-dialogs.js +68 -22
- package/dist/ui/settings-draft.js +51 -0
- package/dist/ui/settings-guests.js +244 -78
- package/dist/ui/settings-learn.js +2 -2
- package/dist/ui/settings-model.js +2 -0
- package/dist/ui/settings-registry.js +142 -0
- package/dist/ui/settings-strings.js +102 -44
- package/dist/ui/settings-web-authority.js +34 -23
- package/dist/ui/shared/app-i18n.js +14 -0
- package/dist/ui/shared/app-nav.js +10 -5
- package/dist/ui/shared/app-prefs.js +80 -2
- package/dist/ui/shared/app-route.js +39 -5
- package/dist/ui/shared/app-shell.js +59 -4
- package/dist/ui/shared/app-stream.js +2 -1
- package/dist/ui/shared/app.css +254 -17
- package/dist/ui/shared/app.js +3 -0
- package/dist/ui/shared/effect-stock-trust.js +1 -1
- package/dist/ui/shared/identity-color.js +4 -4
- package/dist/ui/shared/signin.js +2 -2
- package/dist/ui/shared/ui-dialog.js +6 -1
- package/dist/ui/shared/ui-icons.js +2 -0
- package/dist/ui/shared/web-authorize.js +13 -12
- package/dist/web-api.d.ts +8 -2
- package/dist/web-api.js +160 -5
- package/dist/web-api.js.map +1 -1
- package/dist/web-auth.js +7 -1
- package/dist/web-auth.js.map +1 -1
- package/dist/web-experimental.d.ts +1 -1
- package/dist/web-experimental.js +1 -1
- package/dist/web-experimental.js.map +1 -1
- package/dist/web-mod-agent.js +2 -2
- package/dist/web-mod-agent.js.map +1 -1
- package/dist/web-office-skins.d.ts +108 -0
- package/dist/web-office-skins.js +298 -0
- package/dist/web-office-skins.js.map +1 -0
- package/dist/web-office-store.d.ts +8 -2
- package/dist/web-office-store.js +37 -7
- package/dist/web-office-store.js.map +1 -1
- package/dist/web-settings-patch.d.ts +6 -0
- package/dist/web-settings-patch.js +61 -4
- package/dist/web-settings-patch.js.map +1 -1
- package/dist/web-shell-routes.d.ts +4 -2
- package/dist/web-shell-routes.js +17 -7
- package/dist/web-shell-routes.js.map +1 -1
- package/dist/working-recovery.d.ts +104 -0
- package/dist/working-recovery.js +263 -0
- package/dist/working-recovery.js.map +1 -0
- package/dist/working-state-journal.d.ts +3 -1
- package/dist/working-state-journal.js +15 -2
- package/dist/working-state-journal.js.map +1 -1
- package/package.json +1 -1
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
// "studio" (the zones layout: work desks, a lounge, an alert corner and a nap corner; people walk to the area for their
|
|
5
5
|
// state). Every drawing call goes through the OfficeSkin interface below, so a skin pack (pictures from an artist,
|
|
6
6
|
// format v1 in fleet-office-pack.js) can replace any part; a part a pack does not supply falls back to the built-in
|
|
7
|
-
// drawing.
|
|
7
|
+
// drawing. An admin uploads a pack (#2513, the host keeps it: src/web-office-skins.ts); loadPackSkin() reads it back —
|
|
8
|
+
// its manifest checked here again (checkSkinPack), its pictures by their content ids — and spriteSkin() draws it.
|
|
8
9
|
//
|
|
9
10
|
// ── The interface ──
|
|
10
11
|
//
|
|
@@ -22,16 +23,25 @@
|
|
|
22
23
|
// @property {(ctx, desk, look, env) => void} drawDeskFront desks: in front — the desk's front, its sign, folders, name tag
|
|
23
24
|
// @property {(ctx, L, env) => void} [drawBackground] zones: the whole 1280×720 background, areas and their names
|
|
24
25
|
// @property {(ctx, item, active, ms, env) => void} [drawFurniture] zones: one furniture item (active: activeWhen holds)
|
|
26
|
+
// @property {(ctx, L, env) => void} [drawForeground] zones: a pack's foreground over the whole 1280×720 scene, above
|
|
27
|
+
// the people, furniture and items in the air, below the crowd counts, name tags and bubbles
|
|
25
28
|
// @property {(ctx, x, y, look, env) => void} [drawTag] zones: a name tag under feet at (x, y)
|
|
29
|
+
// @property {(ctx, area, n, env) => void} [drawCrowd] zones: an area's "+n" (who found no spot in it)
|
|
26
30
|
// @property {(ctx, desk, look, ms, env) => void} drawCharacter the occupant in its pose at time `ms` (0: still); in
|
|
27
|
-
// zones `desk` is the character's box at its current position (its feet at desk.seat)
|
|
31
|
+
// zones `desk` is the character's box at its current position (its feet at desk.seat); walking, `look.heading` is
|
|
32
|
+
// the current step's { dx, dy }
|
|
33
|
+
// @property {(look, at) => {x, y, w, h}} [charBox] zones: the box drawCharacter covers with the feet at `at`
|
|
34
|
+
// ({ x, y, walking, heading }): what hover and click hit (fleet-office-zones.js personAt)
|
|
28
35
|
// @property {(ctx, prop, ms, env) => void} drawProp a traffic item in the air: { kind, hue, x, y, t }
|
|
29
36
|
// @property {(ctx, x, y, text, env, lift?) => void} drawBubble a speech bubble whose tail points at (x, y), its box `lift` px higher
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
//
|
|
37
|
+
// @property {(env) => string} matte #2540: the colour around the scene (the Office fills the panel; the scene is 16:9)
|
|
38
|
+
// @property {object|null} [backdrop] a pack's loaded background picture (the page samples its edges for the matte)
|
|
39
|
+
// The desks layout's back and front layers change only when the fleet (or the zoom) does: the renderer keeps them as
|
|
40
|
+
// pictures and draws the characters, props and bubbles each frame. In zones the background is kept; furniture, people (walking
|
|
41
|
+
// along the pack's waypoints), tags, props and bubbles are drawn each frame, furniture by depth around the people (depth "y":
|
|
42
|
+
// among them by base y), then the foreground, then the crowd counts, tags and bubbles.
|
|
33
43
|
import { frameAt, nameHash } from "./fleet-office-model.js";
|
|
34
|
-
import { CHAR_DEPTH, LOGICAL } from "./fleet-office-pack.js";
|
|
44
|
+
import { CHAR_DEPTH, LOGICAL, checkSkinPack, own, packImages, poseStrip, stripBox } from "./fleet-office-pack.js";
|
|
35
45
|
|
|
36
46
|
export { nameHash };
|
|
37
47
|
|
|
@@ -212,14 +222,16 @@ function drawDeskFront(ctx, d, look, env) {
|
|
|
212
222
|
ctx.fillStyle = pal.text2; ctx.font = "600 8px system-ui, sans-serif"; ctx.textAlign = "center"; ctx.textBaseline = "middle";
|
|
213
223
|
ctx.fillText(fitText(ctx, env.labels.closed, 32), d.x + 48, d.y + 63);
|
|
214
224
|
}
|
|
215
|
-
// The name tag.
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
225
|
+
// The name tag (#2554: none when `notag` — a tile too small to read it; the page draws it again on hover).
|
|
226
|
+
if (!look.notag) {
|
|
227
|
+
ctx.fillStyle = pal.surface;
|
|
228
|
+
ctx.fillRect(d.x + 6, d.y + 78, d.w - 12, 16);
|
|
229
|
+
ctx.strokeStyle = look.match ? pal.focus : pal.border;
|
|
230
|
+
ctx.lineWidth = look.match ? 2 : 1;
|
|
231
|
+
ctx.strokeRect(d.x + 6.5, d.y + 78.5, d.w - 13, 15);
|
|
232
|
+
ctx.fillStyle = pal.text; ctx.font = "600 11px system-ui, sans-serif"; ctx.textAlign = "center"; ctx.textBaseline = "middle";
|
|
233
|
+
ctx.fillText(fitText(ctx, look.label, d.w - 18), d.x + d.w / 2, d.y + 86.5);
|
|
234
|
+
}
|
|
223
235
|
ctx.restore();
|
|
224
236
|
if (look.match) { ctx.save(); ctx.strokeStyle = pal.focus; ctx.lineWidth = 2; ctx.strokeRect(d.x + 2, d.y + 2, d.w - 4, d.h - 6); ctx.restore(); }
|
|
225
237
|
}
|
|
@@ -271,21 +283,58 @@ function drawBubble(ctx, x, y, text, env, lift = 0) {
|
|
|
271
283
|
ctx.restore();
|
|
272
284
|
}
|
|
273
285
|
|
|
286
|
+
/** A zones name tag's widest text (logical units): longer names are cut with an ellipsis. */
|
|
287
|
+
export const TAG_TEXT_W = 84;
|
|
288
|
+
const TAG_FONT = "600 10px system-ui, sans-serif";
|
|
289
|
+
/** The box a name tag under feet at (x, y) takes (what drawTag draws; bubbles keep clear of it, #2513). */
|
|
290
|
+
export function tagBox(ctx, x, y, label) {
|
|
291
|
+
ctx.save();
|
|
292
|
+
ctx.font = TAG_FONT;
|
|
293
|
+
const t = fitText(ctx, label, TAG_TEXT_W), w = Math.ceil((ctx.measureText ? ctx.measureText(t).width : t.length * 6) + 10);
|
|
294
|
+
ctx.restore();
|
|
295
|
+
return { x: Math.round(x - w / 2), y: y + 3, w, h: 14, text: t };
|
|
296
|
+
}
|
|
274
297
|
/** Zones: a name tag under someone's feet at (x, y). */
|
|
275
298
|
function drawTag(ctx, x, y, look, env) {
|
|
276
299
|
const { pal } = env;
|
|
277
300
|
ctx.save();
|
|
278
301
|
if (look.dim) ctx.globalAlpha = 0.35;
|
|
279
|
-
ctx.font =
|
|
280
|
-
const t = fitText(ctx, look.label,
|
|
302
|
+
ctx.font = TAG_FONT;
|
|
303
|
+
const t = fitText(ctx, look.label, TAG_TEXT_W), w = Math.ceil((ctx.measureText ? ctx.measureText(t).width : t.length * 6) + 10);
|
|
281
304
|
ctx.fillStyle = pal.surface; ctx.fillRect(Math.round(x - w / 2), y + 3, w, 14);
|
|
282
305
|
ctx.strokeStyle = look.match ? pal.focus : pal.border; ctx.lineWidth = look.match ? 2 : 1; ctx.strokeRect(Math.round(x - w / 2) + 0.5, y + 3.5, w - 1, 13);
|
|
283
306
|
ctx.fillStyle = pal.text; ctx.textAlign = "center"; ctx.textBaseline = "middle"; ctx.fillText(t, x, y + 10.5);
|
|
284
307
|
ctx.restore();
|
|
285
308
|
}
|
|
286
309
|
|
|
310
|
+
/**
|
|
311
|
+
* Zones: how many more want an area than it has room for — a chip in its bottom-right corner, inside its rect, so it
|
|
312
|
+
* never covers the area's name (#2513: "+42" over the nap corner's title).
|
|
313
|
+
*/
|
|
314
|
+
export function crowdChip(ctx, area, n) {
|
|
315
|
+
ctx.save();
|
|
316
|
+
ctx.font = "700 12px system-ui, sans-serif";
|
|
317
|
+
const text = `+${n}`, w = Math.ceil((ctx.measureText ? ctx.measureText(text).width : text.length * 7) + 12), h = 18, r = area.rect;
|
|
318
|
+
ctx.restore();
|
|
319
|
+
return { x: Math.round(r.x + r.w - w - 6), y: Math.round(r.y + r.h - h - 6), w, h, text };
|
|
320
|
+
}
|
|
321
|
+
function drawCrowd(ctx, area, n, env) {
|
|
322
|
+
if (!n) return;
|
|
323
|
+
const { pal } = env, b = crowdChip(ctx, area, n);
|
|
324
|
+
ctx.save();
|
|
325
|
+
ctx.fillStyle = pal.surface; ctx.fillRect(b.x, b.y, b.w, b.h);
|
|
326
|
+
ctx.strokeStyle = pal.border; ctx.lineWidth = 1; ctx.strokeRect(b.x + 0.5, b.y + 0.5, b.w - 1, b.h - 1);
|
|
327
|
+
ctx.fillStyle = pal.text; ctx.font = "700 12px system-ui, sans-serif"; ctx.textAlign = "center"; ctx.textBaseline = "middle";
|
|
328
|
+
ctx.fillText(b.text, b.x + b.w / 2, b.y + b.h / 2 + 0.5);
|
|
329
|
+
ctx.restore();
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/** The built-in character in zones covers this box around its feet (as zonesLayout's desk box). */
|
|
333
|
+
const charBox = (look, at) => ({ x: at.x - 24, y: at.y - 52, w: 48, h: 66 });
|
|
334
|
+
/** #2540: Desks' matte (around the scene): the page's background token, which the desks' floor is too (Studio has its own). */
|
|
335
|
+
const matte = (env) => env.pal.bg;
|
|
287
336
|
/** @type {OfficeSkin} */
|
|
288
|
-
export const pixelSkin = { id: "pixel", drawFloor, drawRoom, drawDeskBack, drawCharacter, drawDeskFront, drawProp, drawBubble, drawTag };
|
|
337
|
+
export const pixelSkin = { id: "pixel", drawFloor, drawRoom, drawDeskBack, drawCharacter, drawDeskFront, drawProp, drawBubble, drawTag, drawCrowd, charBox, matte };
|
|
289
338
|
|
|
290
339
|
// ── The built-in studio: the zones layout, drawn procedurally ──
|
|
291
340
|
|
|
@@ -346,8 +395,6 @@ function drawBackground(ctx, L, env) {
|
|
|
346
395
|
ctx.strokeStyle = pal.border; ctx.lineWidth = 2; ctx.strokeRect(r.x + 1, r.y + 1, r.w - 2, r.h - 2);
|
|
347
396
|
ctx.fillStyle = pal.text; ctx.font = "600 16px system-ui, sans-serif"; ctx.textBaseline = "top"; ctx.textAlign = "left";
|
|
348
397
|
ctx.fillText(fitText(ctx, env.labels.areas[a.name] || a.name, r.w - 60), r.x + 12, r.y + 10);
|
|
349
|
-
const crowd = L.crowd && L.crowd.get(a.name);
|
|
350
|
-
if (crowd) { ctx.fillStyle = pal.text2; ctx.textAlign = "right"; ctx.fillText(`+${crowd}`, r.x + r.w - 12, r.y + 10); }
|
|
351
398
|
ctx.restore();
|
|
352
399
|
}
|
|
353
400
|
}
|
|
@@ -396,9 +443,11 @@ function drawFurniture(ctx, f, active, ms, env) {
|
|
|
396
443
|
}
|
|
397
444
|
}
|
|
398
445
|
/** @type {OfficeSkin} */
|
|
399
|
-
|
|
446
|
+
/** #2540: the studio's matte is its hall floor (the scene's edge), so the panel around the scene reads as more of it. */
|
|
447
|
+
const studioMatte = (env) => (ZONE_FLOOR[env.mode] || ZONE_FLOOR.dark).hall;
|
|
448
|
+
export const studioSkin = { ...pixelSkin, id: "studio", pack: STUDIO_PACK, drawBackground, drawFurniture, matte: studioMatte };
|
|
400
449
|
|
|
401
|
-
// ── Skin packs
|
|
450
|
+
// ── Skin packs ──
|
|
402
451
|
|
|
403
452
|
/**
|
|
404
453
|
* An OfficeSkin from a checked pack (fleet-office-pack.js checkSkinPack) and its loaded pictures (`images`: src → a
|
|
@@ -406,20 +455,29 @@ export const studioSkin = { ...pixelSkin, id: "studio", pack: STUDIO_PACK, drawB
|
|
|
406
455
|
*/
|
|
407
456
|
export function spriteSkin(pack, images, base = pixelSkin) {
|
|
408
457
|
const img = (src) => (src && images.get(src)) || null;
|
|
409
|
-
|
|
458
|
+
// A strip's frame at its displayed size (dw × dh, unsmoothed), its anchor on (x, y); mirrored about the anchor.
|
|
459
|
+
const strip = (ctx, st, ms, x, y, frame = null, mirror = false) => {
|
|
410
460
|
const im = img(st && st.src);
|
|
411
461
|
if (!im) return false;
|
|
412
|
-
const f = frame ?? frameAt(ms, st.frames, st.fps),
|
|
462
|
+
const f = frame ?? frameAt(ms, st.frames, st.fps), b = stripBox(st, x, y, mirror);
|
|
413
463
|
ctx.save();
|
|
414
464
|
ctx.imageSmoothingEnabled = false;
|
|
415
|
-
ctx.drawImage(im, f * st.w, 0, st.w, st.h,
|
|
465
|
+
if (mirror) { ctx.translate(b.x + b.w, b.y); ctx.scale(-1, 1); ctx.drawImage(im, f * st.w, 0, st.w, st.h, 0, 0, b.w, b.h); }
|
|
466
|
+
else ctx.drawImage(im, f * st.w, 0, st.w, st.h, b.x, b.y, b.w, b.h);
|
|
416
467
|
ctx.restore();
|
|
417
468
|
return true;
|
|
418
469
|
};
|
|
470
|
+
const setOf = (look) => own(pack.characters, own(pack.byInstance, look.name)) || own(pack.characters, "default");
|
|
471
|
+
/** The strip a look shows (its pose; walking, by its heading), when its picture is loaded. */
|
|
472
|
+
const shown = (look, walking = look.pose === "walk") => {
|
|
473
|
+
const ps = poseStrip(setOf(look), walking ? "walk" : look.pose, look.heading);
|
|
474
|
+
return ps && img(ps.strip.src) ? ps : null;
|
|
475
|
+
};
|
|
419
476
|
return {
|
|
420
477
|
...base,
|
|
421
478
|
id: pack.id,
|
|
422
479
|
pack,
|
|
480
|
+
backdrop: img(pack.background),
|
|
423
481
|
drawRoom(ctx, room, env) {
|
|
424
482
|
const floor = img(pack.room.floor), wall = img(pack.room.wall);
|
|
425
483
|
if (!floor) { base.drawRoom(ctx, room, env); return; }
|
|
@@ -448,6 +506,11 @@ export function spriteSkin(pack, images, base = pixelSkin) {
|
|
|
448
506
|
if (!im) { drawBackground(ctx, L, env); return; }
|
|
449
507
|
ctx.save(); ctx.imageSmoothingEnabled = false; ctx.drawImage(im, 0, 0, LOGICAL.w, LOGICAL.h); ctx.restore();
|
|
450
508
|
},
|
|
509
|
+
drawForeground(ctx, L, env) {
|
|
510
|
+
const im = img(pack.foreground);
|
|
511
|
+
if (!im) return;
|
|
512
|
+
ctx.save(); ctx.imageSmoothingEnabled = false; ctx.drawImage(im, 0, 0, LOGICAL.w, LOGICAL.h); ctx.restore();
|
|
513
|
+
},
|
|
451
514
|
drawFurniture(ctx, f, active, ms, env) {
|
|
452
515
|
if (f.shape) { drawFurniture(ctx, f, active, ms, env); return; }
|
|
453
516
|
// Active: its strip (or activeSprite) plays; otherwise its first frame holds.
|
|
@@ -456,19 +519,67 @@ export function spriteSkin(pack, images, base = pixelSkin) {
|
|
|
456
519
|
},
|
|
457
520
|
drawCharacter(ctx, d, look, ms, env) {
|
|
458
521
|
if (look.pose === "away") return;
|
|
459
|
-
const
|
|
522
|
+
const ps = shown(look);
|
|
460
523
|
ctx.save();
|
|
461
524
|
if (look.dim) ctx.globalAlpha = 0.35;
|
|
462
|
-
const done = !!
|
|
525
|
+
const done = !!ps && strip(ctx, ps.strip, ms, d.seat.x, d.seat.y, null, ps.mirror);
|
|
463
526
|
ctx.restore();
|
|
464
527
|
if (!done) base.drawCharacter(ctx, d, look, ms, env);
|
|
465
528
|
},
|
|
466
|
-
|
|
529
|
+
charBox(look, at) {
|
|
530
|
+
const ps = shown({ ...look, heading: at.heading }, !!at.walking);
|
|
531
|
+
return ps ? stripBox(ps.strip, at.x, at.y, ps.mirror) : base.charBox(look, at);
|
|
532
|
+
},
|
|
533
|
+
drawProp(ctx, it, ms, env) { if (!strip(ctx, own(pack.props, it.hue), ms, it.x, it.y)) base.drawProp(ctx, it, ms, env); },
|
|
467
534
|
};
|
|
468
535
|
}
|
|
469
536
|
|
|
470
|
-
/** The skins this page
|
|
537
|
+
/** The skins built into this page; an uploaded pack is loaded by loadPackSkin. */
|
|
471
538
|
export const SKINS = new Map([["pixel", pixelSkin], ["studio", studioSkin]]);
|
|
472
|
-
export const
|
|
539
|
+
export const BUILT_IN_SKINS = ["pixel", "studio"];
|
|
540
|
+
/** A skin by id: a built-in one, else a pack loaded here (`packs`: id → skin), else the default (pixel). */
|
|
541
|
+
export const skinFor = (id, packs = null) => SKINS.get(id) || (packs && packs.get(id)) || pixelSkin;
|
|
542
|
+
|
|
543
|
+
const loaded = new Map(); // `${id}@${sha}` → Promise<OfficeSkin | null>
|
|
544
|
+
const MAX_LOADED = 16;
|
|
545
|
+
const defaultImage = (url) => new Promise((resolve) => {
|
|
546
|
+
if (typeof Image !== "function") { resolve(null); return; }
|
|
547
|
+
const im = new Image();
|
|
548
|
+
im.onload = () => resolve(im);
|
|
549
|
+
im.onerror = () => resolve(null);
|
|
550
|
+
im.src = url;
|
|
551
|
+
});
|
|
552
|
+
/**
|
|
553
|
+
* An uploaded pack (GET /ui/office-skins/<id>?v=<sha>) as an OfficeSkin: its manifest checked (null when it is not a
|
|
554
|
+
* pack: the page then draws a built-in style and says so), its pictures loaded from their content ids (one that does
|
|
555
|
+
* not load is drawn by the built-in skin). Kept per id and sha, so a pack is fetched once.
|
|
556
|
+
*/
|
|
557
|
+
export function loadPackSkin(id, sha, { fetch: get = globalThis.fetch, image = defaultImage } = {}) {
|
|
558
|
+
const key = `${id}@${sha}`;
|
|
559
|
+
if (loaded.has(key)) return loaded.get(key);
|
|
560
|
+
const base = `/ui/office-skins/${encodeURIComponent(id)}`;
|
|
561
|
+
const job = (async () => {
|
|
562
|
+
try {
|
|
563
|
+
const r = await get(`${base}?v=${encodeURIComponent(sha)}`, { method: "GET", headers: { "Content-Type": "application/json" } });
|
|
564
|
+
if (!r || !r.ok) return null;
|
|
565
|
+
const d = await r.json();
|
|
566
|
+
const c = checkSkinPack(d && d.manifest);
|
|
567
|
+
if (!c.ok || c.pack.id !== id) return null;
|
|
568
|
+
const files = d.files && typeof d.files === "object" ? d.files : {};
|
|
569
|
+
const images = new Map();
|
|
570
|
+
await Promise.all(packImages(c.pack).map(async (src) => {
|
|
571
|
+
const fid = own(files, src);
|
|
572
|
+
if (typeof fid !== "string" || !/^[0-9a-f]{32}$/.test(fid)) return;
|
|
573
|
+
const im = await image(`${base}/img/${fid}.png`);
|
|
574
|
+
if (im) images.set(src, im);
|
|
575
|
+
}));
|
|
576
|
+
return spriteSkin(c.pack, images);
|
|
577
|
+
} catch { return null; }
|
|
578
|
+
})();
|
|
579
|
+
loaded.set(key, job);
|
|
580
|
+
if (loaded.size > MAX_LOADED) loaded.delete(loaded.keys().next().value);
|
|
581
|
+
job.then((skin) => { if (!skin) loaded.delete(key); }); // a failed read is tried again next time
|
|
582
|
+
return job;
|
|
583
|
+
}
|
|
473
584
|
/** The people's depth in zones (fleet-office-pack.js). */
|
|
474
585
|
export { CHAR_DEPTH };
|
|
@@ -2,12 +2,32 @@
|
|
|
2
2
|
// - the offices people made: the host's (GET / PUT /ui/offices, src/web-office-store.ts — one list for every viewer);
|
|
3
3
|
// what comes back is checked again here (fleet-office-model.js officesOk), never trusted;
|
|
4
4
|
// - this browser's own choices (localStorage; blocked storage: this page only, as the canvas's Canvas/List): which
|
|
5
|
-
// office is shown, the auto office's member filter, and its style (skin)
|
|
6
|
-
//
|
|
7
|
-
//
|
|
5
|
+
// office is shown, the auto office's member filter, and its style (skin); #2540: the view (zoom and centre) and the
|
|
6
|
+
// fit mode (Fit or Fill) per office; #2554: whether All offices shows the lobby tile.
|
|
7
|
+
// - the skin packs an admin uploaded (#2513: GET /ui/office-skins, POST to upload one, DELETE to remove one;
|
|
8
|
+
// src/web-office-skins.ts). The office editor and the layout editor call saveOffices() with the whole list and the rev
|
|
9
|
+
// they read (a change in between answers 409).
|
|
8
10
|
import { AUTO_ID, filterOk, officesOk } from "./fleet-office-model.js";
|
|
11
|
+
import { fitModeOk, savedOk } from "./fleet-office-view.js";
|
|
9
12
|
|
|
10
|
-
const CHOICE_KEY = "agend_org_office", FILTER_KEY = "agend_org_office_filter", SKIN_KEY = "agend_org_office_skin";
|
|
13
|
+
const CHOICE_KEY = "agend_org_office", FILTER_KEY = "agend_org_office_filter", SKIN_KEY = "agend_org_office_skin", ASLEEP_KEY = "agend_org_office_hide_asleep";
|
|
14
|
+
/** #2554: "0" when All offices leaves the lobby tile out (default: shown). */
|
|
15
|
+
const LOBBY_KEY = "agend_org_office_lobby";
|
|
16
|
+
/** #2540: { "<office>@<style>": [k, cx, cy] (and a 1 after them: a fit) } — the last VIEWS_MAX views, the most recent last. */
|
|
17
|
+
const VIEW_KEY = "agend_org_office_view", VIEWS_MAX = 32;
|
|
18
|
+
/** #2540: { "<office>@<style>": "cover" } — the offices whose fit fills the panel (the rest: "contain"), the last VIEWS_MAX. */
|
|
19
|
+
const FIT_KEY = "agend_org_office_fit";
|
|
20
|
+
const PACK_ID = /^[A-Za-z0-9_-]{1,40}$/;
|
|
21
|
+
/** The host's list of skin packs, checked: [{ id, name, sha }] (anything odd dropped). */
|
|
22
|
+
export function skinListOk(d) {
|
|
23
|
+
const out = [], ids = new Set();
|
|
24
|
+
for (const p of Array.isArray(d && d.packs) ? d.packs.slice(0, 64) : []) {
|
|
25
|
+
if (!p || typeof p.id !== "string" || !PACK_ID.test(p.id) || ids.has(p.id) || typeof p.sha !== "string" || !/^[0-9a-f]{32}$/.test(p.sha)) continue;
|
|
26
|
+
ids.add(p.id);
|
|
27
|
+
out.push({ id: p.id, name: typeof p.name === "string" && p.name.trim() ? p.name.trim().slice(0, 60) : p.id, sha: p.sha });
|
|
28
|
+
}
|
|
29
|
+
return out;
|
|
30
|
+
}
|
|
11
31
|
|
|
12
32
|
/** `fetch`: the caller's (a lease's, so an answer after the person moved on is dropped); `storage`: default localStorage. */
|
|
13
33
|
export function createOfficeStore({ fetch: get = globalThis.fetch, storage } = {}) {
|
|
@@ -37,7 +57,35 @@ export function createOfficeStore({ fetch: get = globalThis.fetch, storage } = {
|
|
|
37
57
|
return r.ok ? { ok: true, rev: d.rev, offices: officesOk(d.offices) } : { ok: false, status: r.status, error: d.error || "offices_failed" };
|
|
38
58
|
} catch { return { ok: false, status: 0, error: "offices_failed" }; }
|
|
39
59
|
},
|
|
40
|
-
/** The
|
|
60
|
+
/** The skin packs the host keeps: { rev, packs: [{ id, name, sha }] }; null when they could not be read. */
|
|
61
|
+
async skins() {
|
|
62
|
+
try {
|
|
63
|
+
const r = await get("/ui/office-skins", { method: "GET", headers });
|
|
64
|
+
if (!r || !r.ok) return null;
|
|
65
|
+
const d = await r.json();
|
|
66
|
+
return { rev: typeof (d && d.rev) === "string" ? d.rev : "", packs: skinListOk(d) };
|
|
67
|
+
} catch { return null; }
|
|
68
|
+
},
|
|
69
|
+
/** Upload a pack (an admin session): { manifest, files: { name: base64 }, name? } → { ok, pack } or { ok: false, status, error, detail }. */
|
|
70
|
+
async uploadSkin(body) {
|
|
71
|
+
try {
|
|
72
|
+
const r = await get("/ui/office-skins", { method: "POST", headers, body: JSON.stringify(body) });
|
|
73
|
+
const d = await r.json().catch(() => ({}));
|
|
74
|
+
return r.ok ? { ok: true, pack: d.pack, rev: d.rev } : { ok: false, status: r.status, error: d.error || "skin_failed", detail: d.detail || "" };
|
|
75
|
+
} catch { return { ok: false, status: 0, error: "skin_failed", detail: "" }; }
|
|
76
|
+
},
|
|
77
|
+
/** Remove a pack (an admin session). */
|
|
78
|
+
async deleteSkin(id) {
|
|
79
|
+
try {
|
|
80
|
+
const r = await get(`/ui/office-skins/${encodeURIComponent(id)}`, { method: "DELETE", headers });
|
|
81
|
+
const d = await r.json().catch(() => ({}));
|
|
82
|
+
return r.ok ? { ok: true } : { ok: false, status: r.status, error: d.error || "skin_failed" };
|
|
83
|
+
} catch { return { ok: false, status: 0, error: "skin_failed" }; }
|
|
84
|
+
},
|
|
85
|
+
/**
|
|
86
|
+
* The office shown here: an id (one the host no longer has falls back to the auto office where it is used), or
|
|
87
|
+
* #2554's All offices (fleet-office-overview.js ALL_ID; with fewer than two offices stored: the auto office).
|
|
88
|
+
*/
|
|
41
89
|
choice: () => { const v = read(CHOICE_KEY); return typeof v === "string" && v && v.length <= 40 ? v : AUTO_ID; },
|
|
42
90
|
saveChoice: (id) => write(CHOICE_KEY, String(id)),
|
|
43
91
|
filter: () => { const s = read(FILTER_KEY); try { return filterOk(s ? JSON.parse(s) : null); } catch { return filterOk(null); } },
|
|
@@ -45,5 +93,36 @@ export function createOfficeStore({ fetch: get = globalThis.fetch, storage } = {
|
|
|
45
93
|
/** The whole fleet's style here: a skin id ("pixel": desks, "studio": zones); an unknown one is the default where used. */
|
|
46
94
|
skin: () => { const v = read(SKIN_KEY); return typeof v === "string" && /^[A-Za-z0-9_-]{1,40}$/.test(v) ? v : "pixel"; },
|
|
47
95
|
saveSkin: (id) => write(SKIN_KEY, String(id)),
|
|
96
|
+
/** Whether paused and stopped people are left out of the Office here (default: shown). */
|
|
97
|
+
hideAsleep: () => read(ASLEEP_KEY) === "1",
|
|
98
|
+
saveHideAsleep: (v) => write(ASLEEP_KEY, v ? "1" : "0"),
|
|
99
|
+
/** #2554: whether All offices shows the lobby tile (the instances in no stored office) here (default: shown). */
|
|
100
|
+
lobbyTile: () => read(LOBBY_KEY) !== "0",
|
|
101
|
+
saveLobbyTile: (v) => write(LOBBY_KEY, v ? "1" : "0"),
|
|
102
|
+
/** The view kept for `key` (an office and its style): { k, cx, cy } (fleet-office-view.js savedOf), or null. */
|
|
103
|
+
view: (key) => {
|
|
104
|
+
try { const m = JSON.parse(read(VIEW_KEY) || "{}"); const a = m && Object.prototype.hasOwnProperty.call(m, key) ? m[key] : null;
|
|
105
|
+
return Array.isArray(a) ? savedOk({ k: a[0], cx: a[1], cy: a[2], fit: a[3] === 1 }) : null; } catch { return null; }
|
|
106
|
+
},
|
|
107
|
+
saveView: (key, v) => {
|
|
108
|
+
const ok = savedOk(v);
|
|
109
|
+
if (!ok || typeof key !== "string" || key.length > 100) return;
|
|
110
|
+
let m = {};
|
|
111
|
+
try { const o = JSON.parse(read(VIEW_KEY) || "{}"); if (o && typeof o === "object" && !Array.isArray(o)) m = o; } catch { m = {}; }
|
|
112
|
+
const r = (n) => Math.round(n * 1000) / 1000;
|
|
113
|
+
const next = Object.entries(m).filter(([k2, a]) => k2 !== key && Array.isArray(a)).slice(-(VIEWS_MAX - 1));
|
|
114
|
+
write(VIEW_KEY, JSON.stringify(Object.fromEntries([...next, [key, [r(ok.k), r(ok.cx), r(ok.cy), ...(ok.fit ? [1] : [])]]])));
|
|
115
|
+
},
|
|
116
|
+
/** The fit mode kept for `key`: "contain" (the default) or "cover" (Fill). */
|
|
117
|
+
fitMode: (key) => {
|
|
118
|
+
try { const m = JSON.parse(read(FIT_KEY) || "{}"); return fitModeOk(m && Object.prototype.hasOwnProperty.call(m, key) ? m[key] : null); } catch { return "contain"; }
|
|
119
|
+
},
|
|
120
|
+
saveFitMode: (key, mode) => {
|
|
121
|
+
if (typeof key !== "string" || key.length > 100) return;
|
|
122
|
+
let m = {};
|
|
123
|
+
try { const o = JSON.parse(read(FIT_KEY) || "{}"); if (o && typeof o === "object" && !Array.isArray(o)) m = o; } catch { m = {}; }
|
|
124
|
+
const next = Object.entries(m).filter(([k2, v]) => k2 !== key && v === "cover").slice(-(VIEWS_MAX - 1));
|
|
125
|
+
write(FIT_KEY, JSON.stringify(Object.fromEntries(fitModeOk(mode) === "cover" ? [...next, [key, "cover"]] : next)));
|
|
126
|
+
},
|
|
48
127
|
};
|
|
49
128
|
}
|
|
@@ -0,0 +1,154 @@
|
|
|
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 stays wholly inside.
|
|
13
|
+
//
|
|
14
|
+
// Around the scene is a matte: the pack's `matte` (fleet-office-pack.js), else the colour of the background's edge
|
|
15
|
+
// pixels (sampled once per style: edgeMatte), else the skin's own (Studio's hall floor; Desks': the dashboard's
|
|
16
|
+
// background). It paints the whole canvas outside the scene (and the stage behind it), never the page's grey.
|
|
17
|
+
import { clampK, pinchTo, zoomTo } from "./fleet-canvas-view.js";
|
|
18
|
+
import { CANVAS_MAX } from "./fleet-office-model.js";
|
|
19
|
+
|
|
20
|
+
/** The zoom's range, in CSS px per logical unit. */
|
|
21
|
+
export const OK_MIN = 0.1, OK_MAX = 8;
|
|
22
|
+
const EPS = 1e-6;
|
|
23
|
+
const clampO = (k) => clampK(k, OK_MIN, OK_MAX);
|
|
24
|
+
/** The highest pixel level within the range at this ratio (at least 1). */
|
|
25
|
+
const topLevel = (dpr) => Math.max(1, Math.floor(OK_MAX * dpr + EPS));
|
|
26
|
+
|
|
27
|
+
/** `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. */
|
|
28
|
+
export function snapK(k, dpr = 1) {
|
|
29
|
+
const c = clampO(k), n = c * dpr;
|
|
30
|
+
if (n < 1 - EPS) return c;
|
|
31
|
+
return Math.min(topLevel(dpr), Math.max(1, Math.round(n))) / dpr;
|
|
32
|
+
}
|
|
33
|
+
/** Whether `k` is a pixel level (a whole number of device px per logical unit). */
|
|
34
|
+
export const isPixelK = (k, dpr = 1) => k * dpr >= 1 - EPS && Math.abs(k * dpr - Math.round(k * dpr)) < 1e-6;
|
|
35
|
+
/** One step in (`dir` > 0) or out: the next pixel level; below one device pixel a unit, by 1.25. */
|
|
36
|
+
export function stepK(k, dir, dpr = 1) {
|
|
37
|
+
const n = k * dpr;
|
|
38
|
+
if (dir > 0) {
|
|
39
|
+
if (n >= 1 - EPS) return Math.min(topLevel(dpr), Math.floor(n + EPS) + 1) / dpr;
|
|
40
|
+
return n * 1.25 >= 1 - EPS ? 1 / dpr : clampO(k * 1.25);
|
|
41
|
+
}
|
|
42
|
+
if (n > 1 + EPS) return Math.max(1, Math.ceil(n - EPS) - 1) / dpr;
|
|
43
|
+
return clampO(k * 0.8);
|
|
44
|
+
}
|
|
45
|
+
/** The view at scale `k` (in range, continuous: the wheel's) about the viewport point (cx, cy). */
|
|
46
|
+
export const zoomOffice = (v, k, cx, cy) => zoomTo(v, clampO(k), cx, cy);
|
|
47
|
+
/** A pinch step: `raw` the zoom the pinch has reached (continuous); the world point under the old midpoint follows. */
|
|
48
|
+
export const pinchOffice = (v, from, to, raw) => pinchTo(v, from, to, clampO(raw));
|
|
49
|
+
|
|
50
|
+
/** The fit modes: "contain" (the whole scene shows) and "cover" (the panel is filled, the overflow cropped). */
|
|
51
|
+
export const FIT_MODES = ["contain", "cover"];
|
|
52
|
+
export const fitModeOk = (m) => (m === "cover" ? "cover" : "contain");
|
|
53
|
+
/** The scale at which a `w`×`h` scene fits a `size` viewport: contain (the whole scene) or cover (the whole panel). */
|
|
54
|
+
export function fitK(w, h, size, mode = "contain", margin = 0) {
|
|
55
|
+
const sx = (size.w - 2 * margin) / Math.max(1, w), sy = (size.h - 2 * margin) / Math.max(1, h);
|
|
56
|
+
return clampO(fitModeOk(mode) === "cover" ? Math.max(sx, sy) : Math.min(sx, sy));
|
|
57
|
+
}
|
|
58
|
+
/** The scene at fitK, centred. */
|
|
59
|
+
export function fitScene(w, h, size, mode = "contain", margin = 0) {
|
|
60
|
+
const k = fitK(w, h, size, mode, margin);
|
|
61
|
+
return { k, x: (size.w - w * k) / 2, y: (size.h - h * k) / 2 };
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* 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,
|
|
65
|
+
* 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.
|
|
66
|
+
*/
|
|
67
|
+
export const desksK = (dpr = 1) => Math.max(1, Math.round(dpr)) / dpr;
|
|
68
|
+
/** The view a scene opens on with nothing kept: zones fitted (the office's mode), desks from the top left at desksK. */
|
|
69
|
+
export const openingView = (w, h, size, dpr, zones, mode = "contain") => (zones ? fitScene(w, h, size, mode) : { k: desksK(dpr), x: 0, y: 0 });
|
|
70
|
+
/** One axis of clampPan: offset `o` of a scene `s` long in a viewport `n` long. */
|
|
71
|
+
const clampAxis = (o, s, n) => (s >= n ? Math.min(0, Math.max(n - s, o)) : Math.min(n - s, Math.max(0, o)));
|
|
72
|
+
/**
|
|
73
|
+
* The view kept on the scene: along an axis where the scene is at least the viewport it covers the viewport (no gap at
|
|
74
|
+
* either edge); where it is smaller it stays wholly inside. Nothing can be scrolled off-screen.
|
|
75
|
+
*/
|
|
76
|
+
export function clampPan(v, w, h, size) {
|
|
77
|
+
return { k: v.k, x: clampAxis(v.x, w * v.k, size.w), y: clampAxis(v.y, h * v.k, size.h) };
|
|
78
|
+
}
|
|
79
|
+
/** The zoom as a person reads it: "150%". */
|
|
80
|
+
export const zoomLabel = (k) => `${Math.round(k * 100)}%`;
|
|
81
|
+
/**
|
|
82
|
+
* 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
|
|
83
|
+
* a third of its height), `margin` px in from the bottom right corner — always inside the stage.
|
|
84
|
+
*/
|
|
85
|
+
export function miniBox(size, w, h, margin = 12) {
|
|
86
|
+
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);
|
|
87
|
+
let bw = Math.min(280, Math.max(120, Math.round(size.w / 7)), roomW), bh = bw / ar;
|
|
88
|
+
const maxH = Math.min(roomH, Math.max(60, size.h / 3));
|
|
89
|
+
if (bh > maxH) { bh = maxH; bw = bh * ar; }
|
|
90
|
+
bw = Math.floor(bw); bh = Math.floor(bh);
|
|
91
|
+
return { x: size.w - margin - bw, y: size.h - margin - bh, w: bw, h: bh };
|
|
92
|
+
}
|
|
93
|
+
/** The view's frame on the overview (logical units): the part of the scene in view (never outside it). */
|
|
94
|
+
export function miniFrame(v, size, w, h) {
|
|
95
|
+
const x0 = Math.max(0, -v.x / v.k), y0 = Math.max(0, -v.y / v.k);
|
|
96
|
+
const x1 = Math.min(w, (size.w - v.x) / v.k), y1 = Math.min(h, (size.h - v.y) / v.k);
|
|
97
|
+
return { x: x0, y: y0, width: Math.max(0, x1 - x0), height: Math.max(0, y1 - y0) };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ── Kept per device, per office: the zoom and the logical point in the middle (the viewport's size may differ) ──
|
|
101
|
+
|
|
102
|
+
/** 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). */
|
|
103
|
+
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 } : {}) });
|
|
104
|
+
/** A kept view, checked: { k, cx, cy } with finite numbers (k in range), else null. */
|
|
105
|
+
export function savedOk(x) {
|
|
106
|
+
if (!x || typeof x !== "object") return null;
|
|
107
|
+
const { k, cx, cy } = x;
|
|
108
|
+
if (![k, cx, cy].every((n) => typeof n === "number" && Number.isFinite(n)) || k <= 0) return null;
|
|
109
|
+
if (Math.abs(cx) > 1e6 || Math.abs(cy) > 1e6) return null;
|
|
110
|
+
return { k: clampO(k), cx, cy, ...(x.fit === true ? { fit: true } : {}) };
|
|
111
|
+
}
|
|
112
|
+
/** The view a kept one gives in a `size` viewport: the same zoom, the same logical point in the middle. */
|
|
113
|
+
export function viewFromSaved(s, size) {
|
|
114
|
+
const k = clampO(s.k);
|
|
115
|
+
return { k, x: size.w / 2 - s.cx * k, y: size.h / 2 - s.cy * k };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ── Drawing ──
|
|
119
|
+
|
|
120
|
+
/** The cached layers' budget (device px): beyond it they are drawn coarser and scaled up by a whole number. */
|
|
121
|
+
export const LAYER_MAX = { side: CANVAS_MAX.side, area: 8 * 1024 * 1024 };
|
|
122
|
+
/**
|
|
123
|
+
* The layers' resolution (layer px per logical unit) for a view at `a` device px a unit and a `w`×`h` scene: `a`
|
|
124
|
+
* itself (the layer is blitted 1:1), or — over the budget — `a` divided by the smallest whole number that fits it,
|
|
125
|
+
* so the blit is still an integer scale-up (nearest neighbour: crisp).
|
|
126
|
+
*/
|
|
127
|
+
export function layerScale(a, w, h, max = LAYER_MAX) {
|
|
128
|
+
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)));
|
|
129
|
+
if (a <= cap) return a;
|
|
130
|
+
return a / Math.ceil(a / cap - EPS);
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* The matte from a background's edge pixels: `strips` are RGBA pixel arrays (the top and bottom rows, the left and
|
|
134
|
+
* right columns); the mean of their opaque pixels as #rrggbb, or null when none is opaque.
|
|
135
|
+
*/
|
|
136
|
+
export function edgeMatte(strips) {
|
|
137
|
+
let r = 0, g = 0, b = 0, n = 0;
|
|
138
|
+
for (const s of strips) {
|
|
139
|
+
if (!s || typeof s.length !== "number") continue;
|
|
140
|
+
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++; }
|
|
141
|
+
}
|
|
142
|
+
if (!n) return null;
|
|
143
|
+
const hx = (v) => Math.round(v / n).toString(16).padStart(2, "0");
|
|
144
|
+
return `#${hx(r)}${hx(g)}${hx(b)}`;
|
|
145
|
+
}
|
|
146
|
+
/** The edge strips of a canvas (its 2d context's pixels), for edgeMatte; null when they cannot be read. */
|
|
147
|
+
export function edgeStrips(c) {
|
|
148
|
+
try {
|
|
149
|
+
const g = c && typeof c.getContext === "function" ? c.getContext("2d") : null;
|
|
150
|
+
if (!g || typeof g.getImageData !== "function" || !(c.width > 0) || !(c.height > 0)) return null;
|
|
151
|
+
const w = c.width, h = c.height;
|
|
152
|
+
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];
|
|
153
|
+
} catch { return null; } // a tainted or lost canvas: the skin's matte
|
|
154
|
+
}
|