diff --git a/README.md b/README.md index 12ace5d..c8c2fac 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ Try the demos: `isokit studio examples/demo` (a works site) and | Do | How | |---|---| | Add | pick an object on the left, click on the map (Shift-click places several) | -| Draw | walls, roads, fences, hedges: drag along the map; floors, areas, buildings, roofs, rugs: drag a rectangle | +| Draw | walls, fences, hedges, roads: drag along the map, or click corner after corner for a run around a plot (double-click / Enter ends it, the first corner closes it); ends snap onto the walls and fences they meet. Floors, areas, buildings, roofs, rugs: drag a rectangle | | Select | click it on the map or in the object list | | Move | drag it, or arrow keys by the snap step (0.1 / 0.5 / 1 / 5 m) | | Turn | R / Shift+R or ⟲ ⟳ | @@ -72,6 +72,12 @@ The **House** palette has the parts, **Furniture** the interior: 4. **Roof** (hip, gable, flat, optional solar) as a rectangle over the walls, and **Stairs**. +**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 +pillars). Fences and site walls whose ends meet join like house walls: corners +close, a T stops at the other's face, a straight run shares one post, and a +fence ending at a wall stops at its face. + Level **GF** shows the house cut open: walls in front of a room are cut low (walls tell inside from outside by the floors next to them), the roof becomes an outline on the ground. For more storeys, view **1F** (2F, …): new walls, floors diff --git a/crates/isokit-project/src/mcp.rs b/crates/isokit-project/src/mcp.rs index f0101b7..f4a64ae 100644 --- a/crates/isokit-project/src/mcp.rs +++ b/crates/isokit-project/src/mcp.rs @@ -21,7 +21,9 @@ Houses are built from parts (categories House and Furniture): a floor per room ( (x, y = middle of the wall line, rot 0 runs along x, rot 90 along y, w = length; walls meet at their centre lines), \ then windows and doors with x, y on a wall line (they sit on that wall and cut their opening; a door swings towards \ the wall's +y side, rot + 180 swings it the other way), a roof (w x d = outer size of the walls) and furniture \ -(fronts face local -y, backs go against a wall). Level gf shows the house cut open; a part's storey is its z / 3 m."; +(fronts face local -y, backs go against a wall). Level gf shows the house cut open; a part's storey is its z / 3 m. \ +Fences (props.style: mesh, wood, bars, half stone, cobble, brick) and site walls join where their centre-line ends \ +meet, so for a plot boundary put each piece's ends exactly on the next piece's end or line."; fn tool(name: &str, description: &str, props: Value, required: &[&str]) -> Value { json!({ "name": name, "description": description, diff --git a/crates/isokit-script/src/tests.rs b/crates/isokit-script/src/tests.rs index f19ff3f..cc3db8d 100644 --- a/crates/isokit-script/src/tests.rs +++ b/crates/isokit-script/src/tests.rs @@ -399,6 +399,122 @@ fn walls_meeting_at_a_t_or_in_line_do_not_overlap() { assert!((n1[0] + 2.075).abs() < 1e-6 && (n2[3] - 2.075).abs() < 1e-6); } +/// A 10 x 6 m fence perimeter, a fence across it (a T at both ends; at the +/// north it meets the joint of the north fence's two pieces) and a spur from +/// the east fence to a site wall. +fn fence_doc(style: &str) -> Doc { + let mut doc = Doc::default(); + for (id, x, y, rot, w) in [ + ("south", 0.0, -3.0, 0.0, 10.0), + ("north", -2.5, 3.0, 0.0, 5.0), + ("north2", 2.5, 3.0, 0.0, 5.0), + ("west", -5.0, 0.0, 90.0, 6.0), + ("east", 5.0, 0.0, 90.0, 6.0), + ("inner", 0.0, 0.0, 90.0, 6.0), + ("spur", 6.5, 0.0, 0.0, 3.0), + ] { + let mut o = obj(id, "fence", x, y, json!({ "w": w, "style": style })); + o.rot = rot; + doc.objects.push(o); + } + let mut wall = obj("wall", "wall", 8.0, 0.0, json!({"w": 10.0})); + wall.rot = 90.0; + doc.objects.push(wall); + doc +} + +#[test] +fn fences_join_like_walls_in_every_design() { + let r = Runtime::new(&Bundle::default()).unwrap(); + for (style, ht) in [ + ("mesh", 0.04), + ("wood", 0.05), + ("bars", 0.04), + ("half stone", 0.16), + ("cobble", 0.22), + ("brick", 0.18), + ] { + let doc = fence_doc(style); + let b = r.build("site", 0, &cfg(), &doc).unwrap(); + assert!( + b.scene.warnings.is_empty(), + "{style}: {:?}", + b.scene.warnings + ); + let bx = |id: &str| b.boxes[id]; + let near = |a: f64, e: f64| (a - e).abs() < 1e-6; + // corners: the fence along x runs through, the other stops at its face + let (s, w) = (bx("south"), bx("west")); + assert!( + near(s[0], -5.0 - ht) && near(s[3], 5.0 + ht), + "{style} south {s:?}" + ); + assert!( + near(w[1], -3.0 + ht) && near(w[4], 3.0 - ht), + "{style} west {w:?}" + ); + // a T at the south, the end of a straight run at the north: it stops at the faces + let i = bx("inner"); + assert!( + near(i[1], -3.0 + ht) && near(i[4], 3.0 - ht), + "{style} inner {i:?}" + ); + // in line: the lower piece runs through the shared post, the other starts at it + let (n1, n2) = (bx("north"), bx("north2")); + assert!( + near(n1[3], ht) && near(n2[0], ht), + "{style} north {n1:?} {n2:?}" + ); + // the spur stops at the east fence and at the site wall's face + let sp = bx("spur"); + assert!( + near(sp[0], 5.0 + ht) && near(sp[3], 8.0 - 0.12), + "{style} spur {sp:?}" + ); + for rot in 0..4u8 { + let b = r.build("site", rot, &cfg(), &doc).unwrap(); + let out = render(&b.scene, rot, &RenderOpts::default()); + assert_eq!(out.cycles, 0, "{style}/{rot}: depth-sort cycles"); + } + } + // the brick wall has pillars as their own sprites, with brick courses on its faces + let b = r.build("site", 0, &cfg(), &fence_doc("brick")).unwrap(); + assert!(sprites_of(&b, "south") > 4); + assert!(render(&b.scene, 0, &RenderOpts::default()) + .html + .contains("iso-seam")); +} + +#[test] +fn site_walls_join_at_corners() { + let r = Runtime::new(&Bundle::default()).unwrap(); + let mut doc = Doc::default(); + for (id, x, y, rot, w) in [ + ("s", 0.0, -3.0, 0.0, 10.0), + ("n", 0.0, 3.0, 0.0, 10.0), + ("w", -5.0, 0.0, 90.0, 6.0), + ("e", 5.0, 0.0, 90.0, 6.0), + ] { + let mut o = obj(id, "wall", x, y, json!({ "w": w })); + o.rot = rot; + doc.objects.push(o); + } + let b = r.build("site", 0, &cfg(), &doc).unwrap(); + let (s, w) = (b.boxes["s"], b.boxes["w"]); + assert!( + (s[0] + 5.12).abs() < 1e-6 && (s[3] - 5.12).abs() < 1e-6, + "{s:?}" + ); + assert!( + (w[1] + 2.88).abs() < 1e-6 && (w[4] - 2.88).abs() < 1e-6, + "{w:?}" + ); + for rot in 0..4u8 { + let b = r.build("site", rot, &cfg(), &doc).unwrap(); + assert_eq!(render(&b.scene, rot, &RenderOpts::default()).cycles, 0); + } +} + #[test] fn cut_levels_lower_the_walls_in_front_of_rooms() { let r = Runtime::new(&Bundle::default()).unwrap(); diff --git a/crates/isokit-script/stdlib/ground.rhai b/crates/isokit-script/stdlib/ground.rhai index c80a676..53c799b 100644 --- a/crates/isokit-script/stdlib/ground.rhai +++ b/crates/isokit-script/stdlib/ground.rhai @@ -91,6 +91,7 @@ fn prefab_parking(o) { fn meta_wall() { #{ label: "Wall", category: "Site", icon: "▬", shape: "line", + help: "Drag along the map, or click corner after corner for a run around a plot. Walls that meet join at corners and T-junctions.", 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)" }, @@ -98,37 +99,179 @@ fn meta_wall() { ] } } fn prefab_wall(o) { - // long walls in pieces of at most 30 m, each its own sprite for the depth sort + // ends joined to the walls they meet; 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; + let x0 = -p.w / 2.0 - line_joint(o, -1.0, ["wall"], []).ext; + let x1 = p.w / 2.0 + line_joint(o, 1.0, ["wall"], []).ext; + let n = ((x1 - x0) / 30.0).ceiling().to_int(); + let L = (x1 - x0) / 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" }); + let a = x0 + i * L; + sprite().box(a, -p.t / 2.0, a + L, p.t / 2.0, 0.0, p.h, "wallp", #{ top: "wallt" }); } } +fn fence_styles() { ["mesh", "wood", "bars", "half stone", "cobble", "brick"] } + +/// Half the thickness of a fence design: its posts, base or pillars. +fn fence_ht(style) { + switch style { "wood" => 0.05, "half stone" => 0.16, "cobble" => 0.22, "brick" => 0.18, _ => 0.04 } +} + fn meta_fence() { #{ label: "Fence", category: "Site", icon: "┼", shape: "line", + help: "Drag along the map, or click corner after corner for a run around a plot. Fences that meet join at corners and T-junctions and stop at walls. Designs: mesh, wooden boards, steel bars, mesh on a stone base, cobble wall, brick wall with pillars.", 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)" }, + #{ key: "style", type: "choice", def: "mesh", choices: fence_styles(), label: "Design" }, ] } } 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(); + let st = p.style; + let ea = line_joint(o, -1.0, ["fence"], ["wall", "house_wall"]); + let eb = line_joint(o, 1.0, ["fence"], ["wall", "house_wall"]); + let x0 = -p.w / 2.0 - ea.ext; + let x1 = p.w / 2.0 + eb.ext; + if x1 - x0 < 0.05 { return; } + // posts (pillars) evenly along the length; the end ones only where this + // fence draws them, kept inside the stretch it covers + let ph = fence_ht(st); + let gap = if st == "brick" { 3.0 } else { 2.5 }; + let k = fmax(1.0, (p.w / gap).round()).to_int(); + let posts = []; + if st != "cobble" { 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"); + if (j == 0 && !ea.post) || (j == k && !eb.post) { continue; } + posts.push(fmin(fmax(-p.w / 2.0 + j * p.w / k, x0 + ph), x1 - ph)); } } + if st == "brick" { brick_fence(x0, x1, posts, p.h); return; } + // pieces of at most 30 m, each its own sprite for the depth sort + let n = ((x1 - x0) / 30.0).ceiling().to_int(); + let L = (x1 - x0) / n; + for i in 0..n { + let a = x0 + i * L; + let b = a + L; + let ps = []; + for x in posts { + if x >= a - 1e-6 && (x < b - 1e-6 || i == n - 1) { ps.push(fmin(fmax(x, a + ph), b - ph)); } + } + fence_piece(sprite(), a, b, ps, p.h, st, i); + } +} + +/// One stretch a..b of a fence (not brick) with its posts ps. +fn fence_piece(s, a, b, ps, h, st, seed) { + let L = b - a; + if st == "wood" { + s.box(a, -0.025, b, 0.025, 0.0, h, "wood"); + let boards = []; + let m = fmax(1.0, (L / 0.2).round()).to_int(); + for j in 0..m { boards.push([j * L / m, (j + 1) * L / m, 0.04, h - 0.02]); } + s.open([a, -0.025], [b, -0.025], boards, "seam"); + s.open([b, 0.025], [a, 0.025], boards, "seam"); + for x in ps { s.box(x - 0.05, -0.05, x + 0.05, 0.05, 0.0, h + 0.06, "woodd"); } + } else if st == "bars" { + let bars = []; + let m = fmax(2.0, (L / 0.16).round()).to_int(); + for j in 1..m { let u = j * L / m; bars.push([u - 0.014, u + 0.014, 0.0, h - 0.05]); } + s.open([a, 0.0], [b, 0.0], bars, "steel"); + s.open([b, 0.0], [a, 0.0], bars, "steel"); + s.box(a, -0.025, b, 0.025, 0.1, 0.15, "steel"); + s.box(a, -0.025, b, 0.025, h - 0.06, h, "steel"); + for x in ps { s.box(x - 0.04, -0.04, x + 0.04, 0.04, 0.0, h + 0.05, "steel"); } + } else if st == "half stone" { + let hb = fmin(0.6, h * 0.4); + s.box(a, -0.16, b, 0.16, 0.0, hb, "cobble", #{ top: "capst" }); + let j = stone_joints(L, 0.0, hb, seed); + s.open([a, -0.16], [b, -0.16], j, "seam"); + s.open([b, 0.16], [a, 0.16], j, "seam"); + s.box(a, -0.03, b, 0.03, hb, h, "fence"); + for x in ps { s.box(x - 0.04, -0.04, x + 0.04, 0.04, hb, h + 0.05, "steel"); } + } else if st == "cobble" { + let hc = fmax(0.2, h - 0.08); + s.box(a, -0.2, b, 0.2, 0.0, hc, "cobble"); + s.box(a, -0.22, b, 0.22, hc, h, "capst"); + let j = stone_joints(L, 0.0, hc, seed); + s.open([a, -0.2], [b, -0.2], j, "seam"); + s.open([b, 0.2], [a, 0.2], j, "seam"); + } else { + // mesh + s.box(a, -0.03, b, 0.03, 0.0, h, "fence"); + for x in ps { s.box(x - 0.04, -0.04, x + 0.04, 0.04, 0.0, h + 0.05, "steel"); } + } +} + +/// A brick wall between pillars: pillars and wall stretches are separate +/// sprites (the pillars are thicker), with a coping on top and brick courses. +fn brick_fence(x0, x1, posts, h) { + let hw = fmax(0.3, h - 0.06); + let u = x0; + for x in posts + [x1 + 1.0] { + let b = fmin(x - 0.18, x1); + if b > u + 0.02 { + let n = ((b - u) / 30.0).ceiling().to_int(); + let L = (b - u) / n; + for k in 0..n { + let s = sprite(); + let a = u + k * L; + s.box(a, -0.12, a + L, 0.12, 0.0, hw, "brick"); + s.box(a, -0.14, a + L, 0.14, hw, h, "plinth"); + let j = brick_joints(L, 0.0, hw, a); + s.open([a, -0.12], [a + L, -0.12], j, "seam"); + s.open([a + L, 0.12], [a, 0.12], j, "seam"); + } + } + if x <= x1 { + let s = sprite(); + s.box(x - 0.17, -0.17, x + 0.17, 0.17, 0.0, h + 0.1, "brick"); + s.box(x - 0.18, -0.18, x + 0.18, 0.18, h + 0.1, h + 0.17, "plinth"); + u = x + 0.18; + } + } +} + +/// Joints of a stone face L long from z0 to z1: rows of stones of varying length. +fn stone_joints(L, z0, z1, seed) { + let out = []; + let rows = fmax(1.0, ((z1 - z0) / 0.22).round()).to_int(); + let rh = (z1 - z0) / rows; + for r in 0..rows { + let k = seed * 31 + r * 17 + 7; + let u = 0.0; + while u < L - 0.05 { + k = (k * 1103 + 12345) % 9973; + let u1 = fmin(L, u + 0.28 + (k % 100) / 100.0 * 0.34); + if L - u1 < 0.12 { u1 = L; } + out.push([u, u1, z0 + r * rh, z0 + (r + 1) * rh]); + u = u1; + } + } + out +} + +/// Brick courses on a face L long from z0 to z1, in running bond; `at` +/// shifts the bond so neighbouring stretches line up. +fn brick_joints(L, z0, z1, at) { + let out = []; + let rows = fmax(1.0, ((z1 - z0) / 0.25).round()).to_int(); + let rh = (z1 - z0) / rows; + for r in 0..rows { + let off = if r % 2 == 0 { 0.0 } else { 0.25 }; + let u = 0.0; + let first = 0.5 - ((at + off) % 0.5 + 0.5) % 0.5; + let u1 = if first < 0.05 { first + 0.5 } else { first }; + while u < L - 0.02 { + let e = fmin(L, u1); + out.push([u, e, z0 + r * rh, z0 + (r + 1) * rh]); + u = e; + u1 = e + 0.5; + } + } + out } fn meta_lamp() { diff --git a/crates/isokit-script/stdlib/house.rhai b/crates/isokit-script/stdlib/house.rhai index 9472d08..027a25a 100644 --- a/crates/isokit-script/stdlib/house.rhai +++ b/crates/isokit-script/stdlib/house.rhai @@ -135,8 +135,6 @@ fn prefab_house_wall(o) { if end > x + 0.01 { wall_run(x, end, t, top, p.mat, cap); } } -fn along_world_x(rot) { ((rot / 90.0).round().to_int() % 2) == 0 } - /// How far wall o reaches past end e (-1 or 1) of its centre line. Alone: /// half its thickness, so a corner closes. Where other walls meet it, /// the walls must not overlap (the depth sort needs them apart): along a diff --git a/crates/isokit-script/stdlib/util.rhai b/crates/isokit-script/stdlib/util.rhai index 451120e..aaf3cb5 100644 --- a/crates/isokit-script/stdlib/util.rhai +++ b/crates/isokit-script/stdlib/util.rhai @@ -57,3 +57,66 @@ fn drive(dx, dy, dur, delay) { this.begin("a drive", `--dx:${v[0]}px;--dy:${v[1]}px;animation-duration:${dur}s;animation-delay:-${delay}s`); true } + +/// True when a piece turned by rot runs along world x (rot 0 or 180). +fn along_world_x(rot) { ((rot / 90.0).round().to_int() % 2) == 0 } + +/// Half the thickness of a fence or wall piece: what the joints keep apart. +fn line_ht(w) { + if w.type == "fence" { fence_ht(w.p.style) } else { w.p.t / 2.0 } +} + +/// How line piece o (a fence or wall along its local x, length p.w) meets +/// other pieces at its end e (-1 or 1): #{ ext, post }, how far it reaches +/// past the end and whether it draws the end post. `same`: the types it joins +/// with; `stops`: types it ends against. The rules of the house walls, so +/// pieces never overlap (the depth sort needs them apart): +/// - alone it reaches half its thickness past the end, flush with its post; +/// - on a straight run the lower piece runs through and draws the shared +/// post, the other starts at that post; +/// - at a corner the thicker piece (equal: the one along world x) runs +/// through, the other stops at its face; +/// - at a T it stops at the other's face without an end post (the other +/// piece has its own there); against a `stops` piece it keeps its post. +fn line_joint(o, e, same, stops) { + let ht = line_ht(o); + let p = to_world(e * o.p.w / 2.0, 0.0); + let c = to_world(0.0, 0.0); + let q = to_world(e * (o.p.w / 2.0 - 0.05), 0.0); + let ox = along_world_x(o.rot); + let face = 0.0; + let has_face = false; + let face_post = false; + let run = #{}; + let corner = []; + for w in objects_on(same + stops, p[0], p[1], 0.5) { + if w.id == o.id || (w.z - o.z).abs() > 0.5 { continue; } + let wt = line_ht(w); + if w.v.abs() > wt + 0.02 { continue; } + let at_end = (w.u.abs() - w.p.w / 2.0).abs() < 0.02; + let wx = along_world_x(w.rot); + let side = if local_of(w, q[0], q[1])[1] >= w.v { 1.0 } else { -1.0 }; + let stop = -(wt - side * w.v); + if !same.contains(w.type) || (!at_end && wx != ox) { + face = stop; + has_face = true; + face_post = !same.contains(w.type); + } else if wx == ox { + if at_end { run = #{ lower: if ox { c[0] < w.x } else { c[1] < w.y }, wt: wt }; } + } else { + corner.push(#{ wt: wt, stop: stop }); + } + } + if has_face { return #{ ext: face, post: face_post }; } + if run.len() > 0 { + return if run.lower { #{ ext: ht, post: true } } else { #{ ext: -run.wt, post: false } }; + } + // two pieces end here from both sides: a straight run this one meets as a T + if corner.len() > 1 { return #{ ext: corner[0].stop, post: false }; } + if corner.len() == 1 { + let k = corner[0]; + let mine = ht > k.wt + 0.005 || ((ht - k.wt).abs() <= 0.005 && ox); + return if mine { #{ ext: ht, post: true } } else { #{ ext: k.stop, post: false } }; + } + #{ ext: ht, post: true } +} diff --git a/crates/isokit-theme/themes/default.toml b/crates/isokit-theme/themes/default.toml index 7efdc0e..964e7e3 100644 --- a/crates/isokit-theme/themes/default.toml +++ b/crates/isokit-theme/themes/default.toml @@ -249,6 +249,8 @@ wframe = "#c8ccd2" sill = "#9aa0a8" lshade = "#d9c9a0" roofr = "#8a4a3a" +cobble = "#6c675f" +capst = "#8c877e" [palette.light] sky1 = "#a9d4fb" @@ -493,6 +495,8 @@ wframe = "#ffffff" sill = "#e6e2da" lshade = "#f2e6c8" roofr = "#b8613f" +cobble = "#aaa195" +capst = "#d8d2c6" [materials] warn = "#e0412f" @@ -717,6 +721,8 @@ rug3 = "rug3" sill = "sill" lshade = "lshade" roofr = "roofr" +cobble = "cobble" +capst = "capst" doorw = "furnw" doorg = "door" f-oak = "oak" diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index b7af77f..d3ede08 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -81,6 +81,10 @@ Each phase ends with tests, a Studio/viewer check in Edge, commit and push. 8. Waypoint animation: a person or car moving along a path of waypoints 9. ~~Build your own house~~ (done, see above) +Done since: the middle mouse button only moves the camera; fences and site walls join like house +walls (`line_joint`); fence designs (mesh, wood, bars, half stone, cobble, brick with pillars); +runs of walls and fences drawn corner by corner, ends snapping onto the pieces they meet. + ## Phase 2: richer editing - [x] Interior prefabs (see Build your own house); a building `rooms` prop is still open diff --git a/docs/SCRIPTING.md b/docs/SCRIPTING.md index 234173d..ff13149 100644 --- a/docs/SCRIPTING.md +++ b/docs/SCRIPTING.md @@ -100,6 +100,22 @@ parts (`stdlib/house.rhai`, `stdlib/furniture.rhai`): `storey_of(z)`, `wall_top(w)`, `opening_at(o)`, `s.slab(a, b, th, z0, z1, mat)`, `s.chair_at(x, y, bx, by, mat)`, `s.office_chair(x, y, mat)`. +## Joints of walls and fences + +`line_joint(o, e, same, stops)` (util.rhai) tells a line piece (centre line +along local x, length `p.w`) how it meets others at its end `e` (-1 or 1): +`#{ ext, post }`, how far it reaches past the end and whether it draws its end +post. `same` are the types it joins with, `stops` the types it ends against; +half thicknesses come from `line_ht(o)` (`p.t / 2`, for fences the design's). +Alone a piece reaches half its thickness past the end; on a straight run the +lower piece runs through and draws the shared post; at a corner the thicker +piece (equal: the one along world x) runs through and the other stops at its +face; at a T the stem stops at the face without an end post; against a `stops` +piece it stops at the face and keeps its post. Pieces never overlap, which the +depth sort needs. `fence` (designs `style`: mesh, wood, bars, half stone, +cobble, brick) and the site `wall` use it; `house_wall` has its own version +(`wall_ext`, house.rhai). + ## House parts `house_wall`, `window`, `door`, `floor`, `roof`, `stairs` and the furniture diff --git a/examples/house/scene.json b/examples/house/scene.json index ac5203a..11d0a86 100644 --- a/examples/house/scene.json +++ b/examples/house/scene.json @@ -62,7 +62,11 @@ {"id": "plant-terrace", "type": "plant", "x": 10.6, "y": -4.0, "props": {"h": 1.4, "pot": "concb"}}, {"id": "lamp-path", "type": "lamp", "x": 0.3, "y": -6.8, "props": {"h": 3.5}}, {"id": "hedge-n", "type": "hedge", "x": 2, "y": 13.6, "props": {"w": 34, "d": 0.8, "h": 1.6}, "name": "Hedge"}, - {"id": "fence-e", "type": "fence", "x": 20.6, "y": 0.5, "rot": 90, "props": {"w": 27.5, "h": 1.4}, "name": "Fence"}, + {"id": "wall-front-a", "type": "fence", "name": "Front wall", "x": -14.0, "y": -7.2, "props": {"w": 5.2, "h": 1.2, "style": "brick"}}, + {"id": "wall-front-b", "type": "fence", "name": "Front wall", "x": -4.8, "y": -7.2, "props": {"w": 5.6, "h": 1.2, "style": "brick"}}, + {"id": "wall-front-c", "type": "fence", "name": "Front wall", "x": 10.3, "y": -7.2, "props": {"w": 20.6, "h": 1.2, "style": "brick"}, "info": "Brick, 1.2 m, with pillars; gaps for the drive and the front path"}, + {"id": "fence-w", "type": "fence", "name": "Garden fence west", "x": -16.6, "y": 2.95, "rot": 90, "props": {"w": 20.3, "h": 1.8, "style": "wood"}}, + {"id": "fence-e", "type": "fence", "name": "Garden fence east", "x": 20.6, "y": 2.95, "rot": 90, "props": {"w": 20.3, "h": 1.8, "style": "wood"}}, {"id": "tree-1", "type": "tree", "x": -12.5, "y": 9.0, "props": {"r": 2.2}}, {"id": "tree-2", "type": "tree", "x": 13.5, "y": 7.5, "props": {"r": 2.4}}, {"id": "tree-3", "type": "tree", "x": 16.0, "y": -4.5, "props": {"r": 1.8}}, diff --git a/studio/index.html b/studio/index.html index 16f2610..c6c8e9a 100644 --- a/studio/index.html +++ b/studio/index.html @@ -62,7 +62,7 @@ document.documentElement.setAttribute("data-theme",t);})();
| Add | pick an object on the left, click on the map (Shift-click to place several) |
|---|---|
| Draw | walls, roads, fences: drag along the map; floors, areas, buildings, roofs: drag a rectangle; Room: drag a rectangle for a floor with walls around it |
| Draw | walls, fences, hedges, roads: drag along the map for one piece, or click corner after corner for a run (double-click or Enter ends it, clicking the first corner closes it, Backspace drops the last corner); ends snap onto the walls and fences they meet, which then join at corners and T-junctions. Floors, areas, buildings, roofs: drag a rectangle; Room: drag a rectangle for a floor with walls around it |
| Windows, doors | click on a wall: they snap onto it, cut their opening and move, turn, copy and delete with the wall; R turns a door to the other hinge and swing side |
| Storeys | GF, 1F, … cut a house open; on 1F and up, new walls, floors and furniture go onto that storey (3 m each, parameter storey_h) |
| Select | click it on the map or in the list |