/**
 * TerrainGenWorker.js — Web Worker для процедурной генерации ландшафта.
 *
 * Архитектура (Этап C оптимизации):
 *   - main thread → отправляет params + bbox → worker
 *   - worker (фоновый поток) → сэмплит heightmap, surface extraction,
 *     деревья, decorations
 *   - worker → отправляет результат: Array<{x,y,z,m}> + decorations[]
 *   - main thread → передаёт в terrainManager.loadFromArray + decoManager
 *
 * Worker устраняет 5-15 секунд блокировки UI на больших картах.
 * UI остаётся отзывчивым — progress-bar анимируется плавно.
 *
 * Worker код — self-contained, без импортов. Загружается через blob URL.
 *
 * НЕ импортируется напрямую. Используй getTerrainGenWorkerUrl().
 */

const SOURCE = `
"use strict";

// ============================================================================
// SimplexNoise (inline копия из engine/voxel/SimplexNoise.js)
// ============================================================================
function mulberry32(seed) {
    let t = seed | 0;
    return function () {
        t = (t + 0x6D2B79F5) | 0;
        let r = Math.imul(t ^ (t >>> 15), 1 | t);
        r = (r + Math.imul(r ^ (r >>> 7), 61 | r)) ^ r;
        return ((r ^ (r >>> 14)) >>> 0) / 4294967296;
    };
}

const GRAD2 = [
    1, 1, -1, 1, 1, -1, -1, -1,
    1, 0, -1, 0, 1, 0, -1, 0,
    0, 1, 0, -1, 0, 1, 0, -1,
    1, 1, 0, 0, -1, 1, 0, 0,
];
const F2 = 0.5 * (Math.sqrt(3) - 1);
const G2 = (3 - Math.sqrt(3)) / 6;

class SimplexNoise {
    constructor(seed) {
        this.seed = seed;
        const rand = mulberry32(seed);
        const perm = new Uint8Array(512);
        const p = new Uint8Array(256);
        for (let i = 0; i < 256; i++) p[i] = i;
        for (let i = 255; i > 0; i--) {
            const j = Math.floor(rand() * (i + 1));
            const tmp = p[i]; p[i] = p[j]; p[j] = tmp;
        }
        for (let i = 0; i < 512; i++) perm[i] = p[i & 255];
        this.perm = perm;
    }
    noise2(xin, yin) {
        const perm = this.perm;
        const s = (xin + yin) * F2;
        const i = Math.floor(xin + s);
        const j = Math.floor(yin + s);
        const t = (i + j) * G2;
        const X0 = i - t, Y0 = j - t;
        const x0 = xin - X0, y0 = yin - Y0;
        let i1, j1;
        if (x0 > y0) { i1 = 1; j1 = 0; } else { i1 = 0; j1 = 1; }
        const x1 = x0 - i1 + G2, y1 = y0 - j1 + G2;
        const x2 = x0 - 1 + 2 * G2, y2 = y0 - 1 + 2 * G2;
        const ii = i & 255, jj = j & 255;
        const gi0 = perm[ii + perm[jj]] & 15;
        const gi1 = perm[ii + i1 + perm[jj + j1]] & 15;
        const gi2 = perm[ii + 1 + perm[jj + 1]] & 15;
        let n0 = 0, n1 = 0, n2 = 0;
        let t0 = 0.5 - x0 * x0 - y0 * y0;
        if (t0 >= 0) { t0 *= t0; n0 = t0 * t0 * (GRAD2[gi0*2]*x0 + GRAD2[gi0*2+1]*y0); }
        let t1m = 0.5 - x1 * x1 - y1 * y1;
        if (t1m >= 0) { t1m *= t1m; n1 = t1m * t1m * (GRAD2[gi1*2]*x1 + GRAD2[gi1*2+1]*y1); }
        let t2m = 0.5 - x2 * x2 - y2 * y2;
        if (t2m >= 0) { t2m *= t2m; n2 = t2m * t2m * (GRAD2[gi2*2]*x2 + GRAD2[gi2*2+1]*y2); }
        return 70 * (n0 + n1 + n2);
    }
    fbm2(x, y, octaves, persistence, lacunarity) {
        let total = 0, amp = 1, freq = 1, norm = 0;
        for (let i = 0; i < octaves; i++) {
            total += this.noise2(x * freq, y * freq) * amp;
            norm += amp;
            amp *= persistence;
            freq *= lacunarity;
        }
        return total / norm;
    }
}

// ============================================================================
// Хэш для детерминированной расстановки структур
// ============================================================================
function hash2(x, y, seed) {
    let h = (x * 374761393 + y * 668265263 + seed * 1442695040) | 0;
    h = Math.imul(h ^ (h >>> 13), 1274126177);
    return ((h ^ (h >>> 16)) >>> 0) / 4294967296;
}

// ============================================================================
// WorldGenerator (упрощённая копия — sample + bbox)
// ============================================================================
let G_PARAMS = null;
let G_HEIGHT_NOISE = null;
let G_BIOME_NOISE = null;

function setupGenerator(params) {
    G_PARAMS = params;
    G_HEIGHT_NOISE = new SimplexNoise(params.seed);
    G_BIOME_NOISE = new SimplexNoise(params.seed + (params.biomeMap?.seedOffset ?? 100));
}

function rawHeight(x, z) {
    const hp = G_PARAMS.heightmap;
    let wx = x, wz = z;
    if (G_PARAMS.domainWarp?.enabled) {
        const ws = G_PARAMS.domainWarp.scale;
        const wstr = G_PARAMS.domainWarp.strength;
        wx = x + G_HEIGHT_NOISE.noise2(x * ws, z * ws) * wstr;
        wz = z + G_HEIGHT_NOISE.noise2(x * ws + 100, z * ws + 100) * wstr;
    }
    return G_HEIGHT_NOISE.fbm2(wx * hp.scale, wz * hp.scale, hp.octaves, hp.persistence, hp.lacunarity);
}

function sampleBiome(x, z) {
    const bm = G_PARAMS.biomeMap;
    const n = (G_BIOME_NOISE.fbm2(x * bm.scale, z * bm.scale, 3, 0.5, 2.0) + 1) * 0.5;
    const biomes = G_PARAMS.biomes;
    for (let i = 0; i < biomes.length; i++) {
        const b = biomes[i];
        if (n >= b.threshold[0] && n < b.threshold[1]) return b;
    }
    return biomes[biomes.length - 1];
}

function smoothBiomeBonus(x, z) {
    const BLEND_WIDTH = 0.08;
    const bm = G_PARAMS.biomeMap;
    const n = (G_BIOME_NOISE.fbm2(x * bm.scale, z * bm.scale, 3, 0.5, 2.0) + 1) * 0.5;
    const biomes = G_PARAMS.biomes;
    let currentIdx = -1;
    for (let i = 0; i < biomes.length; i++) {
        const b = biomes[i];
        if (n >= b.threshold[0] && n < b.threshold[1]) { currentIdx = i; break; }
    }
    if (currentIdx === -1) currentIdx = biomes.length - 1;
    const cur = biomes[currentIdx];
    const curBonus = cur.heightBonus ?? 1;
    const t0 = cur.threshold[0];
    const t1 = cur.threshold[1];
    const distFromStart = n - t0;
    const distFromEnd = t1 - n;
    if (distFromStart < BLEND_WIDTH && currentIdx > 0) {
        const prev = biomes[currentIdx - 1];
        const prevBonus = prev.heightBonus ?? 1;
        const tt = distFromStart / BLEND_WIDTH;
        const ts = tt * tt * (3 - 2 * tt);
        return prevBonus * (1 - ts) + curBonus * ts;
    }
    if (distFromEnd < BLEND_WIDTH && currentIdx < biomes.length - 1) {
        const next = biomes[currentIdx + 1];
        const nextBonus = next.heightBonus ?? 1;
        const tt = distFromEnd / BLEND_WIDTH;
        const ts = tt * tt * (3 - 2 * tt);
        return nextBonus * (1 - ts) + curBonus * ts;
    }
    return curBonus;
}

function sampleHeight(x, z) {
    const hp = G_PARAMS.heightmap;
    let n = rawHeight(x, z);
    n = (n + 1) * 0.5;
    n = Math.pow(Math.max(0, Math.min(1, n)), hp.exponent);
    const bonus = smoothBiomeBonus(x, z);
    return Math.round(hp.baseHeight + n * hp.amplitude * bonus);
}

function sampleTreeAt(x, z) {
    const s = G_PARAMS.structures?.trees;
    if (!s || !s.enabled) return null;
    const biome = sampleBiome(x, z);
    const treeProb = (biome.features?.trees ?? 0) * s.density;
    if (treeProb <= 0) return null;
    const grid = Math.max(2, s.minDistance);
    const gx = Math.floor(x / grid);
    const gz = Math.floor(z / grid);
    const r = hash2(gx, gz, G_PARAMS.seed + 999);
    if (r >= treeProb) return null;
    const ox = Math.floor(hash2(gx, gz, G_PARAMS.seed + 1001) * grid);
    const oz = Math.floor(hash2(gx, gz, G_PARAMS.seed + 1002) * grid);
    if (gx * grid + ox !== x || gz * grid + oz !== z) return null;
    const treeTypes = biome.features?.treeTypes ?? ['oak'];
    const tt = Math.floor(hash2(gx, gz, G_PARAMS.seed + 1003) * treeTypes.length);
    return { type: treeTypes[tt % treeTypes.length] };
}

// ============================================================================
// Полная генерация: возвращает voxels[] + decorations[]
// ============================================================================
function generateWorld(bbox, progressCb) {
    const { x0, z0, x1, z1 } = bbox;
    const width = x1 - x0;
    const depth = z1 - z0;
    const total = width * depth;
    let done = 0;
    const totalW = total * 2 + total * 0.3;

    // === Шаг 1: heightmap ===
    const heightmap = new Map();
    for (let z = z0; z < z1; z++) {
        for (let x = x0; x < x1; x++) {
            const h = sampleHeight(x, z);
            const biome = sampleBiome(x, z);
            heightmap.set(x + ',' + z, { h, biome });
        }
        done += width;
        if ((z - z0) % 16 === 0 && progressCb) progressCb(done, totalW, 'heightmap');
    }

    // === Шаг 2: surface extraction ===
    const voxelMap = new Map();   // "x,y,z" → matId
    const setVoxel = (x, y, z, m) => voxelMap.set(x + ',' + y + ',' + z, m);
    const getVoxel = (x, y, z) => voxelMap.get(x + ',' + y + ',' + z);

    for (let z = z0; z < z1; z++) {
        for (let x = x0; x < x1; x++) {
            const info = heightmap.get(x + ',' + z);
            const h = info.h;
            const biome = info.biome;
            const nb = [];
            for (const [dx, dz] of [[-1, 0], [1, 0], [0, -1], [0, 1]]) {
                const nbInfo = heightmap.get((x + dx) + ',' + (z + dz));
                nb.push(nbInfo ? nbInfo.h : 0);
            }
            const minNbH = Math.min(...nb);
            if (h <= 0) {
                setVoxel(x, 0, z, biome.softMaterial);
                done++;
                continue;
            }
            setVoxel(x, h - 1, z, biome.topMaterial);
            if (h >= 2 && (h - 2) >= minNbH) setVoxel(x, h - 2, z, biome.softMaterial);
            if (h >= 3 && (h - 3) >= minNbH) setVoxel(x, h - 3, z, biome.softMaterial);
            const bottomVisible = Math.max(0, minNbH);
            const topVisible = h - 3;
            for (let y = bottomVisible; y < topVisible; y++) {
                setVoxel(x, y, z, biome.hardMaterial);
            }
            if (minNbH <= 0 && !getVoxel(x, 0, z)) {
                setVoxel(x, 0, z, biome.hardMaterial);
            }
            done++;
        }
        if ((z - z0) % 16 === 0 && progressCb) progressCb(done, totalW, 'surface');
    }

    // === Шаг 3: деревья ===
    let treesPlaced = 0;
    if (G_PARAMS.structures?.trees?.enabled) {
        for (let z = z0; z < z1; z++) {
            for (let x = x0; x < x1; x++) {
                const tree = sampleTreeAt(x, z);
                if (!tree) continue;
                const info = heightmap.get(x + ',' + z);
                if (!info || info.h < 1) continue;
                if (info.biome.topMaterial !== 'grass') continue;
                placeTree(x, info.h - 1, z, tree.type, setVoxel);
                treesPlaced++;
            }
            done += width * 0.3;
            if ((z - z0) % 32 === 0 && progressCb) progressCb(done, totalW, 'trees');
        }
    }

    // === Шаг 4: decorations ===
    const decorations = [];
    const decoParams = G_PARAMS.structures?.decorations ?? {};
    const flowersDensity = decoParams.flowersDensity ?? 0.015;
    const grassDensity = decoParams.grassDensity ?? 0.05;
    if ((flowersDensity > 0 || grassDensity > 0) && decoParams.enabled !== false) {
        const FLOWERS_BY_BIOME = {
            grass: ['daisy', 'cornflower', 'poppy', 'dandelion'],
            forest: ['daisy', 'fly_mushroom', 'brown_mushroom', 'fly_mushroom'],
            plain: ['dandelion', 'cornflower', 'daisy'],
        };
        const GRASS_POOL = [
            'grass_short', 'grass_short', 'grass_short', 'grass_short',
            'grass_tuft', 'grass_tuft', 'grass_tuft',
            'grass_blade', 'grass_blade',
            'grass_wide', 'grass_wide',
            'grass_tall', 'grass_clump', 'grass_dry',
        ];
        for (let z = z0; z < z1; z++) {
            for (let x = x0; x < x1; x++) {
                const info = heightmap.get(x + ',' + z);
                if (!info || info.h < 1) continue;
                if (info.biome.topMaterial !== 'grass') continue;
                const topY = info.h - 1;
                const aboveMat = getVoxel(x, topY + 1, z);
                if (aboveMat) continue;
                const wx = (x + 0.5) * 0.25;
                const wy = info.h * 0.25;
                const wz = (z + 0.5) * 0.25;
                const rFlower = hash2(x, z, G_PARAMS.seed + 5001);
                if (rFlower < flowersDensity) {
                    const biomeId = info.biome.id;
                    const pool = FLOWERS_BY_BIOME[biomeId] || FLOWERS_BY_BIOME.plain;
                    if (pool && pool.length > 0) {
                        const modelIdx = Math.floor(hash2(x, z, G_PARAMS.seed + 5002) * pool.length);
                        const modelId = pool[modelIdx];
                        const rotation = Math.floor(hash2(x, z, G_PARAMS.seed + 5003) * 4);
                        decorations.push({ x: wx, y: wy, z: wz, modelId, rotation });
                        continue;
                    }
                }
                const rGrass = hash2(x, z, G_PARAMS.seed + 5004);
                if (rGrass < grassDensity) {
                    const modelIdx = Math.floor(hash2(x, z, G_PARAMS.seed + 5005) * GRASS_POOL.length);
                    const modelId = GRASS_POOL[modelIdx];
                    const rotation = Math.floor(hash2(x, z, G_PARAMS.seed + 5006) * 4);
                    const scale = 0.7 + hash2(x, z, G_PARAMS.seed + 5007) * 0.7;
                    decorations.push({ x: wx, y: wy, z: wz, modelId, rotation, scale });
                }
            }
        }
    }

    // Конвертируем Map в массив для возврата
    const voxels = [];
    for (const [key, m] of voxelMap) {
        const lastComma = key.lastIndexOf(',');
        const midComma = key.lastIndexOf(',', lastComma - 1);
        voxels.push({
            x: parseInt(key.slice(0, midComma), 10),
            y: parseInt(key.slice(midComma + 1, lastComma), 10),
            z: parseInt(key.slice(lastComma + 1), 10),
            m,
        });
    }
    return { voxels, decorations, treesPlaced };
}

// ============================================================================
// _placeTree — копия из engine/voxel/WorldGenerator.js (упрощённая)
// ============================================================================
function trunkOffsets(thickness) {
    if (thickness <= 0) return [[0, 0]];
    if (thickness === 1) return [[0, 0], [1, 0], [-1, 0], [0, 1], [0, -1]];
    if (thickness === 2) {
        return [
            [0, 0], [1, 0], [-1, 0], [0, 1], [0, -1],
            [1, 1], [-1, -1], [1, -1], [-1, 1],
        ];
    }
    const out = [];
    for (let dx = -2; dx <= 2; dx++) {
        for (let dz = -2; dz <= 2; dz++) {
            if (Math.abs(dx) === 2 && Math.abs(dz) === 2) continue;
            out.push([dx, dz]);
        }
    }
    return out;
}

function placeCrown(cx, cy, cz, r, squish, placeLeaf) {
    const r2sq = r * r;
    const innerR2 = r > 2 ? (r - 2) * (r - 2) : 0;
    const yRange = Math.floor(r * squish) + 2;
    for (let dx = -r - 1; dx <= r + 1; dx++) {
        for (let dy = -Math.floor(r / 2); dy <= yRange; dy++) {
            for (let dz = -r - 1; dz <= r + 1; dz++) {
                const dyEff = dy / squish;
                const d2 = dx * dx + dyEff * dyEff + dz * dz;
                if (d2 > r2sq + 2) continue;
                if (d2 < innerR2) continue;
                const edgeThresh = r2sq - 1.5;
                if (d2 > edgeThresh) {
                    if (hash2(cx + dx, cz + dz, G_PARAMS.seed + 3000 + dy) > 0.5) continue;
                }
                if (dx === 0 && dz === 0 && dy < 0) continue;
                const colorNoise = hash2(cx + dx, cz + dz + dy * 11, G_PARAMS.seed + 3500);
                placeLeaf(cx + dx, cy + dy, cz + dz, colorNoise);
            }
        }
    }
}

function placeTree(tx, topY, tz, type, setVoxel) {
    const r1 = hash2(tx, tz, G_PARAMS.seed + 2001);
    const r2 = hash2(tx, tz, G_PARAMS.seed + 2002);
    const r3 = hash2(tx, tz, G_PARAMS.seed + 2003);
    const sizeScale = Math.max(0.3, Math.min(5.0, G_PARAMS.structures?.trees?.sizeScale ?? 1.0));
    let primaryTrunk, secondaryTrunk, primaryLeaf, secondaryLeaf;
    let secondaryLeafChance, trunkH, crownR, crownSquish, trunkThickness;

    if (type === 'birch') {
        primaryTrunk = 'trunk_white'; secondaryTrunk = 'trunk_white';
        primaryLeaf = 'leaves'; secondaryLeaf = 'leaves_orange';
        secondaryLeafChance = 0.10;
        trunkH = Math.round((10 + r1 * 4) * sizeScale);
        crownR = Math.round(3 * sizeScale);
        crownSquish = 1.5;
        trunkThickness = sizeScale < 1.5 ? 0 : 1;
    } else if (type === 'autumn') {
        primaryTrunk = 'trunk'; secondaryTrunk = 'wood';
        primaryLeaf = 'leaves_orange'; secondaryLeaf = 'leaves';
        secondaryLeafChance = 0.25;
        trunkH = Math.round((9 + r1 * 4) * sizeScale);
        crownR = Math.round((4 + r2 * 2) * sizeScale);
        crownSquish = 0.85;
        trunkThickness = sizeScale < 0.8 ? 0 : sizeScale < 1.5 ? 1 : sizeScale < 2.5 ? 2 : 3;
    } else {
        primaryTrunk = 'trunk'; secondaryTrunk = 'wood';
        primaryLeaf = 'leaves'; secondaryLeaf = 'leaves_orange';
        secondaryLeafChance = 0.07;
        trunkH = Math.round((8 + r1 * 4) * sizeScale);
        crownR = Math.round((3 + r2 * 2) * sizeScale);
        crownSquish = 1.0;
        trunkThickness = sizeScale < 0.8 ? 0 : sizeScale < 1.5 ? 1 : sizeScale < 2.5 ? 2 : 3;
    }
    if (trunkH < 3) trunkH = 3;
    if (crownR < 2) crownR = 2;

    const placeLeaf = (px, py, pz, noise) => {
        const isSecondary = noise < secondaryLeafChance;
        setVoxel(px, py, pz, isSecondary ? secondaryLeaf : primaryLeaf);
    };
    const placeTrunk = (px, py, pz, noise) => {
        const isSecondary = (type !== 'birch') && noise < 0.05;
        setVoxel(px, py, pz, isSecondary ? secondaryTrunk : primaryTrunk);
    };

    const taperStart = Math.floor(trunkH * 0.7);
    for (let dy = 1; dy <= trunkH; dy++) {
        const thicknessAtY = dy < taperStart
            ? trunkThickness
            : Math.max(0, trunkThickness - Math.floor((dy - taperStart) * 0.5));
        const offsets = trunkOffsets(thicknessAtY);
        for (const [ox, oz] of offsets) {
            const noise = hash2(tx + ox * 13, tz + oz * 17 + dy * 7, G_PARAMS.seed + 2100);
            placeTrunk(tx + ox, topY + dy, tz + oz, noise);
        }
    }

    if (trunkThickness >= 2) {
        const baseH = Math.min(3, Math.floor(trunkH / 5));
        for (let dy = 1; dy <= baseH; dy++) {
            for (const [ox, oz] of [[2, 0], [-2, 0], [0, 2], [0, -2], [1, 1], [-1, -1], [1, -1], [-1, 1]]) {
                if (hash2(tx + ox * 13, tz + oz * 17 + dy, G_PARAMS.seed + 2200) > 0.55) continue;
                placeTrunk(tx + ox, topY + dy, tz + oz, 0.5);
            }
        }
    }

    const branchTips = [];
    const numBranches = type === 'birch'
        ? (sizeScale < 1.5 ? 2 : 3)
        : Math.max(2, Math.min(8, Math.round((3 + r3 * 2) * Math.sqrt(sizeScale))));
    const branchLenBase = Math.max(2, Math.floor(crownR * 0.8 + sizeScale));

    for (let b = 0; b < numBranches; b++) {
        const baseAngle = (b / numBranches) * Math.PI * 2;
        const angleJitter = (hash2(tx + b * 31, tz + b * 53, G_PARAMS.seed + 2300) - 0.5) * 0.6;
        const angle = baseAngle + angleJitter + r1 * 0.5;
        const startYFrac = 0.5 + (b / numBranches) * 0.45;
        const branchStartY = Math.max(2, Math.floor(trunkH * startYFrac));
        const branchLen = branchLenBase + Math.floor(hash2(tx + b, tz, G_PARAMS.seed + 2400) * 3);
        let bx = tx, by = topY + branchStartY, bz = tz;
        const dirX = Math.cos(angle), dirZ = Math.sin(angle);
        for (let step = 1; step <= branchLen; step++) {
            bx = Math.round(tx + dirX * step);
            bz = Math.round(tz + dirZ * step);
            const upBias = step * 0.4;
            const jitterY = (hash2(bx * 7, bz * 11, G_PARAMS.seed + 2500 + step) - 0.3) * 1.5;
            by = topY + branchStartY + Math.floor(upBias + jitterY);
            const noise = hash2(bx, bz + step * 3, G_PARAMS.seed + 2310);
            placeTrunk(bx, by, bz, noise);
            if (trunkThickness >= 2 && step <= branchLen / 2) {
                const sx = Math.round(bx - dirZ * 0.7);
                const sz = Math.round(bz + dirX * 0.7);
                if (sx !== bx || sz !== bz) {
                    placeTrunk(sx, by, sz, noise + 0.1);
                }
            }
        }
        branchTips.push({ x: bx, y: by, z: bz });
    }

    const crownCy = topY + trunkH;
    const mainCrownR = Math.max(2, Math.floor(crownR * 0.85));
    placeCrown(tx, crownCy, tz, mainCrownR, crownSquish, placeLeaf);

    const subCrownRBase = Math.max(2, Math.floor(crownR * 0.6));
    for (const tip of branchTips) {
        const sizeJitter = hash2(tip.x, tip.z, G_PARAMS.seed + 2600) * 0.4 - 0.2;
        const subR = Math.max(2, Math.round(subCrownRBase * (1.0 + sizeJitter)));
        placeCrown(tip.x, tip.y + 1, tip.z, subR, crownSquish, placeLeaf);
    }
}

// ============================================================================
// Worker message handler
// ============================================================================
self.onmessage = function(e) {
    const msg = e.data;
    if (msg.type === 'generate') {
        try {
            setupGenerator(msg.params);
            const t0 = performance.now();
            const result = generateWorld(msg.bbox, (done, total, phase) => {
                self.postMessage({ type: 'progress', done, total, phase });
            });
            const dt = performance.now() - t0;
            // Передаём voxels — может быть большой массив, лучше JSON через
            // postMessage (не Transferable т.к. это plain objects).
            self.postMessage({
                type: 'done',
                voxels: result.voxels,
                decorations: result.decorations,
                treesPlaced: result.treesPlaced,
                timeMs: Math.round(dt),
            });
        } catch (err) {
            self.postMessage({ type: 'error', message: err.message, stack: err.stack });
        }
    }
};
`;

/**
 * Создаёт Blob URL Worker'а. Использовать в new Worker(url).
 */
export function getTerrainGenWorkerUrl() {
    const blob = new Blob([SOURCE], { type: 'application/javascript' });
    return URL.createObjectURL(blob);
}
