feat: Stifte, Seiten löschen, lückenlose Seiten, sauberer Textmarker (Issues #11, #12, #14, #15, #17, #19)
- #15: Koordinatensystem ohne Kerbe am Ursprung (Achsen beginnen eine halbe Linienbreite hinter dem Ursprung; im PDF ein Linienzug mit spitzer Ecke) - #17: Jeder Strich deckt ein Pixel nur einmal (Stencil-Puffer), der Textmarker wird innerhalb eines Strichs nicht dunkler. Im PDF-Export ist der Textmarker jetzt halbtransparent statt deckend. - #11: ➕ in der Werkzeugleiste fügt nach der aktuellen Seite eine Seite ein - #12: Seite löschen im ☰-Menü mit Rückfrage, in der Sitzung wiederherstellbar - #14: Seiten liegen lückenlos untereinander, eine dunkle Linie markiert die Grenze - #19: Vier Stifte mit eigener Farbe und Stärke, die sich Änderungen merken (ui.json); Blätterpfeile nur noch in der Einzelseitenansicht Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
904b36e5f7
commit
3890b6f182
14 files changed
+844
-80
No files matched your search
+135
-4
@@ -166,6 +166,10 @@ pub struct CanvasState {
|
||||
/// Multisampling der Zeichenfläche (1 = aus) und das zugehörige Zwischenbild
|
||||
msaa_samples: u32,
|
||||
msaa_view: Option<wgpu::TextureView>,
|
||||
/// Stencil-Puffer: Jeder Strich deckt ein Pixel nur einmal, damit sich halbtransparente
|
||||
/// Tinte (Textmarker) dort, wo der Strich sich selbst überlappt, nicht verdunkelt
|
||||
stencil_view: wgpu::TextureView,
|
||||
stencil_reset_pipeline: wgpu::RenderPipeline,
|
||||
/// Meldung „keine Darstellungsmodi“ wurde bereits ausgegeben
|
||||
presentation_warned: bool,
|
||||
pub device: wgpu::Device,
|
||||
@@ -343,6 +347,7 @@ impl CanvasState {
|
||||
1
|
||||
};
|
||||
let msaa_view = create_msaa_view(&device, &config, msaa_samples);
|
||||
let stencil_view = create_stencil_view(&device, &config, msaa_samples);
|
||||
|
||||
// 5. Shader & Bind Group Layout
|
||||
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||
@@ -423,7 +428,7 @@ impl CanvasState {
|
||||
polygon_mode: wgpu::PolygonMode::Fill,
|
||||
conservative: false,
|
||||
},
|
||||
depth_stencil: None,
|
||||
depth_stencil: Some(stroke_stencil()),
|
||||
multisample: wgpu::MultisampleState {
|
||||
count: msaa_samples,
|
||||
..Default::default()
|
||||
@@ -431,6 +436,34 @@ impl CanvasState {
|
||||
multiview: None,
|
||||
});
|
||||
|
||||
// Setzt den Stencil-Puffer zurück, wenn die Strichnummern überlaufen
|
||||
let stencil_reset_pipeline =
|
||||
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
label: Some("Stencil Reset Pipeline"),
|
||||
layout: Some(&pipeline_layout),
|
||||
vertex: wgpu::VertexState {
|
||||
module: &shader,
|
||||
entry_point: "vs_fullscreen",
|
||||
buffers: &[],
|
||||
},
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: &shader,
|
||||
entry_point: "fs_none",
|
||||
targets: &[Some(wgpu::ColorTargetState {
|
||||
format: config.format,
|
||||
blend: None,
|
||||
write_mask: wgpu::ColorWrites::empty(),
|
||||
})],
|
||||
}),
|
||||
primitive: wgpu::PrimitiveState::default(),
|
||||
depth_stencil: Some(stencil_reset()),
|
||||
multisample: wgpu::MultisampleState {
|
||||
count: msaa_samples,
|
||||
..Default::default()
|
||||
},
|
||||
multiview: None,
|
||||
});
|
||||
|
||||
// TriangleList-Pipeline (für Hintergrundpapier und Muster)
|
||||
let triangle_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
label: Some("Background TriangleList Pipeline"),
|
||||
@@ -458,7 +491,7 @@ impl CanvasState {
|
||||
polygon_mode: wgpu::PolygonMode::Fill,
|
||||
conservative: false,
|
||||
},
|
||||
depth_stencil: None,
|
||||
depth_stencil: Some(passive_stencil()),
|
||||
multisample: wgpu::MultisampleState {
|
||||
count: msaa_samples,
|
||||
..Default::default()
|
||||
@@ -510,7 +543,7 @@ impl CanvasState {
|
||||
})],
|
||||
}),
|
||||
primitive: wgpu::PrimitiveState::default(),
|
||||
depth_stencil: None,
|
||||
depth_stencil: Some(passive_stencil()),
|
||||
multisample: wgpu::MultisampleState {
|
||||
count: msaa_samples,
|
||||
..Default::default()
|
||||
@@ -549,6 +582,8 @@ impl CanvasState {
|
||||
adapter,
|
||||
msaa_samples,
|
||||
msaa_view,
|
||||
stencil_view,
|
||||
stencil_reset_pipeline,
|
||||
presentation_warned: false,
|
||||
device,
|
||||
queue,
|
||||
@@ -698,6 +733,7 @@ impl CanvasState {
|
||||
}
|
||||
self.surface.configure(&self.device, &self.config);
|
||||
self.msaa_view = create_msaa_view(&self.device, &self.config, self.msaa_samples);
|
||||
self.stencil_view = create_stencil_view(&self.device, &self.config, self.msaa_samples);
|
||||
self.recalculate_viewport();
|
||||
}
|
||||
}
|
||||
@@ -1340,10 +1376,18 @@ impl CanvasState {
|
||||
},
|
||||
},
|
||||
})],
|
||||
depth_stencil_attachment: None,
|
||||
depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
|
||||
view: &self.stencil_view,
|
||||
depth_ops: None,
|
||||
stencil_ops: Some(wgpu::Operations {
|
||||
load: wgpu::LoadOp::Clear(0),
|
||||
store: wgpu::StoreOp::Discard,
|
||||
}),
|
||||
}),
|
||||
timestamp_writes: None,
|
||||
occlusion_query_set: None,
|
||||
});
|
||||
let mut stencil_ref = 0;
|
||||
|
||||
// 0. Nachbarseiten der durchgehenden Ansicht (je mit eigener Kamera)
|
||||
for (i, neighbor) in self.neighbors.iter().enumerate() {
|
||||
@@ -1362,6 +1406,7 @@ impl CanvasState {
|
||||
render_pass.set_vertex_buffer(0, buffer.slice(..));
|
||||
for range in &neighbor.stroke_ranges {
|
||||
if range.end > range.start {
|
||||
self.next_stroke_stencil(&mut render_pass, &mut stencil_ref);
|
||||
render_pass.draw(range.clone(), 0..1);
|
||||
}
|
||||
}
|
||||
@@ -1393,6 +1438,7 @@ impl CanvasState {
|
||||
render_pass.set_vertex_buffer(0, self.committed_vertex_buffer.slice(..));
|
||||
for range in &self.committed_strokes {
|
||||
if range.end > range.start {
|
||||
self.next_stroke_stencil(&mut render_pass, &mut stencil_ref);
|
||||
render_pass.draw(range.clone(), 0..1);
|
||||
}
|
||||
}
|
||||
@@ -1401,6 +1447,7 @@ impl CanvasState {
|
||||
// 5. Temporäre Live-Geometrie des aktuellen Strichs rendern (Zero Latency)
|
||||
if self.live_vertex_count > 0 {
|
||||
render_pass.set_vertex_buffer(0, self.live_vertex_buffer.slice(..));
|
||||
self.next_stroke_stencil(&mut render_pass, &mut stencil_ref);
|
||||
render_pass.draw(0..self.live_vertex_count, 0..1);
|
||||
}
|
||||
}
|
||||
@@ -1437,6 +1484,20 @@ impl CanvasState {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Gibt dem nächsten Strich eine eigene Stencil-Nummer: Die Strich-Pipeline zeichnet nur
|
||||
/// Pixel, die noch nicht diese Nummer tragen. Nach 255 Strichen wird der Stencil-Puffer
|
||||
/// zurückgesetzt, damit kein späterer Strich die Nummer eines früheren wiederfindet.
|
||||
fn next_stroke_stencil<'a>(&'a self, pass: &mut wgpu::RenderPass<'a>, stencil_ref: &mut u32) {
|
||||
if *stencil_ref == u32::from(u8::MAX) {
|
||||
pass.set_pipeline(&self.stencil_reset_pipeline);
|
||||
pass.draw(0..3, 0..1);
|
||||
pass.set_pipeline(&self.strip_pipeline);
|
||||
*stencil_ref = 0;
|
||||
}
|
||||
*stencil_ref += 1;
|
||||
pass.set_stencil_reference(*stencil_ref);
|
||||
}
|
||||
|
||||
/// Rendert nur die Canvas-Fläche ohne UI (z. B. für Headless-Tests).
|
||||
pub fn render_canvas_only(&mut self) -> Result<(), wgpu::SurfaceError> {
|
||||
let dummy_delta = egui::TexturesDelta::default();
|
||||
@@ -1477,6 +1538,76 @@ fn create_msaa_view(
|
||||
})
|
||||
}
|
||||
|
||||
/// Format des Stencil-Puffers der Zeichenfläche.
|
||||
const STENCIL_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Depth24PlusStencil8;
|
||||
|
||||
/// Stencil-Puffer in Fenstergröße (mit derselben Abtastzahl wie die Zeichenfläche).
|
||||
fn create_stencil_view(
|
||||
device: &wgpu::Device,
|
||||
config: &wgpu::SurfaceConfiguration,
|
||||
samples: u32,
|
||||
) -> wgpu::TextureView {
|
||||
device
|
||||
.create_texture(&wgpu::TextureDescriptor {
|
||||
label: Some("Stroke Stencil Buffer"),
|
||||
size: wgpu::Extent3d {
|
||||
width: config.width.max(1),
|
||||
height: config.height.max(1),
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
mip_level_count: 1,
|
||||
sample_count: samples,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: STENCIL_FORMAT,
|
||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
|
||||
view_formats: &[],
|
||||
})
|
||||
.create_view(&wgpu::TextureViewDescriptor::default())
|
||||
}
|
||||
|
||||
/// Stencil-Zustand mit gleicher Prüfung und Aktion für Vorder- und Rückseiten.
|
||||
fn stencil_state(
|
||||
compare: wgpu::CompareFunction,
|
||||
pass_op: wgpu::StencilOperation,
|
||||
) -> wgpu::DepthStencilState {
|
||||
let face = wgpu::StencilFaceState {
|
||||
compare,
|
||||
fail_op: wgpu::StencilOperation::Keep,
|
||||
depth_fail_op: wgpu::StencilOperation::Keep,
|
||||
pass_op,
|
||||
};
|
||||
wgpu::DepthStencilState {
|
||||
format: STENCIL_FORMAT,
|
||||
depth_write_enabled: false,
|
||||
depth_compare: wgpu::CompareFunction::Always,
|
||||
stencil: wgpu::StencilState {
|
||||
front: face,
|
||||
back: face,
|
||||
read_mask: 0xff,
|
||||
write_mask: 0xff,
|
||||
},
|
||||
bias: wgpu::DepthBiasState::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Pipelines ohne Strichnummer (Papier, Muster, Bilder) lassen den Stencil-Puffer unberührt.
|
||||
pub(crate) fn passive_stencil() -> wgpu::DepthStencilState {
|
||||
stencil_state(wgpu::CompareFunction::Always, wgpu::StencilOperation::Keep)
|
||||
}
|
||||
|
||||
/// Striche: nur Pixel, die die eigene Nummer noch nicht tragen, und diese dann markieren.
|
||||
fn stroke_stencil() -> wgpu::DepthStencilState {
|
||||
stencil_state(
|
||||
wgpu::CompareFunction::NotEqual,
|
||||
wgpu::StencilOperation::Replace,
|
||||
)
|
||||
}
|
||||
|
||||
/// Zurücksetzen: überall 0 eintragen.
|
||||
fn stencil_reset() -> wgpu::DepthStencilState {
|
||||
stencil_state(wgpu::CompareFunction::Always, wgpu::StencilOperation::Zero)
|
||||
}
|
||||
|
||||
/// Zu rasterndes Textfeld.
|
||||
struct TextSpec {
|
||||
text: crate::text::TextBox,
|
||||
|
||||
Reference in new issue
Block a user