/* 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);
}