Worldshaper/src/mapEditorPopup/graphicsDocumentHelpers.ts

146 lines
5.5 KiB
TypeScript
Raw Normal View History

2026-06-26 18:18:14 -04:00
import {
getSpriteRows,
normalizeImageRecordForSave,
normalizeImagesPayloadForSave,
normalizeTileRecordForSave,
type JsonObject,
type JsonValue,
} from "../editorCore";
export type GraphicRole = "tile" | "sprite" | "other";
export function normalizeGraphicRoles(value: unknown): string[] {
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 hydrateImageRecordRows(entry: JsonObject | null | undefined): JsonObject | null {
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
return null;
}
return {
...entry,
rows: getSpriteRows(entry),
};
}
export function getImageRecordFromPayload(
imagesPayload: JsonObject | null | undefined,
recordId: string,
): JsonObject | null {
const normalizedId = String(recordId || "").trim();
if (!normalizedId) {
return null;
}
const records = Array.isArray(imagesPayload?.images) ? imagesPayload.images : [];
const matchedEntry = (records.find((entry) => (
entry
&& typeof entry === "object"
&& !Array.isArray(entry)
&& String((entry as JsonObject).id || "").trim() === normalizedId
)) as JsonObject | undefined) || null;
return hydrateImageRecordRows(matchedEntry);
}
export function buildImageRecordFromTileRecord(
record: JsonObject,
existingRecord?: JsonObject | null,
cloneValue: <T>(value: T) => T = structuredClone,
): JsonObject {
const existing = existingRecord && typeof existingRecord === "object" && !Array.isArray(existingRecord)
? existingRecord
: {};
const existingRoles = normalizeGraphicRoles(existing.roles);
return normalizeImageRecordForSave({
...cloneValue(existing),
id: String(record?.id || existing.id || "").trim(),
name: String(record?.name || existing.name || "").trim(),
description: String(record?.description || existing.description || "").trim(),
width: Math.max(1, Number(record?.width) || Number(existing.width) || 16),
height: Math.max(1, Number(record?.height) || Number(existing.height) || 16),
pixelScale: Math.max(1, Number(record?.pixelScale) || Number(existing.pixelScale) || 1),
opacity: Math.max(0, Math.min(1, Number(record?.opacity ?? existing.opacity ?? 1))),
rows: Array.isArray(record?.rows)
? record.rows.map((row) => String(row || ""))
: getSpriteRows(existing),
tags: cloneValue(record?.tags) || cloneValue(existing.tags) || [],
roles: Array.from(new Set([...existingRoles, "tile"])),
tileSymbol: String(record?.tileSymbol || record?.symbol || existing.tileSymbol || "").trim().charAt(0),
});
}
export function buildImageRecordFromSpriteRecord(
record: JsonObject,
graphicRole: GraphicRole,
existingRecord?: JsonObject | null,
cloneValue: <T>(value: T) => T = structuredClone,
): JsonObject {
const existing = existingRecord && typeof existingRecord === "object" && !Array.isArray(existingRecord)
? existingRecord
: {};
const existingRoles = normalizeGraphicRoles(existing.roles);
const wantsSpriteRole = graphicRole !== "other";
const nextRoles = wantsSpriteRole
? Array.from(new Set([...existingRoles, "sprite"]))
: existingRoles.filter((entry) => entry !== "sprite");
return normalizeImageRecordForSave({
...cloneValue(existing),
id: String(record?.id || existing.id || "").trim(),
name: String(record?.name || existing.name || "").trim(),
description: String(record?.description || existing.description || "").trim(),
width: Math.max(1, Number(record?.width) || Number(existing.width) || 16),
height: Math.max(1, Number(record?.height) || Number(existing.height) || 16),
pixelScale: Math.max(1, Number(record?.pixelScale) || Number(existing.pixelScale) || 1),
opacity: Math.max(0, Math.min(1, Number(record?.opacity ?? existing.opacity ?? 1))),
rows: Array.isArray(record?.rows)
? record.rows.map((row) => String(row || ""))
: getSpriteRows(existing),
tags: cloneValue(record?.tags) || cloneValue(existing.tags) || [],
roles: nextRoles,
tileSymbol: String(existing.tileSymbol || record?.tileSymbol || record?.symbol || "").trim().charAt(0),
});
}
export function normalizeImagesPayloadSnapshot(
payload: JsonObject | null | undefined,
cloneValue: <T>(value: T) => T = structuredClone,
): JsonObject {
const normalized = (normalizeImagesPayloadForSave(cloneValue(payload || { schemaVersion: 1, images: [] }) as JsonValue) as JsonObject) || {
schemaVersion: 1,
images: [],
};
const records = Array.isArray(normalized.images) ? normalized.images : [];
return {
...normalized,
images: records.map((entry) => {
const hydratedEntry = hydrateImageRecordRows(
entry && typeof entry === "object" && !Array.isArray(entry)
? entry as JsonObject
: null,
);
return hydratedEntry || entry;
}),
};
}
export function buildTileRecordFromImageRecord(entry: JsonObject, cloneValue: <T>(value: T) => T = structuredClone): JsonObject {
return normalizeTileRecordForSave({
id: String(entry.id || "").trim(),
symbol: String(entry.tileSymbol || entry.symbol || "").trim().charAt(0),
name: String(entry.name || "").trim(),
description: String(entry.description || "").trim(),
width: Number(entry.width) || 16,
height: Number(entry.height) || 16,
pixelScale: Number(entry.pixelScale) || 1,
opacity: Number(entry.opacity ?? 1),
rows: getSpriteRows(entry),
tags: cloneValue(entry.tags) || [],
});
}