Point handles for routes and outlines; Area corners; bus goes round right
Deploy web Studio / deploy (push) Successful in 1m4s
Deploy web Studio / deploy (push) Successful in 1m4s
Studio: one selected object with pts (a route, an outlined area) shows a handle on each point and a + in the middle of each side: drag a point to move it, drag a + to add one there, double-click a point to remove it (open routes keep 2, outlines 3). Points snap to the grid and onto wall, fence and road ends, at any angle; one undo step each. Handles keep their screen size (new viewer callback onView on every redraw) and follow re-renders and selection. The viewer captures the pointer, so a double- click targets the drawing: the last point pressed is remembered instead. Prefab meta `outline`: the prefab draws pts as its outline. Area has it: Edit corners turns its rectangle into four points, Rectangle again removes them. Routes: where a path doubles back, the lane offset puts a point on each side, so it rounds into a U-turn instead of a diagonal across the lanes. The streets demo bus now runs east, three quarters round the roundabout (clockwise, left-hand traffic), down south street, turns and comes back by the short way, instead of going round the wrong way on its way out. Tests: Rust 81 (a doubling-back route: each mover once, no sort cycles), Studio 134 (corners, move, add, remove, undo), web 100.
This commit is contained in:
1 parent
540b37020c
commit
c8becc2e59
12 files changed
+189
-11
No files matched your search
@@ -185,6 +185,12 @@ body.st-listdrag, body.st-listdrag * { cursor: grabbing !important; }
|
||||
/* long object lists: rows out of sight are not laid out or painted */
|
||||
#objects li { content-visibility: auto; contain-intrinsic-size: auto 28px; }
|
||||
|
||||
/* point handles of the selected route or outline: points (drag, double-click removes), a + per side (drag adds) */
|
||||
.iso-svg .st-hline { fill: none; stroke: var(--iso-accent); stroke-width: 1.5; stroke-dasharray: 4 3; vector-effect: non-scaling-stroke; pointer-events: none; }
|
||||
.iso-svg .st-hpt { fill: #fff; stroke: var(--iso-accent); stroke-width: 2; vector-effect: non-scaling-stroke; cursor: grab; }
|
||||
.iso-svg .st-hpt:hover, .iso-svg .st-hpt.st-on { fill: var(--iso-accent); }
|
||||
.iso-svg .st-hmid { fill: var(--iso-accent); fill-opacity: .45; stroke: #fff; stroke-width: 1; vector-effect: non-scaling-stroke; cursor: copy; }
|
||||
.iso-svg .st-hmid:hover { fill-opacity: 1; }
|
||||
/* previews while placing: footprints, wall planes, where a window lands */
|
||||
.iso-svg .st-ghosts { pointer-events: none; }
|
||||
.iso-svg .st-ghost { fill: color-mix(in srgb, var(--iso-accent) 16%, transparent); stroke: var(--iso-accent); stroke-width: 1.5; stroke-dasharray: 5 3; vector-effect: non-scaling-stroke; stroke-linejoin: round; }
|
||||
|
||||
+100
-1
@@ -375,7 +375,9 @@
|
||||
$("stats").textContent = d.sprites + " sprites · " + d.nodes + " nodes · " + d.ms + " ms";
|
||||
var w = $("warnbtn"); w.hidden = !S.warnings.length; w.textContent = S.warnings.length + " warning" + (S.warnings.length === 1 ? "" : "s");
|
||||
var ids = selIds(); if (ids.length) S.view.selectObj(ids.length > 1 ? ids : ids[0]);
|
||||
drawHandles();
|
||||
},
|
||||
onView: function () { if (!ptDrag) drawHandles(); },
|
||||
// once per frame at most: pointers send moves faster than that; the x/y readout about ten
|
||||
// times a second (each new text costs a layout and a paint of the page)
|
||||
onHover: function (w, e) {
|
||||
@@ -396,6 +398,7 @@
|
||||
tool = { m: pm, z: z, a: a, b: null };
|
||||
return true;
|
||||
}
|
||||
if (ptDown(e)) return true;
|
||||
// Ctrl / Shift: a click adds or takes out an object, a drag draws a box around more
|
||||
if (e.shiftKey || e.ctrlKey || e.metaKey) { drag = { box: true, x0: e.clientX, y0: e.clientY, hit: hit.obj }; return true; }
|
||||
if (!hit.obj) return false;
|
||||
@@ -415,6 +418,7 @@
|
||||
},
|
||||
move: function (e, mv) {
|
||||
if (S.placing) { if (tool && mv.world) drawTool(e); return; }
|
||||
if (ptDrag) { ptMove(e); return; }
|
||||
if (drag && drag.box) { drawBand(drag, e); return; }
|
||||
if (!drag || !mv.world || !drag.w0) return;
|
||||
if (!drag.moved) {
|
||||
@@ -439,6 +443,7 @@
|
||||
},
|
||||
up: function (e, mv) {
|
||||
if (S.placing) { var t = tool; tool = null; ghost(null); if (t && !mv.cancel) finishTool(t, e); return; }
|
||||
if (ptDrag) { ptUp(mv); return; }
|
||||
var d = drag; drag = null; ghost(null);
|
||||
if (!d) return;
|
||||
if (d.box) { endBand(d, e, mv); return; }
|
||||
@@ -453,7 +458,7 @@
|
||||
else S.view.refresh();
|
||||
},
|
||||
cancel: function () { if (drag && drag.els) S.view.refresh(); if (drag && drag.box) band(null); drag = null; tool = null; ghost(null); },
|
||||
dblclick: function (e) { if (!chain) return false; finishChain(e.shiftKey); return true; }
|
||||
dblclick: function (e) { if (ptDelete(e)) return true; if (!chain) return false; finishChain(e.shiftKey); return true; }
|
||||
}
|
||||
});
|
||||
window.__studio = { S: S, view: S.view, op: op, select: select, selectMany: selectMany, reloadAll: reloadAll, toast: toast, api: api, obj: obj, snapStep: snapStep, ready: true };
|
||||
@@ -572,6 +577,7 @@
|
||||
}
|
||||
function setPlacing(kind) {
|
||||
S.placing = kind; tool = null; chain = null;
|
||||
drawHandles();
|
||||
if (S.view) ghost(null);
|
||||
document.querySelector(".st-centre").classList.toggle("st-placing", !!kind);
|
||||
Array.prototype.forEach.call(document.querySelectorAll(".st-tile"), function (b) { b.setAttribute("aria-pressed", String(b.getAttribute("data-type") === kind)); });
|
||||
@@ -601,6 +607,7 @@
|
||||
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();
|
||||
drawHandles();
|
||||
if (!quiet) renderRight();
|
||||
}
|
||||
function select(id, quiet) {
|
||||
@@ -726,6 +733,93 @@
|
||||
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);
|
||||
}
|
||||
// ------------------------------------------------------------ point handles (objects with pts)
|
||||
// One selected object with points (a route's waypoints, an area's outline) shows a handle on
|
||||
// each point and a "+" in the middle of each side: drag a point to move it, drag a "+" to add
|
||||
// one there, double-click a point to remove it. Points snap like a run's corners; one undo step each.
|
||||
// ptPress: the last point pressed; the viewer captures the pointer, so a double-click's target
|
||||
// is the drawing, not the handle
|
||||
var ptDrag = null, ptPress = null;
|
||||
function ptsOf(o) {
|
||||
if (!o || S.placing || selIds().length !== 1 || lockedEff(o) || hiddenEff(o) || !o.pts || o.pts.length < 2) return null;
|
||||
var m = prefab(o.type);
|
||||
if (!m) return null;
|
||||
// routes say whether they close; any other outline (areas) is a closed polygon
|
||||
var closed = m.shape === "path" ? !!propOf(o, "closed") && o.pts.length >= 3 : true;
|
||||
return { o: o, m: m, closed: closed, z: o.z || 0, min: closed ? 3 : 2, w: o.pts.map(function (q) { return worldOf(o, q[0], q[1]); }) };
|
||||
}
|
||||
function drawHandles(pts) {
|
||||
var svg = S.view && S.view.svg, g = svg && svg.querySelector(".st-handles"), inf = ptsOf(S.sel ? obj(S.sel) : null);
|
||||
if (!svg) return;
|
||||
if (!inf) { if (g) g.parentNode.removeChild(g); return; }
|
||||
if (!g) { g = document.createElementNS("http://www.w3.org/2000/svg", "g"); g.setAttribute("class", "st-handles"); }
|
||||
svg.appendChild(g); // last, above the drawing and the ghosts
|
||||
var rot = S.view.getRotation(), k = S.view.screenPx(), w = pts || inf.w, n = w.length, out = [], i;
|
||||
var sc = w.map(function (p) { return IsoKit.project(rot, p[0], p[1], inf.z); });
|
||||
out.push('<path class="st-hline" d="' + sc.map(function (s, j) { return (j ? "L" : "M") + s[0].toFixed(1) + " " + s[1].toFixed(1); }).join("") + (inf.closed ? "Z" : "") + '"/>');
|
||||
var sides = inf.closed ? n : n - 1;
|
||||
for (i = 0; i < sides; i++) {
|
||||
var a = sc[i], b = sc[(i + 1) % n];
|
||||
out.push('<circle class="st-hmid" data-mid="' + i + '" cx="' + ((a[0] + b[0]) / 2).toFixed(1) + '" cy="' + ((a[1] + b[1]) / 2).toFixed(1) + '" r="' + (4 * k).toFixed(2) + '"><title>Drag to add a point here</title></circle>');
|
||||
}
|
||||
for (i = 0; i < n; i++) {
|
||||
out.push('<circle class="st-hpt' + (ptDrag && ptDrag.i === i ? " st-on" : "") + '" data-pt="' + i + '" cx="' + sc[i][0].toFixed(1) + '" cy="' + sc[i][1].toFixed(1) + '" r="' + (5.5 * k).toFixed(2) + '"><title>Point ' + (i + 1) + ": drag to move, double-click to remove</title></circle>");
|
||||
}
|
||||
g.innerHTML = out.join("");
|
||||
}
|
||||
// a press on a handle starts a point drag (a "+" first adds its point)
|
||||
function ptDown(e) {
|
||||
var el = e.target && e.target.closest ? e.target.closest(".st-hpt, .st-hmid") : null;
|
||||
var inf = el && ptsOf(S.sel ? obj(S.sel) : null);
|
||||
if (!inf) return false;
|
||||
var pts = inf.w.map(function (p) { return p.slice(); }), i;
|
||||
if (el.hasAttribute("data-mid")) {
|
||||
var j = +el.getAttribute("data-mid"), a = pts[j], b = pts[(j + 1) % pts.length];
|
||||
i = j + 1; pts.splice(i, 0, [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2]);
|
||||
} else i = +el.getAttribute("data-pt");
|
||||
ptPress = el.hasAttribute("data-mid") ? null : { id: inf.o.id, i: i, t: Date.now() };
|
||||
ptDrag = { inf: inf, pts: pts, i: i, add: el.hasAttribute("data-mid"), moved: false };
|
||||
// redrawn on the first move only: replacing the handle under the pointer now would lose
|
||||
// the double-click that removes it
|
||||
return true;
|
||||
}
|
||||
// a point snaps to the grid and onto the ends and lines of walls, fences and roads, at any angle
|
||||
function ptMove(e) {
|
||||
var d = ptDrag, inf = d.inf;
|
||||
var q = snapCorner(worldAt(e, inf.z), inf.z, null, 12 * pxM(e));
|
||||
d.pts[d.i] = q; d.moved = true;
|
||||
drawHandles(d.pts);
|
||||
$("cursor").textContent = "point " + (d.i + 1) + ": x " + q[0].toFixed(2) + " y " + q[1].toFixed(2);
|
||||
}
|
||||
function ptUp(mv) {
|
||||
var d = ptDrag; ptDrag = null;
|
||||
if (!d) return;
|
||||
if (!d.moved && !d.add) return;
|
||||
if (mv.cancel) { drawHandles(); return; }
|
||||
setPts(d.inf.o, d.pts, d.add ? "Point added" : "Point " + (d.i + 1) + " moved");
|
||||
}
|
||||
function setPts(o, w, msg) {
|
||||
var pts = w.map(function (p) { var l = localOf(o, p[0], p[1]); return [r3(l[0]), r3(l[1])]; });
|
||||
update(o.id, { pts: pts }).then(function (r) { if (r) toast(msg + " · Ctrl+Z takes it back"); });
|
||||
}
|
||||
function ptDelete(e) {
|
||||
var el = e.target && e.target.closest ? e.target.closest(".st-hpt") : null, inf = ptsOf(S.sel ? obj(S.sel) : null);
|
||||
var i = el ? +el.getAttribute("data-pt") : ptPress && inf && ptPress.id === inf.o.id && Date.now() - ptPress.t < 800 ? ptPress.i : -1;
|
||||
ptPress = null;
|
||||
if (!inf || i < 0 || i >= inf.w.length) return false;
|
||||
if (inf.w.length <= inf.min) { toast("A " + (inf.closed ? "closed outline" : "route") + " needs at least " + inf.min + " points", true); return true; }
|
||||
var w = inf.w.slice();
|
||||
w.splice(i, 1);
|
||||
setPts(inf.o, w, "Point " + (i + 1) + " removed");
|
||||
return true;
|
||||
}
|
||||
// an outline prefab drawn as a rectangle: its four corners become points
|
||||
function editCorners(o) {
|
||||
var w = +propOf(o, "w") || 1, d = +propOf(o, "d") || 1;
|
||||
update(o.id, { pts: [[-w / 2, -d / 2], [w / 2, -d / 2], [w / 2, d / 2], [-w / 2, d / 2]].map(function (q) { return [r3(q[0]), r3(q[1])]; }) })
|
||||
.then(function () { toast("Corners: drag a point, drag a + to add one, double-click a point to remove it"); });
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------ 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
|
||||
@@ -1091,6 +1185,9 @@
|
||||
field("xy", "Position x y z", '<div class="st-xy"><input type="number" step="any" data-f="x" value="' + o.x + '" title="x (east), m"><input type="number" step="any" data-f="y" value="' + o.y + '" title="y (north), m"><input type="number" step="any" data-f="z" value="' + (o.z || 0) + '" title="z (height), m"></div>') +
|
||||
field("rot", "Rotation", '<div class="st-rot"><button type="button" data-act="rotl" title="Shift+R">⟲</button><input type="number" step="any" data-f="rot" value="' + (+(o.rot || 0).toFixed(4)) + '" title="degrees, counter-clockwise from east; any angle">°<button type="button" data-act="rotr" title="R">⟳</button></div>') +
|
||||
"</div>" +
|
||||
(o.pts && o.pts.length >= 2 ? '<h2>Points <small>' + o.pts.length + '</small></h2><p class="st-help">Drag a point on the map to move it, drag a + to add one, double-click a point to remove it.</p>' +
|
||||
(m.outline ? '<div class="st-actions"><button type="button" data-act="unpts" title="Back to a rectangle of width × length">Rectangle again</button></div>' : "")
|
||||
: m.outline ? '<h2>Outline</h2><div class="st-actions"><button type="button" data-act="pts" title="Turn the rectangle into four corners you can drag">Edit corners</button></div>' : "") +
|
||||
(m.props.length ? "<h2>Properties</h2><div class=\"st-form\">" + m.props.map(function (d) { return field(d.key, d.label || d.key, propInput(d, p[d.key])); }).join("") + "</div>" : "") +
|
||||
'<h2>Label and tooltip</h2><div class="st-form">' +
|
||||
field("name", "Name", '<input data-f="name" value="' + esc(o.name || "") + '" placeholder="' + esc(m.label) + '">') +
|
||||
@@ -1184,6 +1281,8 @@
|
||||
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 === "pts") editCorners(o);
|
||||
else if (a === "unpts") update(S.sel, { pts: null });
|
||||
else if (a === "rotl") turn(S.sel, -90);
|
||||
else if (a === "rotr") turn(S.sel, 90);
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user