Conveyors and producers: goods flow from presses down the belts
Deploy web Studio / deploy (push) Successful in 1m4s
Deploy web Studio / deploy (push) Successful in 1m4s
- Producer (category Industry): a press making cartons, crates, drums, sacks or parts at a set rate per minute; its ram goes down once per item, a green / red light shows running / stopped. - Conveyor: drawn the way it runs (new prefab meta `directed`: the Studio turns the piece from where the drag starts), belt width, height, rise (inclines), speed, goods and spacing; legs, rails and a drive motor. Corners join without overlap (the receiving belt runs through, the feeding one stops at its side). - A conveyor starting on a producer's output side carries its goods at its rate; one starting at another's end carries on with what that one carries. The gap is speed / rate. A stopped press or belt stops the goods after it. Tooltips show speed, rate and source. - The goods ride a group moving one gap per loop (no reordering), first fading in, last out. - Script API: world_of(o, u, v), info_row(key, value); objects_on falls back to half of p.d. - Theme: industry colours, LED colours (the marker's LED had none). - examples/factory + Factory demo in the web Studio; MCP instructions on production lines. Checks: 68 Rust tests (conveyor joints, feed and spacing, every view of the factory demo), Studio 108/108 (draw direction, feed from a producer, rate change), web 89/89.
This commit is contained in:
1 parent
a202283a60
commit
18dd324543
20 files changed
+617
-18
No files matched your search
@@ -652,6 +652,16 @@ pub(crate) fn register_scene(e: &mut Engine, st: &Shared) {
|
||||
tag(&s, x, y, z, t, n(&p)?, Some(id), k)
|
||||
},
|
||||
);
|
||||
// a row more in the tooltip of the object being drawn
|
||||
let s = st.clone();
|
||||
e.register_fn("info_row", move |k: D, v: D| {
|
||||
let mut b = s.borrow_mut();
|
||||
if let Some(id) = b.obj.clone() {
|
||||
if let Some(i) = b.scene.info.get_mut(&id) {
|
||||
i.rows.push((k.to_string(), v.to_string()));
|
||||
}
|
||||
}
|
||||
});
|
||||
let s = st.clone();
|
||||
e.register_fn("info", move |id: &str, name: &str| {
|
||||
s.borrow_mut().scene.info(id, name, "")
|
||||
@@ -933,7 +943,8 @@ fn register_objects(e: &mut Engine, st: &Shared) {
|
||||
let m = d.read_lock::<Map>()?;
|
||||
let l = xf_of(&m).inv(x, y);
|
||||
let half = prop(&m, "w").unwrap_or(1.0) / 2.0;
|
||||
let lim = prop(&m, "t").unwrap_or(0.0) / 2.0 + tol;
|
||||
// half the thickness (walls), else half the width (conveyors)
|
||||
let lim = prop(&m, "t").or_else(|| prop(&m, "d")).unwrap_or(0.0) / 2.0 + tol;
|
||||
(l[0].abs() <= half + 1e-6 && l[1].abs() <= lim + 1e-6)
|
||||
.then(|| with_uv(d, l[0], l[1]))
|
||||
})
|
||||
@@ -1008,4 +1019,9 @@ fn register_objects(e: &mut Engine, st: &Shared) {
|
||||
let p = xf_of(&o).inv(n(&x)?, n(&y)?);
|
||||
Ok(vec![Dynamic::from_float(p[0]), Dynamic::from_float(p[1])])
|
||||
});
|
||||
// the world plan point of (u, v) in object o's frame
|
||||
e.register_fn("world_of", |o: Map, u: D, v: D| -> RR<Array> {
|
||||
let p = xf_of(&o).p(n(&u)?, n(&v)?);
|
||||
Ok(vec![Dynamic::from_float(p[0]), Dynamic::from_float(p[1])])
|
||||
});
|
||||
}
|
||||
@@ -119,6 +119,10 @@ pub struct PrefabMeta {
|
||||
/// it at z = 1 x storey height (parameter `storey_h`, default 3 m).
|
||||
#[serde(default, skip_serializing_if = "is_false")]
|
||||
pub storey: bool,
|
||||
/// A line whose way matters (conveyors run along local +x): the Studio
|
||||
/// turns it to point from where the drag starts to where it ends.
|
||||
#[serde(default, skip_serializing_if = "is_false")]
|
||||
pub directed: bool,
|
||||
#[serde(default)]
|
||||
pub props: Vec<PropDef>,
|
||||
/// Module the prefab comes from ("isokit/site", "lib/kiosk.rhai").
|
||||
|
||||
@@ -15,4 +15,5 @@ pub const MODULES: &[(&str, &str)] = &[
|
||||
("isokit/equipment", include_str!("../stdlib/equipment.rhai")),
|
||||
("isokit/house", include_str!("../stdlib/house.rhai")),
|
||||
("isokit/furniture", include_str!("../stdlib/furniture.rhai")),
|
||||
("isokit/industry", include_str!("../stdlib/industry.rhai")),
|
||||
];
|
||||
@@ -542,6 +542,107 @@ fn ponds_have_fish_swimming_in_every_shape() {
|
||||
.contains("iso-swim"));
|
||||
}
|
||||
|
||||
fn factory_doc() -> Doc {
|
||||
let mut doc = Doc::default();
|
||||
for (id, kind, x, y, rot, props) in [
|
||||
("press1", "producer", -6.0, -4.0, 0.0, json!({"rate": 20})),
|
||||
("belt1", "conveyor", -1.4, -4.0, 0.0, json!({"w": 6})),
|
||||
("belt2", "conveyor", 1.6, -1.0, 90.0, json!({"w": 6})),
|
||||
(
|
||||
"belt3",
|
||||
"conveyor",
|
||||
1.6,
|
||||
5.0,
|
||||
90.0,
|
||||
json!({"w": 6, "speed": 0.8, "rise": 1.6}),
|
||||
),
|
||||
(
|
||||
"belt5",
|
||||
"conveyor",
|
||||
8.0,
|
||||
6.0,
|
||||
0.0,
|
||||
json!({"w": 8, "item": "crate", "gap": 2}),
|
||||
),
|
||||
(
|
||||
"press3",
|
||||
"producer",
|
||||
-6.0,
|
||||
8.0,
|
||||
0.0,
|
||||
json!({"rate": 6, "product": "part", "running": false}),
|
||||
),
|
||||
("belt6", "conveyor", -2.0, 8.0, 0.0, json!({"w": 5})),
|
||||
// a closed ring: following the feed back ends after a few belts
|
||||
("r1", "conveyor", 30.0, -3.0, 0.0, json!({"w": 6})),
|
||||
("r2", "conveyor", 33.0, 0.0, 90.0, json!({"w": 6})),
|
||||
("r3", "conveyor", 30.0, 3.0, 180.0, json!({"w": 6})),
|
||||
("r4", "conveyor", 27.0, 0.0, 270.0, json!({"w": 6})),
|
||||
] {
|
||||
let mut o = obj(id, kind, x, y, props);
|
||||
o.rot = rot;
|
||||
doc.objects.push(o);
|
||||
}
|
||||
doc
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn conveyors_carry_what_the_producer_makes() {
|
||||
let r = Runtime::new(&Bundle::default()).unwrap();
|
||||
assert!(r.prefab("conveyor").unwrap().directed);
|
||||
let doc = factory_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"));
|
||||
// goods move on the running belts (not behind the stopped press), the running press's ram goes
|
||||
assert_eq!(
|
||||
out.html.matches("iso-a iso-ride\"").count(),
|
||||
8,
|
||||
"view {view}"
|
||||
);
|
||||
assert_eq!(out.html.matches("iso-a iso-ram").count(), 1, "view {view}");
|
||||
}
|
||||
let b = r.build("site", 1, &cfg(), &doc).unwrap();
|
||||
let near = |a: f64, e: f64| (a - e).abs() < 1e-6;
|
||||
// the first belt starts flush at the press and stops at the side of the belt it feeds,
|
||||
// which runs through the corner; in line the belts just meet
|
||||
let (b1, b2, b3) = (b.boxes["belt1"], b.boxes["belt2"], b.boxes["belt3"]);
|
||||
assert!(near(b1[0], -4.4) && near(b1[3], 1.2), "belt1 {b1:?}");
|
||||
assert!(near(b2[1], -4.4) && near(b2[4], 2.0), "belt2 {b2:?}");
|
||||
assert!(near(b3[1], 2.0), "belt3 {b3:?}");
|
||||
// goods and their spacing come down the line: 20 a minute at 0.8 m/s is 2.4 m apart
|
||||
let out = render(&b.scene, 1, &RenderOpts::default());
|
||||
let row = |id: &str, k: &str| {
|
||||
out.info[id]
|
||||
.rows
|
||||
.iter()
|
||||
.find(|r| r.0 == k)
|
||||
.map(|r| r.1.clone())
|
||||
.unwrap_or_default()
|
||||
};
|
||||
assert_eq!(
|
||||
row("belt1", "Carries"),
|
||||
"20 cartons a minute, 1.5 m apart (from press1)"
|
||||
);
|
||||
assert_eq!(
|
||||
row("belt3", "Carries"),
|
||||
"20 cartons a minute, 2.4 m apart (from belt2)"
|
||||
);
|
||||
assert_eq!(row("belt5", "Carries"), "15 crates a minute, 2 m apart");
|
||||
assert_eq!(
|
||||
row("belt6", "Carries"),
|
||||
"6 parts a minute, 5 m apart (from press3)"
|
||||
);
|
||||
assert_eq!(
|
||||
row("press3", "Makes"),
|
||||
"6 parts a minute, one every 10 s (stopped)"
|
||||
);
|
||||
assert_eq!(row("belt3", "Speed"), "0.8 m/s");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn site_walls_join_at_corners() {
|
||||
let r = Runtime::new(&Bundle::default()).unwrap();
|
||||
|
||||
Reference in new issue
Block a user