single-line-walls-script
by Cihad Paksoy
wallsfloor-plandouble-linerun-scriptautomation
Description
Single-line → double-line wall conversion via run_script (sandboxed JS). ONE tool call computes and draws every face — includes a ready-to-run axis-aligned generator. Use this variant when Run Script is enabled; it avoids the many-draw-step timeout of the hand-drawn method.
Example prompts
- /single-line-walls-script Convert the single-line walls in this plan into 200-unit-thick double-line walls
- /single-line-walls-script Turn these center-lines into 150 mm double-line walls
- /single-line-walls-script Make 200-thick double walls from this single-line sketch on layer 0
Full content
---
name: single-line-walls-script
description: Single-line → double-line wall conversion via run_script (sandboxed JS). ONE tool call computes and draws every face — includes a ready-to-run axis-aligned generator. Use this variant when Run Script is enabled; it avoids the many-draw-step timeout of the hand-drawn method.
tags: walls, floor-plan, double-line, run-script, automation
---
# Single line → double-line walls (run_script variant)
This is the SCRIPTED variant of the wall conversion. Instead of drawing each face by hand
(dozens of `draw` calls in one turn — which can exceed the server time limit and cut the run
off mid-stream), you issue ONE `run_script` call whose JavaScript reads the center-lines,
classifies every junction, and draws all faces in a single write. Prefer this whenever
`run_script` is available. If it is NOT (the tool isn't in the menu), use the plain
`single-line-walls` skill instead — do not try to hand-draw a large plan face by face.
`run_script` writes to the PREVIEW layer only; permanence still requires the normal
render → create_preview → user approval → commit gate. The sandbox is real JavaScript
(ES2023): `cad.*` reads the drawing, `cad.draw(entities)` writes previews, `console.log`
collects logs, and you end with `result = <value>`. There is NO delete inside the sandbox —
deletion of the originals is a separate tool step (§3).
## 1. Confirm scope + thickness (ask ONLY if genuinely unclear)
- Thickness t → half h = t/2. If unstated, ask (common: 100 / 150 / 200 / 250).
- Scale sanity: if t is > ~10% of a plan's short side (too thick) or < ~1% (too thin),
flag it and confirm before running.
- Scope: which layer(s)/plan. If several candidate line layers or separate plans exist, ask
first (offer an "all" option). Set the script's `LAYERS` accordingly.
Nothing else is a legitimate question — the preview is the user's decision gate.
## 2. Run the generator
Call `run_script` with the script below, editing ONLY the three top constants:
- `THICKNESS` — wall thickness in drawing units (e.g. 200).
- `LAYERS` — `["0"]` (or the confirmed layer names) to restrict; `null` = every layer that
has lines.
- `TARGET_LAYER` — `null` draws each face on its own center-line's layer; set a name to force one.
It reads the center-lines, detects each node (L corner / T junction / X crossing / free end),
offsets ±h with proper corner mitering (outer face extends, inner shortens), breaks the
through-wall face at T/X openings, caps free ends, and draws every face on the preview layer.
It RETURNS `centerline_handles` (needed for §3) plus counts and any `skipped_nonorthogonal`.
```js
// Single-line → double-line walls. Edit the 3 constants, then run.
const THICKNESS = 200; // wall thickness in drawing units
const LAYERS = null; // ["0"] to restrict to layers; null = all layers with lines
const TARGET_LAYER = null; // null = draw each face on its center-line's own layer
const h = THICKNESS / 2;
const TOL = Math.max(1e-6, h * 0.05); // coincidence tolerance
const eq = (p, u) => Math.abs(p - u) <= TOL;
// 1) center-line handles (aggregate caps ~200/layer; restrict LAYERS for larger plans)
const agg = cad.aggregate({ metric: "count", group_by: "layer", include_handles: true });
let handles = [];
for (const g of (agg.groups || [])) {
if (LAYERS && !LAYERS.includes(g.key)) continue;
handles = handles.concat(g.handles || []);
}
// 2) geometry — tolerate the {properties}/{entities}/flat shapes the bridge may return
let ents = cad.entities(handles);
if (ents.length && ents[0] && !ents[0].type)
ents = ents.flatMap((o) => o.properties || o.entities || []);
const segs = [], skipped = [];
for (const e of ents) {
const sp = e.start_point, ep = e.end_point;
if (e.type !== "Line" || !sp || !ep) { if (e && e.handle) skipped.push(e.handle); continue; }
if (eq(sp.y, ep.y) && !eq(sp.x, ep.x))
segs.push({ ax: "h", c: (sp.y + ep.y) / 2, a: Math.min(sp.x, ep.x), b: Math.max(sp.x, ep.x), lay: e.layer, hn: e.handle });
else if (eq(sp.x, ep.x) && !eq(sp.y, ep.y))
segs.push({ ax: "v", c: (sp.x + ep.x) / 2, a: Math.min(sp.y, ep.y), b: Math.max(sp.y, ep.y), lay: e.layer, hn: e.handle });
else skipped.push(e.handle); // diagonal / zero-length — not handled by this axis-aligned script
}
// map (along u, offset off) → [x, y] in a segment's own frame
const pt = (s, u, off) => (s.ax === "h" ? [u, off] : [off, u]);
// node at seg i's end (along-coord uEnd): L corner (with body dir dV), T (i is the branch),
// collinear splice, or free
function endNode(i, uEnd) {
const s = segs[i];
for (let j = 0; j < segs.length; j++) {
if (j === i) continue;
const t = segs[j];
if (t.ax === s.ax || !eq(t.c, uEnd)) continue;
if (s.c < t.a - TOL || s.c > t.b + TOL) continue;
const atA = eq(s.c, t.a), atB = eq(s.c, t.b);
if (atA || atB) return { kind: "L", dV: atA ? 1 : -1 };
return { kind: "T" };
}
for (let j = 0; j < segs.length; j++) {
if (j === i) continue;
const t = segs[j];
if (t.ax !== s.ax || !eq(t.c, s.c)) continue;
if (eq(t.a, uEnd) || eq(t.b, uEnd)) return { kind: "splice" };
}
return { kind: "free" };
}
// openings on seg i's faces where a perpendicular wall crosses its interior (T-through / X)
function breaks(i) {
const s = segs[i], out = { p: [], m: [] };
for (let j = 0; j < segs.length; j++) {
if (j === i) continue;
const t = segs[j];
if (t.ax === s.ax) continue;
if (t.c < s.a + TOL || t.c > s.b - TOL) continue; // t must cross i's INTERIOR
if (s.c < t.a - TOL || s.c > t.b + TOL) continue;
const gap = [t.c - h, t.c + h];
if (s.c > t.a + TOL && s.c < t.b - TOL) { out.p.push(gap); out.m.push(gap); } // X → both faces
else (eq(s.c, t.a) ? out.p : out.m).push(gap); // T → near face
}
return out;
}
// subtract gap intervals from [lo, hi] → surviving pieces
function carve(lo, hi, gaps) {
let ps = [[lo, hi]];
for (const [g0, g1] of gaps) {
const nx = [];
for (const [x0, x1] of ps) {
if (g1 <= x0 + TOL || g0 >= x1 - TOL) { nx.push([x0, x1]); continue; }
if (g0 > x0 + TOL) nx.push([x0, g0]);
if (g1 < x1 - TOL) nx.push([g1, x1]);
}
ps = nx;
}
return ps.filter(([a, b]) => b - a > TOL);
}
// 3) build faces
const out = [];
for (let i = 0; i < segs.length; i++) {
const s = segs[i], lay = TARGET_LAYER || s.lay;
const nLo = endNode(i, s.a), nHi = endNode(i, s.b), br = breaks(i);
for (const side of [1, -1]) {
const off = s.c + side * h;
let lo = s.a, hi = s.b;
if (nLo.kind === "L") lo += (side === -nLo.dV ? -h : h); // outer extend / inner shorten
else if (nLo.kind === "T") lo += h; // branch: both faces shorten
if (nHi.kind === "L") hi += (side === -nHi.dV ? h : -h);
else if (nHi.kind === "T") hi -= h;
for (const [u0, u1] of carve(lo, hi, side === 1 ? br.p : br.m))
out.push({ type: "line", layer: lay, params: { start: pt(s, u0, off), end: pt(s, u1, off) } });
}
if (nLo.kind === "free") out.push({ type: "line", layer: lay, params: { start: pt(s, s.a, s.c - h), end: pt(s, s.a, s.c + h) } });
if (nHi.kind === "free") out.push({ type: "line", layer: lay, params: { start: pt(s, s.b, s.c - h), end: pt(s, s.b, s.c + h) } });
}
// 4) draw (write cap is 256 entities/script; page a very large plan by region instead)
const op_ids = [];
for (let i = 0; i < out.length; i += 128) {
const r = cad.draw(out.slice(i, i + 128));
if (r && r.op_id) op_ids.push(r.op_id);
}
result = {
thickness: THICKNESS,
centerlines: segs.length,
faces_drawn: out.length,
centerline_handles: segs.map((s) => s.hn), // pass these to delete_entities in the next step
skipped_nonorthogonal: skipped,
op_ids,
};
```
Limits & fallbacks: axis-aligned (horizontal/vertical) walls only — diagonals come back in
`skipped_nonorthogonal`; convert those by hand. Up to 256 faces per run; for a huge plan set
`LAYERS`/region and run in parts. If `result.centerlines` is 0, the layer filter or scope is
wrong — recheck §1 before drawing.
## 3. Delete the originals
The sandbox has no delete. Take `result.centerline_handles` and call
`delete_entities(handles)` — this removes the single-line originals. It is PART of the preview
(journalled, reversible on reject); never defer it to after approval. Skip only if the user
asked to keep the center-lines.
## 4. Verify → preview → stop
`render_and_verify` → fix `severity:error` findings (partial-overlap warnings at T/X joints
are expected, not blockers). Then you MUST call `create_preview` and STOP for the user's
approval; work without `create_preview` is half-done. Give a short summary after commit.
## Autonomy
Don't ask "shall I?" for intermediate steps — the preview is the decision gate. If the script
skips diagonals or a joint looks imperfect, draw what it produced and explain it in the
preview; partial beats nothing.
## Example prompts
- /single-line-walls-script Convert the single-line walls in this plan into 200-unit-thick double-line walls
- /single-line-walls-script Turn these center-lines into 150 mm double-line walls
- /single-line-walls-script Make 200-thick double walls from this single-line sketch on layer 0