var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
  for (var name in all)
    __defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
  if (from && typeof from === "object" || typeof from === "function") {
    for (let key of __getOwnPropNames(from))
      if (!__hasOwnProp.call(to, key) && key !== except)
        __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
  }
  return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var glb_exports = {};
__export(glb_exports, {
  __testing: () => __testing,
  createBoxelSceneGraph: () => createBoxelSceneGraph,
  exportModelToGLB: () => exportModelToGLB
});
module.exports = __toCommonJS(glb_exports);
var import_faces = require("../voxels/faces.js");
var import_primitives = require("../primitives.js");
var import_scene = require("../scene.js");
var import_skinned_mesh = require("../skinned-mesh.js");
var import_animations = require("../animations.js");
const DEFAULT_COLOR = [59, 130, 246];
const DEFAULT_OPACITY = 0.45;
const ATLAS_PADDING = 1;
const FALLBACK_TEXTURE_ID = "__fallback__";
const OVERLAY_FALLBACK_TEXTURE_ID = "__overlay_fallback__";
async function exportModelToGLB(options) {
  const { THREE, GLTFExporterCtor, model, mode = "merged", binary = true, animationIds } = options;
  if (mode === "skinned") {
    const skinned = (0, import_skinned_mesh.buildEntitySkinnedMesh)({ THREE, entity: model });
    const root = new THREE.Group();
    root.add(skinned.mesh);
    root.updateMatrixWorld(true);
    const nodeLookup = /* @__PURE__ */ new Map();
    skinned.boneMap.forEach((bone, nodeId) => nodeLookup.set(nodeId, bone));
    const animations = (0, import_animations.buildAnimations)(THREE, model, nodeLookup);
    const clips = animationIds === null ? [] : animations.filter((animation) => {
      if (!Array.isArray(animationIds) || animationIds.length === 0)
        return true;
      return animationIds.includes(animation.id);
    }).map((animation) => animation.clip);
    const exporter2 = new GLTFExporterCtor();
    const dispose = () => {
      skinned.dispose();
    };
    return new Promise((resolve, reject) => {
      try {
        exporter2.parse(root, (result) => {
          try {
            const blob = result instanceof ArrayBuffer ? new Blob([result], { type: "model/gltf-binary" }) : new Blob([JSON.stringify(result)], { type: "model/gltf+json" });
            dispose();
            resolve(blob);
          } catch (error) {
            dispose();
            reject(error);
          }
        }, (error) => {
          dispose();
          reject(error instanceof Error ? error : new Error("gltf_export_failed"));
        }, { binary, animations: clips });
      } catch (error) {
        dispose();
        reject(error instanceof Error ? error : new Error(String(error)));
      }
    });
  }
  const context = createExportContext(THREE, model);
  const sceneGraph = buildSceneGraph(context, mode);
  const exporter = new GLTFExporterCtor();
  const exportScene = new THREE.Scene();
  exportScene.background = null;
  exportScene.add(sceneGraph.root);
  exportScene.updateMatrixWorld(true);
  const animationClips = resolveAnimationClips(sceneGraph, mode, animationIds);
  return new Promise((resolve, reject) => {
    try {
      exporter.parse(exportScene, (result) => {
        try {
          const blob = result instanceof ArrayBuffer ? new Blob([result], { type: "model/gltf-binary" }) : new Blob([JSON.stringify(result)], { type: "model/gltf+json" });
          sceneGraph.dispose();
          disposeExportContext(context);
          resolve(blob);
        } catch (error) {
          sceneGraph.dispose();
          disposeExportContext(context);
          reject(error);
        }
      }, (error) => {
        sceneGraph.dispose();
        disposeExportContext(context);
        reject(error instanceof Error ? error : new Error("gltf_export_failed"));
      }, { binary, animations: animationClips });
    } catch (error) {
      sceneGraph.dispose();
      disposeExportContext(context);
      reject(error instanceof Error ? error : new Error(String(error)));
    }
  });
}
function createBoxelSceneGraph(options) {
  const { THREE, model } = options;
  const mode = options.mode ?? "merged";
  const context = createExportContext(THREE, model);
  const sceneGraph = buildSceneGraph(context, mode);
  let disposed = false;
  const dispose = () => {
    if (disposed)
      return;
    sceneGraph.dispose();
    disposeExportContext(context);
    disposed = true;
  };
  return {
    root: sceneGraph.root,
    nodeLookup: sceneGraph.nodeLookup,
    animations: sceneGraph.animations,
    dispose
  };
}
function createExportContext(THREE, model) {
  const materialsById = new Map(model.materials.map((material) => [material.id, material]));
  const primitivesById = new Map(model.primitives.map((primitive) => [primitive.id, primitive]));
  const texturesById = new Map(model.textures.map((texture) => [texture.id, texture]));
  const patternsById = new Map(model.patterns.map((pattern) => [pattern.id, pattern]));
  const voxelAtlasEntries = [];
  const voxelSurfacesByPrimitive = /* @__PURE__ */ new Map();
  let overlayFallbackNeeded = false;
  model.primitives.forEach((primitive) => {
    if (primitive.kind === "textured_box") {
      if (hasOverlayTextures(primitive)) {
        overlayFallbackNeeded = true;
      }
    }
    if (primitive.kind !== "voxel_box") {
      return;
    }
    const surfaces = (0, import_faces.generateVoxelFaceSurfaces)({
      primitive,
      materialsById,
      defaultColor: DEFAULT_COLOR
    });
    if (surfaces.length === 0) {
      return;
    }
    const cachedSurfaces = surfaces.map((surface, index) => {
      const textureId = `__voxel__${primitive.id}__${surface.face}__${index}`;
      voxelAtlasEntries.push({
        id: textureId,
        width: surface.textureWidth,
        height: surface.textureHeight,
        color: surface.color,
        emissive: surface.emissive,
        hasEmissive: surface.hasEmissive,
        hasTransparency: surface.hasTransparency
      });
      return { ...surface, textureId };
    });
    voxelSurfacesByPrimitive.set(primitive.id, cachedSurfaces);
  });
  const extraEntries = [...voxelAtlasEntries];
  if (overlayFallbackNeeded) {
    extraEntries.push(buildOverlayFallbackTexturePixels());
  }
  const usedTextureIds = collectUsedTextureIds(model, primitivesById);
  const atlas = buildTextureAtlas({ THREE, materialsById, texturesById, patternsById, usedTextureIds }, extraEntries);
  return {
    THREE,
    model,
    materialsById,
    primitivesById,
    texturesById,
    patternsById,
    atlas,
    voxelSurfacesByPrimitive,
    plainMaterialCache: /* @__PURE__ */ new Map()
  };
}
function disposeExportContext(context) {
  if (context.atlas) {
    context.atlas.map.dispose();
    context.atlas.emissiveMap?.dispose();
  }
  context.plainMaterialCache.forEach((material) => material.dispose());
  context.plainMaterialCache.clear();
}
function buildTextureAtlas(options, extraEntries = []) {
  const { THREE, materialsById, texturesById, patternsById, usedTextureIds } = options;
  if (usedTextureIds.size === 0) {
    if (extraEntries.length === 0) {
      return null;
    }
  }
  const entries = [];
  for (const textureId of usedTextureIds) {
    const textureDef = texturesById.get(textureId);
    if (!textureDef)
      continue;
    const pattern = patternsById.get(textureDef.pattern);
    if (!pattern)
      continue;
    entries.push(buildTexturePixels(textureDef, pattern, materialsById));
  }
  if (extraEntries.length > 0) {
    extraEntries.forEach((entry) => {
      entries.push(entry);
    });
  }
  const fallbackEntry = buildFallbackTexturePixels();
  entries.push(fallbackEntry);
  const paddedWidths = entries.map((entry) => entry.width + ATLAS_PADDING * 2);
  const paddedHeights = entries.map((entry) => entry.height + ATLAS_PADDING * 2);
  const cellWidth = Math.max(...paddedWidths);
  const cellHeight = Math.max(...paddedHeights);
  const textureCount = entries.length;
  const columns = Math.max(1, Math.ceil(Math.sqrt(textureCount)));
  const rows = Math.max(1, Math.ceil(textureCount / columns));
  const atlasWidth = THREE.MathUtils.ceilPowerOfTwo(columns * cellWidth);
  const atlasHeight = THREE.MathUtils.ceilPowerOfTwo(rows * cellHeight);
  const colorData = new Uint8Array(atlasWidth * atlasHeight * 4);
  const emissiveData = new Uint8Array(atlasWidth * atlasHeight * 4);
  let hasEmissive = false;
  let hasTransparency = false;
  const regions = /* @__PURE__ */ new Map();
  entries.forEach((entry, index) => {
    const column = index % columns;
    const row = Math.floor(index / columns);
    const originX = column * cellWidth;
    const originY = row * cellHeight;
    const paddedX = originX + ATLAS_PADDING;
    const paddedY = originY + ATLAS_PADDING;
    blitTexture(entry.color, entry.width, entry.height, atlasWidth, colorData, paddedX, paddedY);
    extendEdgePixels(colorData, entry.width, entry.height, atlasWidth, atlasHeight, paddedX, paddedY);
    if (entry.hasEmissive && entry.emissive) {
      blitTexture(entry.emissive, entry.width, entry.height, atlasWidth, emissiveData, paddedX, paddedY);
      extendEdgePixels(emissiveData, entry.width, entry.height, atlasWidth, atlasHeight, paddedX, paddedY);
      hasEmissive = true;
    } else {
      fillTransparentRegion(emissiveData, entry.width, entry.height, atlasWidth, paddedX, paddedY);
    }
    hasTransparency = hasTransparency || entry.hasTransparency;
    const region = {
      uMin: (paddedX + 0) / atlasWidth,
      vMin: (paddedY + 0) / atlasHeight,
      uMax: (paddedX + entry.width) / atlasWidth,
      vMax: (paddedY + entry.height) / atlasHeight
    };
    regions.set(entry.id, region);
  });
  const map = new THREE.DataTexture(colorData, atlasWidth, atlasHeight);
  map.needsUpdate = true;
  map.magFilter = THREE.NearestFilter;
  map.minFilter = THREE.NearestFilter;
  map.wrapS = THREE.ClampToEdgeWrapping;
  map.wrapT = THREE.ClampToEdgeWrapping;
  map.flipY = false;
  if ("colorSpace" in map) {
    map.colorSpace = THREE.SRGBColorSpace ?? map.colorSpace;
  } else if ("encoding" in map) {
    map.encoding = THREE.sRGBEncoding ?? map.encoding;
  }
  let emissiveMap = null;
  if (hasEmissive) {
    emissiveMap = new THREE.DataTexture(emissiveData, atlasWidth, atlasHeight);
    emissiveMap.needsUpdate = true;
    emissiveMap.magFilter = THREE.NearestFilter;
    emissiveMap.minFilter = THREE.NearestFilter;
    emissiveMap.wrapS = THREE.ClampToEdgeWrapping;
    emissiveMap.wrapT = THREE.ClampToEdgeWrapping;
    emissiveMap.flipY = false;
  }
  const fallbackRegion = regions.get(FALLBACK_TEXTURE_ID) ?? {
    uMin: 0,
    vMin: 0,
    uMax: 1,
    vMax: 1
  };
  return { regions, fallbackRegion, map, emissiveMap, hasTransparency };
}
function buildTexturePixels(textureDef, pattern, materialsById) {
  const [width, height] = pattern.size;
  const color = new Uint8Array(width * height * 4);
  const emissive = new Uint8Array(width * height * 4);
  let hasEmissive = false;
  let hasTransparency = false;
  for (let y = 0; y < height; y += 1) {
    for (let x = 0; x < width; x += 1) {
      const sourceIndex = y * width + x;
      const targetY = height - 1 - y;
      const targetIndex = (targetY * width + x) * 4;
      const paletteIndex = pattern.data[sourceIndex];
      const materialId = textureDef.materials[paletteIndex];
      const material = materialId ? materialsById.get(materialId) : void 0;
      const baseColor = material?.baseColor ?? DEFAULT_COLOR;
      const opacity = material?.opacity !== void 0 ? clamp(material.opacity, 0, 1) : 1;
      color[targetIndex] = baseColor[0];
      color[targetIndex + 1] = baseColor[1];
      color[targetIndex + 2] = baseColor[2];
      color[targetIndex + 3] = Math.round(opacity * 255);
      if (opacity < 1) {
        hasTransparency = true;
      }
      if (material?.emissiveColor) {
        emissive[targetIndex] = material.emissiveColor[0];
        emissive[targetIndex + 1] = material.emissiveColor[1];
        emissive[targetIndex + 2] = material.emissiveColor[2];
        emissive[targetIndex + 3] = 255;
        hasEmissive = true;
      } else {
        emissive[targetIndex] = 0;
        emissive[targetIndex + 1] = 0;
        emissive[targetIndex + 2] = 0;
        emissive[targetIndex + 3] = 0;
      }
    }
  }
  return { id: textureDef.id, width, height, color, emissive: hasEmissive ? emissive : void 0, hasEmissive, hasTransparency };
}
function buildFallbackTexturePixels() {
  const color = new Uint8Array(4);
  color[0] = DEFAULT_COLOR[0];
  color[1] = DEFAULT_COLOR[1];
  color[2] = DEFAULT_COLOR[2];
  color[3] = 255;
  const emissive = new Uint8Array(4);
  emissive[0] = 0;
  emissive[1] = 0;
  emissive[2] = 0;
  emissive[3] = 0;
  return {
    id: FALLBACK_TEXTURE_ID,
    width: 1,
    height: 1,
    color,
    emissive,
    hasEmissive: false,
    hasTransparency: false
  };
}
function buildOverlayFallbackTexturePixels() {
  const color = new Uint8Array(4);
  color[0] = 0;
  color[1] = 0;
  color[2] = 0;
  color[3] = 0;
  const emissive = new Uint8Array(4);
  emissive[0] = 0;
  emissive[1] = 0;
  emissive[2] = 0;
  emissive[3] = 0;
  return {
    id: OVERLAY_FALLBACK_TEXTURE_ID,
    width: 1,
    height: 1,
    color,
    emissive,
    hasEmissive: false,
    hasTransparency: true
  };
}
function blitTexture(source, width, height, atlasWidth, target, offsetX, offsetY) {
  for (let y = 0; y < height; y += 1) {
    for (let x = 0; x < width; x += 1) {
      const sx = x;
      const sy = y;
      const sourceIndex = (sy * width + sx) * 4;
      const tx = offsetX + x;
      const ty = offsetY + y;
      const targetIndex = (ty * atlasWidth + tx) * 4;
      target[targetIndex] = source[sourceIndex];
      target[targetIndex + 1] = source[sourceIndex + 1];
      target[targetIndex + 2] = source[sourceIndex + 2];
      target[targetIndex + 3] = source[sourceIndex + 3];
    }
  }
}
function fillTransparentRegion(target, width, height, atlasWidth, offsetX, offsetY) {
  for (let y = 0; y < height; y += 1) {
    for (let x = 0; x < width; x += 1) {
      const tx = offsetX + x;
      const ty = offsetY + y;
      const targetIndex = (ty * atlasWidth + tx) * 4;
      target[targetIndex] = 0;
      target[targetIndex + 1] = 0;
      target[targetIndex + 2] = 0;
      target[targetIndex + 3] = 0;
    }
  }
}
function extendEdgePixels(target, width, height, atlasWidth, atlasHeight, offsetX, offsetY) {
  const startX = offsetX;
  const endX = offsetX + width - 1;
  const startY = offsetY;
  const endY = offsetY + height - 1;
  if (startY > 0) {
    for (let x = startX; x <= endX; x += 1) {
      copyPixel(target, atlasWidth, x, startY, x, startY - 1);
    }
  }
  if (endY + 1 < atlasHeight) {
    for (let x = startX; x <= endX; x += 1) {
      copyPixel(target, atlasWidth, x, endY, x, endY + 1);
    }
  }
  if (startX > 0) {
    for (let y = startY; y <= endY; y += 1) {
      copyPixel(target, atlasWidth, startX, y, startX - 1, y);
    }
  }
  if (endX + 1 < atlasWidth) {
    for (let y = startY; y <= endY; y += 1) {
      copyPixel(target, atlasWidth, endX, y, endX + 1, y);
    }
  }
  const cornerTargets = [];
  if (startX > 0 && startY > 0)
    cornerTargets.push({ sx: startX, sy: startY, dx: startX - 1, dy: startY - 1 });
  if (startX > 0 && endY + 1 < atlasHeight)
    cornerTargets.push({ sx: startX, sy: endY, dx: startX - 1, dy: endY + 1 });
  if (endX + 1 < atlasWidth && startY > 0)
    cornerTargets.push({ sx: endX, sy: startY, dx: endX + 1, dy: startY - 1 });
  if (endX + 1 < atlasWidth && endY + 1 < atlasHeight)
    cornerTargets.push({ sx: endX, sy: endY, dx: endX + 1, dy: endY + 1 });
  cornerTargets.forEach(({ sx, sy, dx, dy }) => {
    copyPixel(target, atlasWidth, sx, sy, dx, dy);
  });
}
function copyPixel(buffer, atlasWidth, sourceX, sourceY, destX, destY) {
  const sourceIndex = (sourceY * atlasWidth + sourceX) * 4;
  const destIndex = (destY * atlasWidth + destX) * 4;
  buffer[destIndex] = buffer[sourceIndex];
  buffer[destIndex + 1] = buffer[sourceIndex + 1];
  buffer[destIndex + 2] = buffer[sourceIndex + 2];
  buffer[destIndex + 3] = buffer[sourceIndex + 3];
}
function clamp(value, min, max) {
  return Math.min(max, Math.max(min, value));
}
function hasOverlayTextures(box) {
  return Boolean(box.topOverlay || box.bottomOverlay || box.northOverlay || box.southOverlay || box.eastOverlay || box.westOverlay || box.sidesOverlay || box.allOverlay);
}
function collectUsedTextureIds(model, primitivesById) {
  const textureIds = /* @__PURE__ */ new Set();
  const visitPrimitive = (primitive, visited) => {
    if (visited.has(primitive.id))
      return;
    visited.add(primitive.id);
    switch (primitive.kind) {
      case "plain_box":
      case "voxel_box":
        break;
      case "textured_box": {
        const textured = primitive;
        import_faces.FACE_ORDER.forEach((face) => {
          const textureId = getFaceTextureId(textured, face);
          if (textureId)
            textureIds.add(textureId);
        });
        import_faces.FACE_ORDER.forEach((face) => {
          const overlayId = getOverlayTextureId(textured, face);
          if (overlayId)
            textureIds.add(overlayId);
        });
        break;
      }
      case "combo_box": {
        const combo = primitive;
        combo.slots.forEach((slot) => {
          const child = primitivesById.get(slot.primitive);
          if (child) {
            visitPrimitive(child, new Set(visited));
          }
        });
        break;
      }
      default:
        break;
    }
  };
  const traverseNode = (node) => {
    if (node.mesh) {
      const primitive = primitivesById.get(node.mesh);
      if (primitive) {
        visitPrimitive(primitive, /* @__PURE__ */ new Set());
      }
    }
    node.children.forEach(traverseNode);
  };
  traverseNode(model.geometry.root);
  return textureIds;
}
function buildSceneGraph(context, mode) {
  if (mode === "merged") {
    const scene2 = (0, import_scene.createBoxelScene)({ THREE: context.THREE, entity: context.model });
    const root = buildMergedGroupFromScene(context, scene2);
    scene2.dispose();
    const dispose = () => {
      const geometries = /* @__PURE__ */ new Set();
      const materials = /* @__PURE__ */ new Set();
      root.traverse((object) => {
        if (object instanceof context.THREE.Mesh) {
          const mesh = object;
          if (mesh.geometry)
            geometries.add(mesh.geometry);
          const material = mesh.material;
          if (Array.isArray(material))
            material.forEach((mat) => materials.add(mat));
          else if (material)
            materials.add(material);
        }
      });
      for (const geometry of geometries) {
        geometry.dispose?.();
      }
      for (const materialEntry of materials) {
        const material = materialEntry;
        material.map?.dispose?.();
        material.emissiveMap?.dispose?.();
        material.dispose?.();
      }
    };
    return { root, nodeLookup: /* @__PURE__ */ new Map(), animations: [], dispose };
  }
  if (mode === "skinned") {
    const skinned = (0, import_skinned_mesh.buildEntitySkinnedMesh)({ THREE: context.THREE, entity: context.model });
    const root = new context.THREE.Group();
    root.add(skinned.mesh);
    const nodeLookup2 = /* @__PURE__ */ new Map();
    skinned.boneMap.forEach((bone, nodeId) => nodeLookup2.set(nodeId, bone));
    const animations = (0, import_animations.buildAnimations)(context.THREE, context.model, nodeLookup2);
    const dispose = () => {
      skinned.dispose();
    };
    return {
      root,
      nodeLookup: nodeLookup2,
      animations,
      dispose
    };
  }
  const scene = (0, import_scene.createBoxelScene)({ THREE: context.THREE, entity: context.model });
  const nodeLookup = /* @__PURE__ */ new Map();
  scene.nodes.forEach((meta) => {
    nodeLookup.set(meta.id, meta.object);
  });
  return {
    root: scene.root,
    nodeLookup,
    animations: scene.animations,
    dispose: scene.dispose
  };
}
function resolveAnimationClips(sceneGraph, mode, animationIds) {
  if (mode !== "hierarchical")
    return [];
  if (animationIds === null)
    return [];
  if (Array.isArray(animationIds) && animationIds.length === 0)
    return [];
  const allowed = Array.isArray(animationIds) && animationIds.length > 0 ? new Set(animationIds) : null;
  return sceneGraph.animations.filter((animation) => allowed ? allowed.has(animation.id) : true).map((animation) => animation.clip);
}
function buildMergedGroupFromScene(context, scene) {
  const { THREE, materialsById, primitivesById, atlas, voxelSurfacesByPrimitive } = context;
  const group = new THREE.Group();
  const texturedAccumulator = atlas ? new MeshAccumulator(true) : null;
  const plainBuckets = /* @__PURE__ */ new Map();
  scene.root.updateMatrixWorld(true);
  scene.root.traverse((object) => {
    if (!(object instanceof THREE.Mesh)) {
      return;
    }
    const mesh = object;
    const primitiveId = mesh.userData?.primitiveId;
    const matrix = mesh.matrixWorld.clone();
    const sizeFromUserData = mesh.userData?.size;
    if (!primitiveId) {
      if (mesh.userData?.placeholder && sizeFromUserData) {
        const bucket = ensurePlainBucket({
          THREE,
          plainBuckets,
          options: {
            baseColor: DEFAULT_COLOR,
            emissive: null,
            opacity: DEFAULT_OPACITY
          }
        });
        appendBoxToAccumulator({
          THREE,
          accumulator: bucket.accumulator,
          size: sizeFromUserData,
          matrix
        });
      }
      return;
    }
    const primitive = primitivesById.get(primitiveId);
    if (!primitive) {
      console.warn("[entity-export] primitive not found for merged export", primitiveId);
      return;
    }
    switch (primitive.kind) {
      case "plain_box": {
        const plain = primitive;
        const materialDef = materialsById.get(plain.material);
        const bucket = ensurePlainBucket({
          THREE,
          plainBuckets,
          options: {
            baseColor: materialDef?.baseColor ?? DEFAULT_COLOR,
            emissive: materialDef?.emissiveColor ?? null,
            opacity: materialDef?.opacity ?? 1
          }
        });
        appendBoxToAccumulator({ THREE, accumulator: bucket.accumulator, size: plain.size, matrix });
        break;
      }
      case "textured_box": {
        const box = primitive;
        const isOverlayMesh = mesh.userData?.overlay === true;
        if (!atlas || !texturedAccumulator) {
          if (isOverlayMesh) {
            break;
          }
          const bucket = ensurePlainBucket({
            THREE,
            plainBuckets,
            options: { baseColor: DEFAULT_COLOR, emissive: null, opacity: 1 }
          });
          appendBoxToAccumulator({ THREE, accumulator: bucket.accumulator, size: box.size, matrix });
          break;
        }
        appendTexturedBoxToAccumulator({
          THREE,
          accumulator: texturedAccumulator,
          atlas,
          box,
          matrix,
          size: isOverlayMesh ? sizeFromUserData ?? box.size : void 0,
          getTextureId: isOverlayMesh ? (candidate, face) => getOverlayTextureId(candidate, face) ?? OVERLAY_FALLBACK_TEXTURE_ID : void 0
        });
        break;
      }
      case "voxel_box": {
        const voxel = primitive;
        if (!voxel.voxels || voxel.voxels.length === 0) {
          break;
        }
        const surfaces = voxelSurfacesByPrimitive.get(voxel.id);
        if (!atlas || !texturedAccumulator || !surfaces || surfaces.length === 0) {
          const fallbackSize = voxel.size ?? [1, 1, 1];
          const bucket = ensurePlainBucket({
            THREE,
            plainBuckets,
            options: { baseColor: DEFAULT_COLOR, emissive: null, opacity: DEFAULT_OPACITY }
          });
          appendBoxToAccumulator({ THREE, accumulator: bucket.accumulator, size: fallbackSize, matrix });
          break;
        }
        surfaces.forEach((surface) => {
          const region = atlas.regions.get(surface.textureId) ?? atlas.fallbackRegion;
          const geometry = new THREE.BufferGeometry();
          geometry.setAttribute("position", new THREE.Float32BufferAttribute(surface.geometry.positions, 3));
          geometry.setAttribute("normal", new THREE.Float32BufferAttribute(surface.geometry.normals, 3));
          const baseUVAttr = new THREE.Float32BufferAttribute(surface.geometry.uvs, 2);
          geometry.setAttribute("uv", baseUVAttr);
          geometry.setIndex(new THREE.BufferAttribute(surface.geometry.indices, 1));
          const customUVs = new Float32Array(baseUVAttr.count * 2);
          const uSpan = region.uMax - region.uMin;
          const vSpan = region.vMax - region.vMin;
          for (let i = 0; i < baseUVAttr.count; i += 1) {
            const baseU = baseUVAttr.getX(i);
            const baseV = baseUVAttr.getY(i);
            customUVs[i * 2] = region.uMin + baseU * uSpan;
            customUVs[i * 2 + 1] = region.vMin + baseV * vSpan;
          }
          texturedAccumulator.appendGeometry({ THREE, geometry, matrix, customUVs });
        });
        break;
      }
      case "combo_box":
        break;
      default:
        break;
    }
  });
  if (texturedAccumulator && texturedAccumulator.hasGeometry()) {
    const geometry = texturedAccumulator.build(THREE);
    const material = createTexturedMaterial(THREE, atlas);
    const mesh = new THREE.Mesh(geometry, material);
    group.add(mesh);
  }
  for (const bucket of plainBuckets.values()) {
    if (!bucket.accumulator.hasGeometry())
      continue;
    const geometry = bucket.accumulator.build(THREE);
    const mesh = new THREE.Mesh(geometry, bucket.material);
    group.add(mesh);
  }
  return group;
}
function appendTexturedBoxToAccumulator(options) {
  const { THREE, accumulator, atlas, box, matrix } = options;
  const size = options.size ?? box.size;
  const textureResolver = options.getTextureId ?? getFaceTextureId;
  const geometry = (0, import_primitives.createBoxGeometry)(THREE, size);
  const uvAttr = geometry.getAttribute("uv");
  const customUVs = new Float32Array(uvAttr.count * 2);
  customUVs.set(uvAttr.array);
  const faceVertexSets = (0, import_primitives.collectGroupVertexIndices)(geometry);
  if (faceVertexSets.length === 0) {
    geometry.dispose();
    return;
  }
  import_faces.FACE_ORDER.forEach((face, faceIndex) => {
    const resolvedId = textureResolver(box, face);
    const region = atlas.regions.get(resolvedId ?? FALLBACK_TEXTURE_ID) ?? atlas.fallbackRegion;
    const vertexIndices = faceVertexSets[faceIndex];
    if (!vertexIndices || vertexIndices.size === 0)
      return;
    const uSpan = region.uMax - region.uMin;
    const vSpan = region.vMax - region.vMin;
    vertexIndices.forEach((vertexIndex) => {
      const baseU = uvAttr.getX(vertexIndex);
      const baseV = uvAttr.getY(vertexIndex);
      customUVs[vertexIndex * 2] = region.uMin + baseU * uSpan;
      customUVs[vertexIndex * 2 + 1] = region.vMin + baseV * vSpan;
    });
  });
  accumulator.appendGeometry({ THREE, geometry, matrix, customUVs });
}
function appendBoxToAccumulator(options) {
  const { THREE, accumulator, size, matrix } = options;
  const geometry = (0, import_primitives.createBoxGeometry)(THREE, size);
  accumulator.appendGeometry({ THREE, geometry, matrix });
}
function getFaceTextureId(box, face) {
  switch (face) {
    case "top":
      return box.top ?? box.all ?? box.sides;
    case "bottom":
      return box.bottom ?? box.all ?? box.sides;
    case "north":
      return box.north ?? box.sides ?? box.all;
    case "south":
      return box.south ?? box.sides ?? box.all;
    case "east":
      return box.east ?? box.sides ?? box.all;
    case "west":
      return box.west ?? box.sides ?? box.all;
    default:
      return box.all;
  }
}
function getOverlayTextureId(box, face) {
  switch (face) {
    case "top":
      return box.topOverlay ?? box.allOverlay ?? box.sidesOverlay;
    case "bottom":
      return box.bottomOverlay ?? box.allOverlay ?? box.sidesOverlay;
    case "north":
      return box.northOverlay ?? box.sidesOverlay ?? box.allOverlay;
    case "south":
      return box.southOverlay ?? box.sidesOverlay ?? box.allOverlay;
    case "east":
      return box.eastOverlay ?? box.sidesOverlay ?? box.allOverlay;
    case "west":
      return box.westOverlay ?? box.sidesOverlay ?? box.allOverlay;
    default:
      return box.allOverlay ?? box.sidesOverlay;
  }
}
function createTexturedMaterial(THREE, atlas) {
  const params = {
    map: atlas.map,
    metalness: 0,
    roughness: 1,
    transparent: atlas.hasTransparency
  };
  if (atlas.emissiveMap) {
    params.emissiveMap = atlas.emissiveMap;
  }
  const material = new THREE.MeshStandardMaterial(params);
  if (atlas.emissiveMap) {
    material.emissive = new THREE.Color(1, 1, 1);
  }
  return material;
}
function createPlainMaterial(THREE, options) {
  const { baseColor, emissive, opacity } = options;
  const material = new THREE.MeshStandardMaterial({
    color: new THREE.Color(baseColor[0] / 255, baseColor[1] / 255, baseColor[2] / 255),
    metalness: 0,
    roughness: 1,
    transparent: opacity < 1,
    opacity
  });
  if (emissive) {
    material.emissive = new THREE.Color(emissive[0] / 255, emissive[1] / 255, emissive[2] / 255);
  }
  material.userData = material.userData ?? {};
  material.userData.gltfExtras = {
    playdropBaseColor: baseColor,
    playdropOpacity: opacity
  };
  return material;
}
function getPlainMaterial(context, options) {
  const key = plainMaterialKey(options);
  const cached = context.plainMaterialCache.get(key);
  if (cached) {
    return cached;
  }
  const material = createPlainMaterial(context.THREE, options);
  context.plainMaterialCache.set(key, material);
  return material;
}
function ensurePlainBucket({ THREE, plainBuckets, options }) {
  const key = plainMaterialKey(options);
  let bucket = plainBuckets.get(key);
  if (!bucket) {
    bucket = { accumulator: new MeshAccumulator(false), material: createPlainMaterial(THREE, options) };
    plainBuckets.set(key, bucket);
  }
  return bucket;
}
function plainMaterialKey(options) {
  const base = `${options.baseColor[0]}-${options.baseColor[1]}-${options.baseColor[2]}`;
  const emissive = options.emissive ? `${options.emissive[0]}-${options.emissive[1]}-${options.emissive[2]}` : "none";
  const opacity = options.opacity.toFixed(4);
  return `plain|${base}|${emissive}|${opacity}`;
}
class MeshAccumulator {
  constructor(withUVs) {
    this.positions = [];
    this.normals = [];
    this.indices = [];
    this.vertexCount = 0;
    this.uvs = withUVs ? [] : null;
  }
  appendGeometry(options) {
    const { THREE, geometry, matrix, customUVs } = options;
    const rawPositionAttr = geometry.getAttribute("position");
    const rawNormalAttr = geometry.getAttribute("normal");
    const rawUvAttr = geometry.getAttribute("uv");
    const indexAttr = geometry.index;
    if (!rawPositionAttr || !rawNormalAttr || !indexAttr) {
      geometry.dispose();
      return;
    }
    if (!(rawPositionAttr instanceof THREE.BufferAttribute) || !(rawNormalAttr instanceof THREE.BufferAttribute)) {
      geometry.dispose();
      return;
    }
    const positionAttr = rawPositionAttr;
    const normalAttr = rawNormalAttr;
    const uvAttr = rawUvAttr && rawUvAttr instanceof THREE.BufferAttribute ? rawUvAttr : null;
    if (!(indexAttr instanceof THREE.BufferAttribute)) {
      geometry.dispose();
      return;
    }
    const vertex = new THREE.Vector3();
    const normal = new THREE.Vector3();
    const uv = new THREE.Vector2();
    const normalMatrix = new THREE.Matrix3().getNormalMatrix(matrix);
    for (let i = 0; i < positionAttr.count; i += 1) {
      vertex.fromBufferAttribute(positionAttr, i);
      vertex.applyMatrix4(matrix);
      this.positions.push(vertex.x, vertex.y, vertex.z);
      normal.fromBufferAttribute(normalAttr, i);
      normal.applyMatrix3(normalMatrix).normalize();
      this.normals.push(normal.x, normal.y, normal.z);
      if (this.uvs) {
        if (customUVs) {
          this.uvs.push(customUVs[i * 2], customUVs[i * 2 + 1]);
        } else if (uvAttr) {
          uv.fromBufferAttribute(uvAttr, i);
          this.uvs.push(uv.x, uv.y);
        } else {
          this.uvs.push(0, 0);
        }
      }
    }
    const typedIndexAttr = indexAttr;
    const indexArray = typedIndexAttr.array;
    for (let i = 0; i < typedIndexAttr.count; i += 1) {
      this.indices.push(this.vertexCount + indexArray[i]);
    }
    this.vertexCount += positionAttr.count;
    geometry.dispose();
  }
  hasGeometry() {
    return this.positions.length > 0;
  }
  build(THREE) {
    const geometry = new THREE.BufferGeometry();
    geometry.setAttribute("position", new THREE.Float32BufferAttribute(this.positions, 3));
    geometry.setAttribute("normal", new THREE.Float32BufferAttribute(this.normals, 3));
    if (this.uvs) {
      geometry.setAttribute("uv", new THREE.Float32BufferAttribute(this.uvs, 2));
    }
    const indexArray = this.vertexCount > 65535 ? new Uint32Array(this.indices) : new Uint16Array(this.indices);
    geometry.setIndex(new THREE.BufferAttribute(indexArray, 1));
    geometry.computeBoundingBox();
    geometry.computeBoundingSphere();
    return geometry;
  }
}
const __testing = {
  buildTextureAtlas,
  collectUsedTextureIds,
  MeshAccumulator,
  createExportContext,
  buildSceneGraph,
  resolveAnimationClips
};
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
  __testing,
  createBoxelSceneGraph,
  exportModelToGLB
});
