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 <[email protected]>
This commit is contained in:
1 parent
07f25f4bca
commit
f7106c7d30
8 files changed
+433
-154
No files matched your search
@@ -231,19 +231,23 @@ impl ImageModel {
|
||||
}
|
||||
kind => {
|
||||
let hidden = super::pdf_layers::hidden_layers(u);
|
||||
let backdrop = super::underlay_vector::Backdrop::of([background[0], background[1], background[2]]);
|
||||
// The adjusted-pixel memo keys on the source: one per set of
|
||||
// hidden layers.
|
||||
let source = if hidden.is_empty() {
|
||||
def.file_path.clone()
|
||||
} else {
|
||||
format!("{}#{}", def.file_path, hidden.join("|"))
|
||||
};
|
||||
// hidden layers and backdrop.
|
||||
let source = format!(
|
||||
"{}#{}~{}{}",
|
||||
def.file_path,
|
||||
hidden.join("|"),
|
||||
backdrop.max_channel,
|
||||
backdrop.light
|
||||
);
|
||||
let raster = super::underlay_vector::display_raster(
|
||||
kind,
|
||||
&def.file_path,
|
||||
page,
|
||||
&hidden,
|
||||
screen_side.unwrap_or(super::underlay_vector::RASTER_SIDE),
|
||||
backdrop,
|
||||
)?;
|
||||
(source, raster)
|
||||
}
|
||||
@@ -253,6 +257,11 @@ impl ImageModel {
|
||||
let bg_max = background[0].max(background[1]).max(background[2]);
|
||||
let bg_min = background[0].min(background[1]).min(background[2]);
|
||||
let bg_lum = (bg_max + bg_min) / 2.0;
|
||||
let dgn = def.underlay_type == UnderlayType::Dgn;
|
||||
// A DGN model is faded into the background before its contrast is
|
||||
// applied (the reference's order), so it is drawn opaque.
|
||||
let bg_rgb = [0, 1, 2].map(|i| (background[i].clamp(0.0, 1.0) * 255.0).round() as u8);
|
||||
let fade_before_contrast = (dgn && u.fade > 0).then_some((bg_rgb, u.fade.min(100)));
|
||||
let pixels = pdf_raster::adjusted_pixels(
|
||||
&source,
|
||||
page,
|
||||
@@ -260,7 +269,8 @@ impl ImageModel {
|
||||
PageAdjust {
|
||||
contrast: u.contrast.min(100),
|
||||
// Measured: PDF 0.5; DWF 0.243, or 0.73 once its colours are
|
||||
// turned over for a dark background; DGN 0.65.
|
||||
// turned over for a dark background; a DGN model's from its
|
||||
// own colours, over any background.
|
||||
contrast_pivot: match def.underlay_type {
|
||||
UnderlayType::Pdf => 500,
|
||||
UnderlayType::Dwf
|
||||
@@ -269,14 +279,22 @@ impl ImageModel {
|
||||
730
|
||||
}
|
||||
UnderlayType::Dwf => 243,
|
||||
UnderlayType::Dgn => 650,
|
||||
UnderlayType::Dgn => super::underlay_vector::dgn_contrast_pivot(
|
||||
&def.file_path,
|
||||
page,
|
||||
&super::pdf_layers::hidden_layers(u),
|
||||
super::underlay_vector::Backdrop::of([background[0], background[1], background[2]]),
|
||||
fade_before_contrast,
|
||||
)
|
||||
.unwrap_or(404),
|
||||
},
|
||||
monochrome: u.flags.contains(UnderlayDisplayFlags::MONOCHROME),
|
||||
adjust_for_background: u.flags.contains(UnderlayDisplayFlags::ADJUST_FOR_BACKGROUND),
|
||||
// A DGN model is drawn for a black background, so its
|
||||
// colours turn over on a light one instead (white text stays
|
||||
// light on a dark background, as the reference shows it).
|
||||
dark_background: (bg_lum < 0.5) != (def.underlay_type == UnderlayType::Dgn),
|
||||
// A DGN model's colours follow the background through its
|
||||
// colour table (the raster above), whatever this setting.
|
||||
adjust_for_background: u.flags.contains(UnderlayDisplayFlags::ADJUST_FOR_BACKGROUND)
|
||||
&& !dgn,
|
||||
dark_background: bg_lum < 0.5,
|
||||
fade_before_contrast,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -329,7 +347,7 @@ impl ImageModel {
|
||||
pixels,
|
||||
width: raster.width,
|
||||
height: raster.height,
|
||||
opacity: 1.0 - u.fade.min(100) as f32 / 100.0,
|
||||
opacity: if dgn { 1.0 } else { 1.0 - u.fade.min(100) as f32 / 100.0 },
|
||||
corners,
|
||||
corners_low,
|
||||
draw_depth: 0.0,
|
||||
|
||||
@@ -217,13 +217,18 @@ pub struct PageAdjust {
|
||||
/// towards `contrast_pivot` (hue and saturation kept).
|
||||
pub contrast: u8,
|
||||
/// The lightness contrast 0 leaves every colour at, in thousandths:
|
||||
/// measured 500 for PDF, 243 for DWF and 650 for DGN underlays.
|
||||
/// measured 500 for PDF and 243 for DWF; a DGN model's follows its
|
||||
/// colours.
|
||||
pub contrast_pivot: u16,
|
||||
pub monochrome: bool,
|
||||
/// Lighten dark, unsaturated content on a dark background (black lines
|
||||
/// and text become white), keeping coloured content.
|
||||
pub adjust_for_background: bool,
|
||||
pub dark_background: bool,
|
||||
/// A fade blended into the colours before the contrast (a DGN
|
||||
/// model's): the background and the share, 0..=100, of the way each
|
||||
/// colour is taken to it.
|
||||
pub fade_before_contrast: Option<([u8; 3], u8)>,
|
||||
}
|
||||
|
||||
type AdjustKey = (String, String, PageAdjust);
|
||||
@@ -236,7 +241,7 @@ fn adjusted_cache() -> &'static Mutex<HashMap<AdjustKey, Arc<Vec<u8>>>> {
|
||||
/// The display raster with the underlay's contrast / monochrome / background
|
||||
/// adjustments applied, memoised per source, page and adjustment.
|
||||
pub fn adjusted_pixels(path: &str, page: &str, raster: &PdfPage, adjust: PageAdjust) -> Arc<Vec<u8>> {
|
||||
let identity = adjust.contrast >= 100 && !adjust.monochrome && !adjust.adjust_for_background;
|
||||
let identity = adjust.contrast >= 100 && !adjust.monochrome && !adjust.adjust_for_background && adjust.fade_before_contrast.is_none();
|
||||
if identity {
|
||||
return raster.pixels.clone();
|
||||
}
|
||||
@@ -268,6 +273,12 @@ pub fn adjusted_pixels(path: &str, page: &str, raster: &PdfPage, adjust: PageAdj
|
||||
let (h, s, l) = rgb_to_hsl(rgb);
|
||||
rgb = hsl_to_rgb(h, s, (1.0 - l + 20.0 / 255.0).min(1.0));
|
||||
}
|
||||
if let Some((bg, fade)) = adjust.fade_before_contrast {
|
||||
let f = fade.min(100) as f32 / 100.0;
|
||||
for (c, b) in rgb.iter_mut().zip(bg) {
|
||||
*c += (b as f32 / 255.0 - *c) * f;
|
||||
}
|
||||
}
|
||||
if adjust.contrast < 100 {
|
||||
// Lightness is pulled towards the pivot, hue and saturation kept:
|
||||
// contrast 0 leaves every colour at the pivot's lightness.
|
||||
@@ -276,6 +287,7 @@ pub fn adjusted_pixels(path: &str, page: &str, raster: &PdfPage, adjust: PageAdj
|
||||
let pivot = adjust.contrast_pivot as f32 / 1000.0;
|
||||
rgb = hsl_to_rgb(h, s, pivot + (l - pivot) * k);
|
||||
}
|
||||
|
||||
for (dst, c) in px[..3].iter_mut().zip(rgb) {
|
||||
*dst = (c.clamp(0.0, 1.0) * 255.0).round() as u8;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use super::model::{arc_cubics, bspline_points, paths_bounds, Path, PathBuilder, Segment, Sheet, SubPath, Text};
|
||||
use super::model::{arc_cubics, bspline_points, dgn_color_table, paths_bounds, Path, PathBuilder, Segment, Sheet, SubPath, Text, DGN_DEFAULT_COLORS};
|
||||
|
||||
/// A 32-bit integer stored as two little-endian words, high word first.
|
||||
fn int32(b: &[u8], at: usize) -> Option<i32> {
|
||||
@@ -76,36 +76,13 @@ fn elements(b: &[u8]) -> Vec<&[u8]> {
|
||||
out
|
||||
}
|
||||
|
||||
/// The file's colour table (element type 5 on level 1): the colour of
|
||||
/// index i is at 41 + 3i.
|
||||
/// The file's colour table (element type 5 on level 1): index i < 255 at
|
||||
/// 41 + 3i, the background (255) at 38.
|
||||
fn color_table(elements: &[&[u8]]) -> Option<Vec<[u8; 3]>> {
|
||||
elements.iter().find_map(|e| {
|
||||
(e[1] & 0x7f == 5 && e[0] & 0x3f == 1 && e.len() >= 41 + 765).then(|| {
|
||||
(0..256)
|
||||
.map(|i| {
|
||||
let at = 41 + i * 3;
|
||||
e.get(at..at + 3).map(|c| [c[0], c[1], c[2]]).unwrap_or([255, 255, 255])
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// A usable default: the first eight DGN colours, then greys.
|
||||
// ponytail: The full DGN default table is not reproduced; the files
|
||||
// the reference writes carry their own colour table.
|
||||
fn default_colors() -> Vec<[u8; 3]> {
|
||||
let base: [[u8; 3]; 8] = [
|
||||
[255, 255, 255],
|
||||
[0, 0, 255],
|
||||
[0, 255, 0],
|
||||
[255, 0, 0],
|
||||
[255, 255, 0],
|
||||
[255, 0, 255],
|
||||
[255, 127, 0],
|
||||
[0, 255, 255],
|
||||
];
|
||||
(0..256).map(|i| if i < 8 { base[i] } else { [i as u8; 3] }).collect()
|
||||
elements
|
||||
.iter()
|
||||
.find(|e| e[1] == 5 && e[0] & 0x3f == 1)
|
||||
.and_then(|e| dgn_color_table(e, 41))
|
||||
}
|
||||
|
||||
/// Model names of a V7 file (it has one).
|
||||
@@ -432,7 +409,9 @@ pub fn model(bytes: &[u8], hidden: &[String]) -> Option<Sheet> {
|
||||
}
|
||||
let u = units(bytes)?;
|
||||
let all = elements(bytes);
|
||||
let colors = color_table(&all).unwrap_or_else(default_colors);
|
||||
let table = color_table(&all);
|
||||
let table_colors = table.is_some();
|
||||
let colors = table.unwrap_or_else(|| DGN_DEFAULT_COLORS.to_vec());
|
||||
let names = level_names(&all);
|
||||
let off = |e: &[u8]| {
|
||||
let l = level(e);
|
||||
@@ -463,7 +442,7 @@ pub fn model(bytes: &[u8], hidden: &[String]) -> Option<Sheet> {
|
||||
let raw = read_run(&top, &colors);
|
||||
let per_unit = u.uor_per_sub * u.sub_per_master;
|
||||
let m: Affine = [1.0 / per_unit, 0.0, 0.0, 1.0 / per_unit, -u.origin[0] / per_unit, -u.origin[1] / per_unit];
|
||||
let mut sheet = Sheet { sub_per_master: u.sub_per_master, ..Default::default() };
|
||||
let mut sheet = Sheet { sub_per_master: u.sub_per_master, table_colors, ..Default::default() };
|
||||
place(&raw, &m, &defs, 0, &mut sheet);
|
||||
sheet.rect = paths_bounds(&sheet.paths).or_else(|| {
|
||||
let t = sheet.texts.first()?;
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
use std::collections::HashMap;
|
||||
use std::io::Read;
|
||||
|
||||
use super::model::{arc_cubics, paths_bounds, Path, PathBuilder, Segment, Sheet, SubPath, Text};
|
||||
use super::model::{arc_cubics, dgn_color_table, paths_bounds, Path, PathBuilder, Segment, Sheet, SubPath, Text, DGN_DEFAULT_COLORS};
|
||||
|
||||
// ── Compound file ───────────────────────────────────────────────────────────
|
||||
|
||||
@@ -188,7 +188,9 @@ fn elements(d: &[u8]) -> Vec<&[u8]> {
|
||||
// The stream can end inside its last element (its trailing words are
|
||||
// not stored); what is there is still read.
|
||||
let e = &d[at..(at + len).min(d.len())];
|
||||
out.push(e);
|
||||
if !is_deleted(e) {
|
||||
out.push(e);
|
||||
}
|
||||
at += len;
|
||||
}
|
||||
out
|
||||
@@ -202,6 +204,12 @@ fn flags(e: &[u8]) -> u16 {
|
||||
u16::from_le_bytes([e[2], e[3]])
|
||||
}
|
||||
|
||||
/// A deleted element (flag 0x80 of its second byte), kept in the stream but
|
||||
/// no longer part of the file.
|
||||
fn is_deleted(e: &[u8]) -> bool {
|
||||
e[1] & 0x80 != 0
|
||||
}
|
||||
|
||||
/// A component of a complex element, cell or shared cell definition.
|
||||
fn is_component(e: &[u8]) -> bool {
|
||||
flags(e) & 0x4000 != 0
|
||||
@@ -290,7 +298,7 @@ impl Styles {
|
||||
}
|
||||
}
|
||||
|
||||
/// Geometry in raw UORs, placed through a transform.
|
||||
/// Plan geometry in UORs, and the shared cells it places.
|
||||
#[derive(Default, Clone)]
|
||||
struct Raw {
|
||||
paths: Vec<Path>,
|
||||
@@ -301,8 +309,31 @@ struct Raw {
|
||||
#[derive(Clone)]
|
||||
struct Instance {
|
||||
name: String,
|
||||
m: [f64; 4],
|
||||
origin: [f64; 2],
|
||||
/// The definition's placement, with every enclosing one applied.
|
||||
xf: Xf,
|
||||
}
|
||||
|
||||
/// A placement in UORs: x' = r[0]·(x, y, z, 1), y' = r[1]·…, z' = r[2]·….
|
||||
type Xf = [[f64; 4]; 3];
|
||||
|
||||
const IDENTITY: Xf = [[1.0, 0.0, 0.0, 0.0], [0.0, 1.0, 0.0, 0.0], [0.0, 0.0, 1.0, 0.0]];
|
||||
|
||||
fn xf_vector(m: &Xf, v: [f64; 3]) -> [f64; 3] {
|
||||
m.map(|r| r[0] * v[0] + r[1] * v[1] + r[2] * v[2])
|
||||
}
|
||||
|
||||
fn xf_point(m: &Xf, p: [f64; 3]) -> [f64; 3] {
|
||||
m.map(|r| r[0] * p[0] + r[1] * p[1] + r[2] * p[2] + r[3])
|
||||
}
|
||||
|
||||
fn xf_compose(outer: &Xf, inner: &Xf) -> Xf {
|
||||
let mut out = [[0.0; 4]; 3];
|
||||
for (i, row) in out.iter_mut().enumerate() {
|
||||
for (j, v) in row.iter_mut().enumerate() {
|
||||
*v = (0..3).map(|k| outer[i][k] * inner[k][j]).sum::<f64>() + if j == 3 { outer[i][3] } else { 0.0 };
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// A 3D element (flag 0x0800 of its properties word): three coordinates a
|
||||
@@ -311,17 +342,22 @@ fn is_3d(e: &[u8]) -> bool {
|
||||
u32_at(e, 40).is_some_and(|v| v & 0x0800 != 0)
|
||||
}
|
||||
|
||||
/// The in-plane x and y axes (projected onto the plan) of a 3D element's
|
||||
/// orientation quaternion w, x, y, z stored at `at` (the rows of its
|
||||
/// rotation matrix).
|
||||
fn quat_axes(e: &[u8], at: usize) -> Option<([f64; 2], [f64; 2])> {
|
||||
/// The in-plane x and y axes of a 3D element's orientation quaternion w, x,
|
||||
/// y, z stored at `at` (the rows of its rotation matrix).
|
||||
fn quat_axes(e: &[u8], at: usize) -> Option<([f64; 3], [f64; 3])> {
|
||||
let (w, x, y, z) = (f64_at(e, at)?, f64_at(e, at + 8)?, f64_at(e, at + 16)?, f64_at(e, at + 24)?);
|
||||
Some((
|
||||
[1.0 - 2.0 * (y * y + z * z), 2.0 * (x * y - w * z)],
|
||||
[2.0 * (x * y + w * z), 1.0 - 2.0 * (x * x + z * z)],
|
||||
[1.0 - 2.0 * (y * y + z * z), 2.0 * (x * y - w * z), 2.0 * (x * z + w * y)],
|
||||
[2.0 * (x * y + w * z), 1.0 - 2.0 * (x * x + z * z), 2.0 * (y * z - w * x)],
|
||||
))
|
||||
}
|
||||
|
||||
/// The in-plane axes of a 2D element turned by `rotation` (radians).
|
||||
fn turned_axes(rotation: f64) -> ([f64; 3], [f64; 3]) {
|
||||
let (c, s) = (rotation.cos(), rotation.sin());
|
||||
([c, s, 0.0], [-s, c, 0.0])
|
||||
}
|
||||
|
||||
/// An elliptical arc on axes `u` and `v` (plan projections) as cubics.
|
||||
fn projected_arc(c: [f64; 2], a: f64, b: f64, u: [f64; 2], v: [f64; 2], start: f64, sweep: f64) -> Vec<Segment> {
|
||||
let map = |p: [f64; 2]| [c[0] + a * p[0] * u[0] + b * p[1] * v[0], c[1] + a * p[0] * u[1] + b * p[1] * v[1]];
|
||||
@@ -331,7 +367,8 @@ fn projected_arc(c: [f64; 2], a: f64, b: f64, u: [f64; 2], v: [f64; 2], start: f
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn read_run(run: &[&[u8]], styles: &Styles) -> Raw {
|
||||
/// Reads a run of elements placed through `xf` into plan geometry.
|
||||
fn read_run(run: &[&[u8]], styles: &Styles, xf: &Xf) -> Raw {
|
||||
let mut raw = Raw::default();
|
||||
// A complex shape or chain collects its components into one path.
|
||||
let mut complex: Option<(usize, PathBuilder, Option<([u8; 3], f64)>, Option<[u8; 3]>, bool)> = None;
|
||||
@@ -363,14 +400,25 @@ fn read_run(run: &[&[u8]], styles: &Styles) -> Raw {
|
||||
raw.paths.push(path);
|
||||
}
|
||||
}
|
||||
let plan = |p: [f64; 3]| {
|
||||
let q = xf_point(xf, p);
|
||||
[q[0], q[1]]
|
||||
};
|
||||
let axis = |v: [f64; 3]| {
|
||||
let q = xf_vector(xf, v);
|
||||
[q[0], q[1]]
|
||||
};
|
||||
for e in run {
|
||||
if e.len() < 104 || styles.hidden.contains(&u32_at(e, 12).unwrap_or(0)) {
|
||||
continue;
|
||||
}
|
||||
let color = styles.color(e);
|
||||
let stroke = Some((color, -1.0));
|
||||
let p2 = |at: usize| -> Option<[f64; 2]> { Some([f64_at(e, at)?, f64_at(e, at + 8)?]) };
|
||||
let d3 = is_3d(e);
|
||||
// A point: x, y (and z in a 3D element).
|
||||
let p3 = |at: usize| -> Option<[f64; 3]> {
|
||||
Some([f64_at(e, at)?, f64_at(e, at + 8)?, if d3 { f64_at(e, at + 16)? } else { 0.0 }])
|
||||
};
|
||||
match kind(e) {
|
||||
12 | 14 => {
|
||||
let n = u32_at(e, 104).unwrap_or(0) as usize;
|
||||
@@ -380,14 +428,14 @@ fn read_run(run: &[&[u8]], styles: &Styles) -> Raw {
|
||||
}
|
||||
}
|
||||
3 => {
|
||||
if let (Some(a), Some(b)) = (p2(104), p2(if d3 { 128 } else { 120 })) {
|
||||
push(&mut raw, &mut complex, Path { subpaths: vec![SubPath { segments: vec![Segment::Line(a, b)], closed: false }], stroke, fill: None });
|
||||
if let (Some(a), Some(b)) = (p3(104), p3(if d3 { 128 } else { 120 })) {
|
||||
push(&mut raw, &mut complex, Path { subpaths: vec![SubPath { segments: vec![Segment::Line(plan(a), plan(b))], closed: false }], stroke, fill: None });
|
||||
}
|
||||
}
|
||||
4 | 6 | 11 => {
|
||||
let n = u32_at(e, 104).unwrap_or(0) as usize;
|
||||
let stride = if d3 { 24 } else { 16 };
|
||||
let pts: Vec<[f64; 2]> = (0..n).filter_map(|k| p2(112 + k * stride)).collect();
|
||||
let pts: Vec<[f64; 2]> = (0..n).filter_map(|k| p3(112 + k * stride)).map(plan).collect();
|
||||
if pts.len() >= 2 {
|
||||
let mut pb = PathBuilder::default();
|
||||
pb.move_to(pts[0]);
|
||||
@@ -400,14 +448,16 @@ fn read_run(run: &[&[u8]], styles: &Styles) -> Raw {
|
||||
push(&mut raw, &mut complex, Path { subpaths: pb.finish(), stroke, fill: None });
|
||||
}
|
||||
}
|
||||
15 | 16 if d3 => {
|
||||
// Axes, orientation and centre: ellipse a 104, b 112,
|
||||
15 | 16 => {
|
||||
// Radii, orientation and centre. 3D: ellipse a 104, b 112,
|
||||
// quaternion 120, centre 152; arc start 104, sweep 112, a 120,
|
||||
// b 128, quaternion 136, centre 168.
|
||||
let arc = if kind(e) == 15 {
|
||||
(|| Some((f64_at(e, 104)?, f64_at(e, 112)?, quat_axes(e, 120)?, p2(152)?, 0.0, std::f64::consts::TAU)))()
|
||||
} else {
|
||||
(|| Some((f64_at(e, 120)?, f64_at(e, 128)?, quat_axes(e, 136)?, p2(168)?, f64_at(e, 104)?, f64_at(e, 112)?)))()
|
||||
// b 128, quaternion 136, centre 168. 2D: the orientation is an
|
||||
// angle, ellipse centre 128, arc centre 144.
|
||||
let arc = match (kind(e), d3) {
|
||||
(15, true) => (|| Some((f64_at(e, 104)?, f64_at(e, 112)?, quat_axes(e, 120)?, p3(152)?, 0.0, 0.0)))(),
|
||||
(_, true) => (|| Some((f64_at(e, 120)?, f64_at(e, 128)?, quat_axes(e, 136)?, p3(168)?, f64_at(e, 104)?, f64_at(e, 112)?)))(),
|
||||
(15, false) => (|| Some((f64_at(e, 104)?, f64_at(e, 112)?, turned_axes(f64_at(e, 120)?), p3(128)?, 0.0, 0.0)))(),
|
||||
_ => (|| Some((f64_at(e, 120)?, f64_at(e, 128)?, turned_axes(f64_at(e, 136)?), p3(144)?, f64_at(e, 104)?, f64_at(e, 112)?)))(),
|
||||
};
|
||||
let Some((a, b, (u, v), c, start, sweep)) = arc else { continue };
|
||||
let sweep = if sweep == 0.0 { std::f64::consts::TAU } else { sweep };
|
||||
@@ -415,60 +465,52 @@ fn read_run(run: &[&[u8]], styles: &Styles) -> Raw {
|
||||
push(
|
||||
&mut raw,
|
||||
&mut complex,
|
||||
Path { subpaths: vec![SubPath { segments: projected_arc(c, a, b, u, v, start, sweep), closed }], stroke, fill: None },
|
||||
Path { subpaths: vec![SubPath { segments: projected_arc(plan(c), a, b, axis(u), axis(v), start, sweep), closed }], stroke, fill: None },
|
||||
);
|
||||
}
|
||||
15 | 16 => {
|
||||
let arc = if kind(e) == 15 {
|
||||
(|| Some((f64_at(e, 104)?, f64_at(e, 112)?, f64_at(e, 120)?, p2(128)?, 0.0, std::f64::consts::TAU)))()
|
||||
17 => {
|
||||
// 3D: width 112, height 120, quaternion 144, origin 176, the
|
||||
// characters after the string marker. 2D: angle 144, origin
|
||||
// 152, a byte count (with its 4-byte marker) at 110 and the
|
||||
// characters from 174.
|
||||
let placed = if d3 {
|
||||
(|| Some((f64_at(e, 112)?, f64_at(e, 120)?, quat_axes(e, 144)?, p3(176)?, marked_string(e, 200)?)))()
|
||||
} else {
|
||||
(|| Some((f64_at(e, 120)?, f64_at(e, 128)?, f64_at(e, 136)?, p2(144)?, f64_at(e, 104)?, f64_at(e, 112)?)))()
|
||||
(|| {
|
||||
let count = u16::from_le_bytes([e[110], e[111]]) as usize;
|
||||
let chars = e.get(174..170 + count.max(4))?;
|
||||
let text = String::from_utf8_lossy(chars).trim_end_matches(char::from(0)).to_string();
|
||||
Some((f64_at(e, 112)?, f64_at(e, 120)?, turned_axes(f64_at(e, 144)?), p3(152)?, text))
|
||||
})()
|
||||
};
|
||||
let Some((a, b, rot, c, start, sweep)) = arc else { continue };
|
||||
let pieces = arc_cubics(c, a, b, rot, start, if sweep == 0.0 { std::f64::consts::TAU } else { sweep });
|
||||
let closed = sweep.abs() >= std::f64::consts::TAU - 1e-9 || sweep == 0.0;
|
||||
push(
|
||||
&mut raw,
|
||||
&mut complex,
|
||||
Path { subpaths: vec![SubPath { segments: pieces.into_iter().map(|[a, b, c, d]| Segment::Cubic(a, b, c, d)).collect(), closed }], stroke, fill: None },
|
||||
);
|
||||
}
|
||||
17 if d3 => {
|
||||
// Width 112, height 120, quaternion 144, origin 176; the
|
||||
// characters follow the string marker.
|
||||
let (Some(width), Some(height), Some((u, _)), Some(origin)) = (f64_at(e, 112), f64_at(e, 120), quat_axes(e, 144), p2(176)) else {
|
||||
let Some((width, height, (u, v), origin, text)) = placed else { continue };
|
||||
// The text's axes as the plan shows them: its baseline sets the
|
||||
// angle, and each axis's plan length scales its size.
|
||||
let (pu, pv) = (axis(u), axis(v));
|
||||
let (su, sv) = (pu[0].hypot(pu[1]), pv[0].hypot(pv[1]));
|
||||
if su < 1e-9 || sv < 1e-9 {
|
||||
continue;
|
||||
};
|
||||
let Some(text) = marked_string(e, 200) else { continue };
|
||||
let (height, width) = (height * 6.0 / 1000.0, width * 6.0 / 1000.0);
|
||||
}
|
||||
let (height, width) = (height * 6.0 / 1000.0 * sv, width * 6.0 / 1000.0 * su);
|
||||
raw.texts.push(Text {
|
||||
text,
|
||||
origin,
|
||||
origin: plan(origin),
|
||||
height,
|
||||
width_factor: if height > 0.0 { width / height } else { 1.0 },
|
||||
rotation: u[1].atan2(u[0]),
|
||||
rotation: pu[1].atan2(pu[0]),
|
||||
color,
|
||||
font: "txt".to_string(),
|
||||
});
|
||||
}
|
||||
17 => {
|
||||
let (Some(width), Some(height), Some(rotation), Some(origin)) = (f64_at(e, 112), f64_at(e, 120), f64_at(e, 144), p2(152)) else {
|
||||
continue;
|
||||
};
|
||||
// The text: a byte count (with its 4-byte marker) and the
|
||||
// characters after the marker.
|
||||
let count = u16::from_le_bytes([e[110], e[111]]) as usize;
|
||||
let Some(chars) = e.get(174..170 + count.max(4)) else { continue };
|
||||
let text = String::from_utf8_lossy(chars).trim_end_matches(char::from(0)).to_string();
|
||||
let (height, width) = (height * 6.0 / 1000.0, width * 6.0 / 1000.0);
|
||||
raw.texts.push(Text { text, origin, height, width_factor: if height > 0.0 { width / height } else { 1.0 }, rotation, color, font: "txt".to_string() });
|
||||
}
|
||||
35 => {
|
||||
let (Some(m0), Some(m1), Some(m3), Some(m4), Some(origin)) = (f64_at(e, 160), f64_at(e, 168), f64_at(e, 184), f64_at(e, 192), p2(232)) else {
|
||||
continue;
|
||||
};
|
||||
// Shared cell instance: a 3×3 matrix by rows from 160, the
|
||||
// origin at 232 (z at 248 in 3D), the definition's name after
|
||||
// the string marker.
|
||||
let m: Option<Vec<f64>> = (0..9).map(|k| f64_at(e, 160 + k * 8)).collect();
|
||||
let (Some(m), Some(o)) = (m, p3(232)) else { continue };
|
||||
let Some(name) = marked_string(e, 248) else { continue };
|
||||
raw.instances.push(Instance { name: name.to_ascii_uppercase(), m: [m0, m1, m3, m4], origin });
|
||||
let local: Xf = [[m[0], m[1], m[2], o[0]], [m[3], m[4], m[5], o[1]], [m[6], m[7], m[8], o[2]]];
|
||||
raw.instances.push(Instance { name: name.to_ascii_uppercase(), xf: xf_compose(xf, &local) });
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
@@ -489,18 +531,9 @@ fn apply(m: &Affine, p: [f64; 2]) -> [f64; 2] {
|
||||
[m[0] * p[0] + m[1] * p[1] + m[4], m[2] * p[0] + m[3] * p[1] + m[5]]
|
||||
}
|
||||
|
||||
fn compose(o: &Affine, i: &Affine) -> Affine {
|
||||
[
|
||||
o[0] * i[0] + o[1] * i[2],
|
||||
o[0] * i[1] + o[1] * i[3],
|
||||
o[2] * i[0] + o[3] * i[2],
|
||||
o[2] * i[1] + o[3] * i[3],
|
||||
o[0] * i[4] + o[1] * i[5] + o[4],
|
||||
o[2] * i[4] + o[3] * i[5] + o[5],
|
||||
]
|
||||
}
|
||||
|
||||
fn place(raw: &Raw, m: &Affine, defs: &HashMap<String, Raw>, depth: usize, out: &mut Sheet) {
|
||||
/// Places plan geometry through the sheet mapping `m`, and the shared cells
|
||||
/// it instances (each definition read through its own placement).
|
||||
fn place(raw: &Raw, m: &Affine, defs: &HashMap<String, Vec<&[u8]>>, styles: &Styles, depth: usize, out: &mut Sheet) {
|
||||
let map = |p: [f64; 2]| apply(m, p);
|
||||
for path in &raw.paths {
|
||||
out.paths.push(Path {
|
||||
@@ -533,8 +566,7 @@ fn place(raw: &Raw, m: &Affine, defs: &HashMap<String, Raw>, depth: usize, out:
|
||||
}
|
||||
for inst in &raw.instances {
|
||||
let Some(def) = defs.get(&inst.name) else { continue };
|
||||
let local: Affine = [inst.m[0], inst.m[1], inst.m[2], inst.m[3], inst.origin[0], inst.origin[1]];
|
||||
place(def, &compose(m, &local), defs, depth + 1, out);
|
||||
place(&read_run(def, styles, &inst.xf), m, defs, styles, depth + 1, out);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -627,11 +659,10 @@ pub fn model(bytes: &[u8], name: &str, hidden: &[String]) -> Option<Sheet> {
|
||||
let (_, graphics, header) = all.iter().find(|m| m.0.eq_ignore_ascii_case(name)).or_else(|| all.first())?;
|
||||
let non_model = non_model(&cfb);
|
||||
let nm = elements(&non_model);
|
||||
let colors = nm
|
||||
.iter()
|
||||
.find(|e| kind(e) == 5 && e.len() >= 37 + 768)
|
||||
.map(|e| (0..256).map(|i| [e[37 + i * 3], e[38 + i * 3], e[39 + i * 3]]).collect())
|
||||
.unwrap_or_else(|| vec![[255, 255, 255]; 256]);
|
||||
// Colours 0-254 from byte 37, the background (255) opening the table.
|
||||
let color_table = nm.iter().find(|e| kind(e) == 5).and_then(|e| dgn_color_table(e, 37));
|
||||
let table_colors = color_table.is_some();
|
||||
let colors = color_table.unwrap_or_else(|| DGN_DEFAULT_COLORS.to_vec());
|
||||
let table = level_table(&nm);
|
||||
let levels = table.iter().map(|l| (l.0, l.2)).collect();
|
||||
let hidden = table
|
||||
@@ -641,7 +672,7 @@ pub fn model(bytes: &[u8], name: &str, hidden: &[String]) -> Option<Sheet> {
|
||||
.collect();
|
||||
let styles = Styles { colors, levels, hidden, extended: extended_colors(&cfb) };
|
||||
// Shared cell definitions: a type-34 element and its components.
|
||||
let mut defs = HashMap::new();
|
||||
let mut defs: HashMap<String, Vec<&[u8]>> = HashMap::new();
|
||||
let mut i = 0;
|
||||
while i < nm.len() {
|
||||
if kind(nm[i]) == 34 && !is_component(nm[i]) {
|
||||
@@ -650,17 +681,46 @@ pub fn model(bytes: &[u8], name: &str, hidden: &[String]) -> Option<Sheet> {
|
||||
while j < nm.len() && is_component(nm[j]) {
|
||||
j += 1;
|
||||
}
|
||||
defs.insert(name, read_run(&nm[i + 1..j], &styles));
|
||||
defs.insert(name, nm[i + 1..j].to_vec());
|
||||
i = j;
|
||||
} else {
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
let raw = read_run(&elements(graphics), &styles);
|
||||
let graphics = elements(graphics);
|
||||
let raw = read_run(&graphics, &styles, &IDENTITY);
|
||||
let (per_unit, sub_uor) = units(header);
|
||||
let m: Affine = [1.0 / per_unit, 0.0, 0.0, 1.0 / per_unit, 0.0, 0.0];
|
||||
let mut sheet = Sheet { sub_per_master: per_unit / sub_uor, ..Default::default() };
|
||||
place(&raw, &m, &defs, 0, &mut sheet);
|
||||
sheet.rect = paths_bounds(&sheet.paths)?;
|
||||
let origin = global_origin(header);
|
||||
let m: Affine = [1.0 / per_unit, 0.0, 0.0, 1.0 / per_unit, -origin[0] / per_unit, -origin[1] / per_unit];
|
||||
let mut sheet = Sheet { sub_per_master: per_unit / sub_uor, table_colors, ..Default::default() };
|
||||
place(&raw, &m, &defs, &styles, 0, &mut sheet);
|
||||
// The model's extent is its elements' stored ranges (text by its full
|
||||
// box), as the reference sizes a model.
|
||||
sheet.rect = match stored_range(&graphics) {
|
||||
Some([x0, y0, x1, y1]) => {
|
||||
let (lo, hi) = (apply(&m, [x0, y0]), apply(&m, [x1, y1]));
|
||||
[lo[0], lo[1], hi[0], hi[1]]
|
||||
}
|
||||
None => paths_bounds(&sheet.paths)?,
|
||||
};
|
||||
Some(sheet)
|
||||
}
|
||||
|
||||
/// The model's global origin (UORs; the design plane point drawn at the
|
||||
/// model's 0,0).
|
||||
fn global_origin(header: &[u8]) -> [f64; 2] {
|
||||
let get = |at: usize| f64_at(header, at).filter(|v| v.is_finite()).unwrap_or(0.0);
|
||||
[get(4212), get(4220)]
|
||||
}
|
||||
|
||||
/// The union of the top-level elements' ranges (UORs): each graphic element
|
||||
/// stores its low corner at 56 and its size at 80, as 64-bit integers.
|
||||
fn stored_range(graphics: &[&[u8]]) -> Option<[f64; 4]> {
|
||||
let i64_at = |e: &[u8], at: usize| Some(i64::from_le_bytes(e.get(at..at + 8)?.try_into().ok()?) as f64);
|
||||
let mut r = [f64::INFINITY, f64::INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY];
|
||||
for e in graphics.iter().filter(|e| e.len() >= 104 && !is_component(e)) {
|
||||
let (Some(x), Some(y), Some(w), Some(h)) = (i64_at(e, 56), i64_at(e, 64), i64_at(e, 80), i64_at(e, 88)) else { continue };
|
||||
r = [r[0].min(x), r[1].min(y), r[2].max(x + w), r[3].max(y + h)];
|
||||
}
|
||||
(r[0] <= r[2]).then_some(r)
|
||||
}
|
||||
@@ -180,7 +180,7 @@ pub fn sheet(bytes: &[u8], name: &str) -> Option<Sheet> {
|
||||
xps::read(data, w2x.map(|v| v.as_slice()), entry, &family)?
|
||||
};
|
||||
let rect = view.or_else(|| paths_bounds(&paths))?;
|
||||
Some(Sheet { rect, paths, texts, sub_per_master: 1.0 })
|
||||
Some(Sheet { rect, paths, texts, sub_per_master: 1.0, table_colors: false, element_colors: Vec::new() })
|
||||
}
|
||||
|
||||
/// An XPS obfuscated font (.odttf): its first 32 bytes are XORed with the
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -39,6 +39,47 @@ pub struct Sheet {
|
||||
pub texts: Vec<Text>,
|
||||
/// DGN: sub units per master unit (the scale a Sub conversion offers).
|
||||
pub sub_per_master: f64,
|
||||
/// DGN: the colours come from the file's own colour table, which the
|
||||
/// reference shows adapted to the background ([`dgn_table_color`]).
|
||||
pub table_colors: bool,
|
||||
/// The colour of each path and text the sheet draws, one per element.
|
||||
pub element_colors: Vec<[u8; 3]>,
|
||||
}
|
||||
|
||||
/// How a colour from a DGN file's colour table shows over a background
|
||||
/// (its largest channel and whether it is light), as the reference draws
|
||||
/// it: a near-white colour (61 or less from white, channels summed) is white,
|
||||
/// or black on a light background; over a background with a channel of 32 or
|
||||
/// more, colours whose largest channel is 76, 127, 153 or 204 drop a shade
|
||||
/// (to 38, 76, 127 or 165, hue kept) and the greys 91, 132, 173 and 214 drop
|
||||
/// to 45, 91, 137 and 183 — grey 51 to black from a channel of 30.
|
||||
pub fn dgn_table_color(rgb: [u8; 3], bg_max: u8, light: bool) -> [u8; 3] {
|
||||
if rgb.iter().map(|&c| 255 - c as u32).sum::<u32>() <= 61 {
|
||||
return if light { [0; 3] } else { [255; 3] };
|
||||
}
|
||||
if rgb[0] == rgb[1] && rgb[1] == rgb[2] {
|
||||
let v = match rgb[0] {
|
||||
51 if bg_max >= 30 => 0,
|
||||
91 if bg_max >= 32 => 45,
|
||||
132 if bg_max >= 32 => 91,
|
||||
173 if bg_max >= 32 => 137,
|
||||
214 if bg_max >= 32 => 183,
|
||||
v => v,
|
||||
};
|
||||
return [v; 3];
|
||||
}
|
||||
let top = rgb.into_iter().max().unwrap_or(0);
|
||||
let to = match top {
|
||||
76 => 38,
|
||||
127 => 76,
|
||||
153 => 127,
|
||||
204 => 165,
|
||||
_ => return rgb,
|
||||
};
|
||||
if bg_max < 32 {
|
||||
return rgb;
|
||||
}
|
||||
rgb.map(|c| ((c as u32 * to + top as u32 / 2) / top as u32) as u8)
|
||||
}
|
||||
|
||||
/// Builds paths from pen moves.
|
||||
@@ -131,7 +172,8 @@ pub fn arc_cubics(
|
||||
out
|
||||
}
|
||||
|
||||
/// Bounds of the paths, when there are any.
|
||||
/// Tight bounds of the paths (curves by their extreme points, not their
|
||||
/// control points), when there are any.
|
||||
pub fn paths_bounds(paths: &[Path]) -> Option<[f64; 4]> {
|
||||
let mut b = [f64::INFINITY, f64::INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY];
|
||||
let mut add = |p: [f64; 2]| {
|
||||
@@ -147,9 +189,10 @@ pub fn paths_bounds(paths: &[Path]) -> Option<[f64; 4]> {
|
||||
}
|
||||
Segment::Cubic(a, c1, c2, c) => {
|
||||
add(*a);
|
||||
add(*c1);
|
||||
add(*c2);
|
||||
add(*c);
|
||||
for t in cubic_extrema(*a, *c1, *c2, *c) {
|
||||
add(cubic_at(*a, *c1, *c2, *c, t));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -158,6 +201,85 @@ pub fn paths_bounds(paths: &[Path]) -> Option<[f64; 4]> {
|
||||
(b[0] <= b[2]).then_some(b)
|
||||
}
|
||||
|
||||
fn cubic_at(p0: [f64; 2], p1: [f64; 2], p2: [f64; 2], p3: [f64; 2], t: f64) -> [f64; 2] {
|
||||
let u = 1.0 - t;
|
||||
let (a, b, c, d) = (u * u * u, 3.0 * u * u * t, 3.0 * u * t * t, t * t * t);
|
||||
[a * p0[0] + b * p1[0] + c * p2[0] + d * p3[0], a * p0[1] + b * p1[1] + c * p2[1] + d * p3[1]]
|
||||
}
|
||||
|
||||
/// Parameters in (0, 1) where a cubic turns in x or in y.
|
||||
fn cubic_extrema(p0: [f64; 2], p1: [f64; 2], p2: [f64; 2], p3: [f64; 2]) -> Vec<f64> {
|
||||
let mut out = Vec::new();
|
||||
for i in 0..2 {
|
||||
// The derivative's quadratic a·t² + b·t + c.
|
||||
let a = 3.0 * (-p0[i] + 3.0 * p1[i] - 3.0 * p2[i] + p3[i]);
|
||||
let b = 6.0 * (p0[i] - 2.0 * p1[i] + p2[i]);
|
||||
let c = 3.0 * (p1[i] - p0[i]);
|
||||
if a.abs() < 1e-12 {
|
||||
if b.abs() > 1e-12 {
|
||||
out.push(-c / b);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
let disc = b * b - 4.0 * a * c;
|
||||
if disc >= 0.0 {
|
||||
let r = disc.sqrt();
|
||||
out.push((-b + r) / (2.0 * a));
|
||||
out.push((-b - r) / (2.0 * a));
|
||||
}
|
||||
}
|
||||
out.retain(|t| *t > 0.0 && *t < 1.0);
|
||||
out
|
||||
}
|
||||
|
||||
/// The colour table a DGN file without one is drawn with (index 255, the
|
||||
/// background entry, black).
|
||||
pub const DGN_DEFAULT_COLORS: [[u8; 3]; 256] = [
|
||||
[255, 255, 255], [0, 0, 255], [0, 255, 0], [255, 0, 0], [255, 255, 0], [255, 0, 255], [255, 127, 0], [0, 255, 255],
|
||||
[64, 64, 64], [192, 192, 192], [254, 0, 96], [160, 224, 0], [0, 254, 160], [128, 0, 160], [176, 176, 176], [0, 240, 240],
|
||||
[240, 240, 240], [0, 0, 240], [0, 240, 0], [240, 0, 0], [240, 240, 0], [240, 0, 240], [240, 122, 0], [0, 240, 240],
|
||||
[240, 240, 240], [0, 0, 240], [0, 240, 0], [240, 0, 0], [240, 240, 0], [240, 0, 240], [240, 122, 0], [0, 225, 225],
|
||||
[225, 225, 225], [0, 0, 225], [0, 225, 0], [225, 0, 0], [225, 225, 0], [225, 0, 225], [225, 117, 0], [0, 225, 225],
|
||||
[225, 225, 225], [0, 0, 225], [0, 225, 0], [225, 0, 0], [225, 225, 0], [225, 0, 225], [225, 117, 0], [0, 210, 210],
|
||||
[210, 210, 210], [0, 0, 210], [0, 210, 0], [210, 0, 0], [210, 210, 0], [210, 0, 210], [210, 112, 0], [0, 210, 210],
|
||||
[210, 210, 210], [0, 0, 210], [0, 210, 0], [210, 0, 0], [210, 210, 0], [210, 0, 210], [210, 112, 0], [0, 195, 195],
|
||||
[195, 195, 195], [0, 0, 195], [0, 195, 0], [195, 0, 0], [195, 195, 0], [195, 0, 195], [195, 107, 0], [0, 195, 195],
|
||||
[195, 195, 195], [0, 0, 195], [0, 195, 0], [195, 0, 0], [195, 195, 0], [195, 0, 195], [195, 107, 0], [0, 180, 180],
|
||||
[180, 180, 180], [0, 0, 180], [0, 180, 0], [180, 0, 0], [180, 180, 0], [180, 0, 180], [180, 102, 0], [0, 180, 180],
|
||||
[180, 180, 180], [0, 0, 180], [0, 180, 0], [180, 0, 0], [180, 180, 0], [180, 0, 180], [180, 102, 0], [0, 165, 165],
|
||||
[165, 165, 165], [0, 0, 165], [0, 165, 0], [165, 0, 0], [165, 165, 0], [165, 0, 165], [165, 97, 0], [0, 165, 165],
|
||||
[165, 165, 165], [0, 0, 165], [0, 165, 0], [165, 0, 0], [165, 165, 0], [165, 0, 165], [165, 97, 0], [0, 150, 150],
|
||||
[150, 150, 150], [0, 0, 150], [0, 150, 0], [150, 0, 0], [150, 150, 0], [150, 0, 150], [150, 92, 0], [0, 150, 150],
|
||||
[150, 150, 150], [0, 0, 150], [0, 150, 0], [150, 0, 0], [150, 150, 0], [150, 0, 150], [150, 92, 0], [0, 135, 135],
|
||||
[135, 135, 135], [0, 0, 135], [0, 135, 0], [135, 0, 0], [135, 135, 0], [135, 0, 135], [135, 87, 0], [0, 135, 135],
|
||||
[135, 135, 135], [0, 0, 135], [0, 135, 0], [135, 0, 0], [135, 135, 0], [135, 0, 135], [135, 87, 0], [0, 120, 120],
|
||||
[120, 120, 120], [0, 0, 120], [0, 120, 0], [120, 0, 0], [120, 120, 0], [120, 0, 120], [120, 82, 0], [0, 120, 120],
|
||||
[120, 120, 120], [0, 0, 120], [0, 120, 0], [120, 0, 0], [120, 120, 0], [120, 0, 120], [120, 82, 0], [0, 105, 105],
|
||||
[105, 105, 105], [0, 0, 105], [0, 105, 0], [105, 0, 0], [105, 105, 0], [105, 0, 105], [105, 77, 0], [0, 105, 105],
|
||||
[105, 105, 105], [0, 0, 105], [0, 105, 0], [105, 0, 0], [105, 105, 0], [105, 0, 105], [105, 77, 0], [0, 90, 90],
|
||||
[90, 90, 90], [0, 0, 90], [0, 90, 0], [90, 0, 0], [90, 90, 0], [90, 0, 90], [90, 72, 0], [0, 90, 90],
|
||||
[90, 90, 90], [0, 0, 90], [0, 90, 0], [90, 0, 0], [90, 90, 0], [90, 0, 90], [90, 72, 0], [0, 75, 75],
|
||||
[75, 75, 75], [0, 0, 75], [0, 75, 0], [75, 0, 0], [75, 75, 0], [75, 0, 75], [75, 67, 0], [0, 75, 75],
|
||||
[75, 75, 75], [0, 0, 75], [0, 75, 0], [75, 0, 0], [75, 75, 0], [75, 0, 75], [75, 67, 0], [0, 60, 60],
|
||||
[60, 60, 60], [0, 0, 60], [0, 60, 0], [60, 0, 0], [60, 60, 0], [60, 0, 60], [60, 62, 0], [0, 60, 60],
|
||||
[60, 60, 60], [0, 0, 60], [0, 60, 0], [60, 0, 0], [60, 60, 0], [60, 0, 60], [60, 62, 0], [0, 45, 45],
|
||||
[45, 45, 45], [0, 0, 45], [0, 45, 0], [45, 0, 0], [45, 45, 0], [45, 0, 45], [45, 57, 0], [0, 45, 45],
|
||||
[45, 45, 45], [0, 0, 45], [0, 45, 0], [45, 0, 0], [45, 45, 0], [45, 0, 45], [45, 57, 0], [0, 30, 30],
|
||||
[30, 30, 30], [0, 0, 30], [0, 30, 0], [30, 0, 0], [30, 30, 0], [30, 0, 30], [30, 52, 0], [0, 30, 30],
|
||||
[30, 30, 30], [0, 0, 30], [0, 30, 0], [30, 0, 0], [30, 30, 0], [30, 0, 30], [192, 192, 192], [0, 0, 0],
|
||||
];
|
||||
|
||||
/// A colour table element's 256 colours: entries 0-254 from `first`, and
|
||||
/// entry 255 (the background) from the three bytes that open the table at
|
||||
/// `first - 3`.
|
||||
pub fn dgn_color_table(e: &[u8], first: usize) -> Option<Vec<[u8; 3]>> {
|
||||
let bg = e.get(first - 3..first)?;
|
||||
let body = e.get(first..first + 255 * 3)?;
|
||||
let mut out: Vec<[u8; 3]> = body.chunks_exact(3).map(|c| [c[0], c[1], c[2]]).collect();
|
||||
out.push([bg[0], bg[1], bg[2]]);
|
||||
Some(out)
|
||||
}
|
||||
|
||||
/// A line of text the host draws with its own font.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Text {
|
||||
|
||||
@@ -146,7 +146,8 @@ pub fn item_thumbs(kind: codec::entities::UnderlayType, path: &str) -> Vec<PageT
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.map(|label| {
|
||||
let image = underlay_vector::display_raster(kind, path, &label, &[], 220.0)
|
||||
let backdrop = underlay_vector::Backdrop { max_channel: 0, light: false };
|
||||
let image = underlay_vector::display_raster(kind, path, &label, &[], 220.0, backdrop)
|
||||
.map(|page| image::Handle::from_rgba(page.width, page.height, page.pixels.to_vec()));
|
||||
PageThumb { label, image }
|
||||
})
|
||||
|
||||
Reference in new issue
Block a user