@blkc-code/render-canvas 0.0.0-stage → 0.3.0
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/draw.d.ts +26 -0
- package/dist/draw.d.ts.map +1 -0
- package/dist/draw.js +760 -0
- package/dist/draw.js.map +1 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +60 -0
- package/dist/index.js.map +1 -0
- package/dist/office.d.ts +39 -0
- package/dist/office.d.ts.map +1 -0
- package/dist/office.js +231 -0
- package/dist/office.js.map +1 -0
- package/dist/runtime.d.ts +2 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +11 -0
- package/dist/runtime.js.map +1 -0
- package/dist/runtime.txt +11 -0
- package/dist/types.d.ts +55 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/world.d.ts +129 -0
- package/dist/world.d.ts.map +1 -0
- package/dist/world.js +428 -0
- package/dist/world.js.map +1 -0
- package/package.json +43 -4
- package/src/draw.ts +725 -0
- package/src/index.ts +73 -0
- package/src/office.ts +255 -0
- package/src/runtime.ts +11 -0
- package/src/types.ts +49 -0
- package/src/world.ts +466 -0
- package/README.md +0 -3
package/src/index.ts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BLKC Render-Canvas.
|
|
3
|
+
*
|
|
4
|
+
* A canvas Block is ordinary markup from @blkc-code/render-web plus its data, and this
|
|
5
|
+
* package brings it to life. Call mountCanvasBlocks() once the page is in the document:
|
|
6
|
+
* a <canvas data-blkc-canvas="office"> with a JSON <script> beside it starts running.
|
|
7
|
+
*
|
|
8
|
+
* @package @blkc-code/render-canvas
|
|
9
|
+
* @license Apache-2.0
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { createOffice, type OfficeHandle } from "./office.js";
|
|
13
|
+
import type { OfficeScene } from "./types.js";
|
|
14
|
+
|
|
15
|
+
export { createOffice } from "./office.js";
|
|
16
|
+
export type { OfficeHandle, OfficeOptions } from "./office.js";
|
|
17
|
+
export * from "./types.js";
|
|
18
|
+
export { FURNITURE, agentAt, applyLayout, applyUpdate, createWorld, findPath, step } from "./world.js";
|
|
19
|
+
export type { Agent, World } from "./world.js";
|
|
20
|
+
|
|
21
|
+
export interface MountedBlock {
|
|
22
|
+
element: HTMLCanvasElement;
|
|
23
|
+
destroy(): void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
type CanvasBlockFactory = (canvas: HTMLCanvasElement, data: unknown, root: Element) => { destroy(): void };
|
|
27
|
+
|
|
28
|
+
const factories = new Map<string, CanvasBlockFactory>();
|
|
29
|
+
|
|
30
|
+
/** Teach mountCanvasBlocks about another kind of canvas Block. */
|
|
31
|
+
export function registerCanvasBlock(id: string, factory: CanvasBlockFactory): void {
|
|
32
|
+
factories.set(id, factory);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function mountOffice(canvas: HTMLCanvasElement, data: unknown, root: Element): OfficeHandle {
|
|
36
|
+
const buttons = [...root.querySelectorAll<HTMLButtonElement>("button[data-blkc-agent]")];
|
|
37
|
+
const office = createOffice(canvas, data as OfficeScene, {
|
|
38
|
+
onSelect(id) {
|
|
39
|
+
for (const button of buttons) button.setAttribute("aria-pressed", String(button.dataset["blkcAgent"] === id));
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
for (const button of buttons) {
|
|
43
|
+
button.disabled = false;
|
|
44
|
+
button.addEventListener("click", () => office.select(button.dataset["blkcAgent"]));
|
|
45
|
+
}
|
|
46
|
+
return office;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
registerCanvasBlock("office", mountOffice);
|
|
50
|
+
|
|
51
|
+
const mounted = new WeakSet<Element>();
|
|
52
|
+
|
|
53
|
+
/** Start every canvas Block under `root` that isn't running yet. */
|
|
54
|
+
export function mountCanvasBlocks(root: ParentNode = document): MountedBlock[] {
|
|
55
|
+
const out: MountedBlock[] = [];
|
|
56
|
+
for (const canvas of root.querySelectorAll<HTMLCanvasElement>("canvas[data-blkc-canvas]")) {
|
|
57
|
+
if (mounted.has(canvas)) continue;
|
|
58
|
+
const factory = factories.get(canvas.dataset["blkcCanvas"] ?? "");
|
|
59
|
+
if (!factory) continue;
|
|
60
|
+
const host = canvas.closest("[data-blkc-canvas-root]") ?? canvas.parentElement ?? canvas;
|
|
61
|
+
const source = host.querySelector("script[data-blkc-canvas-data]");
|
|
62
|
+
let data: unknown = {};
|
|
63
|
+
try {
|
|
64
|
+
data = JSON.parse(source?.textContent || "{}");
|
|
65
|
+
} catch {
|
|
66
|
+
data = {};
|
|
67
|
+
}
|
|
68
|
+
mounted.add(canvas);
|
|
69
|
+
const block = factory(canvas, data, host);
|
|
70
|
+
out.push({ element: canvas, destroy: () => block.destroy() });
|
|
71
|
+
}
|
|
72
|
+
return out;
|
|
73
|
+
}
|
package/src/office.ts
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/** Puts a world on a canvas: sizing, the camera, the frame loop, and input. */
|
|
2
|
+
|
|
3
|
+
import { daylightAt, drawBuilding, drawWorld, TILE, type Daylight, type Palette } from "./draw.js";
|
|
4
|
+
import type { OfficeLayout, OfficeScene } from "./types.js";
|
|
5
|
+
import { agentAt, applyLayout, applyUpdate, createWorld, step, type World } from "./world.js";
|
|
6
|
+
|
|
7
|
+
export interface OfficeHandle {
|
|
8
|
+
world: World;
|
|
9
|
+
/** Change agents or tasks without restarting anyone. */
|
|
10
|
+
update(scene: Pick<OfficeScene, "agents" | "tasks">): void;
|
|
11
|
+
/** Change the building while everyone is in it. */
|
|
12
|
+
setLayout(layout: OfficeLayout): void;
|
|
13
|
+
/** Pick an agent by id, or pass nothing to clear. */
|
|
14
|
+
select(id?: string): void;
|
|
15
|
+
/** The tile under a pointer event, allowing for the camera. */
|
|
16
|
+
tileAt(event: { clientX: number; clientY: number }): { x: number; y: number };
|
|
17
|
+
/** Draw something over the office each frame, in pixels at 16 a tile. For build mode. */
|
|
18
|
+
setOverlay(draw: ((c: CanvasRenderingContext2D) => void) | undefined): void;
|
|
19
|
+
/** Stop clicks picking agents and dragging the camera, while something else handles them. */
|
|
20
|
+
setInteractive(on: boolean): void;
|
|
21
|
+
/** Zoom by a factor around a point on the canvas, or reset with no arguments. */
|
|
22
|
+
zoom(factor?: number, event?: { clientX: number; clientY: number }): void;
|
|
23
|
+
destroy(): void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface OfficeOptions {
|
|
27
|
+
/** Called when the selection changes, by click or by select(). */
|
|
28
|
+
onSelect?: (id: string | undefined) => void;
|
|
29
|
+
/** Wheel to zoom and drag to pan. For a page that is mainly the office: it takes the wheel. */
|
|
30
|
+
camera?: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const SAFE_COLOUR = /^(#[0-9a-f]{3,8}|rgba?\([\d\s.,%/]+\)|hsla?\([\d\s.,%/a-z]+\))$/i;
|
|
34
|
+
const MAX_ZOOM = 4;
|
|
35
|
+
|
|
36
|
+
function readPalette(canvas: HTMLCanvasElement): Palette {
|
|
37
|
+
const styles = getComputedStyle(canvas);
|
|
38
|
+
const token = (name: string, fallback: string): string => {
|
|
39
|
+
const value = styles.getPropertyValue(name).trim();
|
|
40
|
+
return SAFE_COLOUR.test(value) ? value : fallback;
|
|
41
|
+
};
|
|
42
|
+
return {
|
|
43
|
+
primary: token("--blkc-color-primary", "#3b82f6"),
|
|
44
|
+
accent: token("--blkc-color-accent", "#f59e0b"),
|
|
45
|
+
background: token("--blkc-color-background", "#1a1a2e"),
|
|
46
|
+
text: token("--blkc-color-text", "#ffffff"),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Draw an office and keep it running until destroy(). */
|
|
51
|
+
export function createOffice(canvas: HTMLCanvasElement, scene: OfficeScene = {}, options: OfficeOptions = {}): OfficeHandle {
|
|
52
|
+
const world = createWorld(scene);
|
|
53
|
+
const context = canvas.getContext("2d");
|
|
54
|
+
if (!context) throw new Error("This browser can't draw to a canvas.");
|
|
55
|
+
const c = context;
|
|
56
|
+
const palette = readPalette(canvas);
|
|
57
|
+
const still = typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
58
|
+
|
|
59
|
+
let selected: string | undefined;
|
|
60
|
+
let scale = 0;
|
|
61
|
+
let time = 0;
|
|
62
|
+
let last = 0;
|
|
63
|
+
let frame = 0;
|
|
64
|
+
let visible = true;
|
|
65
|
+
let destroyed = false;
|
|
66
|
+
let interactive = true;
|
|
67
|
+
let overlay: ((c: CanvasRenderingContext2D) => void) | undefined;
|
|
68
|
+
const view = { x: 0, y: 0, zoom: 1 };
|
|
69
|
+
let hour = new Date().getHours();
|
|
70
|
+
let daylight: Daylight = daylightAt(hour);
|
|
71
|
+
let building: HTMLCanvasElement | undefined;
|
|
72
|
+
let buildingDirty = true;
|
|
73
|
+
|
|
74
|
+
const width = (): number => world.cols * TILE;
|
|
75
|
+
const height = (): number => world.rows * TILE;
|
|
76
|
+
|
|
77
|
+
const clampView = (): void => {
|
|
78
|
+
view.zoom = Math.min(MAX_ZOOM, Math.max(1, view.zoom));
|
|
79
|
+
view.x = Math.min(width() - width() / view.zoom, Math.max(0, view.x));
|
|
80
|
+
view.y = Math.min(height() - height() / view.zoom, Math.max(0, view.y));
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const refreshBuilding = (): void => {
|
|
84
|
+
if (typeof document === "undefined") return;
|
|
85
|
+
building ??= document.createElement("canvas");
|
|
86
|
+
building.width = width() * scale;
|
|
87
|
+
building.height = height() * scale;
|
|
88
|
+
const layer = building.getContext("2d");
|
|
89
|
+
if (!layer) return;
|
|
90
|
+
layer.setTransform(scale, 0, 0, scale, 0, 0);
|
|
91
|
+
layer.imageSmoothingEnabled = false;
|
|
92
|
+
drawBuilding(layer, world, palette, daylight);
|
|
93
|
+
buildingDirty = false;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const paint = (): void => {
|
|
97
|
+
if (buildingDirty) refreshBuilding();
|
|
98
|
+
const zoomed = scale * view.zoom;
|
|
99
|
+
c.setTransform(zoomed, 0, 0, zoomed, -view.x * zoomed, -view.y * zoomed);
|
|
100
|
+
c.imageSmoothingEnabled = false;
|
|
101
|
+
drawWorld(c, world, time, palette, daylight, building, selected);
|
|
102
|
+
overlay?.(c);
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
// The backing store is a whole multiple of the art, so every pixel stays square and text stays sharp.
|
|
106
|
+
const resize = (): void => {
|
|
107
|
+
const shown = canvas.getBoundingClientRect().width || width();
|
|
108
|
+
const ratio = typeof devicePixelRatio === "number" ? devicePixelRatio : 1;
|
|
109
|
+
const next = Math.max(1, Math.ceil((shown * ratio) / width()));
|
|
110
|
+
if (next === scale && canvas.width === width() * scale && canvas.height === height() * scale) return;
|
|
111
|
+
scale = next;
|
|
112
|
+
canvas.width = width() * scale;
|
|
113
|
+
canvas.height = height() * scale;
|
|
114
|
+
buildingDirty = true;
|
|
115
|
+
paint();
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const loop = (now: number): void => {
|
|
119
|
+
if (destroyed) return;
|
|
120
|
+
frame = requestAnimationFrame(loop);
|
|
121
|
+
if (!visible) {
|
|
122
|
+
last = now;
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const dt = Math.min((now - last) / 1000 || 0, 0.1); // a stalled tab mustn't teleport people
|
|
126
|
+
last = now;
|
|
127
|
+
time += dt;
|
|
128
|
+
step(world, dt);
|
|
129
|
+
const nowHour = new Date().getHours();
|
|
130
|
+
if (nowHour !== hour) {
|
|
131
|
+
hour = nowHour;
|
|
132
|
+
daylight = daylightAt(hour);
|
|
133
|
+
buildingDirty = true;
|
|
134
|
+
}
|
|
135
|
+
paint();
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const select = (id?: string): void => {
|
|
139
|
+
selected = id === selected ? undefined : id;
|
|
140
|
+
options.onSelect?.(selected);
|
|
141
|
+
if (still) paint();
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const tileAt = (event: { clientX: number; clientY: number }): { x: number; y: number } => {
|
|
145
|
+
const box = canvas.getBoundingClientRect();
|
|
146
|
+
const px = ((event.clientX - box.left) / box.width) * (width() / view.zoom) + view.x;
|
|
147
|
+
const py = ((event.clientY - box.top) / box.height) * (height() / view.zoom) + view.y;
|
|
148
|
+
return { x: px / TILE, y: py / TILE };
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
let drag: { x: number; y: number; moved: boolean } | undefined;
|
|
152
|
+
const onDown = (event: PointerEvent): void => {
|
|
153
|
+
if (!interactive) return;
|
|
154
|
+
drag = { x: event.clientX, y: event.clientY, moved: false };
|
|
155
|
+
};
|
|
156
|
+
const onMove = (event: PointerEvent): void => {
|
|
157
|
+
if (!interactive) return;
|
|
158
|
+
if (drag && options.camera && view.zoom > 1) {
|
|
159
|
+
const box = canvas.getBoundingClientRect();
|
|
160
|
+
const dx = event.clientX - drag.x;
|
|
161
|
+
const dy = event.clientY - drag.y;
|
|
162
|
+
if (drag.moved || Math.abs(dx) + Math.abs(dy) > 3) {
|
|
163
|
+
drag.moved = true;
|
|
164
|
+
view.x -= (dx / box.width) * (width() / view.zoom);
|
|
165
|
+
view.y -= (dy / box.height) * (height() / view.zoom);
|
|
166
|
+
drag.x = event.clientX;
|
|
167
|
+
drag.y = event.clientY;
|
|
168
|
+
clampView();
|
|
169
|
+
canvas.style.cursor = "grabbing";
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
const { x, y } = tileAt(event);
|
|
174
|
+
canvas.style.cursor = agentAt(world, x, y) ? "pointer" : options.camera && view.zoom > 1 ? "grab" : "default";
|
|
175
|
+
};
|
|
176
|
+
const onUp = (event: PointerEvent): void => {
|
|
177
|
+
if (!interactive) return;
|
|
178
|
+
const wasDrag = drag?.moved;
|
|
179
|
+
drag = undefined;
|
|
180
|
+
if (wasDrag) return;
|
|
181
|
+
const { x, y } = tileAt(event);
|
|
182
|
+
const agent = agentAt(world, x, y);
|
|
183
|
+
if (agent || selected) select(agent?.id ?? selected);
|
|
184
|
+
};
|
|
185
|
+
const zoom = (factor?: number, event?: { clientX: number; clientY: number }): void => {
|
|
186
|
+
if (factor === undefined) {
|
|
187
|
+
view.zoom = 1;
|
|
188
|
+
view.x = 0;
|
|
189
|
+
view.y = 0;
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
const before = event ? tileAt(event) : { x: view.x / TILE + width() / view.zoom / TILE / 2, y: view.y / TILE + height() / view.zoom / TILE / 2 };
|
|
193
|
+
view.zoom = Math.min(MAX_ZOOM, Math.max(1, view.zoom * factor));
|
|
194
|
+
if (event) {
|
|
195
|
+
const after = tileAt(event);
|
|
196
|
+
view.x += (before.x - after.x) * TILE;
|
|
197
|
+
view.y += (before.y - after.y) * TILE;
|
|
198
|
+
} else {
|
|
199
|
+
view.x = before.x * TILE - width() / view.zoom / 2;
|
|
200
|
+
view.y = before.y * TILE - height() / view.zoom / 2;
|
|
201
|
+
}
|
|
202
|
+
clampView();
|
|
203
|
+
};
|
|
204
|
+
const onWheel = (event: WheelEvent): void => {
|
|
205
|
+
event.preventDefault();
|
|
206
|
+
zoom(event.deltaY < 0 ? 1.2 : 1 / 1.2, event);
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
canvas.addEventListener("pointerdown", onDown);
|
|
210
|
+
canvas.addEventListener("pointermove", onMove);
|
|
211
|
+
canvas.addEventListener("pointerup", onUp);
|
|
212
|
+
if (options.camera) canvas.addEventListener("wheel", onWheel, { passive: false });
|
|
213
|
+
|
|
214
|
+
const resizer = typeof ResizeObserver === "function" ? new ResizeObserver(resize) : undefined;
|
|
215
|
+
resizer?.observe(canvas);
|
|
216
|
+
const viewer = typeof IntersectionObserver === "function"
|
|
217
|
+
? new IntersectionObserver((entries) => { visible = entries.some((entry) => entry.isIntersecting); })
|
|
218
|
+
: undefined;
|
|
219
|
+
viewer?.observe(canvas);
|
|
220
|
+
|
|
221
|
+
resize();
|
|
222
|
+
if (scale === 0) { scale = 1; paint(); }
|
|
223
|
+
if (!still) frame = requestAnimationFrame(loop);
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
world,
|
|
227
|
+
update(next) {
|
|
228
|
+
applyUpdate(world, next);
|
|
229
|
+
if (still) paint();
|
|
230
|
+
},
|
|
231
|
+
setLayout(layout) {
|
|
232
|
+
applyLayout(world, layout);
|
|
233
|
+
buildingDirty = true;
|
|
234
|
+
view.zoom = Math.min(view.zoom, MAX_ZOOM);
|
|
235
|
+
clampView();
|
|
236
|
+
scale = 0;
|
|
237
|
+
resize();
|
|
238
|
+
},
|
|
239
|
+
select,
|
|
240
|
+
tileAt,
|
|
241
|
+
setOverlay(draw) { overlay = draw; },
|
|
242
|
+
setInteractive(on) { interactive = on; drag = undefined; if (!on) canvas.style.cursor = "crosshair"; },
|
|
243
|
+
zoom,
|
|
244
|
+
destroy() {
|
|
245
|
+
destroyed = true;
|
|
246
|
+
cancelAnimationFrame(frame);
|
|
247
|
+
resizer?.disconnect();
|
|
248
|
+
viewer?.disconnect();
|
|
249
|
+
canvas.removeEventListener("pointerdown", onDown);
|
|
250
|
+
canvas.removeEventListener("pointermove", onMove);
|
|
251
|
+
canvas.removeEventListener("pointerup", onUp);
|
|
252
|
+
canvas.removeEventListener("wheel", onWheel);
|
|
253
|
+
},
|
|
254
|
+
};
|
|
255
|
+
}
|
package/src/runtime.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** The script tag version: starts every canvas Block on the page by itself. */
|
|
2
|
+
import { FURNITURE, createOffice, findPath, mountCanvasBlocks } from "./index.js";
|
|
3
|
+
|
|
4
|
+
// For pages that drive an office themselves, such as the blkc dev app.
|
|
5
|
+
(globalThis as unknown as { BLKCCanvas: unknown }).BLKCCanvas = { FURNITURE, createOffice, findPath, mountCanvasBlocks };
|
|
6
|
+
|
|
7
|
+
if (document.readyState === "loading") {
|
|
8
|
+
document.addEventListener("DOMContentLoaded", () => mountCanvasBlocks());
|
|
9
|
+
} else {
|
|
10
|
+
mountCanvasBlocks();
|
|
11
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/** What a Kit writes to describe an office. All of it is plain JSON. */
|
|
2
|
+
|
|
3
|
+
export type AgentState = "working" | "thinking" | "blocked" | "reviewing" | "idle" | "done" | "away";
|
|
4
|
+
export type TaskState = "todo" | "doing" | "done";
|
|
5
|
+
export type FloorStyle = "wood" | "carpet" | "tile" | "stone" | "concrete" | "grass";
|
|
6
|
+
export type FurnitureType =
|
|
7
|
+
| "desk" | "coffee" | "sofa" | "plant" | "fern" | "board" | "whiteboard" | "shelf" | "server" | "table" | "rug"
|
|
8
|
+
| "water" | "arcade" | "fridge" | "printer" | "lamp" | "beanbag" | "tv" | "vending" | "clock" | "poster" | "trophy";
|
|
9
|
+
|
|
10
|
+
export interface OfficeRoom {
|
|
11
|
+
id?: string;
|
|
12
|
+
name?: string;
|
|
13
|
+
x: number;
|
|
14
|
+
y: number;
|
|
15
|
+
w: number;
|
|
16
|
+
h: number;
|
|
17
|
+
floor?: FloorStyle;
|
|
18
|
+
/** Project folders this room stands for, so agents go there when they work in them. */
|
|
19
|
+
paths?: string[];
|
|
20
|
+
}
|
|
21
|
+
export interface OfficeDoor { x: number; y: number }
|
|
22
|
+
export interface OfficeFurniture { type: FurnitureType; x: number; y: number }
|
|
23
|
+
export interface OfficeAgent {
|
|
24
|
+
id: string;
|
|
25
|
+
name?: string;
|
|
26
|
+
role?: string;
|
|
27
|
+
state?: AgentState;
|
|
28
|
+
task?: string;
|
|
29
|
+
/** The id of the room this agent is working in. */
|
|
30
|
+
area?: string;
|
|
31
|
+
/** Character colours. Anything left out is picked from the id. */
|
|
32
|
+
hair?: string;
|
|
33
|
+
skin?: string;
|
|
34
|
+
shirt?: string;
|
|
35
|
+
}
|
|
36
|
+
export interface OfficeTask { title: string; state?: TaskState; agent?: string }
|
|
37
|
+
|
|
38
|
+
export interface OfficeLayout {
|
|
39
|
+
cols?: number;
|
|
40
|
+
rows?: number;
|
|
41
|
+
rooms?: OfficeRoom[];
|
|
42
|
+
doors?: OfficeDoor[];
|
|
43
|
+
furniture?: OfficeFurniture[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface OfficeScene extends OfficeLayout {
|
|
47
|
+
agents?: OfficeAgent[];
|
|
48
|
+
tasks?: OfficeTask[];
|
|
49
|
+
}
|