Add utilities for managing terrain layers (add, remove, and check existence by material ID)

This commit is contained in:
2026-05-13 00:43:49 +02:00
parent 48f441a99d
commit 4d19b48e0f

View File

@@ -1618,6 +1618,152 @@ export function cloneTerrain(terrain: Terrain): Terrain {
return createTerrain(terrain);
}
export function findTerrainLayerIndexByMaterialId(
terrain: Pick<Terrain, "layers">,
materialId: string | null
): number {
return terrain.layers.findIndex((layer) => layer.materialId === materialId);
}
export function createTerrainWithAddedLayer(
terrain: Terrain,
materialId: string | null = null
): Terrain {
if (terrain.layers.length >= MAX_TERRAIN_LAYER_COUNT) {
throw new Error(
`Terrain already has the maximum of ${MAX_TERRAIN_LAYER_COUNT} material layers.`
);
}
const oldStoredWeightCount = getTerrainStoredPaintWeightCount(
terrain.layers.length
);
const nextLayers = [
...cloneTerrainLayers(terrain.layers),
{
materialId: normalizeTerrainLayerMaterialId(
materialId,
`Terrain layer ${terrain.layers.length}.materialId`
)
}
];
const nextStoredWeightCount = getTerrainStoredPaintWeightCount(
nextLayers.length
);
const sampleCount = terrain.sampleCountX * terrain.sampleCountZ;
const nextPaintWeights = new Array<number>(
sampleCount * nextStoredWeightCount
).fill(0);
for (let sampleIndex = 0; sampleIndex < sampleCount; sampleIndex += 1) {
const oldOffset = sampleIndex * oldStoredWeightCount;
const nextOffset = sampleIndex * nextStoredWeightCount;
for (
let layerOffset = 0;
layerOffset < oldStoredWeightCount;
layerOffset += 1
) {
nextPaintWeights[nextOffset + layerOffset] =
terrain.paintWeights[oldOffset + layerOffset] ?? 0;
}
}
return createTerrain({
...terrain,
layers: nextLayers,
paintWeights: nextPaintWeights
});
}
export function createTerrainWithRemovedLayer(
terrain: Terrain,
layerIndex: number
): Terrain {
if (
!Number.isInteger(layerIndex) ||
layerIndex < 0 ||
layerIndex >= terrain.layers.length
) {
throw new Error(`Terrain layer index ${layerIndex} is out of range.`);
}
if (layerIndex === 0) {
throw new Error("Terrain base layer cannot be removed.");
}
if (terrain.layers.length <= MIN_TERRAIN_LAYER_COUNT) {
throw new Error("Terrain must keep at least one material layer.");
}
const oldStoredWeightCount = getTerrainStoredPaintWeightCount(
terrain.layers.length
);
const nextLayers = cloneTerrainLayers(terrain.layers).filter(
(_, currentLayerIndex) => currentLayerIndex !== layerIndex
);
const nextStoredWeightCount = getTerrainStoredPaintWeightCount(
nextLayers.length
);
const sampleCount = terrain.sampleCountX * terrain.sampleCountZ;
const nextPaintWeights = new Array<number>(
sampleCount * nextStoredWeightCount
).fill(0);
const removedLayerOffset = layerIndex - 1;
for (let sampleIndex = 0; sampleIndex < sampleCount; sampleIndex += 1) {
const oldOffset = sampleIndex * oldStoredWeightCount;
const nextOffset = sampleIndex * nextStoredWeightCount;
let nextLayerOffset = 0;
for (
let oldLayerOffset = 0;
oldLayerOffset < oldStoredWeightCount;
oldLayerOffset += 1
) {
if (oldLayerOffset === removedLayerOffset) {
continue;
}
nextPaintWeights[nextOffset + nextLayerOffset] =
terrain.paintWeights[oldOffset + oldLayerOffset] ?? 0;
nextLayerOffset += 1;
}
}
return createTerrain({
...terrain,
layers: nextLayers,
paintWeights: nextPaintWeights
});
}
export function ensureTerrainMaterialLayer(
terrain: Terrain,
materialId: string
): { terrain: Terrain; layerIndex: number; added: boolean } {
const existingLayerIndex = findTerrainLayerIndexByMaterialId(
terrain,
materialId
);
if (existingLayerIndex !== -1) {
return {
terrain,
layerIndex: existingLayerIndex,
added: false
};
}
const nextTerrain = createTerrainWithAddedLayer(terrain, materialId);
return {
terrain: nextTerrain,
layerIndex: nextTerrain.layers.length - 1,
added: true
};
}
export function areTerrainsEqual(left: Terrain, right: Terrain): boolean {
return (
left.id === right.id &&