Crusader_Decomp/map_renderer/src/lib/sorting.js
2026-03-27 10:04:44 +01:00

394 lines
12 KiB
JavaScript

import { FLAG_FLIPPED } from "./formats.js";
function rectIntersects(left, right) {
return left.left < right.right && left.right > right.left && left.top < right.bottom && left.bottom > right.top;
}
function rectContains(outer, inner) {
return outer.left <= inner.left && outer.top <= inner.top && outer.right >= inner.right && outer.bottom >= inner.bottom;
}
function listLessThan(left, right) {
if (left.sprite !== right.sprite) {
return left.sprite < right.sprite;
}
if (left.z !== right.z) {
return left.z < right.z;
}
return left.flat > right.flat;
}
function overlap(left, right) {
if (!rectIntersects(left, right)) {
return false;
}
const pointTopDiff = [left.sx_top - right.sx_bot, left.sy_top - right.sy_bot];
const pointBotDiff = [left.sx_bot - right.sx_top, left.sy_bot - right.sy_top];
const dotTopLeft = pointTopDiff[0] + pointTopDiff[1] * 2;
const dotTopRight = -pointTopDiff[0] + pointTopDiff[1] * 2;
const dotBotLeft = pointBotDiff[0] - pointBotDiff[1] * 2;
const dotBotRight = -pointBotDiff[0] - pointBotDiff[1] * 2;
const rightClear = left.sx_right <= right.sx_left;
const leftClear = left.sx_left >= right.sx_right;
const topLeftClear = dotTopLeft >= 0;
const topRightClear = dotTopRight >= 0;
const botLeftClear = dotBotLeft >= 0;
const botRightClear = dotBotRight >= 0;
const clear = rightClear || leftClear || botRightClear || botLeftClear || topRightClear || topLeftClear;
return !clear;
}
function occludes(left, right) {
if (!rectContains(left, right)) {
return false;
}
const pointTopDiff = [left.sx_top - right.sx_top, left.sy_top - right.sy_top];
const pointBotDiff = [left.sx_bot - right.sx_bot, left.sy_bot - right.sy_bot];
const dotTopLeft = pointTopDiff[0] + pointTopDiff[1] * 2;
const dotTopRight = -pointTopDiff[0] + pointTopDiff[1] * 2;
const dotBotLeft = pointBotDiff[0] - pointBotDiff[1] * 2;
const dotBotRight = -pointBotDiff[0] - pointBotDiff[1] * 2;
const rightRes = left.sx_right >= right.sx_right;
const leftRes = left.sx_left <= right.sx_left;
const topLeftRes = dotTopLeft <= 0;
const topRightRes = dotTopRight <= 0;
const botLeftRes = dotBotLeft <= 0;
const botRightRes = dotBotRight <= 0;
return rightRes && leftRes && botRightRes && botLeftRes && topRightRes && topLeftRes;
}
function below(left, right) {
if (left.sprite !== right.sprite) {
return left.sprite < right.sprite;
}
if (left.flat && right.flat) {
if (left.z !== right.z) {
return left.z < right.z;
}
} else if (left.invitem === right.invitem) {
if (left.z_top <= right.z) {
return true;
}
if (left.z >= right.z_top) {
return false;
}
}
const yFlatLeft = left.y_far === left.y;
const yFlatRight = right.y_far === right.y;
if (yFlatLeft && yFlatRight) {
if (Math.floor(left.y / 32) !== Math.floor(right.y / 32)) {
return left.y < right.y;
}
} else {
if (left.y <= right.y_far) {
return true;
}
if (left.y_far >= right.y) {
return false;
}
}
const xFlatLeft = left.x_left === left.x;
const xFlatRight = right.x_left === right.x;
if (xFlatLeft && xFlatRight) {
if (Math.floor(left.x / 32) !== Math.floor(right.x / 32)) {
return left.x < right.x;
}
} else {
if (left.x <= right.x_left) {
return true;
}
if (left.x_left >= right.x) {
return false;
}
}
if (left.z_top - 8 <= right.z && left.z < right.z_top - 8) {
return true;
}
if (left.z >= right.z_top - 8 && left.z_top - 8 > right.z) {
return false;
}
if (yFlatLeft !== yFlatRight) {
if (Math.floor(left.y / 32) <= Math.floor(right.y_far / 32)) {
return true;
}
if (Math.floor(left.y_far / 32) >= Math.floor(right.y / 32)) {
return false;
}
const yCenterLeft = Math.floor((Math.floor(left.y_far / 32) + Math.floor(left.y / 32)) / 2);
const yCenterRight = Math.floor((Math.floor(right.y_far / 32) + Math.floor(right.y / 32)) / 2);
if (yCenterLeft !== yCenterRight) {
return yCenterLeft < yCenterRight;
}
}
if (xFlatLeft !== xFlatRight) {
if (Math.floor(left.x / 32) <= Math.floor(right.x_left / 32)) {
return true;
}
if (Math.floor(left.x_left / 32) >= Math.floor(right.x / 32)) {
return false;
}
const xCenterLeft = Math.floor((Math.floor(left.x_left / 32) + Math.floor(left.x / 32)) / 2);
const xCenterRight = Math.floor((Math.floor(right.x_left / 32) + Math.floor(right.x / 32)) / 2);
if (xCenterLeft !== xCenterRight) {
return xCenterLeft < xCenterRight;
}
}
if (left.flat || right.flat) {
if (left.z !== right.z) return left.z < right.z;
if (left.invitem !== right.invitem) return left.invitem < right.invitem;
if (left.flat !== right.flat) return left.flat > right.flat;
if (left.trans !== right.trans) return left.trans < right.trans;
if (left.anim !== right.anim) return left.anim < right.anim;
if (left.draw !== right.draw) return left.draw > right.draw;
if (left.solid !== right.solid) return left.solid > right.solid;
if (left.occl !== right.occl) return left.occl > right.occl;
if (left.fbigsq !== right.fbigsq) return left.fbigsq > right.fbigsq;
}
if (left.x === right.x && left.y === right.y && left.trans !== right.trans) {
return left.trans < right.trans;
}
if (left.land && right.land && left.roof !== right.roof) {
return left.roof < right.roof;
}
if (left.roof !== right.roof) {
return left.roof > right.roof;
}
if (left.z !== right.z) {
return left.z < right.z;
}
if (xFlatLeft || xFlatRight || yFlatLeft || yFlatRight) {
if (left.sx_left !== right.sx_left) {
return left.sx_left > right.sx_left;
}
if (left.sy_bot !== right.sy_bot) {
return left.sy_bot < right.sy_bot;
}
}
if (left.x + left.y !== right.x + right.y) return left.x + left.y < right.x + right.y;
if (left.x_left + left.y_far !== right.x_left + right.y_far) return left.x_left + left.y_far < right.x_left + right.y_far;
if (left.y !== right.y) return left.y < right.y;
if (left.x !== right.x) return left.x < right.x;
if (left.item.shape !== right.item.shape) return left.item.shape < right.item.shape;
return left.item.frame < right.item.frame;
}
function buildSortNode(item, info, frame, pixels) {
const flipped = Boolean(item.flags & FLAG_FLIPPED);
const xdim = (flipped ? info.y : info.x) * 32;
const ydim = (flipped ? info.x : info.y) * 32;
const zdim = info.z * 8;
const x = item.x;
const y = item.y;
const z = item.z;
const xLeft = x - xdim;
const yFar = y - ydim;
const zTop = z + zdim;
const sxLeft = Math.trunc(xLeft / 4 - y / 4);
const sxRight = Math.trunc(x / 4 - yFar / 4);
const sxTop = Math.trunc(xLeft / 4 - yFar / 4);
const syTop = Math.trunc(xLeft / 8 + yFar / 8 - zTop);
const sxBot = Math.trunc(x / 4 - y / 4);
const syBot = Math.trunc(x / 8 + y / 8 - z);
const left = flipped ? sxBot + frame.xoff - frame.width : sxBot - frame.xoff;
const top = syBot - frame.yoff;
const right = left + frame.width;
const bottom = top + frame.height;
return {
item,
info,
frame,
pixels,
left,
top,
right,
bottom,
x,
x_left: xLeft,
y,
y_far: yFar,
z,
z_top: zTop,
sx_left: sxLeft,
sx_right: sxRight,
sx_top: sxTop,
sy_top: syTop,
sx_bot: sxBot,
sy_bot: syBot,
fbigsq: xdim === ydim && xdim >= 128,
flat: zdim === 0,
occl: info.isOccl && !info.isTranslucent,
solid: info.isSolid,
draw: info.isDraw,
roof: info.isRoof,
noisy: info.isNoisy,
anim: info.animType !== 0,
trans: info.isTranslucent,
fixed: info.isFixed,
land: info.isLand,
sprite: false,
invitem: info.isInvitem,
occluded: false,
order: -1,
depends: []
};
}
function insertDependencySorted(depends, node) {
for (let index = 0; index < depends.length; index += 1) {
const current = depends[index];
if (current === node) {
return false;
}
if (listLessThan(node, current)) {
depends.splice(index, 0, node);
return true;
}
}
depends.push(node);
return true;
}
function resolvePaintOrder(ordered, progress, checkpointEvery = 0) {
const painted = [];
function visit(node) {
if (node.occluded || node.order >= 0) {
return;
}
node.order = -2;
for (const dependency of node.depends) {
if (dependency.order === -2) {
break;
}
if (dependency.order === -1) {
visit(dependency);
}
}
node.order = painted.length ? painted[painted.length - 1].order + 1 : 0;
painted.push(node);
if (progress && checkpointEvery > 0 && painted.length % checkpointEvery === 0) {
progress(`paint resolved=${painted.length} of ${ordered.length}`);
}
}
for (const node of ordered) {
if (node.order === -1) {
visit(node);
}
}
if (progress) {
progress(`paint complete resolved=${painted.length} of ${ordered.length}`);
}
return painted;
}
export function prepareSortedItems(items, archive, shapeInfos, options = {}) {
const { progress, checkpointEvery = 0, maxInvalidDetails = 20 } = options;
const ordered = [];
let minLeft = Number.MAX_SAFE_INTEGER;
let minTop = Number.MAX_SAFE_INTEGER;
let maxRight = -Number.MAX_SAFE_INTEGER;
let maxBottom = -Number.MAX_SAFE_INTEGER;
let occludedCount = 0;
let invalidItemCount = 0;
const invalidItems = [];
let dependencyCount = 0;
for (let itemIndex = 0; itemIndex < items.length; itemIndex += 1) {
const item = items[itemIndex];
let frame;
let pixels;
try {
const decoded = archive.decodeFrame(item.shape, item.frame);
frame = decoded.frame;
pixels = decoded.pixels;
} catch (error) {
invalidItemCount += 1;
if (invalidItems.length < maxInvalidDetails) {
invalidItems.push({
shape: item.shape,
frame: item.frame,
x: item.x,
y: item.y,
z: item.z,
source: item.source,
reason: error instanceof Error ? error.message : String(error)
});
}
continue;
}
const node = buildSortNode(item, shapeInfos[item.shape], frame, pixels);
minLeft = Math.min(minLeft, node.left);
minTop = Math.min(minTop, node.top);
maxRight = Math.max(maxRight, node.right);
maxBottom = Math.max(maxBottom, node.bottom);
let insertAt = ordered.length;
for (let index = 0; index < ordered.length; index += 1) {
const other = ordered[index];
if (insertAt === ordered.length && listLessThan(node, other)) {
insertAt = index;
}
if (other.occluded) {
continue;
}
if (!overlap(node, other)) {
continue;
}
if (below(node, other)) {
if (other.occl && occludes(other, node)) {
node.occluded = true;
occludedCount += 1;
break;
}
if (insertDependencySorted(other.depends, node)) {
dependencyCount += 1;
}
} else if (node.occl && occludes(node, other)) {
if (!other.occluded) {
other.occluded = true;
occludedCount += 1;
}
} else if (insertDependencySorted(node.depends, other)) {
dependencyCount += 1;
}
}
ordered.splice(insertAt, 0, node);
if (progress && checkpointEvery > 0 && (itemIndex + 1) % checkpointEvery === 0) {
progress(
`sort processed=${itemIndex + 1} valid=${ordered.length} occluded=${occludedCount} invalid=${invalidItemCount} dependencies=${dependencyCount}`
);
}
}
if (progress) {
progress(
`sort complete processed=${items.length} valid=${ordered.length} occluded=${occludedCount} invalid=${invalidItemCount} dependencies=${dependencyCount}`
);
}
return {
minLeft,
minTop,
maxRight,
maxBottom,
prepared: resolvePaintOrder(ordered, progress, checkpointEvery),
occludedCount,
invalidItemCount,
invalidItems
};
}