Streets: sidewalks, road curves, junctions and roundabouts
Deploy web Studio / deploy (push) Successful in 4m31s

Roads take sidewalks (none, both, left, right): 12 cm high paving with
kerb faces drawn where they face the viewer. Road curve (radius, angle,
left/right) carries a street round a bend; centre lines are true arcs
when cut and offset, so curve joints are seamless. A road ending on
another road's centre line opens that road's kerb and sidewalk (T
junction); two roads ending at one corner close it. Roundabout: ring
radius and width, one or two lanes, up to four arms with flared kerbs,
splitter islands, give-way lines on the entry side (traffic on the left
or right) and a central island with apron.

Studio: a road run drawn corner by corner gets a curve in each corner
(prefab meta `corner`). Tooltips list the streets each one meets.
Engine: open `polyline` primitive (Prim::Poly open) so dashes run on
round curves. examples/streets and a Streets demo in the web Studio;
MCP guidance, docs. ROADMAP: backlog 5-7 done; multi-select and the
hosted copy marked done.

Tests: Rust 72, Studio 113, web 95.
This commit is contained in:
grabowski committed 2026-10-25 12:32:52 +07:00
1 parent 2442966743
commit c91d63663b
24 files changed
+989 -29

No files matched your search

+116
View File
@@ -732,6 +732,122 @@ fn robot_arms_pass_goods_from_belt_to_belt() {
assert!(out.info["robot1"].rows[1].1.contains("waiting"));
}
fn street_doc() -> Doc {
let mut doc = Doc::default();
let walk = json!({"walk": "both"});
for (id, kind, x, y, rot, props) in [
(
"main",
"road",
-10.0,
0.0,
0.0,
json!({"w": 40, "walk": "both"}),
),
(
"side",
"road",
-14.0,
-12.0,
90.0,
json!({"w": 24, "walk": "both"}),
),
("bend", "road_curve", 10.0, 0.0, 0.0, walk.clone()),
(
"north",
"road",
20.0,
18.0,
90.0,
json!({"w": 16, "walk": "both"}),
),
(
"c1",
"road",
-30.0,
20.0,
0.0,
json!({"w": 20, "walk": "both"}),
),
(
"c2",
"road",
-20.0,
30.0,
90.0,
json!({"w": 20, "walk": "both"}),
),
("rb", "roundabout", -54.0, 0.0, 0.0, json!({"arm_n": false})),
] {
let mut o = obj(id, kind, x, y, props);
o.rot = rot;
doc.objects.push(o);
}
doc
}
/// Sidewalk strips of object id (paving pieces).
fn walks_of(b: &crate::Built, id: &str) -> usize {
b.scene
.layers
.iter()
.flat_map(|l| l.sprites.iter())
.filter(|s| s.obj.as_deref() == Some(id))
.flat_map(|s| s.prims.iter())
.filter(|p| matches!(p, isokit_core::Prim::Flat { cls, .. } if cls.contains("g-walk")))
.count()
}
#[test]
fn streets_curve_join_and_meet_at_roundabouts() {
let r = Runtime::new(&Bundle::default()).unwrap();
assert_eq!(r.prefab("road").unwrap().corner, "road_curve");
let doc = street_doc();
for view in 0..4u8 {
let b = r.build("site", view, &cfg(), &doc).unwrap();
assert!(b.scene.warnings.is_empty(), "{:?}", b.scene.warnings);
let out = render(&b.scene, view, &RenderOpts::default());
assert_eq!(out.cycles, 0, "view {view}: depth-sort cycles");
assert!(!out.html.contains("NaN"));
}
let b = r.build("site", 0, &cfg(), &doc).unwrap();
let near = |a: [f64; 6], want: [f64; 6]| a.iter().zip(want).all(|(a, w)| (a - w).abs() < 1e-3);
// the side road ends on the main street's centre line; the main street's south
// sidewalk leaves a gap for it (two pieces), its north one runs through
assert_eq!(walks_of(&b, "main"), 3);
assert_eq!(walks_of(&b, "side"), 2);
let sd = b.boxes["side"];
assert!(near(sd, [-19.0, -24.0, 0.0, -9.0, 0.0, 0.12]), "{sd:?}");
// the curve carries the street on round 90 degrees, overlapping both by 4 cm
// along its centre line (6 cm at its outer edge); the main street meets the
// roundabout's east arm end to end
let bd = b.boxes["bend"];
assert!(near(bd, [9.94, -5.0, 0.0, 25.0, 10.06, 0.12]), "{bd:?}");
let mn = b.boxes["main"];
assert!(near(mn, [-30.04, -5.0, 0.0, 10.04, 5.0, 0.12]), "{mn:?}");
// two roads ending at one corner close it: the outer sidewalks go round it
let (c1, c2) = (b.boxes["c1"], b.boxes["c2"]);
assert!(
(c1[3] + 15.0).abs() < 1e-6 && (c1[1] - 15.0).abs() < 1e-6,
"{c1:?}"
);
assert!(
(c2[1] - 15.0).abs() < 1e-6 && (c2[3] + 15.0).abs() < 1e-6,
"{c2:?}"
);
// the roundabout: arms east, west and south ending 24 m out, the ring's sidewalk to the north
let rb = b.boxes["rb"];
assert!(
(rb[0] + 78.0).abs() < 1e-6
&& (rb[3] + 30.0).abs() < 1e-6
&& (rb[1] + 24.0).abs() < 1e-6
&& (rb[4] - 16.0).abs() < 0.01,
"{rb:?}"
);
let html = render(&b.scene, 0, &RenderOpts::default()).html;
assert!(html.contains("iso-giveway") && html.contains("iso-kerbf"));
}
#[test]
fn site_walls_join_at_corners() {
let r = Runtime::new(&Bundle::default()).unwrap();