extends Node3D ## Bauen-Werkzeug (Karte t_97ea59c5): Modular-Builder-Pieces in der App setzen — ## Fundament/Wand/Dach/Treppe/Tür/Fenster auf einem 2-m-Modulraster. ## ## Bedienung (Chef-Grundsatz Sly: easy, deutsch): ## Bauteil rechts anklicken → Vorschau folgt dem Cursor (rastet aufs Raster) ## Linksklick → Bauteil setzen (auch AUF andere Teile — zum ## Stapeln: Wand oben drauf, Dach auf die Wände) ## Umschalt+Klick → gesetztes Bauteil AUSWÄHLEN (orange Rahmen) ## R → Vorschau drehen (90°) / Auswahl drehen ## Bild↑/Bild↓ → Auswahl anheben/absenken (0,2 m — Wände aufs ## Fundament setzen) ## Entf → Auswahl löschen ## ESC → Auswahl weg, nochmal = Werkzeug beenden ## (Rechte Maustaste = immer Flug, t_2470de89) ## ## UNDO: EIN Rückgängig-Schritt PRO Bauteil (ActionHistory, Block "gebaeude"). const MODE_HELP := "Linksklick: Bauteil setzen · Umschalt+Klick: Bauteil auswählen · R: drehen (90°) ·\nBild↑/↓: anheben/absenken · Entf: löschen · Mittlere Maustaste: Kamera · Strg+Z: zurück" var main # Hauptfenster (script-erweitert, Variant) var camera: Camera3D var view_builder # Welt-Bauer (script-erweitert, Variant) var interaction # Objekt-Interaktion (für Auswahl-Entkoppelung) var active := false var kind_id := "fundament" var ghost_rot := 0.0 # 90°-Schritte var selected_index := -1 var _ghost: MeshInstance3D var _cursor_valid := false var _cursor := Vector3.ZERO # Terrain-Cache für den Boden-Raycast (lesen) var _heights := PackedFloat32Array() var _res := 0 var _sx := 0.0 var _sz := 0.0 func setup(p_main, p_camera: Camera3D, p_builder, p_interaction) -> void: main = p_main camera = p_camera view_builder = p_builder interaction = p_interaction _ensure_ghost() func _ensure_ghost() -> void: if _ghost != null and is_instance_valid(_ghost): return _ghost = MeshInstance3D.new() _ghost.visible = false var mat := StandardMaterial3D.new() mat.albedo_color = Color(1.0, 0.75, 0.2, 0.45) mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED mat.emission_enabled = true mat.emission = Color(1.0, 0.6, 0.1) * 0.3 _ghost.material_override = mat add_child(_ghost) # ------------------------------------------------------------ Aktivierung --- func activate() -> bool: active = true selected_index = -1 ghost_rot = 0.0 _cursor_valid = false _refresh_from_doc() _rebuild_ghost_mesh() if view_builder != null: view_builder.call("set_gebaeude_selected", -1) return true func deactivate() -> void: active = false selected_index = -1 if _ghost != null and is_instance_valid(_ghost): _ghost.visible = false if view_builder != null: view_builder.call("set_gebaeude_selected", -1) ## Nach Undo/Redo/Laden/Neu: Cache frisch, Auswahl nur halten, wenn das Teil ## noch existiert. func refresh_from_doc() -> void: _refresh_from_doc() _rebuild_ghost_mesh() if selected_index >= 0 and selected_index >= BuildOps.piece_count(main.doc): selected_index = -1 if active: view_builder.call("set_gebaeude_selected", selected_index) func _refresh_from_doc() -> void: var terrain: Dictionary = main.doc.get("terrain", {}) _res = int(terrain.get("resolution", 0)) _sx = float(terrain.get("size_x_m", 0.0)) _sz = float(terrain.get("size_z_m", 0.0)) if _res >= 2 and _sx > 0.0 and _sz > 0.0: _heights = SvmapIO.decode_heights(terrain) else: _heights = PackedFloat32Array() # ------------------------------------------------------------- Werkzeug-UI -- func set_kind(next_kind: String) -> void: if not BuildOps.KINDS.has(next_kind): return kind_id = next_kind selected_index = -1 if view_builder != null: view_builder.call("set_gebaeude_selected", -1) _rebuild_ghost_mesh() if active: main.call("_set_status", "%s ausgewählt — Linksklick setzt es aufs Raster (auch auf andere Bauteile zum Stapeln). Umschalt+Klick wählt gesetzte Teile aus." % BuildOps.kind_label(kind_id)) func _rebuild_ghost_mesh() -> void: if _ghost == null or not is_instance_valid(_ghost): return var probe := BuildOps.make_piece(kind_id, Vector3.ZERO, 0.0) _ghost.mesh = BuildOps.piece_mesh(probe) # --------------------------------------------------------------- Eingaben --- func _unhandled_input(event: InputEvent) -> void: if not active: return if event is InputEventMouseMotion: _update_cursor((event as InputEventMouseMotion).position) return if event is InputEventMouseButton: var mb := event as InputEventMouseButton if mb.button_index == MOUSE_BUTTON_LEFT and mb.pressed: if mb.shift_pressed: _select_at(mb.position) else: _place_piece() get_viewport().set_input_as_handled() # Rechte Maustaste: absichtlich NICHT behandelt (Karte t_2470de89) — # sie gehört mit absolutem Vorrang dem Freelook-Flug. Deselect/Werkzeug # beenden läuft über ESC (main.gd), die Auswahl bleibt im Flug erhalten. func _update_cursor(viewport_pos: Vector2) -> void: var hit: Variant = _ray_hit(viewport_pos) if hit == null: _cursor_valid = false if _ghost != null and is_instance_valid(_ghost): _ghost.visible = false return _cursor_valid = true _cursor = hit as Vector3 if _ghost != null and is_instance_valid(_ghost): _ghost.position = BuildOps.snapped_pos(kind_id, _cursor) _ghost.rotation.y = deg_to_rad(ghost_rot) _ghost.visible = true ## Strahl → Trefferpunkt: ZUERST auf die gebackenen Bauteile (Stapeln!), ## sonst auf das Terrain. Entfernung entscheidet. func _ray_hit(viewport_pos: Vector2) -> Variant: var origin := camera.project_ray_origin(viewport_pos) var dir := camera.project_ray_normal(viewport_pos) var best: Variant = null var best_t := INF var baked: Dictionary = view_builder.call("gebaeude_baked") if not baked.get("faces", PackedVector3Array()).is_empty(): var pick := BuildOps.ray_pick(baked, origin, dir) if not pick.is_empty(): best_t = (origin - (pick["punkt"] as Vector3)).length() best = pick["punkt"] var terrain: Dictionary = main.doc.get("terrain", {}) if _res >= 2 and _heights.size() >= _res * _res: var ground: Variant = TerrainField.raycast(_heights, _res, _sx, _sz, origin, dir) if ground != null: var t := (origin - (ground as Vector3)).length() if t < best_t: best = ground return best # ------------------------------------------------------------ Platzieren ---- func _place_piece() -> void: if not _cursor_valid: main.call("_set_status", "Da war kein Boden unter dem Mauszeiger — klicke IN die Karte (die grüne Fläche).", true) return var before: Dictionary = main.history.snapshot(["gebaeude"]) var pos: Vector3 = BuildOps.snapped_pos(kind_id, _cursor) var teil := BuildOps.make_piece(kind_id, pos, ghost_rot) if teil.is_empty(): return BuildOps.ensure_block(main.doc).append(teil) var label := BuildOps.kind_label(kind_id) main.history.commit("%s gesetzt" % label, before, main.history.snapshot(["gebaeude"])) _after_build_change() main.call("_set_status", "%s gesetzt (%d Bauteile). Weiter klicken zum Bauen — R dreht, Strg+Z nimmt das letzte Teil zurück." % [ label, BuildOps.piece_count(main.doc)]) func _select_at(viewport_pos: Vector2) -> void: var origin := camera.project_ray_origin(viewport_pos) var dir := camera.project_ray_normal(viewport_pos) var baked: Dictionary = view_builder.call("gebaeude_baked") var pick := BuildOps.ray_pick(baked, origin, dir) if pick.is_empty(): selected_index = -1 view_builder.call("set_gebaeude_selected", -1) main.call("_set_status", "Kein Bauteil getroffen — Umschalt+Klick direkt AUF ein Bauteil (Wand, Dach …).") return selected_index = int(pick["piece"]) view_builder.call("set_gebaeude_selected", selected_index) var label := _piece_label(selected_index) main.call("_set_status", "%s ausgewählt — R drehen · Bild↑/↓ anheben · Entf löschen · Esc: Auswahl weg." % label) func _piece_label(idx: int) -> String: var list := BuildOps.pieces(main.doc) if idx < 0 or idx >= list.size(): return "Bauteil" return "%s (Teil %d von %d)" % [BuildOps.kind_label(str((list[idx] as Dictionary).get("id", "?"))), idx + 1, list.size()] func rotate_selected() -> void: if selected_index < 0: return var list := BuildOps.pieces(main.doc) if selected_index >= list.size(): return var teil: Dictionary = list[selected_index] var before: Dictionary = main.history.snapshot(["gebaeude"]) teil["rot"] = wrapf(float(teil.get("rot", 0.0)) + 90.0, 0.0, 360.0) main.history.commit("%s gedreht (90°)" % _piece_label(selected_index), before, main.history.snapshot(["gebaeude"]), "bauen-rot:%d" % selected_index) _after_build_change() view_builder.call("set_gebaeude_selected", selected_index) ## Auswahl anheben/absenken (0,2 m — passend zur Fundament-Dicke), merge-gekoppelt. func lift_selected(delta_m: float) -> void: if selected_index < 0: return var list := BuildOps.pieces(main.doc) if selected_index >= list.size(): return var teil: Dictionary = list[selected_index] var pos: Array = teil.get("pos", [0.0, 0.0, 0.0]) var before: Dictionary = main.history.snapshot(["gebaeude"]) teil["pos"] = [float(pos[0]), BuildOps.snap_y(float(pos[1]) + delta_m), float(pos[2])] main.history.commit("%s %s" % [_piece_label(selected_index), "angehoben" if delta_m > 0.0 else "abgesenkt"], before, main.history.snapshot(["gebaeude"]), "bauen-lift:%d" % selected_index) _after_build_change() view_builder.call("set_gebaeude_selected", selected_index) ## Pfeiltasten (Karte t_55257792): Bauteil im 2-m-Modulraster verschieben — ## EINE Zelle pro Tastendruck. Das exakte ±2-m-Delta erhält die Raster- ## Ausrichtung ("ecke" = gerade Meter, "mitte" = ungerade Meter bleibt), Y ## bleibt (Höhe ändert die Pfeiltaste nicht). EIN Undo-Schritt pro Tastendruck. func nudge_selected(delta: Vector3) -> void: if selected_index < 0: main.call("_set_status", "Nichts ausgewählt — Umschalt+Klick wählt ein Bauteil aus, dann verschieben die Pfeiltasten es im 2-m-Raster.") return var list := BuildOps.pieces(main.doc) if selected_index >= list.size(): selected_index = -1 return var teil: Dictionary = list[selected_index] var pos: Array = teil.get("pos", [0.0, 0.0, 0.0]) var before: Dictionary = main.history.snapshot(["gebaeude"]) var nx := float(pos[0]) + delta.x var nz := float(pos[2]) + delta.z teil["pos"] = [nx, float(pos[1]), nz] main.history.commit("%s verschoben (Pfeiltaste)" % _piece_label(selected_index), before, main.history.snapshot(["gebaeude"])) _after_build_change() view_builder.call("set_gebaeude_selected", selected_index) main.call("_set_status", "%s — 2 m pro Tastendruck, Raster bleibt eingehalten." % _piece_label(selected_index)) ## Strg+Bild↑ (Karte t_55257792): Bauteil direkt auf das Terrain absetzen ## (Fußpunkt = Terrain-Höhe, auf 5 cm gerastet). EIN Undo-Schritt. func snap_selected_to_floor() -> void: if selected_index < 0: main.call("_set_status", "Nichts ausgewählt — Umschalt+Klick wählt ein Bauteil aus, dann setzt Strg+Bild↑ es auf den Boden.", true) return var list := BuildOps.pieces(main.doc) if selected_index >= list.size(): selected_index = -1 return var teil: Dictionary = list[selected_index] var pos: Array = teil.get("pos", [0.0, 0.0, 0.0]) var ground := SvmapDocument.terrain_height_at(main.doc, float(pos[0]), float(pos[2])) var target := BuildOps.snap_y(ground) if absf(float(pos[1]) - target) < 0.0001: main.call("_set_status", "%s liegt schon auf dem Boden." % _piece_label(selected_index)) return var before: Dictionary = main.history.snapshot(["gebaeude"]) teil["pos"] = [float(pos[0]), target, float(pos[2])] main.history.commit("%s auf den Boden gesetzt" % _piece_label(selected_index), before, main.history.snapshot(["gebaeude"])) _after_build_change() view_builder.call("set_gebaeude_selected", selected_index) main.call("_set_status", "%s auf den Boden gesetzt — Strg+Z nimmt es zurück." % _piece_label(selected_index)) func delete_selected() -> void: if selected_index < 0: main.call("_set_status", "Nichts ausgewählt — Umschalt+Klick wählt ein Bauteil aus (Entf löscht es dann).", true) return var list: Array = BuildOps.pieces(main.doc) if selected_index >= list.size(): selected_index = -1 return var label := _piece_label(selected_index) var before: Dictionary = main.history.snapshot(["gebaeude"]) list.remove_at(selected_index) main.history.commit("%s gelöscht" % label, before, main.history.snapshot(["gebaeude"])) selected_index = -1 _after_build_change() main.call("_set_status", "%s — Strg+Z holt es zurück." % label.replace(" (Teil", " gelöscht (Teil")) func _after_build_change() -> void: main.call("_after_structure_change") if selected_index >= 0 and selected_index < BuildOps.piece_count(main.doc): view_builder.call("set_gebaeude_selected", selected_index) ## R-Taste: Auswahl drehen — sonst die Vorschau (90°-Raster). func rotate_input() -> void: if selected_index >= 0: rotate_selected() return ghost_rot = wrapf(ghost_rot + 90.0, 0.0, 360.0) if _ghost != null and is_instance_valid(_ghost): _ghost.rotation.y = deg_to_rad(ghost_rot) main.call("_set_status", "Vorschau gedreht auf %.0f° — Linksklick setzt das Bauteil so." % ghost_rot) ## Teil per Baum/Index anwählen (ohne Maus — auch für Demos/Tests). func select_index(idx: int) -> void: if idx < 0 or idx >= BuildOps.piece_count(main.doc): selected_index = -1 view_builder.call("set_gebaeude_selected", -1) return selected_index = idx view_builder.call("set_gebaeude_selected", idx) main.call("_set_status", "%s ausgewählt — R drehen · Bild↑/↓ anheben · Entf löschen · Rechtsklick: Auswahl weg." % _piece_label(idx)) # ---------------------------------------------------------- Demo/Headless --- ## Demo-Hook: Bauteil programmatisch setzen (inkl. History) — exakt wie Klick. func demo_place(p_kind: String, pos: Vector3, rot_deg: float = 0.0) -> Dictionary: var before: Dictionary = main.history.snapshot(["gebaeude"]) var teil := BuildOps.make_piece(p_kind, pos, rot_deg) BuildOps.ensure_block(main.doc).append(teil) main.history.commit("%s gesetzt" % BuildOps.kind_label(p_kind), before, main.history.snapshot(["gebaeude"])) return teil ## Demo-Hook: Ghost an Position parken (für Screenshots ohne Maus). func demo_park_ghost(p_kind: String, pos: Vector3, rot_deg: float = 0.0) -> void: set_kind(p_kind) _cursor = pos _cursor_valid = true if _ghost != null and is_instance_valid(_ghost): _ghost.position = BuildOps.snapped_pos(p_kind, pos) _ghost.rotation.y = deg_to_rad(rot_deg) _ghost.visible = true