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

+1
View File
@@ -24,6 +24,7 @@ on:
- "examples/factory/**" - "examples/factory/**"
- "examples/streets/**" - "examples/streets/**"
- "examples/country/**" - "examples/country/**"
- "examples/datacentre/**"
- "tools/build_web.py" - "tools/build_web.py"
- "Cargo.toml" - "Cargo.toml"
- "Cargo.lock" - "Cargo.lock"
+27 -2
View File
@@ -40,8 +40,9 @@ isokit build C:/dev/myproject/site-view # dist/index.html, no server needed
Try the demos: `isokit studio examples/demo` (a works site), Try the demos: `isokit studio examples/demo` (a works site),
`isokit studio examples/house` (a furnished house built from parts), `isokit studio examples/house` (a furnished house built from parts),
`isokit studio examples/factory` (presses, conveyors and robot arms) and `isokit studio examples/factory` (presses, conveyors and robot arms),
`isokit studio examples/streets` (sidewalks, curves, junctions, a roundabout). `isokit studio examples/streets` (sidewalks, curves, junctions, a roundabout) and
`isokit studio examples/datacentre` (a three-storey data centre).
### Studio (for everyone) ### Studio (for everyone)
@@ -102,6 +103,30 @@ rounded, and the movers pass in front of and behind trees and houses as they
go. The streets demo has traffic round the block, a bus, motorbikes, cyclists go. The streets demo has traffic round the block, a bus, motorbikes, cyclists
and people strolling. and people strolling.
**Data centres** (palette groups Data centre, Cooling, Fire safety, Power):
the DC building is the shell of a multi-storey data centre (storeys of the
project's `storey_h`, 5 m in the demo); GF, 1F, 2F cut it open, and parts on
closed storeys are not drawn at all. On each storey a Raised floor (0.6 m
tiles over the supply plenum) carries Rack rows and Rack pods: cabinets of
any width, depth and height in U (servers, storage, GPU, network or empty,
mesh, glass or solid doors, status LEDs), in-row coolers between groups of
racks, and pods with cold aisle containment (vent tiles, glass roof, sliding
end doors, cold air rising) or hot aisle containment (backs to the aisle,
optional duct to the ceiling); Cages for colocation. Cooling: CRAH / CRAC
units, chillers and dry or adiabatic coolers with fan rows, cooling towers
with a plume, flow and return pipe runs. Fire safety: detector grids with
their coverage, VESDA sampling pipes with the detector unit, pre-action
sprinkler and gas discharge pipes, clean-agent cylinder banks (Novec 1230,
FM-200, Inergen), alarm and release panels, call points, abort switches,
beacons, extinguishers. Power: UPS, battery, PDU, switchgear and RPP
line-ups, busways with tap-off boxes, cable trays and fibre ducts,
transformers. The tooltips do the sums: IT load per pod, floor and building,
kW/m², cooling against load per storey (N+1 and so on), UPS loading,
battery runtime, the agent a gas bank needs for its halls (a warning label
when it is short). `examples/datacentre` (Data centre demo in the web Studio)
has a power room, a hall with cold aisle pods, a GPU hall with hot aisle pods
and cages, and chillers on the roof.
**Fences and walls** around the plot: the Fence has six designs (mesh, wooden **Fences and walls** around the plot: the Fence has six designs (mesh, wooden
boards, steel bars, mesh on a stone base, cobble wall, brick wall with boards, steel bars, mesh on a stone base, cobble wall, brick wall with
pillars). Fences and site walls whose ends meet join like house walls: corners pillars). Fences and site walls whose ends meet join like house walls: corners
+10
View File
@@ -59,6 +59,10 @@ pub struct Rendered {
pub nodes: usize, pub nodes: usize,
/// Depth-sort cycles that had to be broken (0 is ideal). /// Depth-sort cycles that had to be broken (0 is ideal).
pub cycles: usize, 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>, pub warnings: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")] #[serde(skip_serializing_if = "Vec::is_empty")]
pub order: Vec<OrderItem>, 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 frame = [1e9f64, 1e9, -1e9, -1e9];
let mut sprites = 0; let mut sprites = 0;
let mut cycles = 0; let mut cycles = 0;
let mut cycle_at = vec![];
let mut order = vec![]; let mut order = vec![];
let mut warnings = scene.warnings.clone(); let mut warnings = scene.warnings.clone();
@@ -748,6 +753,10 @@ pub fn render(scene: &Scene, rot: u8, opts: &RenderOpts) -> Rendered {
.collect(); .collect();
let o = sort::sort_hulls(&pairs, &hulls, layer.opts.zfirst); let o = sort::sort_hulls(&pairs, &hulls, layer.opts.zfirst);
cycles += o.cycles; 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 o.order
} else { } else {
(0..live.len()).collect() (0..live.len()).collect()
@@ -876,6 +885,7 @@ pub fn render(scene: &Scene, rot: u8, opts: &RenderOpts) -> Rendered {
sprites, sprites,
nodes, nodes,
cycles, cycles,
cycle_at,
warnings, warnings,
order, order,
html, html,
+28
View File
@@ -153,6 +153,9 @@ fn rect_of(b: &Bb) -> Hull {
pub struct Order { pub struct Order {
pub order: Vec<usize>, pub order: Vec<usize>,
pub cycles: 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 { 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 { let mut out = Order {
order: Vec::with_capacity(n), order: Vec::with_capacity(n),
cycles: 0, cycles: 0,
broken: vec![],
}; };
for _ in 0..n { for _ in 0..n {
let mut best: Option<usize> = None; 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"); 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; done[b] = true;
out.order.push(b); out.order.push(b);
for &j in &adj[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]) ([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] #[test]
fn separated_on_x_draws_far_first() { fn separated_on_x_draws_far_first() {
let o = sort( let o = sort(
+11 -1
View File
@@ -137,7 +137,17 @@ pub fn check(p: &Project) -> Vec<Finding> {
)); ));
} }
if v.cycles > 0 { 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 for cls in v
.html .html
+4
View File
@@ -201,6 +201,9 @@ pub struct View {
pub sprites: usize, pub sprites: usize,
pub nodes: usize, pub nodes: usize,
pub cycles: 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>, pub warnings: Vec<String>,
/// Object id -> world box [x0, y0, z0, x1, y1, z1]. /// Object id -> world box [x0, y0, z0, x1, y1, z1].
pub boxes: BTreeMap<String, [f64; 6]>, pub boxes: BTreeMap<String, [f64; 6]>,
@@ -428,6 +431,7 @@ impl Project {
sprites: r.sprites, sprites: r.sprites,
nodes: r.nodes, nodes: r.nodes,
cycles: r.cycles, cycles: r.cycles,
cycle_at: r.cycle_at,
warnings: r.warnings, warnings: r.warnings,
boxes: built.boxes, boxes: built.boxes,
ms: (t0.elapsed().as_secs_f64() * 1e4).round() / 10.0, 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 \ 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. \ 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 \ 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 { fn tool(name: &str, description: &str, props: Value, required: &[&str]) -> Value {
json!({ "name": name, "description": description, 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"); 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) { fn example_renders_every_view_cleanly(name: &str) {
let p = Project::load( let p = Project::load(
Path::new(env!("CARGO_MANIFEST_DIR")) 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/house", include_str!("../stdlib/house.rhai")),
("isokit/furniture", include_str!("../stdlib/furniture.rhai")), ("isokit/furniture", include_str!("../stdlib/furniture.rhai")),
("isokit/industry", include_str!("../stdlib/industry.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 r = Runtime::new(&Bundle::default()).unwrap();
let mut doc = Doc::default(); let mut doc = Doc::default();
for (i, m) in r.catalog().iter().enumerate() { 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 // switch every bool prop on to exercise all branches
for p in &m.props { for p in &m.props {
if p.kind == "bool" { 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"); 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 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. // 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 wood_choices() { ["wood", "woodd", "furnw", "steel", "kitchenc"] }
fn fabric_choices() { ["uph1", "uph2", "uph3", "uph4", "sofa", "bed", "cloth"] } 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 } 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_cut_h() { 1.0 }
fn wall_types() { ["house_wall"] } fn wall_types() { ["house_wall"] }
fn opening_types() { ["window", "door"] } fn opening_types() { ["window", "door"] }
fn room_types() { ["floor"] } fn room_types() { ["floor", "raised_floor"] }
fn house_wall_materials() { ["render", "render2", "brick", "wood", "wall", "wall2", "wallp", "concb", "office", "furnw"] } 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 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"] } fn house_roof_materials() { ["rooft", "roofr", "roof", "roof2", "roof3", "roofcap", "canopy"] }
@@ -116,7 +131,7 @@ fn meta_house_wall() {
fn prefab_house_wall(o) { fn prefab_house_wall(o) {
let p = o.p; let p = o.p;
if house_view(o) == 2 { return; } if part_hidden(o) { return; }
let top = wall_top(o); let top = wall_top(o);
let cap = if top < p.h { "cut" } else { p.mat }; let cap = if top < p.h { "cut" } else { p.mat };
let t = p.t / 2.0; let t = p.t / 2.0;
@@ -203,7 +218,7 @@ fn meta_window() {
fn prefab_window(o) { fn prefab_window(o) {
let c = opening_at(o); let c = opening_at(o);
let w = c.wall; let w = c.wall;
if house_view(w) == 2 { return; } if part_hidden(w) { return; }
let H = w.p.h; let H = w.p.h;
let t = w.p.t / 2.0; let t = w.p.t / 2.0;
let top = wall_top(w); let top = wall_top(w);
@@ -247,7 +262,7 @@ fn meta_door() {
fn prefab_door(o) { fn prefab_door(o) {
let c = opening_at(o); let c = opening_at(o);
let w = c.wall; let w = c.wall;
if house_view(w) == 2 { return; } if part_hidden(w) { return; }
let p = o.p; let p = o.p;
let H = w.p.h; let H = w.p.h;
let t = w.p.t / 2.0; let t = w.p.t / 2.0;
@@ -314,7 +329,7 @@ fn meta_floor() {
} }
fn prefab_floor(o) { fn prefab_floor(o) {
if house_view(o) == 2 { return; } if part_hidden(o) { return; }
let p = o.p; let p = o.p;
let w = p.w / 2.0; let w = p.w / 2.0;
let d = p.d / 2.0; let d = p.d / 2.0;
@@ -495,7 +510,7 @@ fn meta_stairs() {
} }
fn prefab_stairs(o) { fn prefab_stairs(o) {
if house_view(o) == 2 { return; } if part_hidden(o) { return; }
let p = o.p; let p = o.p;
let w = p.w / 2.0; let w = p.w / 2.0;
let n = fmax(2.0, (p.rise / 0.18).round()).to_int(); 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 people_colours() { ["hivis", "guest1", "guest2", "guest3", "guest4", "guide", "suit"] }
fn meta_person() { fn meta_person() {
#{ label: "Person", category: "People", icon: "🧍", #{ label: "Person", category: "People", icon: "🧍", storey: true,
props: [ props: [
#{ key: "cls", type: "choice", def: "hivis", choices: people_colours(), label: "Clothes" }, #{ key: "cls", type: "choice", def: "hivis", choices: people_colours(), label: "Clothes" },
#{ key: "seated", type: "bool", def: false, label: "Seated" }, #{ 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) { fn prefab_person(o) {
if part_hidden(o) { return; }
let z = dc_lift(o);
let s = sprite(); 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() { fn meta_crowd() {
#{ label: "Crowd", category: "People", icon: "👥", #{ label: "Crowd", category: "People", icon: "👥", storey: true,
props: [ props: [
#{ key: "n", type: "int", def: 5, min: 2, max: 40, step: 1, label: "People" }, #{ 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)" }, #{ key: "r", type: "float", def: 2.0, min: 0.6, max: 15.0, step: 0.1, label: "Spread (m)" },
] } ] }
} }
fn prefab_crowd(o) { fn prefab_crowd(o) {
if part_hidden(o) { return; }
let z = dc_lift(o);
let cols = ["guest1", "guest2", "guest3", "guest4", "hivis"]; let cols = ["guest1", "guest2", "guest3", "guest4", "hivis"];
for i in 0..o.p.n { for i in 0..o.p.n {
// one sprite each, scattered deterministically in a disc // one sprite each, scattered deterministically in a disc
let a = hash(i * 1.7 + o.x) * 6.2832; let a = hash(i * 1.7 + o.x) * 6.2832;
let r = o.p.r * hash(i * 3.3 + o.y).sqrt(); 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" kerbf = "#7d8494"
apron = "#4a4840" 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] [palette.light]
sky1 = "#a9d4fb" sky1 = "#a9d4fb"
sky2 = "#e8f4ff" sky2 = "#e8f4ff"
@@ -626,6 +686,66 @@ walk = "#d9d6cf"
kerbf = "#a5a9b1" kerbf = "#a5a9b1"
apron = "#b9ad98" 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] [materials]
warn = "#e0412f" warn = "#e0412f"
suit = "suit" suit = "suit"
@@ -903,6 +1023,56 @@ f-carpet = "fcarpet"
f-stone = "fstone" f-stone = "fstone"
f-conc = "conc" 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] [rules]
".iso-ground .iso-dirt" = "stroke: var(--iso-edge); stroke-width: .4" ".iso-ground .iso-dirt" = "stroke: var(--iso-edge); stroke-width: .4"
".iso-svg .iso-grid" = "fill: none; stroke: var(--iso-grid); stroke-width: .5" ".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-hobr" = "fill: none; stroke: #6a707b; stroke-width: .3"
".iso-svg .iso-metal" = "fill: var(--iso-steel); stroke: none" ".iso-svg .iso-metal" = "fill: var(--iso-steel); stroke: none"
".iso-svg .iso-keyb" = "fill: #2b2f37; 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 }"
+25
View File
@@ -71,6 +71,31 @@ Each phase ends with tests, a Studio/viewer check in Edge, commit and push.
- [x] Diagonal walls (any angle, mitred corners, windows and doors on them) - [x] Diagonal walls (any angle, mitred corners, windows and doors on them)
- [ ] Openings across two walls; floor openings for stairs - [ ] Openings across two walls; floor openings for stairs
## Data centres (done)
- [x] `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, parts on
closed storeys inside it are left out (`part_hidden`, also for house parts and people)
- [x] Raised floor (tiles, void with pedestals); racks, pods, units, cabinets and people stand on it
- [x] Rack row and Rack pod: cabinets by U, width, depth, equipment (servers, storage, GPU, network,
empty), fill, doors, status LEDs; in-row coolers; cold / hot aisle containment (vent tiles,
roof, end doors, ceiling duct, airflow); colocation Cage
- [x] Cooling: CRAH / CRAC units, chillers, dry and adiabatic coolers, cooling towers, pipe runs
- [x] Fire safety: detector grids with coverage, VESDA / sprinkler / gas discharge pipes, clean-agent
cylinder banks sized for their halls (Novec 1230, FM-200, Inergen), panels, call points, abort
switches, beacons, extinguishers
- [x] Power: UPS, battery, PDU, switchgear and RPP line-ups, busways, cable trays, transformers
- [x] Tooltips do the sums: IT load and kW/m², cooling redundancy per storey, UPS load, battery
runtime, plant cooling, agent quantity (warning label when short)
- [x] Overhead services in their own `ceiling` layer (z first) after the floor; `isokit check` names
where the depth sort broke a cycle
- [x] `examples/datacentre` (three storeys and a roof), Data centre demo in the web Studio
- [ ] Next: floor openings and a lift / stair core through the slabs; cold aisle airflow from the
CRAHs drawn under the raised floor (section cut, Phase 4); generator yard and fuel system
parts (day tanks, ATS); security (CCTV, access control, mantraps); leak detection; liquid
cooling (CDUs, rear-door heat exchangers, manifolds); a row-by-row power and cooling budget
table on the building
## Backlog (2026-10) ## Backlog (2026-10)
1. ~~Web performance~~ (done, see below) 1. ~~Web performance~~ (done, see below)
+42
View File
@@ -293,6 +293,48 @@ prefabs work together through the document, not through a parent object:
- Ground-level floors and rugs go to the `floor` layer; upper floors are slabs - Ground-level floors and rugs go to the `floor` layer; upper floors are slabs
whose top is at the object's z. Furniture starts 0.02 m above its z. whose top is at the object's z. Furniture starts 0.02 m above its z.
## Data centres
`datacentre.rhai` lays out multi-storey data centres from parts that read
each other through the document, like house parts:
- `dc_building` is the shell: `storeys` of `storey_h` (the project
parameter; 5 m suits data halls), outer walls split into pieces of at most
15 m, a slab per storey and a flat roof with a parapet. On level `kf` the
storeys below are closed, the walls facing the viewer are cut 1.2 m above
storey k, the others reach its ceiling, and everything above is left out.
- `part_hidden(o)` (house.rhai) leaves out a part above the storey a level
cuts open, and a part on a closed storey inside a `dc_building`: the shell
hides it, and drawing it would only make sort cycles with what stands on
the slab above. House walls, doors, floors, stairs, furniture, people and
every data centre part use it (`dc_skip` is the same rule).
- `dc_lift(o)`: the top of a `raised_floor` under o on its storey; racks,
pods, CRAH units, cabinets, gas banks, devices and people stand on it.
- A rack line is one sprite with a box per cabinet (`seq`) and the faces
painted with `open()` per class (equipment, LED strips, rear PDUs), so a
pod of 20 racks costs a few dozen nodes. In-row coolers (`inrow`) are
0.3 m units between groups of racks. A pod is three sprites (two rows and
the aisle between them: vent tiles, airflow puffs, end doors, roof, duct),
which never overlap.
- Overhead services (`cable_tray`, `busway`, `fire_pipe`, `smoke_detectors`)
draw in the layer `ceiling` (`dc_ceiling_layer()`: sorted, z first) after
everything on the floor, with sort boxes as tight as the run itself
(hangers do not count): a tray beside a pod with a busway over the pod and
under the tray has no valid order as boxes in one layer. Keep runs that
cross or lie side by side at heights that do not overlap. The project
parameter `ceiling` = false leaves them out.
- Totals: `dc_it(storey)` (IT load, racks; every storey for -1),
`dc_cool(storey)` (CRAH duty and standby, in-row coolers),
`dc_redundancy(cooling, kw)` ("N+1, 140 kW spare"), `dc_plant()` (chillers
and towers), `dc_agent(agent, V, t, litres)` (Novec 1230 at 4.5 %, FM-200
at 7 % by NFPA 2001 / ISO 14520, Inergen at 39.9 %), `dc_gas_volume(o)`.
A gas bank that is short puts a `warntag` on the view.
- Wall devices facing away from the viewer on the storey cut open hang on
the far side of a wall that is cut low: they draw ghosted.
`isokit check` names where the depth sort broke a cycle ("a before b, c":
sprite ids, `Rendered.cycle_at`), which points at the sprites to split.
## Rules that keep the depth sort right ## Rules that keep the depth sort right
These are lessons from the CNX/Mooban views, and the engine relies on them: These are lessons from the CNX/Mooban views, and the engine relies on them:
+56
View File
@@ -0,0 +1,56 @@
# isokit data centre demo: a three-storey data centre laid out from parts.
# Open it with `isokit studio examples/datacentre`.
title = "isokit data centre"
note = "A three-storey data centre: power and plant on the ground floor, a hall with cold aisle containment on 1F, GPU pods with hot aisle containment and colocation cages on 2F, chillers on the roof. GF, 1F and 2F cut the building open; tap racks, coolers, gas banks and detectors for their numbers."
default_level = "1f"
default_rotation = 1
prelude = []
theme = []
[[level]]
id = "site"
label = "Site"
title = "The data centre closed, with its roof plant"
[[level]]
id = "gf"
label = "GF"
title = "Ground floor: lobby and NOC, staging, power room"
[[level]]
id = "1f"
label = "1F"
title = "Data hall A: cold aisle containment"
[[level]]
id = "2f"
label = "2F"
title = "Data hall B: GPU pods (hot aisle) and colocation cages"
[[param]]
name = "storey_h"
type = "float"
default = 5.0
label = "Storey height (m)"
[[param]]
name = "ceiling"
type = "bool"
default = true
label = "Ceiling services"
[[param]]
name = "labels"
type = "bool"
default = true
label = "Labels"
[[param]]
name = "running"
type = "bool"
default = true
label = "Animation"
[build]
out = "dist/index.html"
budget = 12000
+184
View File
@@ -0,0 +1,184 @@
{
"version": 1,
"objects": [
{"id": "site", "type": "site", "x": 0, "y": 2, "props": {"w": 92, "d": 74, "mat": "g-grass"}, "name": "Site", "locked": true},
{"id": "apron", "type": "area", "x": 0, "y": 1, "props": {"w": 64, "d": 44, "mat": "g-conc"}, "name": "Yard apron", "locked": true},
{"id": "road", "type": "road", "x": 0, "y": -29, "props": {"w": 92, "d": 7}, "name": "Main road"},
{"id": "access", "type": "road", "x": 0, "y": -21.5, "rot": 90, "props": {"w": 15, "d": 6, "dashed": false}, "name": "Access road"},
{"id": "parking", "type": "parking", "x": -22, "y": -17, "props": {"n": 8, "bay_w": 2.5, "bay_d": 5}, "name": "Staff parking"},
{"id": "car-0", "type": "car", "x": -30.75, "y": -17, "rot": 90, "props": {"colour": "car1"}, "name": "Car"},
{"id": "car-1", "type": "car", "x": -28.25, "y": -17, "rot": 90, "props": {"colour": "car3"}, "name": "Car"},
{"id": "car-3", "type": "car", "x": -23.25, "y": -17, "rot": 90, "props": {"colour": "car4"}, "name": "Car"},
{"id": "car-4", "type": "car", "x": -20.75, "y": -17, "rot": 90, "props": {"colour": "car1"}, "name": "Car"},
{"id": "truck", "type": "truck", "x": 0, "y": -18.5, "rot": 90, "props": {"len": 8}, "name": "Delivery truck", "info": "Racks arriving at the loading dock"},
{"id": "f-s1", "type": "fence", "x": -31.5, "y": -24.0, "props": {"w": 21.0, "h": 2.4, "style": "mesh"}, "name": "Security fence"},
{"id": "f-s2", "type": "fence", "x": -12.5, "y": -24.0, "props": {"w": 17.0, "h": 2.4, "style": "mesh"}, "name": "Security fence"},
{"id": "f-s3", "type": "fence", "x": 12.5, "y": -24.0, "props": {"w": 17.0, "h": 2.4, "style": "mesh"}, "name": "Security fence"},
{"id": "f-s4", "type": "fence", "x": 31.5, "y": -24.0, "props": {"w": 21.0, "h": 2.4, "style": "mesh"}, "name": "Security fence"},
{"id": "f-e1", "type": "fence", "x": 42.0, "y": -9.75, "rot": 90.0, "props": {"w": 28.5, "h": 2.4, "style": "mesh"}, "name": "Security fence"},
{"id": "f-e2", "type": "fence", "x": 42.0, "y": 18.75, "rot": 90.0, "props": {"w": 28.5, "h": 2.4, "style": "mesh"}, "name": "Security fence"},
{"id": "f-n1", "type": "fence", "x": 28.0, "y": 33.0, "rot": 180.0, "props": {"w": 28.0, "h": 2.4, "style": "mesh"}, "name": "Security fence"},
{"id": "f-n2", "type": "fence", "x": 0.0, "y": 33.0, "rot": 180.0, "props": {"w": 28.0, "h": 2.4, "style": "mesh"}, "name": "Security fence"},
{"id": "f-n3", "type": "fence", "x": -28.0, "y": 33.0, "rot": 180.0, "props": {"w": 28.0, "h": 2.4, "style": "mesh"}, "name": "Security fence"},
{"id": "f-w1", "type": "fence", "x": -42.0, "y": 18.75, "rot": 270.0, "props": {"w": 28.5, "h": 2.4, "style": "mesh"}, "name": "Security fence"},
{"id": "f-w2", "type": "fence", "x": -42.0, "y": -9.75, "rot": 270.0, "props": {"w": 28.5, "h": 2.4, "style": "mesh"}, "name": "Security fence"},
{"id": "guard", "type": "shed", "x": 8, "y": -21, "props": {"w": 3, "d": 2.4, "h": 2.7}, "name": "Gatehouse", "tag": "Security"},
{"id": "guard-p", "type": "person", "x": 6.2, "y": -21.6, "props": {"cls": "suit"}, "name": "Guard"},
{"id": "gen-1", "type": "generator", "x": -15, "y": 19, "props": {"running": true}, "name": "Generator 1", "info": "2.5 MVA diesel genset, N+1"},
{"id": "gen-2", "type": "generator", "x": -7, "y": 19, "props": {"running": false}, "name": "Generator 2", "info": "2.5 MVA diesel genset, N+1"},
{"id": "gen-3", "type": "generator", "x": 1, "y": 19, "props": {"running": false}, "name": "Generator 3", "info": "2.5 MVA diesel genset, N+1"},
{"id": "gen-4", "type": "generator", "x": 9, "y": 19, "props": {"running": false}, "name": "Generator 4", "info": "2.5 MVA diesel genset, N+1"},
{"id": "fuel", "type": "tank", "x": 19, "y": 19.5, "props": {"r": 1.8, "h": 3.4, "mat": "tank"}, "name": "Diesel tank", "info": "48 h of fuel at full load"},
{"id": "tx-1", "type": "transformer", "x": 29, "y": 7, "rot": 90, "props": {"kva": 3150}, "name": "Transformer T1", "tag": "T1"},
{"id": "tx-2", "type": "transformer", "x": 29, "y": 0.5, "rot": 90, "props": {"kva": 3150}, "name": "Transformer T2", "tag": "T2"},
{"id": "ct", "type": "cooling_tower", "x": 31, "y": -12, "props": {"cells": 2, "kw": 750}, "name": "Cooling tower", "tag": "Condenser water"},
{"id": "ct-pipe", "type": "pipe_run", "x": 25.0, "y": -12, "props": {"w": 4.0, "kind": "condenser", "dn": 0.3, "h": 1.0}, "name": "Condenser pipes"},
{"id": "tree-0", "type": "tree", "x": -30, "y": 28, "props": {"r": 2.2}, "name": "Tree"},
{"id": "tree-1", "type": "tree", "x": -22, "y": 28, "props": {"r": 2.2}, "name": "Tree"},
{"id": "tree-2", "type": "tree", "x": 22, "y": 28, "props": {"r": 2.2}, "name": "Tree"},
{"id": "tree-3", "type": "tree", "x": 34, "y": 28, "props": {"r": 2.2}, "name": "Tree"},
{"id": "trees-w", "type": "tree_row", "x": -37, "y": 4, "rot": 90, "props": {"w": 40, "spacing": 7, "r": 1.8}, "name": "Trees"},
{"id": "lamp-0", "type": "lamp", "x": -12, "y": -23, "props": {"h": 6}, "name": "Lamp"},
{"id": "lamp-1", "type": "lamp", "x": 12, "y": -23, "props": {"h": 6}, "name": "Lamp"},
{"id": "lamp-2", "type": "lamp", "x": -26, "y": 12, "props": {"h": 6}, "name": "Lamp"},
{"id": "lamp-3", "type": "lamp", "x": 26, "y": 14, "props": {"h": 6}, "name": "Lamp"},
{"id": "dc", "type": "dc_building", "x": 0, "y": 0, "props": {"w": 42, "d": 26, "storeys": 3, "door_at": 0.16666666666666666, "dock_at": 0.5}, "name": "Data centre CNX-1", "info": "Three storeys: power and plant on the ground floor, two data halls above, chillers on the roof"},
{"id": "gf-w1", "type": "house_wall", "x": -7, "y": 0, "rot": 90, "props": {"w": 25.2, "h": 4.7, "t": 0.2, "mat": "wall2"}, "name": "GF partition (lobby)"},
{"id": "gf-w1-door", "type": "door", "x": -7, "y": -9, "rot": 90, "props": {"w": 1.0, "h": 2.2, "style": "panel", "mat": "doorg"}, "name": "Lobby door"},
{"id": "gf-e1", "type": "house_wall", "x": 7, "y": 0, "rot": 90, "props": {"w": 25.2, "h": 4.7, "t": 0.2, "mat": "firewall"}, "name": "GF fire wall (power room)"},
{"id": "gf-e1-door", "type": "door", "x": 7, "y": -9, "rot": 90, "props": {"w": 1.2, "h": 2.2, "style": "panel", "mat": "doorg"}, "name": "Power room door"},
{"id": "gf-stairs", "type": "stairs", "x": -19.4, "y": 7.5, "props": {"w": 1.4, "run": 7, "rise": 4.7, "mat": "concb"}, "name": "Stairs to 1F"},
{"id": "noc-1", "type": "desk", "x": -15, "y": -3, "props": {"w": 1.6}, "name": "NOC desk"},
{"id": "noc-2", "type": "desk", "x": -12.6, "y": -3, "props": {"w": 1.6, "chair": false}, "name": "NOC desk"},
{"id": "noc-3", "type": "desk", "x": -15, "y": -6.5, "props": {"w": 1.6}, "name": "NOC desk"},
{"id": "noc-p", "type": "person", "x": -12.6, "y": -3.8, "props": {"cls": "suit", "seated": true}, "name": "NOC operator"},
{"id": "gf-facp", "type": "fire_device", "x": -17.5, "y": -12.53, "rot": 180, "props": {"kind": "alarm panel", "label": "Main fire alarm panel"}, "name": "Fire alarm panel", "tag": "FACP"},
{"id": "gf-cp-1", "type": "fire_device", "x": -12.4, "y": -12.53, "rot": 180, "props": {"kind": "call point", "label": "Lobby"}, "name": "Call point"},
{"id": "gf-bc-1", "type": "fire_device", "x": -7.27, "y": 2, "rot": 270, "props": {"kind": "beacon"}, "name": "Sounder-beacon"},
{"id": "gf-ext-1", "type": "fire_device", "x": -8, "y": -12.4, "rot": 180, "props": {"kind": "extinguisher"}, "name": "Extinguisher"},
{"id": "staging", "type": "rack_row", "x": 0, "y": 5.5, "props": {"n": 6, "kind": "empty", "u": 48, "doors": "none"}, "name": "Racks waiting for install", "tag": "Staging"},
{"id": "staging-p", "type": "person", "x": 1.8, "y": 3.6, "props": {"cls": "hivis"}, "name": "Technician"},
{"id": "gf-ext-2", "type": "fire_device", "x": 6.73, "y": -6, "rot": 90, "props": {"kind": "extinguisher"}, "name": "Extinguisher"},
{"id": "ups", "type": "power_cab", "x": 13.5, "y": 10.9, "props": {"kind": "UPS", "n": 6, "kw": 800}, "name": "UPS line-up", "tag": "UPS 6 × 800 kW"},
{"id": "batt-1", "type": "power_cab", "x": 13.5, "y": 7.6, "props": {"kind": "battery", "n": 12, "kwh": 60}, "name": "Battery string A"},
{"id": "batt-2", "type": "power_cab", "x": 13.5, "y": 4.6, "props": {"kind": "battery", "n": 12, "kwh": 60}, "name": "Battery string B"},
{"id": "swg", "type": "power_cab", "x": 13.5, "y": -3.5, "props": {"kind": "switchgear", "n": 7}, "name": "LV switchgear", "tag": "Switchgear"},
{"id": "gf-gas", "type": "gas_bank", "x": 20.45, "y": -8, "rot": 270, "props": {"agent": "Inergen", "n": 21, "litres": 140, "vol": 1620}, "name": "Inergen bank (power room)", "tag": "Inergen"},
{"id": "gf-cp-2", "type": "fire_device", "x": 7.27, "y": -7.6, "rot": 90, "props": {"kind": "call point", "label": "Power room"}, "name": "Call point"},
{"id": "gf-bc-2", "type": "fire_device", "x": 20.53, "y": 2, "rot": 270, "props": {"kind": "beacon"}, "name": "Sounder-beacon"},
{"id": "gf-det-w", "type": "smoke_detectors", "x": -13.85, "y": 0, "props": {"w": 13.4, "d": 25.2, "spacing": 6.5, "h": 4.4, "zone": "GF lobby"}, "name": "Detectors GF lobby"},
{"id": "gf-det-m", "type": "smoke_detectors", "x": 0, "y": 0, "props": {"w": 13.8, "d": 25.2, "spacing": 6.5, "h": 4.4, "zone": "GF staging"}, "name": "Detectors GF staging"},
{"id": "gf-det-e", "type": "smoke_detectors", "x": 13.85, "y": 0, "props": {"w": 13.4, "d": 25.2, "spacing": 6.5, "h": 4.4, "kind": "multi-sensor", "zone": "GF power room"}, "name": "Detectors GF power room"},
{"id": "1f-p1", "type": "house_wall", "x": -17.3, "y": 0, "z": 5.0, "rot": 90, "props": {"w": 25.2, "h": 4.7, "t": 0.2, "mat": "firewall"}, "name": "1F fire wall (hall)"},
{"id": "1f-p2", "type": "house_wall", "x": -18.95, "y": 2.9, "z": 5.0, "props": {"w": 3.3, "h": 4.7, "t": 0.2, "mat": "wall2"}, "name": "1F core wall"},
{"id": "1f-p1-d1", "type": "door", "x": -17.3, "y": 8.5, "z": 5.0, "rot": 90, "props": {"w": 1.2, "h": 2.2, "style": "double", "mat": "doorg"}, "name": "Hall doors"},
{"id": "1f-p1-d2", "type": "door", "x": -17.3, "y": -1.0, "z": 5.0, "rot": 90, "props": {"w": 1.0, "h": 2.1, "style": "panel", "mat": "doorg"}, "name": "Gas room door"},
{"id": "1f-rf", "type": "raised_floor", "x": 1.8, "y": 0, "z": 5.0, "props": {"w": 37.8, "d": 25.2, "h": 0.6, "mat": "dcrf"}, "name": "1F raised floor"},
{"id": "1f-det", "type": "smoke_detectors", "x": 1.8, "y": 0, "z": 5.0, "props": {"w": 37.8, "d": 25.2, "spacing": 6.0, "h": 4.55, "zone": "1F hall"}, "name": "Detectors 1F hall"},
{"id": "1f-det-g", "type": "smoke_detectors", "x": -19.05, "y": -4.9, "z": 5.0, "props": {"w": 3.1, "d": 15.4, "spacing": 6.0, "h": 4.4, "coverage": false, "zone": "1F gas room"}, "name": "Detectors 1F gas room"},
{"id": "1f-vesda-1", "type": "fire_pipe", "x": 1.75, "y": -9, "z": 5.0, "props": {"w": 37.5, "kind": "VESDA", "spacing": 4.0, "h": 4.4}, "name": "VESDA pipe 1"},
{"id": "1f-vesda-2", "type": "fire_pipe", "x": 1.75, "y": -3, "z": 5.0, "props": {"w": 37.5, "kind": "VESDA", "spacing": 4.0, "h": 4.4}, "name": "VESDA pipe 2"},
{"id": "1f-vesda-3", "type": "fire_pipe", "x": 1.75, "y": 3, "z": 5.0, "props": {"w": 37.5, "kind": "VESDA", "spacing": 4.0, "h": 4.4}, "name": "VESDA pipe 3"},
{"id": "1f-vesda-4", "type": "fire_pipe", "x": 1.75, "y": 9, "z": 5.0, "props": {"w": 37.5, "kind": "VESDA", "spacing": 4.0, "h": 4.4}, "name": "VESDA pipe 4"},
{"id": "1f-tray", "type": "cable_tray", "x": 2, "y": 0, "z": 5.0, "props": {"w": 36, "kind": "ladder", "width": 0.6, "h": 4.0, "fill": 60}, "name": "Main power tray"},
{"id": "1f-rel", "type": "fire_device", "x": -17.03, "y": 5.4, "z": 5.0, "rot": 90, "props": {"kind": "release panel", "label": "1F hall"}, "name": "Release panel"},
{"id": "1f-abort", "type": "fire_device", "x": -17.03, "y": 6.4, "z": 5.0, "rot": 90, "props": {"kind": "abort switch"}, "name": "Abort switch"},
{"id": "1f-cp", "type": "fire_device", "x": -17.03, "y": 10.2, "z": 5.0, "rot": 90, "props": {"kind": "call point", "label": "1F hall"}, "name": "Call point"},
{"id": "1f-bc-1", "type": "fire_device", "x": -17.03, "y": -6, "z": 5.0, "rot": 90, "props": {"kind": "beacon"}, "name": "Sounder-beacon"},
{"id": "1f-bc-2", "type": "fire_device", "x": 20.53, "y": 5, "z": 5.0, "rot": 270, "props": {"kind": "beacon"}, "name": "Sounder-beacon"},
{"id": "1f-ext", "type": "fire_device", "x": -16.8, "y": 1.2, "z": 5.0, "rot": 90, "props": {"kind": "extinguisher"}, "name": "Extinguisher"},
{"id": "1f-stairs", "type": "stairs", "x": -19.4, "y": 7.9, "z": 5.0, "props": {"w": 1.4, "run": 6.4, "rise": 4.7, "mat": "concb"}, "name": "Stairs 1F"},
{"id": "1f-crah-n1", "type": "crah", "x": -13, "y": 11.95, "z": 5.0, "props": {"kw": 150, "standby": false}, "name": "CRAH 1-N1"},
{"id": "1f-crah-s1", "type": "crah", "x": -13, "y": -11.95, "z": 5.0, "rot": 180, "props": {"kw": 150, "standby": true}, "name": "CRAH 1-S1"},
{"id": "1f-crah-n2", "type": "crah", "x": -6.5, "y": 11.95, "z": 5.0, "props": {"kw": 150, "standby": false}, "name": "CRAH 1-N2"},
{"id": "1f-crah-s2", "type": "crah", "x": -6.5, "y": -11.95, "z": 5.0, "rot": 180, "props": {"kw": 150, "standby": false}, "name": "CRAH 1-S2"},
{"id": "1f-crah-n3", "type": "crah", "x": 0, "y": 11.95, "z": 5.0, "props": {"kw": 150, "standby": false}, "name": "CRAH 1-N3"},
{"id": "1f-crah-s3", "type": "crah", "x": 0, "y": -11.95, "z": 5.0, "rot": 180, "props": {"kw": 150, "standby": false}, "name": "CRAH 1-S3"},
{"id": "1f-crah-n4", "type": "crah", "x": 6.5, "y": 11.95, "z": 5.0, "props": {"kw": 150, "standby": false}, "name": "CRAH 1-N4"},
{"id": "1f-crah-s4", "type": "crah", "x": 6.5, "y": -11.95, "z": 5.0, "rot": 180, "props": {"kw": 150, "standby": false}, "name": "CRAH 1-S4"},
{"id": "1f-crah-n5", "type": "crah", "x": 13, "y": 11.95, "z": 5.0, "props": {"kw": 150, "standby": true}, "name": "CRAH 1-N5"},
{"id": "1f-crah-s5", "type": "crah", "x": 13, "y": -11.95, "z": 5.0, "rot": 180, "props": {"kw": 150, "standby": false}, "name": "CRAH 1-S5"},
{"id": "1f-pod-A1", "type": "dc_pod", "x": -11.5, "y": 4.3, "z": 5.0, "rot": 90, "props": {"n": 10, "kind": "servers", "kw": 7, "fill": 75, "u": 42, "contain": "cold"}, "name": "Pod A1", "tag": "Pod A1"},
{"id": "1f-pod-A2", "type": "dc_pod", "x": -11.5, "y": -4.3, "z": 5.0, "rot": 90, "props": {"n": 10, "kind": "servers", "kw": 7, "fill": 75, "u": 42, "contain": "cold"}, "name": "Pod A2", "tag": "Pod A2"},
{"id": "1f-bus-AL", "type": "busway", "x": -12.7, "y": 0, "z": 5.0, "rot": 90, "props": {"w": 15, "h": 3.15, "amps": 630}, "name": "Busway"},
{"id": "1f-bus-AR", "type": "busway", "x": -10.3, "y": 0, "z": 5.0, "rot": 90, "props": {"w": 15, "h": 3.15, "amps": 630}, "name": "Busway"},
{"id": "1f-fibre-A", "type": "cable_tray", "x": -11.5, "y": 0, "z": 5.0, "rot": 90, "props": {"w": 15, "kind": "fibre", "width": 0.3, "h": 3.65, "fill": 40}, "name": "Fibre duct"},
{"id": "1f-pod-B1", "type": "dc_pod", "x": -5.5, "y": 4.3, "z": 5.0, "rot": 90, "props": {"n": 10, "kind": "servers", "kw": 7, "fill": 75, "u": 42, "contain": "cold"}, "name": "Pod B1", "tag": "Pod B1"},
{"id": "1f-pod-B2", "type": "dc_pod", "x": -5.5, "y": -4.3, "z": 5.0, "rot": 90, "props": {"n": 10, "kind": "servers", "kw": 7, "fill": 75, "u": 42, "contain": "cold"}, "name": "Pod B2", "tag": "Pod B2"},
{"id": "1f-bus-BL", "type": "busway", "x": -6.7, "y": 0, "z": 5.0, "rot": 90, "props": {"w": 15, "h": 3.15, "amps": 630}, "name": "Busway"},
{"id": "1f-bus-BR", "type": "busway", "x": -4.3, "y": 0, "z": 5.0, "rot": 90, "props": {"w": 15, "h": 3.15, "amps": 630}, "name": "Busway"},
{"id": "1f-fibre-B", "type": "cable_tray", "x": -5.5, "y": 0, "z": 5.0, "rot": 90, "props": {"w": 15, "kind": "fibre", "width": 0.3, "h": 3.65, "fill": 40}, "name": "Fibre duct"},
{"id": "1f-pod-C1", "type": "dc_pod", "x": 0.5, "y": 4.3, "z": 5.0, "rot": 90, "props": {"n": 10, "kind": "storage", "kw": 7, "fill": 75, "u": 42, "contain": "cold"}, "name": "Pod C1", "tag": "Pod C1"},
{"id": "1f-pod-C2", "type": "dc_pod", "x": 0.5, "y": -4.3, "z": 5.0, "rot": 90, "props": {"n": 10, "kind": "storage", "kw": 7, "fill": 75, "u": 42, "contain": "cold"}, "name": "Pod C2", "tag": "Pod C2"},
{"id": "1f-bus-CL", "type": "busway", "x": -0.7, "y": 0, "z": 5.0, "rot": 90, "props": {"w": 15, "h": 3.15, "amps": 630}, "name": "Busway"},
{"id": "1f-bus-CR", "type": "busway", "x": 1.7, "y": 0, "z": 5.0, "rot": 90, "props": {"w": 15, "h": 3.15, "amps": 630}, "name": "Busway"},
{"id": "1f-fibre-C", "type": "cable_tray", "x": 0.5, "y": 0, "z": 5.0, "rot": 90, "props": {"w": 15, "kind": "fibre", "width": 0.3, "h": 3.65, "fill": 40}, "name": "Fibre duct"},
{"id": "1f-pod-D1", "type": "dc_pod", "x": 6.5, "y": 4.3, "z": 5.0, "rot": 90, "props": {"n": 10, "kind": "servers", "kw": 7, "fill": 75, "u": 42, "contain": "cold"}, "name": "Pod D1", "tag": "Pod D1"},
{"id": "1f-pod-D2", "type": "dc_pod", "x": 6.5, "y": -4.3, "z": 5.0, "rot": 90, "props": {"n": 10, "kind": "servers", "kw": 7, "fill": 75, "u": 42, "contain": "cold"}, "name": "Pod D2", "tag": "Pod D2"},
{"id": "1f-bus-DL", "type": "busway", "x": 5.3, "y": 0, "z": 5.0, "rot": 90, "props": {"w": 15, "h": 3.15, "amps": 630}, "name": "Busway"},
{"id": "1f-bus-DR", "type": "busway", "x": 7.7, "y": 0, "z": 5.0, "rot": 90, "props": {"w": 15, "h": 3.15, "amps": 630}, "name": "Busway"},
{"id": "1f-fibre-D", "type": "cable_tray", "x": 6.5, "y": 0, "z": 5.0, "rot": 90, "props": {"w": 15, "kind": "fibre", "width": 0.3, "h": 3.65, "fill": 40}, "name": "Fibre duct"},
{"id": "1f-net", "type": "rack_row", "x": 13.6, "y": 0, "z": 5.0, "rot": 270, "props": {"n": 8, "kind": "network", "kw": 4, "fill": 80, "doors": "glass"}, "name": "Network row (meet-me)", "tag": "Meet-me room"},
{"id": "1f-pdu", "type": "power_cab", "x": 17.2, "y": 0, "z": 5.0, "rot": 270, "props": {"kind": "PDU", "n": 6, "kw": 300}, "name": "PDUs 1F"},
{"id": "1f-gaspipe-1", "type": "fire_pipe", "x": 1.75, "y": -6, "z": 5.0, "props": {"w": 37.5, "kind": "gas", "spacing": 6.2, "h": 4.25}, "name": "Gas discharge pipe"},
{"id": "1f-gaspipe-2", "type": "fire_pipe", "x": 1.75, "y": 6, "z": 5.0, "props": {"w": 37.5, "kind": "gas", "spacing": 6.2, "h": 4.25}, "name": "Gas discharge pipe"},
{"id": "1f-gas", "type": "gas_bank", "x": -20.45, "y": -5.4, "z": 5.0, "rot": 90, "props": {"agent": "Novec 1230", "n": 18, "litres": 140}, "name": "Novec 1230 bank (hall A)", "tag": "Novec 1230"},
{"id": "1f-tech", "type": "person", "x": 3.5, "y": 0.6, "z": 5.0, "props": {"cls": "hivis"}, "name": "Technician"},
{"id": "2f-p1", "type": "house_wall", "x": -17.3, "y": 0, "z": 10.0, "rot": 90, "props": {"w": 25.2, "h": 4.7, "t": 0.2, "mat": "firewall"}, "name": "2F fire wall (hall)"},
{"id": "2f-p2", "type": "house_wall", "x": -18.95, "y": 2.9, "z": 10.0, "props": {"w": 3.3, "h": 4.7, "t": 0.2, "mat": "wall2"}, "name": "2F core wall"},
{"id": "2f-p1-d1", "type": "door", "x": -17.3, "y": 8.5, "z": 10.0, "rot": 90, "props": {"w": 1.2, "h": 2.2, "style": "double", "mat": "doorg"}, "name": "Hall doors"},
{"id": "2f-p1-d2", "type": "door", "x": -17.3, "y": -1.0, "z": 10.0, "rot": 90, "props": {"w": 1.0, "h": 2.1, "style": "panel", "mat": "doorg"}, "name": "Gas room door"},
{"id": "2f-rf", "type": "raised_floor", "x": 1.8, "y": 0, "z": 10.0, "props": {"w": 37.8, "d": 25.2, "h": 0.6, "mat": "dcrf2"}, "name": "2F raised floor"},
{"id": "2f-det", "type": "smoke_detectors", "x": 1.8, "y": 0, "z": 10.0, "props": {"w": 37.8, "d": 25.2, "spacing": 6.0, "h": 4.55, "zone": "2F hall"}, "name": "Detectors 2F hall"},
{"id": "2f-det-g", "type": "smoke_detectors", "x": -19.05, "y": -4.9, "z": 10.0, "props": {"w": 3.1, "d": 15.4, "spacing": 6.0, "h": 4.4, "coverage": false, "zone": "2F gas room"}, "name": "Detectors 2F gas room"},
{"id": "2f-vesda-1", "type": "fire_pipe", "x": 1.75, "y": -9, "z": 10.0, "props": {"w": 37.5, "kind": "VESDA", "spacing": 4.0, "h": 4.4}, "name": "VESDA pipe 1"},
{"id": "2f-vesda-2", "type": "fire_pipe", "x": 1.75, "y": -3, "z": 10.0, "props": {"w": 37.5, "kind": "VESDA", "spacing": 4.0, "h": 4.4}, "name": "VESDA pipe 2"},
{"id": "2f-vesda-3", "type": "fire_pipe", "x": 1.75, "y": 3, "z": 10.0, "props": {"w": 37.5, "kind": "VESDA", "spacing": 4.0, "h": 4.4}, "name": "VESDA pipe 3"},
{"id": "2f-vesda-4", "type": "fire_pipe", "x": 1.75, "y": 9, "z": 10.0, "props": {"w": 37.5, "kind": "VESDA", "spacing": 4.0, "h": 4.4}, "name": "VESDA pipe 4"},
{"id": "2f-tray", "type": "cable_tray", "x": 2, "y": 0, "z": 10.0, "props": {"w": 36, "kind": "ladder", "width": 0.6, "h": 4.0, "fill": 60}, "name": "Main power tray"},
{"id": "2f-rel", "type": "fire_device", "x": -17.03, "y": 5.4, "z": 10.0, "rot": 90, "props": {"kind": "release panel", "label": "2F hall"}, "name": "Release panel"},
{"id": "2f-abort", "type": "fire_device", "x": -17.03, "y": 6.4, "z": 10.0, "rot": 90, "props": {"kind": "abort switch"}, "name": "Abort switch"},
{"id": "2f-cp", "type": "fire_device", "x": -17.03, "y": 10.2, "z": 10.0, "rot": 90, "props": {"kind": "call point", "label": "2F hall"}, "name": "Call point"},
{"id": "2f-bc-1", "type": "fire_device", "x": -17.03, "y": -6, "z": 10.0, "rot": 90, "props": {"kind": "beacon"}, "name": "Sounder-beacon"},
{"id": "2f-bc-2", "type": "fire_device", "x": 20.53, "y": 5, "z": 10.0, "rot": 270, "props": {"kind": "beacon"}, "name": "Sounder-beacon"},
{"id": "2f-ext", "type": "fire_device", "x": -16.8, "y": 1.2, "z": 10.0, "rot": 90, "props": {"kind": "extinguisher"}, "name": "Extinguisher"},
{"id": "2f-stairs", "type": "stairs", "x": -19.4, "y": 7.9, "z": 10.0, "props": {"w": 1.4, "run": 6.4, "rise": 4.7, "mat": "concb"}, "name": "Stairs 2F"},
{"id": "2f-gpu-11", "type": "dc_pod", "x": -11.5, "y": 4.2, "z": 10.0, "rot": 90, "props": {"n": 8, "kind": "GPU", "kw": 30, "fill": 85, "u": 48, "contain": "hot", "inrow": 2, "inrow_kw": 90, "duct": true, "duct_h": 4.2, "doors": "glass"}, "name": "GPU pod 11", "tag": "GPU 11"},
{"id": "2f-gpu-12", "type": "dc_pod", "x": -11.5, "y": -4.2, "z": 10.0, "rot": 90, "props": {"n": 8, "kind": "GPU", "kw": 30, "fill": 85, "u": 48, "contain": "hot", "inrow": 2, "inrow_kw": 90, "duct": true, "duct_h": 4.2, "doors": "glass"}, "name": "GPU pod 12", "tag": "GPU 12"},
{"id": "2f-bus-1L", "type": "busway", "x": -12.7, "y": 0, "z": 10.0, "rot": 90, "props": {"w": 14, "h": 3.2, "amps": 1250}, "name": "Busway"},
{"id": "2f-bus-1R", "type": "busway", "x": -10.3, "y": 0, "z": 10.0, "rot": 90, "props": {"w": 14, "h": 3.2, "amps": 1250}, "name": "Busway"},
{"id": "2f-gpu-21", "type": "dc_pod", "x": -5.5, "y": 4.2, "z": 10.0, "rot": 90, "props": {"n": 8, "kind": "GPU", "kw": 30, "fill": 85, "u": 48, "contain": "hot", "inrow": 2, "inrow_kw": 90, "duct": false, "duct_h": 4.2, "doors": "glass"}, "name": "GPU pod 21", "tag": "GPU 21"},
{"id": "2f-gpu-22", "type": "dc_pod", "x": -5.5, "y": -4.2, "z": 10.0, "rot": 90, "props": {"n": 8, "kind": "GPU", "kw": 30, "fill": 85, "u": 48, "contain": "hot", "inrow": 2, "inrow_kw": 90, "duct": false, "duct_h": 4.2, "doors": "glass"}, "name": "GPU pod 22", "tag": "GPU 22"},
{"id": "2f-bus-2L", "type": "busway", "x": -6.7, "y": 0, "z": 10.0, "rot": 90, "props": {"w": 14, "h": 3.2, "amps": 1250}, "name": "Busway"},
{"id": "2f-bus-2R", "type": "busway", "x": -4.3, "y": 0, "z": 10.0, "rot": 90, "props": {"w": 14, "h": 3.2, "amps": 1250}, "name": "Busway"},
{"id": "2f-cage-1", "type": "dc_cage", "x": 4.6, "y": 4.5, "z": 10.0, "props": {"w": 7.2, "d": 5.7}, "name": "Cage 1", "tag": "Cage 1"},
{"id": "2f-colo-1", "type": "dc_pod", "x": 4.6, "y": 4.5, "z": 10.0, "props": {"n": 8, "kind": "servers", "kw": 8, "fill": 55, "contain": "none", "doors": "solid"}, "name": "Cage 1 racks"},
{"id": "2f-fibre-1", "type": "cable_tray", "x": 4.6, "y": 4.5, "z": 10.0, "props": {"w": 7.2, "kind": "fibre", "width": 0.3, "h": 3.65, "fill": 30}, "name": "Fibre duct"},
{"id": "2f-cage-2", "type": "dc_cage", "x": 4.6, "y": -4.5, "z": 10.0, "props": {"w": 7.2, "d": 5.7}, "name": "Cage 2", "tag": "Cage 2"},
{"id": "2f-colo-2", "type": "dc_pod", "x": 4.6, "y": -4.5, "z": 10.0, "props": {"n": 8, "kind": "network", "kw": 8, "fill": 65, "contain": "none", "doors": "mesh"}, "name": "Cage 2 racks"},
{"id": "2f-fibre-2", "type": "cable_tray", "x": 4.6, "y": -4.5, "z": 10.0, "props": {"w": 7.2, "kind": "fibre", "width": 0.3, "h": 3.65, "fill": 30}, "name": "Fibre duct"},
{"id": "2f-cage-3", "type": "dc_cage", "x": 13.6, "y": 4.5, "z": 10.0, "props": {"w": 7.2, "d": 5.7}, "name": "Cage 3", "tag": "Cage 3"},
{"id": "2f-colo-3", "type": "dc_pod", "x": 13.6, "y": 4.5, "z": 10.0, "props": {"n": 8, "kind": "storage", "kw": 8, "fill": 75, "contain": "none", "doors": "solid"}, "name": "Cage 3 racks"},
{"id": "2f-fibre-3", "type": "cable_tray", "x": 13.6, "y": 4.5, "z": 10.0, "props": {"w": 7.2, "kind": "fibre", "width": 0.3, "h": 3.65, "fill": 30}, "name": "Fibre duct"},
{"id": "2f-cage-4", "type": "dc_cage", "x": 13.6, "y": -4.5, "z": 10.0, "props": {"w": 7.2, "d": 5.7}, "name": "Cage 4", "tag": "Cage 4"},
{"id": "2f-colo-4", "type": "dc_pod", "x": 13.6, "y": -4.5, "z": 10.0, "props": {"n": 8, "kind": "servers", "kw": 8, "fill": 85, "contain": "none", "doors": "mesh"}, "name": "Cage 4 racks"},
{"id": "2f-fibre-4", "type": "cable_tray", "x": 13.6, "y": -4.5, "z": 10.0, "props": {"w": 7.2, "kind": "fibre", "width": 0.3, "h": 3.65, "fill": 30}, "name": "Fibre duct"},
{"id": "2f-crah-n1", "type": "crah", "x": 2, "y": 11.95, "z": 10.0, "props": {"kw": 150, "standby": false}, "name": "CRAH 2-N1"},
{"id": "2f-crah-s1", "type": "crah", "x": 2, "y": -11.95, "z": 10.0, "rot": 180, "props": {"kw": 150}, "name": "CRAH 2-S1"},
{"id": "2f-crah-n2", "type": "crah", "x": 9, "y": 11.95, "z": 10.0, "props": {"kw": 150, "standby": false}, "name": "CRAH 2-N2"},
{"id": "2f-crah-s2", "type": "crah", "x": 9, "y": -11.95, "z": 10.0, "rot": 180, "props": {"kw": 150}, "name": "CRAH 2-S2"},
{"id": "2f-crah-n3", "type": "crah", "x": 16, "y": 11.95, "z": 10.0, "props": {"kw": 150, "standby": true}, "name": "CRAH 2-N3"},
{"id": "2f-crah-s3", "type": "crah", "x": 16, "y": -11.95, "z": 10.0, "rot": 180, "props": {"kw": 150}, "name": "CRAH 2-S3"},
{"id": "2f-spr-1", "type": "fire_pipe", "x": 9.5, "y": -8.5, "z": 10.0, "props": {"w": 21, "kind": "sprinkler", "spacing": 3.0, "h": 4.25, "unit": false}, "name": "Pre-action sprinkler pipe"},
{"id": "2f-spr-2", "type": "fire_pipe", "x": 9.5, "y": 8.5, "z": 10.0, "props": {"w": 21, "kind": "sprinkler", "spacing": 3.0, "h": 4.25, "unit": false}, "name": "Pre-action sprinkler pipe"},
{"id": "2f-gas", "type": "gas_bank", "x": -20.45, "y": -5.4, "z": 10.0, "rot": 90, "props": {"agent": "FM-200", "n": 18, "litres": 140}, "name": "FM-200 bank (hall B)", "tag": "FM-200"},
{"id": "2f-tech", "type": "person", "x": -2.0, "y": 1.0, "z": 10.0, "props": {"cls": "guest2"}, "name": "Engineer"},
{"id": "chiller-1", "type": "chiller", "x": -12, "y": -7, "z": 15.0, "props": {"w": 9, "kw": 1000}, "name": "Chiller 1", "tag": "CH1"},
{"id": "chiller-2", "type": "chiller", "x": -1, "y": -7, "z": 15.0, "props": {"w": 9, "kw": 1000}, "name": "Chiller 2"},
{"id": "chiller-3", "type": "chiller", "x": 10, "y": -7, "z": 15.0, "props": {"w": 9, "kw": 1000}, "name": "Chiller 3"},
{"id": "chiller-4", "type": "chiller", "x": -6.5, "y": 6, "z": 15.0, "props": {"w": 9, "kw": 1000}, "name": "Chiller 4"},
{"id": "chiller-5", "type": "chiller", "x": 4.5, "y": 6, "z": 15.0, "props": {"w": 9, "kw": 1000}, "name": "Chiller 5"},
{"id": "dry", "type": "chiller", "x": 15.5, "y": 6, "z": 15.0, "props": {"w": 8, "kw": 600, "kind": "adiabatic"}, "name": "Adiabatic cooler (GPU loop)"},
{"id": "roof-pipes", "type": "pipe_run", "x": 0, "y": -0.5, "z": 15.0, "props": {"w": 38, "dn": 0.35, "h": 0.9}, "name": "Chilled water headers"}
]
}
+1
View File
@@ -151,6 +151,7 @@ def main():
(web / "factory.json").write_text(json.dumps(demo_files("factory"), ensure_ascii=False), encoding="utf-8") (web / "factory.json").write_text(json.dumps(demo_files("factory"), ensure_ascii=False), encoding="utf-8")
(web / "streets.json").write_text(json.dumps(demo_files("streets"), ensure_ascii=False), encoding="utf-8") (web / "streets.json").write_text(json.dumps(demo_files("streets"), ensure_ascii=False), encoding="utf-8")
(web / "country.json").write_text(json.dumps(demo_files("country"), ensure_ascii=False), encoding="utf-8") (web / "country.json").write_text(json.dumps(demo_files("country"), ensure_ascii=False), encoding="utf-8")
(web / "datacentre.json").write_text(json.dumps(demo_files("datacentre"), ensure_ascii=False), encoding="utf-8")
h = hashlib.sha256() h = hashlib.sha256()
for p in sorted((out / "assets").rglob("*")): for p in sorted((out / "assets").rglob("*")):
+28
View File
@@ -404,6 +404,34 @@ try:
check(len(g) == 1 and members == ["bush-1", "bush-2"] and hid == 0 and not any(o["type"] == "group" for o in doc(pg)["objects"]), check(len(g) == 1 and members == ["bush-1", "bush-2"] and hid == 0 and not any(o["type"] == "group" for o in doc(pg)["objects"]),
f"web Studio groups: {members} in {g[0]['id'] if g else None}, hidden with it, undone") f"web Studio groups: {members} in {g[0]['id'] if g else None}, hidden with it, undone")
# ---------------------------------------------------------------- the data centre demo: storeys, racks, cooling, fire safety
pg.locator("#wb-projects").click()
pg.locator("#wb-ndc").click()
pg.wait_for_load_state()
ready(pg)
pg.wait_for_timeout(800)
nd = len(doc(pg)["objects"])
ids_in = lambda sel: pg.evaluate(f"() => [...new Set([...document.querySelectorAll(`{sel}`)].map(e => e.dataset.obj))]")
pods = [i for i in ids_in(".iso-svg [data-obj]") if i.startswith("1f-pod")]
overhead = ids_in(".iso-svg [data-layer='ceiling'] [data-obj]")
# it opens on 1F: 8 pods; trays, busways, VESDA and gas pipes, detectors sort in the ceiling layer
check(title(pg) == "isokit data centre" and nd == 179 and len(pods) == 8 and len(overhead) == 21
and pg.evaluate("() => __studio.S.warnings.length") == 0,
f"Data centre demo opens on 1F ({nd} objects, {len(pods)} pods, {len(overhead)} overhead runs, no warnings)")
irow = lambda id_, k: pg.evaluate(f"() => Object.fromEntries((__studio.view.info('{id_}') || {{rows: []}}).rows)['{k}'] || ''")
red, gas = irow("1f-crah-n1", "Redundancy"), irow("1f-gas", "Status")
check(red == "N+2, 348 kW spare" and gas == "enough, 3 % spare", f"1F cooling {red}; Novec bank {gas}")
shot(pg, "web-datacentre.jpg")
# GF cut open: the power room shows, the halls above are left out
pg.evaluate("__studio.view.setLevel('gf')")
pg.wait_for_timeout(1200)
gf = ids_in(".iso-svg [data-obj]")
run = irow("batt-1", "Runtime")
check("ups" in gf and not any(i.startswith("1f-") for i in gf) and run == "about 21.7 min at 3,584 kW IT",
f"GF shows the power room and none of 1F; batteries {run}")
pg.evaluate("__studio.view.setLevel('1f')")
pg.wait_for_timeout(600)
# ---------------------------------------------------------------- a system asking for less motion # ---------------------------------------------------------------- a system asking for less motion
# (Windows: Animation effects off): the view starts still, but the button stays and turns it on # (Windows: Animation effects off): the view starts still, but the button stays and turns it on
rctx = b.new_context(viewport={"width": 1200, "height": 800}, reduced_motion="reduce") rctx = b.new_context(viewport={"width": 1200, "height": 800}, reduced_motion="reduce")
+2 -1
View File
@@ -367,7 +367,7 @@ function injectUI() {
'<h3>Projects</h3>' + '<h3>Projects</h3>' +
'<div class="wb-cur" id="wb-cur"></div>' + '<div class="wb-cur" id="wb-cur"></div>' +
'<ul class="wb-plist" id="wb-plist"></ul>' + '<ul class="wb-plist" id="wb-plist"></ul>' +
'<div class="wb-new"><input id="wb-ntitle" placeholder="Title of a new project" autocomplete="off"><button type="button" id="wb-ncreate">New project</button><button type="button" id="wb-ndemo" title="A fresh copy of the demo scene">Demo copy</button><button type="button" id="wb-nhouse" title="A house built from parts: walls, windows, doors, floors, furniture">House demo</button><button type="button" id="wb-nfactory" title="Presses making goods at a set rate, conveyors carrying them">Factory demo</button><button type="button" id="wb-nstreets" title="Streets with sidewalks, a rounded corner, junctions and a roundabout">Streets demo</button><button type="button" id="wb-ncountry" title="Elephants, deer, boar, beehives and a wildflower meadow; a lane, fence and hedgerow at an angle">Country demo</button></div>' + '<div class="wb-new"><input id="wb-ntitle" placeholder="Title of a new project" autocomplete="off"><button type="button" id="wb-ncreate">New project</button><button type="button" id="wb-ndemo" title="A fresh copy of the demo scene">Demo copy</button><button type="button" id="wb-nhouse" title="A house built from parts: walls, windows, doors, floors, furniture">House demo</button><button type="button" id="wb-nfactory" title="Presses making goods at a set rate, conveyors carrying them">Factory demo</button><button type="button" id="wb-nstreets" title="Streets with sidewalks, a rounded corner, junctions and a roundabout">Streets demo</button><button type="button" id="wb-ncountry" title="Elephants, deer, boar, beehives and a wildflower meadow; a lane, fence and hedgerow at an angle">Country demo</button><button type="button" id="wb-ndc" title="A three-storey data centre: power room, a hall with cold aisle containment, GPU pods with hot aisle containment, cages, cooling, fire detection and suppression">Data centre demo</button></div>' +
'<div class="wb-drop" id="wb-drop">Import: drop a project folder or .zip here, or <button type="button" id="wb-izip" title="Import a project .zip into this browser">.zip…</button><button type="button" id="wb-idir" title="Copy a project folder into this browser">folder…</button>' + '<div class="wb-drop" id="wb-drop">Import: drop a project folder or .zip here, or <button type="button" id="wb-izip" title="Import a project .zip into this browser">.zip…</button><button type="button" id="wb-idir" title="Copy a project folder into this browser">folder…</button>' +
(canOpenFolder ? '<span class="st-grow"></span><button type="button" id="wb-ofolder" title="Edit the files in the folder on disk directly; changes made there show up live">Open folder live…</button>' : "") + "</div>" + (canOpenFolder ? '<span class="st-grow"></span><button type="button" id="wb-ofolder" title="Edit the files in the folder on disk directly; changes made there show up live">Open folder live…</button>' : "") + "</div>" +
'<p class="st-help">Projects are kept in this browser. ' + (canOpenFolder ? "A folder opened live is edited on disk, so agents (<b>isokit mcp</b>), editors and git see the same files." : "To edit a folder on disk directly, use Chrome or Edge.") + "</p>" + '<p class="st-help">Projects are kept in this browser. ' + (canOpenFolder ? "A folder opened live is edited on disk, so agents (<b>isokit mcp</b>), editors and git see the same files." : "To edit a folder on disk directly, use Chrome or Edge.") + "</p>" +
@@ -421,6 +421,7 @@ function wireProjects() {
$("wb-nfactory").addEventListener("click", async function () { openProject((await createProject(await demoFiles("factory.json"))).id); }); $("wb-nfactory").addEventListener("click", async function () { openProject((await createProject(await demoFiles("factory.json"))).id); });
$("wb-nstreets").addEventListener("click", async function () { openProject((await createProject(await demoFiles("streets.json"))).id); }); $("wb-nstreets").addEventListener("click", async function () { openProject((await createProject(await demoFiles("streets.json"))).id); });
$("wb-ncountry").addEventListener("click", async function () { openProject((await createProject(await demoFiles("country.json"))).id); }); $("wb-ncountry").addEventListener("click", async function () { openProject((await createProject(await demoFiles("country.json"))).id); });
$("wb-ndc").addEventListener("click", async function () { openProject((await createProject(await demoFiles("datacentre.json"))).id); });
$("wb-izip").addEventListener("click", function () { $("wb-fzip").click(); }); $("wb-izip").addEventListener("click", function () { $("wb-fzip").click(); });
$("wb-idir").addEventListener("click", function () { $("wb-fdir").click(); }); $("wb-idir").addEventListener("click", function () { $("wb-fdir").click(); });
$("wb-fzip").addEventListener("change", function () { var f = this.files[0]; if (f) importZip(f); this.value = ""; }); $("wb-fzip").addEventListener("change", function () { var f = this.files[0]; if (f) importZip(f); this.value = ""; });