2D-Engine-Parität: WASM-Pfad rendert deckungsgleich zum SVG-Referenzpfad
Sechs Lücken geschlossen: Text-Massstab vom Geometrie-Massstab entkoppelt (set_text_scale, spiegelt SVG-Referenzskala); glyphon auf ColorMode::Web (Text war linear-konvertiert zu dunkel); z-basierte Maler-Reihenfolge (interleavte draw_sequence statt fills-vor-lines, Alt-Szenen unverändert); CSS-Klassenfarben/-Opacities in toRenderScene gespiegelt (Türschwenk etc.); greyed-Dimmung 0.3 auf allen Primitiven; Dämmschraffur am Modell-Ursprung verankert (userSpaceOnUse), exakte Bézier-Wellenform statt Sinus und kachelgekoppelte Strichbreite (widthScreen-Modus). Probe scripts/probe-engine-parity.mjs vergleicht ?gl=0 gegen ?engine=wasm; Rest-Diff nur AA/Glyphen-Rasterung. cargo 17/17, vitest 94/94, Builds grün.
This commit is contained in:
@@ -0,0 +1,60 @@
|
|||||||
|
// Vergleicht den SVG-Referenzpfad gegen den nativen render2d-WASM/WebGPU-Pfad
|
||||||
|
// (?engine=wasm): lädt die App einmal ohne Engine-Param (SVG) und einmal mit
|
||||||
|
// ?engine=wasm, gleicher Viewport/Zoom, schießt je einen Screenshot. Prüft
|
||||||
|
// zusätzlich, ob WebGPU verfügbar ist und der native Renderer tatsächlich
|
||||||
|
// initialisiert hat (statt still auf SVG zurückzufallen).
|
||||||
|
|
||||||
|
import puppeteer from "puppeteer";
|
||||||
|
|
||||||
|
const BASE = process.env.PROBE_URL || "http://localhost:5187/";
|
||||||
|
|
||||||
|
const browser = await puppeteer.launch({
|
||||||
|
headless: "new",
|
||||||
|
args: [
|
||||||
|
"--no-sandbox",
|
||||||
|
"--enable-unsafe-webgpu",
|
||||||
|
"--enable-features=Vulkan",
|
||||||
|
"--use-angle=vulkan",
|
||||||
|
"--ignore-gpu-blocklist",
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
async function shoot(url, path, tag) {
|
||||||
|
const page = await browser.newPage();
|
||||||
|
await page.setViewport({ width: 1400, height: 900, deviceScaleFactor: 1 });
|
||||||
|
const logs = [];
|
||||||
|
page.on("console", (m) => logs.push(`[${m.type()}] ${m.text()}`));
|
||||||
|
page.on("pageerror", (e) => logs.push(`[PAGEERROR] ${e.message}`));
|
||||||
|
try {
|
||||||
|
await page.goto(url, { waitUntil: "networkidle0", timeout: 30000 });
|
||||||
|
} catch (e) {
|
||||||
|
logs.push(`[GOTO] ${e.message}`);
|
||||||
|
}
|
||||||
|
// Warten, bis Plan + evtl. WASM/WebGPU-Renderer initialisiert und gezeichnet hat.
|
||||||
|
await new Promise((r) => setTimeout(r, 2500));
|
||||||
|
|
||||||
|
const info = await page.evaluate(() => {
|
||||||
|
const svg = document.querySelector(".plan-svg");
|
||||||
|
const canvases = document.querySelectorAll("canvas");
|
||||||
|
return {
|
||||||
|
hasGpu: typeof navigator !== "undefined" && "gpu" in navigator,
|
||||||
|
hasSvg: !!svg,
|
||||||
|
svgChildren: svg ? svg.querySelectorAll("path,polygon,polyline,line,rect,text").length : 0,
|
||||||
|
canvasCount: canvases.length,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.screenshot({ path });
|
||||||
|
console.log(`\n=== ${tag} (${url}) ===`);
|
||||||
|
console.log(JSON.stringify(info, null, 2));
|
||||||
|
const warns = logs.filter((l) => /warn|error|PAGEERROR/i.test(l));
|
||||||
|
console.log("Warnungen/Fehler:", warns.length ? "\n" + warns.join("\n") : "(keine)");
|
||||||
|
await page.close();
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
await shoot(BASE + "?gl=0", "scripts/probe-parity-svg.png", "SVG-Referenzpfad");
|
||||||
|
await shoot(BASE + "?engine=wasm", "scripts/probe-parity-wasm.png", "WASM/WebGPU-Engine");
|
||||||
|
|
||||||
|
await browser.close();
|
||||||
|
console.log("\nScreenshots: scripts/probe-parity-svg.png vs scripts/probe-parity-wasm.png");
|
||||||
@@ -32,10 +32,12 @@ pub fn demo_scene() -> Scene {
|
|||||||
FillPolygon {
|
FillPolygon {
|
||||||
pts: l.clone(),
|
pts: l.clone(),
|
||||||
color: wall_grey,
|
color: wall_grey,
|
||||||
|
z: 0,
|
||||||
},
|
},
|
||||||
FillPolygon {
|
FillPolygon {
|
||||||
pts: room.clone(),
|
pts: room.clone(),
|
||||||
color: room_blue,
|
color: room_blue,
|
||||||
|
z: 2,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
outlines: vec![
|
outlines: vec![
|
||||||
@@ -44,12 +46,14 @@ pub fn demo_scene() -> Scene {
|
|||||||
color: ink,
|
color: ink,
|
||||||
width_mm: 0.35,
|
width_mm: 0.35,
|
||||||
dash: None,
|
dash: None,
|
||||||
|
z: 1,
|
||||||
},
|
},
|
||||||
Outline {
|
Outline {
|
||||||
pts: room,
|
pts: room,
|
||||||
color: ink,
|
color: ink,
|
||||||
width_mm: 0.18,
|
width_mm: 0.18,
|
||||||
dash: None,
|
dash: None,
|
||||||
|
z: 3,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
polylines: vec![],
|
polylines: vec![],
|
||||||
@@ -60,6 +64,7 @@ pub fn demo_scene() -> Scene {
|
|||||||
color: ink,
|
color: ink,
|
||||||
width_mm: 0.25,
|
width_mm: 0.25,
|
||||||
dash: None,
|
dash: None,
|
||||||
|
z: 4,
|
||||||
}],
|
}],
|
||||||
// Ein Raumstempel-artiger Text (echte Glyphen via Atlas), mittig im Raum.
|
// Ein Raumstempel-artiger Text (echte Glyphen via Atlas), mittig im Raum.
|
||||||
texts: vec![Text {
|
texts: vec![Text {
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ use wgpu::util::DeviceExt;
|
|||||||
|
|
||||||
use crate::ortho::{compute_ortho_matrix, corrected_view_box, meet_scale, mm_to_device_px, Mat4};
|
use crate::ortho::{compute_ortho_matrix, corrected_view_box, meet_scale, mm_to_device_px, Mat4};
|
||||||
use crate::shaders::{ARC_WGSL, FILL_WGSL, LINE_WGSL};
|
use crate::shaders::{ARC_WGSL, FILL_WGSL, LINE_WGSL};
|
||||||
use crate::tessellate::{compile_scene, to_screen, ArcInstanceData, GpuGeometry, MAX_ARC_DASH};
|
use crate::tessellate::{compile_scene, to_screen, ArcInstanceData, BatchKind, GpuGeometry, MAX_ARC_DASH};
|
||||||
use crate::types::{Scene, Text, TextAlign, ViewBox};
|
use crate::types::{Scene, Text, TextAlign, ViewBox};
|
||||||
|
|
||||||
/// MSAA-Faktor: 4x Multisampling fuer glatte Linien-/Fuellkanten (wie im Browser).
|
/// MSAA-Faktor: 4x Multisampling fuer glatte Linien-/Fuellkanten (wie im Browser).
|
||||||
@@ -170,6 +170,16 @@ pub struct Renderer {
|
|||||||
pub clear_color: wgpu::Color,
|
pub clear_color: wgpu::Color,
|
||||||
/// Papier-Massstab-Nenner (1:N) fuer echte mm-Strichbreiten.
|
/// Papier-Massstab-Nenner (1:N) fuer echte mm-Strichbreiten.
|
||||||
pub paper_scale_n: f32,
|
pub paper_scale_n: f32,
|
||||||
|
/// Papier-Massstab-Nenner (1:N) NUR fuer die Text-Schriftgroesse — bewusst
|
||||||
|
/// vom geometrischen `paper_scale_n` ENTKOPPELT: der SVG-Referenzpfad haelt
|
||||||
|
/// die Raumstempel-Schriftgroesse im normalen Anzeigemodus auf einer festen
|
||||||
|
/// Referenzskala 1:100 (lesbar, unabhaengig vom zufaelligen Einpass-Massstab
|
||||||
|
/// der aktuellen Ansicht) und wechselt erst im Druck-Modus auf den echten
|
||||||
|
/// gemessenen Massstab; siehe PlanView.tsx `textScaleForGl`/case "text". Ohne
|
||||||
|
/// diese Trennung wuerde Text hier mit dem live aus dem Ausschnitt
|
||||||
|
/// abgeleiteten `paper_scale_n` skalieren (z.B. 1:32 statt 1:100) und damit
|
||||||
|
/// sichtbar kleiner/groesser als im SVG-Pfad erscheinen.
|
||||||
|
pub text_scale_n: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Ein Uniform-Puffer, der `capacity` Globals-Bloecke fasst, plus Bind-Group.
|
/// Ein Uniform-Puffer, der `capacity` Globals-Bloecke fasst, plus Bind-Group.
|
||||||
@@ -410,6 +420,7 @@ impl Renderer {
|
|||||||
a: 1.0,
|
a: 1.0,
|
||||||
},
|
},
|
||||||
paper_scale_n: 100.0,
|
paper_scale_n: 100.0,
|
||||||
|
text_scale_n: 100.0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -499,7 +510,18 @@ impl Renderer {
|
|||||||
}
|
}
|
||||||
let cache = glyphon::Cache::new(device);
|
let cache = glyphon::Cache::new(device);
|
||||||
let viewport = glyphon::Viewport::new(device, &cache);
|
let viewport = glyphon::Viewport::new(device, &cache);
|
||||||
let mut atlas = glyphon::TextAtlas::new(device, queue, &cache, self.format);
|
// ColorMode::Web statt Default (Accurate): die Surface ist non-sRGB und
|
||||||
|
// ALLE Geometrie-Passes schreiben ihre sRGB-Farbwerte unkonvertiert —
|
||||||
|
// Accurate wuerde die Textfarbe nach linear wandeln und die Glyphen
|
||||||
|
// deutlich dunkler zeichnen als der SVG-Referenzpfad (z.B. #5a7a9a ->
|
||||||
|
// (26,50,82) statt (90,122,154)).
|
||||||
|
let mut atlas = glyphon::TextAtlas::with_color_mode(
|
||||||
|
device,
|
||||||
|
queue,
|
||||||
|
&cache,
|
||||||
|
self.format,
|
||||||
|
glyphon::ColorMode::Web,
|
||||||
|
);
|
||||||
let renderer = glyphon::TextRenderer::new(
|
let renderer = glyphon::TextRenderer::new(
|
||||||
&mut atlas,
|
&mut atlas,
|
||||||
device,
|
device,
|
||||||
@@ -592,6 +614,9 @@ impl Renderer {
|
|||||||
let (vw, vh) = (viewport.0 as f32, viewport.1 as f32);
|
let (vw, vh) = (viewport.0 as f32, viewport.1 as f32);
|
||||||
let proj: Mat4 = compute_ortho_matrix(view_box, vw, vh);
|
let proj: Mat4 = compute_ortho_matrix(view_box, vw, vh);
|
||||||
let mm_px = mm_to_device_px(view_box, vw, vh, self.paper_scale_n);
|
let mm_px = mm_to_device_px(view_box, vw, vh, self.paper_scale_n);
|
||||||
|
// Eigene mm->px-Umrechnung fuer Text (siehe `text_scale_n`-Doku): gleiche
|
||||||
|
// Formel, aber am Text-Referenz-Massstab statt am Geometrie-Massstab.
|
||||||
|
let text_mm_px = mm_to_device_px(view_box, vw, vh, self.text_scale_n);
|
||||||
// Geraete-px je Bildschirm-Einheit (== meet-Skala) — der Bogen-Shader braucht
|
// Geraete-px je Bildschirm-Einheit (== meet-Skala) — der Bogen-Shader braucht
|
||||||
// beides: px_per_screen fuer Radial-/Kappen-/Dash-AA, stroke_scale fuer die
|
// beides: px_per_screen fuer Radial-/Kappen-/Dash-AA, stroke_scale fuer die
|
||||||
// Papier-mm-Breite (identisch zu den Linien).
|
// Papier-mm-Breite (identisch zu den Linien).
|
||||||
@@ -609,27 +634,38 @@ impl Renderer {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Alle Globals-Bloecke der Reihenfolge nach (erst Fuell-, dann Linien-Batches)
|
// Alle Globals-Bloecke in MALER-Reihenfolge (`draw_sequence`, interleaved
|
||||||
// sammeln, den Puffer einmal schreiben, danach nur noch dynamisch binden.
|
// Fuell-/Linien-Batches) sammeln, den Puffer einmal schreiben, danach nur
|
||||||
|
// noch dynamisch binden — Reihenfolge identisch zur Draw-Schleife unten.
|
||||||
let mut blocks: Vec<Globals> = Vec::new();
|
let mut blocks: Vec<Globals> = Vec::new();
|
||||||
if let Some(scene) = &self.scene {
|
if let Some(scene) = &self.scene {
|
||||||
for b in &scene.geo.fill_batches {
|
for &(kind, idx) in &scene.geo.draw_sequence {
|
||||||
blocks.push(Globals {
|
match kind {
|
||||||
view_proj: proj,
|
BatchKind::Fill => {
|
||||||
color: b.color,
|
let b = &scene.geo.fill_batches[idx as usize];
|
||||||
viewport_px: [vw, vh],
|
blocks.push(Globals {
|
||||||
stroke_px: 0.0,
|
view_proj: proj,
|
||||||
stroke_scale: mm_px,
|
color: b.color,
|
||||||
});
|
viewport_px: [vw, vh],
|
||||||
}
|
stroke_px: 0.0,
|
||||||
for b in &scene.geo.line_batches {
|
stroke_scale: mm_px,
|
||||||
blocks.push(Globals {
|
});
|
||||||
view_proj: proj,
|
}
|
||||||
color: b.color,
|
BatchKind::Line => {
|
||||||
viewport_px: [vw, vh],
|
let b = &scene.geo.line_batches[idx as usize];
|
||||||
stroke_px: b.stroke_mm,
|
blocks.push(Globals {
|
||||||
stroke_scale: mm_px,
|
view_proj: proj,
|
||||||
});
|
color: b.color,
|
||||||
|
viewport_px: [vw, vh],
|
||||||
|
stroke_px: b.stroke_mm,
|
||||||
|
// Schraffur-Striche (`width_screen`) sind an die
|
||||||
|
// Musterkachel gekoppelt (Bildschirm-Einheiten,
|
||||||
|
// zoom-skalierend wie der SVG-`<pattern>`-Strich) —
|
||||||
|
// Geraete-px = Breite * meet-Skala statt * mm->px.
|
||||||
|
stroke_scale: if b.width_screen { px_per_screen } else { mm_px },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -665,7 +701,7 @@ impl Renderer {
|
|||||||
let attrs = glyphon::Attrs::new()
|
let attrs = glyphon::Attrs::new()
|
||||||
.family(glyphon::Family::Name(&family));
|
.family(glyphon::Family::Name(&family));
|
||||||
for t in &scene.texts {
|
for t in &scene.texts {
|
||||||
let font_px = t.size_mm * mm_px;
|
let font_px = t.size_mm * text_mm_px;
|
||||||
// Unlesbar kleine Glyphen ueberspringen (spart Shaping/Atlas).
|
// Unlesbar kleine Glyphen ueberspringen (spart Shaping/Atlas).
|
||||||
if !(font_px >= 1.0) || t.content.is_empty() {
|
if !(font_px >= 1.0) || t.content.is_empty() {
|
||||||
continue;
|
continue;
|
||||||
@@ -794,33 +830,46 @@ impl Renderer {
|
|||||||
|
|
||||||
if let (Some(scene), Some(u)) = (&self.scene, &self.uniform) {
|
if let (Some(scene), Some(u)) = (&self.scene, &self.uniform) {
|
||||||
let stride = self.uniform_stride as u32;
|
let stride = self.uniform_stride as u32;
|
||||||
let mut block = 0u32; // Laufindex ueber Fuell- dann Linien-Batches
|
|
||||||
|
|
||||||
// 1) Fuellungen.
|
// Geometrie in MALER-Reihenfolge (`draw_sequence`): Fuell- und
|
||||||
if let (Some(vbo), Some(ibo)) = (&scene.fill_vbo, &scene.fill_ibo) {
|
// Linien-Batches interleaved, Pipeline-/Puffer-Wechsel nur beim
|
||||||
pass.set_pipeline(&self.fill_pipeline);
|
// Wechsel der Batch-Art. Nur so deckt eine spaetere Fuellung
|
||||||
pass.set_vertex_buffer(0, vbo.slice(..));
|
// einen frueheren Strich ab (SVG-Paritaet, Primitive-Reihenfolge).
|
||||||
pass.set_index_buffer(ibo.slice(..), wgpu::IndexFormat::Uint32);
|
let mut bound: Option<BatchKind> = None;
|
||||||
for b in &scene.geo.fill_batches {
|
for (block, &(kind, idx)) in scene.geo.draw_sequence.iter().enumerate() {
|
||||||
pass.set_bind_group(0, &u.bind_group, &[block * stride]);
|
match kind {
|
||||||
let start = b.start_index;
|
BatchKind::Fill => {
|
||||||
pass.draw_indexed(start..start + b.index_count, 0, 0..1);
|
let (Some(vbo), Some(ibo)) = (&scene.fill_vbo, &scene.fill_ibo)
|
||||||
block += 1;
|
else {
|
||||||
}
|
continue;
|
||||||
} else {
|
};
|
||||||
block += scene.geo.fill_batches.len() as u32;
|
if bound != Some(BatchKind::Fill) {
|
||||||
}
|
pass.set_pipeline(&self.fill_pipeline);
|
||||||
|
pass.set_vertex_buffer(0, vbo.slice(..));
|
||||||
// 2) Linien (Umrisse + freie Linien).
|
pass.set_index_buffer(ibo.slice(..), wgpu::IndexFormat::Uint32);
|
||||||
if let (Some(vbo), Some(ibo)) = (&scene.line_vbo, &scene.line_ibo) {
|
bound = Some(BatchKind::Fill);
|
||||||
pass.set_pipeline(&self.line_pipeline);
|
}
|
||||||
pass.set_vertex_buffer(0, vbo.slice(..));
|
let b = &scene.geo.fill_batches[idx as usize];
|
||||||
pass.set_index_buffer(ibo.slice(..), wgpu::IndexFormat::Uint32);
|
pass.set_bind_group(0, &u.bind_group, &[block as u32 * stride]);
|
||||||
for b in &scene.geo.line_batches {
|
let start = b.start_index;
|
||||||
pass.set_bind_group(0, &u.bind_group, &[block * stride]);
|
pass.draw_indexed(start..start + b.index_count, 0, 0..1);
|
||||||
let start = b.start_index;
|
}
|
||||||
pass.draw_indexed(start..start + b.index_count, 0, 0..1);
|
BatchKind::Line => {
|
||||||
block += 1;
|
let (Some(vbo), Some(ibo)) = (&scene.line_vbo, &scene.line_ibo)
|
||||||
|
else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if bound != Some(BatchKind::Line) {
|
||||||
|
pass.set_pipeline(&self.line_pipeline);
|
||||||
|
pass.set_vertex_buffer(0, vbo.slice(..));
|
||||||
|
pass.set_index_buffer(ibo.slice(..), wgpu::IndexFormat::Uint32);
|
||||||
|
bound = Some(BatchKind::Line);
|
||||||
|
}
|
||||||
|
let b = &scene.geo.line_batches[idx as usize];
|
||||||
|
pass.set_bind_group(0, &u.bind_group, &[block as u32 * stride]);
|
||||||
|
let start = b.start_index;
|
||||||
|
pass.draw_indexed(start..start + b.index_count, 0, 0..1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,8 +30,8 @@ pub mod web;
|
|||||||
pub use demo::{demo_scene, initial_view_box};
|
pub use demo::{demo_scene, initial_view_box};
|
||||||
pub use ortho::{compute_ortho_matrix, meet_scale, mm_to_device_px, Mat4};
|
pub use ortho::{compute_ortho_matrix, meet_scale, mm_to_device_px, Mat4};
|
||||||
pub use tessellate::{
|
pub use tessellate::{
|
||||||
arc_screen_params, compile_scene, prepare_arc_dash, triangulate, ArcInstanceData, GpuGeometry,
|
arc_screen_params, compile_scene, prepare_arc_dash, triangulate, ArcInstanceData, BatchKind,
|
||||||
MAX_ARC_DASH, PX_PER_M,
|
GpuGeometry, MAX_ARC_DASH, PX_PER_M,
|
||||||
};
|
};
|
||||||
pub use types::{Arc, FillPolygon, Line, Outline, Point, Rgba, Scene, Text, TextAlign, ViewBox};
|
pub use types::{Arc, FillPolygon, Line, Outline, Point, Rgba, Scene, Text, TextAlign, ViewBox};
|
||||||
|
|
||||||
|
|||||||
@@ -218,6 +218,7 @@ fn stroke_dashed_or_solid(
|
|||||||
closed: bool,
|
closed: bool,
|
||||||
color: Rgba,
|
color: Rgba,
|
||||||
stroke_mm: f32,
|
stroke_mm: f32,
|
||||||
|
width_screen: bool,
|
||||||
dash: Option<&[f32]>,
|
dash: Option<&[f32]>,
|
||||||
bounds: &mut Bounds,
|
bounds: &mut Bounds,
|
||||||
) {
|
) {
|
||||||
@@ -225,11 +226,11 @@ fn stroke_dashed_or_solid(
|
|||||||
Some(d) if !d.is_empty() => {
|
Some(d) if !d.is_empty() => {
|
||||||
for piece in split_dash(pts, d) {
|
for piece in split_dash(pts, d) {
|
||||||
if piece.len() >= 2 {
|
if piece.len() >= 2 {
|
||||||
geo.stroke_polyline(&piece, false, color, stroke_mm, bounds);
|
geo.stroke_polyline(&piece, false, color, stroke_mm, width_screen, bounds);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => geo.stroke_polyline(pts, closed, color, stroke_mm, bounds),
|
_ => geo.stroke_polyline(pts, closed, color, stroke_mm, width_screen, bounds),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -351,6 +352,13 @@ pub fn triangulate(pts: &[Point]) -> Vec<u32> {
|
|||||||
tris
|
tris
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Pipeline-Art eines Batches in der Maler-Reihenfolge (`GpuGeometry::draw_sequence`).
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum BatchKind {
|
||||||
|
Fill,
|
||||||
|
Line,
|
||||||
|
}
|
||||||
|
|
||||||
/// Ein zusammenhaengender Zeichen-Batch (Index-Bereich) mit einer Farbe.
|
/// Ein zusammenhaengender Zeichen-Batch (Index-Bereich) mit einer Farbe.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct FillBatch {
|
pub struct FillBatch {
|
||||||
@@ -365,8 +373,12 @@ pub struct LineBatch {
|
|||||||
pub start_index: u32,
|
pub start_index: u32,
|
||||||
pub index_count: u32,
|
pub index_count: u32,
|
||||||
pub color: Rgba,
|
pub color: Rgba,
|
||||||
/// ECHTE Strichbreite in Papier-Millimeter.
|
/// ECHTE Strichbreite in Papier-Millimeter — bzw. in Bildschirm-Einheiten
|
||||||
|
/// (viewBox-px), wenn `width_screen` gesetzt ist (Schraffur-Muster).
|
||||||
pub stroke_mm: f32,
|
pub stroke_mm: f32,
|
||||||
|
/// true: `stroke_mm` traegt Bildschirm-Einheiten (zoom-skalierend); der
|
||||||
|
/// Renderer nutzt dann die meet-Skala statt der mm->px-Umrechnung.
|
||||||
|
pub width_screen: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// GPU-fertige Geometrie: getrennte Puffer fuer Flaechen (Poche) und Striche.
|
/// GPU-fertige Geometrie: getrennte Puffer fuer Flaechen (Poche) und Striche.
|
||||||
@@ -385,6 +397,12 @@ pub struct GpuGeometry {
|
|||||||
/// Analytisch gezeichnete Boegen (je EINE Instanz, kein Segment-Mesh mehr):
|
/// Analytisch gezeichnete Boegen (je EINE Instanz, kein Segment-Mesh mehr):
|
||||||
/// die GPU rendert sie pro Frame exakt rund per SDF-Fragment-Shader.
|
/// die GPU rendert sie pro Frame exakt rund per SDF-Fragment-Shader.
|
||||||
pub arcs: Vec<ArcInstanceData>,
|
pub arcs: Vec<ArcInstanceData>,
|
||||||
|
/// Maler-Reihenfolge ueber BEIDE Batch-Arten hinweg: (Art, Index in
|
||||||
|
/// `fill_batches` bzw. `line_batches`). Der Renderer zeichnet exakt in dieser
|
||||||
|
/// Reihenfolge (Pipeline-Wechsel pro Eintrag) — nur so deckt eine spaetere
|
||||||
|
/// Fuellung (Wand-Poche) einen frueheren Strich (Raum-Umriss) ab, wie im
|
||||||
|
/// SVG-Referenzpfad (Primitive strikt in Array-Reihenfolge).
|
||||||
|
pub draw_sequence: Vec<(BatchKind, u32)>,
|
||||||
/// Modell-Bounds (Meter) fuer Debug/Einpassen; nicht render-kritisch.
|
/// Modell-Bounds (Meter) fuer Debug/Einpassen; nicht render-kritisch.
|
||||||
pub bounds: [f32; 4], // [min_x, min_y, max_x, max_y]
|
pub bounds: [f32; 4], // [min_x, min_y, max_x, max_y]
|
||||||
}
|
}
|
||||||
@@ -396,12 +414,18 @@ fn same_rgba(a: Rgba, b: Rgba) -> bool {
|
|||||||
|
|
||||||
impl GpuGeometry {
|
impl GpuGeometry {
|
||||||
/// Ordnungserhaltendes Batch-Merging fuer Fuellungen: aufeinanderfolgende
|
/// Ordnungserhaltendes Batch-Merging fuer Fuellungen: aufeinanderfolgende
|
||||||
/// Indizes gleicher Farbe werden zu EINEM Draw-Call zusammengefasst.
|
/// Indizes gleicher Farbe werden zu EINEM Draw-Call zusammengefasst. Gemergt
|
||||||
|
/// wird nur, wenn der letzte Eintrag der MALER-Reihenfolge (`draw_sequence`)
|
||||||
|
/// derselbe Fuell-Batch ist — sonst wuerde ein dazwischenliegender Linien-
|
||||||
|
/// Batch uebersprungen und die Z-Reihenfolge verfaelscht.
|
||||||
fn add_fill_batch(&mut self, count: u32, color: Rgba) {
|
fn add_fill_batch(&mut self, count: u32, color: Rgba) {
|
||||||
if let Some(last) = self.fill_batches.last_mut() {
|
let last_idx = self.fill_batches.len().wrapping_sub(1) as u32;
|
||||||
if same_rgba(last.color, color) {
|
if self.draw_sequence.last() == Some(&(BatchKind::Fill, last_idx)) {
|
||||||
last.index_count += count;
|
if let Some(last) = self.fill_batches.last_mut() {
|
||||||
return;
|
if same_rgba(last.color, color) {
|
||||||
|
last.index_count += count;
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.fill_batches.push(FillBatch {
|
self.fill_batches.push(FillBatch {
|
||||||
@@ -409,13 +433,21 @@ impl GpuGeometry {
|
|||||||
index_count: count,
|
index_count: count,
|
||||||
color,
|
color,
|
||||||
});
|
});
|
||||||
|
self.draw_sequence
|
||||||
|
.push((BatchKind::Fill, (self.fill_batches.len() - 1) as u32));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn add_line_batch(&mut self, count: u32, color: Rgba, stroke_mm: f32) {
|
fn add_line_batch(&mut self, count: u32, color: Rgba, stroke_mm: f32, width_screen: bool) {
|
||||||
if let Some(last) = self.line_batches.last_mut() {
|
let last_idx = self.line_batches.len().wrapping_sub(1) as u32;
|
||||||
if last.stroke_mm == stroke_mm && same_rgba(last.color, color) {
|
if self.draw_sequence.last() == Some(&(BatchKind::Line, last_idx)) {
|
||||||
last.index_count += count;
|
if let Some(last) = self.line_batches.last_mut() {
|
||||||
return;
|
if last.stroke_mm == stroke_mm
|
||||||
|
&& last.width_screen == width_screen
|
||||||
|
&& same_rgba(last.color, color)
|
||||||
|
{
|
||||||
|
last.index_count += count;
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.line_batches.push(LineBatch {
|
self.line_batches.push(LineBatch {
|
||||||
@@ -423,7 +455,10 @@ impl GpuGeometry {
|
|||||||
index_count: count,
|
index_count: count,
|
||||||
color,
|
color,
|
||||||
stroke_mm,
|
stroke_mm,
|
||||||
|
width_screen,
|
||||||
});
|
});
|
||||||
|
self.draw_sequence
|
||||||
|
.push((BatchKind::Line, (self.line_batches.len() - 1) as u32));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Zeichnet einen zusammenhaengenden Linienzug (Modell-Meter) als EINEN
|
/// Zeichnet einen zusammenhaengenden Linienzug (Modell-Meter) als EINEN
|
||||||
@@ -439,6 +474,7 @@ impl GpuGeometry {
|
|||||||
closed: bool,
|
closed: bool,
|
||||||
color: Rgba,
|
color: Rgba,
|
||||||
stroke_mm: f32,
|
stroke_mm: f32,
|
||||||
|
width_screen: bool,
|
||||||
bounds: &mut Bounds,
|
bounds: &mut Bounds,
|
||||||
) {
|
) {
|
||||||
// Auf Bildschirm-Raum abbilden + aufeinanderfolgende Duplikate entfernen.
|
// Auf Bildschirm-Raum abbilden + aufeinanderfolgende Duplikate entfernen.
|
||||||
@@ -527,7 +563,7 @@ impl GpuGeometry {
|
|||||||
self.line_idx
|
self.line_idx
|
||||||
.extend_from_slice(&[a, a + 1, b, a + 1, b + 1, b]);
|
.extend_from_slice(&[a, a + 1, b, a + 1, b + 1, b]);
|
||||||
}
|
}
|
||||||
self.add_line_batch((segs * 6) as u32, color, stroke_mm);
|
self.add_line_batch((segs * 6) as u32, color, stroke_mm, width_screen);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -583,32 +619,63 @@ pub fn compile_scene(scene: &Scene) -> GpuGeometry {
|
|||||||
let mut geo = GpuGeometry::default();
|
let mut geo = GpuGeometry::default();
|
||||||
let mut bounds = Bounds::new();
|
let mut bounds = Bounds::new();
|
||||||
|
|
||||||
// 1) Gefuellte Polygone.
|
// Maler-Reihenfolge herstellen: alle Elemente (Fuellungen, Umrisse, offene
|
||||||
for poly in &scene.fills {
|
// Polylinien, freie Linien) stabil nach ihrem `z` sortieren. Bei gleichem z
|
||||||
compile_fill(&mut geo, poly, &mut bounds);
|
// (insb. Alt-Szenen mit z=0 ueberall) gilt die bisherige Kategorien-
|
||||||
|
// Reihenfolge Fuellungen -> Umrisse -> Polylinien -> Linien (`seq`).
|
||||||
|
enum Item<'a> {
|
||||||
|
Fill(&'a crate::types::FillPolygon),
|
||||||
|
Outline(&'a crate::types::Outline),
|
||||||
|
Poly(&'a crate::types::Polyline),
|
||||||
|
Line(&'a Line),
|
||||||
|
}
|
||||||
|
let mut items: Vec<(u32, usize, Item)> = Vec::with_capacity(
|
||||||
|
scene.fills.len() + scene.outlines.len() + scene.polylines.len() + scene.lines.len(),
|
||||||
|
);
|
||||||
|
let mut seq = 0usize;
|
||||||
|
for f in &scene.fills {
|
||||||
|
items.push((f.z, seq, Item::Fill(f)));
|
||||||
|
seq += 1;
|
||||||
}
|
}
|
||||||
// 2) Umrisse (geschlossene Ringe als EIN gehrter Streifen, inkl. Schluss-Kante
|
|
||||||
// — ausser bei Dash, siehe `stroke_dashed_or_solid`).
|
|
||||||
for o in &scene.outlines {
|
for o in &scene.outlines {
|
||||||
if o.width_mm > 0.0 && o.pts.len() >= 2 {
|
items.push((o.z, seq, Item::Outline(o)));
|
||||||
stroke_dashed_or_solid(&mut geo, &o.pts, true, o.color, o.width_mm, o.dash.as_deref(), &mut bounds);
|
seq += 1;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// 3) Offene Polylinien (verbundene Umrisskanten / 2D-Zeichnungszuege) als EIN
|
|
||||||
// gehrter Streifen — die inneren Ecken bekommen so eine Gehrung statt
|
|
||||||
// Stumpfkappen (closed=false).
|
|
||||||
for pl in &scene.polylines {
|
for pl in &scene.polylines {
|
||||||
if pl.width_mm > 0.0 && pl.pts.len() >= 2 {
|
items.push((pl.z, seq, Item::Poly(pl)));
|
||||||
stroke_dashed_or_solid(&mut geo, &pl.pts, false, pl.color, pl.width_mm, pl.dash.as_deref(), &mut bounds);
|
seq += 1;
|
||||||
|
}
|
||||||
|
for l in &scene.lines {
|
||||||
|
items.push((l.z, seq, Item::Line(l)));
|
||||||
|
seq += 1;
|
||||||
|
}
|
||||||
|
items.sort_by_key(|(z, s, _)| (*z, *s));
|
||||||
|
|
||||||
|
for (_, _, item) in &items {
|
||||||
|
match item {
|
||||||
|
// Gefuellte Polygone.
|
||||||
|
Item::Fill(poly) => compile_fill(&mut geo, poly, &mut bounds),
|
||||||
|
// Umrisse (geschlossene Ringe als EIN gehrter Streifen, inkl.
|
||||||
|
// Schluss-Kante — ausser bei Dash, siehe `stroke_dashed_or_solid`).
|
||||||
|
Item::Outline(o) => {
|
||||||
|
if o.width_mm > 0.0 && o.pts.len() >= 2 {
|
||||||
|
stroke_dashed_or_solid(&mut geo, &o.pts, true, o.color, o.width_mm, false, o.dash.as_deref(), &mut bounds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Offene Polylinien (verbundene Umrisskanten / 2D-Zeichnungszuege) als
|
||||||
|
// EIN gehrter Streifen — innere Ecken gehrt statt Stumpfkappen.
|
||||||
|
Item::Poly(pl) => {
|
||||||
|
if pl.width_mm > 0.0 && pl.pts.len() >= 2 {
|
||||||
|
stroke_dashed_or_solid(&mut geo, &pl.pts, false, pl.color, pl.width_mm, pl.width_screen, pl.dash.as_deref(), &mut bounds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Freie Einzel-Linien (Tuerblaetter, Referenzlinien).
|
||||||
|
Item::Line(l) => compile_line(&mut geo, l, &mut bounds),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 4) Freie Einzel-Linien (Tuerblaetter, Referenzlinien).
|
// Kreisboegen: NICHT in Segmente zerlegt — je Bogen eine analytische Instanz
|
||||||
for l in &scene.lines {
|
// (`ArcInstanceData`), die die GPU per SDF-Fragment-Shader exakt rund rendert
|
||||||
compile_line(&mut geo, l, &mut bounds);
|
// (eigene Pipeline, nach der uebrigen Geometrie — Tuerschwenk liegt oben).
|
||||||
}
|
|
||||||
// 5) Kreisboegen: NICHT mehr in Segmente zerlegt — je Bogen eine analytische
|
|
||||||
// Instanz (`ArcInstanceData`), die die GPU per SDF-Fragment-Shader exakt
|
|
||||||
// rund rendert (siehe `gpu::Renderer` Arc-Pipeline).
|
|
||||||
for a in &scene.arcs {
|
for a in &scene.arcs {
|
||||||
compile_arc(&mut geo, a, &mut bounds);
|
compile_arc(&mut geo, a, &mut bounds);
|
||||||
}
|
}
|
||||||
@@ -667,7 +734,7 @@ fn compile_arc(geo: &mut GpuGeometry, a: &Arc, bounds: &mut Bounds) {
|
|||||||
|
|
||||||
fn compile_line(geo: &mut GpuGeometry, l: &Line, bounds: &mut Bounds) {
|
fn compile_line(geo: &mut GpuGeometry, l: &Line, bounds: &mut Bounds) {
|
||||||
let w = if l.width_mm > 0.0 { l.width_mm } else { 0.18 };
|
let w = if l.width_mm > 0.0 { l.width_mm } else { 0.18 };
|
||||||
stroke_dashed_or_solid(geo, &[l.a, l.b], false, l.color, w, l.dash.as_deref(), bounds);
|
stroke_dashed_or_solid(geo, &[l.a, l.b], false, l.color, w, false, l.dash.as_deref(), bounds);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -815,6 +882,7 @@ mod tests {
|
|||||||
color: [0.0, 0.0, 0.0, 1.0],
|
color: [0.0, 0.0, 0.0, 1.0],
|
||||||
width_mm: 0.2,
|
width_mm: 0.2,
|
||||||
dash: None,
|
dash: None,
|
||||||
|
z: 0,
|
||||||
}],
|
}],
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
@@ -824,6 +892,7 @@ mod tests {
|
|||||||
color: [0.0, 0.0, 0.0, 1.0],
|
color: [0.0, 0.0, 0.0, 1.0],
|
||||||
width_mm: 0.2,
|
width_mm: 0.2,
|
||||||
dash: Some(vec![10.0, 10.0]),
|
dash: Some(vec![10.0, 10.0]),
|
||||||
|
z: 0,
|
||||||
}],
|
}],
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -22,6 +22,13 @@ pub struct FillPolygon {
|
|||||||
pub pts: Vec<Point>,
|
pub pts: Vec<Point>,
|
||||||
/// Fuellfarbe (RGBA 0..1).
|
/// Fuellfarbe (RGBA 0..1).
|
||||||
pub color: Rgba,
|
pub color: Rgba,
|
||||||
|
/// Maler-Reihenfolge (kleiner = frueher/unten). Der SVG-Referenzpfad zeichnet
|
||||||
|
/// die Primitive strikt in Array-Reihenfolge (Fuellung/Umriss INTERLEAVED) —
|
||||||
|
/// ein spaeteres Fuellpolygon (Wand-Poche) deckt z.B. den frueheren Raum-
|
||||||
|
/// Umriss ab. `compile_scene` stellt diese Reihenfolge kategorien-uebergreifend
|
||||||
|
/// wieder her. Default 0 (Alt-Szenen: bisherige Kategorien-Reihenfolge).
|
||||||
|
#[serde(default)]
|
||||||
|
pub z: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Ein Liniensegment mit echter Papier-mm-Breite.
|
/// Ein Liniensegment mit echter Papier-mm-Breite.
|
||||||
@@ -39,6 +46,9 @@ pub struct Line {
|
|||||||
/// Strichmuster in Papier-mm; None/leer = durchgezogen (siehe `tessellate::split_dash`).
|
/// Strichmuster in Papier-mm; None/leer = durchgezogen (siehe `tessellate::split_dash`).
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub dash: Option<Vec<f32>>,
|
pub dash: Option<Vec<f32>>,
|
||||||
|
/// Maler-Reihenfolge (kleiner = frueher/unten), siehe `FillPolygon::z`.
|
||||||
|
#[serde(default)]
|
||||||
|
pub z: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Ein geschlossenes Polygon als Umriss (nur Kanten, keine Fuellung) — bequemer
|
/// Ein geschlossenes Polygon als Umriss (nur Kanten, keine Fuellung) — bequemer
|
||||||
@@ -58,6 +68,9 @@ pub struct Outline {
|
|||||||
/// ueber die Dash-Luecke hinweg, siehe `tessellate::split_dash`).
|
/// ueber die Dash-Luecke hinweg, siehe `tessellate::split_dash`).
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub dash: Option<Vec<f32>>,
|
pub dash: Option<Vec<f32>>,
|
||||||
|
/// Maler-Reihenfolge (kleiner = frueher/unten), siehe `FillPolygon::z`.
|
||||||
|
#[serde(default)]
|
||||||
|
pub z: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Ein OFFENER Linienzug mit echter Papier-mm-Breite. Anders als `Outline`
|
/// Ein OFFENER Linienzug mit echter Papier-mm-Breite. Anders als `Outline`
|
||||||
@@ -71,12 +84,23 @@ pub struct Polyline {
|
|||||||
pub pts: Vec<Point>,
|
pub pts: Vec<Point>,
|
||||||
/// Strichfarbe (RGBA 0..1).
|
/// Strichfarbe (RGBA 0..1).
|
||||||
pub color: Rgba,
|
pub color: Rgba,
|
||||||
/// Strichbreite in echten Papier-Millimetern.
|
/// Strichbreite in echten Papier-Millimetern — bzw. in BILDSCHIRM-EinheITEN
|
||||||
|
/// (viewBox-px), wenn `width_screen` gesetzt ist.
|
||||||
#[serde(rename = "widthMm")]
|
#[serde(rename = "widthMm")]
|
||||||
pub width_mm: f32,
|
pub width_mm: f32,
|
||||||
|
/// true: `width_mm` ist eine Breite in BILDSCHIRM-Einheiten (viewBox-px),
|
||||||
|
/// die MIT dem Zoom skaliert — wie die SVG-Schraffur-Muster: deren Strich
|
||||||
|
/// (`hatchStrokePx`) ist an die Musterkachel gekoppelt, nicht an Papier-mm.
|
||||||
|
/// Der Renderer rechnet dann Geraete-px = Breite * meet-Skala (statt
|
||||||
|
/// * mm->px). Default false (echte Papier-mm).
|
||||||
|
#[serde(default, rename = "widthScreen")]
|
||||||
|
pub width_screen: bool,
|
||||||
/// Strichmuster in Papier-mm; None/leer = durchgezogen.
|
/// Strichmuster in Papier-mm; None/leer = durchgezogen.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub dash: Option<Vec<f32>>,
|
pub dash: Option<Vec<f32>>,
|
||||||
|
/// Maler-Reihenfolge (kleiner = frueher/unten), siehe `FillPolygon::z`.
|
||||||
|
#[serde(default)]
|
||||||
|
pub z: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Ein Kreisbogen (kuerzerer Sweep von `from` nach `to` um `center`) in Modell-
|
/// Ein Kreisbogen (kuerzerer Sweep von `from` nach `to` um `center`) in Modell-
|
||||||
|
|||||||
@@ -148,6 +148,14 @@ impl WebPlanRenderer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Papier-Massstab-Nenner (1:N) NUR fuer die Text-Schriftgroesse setzen —
|
||||||
|
/// entkoppelt von `set_paper_scale` (siehe `Renderer::text_scale_n`-Doku).
|
||||||
|
pub fn set_text_scale(&mut self, n: f32) {
|
||||||
|
if n > 0.0 {
|
||||||
|
self.renderer.text_scale_n = n;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Surface an eine neue Pixelgroesse anpassen (DPR beachtet der Aufrufer).
|
/// Surface an eine neue Pixelgroesse anpassen (DPR beachtet der Aufrufer).
|
||||||
pub fn resize(&mut self, width: u32, height: u32) {
|
pub fn resize(&mut self, width: u32, height: u32) {
|
||||||
let (w, h) = (width.max(1), height.max(1));
|
let (w, h) = (width.max(1), height.max(1));
|
||||||
|
|||||||
+15
-3
@@ -475,11 +475,23 @@ export const PlanView = forwardRef<PlanViewHandle, PlanViewProps>(
|
|||||||
const paperScaleForGl = (v: ViewBox): number =>
|
const paperScaleForGl = (v: ViewBox): number =>
|
||||||
paperScaleRef.current ?? scaleFromView(v, svgRef.current) ?? 100;
|
paperScaleRef.current ?? scaleFromView(v, svgRef.current) ?? 100;
|
||||||
|
|
||||||
|
// Referenz-Nenner (1:N) NUR für die Text-Schriftgröße der Engine — spiegelt
|
||||||
|
// EXAKT die SVG-Formel (case "text" unten, gleiche `print`-Bedingung): im
|
||||||
|
// normalen Anzeigemodus fest auf die Referenzskala 1:100 (Raumstempel bleibt
|
||||||
|
// lesbar, unabhängig vom zufälligen Einpass-Massstab der Ansicht), erst im
|
||||||
|
// Druck-Modus der echte gemessene `paperScale`. BEWUSST NICHT `paperScaleForGl`
|
||||||
|
// (das dort verwendete live abgeleitete N wäre für Text willkürlich/unlesbar
|
||||||
|
// und würde vom SVG-Referenzpfad abweichen — siehe Doku in `gpu::Renderer`).
|
||||||
|
const textScaleForGl = (): number => {
|
||||||
|
const print = !hairline && paperScale != null && paperScale > 0;
|
||||||
|
return print ? paperScale! : 100;
|
||||||
|
};
|
||||||
|
|
||||||
// GL-Geometrie beim Plan-Wechsel neu tessellieren + sofort neu zeichnen.
|
// GL-Geometrie beim Plan-Wechsel neu tessellieren + sofort neu zeichnen.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!useGpuRenderer) return;
|
if (!useGpuRenderer) return;
|
||||||
updateGeometry(plan);
|
updateGeometry(plan);
|
||||||
renderGl(viewRef.current, paperScaleForGl(viewRef.current));
|
renderGl(viewRef.current, paperScaleForGl(viewRef.current), textScaleForGl());
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [useGpuRenderer, plan, updateGeometry, renderGl]);
|
}, [useGpuRenderer, plan, updateGeometry, renderGl]);
|
||||||
|
|
||||||
@@ -487,7 +499,7 @@ export const PlanView = forwardRef<PlanViewHandle, PlanViewProps>(
|
|||||||
// keine Re-Tessellierung. Auch bei paperScale-Wechsel neu (Strichbreiten).
|
// keine Re-Tessellierung. Auch bei paperScale-Wechsel neu (Strichbreiten).
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (useGpuRenderer) {
|
if (useGpuRenderer) {
|
||||||
renderGl(view, paperScaleForGl(view));
|
renderGl(view, paperScaleForGl(view), textScaleForGl());
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [useGpuRenderer, renderGl, view, paperScale]);
|
}, [useGpuRenderer, renderGl, view, paperScale]);
|
||||||
@@ -1065,7 +1077,7 @@ export const PlanView = forwardRef<PlanViewHandle, PlanViewProps>(
|
|||||||
if (!v || !v0) return;
|
if (!v || !v0) return;
|
||||||
viewRef.current = v; // abgeleitete Reads (Snap-Toleranz etc.) frisch halten
|
viewRef.current = v; // abgeleitete Reads (Snap-Toleranz etc.) frisch halten
|
||||||
applySvgTransform(v0, v); // SVG per GPU-Composit (kein Re-Raster)
|
applySvgTransform(v0, v); // SVG per GPU-Composit (kein Re-Raster)
|
||||||
renderGl(v, paperScaleForGl(v)); // GL exakt auf V (Strich in echten Papier-mm)
|
renderGl(v, paperScaleForGl(v), textScaleForGl()); // GL exakt auf V (Strich in echten Papier-mm)
|
||||||
};
|
};
|
||||||
/** Pan-/Zoom-Ziel imperativ anwenden (rAF-gedrosselt), ohne React-Render. */
|
/** Pan-/Zoom-Ziel imperativ anwenden (rAF-gedrosselt), ohne React-Render. */
|
||||||
const enqueuePan = (v: ViewBox) => {
|
const enqueuePan = (v: ViewBox) => {
|
||||||
|
|||||||
@@ -240,10 +240,12 @@ export function hatchLineFamily(
|
|||||||
const nrm: Vec2 = { x: -dir.y, y: dir.x }; // Linien-Normale (Scan-Richtung)
|
const nrm: Vec2 = { x: -dir.y, y: dir.x }; // Linien-Normale (Scan-Richtung)
|
||||||
|
|
||||||
// Rechtwinklige Projektion aller Ecken auf die Normale → Abdeckungsbereich.
|
// Rechtwinklige Projektion aller Ecken auf die Normale → Abdeckungsbereich.
|
||||||
const center: Vec2 = {
|
// ANKER = MODELL-URSPRUNG (nicht die Polygon-Mitte): der SVG-`<pattern>` ist
|
||||||
x: (bb.minX + bb.maxX) / 2,
|
// `userSpaceOnUse`, sein Kachelgitter liegt global im viewBox-Raum fest
|
||||||
y: (bb.minY + bb.maxY) / 2,
|
// (Ursprung (0,0) = Modell-Ursprung). Nur mit demselben Anker liegen die
|
||||||
};
|
// Musterlinien deckungsgleich zum SVG-Referenzpfad — und benachbarte Polygone
|
||||||
|
// teilen dieselbe Muster-Phase statt je eigener (bbox-abhängiger) Versätze.
|
||||||
|
const center: Vec2 = { x: 0, y: 0 };
|
||||||
let dMin = Infinity, dMax = -Infinity;
|
let dMin = Infinity, dMax = -Infinity;
|
||||||
for (const p of poly) {
|
for (const p of poly) {
|
||||||
const d = (p.x - center.x) * nrm.x + (p.y - center.y) * nrm.y;
|
const d = (p.x - center.x) * nrm.x + (p.y - center.y) * nrm.y;
|
||||||
@@ -415,24 +417,24 @@ export function buildHatchRuns(poly: Vec2[], hatch: HatchRender): HatchRun[] {
|
|||||||
* endet exakt an den Polygonkanten (even-odd-Clipping, konkav-fähig).
|
* endet exakt an den Polygonkanten (even-odd-Clipping, konkav-fähig).
|
||||||
*/
|
*/
|
||||||
function insulationRuns(poly: Vec2[], hatch: HatchRender, scale: number): HatchRun[] {
|
function insulationRuns(poly: Vec2[], hatch: HatchRender, scale: number): HatchRun[] {
|
||||||
const bb = bbox(poly);
|
|
||||||
const rot = toModelAngleRad(hatch.angle);
|
const rot = toModelAngleRad(hatch.angle);
|
||||||
|
|
||||||
// Muster-Parameter in Metern (aus den SVG-px-Werten der Kachel).
|
// Muster-Parameter in Metern (aus den SVG-px-Werten der Kachel).
|
||||||
const wavelength = 14 * scale * PX_TO_M; // Kachelbreite
|
const wavelength = 14 * scale * PX_TO_M; // Kachelbreite
|
||||||
const rowGap = 10 * scale * PX_TO_M; // Kachelhöhe → Zeilenabstand
|
const rowGap = 10 * scale * PX_TO_M; // Kachelhöhe → Zeilenabstand
|
||||||
const amplitude = 3 * scale * PX_TO_M; // Wellen-Amplitude (weich)
|
|
||||||
|
|
||||||
// Lokales Muster-Koordinatensystem: u entlang der Welle, v quer (Zeilen).
|
// Lokales Muster-Koordinatensystem: u entlang der Welle, v quer (Zeilen).
|
||||||
|
// ANKER = MODELL-URSPRUNG, wie beim SVG-`userSpaceOnUse`-Pattern (siehe
|
||||||
|
// hatchLineFamily): so liegen Wellen-Phase UND Zeilenraster deckungsgleich
|
||||||
|
// zum SVG-Referenzpfad und über alle Polygone hinweg in EINEM Gitter.
|
||||||
const uDir = rotate({ x: 1, y: 0 }, rot);
|
const uDir = rotate({ x: 1, y: 0 }, rot);
|
||||||
const vDir = rotate({ x: 0, y: 1 }, rot);
|
const vDir = rotate({ x: 0, y: 1 }, rot);
|
||||||
|
|
||||||
// Abdeckungsbereich in (u,v) über die Polygon-Ecken bestimmen.
|
// Abdeckungsbereich in (u,v) über die Polygon-Ecken bestimmen.
|
||||||
const center: Vec2 = { x: (bb.minX + bb.maxX) / 2, y: (bb.minY + bb.maxY) / 2 };
|
|
||||||
let uMin = Infinity, uMax = -Infinity, vMin = Infinity, vMax = -Infinity;
|
let uMin = Infinity, uMax = -Infinity, vMin = Infinity, vMax = -Infinity;
|
||||||
for (const p of poly) {
|
for (const p of poly) {
|
||||||
const u = (p.x - center.x) * uDir.x + (p.y - center.y) * uDir.y;
|
const u = p.x * uDir.x + p.y * uDir.y;
|
||||||
const v = (p.x - center.x) * vDir.x + (p.y - center.y) * vDir.y;
|
const v = p.x * vDir.x + p.y * vDir.y;
|
||||||
if (u < uMin) uMin = u;
|
if (u < uMin) uMin = u;
|
||||||
if (u > uMax) uMax = u;
|
if (u > uMax) uMax = u;
|
||||||
if (v < vMin) vMin = v;
|
if (v < vMin) vMin = v;
|
||||||
@@ -447,22 +449,38 @@ function insulationRuns(poly: Vec2[], hatch: HatchRender, scale: number): HatchR
|
|||||||
|
|
||||||
const runs: HatchRun[] = [];
|
const runs: HatchRun[] = [];
|
||||||
const toWorld = (u: number, v: number): Vec2 => ({
|
const toWorld = (u: number, v: number): Vec2 => ({
|
||||||
x: center.x + uDir.x * u + vDir.x * v,
|
x: uDir.x * u + vDir.x * v,
|
||||||
y: center.y + uDir.y * u + vDir.y * v,
|
y: uDir.y * u + vDir.y * v,
|
||||||
});
|
});
|
||||||
|
|
||||||
const firstRow = Math.floor(vMin / rowGap) - 1;
|
// Die Wellen-Mittellinie liegt in der SVG-Kachel bei y = h/2 (my = 5·scale),
|
||||||
const lastRow = Math.ceil(vMax / rowGap) + 1;
|
// die Zeilen also auf HALBEN Rasterlinien: v = (r + 0.5)·rowGap.
|
||||||
|
const firstRow = Math.floor(vMin / rowGap - 0.5) - 1;
|
||||||
|
const lastRow = Math.ceil(vMax / rowGap - 0.5) + 1;
|
||||||
const uStart = Math.floor((uMin - wavelength) / du) * du;
|
const uStart = Math.floor((uMin - wavelength) / du) * du;
|
||||||
const uEnd = uMax + wavelength;
|
const uEnd = uMax + wavelength;
|
||||||
|
|
||||||
|
// EXAKT die SVG-Wellenform: die Kachel zeichnet zwei quadratische Béziers
|
||||||
|
// `M0 5s Q 3.5s -1s 7s 5s Q 10.5s 11s 14s 5s`. Deren x(t) ist LINEAR (7s·t je
|
||||||
|
// Halbwelle), die Auslenkung eine Parabel ±12s·t(1−t) (Scheitel ±3s) — also
|
||||||
|
// Halbwellen-Parabeln statt einer Sinuskurve (flachere Kämme, steilere
|
||||||
|
// Nulldurchgänge). Erste Halbwelle: Kamm nach OBEN (Bildschirm) = +v (Modell).
|
||||||
|
const half = wavelength / 2;
|
||||||
|
const waveAt = (u: number): number => {
|
||||||
|
const phase = ((u % wavelength) + wavelength) % wavelength;
|
||||||
|
const t = (phase % half) / half;
|
||||||
|
const mag = 12 * scale * PX_TO_M * t * (1 - t);
|
||||||
|
return phase < half ? mag : -mag;
|
||||||
|
};
|
||||||
|
|
||||||
for (let r = firstRow; r <= lastRow; r++) {
|
for (let r = firstRow; r <= lastRow; r++) {
|
||||||
const vBase = r * rowGap;
|
const vBase = (r + 0.5) * rowGap;
|
||||||
// Ganze Welle als EINE Polylinie tessellieren, dann DURCHGEHEND clippen.
|
// Ganze Welle als EINE Polylinie tessellieren, dann DURCHGEHEND clippen.
|
||||||
|
// Das u-Raster ist an Vielfachen von du = λ/22 ausgerichtet → die Null-
|
||||||
|
// durchgänge (Vielfache von λ/2 = 11·du) liegen exakt auf Stützpunkten.
|
||||||
const wavePts: Vec2[] = [];
|
const wavePts: Vec2[] = [];
|
||||||
for (let u = uStart; u <= uEnd + 1e-9; u += du) {
|
for (let u = uStart; u <= uEnd + 1e-9; u += du) {
|
||||||
const wave = amplitude * Math.sin((2 * Math.PI * u) / wavelength);
|
wavePts.push(toWorld(u, vBase + waveAt(u)));
|
||||||
wavePts.push(toWorld(u, vBase + wave));
|
|
||||||
}
|
}
|
||||||
for (const run of clipPolylineToPolygon(wavePts, poly)) runs.push(run);
|
for (const run of clipPolylineToPolygon(wavePts, poly)) runs.push(run);
|
||||||
}
|
}
|
||||||
|
|||||||
+107
-20
@@ -31,18 +31,36 @@ export type RRgba = [number, number, number, number];
|
|||||||
export interface RFill {
|
export interface RFill {
|
||||||
pts: RPoint[];
|
pts: RPoint[];
|
||||||
color: RRgba;
|
color: RRgba;
|
||||||
|
/**
|
||||||
|
* Maler-Reihenfolge (kleiner = früher/unten). Der SVG-Pfad zeichnet die
|
||||||
|
* Primitive strikt in Array-Reihenfolge (Füllung/Schraffur/Umriss
|
||||||
|
* interleaved) — eine spätere Wand-Poché deckt z. B. den früheren
|
||||||
|
* Raum-Umriss ab. Der native Renderer (`compile_scene`) stellt diese
|
||||||
|
* Reihenfolge über die getrennten Szenen-Arrays hinweg wieder her.
|
||||||
|
*/
|
||||||
|
z: number;
|
||||||
}
|
}
|
||||||
export interface ROutline {
|
export interface ROutline {
|
||||||
pts: RPoint[];
|
pts: RPoint[];
|
||||||
color: RRgba;
|
color: RRgba;
|
||||||
widthMm: number;
|
widthMm: number;
|
||||||
dash?: number[] | null;
|
dash?: number[] | null;
|
||||||
|
/** Maler-Reihenfolge, siehe {@link RFill.z}. */
|
||||||
|
z: number;
|
||||||
}
|
}
|
||||||
export interface RPolyline {
|
export interface RPolyline {
|
||||||
pts: RPoint[];
|
pts: RPoint[];
|
||||||
color: RRgba;
|
color: RRgba;
|
||||||
widthMm: number;
|
widthMm: number;
|
||||||
dash?: number[] | null;
|
dash?: number[] | null;
|
||||||
|
/** Maler-Reihenfolge, siehe {@link RFill.z}. */
|
||||||
|
z: number;
|
||||||
|
/**
|
||||||
|
* true: `widthMm` trägt BILDSCHIRM-Einheiten (viewBox-px, zoom-skalierend)
|
||||||
|
* statt Papier-mm — für Schraffur-Striche, deren Breite im SVG an die
|
||||||
|
* Musterkachel gekoppelt ist (`hatchStrokePx`), nicht an Papier-mm.
|
||||||
|
*/
|
||||||
|
widthScreen?: boolean;
|
||||||
}
|
}
|
||||||
export interface RArc {
|
export interface RArc {
|
||||||
center: RPoint;
|
center: RPoint;
|
||||||
@@ -59,6 +77,8 @@ export interface RLine {
|
|||||||
color: RRgba;
|
color: RRgba;
|
||||||
widthMm: number;
|
widthMm: number;
|
||||||
dash?: number[] | null;
|
dash?: number[] | null;
|
||||||
|
/** Maler-Reihenfolge, siehe {@link RFill.z}. */
|
||||||
|
z: number;
|
||||||
}
|
}
|
||||||
export type RTextAlign = "left" | "center" | "right";
|
export type RTextAlign = "left" | "center" | "right";
|
||||||
/** EINE Textzeile; serde-kompatibel zu render2d::types::Text. */
|
/** EINE Textzeile; serde-kompatibel zu render2d::types::Text. */
|
||||||
@@ -82,6 +102,41 @@ export interface RScene {
|
|||||||
|
|
||||||
const DEFAULT_LINE = "#1a1a1a";
|
const DEFAULT_LINE = "#1a1a1a";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Strichfarben/-deckkraft je CSS-Klasse — Spiegel der `.plan-svg .<cls>`-Regeln
|
||||||
|
* in styles.css: Im SVG-Pfad ÜBERSTIMMT eine CSS-`stroke`-Property das inline
|
||||||
|
* gesetzte `stroke`-Attribut (Präsentationsattribut), d. h. für diese Klassen
|
||||||
|
* gilt IMMER die CSS-Farbe. Klassen ohne CSS-`stroke` (draw2d, stair-*,
|
||||||
|
* context-line) behalten die Modell-Farbe; context-line trägt zusätzlich
|
||||||
|
* `opacity: 0.7`, wall-axis `opacity: 0.85`.
|
||||||
|
*/
|
||||||
|
const CLS_STROKE: Record<string, string> = {
|
||||||
|
"door-leaf": "#2b3039",
|
||||||
|
"door-swing": "#6b7280",
|
||||||
|
"door-frame": "#2b3039",
|
||||||
|
"window-glass": "#4a86c7",
|
||||||
|
"wall-axis": "#2f5d54", // var(--accent) des hellen Standard-Themes
|
||||||
|
};
|
||||||
|
const CLS_OPACITY: Record<string, number> = {
|
||||||
|
"wall-axis": 0.85,
|
||||||
|
"context-line": 0.7,
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Deckkraft gedimmter (greyed) Primitive — wie GREYED_OPACITY in PlanView. */
|
||||||
|
const GREYED_ALPHA = 0.3;
|
||||||
|
|
||||||
|
/** Farbe mit Klassen-Deckkraft und Greyed-Dimmung multiplizieren. */
|
||||||
|
function withOpacity(col: RRgba, cls: string | undefined, greyed: boolean | undefined): RRgba {
|
||||||
|
const o = (cls ? CLS_OPACITY[cls] ?? 1 : 1) * (greyed ? GREYED_ALPHA : 1);
|
||||||
|
return o === 1 ? col : [col[0], col[1], col[2], col[3] * o];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Strichfarbe einer line/arc-Primitive: CSS-Klassen-Farbe > Modell-Farbe > Tinte. */
|
||||||
|
function strokeColorFor(cls: string | undefined, color: string | undefined): RRgba {
|
||||||
|
const css = cls ? CLS_STROKE[cls] : undefined;
|
||||||
|
return toRgba(css ?? color ?? DEFAULT_LINE, 1) ?? [0.1, 0.1, 0.1, 1];
|
||||||
|
}
|
||||||
|
|
||||||
/** 1 Punkt (pt) in Papier-Millimetern. */
|
/** 1 Punkt (pt) in Papier-Millimetern. */
|
||||||
const PT_TO_MM = 25.4 / 72;
|
const PT_TO_MM = 25.4 / 72;
|
||||||
/**
|
/**
|
||||||
@@ -241,29 +296,36 @@ export function planToRenderScene(plan: Plan): RScene {
|
|||||||
// Sichtbare Umriss-Läufe werden erst GESAMMELT (nach Stil gruppiert) und am
|
// Sichtbare Umriss-Läufe werden erst GESAMMELT (nach Stil gruppiert) und am
|
||||||
// Ende über Polygon-Grenzen hinweg zusammengenäht — erst dadurch bekommen
|
// Ende über Polygon-Grenzen hinweg zusammengenäht — erst dadurch bekommen
|
||||||
// Wand-zu-Wand-Ecken (zwei Polygone!) eine Gehrung statt einer Kerbe.
|
// Wand-zu-Wand-Ecken (zwei Polygone!) eine Gehrung statt einer Kerbe.
|
||||||
const edgeRunGroups = new Map<string, { color: RRgba; widthMm: number; runs: RPoint[][] }>();
|
const edgeRunGroups = new Map<string, { color: RRgba; widthMm: number; runs: RPoint[][]; z: number }>();
|
||||||
const collectEdgeRun = (run: RPoint[], color: RRgba, widthMm: number) => {
|
const collectEdgeRun = (run: RPoint[], color: RRgba, widthMm: number, z: number) => {
|
||||||
const k = `${color.join(",")}|${widthMm}`;
|
const k = `${color.join(",")}|${widthMm}`;
|
||||||
let g = edgeRunGroups.get(k);
|
let g = edgeRunGroups.get(k);
|
||||||
if (!g) {
|
if (!g) {
|
||||||
g = { color, widthMm, runs: [] };
|
g = { color, widthMm, runs: [], z };
|
||||||
edgeRunGroups.set(k, g);
|
edgeRunGroups.set(k, g);
|
||||||
}
|
}
|
||||||
g.runs.push(run);
|
g.runs.push(run);
|
||||||
|
// Die Kette zeichnet, wenn ihr SPÄTESTER Beitrag im SVG zeichnen würde.
|
||||||
|
g.z = Math.max(g.z, z);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Maler-Reihenfolge: läuft in EXAKT der Reihenfolge mit, in der der SVG-Pfad
|
||||||
|
// die Elemente zeichnet (Primitive in Array-Reihenfolge; je Polygon Füllung →
|
||||||
|
// Schraffur → Umriss). Jedes emittierte Szenen-Element bekommt das nächste z.
|
||||||
|
let zc = 0;
|
||||||
|
|
||||||
// Laufender, verketteter 2D-Zeichnungs-Zug (gleiche drawingId + Stil, End-an-Start).
|
// Laufender, verketteter 2D-Zeichnungs-Zug (gleiche drawingId + Stil, End-an-Start).
|
||||||
let curPts: RPoint[] | null = null;
|
let curPts: RPoint[] | null = null;
|
||||||
let curSrc: LinePrim | null = null;
|
let curSrc: LinePrim | null = null;
|
||||||
const flushRun = () => {
|
const flushRun = () => {
|
||||||
if (curPts && curSrc && curPts.length >= 2) {
|
if (curPts && curSrc && curPts.length >= 2) {
|
||||||
const col = toRgba(curSrc.color ?? DEFAULT_LINE, 1) ?? [0.1, 0.1, 0.1, 1];
|
const col = withOpacity(strokeColorFor(curSrc.cls, curSrc.color), curSrc.cls, curSrc.greyed);
|
||||||
const closed = curPts.length > 2 && samePt(curPts[0], curPts[curPts.length - 1]);
|
const closed = curPts.length > 2 && samePt(curPts[0], curPts[curPts.length - 1]);
|
||||||
const dash = curSrc.dash ?? null;
|
const dash = curSrc.dash ?? null;
|
||||||
if (closed) {
|
if (closed) {
|
||||||
outlines.push({ pts: curPts.slice(0, -1), color: col, widthMm: curSrc.weightMm, dash });
|
outlines.push({ pts: curPts.slice(0, -1), color: col, widthMm: curSrc.weightMm, dash, z: zc++ });
|
||||||
} else {
|
} else {
|
||||||
polylines.push({ pts: curPts, color: col, widthMm: curSrc.weightMm, dash });
|
polylines.push({ pts: curPts, color: col, widthMm: curSrc.weightMm, dash, z: zc++ });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
curPts = null;
|
curPts = null;
|
||||||
@@ -278,33 +340,53 @@ export function planToRenderScene(plan: Plan): RScene {
|
|||||||
|
|
||||||
// Füllung
|
// Füllung
|
||||||
const fillCol = toRgba(p.fill, 1);
|
const fillCol = toRgba(p.fill, 1);
|
||||||
if (fillCol && pts.length >= 3) fills.push({ pts, color: fillCol });
|
if (fillCol && pts.length >= 3) {
|
||||||
|
fills.push({ pts, color: withOpacity(fillCol, undefined, p.greyed), z: zc++ });
|
||||||
|
}
|
||||||
|
|
||||||
// Schraffur: die aufs Polygon geclippten Musterlinien wie im Browser
|
// Schraffur: die aufs Polygon geclippten Musterlinien wie im Browser
|
||||||
// (glPlanHatch — identische Geometrie), als Polylinien. "none"/"solid"
|
// (glPlanHatch — identische Geometrie), als Polylinien. "none"/"solid"
|
||||||
// brauchen keine Linien (solid deckt die Füllung farbig ab).
|
// brauchen keine Linien (solid deckt die Füllung farbig ab).
|
||||||
if (p.hatch && p.hatch.pattern !== "none" && p.hatch.pattern !== "solid") {
|
if (p.hatch && p.hatch.pattern !== "none" && p.hatch.pattern !== "solid") {
|
||||||
const hatchCol = toRgba(p.hatch.color, 1) ?? [0.1, 0.1, 0.1, 1];
|
const hatchCol = withOpacity(
|
||||||
|
toRgba(p.hatch.color, 1) ?? [0.1, 0.1, 0.1, 1],
|
||||||
|
undefined,
|
||||||
|
p.greyed,
|
||||||
|
);
|
||||||
|
// Strichbreite wie das SVG-Muster (`hatchStrokePx`): in viewBox-px, an
|
||||||
|
// die Kachel gekoppelt und mit dem Zoom skalierend — NICHT Papier-mm
|
||||||
|
// (sonst wird die Schraffur bei kleinen Masstäben zur Haarlinie).
|
||||||
const hatchMm = p.hatch.lineWeight > 0 ? p.hatch.lineWeight : 0.13;
|
const hatchMm = p.hatch.lineWeight > 0 ? p.hatch.lineWeight : 0.13;
|
||||||
|
const hatchPx = Math.max(0.6, hatchMm * (1 / 0.13));
|
||||||
const runs = applyDashRuns(buildHatchRuns(p.pts, p.hatch), p.hatch.dash);
|
const runs = applyDashRuns(buildHatchRuns(p.pts, p.hatch), p.hatch.dash);
|
||||||
|
// Alle Musterläufe eines Polygons teilen EIN z (überlappen einander nicht).
|
||||||
|
const hatchZ = zc++;
|
||||||
for (const run of runs) {
|
for (const run of runs) {
|
||||||
if (run.length >= 2) {
|
if (run.length >= 2) {
|
||||||
polylines.push({ pts: run.map((v) => [v.x, v.y]), color: hatchCol, widthMm: hatchMm });
|
polylines.push({
|
||||||
|
pts: run.map((v) => [v.x, v.y]),
|
||||||
|
color: hatchCol,
|
||||||
|
widthMm: hatchPx,
|
||||||
|
widthScreen: true,
|
||||||
|
z: hatchZ,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Umriss
|
// Umriss
|
||||||
const strokeCol = toRgba(p.stroke, 1);
|
const strokeColRaw = toRgba(p.stroke, 1);
|
||||||
if (strokeCol && p.strokeWidthMm > 0) {
|
if (strokeColRaw && p.strokeWidthMm > 0) {
|
||||||
|
const strokeCol = withOpacity(strokeColRaw, undefined, p.greyed);
|
||||||
const suppressed = p.noStrokeEdges ?? [];
|
const suppressed = p.noStrokeEdges ?? [];
|
||||||
if (suppressed.length === 0) {
|
if (suppressed.length === 0) {
|
||||||
// Ganzer Ring → geschlossener, gehrter Umriss.
|
// Ganzer Ring → geschlossener, gehrter Umriss.
|
||||||
outlines.push({ pts, color: strokeCol, widthMm: p.strokeWidthMm });
|
outlines.push({ pts, color: strokeCol, widthMm: p.strokeWidthMm, z: zc++ });
|
||||||
} else {
|
} else {
|
||||||
// Nur die sichtbaren Kanten — sammeln, die Naht ans Ende verschoben.
|
// Nur die sichtbaren Kanten — sammeln, die Naht ans Ende verschoben.
|
||||||
|
const runZ = zc++;
|
||||||
for (const run of visibleEdgeRuns(pts, suppressed)) {
|
for (const run of visibleEdgeRuns(pts, suppressed)) {
|
||||||
collectEdgeRun(run, strokeCol, p.strokeWidthMm);
|
collectEdgeRun(run, strokeCol, p.strokeWidthMm, runZ);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -323,12 +405,13 @@ export function planToRenderScene(plan: Plan): RScene {
|
|||||||
} else {
|
} else {
|
||||||
// Genuin einzelnes Segment (Türblatt, Referenzlinie, Symbol) → Stumpfkappen ok.
|
// Genuin einzelnes Segment (Türblatt, Referenzlinie, Symbol) → Stumpfkappen ok.
|
||||||
flushRun();
|
flushRun();
|
||||||
const col = toRgba(p.color ?? DEFAULT_LINE, 1) ?? [0.1, 0.1, 0.1, 1];
|
const col = withOpacity(strokeColorFor(p.cls, p.color), p.cls, p.greyed);
|
||||||
lines.push({ a: [p.a.x, p.a.y], b: [p.b.x, p.b.y], color: col, widthMm: p.weightMm, dash: p.dash ?? null });
|
lines.push({ a: [p.a.x, p.a.y], b: [p.b.x, p.b.y], color: col, widthMm: p.weightMm, dash: p.dash ?? null, z: zc++ });
|
||||||
}
|
}
|
||||||
} else if (p.kind === "arc") {
|
} else if (p.kind === "arc") {
|
||||||
flushRun();
|
flushRun();
|
||||||
const col = toRgba(DEFAULT_LINE, 1) ?? [0.1, 0.1, 0.1, 1];
|
// Farbe wie im SVG: CSS-Klassen-Regel (z. B. .door-swing → #6b7280).
|
||||||
|
const col = withOpacity(strokeColorFor(p.cls, undefined), p.cls, p.greyed);
|
||||||
// Bogen unvortessellliert an Rust übergeben (`RArc`) — die exakte runde
|
// Bogen unvortessellliert an Rust übergeben (`RArc`) — die exakte runde
|
||||||
// Darstellung (analytischer SDF-Shader, `ARC_WGSL`) und ein evtl. Strich-
|
// Darstellung (analytischer SDF-Shader, `ARC_WGSL`) und ein evtl. Strich-
|
||||||
// muster übernimmt der native Renderer (`compile_scene`) drüben.
|
// muster übernimmt der native Renderer (`compile_scene`) drüben.
|
||||||
@@ -348,7 +431,7 @@ export function planToRenderScene(plan: Plan): RScene {
|
|||||||
// "text"): Rich-Text-Zeilen + Live-Zusatzzeilen, Block vertikal um den
|
// "text"): Rich-Text-Zeilen + Live-Zusatzzeilen, Block vertikal um den
|
||||||
// Anker zentriert, Zeilenvorschub 1.3·Basisgröße.
|
// Anker zentriert, Zeilenvorschub 1.3·Basisgröße.
|
||||||
flushRun();
|
flushRun();
|
||||||
const defCol = toRgba(p.color, 1) ?? [0.1, 0.1, 0.1, 1];
|
const defCol = withOpacity(toRgba(p.color, 1) ?? [0.1, 0.1, 0.1, 1], undefined, p.greyed);
|
||||||
// unitPerPt = PT_TO_MM → tspan.fontSize ist direkt die Papier-mm-Größe.
|
// unitPerPt = PT_TO_MM → tspan.fontSize ist direkt die Papier-mm-Größe.
|
||||||
const docLines = docToLines(p.doc, {
|
const docLines = docToLines(p.doc, {
|
||||||
x: 0,
|
x: 0,
|
||||||
@@ -368,7 +451,11 @@ export function planToRenderScene(plan: Plan): RScene {
|
|||||||
sizeMm: line.tspans.length
|
sizeMm: line.tspans.length
|
||||||
? Math.max(...line.tspans.map((t) => t.fontSize))
|
? Math.max(...line.tspans.map((t) => t.fontSize))
|
||||||
: baseMm,
|
: baseMm,
|
||||||
color: toRgba(line.tspans[0]?.fill ?? p.color, 1) ?? defCol,
|
color: withOpacity(
|
||||||
|
toRgba(line.tspans[0]?.fill ?? p.color, 1) ?? defCol,
|
||||||
|
undefined,
|
||||||
|
p.greyed,
|
||||||
|
),
|
||||||
align: line.align,
|
align: line.align,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -402,9 +489,9 @@ export function planToRenderScene(plan: Plan): RScene {
|
|||||||
for (const chain of stitchRuns(g.runs)) {
|
for (const chain of stitchRuns(g.runs)) {
|
||||||
if (chain.pts.length < 2) continue;
|
if (chain.pts.length < 2) continue;
|
||||||
if (chain.closed) {
|
if (chain.closed) {
|
||||||
outlines.push({ pts: chain.pts, color: g.color, widthMm: g.widthMm });
|
outlines.push({ pts: chain.pts, color: g.color, widthMm: g.widthMm, z: g.z });
|
||||||
} else {
|
} else {
|
||||||
polylines.push({ pts: chain.pts, color: g.color, widthMm: g.widthMm });
|
polylines.push({ pts: chain.pts, color: g.color, widthMm: g.widthMm, z: g.z });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,8 @@
|
|||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import type { Plan } from "./generatePlan";
|
import type { Plan } from "./generatePlan";
|
||||||
import { planToRenderScene } from "./toRenderScene";
|
import { planToRenderScene } from "./toRenderScene";
|
||||||
|
// wgpu-22-Limit-Shim (maxInterStageShaderComponents), geteilt mit useWasm3dRenderer.
|
||||||
|
import { patchRequestDeviceLimits } from "../engine/requestDeviceShim";
|
||||||
|
|
||||||
interface ViewBox {
|
interface ViewBox {
|
||||||
x: number;
|
x: number;
|
||||||
@@ -23,39 +25,12 @@ interface ViewBox {
|
|||||||
h: number;
|
h: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Kompatibilitäts-Shim: wgpu 22 sendet in `requestDevice` noch das Limit
|
|
||||||
* `maxInterStageShaderComponents`, das AUS DER WebGPU-Spec ENTFERNT wurde. Neuere
|
|
||||||
* Chromium-Builds lehnen ein nicht-`undefined`-Wert dafür mit `OperationError`
|
|
||||||
* ab (→ Device-Erzeugung schlägt fehl). Wir entfernen den Schlüssel EINMAL global
|
|
||||||
* aus dem Descriptor, bevor wgpu ihn stellt. Entfällt, sobald render2d auf ein
|
|
||||||
* wgpu (+ glyphon) hochgezogen wird, das dieses Limit nicht mehr mitschickt.
|
|
||||||
*/
|
|
||||||
let requestDevicePatched = false;
|
|
||||||
function patchRequestDeviceLimits(): void {
|
|
||||||
if (requestDevicePatched) return;
|
|
||||||
const AdapterCtor = (globalThis as any).GPUAdapter;
|
|
||||||
if (!AdapterCtor?.prototype?.requestDevice) return;
|
|
||||||
const orig = AdapterCtor.prototype.requestDevice;
|
|
||||||
AdapterCtor.prototype.requestDevice = function (
|
|
||||||
this: unknown,
|
|
||||||
desc?: any,
|
|
||||||
) {
|
|
||||||
if (desc?.requiredLimits && "maxInterStageShaderComponents" in desc.requiredLimits) {
|
|
||||||
const requiredLimits = { ...desc.requiredLimits };
|
|
||||||
delete requiredLimits.maxInterStageShaderComponents;
|
|
||||||
desc = { ...desc, requiredLimits };
|
|
||||||
}
|
|
||||||
return orig.call(this, desc);
|
|
||||||
};
|
|
||||||
requestDevicePatched = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Minimal-Typ des WASM-Exports (das echte .d.ts entsteht erst mit `build:engine`). */
|
/** Minimal-Typ des WASM-Exports (das echte .d.ts entsteht erst mit `build:engine`). */
|
||||||
interface WebPlanRendererLike {
|
interface WebPlanRendererLike {
|
||||||
set_scene(json: string): void;
|
set_scene(json: string): void;
|
||||||
set_view_box(x: number, y: number, w: number, h: number): void;
|
set_view_box(x: number, y: number, w: number, h: number): void;
|
||||||
set_paper_scale(n: number): void;
|
set_paper_scale(n: number): void;
|
||||||
|
set_text_scale(n: number): void;
|
||||||
resize(w: number, h: number): void;
|
resize(w: number, h: number): void;
|
||||||
render(): void;
|
render(): void;
|
||||||
}
|
}
|
||||||
@@ -89,7 +64,7 @@ function getRenderer(canvas: HTMLCanvasElement): Promise<WebPlanRendererLike> {
|
|||||||
let p = rendererCache.get(canvas);
|
let p = rendererCache.get(canvas);
|
||||||
if (!p) {
|
if (!p) {
|
||||||
p = (async () => {
|
p = (async () => {
|
||||||
patchRequestDeviceLimits(); // wgpu-22-Limit-Shim (siehe oben)
|
patchRequestDeviceLimits(); // wgpu-22-Limit-Shim (src/engine/requestDeviceShim)
|
||||||
const mod = await loadEngine();
|
const mod = await loadEngine();
|
||||||
// Canvas VOR dem Surface-Erzeugen auf die Zielpixel (DPR) bringen.
|
// Canvas VOR dem Surface-Erzeugen auf die Zielpixel (DPR) bringen.
|
||||||
const rect = canvas.getBoundingClientRect();
|
const rect = canvas.getBoundingClientRect();
|
||||||
@@ -149,9 +124,15 @@ export function useWasmPlanRenderer(
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
/** Einen Frame zeichnen (Canvas-Größe + dpr hier abgleichen). */
|
/**
|
||||||
|
* Einen Frame zeichnen (Canvas-Größe + dpr hier abgleichen). `textScaleN`
|
||||||
|
* bewusst getrennt von `paperScaleN`: er treibt NUR die Schriftgröße der
|
||||||
|
* Raumstempel (spiegelt die SVG-Formel — feste Referenzskala 1:100 im
|
||||||
|
* Anzeigemodus, siehe PlanView.tsx `textScaleForGl`), während `paperScaleN`
|
||||||
|
* weiter die echten Papier-mm-Strichbreiten der Geometrie treibt.
|
||||||
|
*/
|
||||||
const render = useCallback(
|
const render = useCallback(
|
||||||
(viewBox: ViewBox, paperScaleN = 100) => {
|
(viewBox: ViewBox, paperScaleN = 100, textScaleN = 100) => {
|
||||||
const r = rendererRef.current;
|
const r = rendererRef.current;
|
||||||
const canvas = canvasRef.current;
|
const canvas = canvasRef.current;
|
||||||
if (!r || !canvas) return;
|
if (!r || !canvas) return;
|
||||||
@@ -166,6 +147,7 @@ export function useWasmPlanRenderer(
|
|||||||
try {
|
try {
|
||||||
r.resize(w, h);
|
r.resize(w, h);
|
||||||
r.set_paper_scale(paperScaleN);
|
r.set_paper_scale(paperScaleN);
|
||||||
|
r.set_text_scale(textScaleN);
|
||||||
r.set_view_box(viewBox.x, viewBox.y, viewBox.w, viewBox.h);
|
r.set_view_box(viewBox.x, viewBox.y, viewBox.w, viewBox.h);
|
||||||
r.render();
|
r.render();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
Reference in New Issue
Block a user