Add GLB XKT pipeline and LOD controls
This commit is contained in:
+560
-15
@@ -25,7 +25,7 @@ from OCP.XCAFDoc import XCAFDoc_DocumentTool, XCAFDoc_ShapeTool
|
||||
|
||||
|
||||
CONVERTER_NAME = "NodeDcBimConverter"
|
||||
CONVERTER_VERSION = "0.4.7"
|
||||
CONVERTER_VERSION = "0.4.9"
|
||||
UPLOADS_DIR = Path(os.environ.get("NODEDC_BIM_CONVERTER_UPLOADS_DIR", "/beam/uploads")).resolve()
|
||||
POLL_INTERVAL_SECONDS = float(os.environ.get("NODEDC_BIM_CONVERTER_INTERVAL_SECONDS", "10"))
|
||||
PROCESS_ONCE = os.environ.get("NODEDC_BIM_CONVERTER_ONCE", "").lower() in {"1", "true", "yes"}
|
||||
@@ -37,6 +37,11 @@ REPROCESS_FAILED_ON_VERSION_CHANGE = os.environ.get(
|
||||
).lower() in {"1", "true", "yes"}
|
||||
XKT_ENABLED = os.environ.get("NODEDC_BIM_CONVERTER_XKT_ENABLED", "1").lower() not in {"0", "false", "no"}
|
||||
XKT_COMMAND = os.environ.get("NODEDC_BIM_CONVERTER_XKT_COMMAND", "xeokit-convert")
|
||||
GLB_XKT_ENABLED = os.environ.get("NODEDC_BIM_CONVERTER_GLB_XKT_ENABLED", "1").lower() not in {
|
||||
"0",
|
||||
"false",
|
||||
"no",
|
||||
}
|
||||
GLB_DRACO_ENABLED = os.environ.get("NODEDC_BIM_CONVERTER_GLB_DRACO_ENABLED", "1").lower() not in {
|
||||
"0",
|
||||
"false",
|
||||
@@ -44,6 +49,11 @@ GLB_DRACO_ENABLED = os.environ.get("NODEDC_BIM_CONVERTER_GLB_DRACO_ENABLED", "1"
|
||||
}
|
||||
GLTF_TRANSFORM_COMMAND = os.environ.get("NODEDC_BIM_CONVERTER_GLTF_TRANSFORM_COMMAND", "gltf-transform")
|
||||
NODE_OPTIONS = os.environ.get("NODE_OPTIONS", "--max-old-space-size=8192")
|
||||
GLB_LOD_ENABLED = os.environ.get("NODEDC_BIM_CONVERTER_GLB_LOD_ENABLED", "1").lower() not in {
|
||||
"0",
|
||||
"false",
|
||||
"no",
|
||||
}
|
||||
STEP_MESH_LINEAR_DEFLECTION = float(os.environ.get("NODEDC_BIM_CONVERTER_STEP_LINEAR_DEFLECTION", "0.1"))
|
||||
STEP_MESH_ANGULAR_DEFLECTION = float(os.environ.get("NODEDC_BIM_CONVERTER_STEP_ANGULAR_DEFLECTION", "0.1"))
|
||||
PROCESSING_STALE_SECONDS = float(os.environ.get("NODEDC_BIM_CONVERTER_PROCESSING_STALE_SECONDS", "60"))
|
||||
@@ -52,12 +62,41 @@ CONVERSION_TIMEOUT_SECONDS = float(os.environ.get("NODEDC_BIM_CONVERTER_TIMEOUT_
|
||||
GLB_DRACO_TIMEOUT_SECONDS = float(
|
||||
os.environ.get("NODEDC_BIM_CONVERTER_GLB_DRACO_TIMEOUT_SECONDS", str(CONVERSION_TIMEOUT_SECONDS))
|
||||
)
|
||||
GLB_LOD_TIMEOUT_SECONDS = float(
|
||||
os.environ.get("NODEDC_BIM_CONVERTER_GLB_LOD_TIMEOUT_SECONDS", str(max(CONVERSION_TIMEOUT_SECONDS, 900)))
|
||||
)
|
||||
XKT_TIMEOUT_SECONDS = float(
|
||||
os.environ.get("NODEDC_BIM_CONVERTER_XKT_TIMEOUT_SECONDS", str(max(CONVERSION_TIMEOUT_SECONDS, 900)))
|
||||
)
|
||||
XCAF_TIMEOUT_SECONDS = float(os.environ.get("NODEDC_BIM_CONVERTER_XCAF_TIMEOUT_SECONDS", str(CONVERSION_TIMEOUT_SECONDS)))
|
||||
CADQUERY_TIMEOUT_SECONDS = float(
|
||||
os.environ.get("NODEDC_BIM_CONVERTER_CADQUERY_TIMEOUT_SECONDS", str(CONVERSION_TIMEOUT_SECONDS))
|
||||
)
|
||||
STEP_EXTENSIONS = {".step", ".stp"}
|
||||
GLB_EXTENSIONS = {".glb"}
|
||||
GLB_LOD_PROFILES = [
|
||||
{
|
||||
"id": "full",
|
||||
"label": "Полный",
|
||||
"suffix": "lod-full",
|
||||
"ratio": None,
|
||||
"error": None,
|
||||
},
|
||||
{
|
||||
"id": "desktop",
|
||||
"label": "Средний",
|
||||
"suffix": "lod-desktop",
|
||||
"ratio": 0.35,
|
||||
"error": 0.005,
|
||||
},
|
||||
{
|
||||
"id": "mobile",
|
||||
"label": "Мобильный",
|
||||
"suffix": "lod-mobile",
|
||||
"ratio": 0.12,
|
||||
"error": 0.02,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def now_iso() -> str:
|
||||
@@ -123,6 +162,26 @@ def draco_optimization_attempt_count(manifest: dict[str, Any]) -> int:
|
||||
return 0
|
||||
|
||||
|
||||
def xkt_conversion_attempt_count(manifest: dict[str, Any]) -> int:
|
||||
conversion = manifest.get("xktConversion")
|
||||
if not isinstance(conversion, dict):
|
||||
return 0
|
||||
try:
|
||||
return max(0, int(conversion.get("attempts") or 0))
|
||||
except Exception:
|
||||
return 0
|
||||
|
||||
|
||||
def lod_generation_attempt_count(manifest: dict[str, Any]) -> int:
|
||||
generation = manifest.get("lodGeneration")
|
||||
if not isinstance(generation, dict):
|
||||
return 0
|
||||
try:
|
||||
return max(0, int(generation.get("attempts") or 0))
|
||||
except Exception:
|
||||
return 0
|
||||
|
||||
|
||||
def update_asset_manifest_version(source_path: Path, version_payload: dict[str, Any]) -> None:
|
||||
asset_path = asset_manifest_path(source_path)
|
||||
if not asset_path:
|
||||
@@ -740,15 +799,23 @@ def convert_glb_to_xkt(glb_path: Path, xkt_path: Path) -> dict[str, Any]:
|
||||
|
||||
print(f"[{CONVERTER_NAME}] XKT export {glb_path.name}", flush=True)
|
||||
with log_path.open("w", encoding="utf-8", errors="replace") as log_file:
|
||||
result = subprocess.run(
|
||||
command,
|
||||
cwd=str(glb_path.parent),
|
||||
env=env,
|
||||
stdout=log_file,
|
||||
stderr=subprocess.STDOUT,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
try:
|
||||
result = subprocess.run(
|
||||
command,
|
||||
cwd=str(glb_path.parent),
|
||||
env=env,
|
||||
stdout=log_file,
|
||||
stderr=subprocess.STDOUT,
|
||||
text=True,
|
||||
timeout=XKT_TIMEOUT_SECONDS,
|
||||
check=False,
|
||||
)
|
||||
except subprocess.TimeoutExpired as exc:
|
||||
try:
|
||||
tmp_xkt_path.unlink()
|
||||
except Exception:
|
||||
pass
|
||||
raise TimeoutError(f"XKT export timed out after {int(XKT_TIMEOUT_SECONDS)} seconds.") from exc
|
||||
|
||||
if result.returncode != 0:
|
||||
tail = read_log_tail(log_path)
|
||||
@@ -1196,6 +1263,29 @@ def glb_draco_artifact_path(source_path: Path) -> Path:
|
||||
return source_path.with_name(f"{source_path.stem}.draco.glb")
|
||||
|
||||
|
||||
def glb_xkt_artifact_path(source_path: Path) -> Path:
|
||||
return source_path.with_name(f"{source_path.stem}.xkt")
|
||||
|
||||
|
||||
def glb_lod_artifact_path(source_path: Path, profile: dict[str, Any]) -> Path:
|
||||
return source_path.with_name(f"{source_path.stem}.{profile['suffix']}.glb")
|
||||
|
||||
|
||||
def lod_records_ready(lods: Any) -> bool:
|
||||
if not isinstance(lods, list) or len(lods) < len(GLB_LOD_PROFILES):
|
||||
return False
|
||||
by_id = {
|
||||
str(item.get("id")): item
|
||||
for item in lods
|
||||
if isinstance(item, dict) and item.get("id")
|
||||
}
|
||||
for profile in GLB_LOD_PROFILES:
|
||||
item = by_id.get(profile["id"])
|
||||
if not item or not upload_src_exists(item.get("artifactSrc")):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def should_optimize_glb(source_path: Path, manifest: dict[str, Any], artifact_path: Path) -> bool:
|
||||
if not GLB_DRACO_ENABLED or not source_path.exists() or not manifest:
|
||||
return False
|
||||
@@ -1331,18 +1421,32 @@ def optimize_glb_one(source_path: Path) -> bool:
|
||||
|
||||
source_size = source_path.stat().st_size
|
||||
artifact_size = artifact_path.stat().st_size
|
||||
draco_artifact_src = src_from_path(artifact_path)
|
||||
existing_artifact_src = manifest.get("artifactSrc")
|
||||
existing_artifact_type = manifest.get("artifactType") or manifest.get("targetFormat")
|
||||
preserve_xkt_artifact = (
|
||||
existing_artifact_type == "xkt"
|
||||
and isinstance(existing_artifact_src, str)
|
||||
and upload_src_exists(existing_artifact_src)
|
||||
)
|
||||
preferred_artifact_src = existing_artifact_src if preserve_xkt_artifact else draco_artifact_src
|
||||
preferred_artifact_type = "xkt" if preserve_xkt_artifact else "gltf"
|
||||
ready_manifest = {
|
||||
**base_manifest,
|
||||
"artifactSha256": calculate_file_sha256(artifact_path),
|
||||
"artifactSrc": src_from_path(artifact_path),
|
||||
"artifactType": "gltf",
|
||||
"artifactSrc": preferred_artifact_src,
|
||||
"artifactType": preferred_artifact_type,
|
||||
"dracoArtifactSrc": draco_artifact_src,
|
||||
"dracoArtifactType": "gltf",
|
||||
"fallbackArtifactSrc": source_src,
|
||||
"fallbackArtifactType": "gltf",
|
||||
"message": "Model is ready. GLB geometry optimized with Draco.",
|
||||
"targetFormat": "gltf",
|
||||
"targetFormat": "xkt" if preserve_xkt_artifact else "gltf",
|
||||
"dracoOptimization": {
|
||||
"status": "ready",
|
||||
"attempts": attempt,
|
||||
"artifactSrc": draco_artifact_src,
|
||||
"artifactType": "gltf",
|
||||
"converterName": CONVERTER_NAME,
|
||||
"converterVersion": CONVERTER_VERSION,
|
||||
"command": command,
|
||||
@@ -1358,7 +1462,7 @@ def optimize_glb_one(source_path: Path) -> bool:
|
||||
write_json_atomic(manifest_file, ready_manifest)
|
||||
update_asset_manifest_version(source_path, ready_manifest)
|
||||
print(
|
||||
f"[{CONVERTER_NAME}] GLB Draco ready {ready_manifest['artifactSrc']} "
|
||||
f"[{CONVERTER_NAME}] GLB Draco ready {draco_artifact_src} "
|
||||
f"({source_size} -> {artifact_size} bytes)",
|
||||
flush=True,
|
||||
)
|
||||
@@ -1389,6 +1493,445 @@ def optimize_glb_one(source_path: Path) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def should_convert_glb_to_xkt(source_path: Path, manifest: dict[str, Any], xkt_path: Path) -> bool:
|
||||
if not XKT_ENABLED or not GLB_XKT_ENABLED or not source_path.exists() or not manifest:
|
||||
return False
|
||||
if source_path.name.endswith(".draco.glb") or ".lod-" in source_path.name:
|
||||
return False
|
||||
source_format = str(manifest.get("sourceFormat") or "").replace(".", "").lower()
|
||||
if source_format != "glb":
|
||||
return False
|
||||
if manifest.get("status") not in {None, "ready"}:
|
||||
return False
|
||||
|
||||
conversion = manifest.get("xktConversion")
|
||||
if not isinstance(conversion, dict):
|
||||
conversion = {}
|
||||
|
||||
conversion_status = conversion.get("status")
|
||||
conversion_version = conversion.get("converterVersion")
|
||||
artifact_src = manifest.get("artifactSrc")
|
||||
artifact_type = manifest.get("artifactType") or manifest.get("targetFormat")
|
||||
artifact_ready = artifact_type == "xkt" and isinstance(artifact_src, str) and upload_src_exists(artifact_src)
|
||||
if conversion_status == "ready" and artifact_ready:
|
||||
return REPROCESS_READY_ON_VERSION_CHANGE and conversion_version != CONVERTER_VERSION
|
||||
if conversion_status == "ready" and xkt_path.exists() and not artifact_ready:
|
||||
return True
|
||||
|
||||
if conversion_status == "failed":
|
||||
attempts = xkt_conversion_attempt_count(manifest)
|
||||
if attempts >= CONVERSION_MAX_ATTEMPTS:
|
||||
return REPROCESS_FAILED_ON_VERSION_CHANGE and conversion_version != CONVERTER_VERSION
|
||||
return True
|
||||
|
||||
if conversion_status == "processing":
|
||||
updated_at = conversion.get("updatedAt") or manifest.get("updatedAt")
|
||||
if updated_at:
|
||||
try:
|
||||
updated = datetime.fromisoformat(str(updated_at).replace("Z", "+00:00"))
|
||||
if (datetime.now(timezone.utc) - updated).total_seconds() < PROCESSING_STALE_SECONDS:
|
||||
return False
|
||||
except Exception:
|
||||
pass
|
||||
return True
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def convert_glb_to_xkt_one(source_path: Path) -> bool:
|
||||
manifest_file = manifest_path(source_path)
|
||||
if not manifest_file.exists():
|
||||
return False
|
||||
|
||||
manifest = read_json(manifest_file)
|
||||
xkt_path = glb_xkt_artifact_path(source_path)
|
||||
if not should_convert_glb_to_xkt(source_path, manifest, xkt_path):
|
||||
return False
|
||||
|
||||
source_src = src_from_path(source_path)
|
||||
base_manifest = {
|
||||
**manifest,
|
||||
"createdAt": manifest.get("createdAt") or now_iso(),
|
||||
"downloadSrc": manifest.get("downloadSrc") or source_src,
|
||||
"sourceFormat": "glb",
|
||||
"sourceSrc": manifest.get("sourceSrc") or source_src,
|
||||
"status": "ready",
|
||||
}
|
||||
|
||||
attempts = 0
|
||||
conversion = manifest.get("xktConversion")
|
||||
if isinstance(conversion, dict) and conversion.get("converterVersion") == CONVERTER_VERSION:
|
||||
attempts = xkt_conversion_attempt_count(manifest)
|
||||
if isinstance(conversion, dict) and conversion.get("status") == "processing" and attempts >= CONVERSION_MAX_ATTEMPTS:
|
||||
failed_manifest = {
|
||||
**base_manifest,
|
||||
"message": "Model is ready. XKT conversion did not finish; GLB fallback is used.",
|
||||
"xktConversion": {
|
||||
**conversion,
|
||||
"status": "failed",
|
||||
"attempts": attempts,
|
||||
"converterName": CONVERTER_NAME,
|
||||
"converterVersion": CONVERTER_VERSION,
|
||||
"error": f"XKT conversion stopped before completion after {attempts} attempt(s).",
|
||||
"updatedAt": now_iso(),
|
||||
},
|
||||
"updatedAt": now_iso(),
|
||||
}
|
||||
write_json_atomic(manifest_file, failed_manifest)
|
||||
update_asset_manifest_version(source_path, failed_manifest)
|
||||
print(
|
||||
f"[{CONVERTER_NAME}] GLB XKT failed {source_path}: max attempts reached ({attempts})",
|
||||
file=sys.stderr,
|
||||
flush=True,
|
||||
)
|
||||
return False
|
||||
|
||||
attempt = attempts + 1
|
||||
command = [
|
||||
XKT_COMMAND,
|
||||
"-s",
|
||||
str(source_path),
|
||||
"-f",
|
||||
"glb",
|
||||
"-o",
|
||||
str(xkt_path.with_name(f"{xkt_path.name}.tmp")),
|
||||
"-t",
|
||||
"-n",
|
||||
"-e",
|
||||
"0",
|
||||
"-b",
|
||||
]
|
||||
processing_manifest = {
|
||||
**base_manifest,
|
||||
"message": "Model is ready. Converting GLB to XKT viewer artifact.",
|
||||
"targetFormat": "xkt",
|
||||
"xktConversion": {
|
||||
"status": "processing",
|
||||
"attempts": attempt,
|
||||
"converterName": CONVERTER_NAME,
|
||||
"converterVersion": CONVERTER_VERSION,
|
||||
"command": command,
|
||||
"updatedAt": now_iso(),
|
||||
},
|
||||
"updatedAt": now_iso(),
|
||||
}
|
||||
write_json_atomic(manifest_file, processing_manifest)
|
||||
update_asset_manifest_version(source_path, processing_manifest)
|
||||
|
||||
print(f"[{CONVERTER_NAME}] converting GLB to XKT {source_path}", flush=True)
|
||||
try:
|
||||
xkt_stats = convert_glb_to_xkt(source_path, xkt_path)
|
||||
source_size = source_path.stat().st_size
|
||||
artifact_size = xkt_path.stat().st_size
|
||||
xkt_artifact_src = src_from_path(xkt_path)
|
||||
fallback_artifact_src = (
|
||||
manifest.get("dracoArtifactSrc")
|
||||
or (
|
||||
manifest.get("artifactSrc")
|
||||
if (manifest.get("artifactType") or manifest.get("targetFormat")) == "gltf"
|
||||
else None
|
||||
)
|
||||
or source_src
|
||||
)
|
||||
ready_manifest = {
|
||||
**base_manifest,
|
||||
"artifactSha256": calculate_file_sha256(xkt_path),
|
||||
"artifactSrc": xkt_artifact_src,
|
||||
"artifactType": "xkt",
|
||||
"fallbackArtifactSrc": fallback_artifact_src,
|
||||
"fallbackArtifactType": "gltf",
|
||||
"glbArtifactSrc": source_src,
|
||||
"targetFormat": "xkt",
|
||||
"xktArtifactSrc": xkt_artifact_src,
|
||||
"xkt": xkt_stats,
|
||||
"xktConversion": {
|
||||
"status": "ready",
|
||||
"attempts": attempt,
|
||||
"artifactSrc": xkt_artifact_src,
|
||||
"artifactType": "xkt",
|
||||
"converterName": CONVERTER_NAME,
|
||||
"converterVersion": CONVERTER_VERSION,
|
||||
"command": command,
|
||||
"sourceSize": source_size,
|
||||
"artifactSize": artifact_size,
|
||||
"compressionRatio": round(artifact_size / source_size, 6) if source_size else None,
|
||||
"updatedAt": now_iso(),
|
||||
},
|
||||
"message": "Model is ready. XKT viewer artifact generated.",
|
||||
"updatedAt": now_iso(),
|
||||
}
|
||||
write_json_atomic(manifest_file, ready_manifest)
|
||||
update_asset_manifest_version(source_path, ready_manifest)
|
||||
print(
|
||||
f"[{CONVERTER_NAME}] GLB XKT ready {xkt_artifact_src} "
|
||||
f"({source_size} -> {artifact_size} bytes)",
|
||||
flush=True,
|
||||
)
|
||||
return True
|
||||
except Exception as exc:
|
||||
tmp_path = xkt_path.with_name(f"{xkt_path.name}.tmp")
|
||||
if tmp_path.exists():
|
||||
try:
|
||||
tmp_path.unlink()
|
||||
except Exception:
|
||||
pass
|
||||
failed_manifest = {
|
||||
**base_manifest,
|
||||
"message": "Model is ready. XKT conversion failed; GLB fallback is used.",
|
||||
"xktConversion": {
|
||||
"status": "failed",
|
||||
"attempts": attempt,
|
||||
"converterName": CONVERTER_NAME,
|
||||
"converterVersion": CONVERTER_VERSION,
|
||||
"command": command,
|
||||
"error": str(exc),
|
||||
"updatedAt": now_iso(),
|
||||
},
|
||||
"updatedAt": now_iso(),
|
||||
}
|
||||
write_json_atomic(manifest_file, failed_manifest)
|
||||
update_asset_manifest_version(source_path, failed_manifest)
|
||||
print(f"[{CONVERTER_NAME}] GLB XKT failed {source_path}: {exc}", file=sys.stderr, flush=True)
|
||||
return False
|
||||
|
||||
|
||||
def should_generate_glb_lods(source_path: Path, manifest: dict[str, Any]) -> bool:
|
||||
if not GLB_LOD_ENABLED or not source_path.exists() or not manifest:
|
||||
return False
|
||||
if source_path.name.endswith(".draco.glb") or ".lod-" in source_path.name:
|
||||
return False
|
||||
source_format = str(manifest.get("sourceFormat") or "").replace(".", "").lower()
|
||||
if source_format != "glb":
|
||||
return False
|
||||
if manifest.get("status") not in {None, "ready"}:
|
||||
return False
|
||||
|
||||
generation = manifest.get("lodGeneration")
|
||||
if not isinstance(generation, dict):
|
||||
return False
|
||||
|
||||
generation_status = generation.get("status")
|
||||
generation_version = generation.get("converterVersion")
|
||||
if generation_status == "ready" and lod_records_ready(manifest.get("lods")):
|
||||
return REPROCESS_READY_ON_VERSION_CHANGE and generation_version != CONVERTER_VERSION
|
||||
|
||||
if generation_status == "failed":
|
||||
attempts = lod_generation_attempt_count(manifest)
|
||||
if attempts >= CONVERSION_MAX_ATTEMPTS:
|
||||
return REPROCESS_FAILED_ON_VERSION_CHANGE and generation_version != CONVERTER_VERSION
|
||||
return True
|
||||
|
||||
if generation_status == "processing":
|
||||
updated_at = generation.get("updatedAt") or manifest.get("updatedAt")
|
||||
if updated_at:
|
||||
try:
|
||||
updated = datetime.fromisoformat(str(updated_at).replace("Z", "+00:00"))
|
||||
if (datetime.now(timezone.utc) - updated).total_seconds() < PROCESSING_STALE_SECONDS:
|
||||
return False
|
||||
except Exception:
|
||||
pass
|
||||
return True
|
||||
|
||||
return generation_status == "queued"
|
||||
|
||||
|
||||
def glb_lod_command(source_path: Path, target_path: Path, profile: dict[str, Any]) -> list[str]:
|
||||
if profile.get("ratio") is None:
|
||||
return [GLTF_TRANSFORM_COMMAND, "draco", str(source_path), str(target_path)]
|
||||
return [
|
||||
GLTF_TRANSFORM_COMMAND,
|
||||
"optimize",
|
||||
str(source_path),
|
||||
str(target_path),
|
||||
"--compress",
|
||||
"draco",
|
||||
"--texture-compress",
|
||||
"false",
|
||||
"--simplify-ratio",
|
||||
str(profile["ratio"]),
|
||||
"--simplify-error",
|
||||
str(profile["error"]),
|
||||
]
|
||||
|
||||
|
||||
def run_gltf_transform_lod(source_path: Path, target_path: Path, profile: dict[str, Any]) -> tuple[list[str], str, str]:
|
||||
tmp_path = target_path.with_suffix(".tmp.glb")
|
||||
if tmp_path.exists():
|
||||
tmp_path.unlink()
|
||||
command = glb_lod_command(source_path, tmp_path, profile)
|
||||
run_env = os.environ.copy()
|
||||
run_env["NODE_OPTIONS"] = NODE_OPTIONS
|
||||
result = subprocess.run(
|
||||
command,
|
||||
cwd=str(Path(__file__).resolve().parent),
|
||||
env=run_env,
|
||||
text=True,
|
||||
capture_output=True,
|
||||
timeout=GLB_LOD_TIMEOUT_SECONDS,
|
||||
check=False,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError(
|
||||
limited_process_output(result.stderr)
|
||||
or limited_process_output(result.stdout)
|
||||
or f"gltf-transform exited with code {result.returncode}"
|
||||
)
|
||||
if not tmp_path.exists() or tmp_path.stat().st_size <= 0:
|
||||
raise RuntimeError("gltf-transform did not produce a LOD GLB artifact")
|
||||
tmp_path.replace(target_path)
|
||||
return command, limited_process_output(result.stdout), limited_process_output(result.stderr)
|
||||
|
||||
|
||||
def generate_glb_lods_one(source_path: Path) -> bool:
|
||||
manifest_file = manifest_path(source_path)
|
||||
if not manifest_file.exists():
|
||||
return False
|
||||
|
||||
manifest = read_json(manifest_file)
|
||||
if not should_generate_glb_lods(source_path, manifest):
|
||||
return False
|
||||
|
||||
source_src = src_from_path(source_path)
|
||||
base_manifest = {
|
||||
**manifest,
|
||||
"createdAt": manifest.get("createdAt") or now_iso(),
|
||||
"downloadSrc": manifest.get("downloadSrc") or source_src,
|
||||
"sourceFormat": "glb",
|
||||
"sourceSrc": manifest.get("sourceSrc") or source_src,
|
||||
"status": "ready",
|
||||
}
|
||||
|
||||
attempts = 0
|
||||
generation = manifest.get("lodGeneration")
|
||||
if isinstance(generation, dict) and generation.get("converterVersion") == CONVERTER_VERSION:
|
||||
attempts = lod_generation_attempt_count(manifest)
|
||||
if isinstance(generation, dict) and generation.get("status") == "processing" and attempts >= CONVERSION_MAX_ATTEMPTS:
|
||||
failed_manifest = {
|
||||
**base_manifest,
|
||||
"message": "Model is ready. LOD generation did not finish.",
|
||||
"lodGeneration": {
|
||||
**generation,
|
||||
"status": "failed",
|
||||
"attempts": attempts,
|
||||
"converterName": CONVERTER_NAME,
|
||||
"converterVersion": CONVERTER_VERSION,
|
||||
"error": f"LOD generation stopped before completion after {attempts} attempt(s).",
|
||||
"updatedAt": now_iso(),
|
||||
},
|
||||
"updatedAt": now_iso(),
|
||||
}
|
||||
write_json_atomic(manifest_file, failed_manifest)
|
||||
update_asset_manifest_version(source_path, failed_manifest)
|
||||
print(
|
||||
f"[{CONVERTER_NAME}] GLB LOD failed {source_path}: max attempts reached ({attempts})",
|
||||
file=sys.stderr,
|
||||
flush=True,
|
||||
)
|
||||
return False
|
||||
|
||||
attempt = attempts + 1
|
||||
processing_manifest = {
|
||||
**base_manifest,
|
||||
"message": "Model is ready. Generating GLB LOD levels.",
|
||||
"lodGeneration": {
|
||||
**(generation if isinstance(generation, dict) else {}),
|
||||
"status": "processing",
|
||||
"attempts": attempt,
|
||||
"converterName": CONVERTER_NAME,
|
||||
"converterVersion": CONVERTER_VERSION,
|
||||
"profiles": [
|
||||
{key: profile[key] for key in ("id", "label", "ratio", "error") if key in profile}
|
||||
for profile in GLB_LOD_PROFILES
|
||||
],
|
||||
"updatedAt": now_iso(),
|
||||
},
|
||||
"updatedAt": now_iso(),
|
||||
}
|
||||
write_json_atomic(manifest_file, processing_manifest)
|
||||
update_asset_manifest_version(source_path, processing_manifest)
|
||||
|
||||
source_size = source_path.stat().st_size
|
||||
records = []
|
||||
commands = []
|
||||
print(f"[{CONVERTER_NAME}] generating GLB LODs {source_path}", flush=True)
|
||||
|
||||
try:
|
||||
for profile in GLB_LOD_PROFILES:
|
||||
artifact_path = glb_lod_artifact_path(source_path, profile)
|
||||
command, stdout, stderr = run_gltf_transform_lod(source_path, artifact_path, profile)
|
||||
commands.append({
|
||||
"id": profile["id"],
|
||||
"command": command,
|
||||
"stdout": stdout,
|
||||
"stderr": stderr,
|
||||
})
|
||||
artifact_size = artifact_path.stat().st_size
|
||||
records.append({
|
||||
"id": profile["id"],
|
||||
"label": profile["label"],
|
||||
"artifactSrc": src_from_path(artifact_path),
|
||||
"type": "gltf",
|
||||
"ratio": profile.get("ratio"),
|
||||
"error": profile.get("error"),
|
||||
"size": artifact_size,
|
||||
"sha256": calculate_file_sha256(artifact_path),
|
||||
"sourceSize": source_size,
|
||||
"compressionRatio": round(artifact_size / source_size, 6) if source_size else None,
|
||||
"generatedAt": now_iso(),
|
||||
})
|
||||
|
||||
ready_manifest = {
|
||||
**base_manifest,
|
||||
"lods": records,
|
||||
"lodGeneration": {
|
||||
"status": "ready",
|
||||
"attempts": attempt,
|
||||
"converterName": CONVERTER_NAME,
|
||||
"converterVersion": CONVERTER_VERSION,
|
||||
"commands": commands,
|
||||
"sourceSize": source_size,
|
||||
"updatedAt": now_iso(),
|
||||
},
|
||||
"message": "Model is ready. GLB LOD levels generated.",
|
||||
"updatedAt": now_iso(),
|
||||
}
|
||||
write_json_atomic(manifest_file, ready_manifest)
|
||||
update_asset_manifest_version(source_path, ready_manifest)
|
||||
print(
|
||||
f"[{CONVERTER_NAME}] GLB LOD ready {source_src}: "
|
||||
+ ", ".join(f"{item['id']}={item['size']}" for item in records),
|
||||
flush=True,
|
||||
)
|
||||
return True
|
||||
except Exception as exc:
|
||||
for profile in GLB_LOD_PROFILES:
|
||||
tmp_path = glb_lod_artifact_path(source_path, profile).with_suffix(".tmp.glb")
|
||||
if tmp_path.exists():
|
||||
try:
|
||||
tmp_path.unlink()
|
||||
except Exception:
|
||||
pass
|
||||
failed_manifest = {
|
||||
**base_manifest,
|
||||
"message": "Model is ready. LOD generation failed.",
|
||||
"lodGeneration": {
|
||||
**(generation if isinstance(generation, dict) else {}),
|
||||
"status": "failed",
|
||||
"attempts": attempt,
|
||||
"converterName": CONVERTER_NAME,
|
||||
"converterVersion": CONVERTER_VERSION,
|
||||
"commands": commands,
|
||||
"error": str(exc),
|
||||
"updatedAt": now_iso(),
|
||||
},
|
||||
"updatedAt": now_iso(),
|
||||
}
|
||||
write_json_atomic(manifest_file, failed_manifest)
|
||||
update_asset_manifest_version(source_path, failed_manifest)
|
||||
print(f"[{CONVERTER_NAME}] GLB LOD failed {source_path}: {exc}", file=sys.stderr, flush=True)
|
||||
return False
|
||||
|
||||
|
||||
def scan_once() -> int:
|
||||
if not UPLOADS_DIR.exists():
|
||||
print(f"[{CONVERTER_NAME}] uploads dir does not exist: {UPLOADS_DIR}", flush=True)
|
||||
@@ -1403,7 +1946,9 @@ def scan_once() -> int:
|
||||
if suffix in STEP_EXTENSIONS:
|
||||
did_process = process_one(source_path)
|
||||
elif suffix in GLB_EXTENSIONS:
|
||||
did_process = optimize_glb_one(source_path)
|
||||
did_process = convert_glb_to_xkt_one(source_path)
|
||||
did_process = optimize_glb_one(source_path) or did_process
|
||||
did_process = generate_glb_lods_one(source_path) or did_process
|
||||
if did_process:
|
||||
processed += 1
|
||||
return processed
|
||||
|
||||
Reference in New Issue
Block a user