// Railway Simulation Master File (Consist-centric)

// ---------- Canvas ----------
const canvas = document.getElementById("railway");
const ctx = canvas.getContext("2d");

function resizeCanvasToCSS() {
  const dpr = Math.max(1, Math.min(window.devicePixelRatio || 1, 3));
  const cssW = canvas.clientWidth;
  const cssH = canvas.clientHeight;
  const needResize =
    canvas.width !== Math.round(cssW * dpr) ||
    canvas.height !== Math.round(cssH * dpr);
  if (needResize) {
    canvas.width = Math.round(cssW * dpr);
    canvas.height = Math.round(cssH * dpr);
    ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
  }
}
// ---------- World <-> Screen transform ----------
let worldScale = 1;
let worldOffsetX = 0;
let worldOffsetY = 0;
let worldRotation = 0;
let collisions = 0;
let followConsistId = null; // id of consist being followed, or null
let justMerged = false;

function worldToScreen(wx, wy) {
  // 1. scale+pan
  let sx = wx * worldScale + worldOffsetX;
  let sy = wy * worldScale + worldOffsetY;

  // 2. rotate about canvas centre (CSS px)
  const cx = canvas.clientWidth / 2;
  const cy = canvas.clientHeight / 2;
  const dx = sx - cx;
  const dy = sy - cy;

  const cos = Math.cos(worldRotation),
    sin = Math.sin(worldRotation);
  return {
    x: cx + cos * dx - sin * dy,
    y: cy + sin * dx + cos * dy,
  };
}

function screenToWorld(sx, sy) {
  // 1. undo rotation about canvas centre (CSS px)
  const cx = canvas.clientWidth / 2;
  const cy = canvas.clientHeight / 2;
  const dx = sx - cx;
  const dy = sy - cy;

  const cos = Math.cos(-worldRotation),
    sin = Math.sin(-worldRotation);
  const ux = cos * dx - sin * dy;
  const uy = sin * dx + cos * dy;

  const unrotX = cx + ux;
  const unrotY = cy + uy;

  // 2. undo scale+pan
  return {
    x: (unrotX - worldOffsetX) / worldScale,
    y: (unrotY - worldOffsetY) / worldScale,
  };
}

function updateWorldTransform({ fit = true, margin = 40 } = {}) {
  if (!trackLayout || !fit) return; // preserve current pan/zoom when fit=false

  // Use CSS size (clientWidth/Height), *not* canvas.width/height (which are DPR-scaled)
  const W = canvas.clientWidth;
  const H = canvas.clientHeight;

  const { minX, minY, maxX, maxY } = getLayoutBounds();
  const layoutW = Math.max(1, maxX - minX);
  const layoutH = Math.max(1, maxY - minY);

  const scaleX = (W - margin) / layoutW;
  const scaleY = (H - margin) / layoutH;
  worldScale = Math.max(0.0001, Math.min(scaleX, scaleY));

  // Center the layout inside the canvas
  const worldW = layoutW * worldScale;
  const worldH = layoutH * worldScale;
  worldOffsetX = (W - worldW) / 2 - minX * worldScale;
  worldOffsetY = (H - worldH) / 2 - minY * worldScale;
}
// screen px -> world units (keeps screen-constant sizes)
function px(n) {
  return n / worldScale;
}

// ---------- Data ----------
let trackLayout = null;
let switchStates = {};
let signalStates = {};
const images = {};

let rollingstock = { locomotives: [], carriages: [], tasks: [] };
let locos = []; // vehicle objects of type 'loco'
let cars = []; // vehicle objects of type 'car'

let showNNodes = false;

// === Consist-centric model ===
let consists = []; // each: { id, vehicles[], liveLocoId|null, dir:+1|-1, moving, waiting, status, statusAlpha }
let nextConsistId = 1;

let rafIds = {}; // animation frame IDs per consist.id

// ---------- Tuning (world units unless px(...) noted) ----------
const GAP = 1;
const SPEED = 0.3; // meters per frame (converted to fraction via physLen)
const STOP_MARGIN = 1; //  converted  in checks
const AUTOCONNECT_THROTTLE = 0.3; //throttle must be under this to autoconnect other wise collision
const EPS = 1e-4;
const LOCO_WIDTH = 2.25; // world units (meters)
const CAR_WIDTH = 2.8; // world units (meters)
const NODERADIUSPX = 17; // screen px for node hit radius
const TRACKWIDTH = 0.5;
const STOPPED_STRING = "Hit";
const FOLLOW_TRAIN_STRING = "View Train";
const FOLLOWING_TRAIN_STRING = "Viewing";

const BUFFER_COLOUR = "red";
const STOP_COLOUR = "red";
const GO_COLOUR = "limegreen";

const COMPASS_DIAMETER = 60;
const COMPASS_SCALE = 1.5;

// meters (your lengths are in meters)
const BOGIE_INSET = 1.2; // how far in from each buffer the bogie sits
const BOGIE_LENGTH = 2.2; // visual bogie frame length
const BOGIE_WIDTH = 2.3; // visual bogie frame width
const WHEEL_OFFSET = 0.65; // wheel centers from bogie center along x (visual)
const WHEEL_RADIUS = 0.35; // visual wheel radius (meters)
const MAXLENGTHBEFOREBOGIES = 12;

// ---------- Load ----------
const params = new URLSearchParams(window.location.search);
const layoutFile = params.get("layout") || "layoutallschematic.json";
const rollingFile = params.get("rollingstock") || "rollingstockall.json";
const taskId = Math.max(0, parseInt(params.get("task") || 1) - 1);
let drawnodes = params.get("drawnodes") || 0;
let calculatestartsegments = Number(params.get("startsegments")) || 0;
let debug = Number(params.get("debug")) || 0;
showNNodes = drawnodes == 1;

Promise.all([
  fetch(layoutFile).then((r) => r.json()),
  fetch(rollingFile).then((r) => r.json()),
])
  .then(([layout, stock]) => {
  
    trackLayout=layout;
//trackLayout = cleanTrackLayout(layout);
normalizeConnections(trackLayout, "B2");
console.log("canonical segments:", Object.keys(trackLayout.canonicalSegments || {}).length);



    const root = stock?.rollingstock ?? stock ?? {};
    rollingstock = {
      locomotives: root.locomotives || [],
      carriages: root.carriages || [],
      tasks: root.tasks || [],
    };

    initCompass();
    initSwitchStates();
    initSignalStates();
    initStock();
    buildTaskList();
    loadTask(rollingstock.tasks[taskId]);
    //placeStartConsists(rollingstock.tasks[0]);
    resizeCanvasToCSS();
    updateWorldTransform({ fit: true });

    // preload feature SVGs, then continue
    return initFeatures();
  })
  .then(() => {
    buildControlsFromConsists();
    drawNetwork("1");
  })
  .catch((e) => console.error("JSON load error:", e));

function initFeatures() {
  if (!trackLayout.features) return Promise.resolve();

  const tasks = [];

  for (const f of Object.values(trackLayout.features)) {
    if (f.type === "svg" && f.src && !images[f.src]) {
      tasks.push(preloadSVG(f.src));
    }
  }

  return Promise.all(tasks);
}

function preloadSVG(src) {
  return new Promise((resolve) => {
    const img = new Image();
    img.onload = () => {
      images[src] = img;
      resolve();
    };
    img.onerror = () => {
      console.warn("SVG failed to load:", src);
      images[src] = null; // mark as failed
      resolve(); // still resolve, don’t reject
    };
    img.src = src;
  });
}

// On first ResizeObserver/resize (or on orientation change) do a fresh fit:
let didInitialFit = false;
const ro = new ResizeObserver(() => {
  resizeCanvasToCSS();
  if (!didInitialFit) {
    updateWorldTransform({ fit: true });
    didInitialFit = true;
  }
  drawNetwork("2");
});
ro.observe(canvas);

// ---------- Init ----------
function initCompass() {
  worldRotation = rotationFromCompass(trackLayout.compass);
}

function rotationFromCompass(dir) {
  switch (dir) {
    case "N":
      return 0;
    case "NE":
      return Math.PI / 4;
    case "E":
      return Math.PI / 2;
    case "SE":
      return (3 * Math.PI) / 4;
    case "S":
      return Math.PI;
    case "SW":
      return (-3 * Math.PI) / 4;
    case "W":
      return -Math.PI / 2;
    case "NW":
      return -Math.PI / 4;
    default:
      return 0;
  }
}

function initSwitchStates() {
  for (const id in trackLayout.tracks)
    if (trackLayout.tracks[id].type == "point") switchStates[id] = 0;
}

function initSignalStates() {
  signalStates = {};
  for (const id in trackLayout.tracks) {
    if (
      trackLayout.tracks[id].type != "signal" &&
      trackLayout.tracks[id].type != "pointsignal"
    )
      continue;
    /*    signalStates[id] = (degree(id) === 1); // only ends are buffers by default
    signalStates[id] = (degree(id) === 2); // signals defaultto off unless state set to Go in layout*/
    const node = trackLayout.tracks[id];
    if (node.state) {
      // use JSON state if provided
      signalStates[id] = node.state.toUpperCase() !== "GO";
    } else {
      // fall back to old rule
      signalStates[id] = true;
    }
  }
}

function initStock() {
  locos = [];
  cars = [];
  rollingstock.locomotives.forEach((ld) => {
    locos.push({
      id: ld.label,
      name: ld.name ?? ld.label,
      color: toHexColor(ld.color || "blue"),
      type: "loco",
      width: ld.width || LOCO_WIDTH,
      neutral: true, // becomes false for the live loco in a consist
      length: +ld.length || 10, // meters
      leadPath: [],
      leadT: 0.5,
      trail: [],
      heading: 0,
      px: null,
      py: null,
    });
  });
  rollingstock.carriages.forEach((cd) => {
    cars.push({
      id: cd.label,
      name: cd.name ?? cd.label,
      color: toHexColor(cd.color || "darkgray"),
      type: "car",
      length: +cd.length || 15,

      leadPath: [],
      leadT: 0.5,
      trail: [],
      heading: 0,
      px: null,
      py: null,
    });
  });
}

// ---------- Start positions -> Consists ----------
function getVehicleById(id) {
  return (
    locos.find((v) => v.id === id) || cars.find((v) => v.id === id) || null
  );
}

// Create a consist from an array of VEHICLE OBJECTS (front->back order)
function makeConsist(
  vehicles,
  liveLocoId = null,
  initialPath = null,
  initialT = 0.5,
  dir = +1,
  options = {}
) {
  const { pivotIndex = null, preserveWorld = false } = options;

  const vs = vehicles.slice();

  // Seed only what was given (pivot) unless preserving world
  if (!preserveWorld) {
    vs.forEach((v) => {
      if (v.leadPath && v.leadPath.length === 2) {
        // keep the path seeded in placeStartConsists (pivot)
        v.leadPath = v.leadPath.slice();
        v.leadT = v.leadT ?? initialT ?? 0.5;
      } else {
        // leave unseeded; will be solved from pivot later
        v.leadPath = null;
        v.leadT = null;
      }
    });
  }

  // Ensure a trail exists (safe no-op if already present)
  vs.forEach((v) => {
    if (!v.trail || !v.trail.length) {
      if (v.leadPath && v.leadPath.length === 2) {
        const [a, b] = v.leadPath;
        const seg = segmentVector(a, b);
        v.trail = seg ? [{ from: a, to: b, len: seg.len }] : [];
      } else {
        v.trail = [];
      }
    }
  });

  // Choose live loco if none explicitly given
  if (!liveLocoId) {
    const firstL = vs.find((x) => x.type === "loco");
    liveLocoId = firstL?.id ?? null;
  }
  vs.forEach((v) => {
    if (v.type === "loco") v.neutral = v.id !== liveLocoId;
  });

  const c = {
    id: `T${nextConsistId++}`,
    vehicles: vs,
    liveLocoId,
    dir,
    speed: SPEED,
    throttle: 0.5,
    moving: false,
    nudging: false,
    status: "",
    statusAlpha: 0,
    anchorIndex: typeof pivotIndex === "number" ? pivotIndex : null,
  };

  // Solve trailing vehicle positions if we didn’t preserve
  if (!preserveWorld) solveConsistPositions(c);

  return c;
}


function doesSegmentExist(A, B) {
  const adj = trackLayout.adj;
  return !!(adj && adj[A] && adj[A].includes(B));
}

function oneHopVia(A, B) {
  const adj = trackLayout.adj || {};
  return (adj[A] || []).find((x) => (adj[x] || []).includes(B)) || null;
}
function placeStartConsists(task) {
  consists = [];
  const startList = task?.startsegments || [];

  for (const entry of startList) {
    const [A, B] = String(entry.segment || "").split("-");
    if (!trackLayout.tracks[A] || !trackLayout.tracks[B]) {
      console.error("[placeStartConsists] bad segment", entry.segment);
      continue;
    }

    const ids = (entry.stock || []).filter(Boolean);
    if (!ids.length) continue;

    const vehicles = ids.map(getVehicleById).filter(Boolean);
    if (!vehicles.length) continue;

    // canonical path for A–B
    const canonKey = [A, B].sort().join("-");
    const canonPath =
      trackLayout.canonicalSegments?.[canonKey]?.forward?.slice() || [A, B];

    const segLen = dist(A, B);

    // pivot placement
    let t = 0.5;
    if (typeof entry.distance === "number" && segLen > 0) {
      t = Math.max(0, Math.min(1, entry.distance / segLen));
    }

    const pivot = vehicles[0];
    pivot.leadPath = canonPath.slice();
    pivot.leadT = t;

    // --- place trailing cars properly behind pivot ---
    for (let i = 1; i < vehicles.length; i++) {
      const car = vehicles[i];
      const ahead = vehicles[i - 1];
      let distance = spacing(ahead, car);

      let path = ahead.leadPath.slice();
      let tt = ahead.leadT;
      car.leadPath = null;
      car.leadT = null;

      while (distance > 0) {
        const result = walkAlong(path, tt, distance, -1);

        if (result && result.t >= 0 && result.t <= 1) {
          // landed inside this segment
          car.leadPath = result.seg.slice();
          car.leadT = result.t;
          distance = 0;
        } else {
          // overshoot → compute leftover and step into previous leg
          const segLen = dist(...path);
          const remainingToNearEnd = tt * segLen;
          const leftover = distance - remainingToNearEnd;

          // if we are exactly on a node, accept and stop
          if (Math.abs(leftover) < 1e-6) {
            car.leadPath = path.slice();
            car.leadT = 0;  // exactly at node
            distance = 0;
            break;
          }

          const [X, Y] = path;
          const prevNode = (tt <= 0) ? X : Y;
          const cameFrom = (tt <= 0) ? Y : X;

          const newLeg = buildLeg(prevNode, cameFrom);
          if (!newLeg) {
            console.error(
              `[placeStartConsists] No previous leg from ${prevNode} for ${car.id}`
            );
            break;
          }

          path = newLeg;
          tt = 1;          // start from far end of new segment
          distance = leftover;
        }
      }

      if (!car.leadPath) {
        console.error(
          `[placeStartConsists] Failed to place ${car.id} behind ${ahead.id}`
        );
      }
    }

    // choose live loco
    const liveLoco = vehicles.find(v => v.type === "loco")?.id ?? pivot.id;

    // build consist (preserve seeded leadPath/leadT)
    const consist = makeConsist(
      vehicles,
      liveLoco,
      canonPath,
      t,
      +1,
      { preserveWorld: true }
    );

    // compute px/py for all vehicles
    solveConsistPositions(consist);

    consists.push(consist);
    console.log(
      `[placeStartConsists] Built consist ${consist.id}, liveLoco=${liveLoco}, vehicles=${vehicles.map(v => v.id).join(", ")}`
    );

    // --- DEBUG TABLE ---
    console.table(vehicles.map(v => ({
      id: v.id,
      leadPath: v.leadPath ? v.leadPath.join("-") : null,
      leadT: v.leadT,
      px: v.px,
      py: v.py
    })));
  }

  buildControlsFromConsists();
  drawNetwork("3");
}

/* before track fix
function placeStartConsists(task) {
  consists = [];
  nextConsistId = 1;

  if (task) {
    for (const entry of task.startsegments) {
      const [A, B] = String(entry.segment || "").split("-");

      if (!trackLayout.tracks[A] || !trackLayout.tracks[B]) continue;

      // node existence
      if (!trackLayout.tracks[A] || !trackLayout.tracks[B]) {
        const missing = [A, B].filter((n) => !trackLayout.tracks[n]).join(", ");
        console.error(
          `[startsegments] Unknown node(s) ${missing} in "${segStr}" for [${(
            entry.stock || []
          ).join(", ")}]; skipping.`
        );
        continue;
      }

      // segment existence (fast, via adj)
      if (!doesSegmentExist(A, B)) {
        const via = oneHopVia(A, B);
        console.error(
          `[startsegments] Segment "${A}-${B}" does not exist; skipping stock [${(
            entry.stock || []
          ).join(", ")}].`
        );
        if (via)
          console.info(
            `  Hint: now routed via "${via}". Try "${A}-${via}" or "${via}-${B}".`
          );
        continue;
      }

      const ids = (entry.stock || []).filter(Boolean);
      if (!ids.length) continue;

      const vehicles = ids.map(getVehicleById).filter(Boolean);
      if (!vehicles.length) continue;

      const firstLoco = vehicles.find((v) => v.type === "loco")?.id ?? null;

      // segment info
      const segLen = dist(A, B);
      const path = [A, B];

      // default: centre of segment
      let t = 0.5;

      // override with distance if provided
      if (typeof entry.distance === "number" && segLen > 0) {
        t = Math.max(0, Math.min(1, entry.distance / segLen));
      }

      // seed common path & trail for all vehicles
      vehicles.forEach((v) => {
        v.leadPath = path.slice();
        v.leadT = t;
        v.trail = [{ from: A, to: B, len: segLen }];
      });

      // make consist with anchor at t
      const c = makeConsist(vehicles, firstLoco, path, t, +1);
      // stash path + anchor so calculateStartSegments can use them
      c.path = path;
      c.anchorT = t;
      //ensureRoomBehindConsist(c);
      solveConsistPositions(c);
      consists.push(c);
    }
  }
  buildControlsFromConsists();
  drawNetwork("3");
}
  */
/* orienting all vehicles the same on the line was a bodge
function placeStartConsists(task) {
  consists = [];
  nextConsistId = 1;

  if (!task) return;

  for (const entry of task.startsegments) {
    let [A, B] = String(entry.segment || "").split("-");
    if (!A || !B) continue;

    // --- 1️⃣  node existence check ---
    if (!trackLayout.tracks[A] || !trackLayout.tracks[B]) {
      const missing = [A, B].filter(n => !trackLayout.tracks[n]).join(", ");
      console.error(`[startsegments] Unknown node(s) ${missing} in "${A}-${B}"; skipping.`);
      continue;
    }

    // --- 2️⃣  verify segment exists ---
    if (!doesSegmentExist(A, B)) {
      const via = oneHopVia(A, B);
      console.error(`[startsegments] Segment "${A}-${B}" does not exist; skipping stock [${(entry.stock || []).join(", ")}].`);
      if (via)
        console.info(`  Hint: now routed via "${via}". Try "${A}-${via}" or "${via}-${B}".`);
      continue;
    }

    // --- 3️⃣  normalise segment direction using canonical map ---
    const segLen = dist(A, B);
    const key = [A, B].sort().join("-");
    const canon = trackLayout.canonicalSegments?.[key];
    if (canon && canon.forward) {
      const [CA, CB] = canon.forward;
      if (A !== CA || B !== CB) {
        // segment was entered reversed → flip it
        [A, B] = [B, A];
        if (typeof entry.distance === "number" && segLen > 0) {
          entry.distance = Math.max(0, segLen - entry.distance);
        }
      }
    }

    // --- 4️⃣  collect vehicle objects ---
    const ids = (entry.stock || []).filter(Boolean);
    if (!ids.length) continue;
    const vehicles = ids.map(getVehicleById).filter(Boolean);
    if (!vehicles.length) continue;

    const firstLoco = vehicles.find(v => v.type === "loco")?.id ?? null;

    // --- 5️⃣  position along segment ---
    let t = 0.5;
    if (typeof entry.distance === "number" && segLen > 0) {
      t = Math.max(0, Math.min(1, entry.distance / segLen));
    }

    const path = [A, B];

    // --- 6️⃣  seed geometry for all vehicles ---
    vehicles.forEach(v => {
      v.leadPath = path.slice();
      v.leadT = t;
      v.trail = [{ from: A, to: B, len: segLen }];
    });

    // --- 7️⃣  build consist and solve ---
    console.log(`[start] ${A}-${B}  t=${t.toFixed(2)}  segLen=${segLen}  stock=${ids}`);

    const c = makeConsist(vehicles, firstLoco, path, t, +1);
    c.path = path;
    c.anchorT = t;
    solveConsistPositions(c);

    consists.push(c);
  }

  buildControlsFromConsists();
  drawNetwork("3");
}
*/




// ensure there's room behind the pivot vehicle
function ensureRoomBehindConsist(consist) {
  const { pivotIndex, needBehind } = requiredDistancesConsist(consist);
  const pivot = consist.vehicles[pivotIndex];
  const seg = pivot?.trail?.slice(-1)[0];
  if (!seg) return;
  const segLen = seg.len;
  const available = (pivot.leadT ?? 0.5) * segLen;
  if (available < needBehind) {
    pivot.leadT = Math.min(0.98, needBehind / segLen + 0.02);
  }
}

// ---------- Controls ----------
function updateCollisionCounter() {
  const el = document.getElementById("collisionCounter");
  if (el) el.textContent = "Hits: " + collisions;
}

function buildControlsFromConsists() {
  const el = document.getElementById("controls");
  if (!el) return;
  el.innerHTML = "";

  // Consists that have at least one loco → Train numbering
  /*const trains = consists
    .map(c => ({ c, hasLoco: c.vehicles.some(v => v.type === "loco") }))
    .filter(x => x.hasLoco)
    .map((x, i) => ({ ...x, trainNo: i + 1 }));*/

  // Build an index lookup from loco label to its order
  const locoOrder = {};
  rollingstock.locomotives.forEach((loco, i) => {
    locoOrder[loco.label] = i;
  });

  const trains = consists
    .map((c) => {
      //console.log(c);
      // collect all loco labels in this consist
      const locoIds = c.vehicles
        .filter((v) => v.type === "loco")
        .map((v) => v.id);
      if (locoIds.length === 0) return null;

      // find the earliest loco index from locoOrder
      let keyIndex = Infinity;
      for (const id of locoIds) {
        const idx = locoOrder[id];
        if (idx !== undefined && idx < keyIndex) {
          keyIndex = idx;
        }
      }

      return { c, locoIds, keyIndex };
    })
    .filter((x) => x !== null)
    .sort((a, b) => a.keyIndex - b.keyIndex)
    .map((x, i) => ({
      ...x,
      trainNo: i + 1, // sequential numbering after sort
    }));

  for (const { c, trainNo } of trains) {
    const row = document.createElement("div");
    row.style.margin = "4px";

    const live = c.vehicles.find(
      (v) => v.id === c.liveLocoId && v.type === "loco"
    );
    const firstLoco = c.vehicles.find((v) => v.type === "loco");
    const labelColor = live?.color || firstLoco?.color || "gray";
    //    const direction = c.dir === 1 ? "(Forward)" : "(Reverse)";
    const who = live
      ? live.name || live.id
      : firstLoco?.name || firstLoco?.id || "no live loco";

    // Clickable colored label (flips direction / promotes first loco if none)
    const label = document.createElement("span");
    label.className = "train-label";
    label.style.backgroundColor = labelColor;
    label.style.color = bestTextColor(labelColor);
    //label.style.padding = "8px 8px";
    /*label.style.marginRight = "6px";
    label.style.borderRadius = "4px";
    label.style.minHeight = "2em";*/
    label.title = "Train";

    // Put the arrow visibly inside the label
    //label.textContent = `Train ${trainNo}: ${who}`;
    label.textContent = `${who}`;
    /*
    label.onclick = (e) => {
      // If no live loco yet, promote the first loco in the consist
      if (!c.liveLocoId) {
        if (firstLoco) {
          c.liveLocoId = firstLoco.id;
          c.vehicles.forEach(x => { if (x.type === "loco") x.neutral = (x.id !== firstLoco.id); });
        }
      } else {
        // Flip direction
        c.dir *= -1;
        // Refresh UI no need to do a complete rebuild
        if(c.dir===1){
          e.target.textContent=e.target.textContent.replace("Reverse","Forward");
        }else{
          e.target.textContent=e.target.textContent.replace("Forward","Reverse");
        }

  
      }
      // Recompute headings immediately so the canvas arrow flips now
      if (typeof solveConsistPositions === "function") {
        solveConsistPositions(c);
      } else if (typeof solveConsist === "function") {
        // fallback if you're still using the loco-based solver
        const liveL = c.vehicles.find(v => v.id === c.liveLocoId);
        if (liveL) solveConsist(liveL);
      }

      drawNetwork();
    };*/

    row.appendChild(label);
    // === NEW: FWD REV buttons ===
    // --- Direction buttons ---
    const fwdBtn = document.createElement("button");
    fwdBtn.textContent = "Fwd ↑";
    fwdBtn.className = "fwd-btn";

    const revBtn = document.createElement("button");
    revBtn.textContent = "Rev ↓";
    revBtn.className = "rev-btn";

    // Initialise active state
    if (c.dir === 1) {
      fwdBtn.classList.add("active");
    } else {
      revBtn.classList.add("active");
    }

    // Handlers
    fwdBtn.onclick = () => {
      if (c.dir == -1) {
        c.dir = 1;
        drawNetwork("4");
      }

      fwdBtn.classList.add("active");
      revBtn.classList.remove("active");
    };

    revBtn.onclick = () => {
      if (c.dir == 1) {
        c.dir = -1;
        drawNetwork("5");
      }

      revBtn.classList.add("active");
      fwdBtn.classList.remove("active");
    };

    // Add them to row BEFORE throttle
    row.appendChild(fwdBtn);
    row.appendChild(revBtn);

    // === NEW: Throttle slider ===

    const throttleWrap = document.createElement("div");
    // Value label
    /*const throttleLabel = document.createElement("span");
throttleLabel.style.marginLeft = "6px";
throttleLabel.textContent = c.throttle.toFixed(2);  // show 2 decimals
throttleWrap.appendChild(throttleLabel);*/

    throttleWrap.style.position = "relative";
    throttleWrap.style.display = "inline-block";
    throttleWrap.style.width = "200px"; // fixed width so we can place the tick
    throttleWrap.style.marginRight = "10px";

    const throttle = document.createElement("input");
    throttle.type = "range";
    throttle.min = 0.1;
    throttle.max = 1;
    throttle.step = 0.05;
    throttle.value = c.throttle;
    throttle.style.width = "100%";
    throttle.title = "Throttle";
    throttle.className = "throttle";

    throttle.oninput = () => {
      c.throttle = parseFloat(throttle.value);
      //throttleLabel.textContent=c.throttle.toFixed(2);
    };
    throttleWrap.appendChild(throttle);

    // Add tick marker

    const safePct =
      ((AUTOCONNECT_THROTTLE - throttle.min) / (throttle.max - throttle.min)) *
      100;

    const marker = document.createElement("div");
    marker.style.position = "absolute";
    marker.style.top = "0";
    marker.style.bottom = "0";
    marker.style.left = safePct + "%";
    marker.style.left = `calc(${safePct}% + 8px)`; // shift left by half thumb width

    marker.style.width = "2px";
    marker.style.background = GO_COLOUR;
    marker.style.pointerEvents = "none"; // don’t block slider

    throttleWrap.appendChild(marker);

    row.appendChild(throttleWrap);

    // Run button
    const runBtn = document.createElement("button");
    runBtn.textContent = "Run";
    runBtn.className = "run-btn";
    const stopBtn = document.createElement("button");

    runBtn.onclick = () => {
      if (!c.liveLocoId && firstLoco) {
        c.liveLocoId = firstLoco.id;
        c.vehicles.forEach((x) => {
          if (x.type === "loco") x.neutral = x.id !== firstLoco.id;
        });
         solveConsistPositions(c);
      }
      runBtn.classList.add("active");
      stopBtn.classList.remove("active");
      runConsist(c);
      //buildControlsFromConsists();
    };

    // Stop button

    stopBtn.textContent = "Stop";
    stopBtn.className = "stop-btn";

    if (c.moving) {
      runBtn.classList.add("active");
      stopBtn.classList.remove("active");
    } else {
      stopBtn.classList.add("active");
      runBtn.classList.remove("active");
    }
    row.appendChild(runBtn);
    row.appendChild(stopBtn);
    stopBtn.onclick = () => {
      panToFollow;
      stopBtn.classList.add("active");
      runBtn.classList.remove("active");
      //setStatusC(c, "Stopped: manual");
      stopConsist(c);
      //buildControlsFromConsists();
      //drawNetwork();
    };

    const followBtn = document.createElement("button");
    followBtn.className = "follow-btn";

    if (followConsistId === c.id) {
      followBtn.textContent = FOLLOWING_TRAIN_STRING;
      followBtn.classList.add("active");
    } else {
      followBtn.textContent = FOLLOW_TRAIN_STRING;
    }
    followBtn.textContent =
      followConsistId === c.id ? FOLLOWING_TRAIN_STRING : FOLLOW_TRAIN_STRING;

    followBtn.style.marginLeft = "5px";
    followBtn.onclick = () => {
      if (followConsistId === c.id) {
        followConsistId = null; // toggle off
        followBtn.textContent = FOLLOW_TRAIN_STRING;
        followBtn.classList.remove("active");
      } else {
        followConsistId = c.id;
        // reset all other follow buttons
        document.querySelectorAll(".follow-btn").forEach((b) => {
          b.textContent = FOLLOW_TRAIN_STRING;
          b.classList.remove("active");
        });
        followBtn.textContent = FOLLOWING_TRAIN_STRING;
        followBtn.classList.add("active");
        drawNetwork("6");
      }
    };
    followBtn.classList.add("follow-btn");
    row.appendChild(followBtn);

    el.appendChild(row);
  }

  /*
  const debugRow = document.createElement("div");
  debugRow.style.margin = "6px 0";

  const chk = document.createElement("input");
  chk.type = "checkbox";
  chk.id = "toggle-nnodes";
  chk.checked = showNNodes;
  chk.onchange = () => { showNNodes = chk.checked; drawNetwork(); };

  const lbl = document.createElement("label");
  lbl.htmlFor = "toggle-nnodes";
  lbl.textContent = " Show hidden nodes (for debugging and track construcion)";

  debugRow.appendChild(chk);
  debugRow.appendChild(lbl);
  el.appendChild(debugRow);
  */
}

function stopConsist(consist) {
  consist.moving = false;
  if (rafIds[consist.id]){

   cancelAnimationFrame(rafIds[consist.id]);
   delete rafIds[consist.id];
  }
  buildControlsFromConsists();
  drawNetwork("7");
}

// ---------- Switch logic ----------
function getActiveLegs(pid) {
  // Use the JSON order for P*: [fixed, legA, legB]
  const c = trackLayout.tracks[pid].connections;
  if (!c || c.length < 3) return null;
  const fixed = c[0];
  const active = c[1 + (switchStates[pid] % 2)];
  const inactive = c.find((n) => n !== fixed && n !== active);
  return { fixed, active, inactive };
}

function chooseNextFrom(node, prev) {
  if (!trackLayout.tracks[node]) return null;
  if (signalStates[node]) return null; // blocked buffer

  if (trackLayout.tracks[node].type == "point") {
    const legs = getActiveLegs(node);
    if (!legs) return null;
    const { fixed, active, inactive } = legs;
    if (prev === inactive) return null; // can't enter via inactive
    let nxt = null;
    if (prev === fixed) nxt = active;
    else if (prev === active) nxt = fixed;
    else if (!prev) nxt = fixed; // starting on a point
    if (!nxt) return null;
    if (!isBogieTurnAllowed(node, prev, nxt)) return null;
    return nxt;
  }

  // Non-P: pick the neighbor that's not 'prev'
  const nbrs = trackLayout.adj[node] || [];
  // deg=1 → end; deg=2 → pass-through; deg>2 (shouldn't happen for non-P) → pick any not prev
  return nbrs.find((n) => n !== prev) || null;
}

function clamp(x, a, b) {
  return Math.max(a, Math.min(b, x));
}
function isBogieTurnAllowed(pid, prev, next) {
  if (!trackLayout.tracks[pid].type == "point" || !prev || !next) return true;
  const [px, py] = trackLayout.tracks[prev].position;
  const [cx, cy] = trackLayout.tracks[pid].position;
  const [nx, ny] = trackLayout.tracks[next].position;
  const v1 = [px - cx, py - cy],
    v2 = [nx - cx, ny - cy];
  const cos = clamp(
    (v1[0] * v2[0] + v1[1] * v2[1]) / (Math.hypot(...v1) * Math.hypot(...v2)),
    -1,
    1
  );
  return (Math.acos(cos) * 180) / Math.PI >= 90;
}

function buildLeg(at, from) {
  const nxt = chooseNextFrom(at, from);
  return nxt ? [at, nxt] : [];
}

// ---------- Geometry ----------
function pointInVehicle(mx, my, v) {
  if (v.px == null) return false;
  const dx = mx - v.px,
    dy = my - v.py;
  const c = Math.cos(v.heading),
    s = Math.sin(v.heading);
  const localX = dx * c + dy * s; // along length
  const localY = -dx * s + dy * c; // across width
  const halfL = v.length / 2;
  const isLoco = v.type === "loco";
  const halfW = (isLoco ? v.width : CAR_WIDTH) / 2;
  return Math.abs(localX) <= halfL && Math.abs(localY) <= halfW;
}
function getLayoutBounds() {
  let minX = Infinity,
    minY = Infinity,
    maxX = -Infinity,
    maxY = -Infinity;
  for (const id in trackLayout.tracks) {
    const [x, y] = trackLayout.tracks[id].position;
    minX = Math.min(minX, x);
    minY = Math.min(minY, y);
    maxX = Math.max(maxX, x);
    maxY = Math.max(maxY, y);
    if (typeof trackLayout.tracks[id].label === "string") {
      //bodge a bit more for buffertext
      margin = trackLayout.tracks[id].label.length * 1.5;
      minX = Math.min(minX, x - margin);
      minY = Math.min(minY, y - margin);
      maxX = Math.max(maxX, x + margin);
      maxY = Math.max(maxY, y + margin);
    }
  }
  return { minX, minY, maxX, maxY };
}
function dist(a, b) {
  if (trackLayout.lengths) {
    const k1 = `${a}-${b}`,
      k2 = `${b}-${a}`;
    if (trackLayout.lengths[k1] != null) return trackLayout.lengths[k1];
    if (trackLayout.lengths[k2] != null) return trackLayout.lengths[k2];
  }
  const [ax, ay] = trackLayout.tracks[a].position,
    [bx, by] = trackLayout.tracks[b].position;
  return Math.hypot(bx - ax, by - ay);
}
function segmentVector(a, b) {
  const [x1, y1] = trackLayout.tracks[a].position,
    [x2, y2] = trackLayout.tracks[b].position;
  const len = Math.hypot(x2 - x1, y2 - y1);
  const physLen = dist(a, b);
  return { dx: x2 - x1, dy: y2 - y1, len, physLen };
}


// --- STRICT point sampler ---
// Never reorders the segment, never flips t.
// Always treats [A,B] literally as "A → B".
function pointOnSegStrict(A, B, t) {
  // clamp t just to be safe
  if (t < 0) t = 0;
  if (t > 1) t = 1;

  const [ax, ay] = trackLayout.tracks[A].position;
  const [bx, by] = trackLayout.tracks[B].position;

  const px = ax + (bx - ax) * t;
  const py = ay + (by - ay) * t;
  const heading = Math.atan2(by - ay, bx - ax); // orientation A→B

  return { px, py, heading };
}

//afterfixing track heading
function pointOnSegment(a, b, t) {
  const key = [a, b].sort().join("-");
  const canon = trackLayout.canonicalSegments?.[key];
  let [ca, cb] = canon ? canon.forward : [a, b];

  const [x1, y1] = trackLayout.tracks[ca].position;
  const [x2, y2] = trackLayout.tracks[cb].position;

  return {
    px: x1 + (x2 - x1) * t,
    py: y1 + (y2 - y1) * t,
    heading: Math.atan2(y2 - y1, x2 - x1)  // stable, canonical
  };
}

/*befor fixing track heading
function pointOnSegment(a, b, t) {
  const [x1, y1] = trackLayout.tracks[a].position,
    [x2, y2] = trackLayout.tracks[b].position;
  return {
    px: x1 + (x2 - x1) * t,
    py: y1 + (y2 - y1) * t,
    heading: Math.atan2(y2 - y1, x2 - x1),
  };
}*/






function getVehicleRect(v, isLoco = false) {
  const w = isLoco ? v.width : CAR_WIDTH;
  const l = v.length;
  const hw = w / 2,
    hl = l / 2,
    cos = Math.cos(v.heading),
    sin = Math.sin(v.heading);
  const pts = [
    { x: -hl, y: -hw },
    { x: hl, y: -hw },
    { x: hl, y: hw },
    { x: -hl, y: hw },
  ];
  return pts.map((p) => ({
    x: v.px + p.x * cos - p.y * sin,
    y: v.py + p.x * sin + p.y * cos,
  }));
}
function rectsIntersect(r1, r2) {
  const polys = [r1, r2];
  for (const poly of polys) {
    for (let i = 0; i < poly.length; i++) {
      const j = (i + 1) % poly.length;
      const edge = { x: poly[j].x - poly[i].x, y: poly[j].y - poly[i].y };
      const axis = { x: -edge.y, y: edge.x };
      const len = Math.hypot(axis.x, axis.y);
      const nx = axis.x / len,
        ny = axis.y / len;
      let min1 = Infinity,
        max1 = -Infinity;
      for (const p of r1) {
        const pr = p.x * nx + p.y * ny;
        min1 = Math.min(min1, pr);
        max1 = Math.max(max1, pr);
      }
      let min2 = Infinity,
        max2 = -Infinity;
      for (const p of r2) {
        const pr = p.x * nx + p.y * ny;
        min2 = Math.min(min2, pr);
        max2 = Math.max(max2, pr);
      }
      if (max1 < min2 || max2 < min1) return false;
    }
  }
  return true;
}

// ---------- Walking / Trail ----------
//after fixing heading
function walkAlong(path, t, dist, dir) {
  if (!Array.isArray(path) || path.length !== 2) {
    console.warn("[walkAlong] bad seg:", path);
    return null;
  }
  if (dir !== 1 && dir !== -1) {
    console.warn("[walkAlong] bad dir (must be ±1):", dir);
    return null;
  }

  const [A, B] = path;
  const seg = segmentVector(A, B);
  if (!seg || seg.physLen <= 0) {
    console.warn("[walkAlong] invalid segment:", A, B);
    return null;
  }

  // distance → fraction of segment
  const deltaT = (dist / seg.physLen) * dir;
  let newT = t + deltaT;

  // clamp for now
  if (newT < 0) newT = 0;
  if (newT > 1) newT = 1;

  // interpolate coords
  const [ax, ay] = trackLayout.tracks[A].position;
  const [bx, by] = trackLayout.tracks[B].position;
  const px = ax + (bx - ax) * newT;
  const py = ay + (by - ay) * newT;

  // heading along A→B, flip if dir is backwards
  let heading = Math.atan2(by - ay, bx - ax);
  if (dir === -1) heading = (heading + Math.PI) % (2 * Math.PI);

  return { seg: [A, B], t: newT, px, py, heading, stepDir: dir };
}



//before fixing heading
/*
function walkAlong(path, t, d, dir) {
  let [a, b] = path.slice();
  let { len } = segmentVector(a, b);
  let pos = t * len + (dir === 1 ? d : -d);
  while (pos > len + EPS || pos < -EPS) {
    if (dir === +1 && pos > len + EPS) {
      const nxt = chooseNextFrom(b, a);
      if (!nxt) {
        pos = len;
        break;
      }
      pos -= len;
      a = b;
      b = nxt;
      ({ len } = segmentVector(a, b));
    } else if (dir === -1 && pos < -EPS) {
      const prev = chooseNextFrom(a, b);
      if (!prev) {
        pos = 0;
        break;
      }
      b = a;
      a = prev;
      ({ len } = segmentVector(a, b));
      pos += len;
    } else break;
  }
  const newT = clamp(pos / len, 0, 1);
  return { path: [a, b], t: newT, ...pointOnSegment(a, b, newT) };
}
*/

function ensureTrailHas(v) {
  if (!v || !Array.isArray(v.leadPath) || v.leadPath.length < 2) {
    console.warn("ensureTrailHas: bad leadPath for", v?.id, v?.leadPath);
    return;
  }

  const [a, b] = v.leadPath;
  const seg = segmentVector(a, b);
  if (!seg) {
    console.warn("ensureTrailHas: no segment vector for", a, b, "on", v?.id);
    return;
  }

  const len = seg.len;
  const last = v.trail?.[v.trail.length - 1];

  if (!last || last.from !== a || last.to !== b || last.len !== len) {
    if (!v.trail) v.trail = [];
    v.trail.push({ from: a, to: b, len });
  }
}



function trimBackTrail(v, needBack) {
  let acc = 0;
  for (let i = v.trail.length - 1; i >= 0; i--) {
    acc += v.trail[i].len;
    if (acc >= needBack) {
      v.trail = v.trail.slice(i);
      return;
    }
  }
}

// ---------- Consist solving ----------
function spacing(prev, curr) {
  return prev.length / 2 + GAP + curr.length / 2;
}

function getPivotIndex(consist) {
  if (consist.liveLocoId) {
    const i = consist.vehicles.findIndex((v) => v.id === consist.liveLocoId);
    if (i >= 0) return i;
  }
  if (typeof consist.anchorIndex === "number") {
    return Math.max(
      0,
      Math.min(consist.anchorIndex, consist.vehicles.length - 1)
    );
  }
  return 0;
}

function requiredDistancesConsist(consist) {
  const pivotIndex = getPivotIndex(consist);
  let needBehind = 0,
    needAhead = 0;

  let oldPivotLength = consist.vehicles[pivotIndex].length;
  if (justMerged) {
    consist.vehicles[pivotIndex].length = oldPivotLength / 2; //only half the pivot required
  }

  for (let i = pivotIndex + 1; i < consist.vehicles.length; i++)
    needBehind += spacing(consist.vehicles[i - 1], consist.vehicles[i]);
  for (let i = pivotIndex - 1; i >= 0; i--)
    needAhead += spacing(consist.vehicles[i], consist.vehicles[i + 1]);
  consist.vehicles[pivotIndex].length = oldPivotLength; //put it back
  needBehind -= GAP;
  needAhead -= GAP;
  return { pivotIndex, needBehind, needAhead };
}

// --- Canonical orientation helper ---
// Ensures seg is in canonical order and adjusts t accordingly.
// If a custom canonical map exists (trackLayout.canon[key].forward = [X,Y]),
// use it. Otherwise fall back to simple lexicographic ordering.
function normaliseCanonical(seg, t) {
  if (!Array.isArray(seg) || seg.length !== 2) return { seg, t };
  let [a, b] = seg;

  // Try custom canonical map first
  const key = [a, b].slice().sort().join("-");
  const canon = trackLayout.canon && trackLayout.canon[key];
  if (canon && Array.isArray(canon.forward) && canon.forward.length === 2) {
    const [c0, c1] = canon.forward;
    // Already canonical
    if (a === c0 && b === c1) {
      return { seg: [a, b], t };
    }
    // Reversed: flip t
    if (a === c1 && b === c0) {
      return { seg: [c0, c1], t: 1 - t };
    }
    // If the segment uses unexpected ids, fall back to lexicographic.
  }

  // Fallback: lexicographic canonical ordering
  if (String(a) <= String(b)) {
    return { seg: [a, b], t };
  } else {
    return { seg: [b, a], t: 1 - t };
  }
}

function travelAcross(seg, t, dist, dir) {
  // seg = [A,B], t in [0,1], dist in world units, dir = ±1
  const [A, B] = seg;

  const ax0 = trackLayout.tracks[A].position[0];
  const ay0 = trackLayout.tracks[A].position[1];
  const bx0 = trackLayout.tracks[B].position[0];
  const by0 = trackLayout.tracks[B].position[1];

  const segLen0 = Math.hypot(bx0 - ax0, by0 - ay0);
  const EPS_T = 1e-3;

  let remaining = dist;
  let currSeg = [A, B];
  let currT = t;

  while (remaining > 0) {
    // distance available in current segment in the chosen direction
    const dx = trackLayout.tracks[currSeg[1]].position[0] - trackLayout.tracks[currSeg[0]].position[0];
    const dy = trackLayout.tracks[currSeg[1]].position[1] - trackLayout.tracks[currSeg[0]].position[1];
    const segLen = Math.hypot(dx, dy);

    const avail = dir > 0 ? (1 - currT) * segLen : currT * segLen;

    if (remaining <= avail + 1e-9) {
      // finish within this segment
      const step = (remaining / segLen) * dir;
      const newT = Math.max(EPS_T, Math.min(1 - EPS_T, currT + step));

      const ax = trackLayout.tracks[currSeg[0]].position[0];
      const ay = trackLayout.tracks[currSeg[0]].position[1];
      const bx = trackLayout.tracks[currSeg[1]].position[0];
      const by = trackLayout.tracks[currSeg[1]].position[1];
      const wx = ax + (bx - ax) * newT;
      const wy = ay + (by - ay) * newT;

      console.log(
        `[travel-debug] FINISH seg=${currSeg[0]}-${currSeg[1]}, dir=${dir}, t=${newT.toFixed(3)}, coords=(${wx.toFixed(2)},${wy.toFixed(2)}), remaining=0`
      );
      return { seg: currSeg.slice(), t: newT };
    }

    // consume this segment fully and hop to next
    remaining -= avail;

    const exitNode = dir > 0 ? currSeg[1] : currSeg[0];
    const fromNode = dir > 0 ? currSeg[0] : currSeg[1];

    const nextLeg = buildLeg(exitNode, fromNode);
    if (!nextLeg) {
      const nodeCoords = trackLayout.tracks[exitNode].position;
      console.log(
        `[travel-debug] DEAD END at ${exitNode}, seg=${currSeg[0]}-${currSeg[1]}, dir=${dir}, coords=(${nodeCoords[0].toFixed(2)},${nodeCoords[1].toFixed(2)})`
      );
      // park at boundary
      const stopT = dir > 0 ? 1 - EPS_T : EPS_T;
      return { seg: currSeg.slice(), t: stopT };
    }

    // ✅ Orient the next leg based on direction:
    // forward: exitNode must be first, seed t=0
    // backward: exitNode must be second, seed t=1
    let oriented;
    if (dir > 0) {
      oriented = nextLeg[0] === exitNode ? nextLeg : [nextLeg[1], nextLeg[0]];
      currSeg = oriented;
      currT = EPS_T; // just into the segment away from the node
    } else {
      oriented = nextLeg[1] === exitNode ? nextLeg : [nextLeg[1], nextLeg[0]];
      currSeg = oriented;
      currT = 1 - EPS_T; // just into the segment away from the node
    }

    const ax = trackLayout.tracks[currSeg[0]].position[0];
    const ay = trackLayout.tracks[currSeg[0]].position[1];
    const bx = trackLayout.tracks[currSeg[1]].position[0];
    const by = trackLayout.tracks[currSeg[1]].position[1];
    const wx = ax + (bx - ax) * currT;
    const wy = ay + (by - ay) * currT;

    console.log(
      `[travel-debug] HOP seg=${currSeg[0]}-${currSeg[1]}, dir=${dir}, seeded t=${currT.toFixed(3)}, coords=(${wx.toFixed(
        2
      )},${wy.toFixed(2)}), remaining=${remaining.toFixed(3)}`
    );
  }

  return { seg: currSeg.slice(), t: currT };
}


function solveConsistPositions(consist, index = null) {
  if (!consist || !consist.vehicles || !consist.vehicles.length) return;

  const pivotIndex = index != null ? index : getPivotIndex(consist);
  const pivot = consist.vehicles[pivotIndex];

  if (!pivot.leadPath || pivot.leadPath.length !== 2) {
    console.error(
      `[solveConsistPositions] ${consist.id}: pivot ${pivot?.id} has no valid leadPath`
    );
    return;
  }

  // --- place pivot (STRICT, no canonicalisation) ---
  ensureTrailHas(pivot);
  {
    const [A, B] = pivot.leadPath;
    const lc = pointOnSegStrict(A, B, pivot.leadT);
    pivot.px = lc.px;
    pivot.py = lc.py;
    pivot.heading = (lc.heading + 2 * Math.PI) % (2 * Math.PI);
    computeBogies(pivot);

    console.log(
      `[solve-debug] ${consist.id} PIVOT ${pivot.id} seg=${A}-${B}, t=${pivot.leadT.toFixed(
        3
      )}, coords=(${pivot.px.toFixed(2)},${pivot.py.toFixed(2)})`
    );
  }

  // --- vehicles behind pivot (tail) ---
  for (let i = pivotIndex + 1; i < consist.vehicles.length; i++) {
    const prev = consist.vehicles[i - 1];
    const curr = consist.vehicles[i];
    if (!prev.leadPath || prev.leadPath.length !== 2) {
      console.error(
        `[solveConsistPositions] ${consist.id}: prev ${prev?.id} has no valid path for ${curr?.id}`
      );
      continue;
    }

    const d = spacing(prev, curr);
    const pos = travelAcross(prev.leadPath, prev.leadT, d, -1);

    // keep orientation from travelAcross
    curr.leadPath = pos.seg;
    curr.leadT = pos.t;

    ensureTrailHas(curr);

    const [A, B] = curr.leadPath;
    const cc = pointOnSegStrict(A, B, curr.leadT);
    curr.px = cc.px;
    curr.py = cc.py;
    curr.heading = (cc.heading + 2 * Math.PI) % (2 * Math.PI);
    computeBogies(curr);

    console.log(
      `[solve-debug] ${consist.id} TAIL ${curr.id} seg=${A}-${B}, t=${curr.leadT.toFixed(
        3
      )}, coords=(${curr.px.toFixed(2)},${curr.py.toFixed(2)}), d=${d.toFixed(2)}`
    );
  }

  // --- vehicles ahead of pivot (front) ---
  for (let i = pivotIndex - 1; i >= 0; i--) {
    const next = consist.vehicles[i + 1];
    const curr = consist.vehicles[i];
    if (!next.leadPath || next.leadPath.length !== 2) {
      console.error(
        `[solveConsistPositions] ${consist.id}: next ${next?.id} has no valid path for ${curr?.id}`
      );
      continue;
    }

    const d = spacing(curr, next);
    const pos = travelAcross(next.leadPath, next.leadT, d, +1);

    // keep orientation from travelAcross
    curr.leadPath = pos.seg;
    curr.leadT = pos.t;

    ensureTrailHas(curr);

    const [A, B] = curr.leadPath;
    const cc = pointOnSegStrict(A, B, curr.leadT);
    curr.px = cc.px;
    curr.py = cc.py;
    curr.heading = (cc.heading + 2 * Math.PI) % (2 * Math.PI);
    computeBogies(curr);

    console.log(
      `[solve-debug] ${consist.id} FRONT ${curr.id} seg=${A}-${B}, t=${curr.leadT.toFixed(
        3
      )}, coords=(${curr.px.toFixed(2)},${curr.py.toFixed(2)}), d=${d.toFixed(2)}`
    );
  }
}

// ---------- Snapshot/restore (consist) ----------
function snapshotConsistStateC(consist) {
  return consist.vehicles.map((v) => ({
    id: v.id,
    leadPath: v.leadPath.slice(),
    leadT: v.leadT,
  }));
}
function restoreConsistStateC(consist, snap) {
  snap.forEach((s) => {
    const v = consist.vehicles.find((x) => x.id === s.id);
    if (v) {
      v.leadPath = s.leadPath.slice();
      v.leadT = s.leadT;
      ensureTrailHas(v);
    }
  });
  solveConsistPositions(consist);
}

// ---------- Run / Animate (consist) ----------
function setStatusC(consist, text, { hit = true } = {}) {
  if (hit && !consist.nudging) {
    collisions++;
    buildControlsFromConsists();
    updateCollisionCounter();
  }

  consist.status = text || "";
  consist.statusAlpha = consist.status ? 1 : 0;
}
function clearStatusC(consist) {
  if (consist.status && consist.statusAlpha === 0) consist.status = "";
}

function runConsist(consist) {
  if (consist.moving) return;
  const pivot = consist.vehicles[getPivotIndex(consist)];
  ensureTrailHas(pivot);
  consist.moving = true;
  clearStatusC(consist);
  if (rafIds[consist.id]) {
    cancelAnimationFrame(rafIds[consist.id]);
    delete rafIds[consist.id];
  }
  animateConsist(consist);
}

function vehicleFrontStop(v, dir) {
  const front = walkAlong(v.leadPath, v.leadT, v.length / 2 + STOP_MARGIN, dir);
  const [fa, fb] = front.seg;

  if (dir === 1 && front.t >= 1 - EPS) {
    const node = fb,
      prev = fa;
    if (signalStates[node])
      return { stop: true, reason: "buffer", node, vehicle: v.id };
    const nxt = chooseNextFrom(node, prev);
    if (!nxt) return { stop: true, reason: "end", node, vehicle: v.id };
  }
  if (dir === -1 && front.t <= EPS) {
    const node = fa,
      prev = fb;
    if (signalStates[node])
      return { stop: true, reason: "buffer", node, vehicle: v.id };
    const nxt = chooseNextFrom(node, prev);
    if (!nxt) return { stop: true, reason: "end", node, vehicle: v.id };
  }
  return { stop: false };
}

function animateConsist(consist) {
  if (!consists.find(c => c.id === consist.id)) return;
  if (!consist.moving) return;

  const pivotIndex = getPivotIndex(consist);
  const pivot = consist.vehicles[pivotIndex];
  if (!pivot || !pivot.leadPath || pivot.leadPath.length < 2) {
    consist.moving = false;
    return;
  }

  const [a, b] = pivot.leadPath;
  const seg = segmentVector(a, b);
  const dt = ((consist.throttle * consist.speed) / seg.physLen) * consist.dir;
  let nextT = pivot.leadT + dt;

  const EPS_T = EPS || 1e-3;
  const tBeforeFwd = 1 - EPS_T;
  const tBeforeBack = EPS_T;

  console.log(
    `[anim-debug] tick ${consist.id} seg=${a}-${b}, dir=${consist.dir}, t=${pivot.leadT.toFixed(3)} dt=${dt.toFixed(4)} segLen=${seg.physLen.toFixed(3)} thr=${consist.throttle.toFixed(3)} spd=${consist.speed.toFixed(3)} nextT=${nextT.toFixed(3)}`
  );

  let overshoot = 0;
  let boundaryT = null;

  if (nextT >= 1) {
    overshoot = nextT - 1;
    boundaryT = 1;
  } else if (nextT <= 0) {
    overshoot = -nextT;
    boundaryT = 0;
  }

  if (boundaryT !== null && overshoot > 0) {
    // about to cross a node
    const stopT = boundaryT === 1 ? tBeforeFwd : tBeforeBack;

    // collision check at the boundary
    if (willCollide(consist, stopT)) {
      consist.moving = false;
      pivot.leadT = stopT;
      ensureTrailHas(pivot);
      solveConsistPositions(consist);
      drawNetwork("stop-collision");
      return;
    }

    const exitNode = boundaryT === 1 ? b : a;
    const fromNode = boundaryT === 1 ? a : b;

    // leftover distance in world units across the boundary
    const leftoverDist = overshoot * seg.physLen;

    console.log(
      `[hop-debug] ${consist.id} exiting ${a}-${b} at ${exitNode}, dir=${consist.dir}, overshootT=${overshoot.toFixed(
        4
      )}, leftoverDist=${leftoverDist.toFixed(3)}`
    );

    // Build next leg
    const leg = buildLeg(exitNode, fromNode);

    if (leg && leg.length === 2) {
      // ✅ Orient by direction (same policy as travelAcross)
      let oriented;
      let seedT;
      if (consist.dir > 0) {
        oriented = leg[0] === exitNode ? leg : [leg[1], leg[0]];
        seedT = EPS_T;
      } else {
        oriented = leg[1] === exitNode ? leg : [leg[1], leg[0]];
        seedT = 1 - EPS_T;
      }

      // Walk any remaining distance *inside* the next segment using travelAcross,
      // starting at the boundary-seeded t.
      const hopped = travelAcross(oriented, seedT, leftoverDist, consist.dir);

      const [HA, HB] = hopped.seg;
      const ps = pointOnSegStrict(HA, HB, hopped.t);
      console.log(
        `[hop-debug] oriented leg=${oriented[0]}-${oriented[1]}, hop.t=${hopped.t.toFixed(3)} → clamped=(${ps.px.toFixed(
          2
        )},${ps.py.toFixed(2)})`
      );

      if (willCollide(consist, hopped.t, hopped.seg)) {
        consist.moving = false;
        pivot.leadT = stopT;
        ensureTrailHas(pivot);
      } else {
        pivot.leadPath = hopped.seg;
        pivot.leadT = hopped.t;
        ensureTrailHas(pivot);
        clearStatusC(consist);
      }
    } else {
      // end of line
      pivot.leadT = stopT;
      consist.moving = false;
      setStatusC(consist, `${STOPPED_STRING}: end of line`);
    }
  } else {
    // Normal advance within the same segment
    const clampedNextT = Math.max(EPS_T, Math.min(1 - EPS_T, nextT));
    if (willCollide(consist, clampedNextT)) {
      consist.moving = false;
    } else {
      pivot.leadT = clampedNextT;
      ensureTrailHas(pivot);
      clearStatusC(consist);
    }
  }

  // Solve vehicle positions
  solveConsistPositions(consist);

  // Position debug after solve
  console.log(
    `[solve-debug] ${consist.id} PIVOT ${pivot.id} seg=${pivot.leadPath[0]}-${pivot.leadPath[1]}, t=${pivot.leadT.toFixed(
      3
    )}, coords=(${pivot.px?.toFixed(2)},${pivot.py?.toFixed(2)})`
  );

  checkSignalEntry(consist);
  checkSignalClear(consist);
  if (!consist.moving) buildControlsFromConsists();

  // Front stop checks
  for (const v of consist.vehicles) {
    const s = vehicleFrontStop(v, consist.dir);
    if (s.stop) {
      consist.moving = false;
      const cause = s.reason === "buffer" ? bufferString(s.node) : s.node + " unable to proceed";
      setStatusC(consist, `${STOPPED_STRING}: ${v.name} at ${cause}`);
      drawNetwork("veh-stop");
      return;
    }
  }

  const { needBehind } = requiredDistancesConsist(consist);
  trimBackTrail(pivot, needBehind);

  if (!consists.find(c => c.id === consist.id)) return;
  drawNetwork("anim");
  rafIds[consist.id] = requestAnimationFrame(() => animateConsist(consist));
}


// ---------- Collision & Auto-coupling (between consists) ----------
function isContinuousSameTrack(v, o, dir) {
  const [va, vb] = v.leadPath,
    [oa, ob] = o.leadPath;
  let shared = null;
  if (va === oa || va === ob) shared = va;
  else if (vb === oa || vb === ob) shared = vb;
  if (!shared) return false;
  const otherO = oa === shared ? ob : ob === shared ? oa : null;
  if (!otherO) return false;
  if (dir === 1) {
    if (shared !== vb) return false;
    const next = chooseNextFrom(vb, va);
    return next === otherO;
  } else {
    if (shared !== va) return false;
    const next = chooseNextFrom(va, vb);
    return next === otherO;
  }
}

/* almost
function willCollide(consist, nextLeadT, nextLeadPath = null) {
  // preview pivot move
  const pivot = consist.vehicles[getPivotIndex(consist)];
  const snap = snapshotConsistStateC(consist);
  const oldPath = pivot.leadPath.slice();
  const oldT = pivot.leadT;

  if (nextLeadPath) pivot.leadPath = nextLeadPath;
  pivot.leadT = nextLeadT;
  ensureTrailHas(pivot);
  solveConsistPositions(consist);

  // collect vehicles from OTHER consists
  const others = [];
  for (const c of consists) {
    if (c.id === consist.id) continue;
    for (const v of c.vehicles) {
      if (v.px != null && v.py != null) others.push({ v, owner: c });
    }
  }

  let collide = false;
  let toMerge = null;     // the stationary consist we hit
  let hitInfo = null;     // {isApproaching, movingId, stationaryId}

  // detect overlap and approach
  for (const v of consist.vehicles) {
    const rectV = getVehicleRect(v, v.type === "loco" && v.id === consist.liveLocoId);
    for (const { v: o, owner } of others) {
      const rectO = getVehicleRect(o, o.type === "loco" && o.id === owner.liveLocoId);
      if (!rectsIntersect(rectV, rectO)) continue;

      const sameSeg =
        (v.leadPath[0] === o.leadPath[0] && v.leadPath[1] === o.leadPath[1]) ||
        (v.leadPath[0] === o.leadPath[1] && v.leadPath[1] === o.leadPath[0]);
      const continuous = isContinuousSameTrack(v, o, consist.dir);

      if (sameSeg || continuous) {
        const approach = isApproachingConsist(consist, owner);
        if (approach.isApproaching) {
          toMerge = owner;     // stationary
          hitInfo = approach;  // remember which couplers touched
        }
      } else {
        collide = true;
      }
      break;
    }
    if (collide || toMerge) break;
  }

  // two moving trains collide → stop both
  if (consist.moving && toMerge && toMerge.moving) {
    stopConsist(toMerge);
    stopConsist(consist);
    setStatusC(consist, `${STOPPED_STRING}: collision with moving train`);
    setStatusC(toMerge, `${STOPPED_STRING}: collision with moving train`);
    return;
  }

  // too fast for auto couple
  if (!consist.nudging && toMerge && consist.throttle > AUTOCONNECT_THROTTLE) {
    setStatusC(consist, `${STOPPED_STRING}: too fast for auto couple`);
    stopConsist(consist);
    return;
  }

  // restore preview state
  restoreConsistStateC(consist, snap);
  pivot.leadPath = oldPath;
  pivot.leadT = oldT;

  // handle auto-couple with deterministic merged order
  if (toMerge && hitInfo) {
    const staticList = toMerge.vehicles.slice();
    const movingList = consist.vehicles.slice();

    const sIdx = staticList.findIndex(v => v.id === hitInfo.stationaryId);
    const mIdx = movingList.findIndex(v => v.id === hitInfo.movingId);

    // must be at ends of both trains to couple
    const staticAtEnd = (sIdx === 0) || (sIdx === staticList.length - 1);
    const movingAtEnd = (mIdx === 0) || (mIdx === movingList.length - 1);
    if (!staticAtEnd || !movingAtEnd) {
      setStatusC(consist, `${STOPPED_STRING}: train ahead`);
      return true;
    }

    // Build merged order:
    const staticSeq =
      (sIdx === 0) ? staticList.slice().reverse() : staticList.slice();
    const movingSeq =
      (mIdx === 0) ? movingList.slice() : movingList.slice().reverse();

    const mergedVehicles = [...staticSeq, ...movingSeq];

    // survivor = stationary consist
    toMerge.vehicles = mergedVehicles;

    // --- preserve correct live loco and dir ---
    let desiredLive, desiredDir;
    if (consist.liveLocoId) {
      // moving consist had the active loco
      desiredLive = consist.liveLocoId;
      desiredDir  = consist.dir;
    } else if (toMerge.liveLocoId) {
      // static had the active loco
      desiredLive = toMerge.liveLocoId;
      desiredDir  = toMerge.dir;
    } else {
      desiredLive = null;
      desiredDir  = 1;
    }

    // neutralise all other locos
    if (desiredLive) {
      toMerge.vehicles.forEach(v => {
        if (v.type === "loco" && v.id !== desiredLive) v.neutral = true;
      });
    }

    // drop the moving consist
    consists = consists.filter(c => c.id !== consist.id);

    // Solve with static end pinned (index 0 is static by construction)
    const savedLive   = toMerge.liveLocoId;
    const savedAnchor = toMerge.anchorIndex;

    toMerge.liveLocoId = null;
    toMerge.anchorIndex = 0;
    
    

    solveConsistPositions(toMerge);

    // restore loco + direction
    toMerge.liveLocoId = desiredLive || savedLive || null;
    //toMerge.dir        = desiredDir;
    toMerge.dir = computeDirFromLoco(toMerge);
    toMerge.anchorIndex = savedAnchor ?? 0;
    
  
    updateNodeOccupancyFromConsists();
    toMerge.moving = false;
    if (consist.nudging) {
      toMerge.throttle = consist.oldThrottle;
    }
    setStatusC(toMerge, `Stopped: coupled ${movingList.length} vehicle(s) `, { hit: false });
    buildControlsFromConsists();

    return true;
  }

  if (collide) {
    setStatusC(consist, `${STOPPED_STRING}: train ahead`);
    return true;
  }


  return false;
}

*/
function willCollide(consist, nextLeadT, nextLeadPath = null) {
  const pivot = consist.vehicles[getPivotIndex(consist)];
  const snap = snapshotConsistStateC(consist);
  const oldPath = pivot.leadPath.slice();
  const oldT = pivot.leadT;

  if (nextLeadPath) pivot.leadPath = nextLeadPath;
  pivot.leadT = nextLeadT;

  console.log(
    `[willCollide] BEFORE trail solve: leadPath=${pivot.leadPath.join("-")}, t=${pivot.leadT.toFixed(3)}`
  );

  ensureTrailHas(pivot);
  solveConsistPositions(consist);

  console.log(
    `[willCollide] AFTER trail solve:  leadPath=${pivot.leadPath.join("-")}, t=${pivot.leadT.toFixed(3)}, coords=(${pivot.px?.toFixed(2)},${pivot.py?.toFixed(2)})`
  );
    ensureTrailHas(pivot);
  solveConsistPositions(consist);

   const pass=isConsistTryingToPassRedPointSignal(consist)
  if(pass.blocked){

    stopConsist(consist);
    setStatusC(consist, `${STOPPED_STRING}: can't pass signal ${trackLayout.tracks[pass.signalId].label || pass.signalId} towards point`);
    
    return true;
  }
  // collect vehicles from OTHER consists
  const others = [];
  for (const c of consists) {
    if (c.id === consist.id) continue;
    for (const v of c.vehicles) {
      if (v.px != null && v.py != null) others.push({ v, owner: c });
    }
  }

  let collide = false;
  let toMerge = null;
  let hitInfo = null;

  // detect overlap and approach
  for (const v of consist.vehicles) {
    const rectV = getVehicleRect(v, v.type === "loco" && v.id === consist.liveLocoId);
    for (const { v: o, owner } of others) {
      const rectO = getVehicleRect(o, o.type === "loco" && o.id === owner.liveLocoId);
      if (!rectsIntersect(rectV, rectO)) continue;

      const sameSeg =
        (v.leadPath[0] === o.leadPath[0] && v.leadPath[1] === o.leadPath[1]) ||
        (v.leadPath[0] === o.leadPath[1] && v.leadPath[1] === o.leadPath[0]);
      const continuous = isContinuousSameTrack(v, o, consist.dir);

      if (sameSeg || continuous) {
        const approach = isApproachingConsist(consist, owner);        
        if (approach.isApproaching) {
          toMerge = owner;
          hitInfo = approach;
          console.log(approach);
        }
      } else {
        // Only treat as collision if we're not already merging with this consist
  
        console.log("this 1",rectV,rectO,v,o);
        collide = true;
      }
      break;
    }
    if (collide || toMerge) break;
  }

  // both moving → stop
  if (consist.moving && toMerge && toMerge.moving) {
    stopConsist(toMerge);
    stopConsist(consist);
    setStatusC(consist, `${STOPPED_STRING}: collision with moving train`);
    setStatusC(toMerge, `${STOPPED_STRING}: collision with moving train`);
    return;
  }

  // too fast for auto-couple
  if (!consist.nudging && toMerge && consist.throttle > AUTOCONNECT_THROTTLE) {
    setStatusC(consist, `${STOPPED_STRING}: too fast for auto couple`);
    stopConsist(consist);
    return;
  }

  // restore preview
  restoreConsistStateC(consist, snap);
  pivot.leadPath = oldPath;
  pivot.leadT = oldT;

  // --- AUTO COUPLE ---
  if (toMerge && hitInfo) {
    // cancel RAFs
    try { cancelAnimationFrame(rafIds[consist.id]); delete rafIds[consist.id]; } catch {}
    try { cancelAnimationFrame(rafIds[toMerge.id]); delete rafIds[toMerge.id]; } catch {}

    const staticList = toMerge.vehicles.slice();
    const movingList = consist.vehicles.slice();

    const sIdx = staticList.findIndex(v => v.id === hitInfo.stationaryId);
    const mIdx = movingList.findIndex(v => v.id === hitInfo.movingId);

    const staticAtEnd = (sIdx === 0) || (sIdx === staticList.length - 1);
    const movingAtEnd = (mIdx === 0) || (mIdx === movingList.length - 1);
    if (!staticAtEnd || !movingAtEnd) {
      console.log("1");
      setStatusC(consist, `${STOPPED_STRING}: train ahead`);
      return true;
    }

    // Build deterministic order
    const staticSeq = (sIdx === 0) ? staticList.slice().reverse() : staticList.slice();
    const movingSeq = (mIdx === 0) ? movingList.slice() : movingList.slice().reverse();
    const mergedVehicles = [...staticSeq, ...movingSeq];


    // remove moving consists
    consists = consists.filter(c => c.id !== consist.id );
    console.log("caclulate direction from frontbacks and directions",hitInfo);
    console.log("moinving dir",consist.dir,"stationary dir ",toMerge.dir);
    let dir=consist.dir;
    if((consist.dir==toMerge.dir && hitInfo.movingEnd==hitInfo.stationaryEnd)){
      console.log("//merging with a consist that has a different idea of what forward direction is");
      dir*=-1;
    }

//    addCarsToConsist(toMerge, movingSeq,hitInfo.stationaryEnd,consist.liveLocoId,dir);    
    /*addCarsToConsist(toMerge, movingSeq,hitInfo.stationaryEnd,consist.liveLocoId,toMerge.dir);    */
    
  addCarsToConsist(toMerge, movingSeq,hitInfo.stationaryEnd,consist.liveLocoId,toMerge.dir);   
       stopConsist(toMerge)   ;

    toMerge.throttle=consist.throttle;

    
    setStatusC(toMerge, `Stopped: coupled ${staticList.length} vehicle(s)`, { hit: false });
    buildControlsFromConsists();
    drawNetwork("after-couple");

    return true;
  }

  if (collide) {
          console.log("2");
    setStatusC(consist, `${STOPPED_STRING}: train ahead`);
    return true;
  }

  return false;
}


function nearestVehicle(movingConsist, stationaryConsist) {
  let nearest = null;
  let bestDist = Infinity;
  for (const v of stationaryConsist.vehicles) {
    const dx = v.px - movingConsist.px;
    const dy = v.py - movingConsist.py;
    const d2 = dx * dx + dy * dy;
    if (d2 < bestDist) {
      bestDist = d2;
      nearest = v;
    }
  }
  return nearest;
}


function debugConsistPositions(consist, label = "") {
  
  for (let i = 0; i < consist.vehicles.length; i++) {
    const v = consist.vehicles[i];
  //  console.log(`  Vehicle ${v.id}: px=${v.px?.toFixed(2)}, py=${v.py?.toFixed(2)}, heading=${(v.heading*180/Math.PI).toFixed(1)}°` );

    if (i > 0) {
      const prev = consist.vehicles[i - 1];
      const dx = v.px - prev.px;
      const dy = v.py - prev.py;
      const dist = Math.hypot(dx, dy);

      // expected distance = half length of prev + half length of this + GAP
      const expected = (prev.length / 2) + (v.length / 2) + GAP;

      if (Math.abs(dist - expected) < 1.0) {
//        console.log(`     ↳ roughly OK (dist=${dist.toFixed(2)}, expected=${expected.toFixed(2)})`);
      } else {
        console.log(`Consist ${consist.id} debug positions ${label}`);
        console.log(`     ↳ definitely way off! (dist=${dist.toFixed(2)}, expected=${expected.toFixed(2)})`);
        console.trace();
      }
    }
  }
}

function isApproachingConsist(movingConsist, stationaryConsist) {
  if (!movingConsist?.vehicles?.length || !stationaryConsist?.vehicles?.length)
    return { isApproaching: false };

  const mvCount = movingConsist.vehicles.length;
  const stCount = stationaryConsist.vehicles.length;

  // --- determine the "front" vehicle of the moving consist ---
  const movingFront =
    movingConsist.dir === 1
      ? movingConsist.vehicles[0] // forward = first
      : movingConsist.vehicles[mvCount - 1]; // reversed = last

  // --- get stationary ends ---
  const stationaryFront = stationaryConsist.vehicles[0];
  const stationaryBack  = stationaryConsist.vehicles[stCount - 1];

  // --- find nearest vehicle pair ---
  const nearest = nearestVehicle(movingFront, stationaryConsist);
  if (!nearest) return { isApproaching: false };

  const other = nearest;
  const curDx = other.px - movingFront.px;
  const curDy = other.py - movingFront.py;
  const curDist = Math.hypot(curDx, curDy);

  // --- predict movement a short step forward along dir ---
  let heading = movingFront.heading;
  if (movingConsist.dir === -1) heading += Math.PI; // flipped direction

  const step = GAP; // look-ahead distance
  const projX = movingFront.px + Math.cos(heading) * step;
  const projY = movingFront.py + Math.sin(heading) * step;

  const newDx = other.px - projX;
  const newDy = other.py - projY;
  const newDist = Math.hypot(newDx, newDy);

  const approaching = newDist < curDist;

  // --- decide which end of stationary consist we’re approaching ---
  let stationaryEnd;

  if (stCount === 1) {
      //just use heading to decide fron or back it has nothing to do with forward and reverse
    const st = stationaryConsist.vehicles[0];
    let stHeading = st.heading;
//    if (stationaryConsist.dir === -1) stHeading += Math.PI;

    const relAngle = Math.atan2(movingFront.py - st.py, movingFront.px - st.px);
    const dAngle = Math.abs(((stHeading - relAngle + Math.PI * 3) % (Math.PI * 2)) - Math.PI);
    stationaryEnd = (dAngle < Math.PI / 2) ? "front" : "back";
  } else {
    // Multi-car: whichever end is closer in world space
    const frontDist = Math.hypot(movingFront.px - stationaryFront.px, movingFront.py - stationaryFront.py);
    const backDist  = Math.hypot(movingFront.px - stationaryBack.px,  movingFront.py - stationaryBack.py);
    stationaryEnd = (frontDist < backDist) ? "front" : "back";
  }

  // --- determine which end of the moving consist is the one hitting ---
  const movingEnd =
    mvCount > 1
      ? (movingConsist.dir === 1 ? "front" : "back") // multi-car
      : (movingConsist.dir === 1 ? "front" : "back"); // single-car same rule

  return {
    isApproaching: approaching,
    movingId: movingFront.id,
    stationaryId: other.id,
    stationaryEnd,
    movingEnd
  };
}

// Smooth anchor (screen-space) state
let followAnchorX = null;
let followAnchorY = null;
let followPrevId = null;

function panToFollow() {
  if (!followConsistId) {
    followAnchorX = followAnchorY = null;
    followPrevId = null;
    return;
  }

  const c = consists.find((cc) => cc.id === followConsistId);
  if (!c) {
    followConsistId = null;
    return;
  }

  // Pick live loco or lead vehicle
  let target = c.liveLocoId
    ? c.vehicles.find((v) => v.id === c.liveLocoId)
    : c.dir === 1
    ? c.vehicles[c.vehicles.length - 1]
    : c.vehicles[0];
  if (!target) return;

  const cx = canvas.clientWidth / 2;
  const cy = canvas.clientHeight / 2;
  const a = canvas.clientWidth / 4; // ellipse major radius (x)
  const b = canvas.clientHeight / 4; // ellipse minor radius (y)

  // Heading in world; flip if consist is reversed
  let ang = target.heading + (c.dir === 1 ? Math.PI : 0);

  // Convert world heading to a SCREEN-SPACE direction using two points
  const locoScr = worldToScreen(target.px, target.py);
  const fwdScr = worldToScreen(
    target.px + Math.cos(ang),
    target.py + Math.sin(ang)
  );
  let ux = fwdScr.x - locoScr.x;
  let uy = fwdScr.y - locoScr.y;
  const len = Math.hypot(ux, uy);
  if (len < 1e-6) {
    ux = 1;
    uy = 0;
  } else {
    ux /= len;
    uy /= len;
  }

  // Intersection of line from centre along (ux,uy) with ellipse (x/a)^2+(y/b)^2=1
  // t = 1/sqrt( (ux^2/a^2) + (uy^2/b^2) ), anchor = centre + t * (ux,uy)
  const denom = (ux * ux) / (a * a) + (uy * uy) / (b * b);
  const t = denom > 0 ? 1 / Math.sqrt(denom) : 0;
  const anchorTargetX = cx + t * ux;
  const anchorTargetY = cy + t * uy;

  // Ease the anchor (so direction flips don’t snap)
  const SMOOTH = 0.01;
  if (followPrevId !== c.id || followAnchorX == null) {
    followAnchorX = anchorTargetX;
    followAnchorY = anchorTargetY;
    followPrevId = c.id;
  } else {
    followAnchorX += (anchorTargetX - followAnchorX) * SMOOTH;
    followAnchorY += (anchorTargetY - followAnchorY) * SMOOTH;
  }

  // Screen delta to move loco → anchor
  const dx = followAnchorX - locoScr.x;
  const dy = followAnchorY - locoScr.y;

  // Convert that screen delta into worldOffset delta (your proven method)
  const cosR = Math.cos(-worldRotation);
  const sinR = Math.sin(-worldRotation);
  const wx = dx * cosR - dy * sinR;
  const wy = dx * sinR + dy * cosR;

  worldOffsetX += wx;
  worldOffsetY += wy;

  // Optional safety: keep the loco visible with a small margin
  const margin = 24;
  const after = worldToScreen(target.px, target.py);
  if (after.x < margin) worldOffsetX += margin - after.x;
  if (after.x > canvas.clientWidth - margin)
    worldOffsetX += canvas.clientWidth - margin - after.x;
  if (after.y < margin) worldOffsetY += margin - after.y;
  if (after.y > canvas.clientHeight - margin)
    worldOffsetY += canvas.clientHeight - margin - after.y;
}

/*centre
function panToFollow() {
  if (!followConsistId) return;

  const c = consists.find(cc => cc.id === followConsistId);
  if (!c) return;

  // Prefer live loco
  let target = null;
  if (c.liveLocoId) {
    target = c.vehicles.find(v => v.id === c.liveLocoId);
  }

  // Fallback: use lead vehicle
  if (!target) {
    target = (c.dir === 1)
      ? c.vehicles[c.vehicles.length - 1]
      : c.vehicles[0];
  }

  if (!target) return;

  const screenCenterX = canvas.clientWidth / 2;
  const screenCenterY = canvas.clientHeight / 2;

  const worldX = target.px;
  const worldY = target.py;

  // Current screen position of target
  const screenPos = worldToScreen(worldX, worldY);

  // How far off-centre it is (in screen px)
  const dx = screenCenterX - screenPos.x;
  const dy = screenCenterY - screenPos.y;

  // Rotate that delta back into world space
  const cosR = Math.cos(-worldRotation);
  const sinR = Math.sin(-worldRotation);
  const wx = dx * cosR - dy * sinR;
  const wy = dx * sinR + dy * cosR;

  worldOffsetX += wx;
  worldOffsetY += wy;
}
*/
// ---------- Drawing ----------
function drawNetwork(label=null) {
  if (false && debug && label)
  {
    console.log("drawnetwork",label);    
  }
  if (!trackLayout || !trackLayout.tracks) return;
  ctx.save();
  // Ensure DPR is applied (in case DPR changed)
  const dpr = Math.max(1, Math.min(window.devicePixelRatio || 1, 3));

  ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
  ctx.clearRect(0, 0, canvas.width, canvas.height);

  ctx.translate(canvas.clientWidth / 2, canvas.clientHeight / 2); // CSS px
  ctx.rotate(worldRotation);
  ctx.translate(-canvas.clientWidth / 2, -canvas.clientHeight / 2);
  panToFollow();
  ctx.translate(worldOffsetX, worldOffsetY);
  ctx.scale(worldScale, worldScale);

  // === FEATURES BELOW TRACKS ===
  drawFeatures(ctx, 0);

  drawTracksAndNodes(ctx);

  // === FEATURES ABOVETRACKS ===
  drawFeatures(ctx, 1);

  // === CONSISTS (without status)===
  drawConsists(ctx);

  // === FEATURES ABOVETRIANS ===
  drawFeatures(ctx, 2);

  // === CONSISTS status above everything===
  
  drawConsists(ctx, { juststatus: true });

  drawCompass();
  ctx.restore();
  calculateStartSegments();
}

function drawTracksAndNodes(ctx) {
  // === TRACKS ===
  forEachEdge((a, b) => {
    const [ax, ay] = trackLayout.tracks[a].position;
    const [bx, by] = trackLayout.tracks[b].position;

    const k1 = `${a}-${b}`,
      k2 = `${b}-${a}`;
    const overrideLen = trackLayout.lengths?.[k1] ?? trackLayout.lengths?.[k2];

    ctx.beginPath();
    ctx.moveTo(ax, ay);
    ctx.lineTo(bx, by);
    if (overrideLen) {
      ctx.setLineDash([px(8), px(6)]);
      ctx.strokeStyle = "grey";
    } else {
      ctx.setLineDash([]);
      ctx.strokeStyle = "black";
    }
    ctx.lineWidth = TRACKWIDTH /*px(4)*/;
    ctx.stroke();

    if (overrideLen) {
      const midX = (ax + bx) / 2,
        midY = (ay + by) / 2;
      ctx.save();
      ctx.setLineDash([]);
      ctx.fillStyle = "grey";
      ctx.font = `${12 / worldScale}px sans-serif`;
      ctx.textAlign = "center";

      drawTextOutlinedScreen("long stretch", midX, midY, {
        fillStyle: "grey",
        size: 12,
        align: "center",
        dy: -6,
      });

      //ctx.fillText("long stretch", midX, midY - px(6));

      ctx.restore();
    }
  });
  ctx.setLineDash([]);

  // === NODES ===
  for (const id in trackLayout.tracks) {
    const [x, y] = trackLayout.tracks[id].position;

    switch (trackLayout.tracks[id].type) {
      case "buffer":
        drawBuffer(ctx, id);
        break;
      case "signal":
        ctx.fillStyle = signalStates[id] ? STOP_COLOUR : GO_COLOUR;
        drawNode(ctx, id, x, y);
        break;
      case "point":
        ctx.fillStyle = "lightgray";
        drawNode(ctx, id, x, y);
        drawPointLegs(ctx, id, x, y);
        break;
      case "pass":
        if (showNNodes) {
          ctx.fillStyle = "lightblue";
          drawNode(ctx, id, x, y);
        }
        break;
      case "pointsignal":
        drawPointSignal(ctx, id);
        break;

      default:
        console.log(`node ${id} of unknown type {trackLayout.tracks[id].type}`);
        /*        ctx.fillStyle = "yellow"; //not delt with yet!
        drawNode(ctx, id, x, y);
        */
        break;
    }
  }
}

function drawConsists(ctx, juststatus = false) {
  // === CONSISTS ===
  for (const c of consists) {
    if (!juststatus) {
      
      //debugConsistPositions(c,"calling in drawConsists");
      // bodies aligned to bogies (if present)
      for (const veh of c.vehicles) {
        if (veh.px == null) continue;

        const { cx, cy, ang } = getVisualPose(veh);
        const h = veh.type === "loco" ? veh.width : CAR_WIDTH;

        ctx.save();
        ctx.translate(cx, cy);
        ctx.rotate(ang);
        ctx.fillStyle =
          veh.color || (veh.type === "loco" ? "blue" : "darkgray");
        ctx.fillRect(-veh.length / 2, -h / 2, veh.length, h);
        ctx.lineWidth = px(1);
        ctx.strokeStyle = "black";
        ctx.strokeRect(-veh.length / 2, -h / 2, veh.length, h);

        // Neutral loco badge (bold N on black disc)
        if (veh.type === "loco" && veh.neutral) {
          const r = veh.width / 2;
          ctx.beginPath();
          ctx.fillStyle = "black";
          ctx.arc(0, 0, r, 0, Math.PI * 2);
          ctx.fill();
          /*ctx.fillStyle = "white";
        ctx.font = `bold ${LOCO_WIDTH*.9}px sans-serif`;
        ctx.textAlign = "center";
        ctx.textBaseline = "middle";
        ctx.fillText("N", 0, LOCO_WIDTH*.1);*/

          /*ctx.save();*/
          //ctx.globalAlpha = c.statusAlpha;
          /*ctx.fillStyle = "black";
      ctx.font = `${12 / worldScale}px sans-serif`;
      ctx.textAlign = "center";*/
          //ctx.fillText(c.status, cx, cy - CAR_WIDTH/2-px(3));

          //drawTextOutlinedWorld(c.status,cx, cy - CAR_WIDTH/2-px(3), { font:"Arial",globalAlpha:c.statusAlpha,size: 12,  align: "center",  baseline: "alphabetic" });
          drawTextOutlinedScreen("N", cx, cy, {
            outlinePx: 1,
            fillStyle: "white", 
            size: veh.width * 0.9 * worldScale,
            textBaseline: "middle",
            align: "center",
            dy: veh.width * 0.35 * worldScale,
          });
        }
        ctx.restore();

        // bogies under the body
        if (veh.bogies && veh.bogies.length === 2) {
          drawBogie(veh.bogies[0]);
          drawBogie(veh.bogies[1]);
        }

        if (debug) {
  const cos = Math.cos(veh.heading);
  const sin = Math.sin(veh.heading);

  // half length along heading vector
  const halfL = veh.length / 2;
  const hx = cos * halfL;
  const hy = sin * halfL;

  const frontX = veh.px + hx;
  const frontY = veh.py + hy;
  const backX  = veh.px - hx;
  const backY  = veh.py - hy;

  drawTextOutlinedScreen("F", frontX, frontY, {
    size: 8,
    fillStyle: "red",
    outlinePx: 2,
    align: "center",
    dy:0,
    textBaseline: "middle"
  });

  drawTextOutlinedScreen("B", backX, backY, {
    size: 8,
    fillStyle: "blue",
    outlinePx: 2,
    align: "center",
        dy:0,
    textBaseline: "middle"
  });
}

      }

      // direction arrow at the live loco (using visual pose)
      if (c.liveLocoId) {
        const live = c.vehicles.find((v) => v.id === c.liveLocoId);
        if (live && live.px != null) {
          let { cx, cy, ang } = getVisualPose(live);
          if (c.dir === -1) {
            ang = ang + Math.PI;
          }
          drawArrow(cx, cy, ang, { width: live.width || LOCO_WIDTH });
        }
      }

      // coupling lines
      for (let i = 0; i < c.vehicles.length - 1; i++) {
        drawCouplingLine(c.vehicles[i], c.vehicles[i + 1]);
      }
    } else {
      
      // status above pivot
      const pivot = c.vehicles[getPivotIndex(c)];
      if (c.status && c.statusAlpha > 0 && pivot?.px != null) {
        const { cx, cy } = getVisualPose(pivot);
        drawTextOutlinedScreen(c.status, cx, cy, {
          size: 12,
          align: "center",
          dy: -CAR_WIDTH * worldScale - 3,
         
          globalAlpha: c.statusAlpha,
          background: "rgba(220,220,220,1)",
          boxStroke: "rgba(180,180,180,0.9)",
        });


      }
      if (c.statusAlpha > 0) {
        //    console.log (c.status,c.statusAlpha)
        c.statusAlpha = Math.max(0, c.statusAlpha - 0.06);
        if (c.statusAlpha === 0) c.status = "";
      }
    }
  }
}

function drawArrow(cx, cy, ang, { width = LOCO_WIDTH } = {}) {
  const wing = width * 0.8; //px(10)
  const tip = [
    cx + Math.cos(ang) * wing * 0.8,
    cy + Math.sin(ang) * wing * 0.8,
  ];
  const left = [
    cx + Math.cos(ang + 2.5) * wing,
    cy + Math.sin(ang + 2.5) * wing,
  ];
  const right = [
    cx + Math.cos(ang - 2.5) * wing,
    cy + Math.sin(ang - 2.5) * wing,
  ];
  ctx.beginPath();
  ctx.moveTo(...tip);
  ctx.lineTo(...left);
  ctx.lineTo(...right);
  ctx.closePath();

  ctx.lineWidth = 2 / worldScale; //draw black bordder in case on a yellow or lowcontrast train color
  ctx.strokeStyle = "black";
  ctx.stroke();
  ctx.fillStyle = "yellow";
  ctx.globalAlpha = 1;

  ctx.fill();
}

function drawCouplingLine(a, b) {
  if (a.px == null || b.px == null) return;

  const pa = getVisualPose(a);
  const pb = getVisualPose(b);

  const uaX = Math.cos(pa.ang),
    uaY = Math.sin(pa.ang);
  const ubX = Math.cos(pb.ang),
    ubY = Math.sin(pb.ang);

  // vector a→b (world)
  const abX = pb.cx - pa.cx,
    abY = pb.cy - pa.cy;

  // choose facing ends
  const aUseFront = abX * uaX + abY * uaY > 0;
  const bUseFront = (pa.cx - pb.cx) * ubX + (pa.cy - pb.cy) * ubY > 0;

  // end-face centers
  const aEndX = pa.cx + (aUseFront ? +1 : -1) * uaX * (a.length / 2);
  const aEndY = pa.cy + (aUseFront ? +1 : -1) * uaY * (a.length / 2);
  const bEndX = pb.cx + (bUseFront ? +1 : -1) * ubX * (b.length / 2);
  const bEndY = pb.cy + (bUseFront ? +1 : -1) * ubY * (b.length / 2);

  if (Math.hypot(bEndX - aEndX, bEndY - aEndY) < 1e-6) return;

  ctx.save();
  ctx.beginPath();
  ctx.moveTo(aEndX, aEndY);
  ctx.lineTo(bEndX, bEndY);
  ctx.strokeStyle = "white";
  ctx.lineWidth = TRACKWIDTH * 0.5;
  ctx.lineCap = "round";
  ctx.stroke();
  ctx.restore();
}

function drawBuffer(ctx, id) {
  const node = trackLayout.tracks[id];
  if (!node) return;

  const [x, y] = node.position;
  const connId = (node.connections || [])[0];
  if (!connId) return;

  const [nx, ny] = trackLayout.tracks[connId].position;

  // direction vector (neighbor → buffer)
  let dx = x - nx;
  let dy = y - ny;
  const len = Math.hypot(dx, dy);
  if (len === 0) return;
  dx /= len;
  dy /= len;

  // perpendicular vector (rotate 90°)
  const pxv = -dy;
  const pyv = dx;

  let width = 3;
  const bufferLength = -STOP_MARGIN + px(width); // how far beyond node
  const bufferWidth = 1.2 * CAR_WIDTH; // width of crossbar

  // anchor point just past the node
  const bx = x + dx * bufferLength;
  const by = y + dy * bufferLength;

  // crossbar endpoints
  const x1 = bx + (pxv * bufferWidth) / 2;
  const y1 = by + (pyv * bufferWidth) / 2;
  const x2 = bx - (pxv * bufferWidth) / 2;
  const y2 = by - (pyv * bufferWidth) / 2;

  ctx.beginPath();
  ctx.moveTo(x1, y1);
  ctx.lineTo(x2, y2);
  ctx.lineWidth = px(width);
  ctx.strokeStyle = BUFFER_COLOUR;
  ctx.stroke();

  // --- label ---

  let labelText = id;
  if (!showNNodes && typeof node.label === "string") {
    labelText = node.label;
  }

  // measure text width in SCREEN px
  ctx.save();
  ctx.font = `12px sans-serif`;
  const textWidth = ctx.measureText(labelText).width;
  ctx.restore();

  // base margin in screen px
  const marginPx = 2;

  // total offset in screen px = margin + half text length
  const offsetPx = marginPx + textWidth / 2;

  // now convert that into WORLD units
  const offsetWorld = px(offsetPx);

  // push label outward along the track
  const lx = x + dx * offsetWorld;
  const ly = y + dy * offsetWorld;

  drawTextOutlinedScreen(labelText, lx, ly, {
    size: 12,
    fill: "#000",
    outline: "#fff",
    outlinePx: 3,
    align: "center",
    baseline: "middle",
    dy: 0,
  });
}

function drawFeatures(ctx, zOrder = 2) {
  //not done yeat but Zorder 2 is above trains
  //could add 0 (below eveerything)
  //could add 1 (above tracks below trains)

  if (!trackLayout.features) return;

  for (const [id, f] of Object.entries(trackLayout.features)) {
    if (f.zOrder == zOrder) {
      ctx.save();
      ctx.translate(f.position[0], f.position[1]);
      if (f.angle) ctx.rotate((f.angle * Math.PI) / 180);

      const img = f.src ? images[f.src] : null;

      if (f.type === "svg" && img) {
        // Draw SVG fitted into the world-space rectangle
        ctx.drawImage(img, 0, 0, f.size[0], -f.size[1]);

        if (f.drawTextLabel) {
          drawTextOutlinedScreen(
            f.drawTextLabel,
            f.position[0] + f.size[0] / 2,
            f.position[1] - f.size[1] / 2,
            { fillStyle: "grey", size: 12, align: "center", dy: 4 }
          );
        }
      } else {
        // Fallback rectangle
        ctx.fillStyle = f.fallbackColor || "lightgray";
        ctx.fillRect(0, 0, f.size[0], -f.size[1]);

        // Optional feature ID label
        ctx.fillStyle = "black";
        /*ctx.font = "10px sans-serif";
      ctx.textAlign = "center";
      ctx.textBaseline = "middle";*/
        ctx.font = `${12 / worldScale}px sans-serif`;
        ctx.textAlign = "center";

        if (f.drawTextLabel) {
          drawTextOutlinedScreen(
            f.drawTextLabel,
            f.position[0] + f.size[0] / 2,
            f.position[1] - f.size[1] / 2,
            { fillStyle: "grey", size: 12, align: "center", dy: 4 }
          );
        }

        /*      ctx.fillText(id, 0, 0);*/
      }

      ctx.restore();
    }
  }
}

// Allow multi-touch on the canvas
canvas.style.touchAction = "none";

canvas.style.touchAction = "none"; // ensure multi-touch reaches canvas

document.addEventListener("DOMContentLoaded", () => {
  const select = document.getElementById("layoutSelect");

  // Set current dropdown from query string
  const params = new URLSearchParams(window.location.search);
  const layout = params.get("layout");
  const rolling = params.get("rollingstock");

  if (layout && rolling) {
    for (let i = 0; i < select.options.length; i++) {
      const opt = select.options[i];
      if (
        opt.dataset.layout === layout &&
        opt.dataset.rollingstock === rolling
      ) {
        select.selectedIndex = i;
        break;
      }
    }
  }

  // Change event → build URL and force reload
  select.addEventListener("change", () => {
    const opt = select.options[select.selectedIndex];
    const newLayout = opt.dataset.layout;
    const newRolling = opt.dataset.rollingstock;

    const url = new URL(window.location.href);
    url.searchParams.set("layout", newLayout);
    url.searchParams.set("rollingstock", newRolling);

    window.location.href = url.toString(); // ← this reloads the page
  });
});

// ---- Mouse wheel zoom (also handles trackpad pinch on macOS) ----
const WHEEL_ZOOM_SENSITIVITY = 1.0018; // lower = faster zoom

function wheelDeltaPixels(e) {
  // Normalize deltaY regardless of deltaMode (pixels/lines/pages)
  // 0: pixels, 1: lines, 2: pages
  if (e.deltaMode === 1) return e.deltaY * 16; // ~16px per line
  if (e.deltaMode === 2) return e.deltaY * 800; // rough page
  return e.deltaY; // pixels
}

canvas.addEventListener(
  "wheel",
  (e) => {
    // If you want ONLY ctrl+wheel to zoom, uncomment the next two lines:
    // if (!e.ctrlKey) return;
    // e.preventDefault();

    // Prevent page scroll when wheel is over the canvas
    e.preventDefault();

    const r = canvas.getBoundingClientRect();
    const sx = e.clientX - r.left;
    const sy = e.clientY - r.top;

    // Smooth exponential zoom
    const delta = wheelDeltaPixels(e);
    const factor = Math.pow(WHEEL_ZOOM_SENSITIVITY, -delta);

    applyScaleAtCanvasPoint(sx, sy, worldScale * factor); // keeps (sx,sy) pinned
    drawNetwork("10");
  },
  { passive: false }
);

// ---------- Pan & Pinch (with gesture state) ----------
const fingers = new Map(); // pointerId -> {sx, sy} canvas-local
let gesture = "idle"; // 'idle' | 'panning' | 'pinching'
let panFingerId = null;
let panStart = null; // {sx, sy}
let startOffset = null; // {x, y}

let pinchStartDist = 0;
let pinchStartScale = 1;
let pinchCenter = null; // {sx, sy}

function clientToCanvas(e) {
  const r = canvas.getBoundingClientRect();
  return { sx: e.clientX - r.left, sy: e.clientY - r.top };
}

// Keep world point under (sx,sy) fixed while scaling
/*function applyScaleAtCanvasPoint(sx, sy, newScale){
  // use your global limits if you have them
  const ZOOM_MIN = 0.2, ZOOM_MAX = 30;
  if (newScale < ZOOM_MIN) newScale = ZOOM_MIN;
  if (newScale > ZOOM_MAX) newScale = ZOOM_MAX;

  const w = screenToWorld(sx, sy);
  worldScale = newScale;
  worldOffsetX = sx - w.x * worldScale;
  worldOffsetY = sy - w.y * worldScale;
}

*/

function applyScaleAtCanvasPoint(sx, sy, newScale) {
  const ZOOM_MIN = 0.2,
    ZOOM_MAX = 30;
  if (newScale < ZOOM_MIN) newScale = ZOOM_MIN;
  if (newScale > ZOOM_MAX) newScale = ZOOM_MAX;

  // 1. World coordinate under the mouse BEFORE zoom
  const worldBefore = screenToWorld(sx, sy);

  // 2. Apply the new scale
  worldScale = newScale;

  // 3. Where would that world point map AFTER zoom+rotation?
  const screenAfter = worldToScreen(worldBefore.x, worldBefore.y);

  // 4. Correction delta in *screen* coords
  const dx = sx - screenAfter.x;
  const dy = sy - screenAfter.y;

  // 5. Rotate correction back into world offset space
  const cosR = Math.cos(-worldRotation);
  const sinR = Math.sin(-worldRotation);

  const wx = dx * cosR - dy * sinR;
  const wy = dx * sinR + dy * cosR;

  worldOffsetX += wx;
  worldOffsetY += wy;
}

canvas.addEventListener(
  "pointerdown",
  (e) => {
    canvas.setPointerCapture(e.pointerId);
    const p = clientToCanvas(e);
    fingers.set(e.pointerId, p);

    if (fingers.size === 1) {
      if (followConsistId) {
        popupMessage(
          `Unable to pan because a train has ${FOLLOWING_TRAIN_STRING} set`
        );
        return; // don’t start panning
      }

      // begin PAN
      gesture = "panning";
      panFingerId = e.pointerId;
      panStart = { sx: p.sx, sy: p.sy };
      startOffset = { x: worldOffsetX, y: worldOffsetY };
    } else if (fingers.size === 2) {
      // transition to PINCH
      const pts = [...fingers.values()];
      pinchStartDist = Math.hypot(pts[0].sx - pts[1].sx, pts[0].sy - pts[1].sy);
      pinchStartScale = worldScale;
      pinchCenter = {
        sx: (pts[0].sx + pts[1].sx) / 2,
        sy: (pts[0].sy + pts[1].sy) / 2,
      };
      gesture = "pinching";
      panFingerId = null; // not panning anymore
    }
  },
  { passive: true }
);

canvas.addEventListener(
  "pointermove",
  (e) => {
    if (!fingers.has(e.pointerId)) return;
    const p = clientToCanvas(e);
    fingers.set(e.pointerId, p);

    if (
      gesture === "panning" &&
      fingers.size === 1 &&
      e.pointerId === panFingerId
    ) {
      const dx = p.sx - panStart.sx;
      const dy = p.sy - panStart.sy;
      /*worldOffsetX = startOffset.x + dx;
    worldOffsetY = startOffset.y + dy;*/

      // rotate screen delta into world space
      const cosR = Math.cos(-worldRotation);
      const sinR = Math.sin(-worldRotation);

      const wx = dx * cosR - dy * sinR;
      const wy = dx * sinR + dy * cosR;

      worldOffsetX = startOffset.x + wx;
      worldOffsetY = startOffset.y + wy;
      drawNetwork("11");      
    } else if (
      gesture === "pinching" &&
      fingers.size === 2 &&
      pinchStartDist > 0
    ) {
      const pts = [...fingers.values()];
      const dist = Math.hypot(pts[0].sx - pts[1].sx, pts[0].sy - pts[1].sy);
      const scale = pinchStartScale * (dist / pinchStartDist);
      applyScaleAtCanvasPoint(pinchCenter.sx, pinchCenter.sy, scale);
      drawNetwork("12");
    }
  },
  { passive: true }
);

function endPointer(e) {
  // remove this finger
  if (!fingers.has(e.pointerId)) return;
  const wasPinching = gesture === "pinching";
  fingers.delete(e.pointerId);

  if (wasPinching) {
    if (fingers.size === 1) {
      // Pinch ended but one finger remains: start *fresh* pan anchored at that finger
      const [[id, p]] = fingers; // get the remaining finger
      gesture = "panning";
      panFingerId = id;
      panStart = { sx: p.sx, sy: p.sy };
      startOffset = { x: worldOffsetX, y: worldOffsetY }; // reset anchor ⇒ no jump
    } else {
      // no fingers left
      gesture = "idle";
      panFingerId = null;
      panStart = null;
      startOffset = null;
    }
    pinchStartDist = 0;
    pinchCenter = null;
    return;
  }

  if (gesture === "panning" && e.pointerId === panFingerId) {
    if (fingers.size === 1) {
      // continue panning with the remaining finger, but reset anchor to prevent drift
      const [[id, p]] = fingers;
      panFingerId = id;
      panStart = { sx: p.sx, sy: p.sy };
      startOffset = { x: worldOffsetX, y: worldOffsetY };
    } else {
      // no fingers left
      gesture = "idle";
      panFingerId = null;
      panStart = null;
      startOffset = null;
    }
  }
}
canvas.addEventListener("pointerup", endPointer, { passive: true });
canvas.addEventListener("pointercancel", endPointer, { passive: true });

// debugghing mouse move
if (false &&debug) {
  const debugDiv = document.querySelector(".debug"); // optional debug div

  canvas.addEventListener("mousemove", (e) => {
    const rect = canvas.getBoundingClientRect();

    // screen coords relative to canvas
    const sx = e.clientX - rect.left;
    const sy = e.clientY - rect.top;

    // convert to world coords
    const { x: wx, y: wy } = screenToWorld(sx, sy);

    // log or display
    if (debugDiv) {
      debugDiv.textContent = `World: ${wx.toFixed(2)}, ${wy.toFixed(2)}`;
    } else {
      console.log("World:", wx, wy);
    }
  });
}

// ---------- click handler ----------
canvas.addEventListener("click", (e) => {
  if (isInCompass(e)) {
    updateCompassRotation(e, { step: Math.PI / 4 });
  }
  const r = canvas.getBoundingClientRect();
  const sx = e.clientX - r.left,
    sy = e.clientY - r.top;
  const { x: mx, y: my } = screenToWorld(sx, sy);

  // --- helpers ---
  function localAlong(mx, my, v) {
    const dx = mx - v.px,
      dy = my - v.py;
    const c = Math.cos(v.heading),
      s = Math.sin(v.heading);
    // along = local X, across = local Y
    return { localX: dx * c + dy * s, localY: -dx * s + dy * c };
  }
  function isClickOnDirection(mx, my, veh) {
    const { localX } = localAlong(mx, my, veh);
    return Math.abs(localX) <= LOCO_WIDTH;
  }

  // seam world point between adjacent vehicles A|B (at their facing ends)
  function seamPointBetween(A, B) {
    const ux = B.px - A.px,
      uy = B.py - A.py;
    const d = Math.hypot(ux, uy) || 1;
    const uX = ux / d,
      uY = uy / d; // A -> B
    const aEndX = A.px + uX * (A.length / 2); // A's end facing B
    const aEndY = A.py + uY * (A.length / 2);
    const bEndX = B.px - uX * (B.length / 2); // B's end facing A
    const bEndY = B.py - uY * (B.length / 2);
    return { x: (aEndX + bEndX) / 2, y: (aEndY + bEndY) / 2 };
  }
  // nearest seam around vehicle index i in consist c
  function pickNearestSeam(mx, my, c, i) {
    const seams = [];
    if (i > 0) {
      const P = seamPointBetween(c.vehicles[i - 1], c.vehicles[i]); // (i-1|i)
      seams.push({
        kind: "before",
        idx: i,
        x: P.x,
        y: P.y,
        dist: Math.hypot(mx - P.x, my - P.y),
      });
    }
    if (i < c.vehicles.length - 1) {
      const P = seamPointBetween(c.vehicles[i], c.vehicles[i + 1]); // (i|i+1)
      seams.push({
        kind: "after",
        idx: i,
        x: P.x,
        y: P.y,
        dist: Math.hypot(mx - P.x, my - P.y),
      });
    }
    if (!seams.length) return null;
    seams.sort((a, b) => a.dist - b.dist);
    return seams[0];
  }

  // split utility (anchors both sides at the seam; preserves world pose)
  function splitAtChosenSeam(c, chosen) {
    let front, back;
    if (chosen.kind === "before") {
      front = c.vehicles.slice(0, chosen.idx);
      back = c.vehicles.slice(chosen.idx);
    } else {
      front = c.vehicles.slice(0, chosen.idx + 1);
      back = c.vehicles.slice(chosen.idx + 1);
    }

    c.moving = false;
    consists = consists.filter((x) => x.id !== c.id);
    try { cancelAnimationFrame(rafIds[consist.id]); delete rafIds[consist.id]; } catch {}//drop id from animationloops


    if (front.length) {
      const liveFront = front.some((x) => x.id === c.liveLocoId)
        ? c.liveLocoId
        : null;
      const c1 = makeConsist(front, liveFront, null, null, c.dir, {
        pivotIndex: front.length - 1,
        preserveWorld: true,
      });
      solveConsistPositions(c1);
      consists.push(c1);
      setStatusC(c1, "uncoupled", { hit: false });
    }
    if (back.length) {
      const liveBack = back.some((x) => x.id === c.liveLocoId)
        ? c.liveLocoId
        : null;
        
      const c2 = makeConsist(back, liveBack, null, null, 1, {
        pivotIndex: 0,
        preserveWorld: true,
      });
      solveConsistPositions(c2);
      consists.push(c2);
      setStatusC(c2, "uncoupled", { hit: false });
    }

    buildControlsFromConsists();
    drawNetwork("13");
  }

  // --- pass once over consists/vehicles; handle first hit ---
  for (const c of consists) {
      // --- check for click between carriages first ---
  for (let i = 0; i < c.vehicles.length - 1; i++) {
    const seam = seamPointBetween(c.vehicles[i], c.vehicles[i + 1]);
    const d = Math.hypot(mx - seam.x, my - seam.y);
    if (d < GAP * 0.5) { // widen/contract detection radius as you like
      console.log("clicked between cars", c.vehicles[i].label, "|", c.vehicles[i + 1].label);
      splitAtChosenSeam(c, { kind: "after", idx: i, x: seam.x, y: seam.y });
      updateNodeOccupancyFromConsists();
      drawNetwork("seam click");
      return;
    }
  }

    for (let i = 0; i < c.vehicles.length; i++) {
      const v = c.vehicles[i];
      const isLoco = v.type === "loco";
      if (!pointInVehicle(mx, my, v)) continue;

      // 1) If a loco that's not live yet → make it live and stop.
      if (isLoco && c.liveLocoId !== v.id) {
        c.liveLocoId = v.id;
        c.vehicles.forEach((x) => {
          if (x.type === "loco") x.neutral = x.id !== v.id;
        });
        solveConsistPositions(c);
        buildControlsFromConsists();
        drawNetwork("14");
        return;
      }

      // 2) If click on direction arrow: flip
      if (isClickOnDirection(mx, my, v)) {
        if (isLoco && c.liveLocoId === v.id) {
          c.dir *= -1;
          solveConsistPositions(c);
          buildControlsFromConsists();
          drawNetwork("15");
        } else {
          // middle of a carriage (or neutral loco) → no action
          drawNetwork("16");
        }
        return;
      }

      // 3) Otherwise uncouple at the NEAREST COUPLER to the tap
      const end = getClickedEnd(mx, my, v);
      const connected = isEndConnected(v, end);

      if (connected) {
        //only spli if connected at that end
        const chosen = pickNearestSeam(mx, my, c, i);
        if (!chosen) {
          drawNetwork("17");
          return;
        } // single-vehicle consist
        splitAtChosenSeam(c, chosen);
        updateNodeOccupancyFromConsists();
        return;
      } else {
        // end is unconnected → nudge GAP metres in that direction
        nudgeConsist(c, GAP * 1.005, end); //reconnect anything that's been disconnected
        drawNetwork("18");
        return;
      }
    }
  }

  // --- nodes (buffers / switches) ---
  for (const id in signalStates) {
    if (trackLayout.tracks[id].type === "pointsignal") {
      if (isClickInPointSignal(id, mx, my)) {
        const sig = trackLayout.tracks[id];

        const pointId = sig.point; // e.g. "P1"
        const legId = sig.pointleg; // e.g. "N2"

        // don't allow clearing if the point is occupied
        if (isNodeOccupied(pointId)) {
          // keep / force STOP
          signalStates[id] = true;
          popupMessage(`Unable to switch signal (point ${pointId} occupied)`);

          console.log(`Pointsignal ${id} blocked (point ${pointId} occupied).`);
          drawNetwork("19");
          return;
        }

        // align the point to the governed leg (flip if needed)
        const ok = alignPointToLeg(pointId, legId);
        if (!ok) {
          // couldn't align (unknown leg, fixed, or some guard failed) → stay STOP
          signalStates[id] = true;
          console.log(
            `Pointsignal ${id} could not align point ${pointId} to ${legId}.`
          );
          drawNetwork("20");
          return;
        }

        // clear the pointsignal to GO
        signalStates[id] = !signalStates[id];
//        console.log(`Pointsignal ${id} clear so can flip; point ${pointId} aligned to ${legId}.` );

        // force ALL other signals on this point to red
        if (!signalStates[id]) {
          for (const sid in trackLayout.tracks) {
            const other = trackLayout.tracks[sid];
            if (
              other?.type === "pointsignal" &&
              other.point === pointId &&
              sid !== id
            ) {
              signalStates[sid] = true; // STOP
            }
          }
        }
        drawNetwork("21");
        return;
      }
    } else {
      //check signal on strack
      const [x, y] = trackLayout.tracks[id].position;
      if (Math.hypot(mx - x, my - y) < px(NODERADIUSPX)) {
        if (!isNodeOccupied(id)) {
          signalStates[id] = !signalStates[id];
          trackLayout.tracks[id].occupiedBy = null;
          drawNetwork("22");
        }
        return;
      }
    }
  }
  for (const pid in switchStates) {
    const [x, y] = trackLayout.tracks[pid].position;
    if (Math.hypot(mx - x, my - y) < px(NODERADIUSPX)) {
      if (!isNodeOccupied(pid)) {
        switchStates[pid] = (switchStates[pid] + 1) % 2;
        updatePointSignalsForPoint(pid);

        trackLayout.tracks[pid].occupiedBy = null;
        drawNetwork("23");
      }
      return;
    }
  }
});

// ---------- end of click handler ----------

// ---------- Fit on resize ----------
//window.addEventListener("resize", () => {   updateWorldTransform({ fit: true}); drawNetwork(); });
function computeBogies(v) {
  v.bogies = [];
  if (!v.leadPath || v.leadPath.length !== 2 || v.length <= MAXLENGTHBEFOREBOGIES) return;

  const half = v.length / 2;
  const inset = Math.max(0.1, Math.min(BOGIE_INSET, half - 0.1));
  const d = half - inset;

  const f = walkAlong(v.leadPath, v.leadT, d, +1);
  const r = walkAlong(v.leadPath, v.leadT, d, -1);

  if (f) {
    const [A,B] = f.seg;
    const p = pointOnSegStrict(A,B,f.t);
    v.bogies.push({ px: p.px, py: p.py, heading: p.heading, seg: f.seg, t: f.t });
  }
  if (r) {
    const [A,B] = r.seg;
    const p = pointOnSegStrict(A,B,r.t);
    v.bogies.push({ px: p.px, py: p.py, heading: p.heading, seg: r.seg, t: r.t });
  }
}


function drawBogie(b) {
  return;
  if (!b) return;
  ctx.save();
  ctx.translate(b.px, b.py);
  ctx.rotate(b.heading);

  // bogie frame
  ctx.fillStyle = "#333";
  ctx.fillRect(-BOGIE_LENGTH / 2, -BOGIE_WIDTH / 2, BOGIE_LENGTH, BOGIE_WIDTH);
  ctx.lineWidth = px(1);
  ctx.strokeStyle = "#111";
  ctx.strokeRect(
    -BOGIE_LENGTH / 2,
    -BOGIE_WIDTH / 2,
    BOGIE_LENGTH,
    BOGIE_WIDTH
  );

  // wheels (purely visual; radii are meters so they scale with zoom)
  ctx.fillStyle = "#222";
  const wx = WHEEL_OFFSET;
  ctx.beginPath();
  ctx.arc(+wx, 0, WHEEL_RADIUS, 0, Math.PI * 2);
  ctx.arc(-wx, 0, WHEEL_RADIUS, 0, Math.PI * 2);
  ctx.fill();

  ctx.restore();
}

// Use bogies (if present) to define the visual pose of the vehicle
function getVisualPose(v) {
  
  if (v.bogies && v.bogies.length === 2) {
    const front = v.bogies[0]; // computeBogies() pushes [front, rear]
    const rear = v.bogies[1];
    if (!isFinite(front.px) || !isFinite(front.py) ||
    !isFinite(rear.px)  || !isFinite(rear.py)) {
  console.error("[getVisualPose] invalid bogie positions for", v.id, front, rear);
}
    const cx = (front.px + rear.px) / 2;
    const cy = (front.py + rear.py) / 2;
    const ang = Math.atan2(front.py - rear.py, front.px - rear.px); // rear→front
    return { cx, cy, ang };
  }
  return { cx: v.px, cy: v.py, ang: v.heading };
}

/*
function normalizeConnections(layout) {
  const tracks = layout.tracks;
  // Build symmetric adjacency with unique neighbors
  const adj = {};
  for (const id in tracks) adj[id] = new Set();

  for (const a in tracks) {
    const conns = tracks[a].connections || [];
    for (const b of conns) {
      if (!tracks[b]) continue; // ignore dangling refs
      adj[a].add(b);
      adj[b].add(a); // ensure reverse edge exists
    }
  }

  // Freeze as arrays for use everywhere
  layout.adj = {};
  for (const id in adj) layout.adj[id] = Array.from(adj[id]);
}
  */
function normalizeConnections(layout, rootId = "S1") {
  const tracks = layout.tracks;
  const adj = {};
  for (const id in tracks) adj[id] = new Set();

  for (const a in tracks) {
    const conns = tracks[a].connections || [];
    for (const b of conns) {
      if (!tracks[b]) continue;
      adj[a].add(b);
      adj[b].add(a);
    }
  }

  layout.adj = {};
  for (const id in adj) layout.adj[id] = Array.from(adj[id]);

  // --- build canonical direction map ---
  const canon = {};
  const visited = new Set([rootId]);
  const queue = [rootId];

  while (queue.length) {
    const A = queue.shift();
    for (const B of adj[A]) {
      const key = [A, B].sort().join("-");
      if (!canon[key]) canon[key] = { forward: [A, B] };
      if (!visited.has(B)) {
        visited.add(B);
        queue.push(B);
      }
    }
  }

  layout.canonicalSegments = canon;
}



function degree(id) {
  return trackLayout.adj?.[id]?.length || 0;
}

function forEachEdge(cb) {
  const seen = new Set();
  for (const a in trackLayout.adj) {
    for (const b of trackLayout.adj[a]) {
      const key = a < b ? `${a}|${b}` : `${b}|${a}`;
      if (seen.has(key)) continue;
      seen.add(key);
      cb(a, b);
    }
  }
}

function drawTextOutlinedScreen(
  text,
  worldX,
  worldY,
  {
    size = 12, // px on screen
    fill = "#000",
    outline = "#fff",
    outlinePx = 3, // px on screen
    font = "sans-serif",
    align = "center",
    fillStyle = "black",
    baseline = "alphabetic",
    globalAlpha = 1,
    dx = 0, // px offset in SCREEN coords (right +)
    dy = -16, // px offset in SCREEN coords (up -)
    lineHeight = 1, // multiplier of font size
  } = {}
) {
  // 1) world → screen
  const { x: sx, y: sy } = worldToScreen(worldX, worldY);

  // 2) draw in SCREEN space with the same DPR transform
  const dpr = Math.max(1, Math.min(window.devicePixelRatio || 1, 3));
  ctx.save();
  ctx.setTransform(dpr, 0, 0, dpr, 0, 0); // <-- keep DPR
  ctx.globalAlpha = globalAlpha;

  ctx.font = `${size}px ${font}`;
  ctx.textAlign = align;
  ctx.textBaseline = baseline;
  ctx.lineJoin = "round";
  ctx.lineWidth = outlinePx;

  const tx = sx + dx;
  let ty = sy + dy;

  const lines = String(text).split(/\r?\n/);

  for (let i = 0; i < lines.length; i++) {
    const line = lines[i];

    ctx.strokeStyle = outline;
    ctx.strokeText(line, tx, ty);
    ctx.fillStyle = fillStyle;
    ctx.fillText(line, tx, ty);

    ty += size * lineHeight; // advance for next line
  }

  ctx.restore();
}
/*
function drawTextOutlinedScreen(
  text,
  worldX,
  worldY,
  {
    size = 12,
    fill = "#000",
    outline = "#fff",
    outlinePx = 3,
    font = "sans-serif",
    align = "center",
    baseline = "alphabetic",
    globalAlpha = 1,
    dx = 0,
    dy = -16,
    background = null,   // e.g. "rgba(220,220,220,0.8)"
    paddingX = 4,
    paddingY = 2,
    borderRadius = 4,
    boxStroke = null,
  } = {}
) {
  // 1) world → screen
  const { x: sx, y: sy } = worldToScreen(worldX, worldY);

  const dpr = Math.max(1, Math.min(window.devicePixelRatio || 1, 3));
  ctx.save();
  ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
  ctx.globalAlpha = globalAlpha;

  ctx.font = `${size}px ${font}`;
  ctx.textAlign = align;
  ctx.textBaseline = baseline;

  const anchorX = sx + dx;
  const anchorY = sy + dy;

  // --- background box ---
  if (background) {
    const metrics = ctx.measureText(text);
    const ascent  = metrics.actualBoundingBoxAscent  || size * 0.8;
    const descent = metrics.actualBoundingBoxDescent || size * 0.2;
    const textW   = metrics.width;
    const textH   = ascent + descent;

    const boxW = textW + paddingX * 2;
    const boxH = textH + paddingY * 2;

    let bx = anchorX;
    let by = anchorY;

    // Horizontal alignment
    if (align === "center") bx -= boxW / 2;
    else if (align === "right" || align === "end") bx -= boxW;

    // Vertical alignment relative to baseline
    // baseline is the anchorY for text
    if (baseline === "top" || baseline === "hanging") {
      // text drawn with top at anchorY
      by -= paddingY;
    } else if (baseline === "middle") {
      // anchorY is vertical middle of text
      by -= (textH / 2 + paddingY);
    } else if (baseline === "alphabetic" || baseline === "ideographic") {
      // anchorY is baseline, so we need to shift up by ascent
      by -= ascent + paddingY;
    } else if (baseline === "bottom") {
      // anchorY is bottom, so just subtract box height
      by -= (textH + paddingY * 2);
    }

    ctx.fillStyle = background;
    if (ctx.roundRect) {
      ctx.beginPath();
      ctx.roundRect(bx, by, boxW, boxH, borderRadius);
      ctx.fill();
      if (boxStroke) {
        ctx.strokeStyle = boxStroke;
        ctx.stroke();
      }
    } else {
      ctx.fillRect(bx, by, boxW, boxH);
      if (boxStroke) {
        ctx.strokeStyle = boxStroke;
        ctx.strokeRect(bx, by, boxW, boxH);
      }
    }
  }

  // --- text outline ---
  if (outlinePx > 0) {
    ctx.lineWidth = outlinePx;
    ctx.strokeStyle = outline;
    ctx.strokeText(text, anchorX, anchorY);
  }

  // --- text fill ---
  ctx.fillStyle = fill;
  ctx.fillText(text, anchorX, anchorY);

  ctx.restore();
}
*/

function drawTextOutlinedWorld(
  text,
  x,
  y,
  {
    size = 12, // desired screen px height
    fill = "#000",
    outline = "#fff",
    outlinePx = 3, // outline thickness in screen px
    font = "sans-serif",
    align = "start",
    globalAlpha = 1,
    baseline = "alphabetic",
  } = {}
) {
  ctx.save();
  ctx.font = `${size / worldScale}px ${font}`;
  ctx.globalAlpha = globalAlpha;
  ctx.textAlign = align;
  ctx.textBaseline = baseline;
  ctx.lineJoin = "round";
  ctx.lineWidth = px(outlinePx); // keeps outline ~constant on screen
  ctx.strokeStyle = outline;
  ctx.strokeText(text, x, y); // halo behind
  ctx.fillStyle = fill;
  ctx.fillText(text, x, y); // text on top
  ctx.restore();
}

function bufferString(id) {
  const t = trackLayout.tracks[id];
  if (!t) return "unknown";

  return trackLayout.tracks[id].type == "buffer" ? "buffer" : "signal";
}

function getClickedEnd(mx, my, car) {
  let heading = car.heading;
  /*  if(car.type==='loco'){//may have to do this just for live loco??
      for (const c of consists) {
    const idx = c.vehicles.findIndex(v => v.id === car.id);
    if (idx !== -1) {
     if(c.dir==-1){
      heading=heading+Math.PI;
     }
    }
  }
}*/
  const dx = Math.cos(heading);
  const dy = Math.sin(heading);
  // project click onto car's longitudinal axis
  const relx = (mx - car.px) * dx + (my - car.py) * dy;
  const half = car.length / 2;

  if (relx >= half / 4) {
    return "front";
  }
  if (relx <= -half / 4) {
    return "rear";
  }

  return null; // clicked somewhere in the middle
}

function isEndConnected(car, end) {
  for (const c of consists) {
    const idx = c.vehicles.findIndex((v) => v.id === car.id);
    if (idx !== -1) {
      let neighbour = null;

      if (end === "rear" && idx < c.vehicles.length - 1) {
        neighbour = c.vehicles[idx + 1];
      }
      if (end === "front" && idx > 0) {
        neighbour = c.vehicles[idx - 1];
      }

      if (neighbour) {
        //console.log(`Car ${car.id} ${end} end is connected to ${neighbour.id} (${neighbour.heading})`);
        return true;
      } else {
        //console.log(`Car ${car.id} ${end} end is UNCONNECTED`);
        return false;
      }
    }
  }

  //  console.log(`Car ${car.id} not found in any consist`);
  return false;
}

function nudgeConsist(consist, metres, end) {
  if (!consist || consist.moving) return false;

  const oldDir = consist.dir;
  const oldSpeed = consist.speed;
  const oldMoving = consist.moving;
  consist.oldThrottle = consist.throttle;

  // Temporarily configure for one-step move
  consist.dir = end === "front" ? +1 : -1;

  consist.speed = metres; // force distance per step = metres

  consist.moving = true;
  consist.nudging = true;
  consist.throttle = 1;

  // Run one frame of the existing animation logic
  animateConsist(consist);
  consist.nudging = false;

  // Restore state
  consist.speed = oldSpeed;
  consist.moving = oldMoving;
  consist.dir = oldDir;
  consist.throttle = consist.oldThrottle;

  return true;
}
/*
function isNodeOccupied(nodeId) {
  const node = trackLayout.tracks[nodeId];
  if (!node) return false;

  for (const c of consists) {
    if (!c.vehicles.length) continue;

    // 1. Compute span of consist
    let spanMin = Infinity,
      spanMax = -Infinity;
    for (const v of c.vehicles) {
      for (const bogie of v.bogies) {
        if (!bogie.trail) continue;
        const d = bogie.trailPos; // distance along trail
        spanMin = Math.min(spanMin, d);
        spanMax = Math.max(spanMax, d);
      }
    }

    if (spanMin === Infinity) continue; // no trails yet

    // 2. Project node position onto same trail
    const dNode = projectNodeOntoTrail(nodeId, c.vehicles[0].bogies[0].trail);
    if (dNode === null) continue;

    // 3. Check overlap
    if (dNode >= spanMin && dNode <= spanMax) {
      return true;
    }
  }
  return false;
}*/
/*---prevent switches and points been clicked if a train is spannign them--*/
function projectNodeT(node, rear, front) {
  // both rear/front have .path [a,b], same segment
  const A = trackLayout.tracks[rear.path[0]];
  const B = trackLayout.tracks[rear.path[1]];
  const vx = B.x - A.x,
    vy = B.y - A.y;
  const segLen2 = vx * vx + vy * vy;
  if (segLen2 === 0) return null;

  const nx = node.x - A.x,
    ny = node.y - A.y;
  return (nx * vx + ny * vy) / segLen2; // 0..1 fraction
}

function isNodeOccupied(nodeId) {
  const node = trackLayout.tracks[nodeId];
  if (!node) return false;
  const pt = getNodeTriggerPoint(nodeId);
  if (!pt) return false;

  const nodeX = pt.x; //node.position[0];
  const nodeY = pt.y; //node.position[1];
  const tol = GAP / 2; // metres tolerance

  for (const c of consists) {
    for (const v of c.vehicles) {
      const { rear, front } = carEndsWorld(v);

      // --- body span check ---
      const dNodeRear = Math.hypot(nodeX - rear.px, nodeY - rear.py);
      const dNodeFront = Math.hypot(nodeX - front.px, nodeY - front.py);
      const dFrontRear = Math.hypot(front.px - rear.px, front.py - rear.py);

      if (Math.abs(dNodeRear + dNodeFront - dFrontRear) < tol) {
        //console.log(`Node ${nodeId} under car body ${v.id}`);
        return true;
      }

      // --- coupler check (rear & front) ---
      if (dNodeRear < tol) {
        //console.log(`Node ${nodeId} at REAR coupler of ${v.id}`);
        return true;
      }
      if (dNodeFront < tol) {
        //console.log(`Node ${nodeId} at FRONT coupler of ${v.id}`);
        return true;
      }
    }
  }

  return false;
}

function carEndsWorld(v) {
  ensureTrailHas(v);

  const rear = walkAlong(v.leadPath, v.leadT, v.length / 2, -1);
  const front = walkAlong(v.leadPath, v.leadT, v.length / 2, +1);

  return { rear, front };
}

function drawCompass() {
  const scale = Math.min(COMPASS_SCALE, window.devicePixelRatio || 1);
  const size = COMPASS_DIAMETER * scale; // compass diameter in px
  const margin = 10;
  const cx = canvas.width - size / 2 - margin;
  const cy = size / 2 + margin;

  ctx.save();
  ctx.resetTransform();

  ctx.translate(cx, cy);

  // outer circle
  ctx.beginPath();
  ctx.arc(0, 0, size / 2, 0, Math.PI * 2);
  ctx.fillStyle = "rgba(255,255,255,0.7)";
  ctx.fill();
  ctx.strokeStyle = "black";
  ctx.stroke();

  ctx.rotate(worldRotation);
  // south arrow (rotates with worldRotation)
  let w = 6 * scale;
  ctx.beginPath();
  ctx.moveTo(0, size / 2 - 12);
  ctx.lineTo(w, 0);
  ctx.lineTo(-w, 0);
  ctx.closePath();
  ctx.fillStyle = "grey";
  ctx.fill();

  // north arrow (rotates with worldRotation)
  ctx.beginPath();
  ctx.moveTo(0, -size / 2 + 12);
  ctx.lineTo(w, 0);
  ctx.lineTo(-w, 0);
  ctx.closePath();
  ctx.fillStyle = "red";
  ctx.fill();
  let fontsize = 10 * scale;
  ctx.font = `${fontsize}px sans-serif`;
  ctx.textAlign = "center";
  ctx.textBaseline = "alphabetic";
  ctx.fillStyle = "black";
  ctx.fillText("N", 0, -size / 2 + 12);

  ctx.restore();
}

function updateCompassRotation(e, { step = 0 } = {}) {
  const rect = canvas.getBoundingClientRect();
  if (step) {
    worldRotation = (worldRotation + step) % (2 * Math.PI);
  } else {
    const size = COMPASS_DIAMETER;
    const margin = 10;
    const cx = rect.width - size / 2 - margin;
    const cy = size / 2 + margin;
    const sx = e.clientX - rect.left;
    const sy = e.clientY - rect.top;
    const angle = Math.atan2(sy - cy, sx - cx); // radians
    worldRotation = angle + Math.PI / 2; // align so arrow points north
  }
  drawNetwork("24");
}

/*let compassDragging = false;

canvas.addEventListener("mousedown", (e) => {
  if (isInCompass(e)) {
    compassDragging = true;
  }
});
canvas.addEventListener("mouseup", () => {
  compassDragging = false;
});
canvas.addEventListener("mousemove", (e) => {
  if (!compassDragging) return;
  updateCompassRotation(e);
});*/
function isInCompass(e) {
  const rect = canvas.getBoundingClientRect();
  const size = 60 * Math.min(COMPASS_SCALE, window.devicePixelRatio || 1);
  const margin = 10;
  const cx = rect.width - size / 2 - margin;
  const cy = size / 2 + margin;

  const sx = e.clientX - rect.left;
  const sy = e.clientY - rect.top;

  return Math.hypot(sx - cx, sy - cy) <= size / 2;
}
function bestTextColor(bgColor) {
  // bgColor can be like "#rrggbb" or "rgb(r,g,b)"
  let r, g, b;

  if (!bgColor.startsWith("#")) {
    bgColor = toHexColor(bgColor);
  }

  const hex = bgColor.substring(1);
  const bigint = parseInt(hex, 16);
  if (hex.length === 6) {
    r = (bigint >> 16) & 255;
    g = (bigint >> 8) & 255;
    b = bigint & 255;
  }

  // Perceived brightness (YIQ formula)
  const yiq = (r * 299 + g * 587 + b * 114) / 1000;

  return yiq >= 128 ? "black" : "white";
}

function toHexColor(colorStr) {
  // Create a dummy element
  const el = document.createElement("div");
  el.style.color = colorStr;
  document.body.appendChild(el);

  // Let the browser resolve it
  const cs = getComputedStyle(el).color;
  document.body.removeChild(el);

  // cs will be in form "rgb(r, g, b)" or "rgba(r, g, b, a)"
  const m = cs.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
  if (!m) return null;

  const r = parseInt(m[1]).toString(16).padStart(2, "0");
  const g = parseInt(m[2]).toString(16).padStart(2, "0");
  const b = parseInt(m[3]).toString(16).padStart(2, "0");

  return `#${r}${g}${b}`;
}

function popupMessage(msgText) {
  // Avoid duplicates (same message still visible)
  if (document.getElementById("popupMsg")) return;
  const scale = Math.min(2.5, window.devicePixelRatio || 1);

  const msg = document.createElement("div");
  msg.id = "popupMsg";
  msg.textContent = msgText;
  msg.style.position = "absolute";
  msg.style.top = "10px";
  msg.style.left = "50%";
  msg.style.transform = "translateX(-50%)";
  msg.style.background = "rgba(0,0,0,0.7)";
  msg.style.color = "white";
  msg.style.zIndex = 9999;
  msg.style.opacity = "1";
  msg.style.transition = "opacity 1s ease";
  msg.style.fontSize = `${12 * scale}px`;
  msg.style.padding = `${6 * scale}px ${10 * scale}px`;
  msg.style.borderRadius = `${4 * scale}px`;

  document.body.appendChild(msg);

  // Fade out after 1.5s, then remove at 2s
  setTimeout(() => {
    msg.style.opacity = "0";
    setTimeout(() => msg.remove(), 1000);
  }, 3500);
}

function loadTask(task) {
  // Clear the world
  collisions = 0;
  updateCollisionCounter();
  consists = [];
  nextConsistId = 1;
  cancelAllRafs();

  // Display task info
  if (task) {
    document.getElementById("taskTitle").textContent = task.title;
    document.getElementById("taskDesc").textContent = task.description;
  } else {
    document.getElementById("taskTitle").textContent = "";
    document.getElementById("taskDesc").textContent = "";
  }
  // restore node states (points + signals) if present
  if (task && task.nodestates) {
    applyNodeStates(task.nodestates);
  }
  placeStartConsists(task);
  updateNodeOccupancyFromConsists();
  buildControlsFromConsists();
  drawNetwork("25");
}

function findVehicleDef(label) {
  const loco = rollingstock.locomotives.find((l) => l.label === label);
  if (loco) return { ...loco };
  const car = rollingstock.carriages.find((c) => c.label === label);
  if (car) return { ...car };
  throw new Error("Unknown stock: " + label);
}

function buildTaskList() {
  const taskList = document.getElementById("taskList");
  taskList.innerHTML = "";

  rollingstock.tasks.forEach((task, i) => {
    const btn = document.createElement("button");
    btn.textContent = task.title;
    btn.onclick = () => loadTask(task);
    taskList.appendChild(btn);
  });
}

function cancelAllRafs() {
  for (const id in rafIds) {
    if (rafIds[id]) {
      cancelAnimationFrame(rafIds[id]);
      delete rafIds[id];
    }
  }
  rafIds = {}; // reset
}

function updateNodeOccupancyFromConsists() {
  for (const sid in trackLayout.tracks) {
    const node = trackLayout.tracks[sid];
    node.occupiedBy = null;

    if (isNodeOccupied(sid)) {
      // find which consist is actually on this node
      for (const c of consists) {
        for (const v of c.vehicles) {
          const { rear, front } = carEndsWorld(v);
          const [nx, ny] = node.position;

          const tol = GAP / 2;
          const dRear = Math.hypot(nx - rear.px, ny - rear.py);
          const dFront = Math.hypot(nx - front.px, ny - front.py);
          const dBody = Math.hypot(nx - v.px, ny - v.py);

          if (dRear < tol || dFront < tol || dBody < tol) {
            node.occupiedBy = c.id;
            break;
          }
        }
        if (node.occupiedBy) break;
      }
    }
  }
}

/*
function checkSignalEntry(consist) {
  //only check if we haven't stopped the conist at it in animate
  //and the stet is green  we can't enter a red
  if (consist.moving) {
    for (const sid in signalStates) {
      const node = trackLayout.tracks[sid];  
       if (!node || node.type !== "signal") continue;  // ignore pointsignal for now 
    
      // If already locked by another consist, skip
      if (node.occupiedBy ) continue;// && node.occupiedBy !== consist.id

      // Is this node physically under *any* part of this consist?
      // we can enter a red
      if (signalStates[sid] == false) {
        const occ = isNodeOccupied(sid);
        if (occ) {
          node.occupiedBy = consist.id;
          //      signalStates[sid] = false; // set to green while occupied
          //console.log("is occupied ", consist);
        } else {
          node.occupiedBy = null;
        }
      }
    }
  }
}
*/
function checkSignalEntry(consist) {
  if (!consist.moving) return;

  for (const sid in signalStates) {
    const node = trackLayout.tracks[sid];
    if (!node) continue;

    // Normal signal logic
    if (node.type === "signal") {
      if (node.occupiedBy) continue; // skip if locked

      if (signalStates[sid] === false) {
        // GO (green) in your scheme
        const occ = isNodeOccupied(sid);
        if (occ) {
          node.occupiedBy = consist.id;
        } else {
          node.occupiedBy = null;
        }
      }
    }

    // Point signal logic
    else if (node.type === "pointsignal") {
      if (node.occupiedBy) continue;
      const occ = isNodeOccupied(sid);
      if (!occ) {
        //if(sid=='S10')console.log("train not over  poitnsignal trigger ",sid );
        continue;
      }
      
      // Only relevant if train would actually meet the point via this leg
      const approaching = isApproachingPoint(
        consist,
        node.point,
        node.pointleg
      );
      if (!approaching) {
        continue;
      } else {
        node.occupiedBy = consist.id;
      }
    }
  }
}

function checkSignalClear(consist) {
  //  if(consist.moving) ???? JD
  for (const sid in signalStates) {
    const node = trackLayout.tracks[sid];
    if (!node) continue;

    if (node.type == "signal" || node.type == "pointsignal") {
      if (node.occupiedBy !== consist.id) continue; // only care about nodes this consist touched

      // Re-check if anything is still physically on this node
      const occ = isNodeOccupied(sid);

      if (!occ) {
        //console.log(`Signal ${sid} cleared by consist ${consist.id}`);
        signalStates[sid] = true; // back to red
        node.occupiedBy = null; // release lock
      }
    }
  }
}


function getPointLeg(id, state) {
  const node = trackLayout.tracks[id];
  if (!node || !node.connections) return null;

  // The first connection is the always-live leg.
  // Remaining legs are selectable, choose based on state (0/1).
  const legs = node.connections.slice(1);

  if (legs.length === 2) {
    return legs[state];
  }

  return null;
}


function setPointLeg(id, legId) {
  const node = trackLayout.tracks[id];
  if (!node || !node.connections) return;

  
// The first connection is the always-live leg.
  // Remaining legs are selectable, choose based on state (0/1).
  const legs = node.connections.slice(1);
  const idx = legs.indexOf(legId);
  if (idx >= 0) {
    switchStates[id] = idx;
  }
}

function applyNodeStates(nodestates) {
  for (const entry of nodestates) {
    if (entry.state && typeof entry.state === "string") {
      // point by leg ID
      setPointLeg(entry.id, entry.state);
    } else if (typeof entry.state === "number") {
      // numeric fallback
      switchStates[entry.id] = entry.state;
    } else if (entry.signal) {
      // signals from signalStates
      if (entry.signal === "STOP") signalStates[entry.id] = true;
      if (entry.signal === "GO") signalStates[entry.id] = false;
    }
  }
  drawNetwork("26");
}

function calculateStartSegments() {
  if (!calculatestartsegments) return;

  // ensure output div exists
  let outDiv = document.getElementById("startsegments-output");
  if (!outDiv) {
    outDiv = document.createElement("div");
    outDiv.id = "startsegments-output";
    Object.assign(outDiv.style, {
      position: "fixed",
      bottom: "0",
      left: "0",
      right: "0",
      maxHeight: "200px",
      overflow: "auto",
      background: "#111",
      color: "#0f0",
      fontFamily: "monospace",
      fontSize: "12px",
      padding: "8px",
      borderTop: "2px solid #444",
      whiteSpace: "pre",
    });
    document.body.appendChild(outDiv);
    // push up the page so content isn't hidden
    document.body.style.marginBottom = "220px";
  }

  const startsegments = [];
  const nodestates = [];

  // --- trains ---
  for (const consist of consists) {
    if (!consist.vehicles?.length) continue;

    const lead = consist.vehicles[0];
    if (!lead.leadPath || lead.leadPath.length < 2) continue;

    const [A, B] = lead.leadPath;
    const segLen = dist(A, B);

    const t = typeof lead.leadT === "number" ? lead.leadT : 0;
    const distance = Math.round(t * segLen);

    startsegments.push({
      segment: `${A}-${B}`,
      stock: consist.vehicles.map((v) => v.id),
      distance,
    });
  }

  // --- points (from switchStates) ---
  if (switchStates) {
    for (const [id, state] of Object.entries(switchStates)) {
      const leg = getPointLeg(id, state);
      if (leg) {
        nodestates.push({ id, state: leg });
      }
    }
  }

  // --- signals (only degree = 2, from signalStates) ---
  if (signalStates) {
    for (const [id, state] of Object.entries(signalStates)) {
      if (degree(id) === 2) {
        const sig = state ? "STOP" : "GO";
        nodestates.push({ id, signal: sig });
      }
    }
  }

  // --- compressed JSON output ---
  const exportObj = { startsegments, nodestates };

  const json =
    "{\n" +
    Object.entries(exportObj)
      .map(
        ([key, arr]) =>
          `  "${key}": [\n` +
          arr.map((s) => "    " + JSON.stringify(s)).join(",\n") +
          "\n  ]"
      )
      .join(",\n") +
    "\n}";

  outDiv.textContent = json;
}

function drawNode(ctx, id, x, y) {
  ctx.beginPath();
  ctx.arc(x, y, px(NODERADIUSPX), 0, Math.PI * 2);

  ctx.fill();
  ctx.lineWidth = px(1);
  ctx.strokeStyle = "black";
  ctx.stroke();

  let str = id; //drawing node id when in 'drawnodes' mode is more useful for working out roollingsstock/layouts
  if (!showNNodes && typeof trackLayout.tracks[id].label === "string") {
    str = trackLayout.tracks[id].label;
  }
  drawTextOutlinedScreen(str, x, y, {
    size: 11,
    align: "center",
    dy: -NODERADIUSPX + 10,
  });
}

function drawPointLegs(ctx, id, x, y) {
  const legs = getActiveLegs(id);
  if (legs) {
    const { fixed, active, inactive } = legs,
      r = px(NODERADIUSPX);
    [fixed, active].forEach((n) => {
      const [nx, ny] = trackLayout.tracks[n].position;
      const ang = Math.atan2(ny - y, nx - x);
      ctx.beginPath();
      ctx.moveTo(x, y);
      ctx.lineTo(x + Math.cos(ang) * r, y + Math.sin(ang) * r);
      ctx.lineWidth = TRACKWIDTH;
      ctx.strokeStyle = "black";
      ctx.stroke();
    });
    if (inactive) {
      const [nxIn, nyIn] = trackLayout.tracks[inactive].position;
      const [nxAct, nyAct] = trackLayout.tracks[active].position;

      const angInactive = Math.atan2(nyIn - y, nxIn - x);
      const angActive = Math.atan2(nyAct - y, nxAct - x);

      // normalize angle difference to [-π, π]
      let dAng = angInactive - angActive;
      while (dAng > Math.PI) dAng -= 2 * Math.PI;
      while (dAng < -Math.PI) dAng += 2 * Math.PI;

      const sweep = Math.PI / 4; // 45°
      const arcRadius = r + px(0);

      ctx.beginPath();

      if (dAng > 0) {
        // inactive is CCW from active → sweep further CCW (away from active)
        ctx.arc(x, y, arcRadius, angInactive, angInactive + sweep);
      } else {
        // inactive is CW from active → sweep further CW (away from active)
        ctx.arc(x, y, arcRadius, angInactive, angInactive - sweep, true);
      }

      ctx.strokeStyle = STOP_COLOUR;
      ctx.lineWidth = px(4);
      ctx.stroke();
    }
  }
}

function drawPointSignal(ctx, id) {
  const node = trackLayout.tracks[id];
  if (!node || !node.directionvector) return;

  const [x, y] = node.position;

  // parse directionvector "N2-P1"
  const [fromId, toId] = node.directionvector.split("-");
  const fromNode = trackLayout.tracks[fromId];
  const toNode = trackLayout.tracks[toId];
  if (!fromNode || !toNode) return;

  // direction vector from 'fromId' → 'toId'
  let dx = toNode.position[0] - fromNode.position[0];
  let dy = toNode.position[1] - fromNode.position[1];
  const len = Math.hypot(dx, dy);
  if (len === 0) return;
  dx /= len;
  dy /= len;

  // arrow sizes
  const totalLength = px(NODERADIUSPX * 2);
  const headLength = px(NODERADIUSPX * 0.8);
  const headWidth  = px(NODERADIUSPX * 0.2);

  // draw arrow with black colour + white outline
  drawSignalPointArrow(
    ctx,
    x,
    y,
    dx,
    dy,
    totalLength,
    headLength,
    headWidth,
    "black"
  );
  
  // projection (for perpendicular offset direction)
  const ax = fromNode.position[0],
        ay = fromNode.position[1];
  const vx = x - ax,
        vy = y - ay;
  const s  = vx * dx + vy * dy;
  const cx = ax + dx * s;
  const cy = ay + dy * s;

  let nx = x - cx,
      ny = y - cy;
  let nlen = Math.hypot(nx, ny);
  if (nlen === 0) {
    nx = -dy;
    ny = dx;
    nlen = 1;
  }
  nx /= nlen;
  ny /= nlen;

  // midpoint across the entire arrow (shaft + head)
  const tx = x + dx * totalLength;
  const ty = y + dy * totalLength;
  const mx = (x + tx) / 2;
  const my = (y + ty) / 2;

  // perpendicular offset for the signal disc
  const margin = px(5);
  const offset = px(NODERADIUSPX) + margin;
  const cx2 = mx + nx * offset;
  const cy2 = my + ny * offset;

  // draw the signal disc
  ctx.fillStyle = signalStates[id] ? STOP_COLOUR : GO_COLOUR;
  drawNode(ctx, id, cx2, cy2);

  // optional: trigger cross overlay
  const pt = getNodeTriggerPoint(id); // world coords {x,y}
  if (pt) {
    const size = 0.7 * (TRACKWIDTH / 2); // half the cross size
    ctx.beginPath();
    ctx.moveTo(pt.x - size, pt.y - size);
    ctx.lineTo(pt.x + size, pt.y + size);
    ctx.moveTo(pt.x - size, pt.y + size);
    ctx.lineTo(pt.x + size, pt.y - size);
    ctx.strokeStyle = "white";
    ctx.lineWidth = px(1);
    ctx.stroke();
  }
}

/*
function drawPointSignal(ctx, id) {
  const node = trackLayout.tracks[id];
  if (!node || !node.directionvector) return;

  const [x, y] = node.position;

  // parse directionvector "N2-P1"
  const [fromId, toId] = node.directionvector.split("-");
  const fromNode = trackLayout.tracks[fromId];
  const toNode = trackLayout.tracks[toId];
  if (!fromNode || !toNode) return;

  // direction vector from 'fromId' → 'toId'
  let dx = toNode.position[0] - fromNode.position[0];
  let dy = toNode.position[1] - fromNode.position[1];
  const len = Math.hypot(dx, dy);
  if (len === 0) return;
  dx /= len;
  dy /= len;

  // arrow sizes
  const totalLength = px(NODERADIUSPX * 2);
  const headLength = px(NODERADIUSPX * 0.8);
  const headWidth = px(NODERADIUSPX * 0.2);

  // draw arrow with black colour + white outline
  drawSignalPointArrow(
    ctx,
    x,
    y,
    dx,
    dy,
    totalLength,
    headLength,
    headWidth,
    "black"
  );
  
  // projection
  const ax = fromNode.position[0],
    ay = fromNode.position[1];
  const vx = x - ax,
    vy = y - ay;
  const s = vx * dx + vy * dy;
  const cx = ax + dx * s;
  const cy = ay + dy * s;

  let nx = x - cx,
    ny = y - cy;
  let nlen = Math.hypot(nx, ny);
  if (nlen === 0) {
    nx = -dy;
    ny = dx;
    nlen = 1;
  }
  nx /= nlen;
  ny /= nlen;

  const shaftLength = Math.max(totalLength - headLength, 0);
  const sx = x + dx * shaftLength;
  const sy = y + dy * shaftLength;
  const mx = (x + sx) / 2;
  const my = (y + sy) / 2;

  const margin = px(4);
  const offset = px(NODERADIUSPX) + margin;
  const cx2 = mx + nx * offset;
  const cy2 = my + ny * offset;

  ctx.fillStyle = signalStates[id] ? STOP_COLOUR : GO_COLOUR;
  drawNode(ctx, id, cx2, cy2);

  const pt = getNodeTriggerPoint(id); // world coords {x,y}
    if (pt) {
      const size =.7* (TRACKWIDTH/2); // half the cross size
      ctx.beginPath();
      ctx.moveTo(pt.x - size, pt.y - size);
      ctx.lineTo(pt.x + size, pt.y + size);
      ctx.moveTo(pt.x - size, pt.y + size);
      ctx.lineTo(pt.x + size, pt.y - size);
      ctx.strokeStyle = "white";
      ctx.lineWidth = px(1);
      ctx.stroke();
    }
}*/

function getPointSignalCirclePosition(id) {
  const node = trackLayout.tracks[id];
  if (!node || node.type !== "pointsignal" || !node.directionvector)
    return null;

  const [x, y] = node.position;
  const [fromId, toId] = node.directionvector.split("-");
  const fromNode = trackLayout.tracks[fromId];
  const toNode = trackLayout.tracks[toId];
  if (!fromNode || !toNode) return null;

  // direction vector (from → to)
  let dx = toNode.position[0] - fromNode.position[0];
  let dy = toNode.position[1] - fromNode.position[1];
  const len = Math.hypot(dx, dy);
  if (len === 0) return null;
  dx /= len;
  dy /= len;

  // arrow shaft midpoint
  const totalLength = px(NODERADIUSPX * 2);
  const headLength = px(NODERADIUSPX * 0.8);
  const shaftLength = Math.max(totalLength - headLength, 0);
  const sx = x + dx * shaftLength;
  const sy = y + dy * shaftLength;
  const mx = (x + sx) / 2;
  const my = (y + sy) / 2;

  // track direction (same as above) → outward normal
  let tdx = dx,
    tdy = dy;
  const ax = fromNode.position[0],
    ay = fromNode.position[1];
  const vx = x - ax,
    vy = y - ay;
  const s = vx * tdx + vy * tdy;
  const cx = ax + tdx * s;
  const cy = ay + tdy * s;
  let nx = x - cx,
    ny = y - cy;
  let nlen = Math.hypot(nx, ny);
  if (nlen === 0) {
    nx = -tdy;
    ny = tdx;
    nlen = 1;
  }
  nx /= nlen;
  ny /= nlen;

  // final offset: radius + margin
  const margin = px(4);
  const offset = px(NODERADIUSPX) + margin;
  return [mx + nx * offset, my + ny * offset];
}

function isClickInPointSignal(id, mx, my) {
  const pos = getPointSignalCirclePosition(id);
  if (!pos) return false;

  const [cx, cy] = pos;
  return Math.hypot(mx - cx, my - cy) < px(NODERADIUSPX);
}

function alignPointToLeg(pointId, legId) {
  const legs = getActiveLegs(pointId);
  if (!legs) return false;

  // If already aligned, nothing to do.
  if (legs.active === legId) return true;

  // If the requested leg is the "inactive" branch, flip once.
  if (legs.inactive === legId) {
    if (isNodeOccupied(pointId)) return false; // safety – shouldn't happen because we checked earlier
    switchStates[pointId] = (switchStates[pointId] + 1) % 2;
    trackLayout.tracks[pointId].occupiedBy = null;
    return true;
  }

  // If someone asked for the fixed side or an unknown leg, we can't "align".
  // (In your model, pointleg should be one of the two throw legs.)
  return false;
}

function updatePointSignalsForPoint(pointId) {
  const legs = getActiveLegs(pointId);
  if (!legs) return;

  for (const sid in trackLayout.tracks) {
    const n = trackLayout.tracks[sid];
    if (n?.type === "pointsignal" && n.point === pointId) {
      if (n.pointleg !== legs.active) {
        // leg not active → STOP
        signalStates[sid] = true;
      }
      // if n.pointleg === legs.active:
      // do nothing → stays whatever it was (usually red until user clicks it)
    }
  }
}

function drawSignalPointArrow(
  ctx,
  x,
  y,
  dx,
  dy,
  totalLength,
  headLength,
  headWidth,
  colour
) {
  // shaft length
  const shaftLength = Math.max(totalLength - headLength, 0);

  // shaft end (start of arrowhead)
  const sx = x + dx * shaftLength;
  const sy = y + dy * shaftLength;

  // tip
  const tx = x + dx * totalLength;
  const ty = y + dy * totalLength;

  // shaft with outline
  ctx.beginPath();
  ctx.moveTo(x, y);
  ctx.lineTo(sx, sy);
  ctx.lineWidth = 3 / worldScale; // outline width
  ctx.strokeStyle = "white"; // outline first
  ctx.stroke();

  // arrowhead (triangle points)
  const leftX = tx - dx * headLength + dy * headWidth;
  const leftY = ty - dy * headLength - dx * headWidth;
  const rightX = tx - dx * headLength - dy * headWidth;
  const rightY = ty - dy * headLength + dx * headWidth;

  // arrowhead with outline
  ctx.beginPath();
  ctx.moveTo(tx, ty);
  ctx.lineTo(leftX, leftY);
  ctx.lineTo(rightX, rightY);
  ctx.closePath();

  ctx.lineWidth = 3 / worldScale;
  ctx.strokeStyle = "white";
  ctx.stroke();

  ctx.fillStyle = colour;
  ctx.fill();

  ctx.beginPath();
  ctx.moveTo(x, y);
  ctx.lineTo(sx, sy);
  ctx.lineWidth = 2 / worldScale; // inner line
  ctx.strokeStyle = colour;
  ctx.stroke();
}

function cleanTrackLayout(layout) {
  const validTypes = new Set([
    "buffer",
    "signal",
    "pointsignal",
    "point",
    "pass",
  ]);
  const cleaned = { ...layout, tracks: {} };

  for (const id in layout.tracks) {
    const node = layout.tracks[id];
    if (node && validTypes.has(node.type)) {
      cleaned.tracks[id] = node;
    } else {
      console.warn(`Dropping node ${id} with unknown type: ${node?.type}`);
    }
  }

  return cleaned;
}

/*------point dignal experiment------*/
// --- Pointsignal trigger geometry helpers ---
function getPointSignalTrigger(sid) {
  const s = trackLayout.tracks[sid];
  if (s._trigger) return s._trigger;

  const [a, b] = (s.triggersection || "").split("-");
  if (!trackLayout.tracks[a] || !trackLayout.tracks[b]) return null;

  const [ax, ay] = trackLayout.tracks[a].position;
  const [bx, by] = trackLayout.tracks[b].position;
  const [sx, sy] = s.position;

  // project signal icon onto segment AB
  const vx = bx - ax,
    vy = by - ay;
  const L2 = vx * vx + vy * vy || 1;
  let t = ((sx - ax) * vx + (sy - ay) * vy) / L2;
  t = Math.max(0, Math.min(1, t));
  const px = ax + t * vx;
  const py = ay + t * vy;

  // tangent and perpendicular
  const L = Math.hypot(vx, vy) || 1;
  const tx = vx / L,
    ty = vy / L;
  const nx = -ty,
    ny = tx;

  s._trigger = { px, py, tx, ty, nx, ny };
  return s._trigger;
}

function getNodeTriggerPoint(nodeId) {
  const node = trackLayout.tracks[nodeId];
  if (!node) return null;

  // plain signals and normal nodes → just use their position
  if (node.type !== "pointsignal" || !node.triggersection) {
    return { x: node.position[0], y: node.position[1] };
  }

  // pointsignal with triggersection → project onto section
  if(!node.triggerpoint)
  {
   const [a, b] = node.triggersection.split("-");
   const ta = trackLayout.tracks[a];
   const tb = trackLayout.tracks[b];
   if (!ta || !tb) return null;

   const [ax, ay] = ta.position;
   const [bx, by] = tb.position;
   const [sx, sy] = node.position; 
   const vx = bx - ax,
    vy = by - ay;
   const L2 = vx * vx + vy * vy || 1;
   let t = ((sx - ax) * vx + (sy - ay) * vy) / L2;
   t = Math.max(0, Math.min(1, t));
   node.triggerpoint={x:ax + t * vx, y:ay + t * vy};
  }

  return { x: node.triggerpoint.x, y: node.triggerpoint.y };
}


/*
function isApproachingPoint(consist, pointId, legId) {
  if(!consist.moving) return false;
  const pivot =
    consist.vehicles[consist.dir === 1 ? consist.vehicles.length - 1 : 0];
  if (!pivot?.leadPath) return false;

  const [a, b] = pivot.leadPath;

  // Determine forward direction
  let from, to;
  if (consist.dir === 1) {
    from = a;
    to = b; // moving a -> b
  } else {
    from = b;
    to = a; // moving b -> a
  }

  // If this next step is exactly the protected leg into the point
  if (from === legId && to === pointId) return true;

  // Do a limited forward search starting at "to"
  const visited = new Set([from]);
  function dfs(node, prev, depth = 0) {
    if (node === pointId && prev === legId) return true;
    if (depth > 5) return false;
    for (const next of trackLayout.tracks[node].connections) {
      if (next === prev) continue;
      if (visited.has(next)) continue;
      visited.add(next);
      if (dfs(next, node, depth + 1)) return true;
    }
    return false;
  }

  return dfs(to, from);
}

*/
function isConsistTryingToPassRedPointSignal(consist) {
  if (consist.moving) {
    for (const sid in trackLayout.tracks) {
      const node = trackLayout.tracks[sid];
      if (node.occupiedBy === consist.id && node.type === "pointsignal" && signalStates[sid]) {
        if (isApproachingPoint(consist, node.point, node.pointleg)) {
          return { blocked: true, signalId: sid };
        }
      }
    }
  }
  return { blocked: false, signalId: null };
}

function isApproachingPoint(consist, pointId, legId, opts = {}) {
  const { maxSteps = 30, returnPath = false } = opts;
  if (!consist?.moving) return returnPath ? { approaching: false, path: [] } : false;

  if (!trackLayout.tracks[pointId] || !trackLayout.adj?.[legId]) {
    return returnPath ? { approaching: false, path: [] } : false;
  }

  // use the nose vehicle just to seed the edge
  const nose = (consist.dir === 1)
    ? consist.vehicles[consist.vehicles.length - 1]
    : consist.vehicles[0];
  if (!nose?.leadPath) return returnPath ? { approaching: false, path: [] } : false;

  // orient edge
  let [a, b] = nose.leadPath;
  let from = (consist.dir === 1) ? a : b;
  let to   = (consist.dir === 1) ? b : a;

  const path = [from, to];

  for (let step = 0; step < maxSteps; step++) {
    // success if we’re entering the point via the protected leg
    if (to === pointId && from === legId) {
      return returnPath ? { approaching: true, path } : true;
    }

    // choose continuation
    let next = null;
    const node = trackLayout.tracks[to];
    if (node.type === "point") {
      const legs = getActiveLegs(to);
      if (legs) {
        if (legs.active && legs.active !== from) next = legs.active;
        else if (legs.fixed && legs.fixed !== from) next = legs.fixed;
      }
    } else {
      next = (trackLayout.adj[to] || []).find(n => n !== from);
    }

    if (!next) break;

    from = to;
    to = next;
    path.push(to);
  }

  return returnPath ? { approaching: false, path } : false;
}


function computeDirFromLoco(consist) {
  if (!consist.liveLocoId) return 1; // default
  const loco = consist.vehicles.find(v => v.id === consist.liveLocoId);
  if (!loco || !loco.leadPath || loco.leadPath.length < 2) return 1;

  const [a, b] = loco.leadPath;
  const nodeA = trackLayout.tracks[a];
  const nodeB = trackLayout.tracks[b];
  if (!nodeA || !nodeB) return 1;

  const dx = nodeB.position[0] - nodeA.position[0];
  const dy = nodeB.position[1] - nodeA.position[1];
  const pathAngle = Math.atan2(dy, dx);

  // Compare loco.heading to pathAngle
  const d = Math.abs(normalizeAngle(loco.heading - pathAngle));
  // If heading is within 90° of path → forwards
  return (d < Math.PI / 2) ? 1 : -1;
}

function normalizeAngle(a) {
  while (a <= -Math.PI) a += 2*Math.PI;
  while (a >  Math.PI) a -= 2*Math.PI;
  return a;
}

function addCarsToConsist(consist, newCars, end = "back",live,dir=1) {
  if (!consist || !newCars?.length) return;

  const GAP_DIST = GAP || 1.0;

  // --- determine reference vehicle (anchor) ---
  const anchor =
    end === "front"
      ? consist.vehicles[0]                           // add to front
      : consist.vehicles[consist.vehicles.length - 1]; // add to back

  if (!anchor || anchor.px == null || anchor.py == null) {
    console.error("[addCarsToConsist] anchor has no valid position");
    return;
  }

  // --- calculate the direction vector of the anchor end ---
  let heading = anchor.heading;
  if (consist.vehicles.length === 1) {
    // single-car: direction depends on requested end & dir
    heading = consist.dir === 1 ? anchor.heading : anchor.heading + Math.PI;
    if (end === "back") heading += Math.PI; // add to back of facing dir
  } else {
    // multi-car: invert heading when adding to front
    if (end === "front") heading += Math.PI;
  }

  // --- convert heading to a normalised vector ---
  const dx = Math.cos(heading);
  const dy = Math.sin(heading);

  // --- determine placement start point (front/back edge) ---
  const anchorEdgeX = anchor.px + dx * (anchor.length / 2);
  const anchorEdgeY = anchor.py + dy * (anchor.length / 2);

  // --- place each new car sequentially away from the anchor ---
  let cursorX = anchorEdgeX;
  let cursorY = anchorEdgeY;
  let anchorIndex=0;
  const placed = [];

  for (const car of newCars) {
    // move one gap + half car length further out
    cursorX += dx * (GAP_DIST + car.length / 2);
    cursorY += dy * (GAP_DIST + car.length / 2);

    // assign approximate world position
    car.px = cursorX;
    car.py = cursorY;
    car.heading = heading;
    car.leadPath = anchor.leadPath.slice();
    car.leadT = anchor.leadT;
    car.trail = anchor.trail ? anchor.trail.slice() : [];

    // move cursor for next car’s far edge
    cursorX += dx * (car.length / 2);
    cursorY += dy * (car.length / 2);

    placed.push(car);
  }

  // --- merge into consist list ---
  if (end === "front") {
    consist.vehicles = [...placed.reverse(), ...consist.vehicles];
    anchorIndex=consist.vehicles.length-1;//make sure it's on the satic side
  } else {
    consist.vehicles = [...consist.vehicles, ...placed];
    anchorIndex=0;//make sure it's on teh satic side
  }

  // --- recompute consist geometry but keep anchor fixed ---
  
  


  consist.liveLocoId=live;
  consist.dir=dir;
  consist.vehicles.forEach((x) => {
          if (x.type === "loco") x.neutral = x.id !== live;
        });

/*
  oldpx=consist.vehicles[anchorIndex].px;
  oldpy=consist.vehicles[anchorIndex].py;        
  normalizeConsistDirections(consist,0);//to front
  consist.vehicles[anchorIndex].px=oldpx;
  consist.vehicles[anchorIndex].py=oldpy;
*/
  solveConsistPositions(consist, anchorIndex);

  updateNodeOccupancyFromConsists();
  consist.id= `T${nextConsistId++}`;//give it a new id (other id being canclled out of animaiton callback)

  console.log(
    `[addCarsToConsist] added ${newCars.length} cars to ${end}`,
    placed.map(v => v.id),`new is `,consist.vehicles.map(v => v.id));
  
  

}

function normalizeConsistDirections(consist,anchorIdx) {

  if (!consist || !consist.vehicles?.length) return;

  const desiredDir = consist.dir;
//  const anchorIdx = getPivotIndex(consist);
  const anchor = consist.vehicles[anchorIdx];

  // Snapshot anchor world position before touching it
  const anchorPose = {
    px: anchor.px,
    py: anchor.py,
    heading: anchor.heading,
    leadPath: anchor.leadPath ? anchor.leadPath.slice() : null,
    leadT: anchor.leadT
  };

  // Normalize all vehicles except the anchor
  for (let i = 0; i < consist.vehicles.length; i++) {
    const v = consist.vehicles[i];
    if (!v.leadPath || v.leadPath.length < 2) continue;
    if (i === anchorIdx) continue;

    const [a, b] = v.leadPath;
    const nodeA = trackLayout.tracks[a];
    const nodeB = trackLayout.tracks[b];
    if (!nodeA || !nodeB) continue;

    const dx = nodeB.position[0] - nodeA.position[0];
    const dy = nodeB.position[1] - nodeA.position[1];
    const segAngle = Math.atan2(dy, dx);

    let diff = normalizeAngle(v.heading - segAngle);
    const facingForward = Math.abs(diff) < Math.PI / 2;
    const shouldFlip = desiredDir === 1 ? !facingForward : facingForward;

    if (shouldFlip) {
      v.leadPath = [b, a];
      v.leadT = 1 - v.leadT;
      v.heading = normalizeAngle(v.heading + Math.PI);
    }
  }

  // Re-anchor the consist using the saved world position
  solveConsistPositions(consist, anchorIdx);

  // Restore anchor’s physical placement exactly
  anchor.px = anchorPose.px;
  anchor.py = anchorPose.py;
  anchor.heading = anchorPose.heading;
  anchor.leadPath = anchorPose.leadPath;
  anchor.leadT = anchorPose.leadT;

  // Re-solve once more to re-chain other cars around the fixed anchor
  solveConsistPositions(consist, anchorIdx);
}