Compare commits

...

2 Commits

Author SHA1 Message Date
Homer d6cb9947b2 Asset database file ops + script-file creation helpers
Groundwork for the Stage-10 editor-UX batch (file explorer, New Script
button), all pure logic proven by unit tests:

- `AssetDatabase::move_asset` / `move_folder` / `delete_asset`: perform
  the disk operation and the uid-map update together, so renaming or
  moving an asset (or a whole folder) keeps its uid and every saved
  `AssetRef` resolving. Kinds re-classify from the new path; `..` and
  existing destinations are refused via the new `AssetDbError` enum;
  deleted uids are never reused.
- Editor `assets.rs`: `script_template` (compiles under the sandboxed
  ScriptEngine — covered by a test), `sanitize_script_stem`, and
  `create_script_file` which writes a collision-free
  `assets/scripts/<stem>.rhai` and returns the relative path for
  database registration.
- docs/assets.md: new "File operations" section + the missing Script
  row in the typed-folder table.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 20:29:08 +02:00
Homer e88a7e2bb1 Fix float-literal fallback lints for Rust 1.97
rustc 1.97 promotes the float-literal type-fallback check on generic
`impl Into<f32>` arguments (rust#154024) to a denied lint, which breaks
the editor's `#![deny(warnings)]` build: every bare literal passed to
`egui::Stroke::new` now needs an explicit type. Suffix the eleven
affected literals with `_f32`; no behavior change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 20:29:08 +02:00
5 changed files with 440 additions and 13 deletions
+25
View File
@@ -164,10 +164,34 @@ breaking references, since the uid travels with the file in the manifest.
| `Model` | `models/` | `gltf`, `glb`, `obj` |
| `Audio` | `audio/` | `wav`, `ogg`, `mp3`, `flac` |
| `Ui` | `ui/` | (recognised by folder — shares `.ron` with scenes) |
| `Script` | `scripts/` | `rhai` |
`AssetKind::classify(path)` infers the kind: the leading folder wins, with the
file extension as a fallback for files dropped directly in `assets/`.
### File operations — rename, move, delete
The database also *performs* file reorganisation, so the editor's file explorer
(and any tool) can rename or move assets **without breaking saved references**
— the disk operation and the uid map are updated together:
```rust
# use oxide_engine::asset::{AssetDatabase, AssetUid};
# fn demo(db: &mut AssetDatabase, uid: AssetUid) -> Result<(), oxide_engine::asset::AssetDbError> {
db.move_asset(uid, "textures/env/brick.png")?; // rename/move; uid unchanged
db.move_folder("textures/env", "textures/world")?; // every entry under it follows
db.delete_asset(uid)?; // file + entry; uid never reused
db.save()?; // persist the new paths
# Ok(())
# }
```
All three refuse to touch anything outside `assets/` (`..` is rejected) and
refuse to overwrite an existing destination ([`AssetDbError`] has a variant per
failure). Kinds are re-classified from the new path, since a move can change
the typed folder. Deleting drops the entry but never reuses its uid — a
dangling reference stays dangling instead of silently pointing at a new file.
### Asset-reference fields in the inspector
A component points at an asset with an [`AssetRef<T>`] field — **not** a live
@@ -209,6 +233,7 @@ projects are seeded with a bundled default UI font (Inter, SIL OFL) under
`fonts/`, referenced by its project-relative path like any other asset.
[`AssetDatabase`]: ../engine/src/asset/database.rs
[`AssetDbError`]: ../engine/src/asset/database.rs
[`AssetKind`]: ../engine/src/asset/database.rs
[`AssetUid`]: ../engine/src/asset/database.rs
[`AssetRef<T>`]: ../engine/src/asset/database.rs
+123 -2
View File
@@ -1,5 +1,7 @@
//! Bundled editor assets — locating the shared `assets/` tree and seeding a
//! new project's default content (currently the default UI font).
//! Bundled editor assets — locating the shared `assets/` tree, seeding a new
//! project's default content (currently the default UI font), and creating new
//! script files from the built-in template (the inspector's "New Script"
//! button).
//!
//! The editor ships a small set of shared assets (icons, the default UI font, …)
//! installed by `install.sh` to `$PREFIX/share/oxide/assets`. At runtime we have
@@ -91,6 +93,79 @@ pub fn seed_default_font(project_assets_dir: &Path) -> std::io::Result<bool> {
Ok(true)
}
/// The `.rhai` source the "New Script" button writes, personalised with the
/// script's file stem so the Console output identifies which script speaks.
///
/// Kept to the two lifecycle hooks `docs/scripting.md` teaches first; the
/// `update` body ships commented out so a freshly created script visibly runs
/// (the `init` print) without moving anything until the author opts in.
pub fn script_template(stem: &str) -> String {
format!(
r#"// {stem}.rhai — attached via a Script component.
//
// Top-level statements run once when the script (re)starts. `init()` runs
// once after them; `update(dt)` runs every frame (dt = seconds).
fn init() {{
print("{stem}: init");
}}
fn update(dt) {{
// e.g. rotate_y(dt * 1.5);
}}
"#
)
}
/// Reduces a typed script name to a safe file stem: keeps ASCII alphanumerics,
/// `-` and `_`, folds anything else (spaces, punctuation, Unicode) to `_`,
/// collapses runs, trims the ends, and drops a trailing `.rhai` the user may
/// have typed. An unusable input yields `"new_script"`.
pub fn sanitize_script_stem(name: &str) -> String {
let trimmed = name.trim();
let trimmed = trimmed.strip_suffix(".rhai").unwrap_or(trimmed);
let mut stem = String::with_capacity(trimmed.len());
for c in trimmed.chars() {
if c.is_ascii_alphanumeric() || c == '-' || c == '_' {
stem.push(c);
} else if !stem.ends_with('_') {
stem.push('_');
}
}
let stem = stem.trim_matches('_');
if stem.is_empty() {
"new_script".to_owned()
} else {
stem.to_owned()
}
}
/// Creates a new script file under `<assets_dir>/scripts/` from the template,
/// returning its assets-relative path (e.g. `"scripts/my_script.rhai"`) for
/// registration in the [`AssetDatabase`](oxide_engine::asset::AssetDatabase).
///
/// The desired name is [sanitized](sanitize_script_stem); a taken name gets a
/// numeric suffix (`stem_2`, `stem_3`, …) instead of failing or overwriting,
/// so the button always succeeds on a writable project.
pub fn create_script_file(assets_dir: &Path, desired_name: &str) -> std::io::Result<String> {
use oxide_engine::asset::AssetKind;
let stem = sanitize_script_stem(desired_name);
let dir = assets_dir.join(AssetKind::Script.folder());
std::fs::create_dir_all(&dir)?;
let mut candidate = stem.clone();
let mut n = 1;
while dir.join(format!("{candidate}.rhai")).exists() {
n += 1;
candidate = format!("{stem}_{n}");
}
std::fs::write(
dir.join(format!("{candidate}.rhai")),
script_template(&candidate),
)?;
Ok(format!("{}/{candidate}.rhai", AssetKind::Script.folder()))
}
#[cfg(test)]
mod tests {
use super::*;
@@ -124,4 +199,50 @@ mod tests {
std::fs::remove_dir_all(tmp).ok();
}
#[test]
fn sanitize_covers_typed_names() {
assert_eq!(sanitize_script_stem("spin"), "spin");
assert_eq!(sanitize_script_stem(" My Cool Script! "), "My_Cool_Script");
assert_eq!(sanitize_script_stem("door.rhai"), "door");
assert_eq!(sanitize_script_stem("a//b\\c"), "a_b_c");
assert_eq!(sanitize_script_stem("čárka"), "rka");
// Unusable inputs fall back rather than producing "" or "_".
assert_eq!(sanitize_script_stem(""), "new_script");
assert_eq!(sanitize_script_stem("!!!"), "new_script");
assert_eq!(sanitize_script_stem(".rhai"), "new_script");
}
#[test]
fn create_script_writes_template_and_dodges_collisions() {
let mut tmp = std::env::temp_dir();
tmp.push(format!("oxide_newscript_{}", std::process::id()));
let assets = tmp.join("assets");
std::fs::create_dir_all(&assets).unwrap();
let rel = create_script_file(&assets, "door opener").unwrap();
assert_eq!(rel, "scripts/door_opener.rhai");
let text = std::fs::read_to_string(assets.join(&rel)).unwrap();
assert!(text.contains("fn update(dt)"));
// Same name again: suffixed, nothing overwritten.
let rel2 = create_script_file(&assets, "door opener").unwrap();
assert_eq!(rel2, "scripts/door_opener_2.rhai");
let rel3 = create_script_file(&assets, "door opener").unwrap();
assert_eq!(rel3, "scripts/door_opener_3.rhai");
std::fs::remove_dir_all(tmp).ok();
}
#[test]
fn template_compiles_in_the_script_engine() {
// The template must never ship a syntax error: compile it exactly as
// the runtime would.
let asset = oxide_script::ScriptAsset::from_source(
"new_script.rhai",
script_template("new_script"),
);
let engine = oxide_script::ScriptEngine::new();
engine.compile(&asset).expect("template compiles");
}
}
+10 -10
View File
@@ -2251,7 +2251,7 @@ impl<'a> ShellTabViewer<'a> {
stroke_rect(
&painter,
tab_rect.shrink(1.5),
egui::Stroke::new(3.0, color),
egui::Stroke::new(3.0_f32, color),
);
painter.text(
tab_rect.left_top() + egui::vec2(8.0, 6.0),
@@ -2312,7 +2312,7 @@ impl<'a> ShellTabViewer<'a> {
egui::Color32::from_rgb(110, 220, 120) // green: solid
};
let selected = self.state.selected == Some(entity);
let stroke = egui::Stroke::new(if selected { 2.0 } else { 1.2 }, color);
let stroke = egui::Stroke::new(if selected { 2.0_f32 } else { 1.2_f32 }, color);
for (a, b) in collider_wire_segments(&col) {
let (Some(pa), Some(pb)) = (
project(pose.transform_point(a), &overlay.view_proj, tab_rect),
@@ -2344,7 +2344,7 @@ impl<'a> ShellTabViewer<'a> {
project(viz.origin, &overlay.view_proj, tab_rect),
project(viz.end, &overlay.view_proj, tab_rect),
) {
painter.line_segment([a, b], egui::Stroke::new(1.5, cyan));
painter.line_segment([a, b], egui::Stroke::new(1.5_f32, cyan));
}
// The hit surface: a dot at the contact point + a normal whisker.
@@ -2359,7 +2359,7 @@ impl<'a> ShellTabViewer<'a> {
project(hit.point, &overlay.view_proj, tab_rect),
project(tip, &overlay.view_proj, tab_rect),
) {
painter.line_segment([a, b], egui::Stroke::new(2.0, magenta));
painter.line_segment([a, b], egui::Stroke::new(2.0_f32, magenta));
}
}
}
@@ -2470,7 +2470,7 @@ impl<'a> ShellTabViewer<'a> {
) else {
return;
};
painter.line_segment([pa, pb], egui::Stroke::new(2.5, color));
painter.line_segment([pa, pb], egui::Stroke::new(2.5_f32, color));
// Arrowhead at the tip — a small filled triangle perpendicular to
// the line direction in screen space.
let dir = (pb - pa).normalized();
@@ -2521,7 +2521,7 @@ impl<'a> ShellTabViewer<'a> {
} else {
egui::Color32::from_rgba_unmultiplied(255, 120, 120, if engaged { 220 } else { 130 })
};
let stroke = egui::Stroke::new(1.5, color);
let stroke = egui::Stroke::new(1.5_f32, color);
painter.add(egui::Shape::convex_polygon(projected, color, stroke));
}
@@ -2551,7 +2551,7 @@ impl<'a> ShellTabViewer<'a> {
return;
}
let color = axis_color(axis, engaged);
painter.add(egui::Shape::line(pts, egui::Stroke::new(2.0, color)));
painter.add(egui::Shape::line(pts, egui::Stroke::new(2.0_f32, color)));
}
fn hierarchy(&mut self, ui: &mut egui::Ui) {
@@ -2705,7 +2705,7 @@ impl<'a> ShellTabViewer<'a> {
DropZone::Child
};
let accent = ui.visuals().selection.bg_fill;
let line = egui::Stroke::new(2.0, accent);
let line = egui::Stroke::new(2.0_f32, accent);
match zone {
DropZone::Before => {
ui.painter().line_segment(
@@ -3333,7 +3333,7 @@ impl<'a> ShellTabViewer<'a> {
if r.contains(pos) {
let above = (pos.y - r.top()) < r.height() / 2.0;
let accent = ui.visuals().selection.bg_fill;
let line = egui::Stroke::new(2.0, accent);
let line = egui::Stroke::new(2.0_f32, accent);
let y = if above { r.top() } else { r.bottom() };
ui.painter().line_segment(
[egui::pos2(r.left(), y), egui::pos2(r.right(), y)],
@@ -5165,7 +5165,7 @@ fn draw_ui_preview(ui: &mut egui::Ui, panel: &UiPanel) {
stroke_rect(
&painter,
egui::Rect::from_min_size(origin, draw),
egui::Stroke::new(1.0, egui::Color32::DARK_GRAY),
egui::Stroke::new(1.0_f32, egui::Color32::DARK_GRAY),
);
let theme = UiTheme::new();
+280
View File
@@ -178,6 +178,30 @@ pub fn asset_ref_target(type_name: &str) -> Option<&str> {
Some(inner.rsplit("::").next().unwrap_or(inner).trim())
}
/// Errors from [`AssetDatabase`] file operations ([`move_asset`], [`delete_asset`],
/// [`move_folder`]) — the ops that touch both the on-disk file *and* the uid map.
///
/// [`move_asset`]: AssetDatabase::move_asset
/// [`delete_asset`]: AssetDatabase::delete_asset
/// [`move_folder`]: AssetDatabase::move_folder
#[derive(Debug, thiserror::Error)]
pub enum AssetDbError {
/// The uid is not recorded in the database.
#[error("unknown asset uid {0:?}")]
UnknownUid(AssetUid),
/// The destination path is already taken, in the database or on disk.
/// Nothing was changed; pick another name.
#[error("destination already exists: {0}")]
DestinationExists(String),
/// The path is empty, escapes the assets directory (`..`), or would move a
/// folder into itself.
#[error("invalid assets-relative path: {0:?}")]
InvalidPath(String),
/// The underlying filesystem operation failed.
#[error(transparent)]
Io(#[from] std::io::Error),
}
/// A stable, per-project identifier for one asset.
///
/// Unlike [`AssetId`](super::AssetId) — which is process-unique and changes
@@ -507,6 +531,126 @@ impl AssetDatabase {
Some(server.load::<T>(path))
}
// --- file operations (disk + uid map together) --------------------------
//
// These keep the uid attached to its asset across renames/moves — the whole
// point of the uid layer — so the editor's file explorer can reorganise a
// project without breaking any saved `AssetRef`. Deletion drops the entry
// but never reuses its uid (the allocator only counts up).
/// Renames/moves the asset `uid` to `new_relative_path` (relative to
/// `assets/`), on disk and in the map, **keeping its uid** so saved
/// references still resolve. The kind is re-classified from the new path.
/// Parent directories are created as needed; moving onto an existing path
/// is refused. A no-op if the path is unchanged.
///
/// Call [`save`](Self::save) afterwards to persist the new path.
pub fn move_asset(
&mut self,
uid: AssetUid,
new_relative_path: impl AsRef<str>,
) -> Result<(), AssetDbError> {
let entry = self.by_uid.get(&uid).ok_or(AssetDbError::UnknownUid(uid))?;
let new_path = checked_relative(new_relative_path.as_ref())?;
if new_path == entry.path {
return Ok(());
}
if self.by_path.contains_key(&new_path) {
return Err(AssetDbError::DestinationExists(new_path));
}
let assets_dir = self.assets_dir();
let from_abs = assets_dir.join(entry.path.replace('/', std::path::MAIN_SEPARATOR_STR));
let to_abs = assets_dir.join(new_path.replace('/', std::path::MAIN_SEPARATOR_STR));
if to_abs.exists() {
return Err(AssetDbError::DestinationExists(new_path));
}
if let Some(parent) = to_abs.parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::rename(&from_abs, &to_abs)?;
let old_path = entry.path.clone();
self.by_path.remove(&old_path);
self.by_path.insert(new_path.clone(), uid);
let entry = self.by_uid.get_mut(&uid).expect("entry checked above");
entry.kind = AssetKind::classify(&new_path);
entry.path = new_path;
Ok(())
}
/// Deletes the asset `uid`: removes its file from disk and drops its entry.
/// A file already missing from disk is fine (the entry is still dropped);
/// the uid is never reused. Call [`save`](Self::save) afterwards.
pub fn delete_asset(&mut self, uid: AssetUid) -> Result<(), AssetDbError> {
let entry = self.by_uid.get(&uid).ok_or(AssetDbError::UnknownUid(uid))?;
let abs = self
.assets_dir()
.join(entry.path.replace('/', std::path::MAIN_SEPARATOR_STR));
match std::fs::remove_file(&abs) {
Ok(()) => {}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
Err(e) => return Err(e.into()),
}
let path = entry.path.clone();
self.by_uid.remove(&uid);
self.by_path.remove(&path);
Ok(())
}
/// Renames/moves the folder `from` to `to` (both relative to `assets/`),
/// on disk and in the map: every entry under `from/` is rewritten to live
/// under `to/`, **keeping its uid** (kinds are re-classified — moving out
/// of a typed folder can change them). Refuses to overwrite an existing
/// destination or to move a folder into itself. Returns how many entries
/// moved. Call [`save`](Self::save) afterwards.
pub fn move_folder(
&mut self,
from: impl AsRef<str>,
to: impl AsRef<str>,
) -> Result<usize, AssetDbError> {
let from = checked_relative(from.as_ref())?;
let to = checked_relative(to.as_ref())?;
if from == to {
return Ok(0);
}
if to.starts_with(&format!("{from}/")) {
return Err(AssetDbError::InvalidPath(to));
}
let assets_dir = self.assets_dir();
let from_abs = assets_dir.join(from.replace('/', std::path::MAIN_SEPARATOR_STR));
let to_abs = assets_dir.join(to.replace('/', std::path::MAIN_SEPARATOR_STR));
if !from_abs.is_dir() {
return Err(AssetDbError::Io(std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("no such folder under assets/: {from}"),
)));
}
if to_abs.exists() {
return Err(AssetDbError::DestinationExists(to));
}
if let Some(parent) = to_abs.parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::rename(&from_abs, &to_abs)?;
let prefix = format!("{from}/");
let moved: Vec<AssetUid> = self
.by_uid
.values()
.filter(|e| e.path.starts_with(&prefix))
.map(|e| e.uid)
.collect();
for &uid in &moved {
let entry = self.by_uid.get_mut(&uid).expect("collected above");
let new_path = format!("{to}/{}", &entry.path[prefix.len()..]);
let old_path = std::mem::replace(&mut entry.path, new_path.clone());
entry.kind = AssetKind::classify(&new_path);
self.by_path.remove(&old_path);
self.by_path.insert(new_path, uid);
}
Ok(moved.len())
}
// --- internals ---------------------------------------------------------
fn highest_uid(&self) -> u64 {
@@ -522,6 +666,20 @@ fn normalize_relative(path: &str) -> String {
trimmed.trim_start_matches('/').to_string()
}
/// Normalizes like [`normalize_relative`] and rejects paths that are empty or
/// escape the assets directory (`..` components) — file operations must never
/// touch anything outside `assets/`.
fn checked_relative(path: &str) -> Result<String, AssetDbError> {
let norm = normalize_relative(path);
let escapes = Path::new(&norm)
.components()
.any(|c| !matches!(c, std::path::Component::Normal(_)));
if norm.is_empty() || escapes {
return Err(AssetDbError::InvalidPath(path.to_string()));
}
Ok(norm)
}
/// Recursively collects files under `dir`, pushing each one's path relative to
/// `base` (forward slashes) into `out`. A missing `dir` is silently skipped.
fn collect_files(dir: &Path, base: &Path, out: &mut Vec<String>) {
@@ -738,6 +896,128 @@ mod tests {
std::fs::remove_dir_all(root).ok();
}
#[test]
fn move_asset_keeps_uid_and_reclassifies() {
let root = temp_root("move_asset");
touch_asset(&root, "textures/wall.png");
let mut db = AssetDatabase::new(&root);
db.scan();
let uid = db.uid_of("textures/wall.png").unwrap();
// Rename within the folder: same uid, file moved on disk.
db.move_asset(uid, "textures/brick.png").unwrap();
assert_eq!(db.relative_path(uid), Some("textures/brick.png"));
assert!(db.uid_of("textures/wall.png").is_none());
assert!(!root.join(ASSETS_DIR).join("textures/wall.png").exists());
assert!(root.join(ASSETS_DIR).join("textures/brick.png").exists());
// Move into a subfolder that does not exist yet (created), then out of
// the typed folder entirely — kind follows the path.
db.move_asset(uid, "textures/env/brick.png").unwrap();
assert!(root
.join(ASSETS_DIR)
.join("textures/env/brick.png")
.exists());
assert_eq!(db.entry(uid).unwrap().kind, AssetKind::Texture);
db.move_asset(uid, "misc/brick.dat").unwrap();
assert_eq!(db.entry(uid).unwrap().kind, AssetKind::Other);
// A rescan does not disturb the moved entry's uid... (misc/ is not a
// typed folder, so the entry survives only because scan never saw it —
// move back first to prove the typed-folder case.)
db.move_asset(uid, "textures/brick.png").unwrap();
db.scan();
assert_eq!(db.uid_of("textures/brick.png"), Some(uid));
// No-op and error cases.
db.move_asset(uid, "textures/brick.png").unwrap(); // unchanged path: Ok
touch_asset(&root, "textures/taken.png");
db.scan();
assert!(matches!(
db.move_asset(uid, "textures/taken.png"),
Err(AssetDbError::DestinationExists(_))
));
assert!(matches!(
db.move_asset(uid, "../outside.png"),
Err(AssetDbError::InvalidPath(_))
));
assert!(matches!(
db.move_asset(AssetUid(999), "textures/x.png"),
Err(AssetDbError::UnknownUid(_))
));
std::fs::remove_dir_all(root).ok();
}
#[test]
fn delete_asset_removes_file_and_entry_without_reusing_uid() {
let root = temp_root("delete_asset");
touch_asset(&root, "audio/click.wav");
let mut db = AssetDatabase::new(&root);
db.scan();
let uid = db.uid_of("audio/click.wav").unwrap();
db.delete_asset(uid).unwrap();
assert!(db.entry(uid).is_none());
assert!(!root.join(ASSETS_DIR).join("audio/click.wav").exists());
// The freed uid is not handed to the next registration.
let next = db.register("audio/other.wav");
assert_ne!(next, uid);
// Deleting an entry whose file is already gone still drops the entry.
std::fs::remove_file(root.join(ASSETS_DIR).join("audio/other.wav")).ok();
db.delete_asset(next).unwrap();
assert!(db.entry(next).is_none());
assert!(matches!(
db.delete_asset(uid),
Err(AssetDbError::UnknownUid(_))
));
std::fs::remove_dir_all(root).ok();
}
#[test]
fn move_folder_rewrites_entries_and_guards_self_moves() {
let root = temp_root("move_folder");
touch_asset(&root, "textures/env/wall.png");
touch_asset(&root, "textures/env/floor.png");
touch_asset(&root, "textures/hero.png");
let mut db = AssetDatabase::new(&root);
db.scan();
let wall = db.uid_of("textures/env/wall.png").unwrap();
let hero = db.uid_of("textures/hero.png").unwrap();
let moved = db.move_folder("textures/env", "textures/world").unwrap();
assert_eq!(moved, 2);
assert_eq!(db.relative_path(wall), Some("textures/world/wall.png"));
assert!(db.uid_of("textures/world/floor.png").is_some());
assert_eq!(db.relative_path(hero), Some("textures/hero.png"));
assert!(root
.join(ASSETS_DIR)
.join("textures/world/wall.png")
.exists());
assert!(!root.join(ASSETS_DIR).join("textures/env").exists());
// Guards: into itself, onto an existing folder, missing source.
assert!(matches!(
db.move_folder("textures", "textures/world/deeper"),
Err(AssetDbError::InvalidPath(_))
));
std::fs::create_dir_all(root.join(ASSETS_DIR).join("models")).unwrap();
assert!(matches!(
db.move_folder("textures/world", "models"),
Err(AssetDbError::DestinationExists(_))
));
assert!(matches!(
db.move_folder("textures/nope", "textures/x"),
Err(AssetDbError::Io(_))
));
std::fs::remove_dir_all(root).ok();
}
struct TxtLoader;
impl crate::asset::AssetLoader for TxtLoader {
type Asset = String;
+2 -1
View File
@@ -18,7 +18,8 @@ mod handle;
mod server;
pub use database::{
asset_ref_target, AssetDatabase, AssetEntry, AssetKind, AssetRef, AssetUid, ASSET_MANIFEST_FILE,
asset_ref_target, AssetDatabase, AssetDbError, AssetEntry, AssetKind, AssetRef, AssetUid,
ASSET_MANIFEST_FILE,
};
pub use gltf::{load_gltf, load_gltf_slice, GltfError, GltfLoader, GltfMesh, GltfModel};
pub use handle::{AssetId, Handle, LoadState};