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

+9 -7
View File
@@ -12,7 +12,7 @@
* { html, info, bbox: [x, y, w, h], focus: [x, y], north, boxes, warnings }
*
* view.update(cfg) view.refresh() view.setLevel(id) view.setRotation(r) view.rotate(d)
* view.select(id) view.selectObj(id) view.focus(id | [x, y, z], zoom) view.fit()
* view.select(id) view.selectObj(id | [ids]) view.focus(id | [x, y, z], zoom) view.fit()
* view.toWorld(clientX, clientY, z) -> [x, y] view.ready() -> Promise
* view.info(id) view.stats() view.fullscreen(on) view.destroy()
*/
@@ -258,21 +258,23 @@
if (!id) return;
Array.prototype.forEach.call(q("data-id", id), function (e) { if (!e.classList.contains("iso-tag")) e.classList.add("iso-sel"); });
}
// outline an object, or several (an array of ids)
function selectObj(id) {
Array.prototype.forEach.call(svg.querySelectorAll(".iso-osel"), function (e) { e.classList.remove("iso-osel"); });
st.selObj = id || null;
if (id) Array.prototype.forEach.call(q("data-obj", id), function (e) { e.classList.add("iso-osel"); });
var ids = Array.isArray(id) ? id : id ? [id] : [];
st.selObj = ids.length ? id : null;
ids.forEach(function (i) { Array.prototype.forEach.call(q("data-obj", i), function (e) { e.classList.add("iso-osel"); }); });
}
function targetId(t) { var e = t && t.closest ? t.closest("[data-id]") : null; return e && svg.contains(e) ? e.getAttribute("data-id") : null; }
function targetObj(t) { var e = t && t.closest ? t.closest("[data-obj]") : null; return e && svg.contains(e) ? e.getAttribute("data-obj") : null; }
function targetSrc(t) { var e = t && t.closest ? t.closest("[data-src]") : null; return e && svg.contains(e) ? e.getAttribute("data-src") : null; }
function pick(t, cx, cy) {
function pick(t, cx, cy, ev) {
var id = targetId(t), obj = targetObj(t);
if (!id && !obj) { select(null); hideTip(); call(opts.onPick, null); return; }
if (!id && !obj) { select(null); hideTip(); call(opts.onPick, null, ev); return; }
var info = infoOf(id || obj);
if (!opts.noSelect) select(id);
if (!opts.noTip) showTip(info, cx, cy);
call(opts.onPick, { id: info.id || id, name: info.name, detail: info.detail, rows: info.rows || [], obj: obj, src: targetSrc(t) });
call(opts.onPick, { id: info.id || id, name: info.name, detail: info.detail, rows: info.rows || [], obj: obj, src: targetSrc(t) }, ev);
}
// client point -> world plan at height z
function toWorld(cx, cy, z) {
@@ -340,7 +342,7 @@
if (npt() < 2) pinch = null;
if (wasDown) {
if (claimed) call(opts.interact.up, e, { moved: moved, world: toWorld(e.clientX, e.clientY, 0), cancel: e.type !== "pointerup" });
else if (!moved && !down.cam && e.type === "pointerup") pick(e.target, e.clientX, e.clientY);
else if (!moved && !down.cam && e.type === "pointerup") pick(e.target, e.clientX, e.clientY, e);
}
if (npt() === 0) { down = null; claimed = false; wrap.classList.remove("iso-dragging"); }
}