Collider via collide-Property + Quickhull, OBJ-Pfad entfernt
col_*-Namenspräfix raus: Rollen steuern nur noch Custom Properties (collide=1 sichtbar+Collider, "proxy" unsichtbar, signal wie gehabt). Collider sind die konvexe Hülle der Vertices — konkave Meshes erlaubt. glb ist der einzige Modell-Pfad; suzanne.obj → suzanne.glb. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+58
-36
@@ -1,18 +1,19 @@
|
||||
//! Neutrales 3D-Modell — das gemeinsame Ergebnis der Blender-Loader
|
||||
//! (engine::obj, engine::gltf). Konsumenten (render::props, collision)
|
||||
//! kennen nur diese Struktur; welcher Parser sie erzeugt hat, ist ihnen
|
||||
//! egal — so bleibt ein Format-Wechsel ein lokaler Loader-Tausch.
|
||||
//! Neutrales 3D-Modell — das gemeinsame Ergebnis des Blender-Loaders
|
||||
//! (engine::gltf). Konsumenten (render::props, collision, trigger) kennen
|
||||
//! nur diese Struktur; welcher Parser sie erzeugt hat, ist ihnen egal — so
|
||||
//! bleibt ein Format-Wechsel ein lokaler Loader-Tausch.
|
||||
//!
|
||||
//! Naming-Konvention (Objekt-Präfix, case-insensitiv):
|
||||
//! `col_*` → unsichtbar, collidierbar (Collision-Proxy, konvex!)
|
||||
//! sonst → nur sichtbar (`vis_*` sagt dasselbe explizit)
|
||||
//! Was ein Objekt *ist*, steuern seine Blender-Custom-Properties (keine
|
||||
//! Namens-Präfixe — Namen bleiben frei für die Autorin), siehe
|
||||
//! [`apply_props`]:
|
||||
//! `collide = true` → sichtbar **und** Collider (konvexe Hülle des
|
||||
//! Meshes, siehe collision::object_planes)
|
||||
//! `collide = "proxy"` → unsichtbarer Collider (reines Kollisionsvolumen)
|
||||
//! `signal = <name>` → Trigger; mit Collider Klick-Ziel, ohne Collider
|
||||
//! unsichtbare Betretens-Zone (engine::trigger)
|
||||
//! nichts davon → nur sichtbar
|
||||
//!
|
||||
//! Anders als irl3d („ohne Präfix = beides"): dessen Physik kollidierte per
|
||||
//! Dreieck, unsere per konvexem Ebenen-Schnitt — konkave Sichtgeometrie als
|
||||
//! Default-Collider ergäbe hier nur ein sinnloses Hüll-Volumen. Kollision
|
||||
//! ist deshalb immer ein bewusst gebauter `col_*`-Proxy.
|
||||
//!
|
||||
//! Objektnamen sind zugleich die künftigen Interact-Keys: Blender-Suffixe
|
||||
//! Objektnamen sind zugleich die Interact-Keys: Blender-Suffixe
|
||||
//! (`Thing.001`) strippt `signals::signal_key` beim Dispatch, nicht hier —
|
||||
//! die Loader bleiben frei von Signal-Wissen.
|
||||
|
||||
@@ -56,34 +57,41 @@ pub struct Empty {
|
||||
pub props: Props,
|
||||
}
|
||||
|
||||
/// Präfix-Klassifikation (case-insensitiv): `col_` = nur Collider, alles
|
||||
/// andere nur sichtbar (siehe Modul-Doc, Abweichung von irl3d).
|
||||
pub fn classify(name: &str) -> (bool, bool) {
|
||||
let is_col = name.get(..4).is_some_and(|p| p.eq_ignore_ascii_case("col_"));
|
||||
(!is_col, is_col)
|
||||
}
|
||||
/// Custom-Property-Schlüssel, der ein Objekt zum Collider macht (siehe
|
||||
/// Modul-Doc): truthy (`true`, `1`, …) = sichtbar + Collider, `"proxy"` =
|
||||
/// unsichtbarer Collider. `0`/`false`/`no`/leer zählen als aus, damit die
|
||||
/// Autorin die Property abschalten kann, ohne sie zu löschen.
|
||||
pub const COLLIDE_PROP: &str = "collide";
|
||||
|
||||
/// Custom-Property-Schlüssel, der ein Objekt zum Trigger macht (Wert =
|
||||
/// Signal-Name für `signals::dispatch`). Die Collider-Frage entscheidet die
|
||||
/// Semantik — anfassbar = klickbar, durchlaufbar = Betretens-Zone:
|
||||
/// `col_*` + `signal` → solide **und** klickbar (Point-and-Click)
|
||||
/// sonst + `signal` → unsichtbare Trigger-Zone, feuert beim Betreten
|
||||
/// `collide` + `signal` → solide **und** klickbar (Point-and-Click)
|
||||
/// nur `signal` → unsichtbare Trigger-Zone, feuert beim Betreten
|
||||
/// (einmal je Eintritt; erneut erst nach Verlassen)
|
||||
pub const SIGNAL_PROP: &str = "signal";
|
||||
|
||||
/// Die `signal`-Regel anwenden (nach `classify`, wenn `props` bekannt sind):
|
||||
/// eine Betretens-Zone ist ein reines Trigger-Volumen und wird nie gerendert.
|
||||
pub fn apply_signal_rule(o: &mut Object) {
|
||||
/// Die Property-Regeln anwenden, sobald `props` gelesen sind (Objekte
|
||||
/// starten sichtbar und ohne Collider): `collide` schaltet den Collider,
|
||||
/// `"proxy"` und Betretens-Zonen sind reine Volumen und werden nie gerendert.
|
||||
pub fn apply_props(o: &mut Object) {
|
||||
if let Some(v) = o.props.get(COLLIDE_PROP) {
|
||||
let v = v.to_ascii_lowercase();
|
||||
if !matches!(v.as_str(), "" | "0" | "false" | "no") {
|
||||
o.collider = true;
|
||||
if v == "proxy" { o.visible = false; }
|
||||
}
|
||||
}
|
||||
if !o.collider && o.props.contains_key(SIGNAL_PROP) {
|
||||
o.visible = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Test-Helfer: Einheitswürfel-Objekt bei `min..min+1` (Quads CCW von außen,
|
||||
/// Fan-trianguliert wie die Loader) mit optionaler `signal`-Property —
|
||||
/// geteilt von trigger-/session-Tests.
|
||||
/// Fan-trianguliert wie der Loader) mit optionalen `collide`-/`signal`-
|
||||
/// Properties — geteilt von trigger-/session-/collision-Tests.
|
||||
#[cfg(test)]
|
||||
pub(crate) fn test_cube(name: &str, min: [f32; 3], signal: Option<&str>) -> Object {
|
||||
pub(crate) fn test_cube(name: &str, min: [f32; 3], collide: Option<&str>, signal: Option<&str>) -> Object {
|
||||
let v = |dx: f32, dy: f32, dz: f32| [min[0] + dx, min[1] + dy, min[2] + dz];
|
||||
let verts = vec![
|
||||
v(0.0, 0.0, 0.0), v(1.0, 0.0, 0.0), v(1.0, 1.0, 0.0), v(0.0, 1.0, 0.0),
|
||||
@@ -99,17 +107,17 @@ pub(crate) fn test_cube(name: &str, min: [f32; 3], signal: Option<&str>) -> Obje
|
||||
tris.push([q[0], q[2], q[3]]);
|
||||
}
|
||||
let n = tris.len();
|
||||
let (visible, collider) = classify(name);
|
||||
let mut props = Props::new();
|
||||
if let Some(c) = collide { props.insert(COLLIDE_PROP.into(), c.into()); }
|
||||
if let Some(s) = signal { props.insert(SIGNAL_PROP.into(), s.into()); }
|
||||
let mut o = Object {
|
||||
name: name.into(),
|
||||
uvs: vec![[0.0, 0.0]; verts.len()],
|
||||
verts, tris,
|
||||
tri_mats: vec![0; n],
|
||||
visible, collider, props,
|
||||
visible: true, collider: false, props,
|
||||
};
|
||||
apply_signal_rule(&mut o);
|
||||
apply_props(&mut o);
|
||||
o
|
||||
}
|
||||
|
||||
@@ -118,11 +126,25 @@ mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn classify_is_prefix_and_case_insensitive() {
|
||||
assert_eq!(classify("col_Wand"), (false, true));
|
||||
assert_eq!(classify("COL_Wand"), (false, true));
|
||||
assert_eq!(classify("vis_Deko"), (true, false));
|
||||
assert_eq!(classify("Suzanne"), (true, false));
|
||||
assert_eq!(classify(""), (true, false));
|
||||
fn collide_prop_rules() {
|
||||
// truthy → sichtbar + Collider; "proxy" → unsichtbarer Collider.
|
||||
let o = test_cube("Kiste", [0.0; 3], Some("true"), None);
|
||||
assert!(o.visible && o.collider);
|
||||
let o = test_cube("Wand", [0.0; 3], Some("proxy"), None);
|
||||
assert!(!o.visible && o.collider);
|
||||
// Abschaltwerte zählen als aus; ohne Property nur sichtbar.
|
||||
let o = test_cube("Aus", [0.0; 3], Some("0"), None);
|
||||
assert!(o.visible && !o.collider);
|
||||
let o = test_cube("Deko", [0.0; 3], None, None);
|
||||
assert!(o.visible && !o.collider);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn signal_without_collider_is_invisible_zone() {
|
||||
let o = test_cube("Zone", [0.0; 3], None, Some("betreten"));
|
||||
assert!(!o.visible && !o.collider);
|
||||
// Mit Collider bleibt das Objekt sichtbar (Klick-Ziel).
|
||||
let o = test_cube("Kiste", [0.0; 3], Some("1"), Some("kiste"));
|
||||
assert!(o.visible && o.collider);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user