Streets: sidewalks, road curves, junctions and roundabouts
Deploy web Studio / deploy (push) Successful in 4m31s
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:
1 parent
2442966743
commit
c91d63663b
24 files changed
+989
-29
No files matched your search
@@ -22,6 +22,7 @@ on:
|
|||||||
- "examples/demo/**"
|
- "examples/demo/**"
|
||||||
- "examples/house/**"
|
- "examples/house/**"
|
||||||
- "examples/factory/**"
|
- "examples/factory/**"
|
||||||
|
- "examples/streets/**"
|
||||||
- "tools/build_web.py"
|
- "tools/build_web.py"
|
||||||
- "Cargo.toml"
|
- "Cargo.toml"
|
||||||
- "Cargo.lock"
|
- "Cargo.lock"
|
||||||
|
|||||||
@@ -39,15 +39,16 @@ 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) and
|
`isokit studio examples/house` (a furnished house built from parts),
|
||||||
`isokit studio examples/factory` (presses, conveyors and robot arms).
|
`isokit studio examples/factory` (presses, conveyors and robot arms) and
|
||||||
|
`isokit studio examples/streets` (sidewalks, curves, junctions, a roundabout).
|
||||||
|
|
||||||
### Studio (for everyone)
|
### Studio (for everyone)
|
||||||
|
|
||||||
| Do | How |
|
| Do | How |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Add | pick an object on the left, click on the map (Shift-click places several) |
|
| Add | pick an object on the left, click on the map (Shift-click places several) |
|
||||||
| 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 |
|
| 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; a road run gets a curve in each corner. Floors, areas, buildings, roofs, rugs: drag a rectangle |
|
||||||
| Select | click it on the map or in the object list; click a wall or fence again to select every piece joined to it |
|
| Select | click it on the map or in the object list; click a wall or fence again to select every piece joined to it |
|
||||||
| Several | Ctrl+click (map or list) adds or takes out one, Shift+click in the list a range, Shift+drag on the map a box, Ctrl+A all; then drag one to move them all, arrows, R, Ctrl+D, Delete and the property form work on all of them |
|
| Several | Ctrl+click (map or list) adds or takes out one, Shift+click in the list a range, Shift+drag on the map a box, Ctrl+A all; then drag one to move them all, arrows, R, Ctrl+D, Delete and the property form work on all of them |
|
||||||
| Move | drag it, or arrow keys by the snap step (0.1 / 0.5 / 1 / 5 m) |
|
| Move | drag it, or arrow keys by the snap step (0.1 / 0.5 / 1 / 5 m) |
|
||||||
@@ -201,6 +202,14 @@ off and puts them on the next belt or a pallet, in step with them. Tooltips
|
|||||||
show the numbers. `examples/factory` (Factory demo in the web Studio) has a
|
show the numbers. `examples/factory` (Factory demo in the web Studio) has a
|
||||||
few lines.
|
few lines.
|
||||||
|
|
||||||
|
Streets: a Road can have sidewalks on both sides or one (12 cm high paving
|
||||||
|
with kerbs). A Road curve carries a street round a bend, a road ending on
|
||||||
|
another road's centre line makes a junction (the sidewalk opens there), and a
|
||||||
|
Roundabout has up to four arms with flared kerbs, splitter islands and
|
||||||
|
give-way lines for traffic on the left or right. Roads meet by their centre
|
||||||
|
lines, so put the ends on each other. `examples/streets` (Streets demo in the
|
||||||
|
web Studio) is a small town quarter.
|
||||||
|
|
||||||
Animations (fans, smoke, fish, traffic) step on about ten times a second by
|
Animations (fans, smoke, fish, traffic) step on about ten times a second by
|
||||||
default ("light"): running CSS animations repaint the whole drawing every frame,
|
default ("light"): running CSS animations repaint the whole drawing every frame,
|
||||||
which keeps a slow laptop busy. The ▷ button in the view toolbar switches
|
which keeps a slow laptop busy. The ▷ button in the view toolbar switches
|
||||||
@@ -236,6 +245,7 @@ web/ web Studio shell: WASM backend for studio.js, projects, f
|
|||||||
examples/demo demo project (works site)
|
examples/demo demo project (works site)
|
||||||
examples/house house built from parts (walls, windows, doors, floors, furniture, roof)
|
examples/house house built from parts (walls, windows, doors, floors, furniture, roof)
|
||||||
examples/factory production yard: producers, conveyors, robot arms
|
examples/factory production yard: producers, conveyors, robot arms
|
||||||
|
examples/streets town quarter: sidewalks, curves, junctions, a roundabout
|
||||||
deploy/Caddyfile the web Studio's vhost on the buildfor.life web server (s1)
|
deploy/Caddyfile the web Studio's vhost on the buildfor.life web server (s1)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -145,11 +145,13 @@ pub enum Prim {
|
|||||||
two_sided: bool,
|
two_sided: bool,
|
||||||
},
|
},
|
||||||
/// 3D polygons merged into one path with a fixed class (PV modules,
|
/// 3D polygons merged into one path with a fixed class (PV modules,
|
||||||
/// signs, markings on slopes): no shading, no culling.
|
/// signs, markings on slopes): no shading, no culling. `open`: lines
|
||||||
|
/// that are not closed (a dashed line round a curve stays one dash run).
|
||||||
Poly {
|
Poly {
|
||||||
polys: Vec<Vec<P3>>,
|
polys: Vec<Vec<P3>>,
|
||||||
cls: String,
|
cls: String,
|
||||||
id: Option<String>,
|
id: Option<String>,
|
||||||
|
open: bool,
|
||||||
},
|
},
|
||||||
/// Screen-space circle of radius r (screen units) at a world point,
|
/// Screen-space circle of radius r (screen units) at a world point,
|
||||||
/// shifted by off (screen units): heads, lamps, LEDs, tree crowns.
|
/// shifted by off (screen units): heads, lamps, LEDs, tree crowns.
|
||||||
|
|||||||
@@ -415,14 +415,19 @@ impl Ctx<'_> {
|
|||||||
id,
|
id,
|
||||||
two_sided,
|
two_sided,
|
||||||
} => self.face(pts, mat, id.as_deref(), *two_sided),
|
} => self.face(pts, mat, id.as_deref(), *two_sided),
|
||||||
Prim::Poly { polys, cls, id } => {
|
Prim::Poly {
|
||||||
|
polys,
|
||||||
|
cls,
|
||||||
|
id,
|
||||||
|
open,
|
||||||
|
} => {
|
||||||
if polys.is_empty() {
|
if polys.is_empty() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let p = self.p;
|
let p = self.p;
|
||||||
self.path_open(cls, id.as_deref());
|
self.path_open(cls, id.as_deref());
|
||||||
for q in polys {
|
for q in polys {
|
||||||
p.d(&mut self.out, q, false);
|
p.d(&mut self.out, q, *open);
|
||||||
}
|
}
|
||||||
self.out.push_str("\"/>");
|
self.out.push_str("\"/>");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -447,6 +447,21 @@ impl Sprite {
|
|||||||
|
|
||||||
/// 3D polygons as one path with a fixed class.
|
/// 3D polygons as one path with a fixed class.
|
||||||
pub fn poly(&mut self, polys: Vec<Vec<P3>>, cls: &str, id: Option<String>) -> &mut Self {
|
pub fn poly(&mut self, polys: Vec<Vec<P3>>, cls: &str, id: Option<String>) -> &mut Self {
|
||||||
|
self.poly_path(polys, cls, id, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Open 3D polylines as one path (markings along curves: one dash run).
|
||||||
|
pub fn polyline(&mut self, lines: Vec<Vec<P3>>, cls: &str, id: Option<String>) -> &mut Self {
|
||||||
|
self.poly_path(lines, cls, id, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn poly_path(
|
||||||
|
&mut self,
|
||||||
|
polys: Vec<Vec<P3>>,
|
||||||
|
cls: &str,
|
||||||
|
id: Option<String>,
|
||||||
|
open: bool,
|
||||||
|
) -> &mut Self {
|
||||||
let polys: Vec<Vec<P3>> = polys
|
let polys: Vec<Vec<P3>> = polys
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|p| p.into_iter().map(|q| self.xf.p3(q)).collect())
|
.map(|p| p.into_iter().map(|q| self.xf.p3(q)).collect())
|
||||||
@@ -458,6 +473,7 @@ impl Sprite {
|
|||||||
polys,
|
polys,
|
||||||
cls: cls.into(),
|
cls: cls.into(),
|
||||||
id,
|
id,
|
||||||
|
open,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,13 @@ side; a conveyor (rot 0 runs towards +x, 90 +y, 180 -x, 270 -y; w = length, prop
|
|||||||
at that side carries them, and a conveyor whose start is at another's end carries on with them (corners join). \
|
at that side carries them, and a conveyor whose start is at another's end carries on with them (corners join). \
|
||||||
The gap between goods is speed / rate, so set the rate on the producer and the speeds on the belts. \
|
The gap between goods is speed / rate, so set the rate on the producer and the speeds on the belts. \
|
||||||
A robot_arm (props.reach, default 2 m) picks goods off the end of the nearest conveyor within its reach and puts \
|
A robot_arm (props.reach, default 2 m) picks goods off the end of the nearest conveyor within its reach and puts \
|
||||||
them on the start of another conveyor within reach, or on a pallet beside it; place it 1.5 to 2 m past a belt's end.";
|
them on the start of another conveyor within reach, or on a pallet beside it; place it 1.5 to 2 m past a belt's end. \
|
||||||
|
Streets (category Ground) join by their centre lines: a road (x, y = middle, rot 0 along x, w = length, d = width, \
|
||||||
|
props.walk none/both/left/right adds raised sidewalks walk_w wide) ending on another road's centre line makes a \
|
||||||
|
junction there; a road_curve starts at its x, y heading along local +x (rot 0/90/180/270), 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 sharp corner. \
|
||||||
|
A roundabout (props.r ring radius, arm_e/n/w/s, arm_len) has its arm ends r + arm_len from its centre: put the \
|
||||||
|
roads' ends exactly there, with the same width as road_d.";
|
||||||
|
|
||||||
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,
|
||||||
|
|||||||
@@ -72,8 +72,8 @@ fn demo_passes_the_check() {
|
|||||||
assert!(f.is_empty(), "{f:?}");
|
assert!(f.is_empty(), "{f:?}");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The house and factory demos: every view draws every object without warnings or
|
/// The house, factory and streets demos: every view draws every object without warnings
|
||||||
/// depth-sort cycles (walls and belts must not overlap), and the projects pass the check.
|
/// or depth-sort cycles (walls and belts must not overlap), and the projects pass the check.
|
||||||
#[test]
|
#[test]
|
||||||
fn house_demo_renders_every_view_cleanly() {
|
fn house_demo_renders_every_view_cleanly() {
|
||||||
example_renders_every_view_cleanly("house");
|
example_renders_every_view_cleanly("house");
|
||||||
@@ -84,6 +84,11 @@ fn factory_demo_renders_every_view_cleanly() {
|
|||||||
example_renders_every_view_cleanly("factory");
|
example_renders_every_view_cleanly("factory");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn streets_demo_renders_every_view_cleanly() {
|
||||||
|
example_renders_every_view_cleanly("streets");
|
||||||
|
}
|
||||||
|
|
||||||
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"))
|
||||||
|
|||||||
@@ -321,6 +321,16 @@ pub fn register_sprite(e: &mut Engine) {
|
|||||||
ok(s)
|
ok(s)
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
// open polylines through plan points at height z (or 3D points without z)
|
||||||
|
e.register_fn(
|
||||||
|
"polyline",
|
||||||
|
|s: &mut SpriteH, p: D, z: D, cls: &str| -> RR<SpriteH> {
|
||||||
|
let z = n(&z)?;
|
||||||
|
let lines = polys3_or_flat(&p, z)?;
|
||||||
|
s.0.borrow_mut().polyline(lines, &classes(cls), None);
|
||||||
|
ok(s)
|
||||||
|
},
|
||||||
|
);
|
||||||
e.register_fn("poly", |s: &mut SpriteH, p: D, cls: &str| -> RR<SpriteH> {
|
e.register_fn("poly", |s: &mut SpriteH, p: D, cls: &str| -> RR<SpriteH> {
|
||||||
s.0.borrow_mut().poly(polys3(&p)?, &classes(cls), None);
|
s.0.borrow_mut().poly(polys3(&p)?, &classes(cls), None);
|
||||||
ok(s)
|
ok(s)
|
||||||
|
|||||||
@@ -90,6 +90,15 @@ pub fn polys3(d: &Dynamic) -> RR<Vec<Vec<P3>>> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// One line or several, of plan points `[x, y]` (put at height z) or 3D points.
|
||||||
|
pub fn polys3_or_flat(d: &Dynamic, z: f64) -> RR<Vec<Vec<P3>>> {
|
||||||
|
polys(d).map(|ls| {
|
||||||
|
ls.into_iter()
|
||||||
|
.map(|l| l.into_iter().map(|p| [p[0], p[1], z]).collect())
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// `[[a, b, c, d], ...]` (segments, opening lists)
|
/// `[[a, b, c, d], ...]` (segments, opening lists)
|
||||||
pub fn quads(d: &Dynamic, what: &str) -> RR<Vec<[f64; 4]>> {
|
pub fn quads(d: &Dynamic, what: &str) -> RR<Vec<[f64; 4]>> {
|
||||||
arr(d, what)?
|
arr(d, what)?
|
||||||
|
|||||||
@@ -123,6 +123,10 @@ pub struct PrefabMeta {
|
|||||||
/// turns it to point from where the drag starts to where it ends.
|
/// turns it to point from where the drag starts to where it ends.
|
||||||
#[serde(default, skip_serializing_if = "is_false")]
|
#[serde(default, skip_serializing_if = "is_false")]
|
||||||
pub directed: bool,
|
pub directed: bool,
|
||||||
|
/// A prefab that rounds the corners of a run (roads: "road_curve"): the
|
||||||
|
/// Studio puts one in each corner of a run drawn corner by corner.
|
||||||
|
#[serde(default, skip_serializing_if = "String::is_empty")]
|
||||||
|
pub corner: String,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub props: Vec<PropDef>,
|
pub props: Vec<PropDef>,
|
||||||
/// Module the prefab comes from ("isokit/site", "lib/kiosk.rhai").
|
/// Module the prefab comes from ("isokit/site", "lib/kiosk.rhai").
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
pub const MODULES: &[(&str, &str)] = &[
|
pub const MODULES: &[(&str, &str)] = &[
|
||||||
("isokit/util", include_str!("../stdlib/util.rhai")),
|
("isokit/util", include_str!("../stdlib/util.rhai")),
|
||||||
("isokit/ground", include_str!("../stdlib/ground.rhai")),
|
("isokit/ground", include_str!("../stdlib/ground.rhai")),
|
||||||
|
("isokit/streets", include_str!("../stdlib/streets.rhai")),
|
||||||
("isokit/nature", include_str!("../stdlib/nature.rhai")),
|
("isokit/nature", include_str!("../stdlib/nature.rhai")),
|
||||||
("isokit/building", include_str!("../stdlib/building.rhai")),
|
("isokit/building", include_str!("../stdlib/building.rhai")),
|
||||||
("isokit/vehicles", include_str!("../stdlib/vehicles.rhai")),
|
("isokit/vehicles", include_str!("../stdlib/vehicles.rhai")),
|
||||||
|
|||||||
@@ -732,6 +732,122 @@ fn robot_arms_pass_goods_from_belt_to_belt() {
|
|||||||
assert!(out.info["robot1"].rows[1].1.contains("waiting"));
|
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]
|
#[test]
|
||||||
fn site_walls_join_at_corners() {
|
fn site_walls_join_at_corners() {
|
||||||
let r = Runtime::new(&Bundle::default()).unwrap();
|
let r = Runtime::new(&Bundle::default()).unwrap();
|
||||||
|
|||||||
@@ -46,24 +46,28 @@ fn prefab_area(o) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn meta_road() {
|
fn meta_road() {
|
||||||
#{ label: "Road", category: "Ground", icon: "═", shape: "line",
|
#{ label: "Road", category: "Ground", icon: "═", shape: "line", corner: "road_curve",
|
||||||
help: "A straight road along its length, with a dashed centre line.",
|
help: "A straight road along its length, with a dashed centre line and sidewalks if you like (left and right looking along local +x). Roads ending on another road's side make a junction; a run drawn corner by corner gets a curve in each corner.",
|
||||||
props: [
|
props: [
|
||||||
#{ key: "w", type: "float", def: 40.0, min: 2.0, max: 2000.0, step: 1.0, label: "Length (m)" },
|
#{ 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: "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: "dashed", type: "bool", def: true, label: "Centre line" },
|
||||||
#{ key: "kerb", type: "bool", def: true, label: "Kerb lines" },
|
#{ key: "kerb", type: "bool", def: true, label: "Kerb lines" },
|
||||||
|
#{ key: "walk", type: "choice", def: "none", choices: walk_sides(), label: "Sidewalks" },
|
||||||
|
#{ key: "walk_w", type: "float", def: 2.0, min: 0.8, max: 8.0, step: 0.1, label: "Walk width (m)" },
|
||||||
] }
|
] }
|
||||||
}
|
}
|
||||||
fn prefab_road(o) {
|
fn prefab_road(o) {
|
||||||
let p = o.p;
|
let p = o.p;
|
||||||
let w = p.w / 2.0;
|
|
||||||
let d = p.d / 2.0;
|
|
||||||
layer("ground");
|
layer("ground");
|
||||||
let s = sprite();
|
let others = other_street_ends(o);
|
||||||
s.flat(rect(-w, -d, w, d), 0.007, "g-asph t");
|
let ends = street_ends(o);
|
||||||
if p.dashed { s.lines([[-w, 0.0, w, 0.0]], 0.012, "dash"); }
|
let c = line_path([[-p.w / 2.0, 0.0], [p.w / 2.0, 0.0]]);
|
||||||
if p.kerb { s.lines([[-w, -d, w, -d], [-w, d, w, d]], 0.012, "kerb"); }
|
let j0 = street_join(o, ends[0], others);
|
||||||
|
let j1 = street_join(o, ends[1], others);
|
||||||
|
let gaps = street_gaps(o, others);
|
||||||
|
street_path(o, c, j0, j1, gaps);
|
||||||
|
street_info(p, `${num1(p.w)} m long, ${num1(p.d)} m wide`, [j0.meet, j1.meet], gaps[2]);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn meta_parking() {
|
fn meta_parking() {
|
||||||
|
|||||||
@@ -0,0 +1,566 @@
|
|||||||
|
// isokit/streets: roads drawn along a centre line (straight roads, curves,
|
||||||
|
// roundabout arms) with kerbs and raised sidewalks, and how streets meet:
|
||||||
|
// - end to end they carry on seamlessly (a curve continues a road);
|
||||||
|
// - a road ending on another road's side leaves a gap in that road's
|
||||||
|
// sidewalk and kerb, and stops its own kerbs and sidewalks at it;
|
||||||
|
// - two roads ending at one corner close it: each runs over the corner, the
|
||||||
|
// inner sidewalks stop at the other road's kerb, the outer ones go round.
|
||||||
|
// Everything lies on the unsorted ground layer, so nothing can upset the
|
||||||
|
// depth sort; sidewalks are 12 cm high paving with their kerb face drawn
|
||||||
|
// where it is turned to the viewer.
|
||||||
|
|
||||||
|
fn walk_sides() { ["none", "both", "left", "right"] }
|
||||||
|
fn walk_h() { 0.12 }
|
||||||
|
fn street_kinds() { ["road", "road_curve", "roundabout"] }
|
||||||
|
|
||||||
|
/// Sidewalk widths [left, right] of a street looking along its local +x.
|
||||||
|
fn walk_widths(p) {
|
||||||
|
let w = p.walk_w;
|
||||||
|
if p.walk == "both" { [w, w] } else if p.walk == "left" { [w, 0.0] } else if p.walk == "right" { [0.0, w] } else { [0.0, 0.0] }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------- polylines
|
||||||
|
|
||||||
|
fn seg_len(a, b) { ((b[0] - a[0]) * (b[0] - a[0]) + (b[1] - a[1]) * (b[1] - a[1])).sqrt() }
|
||||||
|
|
||||||
|
fn path_len(c) {
|
||||||
|
let l = 0.0;
|
||||||
|
for i in 1..c.len() { l += seg_len(c[i - 1], c[i]); }
|
||||||
|
l
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rev(a) {
|
||||||
|
let r = a;
|
||||||
|
r.reverse();
|
||||||
|
r
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The point at distance s along polyline c; before its start and past its
|
||||||
|
/// end it goes on straight along the first and last segment.
|
||||||
|
fn path_at(c, s) {
|
||||||
|
let n = c.len();
|
||||||
|
if s <= 0.0 {
|
||||||
|
let a = c[0];
|
||||||
|
let b = c[1];
|
||||||
|
let l = seg_len(a, b);
|
||||||
|
return [a[0] + (b[0] - a[0]) / l * s, a[1] + (b[1] - a[1]) / l * s];
|
||||||
|
}
|
||||||
|
let acc = 0.0;
|
||||||
|
for i in 1..n {
|
||||||
|
let a = c[i - 1];
|
||||||
|
let b = c[i];
|
||||||
|
let l = seg_len(a, b);
|
||||||
|
if acc + l >= s || i == n - 1 {
|
||||||
|
let t = (s - acc) / l;
|
||||||
|
return [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t];
|
||||||
|
}
|
||||||
|
acc += l;
|
||||||
|
}
|
||||||
|
c[n - 1]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The part of polyline c from distance s0 to s1 along it (either may lie
|
||||||
|
/// beyond its ends).
|
||||||
|
fn path_cut(c, s0, s1) {
|
||||||
|
let out = [];
|
||||||
|
if s1 - s0 < 0.01 { return out; }
|
||||||
|
out.push(path_at(c, s0));
|
||||||
|
let acc = 0.0;
|
||||||
|
for i in 0..c.len() {
|
||||||
|
if i > 0 { acc += seg_len(c[i - 1], c[i]); }
|
||||||
|
if acc > s0 + 0.01 && acc < s1 - 0.01 { out.push(c[i]); }
|
||||||
|
}
|
||||||
|
out.push(path_at(c, s1));
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Polyline c moved sideways by off (positive: to the left of its way):
|
||||||
|
/// each segment along its own normal, neighbours meeting at their mitre.
|
||||||
|
fn path_offset(c, off) {
|
||||||
|
let n = c.len();
|
||||||
|
let ns = [];
|
||||||
|
for i in 1..n {
|
||||||
|
let dx = c[i][0] - c[i - 1][0];
|
||||||
|
let dy = c[i][1] - c[i - 1][1];
|
||||||
|
let l = fmax((dx * dx + dy * dy).sqrt(), 1e-12);
|
||||||
|
ns.push([-dy / l, dx / l]);
|
||||||
|
}
|
||||||
|
let out = [];
|
||||||
|
for i in 0..n {
|
||||||
|
let m = if i == 0 { ns[0] } else if i == n - 1 { ns[n - 2] } else {
|
||||||
|
let a = ns[i - 1];
|
||||||
|
let b = ns[i];
|
||||||
|
let k = 1.0 / fmax(1.0 + a[0] * b[0] + a[1] * b[1], 0.2);
|
||||||
|
[(a[0] + b[0]) * k, (a[1] + b[1]) * k]
|
||||||
|
};
|
||||||
|
out.push([c[i][0] + m[0] * off, c[i][1] + m[1] * off]);
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Points of the arc round (cx, cy), radius r, from angle a0 to a1
|
||||||
|
/// (radians, either way round): chords of at most 1.5 m and 6 degrees.
|
||||||
|
fn arc_pts(cx, cy, r, a0, a1) {
|
||||||
|
let da = (a1 - a0).abs();
|
||||||
|
let n = fmin(160.0, fmax(2.0, fmax((da * r / 1.5).ceiling(), (da / 0.105).ceiling()))).to_int();
|
||||||
|
let out = [];
|
||||||
|
for i in 0..(n + 1) {
|
||||||
|
let t = a0 + (a1 - a0) * i / n;
|
||||||
|
out.push([cx + r * t.cos(), cy + r * t.sin()]);
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
// A street's centre line: #{ arc: false, pts } (a polyline) or #{ arc: true,
|
||||||
|
// cx, cy, r, a0, a1 } (an arc from angle a0 to a1, either way round). Arcs
|
||||||
|
// stay arcs when cut and moved sideways, so kerbs and sidewalks meet the
|
||||||
|
// streets at their ends exactly (a polyline's end chord is a few degrees off).
|
||||||
|
|
||||||
|
fn line_path(pts) { #{ arc: false, pts: pts } }
|
||||||
|
fn arc_path(cx, cy, r, a0, a1) { #{ arc: true, cx: cx, cy: cy, r: r, a0: a0, a1: a1 } }
|
||||||
|
|
||||||
|
fn pl_len(p) { if p.arc { p.r * (p.a1 - p.a0).abs() } else { path_len(p.pts) } }
|
||||||
|
|
||||||
|
/// The part of centre line p from distance s0 to s1 along it.
|
||||||
|
fn pl_cut(p, s0, s1) {
|
||||||
|
if !p.arc { return line_path(path_cut(p.pts, s0, s1)); }
|
||||||
|
let dir = if p.a1 >= p.a0 { 1.0 } else { -1.0 };
|
||||||
|
arc_path(p.cx, p.cy, p.r, p.a0 + dir * s0 / p.r, p.a0 + dir * s1 / p.r)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Plan points of centre line p moved sideways by off (positive: left of its way).
|
||||||
|
fn pl_off(p, off) {
|
||||||
|
if !p.arc { return path_offset(p.pts, off); }
|
||||||
|
let dir = if p.a1 >= p.a0 { 1.0 } else { -1.0 };
|
||||||
|
arc_pts(p.cx, p.cy, p.r - dir * off, p.a0, p.a1)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [x, y, tx, ty] at the start (e = 0) or end (e = 1) of p: the point and
|
||||||
|
/// the unit direction of the way there.
|
||||||
|
fn pl_end(p, e) {
|
||||||
|
if p.arc {
|
||||||
|
let dir = if p.a1 >= p.a0 { 1.0 } else { -1.0 };
|
||||||
|
let a = if e == 0 { p.a0 } else { p.a1 };
|
||||||
|
return [p.cx + p.r * a.cos(), p.cy + p.r * a.sin(), -dir * a.sin(), dir * a.cos()];
|
||||||
|
}
|
||||||
|
let c = p.pts;
|
||||||
|
let n = c.len();
|
||||||
|
let a = if e == 0 { c[0] } else { c[n - 2] };
|
||||||
|
let b = if e == 0 { c[1] } else { c[n - 1] };
|
||||||
|
let l = seg_len(a, b);
|
||||||
|
let q = if e == 0 { a } else { b };
|
||||||
|
[q[0], q[1], (b[0] - a[0]) / l, (b[1] - a[1]) / l]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [a, b] without the ranges in gaps: the pieces left, at least 5 cm long.
|
||||||
|
fn ranges(a, b, gaps) {
|
||||||
|
let out = [[a, b]];
|
||||||
|
for g in gaps {
|
||||||
|
let next = [];
|
||||||
|
for r in out {
|
||||||
|
if g[1] <= r[0] || g[0] >= r[1] { next.push(r); continue; }
|
||||||
|
if g[0] > r[0] { next.push([r[0], g[0]]); }
|
||||||
|
if g[1] < r[1] { next.push([g[1], r[1]]); }
|
||||||
|
}
|
||||||
|
out = next;
|
||||||
|
}
|
||||||
|
out.filter(|r| r[1] - r[0] > 0.05)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------- kerbs and sidewalks
|
||||||
|
|
||||||
|
/// The faces up from line k to height h whose outward normal (to the right
|
||||||
|
/// of k, or to its left) is turned to the viewer: polygons for poly().
|
||||||
|
fn kerb_faces(k, h, right) {
|
||||||
|
let fs = [];
|
||||||
|
for i in 1..k.len() {
|
||||||
|
let a = k[i - 1];
|
||||||
|
let b = k[i];
|
||||||
|
let nx = b[1] - a[1];
|
||||||
|
let ny = a[0] - b[0];
|
||||||
|
if !right { nx = -nx; ny = -ny; }
|
||||||
|
if vis(nx, ny) { fs.push([[a[0], a[1], 0.0], [b[0], b[1], 0.0], [b[0], b[1], h], [a[0], a[1], h]]); }
|
||||||
|
}
|
||||||
|
fs
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A sidewalk between kerb line k (plan points with the road on their
|
||||||
|
/// right) and its back line o (as many points, w to the left): its paving,
|
||||||
|
/// back edge and the ends where caps says [start, end] (not where another
|
||||||
|
/// sidewalk carries on). The kerb face goes with the kerb.
|
||||||
|
fn walk_strip(k, o, caps) {
|
||||||
|
if k.len() < 2 || o.len() != k.len() { return this; }
|
||||||
|
let h = walk_h();
|
||||||
|
this.flat(k + rev(o), h, "g-walk t");
|
||||||
|
let fs = kerb_faces(o, h, false);
|
||||||
|
let n = k.len();
|
||||||
|
if caps[0] { fs += kerb_faces([o[0], k[0]], h, true); }
|
||||||
|
if caps[1] { fs += kerb_faces([k[n - 1], o[n - 1]], h, true); }
|
||||||
|
if fs.len() > 0 { this.poly(fs, "kerbf"); }
|
||||||
|
this
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------- where streets meet
|
||||||
|
|
||||||
|
/// The open ends of street o in world plan coordinates: [#{ x, y, dx, dy,
|
||||||
|
/// d, wl, wr }] with (dx, dy) the unit direction out of the street, d its
|
||||||
|
/// carriageway width and wl, wr its sidewalk widths left and right looking out.
|
||||||
|
fn street_ends(o) {
|
||||||
|
let ends = [];
|
||||||
|
let p = o.p;
|
||||||
|
let add = |u, v, du, dv, d, wl, wr| {
|
||||||
|
let a = world_of(o, u, v);
|
||||||
|
let b = world_of(o, u + du, v + dv);
|
||||||
|
ends.push(#{ x: a[0], y: a[1], dx: b[0] - a[0], dy: b[1] - a[1], d: d, wl: wl, wr: wr, id: o.id });
|
||||||
|
};
|
||||||
|
if o.type == "road" || o.type == "road_curve" {
|
||||||
|
let ws = walk_widths(p);
|
||||||
|
let c = if o.type == "road" { line_path([[-p.w / 2.0, 0.0], [p.w / 2.0, 0.0]]) } else { curve_line(p) };
|
||||||
|
let a = pl_end(c, 0);
|
||||||
|
let b = pl_end(c, 1);
|
||||||
|
add.call(a[0], a[1], -a[2], -a[3], p.d, ws[1], ws[0]);
|
||||||
|
add.call(b[0], b[1], b[2], b[3], p.d, ws[0], ws[1]);
|
||||||
|
} else if o.type == "roundabout" {
|
||||||
|
let w = if p.walk { p.walk_w } else { 0.0 };
|
||||||
|
let R = p.r + p.arm_len;
|
||||||
|
for a in round_arms(p) {
|
||||||
|
let ca = a.cos();
|
||||||
|
let sa = a.sin();
|
||||||
|
add.call(R * ca, R * sa, ca, sa, p.road_d, w, w);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ends
|
||||||
|
}
|
||||||
|
|
||||||
|
/// How street o meets the others at its end e (one of street_ends(o)):
|
||||||
|
/// #{ asph, dash, kl, kr, wl, wr, free, meet }: how far its asphalt, centre
|
||||||
|
/// line, kerbs and sidewalks (left and right looking out) reach past e
|
||||||
|
/// (negative: they stop short of it), whether nothing meets it there, and
|
||||||
|
/// the id of the street that does.
|
||||||
|
fn street_join(o, e, others) {
|
||||||
|
let j = #{ asph: 0.0, dash: 0.0, kl: 0.0, kr: 0.0, wl: 0.0, wr: 0.0, free: true, meet: "" };
|
||||||
|
// pieces that carry on overlap by a few cm: edge to edge, the ground shows through as a hairline
|
||||||
|
let lap = 0.04;
|
||||||
|
for f in others {
|
||||||
|
if (f.x - e.x).abs() > 0.3 || (f.y - e.y).abs() > 0.3 { continue; }
|
||||||
|
j.free = false;
|
||||||
|
j.meet = f.id;
|
||||||
|
let dot = e.dx * f.dx + e.dy * f.dy;
|
||||||
|
if dot < -0.9 { j.asph = lap; j.kl = lap; j.kr = lap; j.wl = lap; j.wr = lap; }
|
||||||
|
if dot.abs() > 0.2 { continue; }
|
||||||
|
// a corner: the other street's body lies to the left (cross > 0) or right
|
||||||
|
let left = e.dx * -f.dy - e.dy * -f.dx > 0.0;
|
||||||
|
// its outer side is the one away from this street's body
|
||||||
|
let mine_on_its_left = f.dx * -e.dy - f.dy * -e.dx > 0.0;
|
||||||
|
let wo = if mine_on_its_left { f.wr } else { f.wl };
|
||||||
|
let h = f.d / 2.0;
|
||||||
|
j.asph = h;
|
||||||
|
if left { j.kl = -h; j.wl = -h; j.kr = h; j.wr = h + wo; } else { j.kr = -h; j.wr = -h; j.kl = h; j.wl = h + wo; }
|
||||||
|
return j;
|
||||||
|
}
|
||||||
|
// ending on a road's side (a T): kerbs stop at its kerb, sidewalks at its sidewalk
|
||||||
|
for c in objects_on(["road"], e.x, e.y, 0.3) {
|
||||||
|
if c.id == o.id || c.v.abs() > 0.3 || c.u.abs() > c.p.w / 2.0 - 0.3 { continue; }
|
||||||
|
let l = local_of(c, e.x - e.dx, e.y - e.dy);
|
||||||
|
let ws = walk_widths(c.p);
|
||||||
|
let h = c.p.d / 2.0;
|
||||||
|
let w = if l[1] > c.v { ws[0] } else { ws[1] };
|
||||||
|
j.free = false;
|
||||||
|
j.meet = c.id;
|
||||||
|
j.dash = -h;
|
||||||
|
j.kl = -h;
|
||||||
|
j.kr = -h;
|
||||||
|
j.wl = -(h + w) + 0.04;
|
||||||
|
j.wr = -(h + w) + 0.04;
|
||||||
|
return j;
|
||||||
|
}
|
||||||
|
j
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Where the streets in `others` end on road o's sides: gaps [left, right]
|
||||||
|
/// in its kerbs and sidewalks, as distance ranges along it from its start,
|
||||||
|
/// then the ids of those streets.
|
||||||
|
fn street_gaps(o, others) {
|
||||||
|
let g = [[], [], []];
|
||||||
|
for f in others {
|
||||||
|
let l = local_of(o, f.x, f.y);
|
||||||
|
if l[1].abs() > 0.3 || l[0].abs() > o.p.w / 2.0 - 0.3 { continue; }
|
||||||
|
let b = local_of(o, f.x - f.dx, f.y - f.dy);
|
||||||
|
let s = l[0] + o.p.w / 2.0;
|
||||||
|
let r = [s - f.d / 2.0, s + f.d / 2.0];
|
||||||
|
if b[1] > l[1] { g[0].push(r); } else { g[1].push(r); }
|
||||||
|
g[2].push(f.id);
|
||||||
|
}
|
||||||
|
g
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tooltip rows of a street: its size, sidewalks and the streets it meets
|
||||||
|
/// (ids, at its ends and along its sides).
|
||||||
|
fn street_info(p, what, ends, sides) {
|
||||||
|
info_row("Street", what);
|
||||||
|
let ws = walk_widths(p);
|
||||||
|
if ws[0] > 0.0 && ws[1] > 0.0 { info_row("Sidewalks", `${num1(p.walk_w)} m on both sides`); } else if ws[0] > 0.0 { info_row("Sidewalks", `${num1(p.walk_w)} m on the left`); } else if ws[1] > 0.0 { info_row("Sidewalks", `${num1(p.walk_w)} m on the right`); }
|
||||||
|
let meets = [];
|
||||||
|
for id in ends + sides { if id != "" && !meets.contains(id) { meets.push(id); } }
|
||||||
|
if meets.len() > 0 { info_row("Meets", meets.reduce(|s, x| if s == "" { x } else { s + ", " + x }, "")); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The ends of every other street on this level.
|
||||||
|
fn other_street_ends(o) {
|
||||||
|
let out = [];
|
||||||
|
for c in objects_of(street_kinds()) {
|
||||||
|
if c.id != o.id { out += street_ends(c); }
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draws street o along centre line c (a line_path or arc_path in its
|
||||||
|
/// frame, in its way), its carriageway o.p.d wide, joined at its start and
|
||||||
|
/// end as j0 and j1 (from street_join), with sidewalk gaps [left, right]
|
||||||
|
/// (distance ranges along c).
|
||||||
|
fn street_path(o, c, j0, j1, gaps) {
|
||||||
|
let p = o.p;
|
||||||
|
let L = pl_len(c);
|
||||||
|
let h = p.d / 2.0;
|
||||||
|
let ws = walk_widths(p);
|
||||||
|
let s = sprite();
|
||||||
|
let ca = pl_cut(c, -j0.asph, L + j1.asph);
|
||||||
|
s.flat(pl_off(ca, h) + rev(pl_off(ca, -h)), 0.007, "g-asph t");
|
||||||
|
if p.dashed && L + j1.dash + j0.dash > 0.05 {
|
||||||
|
s.polyline(pl_off(pl_cut(c, -j0.dash, L + j1.dash), 0.0), 0.012, "dash");
|
||||||
|
}
|
||||||
|
// side 0: left of the way (the start end's right, the end's left); side 1: right
|
||||||
|
for side in [0, 1] {
|
||||||
|
let sg = if side == 0 { 1.0 } else { -1.0 };
|
||||||
|
let k0 = if side == 0 { j0.kr } else { j0.kl };
|
||||||
|
let k1 = if side == 0 { j1.kl } else { j1.kr };
|
||||||
|
let w0 = if side == 0 { j0.wr } else { j0.wl };
|
||||||
|
let w1 = if side == 0 { j1.wl } else { j1.wr };
|
||||||
|
let ww = ws[side];
|
||||||
|
for r in ranges(-k0, L + k1, gaps[side]) {
|
||||||
|
let line = pl_off(pl_cut(c, r[0], r[1]), sg * h);
|
||||||
|
if side == 1 { line.reverse(); }
|
||||||
|
if ww > 0.0 {
|
||||||
|
let fs = kerb_faces(line, walk_h(), true);
|
||||||
|
if fs.len() > 0 { s.poly(fs, "kerbf"); }
|
||||||
|
} else if p.kerb {
|
||||||
|
s.polyline(line, 0.012, "kerb");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ww > 0.0 {
|
||||||
|
let rs = ranges(-w0, L + w1, gaps[side]);
|
||||||
|
for i in 0..rs.len() {
|
||||||
|
let r = rs[i];
|
||||||
|
let cut = pl_cut(c, r[0], r[1]);
|
||||||
|
let line = pl_off(cut, sg * h);
|
||||||
|
let back = pl_off(cut, sg * (h + ww));
|
||||||
|
// caps where a piece ends at a gap or a free street end (sides run opposite ways)
|
||||||
|
let at0 = i > 0 || j0.free;
|
||||||
|
let at1 = i + 1 < rs.len() || j1.free;
|
||||||
|
if side == 1 { line.reverse(); back.reverse(); }
|
||||||
|
s.walk_strip(line, back, if side == 0 { [at0, at1] } else { [at1, at0] });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------- curves
|
||||||
|
|
||||||
|
/// The centre line of curve p in its frame: from (0, 0) along +x, turning
|
||||||
|
/// left (towards +y) or right by p.angle degrees on radius p.r.
|
||||||
|
fn curve_line(p) {
|
||||||
|
let A = p.angle * PI() / 180.0;
|
||||||
|
let sg = if p.turn == "right" { -1.0 } else { 1.0 };
|
||||||
|
arc_path(0.0, sg * p.r, p.r, -sg * PI() / 2.0, -sg * PI() / 2.0 + sg * A)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn meta_road_curve() {
|
||||||
|
#{ label: "Road curve", category: "Ground", icon: "◜",
|
||||||
|
help: "A curved road piece. It starts at its position heading along local +x (turn it in 90 degree steps) and bends left or right; its ends meet roads and other curves end to end. Drawing a road run corner by corner puts one in each corner.",
|
||||||
|
props: [
|
||||||
|
#{ key: "r", type: "float", def: 10.0, min: 3.0, max: 300.0, step: 0.5, label: "Radius (m)" },
|
||||||
|
#{ key: "angle", type: "float", def: 90.0, min: 5.0, max: 180.0, step: 5.0, label: "Angle (deg)" },
|
||||||
|
#{ key: "turn", type: "choice", def: "left", choices: ["left", "right"], label: "Turns" },
|
||||||
|
#{ 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" },
|
||||||
|
#{ key: "walk", type: "choice", def: "none", choices: walk_sides(), label: "Sidewalks" },
|
||||||
|
#{ key: "walk_w", type: "float", def: 2.0, min: 0.8, max: 8.0, step: 0.1, label: "Walk width (m)" },
|
||||||
|
] }
|
||||||
|
}
|
||||||
|
fn prefab_road_curve(o) {
|
||||||
|
let p = o.p;
|
||||||
|
if p.r <= p.d / 2.0 + 0.2 { warn(`${o.id}: radius ${p.r} m is too tight for a ${p.d} m road`); }
|
||||||
|
layer("ground");
|
||||||
|
let c = curve_line(p);
|
||||||
|
let others = other_street_ends(o);
|
||||||
|
let ends = street_ends(o);
|
||||||
|
let j0 = street_join(o, ends[0], others);
|
||||||
|
let j1 = street_join(o, ends[1], others);
|
||||||
|
street_path(o, c, j0, j1, [[], []]);
|
||||||
|
street_info(p, `${num1(p.angle)} degree turn to the ${p.turn}, ${num1(p.r)} m radius, ${num1(p.d)} m wide`, [j0.meet, j1.meet], []);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------- roundabouts
|
||||||
|
|
||||||
|
/// The angles (radians, in the roundabout's frame) of its arms.
|
||||||
|
fn round_arms(p) {
|
||||||
|
let out = [];
|
||||||
|
if p.arm_e { out.push(0.0); }
|
||||||
|
if p.arm_n { out.push(PI() / 2.0); }
|
||||||
|
if p.arm_w { out.push(PI()); }
|
||||||
|
if p.arm_s { out.push(PI() * 1.5); }
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
/// (x, y) turned by angle a.
|
||||||
|
fn turned(a, x, y) { [x * a.cos() - y * a.sin(), x * a.sin() + y * a.cos()] }
|
||||||
|
|
||||||
|
fn meta_roundabout() {
|
||||||
|
#{ label: "Roundabout", category: "Ground", icon: "◎",
|
||||||
|
help: "A roundabout with up to four arms (east, north, west, south in its frame) ending Radius + Arm length from its centre, where straight roads carry on. Kerbs flare into the ring; splitter islands and give-way lines sit at the entries (traffic on the left or the right).",
|
||||||
|
props: [
|
||||||
|
#{ key: "r", type: "float", def: 14.0, min: 6.0, max: 60.0, step: 0.5, label: "Radius (m)" },
|
||||||
|
#{ key: "lane", type: "float", def: 7.0, min: 4.0, max: 16.0, step: 0.5, label: "Ring width (m)" },
|
||||||
|
#{ key: "lanes", type: "int", def: 1, min: 1, max: 2, step: 1, label: "Ring lanes" },
|
||||||
|
#{ key: "arm_e", type: "bool", def: true, label: "Arm east" },
|
||||||
|
#{ key: "arm_n", type: "bool", def: true, label: "Arm north" },
|
||||||
|
#{ key: "arm_w", type: "bool", def: true, label: "Arm west" },
|
||||||
|
#{ key: "arm_s", type: "bool", def: true, label: "Arm south" },
|
||||||
|
#{ key: "arm_len", type: "float", def: 10.0, min: 4.0, max: 80.0, step: 0.5, label: "Arm length (m)" },
|
||||||
|
#{ key: "road_d", type: "float", def: 6.0, min: 3.0, max: 20.0, step: 0.5, label: "Arm width (m)" },
|
||||||
|
#{ key: "island", type: "choice", def: "tree", choices: ["tree", "grass", "paved"], label: "Island" },
|
||||||
|
#{ key: "splitters", type: "bool", def: true, label: "Splitter islands" },
|
||||||
|
#{ key: "drive", type: "choice", def: "left", choices: ["left", "right"], label: "Traffic on the" },
|
||||||
|
#{ key: "walk", type: "bool", def: true, label: "Sidewalks" },
|
||||||
|
#{ key: "walk_w", type: "float", def: 2.0, min: 0.8, max: 8.0, step: 0.1, label: "Walk width (m)" },
|
||||||
|
] }
|
||||||
|
}
|
||||||
|
fn prefab_roundabout(o) {
|
||||||
|
let p = o.p;
|
||||||
|
let r = p.r;
|
||||||
|
let ri = fmax(1.5, r - p.lane);
|
||||||
|
let h = p.road_d / 2.0;
|
||||||
|
let R = r + p.arm_len;
|
||||||
|
let ww = if p.walk { p.walk_w } else { 0.0 };
|
||||||
|
let arms = round_arms(p);
|
||||||
|
// arm ends where a street carries on: no sidewalk end faces there
|
||||||
|
let others = other_street_ends(o);
|
||||||
|
let carried = |a| {
|
||||||
|
let e = world_of(o, R * a.cos(), R * a.sin());
|
||||||
|
others.some(|f| (f.x - e[0]).abs() < 0.3 && (f.y - e[1]).abs() < 0.3)
|
||||||
|
};
|
||||||
|
layer("ground");
|
||||||
|
let s = sprite();
|
||||||
|
// flared kerbs: a fillet of radius f between each arm edge and the ring
|
||||||
|
let f = fmax(ww + 1.0, fmin(6.0, p.arm_len * 0.6));
|
||||||
|
let cx = ((r + f) * (r + f) - (h + f) * (h + f)).sqrt();
|
||||||
|
if cx > R - 0.3 {
|
||||||
|
f = fmax(0.5, ww + 0.5);
|
||||||
|
cx = ((r + f) * (r + f) - (h + f) * (h + f)).sqrt();
|
||||||
|
}
|
||||||
|
let phi = plan_angle(cx, h + f);
|
||||||
|
let xi = (r * r - h * h).sqrt();
|
||||||
|
// carriageway: ring, arms and the flares between them
|
||||||
|
s.flat([arc_pts(0.0, 0.0, r, 0.0, 2.0 * PI()), arc_pts(0.0, 0.0, ri, 0.0, 2.0 * PI())], 0.007, "g-asph t");
|
||||||
|
// each arm with its flares is one outline reaching into the ring's
|
||||||
|
// asphalt (pieces sharing an edge would show a hairline seam)
|
||||||
|
let flares = [];
|
||||||
|
for a in arms {
|
||||||
|
let fr = turned(a, cx, -(h + f));
|
||||||
|
let fl = turned(a, cx, h + f);
|
||||||
|
let out = [turned(a, R, -h), turned(a, cx, -h)];
|
||||||
|
out += arc_pts(fr[0], fr[1], f, a + PI() / 2.0, a + PI() - phi);
|
||||||
|
out += [turned(a, xi - 0.5, -h), turned(a, xi - 0.5, h)];
|
||||||
|
out += arc_pts(fl[0], fl[1], f, a - PI() + phi, a - PI() / 2.0);
|
||||||
|
out += [turned(a, cx, h), turned(a, R, h)];
|
||||||
|
flares.push(out);
|
||||||
|
}
|
||||||
|
if flares.len() > 0 { s.flat(flares, 0.007, "g-asph t"); }
|
||||||
|
// the kerb line round the outside, sector by sector between the arms
|
||||||
|
let chains = [];
|
||||||
|
let caps = [];
|
||||||
|
if arms.len() == 0 {
|
||||||
|
chains.push(arc_pts(0.0, 0.0, r, 2.0 * PI(), 0.0));
|
||||||
|
caps.push([false, false]);
|
||||||
|
} else {
|
||||||
|
for i in 0..arms.len() {
|
||||||
|
let a = arms[i];
|
||||||
|
let b = if i + 1 < arms.len() { arms[i + 1] } else { arms[0] + 2.0 * PI() };
|
||||||
|
let ch = [turned(a, R, h), turned(a, cx, h)];
|
||||||
|
let fa = turned(a, cx, h + f);
|
||||||
|
ch += arc_pts(fa[0], fa[1], f, a - PI() / 2.0, a - PI() + phi);
|
||||||
|
ch += arc_pts(0.0, 0.0, r, a + phi, b - phi);
|
||||||
|
let fb = turned(b, cx, -(h + f));
|
||||||
|
ch += arc_pts(fb[0], fb[1], f, b + PI() - phi, b + PI() / 2.0);
|
||||||
|
ch.push(turned(b, R, -h));
|
||||||
|
// drawn with the road on its right: from arm b's end round to arm a's
|
||||||
|
chains.push(rev(ch));
|
||||||
|
caps.push([!carried.call(b), !carried.call(a)]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for i in 0..chains.len() {
|
||||||
|
let ch = chains[i];
|
||||||
|
if ww > 0.0 {
|
||||||
|
let fs = kerb_faces(ch, walk_h(), true);
|
||||||
|
if fs.len() > 0 { s.poly(fs, "kerbf"); }
|
||||||
|
s.walk_strip(ch, path_offset(ch, ww), caps[i]);
|
||||||
|
} else {
|
||||||
|
s.polyline(ch, 0.012, "kerb");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// markings: give-way lines across the entry lanes, the lane line, the arms' centre lines
|
||||||
|
let entry = if p.drive == "right" { 1.0 } else { -1.0 };
|
||||||
|
let sl = if p.splitters { fmin(6.0, p.arm_len - 1.5) } else { 0.0 };
|
||||||
|
let segs = [];
|
||||||
|
let gw = [];
|
||||||
|
for a in arms {
|
||||||
|
let x0 = r + 0.5;
|
||||||
|
let ia = if p.splitters { 1.0 } else { 0.2 };
|
||||||
|
let g0 = turned(a, x0, entry * ia);
|
||||||
|
let g1 = turned(a, x0, entry * (h - 0.25));
|
||||||
|
gw.push([g0[0], g0[1], g1[0], g1[1]]);
|
||||||
|
let d0 = turned(a, x0 + sl + 0.6, 0.0);
|
||||||
|
let d1 = turned(a, R, 0.0);
|
||||||
|
if R - (x0 + sl + 0.6) > 0.5 { segs.push([d0[0], d0[1], d1[0], d1[1]]); }
|
||||||
|
}
|
||||||
|
if gw.len() > 0 { s.lines(gw, 0.012, "giveway"); }
|
||||||
|
if segs.len() > 0 { s.lines(segs, 0.012, "dash"); }
|
||||||
|
if p.lanes > 1 { s.polyline(arc_pts(0.0, 0.0, (r + ri) / 2.0, 0.0, 2.0 * PI()), 0.012, "lane"); }
|
||||||
|
// the raised islands: splitters at the entries, the centre island with its apron
|
||||||
|
let kh = walk_h();
|
||||||
|
if p.splitters {
|
||||||
|
for a in arms {
|
||||||
|
let x0 = r + 0.8;
|
||||||
|
let isl = [turned(a, x0, -0.9), turned(a, x0 + sl, -0.25), turned(a, x0 + sl, 0.25), turned(a, x0, 0.9)];
|
||||||
|
s.flat(isl, kh, "g-walk t");
|
||||||
|
let fs = kerb_faces(isl + [isl[0]], kh, true);
|
||||||
|
if fs.len() > 0 { s.poly(fs, "kerbf"); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let ring = arc_pts(0.0, 0.0, ri, 0.0, 2.0 * PI());
|
||||||
|
let apron = fmin(1.5, ri * 0.35);
|
||||||
|
let top = if p.island == "paved" { "g-walk" } else { "g-grass" };
|
||||||
|
s.flat(ring, kh, "g-apron t");
|
||||||
|
s.flat(arc_pts(0.0, 0.0, ri - apron, 0.0, 2.0 * PI()), kh + 0.01, top + " t");
|
||||||
|
let fs = kerb_faces(ring, kh, true);
|
||||||
|
if fs.len() > 0 { s.poly(fs, "kerbf"); }
|
||||||
|
if p.island == "tree" {
|
||||||
|
layer("main");
|
||||||
|
sprite().tree_at(0.0, 0.0, fmin(3.0, (ri - apron) * 0.7));
|
||||||
|
}
|
||||||
|
let names = ["east", "north", "west", "south"];
|
||||||
|
let on = [p.arm_e, p.arm_n, p.arm_w, p.arm_s];
|
||||||
|
let which = [];
|
||||||
|
for i in 0..4 { if on[i] { which.push(names[i]); } }
|
||||||
|
info_row("Ring", `${num1(r)} m radius, ${num1(p.lane)} m wide, ${p.lanes} lane${if p.lanes > 1 { "s" } else { "" }}`);
|
||||||
|
if arms.len() > 0 { info_row("Arms", `${arms.len()}: ${which.reduce(|s, x| if s == "" { x } else { s + ", " + x }, "")}; ${num1(p.road_d)} m wide`); }
|
||||||
|
info_row("Traffic", `on the ${p.drive}, round the island ${if p.drive == "left" { "clockwise" } else { "anticlockwise" }}`);
|
||||||
|
let meets = [];
|
||||||
|
for a in arms {
|
||||||
|
let e = world_of(o, R * a.cos(), R * a.sin());
|
||||||
|
for f in others { if (f.x - e[0]).abs() < 0.3 && (f.y - e[1]).abs() < 0.3 && !meets.contains(f.id) { meets.push(f.id); } }
|
||||||
|
}
|
||||||
|
if meets.len() > 0 { info_row("Meets", meets.reduce(|s, x| if s == "" { x } else { s + ", " + x }, "")); }
|
||||||
|
}
|
||||||
@@ -272,6 +272,10 @@ mach3 = "#5b6370"
|
|||||||
mach4 = "#b8692b"
|
mach4 = "#b8692b"
|
||||||
robot = "#e07a1f"
|
robot = "#e07a1f"
|
||||||
robotb = "#3a3f48"
|
robotb = "#3a3f48"
|
||||||
|
# streets: sidewalk paving, kerb stone faces, roundabout apron
|
||||||
|
walk = "#555c6b"
|
||||||
|
kerbf = "#7d8494"
|
||||||
|
apron = "#4a4840"
|
||||||
|
|
||||||
[palette.light]
|
[palette.light]
|
||||||
sky1 = "#a9d4fb"
|
sky1 = "#a9d4fb"
|
||||||
@@ -538,6 +542,9 @@ mach3 = "#8a93a1"
|
|||||||
mach4 = "#e08a43"
|
mach4 = "#e08a43"
|
||||||
robot = "#f08c2b"
|
robot = "#f08c2b"
|
||||||
robotb = "#5a616d"
|
robotb = "#5a616d"
|
||||||
|
walk = "#d9d6cf"
|
||||||
|
kerbf = "#a5a9b1"
|
||||||
|
apron = "#b9ad98"
|
||||||
|
|
||||||
[materials]
|
[materials]
|
||||||
warn = "#e0412f"
|
warn = "#e0412f"
|
||||||
@@ -548,6 +555,8 @@ g-asph = "asph"
|
|||||||
g-conc = "conc"
|
g-conc = "conc"
|
||||||
g-pad = "pad"
|
g-pad = "pad"
|
||||||
g-path = "path"
|
g-path = "path"
|
||||||
|
g-walk = "walk"
|
||||||
|
g-apron = "apron"
|
||||||
plinth = "plinth"
|
plinth = "plinth"
|
||||||
wall = "wall"
|
wall = "wall"
|
||||||
wall2 = "wall2"
|
wall2 = "wall2"
|
||||||
@@ -809,6 +818,9 @@ f-conc = "conc"
|
|||||||
".iso-svg .iso-win" = "fill: var(--iso-win); stroke: none"
|
".iso-svg .iso-win" = "fill: var(--iso-win); stroke: none"
|
||||||
".iso-svg .iso-school" = "pointer-events: none"
|
".iso-svg .iso-school" = "pointer-events: none"
|
||||||
".iso-svg .iso-swimbox" = "fill: none; stroke: none"
|
".iso-svg .iso-swimbox" = "fill: none; stroke: none"
|
||||||
|
".iso-svg .iso-kerbf" = "fill: var(--iso-kerbf); stroke: none"
|
||||||
|
".iso-svg .iso-lane" = "fill: none; stroke: var(--iso-dash); stroke-width: .6; stroke-dasharray: 4 6"
|
||||||
|
".iso-svg .iso-giveway" = "fill: none; stroke: var(--iso-dash); stroke-width: .9; stroke-dasharray: 2 2"
|
||||||
".iso-svg .iso-ledr" = "fill: var(--iso-led-r); stroke: none"
|
".iso-svg .iso-ledr" = "fill: var(--iso-led-r); stroke: none"
|
||||||
".iso-svg .iso-ledg" = "fill: var(--iso-led-g); stroke: none"
|
".iso-svg .iso-ledg" = "fill: var(--iso-led-g); stroke: none"
|
||||||
".iso-svg .iso-ledo" = "fill: var(--iso-led-o); stroke: none"
|
".iso-svg .iso-ledo" = "fill: var(--iso-led-o); stroke: none"
|
||||||
|
|||||||
+17
-5
@@ -34,7 +34,8 @@ Each phase ends with tests, a Studio/viewer check in Edge, commit and push.
|
|||||||
- [x] Code editor for scripts, prefabs, theme and manifest, with script errors by file and line
|
- [x] Code editor for scripts, prefabs, theme and manifest, with script errors by file and line
|
||||||
- [x] MCP tools inside the page (`isokit.mcp`), WebMCP registration when the browser offers it
|
- [x] MCP tools inside the page (`isokit.mcp`), WebMCP registration when the browser offers it
|
||||||
- [x] `tools/build_web.py` (static output), `tools/check_web.py` (44 checks, Edge)
|
- [x] `tools/build_web.py` (static output), `tools/check_web.py` (44 checks, Edge)
|
||||||
- [ ] Deploy a hosted copy; render in a Web Worker for very large scenes
|
- [x] Hosted copy at https://isokit.buildfor.life/ (deployed by `.gitea/workflows/deploy-web.yml`)
|
||||||
|
- [ ] Render in a Web Worker for very large scenes
|
||||||
|
|
||||||
## Agent window (done)
|
## Agent window (done)
|
||||||
|
|
||||||
@@ -75,9 +76,9 @@ Each phase ends with tests, a Studio/viewer check in Edge, commit and push.
|
|||||||
2. ~~Conveyors~~ (done, see below)
|
2. ~~Conveyors~~ (done, see below)
|
||||||
3. ~~Producers with adjustable production speed~~ (done, see below)
|
3. ~~Producers with adjustable production speed~~ (done, see below)
|
||||||
4. ~~Robotic arms~~ (done, see below)
|
4. ~~Robotic arms~~ (done, see below)
|
||||||
5. Rounded streets: curved road segments
|
5. ~~Rounded streets: curved road segments~~ (done, see below)
|
||||||
6. Sidewalks along streets
|
6. ~~Sidewalks along streets~~ (done, see below)
|
||||||
7. Roundabouts
|
7. ~~Roundabouts~~ (done, see below)
|
||||||
8. Waypoint animation: a person or car moving along a path of waypoints
|
8. Waypoint animation: a person or car moving along a path of waypoints
|
||||||
9. ~~Build your own house~~ (done, see above)
|
9. ~~Build your own house~~ (done, see above)
|
||||||
|
|
||||||
@@ -108,12 +109,23 @@ another belt or on a pallet, one move per item (or a set cycle), waiting while t
|
|||||||
stands; the belt after it runs in step. Drawn as a 16-pose flipbook (inverse kinematics, links as
|
stands; the belt after it runs in step. Drawn as a 16-pose flipbook (inverse kinematics, links as
|
||||||
the new native `beam` primitive) shown by a stepped CSS animation.
|
the new native `beam` primitive) shown by a stepped CSS animation.
|
||||||
|
|
||||||
|
Streets (stdlib/streets.rhai): roads take sidewalks (none, both, left, right; 12 cm high paving with
|
||||||
|
kerb faces); Road curve (radius, angle, left / right) carries a street round a bend, its kerbs and
|
||||||
|
sidewalks drawn as true arcs so the 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 and give-way lines on the entry side (traffic on the left or right), central island with
|
||||||
|
apron (tree, grass or paved). A road run drawn corner by corner gets a curve in each corner (prefab
|
||||||
|
meta `corner`). Tooltips list the streets each one meets. New `polyline` primitive (open paths, so
|
||||||
|
dashes run on round curves). `examples/streets`, Streets demo in the web Studio.
|
||||||
|
|
||||||
## Phase 2: richer editing
|
## Phase 2: richer editing
|
||||||
|
|
||||||
- [x] Interior prefabs (see Build your own house); a building `rooms` prop is still open
|
- [x] Interior prefabs (see Build your own house); a building `rooms` prop is still open
|
||||||
- [ ] Path objects in the Studio: click points for areas, roads (polyline with width), fences, walls,
|
- [ ] Path objects in the Studio: click points for areas, roads (polyline with width), fences, walls,
|
||||||
pipe runs; vertex handles
|
pipe runs; vertex handles
|
||||||
- [ ] Multi-select (box select, Shift-click), group move, align/distribute
|
- [x] Multi-select (box select, Ctrl+click, Shift range), group move, turn, copy, delete, edit
|
||||||
|
- [ ] Align / distribute the selection
|
||||||
- [ ] Copy/paste between projects; prefab thumbnails in the palette (rendered icons)
|
- [ ] Copy/paste between projects; prefab thumbnails in the palette (rendered icons)
|
||||||
- [ ] Measure tool and grid overlay; numeric resize handles on the selection
|
- [ ] Measure tool and grid overlay; numeric resize handles on the selection
|
||||||
- [ ] Studio editor for `theme.toml` (colour pickers) and for project parameters
|
- [ ] Studio editor for `theme.toml` (colour pickers) and for project parameters
|
||||||
|
|||||||
@@ -88,6 +88,7 @@ shaded automatically by face direction.
|
|||||||
| `flat(poly or polys, z, cls[, id])` | flat polygon(s); later ones cut holes |
|
| `flat(poly or polys, z, cls[, id])` | flat polygon(s); later ones cut holes |
|
||||||
| `open(a, b, [[u0, u1, z0, z1], …], cls[, id])` | windows / doors on the face a→b (outward normal on the right) |
|
| `open(a, b, [[u0, u1, z0, z1], …], cls[, id])` | windows / doors on the face a→b (outward normal on the right) |
|
||||||
| `lines(segs, z, cls)`, `disc(cx, cy, z, r, cls[, id])` | markings |
|
| `lines(segs, z, cls)`, `disc(cx, cy, z, r, cls[, id])` | markings |
|
||||||
|
| `polyline(lines, z, cls)` | open lines through plan points at height z (or 3D points): a dashed line stays one dash run round a curve |
|
||||||
| `circle(x, y, z, r_px, cls[, dx, dy])`, `stem(x, y, z0, z1, w_px, cls)` | screen-space details (crowns, poles) |
|
| `circle(x, y, z, r_px, cls[, dx, dy])`, `stem(x, y, z0, z1, w_px, cls)` | screen-space details (crowns, poles) |
|
||||||
| `person(x, y, z[, cls])`, `sit(x, y, z, cls)`, `head(x, y, z, r)`, `led(x, y, z[, cls])` | figures |
|
| `person(x, y, z[, cls])`, `sit(x, y, z, cls)`, `head(x, y, z, r)`, `led(x, y, z[, cls])` | figures |
|
||||||
| `puffs(x, y, z[, kind[, n, dur]])`, `fan(x, y, z, r)` | animated steam / smoke / fans |
|
| `puffs(x, y, z[, kind[, n, dur]])`, `fan(x, y, z, r)` | animated steam / smoke / fans |
|
||||||
@@ -153,6 +154,34 @@ through the document, like house parts:
|
|||||||
Poses pointing away from the viewer are drawn before the base, the others
|
Poses pointing away from the viewer are drawn before the base, the others
|
||||||
after it.
|
after it.
|
||||||
|
|
||||||
|
## Streets
|
||||||
|
|
||||||
|
`road`, `road_curve` and `roundabout` (stdlib/streets.rhai; `road` lives in
|
||||||
|
ground.rhai) meet by their centre lines:
|
||||||
|
|
||||||
|
- Each street is a centre line (a polyline or a true arc) with the
|
||||||
|
carriageway `d` wide around it; `walk` (none, both, left, right, looking
|
||||||
|
along local +x) adds sidewalks `walk_w` wide behind the kerbs: 12 cm high
|
||||||
|
paving with the kerb face drawn where it is turned to the viewer. Arcs stay
|
||||||
|
arcs when cut and offset, so a curve meets a straight road exactly.
|
||||||
|
- End to end (a curve continuing a road, a road continuing a roundabout arm)
|
||||||
|
the pieces carry on, overlapping by 4 cm so no hairline shows.
|
||||||
|
- A street ending on a road's centre line makes a T: the road leaves a gap in
|
||||||
|
its kerb and sidewalk on that side, the street stops its centre line and
|
||||||
|
kerbs at the road's edge and its sidewalks at the road's sidewalk.
|
||||||
|
- Two roads ending at one point at right angles close the corner: both run
|
||||||
|
over it, the inner sidewalks stop at the other road's kerb, the outer ones
|
||||||
|
go round.
|
||||||
|
- A roundabout's arms end `r + arm_len` from its centre, along its local
|
||||||
|
+x, +y, -x, -y. Its kerbs flare into the ring with a fillet; splitter
|
||||||
|
islands and give-way lines sit on the entry side for `drive` left or right.
|
||||||
|
- The prefab meta `corner: "road_curve"` on `road` tells the Studio to round
|
||||||
|
each corner of a road run with a curve (up to its default radius, less
|
||||||
|
where the pieces next to a corner are short).
|
||||||
|
- `street_ends(o)`, `street_join(o, end, others)`, `street_gaps(o, others)`
|
||||||
|
and `street_path(o, line, j0, j1, gaps)` are there for your own street
|
||||||
|
prefabs; tooltips list the streets each one meets (`Meets`).
|
||||||
|
|
||||||
## House parts
|
## House parts
|
||||||
|
|
||||||
`house_wall`, `window`, `door`, `floor`, `roof`, `stairs` and the furniture
|
`house_wall`, `window`, `door`, `floor`, `roof`, `stairs` and the furniture
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
# isokit streets demo: roads with sidewalks, a rounded corner, junctions and a roundabout.
|
||||||
|
# Open it with `isokit studio examples/streets`.
|
||||||
|
title = "isokit streets"
|
||||||
|
note = "Streets drawn along their centre lines: curves carry them round corners, side streets open the sidewalks they join, and the roundabout's kerbs flare into its arms. Tap a street for its numbers. Drag to pan, Ctrl + wheel or pinch to zoom."
|
||||||
|
default_level = "site"
|
||||||
|
default_rotation = 1
|
||||||
|
prelude = []
|
||||||
|
theme = []
|
||||||
|
|
||||||
|
[[level]]
|
||||||
|
id = "site"
|
||||||
|
label = "Site"
|
||||||
|
title = "A town quarter"
|
||||||
|
|
||||||
|
[[param]]
|
||||||
|
name = "labels"
|
||||||
|
type = "bool"
|
||||||
|
default = true
|
||||||
|
label = "Labels"
|
||||||
|
|
||||||
|
[[param]]
|
||||||
|
name = "running"
|
||||||
|
type = "bool"
|
||||||
|
default = true
|
||||||
|
label = "Animation"
|
||||||
|
|
||||||
|
[build]
|
||||||
|
out = "dist/index.html"
|
||||||
|
budget = 6000
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"objects": [
|
||||||
|
{"id": "site", "type": "site", "name": "Quarter", "x": 0, "y": 4, "locked": true, "props": {"w": 112, "d": 92, "mat": "g-grass"}},
|
||||||
|
{"id": "rb", "type": "roundabout", "name": "Roundabout", "x": 0, "y": 0, "props": {"r": 12, "lane": 7, "arm_len": 8, "road_d": 6.5, "walk_w": 2.5}},
|
||||||
|
{"id": "east-ave", "type": "road", "name": "East avenue", "x": 36, "y": 0, "props": {"walk": "both", "walk_w": 2.5, "d": 6.5, "w": 32}},
|
||||||
|
{"id": "west-st", "type": "road", "name": "West street", "x": -36, "y": 0, "props": {"walk": "both", "walk_w": 2.5, "d": 6.5, "w": 32}},
|
||||||
|
{"id": "south-st", "type": "road", "name": "South street", "x": 0, "y": -31, "rot": 90, "props": {"walk": "both", "walk_w": 2.5, "d": 6.5, "w": 22}},
|
||||||
|
{"id": "north-st", "type": "road", "name": "North street", "x": 0, "y": 27, "rot": 90, "props": {"walk": "both", "walk_w": 2.5, "d": 6.5, "w": 14}},
|
||||||
|
{"id": "bend", "type": "road_curve", "name": "Corner", "x": 0, "y": 34, "rot": 90, "props": {"walk": "both", "walk_w": 2.5, "d": 6.5, "r": 12, "turn": "right"}},
|
||||||
|
{"id": "park-rd", "type": "road", "name": "Park road", "x": 34, "y": 46, "props": {"walk": "both", "walk_w": 2.5, "d": 6.5, "w": 44}},
|
||||||
|
{"id": "cross-st", "type": "road", "name": "Cross street", "x": 34, "y": 23, "rot": 90, "props": {"w": 46, "d": 5.5, "walk": "both", "walk_w": 2}},
|
||||||
|
{"id": "offices", "type": "building", "name": "Offices", "x": 19, "y": 27, "props": {"w": 14, "d": 13, "floors": 3, "pv": true}},
|
||||||
|
{"id": "flats", "type": "building", "name": "Flats", "x": 46, "y": 23, "props": {"w": 10, "d": 28, "floors": 4, "roof": "gable", "mat": "brick"}},
|
||||||
|
{"id": "school", "type": "building", "name": "School", "x": -33, "y": -21, "props": {"w": 20, "d": 14, "floors": 2, "ac": true}},
|
||||||
|
{"id": "shop", "type": "building", "name": "Corner shop", "x": -14, "y": 30, "props": {"w": 10, "d": 9, "floors": 1}},
|
||||||
|
{"id": "parking", "type": "parking", "name": "Car park", "x": -36, "y": 22, "props": {"n": 8}},
|
||||||
|
{"id": "pond", "type": "pond", "name": "Park pond", "x": 32, "y": -24},
|
||||||
|
{"id": "tree-1", "type": "tree", "x": 16, "y": -16, "props": {"r": 2.2}},
|
||||||
|
{"id": "tree-2", "type": "tree", "x": 20, "y": -33, "props": {"r": 2.0}},
|
||||||
|
{"id": "tree-3", "type": "tree", "x": 46, "y": -14},
|
||||||
|
{"id": "tree-4", "type": "tree", "x": 48, "y": -34, "props": {"r": 2.4}},
|
||||||
|
{"id": "trees-west", "type": "tree_row", "name": "Avenue trees", "x": -36, "y": 9, "props": {"w": 28}},
|
||||||
|
{"id": "car-1", "type": "car", "x": 30, "y": 1.6, "props": {"moving": true, "travel": 22, "dur": 6, "colour": "car1"}},
|
||||||
|
{"id": "car-2", "type": "car", "x": 44, "y": -1.6, "rot": 180, "props": {"moving": true, "travel": 22, "dur": 7}},
|
||||||
|
{"id": "car-3", "type": "car", "x": -1.6, "y": -30, "rot": 90, "props": {"colour": "car3"}},
|
||||||
|
{"id": "bus", "type": "truck", "name": "Delivery", "x": -40, "y": 1.6, "props": {"len": 7}},
|
||||||
|
{"id": "moto", "type": "moto", "x": 1.6, "y": 26, "rot": 270},
|
||||||
|
{"id": "walker-1", "type": "person", "x": 24, "y": 4.6, "props": {"cls": "guest1"}},
|
||||||
|
{"id": "walker-2", "type": "person", "x": -26, "y": -4.7},
|
||||||
|
{"id": "walker-3", "type": "person", "x": 31.6, "y": 30, "props": {"cls": "guest3"}},
|
||||||
|
{"id": "crowd", "type": "crowd", "name": "Pupils", "x": -24, "y": -11, "props": {"n": 6, "r": 1.6}},
|
||||||
|
{"id": "lamp-1", "type": "lamp", "x": 26, "y": -5.2},
|
||||||
|
{"id": "lamp-2", "type": "lamp", "x": 44, "y": -5.2},
|
||||||
|
{"id": "lamp-3", "type": "lamp", "x": -28, "y": 5.2},
|
||||||
|
{"id": "lamp-4", "type": "lamp", "x": -44, "y": 5.2},
|
||||||
|
{"id": "lamp-5", "type": "lamp", "x": 5.2, "y": -28}
|
||||||
|
]
|
||||||
|
}
|
||||||
+40
-6
@@ -174,7 +174,8 @@
|
|||||||
polys.push(cornerMark(c.pts[0], c.z));
|
polys.push(cornerMark(c.pts[0], c.z));
|
||||||
var closes = c.pts.length > 2 && Math.hypot(q[0] - c.pts[0][0], q[1] - c.pts[0][1]) < 1e-6;
|
var closes = c.pts.length > 2 && Math.hypot(q[0] - c.pts[0][0], q[1] - c.pts[0][1]) < 1e-6;
|
||||||
var n = c.pts.length - 1 + (Math.hypot(q[0] - last[0], q[1] - last[1]) > 0.05 ? 1 : 0);
|
var n = c.pts.length - 1 + (Math.hypot(q[0] - last[0], q[1] - last[1]) > 0.05 ? 1 : 0);
|
||||||
ghost(polys, c.m.label + ": " + n + " piece" + (n === 1 ? "" : "s") + ", " + (runLength(c.pts) + Math.hypot(q[0] - last[0], q[1] - last[1])).toFixed(1) + " m · " +
|
ghost(polys, c.m.label + ": " + n + " piece" + (n === 1 ? "" : "s") + ", " + (runLength(c.pts) + Math.hypot(q[0] - last[0], q[1] - last[1])).toFixed(1) + " m" +
|
||||||
|
(c.m.corner ? ", corners rounded" : "") + " · " +
|
||||||
(closes ? "click to close the run" : "click the next corner · double-click or Enter to finish · click the first corner to close") + " · Backspace undoes a corner · Esc cancels");
|
(closes ? "click to close the run" : "click the next corner · double-click or Enter to finish · click the first corner to close") + " · Backspace undoes a corner · Esc cancels");
|
||||||
}
|
}
|
||||||
// the turn of a line piece from p to q: along x or y; directed lines (conveyors) point from p to q
|
// the turn of a line piece from p to q: along x or y; directed lines (conveyors) point from p to q
|
||||||
@@ -183,11 +184,44 @@
|
|||||||
if (!m.directed) return ax ? 0 : 90;
|
if (!m.directed) return ax ? 0 : 90;
|
||||||
return ax ? (q[0] >= p[0] ? 0 : 180) : (q[1] >= p[1] ? 90 : 270);
|
return ax ? (q[0] >= p[0] ? 0 : 180) : (q[1] >= p[1] ? 90 : 270);
|
||||||
}
|
}
|
||||||
// the pieces of a run as add ops (pieces longer than the prefab allows are split)
|
// the corners of a run rounded by the prefab's corner piece (roads: curves of up to its default
|
||||||
|
// radius, less where the pieces next to a corner are short): [segments [p, q]], [curve add ops]
|
||||||
|
function roundRun(m, pts, z) {
|
||||||
|
var segs = [], curves = [], cm = m.corner && prefab(m.corner), i;
|
||||||
|
for (i = 1; i < pts.length; i++) if (Math.hypot(pts[i][0] - pts[i - 1][0], pts[i][1] - pts[i - 1][1]) >= 0.05) segs.push([pts[i - 1].slice(), pts[i].slice()]);
|
||||||
|
var n = segs.length;
|
||||||
|
if (!cm || n < 2) return { segs: segs, curves: curves };
|
||||||
|
var closed = n > 2 && Math.hypot(segs[n - 1][1][0] - segs[0][0][0], segs[n - 1][1][1] - segs[0][0][1]) < 1e-6;
|
||||||
|
var len = segs.map(function (s) { return Math.hypot(s[1][0] - s[0][0], s[1][1] - s[0][1]); });
|
||||||
|
var dir = segs.map(function (s, k) { return [(s[1][0] - s[0][0]) / len[k], (s[1][1] - s[0][1]) / len[k]]; });
|
||||||
|
// turn at the start of segment k (from segment k - 1), 0 where the run goes straight on
|
||||||
|
var turn = segs.map(function (s, k) {
|
||||||
|
if (k === 0 && !closed) return 0;
|
||||||
|
var a = dir[(k + n - 1) % n], b = dir[k], cr = a[0] * b[1] - a[1] * b[0];
|
||||||
|
return Math.abs(cr) > 0.5 && a[0] * b[0] + a[1] * b[1] < 0.5 ? cr : 0;
|
||||||
|
});
|
||||||
|
var R0 = +defOf(cm, "r") || 10, rmin = Math.max(+((cm.props || []).filter(function (d) { return d.key === "r"; })[0] || {}).min || 0, (+defOf(m, "d") || 0) / 2 + 0.5);
|
||||||
|
for (var k = 0; k < n; k++) {
|
||||||
|
if (!turn[k]) continue;
|
||||||
|
var a = (k + n - 1) % n, nextTurn = turn[(k + 1) % n] && (k + 1 < n || closed);
|
||||||
|
var r = Math.min(R0, len[a] * (turn[a] ? 0.5 : 1), len[k] * (nextTurn ? 0.5 : 1));
|
||||||
|
if (r < rmin) continue;
|
||||||
|
var P = segs[k][0], da = dir[a], db = dir[k];
|
||||||
|
var s0 = [P[0] - da[0] * r, P[1] - da[1] * r];
|
||||||
|
var add = { op: "add", type: m.corner, x: r3(s0[0]), y: r3(s0[1]), rot: (Math.round(Math.atan2(da[1], da[0]) * 2 / Math.PI) * 90 + 360) % 360,
|
||||||
|
props: { r: r3(r), angle: 90, turn: turn[k] > 0 ? "left" : "right" } };
|
||||||
|
if (z) add.z = z;
|
||||||
|
curves.push(add);
|
||||||
|
segs[a][1] = s0;
|
||||||
|
segs[k][0] = [P[0] + db[0] * r, P[1] + db[1] * r];
|
||||||
|
}
|
||||||
|
return { segs: segs, curves: curves };
|
||||||
|
}
|
||||||
|
// the pieces of a run as add ops (pieces longer than the prefab allows are split; corners rounded)
|
||||||
function runOps(m, pts, z) {
|
function runOps(m, pts, z) {
|
||||||
var ops = [], max = +((m.props || []).filter(function (d) { return d.key === "w"; })[0] || {}).max || 1e9;
|
var ops = [], max = +((m.props || []).filter(function (d) { return d.key === "w"; })[0] || {}).max || 1e9, rr = roundRun(m, pts, z);
|
||||||
for (var i = 1; i < pts.length; i++) {
|
for (var i = 0; i < rr.segs.length; i++) {
|
||||||
var p = pts[i - 1], q = pts[i], L = Math.hypot(q[0] - p[0], q[1] - p[1]), n = Math.ceil(L / max - 1e-9);
|
var p = rr.segs[i][0], q = rr.segs[i][1], L = Math.hypot(q[0] - p[0], q[1] - p[1]), n = Math.ceil(L / max - 1e-9);
|
||||||
if (L < 0.05) continue;
|
if (L < 0.05) continue;
|
||||||
for (var j = 0; j < n; j++) {
|
for (var j = 0; j < n; j++) {
|
||||||
var t = (j + 0.5) / n, add = { op: "add", type: m.type, x: r3(p[0] + (q[0] - p[0]) * t), y: r3(p[1] + (q[1] - p[1]) * t), rot: lineRot(m, p, q), props: { w: clampProp(m, "w", L / n) } };
|
var t = (j + 0.5) / n, add = { op: "add", type: m.type, x: r3(p[0] + (q[0] - p[0]) * t), y: r3(p[1] + (q[1] - p[1]) * t), rot: lineRot(m, p, q), props: { w: clampProp(m, "w", L / n) } };
|
||||||
@@ -195,7 +229,7 @@
|
|||||||
ops.push(add);
|
ops.push(add);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return ops;
|
return ops.concat(rr.curves);
|
||||||
}
|
}
|
||||||
function finishChain(keep) {
|
function finishChain(keep) {
|
||||||
var c = chain; chain = null; ghost(null);
|
var c = chain; chain = null; ghost(null);
|
||||||
|
|||||||
@@ -149,6 +149,7 @@ def main():
|
|||||||
(web / "demo.json").write_text(json.dumps(demo_files(), ensure_ascii=False), encoding="utf-8")
|
(web / "demo.json").write_text(json.dumps(demo_files(), ensure_ascii=False), encoding="utf-8")
|
||||||
(web / "house.json").write_text(json.dumps(demo_files("house"), ensure_ascii=False), encoding="utf-8")
|
(web / "house.json").write_text(json.dumps(demo_files("house"), ensure_ascii=False), encoding="utf-8")
|
||||||
(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")
|
||||||
|
|
||||||
h = hashlib.sha256()
|
h = hashlib.sha256()
|
||||||
for p in sorted((out / "assets").rglob("*")):
|
for p in sorted((out / "assets").rglob("*")):
|
||||||
|
|||||||
@@ -471,6 +471,38 @@ with sync_playwright() as p:
|
|||||||
pg.evaluate("__studio.select(null)")
|
pg.evaluate("__studio.select(null)")
|
||||||
pg.wait_for_timeout(300)
|
pg.wait_for_timeout(300)
|
||||||
|
|
||||||
|
# a road run drawn corner by corner off the main road: a curve rounds its corner, the road it
|
||||||
|
# starts on gets a junction (its south kerb opens), and one undo step takes the run back
|
||||||
|
pg.evaluate("__studio.view.focus([6, -27, 0], 2.2)")
|
||||||
|
pg.wait_for_timeout(400)
|
||||||
|
n4 = len(doc()["objects"])
|
||||||
|
kerbs = lambda: pg.locator('.iso-svg g[data-obj="road"] .iso-kerb').count()
|
||||||
|
k0 = kerbs()
|
||||||
|
pg.locator('.st-tile[data-type="road"]').click()
|
||||||
|
for x, y in [(-2, -19.3), (-2.1, -32), (20, -32.1)]:
|
||||||
|
pg.mouse.click(*at(x, y))
|
||||||
|
pg.wait_for_timeout(150)
|
||||||
|
pg.mouse.move(*at(20, -36))
|
||||||
|
pg.wait_for_timeout(150)
|
||||||
|
check("corners rounded" in pg.locator("#hint").inner_text(), "a road run says its corners get rounded: " + pg.locator("#hint").inner_text()[:50])
|
||||||
|
pg.keyboard.press("Enter")
|
||||||
|
pg.wait_for_timeout(1200)
|
||||||
|
run = doc()["objects"][n4:]
|
||||||
|
got = sorted((o["type"], o["x"], o["y"], o.get("rot", 0), o["props"].get("w", o["props"].get("r"))) for o in run)
|
||||||
|
want = sorted([("road", -2, -20.75, 90, 2.5), ("road", 14, -32, 0, 12), ("road_curve", -2, -22, 270, 10)])
|
||||||
|
check(got == want, f"the run's corner gets a curve: {got}")
|
||||||
|
rv = get("/api/render?level=site&rot=0")
|
||||||
|
stub = next((o["id"] for o in run if o["type"] == "road" and o["x"] == -2), None)
|
||||||
|
sb = rv["boxes"].get(stub, [0] * 6)
|
||||||
|
check(abs(sb[4] + 19.5) < 1e-6 and kerbs() == k0 + 1,
|
||||||
|
f"the run starts on the main road's centre line and opens its kerb: {sb[4]:.2f}, {k0} -> {kerbs()} kerb lines")
|
||||||
|
pg.evaluate("__studio.select(null)")
|
||||||
|
pg.wait_for_timeout(300)
|
||||||
|
pg.screenshot(path=str(OUT / "studio-road-run.jpg"), type="jpeg", quality=80)
|
||||||
|
pg.locator("#undo").click()
|
||||||
|
pg.wait_for_timeout(900)
|
||||||
|
check(len(doc()["objects"]) == n4 and kerbs() == k0, "one undo takes the road run back")
|
||||||
|
|
||||||
# an agent edits over MCP: the open Studio follows
|
# an agent edits over MCP: the open Studio follows
|
||||||
init = mcp({"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {"protocolVersion": "2025-06-18", "capabilities": {}, "clientInfo": {"name": "test", "version": "0"}}})
|
init = mcp({"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {"protocolVersion": "2025-06-18", "capabilities": {}, "clientInfo": {"name": "test", "version": "0"}}})
|
||||||
check(init["result"]["serverInfo"]["name"] == "isokit", "MCP initialize")
|
check(init["result"]["serverInfo"]["name"] == "isokit", "MCP initialize")
|
||||||
|
|||||||
@@ -326,6 +326,22 @@ try:
|
|||||||
check("(from robot-b)" in loads and flips == 32, f"robot arms pass the goods on ({flips} poses): " + loads)
|
check("(from robot-b)" in loads and flips == 32, f"robot arms pass the goods on ({flips} poses): " + loads)
|
||||||
shot(pg, "web-factory.jpg")
|
shot(pg, "web-factory.jpg")
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- the streets demo: sidewalks, curves, a roundabout
|
||||||
|
pg.locator("#wb-projects").click()
|
||||||
|
pg.locator("#wb-nstreets").click()
|
||||||
|
pg.wait_for_load_state()
|
||||||
|
ready(pg)
|
||||||
|
pg.wait_for_timeout(500)
|
||||||
|
ns = len(doc(pg)["objects"])
|
||||||
|
walks = pg.evaluate("() => document.querySelectorAll('.iso-svg .iso-g-walk').length")
|
||||||
|
gw = pg.evaluate("() => document.querySelectorAll('.iso-svg .iso-giveway').length")
|
||||||
|
check(title(pg) == "isokit streets" and ns > 25 and walks > 15 and gw == 1 and pg.evaluate("() => __studio.S.warnings.length") == 0,
|
||||||
|
f"Streets demo opens a town quarter ({ns} objects, {walks} sidewalk pieces, give-way lines, no warnings)")
|
||||||
|
meets = pg.evaluate("() => Object.fromEntries((__studio.view.info('cross-st') || {rows: []}).rows)['Meets'] || ''")
|
||||||
|
ring = pg.evaluate("() => Object.fromEntries((__studio.view.info('rb') || {rows: []}).rows)['Arms'] || ''")
|
||||||
|
check(meets == "east-ave, park-rd" and ring.startswith("4:"), f"streets know what they meet: cross-st meets {meets}; roundabout arms {ring}")
|
||||||
|
shot(pg, "web-streets.jpg")
|
||||||
|
|
||||||
# ---------------------------------------------------------------- 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
@@ -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></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></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>" +
|
||||||
@@ -419,6 +419,7 @@ function wireProjects() {
|
|||||||
$("wb-ndemo").addEventListener("click", async function () { openProject((await createProject(await demoFiles())).id); });
|
$("wb-ndemo").addEventListener("click", async function () { openProject((await createProject(await demoFiles())).id); });
|
||||||
$("wb-nhouse").addEventListener("click", async function () { openProject((await createProject(await demoFiles("house.json"))).id); });
|
$("wb-nhouse").addEventListener("click", async function () { openProject((await createProject(await demoFiles("house.json"))).id); });
|
||||||
$("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-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 = ""; });
|
||||||
|
|||||||
Reference in new issue
Block a user