(function () {
  const BASE_COLOR_LAYER_ID = "__base_color__";
  const ORIGINAL_BASE_TEXTURE_LAYER_ID = "__original_base_texture__";
  const ORIGINAL_GAME_LIVERY_START_POINT_ID = "original_game_livery";
  const BASE_COLOR_DEFAULT = "#ffffff";
  const BODY_BASE_COLOR_DEFAULT = "#7a00ff";
  const SPLAT_MAP_BASE_COLOR_DEFAULT = "#000000";
  const SPLAT_MAP_PALETTE = Object.freeze([
    { id: "texture", label: "Authored maps", color: "#000000", description: "Uses the authored texture and PBR maps." },
    { id: "red", label: "Red channel", color: "#ff0000", description: "Uses the red JSON material channel." },
    { id: "green", label: "Green channel", color: "#00ff00", description: "Uses the green JSON material channel." },
    { id: "blue", label: "Carbon", color: "#0000ff", description: "Uses the carbon material branch." },
  ]);
  const DRAFT_OVERLAY_COLOR = "#ffd84d";
  const DRAFT_OVERLAY_DARK = "#111318";
  const DRAFT_CLOSE_COLOR = "#35d07f";
  const DRAFT_POINT_SCREEN_RADIUS = 4;
  const DRAFT_FIRST_POINT_SCREEN_RADIUS = 6;
  const POLYGON_CLOSE_SCREEN_RADIUS = 8;
  const STRIPE_DRAFT_BEZIER_THRESHOLD = 4;
  const STRIPE_BEZIER_HANDLE_COLOR = "#7ae7ff";
  const POLYGON_EDIT_POINT_HOVER_COLOR = "#c78f19";
  const BEZIER_HANDLE_HOVER_COLOR = "#18a7c2";
  const ROTATION_SNAP_ANGLES = [-180, -135, -90, -45, 0, 45, 90, 135, 180];
  const CANVAS_PASTEBOARD_RATIO = 0.12;

  const state = {
    context: null,
    project: null,
    layerDocument: null,
    activeSurfaceKey: "ext_skin_c",
    pendingSurfaceKey: "ext_skin_c",
    konva: null,
    baseImageElement: null,
    imageCache: new Map(),
    libraryThumbnailCache: new Map(),
    textureSize: { width: 4096, height: 4096 },
    view: { scale: 1, x: 0, y: 0 },
    activeTool: "select",
    polygonDraftPoints: [],
    stripeDraftBezierHandles: [],
    stripeDraftDrag: null,
    suppressDraftClick: false,
    polygonDraftSnap: false,
    polygonEditLayerId: "",
    polygonEditPointIndex: -1,
    polygonEditSelectedPointIndices: [],
    polygonEditPointDrag: null,
    polygonEditEdgeHover: null,
    polygonEditAddDrag: null,
    selectedGuideId: "",
    canvasPan: null,
    suppressCanvasClick: false,
    spacePanActive: false,
    selectedLayerId: "",
    selectedLayerIds: [],
    undoStack: [],
    redoStack: [],
    historyLimit: 30,
    historySnapshot: null,
    renamingLayerId: "",
    renameOriginalValue: "",
    draggedLayerId: "",
    dragOverLayerId: "",
    layerPointerDrag: null,
    libraryPointerDrag: null,
    draggedLibraryAssetId: "",
    dragOverLibraryGroupId: "",
    hoveredLayerId: "",
    multiDrag: null,
    moveDrag: null,
    groupTransform: null,
    pointTransform: null,
    assetLibrary: null,
    projectAssetLibrary: null,
    libraryActiveScope: "project",
    libraryCollapsed: true,
    librarySearchQuery: "",
    editorCollapsed: false,
    previewCollapsed: false,
    previewScene: "dark",
    previewFloorVisible: true,
    previewUvMeshVisible: false,
    previewMode: "finalLivery",
    previewCameraPreset: "free",
    previewMaterialFinish: null,
    editorPreviewSplit: null,
    editorPreviewSplitDrag: null,
    libraryLoading: false,
    uvOverlayVisible: true,
    uvOverlayInverted: false,
    partsUvOverlayVisible: true,
    partsUvOverlayInverted: false,
    uvOverlayRequestId: 0,
    suppressPropertyEvents: false,
    fontLoadPromise: null,
    dirty: false,
    changeRevision: 0,
    savedRevision: 0,
    autosaveTimer: null,
    autosavePending: false,
    autosaveDelayMs: 1200,
    updateFeedbackTimer: null,
    updateFeedbackHideTimer: null,
    materialFinishFeedbackTimer: null,
    materialFinishFeedbackHideTimer: null,
    saving: false,
    baking: false,
    textureSwitching: false,
    textureRequestId: 0,
    layerRenderRequestId: 0,
    opening: false,
    gradientPopoverOpen: false,
    splatMapColor: "#ff0000",
    bodyBaseColor: BODY_BASE_COLOR_DEFAULT,
    activeColor: "#e43d30",
    colorFavorites: [null, null, null, null, null],
    colorFavoritesLoaded: false,
    colorFavoritesOpen: false,
    colorFavoriteEditingSlot: -1,
    colorFavoritesSaving: false,
  };

  function $(id) {
    return document.getElementById(id);
  }

  function app() {
    return window.LiveryLabLayerStudioApp || {};
  }

  function escapeHtml(value) {
    return String(value ?? "").replace(/[&<>"']/g, (char) => ({
      "&": "&amp;",
      "<": "&lt;",
      ">": "&gt;",
      '"': "&quot;",
      "'": "&#39;",
    }[char]));
  }

  const LAYER_ACTION_ICONS = {
    visible: `<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M2.5 12s3.5-6 9.5-6 9.5 6 9.5 6-3.5 6-9.5 6-9.5-6-9.5-6Z"/><circle cx="12" cy="12" r="2.8"/></svg>`,
    hidden: `<svg viewBox="0 0 24 24" aria-hidden="true"><path d="m3 3 18 18"/><path d="M10.6 6.2A9.8 9.8 0 0 1 12 6c6 0 9.5 6 9.5 6a15 15 0 0 1-3.2 3.8"/><path d="M6.6 6.7A15 15 0 0 0 2.5 12s3.5 6 9.5 6c1.7 0 3.2-.5 4.4-1.2"/><path d="M10.2 10.2a2.8 2.8 0 0 0 3.6 3.6"/></svg>`,
    lock: `<svg viewBox="0 0 24 24" aria-hidden="true"><rect x="5" y="10" width="14" height="10" rx="1"/><path d="M8 10V7a4 4 0 0 1 8 0v3"/></svg>`,
    unlock: `<svg viewBox="0 0 24 24" aria-hidden="true"><rect x="5" y="10" width="14" height="10" rx="1"/><path d="M8 10V7a4 4 0 0 1 7.3-2.2"/></svg>`,
    up: `<svg viewBox="0 0 24 24" aria-hidden="true"><path d="m12 5-6 6"/><path d="m12 5 6 6"/><path d="M12 5v14"/></svg>`,
    down: `<svg viewBox="0 0 24 24" aria-hidden="true"><path d="m12 19-6-6"/><path d="m12 19 6-6"/><path d="M12 5v14"/></svg>`,
    edit: `<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 20h4"/><path d="M14.5 4.5 19.5 9.5"/><path d="M18 3a2.1 2.1 0 0 1 3 3L8 19l-4 1 1-4Z"/></svg>`,
    duplicate: `<svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="11" height="11" rx="1"/><path d="M5 15V5h10"/></svg>`,
    delete: `<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 7h16"/><path d="M10 11v6"/><path d="M14 11v6"/><path d="M6 7l1 14h10l1-14"/><path d="M9 7V4h6v3"/></svg>`,
    expand: `<svg viewBox="0 0 24 24" aria-hidden="true"><path d="m8 10 4 4 4-4"/></svg>`,
    collapse: `<svg viewBox="0 0 24 24" aria-hidden="true"><path d="m10 8 4 4-4 4"/></svg>`,
    group: `<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M10 8.5 8.5 7a4 4 0 0 0-5.7 5.7l2 2a4 4 0 0 0 5.7 0L12 13"/><path d="m14 15.5 1.5 1.5a4 4 0 0 0 5.7-5.7l-2-2a4 4 0 0 0-5.7 0L12 11"/><path d="m9 12 6 0"/></svg>`,
    ungroup: `<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8.5 7a4 4 0 0 0-5.7 5.7l2 2a4 4 0 0 0 5.2.4"/><path d="M15.5 17a4 4 0 0 0 5.7-5.7l-2-2a4 4 0 0 0-5.2-.4"/><path d="m4 4 16 16"/><path d="m9 12 2 0"/><path d="m13 12 2 0"/></svg>`,
    mask: `<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8 4h8l5 7-4 9H7l-4-9Z"/><path d="M7 12h10"/><path d="M12 7v10"/></svg>`,
    unmask: `<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8 4h8l5 7-4 9H7l-4-9Z"/><path d="m5 5 14 14"/></svg>`,
    drag: `<svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="5" r="2.25" fill="currentColor" stroke="none"/><circle cx="12" cy="12" r="2.25" fill="currentColor" stroke="none"/><circle cx="12" cy="19" r="2.25" fill="currentColor" stroke="none"/></svg>`,
    invert: `<svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="8"/><path d="M12 4v16"/><path d="M12 20a8 8 0 0 0 0-16Z" fill="currentColor" stroke="none"/></svg>`,
    fill: `<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 19h14"/><path d="m7 14 6-6 4 4-6 6Z"/><path d="M13 8 9 4"/><path d="M17 12l2 2"/></svg>`,
    undo: `<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M9 7 4 12l5 5"/><path d="M5 12h9a6 6 0 0 1 6 6v1"/></svg>`,
    redo: `<svg viewBox="0 0 24 24" aria-hidden="true"><path d="m15 7 5 5-5 5"/><path d="M19 12h-9a6 6 0 0 0-6 6v1"/></svg>`,
  };

  const PANEL_ICONS = {
    left: `<svg viewBox="0 0 24 24" aria-hidden="true"><path d="m15 6-6 6 6 6"/></svg>`,
    right: `<svg viewBox="0 0 24 24" aria-hidden="true"><path d="m9 6 6 6-6 6"/></svg>`,
  };

  const UV_OVERLAY_BASE_ID = "__uv_overlay__";
  const UV_OVERLAY_PARTS_ID = "__uv_parts_overlay__";

  function isUvOverlayLayerId(layerId) {
    return layerId === UV_OVERLAY_BASE_ID || layerId === UV_OVERLAY_PARTS_ID;
  }

  function uvOverlayLayerConfig(layerId) {
    if (layerId === UV_OVERLAY_PARTS_ID) {
      return {
        id: UV_OVERLAY_PARTS_ID,
        role: "parts",
        name: "UV PARTS OVERLAY",
        cachePrefix: "uv-parts-overlay",
        nodeKey: "partsUvOverlayNode",
        visibleKey: "partsUvOverlayVisible",
        invertedKey: "partsUvOverlayInverted",
      };
    }
    return {
      id: UV_OVERLAY_BASE_ID,
      role: "base",
      name: "UV BASE OVERLAY",
      cachePrefix: "uv-overlay",
      nodeKey: "uvOverlayNode",
      visibleKey: "uvOverlayVisible",
      invertedKey: "uvOverlayInverted",
    };
  }

  const MATERIAL_FINISH_DEFAULT = Object.freeze({
    metalness: 0,
    roughness: 0.3,
    clearCoat: 1,
    clearCoatRoughness: 0.3,
    scope: "base",
  });

  const MATERIAL_FINISH_PRESETS = {
    gloss: { metalness: 0, roughness: 0.43, clearCoat: 0.6, clearCoatRoughness: 0 },
    matte: { metalness: 0, roughness: 0.86, clearCoat: 0.1, clearCoatRoughness: 0.57 },
    metallic: { metalness: 0.9, roughness: 0.71, clearCoat: 0.8, clearCoatRoughness: 0 },
    satinMetallic: { metalness: 0.9, roughness: 0.71, clearCoat: 0.5, clearCoatRoughness: 0.43 },
    matteMetallic: { metalness: 0.9, roughness: 0.86, clearCoat: 0.1, clearCoatRoughness: 0.57 },
    chrome: { metalness: 1, roughness: 0.29, clearCoat: 0.8, clearCoatRoughness: 0 },
    reset: MATERIAL_FINISH_DEFAULT,
  };

  function iconHtml(name) {
    return window.uiIcon?.(name) || "";
  }

  function layerActionButton(action, title, icon, disabled = false) {
    return `<button class="layer-studio-icon-button" type="button" data-layer-action="${action}" title="${escapeHtml(title)}" aria-label="${escapeHtml(title)}" ${disabled ? "disabled" : ""}>${icon}</button>`;
  }

  function setText(id, value) {
    const node = $(id);
    if (node) node.textContent = String(value ?? "");
  }

  function isDraftTool(tool = state.activeTool) {
    return tool === "polygon" || tool === "stripe";
  }

  function usesPrecisionCursor() {
    return true;
  }

  function updateCanvasCursor(cursor = "") {
    const container = state.konva?.stage?.container?.();
    if (!container) return;
    if (cursor === null) {
      container.style.cursor = "";
      return;
    }
    container.style.cursor = cursor || (state.spacePanActive ? "grab" : usesPrecisionCursor() ? "crosshair" : "grab");
  }

  function updateTransformerCursor(event = null) {
    const transformer = state.konva?.transformer;
    const target = event?.target;
    const anchorName = transformer?.getActiveAnchor?.() || target?.name?.();
    const cleanAnchorName = cleanText(anchorName).split(" ")[0];
    if (cleanAnchorName === "rotater" || target?.hasName?.("rotater")) {
      updateCanvasCursor("grab");
      return true;
    }
    updateCanvasCursor();
    return false;
  }

  function fontPresets() {
    return Array.isArray(window.LiveryLabLayerStudioFonts?.presets)
      ? window.LiveryLabLayerStudioFonts.presets
      : [];
  }

  function defaultFontPreset() {
    return fontPresets()[0] || { id: "system", label: "Arial", family: "Arial", files: [] };
  }

  function fontPresetByFamily(family) {
    const clean = cleanText(family);
    return fontPresets().find((preset) => preset.family === clean || preset.id === clean) || defaultFontPreset();
  }

  function fontWeightValue(value, fallback = 700) {
    return intValue(value, fallback, 100);
  }

  function fontStyleForWeight(weight) {
    return fontWeightValue(weight, 400) >= 700 ? "bold" : "normal";
  }

  async function ensureAppFontsLoaded() {
    if (state.fontLoadPromise) return state.fontLoadPromise;
    state.fontLoadPromise = (async () => {
      if (typeof window.FontFace !== "function" || !document.fonts) return false;
      const loads = [];
      fontPresets().forEach((preset) => {
        (Array.isArray(preset.files) ? preset.files : []).forEach((file) => {
          const path = cleanText(file.path);
          if (!preset.family || !path) return;
          const face = new window.FontFace(preset.family, `url("${path}")`, {
            style: cleanText(file.style, "normal"),
            weight: String(file.weight || 400),
          });
          loads.push(face.load()
            .then((loaded) => {
              document.fonts.add(loaded);
              return loaded;
            })
            .catch((error) => {
              app().log?.(`Layer Studio font failed: ${preset.family} / ${path}: ${error?.message || String(error)}`, true);
              return null;
            }));
        });
      });
      await Promise.all(loads);
      return true;
    })();
    return state.fontLoadPromise;
  }

  function setDirty(dirty, options = {}) {
    const nextDirty = Boolean(dirty);
    if (nextDirty && options.bumpRevision !== false) state.changeRevision += 1;
    if (!nextDirty) {
      if (state.autosaveTimer) {
        window.clearTimeout(state.autosaveTimer);
        state.autosaveTimer = null;
      }
      state.autosavePending = false;
      state.savedRevision = state.changeRevision;
    }
    state.dirty = nextDirty;
    updateDirtyIndicator(options.status);
    if (nextDirty && options.autosave !== false) scheduleLayerAutosave();
  }

  function updateDirtyIndicator(status = "") {
    const node = $("layerStudioDirtyState");
    if (!node) return;
    node.textContent = status || (state.dirty ? "Unsaved changes" : "Saved");
    node.classList.toggle("is-dirty", state.dirty);
    node.classList.toggle("is-clean", !state.dirty);
    $("layerStudioSaveProject")?.classList.toggle("my-skin-game-sync-pulse", state.dirty);
  }

  function scheduleLayerAutosave(delay = state.autosaveDelayMs) {
    if (!state.context?.skinKey || !state.layerDocument?.layers || state.opening) return;
    if (state.autosaveTimer) window.clearTimeout(state.autosaveTimer);
    state.autosaveTimer = window.setTimeout(() => {
      state.autosaveTimer = null;
      runLayerAutosave();
    }, Math.max(250, Number(delay) || state.autosaveDelayMs));
  }

  async function runLayerAutosave() {
    if (!state.dirty || !state.context?.skinKey || !state.layerDocument?.layers) return false;
    if (state.saving) {
      state.autosavePending = true;
      return false;
    }
    return saveLayerStudioProject({ silent: true, autosave: true });
  }

  function updateHistoryButtons() {
    const undo = $("layerStudioUndo");
    const redo = $("layerStudioRedo");
    if (undo) {
      undo.innerHTML = LAYER_ACTION_ICONS.undo;
      undo.disabled = !state.undoStack.length;
      undo.title = state.undoStack.length ? "Undo" : "Undo unavailable";
      undo.setAttribute("aria-label", undo.title);
    }
    if (redo) {
      redo.innerHTML = LAYER_ACTION_ICONS.redo;
      redo.disabled = !state.redoStack.length;
      redo.title = state.redoStack.length ? "Redo" : "Redo unavailable";
      redo.setAttribute("aria-label", redo.title);
    }
  }

  function layerHistorySnapshot(label = "") {
    return {
      label: cleanText(label),
      layerDocument: cloneJson(state.layerDocument),
      selectedLayerId: state.selectedLayerId || "",
      selectedLayerIds: selectedLayerIds(),
      polygonEditLayerId: state.polygonEditLayerId || "",
      polygonEditPointIndex: state.polygonEditPointIndex,
      polygonEditSelectedPointIndices: Array.isArray(state.polygonEditSelectedPointIndices) ? state.polygonEditSelectedPointIndices.slice() : [],
      selectedGuideId: state.selectedGuideId || "",
    };
  }

  function historySnapshotsEqual(left, right) {
    if (!left || !right) return false;
    return JSON.stringify(left.layerDocument || null) === JSON.stringify(right.layerDocument || null)
      && (left.selectedLayerId || "") === (right.selectedLayerId || "")
      && JSON.stringify(left.selectedLayerIds || []) === JSON.stringify(right.selectedLayerIds || [])
      && (left.polygonEditLayerId || "") === (right.polygonEditLayerId || "")
      && Number(left.polygonEditPointIndex ?? -1) === Number(right.polygonEditPointIndex ?? -1)
      && JSON.stringify(left.polygonEditSelectedPointIndices || []) === JSON.stringify(right.polygonEditSelectedPointIndices || [])
      && (left.selectedGuideId || "") === (right.selectedGuideId || "");
  }

  function pushHistory(label = "Layer change") {
    if (!state.layerDocument) return null;
    if (state.historySnapshot) commitHistory();
    const snapshot = layerHistorySnapshot(label);
    state.undoStack.push(snapshot);
    if (state.undoStack.length > state.historyLimit) state.undoStack.shift();
    state.redoStack = [];
    updateHistoryButtons();
    return snapshot;
  }

  function pushHistorySnapshot(snapshot) {
    if (!snapshot?.layerDocument) return;
    state.undoStack.push(snapshot);
    if (state.undoStack.length > state.historyLimit) state.undoStack.shift();
    state.redoStack = [];
    updateHistoryButtons();
  }

  function beginHistory(label = "Layer change") {
    if (state.historySnapshot || !state.layerDocument) return;
    state.historySnapshot = layerHistorySnapshot(label);
  }

  function commitHistory() {
    const snapshot = state.historySnapshot;
    state.historySnapshot = null;
    if (!snapshot) return;
    if (historySnapshotsEqual(snapshot, layerHistorySnapshot(snapshot.label))) {
      updateHistoryButtons();
      return;
    }
    state.undoStack.push(snapshot);
    if (state.undoStack.length > state.historyLimit) state.undoStack.shift();
    state.redoStack = [];
    updateHistoryButtons();
  }

  function cancelHistory() {
    state.historySnapshot = null;
  }

  async function restoreHistorySnapshot(snapshot) {
    if (!snapshot?.layerDocument) return false;
    state.layerDocument = normalizeLayerDocument(cloneJson(snapshot.layerDocument), state.textureSize);
    state.selectedLayerId = snapshot.selectedLayerId || "";
    state.selectedLayerIds = Array.isArray(snapshot.selectedLayerIds) ? snapshot.selectedLayerIds.slice() : [];
    state.polygonEditLayerId = snapshot.polygonEditLayerId || "";
    state.polygonEditPointIndex = Number.isInteger(snapshot.polygonEditPointIndex) ? snapshot.polygonEditPointIndex : -1;
    state.polygonEditSelectedPointIndices = Array.isArray(snapshot.polygonEditSelectedPointIndices) ? snapshot.polygonEditSelectedPointIndices.slice() : [];
    state.selectedGuideId = snapshot.selectedGuideId || "";
    state.renamingLayerId = "";
    state.renameOriginalValue = "";
    state.hoveredLayerId = "";
    state.multiDrag = null;
    state.polygonEditPointDrag = null;
    await rebuildLayerWorkflow({ keepSelection: true });
    updateTransformerForSelection();
    renderGuides();
    renderMultiSelectionBox();
    renderLayerHoverBox();
    setDirty(true);
    updateHistoryButtons();
    return true;
  }

  async function undoLayerHistory() {
    const snapshot = state.undoStack.pop();
    if (!snapshot) return false;
    state.redoStack.push(layerHistorySnapshot("Redo"));
    await restoreHistorySnapshot(snapshot);
    return true;
  }

  async function redoLayerHistory() {
    const snapshot = state.redoStack.pop();
    if (!snapshot) return false;
    state.undoStack.push(layerHistorySnapshot("Undo"));
    await restoreHistorySnapshot(snapshot);
    return true;
  }

  function setActiveTool(tool) {
    state.activeTool = tool === "polygon" || tool === "stripe" ? tool : "select";
    if (!isDraftTool()) cancelPolygonDraft();
    if (isDraftTool()) exitPolygonEditMode();
    const select = $("layerStudioToolSelect");
    const polygon = $("layerStudioToolPolygon");
    const stripe = $("layerStudioToolStripe");
    if (select) {
      select.classList.toggle("is-active", state.activeTool === "select");
      select.setAttribute("aria-pressed", state.activeTool === "select" ? "true" : "false");
    }
    if (polygon) {
      polygon.classList.toggle("is-active", state.activeTool === "polygon");
      polygon.setAttribute("aria-pressed", state.activeTool === "polygon" ? "true" : "false");
    }
    if (stripe) {
      stripe.classList.toggle("is-active", state.activeTool === "stripe");
      stripe.setAttribute("aria-pressed", state.activeTool === "stripe" ? "true" : "false");
    }
    updateStageInteractivity();
    const container = state.konva?.stage?.container?.();
    if (container) container.style.cursor = usesPrecisionCursor() ? "crosshair" : "grab";
    setText(
      "layerStudioEditorStatus",
      state.activeTool === "polygon"
        ? "Polygon tool active.\nClick texture points, click the first point or press Enter to finish, Esc to cancel."
        : state.activeTool === "stripe"
        ? "Stripe tool active.\nClick texture points, click-drag a new point to shape a Bezier curve, press Enter to finish, Esc to cancel."
        : "Select tool active.",
    );
  }

  function cloneJson(value) {
    return JSON.parse(JSON.stringify(value || null));
  }

  function numberValue(value, fallback, min = null, max = null) {
    let next = Number(String(value ?? "").trim().replace(",", "."));
    if (!Number.isFinite(next)) next = fallback;
    if (min !== null) next = Math.max(min, next);
    if (max !== null) next = Math.min(max, next);
    return next;
  }

  function intValue(value, fallback, min = 0) {
    return Math.max(min, Math.round(numberValue(value, fallback)));
  }

  function coordinateValue(value, fallback = 0) {
    return Math.round(numberValue(value, fallback));
  }

  function normalizeLayerPoint(x, y) {
    return {
      x: coordinateValue(x),
      y: coordinateValue(y),
    };
  }

  function cleanText(value, fallback = "") {
    const text = String(value || "").trim();
    return text || fallback;
  }

  function nextPaintFrame() {
    return new Promise((resolve) => {
      window.requestAnimationFrame(() => window.requestAnimationFrame(resolve));
    });
  }

  function cssColorValue(value, fallback = "#ffffff") {
    const text = cleanText(value, fallback);
    return /^#(?:[0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$/i.test(text) ? text : fallback;
  }

  function normalizeFavoriteColor(value, fallback = "") {
    const text = String(value ?? "").trim().replace(/^#/, "");
    if (!/^[0-9a-f]{6}$/i.test(text)) return fallback;
    return `#${text.toUpperCase()}`;
  }

  function activeDrawingColor(fallback = "#e43d30") {
    state.activeColor = normalizeFavoriteColor(state.activeColor, normalizeFavoriteColor(fallback, "#E43D30"));
    return state.activeColor;
  }

  function layerTextValue(value, fallback = "NEW TEXT") {
    if (value === null || value === undefined) return fallback;
    const text = String(value).replace(/\r\n?/g, "\n");
    return text.length ? text : fallback;
  }

  function textLayerName(text, fallback = "Text") {
    return String(text ?? "").replace(/\s+/g, " ").trim().slice(0, 48) || fallback;
  }

  function textLayerMetrics(layer) {
    const fontSize = intValue(layer?.fontSize, 96, 4);
    const fontWeight = fontWeightValue(layer?.fontWeight, 700);
    const family = cleanText(layer?.fontFamily, defaultFontPreset().family || "Arial");
    const lines = layerTextValue(layer?.text, " ").split("\n");
    const canvas = textLayerMetrics.canvas || (textLayerMetrics.canvas = document.createElement("canvas"));
    const ctx = canvas.getContext("2d");
    const fallbackAscent = Math.round(fontSize * 0.82);
    const fallbackDescent = Math.max(1, Math.round(fontSize * 0.22));
    const fallbackLineHeight = Math.max(1, fallbackAscent + fallbackDescent);
    if (!ctx) return { width: 1, height: Math.max(1, lines.length * fallbackLineHeight), ascent: fallbackAscent, lineAdvance: fallbackLineHeight };
    ctx.font = `${fontWeight} ${fontSize}px "${family}"`;
    const metrics = lines.map((line) => ctx.measureText(line || " "));
    const width = Math.max(1, Math.ceil(Math.max(...metrics.map((row) => row.width))));
    const lineBoxes = metrics.map((row) => {
      const actualHeight = numberValue(row.actualBoundingBoxAscent, 0) + numberValue(row.actualBoundingBoxDescent, 0);
      return {
        ascent: numberValue(row.actualBoundingBoxAscent, fallbackAscent),
        descent: numberValue(row.actualBoundingBoxDescent, fallbackDescent),
        actualHeight: actualHeight || fallbackLineHeight,
      };
    });
    const maxAscent = Math.max(1, Math.ceil(Math.max(...lineBoxes.map((row) => row.ascent))));
    const maxDescent = Math.max(1, Math.ceil(Math.max(...lineBoxes.map((row) => row.descent))));
    const lineAdvance = Math.max(1, Math.ceil(maxAscent + maxDescent));
    const height = Math.max(1, Math.ceil(lines.length * lineAdvance));
    return { width, height, ascent: maxAscent, lineAdvance };
  }

  function renderTextLayerCanvas(layer) {
    const metrics = textLayerMetrics(layer);
    const canvas = document.createElement("canvas");
    canvas.width = Math.max(1, metrics.width);
    canvas.height = Math.max(1, metrics.height);
    const ctx = canvas.getContext("2d");
    if (!ctx) return { canvas, metrics };
    const fontSize = intValue(layer?.fontSize, 96, 4);
    const fontWeight = fontWeightValue(layer?.fontWeight, 700);
    const family = cleanText(layer?.fontFamily, defaultFontPreset().family || "Arial");
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    ctx.fillStyle = cleanText(layer?.fill, "#ffffff");
    ctx.font = `${fontWeight} ${fontSize}px "${family}"`;
    ctx.textBaseline = "alphabetic";
    layerTextValue(layer?.text, " ").split("\n").forEach((line, index) => {
      ctx.fillText(line, 0, metrics.ascent + index * metrics.lineAdvance);
    });
    return { canvas, metrics };
  }

  function formatFileSize(size) {
    const value = Number(size || 0);
    if (!Number.isFinite(value) || value <= 0) return "-";
    if (value < 1024) return `${Math.round(value)} B`;
    if (value < 1024 * 1024) return `${(value / 1024).toFixed(1)} KB`;
    return `${(value / 1024 / 1024).toFixed(1)} MB`;
  }

  function showWorkspace() {
    const workspace = $("layerStudioWorkspace");
    if (workspace) workspace.hidden = false;
    resizeCanvas();
    window.LiveryLabPreview3d?.resize?.();
  }

  function destroyCanvas() {
    clearPolygonEditHandles();
    state.polygonEditLayerId = "";
    state.polygonEditPointIndex = -1;
    state.polygonEditSelectedPointIndices = [];
    state.polygonEditPointDrag = null;
    state.konva?.stage?.destroy();
    state.konva = null;
    state.baseImageElement = null;
    state.canvasPan = null;
    state.suppressCanvasClick = false;
    state.spacePanActive = false;
    state.view = { scale: 1, x: 0, y: 0 };
    const host = $("layerStudioCanvasHost");
    if (host) host.innerHTML = "";
  }

  function closeWorkspace() {
    hideUpdateTextureFeedback(true);
    app().closePreview?.();
    const workspace = $("layerStudioWorkspace");
    if (workspace) workspace.hidden = true;
    state.context = null;
    state.project = null;
    state.layerDocument = null;
    state.selectedLayerId = "";
    state.selectedLayerIds = [];
    state.activeTool = "select";
    state.polygonEditLayerId = "";
    state.polygonEditPointIndex = -1;
    state.polygonEditSelectedPointIndices = [];
    state.polygonEditPointDrag = null;
    state.selectedGuideId = "";
    state.canvasPan = null;
    state.suppressCanvasClick = false;
    state.spacePanActive = false;
    setColorFavoritesOpen(false);
    cancelPolygonDraft();
    clearPolygonEditHandles();
    setDirty(false);
    state.imageCache.clear();
    setText("layerStudioTitle", "Layer Studio");
    setText("layerStudioMeta", "No livery loaded");
    setText("layerStudioTextureLabel", "EXT_SKIN_C");
    setText("layerStudioSelectionStatus", "No layer selected");
    setText("layerStudioEditorStatus", "Waiting for Layer Studio workspace.");
    setText("layerStudioPreviewStatus", "Waiting for 3D preview.");
    state.assetLibrary = null;
    state.projectAssetLibrary = null;
    state.libraryActiveScope = "project";
    renderLayerList();
    renderAssetLibrary();
    renderPropertiesPanel();
    syncGuideControls();
    destroyCanvas();
    const empty = $("layerStudioTextureEmpty");
    if (empty) empty.style.display = "";
  }

  function surfaceKey(value, fallback = "ext_skin_c") {
    const key = cleanText(value).toLowerCase().replace(/[^0-9a-z_]+/g, "_").replace(/^_+|_+$/g, "");
    return key || fallback;
  }

  function isSplatMapSurface(key = state.activeSurfaceKey) {
    return surfaceKey(key) === "ext_skin_d";
  }

  function defaultBaseColorForSurface(key = state.activeSurfaceKey) {
    return isSplatMapSurface(key) ? SPLAT_MAP_BASE_COLOR_DEFAULT : BASE_COLOR_DEFAULT;
  }

  function normalizeSplatMapColor(value, fallback = state.splatMapColor || "#ff0000") {
    const color = cssColorValue(value, fallback).slice(0, 7).toLowerCase();
    const match = SPLAT_MAP_PALETTE.find((item) => item.color === color);
    if (match) return match.color;
    return SPLAT_MAP_PALETTE.find((item) => item.color === fallback)?.color || "#ff0000";
  }

  function activeSplatMapColor() {
    state.splatMapColor = normalizeSplatMapColor(state.splatMapColor || "#ff0000");
    return state.splatMapColor;
  }

  function setColorFavoritesStatus(message = "", isError = false) {
    const status = $("layerStudioColorFavoritesStatus");
    if (!status) return;
    status.textContent = String(message || "");
    status.classList.toggle("is-error", Boolean(isError));
  }

  function setColorFavoritesOpen(open) {
    const allowed = !isSplatMapSurface();
    state.colorFavoritesOpen = Boolean(open && allowed);
    if (!state.colorFavoritesOpen) state.colorFavoriteEditingSlot = -1;
    const toggle = $("layerStudioColorFavoritesToggle");
    const panel = $("layerStudioColorFavoritesPanel");
    if (toggle) {
      toggle.setAttribute("aria-expanded", state.colorFavoritesOpen ? "true" : "false");
      toggle.classList.toggle("is-open", state.colorFavoritesOpen);
    }
    if (panel) panel.hidden = !state.colorFavoritesOpen;
    renderColorFavorites();
  }

  function syncColorFavoritesAvailability() {
    const splatMode = isSplatMapSurface();
    const dock = $("layerStudioColorFavoritesDock");
    const toggle = $("layerStudioColorFavoritesToggle");
    const swatch = $("layerStudioActiveColorSwatch");
    if (dock) dock.hidden = splatMode;
    if (swatch) swatch.style.setProperty("--layer-studio-active-color", activeDrawingColor());
    if (toggle) {
      toggle.disabled = splatMode || state.colorFavoritesSaving;
      toggle.title = splatMode
        ? "Favorite colors are unavailable for EXT_SKIN_D. Use the fixed splat map palette."
        : "Open favorite colors";
    }
    if (splatMode && state.colorFavoritesOpen) setColorFavoritesOpen(false);
  }

  function colorFavoriteRowMarkup(color, slot) {
    const editing = state.colorFavoriteEditingSlot === slot || !color;
    if (editing) {
      const value = normalizeFavoriteColor(color, activeDrawingColor());
      return `
        <div class="layer-studio-color-favorite-row is-editing" data-color-favorite-slot="${slot}">
          <input class="layer-studio-color-favorite-picker" type="color" value="${value}" data-color-favorite-picker="${slot}" aria-label="Favorite color ${slot + 1}" />
          <input class="layer-studio-color-favorite-hex" type="text" value="${color || ""}" placeholder="#RRGGBB" maxlength="7" spellcheck="false" data-color-favorite-hex="${slot}" aria-label="Favorite color ${slot + 1} hex value" />
          <button type="button" data-color-favorite-action="save" data-color-favorite-slot="${slot}" ${state.colorFavoritesSaving ? "disabled" : ""}>Save</button>
          ${color ? `<button class="layer-studio-color-favorite-icon" type="button" data-color-favorite-action="cancel" data-color-favorite-slot="${slot}" aria-label="Cancel editing favorite color ${slot + 1}" title="Cancel">×</button>` : ""}
        </div>
      `;
    }
    const active = color === activeDrawingColor();
    return `
      <div class="layer-studio-color-favorite-row${active ? " is-active" : ""}" data-color-favorite-slot="${slot}">
        <button class="layer-studio-color-favorite-apply" type="button" data-color-favorite-action="apply" data-color-favorite-slot="${slot}" data-color-favorite-color="${color}" aria-label="Use favorite color ${color}">
          <span class="layer-studio-color-favorite-swatch" style="--layer-studio-favorite-color: ${color}" aria-hidden="true"></span>
          <code>${color}</code>
        </button>
        <button class="layer-studio-color-favorite-icon" type="button" data-color-favorite-action="edit" data-color-favorite-slot="${slot}" aria-label="Edit favorite color ${slot + 1}" title="Edit">${LAYER_ACTION_ICONS.edit}</button>
        <button class="layer-studio-color-favorite-icon is-delete" type="button" data-color-favorite-action="delete" data-color-favorite-slot="${slot}" aria-label="Delete favorite color ${slot + 1}" title="Delete">${LAYER_ACTION_ICONS.delete}</button>
      </div>
    `;
  }

  function renderColorFavorites() {
    const list = $("layerStudioColorFavoritesList");
    if (list) {
      const favorites = Array.from({ length: 5 }, (_unused, slot) => state.colorFavorites[slot] || null);
      list.innerHTML = favorites.map((color, slot) => colorFavoriteRowMarkup(color, slot)).join("");
    }
    syncColorFavoritesAvailability();
  }

  async function loadColorFavorites() {
    if (state.colorFavoritesLoaded) {
      renderColorFavorites();
      return true;
    }
    if (typeof window.pywebview?.api?.load_layer_studio_color_favorites !== "function") {
      setColorFavoritesStatus("Favorite color storage is unavailable in this build.", true);
      return false;
    }
    try {
      const result = await window.pywebview.api.load_layer_studio_color_favorites();
      if (!result?.ok) {
        setColorFavoritesStatus(result?.message || "Favorite colors could not be loaded.", true);
        return false;
      }
      const values = Array.isArray(result.data?.favorites) ? result.data.favorites : [];
      state.colorFavorites = Array.from({ length: 5 }, (_unused, slot) => normalizeFavoriteColor(values[slot]) || null);
      state.colorFavoritesLoaded = true;
      setColorFavoritesStatus("");
      renderColorFavorites();
      return true;
    } catch (error) {
      setColorFavoritesStatus(error?.message || "Favorite colors could not be loaded.", true);
      return false;
    }
  }

  async function persistColorFavorites(nextFavorites, successMessage) {
    if (state.colorFavoritesSaving) return false;
    if (typeof window.pywebview?.api?.save_layer_studio_color_favorites !== "function") {
      setColorFavoritesStatus("Favorite color storage is unavailable in this build.", true);
      return false;
    }
    state.colorFavoritesSaving = true;
    renderColorFavorites();
    try {
      const result = await window.pywebview.api.save_layer_studio_color_favorites({ favorites: nextFavorites });
      if (!result?.ok) {
        setColorFavoritesStatus(result?.message || "Favorite colors could not be saved.", true);
        return false;
      }
      const values = Array.isArray(result.data?.favorites) ? result.data.favorites : nextFavorites;
      state.colorFavorites = Array.from({ length: 5 }, (_unused, slot) => normalizeFavoriteColor(values[slot]) || null);
      state.colorFavoriteEditingSlot = -1;
      setColorFavoritesStatus(successMessage || "Favorite colors saved.");
      return true;
    } catch (error) {
      setColorFavoritesStatus(error?.message || "Favorite colors could not be saved.", true);
      return false;
    } finally {
      state.colorFavoritesSaving = false;
      renderColorFavorites();
    }
  }

  async function saveColorFavoriteSlot(slot, value) {
    const index = intValue(slot, -1, -1);
    if (index < 0 || index >= 5) return false;
    const color = normalizeFavoriteColor(value);
    if (!color) {
      setColorFavoritesStatus("Enter a valid six-digit hex color such as #E43D30.", true);
      return false;
    }
    if (state.colorFavorites.some((saved, savedSlot) => savedSlot !== index && saved === color)) {
      setColorFavoritesStatus(`${color} is already saved.`, true);
      return false;
    }
    const favorites = state.colorFavorites.slice(0, 5);
    favorites[index] = color;
    return await persistColorFavorites(favorites, `${color} saved.`);
  }

  async function deleteColorFavoriteSlot(slot) {
    const index = intValue(slot, -1, -1);
    if (index < 0 || index >= 5 || !state.colorFavorites[index]) return false;
    const favorites = state.colorFavorites.slice(0, 5);
    const color = favorites[index];
    favorites[index] = null;
    return await persistColorFavorites(favorites, `${color} removed.`);
  }

  function applyFavoriteColor(value) {
    if (isSplatMapSurface()) return false;
    const color = normalizeFavoriteColor(value);
    if (!color) return false;
    state.activeColor = color;
    const selected = selectedLayers();
    const layer = state.activeTool === "select" && selected.length === 1 ? selected[0] : null;
    if (layer && !layer.locked && (isPointLayerType(layer.type) || isShapeLayerType(layer.type) || isTextLayerType(layer.type))) {
      if (fillGradientForLayer(layer)) {
        setColorFavoritesStatus(`${color} selected for new layers. The selected gradient was not changed.`);
      } else {
        pushHistory("Apply favorite color");
        patchLayer(layer.id, layer.type === "stripe" ? { stroke: color } : { fill: color });
        setColorFavoritesStatus(`${color} applied to ${layer.name || layer.id}.`);
      }
    } else if (isDraftTool()) {
      setColorFavoritesStatus(`${color} selected for the active ${state.activeTool} tool.`);
    } else {
      setColorFavoritesStatus(`${color} selected for new layers.`);
    }
    renderColorFavorites();
    return true;
  }

  function textureSurfaces(context = state.context) {
    return Array.isArray(context?.textureSurfaces) ? context.textureSurfaces.filter((row) => row && typeof row === "object") : [];
  }

  function textureSurfaceByKey(key, context = state.context) {
    const target = surfaceKey(key);
    return textureSurfaces(context).find((row) => surfaceKey(row.key) === target) || null;
  }

  function activeTextureSurface(context = state.context) {
    const surfaces = textureSurfaces(context);
    const key = surfaceKey(context?.activeSurfaceKey || state.activeSurfaceKey);
    return surfaces.find((row) => surfaceKey(row.key) === key) || surfaces[0] || null;
  }

  function activeTextureSurfaceLabel(context = state.context) {
    const surface = activeTextureSurface(context);
    return cleanText(surface?.label || surface?.fileName || surface?.activeTexture?.name, "EXT_SKIN_C");
  }

  function isSpecPartsSurfaceKey(key, context = state.context) {
    return false;
  }

  function texturePath(context = state.context) {
    const surface = activeTextureSurface(context);
    return String(surface?.activeTexture?.path || context?.activeTexture?.path || context?.previewProject?.liveTexturePath || context?.previewProject?.texturePath || "").trim();
  }

  function texturePathForSurface(key, context = state.context) {
    const surface = textureSurfaceByKey(key, context);
    const project = context?.previewProject || state.project || {};
    if (surface?.activeTexture?.path) return String(surface.activeTexture.path).trim();
    const normalized = surfaceKey(key);
    if (normalized === "ext_banner_c") return String(project.liveBannerTexturePath || project.bannerTexturePath || "").trim();
    if (normalized === "ext_skin_d") return String(project.liveDetailTexturePath || project.detailTexturePath || "").trim();
    return String(project.liveTexturePath || project.texturePath || "").trim();
  }

  function baseTexturePath(context = state.context) {
    const surface = activeTextureSurface(context);
    return String(surface?.baseTexture?.path || context?.baseTexture?.path || "").trim();
  }

  function editorTexturePath(context = state.context) {
    const base = context?.baseTexture || {};
    const cleanBase = String(base.path || "").trim();
    return cleanBase && base.exists !== false ? cleanBase : texturePath(context);
  }

  function defaultLayerDocument(textureSize = state.textureSize, textureKey = state.activeSurfaceKey) {
    return {
      version: 1,
      texture: surfaceKey(textureKey),
      textureSize: {
        width: intValue(textureSize?.width, 4096, 1),
        height: intValue(textureSize?.height, 4096, 1),
      },
      baseTexture: "base.png",
      baseColor: {
        fill: defaultBaseColorForSurface(textureKey),
        visible: true,
      },
      guides: [],
      guidesVisible: true,
      snapEnabled: true,
      layers: [],
    };
  }

  function normalizeBaseColor(raw, textureKey = state.activeSurfaceKey) {
    const defaultFill = defaultBaseColorForSurface(textureKey);
    return {
      fill: cssColorValue(raw?.fill, defaultFill),
      visible: raw?.visible !== false,
      autoHiddenForNonFallbackSplatMap: raw?.autoHiddenForNonFallbackSplatMap === true,
      autoHiddenForOriginalBaseTexture: raw?.autoHiddenForOriginalBaseTexture === true,
      autoHiddenForActiveTexture: raw?.autoHiddenForActiveTexture === true,
    };
  }

  function hideUpdateTextureFeedback(immediate = false) {
    if (state.updateFeedbackTimer) window.clearTimeout(state.updateFeedbackTimer);
    if (state.updateFeedbackHideTimer) window.clearTimeout(state.updateFeedbackHideTimer);
    state.updateFeedbackTimer = null;
    state.updateFeedbackHideTimer = null;
    const feedback = $("layerStudioUpdateTextureFeedback");
    if (!feedback) return;
    feedback.classList.remove("is-visible");
    if (immediate) {
      feedback.hidden = true;
      return;
    }
    state.updateFeedbackHideTimer = window.setTimeout(() => {
      feedback.hidden = true;
      state.updateFeedbackHideTimer = null;
    }, 180);
  }

  function showUpdateTextureFeedback(data = {}) {
    const feedback = $("layerStudioUpdateTextureFeedback");
    if (!feedback) return;
    hideUpdateTextureFeedback(true);
    const label = cleanText(data.surfaceLabel || data.surfaceKey, "Texture");
    const changed = data.changed === true;
    const updateRequired = data.gameUpdateRequired === true;
    feedback.textContent = changed
      ? (updateRequired
        ? `${label} saved - Update in Game required`
        : `${label} saved - Activate in Game when ready`)
      : `${label} is already current`;
    feedback.classList.toggle("is-update-required", changed && updateRequired);
    feedback.classList.toggle("is-local", changed && !updateRequired);
    feedback.classList.toggle("is-current", !changed);
    feedback.hidden = false;
    window.requestAnimationFrame(() => feedback.classList.add("is-visible"));
    state.updateFeedbackTimer = window.setTimeout(() => hideUpdateTextureFeedback(false), 4000);
  }

  function hideMaterialFinishFeedback(immediate = false) {
    if (state.materialFinishFeedbackTimer) window.clearTimeout(state.materialFinishFeedbackTimer);
    if (state.materialFinishFeedbackHideTimer) window.clearTimeout(state.materialFinishFeedbackHideTimer);
    state.materialFinishFeedbackTimer = null;
    state.materialFinishFeedbackHideTimer = null;
    const feedback = $("layerStudioPaintFinishFeedback");
    if (!feedback) return;
    feedback.classList.remove("is-visible");
    if (immediate) {
      feedback.hidden = true;
      return;
    }
    state.materialFinishFeedbackHideTimer = window.setTimeout(() => {
      feedback.hidden = true;
      state.materialFinishFeedbackHideTimer = null;
    }, 180);
  }

  function showMaterialFinishFeedback(data = {}) {
    const feedback = $("layerStudioPaintFinishFeedback");
    if (!feedback) return;
    hideMaterialFinishFeedback(true);
    const changed = data.changed === true;
    const updateRequired = data.gameUpdateRequired === true;
    feedback.textContent = changed
      ? (updateRequired
        ? "Paint finish saved - Update in Game required"
        : "Paint finish saved - Activate in Game when ready")
      : "Paint finish is already current";
    feedback.classList.toggle("is-update-required", changed && updateRequired);
    feedback.classList.toggle("is-local", changed && !updateRequired);
    feedback.classList.toggle("is-current", !changed);
    feedback.hidden = false;
    window.requestAnimationFrame(() => feedback.classList.add("is-visible"));
    state.materialFinishFeedbackTimer = window.setTimeout(() => hideMaterialFinishFeedback(false), 4000);
  }

  function normalizeGuide(raw, index, textureSize) {
    const axis = cleanText(raw?.axis, "x").toLowerCase() === "y" ? "y" : "x";
    const limit = axis === "x" ? textureSize.width : textureSize.height;
    return {
      id: cleanText(raw?.id, `guide_${axis}_${String(index + 1).padStart(3, "0")}`),
      axis,
      position: Math.max(0, Math.min(limit, intValue(raw?.position, Math.round(limit / 2), 0))),
    };
  }

  function normalizeImageLayer(raw, index, textureSize) {
    const width = intValue(raw?.width, 256, 1);
    const height = intValue(raw?.height, 256, 1);
    const rawSourceType = cleanText(raw?.sourceType, "project").toLowerCase().replace("_", "");
    const sourceType = rawSourceType === "library"
      ? "library"
      : rawSourceType === "projectlibrary"
        ? "projectLibrary"
        : "project";
    return {
      id: cleanText(raw?.id, `layer_${String(index + 1).padStart(3, "0")}`),
      name: cleanText(raw?.name, `Image Layer ${index + 1}`),
      type: "image",
      sourceType,
      source: cleanText(raw?.source),
      x: coordinateValue(raw?.x, Math.max(0, (textureSize.width - width) / 2)),
      y: coordinateValue(raw?.y, Math.max(0, (textureSize.height - height) / 2)),
      width,
      height,
      rotation: numberValue(raw?.rotation, 0),
      flipX: Boolean(raw?.flipX),
      flipY: Boolean(raw?.flipY),
      opacity: numberValue(raw?.opacity, 1, 0, 1),
      ratioLocked: raw?.ratioLocked !== false,
      visible: raw?.visible !== false,
      locked: Boolean(raw?.locked),
      order: intValue(raw?.order, index, 0),
    };
  }

  function normalizePolygonLayer(raw, index, textureSize) {
    const points = Array.isArray(raw?.points)
      ? raw.points.map((value) => numberValue(value, 0)).filter((value) => Number.isFinite(value))
      : [];
    const fallbackPoints = [
      Math.max(0, Math.round(textureSize.width / 2 - 220)), Math.max(0, Math.round(textureSize.height / 2 - 160)),
      Math.min(textureSize.width, Math.round(textureSize.width / 2 + 220)), Math.max(0, Math.round(textureSize.height / 2 - 160)),
      Math.min(textureSize.width, Math.round(textureSize.width / 2 + 180)), Math.min(textureSize.height, Math.round(textureSize.height / 2 + 160)),
      Math.max(0, Math.round(textureSize.width / 2 - 180)), Math.min(textureSize.height, Math.round(textureSize.height / 2 + 160)),
    ];
    const cleanPoints = points.length >= 6 ? points : fallbackPoints;
    return {
      id: cleanText(raw?.id, `layer_${String(index + 1).padStart(3, "0")}`),
      name: cleanText(raw?.name, `Polygon Layer ${index + 1}`),
      type: "polygon",
      points: cleanPoints,
      bezierHandles: normalizePointBezierHandles(raw?.bezierHandles, cleanPoints, textureSize, { closed: true }),
      fill: cleanText(raw?.fill, "#e43d30"),
      fillGradient: normalizeFillGradient(raw?.fillGradient, cleanText(raw?.fill, "#e43d30")),
      stroke: raw?.stroke ?? null,
      strokeWidth: intValue(raw?.strokeWidth, 0, 0),
      opacity: numberValue(raw?.opacity, 1, 0, 1),
      visible: raw?.visible !== false,
      locked: Boolean(raw?.locked),
      order: intValue(raw?.order, index, 0),
    };
  }

  function normalizePointBezierHandles(raw, points, textureSize, options = {}) {
    if (!Array.isArray(raw)) return [];
    const pointCount = Math.floor((Array.isArray(points) ? points.length : 0) / 2);
    const segmentCount = Math.max(0, options.closed ? pointCount : pointCount - 1);
    if (!segmentCount) return [];
    const handles = [];
    let hasHandle = false;
    for (let index = 0; index < segmentCount; index += 1) {
      const source = raw[index];
      if (!source || typeof source !== "object") {
        handles.push(null);
        continue;
      }
      const out = source.out && typeof source.out === "object" ? normalizeLayerPoint(source.out.x, source.out.y) : null;
      const input = source.in && typeof source.in === "object" ? normalizeLayerPoint(source.in.x, source.in.y) : null;
      if (!out && !input) {
        handles.push(null);
        continue;
      }
      hasHandle = true;
      handles.push({ out, in: input });
    }
    return hasHandle ? handles : [];
  }

  function normalizeStripeBezierHandles(raw, points, textureSize) {
    return normalizePointBezierHandles(raw, points, textureSize, { closed: false });
  }

  function normalizePolygonBezierHandles(raw, points, textureSize) {
    return normalizePointBezierHandles(raw, points, textureSize, { closed: true });
  }

  function normalizeStripeLayer(raw, index, textureSize) {
    const points = Array.isArray(raw?.points)
      ? raw.points.map((value) => numberValue(value, 0)).filter((value) => Number.isFinite(value))
      : [];
    const fallbackPoints = [
      Math.max(0, Math.round(textureSize.width / 2 - 420)), Math.max(0, Math.round(textureSize.height / 2 - 120)),
      Math.max(0, Math.round(textureSize.width / 2 - 120)), Math.max(0, Math.round(textureSize.height / 2 - 180)),
      Math.min(textureSize.width, Math.round(textureSize.width / 2 + 420)), Math.min(textureSize.height, Math.round(textureSize.height / 2 + 140)),
    ];
    const lineCap = cleanText(raw?.lineCap, "round").toLowerCase();
    const lineJoin = cleanText(raw?.lineJoin, "round").toLowerCase();
    const stroke = cleanText(raw?.stroke || raw?.fill, "#f07a00");
    return {
      id: cleanText(raw?.id, `stripe_${String(index + 1).padStart(3, "0")}`),
      name: cleanText(raw?.name, `Stripe ${index + 1}`),
      type: "stripe",
      points: points.length >= 4 ? points : fallbackPoints,
      stroke,
      strokeGradient: normalizeFillGradient(raw?.strokeGradient, stroke),
      strokeWidth: intValue(raw?.strokeWidth, 20, 1),
      opacity: numberValue(raw?.opacity, 1, 0, 1),
      tension: numberValue(raw?.tension, 0, 0, 1),
      bezierHandles: normalizeStripeBezierHandles(raw?.bezierHandles, points.length >= 4 ? points : fallbackPoints, textureSize),
      lineCap: ["butt", "round", "square"].includes(lineCap) ? lineCap : "round",
      lineJoin: ["miter", "round", "bevel"].includes(lineJoin) ? lineJoin : "round",
      visible: raw?.visible !== false,
      locked: Boolean(raw?.locked),
      order: intValue(raw?.order, index, 0),
    };
  }

  function isPointLayerType(type) {
    return type === "polygon" || type === "stripe";
  }

  function isShapeLayerType(type) {
    return type === "rectangle" || type === "ellipse";
  }

  function isTextLayerType(type) {
    return type === "text";
  }

  function rgbaColorValue(color, alpha = 1) {
    const hex = cssColorValue(color, "#e43d30").replace("#", "");
    const full = hex.length === 3
      ? hex.split("").map((char) => `${char}${char}`).join("")
      : hex.padEnd(6, "0").slice(0, 6);
    const red = parseInt(full.slice(0, 2), 16);
    const green = parseInt(full.slice(2, 4), 16);
    const blue = parseInt(full.slice(4, 6), 16);
    return `rgba(${red}, ${green}, ${blue}, ${numberValue(alpha, 1, 0, 1)})`;
  }

  function gradientCssValue(gradient) {
    const data = normalizeFillGradient(gradient, "#e43d30");
    if (!data) return "linear-gradient(90deg, #e43d30, #e43d30)";
    const first = rgbaColorValue(data.colorA, data.alphaA);
    const second = rgbaColorValue(data.colorB, data.alphaB);
    if (data.type === "radial") return `radial-gradient(circle, ${second}, ${first})`;
    return `linear-gradient(${numberValue(data.angle, 90, 0, 360)}deg, ${first}, ${second})`;
  }

  function defaultFillGradient(type = "linear") {
    return {
      type: type === "radial" ? "radial" : "linear",
      colorA: "#000000",
      alphaA: 1,
      colorB: "#ffffff",
      alphaB: 1,
      angle: 90,
    };
  }

  function normalizeFillGradient(raw, fallbackColor = "#e43d30") {
    const data = raw && typeof raw === "object" ? raw : {};
    const type = cleanText(data.type, "solid").toLowerCase();
    if (type !== "linear" && type !== "radial") return null;
    return {
      type,
      colorA: cssColorValue(data.colorA || data.from || "#000000", "#000000"),
      alphaA: numberValue(data.alphaA, 1, 0, 1),
      colorB: cssColorValue(data.colorB || data.to || "#ffffff", "#ffffff"),
      alphaB: numberValue(data.alphaB, 1, 0, 1),
      angle: numberValue(data.angle, 90, 0, 360),
    };
  }

  function fillGradientForLayer(layer) {
    if (layer?.type === "stripe") {
      return normalizeFillGradient(layer.strokeGradient, layer.stroke || "#f07a00");
    }
    if (!layer || (!isShapeLayerType(layer.type) && layer.type !== "polygon")) return null;
    return normalizeFillGradient(layer.fillGradient, layer.fill || "#e43d30");
  }

  function shapeLayerFillStyle(ctx, layer, width, height, offsetX = 0, offsetY = 0) {
    const gradient = fillGradientForLayer(layer);
    if (!gradient) return cleanText(layer?.fill, "#e43d30");
    if (gradient.type === "radial") {
      const radius = Math.max(1, Math.sqrt(width * width + height * height) / 2);
      const centerX = offsetX + width / 2;
      const centerY = offsetY + height / 2;
      const radial = ctx.createRadialGradient(centerX, centerY, 0, centerX, centerY, radius);
      radial.addColorStop(0, rgbaColorValue(gradient.colorB, gradient.alphaB));
      radial.addColorStop(1, rgbaColorValue(gradient.colorA, gradient.alphaA));
      return radial;
    }
    const radians = ((gradient.angle - 90) * Math.PI) / 180;
    const centerX = offsetX + width / 2;
    const centerY = offsetY + height / 2;
    const directionX = Math.cos(radians);
    const directionY = Math.sin(radians);
    const half = (Math.abs(directionX) * width + Math.abs(directionY) * height) / 2;
    const dx = directionX * half;
    const dy = directionY * half;
    const linear = ctx.createLinearGradient(centerX - dx, centerY - dy, centerX + dx, centerY + dy);
    linear.addColorStop(0, rgbaColorValue(gradient.colorA, gradient.alphaA));
    linear.addColorStop(1, rgbaColorValue(gradient.colorB, gradient.alphaB));
    return linear;
  }

  function setCanvasFillStyle(ctx, style) {
    if (typeof ctx?.setAttr === "function") {
      ctx.setAttr("fillStyle", style);
    } else {
      ctx.fillStyle = style;
    }
  }

  function fillEllipseRadialGradient(ctx, layer, width, height) {
    const gradient = fillGradientForLayer(layer);
    if (!gradient || gradient.type !== "radial") return false;
    ctx.save();
    ctx.translate(width / 2, height / 2);
    ctx.scale(width / 2, height / 2);
    const radial = ctx.createRadialGradient(0, 0, 0, 0, 0, 1);
    radial.addColorStop(0, rgbaColorValue(gradient.colorB, gradient.alphaB));
    radial.addColorStop(1, rgbaColorValue(gradient.colorA, gradient.alphaA));
    setCanvasFillStyle(ctx, radial);
    ctx.beginPath();
    ctx.arc(0, 0, 1, 0, Math.PI * 2);
    ctx.fill();
    ctx.restore();
    return true;
  }

  function isPngMaskGroupType(type) {
    return cleanText(type).toLowerCase() === "pngmaskgroup";
  }

  function isPngMaskGroupLayer(layer) {
    return Boolean(layer && layer.type === "pngMaskGroup");
  }

  function supportsRatioLock(layer) {
    return Boolean(layer && (layer.type === "image" || isShapeLayerType(layer.type)));
  }

  function layerTransformerScaleBehavior(layer) {
    if (isTextLayerType(layer?.type)) {
      return { keepRatio: true, shiftBehavior: "inverted" };
    }
    return {
      keepRatio: supportsRatioLock(layer)
        ? layer.ratioLocked !== false
        : !isShapeLayerType(layer?.type),
      shiftBehavior: "default",
    };
  }

  function normalizeShapeLayer(raw, index, textureSize, type) {
    const width = intValue(raw?.width, 360, 1);
    const height = intValue(raw?.height, 360, 1);
    return {
      id: cleanText(raw?.id, `${type}_${String(index + 1).padStart(3, "0")}`),
      name: cleanText(raw?.name, type === "ellipse" ? `Ellipse ${index + 1}` : `Rectangle ${index + 1}`),
      type,
      x: coordinateValue(raw?.x, Math.max(0, Math.round((textureSize.width - width) / 2))),
      y: coordinateValue(raw?.y, Math.max(0, Math.round((textureSize.height - height) / 2))),
      width,
      height,
      rotation: numberValue(raw?.rotation, 0),
      fill: cleanText(raw?.fill, "#e43d30"),
      fillGradient: normalizeFillGradient(raw?.fillGradient, cleanText(raw?.fill, "#e43d30")),
      opacity: numberValue(raw?.opacity, 1, 0, 1),
      ratioLocked: raw?.ratioLocked !== false,
      visible: raw?.visible !== false,
      locked: Boolean(raw?.locked),
      order: intValue(raw?.order, index, 0),
    };
  }

  function normalizeTextLayer(raw, index, textureSize) {
    const fontPreset = fontPresetByFamily(raw?.fontFamily);
    const fontSize = intValue(raw?.fontSize, 96, 4);
    const text = layerTextValue(raw?.text, "NEW TEXT");
    const metrics = textLayerMetrics({
      text,
      fontFamily: cleanText(fontPreset.family, "Arial"),
      fontWeight: fontWeightValue(raw?.fontWeight, 700),
      fontSize,
    });
    const width = intValue(raw?.width, metrics.width, 1);
    const height = intValue(raw?.height, metrics.height, 1);
    return {
      id: cleanText(raw?.id, `text_${String(index + 1).padStart(3, "0")}`),
      name: textLayerName(text, `Text ${index + 1}`),
      type: "text",
      text,
      fontSourceType: "app",
      fontFamily: cleanText(fontPreset.family, "Arial"),
      fontWeight: fontWeightValue(raw?.fontWeight, 700),
      fontSize,
      x: coordinateValue(raw?.x, Math.max(0, Math.round((textureSize.width - width) / 2))),
      y: coordinateValue(raw?.y, Math.max(0, Math.round(textureSize.height / 2 - fontSize))),
      width,
      height,
      rotation: numberValue(raw?.rotation, 0),
      fill: cleanText(raw?.fill, "#ffffff"),
      opacity: numberValue(raw?.opacity, 1, 0, 1),
      visible: raw?.visible !== false,
      locked: Boolean(raw?.locked),
      order: intValue(raw?.order, index, 0),
    };
  }

  function normalizeRenderableLayer(raw, index, textureSize) {
    const type = cleanText(raw?.type, "image").toLowerCase();
    if (type === "polygon") return normalizePolygonLayer(raw, index, textureSize);
    if (type === "stripe") return normalizeStripeLayer(raw, index, textureSize);
    if (isShapeLayerType(type)) return normalizeShapeLayer(raw, index, textureSize, type);
    if (isTextLayerType(type)) return normalizeTextLayer(raw, index, textureSize);
    return normalizeImageLayer(raw, index, textureSize);
  }

  function normalizeGroupLayer(raw, index, textureSize, seen) {
    const children = (Array.isArray(raw?.children) ? raw.children : [])
      .filter((layer) => {
        const type = cleanText(layer?.type, "image").toLowerCase();
        return layer && typeof layer === "object" && (type === "image" || isPointLayerType(type) || isShapeLayerType(type) || isTextLayerType(type));
      })
      .map((layer, childIndex) => normalizeRenderableLayer(layer, childIndex, textureSize));
    const group = {
      id: cleanText(raw?.id, `group_${String(index + 1).padStart(3, "0")}`),
      name: cleanText(raw?.name, `Group ${index + 1}`),
      type: "group",
      visible: raw?.visible !== false,
      locked: Boolean(raw?.locked),
      collapsed: Boolean(raw?.collapsed),
      order: intValue(raw?.order, index, 0),
      children,
    };
    const originalGroupId = group.id;
    let groupSuffix = 2;
    while (seen.has(group.id)) {
      group.id = `${originalGroupId}_${groupSuffix}`;
      groupSuffix += 1;
    }
    seen.add(group.id);
    group.children = group.children.map((child, childIndex) => {
      const originalId = child.id;
      let suffix = 2;
      while (seen.has(child.id)) {
        child.id = `${originalId}_${suffix}`;
        suffix += 1;
      }
      seen.add(child.id);
      return { ...child, order: childIndex, groupId: group.id };
    });
    return group;
  }

  function uniqueLayerIdForSeen(id, seen) {
    const base = cleanText(id, "layer");
    let next = base;
    let suffix = 2;
    while (seen.has(next)) {
      next = `${base}_${suffix}`;
      suffix += 1;
    }
    seen.add(next);
    return next;
  }

  function normalizePngMaskGroupLayer(raw, index, textureSize, seen) {
    const groupId = uniqueLayerIdForSeen(cleanText(raw?.id, `mask_${String(index + 1).padStart(3, "0")}`), seen);
    const rawChildren = Array.isArray(raw?.children) ? raw.children : [];
    const maskSource = raw?.mask && typeof raw.mask === "object"
      ? raw.mask
      : rawChildren.find((child) => cleanText(child?.type).toLowerCase() === "polygon") || {};
    const childSource = raw?.child && typeof raw.child === "object"
      ? raw.child
      : rawChildren.find((child) => cleanText(child?.type).toLowerCase() === "image") || {};
    const mask = normalizePolygonLayer(maskSource, 0, textureSize);
    const child = normalizeImageLayer(childSource, 1, textureSize);
    mask.id = uniqueLayerIdForSeen(mask.id, seen);
    child.id = uniqueLayerIdForSeen(child.id, seen);
    return {
      id: groupId,
      name: cleanText(raw?.name, `PNG Mask ${index + 1}`),
      type: "pngMaskGroup",
      visible: raw?.visible !== false,
      locked: Boolean(raw?.locked),
      collapsed: Boolean(raw?.collapsed),
      order: intValue(raw?.order, index, 0),
      mask: { ...mask, order: 0, groupId, maskRole: "mask" },
      child: { ...child, order: 1, groupId, maskRole: "image" },
    };
  }

  function normalizeLayerDocument(raw, textureSize = state.textureSize) {
    const source = raw && typeof raw === "object" ? raw : {};
    const textureKey = surfaceKey(source.texture || state.activeSurfaceKey);
    const fallback = defaultLayerDocument(textureSize, textureKey);
    const size = source.textureSize && typeof source.textureSize === "object" ? source.textureSize : fallback.textureSize;
    const normalizedSize = {
      width: intValue(size.width, fallback.textureSize.width, 1),
      height: intValue(size.height, fallback.textureSize.height, 1),
    };
    const seen = new Set();
    const seenGuides = new Set();
    const guides = (Array.isArray(source.guides) ? source.guides : [])
      .filter((guide) => guide && typeof guide === "object")
      .map((guide, index) => {
        const normalized = normalizeGuide(guide, index, normalizedSize);
        const originalId = normalized.id;
        let suffix = 2;
        while (seenGuides.has(normalized.id)) {
          normalized.id = `${originalId}_${suffix}`;
          suffix += 1;
        }
        seenGuides.add(normalized.id);
        return normalized;
      });
    const layers = (Array.isArray(source.layers) ? source.layers : [])
      .filter((layer) => {
        const type = cleanText(layer?.type, "image").toLowerCase();
        return layer && typeof layer === "object" && (type === "image" || isPointLayerType(type) || isShapeLayerType(type) || isTextLayerType(type) || type === "group" || isPngMaskGroupType(type));
      })
      .map((layer, index) => {
        const type = cleanText(layer?.type, "image").toLowerCase();
        const normalized = isPngMaskGroupType(type)
          ? normalizePngMaskGroupLayer(layer, index, normalizedSize, seen)
          : type === "group"
          ? normalizeGroupLayer(layer, index, normalizedSize, seen)
          : normalizeRenderableLayer(layer, index, normalizedSize);
        if (type === "group" || isPngMaskGroupType(type)) return normalized;
        const originalId = normalized.id;
        let suffix = 2;
        while (seen.has(normalized.id)) {
          normalized.id = `${originalId}_${suffix}`;
          suffix += 1;
        }
        seen.add(normalized.id);
        return normalized;
      })
      .sort((left, right) => (left.order - right.order) || left.id.localeCompare(right.id))
      .map((layer, index) => ({ ...layer, order: index }));
    return {
      version: 1,
      texture: textureKey,
      textureSize: normalizedSize,
      baseTexture: cleanText(source.baseTexture, fallback.baseTexture),
      baseColor: normalizeBaseColor(source.baseColor || fallback.baseColor, textureKey),
      guides,
      guidesVisible: source.guidesVisible !== false,
      snapEnabled: source.snapEnabled !== false,
      layers,
    };
  }

  function serializeLayerDocument() {
    return normalizeLayerDocument(cloneJson(state.layerDocument), state.textureSize);
  }

  function guideList() {
    return Array.isArray(state.layerDocument?.guides) ? state.layerDocument.guides : [];
  }

  function layerList() {
    return Array.isArray(state.layerDocument?.layers) ? state.layerDocument.layers : [];
  }

  function baseColorState(documentModel = state.layerDocument) {
    return normalizeBaseColor(documentModel?.baseColor || {});
  }

  function setCanvasStrokeStyle(ctx, style) {
    if (typeof ctx?.setAttr === "function") {
      ctx.setAttr("strokeStyle", style);
    } else {
      ctx.strokeStyle = style;
    }
  }

  function usesOriginalGameLiveryStartPoint() {
    const startPoint = state.project?.liveryStartPoint || state.context?.liveryStartPoint || {};
    const mode = cleanText(startPoint.mode || startPoint.startPointId || startPoint.id).toLowerCase();
    return mode === ORIGINAL_GAME_LIVERY_START_POINT_ID;
  }

  function hasOriginalBaseTextureLayer() {
    const surface = activeTextureSurface();
    const base = surface?.baseTexture || state.context?.baseTexture || {};
    const hasBaseTexture = Boolean(cleanText(base.path || baseTexturePath()) && base.exists !== false);
    if (!hasBaseTexture) return false;
    if (usesOriginalGameLiveryStartPoint()) return true;
    return baseColorState().autoHiddenForOriginalBaseTexture === true;
  }

  function originalBaseTextureLayerName() {
    return isSplatMapSurface() ? "AC EVO DEFAULT MASK" : "AC EVO DEFAULT TEXTURE";
  }

  function flatLayerList(options = {}) {
    return flatLayerListForDocument(state.layerDocument, options);
  }

  function flatLayerListForDocument(documentModel = state.layerDocument, options = {}) {
    const includeHiddenChildren = options.includeHiddenChildren !== false;
    const rows = [];
    const layers = Array.isArray(documentModel?.layers) ? documentModel.layers : [];
    layers.forEach((layer) => {
      if (isPngMaskGroupLayer(layer)) {
        if (options.includeMaskChildren === true) {
          const groupVisible = layer.visible !== false;
          const groupLocked = Boolean(layer.locked);
          [layer.mask, layer.child].filter(Boolean).forEach((child) => {
            if (!includeHiddenChildren && (!groupVisible || child.visible === false)) return;
            rows.push({
              ...child,
              groupId: layer.id,
              groupName: layer.name,
              maskGroupId: layer.id,
              maskRole: child.maskRole || (child.type === "polygon" ? "mask" : "image"),
              groupVisible,
              groupLocked,
              effectiveVisible: groupVisible && child.visible !== false,
              effectiveLocked: groupLocked || Boolean(child.locked),
            });
          });
        } else {
          rows.push(layer);
        }
        return;
      }
      if (layer.type !== "group") {
        rows.push(layer);
        return;
      }
      const groupVisible = layer.visible !== false;
      const groupLocked = Boolean(layer.locked);
      (Array.isArray(layer.children) ? layer.children : []).forEach((child) => {
        if (!includeHiddenChildren && (!groupVisible || child.visible === false)) return;
        rows.push({
          ...child,
          groupId: layer.id,
          groupName: layer.name,
          groupVisible,
          groupLocked,
          effectiveVisible: groupVisible && child.visible !== false,
          effectiveLocked: groupLocked || Boolean(child.locked),
        });
      });
    });
    return rows;
  }

  function findLayer(layerId = state.selectedLayerId) {
    const id = cleanText(layerId);
    if (!id) return null;
    for (const layer of layerList()) {
      if (layer.id === id) return layer;
      if (isPngMaskGroupLayer(layer)) {
        const child = [layer.mask, layer.child].filter(Boolean).find((row) => row.id === id);
        if (child) return {
          ...child,
          groupId: layer.id,
          groupName: layer.name,
          maskGroupId: layer.id,
          maskRole: child.maskRole || (child.type === "polygon" ? "mask" : "image"),
          groupVisible: layer.visible !== false,
          groupLocked: Boolean(layer.locked),
          effectiveVisible: layer.visible !== false && child.visible !== false,
          effectiveLocked: Boolean(layer.locked) || Boolean(child.locked),
        };
      }
      if (layer.type === "group") {
        const child = (layer.children || []).find((row) => row.id === id);
        if (child) return {
          ...child,
          groupId: layer.id,
          groupName: layer.name,
          groupVisible: layer.visible !== false,
          groupLocked: Boolean(layer.locked),
          effectiveVisible: layer.visible !== false && child.visible !== false,
          effectiveLocked: Boolean(layer.locked) || Boolean(child.locked),
        };
      }
    }
    return null;
  }

  function findGroup(groupId) {
    const id = cleanText(groupId);
    if (!id) return null;
    return layerList().find((layer) => layer.type === "group" && layer.id === id) || null;
  }

  function findPngMaskGroup(groupId) {
    const id = cleanText(groupId);
    if (!id) return null;
    return layerList().find((layer) => isPngMaskGroupLayer(layer) && layer.id === id) || null;
  }

  function updateLayerInDocument(layerId, updater) {
    const id = cleanText(layerId);
    let updated = null;
    state.layerDocument.layers = layerList().map((layer) => {
      if (layer.id === id) {
        updated = updater(layer);
        return updated || layer;
      }
      if (isPngMaskGroupLayer(layer)) {
        if (layer.mask?.id === id) {
          updated = updater(layer.mask, layer);
          return updated ? { ...layer, mask: { ...updated, order: 0, groupId: layer.id, maskRole: "mask" } } : layer;
        }
        if (layer.child?.id === id) {
          updated = updater(layer.child, layer);
          return updated ? { ...layer, child: { ...updated, order: 1, groupId: layer.id, maskRole: "image" } } : layer;
        }
        return layer;
      }
      if (layer.type !== "group") return layer;
      const children = (layer.children || []).map((child) => {
        if (child.id !== id) return child;
        updated = updater(child, layer);
        return updated || child;
      });
      return { ...layer, children };
    });
    return updated;
  }

  function removeLayersFromDocument(ids) {
    const removeSet = new Set((ids || []).map(cleanText).filter(Boolean));
    if (!removeSet.size) return false;
    let changed = false;
    state.layerDocument.layers = layerList()
      .filter((layer) => {
        const maskChildRemoved = isPngMaskGroupLayer(layer) && (removeSet.has(layer.mask?.id) || removeSet.has(layer.child?.id));
        const keep = !removeSet.has(layer.id) && !maskChildRemoved;
        if (!keep) changed = true;
        return keep;
      })
      .map((layer) => {
        if (layer.type !== "group") return layer;
        const children = (layer.children || []).filter((child) => {
          const keep = !removeSet.has(child.id);
          if (!keep) changed = true;
          return keep;
        });
        return { ...layer, children };
      });
    return changed;
  }

  function selectedLayerIds() {
    const seen = new Set();
    const ids = [];
    for (const id of state.selectedLayerIds || []) {
      const clean = cleanText(id);
      if (!clean || seen.has(clean) || !findLayer(clean)) continue;
      seen.add(clean);
      ids.push(clean);
    }
    if (!ids.length && findLayer(state.selectedLayerId)) ids.push(state.selectedLayerId);
    return ids;
  }

  function selectedLayers() {
    return selectedLayerIds()
      .map((id) => findLayer(id))
      .filter(Boolean);
  }

  function primarySelectedLayer() {
    return findLayer(state.selectedLayerId) || selectedLayers()[0] || null;
  }

  function isLayerSelected(layerId) {
    return selectedLayerIds().includes(cleanText(layerId));
  }

  function syncSelectionState(fallbackId = "") {
    const ids = selectedLayerIds();
    if (!ids.length && fallbackId && findLayer(fallbackId)) ids.push(fallbackId);
    state.selectedLayerIds = ids;
    state.selectedLayerId = ids.includes(state.selectedLayerId) ? state.selectedLayerId : ids[ids.length - 1] || "";
  }

  function layerNode(layerId = state.selectedLayerId) {
    const id = cleanText(layerId);
    if (!id) return null;
    return state.konva?.objectLayer?.findOne?.(`#${id}`) || null;
  }

  function findGuide(guideId = state.selectedGuideId) {
    const id = cleanText(guideId);
    if (!id) return null;
    return guideList().find((guide) => guide.id === id) || null;
  }

  function guidePositionLimit(axis) {
    return axis === "y" ? state.textureSize.height : state.textureSize.width;
  }

  function clampGuidePosition(axis, position) {
    return Math.max(0, Math.min(guidePositionLimit(axis), Math.round(numberValue(position, 0))));
  }

  function nextGuideId(axis) {
    const prefix = axis === "y" ? "guide_h" : "guide_v";
    const existing = new Set(guideList().map((guide) => guide.id));
    let index = guideList().length + 1;
    let id = `${prefix}_${String(index).padStart(3, "0")}`;
    while (existing.has(id)) {
      index += 1;
      id = `${prefix}_${String(index).padStart(3, "0")}`;
    }
    return id;
  }

  function syncGuideControls() {
    const documentModel = state.layerDocument || {};
    const visible = documentModel.guidesVisible !== false;
    const snap = documentModel.snapEnabled !== false;
    const count = guideList().length;
    const visibleButton = $("layerStudioToggleGuides");
    const snapButton = $("layerStudioToggleSnap");
    const clearButton = $("layerStudioClearGuides");
    if (visibleButton) {
      visibleButton.classList.toggle("is-active", visible);
      visibleButton.setAttribute("aria-pressed", visible ? "true" : "false");
      visibleButton.textContent = visible ? "Guides On" : "Guides Off";
    }
    if (snapButton) {
      snapButton.classList.toggle("is-active", snap);
      snapButton.setAttribute("aria-pressed", snap ? "true" : "false");
      snapButton.textContent = snap ? "Snap On" : "Snap Off";
    }
    if (clearButton) clearButton.disabled = count === 0;
    const selected = selectedLayers();
    const groupButton = $("layerStudioCreateGroup");
    const ungroupButton = $("layerStudioUngroupSelected");
    if (groupButton) groupButton.disabled = !canCreateGroupFromSelection();
    if (ungroupButton) ungroupButton.disabled = !canUngroupSelection();
    const canFlip = canFlipSelection();
    $("layerStudioFlipHorizontal")?.toggleAttribute("disabled", !canFlip);
    $("layerStudioFlipVertical")?.toggleAttribute("disabled", !canFlip);
    const alignEnabled = selected.filter((layer) => layer.visible !== false && layer.effectiveVisible !== false).length >= 2;
    document.querySelectorAll("[data-layer-align]").forEach((button) => {
      button.disabled = !alignEnabled;
    });
    setText("layerStudioGuideSummary", count ? `${count} guide${count === 1 ? "" : "s"}` : "No guides");
  }

  function updateGuide(guideId, patch, options = {}) {
    const id = cleanText(guideId);
    const guides = guideList();
    const index = guides.findIndex((guide) => guide.id === id);
    if (index < 0) return null;
    const current = guides[index];
    const axis = patch.axis ? (cleanText(patch.axis).toLowerCase() === "y" ? "y" : "x") : current.axis;
    const next = {
      ...current,
      ...patch,
      axis,
      position: clampGuidePosition(axis, patch.position ?? current.position),
    };
    state.layerDocument.guides = guides.map((guide) => (guide.id === id ? next : guide));
    state.selectedGuideId = next.id;
    if (options.render !== false) renderGuides();
    syncGuideControls();
    if (options.dirty !== false) setDirty(true);
    return next;
  }

  function addGuide(axis) {
    const cleanAxis = axis === "y" ? "y" : "x";
    if (!state.layerDocument) state.layerDocument = defaultLayerDocument(state.textureSize);
    pushHistory(cleanAxis === "x" ? "Add vertical guide" : "Add horizontal guide");
    const guide = normalizeGuide({
      id: nextGuideId(cleanAxis),
      axis: cleanAxis,
      position: Math.round(guidePositionLimit(cleanAxis) / 2),
    }, guideList().length, state.textureSize);
    state.layerDocument.guides = [...guideList(), guide];
    state.layerDocument.guidesVisible = true;
    state.selectedGuideId = guide.id;
    renderGuides();
    syncGuideControls();
    setDirty(true);
    commitHistory();
    setText("layerStudioEditorStatus", `${cleanAxis === "x" ? "Vertical" : "Horizontal"} guide added at ${guide.position}px.`);
    return guide;
  }

  function removeSelectedGuide() {
    const guide = findGuide();
    if (!guide) return false;
    pushHistory("Delete guide");
    state.layerDocument.guides = guideList().filter((row) => row.id !== guide.id);
    state.selectedGuideId = "";
    renderGuides();
    syncGuideControls();
    setDirty(true);
    commitHistory();
    setText("layerStudioEditorStatus", "Guide removed.");
    return true;
  }

  function clearGuides() {
    if (!guideList().length) return false;
    pushHistory("Clear guides");
    state.layerDocument.guides = [];
    state.selectedGuideId = "";
    renderGuides();
    syncGuideControls();
    setDirty(true);
    commitHistory();
    setText("layerStudioEditorStatus", "Guides cleared.");
    return true;
  }

  function setGuidesVisible(visible) {
    if (!state.layerDocument) return;
    pushHistory("Toggle guides");
    state.layerDocument.guidesVisible = Boolean(visible);
    renderGuides();
    syncGuideControls();
    setDirty(true);
  }

  function setSnapEnabled(enabled) {
    if (!state.layerDocument) return;
    pushHistory("Toggle snapping");
    state.layerDocument.snapEnabled = Boolean(enabled);
    syncGuideControls();
    setDirty(true);
  }

  function renderBaseColor() {
    const rect = state.konva?.baseColorRect;
    if (!rect) return;
    const baseColor = baseColorState();
    rect.fill(baseColor.fill);
    rect.visible(baseColor.visible);
    rect.size({ width: state.textureSize.width, height: state.textureSize.height });
    rect.getLayer()?.batchDraw?.();
  }

  function patchBaseColor(patch, options = {}) {
    if (!state.layerDocument) return false;
    state.layerDocument.baseColor = normalizeBaseColor({
      ...baseColorState(),
      ...(patch || {}),
    }, state.activeSurfaceKey);
    renderBaseColor();
    renderLayerList();
    if (options.dirty !== false) setDirty(true);
    return true;
  }

  function shouldAutoHideSplatMapBaseColor() {
    if (!isSplatMapSurface()) return false;
    const baseColor = baseColorState();
    if (baseColor.autoHiddenForNonFallbackSplatMap || baseColor.visible === false) return false;
    const baseTexture = cleanText(baseTexturePath());
    const activeTexture = cleanText(texturePath());
    return Boolean(baseTexture || activeTexture);
  }

  function shouldAutoHideOriginalBaseTextureBaseColor() {
    if (!hasOriginalBaseTextureLayer()) return false;
    const baseColor = baseColorState();
    if (baseColor.autoHiddenForOriginalBaseTexture || baseColor.visible === false) return false;
    return true;
  }

  function shouldAutoHideActiveTextureBaseColor() {
    if (surfaceKey(state.activeSurfaceKey) !== "ext_banner_c") return false;
    const baseColor = baseColorState();
    if (baseColor.autoHiddenForActiveTexture || baseColor.visible === false) return false;
    return Boolean(cleanText(texturePath()));
  }

  function applySplatMapBaseColorAutoHide() {
    if (!state.layerDocument || !shouldAutoHideSplatMapBaseColor()) return false;
    state.layerDocument.baseColor = normalizeBaseColor({
      ...baseColorState(),
      visible: false,
      autoHiddenForNonFallbackSplatMap: true,
    }, state.activeSurfaceKey);
    renderBaseColor();
    renderLayerList();
    setDirty(true);
    return true;
  }

  function applyOriginalBaseTextureBaseColorAutoHide() {
    if (!state.layerDocument || !shouldAutoHideOriginalBaseTextureBaseColor()) return false;
    state.layerDocument.baseColor = normalizeBaseColor({
      ...baseColorState(),
      visible: false,
      autoHiddenForOriginalBaseTexture: true,
    }, state.activeSurfaceKey);
    renderBaseColor();
    renderLayerList();
    setDirty(true);
    return true;
  }

  function applyActiveTextureBaseColorAutoHide() {
    if (!state.layerDocument || !shouldAutoHideActiveTextureBaseColor()) return false;
    state.layerDocument.baseColor = normalizeBaseColor({
      ...baseColorState(),
      visible: false,
      autoHiddenForActiveTexture: true,
    }, state.activeSurfaceKey);
    renderBaseColor();
    renderLayerList();
    setDirty(true);
    return true;
  }

  function applyBaseColorAutoHideRules() {
    return applySplatMapBaseColorAutoHide() || applyOriginalBaseTextureBaseColorAutoHide() || applyActiveTextureBaseColorAutoHide();
  }

  function openAnchoredColorPicker(initialValue, anchorEl, onColor) {
    const input = document.createElement("input");
    input.type = "color";
    input.value = cssColorValue(initialValue, BASE_COLOR_DEFAULT).slice(0, 7);
    const anchorRect = anchorEl?.getBoundingClientRect?.();
    const inputWidth = anchorRect ? Math.max(36, Math.round(anchorRect.width)) : 36;
    const inputHeight = 30;
    input.style.position = "fixed";
    input.style.left = anchorRect ? `${Math.max(8, Math.round(anchorRect.left + (anchorRect.width - inputWidth) / 2))}px` : "8px";
    input.style.top = anchorRect ? `${Math.max(8, Math.round(anchorRect.top - inputHeight - 6))}px` : "8px";
    input.style.width = `${inputWidth}px`;
    input.style.height = `${inputHeight}px`;
    input.style.padding = "0";
    input.style.border = "1px solid rgba(255, 255, 255, 0.75)";
    input.style.background = "transparent";
    input.style.boxShadow = "0 8px 20px rgba(0, 0, 0, 0.35)";
    input.style.zIndex = "9999";
    document.body.appendChild(input);
    const apply = () => {
      onColor?.(input.value);
    };
    input.addEventListener("input", apply);
    input.addEventListener("change", apply);
    input.addEventListener("blur", () => {
      window.setTimeout(() => input.remove(), 0);
    }, { once: true });
    input.focus({ preventScroll: true });
    if (typeof input.showPicker === "function") {
      try {
        input.showPicker();
      } catch (_error) {
        input.click();
      }
    } else {
      input.click();
    }
    return true;
  }

  function chooseBaseColor(anchorEl = null) {
    if (!state.layerDocument) return false;
    if (isSplatMapSurface()) {
      if (baseColorState().fill !== SPLAT_MAP_BASE_COLOR_DEFAULT || baseColorState().visible !== true) {
        pushHistory("Reset splat map base");
        patchBaseColor({ fill: SPLAT_MAP_BASE_COLOR_DEFAULT, visible: true }, { dirty: true });
      }
      return true;
    }
    let historyStarted = false;
    return openAnchoredColorPicker(baseColorState().fill, anchorEl, (color) => {
      if (!historyStarted) {
        pushHistory("Change base color");
        historyStarted = true;
      }
      patchBaseColor({ fill: color }, { dirty: true });
    });
  }

  function chooseLayerSwatchColor(layerId, anchorEl = null) {
    const layer = findLayer(layerId);
    if (!layer || layer.locked || layer.maskRole === "mask" || (!isPointLayerType(layer.type) && !isShapeLayerType(layer.type))) return false;
    if (isSplatMapSurface()) {
      pushHistory(layer.type === "stripe" ? "Change stripe mask color" : "Change mask fill color");
      const color = activeSplatMapColor();
      patchLayer(layer.id, layer.type === "stripe" ? { stroke: color, strokeGradient: null } : { fill: color, fillGradient: null }, { renderList: true });
      commitHistory();
      return true;
    }
    let historyStarted = false;
    const initialValue = layer.type === "stripe" ? layer.stroke || "#f07a00" : layer.fill || "#e43d30";
    return openAnchoredColorPicker(initialValue, anchorEl, (color) => {
      const current = findLayer(layerId);
      if (!current || current.locked || current.maskRole === "mask" || (!isPointLayerType(current.type) && !isShapeLayerType(current.type))) return;
      if (!historyStarted) {
        pushHistory(current.type === "stripe" ? "Change stripe color" : "Change fill color");
        historyStarted = true;
      }
      patchLayer(current.id, current.type === "stripe" ? { stroke: color } : { fill: color }, { renderList: true });
    });
  }

  function renderSplatMapPalette(layer = primarySelectedLayer()) {
    const palette = $("layerStudioSplatPalette");
    if (!palette) return;
    const visible = isSplatMapSurface() && layer && !layer.locked && (isPointLayerType(layer.type) || isShapeLayerType(layer.type));
    palette.hidden = !visible;
    if (!visible) {
      palette.replaceChildren();
      return;
    }
    const layerColor = normalizeSplatMapColor(layer.type === "stripe" ? layer.stroke : layer.fill, activeSplatMapColor());
    state.splatMapColor = layerColor;
    palette.replaceChildren(...SPLAT_MAP_PALETTE.map((item) => {
      const button = document.createElement("button");
      button.type = "button";
      button.dataset.splatColor = item.color;
      button.title = `${item.label}: ${item.description}`;
      button.setAttribute("aria-label", `${item.label}: ${item.description}`);
      button.style.setProperty("--layer-studio-splat-color", item.color);
      button.classList.toggle("is-active", item.color === layerColor);
      return button;
    }));
  }

  function applySplatMapPaletteColor(color) {
    const layer = primarySelectedLayer();
    if (!isSplatMapSurface() || !layer || layer.locked || (!isPointLayerType(layer.type) && !isShapeLayerType(layer.type))) return false;
    const nextColor = normalizeSplatMapColor(color);
    state.splatMapColor = nextColor;
    beginHistory("Change splat map color");
    const updated = patchLayer(layer.id, layer.type === "stripe" ? { stroke: nextColor, strokeGradient: null } : { fill: nextColor, fillGradient: null });
    if (updated) {
      setDirty(true);
      renderSplatMapPalette(updated);
    }
    commitHistory();
    return Boolean(updated);
  }

  function polygonBounds(points) {
    const values = Array.isArray(points) ? points : [];
    const xs = [];
    const ys = [];
    for (let index = 0; index < values.length - 1; index += 2) {
      xs.push(Number(values[index]) || 0);
      ys.push(Number(values[index + 1]) || 0);
    }
    if (!xs.length || !ys.length) return { x: 0, y: 0, width: 1, height: 1 };
    const minX = Math.min(...xs);
    const minY = Math.min(...ys);
    const maxX = Math.max(...xs);
    const maxY = Math.max(...ys);
    return {
      x: minX,
      y: minY,
      width: Math.max(1, maxX - minX),
      height: Math.max(1, maxY - minY),
    };
  }

  function visibleLayerRows() {
    const rows = [];
    if (uvOverlayPath(state.context, null, "parts")) {
      rows.push({
        id: UV_OVERLAY_PARTS_ID,
        name: "UV PARTS OVERLAY",
        virtual: true,
        compactOverlay: true,
        visible: state.partsUvOverlayVisible,
        inverted: state.partsUvOverlayInverted,
        locked: true,
        order: Number.MAX_SAFE_INTEGER,
      });
    }
    if (uvOverlayPath(state.context, null, "base")) {
      rows.push({
        id: UV_OVERLAY_BASE_ID,
        name: "UV BASE OVERLAY",
        virtual: true,
        compactOverlay: true,
        visible: state.uvOverlayVisible,
        inverted: state.uvOverlayInverted,
        locked: true,
        order: Number.MAX_SAFE_INTEGER - 1,
      });
    }
    layerList().slice().sort((left, right) => right.order - left.order).forEach((layer) => {
      rows.push(layer);
      if (isPngMaskGroupLayer(layer) && !layer.collapsed) {
        const mask = layer.mask ? {
          ...layer.mask,
          groupId: layer.id,
          groupName: layer.name,
          maskGroupId: layer.id,
          maskRole: "mask",
          groupVisible: layer.visible !== false,
          groupLocked: Boolean(layer.locked),
          effectiveVisible: layer.visible !== false && layer.mask.visible !== false,
          effectiveLocked: Boolean(layer.locked) || Boolean(layer.mask.locked),
          childRow: true,
        } : null;
        const child = layer.child ? {
          ...layer.child,
          groupId: layer.id,
          groupName: layer.name,
          maskGroupId: layer.id,
          maskRole: "image",
          groupVisible: layer.visible !== false,
          groupLocked: Boolean(layer.locked),
          effectiveVisible: layer.visible !== false && layer.child.visible !== false,
          effectiveLocked: Boolean(layer.locked) || Boolean(layer.child.locked),
          childRow: true,
        } : null;
        rows.push(...[mask, child].filter(Boolean));
      } else if (layer.type === "group" && !layer.collapsed) {
        rows.push(...(layer.children || []).slice().sort((left, right) => right.order - left.order).map((child) => ({
          ...child,
          groupId: layer.id,
          groupName: layer.name,
          groupVisible: layer.visible !== false,
          groupLocked: Boolean(layer.locked),
          effectiveVisible: layer.visible !== false && child.visible !== false,
          effectiveLocked: Boolean(layer.locked) || Boolean(child.locked),
          childRow: true,
        })));
      }
    });
    const baseColor = baseColorState();
    rows.push({
      id: BASE_COLOR_LAYER_ID,
      name: "base color",
      source: "Full texture background",
      virtual: true,
      systemLayer: "baseColor",
      visible: baseColor.visible,
      fill: baseColor.fill,
      locked: true,
      order: Number.MIN_SAFE_INTEGER,
    });
    if (hasOriginalBaseTextureLayer()) {
      rows.push({
        id: ORIGINAL_BASE_TEXTURE_LAYER_ID,
        name: originalBaseTextureLayerName(),
        source: "Original start point texture",
        virtual: true,
        systemLayer: "baseTexture",
        visible: true,
        locked: true,
        order: Number.MIN_SAFE_INTEGER - 1,
      });
    }
    return rows;
  }

  function isLayerCanvasSelectable(layer) {
    return Boolean(layer && layer.visible !== false && layer.effectiveVisible !== false && !layer.locked && !layer.effectiveLocked && (isPngMaskGroupLayer(layer) || layer.source || isPointLayerType(layer.type) || isShapeLayerType(layer.type) || isTextLayerType(layer.type)));
  }

  function isLayerCanvasDraggable(layer) {
    if (!isLayerCanvasSelectable(layer)) return false;
    return !(isPointLayerType(layer.type) && state.polygonEditLayerId === layer.id);
  }

  function clampLayerPatch(layer, patch) {
    if (isPngMaskGroupLayer(layer)) {
      const next = { ...layer, ...patch };
      next.name = cleanText(next.name, layer.name || layer.id || "PNG Mask");
      next.visible = next.visible !== false;
      next.locked = Boolean(next.locked);
      next.collapsed = Boolean(next.collapsed);
      next.mask = layer.mask;
      next.child = layer.child;
      return next;
    }
    if (layer?.type === "group") {
      const next = { ...layer, ...patch };
      next.name = cleanText(next.name, layer.name || layer.id || "Layer Group");
      next.visible = next.visible !== false;
      next.locked = Boolean(next.locked);
      next.collapsed = Boolean(next.collapsed);
      next.children = Array.isArray(layer.children) ? layer.children : [];
      return next;
    }
    if (isPointLayerType(layer?.type)) {
      const next = { ...layer, ...patch };
      const bounds = polygonBounds(layer.points);
      if (patch.x !== undefined || patch.y !== undefined) {
        const nextX = coordinateValue(patch.x ?? bounds.x, bounds.x);
        const nextY = coordinateValue(patch.y ?? bounds.y, bounds.y);
        const dx = nextX - bounds.x;
        const dy = nextY - bounds.y;
        next.points = layer.points.map((value, index) => coordinateValue(value) + (index % 2 === 0 ? dx : dy));
        if (isPointLayerType(layer.type)) {
          next.bezierHandles = moveBezierHandles(layer.bezierHandles, dx, dy);
        }
      }
      if (isPointLayerType(layer.type) && patch.points && !patch.bezierHandles) {
        const delta = uniformPointDelta(layer.points, next.points);
        if (delta) next.bezierHandles = moveBezierHandles(layer.bezierHandles, delta.dx, delta.dy);
      }
      next.name = cleanText(next.name, layer.name || layer.id || (layer.type === "stripe" ? "Stripe" : "Polygon"));
      if (next.type === "stripe") {
        const lineCap = cleanText(next.lineCap, layer.lineCap || "round").toLowerCase();
        const lineJoin = cleanText(next.lineJoin, layer.lineJoin || "round").toLowerCase();
        next.stroke = cleanText(next.stroke || next.fill, layer.stroke || "#f07a00");
        next.strokeGradient = normalizeFillGradient(next.strokeGradient, next.stroke);
        next.strokeWidth = intValue(next.strokeWidth, layer.strokeWidth || 20, 1);
        next.tension = numberValue(next.tension, layer.tension || 0, 0, 1);
        next.bezierHandles = normalizeStripeBezierHandles(next.bezierHandles, next.points, state.textureSize);
        next.lineCap = ["butt", "round", "square"].includes(lineCap) ? lineCap : "round";
        next.lineJoin = ["miter", "round", "bevel"].includes(lineJoin) ? lineJoin : "round";
      } else {
        next.fill = cleanText(next.fill, layer.fill || "#e43d30");
        next.stroke = next.stroke ?? null;
        next.strokeWidth = intValue(next.strokeWidth, layer.strokeWidth || 0, 0);
        next.bezierHandles = normalizePolygonBezierHandles(next.bezierHandles, next.points, state.textureSize);
      }
      next.opacity = numberValue(next.opacity, layer.opacity ?? 1, 0, 1);
      next.visible = next.visible !== false;
      next.locked = Boolean(next.locked);
      return next;
    }
    const next = { ...layer, ...patch };
    next.name = cleanText(next.name, layer.name || layer.id || "Layer");
    next.x = coordinateValue(next.x, layer.x ?? 0);
    next.y = coordinateValue(next.y, layer.y ?? 0);
    next.width = intValue(next.width, layer.width ?? 1, 1);
    next.height = intValue(next.height, layer.height ?? 1, 1);
    next.rotation = numberValue(next.rotation, layer.rotation ?? 0);
    if (next.type === "image") {
      next.flipX = Boolean(next.flipX);
      next.flipY = Boolean(next.flipY);
    }
    if (isShapeLayerType(next.type)) next.fill = cleanText(next.fill, layer.fill || "#e43d30");
    if (isTextLayerType(next.type)) {
      const fontPreset = fontPresetByFamily(next.fontFamily);
      next.text = layerTextValue(next.text, layer.text || "NEW TEXT");
      next.name = textLayerName(next.text);
      next.fontSourceType = "app";
      next.fontFamily = cleanText(fontPreset.family, "Arial");
      next.fontWeight = fontWeightValue(next.fontWeight, layer.fontWeight || 700);
      next.fontSize = intValue(next.fontSize, layer.fontSize || 96, 4);
      next.fill = cleanText(next.fill, layer.fill || "#ffffff");
      const typographyChanged = ["text", "fontFamily", "fontWeight", "fontSize"]
        .some((property) => Object.prototype.hasOwnProperty.call(patch, property));
      if (typographyChanged) {
        const metrics = textLayerMetrics(next);
        next.width = metrics.width;
        next.height = metrics.height;
      }
    }
    next.opacity = numberValue(next.opacity, layer.opacity ?? 1, 0, 1);
    next.visible = next.visible !== false;
    next.locked = Boolean(next.locked);
    return next;
  }

  function patchLayer(layerId, patch, options = {}) {
    const id = cleanText(layerId);
    if (!id || !state.layerDocument?.layers) return null;
    let updated = null;
    updateLayerInDocument(id, (layer) => {
      updated = clampLayerPatch(layer, patch);
      return updated;
    });
    state.layerDocument = normalizeLayerDocument(state.layerDocument, state.textureSize);
    updated = findLayer(id);
    if (!updated) {
      setSelectedLayer("");
      return null;
    }
    if (options.updateNode !== false) {
      if (updated.maskGroupId) {
        applyLayerToNode(findPngMaskGroup(updated.maskGroupId));
      } else {
        applyLayerToNode(updated);
      }
    }
    if (options.renderList !== false) renderLayerList();
    if (options.renderProperties !== false) renderPropertiesPanel();
    updateTransformerForSelection();
    if (options.renderEditHandles !== false && state.polygonEditLayerId === updated.id) {
      if (!isPointLayerType(updated.type) || updated.locked || updated.visible === false) {
        exitPolygonEditMode();
      } else {
        renderPolygonEditHandles();
      }
    }
    updateCanvasStatus();
    if (options.dirty !== false) setDirty(true);
    return updated;
  }

  function drawStripeLayerPath(ctx, layer) {
    drawStripePath(ctx, layer.points || [], numberValue(layer.tension, 0, 0, 1), layer.bezierHandles);
  }

  function stripeLayerStrokeStyle(ctx, layer) {
    const gradient = fillGradientForLayer(layer);
    if (!gradient) return cleanText(layer?.stroke, "#f07a00");
    const bounds = stripeBounds(layer);
    return shapeLayerFillStyle(ctx, layer, bounds.width, bounds.height, bounds.x, bounds.y);
  }

  function strokeStripeCanvasPath(ctx, layer, style) {
    setCanvasStrokeStyle(ctx, style);
    const attrs = {
      lineWidth: numberValue(layer?.strokeWidth, 20, 1),
      lineCap: cleanText(layer?.lineCap, "round"),
      lineJoin: cleanText(layer?.lineJoin, "round"),
    };
    if (typeof ctx?.setAttr === "function") {
      Object.entries(attrs).forEach(([key, value]) => ctx.setAttr(key, value));
    } else {
      Object.assign(ctx, attrs);
    }
    ctx.stroke();
  }

  function drawPolygonLayerPath(ctx, layer) {
    drawPolygonPath(ctx, layer.points || [], layer.bezierHandles);
  }

  function configurePolygonNode(node, layer) {
    const bounds = polygonBounds(layer.points || []);
    const gradient = fillGradientForLayer(layer);
    node.setAttrs({
      layerModel: cloneJson(layer),
      fill: layer.fill || "#e43d30",
      fillGradient: gradient,
      fillBounds: bounds,
      stroke: layer.stroke || undefined,
      strokeWidth: layer.strokeWidth || 0,
      opacity: layer.opacity,
      visible: layer.visible,
      draggable: isLayerCanvasDraggable(layer),
      listening: isLayerCanvasSelectable(layer),
    });
  }

  function createPolygonNode(modelLayer) {
    const node = new window.Konva.Shape({
      name: modelLayer.id,
      id: modelLayer.id,
      sceneFunc(context, shape) {
        const layer = shape.getAttr("layerModel") || {};
        const bounds = shape.getAttr("fillBounds") || polygonBounds(layer.points || []);
        context.beginPath();
        drawPolygonLayerPath(context, layer);
        if (shape.getAttr("fillGradient")) {
          setCanvasFillStyle(context, shapeLayerFillStyle(context, layer, Math.max(1, bounds.width || 1), Math.max(1, bounds.height || 1), bounds.x || 0, bounds.y || 0));
          context.fill();
          context.strokeShape(shape);
        } else {
          context.fillStrokeShape(shape);
        }
      },
      hitFunc(context, shape) {
        const layer = shape.getAttr("layerModel") || {};
        context.beginPath();
        drawPolygonLayerPath(context, layer);
        context.fillStrokeShape(shape);
      },
    });
    configurePolygonNode(node, modelLayer);
    return node;
  }

  function configureStripeNode(node, layer) {
    node.setAttrs({
      layerModel: cloneJson(layer),
      stroke: layer.stroke || "#f07a00",
      strokeGradient: fillGradientForLayer(layer),
      strokeWidth: layer.strokeWidth || 20,
      lineCap: layer.lineCap || "round",
      lineJoin: layer.lineJoin || "round",
      opacity: layer.opacity,
      visible: layer.visible,
      draggable: isLayerCanvasDraggable(layer),
      listening: isLayerCanvasSelectable(layer),
    });
  }

  function createStripeNode(modelLayer) {
    const node = new window.Konva.Shape({
      name: modelLayer.id,
      id: modelLayer.id,
      sceneFunc(context, shape) {
        const layer = shape.getAttr("layerModel") || {};
        context.beginPath();
        drawStripeLayerPath(context, layer);
        if (shape.getAttr("strokeGradient")) {
          strokeStripeCanvasPath(context, layer, stripeLayerStrokeStyle(context, layer));
        } else {
          context.strokeShape(shape);
        }
      },
      hitFunc(context, shape) {
        const layer = shape.getAttr("layerModel") || {};
        context.beginPath();
        drawStripeLayerPath(context, layer);
        context.lineWidth = Math.max(24, numberValue(layer.strokeWidth, 20, 1));
        context.strokeShape(shape);
      },
    });
    configureStripeNode(node, modelLayer);
    return node;
  }

  function configureShapeNode(node, layer) {
    const gradient = fillGradientForLayer(layer);
    node.setAttrs({
      layerModel: cloneJson(layer),
      x: layer.x,
      y: layer.y,
      width: layer.width,
      height: layer.height,
      rotation: layer.rotation,
      fill: layer.fill || "#e43d30",
      fillGradient: gradient,
      opacity: layer.opacity,
      visible: layer.visible,
      draggable: isLayerCanvasSelectable(layer),
      listening: isLayerCanvasSelectable(layer),
    });
  }

  function createShapeNode(modelLayer) {
    const node = new window.Konva.Shape({
      name: modelLayer.id,
      id: modelLayer.id,
      sceneFunc(ctx, shape) {
        const layer = shape.getAttr("layerModel") || {};
        const width = Math.max(1, shape.width());
        const height = Math.max(1, shape.height());
        ctx.beginPath();
        if (layer.type === "ellipse") {
          ctx.ellipse(width / 2, height / 2, width / 2, height / 2, 0, 0, Math.PI * 2);
        } else {
          ctx.rect(0, 0, width, height);
        }
        ctx.closePath();
        if (shape.getAttr("fillGradient")) {
          if (!fillEllipseRadialGradient(ctx, layer, width, height)) {
            setCanvasFillStyle(ctx, shapeLayerFillStyle(ctx, layer, width, height));
            ctx.fill();
          }
          ctx.strokeShape(shape);
        } else {
          ctx.fillStrokeShape(shape);
        }
      },
      hitFunc(ctx, shape) {
        const layer = shape.getAttr("layerModel") || {};
        const width = Math.max(1, shape.width());
        const height = Math.max(1, shape.height());
        ctx.beginPath();
        if (layer.type === "ellipse") {
          ctx.ellipse(width / 2, height / 2, width / 2, height / 2, 0, 0, Math.PI * 2);
        } else {
          ctx.rect(0, 0, width, height);
        }
        ctx.closePath();
        ctx.fillStrokeShape(shape);
      },
    });
    configureShapeNode(node, modelLayer);
    return node;
  }

  function configurePngMaskGroupNode(node, layer) {
    const mask = layer?.mask || {};
    node.setAttrs({
      layerModel: cloneJson(layer),
      visible: layer.visible !== false,
      draggable: isLayerCanvasSelectable(layer),
      listening: isLayerCanvasSelectable(layer),
      clipFunc(context) {
        context.beginPath();
        drawPolygonPath(context, mask.points || [], mask.bezierHandles);
        context.closePath();
      },
    });
  }

  function createPngMaskGroupNode(modelLayer, image) {
    const child = modelLayer.child || {};
    const node = new window.Konva.Group({
      x: 0,
      y: 0,
      name: modelLayer.id,
      id: modelLayer.id,
    });
    configurePngMaskGroupNode(node, modelLayer);
    const imageNode = new window.Konva.Image({
      image,
      x: child.x,
      y: child.y,
      width: child.width,
      height: child.height,
      offsetX: child.flipX ? child.width : 0,
      offsetY: child.flipY ? child.height : 0,
      scaleX: child.flipX ? -1 : 1,
      scaleY: child.flipY ? -1 : 1,
      rotation: child.rotation,
      opacity: numberValue(child.opacity, 1, 0, 1),
      visible: child.visible !== false,
      listening: true,
      name: `${modelLayer.id}Image`,
      id: `${modelLayer.id}Image`,
    });
    node.add(imageNode);
    return node;
  }

  function movePngMaskGroupByDelta(groupId, dx, dy) {
    const id = cleanText(groupId);
    const deltaX = Math.round(numberValue(dx, 0));
    const deltaY = Math.round(numberValue(dy, 0));
    if (!id || (!deltaX && !deltaY) || !state.layerDocument?.layers) return false;
    let changed = false;
    state.layerDocument.layers = layerList().map((layer) => {
      if (layer.id !== id || !isPngMaskGroupLayer(layer)) return layer;
      changed = true;
      const mask = {
        ...cloneJson(layer.mask),
        points: (layer.mask?.points || []).map((value, index) => coordinateValue(value) + (index % 2 === 0 ? deltaX : deltaY)),
        bezierHandles: moveBezierHandles(layer.mask?.bezierHandles, deltaX, deltaY),
      };
      const child = {
        ...cloneJson(layer.child),
        x: coordinateValue(layer.child?.x) + deltaX,
        y: coordinateValue(layer.child?.y) + deltaY,
      };
      return {
        ...layer,
        mask: { ...mask, order: 0, groupId: layer.id, maskRole: "mask" },
        child: { ...child, order: 1, groupId: layer.id, maskRole: "image" },
      };
    });
    if (!changed) return false;
    state.layerDocument = normalizeLayerDocument(state.layerDocument, state.textureSize);
    setDirty(true);
    return true;
  }

  function applyLayerToNode(layer) {
    const node = layerNode(layer?.id);
    if (!node) return;
    if (isPngMaskGroupLayer(layer)) {
      configurePngMaskGroupNode(node, layer);
      const imageNode = node.findOne?.(`#${layer.id}Image`);
      const child = layer.child || {};
      if (imageNode) {
        imageNode.position({ x: child.x, y: child.y });
        imageNode.size({ width: child.width, height: child.height });
        imageNode.offsetX(child.flipX ? child.width : 0);
        imageNode.offsetY(child.flipY ? child.height : 0);
        imageNode.scale({ x: child.flipX ? -1 : 1, y: child.flipY ? -1 : 1 });
        imageNode.rotation(child.rotation);
        imageNode.opacity(numberValue(child.opacity, 1, 0, 1));
        imageNode.visible(child.visible !== false);
      }
      node.getLayer()?.batchDraw?.();
      return;
    }
    if (layer.type === "stripe") {
      node.position({ x: 0, y: 0 });
      configureStripeNode(node, layer);
      node.getLayer()?.batchDraw?.();
      return;
    }
    if (layer.type === "polygon") {
      node.position({ x: 0, y: 0 });
      configurePolygonNode(node, layer);
      node.getLayer()?.batchDraw?.();
      return;
    }
    if (isShapeLayerType(layer.type)) {
      configureShapeNode(node, layer);
      node.scale({ x: 1, y: 1 });
      node.getLayer()?.batchDraw?.();
      return;
    }
    if (isTextLayerType(layer.type)) {
      const rendered = renderTextLayerCanvas(layer);
      node.size({ width: layer.width, height: layer.height });
      node.image(rendered.canvas);
      node.position({ x: layer.x, y: layer.y });
      node.offsetY(0);
      node.rotation(layer.rotation);
      node.opacity(layer.opacity);
      node.visible(layer.visible);
      node.draggable(isLayerCanvasSelectable(layer));
      node.listening(isLayerCanvasSelectable(layer));
      node.scale({ x: 1, y: 1 });
      node.getLayer()?.batchDraw?.();
      return;
    }
    node.position({ x: layer.x, y: layer.y });
    node.size({ width: layer.width, height: layer.height });
    node.offsetX(layer.type === "image" && layer.flipX ? layer.width : 0);
    node.offsetY(layer.type === "image" && layer.flipY ? layer.height : 0);
    node.rotation(layer.rotation);
    node.opacity(layer.opacity);
    node.visible(layer.visible);
    node.draggable(isLayerCanvasSelectable(layer));
    node.listening(isLayerCanvasSelectable(layer));
    node.scale({
      x: layer.type === "image" && layer.flipX ? -1 : 1,
      y: layer.type === "image" && layer.flipY ? -1 : 1,
    });
    node.getLayer()?.batchDraw?.();
  }

  function nodeTextureBounds(layerId) {
    const modelLayer = findLayer(layerId);
    if (modelLayer?.type === "stripe") return stripeBounds(modelLayer);
    if (modelLayer?.type === "polygon") return pointLayerBounds(modelLayer);
    const node = layerNode(layerId);
    const stage = state.konva?.stage;
    if (!node || !stage) return null;
    const rect = node.getClientRect({ relativeTo: stage });
    if (!rect || !Number.isFinite(rect.x) || !Number.isFinite(rect.y)) return null;
    return {
      x: numberValue(rect.x, 0),
      y: numberValue(rect.y, 0),
      width: Math.max(1, numberValue(rect.width, 1)),
      height: Math.max(1, numberValue(rect.height, 1)),
    };
  }

  function layerMoveSnapshot(layer) {
    if (!layer) return null;
    if (isPointLayerType(layer.type)) {
      return {
        id: layer.id,
        type: layer.type,
        points: Array.isArray(layer.points) ? layer.points.slice() : [],
        bezierHandles: cloneJson(layer.bezierHandles || []),
        strokeWidth: layer.strokeWidth,
      };
    }
    const bounds = nodeTextureBounds(layer.id);
    return {
      id: layer.id,
      type: layer.type,
      x: numberValue(layer.x, 0),
      y: numberValue(layer.y, 0),
      width: Math.max(1, numberValue(layer.width, 1)),
      height: Math.max(1, numberValue(layer.height, 1)),
      boundsX: bounds?.x,
      boundsY: bounds?.y,
      boundsWidth: bounds?.width,
      boundsHeight: bounds?.height,
    };
  }

  function snapshotBounds(snapshot, dx = 0, dy = 0) {
    if (!snapshot) return null;
    if (isPointLayerType(snapshot.type)) {
      const bounds = snapshot.type === "stripe" ? stripeBounds(snapshot) : pointLayerBounds(snapshot);
      return {
        x: bounds.x + dx,
        y: bounds.y + dy,
        width: bounds.width,
        height: bounds.height,
      };
    }
    return {
      x: numberValue(snapshot.boundsX, numberValue(snapshot.x, 0)) + dx,
      y: numberValue(snapshot.boundsY, numberValue(snapshot.y, 0)) + dy,
      width: Math.max(1, numberValue(snapshot.boundsWidth, numberValue(snapshot.width, 1))),
      height: Math.max(1, numberValue(snapshot.boundsHeight, numberValue(snapshot.height, 1))),
    };
  }

  function combinedSnapshotBounds(snapshots, dx = 0, dy = 0) {
    const boxes = Object.values(snapshots || {})
      .map((snapshot) => snapshotBounds(snapshot, dx, dy))
      .filter(Boolean);
    if (!boxes.length) return null;
    const minX = Math.min(...boxes.map((box) => box.x));
    const minY = Math.min(...boxes.map((box) => box.y));
    const maxX = Math.max(...boxes.map((box) => box.x + box.width));
    const maxY = Math.max(...boxes.map((box) => box.y + box.height));
    return {
      x: minX,
      y: minY,
      width: Math.max(1, maxX - minX),
      height: Math.max(1, maxY - minY),
    };
  }

  function snapTargets(axis) {
    const targets = [axis === "x" ? state.textureSize.width / 2 : state.textureSize.height / 2];
    guideList()
      .filter((guide) => guide.axis === axis)
      .forEach((guide) => targets.push(numberValue(guide.position, 0)));
    return targets;
  }

  function snapAxisDelta(values, targets, threshold) {
    let best = 0;
    let bestDistance = threshold + 1;
    values.forEach((value) => {
      targets.forEach((target) => {
        const delta = target - value;
        const distance = Math.abs(delta);
        if (distance <= threshold && distance < bestDistance) {
          best = delta;
          bestDistance = distance;
        }
      });
    });
    return Math.round(best);
  }

  function axisLockedDragDelta(dx, dy, enabled) {
    const roundedDx = Math.round(numberValue(dx, 0));
    const roundedDy = Math.round(numberValue(dy, 0));
    if (!enabled) return { dx: roundedDx, dy: roundedDy };
    return Math.abs(roundedDx) >= Math.abs(roundedDy)
      ? { dx: roundedDx, dy: 0 }
      : { dx: 0, dy: roundedDy };
  }

  function snappedDragDelta(anchorId, dx, dy, options = {}) {
    const drag = state.moveDrag && state.moveDrag.anchorId === anchorId ? state.moveDrag : null;
    const locked = axisLockedDragDelta(dx, dy, Boolean(options.shiftKey));
    const roundedDx = locked.dx;
    const roundedDy = locked.dy;
    if (!drag || state.layerDocument?.snapEnabled === false) return { dx: roundedDx, dy: roundedDy };
    const bounds = combinedSnapshotBounds(drag.snapshots, roundedDx, roundedDy);
    if (!bounds) return { dx: roundedDx, dy: roundedDy };
    const threshold = Math.max(2, 8 / Math.max(0.01, state.view.scale || 1));
    const xValues = [bounds.x, bounds.x + bounds.width / 2, bounds.x + bounds.width];
    const yValues = [bounds.y, bounds.y + bounds.height / 2, bounds.y + bounds.height];
    return {
      dx: roundedDx + snapAxisDelta(xValues, snapTargets("x"), threshold),
      dy: roundedDy + snapAxisDelta(yValues, snapTargets("y"), threshold),
    };
  }

  function snappedNodeDelta(node, current, options = {}) {
    const snapshot = state.moveDrag?.snapshots?.[node.id()];
    if (!snapshot) return { dx: Math.round(node.x() || 0), dy: Math.round(node.y() || 0) };
    if (isPointLayerType(current?.type || snapshot.type)) {
      return snappedDragDelta(node.id(), Math.round(node.x() || 0), Math.round(node.y() || 0), options);
    }
    return snappedDragDelta(
      node.id(),
      Math.round(node.x() - numberValue(snapshot.x, current?.x || 0)),
      Math.round(node.y() - numberValue(snapshot.y, current?.y || 0)),
      options,
    );
  }

  function applySnappedNodePosition(node, current, options = {}) {
    const snapshot = state.moveDrag?.snapshots?.[node.id()];
    const snapped = snappedNodeDelta(node, current, options);
    if (!snapshot || isPointLayerType(current?.type || snapshot?.type)) {
      node.position({ x: snapped.dx, y: snapped.dy });
      return snapped;
    }
    node.position({
      x: numberValue(snapshot.x, current?.x || 0) + snapped.dx,
      y: numberValue(snapshot.y, current?.y || 0) + snapped.dy,
    });
    return snapped;
  }

  function alignmentBounds(layer) {
    if (!layer || layer.visible === false || layer.effectiveVisible === false) return null;
    const bounds = nodeTextureBounds(layer.id);
    if (bounds) return bounds;
    if (layer.type === "stripe") return stripeBounds(layer);
    if (layer.type === "polygon") return pointLayerBounds(layer);
    return {
      x: numberValue(layer.x, 0),
      y: numberValue(layer.y, 0),
      width: Math.max(1, numberValue(layer.width, 1)),
      height: Math.max(1, numberValue(layer.height, 1)),
    };
  }

  function alignmentDelta(bounds, referenceBounds, mode) {
    if (!bounds || !referenceBounds) return { dx: 0, dy: 0 };
    if (mode === "left") return { dx: referenceBounds.x - bounds.x, dy: 0 };
    if (mode === "center-x") return { dx: (referenceBounds.x + referenceBounds.width / 2) - (bounds.x + bounds.width / 2), dy: 0 };
    if (mode === "right") return { dx: (referenceBounds.x + referenceBounds.width) - (bounds.x + bounds.width), dy: 0 };
    if (mode === "top") return { dx: 0, dy: referenceBounds.y - bounds.y };
    if (mode === "center-y") return { dx: 0, dy: (referenceBounds.y + referenceBounds.height / 2) - (bounds.y + bounds.height / 2) };
    if (mode === "bottom") return { dx: 0, dy: (referenceBounds.y + referenceBounds.height) - (bounds.y + bounds.height) };
    return { dx: 0, dy: 0 };
  }

  function alignSelectedLayers(mode) {
    const action = cleanText(mode);
    const selected = selectedLayers()
      .filter((layer) => layer.visible !== false && layer.effectiveVisible !== false);
    if (selected.length < 2) return false;
    const reference = primarySelectedLayer() || selected[0];
    if (!reference || !selected.some((layer) => layer.id === reference.id)) return false;
    const referenceBounds = alignmentBounds(reference);
    if (!referenceBounds) return false;
    const movableIds = new Set(selected
      .filter((layer) => layer.id !== reference.id && !layer.locked && !layer.effectiveLocked)
      .map((layer) => layer.id));
    if (!movableIds.size) return false;
    beginHistory(`Align ${action}`);
    let changed = false;
    const alignLayer = (layer) => {
      if (!movableIds.has(layer.id)) return layer;
      const bounds = alignmentBounds(layer);
      const delta = alignmentDelta(bounds, referenceBounds, action);
      const dx = Math.round(numberValue(delta.dx, 0));
      const dy = Math.round(numberValue(delta.dy, 0));
      if (!dx && !dy) return layer;
      const next = isPointLayerType(layer.type)
        ? clampLayerPatch(layer, {
          points: (layer.points || []).map((value, index) => coordinateValue(value) + (index % 2 === 0 ? dx : dy)),
        })
        : clampLayerPatch(layer, {
          x: Math.round(numberValue(layer.x, 0) + dx),
          y: Math.round(numberValue(layer.y, 0) + dy),
        });
      changed = true;
      applyLayerToNode(next);
      return next;
    };
    state.layerDocument.layers = layerList().map((layer) => {
      if (layer.type === "group") {
        return {
          ...layer,
          children: (layer.children || []).map((child) => alignLayer(child)),
        };
      }
      return alignLayer(layer);
    });
    if (!changed) {
      cancelHistory();
      return false;
    }
    state.layerDocument = normalizeLayerDocument(state.layerDocument, state.textureSize);
    renderLayerList();
    renderPropertiesPanel();
    updateTransformerForSelection();
    renderMultiSelectionBox();
    renderLayerHoverBox();
    updateCanvasStatus();
    setDirty(true);
    commitHistory();
    return true;
  }

  function isFlipLayerType(type) {
    return type === "image" || isPointLayerType(type) || isShapeLayerType(type);
  }

  function selectedFlipLayers() {
    const selected = selectedLayers();
    const rows = [];
    const seen = new Set();
    let blocked = false;
    const addLayer = (layer) => {
      if (!layer || layer.visible === false || layer.effectiveVisible === false) return;
      if (layer.locked || layer.effectiveLocked) {
        blocked = true;
        return;
      }
      if (!isFlipLayerType(layer.type)) {
        blocked = true;
        return;
      }
      if (seen.has(layer.id)) return;
      seen.add(layer.id);
      rows.push(layer);
    };
    selected.forEach((layer) => {
      if (layer.type !== "group") {
        addLayer(layer);
        return;
      }
      if (layer.locked || layer.visible === false) {
        blocked = true;
        return;
      }
      (layer.children || []).forEach((child) => addLayer(findLayer(child.id) || child));
    });
    return { layers: rows, blocked };
  }

  function canFlipSelection() {
    const selection = selectedFlipLayers();
    return Boolean(selection.layers.length && !selection.blocked);
  }

  function reflectPointInBounds(point, bounds, axis) {
    const x = numberValue(point.x, 0);
    const y = numberValue(point.y, 0);
    if (axis === "vertical") {
      return {
        x,
        y: bounds.y + bounds.height - (y - bounds.y),
      };
    }
    return {
      x: bounds.x + bounds.width - (x - bounds.x),
      y,
    };
  }

  function normalizeRotationAngle(degrees) {
    let angle = numberValue(degrees, 0) % 360;
    if (angle > 180) angle -= 360;
    if (angle <= -180) angle += 360;
    if (Math.abs(Math.abs(angle) - 180) < 0.0001) return 0;
    return angle;
  }

  function snapRotationAngle(degrees, step = 45) {
    const snapStep = Math.max(1, numberValue(step, 45));
    return normalizeRotationAngle(Math.round(numberValue(degrees, 0) / snapStep) * snapStep);
  }

  function simpleTransformLayer(layer) {
    return layer?.type === "image" || isShapeLayerType(layer?.type) || isTextLayerType(layer?.type);
  }

  function setTransformerRotationSnap(enabled) {
    const transformer = state.konva?.transformer;
    if (!transformer) return;
    const activeNode = transformer.nodes?.()[0];
    const activeLayer = findLayer(activeNode?.id?.());
    const isGroupTransform = activeNode?.name?.() === "groupTransformBox";
    const isPointTransform = activeNode?.name?.() === "pointLayerTransformBox";
    const useSnap = Boolean(enabled && (isGroupTransform || isPointTransform || simpleTransformLayer(activeLayer)) && transformer.getActiveAnchor?.() === "rotater");
    if (typeof transformer.rotationSnaps === "function") transformer.rotationSnaps(useSnap ? ROTATION_SNAP_ANGLES : []);
    if (typeof transformer.rotationSnapTolerance === "function") transformer.rotationSnapTolerance(useSnap ? 22.5 : 0);
  }

  function originFromLayerCenter(center, width, height, rotationDegrees) {
    const rotation = (numberValue(rotationDegrees, 0) * Math.PI) / 180;
    const cos = Math.cos(rotation);
    const sin = Math.sin(rotation);
    const dx = width / 2;
    const dy = height / 2;
    return {
      x: center.x - (dx * cos - dy * sin),
      y: center.y - (dx * sin + dy * cos),
    };
  }

  function flipLayerGeometry(layer, bounds, axis) {
    if (isPointLayerType(layer.type)) {
      const points = (layer.points || []).slice();
      const nextPoints = [];
      for (let index = 0; index < points.length; index += 2) {
        const point = reflectPointInBounds({ x: points[index], y: points[index + 1] }, bounds, axis);
        nextPoints.push(point.x, point.y);
      }
      const patch = { points: nextPoints };
      if (isPointLayerType(layer.type) && Array.isArray(layer.bezierHandles)) {
        patch.bezierHandles = layer.bezierHandles.map((handle) => {
          if (!handle) return null;
          const out = handle.out ? reflectPointInBounds(handle.out, bounds, axis) : null;
          const input = handle.in ? reflectPointInBounds(handle.in, bounds, axis) : null;
          return {
            out: out ? { x: out.x, y: out.y } : null,
            in: input ? { x: input.x, y: input.y } : null,
          };
        });
      }
      return clampLayerPatch(layer, patch);
    }
    if (layer.type === "image") {
      const width = Math.max(1, numberValue(layer.width, 1));
      const height = Math.max(1, numberValue(layer.height, 1));
      const rotation = numberValue(layer.rotation, 0);
      const center = rotatedLayerPoint(layer, width / 2, height / 2);
      const nextCenter = reflectPointInBounds(center, bounds, axis);
      const nextOrigin = originFromLayerCenter(nextCenter, width, height, rotation);
      return clampLayerPatch(layer, {
        x: Math.round(nextOrigin.x),
        y: Math.round(nextOrigin.y),
        flipX: axis === "horizontal" ? !Boolean(layer.flipX) : Boolean(layer.flipX),
        flipY: axis === "vertical" ? !Boolean(layer.flipY) : Boolean(layer.flipY),
      });
    }
    if (!isShapeLayerType(layer.type)) return layer;
    const width = Math.max(1, numberValue(layer.width, 1));
    const height = Math.max(1, numberValue(layer.height, 1));
    const center = rotatedLayerPoint(layer, width / 2, height / 2);
    const nextCenter = reflectPointInBounds(center, bounds, axis);
    const rotation = numberValue(layer.rotation, 0);
    const nextRotation = axis === "vertical"
      ? normalizeRotationAngle(-rotation)
      : normalizeRotationAngle(180 - rotation);
    const nextOrigin = originFromLayerCenter(nextCenter, width, height, nextRotation);
    const patch = {
      x: Math.round(nextOrigin.x),
      y: Math.round(nextOrigin.y),
      rotation: nextRotation,
    };
    return clampLayerPatch(layer, patch);
  }

  function flipSelectedLayers(axis) {
    const mode = axis === "vertical" ? "vertical" : "horizontal";
    const selection = selectedFlipLayers();
    if (!selection.layers.length) {
      setText("layerStudioEditorStatus", "Select PNG layers, shapes, polygons or stripes before flipping.");
      return false;
    }
    if (selection.blocked) {
      setText("layerStudioEditorStatus", "Flip is available for unlocked PNG layers, shapes, polygons and stripes only.");
      return false;
    }
    const bounds = layerNodeBoundsForLayers(selection.layers, { allowSingle: true });
    if (!bounds) return false;
    beginHistory(mode === "vertical" ? "Flip vertical" : "Flip horizontal");
    const transformed = new Map(selection.layers.map((layer) => [layer.id, flipLayerGeometry(layer, bounds, mode)]));
    let changed = false;
    state.layerDocument.layers = layerList().map((layer) => {
      if (layer.type === "group") {
        const children = (layer.children || []).map((child) => {
          const next = transformed.get(child.id);
          if (!next) return child;
          changed = true;
          return next;
        });
        return { ...layer, children };
      }
      const next = transformed.get(layer.id);
      if (!next) return layer;
      changed = true;
      return next;
    });
    if (!changed) {
      cancelHistory();
      return false;
    }
    state.layerDocument = normalizeLayerDocument(state.layerDocument, state.textureSize);
    transformed.forEach((layer) => applyLayerToNode(findLayer(layer.id)));
    renderLayerList();
    renderPropertiesPanel();
    updateTransformerForSelection();
    renderMultiSelectionBox();
    renderLayerHoverBox();
    updateCanvasStatus();
    setDirty(true);
    commitHistory();
    setText("layerStudioEditorStatus", mode === "vertical" ? "Selection flipped vertically." : "Selection flipped horizontally.");
    return true;
  }

  function draggableLayerIdsForSelection(anchor) {
    const selected = selectedLayers();
    if (!anchor || !selected.length) return [];
    const selectedIds = selected.map((layer) => layer.id);
    const ids = new Set();
    if (selectedIds.includes(anchor.id)) {
      selected.forEach((layer) => {
        if (layer.type === "group") {
          (layer.children || []).forEach((child) => ids.add(child.id));
        } else {
          ids.add(layer.id);
        }
      });
    } else if (anchor.groupId && selectedIds.includes(anchor.groupId)) {
      const group = findGroup(anchor.groupId);
      (group?.children || []).forEach((child) => ids.add(child.id));
    }
    return Array.from(ids).filter((id) => {
      const layer = findLayer(id);
      return layer && !layer.locked && !layer.effectiveLocked;
    });
  }

  function beginMultiDrag(anchorId) {
    const anchor = findLayer(anchorId);
    const ids = draggableLayerIdsForSelection(anchor);
    if (!anchor || !ids.length) {
      state.moveDrag = null;
      state.multiDrag = null;
      return null;
    }
    const snapshots = {};
    ids.forEach((id) => {
      const layer = findLayer(id);
      if (layer) snapshots[id] = layerMoveSnapshot(layer);
    });
    state.moveDrag = { anchorId: anchor.id, snapshots };
    if (ids.length < 2) {
      state.multiDrag = null;
      return null;
    }
    state.multiDrag = { anchorId: anchor.id, snapshots };
    return state.multiDrag;
  }

  function applyMultiDrag(anchorId, dx, dy, options = {}) {
    const drag = state.multiDrag && state.multiDrag.anchorId === anchorId ? state.multiDrag : null;
    if (!drag) return false;
    const skipNodeId = cleanText(options.skipNodeId);
    const snapped = snappedDragDelta(anchorId, dx, dy, options);
    const roundedDx = snapped.dx;
    const roundedDy = snapped.dy;
    let changed = false;
    const moveLayer = (layer) => {
      const snapshot = drag.snapshots[layer.id];
      if (!snapshot || layer.locked) return layer;
      let next = layer;
      if (isPointLayerType(layer.type)) {
        next = clampLayerPatch(layer, {
          points: snapshot.points.map((value, index) => coordinateValue(value) + (index % 2 === 0 ? roundedDx : roundedDy)),
        });
      } else {
        next = clampLayerPatch(layer, {
          x: Math.round(snapshot.x + roundedDx),
          y: Math.round(snapshot.y + roundedDy),
        });
      }
      changed = true;
      if (next.id !== skipNodeId) applyLayerToNode(next);
      return next;
    };
    state.layerDocument.layers = layerList().map((layer) => {
      if (layer.type === "group") {
        return {
          ...layer,
          children: (layer.children || []).map((child) => moveLayer(child)),
        };
      }
      return moveLayer(layer);
    });
    if (changed) {
      renderPropertiesPanel();
      renderLayerList();
      updateTransformerForSelection();
      renderMultiSelectionBox();
      renderLayerHoverBox();
      updateCanvasStatus();
      setDirty(true);
    }
    return changed;
  }

  function endMultiDrag() {
    state.multiDrag = null;
    state.moveDrag = null;
  }

  function nudgeSelectedLayers(dx, dy) {
    const roundedDx = Math.round(numberValue(dx, 0));
    const roundedDy = Math.round(numberValue(dy, 0));
    if (!roundedDx && !roundedDy) return false;
    const ids = new Set();
    selectedLayers().forEach((layer) => {
      if (layer.type === "group") {
        (layer.children || []).forEach((child) => ids.add(child.id));
      } else {
        ids.add(layer.id);
      }
    });
    const movableIds = new Set(Array.from(ids).filter((id) => {
      const layer = findLayer(id);
      return layer && !layer.locked && !layer.effectiveLocked;
    }));
    if (!movableIds.size) return false;
    pushHistory(movableIds.size > 1 ? "Nudge layers" : "Nudge layer");
    let changed = false;
    const nudgeLayer = (layer) => {
      if (!movableIds.has(layer.id) || layer.locked || layer.effectiveLocked) return layer;
      const next = isPointLayerType(layer.type)
        ? clampLayerPatch(layer, {
          points: (layer.points || []).map((value, index) => coordinateValue(value) + (index % 2 === 0 ? roundedDx : roundedDy)),
        })
        : clampLayerPatch(layer, {
          x: numberValue(layer.x, 0) + roundedDx,
          y: numberValue(layer.y, 0) + roundedDy,
        });
      changed = true;
      applyLayerToNode(next);
      return next;
    };
    state.layerDocument.layers = layerList().map((layer) => {
      if (layer.type === "group") {
        return {
          ...layer,
          children: (layer.children || []).map((child) => nudgeLayer(child)),
        };
      }
      return nudgeLayer(layer);
    });
    if (!changed) return false;
    renderPropertiesPanel();
    renderLayerList();
    updateTransformerForSelection();
    renderMultiSelectionBox();
    renderLayerHoverBox();
    updateCanvasStatus();
    setDirty(true);
    return true;
  }

  function normalizeLayerOrders() {
    const sorted = layerList().slice().sort((left, right) => (left.order - right.order) || left.id.localeCompare(right.id));
    sorted.forEach((layer, index) => {
      layer.order = index;
      if (isPngMaskGroupLayer(layer)) {
        if (layer.mask) layer.mask = { ...layer.mask, order: 0, groupId: layer.id, maskRole: "mask" };
        if (layer.child) layer.child = { ...layer.child, order: 1, groupId: layer.id, maskRole: "image" };
      } else if (layer.type === "group") {
        layer.children = (layer.children || []).map((child, childIndex) => ({ ...child, order: childIndex, groupId: layer.id }));
      }
    });
    state.layerDocument.layers = sorted;
  }

  function syncKonvaLayerOrder() {
    const objectLayer = state.konva?.objectLayer;
    if (!objectLayer) return;
    flatLayerList({ includeHiddenChildren: false })
      .forEach((layer) => {
        const node = layerNode(layer.id);
        if (node) node.moveToTop();
      });
    objectLayer.batchDraw();
  }

  async function rebuildLayerWorkflow(options = {}) {
    normalizeLayerOrders();
    state.layerDocument = normalizeLayerDocument(state.layerDocument, state.textureSize);
    if (options.keepSelection === true) {
      syncSelectionState();
    } else if (!findLayer(state.selectedLayerId)) {
      state.selectedLayerId = "";
      state.selectedLayerIds = [];
    }
    await rebuildKonvaLayers();
    renderLayerList();
    renderPropertiesPanel();
    renderGuides();
  }

  function duplicateLayer(layerId) {
    const source = findLayer(layerId);
    if (!source || !state.layerDocument?.layers) return null;
    pushHistory("Duplicate layer");
    normalizeLayerOrders();
    const existingIds = new Set(layerList().map((layer) => layer.id));
    flatLayerList({ includeMaskChildren: true }).forEach((layer) => existingIds.add(layer.id));
    const baseId = `${source.id}_copy`;
    let nextId = baseId;
    let suffix = 2;
    while (existingIds.has(nextId)) {
      nextId = `${baseId}_${suffix}`;
      suffix += 1;
    }
    const insertOrder = source.order + 1;
    state.layerDocument.layers.forEach((layer) => {
      if (layer.order >= insertOrder) layer.order += 1;
    });
    const copy = {
      ...cloneJson(source),
      id: nextId,
      name: `${source.name || source.id} Copy`,
      order: insertOrder,
      locked: false,
      visible: true,
    };
    if (isPngMaskGroupLayer(source)) {
      const cloneMaskedLayer = (child, role) => {
        const childBaseId = `${child.id}_copy`;
        let childId = childBaseId;
        let childSuffix = 2;
        while (existingIds.has(childId)) {
          childId = `${childBaseId}_${childSuffix}`;
          childSuffix += 1;
        }
        existingIds.add(childId);
        const childCopy = {
          ...cloneJson(child),
          id: childId,
          order: role === "mask" ? 0 : 1,
          groupId: nextId,
          maskRole: role,
        };
        return childCopy;
      };
      copy.mask = cloneMaskedLayer(source.mask, "mask");
      copy.child = cloneMaskedLayer(source.child, "image");
    } else if (source.type === "group") {
      copy.children = (source.children || []).map((child, childIndex) => {
        const childBaseId = `${child.id}_copy`;
        let childId = childBaseId;
        let childSuffix = 2;
        while (existingIds.has(childId)) {
          childId = `${childBaseId}_${childSuffix}`;
          childSuffix += 1;
        }
        existingIds.add(childId);
        const childCopy = {
          ...cloneJson(child),
          id: childId,
          order: childIndex,
          groupId: nextId,
        };
        return childCopy;
      });
    }
    state.layerDocument.layers.push(copy);
    state.selectedLayerId = nextId;
    state.selectedLayerIds = [nextId];
    setDirty(true);
    return copy;
  }

  function duplicateSelectedLayers(anchorId = state.selectedLayerId) {
    const ids = selectedLayerIds().includes(cleanText(anchorId)) ? selectedLayerIds() : [cleanText(anchorId)].filter(Boolean);
    const sources = ids
      .map((id) => findLayer(id))
      .filter(Boolean)
      .sort((left, right) => (left.order - right.order) || left.id.localeCompare(right.id));
    if (!sources.length || !state.layerDocument?.layers) return [];
    if (sources.length === 1) {
      const copy = duplicateLayer(sources[0].id);
      state.selectedLayerIds = copy ? [copy.id] : [];
      return copy ? [copy] : [];
    }
    pushHistory("Duplicate layers");
    normalizeLayerOrders();
    const existingIds = new Set(layerList().map((layer) => layer.id));
    flatLayerList({ includeMaskChildren: true }).forEach((layer) => existingIds.add(layer.id));
    const maxOrder = Math.max(...sources.map((layer) => layer.order));
    const insertOrder = maxOrder + 1;
    state.layerDocument.layers.forEach((layer) => {
      if (layer.order >= insertOrder) layer.order += sources.length;
    });
    const copies = sources.map((source, index) => {
      const baseId = `${source.id}_copy`;
      let nextId = baseId;
      let suffix = 2;
      while (existingIds.has(nextId)) {
        nextId = `${baseId}_${suffix}`;
        suffix += 1;
      }
      existingIds.add(nextId);
      const copy = {
        ...cloneJson(source),
        id: nextId,
        name: `${source.name || source.id} Copy`,
        order: insertOrder + index,
        locked: false,
        visible: true,
      };
      if (isPngMaskGroupLayer(source)) {
        const cloneMaskedLayer = (child, role) => {
          const childBaseId = `${child.id}_copy`;
          let childId = childBaseId;
          let childSuffix = 2;
          while (existingIds.has(childId)) {
            childId = `${childBaseId}_${childSuffix}`;
            childSuffix += 1;
          }
          existingIds.add(childId);
          const childCopy = {
            ...cloneJson(child),
            id: childId,
            order: role === "mask" ? 0 : 1,
            groupId: nextId,
            maskRole: role,
          };
          return childCopy;
        };
        copy.mask = cloneMaskedLayer(source.mask, "mask");
        copy.child = cloneMaskedLayer(source.child, "image");
      } else if (source.type === "group") {
        copy.children = (source.children || []).map((child, childIndex) => {
          const childBaseId = `${child.id}_copy`;
          let childId = childBaseId;
          let childSuffix = 2;
          while (existingIds.has(childId)) {
            childId = `${childBaseId}_${childSuffix}`;
            childSuffix += 1;
          }
          existingIds.add(childId);
          const childCopy = {
            ...cloneJson(child),
            id: childId,
            order: childIndex,
            groupId: nextId,
          };
          return childCopy;
        });
      }
      return copy;
    });
    state.layerDocument.layers.push(...copies);
    state.selectedLayerIds = copies.map((layer) => layer.id);
    state.selectedLayerId = state.selectedLayerIds[state.selectedLayerIds.length - 1] || "";
    setDirty(true);
    return copies;
  }

  function deleteLayer(layerId) {
    const id = cleanText(layerId);
    const layer = findLayer(id);
    if (!layer) return false;
    if (layer.maskGroupId) {
      setText("layerStudioEditorStatus", "Use Remove PNG mask on the mask parent before deleting masked child layers.");
      return false;
    }
    const sorted = visibleLayerRows().filter((row) => !row.virtual).slice().reverse();
    const sortedIndex = sorted.findIndex((row) => row.id === id);
    pushHistory("Delete layer");
    if (!removeLayersFromDocument([id])) return false;
    const remaining = sorted.filter((row) => row.id !== id && findLayer(row.id));
    const nextSelection = remaining[sortedIndex]?.id || remaining[sortedIndex - 1]?.id || "";
    state.selectedLayerId = nextSelection;
    state.selectedLayerIds = nextSelection ? [nextSelection] : [];
    setDirty(true);
    return true;
  }

  function deleteSelectedLayers(anchorId = state.selectedLayerId) {
    const ids = selectedLayerIds().includes(cleanText(anchorId)) ? selectedLayerIds() : [cleanText(anchorId)].filter(Boolean);
    const deleteSet = new Set(ids.filter((id) => findLayer(id)));
    if (!deleteSet.size) return false;
    if (Array.from(deleteSet).some((id) => findLayer(id)?.maskGroupId)) {
      setText("layerStudioEditorStatus", "Use Remove PNG mask on the mask parent before deleting masked child layers.");
      return false;
    }
    const sorted = visibleLayerRows().filter((row) => !row.virtual).slice().reverse();
    const firstIndex = sorted.findIndex((layer) => deleteSet.has(layer.id));
    pushHistory(deleteSet.size > 1 ? "Delete layers" : "Delete layer");
    if (!removeLayersFromDocument(Array.from(deleteSet))) return false;
    const remaining = sorted.filter((layer) => !deleteSet.has(layer.id) && findLayer(layer.id));
    const nextSelection = remaining[firstIndex]?.id || remaining[firstIndex - 1]?.id || "";
    state.selectedLayerIds = nextSelection ? [nextSelection] : [];
    state.selectedLayerId = nextSelection;
    setDirty(true);
    return true;
  }

  function canRenameLayerInline(layer) {
    return Boolean(layer && !layer.virtual && (layer.type === "group" || isPngMaskGroupLayer(layer) || isPointLayerType(layer.type) || isShapeLayerType(layer.type) || isTextLayerType(layer.type)));
  }

  function fallbackLayerName(layer) {
    if (layer?.type === "group") return "Group";
    if (isPngMaskGroupLayer(layer)) return "PNG Mask";
    if (layer?.type === "polygon") return "Polygon";
    if (layer?.type === "stripe") return "Stripe";
    if (layer?.type === "rectangle") return "Rectangle";
    if (layer?.type === "ellipse") return "Ellipse";
    if (layer?.type === "text") return "Text";
    return "Layer";
  }

  function focusRenameInput() {
    window.requestAnimationFrame(() => {
      const input = $("layerStudioLayerList")?.querySelector?.(".layer-studio-layer-rename-input");
      if (!input) return;
      input.focus();
      input.select();
    });
  }

  function startInlineRename(layerId) {
    const layer = findLayer(layerId);
    if (!canRenameLayerInline(layer)) return false;
    state.renamingLayerId = layer.id;
    state.renameOriginalValue = cleanText(layer.name, fallbackLayerName(layer));
    renderLayerList();
    focusRenameInput();
    return true;
  }

  function cancelInlineRename() {
    if (!state.renamingLayerId) return;
    state.renamingLayerId = "";
    state.renameOriginalValue = "";
    renderLayerList();
  }

  function commitInlineRename(value) {
    const id = cleanText(state.renamingLayerId);
    if (!id) return false;
    const layer = findLayer(id);
    if (!canRenameLayerInline(layer)) {
      cancelInlineRename();
      return false;
    }
    const nextName = cleanText(value, fallbackLayerName(layer));
    state.renamingLayerId = "";
    state.renameOriginalValue = "";
    if (nextName === cleanText(layer.name)) {
      renderLayerList();
      return true;
    }
    pushHistory("Rename layer");
    patchLayer(id, { name: nextName }, { updateNode: false });
    return true;
  }

  function moveLayer(layerId, direction) {
    const id = cleanText(layerId);
    const sorted = layerList().slice().sort((left, right) => left.order - right.order);
    const index = sorted.findIndex((layer) => layer.id === id);
    const targetIndex = index + direction;
    if (index < 0 || targetIndex < 0 || targetIndex >= sorted.length) return false;
    pushHistory("Reorder layer");
    const currentOrder = sorted[index].order;
    sorted[index].order = sorted[targetIndex].order;
    sorted[targetIndex].order = currentOrder;
    state.selectedLayerId = id;
    state.selectedLayerIds = [id];
    setDirty(true);
    return true;
  }

  function renderLayerList() {
    const host = $("layerStudioLayerList");
    if (!host) return;
    const rows = visibleLayerRows();
    if (!rows.length) {
      host.innerHTML = `<div class="layer-studio-layer-empty">No editable layers yet</div>`;
      return;
    }
    host.innerHTML = rows.map((layer) => {
      const missingPng = isLayerPngMissing(layer);
      const classes = [
        "layer-studio-layer-row",
        isLayerSelected(layer.id) ? "is-selected" : "",
        isLayerSelected(layer.id) && selectedLayerIds().length > 1 ? "is-multi-selected" : "",
        layer.id === state.draggedLayerId ? "is-dragging" : "",
        layer.id === state.dragOverLayerId ? "is-drop-target" : "",
        layer.id === state.hoveredLayerId ? "is-hovered" : "",
        layer.visible === false || layer.effectiveVisible === false ? "is-hidden" : "",
        layer.locked || layer.effectiveLocked ? "is-locked" : "",
        layer.virtual ? "is-virtual" : "",
        layer.compactOverlay ? "is-compact-overlay" : "",
        layer.type === "group" ? "is-group" : "",
        isPngMaskGroupLayer(layer) ? "is-group is-mask-group" : "",
        layer.type === "stripe" ? "is-stripe" : "",
        missingPng ? "is-missing" : "",
        state.polygonEditLayerId === layer.id ? "is-point-editing" : "",
        layer.childRow ? "is-group-child" : "",
        layer.childRow && layer.maskGroupId ? "is-mask-child" : "",
        layer.childRow && !layer.maskGroupId ? "is-normal-group-child" : "",
      ].filter(Boolean).join(" ");
      const draggable = !layer.virtual && !layer.childRow;
      const flags = [
        layer.virtual && layer.systemLayer ? "system" : "",
        layer.virtual && !layer.systemLayer && !layer.compactOverlay ? "overlay" : "",
        layer.type === "group" ? `${(layer.children || []).length} layers` : "",
        isPngMaskGroupLayer(layer) ? "PNG mask" : "",
        layer.maskRole === "mask" ? "mask" : "",
        layer.maskRole === "image" ? "masked PNG" : "",
        layer.childRow ? `in ${layer.groupName || "group"}` : "",
        layer.sourceType === "library" ? "my library" : "",
        layer.sourceType === "projectLibrary" ? "project asset" : "",
        missingPng ? "PNG not found" : "",
        isUvOverlayLayerId(layer.id) && layer.inverted && !layer.compactOverlay ? "inverted" : "",
        layer.visible === false || layer.effectiveVisible === false ? "hidden" : "",
        layer.locked || layer.effectiveLocked ? "locked" : "",
      ].filter(Boolean).join(" / ") || "editable";
      const sourceLabel = layer.id === BASE_COLOR_LAYER_ID
        ? "Full texture background"
        : layer.id === ORIGINAL_BASE_TEXTURE_LAYER_ID
        ? "Original start point texture"
        : isPngMaskGroupLayer(layer)
        ? (layer.collapsed ? "Collapsed PNG mask" : "Expanded PNG mask")
        : layer.maskRole === "mask"
        ? "Mask polygon"
        : layer.maskRole === "image"
        ? "Masked PNG"
        : layer.type === "group"
        ? (layer.collapsed ? "Collapsed group" : "Expanded group")
        : layer.type === "polygon"
        ? "Polygon fill"
        : layer.type === "stripe"
        ? `Stripe / ${layer.strokeWidth || 0}px`
        : layer.type === "rectangle"
        ? "Rectangle fill"
        : layer.type === "ellipse"
        ? "Ellipse fill"
        : layer.type === "text"
        ? `Text: ${layer.text || "-"}`
        : missingPng
          ? `PNG not found: ${layer.source || "-"}`
        : layer.sourceType === "library" || layer.sourceType === "projectLibrary"
          ? `${layer.sourceType === "library" ? "My Library" : "Project Assets"}: ${layerAssetById(layer)?.name || layer.source || "-"}`
          : layer.source || "-";
      const isGroup = layer.type === "group";
      const isMaskGroup = isPngMaskGroupLayer(layer);
      const isGroupLike = isGroup || isMaskGroup;
      const canCreateGroup = canCreateGroupFromSelection();
      const canCreateMask = canCreatePngMaskFromSelection();
      const isRenaming = state.renamingLayerId === layer.id && canRenameLayerInline(layer);
      const canRename = canRenameLayerInline(layer);
      const nameMarkup = isRenaming
        ? `<input class="layer-studio-layer-rename-input" type="text" value="${escapeHtml(state.renameOriginalValue || layer.name || fallbackLayerName(layer))}" data-layer-rename-input="${escapeHtml(layer.id)}" aria-label="Rename layer" />`
        : `<strong class="layer-studio-layer-name" ${canRename ? `data-layer-rename="${escapeHtml(layer.id)}" title="Double-click to rename"` : ""}>${escapeHtml(layer.name || layer.id)}</strong>`;
      const strokeSwatch = layer.id === BASE_COLOR_LAYER_ID
        ? `<button class="layer-studio-layer-stroke-swatch" type="button" style="--layer-studio-layer-stroke: ${cssColorValue(layer.fill, "#ffffff")}" data-layer-action="base-color-fill" title="Choose base color" aria-label="Choose base color"></button>`
        : layer.type === "polygon" && layer.maskRole !== "mask"
          ? `<button class="layer-studio-layer-stroke-swatch" type="button" style="--layer-studio-layer-stroke: ${cssColorValue(layer.fill, "#e43d30")}" data-layer-action="point-layer-color" title="Choose polygon color" aria-label="Choose polygon color"></button>`
        : layer.type === "stripe"
          ? `<button class="layer-studio-layer-stroke-swatch" type="button" style="--layer-studio-layer-stroke: ${fillGradientForLayer(layer) ? gradientCssValue(fillGradientForLayer(layer)) : cssColorValue(layer.stroke, "#f07a00")}" data-layer-action="point-layer-color" title="Choose stripe color" aria-label="Choose stripe color"></button>`
        : isShapeLayerType(layer.type)
          ? `<button class="layer-studio-layer-stroke-swatch" type="button" style="--layer-studio-layer-stroke: ${cssColorValue(layer.fill, "#e43d30")}" data-layer-action="point-layer-color" title="Choose fill color" aria-label="Choose fill color"></button>`
          : "";
      const dragHandle = draggable
        ? `<button class="layer-studio-icon-button layer-studio-layer-drag-handle" type="button" draggable="false" data-layer-drag-handle title="Reorder layer" aria-label="Reorder layer">${LAYER_ACTION_ICONS.drag}</button>`
        : "";
      const controlsCompact = isUvOverlayLayerId(layer.id) || layer.id === BASE_COLOR_LAYER_ID || layer.id === ORIGINAL_BASE_TEXTURE_LAYER_ID ? " is-uv-overlay" : "";
      const controlsMarkup = layer.id === BASE_COLOR_LAYER_ID
        ? `
            ${layerActionButton("base-color-visible", layer.visible === false ? "Show base color" : "Hide base color", layer.visible === false ? LAYER_ACTION_ICONS.hidden : LAYER_ACTION_ICONS.visible)}
            ${layerActionButton("base-color-fill", "Choose base color", LAYER_ACTION_ICONS.fill)}
          `
        : layer.id === ORIGINAL_BASE_TEXTURE_LAYER_ID
        ? `${layerActionButton("locked", "Original texture is locked", LAYER_ACTION_ICONS.lock, true)}`
        : isUvOverlayLayerId(layer.id)
        ? `
            ${layerActionButton("visible", layer.visible === false ? "Show overlay" : "Hide overlay", layer.visible === false ? LAYER_ACTION_ICONS.hidden : LAYER_ACTION_ICONS.visible)}
            ${layerActionButton("uv-invert", layer.inverted ? "Show normal overlay" : "Invert overlay", LAYER_ACTION_ICONS.invert)}
          `
        : `
            ${isGroupLike ? layerActionButton("group-toggle", layer.collapsed ? "Expand group" : "Collapse group", layer.collapsed ? LAYER_ACTION_ICONS.collapse : LAYER_ACTION_ICONS.expand) : ""}
            ${layerActionButton("visible", layer.visible === false ? "Show layer" : "Hide layer", layer.visible === false ? LAYER_ACTION_ICONS.hidden : LAYER_ACTION_ICONS.visible)}
            ${layerActionButton("lock", layer.locked ? "Unlock layer" : "Lock layer", layer.locked ? LAYER_ACTION_ICONS.lock : LAYER_ACTION_ICONS.unlock, layer.virtual)}
            ${layerActionButton("edit", layer.type === "stripe" ? "Edit stripe points" : "Edit polygon points", LAYER_ACTION_ICONS.edit, layer.virtual || !isPointLayerType(layer.type) || layer.locked || layer.effectiveLocked || layer.visible === false || layer.effectiveVisible === false)}
            ${canCreateGroup && !layer.childRow && isLayerSelected(layer.id) ? layerActionButton("group", "Group selected layers", LAYER_ACTION_ICONS.group) : ""}
            ${canCreateMask && !layer.childRow && isLayerSelected(layer.id) ? layerActionButton("mask", "Create PNG mask", LAYER_ACTION_ICONS.mask) : ""}
            ${layerActionButton("duplicate", "Duplicate layer", LAYER_ACTION_ICONS.duplicate, layer.virtual || Boolean(layer.maskRole))}
            ${isGroup ? layerActionButton("ungroup", "Ungroup", LAYER_ACTION_ICONS.ungroup, layer.virtual) : ""}
            ${isMaskGroup ? layerActionButton("unmask", "Remove PNG mask", LAYER_ACTION_ICONS.unmask, layer.virtual) : ""}
            ${layerActionButton("delete", "Delete layer", LAYER_ACTION_ICONS.delete, layer.virtual || Boolean(layer.maskRole))}
          `;
      return `
        <div class="${classes}" role="button" tabindex="0" data-layer-id="${escapeHtml(layer.id)}" draggable="false">
          <div class="layer-studio-layer-main">
            <div class="layer-studio-layer-title-row">
              ${strokeSwatch}
              ${nameMarkup}
            </div>
            ${layer.compactOverlay ? "" : `<span>${escapeHtml(sourceLabel)}</span>`}
          </div>
          ${dragHandle}
          <div class="layer-studio-layer-controls${controlsCompact}">
            ${controlsMarkup}
          </div>
          ${layer.compactOverlay ? "" : `<span class="layer-studio-layer-flags">${escapeHtml(flags)}</span>`}
        </div>
      `;
    }).join("");
    scrollSelectedLayerIntoView();
  }

  function scrollSelectedLayerIntoView() {
    const host = $("layerStudioLayerList");
    const selectedId = cleanText(state.selectedLayerId || selectedLayerIds()[0]);
    if (!host || !selectedId) return;
    window.requestAnimationFrame(() => {
      const row = Array.from(host.querySelectorAll("[data-layer-id]"))
        .find((node) => node.dataset.layerId === selectedId);
      row?.scrollIntoView?.({ block: "nearest", inline: "nearest" });
    });
  }

  function updateLayerDragClasses() {
    const host = $("layerStudioLayerList");
    if (!host) return;
    host.querySelectorAll("[data-layer-id]").forEach((row) => {
      const layerId = row.dataset.layerId || "";
      row.classList.toggle("is-dragging", layerId === state.draggedLayerId);
      row.classList.toggle("is-drop-target", layerId === state.dragOverLayerId);
    });
  }

  function updateLayerHoverClasses() {
    const host = $("layerStudioLayerList");
    if (!host) return;
    host.querySelectorAll("[data-layer-id]").forEach((row) => {
      row.classList.toggle("is-hovered", (row.dataset.layerId || "") === state.hoveredLayerId);
    });
  }

  function setHoveredLayer(layerId) {
    const id = cleanText(layerId);
    const nextId = id && !isUvOverlayLayerId(id) && id !== BASE_COLOR_LAYER_ID && id !== ORIGINAL_BASE_TEXTURE_LAYER_ID && findLayer(id) ? id : "";
    if (state.hoveredLayerId === nextId) return;
    state.hoveredLayerId = nextId;
    updateLayerHoverClasses();
    renderLayerHoverBox();
  }

  function libraryScopeLabel(scope = state.libraryActiveScope) {
    return scope === "global" ? "My Library" : "Project Assets";
  }

  function activeLibraryScope() {
    return state.libraryActiveScope === "global" ? "global" : "project";
  }

  function libraryForScope(scope = activeLibraryScope()) {
    return scope === "global" ? state.assetLibrary : state.projectAssetLibrary;
  }

  function setLibraryForScope(scope, library) {
    if (scope === "global") state.assetLibrary = library || { assets: [] };
    else state.projectAssetLibrary = library || { assets: [] };
  }

  function libraryAssets(scope = activeLibraryScope()) {
    const library = libraryForScope(scope);
    return Array.isArray(library?.assets) ? library.assets : [];
  }

  function libraryGroups(scope = activeLibraryScope()) {
    const library = libraryForScope(scope);
    const groups = Array.isArray(library?.groups) ? library.groups : [];
    return groups.length ? groups : [{ id: "ungrouped", name: "Ungrouped", collapsed: false, order: 0 }];
  }

  function libraryAssetById(assetId, scope = activeLibraryScope()) {
    const id = cleanText(assetId);
    if (!id) return null;
    return libraryAssets(scope).find((asset) => asset.id === id) || null;
  }

  function layerAssetById(layer) {
    const sourceType = cleanText(layer?.sourceType, "project").toLowerCase().replace("_", "");
    if (sourceType === "projectlibrary") return libraryAssetById(layer?.source, "project");
    if (sourceType === "library") return libraryAssetById(layer?.source, "global") || libraryAssetById(layer?.source, "project");
    return null;
  }

  function libraryThumbnailCacheKey(asset) {
    const id = cleanText(asset?.id);
    const path = cleanText(asset?.path);
    const hash = cleanText(asset?.hash);
    return id && path ? `library-thumb|${id}|${hash || path}` : "";
  }

  function libraryGroupById(groupId) {
    const id = cleanText(groupId, "ungrouped");
    return libraryGroups().find((group) => group.id === id) || libraryGroups()[0] || null;
  }

  function isLayerPngMissing(layer) {
    if (!layer || layer.type !== "image") return false;
    const sourceType = cleanText(layer.sourceType, "project").toLowerCase().replace("_", "");
    if (sourceType !== "library" && sourceType !== "projectlibrary") return false;
    const asset = layerAssetById(layer);
    return !asset || asset.exists === false;
  }

  function setLibraryCollapsed(collapsed) {
    state.libraryCollapsed = Boolean(collapsed);
    const panel = $("layerStudioLibraryPanel");
    const toggle = $("layerStudioLibraryToggle");
    if (panel) panel.classList.toggle("is-collapsed", state.libraryCollapsed);
    panel?.parentElement?.classList.toggle("is-library-collapsed", state.libraryCollapsed);
    if (toggle) {
      toggle.setAttribute("aria-expanded", state.libraryCollapsed ? "false" : "true");
      toggle.title = state.libraryCollapsed ? "Open PNG library" : "Close PNG library";
      toggle.setAttribute("aria-label", toggle.title);
      toggle.innerHTML = state.libraryCollapsed
        ? '<span class="layer-studio-library-rail-label">PNG Library</span>'
        : PANEL_ICONS.right;
    }
    applyEditorPreviewSplit();
    resizeCanvas();
    window.LiveryLabPreview3d?.resize?.();
  }

  function refreshWorkspaceLayout() {
    applyEditorPreviewSplit();
    resizeCanvas();
    window.LiveryLabPreview3d?.resize?.();
    window.setTimeout(() => {
      applyEditorPreviewSplit();
      resizeCanvas();
      window.LiveryLabPreview3d?.resize?.();
    }, 180);
  }

  function clampNumber(value, min, max) {
    const numeric = Number(value);
    if (!Number.isFinite(numeric)) return min;
    return Math.min(Math.max(numeric, min), max);
  }

  function layerStudioBody() {
    return $("layerStudioLibraryPanel")?.parentElement || null;
  }

  function editorPreviewAvailableWidth() {
    const body = layerStudioBody();
    const layersPanel = body?.querySelector?.(".layer-studio-properties-panel");
    if (!body || !layersPanel) return 0;
    const libraryRailWidth = 52;
    const splitterWidth = state.editorCollapsed || state.previewCollapsed ? 0 : 7;
    return Math.floor(body.clientWidth - libraryRailWidth - layersPanel.offsetWidth - splitterWidth);
  }

  function editorPreviewMinWidth(totalWidth) {
    return Math.max(120, Math.min(280, Math.floor((totalWidth - 20) / 2)));
  }

  function setEditorPreviewSplitFromEditorWidth(editorWidth, totalWidth) {
    const total = Math.max(0, Math.floor(totalWidth || editorPreviewAvailableWidth()));
    if (!total) return;
    const minWidth = editorPreviewMinWidth(total);
    const nextEditorWidth = clampNumber(editorWidth, minWidth, total - minWidth);
    state.editorPreviewSplit = {
      ratio: nextEditorWidth / total,
    };
    applyEditorPreviewSplit();
  }

  function applyEditorPreviewSplit() {
    const body = layerStudioBody();
    if (!body) return;
    if (state.editorCollapsed || state.previewCollapsed || !state.editorPreviewSplit) {
      body.style.removeProperty("--layer-studio-editor-col");
      body.style.removeProperty("--layer-studio-preview-col");
      updateEditorPreviewSplitterA11y();
      return;
    }
    const total = editorPreviewAvailableWidth();
    if (total <= 0) return;
    const minWidth = editorPreviewMinWidth(total);
    const minRatio = total > 0 ? minWidth / total : 0.16;
    const ratio = clampNumber(state.editorPreviewSplit.ratio, minRatio, 1 - minRatio);
    const editorWidth = Math.round(total * ratio);
    const editorFr = Math.max(0.001, ratio);
    const previewFr = Math.max(0.001, 1 - ratio);
    body.style.setProperty("--layer-studio-editor-col", `minmax(0, ${editorFr}fr)`);
    body.style.setProperty("--layer-studio-preview-col", `minmax(0, ${previewFr}fr)`);
    updateEditorPreviewSplitterA11y(editorWidth, total);
  }

  function updateEditorPreviewSplitterA11y(editorWidth, totalWidth) {
    const splitter = $("layerStudioViewSplitter");
    if (!splitter) return;
    const total = totalWidth || editorPreviewAvailableWidth();
    const minWidth = editorPreviewMinWidth(total);
    splitter.disabled = state.editorCollapsed || state.previewCollapsed;
    splitter.setAttribute("aria-disabled", splitter.disabled ? "true" : "false");
    if (!total || splitter.disabled) {
      splitter.removeAttribute("aria-valuemin");
      splitter.removeAttribute("aria-valuemax");
      splitter.removeAttribute("aria-valuenow");
      return;
    }
    const currentEditorWidth = editorWidth || $("layerStudioEditorPanel")?.getBoundingClientRect?.().width || Math.round(total / 2);
    splitter.setAttribute("aria-valuemin", String(minWidth));
    splitter.setAttribute("aria-valuemax", String(Math.max(minWidth, total - minWidth)));
    splitter.setAttribute("aria-valuenow", String(Math.round(currentEditorWidth)));
  }

  function refreshEditorPreviewSplitLayout() {
    applyEditorPreviewSplit();
    resizeCanvas();
    window.LiveryLabPreview3d?.resize?.();
  }

  function startEditorPreviewSplitDrag(event) {
    if (event.button !== 0 || state.editorCollapsed || state.previewCollapsed) return;
    const editorPanel = $("layerStudioEditorPanel");
    const previewPanel = $("layerStudioPreviewPanel");
    const total = editorPreviewAvailableWidth();
    if (!editorPanel || !previewPanel || total <= 0) return;
    event.preventDefault();
    const editorWidth = editorPanel.getBoundingClientRect().width;
    state.editorPreviewSplitDrag = {
      startX: event.clientX,
      startEditorWidth: editorWidth,
      totalWidth: total,
    };
    const body = layerStudioBody();
    body?.classList.add("is-view-resizing");
    $("layerStudioViewSplitter")?.classList.add("is-active");
    document.addEventListener("pointermove", handleEditorPreviewSplitDrag);
    document.addEventListener("pointerup", stopEditorPreviewSplitDrag, { once: true });
    document.addEventListener("pointercancel", stopEditorPreviewSplitDrag, { once: true });
  }

  function handleEditorPreviewSplitDrag(event) {
    const drag = state.editorPreviewSplitDrag;
    if (!drag) return;
    event.preventDefault();
    setEditorPreviewSplitFromEditorWidth(drag.startEditorWidth + event.clientX - drag.startX, drag.totalWidth);
    resizeCanvas();
    window.LiveryLabPreview3d?.resize?.();
  }

  function stopEditorPreviewSplitDrag() {
    state.editorPreviewSplitDrag = null;
    layerStudioBody()?.classList.remove("is-view-resizing");
    $("layerStudioViewSplitter")?.classList.remove("is-active");
    document.removeEventListener("pointermove", handleEditorPreviewSplitDrag);
  }

  function nudgeEditorPreviewSplit(delta) {
    if (state.editorCollapsed || state.previewCollapsed) return;
    const editorWidth = $("layerStudioEditorPanel")?.getBoundingClientRect?.().width || 0;
    const total = editorPreviewAvailableWidth();
    if (!editorWidth || !total) return;
    setEditorPreviewSplitFromEditorWidth(editorWidth + delta, total);
    resizeCanvas();
    window.LiveryLabPreview3d?.resize?.();
  }

  function setWorkspacePanelCollapsed(panelName, collapsed) {
    const target = cleanText(panelName);
    const shouldCollapse = Boolean(collapsed);
    if (target === "editor") {
      state.editorCollapsed = shouldCollapse;
      if (shouldCollapse) state.previewCollapsed = false;
    } else if (target === "preview") {
      state.previewCollapsed = shouldCollapse;
      if (shouldCollapse) state.editorCollapsed = false;
    } else {
      return;
    }
    const body = $("layerStudioLibraryPanel")?.parentElement;
    const editorPanel = $("layerStudioEditorPanel");
    const previewPanel = $("layerStudioPreviewPanel");
    const editorToggle = $("layerStudioEditorCollapse");
    const previewToggle = $("layerStudioPreviewCollapse");
    body?.classList.toggle("is-editor-collapsed", state.editorCollapsed);
    body?.classList.toggle("is-preview-collapsed", state.previewCollapsed);
    editorPanel?.classList.toggle("is-collapsed-horizontal", state.editorCollapsed);
    previewPanel?.classList.toggle("is-collapsed-horizontal", state.previewCollapsed);
    if (editorToggle) {
      editorToggle.setAttribute("aria-expanded", state.editorCollapsed ? "false" : "true");
      editorToggle.title = state.editorCollapsed ? "Expand 2D editor" : "Collapse 2D editor";
      editorToggle.setAttribute("aria-label", editorToggle.title);
      editorToggle.innerHTML = state.editorCollapsed ? PANEL_ICONS.right : PANEL_ICONS.left;
    }
    if (previewToggle) {
      previewToggle.setAttribute("aria-expanded", state.previewCollapsed ? "false" : "true");
      previewToggle.title = state.previewCollapsed ? "Expand 3D preview" : "Collapse 3D preview";
      previewToggle.setAttribute("aria-label", previewToggle.title);
      previewToggle.innerHTML = state.previewCollapsed ? PANEL_ICONS.left : PANEL_ICONS.right;
    }
    refreshWorkspaceLayout();
  }

  function renderAssetLibrary() {
    const host = $("layerStudioLibraryList");
    const status = $("layerStudioLibraryStatus");
    if (!host) return;
    syncLibraryScopeTabs();
    const assets = libraryAssets();
    const groups = libraryGroups();
    const query = cleanText(state.librarySearchQuery).toLowerCase();
    const matchesSearch = (asset) => {
      if (!query) return true;
      return [
        asset?.name,
        asset?.id,
        asset?.file,
        asset?.path,
      ].some((value) => cleanText(value).toLowerCase().includes(query));
    };
    const visibleAssets = query ? assets.filter(matchesSearch) : assets;
    const visibleGroups = query
      ? groups.filter((group) => visibleAssets.some((asset) => cleanText(asset.groupId, "ungrouped") === group.id))
      : groups;
    if (status) {
      status.textContent = state.libraryLoading
        ? "Loading..."
        : query
          ? `${libraryScopeLabel()}: ${visibleAssets.length} of ${assets.length} PNG asset${assets.length === 1 ? "" : "s"}`
          : `${libraryScopeLabel()}: ${assets.length} PNG asset${assets.length === 1 ? "" : "s"} / ${groups.length} group${groups.length === 1 ? "" : "s"}`;
    }
    if (state.libraryLoading) {
      host.innerHTML = `<div class="layer-studio-layer-empty">Loading ${escapeHtml(libraryScopeLabel())}...</div>`;
      return;
    }
    if (!assets.length && groups.length <= 1) {
      host.innerHTML = `<div class="layer-studio-layer-empty">${activeLibraryScope() === "global" ? "No shared PNG assets yet" : "No project PNG assets yet"}</div>`;
      return;
    }
    if (query && !visibleAssets.length) {
      host.innerHTML = `<div class="layer-studio-layer-empty">No PNGs match this search</div>`;
      return;
    }
    host.innerHTML = visibleGroups.map((group) => {
      const groupAssets = visibleAssets.filter((asset) => cleanText(asset.groupId, "ungrouped") === group.id);
      const collapsed = query ? false : group.collapsed === true;
      const groupClasses = [
        "layer-studio-library-group",
        collapsed ? "is-collapsed" : "",
        group.id === state.dragOverLibraryGroupId ? "is-drop-target" : "",
      ].filter(Boolean).join(" ");
      return `
        <section class="${groupClasses}" data-library-group-id="${escapeHtml(group.id)}">
          <div class="layer-studio-library-group-head">
            <button class="layer-studio-icon-button" type="button" data-library-group-action="toggle" title="${collapsed ? "Expand group" : "Collapse group"}" aria-label="${collapsed ? "Expand group" : "Collapse group"}">${collapsed ? LAYER_ACTION_ICONS.collapse : LAYER_ACTION_ICONS.expand}</button>
            <strong>${escapeHtml(group.name || group.id)}</strong>
            <span>${groupAssets.length}</span>
            <button class="layer-studio-icon-button" type="button" data-library-group-action="rename" title="Rename group" aria-label="Rename group" ${group.id === "ungrouped" ? "disabled" : ""}>${LAYER_ACTION_ICONS.edit}</button>
            <button class="layer-studio-icon-button" type="button" data-library-group-action="delete" title="Delete group" aria-label="Delete group" ${group.id === "ungrouped" ? "disabled" : ""}>${LAYER_ACTION_ICONS.delete}</button>
          </div>
          <div class="layer-studio-library-group-body">
            ${groupAssets.length ? groupAssets.map((asset) => {
              const exists = asset.exists !== false;
              const assetClasses = [
                "layer-studio-library-item",
                exists ? "" : "is-missing",
                asset.id === state.draggedLibraryAssetId ? "is-dragging" : "",
              ].filter(Boolean).join(" ");
              const meta = [
                `${Number(asset.width || 0) || "-"} x ${Number(asset.height || 0) || "-"}`,
                formatFileSize(asset.size),
                exists ? "" : "missing",
              ].filter(Boolean).join(" | ");
              const thumbKey = libraryThumbnailCacheKey(asset);
              const thumbSrc = thumbKey ? state.libraryThumbnailCache.get(thumbKey) || "" : "";
              return `
                <div class="${assetClasses}" data-library-asset-id="${escapeHtml(asset.id)}" draggable="false">
                  <button class="layer-studio-icon-button layer-studio-library-drag-handle" type="button" draggable="false" data-library-drag-handle title="Move PNG to group" aria-label="Move PNG to group">${LAYER_ACTION_ICONS.drag}</button>
                  <div class="layer-studio-library-thumb">
                    ${exists ? `<img src="${escapeHtml(thumbSrc)}" alt="" data-library-preview="${escapeHtml(asset.id)}" />` : `<span>Missing</span>`}
                  </div>
                  <div class="layer-studio-library-main">
                    <strong>${escapeHtml(asset.name || asset.id)}</strong>
                    <span>${escapeHtml(meta)}</span>
                  </div>
                  <div class="layer-studio-library-item-actions">
                    <button class="layer-studio-icon-button" type="button" data-library-action="delete" title="Delete PNG" aria-label="Delete PNG">${LAYER_ACTION_ICONS.delete}</button>
                    <button class="icon-text" type="button" data-library-action="insert" ${exists ? "" : "disabled"}>${iconHtml("plus")}Place PNG</button>
                  </div>
                </div>
              `;
            }).join("") : `<div class="layer-studio-layer-empty">No PNGs in this group</div>`}
          </div>
        </section>
      `;
    }).join("");
    loadLibraryThumbnails();
  }

  function libraryGroupElement(groupId) {
    const host = $("layerStudioLibraryList");
    const id = cleanText(groupId, "ungrouped");
    if (!host || !id) return null;
    return Array.from(host.querySelectorAll("[data-library-group-id]"))
      .find((group) => (group.dataset.libraryGroupId || "") === id) || null;
  }

  function libraryGroupBodyElement(groupId) {
    return libraryGroupElement(groupId)?.querySelector(".layer-studio-library-group-body") || null;
  }

  function updateLibraryGroupCounts() {
    const query = cleanText(state.librarySearchQuery).toLowerCase();
    libraryGroups().forEach((group) => {
      const groupNode = libraryGroupElement(group.id);
      const countNode = groupNode?.querySelector(".layer-studio-library-group-head span");
      if (!countNode) return;
      const count = libraryAssets().filter((asset) => {
        if (cleanText(asset.groupId, "ungrouped") !== group.id) return false;
        if (!query) return true;
        return [
          asset?.name,
          asset?.id,
          asset?.file,
          asset?.path,
        ].some((value) => cleanText(value).toLowerCase().includes(query));
      }).length;
      countNode.textContent = String(count);
    });
  }

  function syncLibraryScopeTabs() {
    document.querySelectorAll("[data-library-scope]").forEach((button) => {
      const active = (button.dataset.libraryScope || "project") === activeLibraryScope();
      button.classList.toggle("is-active", active);
      button.setAttribute("aria-selected", active ? "true" : "false");
    });
  }

  async function setLibraryScope(scope) {
    const nextScope = scope === "global" ? "global" : "project";
    if (state.libraryActiveScope === nextScope) {
      renderAssetLibrary();
      return true;
    }
    state.libraryActiveScope = nextScope;
    state.librarySearchQuery = "";
    const search = $("layerStudioLibrarySearch");
    if (search) search.value = "";
    renderAssetLibrary();
    return await loadActiveAssetLibrary({ silent: true });
  }

  function isLayerPointerReorderCandidate(layerId) {
    const id = cleanText(layerId);
    if (!id || isUvOverlayLayerId(id) || id === BASE_COLOR_LAYER_ID) return false;
    const layer = findLayer(id);
    return Boolean(layer && !layer.virtual && !layer.childRow);
  }

  function layerPointerDropTargetFromY(clientY) {
    const host = $("layerStudioLayerList");
    if (!host || !state.layerPointerDrag?.sourceId) return "";
    const rows = Array.from(host.querySelectorAll("[data-layer-id]"))
      .filter((row) => isLayerPointerReorderCandidate(row.dataset.layerId));
    let targetId = "";
    let closestDistance = Number.POSITIVE_INFINITY;
    rows.forEach((row) => {
      const layerId = cleanText(row.dataset.layerId);
      if (!layerId || layerId === state.layerPointerDrag.sourceId) return;
      const rect = row.getBoundingClientRect();
      const middleY = rect.top + (rect.height / 2);
      const distance = Math.abs(clientY - middleY);
      if (distance < closestDistance) {
        closestDistance = distance;
        targetId = layerId;
      }
    });
    return targetId;
  }

  function refreshLayerReorderUi() {
    normalizeLayerOrders();
    syncSelectionState();
    renderLayerList();
    renderPropertiesPanel();
    updateTransformerForSelection();
    renderMultiSelectionBox();
    renderLayerHoverBox();
    updateCanvasStatus();
  }

  function clearLayerPointerDrag() {
    state.layerPointerDrag = null;
    state.draggedLayerId = "";
    state.dragOverLayerId = "";
    window.removeEventListener("pointermove", handleLayerPointerDragMove, true);
    window.removeEventListener("pointerup", handleLayerPointerDragEnd, true);
    window.removeEventListener("pointercancel", handleLayerPointerDragCancel, true);
    window.removeEventListener("keydown", handleLayerPointerDragKeydown, true);
    updateLayerDragClasses();
  }

  function handleLayerPointerDragMove(event) {
    const drag = state.layerPointerDrag;
    if (!drag || event.pointerId !== drag.pointerId) return;
    event.preventDefault();
    const distance = Math.hypot(event.clientX - drag.startX, event.clientY - drag.startY);
    if (!drag.hasMoved && distance < 4) return;
    drag.hasMoved = true;
    const targetId = layerPointerDropTargetFromY(event.clientY);
    if (state.dragOverLayerId !== targetId) {
      state.dragOverLayerId = targetId;
      updateLayerDragClasses();
    }
  }

  function handleLayerPointerDragEnd(event) {
    const drag = state.layerPointerDrag;
    if (!drag || event.pointerId !== drag.pointerId) return;
    event.preventDefault();
    const sourceId = drag.sourceId;
    const targetId = drag.hasMoved ? (state.dragOverLayerId || layerPointerDropTargetFromY(event.clientY)) : "";
    clearLayerPointerDrag();
    if (targetId && reorderLayerByDrop(sourceId, targetId)) {
      refreshLayerReorderUi();
    } else {
      renderLayerList();
    }
  }

  function handleLayerPointerDragCancel(event) {
    const drag = state.layerPointerDrag;
    if (!drag || event.pointerId !== drag.pointerId) return;
    event.preventDefault();
    clearLayerPointerDrag();
  }

  function handleLayerPointerDragKeydown(event) {
    if (event.key !== "Escape" || !state.layerPointerDrag) return;
    event.preventDefault();
    clearLayerPointerDrag();
  }

  function syncLibraryGroupEmptyState(groupId) {
    const body = libraryGroupBodyElement(groupId);
    if (!body) return;
    const hasItems = Boolean(body.querySelector("[data-library-asset-id]"));
    const emptyNode = body.querySelector(".layer-studio-layer-empty");
    if (hasItems) {
      emptyNode?.remove();
    } else if (!emptyNode) {
      body.insertAdjacentHTML("beforeend", `<div class="layer-studio-layer-empty">No PNGs in this group</div>`);
    }
  }

  function moveLibraryAssetRow(assetId, previousGroupId, targetGroupId) {
    const host = $("layerStudioLibraryList");
    const id = cleanText(assetId);
    const row = Array.from(host?.querySelectorAll("[data-library-asset-id]") || [])
      .find((node) => (node.dataset.libraryAssetId || "") === id);
    const targetBody = libraryGroupBodyElement(targetGroupId);
    if (!row || !targetBody) return false;
    const targetEmptyNode = targetBody.querySelector(".layer-studio-layer-empty");
    targetEmptyNode?.remove();
    row.classList.remove("is-dragging");
    targetBody.appendChild(row);
    syncLibraryGroupEmptyState(previousGroupId);
    syncLibraryGroupEmptyState(targetGroupId);
    updateLibraryGroupCounts();
    updateLibraryDragClasses();
    return true;
  }

  function updateLibraryDragClasses() {
    const host = $("layerStudioLibraryList");
    if (!host) return;
    host.querySelectorAll("[data-library-asset-id]").forEach((row) => {
      row.classList.toggle("is-dragging", (row.dataset.libraryAssetId || "") === state.draggedLibraryAssetId);
    });
    host.querySelectorAll("[data-library-group-id]").forEach((group) => {
      group.classList.toggle("is-drop-target", (group.dataset.libraryGroupId || "") === state.dragOverLibraryGroupId);
    });
  }

  function libraryPointerDropTargetFromPoint(clientX, clientY) {
    const host = $("layerStudioLibraryList");
    if (!host || !state.libraryPointerDrag?.sourceId) return "";
    const target = document.elementFromPoint(clientX, clientY)?.closest?.("[data-library-group-id]");
    if (!target || !host.contains(target)) return "";
    const groupId = cleanText(target.dataset.libraryGroupId, "ungrouped");
    const asset = libraryAssetById(state.libraryPointerDrag.sourceId);
    if (!asset || !groupId || cleanText(asset.groupId, "ungrouped") === groupId) return "";
    return groupId;
  }

  function clearLibraryPointerDrag() {
    state.libraryPointerDrag = null;
    state.draggedLibraryAssetId = "";
    state.dragOverLibraryGroupId = "";
    window.removeEventListener("pointermove", handleLibraryPointerDragMove, true);
    window.removeEventListener("pointerup", handleLibraryPointerDragEnd, true);
    window.removeEventListener("pointercancel", handleLibraryPointerDragCancel, true);
    window.removeEventListener("keydown", handleLibraryPointerDragKeydown, true);
    updateLibraryDragClasses();
  }

  function handleLibraryPointerDragMove(event) {
    const drag = state.libraryPointerDrag;
    if (!drag || event.pointerId !== drag.pointerId) return;
    event.preventDefault();
    const distance = Math.hypot(event.clientX - drag.startX, event.clientY - drag.startY);
    if (!drag.hasMoved && distance < 4) return;
    drag.hasMoved = true;
    const targetGroupId = libraryPointerDropTargetFromPoint(event.clientX, event.clientY);
    if (state.dragOverLibraryGroupId !== targetGroupId) {
      state.dragOverLibraryGroupId = targetGroupId;
      updateLibraryDragClasses();
    }
  }

  function handleLibraryPointerDragEnd(event) {
    const drag = state.libraryPointerDrag;
    if (!drag || event.pointerId !== drag.pointerId) return;
    event.preventDefault();
    const sourceId = drag.sourceId;
    const targetGroupId = drag.hasMoved
      ? (state.dragOverLibraryGroupId || libraryPointerDropTargetFromPoint(event.clientX, event.clientY))
      : "";
    clearLibraryPointerDrag();
    if (!sourceId || !targetGroupId) return;
    moveLibraryAsset(sourceId, targetGroupId)
      .finally(() => updateLibraryDragClasses());
  }

  function handleLibraryPointerDragCancel(event) {
    const drag = state.libraryPointerDrag;
    if (!drag || event.pointerId !== drag.pointerId) return;
    event.preventDefault();
    clearLibraryPointerDrag();
  }

  function handleLibraryPointerDragKeydown(event) {
    if (event.key !== "Escape" || !state.libraryPointerDrag) return;
    event.preventDefault();
    clearLibraryPointerDrag();
  }

  async function loadAssetLibrary(options = {}) {
    if (!window.pywebview?.api?.list_layer_studio_asset_library) {
      setText("layerStudioLibraryStatus", "Library API unavailable");
      return false;
    }
    state.libraryLoading = true;
    renderAssetLibrary();
    try {
      const result = await window.pywebview.api.list_layer_studio_asset_library();
      if (!result?.ok) {
        setText("layerStudioLibraryStatus", result?.message || "Library failed");
        return false;
      }
      state.assetLibrary = result.data || { assets: [] };
      renderAssetLibrary();
      if (!options.silent) app().showResult?.(result, "Layer Studio library loaded", "list_layer_studio_asset_library");
      return true;
    } catch (error) {
      const message = error?.message || String(error);
      setText("layerStudioLibraryStatus", "Library failed");
      setText("layerStudioEditorStatus", `Layer Studio library failed:\n${message}`);
      app().log?.(message, true);
      return false;
    } finally {
      state.libraryLoading = false;
      renderAssetLibrary();
    }
  }

  async function loadLibraryThumbnails() {
    if (!window.pywebview?.api?.load_image_preview) return;
    const images = Array.from(document.querySelectorAll("[data-library-preview]"));
    for (const image of images) {
      const asset = libraryAssetById(image.dataset.libraryPreview);
      const path = cleanText(asset?.path);
      if (!path) continue;
      const cacheKey = libraryThumbnailCacheKey(asset);
      if (!cacheKey) continue;
      try {
        if (state.libraryThumbnailCache.has(cacheKey)) {
          image.src = state.libraryThumbnailCache.get(cacheKey) || "";
          continue;
        }
        if (!state.imageCache.has(cacheKey)) {
          state.imageCache.set(cacheKey, (async () => {
            const result = await window.pywebview.api.load_image_preview(path, `layer-studio-library-${asset.id}`);
            if (!result?.ok || !(result.data?.data_url || result.data?.dataUrl)) return "";
            return result.data.data_url || result.data.dataUrl;
          })());
        }
        const dataUrl = await state.imageCache.get(cacheKey);
        if (dataUrl) {
          state.libraryThumbnailCache.set(cacheKey, dataUrl);
          image.src = dataUrl;
        }
      } catch (error) {
        state.imageCache.delete(cacheKey);
        state.libraryThumbnailCache.delete(cacheKey);
      }
    }
  }

  function renderFontPresetOptions() {
    const fontSelect = $("layerStudioPropFontFamily");
    if (!fontSelect) return;
    const presets = fontPresets();
    const options = presets.length ? presets : [defaultFontPreset()];
    const categories = Array.isArray(window.LiveryLabLayerStudioFonts?.categories)
      ? window.LiveryLabLayerStudioFonts.categories
      : [];
    const categoryMap = new Map(categories.map((category) => [category.id, category]));
    const grouped = new Map();
    options.forEach((preset) => {
      const categoryId = cleanText(preset.category, "default");
      if (!grouped.has(categoryId)) grouped.set(categoryId, []);
      grouped.get(categoryId).push(preset);
    });
    const categoryOrder = [
      ...categories.map((category) => category.id),
      ...Array.from(grouped.keys()).filter((id) => !categoryMap.has(id)),
    ];
    fontSelect.innerHTML = categoryOrder
      .filter((categoryId) => grouped.has(categoryId))
      .map((categoryId) => {
        const category = categoryMap.get(categoryId) || { label: "Bundled Fonts" };
        const fontOptions = grouped.get(categoryId).map((preset) => (
          `<option value="${escapeHtml(preset.family)}">${escapeHtml(preset.label || preset.family)}</option>`
        )).join("");
        return `<optgroup label="${escapeHtml(category.label)}">${fontOptions}</optgroup>`;
      })
      .join("");
  }

  function renderPropertiesPanel() {
    const selected = selectedLayers();
    const multiSelected = selected.length > 1;
    const layer = multiSelected ? null : primarySelectedLayer();
    const disabled = !layer || multiSelected;
    const renderedLayerId = layer?.id || "";
    const form = $("layerStudioPropertiesForm");
    form?.classList.toggle("is-collapsed", !layer);
    form?.setAttribute("aria-hidden", layer ? "false" : "true");
    const splatMode = isSplatMapSurface();
    const isTextLayer = isTextLayerType(layer?.type);
    const supportsGradientFill = Boolean(!splatMode && layer && !multiSelected && !layer.locked && (layer.type === "polygon" || layer.type === "stripe" || isShapeLayerType(layer.type)));
    const fillGradient = fillGradientForLayer(layer);
    const fillMode = fillGradient?.type || "solid";
    if (!supportsGradientFill || fillMode === "solid") state.gradientPopoverOpen = false;
    const nameWrap = $("layerStudioPropNameWrap");
    if (nameWrap) nameWrap.hidden = Boolean(isTextLayer);
    ["layerStudioPropTextWrap", "layerStudioPropFontFamilyWrap", "layerStudioPropFontWeightWrap", "layerStudioPropFontSizeWrap"].forEach((id) => {
      const wrap = $(id);
      if (wrap) wrap.hidden = !isTextLayer;
    });
    const fillModeWrap = $("layerStudioPropFillModeWrap");
    if (fillModeWrap) fillModeWrap.hidden = !supportsGradientFill;
    const fillWrap = $("layerStudioPropFillWrap");
    if (fillWrap) fillWrap.hidden = Boolean(splatMode || (supportsGradientFill && fillMode !== "solid"));
    renderSplatMapPalette(layer);
    const splatLegend = $("layerStudioSplatLegend");
    const splatPalette = $("layerStudioSplatPalette");
    if (splatLegend) splatLegend.hidden = !splatMode || !layer || splatPalette?.hidden !== false;
    const gradientControl = $("layerStudioGradientControl");
    const gradientToggle = $("layerStudioGradientToggle");
    const gradientPopover = $("layerStudioGradientPopover");
    const gradientVisible = supportsGradientFill && fillMode !== "solid";
    if (gradientControl) gradientControl.hidden = !gradientVisible;
    if (gradientToggle) {
      gradientToggle.disabled = !gradientVisible;
      gradientToggle.setAttribute("aria-expanded", state.gradientPopoverOpen && gradientVisible ? "true" : "false");
    }
    if (gradientPopover) gradientPopover.hidden = !gradientVisible || !state.gradientPopoverOpen;
    const gradientPreviewStyle = gradientCssValue(fillGradient || defaultFillGradient(fillMode));
    ["layerStudioGradientPreview", "layerStudioGradientTogglePreview"].forEach((id) => {
      const node = $(id);
      if (node) node.style.setProperty("--layer-studio-gradient-preview", gradientPreviewStyle);
    });
    const angleWrap = $("layerStudioPropGradientAngleWrap");
    if (angleWrap) angleWrap.hidden = fillMode !== "linear";
    setText("layerStudioPropWidthLabel", layer?.type === "stripe" ? "Stroke width" : "Width");
    setText("layerStudioPropFillLabel", splatMode ? "Mask" : layer?.type === "stripe" ? "Stroke" : "Fill");
    setText("layerStudioPropFillModeLabel", layer?.type === "stripe" ? "Stroke mode" : "Fill mode");
    setText("layerStudioGradientToggleLabel", "Gradient");
    $("layerStudioPropFillWrap")?.classList.toggle("is-stroke-control", layer?.type === "stripe");
    const tensionWrap = $("layerStudioPropTensionWrap");
    if (tensionWrap) tensionWrap.hidden = layer?.type !== "stripe";
    const ratioButton = $("layerStudioPropRatioLock");
    const ratioSupported = !disabled && supportsRatioLock(layer);
    if (ratioButton) {
      ratioButton.hidden = !ratioSupported;
      ratioButton.disabled = !ratioSupported;
      const ratioLocked = layer?.ratioLocked !== false;
      ratioButton.classList.toggle("is-active", ratioLocked);
      ratioButton.setAttribute("aria-pressed", ratioLocked ? "true" : "false");
      ratioButton.title = ratioLocked ? "Width and height are linked" : "Width and height are unlinked";
      ratioButton.setAttribute("aria-label", ratioButton.title);
    }
    renderFontPresetOptions();
    state.suppressPropertyEvents = true;
    const values = {
      layerStudioPropName: layer?.name || "",
      layerStudioPropText: layer?.type === "text" ? layer.text || "" : "",
      layerStudioPropFontFamily: layer?.type === "text" ? layer.fontFamily || defaultFontPreset().family : defaultFontPreset().family,
      layerStudioPropFontWeight: layer?.type === "text" ? String(fontWeightValue(layer.fontWeight, 700)) : "700",
      layerStudioPropFontSize: layer?.type === "text" ? intValue(layer.fontSize, 96, 4) : "",
      layerStudioPropX: layer ? (isPointLayerType(layer.type) ? polygonBounds(layer.points).x : layer.x ?? "") : "",
      layerStudioPropY: layer ? (isPointLayerType(layer.type) ? polygonBounds(layer.points).y : layer.y ?? "") : "",
      layerStudioPropWidth: layer ? (layer.type === "stripe" ? intValue(layer.strokeWidth, 20, 1) : isPointLayerType(layer.type) ? polygonBounds(layer.points).width : layer.width ?? "") : "",
      layerStudioPropHeight: layer ? (isPointLayerType(layer.type) ? polygonBounds(layer.points).height : layer.height ?? "") : "",
      layerStudioPropRotation: layer?.type === "group" || isPngMaskGroupLayer(layer) || isPointLayerType(layer?.type) ? "0" : layer ? Number(layer.rotation || 0).toFixed(1).replace(/\.0$/, "") : "",
      layerStudioPropOpacity: layer ? Number(layer.opacity ?? 1).toFixed(2).replace(/0$/, "").replace(/\.$/, "") : "",
      layerStudioPropTension: layer?.type === "stripe" ? Number(layer.tension ?? 0).toFixed(2).replace(/0$/, "").replace(/\.$/, "") : "",
      layerStudioPropFill: splatMode
        ? normalizeSplatMapColor(layer?.type === "stripe" ? layer.stroke : layer?.fill, activeSplatMapColor())
        : layer?.type === "stripe" ? layer.stroke || "#f07a00" : layer?.fill || "#e43d30",
      layerStudioPropFillMode: fillMode,
      layerStudioPropGradientColorA: fillGradient?.colorA || "#000000",
      layerStudioPropGradientAlphaA: Number(fillGradient?.alphaA ?? 1).toFixed(2).replace(/0$/, "").replace(/\.$/, ""),
      layerStudioPropGradientColorB: fillGradient?.colorB || "#ffffff",
      layerStudioPropGradientAlphaB: Number(fillGradient?.alphaB ?? 1).toFixed(2).replace(/0$/, "").replace(/\.$/, ""),
      layerStudioPropGradientAngle: Number(fillGradient?.angle ?? 90).toFixed(0),
    };
    Object.entries(values).forEach(([id, value]) => {
      const input = $(id);
      if (!input) return;
      input.dataset.layerId = renderedLayerId;
      input.value = value;
      const groupReadOnly = (layer?.type === "group" || isPngMaskGroupLayer(layer)) && [
        "layerStudioPropX",
        "layerStudioPropY",
        "layerStudioPropWidth",
        "layerStudioPropHeight",
        "layerStudioPropOpacity",
        "layerStudioPropTension",
        "layerStudioPropFill",
        "layerStudioPropText",
        "layerStudioPropFontFamily",
        "layerStudioPropFontWeight",
        "layerStudioPropFontSize",
      ].includes(id);
      const pointReadOnly = isPointLayerType(layer?.type) && id === "layerStudioPropHeight";
      const polygonWidthReadOnly = layer?.type === "polygon" && id === "layerStudioPropWidth";
      const supportsText = !splatMode && isTextLayerType(layer?.type);
      const textOnly = ["layerStudioPropText", "layerStudioPropFontFamily", "layerStudioPropFontWeight", "layerStudioPropFontSize"].includes(id);
      const stripeOnly = id === "layerStudioPropTension";
      const gradientOnly = [
        "layerStudioPropFillMode",
        "layerStudioPropGradientColorA",
        "layerStudioPropGradientAlphaA",
        "layerStudioPropGradientColorB",
        "layerStudioPropGradientAlphaB",
        "layerStudioPropGradientAngle",
      ].includes(id);
      const supportsFill = !splatMode && (isPointLayerType(layer?.type) || isShapeLayerType(layer?.type) || supportsText);
      const imageOnly = !supportsFill && id === "layerStudioPropFill";
      input.disabled = disabled || groupReadOnly || imageOnly || (gradientOnly && !supportsGradientFill) || (textOnly && !supportsText) || (stripeOnly && layer?.type !== "stripe") || pointReadOnly || polygonWidthReadOnly || (isTextLayer && id === "layerStudioPropName") || (layer?.locked && id !== "layerStudioPropName");
    });
    state.suppressPropertyEvents = false;
    setText(
      "layerStudioSelectionStatus",
      multiSelected
        ? `${selected.length} layers selected`
        : layer
        ? `${layer.name || layer.id}${layer.type === "group" ? ` / ${(layer.children || []).length} layers` : ""}${isPngMaskGroupLayer(layer) ? " / PNG mask" : ""}${layer.locked ? " / locked" : ""}${layer.visible === false ? " / hidden" : ""}`
        : "No layer selected",
    );
  }

  function updateTransformerForSelection() {
    const transformer = state.konva?.transformer;
    if (!transformer) return;
    const selected = selectedLayers();
    const layer = selected.length === 1 ? selected[0] : null;
    transformer.shiftBehavior?.("default");
    if (isPngMaskGroupLayer(layer)) {
      hideGroupTransformBox();
      hidePointTransformBox();
      transformer.nodes([]);
      transformer.getLayer()?.batchDraw?.();
      renderMultiSelectionBox();
      renderLayerHoverBox();
      return;
    }
    if (layer?.type === "group") {
      hidePointTransformBox();
      const box = updateGroupTransformBox(layer);
      transformer.keepRatio(true);
      transformer.enabledAnchors(["top-left", "top-right", "bottom-left", "bottom-right"]);
      transformer.nodes(box ? [box] : []);
      transformer.getLayer()?.batchDraw?.();
      renderMultiSelectionBox();
      renderLayerHoverBox();
      return;
    }
    hideGroupTransformBox();
    if (layer && isPointLayerType(layer.type) && isLayerCanvasSelectable(layer)) {
      const box = updatePointTransformBox(layer);
      transformer.keepRatio(false);
      transformer.enabledAnchors(["top-left", "top-right", "bottom-left", "bottom-right", "middle-left", "middle-right", "top-center", "bottom-center"]);
      transformer.nodes(box ? [box] : []);
      transformer.getLayer()?.batchDraw?.();
      renderMultiSelectionBox();
      renderLayerHoverBox();
      return;
    }
    hidePointTransformBox();
    const node = layer && !isPointLayerType(layer.type) && isLayerCanvasSelectable(layer) ? layerNode(layer.id) : null;
    const scaleBehavior = layerTransformerScaleBehavior(layer);
    transformer.enabledAnchors(["top-left", "top-right", "bottom-left", "bottom-right", "middle-left", "middle-right", "top-center", "bottom-center"]);
    transformer.shiftBehavior?.(scaleBehavior.shiftBehavior);
    transformer.keepRatio(scaleBehavior.keepRatio);
    transformer.nodes(node ? [node] : []);
    transformer.getLayer()?.batchDraw?.();
    renderMultiSelectionBox();
    renderLayerHoverBox();
  }

  function layerNodeBoundsForLayers(layers, options = {}) {
    const stage = state.konva?.stage;
    const rows = Array.isArray(layers) ? layers.filter(Boolean) : [];
    const boxes = [];
    const nodeMap = new Map();
    rows.forEach((layer) => {
      if (isPngMaskGroupLayer(layer)) {
        const bounds = pointLayerBounds(layer.mask);
        if (bounds) boxes.push(bounds);
        return;
      }
      if (layer.type === "group") {
        flatLayerList({ includeHiddenChildren: false })
          .filter((child) => child.groupId === layer.id && child.effectiveVisible !== false)
          .forEach((child) => {
            if (child.type === "stripe") {
              boxes.push(stripeBounds(child));
              return;
            }
            if (child.type === "polygon") {
              boxes.push(pointLayerBounds(child));
              return;
            }
            const node = layerNode(child.id);
            if (node) nodeMap.set(child.id, node);
          });
        return;
      }
      if (layer.visible === false || layer.effectiveVisible === false) return;
      if (layer.type === "stripe") {
        boxes.push(stripeBounds(layer));
        return;
      }
      if (layer.type === "polygon") {
        boxes.push(pointLayerBounds(layer));
        return;
      }
      const node = layerNode(layer.id);
      if (node) nodeMap.set(layer.id, node);
    });
    const nodes = Array.from(nodeMap.values());
    const singlePointLayer = rows.length === 1 && isPointLayerType(rows[0]?.type);
    const allowSingle = options.allowSingle === true;
    const nodeBoxes = stage ? nodes.map((node) => node.getClientRect({ relativeTo: stage })) : [];
    boxes.push(...nodeBoxes);
    if (!boxes.length || (!allowSingle && boxes.length < 2 && !singlePointLayer)) return null;
    const minX = Math.min(...boxes.map((box) => box.x));
    const minY = Math.min(...boxes.map((box) => box.y));
    const maxX = Math.max(...boxes.map((box) => box.x + box.width));
    const maxY = Math.max(...boxes.map((box) => box.y + box.height));
    return {
      x: minX,
      y: minY,
      width: Math.max(1, maxX - minX),
      height: Math.max(1, maxY - minY),
    };
  }

  function hideGroupTransformBox() {
    const box = state.konva?.groupTransformBox;
    if (!box) return;
    box.visible(false);
    box.draggable(false);
    box.getLayer()?.batchDraw?.();
  }

  function ensureGroupTransformBox() {
    const uiLayer = state.konva?.uiLayer;
    if (!uiLayer || !window.Konva) return null;
    if (state.konva.groupTransformBox) return state.konva.groupTransformBox;
    const box = new window.Konva.Rect({
      name: "groupTransformBox",
      listening: true,
      draggable: true,
      stroke: "#69d9ff",
      strokeWidth: 3,
      dash: [16, 9],
      fill: "rgba(105, 217, 255, 0.025)",
      visible: false,
    });
    box.on("dragstart", () => beginGroupTransform("Move group"));
    box.on("dragmove", (event) => {
      const snapshot = state.groupTransform;
      if (!snapshot || !event.evt?.shiftKey) return;
      const locked = axisLockedDragDelta(
        numberValue(box.x(), snapshot.bounds.x) - snapshot.bounds.x,
        numberValue(box.y(), snapshot.bounds.y) - snapshot.bounds.y,
        true,
      );
      box.position({
        x: snapshot.bounds.x + locked.dx,
        y: snapshot.bounds.y + locked.dy,
      });
    });
    box.on("dragend", () => {
      applyGroupTransformBox();
      commitHistory();
      state.groupTransform = null;
    });
    uiLayer.add(box);
    state.konva.groupTransformBox = box;
    return box;
  }

  function updateGroupTransformBox(group) {
    const box = ensureGroupTransformBox();
    if (!box || !group || group.locked || group.visible === false) {
      hideGroupTransformBox();
      return null;
    }
    if (!groupTransformChildren(group).length) {
      hideGroupTransformBox();
      return null;
    }
    if (!state.groupTransform) {
      const bounds = layerNodeBoundsForLayers([group], { allowSingle: true });
      if (!bounds) {
        hideGroupTransformBox();
        return null;
      }
      box.position({ x: bounds.x, y: bounds.y });
      box.size({ width: bounds.width, height: bounds.height });
      box.rotation(0);
      box.scale({ x: 1, y: 1 });
    }
    box.draggable(true);
    box.visible(true);
    box.moveToTop();
    state.konva?.transformer?.moveToTop?.();
    return box;
  }

  function groupTransformChildren(group) {
    return (group?.children || [])
      .filter((child) => child && !child.locked)
      .map((child) => cloneJson(child));
  }

  function beginGroupTransform(label = "Transform group") {
    const group = primarySelectedLayer();
    const box = state.konva?.groupTransformBox;
    if (!group || group.type !== "group" || !box || state.groupTransform) return false;
    const bounds = {
      x: numberValue(box.x(), 0),
      y: numberValue(box.y(), 0),
      width: Math.max(1, numberValue(box.width(), 1)),
      height: Math.max(1, numberValue(box.height(), 1)),
    };
    const children = groupTransformChildren(group);
    if (!children.length) return false;
    beginHistory(label);
    state.groupTransform = {
      groupId: group.id,
      bounds,
      children,
    };
    return true;
  }

  function rotatedLayerPoint(layer, dx, dy) {
    const rotation = (numberValue(layer.rotation, 0) * Math.PI) / 180;
    const cos = Math.cos(rotation);
    const sin = Math.sin(rotation);
    const x = numberValue(layer.x, 0);
    const y = numberValue(layer.y, 0);
    return {
      x: x + dx * cos - dy * sin,
      y: y + dx * sin + dy * cos,
    };
  }

  function pointDistance(left, right) {
    return Math.hypot(numberValue(right.x, 0) - numberValue(left.x, 0), numberValue(right.y, 0) - numberValue(left.y, 0));
  }

  function pointAngle(left, right) {
    return (Math.atan2(numberValue(right.y, 0) - numberValue(left.y, 0), numberValue(right.x, 0) - numberValue(left.x, 0)) * 180) / Math.PI;
  }

  function transformGroupChild(child, mapPoint) {
    if (isPointLayerType(child.type)) {
      const points = (child.points || []).slice();
      const nextPoints = [];
      for (let index = 0; index < points.length; index += 2) {
        const point = mapPoint(numberValue(points[index], 0), numberValue(points[index + 1], 0));
        nextPoints.push(point.x, point.y);
      }
      const patch = { points: nextPoints };
      if (isPointLayerType(child.type) && Array.isArray(child.bezierHandles)) {
        patch.bezierHandles = child.bezierHandles.map((handle) => {
          if (!handle) return null;
          const out = handle.out ? mapPoint(handle.out.x, handle.out.y) : null;
          const input = handle.in ? mapPoint(handle.in.x, handle.in.y) : null;
          return {
            out: out ? { x: out.x, y: out.y } : null,
            in: input ? { x: input.x, y: input.y } : null,
          };
        });
      }
      return clampLayerPatch(child, patch);
    }
    const width = Math.max(1, numberValue(child.width, 1));
    const height = Math.max(1, numberValue(child.height, 1));
    const oldOrigin = rotatedLayerPoint(child, 0, 0);
    const oldX = rotatedLayerPoint(child, width, 0);
    const oldY = rotatedLayerPoint(child, 0, height);
    const nextOrigin = mapPoint(oldOrigin.x, oldOrigin.y);
    const nextX = mapPoint(oldX.x, oldX.y);
    const nextY = mapPoint(oldY.x, oldY.y);
    const nextWidth = Math.max(1, Math.round(pointDistance(nextOrigin, nextX)));
    const nextHeight = Math.max(1, Math.round(pointDistance(nextOrigin, nextY)));
    const nextRotation = pointAngle(nextOrigin, nextX);
    if (isTextLayerType(child.type)) {
      const scale = nextWidth / width;
      return clampLayerPatch(child, {
        x: Math.round(nextOrigin.x),
        y: Math.round(nextOrigin.y),
        fontSize: Math.max(4, Math.round(numberValue(child.fontSize, 96) * scale)),
        rotation: nextRotation,
      });
    }
    return clampLayerPatch(child, {
      x: Math.round(nextOrigin.x),
      y: Math.round(nextOrigin.y),
      width: nextWidth,
      height: nextHeight,
      rotation: nextRotation,
    });
  }

  function applyGroupTransformBox() {
    const snapshot = state.groupTransform;
    const box = state.konva?.groupTransformBox;
    if (!snapshot || !box || !state.layerDocument?.layers) return false;
    const transform = box.getTransform();
    const mapPoint = (x, y) => transform.point({
      x: numberValue(x, 0) - snapshot.bounds.x,
      y: numberValue(y, 0) - snapshot.bounds.y,
    });
    const transformed = new Map(snapshot.children.map((child) => [child.id, transformGroupChild(child, mapPoint)]));
    let changed = false;
    state.layerDocument.layers = layerList().map((layer) => {
      if (layer.id !== snapshot.groupId || layer.type !== "group") return layer;
      changed = true;
      return {
        ...layer,
        children: (layer.children || []).map((child) => transformed.get(child.id) || child),
      };
    });
    if (!changed) return false;
    state.layerDocument = normalizeLayerDocument(state.layerDocument, state.textureSize);
    transformed.forEach((child) => applyLayerToNode(child));
    renderLayerList();
    renderPropertiesPanel();
    state.groupTransform = null;
    updateGroupTransformBox(findGroup(snapshot.groupId));
    updateTransformerForSelection();
    renderMultiSelectionBox();
    renderLayerHoverBox();
    updateCanvasStatus();
    setDirty(true);
    return true;
  }

  function rotateGroupByDelta(groupId, deltaDegrees) {
    const group = findGroup(groupId);
    if (!group || group.locked || !state.layerDocument?.layers) return false;
    const delta = numberValue(deltaDegrees, 0);
    if (!Number.isFinite(delta) || Math.abs(delta) < 0.0001) return false;
    const bounds = layerNodeBoundsForLayers([group], { allowSingle: true });
    if (!bounds || bounds.width <= 0 || bounds.height <= 0) return false;
    const children = groupTransformChildren(group);
    if (!children.length) return false;
    beginHistory("Rotate group");
    const center = {
      x: bounds.x + bounds.width / 2,
      y: bounds.y + bounds.height / 2,
    };
    const radians = (delta * Math.PI) / 180;
    const cos = Math.cos(radians);
    const sin = Math.sin(radians);
    const mapPoint = (x, y) => {
      const dx = numberValue(x, 0) - center.x;
      const dy = numberValue(y, 0) - center.y;
      return {
        x: center.x + dx * cos - dy * sin,
        y: center.y + dx * sin + dy * cos,
      };
    };
    const transformed = new Map(children.map((child) => [child.id, transformGroupChild(child, mapPoint)]));
    let changed = false;
    state.layerDocument.layers = layerList().map((layer) => {
      if (layer.id !== group.id || layer.type !== "group") return layer;
      changed = true;
      return {
        ...layer,
        children: (layer.children || []).map((child) => transformed.get(child.id) || child),
      };
    });
    if (!changed) return false;
    state.layerDocument = normalizeLayerDocument(state.layerDocument, state.textureSize);
    transformed.forEach((child) => applyLayerToNode(child));
    renderLayerList();
    renderPropertiesPanel();
    updateGroupTransformBox(findGroup(group.id));
    updateTransformerForSelection();
    renderMultiSelectionBox();
    renderLayerHoverBox();
    setText("layerStudioEditorStatus", `Group rotated by ${delta.toFixed(1).replace(/\.0$/, "")} deg.`);
    setDirty(true);
    return true;
  }

  function selectedLayerNodeBounds() {
    const selected = selectedLayers();
    if (selected.length === 1 && isPngMaskGroupLayer(selected[0])) {
      return layerNodeBoundsForLayers(selected, { allowSingle: true });
    }
    return layerNodeBoundsForLayers(selected);
  }

  async function loadProjectAssetLibrary(options = {}) {
    if (!window.pywebview?.api?.list_layer_studio_project_asset_library || !state.context?.skinKey) {
      setText("layerStudioLibraryStatus", "Project library API unavailable");
      return false;
    }
    state.libraryLoading = true;
    renderAssetLibrary();
    try {
      const result = await window.pywebview.api.list_layer_studio_project_asset_library({
        skinKey: state.context.skinKey,
        root: app().root?.() || "",
      });
      if (!result?.ok) {
        setText("layerStudioLibraryStatus", result?.message || "Project library failed");
        return false;
      }
      state.projectAssetLibrary = result.data || { assets: [] };
      renderAssetLibrary();
      if (!options.silent) app().showResult?.(result, "Layer Studio project library loaded", "list_layer_studio_project_asset_library");
      return true;
    } catch (error) {
      const message = error?.message || String(error);
      setText("layerStudioLibraryStatus", "Project library failed");
      setText("layerStudioEditorStatus", `Layer Studio project library failed:\n${message}`);
      app().log?.(message, true);
      return false;
    } finally {
      state.libraryLoading = false;
      renderAssetLibrary();
    }
  }

  async function loadActiveAssetLibrary(options = {}) {
    return activeLibraryScope() === "global" ? loadAssetLibrary(options) : loadProjectAssetLibrary(options);
  }

  function hoveredLayerNodeBounds() {
    const layer = findLayer(state.hoveredLayerId);
    if (!layer || isLayerSelected(layer.id)) return null;
    return layerNodeBoundsForLayers([layer], { allowSingle: true });
  }

  function hidePointTransformBox() {
    const box = state.konva?.pointTransformBox;
    if (!box) return;
    box.visible(false);
    box.draggable(false);
    box.getLayer()?.batchDraw?.();
  }

  function ensurePointTransformBox() {
    const uiLayer = state.konva?.uiLayer;
    if (!uiLayer || !window.Konva) return null;
    if (state.konva.pointTransformBox) return state.konva.pointTransformBox;
    const box = new window.Konva.Rect({
      name: "pointLayerTransformBox",
      listening: true,
      draggable: true,
      stroke: "#69d9ff",
      strokeWidth: 3,
      dash: [12, 8],
      fill: "rgba(105, 217, 255, 0.025)",
      visible: false,
    });
    box.on("dragstart", () => beginPointTransform("Move point layer"));
    box.on("dragmove", (event) => {
      const snapshot = state.pointTransform;
      if (!snapshot || !event.evt?.shiftKey) return;
      const locked = axisLockedDragDelta(
        numberValue(box.x(), snapshot.bounds.x) - snapshot.bounds.x,
        numberValue(box.y(), snapshot.bounds.y) - snapshot.bounds.y,
        true,
      );
      box.position({
        x: snapshot.bounds.x + locked.dx,
        y: snapshot.bounds.y + locked.dy,
      });
    });
    box.on("dragend", () => {
      applyPointTransformBox();
      commitHistory();
      state.pointTransform = null;
    });
    box.on("dblclick dbltap", (event) => {
      event.cancelBubble = true;
      const layerId = state.pointTransform?.layerId || state.selectedLayerId;
      enterPolygonEditMode(layerId);
    });
    uiLayer.add(box);
    state.konva.pointTransformBox = box;
    return box;
  }

  function updatePointTransformBox(layer) {
    const box = ensurePointTransformBox();
    if (!box || !layer || !isPointLayerType(layer.type) || layer.locked || layer.visible === false || state.polygonEditLayerId === layer.id) {
      hidePointTransformBox();
      return null;
    }
    if (!state.pointTransform) {
      const bounds = layer.type === "stripe" ? stripeBounds(layer) : pointLayerBounds(layer);
      if (!bounds) {
        hidePointTransformBox();
        return null;
      }
      box.position({ x: bounds.x, y: bounds.y });
      box.size({ width: bounds.width, height: bounds.height });
      box.rotation(0);
      box.scale({ x: 1, y: 1 });
    }
    box.draggable(true);
    box.visible(true);
    box.moveToTop();
    state.konva?.transformer?.moveToTop?.();
    return box;
  }

  function beginPointTransform(label = "Transform point layer") {
    const layer = primarySelectedLayer();
    const box = state.konva?.pointTransformBox;
    if (!layer || !isPointLayerType(layer.type) || !box || state.pointTransform) return false;
    const bounds = {
      x: numberValue(box.x(), 0),
      y: numberValue(box.y(), 0),
      width: Math.max(1, numberValue(box.width(), 1)),
      height: Math.max(1, numberValue(box.height(), 1)),
    };
    beginHistory(label);
    state.pointTransform = {
      layerId: layer.id,
      bounds,
      layer: cloneJson(layer),
    };
    return true;
  }

  function applyPointTransformBox() {
    const snapshot = state.pointTransform;
    const box = state.konva?.pointTransformBox;
    if (!snapshot || !box || !state.layerDocument?.layers) return false;
    const transform = box.getTransform();
    const mapPoint = (x, y) => transform.point({
      x: numberValue(x, 0) - snapshot.bounds.x,
      y: numberValue(y, 0) - snapshot.bounds.y,
    });
    const transformed = transformGroupChild(snapshot.layer, mapPoint);
    let changed = false;
    updateLayerInDocument(snapshot.layerId, () => {
      changed = true;
      return transformed;
    });
    if (!changed) return false;
    state.layerDocument = normalizeLayerDocument(state.layerDocument, state.textureSize);
    const updated = findLayer(snapshot.layerId);
    if (updated) applyLayerToNode(updated);
    renderLayerList();
    renderPropertiesPanel();
    state.pointTransform = null;
    updatePointTransformBox(updated);
    updateTransformerForSelection();
    renderMultiSelectionBox();
    renderLayerHoverBox();
    updateCanvasStatus();
    setDirty(true);
    return true;
  }

  function renderMultiSelectionBox() {
    const uiLayer = state.konva?.uiLayer;
    if (!uiLayer || !window.Konva) return;
    const selected = selectedLayers();
    if (selected.length === 1 && (selected[0]?.type === "group" || isPointLayerType(selected[0]?.type))) {
      const box = state.konva.multiSelectionBox;
      if (box) {
        box.visible(false);
        uiLayer.batchDraw();
      }
      return;
    }
    const bounds = selectedLayerNodeBounds();
    let box = state.konva.multiSelectionBox;
    if (!bounds) {
      if (box) {
        box.visible(false);
        uiLayer.batchDraw();
      }
      return;
    }
    if (!box) {
      box = new window.Konva.Rect({
        name: "multiSelectionBox",
        listening: false,
        stroke: "#69d9ff",
        strokeWidth: 4,
        dash: [18, 10],
        fill: "rgba(105, 217, 255, 0.035)",
        visible: true,
      });
      uiLayer.add(box);
      state.konva.multiSelectionBox = box;
    }
    box.position({ x: bounds.x, y: bounds.y });
    box.size({ width: bounds.width, height: bounds.height });
    box.visible(true);
    box.moveToTop();
    state.konva?.transformer?.moveToTop?.();
    state.konva?.polygonEditGroup?.moveToTop?.();
    uiLayer.batchDraw();
  }

  function renderGuides() {
    const uiLayer = state.konva?.uiLayer;
    if (!uiLayer || !window.Konva) {
      syncGuideControls();
      return;
    }
    if (state.konva.guideGroup) {
      state.konva.guideGroup.destroy();
      state.konva.guideGroup = null;
    }
    const group = new window.Konva.Group({
      name: "guideGroup",
      visible: state.layerDocument?.guidesVisible !== false,
      listening: true,
    });
    const width = state.textureSize.width;
    const height = state.textureSize.height;
    guideList().forEach((guide) => {
      const isVertical = guide.axis === "x";
      const selected = guide.id === state.selectedGuideId;
      const line = new window.Konva.Line({
        x: isVertical ? guide.position : 0,
        y: isVertical ? 0 : guide.position,
        points: isVertical ? [0, 0, 0, height] : [0, 0, width, 0],
        stroke: selected ? "#ff2bd6" : "#25c2f7",
        strokeWidth: selected ? 3 : 2,
        dash: selected ? [18, 8] : [12, 10],
        opacity: selected ? 0.9 : 0.64,
        hitStrokeWidth: 24,
        draggable: true,
        strokeScaleEnabled: false,
        name: "layerStudioGuide",
        id: guide.id,
      });
      line.on("click tap", (event) => {
        if (shouldIgnoreCanvasObjectAction()) {
          event.cancelBubble = true;
          return;
        }
        event.cancelBubble = true;
        state.selectedGuideId = guide.id;
        state.selectedLayerId = "";
        state.selectedLayerIds = [];
        renderLayerList();
        renderPropertiesPanel();
        updateTransformerForSelection();
        renderGuides();
      });
      line.on("mouseenter", () => {
        const container = state.konva?.stage?.container?.();
        if (container) container.style.cursor = isVertical ? "ew-resize" : "ns-resize";
      });
      line.on("mouseleave", () => {
        const container = state.konva?.stage?.container?.();
        if (container) container.style.cursor = state.spacePanActive ? "grab" : usesPrecisionCursor() ? "crosshair" : "grab";
      });
      line.on("dragstart", (event) => {
        if (state.spacePanActive) {
          event.cancelBubble = true;
          line.stopDrag?.();
          return;
        }
        event.cancelBubble = true;
        state.selectedGuideId = guide.id;
        state.selectedLayerId = "";
        state.selectedLayerIds = [];
        renderLayerList();
        renderPropertiesPanel();
        updateTransformerForSelection();
        beginHistory("Move guide");
      });
      line.on("dragmove", (event) => {
        event.cancelBubble = true;
        const position = clampGuidePosition(guide.axis, isVertical ? line.x() : line.y());
        line.position(isVertical ? { x: position, y: 0 } : { x: 0, y: position });
        updateGuide(guide.id, { position }, { render: false });
      });
      line.on("dragend", (event) => {
        event.cancelBubble = true;
        const position = clampGuidePosition(guide.axis, isVertical ? line.x() : line.y());
        updateGuide(guide.id, { position }, { render: false });
        commitHistory();
        renderGuides();
        setText("layerStudioEditorStatus", `${isVertical ? "Vertical" : "Horizontal"} guide: ${position}px`);
      });
      group.add(line);
    });
    uiLayer.add(group);
    group.moveToBottom();
    state.konva.guideGroup = group;
    state.konva.multiSelectionBox?.moveToTop?.();
    state.konva.hoverBox?.moveToTop?.();
    state.konva.transformer?.moveToTop?.();
    state.konva.polygonEditGroup?.moveToTop?.();
    state.konva.polygonDraftLine?.moveToTop?.();
    syncGuideControls();
    uiLayer.batchDraw();
  }

  function renderLayerHoverBox() {
    const uiLayer = state.konva?.uiLayer;
    if (!uiLayer || !window.Konva) return;
    const bounds = hoveredLayerNodeBounds();
    let box = state.konva.layerHoverBox;
    if (!bounds) {
      if (box) {
        box.visible(false);
        uiLayer.batchDraw();
      }
      return;
    }
    if (!box) {
      box = new window.Konva.Rect({
        name: "layerHoverBox",
        listening: false,
        stroke: "#ff3cff",
        strokeWidth: 4,
        dash: [14, 8],
        fill: "rgba(255, 60, 255, 0.035)",
        visible: true,
      });
      uiLayer.add(box);
      state.konva.layerHoverBox = box;
    }
    box.position({ x: bounds.x, y: bounds.y });
    box.size({ width: bounds.width, height: bounds.height });
    box.visible(true);
    box.moveToTop();
    state.konva?.transformer?.moveToTop?.();
    state.konva?.polygonEditGroup?.moveToTop?.();
    uiLayer.batchDraw();
  }

  function updateStageInteractivity() {
    state.konva?.stage?.draggable?.(state.activeTool === "select" && !state.polygonEditLayerId);
  }

  function shouldIgnoreCanvasObjectAction() {
    if (state.suppressCanvasClick) {
      state.suppressCanvasClick = false;
      return true;
    }
    return Boolean(state.spacePanActive);
  }

  function eventTargetHasName(eventTarget, name) {
    if (!eventTarget || !name) return false;
    if (typeof eventTarget.hasName === "function" && eventTarget.hasName(name)) return true;
    return eventTarget.name?.() === name;
  }

  function clearPolygonEditHandles() {
    const group = state.konva?.polygonEditGroup;
    state.polygonEditPointDrag = null;
    if (group) {
      group.destroy();
      state.konva.polygonEditGroup = null;
      state.konva?.uiLayer?.batchDraw?.();
    }
  }

  function polygonPointCount(layer) {
    return Math.floor((Array.isArray(layer?.points) ? layer.points.length : 0) / 2);
  }

  function pointAt(points, index, closed = false) {
    const count = Math.floor((Array.isArray(points) ? points.length : 0) / 2);
    if (!count) return { x: 0, y: 0 };
    const safeIndex = closed ? ((index % count) + count) % count : Math.max(0, Math.min(count - 1, index));
    return {
      x: numberValue(points[safeIndex * 2], 0),
      y: numberValue(points[safeIndex * 2 + 1], 0),
    };
  }

  function stripePointAt(points, index) {
    return pointAt(points, index, false);
  }

  function closestPointOnSegment(point, start, end) {
    const px = numberValue(point?.x, 0);
    const py = numberValue(point?.y, 0);
    const sx = numberValue(start?.x, 0);
    const sy = numberValue(start?.y, 0);
    const ex = numberValue(end?.x, sx);
    const ey = numberValue(end?.y, sy);
    const dx = ex - sx;
    const dy = ey - sy;
    const lengthSq = (dx * dx) + (dy * dy);
    if (!lengthSq) return normalizeLayerPoint(sx, sy);
    const t = Math.max(0, Math.min(1, (((px - sx) * dx) + ((py - sy) * dy)) / lengthSq));
    return normalizeLayerPoint(sx + (dx * t), sy + (dy * t));
  }

  function cubicBezierPoint(start, out, input, end, t) {
    const safeT = Math.max(0, Math.min(1, numberValue(t, 0)));
    const inv = 1 - safeT;
    const inv2 = inv * inv;
    const t2 = safeT * safeT;
    return normalizeLayerPoint(
      (inv2 * inv * start.x) + (3 * inv2 * safeT * out.x) + (3 * inv * t2 * input.x) + (t2 * safeT * end.x),
      (inv2 * inv * start.y) + (3 * inv2 * safeT * out.y) + (3 * inv * t2 * input.y) + (t2 * safeT * end.y),
    );
  }

  function pointLayerSegmentSamples(layer, segmentIndex, steps = 24) {
    const points = Array.isArray(layer?.points) ? layer.points : [];
    const closed = layer?.type === "polygon";
    const start = pointAt(points, segmentIndex, closed);
    const end = pointAt(points, segmentIndex + 1, closed);
    const handle = bezierHandleForSegment(layer, segmentIndex);
    if (!handle.active) return [start, end];
    const safeSteps = Math.max(4, intValue(steps, 24, 4));
    const samples = [];
    for (let index = 0; index <= safeSteps; index += 1) {
      samples.push(cubicBezierPoint(start, handle.out, handle.in, end, index / safeSteps));
    }
    return samples;
  }

  function closestPointOnSampledPath(point, samples) {
    const source = Array.isArray(samples) ? samples : [];
    if (!source.length) return null;
    if (source.length === 1) return normalizeLayerPoint(source[0].x, source[0].y);
    let closest = null;
    let closestDistance = Number.POSITIVE_INFINITY;
    for (let index = 0; index < source.length - 1; index += 1) {
      const candidate = closestPointOnSegment(point, source[index], source[index + 1]);
      const dx = numberValue(candidate.x, 0) - numberValue(point?.x, 0);
      const dy = numberValue(candidate.y, 0) - numberValue(point?.y, 0);
      const distance = (dx * dx) + (dy * dy);
      if (distance < closestDistance) {
        closestDistance = distance;
        closest = candidate;
      }
    }
    return closest;
  }

  function addPointToPointLayerSegment(layer, segmentIndex, point, options = {}) {
    if (!layer || !isPointLayerType(layer.type)) return false;
    const cleanPoint = normalizeLayerPoint(point?.x, point?.y);
    const nextPoints = layer.points.slice();
    nextPoints.splice((segmentIndex + 1) * 2, 0, cleanPoint.x, cleanPoint.y);
    const nextHandles = Array.isArray(layer.bezierHandles)
      ? layer.bezierHandles.slice()
      : null;
    if (nextHandles) {
      const hadSegmentHandle = Boolean(nextHandles[segmentIndex]);
      const firstDefault = layer.type === "polygon" ? polygonSegmentDefaultHandles(nextPoints, segmentIndex) : stripeSegmentDefaultHandles(nextPoints, segmentIndex);
      const secondDefault = layer.type === "polygon" ? polygonSegmentDefaultHandles(nextPoints, segmentIndex + 1) : stripeSegmentDefaultHandles(nextPoints, segmentIndex + 1);
      nextHandles.splice(segmentIndex, 1, hadSegmentHandle ? firstDefault : null, hadSegmentHandle ? secondDefault : null);
    }
    if (options.history !== false) pushHistory(layer.type === "stripe" ? "Add stripe point" : "Add polygon point");
    setPolygonPoints(layer.id, nextPoints, { bezierHandles: nextHandles || undefined, renderHandles: false });
    selectPolygonEditPoint(segmentIndex + 1);
    renderPolygonEditHandles();
    return true;
  }

  function rotatePointLayerByDelta(layerId, deltaDegrees) {
    const layer = findLayer(layerId);
    if (!layer || !isPointLayerType(layer.type) || layer.locked || !state.layerDocument?.layers) return false;
    const delta = numberValue(deltaDegrees, 0);
    if (!Number.isFinite(delta) || Math.abs(delta) < 0.0001) return false;
    const bounds = layer.type === "stripe" ? stripeBounds(layer) : pointLayerBounds(layer);
    if (!bounds || bounds.width <= 0 || bounds.height <= 0) return false;
    const center = {
      x: bounds.x + bounds.width / 2,
      y: bounds.y + bounds.height / 2,
    };
    const radians = (delta * Math.PI) / 180;
    const cos = Math.cos(radians);
    const sin = Math.sin(radians);
    const mapPoint = (x, y) => {
      const dx = numberValue(x, 0) - center.x;
      const dy = numberValue(y, 0) - center.y;
      return {
        x: center.x + dx * cos - dy * sin,
        y: center.y + dx * sin + dy * cos,
      };
    };
    beginHistory(layer.type === "stripe" ? "Rotate stripe" : "Rotate polygon");
    const updated = patchLayer(layer.id, transformGroupChild(layer, mapPoint), { updateNode: false });
    if (!updated) {
      cancelHistory();
      return false;
    }
    applyLayerToNode(updated);
    renderMultiSelectionBox();
    renderLayerHoverBox();
    commitHistory();
    setText("layerStudioEditorStatus", `${layer.type === "stripe" ? "Stripe" : "Polygon"} rotated by ${delta.toFixed(1).replace(/\.0$/, "")} deg.`);
    return true;
  }

  function nearestPointLayerSegmentHover(layer, pointer) {
    if (!layer || !isPointLayerType(layer.type) || !pointer) return null;
    const points = Array.isArray(layer.points) ? layer.points : [];
    const pointThreshold = textureUnitsForScreenPixels(20);
    const pointThresholdSq = pointThreshold * pointThreshold;
    for (let index = 0; index < points.length - 1; index += 2) {
      const dx = numberValue(points[index], 0) - numberValue(pointer.x, 0);
      const dy = numberValue(points[index + 1], 0) - numberValue(pointer.y, 0);
      if ((dx * dx) + (dy * dy) <= pointThresholdSq) return null;
    }
    const count = polygonPointCount(layer);
    const segmentCount = layer.type === "stripe" ? count - 1 : count;
    if (segmentCount <= 0) return null;
    const threshold = textureUnitsForScreenPixels(18);
    const thresholdSq = threshold * threshold;
    let best = null;
    let bestDistance = Number.POSITIVE_INFINITY;
    for (let segmentIndex = 0; segmentIndex < segmentCount; segmentIndex += 1) {
      const samples = pointLayerSegmentSamples(layer, segmentIndex, 28);
      const point = closestPointOnSampledPath(pointer, samples);
      if (!point) continue;
      const dx = numberValue(point.x, 0) - numberValue(pointer.x, 0);
      const dy = numberValue(point.y, 0) - numberValue(pointer.y, 0);
      const distance = (dx * dx) + (dy * dy);
      if (distance < bestDistance) {
        bestDistance = distance;
        best = { layerId: layer.id, segmentIndex, point, distance };
      }
    }
    return best && bestDistance <= thresholdSq ? best : null;
  }

  function updatePolygonEditEdgeHover(pointer = pointerTexturePosition()) {
    const layer = findLayer(state.polygonEditLayerId);
    const group = state.konva?.polygonEditGroup;
    const marker = group?.findOne?.(".polygonEditAddMarker");
    if (!layer || !group || !marker || !isPointLayerType(layer.type) || layer.locked || layer.visible === false) {
      state.polygonEditEdgeHover = null;
      marker?.visible?.(false);
      return null;
    }
    const hover = nearestPointLayerSegmentHover(layer, pointer);
    state.polygonEditEdgeHover = hover;
    if (!hover) {
      marker.visible(false);
      group.getLayer()?.batchDraw?.();
      return null;
    }
    marker.position(hover.point);
    marker.visible(true);
    marker.moveToTop();
    group.getLayer()?.batchDraw?.();
    return hover;
  }

  function updatePolygonEditAddDrag(pointer, snap = false, breakCoupling = false) {
    const drag = state.polygonEditAddDrag;
    const layer = findLayer(drag?.layerId);
    if (!drag || !pointer || !layer || layer.locked || !isPointLayerType(layer.type)) return false;
    const rawPoint = normalizeLayerPoint(pointer.x, pointer.y);
    const anchor = pointAt(layer.points || [], drag.pointIndex, layer.type === "polygon");
    const snappedPoint = snapBezierHandlePoint(rawPoint, anchor, snap);
    const cleanPoint = normalizeLayerPoint(snappedPoint.x, snappedPoint.y);
    const threshold = textureUnitsForScreenPixels(STRIPE_DRAFT_BEZIER_THRESHOLD);
    if (!drag.dragging && stripeDraftBezierDistance(cleanPoint, anchor) < threshold) return false;
    drag.dragging = true;
    const segmentCount = layer.type === "stripe" ? polygonPointCount(layer) - 1 : polygonPointCount(layer);
    const nextHandles = Array.isArray(layer.bezierHandles) ? layer.bezierHandles.slice() : [];
    while (nextHandles.length < segmentCount) nextHandles.push(null);
    const incomingSegment = drag.pointIndex > 0 ? drag.pointIndex - 1 : (layer.type === "polygon" ? segmentCount - 1 : -1);
    const outgoingSegment = layer.type === "polygon" || drag.pointIndex < polygonPointCount(layer) - 1 ? drag.pointIndex : -1;
    const incomingPoint = breakCoupling ? cleanPoint : mirroredBezierPoint(cleanPoint, anchor);
    if (incomingSegment >= 0 && incomingSegment < segmentCount) {
      const current = nextHandles[incomingSegment] || {};
      const fallback = bezierHandleForSegment(layer, incomingSegment);
      nextHandles[incomingSegment] = {
        out: current.out || fallback.out,
        in: incomingPoint,
      };
    }
    if (!breakCoupling && outgoingSegment >= 0 && outgoingSegment < segmentCount) {
      const current = nextHandles[outgoingSegment] || {};
      const fallback = bezierHandleForSegment(layer, outgoingSegment);
      nextHandles[outgoingSegment] = {
        out: cleanPoint,
        in: current.in || fallback.in,
      };
    }
    setPolygonPoints(layer.id, layer.points.slice(), { bezierHandles: nextHandles, renderProperties: false, renderHandles: false });
    renderPolygonEditHandles();
    return true;
  }

  function stripeBounds(layer) {
    return pointLayerBounds(layer, Math.max(1, numberValue(layer?.strokeWidth, 20, 1) / 2));
  }

  function pointLayerBounds(layer, pad = 0) {
    const points = Array.isArray(layer?.points) ? layer.points : [];
    const xs = [];
    const ys = [];
    for (let index = 0; index < points.length - 1; index += 2) {
      xs.push(numberValue(points[index], 0));
      ys.push(numberValue(points[index + 1], 0));
    }
    (Array.isArray(layer?.bezierHandles) ? layer.bezierHandles : []).forEach((handle) => {
      if (handle?.out) {
        xs.push(numberValue(handle.out.x, 0));
        ys.push(numberValue(handle.out.y, 0));
      }
      if (handle?.in) {
        xs.push(numberValue(handle.in.x, 0));
        ys.push(numberValue(handle.in.y, 0));
      }
    });
    if (!xs.length || !ys.length) return { x: 0, y: 0, width: 1, height: 1 };
    const safePad = Math.max(0, numberValue(pad, 0));
    const minX = Math.min(...xs) - safePad;
    const minY = Math.min(...ys) - safePad;
    const maxX = Math.max(...xs) + safePad;
    const maxY = Math.max(...ys) + safePad;
    return {
      x: minX,
      y: minY,
      width: Math.max(1, maxX - minX),
      height: Math.max(1, maxY - minY),
    };
  }

  function pointSegmentDefaultHandles(points, segmentIndex, closed = false) {
    const start = pointAt(points, segmentIndex, closed);
    const end = pointAt(points, segmentIndex + 1, closed);
    return {
      out: {
        x: Math.round(start.x + (end.x - start.x) / 3),
        y: Math.round(start.y + (end.y - start.y) / 3),
      },
      in: {
        x: Math.round(start.x + ((end.x - start.x) * 2) / 3),
        y: Math.round(start.y + ((end.y - start.y) * 2) / 3),
      },
    };
  }

  function stripeSegmentDefaultHandles(points, segmentIndex) {
    return pointSegmentDefaultHandles(points, segmentIndex, false);
  }

  function polygonSegmentDefaultHandles(points, segmentIndex) {
    return pointSegmentDefaultHandles(points, segmentIndex, true);
  }

  function bezierHandleForSegment(layer, segmentIndex) {
    const points = Array.isArray(layer?.points) ? layer.points : [];
    const closed = layer?.type === "polygon";
    const raw = Array.isArray(layer?.bezierHandles) ? layer.bezierHandles[segmentIndex] : null;
    const defaults = pointSegmentDefaultHandles(points, segmentIndex, closed);
    return {
      out: raw?.out ? normalizeLayerPoint(raw.out.x, raw.out.y) : defaults.out,
      in: raw?.in ? normalizeLayerPoint(raw.in.x, raw.in.y) : defaults.in,
      active: Boolean(raw?.out || raw?.in),
    };
  }

  function stripeBezierHandleForSegment(layer, segmentIndex) {
    return bezierHandleForSegment(layer, segmentIndex);
  }

  function pointLayerHasBezierHandles(layer) {
    return Array.isArray(layer?.bezierHandles) && layer.bezierHandles.some((handle) => handle && (handle.out || handle.in));
  }

  function stripeHasBezierHandles(layer) {
    return pointLayerHasBezierHandles(layer);
  }

  function moveBezierPoint(point, dx, dy) {
    if (!point) return null;
    return normalizeLayerPoint(numberValue(point.x, 0) + dx, numberValue(point.y, 0) + dy);
  }

  function moveBezierHandles(handles, dx, dy) {
    if (!Array.isArray(handles) || (!dx && !dy)) return handles;
    return handles.map((handle) => (handle
      ? {
          out: moveBezierPoint(handle.out, dx, dy),
          in: moveBezierPoint(handle.in, dx, dy),
        }
      : null));
  }

  function moveStripeBezierHandles(handles, dx, dy) {
    return moveBezierHandles(handles, dx, dy);
  }

  function moveBezierHandlesForPoint(layer, pointIndex, dx, dy) {
    const handles = Array.isArray(layer?.bezierHandles) ? layer.bezierHandles.slice() : [];
    if (!handles.length || (!dx && !dy)) return handles;
    if (pointIndex > 0 && handles[pointIndex - 1]?.in) {
      handles[pointIndex - 1] = {
        ...(handles[pointIndex - 1] || {}),
        in: moveBezierPoint(handles[pointIndex - 1].in, dx, dy),
      };
    } else if (layer?.type === "polygon" && pointIndex === 0 && handles[handles.length - 1]?.in) {
      handles[handles.length - 1] = {
        ...(handles[handles.length - 1] || {}),
        in: moveBezierPoint(handles[handles.length - 1].in, dx, dy),
      };
    }
    if (handles[pointIndex]?.out) {
      handles[pointIndex] = {
        ...(handles[pointIndex] || {}),
        out: moveBezierPoint(handles[pointIndex].out, dx, dy),
      };
    }
    return handles;
  }

  function moveBezierHandlesForPoints(layer, pointIndices, dx, dy) {
    const selected = Array.from(new Set((pointIndices || []).map((value) => intValue(value, -1)).filter((value) => value >= 0)));
    if (!selected.length) return Array.isArray(layer?.bezierHandles) ? layer.bezierHandles.slice() : [];
    return selected.reduce((handles, pointIndex) => (
      moveBezierHandlesForPoint({ ...layer, bezierHandles: handles }, pointIndex, dx, dy)
    ), Array.isArray(layer?.bezierHandles) ? layer.bezierHandles.slice() : []);
  }

  function oppositeBezierHandleForPoint(layer, pointIndex, handleKey) {
    const count = polygonPointCount(layer);
    const isStripe = layer?.type === "stripe";
    if (!count || pointIndex < 0 || pointIndex >= count) return null;
    if (handleKey === "out") {
      if (pointIndex > 0) return { segmentIndex: pointIndex - 1, handleKey: "in" };
      return isStripe ? null : { segmentIndex: count - 1, handleKey: "in" };
    }
    if (handleKey === "in") {
      if (!isStripe || pointIndex < count - 1) return { segmentIndex: pointIndex, handleKey: "out" };
    }
    return null;
  }

  function mirroredBezierPoint(point, anchor) {
    if (!point || !anchor) return null;
    return normalizeLayerPoint(
      numberValue(anchor.x, 0) - (numberValue(point.x, 0) - numberValue(anchor.x, 0)),
      numberValue(anchor.y, 0) - (numberValue(point.y, 0) - numberValue(anchor.y, 0)),
    );
  }

  function moveStripeBezierHandlesForPoint(layer, pointIndex, dx, dy) {
    return moveBezierHandlesForPoint(layer, pointIndex, dx, dy);
  }

  function uniformPointDelta(previousPoints, nextPoints) {
    if (!Array.isArray(previousPoints) || !Array.isArray(nextPoints) || previousPoints.length !== nextPoints.length || previousPoints.length < 2) return null;
    const dx = coordinateValue(nextPoints[0]) - coordinateValue(previousPoints[0]);
    const dy = coordinateValue(nextPoints[1]) - coordinateValue(previousPoints[1]);
    for (let index = 2; index < previousPoints.length - 1; index += 2) {
      if (coordinateValue(nextPoints[index]) - coordinateValue(previousPoints[index]) !== dx) return null;
      if (coordinateValue(nextPoints[index + 1]) - coordinateValue(previousPoints[index + 1]) !== dy) return null;
    }
    return { dx, dy };
  }

  function stripeDraftBezierDistance(point, origin) {
    if (!point || !origin) return 0;
    return Math.hypot(numberValue(point.x, 0) - numberValue(origin.x, 0), numberValue(point.y, 0) - numberValue(origin.y, 0));
  }

  function updateStripeDraftBezier(pointer, snap = false, breakCoupling = false) {
    const drag = state.stripeDraftDrag;
    if (!drag || !pointer || drag.segmentIndex < 0) return false;
    const rawPoint = normalizeLayerPoint(pointer.x, pointer.y);
    drag.snap = Boolean(snap);
    const snappedPoint = snapBezierHandlePoint(rawPoint, drag.point, drag.snap);
    const cleanPoint = normalizeLayerPoint(snappedPoint.x, snappedPoint.y);
    const threshold = textureUnitsForScreenPixels(STRIPE_DRAFT_BEZIER_THRESHOLD);
    if (!drag.dragging && stripeDraftBezierDistance(cleanPoint, drag.point) < threshold) return false;
    drag.dragging = true;
    const nextHandles = Array.isArray(state.stripeDraftBezierHandles) ? state.stripeDraftBezierHandles.slice() : [];
    while (nextHandles.length <= drag.segmentIndex) nextHandles.push(null);
    const fallback = pointSegmentDefaultHandles(state.polygonDraftPoints, drag.segmentIndex, state.activeTool === "polygon");
    const current = nextHandles[drag.segmentIndex] || {};
    const incomingPoint = breakCoupling ? cleanPoint : mirroredBezierPoint(cleanPoint, drag.point);
    nextHandles[drag.segmentIndex] = {
      out: current.out || fallback.out,
      in: incomingPoint,
    };
    if (!breakCoupling) {
      const nextSegmentIndex = drag.pointIndex;
      while (nextHandles.length <= nextSegmentIndex) nextHandles.push(null);
      const nextCurrent = nextHandles[nextSegmentIndex] || {};
      nextHandles[nextSegmentIndex] = {
        ...nextCurrent,
        out: cleanPoint,
      };
    }
    state.stripeDraftBezierHandles = nextHandles.map((handle) => (handle
      ? {
          out: handle.out ? normalizeLayerPoint(handle.out.x, handle.out.y) : null,
          in: handle.in ? normalizeLayerPoint(handle.in.x, handle.in.y) : null,
        }
      : null));
    renderPolygonDraft(cleanPoint);
    return true;
  }

  function addStripeDraftPoint(event) {
    const pointer = pointerTexturePosition();
    const point = polygonDraftPoint(pointer, Boolean(event?.evt?.shiftKey));
    if (!point) return false;
    state.polygonDraftSnap = Boolean(event?.evt?.shiftKey);
    state.polygonDraftPoints.push(point.x, point.y);
    const pointIndex = Math.floor(state.polygonDraftPoints.length / 2) - 1;
    state.stripeDraftDrag = {
      pointIndex,
      segmentIndex: pointIndex - 1,
      point,
      snap: Boolean(event?.evt?.shiftKey),
      dragging: false,
    };
    state.suppressDraftClick = true;
    renderPolygonDraft(point);
    return true;
  }

  function snapPointTo45Degrees(point, previousPoint) {
    if (!point || !previousPoint) return point;
    const dx = numberValue(point.x, 0) - numberValue(previousPoint.x, 0);
    const dy = numberValue(point.y, 0) - numberValue(previousPoint.y, 0);
    if (dx === 0 && dy === 0) return normalizeLayerPoint(point.x, point.y);
    const angle = Math.atan2(dy, dx);
    const snappedAngle = Math.round(angle / (Math.PI / 4)) * (Math.PI / 4);
    const distance = Math.sqrt(dx * dx + dy * dy);
    return normalizeLayerPoint(
      previousPoint.x + Math.cos(snappedAngle) * distance,
      previousPoint.y + Math.sin(snappedAngle) * distance,
    );
  }

  function snapBezierHandlePoint(point, anchorPoint, snap = false) {
    if (!snap || !point || !anchorPoint) return point;
    return snapPointTo45Degrees(point, anchorPoint);
  }

  function polygonDraftPoint(pointer, snap = state.polygonDraftSnap) {
    if (!pointer) return null;
    const point = normalizeLayerPoint(pointer.x, pointer.y);
    const points = state.polygonDraftPoints;
    if (!snap || points.length < 2) return point;
    return snapPointTo45Degrees(point, {
      x: points[points.length - 2],
      y: points[points.length - 1],
    });
  }

  function textureUnitsForScreenPixels(pixels) {
    const scale = Math.max(0.04, numberValue(state.konva?.stage?.scaleX?.(), state.view.scale || 1));
    return Math.max(1, numberValue(pixels, 1)) / scale;
  }

  function polygonDraftClosePoint(pointer) {
    if (state.activeTool !== "polygon" || !pointer || state.polygonDraftPoints.length < 6) return null;
    const first = {
      x: state.polygonDraftPoints[0],
      y: state.polygonDraftPoints[1],
    };
    const dx = numberValue(pointer.x, 0) - numberValue(first.x, 0);
    const dy = numberValue(pointer.y, 0) - numberValue(first.y, 0);
    const threshold = textureUnitsForScreenPixels(POLYGON_CLOSE_SCREEN_RADIUS);
    return Math.sqrt(dx * dx + dy * dy) <= threshold ? first : null;
  }

  function polygonDraftPreviewPoint(pointer, snap = state.polygonDraftSnap) {
    return polygonDraftClosePoint(pointer) || polygonDraftPoint(pointer, snap);
  }

  function setPolygonPoints(layerId, points, options = {}) {
    const layer = findLayer(layerId);
    if (!layer || !isPointLayerType(layer.type)) return null;
    let updated = null;
    updateLayerInDocument(layer.id, (current) => {
      if (!current || !isPointLayerType(current.type)) return current;
      const next = {
        ...current,
        points: Array.isArray(points)
          ? points.map((value) => numberValue(value, 0))
          : Array.isArray(current.points)
          ? current.points.slice()
          : [],
      };
      if (next.type === "stripe" && Array.isArray(options.bezierHandles)) {
        next.bezierHandles = normalizeStripeBezierHandles(options.bezierHandles, next.points, state.textureSize);
      } else if (next.type === "polygon" && Array.isArray(options.bezierHandles)) {
        next.bezierHandles = normalizePolygonBezierHandles(options.bezierHandles, next.points, state.textureSize);
      }
      updated = next;
      return next;
    });
    state.layerDocument = normalizeLayerDocument(state.layerDocument, state.textureSize);
    updated = findLayer(layer.id) || updated;
    if (!updated) return null;
    applyLayerToNode(updated);
    if (options.dirty !== false) setDirty(true);
    if (options.renderProperties !== false) renderPropertiesPanel();
    if (options.renderHandles !== false) renderPolygonEditHandles();
    updateCanvasStatus();
    return updated;
  }

  function selectedPolygonEditPointIndices(count = Number.POSITIVE_INFINITY) {
    const limit = Number.isFinite(count) ? count : Number.POSITIVE_INFINITY;
    const selected = Array.isArray(state.polygonEditSelectedPointIndices)
      ? state.polygonEditSelectedPointIndices
      : [];
    return Array.from(new Set(selected
      .map((value) => intValue(value, -1))
      .filter((value) => value >= 0 && value < limit)))
      .sort((a, b) => a - b);
  }

  function isPolygonEditPointSelected(pointIndex) {
    return selectedPolygonEditPointIndices().includes(intValue(pointIndex, -1));
  }

  function syncPolygonEditPointSelection(count = polygonPointCount(findLayer(state.polygonEditLayerId))) {
    const selected = selectedPolygonEditPointIndices(count);
    const active = state.polygonEditPointIndex >= 0 && state.polygonEditPointIndex < count
      ? state.polygonEditPointIndex
      : (selected[0] ?? -1);
    const nextSelected = active >= 0 && !selected.includes(active)
      ? Array.from(new Set([...selected, active])).sort((a, b) => a - b)
      : selected;
    state.polygonEditPointIndex = active;
    state.polygonEditSelectedPointIndices = nextSelected;
    selectPolygonEditPoint(active, { preserveSelection: true });
  }

  function selectPolygonEditPoint(pointIndex, options = {}) {
    const count = polygonPointCount(findLayer(state.polygonEditLayerId));
    const index = Number.isFinite(pointIndex) ? Math.max(-1, Math.min(count - 1, intValue(pointIndex, -1))) : -1;
    const additive = Boolean(options.additive);
    const preserveSelection = Boolean(options.preserveSelection);
    if (preserveSelection) {
      state.polygonEditPointIndex = index;
      state.polygonEditSelectedPointIndices = selectedPolygonEditPointIndices(count);
    } else if (index < 0 || !count) {
      state.polygonEditPointIndex = -1;
      state.polygonEditSelectedPointIndices = [];
    } else if (additive) {
      const selected = selectedPolygonEditPointIndices(count);
      const exists = selected.includes(index);
      const nextSelected = exists && selected.length > 1
        ? selected.filter((value) => value !== index)
        : Array.from(new Set([...selected, index])).sort((a, b) => a - b);
      state.polygonEditSelectedPointIndices = nextSelected;
      state.polygonEditPointIndex = exists && selected.length > 1 ? nextSelected[0] : index;
    } else {
      state.polygonEditPointIndex = index;
      state.polygonEditSelectedPointIndices = [index];
    }
    const group = state.konva?.polygonEditGroup;
    const layer = findLayer(state.polygonEditLayerId);
    const baseFill = DRAFT_OVERLAY_COLOR;
    const selectedStrokeWidth = textureUnitsForScreenPixels(4);
    const defaultStrokeWidth = textureUnitsForScreenPixels(2.5);
    if (!group) return;
    const selectedIndices = selectedPolygonEditPointIndices(polygonPointCount(layer));
    group.find?.(".polygonEditPoint")?.forEach((node) => {
      const selected = selectedIndices.includes(Number(node.getAttr("pointIndex")));
      node.fill(selected ? "#ffffff" : baseFill);
      node.stroke(selected ? baseFill : "#ffffff");
      node.strokeWidth(selected ? selectedStrokeWidth : defaultStrokeWidth);
    });
    group.getLayer()?.batchDraw?.();
  }

  function syncPolygonEditControlHover(target) {
    const group = state.konva?.polygonEditGroup;
    if (!group) return false;
    const targetNode = target && typeof target.hasName === "function" ? target : null;
    const hoveredPoint = targetNode?.hasName?.("polygonEditPoint") ? targetNode : null;
    const hoveredHandle = targetNode?.hasName?.("stripeBezierHandle") ? targetNode : null;
    const selectedStrokeWidth = textureUnitsForScreenPixels(4);
    const defaultStrokeWidth = textureUnitsForScreenPixels(2.5);
    let changed = false;
    const selectedIndices = selectedPolygonEditPointIndices(polygonPointCount(findLayer(state.polygonEditLayerId)));
    group.find?.(".polygonEditPoint")?.forEach((node) => {
      const selected = selectedIndices.includes(Number(node.getAttr("pointIndex")));
      const hovered = node === hoveredPoint;
      const fill = hovered ? POLYGON_EDIT_POINT_HOVER_COLOR : selected ? "#ffffff" : DRAFT_OVERLAY_COLOR;
      const stroke = selected ? DRAFT_OVERLAY_COLOR : "#ffffff";
      const strokeWidth = selected ? selectedStrokeWidth : defaultStrokeWidth;
      if (node.fill() !== fill || node.stroke() !== stroke || node.strokeWidth() !== strokeWidth) changed = true;
      node.fill(fill);
      node.stroke(stroke);
      node.strokeWidth(strokeWidth);
    });
    group.find?.(".stripeBezierHandle")?.forEach((node) => {
      const hovered = node === hoveredHandle;
      const fill = hovered ? BEZIER_HANDLE_HOVER_COLOR : cleanText(node.getAttr("defaultFill"), "#111318");
      const opacity = hovered ? 0.95 : numberValue(node.getAttr("defaultOpacity"), 0.72, 0, 1);
      if (node.fill() !== fill || node.opacity() !== opacity) changed = true;
      node.fill(fill);
      node.opacity(opacity);
    });
    if (changed) group.getLayer()?.batchDraw?.();
    if (hoveredPoint || hoveredHandle) {
      updateCanvasCursor("grab");
      return true;
    }
    return false;
  }

  function renderPolygonEditHandles() {
    const layer = findLayer(state.polygonEditLayerId);
    const uiLayer = state.konva?.uiLayer;
    if (!uiLayer || !window.Konva || !layer || !isPointLayerType(layer.type) || layer.locked || layer.visible === false) {
      clearPolygonEditHandles();
      return;
    }
    clearPolygonEditHandles();
    const points = Array.isArray(layer.points) ? layer.points : [];
    const count = polygonPointCount(layer);
    const isStripe = layer.type === "stripe";
    const minPoints = isStripe ? 2 : 3;
    const handleColor = DRAFT_OVERLAY_COLOR;
    if (count < minPoints) return;
    const selectedPointIndex = state.polygonEditPointIndex >= 0 && state.polygonEditPointIndex < count
      ? state.polygonEditPointIndex
      : -1;
    const pointRadius = textureUnitsForScreenPixels(14);
    const bezierRadius = textureUnitsForScreenPixels(9);
    const pointStrokeWidth = textureUnitsForScreenPixels(2.5);
    const bezierStrokeWidth = textureUnitsForScreenPixels(2);
    const handleLineStrokeWidth = textureUnitsForScreenPixels(1.5);
    const handleLineDash = [textureUnitsForScreenPixels(6), textureUnitsForScreenPixels(5)];
    const addMarkerHalf = textureUnitsForScreenPixels(5);
    const group = new window.Konva.Group({
      name: "polygonEditHandles",
      listening: true,
    });
    const addMarker = new window.Konva.Group({
      name: "polygonEditAddMarker",
      visible: false,
      listening: false,
    });
    addMarker.add(new window.Konva.Circle({
      radius: textureUnitsForScreenPixels(10),
      fill: "#111318",
      stroke: handleColor,
      strokeWidth: textureUnitsForScreenPixels(2),
      opacity: 0.95,
    }));
    addMarker.add(new window.Konva.Line({
      points: [-addMarkerHalf, 0, addMarkerHalf, 0],
      stroke: handleColor,
      strokeWidth: textureUnitsForScreenPixels(2),
    }));
    addMarker.add(new window.Konva.Line({
      points: [0, -addMarkerHalf, 0, addMarkerHalf],
      stroke: handleColor,
      strokeWidth: textureUnitsForScreenPixels(2),
    }));
    group.add(addMarker);
    if (isPointLayerType(layer.type) && selectedPointIndex >= 0) {
      const segmentCount = isStripe ? count - 1 : count;
      const selectedSegments = [];
      if (!isStripe || selectedPointIndex < count - 1) {
        selectedSegments.push({
          segmentIndex: selectedPointIndex,
          handleKey: "out",
          anchorIndex: selectedPointIndex,
        });
      }
      if (selectedPointIndex > 0) {
        selectedSegments.push({
          segmentIndex: selectedPointIndex - 1,
          handleKey: "in",
          anchorIndex: selectedPointIndex,
        });
      } else if (!isStripe) {
        selectedSegments.push({
          segmentIndex: count - 1,
          handleKey: "in",
          anchorIndex: selectedPointIndex,
        });
      }
      selectedSegments
        .filter((entry) => entry.segmentIndex >= 0 && entry.segmentIndex < segmentCount)
        .forEach((selectedSegment) => {
        const segmentIndex = selectedSegment.segmentIndex;
        const handles = bezierHandleForSegment(layer, segmentIndex);
        const selectedHandlePoint = selectedSegment.handleKey === "out" ? handles.out : handles.in;
        const anchor = pointAt(points, selectedSegment.anchorIndex, !isStripe);
        const handleLine = new window.Konva.Line({
          points: [anchor.x, anchor.y, selectedHandlePoint.x, selectedHandlePoint.y],
          stroke: STRIPE_BEZIER_HANDLE_COLOR,
          strokeWidth: handleLineStrokeWidth,
          dash: handleLineDash,
          opacity: handles.active ? 0.8 : 0.45,
          listening: false,
          name: "stripeBezierHandleLine",
          segmentIndex,
          handleKey: selectedSegment.handleKey,
        });
        group.add(handleLine);
        [{
          key: selectedSegment.handleKey,
          point: selectedHandlePoint,
        }].forEach((entry) => {
          const defaultHandleFill = handles.active ? STRIPE_BEZIER_HANDLE_COLOR : "#111318";
          const defaultHandleOpacity = handles.active ? 0.95 : 0.72;
          const handle = new window.Konva.Circle({
            x: entry.point.x,
            y: entry.point.y,
            radius: bezierRadius,
            fill: defaultHandleFill,
            stroke: STRIPE_BEZIER_HANDLE_COLOR,
            strokeWidth: bezierStrokeWidth,
            opacity: defaultHandleOpacity,
            draggable: true,
            name: "stripeBezierHandle",
            segmentIndex,
            handleKey: entry.key,
            defaultFill: defaultHandleFill,
            defaultOpacity: defaultHandleOpacity,
          });
          handle.on("mouseenter", () => {
            updateCanvasCursor("grab");
            handle.fill(BEZIER_HANDLE_HOVER_COLOR);
            handle.opacity(0.95);
            handle.getLayer()?.batchDraw?.();
          });
          handle.on("mouseleave", () => {
            updateCanvasCursor();
            handle.fill(defaultHandleFill);
            handle.opacity(defaultHandleOpacity);
            handle.getLayer()?.batchDraw?.();
          });
          handle.on("dragstart", (event) => {
            if (state.spacePanActive) {
              event.cancelBubble = true;
              handle.stopDrag?.();
              return;
            }
            updateCanvasCursor("grabbing");
            beginHistory(layer.type === "stripe" ? "Edit stripe Bezier handle" : "Edit polygon Bezier handle");
          });
          handle.on("dragmove", (event) => {
            event.cancelBubble = true;
            const active = findLayer(state.polygonEditLayerId);
            if (!active || active.locked || !isPointLayerType(active.type)) return;
            const activeIsStripe = active.type === "stripe";
            const anchor = pointAt(active.points || [], selectedSegment.anchorIndex, !activeIsStripe);
            const rawPoint = normalizeLayerPoint(handle.x(), handle.y());
            const snappedPoint = snapBezierHandlePoint(rawPoint, anchor, Boolean(event.evt?.shiftKey));
            const cleanPoint = normalizeLayerPoint(snappedPoint.x, snappedPoint.y);
            handle.position(cleanPoint);
            const nextHandles = Array.isArray(active.bezierHandles) ? active.bezierHandles.slice() : [];
            const activeSegmentCount = activeIsStripe ? polygonPointCount(active) - 1 : polygonPointCount(active);
            while (nextHandles.length < activeSegmentCount) nextHandles.push(null);
            const current = nextHandles[segmentIndex] || {};
            const fallback = bezierHandleForSegment(active, segmentIndex);
            nextHandles[segmentIndex] = {
              out: entry.key === "out" ? cleanPoint : (current.out || fallback.out),
              in: entry.key === "in" ? cleanPoint : (current.in || fallback.in),
            };
            const opposite = event.evt?.altKey
              ? null
              : oppositeBezierHandleForPoint(active, selectedSegment.anchorIndex, entry.key);
            if (opposite && opposite.segmentIndex >= 0 && opposite.segmentIndex < activeSegmentCount) {
              const oppositeCurrent = nextHandles[opposite.segmentIndex] || {};
              const oppositeFallback = bezierHandleForSegment(active, opposite.segmentIndex);
              const mirrored = mirroredBezierPoint(cleanPoint, anchor);
              nextHandles[opposite.segmentIndex] = {
                out: opposite.handleKey === "out" ? mirrored : (oppositeCurrent.out || oppositeFallback.out),
                in: opposite.handleKey === "in" ? mirrored : (oppositeCurrent.in || oppositeFallback.in),
              };
            }
            setPolygonPoints(active.id, active.points.slice(), { bezierHandles: nextHandles, renderProperties: false, renderHandles: false });
            const updated = findLayer(active.id);
            const next = bezierHandleForSegment(updated, segmentIndex);
            const nextPoint = entry.key === "out" ? next.out : next.in;
            handleLine.points([anchor.x, anchor.y, nextPoint.x, nextPoint.y]);
            handleLine.opacity(0.8);
            const editGroup = state.konva?.polygonEditGroup;
            if (opposite && editGroup) {
              const oppositeNode = editGroup.find?.(".stripeBezierHandle")?.find((node) => (
                Number(node.getAttr("segmentIndex")) === opposite.segmentIndex
                && node.getAttr("handleKey") === opposite.handleKey
              ));
              const oppositeLine = editGroup.find?.(".stripeBezierHandleLine")?.find((node) => (
                Number(node.getAttr("segmentIndex")) === opposite.segmentIndex
                && node.getAttr("handleKey") === opposite.handleKey
              ));
              const oppositeUpdated = bezierHandleForSegment(updated, opposite.segmentIndex);
              const oppositePoint = opposite.handleKey === "out" ? oppositeUpdated.out : oppositeUpdated.in;
              const oppositeAnchor = pointAt(updated.points || [], selectedSegment.anchorIndex, !activeIsStripe);
              oppositeNode?.position?.(oppositePoint);
              oppositeLine?.points?.([oppositeAnchor.x, oppositeAnchor.y, oppositePoint.x, oppositePoint.y]);
              oppositeLine?.opacity?.(0.8);
            }
          });
          handle.on("dragend", (event) => {
            event.cancelBubble = true;
            const active = findLayer(state.polygonEditLayerId);
            if (!active || active.locked || !isPointLayerType(active.type)) return;
            patchLayer(active.id, {
              points: active.points.slice(),
              bezierHandles: active.bezierHandles,
            }, { renderList: false, renderEditHandles: false });
            commitHistory();
            renderPolygonEditHandles();
            updateCanvasCursor();
          });
          group.add(handle);
        });
      });
    }
    for (let index = 0; index < count; index += 1) {
      const point = new window.Konva.Circle({
        x: numberValue(points[index * 2], 0),
        y: numberValue(points[index * 2 + 1], 0),
        radius: pointRadius,
        fill: handleColor,
        stroke: "#ffffff",
        strokeWidth: pointStrokeWidth,
        draggable: true,
        name: "polygonEditPoint",
        pointIndex: index,
      });
      point.on("mousedown tap", (event) => {
        if (shouldIgnoreCanvasObjectAction()) {
          event.cancelBubble = true;
          return;
        }
        event.cancelBubble = true;
        if (!event.evt?.shiftKey && !isPolygonEditPointSelected(index)) {
          selectPolygonEditPoint(index);
        }
      });
      point.on("mouseenter", () => {
        updateCanvasCursor("grab");
        if (Number(point.getAttr("pointIndex")) !== state.polygonEditPointIndex) {
          point.fill(POLYGON_EDIT_POINT_HOVER_COLOR);
          point.getLayer()?.batchDraw?.();
        }
      });
      point.on("mouseleave", () => {
        updateCanvasCursor();
        syncPolygonEditPointSelection(count);
      });
      point.on("click tap", (event) => {
        if (shouldIgnoreCanvasObjectAction()) {
          event.cancelBubble = true;
          return;
        }
        event.cancelBubble = true;
        selectPolygonEditPoint(index, { additive: Boolean(event.evt?.shiftKey) });
        renderPolygonEditHandles();
      });
      point.on("dragstart", (event) => {
        if (state.spacePanActive) {
          event.cancelBubble = true;
          point.stopDrag?.();
          return;
        }
        if (!isPolygonEditPointSelected(index)) selectPolygonEditPoint(index);
        const active = findLayer(state.polygonEditLayerId);
        const selected = selectedPolygonEditPointIndices(polygonPointCount(active));
        state.polygonEditPointDrag = active && isPointLayerType(active.type)
          ? {
              pointIndex: index,
              selectedPointIndices: selected.includes(index) ? selected : [index],
              points: active.points.slice(),
              bezierHandles: Array.isArray(active.bezierHandles) ? active.bezierHandles.slice() : [],
            }
          : null;
        updateCanvasCursor("grabbing");
        beginHistory(layer.type === "stripe" ? "Edit stripe point" : "Edit polygon point");
      });
      point.on("dragmove", (event) => {
        event.cancelBubble = true;
        const active = findLayer(state.polygonEditLayerId);
        if (!active || active.locked || !isPointLayerType(active.type)) return;
        const drag = state.polygonEditPointDrag;
        const rawPoint = { x: point.x(), y: point.y() };
        let nextPoint = rawPoint;
        if (isPointLayerType(active.type) && event.evt?.shiftKey) {
          const points = drag?.points || active.points || [];
          const activeIsStripe = active.type === "stripe";
          const anchor = index > 0
            ? pointAt(points, index - 1, !activeIsStripe)
            : pointAt(points, activeIsStripe ? index + 1 : -1, !activeIsStripe);
          nextPoint = snapPointTo45Degrees(rawPoint, anchor);
        }
        const cleanPoint = normalizeLayerPoint(nextPoint.x, nextPoint.y);
        const sourcePoints = Array.isArray(drag?.points) ? drag.points : active.points.slice();
        const sourceLayer = drag
          ? { ...active, points: sourcePoints, bezierHandles: drag.bezierHandles }
          : active;
        const oldX = numberValue(sourcePoints[index * 2], 0);
        const oldY = numberValue(sourcePoints[index * 2 + 1], 0);
        const dx = cleanPoint.x - oldX;
        const dy = cleanPoint.y - oldY;
        const selected = drag?.selectedPointIndices?.length ? drag.selectedPointIndices : [index];
        const nextPoints = sourcePoints.slice();
        selected.forEach((pointIndex) => {
          const moved = normalizeLayerPoint(
            numberValue(sourcePoints[pointIndex * 2], 0) + dx,
            numberValue(sourcePoints[pointIndex * 2 + 1], 0) + dy,
          );
          nextPoints[pointIndex * 2] = moved.x;
          nextPoints[pointIndex * 2 + 1] = moved.y;
        });
        const nextHandles = isPointLayerType(active.type)
          ? moveBezierHandlesForPoints(sourceLayer, selected, dx, dy)
          : undefined;
        setPolygonPoints(active.id, nextPoints, { bezierHandles: nextHandles, renderProperties: false, renderHandles: false });
        const updated = findLayer(active.id);
        const editGroup = state.konva?.polygonEditGroup;
        if (updated && editGroup) {
          selected.forEach((pointIndex) => {
            const node = editGroup.find?.(".polygonEditPoint")?.find((entry) => Number(entry.getAttr("pointIndex")) === pointIndex);
            node?.position?.(pointAt(updated.points, pointIndex, active.type === "polygon"));
          });
        }
      });
      point.on("dragend", (event) => {
        event.cancelBubble = true;
        const active = findLayer(state.polygonEditLayerId);
        if (!active || active.locked || !isPointLayerType(active.type)) return;
        patchLayer(active.id, {
          points: active.points.slice(),
          ...(isPointLayerType(active.type) ? { bezierHandles: active.bezierHandles } : {}),
        }, { renderList: false, renderEditHandles: false });
        commitHistory();
        state.polygonEditPointDrag = null;
        syncPolygonEditPointSelection(polygonPointCount(active));
        renderPolygonEditHandles();
        updateCanvasCursor();
      });
      group.add(point);
    }
    uiLayer.add(group);
    state.konva.polygonEditGroup = group;
    syncPolygonEditPointSelection(count);
    state.konva?.transformer?.moveToTop?.();
    group.moveToTop();
    uiLayer.batchDraw();
  }

  function enterPolygonEditMode(layerId) {
    const layer = findLayer(layerId);
    if (!layer || !isPointLayerType(layer.type) || layer.locked || layer.visible === false) return false;
    state.activeTool = "select";
    cancelPolygonDraft();
    state.polygonEditLayerId = layer.id;
    state.polygonEditPointIndex = -1;
    state.polygonEditSelectedPointIndices = [];
    state.polygonEditPointDrag = null;
    state.selectedLayerId = layer.id;
    hidePointTransformBox();
    hideGroupTransformBox();
    state.pointTransform = null;
    state.groupTransform = null;
    state.konva?.transformer?.nodes?.([]);
    layerNode(layer.id)?.draggable?.(false);
    updateStageInteractivity();
    renderLayerList();
    renderPropertiesPanel();
    renderGuides();
    updateTransformerForSelection();
    renderPolygonEditHandles();
    setText(
      "layerStudioEditorStatus",
      layer.type === "stripe"
        ? "Stripe point edit mode.\nSelect a gold point to show its blue Bezier handles. Drag handles together, hold Alt to break coupling, Shift to snap."
        : "Polygon point edit mode.\nSelect a gold point to show its blue Bezier handles. Drag handles together, hold Alt to break coupling, Shift to snap.",
    );
    return true;
  }

  function exitPolygonEditMode() {
    const layerId = state.polygonEditLayerId;
    if (!layerId) return;
    state.polygonEditLayerId = "";
    state.polygonEditPointIndex = -1;
    state.polygonEditSelectedPointIndices = [];
    state.polygonEditPointDrag = null;
    state.polygonEditEdgeHover = null;
    state.polygonEditAddDrag = null;
    const layer = findLayer(layerId);
    if (layer) layerNode(layerId)?.draggable?.(isLayerCanvasDraggable(layer));
    clearPolygonEditHandles();
    updateStageInteractivity();
    updateCanvasStatus();
  }

  function deleteSelectedPolygonPoint() {
    const layer = findLayer(state.polygonEditLayerId);
    const pointIndex = state.polygonEditPointIndex;
    if (!layer || !isPointLayerType(layer.type) || layer.locked || pointIndex < 0) return false;
    const count = polygonPointCount(layer);
    const minPoints = layer.type === "stripe" ? 2 : 3;
    const label = layer.type === "stripe" ? "Stripe" : "Polygon";
    if (count <= minPoints) {
      setText("layerStudioEditorStatus", `${label} needs at least ${minPoints} points.`);
      return false;
    }
    const nextPoints = layer.points.slice();
    nextPoints.splice(pointIndex * 2, 2);
    const nextHandles = isPointLayerType(layer.type) && Array.isArray(layer.bezierHandles)
      ? layer.bezierHandles.slice()
      : null;
    if (nextHandles) {
      if (pointIndex === 0) nextHandles.splice(0, 1);
      else nextHandles.splice(pointIndex - 1, 1);
    }
    pushHistory(layer.type === "stripe" ? "Delete stripe point" : "Delete polygon point");
    patchLayer(layer.id, {
      points: nextPoints,
      ...(nextHandles ? { bezierHandles: nextHandles } : {}),
    }, { renderList: false, renderEditHandles: false });
    state.polygonEditPointIndex = Math.min(pointIndex, count - 2);
    renderPolygonEditHandles();
    setText("layerStudioEditorStatus", `${label} point removed.\nPoints: ${count - 1}`);
    return true;
  }

  function setSelectedLayer(layerId, options = {}) {
    const nextId = cleanText(layerId);
    const additive = Boolean(options.additive);
    const nextExists = Boolean(findLayer(nextId));
    if (state.polygonEditLayerId && (!nextExists || state.polygonEditLayerId !== nextId)) exitPolygonEditMode();
    if (!nextExists) {
      state.selectedLayerId = "";
      state.selectedLayerIds = [];
      state.selectedGuideId = "";
    } else if (additive) {
      const ids = selectedLayerIds();
      if (ids.includes(nextId)) {
        state.selectedLayerIds = ids.filter((id) => id !== nextId);
        state.selectedLayerId = state.selectedLayerIds[state.selectedLayerIds.length - 1] || "";
      } else {
        state.selectedLayerIds = [...ids, nextId];
        state.selectedLayerId = nextId;
      }
    } else {
      state.selectedLayerId = nextId;
      state.selectedLayerIds = [nextId];
    }
    syncSelectionState();
    state.hoveredLayerId = "";
    if (state.selectedLayerId) state.selectedGuideId = "";
    renderLayerList();
    renderPropertiesPanel();
    renderGuides();
    updateTransformerForSelection();
    if (state.polygonEditLayerId) renderPolygonEditHandles();
    updateCanvasStatus();
  }

  function redirectGroupedCanvasChildSelection(layerId, event) {
    const layer = findLayer(layerId);
    if (event.evt?.ctrlKey || !layer?.groupId || isLayerSelected(layer.id)) return false;
    const group = findGroup(layer.groupId);
    if (!group || group.locked || group.visible === false) return false;
    event.cancelBubble = true;
    setSelectedLayer(group.id, { additive: Boolean(event.evt?.shiftKey) });
    return true;
  }

  function selectLayerFromCanvas(layerId, event) {
    const layer = findLayer(layerId);
    if (!isLayerCanvasSelectable(layer)) return false;
    if (redirectGroupedCanvasChildSelection(layerId, event)) return false;
    event.cancelBubble = true;
    setSelectedLayer(layer.id, { additive: Boolean(event.evt?.shiftKey) });
    return true;
  }

  function guardGroupedCanvasChildDrag(layerId, event, node) {
    const layer = findLayer(layerId);
    if (event.evt?.ctrlKey && layer?.groupId && !isLayerSelected(layer.id)) {
      setSelectedLayer(layer.id);
      return false;
    }
    if (!redirectGroupedCanvasChildSelection(layerId, event)) return false;
    node?.stopDrag?.();
    return true;
  }

  function reorderLayerByDrop(sourceId, targetId) {
    const source = cleanText(sourceId);
    const target = cleanText(targetId);
    if (!source || !target || source === target || isUvOverlayLayerId(source) || isUvOverlayLayerId(target) || source === BASE_COLOR_LAYER_ID || target === BASE_COLOR_LAYER_ID) return false;
    const sorted = layerList().slice().sort((left, right) => (left.order - right.order) || left.id.localeCompare(right.id));
    const fromIndex = sorted.findIndex((layer) => layer.id === source);
    const toIndex = sorted.findIndex((layer) => layer.id === target);
    if (fromIndex < 0 || toIndex < 0) return false;
    pushHistory("Reorder layer");
    const [moved] = sorted.splice(fromIndex, 1);
    sorted.splice(toIndex, 0, moved);
    sorted.forEach((layer, index) => {
      layer.order = index;
    });
    state.layerDocument.layers = sorted;
    syncKonvaLayerOrder();
    state.selectedLayerId = source;
    state.selectedLayerIds = [source];
    setDirty(true);
    return true;
  }

  function normalizeNodeTransform(node) {
    const id = node?.id?.();
    const layer = findLayer(id);
    if (!node || !layer) return null;
    const scaleX = node.scaleX() || 1;
    const scaleY = node.scaleY() || 1;
    if (isTextLayerType(layer.type)) {
      const absScaleX = Math.max(0.05, Math.abs(scaleX));
      const absScaleY = Math.max(0.05, Math.abs(scaleY));
      const proportional = Math.abs(absScaleX - absScaleY) < 0.001;
      const patch = {
        x: Math.round(node.x()),
        y: Math.round(node.y()),
        rotation: Number(node.rotation() || 0),
      };
      if (proportional) {
        patch.fontSize = Math.max(4, Math.round((layer.fontSize || 96) * absScaleX));
      } else {
        patch.width = Math.max(1, Math.round(node.width() * absScaleX));
        patch.height = Math.max(1, Math.round(node.height() * absScaleY));
      }
      node.scale({ x: 1, y: 1 });
      const updated = patchLayer(id, patch, { updateNode: false });
      if (updated) applyLayerToNode(updated);
      if (updated) setDirty(true);
      return updated;
    }
    const width = Math.max(1, Math.round(node.width() * Math.abs(scaleX)));
    const height = Math.max(1, Math.round(node.height() * Math.abs(scaleY)));
    const nextFlipX = layer.type === "image" ? scaleX < 0 : undefined;
    const nextFlipY = layer.type === "image" ? scaleY < 0 : undefined;
    node.scale({ x: 1, y: 1 });
    node.offset({ x: 0, y: 0 });
    node.size({ width, height });
    const patch = {
      x: Math.round(node.x()),
      y: Math.round(node.y()),
      width,
      height,
      rotation: Number(node.rotation() || 0),
    };
    if (layer.type === "image") {
      patch.flipX = nextFlipX;
      patch.flipY = nextFlipY;
    }
    const updated = patchLayer(id, patch, { updateNode: false });
    if (updated) applyLayerToNode(updated);
    if (updated) setDirty(true);
    return updated;
  }

  function uvOverlayPath(context = state.context, textureKey = null, overlayRole = "base") {
    const project = context?.previewProject || {};
    const activeKey = surfaceKey(textureKey || context?.activeSurfaceKey || state.activeSurfaceKey);
    const surface = textureSurfaceByKey(activeKey, context);
    const isBanner = activeKey === "ext_banner_c";
    const role = cleanText(overlayRole || "base");
    if (role === "parts") {
      if (activeKey === "ext_banner_c") return "";
      const partsTemplate = project.partsUvTemplate && typeof project.partsUvTemplate === "object" ? project.partsUvTemplate : {};
      const rawParts = cleanText(
        partsTemplate.path
        || partsTemplate.uvTemplatePngPath
        || project.partsUvTemplatePath
        || context?.partsUvTemplate?.path
        || "",
      );
      if (!rawParts) return "";
      if (/^[a-zA-Z]:[\\/]/.test(rawParts) || rawParts.startsWith("\\\\")) return rawParts;
      const folder = cleanText(context?.projectFolder || project.projectFolder);
      if (!folder) return rawParts;
      return `${folder.replace(/[\\/]+$/, "")}\\${rawParts.replace(/^[\\/]+/, "").replace(/\//g, "\\")}`;
    }
    const surfaceTemplate = surface?.uvTemplate && typeof surface.uvTemplate === "object" ? surface.uvTemplate : {};
    const template = Object.keys(surfaceTemplate).length
      ? surfaceTemplate
      : isBanner
      ? (project.bannerUvTemplate && typeof project.bannerUvTemplate === "object" ? project.bannerUvTemplate : {})
      : (project.uvTemplate && typeof project.uvTemplate === "object" ? project.uvTemplate : {});
    const raw = cleanText(
      template.path
      || template.uvTemplatePngPath
      || surface?.uvTemplatePath
      || (isBanner ? project.bannerUvTemplatePath : project.uvTemplatePath)
      || (isBanner ? context?.bannerUvTemplate?.path : context?.uvTemplate?.path)
      || "",
    );
    if (!raw) {
      const folder = cleanText(context?.projectFolder || project.projectFolder);
      return folder ? `${folder.replace(/[\\/]+$/, "")}\\${isBanner ? "EXT_BANNER_UV.png" : "EXT_SKIN_UV.png"}` : "";
    }
    if (/^[a-zA-Z]:[\\/]/.test(raw) || raw.startsWith("\\\\")) return raw;
    const folder = cleanText(context?.projectFolder || project.projectFolder);
    if (!folder) return raw;
    return `${folder.replace(/[\\/]+$/, "")}\\${raw.replace(/^[\\/]+/, "").replace(/\//g, "\\")}`;
  }

  async function createInvertedImageElement(image) {
    const width = Math.max(1, Number(image?.naturalWidth || image?.width || state.textureSize.width || 1));
    const height = Math.max(1, Number(image?.naturalHeight || image?.height || state.textureSize.height || 1));
    const canvas = document.createElement("canvas");
    canvas.width = width;
    canvas.height = height;
    const ctx = canvas.getContext("2d");
    if (!ctx) return image;
    ctx.drawImage(image, 0, 0, width, height);
    const imageData = ctx.getImageData(0, 0, width, height);
    const data = imageData.data;
    for (let index = 0; index < data.length; index += 4) {
      data[index] = 255 - data[index];
      data[index + 1] = 255 - data[index + 1];
      data[index + 2] = 255 - data[index + 2];
    }
    ctx.putImageData(imageData, 0, 0);
    return await loadCanvasImage(canvas.toDataURL("image/png"));
  }

  async function refreshUvOverlayLayer(config) {
    const requestId = ++state.uvOverlayRequestId;
    const uiLayer = state.konva?.uiLayer;
    if (!uiLayer || !window.Konva) return false;
    const existing = state.konva[config.nodeKey];
    if (!state[config.visibleKey]) {
      if (existing) {
        existing.visible(false);
        uiLayer.batchDraw();
      }
      return false;
    }
    const path = uvOverlayPath(state.context, null, config.role);
    if (!path || !window.pywebview?.api?.load_image_preview) {
      if (existing) {
        existing.visible(false);
        uiLayer.batchDraw();
      }
      return false;
    }
    const cacheKey = `${config.cachePrefix}|${path}`;
    try {
      if (!state.imageCache.has(cacheKey)) {
        state.imageCache.set(cacheKey, (async () => {
          const result = await window.pywebview.api.load_image_preview(path, "layer-studio-uv-overlay");
          if (!result?.ok || !(result.data?.data_url || result.data?.dataUrl)) {
            throw new Error(result?.message || `UV overlay could not be loaded: ${path}`);
          }
          return await loadCanvasImage(result.data.data_url || result.data.dataUrl);
        })());
      }
      const baseImage = await state.imageCache.get(cacheKey);
      if (requestId !== state.uvOverlayRequestId || !baseImage) return false;
      let image = baseImage;
      if (state[config.invertedKey]) {
        const invertedCacheKey = `${config.cachePrefix}-inverted|${path}`;
        if (!state.imageCache.has(invertedCacheKey)) {
          state.imageCache.set(invertedCacheKey, createInvertedImageElement(baseImage));
        }
        image = await state.imageCache.get(invertedCacheKey);
      }
      if (requestId !== state.uvOverlayRequestId || !image) return false;
      let node = state.konva[config.nodeKey];
      if (!node) {
        node = new window.Konva.Image({
          image,
          x: 0,
          y: 0,
          width: state.textureSize.width,
          height: state.textureSize.height,
          opacity: 0.9,
          visible: true,
          listening: false,
          name: config.nodeKey,
        });
        uiLayer.add(node);
        state.konva[config.nodeKey] = node;
      } else {
        node.image(image);
        node.visible(true);
        node.size({ width: state.textureSize.width, height: state.textureSize.height });
      }
      node.moveToTop();
      state.konva.transformer?.moveToTop?.();
      uiLayer.batchDraw();
      return true;
    } catch (error) {
      setText("layerStudioEditorStatus", `UV overlay failed:\n${error?.message || String(error)}`);
      return false;
    }
  }

  async function refreshUvOverlay() {
    const configs = [uvOverlayLayerConfig(UV_OVERLAY_BASE_ID), uvOverlayLayerConfig(UV_OVERLAY_PARTS_ID)];
    const results = [];
    for (const config of configs) {
      results.push(await refreshUvOverlayLayer(config));
    }
    return results.some(Boolean);
  }

  async function loadPreviewImageDataUrl(path, namespace) {
    const cleanPath = cleanText(path);
    if (!cleanPath || !window.pywebview?.api?.load_image_preview) return "";
    const result = await window.pywebview.api.load_image_preview(cleanPath, namespace || "layer-studio-preview");
    if (!result?.ok || !(result.data?.data_url || result.data?.dataUrl)) {
      throw new Error(result?.message || `Preview image could not be loaded: ${cleanPath}`);
    }
    return result.data.data_url || result.data.dataUrl;
  }

  async function createPreviewTextureDataUrl(textureSourcePath, textureKey = null, options = {}) {
    if (!state.previewUvMeshVisible) return "";
    const normalizedTextureKey = surfaceKey(textureKey || state.activeSurfaceKey);
    const sourcePath = cleanText(textureSourcePath || texturePath());
    const overlayConfigs = [
      uvOverlayLayerConfig(UV_OVERLAY_BASE_ID),
      uvOverlayLayerConfig(UV_OVERLAY_PARTS_ID),
    ];
    const overlaySources = overlayConfigs
      .filter((config) => state[config.visibleKey])
      .map((config) => ({
        ...config,
        path: uvOverlayPath(state.context, normalizedTextureKey, config.role),
      }))
      .filter((config) => cleanText(config.path));
    if (!sourcePath || !overlaySources.length) return "";
    try {
      const baseUrl = await loadPreviewImageDataUrl(sourcePath, "layer-studio-preview-base");
      const baseImage = await loadCanvasImage(baseUrl);
      const overlayImages = [];
      for (const config of overlaySources) {
        const overlayUrl = await loadPreviewImageDataUrl(config.path, `layer-studio-preview-${config.role}-uv`);
        const baseOverlayImage = await loadCanvasImage(overlayUrl);
        const image = state[config.invertedKey]
          ? await createInvertedImageElement(baseOverlayImage)
          : baseOverlayImage;
        overlayImages.push(image);
      }
      const width = Math.max(1, Number(baseImage.naturalWidth || baseImage.width || state.textureSize.width || 1));
      const height = Math.max(1, Number(baseImage.naturalHeight || baseImage.height || state.textureSize.height || 1));
      const canvas = document.createElement("canvas");
      canvas.width = width;
      canvas.height = height;
      const ctx = canvas.getContext("2d");
      if (!ctx) return "";
      ctx.clearRect(0, 0, width, height);
      if (options.overlayOnly !== true) ctx.drawImage(baseImage, 0, 0, width, height);
      ctx.globalAlpha = 0.88;
      overlayImages.forEach((image) => {
        ctx.drawImage(image, 0, 0, width, height);
      });
      ctx.globalAlpha = 1;
      return canvas.toDataURL("image/png");
    } catch (error) {
      const message = error?.message || String(error);
      if (!(normalizedTextureKey === "ext_banner_c" && /not found|cannot find|not exist|no such file|errno 2|filenotfound/i.test(message))) {
        setText("layerStudioPreviewStatus", `UV Mesh overlay skipped:\n${message}`);
      }
      return "";
    }
  }

  function clampMaterialFinishValue(value, fallback) {
    const number = Number(value);
    if (!Number.isFinite(number)) return fallback;
    return Math.min(1, Math.max(0, number));
  }

  function normalizeLayerStudioMaterialFinish(value) {
    const sharedNormalize = window.LiveryLabMaterialFinish?.normalize;
    if (typeof sharedNormalize === "function") return sharedNormalize(value);
    const raw = value && typeof value === "object" ? value : {};
    return {
      metalness: clampMaterialFinishValue(raw.metalness, MATERIAL_FINISH_DEFAULT.metalness),
      roughness: clampMaterialFinishValue(raw.roughness, MATERIAL_FINISH_DEFAULT.roughness),
      clearCoat: clampMaterialFinishValue(raw.clearCoat, MATERIAL_FINISH_DEFAULT.clearCoat),
      clearCoatRoughness: clampMaterialFinishValue(raw.clearCoatRoughness, MATERIAL_FINISH_DEFAULT.clearCoatRoughness),
      scope: "base",
    };
  }

  function formatMaterialFinishValue(value) {
    return window.LiveryLabMaterialFinish?.format?.(value)
      || clampMaterialFinishValue(value, 0).toFixed(2);
  }

  function sharedMaterialFinish() {
    return window.LiveryLabMaterialFinish || null;
  }

  function syncLayerStudioPaintFinishPanel() {
    const finish = normalizeLayerStudioMaterialFinish(state.previewMaterialFinish);
    const valueLabels = {
      metalness: "layerStudioFinishMetalnessValue",
      roughness: "layerStudioFinishRoughnessValue",
      clearCoat: "layerStudioFinishClearCoatValue",
      clearCoatRoughness: "layerStudioFinishClearCoatRoughnessValue",
    };
    Object.entries(valueLabels).forEach(([key, id]) => {
      const text = formatMaterialFinishValue(finish[key]);
      setText(id, text);
      document.querySelectorAll(`[data-layer-studio-finish-field="${key}"]`).forEach((input) => {
        input.value = text;
      });
    });
    const panel = $("layerStudioPaintFinishPanel");
    $("layerStudioPreviewPaintFinish")?.classList.toggle("active", Boolean(panel && !panel.hidden));
  }

  function setLayerStudioPaintFinishPanelVisible(visible) {
    const panel = $("layerStudioPaintFinishPanel");
    if (!panel) return;
    panel.hidden = !visible;
    syncLayerStudioPaintFinishPanel();
    if (visible) {
      closeLayerStudioPreviewMenus();
      setText("layerStudioPreviewStatus", "Paint Finish panel opened.");
    }
  }

  function setLayerStudioPaintFinish(values, options = {}) {
    const finish = sharedMaterialFinish()?.normalize?.(values) || normalizeLayerStudioMaterialFinish(values);
    state.previewMaterialFinish = finish;
    if (state.project) state.project.materialFinish = finish;
    if (state.context) state.context.materialFinish = finish;
    if (!options.fromShared) {
      sharedMaterialFinish()?.set?.(finish, {
        quiet: true,
        source: options.source || "layerStudio",
      });
    }
    window.LiveryLabPreview3d?.setMaterialViewFinish?.(finish);
    syncLayerStudioPaintFinishPanel();
    if (!options.silent) setText("layerStudioPreviewStatus", "Paint finish applied to the 3D preview.");
    return finish;
  }

  function syncLayerStudioPaintFinishFromShared(finish) {
    const normalized = normalizeLayerStudioMaterialFinish(finish);
    state.previewMaterialFinish = normalized;
    if (state.project) state.project.materialFinish = normalized;
    if (state.context) state.context.materialFinish = normalized;
    window.LiveryLabPreview3d?.setMaterialViewFinish?.(normalized);
    syncLayerStudioPaintFinishPanel();
  }

  function normalizeLayerStudioBodyBaseColor(value) {
    return cssColorValue(value, BODY_BASE_COLOR_DEFAULT).slice(0, 7);
  }

  function layerStudioBodyBaseColorEnabled(project = state.project || {}) {
    return project?.bodyBaseColorEnabled === true;
  }

  function syncLayerStudioBodyBaseColorControls() {
    const input = $("layerStudioBodyBaseColor");
    const save = $("layerStudioSaveBodyBaseColor");
    const label = $("layerStudioBodyBaseColorLabel");
    const wrap = $("layerStudioBodyBaseColorWrap");
    const enabled = layerStudioBodyBaseColorEnabled();
    const mode = cleanText(state.project?.bodyBaseColorMode || state.project?.bodyBaseColorCapability?.mode).toLowerCase();
    const synced = mode === "saved_car_synced";
    const color = normalizeLayerStudioBodyBaseColor(state.bodyBaseColor || state.project?.bodyBaseColor || BODY_BASE_COLOR_DEFAULT);
    if (input) {
      input.value = color;
      input.disabled = !enabled;
    }
    if (save) {
      save.disabled = !enabled;
      save.textContent = synced ? "Apply Body Color to Game" : "Save Fixed Livery Color";
    }
    if (label) label.textContent = synced ? "AC EVO Body Color" : "Fixed Livery Color";
    if (wrap) {
      wrap.classList.toggle("is-disabled", !enabled);
      wrap.title = enabled ? (synced ? "Synchronized with the active AC EVO SavedCar." : "This car does not expose an editable native Body Color.") : "This livery does not expose a body color.";
    }
  }

  async function setLayerStudioBodyBaseColor(value, options = {}) {
    const color = normalizeLayerStudioBodyBaseColor(value);
    state.bodyBaseColor = color;
    if (state.project) state.project.bodyBaseColor = color;
    if (state.context?.previewProject) state.context.previewProject.bodyBaseColor = color;
    syncLayerStudioBodyBaseColorControls();
    const update = window.LiveryLabPreview3d?.setMaterialViewBodyBaseColor?.(color);
    window.LiveryLabPreview3d?.setMaterialViewBodyBaseColorEnabled?.(
      layerStudioPreviewMode() === "finalLivery" && layerStudioBodyBaseColorEnabled(),
    );
    if (!options.quiet) {
      setText(
        "layerStudioPreviewStatus",
        [
          `In-game Body Color: ${color}`,
          layerStudioPreviewMode() === "finalLivery" ? "Final Livery preview updated." : "Switch to Final Livery to preview body-color areas.",
          `Updated materials: ${update?.updated ?? 0}`,
        ].join("\n"),
      );
    }
    return color;
  }

  async function saveLayerStudioBodyBaseColor() {
    if (!layerStudioBodyBaseColorEnabled()) {
      setText("layerStudioPreviewStatus", "In-game Body Color is not available for this livery.");
      return null;
    }
    const officialProject = cleanText(state.project?.projectType).toLowerCase() === "official_external_livery";
    const saveMethod = window.pywebview?.api?.save_preview3d_project;
    if (typeof saveMethod !== "function") {
      setText("layerStudioPreviewStatus", "Body color saving needs an updated LiveryLab.exe build.");
      return null;
    }
    const payload = await layerStudioPreviewProject({ previewMode: "finalLivery" });
    payload.bodyBaseColor = normalizeLayerStudioBodyBaseColor(state.bodyBaseColor || payload.bodyBaseColor || BODY_BASE_COLOR_DEFAULT);
    const result = await saveMethod.call(window.pywebview.api, payload);
    if (!result?.ok) {
      setText("layerStudioPreviewStatus", `In-game Body Color save failed:\n${result?.message || ""}`);
      return null;
    }
    const savedProject = result.data?.project || {};
    const saveStatus = officialProject ? savedProject : (result.data || {});
    state.project = {
      ...(state.project || {}),
      bodyBaseColor: payload.bodyBaseColor,
      ...(savedProject.bodyBaseColor ? { bodyBaseColor: savedProject.bodyBaseColor } : {}),
      ...(savedProject.bodyBaseColorMode ? { bodyBaseColorMode: savedProject.bodyBaseColorMode } : {}),
      ...(savedProject.bodyBaseColorCapability ? { bodyBaseColorCapability: savedProject.bodyBaseColorCapability } : {}),
    };
    if (savedProject.officialMaterialChannels) state.project.officialMaterialChannels = savedProject.officialMaterialChannels;
    if (state.context?.previewProject) state.context.previewProject = { ...state.context.previewProject, bodyBaseColor: state.project.bodyBaseColor };
    state.bodyBaseColor = normalizeLayerStudioBodyBaseColor(state.project.bodyBaseColor || payload.bodyBaseColor);
    syncLayerStudioBodyBaseColorControls();
    setText(
      "layerStudioPreviewStatus",
      saveStatus.gameUpdateRequired
        ? `In-game Body Color saved: ${state.bodyBaseColor}\nUpdate in Game is required.`
        : `${cleanText(savedProject.bodyBaseColorMode).toLowerCase() === "saved_car_synced" ? "AC EVO Body Color applied" : "Fixed Livery Color saved"}: ${state.bodyBaseColor}`,
    );
    if (officialProject) {
      showUpdateTextureFeedback({
        changed: saveStatus.changed === true,
        gameUpdateRequired: saveStatus.gameUpdateRequired === true,
        surfaceLabel: "Body color",
      });
    }
    return result.data || null;
  }

  function layerStudioPreviewMode() {
    return state.previewMode === "finalLivery" ? "finalLivery" : "textureSlot";
  }

  function labelForLayerStudioPreviewMode(mode = layerStudioPreviewMode()) {
    return mode === "finalLivery" ? "Final Livery" : "Texture Slot";
  }

  async function layerStudioPreviewProject(options = {}) {
    const mode = cleanText(options.previewMode || layerStudioPreviewMode(), "textureSlot") === "finalLivery" ? "finalLivery" : "textureSlot";
    const activeKey = surfaceKey(options.textureKey || state.activeSurfaceKey || "ext_skin_c");
    const previewBodySurfaceKey = mode === "finalLivery"
      ? "ext_skin_c"
      : activeKey === "ext_banner_c" ? "ext_skin_c" : activeKey;
    const project = {
      ...(state.project || {}),
      layerStudioSurfaceKey: activeKey,
      layerStudioPreviewMode: mode,
      canvasId: "layerStudioPreviewCanvas",
      emptyId: "layerStudioPreviewEmpty",
      statusId: "layerStudioPreviewStatus",
      statusPrefix: "Layer Studio 3D preview",
      sceneMode: state.previewScene || "light",
      cameraPreset: state.previewCameraPreset || "free",
      previewMode: mode === "finalLivery" ? "materialView" : "textureInspect",
      bodyBaseColor: normalizeLayerStudioBodyBaseColor(state.bodyBaseColor || state.project?.bodyBaseColor || BODY_BASE_COLOR_DEFAULT),
      materialFinish: state.previewMaterialFinish || state.project?.materialFinish || state.context?.materialFinish || null,
    };
    if (options.preserveCameraView === true) project.preserveCameraView = true;
    else delete project.preserveCameraView;
    if (mode === "textureSlot" && activeKey === "ext_skin_d") {
      project.bodyBaseColorEnabled = false;
    } else if (mode === "finalLivery") {
      project.bodyBaseColorEnabled = layerStudioBodyBaseColorEnabled(project);
    }
    const nextTexturePath = cleanText(options.texturePath || texturePathForSurface(previewBodySurfaceKey) || texturePathForSurface("ext_skin_c") || project.texturePath || project.liveTexturePath);
    const nextDetailTexturePath = cleanText(options.detailTexturePath || texturePathForSurface("ext_skin_d") || project.detailTexturePath || project.liveDetailTexturePath);
    const nextBannerTexturePath = cleanText(options.bannerTexturePath || texturePathForSurface("ext_banner_c") || project.bannerTexturePath || project.liveBannerTexturePath);
    if (nextTexturePath) {
      project.texturePath = nextTexturePath;
      project.liveTexturePath = nextTexturePath;
    }
    if (nextDetailTexturePath) {
      project.detailTexturePath = nextDetailTexturePath;
      project.liveDetailTexturePath = nextDetailTexturePath;
    }
    if (nextBannerTexturePath) {
      project.bannerTexturePath = nextBannerTexturePath;
      project.liveBannerTexturePath = nextBannerTexturePath;
    }
    if (options.textureDataUrl) {
      project.textureDataUrl = options.textureDataUrl;
    } else {
      delete project.textureDataUrl;
    }
    if (state.previewUvMeshVisible && nextTexturePath) {
      const uvMeshOverlayDataUrl = await createPreviewTextureDataUrl(nextTexturePath, previewBodySurfaceKey, { overlayOnly: true });
      if (uvMeshOverlayDataUrl) project.uvMeshOverlayDataUrl = uvMeshOverlayDataUrl;
    } else {
      delete project.uvMeshOverlayDataUrl;
    }
    if (options.bannerTextureDataUrl) {
      project.bannerTextureDataUrl = options.bannerTextureDataUrl;
    } else if (state.previewUvMeshVisible && nextBannerTexturePath) {
      const bannerTextureDataUrl = await createPreviewTextureDataUrl(nextBannerTexturePath, "ext_banner_c");
      if (bannerTextureDataUrl) project.bannerTextureDataUrl = bannerTextureDataUrl;
    }
    if (!state.previewUvMeshVisible) delete project.bannerTextureDataUrl;
    return project;
  }

  function syncPreviewMenuState() {
    const floor = $("layerStudioPreviewShowFloor");
    const uv = $("layerStudioPreviewShowUvMesh");
    if (floor) floor.checked = state.previewFloorVisible;
    if (uv) uv.checked = state.previewUvMeshVisible;
    syncLayerStudioPreviewTextureButtons();
    syncLayerStudioBodyBaseColorControls();
    document.querySelectorAll("[data-layer-studio-preview-mode]").forEach((button) => {
      const mode = cleanText(button.dataset.layerStudioPreviewMode, "textureSlot") === "finalLivery" ? "finalLivery" : "textureSlot";
      const active = mode === layerStudioPreviewMode();
      button.classList.toggle("active", active);
      button.setAttribute("aria-checked", active ? "true" : "false");
    });
    document.querySelectorAll("[data-layer-studio-camera-preset]").forEach((button) => {
      button.classList.toggle("active", button.dataset.layerStudioCameraPreset === state.previewCameraPreset);
    });
    document.querySelectorAll("[data-layer-studio-preview-scene]").forEach((button) => {
      const active = button.dataset.layerStudioPreviewScene === state.previewScene;
      button.classList.toggle("active", active);
      button.setAttribute("aria-checked", active ? "true" : "false");
    });
  }

  function syncLayerStudioPreviewTextureButtons() {
    const surfaces = textureSurfaces();
    const activeKey = surfaceKey(state.activeSurfaceKey);
    const busy = state.textureSwitching || state.baking;
    const finalMode = layerStudioPreviewMode() === "finalLivery";
    document.querySelectorAll("[data-layer-studio-preview-texture-key]").forEach((button) => {
      const key = surfaceKey(button.dataset.layerStudioPreviewTextureKey);
      const surface = surfaces.find((row) => surfaceKey(row.key) === key) || null;
      const active = key === activeKey;
      button.classList.toggle("active", active);
      button.setAttribute("aria-checked", active ? "true" : "false");
      button.disabled = busy || finalMode || !surface;
      button.title = finalMode
        ? "Texture slots are controlled by the Final Livery preview."
        : surface ? "" : "Texture slot is not available for this project.";
    });
  }

  function closeLayerStudioPreviewMenus(except = null) {
    document.querySelectorAll("#layerStudioPreviewMenu .livery-preview-menu-group.is-open").forEach((group) => {
      if (except && group === except) return;
      group.classList.remove("is-open");
      group.querySelector(".livery-preview-menu-trigger")?.setAttribute("aria-expanded", "false");
    });
  }

  function openLayerStudioPreviewMenu(group) {
    if (!group || group.classList.contains("is-disabled")) return;
    closeLayerStudioPreviewMenus(group);
    group.classList.add("is-open");
    group.querySelector(".livery-preview-menu-trigger")?.setAttribute("aria-expanded", "true");
  }

  function initLayerStudioPreviewMenuController() {
    const menu = $("layerStudioPreviewMenu");
    if (!menu || menu.dataset.controllerReady === "1") return;
    menu.dataset.controllerReady = "1";
    const groups = [...menu.querySelectorAll(".livery-preview-menu-group")];
    groups.forEach((group) => {
      const trigger = group.querySelector(".livery-preview-menu-trigger");
      const dropdown = group.querySelector(".livery-preview-menu-dropdown");
      trigger?.setAttribute("aria-haspopup", "menu");
      trigger?.setAttribute("aria-expanded", "false");
      group.addEventListener("pointerenter", () => openLayerStudioPreviewMenu(group));
      trigger?.addEventListener("focus", () => openLayerStudioPreviewMenu(group));
      trigger?.addEventListener("click", (event) => {
        event.preventDefault();
        if (group.classList.contains("is-open")) closeLayerStudioPreviewMenus();
        else openLayerStudioPreviewMenu(group);
      });
      dropdown?.addEventListener("click", (event) => {
        const target = event.target;
        if (target?.closest?.("input, select, label")) return;
        if (target?.closest?.("button")) closeLayerStudioPreviewMenus();
      });
    });
    menu.addEventListener("pointerleave", () => window.setTimeout(() => closeLayerStudioPreviewMenus(), 120));
    document.addEventListener("pointerdown", (event) => {
      if (!menu.contains(event.target)) closeLayerStudioPreviewMenus();
    });
    document.addEventListener("keydown", (event) => {
      if (event.key === "Escape") closeLayerStudioPreviewMenus();
    });
    syncPreviewMenuState();
  }

  async function reloadLayerStudioPreviewTexture(options = {}) {
    const mode = layerStudioPreviewMode();
    const activeKey = surfaceKey(state.activeSurfaceKey);
    const sourcePath = mode === "finalLivery"
      ? cleanText(options.texturePath || texturePathForSurface("ext_skin_c"))
      : cleanText(options.texturePath || texturePath());
    const detailPath = mode === "finalLivery"
      ? cleanText(options.detailTexturePath || texturePathForSurface("ext_skin_d") || state.project?.detailTexturePath || state.project?.liveDetailTexturePath)
      : "";
    if (!sourcePath) {
      setText("layerStudioPreviewStatus", mode === "finalLivery" ? "No EXT_SKIN_C texture is available for Final Livery preview." : "No texture is available for the 3D preview.");
      return null;
    }
    const forceModelReload = options.forceModelReload === true || (state.previewUvMeshVisible && options.forceModelReload !== false);
    setText(
      "layerStudioPreviewStatus",
      forceModelReload
        ? (state.previewUvMeshVisible ? `Reloading ${labelForLayerStudioPreviewMode(mode)} with UV Mesh overlay...` : `Reloading ${labelForLayerStudioPreviewMode(mode)}...`)
        : (state.previewUvMeshVisible ? `Reloading ${labelForLayerStudioPreviewMode(mode)} texture with UV Mesh overlay...` : `Reloading ${labelForLayerStudioPreviewMode(mode)} texture...`),
    );
    const previewOptions = {};
    if (mode === "finalLivery") {
      previewOptions.texturePath = sourcePath;
      if (detailPath) previewOptions.detailTexturePath = detailPath;
    } else if (activeKey === "ext_banner_c") {
      previewOptions.bannerTexturePath = sourcePath;
    } else if (activeKey === "ext_skin_d") {
      previewOptions.detailTexturePath = sourcePath;
    } else {
      previewOptions.texturePath = sourcePath;
    }
    const previewProject = await layerStudioPreviewProject({ ...previewOptions, preserveCameraView: forceModelReload });
    if (mode === "textureSlot" && activeKey === "ext_skin_d") {
      previewProject.detailTexturePath = sourcePath;
      previewProject.liveDetailTexturePath = sourcePath;
    }
    let result = null;
    if (forceModelReload) {
      result = await app().loadPreview?.({ ...(state.context || {}), project: previewProject });
    } else if (window.LiveryLabPreview3d?.hasLoadedModel?.()) {
      result = await window.LiveryLabPreview3d.reloadTexture(previewProject);
    } else {
      result = await app().loadPreview?.({ project: previewProject });
    }
    const applied = Number(result?.appliedMaterials || 0);
    setText(
      "layerStudioPreviewStatus",
      [
        `${labelForLayerStudioPreviewMode(mode)} reloaded.`,
        `Texture: ${sourcePath}`,
        mode === "finalLivery" && detailPath ? `Splat map: ${detailPath}` : "",
        state.previewUvMeshVisible ? "UV Mesh overlay: visible" : "UV Mesh overlay: hidden",
        applied ? `Applied materials: ${applied}` : "",
      ].filter(Boolean).join("\n"),
    );
    return result;
  }

  function setLayerStudioPreviewScene(sceneName) {
    state.previewScene = cleanText(sceneName, "light") === "dark" ? "dark" : "light";
    syncPreviewMenuState();
    window.LiveryLabPreview3d?.setSceneMode?.(state.previewScene);
    setText("layerStudioPreviewStatus", `Scene: ${state.previewScene === "dark" ? "Dark Scene" : "Light Scene"}`);
  }

  function setLayerStudioPreviewFloorVisible(visible) {
    state.previewFloorVisible = Boolean(visible);
    syncPreviewMenuState();
    window.LiveryLabPreview3d?.setFloorVisible?.(state.previewFloorVisible);
  }

  async function setLayerStudioPreviewMode(mode) {
    const nextMode = cleanText(mode, "textureSlot") === "finalLivery" ? "finalLivery" : "textureSlot";
    if (state.previewMode === nextMode) {
      syncPreviewMenuState();
      return;
    }
    state.previewMode = nextMode;
    syncPreviewMenuState();
    await reloadLayerStudioPreviewTexture();
  }

  function labelForLayerStudioCameraPreset(preset) {
    const labels = {
      free: "Free Cam",
      front: "Front",
      rear: "Rear",
      left: "Left",
      right: "Right",
      top: "Top",
      "front-three-quarter": "Front 3/4",
      "rear-three-quarter": "Rear 3/4",
    };
    return labels[preset] || labels.free;
  }

  function setLayerStudioPreviewCameraPreset(preset, options = {}) {
    const next = cleanText(preset, "free") || "free";
    state.previewCameraPreset = next;
    syncPreviewMenuState();
    const result = window.LiveryLabPreview3d?.setCameraPreset?.(next);
    if (!options.silent) {
      setText(
        "layerStudioPreviewStatus",
        [
          `Camera: ${labelForLayerStudioCameraPreset(result?.preset || next)}`,
          result?.locked ? "Rotation locked. Pan and zoom are enabled." : "Free orbit enabled.",
        ].join("\n"),
      );
    }
  }

  async function setLayerStudioPreviewUvMeshVisible(visible) {
    state.previewUvMeshVisible = Boolean(visible);
    syncPreviewMenuState();
    await reloadLayerStudioPreviewTexture({ forceModelReload: true });
  }

  async function selectLayerStudioPreviewTextureSlot(textureKey) {
    const nextKey = surfaceKey(textureKey);
    const surface = textureSurfaces().find((row) => surfaceKey(row.key) === nextKey) || null;
    if (!surface || state.textureSwitching) return false;
    const select = $("layerStudioTextureSelect");
    if (select) select.value = nextKey;
    state.pendingSurfaceKey = nextKey;
    syncTextureSurfaceControls();
    if (surfaceKey(state.activeSurfaceKey) !== nextKey) {
      const loaded = await loadSelectedTextureSurface();
      if (!loaded) return false;
    }
    await reloadLayerStudioPreviewTexture();
    return true;
  }

  function applyLayerStudioPaintFinish() {
    return setLayerStudioPaintFinish(
      state.previewMaterialFinish || state.project?.materialFinish || state.context?.materialFinish || null,
      { silent: true },
    );
  }

  function resolveLayerSourcePath(layer) {
    const source = cleanText(layer?.source);
    if (!source) return "";
    const sourceType = cleanText(layer?.sourceType, "project").toLowerCase().replace("_", "");
    if (sourceType === "library") {
      return cleanText((libraryAssetById(source, "global") || libraryAssetById(source, "project"))?.path);
    }
    if (sourceType === "projectlibrary") {
      return cleanText(libraryAssetById(source, "project")?.path);
    }
    if (/^[a-zA-Z]:[\\/]/.test(source) || source.startsWith("\\\\")) return source;
    const root = cleanText(state.context?.layerRoot || state.context?.projectFolder);
    if (!root) return source;
    return `${root.replace(/[\\/]+$/, "")}\\${source.replace(/^[\\/]+/, "").replace(/\//g, "\\")}`;
  }

  async function loadLayerImage(layer) {
    const path = resolveLayerSourcePath(layer);
    if (!path || !window.pywebview?.api?.load_image_preview) return null;
    const cacheKey = `${layer.id}|${path}`;
    if (!state.imageCache.has(cacheKey)) {
      state.imageCache.set(cacheKey, (async () => {
        const result = await window.pywebview.api.load_image_preview(path, `layer-studio-${layer.id}`);
        if (!result?.ok || !(result.data?.data_url || result.data?.dataUrl)) {
          throw new Error(result?.message || `Layer image could not be loaded: ${path}`);
        }
        return await loadCanvasImage(result.data.data_url || result.data.dataUrl);
      })());
    }
    return await state.imageCache.get(cacheKey);
  }

  async function missingPngPlaceholderImage(layer) {
    const cacheKey = "missing-png-placeholder";
    if (!state.imageCache.has(cacheKey)) {
      state.imageCache.set(cacheKey, (async () => {
        const canvas = document.createElement("canvas");
        canvas.width = 512;
        canvas.height = 256;
        const ctx = canvas.getContext("2d");
        if (!ctx) return null;
        ctx.fillStyle = "rgba(0, 0, 0, 0.66)";
        ctx.fillRect(0, 0, canvas.width, canvas.height);
        ctx.strokeStyle = "#ff5a4f";
        ctx.lineWidth = 10;
        ctx.setLineDash([22, 14]);
        ctx.strokeRect(10, 10, canvas.width - 20, canvas.height - 20);
        ctx.setLineDash([]);
        ctx.fillStyle = "#ff5a4f";
        ctx.font = "700 42px sans-serif";
        ctx.textAlign = "center";
        ctx.textBaseline = "middle";
        ctx.fillText("PNG not found", canvas.width / 2, canvas.height / 2 - 16);
        ctx.fillStyle = "rgba(255, 255, 255, 0.76)";
        ctx.font = "500 22px sans-serif";
        ctx.fillText("Library asset is missing", canvas.width / 2, canvas.height / 2 + 34);
        return await loadCanvasImage(canvas.toDataURL("image/png"));
      })());
    }
    const image = await state.imageCache.get(cacheKey);
    if (isLayerPngMissing(layer)) {
      setText("layerStudioEditorStatus", `PNG not found:\n${layer.name || layer.id}\nLibrary asset: ${layer.source || "-"}`);
    }
    return image;
  }

  async function loadBakeBaseImage() {
    const cleanBasePath = baseTexturePath();
    if (cleanBasePath && window.pywebview?.api?.load_image_preview) {
      const cacheKey = `bake-base|${cleanBasePath}`;
      if (!state.imageCache.has(cacheKey)) {
        state.imageCache.set(cacheKey, (async () => {
          const result = await window.pywebview.api.load_image_preview(cleanBasePath, "layer-studio-bake-base");
          if (!result?.ok || !(result.data?.data_url || result.data?.dataUrl)) {
            throw new Error(result?.message || `Clean base texture could not be loaded: ${cleanBasePath}`);
          }
          return await loadCanvasImage(result.data.data_url || result.data.dataUrl);
        })());
      }
      try {
        return await state.imageCache.get(cacheKey);
      } catch (error) {
        state.imageCache.delete(cacheKey);
        app().log?.(`Layer Studio clean base fallback: ${error?.message || String(error)}`);
      }
    }
    if (state.baseImageElement) return state.baseImageElement;
    const activePath = texturePath();
    if (activePath && window.pywebview?.api?.load_image_preview) {
      const cacheKey = `bake-active-base|${activePath}`;
      if (!state.imageCache.has(cacheKey)) {
        state.imageCache.set(cacheKey, (async () => {
          const result = await window.pywebview.api.load_image_preview(activePath, "layer-studio-bake-active-base");
          if (!result?.ok || !(result.data?.data_url || result.data?.dataUrl)) {
            throw new Error(result?.message || `Active EXT_SKIN_C texture could not be loaded: ${activePath}`);
          }
          return await loadCanvasImage(result.data.data_url || result.data.dataUrl);
        })());
      }
      try {
        return await state.imageCache.get(cacheKey);
      } catch (error) {
        state.imageCache.delete(cacheKey);
        app().log?.(`Layer Studio active base fallback failed: ${error?.message || String(error)}`, true);
      }
    }
    const loaded = await loadBaseTexture();
    if (!loaded || !state.baseImageElement) {
      throw new Error("Base EXT_SKIN_C texture is not loaded.");
    }
    return state.baseImageElement;
  }

  async function drawPngMaskGroupToBakeCanvas(ctx, layer) {
    const mask = layer?.mask || {};
    const child = layer?.child || {};
    const points = Array.isArray(mask.points) ? mask.points : [];
    if (points.length < 6 || !child.source || child.visible === false) return false;
    let image = null;
    try {
      image = isLayerPngMissing(child) ? null : await loadLayerImage(child);
    } catch (error) {
      state.imageCache.delete(`${child.id}|${resolveLayerSourcePath(child)}`);
      app().log?.(`Layer Studio bake skipped masked PNG: ${child.name || child.id}: ${error?.message || String(error)}`);
    }
    if (!image) return false;
    const imageWidth = intValue(child.width, 1, 1);
    const imageHeight = intValue(child.height, 1, 1);
    ctx.beginPath();
    drawPolygonPath(ctx, points, mask.bezierHandles);
    ctx.closePath();
    ctx.clip();
    ctx.globalAlpha = numberValue(child.opacity, 1, 0, 1);
    ctx.translate(numberValue(child.x, 0), numberValue(child.y, 0));
    ctx.rotate((numberValue(child.rotation, 0) * Math.PI) / 180);
    if (child.flipX || child.flipY) {
      ctx.translate(child.flipX ? imageWidth : 0, child.flipY ? imageHeight : 0);
      ctx.scale(child.flipX ? -1 : 1, child.flipY ? -1 : 1);
    }
    ctx.drawImage(image, 0, 0, imageWidth, imageHeight);
    return true;
  }

  function bakeRenderableLayers(documentModel) {
    return flatLayerListForDocument(documentModel, { includeHiddenChildren: false })
      .filter((layer) => layer.visible !== false && layer.effectiveVisible !== false && (isPngMaskGroupLayer(layer) || isPointLayerType(layer.type) || isShapeLayerType(layer.type) || isTextLayerType(layer.type) || (layer.type === "image" && layer.source)));
  }

  async function drawLayerDocumentToBakeCanvas(ctx, documentModel, options = {}) {
    const visibleLayers = bakeRenderableLayers(documentModel);
    if (visibleLayers.some((layer) => isTextLayerType(layer.type))) {
      await ensureAppFontsLoaded();
    }
    for (const layer of visibleLayers) {
      ctx.save();
      ctx.globalAlpha = numberValue(layer.opacity, 1, 0, 1);
      if (isPngMaskGroupLayer(layer)) {
        await drawPngMaskGroupToBakeCanvas(ctx, layer);
      } else if (layer.type === "polygon") {
        const points = Array.isArray(layer.points) ? layer.points : [];
        if (points.length >= 6) {
          const bounds = polygonBounds(points);
          ctx.beginPath();
          drawPolygonPath(ctx, points, layer.bezierHandles);
          setCanvasFillStyle(ctx, shapeLayerFillStyle(ctx, layer, Math.max(1, bounds.width || 1), Math.max(1, bounds.height || 1), bounds.x || 0, bounds.y || 0));
          ctx.fill();
        }
      } else if (layer.type === "stripe") {
        const points = Array.isArray(layer.points) ? layer.points : [];
        if (points.length >= 4) {
          ctx.beginPath();
          drawStripePath(ctx, points, numberValue(layer.tension, 0, 0, 1), layer.bezierHandles);
          strokeStripeCanvasPath(ctx, layer, stripeLayerStrokeStyle(ctx, layer));
        }
      } else if (layer.type === "rectangle") {
        const shapeWidth = intValue(layer.width, 1, 1);
        const shapeHeight = intValue(layer.height, 1, 1);
        ctx.translate(numberValue(layer.x, 0), numberValue(layer.y, 0));
        ctx.rotate((numberValue(layer.rotation, 0) * Math.PI) / 180);
        setCanvasFillStyle(ctx, shapeLayerFillStyle(ctx, layer, shapeWidth, shapeHeight));
        ctx.fillRect(0, 0, shapeWidth, shapeHeight);
      } else if (layer.type === "ellipse") {
        const shapeWidth = intValue(layer.width, 1, 1);
        const shapeHeight = intValue(layer.height, 1, 1);
        ctx.translate(numberValue(layer.x, 0), numberValue(layer.y, 0));
        ctx.rotate((numberValue(layer.rotation, 0) * Math.PI) / 180);
        ctx.beginPath();
        ctx.ellipse(shapeWidth / 2, shapeHeight / 2, shapeWidth / 2, shapeHeight / 2, 0, 0, Math.PI * 2);
        if (!fillEllipseRadialGradient(ctx, layer, shapeWidth, shapeHeight)) {
          setCanvasFillStyle(ctx, shapeLayerFillStyle(ctx, layer, shapeWidth, shapeHeight));
          ctx.fill();
        }
      } else if (layer.type === "text") {
        const rendered = renderTextLayerCanvas(layer);
        ctx.translate(numberValue(layer.x, 0), numberValue(layer.y, 0));
        ctx.rotate((numberValue(layer.rotation, 0) * Math.PI) / 180);
        ctx.drawImage(rendered.canvas, 0, 0, intValue(layer.width, rendered.canvas.width, 1), intValue(layer.height, rendered.canvas.height, 1));
      } else {
        let image = null;
        try {
          image = isLayerPngMissing(layer) ? null : await loadLayerImage(layer);
        } catch (error) {
          state.imageCache.delete(`${layer.id}|${resolveLayerSourcePath(layer)}`);
          app().log?.(`Layer Studio bake skipped missing PNG: ${layer.name || layer.id}: ${error?.message || String(error)}`);
        }
        if (image) {
          const imageWidth = intValue(layer.width, 1, 1);
          const imageHeight = intValue(layer.height, 1, 1);
          ctx.translate(numberValue(layer.x, 0), numberValue(layer.y, 0));
          ctx.rotate((numberValue(layer.rotation, 0) * Math.PI) / 180);
          if (layer.flipX || layer.flipY) {
            ctx.translate(layer.flipX ? imageWidth : 0, layer.flipY ? imageHeight : 0);
            ctx.scale(layer.flipX ? -1 : 1, layer.flipY ? -1 : 1);
          }
          ctx.drawImage(image, 0, 0, imageWidth, imageHeight);
        }
      }
      ctx.restore();
    }
    return visibleLayers.length;
  }

  async function bakeLayerStackDataUrl(options = {}) {
    const includeBaseImage = options.includeBaseImage !== false;
    const includeBaseColor = options.includeBaseColor !== false;
    const baseImage = includeBaseImage ? await loadBakeBaseImage() : null;
    const documentModel = serializeLayerDocument();
    const width = intValue(documentModel.textureSize?.width, 4096, 1);
    const height = intValue(documentModel.textureSize?.height, 4096, 1);
    const canvas = document.createElement("canvas");
    canvas.width = width;
    canvas.height = height;
    const ctx = canvas.getContext("2d");
    if (!ctx) throw new Error("Bake canvas could not be created.");
    ctx.clearRect(0, 0, width, height);
    if (baseImage) ctx.drawImage(baseImage, 0, 0, width, height);
    const baseColor = baseColorState(documentModel);
    if (includeBaseColor && baseColor.visible) {
      ctx.fillStyle = baseColor.fill;
      ctx.fillRect(0, 0, width, height);
    }
    const baseLayerCount = await drawLayerDocumentToBakeCanvas(ctx, documentModel);
    let specLayerCount = 0;
    let specTotalLayerCount = 0;
    const quantized = isSplatMapSurface(documentModel.texture) ? quantizeSplatMapCanvas(ctx, width, height) : { changedPixels: 0 };
    return {
      dataUrl: canvas.toDataURL("image/png"),
      width,
      height,
      bakedLayerCount: baseLayerCount + specLayerCount,
      baseBakedLayerCount: baseLayerCount,
      specBakedLayerCount: specLayerCount,
      totalLayerCount: documentModel.layers.length + specTotalLayerCount,
      specTotalLayerCount,
      combinedSurfaceCount: 1,
      quantizedPixels: quantized.changedPixels || 0,
    };
  }

  function quantizeSplatMapCanvas(ctx, width, height) {
    if (!ctx || !width || !height) return { changedPixels: 0 };
    const palette = SPLAT_MAP_PALETTE.map((item) => {
      const hex = item.color.replace("#", "");
      return {
        r: parseInt(hex.slice(0, 2), 16),
        g: parseInt(hex.slice(2, 4), 16),
        b: parseInt(hex.slice(4, 6), 16),
      };
    });
    const imageData = ctx.getImageData(0, 0, width, height);
    const data = imageData.data;
    let changedPixels = 0;
    for (let index = 0; index < data.length; index += 4) {
      const alpha = data[index + 3];
      if (alpha <= 0) {
        if (data[index] || data[index + 1] || data[index + 2] || data[index + 3] !== 255) changedPixels += 1;
        data[index] = 0;
        data[index + 1] = 0;
        data[index + 2] = 0;
        data[index + 3] = 255;
        continue;
      }
      let best = palette[0];
      let bestDistance = Number.POSITIVE_INFINITY;
      for (const color of palette) {
        const dr = data[index] - color.r;
        const dg = data[index + 1] - color.g;
        const db = data[index + 2] - color.b;
        const distance = dr * dr + dg * dg + db * db;
        if (distance < bestDistance) {
          best = color;
          bestDistance = distance;
        }
      }
      if (data[index] !== best.r || data[index + 1] !== best.g || data[index + 2] !== best.b || data[index + 3] !== 255) {
        changedPixels += 1;
      }
      data[index] = best.r;
      data[index + 1] = best.g;
      data[index + 2] = best.b;
      data[index + 3] = 255;
    }
    if (changedPixels) ctx.putImageData(imageData, 0, 0);
    return { changedPixels };
  }

  function drawStripePath(ctx, rawPoints, tension = 0, bezierHandles = []) {
    const points = rawPoints.map((value) => numberValue(value, 0));
    if (points.length < 4) return;
    ctx.moveTo(points[0], points[1]);
    const handles = Array.isArray(bezierHandles) ? bezierHandles : [];
    if (handles.some((handle) => handle && (handle.out || handle.in))) {
      const segmentCount = Math.floor(points.length / 2) - 1;
      for (let segmentIndex = 0; segmentIndex < segmentCount; segmentIndex += 1) {
        const fallback = stripeSegmentDefaultHandles(points, segmentIndex);
        const handle = handles[segmentIndex] || {};
        const out = handle.out || fallback.out;
        const input = handle.in || fallback.in;
        const end = stripePointAt(points, segmentIndex + 1);
        ctx.bezierCurveTo(
          numberValue(out.x, fallback.out.x),
          numberValue(out.y, fallback.out.y),
          numberValue(input.x, fallback.in.x),
          numberValue(input.y, fallback.in.y),
          end.x,
          end.y,
        );
      }
      return;
    }
    const amount = numberValue(tension, 0, 0, 1);
    if (amount <= 0 || points.length <= 4) {
      for (let index = 2; index < points.length; index += 2) {
        ctx.lineTo(points[index], points[index + 1]);
      }
      return;
    }
    const tensionPoints = expandStripeTensionPoints(points, amount);
    if (tensionPoints.length < 4) {
      for (let index = 2; index < points.length; index += 2) {
        ctx.lineTo(points[index], points[index + 1]);
      }
      return;
    }
    ctx.quadraticCurveTo(tensionPoints[0], tensionPoints[1], tensionPoints[2], tensionPoints[3]);
    let index = 4;
    while (index < tensionPoints.length - 2) {
      ctx.bezierCurveTo(
        tensionPoints[index++],
        tensionPoints[index++],
        tensionPoints[index++],
        tensionPoints[index++],
        tensionPoints[index++],
        tensionPoints[index++],
      );
    }
    ctx.quadraticCurveTo(
      tensionPoints[tensionPoints.length - 2],
      tensionPoints[tensionPoints.length - 1],
      points[points.length - 2],
      points[points.length - 1],
    );
  }

  function stripeControlPoints(x0, y0, x1, y1, x2, y2, tension) {
    const d01 = Math.hypot(x1 - x0, y1 - y0);
    const d12 = Math.hypot(x2 - x1, y2 - y1);
    const sum = d01 + d12;
    if (!sum) return null;
    const fa = (tension * d01) / sum;
    const fb = (tension * d12) / sum;
    return [
      x1 - fa * (x2 - x0),
      y1 - fa * (y2 - y0),
      x1 + fb * (x2 - x0),
      y1 + fb * (y2 - y0),
    ];
  }

  function expandStripeTensionPoints(points, tension) {
    const expanded = [];
    for (let index = 2; index < points.length - 2; index += 2) {
      const cp = stripeControlPoints(
        points[index - 2],
        points[index - 1],
        points[index],
        points[index + 1],
        points[index + 2],
        points[index + 3],
        tension,
      );
      if (!cp || cp.some((value) => Number.isNaN(value))) continue;
      expanded.push(cp[0], cp[1], points[index], points[index + 1], cp[2], cp[3]);
    }
    return expanded;
  }

  function drawPolygonPath(ctx, rawPoints, bezierHandles = []) {
    const points = Array.isArray(rawPoints) ? rawPoints.map((value) => numberValue(value, 0)) : [];
    if (points.length < 6) return;
    drawOpenBezierPath(ctx, points, bezierHandles);
    const count = Math.floor(points.length / 2);
    const handles = Array.isArray(bezierHandles) ? bezierHandles : [];
    const segmentIndex = count - 1;
    const handle = handles[segmentIndex];
    const end = pointAt(points, 0, true);
    if (handle && (handle.out || handle.in)) {
      const fallback = polygonSegmentDefaultHandles(points, segmentIndex);
      const out = handle.out || fallback.out;
      const input = handle.in || fallback.in;
      ctx.bezierCurveTo(
        numberValue(out.x, fallback.out.x),
        numberValue(out.y, fallback.out.y),
        numberValue(input.x, fallback.in.x),
        numberValue(input.y, fallback.in.y),
        end.x,
        end.y,
      );
    } else {
      ctx.lineTo(end.x, end.y);
    }
    ctx.closePath();
  }

  function drawOpenBezierPath(ctx, rawPoints, bezierHandles = []) {
    const points = Array.isArray(rawPoints) ? rawPoints.map((value) => numberValue(value, 0)) : [];
    if (points.length < 2) return;
    const count = Math.floor(points.length / 2);
    ctx.moveTo(points[0], points[1]);
    const handles = Array.isArray(bezierHandles) ? bezierHandles : [];
    for (let segmentIndex = 0; segmentIndex < count - 1; segmentIndex += 1) {
      const handle = handles[segmentIndex];
      const end = pointAt(points, segmentIndex + 1, false);
      if (handle && (handle.out || handle.in)) {
        const fallback = pointSegmentDefaultHandles(points, segmentIndex, false);
        const out = handle.out || fallback.out;
        const input = handle.in || fallback.in;
        ctx.bezierCurveTo(
          numberValue(out.x, fallback.out.x),
          numberValue(out.y, fallback.out.y),
          numberValue(input.x, fallback.in.x),
          numberValue(input.y, fallback.in.y),
          end.x,
          end.y,
        );
      } else {
        ctx.lineTo(end.x, end.y);
      }
    }
  }

  function syncTextureSurfaceControls() {
    const select = $("layerStudioTextureSelect");
    const button = $("layerStudioLoadTexture");
    const surfaces = textureSurfaces();
    const activeKey = surfaceKey(state.activeSurfaceKey);
    const pendingKey = surfaceKey(state.pendingSurfaceKey || activeKey);
    const busy = state.textureSwitching || state.baking;
    if (select) {
      const currentValue = select.value || pendingKey;
      select.replaceChildren();
      surfaces.forEach((surface) => {
        const option = document.createElement("option");
        option.value = surfaceKey(surface.key);
        option.textContent = surface.label || surface.fileName || option.value;
        select.appendChild(option);
      });
      select.disabled = busy || surfaces.length <= 1;
      select.value = surfaces.some((surface) => surfaceKey(surface.key) === pendingKey)
        ? pendingKey
        : (surfaces.some((surface) => surfaceKey(surface.key) === currentValue) ? currentValue : activeKey);
    }
    if (button) {
      button.disabled = busy || !surfaces.length;
      const pendingSurface = surfaces.find((surface) => surfaceKey(surface.key) === pendingKey) || null;
      const canCreateSplatMap = pendingSurface && surfaceKey(pendingSurface.key) === "ext_skin_d" && pendingSurface.activeTexture?.exists === false;
      button.textContent = canCreateSplatMap ? "Create Splat Map" : "Load Texture";
    }
    const bakeButton = $("layerStudioBakePreview");
    if (bakeButton) {
      const activeSurface = surfaces.find((surface) => surfaceKey(surface.key) === activeKey) || null;
      bakeButton.disabled = busy || activeSurface?.bakeEnabled === false;
      bakeButton.title = activeSurface?.bakeEnabled === false
        ? "Texture update is disabled for this surface."
        : "";
    }
    syncLayerStudioPreviewTextureButtons();
    setText("layerStudioTextureLabel", activeTextureSurfaceLabel());
    syncLayerStudioToolAvailability();
    syncColorFavoritesAvailability();
  }

  function syncLayerStudioToolAvailability() {
    const splatMode = isSplatMapSurface();
    const importPng = $("layerStudioImportPng");
    const text = $("layerStudioCreateText");
    const fillMode = $("layerStudioPropFillMode");
    if (importPng) {
      importPng.disabled = splatMode;
      importPng.title = splatMode ? "PNG layers are disabled for EXT_Skin_D mask editing." : "Place PNG";
    }
    if (text) {
      text.disabled = splatMode;
      text.title = splatMode ? "Text layers are disabled for EXT_Skin_D mask editing." : "Text";
    }
    if (fillMode && splatMode) fillMode.value = "solid";
  }

  function applyTextureSurface(surface, options = {}) {
    if (!surface) return false;
    const key = surfaceKey(surface.key);
    state.activeSurfaceKey = key;
    state.pendingSurfaceKey = key;
    state.context = {
      ...(state.context || {}),
      activeSurfaceKey: key,
      surfaceRoot: surface.surfaceRoot || "",
      layersPath: surface.layersPath || "",
      textureSize: surface.textureSize || state.textureSize,
      baseTexture: surface.baseTexture || {},
      activeTexture: surface.activeTexture || {},
      layerDocument: surface.layerDocument || null,
    };
    state.textureSize = {
      width: Number(surface.textureSize?.width || 4096),
      height: Number(surface.textureSize?.height || 4096),
    };
    state.layerDocument = normalizeLayerDocument(surface.layerDocument, state.textureSize);
    const autoHiddenBaseColor = applyBaseColorAutoHideRules();
    state.selectedLayerId = "";
    state.selectedLayerIds = [];
    state.selectedGuideId = "";
    state.undoStack = [];
    state.redoStack = [];
    state.historySnapshot = null;
    updateHistoryButtons();
    syncGuideControls();
    syncTextureSurfaceControls();
    if (options.clean !== false && !autoHiddenBaseColor) setDirty(false);
    return true;
  }

  async function loadSelectedTextureSurface() {
    if (state.textureSwitching) return false;
    const select = $("layerStudioTextureSelect");
    const nextKey = surfaceKey(select?.value || state.pendingSurfaceKey || state.activeSurfaceKey);
    const previousKey = surfaceKey(state.activeSurfaceKey);
    const previousLabel = activeTextureSurfaceLabel();
    state.pendingSurfaceKey = nextKey;
    state.textureSwitching = true;
    syncTextureSurfaceControls();
    try {
      if (nextKey === surfaceKey(state.activeSurfaceKey)) {
        return await loadBaseTexture();
      }
      const surface = textureSurfaces().find((row) => surfaceKey(row.key) === nextKey);
      if (!surface) return false;
      let switchResult = null;
      if (state.dirty) {
        switchResult = await resolveDirtyTextureSwitch(previousLabel);
        if (!switchResult?.ok) {
          state.pendingSurfaceKey = state.activeSurfaceKey;
          return false;
        }
      }
      if (nextKey === "ext_skin_d" && surface.activeTexture?.exists === false) {
        const created = await createSplatMapSurface(surface);
        if (!created?.ok) return false;
        if (switchResult?.action === "baked" || switchResult?.action === "saved") {
          const nextLabel = surface.label || surface.fileName || nextKey;
          setText(
            "layerStudioEditorStatus",
            [
              `${switchResult.label || previousLabel} ${switchResult.action === "baked" ? "saved and baked" : "layers saved"}.`,
              `Created ${nextLabel}.`,
              `Texture: ${texturePath() || "-"}`,
            ].join("\n"),
          );
        }
        return true;
      }
      applyTextureSurface(surface);
      renderLayerList();
      renderPropertiesPanel();
      renderGuides();
      destroyCanvas();
      const loaded = await loadBaseTexture();
      const nextLabel = surface.label || surface.fileName || nextKey;
      const firstLine = switchResult?.action === "baked" && loaded
        ? `${switchResult.label || previousLabel} saved and baked. Loaded ${nextLabel}.`
        : switchResult?.action === "saved" && loaded
        ? `${switchResult.label || previousLabel} layers saved. Loaded ${nextLabel}.`
        : loaded
        ? "Layer Studio texture loaded."
        : "Layer Studio texture could not be loaded.";
      setText(
        "layerStudioEditorStatus",
        [
          firstLine,
          `Texture: ${nextLabel}`,
          `Size: ${state.textureSize.width} x ${state.textureSize.height}`,
          `Layer data: ${surface.layersPath || "-"}`,
        ].join("\n"),
      );
      if (loaded && window.LiveryLabPreview3d?.hasLoadedModel?.()) {
        await reloadLayerStudioPreviewTexture(layerStudioPreviewMode() === "finalLivery" ? {} : { texturePath: texturePath() });
      }
      return loaded;
    } finally {
      state.textureSwitching = false;
      syncTextureSurfaceControls();
    }
  }

  function createSolidPngDataUrl(width, height, color = SPLAT_MAP_BASE_COLOR_DEFAULT) {
    const canvas = document.createElement("canvas");
    canvas.width = Math.max(1, intValue(width, 4096, 1));
    canvas.height = Math.max(1, intValue(height, 4096, 1));
    const ctx = canvas.getContext("2d");
    if (!ctx) throw new Error("Splat map canvas could not be created.");
    ctx.fillStyle = color;
    ctx.fillRect(0, 0, canvas.width, canvas.height);
    return canvas.toDataURL("image/png");
  }

  async function createSplatMapSurface(surface) {
    if (!state.context?.skinKey || !window.pywebview?.api?.bake_layer_studio_preview) return { ok: false };
    const size = surface?.textureSize || state.textureSize || { width: 4096, height: 4096 };
    const label = surface?.label || "EXT_Skin_D";
    const confirmed = await window.showConfirmModal?.({
      type: "info",
      title: "Layer Studio",
      subtitle: "Create Splat Map",
      message: "Create EXT_Skin_D.png as a black material mask for this project.",
      details: [
        `Texture: ${label}`,
        `Size: ${intValue(size.width, 4096, 1)} x ${intValue(size.height, 4096, 1)}`,
        "Black mask areas use the authored livery texture values.",
      ].join("\n"),
      confirmLabel: "Create Splat Map",
      cancelLabel: "Cancel",
    });
    if (!confirmed) {
      state.pendingSurfaceKey = state.activeSurfaceKey;
      syncTextureSurfaceControls();
      return { ok: false };
    }
    const button = $("layerStudioLoadTexture");
    app().setButtonWorking?.(button, true, "Creating...");
    try {
      const result = await window.pywebview.api.bake_layer_studio_preview({
        skinKey: state.context.skinKey,
        surfaceKey: "ext_skin_d",
        root: app().root?.() || "",
        dataUrl: createSolidPngDataUrl(size.width, size.height, SPLAT_MAP_BASE_COLOR_DEFAULT),
      });
      app().showResult?.(result, "Splat map created", "bake_layer_studio_preview");
      if (!result?.ok) {
        setText("layerStudioEditorStatus", result?.message || "Splat map creation failed.");
        return { ok: false };
      }
      const context = result.data?.layerStudio || state.context || {};
      renderContext(context);
      const nextSurface = textureSurfaceByKey("ext_skin_d", context);
      if (nextSurface) applyTextureSurface(nextSurface);
      destroyCanvas();
      const loaded = await loadBaseTexture();
      if (loaded && window.LiveryLabPreview3d?.hasLoadedModel?.()) {
        await reloadLayerStudioPreviewTexture({ texturePath: texturePath() });
      }
      setText("layerStudioEditorStatus", `Splat map created.\nTexture: ${texturePath() || result.data?.path || "-"}`);
      return { ok: true };
    } finally {
      app().setButtonWorking?.(button, false);
      syncTextureSurfaceControls();
    }
  }

  function renderContext(context) {
    state.context = context || {};
    state.project = context?.previewProject || {};
    state.previewMaterialFinish = normalizeLayerStudioMaterialFinish(state.project?.materialFinish || context?.materialFinish || null);
    state.bodyBaseColor = normalizeLayerStudioBodyBaseColor(state.project?.bodyBaseColor || BODY_BASE_COLOR_DEFAULT);
    sharedMaterialFinish()?.sync?.(state.previewMaterialFinish, { source: "layerStudioOpen" });
    state.assetLibrary = context?.assetLibrary || state.assetLibrary || { assets: [] };
    state.projectAssetLibrary = context?.projectAssetLibrary || state.projectAssetLibrary || { assets: [] };
    state.libraryActiveScope = "project";
    const surface = activeTextureSurface(context);
    state.activeSurfaceKey = surfaceKey(context?.activeSurfaceKey || surface?.key || state.activeSurfaceKey);
    state.pendingSurfaceKey = state.activeSurfaceKey;
    state.textureSize = {
      width: Number(surface?.textureSize?.width || context?.textureSize?.width || 4096),
      height: Number(surface?.textureSize?.height || context?.textureSize?.height || 4096),
    };
    state.layerDocument = normalizeLayerDocument(surface?.layerDocument || context?.layerDocument, state.textureSize);
    const autoHiddenBaseColor = applyBaseColorAutoHideRules();
    if (!findLayer(state.selectedLayerId)) state.selectedLayerId = "";
    state.undoStack = [];
    state.redoStack = [];
    state.historySnapshot = null;
    state.selectedGuideId = findGuide(state.selectedGuideId) ? state.selectedGuideId : "";
    updateHistoryButtons();
    syncGuideControls();
    if (!autoHiddenBaseColor) setDirty(false);
    const title = context?.displayName || context?.skinId || "Layer Studio";
    const meta = context?.carModel || "";
    setText("layerStudioTitle", title);
    setText("layerStudioMeta", meta);
    syncTextureSurfaceControls();
    syncPreviewMenuState();
    syncLayerStudioPaintFinishPanel();
    syncLayerStudioBodyBaseColorControls();
    const size = state.textureSize || {};
    setText("layerStudioTextureSize", `${size.width || 4096} x ${size.height || 4096}`);
    const warnings = Array.isArray(context?.warnings) ? context.warnings : [];
    setText(
      "layerStudioEditorStatus",
      [
        `Project: ${context?.projectFolder || "-"}`,
        `Editor base: ${editorTexturePath(context) || "-"}`,
        `Baked preview: ${texturePath(context) || "-"}`,
        `Layer data: ${context?.layersPath || "-"}`,
        `Editable layers: ${state.layerDocument.layers.length}`,
        ...(warnings.length ? ["", "Warnings:", ...warnings.map((row) => `- ${row}`)] : []),
      ].join("\n"),
    );
    renderLayerList();
    renderAssetLibrary();
    renderPropertiesPanel();
  }

  async function loadBaseTexture() {
    const requestId = ++state.textureRequestId;
    const path = editorTexturePath();
    const empty = $("layerStudioTextureEmpty");
    if (!path || !window.pywebview?.api?.load_image_preview) {
      destroyCanvas();
      if (empty) empty.style.display = "";
      setText("layerStudioEditorStatus", "No active EXT_SKIN_C texture is available for this livery yet.");
      return false;
    }
    if (!window.Konva) {
      destroyCanvas();
      if (empty) empty.style.display = "";
      setText("layerStudioEditorStatus", "Konva is not loaded. Rebuild the web assets and reopen the app.");
      return false;
    }
    setText("layerStudioEditorStatus", `Loading Layer Studio editor base...\n${path}`);
    const result = await window.pywebview.api.load_image_preview(path, "layer-studio-base");
    if (requestId !== state.textureRequestId) return false;
    if (!result?.ok || !(result.data?.data_url || result.data?.dataUrl)) {
      destroyCanvas();
      if (empty) empty.style.display = "";
      setText("layerStudioEditorStatus", result?.message || "EXT_SKIN_C could not be loaded.");
      return false;
    }
    const width = result.data.width || state.context?.textureSize?.width || 4096;
    const height = result.data.height || state.context?.textureSize?.height || 4096;
    state.textureSize = { width: Number(width), height: Number(height) };
    state.context = {
      ...(state.context || {}),
      textureSize: { ...state.textureSize },
    };
    const surface = activeTextureSurface();
    if (surface) {
      surface.textureSize = { ...state.textureSize };
      surface.layerDocument = normalizeLayerDocument(surface.layerDocument || state.layerDocument, state.textureSize);
    }
    state.layerDocument = normalizeLayerDocument(state.layerDocument, state.textureSize);
    setText("layerStudioTextureSize", `${width} x ${height}`);
    const imageElement = await loadCanvasImage(result.data.data_url || result.data.dataUrl);
    if (requestId !== state.textureRequestId) return false;
    state.baseImageElement = imageElement;
    initCanvas(imageElement);
    if (empty) empty.style.display = "none";
    setText(
      "layerStudioEditorStatus",
      [
        "Konva canvas ready.",
        `Texture-space source: ${path}`,
        `Layer JSON target: ${state.context?.layersPath || "-"}`,
        `View: ${Math.round(state.view.scale * 100)}% zoom`,
      ].join("\n"),
    );
    return true;
  }

  async function resolveDirtyTextureSwitch(label = activeTextureSurfaceLabel()) {
    const choice = await window.showChoiceModal?.({
      type: "warning",
      title: "Layer Studio",
      subtitle: "Unsaved layer changes",
      message: "Save the current texture layer setup before switching. Choose Update Texture too if the 3D preview should receive this texture change immediately.",
      confirmLabel: "Save + Update Texture",
      alternateLabel: "Save Layers Only",
      cancelLabel: "Cancel",
    });
    if (choice === true || choice === "confirm") {
      const baked = await bakePreviewTexture({ silent: true });
      return baked ? { ok: true, action: "baked", label } : false;
    }
    if (choice === "alternate") {
      const saved = await saveLayerStudioProject({ silent: true });
      return saved ? { ok: true, action: "saved", label } : false;
    }
    if (choice === undefined && window.showConfirmModal) {
      const confirmed = await window.showConfirmModal({
        type: "warning",
        title: "Layer Studio",
        subtitle: "Unsaved layer changes",
        message: "Save and update the current texture before switching?",
        confirmLabel: "Save + Update Texture",
        cancelLabel: "Cancel",
      });
      if (!confirmed) return false;
      const baked = await bakePreviewTexture({ silent: true });
      return baked ? { ok: true, action: "baked", label } : false;
    }
    return false;
  }

  function loadCanvasImage(src) {
    return new Promise((resolve, reject) => {
      const image = new Image();
      image.onload = () => resolve(image);
      image.onerror = () => reject(new Error("Base texture image could not be decoded."));
      image.src = src;
    });
  }

  function stageHostSize() {
    const host = $("layerStudioCanvasHost");
    const rect = host?.getBoundingClientRect?.();
    return {
      width: Math.max(320, Math.floor(rect?.width || 0)),
      height: Math.max(260, Math.floor(rect?.height || 0)),
    };
  }

  function initCanvas(imageElement) {
    destroyCanvas();
    const host = $("layerStudioCanvasHost");
    if (!host || !window.Konva) return;
    const size = stageHostSize();
    const stage = new window.Konva.Stage({
      container: host,
      width: size.width,
      height: size.height,
      draggable: true,
    });
    const backgroundLayer = new window.Konva.Layer({ listening: false });
    const objectLayer = new window.Konva.Layer();
    const uiLayer = new window.Konva.Layer();
    const transformer = new window.Konva.Transformer({
      rotateEnabled: true,
      rotateAnchorCursor: "grab",
      keepRatio: true,
      shiftBehavior: "default",
      ignoreStroke: true,
      enabledAnchors: ["top-left", "top-right", "bottom-left", "bottom-right", "middle-left", "middle-right", "top-center", "bottom-center"],
      boundBoxFunc: (_oldBox, newBox) => {
        if (Math.abs(newBox.width) < 4 || Math.abs(newBox.height) < 4) return _oldBox;
        return newBox;
      },
    });
    transformer.on("mouseover mousemove", (event) => {
      updateTransformerCursor(event);
    });
    transformer.on("mouseout", () => {
      updateCanvasCursor();
    });
    transformer.on("transformstart", (event) => {
      updateTransformerCursor(event);
      const node = transformer.nodes()[0];
      if (node?.name?.() === "groupTransformBox") {
        setTransformerRotationSnap(Boolean(event.evt?.shiftKey));
        beginGroupTransform("Transform group");
        return;
      }
      if (node?.name?.() === "pointLayerTransformBox") {
        setTransformerRotationSnap(Boolean(event.evt?.shiftKey));
        beginPointTransform("Transform point layer");
        return;
      }
      setTransformerRotationSnap(Boolean(event.evt?.shiftKey));
      beginHistory("Transform layer");
    });
    transformer.on("transform", (event) => {
      setTransformerRotationSnap(Boolean(event.evt?.shiftKey));
      updateTransformerCursor(event);
    });
    transformer.on("transformend", () => {
      const node = transformer.nodes()[0];
      if (node?.name?.() === "groupTransformBox") {
        applyGroupTransformBox();
        commitHistory();
        state.groupTransform = null;
        setTransformerRotationSnap(false);
        updateCanvasCursor();
        return;
      }
      if (node?.name?.() === "pointLayerTransformBox") {
        applyPointTransformBox();
        commitHistory();
        state.pointTransform = null;
        setTransformerRotationSnap(false);
        updateCanvasCursor();
        return;
      }
      if (node) normalizeNodeTransform(node);
      commitHistory();
      setTransformerRotationSnap(false);
      updateCanvasCursor();
    });
    const baseTexture = new window.Konva.Image({
      image: imageElement,
      x: 0,
      y: 0,
      width: state.textureSize.width,
      height: state.textureSize.height,
      listening: false,
      name: "baseTexture",
    });
    const baseColor = baseColorState();
    const baseColorRect = new window.Konva.Rect({
      x: 0,
      y: 0,
      width: state.textureSize.width,
      height: state.textureSize.height,
      fill: baseColor.fill,
      visible: baseColor.visible,
      listening: false,
      name: "baseColor",
    });
    backgroundLayer.add(baseTexture);
    backgroundLayer.add(baseColorRect);
    uiLayer.add(transformer);
    stage.add(backgroundLayer);
    stage.add(objectLayer);
    stage.add(uiLayer);
    state.konva = { stage, backgroundLayer, objectLayer, uiLayer, baseTexture, baseColorRect, transformer };
    stage.draggable(state.activeTool === "select");
    fitCanvasToTexture();
    renderGuides();
    bindCanvasInteractions(stage);
    rebuildKonvaLayers();
    refreshUvOverlay();
    updateCanvasStatus();
  }

  async function rebuildKonvaLayers() {
    const requestId = ++state.layerRenderRequestId;
    const layer = state.konva?.objectLayer;
    if (!layer) return;
    layer.destroyChildren();
    const documentModel = normalizeLayerDocument(state.layerDocument, state.textureSize);
    state.layerDocument = documentModel;
    renderBaseColor();
    if (flatLayerList({ includeHiddenChildren: false }).some((row) => isTextLayerType(row.type))) {
      await ensureAppFontsLoaded();
    }
    for (const modelLayer of flatLayerList({ includeHiddenChildren: false })) {
      if (requestId !== state.layerRenderRequestId) return;
      if (modelLayer.effectiveVisible === false) continue;
      try {
        if (isPngMaskGroupLayer(modelLayer)) {
          const child = modelLayer.child || {};
          if (!child.source) continue;
          let image = null;
          try {
            image = isLayerPngMissing(child) ? await missingPngPlaceholderImage(child) : await loadLayerImage(child);
          } catch (error) {
            state.imageCache.delete(`${child.id}|${resolveLayerSourcePath(child)}`);
            image = await missingPngPlaceholderImage(child);
            app().log?.(`Layer Studio masked PNG missing: ${child.name || child.id}: ${error?.message || String(error)}`);
          }
          if (requestId !== state.layerRenderRequestId || !image) return;
          const node = createPngMaskGroupNode(modelLayer, image);
          node.on("click tap", (event) => {
            if (isDraftTool()) return;
            if (shouldIgnoreCanvasObjectAction()) {
              event.cancelBubble = true;
              return;
            }
            event.cancelBubble = true;
            setSelectedLayer(modelLayer.id, { additive: Boolean(event.evt?.shiftKey) });
          });
          node.on("mouseenter", () => setHoveredLayer(modelLayer.id));
          node.on("mouseleave", () => setHoveredLayer(""));
          node.on("dragstart", (event) => {
            if (state.polygonEditLayerId === modelLayer.id) {
              event.cancelBubble = true;
              node.stopDrag?.();
              return;
            }
            if (state.spacePanActive) {
              event.cancelBubble = true;
              node.stopDrag?.();
              return;
            }
            event.cancelBubble = true;
            if (!isLayerSelected(modelLayer.id)) {
              setSelectedLayer(modelLayer.id, { additive: Boolean(event.evt?.shiftKey) });
            }
            beginHistory("Move PNG mask");
          });
          node.on("dragmove", (event) => {
            event.cancelBubble = true;
          });
          node.on("dragend", (event) => {
            event.cancelBubble = true;
            const dx = Math.round(node.x() || 0);
            const dy = Math.round(node.y() || 0);
            node.position({ x: 0, y: 0 });
            if (!movePngMaskGroupByDelta(modelLayer.id, dx, dy)) {
              cancelHistory();
              applyLayerToNode(findPngMaskGroup(modelLayer.id));
              return;
            }
            applyLayerToNode(findPngMaskGroup(modelLayer.id));
            renderLayerList();
            renderPropertiesPanel();
            updateTransformerForSelection();
            renderMultiSelectionBox();
            renderLayerHoverBox();
            commitHistory();
            setText("layerStudioEditorStatus", `PNG mask moved: ${dx}px, ${dy}px`);
          });
          layer.add(node);
          continue;
        }
        if (modelLayer.type === "stripe") {
          const node = createStripeNode(modelLayer);
          node.on("click tap", (event) => {
            if (isDraftTool()) return;
            if (shouldIgnoreCanvasObjectAction()) {
              event.cancelBubble = true;
              return;
            }
            event.cancelBubble = true;
            selectLayerFromCanvas(modelLayer.id, event);
          });
          node.on("dblclick dbltap", (event) => {
            if (isDraftTool()) return;
            if (shouldIgnoreCanvasObjectAction()) {
              event.cancelBubble = true;
              return;
            }
            event.cancelBubble = true;
            enterPolygonEditMode(modelLayer.id);
          });
          node.on("mouseenter", () => setHoveredLayer(modelLayer.id));
          node.on("mouseleave", () => setHoveredLayer(""));
          node.on("dragstart", (event) => {
            if (state.polygonEditLayerId === modelLayer.id) {
              event.cancelBubble = true;
              node.stopDrag?.();
              return;
            }
            if (state.spacePanActive) {
              event.cancelBubble = true;
              node.stopDrag?.();
              return;
            }
            if (guardGroupedCanvasChildDrag(modelLayer.id, event, node)) return;
            if (!isLayerSelected(modelLayer.id)) {
              setSelectedLayer(modelLayer.id);
            }
            beginHistory("Move stripe");
            beginMultiDrag(modelLayer.id);
          });
          node.on("dragmove", (event) => {
            const current = findLayer(node.id());
            if (!current || current.locked) return;
            if (state.multiDrag?.anchorId === node.id()) {
              applyMultiDrag(node.id(), Math.round(node.x()), Math.round(node.y()), { skipNodeId: node.id(), shiftKey: Boolean(event.evt?.shiftKey) });
              applySnappedNodePosition(node, current, { shiftKey: Boolean(event.evt?.shiftKey) });
            } else {
              applySnappedNodePosition(node, current, { shiftKey: Boolean(event.evt?.shiftKey) });
            }
          });
          node.on("dragend", (event) => {
            const current = findLayer(node.id());
            if (!current || current.locked) return;
            applySnappedNodePosition(node, current, { shiftKey: Boolean(event.evt?.shiftKey) });
            const dx = Math.round(node.x() || 0);
            const dy = Math.round(node.y() || 0);
            node.position({ x: 0, y: 0 });
            if (state.multiDrag?.anchorId === node.id()) {
              applyMultiDrag(node.id(), dx, dy, { skipNodeId: node.id(), shiftKey: Boolean(event.evt?.shiftKey) });
              endMultiDrag();
              applyLayerToNode(findLayer(node.id()));
            } else {
              patchLayer(node.id(), {
                points: (current.points || []).map((value, index) => coordinateValue(value) + (index % 2 === 0 ? dx : dy)),
              }, { renderList: false });
            }
            commitHistory();
          });
          layer.add(node);
          continue;
        }
        if (modelLayer.type === "polygon") {
          const node = createPolygonNode(modelLayer);
          node.on("click tap", (event) => {
            if (isDraftTool()) return;
            if (shouldIgnoreCanvasObjectAction()) {
              event.cancelBubble = true;
              return;
            }
            event.cancelBubble = true;
            selectLayerFromCanvas(modelLayer.id, event);
          });
          node.on("mouseenter", () => setHoveredLayer(modelLayer.id));
          node.on("mouseleave", () => setHoveredLayer(""));
          node.on("dblclick dbltap", (event) => {
            if (isDraftTool()) return;
            if (shouldIgnoreCanvasObjectAction()) {
              event.cancelBubble = true;
              return;
            }
            event.cancelBubble = true;
            enterPolygonEditMode(modelLayer.id);
          });
          node.on("dragstart", (event) => {
            if (state.spacePanActive) {
              event.cancelBubble = true;
              node.stopDrag?.();
              return;
            }
            if (guardGroupedCanvasChildDrag(modelLayer.id, event, node)) return;
            beginHistory("Move layer");
            beginMultiDrag(node.id());
          });
          node.on("dragmove", (event) => {
            const current = findLayer(node.id());
            if (!current || current.locked) return;
            if (state.multiDrag?.anchorId === node.id()) {
              applyMultiDrag(node.id(), Math.round(node.x() || 0), Math.round(node.y() || 0), { skipNodeId: node.id(), shiftKey: Boolean(event.evt?.shiftKey) });
              applySnappedNodePosition(node, current, { shiftKey: Boolean(event.evt?.shiftKey) });
            } else {
              applySnappedNodePosition(node, current, { shiftKey: Boolean(event.evt?.shiftKey) });
            }
          });
          node.on("dragend", (event) => {
            const current = findLayer(node.id());
            if (!current || current.locked) return;
            applySnappedNodePosition(node, current, { shiftKey: Boolean(event.evt?.shiftKey) });
            const dx = Math.round(node.x() || 0);
            const dy = Math.round(node.y() || 0);
            node.position({ x: 0, y: 0 });
            if (state.multiDrag?.anchorId === node.id()) {
              applyMultiDrag(node.id(), dx, dy, { skipNodeId: node.id(), shiftKey: Boolean(event.evt?.shiftKey) });
              endMultiDrag();
              applyLayerToNode(findLayer(node.id()));
            } else {
              patchLayer(node.id(), {
                points: (current.points || []).map((value, index) => coordinateValue(value) + (index % 2 === 0 ? dx : dy)),
              }, { renderList: false });
            }
            commitHistory();
          });
          layer.add(node);
          continue;
        }
        if (isShapeLayerType(modelLayer.type)) {
          const node = createShapeNode(modelLayer);
          node.on("click tap", (event) => {
            if (isDraftTool()) return;
            if (shouldIgnoreCanvasObjectAction()) {
              event.cancelBubble = true;
              return;
            }
            event.cancelBubble = true;
            selectLayerFromCanvas(modelLayer.id, event);
          });
          node.on("mouseenter", () => setHoveredLayer(modelLayer.id));
          node.on("mouseleave", () => setHoveredLayer(""));
          node.on("dragstart", (event) => {
            if (state.spacePanActive) {
              event.cancelBubble = true;
              node.stopDrag?.();
              return;
            }
            if (guardGroupedCanvasChildDrag(modelLayer.id, event, node)) return;
            beginHistory("Move layer");
            beginMultiDrag(node.id());
          });
          node.on("dragmove", (event) => {
            const current = findLayer(node.id());
            if (!current || current.locked) return;
            if (state.multiDrag?.anchorId === node.id()) {
              const snapped = snappedNodeDelta(node, current, { shiftKey: Boolean(event.evt?.shiftKey) });
              applyMultiDrag(node.id(), snapped.dx, snapped.dy, { skipNodeId: node.id(), shiftKey: Boolean(event.evt?.shiftKey) });
              applySnappedNodePosition(node, current, { shiftKey: Boolean(event.evt?.shiftKey) });
            } else {
              applySnappedNodePosition(node, current, { shiftKey: Boolean(event.evt?.shiftKey) });
              patchLayer(node.id(), {
                x: Math.round(node.x()),
                y: Math.round(node.y()),
              }, { updateNode: false, renderList: false });
            }
          });
          node.on("dragend transformend", () => {
            normalizeNodeTransform(node);
            endMultiDrag();
            commitHistory();
          });
          layer.add(node);
          continue;
        }
        if (isTextLayerType(modelLayer.type)) {
          const rendered = renderTextLayerCanvas(modelLayer);
          const node = new window.Konva.Image({
            image: rendered.canvas,
            x: modelLayer.x,
            y: modelLayer.y,
            width: modelLayer.width,
            height: modelLayer.height,
            opacity: modelLayer.opacity,
            visible: modelLayer.visible,
            rotation: modelLayer.rotation,
            draggable: isLayerCanvasSelectable(modelLayer),
            listening: isLayerCanvasSelectable(modelLayer),
            name: modelLayer.id,
            id: modelLayer.id,
          });
          node.on("click tap", (event) => {
            if (isDraftTool()) return;
            if (shouldIgnoreCanvasObjectAction()) {
              event.cancelBubble = true;
              return;
            }
            event.cancelBubble = true;
            selectLayerFromCanvas(modelLayer.id, event);
          });
          node.on("mouseenter", () => setHoveredLayer(modelLayer.id));
          node.on("mouseleave", () => setHoveredLayer(""));
          node.on("dragstart", (event) => {
            if (state.spacePanActive) {
              event.cancelBubble = true;
              node.stopDrag?.();
              return;
            }
            if (guardGroupedCanvasChildDrag(modelLayer.id, event, node)) return;
            beginHistory("Move layer");
            beginMultiDrag(node.id());
          });
          node.on("dragmove", (event) => {
            const current = findLayer(node.id());
            if (!current || current.locked) return;
            if (state.multiDrag?.anchorId === node.id()) {
              const snapped = snappedNodeDelta(node, current, { shiftKey: Boolean(event.evt?.shiftKey) });
              applyMultiDrag(node.id(), snapped.dx, snapped.dy, { skipNodeId: node.id(), shiftKey: Boolean(event.evt?.shiftKey) });
              applySnappedNodePosition(node, current, { shiftKey: Boolean(event.evt?.shiftKey) });
            } else {
              applySnappedNodePosition(node, current, { shiftKey: Boolean(event.evt?.shiftKey) });
              patchLayer(node.id(), {
                x: Math.round(node.x()),
                y: Math.round(node.y()),
              }, { updateNode: false, renderList: false });
            }
          });
          node.on("dragend transformend", () => {
            normalizeNodeTransform(node);
            endMultiDrag();
            commitHistory();
          });
          layer.add(node);
          continue;
        }
        if (modelLayer.type !== "image" || !modelLayer.source) continue;
        let image = null;
        try {
          image = isLayerPngMissing(modelLayer) ? await missingPngPlaceholderImage(modelLayer) : await loadLayerImage(modelLayer);
        } catch (error) {
          state.imageCache.delete(`${modelLayer.id}|${resolveLayerSourcePath(modelLayer)}`);
          image = await missingPngPlaceholderImage(modelLayer);
          app().log?.(`Layer Studio PNG missing: ${modelLayer.name || modelLayer.id}: ${error?.message || String(error)}`);
        }
        if (requestId !== state.layerRenderRequestId || !image) return;
        const node = new window.Konva.Image({
          image,
          x: modelLayer.x,
          y: modelLayer.y,
          width: modelLayer.width,
          height: modelLayer.height,
          offsetX: modelLayer.flipX ? modelLayer.width : 0,
          offsetY: modelLayer.flipY ? modelLayer.height : 0,
          scaleX: modelLayer.flipX ? -1 : 1,
          scaleY: modelLayer.flipY ? -1 : 1,
          rotation: modelLayer.rotation,
          opacity: modelLayer.opacity,
          visible: modelLayer.visible,
          draggable: isLayerCanvasSelectable(modelLayer),
          listening: isLayerCanvasSelectable(modelLayer),
          name: modelLayer.id,
          id: modelLayer.id,
        });
        node.on("click tap", (event) => {
          if (isDraftTool()) return;
          if (shouldIgnoreCanvasObjectAction()) {
            event.cancelBubble = true;
            return;
          }
          event.cancelBubble = true;
          selectLayerFromCanvas(modelLayer.id, event);
        });
        node.on("mouseenter", () => setHoveredLayer(modelLayer.id));
        node.on("mouseleave", () => setHoveredLayer(""));
        node.on("dragstart", (event) => {
          if (state.spacePanActive) {
            event.cancelBubble = true;
            node.stopDrag?.();
            return;
          }
          if (guardGroupedCanvasChildDrag(modelLayer.id, event, node)) return;
          beginHistory("Move layer");
          beginMultiDrag(node.id());
        });
        node.on("dragmove", (event) => {
          const current = findLayer(node.id());
          if (!current || current.locked) return;
          if (state.multiDrag?.anchorId === node.id()) {
            const snapped = snappedNodeDelta(node, current, { shiftKey: Boolean(event.evt?.shiftKey) });
            applyMultiDrag(node.id(), snapped.dx, snapped.dy, { skipNodeId: node.id(), shiftKey: Boolean(event.evt?.shiftKey) });
            applySnappedNodePosition(node, current, { shiftKey: Boolean(event.evt?.shiftKey) });
          } else {
            applySnappedNodePosition(node, current, { shiftKey: Boolean(event.evt?.shiftKey) });
            patchLayer(node.id(), {
              x: Math.round(node.x()),
              y: Math.round(node.y()),
            }, { updateNode: false, renderList: false });
          }
        });
        node.on("dragend transformend", () => {
          normalizeNodeTransform(node);
          endMultiDrag();
          commitHistory();
        });
        layer.add(node);
      } catch (error) {
        const message = error?.message || String(error);
        setText("layerStudioEditorStatus", `Layer image failed:\n${modelLayer.name}\n${message}`);
      }
    }
    layer.batchDraw();
    syncKonvaLayerOrder();
    syncSelectionState();
    renderLayerList();
    renderPropertiesPanel();
    updateTransformerForSelection();
    if (state.polygonEditLayerId && findLayer(state.polygonEditLayerId)) {
      renderPolygonEditHandles();
    } else {
      exitPolygonEditMode();
    }
    refreshUvOverlay();
    updateCanvasStatus();
  }

  function bindCanvasInteractions(stage) {
    const beginCanvasPan = (event, suppressClick = false) => {
      event.evt?.preventDefault?.();
      event.cancelBubble = true;
      stage.draggable(false);
      state.suppressCanvasClick = Boolean(suppressClick);
      state.canvasPan = {
        pointerX: numberValue(event.evt?.clientX, 0),
        pointerY: numberValue(event.evt?.clientY, 0),
        stageX: numberValue(stage.x(), 0),
        stageY: numberValue(stage.y(), 0),
      };
      updateCanvasCursor("grabbing");
    };
    const endCanvasPan = () => {
      if (!state.canvasPan) return;
      state.canvasPan = null;
      updateStageInteractivity();
      updateCanvasCursor();
      if (isDraftTool()) {
        renderPolygonDraft(pointerTexturePosition());
      }
      updateCanvasStatus();
    };
    stage.container().addEventListener("contextmenu", (event) => {
      event.preventDefault();
    });
    stage.on("mousedown tap", (event) => {
      const button = Number(event.evt?.button ?? 0);
      if (button === 2 || (button === 0 && state.spacePanActive)) {
        beginCanvasPan(event, button === 0);
        return;
      }
      if (isDraftTool()) {
        event.cancelBubble = true;
        if ((state.activeTool === "stripe" || state.activeTool === "polygon") && button === 0) {
          const pointer = pointerTexturePosition();
          const closePoint = state.activeTool === "polygon" ? polygonDraftClosePoint(pointer) : null;
          if (closePoint) {
            const pointCount = Math.floor(state.polygonDraftPoints.length / 2);
            state.polygonDraftSnap = Boolean(event.evt?.shiftKey);
            state.stripeDraftDrag = {
              pointIndex: 0,
              segmentIndex: pointCount - 1,
              point: closePoint,
              snap: Boolean(event.evt?.shiftKey),
              dragging: false,
              closing: true,
            };
            renderPolygonDraft(closePoint);
            return;
          }
          addStripeDraftPoint(event);
        }
        return;
      }
      if (state.polygonEditLayerId && button === 0) {
        const hover = updatePolygonEditEdgeHover(pointerTexturePosition());
        if (hover && !eventTargetHasName(event.target, "polygonEditPoint") && !eventTargetHasName(event.target, "stripeBezierHandle")) {
          event.evt?.preventDefault?.();
          event.cancelBubble = true;
          state.suppressCanvasClick = true;
          const active = findLayer(hover.layerId);
          pushHistory(active?.type === "stripe" ? "Add stripe point" : "Add polygon point");
          if (addPointToPointLayerSegment(active, hover.segmentIndex, hover.point, { history: false })) {
            state.polygonEditAddDrag = {
              layerId: active.id,
              pointIndex: hover.segmentIndex + 1,
              dragging: false,
            };
          }
          return;
        }
      }
      if (event.target === stage || event.target?.name?.() === "baseTexture" || event.target?.name?.() === "baseColor") {
        setSelectedLayer("");
      }
    });
    stage.on("click tap", async (event) => {
      if (event.evt?.button === 2 || state.canvasPan || state.suppressCanvasClick) {
        state.suppressCanvasClick = false;
        event.cancelBubble = true;
        return;
      }
      if (state.polygonEditLayerId) {
        const hover = updatePolygonEditEdgeHover(pointerTexturePosition());
        if (hover && !eventTargetHasName(event.target, "polygonEditPoint") && !eventTargetHasName(event.target, "stripeBezierHandle")) {
          event.cancelBubble = true;
          const active = findLayer(hover.layerId);
          addPointToPointLayerSegment(active, hover.segmentIndex, hover.point);
          return;
        }
      }
      if (!isDraftTool()) return;
      event.cancelBubble = true;
      if (state.suppressDraftClick) {
        state.suppressDraftClick = false;
        return;
      }
      if (state.activeTool === "stripe") return;
      if (event.evt?.detail && event.evt.detail > 1) return;
      const pointer = pointerTexturePosition();
      if (polygonDraftClosePoint(pointer)) {
        await finishPolygonDraft();
        return;
      }
      const point = polygonDraftPoint(pointer, Boolean(event.evt?.shiftKey));
      if (!point) return;
      state.polygonDraftSnap = Boolean(event.evt?.shiftKey);
      state.polygonDraftPoints.push(point.x, point.y);
      renderPolygonDraft(point);
    });
    stage.on("wheel", (event) => {
      event.evt.preventDefault();
      const pointer = stage.getPointerPosition();
      if (!pointer) return;
      const oldScale = stage.scaleX() || 1;
      const scaleBy = 1.08;
      const direction = event.evt.deltaY > 0 ? -1 : 1;
      const nextScale = Math.max(0.04, Math.min(8, direction > 0 ? oldScale * scaleBy : oldScale / scaleBy));
      const texturePoint = {
        x: (pointer.x - stage.x()) / oldScale,
        y: (pointer.y - stage.y()) / oldScale,
      };
      stage.scale({ x: nextScale, y: nextScale });
      stage.position({
        x: pointer.x - texturePoint.x * nextScale,
        y: pointer.y - texturePoint.y * nextScale,
      });
      updateViewFromStage();
    });
    stage.on("dragmove", () => updateViewFromStage());
    stage.on("mouseup", async () => {
      const draftDrag = state.stripeDraftDrag;
      state.stripeDraftDrag = null;
      if (draftDrag?.closing && draftDrag.dragging && state.activeTool === "polygon") {
        state.suppressDraftClick = true;
        await finishPolygonDraft();
        endCanvasPan();
        return;
      }
      const addDrag = state.polygonEditAddDrag;
      if (addDrag) {
        state.polygonEditAddDrag = null;
        if (addDrag.dragging) {
          const active = findLayer(addDrag.layerId);
          if (active) {
            patchLayer(active.id, {
              points: active.points.slice(),
              bezierHandles: active.bezierHandles,
            }, { renderList: false, renderEditHandles: false });
          }
        }
        commitHistory();
        renderPolygonEditHandles();
      }
      endCanvasPan();
    });
    stage.on("mousemove", (event) => {
      if (state.canvasPan) {
        const dx = numberValue(event.evt?.clientX, state.canvasPan.pointerX) - state.canvasPan.pointerX;
        const dy = numberValue(event.evt?.clientY, state.canvasPan.pointerY) - state.canvasPan.pointerY;
        stage.position({
          x: state.canvasPan.stageX + dx,
          y: state.canvasPan.stageY + dy,
        });
        updateViewFromStage();
      }
      if (state.polygonEditAddDrag) {
        updatePolygonEditAddDrag(pointerTexturePosition(), Boolean(event.evt?.shiftKey), Boolean(event.evt?.altKey));
      } else if (isDraftTool()) {
        const pointer = pointerTexturePosition();
        if (state.stripeDraftDrag) {
          updateStripeDraftBezier(pointer, Boolean(event.evt?.shiftKey), Boolean(event.evt?.altKey));
        } else {
          renderPolygonDraft(pointer);
        }
      } else if (state.polygonEditLayerId) {
        if (syncPolygonEditControlHover(event.target)) {
          state.polygonEditEdgeHover = null;
          state.konva?.polygonEditGroup?.findOne?.(".polygonEditAddMarker")?.visible?.(false);
          updateCanvasStatus();
          return;
        }
        const hover = updatePolygonEditEdgeHover(pointerTexturePosition());
        const container = state.konva?.stage?.container?.();
        if (container && hover && !eventTargetHasName(event.target, "polygonEditPoint") && !eventTargetHasName(event.target, "stripeBezierHandle")) {
          container.style.cursor = "copy";
        } else {
          updateCanvasCursor();
        }
      }
      updateCanvasStatus();
    });
    stage.on("mouseenter", () => {
      updateCanvasCursor();
    });
    stage.on("dragstart", () => {
      updateCanvasCursor(usesPrecisionCursor() ? "crosshair" : "grabbing");
    });
    stage.on("dragend", () => {
      updateCanvasCursor();
      updateViewFromStage();
    });
    stage.on("mouseleave", () => {
      state.canvasPan = null;
      updateStageInteractivity();
      syncPolygonEditControlHover(null);
      updateCanvasCursor(null);
      setHoveredLayer("");
      state.polygonEditEdgeHover = null;
      state.konva?.polygonEditGroup?.findOne?.(".polygonEditAddMarker")?.visible?.(false);
    });
  }

  function fitCanvasToTexture() {
    const stage = state.konva?.stage;
    if (!stage) return;
    const size = stageHostSize();
    stage.size(size);
    const pasteboardWidth = state.textureSize.width * (1 + CANVAS_PASTEBOARD_RATIO * 2);
    const pasteboardHeight = state.textureSize.height * (1 + CANVAS_PASTEBOARD_RATIO * 2);
    const scale = Math.min(size.width / pasteboardWidth, size.height / pasteboardHeight) * 0.94;
    const safeScale = Number.isFinite(scale) && scale > 0 ? scale : 0.1;
    stage.scale({ x: safeScale, y: safeScale });
    stage.position({
      x: Math.round((size.width - state.textureSize.width * safeScale) / 2),
      y: Math.round((size.height - state.textureSize.height * safeScale) / 2),
    });
    updateViewFromStage();
  }

  function resizeCanvas() {
    const stage = state.konva?.stage;
    if (!stage) return;
    const size = stageHostSize();
    stage.size(size);
    stage.batchDraw();
    renderMultiSelectionBox();
    renderLayerHoverBox();
    updateCanvasStatus();
  }

  function updateViewFromStage() {
    const stage = state.konva?.stage;
    if (!stage) return;
    state.view = {
      scale: stage.scaleX() || 1,
      x: stage.x() || 0,
      y: stage.y() || 0,
    };
    stage.batchDraw();
    if (state.polygonEditLayerId) renderPolygonEditHandles();
    renderMultiSelectionBox();
    renderLayerHoverBox();
    updateCanvasStatus();
  }

  function currentViewCenterTexturePoint() {
    const stage = state.konva?.stage;
    if (!stage) {
      return {
        x: state.textureSize.width / 2,
        y: state.textureSize.height / 2,
      };
    }
    const scale = Math.max(0.04, numberValue(stage.scaleX(), state.view.scale || 1));
    const size = stage.size?.() || stageHostSize();
    return {
      x: (size.width / 2 - numberValue(stage.x(), state.view.x || 0)) / scale,
      y: (size.height / 2 - numberValue(stage.y(), state.view.y || 0)) / scale,
    };
  }

  function layerOriginForViewCenter(width, height) {
    const center = currentViewCenterTexturePoint();
    const layerWidth = Math.max(1, numberValue(width, 1));
    const layerHeight = Math.max(1, numberValue(height, 1));
    return {
      x: Math.round(center.x - layerWidth / 2),
      y: Math.round(center.y - layerHeight / 2),
      centerX: Math.round(center.x),
      centerY: Math.round(center.y),
    };
  }

  function pointerTexturePosition() {
    const stage = state.konva?.stage;
    if (!stage) return null;
    const pointer = stage.getPointerPosition();
    if (!pointer) return null;
    const scale = stage.scaleX() || 1;
    return {
      x: (pointer.x - stage.x()) / scale,
      y: (pointer.y - stage.y()) / scale,
    };
  }

  function ensurePolygonDraftNodes() {
    const uiLayer = state.konva?.uiLayer;
    if (!uiLayer || !window.Konva) return null;
    if (!state.konva.polygonDraftLine) {
      state.konva.polygonDraftLine = new window.Konva.Line({
        points: [],
        stroke: DRAFT_OVERLAY_COLOR,
        strokeWidth: 3,
        dash: [10, 7],
        closed: false,
        listening: false,
        name: "polygonDraft",
      });
      uiLayer.add(state.konva.polygonDraftLine);
    }
    if (!state.konva.stripeDraftShape) {
      state.konva.stripeDraftShape = new window.Konva.Shape({
        listening: false,
        visible: false,
        name: "stripeDraft",
        sceneFunc(context, shape) {
          const model = shape.getAttr("draftModel") || {};
          context.beginPath();
          if (model.type === "polygon") {
            drawOpenBezierPath(context, model.points || [], model.bezierHandles || []);
          } else {
            drawStripePath(context, model.points || [], 0, model.bezierHandles || []);
          }
          context.strokeShape(shape);
        },
      });
      uiLayer.add(state.konva.stripeDraftShape);
    }
    if (!state.konva.polygonDraftHandles) {
      state.konva.polygonDraftHandles = new window.Konva.Group({
        listening: false,
        name: "polygonDraftHandles",
      });
      uiLayer.add(state.konva.polygonDraftHandles);
    }
    return state.konva.polygonDraftLine;
  }

  function renderPolygonDraft(pointer = null) {
    const line = ensurePolygonDraftNodes();
    const uiLayer = state.konva?.uiLayer;
    if (!line || !uiLayer) return;
    const points = state.polygonDraftPoints.slice();
    const closePoint = polygonDraftClosePoint(pointer);
    if (pointer && points.length >= 2 && !state.stripeDraftDrag) {
      const point = polygonDraftPreviewPoint(pointer);
      if (point) points.push(point.x, point.y);
    }
    const stripeShape = state.konva?.stripeDraftShape;
    const hasDraftBezier = Array.isArray(state.stripeDraftBezierHandles) && state.stripeDraftBezierHandles.some((handle) => handle && (handle.out || handle.in));
    if (state.activeTool === "stripe" || (state.activeTool === "polygon" && hasDraftBezier)) {
      line.visible(false);
      stripeShape?.setAttrs?.({
        draftModel: {
          type: state.activeTool,
          points,
          bezierHandles: state.stripeDraftBezierHandles,
        },
        stroke: DRAFT_OVERLAY_COLOR,
        strokeWidth: state.activeTool === "stripe" ? 8 : 3,
        dash: state.activeTool === "stripe" ? [] : [10, 7],
        lineCap: "round",
        lineJoin: "round",
        visible: state.activeTool === "stripe" ? points.length >= 4 : points.length >= 4,
      });
      stripeShape?.moveToTop?.();
    } else {
      stripeShape?.visible?.(false);
      line.points(points);
      line.stroke(DRAFT_OVERLAY_COLOR);
      line.strokeWidth(3);
      line.dash([10, 7]);
      line.visible(points.length >= 2);
      line.moveToTop();
    }
    const handles = state.konva?.polygonDraftHandles;
    handles?.destroyChildren?.();
    const handleFill = DRAFT_OVERLAY_COLOR;
    const handleStroke = DRAFT_OVERLAY_DARK;
    const pointRadius = textureUnitsForScreenPixels(DRAFT_POINT_SCREEN_RADIUS);
    const firstRadius = textureUnitsForScreenPixels(closePoint ? DRAFT_FIRST_POINT_SCREEN_RADIUS + 2 : DRAFT_FIRST_POINT_SCREEN_RADIUS);
    for (let index = 0; index < state.polygonDraftPoints.length; index += 2) {
      const isFirst = index === 0;
      const canClose = state.activeTool === "polygon" && state.polygonDraftPoints.length >= 6;
      handles?.add?.(new window.Konva.Circle({
        x: state.polygonDraftPoints[index],
        y: state.polygonDraftPoints[index + 1],
        radius: isFirst && canClose ? firstRadius : pointRadius,
        fill: isFirst && canClose ? "#ffffff" : handleFill,
        stroke: isFirst && canClose ? (closePoint ? DRAFT_CLOSE_COLOR : handleFill) : handleStroke,
        strokeWidth: textureUnitsForScreenPixels(isFirst && canClose ? 2 : 1),
        listening: false,
        name: "polygonDraftPoint",
      }));
    }
    if (state.activeTool === "stripe" || state.activeTool === "polygon") {
      const bezierLineStroke = textureUnitsForScreenPixels(1.5);
      const bezierDash = [textureUnitsForScreenPixels(6), textureUnitsForScreenPixels(5)];
      const bezierRadius = textureUnitsForScreenPixels(9);
      const closedDraft = state.activeTool === "polygon";
      const draftPointCount = Math.floor(state.polygonDraftPoints.length / 2);
      (Array.isArray(state.stripeDraftBezierHandles) ? state.stripeDraftBezierHandles : []).forEach((handle, segmentIndex) => {
        if (!handle || !(handle.out || handle.in)) return;
        const isClosingSegment = closedDraft && segmentIndex === draftPointCount - 1;
        if ((segmentIndex + 1) * 2 + 1 >= state.polygonDraftPoints.length && !isClosingSegment) return;
        const start = pointAt(state.polygonDraftPoints, segmentIndex, closedDraft);
        const end = pointAt(state.polygonDraftPoints, segmentIndex + 1, closedDraft);
        const fallback = pointSegmentDefaultHandles(state.polygonDraftPoints, segmentIndex, closedDraft);
        const out = handle.out || fallback.out;
        const input = handle.in || fallback.in;
        handles?.add?.(new window.Konva.Line({
          points: [start.x, start.y, out.x, out.y, input.x, input.y, end.x, end.y],
          stroke: STRIPE_BEZIER_HANDLE_COLOR,
          strokeWidth: bezierLineStroke,
          dash: bezierDash,
          opacity: 0.8,
          listening: false,
          name: "stripeDraftBezierLine",
        }));
        [out, input].forEach((point) => {
          handles?.add?.(new window.Konva.Circle({
            x: point.x,
            y: point.y,
            radius: bezierRadius,
            fill: STRIPE_BEZIER_HANDLE_COLOR,
            stroke: DRAFT_OVERLAY_DARK,
            strokeWidth: textureUnitsForScreenPixels(2),
            opacity: 0.9,
            listening: false,
            name: "stripeDraftBezierHandle",
          }));
        });
      });
    }
    handles?.visible?.(state.polygonDraftPoints.length >= 2);
    handles?.moveToTop?.();
    state.konva?.transformer?.moveToTop?.();
    uiLayer.batchDraw();
  }

  function cancelPolygonDraft() {
    state.polygonDraftPoints = [];
    state.stripeDraftBezierHandles = [];
    state.stripeDraftDrag = null;
    state.suppressDraftClick = false;
    state.polygonDraftSnap = false;
    const line = state.konva?.polygonDraftLine;
    if (line) {
      line.points([]);
      line.visible(false);
    }
    state.konva?.stripeDraftShape?.visible?.(false);
    state.konva?.polygonDraftHandles?.destroyChildren?.();
    state.konva?.polygonDraftHandles?.visible?.(false);
    state.konva?.uiLayer?.batchDraw?.();
  }

  function nextLayerId(prefix = "layer") {
    const existing = new Set(layerList().map((layer) => layer.id));
    flatLayerList({ includeMaskChildren: true }).forEach((layer) => existing.add(layer.id));
    let index = layerList().length + 1;
    let id = `${prefix}_${String(index).padStart(3, "0")}`;
    while (existing.has(id)) {
      index += 1;
      id = `${prefix}_${String(index).padStart(3, "0")}`;
    }
    return id;
  }

  function groupableSelectedLayers() {
    if (isDraftTool() || state.polygonEditLayerId) return [];
    const ids = selectedLayerIds();
    if (ids.length < 2) return [];
    const topLevelById = new Map(layerList().map((layer) => [layer.id, layer]));
    const layers = ids.map((id) => topLevelById.get(id)).filter(Boolean);
    if (layers.length !== ids.length) return [];
    if (layers.some((layer) => layer.type === "group" || isPngMaskGroupLayer(layer) || layer.locked)) return [];
    return layers;
  }

  function canCreateGroupFromSelection() {
    return groupableSelectedLayers().length >= 2;
  }

  function ungroupableSelectedGroup() {
    if (isDraftTool() || state.polygonEditLayerId) return null;
    const ids = selectedLayerIds();
    if (ids.length !== 1) return null;
    const group = findGroup(ids[0]);
    if (!group || group.locked) return null;
    return group;
  }

  function canUngroupSelection() {
    return Boolean(ungroupableSelectedGroup());
  }

  function maskableSelectedLayers() {
    if (isDraftTool() || state.polygonEditLayerId) return null;
    const ids = selectedLayerIds();
    if (ids.length !== 2) return null;
    const topLevelById = new Map(layerList().map((layer) => [layer.id, layer]));
    const layers = ids.map((id) => topLevelById.get(id)).filter(Boolean);
    if (layers.length !== 2) return null;
    if (layers.some((layer) => layer.locked || layer.visible === false || layer.type === "group" || isPngMaskGroupLayer(layer))) return null;
    const polygon = layers.find((layer) => layer.type === "polygon");
    const image = layers.find((layer) => layer.type === "image");
    if (!polygon || !image || !image.source || isLayerPngMissing(image)) return null;
    if (numberValue(polygon.order, 0) <= numberValue(image.order, 0)) return null;
    return { polygon, image };
  }

  function canCreatePngMaskFromSelection() {
    return Boolean(maskableSelectedLayers());
  }

  function createPngMaskFromSelection() {
    const selection = maskableSelectedLayers();
    if (!selection || !state.layerDocument?.layers) {
      setText("layerStudioEditorStatus", "Select one polygon layer above one PNG layer before creating a mask.");
      return null;
    }
    const { polygon, image } = selection;
    pushHistory("Create PNG mask");
    const groupId = nextLayerId("mask");
    const existingIds = new Set([
      ...layerList().map((layer) => layer.id),
      ...flatLayerList({ includeMaskChildren: true }).map((layer) => layer.id),
    ].filter((id) => id !== groupId && id !== polygon.id && id !== image.id));
    const maskGroup = normalizePngMaskGroupLayer({
      id: groupId,
      name: `${cleanText(polygon.name, "Polygon")} Mask`,
      type: "pngMaskGroup",
      visible: true,
      locked: false,
      collapsed: false,
      order: polygon.order,
      mask: cloneJson(polygon),
      child: cloneJson(image),
    }, layerList().length, state.textureSize, existingIds);
    const selectedSet = new Set([polygon.id, image.id]);
    const nextLayers = layerList()
      .filter((layer) => !selectedSet.has(layer.id))
      .map((layer) => ({ ...layer }));
    nextLayers.push(maskGroup);
    nextLayers.sort((left, right) => (left.order - right.order) || left.id.localeCompare(right.id));
    nextLayers.forEach((layer, index) => {
      layer.order = index;
    });
    state.layerDocument.layers = nextLayers;
    state.selectedLayerIds = [maskGroup.id];
    state.selectedLayerId = maskGroup.id;
    setDirty(true);
    return maskGroup;
  }

  function unmaskLayer(maskGroupId) {
    const group = findPngMaskGroup(maskGroupId);
    if (!group || group.locked) return false;
    pushHistory("Remove PNG mask");
    const child = { ...cloneJson(group.child) };
    const mask = { ...cloneJson(group.mask) };
    delete child.groupId;
    delete child.maskGroupId;
    delete child.maskRole;
    delete mask.groupId;
    delete mask.maskGroupId;
    delete mask.maskRole;
    const nextLayers = [];
    layerList().forEach((layer) => {
      if (layer.id !== group.id) {
        nextLayers.push(layer);
        return;
      }
      nextLayers.push(child, mask);
    });
    nextLayers.forEach((layer, index) => {
      layer.order = index;
    });
    state.layerDocument.layers = nextLayers;
    state.selectedLayerIds = [mask.id, child.id];
    state.selectedLayerId = child.id;
    setDirty(true);
    return true;
  }

  function createGroupFromSelection() {
    const selectedTopLayers = groupableSelectedLayers()
      .sort((left, right) => (left.order - right.order) || left.id.localeCompare(right.id));
    if (selectedTopLayers.length < 2) {
      setText("layerStudioEditorStatus", "Select at least two top-level layers before creating a group.");
      return null;
    }
    pushHistory("Create group");
    const selectedSet = new Set(selectedTopLayers.map((layer) => layer.id));
    const firstOrder = Math.min(...selectedTopLayers.map((layer) => layer.order));
    const groupId = nextLayerId("group");
    const group = {
      id: groupId,
      name: "Layer Group",
      type: "group",
      visible: true,
      locked: false,
      collapsed: false,
      order: firstOrder,
      children: selectedTopLayers.map((layer, index) => ({
        ...cloneJson(layer),
        order: index,
        groupId,
      })),
    };
    const nextLayers = layerList()
      .filter((layer) => !selectedSet.has(layer.id))
      .map((layer) => ({ ...layer }));
    nextLayers.push(group);
    nextLayers.sort((left, right) => (left.order - right.order) || left.id.localeCompare(right.id));
    nextLayers.forEach((layer, index) => {
      layer.order = index;
    });
    state.layerDocument.layers = nextLayers;
    state.selectedLayerIds = [groupId];
    state.selectedLayerId = groupId;
    setDirty(true);
    return group;
  }

  function ungroupLayer(groupId) {
    const group = findGroup(groupId);
    if (!group || group.locked) return false;
    pushHistory("Ungroup layer");
    const children = (group.children || []).map((child) => {
      const copy = { ...cloneJson(child) };
      delete copy.groupId;
      return copy;
    });
    const nextLayers = [];
    layerList().forEach((layer) => {
      if (layer.id !== group.id) {
        nextLayers.push(layer);
        return;
      }
      nextLayers.push(...children);
    });
    nextLayers.forEach((layer, index) => {
      layer.order = index;
    });
    state.layerDocument.layers = nextLayers;
    state.selectedLayerIds = children.map((layer) => layer.id);
    state.selectedLayerId = state.selectedLayerIds[state.selectedLayerIds.length - 1] || "";
    setDirty(true);
    return true;
  }

  async function createGroupFromCurrentSelection() {
    const group = createGroupFromSelection();
    if (!group) return false;
    await rebuildLayerWorkflow({ keepSelection: true });
    setText("layerStudioEditorStatus", `Group created: ${group.name}`);
    return true;
  }

  async function ungroupCurrentSelection() {
    const group = ungroupableSelectedGroup();
    if (!group) return false;
    if (!ungroupLayer(group.id)) return false;
    await rebuildLayerWorkflow({ keepSelection: true });
    setText("layerStudioEditorStatus", `Group removed: ${group.name || group.id}`);
    return true;
  }

  async function createShapeLayer(type) {
    const shapeType = type === "ellipse" ? "ellipse" : "rectangle";
    if (!state.layerDocument?.layers) return null;
    const fillColor = isSplatMapSurface() ? activeSplatMapColor() : activeDrawingColor("#e43d30");
    const shapeWidth = 360;
    const shapeHeight = 360;
    const displayName = shapeType === "ellipse" ? "Ellipse" : "Rectangle";
    const origin = layerOriginForViewCenter(shapeWidth, shapeHeight);
    pushHistory(`Create ${displayName.toLowerCase()}`);
    normalizeLayerOrders();
    const layer = normalizeShapeLayer({
      id: nextLayerId(shapeType),
      name: displayName,
      type: shapeType,
      x: origin.x,
      y: origin.y,
      width: shapeWidth,
      height: shapeHeight,
      rotation: 0,
      fill: fillColor,
      opacity: 1,
      visible: true,
      locked: false,
      order: layerList().length,
    }, layerList().length, state.textureSize, shapeType);
    state.layerDocument.layers.push(layer);
    state.selectedLayerId = layer.id;
    state.selectedLayerIds = [layer.id];
    setActiveTool("select");
    setDirty(true);
    await rebuildLayerWorkflow({ keepSelection: true });
    setText("layerStudioEditorStatus", `${displayName} layer created.`);
    return layer;
  }

  async function createTextLayer() {
    if (!state.layerDocument?.layers) return null;
    if (isSplatMapSurface()) {
      setText("layerStudioEditorStatus", "Text layers are disabled for EXT_Skin_D. Use mask shapes with the fixed splat map colors.");
      return null;
    }
    await ensureAppFontsLoaded();
    const preset = defaultFontPreset();
    const fontSize = 96;
    const text = "NEW TEXT";
    const metrics = textLayerMetrics({
      text,
      fontFamily: preset.family,
      fontWeight: 700,
      fontSize,
    });
    const origin = layerOriginForViewCenter(metrics.width, metrics.height);
    pushHistory("Create text");
    normalizeLayerOrders();
    const layer = normalizeTextLayer({
      id: nextLayerId("text"),
      type: "text",
      text,
      fontSourceType: "app",
      fontFamily: preset.family,
      fontWeight: 700,
      fontSize,
      x: origin.x,
      y: origin.y,
      width: metrics.width,
      height: metrics.height,
      rotation: 0,
      fill: activeDrawingColor("#ffffff"),
      opacity: 1,
      visible: true,
      locked: false,
      order: layerList().length,
    }, layerList().length, state.textureSize);
    state.layerDocument.layers.push(layer);
    state.selectedLayerId = layer.id;
    state.selectedLayerIds = [layer.id];
    setActiveTool("select");
    setDirty(true);
    await rebuildLayerWorkflow({ keepSelection: true });
    setText("layerStudioEditorStatus", "Text layer created.\nEdit content, font and size in the layer detail bar.");
    return layer;
  }

  async function finishPolygonDraft() {
    const points = state.polygonDraftPoints.slice();
    const bezierHandles = normalizePolygonBezierHandles(state.stripeDraftBezierHandles, points, state.textureSize);
    if (points.length < 6) {
      setText("layerStudioEditorStatus", "Polygon needs at least 3 points.");
      return false;
    }
    pushHistory("Create polygon");
    normalizeLayerOrders();
    const layer = normalizePolygonLayer({
      id: nextLayerId("polygon"),
      name: "Polygon",
      points,
      bezierHandles,
      fill: isSplatMapSurface() ? activeSplatMapColor() : activeDrawingColor("#e43d30"),
      opacity: 1,
      visible: true,
      locked: false,
      order: layerList().length,
    }, layerList().length, state.textureSize);
    state.layerDocument.layers.push(layer);
    state.selectedLayerId = layer.id;
    state.selectedLayerIds = [layer.id];
    cancelPolygonDraft();
    setActiveTool("select");
    setDirty(true);
    await rebuildLayerWorkflow({ keepSelection: true });
    setText("layerStudioEditorStatus", `Polygon layer created.\nPoints: ${Math.floor(points.length / 2)}`);
    return true;
  }

  async function finishStripeDraft() {
    const points = state.polygonDraftPoints.slice();
    const bezierHandles = normalizeStripeBezierHandles(state.stripeDraftBezierHandles, points, state.textureSize);
    if (points.length < 4) {
      setText("layerStudioEditorStatus", "Stripe needs at least 2 points.");
      return false;
    }
    pushHistory("Create stripe");
    normalizeLayerOrders();
    const layer = normalizeStripeLayer({
      id: nextLayerId("stripe"),
      name: "Stripe",
      points,
      stroke: isSplatMapSurface() ? activeSplatMapColor() : activeDrawingColor("#f07a00"),
      strokeWidth: 20,
      opacity: 1,
      tension: 0,
      bezierHandles,
      lineCap: "round",
      lineJoin: "round",
      visible: true,
      locked: false,
      order: layerList().length,
    }, layerList().length, state.textureSize);
    state.layerDocument.layers.push(layer);
    state.selectedLayerId = layer.id;
    state.selectedLayerIds = [layer.id];
    cancelPolygonDraft();
    setActiveTool("select");
    setDirty(true);
    await rebuildLayerWorkflow({ keepSelection: true });
    setText("layerStudioEditorStatus", `Stripe layer created.\nPoints: ${Math.floor(points.length / 2)}`);
    return true;
  }

  async function finishDraftLayer() {
    if (state.activeTool === "stripe") return await finishStripeDraft();
    return await finishPolygonDraft();
  }

  function updateCanvasStatus() {
    const pointer = pointerTexturePosition();
    const pointerLine = pointer
      ? `Pointer texture px: ${Math.round(pointer.x)}, ${Math.round(pointer.y)}`
      : "Pointer texture px: -";
    setText(
      "layerStudioEditorStatus",
      [
        state.polygonEditLayerId
          ? `${findLayer(state.polygonEditLayerId)?.type === "stripe" ? "Stripe" : "Polygon"} point edit mode.\nSelect a point to show its Bezier handles, hold Alt to break coupling, Shift to snap.`
          : "Konva canvas ready.",
        `Texture-space: ${state.textureSize.width} x ${state.textureSize.height}`,
        `Editable layers: ${state.layerDocument?.layers?.length || 0}`,
        `Guides: ${guideList().length} / Snap: ${state.layerDocument?.snapEnabled === false ? "off" : "on"}`,
        `Zoom: ${Math.round((state.view.scale || 1) * 100)}%`,
        pointerLine,
      ].join("\n"),
    );
  }

  async function importPngLayer() {
    if (isSplatMapSurface()) {
      setText("layerStudioEditorStatus", "PNG layers are disabled for EXT_Skin_D. Use mask shapes with the fixed splat map colors.");
      return false;
    }
    if (!state.context?.skinKey) {
      window.showStatusModal?.({
        type: "error",
        title: "Layer Studio",
        subtitle: "No livery loaded",
        message: "Open Layer Studio for an editable local livery before importing PNG assets.",
      });
      return false;
    }
    if (!window.pywebview?.api?.choose_path || !window.pywebview?.api?.import_layer_studio_png) {
      setText("layerStudioEditorStatus", "PNG import is not available in this app build yet.");
      return false;
    }
    const button = $("layerStudioImportPng");
    app().setButtonWorking?.(button, true, "Importing...");
    try {
      const picked = await window.pywebview.api.choose_path("file", ["PNG (*.png)", "All files (*.*)"], "");
      if (!picked?.ok) {
        app().showResult?.(picked, "PNG import failed", "choose_path");
        return false;
      }
      const sourcePng = String(picked.data?.path || "").trim();
      if (!sourcePng) return false;
      if (!/\.png$/i.test(sourcePng)) {
        const message = "Layer Studio MVP only supports PNG imports. SVG and other formats are planned for later.";
        setText("layerStudioEditorStatus", message);
        window.showStatusModal?.({
          type: "warning",
          title: "Layer Studio",
          subtitle: "Unsupported file type",
          message,
        });
        return false;
      }

      const result = await window.pywebview.api.import_layer_studio_png({
        skinKey: state.context.skinKey,
        surfaceKey: state.activeSurfaceKey,
        sourcePng,
        root: app().root?.() || "",
        layerDocument: serializeLayerDocument(),
        placement: currentViewCenterTexturePoint(),
      });
      app().showResult?.(result, "PNG imported", "import_layer_studio_png");
      if (!result?.ok) {
        setText("layerStudioEditorStatus", result?.message || "PNG import failed.");
        return false;
      }

      const historySnapshot = layerHistorySnapshot("Import PNG layer");
      const data = result.data || {};
      const context = data.layerStudio || state.context || {};
      renderContext(context);
      pushHistorySnapshot(historySnapshot);
      state.layerDocument = normalizeLayerDocument(data.layerDocument || context.layerDocument, state.textureSize);
      state.selectedLayerId = data.layer?.id || state.selectedLayerId;
      state.selectedLayerIds = state.selectedLayerId ? [state.selectedLayerId] : [];
      if (state.konva?.objectLayer) {
        await rebuildKonvaLayers();
      } else {
        await loadBaseTexture();
      }
      const layer = data.layer || {};
      const asset = data.asset || {};
      setText(
        "layerStudioEditorStatus",
        [
          "PNG imported and added as editable image layer.",
          `Layer: ${layer.name || layer.id || "-"}`,
          `Asset: ${asset.path || data.relativeSource || "-"}`,
          `Editable layers: ${state.layerDocument?.layers?.length || 0}`,
        ].join("\n"),
      );
      return true;
    } catch (error) {
      const message = error?.message || String(error);
      setText("layerStudioEditorStatus", `PNG import failed:\n${message}`);
      app().log?.(message, true);
      return false;
    } finally {
      app().setButtonWorking?.(button, false);
    }
  }

  async function addPngToLibrary() {
    const scope = activeLibraryScope();
    const apiName = scope === "global" ? "add_layer_studio_library_png" : "add_layer_studio_project_library_png";
    if (!window.pywebview?.api?.choose_path || !window.pywebview?.api?.[apiName]) {
      setText("layerStudioLibraryStatus", "Library import unavailable");
      return false;
    }
    const button = $("layerStudioLibraryAddPng");
    app().setButtonWorking?.(button, true, "Adding...");
    try {
      const picked = await window.pywebview.api.choose_path("file", ["PNG (*.png)", "All files (*.*)"], "", "", true);
      if (!picked?.ok) {
        app().showResult?.(picked, "PNG library import failed", "choose_path");
        return false;
      }
      const selectedPaths = Array.isArray(picked.data?.paths)
        ? picked.data.paths.map((path) => cleanText(path)).filter(Boolean)
        : [cleanText(picked.data?.path)].filter(Boolean);
      const sourcePngs = Array.from(new Set(selectedPaths));
      if (!sourcePngs.length) return false;
      const summary = {
        added: 0,
        duplicate: 0,
        skipped: [],
        failed: [],
        imported: [],
      };
      const showImportProgress = (index, total, label = "") => {
        window.showStatusModal?.({
          type: "info",
          title: "PNG Library Upload",
          subtitle: `${index} / ${total}`,
          message: label ? `Importing PNG assets...\n${label}` : "Importing PNG assets...",
          details: [
            `Selected: ${total}`,
            `Added: ${summary.added}`,
            `Already existed: ${summary.duplicate}`,
            `Skipped: ${summary.skipped.length}`,
            `Failed: ${summary.failed.length}`,
          ].join("\n"),
          blockClose: true,
        });
      };
      showImportProgress(0, sourcePngs.length);
      for (let index = 0; index < sourcePngs.length; index += 1) {
        const sourcePng = sourcePngs[index];
        const label = sourcePng.split(/[\\/]/).pop() || sourcePng;
        showImportProgress(index + 1, sourcePngs.length, label);
        if (!/\.png$/i.test(sourcePng)) {
          summary.skipped.push(`${label}: unsupported file type`);
          continue;
        }
        let result = null;
        try {
          result = await window.pywebview.api[apiName]({
            skinKey: state.context?.skinKey || "",
            root: app().root?.() || "",
            sourcePng,
          });
        } catch (error) {
          result = { ok: false, message: error?.message || String(error) };
        }
        if (!result?.ok) {
          summary.failed.push(`${label}: ${result?.message || "Library import failed"}`);
          continue;
        }
        setLibraryForScope(scope, result.data?.assetLibrary || libraryForScope(scope) || { assets: [] });
        const asset = result.data?.asset || {};
        summary.imported.push(asset.name || asset.id || label);
        if (result.data?.duplicate) {
          summary.duplicate += 1;
        } else {
          summary.added += 1;
        }
      }
      if (!libraryForScope(scope)) {
        await loadActiveAssetLibrary({ silent: true });
      }
      renderAssetLibrary();
      const totalOk = summary.added + summary.duplicate;
      const hasIssues = Boolean(summary.failed.length || summary.skipped.length);
      const finalType = summary.failed.length ? (totalOk ? "warning" : "error") : summary.skipped.length ? "warning" : "success";
      const details = [
        `Selected: ${sourcePngs.length}`,
        `Added: ${summary.added}`,
        `Already existed: ${summary.duplicate}`,
        `Skipped: ${summary.skipped.length}`,
        `Failed: ${summary.failed.length}`,
        `Library: ${libraryForScope(scope)?.path || "-"}`,
        ...(summary.imported.length ? ["", "Imported:", ...summary.imported.map((name) => `- ${name}`)] : []),
        ...(summary.skipped.length ? ["", "Skipped:", ...summary.skipped.map((row) => `- ${row}`)] : []),
        ...(summary.failed.length ? ["", "Failed:", ...summary.failed.map((row) => `- ${row}`)] : []),
      ].join("\n");
      window.showStatusModal?.({
        type: finalType,
        title: "PNG Library Upload",
        subtitle: hasIssues ? "Finished with issues" : "Upload complete",
        message: `${totalOk} PNG asset${totalOk === 1 ? "" : "s"} ready in ${libraryScopeLabel(scope)}.`,
        details,
        blockClose: false,
      });
      setText(
        "layerStudioEditorStatus",
        [
          summary.failed.length ? "PNG library upload finished with issues." : "PNG library upload complete.",
          `Added: ${summary.added}`,
          `Already existed: ${summary.duplicate}`,
          `Skipped: ${summary.skipped.length}`,
          `Failed: ${summary.failed.length}`,
          `Library: ${libraryForScope(scope)?.path || "-"}`,
        ].join("\n"),
      );
      setText("layerStudioLibraryStatus", `${libraryScopeLabel(scope)}: ${libraryAssets(scope).length || 0} PNG asset(s)`);
      return summary.failed.length === 0;
    } catch (error) {
      const message = error?.message || String(error);
      setText("layerStudioEditorStatus", `PNG library import failed:\n${message}`);
      window.showStatusModal?.({
        type: "error",
        title: "PNG Library Upload",
        subtitle: "Upload failed",
        message,
      });
      app().log?.(message, true);
      return false;
    } finally {
      app().setButtonWorking?.(button, false);
    }
  }

  async function createLibraryGroup() {
    const scope = activeLibraryScope();
    const apiName = scope === "global" ? "create_layer_studio_library_group" : "create_layer_studio_project_library_group";
    if (!window.pywebview?.api?.[apiName]) {
      setText("layerStudioLibraryStatus", "Library groups unavailable");
      return false;
    }
    const name = await window.showInputModal?.({
      type: "info",
      title: "New PNG Group",
      subtitle: `Organize ${libraryScopeLabel(scope)}`,
      message: `Create a new group in ${libraryScopeLabel(scope)}.`,
      label: "Group name",
      value: "New Group",
      confirmLabel: "Create Group",
    });
    const cleanName = cleanText(name);
    if (!cleanName) return false;
    const result = await window.pywebview.api[apiName]({ skinKey: state.context?.skinKey || "", root: app().root?.() || "", name: cleanName });
    app().showResult?.(result, "PNG library group created", apiName);
    if (!result?.ok) {
      setText("layerStudioLibraryStatus", result?.message || "Group creation failed");
      return false;
    }
    setLibraryForScope(scope, result.data?.assetLibrary || libraryForScope(scope) || { assets: [] });
    renderAssetLibrary();
    return true;
  }

  async function updateLibraryGroup(groupId, patch) {
    const id = cleanText(groupId);
    const scope = activeLibraryScope();
    const apiName = scope === "global" ? "update_layer_studio_library_group" : "update_layer_studio_project_library_group";
    if (!id || !window.pywebview?.api?.[apiName]) return false;
    const result = await window.pywebview.api[apiName]({ skinKey: state.context?.skinKey || "", root: app().root?.() || "", groupId: id, patch: patch || {} });
    if (!result?.ok) {
      app().showResult?.(result, "PNG library group update failed", apiName);
      return false;
    }
    setLibraryForScope(scope, result.data?.assetLibrary || libraryForScope(scope) || { assets: [] });
    renderAssetLibrary();
    return true;
  }

  async function renameLibraryGroup(groupId) {
    const group = libraryGroupById(groupId);
    if (!group || group.id === "ungrouped") return false;
    const name = await window.showInputModal?.({
      type: "info",
      title: "Rename PNG Group",
      subtitle: group.name || group.id,
      message: "Rename this PNG library group.",
      label: "Group name",
      value: group.name || "",
      confirmLabel: "Rename",
    });
    const cleanName = cleanText(name);
    if (!cleanName || cleanName === group.name) return false;
    return updateLibraryGroup(group.id, { name: cleanName });
  }

  async function deleteLibraryGroup(groupId) {
    const group = libraryGroupById(groupId);
    const scope = activeLibraryScope();
    const apiName = scope === "global" ? "delete_layer_studio_library_group" : "delete_layer_studio_project_library_group";
    if (!group || group.id === "ungrouped" || !window.pywebview?.api?.[apiName]) return false;
    const assetsInGroup = libraryAssets().filter((asset) => cleanText(asset.groupId, "ungrouped") === group.id).length;
    const confirmed = await window.showConfirmModal?.({
      type: "warning",
      title: "Delete PNG Group",
      subtitle: group.name || group.id,
      message: `Delete this ${libraryScopeLabel(scope)} group? PNG files are kept and moved to Ungrouped.`,
      details: [`Group: ${group.name || group.id}`, `PNGs moved to Ungrouped: ${assetsInGroup}`].join("\n"),
      confirmLabel: "Delete Group",
      cancelLabel: "Cancel",
    });
    if (!confirmed) return false;
    const result = await window.pywebview.api[apiName]({ skinKey: state.context?.skinKey || "", root: app().root?.() || "", groupId: group.id });
    app().showResult?.(result, "PNG library group deleted", apiName);
    if (!result?.ok) return false;
    setLibraryForScope(scope, result.data?.assetLibrary || libraryForScope(scope) || { assets: [] });
    renderAssetLibrary();
    return true;
  }

  async function moveLibraryAsset(assetId, groupId) {
    const id = cleanText(assetId);
    const targetGroupId = cleanText(groupId, "ungrouped");
    const scope = activeLibraryScope();
    const apiName = scope === "global" ? "move_layer_studio_library_asset" : "move_layer_studio_project_library_asset";
    const asset = libraryAssetById(id);
    const previousGroupId = cleanText(asset?.groupId, "ungrouped");
    if (!asset || previousGroupId === targetGroupId) return false;
    if (!window.pywebview?.api?.[apiName]) return false;
    asset.groupId = targetGroupId;
    if (!moveLibraryAssetRow(id, previousGroupId, targetGroupId)) renderAssetLibrary();
    await nextPaintFrame();
    let result = null;
    try {
      result = await window.pywebview.api[apiName]({ skinKey: state.context?.skinKey || "", root: app().root?.() || "", assetId: id, groupId: targetGroupId });
    } catch (error) {
      result = { ok: false, message: error?.message || String(error) };
    }
    if (!result?.ok) {
      const currentAsset = libraryAssetById(id);
      if (currentAsset) currentAsset.groupId = previousGroupId;
      app().showResult?.(result, "PNG library asset move failed", apiName);
      if (!moveLibraryAssetRow(id, targetGroupId, previousGroupId)) renderAssetLibrary();
      return false;
    }
    setLibraryForScope(scope, result.data?.assetLibrary || libraryForScope(scope) || { assets: [] });
    updateLibraryGroupCounts();
    return true;
  }

  async function deleteLibraryAsset(assetId) {
    const id = cleanText(assetId);
    const asset = libraryAssetById(id);
    const scope = activeLibraryScope();
    const apiName = scope === "global" ? "delete_layer_studio_library_asset" : "delete_layer_studio_project_library_asset";
    if (!asset || !window.pywebview?.api?.[apiName]) return false;
    const confirmed = await window.showConfirmModal?.({
      type: "danger",
      title: "Delete PNG from Library",
      subtitle: asset.name || asset.id,
      message: `This permanently deletes the PNG from ${libraryScopeLabel(scope)}.`,
      details: [
        scope === "global" ? "Legacy projects using this global PNG may show the layer as PNG not found." : "This project may show layers using this PNG as PNG not found.",
        "",
        `Asset: ${asset.name || asset.id}`,
        `File: ${asset.file || asset.path || "-"}`,
      ].join("\n"),
      confirmLabel: "Delete PNG",
      cancelLabel: "Cancel",
    });
    if (!confirmed) return false;
    const result = await window.pywebview.api[apiName]({ skinKey: state.context?.skinKey || "", root: app().root?.() || "", assetId: id });
    app().showResult?.(result, "PNG library asset deleted", apiName);
    if (!result?.ok) return false;
    setLibraryForScope(scope, result.data?.assetLibrary || libraryForScope(scope) || { assets: [] });
    state.imageCache.forEach((_value, key) => {
      if (String(key).includes(id)) state.imageCache.delete(key);
    });
    state.libraryThumbnailCache.forEach((_value, key) => {
      if (String(key).includes(id)) state.libraryThumbnailCache.delete(key);
    });
    renderAssetLibrary();
    renderLayerList();
    await rebuildKonvaLayers();
    updateTransformerForSelection();
    setText("layerStudioEditorStatus", "PNG deleted from library.\nProjects using it now show PNG not found until the layer is replaced or removed.");
    return true;
  }

  async function insertLibraryAsset(assetId) {
    if (isSplatMapSurface()) {
      setText("layerStudioEditorStatus", "PNG layers are disabled for EXT_Skin_D. Use mask shapes with the fixed splat map colors.");
      return false;
    }
    const id = cleanText(assetId);
    if (!id) return false;
    if (!state.context?.skinKey) {
      window.showStatusModal?.({
        type: "error",
        title: "Layer Studio",
        subtitle: "No livery loaded",
        message: "Open Layer Studio for an editable local livery before inserting shared PNG assets.",
      });
      return false;
    }
    if (!state.layerDocument?.layers) state.layerDocument = defaultLayerDocument(state.textureSize);
    const scope = activeLibraryScope();
    const apiName = scope === "global" ? "insert_layer_studio_library_asset" : "insert_layer_studio_project_library_asset";
    if (!libraryForScope(scope)) await loadActiveAssetLibrary({ silent: true });
    const asset = libraryAssetById(id);
    if (!asset || asset.exists === false) {
      setText("layerStudioEditorStatus", `Library PNG insert failed:\nPNG asset is missing from ${libraryScopeLabel(scope)}.`);
      return false;
    }
    if (!window.pywebview?.api?.[apiName]) {
      setText("layerStudioEditorStatus", "Library PNG insert failed:\nInsert API unavailable.");
      return false;
    }
    const row = Array.from(document.querySelectorAll("[data-library-asset-id]"))
      .find((item) => item.dataset.libraryAssetId === id);
    const button = row?.querySelector?.("[data-library-action='insert']");
    app().setButtonWorking?.(button, true, "Adding...");
    try {
      const historySnapshot = layerHistorySnapshot(scope === "global" ? "Copy PNG to project" : "Insert PNG layer");
      const result = await window.pywebview.api[apiName]({
        skinKey: state.context.skinKey,
        surfaceKey: state.activeSurfaceKey,
        assetId: id,
        root: app().root?.() || "",
        layerDocument: serializeLayerDocument(),
        placement: currentViewCenterTexturePoint(),
      });
      app().showResult?.(result, scope === "global" ? "PNG copied to Project Assets" : "Project PNG inserted", apiName);
      if (!result?.ok) {
        setText("layerStudioEditorStatus", result?.message || "Library PNG insert failed.");
        return false;
      }
      const data = result.data || {};
      const context = data.layerStudio || state.context || {};
      renderContext(context);
      pushHistorySnapshot(historySnapshot);
      state.layerDocument = normalizeLayerDocument(data.layerDocument || context.layerDocument, state.textureSize);
      const layer = data.layer || {};
      state.selectedLayerId = layer.id;
      state.selectedLayerIds = [layer.id];
      setActiveTool("select");
      if (state.konva?.objectLayer) {
        await rebuildKonvaLayers();
      } else {
        await loadBaseTexture();
      }
      renderLayerList();
      renderPropertiesPanel();
      updateTransformerForSelection();
      setLibraryCollapsed(true);
      setText(
        "layerStudioEditorStatus",
        [
          scope === "global" ? "PNG copied to Project Assets and inserted as editable image layer." : "Project PNG inserted as editable image layer.",
          `Layer: ${layer.name || layer.id || "-"}`,
          `Asset: ${asset.name || asset.id || "-"}`,
          `Editable layers: ${state.layerDocument?.layers?.length || 0}`,
        ].join("\n"),
      );
      return true;
    } catch (error) {
      const message = error?.message || String(error);
      setText("layerStudioEditorStatus", `Library PNG insert failed:\n${message}`);
      app().log?.(message, true);
      return false;
    } finally {
      app().setButtonWorking?.(button, false);
    }
  }

  async function saveLayerStudioProject(options = {}) {
    if (state.saving) {
      if (options.autosave) state.autosavePending = true;
      return false;
    }
    if (!state.context?.skinKey) {
      if (!options.silent) {
        window.showStatusModal?.({
          type: "error",
          title: "Layer Studio",
          subtitle: "No livery loaded",
          message: "Open Layer Studio for an editable local livery before saving the texture layers.",
        });
      }
      return false;
    }
    if (!window.pywebview?.api?.save_layer_studio_project) {
      setText("layerStudioEditorStatus", "Layer Studio layer save is not available in this app build yet.");
      return false;
    }
    const button = $("layerStudioSaveProject");
    state.saving = true;
    state.autosavePending = false;
    if (state.autosaveTimer) {
      window.clearTimeout(state.autosaveTimer);
      state.autosaveTimer = null;
    }
    const saveRevision = state.changeRevision;
    if (options.autosave) updateDirtyIndicator("Saving...");
    if (!options.silent) app().setButtonWorking?.(button, true, "Saving...");
    try {
      const documentModel = serializeLayerDocument();
      const result = await window.pywebview.api.save_layer_studio_project({
        skinKey: state.context.skinKey,
        surfaceKey: state.activeSurfaceKey,
        root: app().root?.() || "",
        layerDocument: documentModel,
      });
      if (!options.silent) app().showResult?.(result, "Layer Studio layers saved", "save_layer_studio_project");
      if (!result?.ok) {
        setText("layerStudioEditorStatus", result?.message || "Layer Studio layer save failed.");
        return false;
      }
      const data = result.data || {};
      const context = data.layerStudio || state.context || {};
      state.context = { ...state.context, ...context };
      state.project = context.previewProject || state.project || {};
      state.activeSurfaceKey = surfaceKey(context.activeSurfaceKey || data.surfaceKey || state.activeSurfaceKey);
      state.pendingSurfaceKey = state.activeSurfaceKey;
      const savedDocument = normalizeLayerDocument(data.layerDocument || context.layerDocument || documentModel, state.textureSize);
      if (state.changeRevision === saveRevision) {
        state.layerDocument = savedDocument;
        setDirty(false);
      } else {
        state.dirty = true;
        scheduleLayerAutosave();
        updateDirtyIndicator("Unsaved changes");
      }
      syncTextureSurfaceControls();
      renderLayerList();
      renderPropertiesPanel();
      renderGuides();
      if (!options.silent) {
        const warnings = Array.isArray(context.warnings) ? context.warnings : [];
        setText(
          "layerStudioEditorStatus",
          [
            "Layer Studio layers saved.",
            `Layer data: ${data.layersPath || context.layersPath || "-"}`,
            `Editable layers: ${state.layerDocument.layers.length}`,
            "Layer changes are saved but not baked into the project PNG yet. Use Update Texture before publishing them to AC EVO.",
            ...(warnings.length ? ["", "Warnings:", ...warnings.map((row) => `- ${row}`)] : []),
          ].join("\n"),
        );
      }
      return true;
    } catch (error) {
      const message = error?.message || String(error);
      setText("layerStudioEditorStatus", `Layer Studio layer save failed:\n${message}`);
      if (options.autosave) updateDirtyIndicator("Autosave failed");
      if (!options.silent) {
        window.showStatusModal?.({
          type: "error",
          title: "Layer Studio",
          subtitle: "Save failed",
          message,
        });
      }
      app().log?.(message, true);
      return false;
    } finally {
      state.saving = false;
      if (state.autosavePending && state.dirty) scheduleLayerAutosave(250);
      if (!options.silent) app().setButtonWorking?.(button, false);
    }
  }

  async function bakePreviewTexture(options = {}) {
    if (state.baking) return false;
    if (!state.context?.skinKey) {
      window.showStatusModal?.({
        type: "error",
        title: "Layer Studio",
        subtitle: "No livery loaded",
        message: "Open Layer Studio for an editable local livery before updating the texture.",
      });
      return false;
    }
    const surface = activeTextureSurface();
    if (surface?.bakeEnabled === false) {
      const label = cleanText(surface.label || surface.fileName || surface.key, "Layer Studio surface");
      const message = `${label} cannot be baked directly. Switch to EXT_SKIN_C to update the livery texture.`;
      setText("layerStudioEditorStatus", message);
      if (!options.silent) {
        window.showStatusModal?.({
          type: "info",
          title: "Layer Studio",
          subtitle: "Texture update disabled",
          message,
        });
      }
      return false;
    }
    if (!window.pywebview?.api?.bake_layer_studio_preview) {
      setText("layerStudioEditorStatus", "Layer Studio bake is not available in this app build yet.");
      return false;
    }
    const button = $("layerStudioBakePreview");
    state.baking = true;
    syncTextureSurfaceControls();
    app().setButtonWorking?.(button, true, "Updating...");
    try {
      setText("layerStudioEditorStatus", "Saving editable Layer Studio project before updating the texture...");
      const saved = await saveLayerStudioProject({ silent: true });
      if (!saved) {
        setText("layerStudioEditorStatus", "Texture update stopped because the editable project could not be saved.");
        return false;
      }
      setText("layerStudioEditorStatus", "Layers saved.\nBaking Layer Studio preview texture...");
      const baked = await bakeLayerStackDataUrl();
      const result = await window.pywebview.api.bake_layer_studio_preview({
        skinKey: state.context.skinKey,
        surfaceKey: state.activeSurfaceKey,
        root: app().root?.() || "",
        dataUrl: baked.dataUrl,
      });
      if (!options.silent) app().showResult?.(result, "Layer Studio preview baked", "bake_layer_studio_preview");
      if (!result?.ok) {
        setText("layerStudioEditorStatus", result?.message || "Layer Studio bake failed.");
        return false;
      }
      const data = result.data || {};
      const context = data.layerStudio || state.context || {};
      state.context = { ...state.context, ...context };
      state.project = { ...(state.project || {}), ...(data.project || {}) };
      state.activeSurfaceKey = surfaceKey(context.activeSurfaceKey || data.surfaceKey || state.activeSurfaceKey);
      state.pendingSurfaceKey = state.activeSurfaceKey;
      const texturePathNext = data.path || state.project.texturePath || state.project.liveTexturePath || texturePath();
      state.context.activeTexture = {
        ...(state.context.activeTexture || {}),
        path: texturePathNext,
        name: String(texturePathNext || "").split(/[\\/]/).pop() || "EXT_SKIN_C.png",
        exists: true,
      };
      setText("layerStudioPreviewStatus", "Reloading 3D preview texture...");
      const previewOptions = {};
      if (state.activeSurfaceKey === "ext_banner_c") {
        previewOptions.bannerTexturePath = texturePathNext;
      } else if (state.activeSurfaceKey === "ext_skin_d") {
        previewOptions.detailTexturePath = texturePathNext;
      } else {
        previewOptions.texturePath = texturePathNext;
      }
      const previewProject = await layerStudioPreviewProject({ ...previewOptions, preserveCameraView: state.previewUvMeshVisible });
      if (state.activeSurfaceKey === "ext_skin_d") {
        previewProject.detailTexturePath = texturePathNext;
        previewProject.liveDetailTexturePath = texturePathNext;
      }
      syncTextureSurfaceControls();
      let reloaded = null;
      if (state.previewUvMeshVisible) {
        reloaded = await app().loadPreview?.({ ...(state.context || {}), project: previewProject });
      } else if (window.LiveryLabPreview3d?.hasLoadedModel?.()) {
        reloaded = await window.LiveryLabPreview3d.reloadTexture(previewProject);
      } else {
        reloaded = await app().loadPreview?.({ project: previewProject });
      }
      const appliedMaterials = Number(reloaded?.appliedMaterials || 0);
      setText(
        "layerStudioEditorStatus",
        [
          "Layer Studio preview baked.",
          `Output: ${texturePathNext || "-"}`,
          `Size: ${baked.width} x ${baked.height}`,
          `Baked layers: ${baked.bakedLayerCount} / ${baked.totalLayerCount}`,
          baked.quantizedPixels ? `Mask palette cleanup: ${baked.quantizedPixels} pixel(s)` : "",
          baked.combinedSurfaceCount > 1 ? `Base Car layers: ${baked.baseBakedLayerCount}` : "",
          data.baseTextureCreated ? `Base snapshot created: ${data.baseTexturePath || "-"}` : `Base texture: ${baseTexturePath() || data.baseTexturePath || "-"}`,
          "Editor overlays excluded: UV mesh, selection handles, viewport.",
          state.previewUvMeshVisible ? "3D overlay: UV Mesh visible in preview only." : "",
        ].filter(Boolean).join("\n"),
      );
      setText(
        "layerStudioPreviewStatus",
        appliedMaterials > 0 || reloaded?.ok
          ? `3D preview texture reloaded.\nApplied materials: ${appliedMaterials || "-"}`
          : "3D preview reload requested.",
      );
      const handleTextureUpdated = app().handleTextureUpdated;
      if (typeof handleTextureUpdated === "function") {
        try {
          await handleTextureUpdated(data);
        } catch (refreshError) {
          app().log?.(refreshError?.message || String(refreshError), true);
        }
      }
      showUpdateTextureFeedback(data);
      return true;
    } catch (error) {
      const message = error?.message || String(error);
      setText("layerStudioEditorStatus", `Layer Studio bake failed:\n${message}`);
      setText("layerStudioPreviewStatus", `Layer Studio bake failed:\n${message}`);
      window.showStatusModal?.({
        type: "error",
        title: "Layer Studio",
        subtitle: "Bake failed",
        message,
      });
      app().log?.(message, true);
      return false;
    } finally {
      state.baking = false;
      syncTextureSurfaceControls();
      app().setButtonWorking?.(button, false);
    }
  }

  function showShortcutModal() {
    const shortcutColumn = (title, rows) => `
      <section class="layer-studio-shortcut-column">
        <strong>${title}</strong>
        ${rows.map(([keys, label]) => `
          <div class="layer-studio-shortcut-row">
            <span>${keys}</span>
            <span>${label}</span>
          </div>
        `).join("")}
      </section>
    `;
    window.showStatusModal?.({
      type: "info",
      title: "Layer Studio Shortcuts",
      subtitle: "Keyboard controls",
      messageHtml: `
        <div class="layer-studio-shortcut-grid">
          ${shortcutColumn("Layer & Selection", [
            ["<code>Delete</code> / <code>Backspace</code>", "Delete selected layer(s)"],
            ["<code>Delete</code> / <code>Backspace</code>", "Delete selected guide when no layer is selected"],
            ["<code>Shift + Click</code>", "Add or remove layers from the selection"],
            ["<code>Ctrl + Click</code>", "Select a child layer inside a group from the 2D canvas"],
            ["<code>Ctrl+D</code>", "Duplicate selected layer(s)"],
            ["<code>Ctrl+G</code>", "Group selected top-level layers"],
            ["<code>Ctrl+Shift+G</code>", "Ungroup selected group"],
          ])}
          ${shortcutColumn("Movement & View", [
            ["<code>Arrow keys</code>", "Nudge selected layer(s) by 1 texture pixel"],
            ["<code>Shift + Arrow keys</code>", "Nudge selected layer(s) by 10 texture pixels"],
            ["<code>Shift + Drag</code>", "Move selected layer(s) horizontally or vertically"],
            ["<code>Alt + Drag transform handle</code>", "Scale selected layer(s) from center"],
            ["<code>Mouse wheel</code>", "Zoom the 2D texture view"],
            ["<code>Space + Left mouse drag</code>", "Pan the 2D canvas"],
            ["<code>Right mouse drag</code>", "Pan the 2D canvas"],
            ["<code>Middle mouse drag</code>", "Roll the 3D preview camera around the current view axis"],
            ["<code>Shift + Middle mouse drag</code>", "Snap 3D camera roll to 45 degree steps"],
          ])}
          ${shortcutColumn("Save & History", [
            ["<code>Ctrl+S</code>", "Save layers"],
            ["<code>Ctrl+B</code>", "Update texture"],
            ["<code>Ctrl+Z</code>", "Undo"],
            ["<code>Ctrl+Y</code> / <code>Ctrl+Shift+Z</code>", "Redo"],
          ])}
          ${shortcutColumn("Polygon & Stripe Drafting", [
            ["<code>Click</code>", "Add the next polygon or stripe point"],
            ["<code>Click + Drag</code>", "Add a point and create Bezier handles while drafting"],
            ["<code>Shift</code>", "Snap the next segment or draft handle to 45 degree steps"],
            ["<code>Click first point</code>", "Close the current polygon draft"],
            ["<code>Enter</code>", "Finish the current polygon or stripe draft"],
            ["<code>Esc</code>", "Cancel the current polygon or stripe draft"],
          ])}
          ${shortcutColumn("Point & Bezier Editing", [
            ["<code>Click point</code>", "Select a polygon or stripe point"],
            ["<code>Drag point</code>", "Move a polygon or stripe point"],
            ["<code>Hover curve + Click</code>", "Add a linear point on the curve"],
            ["<code>Hover curve + Drag</code>", "Add a point and shape Bezier handles directly"],
            ["<code>Drag Bezier handle</code>", "Adjust coupled Bezier handles for the selected point"],
            ["<code>Alt + Drag Bezier handle</code>", "Break coupled Bezier handles while editing a point"],
            ["<code>Shift + Drag point</code>", "Snap edited point angle to 45 degree steps"],
            ["<code>Shift + Drag Bezier handle</code>", "Snap Bezier handle angle to 45 degree steps"],
            ["<code>Delete</code> / <code>Backspace</code>", "Delete the selected point"],
            ["<code>Enter</code> / <code>Esc</code>", "Exit point edit mode"],
          ])}
        </div>
      `,
    });
  }

  async function handlePropertyChange(input) {
    if (state.suppressPropertyEvents || !input) return;
    const layer = findLayer();
    if (!layer) return;
    if ((input.dataset.layerId || "") !== layer.id) return;
    const prop = input.dataset.layerProp;
    const patch = {};
    if (prop === "name") {
      if (isTextLayerType(layer.type)) return;
      patch.name = cleanText(input.value, layer.name || layer.id);
    } else if (prop === "text") {
      if (layer.locked || !isTextLayerType(layer.type)) return;
      patch.text = layerTextValue(input.value, layer.text || "NEW TEXT");
    } else if (prop === "visible") {
      patch.visible = Boolean(input.checked);
    } else if (prop === "locked") {
      patch.locked = Boolean(input.checked);
    } else if (["x", "y", "width", "height"].includes(prop)) {
      if (layer.locked) return;
      if (layer.type === "stripe" && prop === "width") {
        patch.strokeWidth = intValue(input.value, layer.strokeWidth || 20, 1);
      } else {
        const nextValue = prop === "x" || prop === "y"
          ? coordinateValue(input.value, layer[prop] ?? 0)
          : intValue(input.value, layer[prop] ?? 1, 1);
        patch[prop] = nextValue;
        if (layer.ratioLocked !== false && supportsRatioLock(layer) && (prop === "width" || prop === "height")) {
          const currentWidth = intValue(layer.width, 1, 1);
          const currentHeight = intValue(layer.height, 1, 1);
          if (prop === "width" && currentWidth > 0 && currentHeight > 0) {
            patch.height = Math.max(1, Math.round((nextValue / currentWidth) * currentHeight));
          } else if (prop === "height" && currentWidth > 0 && currentHeight > 0) {
            patch.width = Math.max(1, Math.round((nextValue / currentHeight) * currentWidth));
          }
        }
      }
    } else if (prop === "fontFamily") {
      if (layer.locked || !isTextLayerType(layer.type)) return;
      patch.fontFamily = fontPresetByFamily(input.value).family;
    } else if (prop === "fontWeight") {
      if (layer.locked || !isTextLayerType(layer.type)) return;
      patch.fontWeight = fontWeightValue(input.value, layer.fontWeight || 700);
    } else if (prop === "fontSize") {
      if (layer.locked || !isTextLayerType(layer.type)) return;
      patch.fontSize = intValue(input.value, layer.fontSize || 96, 4);
    } else if (prop === "rotation") {
      if (layer.locked) return;
      if (layer.type === "group") {
        const rotated = rotateGroupByDelta(layer.id, numberValue(input.value, 0));
        input.value = "0";
        if (rotated) renderPropertiesPanel();
        return;
      }
      if (isPointLayerType(layer.type)) {
        const rotated = rotatePointLayerByDelta(layer.id, numberValue(input.value, 0));
        input.value = "0";
        if (rotated) renderPropertiesPanel();
        return;
      }
      patch.rotation = numberValue(input.value, layer.rotation || 0);
    } else if (prop === "opacity") {
      if (layer.locked) return;
      patch.opacity = numberValue(input.value, layer.opacity ?? 1, 0, 1);
    } else if (prop === "tension") {
      if (layer.locked || layer.type !== "stripe") return;
      patch.tension = numberValue(input.value, layer.tension ?? 0, 0, 1);
    } else if (prop === "fill") {
      if (layer.locked || (!isPointLayerType(layer.type) && !isShapeLayerType(layer.type) && !isTextLayerType(layer.type))) return;
      if (layer.type === "stripe") {
        patch.stroke = isSplatMapSurface() ? normalizeSplatMapColor(input.value, layer.stroke || activeSplatMapColor()) : cleanText(input.value, layer.stroke || "#f07a00");
        if (isSplatMapSurface()) patch.strokeGradient = null;
      } else {
        patch.fill = isSplatMapSurface() ? normalizeSplatMapColor(input.value, layer.fill || activeSplatMapColor()) : cleanText(input.value, layer.fill || "#e43d30");
        if (isSplatMapSurface()) patch.fillGradient = null;
      }
    } else if (prop === "fillGradientType") {
      if (isSplatMapSurface()) return;
      if (layer.locked || (layer.type !== "polygon" && layer.type !== "stripe" && !isShapeLayerType(layer.type))) return;
      const type = cleanText(input.value, "solid").toLowerCase();
      const gradientKey = layer.type === "stripe" ? "strokeGradient" : "fillGradient";
      const baseColor = layer.type === "stripe" ? layer.stroke || "#f07a00" : layer.fill || "#e43d30";
      if (type === "solid") {
        patch[gradientKey] = null;
        state.gradientPopoverOpen = false;
      } else {
        patch[gradientKey] = normalizeFillGradient({
          ...(fillGradientForLayer(layer) || defaultFillGradient(type)),
          type,
          colorA: fillGradientForLayer(layer)?.colorA || "#000000",
          colorB: fillGradientForLayer(layer)?.colorB || "#ffffff",
        }, baseColor);
        state.gradientPopoverOpen = true;
      }
    } else if (["fillGradientColorA", "fillGradientAlphaA", "fillGradientColorB", "fillGradientAlphaB", "fillGradientAngle"].includes(prop)) {
      if (isSplatMapSurface()) return;
      if (layer.locked || (layer.type !== "polygon" && layer.type !== "stripe" && !isShapeLayerType(layer.type))) return;
      const gradientKey = layer.type === "stripe" ? "strokeGradient" : "fillGradient";
      const baseColor = layer.type === "stripe" ? layer.stroke || "#f07a00" : layer.fill || "#e43d30";
      const gradient = fillGradientForLayer(layer) || normalizeFillGradient(defaultFillGradient("linear"), baseColor);
      if (!gradient) return;
      if (prop === "fillGradientColorA") gradient.colorA = cssColorValue(input.value, gradient.colorA || baseColor);
      if (prop === "fillGradientColorB") gradient.colorB = cssColorValue(input.value, gradient.colorB || baseColor);
      if (prop === "fillGradientAlphaA") gradient.alphaA = numberValue(input.value, gradient.alphaA ?? 1, 0, 1);
      if (prop === "fillGradientAlphaB") gradient.alphaB = numberValue(input.value, gradient.alphaB ?? 0, 0, 1);
      if (prop === "fillGradientAngle") gradient.angle = numberValue(input.value, gradient.angle ?? 90, 0, 360);
      patch[gradientKey] = gradient;
    } else {
      return;
    }
    beginHistory(`Change ${prop}`);
    const wasVisible = layer.visible !== false;
    const updated = patchLayer(layer.id, patch);
    if (!updated) return;
    if (prop === "fill" && !isSplatMapSurface()) {
      state.activeColor = normalizeFavoriteColor(updated.type === "stripe" ? updated.stroke : updated.fill, state.activeColor);
      renderColorFavorites();
    }
    if (updated.type === "group" || isPngMaskGroupLayer(updated) || (prop === "visible" && !wasVisible && updated.visible) || (updated.visible && !layerNode(updated.id))) {
      await rebuildKonvaLayers();
    }
  }

  async function handleLayerListAction(action, layerId, options = {}) {
    const id = cleanText(layerId);
    if (!action || !id) return;
    if (id === BASE_COLOR_LAYER_ID) {
      if (action === "base-color-visible") {
        pushHistory("Toggle base color");
        patchBaseColor({ visible: !baseColorState().visible });
      } else if (action === "base-color-fill") {
        chooseBaseColor(options.anchorEl);
      } else if (action === "select") {
        setSelectedLayer("");
      }
      return;
    }
    if (id === ORIGINAL_BASE_TEXTURE_LAYER_ID) {
      if (action === "select") setSelectedLayer("");
      return;
    }
    if (isUvOverlayLayerId(id)) {
      const config = uvOverlayLayerConfig(id);
      if (action === "visible") {
        state[config.visibleKey] = !state[config.visibleKey];
        renderLayerList();
        await refreshUvOverlay();
        if (state.previewUvMeshVisible) await reloadLayerStudioPreviewTexture({ forceModelReload: true });
      } else if (action === "uv-invert") {
        state[config.invertedKey] = !state[config.invertedKey];
        renderLayerList();
        await refreshUvOverlay();
        if (state.previewUvMeshVisible) await reloadLayerStudioPreviewTexture({ forceModelReload: true });
      }
      return;
    }
    const layer = findLayer(id);
    if (!layer) return;
    if (action === "select") {
      setSelectedLayer(id, { additive: Boolean(options.additive) });
      return;
    }
    if (action === "point-layer-color") {
      chooseLayerSwatchColor(id, options.anchorEl);
      return;
    }
    if (action === "edit") {
      enterPolygonEditMode(id);
      return;
    }
    if (action === "group-toggle" && (layer.type === "group" || isPngMaskGroupLayer(layer))) {
      pushHistory("Toggle group");
      patchLayer(id, { collapsed: !layer.collapsed }, { updateNode: false, renderProperties: false });
      return;
    }
    if (action === "mask") {
      const maskGroup = createPngMaskFromSelection();
      if (maskGroup) {
        await rebuildLayerWorkflow({ keepSelection: true });
        setText("layerStudioEditorStatus", `PNG mask created: ${maskGroup.name || maskGroup.id}`);
      }
      return;
    }
    if (action === "group") {
      await createGroupFromCurrentSelection();
      return;
    }
    if (action === "visible") {
      const wasVisible = layer.visible !== false;
      pushHistory("Toggle visibility");
      patchLayer(id, { visible: !wasVisible });
      if (layer.type === "group" || isPngMaskGroupLayer(layer) || !wasVisible || layerNode(id)) await rebuildLayerWorkflow({ keepSelection: true });
      return;
    }
    if (action === "lock") {
      pushHistory("Toggle lock");
      patchLayer(id, { locked: !layer.locked });
      if (layer.type === "group" || isPngMaskGroupLayer(layer)) await rebuildLayerWorkflow({ keepSelection: true });
      return;
    }
    if (action === "marker-menu") {
      setSelectedLayer(id, { additive: Boolean(options.additive) });
      return;
    }
    if (action === "duplicate") {
      duplicateSelectedLayers(id);
      await rebuildLayerWorkflow({ keepSelection: true });
      return;
    }
    if (action === "delete") {
      if (deleteSelectedLayers(id)) await rebuildLayerWorkflow({ keepSelection: true });
      return;
    }
    if (action === "ungroup") {
      if (ungroupLayer(id)) await rebuildLayerWorkflow({ keepSelection: true });
      return;
    }
    if (action === "unmask") {
      if (unmaskLayer(id)) {
        await rebuildLayerWorkflow({ keepSelection: true });
        setText("layerStudioEditorStatus", `PNG mask removed: ${layer.name || layer.id}`);
      }
      return;
    }
    if (action === "up") {
      if (moveLayer(id, 1)) await rebuildLayerWorkflow({ keepSelection: true });
      return;
    }
    if (action === "down") {
      if (moveLayer(id, -1)) await rebuildLayerWorkflow({ keepSelection: true });
    }
  }

  async function openForSkin(skin) {
    if (state.opening) return false;
    const skinKey = String(skin?.key || skin?.skinKey || "").trim();
    if (!skinKey) {
      window.showStatusModal?.({
        type: "error",
        title: "Layer Studio",
        subtitle: "No skin selected",
        message: "Select a local editable livery before opening Layer Studio.",
      });
      return false;
    }
    state.opening = true;
    state.activeSurfaceKey = "ext_skin_c";
    state.pendingSurfaceKey = "ext_skin_c";
    const button = $("openMySkinLayerStudio");
    app().setButtonWorking?.(button, true, "Opening...");
    try {
      app().log?.("Running prepare_layer_studio_workspace...");
      const result = await window.pywebview.api.prepare_layer_studio_workspace({
        skinKey,
        root: app().root?.() || "",
        surfaceKey: "ext_skin_c",
      });
      app().showResult?.(result, "Layer Studio workspace prepared", "prepare_layer_studio_workspace");
      if (!result?.ok) return false;
      const data = result.data || {};
      const context = data.layerStudio || {};
      renderContext(context);
      showWorkspace();
      await loadColorFavorites();
      await loadBaseTexture();
      const prepared = await app().preparePreview?.(data);
      if (!prepared?.ok) {
        setText("layerStudioPreviewStatus", prepared?.message || "Could not prepare 3D preview.");
        return false;
      }
      const previewProject = await layerStudioPreviewProject({ texturePath: texturePath() });
      const loaded = await app().loadPreview?.({ ...data, project: previewProject });
      window.LiveryLabPreview3d?.setFloorVisible?.(state.previewFloorVisible);
      window.LiveryLabPreview3d?.setSceneMode?.(state.previewScene);
      setText(
        "layerStudioPreviewStatus",
        loaded?.ok
          ? "3D live preview loaded beside the 2D texture workspace."
          : loaded?.message || "3D live preview could not be loaded.",
      );
      app().setInspectOutputText?.([
        result.message || "Layer Studio workspace prepared",
        "",
        `Project folder: ${context.projectFolder || "-"}`,
        `Active texture: ${texturePath(context) || "-"}`,
        `Layer data: ${context.layersPath || "-"}`,
        `3D preview loaded: ${loaded?.ok ? "yes" : "no"}`,
      ].join("\n"));
      return Boolean(loaded?.ok);
    } catch (error) {
      const message = error?.message || String(error);
      setText("layerStudioPreviewStatus", `Layer Studio failed:\n${message}`);
      app().log?.(message, true);
      return false;
    } finally {
      state.opening = false;
      app().setButtonWorking?.(button, false);
    }
  }

  function bindEvents() {
    setLibraryCollapsed(state.libraryCollapsed);
    setWorkspacePanelCollapsed("editor", state.editorCollapsed);
    initLayerStudioPreviewMenuController();
    renderColorFavorites();
    $("layerStudioBack")?.addEventListener("click", () => closeWorkspace());
    updateHistoryButtons();
    $("layerStudioUndo")?.addEventListener("click", () => undoLayerHistory());
    $("layerStudioRedo")?.addEventListener("click", () => redoLayerHistory());
    $("layerStudioEditorCollapse")?.addEventListener("click", () => setWorkspacePanelCollapsed("editor", !state.editorCollapsed));
    $("layerStudioPreviewCollapse")?.addEventListener("click", () => setWorkspacePanelCollapsed("preview", !state.previewCollapsed));
    $("layerStudioTextureSelect")?.addEventListener("change", (event) => {
      state.pendingSurfaceKey = surfaceKey(event.target?.value || state.activeSurfaceKey);
      syncTextureSurfaceControls();
    });
    $("layerStudioLoadTexture")?.addEventListener("click", () => loadSelectedTextureSurface());
    $("layerStudioPreviewShowFloor")?.addEventListener("change", (event) => setLayerStudioPreviewFloorVisible(event.target?.checked !== false));
    $("layerStudioPreviewShowUvMesh")?.addEventListener("change", (event) => setLayerStudioPreviewUvMeshVisible(Boolean(event.target?.checked)));
    document.querySelectorAll("[data-layer-studio-camera-preset]").forEach((button) => {
      button.addEventListener("click", () => setLayerStudioPreviewCameraPreset(button.dataset.layerStudioCameraPreset || "free"));
    });
    document.querySelectorAll("[data-layer-studio-preview-mode]").forEach((button) => {
      button.addEventListener("click", () => setLayerStudioPreviewMode(button.dataset.layerStudioPreviewMode || "textureSlot"));
    });
    document.querySelectorAll("[data-layer-studio-preview-scene]").forEach((button) => {
      button.addEventListener("click", () => setLayerStudioPreviewScene(button.dataset.layerStudioPreviewScene || "light"));
    });
    $("layerStudioPreviewReloadTextureMenu")?.addEventListener("click", () => reloadLayerStudioPreviewTexture());
    $("layerStudioBodyBaseColor")?.addEventListener("input", (event) => setLayerStudioBodyBaseColor(event.target?.value || BODY_BASE_COLOR_DEFAULT, { quiet: true }));
    $("layerStudioBodyBaseColor")?.addEventListener("change", (event) => setLayerStudioBodyBaseColor(event.target?.value || BODY_BASE_COLOR_DEFAULT));
    $("layerStudioSaveBodyBaseColor")?.addEventListener("click", () => saveLayerStudioBodyBaseColor());
    document.querySelectorAll("[data-layer-studio-preview-texture-key]").forEach((button) => {
      button.addEventListener("click", () => selectLayerStudioPreviewTextureSlot(button.dataset.layerStudioPreviewTextureKey || "ext_skin_c"));
    });
    $("layerStudioPreviewPaintFinish")?.addEventListener("click", () => {
      setLayerStudioPaintFinishPanelVisible(true);
      applyLayerStudioPaintFinish();
    });
    sharedMaterialFinish()?.subscribe?.((finish) => syncLayerStudioPaintFinishFromShared(finish));
    $("layerStudioPaintFinishSave")?.addEventListener("click", async () => {
      setLayerStudioPaintFinish(state.previewMaterialFinish || MATERIAL_FINISH_DEFAULT, {
        silent: true,
        source: "layerStudioSavePrepare",
      });
      const result = await app().saveMaterialFinish?.({ buttonId: "layerStudioPaintFinishSave" });
      if (result) {
        setText("layerStudioPreviewStatus", result.gameUpdateRequired
          ? "Paint finish saved. Update in Game is required."
          : "Paint finish saved locally.");
        showMaterialFinishFeedback(result);
      }
    });
    $("layerStudioPaintFinishClose")?.addEventListener("click", () => setLayerStudioPaintFinishPanelVisible(false));
    document.querySelectorAll("[data-layer-studio-finish-field]").forEach((input) => {
      input.addEventListener("input", () => {
        const field = input.dataset.layerStudioFinishField || "";
        if (!field) return;
        setLayerStudioPaintFinish({
          ...(state.previewMaterialFinish || MATERIAL_FINISH_DEFAULT),
          [field]: input.value,
        });
      });
      input.addEventListener("change", () => syncLayerStudioPaintFinishPanel());
    });
    document.querySelectorAll("[data-layer-studio-finish-preset]").forEach((button) => {
      button.addEventListener("click", () => {
        const presetKey = button.dataset.layerStudioFinishPreset || "";
        const preset = sharedMaterialFinish()?.presets?.[presetKey] || MATERIAL_FINISH_PRESETS[presetKey];
        if (preset) setLayerStudioPaintFinish(preset);
      });
    });
    $("layerStudioViewSplitter")?.addEventListener("pointerdown", startEditorPreviewSplitDrag);
    $("layerStudioViewSplitter")?.addEventListener("keydown", (event) => {
      if (event.key !== "ArrowLeft" && event.key !== "ArrowRight") return;
      event.preventDefault();
      const amount = event.shiftKey ? 80 : 24;
      nudgeEditorPreviewSplit(event.key === "ArrowLeft" ? -amount : amount);
    });
    $("layerStudioToolSelect")?.addEventListener("click", () => setActiveTool("select"));
    $("layerStudioToolPolygon")?.addEventListener("click", () => setActiveTool("polygon"));
    $("layerStudioToolStripe")?.addEventListener("click", () => setActiveTool("stripe"));
    $("layerStudioCreateRectangle")?.addEventListener("click", () => createShapeLayer("rectangle"));
    $("layerStudioCreateEllipse")?.addEventListener("click", () => createShapeLayer("ellipse"));
    $("layerStudioCreateText")?.addEventListener("click", () => createTextLayer());
    $("layerStudioColorFavoritesToggle")?.addEventListener("click", async () => {
      if (isSplatMapSurface()) return;
      if (!state.colorFavoritesLoaded) await loadColorFavorites();
      setColorFavoritesOpen(!state.colorFavoritesOpen);
    });
    $("layerStudioColorFavoritesList")?.addEventListener("click", async (event) => {
      const button = event.target?.closest?.("[data-color-favorite-action]");
      if (!button) return;
      event.preventDefault();
      const action = button.dataset.colorFavoriteAction || "";
      const slot = intValue(button.dataset.colorFavoriteSlot, -1, -1);
      if (action === "apply") {
        applyFavoriteColor(button.dataset.colorFavoriteColor || state.colorFavorites[slot]);
        return;
      }
      if (action === "edit") {
        state.colorFavoriteEditingSlot = slot;
        setColorFavoritesStatus("");
        renderColorFavorites();
        window.requestAnimationFrame(() => document.querySelector(`[data-color-favorite-hex="${slot}"]`)?.focus?.());
        return;
      }
      if (action === "cancel") {
        state.colorFavoriteEditingSlot = -1;
        setColorFavoritesStatus("");
        renderColorFavorites();
        return;
      }
      if (action === "delete") {
        await deleteColorFavoriteSlot(slot);
        return;
      }
      if (action === "save") {
        const input = document.querySelector(`[data-color-favorite-hex="${slot}"]`);
        await saveColorFavoriteSlot(slot, input?.value || "");
      }
    });
    $("layerStudioColorFavoritesList")?.addEventListener("input", (event) => {
      const picker = event.target?.closest?.("[data-color-favorite-picker]");
      if (!picker) return;
      const slot = intValue(picker.dataset.colorFavoritePicker, -1, -1);
      const input = document.querySelector(`[data-color-favorite-hex="${slot}"]`);
      if (input) input.value = normalizeFavoriteColor(picker.value, picker.value);
    });
    $("layerStudioColorFavoritesList")?.addEventListener("keydown", async (event) => {
      const input = event.target?.closest?.("[data-color-favorite-hex]");
      if (!input) return;
      const slot = intValue(input.dataset.colorFavoriteHex, -1, -1);
      if (event.key === "Enter") {
        event.preventDefault();
        await saveColorFavoriteSlot(slot, input.value);
      } else if (event.key === "Escape") {
        event.preventDefault();
        if (state.colorFavorites[slot]) {
          state.colorFavoriteEditingSlot = -1;
          renderColorFavorites();
        } else {
          setColorFavoritesOpen(false);
        }
      }
    });
    $("layerStudioImportPng")?.addEventListener("click", async () => {
      if (isSplatMapSurface()) {
        setText("layerStudioEditorStatus", "PNG layers are disabled for EXT_Skin_D. Use mask shapes with the fixed splat map colors.");
        return;
      }
      const nextCollapsed = !state.libraryCollapsed;
      setLibraryCollapsed(nextCollapsed);
      if (!nextCollapsed) {
        if (!libraryForScope()) await loadActiveAssetLibrary({ silent: true });
        setText("layerStudioEditorStatus", `PNG Library opened.\nUsing ${libraryScopeLabel()}.`);
      } else {
        setText("layerStudioEditorStatus", "PNG Library closed.");
      }
    });
    $("layerStudioCreateGroup")?.addEventListener("click", () => createGroupFromCurrentSelection());
    $("layerStudioUngroupSelected")?.addEventListener("click", () => ungroupCurrentSelection());
    $("layerStudioPropRatioLock")?.addEventListener("click", () => {
      const layer = primarySelectedLayer();
      if (!supportsRatioLock(layer) || layer.locked) return;
      beginHistory("Toggle ratio lock");
      patchLayer(layer.id, { ratioLocked: layer.ratioLocked === false }, { renderProperties: false });
      renderPropertiesPanel();
      updateTransformerForSelection();
      commitHistory();
    });
    $("layerStudioGradientToggle")?.addEventListener("click", (event) => {
      event.preventDefault();
      const layer = primarySelectedLayer();
      if (!layer || layer.locked || (layer.type !== "polygon" && layer.type !== "stripe" && !isShapeLayerType(layer.type)) || !fillGradientForLayer(layer)) return;
      state.gradientPopoverOpen = !state.gradientPopoverOpen;
      renderPropertiesPanel();
    });
    $("layerStudioLibraryToggle")?.addEventListener("click", async () => {
      setLibraryCollapsed(!state.libraryCollapsed);
      if (!state.libraryCollapsed && !libraryForScope()) await loadActiveAssetLibrary({ silent: true });
    });
    $("layerStudioLibraryAddPng")?.addEventListener("click", async () => {
      setLibraryCollapsed(false);
      await addPngToLibrary();
    });
    $("layerStudioLibraryAddGroup")?.addEventListener("click", async () => {
      setLibraryCollapsed(false);
      await createLibraryGroup();
    });
    $("layerStudioLibraryRefresh")?.addEventListener("click", () => loadActiveAssetLibrary());
    document.querySelectorAll("[data-library-scope]").forEach((button) => {
      button.addEventListener("click", () => setLibraryScope(button.dataset.libraryScope || "project"));
    });
    $("layerStudioLibrarySearch")?.addEventListener("input", (event) => {
      state.librarySearchQuery = cleanText(event.target?.value);
      renderAssetLibrary();
    });
    $("layerStudioLibraryList")?.addEventListener("click", async (event) => {
      const groupAction = event.target?.closest?.("[data-library-group-action]");
      if (groupAction) {
        event.preventDefault();
        const groupRow = groupAction.closest("[data-library-group-id]");
        const groupId = groupRow?.dataset.libraryGroupId || "";
        const actionName = groupAction.dataset.libraryGroupAction || "";
        if (actionName === "toggle") {
          const group = libraryGroupById(groupId);
          await updateLibraryGroup(groupId, { collapsed: !(group?.collapsed === true) });
        } else if (actionName === "rename") {
          await renameLibraryGroup(groupId);
        } else if (actionName === "delete") {
          await deleteLibraryGroup(groupId);
        }
        return;
      }
      const action = event.target?.closest?.("[data-library-action]");
      if (!action) return;
      event.preventDefault();
      const row = action.closest("[data-library-asset-id]");
      const assetId = row?.dataset.libraryAssetId || "";
      if (action.dataset.libraryAction === "delete") {
        await deleteLibraryAsset(assetId);
        return;
      }
      if (action.dataset.libraryAction === "insert") {
        await insertLibraryAsset(assetId);
      }
    });
    $("layerStudioLibraryList")?.addEventListener("pointerdown", (event) => {
      const handle = event.target?.closest?.("[data-library-drag-handle]");
      if (!handle || event.button !== 0 || event.target?.closest?.("[data-library-action]")) return;
      const row = handle.closest("[data-library-asset-id]");
      const assetId = cleanText(row?.dataset.libraryAssetId);
      if (!row || !assetId || !libraryAssetById(assetId)) return;
      event.preventDefault();
      event.stopPropagation();
      handle.setPointerCapture?.(event.pointerId);
      if (state.libraryPointerDrag) clearLibraryPointerDrag();
      state.libraryPointerDrag = {
        pointerId: event.pointerId,
        sourceId: assetId,
        startX: event.clientX,
        startY: event.clientY,
        hasMoved: false,
      };
      state.draggedLibraryAssetId = assetId;
      state.dragOverLibraryGroupId = "";
      window.addEventListener("pointermove", handleLibraryPointerDragMove, true);
      window.addEventListener("pointerup", handleLibraryPointerDragEnd, true);
      window.addEventListener("pointercancel", handleLibraryPointerDragCancel, true);
      window.addEventListener("keydown", handleLibraryPointerDragKeydown, true);
      updateLibraryDragClasses();
    });
    $("layerStudioSaveProject")?.addEventListener("click", () => saveLayerStudioProject());
    $("layerStudioBakePreview")?.addEventListener("click", () => bakePreviewTexture());
    $("layerStudioReloadTexture")?.addEventListener("click", () => loadBaseTexture());
    $("layerStudioShortcuts")?.addEventListener("click", () => showShortcutModal());
    $("layerStudioAddVerticalGuide")?.addEventListener("click", () => addGuide("x"));
    $("layerStudioAddHorizontalGuide")?.addEventListener("click", () => addGuide("y"));
    $("layerStudioToggleGuides")?.addEventListener("click", () => setGuidesVisible(state.layerDocument?.guidesVisible === false));
    $("layerStudioToggleSnap")?.addEventListener("click", () => setSnapEnabled(state.layerDocument?.snapEnabled === false));
    $("layerStudioClearGuides")?.addEventListener("click", () => clearGuides());
    $("layerStudioFlipHorizontal")?.addEventListener("click", () => flipSelectedLayers("horizontal"));
    $("layerStudioFlipVertical")?.addEventListener("click", () => flipSelectedLayers("vertical"));
    document.querySelectorAll("[data-layer-align]").forEach((button) => {
      button.addEventListener("click", () => alignSelectedLayers(button.dataset.layerAlign || ""));
    });
    $("layerStudioLayerList")?.addEventListener("mouseover", (event) => {
      const row = event.target?.closest?.("[data-layer-id]");
      if (!row || !$("layerStudioLayerList")?.contains(row)) return;
      setHoveredLayer(row.dataset.layerId || "");
    });
    $("layerStudioLayerList")?.addEventListener("mouseout", (event) => {
      const list = $("layerStudioLayerList");
      const row = event.target?.closest?.("[data-layer-id]");
      const nextRow = event.relatedTarget?.closest?.("[data-layer-id]");
      if (!row || (nextRow && row === nextRow)) return;
      if (list && nextRow && list.contains(nextRow)) {
        setHoveredLayer(nextRow.dataset.layerId || "");
        return;
      }
      setHoveredLayer("");
    });
    $("layerStudioLayerList")?.addEventListener("click", (event) => {
      if (event.target?.closest?.("[data-layer-rename-input]")) return;
      const renameTarget = event.target?.closest?.("[data-layer-rename]");
      if (renameTarget && event.detail >= 2) {
        event.preventDefault();
        event.stopPropagation();
        startInlineRename(renameTarget.dataset.layerRename || "");
        return;
      }
      if (event.target?.closest?.("[data-layer-drag-handle]")) return;
      const actionButton = event.target?.closest?.("[data-layer-action]");
      if (actionButton) {
        event.preventDefault();
        event.stopPropagation();
        const row = actionButton.closest("[data-layer-id]");
        handleLayerListAction(actionButton.dataset.layerAction || "", row?.dataset.layerId || "", { additive: Boolean(event.shiftKey), anchorEl: actionButton });
        return;
      }
      const row = event.target?.closest?.("[data-layer-id]");
      if (row) handleLayerListAction("select", row.dataset.layerId || "", { additive: Boolean(event.shiftKey) });
    });
    $("layerStudioLayerList")?.addEventListener("dblclick", (event) => {
      const name = event.target?.closest?.("[data-layer-rename]");
      if (name) {
        event.preventDefault();
        event.stopPropagation();
        startInlineRename(name.dataset.layerRename || "");
        return;
      }
      const row = event.target?.closest?.("[data-layer-id]");
      const layer = findLayer(row?.dataset.layerId || "");
      if (!layer || !isPointLayerType(layer.type)) return;
      event.preventDefault();
      event.stopPropagation();
      enterPolygonEditMode(layer.id);
    });
    $("layerStudioLayerList")?.addEventListener("focusout", (event) => {
      const input = event.target?.closest?.("[data-layer-rename-input]");
      if (!input) return;
      commitInlineRename(input.value);
    });
    $("layerStudioLayerList")?.addEventListener("keydown", (event) => {
      const input = event.target?.closest?.("[data-layer-rename-input]");
      if (!input) return;
      if (event.key === "Enter") {
        event.preventDefault();
        event.stopImmediatePropagation();
        commitInlineRename(input.value);
      } else if (event.key === "Escape") {
        event.preventDefault();
        event.stopImmediatePropagation();
        cancelInlineRename();
      } else {
        event.stopImmediatePropagation();
      }
    });
    $("layerStudioLayerList")?.addEventListener("pointerdown", (event) => {
      const handle = event.target?.closest?.("[data-layer-drag-handle]");
      if (!handle || event.button !== 0 || event.target?.closest?.("[data-layer-action], [data-layer-rename-input]")) return;
      const row = handle.closest("[data-layer-id]");
      const layerId = cleanText(row?.dataset.layerId);
      if (!row || !isLayerPointerReorderCandidate(layerId)) return;
      event.preventDefault();
      event.stopPropagation();
      handle.setPointerCapture?.(event.pointerId);
      if (state.layerPointerDrag) clearLayerPointerDrag();
      state.layerPointerDrag = {
        pointerId: event.pointerId,
        sourceId: layerId,
        startX: event.clientX,
        startY: event.clientY,
        hasMoved: false,
      };
      state.draggedLayerId = layerId;
      state.dragOverLayerId = "";
      window.addEventListener("pointermove", handleLayerPointerDragMove, true);
      window.addEventListener("pointerup", handleLayerPointerDragEnd, true);
      window.addEventListener("pointercancel", handleLayerPointerDragCancel, true);
      window.addEventListener("keydown", handleLayerPointerDragKeydown, true);
      updateLayerDragClasses();
    });
    $("layerStudioLayerList")?.addEventListener("keydown", (event) => {
      if (event.target?.closest?.("[data-layer-rename-input]")) return;
      if (event.key !== "Enter" && event.key !== " ") return;
      const row = event.target?.closest?.("[data-layer-id]");
      if (!row || event.target?.closest?.("[data-layer-action]")) return;
      event.preventDefault();
      handleLayerListAction("select", row.dataset.layerId || "", { additive: Boolean(event.shiftKey) });
    });
    $("layerStudioPropertiesForm")?.addEventListener("submit", (event) => event.preventDefault());
    $("layerStudioPropertiesForm")?.addEventListener("click", (event) => {
      const button = event.target?.closest?.("[data-splat-color]");
      if (!button) return;
      event.preventDefault();
      applySplatMapPaletteColor(button.dataset.splatColor || "");
    });
    $("layerStudioPropertiesForm")?.addEventListener("focusin", (event) => {
      const input = event.target?.closest?.("[data-layer-prop]");
      if (input && !state.suppressPropertyEvents) beginHistory(`Change ${input.dataset.layerProp || "property"}`);
    });
    $("layerStudioPropertiesForm")?.addEventListener("focusout", () => {
      commitHistory();
    });
    $("layerStudioPropertiesForm")?.addEventListener("input", (event) => {
      const input = event.target?.closest?.("[data-layer-prop]");
      if (input && input.type !== "checkbox" && input.type !== "number") handlePropertyChange(input);
    });
    $("layerStudioPropertiesForm")?.addEventListener("change", (event) => {
      const input = event.target?.closest?.("[data-layer-prop]");
      if (input) handlePropertyChange(input);
    });
    $("layerStudioPropertiesForm")?.addEventListener("keydown", (event) => {
      const input = event.target?.closest?.("[data-layer-prop]");
      if (!input || input.type !== "number" || event.key !== "Enter") return;
      event.preventDefault();
      handlePropertyChange(input);
      input.blur();
    });
    document.addEventListener("pointerdown", (event) => {
      if (state.colorFavoritesOpen && !event.target?.closest?.("#layerStudioColorFavoritesDock")) {
        setColorFavoritesOpen(false);
      }
      if (!state.gradientPopoverOpen) return;
      const control = $("layerStudioGradientControl");
      if (control?.contains?.(event.target)) return;
      state.gradientPopoverOpen = false;
      renderPropertiesPanel();
    });
    $("layerStudioOpenProjectFolder")?.addEventListener("click", async () => {
      const result = await app().openProjectFolder?.(state.context?.projectFolder || "");
      app().showResult?.(result || { ok: false, message: "Project folder could not be opened." }, "Project folder opened", "open_folder");
    });
    document.addEventListener("keydown", async (event) => {
      if ($("layerStudioWorkspace")?.hidden) return;
      const target = event.target;
      const isTyping = target?.matches?.("input, textarea, select, [contenteditable='true']");
      if (isTyping) return;
      const key = String(event.key || "").toLowerCase();
      if (event.code === "Space" || event.key === " " || key === "spacebar") {
        event.preventDefault();
        state.spacePanActive = true;
        const container = state.konva?.stage?.container?.();
        if (container && !state.canvasPan) container.style.cursor = "grab";
        return;
      }
      if (event.ctrlKey && !event.altKey && key === "z") {
        event.preventDefault();
        if (event.shiftKey) {
          await redoLayerHistory();
        } else {
          await undoLayerHistory();
        }
        return;
      }
      if (event.ctrlKey && !event.altKey && key === "y") {
        event.preventDefault();
        await redoLayerHistory();
        return;
      }
      if (event.ctrlKey && !event.altKey && key === "s") {
        event.preventDefault();
        await saveLayerStudioProject();
        return;
      }
      if (event.ctrlKey && !event.altKey && key === "b") {
        event.preventDefault();
        await bakePreviewTexture();
        return;
      }
      if (event.ctrlKey && !event.altKey && key === "d") {
        event.preventDefault();
        if (!isDraftTool() && !state.polygonEditLayerId && duplicateSelectedLayers().length) {
          await rebuildLayerWorkflow({ keepSelection: true });
        }
        return;
      }
      if (event.ctrlKey && !event.altKey && key === "g") {
        event.preventDefault();
        if (event.shiftKey) {
          await ungroupCurrentSelection();
        } else {
          await createGroupFromCurrentSelection();
        }
        return;
      }
      if (event.key === "Shift") {
        setTransformerRotationSnap(true);
        if (isDraftTool()) {
          state.polygonDraftSnap = true;
          const pointer = pointerTexturePosition();
          if (state.stripeDraftDrag) {
            updateStripeDraftBezier(pointer, true, Boolean(event.altKey));
          } else {
            renderPolygonDraft(pointer);
          }
          return;
        }
      }
      if (state.polygonEditLayerId) {
        if (event.key === "Escape" || event.key === "Enter") {
          event.preventDefault();
          exitPolygonEditMode();
          return;
        }
        if (event.key === "Delete" || event.key === "Backspace") {
          event.preventDefault();
          deleteSelectedPolygonPoint();
          return;
        }
      }
      if (isDraftTool()) {
        if (event.key === "Escape") {
          event.preventDefault();
          cancelPolygonDraft();
          setActiveTool("select");
          return;
        }
        if (event.key === "Enter") {
          event.preventDefault();
          await finishDraftLayer();
          return;
        }
      }
      if (event.key === "Escape" && selectedLayerIds().length) {
        event.preventDefault();
        setSelectedLayer("");
        return;
      }
      if ((event.key === "Delete" || event.key === "Backspace") && selectedLayerIds().length) {
        event.preventDefault();
        if (deleteSelectedLayers()) await rebuildLayerWorkflow({ keepSelection: true });
        return;
      }
      if ((event.key === "Delete" || event.key === "Backspace") && state.selectedGuideId) {
        event.preventDefault();
        removeSelectedGuide();
        return;
      }
      const nudgeDirections = {
        ArrowLeft: [-1, 0],
        ArrowRight: [1, 0],
        ArrowUp: [0, -1],
        ArrowDown: [0, 1],
      };
      const nudge = nudgeDirections[event.key];
      if (nudge && selectedLayerIds().length && !isDraftTool() && !event.ctrlKey && !event.altKey && !event.metaKey) {
        event.preventDefault();
        const step = event.shiftKey ? 10 : 1;
        nudgeSelectedLayers(nudge[0] * step, nudge[1] * step);
        return;
      }
      if (!isDraftTool()) return;
    });
    document.addEventListener("keyup", (event) => {
      if (event.code === "Space" || event.key === " " || String(event.key || "").toLowerCase() === "spacebar") {
        state.spacePanActive = false;
        if (state.canvasPan) {
          state.canvasPan = null;
          updateStageInteractivity();
        }
        const container = state.konva?.stage?.container?.();
        if (container) container.style.cursor = usesPrecisionCursor() ? "crosshair" : "grab";
        return;
      }
      if (event.key !== "Shift") return;
      setTransformerRotationSnap(false);
      if (!isDraftTool()) return;
      state.polygonDraftSnap = false;
      const pointer = pointerTexturePosition();
      if (state.stripeDraftDrag) {
        updateStripeDraftBezier(pointer, false, Boolean(event.altKey));
      } else {
        renderPolygonDraft(pointer);
      }
    });
  }

  window.addEventListener("resize", () => refreshEditorPreviewSplitLayout());

  if (document.readyState === "loading") {
    document.addEventListener("DOMContentLoaded", bindEvents, { once: true });
  } else {
    bindEvents();
  }

  window.LiveryLabLayerStudio = {
    openForSkin,
    close: closeWorkspace,
    reloadTexture: loadBaseTexture,
    importPng: importPngLayer,
    saveProject: saveLayerStudioProject,
    bakePreview: bakePreviewTexture,
    bakeLayerStackDataUrl,
    serializeLayerDocument,
    normalizeLayerDocument,
    layerTransformerScaleBehavior,
    normalizeNodeTransform,
    syncColorFavoritesAvailability,
    rebuildLayers: rebuildKonvaLayers,
    state,
  };
}());
