Fences join like walls, fence designs, draw runs corner by corner
Deploy web Studio / deploy (push) Successful in 3m52s
Deploy web Studio / deploy (push) Successful in 3m52s
Joints: line_joint (util.rhai) applies the house-wall rules to fences and site walls, so pieces never overlap and the depth sort stays clean. - A corner is closed: the thicker piece (or, if equal, the one along world x) runs through, and the other stops at its face. - On a straight run, two pieces share one post. - At a T, the stem stops at the other piece's face without a post of its own. - A fence that ends at a site or house wall stops at the wall's face. Fence designs (prop style): mesh, wooden boards, steel bars, mesh on a stone base, cobble wall, and brick wall with pillars. Board, stone and brick joints are drawn on the face turned to the viewer. Brick pillars are separate sprites. Studio: line prefabs (walls, fences, hedges, roads) can be drawn as a run by clicking corner after corner. Double-click or Enter ends the run, clicking the first corner closes it, and Backspace drops the last corner. A run is one undo step. A drag still draws one piece. Corners and ends snap onto the ends and lines of the pieces they meet, and a marker shows where the next corner will snap. The house demo gets a brick front wall with gate pillars and wooden side fences in place of the old east fence, which ran over the road. Tests cover every design's corners, Ts, straight runs and wall stops, with no depth-sort cycles in any rotation, plus site walls. check_studio draws and closes a fence run, undoes it in one step, snaps a dragged spur, and picks a design.
This commit is contained in:
1 parent
7962249191
commit
e4017ce2a8
14 files changed
+559
-27
No files matched your search
+1
-1
@@ -62,7 +62,7 @@ document.documentElement.setAttribute("data-theme",t);})();
|
||||
<h3>isokit Studio</h3>
|
||||
<table>
|
||||
<tr><th>Add</th><td>pick an object on the left, click on the map (Shift-click to place several)</td></tr>
|
||||
<tr><th>Draw</th><td>walls, roads, fences: drag along the map; floors, areas, buildings, roofs: drag a rectangle; Room: drag a rectangle for a floor with walls around it</td></tr>
|
||||
<tr><th>Draw</th><td>walls, fences, hedges, roads: drag along the map for one piece, or click corner after corner for a run (double-click or Enter ends it, clicking the first corner closes it, Backspace drops the last corner); ends snap onto the walls and fences they meet, which then join at corners and T-junctions. Floors, areas, buildings, roofs: drag a rectangle; Room: drag a rectangle for a floor with walls around it</td></tr>
|
||||
<tr><th>Windows, doors</th><td>click on a wall: they snap onto it, cut their opening and move, turn, copy and delete with the wall; R turns a door to the other hinge and swing side</td></tr>
|
||||
<tr><th>Storeys</th><td>GF, 1F, … cut a house open; on 1F and up, new walls, floors and furniture go onto that storey (3 m each, parameter <code>storey_h</code>)</td></tr>
|
||||
<tr><th>Select</th><td>click it on the map or in the list</td></tr>
|
||||
|
||||
+120
-6
@@ -111,6 +111,97 @@
|
||||
return [{ pts: planePts(worldOf(sm.host, sm.u - sm.hw, 0), worldOf(sm.host, sm.u + sm.hw, 0), sm.z + z0, sm.z + z0 + h), cls: "st-plane" }];
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------ drawing runs of walls and fences
|
||||
// Line prefabs (walls, fences, hedges, roads) are drawn corner by corner. Corners snap onto the
|
||||
// ends and lines of the pieces already there, so the scripts join them (line_joint in util.rhai).
|
||||
var chain = null; // a run being drawn: { m, z, pts: [[x, y], ...] }
|
||||
// metres per screen pixel around a pointer event
|
||||
function pxM(e) { var a = S.view.toWorld(e.clientX, e.clientY, 0), b = S.view.toWorld(e.clientX + 10, e.clientY, 0); return Math.hypot(b[0] - a[0], b[1] - a[1]) / 10; }
|
||||
function linePieces(z) {
|
||||
return S.doc.objects.filter(function (o) { var m = prefab(o.type); return !o.hidden && m && m.shape === "line" && Math.abs((o.z || 0) - z) < 0.5; }).map(function (o) {
|
||||
var L = (+propOf(o, "w") || 0) / 2;
|
||||
return { o: o, a: worldOf(o, -L, 0), b: worldOf(o, L, 0), ax: qOf(o.rot) % 2 === 0 };
|
||||
});
|
||||
}
|
||||
// where a corner goes for pointer world point w: with `from` (the previous corner) the piece keeps
|
||||
// to x or y and its far end snaps onto an end or the line of a piece it meets (or the run's own
|
||||
// corners), else onto the grid; without, onto the nearest end, else onto a line, else the grid
|
||||
function snapCorner(w, z, from, R) {
|
||||
var pcs = linePieces(z), own = chain ? chain.pts : [], best = null, i;
|
||||
if (!from) {
|
||||
pcs.forEach(function (p) {
|
||||
[p.a, p.b].forEach(function (q) { var d = Math.hypot(q[0] - w[0], q[1] - w[1]); if (d < R && (!best || d < best.d)) best = { d: d, p: q }; });
|
||||
});
|
||||
if (best) return [r3(best.p[0]), r3(best.p[1])];
|
||||
pcs.forEach(function (p) {
|
||||
var l = localOf(p.o, w[0], w[1]), L = (+propOf(p.o, "w") || 0) / 2;
|
||||
if (Math.abs(l[0]) <= L && Math.abs(l[1]) < R && (!best || Math.abs(l[1]) < best.d)) best = { d: Math.abs(l[1]), p: worldOf(p.o, Math.max(-L, Math.min(L, snap(l[0]))), 0) };
|
||||
});
|
||||
return best ? [r3(best.p[0]), r3(best.p[1])] : [snap(w[0]), snap(w[1])];
|
||||
}
|
||||
var ax = Math.abs(w[0] - from[0]) >= Math.abs(w[1] - from[1]), k = ax ? 0 : 1, lk = 1 - k;
|
||||
var f = w[k], fix = from[lk], cands = [];
|
||||
pcs.forEach(function (p) {
|
||||
if (p.ax === ax) {
|
||||
// in line with this piece: its ends
|
||||
if (Math.abs(p.a[lk] - fix) < 0.01) { cands.push(p.a[k]); cands.push(p.b[k]); }
|
||||
} else {
|
||||
// across this piece: its centre line, where it spans our line
|
||||
var lo = Math.min(p.a[lk], p.b[lk]), hi = Math.max(p.a[lk], p.b[lk]);
|
||||
if (fix >= lo - 1e-6 && fix <= hi + 1e-6) cands.push(p.a[k]);
|
||||
}
|
||||
});
|
||||
for (i = 0; i < own.length; i++) if (Math.abs(own[i][lk] - fix) < 0.01) cands.push(own[i][k]);
|
||||
cands.forEach(function (c) { var d = Math.abs(c - f); if (Math.abs(c - from[k]) > 0.05 && d < R && (!best || d < best.d)) best = { d: d, c: c }; });
|
||||
var out = [0, 0];
|
||||
out[k] = r3(best ? best.c : snap(f)); out[lk] = fix;
|
||||
return out;
|
||||
}
|
||||
// the preview of one piece from p to q: its footprint, and its face when it has a height
|
||||
function pieceGhost(m, p, q, z) {
|
||||
var th = +(defOf(m, "t") || defOf(m, "d")) || 0.3, h = +defOf(m, "h") || 0, ax = Math.abs(q[1] - p[1]) < 1e-9;
|
||||
var x0 = Math.min(p[0], q[0]), x1 = Math.max(p[0], q[0]), y0 = Math.min(p[1], q[1]), y1 = Math.max(p[1], q[1]);
|
||||
var out = [{ pts: ax ? rectPts(x0, y0 - th / 2, x1, y0 + th / 2, z) : rectPts(x0 - th / 2, y0, x0 + th / 2, y1, z) }];
|
||||
if (h > 0) out.push({ pts: planePts(p, q, z, z + h), cls: "st-plane" });
|
||||
return out;
|
||||
}
|
||||
function cornerMark(p, z) { return { pts: rectPts(p[0] - 0.18, p[1] - 0.18, p[0] + 0.18, p[1] + 0.18, z), cls: "st-plane" }; }
|
||||
function runLength(pts) { var L = 0; for (var i = 1; i < pts.length; i++) L += Math.hypot(pts[i][0] - pts[i - 1][0], pts[i][1] - pts[i - 1][1]); return L; }
|
||||
function drawChain(e) {
|
||||
var c = chain, last = c.pts[c.pts.length - 1], q = snapCorner(worldAt(e, c.z), c.z, last, 12 * pxM(e)), polys = [], i;
|
||||
for (i = 1; i < c.pts.length; i++) Array.prototype.push.apply(polys, pieceGhost(c.m, c.pts[i - 1], c.pts[i], c.z));
|
||||
if (Math.hypot(q[0] - last[0], q[1] - last[1]) > 0.05) Array.prototype.push.apply(polys, pieceGhost(c.m, last, q, c.z));
|
||||
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 · " +
|
||||
(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 pieces of a run as add ops (pieces longer than the prefab allows are split)
|
||||
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), ax = Math.abs(q[1] - p[1]) < 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: ax ? 0 : 90, props: { w: clampProp(m, "w", L / n) } };
|
||||
if (z) add.z = z;
|
||||
ops.push(add);
|
||||
}
|
||||
}
|
||||
return ops;
|
||||
}
|
||||
function finishChain(keep) {
|
||||
var c = chain; chain = null; ghost(null);
|
||||
if (!c) return;
|
||||
var ops = runOps(c.m, c.pts, c.z);
|
||||
if (ops.length) {
|
||||
var L = runLength(c.pts);
|
||||
opList(ops).then(function (r) { if (r) toast(c.m.label + ": " + ops.length + " piece" + (ops.length === 1 ? "" : "s") + ", " + L.toFixed(1) + " m · Ctrl+Z takes the run back"); });
|
||||
}
|
||||
if (keep) setPlacing(c.m.type); else setPlacing(null);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------ server state
|
||||
function takeState(r) {
|
||||
if (r.doc) S.doc = r.doc;
|
||||
@@ -182,7 +273,8 @@
|
||||
down: function (e, hit) {
|
||||
if (S.placing) {
|
||||
var pm = pmeta(S.placing), z = placeZ(pm), a = worldAt(e, z);
|
||||
tool = { m: pm, z: z, a: [snap(a[0]), snap(a[1])], b: null };
|
||||
a = pm.shape === "line" ? snapCorner(a, z, chain ? chain.pts[chain.pts.length - 1] : null, 12 * pxM(e)) : [snap(a[0]), snap(a[1])];
|
||||
tool = { m: pm, z: z, a: a, b: null };
|
||||
return true;
|
||||
}
|
||||
if (!hit.obj) return false;
|
||||
@@ -226,7 +318,8 @@
|
||||
} else if (d.dx || d.dy) moveWith(d.id, d.dx, d.dy, d.carry);
|
||||
else S.view.refresh();
|
||||
},
|
||||
cancel: function () { if (drag && drag.els) S.view.refresh(); drag = null; tool = null; ghost(null); }
|
||||
cancel: function () { if (drag && drag.els) S.view.refresh(); drag = null; tool = null; ghost(null); },
|
||||
dblclick: function (e) { if (!chain) return false; finishChain(e.shiftKey); return true; }
|
||||
}
|
||||
});
|
||||
window.__studio = { S: S, view: S.view, op: op, select: select, reloadAll: reloadAll, toast: toast, api: api, obj: obj, snapStep: snapStep, ready: true };
|
||||
@@ -244,6 +337,8 @@
|
||||
// the hover preview while placing a window or door: where on which wall it goes
|
||||
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 || !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);
|
||||
@@ -261,7 +356,8 @@
|
||||
}
|
||||
function drawTool(e) {
|
||||
var t = tool, m = t.m, w = worldAt(e, t.z), z = t.z, polys = [];
|
||||
t.b = [snap(w[0]), snap(w[1])];
|
||||
if (chain) { drawChain(e); return; }
|
||||
t.b = m.shape === "line" ? snapCorner(w, z, t.a, 12 * pxM(e)) : [snap(w[0]), snap(w[1])];
|
||||
if (m.shape !== "line" && m.shape !== "rect") return;
|
||||
var g = toolGeom(t);
|
||||
if (g.line) {
|
||||
@@ -283,6 +379,22 @@
|
||||
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") {
|
||||
// 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));
|
||||
if (Math.hypot(q[0] - last[0], q[1] - last[1]) < 0.05) return;
|
||||
chain.pts.push(q);
|
||||
if (chain.pts.length > 3 && Math.hypot(q[0] - chain.pts[0][0], q[1] - chain.pts[0][1]) < 1e-6) finishChain(keep);
|
||||
else drawChain(e);
|
||||
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);
|
||||
opList(ops).then(function (r) { if (r) toast(m.label + " added" + (keep ? "" : " · Shift to draw several")); });
|
||||
if (!keep) setPlacing(null);
|
||||
return;
|
||||
}
|
||||
if (m === ROOM) {
|
||||
if (!g || !g.ok) { toast("Drag a rectangle on the map to draw a room"); setPlacing(m.type); return; }
|
||||
var fm = prefab("floor"), ops = roomWalls(g.x0, g.y0, g.x1, g.y1, t.z);
|
||||
@@ -326,7 +438,7 @@
|
||||
return out;
|
||||
}
|
||||
function setPlacing(kind) {
|
||||
S.placing = kind; tool = null;
|
||||
S.placing = kind; tool = null; chain = null;
|
||||
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)); });
|
||||
@@ -335,7 +447,7 @@
|
||||
var m = pmeta(kind), lab = m ? m.label : kind;
|
||||
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: " + lab + " (a click gives the default length) · 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 === "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;
|
||||
@@ -397,7 +509,9 @@
|
||||
var k = e.key, ctrl = e.ctrlKey || e.metaKey;
|
||||
if (ctrl && (k === "z" || k === "Z")) { if (e.shiftKey) redo(); else undo(); return true; }
|
||||
if (ctrl && (k === "y" || k === "Y")) { redo(); return true; }
|
||||
if (k === "Escape") { if (S.placing) setPlacing(null); else select(null); return true; }
|
||||
if (chain && k === "Enter") { finishChain(e.shiftKey); return true; }
|
||||
if (chain && k === "Backspace") { chain.pts.pop(); if (!chain.pts.length) { chain = null; ghost(null); setPlacing(S.placing); } else ghost(null); return true; }
|
||||
if (k === "Escape") { if (chain) { chain = null; ghost(null); setPlacing(S.placing); } else if (S.placing) setPlacing(null); else select(null); return true; }
|
||||
if (!S.sel) return false;
|
||||
if (k === "Delete" || k === "Backspace") { remove(S.sel); return true; }
|
||||
if (ctrl && (k === "d" || k === "D")) { duplicate(S.sel); return true; }
|
||||
|
||||
Reference in new issue
Block a user