// @ts-nocheck export function createFilledRows(width, height, fillChar) { return Array.from({ length: Math.max(1, Number(height) || 1) }, () => String(fillChar || " ").repeat(Math.max(1, Number(width) || 1))); } function writeRowSegment(rows, y, x, segment) { if (!Array.isArray(rows) || !segment) { return; } const targetY = Math.floor(Number(y) || 0); if (targetY < 0 || targetY >= rows.length) { return; } const safeX = Math.max(0, Math.floor(Number(x) || 0)); const sourceRow = String(rows[targetY] || ""); const paddedRow = sourceRow.length >= safeX ? sourceRow : (sourceRow + " ".repeat(Math.max(0, safeX - sourceRow.length))); const before = paddedRow.slice(0, safeX); const afterStart = safeX + segment.length; const after = afterStart < paddedRow.length ? paddedRow.slice(afterStart) : ""; rows[targetY] = before + segment + after; } export function composeWorldRoomLayers(chunks, chunkWidth, chunkHeight, originChunkX, originChunkY, worldWidth, worldHeight) { const layerMap = new Map(); (Array.isArray(chunks) ? chunks : []).forEach((chunk) => { const baseChunkX = Math.floor(Number(chunk?.chunkX) || 0); const baseChunkY = Math.floor(Number(chunk?.chunkY) || 0); const offsetX = (baseChunkX - originChunkX) * chunkWidth; const offsetY = (baseChunkY - originChunkY) * chunkHeight; const rawLayers = Array.isArray(chunk?.roomLayers) ? chunk.roomLayers : []; rawLayers.forEach((rawLayer) => { const layerNumber = Number(rawLayer?.layer) || 0; const fillChar = layerNumber === 0 ? "." : " "; if (!layerMap.has(layerNumber)) { layerMap.set(layerNumber, { layer: layerNumber, name: typeof rawLayer?.name === "string" && String(rawLayer.name).trim() ? String(rawLayer.name).trim() : undefined, rows: createFilledRows(worldWidth, worldHeight, fillChar), instanceIds: [], }); } const targetLayer = layerMap.get(layerNumber); const sourceRows = Array.isArray(rawLayer?.rows) ? rawLayer.rows.map((row) => String(row || "")) : []; sourceRows.forEach((row, localY) => { const targetY = offsetY + localY; if (targetY < 0 || targetY >= targetLayer.rows.length) { return; } const maxWidth = Math.max(0, worldWidth - offsetX); writeRowSegment(targetLayer.rows, targetY, offsetX, row.slice(0, maxWidth)); }); const sourceInstanceIds = Array.isArray(rawLayer?.instanceIds) ? rawLayer.instanceIds.map((entry) => String(entry || "").trim()).filter(Boolean) : []; targetLayer.instanceIds = Array.from(new Set([...(targetLayer.instanceIds || []), ...sourceInstanceIds])); }); }); if (!layerMap.has(0)) { layerMap.set(0, { layer: 0, rows: createFilledRows(worldWidth, worldHeight, "."), instanceIds: [], }); } return Array.from(layerMap.values()).sort((a, b) => (Number(a.layer) || 0) - (Number(b.layer) || 0)); } export function composeWorldHeightLayers(chunks, chunkWidth, chunkHeight, originChunkX, originChunkY) { const patches = []; (Array.isArray(chunks) ? chunks : []).forEach((chunk) => { const baseChunkX = Math.floor(Number(chunk?.chunkX) || 0); const baseChunkY = Math.floor(Number(chunk?.chunkY) || 0); const offsetX = (baseChunkX - originChunkX) * chunkWidth; const offsetY = (baseChunkY - originChunkY) * chunkHeight; const rawHeightLayers = Array.isArray(chunk?.heightLayers) ? chunk.heightLayers : []; rawHeightLayers.forEach((entry, index) => { const fallbackId = `height_${baseChunkX}_${baseChunkY}_${index + 1}`; patches.push({ id: String(entry?.id || fallbackId).trim() || fallbackId, name: typeof entry?.name === "string" && String(entry.name).trim() ? String(entry.name).trim() : undefined, z: Math.max(1, Math.floor(Number(entry?.z) || 1)), x: offsetX + Math.max(0, Number(entry?.x) || 0), y: offsetY + Math.max(0, Number(entry?.y) || 0), rows: Array.isArray(entry?.rows) ? entry.rows.map((row) => String(row || "")) : [], }); }); }); return patches.sort((a, b) => { if (a.z !== b.z) { return a.z - b.z; } return String(a.name || a.id).localeCompare(String(b.name || b.id)); }); } export function buildWorldLayerMetadata(chunks) { const layerMap = new Map(); (Array.isArray(chunks) ? chunks : []).forEach((chunk) => { const rawLayers = Array.isArray(chunk?.roomLayers) ? chunk.roomLayers : []; rawLayers.forEach((rawLayer) => { const layerNumber = Number(rawLayer?.layer) || 0; if (layerMap.has(layerNumber)) { return; } layerMap.set(layerNumber, { layer: layerNumber, name: typeof rawLayer?.name === "string" && String(rawLayer.name).trim() ? String(rawLayer.name).trim() : undefined, rows: [], instanceIds: Array.isArray(rawLayer?.instanceIds) ? rawLayer.instanceIds.map((entry) => String(entry || "").trim()).filter(Boolean) : [], }); }); }); if (!layerMap.has(0)) { layerMap.set(0, { layer: 0, rows: [], instanceIds: [], }); } if (!Array.from(layerMap.keys()).some((layerNumber) => layerNumber > 0)) { layerMap.set(1, { layer: 1, rows: [], instanceIds: [], }); } return Array.from(layerMap.values()).sort((a, b) => (Number(a.layer) || 0) - (Number(b.layer) || 0)); } export function sliceNormalizedRows(rows, startX, startY, width, height, fillChar) { return Array.from({ length: Math.max(1, Number(height) || 1) }, (_, rowOffset) => { const sourceRow = String((Array.isArray(rows) ? rows[startY + rowOffset] : "") || ""); const paddedRow = sourceRow.length >= startX + width ? sourceRow : sourceRow + String(fillChar || " ").repeat(Math.max(0, (startX + width) - sourceRow.length)); return paddedRow.slice(startX, startX + width); }); } export function buildChunkHeightLayersFromDocument({ mapDocument, cloneHeightLayers, baseTileX, baseTileY, chunkWidth, chunkHeight }) { return (Array.isArray(mapDocument.heightLayers) ? cloneHeightLayers(mapDocument.heightLayers) : []) .map((entry) => { const patchX = Math.max(0, Number(entry?.x) || 0); const patchY = Math.max(0, Number(entry?.y) || 0); const rows = Array.isArray(entry?.rows) ? entry.rows.map((row) => String(row || "")) : []; const patchWidth = rows.reduce((max, row) => Math.max(max, row.length), 0); const patchHeight = rows.length; const patchRight = patchX + patchWidth; const patchBottom = patchY + patchHeight; const chunkRight = baseTileX + chunkWidth; const chunkBottom = baseTileY + chunkHeight; const overlapLeft = Math.max(baseTileX, patchX); const overlapTop = Math.max(baseTileY, patchY); const overlapRight = Math.min(chunkRight, patchRight); const overlapBottom = Math.min(chunkBottom, patchBottom); if (overlapRight <= overlapLeft || overlapBottom <= overlapTop) { return null; } const localRows = []; for (let y = overlapTop; y < overlapBottom; y += 1) { const sourceRow = String(rows[y - patchY] || ""); localRows.push(sourceRow.slice(overlapLeft - patchX, overlapRight - patchX).replace(/\s+$/g, "")); } return { id: String(entry?.id || "").trim(), name: typeof entry?.name === "string" && entry.name.trim() ? entry.name.trim() : undefined, z: Math.max(1, Number(entry?.z) || 1), x: overlapLeft - baseTileX, y: overlapTop - baseTileY, rows: localRows, }; }) .filter((entry) => entry && entry.id); } export function buildChunkInstancesFromDocument({ mapDocument, cloneValue, baseTileX, baseTileY, chunkWidth, chunkHeight, tileOffsetX, tileOffsetY }) { const chunkInstances = cloneValue(mapDocument.npcOverlays) .filter((npc) => { const localX = Math.floor(Number(npc?.x)); const localY = Math.floor(Number(npc?.y)); return Number.isFinite(localX) && Number.isFinite(localY) && localX >= baseTileX && localX < baseTileX + chunkWidth && localY >= baseTileY && localY < baseTileY + chunkHeight; }) .map((npc) => ({ id: String(npc.id || "").trim(), templateId: String(npc?.record?.templateId || "").trim(), layer: Number(npc.layer) || 0, x: Math.floor(Number(npc.x) || 0) - baseTileX, y: Math.floor(Number(npc.y) || 0) - baseTileY, record: { ...cloneValue(npc.record || {}), id: String(npc.id || "").trim(), layer: Number(npc.layer) || 0, templateId: String(npc?.record?.templateId || "").trim(), name: String(npc.name || npc?.record?.name || ""), entityType: String(npc?.record?.entityType || npc?.entityType || "friendly"), faction: String(npc.faction || npc?.record?.faction || ""), spriteId: String(npc.spriteId || npc?.record?.spriteId || ""), dialogueId: String(npc.dialogueId || npc?.record?.dialogueId || ""), description: String(npc.description || npc?.record?.description || ""), tags: cloneValue(npc?.record?.tags) || [], enabled: typeof npc?.record?.enabled === "boolean" ? npc.record.enabled : true, position: { x: Math.floor(Number(npc.x) || 0) + tileOffsetX, y: Math.floor(Number(npc.y) || 0) + tileOffsetY, }, }, })) .filter((entry) => entry.id); const npcIdsByLayer = new Map(); chunkInstances.forEach((entry) => { const layerNumber = Number(entry.layer) || 0; if (!npcIdsByLayer.has(layerNumber)) { npcIdsByLayer.set(layerNumber, []); } npcIdsByLayer.get(layerNumber).push(entry.id); }); return { chunkInstances, npcIdsByLayer, }; } export function normalizeWorldChunkRows(rows, width, height, fillChar) { const safeWidth = Math.max(1, Math.floor(Number(width) || 1)); const safeHeight = Math.max(1, Math.floor(Number(height) || 1)); return Array.from({ length: safeHeight }, (_entry, rowIndex) => { const sourceRow = String((Array.isArray(rows) ? rows[rowIndex] : "") || ""); return sourceRow.length >= safeWidth ? sourceRow.slice(0, safeWidth) : (sourceRow + String(fillChar || " ").repeat(Math.max(0, safeWidth - sourceRow.length))); }); } export function cloneWorldChunkHeightLayers(source) { return (Array.isArray(source) ? source : []) .map((entry, index) => ({ id: String(entry?.id || `height_patch_${index + 1}`).trim() || `height_patch_${index + 1}`, name: typeof entry?.name === "string" && entry.name.trim() ? entry.name.trim() : undefined, z: Math.max(1, Math.floor(Number(entry?.z) || 1)), x: Math.max(0, Math.floor(Number(entry?.x) || 0)), y: Math.max(0, Math.floor(Number(entry?.y) || 0)), rows: Array.isArray(entry?.rows) ? entry.rows.map((row) => String(row || "")) : [], })) .filter((entry) => entry.id); } export function buildWorldChunkLayerInstanceIds(roomLayers, instances, width, height) { const safeWidth = Math.max(1, Math.floor(Number(width) || 1)); const safeHeight = Math.max(1, Math.floor(Number(height) || 1)); const nextLayers = new Map(); (Array.isArray(roomLayers) ? roomLayers : []).forEach((layer) => { const layerNumber = Math.max(0, Math.floor(Number(layer?.layer) || 0)); nextLayers.set(layerNumber, { layer: layerNumber, name: typeof layer?.name === "string" && layer.name.trim() ? layer.name.trim() : undefined, rows: normalizeWorldChunkRows(layer?.rows, safeWidth, safeHeight, layerNumber === 0 ? "." : " "), instanceIds: [], }); }); if (!nextLayers.has(0)) { nextLayers.set(0, { layer: 0, rows: normalizeWorldChunkRows([], safeWidth, safeHeight, "."), instanceIds: [], }); } if (!Array.from(nextLayers.keys()).some((layerNumber) => layerNumber > 0)) { nextLayers.set(1, { layer: 1, rows: normalizeWorldChunkRows([], safeWidth, safeHeight, " "), instanceIds: [], }); } (Array.isArray(instances) ? instances : []).forEach((entry) => { const layerNumber = Math.max(0, Math.floor(Number(entry?.layer) || 0)); const instanceId = String(entry?.id || "").trim(); if (!instanceId) { return; } if (!nextLayers.has(layerNumber)) { nextLayers.set(layerNumber, { layer: layerNumber, rows: normalizeWorldChunkRows([], safeWidth, safeHeight, layerNumber === 0 ? "." : " "), instanceIds: [], }); } nextLayers.get(layerNumber).instanceIds.push(instanceId); }); return Array.from(nextLayers.values()) .map((entry) => ({ ...entry, instanceIds: Array.from(new Set((Array.isArray(entry.instanceIds) ? entry.instanceIds : []).map((id) => String(id || "").trim()).filter(Boolean))), })) .sort((left, right) => (Number(left.layer) || 0) - (Number(right.layer) || 0)); } export function normalizeWorldChunkInstances({ sourceInstances, chunkX, chunkY, width, height, options, cloneValue, runtimeUniqueId }) { const config = options && typeof options === "object" ? options : {}; const duplicateIds = config.duplicateIds === true; const safeChunkX = Math.floor(Number(chunkX) || 0); const safeChunkY = Math.floor(Number(chunkY) || 0); const safeWidth = Math.max(1, Math.floor(Number(width) || 1)); const safeHeight = Math.max(1, Math.floor(Number(height) || 1)); return (Array.isArray(sourceInstances) ? sourceInstances : []) .map((entry) => { const record = entry?.record && typeof entry.record === "object" && !Array.isArray(entry.record) ? cloneValue(entry.record) : {}; const nextId = duplicateIds ? runtimeUniqueId() : (String(entry?.id || record?.id || runtimeUniqueId()).trim() || runtimeUniqueId()); const nextLayer = Math.max(0, Math.floor(Number(entry?.layer ?? record?.layer) || 0)); const nextX = Math.max(0, Math.min(safeWidth - 1, Math.floor(Number(entry?.x) || 0))); const nextY = Math.max(0, Math.min(safeHeight - 1, Math.floor(Number(entry?.y) || 0))); const nextTemplateId = String(entry?.templateId || record?.templateId || "").trim(); record.id = nextId; record.layer = nextLayer; record.templateId = nextTemplateId; record.position = { x: (safeChunkX * safeWidth) + nextX, y: (safeChunkY * safeHeight) + nextY, }; return { id: nextId, templateId: nextTemplateId, layer: nextLayer, x: nextX, y: nextY, record, }; }) .filter((entry) => entry.id); } export function createEmptyWorldChunkPayload({ chunkX, chunkY, chunkWidth, chunkHeight, worldId }) { const safeChunkX = Math.floor(Number(chunkX) || 0); const safeChunkY = Math.floor(Number(chunkY) || 0); return { schemaVersion: 1, worldId: String(worldId || "").trim(), chunkX: safeChunkX, chunkY: safeChunkY, width: chunkWidth, height: chunkHeight, backgroundTileId: "", roomLayers: [ { layer: 0, rows: Array.from({ length: chunkHeight }, () => ".".repeat(chunkWidth)), instanceIds: [], }, { layer: 1, rows: Array.from({ length: chunkHeight }, () => " ".repeat(chunkWidth)), instanceIds: [], }, ], heightLayers: [], instances: [], }; } export function normalizeCachedWorldChunkPayload({ chunkPayload, chunkX, chunkY, chunkWidth, chunkHeight, worldId, cloneValue, runtimeUniqueId, options }) { const safeChunkX = Math.floor(Number(chunkX ?? chunkPayload?.chunkX) || 0); const safeChunkY = Math.floor(Number(chunkY ?? chunkPayload?.chunkY) || 0); const safeWidth = Math.max(1, Math.floor(Number(chunkPayload?.width) || Number(chunkWidth) || 32)); const safeHeight = Math.max(1, Math.floor(Number(chunkPayload?.height) || Number(chunkHeight) || 32)); const instances = normalizeWorldChunkInstances({ sourceInstances: chunkPayload?.instances, chunkX: safeChunkX, chunkY: safeChunkY, width: safeWidth, height: safeHeight, options, cloneValue, runtimeUniqueId, }); const roomLayers = buildWorldChunkLayerInstanceIds(chunkPayload?.roomLayers, instances, safeWidth, safeHeight); return { schemaVersion: Math.max(1, Math.floor(Number(chunkPayload?.schemaVersion) || 1)), worldId: String(chunkPayload?.worldId || worldId || "").trim(), chunkX: safeChunkX, chunkY: safeChunkY, width: safeWidth, height: safeHeight, backgroundTileId: String(chunkPayload?.backgroundTileId || "").trim(), roomLayers, heightLayers: cloneWorldChunkHeightLayers(chunkPayload?.heightLayers), instances, }; } export function isChunkFillSymbol(ch, fillChar) { const symbol = String(ch || "").charAt(0); return !symbol || symbol === fillChar || symbol === "." || symbol === " "; } export function isWorldChunkPayloadEmpty({ chunkPayload, chunkWidth, chunkHeight, worldId, cloneValue, runtimeUniqueId }) { const normalized = normalizeCachedWorldChunkPayload({ chunkPayload, chunkX: chunkPayload?.chunkX, chunkY: chunkPayload?.chunkY, chunkWidth, chunkHeight, worldId, cloneValue, runtimeUniqueId, }); if (String(normalized?.backgroundTileId || "").trim()) { return false; } if (Array.isArray(normalized?.instances) && normalized.instances.length > 0) { return false; } if ((Array.isArray(normalized?.heightLayers) ? normalized.heightLayers : []).some((entry) => ( Array.isArray(entry?.rows) && entry.rows.some((row) => /[^ .]/.test(String(row || ""))) ))) { return false; } return !(Array.isArray(normalized?.roomLayers) ? normalized.roomLayers : []).some((layer) => { const fillChar = (Number(layer?.layer) || 0) === 0 ? "." : " "; return (Array.isArray(layer?.rows) ? layer.rows : []).some((row) => { const sourceRow = String(row || ""); for (let index = 0; index < sourceRow.length; index += 1) { if (!isChunkFillSymbol(sourceRow.charAt(index), fillChar)) { return true; } } return false; }); }); } export function transformChunkLocalCoord(localX, localY, width, height, operation) { const safeX = Math.floor(Number(localX) || 0); const safeY = Math.floor(Number(localY) || 0); const safeWidth = Math.max(1, Math.floor(Number(width) || 1)); const safeHeight = Math.max(1, Math.floor(Number(height) || 1)); switch (String(operation || "").trim()) { case "flipHorizontal": return { x: (safeWidth - 1) - safeX, y: safeY }; case "flipVertical": return { x: safeX, y: (safeHeight - 1) - safeY }; case "rotate180": return { x: (safeWidth - 1) - safeX, y: (safeHeight - 1) - safeY }; case "rotate90cw": if (safeWidth !== safeHeight) { return null; } return { x: (safeWidth - 1) - safeY, y: safeX }; case "rotate90ccw": if (safeWidth !== safeHeight) { return null; } return { x: safeY, y: (safeHeight - 1) - safeX }; default: return { x: safeX, y: safeY }; } } export function transformChunkRows(rows, width, height, fillChar, operation) { const safeWidth = Math.max(1, Math.floor(Number(width) || 1)); const safeHeight = Math.max(1, Math.floor(Number(height) || 1)); const sourceRows = normalizeWorldChunkRows(rows, safeWidth, safeHeight, fillChar); const nextRows = Array.from({ length: safeHeight }, () => Array.from({ length: safeWidth }, () => String(fillChar || " ").charAt(0) || " ")); for (let rowIndex = 0; rowIndex < safeHeight; rowIndex += 1) { const sourceRow = sourceRows[rowIndex]; for (let columnIndex = 0; columnIndex < safeWidth; columnIndex += 1) { const char = String(sourceRow.charAt(columnIndex) || fillChar).charAt(0) || String(fillChar || " ").charAt(0) || " "; if (isChunkFillSymbol(char, fillChar)) { continue; } const nextCoord = transformChunkLocalCoord(columnIndex, rowIndex, safeWidth, safeHeight, operation); if (!nextCoord) { continue; } nextRows[nextCoord.y][nextCoord.x] = char; } } return nextRows.map((row) => row.join("")); } export function transformChunkHeightPatch(patch, width, height, operation) { const safeWidth = Math.max(1, Math.floor(Number(width) || 1)); const safeHeight = Math.max(1, Math.floor(Number(height) || 1)); const sourceRows = Array.isArray(patch?.rows) ? patch.rows.map((row) => String(row || "")) : []; const patchWidth = sourceRows.reduce((max, row) => Math.max(max, row.length), 0); const patchHeight = sourceRows.length; const transformedCells = []; for (let localY = 0; localY < patchHeight; localY += 1) { const row = sourceRows[localY] || ""; for (let localX = 0; localX < patchWidth; localX += 1) { const char = String(row.charAt(localX) || " ").charAt(0) || " "; if (char === " " || char === ".") { continue; } const worldX = Math.max(0, Math.floor(Number(patch?.x) || 0)) + localX; const worldY = Math.max(0, Math.floor(Number(patch?.y) || 0)) + localY; if (worldX < 0 || worldY < 0 || worldX >= safeWidth || worldY >= safeHeight) { continue; } const nextCoord = transformChunkLocalCoord(worldX, worldY, safeWidth, safeHeight, operation); if (!nextCoord) { continue; } transformedCells.push({ x: nextCoord.x, y: nextCoord.y, char, }); } } if (transformedCells.length <= 0) { return null; } const minX = transformedCells.reduce((min, entry) => Math.min(min, entry.x), transformedCells[0].x); const maxX = transformedCells.reduce((max, entry) => Math.max(max, entry.x), transformedCells[0].x); const minY = transformedCells.reduce((min, entry) => Math.min(min, entry.y), transformedCells[0].y); const maxY = transformedCells.reduce((max, entry) => Math.max(max, entry.y), transformedCells[0].y); const nextRows = Array.from({ length: (maxY - minY) + 1 }, () => Array.from({ length: (maxX - minX) + 1 }, () => " ")); transformedCells.forEach((entry) => { nextRows[entry.y - minY][entry.x - minX] = entry.char; }); return { id: String(patch?.id || "").trim(), name: typeof patch?.name === "string" && patch.name.trim() ? patch.name.trim() : undefined, z: Math.max(1, Math.floor(Number(patch?.z) || 1)), x: minX, y: minY, rows: nextRows.map((row) => row.join("").replace(/\s+$/g, "")), }; } export function transformWorldChunkPayload({ chunkPayload, operation, chunkWidth, chunkHeight, worldId, cloneValue, runtimeUniqueId, options }) { const config = options && typeof options === "object" ? options : {}; const normalized = normalizeCachedWorldChunkPayload({ chunkPayload, chunkX: chunkPayload?.chunkX, chunkY: chunkPayload?.chunkY, chunkWidth, chunkHeight, worldId, cloneValue, runtimeUniqueId, options: config, }); const safeWidth = Math.max(1, Math.floor(Number(normalized?.width) || 1)); const safeHeight = Math.max(1, Math.floor(Number(normalized?.height) || 1)); const normalizedOperation = String(operation || "").trim(); if ((normalizedOperation === "rotate90cw" || normalizedOperation === "rotate90ccw") && safeWidth !== safeHeight) { throw new Error("Chunk rotation requires square chunks."); } const instances = normalizeWorldChunkInstances({ sourceInstances: (Array.isArray(normalized.instances) ? normalized.instances : []).map((entry) => { const nextCoord = transformChunkLocalCoord(entry.x, entry.y, safeWidth, safeHeight, normalizedOperation); return { ...cloneValue(entry), x: nextCoord?.x ?? entry.x, y: nextCoord?.y ?? entry.y, }; }), chunkX: normalized.chunkX, chunkY: normalized.chunkY, width: safeWidth, height: safeHeight, options: config, cloneValue, runtimeUniqueId, }); const roomLayers = buildWorldChunkLayerInstanceIds( (Array.isArray(normalized.roomLayers) ? normalized.roomLayers : []).map((layer) => ({ ...cloneValue(layer), rows: transformChunkRows(layer?.rows, safeWidth, safeHeight, (Number(layer?.layer) || 0) === 0 ? "." : " ", normalizedOperation), })), instances, safeWidth, safeHeight, ); const heightLayers = cloneWorldChunkHeightLayers(normalized.heightLayers) .map((entry) => transformChunkHeightPatch(entry, safeWidth, safeHeight, normalizedOperation)) .filter(Boolean) .sort((left, right) => { if ((Number(left?.z) || 0) !== (Number(right?.z) || 0)) { return (Number(left?.z) || 0) - (Number(right?.z) || 0); } return String(left?.name || left?.id || "").localeCompare(String(right?.name || right?.id || "")); }); return { ...normalized, roomLayers, heightLayers, instances, }; }