Source code of plot #034 back to plot

Download full working sketch as 034.tar.gz.
Unzip, then start a local web server and load the page in a browser.

///<reference path="../pub/lib/paper.d.ts" />
import {info, init, loadLib, setSketch} from "./utils/boilerplate.js";
import {mulberry32, rand, rand_range, rand_select, randn_bm, setRandomGenerator, shuffle} from "./utils/random.js"
import {lnPtDist} from "./utils/geo.js";
import {CanvasMasker} from "./utils/canvas-masker.js";
import {SimplexNoise} from "./utils/simplex-noise.js";
import {genBlueNoise} from "./utils/blue-noise.js";

const pw = 1480;    // Paper width
const ph = 2100;    // Paper height
const w = 1050;     // Drawing width
const h = 1480;     // Drawing height

// Sketch parameters
let faceH = h;
let faceW = w * 0.7;
let sideW = faceW * 0.2;
let nDiags = 7;
let insetLen = 20;
let longitudinalHatchGap = 7;
let higherExtraWidth = 15;

// Pre-calculated from parameters
let faceLeft = (w - (faceW+sideW)) / 2;
let faceTop = (h - faceH) / 2;
let faceRight = faceLeft + faceW;

// Toggle: draw squiggly lines (slow, so we're turning it off while tuning the geomtry)
let squiggly = true;

let cmVisible = false;
let rf = 5;  // Occlusion canvas is this many times larger than our area
let cm;      // Canvas masker

let noise;
let simplex;
let seed = Math.round(Math.random() * 65535);
// seed = 16819;
let noiseSeed = seed;

setSketch(function () {
  setRandomGenerator(mulberry32(seed));
  simplex = new SimplexNoise(noiseSeed);
  info("Seed: " + seed);
  init(w, h, pw, ph);
  if (!cmVisible) cm = new CanvasMasker(w, h, rf);
  else cm = new CanvasMasker(w, h, rf, document.querySelector("#drawing"), "canvas-calc");
  noise = genBlueNoise(32, 32);
  noise.forEach((val, ix) => noise[ix] = Math.pow((val - noise.length / 2) / noise.length, 0.5) * 3);
  draw();
});

let Faces = {
  Front: "Front",
  Side: "Side",
  Top: "Top",
  Bottom: "Bottom",
};

function draw() {

  paper.project.currentStyle.strokeColor = "black";
  paper.project.currentStyle.strokeWidth = 2;

  // Generate diagonals, shuffle them
  let diags = genDiags(nDiags);
  shuffle(diags);

  // Center vertically, and extend z-higher ones horizontally
  let minY = h, maxY = 0;
  diags.forEach(diag => {
    diag.pts.forEach(pt => {
      minY = Math.min(minY, pt.y);
      maxY = Math.max(maxY, pt.y);
    });
  });
  let yOfs = h / 2 - (minY + maxY) / 2;
  for (let i = 0; i < diags.length; ++i)
    diags[i].updateGeo(diags.length / 2 - i, yOfs);

  // Draw and fill... all in the smart order for correct occlusion
  for (let i = 0; i < diags.length; ++i) drawFrontAndSide(diags, i);
  for (let i = 0; i < diags.length; ++i) calcTopBottomSegs(diags, i);
  let tbFillables = [];
  for (let i = 0; i < diags.length; ++i)
    tbFillables.push(...drawTopsBottoms(diags, i));
  fillTopsBottoms(diags, tbFillables);
}

function calcTopBottomSegs(diags, ix) {

  let diag = diags[ix];
  let vec = new Point(sideW, 0);

  cm.clear();
  blockFaces(diags, ix, Faces.Front);
  blockFaces(diags, -1, Faces.Side);

  if (diag.sinkingTop) {
    let visibleLines = cm.getMaskedLine(...diag.sinkingTop);
    for (const vl of visibleLines) {
      if (vl[1].subtract(vl[0]).length < 5) continue;
      diag.sinkingTopSegs.push(vl);
    }
  }
  if (diag.risingBottom) {
    let visibleLines = cm.getMaskedLine(...diag.risingBottom);
    for (const vl of visibleLines) {
      if (vl[1].subtract(vl[0]).length < 5) continue;
      diag.risingBottomSegs.push(vl);
    }
  }
}

function drawTopsBottoms(diags, ix) {

  let fillables = [];

  let diag = diags[ix];
  let vec = new Point(sideW, 0);

  cm.clear();
  blockFaces(diags, ix, Faces.Front);
  blockFaces(diags, -1, Faces.Side);
  blockFaces(diags, -1, Faces.Bottom);
  blockFaces(diags, ix, Faces.Top);

  for (const seg of diag.sinkingTopSegs) {
    let pts = [
      seg[0],
      seg[0].add(vec),
      seg[1].add(vec),
      seg[1],
    ];
    for (let i = 0; i < pts.length; ++i) {
      let pt1 = pts[i];
      let pt2 = pts[(i+1)%pts.length];
      let vlines = cm.getMaskedLine(pt1, pt2);
      for (const vl of vlines) {
        if (squiggly) drawOutlineSegment(...vl, false);
        else project.activeLayer.addChild(Path.Line(...vl));
      }
    }
    fillables.push({ix, pts, top: true});
  }

  cm.clear();
  blockFaces(diags, ix, Faces.Front);
  blockFaces(diags, -1, Faces.Side);
  blockFaces(diags, -1, Faces.Top);
  blockFaces(diags, ix, Faces.Bottom);

  for (const seg of diag.risingBottomSegs) {
    let pts = [
      seg[0],
      seg[0].add(vec),
      seg[1].add(vec),
      seg[1],
    ];
    for (let i = 0; i < pts.length; ++i) {
      let pt1 = pts[i];
      let pt2 = pts[(i+1)%pts.length];
      let vlines = cm.getMaskedLine(pt1, pt2);
      for (const vl of vlines) {
        if (squiggly) drawOutlineSegment(...vl, false);
        else project.activeLayer.addChild(Path.Line(...vl));
      }
    }
    fillables.push({ix, pts, top: false});
  }

  return fillables;
}

function fillTopsBottoms(diags, fillables) {
  cm.clear();
  blockFaces(diags, -1, Faces.Front);
  blockFaces(diags, -1, Faces.Side);

  fillables.sort((a, b) => b.pts[0].x - a.pts[0].x);
  for (const f of fillables) {
    cm.includePoly(f.pts);
    if (f.top) fillTop(f.pts);
    else fillBottom(f.pts, diags[f.ix].getBottomGap());
    cm.blockPoly(f.pts);
  }
}


function drawFrontAndSide(diags, ix) {

  let diag = diags[ix];
  let vec = new Point(sideW, 0);

  // Lower indexes: face and side block me
  cm.clear();
  for (let i = 0; i < ix; ++i) {
    let pts = diags[i].pts;
    cm.blockPoly(pts);
    cm.blockPoly([pts[0], pts[0].add(vec), pts[1].add(vec), pts[1]]);
  }

  // Outline of front
  for (let pass = 0; pass < 2; ++pass) {
    for (let i = 0; i < diag.pts.length; ++i) {
      let pt1 = diag.pts[i];
      let pt2 = diag.pts[diag.ixmod(i + 1)];
      let visibleLines = cm.getMaskedLine(pt1, pt2);
      for (const vl of visibleLines) {
        if (squiggly) drawOutlineSegment(...vl, (pass % 2) == 0);
        else project.activeLayer.addChild(Path.Line(...vl));
      }
    }
  }

  // Outline of side
  let vlines = [];
  vlines.push(...cm.getMaskedLine(diag.pts[0], diag.pts[0].add(vec)));
  vlines.push(...cm.getMaskedLine(diag.pts[0].add(vec), diag.pts[1].add(vec)));
  vlines.push(...cm.getMaskedLine(diag.pts[1].add(vec), diag.pts[1]));
  for (let pass = 0; pass < 2; ++pass) {
    for (const vl of vlines) {
      if (squiggly) drawOutlineSegment(...vl, (pass % 2) == 0);
      else project.activeLayer.addChild(Path.Line(...vl));
    }
  }

  // Fill side
  fillSide(diag);

  // Fill front
  cm.includePoly(diag.pts);
  fillVertNoise(diag.pts);

}

function drawOutlineSegment(pt1, pt2, reverse) {
  let squiggleLen = 10;
  let bgain = 0.7;
  let sgain = 0.8;
  if (squiggly) drawSquigglyLine([pt1, pt2], reverse, squiggleLen, noise, bgain, sgain, 0, null);
  else {
    let ln = Path.Line(pt1, pt2);
    project.activeLayer.addChild(ln);
  }
}

function drawSquigglyLine(ln, reverse, squiggleLen, bnoise, bgain, sgain, sofs, gapper) {

  let subDiv = Math.round(squiggleLen / 5);
  if (subDiv < 1) subDiv = 1;

  let vec = ln[1].subtract(ln[0]);
  let len = vec.length;
  let nsegs = Math.max(2, Math.round(len / squiggleLen));
  nsegs *= subDiv;

  let orto = vec.rotate(90);
  orto.length = 1;
  let noiseOfs = Math.floor(rand() * bnoise.length);

  let getSimplex = pt => {
    let dist = pt.subtract(ln[1]).length;
    let test = new Point(sofs, dist);
    return simplex.noise2D(test.x / w * 40, test.y / h * 4) * sgain;
  }
  let getPt = i => ln[0].add(vec.multiply(i/nsegs));

  // First, get simplex bias along path. We'll offset to get 0 average.
  let sbias = 0;
  for (let i = 0; i <= nsegs; ++i) sbias += getSimplex(getPt(i));
  sbias /= (nsegs + 1);

  let polys = [];
  let pts = [];
  for (let i = 0; i < nsegs + subDiv; i += subDiv) {

    if (i > nsegs) i = nsegs;

    if (gapper && pts.length > 1 && rand() < gapper.interruptProb) {
      polys.push(pts);
      pts = [];
      i -= subDiv;
      i += rand_select(gapper.skips);
    }

    let d = bnoise[(i + noiseOfs) % bnoise.length] * bgain;
    let pt = getPt(i);

    // Add simplex noise for large-ish deviation
    let sn = getSimplex(pt);
    pt = pt.add(orto.multiply(sn - sbias));

    // Add blue noise for squiggles
    pt = pt.add(orto.multiply(d));

    pts.push(pt);
  }
  if (pts.length > 1) polys.push(pts);

  if (reverse) polys.reverse();
  for (const pts of polys) {
    if (reverse) pts.reverse();
    let path = new Path({segments: pts});
    project.activeLayer.addChild(path);
  }
}

function blockFaces(diags, excludeIx, face) {
  let vec = new Point(sideW, 0);
  for (let ix = 0; ix < diags.length; ++ix) {
    if (ix == excludeIx) continue;
    let d = diags[ix];
    if (face == Faces.Front) cm.blockPoly(d.pts);
    else if (face == Faces.Side) {
      cm.blockPoly([d.pts[0], d.pts[0].add(vec), d.pts[1].add(vec), d.pts[1]]);
    }
    else if (face == Faces.Top) {
      for (const seg of d.sinkingTopSegs) {
        cm.blockPoly([seg[0], seg[0].add(vec), seg[1].add(vec), seg[1]]);
      }
    }
    else if (face == Faces.Bottom) {
      for (const seg of d.risingBottomSegs) {
        cm.blockPoly([seg[0], seg[0].add(vec), seg[1].add(vec), seg[1]]);
      }
    }
  }
}

function genDiags(nDiags) {

  let res = [];

  let leftYs = [], rightYs = [];
  let gap = faceH / (nDiags + 1);
  for (let i = 0; i < nDiags; ++i) {
    let ly = faceTop + gap * (i + 0.5);
    ly += rand_range(-1, 1) * gap * 0.3;
    leftYs.push(ly);
    let ry = faceTop + gap / 2 + gap * (i + 0.5);
    ry += rand_range(-1, 1) * gap * 0.3;
    rightYs.push(ry);
  }
  let l2r;
  while (!l2r) l2r = getL2R(nDiags);

  for (let i = 0; i < l2r.length; ++i) {
    let lh, rh;
    while (true) {
      lh = rand_range(gap * 0.2, gap * 1.5);
      rh = rand_range(gap * 0.2, gap * 1);
      let ratio = lh / rh;
      if (ratio < 1) ratio = 1 / ratio;
      if (ratio > 1.5) break;
    }
    res.push(new Diag([
      new Point(faceRight, rightYs[l2r[i]] - rh / 2),
      new Point(faceRight, rightYs[l2r[i]] + rh / 2),
      new Point(faceLeft, leftYs[i] + lh / 2),
      new Point(faceLeft, leftYs[i] - lh / 2),
    ]));
  }

  return res;
}

function getL2R(nDiags) {
  let rightTaken = new Set();
  let l2r = [];
  for (let i = 0; i < nDiags; ++i) l2r.push(-1);
  let ix = 0;
  let iters = 0, maxIters = nDiags * 5;
  while (rightTaken.size < nDiags && iters < maxIters) {
    matchFromLeft(ix);
    ++ix;
  }
  if (iters != maxIters) return l2r;
  return null;

  function matchFromLeft(ix) {
    let opts = [];
    if (ix - 3 >= 0) opts.push(ix - 3);
    if (ix - 2 >= 0) opts.push(ix - 2);
    if (ix - 1 >= 0) opts.push(ix - 1);
    if (ix + 3 < nDiags) opts.push(ix + 3);
    if (ix + 2 < nDiags) opts.push(ix + 2);
    if (ix + 1 < nDiags) opts.push(ix + 1);
    let to;
    while (iters < maxIters) {
      to = rand_select(opts);
      if (!rightTaken.has(to)) break;
      ++iters;
    }
    l2r[ix] = to;
    rightTaken.add(to);
  }
}

class Diag {

  constructor(pts) {
    this.pts = pts;
    this.path = null;
    this.sinkingTop = null;
    this.risingBottom = null;
    this.sinkingTopSegs = [];
    this.risingBottomSegs = [];
    this.init(pts);
  }

  init(pts) {
    this.pts = pts;
    this.path = new Path({segments: pts, closed: true});

    this.sinkingTop = null;
    this.risingBottom = null;
    for (let i = 0; i < this.pts.length; ++i) {
      let pt1 = this.pts[i];
      let pt2 = this.pts[this.ixmod(i + 1)];
      if (pt2.x > pt1.x && pt2.y > pt1.y) this.sinkingTop = [pt1, pt2];
      if (pt2.x < pt1.x && pt2.y > pt1.y) this.risingBottom = [pt1, pt2];
    }
    this.sinkingTopSegs = [];
    this.risingBottomSegs = [];
  }

  updateGeo(horizExtension, yOfs) {
    let pts = [];
    this.pts.forEach(pt => pts.push(pt.clone()));
    pts[0].x += horizExtension * higherExtraWidth;
    pts[1].x += horizExtension * higherExtraWidth;
    pts[2].x -= horizExtension * higherExtraWidth;
    pts[3].x -= horizExtension * higherExtraWidth;
    pts.forEach(pt => pt.y += yOfs);
    this.init(pts);
  }

  ixmod(ix) {
    while (ix < 0) ix += this.pts.length;
    return ix % this.pts.length;
  }

  getBottomGap() {
    // Zero is horizontal; the steeper, the more negative
    let angle = this.pts[0].subtract(this.pts[3]).angle;
    angle = -angle;
    let gap = 3.5 + angle * 0.2;
    return Math.max(3.5, gap);
  }
}


function genHatchLines(rect, angle, gap) {
  let arad = angle / 180 * Math.PI;
  if (angle < -90 || angle > 90) throw "angle must be between -90 and 90";
  let lines = [];
  // Going left from right
  if (Math.abs(angle) < 45) {
    let v = rect.bounds.height * Math.tan(arad);
    let len = Math.sqrt(v ** 2 + rect.bounds.height ** 2);
    let adv = gap / Math.cos(arad);
    let vec = new Point(0, -len).rotate(angle);
    let start = angle > 0 ? rect.bounds.left - v : rect.bounds.left;
    let end = angle > 0 ? rect.bounds.right : rect.bounds.right - v;
    for (let x = start; x < end; x += adv) {
      let pt = new Point(x, rect.bounds.bottom);
      lines.push([pt, pt.add(vec)]);
    }
    return lines;
  }
  // Going top to bottom
  angle = angle - 90;
  if (angle < -90) angle += 180;
  arad = angle / 180 * Math.PI;
  let v = rect.bounds.width * Math.tan(-arad);
  let len = Math.sqrt(v ** 2 + rect.bounds.width ** 2);
  let adv = gap / Math.cos(arad);
  let vec = new Point(len, 0).rotate(angle);
  let start = angle > 0 ? rect.bounds.top + v : rect.bounds.top;
  let end = angle > 0 ? rect.bounds.bottom : rect.bounds.bottom + v;
  for (let y = start; y < end; y += adv) {
    let pt = new Point(rect.bounds.left, y);
    lines.push([pt, pt.add(vec)]);
    if (lines.length > 200) break;
  }
  return lines;
}


function fillVertNoise(pts) {

  let squiggleLen = 10;
  let bgain = 0.6;
  let sgain = 4;

  let targetDarkness = rand_range(0.5, 2);

  let hSpacing = rand_range(6, 12);
  let vSpacing = 8;
  let stickLen = vSpacing;
  let lenVariability = 0.3;
  let ditherThreshold = (1 / targetDarkness) * (15-hSpacing) * 0.05;

  // The path to be filled
  let remaining = new Path({segments: pts, closed: true});
  let rx = remaining.bounds.left;
  let ry = remaining.bounds.top;
  let rw = remaining.bounds.width;
  let rh = remaining.bounds.height;

  let nsz = 64;
  let bn_ints = genBlueNoise(nsz, nsz);
  let bn = [];
  bn_ints.forEach(val => bn.push(val / (nsz **2)));

  let nHoriz = Math.round(rw / hSpacing);
  let nVert = Math.round(rh / vSpacing);
  let topLeft = new Point(rx, ry);
  for (let hix = 0; hix < nHoriz; ++hix) {
    let linesForColumn = [];
    for (let vix = 0; vix < nVert; ++vix) {

      let noiseX = hix % nsz;
      let noiseY = vix % nsz;
      let noiseVal = bn[noiseY * 64 + noiseX];
      if (noiseVal < ditherThreshold) continue;

      let cx = hSpacing * (hix + 0.5);
      let cy = vSpacing * (vix + 0.5);
      cy += rand_range(-vSpacing / 2, vSpacing / 2) * lenVariability;
      let pt = new Point(cx, cy).add(topLeft);

      let len = stickLen + rand_range(-stickLen, stickLen) * lenVariability;
      let vec = new Point(0, len / 2);
      linesForColumn.push([pt.subtract(vec), pt.add(vec)]);
    }
    drawSimplifiedLinesForColumns(linesForColumn, (hix%2)==0);
  }

  function drawSimplifiedLinesForColumns(lines, reverse) {

    if (lines.length == 0) return;

    let sofs = lines[0].x;
    let minGap = 3;

    // Join overlapping ones
    let sarr = [lines[0]];
    for (let i = 1; i < lines.length; ++i) {
      let lastLn = sarr[sarr.length - 1];
      let ln = lines[i];
      // Starts after previous line
      if (ln[0].y > lastLn[1].y + minGap) {
        sarr.push(ln);
        continue;
      }
      // Overlaps with previous line, and ends after
      if (ln[1].y > lastLn[1].y) lastLn[1].y = ln[1].y;
    }

    // Draw
    for (const ln of sarr) {
      let visibleLines = cm.getMaskedLine(ln[0], ln[1], true);
      for (const vl of visibleLines) {
        if (squiggly) drawSquigglyLine(vl, reverse, squiggleLen, noise, bgain, sgain, sofs, null);
        else project.activeLayer.addChild(Path.Line(...vl));
      }
    }
  }
}

function fillSide(diag) {

  let innerHatchGap = 9;
  let squiggleLen = 10;
  let bgain = 0.5;
  let sgain = 3;

  // Hatch around the side only, in case we're tall enough to have a whole inside
  // Otherwise, hatch entire right face
  let innerRect = [];
  if (diag.pts[1].y - diag.pts[0].y > 3 * insetLen) {
    cm.includeRect(diag.pts[0].x, diag.pts[0].y, sideW, insetLen);
    cm.includeRect(diag.pts[1].x, diag.pts[1].y - insetLen, sideW, insetLen);
    cm.includeRect(diag.pts[0].x, diag.pts[0].y, insetLen, diag.pts[1].y - diag.pts[0].y);
    cm.includeRect(diag.pts[0].x + sideW - insetLen, diag.pts[0].y, insetLen, diag.pts[1].y - diag.pts[0].y);
    innerRect.push(new Point(diag.pts[0].x + insetLen, diag.pts[0].y + insetLen));
    innerRect.push(new Point(diag.pts[0].x + sideW - insetLen, diag.pts[0].y + insetLen));
    innerRect.push(new Point(diag.pts[1].x + sideW - insetLen, diag.pts[1].y - insetLen));
    innerRect.push(new Point(diag.pts[1].x + insetLen, diag.pts[1].y - insetLen));
  }
  else cm.includeRect(diag.pts[0].x, diag.pts[0].y, sideW, diag.pts[1].y - diag.pts[0].y);

  let gap = 5;
  let hatchRect = Path.Rectangle(diag.pts[0], new Size(sideW, diag.pts[1].y - diag.pts[0].y));
  let nHatches = Math.round(sideW / gap);
  let lines = genHatchLines(hatchRect, 0, sideW / nHatches);

  for (const [pt1, pt2] of lines) {
    let mlns = cm.getMaskedLine(pt1, pt2, true);
    for (let i = 0; i < mlns.length; ++i) {
      const [pta, ptb] = mlns[i];
      const reverse = i % 2 == 0;
      let sofs = pta.x * 0.5;
      if (squiggly) drawSquigglyLine([pta, ptb], reverse, squiggleLen, noise, bgain, sgain, sofs, null);
      else project.activeLayer.addChild(Path.Line(pta, ptb));
    }
  }

  // We're not a tube: done here
  if (innerRect.length == 0) return;

  // Draw inner outline
  for (let pass = 0; pass < 2; ++pass) {
    for (let i = 0; i < innerRect.length; ++i) {
      let pt1 = innerRect[i];
      let pt2 = innerRect[(i + 1) % innerRect.length];
      let mlns = cm.getMaskedLine(pt1, pt2);
      for (let i = 0; i < mlns.length; ++i) {
        const [pta, ptb] = mlns[i];
        if (squiggly) drawOutlineSegment(pta, ptb, (pass % 2) == 0);
        else project.activeLayer.addChild(Path.Line(pta, ptb));
      }
    }
  }

  // Block boundary that we already hatched.
  cm.blockRect(diag.pts[0].x, diag.pts[0].y, sideW, insetLen);
  cm.blockRect(diag.pts[1].x, diag.pts[1].y - insetLen, sideW, insetLen);
  cm.blockRect(diag.pts[0].x, diag.pts[0].y, insetLen, diag.pts[1].y - diag.pts[0].y);
  cm.blockRect(diag.pts[0].x + sideW - insetLen, diag.pts[0].y, insetLen, diag.pts[1].y - diag.pts[0].y);

  // Explicitly include inner rectangle.
  cm.includeRect(diag.pts[0].x + insetLen, diag.pts[0].y + insetLen, sideW - 2 * insetLen, diag.pts[1].y - diag.pts[0].y - 2 * insetLen);

  let innerW = innerRect[1].x - innerRect[0].x;

  // We've got a rising bottom? Paint the "roof" inside the tube.
  let risingTop = [diag.pts[3], diag.pts[0]];
  if (risingTop[1].y > risingTop[0].y - 10) risingTop = null; // -10: Let's not draw roof for near-horizontal
  if (risingTop) {

    let vec = risingTop[0].subtract(risingTop[1]);
    vec.length *= innerW / -vec.x;
    let blockPts = [innerRect[1], innerRect[1].add(vec), innerRect[0]];

    // Corner line from top right inner corner
    let ln = [innerRect[1], innerRect[1].add(vec)];
    let mlns = cm.getMaskedLine(ln[0], ln[1], true);
    for (let i = 0; i < mlns.length; ++i) {
      const [pta, ptb] = mlns[i];
      if (squiggly) drawOutlineSegment(pta, ptb, false);
      else project.activeLayer.addChild(Path.Line(pta, ptb));
    }
    let gap = diag.getBottomGap();
    vec.length *= gap / vec.y;
    for (let i = 0; i < (innerRect[2].y - innerRect[1].y) / gap; ++i) {
      let pt1 = innerRect[1].add(vec.multiply(i));
      let pt2A = pt1.add(new Point(-sideW, 0));
      let mlns = cm.getMaskedLine(pt1, pt2A, true);
      for (let i = 0; i < mlns.length; ++i) {
        const [pta, ptb] = mlns[i];
        if (squiggly) drawOutlineSegment(pta, ptb, false);
        else project.activeLayer.addChild(Path.Line(pta, ptb));
      }
    }
    cm.blockPoly(blockPts);
  }

  // We've got a sinking bottom? Paint the "floor" inside the tube.
  let sinkingBottom = [diag.pts[1], diag.pts[2]];
  if (sinkingBottom[1].y > sinkingBottom[0].y - 10) sinkingBottom = null; // -10: Let's not draw roof for near-horizontal
  if (sinkingBottom) {

    let vec = sinkingBottom[1].subtract(sinkingBottom[0]);
    vec.length *= innerW / -vec.x;
    let blockPts = [innerRect[2], innerRect[3], innerRect[2].add(vec)];

    // Corner line from bottom right inner corner
    let ln = [innerRect[2], innerRect[2].add(vec)];
    let mlns = cm.getMaskedLine(ln[0], ln[1], true);
    for (let i = 0; i < mlns.length; ++i) {
      const [pta, ptb] = mlns[i];
      if (squiggly) drawOutlineSegment(pta, ptb, false);
      else project.activeLayer.addChild(Path.Line(pta, ptb));
    }
    let breadth = lnPtDist(ln[0], ln[1], innerRect[3]);
    let nLines = Math.round(breadth / longitudinalHatchGap);
    for (let i = 0; i < nLines; ++i) {
      let pt1 = innerRect[2].subtract(new Point(innerW * i / nLines, 0));
      let pt2A = pt1.add(vec);
      let mlns = cm.getMaskedLine(pt1, pt2A, true);
      for (let i = 0; i < mlns.length; ++i) {
        const [pta, ptb] = mlns[i];
        if (squiggly) drawOutlineSegment(pta, ptb, false);
        else project.activeLayer.addChild(Path.Line(pta, ptb));
      }
    }
    cm.blockPoly(blockPts);
  }

  // Fill inside rect (whatever is not blocked) with vertical lines
  for (let x = innerRect[1].x - innerHatchGap; x > innerRect[0].x; x -= innerHatchGap) {
    let pt1 = new Point(x, innerRect[0].y);
    let pt2 = new Point(x, innerRect[3].y);
    let mlns = cm.getMaskedLine(pt1, pt2, true);
    for (let i = 0; i < mlns.length; ++i) {
      const [pta, ptb] = mlns[i];
      if (squiggly) drawOutlineSegment(pta, ptb, false);
      else project.activeLayer.addChild(Path.Line(pta, ptb));
    }
  }
}

function fillTop(pts) {

  // Pts are CW from top left

  // To calculate breadth, we must extend sinking top into quasi infinity
  // Otherwise distance from line segment is larger than distance from line, and we end up with too small a gap
  let topLine = pts[3].subtract(pts[0]);
  let breadth = lnPtDist(pts[0].add(topLine.multiply(500)), pts[0].subtract(topLine.multiply(500)), pts[1]);
  let nLines = Math.ceil(breadth / longitudinalHatchGap);
  let vec = new Point(sideW, 0);

  for (let i = 1; i < nLines; ++i) {
    let pt1 = pts[0].add(vec.multiply(i/nLines));
    let pt2 = pts[3].add(vec.multiply(i/nLines));
    let mlns = cm.getMaskedLine(pt1, pt2, true);
    for (const [pta, ptb] of mlns) {
      if (squiggly) drawOutlineSegment(pta, ptb, false);
      else project.activeLayer.addChild(Path.Line(pta, ptb));
    }
  }
}

function fillBottom(pts, gap) {

  // Pts are CW from top left

  let vside = pts[0].subtract(pts[3]);
  let nLines = Math.round((pts[2].y - pts[1].y) / gap);
  let vec = new Point(sideW, 0);

  for (let i = 1; i < nLines; ++i) {
    let pt1 = pts[3].add(vside.multiply(i/nLines));
    let pt2 = pt1.add(vec);
    let mlns = cm.getMaskedLine(pt1, pt2, true);
    for (const [pta, ptb] of mlns) {
      if (squiggly) drawOutlineSegment(pta, ptb, false);
      else project.activeLayer.addChild(Path.Line(pta, ptb));
    }
  }
}