isokit: project layer, Studio GUI, MCP server, viewer, CLI, demo project and docs
This commit is contained in:
1 parent
95c80ec6b1
commit
564aeb75f6
47 files changed
+5978
-366
No files matched your search
@@ -16,7 +16,7 @@ pub fn r1(out: &mut String, v: f64) {
|
||||
}
|
||||
let a = t.unsigned_abs();
|
||||
let _ = write!(out, "{}", a / 10);
|
||||
if a % 10 != 0 {
|
||||
if !a.is_multiple_of(10) {
|
||||
let _ = write!(out, ".{}", a % 10);
|
||||
}
|
||||
}
|
||||
@@ -65,7 +65,7 @@ pub fn thousands(n: f64) -> String {
|
||||
let s = v.unsigned_abs().to_string();
|
||||
let mut o = String::new();
|
||||
for (i, ch) in s.chars().enumerate() {
|
||||
if i > 0 && (s.len() - i) % 3 == 0 {
|
||||
if i > 0 && (s.len() - i).is_multiple_of(3) {
|
||||
o.push(',');
|
||||
}
|
||||
o.push(ch);
|
||||
|
||||
@@ -46,7 +46,10 @@ pub struct Style {
|
||||
|
||||
impl Style {
|
||||
pub fn mat(mat: impl Into<String>) -> Self {
|
||||
Style { mat: mat.into(), ..Default::default() }
|
||||
Style {
|
||||
mat: mat.into(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
pub fn id(mut self, id: impl Into<String>) -> Self {
|
||||
self.id = Some(id.into());
|
||||
@@ -75,44 +78,116 @@ impl Style {
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub enum Prim {
|
||||
/// Vertical prism over a convex counter-clockwise footprint.
|
||||
Prism { pts: Vec<P2>, z0: Zs, z1: Zs, st: Style },
|
||||
Prism {
|
||||
pts: Vec<P2>,
|
||||
z0: Zs,
|
||||
z1: Zs,
|
||||
st: Style,
|
||||
},
|
||||
/// Upright cylinder (two side faces and an elliptic top).
|
||||
Cyl { c: P2, r: f64, z0: f64, z1: f64, st: Style },
|
||||
Cyl {
|
||||
c: P2,
|
||||
r: f64,
|
||||
z0: f64,
|
||||
z1: f64,
|
||||
st: Style,
|
||||
},
|
||||
/// Frustum (cone) with n sides: radius r0 at z0, r1 at z1.
|
||||
Frust { c: P2, r0: f64, r1: f64, z0: f64, z1: f64, n: u32, st: Style },
|
||||
Frust {
|
||||
c: P2,
|
||||
r0: f64,
|
||||
r1: f64,
|
||||
z0: f64,
|
||||
z1: f64,
|
||||
n: u32,
|
||||
st: Style,
|
||||
},
|
||||
/// Openings (windows, doors) on the vertical face through a -> b; the
|
||||
/// outward normal is on the right of a -> b. Each entry is
|
||||
/// [u0, u1, z0, z1]: distance along the face and heights.
|
||||
Open { a: P2, b: P2, list: Vec<[f64; 4]>, cls: String, id: Option<String> },
|
||||
Open {
|
||||
a: P2,
|
||||
b: P2,
|
||||
list: Vec<[f64; 4]>,
|
||||
cls: String,
|
||||
id: Option<String>,
|
||||
},
|
||||
/// Flat polygon(s) at height z; later polygons cut holes (even-odd).
|
||||
Flat { polys: Vec<Vec<P2>>, z: f64, cls: String, id: Option<String> },
|
||||
Flat {
|
||||
polys: Vec<Vec<P2>>,
|
||||
z: f64,
|
||||
cls: String,
|
||||
id: Option<String>,
|
||||
},
|
||||
/// Line segments [x0, y0, x1, y1] at height z.
|
||||
Lines { segs: Vec<[f64; 4]>, z: f64, cls: String, id: Option<String> },
|
||||
Lines {
|
||||
segs: Vec<[f64; 4]>,
|
||||
z: f64,
|
||||
cls: String,
|
||||
id: Option<String>,
|
||||
},
|
||||
/// Flat ellipse (a circle on the ground plane) at height z.
|
||||
Disc { c: P2, z: f64, r: f64, cls: String, id: Option<String> },
|
||||
Disc {
|
||||
c: P2,
|
||||
z: f64,
|
||||
r: f64,
|
||||
cls: String,
|
||||
id: Option<String>,
|
||||
},
|
||||
/// Planar polygon in 3D (roof slopes, gables, ramps), shaded by its
|
||||
/// normal: mostly upward = top, else left/centre/right by direction.
|
||||
/// One-sided faces (counter-clockwise seen from outside) are culled
|
||||
/// when turned away; two-sided faces always draw.
|
||||
Face { pts: Vec<P3>, mat: String, id: Option<String>, two_sided: bool },
|
||||
Face {
|
||||
pts: Vec<P3>,
|
||||
mat: String,
|
||||
id: Option<String>,
|
||||
two_sided: bool,
|
||||
},
|
||||
/// 3D polygons merged into one path with a fixed class (PV modules,
|
||||
/// signs, markings on slopes): no shading, no culling.
|
||||
Poly { polys: Vec<Vec<P3>>, cls: String, id: Option<String> },
|
||||
Poly {
|
||||
polys: Vec<Vec<P3>>,
|
||||
cls: String,
|
||||
id: Option<String>,
|
||||
},
|
||||
/// Screen-space circle of radius r (screen units) at a world point,
|
||||
/// shifted by off (screen units): heads, lamps, LEDs, tree crowns.
|
||||
Circle { at: P3, r: f64, off: P2, cls: String, id: Option<String> },
|
||||
Circle {
|
||||
at: P3,
|
||||
r: f64,
|
||||
off: P2,
|
||||
cls: String,
|
||||
id: Option<String>,
|
||||
},
|
||||
/// Screen-space vertical strip of half width w between two heights:
|
||||
/// trunks, thin poles.
|
||||
Stem { at: P2, z0: f64, z1: f64, w: f64, cls: String, id: Option<String> },
|
||||
Stem {
|
||||
at: P2,
|
||||
z0: f64,
|
||||
z1: f64,
|
||||
w: f64,
|
||||
cls: String,
|
||||
id: Option<String>,
|
||||
},
|
||||
/// Rising steam or smoke puffs (CSS animation).
|
||||
Puffs { at: P3, kind: String, n: u32, dur: f64 },
|
||||
Puffs {
|
||||
at: P3,
|
||||
kind: String,
|
||||
n: u32,
|
||||
dur: f64,
|
||||
},
|
||||
/// Spinning fan lying in the plane at height at.z (CSS animation).
|
||||
Fan { at: P3, r: f64 },
|
||||
/// Raw SVG translated to a world point (or untranslated without one).
|
||||
/// `{uid}` in the text is replaced by the view's unique id.
|
||||
Raw { at: Option<P3>, svg: String },
|
||||
/// `<g class style>` around nested primitives (animation, ghosting).
|
||||
Group { cls: String, style: String, items: Vec<Prim> },
|
||||
Group {
|
||||
cls: String,
|
||||
style: String,
|
||||
items: Vec<Prim>,
|
||||
},
|
||||
/// Items drawn back to front by the depth of their plan anchor.
|
||||
Seq { items: Vec<(P2, Vec<Prim>)> },
|
||||
}
|
||||
@@ -18,7 +18,9 @@ pub struct Proj {
|
||||
|
||||
impl Proj {
|
||||
pub fn new(rot: i64) -> Self {
|
||||
Proj { rot: rot.rem_euclid(4) as u8 }
|
||||
Proj {
|
||||
rot: rot.rem_euclid(4) as u8,
|
||||
}
|
||||
}
|
||||
|
||||
/// World plan point to view coordinates.
|
||||
@@ -147,7 +149,10 @@ mod tests {
|
||||
let p = Proj::new(rot);
|
||||
let s = p.s(12.5, -3.25, 2.0);
|
||||
let w = p.unproject(s[0], s[1], 2.0);
|
||||
assert!((w[0] - 12.5).abs() < 1e-9 && (w[1] + 3.25).abs() < 1e-9, "rot {rot}: {w:?}");
|
||||
assert!(
|
||||
(w[0] - 12.5).abs() < 1e-9 && (w[1] + 3.25).abs() < 1e-9,
|
||||
"rot {rot}: {w:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,11 @@ pub struct RenderOpts {
|
||||
|
||||
impl Default for RenderOpts {
|
||||
fn default() -> Self {
|
||||
RenderOpts { uid: 0, labels: true, dev: false }
|
||||
RenderOpts {
|
||||
uid: 0,
|
||||
labels: true,
|
||||
dev: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,7 +105,9 @@ impl Ctx<'_> {
|
||||
if !st.no_side {
|
||||
for i in 0..n {
|
||||
let (a, b) = (pts[i], pts[(i + 1) % n]);
|
||||
let Some(v) = p.vis(b[1] - a[1], a[0] - b[0]) else { continue };
|
||||
let Some(v) = p.vis(b[1] - a[1], a[0] - b[0]) else {
|
||||
continue;
|
||||
};
|
||||
let j = (i + 1) % n;
|
||||
if z1.at(i) - z0.at(i) < 1e-6 && z1.at(j) - z0.at(j) < 1e-6 {
|
||||
continue;
|
||||
@@ -109,14 +115,23 @@ impl Ctx<'_> {
|
||||
self.path_open(&format!("{} {}", st.side_cls(), shade(v)), id);
|
||||
p.d(
|
||||
&mut self.out,
|
||||
&[[a[0], a[1], z0.at(i)], [b[0], b[1], z0.at(j)], [b[0], b[1], z1.at(j)], [a[0], a[1], z1.at(i)]],
|
||||
&[
|
||||
[a[0], a[1], z0.at(i)],
|
||||
[b[0], b[1], z0.at(j)],
|
||||
[b[0], b[1], z1.at(j)],
|
||||
[a[0], a[1], z1.at(i)],
|
||||
],
|
||||
false,
|
||||
);
|
||||
self.out.push_str("\"/>");
|
||||
}
|
||||
}
|
||||
if !st.no_top {
|
||||
let tp: Vec<[f64; 3]> = pts.iter().enumerate().map(|(i, q)| [q[0], q[1], z1.at(i)]).collect();
|
||||
let tp: Vec<[f64; 3]> = pts
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, q)| [q[0], q[1], z1.at(i)])
|
||||
.collect();
|
||||
self.path_open(&format!("{} iso-t", st.top_cls()), id);
|
||||
p.d(&mut self.out, &tp, false);
|
||||
self.out.push_str("\"/>");
|
||||
@@ -150,7 +165,10 @@ impl Ctx<'_> {
|
||||
if !st.no_top {
|
||||
let _ = write!(self.out, "<ellipse class=\"{} iso-t\"", st.top_cls());
|
||||
did(&mut self.out, id);
|
||||
let _ = write!(self.out, " cx=\"{bx}\" cy=\"{ty}\" rx=\"{rxs}\" ry=\"{rys}\"/>");
|
||||
let _ = write!(
|
||||
self.out,
|
||||
" cx=\"{bx}\" cy=\"{ty}\" rx=\"{rxs}\" ry=\"{rys}\"/>"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,7 +186,9 @@ impl Ctx<'_> {
|
||||
if st.no_side {
|
||||
continue;
|
||||
}
|
||||
let Some(v) = p.vis(am.cos(), am.sin()) else { continue };
|
||||
let Some(v) = p.vis(am.cos(), am.sin()) else {
|
||||
continue;
|
||||
};
|
||||
self.path_open(&format!("{} {}", st.side_cls(), shade(v)), id);
|
||||
p.d(
|
||||
&mut self.out,
|
||||
@@ -194,7 +214,9 @@ impl Ctx<'_> {
|
||||
return;
|
||||
}
|
||||
let p = self.p;
|
||||
let Some(v) = p.vis(b[1] - a[1], a[0] - b[0]) else { return };
|
||||
let Some(v) = p.vis(b[1] - a[1], a[0] - b[0]) else {
|
||||
return;
|
||||
};
|
||||
let (dx, dy) = (b[0] - a[0], b[1] - a[1]);
|
||||
let l = (dx * dx + dy * dy).sqrt().max(1e-9);
|
||||
let (ux, uy) = (dx / l, dy / l);
|
||||
@@ -262,7 +284,14 @@ impl Ctx<'_> {
|
||||
let rx = r * C * SQ2;
|
||||
let _ = write!(self.out, "<ellipse class=\"{cls}\"");
|
||||
did(&mut self.out, id);
|
||||
let _ = write!(self.out, " cx=\"{}\" cy=\"{}\" rx=\"{}\" ry=\"{}\"/>", r1s(s[0]), r1s(s[1]), r1s(rx), r1s(rx / 2.0));
|
||||
let _ = write!(
|
||||
self.out,
|
||||
" cx=\"{}\" cy=\"{}\" rx=\"{}\" ry=\"{}\"/>",
|
||||
r1s(s[0]),
|
||||
r1s(s[1]),
|
||||
r1s(rx),
|
||||
r1s(rx / 2.0)
|
||||
);
|
||||
}
|
||||
|
||||
/// Planar polygon shaded by its Newell normal. The view direction in
|
||||
@@ -296,7 +325,11 @@ impl Ctx<'_> {
|
||||
nz = -nz;
|
||||
v = self.p.r(nx, ny);
|
||||
}
|
||||
let face = if nz >= 0.5 || v[0] + v[1] <= 1e-6 { "iso-t" } else { shade(v) };
|
||||
let face = if nz >= 0.5 || v[0] + v[1] <= 1e-6 {
|
||||
"iso-t"
|
||||
} else {
|
||||
shade(v)
|
||||
};
|
||||
self.path_open(&format!("{mat} {face}"), id);
|
||||
let p = self.p;
|
||||
p.d(&mut self.out, pts, false);
|
||||
@@ -307,12 +340,31 @@ impl Ctx<'_> {
|
||||
match pr {
|
||||
Prim::Prism { pts, z0, z1, st } => self.prism(pts, z0, z1, st),
|
||||
Prim::Cyl { c, r, z0, z1, st } => self.cyl(*c, *r, *z0, *z1, st),
|
||||
Prim::Frust { c, r0, r1, z0, z1, n, st } => self.frust(*c, *r0, *r1, *z0, *z1, *n, st),
|
||||
Prim::Open { a, b, list, cls, id } => self.open(*a, *b, list, cls, id.as_deref()),
|
||||
Prim::Frust {
|
||||
c,
|
||||
r0,
|
||||
r1,
|
||||
z0,
|
||||
z1,
|
||||
n,
|
||||
st,
|
||||
} => self.frust(*c, *r0, *r1, *z0, *z1, *n, st),
|
||||
Prim::Open {
|
||||
a,
|
||||
b,
|
||||
list,
|
||||
cls,
|
||||
id,
|
||||
} => self.open(*a, *b, list, cls, id.as_deref()),
|
||||
Prim::Flat { polys, z, cls, id } => self.flat(polys, *z, cls, id.as_deref()),
|
||||
Prim::Lines { segs, z, cls, id } => self.lines(segs, *z, cls, id.as_deref()),
|
||||
Prim::Disc { c, z, r, cls, id } => self.disc(*c, *z, *r, cls, id.as_deref()),
|
||||
Prim::Face { pts, mat, id, two_sided } => self.face(pts, mat, id.as_deref(), *two_sided),
|
||||
Prim::Face {
|
||||
pts,
|
||||
mat,
|
||||
id,
|
||||
two_sided,
|
||||
} => self.face(pts, mat, id.as_deref(), *two_sided),
|
||||
Prim::Poly { polys, cls, id } => {
|
||||
if polys.is_empty() {
|
||||
return;
|
||||
@@ -324,7 +376,13 @@ impl Ctx<'_> {
|
||||
}
|
||||
self.out.push_str("\"/>");
|
||||
}
|
||||
Prim::Circle { at, r, off, cls, id } => {
|
||||
Prim::Circle {
|
||||
at,
|
||||
r,
|
||||
off,
|
||||
cls,
|
||||
id,
|
||||
} => {
|
||||
let s = self.p.s(at[0], at[1], at[2]);
|
||||
let _ = write!(self.out, "<circle class=\"{cls}\"");
|
||||
did(&mut self.out, id.as_deref());
|
||||
@@ -336,7 +394,14 @@ impl Ctx<'_> {
|
||||
r1s(*r)
|
||||
);
|
||||
}
|
||||
Prim::Stem { at, z0, z1, w, cls, id } => {
|
||||
Prim::Stem {
|
||||
at,
|
||||
z0,
|
||||
z1,
|
||||
w,
|
||||
cls,
|
||||
id,
|
||||
} => {
|
||||
let b = self.p.s(at[0], at[1], *z0);
|
||||
let t = self.p.s(at[0], at[1], *z1);
|
||||
self.path_open(cls, id.as_deref());
|
||||
@@ -355,7 +420,12 @@ impl Ctx<'_> {
|
||||
}
|
||||
Prim::Puffs { at, kind, n, dur } => {
|
||||
let s = self.p.s(at[0], at[1], at[2]);
|
||||
let _ = write!(self.out, "<g class=\"iso-vent\" transform=\"translate({} {})\">", r1s(s[0]), r1s(s[1]));
|
||||
let _ = write!(
|
||||
self.out,
|
||||
"<g class=\"iso-vent\" transform=\"translate({} {})\">",
|
||||
r1s(s[0]),
|
||||
r1s(s[1])
|
||||
);
|
||||
let r = if kind == "iso-smoke" {
|
||||
3.2
|
||||
} else if kind == "iso-puff2" {
|
||||
@@ -396,7 +466,12 @@ impl Ctx<'_> {
|
||||
match at {
|
||||
Some(at) => {
|
||||
let s = self.p.s(at[0], at[1], at[2]);
|
||||
let _ = write!(self.out, "<g transform=\"translate({} {})\">{svg}</g>", r1s(s[0]), r1s(s[1]));
|
||||
let _ = write!(
|
||||
self.out,
|
||||
"<g transform=\"translate({} {})\">{svg}</g>",
|
||||
r1s(s[0]),
|
||||
r1s(s[1])
|
||||
);
|
||||
}
|
||||
None => self.out.push_str(&svg),
|
||||
}
|
||||
@@ -457,7 +532,11 @@ pub fn boxes(s: &Sprite, p: Proj) -> (Bb, Sb, bool) {
|
||||
pub fn render(scene: &Scene, rot: u8, opts: &RenderOpts) -> Rendered {
|
||||
let p = Proj::new(rot as i64);
|
||||
let uid = opts.uid.to_string();
|
||||
let mut cx = Ctx { p, uid: &uid, out: String::with_capacity(64 * 1024) };
|
||||
let mut cx = Ctx {
|
||||
p,
|
||||
uid: &uid,
|
||||
out: String::with_capacity(64 * 1024),
|
||||
};
|
||||
let mut frame = [1e9f64, 1e9, -1e9, -1e9];
|
||||
let mut sprites = 0;
|
||||
let mut cycles = 0;
|
||||
@@ -494,7 +573,12 @@ pub fn render(scene: &Scene, rot: u8, opts: &RenderOpts) -> Rendered {
|
||||
} else {
|
||||
(0..live.len()).collect()
|
||||
};
|
||||
let _ = write!(cx.out, "<g class=\"{}\" data-layer=\"{}\">", esc(&layer.opts.cls), esc(&layer.name));
|
||||
let _ = write!(
|
||||
cx.out,
|
||||
"<g class=\"{}\" data-layer=\"{}\">",
|
||||
esc(&layer.opts.cls),
|
||||
esc(&layer.name)
|
||||
);
|
||||
for i in idx {
|
||||
let s = live[i];
|
||||
sprites += 1;
|
||||
@@ -583,7 +667,13 @@ pub fn render(scene: &Scene, rot: u8, opts: &RenderOpts) -> Rendered {
|
||||
frame = [-100.0, -100.0, 100.0, 100.0];
|
||||
warnings.push("the scene is empty".into());
|
||||
}
|
||||
let bbox = [frame[0], frame[1] - 30.0, frame[2] - frame[0], frame[3] - frame[1] + 30.0].map(round1);
|
||||
let bbox = [
|
||||
frame[0],
|
||||
frame[1] - 30.0,
|
||||
frame[2] - frame[0],
|
||||
frame[3] - frame[1] + 30.0,
|
||||
]
|
||||
.map(round1);
|
||||
let focus = match scene.focus_point() {
|
||||
Some(f) => p.s(f[0], f[1], f[2]),
|
||||
None => [bbox[0] + bbox[2] / 2.0, bbox[1] + bbox[3] / 2.0],
|
||||
@@ -627,7 +717,12 @@ mod tests {
|
||||
let sc = one_box();
|
||||
for rot in 0..4 {
|
||||
let r = render(&sc, rot, &RenderOpts::default());
|
||||
assert_eq!(r.html.matches("iso-wall iso-").count(), 3, "rot {rot}: {}", r.html);
|
||||
assert_eq!(
|
||||
r.html.matches("iso-wall iso-").count(),
|
||||
3,
|
||||
"rot {rot}: {}",
|
||||
r.html
|
||||
);
|
||||
assert!(r.html.contains("iso-wall iso-t"));
|
||||
assert!(r.html.contains("iso-wall iso-l"));
|
||||
assert!(r.html.contains("iso-wall iso-r"));
|
||||
@@ -641,14 +736,26 @@ mod tests {
|
||||
// JS (iso-mooban.js prismS, rotation 0) for box(0,0,10,6,0,4): the top face
|
||||
let sc = one_box();
|
||||
let r = render(&sc, 0, &RenderOpts::default());
|
||||
assert!(r.html.contains("<path class=\"iso-wall iso-t\" d=\"M0 -16L40 4L64 -8L24 -28Z\"/>"), "{}", r.html);
|
||||
assert!(
|
||||
r.html
|
||||
.contains("<path class=\"iso-wall iso-t\" d=\"M0 -16L40 4L64 -8L24 -28Z\"/>"),
|
||||
"{}",
|
||||
r.html
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn labels_off_hides_plain_tags_only() {
|
||||
let mut sc = one_box();
|
||||
sc.tag([0.0, 0.0, 0.0], "Gate", 9.0, "gate", "iso-signtag");
|
||||
let r = render(&sc, 1, &RenderOpts { labels: false, ..Default::default() });
|
||||
let r = render(
|
||||
&sc,
|
||||
1,
|
||||
&RenderOpts {
|
||||
labels: false,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
assert!(r.html.contains(">Gate<"));
|
||||
assert!(!r.html.contains(">Hall<"));
|
||||
}
|
||||
@@ -683,9 +790,29 @@ mod tests {
|
||||
let mut sc = Scene::new();
|
||||
let mut s = Sprite::new("roof", "");
|
||||
// south-facing roof slope rising to the north (ccw seen from above/outside)
|
||||
s.face(vec![[0.0, 0.0, 3.0], [10.0, 0.0, 3.0], [10.0, 4.0, 5.0], [0.0, 4.0, 5.0]], "iso-roof", None, false);
|
||||
s.face(
|
||||
vec![
|
||||
[0.0, 0.0, 3.0],
|
||||
[10.0, 0.0, 3.0],
|
||||
[10.0, 4.0, 5.0],
|
||||
[0.0, 4.0, 5.0],
|
||||
],
|
||||
"iso-roof",
|
||||
None,
|
||||
false,
|
||||
);
|
||||
// a vertical wall facing north (ccw seen from the north)
|
||||
s.face(vec![[10.0, 6.0, 0.0], [0.0, 6.0, 0.0], [0.0, 6.0, 3.0], [10.0, 6.0, 3.0]], "iso-wall", None, false);
|
||||
s.face(
|
||||
vec![
|
||||
[10.0, 6.0, 0.0],
|
||||
[0.0, 6.0, 0.0],
|
||||
[0.0, 6.0, 3.0],
|
||||
[10.0, 6.0, 3.0],
|
||||
],
|
||||
"iso-wall",
|
||||
None,
|
||||
false,
|
||||
);
|
||||
sc.add_sprite("main", s);
|
||||
let r0 = render(&sc, 0, &RenderOpts::default()); // from the south-east: north wall hidden
|
||||
assert!(r0.html.contains("iso-roof iso-t"));
|
||||
@@ -698,7 +825,14 @@ mod tests {
|
||||
fn dev_mode_adds_source_and_order() {
|
||||
let mut sc = one_box();
|
||||
sc.layers[2].sprites[0].src = Some("scene.rhai:3".into());
|
||||
let r = render(&sc, 0, &RenderOpts { dev: true, ..Default::default() });
|
||||
let r = render(
|
||||
&sc,
|
||||
0,
|
||||
&RenderOpts {
|
||||
dev: true,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
assert!(r.html.contains("data-src=\"scene.rhai:3\""));
|
||||
assert_eq!(r.order.len(), 1);
|
||||
}
|
||||
|
||||
@@ -17,7 +17,11 @@ pub struct LayerOpts {
|
||||
|
||||
impl Default for LayerOpts {
|
||||
fn default() -> Self {
|
||||
LayerOpts { sort: true, zfirst: false, cls: "iso-sprites".into() }
|
||||
LayerOpts {
|
||||
sort: true,
|
||||
zfirst: false,
|
||||
cls: "iso-sprites".into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,9 +82,25 @@ impl Scene {
|
||||
pub fn new() -> Self {
|
||||
Scene {
|
||||
layers: vec![
|
||||
Layer { name: "ground".into(), opts: LayerOpts { sort: false, zfirst: false, cls: "iso-ground".into() }, sprites: vec![] },
|
||||
Layer { name: "floor".into(), opts: LayerOpts::default(), sprites: vec![] },
|
||||
Layer { name: "main".into(), opts: LayerOpts::default(), sprites: vec![] },
|
||||
Layer {
|
||||
name: "ground".into(),
|
||||
opts: LayerOpts {
|
||||
sort: false,
|
||||
zfirst: false,
|
||||
cls: "iso-ground".into(),
|
||||
},
|
||||
sprites: vec![],
|
||||
},
|
||||
Layer {
|
||||
name: "floor".into(),
|
||||
opts: LayerOpts::default(),
|
||||
sprites: vec![],
|
||||
},
|
||||
Layer {
|
||||
name: "main".into(),
|
||||
opts: LayerOpts::default(),
|
||||
sprites: vec![],
|
||||
},
|
||||
],
|
||||
tags: vec![],
|
||||
info: BTreeMap::new(),
|
||||
@@ -101,8 +121,17 @@ impl Scene {
|
||||
self.layers[i].opts = opts;
|
||||
return i;
|
||||
}
|
||||
let at = before.and_then(|b| self.layer_index(b)).unwrap_or(self.layers.len());
|
||||
self.layers.insert(at, Layer { name: name.into(), opts, sprites: vec![] });
|
||||
let at = before
|
||||
.and_then(|b| self.layer_index(b))
|
||||
.unwrap_or(self.layers.len());
|
||||
self.layers.insert(
|
||||
at,
|
||||
Layer {
|
||||
name: name.into(),
|
||||
opts,
|
||||
sprites: vec![],
|
||||
},
|
||||
);
|
||||
at
|
||||
}
|
||||
|
||||
@@ -119,13 +148,39 @@ impl Scene {
|
||||
self.layer_mut(layer).sprites.push(s);
|
||||
}
|
||||
|
||||
pub fn tag(&mut self, at: P3, text: impl Into<String>, prio: f64, id: impl Into<String>, kind: impl Into<String>) {
|
||||
self.tags.push(Tag { at, text: text.into(), prio, id: id.into(), kind: kind.into() });
|
||||
pub fn tag(
|
||||
&mut self,
|
||||
at: P3,
|
||||
text: impl Into<String>,
|
||||
prio: f64,
|
||||
id: impl Into<String>,
|
||||
kind: impl Into<String>,
|
||||
) {
|
||||
self.tags.push(Tag {
|
||||
at,
|
||||
text: text.into(),
|
||||
prio,
|
||||
id: id.into(),
|
||||
kind: kind.into(),
|
||||
});
|
||||
}
|
||||
|
||||
pub fn info(&mut self, id: impl Into<String>, name: impl Into<String>, detail: impl Into<String>) {
|
||||
pub fn info(
|
||||
&mut self,
|
||||
id: impl Into<String>,
|
||||
name: impl Into<String>,
|
||||
detail: impl Into<String>,
|
||||
) {
|
||||
let id = id.into();
|
||||
self.info.insert(id.clone(), Info { id, name: name.into(), detail: detail.into(), rows: vec![] });
|
||||
self.info.insert(
|
||||
id.clone(),
|
||||
Info {
|
||||
id,
|
||||
name: name.into(),
|
||||
detail: detail.into(),
|
||||
rows: vec![],
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
pub fn sprite_count(&self) -> usize {
|
||||
@@ -137,7 +192,9 @@ impl Scene {
|
||||
use crate::prim::Prim;
|
||||
fn walk(p: &Prim, out: &mut std::collections::BTreeSet<String>) {
|
||||
let id = match p {
|
||||
Prim::Prism { st, .. } | Prim::Cyl { st, .. } | Prim::Frust { st, .. } => st.id.clone(),
|
||||
Prim::Prism { st, .. } | Prim::Cyl { st, .. } | Prim::Frust { st, .. } => {
|
||||
st.id.clone()
|
||||
}
|
||||
Prim::Open { id, .. }
|
||||
| Prim::Flat { id, .. }
|
||||
| Prim::Lines { id, .. }
|
||||
@@ -151,7 +208,10 @@ impl Scene {
|
||||
None
|
||||
}
|
||||
Prim::Seq { items } => {
|
||||
items.iter().flat_map(|(_, v)| v.iter()).for_each(|q| walk(q, out));
|
||||
items
|
||||
.iter()
|
||||
.flat_map(|(_, v)| v.iter())
|
||||
.for_each(|q| walk(q, out));
|
||||
None
|
||||
}
|
||||
_ => None,
|
||||
@@ -174,7 +234,10 @@ impl Scene {
|
||||
|
||||
/// The default focus point: the explicit one, else the bounds centre.
|
||||
pub fn focus_point(&self) -> Option<P3> {
|
||||
self.focus.or_else(|| self.bounds.map(|(b, _)| [(b[0] + b[2]) / 2.0, (b[1] + b[3]) / 2.0, 0.0]))
|
||||
self.focus.or_else(|| {
|
||||
self.bounds
|
||||
.map(|(b, _)| [(b[0] + b[2]) / 2.0, (b[1] + b[3]) / 2.0, 0.0])
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -86,7 +86,10 @@ pub fn sort(boxes: &[(Bb, Sb)], zf: bool) -> Order {
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut out = Order { order: Vec::with_capacity(n), cycles: 0 };
|
||||
let mut out = Order {
|
||||
order: Vec::with_capacity(n),
|
||||
cycles: 0,
|
||||
};
|
||||
for _ in 0..n {
|
||||
let mut best: Option<usize> = None;
|
||||
for i in 0..n {
|
||||
@@ -102,7 +105,9 @@ pub fn sort(boxes: &[(Bb, Sb)], zf: bool) -> Order {
|
||||
}
|
||||
best = match best {
|
||||
None => Some(i),
|
||||
Some(b) if indeg[i] < indeg[b] || (indeg[i] == indeg[b] && key[i] < key[b]) => Some(i),
|
||||
Some(b) if indeg[i] < indeg[b] || (indeg[i] == indeg[b] && key[i] < key[b]) => {
|
||||
Some(i)
|
||||
}
|
||||
keep => keep,
|
||||
};
|
||||
}
|
||||
@@ -128,14 +133,26 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn separated_on_x_draws_far_first() {
|
||||
let o = sort(&[bx(5.0, 0.0, 6.0, 1.0, 0.0, 1.0), bx(0.0, 0.0, 1.0, 1.0, 0.0, 1.0)], false);
|
||||
let o = sort(
|
||||
&[
|
||||
bx(5.0, 0.0, 6.0, 1.0, 0.0, 1.0),
|
||||
bx(0.0, 0.0, 1.0, 1.0, 0.0, 1.0),
|
||||
],
|
||||
false,
|
||||
);
|
||||
assert_eq!(o.order, vec![1, 0]);
|
||||
assert_eq!(o.cycles, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stacked_boxes_draw_bottom_first() {
|
||||
let o = sort(&[bx(0.0, 0.0, 1.0, 1.0, 1.0, 2.0), bx(0.0, 0.0, 1.0, 1.0, 0.0, 1.0)], false);
|
||||
let o = sort(
|
||||
&[
|
||||
bx(0.0, 0.0, 1.0, 1.0, 1.0, 2.0),
|
||||
bx(0.0, 0.0, 1.0, 1.0, 0.0, 1.0),
|
||||
],
|
||||
false,
|
||||
);
|
||||
assert_eq!(o.order, vec![1, 0]);
|
||||
}
|
||||
|
||||
|
||||
@@ -63,7 +63,12 @@ impl Xf {
|
||||
pub fn rect(&self, x0: f64, y0: f64, x1: f64, y1: f64) -> [f64; 4] {
|
||||
let a = self.p(x0, y0);
|
||||
let b = self.p(x1, y1);
|
||||
[a[0].min(b[0]), a[1].min(b[1]), a[0].max(b[0]), a[1].max(b[1])]
|
||||
[
|
||||
a[0].min(b[0]),
|
||||
a[1].min(b[1]),
|
||||
a[0].max(b[0]),
|
||||
a[1].max(b[1]),
|
||||
]
|
||||
}
|
||||
pub fn pts(&self, pts: &[P2]) -> Vec<P2> {
|
||||
pts.iter().map(|q| self.p(q[0], q[1])).collect()
|
||||
@@ -76,8 +81,15 @@ impl Xf {
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
enum Frame {
|
||||
Group { cls: String, style: String, items: Vec<Prim> },
|
||||
Seq { items: Vec<(P2, Vec<Prim>)>, cur: Option<(P2, Vec<Prim>)> },
|
||||
Group {
|
||||
cls: String,
|
||||
style: String,
|
||||
items: Vec<Prim>,
|
||||
},
|
||||
Seq {
|
||||
items: Vec<(P2, Vec<Prim>)>,
|
||||
cur: Option<(P2, Vec<Prim>)>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
||||
@@ -101,6 +113,9 @@ pub struct Sprite {
|
||||
frames: Vec<Frame>,
|
||||
}
|
||||
|
||||
/// One item of [`Sprite::seq`]: a plan anchor and the drawing.
|
||||
pub type SeqItem<'a> = (P2, Box<dyn FnOnce(&mut Sprite) + 'a>);
|
||||
|
||||
/// Points of an axis-aligned rectangle, counter-clockwise from (x0, y0).
|
||||
pub fn rect(x0: f64, y0: f64, x1: f64, y1: f64) -> Vec<P2> {
|
||||
vec![[x0, y0], [x1, y0], [x1, y1], [x0, y1]]
|
||||
@@ -108,7 +123,11 @@ pub fn rect(x0: f64, y0: f64, x1: f64, y1: f64) -> Vec<P2> {
|
||||
|
||||
impl Sprite {
|
||||
pub fn new(id: impl Into<String>, cls: impl Into<String>) -> Self {
|
||||
Sprite { id: id.into(), cls: cls.into(), ..Default::default() }
|
||||
Sprite {
|
||||
id: id.into(),
|
||||
cls: cls.into(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_frame(mut self, xf: Xf) -> Self {
|
||||
@@ -141,7 +160,10 @@ impl Sprite {
|
||||
|
||||
pub fn ext_box(&mut self, x0: f64, y0: f64, x1: f64, y1: f64, z0: f64, z1: f64) -> &mut Self {
|
||||
for z in [z0, z1] {
|
||||
self.ext(x0, y0, z).ext(x1, y0, z).ext(x1, y1, z).ext(x0, y1, z);
|
||||
self.ext(x0, y0, z)
|
||||
.ext(x1, y0, z)
|
||||
.ext(x1, y1, z)
|
||||
.ext(x0, y1, z);
|
||||
}
|
||||
self
|
||||
}
|
||||
@@ -156,13 +178,27 @@ impl Sprite {
|
||||
|
||||
/// Axis-aligned box (`box` in scripts; `box` is a Rust keyword).
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn cuboid(&mut self, x0: f64, y0: f64, x1: f64, y1: f64, z0: f64, z1: f64, st: Style) -> &mut Self {
|
||||
pub fn cuboid(
|
||||
&mut self,
|
||||
x0: f64,
|
||||
y0: f64,
|
||||
x1: f64,
|
||||
y1: f64,
|
||||
z0: f64,
|
||||
z1: f64,
|
||||
st: Style,
|
||||
) -> &mut Self {
|
||||
let r = self.xf.rect(x0, y0, x1, y1);
|
||||
let (z0, z1) = (self.xf.z(z0), self.xf.z(z1));
|
||||
let pts = rect(r[0], r[1], r[2], r[3]);
|
||||
self.ext_world(&pts, z0);
|
||||
self.ext_world(&pts, z1);
|
||||
self.push(Prim::Prism { pts, z0: Zs::One(z0), z1: Zs::One(z1), st })
|
||||
self.push(Prim::Prism {
|
||||
pts,
|
||||
z0: Zs::One(z0),
|
||||
z1: Zs::One(z1),
|
||||
st,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn prism(&mut self, pts: Vec<P2>, z0: Zs, z1: Zs, st: Style) -> &mut Self {
|
||||
@@ -188,30 +224,73 @@ impl Sprite {
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn frust(&mut self, cx: f64, cy: f64, r0: f64, r1: f64, z0: f64, z1: f64, n: u32, st: Style) -> &mut Self {
|
||||
pub fn frust(
|
||||
&mut self,
|
||||
cx: f64,
|
||||
cy: f64,
|
||||
r0: f64,
|
||||
r1: f64,
|
||||
z0: f64,
|
||||
z1: f64,
|
||||
n: u32,
|
||||
st: Style,
|
||||
) -> &mut Self {
|
||||
let r = r0.max(r1);
|
||||
self.ext_box(cx - r, cy - r, cx + r, cy + r, z0, z1);
|
||||
let c = self.xf.p(cx, cy);
|
||||
let (z0, z1) = (self.xf.z(z0), self.xf.z(z1));
|
||||
self.push(Prim::Frust { c, r0, r1, z0, z1, n: n.max(3), st })
|
||||
self.push(Prim::Frust {
|
||||
c,
|
||||
r0,
|
||||
r1,
|
||||
z0,
|
||||
z1,
|
||||
n: n.max(3),
|
||||
st,
|
||||
})
|
||||
}
|
||||
|
||||
/// Sloped slab (stair flight, ramp) over x0..x1, rising from y0 (za) to
|
||||
/// y1 (zb), th thick.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn slope(&mut self, x0: f64, x1: f64, y0: f64, y1: f64, za: f64, zb: f64, th: f64, mat: &str) -> &mut Self {
|
||||
pub fn slope(
|
||||
&mut self,
|
||||
x0: f64,
|
||||
x1: f64,
|
||||
y0: f64,
|
||||
y1: f64,
|
||||
za: f64,
|
||||
zb: f64,
|
||||
th: f64,
|
||||
mat: &str,
|
||||
) -> &mut Self {
|
||||
let zt = vec![za, za, zb, zb];
|
||||
let zb_: Vec<f64> = zt.iter().map(|z| z - th).collect();
|
||||
self.prism(rect(x0, y0, x1, y1), Zs::Each(zb_), Zs::Each(zt), Style::mat(mat))
|
||||
self.prism(
|
||||
rect(x0, y0, x1, y1),
|
||||
Zs::Each(zb_),
|
||||
Zs::Each(zt),
|
||||
Style::mat(mat),
|
||||
)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------- surfaces
|
||||
|
||||
/// Openings on the vertical face a -> b (outward normal on the right).
|
||||
pub fn open(&mut self, a: P2, b: P2, list: Vec<[f64; 4]>, cls: &str, id: Option<String>) -> &mut Self {
|
||||
pub fn open(
|
||||
&mut self,
|
||||
a: P2,
|
||||
b: P2,
|
||||
list: Vec<[f64; 4]>,
|
||||
cls: &str,
|
||||
id: Option<String>,
|
||||
) -> &mut Self {
|
||||
let (a, b) = (self.xf.p(a[0], a[1]), self.xf.p(b[0], b[1]));
|
||||
let dz = self.xf.z;
|
||||
let list: Vec<[f64; 4]> = list.into_iter().map(|q| [q[0], q[1], q[2] + dz, q[3] + dz]).collect();
|
||||
let list: Vec<[f64; 4]> = list
|
||||
.into_iter()
|
||||
.map(|q| [q[0], q[1], q[2] + dz, q[3] + dz])
|
||||
.collect();
|
||||
let (dx, dy) = (b[0] - a[0], b[1] - a[1]);
|
||||
let l = (dx * dx + dy * dy).sqrt().max(1e-9);
|
||||
let (ux, uy) = (dx / l, dy / l);
|
||||
@@ -219,16 +298,33 @@ impl Sprite {
|
||||
self.ext.push([a[0] + ux * q[0], a[1] + uy * q[0], q[2]]);
|
||||
self.ext.push([a[0] + ux * q[1], a[1] + uy * q[1], q[3]]);
|
||||
}
|
||||
self.push(Prim::Open { a, b, list, cls: cls.into(), id })
|
||||
self.push(Prim::Open {
|
||||
a,
|
||||
b,
|
||||
list,
|
||||
cls: cls.into(),
|
||||
id,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn flat(&mut self, polys: Vec<Vec<P2>>, z: f64, cls: &str, id: Option<String>) -> &mut Self {
|
||||
pub fn flat(
|
||||
&mut self,
|
||||
polys: Vec<Vec<P2>>,
|
||||
z: f64,
|
||||
cls: &str,
|
||||
id: Option<String>,
|
||||
) -> &mut Self {
|
||||
let polys: Vec<Vec<P2>> = polys.iter().map(|p| self.xf.pts(p)).collect();
|
||||
let z = self.xf.z(z);
|
||||
for p in &polys {
|
||||
self.ext_world(p, z);
|
||||
}
|
||||
self.push(Prim::Flat { polys, z, cls: cls.into(), id })
|
||||
self.push(Prim::Flat {
|
||||
polys,
|
||||
z,
|
||||
cls: cls.into(),
|
||||
id,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn lines(&mut self, segs: Vec<[f64; 4]>, z: f64, cls: &str) -> &mut Self {
|
||||
@@ -245,30 +341,67 @@ impl Sprite {
|
||||
self.ext.push([s[0], s[1], z]);
|
||||
self.ext.push([s[2], s[3], z]);
|
||||
}
|
||||
self.push(Prim::Lines { segs, z, cls: cls.into(), id: None })
|
||||
self.push(Prim::Lines {
|
||||
segs,
|
||||
z,
|
||||
cls: cls.into(),
|
||||
id: None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn disc(&mut self, cx: f64, cy: f64, z: f64, r: f64, cls: &str, id: Option<String>) -> &mut Self {
|
||||
pub fn disc(
|
||||
&mut self,
|
||||
cx: f64,
|
||||
cy: f64,
|
||||
z: f64,
|
||||
r: f64,
|
||||
cls: &str,
|
||||
id: Option<String>,
|
||||
) -> &mut Self {
|
||||
self.ext_box(cx - r, cy - r, cx + r, cy + r, z, z);
|
||||
let c = self.xf.p(cx, cy);
|
||||
let z = self.xf.z(z);
|
||||
self.push(Prim::Disc { c, z, r, cls: cls.into(), id })
|
||||
self.push(Prim::Disc {
|
||||
c,
|
||||
z,
|
||||
r,
|
||||
cls: cls.into(),
|
||||
id,
|
||||
})
|
||||
}
|
||||
|
||||
/// Planar 3D polygon shaded by its normal (see [`Prim::Face`]).
|
||||
pub fn face(&mut self, pts: Vec<P3>, mat: &str, id: Option<String>, two_sided: bool) -> &mut Self {
|
||||
pub fn face(
|
||||
&mut self,
|
||||
pts: Vec<P3>,
|
||||
mat: &str,
|
||||
id: Option<String>,
|
||||
two_sided: bool,
|
||||
) -> &mut Self {
|
||||
let pts: Vec<P3> = pts.into_iter().map(|p| self.xf.p3(p)).collect();
|
||||
self.ext.extend(pts.iter().copied());
|
||||
self.push(Prim::Face { pts, mat: mat.into(), id, two_sided })
|
||||
self.push(Prim::Face {
|
||||
pts,
|
||||
mat: mat.into(),
|
||||
id,
|
||||
two_sided,
|
||||
})
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
let polys: Vec<Vec<P3>> = polys.into_iter().map(|p| p.into_iter().map(|q| self.xf.p3(q)).collect()).collect();
|
||||
let polys: Vec<Vec<P3>> = polys
|
||||
.into_iter()
|
||||
.map(|p| p.into_iter().map(|q| self.xf.p3(q)).collect())
|
||||
.collect();
|
||||
for p in &polys {
|
||||
self.ext.extend(p.iter().copied());
|
||||
}
|
||||
self.push(Prim::Poly { polys, cls: cls.into(), id })
|
||||
self.push(Prim::Poly {
|
||||
polys,
|
||||
cls: cls.into(),
|
||||
id,
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------- screen-space details
|
||||
@@ -277,14 +410,27 @@ impl Sprite {
|
||||
/// sticks out of the sprite).
|
||||
pub fn circle(&mut self, at: P3, r: f64, off: P2, cls: &str) -> &mut Self {
|
||||
let at = self.xf.p3(at);
|
||||
self.push(Prim::Circle { at, r, off, cls: cls.into(), id: None })
|
||||
self.push(Prim::Circle {
|
||||
at,
|
||||
r,
|
||||
off,
|
||||
cls: cls.into(),
|
||||
id: None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn stem(&mut self, x: f64, y: f64, z0: f64, z1: f64, w: f64, cls: &str) -> &mut Self {
|
||||
self.ext(x, y, z0).ext(x, y, z1);
|
||||
let at = self.xf.p(x, y);
|
||||
let (z0, z1) = (self.xf.z(z0), self.xf.z(z1));
|
||||
self.push(Prim::Stem { at, z0, z1, w, cls: cls.into(), id: None })
|
||||
self.push(Prim::Stem {
|
||||
at,
|
||||
z0,
|
||||
z1,
|
||||
w,
|
||||
cls: cls.into(),
|
||||
id: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// A head (ball of radius r metres) centred at z.
|
||||
@@ -307,13 +453,22 @@ impl Sprite {
|
||||
}
|
||||
|
||||
pub fn led(&mut self, x: f64, y: f64, z: f64, cls: &str) -> &mut Self {
|
||||
let c = if cls.is_empty() { "iso-led".to_string() } else { format!("iso-led {cls}") };
|
||||
let c = if cls.is_empty() {
|
||||
"iso-led".to_string()
|
||||
} else {
|
||||
format!("iso-led {cls}")
|
||||
};
|
||||
self.circle([x, y, z], 0.9, [0.0, 0.0], &c)
|
||||
}
|
||||
|
||||
pub fn puffs(&mut self, x: f64, y: f64, z: f64, kind: &str, n: u32, dur: f64) -> &mut Self {
|
||||
let at = self.xf.p3([x, y, z]);
|
||||
self.push(Prim::Puffs { at, kind: kind.into(), n: n.max(1), dur })
|
||||
self.push(Prim::Puffs {
|
||||
at,
|
||||
kind: kind.into(),
|
||||
n: n.max(1),
|
||||
dur,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn fan(&mut self, x: f64, y: f64, z: f64, r: f64) -> &mut Self {
|
||||
@@ -323,14 +478,21 @@ impl Sprite {
|
||||
|
||||
pub fn raw(&mut self, at: Option<P3>, svg: impl Into<String>) -> &mut Self {
|
||||
let at = at.map(|a| self.xf.p3(a));
|
||||
self.push(Prim::Raw { at, svg: svg.into() })
|
||||
self.push(Prim::Raw {
|
||||
at,
|
||||
svg: svg.into(),
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------- structure
|
||||
|
||||
/// Open a `<g class style>`; close it with [`Sprite::end_group`].
|
||||
pub fn begin_group(&mut self, cls: &str, style: &str) -> &mut Self {
|
||||
self.frames.push(Frame::Group { cls: cls.into(), style: style.into(), items: vec![] });
|
||||
self.frames.push(Frame::Group {
|
||||
cls: cls.into(),
|
||||
style: style.into(),
|
||||
items: vec![],
|
||||
});
|
||||
self
|
||||
}
|
||||
|
||||
@@ -344,7 +506,10 @@ impl Sprite {
|
||||
/// Start a back-to-front sequence; each [`Sprite::seq_item`] starts an
|
||||
/// item anchored at a plan point; [`Sprite::end_seq`] closes it.
|
||||
pub fn begin_seq(&mut self) -> &mut Self {
|
||||
self.frames.push(Frame::Seq { items: vec![], cur: None });
|
||||
self.frames.push(Frame::Seq {
|
||||
items: vec![],
|
||||
cur: None,
|
||||
});
|
||||
self
|
||||
}
|
||||
|
||||
@@ -370,7 +535,7 @@ impl Sprite {
|
||||
}
|
||||
|
||||
/// Draw items back to front by the depth of their anchor.
|
||||
pub fn seq(&mut self, items: Vec<(P2, Box<dyn FnOnce(&mut Sprite) + '_>)>) -> &mut Self {
|
||||
pub fn seq(&mut self, items: Vec<SeqItem<'_>>) -> &mut Self {
|
||||
self.begin_seq();
|
||||
for (at, f) in items {
|
||||
self.seq_item(at);
|
||||
@@ -442,7 +607,10 @@ mod tests {
|
||||
let f = Xf::new(3.0, -4.0, 0.0, deg);
|
||||
let w = f.p(1.5, 2.5);
|
||||
let l = f.inv(w[0], w[1]);
|
||||
assert!((l[0] - 1.5).abs() < 1e-12 && (l[1] - 2.5).abs() < 1e-12, "{deg}");
|
||||
assert!(
|
||||
(l[0] - 1.5).abs() < 1e-12 && (l[1] - 2.5).abs() < 1e-12,
|
||||
"{deg}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user