From e5a4e87300eb47b769668c70032fb6cd58f0276c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Fri, 9 Oct 2026 13:56:49 -0400 Subject: [PATCH] S3.9: the org chart draws everyone as their office pixel person, with one shared CEO rule Co-Authored-By: Claude Opus 5.5 --- src/renderer/src/pages/OfficePage.tsx | 5 ++-- src/renderer/src/pages/office/PixelAvatar.tsx | 24 +++++++++++++++++++ src/renderer/src/pages/office/roster.ts | 11 +++++++++ src/renderer/src/pages/office/simulation.ts | 3 ++- src/renderer/src/pages/team/OrgChartTree.tsx | 8 ++++--- src/renderer/src/styles.css | 1 + tests/e2e/office.spec.ts | 15 ++++++++++++ 7 files changed, 60 insertions(+), 7 deletions(-) create mode 100644 src/renderer/src/pages/office/PixelAvatar.tsx create mode 100644 src/renderer/src/pages/office/roster.ts diff --git a/src/renderer/src/pages/OfficePage.tsx b/src/renderer/src/pages/OfficePage.tsx index 55bf07c9..e6f0f8a6 100644 --- a/src/renderer/src/pages/OfficePage.tsx +++ b/src/renderer/src/pages/OfficePage.tsx @@ -12,6 +12,7 @@ import { dayPart, season } from "../shared/palette-clock"; import { pixelScale } from "../shared/pixel-scale"; import type { Camera, OfficeAgentInput } from "./office/office-types"; import { officeStatus } from "./office/agent-state"; +import { officeRoster } from "./office/roster"; import { t } from "../i18n/i18n"; const FPS_INTERVAL = 1000 / 24; @@ -42,9 +43,7 @@ export function OfficePage({ snapshot }: PageProps) { if (simRef.current === null) simRef.current = new OfficeSimulation(); const inputs = useMemo(() => { - const orgAgents = snapshot.agents - .filter((agent) => agent.orgId === orgId && agent.lifecycle === "active") - .sort((a, b) => a.createdAt.localeCompare(b.createdAt) || a.id.localeCompare(b.id)); + const orgAgents = officeRoster(snapshot.agents.filter((agent) => agent.orgId === orgId)); const allTasks = snapshot.tasks.filter((task) => task.orgId === orgId); const orgTasks = allTasks.filter(isBusinessTask); return orgAgents.map((agent) => ({ diff --git a/src/renderer/src/pages/office/PixelAvatar.tsx b/src/renderer/src/pages/office/PixelAvatar.tsx new file mode 100644 index 00000000..a088494e --- /dev/null +++ b/src/renderer/src/pages/office/PixelAvatar.tsx @@ -0,0 +1,24 @@ +import { useEffect, useRef } from "react"; +import { appearanceFor, drawPerson } from "./sprites"; + +const W = 24; +const H = 24; +/** Each sprite pixel shows as a 2×2 block. */ +const ZOOM = 2; + +/** The person as the office draws them, from the shoulders up, so a face on the org chart is the same face at the desk. */ +export function PixelAvatar({ id, isCeo }: { id: string; isCeo: boolean }) { + const ref = useRef(null); + useEffect(() => { + const canvas = ref.current!; + const scale = ZOOM * Math.max(1, Math.ceil(window.devicePixelRatio)); + canvas.width = W * scale; + canvas.height = H * scale; + const ctx = canvas.getContext("2d"); + if (!ctx) throw new Error("The avatar canvas has no 2D context."); + ctx.imageSmoothingEnabled = false; + ctx.setTransform(scale, 0, 0, scale, 0, 0); + drawPerson(ctx, W / 2, H + 10, appearanceFor(id, isCeo), { facing: "south", walkPhase: 0, working: false, carrying: null, timeMs: 0, animSeed: 0 }, isCeo); + }, [id, isCeo]); + return