All community skills

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