Files
cadcraft/crates/ui-egui/tests/gpu_viewport_stencil.rs
T
26179f7428 Draw object transparency on screen, in exports and in plots (#764)
* Draw object transparency on screen, in exports and in plots

Transparency was read and saved (entity group 440, layer xdata, ByLayer and
ByBlock) but never drawn. Display lists now carry each object's effective
transparency (its own, its layer's, or the block reference's), and:

- the canvas (GPU and CPU paths), ui.render and PNG/SVG exports blend it;
  TRANSPARENCYDISPLAY turns it off on screen and is now on by default, as in
  AutoCAD (initial value 1);
- plots draw it only when "Plot transparency" is on: a page setup option
  (PAGESETUP `plotTransparency`, a checkbox in the Page Setup Manager and the
  Plot dialog), overridable per plot, with PLOTTRANSPARENCYOVERRIDE (0 never,
  1 the setting, 2 always). PDF gets /ExtGState alpha, PNG plots blend, SVG
  uses opacity. Plots without it are byte-identical to before.

The public DXF Reference has no plot layout flag for plot transparency, so
the option is kept in the session and in JSON, not in DXF/DWG.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Transparency fixes: attributes, PDF overlaps, drag previews

- Attributes draw with their own transparency (their definition's, layer 0
  meaning the reference's layer, ByBlock the reference's) instead of the
  block reference's.
- PDF plots stroke overlapping transparent objects one by one, so they build
  up where they cross as in PNG plots; opaque output is unchanged.
- Drag and rubber-band previews follow TRANSPARENCYDISPLAY.
- Effective transparency is worked out only when it is drawn, reusing the
  layer the property lookup already found.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Transparency: one PDF stroke per transparent primitive; opaque rebuilt previews

- PDF plots never batch two transparent primitives into one path. Block and
  viewport contents share their reference's handle, so the per-object flush
  missed them; each primitive now blends on its own, as in PNG plots and on
  screen. Opaque output is unchanged.
- Previews the session rebuilds with the current properties (rubber bands,
  copies of objects) are drawn opaque again: they carry CLAYER's
  transparency, not the objects'. The GPU drag of the objects themselves
  keeps following TRANSPARENCYDISPLAY.
- Tests for attributes on a transparent reference layer, attributes without a
  definition, and the drag cache across the TRANSPARENCYDISPLAY toggle.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Selection like AutoCAD: colour highlight and the Options Selection tab

Selecting or hovering an object outlined each filled primitive (TrueType
glyphs, dimension and leader arrowheads, solid hatches, wide polylines)
by the rectangle around its triangles, and dashed everything else. A
selected dimension's text became a row of dashed boxes, one per glyph,
and its closed arrowheads small rectangles.

Selected objects are now redrawn in the selection colour: lines solid,
fills filled half see-through (so the lines and text on a highlighted
hatch stay visible), and the whole object reads as selected, text and
arrowheads included. A wipeout is always outlined, since filling it would
hide the drawing under it.

The Options dialog's Selection tab is laid out as AutoCAD's, with the
settings CADCraft can act on, each one AutoCAD's variable, kept in the
user profile:

- Selection modes: noun/verb selection (PICKFIRST), Shift to add
  (PICKADD), Object grouping (PICKSTYLE bit 1, now saved in the profile).
- Selection effect color (selectionColor, an interface preference; null
  is the theme's).
- Grips: Show grips (GRIPS) and the object selection limit for display of
  grips (GRIPOBJLIMIT, 100; 0 no limit): grips are neither drawn nor
  picked beyond it.
- Selection preview (SELECTIONPREVIEW): the object under the cursor is
  highlighted when a command is active (2) and when none is (1).
- Visual Effect Settings: the selection area effect (SELECTIONAREA,
  WINDOWAREACOLOR 150, CROSSINGAREACOLOR 100, SELECTIONAREAOPACITY 25)
  and the Selection Preview Filter (PREVIEWFILTER 3: 1 objects on locked
  layers, 2 xrefs, 4 tables, 8 multiline text, 16 hatches, 32 groups).

As in AutoCAD these are typed, not on the dialog: HIGHLIGHT (0 shows only
the grips of selected objects; not saved, so every session starts
highlighted), SELECTIONEFFECT (1 the selection colour, 0 dashed lines with
fills outlined along their real boundary) and PREVIEWEFFECT (the preview
dashed 0, thicker 1 or both 2).

Left for later, as CADCraft lacks what they control: lasso, grip tips and
menus, group grips, grips within blocks, associative hatch selection,
command and property preview, PICKDRAG, PICKAUTO and PROPOBJLIMIT.

DisplayList::fill_outlines returns the boundary of the region a filled
primitive covers as closed polylines (edges only one triangle has; the
slab filler's horizontal cuts, which need not line up between trapezoids,
are split at every x on their line and kept where an odd number cover
them), falling back to the bounding box over 600,000 vertices. The GPU
and CPU highlight paths and the drawing comparison overlay share the same
looks.

The new labels have Ukrainian rows (the required catalog); other
languages fall back to English.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: EP995LAB <293597958+EP995LAB@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Cody Vogt <18035288+codyvogt@users.noreply.github.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-13 06:12:48 -04:00

179 lines
8.7 KiB
Rust

//! A clipped layout viewport's contents and selection highlight are clipped on the GPU (#688):
//! the outline is drawn into the stencil buffer as a triangle fan that flips a bit (even-odd),
//! and they draw only where it is set. Renders through the canvas pipelines into an offscreen
//! colour + stencil target and reads the pixels back.
use std::sync::Arc;
use cadcraft_engine::doc::Handle;
use cadcraft_engine::geom::{Bounds2, Vec2};
use cadcraft_engine::render::{DPrim, DisplayList, Kind};
use cadcraft_ui_egui::gpu::{self, CanvasCallback, GpuFaults, LAYER_HIGHLIGHT, LAYER_MASK, LAYER_VIEWPORT, MeshSlot, Stencil};
use egui_wgpu::wgpu;
use egui_wgpu::{CallbackTrait, ScreenDescriptor};
const SIZE: u32 = 64;
const STENCIL: wgpu::TextureFormat = wgpu::TextureFormat::Stencil8;
/// A display list of one black filled polygon given as triangles.
fn tris(pts: Vec<Vec2>) -> DisplayList {
let len = pts.len() as u32;
let bounds = Bounds2::from_points(pts.iter().copied());
let prim =
DPrim { handle: Handle(1), color: cadcraft_color::Rgb(0, 0, 0), aci7: false, lw: 0.0, transparency: 0, kind: Kind::Tris, start: 0, len };
DisplayList { prims: vec![prim], tris: pts, bounds, ..Default::default() }
}
/// A triangle fan over `outline` (what the canvas masks a clipped viewport with).
fn fan(outline: &[Vec2]) -> DisplayList {
let mut t = Vec::new();
for w in outline[1..].windows(2) {
t.extend_from_slice(&[outline[0], w[0], w[1]]);
}
tris(t)
}
/// A callback drawing `list` on `layer`, one world unit per pixel, world (32, 32) at the centre.
fn callback(list: &DisplayList, layer: usize, key: u64, stencil: Stencil) -> CanvasCallback {
let slot: MeshSlot = Arc::default();
*slot.lock().unwrap() = Some(gpu::build_mesh(list, Vec2::ZERO, cadcraft_color::Rgb(255, 255, 255), false));
CanvasCallback { key, slot, offset: [-32.0, -32.0], scale: 1.0, center: [32.0, 32.0], layer, xform: gpu::IDENTITY_XFORM, dash: 0.0, stencil }
}
/// Render the callbacks in order into a white target with a cleared stencil buffer; returns
/// whether each pixel came out dark, row by row from the top.
fn render(rs: &egui_wgpu::RenderState, callbacks: &[CanvasCallback]) -> Vec<bool> {
let (device, queue) = (&rs.device, &rs.queue);
let extent = wgpu::Extent3d { width: SIZE, height: SIZE, depth_or_array_layers: 1 };
let texture = |format, usage| {
device.create_texture(&wgpu::TextureDescriptor {
label: None,
size: extent,
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format,
usage,
view_formats: &[],
})
};
let color = texture(rs.target_format, wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC);
let stencil = texture(STENCIL, wgpu::TextureUsages::RENDER_ATTACHMENT);
let (color_view, stencil_view) = (color.create_view(&Default::default()), stencil.create_view(&Default::default()));
let screen = ScreenDescriptor { size_in_pixels: [SIZE, SIZE], pixels_per_point: 1.0 };
let mut encoder = device.create_command_encoder(&Default::default());
let mut renderer = rs.renderer.write();
for cb in callbacks {
let _ = cb.prepare(device, queue, &screen, &mut encoder, &mut renderer.callback_resources);
}
{
let mut pass = encoder
.begin_render_pass(&wgpu::RenderPassDescriptor {
label: None,
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &color_view,
resolve_target: None,
ops: wgpu::Operations { load: wgpu::LoadOp::Clear(wgpu::Color::WHITE), store: wgpu::StoreOp::Store },
depth_slice: None,
})],
depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
view: &stencil_view,
depth_ops: None,
stencil_ops: Some(wgpu::Operations { load: wgpu::LoadOp::Clear(0), store: wgpu::StoreOp::Discard }),
}),
..Default::default()
})
.forget_lifetime();
let rect = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(SIZE as f32, SIZE as f32));
for cb in callbacks {
let info = egui::PaintCallbackInfo { viewport: rect, clip_rect: rect, pixels_per_point: 1.0, screen_size_px: [SIZE, SIZE] };
cb.paint(info, &mut pass, &renderer.callback_resources);
}
}
let row = SIZE * 4;
let buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: None,
size: u64::from(row * SIZE),
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
mapped_at_creation: false,
});
encoder.copy_texture_to_buffer(
color.as_image_copy(),
wgpu::TexelCopyBufferInfo {
buffer: &buffer,
layout: wgpu::TexelCopyBufferLayout { offset: 0, bytes_per_row: Some(row), rows_per_image: Some(SIZE) },
},
extent,
);
queue.submit([encoder.finish()]);
buffer.slice(..).map_async(wgpu::MapMode::Read, |_| {});
device.poll(wgpu::PollType::Wait { submission_index: None, timeout: Some(std::time::Duration::from_secs(10)) }).unwrap();
let pixels = buffer.slice(..).get_mapped_range().unwrap();
pixels.chunks(4).map(|p| u32::from(p[0]) + u32::from(p[1]) + u32::from(p[2]) < 300).collect()
}
fn dark(px: &[bool], x: u32, y: u32) -> bool {
px[(y * SIZE + x) as usize]
}
#[test]
fn clipped_viewport_contents_draw_only_inside_their_outline() {
let rs = egui_kittest::wgpu::create_render_state(egui_kittest::wgpu::default_wgpu_setup(), Default::default());
let faults = Arc::new(GpuFaults::default());
let target = gpu::install(&rs, &faults, Some(STENCIL));
assert_eq!(target.stencil, Some(STENCIL));
// The contents: a black square over the whole target.
let square = |key| {
let (a, b, c, d) = (Vec2::new(0.0, 0.0), Vec2::new(64.0, 0.0), Vec2::new(64.0, 64.0), Vec2::new(0.0, 64.0));
callback(&tris(vec![a, b, c, a, c, d]), LAYER_VIEWPORT, key, Stencil::Inside)
};
// A circle of radius 20 about the centre (screen and world agree at the centre; y is up).
let circle: Vec<Vec2> =
(0..=64).map(|i| f64::from(i) * std::f64::consts::TAU / 64.0).map(|a| Vec2::new(32.0 + 20.0 * a.cos(), 32.0 + 20.0 * a.sin())).collect();
let px = render(&rs, &[callback(&fan(&circle), LAYER_MASK, 1, Stencil::Mask), square(2)]);
assert!(dark(&px, 32, 32) && dark(&px, 45, 32) && dark(&px, 32, 18));
assert!(!dark(&px, 4, 4) && !dark(&px, 60, 60) && !dark(&px, 58, 32) && !dark(&px, 32, 58));
// A concave L: the fan covers the notch twice, which leaves it unmasked.
let l = [(8.0, 8.0), (56.0, 8.0), (56.0, 24.0), (24.0, 24.0), (24.0, 56.0), (8.0, 56.0), (8.0, 8.0)].map(|(x, y)| Vec2::new(x, y));
let px = render(&rs, &[callback(&fan(&l), LAYER_MASK, 3, Stencil::Mask), square(4)]);
// World (16, 40) is screen (16, 24); world (40, 16) is screen (40, 48); the notch, world
// (40, 40), is screen (40, 24).
assert!(dark(&px, 16, 24) && dark(&px, 40, 48), "inside both arms of the L");
assert!(!dark(&px, 40, 24), "the notch is outside");
assert!(!dark(&px, 2, 2) && !dark(&px, 60, 60));
// The selection highlight inside a clipped viewport stays inside its outline too: a wide
// line across the whole target at world y 32, masked by the circle.
let line = DisplayList {
prims: vec![DPrim {
handle: Handle(1),
color: cadcraft_color::Rgb(0, 0, 0),
aci7: false,
lw: 0.0,
transparency: 0,
kind: Kind::Polyline,
start: 0,
len: 2,
}],
verts: vec![Vec2::new(0.0, 32.0), Vec2::new(64.0, 32.0)],
bounds: Bounds2::new(Vec2::new(0.0, 32.0), Vec2::new(64.0, 32.0)),
..Default::default()
};
let slot: MeshSlot = Arc::default();
*slot.lock().unwrap() = Some(gpu::build_highlight_mesh(&line, &[0], Vec2::ZERO, [0, 0, 0, 255], 3.0, false));
let highlight = CanvasCallback { slot, layer: LAYER_HIGHLIGHT, ..callback(&line, LAYER_HIGHLIGHT, 6, Stencil::Inside) };
let px = render(&rs, &[callback(&fan(&circle), LAYER_MASK, 1, Stencil::Mask), highlight]);
assert!(dark(&px, 32, 32) && dark(&px, 20, 32) && dark(&px, 44, 32), "inside the circle");
assert!(!dark(&px, 4, 32) && !dark(&px, 60, 32), "outside it");
// Without a mask, Off draws everywhere.
let mut off = square(5);
off.stencil = Stencil::Off;
let px = render(&rs, &[off]);
assert!(px.iter().all(|d| *d));
// wgpu reported nothing (validation errors land in the faults instead of panicking).
assert_eq!(faults.take(), None);
}