fix(hatch): preview pattern grip drag

This commit is contained in:
Hakan Seven committed 2026-07-28 20:07:10 +03:00
1 parent a36e0d7fdc
commit 25166b9508
5 files changed
+197

No files matched your search

+6
View File
@@ -865,6 +865,12 @@ impl OpenCADStudio {
self.tabs[i]
.scene
.apply_grip(grip.handle, grip.grip_id, apply);
if matches!(
self.tabs[i].scene.document.get_entity(grip.handle),
Some(acadrust::EntityType::Hatch(_))
) {
self.tabs[i].scene.set_preview_hatch(grip.handle);
}
self.tabs[i].dirty = true;
self.tabs[i].active_grip.as_mut().unwrap().last_world = snapped;
let apply_ms = apply_started.elapsed().as_secs_f64() * 1000.0;
+5
View File
@@ -1536,6 +1536,10 @@ pub struct Scene {
pub selection_filter: HashSet<String>,
/// In-progress preview wires while a command is active (rubber-band + object ghosts).
pub preview_wires: Vec<WireModel>,
/// Live hatch fill used by interactive edits such as dragging the pattern
/// origin grip. Kept separate from the resident hatch set so moving one
/// pattern never re-uploads every hatch in a large drawing.
pub preview_hatches: Arc<Vec<HatchModel>>,
/// In-progress preview SDF glyph quads (grip drag / command preview). Rides
/// a per-frame GPU buffer separate from the epoch-cached base text so the
/// dragged text stays visible while it's hidden from the base set (#316).
@@ -1911,6 +1915,7 @@ impl Scene {
transparency_display: true,
selection_filter: HashSet::default(),
preview_wires: vec![],
preview_hatches: Arc::new(Vec::new()),
preview_text: vec![],
interim_wire: None,
camera_generation: 0,
+54
View File
@@ -257,6 +257,10 @@ pub struct Pipeline {
gpu_hatch: Option<hatch_gpu::HatchGpu>,
/// Compatibility hatch fills — one texture-backed GPU object per hatch.
gpu_hatches_compat: Vec<hatch_web_gpu::HatchWebGpu>,
/// Tiny live hatch batch used by grip previews. Separate from the resident
/// batch so an interactive edit uploads only the hatch being changed.
gpu_preview_hatch: Option<hatch_gpu::HatchGpu>,
gpu_preview_hatches_compat: Vec<hatch_web_gpu::HatchWebGpu>,
/// Wipeout masks — solid fills rendered after wires in a separate pass via
/// the legacy per-primitive `WipeoutGpu` renderer.
gpu_wipeouts: Vec<WipeoutGpu>,
@@ -293,6 +297,7 @@ pub struct Pipeline {
/// Arc makes pointer identity ABA-safe: an unchanged category reuses the
/// same Arc even when an unrelated entity advances `geometry_epoch`.
pub cached_hatch_source: Option<std::sync::Arc<Vec<HatchModel>>>,
pub cached_preview_hatch_source: Option<std::sync::Arc<Vec<HatchModel>>>,
pub cached_wipeout_source: Option<std::sync::Arc<Vec<HatchModel>>>,
pub cached_image_source: Option<std::sync::Arc<Vec<ImageModel>>>,
pub cached_text_source: Option<std::sync::Arc<Vec<text_gpu::TextVertex>>>,
@@ -1827,6 +1832,8 @@ impl Pipeline {
gpu_preview_wires: vec![],
gpu_hatch: None,
gpu_hatches_compat: vec![],
gpu_preview_hatch: None,
gpu_preview_hatches_compat: vec![],
gpu_wipeouts: vec![],
wipeout_skip_flags: vec![],
gpu_images: vec![],
@@ -1842,6 +1849,7 @@ impl Pipeline {
gpu_face3d_edges: vec![],
viewcube,
cached_hatch_source: None,
cached_preview_hatch_source: None,
cached_wipeout_source: None,
cached_image_source: None,
cached_text_source: None,
@@ -2877,6 +2885,35 @@ impl Pipeline {
}
}
pub fn upload_preview_hatches(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
hatches: &[HatchModel],
) {
if let Some(bgl1) = &self.hatch_compat_bgl1 {
self.gpu_preview_hatches_compat = hatches
.iter()
.filter(|h| h.boundary.len() >= 3)
.map(|h| hatch_web_gpu::HatchWebGpu::new(device, queue, h, bgl1))
.collect();
self.gpu_preview_hatch = None;
return;
}
self.gpu_preview_hatches_compat.clear();
let Some(bgl1) = &self.hatch_bgl1 else {
self.gpu_preview_hatch = None;
return;
};
let renderable: Vec<HatchModel> = hatches
.iter()
.filter(|h| h.boundary.len() >= 3)
.cloned()
.collect();
self.gpu_preview_hatch = hatch_gpu::HatchGpu::build(device, bgl1, &renderable);
}
pub fn upload_wipeouts(&mut self, device: &wgpu::Device, wipeouts: &[HatchModel]) {
self.gpu_wipeouts = wipeouts
.iter()
@@ -3063,6 +3100,18 @@ impl Pipeline {
pass.draw(0..batch.vertex_count, 0..1);
}
}
if let (Some(batch), Some(pipeline)) =
(&self.gpu_preview_hatch, &self.hatch_pipeline)
{
if !self.skip_hatch_frame {
pass.set_pipeline(pipeline);
pass.set_stencil_reference(stencil_ref);
pass.set_bind_group(0, &self.uniform_bind_group, &[]);
pass.set_bind_group(1, &batch.bind_group, &[]);
pass.set_vertex_buffer(0, batch.vertex_buffer.slice(..));
pass.draw(0..batch.vertex_count, 0..1);
}
}
// Storage-free compatibility path. Draw before wires so outlines
// remain on top, matching the batched path.
@@ -3076,6 +3125,11 @@ impl Pipeline {
pass.set_vertex_buffer(0, hatch.vertex_buffer.slice(..));
pass.draw(0..6, 0..1);
}
for hatch in &self.gpu_preview_hatches_compat {
pass.set_bind_group(1, &hatch.bind_group, &[]);
pass.set_vertex_buffer(0, hatch.vertex_buffer.slice(..));
pass.draw(0..6, 0..1);
}
}
}
}
+55
View File
@@ -14,6 +14,58 @@ impl Scene {
self.preview_wires = wires;
}
/// Publish the current cached hatch as a one-entity live fill overlay.
/// The edited hatch is hidden from the resident set during a grip drag;
/// this keeps its pattern visible without rebuilding the full hatch batch.
pub fn set_preview_hatch(&mut self, handle: Handle) {
let Some(mut model) = self.hatches.get(&handle).cloned() else {
self.preview_hatches = std::sync::Arc::new(Vec::new());
return;
};
let Some(entity) = self.document.get_entity(handle) else {
self.preview_hatches = std::sync::Arc::new(Vec::new());
return;
};
if !matches!(
model.pattern,
crate::scene::model::hatch_model::HatchPattern::Gradient { .. }
) {
model.color = self.render_style(entity).0;
}
if self.selected.contains(&handle) {
model.color = [0.15, 0.55, 1.00, model.color[3]];
}
model.draw_depth = self
.draw_depth_map()
.get(&handle.value())
.map_or(0.0, |depth| depth[0]);
let mut models = Vec::with_capacity(2);
if let EntityType::Hatch(hatch) = entity {
if let Some(background) = crate::entities::hatch::background_color(hatch) {
let mut backdrop = model.clone();
backdrop.pattern =
crate::scene::model::hatch_model::HatchPattern::Solid;
backdrop.color = match background {
acadrust::types::Color::ByLayer => {
crate::scene::view::render::layer_render_style(
&self.document,
&hatch.common.layer,
)
.color
}
acadrust::types::Color::ByBlock => self.render_style(entity).0,
other => crate::scene::convert::tess_util::aci_to_rgba(&other),
};
backdrop.name = "SOLID".into();
models.push(backdrop);
}
}
models.push(model);
self.preview_hatches = std::sync::Arc::new(models);
}
pub fn set_preview_text(&mut self, verts: Vec<crate::scene::pipeline::text_gpu::TextVertex>) {
// Overlay glyphs — same reasoning as `set_preview_wires`: no geometry
// bump. Uploaded to a dedicated per-frame text buffer in `prepare`.
@@ -25,6 +77,9 @@ impl Scene {
// flips the wire content id back to the base tessellation id, which
// re-uploads the base wires (without the preview) on the next frame.
self.preview_wires = vec![];
if !self.preview_hatches.is_empty() {
self.preview_hatches = std::sync::Arc::new(Vec::new());
}
self.preview_text = vec![];
self.interim_wire = None;
// Drop any point-picked window marquee (STRETCH) so it doesn't linger
+77
View File
@@ -52,6 +52,9 @@ pub struct ViewportData {
/// the main `wires` buffer so a drag re-uploads only this small set each
/// frame, never the resident base buffer. Drawn on top in the wire pass.
pub(in crate::scene) preview_wires: Arc<Vec<WireModel>>,
/// One/few live hatch models for grip editing. Uploaded through a separate
/// tiny GPU batch so the resident hatch buffer remains untouched.
pub(in crate::scene) preview_hatches: Arc<Vec<HatchModel>>,
/// 3DFACE entity wires — separated so they are uploaded to the dedicated
/// face3d pipeline (fill + batched edges) instead of N individual WireGpu.
pub(in crate::scene) face3d_wires: Arc<Vec<WireModel>>,
@@ -268,6 +271,7 @@ impl shader::Primitive for Primitive {
inner.cached_mesh_content_id = u64::MAX;
inner.cached_face3d_key = (u64::MAX, false);
inner.cached_hatch_source = None;
inner.cached_preview_hatch_source = None;
inner.cached_wipeout_source = None;
inner.cached_image_source = None;
inner.cached_text_source = None;
@@ -371,6 +375,23 @@ impl shader::Primitive for Primitive {
);
inner.cached_hatch_source = Some(Arc::clone(&vp.hatches));
}
let preview_hatch_changed = inner
.cached_preview_hatch_source
.as_ref()
.map_or(true, |source| !Arc::ptr_eq(source, &vp.preview_hatches));
if preview_hatch_changed || fill_changed {
inner.upload_preview_hatches(
device,
queue,
if fill_mode {
&vp.preview_hatches[..]
} else {
&[]
},
);
inner.cached_preview_hatch_source =
Some(Arc::clone(&vp.preview_hatches));
}
if wipeout_changed || fill_changed {
inner.upload_wipeouts(
device,
@@ -1100,6 +1121,56 @@ fn render_signature(vp: &ViewportData, clip_w: u32, clip_h: u32) -> u64 {
p[2].to_bits().hash(&mut h);
}
}
// Live hatch preview. Pattern-origin grip drags keep the boundary fixed and
// move only the family anchors, so hash both geometry and pattern data.
vp.preview_hatches.len().hash(&mut h);
for model in vp.preview_hatches.iter() {
model.world_origin[0].to_bits().hash(&mut h);
model.world_origin[1].to_bits().hash(&mut h);
model.boundary.len().hash(&mut h);
for point in model.boundary.iter() {
point[0].to_bits().hash(&mut h);
point[1].to_bits().hash(&mut h);
}
for component in model.color {
component.to_bits().hash(&mut h);
}
model.angle_offset.to_bits().hash(&mut h);
model.scale.to_bits().hash(&mut h);
match &model.pattern {
crate::scene::model::hatch_model::HatchPattern::Solid => {
0_u8.hash(&mut h);
}
crate::scene::model::hatch_model::HatchPattern::Pattern(families) => {
1_u8.hash(&mut h);
families.len().hash(&mut h);
for family in families {
family.angle_deg.to_bits().hash(&mut h);
family.x0.to_bits().hash(&mut h);
family.y0.to_bits().hash(&mut h);
family.dx.to_bits().hash(&mut h);
family.dy.to_bits().hash(&mut h);
for dash in &family.dashes {
dash.to_bits().hash(&mut h);
}
}
}
crate::scene::model::hatch_model::HatchPattern::Gradient {
angle_deg,
color2,
kind,
invert,
} => {
2_u8.hash(&mut h);
angle_deg.to_bits().hash(&mut h);
for component in color2 {
component.to_bits().hash(&mut h);
}
kind.shader_kind().hash(&mut h);
invert.hash(&mut h);
}
}
}
// Grip-drag / command-preview glyph quads (issue #316). A pure-text slide
// leaves `preview_wires` empty and moves ONLY these, so a signature that
// ignored them would let the scene-render cache freeze the dragged text at
@@ -2015,6 +2086,11 @@ impl Scene {
v.extend(self.preview_wires.iter().cloned());
Arc::new(v)
};
let preview_hatches = if show_live_overlay {
Arc::clone(&self.preview_hatches)
} else {
Arc::new(Vec::new())
};
// Build the camera at the *full* viewport's aspect so the ortho
// frustum matches what the viewport entity stores, then post-
@@ -2198,6 +2274,7 @@ impl Scene {
wires: Arc::downgrade(&all_wires),
clip_boundary_ndc,
preview_wires,
preview_hatches,
face3d_wires,
text_verts,
preview_text_verts,