(() => {
  const el = (id) => document.getElementById(id);
  const t = (key, vars) => window.I18n?.t(key, vars) ?? key;
  const Geo = () => window.MapGeo;
  const Occ = () => window.MapOccupancyCanvas;
  const OccEdit = () => window.MapOccupancyEdit;
  const Objects = () => window.MapObjects;
  const Planner = () => window.MapPlannerZones;
  const Behavior = () => window.MapBehaviorZones;
  const Advanced = () => window.MapAdvancedZones;

  const state = {
    mapId: null,
    map: null,
    callbacks: {},
    readOnly: false,
    dirty: false,
    activeTool: "pan",
    /** Selected object type for MiR-style zones (wall | floor | position). */
    objectType: "",
    /** Saved zones — persisted to database via map.zones on save. */
    zones: [],
    /** In-progress draft: shape (wall/floor) or position placement. */
    draft: null,
    selectedZoneId: null,
    undoStack: [],
    /** Grayscale floor-plan raster (map_server PNG) was modified. */
    rasterDirty: false,
    /** Base scan layer edited by eraser (map_base.png). */
    baseDirty: false,
    /** Wall/floor zone ids already baked into composite — hide SVG unless selected. */
    bakedZoneIds: new Set(),
    /** Active brush stroke for pixel eraser. */
    eraserStroke: null,
    /** Dragging a wall/floor vertex in Select mode. */
    vertexDrag: null,
    /** Dragging erase-by-selection rectangle. */
    selectionDrag: null,
    /** Live selection rectangle (image pixels). */
    selectionRect: null,
    /** Placing a position: click anchor + drag for yaw. */
    positionDrag: null,
    /** Pending position dialog payload. */
    pendingPosition: null,
    /** Pending speed/sound zone dialog payload. */
    pendingBehavior: null,
    /** Pending directional / planner / I/O zone dialog payload. */
    pendingAdvanced: null,
    /** Line width chosen in Draw line dialog (cm). */
    lineDrawWidthCm: null,
    /** Cached sounds for sound-zone picker. */
    soundsCache: [],
    /** Suppress click after pointer drag. */
    pointerMoved: false,
    /** Layer 1 — view space (screen pan/zoom only). */
    view: Geo()?.createView(1, 0, 0) || { scale: 1, panX: 0, panY: 0 },
    panning: null,
    tipVisible: true,
    showOrigin: true,
    /** ROS yaml thresholds (occupied_thresh, free_thresh, negate) from map.yaml. */
    yamlMeta: null,
    /** Pending ROS metadata from upload dialog (set before PNG picker). */
    uploadMeta: null,
    /** Dotted path overlay when opened from Setup → Paths (view). */
    pathPreview: null,
    /** Fingerprint of preferred zones when map was loaded (for save warning). */
    initialPreferredFp: "",
  };

  const titleEl = el("mapEditorTitle");
  const dirtyEl = el("mapEditorDirty");
  const canvasWrapEl = el("mapEditorCanvasWrap");
  const viewportEl = el("mapEditorViewport");
  const canvasInnerEl = el("mapEditorCanvasInner");
  const imageLayerEl = el("mapEditorImageLayer");
  const sheetEl = el("mapEditorSheet");
  const gridEl = el("mapEditorSheetGrid");
  const imageEl = el("mapEditorImage");
  const occupancyCanvasEl = el("mapEditorOccupancyCanvas");
  const objectsSvgEl = el("mapEditorObjectsSvg");
  const objectTypeBtnEl = el("mapEditorObjectTypeBtn");
  const objectTypeIconEl = el("mapEditorObjectTypeIcon");
  const objectTypeLabelEl = el("mapEditorObjectTypeLabel");
  const objectTypeMenuEl = el("mapEditorObjectTypeMenu");
  const objectTypeOptionEls = () =>
    objectTypeMenuEl?.querySelectorAll(".mapEditorObjectTypeOption") || [];
  const originEl = el("mapEditorOrigin");
  const originHitEl = el("mapEditorOriginHit");
  const originLabelEl = el("mapEditorOriginLabel");
  const emptyEl = el("mapEditorEmpty");
  const tipEl = el("mapEditorCanvasTip");
  const statusViewEl = el("mapEditorStatusView");
  const statusImageEl = el("mapEditorStatusImage");
  const statusWorldEl = el("mapEditorStatusWorld");
  const uploadInputEl = el("mapEditorUploadInput");
  const menuDialogEl = el("mapEditorMenuDialog");
  const settingsDialogEl = el("mapEditorSettingsDialog");
  const activateDialogEl = el("mapActivateDialog");
  const uploadConfirmDialogEl = el("mapUploadConfirmDialog");
  const uploadMetaDialogEl = el("mapUploadMetaDialog");
  const uploadYamlInputEl = el("mapUploadYamlInput");
  const positionDialogEl = el("mapEditorPositionDialog");
  const positionFormEl = el("mapEditorPositionForm");
  const positionFields = {
    name: el("mapPositionName"),
    x: el("mapPositionX"),
    y: el("mapPositionY"),
    yaw: el("mapPositionYaw"),
  };
  const speedDialogEl = el("mapEditorSpeedDialog");
  const speedFormEl = el("mapEditorSpeedForm");
  const speedFields = {
    speed: el("mapSpeedMps"),
  };
  const soundDialogEl = el("mapEditorSoundDialog");
  const soundFormEl = el("mapEditorSoundForm");
  const soundFields = {
    soundId: el("mapSoundZoneSelect"),
    manageLink: el("mapSoundManageLink"),
  };
  const directionalDialogEl = el("mapEditorDirectionalDialog");
  const directionalFormEl = el("mapEditorDirectionalForm");
  const directionalFields = {
    shapePanel: el("mapDirectionalShapePanel"),
    linePanel: el("mapDirectionalLinePanel"),
    direction: el("mapDirectionalDeg"),
    reversed: el("mapDirectionalReversed"),
    lineWidth: el("mapDirectionalLineWidth"),
  };
  const plannerDialogEl = el("mapEditorPlannerDialog");
  const plannerFormEl = el("mapEditorPlannerForm");
  const plannerFields = {
    noLocalization: el("mapPlannerNoLocalization"),
    lookAhead: el("mapPlannerLookAhead"),
    ignoreObstacles: el("mapPlannerIgnoreObstacles"),
    pathDeviation: el("mapPlannerPathDeviation"),
    pathTimeout: el("mapPlannerPathTimeout"),
  };
  const ioDialogEl = el("mapEditorIoDialog");
  const ioFormEl = el("mapEditorIoForm");
  const drawLineDialogEl = el("mapEditorDrawLineDialog");
  const drawLineFormEl = el("mapEditorDrawLineForm");
  const drawLineFields = {
    width: el("mapDrawLineWidth"),
  };
  const ioFields = {
    module: el("mapIoModule"),
    plcRegister: el("mapIoPlcRegister"),
    plcValue: el("mapIoPlcValue"),
    plcMode: el("mapIoPlcMode"),
  };

  async function refreshMapIoModuleList() {
    const listEl = document.getElementById("mapIoModuleList");
    if (!listEl) return;
    let modules = [];
    try {
      if (window.IoModulesCatalog?.refresh) {
        modules = await window.IoModulesCatalog.refresh();
      } else {
        const res = await fetch("/api/io_modules", { credentials: "include" });
        if (res.ok) {
          const data = await res.json();
          modules = Array.isArray(data.io_modules) ? data.io_modules : [];
        }
      }
    } catch {
      modules = [];
    }
    listEl.innerHTML = modules
      .map((m) => `<option value="${String(m.name || "").replace(/"/g, "&quot;")}"></option>`)
      .join("");
  }

  /** Off-screen base scan layer (map_base.png — eraser edits this). */
  let baseCanvasEl = null;
  function getBaseCanvas() {
    if (!baseCanvasEl) {
      baseCanvasEl = document.createElement("canvas");
      baseCanvasEl.className = "mapEditorBaseCanvas";
      baseCanvasEl.hidden = true;
      baseCanvasEl.setAttribute("aria-hidden", "true");
    }
    return baseCanvasEl;
  }

  /** Off-screen composite = rebake(base + zones). */
  let sourceCanvasEl = null;
  function getSourceCanvas() {
    if (!sourceCanvasEl) {
      sourceCanvasEl = document.createElement("canvas");
      sourceCanvasEl.className = "mapEditorSourceCanvas";
      sourceCanvasEl.hidden = true;
      sourceCanvasEl.setAttribute("aria-hidden", "true");
    }
    return sourceCanvasEl;
  }

  const uploadMetaFields = {
    resolution: el("mapUploadResolution"),
    originX: el("mapUploadOriginX"),
    originY: el("mapUploadOriginY"),
    originYaw: el("mapUploadOriginYaw"),
    negate: el("mapUploadNegate"),
    occupiedThresh: el("mapUploadOccupiedThresh"),
    freeThresh: el("mapUploadFreeThresh"),
  };

  const toolBtnEls = () => document.querySelectorAll(".mapEditorMapTool[data-tool]");

  const settingsFields = {
    name: el("mapSettingsName"),
    desc: el("mapSettingsDesc"),
    resolution: el("mapSettingsResolution"),
    originX: el("mapSettingsOriginX"),
    originY: el("mapSettingsOriginY"),
    originYaw: el("mapSettingsOriginYaw"),
  };

  async function api(path, opts = {}) {
    const res = await fetch(path, { credentials: "include", ...opts });
    if (!res.ok) {
      let msg = res.statusText;
      try {
        const err = await res.json();
        if (err.error) msg = err.error;
      } catch {
        /* ignore */
      }
      throw new Error(msg);
    }
    if (res.status === 204) return null;
    return res.json();
  }

  function mapImageUrl(map) {
    if (!map?.id || !map.image_file) return null;
    return `/api/maps/${encodeURIComponent(map.id)}/image?t=${encodeURIComponent(map.updated_at || "")}`;
  }

  /** Base scan layer for editor rebake (map_base.png). */
  function mapBaseImageUrl(map) {
    if (!map?.id || !map.image_file) return null;
    return `/api/maps/${encodeURIComponent(map.id)}/image/base?t=${encodeURIComponent(map.updated_at || "")}`;
  }

  function viewportSize() {
    const rect = viewportEl?.getBoundingClientRect();
    return { width: rect?.width || 1, height: rect?.height || 1 };
  }

  /** Layer 2 — image dimensions in floor-plan pixels. */
  function floorPlanSize() {
    return Geo()?.imageSize(state.map, imageEl) || { width: 0, height: 0 };
  }

  function hasFloorPlan() {
    return !!(
      state.map?.image_file &&
      imageEl &&
      !imageEl.hidden &&
      imageEl.naturalWidth &&
      occupancyCanvasEl &&
      !occupancyCanvasEl.hidden &&
      occupancyCanvasEl.width
    );
  }

  /** ROS yaml thresholds for occupancy coloring. */
  function mapRenderMeta() {
    const base = mapMetaForOriginDisplay() || state.map || {};
    const yaml = state.yamlMeta || {};
    return {
      occupied_thresh:
        base.occupied_thresh != null ? base.occupied_thresh : yaml.occupied_thresh,
      free_thresh: base.free_thresh != null ? base.free_thresh : yaml.free_thresh,
      negate: base.negate != null ? base.negate : yaml.negate,
    };
  }

  async function loadYamlMeta() {
    state.yamlMeta = await fetchExistingYamlMeta();
  }

  function setOccupancyCanvasVisible(visible) {
    if (!occupancyCanvasEl) return;
    occupancyCanvasEl.hidden = !visible;
    occupancyCanvasEl.setAttribute("aria-hidden", visible ? "false" : "true");
  }

  /** Load base scan, rebake composite (base + zone layers), paint display. */
  function paintOccupancyFromImage() {
    const occ = Occ();
    if (!occ || !occupancyCanvasEl || !imageEl?.naturalWidth) return false;
    initBaseFromImage();
    const ok = refreshDisplayFromSource();
    setOccupancyCanvasVisible(ok);
    return ok;
  }

  function initBaseFromImage() {
    const edit = OccEdit();
    if (!edit || !imageEl?.naturalWidth) return false;
    const ok = edit.initSourceFromImage(getBaseCanvas(), imageEl);
    if (!ok) return false;
    state.baseDirty = false;
    rebakeComposite();
    return true;
  }

  function floorPlanZoneIds() {
    const obj = Objects();
    return new Set(
      state.zones.filter((z) => obj?.isFloorPlanType(z.type)).map((z) => z.id),
    );
  }

  function rebakeComposite() {
    const edit = OccEdit();
    if (!edit?.rebakeComposite(getBaseCanvas(), getSourceCanvas(), state.zones, mapMetaForEditor())) return false;
    state.bakedZoneIds = floorPlanZoneIds();
    refreshDisplayFromSource();
    return true;
  }

  function refreshDisplayFromSource() {
    const edit = OccEdit();
    if (!edit || !occupancyCanvasEl || !getSourceCanvas().width) return false;
    return edit.renderDisplayFromSource(occupancyCanvasEl, getSourceCanvas(), mapRenderMeta(), Occ());
  }

  /**
   * Paint live occupancy grid (record/stream — roadmap step 2+).
   * @param {{ width: number, height: number, data: number[]|Int8Array|string }} grid
   */
  function paintOccupancyGrid(grid) {
    const occ = Occ();
    if (!occ || !occupancyCanvasEl || !grid) return false;
    const ok = occ.renderGrid(occupancyCanvasEl, grid);
    if (ok) setOccupancyCanvasVisible(true);
    return ok;
  }

  function setDirty(flag) {
    state.dirty = !!flag;
    if (dirtyEl) dirtyEl.hidden = !state.dirty;
    el("mapEditorSaveBtn")?.toggleAttribute("disabled", !state.dirty || state.readOnly);
  }

  function dismissCanvasTip() {
    if (!state.tipVisible) return;
    state.tipVisible = false;
    if (tipEl) tipEl.hidden = true;
  }

  function updateCanvasCursor() {
    if (!canvasWrapEl) return;
    canvasWrapEl.classList.toggle("is-pan-tool", state.activeTool === "pan" && !state.panning);
    canvasWrapEl.classList.toggle("is-panning", !!state.panning);
    canvasWrapEl.classList.toggle(
      "is-draw-tool",
      isDrawTool(state.activeTool) ||
        state.activeTool === "select" ||
        state.activeTool === "eraseShape" ||
        state.activeTool === "eraseSelection",
    );
    canvasWrapEl.classList.toggle("is-eraser-tool", state.activeTool === "eraser");
    canvasWrapEl.classList.toggle("is-erase-selection-tool", state.activeTool === "eraseSelection");
  }

  function isDrawTool(tool) {
    return tool === "drawLine" || tool === "drawShape";
  }

  function drawToolSupported(tool) {
    const obj = Objects();
    const type = state.objectType;
    if (!obj || !type) return false;
    if (tool === "drawLine") return obj.supportsDrawLine(type);
    if (tool === "drawShape") return obj.supportsDrawShape(type);
    return false;
  }

  function isInteractionBlocked() {
    return !!(
      state.draft ||
      state.eraserStroke ||
      state.vertexDrag ||
      state.selectionDrag ||
      state.positionDrag
    );
  }

  function clearTransientDrag() {
    state.vertexDrag = null;
    state.selectionDrag = null;
    state.selectionRect = null;
    state.positionDrag = null;
    state.pointerMoved = false;
  }

  function setActiveTool(tool) {
    const allowed = ["pan", "drawLine", "drawShape", "select", "eraser", "eraseShape", "eraseSelection"];
    if (!allowed.includes(tool)) return;
    if (tool !== "pan" && !state.objectType) return;
    if (isDrawTool(tool) && !drawToolSupported(tool)) return;
    if (isDrawTool(tool) && isDrawTool(state.activeTool) && tool !== state.activeTool) cancelDraft();
    if (!isDrawTool(tool)) cancelDraft();
    state.eraserStroke = null;
    clearTransientDrag();
    state.activeTool = tool;
    toolBtnEls().forEach((btn) => {
      btn.classList.toggle("is-active", btn.dataset.tool === tool);
    });
    updateCanvasCursor();
    renderObjects();
  }

  function beginDrawLine() {
    if (state.readOnly || !drawToolSupported("drawLine")) return;
    const obj = Objects();
    const type = state.objectType;
    const minWidth = obj?.minLineWidthCm(type) ?? 10;
    const defaultW = state.lineDrawWidthCm ?? obj?.defaultLineWidthCm(type) ?? 40;
    if (drawLineFields.width) {
      drawLineFields.width.min = String(minWidth);
      drawLineFields.width.step = "1";
      drawLineFields.width.value = String(Math.round(defaultW));
    }
    drawLineDialogEl?.showModal();
  }

  function cancelDrawLineDialog() {
    drawLineDialogEl?.close();
  }

  function commitDrawLineDialog() {
    const obj = Objects();
    const type = state.objectType;
    const minWidth = obj?.minLineWidthCm(type) ?? 10;
    const widthCm = Number(drawLineFields.width?.value);
    if (!Number.isFinite(widthCm) || widthCm < minWidth) {
      alert(t("maps.editor.drawLine.lineWidthInvalid"));
      return false;
    }
    state.lineDrawWidthCm = widthCm;
    if (state.draft) cancelDraft();
    setActiveTool("drawLine");
    drawLineDialogEl?.close();
    return true;
  }

  function syncZonesFromMap() {
    state.zones = Objects()?.parseZones(state.map?.zones, mapMetaForEditor()) || [];
    state.undoStack = [];
    state.draft = null;
    state.selectedZoneId = null;
    state.eraserStroke = null;
    if (getBaseCanvas().width) {
      rebakeComposite();
    } else {
      state.bakedZoneIds = floorPlanZoneIds();
    }
    renderObjects();
    updateUndoUi();
  }

  function pushUndo() {
    const edit = OccEdit();
    state.undoStack.push({
      zones: JSON.parse(JSON.stringify(state.zones)),
      baseRaster: edit?.cloneRaster(getBaseCanvas()),
      bakedIds: [...state.bakedZoneIds],
    });
    if (state.undoStack.length > 30) state.undoStack.shift();
    updateUndoUi();
  }

  function updateUndoUi() {
    const blocked = isInteractionBlocked();
    el("mapEditorUndoBtn")?.toggleAttribute(
      "disabled",
      !state.undoStack.length || state.readOnly || blocked,
    );
  }

  function undoZones() {
    if (!state.undoStack.length || isInteractionBlocked()) return;
    const snap = state.undoStack.pop();
    state.zones = snap.zones;
    state.bakedZoneIds = new Set(snap.bakedIds || []);
    state.draft = null;
    state.selectedZoneId = null;
    OccEdit()?.restoreRaster(getBaseCanvas(), snap.baseRaster);
    rebakeComposite();
    renderObjects();
    setDirty(true);
    state.rasterDirty = true;
    state.baseDirty = true;
    updateUndoUi();
  }

  function mapMetaForEditor() {
    return mapMetaForOriginDisplay() || state.map || {};
  }

  function preferredZonesFingerprint(zones) {
    return zones
      .filter((z) => z && z.type === "preferred")
      .map((z) =>
        JSON.stringify({
          id: z.id,
          points: z.points,
          geometry: z.geometry,
          line_width_cm: z.line_width_cm,
        }),
      )
      .sort()
      .join("|");
  }

  function renderPathPreview() {
    if (!objectsSvgEl || !state.pathPreview?.points?.length) return;
    const geo = Geo();
    const meta = mapMetaForEditor();
    const { width, height } = floorPlanSize();
    if (!geo || !width) return;
    const pts = state.pathPreview.points
      .map((p) => {
        const px = geo.worldToPixel(meta, width, height, p.x, p.y);
        return `${px.x},${px.y}`;
      })
      .join(" ");
    const poly = document.createElementNS("http://www.w3.org/2000/svg", "polyline");
    poly.setAttribute("points", pts);
    poly.setAttribute("fill", "none");
    poly.setAttribute("stroke", "#2563eb");
    poly.setAttribute("stroke-width", "3");
    poly.setAttribute("stroke-dasharray", "8 6");
    poly.setAttribute("stroke-linecap", "round");
    poly.setAttribute("stroke-linejoin", "round");
    poly.classList.add("mapEditorPathPreview");
    poly.setAttribute("pointer-events", "none");
    objectsSvgEl.appendChild(poly);
  }

  function renderObjects() {
    const obj = Objects();
    if (!objectsSvgEl || !obj) return;
    const has = hasFloorPlan();
    const { width, height } = floorPlanSize();
    if (!has || !width) {
      objectsSvgEl.hidden = true;
      objectsSvgEl.setAttribute("aria-hidden", "true");
      return;
    }
    objectsSvgEl.hidden = false;
    objectsSvgEl.setAttribute("aria-hidden", "false");
    objectsSvgEl.setAttribute("width", String(width));
    objectsSvgEl.setAttribute("height", String(height));
    objectsSvgEl.setAttribute("viewBox", `0 0 ${width} ${height}`);
    const visibleZones = state.zones.filter(
      (z) =>
        !obj.isFloorPlanType(z.type) ||
        !state.bakedZoneIds.has(z.id) ||
        z.id === state.selectedZoneId,
    );
    obj.render(objectsSvgEl, visibleZones, {
      mapMeta: mapMetaForEditor(),
      imageWidth: width,
      imageHeight: height,
      selectedId: state.selectedZoneId,
      showVertices: state.activeTool === "select",
      draft: state.draft,
      selectionRect: state.selectionRect,
    });
    renderPathPreview();
  }

  function imagePointFromEvent(evt) {
    const geo = Geo();
    const { width, height } = floorPlanSize();
    const rect = sheetEl?.getBoundingClientRect();
    return geo?.clientToImage(evt.clientX, evt.clientY, rect, width, height);
  }

  function removeZone(id) {
    const obj = Objects();
    const zone = state.zones.find((z) => z.id === id);
    pushUndo();
    state.zones = state.zones.filter((z) => z.id !== id);
    if (state.selectedZoneId === id) state.selectedZoneId = null;
    if (obj?.isFloorPlanType(zone?.type)) {
      rebakeComposite();
      state.rasterDirty = true;
    }
    renderObjects();
    setDirty(true);
  }

  function commitDraft() {
    const obj = Objects();
    if (!state.draft || state.draft.kind !== "shape" || !obj) return false;
    const drawMode = state.draft.drawMode || "shape";
    const zoneType = obj.resolveZoneTypeForDraw(state.draft.type, drawMode);
    const lineWidthCm =
      state.draft.line_width_cm ??
      state.lineDrawWidthCm ??
      obj.defaultLineWidthCm(state.draft.type);
    const extra = {};
    if (drawMode === "line" && obj.isPlannerZoneType(state.draft.type)) {
      extra.geometry = obj.GEOMETRY_LINE;
      extra.line_width_cm = lineWidthCm;
    }
    if (state.draft.type === obj.TYPES.speed) {
      extra.speed_mps = Behavior()?.DEFAULT_SPEED_MPS ?? 0.8;
    }
    if (zoneType === obj.TYPES.directional) {
      extra.direction_deg = 0;
    }
    if (zoneType === obj.TYPES.directional_line || zoneType === obj.TYPES.wall) {
      extra.line_width_cm = lineWidthCm;
    }
    if (state.draft.type === obj.TYPES.planner) {
      Object.assign(extra, Advanced()?.DEFAULT_PLANNER || {});
    }
    if (state.draft.type === obj.TYPES.io) {
      extra.io_module = "";
    }
    const zone = obj.createZone(zoneType, state.draft.points, extra);
    if (!zone) return false;
    pushUndo();
    state.zones = [...state.zones, zone];
    state.draft = null;
    if (obj.isFloorPlanType(zone.type)) {
      rebakeComposite();
      state.rasterDirty = true;
    }
    renderObjects();
    setDirty(true);
    updateDraftUi();
    updateUndoUi();
    if (obj.isBehaviorZoneType(zone.type)) {
      state.selectedZoneId = zone.id;
      renderObjects();
      openBehaviorDialog(zone.id, "create");
    }
    if (obj.isAdvancedZoneType(zone.type)) {
      state.selectedZoneId = zone.id;
      renderObjects();
      openAdvancedDialog(zone.id, "create");
    }
    return true;
  }

  function cancelDraft() {
    state.draft = null;
    state.positionDrag = null;
    updateDraftUi();
    updateUndoUi();
    renderObjects();
  }

  function updateDraftUi() {
    const obj = Objects();
    const shapeDraft = state.draft?.kind === "shape" ? state.draft : null;
    const canConfirm =
      shapeDraft &&
      obj &&
      shapeDraft.points.length >= obj.minPointsForDraw(shapeDraft.type, shapeDraft.drawMode || "shape");
    const confirmBtn = el("mapEditorConfirmDrawBtn");
    if (confirmBtn) {
      confirmBtn.hidden = !shapeDraft;
      confirmBtn.toggleAttribute("disabled", !canConfirm || state.readOnly);
    }
  }

  function objectTypeLabelKey(type) {
    if (type === "wall") return "maps.editor.objectType.wall";
    if (type === "floor") return "maps.editor.objectType.floor";
    if (type === "position") return "maps.editor.objectType.position";
    if (type === "forbidden") return "maps.editor.objectType.forbidden";
    if (type === "preferred") return "maps.editor.objectType.preferred";
    if (type === "unpreferred") return "maps.editor.objectType.unpreferred";
    if (type === "speed") return "maps.editor.objectType.speed";
    if (type === "sound") return "maps.editor.objectType.sound";
    if (type === "directional") return "maps.editor.objectType.directional";
    if (type === "directional_line") return "maps.editor.objectType.directionalLine";
    if (type === "planner") return "maps.editor.objectType.planner";
    if (type === "io") return "maps.editor.objectType.io";
    return "maps.editor.objectTypesNone";
  }

  function objectTypeIconMarkup(type) {
    if (type === "wall") {
      return `<svg width="18" height="18" viewBox="0 0 18 18"><path d="M3 14L15 4" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round"/></svg>`;
    }
    if (type === "floor") {
      return `<svg width="18" height="18" viewBox="0 0 18 18"><polygon points="3,14 9,4 15,14" fill="currentColor" fill-opacity="0.18" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/></svg>`;
    }
    if (type === "position") {
      return `<svg width="18" height="18" viewBox="0 0 18 18"><circle cx="4.5" cy="9" r="2.2" fill="currentColor"/><path d="M7 9h8M13 9l-2.5-2.2M13 9l-2.5 2.2" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg>`;
    }
    if (type === "forbidden") {
      return `<svg width="18" height="18" viewBox="0 0 18 18"><polygon points="3,14 9,4 15,14" fill="currentColor" fill-opacity="0.22" stroke="currentColor" stroke-width="1.5"/><path d="M5 13L13 5" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/></svg>`;
    }
    if (type === "preferred") {
      return `<svg width="18" height="18" viewBox="0 0 18 18"><polygon points="3,14 9,4 15,14" fill="currentColor" fill-opacity="0.28" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/></svg>`;
    }
    if (type === "unpreferred") {
      return `<svg width="18" height="18" viewBox="0 0 18 18"><polygon points="3,14 9,4 15,14" fill="currentColor" fill-opacity="0.18" stroke="currentColor" stroke-width="1.5" stroke-dasharray="3 2" stroke-linejoin="round"/></svg>`;
    }
    if (type === "speed") {
      return `<svg width="18" height="18" viewBox="0 0 18 18"><polygon points="3,14 9,4 15,14" fill="currentColor" fill-opacity="0.2" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/><path d="M6 11h6M9 8v3" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>`;
    }
    if (type === "sound") {
      return `<svg width="18" height="18" viewBox="0 0 18 18"><polygon points="3,14 9,4 15,14" fill="currentColor" fill-opacity="0.18" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/><path d="M6 10.5v-3h2l2.5-1.5v7L8 11.5H6z" fill="currentColor" fill-opacity="0.5"/></svg>`;
    }
    if (type === "directional") {
      return `<svg width="18" height="18" viewBox="0 0 18 18"><polygon points="3,14 9,4 15,14" fill="currentColor" fill-opacity="0.16" stroke="currentColor" stroke-width="1.5"/><path d="M6 9h6M10 9l-2-2M10 9l-2 2" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>`;
    }
    if (type === "directional_line") {
      return `<svg width="18" height="18" viewBox="0 0 18 18"><path d="M3 14L15 4" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round"/><path d="M11 4h4v4" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>`;
    }
    if (type === "planner") {
      return `<svg width="18" height="18" viewBox="0 0 18 18"><polygon points="3,14 9,4 15,14" fill="currentColor" fill-opacity="0.14" stroke="currentColor" stroke-width="1.5" stroke-dasharray="4 2"/><circle cx="9" cy="9" r="2" fill="currentColor"/></svg>`;
    }
    if (type === "io") {
      return `<svg width="18" height="18" viewBox="0 0 18 18"><polygon points="3,14 9,4 15,14" fill="currentColor" fill-opacity="0.12" stroke="currentColor" stroke-width="1.5"/><path d="M6.5 10h5M8 8v4M10 8v4" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/></svg>`;
    }
    return `<svg width="18" height="18" viewBox="0 0 18 18"><rect x="3" y="3" width="12" height="12" rx="1.5" fill="none" stroke="currentColor" stroke-width="1.4" stroke-dasharray="2.5 2"/></svg>`;
  }

  function closeObjectTypeMenu() {
    objectTypeMenuEl?.setAttribute("hidden", "");
    objectTypeBtnEl?.setAttribute("aria-expanded", "false");
    objectTypeMenuEl?.removeAttribute("style");
    el("mapEditorObjectTypePicker")?.classList.remove("is-open");
  }

  function positionObjectTypeMenu() {
    if (!objectTypeBtnEl || !objectTypeMenuEl || objectTypeMenuEl.hidden) return;
    const rect = objectTypeBtnEl.getBoundingClientRect();
    objectTypeMenuEl.style.position = "fixed";
    objectTypeMenuEl.style.top = `${rect.bottom + 1}px`;
    objectTypeMenuEl.style.left = `${rect.left}px`;
    objectTypeMenuEl.style.width = `${rect.width}px`;
    objectTypeMenuEl.style.right = "auto";
    objectTypeMenuEl.style.zIndex = "200";
  }

  function openObjectTypeMenu() {
    if (objectTypeBtnEl?.disabled) return;
    objectTypeMenuEl?.removeAttribute("hidden");
    objectTypeBtnEl?.setAttribute("aria-expanded", "true");
    el("mapEditorObjectTypePicker")?.classList.add("is-open");
    positionObjectTypeMenu();
  }

  function toggleObjectTypeMenu() {
    if (objectTypeMenuEl?.hidden) openObjectTypeMenu();
    else closeObjectTypeMenu();
  }

  function updateObjectTypePickerUi() {
    const type = state.objectType || "";
    if (objectTypeIconEl) {
      objectTypeIconEl.innerHTML = objectTypeIconMarkup(type);
      objectTypeIconEl.className = "mapEditorObjectTypeIcon";
      if (type === "wall") objectTypeIconEl.classList.add("mapEditorObjectTypeOptionIcon--wall");
      if (type === "floor") objectTypeIconEl.classList.add("mapEditorObjectTypeOptionIcon--floor");
      if (type === "position") objectTypeIconEl.classList.add("mapEditorObjectTypeOptionIcon--position");
      if (type === "forbidden") objectTypeIconEl.classList.add("mapEditorObjectTypeOptionIcon--forbidden");
      if (type === "preferred") objectTypeIconEl.classList.add("mapEditorObjectTypeOptionIcon--preferred");
      if (type === "unpreferred") objectTypeIconEl.classList.add("mapEditorObjectTypeOptionIcon--unpreferred");
      if (type === "speed") objectTypeIconEl.classList.add("mapEditorObjectTypeOptionIcon--speed");
      if (type === "sound") objectTypeIconEl.classList.add("mapEditorObjectTypeOptionIcon--sound");
      if (type === "directional") objectTypeIconEl.classList.add("mapEditorObjectTypeOptionIcon--directional");
      if (type === "directional_line") objectTypeIconEl.classList.add("mapEditorObjectTypeOptionIcon--directionalLine");
      if (type === "planner") objectTypeIconEl.classList.add("mapEditorObjectTypeOptionIcon--planner");
      if (type === "io") objectTypeIconEl.classList.add("mapEditorObjectTypeOptionIcon--io");
    }
    if (objectTypeLabelEl) {
      objectTypeLabelEl.textContent = t(objectTypeLabelKey(type));
    }
    objectTypeOptionEls().forEach((node) => {
      const selected = (node.dataset.value || "") === type;
      node.classList.toggle("is-selected", selected);
      node.setAttribute("aria-selected", selected ? "true" : "false");
    });
  }

  function updateObjectToolsUi() {
    const canEdit = hasFloorPlan() && !state.readOnly;
    const hasType = !!state.objectType && canEdit;
    const obj = Objects();
    const isOverlay = obj?.isOverlayObjectType(state.objectType);
    const type = state.objectType || "";
    const canLine = hasType && obj?.supportsDrawLine(type);
    const canShape = hasType && obj?.supportsDrawShape(type);
    objectTypeBtnEl?.toggleAttribute("disabled", !canEdit);
    if (!canEdit) closeObjectTypeMenu();
    el("mapEditorDrawLineBtn")?.toggleAttribute("disabled", !canLine);
    el("mapEditorDrawShapeBtn")?.toggleAttribute("disabled", !canShape);
    el("mapEditorSelectBtn")?.toggleAttribute("disabled", !hasType);
    el("mapEditorEraserBtn")?.toggleAttribute("disabled", !hasType || isOverlay);
    el("mapEditorEraseShapeBtn")?.toggleAttribute("disabled", !hasType);
    el("mapEditorEraseSelectionBtn")?.toggleAttribute("disabled", !hasType || isOverlay);
    updateObjectTypePickerUi();
    updateDraftUi();
  }

  function setObjectType(type) {
    const allowed = new Set([
      "wall",
      "floor",
      "position",
      "forbidden",
      "preferred",
      "unpreferred",
      "speed",
      "sound",
      "directional",
      "directional_line",
      "planner",
      "io",
      "",
    ]);
    state.objectType = allowed.has(type) ? type : "";
    cancelDraft();
    closeObjectTypeMenu();
    updateObjectTypePickerUi();
    const obj = Objects();
    if (state.objectType === obj?.TYPES.position) setActiveTool("select");
    else if (state.objectType) {
      const defaultTool = obj?.defaultDrawTool(state.objectType) || "drawShape";
      if (defaultTool === "drawLine") beginDrawLine();
      else setActiveTool(defaultTool);
    } else setActiveTool("pan");
    updateObjectToolsUi();
  }

  function pointInRect(x0, y0, x1, y1, px, py) {
    const x = Math.min(x0, x1);
    const y = Math.min(y0, y1);
    const w = Math.abs(x1 - x0);
    const h = Math.abs(y1 - y0);
    return px >= x && px <= x + w && py >= y && py <= y + h;
  }

  function zoneIntersectsRect(zone, x0, y0, x1, y1) {
    const obj = Objects();
    if (!obj?.isFloorPlanType(zone?.type) || !zone.points?.length) return false;
    return zone.points.some((p) => pointInRect(x0, y0, x1, y1, p.x, p.y));
  }

  function removeZonesInRect(x0, y0, x1, y1, withUndo = true) {
    const obj = Objects();
    const toRemove = state.zones.filter(
      (z) => obj?.isFloorPlanType(z.type) && zoneIntersectsRect(z, x0, y0, x1, y1),
    );
    if (!toRemove.length) return false;
    if (withUndo) pushUndo();
    const ids = new Set(toRemove.map((z) => z.id));
    state.zones = state.zones.filter((z) => !ids.has(z.id));
    if (state.selectedZoneId && ids.has(state.selectedZoneId)) state.selectedZoneId = null;
    rebakeComposite();
    renderObjects();
    setDirty(true);
    state.rasterDirty = true;
    return true;
  }

  function applyVertexMove(zoneId, pointIndex, pt) {
    const obj = Objects();
    const zone = state.zones.find((z) => z.id === zoneId);
    if (!zone?.points?.[pointIndex] || !pt) return;
    zone.points[pointIndex] = { x: pt.x, y: pt.y };
    if (obj?.isFloorPlanType(zone.type)) {
      rebakeComposite();
      state.rasterDirty = true;
    }
    renderObjects();
    setDirty(true);
  }

  function beginVertexDrag(evt) {
    if (state.activeTool !== "select" || !state.objectType || state.readOnly) return false;
    const pt = imagePointFromEvent(evt);
    const obj = Objects();
    if (!pt || !obj) return false;
    const hit = obj.hitTestVertex(state.zones, pt.x, pt.y, state.selectedZoneId);
    if (!hit) return false;
    evt.preventDefault();
    dismissCanvasTip();
    pushUndo();
    state.selectedZoneId = hit.zoneId;
    state.vertexDrag = hit;
    state.pointerMoved = false;
    updateUndoUi();
    renderObjects();
    return true;
  }

  function continueVertexDrag(evt) {
    if (!state.vertexDrag) return;
    const pt = imagePointFromEvent(evt);
    if (!pt) return;
    state.pointerMoved = true;
    applyVertexMove(state.vertexDrag.zoneId, state.vertexDrag.pointIndex, pt);
  }

  function endVertexDrag() {
    if (!state.vertexDrag) return;
    state.vertexDrag = null;
    updateUndoUi();
  }

  function beginSelectionDrag(evt) {
    if (state.activeTool !== "eraseSelection" || !state.objectType || state.readOnly) return false;
    const obj = Objects();
    if (!obj?.isFloorPlanType(state.objectType)) return false;
    const pt = imagePointFromEvent(evt);
    if (!pt) return false;
    evt.preventDefault();
    dismissCanvasTip();
    state.selectionDrag = { x0: pt.x, y0: pt.y };
    state.selectionRect = { x0: pt.x, y0: pt.y, x1: pt.x, y1: pt.y };
    state.pointerMoved = false;
    renderObjects();
    return true;
  }

  function continueSelectionDrag(evt) {
    if (!state.selectionDrag) return;
    const pt = imagePointFromEvent(evt);
    if (!pt) return;
    state.pointerMoved = true;
    state.selectionRect = {
      x0: state.selectionDrag.x0,
      y0: state.selectionDrag.y0,
      x1: pt.x,
      y1: pt.y,
    };
    renderObjects();
  }

  function endSelectionDrag() {
    if (!state.selectionDrag || !state.selectionRect) {
      state.selectionDrag = null;
      state.selectionRect = null;
      return;
    }
    const { x0, y0, x1, y1 } = state.selectionRect;
    const w = Math.abs(x1 - x0);
    const h = Math.abs(y1 - y0);
    state.selectionDrag = null;
    state.selectionRect = null;
    if (w >= 3 && h >= 3 && state.pointerMoved) {
      pushUndo();
      OccEdit()?.eraseRect(getBaseCanvas(), x0, y0, x1, y1, state.objectType);
      removeZonesInRect(x0, y0, x1, y1, false);
      rebakeComposite();
      state.baseDirty = true;
      state.rasterDirty = true;
      setDirty(true);
    }
    state.pointerMoved = false;
    renderObjects();
    updateUndoUi();
  }

  function beginPositionDrag(evt) {
    const obj = Objects();
    if (
      state.activeTool !== "select" ||
      state.objectType !== obj?.TYPES.position ||
      state.readOnly
    ) {
      return false;
    }
    const pt = imagePointFromEvent(evt);
    if (!pt) return false;
    evt.preventDefault();
    dismissCanvasTip();
    const yaw = obj.yawFromPoints(pt, pt);
    state.positionDrag = { px: pt.x, py: pt.y };
    state.draft = { kind: "position", px: pt.x, py: pt.y, yaw };
    state.pointerMoved = false;
    updateUndoUi();
    renderObjects();
    return true;
  }

  function continuePositionDrag(evt) {
    if (!state.positionDrag || state.draft?.kind !== "position") return;
    const pt = imagePointFromEvent(evt);
    const obj = Objects();
    if (!pt || !obj) return;
    state.pointerMoved = true;
    state.draft.yaw = obj.yawFromPoints(
      { x: state.positionDrag.px, y: state.positionDrag.py },
      pt,
    );
    renderObjects();
  }

  function endPositionDrag() {
    if (!state.positionDrag || state.draft?.kind !== "position") return;
    const anchor = state.positionDrag;
    const draft = state.draft;
    state.positionDrag = null;
    if (!state.pointerMoved) {
      state.draft = null;
      renderObjects();
      updateUndoUi();
      return;
    }
    openPositionDialogFromDraft(anchor.px, anchor.py, draft.yaw);
    state.draft = null;
    updateUndoUi();
    renderObjects();
  }

  function openPositionDialogFromDraft(px, py, yawRad, zoneId = null) {
    const geo = Geo();
    const { width, height } = floorPlanSize();
    const mapMeta = mapMetaForEditor();
    const world = geo?.pixelToWorld(mapMeta, width, height, px, py) || { x: 0, y: 0 };
    const existing = zoneId ? state.zones.find((z) => z.id === zoneId) : null;
    state.pendingPosition = {
      mode: existing ? "edit" : "create",
      zoneId: existing?.id || null,
      px,
      py,
      yaw: existing ? Number(existing.yaw) : yawRad,
    };
    if (positionFields.name) positionFields.name.value = existing?.name || "";
    if (positionFields.x) positionFields.x.value = (existing ? existing.x : world.x).toFixed(3);
    if (positionFields.y) positionFields.y.value = (existing ? existing.y : world.y).toFixed(3);
    const storedYaw = existing ? Number(existing.yaw) : yawRad;
    const yawDeg = (-storedYaw * 180) / Math.PI;
    if (positionFields.yaw) positionFields.yaw.value = yawDeg.toFixed(1);
    positionDialogEl?.showModal();
  }

  function commitPositionFromDialog() {
    const obj = Objects();
    if (!obj || !state.pendingPosition) return false;
    const name = positionFields.name?.value.trim() || "";
    const worldX = Number(positionFields.x?.value);
    const worldY = Number(positionFields.y?.value);
    const yawDeg = Number(positionFields.yaw?.value);
    const yaw = (-yawDeg * Math.PI) / 180;
    if (!Number.isFinite(worldX) || !Number.isFinite(worldY) || !Number.isFinite(yawDeg)) {
      alert(t("maps.editor.position.invalid"));
      return false;
    }
    pushUndo();
    if (state.pendingPosition.mode === "edit" && state.pendingPosition.zoneId) {
      state.zones = state.zones.map((z) =>
        z.id === state.pendingPosition.zoneId
          ? { ...z, name, x: worldX, y: worldY, yaw }
          : z,
      );
      state.selectedZoneId = state.pendingPosition.zoneId;
    } else {
      const zone = obj.createPosition(worldX, worldY, yaw, name);
      if (!zone) return false;
      state.zones = [...state.zones, zone];
      state.selectedZoneId = zone.id;
    }
    state.pendingPosition = null;
    positionDialogEl?.close();
    renderObjects();
    setDirty(true);
    updateUndoUi();
    return true;
  }

  async function fetchSoundsList() {
    try {
      const res = await fetch("/api/sounds", { credentials: "include" });
      if (!res.ok) throw new Error("failed");
      const data = await res.json();
      state.soundsCache = Array.isArray(data.sounds) ? data.sounds : [];
    } catch {
      state.soundsCache = [];
    }
    return state.soundsCache;
  }

  function fillSoundZoneSelect(selectedId = "") {
    const select = soundFields.soundId;
    if (!select) return;
    select.innerHTML = "";
    const empty = document.createElement("option");
    empty.value = "";
    empty.textContent = t("maps.editor.sound.noSound");
    select.appendChild(empty);
    state.soundsCache.forEach((s) => {
      const opt = document.createElement("option");
      opt.value = s.id;
      opt.textContent = s.name || s.id;
      if (s.id === selectedId) opt.selected = true;
      select.appendChild(opt);
    });
    if (selectedId && !state.soundsCache.some((s) => s.id === selectedId)) {
      const missing = document.createElement("option");
      missing.value = selectedId;
      missing.textContent = selectedId;
      missing.selected = true;
      select.appendChild(missing);
    }
  }

  async function openBehaviorDialog(zoneId, mode = "edit") {
    const obj = Objects();
    const zone = state.zones.find((z) => z.id === zoneId);
    if (!obj || !zone) return;
    state.pendingBehavior = { mode, zoneId, type: zone.type };
    if (zone.type === obj.TYPES.speed) {
      if (speedFields.speed) {
        speedFields.speed.min = String(Behavior()?.SPEED_MIN ?? 0.1);
        speedFields.speed.max = String(Behavior()?.SPEED_MAX ?? 1.5);
        speedFields.speed.step = "0.05";
        speedFields.speed.value = String(zone.speed_mps ?? Behavior()?.DEFAULT_SPEED_MPS ?? 0.8);
      }
      speedDialogEl?.showModal();
      return;
    }
    if (zone.type === obj.TYPES.sound) {
      await fetchSoundsList();
      fillSoundZoneSelect(zone.sound_id || "");
      soundDialogEl?.showModal();
    }
  }

  function cancelBehaviorDialog() {
    const pending = state.pendingBehavior;
    state.pendingBehavior = null;
    if (pending?.mode === "create" && pending.zoneId) {
      state.zones = state.zones.filter((z) => z.id !== pending.zoneId);
      if (state.selectedZoneId === pending.zoneId) state.selectedZoneId = null;
      renderObjects();
      setDirty(true);
    }
    speedDialogEl?.close();
    soundDialogEl?.close();
  }

  function commitSpeedFromDialog() {
    const obj = Objects();
    if (!obj || !state.pendingBehavior?.zoneId) return false;
    const speed = obj.clampSpeedMps(Number(speedFields.speed?.value));
    if (!Number.isFinite(speed)) {
      alert(t("maps.editor.speed.invalid"));
      return false;
    }
    const zoneId = state.pendingBehavior.zoneId;
    if (state.pendingBehavior.mode === "edit") pushUndo();
    state.zones = state.zones.map((z) =>
      z.id === zoneId && z.type === obj.TYPES.speed ? { ...z, speed_mps: speed } : z,
    );
    state.selectedZoneId = zoneId;
    state.pendingBehavior = null;
    speedDialogEl?.close();
    renderObjects();
    setDirty(true);
    updateUndoUi();
    return true;
  }

  function commitSoundFromDialog() {
    const obj = Objects();
    if (!obj || !state.pendingBehavior?.zoneId) return false;
    const soundId = soundFields.soundId?.value || "";
    if (!soundId) {
      alert(t("maps.editor.sound.invalid"));
      return false;
    }
    const zoneId = state.pendingBehavior.zoneId;
    if (state.pendingBehavior.mode === "edit") pushUndo();
    state.zones = state.zones.map((z) =>
      z.id === zoneId && z.type === obj.TYPES.sound ? { ...z, sound_id: soundId } : z,
    );
    state.selectedZoneId = zoneId;
    state.pendingBehavior = null;
    soundDialogEl?.close();
    renderObjects();
    setDirty(true);
    updateUndoUi();
    return true;
  }

  function fillDirectionSelect(selected = 0) {
    const select = directionalFields.direction;
    if (!select) return;
    select.innerHTML = "";
    (Advanced()?.DIRECTION_DEGREES || [0, 45, 90, 135, 180, 225, 270, 315]).forEach((deg) => {
      const opt = document.createElement("option");
      opt.value = String(deg);
      opt.textContent = t("maps.editor.directional.degOption", { deg });
      if (deg === selected) opt.selected = true;
      select.appendChild(opt);
    });
  }

  function openAdvancedDialog(zoneId, mode = "edit") {
    const obj = Objects();
    const zone = state.zones.find((z) => z.id === zoneId);
    const adv = Advanced();
    if (!obj || !zone || !adv) return;
    state.pendingAdvanced = { mode, zoneId, type: zone.type };

    if (zone.type === obj.TYPES.directional) {
      if (directionalFields.shapePanel) directionalFields.shapePanel.hidden = false;
      if (directionalFields.linePanel) directionalFields.linePanel.hidden = true;
      fillDirectionSelect(adv.normalizeDirectionDeg(zone.direction_deg));
      directionalDialogEl?.showModal();
      return;
    }
    if (zone.type === obj.TYPES.directional_line) {
      if (directionalFields.shapePanel) directionalFields.shapePanel.hidden = true;
      if (directionalFields.linePanel) directionalFields.linePanel.hidden = false;
      if (directionalFields.reversed) directionalFields.reversed.checked = !!zone.reversed;
      if (directionalFields.lineWidth) {
        directionalFields.lineWidth.value = String(
          Math.round(obj.zoneLineWidthCm(zone, mapMetaForEditor())),
        );
      }
      directionalDialogEl?.showModal();
      return;
    }
    if (zone.type === obj.TYPES.planner) {
      const s = adv.normalizePlannerSettings(zone);
      if (plannerFields.noLocalization) plannerFields.noLocalization.checked = s.no_localization;
      if (plannerFields.lookAhead) plannerFields.lookAhead.checked = s.look_ahead;
      if (plannerFields.ignoreObstacles) plannerFields.ignoreObstacles.checked = s.ignore_obstacles;
      if (plannerFields.pathDeviation) plannerFields.pathDeviation.value = String(s.path_deviation);
      if (plannerFields.pathTimeout) plannerFields.pathTimeout.value = String(s.path_timeout);
      plannerDialogEl?.showModal();
      return;
    }
    if (zone.type === obj.TYPES.io) {
      const s = adv.normalizeIoSettings(zone);
      void refreshMapIoModuleList().then(() => {
        if (ioFields.module) ioFields.module.value = s.io_module || "";
        if (ioFields.plcRegister) {
          ioFields.plcRegister.value = s.plc_register == null ? "" : String(s.plc_register);
        }
        if (ioFields.plcValue) ioFields.plcValue.value = s.plc_value == null ? "" : String(s.plc_value);
        if (ioFields.plcMode) ioFields.plcMode.value = s.plc_mode || "set";
        ioDialogEl?.showModal();
      });
    }
  }

  function cancelAdvancedDialog() {
    const pending = state.pendingAdvanced;
    state.pendingAdvanced = null;
    if (pending?.mode === "create" && pending.zoneId) {
      state.zones = state.zones.filter((z) => z.id !== pending.zoneId);
      if (state.selectedZoneId === pending.zoneId) state.selectedZoneId = null;
      renderObjects();
      setDirty(true);
    }
    directionalDialogEl?.close();
    plannerDialogEl?.close();
    ioDialogEl?.close();
  }

  function commitDirectionalFromDialog() {
    const obj = Objects();
    const adv = Advanced();
    if (!obj || !adv || !state.pendingAdvanced?.zoneId) return false;
    const zoneId = state.pendingAdvanced.zoneId;
    const zone = state.zones.find((z) => z.id === zoneId);
    if (!zone) return false;
    if (state.pendingAdvanced.mode === "edit") pushUndo();

    if (zone.type === obj.TYPES.directional) {
      const direction_deg = adv.normalizeDirectionDeg(Number(directionalFields.direction?.value));
      state.zones = state.zones.map((z) =>
        z.id === zoneId ? { ...z, direction_deg } : z,
      );
    } else if (zone.type === obj.TYPES.directional_line) {
      const line_width_cm = Number(directionalFields.lineWidth?.value);
      const minCm = obj.minLineWidthCm(obj.TYPES.directional_line);
      if (!Number.isFinite(line_width_cm) || line_width_cm < minCm) {
        alert(t("maps.editor.directional.lineWidthInvalid"));
        return false;
      }
      state.zones = state.zones.map((z) =>
        z.id === zoneId
          ? { ...z, reversed: !!directionalFields.reversed?.checked, line_width_cm }
          : z,
      );
    }

    state.selectedZoneId = zoneId;
    state.pendingAdvanced = null;
    directionalDialogEl?.close();
    renderObjects();
    setDirty(true);
    updateUndoUi();
    return true;
  }

  function commitPlannerFromDialog() {
    const obj = Objects();
    const adv = Advanced();
    if (!obj || !adv || !state.pendingAdvanced?.zoneId) return false;
    const zoneId = state.pendingAdvanced.zoneId;
    if (state.pendingAdvanced.mode === "edit") pushUndo();
    const settings = adv.normalizePlannerSettings({
      no_localization: !!plannerFields.noLocalization?.checked,
      look_ahead: !!plannerFields.lookAhead?.checked,
      ignore_obstacles: !!plannerFields.ignoreObstacles?.checked,
      path_deviation: Number(plannerFields.pathDeviation?.value),
      path_timeout: Number(plannerFields.pathTimeout?.value),
    });
    state.zones = state.zones.map((z) =>
      z.id === zoneId && z.type === obj.TYPES.planner ? { ...z, ...settings } : z,
    );
    state.selectedZoneId = zoneId;
    state.pendingAdvanced = null;
    plannerDialogEl?.close();
    renderObjects();
    setDirty(true);
    updateUndoUi();
    return true;
  }

  function commitIoFromDialog() {
    const obj = Objects();
    const adv = Advanced();
    if (!obj || !adv || !state.pendingAdvanced?.zoneId) return false;
    const io_module = ioFields.module?.value.trim() || "";
    if (!io_module) {
      alert(t("maps.editor.io.moduleRequired"));
      return false;
    }
    const zoneId = state.pendingAdvanced.zoneId;
    if (state.pendingAdvanced.mode === "edit") pushUndo();
    const settings = adv.normalizeIoSettings({
      io_module,
      plc_register: ioFields.plcRegister?.value,
      plc_value: ioFields.plcValue?.value,
      plc_mode: ioFields.plcMode?.value,
    });
    state.zones = state.zones.map((z) =>
      z.id === zoneId && z.type === obj.TYPES.io ? { ...z, ...settings } : z,
    );
    state.selectedZoneId = zoneId;
    state.pendingAdvanced = null;
    ioDialogEl?.close();
    renderObjects();
    setDirty(true);
    updateUndoUi();
    return true;
  }

  function onCanvasClick(evt) {
    if (!hasFloorPlan() || state.readOnly || state.pointerMoved) return;
    const pt = imagePointFromEvent(evt);
    if (!pt) return;
    dismissCanvasTip();
    const obj = Objects();
    const { width, height } = floorPlanSize();
    const mapMeta = mapMetaForEditor();
    if (!obj) return;

    if (
      isDrawTool(state.activeTool) &&
      (obj.isFloorPlanType(state.objectType) ||
        obj.isPlannerZoneType(state.objectType) ||
        obj.isBehaviorZoneType(state.objectType) ||
        obj.isAdvancedZoneType(state.objectType))
    ) {
      const drawMode = state.activeTool === "drawLine" ? "line" : "shape";
      if (!state.draft || state.draft.kind !== "shape" || state.draft.drawMode !== drawMode) {
        state.draft = {
          kind: "shape",
          type: state.objectType,
          drawMode,
          line_width_cm:
            state.lineDrawWidthCm ?? obj.defaultLineWidthCm(state.objectType),
          points: [],
          hover: null,
        };
        updateUndoUi();
      }
      let next = { x: pt.x, y: pt.y };
      const pts = state.draft.points;
      if (evt.shiftKey && pts.length) {
        next = obj.constrainAxis(pts[pts.length - 1], next);
      }
      if (obj.isDraftPolyline(state.draft)) {
        if (pts.length && obj.nearPoint(next, pts[pts.length - 1])) return;
        pts.push(next);
        renderObjects();
        updateDraftUi();
        return;
      }
      if (obj.isPolygonType(state.draft.type) && pts.length >= 3 && obj.nearPoint(next, pts[0])) {
        commitDraft();
        return;
      }
      if (pts.length && obj.nearPoint(next, pts[pts.length - 1])) return;
      pts.push(next);
      renderObjects();
      updateDraftUi();
      return;
    }

    if (state.activeTool === "select") {
      const hit = obj.hitTestAny(state.zones, pt.x, pt.y, mapMeta, width, height);
      state.selectedZoneId = hit?.id || null;
      renderObjects();
      return;
    }

    if (state.activeTool === "eraseShape") {
      const hit = obj.hitTestAny(state.zones, pt.x, pt.y, mapMeta, width, height);
      if (hit) removeZone(hit.id);
    }
  }

  function onCanvasDblClick(evt) {
    if (!hasFloorPlan() || state.readOnly) return;
    const obj = Objects();
    const { width, height } = floorPlanSize();
    const pt = imagePointFromEvent(evt);
    if (!pt || !obj) return;
    const hit = obj.hitTestAny(
      state.zones,
      pt.x,
      pt.y,
      mapMetaForEditor(),
      width,
      height,
    );
    if (hit?.type === obj.TYPES.position) {
      state.selectedZoneId = hit.id;
      openPositionDialogFromDraft(pt.x, pt.y, hit.yaw, hit.id);
      renderObjects();
      return;
    }
    if (obj.isBehaviorZoneType(hit?.type)) {
      state.selectedZoneId = hit.id;
      openBehaviorDialog(hit.id, "edit");
      renderObjects();
      return;
    }
    if (obj.isAdvancedZoneType(hit?.type)) {
      state.selectedZoneId = hit.id;
      openAdvancedDialog(hit.id, "edit");
      renderObjects();
    }
  }

  function beginEraserStroke(evt) {
    if (state.activeTool !== "eraser" || !state.objectType || state.readOnly) return false;
    const pt = imagePointFromEvent(evt);
    if (!pt) return false;
    evt.preventDefault();
    dismissCanvasTip();
    pushUndo();
    const edit = OccEdit();
    edit?.paintBrush(getBaseCanvas(), pt.x, pt.y, state.objectType);
    rebakeComposite();
    state.eraserStroke = { lastX: pt.x, lastY: pt.y };
    state.baseDirty = true;
    state.rasterDirty = true;
    setDirty(true);
    updateUndoUi();
    return true;
  }

  function continueEraserStroke(evt) {
    if (!state.eraserStroke || state.activeTool !== "eraser") return;
    const pt = imagePointFromEvent(evt);
    const edit = OccEdit();
    if (!pt || !edit) return;
    edit.paintBrushStroke(
      getBaseCanvas(),
      state.eraserStroke.lastX,
      state.eraserStroke.lastY,
      pt.x,
      pt.y,
      state.objectType,
    );
    state.eraserStroke.lastX = pt.x;
    state.eraserStroke.lastY = pt.y;
    rebakeComposite();
  }

  function endEraserStroke() {
    if (!state.eraserStroke) return;
    state.eraserStroke = null;
    updateUndoUi();
  }

  /** Layer 1 — apply view transform to inner canvas only. */
  function applyViewTransform() {
    Geo()?.applyViewTransform(canvasInnerEl, state.view);
    updateStatusBar();
  }

  function fitToView() {
    dismissCanvasTip();
    const vp = viewportSize();
    const { width, height } = floorPlanSize();
    const blankW = sheetEl?.offsetWidth || 480;
    const blankH = sheetEl?.offsetHeight || 360;
    state.view = Geo()?.fitViewToImage(
      vp.width,
      vp.height,
      hasFloorPlan() ? width : blankW,
      hasFloorPlan() ? height : blankH,
    ) || state.view;
    applyViewTransform();
    updateCanvasCursor();
  }

  function centerSheetInView() {
    const vp = viewportSize();
    const { width, height } = floorPlanSize();
    const blankW = sheetEl?.offsetWidth || 480;
    const blankH = sheetEl?.offsetHeight || 360;
    state.view = Geo()?.centerViewOnImage(
      vp.width,
      vp.height,
      hasFloorPlan() ? width : blankW,
      hasFloorPlan() ? height : blankH,
      state.view,
    ) || state.view;
    applyViewTransform();
  }

  function zoomBy(factor, anchorClientX, anchorClientY) {
    dismissCanvasTip();
    const vpRect = viewportEl?.getBoundingClientRect();
    if (!vpRect) return;
    const anchorVx = anchorClientX != null ? anchorClientX - vpRect.left : vpRect.width / 2;
    const anchorVy = anchorClientY != null ? anchorClientY - vpRect.top : vpRect.height / 2;
    state.view = Geo()?.zoomViewAt(state.view, anchorVx, anchorVy, factor) || state.view;
    applyViewTransform();
  }

  /** Layer 2 — sheet sized 1:1 with PNG pixels. */
  function updateImageLayer() {
    if (!sheetEl || !imageLayerEl) return;
    const has = hasFloorPlan();
    const { width, height } = floorPlanSize();

    sheetEl.classList.toggle("mapEditorSheet--hasImage", has);
    sheetEl.classList.toggle("mapEditorSheet--blank", !has);

    if (has && width && height) {
      sheetEl.style.width = `${width}px`;
      sheetEl.style.height = `${height}px`;
      imageLayerEl.style.width = `${width}px`;
      imageLayerEl.style.height = `${height}px`;
    } else {
      sheetEl.style.width = "";
      sheetEl.style.height = "";
      sheetEl.style.minWidth = "480px";
      sheetEl.style.minHeight = "360px";
      imageLayerEl.style.width = "";
      imageLayerEl.style.height = "";
    }

    if (gridEl && has) {
      const mapForLayer = mapMetaForOriginDisplay() || state.map;
      const steps = Geo()?.gridSteps(mapForLayer) || { minor: 20, major: 100 };
      gridEl.style.setProperty("--map-grid-minor", `${steps.minor}px`);
      gridEl.style.setProperty("--map-grid-major", `${steps.major}px`);
      gridEl.hidden = false;
    } else if (gridEl) {
      gridEl.hidden = true;
    }

    updateOriginMarker();
    renderObjects();
    updateObjectToolsUi();
    updateStatusBar();
  }

  /** Origin fields → world (0,0) on the floor plan (ROS map_server). */
  function mapMetaForOriginDisplay() {
    if (!state.map) return null;
    const base = { ...state.map };
    if (uploadMetaDialogEl?.open) {
      const m = readUploadMetaPayload();
      return { ...base, ...m };
    }
    if (settingsDialogEl?.open || state.dirty) {
      const s = readSettingsPayload();
      return { ...base, ...s };
    }
    return base;
  }

  function setShowOrigin(show) {
    state.showOrigin = !!show;
    const btn = el("mapEditorOriginBtn");
    btn?.classList.toggle("is-active", state.showOrigin);
    btn?.setAttribute("aria-pressed", state.showOrigin ? "true" : "false");
    updateOriginMarker();
  }

  function setOriginLabelVisible(visible) {
    if (!originEl) return;
    originEl.classList.toggle("mapEditorOrigin--showLabel", !!visible);
    if (originLabelEl) originLabelEl.setAttribute("aria-hidden", visible ? "false" : "true");
  }

  function originPixelOnSheet() {
    const geo = Geo();
    const { width, height } = floorPlanSize();
    if (!state.showOrigin || !geo || !hasFloorPlan() || !width || !height) return null;
    return geo.worldToPixel(mapMetaForOriginDisplay(), width, height, 0, 0);
  }

  /** Show origin label when pointer is near world (0,0) — without blocking map tools. */
  function updateOriginLabelFromPointer(evt) {
    if (!state.showOrigin || originEl?.hidden) {
      setOriginLabelVisible(false);
      return;
    }
    const originPt = originPixelOnSheet();
    const imgPt = imagePointFromEvent(evt);
    if (!originPt || !imgPt) {
      setOriginLabelVisible(false);
      return;
    }
    const dist = Math.hypot(imgPt.x - originPt.x, imgPt.y - originPt.y);
    setOriginLabelVisible(dist <= 14);
  }

  function updateOriginMarker() {
    if (!originEl) return;
    const geo = Geo();
    const { width, height } = floorPlanSize();
    if (!state.showOrigin || !geo || !hasFloorPlan() || !width || !height) {
      originEl.hidden = true;
      originEl.setAttribute("aria-hidden", "true");
      return;
    }

    const mapMeta = mapMetaForOriginDisplay();
    const pt = geo.worldToPixel(mapMeta, width, height, 0, 0);
    const ox = Number(mapMeta?.origin_x) || 0;
    const oy = Number(mapMeta?.origin_y) || 0;
    const oyaw = Number(mapMeta?.origin_yaw) || 0;
    const yawDeg = (-oyaw * 180) / Math.PI;
    const onMap = pt.x >= -2 && pt.y >= -2 && pt.x <= width + 2 && pt.y <= height + 2;

    originEl.hidden = false;
    originEl.setAttribute("aria-hidden", "false");
    originEl.classList.toggle("mapEditorOrigin--offMap", !onMap);
    originEl.style.left = `${pt.x}px`;
    originEl.style.top = `${pt.y}px`;
    originEl.style.transform = `rotate(${yawDeg}deg)`;

    if (originLabelEl) {
      originLabelEl.textContent = t("maps.editor.originLabelShort", {
        x: ox.toFixed(2),
        y: oy.toFixed(2),
      });
      originLabelEl.setAttribute("aria-hidden", "true");
    }
    const tooltip = t("maps.editor.originTooltip", {
      x: ox.toFixed(3),
      y: oy.toFixed(3),
      yaw: ((oyaw * 180) / Math.PI).toFixed(1),
    });
    if (originHitEl) {
      originHitEl.title = tooltip;
      originHitEl.setAttribute("aria-label", tooltip);
    }
    setOriginLabelVisible(false);
  }

  function updateStatusBar(pointerClient) {
    const geo = Geo();
    const { width, height } = floorPlanSize();
    const pct = Math.round((state.view.scale || 1) * 100);

    if (statusViewEl) {
      statusViewEl.textContent = t("maps.editor.statusView", {
        zoom: pct,
        panX: Math.round(state.view.panX),
        panY: Math.round(state.view.panY),
      });
    }

    if (!geo || !hasFloorPlan() || !pointerClient) {
      if (statusImageEl) statusImageEl.textContent = t("maps.editor.statusImageIdle");
      if (statusWorldEl) statusWorldEl.textContent = t("maps.editor.statusWorldIdle");
      return;
    }

    const sheetRect = sheetEl?.getBoundingClientRect();
    const imgPt = geo.clientToImage(pointerClient.x, pointerClient.y, sheetRect, width, height);
    if (!imgPt) {
      if (statusImageEl) statusImageEl.textContent = t("maps.editor.statusImageIdle");
      if (statusWorldEl) statusWorldEl.textContent = t("maps.editor.statusWorldIdle");
      return;
    }

    const world = geo.pixelToWorld(state.map, width, height, imgPt.x, imgPt.y);
    if (statusImageEl) {
      statusImageEl.textContent = t("maps.editor.statusImage", {
        px: Math.round(imgPt.x),
        py: Math.round(imgPt.y),
      });
    }
    if (statusWorldEl) {
      statusWorldEl.textContent = t("maps.editor.statusWorld", {
        x: world.x.toFixed(2),
        y: world.y.toFixed(2),
      });
    }
  }

  function renderMapImage(opts = {}) {
    const shouldFit = opts.fitToView !== false;
    const url = mapBaseImageUrl(state.map);
    if (url && imageEl) {
      imageEl.src = url;
      imageEl.hidden = false;
      if (emptyEl) emptyEl.hidden = true;
      setOccupancyCanvasVisible(false);
    } else {
      if (imageEl) {
        imageEl.hidden = true;
        imageEl.removeAttribute("src");
      }
      setOccupancyCanvasVisible(false);
      if (emptyEl) emptyEl.hidden = false;
    }
    updateMenuActionsUi();
    updateImageLayer();
    imageEl?.addEventListener(
      "load",
      () => {
        paintOccupancyFromImage();
        updateImageLayer();
        if (shouldFit) fitToView();
      },
      { once: true },
    );
    if (url && imageEl?.complete && imageEl.naturalWidth) {
      paintOccupancyFromImage();
      updateImageLayer();
    }
    if (!url) {
      centerSheetInView();
      applyViewTransform();
    }
  }

  function fillSettingsForm() {
    const map = state.map;
    if (!map) return;
    if (settingsFields.name) settingsFields.name.value = map.name || "";
    if (settingsFields.desc) settingsFields.desc.value = map.description || "";
    if (settingsFields.resolution) settingsFields.resolution.value = map.resolution != null ? map.resolution : 0.05;
    if (settingsFields.originX) settingsFields.originX.value = map.origin_x != null ? map.origin_x : 0;
    if (settingsFields.originY) settingsFields.originY.value = map.origin_y != null ? map.origin_y : 0;
    if (settingsFields.originYaw) settingsFields.originYaw.value = map.origin_yaw != null ? map.origin_yaw : 0;
  }

  function readSettingsPayload() {
    return {
      name: settingsFields.name?.value.trim() || "",
      description: settingsFields.desc?.value.trim() || "",
      resolution: Number(settingsFields.resolution?.value) || 0.05,
      origin_x: Number(settingsFields.originX?.value) || 0,
      origin_y: Number(settingsFields.originY?.value) || 0,
      origin_yaw: Number(settingsFields.originYaw?.value) || 0,
    };
  }

  function updateHeader() {
    if (titleEl) titleEl.textContent = state.map?.name || "—";
  }

  function updateMenuActionsUi() {
    const hasImage = !!(state.map?.image_file);
    const ro = state.readOnly;
    el("mapMenuUploadOverwrite")?.toggleAttribute("disabled", ro);
    el("mapMenuUploadAppend")?.toggleAttribute("disabled", true);
    el("mapMenuDownload")?.toggleAttribute("disabled", !hasImage);
    el("mapMenuRecordOverwrite")?.toggleAttribute("disabled", true);
    el("mapMenuRecordAppend")?.toggleAttribute("disabled", true);
  }

  function applyReadOnlyUi() {
    const ro = state.readOnly;
    el("mapEditorMenuBtn")?.toggleAttribute("disabled", ro);
    el("mapEditorSaveBtn")?.toggleAttribute("disabled", ro || !state.dirty);
    el("mapEditorSettingsBtn")?.toggleAttribute("disabled", ro);
    updateMenuActionsUi();
    updateObjectToolsUi();
    updateUndoUi();
  }

  async function reloadMap() {
    if (!state.mapId) return;
    state.map = await api(`/api/maps/${encodeURIComponent(state.mapId)}`);
    await loadYamlMeta();
    syncZonesFromMap();
    state.initialPreferredFp = preferredZonesFingerprint(state.zones);
    updateHeader();
    renderMapImage();
    fillSettingsForm();
  }

  function open(mapId, callbacks = {}) {
    state.mapId = mapId;
    state.callbacks = callbacks;
    state.readOnly = !!callbacks.readOnly || !callbacks.canWrite;
    state.dirty = false;
    state.tipVisible = true;
    state.showOrigin = true;
    state.activeTool = "pan";
    state.objectType = "";
    state.zones = [];
    state.draft = null;
    state.selectedZoneId = null;
    state.undoStack = [];
    state.rasterDirty = false;
    state.baseDirty = false;
    state.bakedZoneIds = new Set();
    state.eraserStroke = null;
    state.vertexDrag = null;
    state.selectionDrag = null;
    state.selectionRect = null;
    state.positionDrag = null;
    state.pendingPosition = null;
    state.pathPreview = callbacks.pathPreview || null;
    state.initialPreferredFp = "";
    updateObjectTypePickerUi();
    state.view = Geo()?.createView(1, 0, 0) || { scale: 1, panX: 0, panY: 0 };
    if (tipEl) {
      tipEl.hidden = false;
      tipEl.textContent = t("maps.editor.canvasTip");
    }
    setActiveTool("pan");
    setShowOrigin(true);
    setDirty(false);
    applyReadOnlyUi();
    reloadMap().catch((e) => alert(e.message));
  }

  function close() {
    state.mapId = null;
    state.map = null;
    state.callbacks = {};
    state.uploadMeta = null;
    state.yamlMeta = null;
    state.zones = [];
    state.draft = null;
    state.selectedZoneId = null;
    state.undoStack = [];
    state.objectType = "";
    state.rasterDirty = false;
    state.baseDirty = false;
    state.bakedZoneIds = new Set();
    state.eraserStroke = null;
    state.vertexDrag = null;
    state.selectionDrag = null;
    state.selectionRect = null;
    state.positionDrag = null;
    state.pendingPosition = null;
    state.pathPreview = null;
    state.initialPreferredFp = "";
    drawLineDialogEl?.close();
    menuDialogEl?.close();
    settingsDialogEl?.close();
    positionDialogEl?.close();
    activateDialogEl?.close();
    uploadConfirmDialogEl?.close();
    uploadMetaDialogEl?.close();
  }

  function loadImageDimensions(file) {
    return new Promise((resolve, reject) => {
      const img = new Image();
      const url = URL.createObjectURL(file);
      img.onload = () => {
        URL.revokeObjectURL(url);
        resolve({ width: img.naturalWidth, height: img.naturalHeight });
      };
      img.onerror = () => {
        URL.revokeObjectURL(url);
        reject(new Error("invalid image"));
      };
      img.src = url;
    });
  }

  function readUploadMetaPayload() {
    return {
      resolution: Number(uploadMetaFields.resolution?.value) || 0.05,
      origin_x: Number(uploadMetaFields.originX?.value) || 0,
      origin_y: Number(uploadMetaFields.originY?.value) || 0,
      origin_yaw: Number(uploadMetaFields.originYaw?.value) || 0,
      negate: Number(uploadMetaFields.negate?.value) || 0,
      occupied_thresh: Number(uploadMetaFields.occupiedThresh?.value) || 0.65,
      free_thresh: Number(uploadMetaFields.freeThresh?.value) || 0.196,
    };
  }

  function fillUploadMetaForm(meta) {
    const m = meta || {};
    if (uploadMetaFields.resolution) {
      uploadMetaFields.resolution.value = m.resolution != null ? m.resolution : 0.05;
    }
    if (uploadMetaFields.originX) uploadMetaFields.originX.value = m.origin_x != null ? m.origin_x : 0;
    if (uploadMetaFields.originY) uploadMetaFields.originY.value = m.origin_y != null ? m.origin_y : 0;
    if (uploadMetaFields.originYaw) uploadMetaFields.originYaw.value = m.origin_yaw != null ? m.origin_yaw : 0;
    if (uploadMetaFields.negate) uploadMetaFields.negate.value = m.negate != null ? m.negate : 0;
    if (uploadMetaFields.occupiedThresh) {
      uploadMetaFields.occupiedThresh.value = m.occupied_thresh != null ? m.occupied_thresh : 0.65;
    }
    if (uploadMetaFields.freeThresh) {
      uploadMetaFields.freeThresh.value = m.free_thresh != null ? m.free_thresh : 0.196;
    }
  }

  async function fetchExistingYamlMeta() {
    if (!state.map?.yaml_file) return null;
    try {
      const res = await fetch(`/api/maps/${encodeURIComponent(state.map.id)}/yaml`, {
        credentials: "include",
      });
      if (!res.ok) return null;
      const text = await res.text();
      const parsed = window.MapYaml?.parse(text);
      if (!parsed || parsed.error) return null;
      return parsed;
    } catch {
      return null;
    }
  }

  async function openUploadMetaDialog() {
    const map = state.map;
    const defaults = {
      resolution: map?.resolution != null ? map.resolution : 0.05,
      origin_x: map?.origin_x != null ? map.origin_x : 0,
      origin_y: map?.origin_y != null ? map.origin_y : 0,
      origin_yaw: map?.origin_yaw != null ? map.origin_yaw : 0,
      negate: 0,
      occupied_thresh: 0.65,
      free_thresh: 0.196,
    };
    const fromYaml = await fetchExistingYamlMeta();
    fillUploadMetaForm(fromYaml || defaults);
    menuDialogEl?.close();
    uploadMetaDialogEl?.showModal();
    updateOriginMarker();
  }

  function beginUploadOverwrite() {
    if (!state.map || state.readOnly) return;
    if (state.map.image_file) {
      const textEl = el("mapUploadConfirmText");
      if (textEl) textEl.textContent = t("maps.uploadConfirm.text");
      menuDialogEl?.close();
      uploadConfirmDialogEl?.showModal();
      return;
    }
    openUploadMetaDialog().catch((e) => alert(e.message));
  }

  function applyYamlToUploadForm(text) {
    const parsed = window.MapYaml?.parse(text);
    if (!parsed || parsed.error) {
      alert(t("maps.uploadMeta.invalidYaml"));
      return;
    }
    fillUploadMetaForm(parsed);
  }

  async function saveYamlForMap(imageFilename) {
    const meta = state.uploadMeta || readUploadMetaPayload();
    const yamlText = window.MapYaml?.serialize({
      ...meta,
      image: imageFilename || "map.png",
    });
    if (!yamlText) return;
    await api(`/api/maps/${encodeURIComponent(state.map.id)}/yaml`, {
      method: "POST",
      headers: { "Content-Type": "text/yaml; charset=utf-8" },
      body: yamlText,
    });
  }

  async function uploadImage(file) {
    if (!state.map || !file || state.readOnly) return;
    if (!/\.png$/i.test(file.name)) {
      alert(t("maps.error.pngOnly"));
      return;
    }
    const meta = state.uploadMeta || readUploadMetaPayload();
    if (!meta.resolution || meta.resolution <= 0) {
      alert(t("maps.uploadMeta.invalidResolution"));
      return;
    }
    const dims = await loadImageDimensions(file);
    const pngName = file.name.endsWith(".png") ? file.name : `${file.name}.png`;
    const form = new FormData();
    form.append("file", file, pngName);
    const res = await fetch(`/api/maps/${encodeURIComponent(state.map.id)}/image`, {
      credentials: "include",
      method: "POST",
      body: form,
    });
    if (!res.ok) {
      let msg = res.statusText;
      try {
        const err = await res.json();
        if (err.error) msg = err.error;
      } catch {
        /* ignore */
      }
      throw new Error(msg);
    }
    let updated = await res.json();
    updated = await api(`/api/maps/${encodeURIComponent(state.map.id)}`, {
      method: "PUT",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        resolution: meta.resolution,
        origin_x: meta.origin_x,
        origin_y: meta.origin_y,
        origin_yaw: meta.origin_yaw,
        width: dims.width,
        height: dims.height,
      }),
    });
    try {
      await saveYamlForMap(pngName);
      updated = (await api(`/api/maps/${encodeURIComponent(state.map.id)}`)) || updated;
    } catch {
      /* yaml save is best-effort */
    }
    state.uploadMeta = null;
    state.map = updated;
    state.callbacks.onMapUpdated?.(updated);
    setDirty(false);
    fillSettingsForm();
    await loadYamlMeta();
    renderMapImage();
    menuDialogEl?.close();
    uploadMetaDialogEl?.close();
    promptActivate();
  }

  async function uploadMapImageBlob(blob, kind) {
    if (!state.map || !blob) return null;
    const pngName = state.map.image_file || "map.png";
    const filename = /\.png$/i.test(pngName) ? pngName : `${pngName}.png`;
    const form = new FormData();
    form.append("file", blob, filename);
    const path =
      kind === "base"
        ? `/api/maps/${encodeURIComponent(state.map.id)}/image/base`
        : `/api/maps/${encodeURIComponent(state.map.id)}/image/composite`;
    const res = await fetch(path, {
      credentials: "include",
      method: "POST",
      body: form,
    });
    if (!res.ok) {
      let msg = res.statusText;
      try {
        const err = await res.json();
        if (err.error) msg = err.error;
      } catch {
        /* ignore */
      }
      throw new Error(msg);
    }
    return res.json();
  }

  async function saveMap() {
    if (!state.map || state.readOnly) return;
    const payload = readSettingsPayload();
    if (!payload.name) {
      alert(t("maps.error.nameEmpty"));
      return;
    }
    rebakeComposite();
    const edit = OccEdit();
    if (state.baseDirty && edit) {
      const baseBlob = await edit.exportPngBlob(getBaseCanvas());
      const baseUpdated = await uploadMapImageBlob(baseBlob, "base");
      if (baseUpdated) state.map = { ...state.map, ...baseUpdated };
      state.baseDirty = false;
    }
    if (state.rasterDirty && edit) {
      const compositeBlob = await edit.exportPngBlob(getSourceCanvas());
      const imageUpdated = await uploadMapImageBlob(compositeBlob, "composite");
      if (imageUpdated) state.map = { ...state.map, ...imageUpdated };
      state.rasterDirty = false;
    }
    const preferredChanged =
      preferredZonesFingerprint(state.zones) !== state.initialPreferredFp;
    if (preferredChanged && state.map?.id) {
      try {
        const pathsData = await api(
          `/api/paths?site_id=${encodeURIComponent(state.map.site_id || "")}`,
        );
        const onMap = (pathsData.paths || []).filter((p) => p.map_id === state.map.id);
        if (onMap.length && !confirm(t("maps.editor.preferredPathWarning"))) return;
      } catch {
        /* continue save */
      }
    }
    const updated = await api(`/api/maps/${encodeURIComponent(state.map.id)}`, {
      method: "PUT",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ ...payload, zones: state.zones }),
    });
    state.map = updated;
    syncZonesFromMap();
    state.initialPreferredFp = preferredZonesFingerprint(state.zones);
    state.callbacks.onMapUpdated?.(updated);
    setDirty(false);
    updateHeader();
    renderMapImage({ fitToView: false });
    menuDialogEl?.close();
    promptActivate();
  }

  function promptActivate() {
    if (!state.map?.image_file) return;
    if (state.callbacks.getActiveMapId?.() === state.map.id) return;
    const textEl = el("mapActivateDialogText");
    if (textEl) {
      textEl.textContent = t("maps.activateDialog.text", { name: state.map.name });
    }
    activateDialogEl?.showModal();
  }

  async function activateCurrentMap() {
    if (!state.map) return;
    await api("/api/robot/active_map", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ map_id: state.map.id }),
    });
    state.callbacks.onActivated?.(state.map.id);
    activateDialogEl?.close();
  }

  function bindCanvasPanZoom() {
    const blockNativeDrag = (evt) => {
      evt.preventDefault();
    };

    imageEl?.addEventListener("dragstart", blockNativeDrag);
    sheetEl?.addEventListener("dragstart", blockNativeDrag);
    imageLayerEl?.addEventListener("dragstart", blockNativeDrag);

    viewportEl?.addEventListener("wheel", (evt) => {
      evt.preventDefault();
      dismissCanvasTip();
      const factor = evt.deltaY < 0 ? 1.1 : 0.9;
      zoomBy(factor, evt.clientX, evt.clientY);
    }, { passive: false });

    viewportEl?.addEventListener("mousemove", (evt) => {
      updateStatusBar({ x: evt.clientX, y: evt.clientY });
      updateOriginLabelFromPointer(evt);
      if (state.draft?.kind === "shape" && isDrawTool(state.activeTool)) {
        const pt = imagePointFromEvent(evt);
        const obj = Objects();
        if (pt && obj) {
          let hover = { x: pt.x, y: pt.y };
          const pts = state.draft.points;
          if (evt.shiftKey && pts.length) {
            hover = obj.constrainAxis(pts[pts.length - 1], hover);
          }
          state.draft.hover = hover;
          renderObjects();
        }
      }
    });

    viewportEl?.addEventListener("click", (evt) => {
      if (state.activeTool !== "pan") onCanvasClick(evt);
    });

    viewportEl?.addEventListener("dblclick", (evt) => {
      onCanvasDblClick(evt);
    });

    viewportEl?.addEventListener("mouseleave", () => {
      updateStatusBar();
      setOriginLabelVisible(false);
    });

    viewportEl?.addEventListener("mousedown", (evt) => {
      if (evt.button !== 0) return;
      state.pointerMoved = false;
      if (beginEraserStroke(evt)) return;
      if (beginVertexDrag(evt)) return;
      if (beginSelectionDrag(evt)) return;
      if (beginPositionDrag(evt)) return;
      if (state.activeTool !== "pan") return;
      evt.preventDefault();
      dismissCanvasTip();
      state.panning = {
        startX: evt.clientX,
        startY: evt.clientY,
        startPanX: state.view.panX,
        startPanY: state.view.panY,
      };
      updateCanvasCursor();
    });

    window.addEventListener("mousemove", (evt) => {
      if (state.eraserStroke) {
        continueEraserStroke(evt);
        return;
      }
      if (state.vertexDrag) {
        continueVertexDrag(evt);
        return;
      }
      if (state.selectionDrag) {
        continueSelectionDrag(evt);
        return;
      }
      if (state.positionDrag) {
        continuePositionDrag(evt);
        return;
      }
      if (!state.panning) return;
      state.pointerMoved = true;
      state.view.panX = state.panning.startPanX + (evt.clientX - state.panning.startX);
      state.view.panY = state.panning.startPanY + (evt.clientY - state.panning.startY);
      applyViewTransform();
    });

    window.addEventListener("mouseup", () => {
      endEraserStroke();
      endVertexDrag();
      endSelectionDrag();
      endPositionDrag();
      if (!state.panning) return;
      state.panning = null;
      updateCanvasCursor();
    });

    window.addEventListener("resize", () => {
      if (!state.mapId) return;
      applyViewTransform();
      positionObjectTypeMenu();
    });
  }

  function bindEvents() {
    originHitEl?.addEventListener("focus", () => setOriginLabelVisible(true));
    originHitEl?.addEventListener("blur", () => setOriginLabelVisible(false));

    el("mapEditorBackBtn")?.addEventListener("click", () => {
      if (state.dirty && !confirm(t("maps.editor.unsavedLeave"))) return;
      state.callbacks.onClose?.();
      close();
    });

    el("mapEditorHelpBtn")?.addEventListener("click", () => alert(t("maps.editor.helpText")));
    el("mapEditorMenuBtn")?.addEventListener("click", () => {
      updateMenuActionsUi();
      menuDialogEl?.showModal();
    });
    el("mapMenuCancelBtn")?.addEventListener("click", () => menuDialogEl?.close());
    menuDialogEl?.addEventListener("cancel", (evt) => {
      evt.preventDefault();
      menuDialogEl?.close();
    });
    el("mapEditorSettingsBtn")?.addEventListener("click", () => {
      fillSettingsForm();
      settingsDialogEl?.showModal();
      updateOriginMarker();
    });
    el("mapEditorSaveBtn")?.addEventListener("click", () => {
      saveMap().catch((e) => alert(e.message));
    });
    el("mapEditorPanBtn")?.addEventListener("click", () => setActiveTool("pan"));
    el("mapEditorDrawLineBtn")?.addEventListener("click", () => beginDrawLine());
    el("mapEditorDrawShapeBtn")?.addEventListener("click", () => setActiveTool("drawShape"));
    el("mapEditorSelectBtn")?.addEventListener("click", () => setActiveTool("select"));
    el("mapEditorEraserBtn")?.addEventListener("click", () => setActiveTool("eraser"));
    el("mapEditorEraseShapeBtn")?.addEventListener("click", () => setActiveTool("eraseShape"));
    el("mapEditorEraseSelectionBtn")?.addEventListener("click", () => setActiveTool("eraseSelection"));
    el("mapEditorConfirmDrawBtn")?.addEventListener("click", () => {
      if (!commitDraft()) {
        alert(t("maps.editor.drawNeedMorePoints"));
      }
    });
    el("mapEditorUndoBtn")?.addEventListener("click", () => undoZones());
    objectTypeBtnEl?.addEventListener("click", (evt) => {
      evt.preventDefault();
      evt.stopPropagation();
      toggleObjectTypeMenu();
    });
    objectTypeMenuEl?.addEventListener("click", (evt) => {
      evt.stopPropagation();
      const option = evt.target?.closest?.(".mapEditorObjectTypeOption");
      if (!option) return;
      setObjectType(option.dataset.value ?? "");
    });
    document.addEventListener("click", (evt) => {
      if (evt.target?.closest?.("#mapEditorObjectTypePicker")) return;
      if (evt.target?.closest?.("#mapEditorObjectTypeMenu")) return;
      closeObjectTypeMenu();
    });
    el("mapEditorMappingBar")?.addEventListener("scroll", () => positionObjectTypeMenu(), { passive: true });
    el("mapPositionCancelBtn")?.addEventListener("click", () => {
      state.pendingPosition = null;
      positionDialogEl?.close();
    });
    positionDialogEl?.addEventListener("cancel", (evt) => {
      evt.preventDefault();
      state.pendingPosition = null;
      positionDialogEl?.close();
    });
    positionFormEl?.addEventListener("submit", (evt) => {
      evt.preventDefault();
      if (!commitPositionFromDialog()) return;
    });
    el("mapSpeedCancelBtn")?.addEventListener("click", () => cancelBehaviorDialog());
    speedDialogEl?.addEventListener("cancel", (evt) => {
      evt.preventDefault();
      cancelBehaviorDialog();
    });
    speedFormEl?.addEventListener("submit", (evt) => {
      evt.preventDefault();
      commitSpeedFromDialog();
    });
    el("mapSoundCancelBtn")?.addEventListener("click", () => cancelBehaviorDialog());
    soundDialogEl?.addEventListener("cancel", (evt) => {
      evt.preventDefault();
      cancelBehaviorDialog();
    });
    soundFormEl?.addEventListener("submit", (evt) => {
      evt.preventDefault();
      commitSoundFromDialog();
    });
    soundFields.manageLink?.addEventListener("click", (evt) => {
      evt.preventDefault();
      soundDialogEl?.close();
      state.pendingBehavior = null;
      window.LmApp?.setActivePage?.("sounds");
    });
    el("mapDrawLineCancelBtn")?.addEventListener("click", () => cancelDrawLineDialog());
    drawLineDialogEl?.addEventListener("cancel", (evt) => {
      evt.preventDefault();
      cancelDrawLineDialog();
    });
    drawLineFormEl?.addEventListener("submit", (evt) => {
      evt.preventDefault();
      commitDrawLineDialog();
    });
    el("mapDirectionalCancelBtn")?.addEventListener("click", () => cancelAdvancedDialog());
    directionalDialogEl?.addEventListener("cancel", (evt) => {
      evt.preventDefault();
      cancelAdvancedDialog();
    });
    directionalFormEl?.addEventListener("submit", (evt) => {
      evt.preventDefault();
      commitDirectionalFromDialog();
    });
    el("mapPlannerCancelBtn")?.addEventListener("click", () => cancelAdvancedDialog());
    plannerDialogEl?.addEventListener("cancel", (evt) => {
      evt.preventDefault();
      cancelAdvancedDialog();
    });
    plannerFormEl?.addEventListener("submit", (evt) => {
      evt.preventDefault();
      commitPlannerFromDialog();
    });
    el("mapIoCancelBtn")?.addEventListener("click", () => cancelAdvancedDialog());
    ioDialogEl?.addEventListener("cancel", (evt) => {
      evt.preventDefault();
      cancelAdvancedDialog();
    });
    ioFormEl?.addEventListener("submit", (evt) => {
      evt.preventDefault();
      commitIoFromDialog();
    });
    el("mapEditorOriginBtn")?.addEventListener("click", () => setShowOrigin(!state.showOrigin));
    el("mapEditorFitBtn")?.addEventListener("click", fitToView);
    el("mapEditorCenterBtn")?.addEventListener("click", () => {
      dismissCanvasTip();
      centerSheetInView();
    });
    el("mapEditorZoomInBtn")?.addEventListener("click", () => {
      const rect = viewportEl?.getBoundingClientRect();
      zoomBy(1.2, rect ? rect.left + rect.width / 2 : undefined, rect ? rect.top + rect.height / 2 : undefined);
    });
    el("mapEditorZoomOutBtn")?.addEventListener("click", () => {
      const rect = viewportEl?.getBoundingClientRect();
      zoomBy(1 / 1.2, rect ? rect.left + rect.width / 2 : undefined, rect ? rect.top + rect.height / 2 : undefined);
    });

    el("mapMenuUploadOverwrite")?.addEventListener("click", () => beginUploadOverwrite());
    el("mapMenuDownload")?.addEventListener("click", () => {
      const url = mapImageUrl(state.map);
      if (!url) return;
      const a = document.createElement("a");
      a.href = url;
      a.download = `${state.map.name || "map"}.png`;
      a.click();
      menuDialogEl?.close();
    });

    uploadInputEl?.addEventListener("change", () => {
      const file = uploadInputEl.files?.[0];
      uploadInputEl.value = "";
      if (!file) return;
      uploadImage(file).catch((e) => alert(e.message));
    });

    el("mapUploadConfirmYesBtn")?.addEventListener("click", () => {
      uploadConfirmDialogEl?.close();
      openUploadMetaDialog().catch((e) => alert(e.message));
    });
    el("mapUploadConfirmNoBtn")?.addEventListener("click", () => uploadConfirmDialogEl?.close());
    uploadConfirmDialogEl?.addEventListener("cancel", (evt) => {
      evt.preventDefault();
      uploadConfirmDialogEl?.close();
    });

    el("mapUploadImportYamlBtn")?.addEventListener("click", () => uploadYamlInputEl?.click());
    uploadYamlInputEl?.addEventListener("change", () => {
      const file = uploadYamlInputEl.files?.[0];
      uploadYamlInputEl.value = "";
      if (!file) return;
      const reader = new FileReader();
      reader.onload = () => {
        applyYamlToUploadForm(String(reader.result || ""));
        updateOriginMarker();
      };
      reader.onerror = () => alert(t("maps.uploadMeta.invalidYaml"));
      reader.readAsText(file);
    });

    el("mapUploadMetaCancelBtn")?.addEventListener("click", () => {
      state.uploadMeta = null;
      uploadMetaDialogEl?.close();
      updateOriginMarker();
    });
    uploadMetaDialogEl?.addEventListener("cancel", (evt) => {
      evt.preventDefault();
      state.uploadMeta = null;
      uploadMetaDialogEl?.close();
      updateOriginMarker();
    });
    el("mapUploadMetaForm")?.addEventListener("submit", (evt) => {
      evt.preventDefault();
      const meta = readUploadMetaPayload();
      if (!meta.resolution || meta.resolution <= 0) {
        alert(t("maps.uploadMeta.invalidResolution"));
        return;
      }
      state.uploadMeta = meta;
      uploadMetaDialogEl?.close();
      uploadInputEl?.click();
    });

    el("mapEditorSettingsForm")?.addEventListener("submit", (evt) => {
      evt.preventDefault();
      if (!state.map) return;
      Object.assign(state.map, readSettingsPayload());
      setDirty(true);
      updateImageLayer();
      updateHeader();
      settingsDialogEl?.close();
    });

    el("mapActivateYesBtn")?.addEventListener("click", () => {
      activateCurrentMap().catch((e) => alert(e.message));
    });
    el("mapActivateNoBtn")?.addEventListener("click", () => activateDialogEl?.close());

    Object.values(settingsFields).forEach((node) => {
      node?.addEventListener("input", () => {
        setDirty(true);
        updateOriginMarker();
      });
    });

    Object.values(uploadMetaFields).forEach((node) => {
      node?.addEventListener("input", () => {
        updateOriginMarker();
        if (uploadMetaDialogEl?.open && imageEl?.naturalWidth) {
          paintOccupancyFromImage();
        }
        if (node === uploadMetaFields.resolution && uploadMetaDialogEl?.open) {
          updateImageLayer();
        }
      });
    });

    window.addEventListener("keydown", (evt) => {
      if (!state.mapId || state.readOnly) return;
      if (evt.key === "Enter" && state.draft?.kind === "shape") {
        if (commitDraft()) evt.preventDefault();
        else alert(t("maps.editor.drawNeedMorePoints"));
      }
      if (evt.key === "Escape" && state.draft) {
        cancelDraft();
        evt.preventDefault();
      }
    });

    window.addEventListener("lm:locale-change", () => {
      if (state.tipVisible && tipEl) tipEl.textContent = t("maps.editor.canvasTip");
      updateHeader();
      updateStatusBar();
      updateOriginMarker();
      updateObjectTypePickerUi();
    });
  }

  bindCanvasPanZoom();
  bindEvents();

  window.MapEditorApp = {
    open,
    close,
    reloadMap,
    paintOccupancyGrid,
    paintOccupancyFromImage,
    getZones: () => JSON.parse(JSON.stringify(state.zones)),
    getPlannerZones: () => Planner()?.filterPlannerZones(state.zones) || [],
    getBehaviorZones: () => Behavior()?.filterBehaviorZones(state.zones) || [],
    getAdvancedZones: () => Advanced()?.filterAdvancedZones(state.zones) || [],
    classifyImagePoint: (px, py) => Planner()?.classifyPoint(state.zones, px, py) || null,
    classifyBehavior: (px, py) => Behavior()?.classifyPoint(state.zones, px, py) || null,
    classifyAdvanced: (px, py, headingRad) =>
      Advanced()?.classifyPoint(state.zones, px, py, headingRad) || null,
    getSpeedLimit: (px, py) => Behavior()?.getSpeedLimit(state.zones, px, py) ?? null,
    getSoundAtPoint: (px, py) => Behavior()?.getSoundAtPoint(state.zones, px, py) ?? null,
    getDirectionalConstraint: (px, py, headingRad) =>
      Advanced()?.getDirectionalConstraint(state.zones, px, py, headingRad) ?? null,
    getPlannerSettings: (px, py) => Advanced()?.getPlannerSettings(state.zones, px, py) ?? null,
    getIoActivation: (px, py) => Advanced()?.getIoActivation(state.zones, px, py) ?? null,
    pathCost: (points) => Planner()?.pathCost(state.zones, points) ?? Infinity,
    isPathBlocked: (points) => Planner()?.isPathBlocked(state.zones, points) ?? false,
  };
})();
