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>
This commit is contained in:
+123
-2
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user