// wgpu-Pipeline des nativen 3D-Renderers (nur mit Feature "render").
//
// Eine Pipeline: extrudierte Wand-Meshes mit Tiefenpuffer, View-Projektions-
// Matrix-Uniform und einem Directional-Light im Fragment-Shader. Backface-Culling
// ist aktiv (die Extrusion liefert konsistent nach aussen zeigende Flaechen, siehe
// mesh.rs) — das haelt das Innere der Waende korrekt verdeckt.
//
// Der Renderer ist fenster-/surface-agnostisch: er bekommt `Device`, `Queue` und
// ein `TextureView` (Surface- oder Offscreen-Textur) plus die aktuelle Kamera und
// zeichnet dort hinein. Die Fenster-Anbindung (winit) liegt separat im Spike-Bin.

use bytemuck::{Pod, Zeroable};
use wgpu::util::DeviceExt;

use crate::edges::{build_mesh_edges, EDGE_FLOATS_PER_VERTEX};
use crate::grid::{build_ground_grid, GRID_FLOATS_PER_VERTEX};
use crate::math::{view_projection, Mat4};
use crate::mesh::{
    build_scene_mesh, build_scene_mesh_textured, build_walls_mesh, build_walls_mesh_textured,
};
use crate::section::SectionPlane;
use crate::section_fill::{
    build_cut_cap_lines, build_cut_caps, CAP_FLOATS_PER_VERTEX, CUT_LINE_FLOATS_PER_VERTEX,
};
use crate::shaders::{CAP_WGSL, GRID_WGSL, MESH_TEXTURED_WGSL, MESH_WGSL};
use crate::types::{
    Camera, Mesh, MeshInput, SlabInput, TexturedMesh, WallInput, FLOATS_PER_VERTEX,
    TEXTURED_FLOATS_PER_VERTEX,
};

/// Darstellungsart des 3D-Renderers (Oberleiste: shaded/white/textured/
/// wireframe/hidden). Bestimmt, welche Pipelines je Frame gezeichnet werden und
/// welchen `mode.x`-Wert der Mesh-Shader bekommt.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum RenderStyle {
    /// Beleuchtete Bauteilfarben (Default).
    Shaded,
    /// Clay-Look: einheitlich helles Material, beleuchtet.
    White,
    /// Echte Bild-Textur (prozedurales Schachbrett) auf den Wandflaechen,
    /// beleuchtet wie `Shaded` (eigene Pipeline + Textur-Bind-Group group 1,
    /// siehe `textured_pipeline`).
    Textured,
    /// Nur Modell-Kanten (Feature-Edges), keine gefuellten Flaechen.
    Wireframe,
    /// Hidden-Line: flach-weisse Flaechen (fuellen den Depth-Buffer) + dunkle
    /// Kanten obenauf, tiefengetestet -> nur sichtbare Kanten erscheinen.
    Hidden,
}

impl RenderStyle {
    /// Parst den API-String; unbekannte Werte fallen auf `Shaded` zurueck.
    pub fn from_str_or_shaded(s: &str) -> Self {
        match s {
            "white" => RenderStyle::White,
            "textured" => RenderStyle::Textured,
            "wireframe" => RenderStyle::Wireframe,
            "hidden" => RenderStyle::Hidden,
            _ => RenderStyle::Shaded,
        }
    }

    /// `mode.x`-Wert fuer den Mesh-Shader (siehe shaders.rs): 0 = Material/shaded
    /// (auch textured), 1 = weiss/Clay, 2 = flach-weisse Hidden-Line-Flaechen.
    fn mesh_mode(self) -> f32 {
        match self {
            RenderStyle::White => 1.0,
            RenderStyle::Hidden => 2.0,
            // Shaded, Textured, Wireframe (Flaechen werden bei Wireframe eh nicht
            // gezeichnet) -> Material.
            _ => 0.0,
        }
    }

    /// Werden in diesem Stil gefuellte Flaechen gezeichnet?
    fn draws_faces(self) -> bool {
        !matches!(self, RenderStyle::Wireframe)
    }

    /// Werden in diesem Stil Modell-Kanten gezeichnet?
    fn draws_edges(self) -> bool {
        matches!(self, RenderStyle::Wireframe | RenderStyle::Hidden)
    }
}

/// Tiefenformat des Z-Puffers (32 Bit Float, ueberall verfuegbar).
pub const DEPTH_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Depth32Float;

/// MSAA-Samples fuer Farb- und Tiefenziel. 4x ist auf allen gaengigen Backends
/// (Vulkan/Metal/DX12/GL) fuer die ueblichen Oberflaechenformate verfuegbar und
/// gleicht die Treppenstufen an Wandkanten/Silhouetten spuerbar aus.
const SAMPLE_COUNT: u32 = 4;

/// Uniform-Block, 1:1 zum WGSL-`Globals`-Struct. std140-kompatibel (mat4/vec4 auf
/// 16 Byte ausgerichtet).
#[repr(C)]
#[derive(Clone, Copy, Pod, Zeroable)]
struct Globals {
    view_proj: [f32; 16],
    light_dir: [f32; 4],
    sky_color: [f32; 4],
    ground_color: [f32; 4],
    sun_color: [f32; 4],
    /// x = Darstellungsart (0 = shaded/Material, 1 = weiss/Clay, 2 = hidden);
    /// y = Live-Schnitt aktiv (0/1). Siehe shaders.rs.
    mode: [f32; 4],
    /// Live-Schnittebene: xyz = Ebenennormale (= Blickrichtung, normiert),
    /// w = -dot(normal, point). Ein Fragment mit `dot(world, xyz) + w > 0` liegt
    /// hinter der Ebene und wird (bei aktivem Schnitt, mode.y > 0.5) verworfen.
    section_plane: [f32; 4],
}

impl Default for Globals {
    fn default() -> Self {
        Self {
            view_proj: crate::math::identity(),
            // Richtung ZUM Licht (world), normiert. Sonne oben-vorne (6,12,4).
            light_dir: normalize4([6.0, 12.0, 4.0]),
            // Himmels-Ambient (von oben): helles, leicht kuehles Licht.
            sky_color: [0.66, 0.68, 0.72, 1.0],
            // Boden-Ambient (von unten): dunkler, warmer Ton (Bounce/Schatten).
            ground_color: [0.30, 0.29, 0.27, 1.0],
            // Directional-Sonne: warmweiss, moderat.
            sun_color: [0.55, 0.53, 0.49, 1.0],
            mode: [0.0, 0.0, 0.0, 0.0],
            // Default: kein Schnitt (mode.y = 0). Neutrale Ebene.
            section_plane: [0.0, 0.0, 1.0, 0.0],
        }
    }
}

fn normalize4(v: [f32; 3]) -> [f32; 4] {
    let l = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt().max(1e-9);
    [v[0] / l, v[1] / l, v[2] / l, 0.0]
}

/// GPU-seitige Puffer eines hochgeladenen Meshes.
struct MeshBuffers {
    vbo: wgpu::Buffer,
    ibo: wgpu::Buffer,
    index_count: u32,
}

/// GPU-seitige Puffer des TEXTURIERTEN Wand-Meshes (`RenderStyle::Textured`).
/// Eigenes Vertex-Layout `[pos vec3, normal vec3, uv vec2]`
/// (`TEXTURED_FLOATS_PER_VERTEX`), sonst wie `MeshBuffers`. Zweiter, additiver
/// Puffer neben `MeshBuffers` — der Alt-Pfad bleibt bitgleich unangetastet.
struct TexturedMeshBuffers {
    vbo: wgpu::Buffer,
    /// Zweiter Vertex-Buffer: EIN f32 (Material-Textur-Ebene) je Vertex,
    /// @location(3) im `MESH_TEXTURED_WGSL`. Immer gesetzt (Fallback: `-1` je
    /// Vertex), damit das Pipeline-Layout stets zwei Vertex-Buffer erwartet.
    layer_vbo: wgpu::Buffer,
    ibo: wgpu::Buffer,
    index_count: u32,
}

/// Kantenlaenge der prozeduralen Test-Textur (Schachbrett), in Texeln.
const TEXTURE_SIZE: u32 = 256;

/// Erzeugt ein 256x256-RGBA-Schachbrett IM CODE (kein Asset, keine `image`-Crate):
/// 8x8 Felder abwechselnd hell/dunkel, mit einem duennen Raster als Kachel-Grenze.
/// Rueckgabe = flaches RGBA8-Byte-Array (`4 * TEXTURE_SIZE * TEXTURE_SIZE`), direkt
/// fuer `queue.write_texture` (Zeilen sind `4 * TEXTURE_SIZE` Byte breit, ohne
/// Padding, weil `256 * 4 = 1024` bereits ein Vielfaches von 256 ist). Bewusst
/// selbststaendig, damit der Spike ohne Dateipfade/Asset-Pipeline laeuft.
fn build_checker_texture() -> Vec<u8> {
    let size = TEXTURE_SIZE as usize;
    // 8 Felder je Kante -> jedes Feld 32 Texel breit. Das macht 1 Textur-Kachel
    // (= 1 m Welt, siehe UV-Massstab) in ein 8x8-Schachbrett sichtbar.
    let cell = size / 8;
    let mut data = vec![0u8; size * size * 4];
    // Zwei Grautoene als Schachbrett + eine dunkle Rasterlinie an den Feldgrenzen.
    let light = [220u8, 220, 224, 255];
    let dark = [150u8, 152, 160, 255];
    let line = [70u8, 72, 80, 255];
    for y in 0..size {
        for x in 0..size {
            let checker = ((x / cell) + (y / cell)) % 2 == 0;
            // Duenne Rasterlinie (2 Texel) exakt auf den Feldkanten — betont die
            // weltmassstaebliche Kachelung an Gehrungen/Ecken sichtbar.
            let on_line = (x % cell) < 2 || (y % cell) < 2;
            let px = if on_line {
                line
            } else if checker {
                light
            } else {
                dark
            };
            let o = (y * size + x) * 4;
            data[o..o + 4].copy_from_slice(&px);
        }
    }
    data
}

/// GPU-seitiger Vertex-Buffer einer LineList (kein Indexpuffer). Format
/// [pos vec3, color vec3] — genutzt fuer das Bodengitter UND die Modell-Kanten
/// (identisches Layout/Shader, siehe grid.rs / edges.rs).
struct LineBuffers {
    vbo: wgpu::Buffer,
    vertex_count: u32,
}

/// GPU-seitiger Vertex-Buffer der Schnittflaechen-Kappen (Cut-Caps, TriangleList,
/// kein Indexpuffer). Format `[pos vec3, uv vec2]` (`CAP_FLOATS_PER_VERTEX`) —
/// die (u,v)-Ebenenkoordinaten treiben die prozedurale Schraffur (CAP_WGSL).
struct CapBuffers {
    vbo: wgpu::Buffer,
    vertex_count: u32,
}

/// Der native 3D-Renderer: haelt die Pipeline, das Uniform (View-Projektion +
/// Licht) und das aktuell hochgeladene Mesh. Ein Tiefenpuffer wird passend zur
/// Ziel-Groesse (neu) angelegt.
pub struct Renderer {
    pipeline: wgpu::RenderPipeline,
    /// Wie `pipeline`, aber mit positivem Depth-Bias (Flaechen leicht nach hinten
    /// geschoben). Nur fuer den Hidden-Line-Stil: so gewinnen die danach an ihrer
    /// wahren Tiefe gezeichneten Kanten den Less-Test und liegen sauber obenauf,
    /// waehrend verdeckte Kanten weiterhin weggeschnitten werden (kein Z-Fighting).
    hidden_face_pipeline: wgpu::RenderPipeline,
    /// Eigene, schlanke Linien-Pipeline (LineList, unlit) fuer das Bodengitter
    /// UND die Modell-Kanten (wireframe/hidden — identisches Layout/Shader).
    /// Teilt sich das `Globals`-Uniform/Bind-Group (nur View-Projektion) mit der
    /// Mesh-Pipeline und rendert in denselben Pass/Tiefenpuffer.
    grid_pipeline: wgpu::RenderPipeline,
    /// Wie `grid_pipeline` (LineList, GRID_WGSL), aber OHNE Tiefentest
    /// (`depth_compare: Always`, kein Depth-Write): die Auswahl-Highlight-Linien
    /// liegen immer sichtbar obenauf und scheinen auch hinter Waenden durch.
    highlight_pipeline: wgpu::RenderPipeline,
    /// Schnittflaechen-Kappen (TriangleList, CAP_WGSL, eigenes Vertex-Layout
    /// [pos vec3, uv vec2]). Teilt sich die `Globals`-Bind-Group; leichter
    /// Depth-Bias zur Kamera hin gegen Z-Fighting mit den an der Ebene
    /// per-`discard` gekappten Wandflaechen. Backface-Culling AUS (CullMode::None).
    cap_pipeline: wgpu::RenderPipeline,
    /// Schnitt-Umrisslinien-Pipeline (Cut-Cap-Kanten, P2): TriangleList von
    /// Ribbon-Quads (`section_fill::build_cut_cap_lines`), Vertex-Layout [pos vec3,
    /// color vec3] wie die Grid/Highlight-LineList — teilt darum das `grid_module`
    /// (GRID_WGSL). KEIN Tiefentest (`depth_compare: Always`, kein Write) wie das
    /// Highlight: die Fugenlinien liegen (leicht zur Kamera versetzt) sichtbar auf
    /// den Cut-Caps. Sie werden NICHT an der Schnittebene weggeclippt (sie SIND die
    /// Schnittflaeche) — der Kamera-Lift in `build_cut_cap_lines` haelt sie vor der
    /// `depth > 0`-Kappung von GRID_WGSL.
    cut_line_pipeline: wgpu::RenderPipeline,
    /// Texturierte Wand-Pipeline (`RenderStyle::Textured`, TriangleList,
    /// MESH_TEXTURED_WGSL, Vertex-Layout [pos vec3, normal vec3, uv vec2]). Bindet
    /// group 0 (Globals) UND group 1 (Bild-Textur). Depth-/MSAA-/Farbziel
    /// BITIDENTISCH zur Haupt-`pipeline` (sonst inkompatibler Render-Pass), nur
    /// Shader/Layout unterscheiden sich.
    textured_pipeline: wgpu::RenderPipeline,
    /// Bind-Group des Material-Textur-Arrays (group 1): Array-View + Sampler.
    /// Wird bei `set_material_textures` neu gebaut (mehr Ebenen); bis dahin haelt
    /// sie das 1-Ebenen-Array mit dem Fallback-Schachbrett.
    texture_bind_group: wgpu::BindGroup,
    /// Das Textur-Array selbst. Start: 1 Ebene (Schachbrett). `set_material_textures`
    /// ersetzt es durch ein Array `[Schachbrett, Material0, Material1, …]`.
    texture: wgpu::Texture,
    /// Bind-Group-Layout (group 1), festgehalten fuer den Neubau der Bind-Group
    /// beim Hochladen von Material-Texturen.
    texture_bind_group_layout: wgpu::BindGroupLayout,
    /// Sampler (Repeat/Linear), festgehalten fuer den Bind-Group-Neubau.
    texture_sampler: wgpu::Sampler,
    /// Anzahl gueltiger Ebenen im Textur-Array (>= 1). 1 = nur Schachbrett; nach
    /// `set_material_textures` = 1 + Anzahl Material-Karten. Wandert je Frame nach
    /// `mode.z` (Klemmung der Ebenen-Auswahl im Shader).
    material_layer_count: u32,
    /// Ob die Schachbrett-Texel (Ebene 0) schon hochgeladen wurden. Beim ersten
    /// `render` einmalig gesetzt (lazy Upload, weil `new` keine Queue bekommt).
    texture_uploaded: bool,
    bind_group: wgpu::BindGroup,
    uniform: wgpu::Buffer,
    mesh: Option<MeshBuffers>,
    /// Texturiertes Wand-Mesh (`[pos, normal, uv]`) — zweiter, additiver Puffer
    /// neben `mesh`. Nur im Stil `Textured` gezeichnet; bei `upload_*` zusammen mit
    /// `mesh` erzeugt. None = kein Mesh geladen.
    textured_mesh: Option<TexturedMeshBuffers>,
    /// Modell-Kanten (Feature-Edges, edges.rs) als LineList. Einmalig bei
    /// `set_model`/`upload_*` erzeugt und gecacht (nicht je Frame). Gezeichnet in
    /// den Stilen wireframe/hidden.
    edges: Option<LineBuffers>,
    /// Bodengitter-Vertices (None = nicht erzeugt). Wird nur gezeichnet, wenn
    /// zusaetzlich `grid_visible` gesetzt ist.
    grid: Option<LineBuffers>,
    /// Auswahl-Highlight-Linien (Kanten des selektierten Bauteils, LineList) als
    /// LineBuffers. None = keine Auswahl -> nichts wird gezeichnet. Ueber
    /// `set_highlight_lines` gesetzt/geloescht; im Render-Pass als ALLERLETZTES
    /// (ohne Tiefentest) gezeichnet.
    highlight: Option<LineBuffers>,
    /// Schnittflaechen-Kappen (Cut-Caps) des Live-Schnitts. None = kein Schnitt
    /// aktiv / keine Schnittgeometrie. Ueber `set_cut_caps` gesetzt/geloescht,
    /// gezeichnet NACH den Flaechen, VOR Kanten/Grid/Highlight.
    caps: Option<CapBuffers>,
    /// Schnitt-Umrisslinien (Cut-Cap-Kanten, P2) als Ribbon-TriangleList
    /// (`[pos, color]`). None = kein Schnitt aktiv / keine Kanten. Ueber
    /// `set_cut_lines` gesetzt/geloescht, gezeichnet NACH den Caps.
    cut_lines: Option<LineBuffers>,
    /// Ob das Bodengitter im naechsten `render` gezeichnet wird. Default false —
    /// bestehende Aufrufer ohne `set_ground_grid` bekommen KEIN Gitter.
    grid_visible: bool,
    /// Aktuelle Darstellungsart (Default `Shaded`). Einzige Quelle der Wahrheit;
    /// `set_white_mode` bildet auf `Shaded`/`White` ab.
    style: RenderStyle,
    depth: Option<DepthTarget>,
    msaa: Option<MsaaTarget>,
    /// Farbformat des Render-Ziels (Surface), fuer die MSAA-Zwischentextur.
    color_format: wgpu::TextureFormat,
    globals: Globals,
    /// Loeschfarbe (Hintergrund). Default heller Grauton wie die three.js-Sicht.
    pub clear_color: wgpu::Color,
}

/// Tiefen-Textur samt View, an eine bestimmte Groesse gebunden.
struct DepthTarget {
    view: wgpu::TextureView,
    width: u32,
    height: u32,
}

/// Multisampled Farb-Zwischentextur samt View, an eine bestimmte Groesse
/// gebunden. Wird jeden Frame in das eigentliche Ziel (Surface-View) aufgeloest.
struct MsaaTarget {
    view: wgpu::TextureView,
    width: u32,
    height: u32,
}

impl Renderer {
    /// Erzeugt Pipeline + Layout fuer ein gegebenes Farbformat (Surface).
    pub fn new(device: &wgpu::Device, color_format: wgpu::TextureFormat) -> Self {
        let module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
            label: Some("mesh.wgsl"),
            source: wgpu::ShaderSource::Wgsl(MESH_WGSL.into()),
        });

        let bind_group_layout =
            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
                label: Some("globals.layout"),
                entries: &[wgpu::BindGroupLayoutEntry {
                    binding: 0,
                    visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
                    ty: wgpu::BindingType::Buffer {
                        ty: wgpu::BufferBindingType::Uniform,
                        has_dynamic_offset: false,
                        min_binding_size: wgpu::BufferSize::new(
                            std::mem::size_of::<Globals>() as u64
                        ),
                    },
                    count: None,
                }],
            });

        let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
            label: Some("3d.layout"),
            bind_group_layouts: &[Some(&bind_group_layout)],
            immediate_size: 0,
        });

        // Vertex-Layout: [pos vec3, normal vec3, color vec3], stride 9*4.
        let vertex_layout = wgpu::VertexBufferLayout {
            array_stride: (FLOATS_PER_VERTEX * 4) as u64,
            step_mode: wgpu::VertexStepMode::Vertex,
            attributes: &wgpu::vertex_attr_array![0 => Float32x3, 1 => Float32x3, 2 => Float32x3],
        };

        // Mesh-Pipeline-Fabrik: identische Beschreibung fuer die Standard-Flaechen
        // (kein Bias) und die Hidden-Line-Flaechen (positiver Depth-Bias, damit die
        // spaeter gezeichneten Kanten obenauf liegen). Nur `bias` unterscheidet sich.
        let make_mesh_pipeline = |label: &str, bias: wgpu::DepthBiasState| {
            device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
                label: Some(label),
                layout: Some(&pipeline_layout),
                vertex: wgpu::VertexState {
                    module: &module,
                    entry_point: Some("vs_main"),
                    buffers: &[vertex_layout.clone()],
                    compilation_options: Default::default(),
                },
                fragment: Some(wgpu::FragmentState {
                    module: &module,
                    entry_point: Some("fs_main"),
                    targets: &[Some(wgpu::ColorTargetState {
                        format: color_format,
                        blend: Some(wgpu::BlendState::REPLACE),
                        write_mask: wgpu::ColorWrites::ALL,
                    })],
                    compilation_options: Default::default(),
                }),
                primitive: wgpu::PrimitiveState {
                    topology: wgpu::PrimitiveTopology::TriangleList,
                    // Aussen zeigende Flaechen sind CCW (mesh.rs) -> Rueckseiten cullen.
                    front_face: wgpu::FrontFace::Ccw,
                    cull_mode: Some(wgpu::Face::Back),
                    ..Default::default()
                },
                depth_stencil: Some(wgpu::DepthStencilState {
                    format: DEPTH_FORMAT,
                    depth_write_enabled: Some(true),
                    depth_compare: Some(wgpu::CompareFunction::Less),
                    stencil: wgpu::StencilState::default(),
                    bias,
                }),
                multisample: wgpu::MultisampleState {
                    count: SAMPLE_COUNT,
                    mask: !0,
                    alpha_to_coverage_enabled: false,
                },
                multiview_mask: None,
                cache: None,
            })
        };
        let pipeline = make_mesh_pipeline("mesh.pipeline", wgpu::DepthBiasState::default());
        // Hidden-Line-Flaechen leicht nach hinten schieben (Depth-Bias auf
        // Dreiecke wirkt zuverlaessig — anders als bei Linien), damit die Kanten
        // an ihrer wahren Tiefe den Less-Test gewinnen.
        let hidden_face_pipeline = make_mesh_pipeline(
            "mesh.hidden.pipeline",
            wgpu::DepthBiasState {
                constant: 4,
                slope_scale: 1.0,
                clamp: 0.0,
            },
        );

        // Bodengitter-Pipeline: dieselbe Bind-Group (Globals/View-Projektion) und
        // dasselbe Depth-/MSAA-/Farbziel wie die Mesh-Pipeline, aber LineList-
        // Topologie, ein eigener unlit-Shader (konstante Vertexfarbe) und ein
        // schlankeres Vertex-Layout [pos vec3, color vec3].
        let grid_module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
            label: Some("grid.wgsl"),
            source: wgpu::ShaderSource::Wgsl(GRID_WGSL.into()),
        });
        let grid_vertex_layout = wgpu::VertexBufferLayout {
            array_stride: (GRID_FLOATS_PER_VERTEX * 4) as u64,
            step_mode: wgpu::VertexStepMode::Vertex,
            attributes: &wgpu::vertex_attr_array![0 => Float32x3, 1 => Float32x3],
        };
        // Linien-Pipeline-Fabrik: identische Beschreibung (LineList, GRID_WGSL,
        // gemeinsame Bind-Group/Farbziel/MSAA) fuer das Bodengitter/die Kanten
        // (tiefengetestet) UND die Auswahl-Highlight-Linien (ohne Tiefentest, siehe
        // unten). Nur der `depth_stencil`-Zustand unterscheidet sich.
        let make_line_pipeline = |label: &str, depth: wgpu::DepthStencilState| {
            device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
                label: Some(label),
                layout: Some(&pipeline_layout),
                vertex: wgpu::VertexState {
                    module: &grid_module,
                    entry_point: Some("vs_main"),
                    buffers: &[grid_vertex_layout.clone()],
                    compilation_options: Default::default(),
                },
                fragment: Some(wgpu::FragmentState {
                    module: &grid_module,
                    entry_point: Some("fs_main"),
                    targets: &[Some(wgpu::ColorTargetState {
                        format: color_format,
                        blend: Some(wgpu::BlendState::REPLACE),
                        write_mask: wgpu::ColorWrites::ALL,
                    })],
                    compilation_options: Default::default(),
                }),
                primitive: wgpu::PrimitiveState {
                    topology: wgpu::PrimitiveTopology::LineList,
                    // Linien haben keine Vorder-/Rueckseite -> kein Culling.
                    cull_mode: None,
                    ..Default::default()
                },
                depth_stencil: Some(depth),
                multisample: wgpu::MultisampleState {
                    count: SAMPLE_COUNT,
                    mask: !0,
                    alpha_to_coverage_enabled: false,
                },
                multiview_mask: None,
                cache: None,
            })
        };
        // Grid/Kanten: normaler Tiefentest (Less), schreibt Tiefe -> das Modell
        // verdeckt Gitter/verdeckte Kanten korrekt.
        let grid_pipeline = make_line_pipeline(
            "grid.pipeline",
            wgpu::DepthStencilState {
                format: DEPTH_FORMAT,
                depth_write_enabled: Some(true),
                depth_compare: Some(wgpu::CompareFunction::Less),
                stencil: wgpu::StencilState::default(),
                bias: wgpu::DepthBiasState::default(),
            },
        );
        // Auswahl-Highlight: KEIN Tiefentest (`Always`) und KEIN Tiefen-Write —
        // die Auswahl-Umrisslinien werden unabhaengig von der Szenentiefe immer
        // gezeichnet und scheinen so auch hinter Waenden durch (liegen optisch oben,
        // weil sie als letztes gezeichnet werden).
        let highlight_pipeline = make_line_pipeline(
            "highlight.pipeline",
            wgpu::DepthStencilState {
                format: DEPTH_FORMAT,
                depth_write_enabled: Some(false),
                depth_compare: Some(wgpu::CompareFunction::Always),
                stencil: wgpu::StencilState::default(),
                bias: wgpu::DepthBiasState::default(),
            },
        );

        // Schnitt-Umrisslinien-Pipeline (Cut-Cap-Kanten, P2): dasselbe schlanke
        // LineList-Vertex-Layout [pos vec3, color vec3] und `grid_module` (GRID_WGSL)
        // wie Grid/Highlight, aber als TriangleList (die Kanten sind Ribbon-Quads,
        // damit die Strichstaerke als Welt-Breite durchkommt). Depth wie Highlight
        // (Always, kein Write) -> immer sichtbar auf den Caps.
        let cut_line_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
            label: Some("cut_line.pipeline"),
            layout: Some(&pipeline_layout),
            vertex: wgpu::VertexState {
                module: &grid_module,
                entry_point: Some("vs_main"),
                buffers: &[grid_vertex_layout.clone()],
                compilation_options: Default::default(),
            },
            fragment: Some(wgpu::FragmentState {
                module: &grid_module,
                entry_point: Some("fs_main"),
                targets: &[Some(wgpu::ColorTargetState {
                    format: color_format,
                    blend: Some(wgpu::BlendState::REPLACE),
                    write_mask: wgpu::ColorWrites::ALL,
                })],
                compilation_options: Default::default(),
            }),
            primitive: wgpu::PrimitiveState {
                topology: wgpu::PrimitiveTopology::TriangleList,
                // Ribbons sind flach in der Schnittebene, aus beiden Seiten sichtbar.
                cull_mode: None,
                ..Default::default()
            },
            depth_stencil: Some(wgpu::DepthStencilState {
                format: DEPTH_FORMAT,
                depth_write_enabled: Some(false),
                depth_compare: Some(wgpu::CompareFunction::Always),
                stencil: wgpu::StencilState::default(),
                bias: wgpu::DepthBiasState::default(),
            }),
            multisample: wgpu::MultisampleState {
                count: SAMPLE_COUNT,
                mask: !0,
                alpha_to_coverage_enabled: false,
            },
            multiview_mask: None,
            cache: None,
        });

        // Schnittflaechen-Kappen-Pipeline: eigener Shader (CAP_WGSL), eigenes
        // schlankes Vertex-Layout [pos vec3, uv vec2], TriangleList. Dieselbe
        // Bind-Group (Globals) und dasselbe Depth-/MSAA-/Farbziel wie oben.
        let cap_module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
            label: Some("cap.wgsl"),
            source: wgpu::ShaderSource::Wgsl(CAP_WGSL.into()),
        });
        // Vertex-Layout: [pos vec3, uv vec2, hatch vec4(pattern, angle_rad, scale,
        // line_weight_mm)], stride 9*4 (`CAP_FLOATS_PER_VERTEX`). Das dritte Attribut
        // treibt die musterspezifische Schraffur im Cap-Fragment-Shader (CAP_WGSL);
        // `line_weight_mm` setzt die Musterlinien-Breite je Schraffur.
        let cap_vertex_layout = wgpu::VertexBufferLayout {
            array_stride: (CAP_FLOATS_PER_VERTEX * 4) as u64,
            step_mode: wgpu::VertexStepMode::Vertex,
            attributes: &wgpu::vertex_attr_array![0 => Float32x3, 1 => Float32x2, 2 => Float32x4],
        };
        let cap_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
            label: Some("cap.pipeline"),
            layout: Some(&pipeline_layout),
            vertex: wgpu::VertexState {
                module: &cap_module,
                entry_point: Some("vs_main"),
                buffers: &[cap_vertex_layout],
                compilation_options: Default::default(),
            },
            fragment: Some(wgpu::FragmentState {
                module: &cap_module,
                entry_point: Some("fs_main"),
                targets: &[Some(wgpu::ColorTargetState {
                    format: color_format,
                    blend: Some(wgpu::BlendState::REPLACE),
                    write_mask: wgpu::ColorWrites::ALL,
                })],
                compilation_options: Default::default(),
            }),
            primitive: wgpu::PrimitiveState {
                topology: wgpu::PrimitiveTopology::TriangleList,
                // Die Cap ist eine flache Flaeche und soll aus BEIDEN Richtungen
                // sichtbar sein (das Winding der Rueckprojektion ist nicht
                // kamerakonsistent garantiert) -> kein Backface-Culling.
                cull_mode: None,
                ..Default::default()
            },
            depth_stencil: Some(wgpu::DepthStencilState {
                format: DEPTH_FORMAT,
                depth_write_enabled: Some(true),
                depth_compare: Some(wgpu::CompareFunction::Less),
                stencil: wgpu::StencilState::default(),
                // Leicht ZUR Kamera ziehen (negativer Bias, Gegenteil des
                // hidden_face_pipeline-Bias): die Caps liegen exakt auf der
                // Schnittebene, an der die Wandflaechen weggeschnitten werden —
                // der negative Bias vermeidet Z-Fighting mit an der Grenze
                // gerade noch stehen gebliebenen Wandfragmenten.
                bias: wgpu::DepthBiasState {
                    constant: -4,
                    slope_scale: -1.0,
                    clamp: 0.0,
                },
            }),
            multisample: wgpu::MultisampleState {
                count: SAMPLE_COUNT,
                mask: !0,
                alpha_to_coverage_enabled: false,
            },
            multiview_mask: None,
            cache: None,
        });

        // ── Texturierter Wand-Pfad (RenderStyle::Textured) ──────────────────────
        // Bild-Textur-Bind-Group-Layout (group 1): sampelbare 2D-Float-Textur +
        // Filter-Sampler. `Globals` bleibt group 0 (oben) UND unveraendert.
        let texture_bind_group_layout =
            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
                label: Some("texture.layout"),
                entries: &[
                    wgpu::BindGroupLayoutEntry {
                        binding: 0,
                        visibility: wgpu::ShaderStages::FRAGMENT,
                        ty: wgpu::BindingType::Texture {
                            sample_type: wgpu::TextureSampleType::Float { filterable: true },
                            view_dimension: wgpu::TextureViewDimension::D2Array,
                            multisampled: false,
                        },
                        count: None,
                    },
                    wgpu::BindGroupLayoutEntry {
                        binding: 1,
                        visibility: wgpu::ShaderStages::FRAGMENT,
                        ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
                        count: None,
                    },
                ],
            });

        // Pipeline-Layout des texturierten Pfades: group 0 = Globals (wie oben),
        // group 1 = Textur. Reihenfolge = Bindungs-Index, deshalb beide hier.
        let textured_pipeline_layout =
            device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
                label: Some("3d.textured.layout"),
                bind_group_layouts: &[Some(&bind_group_layout), Some(&texture_bind_group_layout)],
                immediate_size: 0,
            });
        let textured_module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
            label: Some("mesh_textured.wgsl"),
            source: wgpu::ShaderSource::Wgsl(MESH_TEXTURED_WGSL.into()),
        });
        // Vertex-Layout: [pos vec3, normal vec3, uv vec2], stride 8*4.
        let textured_vertex_layout = wgpu::VertexBufferLayout {
            array_stride: (TEXTURED_FLOATS_PER_VERTEX * 4) as u64,
            step_mode: wgpu::VertexStepMode::Vertex,
            attributes: &wgpu::vertex_attr_array![0 => Float32x3, 1 => Float32x3, 2 => Float32x2],
        };
        // ZWEITER Vertex-Buffer: die Material-Textur-Ebene je Vertex (@location(3),
        // 1 f32). Separat gehalten, damit das Haupt-`[pos,normal,uv]`-Layout (und
        // die Geometrie-Paritaet zum Shaded-Pfad) unveraendert bleibt.
        let textured_layer_layout = wgpu::VertexBufferLayout {
            array_stride: 4,
            step_mode: wgpu::VertexStepMode::Vertex,
            attributes: &wgpu::vertex_attr_array![3 => Float32],
        };
        // WICHTIG: Depth-Format, MSAA (`SAMPLE_COUNT`), Color-Target-Format,
        // Topologie und Culling BITIDENTISCH zur Haupt-`pipeline` (make_mesh_pipeline)
        // — die texturierten Waende laufen im GLEICHEN Render-Pass, deshalb muss der
        // Pipeline-State kompatibel sein (sonst wgpu-Validierungspanik). Einziger
        // Unterschied: Shader/Vertex-Layout und das zusaetzliche group-1-Layout.
        let textured_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
            label: Some("mesh.textured.pipeline"),
            layout: Some(&textured_pipeline_layout),
            vertex: wgpu::VertexState {
                module: &textured_module,
                entry_point: Some("vs_main"),
                buffers: &[textured_vertex_layout, textured_layer_layout],
                compilation_options: Default::default(),
            },
            fragment: Some(wgpu::FragmentState {
                module: &textured_module,
                entry_point: Some("fs_main"),
                targets: &[Some(wgpu::ColorTargetState {
                    format: color_format,
                    blend: Some(wgpu::BlendState::REPLACE),
                    write_mask: wgpu::ColorWrites::ALL,
                })],
                compilation_options: Default::default(),
            }),
            primitive: wgpu::PrimitiveState {
                topology: wgpu::PrimitiveTopology::TriangleList,
                front_face: wgpu::FrontFace::Ccw,
                cull_mode: Some(wgpu::Face::Back),
                ..Default::default()
            },
            depth_stencil: Some(wgpu::DepthStencilState {
                format: DEPTH_FORMAT,
                depth_write_enabled: Some(true),
                depth_compare: Some(wgpu::CompareFunction::Less),
                stencil: wgpu::StencilState::default(),
                bias: wgpu::DepthBiasState::default(),
            }),
            multisample: wgpu::MultisampleState {
                count: SAMPLE_COUNT,
                mask: !0,
                alpha_to_coverage_enabled: false,
            },
            multiview_mask: None,
            cache: None,
        });

        // Prozedurale 256x256-Schachbrett-Textur ANLEGEN (kein Asset). RGBA8-srgb,
        // damit die im Shader gesampelten Werte im linearen Raum landen (die
        // Beleuchtung rechnet linear) — analog zum srgb-Surface-Format. Das
        // Hochladen der Texel (queue.write_texture) passiert einmalig beim ersten
        // `render` (siehe `texture_uploaded`), weil `new` bewusst KEINE Queue
        // bekommt — die Signatur bleibt fuer die bestehenden Aufrufer (web.rs,
        // spike3d) unveraendert.
        // Textur-ARRAY (D2Array). Start: EINE Ebene (Fallback-Schachbrett).
        // `set_material_textures` ersetzt das Array spaeter durch mehrere Ebenen.
        let texture = device.create_texture(&wgpu::TextureDescriptor {
            label: Some("materials.texture_array"),
            size: wgpu::Extent3d {
                width: TEXTURE_SIZE,
                height: TEXTURE_SIZE,
                depth_or_array_layers: 1,
            },
            mip_level_count: 1,
            sample_count: 1,
            dimension: wgpu::TextureDimension::D2,
            format: wgpu::TextureFormat::Rgba8UnormSrgb,
            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
            view_formats: &[],
        });
        let texture_view = texture.create_view(&wgpu::TextureViewDescriptor {
            dimension: Some(wgpu::TextureViewDimension::D2Array),
            ..Default::default()
        });
        // Repeat (weltmassstaebliches UV kachelt) + Linear (weiche Filterung).
        let texture_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
            label: Some("materials.sampler"),
            address_mode_u: wgpu::AddressMode::Repeat,
            address_mode_v: wgpu::AddressMode::Repeat,
            address_mode_w: wgpu::AddressMode::Repeat,
            mag_filter: wgpu::FilterMode::Linear,
            min_filter: wgpu::FilterMode::Linear,
            mipmap_filter: wgpu::MipmapFilterMode::Nearest,
            ..Default::default()
        });
        let texture_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
            label: Some("materials.bind"),
            layout: &texture_bind_group_layout,
            entries: &[
                wgpu::BindGroupEntry {
                    binding: 0,
                    resource: wgpu::BindingResource::TextureView(&texture_view),
                },
                wgpu::BindGroupEntry {
                    binding: 1,
                    resource: wgpu::BindingResource::Sampler(&texture_sampler),
                },
            ],
        });

        let uniform = device.create_buffer(&wgpu::BufferDescriptor {
            label: Some("globals.buffer"),
            size: std::mem::size_of::<Globals>() as u64,
            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
            mapped_at_creation: false,
        });
        let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
            label: Some("globals.bind"),
            layout: &bind_group_layout,
            entries: &[wgpu::BindGroupEntry {
                binding: 0,
                resource: uniform.as_entire_binding(),
            }],
        });

        Self {
            pipeline,
            hidden_face_pipeline,
            grid_pipeline,
            highlight_pipeline,
            cap_pipeline,
            cut_line_pipeline,
            textured_pipeline,
            texture_bind_group,
            texture,
            texture_bind_group_layout,
            texture_sampler,
            material_layer_count: 1,
            texture_uploaded: false,
            bind_group,
            uniform,
            mesh: None,
            textured_mesh: None,
            edges: None,
            grid: None,
            highlight: None,
            caps: None,
            cut_lines: None,
            grid_visible: false,
            style: RenderStyle::Shaded,
            depth: None,
            msaa: None,
            color_format,
            globals: Globals::default(),
            // #f5f5f5 heller Hintergrund (wie der 2D-Grundriss).
            clear_color: wgpu::Color {
                r: 0.961,
                g: 0.961,
                b: 0.961,
                a: 1.0,
            },
        }
    }

    /// Erzeugt das Mesh aus geflachten Waenden und laedt die Puffer hoch.
    pub fn upload_walls(&mut self, device: &wgpu::Device, walls: &[WallInput]) {
        self.upload_mesh(device, build_walls_mesh(walls), build_walls_mesh_textured(walls));
    }

    /// Erzeugt das Mesh aus Waenden, Deckenplatten UND rohen Kontext-Meshes
    /// (Terrain/importierte Volumen, siehe `MeshInput`) und laedt die Puffer hoch.
    pub fn upload_model(
        &mut self,
        device: &wgpu::Device,
        walls: &[WallInput],
        slabs: &[SlabInput],
        meshes: &[MeshInput],
    ) {
        self.upload_mesh(
            device,
            build_scene_mesh(walls, slabs, meshes),
            build_scene_mesh_textured(walls, slabs, meshes),
        );
    }

    /// Laedt ein fertiges Mesh in die GPU-Puffer (oder loescht es bei leer) und
    /// erzeugt ZUGLEICH die gecachten Modell-Kanten (Feature-Edges, edges.rs) fuer
    /// die Stile wireframe/hidden — einmalig hier, nicht je Frame.
    fn upload_mesh(&mut self, device: &wgpu::Device, mesh: Mesh, tex_mesh: TexturedMesh) {
        if mesh.indices.is_empty() {
            self.mesh = None;
            self.textured_mesh = None;
            self.edges = None;
            return;
        }

        // Kanten aus der Mesh-Geometrie ableiten und (falls vorhanden) hochladen.
        let edge_verts = build_mesh_edges(&mesh);
        self.edges = if edge_verts.is_empty() {
            None
        } else {
            let vbo = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
                label: Some("edges.vbo"),
                contents: bytemuck::cast_slice(&edge_verts),
                usage: wgpu::BufferUsages::VERTEX,
            });
            Some(LineBuffers {
                vbo,
                vertex_count: (edge_verts.len() / EDGE_FLOATS_PER_VERTEX) as u32,
            })
        };

        let vbo = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
            label: Some("mesh.vbo"),
            contents: bytemuck::cast_slice(&mesh.verts),
            usage: wgpu::BufferUsages::VERTEX,
        });
        let ibo = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
            label: Some("mesh.ibo"),
            contents: bytemuck::cast_slice(&mesh.indices),
            usage: wgpu::BufferUsages::INDEX,
        });
        self.mesh = Some(MeshBuffers {
            vbo,
            ibo,
            index_count: mesh.indices.len() as u32,
        });

        // Texturiertes Pendant (planare Welt-UV je Vertex + Material-Ebenen-Puffer,
        // siehe `build_scene_mesh_textured`) in zwei Vertex-Puffer hochladen. Die
        // Geometrie bleibt bitgleich zum Alt-Pfad; der Stil `Textured` hat ohne
        // Re-Meshing seine Puffer bereit.
        let tvbo = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
            label: Some("mesh.textured.vbo"),
            contents: bytemuck::cast_slice(&tex_mesh.verts),
            usage: wgpu::BufferUsages::VERTEX,
        });
        let tibo = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
            label: Some("mesh.textured.ibo"),
            contents: bytemuck::cast_slice(&tex_mesh.indices),
            usage: wgpu::BufferUsages::INDEX,
        });
        // Material-Ebenen-Puffer (1 f32 je Vertex). Fehlt er (kein Material-Bau),
        // mit `-1` je Vertex fuellen -> alle Vertices auf das Fallback-Schachbrett.
        let vertex_count = tex_mesh.verts.len() / TEXTURED_FLOATS_PER_VERTEX;
        let layer_data: Vec<f32> = if tex_mesh.layers.len() == vertex_count {
            tex_mesh.layers
        } else {
            vec![-1.0; vertex_count]
        };
        let tlvbo = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
            label: Some("mesh.textured.layer.vbo"),
            contents: bytemuck::cast_slice(&layer_data),
            usage: wgpu::BufferUsages::VERTEX,
        });
        self.textured_mesh = Some(TexturedMeshBuffers {
            vbo: tvbo,
            layer_vbo: tlvbo,
            ibo: tibo,
            index_count: tex_mesh.indices.len() as u32,
        });
    }

    /// Setzt die Richtung ZUM Directional-Light (Sonne, world). Das hemisphaerische
    /// Ambient (Himmel/Boden) bleibt bei den Default-Farben.
    pub fn set_light(&mut self, dir_to_light: [f32; 3]) {
        self.globals.light_dir = normalize4(dir_to_light);
    }

    /// Uebersteuert die hemisphaerischen Ambient-Farben (Himmel oben, Boden unten)
    /// und die Sonnenfarbe. Fuer Feinabstimmung des Massenmodell-Looks.
    pub fn set_ambient(&mut self, sky: [f32; 3], ground: [f32; 3], sun: [f32; 3]) {
        self.globals.sky_color = [sky[0], sky[1], sky[2], 1.0];
        self.globals.ground_color = [ground[0], ground[1], ground[2], 1.0];
        self.globals.sun_color = [sun[0], sun[1], sun[2], 1.0];
    }

    /// Darstellungsart setzen (shaded/white/textured/wireframe/hidden). Einzige
    /// Quelle der Wahrheit; `render` leitet daraus Pipeline-Wahl und `mode.x` ab.
    pub fn set_render_style(&mut self, style: RenderStyle) {
        self.style = style;
    }

    /// Rueckwaerts-kompatibler Schalter: `true` bildet auf `White`, `false` auf
    /// `Shaded` ab (kein separater Zustand mehr — vermeidet Widerspruch zum
    /// `style`-Feld). Neue Aufrufer nutzen `set_render_style`.
    pub fn set_white_mode(&mut self, white: bool) {
        self.style = if white {
            RenderStyle::White
        } else {
            RenderStyle::Shaded
        };
    }

    /// Setzt/aktualisiert das Referenz-Bodengitter (grid.rs). Erzeugt den Linien-
    /// Vertex-Buffer aus den Parametern neu und merkt sich die Sichtbarkeit; der
    /// naechste `render` zeichnet das Gitter (bzw. nichts bei `visible == false`).
    /// Ohne Aufruf bleibt das Gitter unsichtbar (Default), damit bestehende
    /// Aufrufer unveraendert bleiben.
    pub fn set_ground_grid(
        &mut self,
        device: &wgpu::Device,
        visible: bool,
        elevation: f32,
        extent: f32,
        spacing: f32,
    ) {
        self.grid_visible = visible;
        let verts = build_ground_grid(elevation, extent, spacing);
        if verts.is_empty() {
            self.grid = None;
            return;
        }
        let vbo = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
            label: Some("grid.vbo"),
            contents: bytemuck::cast_slice(&verts),
            usage: wgpu::BufferUsages::VERTEX,
        });
        self.grid = Some(LineBuffers {
            vbo,
            vertex_count: (verts.len() / GRID_FLOATS_PER_VERTEX) as u32,
        });
    }

    /// Setzt/loescht die Auswahl-Highlight-Linien (Kanten des selektierten Bauteils).
    /// `verts` = interleaved LineList-Vertices `[px,py,pz, r,g,b, ...]` (world,
    /// je 2 Vertices = 1 Segment), exakt wie Grid-/Edge-Vertices
    /// (`GRID_FLOATS_PER_VERTEX`). Leeres Slice -> Highlight loeschen. Analog
    /// `set_ground_grid`, aber ohne Sichtbarkeits-Flag: vorhanden = gezeichnet.
    pub fn set_highlight_lines(&mut self, device: &wgpu::Device, verts: &[f32]) {
        if verts.is_empty() {
            self.highlight = None;
            return;
        }
        let vbo = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
            label: Some("highlight.vbo"),
            contents: bytemuck::cast_slice(verts),
            usage: wgpu::BufferUsages::VERTEX,
        });
        self.highlight = Some(LineBuffers {
            vbo,
            vertex_count: (verts.len() / GRID_FLOATS_PER_VERTEX) as u32,
        });
    }

    /// Aktiviert/deaktiviert den Live-Schnitt und setzt die Schnittebene. `active`
    /// steuert `mode.y` (der Mesh-/Grid-Shader kappt die Geometrie nur bei > 0.5);
    /// `point`/`normal` (world-Meter, Normale = Blickrichtung) fuellen
    /// `section_plane = [n, -dot(n, point)]`. Nur die Uniform-Werte — die Cap-
    /// Geometrie setzt der Aufrufer separat via `set_cut_caps`.
    pub fn set_section_plane(&mut self, active: bool, point: [f32; 3], normal: [f32; 3]) {
        let n = crate::math::normalize(normal);
        let w = -(n[0] * point[0] + n[1] * point[1] + n[2] * point[2]);
        self.globals.section_plane = [n[0], n[1], n[2], w];
        self.globals.mode[1] = if active { 1.0 } else { 0.0 };
    }

    /// Setzt/loescht die Schnittflaechen-Kappen (Cut-Caps). `verts` = interleaved
    /// `[px,py,pz, u,v, ...]` (world-Meter + Ebenenkoordinaten, TriangleList,
    /// `CAP_FLOATS_PER_VERTEX`), wie `section_fill::build_cut_caps` liefert. Leeres
    /// Slice loescht die Caps. Wirkt erst beim naechsten `render`.
    pub fn set_cut_caps(&mut self, device: &wgpu::Device, verts: &[f32]) {
        if verts.is_empty() {
            self.caps = None;
            return;
        }
        let vbo = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
            label: Some("caps.vbo"),
            contents: bytemuck::cast_slice(verts),
            usage: wgpu::BufferUsages::VERTEX,
        });
        self.caps = Some(CapBuffers {
            vbo,
            vertex_count: (verts.len() / CAP_FLOATS_PER_VERTEX) as u32,
        });
    }

    /// Setzt/loescht die Schnitt-Umrisslinien (Cut-Cap-Kanten, P2). `verts` =
    /// interleaved `[px,py,pz, r,g,b, ...]` (world-Meter + Farbe, TriangleList von
    /// Ribbon-Quads, `CUT_LINE_FLOATS_PER_VERTEX`), wie `section_fill::
    /// build_cut_cap_lines` liefert. Leeres Slice loescht die Linien. Wirkt erst
    /// beim naechsten `render`.
    pub fn set_cut_lines(&mut self, device: &wgpu::Device, verts: &[f32]) {
        if verts.is_empty() {
            self.cut_lines = None;
            return;
        }
        let vbo = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
            label: Some("cut_lines.vbo"),
            contents: bytemuck::cast_slice(verts),
            usage: wgpu::BufferUsages::VERTEX,
        });
        self.cut_lines = Some(LineBuffers {
            vbo,
            vertex_count: (verts.len() / CUT_LINE_FLOATS_PER_VERTEX) as u32,
        });
    }

    /// Komfort: Schnittebene setzen UND die Cap-Geometrie aus den (gecachten)
    /// Waenden/Decken neu bauen — bei `active == false` werden Uniform-Flag und
    /// Caps geloescht. Buendelt `set_section_plane` + `build_cut_caps` +
    /// `set_cut_caps`, damit `web.rs` nur EINEN Aufruf braucht.
    pub fn update_section(
        &mut self,
        device: &wgpu::Device,
        active: bool,
        point: [f32; 3],
        normal: [f32; 3],
        walls: &[WallInput],
        slabs: &[SlabInput],
    ) {
        self.set_section_plane(active, point, normal);
        if active {
            let plane = SectionPlane::new(point, normal);
            let caps = build_cut_caps(&plane, walls, slabs);
            self.set_cut_caps(device, &caps);
            // P2: die Schnitt-Umrisslinien (Fugen-/Materiallinien) je ueberlebendem
            // Cut-Cap-Rechteck aus DERSELBEN, dominanz-verschnittenen Rechteck-Menge.
            let lines = build_cut_cap_lines(&plane, walls, slabs);
            self.set_cut_lines(device, &lines);
        } else {
            self.set_cut_caps(device, &[]);
            self.set_cut_lines(device, &[]);
        }
    }

    /// Laedt die Material-Farbkarten fuer den Stil `Textured` als Textur-ARRAY hoch.
    /// `rgba` = dicht gepackte RGBA8-Bytes von `layer_count` Ebenen, jede exakt
    /// `TEXTURE_SIZE × TEXTURE_SIZE` (der Aufrufer skaliert/dekodiert die Bilder
    /// JS-seitig, siehe `web::set_material_textures`/`Wasm3DViewport`). Baut ein
    /// neues Array `[Schachbrett (Ebene 0), Material0 (Ebene 1), …]`, laedt es hoch
    /// und ersetzt die group-1-Bind-Group. Ein Wand-Band verweist per
    /// `WallInput::material_index` (1-basiert) auf seine Ebene; Baender ohne
    /// Material (`-1`) bleiben beim Schachbrett (Ebene 0). `layer_count == 0` (oder
    /// leere/zu kleine Daten) setzt auf das reine Schachbrett-Array zurueck.
    pub fn set_material_textures(
        &mut self,
        device: &wgpu::Device,
        queue: &wgpu::Queue,
        rgba: &[u8],
        layer_count: u32,
    ) {
        let bytes_per_layer = (TEXTURE_SIZE * TEXTURE_SIZE * 4) as usize;
        // Ungueltige/leere Eingabe -> reines Schachbrett-Array (1 Ebene).
        let valid = layer_count > 0 && rgba.len() >= layer_count as usize * bytes_per_layer;
        let material_layers = if valid { layer_count } else { 0 };
        let total_layers = material_layers + 1; // Ebene 0 = Schachbrett

        let texture = device.create_texture(&wgpu::TextureDescriptor {
            label: Some("materials.texture_array"),
            size: wgpu::Extent3d {
                width: TEXTURE_SIZE,
                height: TEXTURE_SIZE,
                depth_or_array_layers: total_layers,
            },
            mip_level_count: 1,
            sample_count: 1,
            dimension: wgpu::TextureDimension::D2,
            format: wgpu::TextureFormat::Rgba8UnormSrgb,
            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
            view_formats: &[],
        });

        // Ebene 0: Schachbrett-Fallback.
        let checker = build_checker_texture();
        let write_layer = |layer: u32, data: &[u8]| {
            queue.write_texture(
                wgpu::TexelCopyTextureInfo {
                    texture: &texture,
                    mip_level: 0,
                    origin: wgpu::Origin3d {
                        x: 0,
                        y: 0,
                        z: layer,
                    },
                    aspect: wgpu::TextureAspect::All,
                },
                data,
                wgpu::TexelCopyBufferLayout {
                    offset: 0,
                    bytes_per_row: Some(4 * TEXTURE_SIZE),
                    rows_per_image: Some(TEXTURE_SIZE),
                },
                wgpu::Extent3d {
                    width: TEXTURE_SIZE,
                    height: TEXTURE_SIZE,
                    depth_or_array_layers: 1,
                },
            );
        };
        write_layer(0, &checker);
        for i in 0..material_layers {
            let start = i as usize * bytes_per_layer;
            write_layer(i + 1, &rgba[start..start + bytes_per_layer]);
        }

        let view = texture.create_view(&wgpu::TextureViewDescriptor {
            dimension: Some(wgpu::TextureViewDimension::D2Array),
            ..Default::default()
        });
        self.texture_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
            label: Some("materials.bind"),
            layout: &self.texture_bind_group_layout,
            entries: &[
                wgpu::BindGroupEntry {
                    binding: 0,
                    resource: wgpu::BindingResource::TextureView(&view),
                },
                wgpu::BindGroupEntry {
                    binding: 1,
                    resource: wgpu::BindingResource::Sampler(&self.texture_sampler),
                },
            ],
        });
        self.texture = texture;
        self.material_layer_count = total_layers;
        // Ebene 0 (Schachbrett) ist bereits geladen -> den Lazy-Upload ueberspringen.
        self.texture_uploaded = true;
    }

    /// Stellt sicher, dass ein Tiefenpuffer passend zur Ziel-Groesse existiert.
    fn ensure_depth(&mut self, device: &wgpu::Device, w: u32, h: u32) {
        let ok = matches!(&self.depth, Some(d) if d.width == w && d.height == h);
        if ok {
            return;
        }
        let texture = device.create_texture(&wgpu::TextureDescriptor {
            label: Some("depth"),
            size: wgpu::Extent3d {
                width: w.max(1),
                height: h.max(1),
                depth_or_array_layers: 1,
            },
            mip_level_count: 1,
            // Muss zur Sample-Zahl der Farb-/Pipeline-Ziele passen (MSAA).
            sample_count: SAMPLE_COUNT,
            dimension: wgpu::TextureDimension::D2,
            format: DEPTH_FORMAT,
            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
            view_formats: &[],
        });
        let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
        self.depth = Some(DepthTarget {
            view,
            width: w.max(1),
            height: h.max(1),
        });
    }

    /// Stellt sicher, dass eine multisampled Farb-Zwischentextur passend zur
    /// Ziel-Groesse existiert (wird jeden Frame in die Surface-View aufgeloest).
    fn ensure_msaa(&mut self, device: &wgpu::Device, w: u32, h: u32) {
        let ok = matches!(&self.msaa, Some(m) if m.width == w && m.height == h);
        if ok {
            return;
        }
        let texture = device.create_texture(&wgpu::TextureDescriptor {
            label: Some("msaa.color"),
            size: wgpu::Extent3d {
                width: w.max(1),
                height: h.max(1),
                depth_or_array_layers: 1,
            },
            mip_level_count: 1,
            sample_count: SAMPLE_COUNT,
            dimension: wgpu::TextureDimension::D2,
            format: self.color_format,
            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
            view_formats: &[],
        });
        let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
        self.msaa = Some(MsaaTarget {
            view,
            width: w.max(1),
            height: h.max(1),
        });
    }

    /// Zeichnet einen Frame und loest ihn in `target` auf (Surface- oder
    /// Offscreen-Textur). Intern wird in eine multisampled Zwischentextur (MSAA)
    /// gerendert und am Ende der Pass in `target` aufgeloest. Die Kamera liefert
    /// die View-Projektions-Matrix; `viewport` bestimmt das Seitenverhaeltnis und
    /// die Tiefen-/MSAA-Puffer-Groesse.
    pub fn render(
        &mut self,
        device: &wgpu::Device,
        queue: &wgpu::Queue,
        target: &wgpu::TextureView,
        camera: &Camera,
        viewport: (u32, u32),
    ) {
        let (w, h) = viewport;
        let aspect = if h > 0 { w as f32 / h as f32 } else { 1.0 };
        let vp: Mat4 = view_projection(camera, aspect);
        self.globals.view_proj = vp;
        // `mode.x` fuer den Mesh-Shader aus dem aktuellen Stil ableiten (Material/
        // Clay/Hidden-Flaechen). Wireframe zeichnet keine Flaechen -> Wert egal.
        self.globals.mode[0] = self.style.mesh_mode();
        // `mode.z` = Anzahl gueltiger Textur-Array-Ebenen (Klemmung im Textur-Shader).
        self.globals.mode[2] = self.material_layer_count as f32;
        queue.write_buffer(&self.uniform, 0, bytemuck::bytes_of(&self.globals));

        // Prozedurale Schachbrett-Textur einmalig hochladen (lazy, weil `new` keine
        // Queue bekommt — Signatur unveraendert). Danach steht sie fuer den Stil
        // `Textured` bereit; kostet ab dem zweiten Frame nichts mehr.
        if !self.texture_uploaded {
            let texel_data = build_checker_texture();
            queue.write_texture(
                wgpu::TexelCopyTextureInfo {
                    texture: &self.texture,
                    mip_level: 0,
                    origin: wgpu::Origin3d::ZERO,
                    aspect: wgpu::TextureAspect::All,
                },
                &texel_data,
                wgpu::TexelCopyBufferLayout {
                    offset: 0,
                    bytes_per_row: Some(4 * TEXTURE_SIZE),
                    rows_per_image: Some(TEXTURE_SIZE),
                },
                wgpu::Extent3d {
                    width: TEXTURE_SIZE,
                    height: TEXTURE_SIZE,
                    depth_or_array_layers: 1,
                },
            );
            self.texture_uploaded = true;
        }

        self.ensure_depth(device, w, h);
        self.ensure_msaa(device, w, h);
        let depth_view = &self.depth.as_ref().unwrap().view;
        let msaa_view = &self.msaa.as_ref().unwrap().view;

        let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
            label: Some("3d.encoder"),
        });
        {
            let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
                label: Some("3d.pass"),
                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
                    // In die multisampled Zwischentextur rendern; wgpu loest sie am
                    // Ende der Pass automatisch in `target` (Surface-View) auf.
                    view: msaa_view,
                    // Ziel ist eine 2D-Textur (kein 3D-Volumen) -> kein Depth-Slice.
                    depth_slice: None,
                    resolve_target: Some(target),
                    ops: wgpu::Operations {
                        load: wgpu::LoadOp::Clear(self.clear_color),
                        // Der multisampled Inhalt selbst wird nach dem Aufloesen nicht
                        // mehr gebraucht — Discard spart Bandbreite.
                        store: wgpu::StoreOp::Discard,
                    },
                })],
                depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
                    view: depth_view,
                    depth_ops: Some(wgpu::Operations {
                        load: wgpu::LoadOp::Clear(1.0),
                        store: wgpu::StoreOp::Store,
                    }),
                    stencil_ops: None,
                }),
                timestamp_writes: None,
                occlusion_query_set: None,
                multiview_mask: None,
            });

            // 1) Gefuellte Flaechen — in allen Stilen ausser "wireframe". Im
            //    Hidden-Line-Stil die leicht nach hinten gebiaste Pipeline, damit
            //    die folgenden Kanten sauber obenauf liegen.
            if self.style.draws_faces() {
                if self.style == RenderStyle::Textured {
                    // Texturierter Pfad: eigene Pipeline + zweiter Vertex-Puffer
                    // ([pos, normal, uv]) + zusaetzliche Textur-Bind-Group (group 1).
                    // Globals bleiben group 0. Fehlt der texturierte Puffer (kein
                    // Mesh geladen), wird nichts gezeichnet.
                    if let Some(t) = &self.textured_mesh {
                        pass.set_pipeline(&self.textured_pipeline);
                        pass.set_bind_group(0, &self.bind_group, &[]);
                        pass.set_bind_group(1, &self.texture_bind_group, &[]);
                        pass.set_vertex_buffer(0, t.vbo.slice(..));
                        pass.set_vertex_buffer(1, t.layer_vbo.slice(..));
                        pass.set_index_buffer(t.ibo.slice(..), wgpu::IndexFormat::Uint32);
                        pass.draw_indexed(0..t.index_count, 0, 0..1);
                    }
                } else if let Some(m) = &self.mesh {
                    let face_pipeline = if self.style == RenderStyle::Hidden {
                        &self.hidden_face_pipeline
                    } else {
                        &self.pipeline
                    };
                    pass.set_pipeline(face_pipeline);
                    pass.set_bind_group(0, &self.bind_group, &[]);
                    pass.set_vertex_buffer(0, m.vbo.slice(..));
                    pass.set_index_buffer(m.ibo.slice(..), wgpu::IndexFormat::Uint32);
                    pass.draw_indexed(0..m.index_count, 0, 0..1);
                }
            }

            // 1b) Schnittflaechen-Kappen (Cut-Caps) NACH den Flaechen, VOR den
            //     Kanten: sie fuellen die an der Schnittebene weggeschnittene Wunde
            //     mit der Diagonalschraffur (CAP_WGSL). Werden selbst NICHT gekappt.
            //     Nur vorhanden, wenn der Live-Schnitt aktiv ist (set_cut_caps).
            if let Some(c) = &self.caps {
                pass.set_pipeline(&self.cap_pipeline);
                pass.set_bind_group(0, &self.bind_group, &[]);
                pass.set_vertex_buffer(0, c.vbo.slice(..));
                pass.draw(0..c.vertex_count, 0..1);
            }

            // 1c) Schnitt-Umrisslinien (Cut-Cap-Kanten, P2) NACH den Caps: die
            //     Material-/Fugenlinien je ueberlebendem Cut-Rechteck (Ribbon-
            //     TriangleList, eigener Stil je Schicht). Ohne Tiefentest (Always)
            //     liegen sie sichtbar auf den Caps; der Kamera-Lift in
            //     `build_cut_cap_lines` haelt sie vor der Ebenen-Kappung.
            if let Some(cl) = &self.cut_lines {
                pass.set_pipeline(&self.cut_line_pipeline);
                pass.set_bind_group(0, &self.bind_group, &[]);
                pass.set_vertex_buffer(0, cl.vbo.slice(..));
                pass.draw(0..cl.vertex_count, 0..1);
            }

            // 2) Modell-Kanten (wireframe/hidden) — LineList-Pipeline (wie Grid).
            //    Im Hidden-Stil obenauf, tiefengetestet gegen die (gebiasten)
            //    Flaechen -> verdeckte Kanten fallen weg. Im Wireframe ohne
            //    Flaechen -> alle Kanten sichtbar.
            if self.style.draws_edges() {
                if let Some(e) = &self.edges {
                    pass.set_pipeline(&self.grid_pipeline);
                    pass.set_bind_group(0, &self.bind_group, &[]);
                    pass.set_vertex_buffer(0, e.vbo.slice(..));
                    pass.draw(0..e.vertex_count, 0..1);
                }
            }

            // 3) Bodengitter NACH dem Modell zeichnen (gemeinsamer Tiefenpuffer ->
            //    das Modell verdeckt das Gitter korrekt). Unabhaengig vom Stil, nur
            //    wenn sichtbar + vorhanden.
            if self.grid_visible {
                if let Some(g) = &self.grid {
                    pass.set_pipeline(&self.grid_pipeline);
                    pass.set_bind_group(0, &self.bind_group, &[]);
                    pass.set_vertex_buffer(0, g.vbo.slice(..));
                    pass.draw(0..g.vertex_count, 0..1);
                }
            }

            // 4) Auswahl-Highlight-Linien als ALLERLETZTES — ohne Tiefentest
            //    (`highlight_pipeline`: depth_compare Always, kein Depth-Write),
            //    damit die Auswahl immer sichtbar obenauf liegt und auch hinter
            //    Waenden durchscheint. Nur wenn per `set_highlight_lines` gesetzt.
            if let Some(hl) = &self.highlight {
                pass.set_pipeline(&self.highlight_pipeline);
                pass.set_bind_group(0, &self.bind_group, &[]);
                pass.set_vertex_buffer(0, hl.vbo.slice(..));
                pass.draw(0..hl.vertex_count, 0..1);
            }
        }
        queue.submit(std::iter::once(encoder.finish()));
    }
}
