Data centres: multi-storey shell, racks and pods, cooling, fire safety, power
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.
This commit is contained in:
grabowski committed 2026-10-27 17:08:39 +07:00
1 parent c8becc2e59
commit 2810477f1b
22 files changed
+2281 -18

No files matched your search

+10
View File
@@ -59,6 +59,10 @@ pub struct Rendered {
pub nodes: usize,
/// Depth-sort cycles that had to be broken (0 is ideal).
pub cycles: usize,
/// Where they were broken: "a before b, c" (sprite ids; a was drawn
/// before b and c, which it should have followed).
#[serde(skip_serializing_if = "Vec::is_empty")]
pub cycle_at: Vec<String>,
pub warnings: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub order: Vec<OrderItem>,
@@ -714,6 +718,7 @@ pub fn render(scene: &Scene, rot: u8, opts: &RenderOpts) -> Rendered {
let mut frame = [1e9f64, 1e9, -1e9, -1e9];
let mut sprites = 0;
let mut cycles = 0;
let mut cycle_at = vec![];
let mut order = vec![];
let mut warnings = scene.warnings.clone();
@@ -748,6 +753,10 @@ pub fn render(scene: &Scene, rot: u8, opts: &RenderOpts) -> Rendered {
.collect();
let o = sort::sort_hulls(&pairs, &hulls, layer.opts.zfirst);
cycles += o.cycles;
for (b, before) in &o.broken {
let ids: Vec<&str> = before.iter().map(|&i| live[i].id.as_str()).collect();
cycle_at.push(format!("{} before {}", live[*b].id, ids.join(", ")));
}
o.order
} else {
(0..live.len()).collect()
@@ -876,6 +885,7 @@ pub fn render(scene: &Scene, rot: u8, opts: &RenderOpts) -> Rendered {
sprites,
nodes,
cycles,
cycle_at,
warnings,
order,
html,
+28
View File
@@ -153,6 +153,9 @@ fn rect_of(b: &Bb) -> Hull {
pub struct Order {
pub order: Vec<usize>,
pub cycles: usize,
/// Each broken cycle: the sprite drawn early to break it and the sprites
/// not drawn yet that should have come before it.
pub broken: Vec<(usize, Vec<usize>)>,
}
pub fn sort(boxes: &[(Bb, Sb)], zf: bool) -> Order {
@@ -203,6 +206,7 @@ pub fn sort_hulls(boxes: &[(Bb, Sb)], hulls: &[Option<Hull>], zf: bool) -> Order
let mut out = Order {
order: Vec::with_capacity(n),
cycles: 0,
broken: vec![],
};
for _ in 0..n {
let mut best: Option<usize> = None;
@@ -227,6 +231,12 @@ pub fn sort_hulls(boxes: &[(Bb, Sb)], hulls: &[Option<Hull>], zf: bool) -> Order
}
}
let b = best.expect("n > 0");
if indeg[b] > 0 {
let before: Vec<usize> = (0..n)
.filter(|&i| !done[i] && adj[i].contains(&b))
.collect();
out.broken.push((b, before));
}
done[b] = true;
out.order.push(b);
for &j in &adj[b] {
@@ -245,6 +255,24 @@ mod tests {
([x0, y0, x1, y1, z0, z1], [0.0, 0.0, 10.0, 10.0])
}
#[test]
fn broken_cycles_name_the_sprites() {
// a before b (x), b before c (y), c before a (z): no order fits all three
let o = sort(
&[
bx(0.0, 0.0, 1.0, 10.0, 5.0, 6.0),
bx(2.0, 0.0, 3.0, 1.0, 0.0, 5.0),
bx(0.0, 2.0, 3.0, 3.0, 0.0, 1.0),
],
false,
);
assert_eq!(o.cycles, 1);
assert_eq!(o.broken.len(), 1);
let (b, before) = &o.broken[0];
assert_eq!(before.len(), 1);
assert_ne!(*b, before[0]);
}
#[test]
fn separated_on_x_draws_far_first() {
let o = sort(
+11 -1
View File
@@ -137,7 +137,17 @@ pub fn check(p: &Project) -> Vec<Finding> {
));
}
if v.cycles > 0 {
out.push(f("info", format!("{}/{rot}: {} depth-sort cycle(s) broken; split large sprites if something draws in front of what it stands behind", l.id, v.cycles), None));
let mut at = v
.cycle_at
.iter()
.take(3)
.cloned()
.collect::<Vec<_>>()
.join("; ");
if v.cycle_at.len() > 3 {
at.push_str("; ...");
}
out.push(f("info", format!("{}/{rot}: {} depth-sort cycle(s) broken ({at}); split large sprites if something draws in front of what it stands behind", l.id, v.cycles), None));
}
for cls in v
.html
+4
View File
@@ -201,6 +201,9 @@ pub struct View {
pub sprites: usize,
pub nodes: usize,
pub cycles: usize,
/// Where the depth sort broke its cycles ("a before b, c": sprite ids).
#[serde(skip_serializing_if = "Vec::is_empty")]
pub cycle_at: Vec<String>,
pub warnings: Vec<String>,
/// Object id -> world box [x0, y0, z0, x1, y1, z1].
pub boxes: BTreeMap<String, [f64; 6]>,
@@ -428,6 +431,7 @@ impl Project {
sprites: r.sprites,
nodes: r.nodes,
cycles: r.cycles,
cycle_at: r.cycle_at,
warnings: r.warnings,
boxes: built.boxes,
ms: (t0.elapsed().as_secs_f64() * 1e4).round() / 10.0,
+11 -1
View File
@@ -46,7 +46,17 @@ props.walk none/both/left/right adds raised sidewalks walk_w wide) ending on ano
junction there (at any angle); a road_curve starts at its x, y heading along its rot, bends props.angle degrees \
left or right on radius r and ends where the next road starts; two roads ending at one point make a mitred corner. \
A roundabout (props.r ring radius, arm_e/n/w/s, arm_len) has its arm ends r + arm_len from its centre: put the \
roads' ends exactly there, with the same width as road_d.";
roads' ends exactly there, with the same width as road_d. \
Data centres (categories Data centre, Cooling, Fire safety, Power): a dc_building (w x d, props.storeys) is the shell; \
its storeys are the project parameter storey_h apart (5 m suits data halls) and levels gf, 1f, 2f cut it open. Put \
every part on its storey (z = storey x storey_h) inside the walls (inner faces at w/2 - t and d/2 - t): a \
raised_floor (w x d) per hall, then rack_row (fronts to local -y) and dc_pod objects (two rows round a contained aisle: \
props.contain cold, hot or none; props.inrow adds an in-row cooler every n racks) on it, which stand on its top by \
themselves; crah units along the walls; power_cab line-ups (UPS, battery, PDU, switchgear, RPP). Overhead runs \
(cable_tray, busway, fire_pipe: VESDA, sprinkler or gas) take props.h above the slab: keep crossing runs at different \
heights (busway 3.2, fibre 3.6, ladder 4.0, gas or sprinkler 4.25, VESDA 4.4, smoke_detectors 4.5 for 5 m storeys). \
A gas_bank sizes its agent for the raised-floor halls on its storey; tooltips report IT load, cooling redundancy, \
UPS load and battery runtime. Chillers and cooling towers go on the roof (z = storeys x storey_h) or in the yard.";
fn tool(name: &str, description: &str, props: Value, required: &[&str]) -> Value {
json!({ "name": name, "description": description,
+39
View File
@@ -94,6 +94,45 @@ fn country_demo_renders_every_view_cleanly() {
example_renders_every_view_cleanly("country");
}
#[test]
fn datacentre_demo_renders_every_view_cleanly() {
let p = Project::load(Path::new(env!("CARGO_MANIFEST_DIR")).join("../../examples/datacentre"))
.unwrap();
assert!(p.runtime.is_ok(), "{:?}", p.runtime.as_ref().err());
// storeys of 5 m; the building covers |x| <= 21, |y| <= 13 and has three
// storeys, the plant on its roof (storey 3)
let storey = |z: f64| ((z + 0.5) / 5.0).floor() as i64;
for l in p.manifest.levels() {
let k: i64 = match l.id.as_str() {
"gf" => 0,
"1f" => 1,
"2f" => 2,
_ => -1,
};
for rot in 0..4 {
let v = p.render(&l.id, rot, None, false).unwrap();
assert!(v.warnings.is_empty(), "{}/{rot}: {:?}", l.id, v.warnings);
assert_eq!(v.cycles, 0, "{}/{rot}: {:?}", l.id, v.cycle_at);
assert!(!v.html.contains("NaN"));
for o in p.doc.objects.iter().filter(|o| o.kind != "group") {
let s = storey(o.z);
let ground = o.kind == "site" || o.kind == "area";
let inside = !ground && o.x.abs() < 21.0 && o.y.abs() < 13.0;
// the cut storey shows, closed storeys inside the shell and storeys above do not
let shown = o.kind == "dc_building" || !inside || s == k || (k < 0 && s >= 3);
assert_eq!(
v.boxes.contains_key(&o.id),
shown,
"{} on {}/{rot}",
o.id,
l.id
);
}
}
}
assert!(check::check(&p).is_empty(), "{:?}", check::check(&p));
}
fn example_renders_every_view_cleanly(name: &str) {
let p = Project::load(
Path::new(env!("CARGO_MANIFEST_DIR"))
+4
View File
@@ -19,4 +19,8 @@ pub const MODULES: &[(&str, &str)] = &[
("isokit/house", include_str!("../stdlib/house.rhai")),
("isokit/furniture", include_str!("../stdlib/furniture.rhai")),
("isokit/industry", include_str!("../stdlib/industry.rhai")),
(
"isokit/datacentre",
include_str!("../stdlib/datacentre.rhai"),
),
];
+206 -1
View File
@@ -221,7 +221,8 @@ fn every_library_prefab_renders_cleanly_in_every_rotation() {
let r = Runtime::new(&Bundle::default()).unwrap();
let mut doc = Doc::default();
for (i, m) in r.catalog().iter().enumerate() {
let mut o = obj(&format!("o{i}"), &m.kind, i as f64 * 15.0, 0.0, json!({}));
// 45 m apart: a data centre shell (40 m) leaves out what stands inside it
let mut o = obj(&format!("o{i}"), &m.kind, i as f64 * 45.0, 0.0, json!({}));
// switch every bool prop on to exercise all branches
for p in &m.props {
if p.kind == "bool" {
@@ -1379,3 +1380,207 @@ fn object_queries_and_frames() {
);
assert_eq!(m[4], 1.0, "one opening on the east wall");
}
// ---------------------------------------------------------------- data centres
fn dc_cfg() -> Map<String, Value> {
let mut m = Map::new();
m.insert("storey_h".into(), json!(5.0));
m
}
fn on_storey(mut o: Object, z: f64) -> Object {
o.z = z;
o
}
/// A two-storey shell: power on GF, a hall on 1F.
fn dc_doc() -> Doc {
Doc {
objects: vec![
obj(
"dc",
"dc_building",
0.0,
0.0,
json!({ "w": 30, "d": 20, "storeys": 2 }),
),
obj(
"ups",
"power_cab",
0.0,
0.0,
json!({ "kind": "UPS", "n": 2, "kw": 200 }),
),
obj(
"batt",
"power_cab",
0.0,
-3.0,
json!({ "kind": "battery", "n": 4, "kwh": 50 }),
),
obj("wall", "house_wall", 6.0, 0.0, json!({ "w": 8, "h": 4.7 })),
on_storey(
obj(
"rf",
"raised_floor",
0.0,
0.0,
json!({ "w": 28.8, "d": 19.2 }),
),
5.0,
),
on_storey(
obj("pod", "dc_pod", -4.0, 0.0, json!({ "n": 10, "kw": 8 })),
5.0,
),
on_storey(
obj(
"row",
"rack_row",
6.0,
5.0,
json!({ "n": 6, "kw": 5, "inrow": 3, "inrow_kw": 30 }),
),
5.0,
),
on_storey(obj("crah1", "crah", -8.0, 8.5, json!({ "kw": 100 })), 5.0),
on_storey(obj("crah2", "crah", 0.0, 8.5, json!({ "kw": 100 })), 5.0),
on_storey(
obj(
"crah3",
"crah",
8.0,
8.5,
json!({ "kw": 100, "standby": true }),
),
5.0,
),
on_storey(obj("gas", "gas_bank", -12.0, -8.0, json!({ "n": 4 })), 5.0),
on_storey(
obj(
"det",
"smoke_detectors",
0.0,
0.0,
json!({ "w": 28.8, "d": 19.2, "spacing": 6 }),
),
5.0,
),
on_storey(
obj("tray", "cable_tray", 0.0, -5.0, json!({ "w": 20 })),
5.0,
),
on_storey(obj("tech", "person", 0.0, -5.0, json!({})), 5.0),
],
..Default::default()
}
}
#[test]
fn data_centre_parts_stand_on_the_raised_floor_and_add_up_their_load() {
let r = Runtime::new(&Bundle::default()).unwrap();
let doc = dc_doc();
let b = r.build("1f", 0, &dc_cfg(), &doc).unwrap();
assert!(b.scene.warnings.is_empty(), "{:?}", b.scene.warnings);
let near = |a: f64, e: f64| (a - e).abs() < 1e-6;
// racks, coolers and people stand on the 0.6 m raised floor of their storey
for id in ["pod", "row", "crah1", "tech"] {
assert!(near(b.boxes[id][2], 5.6), "{id} {:?}", b.boxes[id]);
}
// a 42U cabinet is 2 m high
assert!(near(b.boxes["row"][5], 5.6 + 42.0 * 0.04445 + 0.13));
let out = render(&b.scene, 0, &RenderOpts::default());
assert_eq!(out.cycles, 0, "{:?}", out.cycle_at);
let row = |id: &str, k: &str| {
out.info[id]
.rows
.iter()
.find(|r| r.0 == k)
.map(|r| r.1.clone())
.unwrap_or_default()
};
// 20 racks at 8 kW + 6 at 5 kW; one in-row cooler after the first three racks
assert_eq!(row("pod", "IT load"), "8 kW per rack, 160 kW in all");
assert_eq!(
row("pod", "Containment"),
"cold aisle, 1.2 m, roof and end doors"
);
assert_eq!(row("row", "In-row cooling"), "1 × 30 kW = 30 kW");
// cooling 200 kW duty + 30 kW in-row + 100 kW standby against 190 kW IT: one unit spare
assert_eq!(
row("crah1", "This storey"),
"190 kW IT in 26 racks; 230 kW duty + 100 kW standby cooling"
);
assert_eq!(row("crah1", "Redundancy"), "N+1, 140 kW spare");
assert_eq!(row("rf", "On it"), "26 racks, 190 kW IT");
// Novec 1230 for the hall (28.8 x 19.2 m, 4.7 m slab to slab): 1,704 kg; 4 x 168 kg is short
assert_eq!(row("gas", "Needed"), "1,704 kg");
assert_eq!(row("gas", "Status"), "short: 7 more cylinders");
assert!(out.html.contains(">Gas: 7 cylinders short<"));
// overhead services sort in their own layer after everything on the floor
let ceiling = b.scene.layers.iter().find(|l| l.name == "ceiling").unwrap();
let ids: Vec<&str> = ceiling.sprites.iter().map(|s| s.id.as_str()).collect();
assert!(
ids.contains(&"tray") && ids.contains(&"det") && !ids.contains(&"pod"),
"{ids:?}"
);
let off = r
.build(
"1f",
0,
&{
let mut c = dc_cfg();
c.insert("ceiling".into(), json!(false));
c
},
&doc,
)
.unwrap();
assert!(!off.boxes.contains_key("tray") && !off.boxes.contains_key("det"));
}
#[test]
fn data_centre_storeys_draw_only_what_can_be_seen() {
let r = Runtime::new(&Bundle::default()).unwrap();
let doc = dc_doc();
let drawn = |level: &str| {
let b = r.build(level, 1, &dc_cfg(), &doc).unwrap();
assert!(
b.scene.warnings.is_empty(),
"{level}: {:?}",
b.scene.warnings
);
let out = render(&b.scene, 1, &RenderOpts::default());
assert_eq!(out.cycles, 0, "{level}: {:?}", out.cycle_at);
(b.boxes, out)
};
// closed: the shell hides every storey, so nothing inside it is drawn
let (site, _) = drawn("site");
assert!(site.contains_key("dc"));
for id in ["ups", "wall", "rf", "pod", "crah1", "tech"] {
assert!(
!site.contains_key(id),
"{id} drawn inside the closed building"
);
}
// GF cut open: the power room shows, the hall above is left out
let (gf, out) = drawn("gf");
for id in ["ups", "batt", "wall"] {
assert!(gf.contains_key(id), "{id} missing on GF");
}
assert!(!gf.contains_key("pod") && !gf.contains_key("rf"));
// batteries give their runtime at the IT load of every storey
let runtime = out.info["batt"]
.rows
.iter()
.find(|r| r.0 == "Runtime")
.unwrap()
.1
.clone();
assert_eq!(runtime, "about 56.8 min at 190 kW IT");
// 1F cut open: the hall shows, the closed ground floor's rooms do not
let (f1, _) = drawn("1f");
assert!(f1.contains_key("pod") && f1.contains_key("rf") && f1.contains_key("dc"));
assert!(!f1.contains_key("ups") && !f1.contains_key("wall"));
}
File diff suppressed because it is too large. Load diff
+1 -1
View File
@@ -3,7 +3,7 @@
// on its object's z, so it can sit on any storey (see isokit/house). Pieces
// on storeys above the one a level cuts open are left out.
fn furniture_hidden(o) { house_view(o) == 2 }
fn furniture_hidden(o) { part_hidden(o) }
fn wood_choices() { ["wood", "woodd", "furnw", "steel", "kitchenc"] }
fn fabric_choices() { ["uph1", "uph2", "uph3", "uph4", "sofa", "bed", "cloth"] }
+22 -7
View File
@@ -27,11 +27,26 @@ fn house_view(o) {
if s < k { 0 } else if s == k { 1 } else { 2 }
}
/// Leave part o out? On a storey above the one a level cuts open, or on a
/// closed storey inside a data centre shell (dc_building), whose walls and
/// slabs hide it anyway (and would sort badly against what stands above).
fn part_hidden(o) {
let hv = house_view(o);
if hv == 2 { return true; }
if hv == 1 { return false; }
let s = storey_of(o.z);
for b in objects_at(["dc_building"], o.x, o.y) {
let s0 = storey_of(b.z);
if s >= s0 && s < s0 + b.p.storeys { return true; }
}
false
}
fn wall_cut_h() { 1.0 }
fn wall_types() { ["house_wall"] }
fn opening_types() { ["window", "door"] }
fn room_types() { ["floor"] }
fn house_wall_materials() { ["render", "render2", "brick", "wood", "wall", "wall2", "wallp", "concb", "office", "furnw"] }
fn room_types() { ["floor", "raised_floor"] }
fn house_wall_materials() { ["render", "render2", "brick", "wood", "wall", "wall2", "wallp", "concb", "office", "furnw", "firewall"] }
fn flooring_materials() { ["f-oak", "f-wood", "f-tile", "f-stone", "f-carpet", "f-conc", "z-floor", "z-living", "z-kitchen", "z-bed", "z-wc"] }
fn house_roof_materials() { ["rooft", "roofr", "roof", "roof2", "roof3", "roofcap", "canopy"] }
@@ -116,7 +131,7 @@ fn meta_house_wall() {
fn prefab_house_wall(o) {
let p = o.p;
if house_view(o) == 2 { return; }
if part_hidden(o) { return; }
let top = wall_top(o);
let cap = if top < p.h { "cut" } else { p.mat };
let t = p.t / 2.0;
@@ -203,7 +218,7 @@ fn meta_window() {
fn prefab_window(o) {
let c = opening_at(o);
let w = c.wall;
if house_view(w) == 2 { return; }
if part_hidden(w) { return; }
let H = w.p.h;
let t = w.p.t / 2.0;
let top = wall_top(w);
@@ -247,7 +262,7 @@ fn meta_door() {
fn prefab_door(o) {
let c = opening_at(o);
let w = c.wall;
if house_view(w) == 2 { return; }
if part_hidden(w) { return; }
let p = o.p;
let H = w.p.h;
let t = w.p.t / 2.0;
@@ -314,7 +329,7 @@ fn meta_floor() {
}
fn prefab_floor(o) {
if house_view(o) == 2 { return; }
if part_hidden(o) { return; }
let p = o.p;
let w = p.w / 2.0;
let d = p.d / 2.0;
@@ -495,7 +510,7 @@ fn meta_stairs() {
}
fn prefab_stairs(o) {
if house_view(o) == 2 { return; }
if part_hidden(o) { return; }
let p = o.p;
let w = p.w / 2.0;
let n = fmax(2.0, (p.rise / 0.18).round()).to_int();
+11 -4
View File
@@ -3,30 +3,37 @@
fn people_colours() { ["hivis", "guest1", "guest2", "guest3", "guest4", "guide", "suit"] }
fn meta_person() {
#{ label: "Person", category: "People", icon: "🧍",
#{ label: "Person", category: "People", icon: "🧍", storey: true,
props: [
#{ key: "cls", type: "choice", def: "hivis", choices: people_colours(), label: "Clothes" },
#{ key: "seated", type: "bool", def: false, label: "Seated" },
] }
}
// People are left out like house parts (on a storey above the one a level
// cuts open, or inside a closed data centre storey); on a raised floor they
// stand on its top.
fn prefab_person(o) {
if part_hidden(o) { return; }
let z = dc_lift(o);
let s = sprite();
if o.p.seated { s.sit(0.0, 0.0, 0.0, o.p.cls); } else { s.person(0.0, 0.0, 0.0, o.p.cls); }
if o.p.seated { s.sit(0.0, 0.0, z, o.p.cls); } else { s.person(0.0, 0.0, z, o.p.cls); }
}
fn meta_crowd() {
#{ label: "Crowd", category: "People", icon: "👥",
#{ label: "Crowd", category: "People", icon: "👥", storey: true,
props: [
#{ key: "n", type: "int", def: 5, min: 2, max: 40, step: 1, label: "People" },
#{ key: "r", type: "float", def: 2.0, min: 0.6, max: 15.0, step: 0.1, label: "Spread (m)" },
] }
}
fn prefab_crowd(o) {
if part_hidden(o) { return; }
let z = dc_lift(o);
let cols = ["guest1", "guest2", "guest3", "guest4", "hivis"];
for i in 0..o.p.n {
// one sprite each, scattered deterministically in a disc
let a = hash(i * 1.7 + o.x) * 6.2832;
let r = o.p.r * hash(i * 3.3 + o.y).sqrt();
sprite().person(r * a.cos(), r * a.sin(), 0.0, cols[i % 5]);
sprite().person(r * a.cos(), r * a.sin(), z, cols[i % 5]);
}
}
+200
View File
@@ -318,6 +318,66 @@ walk = "#555c6b"
kerbf = "#7d8494"
apron = "#4a4840"
# data centre
dccab = "#262b34"
dccabt = "#3a414d"
dcsrv = "#5b6678"
dcstor = "#6f7f96"
dcgpu = "#414b5c"
dcnet = "#2f7396"
dcpatch = "#c9ced6"
dcslot = "#2a3039"
dcinrow = "#aab4c2"
dcrf = "#9aa3b0"
dcrf2 = "#7f93ad"
dcrf3 = "#a69e90"
dcrfs = "#5a616d"
dcperf = "#5f86b8"
dccold = "#8fcaff"
dchot = "#ff8a5a"
dcalu = "#b9c2ce"
dcglass = "#8fc8ff"
dcglassh = "#ffb08a"
dccrah = "#c4ccd6"
dcups = "#b8c0cb"
dcbatt = "#3a414d"
dcpdu = "#5d6f8a"
dcswg = "#8a96a6"
dcbus = "#9ea4ad"
dctap = "#c46a24"
dctray = "#8e98a6"
dcfibre = "#e3b62b"
dcbasket = "#a7b0bc"
dccable = "#1b1f26"
dcfibrec = "#f0d060"
dccablec = "#3f78c0"
dcdet = "#e9edf2"
dcdeth = "#f2e2b6"
dcvesda = "#e0574a"
dcfirel = "#e0574a"
dcgaspipe = "#aab3be"
dcpanel = "#c03c35"
dcyel = "#e0b030"
dcwhite = "#f2f4f7"
dcgreen = "#3f9a5a"
dcscreen = "#58d0e0"
dctower = "#9fb0c2"
dctowert = "#7e8fa2"
dcpipeg = "#3f9a5a"
dcpipeg2 = "#2c7344"
dcpipev = "#8a5fc0"
dccoil = "#6f8193"
dcpad = "#8fa58a"
dcband = "#6d7480"
dcvoid = "#1a1e25"
dcleg = "#7a828e"
dcmeshp = "#9aa6b5"
dcpdu0 = "#b8413a"
dcrear = "#1e2229"
dcbay = "#15181d"
dcdoor = "#30363f"
dcbrk = "#2b2f37"
dcpanelb = "#9aa4b2"
[palette.light]
sky1 = "#a9d4fb"
sky2 = "#e8f4ff"
@@ -626,6 +686,66 @@ walk = "#d9d6cf"
kerbf = "#a5a9b1"
apron = "#b9ad98"
# data centre
dccab = "#3b424e"
dccabt = "#5a6371"
dcsrv = "#7d899b"
dcstor = "#94a3b8"
dcgpu = "#5d6878"
dcnet = "#3f8fb5"
dcpatch = "#eef1f5"
dcslot = "#454c58"
dcinrow = "#dde2e8"
dcrf = "#d3d8df"
dcrf2 = "#b6c6da"
dcrf3 = "#d8d0c2"
dcrfs = "#8d95a2"
dcperf = "#8eb0da"
dccold = "#3f86e0"
dchot = "#e0582a"
dcalu = "#a3adbb"
dcglass = "#5aa6e6"
dcglassh = "#f08a5a"
dccrah = "#eef1f4"
dcups = "#e3e7ec"
dcbatt = "#5d6573"
dcpdu = "#7f93ad"
dcswg = "#b2bcc8"
dcbus = "#c3c8cf"
dctap = "#dd7f2f"
dctray = "#aeb7c3"
dcfibre = "#e8c22e"
dcbasket = "#c3cad3"
dccable = "#2a2f38"
dcfibrec = "#f5dc70"
dccablec = "#4f8fd8"
dcdet = "#ffffff"
dcdeth = "#f7ecc8"
dcvesda = "#d24a3c"
dcfirel = "#d0453f"
dcgaspipe = "#9aa3ad"
dcpanel = "#d0453f"
dcyel = "#e8c22e"
dcwhite = "#ffffff"
dcgreen = "#2fae68"
dcscreen = "#2aa8bf"
dctower = "#c8d3df"
dctowert = "#a9b6c4"
dcpipeg = "#2fae68"
dcpipeg2 = "#23864f"
dcpipev = "#9b6fd8"
dccoil = "#8fa1b4"
dcpad = "#a9c0a2"
dcband = "#9aa1ab"
dcvoid = "#3a404b"
dcleg = "#a3abb6"
dcmeshp = "#b9c4d2"
dcpdu0 = "#c9473a"
dcrear = "#2f353f"
dcbay = "#22262d"
dcdoor = "#4a525f"
dcbrk = "#3a404b"
dcpanelb = "#c3cad3"
[materials]
warn = "#e0412f"
suit = "suit"
@@ -903,6 +1023,56 @@ f-carpet = "fcarpet"
f-stone = "fstone"
f-conc = "conc"
# data centre
dccab = "dccab"
dccabt = "dccabt"
dcsrv = "dcsrv"
dcstor = "dcstor"
dcgpu = "dcgpu"
dcnet = "dcnet"
dcpatch = "dcpatch"
dcslot = "dcslot"
dcinrow = "dcinrow"
dcrf = "dcrf"
dcrf2 = "dcrf2"
dcrf3 = "dcrf3"
dcrfs = "dcrfs"
dcalu = "dcalu"
dccrah = "dccrah"
dcups = "dcups"
dcbatt = "dcbatt"
dcpdu = "dcpdu"
dcswg = "dcswg"
dcbus = "dcbus"
dctap = "dctap"
dctray = "dctray"
dcfibre = "dcfibre"
dcbasket = "dcbasket"
dccable = "dccable"
dcfibrec = "dcfibrec"
dccablec = "dccablec"
dcvesda = "dcvesda"
dcgaspipe = "dcgaspipe"
dcpanel = "dcpanel"
dcyel = "dcyel"
dcwhite = "dcwhite"
dcgreen = "dcgreen"
dctower = "dctower"
dctowert = "dctowert"
dcpipeg = "dcpipeg"
dcpipeg2 = "dcpipeg2"
dcpipev = "dcpipev"
dccoil = "dccoil"
dcpad = "dcpad"
dcband = "dcband"
dcvoid = "dcvoid"
dcleg = "dcleg"
dcpdu0 = "dcpdu0"
dcrear = "dcrear"
dcbay = "dcbay"
dcdoor = "dcdoor"
dcbrk = "dcbrk"
dcpanelb = "dcpanelb"
[rules]
".iso-ground .iso-dirt" = "stroke: var(--iso-edge); stroke-width: .4"
".iso-svg .iso-grid" = "fill: none; stroke: var(--iso-grid); stroke-width: .5"
@@ -1029,3 +1199,33 @@ f-conc = "conc"
".iso-svg .iso-hobr" = "fill: none; stroke: #6a707b; stroke-width: .3"
".iso-svg .iso-metal" = "fill: var(--iso-steel); stroke: none"
".iso-svg .iso-keyb" = "fill: #2b2f37; stroke: none"
# data centre
".iso-svg .iso-dctile" = "fill: none; stroke: var(--iso-edge); stroke-width: .2; opacity: .3"
".iso-svg .iso-dcperf" = "fill: var(--iso-dcperf); stroke: none; opacity: .85"
".iso-svg .iso-dcglass, .iso-svg .iso-dcglass.iso-t" = "fill-opacity: .2; --b: var(--iso-dcglass); stroke-opacity: .55"
".iso-svg .iso-dcglassh, .iso-svg .iso-dcglassh.iso-t" = "fill-opacity: .2; --b: var(--iso-dcglassh); stroke-opacity: .55"
".iso-svg .iso-dcglassd" = "fill: var(--iso-dcglass); fill-opacity: .14; stroke: none"
".iso-svg .iso-dcmesh" = "fill: #000; fill-opacity: .3; stroke: none"
".iso-svg .iso-dcmeshp, .iso-svg .iso-dcmeshp.iso-t" = "fill-opacity: .25; --b: var(--iso-dcmeshp); stroke-opacity: .45"
".iso-svg .iso-dcmeshd, .iso-svg .iso-dcmeshd.iso-t" = "fill-opacity: .45; --b: var(--iso-dcmeshp); stroke-opacity: .6"
".iso-svg .iso-dcframe" = "fill: none; stroke: var(--iso-dcalu); stroke-width: .3"
".iso-svg path.iso-dcsrv, .iso-svg path.iso-dcstor, .iso-svg path.iso-dcgpu, .iso-svg path.iso-dcnet, .iso-svg path.iso-dcpatch, .iso-svg path.iso-dcslot, .iso-svg path.iso-dcbay, .iso-svg path.iso-dcrear, .iso-svg path.iso-dcpdu0, .iso-svg path.iso-dcvoid, .iso-svg path.iso-dcleg, .iso-svg path.iso-dcband, .iso-svg path.iso-dcbrk, .iso-svg path.iso-dcdoor" = "stroke: none"
".iso-svg .iso-dcstrip" = "fill: var(--iso-led-g); stroke: none; opacity: .9"
".iso-svg .iso-dcscr" = "fill: var(--iso-dcscreen); stroke: none"
".iso-svg .iso-dccov" = "fill: var(--iso-dchot); fill-opacity: .025; stroke: var(--iso-dchot); stroke-width: .22; stroke-dasharray: 1.2 2.2; opacity: .45; pointer-events: none"
".iso-svg .iso-dcdetd" = "fill: var(--iso-dcdet); stroke: var(--iso-edge); stroke-width: .12"
".iso-svg .iso-dcdethd" = "fill: var(--iso-dcdeth); stroke: var(--iso-edge); stroke-width: .12"
".iso-svg .iso-dcvesl" = "fill: none; stroke: var(--iso-dcvesda); stroke-width: .5; stroke-linecap: round"
".iso-svg .iso-dcsprl" = "fill: none; stroke: var(--iso-dcfirel); stroke-width: .8; stroke-linecap: round"
".iso-svg .iso-dcgasl" = "fill: none; stroke: var(--iso-dcgaspipe); stroke-width: .9; stroke-linecap: round"
".iso-svg .iso-dchole" = "fill: var(--iso-dcwhite); stroke: var(--iso-dcvesda); stroke-width: .2"
".iso-svg .iso-dcspr" = "fill: var(--iso-dcfirel); stroke: none"
".iso-svg .iso-dcsprh" = "fill: var(--iso-dcyel); stroke: none"
".iso-svg .iso-dcdled" = "fill: var(--iso-led-r); stroke: none"
".iso-svg .iso-dcflash" = "animation: iso-dcflash 4s linear infinite"
"@keyframes iso-dcflash" = "0%, 86%, 100% { opacity: .3 } 90% { opacity: 1 }"
".iso-svg .iso-dcblink" = "animation: iso-blink 1.9s ease-in-out infinite"
".iso-svg .iso-dcair, .iso-svg .iso-dcheat" = "transform-box: fill-box; transform-origin: center; animation: iso-dcrise 3s ease-out infinite; opacity: 0; pointer-events: none; stroke: none"
".iso-svg .iso-dcair" = "fill: var(--iso-dccold)"
".iso-svg .iso-dcheat" = "fill: var(--iso-dchot)"
"@keyframes iso-dcrise" = "0% { transform: translate(0, 0) scale(.5); opacity: 0 } 25% { opacity: .6 } 100% { transform: translate(0, -7px) scale(1.3); opacity: 0 }"