New route prefab (routes.rhai): cars, buses, motorbikes, cyclists, people, deer or elephants move along waypoints, round a loop or there and back. Each way keeps to the left of the line (or right), corners are rounded into pieces of at most 45 degrees; there-and-back routes become loops with U-turns, so the two directions never share a strip. Engine: Prim::Travel / sprite.begin_travel / script travel(a, b, dur, windows) ... end(): the mover is drawn at a piece's start and slides to its end while it is on that piece, hidden otherwise. The renderer writes keyframes per piece (named by a hash of their content) into one <style> per view. Each piece is its own sprite with the lane strip as its sort extent (ext_reset), mitred at the corners, so movers pass in front of and behind trees, lamps and houses. Movers that look alike and never meet on a piece share one drawing. iso-travel pauses (not hides) in still, reduced-motion and ghosted views. Studio: meta shape "path" draws point by point (snapping like runs) and saves one object with pts; ending on the first point closes the loop. MCP: pts in the object schema, route guidance. Demos: streets gets traffic round the block, a bus, motorbikes, cyclists and strolling people (replacing the one-axis cars); country gets cars on the lane. Tests: Rust 79 (each mover on exactly one piece at every moment, in every rotation), Studio 125, web 99.
This commit is contained in:
1 parent
ccfd619a99
commit
5ec164a633
20 files changed
+698
-23
No files matched your search
+37
-7
@@ -209,6 +209,12 @@
|
||||
polys.push(cornerMark(c.pts[0], c.z));
|
||||
var closes = c.pts.length > 2 && Math.hypot(q[0] - c.pts[0][0], q[1] - c.pts[0][1]) < 1e-6;
|
||||
var n = c.pts.length - 1 + (Math.hypot(q[0] - last[0], q[1] - last[1]) > 0.05 ? 1 : 0);
|
||||
if (c.m.shape === "path") {
|
||||
ghost(polys, c.m.label + ": " + (n + 1) + " points, " + (runLength(c.pts) + Math.hypot(q[0] - last[0], q[1] - last[1])).toFixed(1) + " m · " +
|
||||
(closes ? "click to close the loop" : "click the next point · double-click or Enter to finish (there and back) · click the first point for a loop") +
|
||||
" · diagonals at 45°, Ctrl: any angle · Backspace undoes a point · Esc cancels");
|
||||
return;
|
||||
}
|
||||
ghost(polys, c.m.label + ": " + n + " piece" + (n === 1 ? "" : "s") + ", " + (runLength(c.pts) + Math.hypot(q[0] - last[0], q[1] - last[1])).toFixed(1) + " m" +
|
||||
(c.m.corner ? ", corners rounded" : "") + " · " +
|
||||
(closes ? "click to close the run" : "click the next corner · double-click or Enter to finish · click the first corner to close") +
|
||||
@@ -223,6 +229,21 @@
|
||||
if (!m.directed) return ax ? 0 : 90;
|
||||
return ax ? (q[0] >= p[0] ? 0 : 180) : (q[1] >= p[1] ? 90 : 270);
|
||||
}
|
||||
// prefabs drawn corner by corner: "line" pieces (walls, fences, roads: one object per piece) and
|
||||
// "path" objects (routes: one object whose pts are the points clicked)
|
||||
function isRun(m) { return !!m && (m.shape === "line" || m.shape === "path"); }
|
||||
// the add op of a path object through world points: x, y in the middle of them, pts relative
|
||||
// to it; ending on the first point closes it (props.closed, when the prefab has that prop)
|
||||
function pathOp(m, pts, z) {
|
||||
var p = pts.slice(), closed = p.length > 3 && Math.hypot(p[p.length - 1][0] - p[0][0], p[p.length - 1][1] - p[0][1]) < 1e-6;
|
||||
if (closed) p.pop();
|
||||
var xs = p.map(function (q) { return q[0]; }), ys = p.map(function (q) { return q[1]; });
|
||||
var cx = r3((Math.min.apply(null, xs) + Math.max.apply(null, xs)) / 2), cy = r3((Math.min.apply(null, ys) + Math.max.apply(null, ys)) / 2);
|
||||
var add = { op: "add", type: m.type, x: cx, y: cy, pts: p.map(function (q) { return [r3(q[0] - cx), r3(q[1] - cy)]; }), props: {} };
|
||||
if ((m.props || []).some(function (d) { return d.key === "closed"; })) add.props.closed = closed;
|
||||
if (z) add.z = z;
|
||||
return add;
|
||||
}
|
||||
// Ctrl (Cmd) frees the angle of the piece being drawn; directed lines (conveyors) keep to the axes
|
||||
function freeAngle(m, e) { return !!(e && (e.ctrlKey || e.metaKey) && !m.directed); }
|
||||
// the corners of a run rounded by the prefab's corner piece (roads: curves of up to its default
|
||||
@@ -276,6 +297,14 @@
|
||||
function finishChain(keep) {
|
||||
var c = chain; chain = null; ghost(null);
|
||||
if (!c) return;
|
||||
if (c.m.shape === "path") {
|
||||
if (c.pts.length >= 2) {
|
||||
var po = pathOp(c.m, c.pts, c.z), np = po.pts.length, closed = !!(po.props && po.props.closed);
|
||||
op(po).then(function (r) { if (r) toast(c.m.label + ": " + np + " points, " + runLength(c.pts).toFixed(1) + " m" + (closed ? ", a loop" : ", there and back") + " · Ctrl+Z takes it back"); });
|
||||
}
|
||||
if (keep) setPlacing(c.m.type); else setPlacing(null);
|
||||
return;
|
||||
}
|
||||
var ops = runOps(c.m, c.pts, c.z);
|
||||
if (ops.length) {
|
||||
var L = runLength(c.pts);
|
||||
@@ -363,7 +392,7 @@
|
||||
down: function (e, hit) {
|
||||
if (S.placing) {
|
||||
var pm = pmeta(S.placing), z = placeZ(pm), a = worldAt(e, z);
|
||||
a = pm.shape === "line" ? snapCorner(a, z, chain ? chain.pts[chain.pts.length - 1] : null, 12 * pxM(e), freeAngle(pm, e)) : [snap(a[0]), snap(a[1])];
|
||||
a = isRun(pm) ? snapCorner(a, z, chain ? chain.pts[chain.pts.length - 1] : null, 12 * pxM(e), freeAngle(pm, e)) : [snap(a[0]), snap(a[1])];
|
||||
tool = { m: pm, z: z, a: a, b: null };
|
||||
return true;
|
||||
}
|
||||
@@ -443,7 +472,7 @@
|
||||
function hoverPlacing(e) {
|
||||
var m = pmeta(S.placing);
|
||||
if (chain) { drawChain(e); return; }
|
||||
if (m && m.shape === "line") { var z0 = placeZ(m); ghost([cornerMark(snapCorner(worldAt(e, z0), z0, null, 12 * pxM(e)), z0)]); return; }
|
||||
if (m && isRun(m)) { var z0 = placeZ(m); ghost([cornerMark(snapCorner(worldAt(e, z0), z0, null, 12 * pxM(e)), z0)]); return; }
|
||||
if (!m || !m.mount || !m.mount.length) return;
|
||||
var z = placeZ(m), sm = snapMount(m, worldAt(e, z), z, null, defOf(m, "w"));
|
||||
ghost(sm ? mountGhost(m, sm) : null);
|
||||
@@ -452,7 +481,7 @@
|
||||
// diagonal by snapCorner), "rect" prefabs (and rooms) fill the rectangle
|
||||
function toolGeom(t) {
|
||||
var a = t.a, b = t.b || t.a, dx = b[0] - a[0], dy = b[1] - a[1];
|
||||
if (t.m.shape === "line") {
|
||||
if (isRun(t.m)) {
|
||||
var L = Math.hypot(dx, dy);
|
||||
return { line: true, len: L, a: a, b: b, x: (a[0] + b[0]) / 2, y: (a[1] + b[1]) / 2, ok: L >= 0.25, rot: lineRot(t.m, a, b) };
|
||||
}
|
||||
@@ -462,8 +491,8 @@
|
||||
function drawTool(e) {
|
||||
var t = tool, m = t.m, w = worldAt(e, t.z), z = t.z, polys = [];
|
||||
if (chain) { drawChain(e); return; }
|
||||
t.b = m.shape === "line" ? snapCorner(w, z, t.a, 12 * pxM(e), freeAngle(m, e)) : [snap(w[0]), snap(w[1])];
|
||||
if (m.shape !== "line" && m.shape !== "rect") return;
|
||||
t.b = isRun(m) ? snapCorner(w, z, t.a, 12 * pxM(e), freeAngle(m, e)) : [snap(w[0]), snap(w[1])];
|
||||
if (!isRun(m) && m.shape !== "rect") return;
|
||||
var g = toolGeom(t);
|
||||
if (g.line) {
|
||||
polys = pieceGhost(m, g.a, g.b, z);
|
||||
@@ -483,7 +512,7 @@
|
||||
function finishTool(t, e) {
|
||||
var m = t.m, keep = e.shiftKey, g = t.b ? toolGeom(t) : null, add;
|
||||
var at = worldAt(e, t.z), p = [snap(at[0]), snap(at[1])];
|
||||
if (m.shape === "line") {
|
||||
if (isRun(m)) {
|
||||
// a run: a click starts it, each click (or drag) adds a corner, the first corner closes it
|
||||
if (chain) {
|
||||
var last = chain.pts[chain.pts.length - 1], q = snapCorner(at, t.z, last, 12 * pxM(e), freeAngle(m, e));
|
||||
@@ -494,7 +523,7 @@
|
||||
return;
|
||||
}
|
||||
if (!g || !g.ok) { chain = { m: m, z: t.z, pts: [t.a] }; drawChain(e); return; }
|
||||
var ops = runOps(m, [t.a, t.b], t.z);
|
||||
var ops = m.shape === "path" ? [pathOp(m, [t.a, t.b], t.z)] : runOps(m, [t.a, t.b], t.z);
|
||||
opList(ops).then(function (r) { if (r) toast(m.label + " added" + (keep ? "" : " · Shift to draw several")); });
|
||||
if (!keep) setPlacing(null);
|
||||
return;
|
||||
@@ -552,6 +581,7 @@
|
||||
h.textContent = m === ROOM ? "Drag a rectangle on the map: a room (floor and walls) · Esc to stop"
|
||||
: m && m.mount && m.mount.length ? "Click on a wall to place: " + lab + " · Esc to stop"
|
||||
: m && m.shape === "line" ? "Drag along the map to draw one " + lab + ", or click corner after corner for a run (double-click or Enter ends it, the first corner closes it); ends snap onto walls and fences · Esc to stop"
|
||||
: m && m.shape === "path" ? "Click point after point on the map for the " + lab + " (double-click or Enter ends it; the first point again makes a loop); points snap onto road and wall ends · Esc to stop"
|
||||
: m && m.shape === "rect" ? "Drag a rectangle on the map: " + lab + " (a click gives the default size) · Esc to stop"
|
||||
: "Click on the map to place: " + lab + " · Esc to stop";
|
||||
h.hidden = false;
|
||||
|
||||
Reference in new issue
Block a user