forked from 1b-agent/opencadstudio-upstream
DGN underlays: global origin, stored extents, colour table display, 3D cells
- V8 models are placed relative to their global origin (model header). - A V8 model's extent is the union of its elements' stored ranges; a V7 model's text box is one text height tall. - Files without a colour table use the standard DGN default table; colour 255 is read from the table's background entry. - Colours from a file's colour table follow the background: near-white colours swap with it, and over non-black backgrounds some shades drop a step. - DGN contrast pulls towards a pivot taken from the model's own colours, after the fade, which is blended into the background first. - Shared cells are read through their full 3D placement, so tilted instances and 3D definitions are drawn in plan. - Deleted V8 elements are skipped; lines along the model's extent stay in the raster. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
07f25f4bca
commit
f7106c7d30
8 files changed
+433
-154
No files matched your search
@@ -93,10 +93,11 @@ pub fn sheet_without(kind: UnderlayType, path: &str, item: &str, hidden: &[Strin
|
||||
UnderlayType::Dgn => dgn8::model(&bytes, item, hidden).unwrap_or_default(),
|
||||
UnderlayType::Pdf => return None,
|
||||
};
|
||||
// A DGN model's extent includes its text: each text's box runs its
|
||||
// length along the baseline and 1.5 × its height above it (as the
|
||||
// reference measures a model). A DWF sheet keeps its plotted view.
|
||||
if kind == UnderlayType::Dgn {
|
||||
// A V7 model's extent includes its text: each text's box runs its
|
||||
// length along the baseline and its height above it (as the
|
||||
// reference measures a model). A V8 model has its stored element
|
||||
// ranges; a DWF sheet keeps its plotted view.
|
||||
if kind == UnderlayType::Dgn && dgn7::is_v7(&bytes) {
|
||||
let mut r = sheet.rect;
|
||||
for t in &sheet.texts {
|
||||
let (strokes, _) = crate::scene::text::lff::tessellate_text_ex(
|
||||
@@ -110,13 +111,14 @@ pub fn sheet_without(kind: UnderlayType, path: &str, item: &str, hidden: &[Strin
|
||||
);
|
||||
let length = strokes.iter().flatten().map(|p| p[0] as f64).fold(0.0, f64::max);
|
||||
let (c, s) = (t.rotation.cos(), t.rotation.sin());
|
||||
for [x, y] in [[0.0, 0.0], [length, 0.0], [length, 1.5 * t.height], [0.0, 1.5 * t.height]] {
|
||||
for [x, y] in [[0.0, 0.0], [length, 0.0], [length, t.height], [0.0, t.height]] {
|
||||
let p = [t.origin[0] + x * c - y * s, t.origin[1] + x * s + y * c];
|
||||
r = [r[0].min(p[0]), r[1].min(p[1]), r[2].max(p[0]), r[3].max(p[1])];
|
||||
}
|
||||
}
|
||||
sheet.rect = r;
|
||||
}
|
||||
sheet.element_colors = element_colors(&sheet);
|
||||
outline_texts(&mut sheet);
|
||||
if !hidden.is_empty() {
|
||||
sheet.rect = sheet_without(kind, path, item, &[])?.rect;
|
||||
@@ -128,6 +130,45 @@ pub fn sheet_without(kind: UnderlayType, path: &str, item: &str, hidden: &[Strin
|
||||
value
|
||||
}
|
||||
|
||||
/// The colour of each path and text of the sheet (a path's fill before
|
||||
/// its stroke).
|
||||
fn element_colors(sheet: &Sheet) -> Vec<[u8; 3]> {
|
||||
sheet
|
||||
.paths
|
||||
.iter()
|
||||
.filter_map(|p| p.fill.or(p.stroke.map(|s| s.0)))
|
||||
.chain(sheet.texts.iter().map(|t| t.color))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// The lightness a DGN model's contrast pulls its colours towards, in
|
||||
/// thousandths. The reference takes it from the model's own colours as
|
||||
/// shown (over `backdrop`, faded by `fade` into the background): about
|
||||
/// 0.788 × their mean lightness, each element counted once (0.788 for an
|
||||
/// all-white model, 0.416 for mid grey, 0.09 for dark red).
|
||||
pub fn dgn_contrast_pivot(
|
||||
path: &str,
|
||||
item: &str,
|
||||
hidden: &[String],
|
||||
backdrop: Backdrop,
|
||||
fade: Option<([u8; 3], u8)>,
|
||||
) -> Option<u16> {
|
||||
let sheet = sheet_without(UnderlayType::Dgn, path, item, hidden)?;
|
||||
if sheet.element_colors.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let lightness = |c: [u8; 3]| {
|
||||
let c = if sheet.table_colors { model::dgn_table_color(c, backdrop.max_channel, backdrop.light) } else { c };
|
||||
let c = match fade {
|
||||
Some((bg, f)) => [0, 1, 2].map(|i| c[i] as f64 + (bg[i] as f64 - c[i] as f64) * f.min(100) as f64 / 100.0),
|
||||
None => c.map(|v| v as f64),
|
||||
};
|
||||
(c[0].max(c[1]).max(c[2]) + c[0].min(c[1]).min(c[2])) / 510.0
|
||||
};
|
||||
let mean = sheet.element_colors.iter().map(|&c| lightness(c)).sum::<f64>() / sheet.element_colors.len() as f64;
|
||||
Some((0.788 * mean * 1000.0).round() as u16)
|
||||
}
|
||||
|
||||
/// Text as strokes (and fills for outline fonts), in the text's colour.
|
||||
fn outline_texts(sheet: &mut Sheet) {
|
||||
for t in std::mem::take(&mut sheet.texts) {
|
||||
@@ -165,21 +206,64 @@ fn outline_texts(sheet: &mut Sheet) {
|
||||
/// Longest side of a sheet's full-size display raster, pixels.
|
||||
pub const RASTER_SIDE: f64 = 3072.0;
|
||||
|
||||
/// The background a sheet is shown over, as far as its colours depend on
|
||||
/// it: the largest channel (in the steps a DGN colour table changes at) and
|
||||
/// whether the background is light.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct Backdrop {
|
||||
pub max_channel: u8,
|
||||
pub light: bool,
|
||||
}
|
||||
|
||||
impl Backdrop {
|
||||
pub fn of(rgb: [f32; 3]) -> Self {
|
||||
let max = rgb[0].max(rgb[1]).max(rgb[2]);
|
||||
let min = rgb[0].min(rgb[1]).min(rgb[2]);
|
||||
let top = (max.clamp(0.0, 1.0) * 255.0).round() as u8;
|
||||
Backdrop {
|
||||
max_channel: match top {
|
||||
0..=29 => 0,
|
||||
30..=31 => 30,
|
||||
_ => 32,
|
||||
},
|
||||
// Light means an HSL lightness of one half or more.
|
||||
light: (max + min) / 2.0 >= 0.5,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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, hidden: &[String], side: f64) -> Option<Arc<PdfPage>> {
|
||||
/// top, memoised per size and backdrop.
|
||||
pub fn display_raster(
|
||||
kind: UnderlayType,
|
||||
path: &str,
|
||||
item: &str,
|
||||
hidden: &[String],
|
||||
side: f64,
|
||||
backdrop: Backdrop,
|
||||
) -> Option<Arc<PdfPage>> {
|
||||
let side = side.clamp(16.0, RASTER_SIDE).round();
|
||||
let key = (path.to_string(), format!("{}@{side}", layered_item(item, hidden)));
|
||||
let key = (
|
||||
path.to_string(),
|
||||
format!("{}@{side}~{}{}", layered_item(item, hidden), backdrop.max_channel, backdrop.light),
|
||||
);
|
||||
if let Some(hit) = raster_cache().lock().unwrap_or_else(|e| e.into_inner()).get(&key) {
|
||||
return hit.clone();
|
||||
}
|
||||
let value = sheet_without(kind, path, item, hidden).and_then(|s| rasterize(&s, side)).map(Arc::new);
|
||||
let value = sheet_without(kind, path, item, hidden).and_then(|s| rasterize(&s, side, backdrop)).map(Arc::new);
|
||||
raster_cache().lock().unwrap_or_else(|e| e.into_inner()).insert(key, value.clone());
|
||||
value
|
||||
}
|
||||
|
||||
fn rasterize(sheet: &Sheet, side: f64) -> Option<PdfPage> {
|
||||
fn rasterize(sheet: &Sheet, side: f64, backdrop: Backdrop) -> Option<PdfPage> {
|
||||
let shown = |c: [u8; 3]| {
|
||||
if sheet.table_colors {
|
||||
model::dgn_table_color(c, backdrop.max_channel, backdrop.light)
|
||||
} else {
|
||||
c
|
||||
}
|
||||
};
|
||||
let [x0, y0, x1, y1] = sheet.rect;
|
||||
let (w, h) = (x1 - x0, y1 - y0);
|
||||
if !(w > 0.0 && h > 0.0) {
|
||||
@@ -188,7 +272,10 @@ fn rasterize(sheet: &Sheet, side: f64) -> Option<PdfPage> {
|
||||
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);
|
||||
// The rectangle's edges fall on the centres of the outer pixels, so a
|
||||
// line along the edge of the extent (every extent has some) is drawn.
|
||||
let (sx, sy) = ((pw.max(2) - 1) as f64 / w, (ph.max(2) - 1) as f64 / h);
|
||||
let to_px = |p: [f64; 2]| (((p[0] - x0) * sx + 0.5) as f32, ((y1 - p[1]) * sy + 0.5) as f32);
|
||||
for path in &sheet.paths {
|
||||
let mut pb = tiny_skia::PathBuilder::new();
|
||||
for sp in &path.subpaths {
|
||||
@@ -212,14 +299,14 @@ fn rasterize(sheet: &Sheet, side: f64) -> Option<PdfPage> {
|
||||
}
|
||||
}
|
||||
let Some(p) = pb.finish() else { continue };
|
||||
if let Some(c) = path.fill {
|
||||
if let Some(c) = path.fill.map(shown) {
|
||||
let mut paint = tiny_skia::Paint::default();
|
||||
paint.set_color_rgba8(c[0], c[1], c[2], 255);
|
||||
// Fills that share an edge (triangle strips) would show a seam.
|
||||
paint.anti_alias = false;
|
||||
pixmap.fill_path(&p, &paint, tiny_skia::FillRule::Winding, tiny_skia::Transform::identity(), None);
|
||||
}
|
||||
if let Some((c, width)) = path.stroke {
|
||||
if let Some((c, width)) = path.stroke.map(|(c, w)| (shown(c), w)) {
|
||||
let mut paint = tiny_skia::Paint::default();
|
||||
paint.set_color_rgba8(c[0], c[1], c[2], 255);
|
||||
// A negative width is in pixels (DGN line weights).
|
||||
|
||||
Reference in new issue
Block a user