Object tree: groups and the drawing order
Deploy web Studio / deploy (push) Successful in 1m5s

Groups: objects of type "group" draw nothing; members carry the group's id
in a new `group` field (groups nest). A hidden group hides everything in it
(Doc::hidden_in_groups, skipped by the driver), a locked one locks it in
the Studio. Removing a group moves its members up into its parent group.
isokit check warns about members of missing groups. New `order` op: the
whole object order at once (each id once), one undo step.

Studio object list as a tree in the document order, front first: each
group sits just after its members, so it draws as one block, and ground
pieces higher in the list lie on top. Groups fold open and shut; every row
has an eye and a lock (shown on hover or when set); a click on a group
selects all of it, so a drag on the map moves it all. Group / Ungroup
(Ctrl+G, Ctrl+Shift+G), Order up/down (Ctrl+] Ctrl+[, with Shift to the
front / back), drag rows above, below or into a group. Group panel: name,
zoom, ungroup, to front / back, lock, hide, delete with contents.

MCP: group field in the object schema, order op, guidance.

Tests: Rust 81 (ops: groups, order, removal; driver: nested hidden
groups), Studio 131, web 100.
This commit is contained in:
grabowski committed 2026-10-25 18:06:04 +07:00
1 parent 5ec164a633
commit 540b37020c
15 files changed
+573 -30

No files matched your search

+28
View File
@@ -62,6 +62,34 @@ pub struct Object {
pub hidden: bool,
#[serde(default, skip_serializing_if = "is_false")]
pub locked: bool,
/// Id of the group (an object of type "group") this object belongs to.
#[serde(default, skip_serializing_if = "String::is_empty")]
pub group: String,
}
/// The type of the objects that group others: they draw nothing; hiding or
/// locking one hides or locks everything in it.
pub const GROUP: &str = "group";
impl Doc {
/// Whether an object is hidden, by itself or by a group it is in (groups can nest).
pub fn hidden_in_groups(&self, o: &Object) -> bool {
let mut cur = o;
for _ in 0..64 {
if cur.hidden {
return true;
}
match self
.objects
.iter()
.find(|g| g.kind == GROUP && g.id == cur.group)
{
Some(g) if !cur.group.is_empty() => cur = g,
_ => return false,
}
}
false
}
}
/// One editable property of a prefab.
+7 -1
View File
@@ -488,6 +488,10 @@ impl Runtime {
.try_cast::<Map>()
.unwrap_or_default();
for o in &doc.objects {
// groups draw nothing; a hidden group hides what is in it
if o.kind == doc::GROUP {
continue;
}
let mut j = serde_json::to_value(o).unwrap_or_default();
let meta = b.catalog.get(&o.kind);
let props = meta
@@ -510,7 +514,9 @@ impl Runtime {
}
let d = dyn_json(&j);
b.all_objects.push(d.clone());
if !o.hidden && (o.levels.is_empty() || o.levels.iter().any(|l| l == level)) {
if !doc.hidden_in_groups(o)
&& (o.levels.is_empty() || o.levels.iter().any(|l| l == level))
{
b.objects.push(d);
}
}
+27
View File
@@ -858,6 +858,33 @@ fn piece(id: &str, kind: &str, a: [f64; 2], b: [f64; 2], props: Value) -> Object
o
}
#[test]
fn hidden_groups_hide_what_is_in_them() {
let r = Runtime::new(&Bundle::default()).unwrap();
let mut g = obj("g", "group", 0.0, 0.0, json!({}));
g.hidden = true;
let mut inner = obj("inner", "group", 0.0, 0.0, json!({}));
inner.group = "g".into();
let mut t1 = obj("t1", "tree", 0.0, 0.0, json!({}));
t1.group = "inner".into();
let mut t2 = obj("t2", "tree", 5.0, 0.0, json!({}));
t2.group = "g".into();
let doc = Doc {
objects: vec![t1, t2, inner, g, obj("t3", "tree", 10.0, 0.0, json!({}))],
..Default::default()
};
let b = r.build("site", 0, &cfg(), &doc).unwrap();
// groups draw nothing and warn about nothing
assert!(b.scene.warnings.is_empty(), "{:?}", b.scene.warnings);
assert_eq!(sprites_of(&b, "g") + sprites_of(&b, "inner"), 0);
assert_eq!(sprites_of(&b, "t1") + sprites_of(&b, "t2"), 0);
assert_eq!(sprites_of(&b, "t3"), 1);
let mut shown = doc.clone();
shown.objects[3].hidden = false;
let b = r.build("site", 0, &cfg(), &shown).unwrap();
assert_eq!(sprites_of(&b, "t1") + sprites_of(&b, "t2"), 2);
}
/// The windows of every route mover piece of object `id`: (piece sprite, windows).
fn travel_windows(b: &crate::Built, id: &str) -> Vec<Vec<[f64; 2]>> {
fn walk(p: &isokit_core::Prim, out: &mut Vec<Vec<[f64; 2]>>) {