Diagonals: streets, walls, fences, hedges and house walls at any angle
Deploy web Studio / deploy (push) Successful in 1m5s
Deploy web Studio / deploy (push) Successful in 1m5s
Object frames turn by any angle (quarter turns stay exact, so scenes along the axes render byte for byte as before); a box in a turned frame is a prism with its own corners. Rotation is kept as given (normalised to 0..360), no longer snapped to quarter turns. Depth sort: sprites whose plan outline is not their box carry their convex outline; when boxes overlap, an outline edge that separates two sprites orders them. A long diagonal wall sorts right against both sides. Joints (line_meet, end_cut, cut_box in util.rhai): where a piece off the axes is involved, corners are mitred and T-junctions slanted; fences put a corner post or pillar in its own sprite and mitre stone bases; Y joints let the lowest id run through. Streets: every end part is a cut e + k*w, so side streets come in at any angle (slanted sidewalk gaps), two streets meet in a mitre with seam patches; fixes a curve's outer sidewalk that was skipped when its kerb and back lines had different point counts. Studio: runs and drags keep to x/y or snap to 45 degree diagonals, Ctrl gives any angle; a road run turning 45 degrees gets a 45 degree curve; Rotation is a number box. Streets demo: a diagonal lane with a hedge. MCP guidance, SCRIPTING/README/ROADMAP. Tests: Rust 76, Studio and web checks green.
This commit is contained in:
1 parent
c91d63663b
commit
96d472625f
18 files changed
+1051
-178
No files matched your search
@@ -563,6 +563,15 @@ impl Ctx<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
/// The sprite's convex plan outline in view space when it is not its box.
|
||||
pub fn hull(s: &Sprite, p: Proj, bb: &Bb) -> Option<sort::Hull> {
|
||||
if s.ext.len() < 3 {
|
||||
return None;
|
||||
}
|
||||
let pts: Vec<[f64; 2]> = s.ext.iter().map(|e| p.r(e[0], e[1])).collect();
|
||||
sort::hull_of(&pts, bb)
|
||||
}
|
||||
|
||||
/// Sort box of a sprite in a rotation: (view box, screen box, has extent).
|
||||
pub fn boxes(s: &Sprite, p: Proj) -> (Bb, Sb, bool) {
|
||||
let mut bb: Bb = [1e9, 1e9, -1e9, -1e9, 1e9, -1e9];
|
||||
@@ -623,7 +632,12 @@ pub fn render(scene: &Scene, rot: u8, opts: &RenderOpts) -> Rendered {
|
||||
}
|
||||
let idx: Vec<usize> = if layer.opts.sort {
|
||||
let pairs: Vec<(Bb, Sb)> = bx.iter().map(|(b, s, _)| (*b, *s)).collect();
|
||||
let o = sort::sort(&pairs, layer.opts.zfirst);
|
||||
let hulls: Vec<Option<sort::Hull>> = live
|
||||
.iter()
|
||||
.zip(&bx)
|
||||
.map(|(s, (b, _, _))| hull(s, p, b))
|
||||
.collect();
|
||||
let o = sort::sort_hulls(&pairs, &hulls, layer.opts.zfirst);
|
||||
cycles += o.cycles;
|
||||
o.order
|
||||
} else {
|
||||
|
||||
@@ -3,15 +3,61 @@
|
||||
//! Each sprite has a view-space box `bb = [vx0, vy0, vx1, vy1, z0, z1]` and a
|
||||
//! screen box `sb = [x0, y0, x1, y1]`. Only pairs whose screen boxes overlap
|
||||
//! are compared; a pair is ordered when the boxes are separated along x,
|
||||
//! then y, then z (or z first under ground). The resulting graph is drawn in
|
||||
//! topological order, choosing the smallest centre key among the ready
|
||||
//! sprites; a cycle is broken at the sprite with the fewest unmet edges.
|
||||
//! then y, then z (or z first under ground). Sprites whose plan outline is not
|
||||
//! their box (pieces turned off the axes, mitred ends) also carry their convex
|
||||
//! outline (`Hull`): when the boxes overlap, an outline edge that separates
|
||||
//! the two orders them (the one on the viewer's side of it goes later), so a
|
||||
//! long diagonal wall sorts right against everything on either side. The
|
||||
//! resulting graph is drawn in topological order, choosing the smallest centre
|
||||
//! key among the ready sprites; a cycle is broken at the sprite with the fewest
|
||||
//! unmet edges.
|
||||
|
||||
pub type Bb = [f64; 6];
|
||||
pub type Sb = [f64; 4];
|
||||
/// Convex plan outline in view space, counter-clockwise.
|
||||
pub type Hull = Vec<[f64; 2]>;
|
||||
|
||||
/// -1: a is drawn before b, 1: after, 0: no separating axis (fall back to the key).
|
||||
pub fn cmp_bb(a: &Bb, b: &Bb, zf: bool) -> i32 {
|
||||
/// The convex outline of view-space plan points, or None when it is just
|
||||
/// the box `bb` (nothing to add to the box test).
|
||||
pub fn hull_of(pts: &[[f64; 2]], bb: &Bb) -> Option<Hull> {
|
||||
let mut p: Vec<[f64; 2]> = pts.to_vec();
|
||||
p.sort_by(|a, b| a[0].total_cmp(&b[0]).then(a[1].total_cmp(&b[1])));
|
||||
p.dedup_by(|a, b| (a[0] - b[0]).abs() < 1e-9 && (a[1] - b[1]).abs() < 1e-9);
|
||||
if p.len() < 2 {
|
||||
return None;
|
||||
}
|
||||
let cross = |o: [f64; 2], a: [f64; 2], b: [f64; 2]| {
|
||||
(a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0])
|
||||
};
|
||||
let mut h: Vec<[f64; 2]> = Vec::with_capacity(p.len() + 1);
|
||||
for pass in 0..2 {
|
||||
let start = h.len();
|
||||
let it: Box<dyn Iterator<Item = &[f64; 2]>> = if pass == 0 {
|
||||
Box::new(p.iter())
|
||||
} else {
|
||||
Box::new(p.iter().rev())
|
||||
};
|
||||
for &q in it {
|
||||
while h.len() >= start + 2 && cross(h[h.len() - 2], h[h.len() - 1], q) <= 1e-12 {
|
||||
h.pop();
|
||||
}
|
||||
h.push(q);
|
||||
}
|
||||
h.pop();
|
||||
}
|
||||
let e = 1e-6;
|
||||
let on_box = |q: &[f64; 2]| {
|
||||
((q[0] - bb[0]).abs() < e || (q[0] - bb[2]).abs() < e)
|
||||
&& ((q[1] - bb[1]).abs() < e || (q[1] - bb[3]).abs() < e)
|
||||
};
|
||||
if h.len() == 4 && h.iter().all(on_box) {
|
||||
return None;
|
||||
}
|
||||
(h.len() >= 2).then_some(h)
|
||||
}
|
||||
|
||||
/// Is a's box separated from b's: -1 a is drawn before b, 1 after, 0 no axis.
|
||||
fn sep_bb(a: &Bb, b: &Bb, zf: bool) -> i32 {
|
||||
let e = 1e-6;
|
||||
if zf {
|
||||
if a[5] <= b[4] + e {
|
||||
@@ -39,6 +85,11 @@ pub fn cmp_bb(a: &Bb, b: &Bb, zf: bool) -> i32 {
|
||||
if b[5] <= a[4] + e {
|
||||
return 1;
|
||||
}
|
||||
0
|
||||
}
|
||||
|
||||
/// Order by centre key: -1 a first, 1 after, 0 equal.
|
||||
fn cmp_key(a: &Bb, b: &Bb) -> i32 {
|
||||
let ka = a[0] + a[2] + a[1] + a[3];
|
||||
let kb = b[0] + b[2] + b[1] + b[3];
|
||||
if ka < kb {
|
||||
@@ -50,6 +101,53 @@ pub fn cmp_bb(a: &Bb, b: &Bb, zf: bool) -> i32 {
|
||||
}
|
||||
}
|
||||
|
||||
/// -1: a is drawn before b, 1: after, 0: no separating axis and equal keys.
|
||||
pub fn cmp_bb(a: &Bb, b: &Bb, zf: bool) -> i32 {
|
||||
match sep_bb(a, b, zf) {
|
||||
0 => cmp_key(a, b),
|
||||
c => c,
|
||||
}
|
||||
}
|
||||
|
||||
/// Is outline a separated from outline b along one of their edge normals?
|
||||
/// The one on the viewer's side (towards +x +y in view space) goes later.
|
||||
fn sep_hull(a: &[[f64; 2]], b: &[[f64; 2]]) -> i32 {
|
||||
let proj = |p: &[[f64; 2]], d: [f64; 2]| {
|
||||
p.iter().fold((f64::MAX, f64::MIN), |(lo, hi), q| {
|
||||
let v = q[0] * d[0] + q[1] * d[1];
|
||||
(lo.min(v), hi.max(v))
|
||||
})
|
||||
};
|
||||
for poly in [a, b] {
|
||||
let n = poly.len();
|
||||
for i in 0..n {
|
||||
let (p, q) = (poly[i], poly[(i + 1) % n]);
|
||||
let (dx, dy) = (q[0] - p[0], q[1] - p[1]);
|
||||
let l = (dx * dx + dy * dy).sqrt();
|
||||
if l < 1e-9 {
|
||||
continue;
|
||||
}
|
||||
let d = [dy / l, -dx / l];
|
||||
let s = d[0] + d[1];
|
||||
if s.abs() < 1e-6 {
|
||||
continue;
|
||||
}
|
||||
let ((alo, ahi), (blo, bhi)) = (proj(a, d), proj(b, d));
|
||||
if ahi <= blo + 1e-6 {
|
||||
return if s > 0.0 { -1 } else { 1 };
|
||||
}
|
||||
if bhi <= alo + 1e-6 {
|
||||
return if s > 0.0 { 1 } else { -1 };
|
||||
}
|
||||
}
|
||||
}
|
||||
0
|
||||
}
|
||||
|
||||
fn rect_of(b: &Bb) -> Hull {
|
||||
vec![[b[0], b[1]], [b[2], b[1]], [b[2], b[3]], [b[0], b[3]]]
|
||||
}
|
||||
|
||||
/// Result of [`sort`]: the draw order and how many times a cycle had to be broken.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Order {
|
||||
@@ -58,7 +156,14 @@ pub struct Order {
|
||||
}
|
||||
|
||||
pub fn sort(boxes: &[(Bb, Sb)], zf: bool) -> Order {
|
||||
sort_hulls(boxes, &[], zf)
|
||||
}
|
||||
|
||||
/// [`sort`] with the outlines of the sprites that have one (`hulls[i]`;
|
||||
/// missing entries count as None).
|
||||
pub fn sort_hulls(boxes: &[(Bb, Sb)], hulls: &[Option<Hull>], zf: bool) -> Order {
|
||||
let n = boxes.len();
|
||||
let hull = |i: usize| hulls.get(i).and_then(|h| h.as_ref());
|
||||
let mut adj: Vec<Vec<usize>> = vec![vec![]; n];
|
||||
let mut indeg = vec![0usize; n];
|
||||
let mut done = vec![false; n];
|
||||
@@ -73,7 +178,16 @@ pub fn sort(boxes: &[(Bb, Sb)], zf: bool) -> Order {
|
||||
if asb[2] <= bsb[0] || bsb[2] <= asb[0] || asb[3] <= bsb[1] || bsb[3] <= asb[1] {
|
||||
continue;
|
||||
}
|
||||
match cmp_bb(ab, bb, zf) {
|
||||
let mut c = sep_bb(ab, bb, zf);
|
||||
if c == 0 && (hull(i).is_some() || hull(j).is_some()) {
|
||||
let ha = hull(i).cloned().unwrap_or_else(|| rect_of(ab));
|
||||
let hb = hull(j).cloned().unwrap_or_else(|| rect_of(bb));
|
||||
c = sep_hull(&ha, &hb);
|
||||
}
|
||||
if c == 0 {
|
||||
c = cmp_key(ab, bb);
|
||||
}
|
||||
match c {
|
||||
c if c < 0 => {
|
||||
adj[i].push(j);
|
||||
indeg[j] += 1;
|
||||
@@ -171,4 +285,32 @@ mod tests {
|
||||
let hi = bx(1.0, 1.0, 3.0, 3.0, -1.0, 0.0);
|
||||
assert_eq!(sort(&[hi, lo], true).order, vec![1, 0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn outlines_order_what_lies_either_side_of_a_diagonal_wall() {
|
||||
// a wall 0.3 m thick from (0, 10) to (10, 0); its box spans the whole square
|
||||
let wall = vec![[0.0, 9.8], [9.8, 0.0], [10.2, 0.0], [0.2, 10.0]];
|
||||
let wb = [0.0, 0.0, 10.2, 10.0, 0.0, 2.0];
|
||||
let far = [2.0, 2.0, 3.0, 3.0, 0.0, 1.0]; // behind it (smaller x + y)
|
||||
let near = [7.0, 7.0, 8.0, 8.0, 0.0, 1.0]; // in front
|
||||
let s = [0.0, 0.0, 10.0, 10.0];
|
||||
let hw = hull_of(&wall, &wb).expect("a turned wall has an outline");
|
||||
assert!(
|
||||
hull_of(&rect_of(&far), &far).is_none(),
|
||||
"a box is just its box"
|
||||
);
|
||||
// both boxes overlap the wall's box; its outline puts the far one first, the near one last
|
||||
let o = sort_hulls(
|
||||
&[(near, s), (wb, s), (far, s)],
|
||||
&[None, Some(hw), None],
|
||||
false,
|
||||
);
|
||||
assert_eq!(o.order, vec![2, 1, 0]);
|
||||
assert_eq!(o.cycles, 0);
|
||||
// past the wall's end but behind its line: still drawn before it
|
||||
assert_eq!(
|
||||
sep_hull(&rect_of(&[10.5, -3.0, 11.0, -2.0, 0.0, 1.0]), &wall),
|
||||
-1
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -9,25 +9,44 @@ use crate::prim::{Fish, Prim, Style, Zs, P2, P3};
|
||||
use crate::proj::C;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Placement frame: translate by (x, y, z) after turning `q` quarter turns
|
||||
/// counter-clockwise (seen from above).
|
||||
/// Placement frame: translate by (x, y, z) after turning counter-clockwise
|
||||
/// (seen from above): `q` quarter turns, or by any angle when `cs` holds its
|
||||
/// cosine and sine (diagonal streets and walls). Quarter turns stay exact.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Xf {
|
||||
pub x: f64,
|
||||
pub y: f64,
|
||||
pub z: f64,
|
||||
pub q: u8,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub cs: Option<[f64; 2]>,
|
||||
}
|
||||
|
||||
impl Xf {
|
||||
/// Frame at (x, y, z) turned by `deg` (rounded to a multiple of 90).
|
||||
/// Frame at (x, y, z) turned by `deg`; within 0.001 degrees of a
|
||||
/// multiple of 90 it is that quarter turn exactly.
|
||||
pub fn new(x: f64, y: f64, z: f64, deg: f64) -> Self {
|
||||
let q = ((deg / 90.0).round() as i64).rem_euclid(4) as u8;
|
||||
Xf { x, y, z, q }
|
||||
let qf = (deg / 90.0).round();
|
||||
let q = (qf as i64).rem_euclid(4) as u8;
|
||||
let cs = if (deg - qf * 90.0).abs() < 1e-3 || !deg.is_finite() {
|
||||
None
|
||||
} else {
|
||||
let a = deg.to_radians();
|
||||
Some([a.cos(), a.sin()])
|
||||
};
|
||||
Xf { x, y, z, q, cs }
|
||||
}
|
||||
/// Does the frame keep to the world axes (a multiple of 90 degrees)?
|
||||
#[inline]
|
||||
pub fn axis(&self) -> bool {
|
||||
self.cs.is_none()
|
||||
}
|
||||
/// Turn a plan vector into the world.
|
||||
#[inline]
|
||||
pub fn v(&self, x: f64, y: f64) -> P2 {
|
||||
if let Some([c, s]) = self.cs {
|
||||
return [c * x - s * y, s * x + c * y];
|
||||
}
|
||||
match self.q {
|
||||
0 => [x, y],
|
||||
1 => [-y, x],
|
||||
@@ -38,6 +57,9 @@ impl Xf {
|
||||
/// World plan vector back into the frame.
|
||||
#[inline]
|
||||
pub fn v_inv(&self, x: f64, y: f64) -> P2 {
|
||||
if let Some([c, s]) = self.cs {
|
||||
return [c * x + s * y, -s * x + c * y];
|
||||
}
|
||||
match self.q {
|
||||
0 => [x, y],
|
||||
1 => [y, -x],
|
||||
@@ -59,8 +81,19 @@ impl Xf {
|
||||
pub fn z(&self, z: f64) -> f64 {
|
||||
z + self.z
|
||||
}
|
||||
/// An axis-aligned rectangle in the frame as a world rectangle.
|
||||
/// An axis-aligned rectangle in the frame as a world rectangle (its
|
||||
/// bounding rectangle when the frame is turned off the axes).
|
||||
pub fn rect(&self, x0: f64, y0: f64, x1: f64, y1: f64) -> [f64; 4] {
|
||||
if !self.axis() {
|
||||
let p = self.pts(&rect(x0, y0, x1, y1));
|
||||
let f = |k: usize, m: fn(f64, f64) -> f64| p.iter().map(|q| q[k]).fold(p[0][k], m);
|
||||
return [
|
||||
f(0, f64::min),
|
||||
f(1, f64::min),
|
||||
f(0, f64::max),
|
||||
f(1, f64::max),
|
||||
];
|
||||
}
|
||||
let a = self.p(x0, y0);
|
||||
let b = self.p(x1, y1);
|
||||
[
|
||||
@@ -188,9 +221,16 @@ impl Sprite {
|
||||
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]);
|
||||
let pts = if self.xf.axis() {
|
||||
let r = self.xf.rect(x0, y0, x1, y1);
|
||||
rect(r[0], r[1], r[2], r[3])
|
||||
} else {
|
||||
// a turned frame: the box's own corners, counter-clockwise
|
||||
let (a, b) = (x0.min(x1), x0.max(x1));
|
||||
let (c, d) = (y0.min(y1), y0.max(y1));
|
||||
self.xf.pts(&rect(a, c, b, d))
|
||||
};
|
||||
self.ext_world(&pts, z0);
|
||||
self.ext_world(&pts, z1);
|
||||
self.push(Prim::Prism {
|
||||
@@ -686,9 +726,34 @@ mod tests {
|
||||
assert_eq!(b, [9.0, 18.0, 1.0, 11.0, 22.0, 4.0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn frames_turn_by_any_angle() {
|
||||
let f = Xf::new(1.0, 2.0, 0.0, 45.0);
|
||||
assert!(!f.axis() && Xf::new(0.0, 0.0, 0.0, 90.0000001).axis());
|
||||
let w = f.p(2f64.sqrt(), 0.0);
|
||||
assert!(
|
||||
(w[0] - 2.0).abs() < 1e-12 && (w[1] - 3.0).abs() < 1e-12,
|
||||
"{w:?}"
|
||||
);
|
||||
// a box turned 45 degrees keeps its own four corners
|
||||
let mut s = Sprite::new("a", "").with_frame(Xf::new(0.0, 0.0, 0.0, 45.0));
|
||||
s.cuboid(-1.0, -0.1, 1.0, 0.1, 0.0, 1.0, Style::mat("x"));
|
||||
match &s.prims[0] {
|
||||
Prim::Prism { pts, .. } => {
|
||||
assert_eq!(pts.len(), 4);
|
||||
let r = 0.9 * 0.5f64.sqrt();
|
||||
assert!(
|
||||
(pts[0][0] + r).abs() < 1e-9 && (pts[0][1] + 1.1 * 0.5f64.sqrt()).abs() < 1e-9,
|
||||
"{pts:?}"
|
||||
);
|
||||
}
|
||||
p => panic!("{p:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn frame_round_trips_points() {
|
||||
for deg in [0.0, 90.0, 180.0, 270.0, -90.0, 450.0] {
|
||||
for deg in [0.0, 90.0, 180.0, 270.0, -90.0, 450.0, 30.0, -137.5] {
|
||||
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]);
|
||||
|
||||
Reference in new issue
Block a user