/* eslint-disable @typescript-eslint/ban-ts-comment */ // @ts-nocheck import { buildSpritePreviewDataUrl, getSpritePalette, normalizeImagePlayback, } from "../editorCore"; import { normalizeEditorTags } from "./tagUtils"; export const TILE_ART_SIZE = 16; export const EYEDROPPER_CURSOR = `url("data:image/svg+xml,${encodeURIComponent( ` `, )}") 4 28, crosshair`; export function cloneValue(value) { if (typeof structuredClone === "function") { return structuredClone(value); } return value == null ? value : JSON.parse(JSON.stringify(value)); } function normalizeRoleList(value) { if (!Array.isArray(value)) { return []; } return Array.from(new Set( value .map((entry) => String(entry || "").trim().toLowerCase()) .filter((entry) => entry === "tile" || entry === "sprite"), )); } export function normalizeTimelineRows(rows) { return Array.from({ length: TILE_ART_SIZE }, (_entry, rowIndex) => { const row = Array.isArray(rows) ? String(rows[rowIndex] || "") : ""; return row.padEnd(TILE_ART_SIZE, ".").slice(0, TILE_ART_SIZE); }); } export function normalizeWorkingFrames(record) { const rawFrames = Array.isArray(record?.frames) ? record.frames.filter((entry) => entry && typeof entry === "object" && !Array.isArray(entry)) : []; const normalizedFrames = rawFrames.map((entry, index) => ({ ...cloneValue(entry), id: String(entry.id || `frame_${index}`).trim() || `frame_${index}`, enabled: entry.enabled !== false, index: Number.isFinite(Number(entry.index)) ? Math.max(0, Math.floor(Number(entry.index))) : index, rows: normalizeTimelineRows(entry.rows), })); if (normalizedFrames.length > 0) { return normalizedFrames; } return [{ id: "frame_0", enabled: true, index: 0, rows: normalizeTimelineRows(record?.rows), }]; } export function sortWorkingFrames(frames) { return frames .map((frame, sourceIndex) => ({ frame, sourceIndex, sortIndex: Number.isFinite(Number(frame?.index)) ? Number(frame.index) : sourceIndex, })) .sort((left, right) => ( left.sortIndex !== right.sortIndex ? left.sortIndex - right.sortIndex : left.sourceIndex - right.sourceIndex )) .map((entry) => entry.frame); } export function normalizeWorkingGraphicRecord(recordType, record) { const source = cloneValue(record) || {}; const roles = normalizeRoleList(source.roles); const nextRoles = recordType === "tile" ? Array.from(new Set([...roles, "tile"])) : ( recordType === "sprite" ? Array.from(new Set([...roles, "sprite"])) : roles.filter((entry) => entry !== "sprite") ); const frames = normalizeWorkingFrames(source).map((frame, index) => ({ ...frame, index, })); const requestedDefaultFrameId = String(source.defaultFrame || "").trim(); const defaultFrameId = String( frames.find((frame) => String(frame.id || "").trim() === requestedDefaultFrameId)?.id || frames[0]?.id || "frame_0", ).trim() || "frame_0"; const workingRows = normalizeTimelineRows( Array.isArray(source.rows) && source.rows.length > 0 ? source.rows : (frames.find((frame) => String(frame.id || "").trim() === defaultFrameId)?.rows || frames[0]?.rows || []) ); return { ...source, id: String(source.id || `${recordType === "tile" ? "tile" : "sprite"}_${Date.now()}`).trim(), name: typeof source.name === "string" ? source.name : "", description: typeof source.description === "string" ? source.description : "", width: TILE_ART_SIZE, height: TILE_ART_SIZE, pixelScale: Math.max(1, Number(source.pixelScale) || 2), opacity: Number.isFinite(Number(source.opacity)) ? Math.max(0, Math.min(1, Number(source.opacity))) : 1, tags: normalizeEditorTags(source.tags), roles: nextRoles, tileSymbol: nextRoles.includes("tile") ? (String(source.tileSymbol ?? source.symbol ?? source.id ?? "T").trim().charAt(0) || "T") : "", defaultFrame: defaultFrameId, speed: Number.isFinite(Number(source.speed)) && Number(source.speed) >= 0 ? Number(source.speed) : 0, playback: normalizeImagePlayback(source.playback), frames, rows: workingRows, }; } export function normalizeOpacityValue(value, fallback = 1) { const parsed = Number(value); if (!Number.isFinite(parsed)) { return fallback; } return Math.max(0, Math.min(1, parsed)); } export function formatOpacityValue(value) { const normalized = normalizeOpacityValue(value, 1); return normalized.toFixed(2).replace(/\.?0+$/, ""); } export function cloneRows(rows) { return Array.isArray(rows) ? rows.map((row) => String(row || "").padEnd(TILE_ART_SIZE, ".").slice(0, TILE_ART_SIZE)) : Array.from({ length: TILE_ART_SIZE }, () => ".".repeat(TILE_ART_SIZE)); } export function buildRowsPreviewRecord(rows) { return { width: TILE_ART_SIZE, height: TILE_ART_SIZE, rows: cloneRows(rows), }; } export function formatPlaybackLabel(value) { const normalized = normalizeImagePlayback(value); if (normalized === "rewind") { return "Rewind"; } if (normalized === "stop") { return "Stop"; } return "Normal"; } export function getWorkingCellSymbol(record, x, y) { const rows = Array.isArray(record?.rows) ? record.rows : []; const row = String(rows[y] || "").padEnd(TILE_ART_SIZE, ".").slice(0, TILE_ART_SIZE); return String(row.charAt(x) || ".").charAt(0) || "."; } export function paintWorkingRowsCell(rows, x, y, symbol) { const nextRows = cloneRows(rows); const nextSymbol = String(symbol || ".").charAt(0) || "."; const targetRow = String(nextRows[y] || "").padEnd(TILE_ART_SIZE, ".").slice(0, TILE_ART_SIZE); nextRows[y] = `${targetRow.slice(0, x)}${nextSymbol}${targetRow.slice(x + 1)}`; return nextRows; } export function getRowsMatrix(rows) { return cloneRows(rows).map((row) => Array.from(row)); } export function buildRowsFromMatrix(matrix) { return Array.from({ length: TILE_ART_SIZE }, (_entry, rowIndex) => { const sourceRow = Array.isArray(matrix?.[rowIndex]) ? matrix[rowIndex] : []; return sourceRow.join("").padEnd(TILE_ART_SIZE, ".").slice(0, TILE_ART_SIZE); }); } export function getAlternatePaintSymbol(record, preferredSymbol) { const normalizedPreferred = String(preferredSymbol || "").charAt(0) || "."; const palette = getSpritePalette(record || undefined); const nextSymbol = Object.keys(palette) .map((symbol) => String(symbol || "").charAt(0)) .find((symbol) => symbol && symbol !== normalizedPreferred && symbol !== "."); return nextSymbol || "."; } export function shiftRows(rows, offsetX, offsetY) { const nextRows = Array.from({ length: TILE_ART_SIZE }, () => ".".repeat(TILE_ART_SIZE).split("")); const sourceRows = cloneRows(rows); for (let y = 0; y < TILE_ART_SIZE; y += 1) { const row = sourceRows[y] || ".".repeat(TILE_ART_SIZE); for (let x = 0; x < TILE_ART_SIZE; x += 1) { const nextX = x + offsetX; const nextY = y + offsetY; if (nextX < 0 || nextX >= TILE_ART_SIZE || nextY < 0 || nextY >= TILE_ART_SIZE) { continue; } nextRows[nextY][nextX] = String(row.charAt(x) || ".").charAt(0) || "."; } } return buildRowsFromMatrix(nextRows); } export function flipRowsHorizontally(rows) { return cloneRows(rows).map((row) => row.split("").reverse().join("")); } export function flipRowsVertically(rows) { return cloneRows(rows).slice().reverse(); } export function rotateRowsClockwise(rows) { const matrix = getRowsMatrix(rows); const nextMatrix = Array.from({ length: TILE_ART_SIZE }, () => Array.from({ length: TILE_ART_SIZE }, () => ".")); for (let y = 0; y < TILE_ART_SIZE; y += 1) { for (let x = 0; x < TILE_ART_SIZE; x += 1) { nextMatrix[x][TILE_ART_SIZE - 1 - y] = String(matrix[y]?.[x] || ".").charAt(0) || "."; } } return buildRowsFromMatrix(nextMatrix); } export function rotateRowsCounterClockwise(rows) { const matrix = getRowsMatrix(rows); const nextMatrix = Array.from({ length: TILE_ART_SIZE }, () => Array.from({ length: TILE_ART_SIZE }, () => ".")); for (let y = 0; y < TILE_ART_SIZE; y += 1) { for (let x = 0; x < TILE_ART_SIZE; x += 1) { nextMatrix[TILE_ART_SIZE - 1 - x][y] = String(matrix[y]?.[x] || ".").charAt(0) || "."; } } return buildRowsFromMatrix(nextMatrix); } export function buildShapeFillMask(shapeKind, startX, startY, endX, endY) { const minX = Math.max(0, Math.min(startX, endX)); const maxX = Math.min(TILE_ART_SIZE - 1, Math.max(startX, endX)); const minY = Math.max(0, Math.min(startY, endY)); const maxY = Math.min(TILE_ART_SIZE - 1, Math.max(startY, endY)); const fillMask = new Set(); const shape = shapeKind === "circle" || shapeKind === "triangle" ? shapeKind : "rectangle"; const width = Math.max(1, (maxX - minX) + 1); const height = Math.max(1, (maxY - minY) + 1); const centerX = minX + (width / 2); const centerY = minY + (height / 2); const denomX = Math.max(0.5, width / 2); const denomY = Math.max(0.5, height / 2); const triangleAx = minX + (width - 1) / 2; const triangleAy = minY; const triangleBx = minX; const triangleBy = maxY; const triangleCx = maxX; const triangleCy = maxY; const triangleDenominator = ((triangleBy - triangleCy) * (triangleAx - triangleCx)) + ((triangleCx - triangleBx) * (triangleAy - triangleCy)); for (let y = minY; y <= maxY; y += 1) { for (let x = minX; x <= maxX; x += 1) { let include; const sampleX = x + 0.5; const sampleY = y + 0.5; if (shape === "rectangle") { include = true; } else if (shape === "circle") { const normX = (sampleX - centerX) / denomX; const normY = (sampleY - centerY) / denomY; include = (normX * normX) + (normY * normY) <= 1; } else if (triangleDenominator !== 0) { const a = (((triangleBy - triangleCy) * (sampleX - triangleCx)) + ((triangleCx - triangleBx) * (sampleY - triangleCy))) / triangleDenominator; const b = (((triangleCy - triangleAy) * (sampleX - triangleCx)) + ((triangleAx - triangleCx) * (sampleY - triangleCy))) / triangleDenominator; const c = 1 - a - b; include = a >= 0 && b >= 0 && c >= 0; } else { include = x === Math.round(triangleAx) && y >= minY && y <= maxY; } if (include === true) { fillMask.add(`${x}:${y}`); } } } return fillMask; } export function buildOutlineMask(fillMask) { const outlineMask = new Set(); fillMask.forEach((key) => { const [xText, yText] = String(key || "").split(":"); const x = Number(xText); const y = Number(yText); const neighbors = [ `${x - 1}:${y}`, `${x + 1}:${y}`, `${x}:${y - 1}`, `${x}:${y + 1}`, ]; if (neighbors.some((neighbor) => !fillMask.has(neighbor))) { outlineMask.add(key); } }); return outlineMask; } export function applyMaskToRows(baseRows, mask, symbol) { const matrix = getRowsMatrix(baseRows); mask.forEach((key) => { const [xText, yText] = String(key || "").split(":"); const x = Number(xText); const y = Number(yText); if (x < 0 || x >= TILE_ART_SIZE || y < 0 || y >= TILE_ART_SIZE) { return; } matrix[y][x] = String(symbol || ".").charAt(0) || "."; }); return buildRowsFromMatrix(matrix); } export function getLineRows(baseRows, startX, startY, endX, endY, symbol) { const normalizedSymbol = String(symbol || ".").charAt(0) || "."; const matrix = getRowsMatrix(baseRows); let x0 = Math.max(0, Math.min(TILE_ART_SIZE - 1, Number(startX) || 0)); let y0 = Math.max(0, Math.min(TILE_ART_SIZE - 1, Number(startY) || 0)); const x1 = Math.max(0, Math.min(TILE_ART_SIZE - 1, Number(endX) || 0)); const y1 = Math.max(0, Math.min(TILE_ART_SIZE - 1, Number(endY) || 0)); const deltaX = Math.abs(x1 - x0); const deltaY = Math.abs(y1 - y0); const stepX = x0 < x1 ? 1 : -1; const stepY = y0 < y1 ? 1 : -1; let error = deltaX - deltaY; while (true) { matrix[y0][x0] = normalizedSymbol; if (x0 === x1 && y0 === y1) { break; } const nextError = error * 2; if (nextError > -deltaY) { error -= deltaY; x0 += stepX; } if (nextError < deltaX) { error += deltaX; y0 += stepY; } } return buildRowsFromMatrix(matrix); } export function buildShapeOptionIconMarkup(shapeKind, variant, tone = "draw") { const normalizedShape = shapeKind === "circle" || shapeKind === "triangle" ? shapeKind : "rectangle"; const normalizedVariant = variant === "outline" || variant === "two-tone" ? variant : "fill"; const normalizedTone = tone === "erase" ? "erase" : "draw"; return "" + `"; } export function buildLineOptionIconMarkup(tone = "draw") { const normalizedTone = tone === "erase" ? "erase" : "draw"; return "" + `"; } export function buildCurrentShapeToolIconMarkup(state) { if (state?.activeTool === "line" || String(state?.activeShapeMenuId || "").trim() === "line") { return buildLineOptionIconMarkup("draw"); } return buildShapeOptionIconMarkup( state?.activeShapeKind || "rectangle", state?.activeShapeVariant || "outline", "draw", ); } export function buildCurrentEraseToolIconMarkup(state) { return buildShapeOptionIconMarkup( state?.activeEraseKind || "rectangle", "fill", "erase", ); } export function buildTransformCategoryIconMarkup(kind) { const normalizedKind = kind === "flip" ? "flip" : "rotate"; return "" + `"; } export function buildTransformOptionIconMarkup(kind) { const normalizedKind = [ "rotate-cw", "rotate-ccw", "flip-h", "flip-v", ].includes(String(kind || "").trim()) ? String(kind || "").trim() : "rotate-cw"; return "" + `"; } export function buildFramePreviewDataUrl(rows, scale = 10) { return buildSpritePreviewDataUrl(buildRowsPreviewRecord(rows), scale); }