Multi-select, select joined walls and fences, pond with swimming fish
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:
grabowski committed 2026-10-24 18:36:39 +07:00
1 parent e4017ce2a8
commit 19f4c83ae8
21 files changed
+759 -59

No files matched your search

+134
View File
@@ -49,3 +49,137 @@ fn prefab_hedge(o) {
let p = o.p;
sprite().box(-p.w / 2.0, -p.d / 2.0, p.w / 2.0, p.d / 2.0, 0.0, p.h, "hedge");
}
// ---------------------------------------------------------------- pond
fn meta_pond() {
#{ label: "Pond", category: "Nature", icon: "◍", shape: "rect",
help: "A garden pond with fish swimming round under lily pads, reeds at the back and a stone or grass edge. Drag a rectangle on the map.",
props: [
#{ key: "w", type: "float", def: 6.0, min: 1.5, max: 60.0, step: 0.5, label: "Width (m)" },
#{ key: "d", type: "float", def: 4.0, min: 1.5, max: 60.0, step: 0.5, label: "Length (m)" },
#{ key: "shape", type: "choice", def: "natural", choices: ["natural", "round", "square"], label: "Shape" },
#{ key: "fish", type: "int", def: 5, min: 0, max: 16, step: 1, label: "Fish" },
#{ key: "lilies", type: "int", def: 3, min: 0, max: 12, step: 1, label: "Lily pads" },
#{ key: "reeds", type: "bool", def: true, label: "Reeds" },
#{ key: "edge", type: "choice", def: "stone", choices: ["stone", "grass"], label: "Edge" },
] }
}
/// The pond's outline inset by `ins` metres: a kidney ("natural"), an
/// ellipse ("round") or a rectangle with round corners ("square").
fn pond_outline(p, ins) {
let a = p.w / 2.0 - ins;
let b = p.d / 2.0 - ins;
let pts = [];
if p.shape == "square" {
let r = fmax(0.05, fmin(a, b) * 0.35);
for k in 0..4 {
let cx = if k == 0 || k == 3 { a - r } else { -a + r };
let cy = if k < 2 { b - r } else { -b + r };
for j in 0..7 {
let t = (k * 90 + j * 15) * PI() / 180.0;
pts.push([cx + r * t.cos(), cy + r * t.sin()]);
}
}
return pts;
}
for j in 0..48 {
let t = j * PI() / 24.0;
let k = 1.0;
if p.shape == "natural" {
// a dent at the back (+y) and a slight wobble, never past the box
let dt = (t - PI() / 2.0) / 0.6;
k = (1.0 - 0.3 * (-dt * dt).exp()) * (0.965 + 0.035 * (3.0 * t + 1.0).sin());
}
pts.push([a * k * t.cos(), b * k * t.sin()]);
}
pts
}
fn prefab_pond(o) {
let p = o.p;
let a = p.w / 2.0;
let b = p.d / 2.0;
let rim = if p.edge == "stone" { fmin(0.35, fmin(a, b) * 0.18) } else { fmin(0.2, fmin(a, b) * 0.1) };
layer("ground");
let s = sprite();
let outer = pond_outline(p, 0.0);
let inner = pond_outline(p, rim);
if p.edge == "stone" {
s.flat([outer], 0.03, "capst t");
// joints across the stone rim
let segs = [];
let step = if outer.len() > 30 { 2 } else { 1 };
for j in range(0, outer.len(), step) { segs.push([outer[j][0], outer[j][1], inner[j][0], inner[j][1]]); }
s.lines(segs, 0.031, "seam");
} else {
s.flat([outer], 0.02, "pondbank t");
}
s.flat([inner], 0.032, "pondw t");
s.flat([pond_outline(p, rim + fmin(a, b) * 0.28)], 0.033, "ponddp t");
// the water the fish keep to: an ellipse well inside, off the natural shape's dent
let wa = (a - rim) * 0.72;
let wb = (b - rim) * 0.72;
let oy = if p.shape == "natural" { -0.12 * b } else { 0.0 };
if p.shape == "natural" { wb *= 0.82; }
let n = p.fish;
for i in 0..n {
let f = 0.45 + 0.55 * ((i * 37 + 11) % 10) / 10.0;
let rx = wa * f;
let ry = wb * f;
let cx = 0.0;
// long ponds: smaller rounds spread along the long side
if rx > ry * 1.6 { cx = (((i * 53) % 9) / 8.0 - 0.5) * (rx - ry * 1.6) * 2.0; rx = ry * 1.6; }
if ry > rx * 1.6 { ry = rx * 1.6; }
let len = fmin(0.9, fmax(0.3, fmin(a, b) * 0.24)) * (0.85 + 0.1 * (i % 3));
let dur = fmax(8.0, 2.0 * PI() * (rx + ry) / 2.0 / 0.32);
s.swim(cx, oy, 0.036, [#{ rx: rx, ry: ry, dur: dur, delay: (i * 7.3) % dur, cw: i % 3 == 1, len: len,
cls: ["koi", "koiw", "koig"][i % 3] }]);
}
// lily pads over the fish, some in flower; rings spreading where a fish came up
for i in 0..p.lilies {
let t = i * 2.4 + 0.7;
let q = 0.35 + 0.4 * ((i * 29 + 3) % 10) / 10.0;
let x = wa * q * t.cos();
let y = oy + wb * q * t.sin();
s.disc(x, y, 0.04, fmin(0.4, fmax(0.18, fmin(a, b) * 0.1)), "lily t");
if i % 2 == 0 { s.disc(x + 0.08, y + 0.05, 0.05, 0.07, "lilyf t"); }
}
if n > 0 {
for i in 0..fmin(3, n).to_int() {
let t = i * 2.1 + 1.9;
s.begin("a rip", `animation-delay:-${i * 1.7}s`);
s.disc(wa * 0.5 * t.cos(), oy + wb * 0.5 * t.sin(), 0.04, fmin(0.6, fmin(a, b) * 0.15), "ripple");
s.end();
}
}
// reeds in clumps on the back edge, upright, so with the other sprites
if p.reeds {
layer("main");
for t in [1.15, 1.95, 2.55] {
let q = pond_outline(p, rim * 0.5 + 0.3);
let j = ((t / (2.0 * PI())) * q.len()).round().to_int() % q.len();
let c = q[j];
// tapered blades leaning out from the clump, cattail heads on some
let blades = [];
let heads = [];
for k in 0..7 {
let x = c[0] + ((k * 37) % 7 - 3) * 0.04;
let y = c[1] + ((k * 53) % 5 - 2) * 0.04;
let h = 0.75 + ((k * 31) % 6) * 0.09;
let lx = ((k * 17) % 7 - 3) * 0.06;
let ly = ((k * 23) % 5 - 2) * 0.06;
blades.push([[x - 0.06, y, 0.03], [x + 0.06, y, 0.03], [x + lx, y + ly, h]]);
blades.push([[x, y - 0.06, 0.03], [x, y + 0.06, 0.03], [x + lx, y + ly, h]]);
if k % 3 == 0 {
let f = 0.62;
let hx = x + lx * f;
let hy = y + ly * f;
heads.push([[hx - 0.035, hy, h * f], [hx + 0.035, hy, h * f], [hx + 0.035 + lx * 0.18, hy + ly * 0.18, h * (f + 0.18)], [hx - 0.035 + lx * 0.18, hy + ly * 0.18, h * (f + 0.18)]]);
}
}
sprite().poly(blades, "reed t").poly(heads, "cattail t");
}
}
}