Viewer3D: route model loading by real URL extension

The loader branched on endsWith(".spz") and sent everything else to
GLTFLoader, which JSON.parse-crashed on any non-glTF asset (ply splats,
obj/fbx/pmx uploads, or splat URLs carrying query strings). Now the
extension is parsed from the URL pathname (query/hash-proof):
spz/ply/splat/ksplat -> Spark SplatMesh, glb/gltf (or no extension) ->
GLTFLoader, anything else -> placeholder shape with a single warn.
This commit is contained in:
Tanish Manku
2026-07-13 22:43:37 -07:00
parent 73b1a6be73
commit 1a7073341a
@@ -277,8 +277,23 @@ export function Viewer3D({
}
};
if (modelUrl.endsWith(".spz")) {
console.log("[Viewer3D] .spz format detected");
// Route by the URL's file extension, ignoring query strings / hashes
// (plain endsWith broke on CDN URLs with query params). Gaussian-splat
// formats go to Spark's SplatMesh; glTF goes to GLTFLoader; other model
// formats (obj/fbx/pmx…) have no loader here — show the fallback shape
// instead of letting GLTFLoader choke trying to JSON.parse binary data.
let urlExtension = "";
try {
const pathname = new URL(modelUrl, window.location.href).pathname;
urlExtension = (pathname.match(/\.([a-z0-9]+)$/i)?.[1] ?? "").toLowerCase();
} catch {
// Unparseable URL — fall through to the GLTF attempt below.
}
const SPLAT_EXTENSIONS = ["spz", "ply", "splat", "ksplat"];
const GLTF_EXTENSIONS = ["glb", "gltf", ""];
if (SPLAT_EXTENSIONS.includes(urlExtension)) {
console.log(`[Viewer3D] splat format detected (.${urlExtension})`);
new SplatMesh({
url: modelUrl,
onLoad: (mesh) => {
@@ -303,6 +318,15 @@ export function Viewer3D({
console.log("Splat loaded");
},
});
} else if (!GLTF_EXTENSIONS.includes(urlExtension)) {
// Known non-glTF model format with no loader wired here (obj, fbx,
// pmx, …). Show the fallback shape rather than spamming a parse error.
console.warn(
`[Viewer3D] unsupported model format ".${urlExtension}" — showing placeholder`,
);
if (cubeRef.current) {
cubeRef.current.visible = true;
}
} else {
const loader = new GLTFLoader();
// Anonymous CORS keeps the WebGL canvas untainted so thumbnail capture