Streets: sidewalks, road curves, junctions and roundabouts
Deploy web Studio / deploy (push) Successful in 4m31s

Roads take sidewalks (none, both, left, right): 12 cm high paving with
kerb faces drawn where they face the viewer. Road curve (radius, angle,
left/right) carries a street round a bend; centre lines are true arcs
when cut and offset, so curve joints are seamless. A road ending on
another road's centre line opens that road's kerb and sidewalk (T
junction); two roads ending at one corner close it. Roundabout: ring
radius and width, one or two lanes, up to four arms with flared kerbs,
splitter islands, give-way lines on the entry side (traffic on the left
or right) and a central island with apron.

Studio: a road run drawn corner by corner gets a curve in each corner
(prefab meta `corner`). Tooltips list the streets each one meets.
Engine: open `polyline` primitive (Prim::Poly open) so dashes run on
round curves. examples/streets and a Streets demo in the web Studio;
MCP guidance, docs. ROADMAP: backlog 5-7 done; multi-select and the
hosted copy marked done.

Tests: Rust 72, Studio 113, web 95.
This commit is contained in:
grabowski committed 2026-10-25 12:32:52 +07:00
1 parent 2442966743
commit c91d63663b
24 files changed
+989 -29

No files matched your search

+40 -6
View File
@@ -174,7 +174,8 @@
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);
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 · " +
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") + " · Backspace undoes a corner · Esc cancels");
}
// the turn of a line piece from p to q: along x or y; directed lines (conveyors) point from p to q
@@ -183,11 +184,44 @@
if (!m.directed) return ax ? 0 : 90;
return ax ? (q[0] >= p[0] ? 0 : 180) : (q[1] >= p[1] ? 90 : 270);
}
// the pieces of a run as add ops (pieces longer than the prefab allows are split)
// the corners of a run rounded by the prefab's corner piece (roads: curves of up to its default
// radius, less where the pieces next to a corner are short): [segments [p, q]], [curve add ops]
function roundRun(m, pts, z) {
var segs = [], curves = [], cm = m.corner && prefab(m.corner), i;
for (i = 1; i < pts.length; i++) if (Math.hypot(pts[i][0] - pts[i - 1][0], pts[i][1] - pts[i - 1][1]) >= 0.05) segs.push([pts[i - 1].slice(), pts[i].slice()]);
var n = segs.length;
if (!cm || n < 2) return { segs: segs, curves: curves };
var closed = n > 2 && Math.hypot(segs[n - 1][1][0] - segs[0][0][0], segs[n - 1][1][1] - segs[0][0][1]) < 1e-6;
var len = segs.map(function (s) { return Math.hypot(s[1][0] - s[0][0], s[1][1] - s[0][1]); });
var dir = segs.map(function (s, k) { return [(s[1][0] - s[0][0]) / len[k], (s[1][1] - s[0][1]) / len[k]]; });
// turn at the start of segment k (from segment k - 1), 0 where the run goes straight on
var turn = segs.map(function (s, k) {
if (k === 0 && !closed) return 0;
var a = dir[(k + n - 1) % n], b = dir[k], cr = a[0] * b[1] - a[1] * b[0];
return Math.abs(cr) > 0.5 && a[0] * b[0] + a[1] * b[1] < 0.5 ? cr : 0;
});
var R0 = +defOf(cm, "r") || 10, rmin = Math.max(+((cm.props || []).filter(function (d) { return d.key === "r"; })[0] || {}).min || 0, (+defOf(m, "d") || 0) / 2 + 0.5);
for (var k = 0; k < n; k++) {
if (!turn[k]) continue;
var a = (k + n - 1) % n, nextTurn = turn[(k + 1) % n] && (k + 1 < n || closed);
var r = Math.min(R0, len[a] * (turn[a] ? 0.5 : 1), len[k] * (nextTurn ? 0.5 : 1));
if (r < rmin) continue;
var P = segs[k][0], da = dir[a], db = dir[k];
var s0 = [P[0] - da[0] * r, P[1] - da[1] * r];
var add = { op: "add", type: m.corner, x: r3(s0[0]), y: r3(s0[1]), rot: (Math.round(Math.atan2(da[1], da[0]) * 2 / Math.PI) * 90 + 360) % 360,
props: { r: r3(r), angle: 90, turn: turn[k] > 0 ? "left" : "right" } };
if (z) add.z = z;
curves.push(add);
segs[a][1] = s0;
segs[k][0] = [P[0] + db[0] * r, P[1] + db[1] * r];
}
return { segs: segs, curves: curves };
}
// the pieces of a run as add ops (pieces longer than the prefab allows are split; corners rounded)
function runOps(m, pts, z) {
var ops = [], max = +((m.props || []).filter(function (d) { return d.key === "w"; })[0] || {}).max || 1e9;
for (var i = 1; i < pts.length; i++) {
var p = pts[i - 1], q = pts[i], L = Math.hypot(q[0] - p[0], q[1] - p[1]), n = Math.ceil(L / max - 1e-9);
var ops = [], max = +((m.props || []).filter(function (d) { return d.key === "w"; })[0] || {}).max || 1e9, rr = roundRun(m, pts, z);
for (var i = 0; i < rr.segs.length; i++) {
var p = rr.segs[i][0], q = rr.segs[i][1], L = Math.hypot(q[0] - p[0], q[1] - p[1]), n = Math.ceil(L / max - 1e-9);
if (L < 0.05) continue;
for (var j = 0; j < n; j++) {
var t = (j + 0.5) / n, add = { op: "add", type: m.type, x: r3(p[0] + (q[0] - p[0]) * t), y: r3(p[1] + (q[1] - p[1]) * t), rot: lineRot(m, p, q), props: { w: clampProp(m, "w", L / n) } };
@@ -195,7 +229,7 @@
ops.push(add);
}
}
return ops;
return ops.concat(rr.curves);
}
function finishChain(keep) {
var c = chain; chain = null; ghost(null);