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