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
@@ -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);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user