render2d: Headless-Rendering — PNG ohne Fenster + Golden-Image-Test

HeadlessRenderer (Feature headless) baut Instance/Adapter/Device ohne
Surface (Vulkan, kein Display-Server); render_to_image rendert in eine
Rgba8Unorm-Texture (non-sRGB wie ColorMode::Web) und liest mit 256-Byte-
Row-Padding zurueck. Draw-Code unveraendert geteilt mit dem Fenster-Pfad.
CLI-Bin render_png (Demo-Szene, 990x630); Demo additiv um 45-Grad-
Schraffur, Tuerblatt und Schwenkbogen erweitert, damit das Golden alle
Pfade abdeckt. Golden-Test mit eingechecktem Referenzbild: 0/623700 Pixel
Abweichung, bit-exakt reproduzierbar; bei Abweichung Diff-Dump nach
target/. Doku in docs/design/engine-headless.md.
This commit is contained in:
2026-07-03 08:31:38 +02:00
parent ce6bd26ff7
commit 4b02e40238
8 changed files with 625 additions and 9 deletions
+62 -9
View File
@@ -6,13 +6,16 @@
// noetig), damit die Szene auch headless testbar bleibt.
use crate::tessellate::PX_PER_M;
use crate::types::{FillPolygon, Line, Outline, Scene, Text, TextAlign, ViewBox};
use crate::types::{Arc, FillPolygon, Line, Outline, Polyline, Scene, Text, TextAlign, ViewBox};
/// Demo-Szene in Modell-Metern: ein L-foermiger Wand-Poche (konkav!), ein Raum
/// und ein paar Striche — genug, um Fuellung, Umriss und Papier-mm-Linien zu sehen.
/// mit 45-Grad-Schraffur, ein Tuerschwenk-Bogen und ein paar Striche — genug, um
/// ALLE Render-Pfade zu sehen (Fuellung, Umriss, Schraffur/widthScreen,
/// analytischer Bogen, Papier-mm-Linien, Text).
pub fn demo_scene() -> Scene {
let wall_grey: [f32; 4] = [0.55, 0.55, 0.55, 1.0];
let room_blue: [f32; 4] = [0.20, 0.45, 0.85, 0.18];
let hatch_blue: [f32; 4] = [0.20, 0.45, 0.85, 0.55];
let ink: [f32; 4] = [0.10, 0.10, 0.10, 1.0];
// Konkaves L (Wandflaeche).
@@ -27,6 +30,34 @@ pub fn demo_scene() -> Scene {
// Ein transluzenter Raum daneben.
let room = vec![[5.0, 0.0], [9.0, 0.0], [9.0, 4.0], [5.0, 4.0]];
// 45-Grad-Schraffur im Raum (wie ein SVG-<pattern>): Strichbreite in
// BILDSCHIRM-Einheiten (`width_screen`), skaliert also mit dem Zoom wie die
// Musterkachel — genau der Pfad, den echte Material-Schraffuren nehmen.
// Linien y = x + c, auf das Raum-Rechteck geklippt.
let (hx0, hx1, hy0, hy1) = (5.0f32, 9.0f32, 0.0f32, 4.0f32);
let hatch_step = 0.5f32;
let mut hatch: Vec<Polyline> = Vec::new();
let mut c = hy0 - hx1 + hatch_step;
while c < hy1 - hx0 {
let xa = hx0.max(hy0 - c);
let xb = hx1.min(hy1 - c);
if xb > xa {
hatch.push(Polyline {
pts: vec![[xa, xa + c], [xb, xb + c]],
color: hatch_blue,
// Breite in Bildschirm-Einheiten (viewBox-px), NICHT Papier-mm.
width_mm: 1.2,
width_screen: true,
dash: None,
// Ueber der Raum-Fuellung (gleiches z=2, Kategorie Polyline kommt
// in der stabilen Sortierung NACH den Fills), unter dem
// Raum-Umriss (z=3).
z: 2,
});
}
c += hatch_step;
}
Scene {
fills: vec![
FillPolygon {
@@ -56,16 +87,38 @@ pub fn demo_scene() -> Scene {
z: 3,
},
],
polylines: vec![],
arcs: vec![],
lines: vec![Line {
a: [0.0, -1.0],
b: [9.0, -1.0],
polylines: hatch,
// Tuerschwenk in der linken Raumwand: analytischer Bogen (SDF-Pipeline),
// Angel bei (5,1), Blatt offen nach (6,1), Schwenk bis (5,2).
arcs: vec![Arc {
center: [5.0, 1.0],
from: [6.0, 1.0],
to: [5.0, 2.0],
r: 1.0,
color: ink,
width_mm: 0.25,
width_mm: 0.18,
dash: None,
z: 4,
}],
lines: vec![
Line {
a: [0.0, -1.0],
b: [9.0, -1.0],
color: ink,
width_mm: 0.25,
dash: None,
z: 4,
},
// Tuerblatt zum Schwenkbogen (die Boegen zeichnen NACH den Linien,
// der Bogen liegt also wie im echten Plan ueber dem Blatt).
Line {
a: [5.0, 1.0],
b: [6.0, 1.0],
color: ink,
width_mm: 0.25,
dash: None,
z: 5,
},
],
// Ein Raumstempel-artiger Text (echte Glyphen via Atlas), mittig im Raum.
texts: vec![Text {
pos: [7.0, 2.0],