use crate::geo::EnuFrame;
use crate::{glb, tmxb};
use rayon::prelude::*;
use serde_json::{json, Value};
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
const MAX_SSE: f64 = 16.0;
const BIG_GE: f64 = 1.0e7;
static TILE_BYTES: AtomicU64 = AtomicU64::new(0);
static SKIPPED: AtomicU64 = AtomicU64::new(0);
pub fn convert(scene_3mx: &Path, outdir: &Path) -> Result<Value, String> {
let scene_dir = scene_3mx.parent().unwrap_or(Path::new("."));
let text = std::fs::read_to_string(scene_3mx).map_err(|e| format!("read scene: {e}"))?;
let scene: Value = serde_json::from_str(&text).map_err(|e| format!("scene json: {e}"))?;
let layer = &scene["layers"][0];
let root_rel = layer["root"]
.as_str()
.ok_or("scene layer has no root")?
.to_string();
let origin: Vec<f64> = layer["SRSOrigin"]
.as_array()
.ok_or("scene layer has no SRSOrigin")?
.iter()
.map(|v| v.as_f64().unwrap_or(0.0))
.collect();
if origin.len() < 3 {
return Err(format!("SRSOrigin needs 3 components, got {}", origin.len()));
}
let srs = layer["SRS"].as_str().ok_or("scene layer has no SRS")?;
let params = crate::geo::parse_srs(srs)?;
let frame = EnuFrame::new([origin[0], origin[1], origin[2]], params);
let root_path = scene_dir.join(&root_rel);
let root_dir = root_path.parent().unwrap().to_path_buf();
let root_data = std::fs::read(&root_path).map_err(|e| format!("read root: {e}"))?;
let root_tmxb = tmxb::parse(&root_data)?;
let nodes = root_tmxb.header["nodes"].as_array().cloned().unwrap_or_default();
std::fs::create_dir_all(outdir).map_err(|e| format!("mkdir out: {e}"))?;
let total_tiles = AtomicU64::new(0);
let results: Vec<Result<Option<Value>, String>> = nodes
.par_iter()
.map(|node| build_external(node, &root_dir, scene_dir, outdir, &frame, &total_tiles))
.collect();
let mut children = Vec::new();
let mut failed = 0u64;
for r in results {
match r {
Ok(Some(t)) => children.push(t),
Ok(None) => {}
Err(e) => {
eprintln!("area failed: {e}");
failed += 1;
}
}
}
if children.is_empty() {
return Err("no areas produced any tiles".into());
}
let bv = union_boxes(&children);
let tileset = json!({
"asset": { "version": "1.1" },
"geometricError": BIG_GE,
"root": {
"transform": frame.root_transform().to_vec(),
"boundingVolume": { "box": bv },
"geometricError": BIG_GE,
"refine": "REPLACE",
"children": children,
}
});
std::fs::write(
outdir.join("tileset.json"),
serde_json::to_vec_pretty(&tileset).unwrap(),
)
.map_err(|e| format!("write tileset: {e}"))?;
Ok(json!({
"tiles": total_tiles.load(Ordering::Relaxed),
"areas_failed": failed,
"geometries_skipped": SKIPPED.load(Ordering::Relaxed),
"areas": nodes.len(),
"content_bytes": TILE_BYTES.load(Ordering::Relaxed),
}))
}
fn build_external(
node: &Value,
file_dir: &Path,
scene_dir: &Path,
outdir: &Path,
frame: &EnuFrame,
counter: &AtomicU64,
) -> Result<Option<Value>, String> {
let mut sub: Vec<Value> = Vec::new();
let mut base_dir: Option<PathBuf> = None;
let glb_seq = AtomicU64::new(0);
if let Some(child_paths) = node["children"].as_array() {
for cp in child_paths {
let Some(rel) = cp.as_str() else { continue };
let child_path = file_dir.join(rel);
let dir = child_path.parent().unwrap().to_path_buf();
let base = base_dir.get_or_insert(dir.clone());
if dir.as_path() != base.as_path() {
return Err(format!(
"area subtree spans multiple dirs ({} vs {}); flat-dir assumption broken",
base.display(),
dir.display()
));
}
let data = std::fs::read(&child_path)
.map_err(|e| format!("read {}: {e}", child_path.display()))?;
let cf = tmxb::parse(&data)?;
if let Some(cnodes) = cf.header["nodes"].as_array() {
for cn in cnodes {
if let Some(t) =
build_tile(cn, &cf, &dir, scene_dir, outdir, frame, counter, &glb_seq)?
{
sub.push(t);
}
}
}
}
}
let Some(base_dir) = base_dir else { return Ok(None) };
if sub.is_empty() {
return Ok(None);
}
let (ext_root, bv, ge) = if sub.len() == 1 {
let t = sub.into_iter().next().unwrap();
let bv = t["boundingVolume"]["box"].clone();
let ge = t["geometricError"].as_f64().unwrap_or(BIG_GE);
(t, bv, ge)
} else {
let bv = union_boxes(&sub);
let ge = sub
.iter()
.map(|t| t["geometricError"].as_f64().unwrap_or(0.0))
.fold(0.0, f64::max);
let root = json!({
"boundingVolume": { "box": bv.clone() },
"geometricError": ge,
"refine": "REPLACE",
"children": sub,
});
(root, json!(bv), ge)
};
let ext = json!({
"asset": { "version": "1.1" },
"geometricError": ge,
"root": ext_root,
});
let area_rel = base_dir.strip_prefix(scene_dir).unwrap_or(&base_dir);
let out_area = outdir.join(area_rel);
std::fs::create_dir_all(&out_area).map_err(|e| format!("mkdir {}: {e}", out_area.display()))?;
let ext_path = out_area.join("tileset.json");
std::fs::write(&ext_path, serde_json::to_vec(&ext).unwrap())
.map_err(|e| format!("write {}: {e}", ext_path.display()))?;
counter.fetch_add(1, Ordering::Relaxed);
let uri = area_rel.join("tileset.json").to_string_lossy().replace('\\', "/");
Ok(Some(json!({
"boundingVolume": { "box": bv },
"geometricError": ge,
"refine": "REPLACE",
"content": { "uri": uri },
})))
}
fn build_tile(
node: &Value,
file: &tmxb::Tmxb,
file_dir: &Path,
scene_dir: &Path,
outdir: &Path,
frame: &EnuFrame,
counter: &AtomicU64,
glb_seq: &AtomicU64,
) -> Result<Option<Value>, String> {
let bb_min = arr3(&node["bbMin"]);
let bb_max = arr3(&node["bbMax"]);
let (bv, diag) = enu_box(bb_min, bb_max, frame);
let msd = node["maxScreenDiameter"].as_f64().unwrap_or(0.0);
let own_ge = if msd > 0.0 { diag / msd * MAX_SSE } else { BIG_GE };
let mut content: Option<Value> = None;
if let Some(draws) = node["resources"].as_array() {
let prims = collect_prims(draws, file, frame);
if !prims.is_empty() {
let bytes = encode_glb(&prims)?;
let uri = write_glb(glb_seq, file_dir, scene_dir, outdir, &bytes)?;
TILE_BYTES.fetch_add(bytes.len() as u64, Ordering::Relaxed);
content = Some(json!({ "uri": uri }));
}
}
let mut children = Vec::new();
if let Some(child_paths) = node["children"].as_array() {
for cp in child_paths {
let Some(rel) = cp.as_str() else { continue };
let child_path = file_dir.join(rel);
let child_dir = child_path.parent().unwrap().to_path_buf();
if child_dir.as_path() != file_dir {
return Err(format!(
"child path leaves area dir ({}); flat-dir assumption broken",
child_path.display()
));
}
let data = std::fs::read(&child_path)
.map_err(|e| format!("read {}: {e}", child_path.display()))?;
let child_file = tmxb::parse(&data)?;
if let Some(cnodes) = child_file.header["nodes"].as_array() {
for cn in cnodes {
if let Some(t) = build_tile(
cn, &child_file, &child_dir, scene_dir, outdir, frame, counter, glb_seq,
)? {
children.push(t);
}
}
}
}
}
if content.is_none() && children.is_empty() {
return Ok(None);
}
let geometric_error = if children.is_empty() {
0.0
} else {
let max_child = children
.iter()
.map(|t| t["geometricError"].as_f64().unwrap_or(0.0))
.fold(0.0, f64::max);
own_ge.max(max_child)
};
counter.fetch_add(1, Ordering::Relaxed);
let mut tile = json!({
"boundingVolume": { "box": bv },
"geometricError": geometric_error,
"refine": "REPLACE",
});
if let Some(c) = content {
tile["content"] = c;
}
if !children.is_empty() {
tile["children"] = json!(children);
}
Ok(Some(tile))
}
enum Mode {
Lossless,
Quantize,
Raw,
}
fn mode() -> &'static Mode {
use std::sync::OnceLock;
static M: OnceLock<Mode> = OnceLock::new();
M.get_or_init(|| {
if std::env::var("TMX_RAW").is_ok() {
Mode::Raw
} else if std::env::var("TMX_QUANTIZE").is_ok() {
Mode::Quantize
} else {
Mode::Lossless
}
})
}
fn encode_glb(prims: &[glb::Prim]) -> Result<Vec<u8>, String> {
match mode() {
Mode::Raw => Ok(glb::write(prims)),
Mode::Lossless => crate::meshopt_glb::write(prims, false),
Mode::Quantize => crate::meshopt_glb::write(prims, true),
}
}
fn collect_prims(draws: &[Value], file: &tmxb::Tmxb, frame: &EnuFrame) -> Vec<glb::Prim> {
let mut prims = Vec::new();
for d in draws {
let Some(gid) = d.as_str() else { continue };
let Some(g) = file.geometries.iter().find(|g| g.id == gid) else {
eprintln!("draw references unknown geometry id {gid}");
SKIPPED.fetch_add(1, Ordering::Relaxed);
continue;
};
let mesh = match openctm::decode(&g.ctm) {
Ok(m) => m,
Err(e) => {
eprintln!("CTM decode {gid}: {e}");
SKIPPED.fetch_add(1, Ordering::Relaxed);
continue;
}
};
let positions: Vec<[f32; 3]> = mesh
.positions
.iter()
.map(|p| {
let e = frame.local_to_enu(*p);
[e[0], e[2], -e[1]]
})
.collect();
let jpeg = g
.texture_id
.as_ref()
.and_then(|t| file.textures.get(t))
.cloned();
let uvs = mesh
.uvs
.map(|uvs| uvs.iter().map(|t| [t[0], 1.0 - t[1]]).collect());
prims.push(glb::Prim {
positions,
normals: None,
uvs,
indices: mesh.indices,
jpeg,
});
}
prims
}
fn write_glb(
glb_seq: &AtomicU64,
file_dir: &Path,
scene_dir: &Path,
outdir: &Path,
bytes: &[u8],
) -> Result<String, String> {
let rel_dir = file_dir.strip_prefix(scene_dir).unwrap_or(file_dir);
let out_dir = outdir.join(rel_dir);
std::fs::create_dir_all(&out_dir).map_err(|e| format!("mkdir {}: {e}", out_dir.display()))?;
let n = glb_seq.fetch_add(1, Ordering::Relaxed);
let fname = format!("t{n}.glb");
std::fs::write(out_dir.join(&fname), bytes).map_err(|e| format!("write glb: {e}"))?;
Ok(fname)
}
fn arr3(v: &Value) -> [f32; 3] {
let a = v.as_array();
let g = |i: usize| a.and_then(|a| a.get(i)).and_then(|x| x.as_f64()).unwrap_or(0.0) as f32;
[g(0), g(1), g(2)]
}
fn enu_box(mn: [f32; 3], mx: [f32; 3], frame: &EnuFrame) -> (Vec<f64>, f64) {
let mut lo = [f64::INFINITY; 3];
let mut hi = [f64::NEG_INFINITY; 3];
for i in 0..8 {
let c = [
if i & 1 == 0 { mn[0] } else { mx[0] },
if i & 2 == 0 { mn[1] } else { mx[1] },
if i & 4 == 0 { mn[2] } else { mx[2] },
];
let e = frame.local_to_enu(c);
for k in 0..3 {
lo[k] = lo[k].min(e[k] as f64);
hi[k] = hi[k].max(e[k] as f64);
}
}
let center = [(lo[0] + hi[0]) / 2.0, (lo[1] + hi[1]) / 2.0, (lo[2] + hi[2]) / 2.0];
let half = [(hi[0] - lo[0]) / 2.0, (hi[1] - lo[1]) / 2.0, (hi[2] - lo[2]) / 2.0];
let diag = 2.0 * (half[0] * half[0] + half[1] * half[1] + half[2] * half[2]).sqrt();
let bx = vec![
center[0], center[1], center[2],
half[0], 0.0, 0.0,
0.0, half[1], 0.0,
0.0, 0.0, half[2],
];
(bx, diag)
}
fn union_boxes(children: &[Value]) -> Vec<f64> {
if children.is_empty() {
return vec![0.0; 12];
}
let mut lo = [f64::INFINITY; 3];
let mut hi = [f64::NEG_INFINITY; 3];
for c in children {
if let Some(b) = c["boundingVolume"]["box"].as_array() {
let g = |i: usize| b[i].as_f64().unwrap_or(0.0);
let center = [g(0), g(1), g(2)];
let half = [g(3), g(7), g(11)];
for k in 0..3 {
lo[k] = lo[k].min(center[k] - half[k]);
hi[k] = hi[k].max(center[k] + half[k]);
}
}
}
let center = [(lo[0] + hi[0]) / 2.0, (lo[1] + hi[1]) / 2.0, (lo[2] + hi[2]) / 2.0];
let half = [(hi[0] - lo[0]) / 2.0, (hi[1] - lo[1]) / 2.0, (hi[2] - lo[2]) / 2.0];
vec![
center[0], center[1], center[2],
half[0], 0.0, 0.0,
0.0, half[1], 0.0,
0.0, 0.0, half[2],
]
}