Works out how far the farthest point of each room is from an exit and simulates people walking out of or into the building, with times, queues and walked distances.
How the precision is kept
Rooms are covered by a grid of 250 mm cells (coarser for very large rooms) and distances are shortest paths over that grid in 64-bit numbers. Walking speed falls with crowd density by a published linear rule and openings pass people at a fixed rate per meter of effective width. Time advances in steps of 0.25 s. The grid, the time step and the typed assumptions set the accuracy; results are rounded to 0.1 s and 0.01 m.
Calculations under this policy
12 calculations in 2 source files. The code is read from the released app when this page loads. It is the calculation people run, not a copy kept for this page.
Covers a room with square cells and marks the ones whose center is inside it.
cell × 1.5 while (W ÷ cell + 1) × (H ÷ cell + 1) > 60000
The cell size starts at the given value and grows by 1.5 until the room's bounding box holds at most 60000 cells. A cell is free when its center passes an even-odd ray test against the outline and is outside every hole.
people-sim.js
lines 93 to 106
BimCraft 0.8.1
/** The grid of one room: free[i] = 1 where the cell's centre is inside the outline and outside the holes. */
function makeGrid(room, cell0) {
const xs = room.poly.map((p) => p[0]), ys = room.poly.map((p) => p[1]);
const x0 = Math.min(...xs), y0 = Math.min(...ys), x1 = Math.max(...xs), y1 = Math.max(...ys);
let cell = cell0;
while (((x1 - x0) / cell + 1) * ((y1 - y0) / cell + 1) > 60000) cell *= 1.5; // very big rooms: coarser
const nx = Math.max(1, Math.ceil((x1 - x0) / cell)), ny = Math.max(1, Math.ceil((y1 - y0) / cell));
const free = new Uint8Array(nx * ny);
for (let j = 0; j < ny; j++) for (let i = 0; i < nx; i++) {
const x = x0 + (i + 0.5) * cell, y = y0 + (j + 0.5) * cell;
if (inRing(room.poly, x, y) && !(room.holes || []).some((h) => inRing(h, x, y))) free[j * nx + i] = 1;
}
return { x0, y0, nx, ny, cell, free, fields: new Map() };
}
Distance field over a room
64-bit
The walked distance from one cell to every other free cell of a room.
d(n) = min over 16 neighbors of d(c) + √(Δi² + Δj²) × cell
Distances are 64-bit sums of step lengths kept in a Float64Array. Paths are restricted to 16 directions, which the file's comment puts within about 2.5 % of a straight line. Diagonal and knight moves are allowed only when the cells they pass are free.
people-sim.js
lines 131 to 150
BimCraft 0.8.1
/** Distance field (mm) from a cell over the free cells: 16 neighbours with true lengths, within about 2.5 % of the straight-line truth. */
function field(g, src) {
const d = new Float64Array(g.nx * g.ny).fill(Infinity), h = new Heap(), nx = g.nx, ny = g.ny, free = g.free, cell = g.cell;
d[src] = 0; h.push(0, src);
while (h.size) {
const [dd, c] = h.pop();
if (dd > d[c]) continue;
const ci = c % nx, cj = (c - ci) / nx;
for (const [di, dj] of NB) {
const i = ci + di, j = cj + dj;
if (i < 0 || j < 0 || i >= nx || j >= ny || !free[j * nx + i]) continue;
if (Math.abs(di) + Math.abs(dj) === 2 && (!free[cj * nx + i] || !free[j * nx + ci])) continue; // no cutting a corner
if (Math.abs(di) === 2 && (!free[cj * nx + ci + di / 2] || !free[j * nx + ci + di / 2])) continue; // knight move: both middle cells free
if (Math.abs(dj) === 2 && (!free[(cj + dj / 2) * nx + ci] || !free[(cj + dj / 2) * nx + i])) continue;
const n = j * nx + i, nd = dd + Math.hypot(di, dj) * cell;
if (nd < d[n]) { d[n] = nd; h.push(nd, n); }
}
}
return d;
}
Route between two places in a room
64-bit
The distance in meters between two points of a room and the path walked.
straight: √(Δx² + Δy²) ÷ 1000; else Σ segment lengths of the string-pulled path ÷ 1000
When every sampled cell on the straight line is free the distance is one Math.hypot. Otherwise the path follows the distance field downhill and is shortened by jumping to the farthest visible vertex; when only the distance is needed the grid distance is returned without that shortening.
people-sim.js
lines 179 to 214
BimCraft 0.8.1
/** Distance in metres between two standing places of one room, and the walked route. */
function inRoomRoute(scene, room, p, q, needPath) {
const g = scene.rooms[room].g;
if (los(g, p, q)) return { d: Math.hypot(q[0] - p[0], q[1] - p[1]) / 1000, path: needPath ? [p, q] : null };
const cq = nearFree(g, q[0], q[1]), cp = nearFree(g, p[0], p[1]);
if (cq < 0 || cp < 0) return { d: Infinity, path: null };
const f = fieldOf(g, cq), d = f[cp];
if (!Number.isFinite(d)) return { d: Infinity, path: null };
if (!needPath) return { d: d / 1000, path: null };
// walk down the field, then string-pull: from each vertex jump to the farthest one it can see
const raw = [p];
let c = cp;
for (let guard = 0; guard < 100000 && f[c] > 0; guard++) {
const ci = c % g.nx, cj = (c - ci) / g.nx;
let best = c, bv = f[c];
for (const [di, dj] of NB.slice(0, 8)) {
const i = ci + di, j = cj + dj;
if (i < 0 || j < 0 || i >= g.nx || j >= g.ny) continue;
const n = j * g.nx + i;
if (f[n] < bv) { bv = f[n]; best = n; }
}
if (best === c) break;
c = best; raw.push(centre(g, c));
}
raw.push(q);
const path = [raw[0]];
let at = 0;
while (at < raw.length - 1) {
let far = at + 1;
for (let k = raw.length - 1; k > at + 1; k--) if (los(g, raw[at], raw[k])) { far = k; break; }
path.push(raw[far]); at = far;
}
let len = 0;
for (let i = 1; i < path.length; i++) len += Math.hypot(path[i][0] - path[i - 1][0], path[i][1] - path[i - 1][1]);
return { d: len / 1000, path };
}
Distance to the goal from every opening
64-bit
The remaining walking distance to an exit, or to a room, from each side of each door and stair.
d(far side of j) = min(d(i) + walk(j → i) + length of opening j)
A shortest-path search over the openings with 64-bit sums in a Float64Array. A door adds no length; a stair adds its length in meters.
people-sim.js
lines 216 to 247
BimCraft 0.8.1
/**
* Cost (m) to a goal from every standing place, counted from standing at the place to the goal: crossing its opening (a stair's length)
* and everything after. goal { exit: true }: an exit's inside place costs 0 (the door itself takes no length). goal { room }: crossing
* into that room is the end. Shut openings are not used.
*/
function costToGoal(scene, goal, shut, exitSet) {
const E = scene.ends, dist = new Float64Array(E.length).fill(Infinity), h = new Heap();
const seed = (i, d) => { if (d < dist[i]) { dist[i] = d; h.push(d, i); } };
E.forEach((e, i) => {
const o = scene.openings[e.o];
if (shut.has(o.id)) return;
if (goal.exit) { if (exitSet.has(o.id) && o.end[1 - e.side].room < 0) seed(i, 0); }
else if (e.room === goal.room && scene.far[i] >= 0) seed(scene.far[i], o.len); // the place on the other side of j crosses into the room
});
while (h.size) {
const [dd, i] = h.pop();
if (dd > dist[i]) continue;
const e = E[i];
for (const j of scene.inRoom[e.room]) { // arriving at j (by crossing its opening from its far side), then walking to i
if (j === i) continue;
const oj = scene.openings[E[j].o];
if (shut.has(oj.id)) continue;
const far = scene.far[j];
if (far < 0) continue;
const walk = inRoomRoute(scene, e.room, E[j].p, e.p, false).d;
if (!Number.isFinite(walk)) continue;
const nd = dd + walk + oj.len;
if (nd < dist[far]) { dist[far] = nd; h.push(nd, far); }
}
}
return dist;
}
Travel distance of each room
64-bit
The farthest point of each room from an exit, in meters along the walls.
max over cells and corners of min over openings (distance to opening + distance beyond it); rounded to 0.01 m
Every free cell center is tried, then every corner of the outline as its nearest cell plus the straight step to the corner. The result is rounded to 0.01 m and the reported point to a whole millimeter.
people-sim.js
lines 264 to 302
BimCraft 0.8.1
/** The farthest point of every room from an exit, walked (m), with the opening that way leads through. */
export function travelDistances(scene, o = {}) {
const shut = shutSet(o), dist = costToGoal(scene, { exit: true }, shut, exitsOf(scene, o));
return scene.rooms.map((r) => {
const ends = scene.inRoom[r.index].filter((i) => Number.isFinite(dist[i]) && !shut.has(scene.openings[scene.ends[i].o].id));
const res = { room: r.index, id: r.id, name: r.name || '', maxM: null, at: null, via: null, reachable: false };
if (!ends.length) return res;
const fields = ends.map((i) => fieldOf(r.g, scene.ends[i].cellId));
const g = r.g;
let worst = -1;
const consider = (v, at) => { if (v > worst) { worst = v; res.at = at.map(Math.round); } };
// from a place to the nearest way out: straight when it can see the opening, else along the room's distance field
const toExit = (c, pt, extra) => {
let best = Infinity, via = -1;
for (let k = 0; k < ends.length; k++) {
const e = scene.ends[ends[k]], straight = los(g, pt, e.p) ? Math.hypot(pt[0] - e.p[0], pt[1] - e.p[1]) : fields[k][c];
const v = straight / 1000 + dist[ends[k]];
if (v < best) { best = v; via = k; }
}
return [best + extra / 1000, via];
};
let viaAt = null;
for (let c = 0; c < g.free.length; c++) {
if (!g.free[c]) continue;
const pt = centre(g, c), [v, via] = toExit(c, pt, 0);
if (v > worst) { consider(v, pt); viaAt = via; }
}
// the corners themselves (the cell centres stop half a cell short of the wall): a corner is its nearest cell plus the step to it
for (const ring of [r.poly, ...(r.holes || [])]) for (const vx of ring) {
const c = nearFree(g, vx[0], vx[1]);
if (c < 0) continue;
const pt = centre(g, c), [v, via] = toExit(c, pt, Math.hypot(vx[0] - pt[0], vx[1] - pt[1]));
if (v > worst) { consider(v, vx); viaAt = via; }
}
if (worst >= 0) res.via = scene.openings[scene.ends[ends[viaAt]].o].id;
if (worst >= 0) { res.maxM = Math.round(worst * 100) / 100; res.reachable = true; }
return res;
});
}
Speed factor from crowd density
64-bit
How much the crowd in a room slows a walker.
D = people ÷ max(area, 0.5); factor = 1 for D ≤ 0.54, else max(min factor, (1 − 0.266·D) ÷ (1 − 0.266 × 0.54))
The constants 0.266 and 0.54 persons per m² are the published values named in the file's header. The room area is floored at 0.5 m², which guards the division.
people-sim.js
line 304
BimCraft 0.8.1
const dens = (count, area, minF) => { const D = count / Math.max(area, 0.5); return D <= D_FREE ? 1 : Math.max(minF, (1 - A * D) / (1 - A * D_FREE)); };
Flow rate of an opening
64-bit
How many people per second a door or stair lets through.
rate = flow per meter × max(0.2, width ÷ 1000 − 2 × boundary)
The width is converted from millimeters with ÷ 1000 and reduced by a boundary layer on each side (0.15 m by default). The capacity shown is this rate rounded to 0.01; the simulation uses the unrounded rate.
How far a walker moves in one time step and the distance added to their total.
Δs = v₀ × factor × Δt; walked += min(Δs, remaining length of the leg)
Positions along a leg are accumulated by repeated addition of Δs. The walked total adds only the part of the step that lies on the leg, so overshoot at the end of a leg is not counted.
Pass times are 64-bit and are not tied to the time step. The counter-flow penalty is 1 ÷ (rate × share) − 1 ÷ rate when the previous pass went the other way within the window. Waits are summed in order.
people-sim.js
lines 442 to 449
BimCraft 0.8.1
const a = q.shift(), base = Math.max(nextFree[oi], a.qT, t);
// counter-flow (0.8 EG.48): a pass in the other direction from the last one, close behind it, costs the opening part of its rate
const dir = a.cross.from >= 0 ? E[a.cross.from].side : 1 - E[a.cross.to].side, sw = S.lastDir !== null && S.lastDir !== dir && base - S.lastPassT <= o.counterWindow;
const passT = base + (sw ? 1 / (rate[oi] * o.counterFlow) - 1 / rate[oi] : 0);
if (sw) S.switches++;
S.lastDir = dir; S.lastPassT = passT;
nextFree[oi] = passT + 1 / rate[oi];
const wait = passT - a.qT;
Reported times and distances
64-bit
Total time, median and 90th percentile times, walked distances and waits.
percentile p = times[ceil(n·p) − 1]; times rounded to 0.1 s, distances to 0.01 m
Percentiles pick one of the sorted exit times, with no interpolation. Every reported time is rounded with Math.round(v × 10) ÷ 10 and every distance with Math.round(v × 100) ÷ 100 after the 64-bit sums and maxima are taken.
The walked length of a stair from its plan walk line and its height.
length = √(plan length² + height²)
The plan length is a sum of Math.hypot segment lengths in millimeters, and the slope length is one Math.hypot of that sum and the stair's height. Landings are counted, not measured.
people-model.js
lines 82 to 84
BimCraft 0.8.1
let plan = 0;
for (let i = 1; i < lay.walk.length; i++) plan += Math.hypot(lay.walk[i][0] - lay.walk[i - 1][0], lay.walk[i][1] - lay.walk[i - 1][1]);
stairs.push({ id: st.guid, a: { room: ra.id, x: a[0], y: a[1] }, b: { room: rb.id, x: b[0], y: b[1] }, length: Math.hypot(plan, height), w: st.width, landings: (lay.pieces || []).filter((pc) => pc.kind === 'landing').length });
People per room
64-bit
How many people each room holds before a run.
count = floor(area in m² ÷ m² per person)
The room area in mm² is divided by 1e6 elsewhere in the file and the count is truncated with Math.floor. The area shown beside it is rounded to 0.1 m²; the count uses the unrounded area. A typed count is truncated to a whole number.
people-model.js
lines 107 to 115
BimCraft 0.8.1
/**
* 0.8 EG.48 (bim-sim_PART_3): the people each room holds before a run, so the person can set a count per room.
* roomCounts(ed, { m2PerPerson, loads }) → [{ id, name, level, levelName, area_m2, occupied, auto, count }] auto = the count the factor gives (0 for a corridor, stair, lobby …);
* count = the person's number when `loads` has the room, else auto. The same `loads` map goes to simulate().
*/
export function roomCounts(ed, { m2PerPerson = 10, loads = {} } = {}) {
const { rooms } = namedRooms(ed);
return rooms.map((r) => { const auto = r.occupied ? Math.floor(r.area_m2 / m2PerPerson) : 0, own = loads[r.id]; return { id: r.id, name: r.name, level: r.level, levelName: r.levelName, area_m2: Math.round(r.area_m2 * 10) / 10, occupied: r.occupied, auto, count: own !== undefined && own !== '' && own !== null ? Math.max(0, Math.floor(Number(own)) || 0) : auto }; });
}
In detail
Method
Dijkstra distance fields over 16 neighbors per cell, string-pulled paths, a shortest-path search over openings, a fixed-step simulation with first-come queues. Speed rule S = k(1 − a·D) with k = 1.4 m/s and a = 0.266, cited in the file as Nelson and MacLennan.
Number types
64-bit numbers and Float64Array for distances; Uint8Array for the free-cell grid; Float32Array only for the playback frames.
Tolerances
Grid cell 250 mm, multiplied by 1.5 until a room has at most 60000 cells. Time step 0.25 s. Comparisons of times use 1e-9 s. Door to room matching: half the wall thickness + 60 mm. Effective width at least 0.2 m.
Limits
The file's own comment puts the 16-neighbor distance within about 2.5 % of the straight-line value. Cell centers stop half a cell short of the walls, so room corners are added separately. Arrival and exit times fall on multiples of the time step for walkers and on the queue's pass times for openings.
Approach: 64-bit, stated limit
The arithmetic is 64-bit, but the method or the data sets the accuracy: a solver tolerance, a mesh, a step size, a datum shift, a table. The limit is stated where the result is shown.