isokit: engine core, theme and Rhai scene runtime with prefab library
This commit is contained in:
commit
95c80ec6b1
32 files changed
+5896
No files matched your search
@@ -0,0 +1,224 @@
|
||||
// isokit/building: buildings with storeys, windows, roofs and cut-open
|
||||
// levels, plus carports and sheds.
|
||||
//
|
||||
// Levels: "gf", "1f", "2f", ... cut a building open at that storey (lower
|
||||
// storeys closed, the storey's floor shown, walls facing the viewer cut low);
|
||||
// any other level (e.g. "roof", "site") shows it closed. A level above the
|
||||
// building's top storey shows it ghosted.
|
||||
|
||||
fn wall_materials() { ["render", "render2", "wall", "wall2", "office", "yardb", "plinth", "wallp", "concb", "firewall"] }
|
||||
fn roof_materials() { ["roof", "roof2", "roof3", "rooft", "roofcap", "canopy"] }
|
||||
fn floor_materials() { ["z-floor", "z-living", "z-offices", "z-kitchen", "z-shop", "z-lab", "z-wh", "z-plant", "z-garage", "z-hall", "z-sport"] }
|
||||
|
||||
fn meta_building() {
|
||||
#{ label: "Building", category: "Buildings", icon: "🏢",
|
||||
help: "Storeys with windows, a door on the front (local -y) face, a flat or gable roof and optional solar. The GF / 1F levels cut it open.",
|
||||
props: [
|
||||
#{ key: "w", type: "float", def: 20.0, min: 2.0, max: 300.0, step: 0.5, label: "Width (m)" },
|
||||
#{ key: "d", type: "float", def: 12.0, min: 2.0, max: 300.0, step: 0.5, label: "Depth (m)" },
|
||||
#{ key: "floors", type: "int", def: 2, min: 1, max: 20, step: 1, label: "Storeys" },
|
||||
#{ key: "floor_h", type: "float", def: 3.4, min: 2.4, max: 12.0, step: 0.1, label: "Storey height (m)" },
|
||||
#{ key: "plinth", type: "float", def: 0.4, min: 0.0, max: 1.5, step: 0.05, label: "Plinth (m)" },
|
||||
#{ key: "mat", type: "material", def: "render", choices: wall_materials(), label: "Walls" },
|
||||
#{ key: "roof", type: "choice", def: "flat", choices: ["flat", "gable"], label: "Roof" },
|
||||
#{ key: "roof_mat", type: "material", def: "roof2", choices: roof_materials(), label: "Roof colour" },
|
||||
#{ key: "rise", type: "float", def: 2.2, min: 0.5, max: 8.0, step: 0.1, label: "Gable rise (m)" },
|
||||
#{ key: "windows", type: "bool", def: true, label: "Windows" },
|
||||
#{ key: "lit", type: "bool", def: true, label: "Lit windows" },
|
||||
#{ key: "door", type: "bool", def: true, label: "Front door" },
|
||||
#{ key: "pv", type: "bool", def: false, label: "Solar panels" },
|
||||
#{ key: "ac", type: "bool", def: false, label: "Roof AC units (flat roof)" },
|
||||
#{ key: "floor_mat", type: "material", def: "z-floor", choices: floor_materials(), label: "Floor (cut-open levels)" },
|
||||
] }
|
||||
}
|
||||
|
||||
fn prefab_building(o) {
|
||||
let p = o.p;
|
||||
let w = p.w / 2.0;
|
||||
let d = p.d / 2.0;
|
||||
let n = p.floors;
|
||||
let H = p.floor_h;
|
||||
let z0 = p.plinth;
|
||||
let k = floor_of(o.level);
|
||||
if k < 0 || k >= n {
|
||||
let s = if k >= n { sprite(o.id, "dim") } else { sprite() };
|
||||
s.box(-w, -d, w, d, 0.0, z0, "plinth");
|
||||
s.box(-w, -d, w, d, z0, z0 + n * H, p.mat, #{ top: p.roof_mat });
|
||||
building_windows(s, p, 0, n);
|
||||
if p.roof == "gable" { gable_roof(s, p, z0 + n * H); } else { flat_roof_kit(s, p, z0 + n * H); }
|
||||
return;
|
||||
}
|
||||
// cut open at storey k: lower storeys closed, the floor of storey k, cut walls
|
||||
let zf = z0 + k * H;
|
||||
if k == 0 { layer("floor"); }
|
||||
let f = sprite();
|
||||
f.box(-w, -d, w, d, 0.0, z0, "plinth");
|
||||
if k > 0 {
|
||||
f.box(-w, -d, w, d, z0, zf, p.mat, #{ top: p.floor_mat });
|
||||
building_windows(f, p, 0, k);
|
||||
}
|
||||
f.flat(rect(-w, -d, w, d), zf + 0.005, p.floor_mat + " t");
|
||||
layer("main");
|
||||
// walls start just above the floor so the depth sort draws them over it
|
||||
cut_walls(-w, -d, w, d, zf + 0.02, 0.9, fmin(H - 0.4, 2.6), 0.25, p.mat, []);
|
||||
}
|
||||
|
||||
/// Windows (and the front door) on all four faces for storeys from..to.
|
||||
fn building_windows(s, p, from, to) {
|
||||
let w = p.w / 2.0;
|
||||
let d = p.d / 2.0;
|
||||
let sd = sides(-w, -d, w, d);
|
||||
for f in ["S", "E", "N", "W"] {
|
||||
let e = sd[f];
|
||||
let L = if f == "S" || f == "N" { p.w } else { p.d };
|
||||
let dark = [];
|
||||
let lit = [];
|
||||
if p.windows {
|
||||
for k in from..to {
|
||||
let z = p.plinth + k * p.floor_h;
|
||||
let gap = if f == "S" && k == 0 && p.door { L / 2.0 } else { -1.0 };
|
||||
let i = 0;
|
||||
for q in win_list(L, z, 0.9, fmin(1.5, p.floor_h - 1.3), 1.6, 3.4, gap) {
|
||||
if p.lit && hash(i * 7.3 + k * 3.1 + L + f.len()) > 0.55 { lit.push(q); } else { dark.push(q); }
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
if dark.len() > 0 { s.open(e[0], e[1], dark, "win"); }
|
||||
if lit.len() > 0 { s.open(e[0], e[1], lit, "win lit"); }
|
||||
if f == "S" && p.door && from == 0 {
|
||||
s.open(e[0], e[1], [[L / 2.0 - 0.9, L / 2.0 + 0.9, p.plinth, p.plinth + 2.3]], "door");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Gable roof with the ridge along local x, eaves on the -y and +y sides;
|
||||
/// solar modules on the -y slope.
|
||||
fn gable_roof(s, p, ze) {
|
||||
let a = p.w / 2.0;
|
||||
let w = a + 0.4;
|
||||
let d = p.d / 2.0;
|
||||
let e = 0.6;
|
||||
let r = p.rise;
|
||||
let k = r / d;
|
||||
let za = ze - e * k;
|
||||
let zr = ze + r;
|
||||
// gable ends first, then the two roof slabs over them
|
||||
s.face([[a, -d, ze], [a, d, ze], [a, 0.0, zr - 0.05]], p.mat);
|
||||
s.face([[-a, d, ze], [-a, -d, ze], [-a, 0.0, zr - 0.05]], p.mat);
|
||||
s.prism(rect(-w, -d - e, w, 0.0), [za - 0.2, za - 0.2, zr - 0.2, zr - 0.2], [za, za, zr, zr], p.roof_mat, #{ side: "fascia" });
|
||||
s.prism(rect(-w, 0.0, w, d + e), [zr - 0.2, zr - 0.2, za - 0.2, za - 0.2], [zr, zr, za, za], p.roof_mat, #{ side: "fascia" });
|
||||
if p.pv {
|
||||
let cw = 1.13;
|
||||
let rh = 1.7;
|
||||
let cols = ((p.w - 1.0) / cw).floor().to_int();
|
||||
let rows = ((d - 0.4) / rh).floor().to_int();
|
||||
let u0 = -cols * cw / 2.0;
|
||||
let mods = [];
|
||||
for i in 0..rows {
|
||||
for j in 0..cols {
|
||||
let va = -(0.3 + i * rh + 0.03);
|
||||
let vb = -(0.3 + (i + 1) * rh - 0.03);
|
||||
let ua = u0 + j * cw + 0.03;
|
||||
let ub = u0 + (j + 1) * cw - 0.03;
|
||||
let zA = zr + va * k + 0.08;
|
||||
let zB = zr + vb * k + 0.08;
|
||||
mods.push([[ua, va, zA], [ub, va, zA], [ub, vb, zB], [ua, vb, zB]]);
|
||||
}
|
||||
}
|
||||
if mods.len() > 0 { s.poly(mods, "pv"); }
|
||||
}
|
||||
}
|
||||
|
||||
/// Parapet, solar rows and AC units on a flat roof at height zt, drawn back
|
||||
/// to front.
|
||||
fn flat_roof_kit(s, p, zt) {
|
||||
let w = p.w / 2.0;
|
||||
let d = p.d / 2.0;
|
||||
let t = 0.2;
|
||||
let ph = 0.5;
|
||||
let mat = p.mat;
|
||||
let items = [
|
||||
[0.0, d, |q| q.box(-w, d - t, w, d, zt, zt + ph, mat, #{ top: "cut" })],
|
||||
[-w, 0.0, |q| q.box(-w, -d + t, -w + t, d - t, zt, zt + ph, mat, #{ top: "cut" })],
|
||||
[w, 0.0, |q| q.box(w - t, -d + t, w, d - t, zt, zt + ph, mat, #{ top: "cut" })],
|
||||
[0.0, -d, |q| q.box(-w, -d, w, -d + t, zt, zt + ph, mat, #{ top: "cut" })],
|
||||
];
|
||||
if p.pv && p.w > 4.0 && p.d > 4.0 {
|
||||
let y = -d + 1.2;
|
||||
while y + 1.0 < d - 1.2 {
|
||||
let y0 = y;
|
||||
let x0 = -w + 1.0;
|
||||
let x1 = w - 1.0;
|
||||
items.push([0.0, y0 + 0.5, |q| q.poly([[[x0, y0, zt + 0.35], [x1, y0, zt + 0.35], [x1, y0 + 1.0, zt + 0.85], [x0, y0 + 1.0, zt + 0.85]]], "pv")]);
|
||||
y += 2.2;
|
||||
}
|
||||
}
|
||||
if p.ac && p.w > 6.0 && p.d > 4.0 {
|
||||
for i in 0..2 {
|
||||
let x = w - 2.2 - i * 2.0;
|
||||
let y = d - 1.4;
|
||||
items.push([x, y, |q| { q.box(x - 0.7, y - 0.5, x + 0.7, y + 0.5, zt, zt + 0.9, "ac"); q.fan(x, y, zt + 0.9, 0.38); }]);
|
||||
}
|
||||
}
|
||||
s.seq(items);
|
||||
}
|
||||
|
||||
fn meta_carport() {
|
||||
#{ label: "Carport", category: "Buildings", icon: "⌂",
|
||||
help: "A canopy on posts with cars parked under it.",
|
||||
props: [
|
||||
#{ key: "w", type: "float", def: 6.0, min: 2.5, max: 60.0, step: 0.5, label: "Width (m)" },
|
||||
#{ key: "d", type: "float", def: 5.5, min: 2.5, max: 12.0, step: 0.5, label: "Depth (m)" },
|
||||
#{ key: "h", type: "float", def: 2.6, min: 2.2, max: 5.0, step: 0.1, label: "Height (m)" },
|
||||
#{ key: "cars", type: "int", def: 2, min: 0, max: 20, step: 1, label: "Cars" },
|
||||
#{ key: "pv", type: "bool", def: false, label: "Solar on the canopy" },
|
||||
] }
|
||||
}
|
||||
fn prefab_carport(o) {
|
||||
let p = o.p;
|
||||
let w = p.w / 2.0;
|
||||
let d = p.d / 2.0;
|
||||
let h = p.h;
|
||||
let s = sprite();
|
||||
// cars and posts back to front inside the canopy's sprite, then the roof over all of them
|
||||
let items = [];
|
||||
for c in [[-w + 0.1, -d + 0.1], [w - 0.1, -d + 0.1], [w - 0.1, d - 0.1], [-w + 0.1, d - 0.1]] {
|
||||
let x = c[0];
|
||||
let y = c[1];
|
||||
items.push([x, y, |q| q.box(x - 0.08, y - 0.08, x + 0.08, y + 0.08, 0.0, h, "steel")]);
|
||||
}
|
||||
let cols = ["car1", "car2", "car3", "car4"];
|
||||
let n = p.cars;
|
||||
for i in 0..n {
|
||||
let x = -w + (i + 0.5) * p.w / n;
|
||||
let col = cols[(i + (hash(o.x + o.y) * 4.0).floor().to_int()) % 4];
|
||||
items.push([x, 0.0, |q| q.car(x - 0.9, -2.2, x + 0.9, 2.2, col)]);
|
||||
}
|
||||
s.seq(items);
|
||||
s.box(-w, -d, w, d, h, h + 0.15, "canopy");
|
||||
if p.pv {
|
||||
s.poly([[[-w + 0.2, -d + 0.2, h + 0.2], [w - 0.2, -d + 0.2, h + 0.2], [w - 0.2, d - 0.2, h + 0.2], [-w + 0.2, d - 0.2, h + 0.2]]], "pv");
|
||||
}
|
||||
}
|
||||
|
||||
fn meta_shed() {
|
||||
#{ label: "Shed / kiosk", category: "Buildings", icon: "▣",
|
||||
props: [
|
||||
#{ key: "w", type: "float", def: 4.0, min: 1.0, max: 30.0, step: 0.5, label: "Width (m)" },
|
||||
#{ key: "d", type: "float", def: 3.0, min: 1.0, max: 30.0, step: 0.5, label: "Depth (m)" },
|
||||
#{ key: "h", type: "float", def: 2.8, min: 1.8, max: 8.0, step: 0.1, label: "Height (m)" },
|
||||
#{ key: "mat", type: "material", def: "render2", choices: wall_materials(), label: "Walls" },
|
||||
#{ key: "roof_mat", type: "material", def: "roof3", choices: roof_materials(), label: "Roof" },
|
||||
] }
|
||||
}
|
||||
fn prefab_shed(o) {
|
||||
let p = o.p;
|
||||
let w = p.w / 2.0;
|
||||
let d = p.d / 2.0;
|
||||
let s = sprite();
|
||||
s.box(-w, -d, w, d, 0.0, 0.15, "plinth");
|
||||
s.box(-w, -d, w, d, 0.15, p.h, p.mat, #{ top: p.roof_mat });
|
||||
let sd = sides(-w, -d, w, d);
|
||||
s.open(sd.S[0], sd.S[1], [[p.w / 2.0 - 0.5, p.w / 2.0 + 0.5, 0.15, 2.2]], "door2");
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
// isokit/equipment: tanks, containers, generators, ground-mounted solar,
|
||||
// chimneys.
|
||||
|
||||
fn meta_tank() {
|
||||
#{ label: "Tank", category: "Equipment", icon: "◯",
|
||||
props: [
|
||||
#{ key: "r", type: "float", def: 2.0, min: 0.3, max: 30.0, step: 0.1, label: "Radius (m)" },
|
||||
#{ key: "h", type: "float", def: 5.0, min: 0.5, max: 60.0, step: 0.1, label: "Height (m)" },
|
||||
#{ key: "mat", type: "material", def: "tank", choices: ["tank", "water", "firetank", "steel", "silok", "siloy", "silor", "chiller"], label: "Colour" },
|
||||
#{ key: "cap", type: "bool", def: true, label: "Cone roof" },
|
||||
] }
|
||||
}
|
||||
fn prefab_tank(o) {
|
||||
let p = o.p;
|
||||
let s = sprite();
|
||||
s.cyl(0.0, 0.0, p.r, 0.0, p.h, p.mat);
|
||||
s.disc(0.0, 0.0, p.h * 0.6, p.r + 0.01, "tankring");
|
||||
if p.cap { s.frust(0.0, 0.0, p.r, 0.25, p.h, p.h + p.r * 0.3, p.mat, 14); }
|
||||
}
|
||||
|
||||
fn meta_container() {
|
||||
#{ label: "Shipping container", category: "Equipment", icon: "▭",
|
||||
props: [
|
||||
#{ key: "size", type: "choice", def: "20ft", choices: ["10ft", "20ft", "40ft"], label: "Size" },
|
||||
#{ key: "colour", type: "choice", def: "rack", choices: ["rack", "truckbox", "classic", "steel", "firewall"], label: "Colour" },
|
||||
#{ key: "stack", type: "int", def: 1, min: 1, max: 4, step: 1, label: "Stacked" },
|
||||
] }
|
||||
}
|
||||
fn prefab_container(o) {
|
||||
let L = if o.p.size == "10ft" { 2.99 } else if o.p.size == "40ft" { 12.19 } else { 6.06 };
|
||||
let s = sprite();
|
||||
for i in 0..o.p.stack {
|
||||
s.box(-L / 2.0, -1.22, L / 2.0, 1.22, i * 2.59, (i + 1) * 2.59 - 0.02, o.p.colour);
|
||||
}
|
||||
}
|
||||
|
||||
fn meta_generator() {
|
||||
#{ label: "Generator", category: "Equipment", icon: "⚡",
|
||||
props: [
|
||||
#{ key: "running", type: "bool", def: false, label: "Running (exhaust)" },
|
||||
] }
|
||||
}
|
||||
fn prefab_generator(o) {
|
||||
let s = sprite();
|
||||
s.box(-2.4, -1.1, 2.4, 1.1, 0.0, 0.25, "plinth");
|
||||
s.box(-2.2, -0.95, 2.2, 0.95, 0.25, 2.4, "genset");
|
||||
s.cyl(1.6, 0.4, 0.12, 2.4, 3.2, "stack");
|
||||
if o.p.running { s.puffs(1.6, 0.4, 3.3, "smoke", 3, 3.2); }
|
||||
}
|
||||
|
||||
fn meta_pv_field() {
|
||||
#{ label: "Solar field", category: "Equipment", icon: "▤",
|
||||
help: "Ground-mounted solar rows facing local -y.",
|
||||
props: [
|
||||
#{ key: "w", type: "float", def: 20.0, min: 2.0, max: 500.0, step: 0.5, label: "Row length (m)" },
|
||||
#{ key: "rows", type: "int", def: 4, min: 1, max: 60, step: 1, label: "Rows" },
|
||||
#{ key: "pitch", type: "float", def: 4.0, min: 2.0, max: 12.0, step: 0.1, label: "Row spacing (m)" },
|
||||
] }
|
||||
}
|
||||
fn prefab_pv_field(o) {
|
||||
let p = o.p;
|
||||
let w = p.w / 2.0;
|
||||
let y0 = -(p.rows - 1) * p.pitch / 2.0;
|
||||
for i in 0..p.rows {
|
||||
// one sprite per row so trees and people sort between them
|
||||
let y = y0 + i * p.pitch;
|
||||
let s = sprite();
|
||||
let n = (p.w / 4.0).ceiling().to_int();
|
||||
for j in 0..(n + 1) {
|
||||
let x = -w + j * p.w / n;
|
||||
s.box(x - 0.05, y + 0.6, x + 0.05, y + 0.7, 0.0, 1.6, "steel");
|
||||
}
|
||||
s.poly([[[-w, y - 1.0, 0.7], [w, y - 1.0, 0.7], [w, y + 1.0, 1.9], [-w, y + 1.0, 1.9]]], "pv");
|
||||
}
|
||||
}
|
||||
|
||||
fn meta_chimney() {
|
||||
#{ label: "Chimney / stack", category: "Equipment", icon: "▮",
|
||||
props: [
|
||||
#{ key: "h", type: "float", def: 12.0, min: 2.0, max: 120.0, step: 0.5, label: "Height (m)" },
|
||||
#{ key: "r", type: "float", def: 0.5, min: 0.1, max: 5.0, step: 0.05, label: "Radius (m)" },
|
||||
#{ key: "smoke", type: "choice", def: "puff", choices: ["none", "puff", "puff2", "smoke"], label: "Plume" },
|
||||
] }
|
||||
}
|
||||
fn prefab_chimney(o) {
|
||||
let p = o.p;
|
||||
let s = sprite();
|
||||
s.cyl(0.0, 0.0, p.r, 0.0, p.h, "stack");
|
||||
s.cyl(0.0, 0.0, p.r * 1.05, p.h - 1.0, p.h - 0.6, "stackband", #{ no_top: true });
|
||||
if p.smoke != "none" { s.puffs(0.0, 0.0, p.h + 0.3, p.smoke, 4, 4.0); }
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
// isokit/ground: site plate, paved and green areas, roads, parking, walls,
|
||||
// fences and lamps. Ground surfaces go to the unsorted "ground" layer in
|
||||
// document order (later objects lie on top).
|
||||
|
||||
fn ground_materials() {
|
||||
["g-grass", "g-grass2", "g-green", "g-garden", "g-asph", "g-conc", "g-pad", "g-path", "g-sand", "g-court", "dirt"]
|
||||
}
|
||||
|
||||
fn meta_site() {
|
||||
#{ label: "Site ground", category: "Ground", icon: "▭",
|
||||
help: "The ground plate: a surface with earth sides. Frames the view.",
|
||||
props: [
|
||||
#{ key: "w", type: "float", def: 60.0, min: 4.0, max: 1000.0, step: 1.0, label: "Width (m)" },
|
||||
#{ key: "d", type: "float", def: 40.0, min: 4.0, max: 1000.0, step: 1.0, label: "Depth (m)" },
|
||||
#{ key: "mat", type: "material", def: "g-grass", choices: ground_materials(), label: "Surface" },
|
||||
#{ key: "base", type: "float", def: 1.6, min: 0.2, max: 10.0, step: 0.1, label: "Earth depth (m)" },
|
||||
] }
|
||||
}
|
||||
fn prefab_site(o) {
|
||||
let p = o.p;
|
||||
let w = p.w / 2.0;
|
||||
let d = p.d / 2.0;
|
||||
layer("ground");
|
||||
sprite().box(-w, -d, w, d, -p.base, 0.0, "dirt", #{ top: p.mat });
|
||||
bounds(-w, -d, w, d, -p.base);
|
||||
}
|
||||
|
||||
fn meta_area() {
|
||||
#{ label: "Area", category: "Ground", icon: "▱",
|
||||
help: "A paved or planted area lying on the ground.",
|
||||
props: [
|
||||
#{ key: "w", type: "float", def: 10.0, min: 0.5, max: 1000.0, step: 0.5, label: "Width (m)" },
|
||||
#{ key: "d", type: "float", def: 6.0, min: 0.5, max: 1000.0, step: 0.5, label: "Depth (m)" },
|
||||
#{ key: "mat", type: "material", def: "g-conc", choices: ground_materials(), label: "Surface" },
|
||||
] }
|
||||
}
|
||||
fn prefab_area(o) {
|
||||
let p = o.p;
|
||||
layer("ground");
|
||||
let s = sprite();
|
||||
if o.pts != () && o.pts.len() >= 3 {
|
||||
s.flat(o.pts, 0.006, p.mat + " t");
|
||||
} else {
|
||||
s.flat(rect(-p.w / 2.0, -p.d / 2.0, p.w / 2.0, p.d / 2.0), 0.006, p.mat + " t");
|
||||
}
|
||||
}
|
||||
|
||||
fn meta_road() {
|
||||
#{ label: "Road", category: "Ground", icon: "═",
|
||||
help: "A straight road along its length, with a dashed centre line.",
|
||||
props: [
|
||||
#{ key: "w", type: "float", def: 40.0, min: 2.0, max: 2000.0, step: 1.0, label: "Length (m)" },
|
||||
#{ key: "d", type: "float", def: 6.0, min: 2.0, max: 40.0, step: 0.5, label: "Width (m)" },
|
||||
#{ key: "dashed", type: "bool", def: true, label: "Centre line" },
|
||||
#{ key: "kerb", type: "bool", def: true, label: "Kerb lines" },
|
||||
] }
|
||||
}
|
||||
fn prefab_road(o) {
|
||||
let p = o.p;
|
||||
let w = p.w / 2.0;
|
||||
let d = p.d / 2.0;
|
||||
layer("ground");
|
||||
let s = sprite();
|
||||
s.flat(rect(-w, -d, w, d), 0.007, "g-asph t");
|
||||
if p.dashed { s.lines([[-w, 0.0, w, 0.0]], 0.012, "dash"); }
|
||||
if p.kerb { s.lines([[-w, -d, w, -d], [-w, d, w, d]], 0.012, "kerb"); }
|
||||
}
|
||||
|
||||
fn meta_parking() {
|
||||
#{ label: "Parking bays", category: "Ground", icon: "P",
|
||||
props: [
|
||||
#{ key: "n", type: "int", def: 6, min: 1, max: 60, step: 1, label: "Bays" },
|
||||
#{ key: "bay_w", type: "float", def: 2.5, min: 2.0, max: 4.0, step: 0.1, label: "Bay width (m)" },
|
||||
#{ key: "bay_d", type: "float", def: 5.0, min: 4.0, max: 7.0, step: 0.1, label: "Bay depth (m)" },
|
||||
] }
|
||||
}
|
||||
fn prefab_parking(o) {
|
||||
let p = o.p;
|
||||
let w = p.n * p.bay_w / 2.0;
|
||||
let d = p.bay_d / 2.0;
|
||||
layer("ground");
|
||||
let s = sprite();
|
||||
s.flat(rect(-w, -d, w, d), 0.008, "g-pad t");
|
||||
let segs = [];
|
||||
for i in 0..(p.n + 1) {
|
||||
let x = -w + i * p.bay_w;
|
||||
segs.push([x, -d, x, d]);
|
||||
}
|
||||
s.lines(segs, 0.014, "bayline");
|
||||
}
|
||||
|
||||
fn meta_wall() {
|
||||
#{ label: "Wall", category: "Site", icon: "▬",
|
||||
props: [
|
||||
#{ key: "w", type: "float", def: 20.0, min: 0.5, max: 500.0, step: 0.5, label: "Length (m)" },
|
||||
#{ key: "h", type: "float", def: 2.2, min: 0.3, max: 8.0, step: 0.1, label: "Height (m)" },
|
||||
#{ key: "t", type: "float", def: 0.24, min: 0.1, max: 1.0, step: 0.02, label: "Thickness (m)" },
|
||||
] }
|
||||
}
|
||||
fn prefab_wall(o) {
|
||||
// long walls in pieces of at most 30 m, each its own sprite for the depth sort
|
||||
let p = o.p;
|
||||
let n = (p.w / 30.0).ceiling().to_int();
|
||||
let L = p.w / n;
|
||||
for i in 0..n {
|
||||
let x0 = -p.w / 2.0 + i * L;
|
||||
sprite().box(x0, -p.t / 2.0, x0 + L, p.t / 2.0, 0.0, p.h, "wallp", #{ top: "wallt" });
|
||||
}
|
||||
}
|
||||
|
||||
fn meta_fence() {
|
||||
#{ label: "Fence", category: "Site", icon: "┼",
|
||||
props: [
|
||||
#{ key: "w", type: "float", def: 20.0, min: 1.0, max: 500.0, step: 0.5, label: "Length (m)" },
|
||||
#{ key: "h", type: "float", def: 1.8, min: 0.5, max: 4.0, step: 0.1, label: "Height (m)" },
|
||||
] }
|
||||
}
|
||||
fn prefab_fence(o) {
|
||||
let p = o.p;
|
||||
let n = (p.w / 30.0).ceiling().to_int();
|
||||
let L = p.w / n;
|
||||
for i in 0..n {
|
||||
let x0 = -p.w / 2.0 + i * L;
|
||||
let s = sprite();
|
||||
s.box(x0, -0.03, x0 + L, 0.03, 0.0, p.h, "fence");
|
||||
let k = (L / 2.5).floor().to_int();
|
||||
for j in 0..(k + 1) {
|
||||
let x = x0 + j * L / fmax(k, 1);
|
||||
s.box(x - 0.04, -0.04, x + 0.04, 0.04, 0.0, p.h + 0.05, "steel");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn meta_lamp() {
|
||||
#{ label: "Street lamp", category: "Site", icon: "†",
|
||||
help: "Keep lamps on verges and kerbs, not on drive areas.",
|
||||
props: [ #{ key: "h", type: "float", def: 5.0, min: 2.0, max: 12.0, step: 0.5, label: "Height (m)" } ] }
|
||||
}
|
||||
fn prefab_lamp(o) {
|
||||
let s = sprite();
|
||||
s.box(-0.07, -0.07, 0.07, 0.07, 0.0, o.p.h, "lampost", #{ no_top: true });
|
||||
s.circle(0.0, 0.0, o.p.h + 0.1, 1.2, "lamp");
|
||||
}
|
||||
|
||||
fn meta_marker() {
|
||||
#{ label: "Marker", category: "Site", icon: "⚑",
|
||||
help: "A pin with a label: set the label in Tag and the tooltip in Info.",
|
||||
props: [ #{ key: "h", type: "float", def: 1.5, min: 0.3, max: 20.0, step: 0.1, label: "Height (m)" } ] }
|
||||
}
|
||||
fn prefab_marker(o) {
|
||||
let s = sprite();
|
||||
s.box(-0.05, -0.05, 0.05, 0.05, 0.0, o.p.h, "steel", #{ no_top: true });
|
||||
s.circle(0.0, 0.0, o.p.h, 2.0, "led ledo");
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
// isokit/nature: trees, hedges, shrubs.
|
||||
|
||||
/// A tree with a round crown of radius r (m) standing at (x, y) of this sprite.
|
||||
fn tree_at(x, y, r) {
|
||||
let h = 1.4 + r * 0.6;
|
||||
this.ext_box(x - r, y - r, x + r, y + r, 0.0, h + r * 1.8);
|
||||
this.stem(x, y, 0.0, h, 0.7, "trunk");
|
||||
let R = r * px() * 1.18;
|
||||
this.circle(x, y, h + r * 0.9, R, "leaf l");
|
||||
this.circle(x, y, h + r * 0.9, R * 0.58, "leaf t", -R * 0.28, -R * 0.3);
|
||||
this
|
||||
}
|
||||
|
||||
fn meta_tree() {
|
||||
#{ label: "Tree", category: "Nature", icon: "🌳",
|
||||
props: [ #{ key: "r", type: "float", def: 1.6, min: 0.5, max: 6.0, step: 0.1, label: "Crown radius (m)" } ] }
|
||||
}
|
||||
fn prefab_tree(o) {
|
||||
sprite().tree_at(0.0, 0.0, o.p.r);
|
||||
}
|
||||
|
||||
fn meta_tree_row() {
|
||||
#{ label: "Row of trees", category: "Nature", icon: "🌲",
|
||||
props: [
|
||||
#{ key: "w", type: "float", def: 30.0, min: 2.0, max: 500.0, step: 1.0, label: "Length (m)" },
|
||||
#{ key: "spacing", type: "float", def: 6.0, min: 2.0, max: 30.0, step: 0.5, label: "Spacing (m)" },
|
||||
#{ key: "r", type: "float", def: 1.5, min: 0.5, max: 6.0, step: 0.1, label: "Crown radius (m)" },
|
||||
] }
|
||||
}
|
||||
fn prefab_tree_row(o) {
|
||||
let p = o.p;
|
||||
let n = (p.w / p.spacing).floor().to_int() + 1;
|
||||
let step = if n > 1 { p.w / (n - 1) } else { 0.0 };
|
||||
for i in 0..n {
|
||||
// one sprite per tree; small deterministic size variation
|
||||
sprite().tree_at(-p.w / 2.0 + i * step, 0.0, p.r * (0.85 + 0.3 * hash(i + p.w)));
|
||||
}
|
||||
}
|
||||
|
||||
fn meta_hedge() {
|
||||
#{ label: "Hedge", category: "Nature", icon: "▬",
|
||||
props: [
|
||||
#{ key: "w", type: "float", def: 10.0, min: 0.5, max: 300.0, step: 0.5, label: "Length (m)" },
|
||||
#{ key: "d", type: "float", def: 0.6, min: 0.3, max: 3.0, step: 0.1, label: "Thickness (m)" },
|
||||
#{ key: "h", type: "float", def: 1.4, min: 0.3, max: 4.0, step: 0.1, label: "Height (m)" },
|
||||
] }
|
||||
}
|
||||
fn prefab_hedge(o) {
|
||||
let p = o.p;
|
||||
sprite().box(-p.w / 2.0, -p.d / 2.0, p.w / 2.0, p.d / 2.0, 0.0, p.h, "hedge");
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
// isokit/people: standing and seated figures, small groups.
|
||||
|
||||
fn people_colours() { ["hivis", "guest1", "guest2", "guest3", "guest4", "guide", "suit"] }
|
||||
|
||||
fn meta_person() {
|
||||
#{ label: "Person", category: "People", icon: "🧍",
|
||||
props: [
|
||||
#{ key: "cls", type: "choice", def: "hivis", choices: people_colours(), label: "Clothes" },
|
||||
#{ key: "seated", type: "bool", def: false, label: "Seated" },
|
||||
] }
|
||||
}
|
||||
fn prefab_person(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); }
|
||||
}
|
||||
|
||||
fn meta_crowd() {
|
||||
#{ label: "Group of people", category: "People", icon: "👥",
|
||||
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) {
|
||||
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]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
// isokit/util: helpers shared by the prefabs.
|
||||
|
||||
/// Floor index of a level id: "gf" -> 0, "1f" -> 1, "2f" -> 2; anything else
|
||||
/// (roof, site, "") -> -1, which means "closed buildings".
|
||||
fn floor_of(lv) {
|
||||
if lv == "gf" { return 0; }
|
||||
if lv.len() >= 2 && lv.ends_with("f") {
|
||||
let k = lv.sub_string(0, lv.len() - 1);
|
||||
let digits = true;
|
||||
for c in k.chars() { if !"0123456789".contains(c) { digits = false; } }
|
||||
if digits { return parse_int(k); }
|
||||
}
|
||||
-1
|
||||
}
|
||||
|
||||
/// Walls of a cut-open room over the rectangle [x0, y0, x1, y1] from height z:
|
||||
/// walls facing the viewer are cut low (lo), the others stay high (hi), so
|
||||
/// the room can be seen into from every rotation. Each wall is its own sprite
|
||||
/// for the depth sort. skip: list of walls to leave out (0 S, 1 N, 2 W, 3 E).
|
||||
fn cut_walls(x0, y0, x1, y1, z, lo, hi, t, mat, skip) {
|
||||
let W = [
|
||||
[x0, y0, x1, y0 + t, 0, -1],
|
||||
[x0, y1 - t, x1, y1, 0, 1],
|
||||
[x0, y0 + t, x0 + t, y1 - t, -1, 0],
|
||||
[x1 - t, y0 + t, x1, y1 - t, 1, 0],
|
||||
];
|
||||
for i in 0..4 {
|
||||
if skip.contains(i) { continue; }
|
||||
let w = W[i];
|
||||
let h = if vis(w[4], w[5]) { lo } else { hi };
|
||||
sprite().box(w[0], w[1], w[2], w[3], z, z + h, mat, #{ top: "cut" });
|
||||
}
|
||||
}
|
||||
|
||||
/// Windows spread along the face a -> b (length L) for one storey:
|
||||
/// returns [[u0, u1, z0, z1], ...]; leaves a gap around u = gap (or -1).
|
||||
fn win_list(L, z0, sill, height, width, spacing, gap) {
|
||||
let out = [];
|
||||
let n = ((L - 1.0) / spacing).floor().to_int();
|
||||
if n < 1 { return out; }
|
||||
let start = (L - (n - 1) * spacing) / 2.0;
|
||||
for i in 0..n {
|
||||
let u = start + i * spacing;
|
||||
if gap >= 0.0 && (u - gap).abs() < width + 0.6 { continue; }
|
||||
out.push([u - width / 2.0, u + width / 2.0, z0 + sill, z0 + sill + height]);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Animate the next drawing of sprite s along (dx, dy) metres of its frame:
|
||||
/// call before drawing, then s.end(). Only animates when the motion runs
|
||||
/// along the axis the depth sort compares second; returns whether it does.
|
||||
fn drive(dx, dy, dur, delay) {
|
||||
let ok = if dx != 0.0 { along_x() } else { !along_x() };
|
||||
if !ok { this.begin(""); return false; }
|
||||
let v = dv(dx, dy, 0.0);
|
||||
this.begin("a drive", `--dx:${v[0]}px;--dy:${v[1]}px;animation-duration:${dur}s;animation-delay:-${delay}s`);
|
||||
true
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
// isokit/vehicles: cars, trucks, motorbikes; optionally driving.
|
||||
|
||||
fn car_colours() { ["car1", "car2", "car3", "car4", "car5"] }
|
||||
|
||||
/// A car filling the plan rectangle; the long side is the driving axis.
|
||||
fn car(x0, y0, x1, y1, cls) {
|
||||
let ax = x1 - x0 >= y1 - y0;
|
||||
let i = 0.9;
|
||||
this.box(x0, y0, x1, y1, 0.25, 1.0, cls);
|
||||
if ax {
|
||||
this.box(x0 + i, y0 + 0.12, x1 - i, y1 - 0.12, 1.0, 1.5, "glassd");
|
||||
} else {
|
||||
this.box(x0 + 0.12, y0 + i, x1 - 0.12, y1 - i, 1.0, 1.5, "glassd");
|
||||
}
|
||||
this
|
||||
}
|
||||
|
||||
fn meta_car() {
|
||||
#{ label: "Car", category: "Vehicles", icon: "🚗",
|
||||
help: "Faces local +x. A driving car moves along its length (in rotations where the depth sort allows it).",
|
||||
props: [
|
||||
#{ key: "colour", type: "choice", def: "car2", choices: car_colours(), label: "Colour" },
|
||||
#{ key: "moving", type: "bool", def: false, label: "Driving" },
|
||||
#{ key: "travel", type: "float", def: 25.0, min: 2.0, max: 300.0, step: 1.0, label: "Travel (m)" },
|
||||
#{ key: "dur", type: "float", def: 11.0, min: 2.0, max: 120.0, step: 0.5, label: "Loop time (s)" },
|
||||
] }
|
||||
}
|
||||
fn prefab_car(o) {
|
||||
let p = o.p;
|
||||
let s = sprite();
|
||||
if p.moving { s.drive(p.travel, 0.0, p.dur, hash(o.x + o.y) * p.dur); }
|
||||
s.car(-2.2, -0.9, 2.2, 0.9, p.colour);
|
||||
if p.moving { s.end(); }
|
||||
}
|
||||
|
||||
fn meta_truck() {
|
||||
#{ label: "Truck", category: "Vehicles", icon: "🚚",
|
||||
props: [
|
||||
#{ key: "len", type: "float", def: 8.0, min: 5.0, max: 16.5, step: 0.5, label: "Length (m)" },
|
||||
#{ key: "box", type: "choice", def: "truckbox", choices: ["truckbox", "cab", "carton", "steel"], label: "Body" },
|
||||
#{ key: "moving", type: "bool", def: false, label: "Driving" },
|
||||
#{ key: "travel", type: "float", def: 40.0, min: 2.0, max: 300.0, step: 1.0, label: "Travel (m)" },
|
||||
#{ key: "dur", type: "float", def: 18.0, min: 2.0, max: 120.0, step: 0.5, label: "Loop time (s)" },
|
||||
] }
|
||||
}
|
||||
fn prefab_truck(o) {
|
||||
let p = o.p;
|
||||
let L = p.len / 2.0;
|
||||
let s = sprite();
|
||||
if p.moving { s.drive(p.travel, 0.0, p.dur, hash(o.x * 3.0 + o.y) * p.dur); }
|
||||
s.box(-L, -1.2, L, 1.2, 0.35, 0.95, "chassis");
|
||||
s.seq([
|
||||
[-0.9, 0.0, |q| q.box(-L, -1.25, L - 2.2, 1.25, 0.95, 3.6, p.box)],
|
||||
[L - 1.1, 0.0, |q| { q.box(L - 2.1, -1.2, L, 1.2, 0.95, 2.9, "cab"); q.box(L - 0.12, -1.0, L, 1.0, 1.9, 2.6, "glassd"); }],
|
||||
]);
|
||||
if p.moving { s.end(); }
|
||||
}
|
||||
|
||||
fn meta_moto() {
|
||||
#{ label: "Motorbike", category: "Vehicles", icon: "🛵",
|
||||
props: [ #{ key: "colour", type: "choice", def: "car1", choices: car_colours(), label: "Colour" } ] }
|
||||
}
|
||||
fn prefab_moto(o) {
|
||||
let s = sprite();
|
||||
s.box(-0.95, -0.3, 0.95, 0.3, 0.3, 0.85, o.p.colour);
|
||||
s.person(0.0, 0.0, 0.55);
|
||||
}
|
||||
Reference in new issue
Block a user