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:
1 parent
5ec164a633
commit
540b37020c
15 files changed
+573
-30
No files matched your search
+236
-17
@@ -26,8 +26,8 @@
|
||||
function snap(v) { var s = snapStep(); return s ? Math.round(v / s) * s : Math.round(v * 100) / 100; }
|
||||
function prefab(kind) { return (S.meta.catalog || []).filter(function (m) { return m.type === kind; })[0]; }
|
||||
function obj(id) { return (S.doc.objects || []).filter(function (o) { return o.id === id; })[0]; }
|
||||
function label(o) { var m = prefab(o.type); return o.name || (m ? m.label : o.type); }
|
||||
function icon(kind) { var m = prefab(kind); return (m && m.icon) || ICON_DEFAULT; }
|
||||
function label(o) { var m = prefab(o.type); return o.name || (o.type === "group" ? "Group" : m ? m.label : o.type); }
|
||||
function icon(kind) { var m = prefab(kind); return kind === "group" ? "▤" : (m && m.icon) || ICON_DEFAULT; }
|
||||
function r3(v) { return Math.round(v * 1000) / 1000; }
|
||||
|
||||
// ------------------------------------------------------------ storeys, frames, mounts
|
||||
@@ -407,7 +407,7 @@
|
||||
drag = { id: o.id, ox: o.x, oy: o.y, w0: hit.world, moved: false, els: null, group: set, carry: [] };
|
||||
return true;
|
||||
}
|
||||
if (!o || o.locked) return false;
|
||||
if (!o || lockedEff(o)) return false;
|
||||
var m = prefab(o.type), mount = !!(m && m.mount && m.mount.length), h = mount ? hostOf(o) : null;
|
||||
drag = { id: o.id, ox: o.x, oy: o.y, w0: hit.world, moved: false, els: null, m: m, mount: mount,
|
||||
rel: h ? qOf((o.rot || 0) - (h.host.rot || 0)) : null, carry: mountedOn(o) };
|
||||
@@ -600,6 +600,7 @@
|
||||
var ids = selIds();
|
||||
S.view.selectObj(ids.length > 1 ? ids : S.sel);
|
||||
Array.prototype.forEach.call(document.querySelectorAll("#objects li"), function (li) { li.setAttribute("aria-selected", String(ids.indexOf(li.getAttribute("data-id")) >= 0)); });
|
||||
otools();
|
||||
if (!quiet) renderRight();
|
||||
}
|
||||
function select(id, quiet) {
|
||||
@@ -645,7 +646,7 @@
|
||||
select(id);
|
||||
}
|
||||
function selectAll() {
|
||||
selectMany((S.doc.objects || []).filter(function (o) { return !o.locked && !o.hidden; }).map(function (o) { return o.id; }));
|
||||
selectMany((S.doc.objects || []).filter(function (o) { return !lockedEff(o) && !hiddenEff(o) && !isGroup(o); }).map(function (o) { return o.id; }));
|
||||
}
|
||||
// the selection box drawn with Shift / Ctrl + drag
|
||||
function band(r) {
|
||||
@@ -662,7 +663,7 @@
|
||||
if (!mv.moved) { if (d.hit) toggleSel(d.hit); return; }
|
||||
// every object drawn wholly inside the box (not locked or hidden)
|
||||
var r = bandRect(d, e), found = (S.doc.objects || []).filter(function (o) {
|
||||
if (o.locked || o.hidden) return false;
|
||||
if (lockedEff(o) || hiddenEff(o) || isGroup(o)) return false;
|
||||
var els = moveEls(o.id), u = null;
|
||||
Array.prototype.forEach.call(els, function (el) {
|
||||
var b = el.getBoundingClientRect();
|
||||
@@ -677,7 +678,7 @@
|
||||
}
|
||||
// what a group move takes along: the selected objects that are not locked, and what sits on them
|
||||
function moveSet(ids) {
|
||||
var objs = ids.map(obj).filter(function (o) { return o && !o.locked; }), seen = {}, carry = [];
|
||||
var objs = ids.map(obj).filter(function (o) { return o && !lockedEff(o) && !isGroup(o); }), seen = {}, carry = [];
|
||||
objs.forEach(function (o) { seen[o.id] = 1; });
|
||||
objs.forEach(function (o) { mountedOn(o).forEach(function (c) { if (!seen[c.id]) { seen[c.id] = 1; carry.push(c); } }); });
|
||||
return { objs: objs, carry: carry };
|
||||
@@ -708,6 +709,7 @@
|
||||
});
|
||||
}
|
||||
function duplicateMany(ids) {
|
||||
ids = ids.filter(function (id) { return !isGroup(obj(id)); });
|
||||
var dd = Math.max(snapStep(), 1) * 2, extra = extraOn(ids);
|
||||
opList(extra.concat(ids).map(function (id) { return { op: "duplicate", id: id, dx: dd, dy: dd }; })).then(function (r) {
|
||||
if (r && r.ids) selectMany(r.ids.slice(-ids.length));
|
||||
@@ -724,6 +726,127 @@
|
||||
var y0 = Math.min.apply(null, bs.map(function (b) { return b[1]; })), y1 = Math.max.apply(null, bs.map(function (b) { return b[4]; }));
|
||||
S.view.focus([(x0 + x1) / 2, (y0 + y1) / 2, 0], 2);
|
||||
}
|
||||
// ------------------------------------------------------------ groups and the drawing order
|
||||
// A group is an object of type "group" that draws nothing; what is in it carries its id in
|
||||
// `group` (groups nest). The object list is a tree built from the document order: a group's
|
||||
// members lie just before the group object, so a group draws as one block at its place, and
|
||||
// ground pieces (areas, beds, ponds, roads) later in the order lie on top. The list shows the
|
||||
// front first. Every change to the tree is one op batch: group changes and the whole order.
|
||||
var GROUP = "group", ICON_GROUP = "▤", openGroups = {}, closedGroups = {};
|
||||
function isGroup(o) { return !!o && o.type === GROUP; }
|
||||
function parentOf(o) { var g = o && o.group ? obj(o.group) : null; return isGroup(g) && g !== o ? g : null; }
|
||||
function ancestors(o) { var out = [], g = parentOf(o); while (g && out.length < 64 && out.indexOf(g) < 0) { out.push(g); g = parentOf(g); } return out; }
|
||||
function hiddenEff(o) { return !!o && (!!o.hidden || ancestors(o).some(function (g) { return g.hidden; })); }
|
||||
function lockedEff(o) { return !!o && (!!o.locked || ancestors(o).some(function (g) { return g.locked; })); }
|
||||
// what is in group gid: its direct members, or (deep) everything below it, groups included
|
||||
function membersOf(gid, deep) {
|
||||
var out = [], todo = [gid];
|
||||
while (todo.length) {
|
||||
var g = todo.shift();
|
||||
(S.doc.objects || []).forEach(function (o) {
|
||||
if (o.group === g && out.indexOf(o) < 0 && o.id !== gid) { out.push(o); if (deep && isGroup(o)) todo.push(o.id); }
|
||||
});
|
||||
}
|
||||
return out;
|
||||
}
|
||||
// the tree, back to front: [{ o, kids }]; a group cycle (only by hand-edited files) stays at the root
|
||||
function tree() {
|
||||
var byId = {}, roots = [];
|
||||
(S.doc.objects || []).forEach(function (o) { byId[o.id] = { o: o, kids: [] }; });
|
||||
(S.doc.objects || []).forEach(function (o) {
|
||||
var p = parentOf(o), cyc = p && ancestors(o).indexOf(o) >= 0;
|
||||
(p && !cyc && byId[p.id] ? byId[p.id].kids : roots).push(byId[o.id]);
|
||||
});
|
||||
return roots;
|
||||
}
|
||||
// the document order of a tree: each group after what is in it
|
||||
function flatten(nodes, out) { nodes.forEach(function (n) { flatten(n.kids, out); out.push(n.o.id); }); return out; }
|
||||
// where a node is: { node, list (its siblings), parent (group id or "") }
|
||||
function findNode(nodes, id, parent) {
|
||||
for (var i = 0; i < nodes.length; i++) {
|
||||
if (nodes[i].o.id === id) return { node: nodes[i], list: nodes, parent: parent || "" };
|
||||
var f = findNode(nodes[i].kids, id, nodes[i].o.id);
|
||||
if (f) return f;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
// the selected objects that are not inside another selected group
|
||||
function topSel(ids) {
|
||||
ids = (ids || selIds()).filter(function (id) { return obj(id); });
|
||||
return ids.filter(function (id) { return !ancestors(obj(id)).some(function (g) { return ids.indexOf(g.id) >= 0; }); });
|
||||
}
|
||||
function docIndex(id) { return (S.doc.objects || []).map(function (o) { return o.id; }).indexOf(id); }
|
||||
// send a changed tree: first `pre` (adds, removes), then group changes, then the order
|
||||
function commitTree(roots, changes, pre, msg) {
|
||||
var ops = (pre || []).concat(changes || []), ids = flatten(roots, []);
|
||||
var cur = (S.doc.objects || []).map(function (o) { return o.id; }).filter(function (id) { return ids.indexOf(id) >= 0 || (pre || []).every(function (p) { return p.op !== "remove" || p.id !== id; }); });
|
||||
if (ids.join("\n") !== cur.join("\n")) ops.push({ op: "order", ids: ids });
|
||||
if (!ops.length) return Promise.resolve(null);
|
||||
return op({ ops: ops }).then(function (r) { if (r && msg) toast(msg); return r; });
|
||||
}
|
||||
function groupSelection() {
|
||||
var ids = topSel();
|
||||
if (ids.length < 2) { toast("Select two or more objects to group them (Ctrl+click in the list or on the map)"); return; }
|
||||
var roots = tree(), locs = ids.map(function (id) { return findNode(roots, id); }).filter(Boolean);
|
||||
locs.sort(function (a, b) { return docIndex(a.node.o.id) - docIndex(b.node.o.id); });
|
||||
var front = locs[locs.length - 1], k = 1;
|
||||
while (obj("group-" + k)) k++;
|
||||
var gid = "group-" + k, n = (S.doc.objects || []).filter(isGroup).length + 1;
|
||||
var gnode = { o: { id: gid, type: GROUP, group: front.parent }, kids: [] };
|
||||
// the group takes the place of the front-most of them, in its parent
|
||||
front.list.splice(front.list.indexOf(front.node) + 1, 0, gnode);
|
||||
locs.forEach(function (l) { l.list.splice(l.list.indexOf(l.node), 1); gnode.kids.push(l.node); });
|
||||
var changes = locs.map(function (l) { return { op: "update", id: l.node.o.id, patch: { group: gid } }; });
|
||||
commitTree(roots, changes, [{ op: "add", type: GROUP, id: gid, name: "Group " + n, group: front.parent }], "Grouped " + locs.length + " · Ctrl+Shift+G ungroups").then(function (r) { if (r) selectGroup(gid); });
|
||||
}
|
||||
// the group the selection is or is in
|
||||
function selGroup() {
|
||||
var o = S.sel ? obj(S.sel) : null;
|
||||
return isGroup(o) ? o : parentOf(o);
|
||||
}
|
||||
function ungroup(g) {
|
||||
g = g || selGroup();
|
||||
if (!g) { toast("Select a group (or something in one) to ungroup it"); return; }
|
||||
var roots = tree(), loc = findNode(roots, g.id), kids = loc.node.kids;
|
||||
Array.prototype.splice.apply(loc.list, [loc.list.indexOf(loc.node), 1].concat(kids));
|
||||
// removing a group moves what was in it up into its parent (ops.rs)
|
||||
commitTree(roots, [], [{ op: "remove", id: g.id }], "Ungrouped " + label(g) + ": " + kids.length + " object" + (kids.length === 1 ? "" : "s") + " · Ctrl+Z groups them again").then(function (r) {
|
||||
if (r) selectMany(kids.map(function (n) { return n.o.id; }));
|
||||
});
|
||||
}
|
||||
function selectGroup(gid) { selectMany([gid].concat(membersOf(gid, true).map(function (o) { return o.id; })), gid); }
|
||||
// forward / backward one place among their siblings, or to the front / back of them
|
||||
function moveOrder(how, ids) {
|
||||
ids = topSel(ids);
|
||||
if (!ids.length) return;
|
||||
var roots = tree(), lists = [];
|
||||
ids.forEach(function (id) { var l = findNode(roots, id); if (l && lists.indexOf(l.list) < 0) lists.push(l.list); });
|
||||
lists.forEach(function (list) {
|
||||
var sel = list.filter(function (n) { return ids.indexOf(n.o.id) >= 0; }), rest = list.filter(function (n) { return ids.indexOf(n.o.id) < 0; }), i;
|
||||
if (how === "front" || how === "back") { list.length = 0; Array.prototype.push.apply(list, how === "front" ? rest.concat(sel) : sel.concat(rest)); return; }
|
||||
if (how === "forward") { for (i = list.length - 2; i >= 0; i--) if (sel.indexOf(list[i]) >= 0 && sel.indexOf(list[i + 1]) < 0) { var a = list[i]; list[i] = list[i + 1]; list[i + 1] = a; } }
|
||||
else { for (i = 1; i < list.length; i++) if (sel.indexOf(list[i]) >= 0 && sel.indexOf(list[i - 1]) < 0) { var b = list[i]; list[i] = list[i - 1]; list[i - 1] = b; } }
|
||||
});
|
||||
var words = { front: "to the front", back: "to the back", forward: "forward", backward: "backward" };
|
||||
commitTree(roots, [], [], null).then(function (r) { if (r) toast("Moved " + words[how] + " · higher in the list draws on top (ground pieces)"); });
|
||||
}
|
||||
// rows dropped above (in front of), below (behind) or into a group row
|
||||
function dropRows(ids, t) {
|
||||
ids = topSel(ids);
|
||||
var tgt = obj(t.id);
|
||||
if (!tgt || ids.indexOf(t.id) >= 0) return;
|
||||
if (ids.some(function (id) { return ancestors(tgt).some(function (g) { return g.id === id; }); })) { toast("A group cannot go into itself", true); return; }
|
||||
var roots = tree(), locs = ids.map(function (id) { return findNode(roots, id); }).filter(Boolean);
|
||||
locs.sort(function (a, b) { return docIndex(a.node.o.id) - docIndex(b.node.o.id); });
|
||||
var nodes = locs.map(function (l) { l.list.splice(l.list.indexOf(l.node), 1); return l.node; });
|
||||
var dest, at, parent;
|
||||
if (t.where === "into") { var g = findNode(roots, t.id); dest = g.node.kids; at = dest.length; parent = t.id; }
|
||||
else { var l = findNode(roots, t.id); dest = l.list; parent = l.parent; at = dest.indexOf(l.node) + (t.where === "above" ? 1 : 0); }
|
||||
Array.prototype.splice.apply(dest, [at, 0].concat(nodes));
|
||||
var changes = nodes.filter(function (n) { return (n.o.group || "") !== parent; }).map(function (n) { return { op: "update", id: n.o.id, patch: { group: parent } }; });
|
||||
commitTree(roots, changes, [], t.where === "into" ? "Moved into " + label(tgt) : null);
|
||||
}
|
||||
|
||||
function opList(ops, quiet) { return op(ops.length === 1 ? ops[0] : { ops: ops }, quiet); }
|
||||
// the object and what sits on it (a wall's windows and doors) as one undo step; the object last, so it stays selected
|
||||
function remove(id) {
|
||||
@@ -757,7 +880,7 @@
|
||||
}
|
||||
function nudge(id, dx, dy) {
|
||||
var o = obj(id), many = selIds().length > 1;
|
||||
if (!o || (o.locked && !many)) return;
|
||||
if (!o || (lockedEff(o) && !many)) return;
|
||||
var s = snapStep() || 0.5;
|
||||
// arrow keys follow the screen: map screen directions onto world axes for the current rotation
|
||||
var rot = S.view.getRotation();
|
||||
@@ -781,6 +904,10 @@
|
||||
if (ctrl && (k === "a" || k === "A")) { selectAll(); return true; }
|
||||
if (!S.sel) return false;
|
||||
var many = selIds().length > 1;
|
||||
// groups and the drawing order: Ctrl+G, Ctrl+Shift+G; Ctrl+] / Ctrl+[ forward / backward, with Shift to the front / back
|
||||
if (ctrl && (k === "g" || k === "G")) { if (e.shiftKey) ungroup(); else groupSelection(); return true; }
|
||||
if (ctrl && (k === "]" || k === "}")) { moveOrder(e.shiftKey ? "front" : "forward"); return true; }
|
||||
if (ctrl && (k === "[" || k === "{")) { moveOrder(e.shiftKey ? "back" : "backward"); return true; }
|
||||
if (k === "Delete" || k === "Backspace") { if (many) removeMany(selIds()); else remove(S.sel); return true; }
|
||||
if (ctrl && (k === "d" || k === "D")) { if (many) duplicateMany(selIds()); else duplicate(S.sel); return true; }
|
||||
if (!ctrl && (k === "r" || k === "R")) { if (many) turnGroup(selIds(), e.shiftKey ? -90 : 90); else turn(S.sel, e.shiftKey ? -90 : 90); return true; }
|
||||
@@ -815,14 +942,38 @@
|
||||
}).join("") + "</div></div>";
|
||||
}).join("") || '<div class="st-help">No object matches.</div>';
|
||||
}
|
||||
// the object tree, front first: groups fold open and shut, every row has a hide and a lock toggle
|
||||
function renderLists() {
|
||||
var q = $("osearch").value.trim().toLowerCase(), list = S.doc.objects || [];
|
||||
var q = $("osearch").value.trim().toLowerCase(), list = S.doc.objects || [], out = [];
|
||||
$("ocount").textContent = list.length;
|
||||
$("objects").innerHTML = list.filter(function (o) {
|
||||
return !q || (o.id + " " + label(o) + " " + o.type).toLowerCase().indexOf(q) >= 0;
|
||||
}).slice().reverse().map(function (o) {
|
||||
return '<li data-id="' + esc(o.id) + '" aria-selected="' + (S.selIds.indexOf(o.id) >= 0) + '"' + (o.hidden ? ' class="st-hidden"' : "") + ' title="' + esc(o.id) + '"><i>' + esc(icon(o.type)) + "</i><span>" + esc(label(o)) + "</span><small>" + (o.locked ? "🔒" : "") + "</small></li>";
|
||||
}).join("");
|
||||
function hit(o) { return (o.id + " " + label(o) + " " + o.type).toLowerCase().indexOf(q) >= 0; }
|
||||
function shows(n) { return !q || hit(n.o) || n.kids.some(shows); }
|
||||
function walk(nodes, depth) {
|
||||
for (var i = nodes.length - 1; i >= 0; i--) {
|
||||
var n = nodes[i], o = n.o, g = isGroup(o);
|
||||
if (!shows(n)) continue;
|
||||
// groups start open; a filter opens every group that holds a match
|
||||
var open = g && (q ? true : !closedGroups[o.id]);
|
||||
var cls = (g ? "st-group" : "") + (o.hidden ? " st-hidden" : hiddenEff(o) ? " st-hidden st-inh" : "") + (lockedEff(o) ? " st-locked" : "");
|
||||
out.push('<li data-id="' + esc(o.id) + '" aria-selected="' + (S.selIds.indexOf(o.id) >= 0) + '" class="' + cls.trim() + '" style="--d:' + depth + '" title="' + esc(o.id) + '">' +
|
||||
(g ? '<b class="st-tw" data-tw title="' + (open ? "Fold" : "Unfold") + '">' + (open ? "▾" : "▸") + "</b>" : '<b class="st-tw"></b>') +
|
||||
"<i>" + esc(icon(o.type)) + "</i><span>" + esc(label(o)) + "</span>" +
|
||||
(g ? "<small>" + membersOf(o.id, true).filter(function (x) { return !isGroup(x); }).length + "</small>" : "") +
|
||||
'<button type="button" class="st-eye' + (o.hidden ? " st-on" : "") + '" data-eye title="' + (o.hidden ? "Show" : "Hide") + '">' + (o.hidden ? "◌" : "◉") + "</button>" +
|
||||
'<button type="button" class="st-lk' + (o.locked ? " st-on" : "") + '" data-lk title="' + (o.locked ? "Unlock" : "Lock") + '">' + (o.locked ? "🔒" : "🔓") + "</button></li>");
|
||||
if (g && open) walk(n.kids, depth + 1);
|
||||
}
|
||||
}
|
||||
walk(tree(), 0);
|
||||
$("objects").innerHTML = out.join("");
|
||||
otools();
|
||||
}
|
||||
// the list's tool strip follows the selection
|
||||
function otools() {
|
||||
var sel = selIds();
|
||||
$("ogroup").disabled = topSel(sel).length < 2;
|
||||
$("oungroup").disabled = !selGroup();
|
||||
$("ofwd").disabled = $("obwd").disabled = !sel.length;
|
||||
}
|
||||
// closed palette categories, remembered in this browser
|
||||
function paletteClosed() { try { return JSON.parse(localStorage.getItem("isokit-palette-closed") || "[]"); } catch (e) { return []; } }
|
||||
@@ -844,16 +995,59 @@
|
||||
$("osearch").addEventListener("input", renderLists);
|
||||
// Ctrl+click adds or takes out one, Shift+click selects the range from the last picked one
|
||||
$("objects").addEventListener("click", function (e) {
|
||||
if (listDragged) { listDragged = false; return; }
|
||||
var li = e.target.closest("li"); if (!li) return;
|
||||
var id = li.getAttribute("data-id");
|
||||
var id = li.getAttribute("data-id"), o = obj(id);
|
||||
if (!o) return;
|
||||
if (e.target.closest("[data-tw]")) { if (closedGroups[id]) delete closedGroups[id]; else closedGroups[id] = 1; renderLists(); return; }
|
||||
if (e.target.closest("[data-eye]")) { update(id, { hidden: !o.hidden }); return; }
|
||||
if (e.target.closest("[data-lk]")) { update(id, { locked: !o.locked }); return; }
|
||||
if (e.ctrlKey || e.metaKey) { toggleSel(id); return; }
|
||||
if (e.shiftKey && S.sel) {
|
||||
var all = Array.prototype.map.call(document.querySelectorAll("#objects li"), function (x) { return x.getAttribute("data-id"); }), a = all.indexOf(S.sel), b = all.indexOf(id);
|
||||
if (a >= 0 && b >= 0) { selectMany(all.slice(Math.min(a, b), Math.max(a, b) + 1), S.sel); return; }
|
||||
}
|
||||
select(id);
|
||||
// a group row selects the group and everything in it, so a drag on the map moves it all
|
||||
if (isGroup(o)) selectGroup(id); else select(id);
|
||||
});
|
||||
$("objects").addEventListener("dblclick", function (e) { var li = e.target.closest("li"); if (li) S.view.focus(li.getAttribute("data-id"), 3); });
|
||||
$("objects").addEventListener("dblclick", function (e) {
|
||||
var li = e.target.closest("li"); if (!li || e.target.closest("button,[data-tw]")) return;
|
||||
var id = li.getAttribute("data-id");
|
||||
if (isGroup(obj(id))) focusMany(membersOf(id, true).map(function (o) { return o.id; })); else S.view.focus(id, 3);
|
||||
});
|
||||
// rows dragged in the list: above or below another row (in front of or behind it), or into a
|
||||
// group (the middle of its row); the selected rows go together when the one dragged is selected
|
||||
var listDrag = null, listDragged = false;
|
||||
function clearDrop() { Array.prototype.forEach.call(document.querySelectorAll("#objects .st-drop-above, #objects .st-drop-below, #objects .st-drop-in"), function (x) { x.classList.remove("st-drop-above", "st-drop-below", "st-drop-in"); }); }
|
||||
$("objects").addEventListener("pointerdown", function (e) {
|
||||
var li = e.target.closest("li");
|
||||
if (e.button !== 0 || !li || e.target.closest("button,[data-tw]") || e.shiftKey || e.ctrlKey || e.metaKey) return;
|
||||
listDrag = { id: li.getAttribute("data-id"), y0: e.clientY, on: false, t: null };
|
||||
});
|
||||
document.addEventListener("pointermove", function (e) {
|
||||
var d = listDrag; if (!d) return;
|
||||
if (!d.on && Math.abs(e.clientY - d.y0) < 6) return;
|
||||
d.on = true; document.body.classList.add("st-listdrag");
|
||||
clearDrop();
|
||||
var el = document.elementFromPoint(e.clientX, e.clientY), li = el && el.closest ? el.closest("#objects li") : null;
|
||||
if (!li || li.getAttribute("data-id") === d.id) { d.t = null; return; }
|
||||
var r = li.getBoundingClientRect(), f = (e.clientY - r.top) / r.height, g = li.classList.contains("st-group");
|
||||
var where = g && f > 0.28 && f < 0.72 ? "into" : f < 0.5 ? "above" : "below";
|
||||
li.classList.add(where === "into" ? "st-drop-in" : "st-drop-" + where);
|
||||
d.t = { id: li.getAttribute("data-id"), where: where };
|
||||
});
|
||||
document.addEventListener("pointerup", function () {
|
||||
var d = listDrag; listDrag = null;
|
||||
if (!d) return;
|
||||
document.body.classList.remove("st-listdrag"); clearDrop();
|
||||
if (!d.on) return;
|
||||
listDragged = true; setTimeout(function () { listDragged = false; }, 0);
|
||||
if (d.t) dropRows(selIds().indexOf(d.id) >= 0 ? selIds() : [d.id], d.t);
|
||||
});
|
||||
$("ogroup").addEventListener("click", function () { groupSelection(); });
|
||||
$("oungroup").addEventListener("click", function () { ungroup(); });
|
||||
$("ofwd").addEventListener("click", function (e) { moveOrder(e.shiftKey ? "front" : "forward"); });
|
||||
$("obwd").addEventListener("click", function (e) { moveOrder(e.shiftKey ? "back" : "backward"); });
|
||||
|
||||
// ------------------------------------------------------------ right panel
|
||||
function field(key, lab, html, full) { return '<label for="f-' + key + '">' + esc(lab) + "</label>" + (full ? '<div class="st-full">' + html + "</div>" : html); }
|
||||
@@ -877,6 +1071,7 @@
|
||||
function renderRight() {
|
||||
var R = $("right"), o = S.sel ? obj(S.sel) : null, ids = selIds();
|
||||
if (!o) { renderProject(R); return; }
|
||||
if (isGroup(o)) { renderGroup(R, o); return; }
|
||||
if (ids.length > 1) { renderMany(R, ids); return; }
|
||||
var m = prefab(o.type) || { label: o.type, props: [] }, p = o.props || {};
|
||||
var levels = S.meta.levels || [];
|
||||
@@ -909,6 +1104,21 @@
|
||||
R.innerHTML = h;
|
||||
}
|
||||
// several objects: what they are, actions for all, and the properties when they are all one type
|
||||
function renderGroup(R, g) {
|
||||
var kids = membersOf(g.id, false), all = membersOf(g.id, true).filter(function (o) { return !isGroup(o); });
|
||||
R.innerHTML = "<h3><i>" + ICON_GROUP + "</i>" + esc(label(g)) + "</h3>" +
|
||||
'<div class="st-sub">Group · ' + kids.length + " inside, " + all.length + " object" + (all.length === 1 ? "" : "s") + " in all · id " + esc(g.id) + "</div>" +
|
||||
'<p class="st-help">Everything in the group is selected: drag one of them on the map to move them all, R turns them, arrow keys nudge. Hiding or locking the group hides or locks all of it. In the list, drag rows into or out of it; higher in the list draws on top (ground pieces).</p>' +
|
||||
'<div class="st-actions">' +
|
||||
'<button type="button" data-act="focus">Zoom to</button>' +
|
||||
'<button type="button" data-act="ungroup" title="Ctrl+Shift+G">Ungroup</button>' +
|
||||
'<button type="button" data-act="front" title="Ctrl+Shift+]">To front</button>' +
|
||||
'<button type="button" data-act="back" title="Ctrl+Shift+[">To back</button>' +
|
||||
'<button type="button" data-act="lock">' + (g.locked ? "Unlock" : "Lock") + "</button>" +
|
||||
'<button type="button" data-act="hide">' + (g.hidden ? "Show" : "Hide") + "</button>" +
|
||||
'<button type="button" data-act="del" class="st-danger">Delete with contents</button></div>' +
|
||||
'<h2>Group</h2><div class="st-form">' + field("name", "Name", '<input data-f="name" value="' + esc(g.name || "") + '" placeholder="Group">') + "</div>";
|
||||
}
|
||||
function renderMany(R, ids) {
|
||||
var os = ids.map(obj), count = {}, order = [];
|
||||
os.forEach(function (o) { if (!count[o.type]) { count[o.type] = 0; order.push(o.type); } count[o.type]++; });
|
||||
@@ -950,6 +1160,15 @@
|
||||
$("right").addEventListener("click", function (e) {
|
||||
var b = e.target.closest("button[data-act]"); if (!b || !S.sel) return;
|
||||
var a = b.getAttribute("data-act"), o = obj(S.sel), ids = selIds();
|
||||
if (isGroup(o)) {
|
||||
if (a === "focus") focusMany(membersOf(o.id, true).map(function (x) { return x.id; }));
|
||||
else if (a === "ungroup") ungroup(o);
|
||||
else if (a === "front" || a === "back") moveOrder(a, [o.id]);
|
||||
else if (a === "lock") update(o.id, { locked: !o.locked });
|
||||
else if (a === "hide") update(o.id, { hidden: !o.hidden });
|
||||
else if (a === "del") removeMany([o.id].concat(membersOf(o.id, true).map(function (x) { return x.id; })));
|
||||
return;
|
||||
}
|
||||
if (ids.length > 1) {
|
||||
if (a === "del") removeMany(ids);
|
||||
else if (a === "dup") duplicateMany(ids);
|
||||
@@ -962,7 +1181,7 @@
|
||||
if (a === "del") remove(S.sel);
|
||||
else if (a === "dup") duplicate(S.sel);
|
||||
else if (a === "focus") S.view.focus(S.sel, 3);
|
||||
else if (a === "front" || a === "back") op({ op: "reorder", id: S.sel, to: a });
|
||||
else if (a === "front" || a === "back") moveOrder(a, [S.sel]);
|
||||
else if (a === "lock") update(S.sel, { locked: !o.locked });
|
||||
else if (a === "hide") update(S.sel, { hidden: !o.hidden });
|
||||
else if (a === "rotl") turn(S.sel, -90);
|
||||
|
||||
Reference in new issue
Block a user