883 lines
No EOL
37 KiB
GDScript
883 lines
No EOL
37 KiB
GDScript
extends Node3D
|
||
## Baut die visuelle Repräsentation der Map im Editor-Viewport:
|
||
## Terrain-Höhenfeld-Mesh + Maßstabsgitter + Himmel/Licht
|
||
## + Echte GLB-Meshes für Bibliotheks-Objekte (GlbMeshStore; ohne GLB =
|
||
## Platzhalter-Mesh aus dem Kollisions-Eintrag) + Spawn-Marker mit Beschriftung.
|
||
## ##
|
||
## Objekt-Marker werden in object_markers gemerkt (Meta: obj_index,
|
||
## pick_radius) — Auswahl/Werkzeuge (interaction.gd) greifen darauf zu,
|
||
## statt die Szene selbst zu durchsuchen.
|
||
|
||
const GRID_COLOR := Color(0.35, 0.45, 0.55, 0.6)
|
||
const AXIS_COLOR := Color(0.9, 0.7, 0.2, 0.9)
|
||
const OBJECT_COLOR := Color(0.55, 0.75, 0.95)
|
||
const TEAM_COLORS := [Color(0.3, 0.6, 1.0), Color(1.0, 0.35, 0.3)] # Blau / Rot
|
||
# Auswahl-Markierung (U6, t_639135dd): Farbe + Breite kommen aus dem Theme
|
||
# (eine Quelle) — Auswahl-Kontur, schwächere Hover-Kontur, dünn-gestrichelte
|
||
# Partner-Kontur. Kein Vollflächen-Orange mehr: das Grundmaterial (mit Textur)
|
||
# bleibt sichtbar, die Markierung lebt als Außenhüllen-Kontur darum herum.
|
||
const THEME_PATH := "res://core/theme/editor_theme.tres"
|
||
const OUTLINE_SHADER := "res://core/shaders/selection_outline.gdshader"
|
||
# Fallback, falls das Theme (noch) nicht geladen werden kann.
|
||
const OUTLINE_FALLBACK := Color(1.0, 0.6, 0.15, 0.95)
|
||
|
||
# Wetter → Licht/Himmel — 1:1-SPIEGEL aus client/scripts/world/weather_presets.gd
|
||
# (Karte t_f72913d4: Editor-Vorschau zeigt exakt was der Client zeigt — dieselbe
|
||
# Projekt-Konvention wie KolliderBau als 1:1-Spiegel von svmap_world, t_1da96275).
|
||
# Bei Preset-Änderungen im Client HIER MITPFLEGEN. Schlüssel = Client-Keys.
|
||
const WEATHER_LIGHT := {
|
||
"mittag": {
|
||
"sky_top": Color(0.30, 0.54, 0.86), "sky_horizon": Color(0.72, 0.83, 0.92),
|
||
"ground_bottom": Color(0.20, 0.23, 0.27), "ground_horizon": Color(0.66, 0.75, 0.83),
|
||
"sky_energy": 1.0,
|
||
"sun_deg": Vector2(-52.0, 30.0), "sun_energy": 1.2, "sun_color": Color(1.0, 0.98, 0.92),
|
||
"ambient_source": "sky", "ambient_color": Color(0.60, 0.70, 0.85), "ambient_energy": 1.0,
|
||
"fog_enabled": false, "fog_color": Color(0.80, 0.85, 0.90),
|
||
"fog_density": 0.0, "fog_sky_affect": 0.0,
|
||
"sky_panorama": false,
|
||
"panorama_fog_color": Color(0.93, 0.80, 0.66), "panorama_energy": 1.0,
|
||
},
|
||
"abend": {
|
||
"sky_top": Color(0.25, 0.30, 0.55), "sky_horizon": Color(0.95, 0.55, 0.28),
|
||
"ground_bottom": Color(0.16, 0.13, 0.14), "ground_horizon": Color(0.80, 0.45, 0.25),
|
||
"sky_energy": 1.0,
|
||
"sun_deg": Vector2(-8.0, 250.0), "sun_energy": 0.9, "sun_color": Color(1.0, 0.62, 0.35),
|
||
"ambient_source": "color", "ambient_color": Color(0.55, 0.40, 0.32), "ambient_energy": 0.8,
|
||
"fog_enabled": true, "fog_color": Color(0.85, 0.55, 0.35),
|
||
"fog_density": 0.004, "fog_sky_affect": 0.2,
|
||
"sky_panorama": false,
|
||
"panorama_fog_color": Color(0.95, 0.72, 0.48), "panorama_energy": 0.8,
|
||
},
|
||
"regen": {
|
||
"sky_top": Color(0.38, 0.41, 0.45), "sky_horizon": Color(0.55, 0.58, 0.62),
|
||
"ground_bottom": Color(0.22, 0.23, 0.25), "ground_horizon": Color(0.50, 0.53, 0.57),
|
||
"sky_energy": 0.9,
|
||
"sun_deg": Vector2(-40.0, 120.0), "sun_energy": 0.25, "sun_color": Color(0.75, 0.78, 0.82),
|
||
"ambient_source": "color", "ambient_color": Color(0.55, 0.58, 0.62), "ambient_energy": 1.0,
|
||
"fog_enabled": true, "fog_color": Color(0.55, 0.58, 0.62),
|
||
"fog_density": 0.015, "fog_sky_affect": 0.6,
|
||
"sky_panorama": false,
|
||
"panorama_fog_color": Color(0.76, 0.72, 0.68), "panorama_energy": 0.55,
|
||
},
|
||
"nacht": {
|
||
"sky_top": Color(0.015, 0.03, 0.08), "sky_horizon": Color(0.05, 0.08, 0.15),
|
||
"ground_bottom": Color(0.01, 0.015, 0.025), "ground_horizon": Color(0.04, 0.06, 0.11),
|
||
"sky_energy": 1.0,
|
||
"sun_deg": Vector2(-65.0, 200.0), "sun_energy": 0.35, "sun_color": Color(0.62, 0.72, 1.0),
|
||
"ambient_source": "color", "ambient_color": Color(0.10, 0.14, 0.25), "ambient_energy": 0.5,
|
||
"fog_enabled": true, "fog_color": Color(0.04, 0.06, 0.11),
|
||
"fog_density": 0.006, "fog_sky_affect": 0.4,
|
||
"sky_panorama": false,
|
||
"panorama_fog_color": Color(0.10, 0.12, 0.18), "panorama_energy": 0.10,
|
||
},
|
||
}
|
||
|
||
## Pack-Panorama-Skybox (Karte t_f72913d4): die Textur liegt im Client-Repo
|
||
## (Nachbar-Klon — KEINE Duplikat-Pflicht im Editor, siehe Spec) und wird über
|
||
## einen absoluten Pfad geladen; fehlt sie → Fallback ProceduralSky.
|
||
const PANORAMA_SKY_CLIENT_REL := "assets/textures/military_desert/Skybox_3.png"
|
||
|
||
## Absoluter Pfad der Pack-Skybox im Nachbar-Klon ("" = kein Klon gefunden).
|
||
static func panorama_sky_pfad() -> String:
|
||
var root := TestRun.find_client(ProjectSettings.globalize_path("res://"),
|
||
TestRun.load_cfg())
|
||
if root.is_empty():
|
||
return ""
|
||
return root.path_join(PANORAMA_SKY_CLIENT_REL)
|
||
|
||
var selected_index := -1 # aktuelles Auswahl-Objekt (Kontur-Markierung)
|
||
var selected_indices: Array = [] # komplette Auswahl (Karte t_f6f29d86 — Mehrfachauswahl)
|
||
var hover_index := -1 # Hover-Objekt (schwächste Kontur; U6-Abstufung)
|
||
var object_markers: Array = [] # Node3D je Objekt (gleiche Reihenfolge wie doc.objects)
|
||
var fehlende_glbs: Array = [] # OIDs ohne echte GLB (Platzhalter-Fallback — Statuszeilen-Hinweis)
|
||
var zone_root: Node3D = null # Conquest-Zonen bzw. Hills (Spielmodus), Kinder je Zone
|
||
var spawn_markers: Array = [] # Node3D je Spawn-Point (Meta: spawn_team/spawn_id/spawn_radius)
|
||
var terrain_view # TerrainMesh-Ansicht (script-erweitert, Variant)
|
||
var grid_node: MeshInstance3D # Maßstabsgitter (Toggle „⊞ Gitter“, Karte t_7ac70014)
|
||
|
||
var _doc_ref: Dictionary = {} # letztes rebuild-Doc (Auswahl-Markierung)
|
||
|
||
var _base_mat: StandardMaterial3D
|
||
var _sym_partners: Array = [] # Indizes der Zwillinge der aktuellen Auswahl
|
||
# Kontur-Materialien (U6): Auswahl / Hover / Spiegel-Zwilling — Farben und
|
||
# Breiten kommen aus dem Theme, der Shader ist die Außenhüllen-Technik.
|
||
var _outline_mat_sel: ShaderMaterial
|
||
var _outline_mat_hover: ShaderMaterial
|
||
var _outline_mat_partner: ShaderMaterial
|
||
|
||
|
||
func _ready() -> void:
|
||
rebuild({}, {})
|
||
|
||
|
||
## Komplette Szene aus dem Map-Dict neu aufbauen. manifest = Bibliotheks-Registry.
|
||
func rebuild(data: Dictionary, manifest: Dictionary) -> void:
|
||
for child in get_children():
|
||
child.queue_free()
|
||
object_markers = []
|
||
spawn_markers = []
|
||
zone_root = null
|
||
grid_node = null
|
||
_doc_ref = data
|
||
_base_mat = StandardMaterial3D.new()
|
||
_base_mat.albedo_color = OBJECT_COLOR
|
||
_base_mat.roughness = 0.8
|
||
# U6 (t_639135dd): keine Vollfarben-Materialien mehr — die Markierung
|
||
# (Auswahl/Hover/Partner) lebt als Außenhüllen-Kontur um das Modell,
|
||
# Farben + Breiten aus dem Theme.
|
||
_markierung_theme_laden()
|
||
_sym_partners = []
|
||
|
||
if data.is_empty():
|
||
return
|
||
|
||
var terrain: Dictionary = data.get("terrain", {})
|
||
var size_x := float(terrain.get("size_x_m", 1024.0))
|
||
var size_z := float(terrain.get("size_z_m", 1024.0))
|
||
var ground_y := float(terrain.get("min_height_m", 0.0))
|
||
var weather := str(data.get("weather", "mittag"))
|
||
# Skybox-Option (Karte t_f72913d4): optionales svmap-Feld `weather_sky`
|
||
# ("sky_panorama"/"prozedural"/leer) — dieselbe Semantik wie im Client.
|
||
var weather_sky := str(data.get("weather_sky", ""))
|
||
|
||
_build_environment(weather, weather_sky)
|
||
_build_terrain(data)
|
||
_build_wasser(data) # Karte t_28fdef03: nach dem Terrain (Senken füllen sich)
|
||
_build_grid(size_x, size_z, ground_y)
|
||
# Gitter default AUS über sichtbarem Terrain (Karte t_7ac70014): das
|
||
# Terrain ist die Bodendarstellung — das Gitter lag als Linien-Overlay
|
||
# DARÜBER und wirkte wie ein „unfertiges“ Editor-Raster. Ohne Terrain-
|
||
# Block bleibt es als Boden-Referenz sichtbar.
|
||
if grid_node != null:
|
||
grid_node.visible = terrain.is_empty()
|
||
_build_objects(data.get("objects", []), manifest)
|
||
_rebuild_spawn_markers(data)
|
||
rebuild_zone_markers(data)
|
||
|
||
|
||
## Terrain als echtes Höhenfeld-Mesh statt flacher Ebene (Karte t_9aba04f1):
|
||
## der Terrain-Pinsel schreibt die Vertices live neu (terrain_view.update_terrain).
|
||
func _build_terrain(data: Dictionary) -> void:
|
||
terrain_view = Node3D.new()
|
||
terrain_view.set_script(load("res://core/viewport/terrain_mesh.gd"))
|
||
add_child(terrain_view)
|
||
terrain_view.call("rebuild_from_doc", data)
|
||
|
||
|
||
## Wasserfläche (Karte t_28fdef03): EINE transparente Fläche auf `water_level`
|
||
## über der GANZEN Map (SvmapWasser.baue — Senken unter der Höhe füllen sich
|
||
## automatisch). OHNE Feld → kein Wasser (Legacy-Maps unverändert). Dasselbe
|
||
## Material-Verfahren wie der Client (SvmapWasser.make_material — Identitäts-
|
||
## Vertrag der Vorschau: Editor zeigt exakt, was der Quest-Client zeigt).
|
||
## Leicht ÜBER dem Terrain eingehängt (y-Offset +0.01 über Reihenfolge), aber
|
||
## die Fläche selbst liegt exakt auf der Wasserhöhe (Z-Fight-Vermeidung macht
|
||
## der Shader über die Tiefen-Textur, nicht über einen Off-Aufschlag).
|
||
func _build_wasser(data: Dictionary) -> void:
|
||
var level := SvmapWasser.level(data)
|
||
if not is_finite(level):
|
||
return # Legacy/fehlendes Feld — kein Wasser, nichts bauen.
|
||
var terrain: Dictionary = data.get("terrain", {})
|
||
var wetter := str(data.get("weather", "mittag"))
|
||
var wasser := SvmapWasser.baue(terrain, level, wetter)
|
||
if wasser != null:
|
||
add_child(wasser)
|
||
|
||
|
||
## Live-Update während eines Pinsel-Strichs (aus terrain_tool.gd): nur die
|
||
## Vertices werden neu geschrieben — Grid/Objekte/Marker bleiben unangetastet.
|
||
## mats (optional) = Material-Schicht (Karte t_603ec1a8, Malen-Modus) —
|
||
## leer = terrain_mesh verwendet die zuletzt gesehene Schicht weiter (Cache,
|
||
## Karte t_55ed041a); nur bei NIE gesehener Schicht (Legacy-v1) gilt Gras.
|
||
## splat_rect_cells (optional, Karte t_7a0a6b79) = Zellrechteck des Mal-
|
||
## Schritts (inkl. Spiegelzellen) — terrain_mesh leitet nur die Splat-Region
|
||
## neu ab (Sub-Rect-Update, kein Voll-Rebuild pro Mal-Frame).
|
||
func update_terrain(heights: PackedFloat32Array, mats: PackedByteArray = PackedByteArray(),
|
||
splat_rect_cells: Rect2i = Rect2i()) -> void:
|
||
if terrain_view != null and is_instance_valid(terrain_view):
|
||
terrain_view.call("update_heights", heights, 0, 0.0, 0.0, mats, splat_rect_cells)
|
||
|
||
|
||
## Misch-Schicht für das nächste Live-Update (Malen mit weichem Rand, Karte 25.10.).
|
||
func set_terrain_mix(mix_b: PackedByteArray, mix_w: PackedByteArray) -> void:
|
||
if terrain_view != null and is_instance_valid(terrain_view):
|
||
terrain_view.call("set_mix", mix_b, mix_w)
|
||
|
||
|
||
## Nur das Terrain neu aufbauen (nach Strich-Commit/Undo) — Objekt-Marker und
|
||
## Auswahl bleiben bestehen, kein Flackern der ganzen Szene.
|
||
func rebuild_terrain_from_doc(data: Dictionary) -> void:
|
||
if terrain_view != null and is_instance_valid(terrain_view):
|
||
terrain_view.call("rebuild_from_doc", data)
|
||
|
||
|
||
## Himmel/Licht — 1:1-Spiegel von client autoload/weather.gd._apply_environment
|
||
## (Karte t_f72913d4): Map-Override > Preset-Key > ProceduralSky; ACES-Tonemap;
|
||
## Fog koppelt im Panorama-Fall an den Skybox-Horizont (aerial perspective).
|
||
## Kein Glow/Bloom/Volumetric — dokumentierte Absage (Mobile/Perf).
|
||
func _build_environment(weather: String, sky_override: String = "") -> void:
|
||
var preset: Dictionary = WEATHER_LIGHT.get(weather, WEATHER_LIGHT["mittag"])
|
||
var env := Environment.new()
|
||
var sky := Sky.new()
|
||
# Katalog-ID (weather_sky) > Preset-Key `sky_panorama` > ProceduralSky.
|
||
var pano_id := ""
|
||
if sky_override == "prozedural":
|
||
pass
|
||
elif sky_override != "":
|
||
pano_id = sky_override
|
||
elif bool(preset.get("sky_panorama", false)):
|
||
pano_id = SkyboxKatalog.ID_PACK_WUESTE
|
||
var panorama := false
|
||
if pano_id != "":
|
||
var tex := SkyboxKatalog.panorama_textur(pano_id)
|
||
if tex != null:
|
||
var pano_mat := PanoramaSkyMaterial.new()
|
||
pano_mat.panorama = tex
|
||
pano_mat.energy_multiplier = float(preset.get("panorama_energy", 1.0))
|
||
sky.sky_material = pano_mat
|
||
panorama = true
|
||
else:
|
||
push_warning("MAPBUILDER: weather_sky=%s, aber Skybox-Textur fehlt/unbekannt — Fallback ProceduralSky" % pano_id)
|
||
if sky.sky_material == null:
|
||
var sky_mat := ProceduralSkyMaterial.new()
|
||
sky_mat.sky_top_color = preset["sky_top"]
|
||
sky_mat.sky_horizon_color = preset["sky_horizon"]
|
||
sky_mat.ground_bottom_color = preset["ground_bottom"]
|
||
sky_mat.ground_horizon_color = preset["ground_horizon"]
|
||
sky_mat.sky_energy_multiplier = float(preset["sky_energy"])
|
||
sky.sky_material = sky_mat
|
||
env.background_mode = Environment.BG_SKY
|
||
env.sky = sky
|
||
# Ambient: "sky" = aus dem Himmel ableiten, "color" = feste Farbe (Client-Logik).
|
||
if str(preset.get("ambient_source", "color")) == "sky":
|
||
env.ambient_light_source = Environment.AMBIENT_SOURCE_SKY
|
||
else:
|
||
env.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR
|
||
env.ambient_light_color = preset["ambient_color"]
|
||
env.ambient_light_energy = float(preset["ambient_energy"])
|
||
# Tonemap ACES — derselbe filmische Look wie der Client; Grading bewusst
|
||
# NUR über Tonemap + Preset-Farben (kein Fullscreen-Adjustment-Pass).
|
||
env.tonemap_mode = Environment.TONE_MAPPER_ACES
|
||
env.fog_enabled = bool(preset["fog_enabled"])
|
||
env.fog_light_color = SkyboxKatalog.fog_farbe(sky_override,
|
||
preset.get("panorama_fog_color", preset["fog_color"]),
|
||
float(preset.get("panorama_energy", 1.0))) if panorama else preset["fog_color"]
|
||
env.fog_density = float(preset["fog_density"])
|
||
env.fog_sky_affect = float(preset["fog_sky_affect"])
|
||
var we := WorldEnvironment.new()
|
||
we.environment = env
|
||
add_child(we)
|
||
|
||
var sun := DirectionalLight3D.new()
|
||
var deg: Vector2 = preset["sun_deg"]
|
||
sun.rotation_degrees = Vector3(deg.x, deg.y, 0.0)
|
||
sun.light_energy = float(preset["sun_energy"])
|
||
sun.light_color = preset["sun_color"]
|
||
sun.shadow_enabled = true
|
||
add_child(sun)
|
||
|
||
|
||
## Maßstabsgitter: Linien alle 100 m, gelbe Mittelachsen (X rot? — Z-Achse
|
||
## blau wäre Godot-Konvention; hier neutral gelb für gute Sichtbarkeit).
|
||
func _build_grid(size_x: float, size_z: float, ground_y: float) -> void:
|
||
var mesh := ImmediateMesh.new()
|
||
mesh.surface_begin(Mesh.PRIMITIVE_LINES)
|
||
var step := _grid_step(size_x)
|
||
var half_x := size_x / 2.0
|
||
var half_z := size_z / 2.0
|
||
var y := ground_y + 0.05
|
||
var x := -half_x
|
||
while x <= half_x + 0.01:
|
||
var major: bool = absf(fmod(x, step * 5.0)) < 0.01 or absf(x) < 0.01
|
||
_add_line(mesh, Vector3(x, y, -half_z), Vector3(x, y, half_z),
|
||
AXIS_COLOR if absf(x) < 0.01 else (GRID_COLOR if not major else GRID_COLOR))
|
||
x += step
|
||
var z := -half_z
|
||
while z <= half_z + 0.01:
|
||
_add_line(mesh, Vector3(-half_x, y, z), Vector3(half_x, y, z),
|
||
AXIS_COLOR if absf(z) < 0.01 else GRID_COLOR)
|
||
z += step
|
||
mesh.surface_end()
|
||
var mi := MeshInstance3D.new()
|
||
mi.mesh = mesh
|
||
var mat := StandardMaterial3D.new()
|
||
mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
||
mat.vertex_color_use_as_albedo = true
|
||
mi.material_override = mat
|
||
grid_node = mi
|
||
add_child(mi)
|
||
|
||
|
||
## Gitter ohne Rebuild zu-/abschalten (Toggle in der Top-Bar, Karte t_7ac70014).
|
||
func set_grid_visible(visible_now: bool) -> void:
|
||
if grid_node != null and is_instance_valid(grid_node):
|
||
grid_node.visible = visible_now
|
||
|
||
|
||
func grid_is_visible() -> bool:
|
||
return grid_node != null and is_instance_valid(grid_node) and grid_node.visible
|
||
|
||
|
||
func _grid_step(size_m: float) -> float:
|
||
# Ziel: ~10–20 Linien pro Achse, runde Werte.
|
||
var raw := size_m / 16.0
|
||
var candidates := [10.0, 25.0, 50.0, 100.0, 250.0, 500.0]
|
||
for c in candidates:
|
||
if raw <= float(c):
|
||
return float(c)
|
||
return 1000.0
|
||
|
||
|
||
func _add_line(mesh: ImmediateMesh, from: Vector3, to: Vector3, color: Color) -> void:
|
||
mesh.surface_set_color(color)
|
||
mesh.surface_add_vertex(from)
|
||
mesh.surface_set_color(color)
|
||
mesh.surface_add_vertex(to)
|
||
|
||
|
||
## Platzhalter je Objekt: Form aus dem Bibliotheks-Manifest (wie der Server-
|
||
## Kollider), Position = Objekt-Fußpunkt. ECHTES GLB-Mesh vorziehen
|
||
## (Karte t_9b50b231, GlbMeshStore): vorhanden = echtes Modell mit Texturen,
|
||
## fehlend = Platzhalter + Eintrag in fehlende_glbs (Statuszeilen-Hinweis).
|
||
## Marker-Root bekommt Metas für Auswahl/Anfassen: obj_index, pick_radius
|
||
## (Kugel-Approximation), half_height, glb (true = echtes Modell — keine
|
||
## Grundfarbe drüberlegen, sonst sind die Texturen weg).
|
||
func _build_objects(objects: Array, manifest: Dictionary) -> void:
|
||
var lib: Dictionary = manifest.get("objects", {})
|
||
fehlende_glbs = []
|
||
for i in objects.size():
|
||
var obj = objects[i]
|
||
if not (obj is Dictionary):
|
||
object_markers.append(null)
|
||
continue
|
||
var oid := str(obj.get("id", ""))
|
||
if not lib.has(oid):
|
||
object_markers.append(null)
|
||
continue # Validierung fängt das — defensiv überspringen.
|
||
var entry: Dictionary = lib[oid]
|
||
var col: Dictionary = entry.get("collision", {})
|
||
var scale := float(obj.get("scale", 1.0))
|
||
var mesh := GlbMeshStore.mesh_fuer(oid)
|
||
var echt := mesh != null
|
||
if not echt:
|
||
if GlbMeshStore.fehlt(oid):
|
||
fehlende_glbs.append(oid)
|
||
mesh = MeshShapes.make_placeholder(str(col.get("shape", "box")), col, scale)
|
||
var pos: Array = obj.get("pos", [0.0, 0.0, 0.0])
|
||
var marker := Node3D.new()
|
||
marker.position = Vector3(float(pos[0]), float(pos[1]), float(pos[2]))
|
||
# Karte t_e166e656: 3-Achsen-Rotation — `rot` (Euler xyz) oder Fallback
|
||
# rot_y_deg, identische Regel wie im Client/Server (SvmapIO.objekt_rotation).
|
||
marker.rotation_degrees = SvmapIO.objekt_rotation(obj)
|
||
marker.set_meta("obj_index", i)
|
||
var mi := MeshInstance3D.new()
|
||
mi.mesh = mesh
|
||
if echt:
|
||
# Normalisiertes GLB liegt schon in Fußpunkt-Koordinaten
|
||
# (Unterkante y = 0) → KEIN half_height-Lift; Objekt-Maßstab
|
||
# über Node-Scale. AABB-Metas mal scale (Node-Scale geht nicht
|
||
# in mesh.get_aabb() ein).
|
||
mi.scale = Vector3.ONE * scale
|
||
else:
|
||
mi.position.y = MeshShapes.half_height(mesh)
|
||
# U6 (t_639135dd): das Grundmaterial wird NIE MEHR durch die Auswahl
|
||
# ersetzt — echte GLB-Objekte tragen ihre Texturen weiter (override null),
|
||
# Platzhalter die Editor-Grundfarbe. Markiert wird per Kontur (unten).
|
||
mi.material_override = null if echt else _base_mat
|
||
marker.add_child(mi)
|
||
var half: Vector3 = (mesh.get_aabb().size * (scale if echt else 1.0) * 0.5) as Vector3
|
||
marker.set_meta("pick_radius", half.length() + maxf(0.6, half.y * 0.25))
|
||
marker.set_meta("half_height",
|
||
MeshShapes.center_height(mesh) * (scale if echt else 1.0))
|
||
marker.set_meta("glb", echt)
|
||
object_markers.append(marker)
|
||
add_child(marker)
|
||
# Farbe je Objekt (Karte t_b0edb807): optionales Feld "tint" — nur
|
||
# Objekte MIT Feld bekommen die Überschreibung (Rückwärtskompatibel).
|
||
# Die Farbe bleibt jetzt auch bei Markierung sichtbar (U6: Auswahl = Kontur,
|
||
# Grundmaterial bleibt) — kein „Orange gewinnt“ mehr.
|
||
if SvmapTint.hat(obj):
|
||
SvmapTint.anwenden(marker, SvmapTint.von(obj))
|
||
# Markierung (Auswahl/Hover/Partner) als Kontur-Hülle anlegen.
|
||
_outline_anwenden(marker, i)
|
||
|
||
|
||
## Auswahl nachträglich umfärben (ohne ganzen Rebuild) — z. B. nach Drag.
|
||
## Einzel-Variante bleibt für Rückwärtskompatibilität bestehen.
|
||
func set_selected(idx: int) -> void:
|
||
selected_index = idx
|
||
set_selected_multi([idx] if idx >= 0 else [])
|
||
|
||
|
||
## Mehrfachauswahl (Karte t_f6f29d86): ALLE markierten Objekte bekommen die
|
||
## Auswahl-Kontur (U6, t_639135dd) — Gruppen/Mehrfachauswahl sind auf einen
|
||
## Blick erkennbar, OHNE die Textur zu übermalen. Das Grundmaterial bleibt
|
||
## überall sichtbar: echte GLB-Objekte tragen ihre Texturen weiter (Karte
|
||
## t_9b50b231), Platzhalter die Editor-Grundfarbe — material_override wird
|
||
## von der Markierung nicht mehr angefasst.
|
||
func set_selected_multi(indices: Array) -> void:
|
||
selected_indices = indices.duplicate()
|
||
selected_index = indices.back() if not indices.is_empty() else -1
|
||
# sym-Zwillinge (M3, t_34908425): aus dem doc ableiten — Partner der
|
||
# Auswahl, die nicht selbst ausgewählt sind, bekommen die dünnere,
|
||
# gestrichelte Partner-Kontur (U6-Abstufung statt dunkler Vollfarbe).
|
||
_sym_partners = _sym_partners_of(indices)
|
||
var objects: Array = _doc_ref.get("objects", [])
|
||
for marker in object_markers:
|
||
if marker == null or not is_instance_valid(marker):
|
||
continue
|
||
var idx := int(marker.get_meta("obj_index", -1))
|
||
_outline_anwenden(marker, idx)
|
||
# Tint-Abgleich (Karte t_b0edb807): ALLE Objekte zeigen ihre doc-Farbe
|
||
# (oder — nach „Zurücksetzen“ — wieder die Pack-Farbe). Der Tint bleibt
|
||
# jetzt auch bei Markierung sichtbar (Auswahl = Kontur um das Modell).
|
||
if idx >= 0 and idx < objects.size() and objects[idx] is Dictionary:
|
||
var obj: Dictionary = objects[idx]
|
||
if SvmapTint.hat(obj):
|
||
SvmapTint.anwenden(marker, SvmapTint.von(obj))
|
||
else:
|
||
SvmapTint.zuruecknehmen(marker)
|
||
|
||
|
||
func _is_partner(idx: int) -> bool:
|
||
return _sym_partners.has(idx)
|
||
|
||
|
||
## sym-Partner der Auswahl aus dem doc bestimmen (M3, t_34908425): Objekte mit
|
||
## sym-Kennung, deren Partner NICHT selbst in der Auswahl ist. Keine sym-Felder
|
||
## im doc (alte Karten, Modus Aus) → leer, Verhalten wie bisher.
|
||
func _sym_partners_of(indices: Array) -> Array:
|
||
var objects: Array = _doc_ref.get("objects", [])
|
||
var out: Array = []
|
||
for i in indices:
|
||
if not (i is int) or i < 0 or i >= objects.size() or not (objects[i] is Dictionary):
|
||
continue
|
||
var key := SymmetryCore.sym_of(objects[i] as Dictionary)
|
||
if key.is_empty():
|
||
continue
|
||
var t := SymmetryCore.find_partner_index(objects, key, i)
|
||
if t >= 0 and not indices.has(t) and not out.has(t):
|
||
out.append(t)
|
||
return out
|
||
|
||
|
||
## Tint frisch aus dem doc auf die MARKIERTEN Objekte anwenden (Inspector-
|
||
## Änderung ohne Rebuild). U6: die Farbe bleibt auch bei Markierung sichtbar
|
||
## (Auswahl = Kontur um das Modell) — kein „Orange gewinnt“-Skip mehr.
|
||
func tint_refresh(indices: Array) -> void:
|
||
var objects: Array = _doc_ref.get("objects", [])
|
||
for i in indices:
|
||
if i < 0 or i >= object_markers.size():
|
||
continue
|
||
var marker = object_markers[i]
|
||
if marker == null or not is_instance_valid(marker):
|
||
continue
|
||
if i >= objects.size() or not (objects[i] is Dictionary):
|
||
continue
|
||
var obj: Dictionary = objects[i]
|
||
if SvmapTint.hat(obj):
|
||
SvmapTint.anwenden(marker, SvmapTint.von(obj))
|
||
else:
|
||
SvmapTint.zuruecknehmen(marker)
|
||
|
||
|
||
func _is_selected(idx: int) -> bool:
|
||
return selected_indices.has(idx)
|
||
|
||
|
||
## Spawn-Zonen (Karte t_c4162c5d): JEDE Zone ist ein flacher, teamfarbiger
|
||
## Kreis auf dem Boden (Radius = design_notes.spawn_radius_m, Editor-eigen)
|
||
## + Label3D mit der Punkt-ID. Marker-Root bekommt Metas für das Spawn-Tool
|
||
## (spawn_team, spawn_id, spawn_radius → Anfassen/Regel).
|
||
func _rebuild_spawn_markers(data: Dictionary) -> void:
|
||
for marker in spawn_markers:
|
||
if marker != null and is_instance_valid(marker):
|
||
marker.queue_free()
|
||
spawn_markers = []
|
||
var zones = SpawnOps.zones_of(data) # bearbeiteter Spawn-Satz (Basis oder Modus)
|
||
if not (zones is Dictionary):
|
||
return
|
||
for team_key in zones.keys():
|
||
if not (zones[team_key] is Dictionary):
|
||
continue
|
||
var team := 0 if str(team_key) == "all" else int(team_key)
|
||
var zone: Dictionary = zones[team_key]
|
||
var points = zone.get("points", [])
|
||
if not (points is Array):
|
||
continue
|
||
for p in points:
|
||
if not (p is Dictionary):
|
||
continue
|
||
var pid: String = str(p.get("id", "?"))
|
||
var pos: Array = p.get("pos", [0.0, 0.0, 0.0])
|
||
var marker := _spawn_marker(
|
||
Vector3(float(pos[0]), float(pos[1]), float(pos[2])),
|
||
team, pid, SpawnOps.zone_radius(data, pid))
|
||
var yaw_col: Color = TEAM_COLORS[team] if team < TEAM_COLORS.size() else Color.WHITE
|
||
marker.set_meta("yaw_color", yaw_col)
|
||
marker.add_child(_yaw_arrow(SpawnOps.zone_radius(data, pid),
|
||
SpawnOps.effective_yaw(data, p), SpawnOps.has_yaw(p), yaw_col))
|
||
spawn_markers.append(marker)
|
||
add_child(marker)
|
||
|
||
|
||
## Zonen des Spielmodus: Conquest = gelb, King of the Hill = orange (mit Nummer der
|
||
## Wanderreihenfolge). Boden-Kreis + durchscheinender Zylinder + Name.
|
||
func rebuild_zone_markers(data: Dictionary) -> void:
|
||
if zone_root != null and is_instance_valid(zone_root):
|
||
zone_root.queue_free()
|
||
zone_root = null
|
||
var mode := ModusKatalog.mode_of(data)
|
||
if not ModusKatalog.has_zones(mode):
|
||
return
|
||
zone_root = Node3D.new()
|
||
zone_root.name = "Zonen"
|
||
add_child(zone_root)
|
||
var col := Color(1.0, 0.8, 0.2) if mode == ModusKatalog.MODE_CONQUEST else Color(1.0, 0.45, 0.15)
|
||
var zlist := ModusKatalog.zones(data, mode)
|
||
for zi in zlist.size():
|
||
var cp = zlist[zi]
|
||
if not (cp is Dictionary):
|
||
continue
|
||
var pos: Array = cp.get("pos", [0, 0, 0])
|
||
var r := float(cp.get("radius_m", ModusKatalog.DEFAULT_RADIUS))
|
||
var node := Node3D.new()
|
||
node.name = "CP_%s" % str(cp.get("id", ""))
|
||
node.position = Vector3(float(pos[0]), float(pos[1]), float(pos[2]))
|
||
var mat := StandardMaterial3D.new()
|
||
mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
||
mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
||
mat.cull_mode = BaseMaterial3D.CULL_DISABLED
|
||
mat.albedo_color = Color(col.r, col.g, col.b, 0.18)
|
||
var cyl := MeshInstance3D.new()
|
||
var cm := CylinderMesh.new()
|
||
cm.top_radius = r
|
||
cm.bottom_radius = r
|
||
cm.height = 8.0
|
||
cm.cap_top = false
|
||
cm.cap_bottom = false
|
||
cyl.mesh = cm
|
||
cyl.material_override = mat
|
||
cyl.position.y = 4.0
|
||
node.add_child(cyl)
|
||
var ring := MeshInstance3D.new()
|
||
var tm := TorusMesh.new()
|
||
tm.inner_radius = r - 0.2
|
||
tm.outer_radius = r + 0.2
|
||
tm.rings = 48
|
||
tm.ring_segments = 6
|
||
ring.mesh = tm
|
||
var rmat := StandardMaterial3D.new()
|
||
rmat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
||
rmat.albedo_color = col
|
||
ring.material_override = rmat
|
||
ring.position.y = 0.15
|
||
node.add_child(ring)
|
||
var label := Label3D.new()
|
||
label.text = ("⚑ %s" if mode == ModusKatalog.MODE_CONQUEST else "♛ %d · %%s" % (zi + 1)) % str(cp.get("label", cp.get("id", "")))
|
||
label.position.y = 10.0
|
||
label.billboard = BaseMaterial3D.BILLBOARD_ENABLED
|
||
label.modulate = col
|
||
label.outline_size = 12
|
||
label.font_size = 160
|
||
label.pixel_size = 0.01
|
||
node.add_child(label)
|
||
if mode == ModusKatalog.MODE_CONQUEST: # Hills sind keine Spawns
|
||
node.set_meta("zone_id", str(cp.get("id", "")))
|
||
node.set_meta("yaw_radius", r)
|
||
node.add_child(_yaw_arrow(r, SpawnOps.effective_yaw(data, cp), SpawnOps.has_yaw(cp), col))
|
||
zone_root.add_child(node)
|
||
|
||
|
||
## Ein Spawn-Marker bauen (flacher Kreis + Label). radius = angezeigter Zonen-Radius.
|
||
func _spawn_marker(pos: Vector3, team: int, pid: String, radius: float) -> Node3D:
|
||
var marker := Node3D.new()
|
||
marker.position = pos
|
||
marker.set_meta("spawn_team", team)
|
||
marker.set_meta("spawn_id", pid)
|
||
marker.set_meta("spawn_radius", radius)
|
||
marker.set_meta("spawn_pick_radius", maxf(radius + 2.0, 4.0))
|
||
marker.set_meta("spawn_selected", false)
|
||
marker.add_child(_spawn_disc(radius, team))
|
||
var label := Label3D.new()
|
||
label.text = pid
|
||
label.position.y = 2.5
|
||
label.billboard = BaseMaterial3D.BILLBOARD_ENABLED
|
||
label.modulate = TEAM_COLORS[team] if team < TEAM_COLORS.size() else Color.WHITE
|
||
label.outline_size = 12
|
||
label.font_size = 160
|
||
label.pixel_size = 0.01
|
||
marker.add_child(label)
|
||
return marker
|
||
|
||
|
||
## Pfeil der Blickrichtung: flach am Boden, zeigt lokal nach -Z, rotation.y = Blickrichtung.
|
||
## Gesetzte Richtung kräftig, Standard (zur Kartenmitte) blass grau. Ohne Tiefentest, damit
|
||
## Hügel und Objekte ihn nicht verdecken.
|
||
func _yaw_arrow(radius: float, yaw_deg: float, is_set: bool, col: Color) -> MeshInstance3D:
|
||
var length := yaw_arrow_length(radius)
|
||
var half_w := maxf(0.25, length * 0.04)
|
||
var head_w := half_w * 3.0
|
||
var head_l := minf(length * 0.4, head_w * 2.5)
|
||
var neck := -(length - head_l)
|
||
var verts := PackedVector3Array([
|
||
Vector3(-half_w, 0, 0), Vector3(half_w, 0, 0), Vector3(half_w, 0, neck),
|
||
Vector3(-half_w, 0, 0), Vector3(half_w, 0, neck), Vector3(-half_w, 0, neck),
|
||
Vector3(-head_w, 0, neck), Vector3(head_w, 0, neck), Vector3(0, 0, -length),
|
||
])
|
||
var arrays := []
|
||
arrays.resize(Mesh.ARRAY_MAX)
|
||
arrays[Mesh.ARRAY_VERTEX] = verts
|
||
var mesh := ArrayMesh.new()
|
||
mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)
|
||
var mat := StandardMaterial3D.new()
|
||
mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
||
mat.cull_mode = BaseMaterial3D.CULL_DISABLED
|
||
mat.no_depth_test = true
|
||
mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
||
mat.albedo_color = col.lightened(0.35) if is_set else Color(0.75, 0.75, 0.75, 0.55)
|
||
var arrow := MeshInstance3D.new()
|
||
arrow.name = "YawArrow"
|
||
arrow.mesh = mesh
|
||
arrow.material_override = mat
|
||
arrow.position.y = 0.4
|
||
arrow.rotation.y = deg_to_rad(yaw_deg)
|
||
arrow.set_meta("yaw_deg", yaw_deg)
|
||
arrow.set_meta("yaw_set", is_set)
|
||
arrow.set_meta("yaw_length", length)
|
||
return arrow
|
||
|
||
|
||
static func yaw_arrow_length(radius: float) -> float:
|
||
return maxf(radius, 3.0) + 2.0
|
||
|
||
|
||
func yaw_arrow_of(node: Node3D) -> MeshInstance3D:
|
||
return node.get_node_or_null("YawArrow") as MeshInstance3D
|
||
|
||
|
||
## Alle Pfeile zum Anfassen: [{kind:"spawn"|"zone", team, id, node, tip:Vector3}].
|
||
func yaw_handles() -> Array:
|
||
var out: Array = []
|
||
var add := func(node: Node3D, entry: Dictionary) -> void:
|
||
var arrow := yaw_arrow_of(node)
|
||
if arrow == null:
|
||
return
|
||
var yaw := deg_to_rad(float(arrow.get_meta("yaw_deg", 0.0)))
|
||
var length := float(arrow.get_meta("yaw_length", 0.0))
|
||
entry["node"] = node
|
||
entry["tip"] = node.global_position + Vector3(-sin(yaw), 0.0, -cos(yaw)) * length
|
||
out.append(entry)
|
||
for marker in spawn_markers:
|
||
if marker != null and is_instance_valid(marker):
|
||
add.call(marker, {"kind": "spawn", "team": int(marker.get_meta("spawn_team", -1)), "id": str(marker.get_meta("spawn_id", ""))})
|
||
if zone_root != null and is_instance_valid(zone_root):
|
||
for node in zone_root.get_children():
|
||
if node.has_meta("zone_id"):
|
||
add.call(node, {"kind": "zone", "team": -1, "id": str(node.get_meta("zone_id"))})
|
||
return out
|
||
|
||
|
||
## Kreisfläche EINES Markers (neu) bauen — Werkzeuge rufen nach Radius-Änderung
|
||
## style_spawn_marker auf. U6 (t_639135dd): die Fläche bleibt IMMER gleich
|
||
## dezent (Teamfarbe, halbtransparent) — die Auswahl zeigt sich als schmaler
|
||
## Kontur-Ring am Zonenrand (Theme-Farbe), nicht als aufgehellte Vollfläche.
|
||
func _spawn_disc(radius: float, team: int, _selected := false) -> MeshInstance3D:
|
||
var mi := MeshInstance3D.new()
|
||
var cyl := CylinderMesh.new()
|
||
cyl.top_radius = radius
|
||
cyl.bottom_radius = radius
|
||
cyl.height = 0.25
|
||
mi.mesh = cyl
|
||
mi.position.y = 0.12
|
||
var col: Color = TEAM_COLORS[team] if team >= 0 and team < TEAM_COLORS.size() else Color.WHITE
|
||
var mat := StandardMaterial3D.new()
|
||
mat.albedo_color = Color(col.r, col.g, col.b, 0.55)
|
||
mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
||
mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
||
mi.material_override = mat
|
||
return mi
|
||
|
||
|
||
## Dezenter Auswahl-Ring EINES Spawn-Markers (U6): dünner Torus am Zonenrand,
|
||
## Farbe aus dem Theme. Muss NUR bei Auswahl existieren (restlos weg bei
|
||
## Abwahl) — style_spawn_marker legt ihn an/entfernt ihn.
|
||
func _spawn_ring(radius: float) -> MeshInstance3D:
|
||
var mi := MeshInstance3D.new()
|
||
var torus := TorusMesh.new()
|
||
var t := maxf(0.15, radius * 0.06)
|
||
torus.inner_radius = maxf(0.01, radius - t)
|
||
torus.outer_radius = radius + t
|
||
mi.mesh = torus
|
||
mi.position.y = 0.26
|
||
var mat := StandardMaterial3D.new()
|
||
mat.albedo_color = _markierung_farbe("selection_outline")
|
||
mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
||
mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
||
mi.material_override = mat
|
||
return mi
|
||
|
||
|
||
## Radius/Anfass-Optik eines Spawn-Markers aktualisieren OHNE Gesamtrebuild
|
||
## (Radius-Regel gedreht, Zone ausgewählt/abgewählt).
|
||
func style_spawn_marker(marker: Node3D, radius: float, selected: bool) -> void:
|
||
if marker == null or not is_instance_valid(marker):
|
||
return
|
||
var team := int(marker.get_meta("spawn_team", -1))
|
||
var pid := str(marker.get_meta("spawn_id", ""))
|
||
marker.set_meta("spawn_radius", radius)
|
||
marker.set_meta("spawn_pick_radius", maxf(radius + 2.0, 4.0))
|
||
marker.set_meta("spawn_selected", selected)
|
||
if marker.get_child_count() > 0 and marker.get_child(0) is MeshInstance3D:
|
||
(marker.get_child(0) as MeshInstance3D).queue_free()
|
||
var disc := _spawn_disc(radius, team, selected)
|
||
marker.add_child(disc)
|
||
marker.move_child(disc, 0)
|
||
# Auswahl-Kontur: alter Ring weg, bei Auswahl neuer (restloser Wechsel).
|
||
var alter_ring: Node = null
|
||
if marker.has_meta("spawn_ring"):
|
||
alter_ring = marker.get_meta("spawn_ring")
|
||
if alter_ring != null and is_instance_valid(alter_ring):
|
||
alter_ring.queue_free()
|
||
marker.remove_meta("spawn_ring")
|
||
if selected:
|
||
var ring := _spawn_ring(radius)
|
||
marker.add_child(ring)
|
||
marker.set_meta("spawn_ring", ring)
|
||
var arrow := yaw_arrow_of(marker)
|
||
if arrow != null: # Länge folgt dem Radius
|
||
var yaw_deg := float(arrow.get_meta("yaw_deg", 0.0))
|
||
var yaw_set := bool(arrow.get_meta("yaw_set", false))
|
||
arrow.name = "YawArrowAlt"
|
||
arrow.queue_free()
|
||
marker.add_child(_yaw_arrow(radius, yaw_deg, yaw_set, marker.get_meta("yaw_color", Color.WHITE)))
|
||
|
||
|
||
## Auswahl-Highlight über ALLE Spawn-Marker (Werkzeug ruft bei Änderung).
|
||
func set_spawn_selected(team: int, pid: String) -> void:
|
||
for marker in spawn_markers:
|
||
if marker == null or not is_instance_valid(marker):
|
||
continue
|
||
var is_sel := int(marker.get_meta("spawn_team", -1)) == team \
|
||
and str(marker.get_meta("spawn_id", "")) == pid
|
||
if bool(marker.get_meta("spawn_selected", false)) == is_sel:
|
||
continue
|
||
style_spawn_marker(marker, float(marker.get_meta("spawn_radius", 6.0)), is_sel)
|
||
# Label sofort umfärben (auch ohne Disc-Neubau sichtbar) — Farbe aus
|
||
# dem Theme statt hartkodiertem Gelb (U6).
|
||
var col: Color = TEAM_COLORS[clampi(team, 0, 1)] if team >= 0 else Color.WHITE
|
||
if marker.get_child_count() > 1 and marker.get_child(1) is Label3D:
|
||
var label := marker.get_child(1) as Label3D
|
||
label.modulate = _markierung_farbe("selection_outline") if is_sel else col
|
||
|
||
|
||
# ------------------------------------------------------ U6-Markierung -------
|
||
# (Karte t_639135dd — dezente Auswahl: Kontur statt Vollorange. Eine Quelle
|
||
# für Farben/Breiten ist das Theme; der Shader ist die Außenhüllen-Technik.)
|
||
|
||
## Theme-Farben + -Breiten in die drei Kontur-Materialien überführen
|
||
## (Auswahl / Hover / Partner). Fehlende Einträge → Fallback-Farbe.
|
||
func _markierung_theme_laden() -> void:
|
||
_outline_mat_sel = _outline_material(
|
||
_markierung_farbe("selection_outline"),
|
||
_markierung_breite("selection_outline_width_mm"), 0.0)
|
||
_outline_mat_hover = _outline_material(
|
||
_markierung_farbe("selection_outline_hover"),
|
||
_markierung_breite("selection_outline_width_hover_mm"), 0.0)
|
||
_outline_mat_partner = _outline_material(
|
||
_markierung_farbe("selection_outline_partner"),
|
||
_markierung_breite("selection_outline_width_partner_mm"), 3.5)
|
||
|
||
|
||
## Eine Markierungs-Farbe aus dem Theme lesen (fehlt sie → Fallback).
|
||
func _markierung_farbe(name: String) -> Color:
|
||
var theme_res: Variant = load(THEME_PATH)
|
||
if theme_res is Theme:
|
||
var th := theme_res as Theme
|
||
if th.has_color(name, "Editor"):
|
||
return th.get_color(name, "Editor")
|
||
return OUTLINE_FALLBACK
|
||
|
||
|
||
## Eine Markierungs-Breite aus dem Theme lesen (Theme speichert Millimeter
|
||
## als Ganzzahl — Theme kennt keine Gleitkommakonstanten).
|
||
func _markierung_breite(name: String) -> float:
|
||
var theme_res: Variant = load(THEME_PATH)
|
||
if theme_res is Theme:
|
||
var th := theme_res as Theme
|
||
if th.has_constant(name, "Editor"):
|
||
return float(th.get_constant(name, "Editor")) / 1000.0
|
||
return 0.06
|
||
|
||
|
||
## Kontur-Material für einen Zustand bauen (streifen > 0 = gestrichelt).
|
||
func _outline_material(farbe: Color, breite: float, streifen: float) -> ShaderMaterial:
|
||
var mat := ShaderMaterial.new()
|
||
var shader: Variant = load(OUTLINE_SHADER)
|
||
if shader is Shader:
|
||
mat.shader = shader
|
||
mat.set_shader_parameter("linien_farbe", farbe)
|
||
mat.set_shader_parameter("breite_m", breite)
|
||
mat.set_shader_parameter("streifen_hz", streifen)
|
||
return mat
|
||
|
||
|
||
## Zustand eines Objekts (U6-Abstufung): Auswahl > Partner > Hover > nichts.
|
||
func _markierung_zustand(idx: int) -> String:
|
||
if _is_selected(idx):
|
||
return "sel"
|
||
if _is_partner(idx):
|
||
return "partner"
|
||
if idx >= 0 and idx == hover_index:
|
||
return "hover"
|
||
return ""
|
||
|
||
|
||
## Kontur-Hülle eines Objekt-Markers anlegen/umschalten/restlos entfernen.
|
||
## Die Hülle hängt als Kind UNTER dem Mesh (erbt Position + Scale — GLB-
|
||
## Skalierung bleibt korrekt) und trägt das Mesh ein zweites Mal mit dem
|
||
## Außenhüllen-Material (visuell = zweite Render-Passage um das Modell,
|
||
## das Grundmaterial mit seiner Textur bleibt unangetastet).
|
||
func _outline_anwenden(marker: Node3D, idx: int) -> void:
|
||
if marker == null or not is_instance_valid(marker):
|
||
return
|
||
var zustand := _markierung_zustand(idx)
|
||
var alt: Node = null
|
||
if marker.has_meta("outline"):
|
||
alt = marker.get_meta("outline")
|
||
if zustand.is_empty():
|
||
if alt != null and is_instance_valid(alt):
|
||
alt.queue_free()
|
||
if marker.has_meta("outline"):
|
||
marker.remove_meta("outline")
|
||
return
|
||
var mat: ShaderMaterial = _outline_mat_sel
|
||
if zustand == "hover":
|
||
mat = _outline_mat_hover
|
||
elif zustand == "partner":
|
||
mat = _outline_mat_partner
|
||
if alt != null and is_instance_valid(alt):
|
||
(alt as MeshInstance3D).material_override = mat
|
||
return
|
||
var mi := marker.get_child(0) as MeshInstance3D
|
||
if mi == null or mi.mesh == null:
|
||
return
|
||
var huelle := MeshInstance3D.new()
|
||
huelle.name = "SelectionOutline"
|
||
huelle.mesh = mi.mesh
|
||
huelle.material_override = mat
|
||
mi.add_child(huelle)
|
||
marker.set_meta("outline", huelle)
|
||
|
||
|
||
## Hover-Markierung (U6-Abstufung, schwächste Kontur). idx = obj_index oder
|
||
## -1 für „kein Hover“. Der Aufrufer ist das Objekt-Werkzeug — der Anschluss
|
||
## an die Mausbewegung liegt in interaction.gd (Grenze dieser Karte) und
|
||
## kommt als Folge-Karte; die Abstufung selbst ist hier voll wirksam.
|
||
func set_hover(idx: int) -> void:
|
||
if hover_index == idx:
|
||
return
|
||
hover_index = idx
|
||
for marker in object_markers:
|
||
if marker == null or not is_instance_valid(marker):
|
||
continue
|
||
_outline_anwenden(marker, int(marker.get_meta("obj_index", -1))) |