232 lines
9.2 KiB
GDScript
232 lines
9.2 KiB
GDScript
@tool
|
|
extends RefCounted
|
|
class_name CghImportRouter
|
|
## Routes a downloaded file to the right importer. CRITICAL: it routes by the
|
|
## asset's CATEGORY first, not just the extension — an `.exr` can be EITHER an
|
|
## HDRI panorama (hdri category) OR a flipbook sprite-sheet (flipbooks category),
|
|
## so extension alone is ambiguous and used to send flipbook .exr sheets to the
|
|
## HDRI importer (sky), which is why flipbooks "imported but never played".
|
|
##
|
|
## After a successful live-instance into the open scene, the source file is ALSO
|
|
## copied into res://CGHEVEN/<category>/ so it shows up in the FileSystem dock and
|
|
## the user can find / reuse it (Godot only imports files that live under res://).
|
|
|
|
const RES_ROOT := "res://CGHEVEN"
|
|
|
|
# Model formats Godot's EDITOR can import (into a PackedScene, or a Mesh for .obj) — far
|
|
# more than the runtime GLTFDocument path, which only handles glTF. Order = most reliable
|
|
# first (used to pick the best model inside a zip, like the Blender addon's priority).
|
|
const EDITOR_MODEL_PRIORITY := ["glb", "gltf", "fbx", "dae", "obj"]
|
|
|
|
static func import_file(path: String, category_slug: String) -> String:
|
|
var ext := path.get_extension().to_lower()
|
|
|
|
# unpack archives first, then re-route on the inner importable file
|
|
if ext == "zip":
|
|
var inner := _extract_zip(path)
|
|
if inner == "":
|
|
return "Extracted zip, but found no importable file inside."
|
|
return import_file(inner, category_slug)
|
|
|
|
var cat := category_slug.to_lower()
|
|
var msg := ""
|
|
# Category decides intent. Match the TOKEN, not exact equality, because the slug can be a
|
|
# SUBCATEGORY ("muzzle-flashes-flipbooks", "space-hdr", …) — matching "flipbooks" exactly
|
|
# used to send subcategory flipbook .exr sheets to the HDRI importer (wrong). Fall back to
|
|
# extension when the category is unknown.
|
|
if cat.contains("flipbook"):
|
|
msg = _do_flipbook(path)
|
|
elif cat.contains("hdr"):
|
|
msg = _do_hdri(path)
|
|
elif cat == "3d-models" or cat.contains("model"):
|
|
msg = _do_3d(path)
|
|
elif ext in CghConfig.EXT_3D:
|
|
msg = _do_3d(path)
|
|
elif ext in CghConfig.EXT_HDRI:
|
|
msg = _do_hdri(path)
|
|
elif ext in CghConfig.EXT_IMAGE:
|
|
msg = _do_flipbook(path)
|
|
else:
|
|
# Not auto-importable — still drop it in the project so the user has it.
|
|
var rp0 := copy_into_project(path, cat)
|
|
return "Saved to %s (no auto-import for .%s)." % [rp0, ext] if rp0 != "" else \
|
|
"Downloaded to %s (no auto-import for .%s)." % [path, ext]
|
|
|
|
# Copy a reusable source into the project (FileSystem dock) and tell the user.
|
|
var rp := copy_into_project(path, cat)
|
|
if rp != "":
|
|
msg += " Saved in %s/" % rp.get_base_dir()
|
|
return msg
|
|
|
|
# ---------------- per-type importers ----------------
|
|
static func _do_flipbook(path: String) -> String:
|
|
var hv := _flipbook_grid(path)
|
|
var spr := CghFlipbookImport.from_sheet(path, hv.x, hv.y)
|
|
if spr == null:
|
|
if path.get_extension().to_lower() == "exr":
|
|
return "Flipbook import failed — this .exr uses a compression Godot can't read. Pick the PNG version from the ▾ menu."
|
|
return "Flipbook import failed."
|
|
if hv == Vector2i(1, 1):
|
|
return "Flipbook added (single frame — grid NxM not in filename, can't auto-play)."
|
|
return "Flipbook added & playing (%dx%d grid)." % [hv.x, hv.y]
|
|
|
|
static func _do_hdri(path: String) -> String:
|
|
var we := CghHdriImport.apply_as_world(path)
|
|
return "HDRI applied as the scene's world/sky." if we else \
|
|
"HDRI import failed — this .exr uses a compression Godot can't read (e.g. DWAA/PXR24). Try another HDRI."
|
|
|
|
static func _do_3d(path: String) -> String:
|
|
var ext := path.get_extension().to_lower()
|
|
var n: Node = _import_fbx(path) if ext == "fbx" else CghGltfImport.import_into_scene(path)
|
|
return "3D model added to the scene." if n else "3D import failed (open a scene first?)."
|
|
|
|
static func _import_fbx(path: String) -> Node:
|
|
# ufbx (4.3+) — FBXDocument mirrors GLTFDocument
|
|
var doc := FBXDocument.new()
|
|
var state := FBXState.new()
|
|
if doc.append_from_file(path, state) != OK:
|
|
return null
|
|
var root := doc.generate_scene(state)
|
|
var edited := EditorInterface.get_edited_scene_root()
|
|
if edited and root:
|
|
edited.add_child(root)
|
|
root.owner = edited
|
|
for c in root.find_children("*", "", true, false):
|
|
c.owner = edited
|
|
return root
|
|
|
|
# ---------------- project copy (FileSystem dock) ----------------
|
|
# Copy the downloaded source into res://CGHEVEN/<category>/<filename> so Godot
|
|
# imports it and it appears in the FileSystem dock (only files under res:// are
|
|
# importable/usable). Returns the res:// path, or "" on failure.
|
|
static func copy_into_project(path: String, category_slug: String) -> String:
|
|
var bytes := FileAccess.get_file_as_bytes(path)
|
|
if bytes.is_empty():
|
|
return ""
|
|
var sub := category_slug if category_slug != "" else "assets"
|
|
var dir := RES_ROOT.path_join(sub)
|
|
DirAccess.make_dir_recursive_absolute(dir)
|
|
var dest := dir.path_join(CghConfig.safe_download_name(path.get_file()))
|
|
var f := FileAccess.open(dest, FileAccess.WRITE)
|
|
if f == null:
|
|
return ""
|
|
f.store_buffer(bytes)
|
|
f.close()
|
|
# Make the editor pick up + import the new file so it shows in the dock.
|
|
if Engine.is_editor_hint():
|
|
var efs := EditorInterface.get_resource_filesystem()
|
|
if efs and not efs.is_scanning():
|
|
efs.scan()
|
|
return dest
|
|
|
|
## Extract EVERY file from a zip into a sibling temp dir and return the best
|
|
## editor-importable MODEL inside (glb/gltf > fbx > dae > obj), or "" if none. Extracts
|
|
## the whole tree (not just the model) so .bin/.mtl/textures land next to it and survive
|
|
## the import. Mirrors the Blender addon's _pick_best_model_in_tree.
|
|
static func extract_model_from_zip(zip_path: String) -> String:
|
|
var reader := ZIPReader.new()
|
|
if reader.open(zip_path) != OK:
|
|
return ""
|
|
var out_dir := zip_path.get_basename() + "_unzip"
|
|
DirAccess.make_dir_recursive_absolute(out_dir)
|
|
var best := ""
|
|
var best_rank := 999
|
|
for entry in reader.get_files():
|
|
if entry.ends_with("/"):
|
|
continue
|
|
var rel := entry.replace("\\", "/")
|
|
var out := out_dir.path_join(rel)
|
|
DirAccess.make_dir_recursive_absolute(out.get_base_dir())
|
|
var f := FileAccess.open(out, FileAccess.WRITE)
|
|
if f:
|
|
f.store_buffer(reader.read_file(entry))
|
|
f.close()
|
|
var rank: int = EDITOR_MODEL_PRIORITY.find(rel.get_extension().to_lower())
|
|
if rank != -1 and rank < best_rank:
|
|
best_rank = rank
|
|
best = out
|
|
reader.close()
|
|
return best
|
|
|
|
## Copy a model file AND its sibling files (textures / .bin / .mtl in the SAME folder)
|
|
## into res://CGHEVEN/<category>/<modelname>/ so the editor import keeps materials. Use
|
|
## this for models extracted from a zip (isolated temp dir); for a bare downloaded file
|
|
## use copy_into_project (single file). Returns the model's res:// path, or "" on failure.
|
|
static func copy_model_into_project(model_path: String, category_slug: String) -> String:
|
|
var src_dir := model_path.get_base_dir()
|
|
var sub := category_slug if category_slug != "" else "assets"
|
|
var stem := CghConfig.safe_download_name(model_path.get_file()).get_basename()
|
|
var dest_dir := RES_ROOT.path_join(sub).path_join(stem)
|
|
DirAccess.make_dir_recursive_absolute(dest_dir)
|
|
var model_dest := ""
|
|
var da := DirAccess.open(src_dir)
|
|
if da:
|
|
da.list_dir_begin()
|
|
var fn := da.get_next()
|
|
while fn != "":
|
|
if not da.current_is_dir():
|
|
var bytes := FileAccess.get_file_as_bytes(src_dir.path_join(fn))
|
|
var d := dest_dir.path_join(CghConfig.safe_download_name(fn))
|
|
var wf := FileAccess.open(d, FileAccess.WRITE)
|
|
if wf:
|
|
wf.store_buffer(bytes)
|
|
wf.close()
|
|
if fn == model_path.get_file():
|
|
model_dest = d
|
|
fn = da.get_next()
|
|
da.list_dir_end()
|
|
if model_dest == "":
|
|
# fallback: copy just the model file (distinct var names — GDScript locals are
|
|
# function-scoped, so re-declaring bytes/wf from the loop above would error).
|
|
var fbytes := FileAccess.get_file_as_bytes(model_path)
|
|
if fbytes.is_empty():
|
|
return ""
|
|
model_dest = dest_dir.path_join(CghConfig.safe_download_name(model_path.get_file()))
|
|
var fwf := FileAccess.open(model_dest, FileAccess.WRITE)
|
|
if fwf == null:
|
|
return ""
|
|
fwf.store_buffer(fbytes)
|
|
fwf.close()
|
|
if Engine.is_editor_hint():
|
|
var efs := EditorInterface.get_resource_filesystem()
|
|
if efs and not efs.is_scanning():
|
|
efs.scan()
|
|
return model_dest
|
|
|
|
static func _extract_zip(zip_path: String) -> String:
|
|
var reader := ZIPReader.new()
|
|
if reader.open(zip_path) != OK:
|
|
return ""
|
|
var ok := CghConfig.EXT_3D + CghConfig.EXT_HDRI + CghConfig.EXT_IMAGE
|
|
var out_dir := zip_path.get_basename() + "_unzip"
|
|
DirAccess.make_dir_recursive_absolute(out_dir)
|
|
var best := ""
|
|
var best_rank := 99
|
|
for entry in reader.get_files():
|
|
if entry.ends_with("/"):
|
|
continue
|
|
var e_ext := entry.get_extension().to_lower()
|
|
if not (e_ext in ok):
|
|
continue
|
|
var out := out_dir + "/" + entry.get_file()
|
|
var f := FileAccess.open(out, FileAccess.WRITE)
|
|
if f:
|
|
f.store_buffer(reader.read_file(entry))
|
|
f.close()
|
|
# prefer 3D > hdri > image when several importable files exist
|
|
var rank := 0 if e_ext in CghConfig.EXT_3D else (1 if e_ext in CghConfig.EXT_HDRI else 2)
|
|
if rank < best_rank:
|
|
best_rank = rank
|
|
best = out
|
|
reader.close()
|
|
return best
|
|
|
|
static func _flipbook_grid(path: String) -> Vector2i:
|
|
# look for an NxM token in the filename, e.g. fire_8x8.png / Portal_04_4K_8X8.exr
|
|
var rx := RegEx.new()
|
|
rx.compile("(\\d+)\\s*[xX]\\s*(\\d+)")
|
|
var m := rx.search(path.get_file())
|
|
if m:
|
|
return Vector2i(int(m.get_string(1)), int(m.get_string(2)))
|
|
return Vector2i(1, 1)
|