forked from 1b-agent/opencadstudio-upstream
Underlays are rasterised for the view's scale; DGN V7 strokes one pixel wide
A page shown small was drawn from its full-size raster and thin lines broke up into dots. Once a zoom settles on another quarter-octave step, PDF, DWF and DGN underlays are rasterised again near their size on screen (never above the full-size raster), frames are requested until that happens, and the adjusted-pixel memo is kept per raster size. V7 element weights no longer widen strokes, matching V8 models. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
cb7860e50e
commit
1cbe89ef62
8 files changed
+108
-19
No files matched your search
@@ -850,6 +850,8 @@ impl OpenCADStudio {
|
||||
self.tabs[i].scene.update(t - self.start);
|
||||
// Dynamic dimensions keep their screen size across zoom bands.
|
||||
self.tabs[i].scene.refresh_dynamic_dimension_scales(false);
|
||||
// Underlays are rasterised again for the zoom once it settles.
|
||||
self.tabs[i].scene.refresh_underlay_resolution(t);
|
||||
|
||||
// If the camera moved since we last synced, write it back to
|
||||
// the document and mark the file dirty.
|
||||
|
||||
+4
-1
@@ -2544,7 +2544,10 @@ impl OpenCADStudio {
|
||||
// driven by input events, but once the cursor stops no event would fire
|
||||
// the one full-quality frame that re-renders hatches — this tick does,
|
||||
// then the scene-render cache holds it and the subscription auto-stops.
|
||||
let nav_settle = if self.tabs[self.active_tab].scene.is_settling() {
|
||||
// Underlay rasters follow the zoom once it settles; tick until then.
|
||||
let nav_settle = if self.tabs[self.active_tab].scene.is_settling()
|
||||
|| self.tabs[self.active_tab].scene.underlay_resolution_stale()
|
||||
{
|
||||
window::frames().map(Message::Tick)
|
||||
} else {
|
||||
Subscription::none()
|
||||
|
||||
+1
-1
@@ -2419,7 +2419,7 @@ impl Scene {
|
||||
} else {
|
||||
self.paper_bg_color
|
||||
};
|
||||
ImageModel::from_underlay(u, def, background)
|
||||
ImageModel::from_underlay(u, def, background, self.underlay_world_per_pixel)
|
||||
}
|
||||
_ => None,
|
||||
},
|
||||
|
||||
+49
-1
@@ -1275,7 +1275,7 @@ fn build_derived_caches_impl(
|
||||
} else {
|
||||
[1.0, 1.0, 1.0, 1.0]
|
||||
};
|
||||
ImageModel::from_underlay(u, def, background).map(|m| (handle, m))
|
||||
ImageModel::from_underlay(u, def, background, None).map(|m| (handle, m))
|
||||
}
|
||||
_ => None,
|
||||
},
|
||||
@@ -2325,6 +2325,11 @@ pub struct Scene {
|
||||
hidden_dynamic_dimensions: HashSet<Handle>,
|
||||
/// The camera generation the dynamic dimensions were last scaled for.
|
||||
dynamic_dimension_camera_gen: Option<u64>,
|
||||
/// The view scale (world units per pixel, in quarter-octave steps) the
|
||||
/// underlay rasters are made for, and a new step waiting for the view
|
||||
/// to settle.
|
||||
underlay_world_per_pixel: Option<f64>,
|
||||
underlay_scale_pending: Option<(i32, iced::time::Instant)>,
|
||||
/// Session-only visibility overrides for constraint glyphs.
|
||||
hidden_parametric_constraints: HashSet<(
|
||||
parametric_constraints::ParametricScope,
|
||||
@@ -2687,6 +2692,8 @@ impl Scene {
|
||||
dynamic_constraint_display: true,
|
||||
hidden_dynamic_dimensions: HashSet::default(),
|
||||
dynamic_dimension_camera_gen: None,
|
||||
underlay_world_per_pixel: None,
|
||||
underlay_scale_pending: None,
|
||||
hidden_parametric_constraints: HashSet::default(),
|
||||
shown_parametric_constraints: HashSet::default(),
|
||||
constraints_epoch: 0,
|
||||
@@ -2739,6 +2746,47 @@ impl Scene {
|
||||
/// model camera, so a cached value applied to mm-sheet entity AABBs would
|
||||
/// be a stale model-world wpp and cull every paper-space annotation.
|
||||
/// Paper-space callers use their own scale instead.
|
||||
/// Rasterise the underlays again when the view's scale has moved to
|
||||
/// another quarter-octave step and stayed there briefly (not on every
|
||||
/// frame of a zoom).
|
||||
pub fn refresh_underlay_resolution(&mut self, now: iced::time::Instant) {
|
||||
let Some(wpp) = self.world_per_pixel() else {
|
||||
return;
|
||||
};
|
||||
let step = (f64::from(wpp).log2() * 4.0).floor() as i32;
|
||||
let current = self.underlay_world_per_pixel.map(|w| (w.log2() * 4.0).round() as i32);
|
||||
if current == Some(step) {
|
||||
self.underlay_scale_pending = None;
|
||||
return;
|
||||
}
|
||||
match self.underlay_scale_pending {
|
||||
Some((pending, since)) if pending == step => {
|
||||
if now.duration_since(since) < std::time::Duration::from_millis(150) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
self.underlay_scale_pending = Some((step, now));
|
||||
return;
|
||||
}
|
||||
}
|
||||
self.underlay_scale_pending = None;
|
||||
self.underlay_world_per_pixel = Some(2f64.powf(step as f64 / 4.0));
|
||||
if self.document.entities().any(|e| matches!(e, EntityType::Underlay(_))) {
|
||||
self.reseed_underlays();
|
||||
}
|
||||
}
|
||||
|
||||
/// The view has moved to another scale step than the underlay rasters
|
||||
/// were made for (frames are requested until they are made again).
|
||||
pub fn underlay_resolution_stale(&self) -> bool {
|
||||
let Some(wpp) = self.world_per_pixel() else {
|
||||
return false;
|
||||
};
|
||||
let step = (f64::from(wpp).log2() * 4.0).floor() as i32;
|
||||
self.underlay_world_per_pixel.map(|w| (w.log2() * 4.0).round() as i32) != Some(step)
|
||||
}
|
||||
|
||||
pub(super) fn world_per_pixel(&self) -> Option<f32> {
|
||||
if self.current_layout != "Model" {
|
||||
return None;
|
||||
|
||||
@@ -181,10 +181,14 @@ impl ImageModel {
|
||||
/// limits the drawn area to its inside, or to the page outside it when
|
||||
/// inverted. `None` when the underlay is off, non-PDF, or the page can't
|
||||
/// be rendered (caller keeps the outline placeholder).
|
||||
/// `world_per_pixel` is the view's scale: the page is rasterised near
|
||||
/// the size it shows on screen (full size when `None` or when larger),
|
||||
/// so thin lines stay one crisp pixel wide when zoomed out.
|
||||
pub fn from_underlay(
|
||||
u: &codec::entities::Underlay,
|
||||
def: &codec::entities::UnderlayDefinition,
|
||||
background: [f32; 4],
|
||||
world_per_pixel: Option<f64>,
|
||||
) -> Option<Self> {
|
||||
use codec::entities::{UnderlayDisplayFlags, UnderlayType};
|
||||
use super::pdf_raster::{self, PageAdjust};
|
||||
@@ -196,16 +200,36 @@ impl ImageModel {
|
||||
}
|
||||
let page = crate::entities::underlay::page_of(def);
|
||||
let rect = crate::entities::underlay::definition_rect(def)?;
|
||||
// The page's longest side on screen, pixels (the view scale is taken
|
||||
// a step finer, so the raster is never enlarged on screen).
|
||||
let screen_side = world_per_pixel
|
||||
.filter(|wpp| *wpp > 0.0)
|
||||
.map(|wpp| {
|
||||
let w = (rect[2] - rect[0]) * u.x_scale.abs();
|
||||
let h = (rect[3] - rect[1]) * u.y_scale.abs();
|
||||
w.max(h) / wpp
|
||||
});
|
||||
let (source, raster) = match def.underlay_type {
|
||||
UnderlayType::Pdf => {
|
||||
// Hidden PDF layers come from the underlay's layer overrides.
|
||||
let source = super::pdf_layers::underlay_source(u, &def.file_path);
|
||||
let raster = pdf_raster::rasterize_page_display(&source, page)?;
|
||||
let inches = (rect[2] - rect[0]).max(rect[3] - rect[1]);
|
||||
let raster = match screen_side {
|
||||
Some(side) if inches > 0.0 && side / inches < pdf_raster::DISPLAY_DPI as f64 => {
|
||||
pdf_raster::rasterize_page_display_at(&source, page, (side / inches).max(8.0) as f32)?
|
||||
}
|
||||
_ => pdf_raster::rasterize_page_display(&source, page)?,
|
||||
};
|
||||
(source, raster)
|
||||
}
|
||||
kind => (
|
||||
def.file_path.clone(),
|
||||
super::underlay_vector::display_raster(kind, &def.file_path, page)?,
|
||||
super::underlay_vector::display_raster(
|
||||
kind,
|
||||
&def.file_path,
|
||||
page,
|
||||
screen_side.unwrap_or(super::underlay_vector::RASTER_SIDE),
|
||||
)?,
|
||||
),
|
||||
};
|
||||
// Dark means an HSL lightness under one half: pure blue counts as
|
||||
|
||||
@@ -75,6 +75,15 @@ pub fn rasterize_page_display(path: &str, page: &str) -> Option<Arc<PdfPage>> {
|
||||
rasterize_cached(path, page, RASTER_DPI, true)
|
||||
}
|
||||
|
||||
/// The display raster at a lower DPI for a page shown small on screen
|
||||
/// (never above the on-screen 150 DPI).
|
||||
pub fn rasterize_page_display_at(path: &str, page: &str, dpi: f32) -> Option<Arc<PdfPage>> {
|
||||
rasterize_cached(path, page, dpi.clamp(1.0, RASTER_DPI), true)
|
||||
}
|
||||
|
||||
/// The DPI of the full-size display raster.
|
||||
pub const DISPLAY_DPI: f32 = RASTER_DPI;
|
||||
|
||||
fn rasterize_cached(path: &str, page: &str, dpi: f32, transparent: bool) -> Option<Arc<PdfPage>> {
|
||||
let key = (path.to_string(), page.to_string(), dpi.to_bits(), transparent);
|
||||
if let Some(hit) = page_cache()
|
||||
@@ -228,7 +237,8 @@ pub fn adjusted_pixels(path: &str, page: &str, raster: &PdfPage, adjust: PageAdj
|
||||
if identity {
|
||||
return raster.pixels.clone();
|
||||
}
|
||||
let key = (path.to_string(), page.to_string(), adjust);
|
||||
// One entry per raster size: a page is rasterised again for the zoom.
|
||||
let key = (path.to_string(), format!("{page}@{}x{}", raster.width, raster.height), adjust);
|
||||
if let Some(hit) = adjusted_cache()
|
||||
.lock()
|
||||
.unwrap_or_else(|error| error.into_inner())
|
||||
|
||||
@@ -198,9 +198,9 @@ fn read_run(run: &[&[u8]], colors: &[[u8; 3]]) -> Raw {
|
||||
}
|
||||
let symbology = uint16(e, 34).unwrap_or(0);
|
||||
let color = colors[(symbology >> 8) as usize];
|
||||
let weight = ((symbology & 0xf8) >> 3) as f64;
|
||||
// Weights are screen pixels: a negative width asks for pixels.
|
||||
let stroke = Some((color, -(weight * 2.0 + 1.0)));
|
||||
// One pixel wide, as V8 models draw: the element weight is a
|
||||
// lineweight, not shown while lineweights are off.
|
||||
let stroke = Some((color, -1.0));
|
||||
let fill = fill_index(e).map(|i| colors[i as usize]);
|
||||
let int_pt = |i: usize| -> Option<[f64; 2]> { Some([int32(e, i)? as f64, int32(e, i + 4)? as f64]) };
|
||||
match kind {
|
||||
@@ -268,7 +268,7 @@ fn read_run(run: &[&[u8]], colors: &[[u8; 3]]) -> Raw {
|
||||
// B-spline curve header: order, pole count.
|
||||
let order = ((e.get(40).copied().unwrap_or(0) & 0x0f) + 2) as usize;
|
||||
let poles = uint16(e, 42).unwrap_or(0) as usize;
|
||||
spline = Some((order, poles, color, -(weight * 2.0 + 1.0)));
|
||||
spline = Some((order, poles, color, -1.0));
|
||||
knots.clear();
|
||||
}
|
||||
26 => {
|
||||
|
||||
@@ -108,28 +108,30 @@ fn outline_texts(sheet: &mut Sheet) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Longest side of a sheet's display raster, pixels.
|
||||
const RASTER_SIDE: f64 = 3072.0;
|
||||
/// Longest side of a sheet's full-size display raster, pixels.
|
||||
pub const RASTER_SIDE: f64 = 3072.0;
|
||||
|
||||
/// The sheet drawn on a transparent background, straight alpha, rows from
|
||||
/// the top, memoised.
|
||||
pub fn display_raster(kind: UnderlayType, path: &str, item: &str) -> Option<Arc<PdfPage>> {
|
||||
let key = (path.to_string(), item.to_string());
|
||||
/// The sheet drawn on a transparent background with `side` pixels on its
|
||||
/// longest side (at most [`RASTER_SIDE`]), straight alpha, rows from the
|
||||
/// top, memoised per size.
|
||||
pub fn display_raster(kind: UnderlayType, path: &str, item: &str, side: f64) -> Option<Arc<PdfPage>> {
|
||||
let side = side.clamp(16.0, RASTER_SIDE).round();
|
||||
let key = (path.to_string(), format!("{item}@{side}"));
|
||||
if let Some(hit) = raster_cache().lock().unwrap_or_else(|e| e.into_inner()).get(&key) {
|
||||
return hit.clone();
|
||||
}
|
||||
let value = sheet(kind, path, item).and_then(|s| rasterize(&s)).map(Arc::new);
|
||||
let value = sheet(kind, path, item).and_then(|s| rasterize(&s, side)).map(Arc::new);
|
||||
raster_cache().lock().unwrap_or_else(|e| e.into_inner()).insert(key, value.clone());
|
||||
value
|
||||
}
|
||||
|
||||
fn rasterize(sheet: &Sheet) -> Option<PdfPage> {
|
||||
fn rasterize(sheet: &Sheet, side: f64) -> Option<PdfPage> {
|
||||
let [x0, y0, x1, y1] = sheet.rect;
|
||||
let (w, h) = (x1 - x0, y1 - y0);
|
||||
if !(w > 0.0 && h > 0.0) {
|
||||
return None;
|
||||
}
|
||||
let scale = RASTER_SIDE / w.max(h);
|
||||
let scale = side / w.max(h);
|
||||
let (pw, ph) = ((w * scale).ceil().max(1.0) as u32, (h * scale).ceil().max(1.0) as u32);
|
||||
let mut pixmap = tiny_skia::Pixmap::new(pw, ph)?;
|
||||
let to_px = |p: [f64; 2]| (((p[0] - x0) * scale) as f32, ((y1 - p[1]) * scale) as f32);
|
||||
|
||||
Reference in new issue
Block a user