Multi-select, select joined walls and fences, pond with swimming fish
Deploy web Studio / deploy (push) Successful in 1m27s
Deploy web Studio / deploy (push) Successful in 1m27s
Selection: S.sel stays the object picked last; S.selIds holds everything selected. - In the object list, Ctrl+click adds or removes an object and Shift+click selects a range. - On the map, Ctrl/Shift+click toggles an object and Shift+drag draws a box that selects what lies wholly inside it (locked and hidden objects are left out). - Ctrl+A selects everything that is not locked; Esc selects nothing. - Clicking a wall or fence a second time selects every piece of the same type joined to it; another click goes back to that one piece. - Dragging one selected object moves them all, together with the windows and doors on them, as one undo step. Arrow keys, R (turns the group about its middle), Ctrl+D, Delete, Lock and Hide all work on the whole selection. - When all selected objects share a type, the property form edits them all; fields where they differ show "mixed". - The agent's system prompt lists the selected objects. The viewer outlines several objects with selectObj([ids]) and passes the click event to onPick. A batch op now answers `ids`, one per operation, so the copies from a group duplicate get selected. Pond (Nature): natural, round or square, with a stone or grass edge, fish, lily pads, spreading ripples and reed clumps. The fish come from a new Swim primitive (script: s.swim). One matrix maps the object's frame to the screen. Each fish turns round its ellipse with a CSS rotation, so off-screen pausing and reduced motion apply as for the other animations. The house demo gets a koi pond in the back garden. Tests: ponds in every shape and rotation have the right number of fish, no NaN and no depth-sort cycles, and stay inside their rectangle. check_studio covers list and map multi-select, a group drag undone in one step, joined-fence selection, a mixed-value form applied to all, box select, delete and undo, Ctrl+A, Esc, and a new pond's fish.
This commit is contained in:
1 parent
e4017ce2a8
commit
19f4c83ae8
21 files changed
+759
-59
No files matched your search
@@ -28,6 +28,17 @@ pub fn r1s(v: f64) -> String {
|
||||
s
|
||||
}
|
||||
|
||||
/// `v` rounded to 0.001, without trailing zeros or `-0` (`NaN` when not finite):
|
||||
/// for small sizes in a local frame (fish, scaled groups).
|
||||
pub fn r3s(v: f64) -> String {
|
||||
if !v.is_finite() {
|
||||
return "NaN".into();
|
||||
}
|
||||
let t = (v * 1000.0).round() / 1000.0;
|
||||
let s = format!("{:.3}", if t == 0.0 { 0.0 } else { t });
|
||||
s.trim_end_matches('0').trim_end_matches('.').to_string()
|
||||
}
|
||||
|
||||
/// Round to 0.1 as a number.
|
||||
pub fn round1(v: f64) -> f64 {
|
||||
(v * 10.0 + 0.5).floor() / 10.0
|
||||
|
||||
@@ -23,7 +23,7 @@ pub mod scene;
|
||||
pub mod sort;
|
||||
pub mod sprite;
|
||||
|
||||
pub use prim::{Prim, Style, Zs, P2, P3};
|
||||
pub use prim::{Fish, Prim, Style, Zs, P2, P3};
|
||||
pub use proj::{shade, Proj, C, CZ};
|
||||
pub use render::{render, RenderOpts, Rendered};
|
||||
pub use scene::{Info, Layer, LayerOpts, Scene, Tag};
|
||||
|
||||
@@ -179,6 +179,15 @@ pub enum Prim {
|
||||
},
|
||||
/// Spinning fan lying in the plane at height at.z (CSS animation).
|
||||
Fan { at: P3, r: f64 },
|
||||
/// Fish swimming round ellipses on the plane at height z (CSS
|
||||
/// animation): centre c, the ellipses' x axis along u (a world unit
|
||||
/// vector, the object's local x).
|
||||
Swim {
|
||||
c: P2,
|
||||
z: f64,
|
||||
u: P2,
|
||||
fish: Vec<Fish>,
|
||||
},
|
||||
/// 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 },
|
||||
@@ -191,3 +200,17 @@ pub enum Prim {
|
||||
/// Items drawn back to front by the depth of their plan anchor.
|
||||
Seq { items: Vec<(P2, Vec<Prim>)> },
|
||||
}
|
||||
|
||||
/// One fish of a [`Prim::Swim`]: it goes round an ellipse of radii (rx, ry)
|
||||
/// once every `dur` seconds, `delay` seconds into its round at the start,
|
||||
/// counter-clockwise (seen from above) unless `cw`; `len` is its length.
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Fish {
|
||||
pub rx: f64,
|
||||
pub ry: f64,
|
||||
pub dur: f64,
|
||||
pub delay: f64,
|
||||
pub cw: bool,
|
||||
pub len: f64,
|
||||
pub cls: String,
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Projection of a scene to SVG for one view rotation.
|
||||
|
||||
use crate::fmt::{esc, hash, r1, r1s, round1};
|
||||
use crate::fmt::{esc, hash, r1, r1s, r3s, round1};
|
||||
use crate::prim::{Prim, Style, P2};
|
||||
use crate::proj::{shade, Proj, C};
|
||||
use crate::scene::{Info, Scene};
|
||||
@@ -279,6 +279,56 @@ impl Ctx<'_> {
|
||||
self.out.push_str("\"/>");
|
||||
}
|
||||
|
||||
/// Fish in the plane at height z: one matrix maps the object's frame
|
||||
/// (metres, x along u) to the screen; each fish sits at (1, 0) of a
|
||||
/// unit circle that a CSS rotation turns and a scale stretches to its
|
||||
/// ellipse. An invisible diagonal centres the turning group's box on
|
||||
/// the circle's middle (`transform-box: fill-box`, as for the fan).
|
||||
fn swim(&mut self, c: P2, z: f64, u: P2, fish: &[crate::prim::Fish]) {
|
||||
if fish.is_empty() {
|
||||
return;
|
||||
}
|
||||
let p = self.p;
|
||||
let o = p.s(c[0], c[1], z);
|
||||
let a = p.s(c[0] + u[0], c[1] + u[1], z);
|
||||
let b = p.s(c[0] - u[1], c[1] + u[0], z);
|
||||
let _ = write!(
|
||||
self.out,
|
||||
"<g class=\"iso-school\" transform=\"matrix({} {} {} {} {} {})\">",
|
||||
r3s(a[0] - o[0]),
|
||||
r3s(a[1] - o[1]),
|
||||
r3s(b[0] - o[0]),
|
||||
r3s(b[1] - o[1]),
|
||||
r1s(o[0]),
|
||||
r1s(o[1])
|
||||
);
|
||||
for f in fish {
|
||||
let (rx, ry, l) = (f.rx.max(0.05), f.ry.max(0.05), f.len.max(0.05));
|
||||
let e = 1.0 + l / rx.min(ry) + 0.05;
|
||||
// a fish nose first along +y (counter-clockwise) or -y
|
||||
let k = if f.cw { -l } else { l };
|
||||
// body widest behind the head, a narrow tail root, a forked tail fin
|
||||
let (b, t) = (body_path(k, l), tail_path(k, l));
|
||||
let _ = write!(
|
||||
self.out,
|
||||
"<g transform=\"scale({} {})\"><g class=\"iso-a iso-swim\" style=\"animation-duration:{}s;animation-delay:-{}s{}\"><path class=\"iso-swimbox\" d=\"M{} {}L{} {}\"/><g transform=\"translate(1 0) scale({} {})\"><path class=\"iso-fish iso-{}\" d=\"{b}{t}\"/></g></g></g>",
|
||||
r3s(rx),
|
||||
r3s(ry),
|
||||
r3s(f.dur.max(1.0)),
|
||||
r3s(f.delay.max(0.0)),
|
||||
if f.cw { ";animation-direction:reverse" } else { "" },
|
||||
r3s(-e),
|
||||
r3s(-e),
|
||||
r3s(e),
|
||||
r3s(e),
|
||||
r3s(1.0 / rx),
|
||||
r3s(1.0 / ry),
|
||||
esc(&f.cls),
|
||||
);
|
||||
}
|
||||
self.out.push_str("</g>");
|
||||
}
|
||||
|
||||
fn disc(&mut self, c: P2, z: f64, r: f64, cls: &str, id: Option<&str>) {
|
||||
let s = self.p.s(c[0], c[1], z);
|
||||
let rx = r * C * SQ2;
|
||||
@@ -461,6 +511,7 @@ impl Ctx<'_> {
|
||||
-a
|
||||
);
|
||||
}
|
||||
Prim::Swim { c, z, u, fish } => self.swim(*c, *z, *u, fish),
|
||||
Prim::Raw { at, svg } => {
|
||||
let svg = svg.replace("{uid}", self.uid);
|
||||
match at {
|
||||
@@ -696,6 +747,40 @@ pub fn render(scene: &Scene, rot: u8, opts: &RenderOpts) -> Rendered {
|
||||
}
|
||||
}
|
||||
|
||||
/// A fish body nose first along +y (k = length, negative: along -y), l wide.
|
||||
fn body_path(k: f64, l: f64) -> String {
|
||||
format!(
|
||||
"M0 {}C{} {} {} {} {} {}L{} {}C{} {} {} {} 0 {}Z",
|
||||
r3s(0.5 * k),
|
||||
r3s(0.24 * l),
|
||||
r3s(0.38 * k),
|
||||
r3s(0.24 * l),
|
||||
r3s(-0.1 * k),
|
||||
r3s(0.05 * l),
|
||||
r3s(-0.28 * k),
|
||||
r3s(-0.05 * l),
|
||||
r3s(-0.28 * k),
|
||||
r3s(-0.24 * l),
|
||||
r3s(-0.1 * k),
|
||||
r3s(-0.24 * l),
|
||||
r3s(0.38 * k),
|
||||
r3s(0.5 * k)
|
||||
)
|
||||
}
|
||||
|
||||
/// The forked tail fin behind [`body_path`].
|
||||
fn tail_path(k: f64, l: f64) -> String {
|
||||
format!(
|
||||
"M0 {}L{} {}L0 {}L{} {}Z",
|
||||
r3s(-0.24 * k),
|
||||
r3s(0.2 * l),
|
||||
r3s(-0.54 * k),
|
||||
r3s(-0.42 * k),
|
||||
r3s(-0.2 * l),
|
||||
r3s(-0.54 * k)
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
//! an object placed at (x, y, z) and turned in 90 degree steps draws around
|
||||
//! its own origin, and the sprite stores world coordinates.
|
||||
|
||||
use crate::prim::{Prim, Style, Zs, P2, P3};
|
||||
use crate::prim::{Fish, Prim, Style, Zs, P2, P3};
|
||||
use crate::proj::C;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@@ -476,6 +476,18 @@ impl Sprite {
|
||||
self.push(Prim::Fan { at, r })
|
||||
}
|
||||
|
||||
/// Fish swimming round ellipses about (cx, cy) on the plane at height z;
|
||||
/// the ellipses follow the frame's axes.
|
||||
pub fn swim(&mut self, cx: f64, cy: f64, z: f64, fish: Vec<Fish>) -> &mut Self {
|
||||
let rx = fish.iter().map(|f| f.rx + f.len).fold(0.0, f64::max);
|
||||
let ry = fish.iter().map(|f| f.ry + f.len).fold(0.0, f64::max);
|
||||
self.ext_box(cx - rx, cy - ry, cx + rx, cy + ry, z, z);
|
||||
let c = self.xf.p(cx, cy);
|
||||
let z = self.xf.z(z);
|
||||
let u = self.xf.v(1.0, 0.0);
|
||||
self.push(Prim::Swim { c, z, u, fish })
|
||||
}
|
||||
|
||||
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 {
|
||||
|
||||
Reference in new issue
Block a user