Deploy web Studio / deploy (push) Successful in 1m4s
datacentre.rhai (palette groups Data centre, Cooling, Fire safety, Power): - dc_building: multi-storey shell (storeys of storey_h), walls in pieces, slabs, roof with parapet, louvres, front door and loading dock; levels cut it open storey by storey - raised_floor; rack_row and dc_pod (cabinets by U, width, depth, equipment, fill, doors, LEDs; in-row coolers; cold / hot aisle containment with vent tiles, roof, end doors, ceiling duct, airflow); dc_cage for colocation - crah, chiller (air-cooled, dry, adiabatic), cooling_tower, pipe_run - smoke_detectors (grid + coverage), fire_pipe (VESDA, sprinkler, gas), gas_bank (Novec 1230, FM-200, Inergen sized for its halls), fire_device (panels, call points, abort switches, beacons, extinguishers) - power_cab (UPS, battery, PDU, switchgear, RPP), busway, cable_tray, transformer - tooltips sum IT load, kW/m2, cooling redundancy per storey, UPS load, battery runtime, plant cooling and agent quantity (warning when short) Engine and stdlib: - part_hidden(o) in house.rhai: parts on closed storeys inside a dc_building are not drawn (they only made sort cycles under the slab above); house parts, furniture and people use it. Existing examples render byte-identical. - overhead services sort in their own "ceiling" layer with tight boxes - sort records which sprites each broken cycle involved; isokit check names them (Rendered.cycle_at) examples/datacentre (three storeys and a roof plant), Data centre demo in the web Studio, tests, SCRIPTING/README/ROADMAP, MCP instructions.
1369 lines
64 KiB
Plaintext
1369 lines
64 KiB
Plaintext
// isokit/datacentre: multi-storey data centres.
|
||
//
|
||
// `dc_building` is the shell: `storeys` of `storey_h` (the project parameter
|
||
// the house parts use too), cut open by the levels gf, 1f, ... (storeys below
|
||
// closed, the walls in front cut low, everything above left out). Inside it
|
||
// every part lives on a storey (its z) and stands on the `raised_floor` under
|
||
// it: rack rows, pods with cold or hot aisle containment, CRAH units, power
|
||
// cabinets. Overhead: busways, cable trays, fire pipes (VESDA, sprinklers,
|
||
// gas nozzles) and detector grids. Plant: chillers, cooling towers, pipe
|
||
// runs, transformers. Parts on closed storeys inside a dc_building are not
|
||
// drawn (the shell hides them); the project parameter `ceiling` = false
|
||
// leaves the overhead services out.
|
||
//
|
||
// Parts read the document: racks total their IT load per storey, CRAH units
|
||
// set the storey's cooling against it (N+1 and so on), gas cylinder banks
|
||
// size the agent for the halls on their storey, batteries give their runtime
|
||
// at the building's IT load.
|
||
|
||
fn dc_rack_types() { ["rack_row", "dc_pod"] }
|
||
|
||
/// Leave part o out? Like house parts (part_hidden in isokit/house): above
|
||
/// the storey a level cuts open, or on a closed storey inside a dc_building.
|
||
fn dc_skip(o) { part_hidden(o) }
|
||
|
||
/// Overhead services (trays, busways, fire pipes, detectors) draw in their
|
||
/// own layer after everything standing on the floor, lower ones first: runs
|
||
/// crossing at different heights over racks would otherwise make depth-sort
|
||
/// cycles (a tray beside a pod, a busway over the pod and under the tray).
|
||
fn dc_ceiling_layer() { layer("ceiling", #{ sort: true, zfirst: true }); }
|
||
|
||
/// Overhead services left out (project parameter `ceiling` = false)?
|
||
fn dc_no_ceiling() {
|
||
let v = cfg("ceiling");
|
||
type_of(v) == "bool" && !v
|
||
}
|
||
|
||
/// Height of the floor surface at world (wx, wy) above o.z: the top of a
|
||
/// raised floor there on o's storey, else 0.
|
||
fn dc_lift_at(o, wx, wy) {
|
||
let s = storey_of(o.z);
|
||
let best = 0.0;
|
||
for f in objects_at(["raised_floor"], wx, wy) {
|
||
if storey_of(f.z) == s { best = fmax(best, f.z + num(f.p.h) - o.z); }
|
||
}
|
||
best
|
||
}
|
||
fn dc_lift(o) { dc_lift_at(o, o.x, o.y) }
|
||
|
||
/// Level name of storey j: "GF", "1F", ...
|
||
fn dc_storey_name(j) { if j == 0 { "GF" } else { `${j}F` } }
|
||
|
||
/// x for a tooltip: whole numbers as they are, others to one decimal.
|
||
fn dc_num(x) {
|
||
let r = round1(x);
|
||
if (r - r.round()).abs() < 1e-9 { `${r.round().to_int()}` } else { `${r}` }
|
||
}
|
||
|
||
// ---------------------------------------------------------------- loads
|
||
|
||
/// Height of a cabinet of u rack units (42U: 2.0 m).
|
||
fn dc_rack_h(u) { num(u) * 0.04445 + 0.13 }
|
||
|
||
/// The units of a line of racks along local x, centred on 0:
|
||
/// [[x0, x1, cooler], ...]. With p.inrow = k > 0 an in-row cooler (0.3 m
|
||
/// wide) follows every k racks (at the end when there is only one group).
|
||
fn dc_units(p) {
|
||
let k = p.inrow;
|
||
let n = p.n;
|
||
let seq = [];
|
||
for i in 0..n {
|
||
seq.push(false);
|
||
if k > 0 && (i + 1) % k == 0 && i + 1 < n { seq.push(true); }
|
||
}
|
||
if k > 0 && !seq.contains(true) { seq.push(true); }
|
||
let rw = num(p.rw);
|
||
let L = 0.0;
|
||
for q in seq { L += if q { 0.3 } else { rw }; }
|
||
let out = [];
|
||
let x = -L / 2.0;
|
||
for q in seq {
|
||
let w = if q { 0.3 } else { rw };
|
||
out.push([x, x + w, q]);
|
||
x += w;
|
||
}
|
||
out
|
||
}
|
||
|
||
/// In-row coolers in one line of racks.
|
||
fn dc_coolers(p) {
|
||
let c = 0;
|
||
for q in dc_units(p) { if q[2] { c += 1; } }
|
||
c
|
||
}
|
||
|
||
fn dc_racks_of(r) { if r.type == "dc_pod" { 2 * r.p.n } else { r.p.n } }
|
||
fn dc_kw_of(r) { if r.p.kind == "empty" { 0.0 } else { dc_racks_of(r).to_float() * num(r.p.kw) } }
|
||
|
||
/// IT load of the racks on storey s (every storey when s < 0):
|
||
/// #{ kw, racks, rows }.
|
||
fn dc_it(s) {
|
||
let kw = 0.0;
|
||
let n = 0;
|
||
let rows = 0;
|
||
for r in objects_of(dc_rack_types()) {
|
||
if s >= 0 && storey_of(r.z) != s { continue; }
|
||
n += dc_racks_of(r);
|
||
kw += dc_kw_of(r);
|
||
rows += 1;
|
||
}
|
||
#{ kw: kw, racks: n, rows: rows }
|
||
}
|
||
|
||
/// Room cooling on storey s: CRAH units on duty and on standby and the
|
||
/// in-row coolers of its racks: #{ duty, standby, units }.
|
||
fn dc_cool(s) {
|
||
let duty = 0.0;
|
||
let sb = 0.0;
|
||
let n = 0;
|
||
for c in objects_of(["crah"]) {
|
||
if storey_of(c.z) != s { continue; }
|
||
n += 1;
|
||
if c.p.standby { sb += num(c.p.kw); } else { duty += num(c.p.kw); }
|
||
}
|
||
for r in objects_of(dc_rack_types()) {
|
||
if storey_of(r.z) != s { continue; }
|
||
let k = dc_coolers(r.p);
|
||
if r.type == "dc_pod" { k *= 2; }
|
||
n += k;
|
||
duty += k.to_float() * num(r.p.inrow_kw);
|
||
}
|
||
#{ duty: duty, standby: sb, units: n }
|
||
}
|
||
|
||
/// Redundancy of cooling c against kw of load: "N+1", "N", "short by ...".
|
||
fn dc_redundancy(c, kw) {
|
||
let total = c.duty + c.standby;
|
||
if c.units == 0 { return "no cooling units"; }
|
||
if total + 1e-6 < kw { return `short by ${fmt(kw - total)} kW`; }
|
||
let per = total / c.units.to_float();
|
||
let spare = ((total - kw) / per + 1e-6).floor().to_int();
|
||
if spare <= 0 { "N (no spare unit)" } else { `N+${spare}, ${fmt(total - kw)} kW spare` }
|
||
}
|
||
|
||
/// Plant cooling (chillers and cooling towers) on the site: #{ kw, units }.
|
||
fn dc_plant() {
|
||
let kw = 0.0;
|
||
let n = 0;
|
||
for c in objects_of(["chiller", "cooling_tower"]) {
|
||
let k = if c.type == "cooling_tower" { c.p.cells } else { 1 };
|
||
n += k;
|
||
kw += k.to_float() * num(c.p.kw);
|
||
}
|
||
#{ kw: kw, units: n }
|
||
}
|
||
|
||
// ---------------------------------------------------------------- building
|
||
|
||
fn meta_dc_building() {
|
||
#{ label: "DC building", category: "Data centre", icon: "🏢", shape: "rect",
|
||
help: "The shell of a multi-storey data centre: storeys of the project's storey height (parameter storey_h), outer walls, slabs and a flat roof with a parapet for the plant. Levels GF, 1F, ... cut it open at that storey; parts inside it on closed storeys are not drawn.",
|
||
props: [
|
||
#{ key: "w", type: "float", def: 40.0, min: 8.0, max: 200.0, step: 0.5, label: "Width (m)" },
|
||
#{ key: "d", type: "float", def: 24.0, min: 8.0, max: 200.0, step: 0.5, label: "Depth (m)" },
|
||
#{ key: "storeys", type: "int", def: 3, min: 1, max: 12, step: 1, label: "Storeys" },
|
||
#{ key: "t", type: "float", def: 0.3, min: 0.15, max: 0.8, step: 0.05, label: "Walls (m)" },
|
||
#{ key: "parapet", type: "float", def: 1.2, min: 0.0, max: 4.0, step: 0.1, label: "Parapet (m)" },
|
||
#{ key: "mat", type: "material", def: "concb", choices: wall_materials(), label: "Walls" },
|
||
#{ key: "roof_mat", type: "material", def: "roof2", choices: roof_materials(), label: "Roof" },
|
||
#{ key: "door", type: "bool", def: true, label: "Front door" },
|
||
#{ key: "door_at", type: "float", def: 0.25, min: 0.0, max: 1.0, step: 0.01, label: "Door at (0-1)" },
|
||
#{ key: "dock", type: "bool", def: true, label: "Loading dock" },
|
||
#{ key: "dock_at", type: "float", def: 0.75, min: 0.0, max: 1.0, step: 0.01, label: "Dock at (0-1)" },
|
||
#{ key: "louvres", type: "bool", def: true, label: "Louvres" },
|
||
] }
|
||
}
|
||
|
||
/// The building's sprite (ghosted on levels above its top storey).
|
||
fn dc_sprite(o, ghost) { if ghost { sprite(o.id, "dim") } else { sprite() } }
|
||
|
||
fn prefab_dc_building(o) {
|
||
let p = o.p;
|
||
let H = storey_h();
|
||
let n = p.storeys;
|
||
let w = num(p.w) / 2.0;
|
||
let d = num(p.d) / 2.0;
|
||
let t = num(p.t);
|
||
let k0 = floor_of(o.level);
|
||
let k = if k0 < 0 { -1 } else { k0 - storey_of(o.z) };
|
||
let ghost = k >= n;
|
||
let closed = k < 0 || ghost;
|
||
let top = n * H;
|
||
// the faces: start point, direction and length; walls along x run
|
||
// through the corners, the others stop at them
|
||
let faces = [
|
||
#{ f: "S", a: [-w, -d], u: [1.0, 0.0], L: 2.0 * w, n: [0.0, -1.0], lo: 0.0 },
|
||
#{ f: "E", a: [w, -d], u: [0.0, 1.0], L: 2.0 * d, n: [1.0, 0.0], lo: t },
|
||
#{ f: "N", a: [w, d], u: [-1.0, 0.0], L: 2.0 * w, n: [0.0, 1.0], lo: 0.0 },
|
||
#{ f: "W", a: [-w, d], u: [0.0, -1.0], L: 2.0 * d, n: [-1.0, 0.0], lo: t },
|
||
];
|
||
for F in faces {
|
||
let zt = if closed { top + num(p.parapet) } else if vis(F.n[0], F.n[1]) { k * H + 1.2 } else { (k + 1) * H };
|
||
let cap = if closed { p.mat } else { "cut" };
|
||
let ops = dc_openings(p, F, H, n, top);
|
||
let lo = F.lo;
|
||
let hi = F.L - F.lo;
|
||
let m = ((hi - lo) / 15.0).ceiling().to_int();
|
||
for i in 0..m {
|
||
let ua = lo + (hi - lo) * i / m;
|
||
let ub = lo + (hi - lo) * (i + 1) / m;
|
||
let pa = [F.a[0] + F.u[0] * ua, F.a[1] + F.u[1] * ua];
|
||
let pb = [F.a[0] + F.u[0] * ub, F.a[1] + F.u[1] * ub];
|
||
// the wall piece behind the face pa -> pb, t thick inwards
|
||
let ix = -F.n[0] * t;
|
||
let iy = -F.n[1] * t;
|
||
let x0 = fmin(fmin(pa[0], pb[0]), fmin(pa[0], pb[0]) + ix);
|
||
let x1 = fmax(fmax(pa[0], pb[0]), fmax(pa[0], pb[0]) + ix);
|
||
let y0 = fmin(fmin(pa[1], pb[1]), fmin(pa[1], pb[1]) + iy);
|
||
let y1 = fmax(fmax(pa[1], pb[1]), fmax(pa[1], pb[1]) + iy);
|
||
let s = dc_sprite(o, ghost);
|
||
s.box(x0, y0, x1, y1, 0.0, zt, p.mat, #{ top: cap });
|
||
for c in ops.keys() {
|
||
let list = [];
|
||
for q in ops[c] {
|
||
let u0 = fmax(q[0], ua);
|
||
let u1 = fmin(q[1], ub);
|
||
let z1 = fmin(q[3], zt);
|
||
if u1 > u0 + 0.01 && z1 > q[2] + 0.01 { list.push([u0 - ua, u1 - ua, q[2], z1]); }
|
||
}
|
||
if list.len() > 0 { s.open(pa, pb, list, c); }
|
||
}
|
||
}
|
||
}
|
||
let ix0 = -w + t;
|
||
let ix1 = w - t;
|
||
let iy0 = -d + t;
|
||
let iy1 = d - t;
|
||
if closed {
|
||
dc_sprite(o, ghost).box(ix0, iy0, ix1, iy1, top - 0.3, top, "plinth", #{ top: p.roof_mat });
|
||
} else if k == 0 {
|
||
layer("floor");
|
||
sprite().flat(rect(ix0, iy0, ix1, iy1), 0.005, "z-floor t");
|
||
layer("main");
|
||
} else {
|
||
sprite().box(ix0, iy0, ix1, iy1, k * H - 0.3, k * H, "plinth", #{ top: "z-floor" });
|
||
}
|
||
info_row("Storeys", `${n} × ${dc_num(H)} m, ${fmt(num(p.w) * num(p.d) * n)} m² gross`);
|
||
let s0 = storey_of(o.z);
|
||
for j in 0..n {
|
||
let it = dc_it(s0 + j);
|
||
if it.racks > 0 { info_row(dc_storey_name(j), `${it.racks} racks, ${fmt(it.kw)} kW IT`); }
|
||
}
|
||
let all = dc_it(-1);
|
||
let pl = dc_plant();
|
||
if all.racks > 0 { info_row("IT load", `${fmt(all.kw)} kW in ${all.racks} racks`); }
|
||
if pl.units > 0 { info_row("Plant cooling", `${fmt(pl.kw)} kW in ${pl.units} chiller / tower units`); }
|
||
}
|
||
|
||
/// Openings on face F of a dc_building, in face coordinates (u from the
|
||
/// face's start, z from the ground), by class: slab edges, louvres on the
|
||
/// side faces, the front door and the loading dock on the front (-y) face
|
||
/// (door_at, dock_at: how far along it, 0 to 1).
|
||
fn dc_openings(p, F, H, n, top) {
|
||
let out = #{};
|
||
let bands = [];
|
||
for j in 1..n { bands.push([0.0, F.L, j * H - 0.3, j * H]); }
|
||
bands.push([0.0, F.L, top - 0.3, top]);
|
||
out["dcband"] = bands;
|
||
if p.louvres && (F.f == "E" || F.f == "W") {
|
||
let lv = [];
|
||
let m = ((F.L - 4.0) / 7.0).floor().to_int();
|
||
for j in 0..n {
|
||
for i in 0..m {
|
||
let u = (F.L - (m - 1) * 7.0) / 2.0 + i * 7.0;
|
||
lv.push([u - 1.6, u + 1.6, j * H + 1.4, j * H + 3.4]);
|
||
}
|
||
}
|
||
if lv.len() > 0 { out["louvre"] = lv; }
|
||
}
|
||
if F.f == "S" {
|
||
// along the front from its -x end, kept off the corners
|
||
if p.door { let u = clamp(F.L * num(p.door_at), 1.2, F.L - 1.2); out["door"] = [[u - 0.9, u + 0.9, 0.0, 2.4]]; }
|
||
if p.dock { let u = clamp(F.L * num(p.dock_at), 2.4, F.L - 2.4); out["door2"] = [[u - 2.0, u + 2.0, 0.0, 4.2]]; }
|
||
}
|
||
out
|
||
}
|
||
|
||
// ---------------------------------------------------------------- raised floor
|
||
|
||
fn meta_raised_floor() {
|
||
#{ label: "Raised floor", category: "Data centre", icon: "▦", shape: "rect", storey: true,
|
||
help: "An access floor on pedestals (0.6 m tiles) whose void is the supply plenum. Racks, pods, CRAH units and cabinets on it stand on its top. Fit it inside the walls; walls take it for a room.",
|
||
props: [
|
||
#{ key: "w", type: "float", def: 12.0, min: 1.2, max: 200.0, step: 0.6, label: "Width (m)" },
|
||
#{ key: "d", type: "float", def: 8.4, min: 1.2, max: 200.0, step: 0.6, label: "Depth (m)" },
|
||
#{ key: "h", type: "float", def: 0.6, min: 0.15, max: 1.5, step: 0.05, label: "Void (m)" },
|
||
#{ key: "mat", type: "material", def: "dcrf", choices: ["dcrf", "dcrf2", "dcrf3"], label: "Tiles" },
|
||
#{ key: "grid", type: "bool", def: true, label: "Tile joints" },
|
||
] }
|
||
}
|
||
|
||
fn prefab_raised_floor(o) {
|
||
if dc_skip(o) { return; }
|
||
let p = o.p;
|
||
let w = num(p.w) / 2.0;
|
||
let d = num(p.d) / 2.0;
|
||
let h = num(p.h);
|
||
let s = sprite();
|
||
s.box(-w, -d, w, d, 0.0, h, "dcrfs", #{ top: p.mat });
|
||
// the void on the sides: dark, with the pedestals every tile
|
||
let sd = sides(-w, -d, w, d);
|
||
for f in ["S", "E", "N", "W"] {
|
||
let L = if f == "S" || f == "N" { 2.0 * w } else { 2.0 * d };
|
||
let legs = [];
|
||
let u = 0.3;
|
||
while u < L - 0.1 {
|
||
legs.push([u - 0.03, u + 0.03, 0.0, h - 0.04]);
|
||
u += 0.6;
|
||
}
|
||
s.open(sd[f][0], sd[f][1], [[0.0, L, 0.0, h - 0.04]], "dcvoid");
|
||
if legs.len() > 0 && legs.len() < 400 { s.open(sd[f][0], sd[f][1], legs, "dcleg"); }
|
||
}
|
||
if p.grid {
|
||
let segs = [];
|
||
let x = -w + 0.6;
|
||
while x < w - 0.05 { segs.push([x, -d, x, d]); x += 0.6; }
|
||
let y = -d + 0.6;
|
||
while y < d - 0.05 { segs.push([-w, y, w, y]); y += 0.6; }
|
||
if segs.len() < 700 { s.lines(segs, h, "dctile"); }
|
||
}
|
||
// what stands on it
|
||
let racks = 0;
|
||
let kw = 0.0;
|
||
for r in objects_of(dc_rack_types()) {
|
||
if storey_of(r.z) != storey_of(o.z) { continue; }
|
||
let l = local_of(o, r.x, r.y);
|
||
if l[0].abs() <= w && l[1].abs() <= d {
|
||
racks += dc_racks_of(r);
|
||
kw += dc_kw_of(r);
|
||
}
|
||
}
|
||
let area = num(p.w) * num(p.d);
|
||
info_row("Area", `${fmt(area)} m², ${fmt(area / 0.36)} tiles of 0.6 m`);
|
||
info_row("Void", `${dc_num(h)} m (supply air plenum)`);
|
||
if racks > 0 {
|
||
info_row("On it", `${racks} racks, ${fmt(kw)} kW IT`);
|
||
info_row("Density", `${dc_num(kw / area)} kW/m² over the floor, ${dc_num(kw / racks.to_float())} kW per rack`);
|
||
}
|
||
}
|
||
|
||
// ---------------------------------------------------------------- racks
|
||
|
||
fn dc_rack_kinds() { ["servers", "storage", "GPU", "network", "empty"] }
|
||
|
||
fn dc_rack_props() {
|
||
[
|
||
#{ key: "n", type: "int", def: 10, min: 1, max: 40, step: 1, label: "Racks" },
|
||
#{ key: "kind", type: "choice", def: "servers", choices: dc_rack_kinds(), label: "Equipment" },
|
||
#{ key: "kw", type: "float", def: 8.0, min: 0.0, max: 150.0, step: 0.5, label: "kW per rack" },
|
||
#{ key: "fill", type: "int", def: 70, min: 0, max: 100, step: 5, label: "Filled (%)" },
|
||
#{ key: "u", type: "int", def: 42, min: 20, max: 52, step: 1, label: "Height (U)" },
|
||
#{ key: "rw", type: "float", def: 0.6, min: 0.6, max: 1.0, step: 0.1, label: "Rack width (m)" },
|
||
#{ key: "rd", type: "float", def: 1.2, min: 0.6, max: 1.4, step: 0.05, label: "Rack depth (m)" },
|
||
#{ key: "doors", type: "choice", def: "mesh", choices: ["mesh", "glass", "solid", "none"], label: "Doors" },
|
||
#{ key: "inrow", type: "int", def: 0, min: 0, max: 20, step: 1, label: "Cooler every" },
|
||
#{ key: "inrow_kw", type: "float", def: 30.0, min: 5.0, max: 150.0, step: 5.0, label: "Cooler kW" },
|
||
#{ key: "leds", type: "bool", def: true, label: "Status LEDs" },
|
||
]
|
||
}
|
||
|
||
/// A face along local x at y with outward normal ny (-1 or 1), from -B to
|
||
/// B: #{ a, b, k } with u(x) = k * x + B along it.
|
||
fn dc_xface(B, y, ny) {
|
||
if ny < 0.0 { #{ a: [-B, y], b: [B, y], k: 1.0 } } else { #{ a: [B, y], b: [-B, y], k: -1.0 } }
|
||
}
|
||
|
||
/// [u0, u1, z0, z1] on face F (dc_xface) for x0..x1.
|
||
fn dc_span(F, B, x0, x1, z0, z1) {
|
||
let u0 = F.k * x0 + B;
|
||
let u1 = F.k * x1 + B;
|
||
[fmin(u0, u1), fmax(u0, u1), z0, z1]
|
||
}
|
||
|
||
/// A line of racks (and in-row coolers) along local x, centred at y = yc,
|
||
/// fronts towards f (-1: local -y, 1: +y), standing at z0, drawn into s.
|
||
/// key seeds which slots are filled and the LED timing.
|
||
fn dc_rack_line(s, p, yc, f, z0, key) {
|
||
let units = dc_units(p);
|
||
let h = dc_rack_h(p.u);
|
||
let zt = z0 + h;
|
||
let rd = num(p.rd) / 2.0;
|
||
let y0 = yc - rd;
|
||
let y1 = yc + rd;
|
||
let items = [];
|
||
for q in units {
|
||
let a = q[0];
|
||
let b = q[1];
|
||
let m = if q[2] { "dcinrow" } else { "dccab" };
|
||
let t = if q[2] { "dcinrow" } else { "dccabt" };
|
||
items.push([(a + b) / 2.0, yc, |c| c.box(a, y0, b, y1, z0, zt, m, #{ top: t })]);
|
||
}
|
||
s.seq(items);
|
||
let B = units[units.len() - 1][1];
|
||
let F = dc_xface(B, yc + f * rd, f);
|
||
let R = dc_xface(B, yc - f * rd, -f);
|
||
let kind = p.kind;
|
||
let bays = [];
|
||
let gear = [];
|
||
let gear2 = [];
|
||
let strips = [];
|
||
let rear = [];
|
||
let pdus = [];
|
||
let grilles = [];
|
||
let rgrilles = [];
|
||
let screens = [];
|
||
let handles = [];
|
||
let leds = [];
|
||
let zb = z0 + 0.1;
|
||
let zh = zt - 0.09;
|
||
let i = 0;
|
||
for q in units {
|
||
let a = q[0];
|
||
let b = q[1];
|
||
if q[2] {
|
||
grilles.push(dc_span(F, B, a + 0.03, b - 0.03, z0 + 0.12, zt - 0.32));
|
||
screens.push(dc_span(F, B, a + 0.07, b - 0.07, zt - 0.26, zt - 0.14));
|
||
rgrilles.push(dc_span(R, B, a + 0.03, b - 0.03, z0 + 0.12, zt - 0.12));
|
||
continue;
|
||
}
|
||
let ia = a + 0.045;
|
||
let ib = b - 0.045;
|
||
bays.push(dc_span(F, B, ia, ib, zb, zh));
|
||
rear.push(dc_span(R, B, ia, ib, zb, zh));
|
||
pdus.push(dc_span(R, B, ia + 0.03, ia + 0.07, zb + 0.15, zh - 0.15));
|
||
pdus.push(dc_span(R, B, ib - 0.07, ib - 0.03, zb + 0.15, zh - 0.15));
|
||
// equipment from the bottom (network: from the top), about `fill` of the slots
|
||
let slot = if kind == "storage" { 0.178 } else if kind == "GPU" { 0.267 } else if kind == "network" { 0.0445 } else { 0.0889 };
|
||
let ns = ((zh - zb - 0.04) / slot).floor().to_int();
|
||
let r = hash(key * 17.3 + i * 3.71);
|
||
let used = if kind == "empty" { 0 } else { clamp((ns * p.fill / 100.0 + (r - 0.5) * 3.0).round(), 0.0, ns).to_int() };
|
||
for j in 0..used {
|
||
if kind == "network" {
|
||
let zz = zh - 0.02 - (j + 1) * slot;
|
||
if j % 3 == 2 { continue; }
|
||
let row = dc_span(F, B, ia + 0.03, ib - 0.03, zz + 0.004, zz + slot - 0.004);
|
||
if j % 3 == 0 { gear2.push(row); } else { gear.push(row); }
|
||
} else {
|
||
// a gap now and then
|
||
if hash(key * 5.1 + i * 2.3 + j * 0.77) < 0.08 { continue; }
|
||
let za = zb + 0.02 + j * slot;
|
||
gear.push(dc_span(F, B, ia + 0.02, ib - 0.02, za + 0.004, za + slot - 0.004));
|
||
if kind == "GPU" { strips.push(dc_span(F, B, ia + 0.06, ib - 0.06, za + slot - 0.05, za + slot - 0.025)); }
|
||
if kind == "storage" { gear2.push(dc_span(F, B, ia + 0.05, ib - 0.05, za + slot * 0.45, za + slot * 0.55)); }
|
||
}
|
||
}
|
||
if p.doors == "solid" { handles.push(dc_span(F, B, ib - 0.07, ib - 0.05, z0 + 0.9, z0 + 1.3)); }
|
||
leds.push([(a + b) / 2.0, i]);
|
||
i += 1;
|
||
}
|
||
let gc = if kind == "storage" { "dcstor" } else if kind == "GPU" { "dcgpu" } else if kind == "network" { "dcnet" } else { "dcsrv" };
|
||
let g2 = if kind == "network" { "dcpatch" } else { "dcslot" };
|
||
if bays.len() > 0 {
|
||
s.open(F.a, F.b, bays, if p.doors == "solid" { "dcdoor" } else { "dcbay" });
|
||
if p.doors != "solid" {
|
||
if gear.len() > 0 { s.open(F.a, F.b, gear, gc); }
|
||
if gear2.len() > 0 { s.open(F.a, F.b, gear2, g2); }
|
||
if strips.len() > 0 { s.open(F.a, F.b, strips, "dcstrip"); }
|
||
if p.doors == "mesh" { s.open(F.a, F.b, bays, "dcmesh"); }
|
||
if p.doors == "glass" { s.open(F.a, F.b, bays, "dcglassd"); }
|
||
}
|
||
if handles.len() > 0 { s.open(F.a, F.b, handles, "metal"); }
|
||
s.open(R.a, R.b, rear, "dcrear");
|
||
s.open(R.a, R.b, pdus, "dcpdu0");
|
||
}
|
||
if grilles.len() > 0 {
|
||
s.open(F.a, F.b, grilles, "grille");
|
||
s.open(F.a, F.b, screens, "dcscr");
|
||
s.open(R.a, R.b, rgrilles, "grille");
|
||
}
|
||
if p.leds && vis(0.0, f) && kind != "empty" {
|
||
let yl = yc + f * (rd + 0.01);
|
||
for l in leds {
|
||
let x = l[0];
|
||
s.begin("a dcblink", `animation-delay:-${round1(hash(key * 7.7 + l[1] * 1.37) * 2.6)}s`);
|
||
s.circle(x - 0.12, yl, zt - 0.05, 0.3, "ledg");
|
||
s.end();
|
||
s.circle(x + 0.12, yl, zt - 0.05, 0.3, if hash(key + l[1] * 9.1) < 0.12 { "ledo" } else { "ledg" });
|
||
}
|
||
}
|
||
#{ B: B, h: h }
|
||
}
|
||
|
||
/// Vent tiles (flat at the floor top z0) between y = ya and yb in front of
|
||
/// the racks of a line, as rectangles for one flat.
|
||
fn dc_vents(p, ya, yb) {
|
||
let out = [];
|
||
for q in dc_units(p) {
|
||
if !q[2] { out.push(rect(q[0] + 0.02, ya + 0.02, q[1] - 0.02, yb - 0.02)); }
|
||
}
|
||
out
|
||
}
|
||
|
||
/// Tooltip rows shared by rack rows and pods.
|
||
fn dc_rack_info(o, lines) {
|
||
let p = o.p;
|
||
let n = lines * p.n;
|
||
let kw = if p.kind == "empty" { 0.0 } else { n.to_float() * num(p.kw) };
|
||
let side = if lines > 1 { `${lines} × ${p.n}` } else { `${p.n}` };
|
||
info_row("Racks", `${side} × ${p.u}U ${p.kind}, ${dc_num(num(p.rw) * 1000.0)} × ${dc_num(num(p.rd) * 1000.0)} mm`);
|
||
if p.kind != "empty" {
|
||
info_row("IT load", `${dc_num(num(p.kw))} kW per rack, ${fmt(kw)} kW in all`);
|
||
info_row("Filled", `about ${p.fill} % of ${n * p.u} U`);
|
||
}
|
||
let c = dc_coolers(p) * lines;
|
||
if c > 0 { info_row("In-row cooling", `${c} × ${fmt(num(p.inrow_kw))} kW = ${fmt(c.to_float() * num(p.inrow_kw))} kW`); }
|
||
}
|
||
|
||
fn meta_rack_row() {
|
||
#{ label: "Rack row", category: "Data centre", icon: "▥", storey: true,
|
||
help: "A row of cabinets along local x, fronts (the cold side) to local -y. It stands on the raised floor under it, with vent tiles in front; in-row coolers between groups of racks (Cooler every).",
|
||
props: dc_rack_props() + [ #{ key: "vents", type: "bool", def: true, label: "Vent tiles" } ] }
|
||
}
|
||
|
||
fn prefab_rack_row(o) {
|
||
if dc_skip(o) { return; }
|
||
let p = o.p;
|
||
let z0 = dc_lift(o);
|
||
let r = dc_rack_line(sprite(), p, 0.0, -1.0, z0, hash(o.x * 3.1 + o.y * 7.7 + o.z));
|
||
if p.vents && z0 > 0.0 && p.kind != "empty" {
|
||
let rd = num(p.rd) / 2.0;
|
||
sprite().flat(dc_vents(p, -rd - 0.6, -rd), z0, "dcperf");
|
||
}
|
||
dc_rack_info(o, 1);
|
||
}
|
||
|
||
fn meta_dc_pod() {
|
||
#{ label: "Rack pod", category: "Data centre", icon: "▤", storey: true,
|
||
help: "Two rows of racks along local x around a shared aisle. Cold aisle containment: the fronts face the aisle, which gets vent tiles, a roof and sliding doors at both ends; hot aisle: the backs face it (optionally with a duct to the ceiling). Stands on the raised floor under it.",
|
||
props: dc_rack_props() + [
|
||
#{ key: "contain", type: "choice", def: "cold", choices: ["cold", "hot", "none"], label: "Containment" },
|
||
#{ key: "aisle", type: "float", def: 1.2, min: 0.9, max: 3.0, step: 0.3, label: "Aisle (m)" },
|
||
#{ key: "roof", type: "bool", def: true, label: "Aisle roof" },
|
||
#{ key: "ends", type: "bool", def: true, label: "End doors" },
|
||
#{ key: "duct", type: "bool", def: false, label: "Duct to ceiling" },
|
||
#{ key: "duct_h", type: "float", def: 4.4, min: 3.0, max: 12.0, step: 0.1, label: "Ceiling (m)" },
|
||
#{ key: "air", type: "bool", def: true, label: "Airflow" },
|
||
] }
|
||
}
|
||
|
||
fn prefab_dc_pod(o) {
|
||
if dc_skip(o) { return; }
|
||
let p = o.p;
|
||
let z0 = dc_lift(o);
|
||
let a = num(p.aisle) / 2.0;
|
||
let rd = num(p.rd) / 2.0;
|
||
let hot = p.contain == "hot";
|
||
let cont = p.contain != "none";
|
||
let key = hash(o.x * 3.1 + o.y * 7.7 + o.z);
|
||
// cold aisle: fronts face the aisle; hot aisle: backs do
|
||
let fa = if hot { -1.0 } else { 1.0 };
|
||
let r = dc_rack_line(sprite(), p, -a - rd, fa, z0, key);
|
||
dc_rack_line(sprite(), p, a + rd, -fa, z0, key + 0.37);
|
||
let B = r.B;
|
||
let zt = z0 + r.h;
|
||
let units = dc_units(p);
|
||
let s = sprite();
|
||
let ends = if !cont || !p.ends { [] } else if vis(1.0, 0.0) { [-B, B] } else { [B, -B] };
|
||
if ends.len() > 0 { dc_aisle_door(s, ends[0], a, z0, zt, hot); }
|
||
if !hot && z0 > 0.0 && p.kind != "empty" { s.flat(dc_vents(p, -a, a), z0, "dcperf"); }
|
||
if p.air && p.kind != "empty" {
|
||
let i = 0;
|
||
for q in units {
|
||
if q[2] { continue; }
|
||
if i % 2 == 0 {
|
||
let x = (q[0] + q[1]) / 2.0;
|
||
if hot { s.puffs(x, 0.0, zt - 0.5, "dcheat", 2, 3.4); } else if z0 > 0.0 { s.puffs(x, 0.0, z0 + 0.15, "dcair", 2, 3.0); }
|
||
}
|
||
i += 1;
|
||
}
|
||
}
|
||
if ends.len() > 0 { dc_aisle_door(s, ends[1], a, z0, zt, hot); }
|
||
if cont && p.roof {
|
||
s.box(-B, -a, B, a, zt, zt + 0.04, if hot { "dcglassh" } else { "dcglass" });
|
||
let segs = [[-B, -a, B, -a], [-B, a, B, a]];
|
||
for q in units { segs.push([q[1], -a, q[1], a]); }
|
||
segs.push([-B, -a, -B, a]);
|
||
s.lines(segs, zt + 0.04, "dcframe");
|
||
}
|
||
if hot && p.duct && num(p.duct_h) > r.h + 0.3 {
|
||
s.box(-B + 0.2, -a + 0.05, B - 0.2, a - 0.05, zt + 0.04, num(p.duct_h), "dcalu");
|
||
}
|
||
s.ext_box(-B, -a, B, a, z0, z0);
|
||
dc_rack_info(o, 2);
|
||
let what = if p.contain == "cold" { "cold aisle" } else if hot { "hot aisle" } else { "none (open aisles)" };
|
||
let parts = [];
|
||
if cont && p.roof { parts.push("roof"); }
|
||
if cont && p.ends { parts.push("end doors"); }
|
||
if hot && p.duct { parts.push("ceiling duct"); }
|
||
let extra = "";
|
||
for x in parts { extra += if extra == "" { ", " + x } else { " and " + x }; }
|
||
info_row("Containment", `${what}, ${dc_num(num(p.aisle))} m${extra}`);
|
||
}
|
||
|
||
/// The sliding doors closing a contained aisle at x (an end of it).
|
||
fn dc_aisle_door(s, x, a, z0, zt, hot) {
|
||
let x0 = if x > 0.0 { x - 0.05 } else { x };
|
||
let x1 = x0 + 0.05;
|
||
let g = if hot { "dcglassh" } else { "dcglass" };
|
||
s.box(x0, -a, x1, -a + 0.05, z0, zt, "dcalu");
|
||
s.box(x0, a - 0.05, x1, a, z0, zt, "dcalu");
|
||
s.box(x0, -a + 0.05, x1, a - 0.05, zt - 0.1, zt, "dcalu");
|
||
s.box(x0 + 0.01, -a + 0.05, x1 - 0.01, a - 0.05, z0, zt - 0.1, g);
|
||
// the meeting stiles, on whichever side faces the viewer
|
||
s.open([x1, -a], [x1, a], [[a - 0.02, a + 0.02, z0, zt - 0.1]], "dcalu");
|
||
s.open([x0, a], [x0, -a], [[a - 0.02, a + 0.02, z0, zt - 0.1]], "dcalu");
|
||
s
|
||
}
|
||
|
||
fn meta_dc_cage() {
|
||
#{ label: "Cage", category: "Data centre", icon: "▢", shape: "rect", storey: true,
|
||
help: "A colocation cage: mesh panels on posts round the rectangle, a door on the local -y side. Leave room between the mesh and the racks inside.",
|
||
props: [
|
||
#{ key: "w", type: "float", def: 6.0, min: 1.2, max: 60.0, step: 0.3, label: "Width (m)" },
|
||
#{ key: "d", type: "float", def: 4.8, min: 1.2, max: 60.0, step: 0.3, label: "Depth (m)" },
|
||
#{ key: "h", type: "float", def: 2.4, min: 1.0, max: 4.0, step: 0.1, label: "Height (m)" },
|
||
#{ key: "door", type: "bool", def: true, label: "Door" },
|
||
] }
|
||
}
|
||
|
||
fn prefab_dc_cage(o) {
|
||
if dc_skip(o) { return; }
|
||
let p = o.p;
|
||
let z0 = dc_lift(o);
|
||
let w = num(p.w) / 2.0;
|
||
let d = num(p.d) / 2.0;
|
||
let h = num(p.h);
|
||
let t = 0.05;
|
||
// one sprite per side (the ones along x run through the corners)
|
||
let walls = [
|
||
[-w, -d, w, -d + t, "x"], [-w, d - t, w, d, "x"],
|
||
[-w, -d + t, -w + t, d - t, "y"], [w - t, -d + t, w, d - t, "y"],
|
||
];
|
||
let k = 0;
|
||
for q in walls {
|
||
let s = sprite();
|
||
let alongx = q[4] == "x";
|
||
let L = if alongx { q[2] - q[0] } else { q[3] - q[1] };
|
||
let gap = if k == 0 && p.door && L > 2.0 { [-0.5, 0.5] } else { [] };
|
||
// mesh panels between posts every 1.2 m (and either side of the door)
|
||
let cuts = [0.0];
|
||
let m = (L / 1.2).ceiling().to_int();
|
||
for i in 1..m { cuts.push(L * i / m); }
|
||
cuts.push(L);
|
||
let posts = [];
|
||
for c in cuts { posts.push(c); }
|
||
if gap.len() > 0 { posts.push(L / 2.0 + gap[0]); posts.push(L / 2.0 + gap[1]); }
|
||
let items = [];
|
||
for c in posts {
|
||
let px = if alongx { q[0] + clamp(c, 0.03, L - 0.03) } else { (q[0] + q[2]) / 2.0 };
|
||
let py = if alongx { (q[1] + q[3]) / 2.0 } else { q[1] + clamp(c, 0.03, L - 0.03) };
|
||
items.push([px, py, |r| r.box(px - 0.03, py - 0.03, px + 0.03, py + 0.03, z0, z0 + h, "steel")]);
|
||
}
|
||
let panels = [];
|
||
for i in 0..(cuts.len() - 1) {
|
||
let u0 = cuts[i] + 0.03;
|
||
let u1 = cuts[i + 1] - 0.03;
|
||
if gap.len() > 0 && u1 > L / 2.0 + gap[0] && u0 < L / 2.0 + gap[1] {
|
||
// the door leaf (closed) in the gap, panels either side of it
|
||
if L / 2.0 + gap[0] - 0.03 > u0 + 0.05 { panels.push([u0, L / 2.0 + gap[0] - 0.03]); }
|
||
if u1 > L / 2.0 + gap[1] + 0.08 { panels.push([L / 2.0 + gap[1] + 0.03, u1]); }
|
||
continue;
|
||
}
|
||
panels.push([u0, u1]);
|
||
}
|
||
for u in panels {
|
||
let a = u[0];
|
||
let b = u[1];
|
||
let cx = if alongx { q[0] + (a + b) / 2.0 } else { (q[0] + q[2]) / 2.0 };
|
||
let cy = if alongx { (q[1] + q[3]) / 2.0 } else { q[1] + (a + b) / 2.0 };
|
||
items.push([cx, cy, |r| if alongx { r.box(q[0] + a, q[1] + 0.01, q[0] + b, q[3] - 0.01, z0 + 0.05, z0 + h - 0.05, "dcmeshp") } else { r.box(q[0] + 0.01, q[1] + a, q[2] - 0.01, q[1] + b, z0 + 0.05, z0 + h - 0.05, "dcmeshp") }]);
|
||
}
|
||
if gap.len() > 0 {
|
||
let a = L / 2.0 + gap[0] + 0.03;
|
||
let b = L / 2.0 + gap[1] - 0.03;
|
||
items.push([q[0] + (a + b) / 2.0, (q[1] + q[3]) / 2.0, |r| r.box(q[0] + a, q[1] + 0.01, q[0] + b, q[3] - 0.01, z0 + 0.02, z0 + 2.05, "dcmeshd")]);
|
||
}
|
||
s.seq(items);
|
||
k += 1;
|
||
}
|
||
let racks = 0;
|
||
for r in objects_of(dc_rack_types()) {
|
||
if storey_of(r.z) != storey_of(o.z) { continue; }
|
||
let l = local_of(o, r.x, r.y);
|
||
if l[0].abs() <= w && l[1].abs() <= d { racks += dc_racks_of(r); }
|
||
}
|
||
info_row("Cage", `${dc_num(num(p.w))} × ${dc_num(num(p.d))} m, ${dc_num(h)} m mesh`);
|
||
if racks > 0 { info_row("Inside", `${racks} racks`); }
|
||
}
|
||
|
||
// ---------------------------------------------------------------- overhead
|
||
|
||
fn meta_cable_tray() {
|
||
#{ label: "Cable tray", category: "Data centre", icon: "☰", shape: "line", storey: true,
|
||
help: "An overhead ladder tray (power), fibre duct (yellow) or wire basket (copper) along local x, hung above the racks. Height above the storey's slab.",
|
||
props: [
|
||
#{ key: "w", type: "float", def: 10.0, min: 0.5, max: 60.0, step: 0.1, label: "Length (m)" },
|
||
#{ key: "kind", type: "choice", def: "ladder", choices: ["ladder", "fibre", "basket"], label: "Kind" },
|
||
#{ key: "width", type: "float", def: 0.45, min: 0.1, max: 1.2, step: 0.05, label: "Width (m)" },
|
||
#{ key: "h", type: "float", def: 3.6, min: 2.0, max: 8.0, step: 0.05, label: "Height (m)" },
|
||
#{ key: "fill", type: "int", def: 50, min: 0, max: 100, step: 10, label: "Cables (%)" },
|
||
] }
|
||
}
|
||
|
||
fn prefab_cable_tray(o) {
|
||
if dc_skip(o) || dc_no_ceiling() { return; }
|
||
let p = o.p;
|
||
let w = num(p.w) / 2.0;
|
||
let b = num(p.width) / 2.0;
|
||
let h = num(p.h);
|
||
let m = if p.kind == "fibre" { "dcfibre" } else if p.kind == "basket" { "dcbasket" } else { "dctray" };
|
||
let side = if p.kind == "fibre" { 0.1 } else { 0.08 };
|
||
let cab = if p.kind == "fibre" { "dcfibrec" } else if p.kind == "basket" { "dccablec" } else { "dccable" };
|
||
dc_ceiling_layer();
|
||
let near = if vis(0.0, 1.0) { 1.0 } else { -1.0 };
|
||
// the rails: the far one is drawn first, the near one last
|
||
let fy0 = if near > 0.0 { -b } else { b - 0.02 };
|
||
let ny0 = if near > 0.0 { b - 0.02 } else { -b };
|
||
let pieces = (num(p.w) / 15.0).ceiling().to_int();
|
||
for i in 0..pieces {
|
||
let x0 = -w + num(p.w) * i / pieces;
|
||
let x1 = -w + num(p.w) * (i + 1) / pieces;
|
||
let s = sprite();
|
||
// hangers first (behind), then the far rail, the bed, cables, the near rail
|
||
let x = x0 + 0.4;
|
||
while x < x1 - 0.1 {
|
||
s.stem(x, -near * (b - 0.02), h + side, h + side + 0.2, 0.1, "metal");
|
||
x += 1.5;
|
||
}
|
||
s.box(x0, fy0, x1, fy0 + 0.02, h, h + side, m);
|
||
if p.kind == "ladder" {
|
||
let rungs = [];
|
||
let x = x0 + 0.15;
|
||
while x < x1 { rungs.push([x - 0.02, -b + 0.02, x + 0.02, b - 0.02]); x += 0.3; }
|
||
s.flat(rungs, h + 0.005, m + " t");
|
||
} else {
|
||
s.box(x0, -b + 0.02, x1, b - 0.02, h, h + 0.015, m);
|
||
}
|
||
if p.fill > 0 {
|
||
let ch = 0.01 + (side - 0.02) * p.fill / 100.0;
|
||
s.box(x0, -b + 0.03, x1, b - 0.03, h + 0.015, h + 0.015 + ch, cab);
|
||
}
|
||
s.box(x0, ny0, x1, ny0 + 0.02, h, h + side, m);
|
||
// sort by the tray alone: its hangers would reach into the runs above
|
||
s.ext_reset();
|
||
s.ext_box(x0, -b, x1, b, h, h + side);
|
||
}
|
||
let what = if p.kind == "fibre" { "fibre duct" } else if p.kind == "basket" { "wire basket (copper)" } else { "ladder tray (power)" };
|
||
info_row("Tray", `${what}, ${dc_num(num(p.width) * 1000.0)} mm wide, ${dc_num(num(p.w))} m`);
|
||
info_row("Height", `${dc_num(h)} m above the slab, cables about ${p.fill} % full`);
|
||
}
|
||
|
||
fn meta_busway() {
|
||
#{ label: "Busway", category: "Power", icon: "≡", shape: "line", storey: true,
|
||
help: "Overhead busbar trunking along local x, hung over a rack row, with a tap-off box for each rack. Height above the storey's slab.",
|
||
props: [
|
||
#{ key: "w", type: "float", def: 8.0, min: 1.0, max: 60.0, step: 0.1, label: "Length (m)" },
|
||
#{ key: "h", type: "float", def: 3.2, min: 2.0, max: 8.0, step: 0.05, label: "Height (m)" },
|
||
#{ key: "amps", type: "int", def: 400, min: 100, max: 1600, step: 50, label: "Rating (A)" },
|
||
#{ key: "taps", type: "float", def: 0.6, min: 0.0, max: 5.0, step: 0.1, label: "Tap-off every (m)" },
|
||
] }
|
||
}
|
||
|
||
fn prefab_busway(o) {
|
||
if dc_skip(o) || dc_no_ceiling() { return; }
|
||
let p = o.p;
|
||
let w = num(p.w) / 2.0;
|
||
let h = num(p.h);
|
||
let tp = num(p.taps);
|
||
dc_ceiling_layer();
|
||
let pieces = (num(p.w) / 15.0).ceiling().to_int();
|
||
let taps = 0;
|
||
for i in 0..pieces {
|
||
let x0 = -w + num(p.w) * i / pieces;
|
||
let x1 = -w + num(p.w) * (i + 1) / pieces;
|
||
let s = sprite();
|
||
let x = x0 + 0.5;
|
||
while x < x1 - 0.1 {
|
||
s.stem(x, 0.0, h + 0.15, h + 0.35, 0.1, "metal");
|
||
x += 2.0;
|
||
}
|
||
if tp > 0.05 {
|
||
let items = [];
|
||
let x = -w + tp / 2.0;
|
||
while x < w {
|
||
if x >= x0 && x < x1 {
|
||
let c = x;
|
||
items.push([c, 0.0, |q| q.box(c - 0.11, -0.1, c + 0.11, 0.1, h - 0.26, h, "dctap")]);
|
||
taps += 1;
|
||
}
|
||
x += tp;
|
||
}
|
||
s.seq(items);
|
||
}
|
||
s.box(x0, -0.08, x1, 0.08, h, h + 0.15, "dcbus");
|
||
s.ext_reset();
|
||
s.ext_box(x0, -0.11, x1, 0.11, h - 0.26, h + 0.15);
|
||
}
|
||
info_row("Busway", `${fmt(p.amps)} A, about ${fmt(1.732 * 0.415 * p.amps * 0.95)} kW at 415 V`);
|
||
info_row("Length", `${dc_num(num(p.w))} m, ${taps} tap-off boxes, ${dc_num(h)} m above the slab`);
|
||
}
|
||
|
||
// ---------------------------------------------------------------- cooling
|
||
|
||
fn meta_crah() {
|
||
#{ label: "CRAH unit", category: "Cooling", icon: "❄", storey: true,
|
||
help: "A computer room air handler (chilled water) or CRAC (DX) unit, front to local -y, blowing down into the raised floor. Its tooltip sets the storey's cooling (units on duty, standby, in-row coolers) against the IT load of its racks.",
|
||
props: [
|
||
#{ key: "kind", type: "choice", def: "CRAH", choices: ["CRAH", "CRAC"], label: "Type" },
|
||
#{ key: "kw", type: "float", def: 120.0, min: 10.0, max: 600.0, step: 5.0, label: "Cooling (kW)" },
|
||
#{ key: "w", type: "float", def: 2.5, min: 0.8, max: 5.0, step: 0.1, label: "Width (m)" },
|
||
#{ key: "standby", type: "bool", def: false, label: "Standby" },
|
||
#{ key: "running", type: "bool", def: true, label: "Fans running" },
|
||
] }
|
||
}
|
||
|
||
fn prefab_crah(o) {
|
||
if dc_skip(o) { return; }
|
||
let p = o.p;
|
||
let z0 = dc_lift(o);
|
||
let W = num(p.w);
|
||
let w = W / 2.0;
|
||
let d = 0.45;
|
||
let h = 1.95;
|
||
let s = sprite();
|
||
s.box(-w, -d, w, d, z0, z0 + h, "dccrah");
|
||
let sd = sides(-w, -d, w, d);
|
||
let nd = fmax(1.0, (W / 0.85).round()).to_int();
|
||
let doors = [];
|
||
for i in 0..nd { doors.push([i * W / nd + 0.03, (i + 1) * W / nd - 0.03, z0 + 0.06, z0 + h - 0.06]); }
|
||
s.open(sd.S[0], sd.S[1], doors, "seam");
|
||
s.open(sd.S[0], sd.S[1], [[0.2, 0.62, z0 + h - 0.42, z0 + h - 0.2]], "dcscr");
|
||
s.open(sd.N[0], sd.N[1], [[0.1, W - 0.1, z0 + h - 0.5, z0 + h - 0.1]], "grille");
|
||
// return air grille on top, the fans under it
|
||
s.flat(rect(-w + 0.08, -d + 0.08, w - 0.08, d - 0.08), z0 + h, "grille");
|
||
let nf = fmax(1.0, (W / 0.8).floor()).to_int();
|
||
let spin = p.running && !p.standby;
|
||
for i in 0..nf {
|
||
let x = -w + (i + 0.5) * W / nf;
|
||
if spin { s.fan(x, 0.0, z0 + h + 0.005, 0.28); } else { s.disc(x, 0.0, z0 + h + 0.005, 0.28, "fanring"); }
|
||
}
|
||
if vis(0.0, -1.0) {
|
||
s.circle(-w + 0.75, -d - 0.01, z0 + h - 0.3, 0.35, if p.standby { "ledo" } else if p.running { "ledg" } else { "ledr" });
|
||
}
|
||
let st = storey_of(o.z);
|
||
let it = dc_it(st);
|
||
let c = dc_cool(st);
|
||
info_row("Type", if p.kind == "CRAC" { "CRAC (DX refrigerant)" } else { "CRAH (chilled water)" });
|
||
info_row("Cooling", `${fmt(num(p.kw))} kW, ${if p.standby { "standby" } else if p.running { "on duty" } else { "stopped" }}`);
|
||
info_row("This storey", `${fmt(it.kw)} kW IT in ${it.racks} racks; ${fmt(c.duty)} kW duty + ${fmt(c.standby)} kW standby cooling`);
|
||
info_row("Redundancy", dc_redundancy(c, it.kw));
|
||
}
|
||
|
||
fn meta_chiller() {
|
||
#{ label: "Chiller", category: "Cooling", icon: "⛆", storey: true,
|
||
help: "An air-cooled chiller, dry cooler or adiabatic cooler along local x with two rows of fans on top, on the roof or in the yard. Its tooltip sets all the chillers and towers against the IT load of every storey.",
|
||
props: [
|
||
#{ key: "kind", type: "choice", def: "chiller", choices: ["chiller", "dry cooler", "adiabatic"], label: "Type" },
|
||
#{ key: "w", type: "float", def: 9.0, min: 3.0, max: 20.0, step: 0.5, label: "Length (m)" },
|
||
#{ key: "kw", type: "float", def: 1000.0, min: 50.0, max: 5000.0, step: 50.0, label: "Cooling (kW)" },
|
||
#{ key: "running", type: "bool", def: true, label: "Fans running" },
|
||
] }
|
||
}
|
||
|
||
fn prefab_chiller(o) {
|
||
if dc_skip(o) { return; }
|
||
let p = o.p;
|
||
let W = num(p.w);
|
||
let w = W / 2.0;
|
||
let d = 1.15;
|
||
let h = 2.4;
|
||
let s = sprite();
|
||
s.box(-w, -d, w, d, 0.0, 0.15, "steel");
|
||
s.box(-w, -d + 0.08, w, d - 0.08, 0.15, h, "chiller");
|
||
let n = fmax(1.0, (W / 1.2).floor()).to_int();
|
||
let coils = [];
|
||
for i in 0..n { coils.push([i * W / n + 0.06, (i + 1) * W / n - 0.06, 0.45, h - 0.15]); }
|
||
let sd = sides(-w, -d + 0.08, w, d - 0.08);
|
||
let cc = if p.kind == "adiabatic" { "dcpad" } else { "dccoil" };
|
||
s.open(sd.S[0], sd.S[1], coils, cc);
|
||
s.open(sd.N[0], sd.N[1], coils, cc);
|
||
s.open(sd.W[0], sd.W[1], [[0.25, 1.1, 0.5, 1.9]], "panel");
|
||
s.open(sd.E[0], sd.E[1], [[0.3, 2.0 * d - 0.46, 0.4, h - 0.2]], "grille");
|
||
for i in 0..n {
|
||
let x = -w + (i + 0.5) * W / n;
|
||
for y in [-0.52, 0.52] {
|
||
if p.running { s.fan(x, y, h + 0.005, 0.42); } else { s.disc(x, y, h + 0.005, 0.42, "fanring"); }
|
||
}
|
||
}
|
||
let all = dc_it(-1);
|
||
let pl = dc_plant();
|
||
info_row("Type", if p.kind == "dry cooler" { "dry cooler (free cooling)" } else if p.kind == "adiabatic" { "adiabatic cooler (wetted pads)" } else { "air-cooled chiller" });
|
||
info_row("Cooling", `${fmt(num(p.kw))} kW, ${2 * n} fans${if p.running { "" } else { " (stopped)" }}`);
|
||
info_row("Plant", `${pl.units} units, ${fmt(pl.kw)} kW for ${fmt(all.kw)} kW IT (${dc_redundancy(#{ duty: pl.kw, standby: 0.0, units: pl.units }, all.kw)})`);
|
||
}
|
||
|
||
fn meta_cooling_tower() {
|
||
#{ label: "Cooling tower", category: "Cooling", icon: "♨", storey: true,
|
||
help: "Open evaporative cooling tower cells along local x (4 m each) with a fan stack on each and a plume when running.",
|
||
props: [
|
||
#{ key: "cells", type: "int", def: 2, min: 1, max: 6, step: 1, label: "Cells" },
|
||
#{ key: "kw", type: "float", def: 2000.0, min: 100.0, max: 10000.0, step: 100.0, label: "kW per cell" },
|
||
#{ key: "running", type: "bool", def: true, label: "Running (plume)" },
|
||
] }
|
||
}
|
||
|
||
fn prefab_cooling_tower(o) {
|
||
if dc_skip(o) { return; }
|
||
let p = o.p;
|
||
let n = p.cells;
|
||
let c = 4.0;
|
||
let h = 3.6;
|
||
let x0 = -n * c / 2.0;
|
||
let s = sprite();
|
||
let items = [];
|
||
for i in 0..n {
|
||
let a = x0 + i * c;
|
||
let b = a + c;
|
||
let x = (a + b) / 2.0;
|
||
items.push([x, 0.0, |q| {
|
||
q.box(a + 0.02, -c / 2.0, b - 0.02, c / 2.0, 0.0, h, "dctower", #{ top: "dctowert" });
|
||
let sd = sides(a + 0.02, -c / 2.0, b - 0.02, c / 2.0);
|
||
for f in ["S", "N"] { q.open(sd[f][0], sd[f][1], [[0.2, c - 0.24, 0.3, 1.5]], "louvre"); }
|
||
for f in ["E", "W"] { q.open(sd[f][0], sd[f][1], [[0.2, c - 0.2, 0.3, 1.5]], "louvre"); }
|
||
q.cyl(x, 0.0, 1.55, h, h + 1.0, "dctower", #{ no_top: true });
|
||
if p.running { q.fan(x, 0.0, h + 0.95, 1.35); } else { q.disc(x, 0.0, h + 0.95, 1.35, "fanring"); }
|
||
}]);
|
||
}
|
||
s.seq(items);
|
||
if p.running {
|
||
for i in 0..n { s.puffs(x0 + (i + 0.5) * c, 0.0, h + 1.3, "puff2", 3, 4.0); }
|
||
}
|
||
let all = dc_it(-1);
|
||
info_row("Cells", `${n} × ${fmt(num(p.kw))} kW = ${fmt(n * num(p.kw))} kW`);
|
||
if all.racks > 0 { info_row("Site IT load", `${fmt(all.kw)} kW`); }
|
||
}
|
||
|
||
fn meta_pipe_run() {
|
||
#{ label: "Pipe run", category: "Cooling", icon: "═", shape: "line", storey: true,
|
||
help: "Flow and return pipes along local x (drag to draw) on stands or overhead: chilled water (blue / red), condenser water (green) or glycol (purple). Height of the pipe centre above the object.",
|
||
props: [
|
||
#{ key: "w", type: "float", def: 10.0, min: 0.5, max: 60.0, step: 0.1, label: "Length (m)" },
|
||
#{ key: "kind", type: "choice", def: "chilled water", choices: ["chilled water", "condenser", "glycol"], label: "Medium" },
|
||
#{ key: "dn", type: "float", def: 0.25, min: 0.05, max: 0.8, step: 0.05, label: "Diameter (m)" },
|
||
#{ key: "h", type: "float", def: 0.8, min: 0.2, max: 8.0, step: 0.05, label: "Height (m)" },
|
||
#{ key: "pair", type: "bool", def: true, label: "Flow and return" },
|
||
#{ key: "stands", type: "bool", def: true, label: "Stands" },
|
||
] }
|
||
}
|
||
|
||
fn prefab_pipe_run(o) {
|
||
if dc_skip(o) { return; }
|
||
let p = o.p;
|
||
let w = num(p.w) / 2.0;
|
||
let r = num(p.dn) / 2.0;
|
||
let z = num(p.h);
|
||
let off = if p.pair { r + 0.06 } else { 0.0 };
|
||
let mats = if p.kind == "condenser" { ["dcpipeg", "dcpipeg2"] } else if p.kind == "glycol" { ["dcpipev", "dcpipev"] } else { ["pipec", "pipes"] };
|
||
// the -y pipe is the flow, +y the return; the far one first
|
||
let ys = if p.pair { if vis(0.0, 1.0) { [[-off, mats[0]], [off, mats[1]]] } else { [[off, mats[1]], [-off, mats[0]]] } } else { [[0.0, mats[0]]] };
|
||
let pieces = (num(p.w) / 15.0).ceiling().to_int();
|
||
for i in 0..pieces {
|
||
let x0 = -w + num(p.w) * i / pieces;
|
||
let x1 = -w + num(p.w) * (i + 1) / pieces;
|
||
let s = sprite();
|
||
if p.stands && z - r > 0.25 {
|
||
let items = [];
|
||
let x = x0 + 0.6;
|
||
while x < x1 - 0.2 {
|
||
let c = x;
|
||
items.push([c, 0.0, |q| {
|
||
q.box(c - 0.04, -0.04, c + 0.04, 0.04, 0.0, z - r - 0.05, "steel");
|
||
q.box(c - 0.05, -off - r, c + 0.05, off + r, z - r - 0.05, z - r, "steel");
|
||
}]);
|
||
x += 3.0;
|
||
}
|
||
s.seq(items);
|
||
}
|
||
// faces edged in their own shade, so a pipe reads as one surface
|
||
for q in ys { s.tube([x0, q[0], z], [x1, q[0], z], r, r, 10, q[1] + " beast"); }
|
||
}
|
||
let what = if p.kind == "condenser" { "condenser water" } else if p.kind == "glycol" { "glycol" } else { "chilled water" };
|
||
info_row("Pipes", `${what}, ${if p.pair { "flow and return" } else { "one pipe" }}, DN ${fmt(num(p.dn) * 1000.0)}`);
|
||
info_row("Run", `${dc_num(num(p.w))} m at ${dc_num(z)} m`);
|
||
}
|
||
|
||
// ---------------------------------------------------------------- fire safety
|
||
|
||
fn meta_smoke_detectors() {
|
||
#{ label: "Detectors", category: "Fire safety", icon: "◉", shape: "rect", storey: true,
|
||
help: "Point detectors on a grid over the rectangle at ceiling height, with their coverage circles. Data halls with high air change rates need a closer spacing than the usual 7.5 m radius.",
|
||
props: [
|
||
#{ key: "w", type: "float", def: 18.0, min: 1.0, max: 200.0, step: 0.5, label: "Width (m)" },
|
||
#{ key: "d", type: "float", def: 12.0, min: 1.0, max: 200.0, step: 0.5, label: "Depth (m)" },
|
||
#{ key: "spacing", type: "float", def: 6.0, min: 2.0, max: 15.0, step: 0.5, label: "Spacing (m)" },
|
||
#{ key: "h", type: "float", def: 4.5, min: 2.0, max: 12.0, step: 0.05, label: "Height (m)" },
|
||
#{ key: "kind", type: "choice", def: "smoke", choices: ["smoke", "heat", "multi-sensor"], label: "Kind" },
|
||
#{ key: "coverage", type: "bool", def: true, label: "Coverage circles" },
|
||
#{ key: "zone", type: "text", def: "", label: "Zone" },
|
||
] }
|
||
}
|
||
|
||
fn prefab_smoke_detectors(o) {
|
||
if dc_skip(o) || dc_no_ceiling() { return; }
|
||
let p = o.p;
|
||
let W = num(p.w);
|
||
let D = num(p.d);
|
||
let sp = num(p.spacing);
|
||
let h = num(p.h);
|
||
let nx = fmax(1.0, (W / sp).ceiling()).to_int();
|
||
let ny = fmax(1.0, (D / sp).ceiling()).to_int();
|
||
let r = (W / nx) * (W / nx) / 4.0 + (D / ny) * (D / ny) / 4.0;
|
||
r = r.sqrt();
|
||
dc_ceiling_layer();
|
||
let s = sprite();
|
||
let pts = [];
|
||
for i in 0..nx {
|
||
for j in 0..ny { pts.push([-W / 2.0 + (i + 0.5) * W / nx, -D / 2.0 + (j + 0.5) * D / ny]); }
|
||
}
|
||
if p.coverage {
|
||
for q in pts { s.disc(q[0], q[1], h - 0.06, r, "dccov"); }
|
||
}
|
||
// the detectors seen from above (the ceiling is not drawn): white discs
|
||
let m = if p.kind == "heat" { "dcdethd" } else { "dcdetd" };
|
||
for q in pts { s.disc(q[0], q[1], h - 0.04, 0.11, m); }
|
||
s.begin("a dcflash", `animation-delay:-${round1(hash(o.x + o.y) * 4.0)}s`);
|
||
for q in pts { s.circle(q[0], q[1], h - 0.075, 0.28, "dcdled"); }
|
||
s.end();
|
||
s.ext_reset();
|
||
s.ext_box(-W / 2.0, -D / 2.0, W / 2.0, D / 2.0, h - 0.08, h);
|
||
let n = nx * ny;
|
||
let z = if p.zone == "" { "" } else { `${p.zone}: ` };
|
||
info_row("Detectors", `${z}${n} ${p.kind} detectors, ${nx} × ${ny} at ${dc_num(W / nx)} × ${dc_num(D / ny)} m`);
|
||
info_row("Coverage", `${dc_num(W * D / n)} m² each, radius ${dc_num(r)} m to the farthest corner`);
|
||
}
|
||
|
||
fn meta_fire_pipe() {
|
||
#{ label: "Fire pipe", category: "Fire safety", icon: "┅", shape: "line", storey: true,
|
||
help: "A pipe at ceiling height along local x: VESDA aspirating smoke detection (sampling holes, the detector unit at the start, down the wall), pre-action sprinkler heads, or clean-agent gas discharge nozzles. Height above the storey's slab.",
|
||
props: [
|
||
#{ key: "w", type: "float", def: 12.0, min: 0.5, max: 100.0, step: 0.1, label: "Length (m)" },
|
||
#{ key: "kind", type: "choice", def: "VESDA", choices: ["VESDA", "sprinkler", "gas"], label: "Kind" },
|
||
#{ key: "spacing", type: "float", def: 4.0, min: 0.5, max: 12.0, step: 0.1, label: "Holes / heads every (m)" },
|
||
#{ key: "h", type: "float", def: 4.3, min: 2.0, max: 12.0, step: 0.05, label: "Height (m)" },
|
||
#{ key: "unit", type: "bool", def: true, label: "Detector unit (VESDA)" },
|
||
] }
|
||
}
|
||
|
||
fn prefab_fire_pipe(o) {
|
||
if dc_skip(o) || dc_no_ceiling() { return; }
|
||
let p = o.p;
|
||
let w = num(p.w) / 2.0;
|
||
let h = num(p.h);
|
||
let sp = num(p.spacing);
|
||
let k = p.kind;
|
||
let cls = if k == "gas" { "dcgasl" } else if k == "sprinkler" { "dcsprl" } else { "dcvesl" };
|
||
let n = fmax(1.0, (num(p.w) / sp).floor()).to_int();
|
||
let xs = [];
|
||
for i in 0..n { xs.push(-w + (i + 0.5) * num(p.w) / n); }
|
||
dc_ceiling_layer();
|
||
let s = sprite();
|
||
s.polyline([[[-w, 0.0, h], [w, 0.0, h]]], 0.0, cls);
|
||
if k == "VESDA" {
|
||
for x in xs { s.circle(x, 0.0, h - 0.02, 0.4, "dchole"); }
|
||
} else if k == "sprinkler" {
|
||
for x in xs { s.stem(x, 0.0, h - 0.12, h, 0.12, "dcspr"); s.circle(x, 0.0, h - 0.13, 0.42, "dcsprh"); }
|
||
} else {
|
||
let items = [];
|
||
for x in xs { let c = x; items.push([c, 0.0, |q| q.cyl(c, 0.0, 0.05, h - 0.14, h, "dcgaspipe")]); }
|
||
s.seq(items);
|
||
}
|
||
// a tight sort box (VESDA holes sit on the pipe, heads and nozzles hang below)
|
||
s.ext_reset();
|
||
s.ext_box(-w, -0.05, w, 0.05, if k == "VESDA" { h - 0.03 } else { h - 0.15 }, h + 0.01);
|
||
if k == "VESDA" && p.unit {
|
||
// the detector unit at the start, at eye height, the pipe dropping to it
|
||
let z0 = dc_lift_at(o, world_of(o, -w, 0.0)[0], world_of(o, -w, 0.0)[1]);
|
||
layer("main");
|
||
let u = sprite();
|
||
u.polyline([[[-w, 0.0, h], [-w, 0.0, z0 + 1.75]]], 0.0, cls);
|
||
u.box(-w - 0.06, -0.18, -w + 0.06, 0.18, z0 + 1.3, z0 + 1.75, "dcvesda", #{ top: "dcvesda" });
|
||
u.ext_box(-w - 0.06, -0.18, -w + 0.06, 0.18, z0 + 1.3, h);
|
||
}
|
||
let what = if k == "gas" { "gas discharge pipe" } else if k == "sprinkler" { "pre-action sprinkler pipe" } else { "VESDA sampling pipe" };
|
||
let heads = if k == "gas" { "nozzles" } else if k == "sprinkler" { "heads" } else { "sampling holes" };
|
||
info_row("Pipe", `${what}, ${dc_num(num(p.w))} m at ${dc_num(h)} m`);
|
||
info_row(if k == "VESDA" { "Holes" } else { "Heads" }, `${n} ${heads}, ${dc_num(num(p.w) / n)} m apart`);
|
||
if k == "VESDA" { info_row("Transport", `about ${fmt(num(p.w) / 1.5 + 5.0)} s from the far hole (aim for < 60 s)`); }
|
||
}
|
||
|
||
/// Clean agent needed for V m³ at t °C: #{ need, per (one cylinder), unit, c }.
|
||
fn dc_agent(agent, V, t, litres) {
|
||
if agent == "FM-200" {
|
||
let S = 0.1269 + 0.000513 * t;
|
||
let c = 7.0;
|
||
#{ need: V / S * c / (100.0 - c), per: litres * 1.0, unit: "kg", c: c, fill: "1.0 kg/l" }
|
||
} else if agent == "Inergen" {
|
||
let S = 0.65799 + 0.00239 * t;
|
||
let c = 39.9;
|
||
#{ need: 2.303 * (0.707 / S) * (100.0 / (100.0 - c)).ln() / 2.302585 * V, per: litres * 0.295, unit: "m³", c: c, fill: "300 bar" }
|
||
} else {
|
||
let S = 0.0664 + 0.0002741 * t;
|
||
let c = 4.5;
|
||
#{ need: V / S * c / (100.0 - c), per: litres * 1.2, unit: "kg", c: c, fill: "1.2 kg/l" }
|
||
}
|
||
}
|
||
|
||
/// Volume a gas bank on storey s protects: its halls (raised floors, void and
|
||
/// ceiling included) or, without any, the dc_building it stands in.
|
||
fn dc_gas_volume(o) {
|
||
let s = storey_of(o.z);
|
||
let H = storey_h() - 0.3;
|
||
let v = 0.0;
|
||
for f in objects_of(["raised_floor"]) {
|
||
if storey_of(f.z) == s { v += num(f.p.w) * num(f.p.d) * H; }
|
||
}
|
||
if v > 0.0 { return v; }
|
||
for b in objects_at(["dc_building"], o.x, o.y) {
|
||
v = (num(b.p.w) - 2.0 * num(b.p.t)) * (num(b.p.d) - 2.0 * num(b.p.t)) * H;
|
||
}
|
||
v
|
||
}
|
||
|
||
fn meta_gas_bank() {
|
||
#{ label: "Gas cylinders", category: "Fire safety", icon: "⛽", storey: true,
|
||
help: "A bank of clean-agent cylinders along local x, backs to +y (against a wall), with the manifold and release heads. It sizes the agent for the protected volume (the raised-floor halls on its storey, void and ceiling included, unless Volume is set) and warns when the bank is short.",
|
||
props: [
|
||
#{ key: "agent", type: "choice", def: "Novec 1230", choices: ["Novec 1230", "FM-200", "Inergen"], label: "Agent" },
|
||
#{ key: "n", type: "int", def: 8, min: 1, max: 40, step: 1, label: "Cylinders" },
|
||
#{ key: "litres", type: "int", def: 140, min: 40, max: 180, step: 10, label: "Cylinder (l)" },
|
||
#{ key: "vol", type: "float", def: 0.0, min: 0.0, max: 200000.0, step: 50.0, label: "Volume (m³, 0: auto)" },
|
||
#{ key: "temp", type: "float", def: 20.0, min: 0.0, max: 40.0, step: 1.0, label: "Design °C" },
|
||
] }
|
||
}
|
||
|
||
fn prefab_gas_bank(o) {
|
||
if dc_skip(o) { return; }
|
||
let p = o.p;
|
||
let z0 = dc_lift(o);
|
||
let r = 0.13 + (num(p.litres) - 80.0) * 0.0005;
|
||
let ch = 1.0 + num(p.litres) * 0.0045;
|
||
let pitch = 2.0 * r + 0.08;
|
||
let n = p.n;
|
||
let B = n * pitch / 2.0;
|
||
let s = sprite();
|
||
// the wall bracket behind, then the cylinders back to front, the manifold over them
|
||
s.box(-B, r + 0.02, B, r + 0.07, z0 + ch * 0.55, z0 + ch * 0.62, "steel");
|
||
let items = [];
|
||
let shoulder = if p.agent == "Inergen" { "dcgreen" } else { "firetank" };
|
||
for i in 0..n {
|
||
let x = -B + (i + 0.5) * pitch;
|
||
items.push([x, 0.0, |q| {
|
||
q.cyl(x, 0.0, r, z0, z0 + ch - r * 0.6, "firetank", #{ no_top: true });
|
||
q.frust(x, 0.0, r, r * 0.35, z0 + ch - r * 0.6, z0 + ch, shoulder, 12);
|
||
q.box(x - 0.06, -0.06, x + 0.06, 0.06, z0 + ch, z0 + ch + 0.1, "keyb");
|
||
}]);
|
||
}
|
||
s.seq(items);
|
||
let zm = z0 + ch + 0.25;
|
||
s.tube([-B, 0.0, zm], [B, 0.0, zm], 0.05, 0.05, 8, "dcgaspipe beast");
|
||
s.tube([B, 0.0, zm], [B, 0.0, zm + 1.0], 0.05, 0.05, 8, "dcgaspipe beast");
|
||
let V = if num(p.vol) > 0.0 { num(p.vol) } else { dc_gas_volume(o) };
|
||
let a = dc_agent(p.agent, V, num(p.temp), num(p.litres));
|
||
let have = n * a.per;
|
||
info_row("Agent", `${p.agent}, ${dc_num(a.c)} % design concentration at ${dc_num(num(p.temp))} °C`);
|
||
info_row("Volume", `${fmt(V)} m³${if num(p.vol) > 0.0 { "" } else { " (halls on this storey, void and ceiling included)" }}`);
|
||
info_row("Needed", `${fmt(a.need)} ${a.unit}`);
|
||
info_row("Installed", `${n} × ${p.litres} l = ${fmt(have)} ${a.unit} (${a.fill})`);
|
||
if V <= 0.0 {
|
||
info_row("Status", "no volume: set one or put raised floors on this storey");
|
||
} else if have + 1e-6 < a.need {
|
||
let more = ((a.need - have) / a.per).ceiling().to_int();
|
||
info_row("Status", `short: ${more} more cylinder${if more == 1 { "" } else { "s" }}`);
|
||
tag(0.0, 0.0, z0 + ch + 0.6, `Gas: ${more} cylinder${if more == 1 { "" } else { "s" }} short`, 9, o.id, "warntag");
|
||
} else {
|
||
info_row("Status", `enough, ${fmt((have / a.need - 1.0) * 100.0)} % spare`);
|
||
}
|
||
}
|
||
|
||
fn dc_device_kinds() { ["alarm panel", "release panel", "call point", "abort switch", "beacon", "extinguisher"] }
|
||
|
||
fn meta_fire_device() {
|
||
#{ label: "Fire device", category: "Fire safety", icon: "🔔", storey: true,
|
||
help: "A fire alarm panel, extinguishant release panel, manual call point, gas abort switch, sounder-beacon or extinguisher, its back to local +y (against a wall).",
|
||
props: [
|
||
#{ key: "kind", type: "choice", def: "call point", choices: dc_device_kinds(), label: "Kind" },
|
||
#{ key: "label", type: "text", def: "", label: "Zone / label" },
|
||
] }
|
||
}
|
||
|
||
fn prefab_fire_device(o) {
|
||
if dc_skip(o) { return; }
|
||
let p = o.p;
|
||
let z0 = dc_lift(o);
|
||
let k = p.kind;
|
||
// a wall device turned away from the viewer on the storey cut open hangs
|
||
// on the far side of a wall that is cut low: ghost it
|
||
let s = if k != "extinguisher" && house_view(o) == 1 && !vis(0.0, -1.0) { sprite(o.id, "ghost") } else { sprite() };
|
||
if k == "alarm panel" {
|
||
s.box(-0.3, 0.02, 0.3, 0.17, z0 + 1.1, z0 + 1.9, "dcpanel");
|
||
let sd = sides(-0.3, 0.02, 0.3, 0.17);
|
||
s.open(sd.S[0], sd.S[1], [[0.12, 0.48, z0 + 1.55, z0 + 1.78]], "dcscr");
|
||
s.open(sd.S[0], sd.S[1], [[0.12, 0.48, z0 + 1.2, z0 + 1.45]], "seam");
|
||
if vis(0.0, -1.0) { s.circle(-0.22, 0.01, z0 + 1.5, 0.3, "ledg"); s.circle(-0.22, 0.01, z0 + 1.4, 0.3, "ledo"); }
|
||
} else if k == "release panel" {
|
||
s.box(-0.22, 0.04, 0.22, 0.17, z0 + 1.2, z0 + 1.75, "dcswg");
|
||
let sd = sides(-0.22, 0.04, 0.22, 0.17);
|
||
s.open(sd.S[0], sd.S[1], [[0.04, 0.4, z0 + 1.6, z0 + 1.7]], "dcyel");
|
||
s.open(sd.S[0], sd.S[1], [[0.08, 0.36, z0 + 1.35, z0 + 1.52]], "dcscr");
|
||
if vis(0.0, -1.0) { s.circle(0.12, 0.03, z0 + 1.28, 0.3, "ledr"); }
|
||
} else if k == "call point" || k == "abort switch" {
|
||
let m = if k == "call point" { "dcpanel" } else { "dcyel" };
|
||
s.box(-0.07, 0.11, 0.07, 0.17, z0 + 1.33, z0 + 1.47, m);
|
||
let sd = sides(-0.07, 0.11, 0.07, 0.17);
|
||
s.open(sd.S[0], sd.S[1], [[0.03, 0.11, z0 + 1.36, z0 + 1.44]], if k == "call point" { "dcwhite" } else { "dcpanel" });
|
||
} else if k == "beacon" {
|
||
// the dome stays in front of the wall face at +y
|
||
s.box(-0.07, 0.07, 0.07, 0.17, z0 + 2.3, z0 + 2.4, "dcpanel");
|
||
s.frust(0.0, 0.12, 0.05, 0.03, z0 + 2.4, z0 + 2.52, "dcpanel", 10);
|
||
s.circle(0.0, 0.12, z0 + 2.47, 0.55, "ledr");
|
||
} else {
|
||
s.cyl(0.0, 0.05, 0.09, z0, z0 + 0.62, "firetank");
|
||
s.box(-0.03, 0.02, 0.03, 0.08, z0 + 0.62, z0 + 0.72, "keyb");
|
||
s.box(0.03, 0.03, 0.16, 0.07, z0 + 0.6, z0 + 0.66, "keyb");
|
||
}
|
||
let what = if k == "alarm panel" { "fire alarm control panel" } else if k == "release panel" { "extinguishant release control panel" } else if k == "call point" { "manual call point" } else if k == "abort switch" { "gas release abort switch (hold to delay)" } else if k == "beacon" { "sounder and flashing beacon" } else { "CO2 extinguisher, 5 kg" };
|
||
info_row("Device", what);
|
||
if p.label != "" { info_row("Zone", p.label); }
|
||
}
|
||
|
||
// ---------------------------------------------------------------- power
|
||
|
||
fn meta_power_cab() {
|
||
#{ label: "Power cabinets", category: "Power", icon: "🔋", storey: true,
|
||
help: "A line-up of UPS, battery, PDU, switchgear or remote power panel cabinets along local x, fronts to local -y. UPS and battery tooltips set rating and runtime against the IT load of the building (every rack row and pod).",
|
||
props: [
|
||
#{ key: "kind", type: "choice", def: "UPS", choices: ["UPS", "battery", "PDU", "switchgear", "RPP"], label: "Kind" },
|
||
#{ key: "n", type: "int", def: 4, min: 1, max: 30, step: 1, label: "Cabinets" },
|
||
#{ key: "kw", type: "float", def: 250.0, min: 0.0, max: 5000.0, step: 10.0, label: "kW / kVA each" },
|
||
#{ key: "kwh", type: "float", def: 40.0, min: 0.0, max: 1000.0, step: 5.0, label: "kWh (battery)" },
|
||
] }
|
||
}
|
||
|
||
fn prefab_power_cab(o) {
|
||
if dc_skip(o) { return; }
|
||
let p = o.p;
|
||
let k = p.kind;
|
||
let z0 = dc_lift(o);
|
||
let cw = if k == "battery" || k == "RPP" { 0.6 } else { 0.8 };
|
||
let cd = if k == "switchgear" { 1.0 } else if k == "RPP" { 0.5 } else if k == "UPS" { 0.9 } else { 0.8 };
|
||
let h = if k == "switchgear" { 2.2 } else if k == "PDU" { 1.9 } else { 2.0 };
|
||
let m = if k == "UPS" { "dcups" } else if k == "battery" { "dcbatt" } else if k == "PDU" || k == "RPP" { "dcpdu" } else { "dcswg" };
|
||
let n = p.n;
|
||
let B = n * cw / 2.0;
|
||
let d = cd / 2.0;
|
||
let s = sprite();
|
||
let items = [];
|
||
for i in 0..n {
|
||
let a = -B + i * cw;
|
||
let b = a + cw;
|
||
items.push([(a + b) / 2.0, 0.0, |q| q.box(a, -d, b, d, z0, z0 + h, m)]);
|
||
}
|
||
s.seq(items);
|
||
let F = dc_xface(B, -d, -1.0);
|
||
let lists = #{ seam: [], grille: [], dcscr: [], dcpanelb: [], dcbrk: [] };
|
||
for i in 0..n {
|
||
let a = -B + i * cw;
|
||
let b = a + cw;
|
||
lists.seam.push(dc_span(F, B, a + 0.03, b - 0.03, z0 + 0.05, z0 + h - 0.05));
|
||
if k == "UPS" {
|
||
lists.dcscr.push(dc_span(F, B, a + 0.2, b - 0.2, z0 + 1.45, z0 + 1.65));
|
||
lists.grille.push(dc_span(F, B, a + 0.1, b - 0.1, z0 + 0.15, z0 + 0.6));
|
||
} else if k == "battery" {
|
||
for j in 0..6 { lists.grille.push(dc_span(F, B, a + 0.08, b - 0.08, z0 + 0.2 + j * 0.28, z0 + 0.32 + j * 0.28)); }
|
||
} else if k == "PDU" {
|
||
lists.grille.push(dc_span(F, B, a + 0.1, b - 0.1, z0 + 0.15, z0 + 0.9));
|
||
lists.dcscr.push(dc_span(F, B, a + 0.25, b - 0.25, z0 + 1.35, z0 + 1.5));
|
||
} else if k == "RPP" {
|
||
lists.dcpanelb.push(dc_span(F, B, a + 0.08, b - 0.08, z0 + 0.5, z0 + 1.7));
|
||
for j in 0..10 { lists.dcbrk.push(dc_span(F, B, a + 0.12, b - 0.12, z0 + 0.58 + j * 0.11, z0 + 0.62 + j * 0.11)); }
|
||
} else {
|
||
for j in 0..3 { lists.dcpanelb.push(dc_span(F, B, a + 0.1, b - 0.1, z0 + 0.2 + j * 0.65, z0 + 0.75 + j * 0.65)); }
|
||
}
|
||
}
|
||
for c in ["seam", "dcpanelb", "dcbrk", "grille", "dcscr"] {
|
||
if lists[c].len() > 0 { s.open(F.a, F.b, lists[c], c); }
|
||
}
|
||
if vis(0.0, -1.0) && k != "battery" {
|
||
for i in 0..n { s.circle(-B + (i + 0.5) * cw + 0.2, -d - 0.01, z0 + h - 0.15, 0.3, "ledg"); }
|
||
}
|
||
let all = dc_it(-1);
|
||
let tot = n * num(p.kw);
|
||
if k == "UPS" {
|
||
info_row("UPS", `${n} × ${fmt(num(p.kw))} kW = ${fmt(tot)} kW`);
|
||
if all.kw > 0.0 {
|
||
info_row("Load", `${fmt(all.kw)} kW IT = ${fmt(all.kw / tot * 100.0)} % of the line-up`);
|
||
info_row("Redundancy", dc_redundancy(#{ duty: tot, standby: 0.0, units: n }, all.kw));
|
||
}
|
||
} else if k == "battery" {
|
||
// the strings work together: runtime from every battery cabinet on the site
|
||
let e = n * num(p.kwh);
|
||
let te = 0.0;
|
||
let tn = 0;
|
||
for b in objects_of(["power_cab"]) {
|
||
if b.p.kind == "battery" { te += b.p.n * num(b.p.kwh); tn += b.p.n; }
|
||
}
|
||
info_row("Batteries", `${n} × ${fmt(num(p.kwh))} kWh = ${fmt(e)} kWh`);
|
||
if tn > n { info_row("All strings", `${tn} cabinets, ${fmt(te)} kWh`); }
|
||
if all.kw > 0.0 { info_row("Runtime", `about ${dc_num(te * 60.0 * 0.9 / all.kw)} min at ${fmt(all.kw)} kW IT`); }
|
||
} else if k == "PDU" {
|
||
info_row("PDUs", `${n} × ${fmt(num(p.kw))} kVA = ${fmt(tot)} kVA`);
|
||
let it = dc_it(storey_of(o.z));
|
||
if it.kw > 0.0 { info_row("This storey", `${fmt(it.kw)} kW IT in ${it.racks} racks`); }
|
||
} else if k == "RPP" {
|
||
info_row("Power panels", `${n} remote power panels, ${n * 42} ways`);
|
||
} else {
|
||
info_row("Switchgear", `${n} panels`);
|
||
}
|
||
}
|
||
|
||
fn meta_transformer() {
|
||
#{ label: "Transformer", category: "Power", icon: "⚡",
|
||
help: "A distribution transformer on a plinth: oil-filled with radiators and bushings, or a dry type in a louvred enclosure.",
|
||
props: [
|
||
#{ key: "kva", type: "int", def: 2500, min: 100, max: 10000, step: 100, label: "Rating (kVA)" },
|
||
#{ key: "kind", type: "choice", def: "oil", choices: ["oil", "dry"], label: "Type" },
|
||
] }
|
||
}
|
||
|
||
fn prefab_transformer(o) {
|
||
if dc_skip(o) { return; }
|
||
let p = o.p;
|
||
let s = sprite();
|
||
s.box(-1.6, -1.3, 1.6, 1.3, 0.0, 0.2, "plinth");
|
||
if p.kind == "dry" {
|
||
s.box(-1.3, -1.0, 1.3, 1.0, 0.2, 2.4, "trafo");
|
||
let sd = sides(-1.3, -1.0, 1.3, 1.0);
|
||
for f in ["S", "N"] { s.open(sd[f][0], sd[f][1], [[0.2, 1.2, 0.5, 2.1], [1.4, 2.4, 0.5, 2.1]], "louvre"); }
|
||
for f in ["E", "W"] { s.open(sd[f][0], sd[f][1], [[0.2, 1.8, 1.4, 2.1]], "louvre"); }
|
||
} else {
|
||
// radiators either side of the tank, the far one first
|
||
let ys = if vis(0.0, 1.0) { [-1.0, 1.0] } else { [1.0, -1.0] };
|
||
let fins = [];
|
||
for i in 0..9 { fins.push([0.08 + i * 0.2, 0.16 + i * 0.2, 0.45, 1.75]); }
|
||
for y in ys {
|
||
let y0 = if y < 0.0 { -1.2 } else { 0.7 };
|
||
if y == ys[1] { s.box(-0.95, -0.7, 0.95, 0.7, 0.2, 2.0, "trafo"); }
|
||
s.box(-0.9, y0, 0.9, y0 + 0.5, 0.4, 1.8, "trafo");
|
||
let sd = sides(-0.9, y0, 0.9, y0 + 0.5);
|
||
s.open(sd.S[0], sd.S[1], fins, "seam");
|
||
s.open(sd.N[0], sd.N[1], fins, "seam");
|
||
}
|
||
let items = [];
|
||
for x in [-0.5, 0.0, 0.5] { let c = x; items.push([c, -0.3, |q| q.cyl(c, -0.3, 0.07, 2.0, 2.5, "bushing")]); }
|
||
for x in [-0.6, -0.2, 0.2, 0.6] { let c = x; items.push([c, 0.35, |q| q.cyl(c, 0.35, 0.05, 2.0, 2.25, "bushing")]); }
|
||
s.seq(items);
|
||
}
|
||
info_row("Transformer", `${fmt(p.kva)} kVA, ${if p.kind == "dry" { "dry type (cast resin)" } else { "oil-filled (ONAN)" }}`);
|
||
info_row("Carries", `about ${fmt(p.kva * 0.9 * 0.8)} kW at 0.9 pf and 80 % loading`);
|
||
}
|