isokit: project layer, Studio GUI, MCP server, viewer, CLI, demo project and docs

This commit is contained in:
grabowski committed 2026-10-22 15:29:07 +07:00
1 parent 95c80ec6b1
commit 564aeb75f6
47 files changed
+5978 -366

No files matched your search

+200 -32
View File
@@ -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}"
);
}
}