M2-Karte t_d8be9ed7: Struktur-Objekte im Editor. Bibliothek +15 Objekte: bruecke_holz (Bruecke Holz), hoehleneingang, felsbogen — deutsche Namen, Gruppe 'bauwerke', Thumbnails gerendert. mesh_shapes: SHAPE_COMPOUND-Platzhalter — EIN Mesh aus allen Verbund-Teilen, in Fusspunkt-Koordinaten gebacken (pos-Versatz + rot_deg), so sind Rampe/Durchgaenge im Editor sichtbar wie die echte Kollision; neu center_height() fuer das Drehzentrum der Auswahl (map_view_builder nutzt es). test_library: 15 Objekte, Undo/Redo-Zaehler dynamisiert -> Suite 334 Checks gruen.

This commit is contained in:
Slyphax 2026-10-15 19:21:25 +00:00
parent cddebb8c26
commit 5dd37e32d7
8 changed files with 50 additions and 6 deletions

View file

@ -13,6 +13,9 @@
"boulder_small": { "poly": 96, "collision": { "shape": "sphere", "radius": 0.8 } }, "boulder_small": { "poly": 96, "collision": { "shape": "sphere", "radius": 0.8 } },
"tree_pine": { "poly": 180, "collision": { "shape": "cylinder", "radius": 0.4, "height": 12.0 } }, "tree_pine": { "poly": 180, "collision": { "shape": "cylinder", "radius": 0.4, "height": 12.0 } },
"bush_small": { "poly": 64, "collision": { "shape": "sphere", "radius": 0.8 } }, "bush_small": { "poly": 64, "collision": { "shape": "sphere", "radius": 0.8 } },
"fass": { "poly": 128, "collision": { "shape": "cylinder", "radius": 0.31, "height": 0.88 } } "fass": { "poly": 128, "collision": { "shape": "cylinder", "radius": 0.31, "height": 0.88 } },
"bruecke_holz": { "poly": 228, "collision": { "shape": "compound", "parts": [{ "shape": "box", "pos": [0, 0.85, 0], "size": [2, 0.3, 3] }, { "shape": "box", "pos": [0, 0.361, -2.806], "size": [2, 0.3, 2.89], "rot_deg": [21.8, 0, 0] }, { "shape": "box", "pos": [0, 0.361, 2.806], "size": [2, 0.3, 2.89], "rot_deg": [-21.8, 0, 0] }, { "shape": "box", "pos": [-0.93, 1.5, 0], "size": [0.14, 0.12, 3] }, { "shape": "box", "pos": [-0.93, 0.964, -2.564], "size": [0.14, 0.12, 2.69], "rot_deg": [21.8, 0, 0] }, { "shape": "box", "pos": [-0.93, 0.964, 2.564], "size": [0.14, 0.12, 2.69], "rot_deg": [-21.8, 0, 0] }, { "shape": "box", "pos": [0.93, 1.5, 0], "size": [0.14, 0.12, 3] }, { "shape": "box", "pos": [0.93, 0.964, -2.564], "size": [0.14, 0.12, 2.69], "rot_deg": [21.8, 0, 0] }, { "shape": "box", "pos": [0.93, 0.964, 2.564], "size": [0.14, 0.12, 2.69], "rot_deg": [-21.8, 0, 0] }] } },
"hoehleneingang": { "poly": 204, "collision": { "shape": "compound", "parts": [{ "shape": "box", "pos": [-1.8, 1.6, 0], "size": [1.4, 3.2, 4] }, { "shape": "box", "pos": [1.8, 1.6, 0], "size": [1.4, 3.2, 4] }, { "shape": "box", "pos": [0, 2.8, 0], "size": [5, 0.8, 4] }, { "shape": "box", "pos": [0, 1.2, 1.8], "size": [2.2, 2.4, 0.4] }] } },
"felsbogen": { "poly": 156, "collision": { "shape": "compound", "parts": [{ "shape": "box", "pos": [-1.9, 1.3, 0], "size": [1.2, 2.6, 2.4] }, { "shape": "box", "pos": [1.9, 1.3, 0], "size": [1.2, 2.6, 2.4] }, { "shape": "box", "pos": [0, 3.1, 0], "size": [5, 1, 2.4] }] } }
} }
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 941 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1,001 B

View file

@ -22,6 +22,9 @@ const NAMES := {
"boulder_small": "Kleiner Felsen", "boulder_small": "Kleiner Felsen",
"tree_pine": "Nadelbaum", "tree_pine": "Nadelbaum",
"bush_small": "Strauch", "bush_small": "Strauch",
"bruecke_holz": "Brücke (Holz)",
"hoehleneingang": "Höhleneingang",
"felsbogen": "Felsbogen",
} }
## Gruppen für den Bibliothek-Browser (Anzeige-Reihenfolge = Reihenfolge hier). ## Gruppen für den Bibliothek-Browser (Anzeige-Reihenfolge = Reihenfolge hier).
@ -34,7 +37,8 @@ const GROUP_LABELS := {
} }
const GROUP_IDS := { const GROUP_IDS := {
"kisten": ["crate_wood", "crate_metal", "barrier_sandbag", "wall_concrete"], "kisten": ["crate_wood", "crate_metal", "barrier_sandbag", "wall_concrete"],
"bauwerke": ["bunker_concrete", "watchtower_wood", "fence_metal"], "bauwerke": ["bunker_concrete", "watchtower_wood", "fence_metal",
"bruecke_holz", "hoehleneingang", "felsbogen"],
"natur": ["tree_pine", "bush_small", "rock_large", "boulder_small"], "natur": ["tree_pine", "bush_small", "rock_large", "boulder_small"],
} }

View file

@ -195,7 +195,7 @@ func _build_objects(objects: Array, manifest: Dictionary) -> void:
marker.add_child(mi) marker.add_child(mi)
var half := (mesh.get_aabb().size * 0.5) as Vector3 var half := (mesh.get_aabb().size * 0.5) as Vector3
marker.set_meta("pick_radius", half.length() + maxf(0.6, half.y * 0.25)) marker.set_meta("pick_radius", half.length() + maxf(0.6, half.y * 0.25))
marker.set_meta("half_height", MeshShapes.half_height(mesh)) marker.set_meta("half_height", MeshShapes.center_height(mesh))
object_markers.append(marker) object_markers.append(marker)
add_child(marker) add_child(marker)

View file

@ -7,6 +7,7 @@ class_name MeshShapes
const SHAPE_BOX := "box" const SHAPE_BOX := "box"
const SHAPE_SPHERE := "sphere" const SHAPE_SPHERE := "sphere"
const SHAPE_CYLINDER := "cylinder" const SHAPE_CYLINDER := "cylinder"
const SHAPE_COMPOUND := "compound"
static func make_placeholder(shape: String, col: Dictionary, scale: float) -> Mesh: static func make_placeholder(shape: String, col: Dictionary, scale: float) -> Mesh:
@ -23,6 +24,33 @@ static func make_placeholder(shape: String, col: Dictionary, scale: float) -> Me
c.bottom_radius = c.top_radius c.bottom_radius = c.top_radius
c.height = maxf(0.01, float(col.get("height", 1.0)) * scale) c.height = maxf(0.01, float(col.get("height", 1.0)) * scale)
return c return c
SHAPE_COMPOUND:
# Struktur-Objekte (t_d8be9ed7): EIN Mesh aus allen Verbund-Teilen,
# gebacken in Fußpunkt-Koordinaten (Teil-pos = Versatz vom Fußpunkt,
# rot_deg = Euler-Grad) — Rampe/Durchgänge also sichtbar wie die
# echte Kollision. half_height() liefert dafür 0 (schon gebacken).
var st := SurfaceTool.new()
st.begin(Mesh.PRIMITIVE_TRIANGLES)
st.set_smooth_group(-1)
for teil_v in col.get("parts", []):
var teil: Dictionary = teil_v if teil_v is Dictionary else {}
var teil_mesh := make_placeholder(str(teil.get("shape", "box")), teil, scale)
if teil_mesh == null:
continue
var off: Array = teil.get("pos", [0.0, 0.0, 0.0])
var rot: Array = teil.get("rot_deg", [0.0, 0.0, 0.0])
var xf := Transform3D(
Basis.from_euler(Vector3(deg_to_rad(float(rot[0])), deg_to_rad(float(rot[1])), deg_to_rad(float(rot[2])))),
Vector3(float(off[0]), float(off[1]), float(off[2])) * scale)
# PrimitiveMesh → Roht_arrays (MeshDataTool bräuchte ArrayMesh).
var arrays: Array = teil_mesh.surface_get_arrays(0)
var verts: PackedVector3Array = arrays[Mesh.ARRAY_VERTEX]
var norms: PackedVector3Array = arrays[Mesh.ARRAY_NORMAL]
var idx: PackedInt32Array = arrays[Mesh.ARRAY_INDEX]
for ii in idx:
st.set_normal(xf.basis * norms[ii])
st.add_vertex(xf * verts[ii])
return st.commit()
_: _:
var b := BoxMesh.new() var b := BoxMesh.new()
var size: Array = col.get("size", [1.0, 1.0, 1.0]) var size: Array = col.get("size", [1.0, 1.0, 1.0])
@ -32,6 +60,7 @@ static func make_placeholder(shape: String, col: Dictionary, scale: float) -> Me
## Halbe Höhe des Meshes über dem Fußpunkt (Mesh-Ursprung = Objekt-Fuß). ## Halbe Höhe des Meshes über dem Fußpunkt (Mesh-Ursprung = Objekt-Fuß).
## Compound-ArrayMeshes sind bereits in Fußpunkt-Koordinaten gebacken → 0.
static func half_height(mesh: Mesh) -> float: static func half_height(mesh: Mesh) -> float:
if mesh is BoxMesh: if mesh is BoxMesh:
return (mesh as BoxMesh).size.y * 0.5 return (mesh as BoxMesh).size.y * 0.5
@ -39,4 +68,12 @@ static func half_height(mesh: Mesh) -> float:
return (mesh as CylinderMesh).height * 0.5 return (mesh as CylinderMesh).height * 0.5
if mesh is SphereMesh: if mesh is SphereMesh:
return (mesh as SphereMesh).radius return (mesh as SphereMesh).radius
return 0.0 return 0.0
## Höhe des Mesh-MITTELPUNKTS über dem Fußpunkt — für das Drehzentrum der
## Objekt-Auswahl. Bei Grundformen = half_height (Mesh ist zentriert); beim
## Compound-ArrayMesh = AABB-Mitte (schon in Fußpunkt-Koordinaten gebacken).
static func center_height(mesh: Mesh) -> float:
var aabb := mesh.get_aabb()
return half_height(mesh) + aabb.position.y + aabb.size.y * 0.5

View file

@ -52,7 +52,7 @@ func _test_manifest_load() -> void:
var manifest := _load_manifest() var manifest := _load_manifest()
_check(not manifest.is_empty(), "Manifest lädt und ist ein Objekt") _check(not manifest.is_empty(), "Manifest lädt und ist ein Objekt")
var lib: Dictionary = manifest.get("objects", {}) var lib: Dictionary = manifest.get("objects", {})
_check(lib.size() == 12, "Katalog hat die 12 etablierten Objekte (ist %d)" % lib.size()) _check(lib.size() == 15, "Katalog hat die 15 etablierten Objekte (ist %d)" % lib.size())
var all_ok := true var all_ok := true
for oid in lib.keys(): for oid in lib.keys():
var entry: Dictionary = lib[oid] var entry: Dictionary = lib[oid]
@ -254,7 +254,7 @@ func _test_crud_undo_all_objects() -> void:
all_ok = false all_ok = false
if first: if first:
first = false first = false
_check(all_ok, "alle 11 Objekte: setzen/drehen/verschieben/löschen je mit Undo+Redo sauber") _check(all_ok, "alle %d Objekte: setzen/drehen/verschieben/löschen je mit Undo+Redo sauber" % lib.size())
# -------------------------------------------------------- svmap-Roundtrip ---- # -------------------------------------------------------- svmap-Roundtrip ----