Merge main into codex/graphic-attributes-cleanup

Resolve conflicts from main's rustfmt pass by taking main's versions of
command.rs, style.rs, view/mod.rs, scene/mod.rs, ui/window/mod.rs and
ui/properties.rs and re-applying this branch's Graphic Attributes changes.
Add the Graphic Attributes strings to the new lo-LA catalog.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01E1QnME6WJbD5iGFae1eeCb
This commit is contained in:
Claude committed 2026-10-11 19:09:37 +00:00
commit 44f8f76ab3
596 files changed
+69069 -34120

No files matched your search

+2 -6
View File
@@ -362,10 +362,7 @@ impl HatchModel {
self.pattern_segments_with_dot_length(Some(0.0))
}
fn pattern_segments_with_dot_length(
&self,
plot_dot_length: Option<f64>,
) -> Vec<[[f64; 2]; 2]> {
fn pattern_segments_with_dot_length(&self, plot_dot_length: Option<f64>) -> Vec<[[f64; 2]; 2]> {
let HatchPattern::Pattern(families) = &self.pattern else {
return Vec::new();
};
@@ -550,8 +547,7 @@ impl HatchModel {
// dashes, negative are gaps, and a zero-length entry is
// a round-capped point.
let n = family.dashes.len();
let dot_len = plot_dot_length
.unwrap_or_else(|| (period * 0.06).max(1e-3));
let dot_len = plot_dot_length.unwrap_or_else(|| (period * 0.06).max(1e-3));
let phase = t0.rem_euclid(period);
// Start at the period boundary at or before t0; the
// span clip below drops anything before t0.
+78 -32
View File
@@ -22,12 +22,36 @@ fn quad_verts(corners: &[[f32; 3]; 4], corners_low: &[[f32; 3]; 4]) -> Vec<Image
let [p0, p1, p2, p3] = *corners;
let [l0, l1, l2, l3] = *corners_low;
vec![
ImageQuadVertex { pos: p0, uv: [0.0, 1.0], pos_low: l0 },
ImageQuadVertex { pos: p1, uv: [1.0, 1.0], pos_low: l1 },
ImageQuadVertex { pos: p2, uv: [1.0, 0.0], pos_low: l2 },
ImageQuadVertex { pos: p0, uv: [0.0, 1.0], pos_low: l0 },
ImageQuadVertex { pos: p2, uv: [1.0, 0.0], pos_low: l2 },
ImageQuadVertex { pos: p3, uv: [0.0, 0.0], pos_low: l3 },
ImageQuadVertex {
pos: p0,
uv: [0.0, 1.0],
pos_low: l0,
},
ImageQuadVertex {
pos: p1,
uv: [1.0, 1.0],
pos_low: l1,
},
ImageQuadVertex {
pos: p2,
uv: [1.0, 0.0],
pos_low: l2,
},
ImageQuadVertex {
pos: p0,
uv: [0.0, 1.0],
pos_low: l0,
},
ImageQuadVertex {
pos: p2,
uv: [1.0, 0.0],
pos_low: l2,
},
ImageQuadVertex {
pos: p3,
uv: [0.0, 0.0],
pos_low: l3,
},
]
}
@@ -39,12 +63,7 @@ fn clip_triangles_px(img: &codec::entities::RasterImage) -> Vec<[f64; 2]> {
use codec::entities::ClipMode;
let w = img.size.x;
let h = img.size.y;
let quad = || {
vec![
[0.0, 0.0], [w, 0.0], [w, h],
[0.0, 0.0], [w, h], [0.0, h],
]
};
let quad = || vec![[0.0, 0.0], [w, 0.0], [w, h], [0.0, 0.0], [w, h], [0.0, h]];
let Some(poly) = crate::entities::raster_image::image_clip_polygon(img) else {
return quad();
};
@@ -54,7 +73,10 @@ fn clip_triangles_px(img: &codec::entities::RasterImage) -> Vec<[f64; 2]> {
} else {
kernel::geom2d::triangulate(&poly, &[])
};
let tris: Vec<[f64; 2]> = triangles.into_iter().flat_map(|t| t.map(|i| points[i])).collect();
let tris: Vec<[f64; 2]> = triangles
.into_iter()
.flat_map(|t| t.map(|i| points[i]))
.collect();
if tris.is_empty() {
quad()
} else {
@@ -107,8 +129,8 @@ pub(crate) fn underlay_adjust(
def: &codec::entities::UnderlayDefinition,
background: [f32; 4],
) -> super::pdf_raster::PageAdjust {
use codec::entities::{UnderlayDisplayFlags, UnderlayType};
use super::pdf_raster::PageAdjust;
use codec::entities::{UnderlayDisplayFlags, UnderlayType};
let page = crate::entities::underlay::page_of(def);
// Dark means an HSL lightness under one half: pure blue counts as
// light, (0, 128, 0) as dark.
@@ -128,7 +150,9 @@ pub(crate) fn underlay_adjust(
contrast_pivot: match def.underlay_type {
UnderlayType::Pdf => 500,
UnderlayType::Dwf
if u.flags.contains(UnderlayDisplayFlags::ADJUST_FOR_BACKGROUND) && bg_lum < 0.5 =>
if u.flags
.contains(UnderlayDisplayFlags::ADJUST_FOR_BACKGROUND)
&& bg_lum < 0.5 =>
{
730
}
@@ -145,7 +169,9 @@ pub(crate) fn underlay_adjust(
monochrome: u.flags.contains(UnderlayDisplayFlags::MONOCHROME),
// A DGN model's colours follow the background through its
// colour table, whatever this setting.
adjust_for_background: u.flags.contains(UnderlayDisplayFlags::ADJUST_FOR_BACKGROUND)
adjust_for_background: u
.flags
.contains(UnderlayDisplayFlags::ADJUST_FOR_BACKGROUND)
&& !dgn,
dark_background: bg_lum < 0.5,
fade_before_contrast,
@@ -155,9 +181,7 @@ pub(crate) fn underlay_adjust(
impl ImageModel {
/// Build an ImageModel from a DXF RasterImage entity.
/// Returns `None` if the image file cannot be opened or decoded.
pub fn from_raster_image(
img: &codec::entities::RasterImage,
) -> Option<Self> {
pub fn from_raster_image(img: &codec::entities::RasterImage) -> Option<Self> {
let w = img.size.x;
let h = img.size.y;
// Model-space geometry is drawn in (WCS - world_offset) so large UTM-
@@ -201,7 +225,11 @@ impl ImageModel {
let fu = (px / w) as f32;
let fv = (py / h) as f32;
ImageQuadVertex {
pos: [ox + ux * fu + vx * fv, oy + uy * fu + vy * fv, oz + uz * fu + vz * fv],
pos: [
ox + ux * fu + vx * fv,
oy + uy * fu + vy * fv,
oz + uz * fu + vz * fv,
],
uv: [fu, 1.0 - fv],
pos_low: [oxl, oyl, ozl],
}
@@ -249,8 +277,8 @@ impl ImageModel {
background: [f32; 4],
world_per_pixel: Option<f64>,
) -> Option<Self> {
use codec::entities::{UnderlayDisplayFlags, UnderlayType};
use super::pdf_raster;
use codec::entities::{UnderlayDisplayFlags, UnderlayType};
if !u.flags.contains(UnderlayDisplayFlags::ON) {
return None;
}
@@ -262,21 +290,25 @@ impl ImageModel {
// The page's longest side on screen, pixels (the view scale is taken
// a step coarser, so the raster is never reduced on screen and its
// one-pixel lines stay unbroken).
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 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 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)?
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)?,
};
@@ -286,7 +318,12 @@ impl ImageModel {
// (`underlay_vector::display_wires`).
_ => return None,
};
let pixels = pdf_raster::adjusted_pixels(&source, page, &raster, underlay_adjust(u, def, background));
let pixels = pdf_raster::adjusted_pixels(
&source,
page,
&raster,
underlay_adjust(u, def, background),
);
// The page rectangle in drawing units (1 unit per PDF inch, or per
// DWF/DGN sheet unit), entity scale applied, from its lower-left
@@ -355,7 +392,16 @@ fn underlay_visible_uv(u: &codec::entities::Underlay, rect: [f64; 4]) -> Vec<[f6
let (page_w, page_h) = (rect[2] - rect[0], rect[3] - rect[1]);
use codec::entities::UnderlayDisplayFlags;
let page = vec![[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]];
let whole = || vec![[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 0.0], [1.0, 1.0], [0.0, 1.0]];
let whole = || {
vec![
[0.0, 0.0],
[1.0, 0.0],
[1.0, 1.0],
[0.0, 0.0],
[1.0, 1.0],
[0.0, 1.0],
]
};
let clip = crate::entities::underlay::clip_polygon_local(u);
if !u.flags.contains(UnderlayDisplayFlags::CLIPPING) || clip.len() < 3 {
return whole();
+96 -40
View File
@@ -1,11 +1,11 @@
//! LOFT command/history integration. Geometry and validation live in the kernel.
use super::sweep_model::embedded_path;
use crate::command::{ExtrudeMode, LoftOptions, LoftSectionSelection};
use codec::entities::{EmbeddedEntity, Point, Surface, SurfaceData, SurfaceKind};
use codec::objects::{SolidHistoryLoft, SolidHistoryLoftParameters, SolidHistoryNodeBase};
use codec::{EntityType, Handle};
use kernel::brep::Body;
use crate::command::{ExtrudeMode, LoftOptions, LoftSectionSelection};
use super::sweep_model::embedded_path;
fn embedded_section(entity: &EntityType) -> Option<EmbeddedEntity> {
match entity {
@@ -16,8 +16,8 @@ fn embedded_section(entity: &EntityType) -> Option<EmbeddedEntity> {
}
pub fn is_section(entity: &EntityType) -> bool {
embedded_section(entity).is_some_and(|entity|
kernel::acis::loft_section_geometry(&[entity]).is_ok())
embedded_section(entity)
.is_some_and(|entity| kernel::acis::loft_section_geometry(&[entity]).is_ok())
}
pub fn is_guide_or_path(entity: &EntityType) -> bool {
@@ -25,71 +25,127 @@ pub fn is_guide_or_path(entity: &EntityType) -> bool {
}
pub fn record(
sections: &[LoftSectionSelection], guides: &[Handle], path: Option<Handle>,
available: &[(Handle, EntityType)], mode: ExtrudeMode, options: LoftOptions,
sections: &[LoftSectionSelection],
guides: &[Handle],
path: Option<Handle>,
available: &[(Handle, EntityType)],
mode: ExtrudeMode,
options: LoftOptions,
) -> Result<SolidHistoryLoft, String> {
let find = |handle| available.iter().find(|(candidate, _)| *candidate == handle)
.map(|(_, entity)| entity).ok_or_else(|| "A selected loft source no longer exists.".to_string());
let find = |handle| {
available
.iter()
.find(|(candidate, _)| *candidate == handle)
.map(|(_, entity)| entity)
.ok_or_else(|| "A selected loft source no longer exists.".to_string())
};
let mut cross_sections = Vec::new();
let mut section_counts = Vec::new();
let mut surface = mode == ExtrudeMode::Surface;
for section in sections {
let members = match section {
LoftSectionSelection::Entity(handle) => vec![embedded_section(find(*handle)?)
.ok_or("Unsupported loft section.")?],
LoftSectionSelection::Point(point) => vec![EmbeddedEntity::Point(Point::from_coords(point.x, point.y, point.z))],
LoftSectionSelection::Join(handles) => handles.iter().map(|handle| {
embedded_section(find(*handle)?).ok_or_else(|| "Unsupported joined loft edge.".to_string())
}).collect::<Result<Vec<_>, _>>()?,
LoftSectionSelection::Entity(handle) => {
vec![embedded_section(find(*handle)?).ok_or("Unsupported loft section.")?]
}
LoftSectionSelection::Point(point) => vec![EmbeddedEntity::Point(Point::from_coords(
point.x, point.y, point.z,
))],
LoftSectionSelection::Join(handles) => handles
.iter()
.map(|handle| {
embedded_section(find(*handle)?)
.ok_or_else(|| "Unsupported joined loft edge.".to_string())
})
.collect::<Result<Vec<_>, _>>()?,
};
if let kernel::brep::LoftSection::Profile { closed, .. } = kernel::acis::loft_section_geometry(&members)? {
if let kernel::brep::LoftSection::Profile { closed, .. } =
kernel::acis::loft_section_geometry(&members)?
{
surface |= !closed;
}
section_counts.push(members.len());
cross_sections.extend(members);
}
let guides = guides.iter().map(|handle| embedded_path(find(*handle)?)
.ok_or_else(|| "Unsupported loft guide.".to_string())).collect::<Result<Vec<_>, _>>()?;
let path_entity = path.map(|handle| embedded_path(find(handle)?)
.ok_or_else(|| "Unsupported loft path.".to_string())).transpose()?;
let guides = guides
.iter()
.map(|handle| {
embedded_path(find(*handle)?).ok_or_else(|| "Unsupported loft guide.".to_string())
})
.collect::<Result<Vec<_>, _>>()?;
let path_entity = path
.map(|handle| {
embedded_path(find(handle)?).ok_or_else(|| "Unsupported loft path.".to_string())
})
.transpose()?;
Ok(SolidHistoryLoft {
base: SolidHistoryNodeBase::new(1), operation_major: 1,
cross_sections, guides,
base: SolidHistoryNodeBase::new(1),
operation_major: 1,
cross_sections,
guides,
parameters: Some(SolidHistoryLoftParameters {
path_entity, normals: options.normals,
start_draft_angle: options.start_draft_angle, end_draft_angle: options.end_draft_angle,
start_magnitude: options.start_magnitude, end_magnitude: options.end_magnitude,
start_continuity: options.start_continuity, end_continuity: options.end_continuity,
start_bulge: options.start_bulge, end_bulge: options.end_bulge,
closed: options.closed, periodic: options.periodic, surface, align_direction: options.align_direction, section_counts,
path_entity,
normals: options.normals,
start_draft_angle: options.start_draft_angle,
end_draft_angle: options.end_draft_angle,
start_magnitude: options.start_magnitude,
end_magnitude: options.end_magnitude,
start_continuity: options.start_continuity,
end_continuity: options.end_continuity,
start_bulge: options.start_bulge,
end_bulge: options.end_bulge,
closed: options.closed,
periodic: options.periodic,
surface,
align_direction: options.align_direction,
section_counts,
}),
..SolidHistoryLoft::default()
})
}
pub fn build_body(
sections: &[LoftSectionSelection], guides: &[Handle], path: Option<Handle>,
available: &[(Handle, EntityType)], mode: ExtrudeMode, options: LoftOptions,
sections: &[LoftSectionSelection],
guides: &[Handle],
path: Option<Handle>,
available: &[(Handle, EntityType)],
mode: ExtrudeMode,
options: LoftOptions,
) -> Result<Body, String> {
kernel::acis::rebuild_loft_with_options(&record(sections, guides, path, available, mode, options)?)
kernel::acis::rebuild_loft_with_options(&record(
sections, guides, path, available, mode, options,
)?)
}
pub fn surface_entity(record: &SolidHistoryLoft) -> EntityType {
let settings = record.parameters.clone().unwrap_or_else(|| SolidHistoryLoftParameters {
normals: 0, ..Default::default()
});
let settings = record
.parameters
.clone()
.unwrap_or_else(|| SolidHistoryLoftParameters {
normals: 0,
..Default::default()
});
let mut surface = Surface::new(SurfaceKind::Lofted);
surface.surface_data = SurfaceData::Lofted {
loft_transform: record.base.transform,
cross_section_entities: record.cross_sections.clone(), guide_entities: record.guides.clone(),
cross_section_entities: record.cross_sections.clone(),
guide_entities: record.guides.clone(),
path_entity: settings.path_entity,
plane_normal_lofting_type: settings.normals,
start_draft_angle: settings.start_draft_angle, end_draft_angle: settings.end_draft_angle,
start_draft_magnitude: settings.start_magnitude, end_draft_magnitude: settings.end_magnitude,
arc_length_parameterization: true, no_twist: true, align_direction: settings.align_direction,
simple_surfaces: true, closed_surfaces: settings.closed, solid: false,
ruled_surface: settings.normals == 0, virtual_guide: false,
cross_sections: Vec::new(), guide_curves: Vec::new(), path_curve: None,
start_draft_angle: settings.start_draft_angle,
end_draft_angle: settings.end_draft_angle,
start_draft_magnitude: settings.start_magnitude,
end_draft_magnitude: settings.end_magnitude,
arc_length_parameterization: true,
no_twist: true,
align_direction: settings.align_direction,
simple_surfaces: true,
closed_surfaces: settings.closed,
solid: false,
ruled_surface: settings.normals == 0,
virtual_guide: false,
cross_sections: Vec::new(),
guide_curves: Vec::new(),
path_curve: None,
};
EntityType::Surface(Box::new(surface))
}
+11 -25
View File
@@ -89,8 +89,7 @@ impl MeshMaterialMapper {
+ self.normal_basis[2][1] * normal[1] as f64
+ self.normal_basis[2][2] * normal[2] as f64,
];
let length =
(mapped[0] * mapped[0] + mapped[1] * mapped[1] + mapped[2] * mapped[2]).sqrt();
let length = (mapped[0] * mapped[0] + mapped[1] * mapped[1] + mapped[2] * mapped[2]).sqrt();
if length > f64::EPSILON {
[
(mapped[0] / length) as f32,
@@ -183,8 +182,7 @@ impl MeshTextureMap {
source: map.source,
file_name: map.file_name.clone(),
procedural: map.texture.is_some(),
image: load_map_image(map, base_dir)
.or_else(|| procedural_map_image(map)),
image: load_map_image(map, base_dir).or_else(|| procedural_map_image(map)),
}
}
@@ -325,8 +323,7 @@ impl MeshMaterial {
.flat_map(|lod| lod.triangle_material_handles.iter().flatten().copied())
.collect();
for handle in handles {
let material =
resolve_material_handle_with_base(document, handle, self, base_dir);
let material = resolve_material_handle_with_base(document, handle, self, base_dir);
set.face_materials.insert(handle, material);
}
}
@@ -368,9 +365,7 @@ fn procedural_map_image(map: &MaterialMap) -> Option<Arc<MaterialImage>> {
} else {
color2
};
rgba.extend(color.map(|channel| {
(channel.clamp(0.0, 1.0) * 255.0).round() as u8
}));
rgba.extend(color.map(|channel| (channel.clamp(0.0, 1.0) * 255.0).round() as u8));
}
}
Some(Arc::new(MaterialImage {
@@ -443,21 +438,9 @@ fn entity_material_mapper(entity: &EntityType) -> Option<MeshMaterialMapper> {
let y = positions.next()?;
let z = positions.next()?;
let basis = glam::DMat3::from_cols(
glam::DVec3::from_array([
x[0] - origin[0],
x[1] - origin[1],
x[2] - origin[2],
]),
glam::DVec3::from_array([
y[0] - origin[0],
y[1] - origin[1],
y[2] - origin[2],
]),
glam::DVec3::from_array([
z[0] - origin[0],
z[1] - origin[1],
z[2] - origin[2],
]),
glam::DVec3::from_array([x[0] - origin[0], x[1] - origin[1], x[2] - origin[2]]),
glam::DVec3::from_array([y[0] - origin[0], y[1] - origin[1], y[2] - origin[2]]),
glam::DVec3::from_array([z[0] - origin[0], z[1] - origin[1], z[2] - origin[2]]),
);
if basis.determinant().abs() <= f64::EPSILON {
return None;
@@ -627,6 +610,9 @@ mod tests {
line.common.extended_data.add_record(record);
let mapper = entity_material_mapper(&EntityType::Line(line)).expect("valid mapper");
assert_eq!(mapper.map_position([4.0, 22.0, 42.0], [0.0; 3]), [1.0, 2.0, 3.0]);
assert_eq!(
mapper.map_position([4.0, 22.0, 42.0], [0.0; 3]),
[1.0, 2.0, 3.0]
);
}
}
+7 -6
View File
@@ -105,8 +105,7 @@ pub struct MeshLodSet {
/// Face-level ACIS material overrides, resolved from each LOD's
/// `triangle_material_handles`. Only handles actually referenced by the
/// tessellation are retained.
pub face_materials:
rustc_hash::FxHashMap<codec::Handle, super::material_model::MeshMaterial>,
pub face_materials: rustc_hash::FxHashMap<codec::Handle, super::material_model::MeshMaterial>,
/// Effective AcDbVisualStyle override resolved from the entity's full,
/// face and edge style handles.
pub visual_style: Option<super::visual_style_model::MeshVisualStyle>,
@@ -293,10 +292,12 @@ impl MeshLodSet {
pub fn geometry_edges(&self) -> (&[[f32; 3]], &[[f32; 3]]) {
self.instance_source.as_ref().map_or(
(self.edge_verts.as_slice(), self.edge_verts_low.as_slice()),
|source| (
source.edge_verts.as_slice(),
source.edge_verts_low.as_slice(),
),
|source| {
(
source.edge_verts.as_slice(),
source.edge_verts_low.as_slice(),
)
},
)
}
+8 -8
View File
@@ -1,22 +1,22 @@
pub mod wire_model;
pub mod gdi;
pub mod hatch_model;
pub mod hatch_patterns;
pub mod image_model;
pub mod instance_model;
pub mod loft_command_model;
pub mod material_model;
pub mod mesh_model;
pub mod object;
pub mod ole_pres;
pub mod pdf_layers;
pub mod pdf_raster;
pub mod pdf_vector;
pub mod point_cloud;
pub mod underlay_vector;
pub mod mesh_model;
pub mod solid_model;
pub mod solid_history;
pub mod sweep_model;
pub mod presspull_model;
pub mod loft_command_model;
pub mod solid_history;
pub mod solid_model;
mod sweep_command_model;
pub mod sweep_model;
pub mod underlay_vector;
pub mod visual_style_model;
pub mod object;
pub mod wire_model;
+10 -2
View File
@@ -136,7 +136,11 @@ fn with_layer_config(bytes: &[u8], all: &[PdfLayer], off: &[&PdfLayer]) -> Optio
.and_then(|i| tail[i + 9..].split_whitespace().next()?.parse().ok())?;
let size: usize = {
let region = String::from_utf8_lossy(&bytes[prev.min(bytes.len())..]).into_owned();
let region = if region.contains("/Size") { region } else { tail.clone() };
let region = if region.contains("/Size") {
region
} else {
tail.clone()
};
let i = region.find("/Size")?;
region[i + 5..]
.trim_start()
@@ -193,7 +197,11 @@ fn without_key(dict: &str, key: &str) -> Option<String> {
let value = rest.trim_start();
let lead = rest.len() - value.len();
let length = if value.starts_with("<<") || value.starts_with('[') {
let (open, close) = if value.starts_with('[') { ("[", "]") } else { ("<<", ">>") };
let (open, close) = if value.starts_with('[') {
("[", "]")
} else {
("<<", ">>")
};
let mut depth = 0i32;
let mut i = 0;
loop {
+43 -8
View File
@@ -92,7 +92,12 @@ pub fn rasterize_page_display_at(path: &str, page: &str, dpi: f32) -> Option<Arc
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);
let key = (
path.to_string(),
page.to_string(),
dpi.to_bits(),
transparent,
);
if let Some(hit) = page_cache()
.lock()
.unwrap_or_else(|error| error.into_inner())
@@ -193,7 +198,11 @@ fn rgb_to_hsl([r, g, b]: [f32; 3]) -> (f32, f32, f32) {
if d <= f32::EPSILON {
return (0.0, 0.0, l);
}
let s = if l > 0.5 { d / (2.0 - max - min) } else { d / (max + min) };
let s = if l > 0.5 {
d / (2.0 - max - min)
} else {
d / (max + min)
};
let h = if max == r {
((g - b) / d).rem_euclid(6.0)
} else if max == g {
@@ -208,7 +217,11 @@ fn hsl_to_rgb(h: f32, s: f32, l: f32) -> [f32; 3] {
if s <= f32::EPSILON {
return [l; 3];
}
let q = if l < 0.5 { l * (1.0 + s) } else { l + s - l * s };
let q = if l < 0.5 {
l * (1.0 + s)
} else {
l + s - l * s
};
let p = 2.0 * l - q;
let channel = |t: f32| {
let t = t.rem_euclid(1.0);
@@ -306,13 +319,25 @@ pub fn adjust_rgb(rgb: [f32; 3], adjust: &PageAdjust) -> [f32; 3] {
/// 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 && adjust.fade_before_contrast.is_none();
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
&& adjust.fade_before_contrast.is_none();
if identity {
return raster.pixels.clone();
}
// 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);
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())
@@ -326,7 +351,14 @@ pub fn adjusted_pixels(path: &str, page: &str, raster: &PdfPage, adjust: PageAdj
if px[3] == 0 {
continue;
}
let rgb = adjust_rgb([px[0] as f32 / 255.0, px[1] as f32 / 255.0, px[2] as f32 / 255.0], &adjust);
let rgb = adjust_rgb(
[
px[0] as f32 / 255.0,
px[1] as f32 / 255.0,
px[2] as f32 / 255.0,
],
&adjust,
);
for (dst, c) in px[..3].iter_mut().zip(rgb) {
*dst = (c.clamp(0.0, 1.0) * 255.0).round() as u8;
}
@@ -399,7 +431,10 @@ mod tests {
register_source(path, Arc::new(bytes));
let page = rasterize_page_display(path, "1").expect("display raster is clamped");
assert_eq!((page.width, page.height), (3072, 3072));
assert!(page.dpi < DISPLAY_DPI, "clamped raster reports its effective DPI");
assert!(
page.dpi < DISPLAY_DPI,
"clamped raster reports its effective DPI"
);
assert!(
rasterize_page_at_dpi(path, "1", 300.0).is_none(),
+90 -21
View File
@@ -151,7 +151,12 @@ pub fn page_content_by_layer(
let base = page_content(&super::pdf_layers::source_with_layers(path, &names), page)?;
let key = |s: &dyn std::fmt::Debug| format!("{s:?}");
let mut seen: HashMap<String, usize> = HashMap::new();
for item in base.paths.iter().map(|p| key(p)).chain(base.texts.iter().map(|t| key(t))) {
for item in base
.paths
.iter()
.map(|p| key(p))
.chain(base.texts.iter().map(|t| key(t)))
{
*seen.entry(item).or_default() += 1;
}
let mut out = Vec::new();
@@ -159,7 +164,11 @@ pub fn page_content_by_layer(
if hidden.contains(&layer.name) {
continue;
}
let others: Vec<String> = names.iter().filter(|n| **n != layer.name).cloned().collect();
let others: Vec<String> = names
.iter()
.filter(|n| **n != layer.name)
.cloned()
.collect();
let Some(mut only) =
page_content(&super::pdf_layers::source_with_layers(path, &others), page)
else {
@@ -203,7 +212,12 @@ fn images_to_last_layer(
// Each layered path and text keyed once; the first layer holding it wins.
let mut owners: std::collections::HashMap<String, usize> = std::collections::HashMap::new();
for (n, (_, pv)) in layered.iter().enumerate() {
for item in pv.paths.iter().map(|p| key(p)).chain(pv.texts.iter().map(|t| key(t))) {
for item in pv
.paths
.iter()
.map(|p| key(p))
.chain(pv.texts.iter().map(|t| key(t)))
{
owners.entry(item).or_insert(n);
}
}
@@ -218,7 +232,9 @@ fn images_to_last_layer(
let image = &full.images[i];
if let Some(n) = layered.iter().position(|(_, pv)| pv.images.contains(image)) {
last = Some(n);
} else if let (Some(n), Some(at)) = (last, base.images.iter().position(|b| b == image)) {
} else if let (Some(n), Some(at)) =
(last, base.images.iter().position(|b| b == image))
{
moves.push((n, base.images.remove(at)));
}
}
@@ -268,7 +284,10 @@ fn read_page_with(path: &str, page: &str, images: bool) -> Option<PageVectors> {
.and_then(|dict| dict.get::<Name>(b"BaseFont"))
.map(|name| String::from_utf8_lossy(&name).into_owned())
.unwrap_or_default();
let name = name.split_once('+').map(|(_, rest)| rest.to_string()).unwrap_or(name);
let name = name
.split_once('+')
.map(|(_, rest)| rest.to_string())
.unwrap_or(name);
if !names.contains(&name) {
names.push(name);
}
@@ -276,7 +295,10 @@ fn read_page_with(path: &str, page: &str, images: bool) -> Option<PageVectors> {
}
let mut texts = device.texts;
for text in &mut texts {
let index = device.font_order.iter().position(|key| *key == text.font_key);
let index = device
.font_order
.iter()
.position(|key| *key == text.font_key);
text.text.font = index
.and_then(|i| names.get(i).cloned())
.filter(|name| !name.is_empty())
@@ -344,7 +366,10 @@ fn subpaths(path: &BezPath, transform: Affine) -> Vec<SubPath> {
match *el {
PathEl::MoveTo(p) => {
let p = transform * p;
out.push(SubPath { segments: Vec::new(), closed: false });
out.push(SubPath {
segments: Vec::new(),
closed: false,
});
start = p;
last = p;
}
@@ -360,14 +385,16 @@ fn subpaths(path: &BezPath, transform: Affine) -> Vec<SubPath> {
let c1 = last + (c - last) * (2.0 / 3.0);
let c2 = p + (c - p) * (2.0 / 3.0);
if let Some(sp) = out.last_mut() {
sp.segments.push(Segment::Cubic(pt(last), pt(c1), pt(c2), pt(p)));
sp.segments
.push(Segment::Cubic(pt(last), pt(c1), pt(c2), pt(p)));
}
last = p;
}
PathEl::CurveTo(c1, c2, p) => {
let (c1, c2, p) = (transform * c1, transform * c2, transform * p);
if let Some(sp) = out.last_mut() {
sp.segments.push(Segment::Cubic(pt(last), pt(c1), pt(c2), pt(p)));
sp.segments
.push(Segment::Cubic(pt(last), pt(c1), pt(c2), pt(p)));
}
last = p;
}
@@ -385,7 +412,10 @@ fn subpaths(path: &BezPath, transform: Affine) -> Vec<SubPath> {
out.retain(|sp| !sp.segments.is_empty());
// A path drawn back to its start without a close operator is closed.
for sp in &mut out {
let (first, last) = (sp.segments[0].start(), sp.segments[sp.segments.len() - 1].end());
let (first, last) = (
sp.segments[0].start(),
sp.segments[sp.segments.len() - 1].end(),
);
if !sp.closed && (first[0] - last[0]).hypot(first[1] - last[1]) < 1e-9 {
sp.closed = true;
}
@@ -396,7 +426,13 @@ fn subpaths(path: &BezPath, transform: Affine) -> Vec<SubPath> {
impl<'a> Device<'a> for Collector {
fn set_soft_mask(&mut self, _: Option<SoftMask<'a>>) {}
fn set_blend_mode(&mut self, _: BlendMode) {}
fn draw_path(&mut self, path: &BezPath, transform: Affine, paint: &Paint<'a>, mode: &PathDrawMode) {
fn draw_path(
&mut self,
path: &BezPath,
transform: Affine,
paint: &Paint<'a>,
mode: &PathDrawMode,
) {
self.finish_text();
let subpaths = subpaths(path, transform);
if subpaths.is_empty() {
@@ -412,7 +448,11 @@ impl<'a> Device<'a> for Collector {
PathDrawMode::Fill(_) => (None, Some(rgb(paint))),
};
self.order.push(Drawn::Path(self.paths.len()));
self.paths.push(PdfPath { subpaths, stroke, fill });
self.paths.push(PdfPath {
subpaths,
stroke,
fill,
});
}
fn push_clip_path(&mut self, _: &ClipPath) {}
fn push_transparency_group(&mut self, _: f32, _: Option<SoftMask<'a>>, _: BlendMode) {}
@@ -593,7 +633,9 @@ pub fn circle_of(sp: &SubPath) -> Option<([f64; 2], f64)> {
}
let tol = r * 2e-3;
let on_circle = cubics.iter().all(|s| {
[0.0, 0.25, 0.5, 0.75].iter().all(|t| (dist(bezier(s, *t)) - r).abs() <= tol)
[0.0, 0.25, 0.5, 0.75]
.iter()
.all(|t| (dist(bezier(s, *t)) - r).abs() <= tol)
});
on_circle.then_some((c, r))
}
@@ -602,7 +644,11 @@ pub fn circle_of(sp: &SubPath) -> Option<([f64; 2], f64)> {
/// PDFOSNAP, DWFOSNAP and DGNOSNAP: whether object snaps find the geometry
/// inside underlays of each kind.
static UNDERLAY_OSNAP: [AtomicBool; 3] = [AtomicBool::new(true), AtomicBool::new(true), AtomicBool::new(true)];
static UNDERLAY_OSNAP: [AtomicBool; 3] = [
AtomicBool::new(true),
AtomicBool::new(true),
AtomicBool::new(true),
];
fn osnap_slot(kind: codec::entities::UnderlayType) -> &'static AtomicBool {
match kind {
@@ -623,7 +669,11 @@ pub fn set_underlay_osnap(kind: codec::entities::UnderlayType, on: bool) {
/// UOSNAP: 1 when snaps find every kind of underlay geometry, 0 when none,
/// 2 when the kinds differ.
pub fn uosnap() -> i16 {
match UNDERLAY_OSNAP.iter().filter(|s| s.load(Ordering::Relaxed)).count() {
match UNDERLAY_OSNAP
.iter()
.filter(|s| s.load(Ordering::Relaxed))
.count()
{
0 => 0,
n if n == UNDERLAY_OSNAP.len() => 1,
_ => 2,
@@ -648,7 +698,9 @@ pub fn underlay_snap_points(
document: &codec::CadDocument,
) -> Vec<(glam::DVec3, crate::scene::model::wire_model::SnapHint)> {
use crate::scene::model::wire_model::SnapHint;
if !underlay_osnap(u.underlay_type) || !u.flags.contains(codec::entities::UnderlayDisplayFlags::ON) {
if !underlay_osnap(u.underlay_type)
|| !u.flags.contains(codec::entities::UnderlayDisplayFlags::ON)
{
return Vec::new();
}
let Some(def) = crate::entities::underlay::definition(u, document) else {
@@ -700,7 +752,12 @@ fn underlay_vectors(
codec::entities::UnderlayType::Pdf => {
page_vectors(&super::pdf_layers::underlay_source(u, &def.file_path), page)
}
kind => super::underlay_vector::page_vectors(kind, &def.file_path, page, &super::pdf_layers::hidden_layers(u)),
kind => super::underlay_vector::page_vectors(
kind,
&def.file_path,
page,
&super::pdf_layers::hidden_layers(u),
),
}
}
@@ -723,7 +780,9 @@ pub fn underlay_snap_geometry(
u: &codec::entities::Underlay,
document: &codec::CadDocument,
) -> Vec<SnapPiece> {
if !underlay_osnap(u.underlay_type) || !u.flags.contains(codec::entities::UnderlayDisplayFlags::ON) {
if !underlay_osnap(u.underlay_type)
|| !u.flags.contains(codec::entities::UnderlayDisplayFlags::ON)
{
return Vec::new();
}
let Some(def) = crate::entities::underlay::definition(u, document) else {
@@ -739,7 +798,11 @@ pub fn underlay_snap_geometry(
// A circle stays one only while the placement scales both axes alike.
let uniform = (u.x_scale.abs() - u.y_scale.abs()).abs() <= 1e-9 * u.x_scale.abs().max(1e-12);
let flatten = |seg: &Segment, points: &mut Vec<[f64; 3]>| {
let steps = if matches!(seg, Segment::Cubic(..)) { 12 } else { 1 };
let steps = if matches!(seg, Segment::Cubic(..)) {
12
} else {
1
};
for k in 1..=steps {
points.push(world(bezier(seg, k as f64 / steps as f64)));
}
@@ -757,14 +820,20 @@ pub fn underlay_snap_geometry(
flatten(seg, &mut points);
}
total += points.len();
out.push(SnapPiece { points, circle: Some((world(c), r * u.x_scale.abs())) });
out.push(SnapPiece {
points,
circle: Some((world(c), r * u.x_scale.abs())),
});
continue;
}
for seg in &sp.segments {
let mut points = vec![world(seg.start())];
flatten(seg, &mut points);
total += points.len();
out.push(SnapPiece { points, circle: None });
out.push(SnapPiece {
points,
circle: None,
});
}
}
}
+302 -76
View File
@@ -72,15 +72,27 @@ fn decode(path: &Path) -> Option<CloudPoints> {
.is_some_and(|e| e.eq_ignore_ascii_case("rcp"));
// A project lists its scans with their own transforms; a scan file
// carries the same transform in its header.
let stem = |p: &Path| p.file_stem().map_or_else(String::new, |s| s.to_string_lossy().into_owned());
let stem = |p: &Path| {
p.file_stem()
.map_or_else(String::new, |s| s.to_string_lossy().into_owned())
};
let scans: Vec<(rcs::Scan, [[f64; 3]; 3], String, String)> = if is_project {
rcp::scans(&bytes, path)?
.into_iter()
.filter_map(|entry| {
let scan = rcs::decode(&std::fs::read(&entry.path).ok()?)?;
// The project's identifier, else the scan file's own.
let id = if entry.id.is_empty() { scan.id.clone() } else { entry.id.clone() };
Some((scan, [entry.translation, entry.rotation, entry.scale], stem(&entry.path), id))
let id = if entry.id.is_empty() {
scan.id.clone()
} else {
entry.id.clone()
};
Some((
scan,
[entry.translation, entry.rotation, entry.scale],
stem(&entry.path),
id,
))
})
.collect()
} else {
@@ -108,12 +120,20 @@ fn decode(path: &Path) -> Option<CloudPoints> {
let place = |p: &[f64; 3]| {
let s = [p[0] * scale[0], p[1] * scale[1], p[2] * scale[2]];
let r = rotate.map(|row| row[0] * s[0] + row[1] * s[1] + row[2] * s[2]);
[r[0] + translation[0], r[1] + translation[1], r[2] + translation[2]]
[
r[0] + translation[0],
r[1] + translation[1],
r[2] + translation[2],
]
};
let first = cloud.positions.len();
cloud.positions.extend(scan.local.iter().map(place));
let id = if id.is_empty() { name.clone() } else { id };
cloud.scan_ranges.push(ScanRange { name, id, range: first..cloud.positions.len() });
cloud.scan_ranges.push(ScanRange {
name,
id,
range: first..cloud.positions.len(),
});
let [lo, hi] = scan.bounds;
for corner in 0..8 {
let p = place(&[
@@ -135,7 +155,9 @@ fn decode(path: &Path) -> Option<CloudPoints> {
}
cloud.intensity.extend_from_slice(&scan.intensity);
cloud.normals.extend(scan.normals.iter().map(|n| {
let r = rotate.map(|row| row[0] * f64::from(n[0]) + row[1] * f64::from(n[1]) + row[2] * f64::from(n[2]));
let r = rotate.map(|row| {
row[0] * f64::from(n[0]) + row[1] * f64::from(n[1]) + row[2] * f64::from(n[2])
});
r.map(|v| v as f32)
}));
}
@@ -204,7 +226,11 @@ pub(crate) fn resolve_source(document: &CadDocument, data: &PointCloudExData) ->
if path.is_absolute() && path.is_file() {
return Some(path);
}
if let Some(found) = sources().lock().ok().and_then(|map| map.get(stored).cloned()) {
if let Some(found) = sources()
.lock()
.ok()
.and_then(|map| map.get(stored).cloned())
{
if found.is_file() {
return Some(found);
}
@@ -291,7 +317,11 @@ pub(crate) fn shows_no_points(document: &CadDocument, data: &PointCloudExData) -
return true;
};
data.hidden_regions.contains(&0)
|| (!cloud.scan_ranges.is_empty() && cloud.scan_ranges.iter().all(|s| data.hidden_scans.contains(&s.id)))
|| (!cloud.scan_ranges.is_empty()
&& cloud
.scan_ranges
.iter()
.all(|s| data.hidden_scans.contains(&s.id)))
}
/// The points `data` shows, placed through its origin and axes and then
@@ -305,21 +335,31 @@ pub(crate) fn placed(
hidden: &[String],
) -> Option<Arc<PlacedCloud>> {
let cloud = load(&resolve_source(document, data)?)?;
let skipped: Vec<std::ops::Range<usize>> =
if hidden.iter().any(|h| h == UNASSIGNED_OFF) {
vec![0..cloud.positions.len()]
} else {
cloud.scan_ranges.iter().filter(|s| hidden.contains(&s.id)).map(|s| s.range.clone()).collect()
};
let skipped: Vec<std::ops::Range<usize>> = if hidden.iter().any(|h| h == UNASSIGNED_OFF) {
vec![0..cloud.positions.len()]
} else {
cloud
.scan_ranges
.iter()
.filter(|s| hidden.contains(&s.id))
.map(|s| s.range.clone())
.collect()
};
// The points shown: POINTCLOUDDENSITY per cent of POINTCLOUDPOINTMAX,
// in tenths by POINTCLOUDLOD, every n-th point of a larger cloud.
let shown = |name| setting(name).map_or(0, |s| setting_value(document, s));
let limit =
(shown("POINTCLOUDPOINTMAX") * shown("POINTCLOUDDENSITY") / 100 * shown("POINTCLOUDLOD") / 10).max(1) as usize;
let limit = (shown("POINTCLOUDPOINTMAX") * shown("POINTCLOUDDENSITY") / 100
* shown("POINTCLOUDLOD")
/ 10)
.max(1) as usize;
let step = cloud.positions.len().div_ceil(limit).max(1);
let style = Stylization::of(document, data, object_color);
let mut origin = data.ucs_origin;
let mut axes = [data.ucs_x_direction, data.ucs_y_direction, data.ucs_z_direction];
let mut axes = [
data.ucs_x_direction,
data.ucs_y_direction,
data.ucs_z_direction,
];
if let Some(transform) = transform {
let placed = transform.apply(origin);
axes = axes.map(|axis| transform.apply(origin + axis) - placed);
@@ -345,7 +385,11 @@ pub(crate) fn placed(
axes.iter().map(bits).for_each(|v| v.hash(&mut h));
for crop in &crops {
let frame = [crop.plane, crop.x_direction, crop.y_direction];
frame.iter().chain(&crop.points).map(bits).for_each(|v| v.hash(&mut h));
frame
.iter()
.chain(&crop.points)
.map(bits)
.for_each(|v| v.hash(&mut h));
(crop.inside, crop.inverted).hash(&mut h);
}
step.hash(&mut h);
@@ -366,10 +410,13 @@ pub(crate) fn placed(
let (low, high) = if data.elevation_apply_to_fixed_range {
(data.elevation_min, data.elevation_max)
} else {
cloud.positions.iter().fold((f64::MAX, f64::MIN), |(lo, hi), p| {
let z = world(p).z;
(lo.min(z), hi.max(z))
})
cloud
.positions
.iter()
.fold((f64::MAX, f64::MIN), |(lo, hi), p| {
let z = world(p).z;
(lo.min(z), hi.max(z))
})
};
let scan_color = |i: usize| {
if cloud.has_rgb {
@@ -382,33 +429,55 @@ pub(crate) fn placed(
};
// A value outside a scheme's range takes the end colour, the scan
// colour, or is hidden (`None`), as the cloud's out-of-range choice says.
let along = |colors: &[[u8; 3]], t: f64, gradient: bool, out_of_range: i16, fixed: bool, i: usize| {
if fixed && !(0.0..=1.0).contains(&t) {
return match out_of_range {
0 => Some(ramp(colors, t, gradient)),
2 => None,
_ => Some(scan_color(i)),
};
}
Some(ramp(colors, t, gradient))
};
let along =
|colors: &[[u8; 3]], t: f64, gradient: bool, out_of_range: i16, fixed: bool, i: usize| {
if fixed && !(0.0..=1.0).contains(&t) {
return match out_of_range {
0 => Some(ramp(colors, t, gradient)),
2 => None,
_ => Some(scan_color(i)),
};
}
Some(ramp(colors, t, gradient))
};
let color = |i: usize, z: f64| -> Option<[u8; 4]> {
let [r, g, b] = match &style {
Stylization::Object(c) => *c,
Stylization::Normal => match cloud.normals.get(i) {
Some(n) => {
let w = axes[0] * f64::from(n[0]) + axes[1] * f64::from(n[1]) + axes[2] * f64::from(n[2]);
let w = axes[0] * f64::from(n[0])
+ axes[1] * f64::from(n[1])
+ axes[2] * f64::from(n[2]);
let w = w / w.length().max(1e-12);
[w.x, w.y, w.z].map(|v| ((v + 1.0) * 127.5).round().clamp(0.0, 255.0) as u8)
}
None => object_color,
},
Stylization::Ramp { colors, gradient, intensity: true, range, out_of_range } => {
let percent = f64::from(cloud.intensity.get(i).copied().unwrap_or(0)) * 100.0 / 255.0;
Stylization::Ramp {
colors,
gradient,
intensity: true,
range,
out_of_range,
} => {
let percent =
f64::from(cloud.intensity.get(i).copied().unwrap_or(0)) * 100.0 / 255.0;
let (from, to) = (range.0 as f64, range.1 as f64);
along(colors, (percent - from) / (to - from).max(1e-9), *gradient, *out_of_range, true, i)?
along(
colors,
(percent - from) / (to - from).max(1e-9),
*gradient,
*out_of_range,
true,
i,
)?
}
Stylization::Ramp { colors, gradient, out_of_range, .. } => along(
Stylization::Ramp {
colors,
gradient,
out_of_range,
..
} => along(
colors,
(z - low) / (high - low).max(1e-9),
*gradient,
@@ -431,7 +500,8 @@ pub(crate) fn placed(
};
let pos = [w.x as f32, w.y as f32, w.z as f32];
let normal = cloud.normals.get(i).map_or([0; 4], |n| {
let v = axes[0] * f64::from(n[0]) + axes[1] * f64::from(n[1]) + axes[2] * f64::from(n[2]);
let v =
axes[0] * f64::from(n[0]) + axes[1] * f64::from(n[1]) + axes[2] * f64::from(n[2]);
let v = v / v.length().max(1e-12);
[v.x, v.y, v.z, 0.0].map(|c| (c * 127.0).round() as i8)
});
@@ -525,7 +595,13 @@ enum Stylization {
/// A colour scheme over intensity (per cent, in the range given) or
/// elevation, blended or in bands; values out of range per
/// `out_of_range` (0 end colours, 1 scan colours, 2 hidden).
Ramp { colors: Vec<[u8; 3]>, gradient: bool, intensity: bool, range: (i64, i64), out_of_range: i16 },
Ramp {
colors: Vec<[u8; 3]>,
gradient: bool,
intensity: bool,
range: (i64, i64),
out_of_range: i16,
},
}
impl Stylization {
@@ -533,31 +609,66 @@ impl Stylization {
let (scheme, intensity, gradient) = match data.stylization_type {
2 => return Self::Object(object_color),
3 => return Self::Normal,
5 => (&data.intensity_color_scheme, true, data.intensity_as_gradient),
4 => (&data.current_color_scheme, false, data.elevation_as_gradient),
5 => (
&data.intensity_color_scheme,
true,
data.intensity_as_gradient,
),
4 => (
&data.current_color_scheme,
false,
data.elevation_as_gradient,
),
// Classification has no data here: scan colours.
_ => return Self::Scan,
};
let colors = ramp_colors(document, scheme, if intensity { "Spectrum" } else { "Earth" });
let colors = ramp_colors(
document,
scheme,
if intensity { "Spectrum" } else { "Earth" },
);
let range = if intensity && data.intensity_max > data.intensity_min {
(i64::from(data.intensity_min), i64::from(data.intensity_max))
} else {
(0, 100)
};
let out_of_range =
if intensity { data.intensity_out_of_range_behavior } else { data.elevation_out_of_range_behavior };
Self::Ramp { colors, gradient, intensity, range: (range.0, range.1), out_of_range }
let out_of_range = if intensity {
data.intensity_out_of_range_behavior
} else {
data.elevation_out_of_range_behavior
};
Self::Ramp {
colors,
gradient,
intensity,
range: (range.0, range.1),
out_of_range,
}
}
}
/// The colour schemes a new drawing's colour map carries, high end first.
const BUILTIN_RAMPS: [(&str, &[u32]); 7] = [
("Hydro", &[0x00bfff, 0x007ca5, 0x005f7f, 0x00394c, 0x00264c, 0x00134c, 0x00004c, 0x000026]),
("Grayscale", &[0xffffff, 0xcccccc, 0x999999, 0x666666, 0x333333, 0x000000]),
(
"Hydro",
&[
0x00bfff, 0x007ca5, 0x005f7f, 0x00394c, 0x00264c, 0x00134c, 0x00004c, 0x000026,
],
),
(
"Grayscale",
&[0xffffff, 0xcccccc, 0x999999, 0x666666, 0x333333, 0x000000],
),
("Earth", &[0xffffc8, 0xf0d796, 0xe1af64, 0xd28732, 0xc35f00]),
("Blues", &[0x0000ff, 0x0000a5, 0x00007f, 0x00004c, 0x000026]),
("Greens", &[0x00ff00, 0x00a500, 0x007f00, 0x004c00, 0x002600]),
("Spectrum", &[0xff00ff, 0x0000ff, 0x00ffff, 0x00ff00, 0xffff00, 0xff0000]),
(
"Greens",
&[0x00ff00, 0x00a500, 0x007f00, 0x004c00, 0x002600],
),
(
"Spectrum",
&[0xff00ff, 0x0000ff, 0x00ffff, 0x00ff00, 0xffff00, 0xff0000],
),
("Reds", &[0xff0000, 0xa50000, 0x7f0000, 0x4c0000, 0x260000]),
];
@@ -580,7 +691,12 @@ pub(crate) fn schemes(document: &CadDocument) -> Vec<(String, String, Vec<[u8; 3
.color_ramps
.iter()
.map(|r| {
let colors = r.colors.iter().filter(|c| c.visible).map(|c| rgb(c.color as u32)).collect();
let colors = r
.colors
.iter()
.filter(|c| c.visible)
.map(|c| rgb(c.color as u32))
.collect();
(r.id.clone(), r.name.clone(), colors)
})
.collect();
@@ -590,7 +706,11 @@ pub(crate) fn schemes(document: &CadDocument) -> Vec<(String, String, Vec<[u8; 3
.iter()
.map(|(name, colors)| {
let id = crate::entities::extended::point_cloud_ramp_id(name).to_string();
(id, name.to_string(), colors.iter().map(|c| rgb(*c)).collect())
(
id,
name.to_string(),
colors.iter().map(|c| rgb(*c)).collect(),
)
})
.collect()
}
@@ -627,9 +747,17 @@ fn ramp_colors(document: &CadDocument, id: &str, fallback: &str) -> Vec<[u8; 3]>
let ClassObjectData::PointCloudColorMap(map) = &object.data else {
continue;
};
if let Some(found) = map.color_ramps.iter().find(|r| !id.is_empty() && r.id.eq_ignore_ascii_case(id)) {
let colors: Vec<[u8; 3]> =
found.colors.iter().filter(|c| c.visible).map(|c| rgb(c.color as u32)).collect();
if let Some(found) = map
.color_ramps
.iter()
.find(|r| !id.is_empty() && r.id.eq_ignore_ascii_case(id))
{
let colors: Vec<[u8; 3]> = found
.colors
.iter()
.filter(|c| c.visible)
.map(|c| rgb(c.color as u32))
.collect();
if !colors.is_empty() {
return colors;
}
@@ -642,14 +770,22 @@ fn ramp_colors(document: &CadDocument, id: &str, fallback: &str) -> Vec<[u8; 3]>
BUILTIN_RAMPS
.iter()
.find(|(ramp, _)| *ramp == name)
.map_or_else(|| vec![[255; 3]], |(_, colors)| colors.iter().map(|c| rgb(*c)).collect())
.map_or_else(
|| vec![[255; 3]],
|(_, colors)| colors.iter().map(|c| rgb(*c)).collect(),
)
}
/// The colour at `t` (0–1, clamped) along a scheme: blended between its
/// colours, or the band `t` falls in. A scheme lists its high end first
/// (Spectrum: magenta at the highest value, red at the lowest).
fn ramp(colors: &[[u8; 3]], t: f64, gradient: bool) -> [u8; 3] {
let t = 1.0 - if t.is_finite() { t.clamp(0.0, 1.0) } else { 0.0 };
let t = 1.0
- if t.is_finite() {
t.clamp(0.0, 1.0)
} else {
0.0
};
if colors.len() < 2 {
return colors.first().copied().unwrap_or([255; 3]);
}
@@ -674,21 +810,96 @@ pub(crate) struct Setting {
/// The reference's defaults and ranges.
const SETTINGS: [Setting; 15] = [
Setting { name: "POINTCLOUDDENSITY", default: 15, min: 1, max: 100 },
Setting { name: "POINTCLOUDRTDENSITY", default: 5, min: 1, max: 100 },
Setting { name: "POINTCLOUDPOINTSIZE", default: 2, min: 1, max: 10 },
Setting { name: "POINTCLOUDLOD", default: 10, min: 1, max: 10 },
Setting { name: "POINTCLOUDLOCK", default: 0, min: 0, max: 1 },
Setting { name: "POINTCLOUDAUTOUPDATE", default: 0, min: 0, max: 1 },
Setting { name: "POINTCLOUDVISRETAIN", default: 1, min: 0, max: 1 },
Setting { name: "POINTCLOUDSHADING", default: 0, min: 0, max: 1 },
Setting { name: "POINTCLOUD2DVSDISPLAY", default: 0, min: 0, max: 1 },
Setting { name: "POINTCLOUDLIGHTSOURCE", default: 0, min: 0, max: 1 },
Setting { name: "POINTCLOUDLIGHTING", default: 0, min: 0, max: 2 },
Setting { name: "POINTCLOUDBOUNDARY", default: 1, min: 0, max: 2 },
Setting { name: "POINTCLOUDCACHESIZE", default: 512, min: 0, max: 32000 },
Setting { name: "POINTCLOUDPOINTMAX", default: 10_000_000, min: 1_000_000, max: 25_000_000 },
Setting { name: "POINTCLOUDPOINTMAXLEGACY", default: 1_500_000, min: 1_500_000, max: 10_000_000 },
Setting {
name: "POINTCLOUDDENSITY",
default: 15,
min: 1,
max: 100,
},
Setting {
name: "POINTCLOUDRTDENSITY",
default: 5,
min: 1,
max: 100,
},
Setting {
name: "POINTCLOUDPOINTSIZE",
default: 2,
min: 1,
max: 10,
},
Setting {
name: "POINTCLOUDLOD",
default: 10,
min: 1,
max: 10,
},
Setting {
name: "POINTCLOUDLOCK",
default: 0,
min: 0,
max: 1,
},
Setting {
name: "POINTCLOUDAUTOUPDATE",
default: 0,
min: 0,
max: 1,
},
Setting {
name: "POINTCLOUDVISRETAIN",
default: 1,
min: 0,
max: 1,
},
Setting {
name: "POINTCLOUDSHADING",
default: 0,
min: 0,
max: 1,
},
Setting {
name: "POINTCLOUD2DVSDISPLAY",
default: 0,
min: 0,
max: 1,
},
Setting {
name: "POINTCLOUDLIGHTSOURCE",
default: 0,
min: 0,
max: 1,
},
Setting {
name: "POINTCLOUDLIGHTING",
default: 0,
min: 0,
max: 2,
},
Setting {
name: "POINTCLOUDBOUNDARY",
default: 1,
min: 0,
max: 2,
},
Setting {
name: "POINTCLOUDCACHESIZE",
default: 512,
min: 0,
max: 32000,
},
Setting {
name: "POINTCLOUDPOINTMAX",
default: 10_000_000,
min: 1_000_000,
max: 25_000_000,
},
Setting {
name: "POINTCLOUDPOINTMAXLEGACY",
default: 1_500_000,
min: 1_500_000,
max: 10_000_000,
},
];
/// How a value was refused: the lines to show, and whether the prompt asks
@@ -701,21 +912,33 @@ pub(crate) struct Refusal {
impl Setting {
pub(crate) fn check(&self, text: &str) -> Result<i64, Refusal> {
let Ok(value) = text.trim().parse::<i64>() else {
return Err(Refusal { lines: vec!["Requires an integer value.".to_string()], ask_again: true });
return Err(Refusal {
lines: vec!["Requires an integer value.".to_string()],
ask_again: true,
});
};
if (self.min..=self.max).contains(&value) {
return Ok(value);
}
Err(if self.max > 100 {
Refusal {
lines: vec![format!("Cannot set {} to that value.", self.name), "*Invalid*".to_string()],
lines: vec![
format!("Cannot set {} to that value.", self.name),
"*Invalid*".to_string(),
],
ask_again: false,
}
} else if self.min == 0 && self.max == 1 {
Refusal { lines: vec!["Requires 0 or 1 only.".to_string()], ask_again: true }
Refusal {
lines: vec!["Requires 0 or 1 only.".to_string()],
ask_again: true,
}
} else {
Refusal {
lines: vec![format!("Requires an integer between {} and {}.", self.min, self.max)],
lines: vec![format!(
"Requires an integer between {} and {}.",
self.min, self.max
)],
ask_again: true,
}
})
@@ -723,7 +946,10 @@ impl Setting {
}
pub(crate) fn setting(name: &str) -> Option<Setting> {
SETTINGS.iter().copied().find(|setting| setting.name.eq_ignore_ascii_case(name))
SETTINGS
.iter()
.copied()
.find(|setting| setting.name.eq_ignore_ascii_case(name))
}
/// A setting's value in the drawing, or its default.
+6 -1
View File
@@ -54,7 +54,12 @@ pub fn scans(bytes: &[u8], project: &Path) -> Option<Vec<ScanEntry>> {
if nodes.is_empty() {
continue;
}
return Some(nodes.into_iter().filter_map(|node| entry(node, folder)).collect());
return Some(
nodes
.into_iter()
.filter_map(|node| entry(node, folder))
.collect(),
);
}
None
}
+15 -5
View File
@@ -40,11 +40,15 @@ pub struct Scan {
const NODE_RECORD: usize = 368;
fn u32_at(b: &[u8], at: usize) -> Option<u32> {
Some(u32::from_le_bytes(b.get(at..at.checked_add(4)?)?.try_into().ok()?))
Some(u32::from_le_bytes(
b.get(at..at.checked_add(4)?)?.try_into().ok()?,
))
}
fn u64_at(b: &[u8], at: usize) -> Option<u64> {
Some(u64::from_le_bytes(b.get(at..at.checked_add(8)?)?.try_into().ok()?))
Some(u64::from_le_bytes(
b.get(at..at.checked_add(8)?)?.try_into().ok()?,
))
}
fn f64_at(b: &[u8], at: usize) -> Option<f64> {
@@ -143,7 +147,11 @@ pub fn decode(b: &[u8]) -> Option<Scan> {
for level in 0..32 {
entries = entries.checked_add(u32_at(b, record + 108 + level * 4)? as usize)?;
}
if entries.checked_mul(4)?.checked_add(points.checked_mul(16)?)? != size {
if entries
.checked_mul(4)?
.checked_add(points.checked_mul(16)?)?
!= size
{
return None;
}
let records = block + entries * 4;
@@ -155,10 +163,12 @@ pub fn decode(b: &[u8]) -> Option<Scan> {
for rec in bytes.chunks_exact(16) {
let v = u64::from_le_bytes(rec[0..8].try_into().ok()?);
let q = |shift: u32| ((v >> shift) & 0x3FFFF) as f64 * 1e-3;
scan.local.push([cube_min[0] + q(0), cube_min[1] + q(18), cube_min[2] + q(36)]);
scan.local
.push([cube_min[0] + q(0), cube_min[1] + q(18), cube_min[2] + q(36)]);
scan.intensity.push(rec[8]);
scan.colors.push([rec[11], rec[10], rec[9]]);
scan.normals.push(normal(u16::from_le_bytes([rec[12], rec[13]])));
scan.normals
.push(normal(u16::from_le_bytes([rec[12], rec[13]])));
}
block = end;
}
+236 -93
View File
@@ -3,22 +3,31 @@
use codec::entities::{AcisData, EmbeddedEntity, Region, Wire};
use codec::types::Vector3;
use codec::{EntityType, Handle};
use glam::DVec3;
use kernel::brep::{self, Body, FaceKey};
use kernel::geom2d::{contains, ring_nesting_depths, signed_area, Curve, Line, Tolerance};
use kernel::space::Plane;
use glam::DVec3;
use rustc_hash::FxHashMap;
use super::{sweep_model, wire_model::WireModel};
use crate::command::WorkingPlane;
use crate::scene::{boundary_faces, exact_hatch_paths, ring_source_handles, BoundarySource, Scene};
use super::{sweep_model, wire_model::WireModel};
const BOUNDARY_TOLERANCE: f64 = 1e-6;
#[derive(Clone)]
pub enum PresspullTargetKind {
Profile { entity: EntityType, source: Option<Handle>, owner: Option<Handle> },
Face { handle: Handle, face: FaceKey, body: Body, offset: bool },
Profile {
entity: EntityType,
source: Option<Handle>,
owner: Option<Handle>,
},
Face {
handle: Handle,
face: FaceKey,
body: Body,
offset: bool,
},
}
#[derive(Clone)]
@@ -37,7 +46,8 @@ pub fn profile_geometry(entity: &EntityType) -> Option<(Plane, Vec<Vec<Curve>>,
}
let embedded = match entity {
EntityType::Region(region) => Some((
EmbeddedEntity::Region(region.clone()), glam::DMat4::IDENTITY.to_cols_array(),
EmbeddedEntity::Region(region.clone()),
glam::DMat4::IDENTITY.to_cols_array(),
)),
_ => sweep_model::embedded_revolve_profile(entity),
};
@@ -74,17 +84,22 @@ fn eligible(scene: &Scene, entity: &EntityType) -> bool {
&& !scene.interaction_layer_frozen(&common.layer)
&& !scene.is_layer_locked(common.handle)
&& scene.belongs_to_visible_block(
common.handle, common.owner_handle, scene.interaction_block_handle(),
common.handle,
common.owner_handle,
scene.interaction_block_handle(),
)
&& !crate::scene::annotative::annotative_offscale_for(
&scene.document, common, scene.displayed_annotation_scale_handle(),
&scene.document,
common,
scene.displayed_annotation_scale_handle(),
scene.annotation_all_visible(),
)
}
fn working_plane(plane: Plane) -> WorkingPlane {
WorkingPlane::new(
DVec3::from_array(plane.origin), DVec3::from_array(plane.x_axis),
DVec3::from_array(plane.origin),
DVec3::from_array(plane.x_axis),
DVec3::from_array(plane.y_axis),
)
}
@@ -98,63 +113,97 @@ fn curves_on_plane(source: Plane, curves: &[Curve], plane: WorkingPlane) -> Vec<
x_axis: [x_axis.x, x_axis.y].into(),
y_axis: [y_axis.x, y_axis.y].into(),
};
curves.iter().filter_map(|curve| {
// A line belongs to more than one plane. Its arbitrary source axes
// must not disqualify an edge which lies in the requested work plane.
if let Curve::Line(line) = curve {
let start = plane.to_local(DVec3::from_array(source.point_at(line.start)));
let end = plane.to_local(DVec3::from_array(source.point_at(line.end)));
return (start.z.abs() <= BOUNDARY_TOLERANCE && end.z.abs() <= BOUNDARY_TOLERANCE)
.then_some(Curve::Line(Line { start: [start.x, start.y], end: [end.x, end.y] }));
}
if origin.z.abs() > BOUNDARY_TOLERANCE || x_axis.z.abs() > 1e-9 || y_axis.z.abs() > 1e-9 {
return None;
}
curve.transformed(&transform)
}).collect()
curves
.iter()
.filter_map(|curve| {
// A line belongs to more than one plane. Its arbitrary source axes
// must not disqualify an edge which lies in the requested work plane.
if let Curve::Line(line) = curve {
let start = plane.to_local(DVec3::from_array(source.point_at(line.start)));
let end = plane.to_local(DVec3::from_array(source.point_at(line.end)));
return (start.z.abs() <= BOUNDARY_TOLERANCE && end.z.abs() <= BOUNDARY_TOLERANCE)
.then_some(Curve::Line(Line {
start: [start.x, start.y],
end: [end.x, end.y],
}));
}
if origin.z.abs() > BOUNDARY_TOLERANCE || x_axis.z.abs() > 1e-9 || y_axis.z.abs() > 1e-9
{
return None;
}
curve.transformed(&transform)
})
.collect()
}
fn boundary_source(curves: Vec<Curve>) -> Option<BoundarySource> {
let curves = curves.into_iter().filter(|curve| {
let length = curve.length();
length.is_finite() && length > BOUNDARY_TOLERANCE
}).collect::<Vec<_>>();
let segments = curves.iter().flat_map(|curve| {
curve.tessellate_angle(kernel::tessellation::DEFAULT_ANGLE)
.windows(2).filter_map(|pair| {
let start = pair[0];
let end = pair[1];
(start.iter().chain(&end).all(|value| value.is_finite()) && start != end)
.then_some(Line { start, end })
}).collect::<Vec<_>>()
}).collect::<Vec<_>>();
let curves = curves
.into_iter()
.filter(|curve| {
let length = curve.length();
length.is_finite() && length > BOUNDARY_TOLERANCE
})
.collect::<Vec<_>>();
let segments = curves
.iter()
.flat_map(|curve| {
curve
.tessellate_angle(kernel::tessellation::DEFAULT_ANGLE)
.windows(2)
.filter_map(|pair| {
let start = pair[0];
let end = pair[1];
(start.iter().chain(&end).all(|value| value.is_finite()) && start != end)
.then_some(Line { start, end })
})
.collect::<Vec<_>>()
})
.collect::<Vec<_>>();
(!segments.is_empty()).then_some(BoundarySource { segments, curves })
}
fn boundary_sources(scene: &Scene, plane: WorkingPlane) -> FxHashMap<Handle, BoundarySource> {
// Read native curve geometry, not shaded meshes, face isolines, temporary
// construction-history overlays or camera-clipped display segments.
scene.document.entities().filter(|entity| eligible(scene, entity)).filter_map(|entity| {
let (source, loops, _) = profile_geometry(entity)?;
let curves = loops.iter().flat_map(|ring| curves_on_plane(source, ring, plane)).collect();
boundary_source(curves).map(|source| (entity.common().handle, source))
}).collect()
scene
.document
.entities()
.filter(|entity| eligible(scene, entity))
.filter_map(|entity| {
let (source, loops, _) = profile_geometry(entity)?;
let curves = loops
.iter()
.flat_map(|ring| curves_on_plane(source, ring, plane))
.collect();
boundary_source(curves).map(|source| (entity.common().handle, source))
})
.collect()
}
pub(crate) fn selected_rings(sources: &FxHashMap<Handle, BoundarySource>, point: [f64; 2]) -> Option<Vec<Vec<[f64; 2]>>> {
pub(crate) fn selected_rings(
sources: &FxHashMap<Handle, BoundarySource>,
point: [f64; 2],
) -> Option<Vec<Vec<[f64; 2]>>> {
let rings = boundary_faces(sources, BOUNDARY_TOLERANCE);
let loops = boundary_loops(sources, &rings)?;
let tolerance = Tolerance::new(BOUNDARY_TOLERANCE);
let outer = rings.iter().enumerate()
let outer = rings
.iter()
.enumerate()
.filter(|(index, _)| contains(&loops[*index], point, tolerance))
.min_by(|(_, left), (_, right)| signed_area(left).abs().total_cmp(&signed_area(right).abs()))?
.min_by(|(_, left), (_, right)| {
signed_area(left).abs().total_cmp(&signed_area(right).abs())
})?
.0;
let depths = ring_nesting_depths(&rings);
let boundary = &loops[outer];
let mut result = vec![rings[outer].clone()];
for (index, ring) in rings.iter().enumerate() {
if index != outer && depths.get(index) == depths.get(outer).map(|depth| depth + 1).as_ref()
&& ring.first().is_some_and(|point| contains(boundary, *point, tolerance))
if index != outer
&& depths.get(index) == depths.get(outer).map(|depth| depth + 1).as_ref()
&& ring
.first()
.is_some_and(|point| contains(boundary, *point, tolerance))
{
result.push(ring.clone());
}
@@ -163,55 +212,82 @@ pub(crate) fn selected_rings(sources: &FxHashMap<Handle, BoundarySource>, point:
}
fn boundary_loops(
sources: &FxHashMap<Handle, BoundarySource>, rings: &[Vec<[f64; 2]>],
sources: &FxHashMap<Handle, BoundarySource>,
rings: &[Vec<[f64; 2]>],
) -> Option<Vec<Vec<Curve>>> {
let exterior = (0..rings.len()).map(|index| index == 0).collect::<Vec<_>>();
let paths = exact_hatch_paths(rings, &exterior, sources, BOUNDARY_TOLERANCE);
if paths.len() != rings.len() {
return None;
}
paths.iter().map(|path| {
path.edges.iter().map(crate::entities::hatch::edge_curve).collect::<Option<Vec<_>>>()
}).collect()
paths
.iter()
.map(|path| {
path.edges
.iter()
.map(crate::entities::hatch::edge_curve)
.collect::<Option<Vec<_>>>()
})
.collect()
}
pub(crate) fn boundary_region(
sources: &FxHashMap<Handle, BoundarySource>, rings: &[Vec<[f64; 2]>], plane: WorkingPlane,
sources: &FxHashMap<Handle, BoundarySource>,
rings: &[Vec<[f64; 2]>],
plane: WorkingPlane,
) -> Option<EntityType> {
let loops = boundary_loops(sources, rings)?;
region_from_loops(&loops, plane)
}
pub(crate) fn region_from_loops(loops: &[Vec<Curve>], plane: WorkingPlane) -> Option<EntityType> {
let kernel_plane = Plane::from_axes(plane.origin.to_array(), plane.x.to_array(), plane.y.to_array());
let kernel_plane = Plane::from_axes(
plane.origin.to_array(),
plane.x.to_array(),
plane.y.to_array(),
);
let sheet = brep::planar_region(kernel_plane, loops)?;
let document = crate::scene::convert::acis_export::solid_to_sat(&sheet)?;
let mut region = Region::new();
region.acis_data = AcisData::from_sat(&document.to_sat_string());
region.point_of_reference = Vector3::new(plane.origin.x, plane.origin.y, plane.origin.z);
region.wires = loops.iter().map(|ring| {
let mut points = Vec::new();
for curve in ring {
let skip = usize::from(!points.is_empty());
points.extend(curve.tessellate_angle(kernel::tessellation::DEFAULT_ANGLE)
.into_iter().skip(skip).map(|point| {
let world = kernel_plane.point_at(point);
Vector3::new(world[0], world[1], world[2])
}));
}
Wire::from_points(points)
}).collect();
region.wires = loops
.iter()
.map(|ring| {
let mut points = Vec::new();
for curve in ring {
let skip = usize::from(!points.is_empty());
points.extend(
curve
.tessellate_angle(kernel::tessellation::DEFAULT_ANGLE)
.into_iter()
.skip(skip)
.map(|point| {
let world = kernel_plane.point_at(point);
Vector3::new(world[0], world[1], world[2])
}),
);
}
Wire::from_points(points)
})
.collect();
Some(EntityType::Region(region))
}
fn face_at_point(scene: &Scene, point: DVec3, preferred: Option<Handle>) -> Option<(Handle, FaceKey, &Body)> {
fn face_at_point(
scene: &Scene,
point: DVec3,
preferred: Option<Handle>,
) -> Option<(Handle, FaceKey, &Body)> {
let mut handles = scene.solid_models.keys().copied().collect::<Vec<_>>();
handles.sort_by_key(|handle| handle.value());
for handle in handles {
if preferred.is_some_and(|preferred| preferred != handle) {
continue;
}
let Some(entity) = scene.document.get_entity(handle) else { continue; };
let Some(entity) = scene.document.get_entity(handle) else {
continue;
};
if !matches!(entity, EntityType::Solid3D(_)) || !eligible(scene, entity) {
continue;
}
@@ -227,13 +303,17 @@ fn profile_owner(scene: &Scene, plane: WorkingPlane, anchor: DVec3) -> Option<(H
let mut handles = scene.solid_models.keys().copied().collect::<Vec<_>>();
handles.sort_by_key(|handle| handle.value());
for handle in handles {
let Some(entity) = scene.document.get_entity(handle) else { continue; };
let Some(entity) = scene.document.get_entity(handle) else {
continue;
};
if !matches!(entity, EntityType::Solid3D(_)) || !eligible(scene, entity) {
continue;
}
let body = &scene.solid_models[&handle];
for face in body.face_keys() {
let Some(profile) = brep::planar_face_profile(body, face) else { continue; };
let Some(profile) = brep::planar_face_profile(body, face) else {
continue;
};
let outward = DVec3::from_array(profile.outward);
if outward.dot(plane.z).abs() < 1.0 - 1e-9
|| profile.plane.distance_to(anchor.to_array())?.abs() > BOUNDARY_TOLERANCE
@@ -254,13 +334,19 @@ fn profile_owner(scene: &Scene, plane: WorkingPlane, anchor: DVec3) -> Option<(H
/// means a bounded-area pick; closed profiles therefore retain their distinct
/// edge and interior behaviors. A solid handle prefers its exact trimmed face.
pub fn resolve_target(
scene: &Scene, handle: Option<Handle>, point: DVec3, plane: WorkingPlane, offset: bool,
scene: &Scene,
handle: Option<Handle>,
point: DVec3,
plane: WorkingPlane,
offset: bool,
) -> Result<PresspullTarget, String> {
if !point.is_finite() {
return Err("PRESSPULL: the selected point is not finite.".to_owned());
}
if let Some(handle) = handle {
let entity = scene.document.get_entity(handle)
let entity = scene
.document
.get_entity(handle)
.ok_or("PRESSPULL: the selected object no longer exists.")?;
if !eligible(scene, entity) {
return Err("PRESSPULL: select a visible object on an unlocked layer.".to_owned());
@@ -269,16 +355,27 @@ pub fn resolve_target(
let profile_plane = working_plane(source_plane);
let local = profile_plane.to_local(point);
let anchor = profile_plane.to_world(DVec3::new(local.x, local.y, 0.0));
let attached = closed.then(|| profile_owner(scene, profile_plane, anchor)).flatten();
let attached = closed
.then(|| profile_owner(scene, profile_plane, anchor))
.flatten();
let owner = attached.map(|(owner, _)| owner);
let direction = attached.map(|(_, outward)| outward).unwrap_or(profile_plane.z);
let direction = attached
.map(|(_, outward)| outward)
.unwrap_or(profile_plane.z);
return Ok(PresspullTarget {
kind: PresspullTargetKind::Profile { entity: entity.clone(), source: Some(handle), owner },
anchor, direction,
kind: PresspullTargetKind::Profile {
entity: entity.clone(),
source: Some(handle),
owner,
},
anchor,
direction,
});
}
if !matches!(entity, EntityType::Solid3D(_)) {
return Err("PRESSPULL: select a planar curve, bounded area or planar solid face.".to_owned());
return Err(
"PRESSPULL: select a planar curve, bounded area or planar solid face.".to_owned(),
);
}
}
@@ -296,25 +393,41 @@ pub fn resolve_target(
}
if let Some(rings) = selected_rings(&sources, [local.x, local.y]) {
let bounded_by_drawing = rings.iter().any(|ring| {
ring_source_handles(ring, &sources).into_iter().any(|source| source != owner)
ring_source_handles(ring, &sources)
.into_iter()
.any(|source| source != owner)
});
if bounded_by_drawing {
let entity = boundary_region(&sources, &rings, face_plane)
.ok_or("PRESSPULL: the selected boundary could not be reconstructed exactly.")?;
let entity = boundary_region(&sources, &rings, face_plane).ok_or(
"PRESSPULL: the selected boundary could not be reconstructed exactly.",
)?;
return Ok(PresspullTarget {
kind: PresspullTargetKind::Profile { entity, source: None, owner: Some(owner) },
anchor, direction,
kind: PresspullTargetKind::Profile {
entity,
source: None,
owner: Some(owner),
},
anchor,
direction,
});
}
}
}
return Ok(PresspullTarget {
kind: PresspullTargetKind::Face { handle: owner, face, body: body.clone(), offset },
anchor, direction,
kind: PresspullTargetKind::Face {
handle: owner,
face,
body: body.clone(),
offset,
},
anchor,
direction,
});
}
if handle.is_some() {
return Err("PRESSPULL: select inside a trimmed planar face, outside its holes.".to_owned());
return Err(
"PRESSPULL: select inside a trimmed planar face, outside its holes.".to_owned(),
);
}
let local = plane.to_local(point);
if local.z.abs() > BOUNDARY_TOLERANCE {
@@ -326,17 +439,33 @@ pub fn resolve_target(
let entity = boundary_region(&sources, &rings, plane)
.ok_or("PRESSPULL: the selected boundary could not be reconstructed exactly.")?;
Ok(PresspullTarget {
kind: PresspullTargetKind::Profile { entity, source: None, owner: None },
anchor: point, direction: plane.z,
kind: PresspullTargetKind::Profile {
entity,
source: None,
owner: None,
},
anchor: point,
direction: plane.z,
})
}
/// Curves only: mouse movement never triangulates a preview body.
pub fn preview_wires(target: &PresspullTarget, distance: f64, color: [f32; 4], isolines: usize) -> Vec<WireModel> {
pub fn preview_wires(
target: &PresspullTarget,
distance: f64,
color: [f32; 4],
isolines: usize,
) -> Vec<WireModel> {
if !distance.is_finite() || distance.abs() <= 1e-9 {
return Vec::new();
}
if let PresspullTargetKind::Face { body, face, offset: true, .. } = &target.kind {
if let PresspullTargetKind::Face {
body,
face,
offset: true,
..
} = &target.kind
{
// Offset changes the intersections with neighbouring surfaces, so a
// constant-section prism would misrepresent sloped walls and caps.
// The kernel's analytic offset performs no Boolean or triangulation;
@@ -354,27 +483,41 @@ pub fn preview_wires(target: &PresspullTarget, distance: f64, color: [f32; 4], i
// exact extrusion cage; its Boolean merge happens only on commit,
// never inside pointer movement. Unrelated faces and lumps need
// no rebuilding for a normal face-extrusion preview.
brep::planar_face_profile(body, *face).map(|profile| (profile.plane, profile.loops,
(DVec3::from_array(profile.outward) * distance).to_array()))
brep::planar_face_profile(body, *face).map(|profile| {
(
profile.plane,
profile.loops,
(DVec3::from_array(profile.outward) * distance).to_array(),
)
})
}
};
let Some((plane, loops, direction)) = geometry else { return Vec::new(); };
let Some((plane, loops, direction)) = geometry else {
return Vec::new();
};
let mut wires = Vec::new();
// Each loop is a lateral sheet. This wire-only overlay does not need the
// planar caps or complete solid topology of extrude_region.
for ring in loops {
let ring = brep::extrusion_profile_pieces(&ring);
let Some(body) = brep::extrude_surface(plane, &ring, direction) else { continue; };
let Some(body) = brep::extrude_surface(plane, &ring, direction) else {
continue;
};
wires.extend(preview_body_wires(&body, color, isolines));
}
wires
}
fn preview_body_wires(body: &Body, color: [f32; 4], isolines: usize) -> Vec<WireModel> {
let wireframe = brep::mesh::tessellate_wireframe(body,
let wireframe = brep::mesh::tessellate_wireframe(
body,
brep::mesh::TessellationTolerance::new(kernel::tessellation::DEFAULT_ANGLE, 1e-9)
.with_isolines(isolines));
wireframe.edges.into_iter().map(|edge| edge.positions)
.with_isolines(isolines),
);
wireframe
.edges
.into_iter()
.map(|edge| edge.positions)
.chain(wireframe.isolines.into_iter().map(|line| line.positions))
.filter(|points| points.len() >= 2)
.map(|points| WireModel::solid_f64("PRESSPULL-PREVIEW".to_owned(), points, color, false))
File diff suppressed because it is too large. Load diff
+28 -28
View File
@@ -12,10 +12,7 @@ const TOL: f64 = 1e-9;
fn tessellation(body: &Body) -> brep::mesh::BodyMesh {
brep::mesh::tessellate(
body,
brep::mesh::TessellationTolerance::new(
kernel::tessellation::DEFAULT_ANGLE,
TOL,
),
brep::mesh::TessellationTolerance::new(kernel::tessellation::DEFAULT_ANGLE, TOL),
)
}
@@ -75,13 +72,7 @@ pub fn cone_frustum_solid(
top_radius: f64,
height: f64,
) -> Option<Body> {
brep::make::frustum(
center,
base_x_radius,
base_y_radius,
top_radius,
height,
)
brep::make::frustum(center, base_x_radius, base_y_radius, top_radius, height)
}
/// Circular or elliptical cylinder standing on the local z = base plane.
@@ -284,12 +275,9 @@ pub fn grip_preview_wires(
) -> Vec<WireModel> {
let tessellation = brep::mesh::tessellate(
body,
brep::mesh::TessellationTolerance::new(
kernel::tessellation::DEFAULT_ANGLE,
TOL,
)
.with_uv_isolines(isolines[0], isolines[1])
.with_planar_isolines(planar_isolines),
brep::mesh::TessellationTolerance::new(kernel::tessellation::DEFAULT_ANGLE, TOL)
.with_uv_isolines(isolines[0], isolines[1])
.with_planar_isolines(planar_isolines),
);
tessellation
.edges
@@ -377,11 +365,7 @@ pub fn planar_face_normal(body: &Body, face: FaceKey) -> Option<[f64; 3]> {
/// - X-axis aligns along the boundary edge emanating from that vertex closest to the pick point.
/// - Z-axis is the face's outward normal.
/// - Y-axis is Z × X (in-plane, right-handed).
pub fn planar_face_ucs(
body: &Body,
face: FaceKey,
pick: [f64; 3],
) -> Option<codec::tables::Ucs> {
pub fn planar_face_ucs(body: &Body, face: FaceKey, pick: [f64; 3]) -> Option<codec::tables::Ucs> {
let normal = planar_face_normal(body, face)?;
let z = DVec3::from_array(normal).normalize_or_zero();
if z.length_squared() < 1e-12 {
@@ -626,7 +610,8 @@ fn mesh_from_tessellation(
}
}
set.complete = tessellation.missing_faces.is_empty();
set.curved_gens.push(super::mesh_model::CurvedGen { source: silhouette });
set.curved_gens
.push(super::mesh_model::CurvedGen { source: silhouette });
Some(set)
}
@@ -733,13 +718,28 @@ mod tests {
#[test]
fn all_primitives_triangulate() {
let c = [0.0, 0.0, 0.0];
assert!(tri_count(&box_solid(c, 10.0, 10.0, 10.0).unwrap()) >= 12, "box");
assert!(tri_count(&wedge_solid(c, 10.0, 10.0, 10.0).unwrap()) >= 6, "wedge");
assert!(tri_count(&cylinder_solid(c, 5.0, 12.0).unwrap()) > 20, "cylinder");
assert!(tri_count(&cone_frustum_solid(c, 5.0, 5.0, 0.0, 12.0).unwrap()) > 10, "cone");
assert!(
tri_count(&box_solid(c, 10.0, 10.0, 10.0).unwrap()) >= 12,
"box"
);
assert!(
tri_count(&wedge_solid(c, 10.0, 10.0, 10.0).unwrap()) >= 6,
"wedge"
);
assert!(
tri_count(&cylinder_solid(c, 5.0, 12.0).unwrap()) > 20,
"cylinder"
);
assert!(
tri_count(&cone_frustum_solid(c, 5.0, 5.0, 0.0, 12.0).unwrap()) > 10,
"cone"
);
assert!(tri_count(&sphere_solid(c, 5.0).unwrap()) > 50, "sphere");
assert!(tri_count(&torus_solid(c, 8.0, 2.0).unwrap()) > 50, "torus");
assert!(tri_count(&pyramid_solid(c, 5.0, 9.0, 6).unwrap()) >= 8, "pyramid");
assert!(
tri_count(&pyramid_solid(c, 5.0, 9.0, 6).unwrap()) >= 8,
"pyramid"
);
}
#[test]
+493 -65
View File
@@ -6,14 +6,21 @@ use codec::types::Vector3;
use codec::EntityType;
use kernel::brep::Body;
use crate::command::{ExtrudeMode, SweepOptions};
use super::sweep_model::{embedded_path, embedded_revolve_profile};
use crate::command::{ExtrudeMode, SweepOptions};
/// A 3D polyline profile or path as the reference records it: a wire body.
fn embedded_sweep_profile(entity: &EntityType) -> Option<(codec::entities::EmbeddedEntity, [f64; 16])> {
fn embedded_sweep_profile(
entity: &EntityType,
) -> Option<(codec::entities::EmbeddedEntity, [f64; 16])> {
match entity {
EntityType::Region(region) => Some((codec::entities::EmbeddedEntity::Region(region.clone()), glam::DMat4::IDENTITY.to_cols_array())),
EntityType::Polyline3D(value) => Some((polyline_wire(value)?, glam::DMat4::IDENTITY.to_cols_array())),
EntityType::Region(region) => Some((
codec::entities::EmbeddedEntity::Region(region.clone()),
glam::DMat4::IDENTITY.to_cols_array(),
)),
EntityType::Polyline3D(value) => {
Some((polyline_wire(value)?, glam::DMat4::IDENTITY.to_cols_array()))
}
_ => embedded_revolve_profile(entity),
}
}
@@ -28,7 +35,9 @@ fn embedded_sweep_path(path: &EntityType) -> Option<codec::entities::EmbeddedEnt
}
fn polyline_wire(value: &codec::entities::Polyline3D) -> Option<codec::entities::EmbeddedEntity> {
let points = value.vertices.iter()
let points = value
.vertices
.iter()
.map(|vertex| [vertex.position.x, vertex.position.y, vertex.position.z])
.collect::<Vec<_>>();
let mut document = codec::entities::acis::SatDocument::new();
@@ -48,7 +57,9 @@ pub fn is_sweep_profile(entity: &EntityType) -> bool {
/// The vertices of a 3D polyline profile that does not lie in one plane.
pub fn spatial_profile(entity: &EntityType) -> Option<(Vec<[f64; 3]>, bool)> {
let EntityType::Polyline3D(_) = entity else { return None };
let EntityType::Polyline3D(_) = entity else {
return None;
};
let (profile, transform) = embedded_sweep_profile(entity)?;
kernel::acis::sweep_spatial_profile(&profile, transform)
}
@@ -59,7 +70,11 @@ pub fn spatial_profile(entity: &EntityType) -> Option<(Vec<[f64; 3]>, bool)> {
/// refuses it: along a path with a corner (85036), and when the profile seen
/// from above crosses itself (85021); a side that is a point when seen from
/// above cannot be swept at all (`None` code).
pub fn spatial_sweep_record(profile: &EntityType, path: &EntityType, options: SweepOptions) -> Result<SolidHistorySweep, Option<u32>> {
pub fn spatial_sweep_record(
profile: &EntityType,
path: &EntityType,
options: SweepOptions,
) -> Result<SolidHistorySweep, Option<u32>> {
let (points, closed) = spatial_profile(profile).ok_or(None)?;
let count = points.len();
let sides = if closed { count } else { count - 1 };
@@ -71,11 +86,15 @@ pub fn spatial_sweep_record(profile: &EntityType, path: &EntityType, options: Sw
for i in 0..sides {
for j in i + 1..sides {
// Neighbouring sides share a corner.
if j == i + 1 || (closed && i == 0 && j == sides - 1) { continue; }
if j == i + 1 || (closed && i == 0 && j == sides - 1) {
continue;
}
let (p, r) = (plan(i), plan(i + 1) - plan(i));
let (q, s) = (plan(j), plan(j + 1) - plan(j));
let denominator = cross(r, s);
if denominator.abs() <= 1e-12 { continue; }
if denominator.abs() <= 1e-12 {
continue;
}
let t = cross(q - p, s) / denominator;
let u = cross(q - p, r) / denominator;
if (0.0..=1.0).contains(&t) && (0.0..=1.0).contains(&u) {
@@ -100,7 +119,9 @@ pub fn spatial_sweep_record(profile: &EntityType, path: &EntityType, options: Sw
bank: options.bank,
sweep_entity_transform: glam::DMat4::IDENTITY.to_cols_array(),
path_entity_transform: glam::DMat4::IDENTITY.to_cols_array(),
reference_point: base_point.map_or(Vector3::new(0.0, 0.0, 0.0), |p| Vector3::new(p[0], p[1], p[2])),
reference_point: base_point.map_or(Vector3::new(0.0, 0.0, 0.0), |p| {
Vector3::new(p[0], p[1], p[2])
}),
miter_option: 2,
..SolidHistorySweep::default()
};
@@ -126,13 +147,20 @@ pub fn scaled_sweep_path_refusal(path: &EntityType) -> Option<&'static str> {
Some(planar) => (planar.curve.is_closed(), true),
None => match path {
EntityType::Polyline3D(value) => {
let points = value.vertices.iter()
.map(|vertex| glam::DVec3::new(vertex.position.x, vertex.position.y, vertex.position.z))
let points = value
.vertices
.iter()
.map(|vertex| {
glam::DVec3::new(vertex.position.x, vertex.position.y, vertex.position.z)
})
.collect::<Vec<_>>();
let directions = points.windows(2)
let directions = points
.windows(2)
.filter_map(|pair| (pair[1] - pair[0]).try_normalize())
.collect::<Vec<_>>();
let straight = directions.windows(2).all(|pair| pair[0].dot(pair[1]) >= 1.0 - 1e-9);
let straight = directions
.windows(2)
.all(|pair| pair[0].dot(pair[1]) >= 1.0 - 1e-9);
(value.is_closed(), straight && !value.is_closed())
}
EntityType::Spline(value) => (value.flags.closed, value.degree > 1),
@@ -151,8 +179,9 @@ pub fn scaled_sweep_path_refusal(path: &EntityType) -> Option<&'static str> {
pub fn is_sweep_path(entity: &EntityType) -> bool {
match embedded_path(entity) {
Some(codec::entities::EmbeddedEntity::Spline(value)) => {
value.degree > 0 && (value.control_points.len() > value.degree as usize
|| value.fit_points.len() >= 2)
value.degree > 0
&& (value.control_points.len() > value.degree as usize
|| value.fit_points.len() >= 2)
}
Some(_) => crate::entities::curve::entity_curve(entity)
.is_some_and(|curve| curve.curve.length().is_finite() && curve.curve.length() > 1e-9),
@@ -161,20 +190,34 @@ pub fn is_sweep_path(entity: &EntityType) -> bool {
}
/// All selected profiles use one base point, preserving their relative offsets.
pub fn sweep_selection_options(profiles: &[EntityType], mut options: SweepOptions) -> Option<SweepOptions> {
pub fn sweep_selection_options(
profiles: &[EntityType],
mut options: SweepOptions,
) -> Option<SweepOptions> {
if options.base_point.is_some() {
return Some(options);
}
// Spatial profiles keep their own anchor.
let profiles = profiles.iter().filter(|profile| spatial_profile(profile).is_none()).collect::<Vec<_>>();
if profiles.is_empty() {
let profiles = profiles
.iter()
.filter(|profile| spatial_profile(profile).is_none())
.collect::<Vec<_>>();
// A single profile takes its base from the path it is swept along
// (`sweep_record`).
// ponytail: several profiles share the group anchor even when the path
// starts inside one of them; the reference rule for that case is unmeasured.
if profiles.len() < 2 {
return Some(options);
}
let geometry = profiles.iter().map(|profile| {
let (entity, transform) = embedded_sweep_profile(profile)?;
let (plane, wires, _) = kernel::acis::sweep_profile_geometry(&entity, transform).ok()?;
Some((plane, wires))
}).collect::<Option<Vec<_>>>()?;
let geometry = profiles
.iter()
.map(|profile| {
let (entity, transform) = embedded_sweep_profile(profile)?;
let (plane, wires, _) =
kernel::acis::sweep_profile_geometry(&entity, transform).ok()?;
Some((plane, wires))
})
.collect::<Option<Vec<_>>>()?;
options.base_point = Some(glam::DVec3::from_array(
kernel::brep::sweep_profile_group_base(&geometry)?,
));
@@ -184,31 +227,22 @@ pub fn sweep_selection_options(profiles: &[EntityType], mut options: SweepOption
/// The path traversed the other way when it was picked nearer its end
/// (measured in plan, as the reference measures the pick), else `None`.
fn reversed_toward_pick(path: &EntityType, pick: glam::DVec3) -> Option<EntityType> {
let (start, end) = match path {
EntityType::Polyline3D(value) if !value.is_closed() => {
let p = |v: &codec::entities::Vertex3DPolyline| glam::DVec3::new(v.position.x, v.position.y, v.position.z);
(p(value.vertices.first()?), p(value.vertices.last()?))
}
EntityType::Spline(value) if !value.flags.closed => {
let points = if value.control_points.is_empty() { &value.fit_points } else { &value.control_points };
let p = |v: &codec::types::Vector3| glam::DVec3::new(v.x, v.y, v.z);
(p(points.first()?), p(points.last()?))
}
_ => {
let planar = crate::entities::curve::entity_curve(path)?;
if planar.curve.is_closed() { return None; }
let at = |t: f64| glam::DVec3::from_array(planar.plane.point_at(planar.curve.point_at(t)));
(at(0.0), at(1.0))
}
};
let (start, end) = path_ends(path)?;
let flat = |p: glam::DVec3| p.truncate().distance(pick.truncate());
if flat(end) >= flat(start) { return None; }
if flat(end) >= flat(start) {
return None;
}
match path {
// An arc has one direction; reversed it is a one-segment polyline.
EntityType::Arc(arc) => {
let mut polyline = codec::LwPolyline::new();
let sweep = (arc.end_angle - arc.start_angle).rem_euclid(std::f64::consts::TAU);
let at = |angle: f64| codec::types::Vector2::new(arc.center.x + arc.radius * angle.cos(), arc.center.y + arc.radius * angle.sin());
let at = |angle: f64| {
codec::types::Vector2::new(
arc.center.x + arc.radius * angle.cos(),
arc.center.y + arc.radius * angle.sin(),
)
};
let mut first = codec::entities::LwVertex::new(at(arc.end_angle));
first.bulge = -(sweep / 4.0).tan();
polyline.vertices = vec![first, codec::entities::LwVertex::new(at(arc.start_angle))];
@@ -220,19 +254,68 @@ fn reversed_toward_pick(path: &EntityType, pick: glam::DVec3) -> Option<EntityTy
}
}
pub fn sweep_record(profile: &EntityType, path: &EntityType, options: SweepOptions) -> Option<SolidHistorySweep> {
/// The start and end of an open path.
fn path_ends(path: &EntityType) -> Option<(glam::DVec3, glam::DVec3)> {
Some(match path {
EntityType::Polyline3D(value) if !value.is_closed() => {
let p = |v: &codec::entities::Vertex3DPolyline| {
glam::DVec3::new(v.position.x, v.position.y, v.position.z)
};
(p(value.vertices.first()?), p(value.vertices.last()?))
}
EntityType::Spline(value) if !value.flags.closed => {
let points = if value.control_points.is_empty() {
&value.fit_points
} else {
&value.control_points
};
let p = |v: &codec::types::Vector3| glam::DVec3::new(v.x, v.y, v.z);
(p(points.first()?), p(points.last()?))
}
_ => {
let planar = crate::entities::curve::entity_curve(path)?;
if planar.curve.is_closed() {
return None;
}
let at =
|t: f64| glam::DVec3::from_array(planar.plane.point_at(planar.curve.point_at(t)));
(at(0.0), at(1.0))
}
})
}
pub fn sweep_record(
profile: &EntityType,
path: &EntityType,
options: SweepOptions,
) -> Option<SolidHistorySweep> {
// Placed from the picked end; the record keeps the original path, and
// the placed frame tells which end the sweep starts from.
if let Some(reversed) = options.path_pick.and_then(|pick| reversed_toward_pick(path, pick)) {
let mut record = sweep_record(profile, &reversed, SweepOptions { path_pick: None, ..options })?;
if let Some(reversed) = options
.path_pick
.and_then(|pick| reversed_toward_pick(path, pick))
{
let mut record = sweep_record(
profile,
&reversed,
SweepOptions {
path_pick: None,
..options
},
)?;
record.path_entity = Some(embedded_sweep_path(path)?);
return Some(record);
}
let (sweep_entity, sweep_entity_transform) = embedded_sweep_profile(profile)?;
let (plane, wires, _) = kernel::acis::sweep_profile_geometry(&sweep_entity, sweep_entity_transform).ok()?;
kernel::acis::sweep_profile_geometry(&sweep_entity, sweep_entity_transform).ok()?;
let base_point = match options.base_point {
Some(point) => point.to_array(),
None => kernel::brep::sweep_profile_base(plane, &wires)?,
None => kernel::acis::sweep_default_base(
&sweep_entity,
sweep_entity_transform,
&embedded_sweep_path(path)?,
)
.ok()?,
};
let mut base = SolidHistoryNodeBase::new(1);
base.transform = glam::DMat4::IDENTITY.to_cols_array();
@@ -258,7 +341,10 @@ pub fn sweep_record(profile: &EntityType, path: &EntityType, options: SweepOptio
/// The record the reference reads: the profile stored already placed at the
/// path start (base point, alignment and profile rotation applied) with flag
/// 295 set, and no reference point.
fn placed_sweep_record(profile: &EntityType, mut record: SolidHistorySweep) -> Option<SolidHistorySweep> {
fn placed_sweep_record(
profile: &EntityType,
mut record: SolidHistorySweep,
) -> Option<SolidHistorySweep> {
let (placed, _) = kernel::acis::sweep_history_placements(&record).ok()?;
let embedded_to_world = glam::DMat4::from_cols(
glam::DVec4::new(placed.x_axis[0], placed.x_axis[1], placed.x_axis[2], 0.0),
@@ -267,7 +353,8 @@ fn placed_sweep_record(profile: &EntityType, mut record: SolidHistorySweep) -> O
glam::DVec4::new(placed.origin[0], placed.origin[1], placed.origin[2], 1.0),
);
// Source world geometry -> placed world geometry.
let map = embedded_to_world * glam::DMat4::from_cols_array(&record.sweep_entity_transform).inverse();
let map =
embedded_to_world * glam::DMat4::from_cols_array(&record.sweep_entity_transform).inverse();
let m = map.to_cols_array_2d();
let transform = codec::types::Transform::from_matrix(codec::types::Matrix4 {
m: [
@@ -283,10 +370,15 @@ fn placed_sweep_record(profile: &EntityType, mut record: SolidHistorySweep) -> O
let (plane, _, _) = kernel::acis::sweep_profile_geometry(
record.sweep_entity.as_ref()?,
record.sweep_entity_transform,
).ok()?;
)
.ok()?;
let x_axis = glam::DVec3::from_array(plane.x_axis).try_normalize()?;
let normal = glam::DVec3::from_array(plane.normal()?);
let base = glam::DVec3::new(record.reference_point.x, record.reference_point.y, record.reference_point.z);
let base = glam::DVec3::new(
record.reference_point.x,
record.reference_point.y,
record.reference_point.z,
);
let source_frame = glam::DMat4::from_cols(
x_axis.extend(0.0),
normal.cross(x_axis).extend(0.0),
@@ -294,7 +386,10 @@ fn placed_sweep_record(profile: &EntityType, mut record: SolidHistorySweep) -> O
base.extend(1.0),
);
let mut moved = profile.clone();
crate::scene::view::dispatch::apply_transform(&mut moved, &crate::command::EntityTransform::Affine(transform));
crate::scene::view::dispatch::apply_transform(
&mut moved,
&crate::command::EntityTransform::Affine(transform),
);
let (sweep_entity, _) = embedded_sweep_profile(&moved)?;
record.sweep_entity = Some(sweep_entity);
record.sweep_entity_transform = source_frame.to_cols_array();
@@ -307,9 +402,18 @@ fn placed_sweep_record(profile: &EntityType, mut record: SolidHistorySweep) -> O
Some(record)
}
pub fn swept_with_options(profile: &EntityType, path: &EntityType, mode: ExtrudeMode, options: SweepOptions) -> Option<Body> {
pub fn swept_with_options(
profile: &EntityType,
path: &EntityType,
mode: ExtrudeMode,
options: SweepOptions,
) -> Option<Body> {
if spatial_profile(profile).is_some() {
return kernel::acis::rebuild_sweep_with_mode(&spatial_sweep_record(profile, path, options).ok()?, true).ok();
return kernel::acis::rebuild_sweep_with_mode(
&spatial_sweep_record(profile, path, options).ok()?,
true,
)
.ok();
}
let record = sweep_record(profile, path, options)?;
kernel::acis::rebuild_sweep_with_mode(&record, mode == ExtrudeMode::Surface).ok()
@@ -326,7 +430,9 @@ pub fn link_sweep_expressions(
handle: codec::Handle,
expressions: &[Option<String>; 2],
) {
if handle.is_null() || expressions.iter().all(Option::is_none) { return; }
if handle.is_null() || expressions.iter().all(Option::is_none) {
return;
}
let mut values = Vec::new();
for (name, expression) in ["ScaleFactor", "TwistAngle"].iter().zip(expressions) {
if let Some(expression) = expression {
@@ -334,17 +440,325 @@ pub fn link_sweep_expressions(
values.push(codec::xdata::XDataValue::String(expression.clone()));
}
}
crate::scene::view::dispatch::set_entity_xdata(document, handle, SWEEP_EXPRESSION_APP, Some(values));
crate::scene::view::dispatch::set_entity_xdata(
document,
handle,
SWEEP_EXPRESSION_APP,
Some(values),
);
}
/// SURFACEASSOCIATIVITY: whether new surfaces stay associative to the
/// objects they were made from. A profile setting, so one value for every
/// drawing.
static SURFACE_ASSOCIATIVITY: std::sync::atomic::AtomicBool =
std::sync::atomic::AtomicBool::new(true);
pub fn surface_associativity() -> bool {
SURFACE_ASSOCIATIVITY.load(std::sync::atomic::Ordering::Relaxed)
}
pub fn set_surface_associativity(on: bool) {
SURFACE_ASSOCIATIVITY.store(on, std::sync::atomic::Ordering::Relaxed);
}
/// The application name under which an associative swept surface keeps
/// the profile and path it was swept from.
pub const SWEEP_SOURCES_APP: &str = "OCS_SWEEP_SOURCES";
/// Marks a swept surface associative to the profile and path it was swept
/// from, with the options it is swept again with when they change; the
/// drawing's associative network for it is written on save.
pub fn link_sweep_sources(
document: &mut codec::CadDocument,
surface: codec::Handle,
profile: codec::Handle,
path: codec::Handle,
options: &SweepOptions,
) {
use codec::xdata::XDataValue;
if surface.is_null() {
return;
}
let point = |p: glam::DVec3| XDataValue::Point3D(Vector3::new(p.x, p.y, p.z));
// The path keeps the direction it was swept in when it is edited.
let reversed = options
.path_pick
.zip(document.get_entity(path))
.is_some_and(|(pick, path)| reversed_toward_pick(path, pick).is_some());
let mut values = vec![
XDataValue::Handle(profile),
XDataValue::Handle(path),
XDataValue::Integer16(
i16::from(options.align) | (i16::from(options.bank) << 1) | (i16::from(reversed) << 2),
),
];
if let Some(base) = options.base_point {
values.push(XDataValue::String("BASE".to_string()));
values.push(point(base));
}
crate::scene::view::dispatch::set_entity_xdata(
document,
surface,
SWEEP_SOURCES_APP,
Some(values),
);
}
/// The profile and path an associative swept surface was swept from.
pub fn sweep_sources(entity: &EntityType) -> Option<(codec::Handle, codec::Handle)> {
let record = entity
.common()
.extended_data
.records()
.iter()
.find(|record| record.application_name == SWEEP_SOURCES_APP)?;
let mut handles = record.values.iter().filter_map(|value| match value {
codec::xdata::XDataValue::Handle(handle) => Some(*handle),
_ => None,
});
Some((handles.next()?, handles.next()?))
}
/// The surface swept again from its current profile and path, with the
/// options it was made with: (surface record, body).
pub fn resweep_from_sources(
surface: &EntityType,
profile: &EntityType,
path: &EntityType,
) -> Option<(SolidHistorySweep, Body)> {
use codec::xdata::XDataValue;
let record = surface
.common()
.extended_data
.records()
.iter()
.find(|record| record.application_name == SWEEP_SOURCES_APP)?;
let EntityType::Surface(value) = surface else {
return None;
};
let SurfaceData::Swept {
options: stored, ..
} = &value.surface_data
else {
return None;
};
let mut options = SweepOptions {
scale: stored.scale_factor,
twist_angle: stored.twist_angle,
..SweepOptions::default()
};
let mut values = record.values.iter();
while let Some(value) = values.next() {
match value {
XDataValue::Integer16(flags) => {
options.align = flags & 1 != 0;
options.bank = flags & 2 != 0;
// Swept from the end: picked there.
if flags & 4 != 0 {
options.path_pick = path_ends(path).map(|(_, end)| end);
}
}
XDataValue::String(key) if key == "BASE" => {
if let Some(XDataValue::Point3D(p)) = values.next() {
options.base_point = Some(glam::DVec3::new(p.x, p.y, p.z));
}
}
_ => {}
}
}
let record = if spatial_profile(profile).is_some() {
spatial_sweep_record(profile, path, options).ok()?
} else {
sweep_record(profile, path, options)?
};
let body = kernel::acis::rebuild_sweep_with_mode(&record, true).ok()?;
Some((record, body))
}
/// An entity's curve as the reference stores it in an edge action
/// parameter: line segment (23: start, vector to the end), arc (11:
/// centre, normal, reference axis, radius, start and end angle, 0) or a
/// composite of those (47: count, then each part's type and values).
pub fn assoc_edge_curve(
entity: &EntityType,
) -> Option<(
i32,
codec::objects::AssocSubcurveKind,
Vec<codec::objects::AssocCurveValue>,
)> {
use codec::objects::{AssocCurveValue as V, AssocSubcurveKind as K};
use codec::types::Vector3 as P;
let point = |p: glam::DVec3| V::Point(P::new(p.x, p.y, p.z));
let segment = |a: glam::DVec3, b: glam::DVec3| vec![point(a), point(b - a)];
// An arc about `normal` from `start` through `sweep` (signed, about the
// normal) on a circle of `radius`; measured from the reference axis.
let arc = |centre: glam::DVec3,
normal: glam::DVec3,
axis: glam::DVec3,
radius: f64,
start: f64,
end: f64| {
vec![
point(centre),
point(normal),
point(axis),
V::Real(radius),
V::Real(start),
V::Real(end),
V::Real(0.0),
]
};
match entity {
EntityType::Line(line) => {
let (a, b) = (
glam::DVec3::new(line.start.x, line.start.y, line.start.z),
glam::DVec3::new(line.end.x, line.end.y, line.end.z),
);
Some((23, K::LineSegment3d, segment(a, b)))
}
EntityType::Circle(circle) => {
let normal = glam::DVec3::new(circle.normal.x, circle.normal.y, circle.normal.z)
.try_normalize()?;
let axis = arbitrary_x(normal);
Some((
11,
K::Arc,
arc(
glam::DVec3::new(circle.center.x, circle.center.y, circle.center.z),
normal,
axis,
circle.radius,
0.0,
std::f64::consts::TAU,
),
))
}
EntityType::Arc(value) => {
let normal =
glam::DVec3::new(value.normal.x, value.normal.y, value.normal.z).try_normalize()?;
let axis = arbitrary_x(normal);
// The end past the start by the arc's sweep; an equal pair is a
// full turn. (A loop adding TAU never ends on a huge angle.)
let sweep = (value.end_angle - value.start_angle).rem_euclid(std::f64::consts::TAU);
let end = value.start_angle
+ if sweep > 0.0 {
sweep
} else {
std::f64::consts::TAU
};
Some((
11,
K::Arc,
arc(
glam::DVec3::new(value.center.x, value.center.y, value.center.z),
normal,
axis,
value.radius,
value.start_angle,
end,
),
))
}
EntityType::LwPolyline(polyline) => {
let normal = glam::DVec3::new(polyline.normal.x, polyline.normal.y, polyline.normal.z)
.try_normalize()?;
let axis_x = arbitrary_x(normal);
let axis_y = normal.cross(axis_x);
let at = |v: &codec::entities::LwVertex| {
axis_x * v.location.x + axis_y * v.location.y + normal * polyline.elevation
};
let count = polyline.vertices.len();
let spans = if polyline.is_closed {
count
} else {
count.saturating_sub(1)
};
if spans == 0 {
return None;
}
let mut values = vec![V::Int(spans as i32)];
for index in 0..spans {
let (from, to) = (
&polyline.vertices[index],
&polyline.vertices[(index + 1) % count],
);
let (a, b) = (at(from), at(to));
if from.bulge.abs() <= 1e-12 {
values.push(V::Int(23));
values.extend(segment(a, b));
} else {
// The arc in the polyline's plane; a clockwise one turns about
// the opposite normal, from the same reference axis.
let bulge = from.bulge;
let chord = b - a;
let centre = a
+ chord * 0.5
+ normal.cross(chord) * ((1.0 - bulge * bulge) / (4.0 * bulge));
let radius = chord.length() * (1.0 + bulge * bulge) / (4.0 * bulge.abs());
let turn = normal * bulge.signum();
let across = turn.cross(axis_x);
let angle =
|p: glam::DVec3| (p - centre).dot(across).atan2((p - centre).dot(axis_x));
let start = angle(a);
let end = start + 4.0 * bulge.atan().abs();
values.push(V::Int(11));
values.extend(arc(centre, turn, axis_x, radius, start, end));
}
}
Some((47, K::None, values))
}
EntityType::Polyline3D(polyline) => {
let points = polyline
.vertices
.iter()
.map(|v| glam::DVec3::new(v.position.x, v.position.y, v.position.z))
.collect::<Vec<_>>();
let spans = if polyline.is_closed() {
points.len()
} else {
points.len().saturating_sub(1)
};
if spans == 0 {
return None;
}
let mut values = vec![V::Int(spans as i32)];
for index in 0..spans {
values.push(V::Int(23));
values.extend(segment(points[index], points[(index + 1) % points.len()]));
}
Some((47, K::None, values))
}
_ => None,
}
}
/// The arbitrary-axis X direction of an entity normal.
fn arbitrary_x(normal: glam::DVec3) -> glam::DVec3 {
let world = if normal.x.abs() < 1.0 / 64.0 && normal.y.abs() < 1.0 / 64.0 {
glam::DVec3::Y
} else {
glam::DVec3::Z
};
world.cross(normal).normalize()
}
/// The scale and twist expressions a swept surface is linked to.
pub fn sweep_expressions(entity: &EntityType) -> Option<[Option<String>; 2]> {
let record = entity.common().extended_data.records().iter()
let record = entity
.common()
.extended_data
.records()
.iter()
.find(|record| record.application_name == SWEEP_EXPRESSION_APP)?;
let strings = record.values.iter().filter_map(|value| match value {
codec::xdata::XDataValue::String(text) => Some(text.clone()),
_ => None,
}).collect::<Vec<_>>();
let strings = record
.values
.iter()
.filter_map(|value| match value {
codec::xdata::XDataValue::String(text) => Some(text.clone()),
_ => None,
})
.collect::<Vec<_>>();
let mut result = [None, None];
for pair in strings.chunks(2) {
if let [name, expression] = pair {
@@ -360,8 +774,18 @@ pub fn sweep_expressions(entity: &EntityType) -> Option<[Option<String>; 2]> {
/// The sweep record a swept surface was made from.
pub fn surface_sweep_record(entity: &EntityType) -> Option<SolidHistorySweep> {
let EntityType::Surface(surface) = entity else { return None };
let SurfaceData::Swept { sweep_entity, path_entity, options, .. } = &surface.surface_data else { return None };
let EntityType::Surface(surface) = entity else {
return None;
};
let SurfaceData::Swept {
sweep_entity,
path_entity,
options,
..
} = &surface.surface_data
else {
return None;
};
let mut base = SolidHistoryNodeBase::new(1);
base.transform = glam::DMat4::IDENTITY.to_cols_array();
Some(SolidHistorySweep {
@@ -380,7 +804,11 @@ pub fn surface_sweep_record(entity: &EntityType) -> Option<SolidHistorySweep> {
sweep_entity_transform: options.sweep_entity_transform,
path_entity_transform: options.path_entity_transform,
reference_point: options.reference_vector,
flags_294_296: [false, options.sweep_entity_transform_computed, options.path_entity_transform_computed],
flags_294_296: [
false,
options.sweep_entity_transform_computed,
options.path_entity_transform_computed,
],
miter_option: 2,
..SolidHistorySweep::default()
})
+49 -58
View File
@@ -1,8 +1,5 @@
// Exact profile sweeps stored as kernel B-reps and ACIS.
use kernel::brep::{self, Body};
use kernel::geom2d::{Arc, Curve, EllipseArc, Line};
use kernel::space::{PlanarCurve, Plane, Vec3};
use codec::entities::{EmbeddedEntity, LwPolyline, LwVertex, Spline};
use codec::objects::{
SolidHistoryLoft, SolidHistoryNodeBase, SolidHistoryOperation, SolidHistoryRevolve,
@@ -10,11 +7,18 @@ use codec::objects::{
};
use codec::types::{Vector2, Vector3};
use codec::EntityType;
use kernel::brep::{self, Body};
use kernel::geom2d::{Arc, Curve, EllipseArc, Line};
use kernel::space::{PlanarCurve, Plane, Vec3};
use crate::entities::curve::entity_curve;
pub use super::sweep_command_model::{
is_sweep_path, is_sweep_profile, scaled_sweep_path_refusal, spatial_profile, spatial_sweep_record, surface_sweep_record, sweep_expressions, link_sweep_expressions, sweep_record, sweep_refusal_code, sweep_selection_options, swept_surface_entity, swept_with_options,
assoc_edge_curve, is_sweep_path, is_sweep_profile, link_sweep_expressions, link_sweep_sources,
resweep_from_sources, scaled_sweep_path_refusal, set_surface_associativity, spatial_profile,
spatial_sweep_record, surface_associativity, surface_sweep_record, sweep_expressions,
sweep_record, sweep_refusal_code, sweep_selection_options, sweep_sources, swept_surface_entity,
swept_with_options, SWEEP_SOURCES_APP,
};
use crate::entities::curve::entity_curve;
/// A drawn profile, as the kernel wants it: the plane it lies in and the
/// chain of pieces closing a loop in that plane.
@@ -130,9 +134,7 @@ fn planar_polygon_entity(entity: &EntityType) -> Option<PlanarCurve> {
let normal = points[2..]
.iter()
.find_map(|point| first.cross(Vec3::from(*point) - origin).normalize())
.or_else(|| {
(!closed).then(|| first.cross(Vec3::Z).normalize().unwrap_or(Vec3::Y))
})?;
.or_else(|| (!closed).then(|| first.cross(Vec3::Z).normalize().unwrap_or(Vec3::Y)))?;
let plane = Plane::orthonormal(points[0], first.to_array(), normal.to_array())?;
let scale = points
.iter()
@@ -148,10 +150,12 @@ fn planar_polygon_entity(entity: &EntityType) -> Option<PlanarCurve> {
let vertices = points
.iter()
.map(|point| {
plane.project(*point).map(|position| kernel::geom2d::PolylineVertex {
position,
bulge: 0.0,
})
plane
.project(*point)
.map(|position| kernel::geom2d::PolylineVertex {
position,
bulge: 0.0,
})
})
.collect::<Option<Vec<_>>>()?;
Some(PlanarCurve::new(
@@ -295,12 +299,7 @@ pub fn extruded_surface(
return None;
}
let (profile, _) = extrusion_profile_of(entity)?;
brep::extrude_surface_tapered(
profile.plane,
&profile.pieces,
direction,
taper_angle,
)
brep::extrude_surface_tapered(profile.plane, &profile.pieces, direction, taper_angle)
}
pub fn extruded_along_path(
@@ -341,8 +340,8 @@ pub fn extruded_along_path(
let Curve::Line(line) = curve.curve else {
return None;
};
let direction = Vec3::from(curve.plane.point_at(line.end))
- Vec3::from(curve.plane.point_at(line.start));
let direction =
Vec3::from(curve.plane.point_at(line.end)) - Vec3::from(curve.plane.point_at(line.start));
brep::extrude_tapered(
profile.plane,
&profile.pieces,
@@ -365,9 +364,8 @@ pub fn straight_path_direction(path: &EntityType) -> Option<[f64; 3]> {
return None;
};
Some(
(Vec3::from(curve.plane.point_at(line.end))
- Vec3::from(curve.plane.point_at(line.start)))
.to_array(),
(Vec3::from(curve.plane.point_at(line.end)) - Vec3::from(curve.plane.point_at(line.start)))
.to_array(),
)
}
@@ -423,12 +421,7 @@ pub fn revolved_surface(
turn_revolved_body(body, from, axis, start_angle)
}
fn turn_revolved_body(
body: Body,
pivot: [f64; 3],
axis: [f64; 3],
angle: f64,
) -> Option<Body> {
fn turn_revolved_body(body: Body, pivot: [f64; 3], axis: [f64; 3], angle: f64) -> Option<Body> {
if !angle.is_finite() {
return None;
}
@@ -526,10 +519,12 @@ pub fn swept(profile: &EntityType, path: &EntityType) -> Option<Body> {
.tessellate_within(scale * 1e-4)
.windows(2)
.filter(|pair| pair[0] != pair[1])
.map(|pair| Curve::Line(Line {
start: pair[0],
end: pair[1],
}))
.map(|pair| {
Curve::Line(Line {
start: pair[0],
end: pair[1],
})
})
.collect()
}
_ => return None,
@@ -599,24 +594,30 @@ pub fn embedded_path(entity: &EntityType) -> Option<EmbeddedEntity> {
polyline.elevation = value.elevation;
polyline.normal = value.normal;
polyline.is_closed = value.is_closed();
polyline.vertices = value.vertices.iter().map(|vertex| {
let mut converted = LwVertex::new(Vector2::new(vertex.location.x, vertex.location.y));
converted.bulge = vertex.bulge;
converted
}).collect();
polyline.vertices = value
.vertices
.iter()
.map(|vertex| {
let mut converted =
LwVertex::new(Vector2::new(vertex.location.x, vertex.location.y));
converted.bulge = vertex.bulge;
converted
})
.collect();
Some(EmbeddedEntity::LwPolyline(polyline))
}
EntityType::Spline(value) => Some(EmbeddedEntity::Spline(value.clone())),
EntityType::Helix(value) => Some(EmbeddedEntity::Spline(value.spline.clone())),
EntityType::Polyline3D(value) if value.vertices.len() >= 2 => {
let mut points = value.vertices.iter().map(|vertex| vertex.position).collect::<Vec<_>>();
let mut points = value
.vertices
.iter()
.map(|vertex| vertex.position)
.collect::<Vec<_>>();
if value.is_closed() && points.first() != points.last() {
points.push(*points.first()?);
}
let mut spline = Spline::from_control_points(
1,
points,
);
let mut spline = Spline::from_control_points(1, points);
spline.flags.linear = true;
spline.flags.planar = false;
spline.flags.closed = value.is_closed();
@@ -714,11 +715,7 @@ pub fn extrusion_history(
scale_factor: 1.0,
sweep_entity_transform,
path_entity_transform,
reference_point: Vector3::new(
reference_point[0],
reference_point[1],
reference_point[2],
),
reference_point: Vector3::new(reference_point[0], reference_point[1], reference_point[2]),
..SolidHistorySweep::default()
}))
}
@@ -741,11 +738,7 @@ pub fn sweep_history(
scale_factor: 1.0,
sweep_entity_transform,
path_entity_transform: glam::DMat4::IDENTITY.to_cols_array(),
reference_point: Vector3::new(
reference_point[0],
reference_point[1],
reference_point[2],
),
reference_point: Vector3::new(reference_point[0], reference_point[1], reference_point[2]),
..SolidHistorySweep::default()
}))
}
@@ -842,10 +835,7 @@ fn circular_loft(profiles: &[EntityType]) -> Option<Body> {
let tolerance = scale * 1e-9;
let mut heights = Vec::with_capacity(sections.len());
for (centre, radius, normal, _) in &sections {
if !radius.is_finite()
|| *radius <= tolerance
|| normal.dot(direction).abs() < 1.0 - 1e-9
{
if !radius.is_finite() || *radius <= tolerance || normal.dot(direction).abs() < 1.0 - 1e-9 {
return None;
}
let offset = *centre - first.0;
@@ -862,7 +852,8 @@ fn circular_loft(profiles: &[EntityType]) -> Option<Body> {
}
let radial_seed = first.3 - direction * first.3.dot(direction);
let radial = radial_seed.normalize()?;
let profile_plane = Plane::from_axes(first.0.to_array(), radial.to_array(), direction.to_array());
let profile_plane =
Plane::from_axes(first.0.to_array(), radial.to_array(), direction.to_array());
let mut points = Vec::with_capacity(sections.len() + 2);
points.push([0.0, 0.0]);
points.extend(
+224 -42
View File
@@ -5,7 +5,10 @@
use std::collections::HashMap;
use super::model::{arc_cubics, bspline_points, dgn_color_table, paths_bounds, Path, PathBuilder, Segment, Sheet, SubPath, Text, DGN_DEFAULT_COLORS};
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> {
@@ -67,7 +70,9 @@ fn elements(b: &[u8]) -> Vec<&[u8]> {
let mut out = Vec::new();
let mut at = 0usize;
while at + 4 <= b.len() && !(b[at] == 0xff && b[at + 1] == 0xff) {
let Some(words) = uint16(b, at + 2) else { break };
let Some(words) = uint16(b, at + 2) else {
break;
};
let len = 4 + words as usize * 2;
let Some(e) = b.get(at..at + len) else { break };
out.push(e);
@@ -135,21 +140,43 @@ fn read_run(run: &[&[u8]], colors: &[[u8; 3]]) -> Raw {
let mut spline: Option<(usize, usize, [u8; 3], f64)> = None;
let mut knots: Vec<f64> = Vec::new();
// 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;
let push = |raw: &mut Raw, complex: &mut Option<(usize, PathBuilder, Option<([u8; 3], f64)>, Option<[u8; 3]>, bool)>, path: Path| {
let mut complex: Option<(
usize,
PathBuilder,
Option<([u8; 3], f64)>,
Option<[u8; 3]>,
bool,
)> = None;
let push = |raw: &mut Raw,
complex: &mut Option<(
usize,
PathBuilder,
Option<([u8; 3], f64)>,
Option<[u8; 3]>,
bool,
)>,
path: Path| {
if let Some((left, pb, _, _, _)) = complex.as_mut() {
for sp in &path.subpaths {
for seg in &sp.segments {
match seg {
Segment::Line(a, b) => {
if pb.current() != Some(*a) {
if pb.current().is_none() { pb.move_to(*a) } else { pb.line_to(*a) }
if pb.current().is_none() {
pb.move_to(*a)
} else {
pb.line_to(*a)
}
}
pb.line_to(*b);
}
Segment::Cubic(a, c1, c2, b) => {
if pb.current() != Some(*a) {
if pb.current().is_none() { pb.move_to(*a) } else { pb.line_to(*a) }
if pb.current().is_none() {
pb.move_to(*a)
} else {
pb.line_to(*a)
}
}
pb.cubic_to(*c1, *c2, *b);
}
@@ -162,7 +189,11 @@ fn read_run(run: &[&[u8]], colors: &[[u8; 3]]) -> Raw {
if closed {
pb.close();
}
raw.paths.push(Path { subpaths: pb.finish(), stroke, fill });
raw.paths.push(Path {
subpaths: pb.finish(),
stroke,
fill,
});
}
} else {
raw.paths.push(path);
@@ -179,61 +210,126 @@ fn read_run(run: &[&[u8]], colors: &[[u8; 3]]) -> Raw {
// 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]) };
let int_pt =
|i: usize| -> Option<[f64; 2]> { Some([int32(e, i)? as f64, int32(e, i + 4)? as f64]) };
match kind {
12 | 14 => {
let n = uint16(e, 38).unwrap_or(0) as usize;
if n > 0 {
complex = Some((n, PathBuilder::default(), stroke, if kind == 14 { fill } else { None }, kind == 14));
complex = Some((
n,
PathBuilder::default(),
stroke,
if kind == 14 { fill } else { None },
kind == 14,
));
}
}
3 => {
if let (Some(a), Some(b)) = (int_pt(36), int_pt(44)) {
push(&mut raw, &mut complex, Path { subpaths: vec![SubPath { segments: vec![Segment::Line(a, b)], closed: false }], stroke, fill: None });
push(
&mut raw,
&mut complex,
Path {
subpaths: vec![SubPath {
segments: vec![Segment::Line(a, b)],
closed: false,
}],
stroke,
fill: None,
},
);
}
}
4 | 6 | 11 => {
let n = uint16(e, 36).unwrap_or(0) as usize;
let pts: Vec<[f64; 2]> = (0..n).filter_map(|k| int_pt(38 + k * 8)).collect();
if pts.len() >= 2 {
push(&mut raw, &mut complex, polyline(&pts, kind == 6, stroke, if kind == 6 { fill } else { None }));
push(
&mut raw,
&mut complex,
polyline(&pts, kind == 6, stroke, if kind == 6 { fill } else { None }),
);
}
}
15 | 16 => {
let arc = if kind == 15 {
(|| Some((vax_double(e, 36)?, vax_double(e, 44)?, int32(e, 52)?, vax_double(e, 56)?, vax_double(e, 64)?, 0.0, 360.0)))()
(|| {
Some((
vax_double(e, 36)?,
vax_double(e, 44)?,
int32(e, 52)?,
vax_double(e, 56)?,
vax_double(e, 64)?,
0.0,
360.0,
))
})()
} else {
(|| {
let sw = int32(e, 40)? as u32;
let mut sweep = if sw & 0x8000_0000 != 0 { -((sw & 0x7fff_ffff) as f64) } else { sw as f64 } / 360000.0;
let mut sweep = if sw & 0x8000_0000 != 0 {
-((sw & 0x7fff_ffff) as f64)
} else {
sw as f64
} / 360000.0;
if sweep == 0.0 {
sweep = 360.0;
}
Some((vax_double(e, 44)?, vax_double(e, 52)?, int32(e, 60)?, vax_double(e, 64)?, vax_double(e, 72)?, int32(e, 36)? as f64 / 360000.0, sweep))
Some((
vax_double(e, 44)?,
vax_double(e, 52)?,
int32(e, 60)?,
vax_double(e, 64)?,
vax_double(e, 72)?,
int32(e, 36)? as f64 / 360000.0,
sweep,
))
})()
};
let Some((a, b, r, ox, oy, start, sweep)) = arc else { continue };
let pieces = arc_cubics([ox, oy], a, b, (r as f64 / 360000.0).to_radians(), start.to_radians(), sweep.to_radians());
let Some((a, b, r, ox, oy, start, sweep)) = arc else {
continue;
};
let pieces = arc_cubics(
[ox, oy],
a,
b,
(r as f64 / 360000.0).to_radians(),
start.to_radians(),
sweep.to_radians(),
);
let closed = sweep.abs() >= 360.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 }],
subpaths: vec![SubPath {
segments: pieces
.into_iter()
.map(|[a, b, c, d]| Segment::Cubic(a, b, c, d))
.collect(),
closed,
}],
stroke,
fill: if closed { fill } else { None },
},
);
}
17 => {
let (Some(width), Some(height), Some(rotation), Some(origin)) = (int32(e, 38), int32(e, 42), int32(e, 46), int_pt(50)) else {
let (Some(width), Some(height), Some(rotation), Some(origin)) =
(int32(e, 38), int32(e, 42), int32(e, 46), int_pt(50))
else {
continue;
};
let n = e.get(58).copied().unwrap_or(0) as usize;
let Some(chars) = e.get(60..60 + n) else { continue };
let Some(chars) = e.get(60..60 + n) else {
continue;
};
let (height, width) = (height as f64 * 6.0 / 1000.0, width as f64 * 6.0 / 1000.0);
raw.texts.push(Text {
text: String::from_utf8_lossy(chars).trim_end_matches('\0').to_string(),
text: String::from_utf8_lossy(chars)
.trim_end_matches('\0')
.to_string(),
origin,
height,
width_factor: if height > 0.0 { width / height } else { 1.0 },
@@ -252,7 +348,10 @@ fn read_run(run: &[&[u8]], colors: &[[u8; 3]]) -> Raw {
26 => {
// Interior knots as fractions of the parameter range.
let n = spline.map(|s| s.1.saturating_sub(s.0)).unwrap_or(0);
knots = (0..n).filter_map(|k| int32(e, 36 + k * 4)).map(|v| v as u32 as f64 / 2_147_483_648.0).collect();
knots = (0..n)
.filter_map(|k| int32(e, 36 + k * 4))
.map(|v| v as u32 as f64 / 2_147_483_648.0)
.collect();
}
21 => {
let n = uint16(e, 36).unwrap_or(0) as usize;
@@ -262,26 +361,49 @@ fn read_run(run: &[&[u8]], colors: &[[u8; 3]]) -> Raw {
let interior = if knots.len() == count - order {
knots.clone()
} else {
(1..=count - order).map(|k| k as f64 / (count - order + 1) as f64).collect()
(1..=count - order)
.map(|k| k as f64 / (count - order + 1) as f64)
.collect()
};
let mut full = vec![0.0; order];
full.extend(interior);
full.extend(vec![1.0; order]);
let pts = bspline_points(order, &poles, &full, 24 * count);
push(&mut raw, &mut complex, polyline(&pts, false, Some((color, width)), None));
push(
&mut raw,
&mut complex,
polyline(&pts, false, Some((color, width)), None),
);
}
}
}
35 => {
// Shared cell instance: the definition's name, a 2×2 matrix
// and the origin.
let name = e.get(164..180).map(|n| String::from_utf8_lossy(n).trim_end_matches('\0').trim().to_string()).unwrap_or_default();
let (Some(m0), Some(m1), Some(m2), Some(m3), Some(ox), Some(oy)) =
(vax_double(e, 76), vax_double(e, 84), vax_double(e, 92), vax_double(e, 100), le_i32(e, 148), le_i32(e, 152))
else {
let name = e
.get(164..180)
.map(|n| {
String::from_utf8_lossy(n)
.trim_end_matches('\0')
.trim()
.to_string()
})
.unwrap_or_default();
let (Some(m0), Some(m1), Some(m2), Some(m3), Some(ox), Some(oy)) = (
vax_double(e, 76),
vax_double(e, 84),
vax_double(e, 92),
vax_double(e, 100),
le_i32(e, 148),
le_i32(e, 152),
) else {
continue;
};
raw.instances.push(Instance { name, m: [m0, m1, m2, m3], origin: [ox as f64, oy as f64] });
raw.instances.push(Instance {
name,
m: [m0, m1, m2, m3],
origin: [ox as f64, oy as f64],
});
}
_ => {}
}
@@ -289,7 +411,12 @@ fn read_run(run: &[&[u8]], colors: &[[u8; 3]]) -> Raw {
raw
}
fn polyline(pts: &[[f64; 2]], closed: bool, stroke: Option<([u8; 3], f64)>, fill: Option<[u8; 3]>) -> Path {
fn polyline(
pts: &[[f64; 2]],
closed: bool,
stroke: Option<([u8; 3], f64)>,
fill: Option<[u8; 3]>,
) -> Path {
let mut pb = PathBuilder::default();
pb.move_to(pts[0]);
for p in &pts[1..] {
@@ -298,14 +425,21 @@ fn polyline(pts: &[[f64; 2]], closed: bool, stroke: Option<([u8; 3], f64)>, fill
if closed {
pb.close();
}
Path { subpaths: pb.finish(), stroke, fill }
Path {
subpaths: pb.finish(),
stroke,
fill,
}
}
/// x' = a·x + b·y + e, y' = c·x + d·y + f.
type Affine = [f64; 6];
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]]
[
m[0] * p[0] + m[1] * p[1] + m[4],
m[2] * p[0] + m[3] * p[1] + m[5],
]
}
fn compose(outer: &Affine, inner: &Affine) -> Affine {
@@ -333,7 +467,9 @@ fn place(raw: &Raw, m: &Affine, defs: &HashMap<String, Raw>, depth: usize, out:
.iter()
.map(|s| match s {
Segment::Line(a, b) => Segment::Line(map(*a), map(*b)),
Segment::Cubic(a, b, c, d) => Segment::Cubic(map(*a), map(*b), map(*c), map(*d)),
Segment::Cubic(a, b, c, d) => {
Segment::Cubic(map(*a), map(*b), map(*c), map(*d))
}
})
.collect(),
closed: sp.closed,
@@ -346,14 +482,28 @@ fn place(raw: &Raw, m: &Affine, defs: &HashMap<String, Raw>, depth: usize, out:
let scale = (m[0] * m[3] - m[1] * m[2]).abs().sqrt();
let turn = m[2].atan2(m[0]);
for t in &raw.texts {
out.texts.push(Text { origin: map(t.origin), height: t.height * scale, rotation: t.rotation + turn, ..t.clone() });
out.texts.push(Text {
origin: map(t.origin),
height: t.height * scale,
rotation: t.rotation + turn,
..t.clone()
});
}
if depth >= 8 {
return;
}
for inst in &raw.instances {
let Some(def) = defs.get(&inst.name.to_ascii_uppercase()) else { continue };
let local: Affine = [inst.m[0], inst.m[1], inst.m[2], inst.m[3], inst.origin[0], inst.origin[1]];
let Some(def) = defs.get(&inst.name.to_ascii_uppercase()) 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);
}
}
@@ -372,11 +522,15 @@ fn level_names(all: &[&[u8]]) -> HashMap<u8, String> {
for e in all.iter().filter(|e| e[1] & 0x7f == 66 && level(e) == 6) {
for j in 36..e.len().saturating_sub(3) {
let n = u16::from_le_bytes([e[j], e[j + 1]]);
if !(1..=63).contains(&n) || (j > 36 && e[j - 1] != 0) || names.contains_key(&(n as u8)) {
if !(1..=63).contains(&n) || (j > 36 && e[j - 1] != 0) || names.contains_key(&(n as u8))
{
continue;
}
let rest = &e[j + 2..];
let len = rest.iter().position(|c| !(c.is_ascii_alphanumeric() || matches!(c, b'-' | b'_' | b'$'))).unwrap_or(rest.len());
let len = rest
.iter()
.position(|c| !(c.is_ascii_alphanumeric() || matches!(c, b'-' | b'_' | b'$')))
.unwrap_or(rest.len());
if len > 0 && rest.get(len) == Some(&0) {
names.insert(n as u8, String::from_utf8_lossy(&rest[..len]).to_string());
}
@@ -399,7 +553,11 @@ pub fn layer_names(bytes: &[u8]) -> Option<Vec<String>> {
.collect();
used.sort_unstable();
used.dedup();
Some(used.into_iter().map(|l| names.get(&l).cloned().unwrap_or_else(|| l.to_string())).collect())
Some(
used.into_iter()
.map(|l| names.get(&l).cloned().unwrap_or_else(|| l.to_string()))
.collect(),
)
}
/// The model's geometry, in master units, without the `hidden` levels.
@@ -427,7 +585,15 @@ pub fn model(bytes: &[u8], hidden: &[String]) -> Option<Sheet> {
while i < all.len() {
let e = all[i];
if e[1] & 0x7f == 34 && e[0] & 0x80 == 0 {
let name = e.get(164..180).map(|n| String::from_utf8_lossy(n).trim_end_matches('\0').trim().to_ascii_uppercase()).unwrap_or_default();
let name = e
.get(164..180)
.map(|n| {
String::from_utf8_lossy(n)
.trim_end_matches('\0')
.trim()
.to_ascii_uppercase()
})
.unwrap_or_default();
let mut j = i + 1;
while j < all.len() && all[j][0] & 0x80 != 0 {
j += 1;
@@ -441,12 +607,28 @@ 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, table_colors, ..Default::default() };
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,
table_colors,
..Default::default()
};
place(&raw, &m, &defs, 0, &mut sheet);
sheet.rect = paths_bounds(&sheet.paths).or_else(|| {
let t = sheet.texts.first()?;
Some([t.origin[0], t.origin[1], t.origin[0] + t.height, t.origin[1] + t.height])
Some([
t.origin[0],
t.origin[1],
t.origin[0] + t.height,
t.origin[1] + t.height,
])
})?;
Some(sheet)
}
+408 -81
View File
@@ -7,7 +7,10 @@
use std::collections::HashMap;
use std::io::Read;
use super::model::{arc_cubics, dgn_color_table, paths_bounds, Path, PathBuilder, Segment, Sheet, SubPath, Text, DGN_DEFAULT_COLORS};
use super::model::{
arc_cubics, dgn_color_table, paths_bounds, Path, PathBuilder, Segment, Sheet, SubPath, Text,
DGN_DEFAULT_COLORS,
};
// ── Compound file ───────────────────────────────────────────────────────────
@@ -53,10 +56,16 @@ impl<'a> Cfb<'a> {
if mini_sector_shift != 6 {
return None;
}
let mut difat: Vec<u32> = (0..109).filter_map(|i| u32_at(b, 76 + i * 4)).filter(|&v| v < END).collect();
let mut difat: Vec<u32> = (0..109)
.filter_map(|i| u32_at(b, 76 + i * 4))
.filter(|&v| v < END)
.collect();
// The DIFAT chain cannot hold more sectors than the file does, so a
// looping chain stops there.
let (mut next, mut count) = (u32_at(b, 68)?, u32_at(b, 72)?.min((b.len() / sector) as u32));
let (mut next, mut count) = (
u32_at(b, 68)?,
u32_at(b, 72)?.min((b.len() / sector) as u32),
);
while count > 0 && next < END {
let at = (next as usize + 1) * sector;
for i in 0..sector / 4 - 1 {
@@ -74,11 +83,22 @@ impl<'a> Cfb<'a> {
fat.push(u32_at(b, at + i * 4).unwrap_or(END));
}
}
let mut cfb = Cfb { b, sector, fat, mini_fat: Vec::new(), mini: Vec::new(), cutoff: u32_at(b, 56)?, entries: Vec::new() };
let mut cfb = Cfb {
b,
sector,
fat,
mini_fat: Vec::new(),
mini: Vec::new(),
cutoff: u32_at(b, 56)?,
entries: Vec::new(),
};
let dir = cfb.chain(u32_at(b, 48)?, None);
for e in dir.chunks_exact(128) {
let len = u16::from_le_bytes([e[64], e[65]]) as usize;
let name16: Vec<u16> = e[..len.saturating_sub(2).min(64)].chunks_exact(2).map(|c| u16::from_le_bytes([c[0], c[1]])).collect();
let name16: Vec<u16> = e[..len.saturating_sub(2).min(64)]
.chunks_exact(2)
.map(|c| u16::from_le_bytes([c[0], c[1]]))
.collect();
cfb.entries.push(Entry {
name: String::from_utf16_lossy(&name16),
kind: e[66],
@@ -93,7 +113,10 @@ impl<'a> Cfb<'a> {
let (root_start, root_size) = (root.start, root.size);
cfb.mini = cfb.chain(root_start, Some(root_size as usize));
let mini_fat = cfb.chain(u32_at(b, 60)?, None);
cfb.mini_fat = mini_fat.chunks_exact(4).map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]])).collect();
cfb.mini_fat = mini_fat
.chunks_exact(4)
.map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]))
.collect();
Some(cfb)
}
@@ -112,7 +135,9 @@ impl<'a> Cfb<'a> {
let mut guard = 0;
while s < END && guard < max_sectors {
let at = (s as usize + 1) * self.sector;
let Some(block) = self.b.get(at..at + self.sector) else { break };
let Some(block) = self.b.get(at..at + self.sector) else {
break;
};
out.extend_from_slice(block);
s = self.fat.get(s as usize).copied().unwrap_or(END);
guard += 1;
@@ -136,7 +161,9 @@ impl<'a> Cfb<'a> {
let max_sectors = self.mini.len() / 64;
while s < END && guard < max_sectors {
let at = s as usize * 64;
let Some(block) = self.mini.get(at..at + 64) else { break };
let Some(block) = self.mini.get(at..at + 64) else {
break;
};
out.extend_from_slice(block);
s = self.mini_fat.get(s as usize).copied().unwrap_or(END);
guard += 1;
@@ -148,13 +175,25 @@ impl<'a> Cfb<'a> {
/// Every stream with its full path ("Dgn-Md/#000000/Dgn^G/$1").
fn streams(&self) -> Vec<(String, &Entry)> {
let mut out = Vec::new();
fn walk<'b>(c: &'b Cfb, idx: u32, path: &str, out: &mut Vec<(String, &'b Entry)>, depth: usize) {
let Some(e) = c.entries.get(idx as usize) else { return };
fn walk<'b>(
c: &'b Cfb,
idx: u32,
path: &str,
out: &mut Vec<(String, &'b Entry)>,
depth: usize,
) {
let Some(e) = c.entries.get(idx as usize) else {
return;
};
if depth > 64 {
return;
}
walk(c, e.left, path, out, depth + 1);
let p = if path.is_empty() { e.name.clone() } else { format!("{path}/{}", e.name) };
let p = if path.is_empty() {
e.name.clone()
} else {
format!("{path}/{}", e.name)
};
if e.kind == 2 {
out.push((p.clone(), e));
} else if e.kind == 1 {
@@ -177,7 +216,10 @@ fn inflate(data: &[u8]) -> Vec<u8> {
for skip in [16usize, 0] {
if let Some(z) = data.get(skip..) {
let mut out = Vec::new();
if flate2::read::ZlibDecoder::new(z).take(STREAM_LIMIT).read_to_end(&mut out).is_ok()
if flate2::read::ZlibDecoder::new(z)
.take(STREAM_LIMIT)
.read_to_end(&mut out)
.is_ok()
&& !out.is_empty()
{
return out;
@@ -194,7 +236,9 @@ fn elements(d: &[u8]) -> Vec<&[u8]> {
let mut out = Vec::new();
let mut at = 4;
while at + 8 <= d.len() {
let Some(words) = u32_at(d, at + 4) else { break };
let Some(words) = u32_at(d, at + 4) else {
break;
};
let len = 4 + words as usize * 2;
if len < 8 {
break;
@@ -233,11 +277,19 @@ fn f64_at(e: &[u8], at: usize) -> Option<f64> {
Some(f64::from_le_bytes(e.get(at..at + 8)?.try_into().ok()?))
}
/// A string stored after the "ff fe 01 00" marker, when the element has one.
fn marked_string(e: &[u8], from: usize) -> Option<String> {
let i = e.get(from..)?.windows(4).position(|w| w == [0xff, 0xfe, 0x01, 0x00])? + from + 4;
let end = e[i..].iter().position(|&c| c == 0 || c < 0x20).map(|n| i + n).unwrap_or(e.len());
let i = e
.get(from..)?
.windows(4)
.position(|w| w == [0xff, 0xfe, 0x01, 0x00])?
+ from
+ 4;
let end = e[i..]
.iter()
.position(|&c| c == 0 || c < 0x20)
.map(|n| i + n)
.unwrap_or(e.len());
Some(String::from_utf8_lossy(&e[i..end]).to_string())
}
@@ -255,7 +307,12 @@ struct Styles {
/// non-model attributes stream reading
/// `<ExtendedColors><Entry Color="(r,g,b)"/>…</ExtendedColors>`.
fn extended_colors(cfb: &Cfb) -> Vec<[u8; 3]> {
let Some(attrs) = cfb.streams().iter().find(|(p, _)| p == "Dgn^NmA/$1").map(|(_, e)| inflate(&cfb.stream(e))) else {
let Some(attrs) = cfb
.streams()
.iter()
.find(|(p, _)| p == "Dgn^NmA/$1")
.map(|(_, e)| inflate(&cfb.stream(e)))
else {
return Vec::new();
};
for at in 0..attrs.len().saturating_sub(2) {
@@ -276,7 +333,10 @@ fn extended_colors(cfb: &Cfb) -> Vec<[u8; 3]> {
{
continue;
}
let units: Vec<u16> = out.chunks_exact(2).map(|c| u16::from_le_bytes([c[0], c[1]])).collect();
let units: Vec<u16> = out
.chunks_exact(2)
.map(|c| u16::from_le_bytes([c[0], c[1]]))
.collect();
let text = String::from_utf16_lossy(&units);
if !text.contains("<ExtendedColors") {
continue;
@@ -285,7 +345,12 @@ fn extended_colors(cfb: &Cfb) -> Vec<[u8; 3]> {
.split("Color=\"(")
.skip(1)
.filter_map(|rest| {
let v: Vec<u8> = rest.split(')').next()?.split(',').filter_map(|n| n.trim().parse().ok()).collect();
let v: Vec<u8> = rest
.split(')')
.next()?
.split(',')
.filter_map(|n| n.trim().parse().ok())
.collect();
(v.len() == 3).then(|| [v[0], v[1], v[2]])
})
.collect();
@@ -301,14 +366,23 @@ impl Styles {
// lists it.
let explicit = u32_at(e, 52).unwrap_or(u32::MAX);
let index = if explicit == u32::MAX {
self.levels.get(&u32_at(e, 12).unwrap_or(0)).copied().unwrap_or(0)
self.levels
.get(&u32_at(e, 12).unwrap_or(0))
.copied()
.unwrap_or(0)
} else {
if let Some(rgb) = (explicit >> 8).checked_sub(1).and_then(|n| self.extended.get(n as usize)) {
if let Some(rgb) = (explicit >> 8)
.checked_sub(1)
.and_then(|n| self.extended.get(n as usize))
{
return *rgb;
}
explicit & 0xff
};
self.colors.get(index as usize).copied().unwrap_or([255, 255, 255])
self.colors
.get(index as usize)
.copied()
.unwrap_or([255, 255, 255])
}
}
@@ -330,7 +404,11 @@ struct Instance {
/// 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]];
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])
@@ -344,7 +422,8 @@ 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 };
*v = (0..3).map(|k| outer[i][k] * inner[k][j]).sum::<f64>()
+ if j == 3 { outer[i][3] } else { 0.0 };
}
}
out
@@ -359,10 +438,23 @@ fn is_3d(e: &[u8]) -> bool {
/// 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)?);
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 * z + w * y)],
[2.0 * (x * y + w * z), 1.0 - 2.0 * (x * x + z * z), 2.0 * (y * z - w * x)],
[
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),
],
))
}
@@ -373,8 +465,21 @@ fn turned_axes(rotation: f64) -> ([f64; 3], [f64; 3]) {
}
/// 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]];
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],
]
};
arc_cubics([0.0, 0.0], 1.0, 1.0, 0.0, start, sweep)
.into_iter()
.map(|[p0, p1, p2, p3]| Segment::Cubic(map(p0), map(p1), map(p2), map(p3)))
@@ -385,8 +490,24 @@ fn projected_arc(c: [f64; 2], a: f64, b: f64, u: [f64; 2], v: [f64; 2], start: f
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;
fn push(raw: &mut Raw, complex: &mut Option<(usize, PathBuilder, Option<([u8; 3], f64)>, Option<[u8; 3]>, bool)>, path: Path) {
let mut complex: Option<(
usize,
PathBuilder,
Option<([u8; 3], f64)>,
Option<[u8; 3]>,
bool,
)> = None;
fn push(
raw: &mut Raw,
complex: &mut Option<(
usize,
PathBuilder,
Option<([u8; 3], f64)>,
Option<[u8; 3]>,
bool,
)>,
path: Path,
) {
if let Some((left, pb, _, _, _)) = complex.as_mut() {
for sp in &path.subpaths {
for seg in &sp.segments {
@@ -394,7 +515,11 @@ fn read_run(run: &[&[u8]], styles: &Styles, xf: &Xf) -> Raw {
Segment::Line(a, b) | Segment::Cubic(a, _, _, b) => (*a, *b),
};
if pb.current() != Some(a) {
if pb.current().is_none() { pb.move_to(a) } else { pb.line_to(a) }
if pb.current().is_none() {
pb.move_to(a)
} else {
pb.line_to(a)
}
}
match seg {
Segment::Line(..) => pb.line_to(b),
@@ -408,7 +533,11 @@ fn read_run(run: &[&[u8]], styles: &Styles, xf: &Xf) -> Raw {
if closed {
pb.close();
}
raw.paths.push(Path { subpaths: pb.finish(), stroke, fill });
raw.paths.push(Path {
subpaths: pb.finish(),
stroke,
fill,
});
}
} else {
raw.paths.push(path);
@@ -431,25 +560,45 @@ fn read_run(run: &[&[u8]], styles: &Styles, xf: &Xf) -> Raw {
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 }])
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;
if n > 0 {
let fill = (kind(e) == 14 && e.windows(4).any(|w| w == [0x07, 0x10, 0x41, 0x00])).then_some(color);
let fill = (kind(e) == 14
&& e.windows(4).any(|w| w == [0x07, 0x10, 0x41, 0x00]))
.then_some(color);
complex = Some((n, PathBuilder::default(), stroke, fill, kind(e) == 14));
}
}
3 => {
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 });
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| p3(112 + k * stride)).map(plan).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]);
@@ -459,7 +608,15 @@ fn read_run(run: &[&[u8]], styles: &Styles, xf: &Xf) -> Raw {
if kind(e) == 6 {
pb.close();
}
push(&mut raw, &mut complex, Path { subpaths: pb.finish(), stroke, fill: None });
push(
&mut raw,
&mut complex,
Path {
subpaths: pb.finish(),
stroke,
fill: None,
},
);
}
}
15 | 16 => {
@@ -468,18 +625,67 @@ fn read_run(run: &[&[u8]], styles: &Styles, xf: &Xf) -> Raw {
// 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)?)))(),
(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
};
let Some((a, b, (u, v), c, start, sweep)) = arc else { continue };
let sweep = if sweep == 0.0 { std::f64::consts::TAU } else { sweep };
let closed = sweep.abs() >= std::f64::consts::TAU - 1e-9;
push(
&mut raw,
&mut complex,
Path { subpaths: vec![SubPath { segments: projected_arc(plan(c), a, b, axis(u), axis(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,
},
);
}
17 => {
@@ -488,16 +694,34 @@ fn read_run(run: &[&[u8]], styles: &Styles, xf: &Xf) -> Raw {
// 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)?)))()
(|| {
Some((
f64_at(e, 112)?,
f64_at(e, 120)?,
quat_axes(e, 144)?,
p3(176)?,
marked_string(e, 200)?,
))
})()
} else {
(|| {
let count = u16::from_le_bytes(e.get(110..112)?.try_into().ok()?) 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 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((width, height, (u, v), origin, text)) = placed else { continue };
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));
@@ -521,10 +745,21 @@ fn read_run(run: &[&[u8]], styles: &Styles, xf: &Xf) -> Raw {
// 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 };
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) });
let (Some(m), Some(o)) = (m, p3(232)) else {
continue;
};
let Some(name) = marked_string(e, 248) else {
continue;
};
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),
});
}
_ => {}
}
@@ -534,7 +769,11 @@ fn read_run(run: &[&[u8]], styles: &Styles, xf: &Xf) -> Raw {
if closed {
pb.close();
}
raw.paths.push(Path { subpaths: pb.finish(), stroke, fill });
raw.paths.push(Path {
subpaths: pb.finish(),
stroke,
fill,
});
}
raw
}
@@ -542,12 +781,22 @@ fn read_run(run: &[&[u8]], styles: &Styles, xf: &Xf) -> Raw {
type Affine = [f64; 6];
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]]
[
m[0] * p[0] + m[1] * p[1] + m[4],
m[2] * p[0] + m[3] * p[1] + m[5],
]
}
/// 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) {
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 {
@@ -560,7 +809,9 @@ fn place(raw: &Raw, m: &Affine, defs: &HashMap<String, Vec<&[u8]>>, styles: &Sty
.iter()
.map(|s| match s {
Segment::Line(a, b) => Segment::Line(map(*a), map(*b)),
Segment::Cubic(a, b, c, d) => Segment::Cubic(map(*a), map(*b), map(*c), map(*d)),
Segment::Cubic(a, b, c, d) => {
Segment::Cubic(map(*a), map(*b), map(*c), map(*d))
}
})
.collect(),
closed: sp.closed,
@@ -573,14 +824,28 @@ fn place(raw: &Raw, m: &Affine, defs: &HashMap<String, Vec<&[u8]>>, styles: &Sty
let scale = (m[0] * m[3] - m[1] * m[2]).abs().sqrt();
let turn = m[2].atan2(m[0]);
for t in &raw.texts {
out.texts.push(Text { origin: map(t.origin), height: t.height * scale, rotation: t.rotation + turn, ..t.clone() });
out.texts.push(Text {
origin: map(t.origin),
height: t.height * scale,
rotation: t.rotation + turn,
..t.clone()
});
}
if depth >= 8 {
return;
}
for inst in &raw.instances {
let Some(def) = defs.get(&inst.name) else { continue };
place(&read_run(def, styles, &inst.xf), m, defs, styles, depth + 1, out);
let Some(def) = defs.get(&inst.name) else {
continue;
};
place(
&read_run(def, styles, &inst.xf),
m,
defs,
styles,
depth + 1,
out,
);
}
}
@@ -607,7 +872,6 @@ fn units(header: &[u8]) -> (f64, f64) {
(master, sub)
}
/// Models of the file: (name, graphic stream, header stream).
fn models(cfb: &Cfb) -> Vec<(String, Vec<u8>, Vec<u8>)> {
let streams = cfb.streams();
@@ -629,7 +893,13 @@ fn models(cfb: &Cfb) -> Vec<(String, Vec<u8>, Vec<u8>)> {
if total >= total_limit {
break;
}
let get = |suffix: &str| streams.iter().find(|(p, _)| *p == format!("{dir}/{suffix}")).map(|(_, e)| inflate(&cfb.stream(e))).unwrap_or_default();
let get = |suffix: &str| {
streams
.iter()
.find(|(p, _)| *p == format!("{dir}/{suffix}"))
.map(|(_, e)| inflate(&cfb.stream(e)))
.unwrap_or_default()
};
let graphics = get("Dgn^G/$1");
let header = get("Dgn~Mh");
total = total
@@ -657,7 +927,9 @@ fn level_table(nm: &[&[u8]]) -> Vec<(u32, String, u32)> {
match kind(e) {
96 => in_levels = u32_at(e, 12) == Some(1),
95 if in_levels && e.len() >= 80 => {
let (Some(id), Some(color)) = (u32_at(e, 32), u32_at(e, 72)) else { continue };
let (Some(id), Some(color)) = (u32_at(e, 32), u32_at(e, 72)) else {
continue;
};
let mut name = marked_string(e, 32).unwrap_or_default();
if id == 64 && name.eq_ignore_ascii_case("Default") {
name = "0".to_string();
@@ -671,24 +943,40 @@ fn level_table(nm: &[&[u8]]) -> Vec<(u32, String, u32)> {
}
fn non_model(cfb: &Cfb) -> Vec<u8> {
cfb.streams().iter().find(|(p, _)| p == "Dgn^Nm/$1").map(|(_, e)| inflate(&cfb.stream(e))).unwrap_or_default()
cfb.streams()
.iter()
.find(|(p, _)| p == "Dgn^Nm/$1")
.map(|(_, e)| inflate(&cfb.stream(e)))
.unwrap_or_default()
}
/// Level names of the file, for the underlay layers list.
pub fn layer_names(bytes: &[u8]) -> Option<Vec<String>> {
let cfb = Cfb::open(bytes)?;
let nm = non_model(&cfb);
Some(level_table(&elements(&nm)).into_iter().map(|l| l.1).filter(|n| !n.is_empty()).collect())
Some(
level_table(&elements(&nm))
.into_iter()
.map(|l| l.1)
.filter(|n| !n.is_empty())
.collect(),
)
}
pub fn model(bytes: &[u8], name: &str, hidden: &[String]) -> Option<Sheet> {
let cfb = Cfb::open(bytes)?;
let all = models(&cfb);
let (_, graphics, header) = all.iter().find(|m| m.0.eq_ignore_ascii_case(name)).or_else(|| all.first())?;
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);
// 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 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);
@@ -698,13 +986,20 @@ pub fn model(bytes: &[u8], name: &str, hidden: &[String]) -> Option<Sheet> {
.filter(|l| hidden.iter().any(|h| h.eq_ignore_ascii_case(&l.1)))
.map(|l| l.0)
.collect();
let styles = Styles { colors, levels, hidden, extended: extended_colors(&cfb) };
let styles = Styles {
colors,
levels,
hidden,
extended: extended_colors(&cfb),
};
// Shared cell definitions: a type-34 element and its components.
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]) {
let name = marked_string(nm[i], 248).unwrap_or_default().to_ascii_uppercase();
let name = marked_string(nm[i], 248)
.unwrap_or_default()
.to_ascii_uppercase();
let mut j = i + 1;
while j < nm.len() && is_component(nm[j]) {
j += 1;
@@ -719,8 +1014,19 @@ pub fn model(bytes: &[u8], name: &str, hidden: &[String]) -> Option<Sheet> {
let raw = read_run(&graphics, &styles, &IDENTITY);
let (per_unit, sub_uor) = units(header);
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() };
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.
@@ -744,10 +1050,23 @@ fn global_origin(header: &[u8]) -> [f64; 2] {
/// 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 };
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)
@@ -826,8 +1145,16 @@ mod cfb_memory_tests {
return NONE;
}
let mid = (lo + hi) / 2;
let left = if mid > lo { build(b, base, lo, mid - 1) } else { NONE };
let right = if mid < hi { build(b, base, mid + 1, hi) } else { NONE };
let left = if mid > lo {
build(b, base, lo, mid - 1)
} else {
NONE
};
let right = if mid < hi {
build(b, base, mid + 1, hi)
} else {
NONE
};
let e = base + (2 + mid * 2) * 128;
b[e + 68..e + 72].copy_from_slice(&left.to_le_bytes());
b[e + 72..e + 76].copy_from_slice(&right.to_le_bytes());
@@ -852,10 +1179,10 @@ mod cfb_memory_tests {
let mut b = vec![0u8; (payload_start + payload_sectors + 1) * sector];
header(&mut b, 12, 0, ENDOFCHAIN, fat_start as u32);
b[80..84].copy_from_slice(&((fat_start + 1) as u32).to_le_bytes()); // DIFAT slot 1
// Directory: root → storage → model storages, sibling links as a
// balanced tree (real CFB directories are red-black; a linear chain
// would hit streams()' existing depth-64 guard). Each storage has
// one "Dgn^G/$1" stream pointing at the shared payload.
// Directory: root → storage → model storages, sibling links as a
// balanced tree (real CFB directories are red-black; a linear chain
// would hit streams()' existing depth-64 guard). Each storage has
// one "Dgn^G/$1" stream pointing at the shared payload.
let root_model = (model_count - 1) / 2;
put_entry(&mut b, sector, 0, "Root", 1, NONE, NONE, 1, ENDOFCHAIN, 0);
put_entry(
+345 -117
View File
@@ -6,7 +6,9 @@
use std::collections::HashMap;
use std::io::{Cursor, Read};
use super::model::{arc_cubics, paths_bounds, Path, PathBuilder, Segment, Sheet, SubPath, Text, CAP_PER_EM};
use super::model::{
arc_cubics, paths_bounds, Path, PathBuilder, Segment, Sheet, SubPath, Text, CAP_PER_EM,
};
/// The package's files, with `\` separators turned into `/`.
fn unzip(bytes: &[u8]) -> Option<HashMap<String, Vec<u8>>> {
@@ -19,7 +21,11 @@ fn unzip(bytes: &[u8]) -> Option<HashMap<String, Vec<u8>>> {
let mut budget: u64 = 1 << 30;
for i in 0..archive.len() {
let file = archive.by_index(i).ok()?;
let name = file.name().replace('\\', "/").trim_start_matches('/').to_string();
let name = file
.name()
.replace('\\', "/")
.trim_start_matches('/')
.to_string();
let mut data = Vec::new();
file.take(budget + 1).read_to_end(&mut data).ok()?;
budget = budget.checked_sub(data.len() as u64)?;
@@ -42,7 +48,9 @@ struct SheetEntry {
}
fn attr<'a>(node: roxmltree::Node<'a, '_>, name: &str) -> Option<&'a str> {
node.attributes().find(|a| a.name() == name).map(|a| a.value())
node.attributes()
.find(|a| a.name() == name)
.map(|a| a.value())
}
fn numbers(text: &str) -> Vec<f64> {
@@ -197,8 +205,12 @@ fn sheets(files: &HashMap<String, Vec<u8>>) -> Vec<SheetEntry> {
if !name.ends_with("descriptor.xml") {
continue;
}
let Ok(text) = std::str::from_utf8(data) else { continue };
let Some(doc) = parse_xml(text.trim_start_matches('\u{feff}')) else { continue };
let Ok(text) = std::str::from_utf8(data) else {
continue;
};
let Some(doc) = parse_xml(text.trim_start_matches('\u{feff}')) else {
continue;
};
let page = doc.root_element();
if page.tag_name().name() != "Page" {
continue;
@@ -218,7 +230,9 @@ fn sheets(files: &HashMap<String, Vec<u8>>) -> Vec<SheetEntry> {
joined
} else {
let parent = dir.rsplit_once('/').map(|(d, _)| d).unwrap_or("");
format!("{parent}/{href}").trim_start_matches('/').to_string()
format!("{parent}/{href}")
.trim_start_matches('/')
.to_string()
}
}
};
@@ -233,7 +247,10 @@ fn sheets(files: &HashMap<String, Vec<u8>>) -> Vec<SheetEntry> {
Some("in") => 25.4,
_ => 1.0,
};
paper_height_mm = attr(node, "height").and_then(|v| v.parse::<f64>().ok()).unwrap_or(0.0) * scale;
paper_height_mm = attr(node, "height")
.and_then(|v| v.parse::<f64>().ok())
.unwrap_or(0.0)
* scale;
}
"GraphicResource" if attr(node, "role") == Some("2d streaming graphics") => {
graphics = attr(node, "href").map(resolve);
@@ -251,7 +268,9 @@ fn sheets(files: &HashMap<String, Vec<u8>>) -> Vec<SheetEntry> {
let Some(graphics) = graphics else { continue };
out.push(SheetEntry {
name: attr(page, "name").unwrap_or("").to_string(),
order: attr(page, "plotOrder").and_then(|v| v.parse().ok()).unwrap_or(0),
order: attr(page, "plotOrder")
.and_then(|v| v.parse().ok())
.unwrap_or(0),
graphics,
paper,
paper_height_mm,
@@ -278,18 +297,26 @@ struct Units {
impl Units {
fn identity() -> Self {
Self { scale: [1.0, 1.0], offset: [0.0, 0.0] }
Self {
scale: [1.0, 1.0],
offset: [0.0, 0.0],
}
}
fn model(&self, p: [f64; 2]) -> [f64; 2] {
[(p[0] - self.offset[0]) / self.scale[0], (p[1] - self.offset[1]) / self.scale[1]]
[
(p[0] - self.offset[0]) / self.scale[0],
(p[1] - self.offset[1]) / self.scale[1],
]
}
fn length(&self, v: f64) -> f64 {
v / self.scale[0].abs().max(1e-12)
}
/// A 4×4 matrix written row by row, translation in the last row.
fn from_matrix(m: &[f64]) -> Option<Self> {
(m.len() == 16 && m[0] != 0.0 && m[5] != 0.0)
.then(|| Self { scale: [m[0], m[5]], offset: [m[12], m[13]] })
(m.len() == 16 && m[0] != 0.0 && m[5] != 0.0).then(|| Self {
scale: [m[0], m[5]],
offset: [m[12], m[13]],
})
}
}
@@ -307,10 +334,18 @@ pub fn sheet(bytes: &[u8], name: &str) -> Option<Sheet> {
} else {
let w2x = entry.w2x.as_ref().and_then(|p| files.get(p));
// An embedded font (FontUri, relative to the page) names its family.
let page_dir = entry.graphics.rsplit_once('/').map(|(d, _)| d.to_string()).unwrap_or_default();
let page_dir = entry
.graphics
.rsplit_once('/')
.map(|(d, _)| d.to_string())
.unwrap_or_default();
let family = |uri: &str| -> Option<String> {
let uri = uri.trim_start_matches("./");
let key = if let Some(abs) = uri.strip_prefix('/') { abs.to_string() } else { format!("{page_dir}/{uri}") };
let key = if let Some(abs) = uri.strip_prefix('/') {
abs.to_string()
} else {
format!("{page_dir}/{uri}")
};
let data = files.get(&key)?;
let name = key.rsplit('/').next()?;
font_family(&deobfuscate(data, name)?)
@@ -318,17 +353,31 @@ 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, table_colors: false, element_colors: Vec::new() })
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
/// GUID of its file name, byte order reversed.
fn deobfuscate(data: &[u8], file_name: &str) -> Option<Vec<u8>> {
let hex: String = file_name.split('.').next()?.chars().filter(|c| c.is_ascii_hexdigit()).collect();
let hex: String = file_name
.split('.')
.next()?
.chars()
.filter(|c| c.is_ascii_hexdigit())
.collect();
if hex.len() != 32 || data.len() < 32 {
return None;
}
let bytes: Vec<u8> = (0..16).filter_map(|i| u8::from_str_radix(&hex[i * 2..i * 2 + 2], 16).ok()).collect();
let bytes: Vec<u8> = (0..16)
.filter_map(|i| u8::from_str_radix(&hex[i * 2..i * 2 + 2], 16).ok())
.collect();
let key: Vec<u8> = bytes.into_iter().rev().collect();
let mut out = data.to_vec();
for i in 0..32 {
@@ -344,7 +393,9 @@ fn font_family(font: &[u8]) -> Option<String> {
let tables = u16be(4)? as usize;
let name = (0..tables).find_map(|t| {
let at = 12 + t * 16;
(font.get(at..at + 4)? == b"name").then(|| u32be(at + 8)).flatten()
(font.get(at..at + 4)? == b"name")
.then(|| u32be(at + 8))
.flatten()
})? as usize;
let count = u16be(name + 2)? as usize;
let strings = name + u16be(name + 4)? as usize;
@@ -360,7 +411,10 @@ fn font_family(font: &[u8]) -> Option<String> {
match platform {
// Windows: UTF-16 big endian.
3 | 0 => {
let units: Vec<u16> = raw.chunks_exact(2).map(|c| u16::from_be_bytes([c[0], c[1]])).collect();
let units: Vec<u16> = raw
.chunks_exact(2)
.map(|c| u16::from_be_bytes([c[0], c[1]]))
.collect();
return Some(String::from_utf16_lossy(&units));
}
_ => fallback = Some(String::from_utf8_lossy(raw).to_string()),
@@ -472,8 +526,14 @@ mod w2d {
// A stream cut off right after its '(' has no body at all.
let end = self.i.saturating_sub(1).max(start + 1);
let body = self.b.get(start + 1..end)?;
let name_len = body.iter().position(|c| c.is_ascii_whitespace() || *c == b'(').unwrap_or(body.len());
Some((String::from_utf8_lossy(&body[..name_len]).to_string(), &body[name_len..]))
let name_len = body
.iter()
.position(|c| c.is_ascii_whitespace() || *c == b'(')
.unwrap_or(body.len());
Some((
String::from_utf8_lossy(&body[..name_len]).to_string(),
&body[name_len..],
))
}
}
@@ -495,15 +555,28 @@ mod w2d {
text.split_whitespace()
.filter_map(|tok| {
let (x, y) = tok.split_once(',')?;
Some([x.trim_matches(')').parse().ok()?, y.trim_matches(')').parse().ok()?])
Some([
x.trim_matches(')').parse().ok()?,
y.trim_matches(')').parse().ok()?,
])
})
.collect()
}
fn rect_of(points: &[[f64; 2]]) -> Option<[f64; 4]> {
let mut r = [f64::INFINITY, f64::INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY];
let mut r = [
f64::INFINITY,
f64::INFINITY,
f64::NEG_INFINITY,
f64::NEG_INFINITY,
];
for p in points {
r = [r[0].min(p[0]), r[1].min(p[1]), r[2].max(p[0]), r[3].max(p[1])];
r = [
r[0].min(p[0]),
r[1].min(p[1]),
r[2].max(p[0]),
r[3].max(p[1]),
];
}
(r[0] < r[2] && r[1] < r[3]).then_some(r)
}
@@ -589,9 +662,17 @@ mod w2d {
let wide = op == b'l';
let mut pb = PathBuilder::default();
for k in 0..2 {
let (dx, dy) = if wide { (r.i32()? as f64, r.i32()? as f64) } else { (r.i16()? as f64, r.i16()? as f64) };
let (dx, dy) = if wide {
(r.i32()? as f64, r.i32()? as f64)
} else {
(r.i16()? as f64, r.i16()? as f64)
};
let p = rel(&mut at, dx, dy);
if k == 0 { pb.move_to(p) } else { pb.line_to(p) }
if k == 0 {
pb.move_to(p)
} else {
pb.line_to(p)
}
}
raw.push((pb.finish(), color, weight, visible));
}
@@ -601,9 +682,17 @@ mod w2d {
let n = r.count()?;
let mut pb = PathBuilder::default();
for k in 0..n {
let (dx, dy) = if wide { (r.i32()? as f64, r.i32()? as f64) } else { (r.i16()? as f64, r.i16()? as f64) };
let (dx, dy) = if wide {
(r.i32()? as f64, r.i32()? as f64)
} else {
(r.i16()? as f64, r.i16()? as f64)
};
let p = rel(&mut at, dx, dy);
if k == 0 { pb.move_to(p) } else { pb.line_to(p) }
if k == 0 {
pb.move_to(p)
} else {
pb.line_to(p)
}
}
raw.push((pb.finish(), color, weight, visible));
}
@@ -613,7 +702,11 @@ mod w2d {
let n = r.count()?;
let mut pts = Vec::with_capacity(n);
for _ in 0..n {
let (dx, dy) = if wide { (r.i32()? as f64, r.i32()? as f64) } else { (r.i16()? as f64, r.i16()? as f64) };
let (dx, dy) = if wide {
(r.i32()? as f64, r.i32()? as f64)
} else {
(r.i16()? as f64, r.i16()? as f64)
};
pts.push(rel(&mut at, dx, dy));
}
// A triangle strip: each point closes a triangle with the
@@ -621,7 +714,11 @@ mod w2d {
let subpaths = pts
.windows(3)
.map(|t| SubPath {
segments: vec![Segment::Line(t[0], t[1]), Segment::Line(t[1], t[2]), Segment::Line(t[2], t[0])],
segments: vec![
Segment::Line(t[0], t[1]),
Segment::Line(t[1], t[2]),
Segment::Line(t[2], t[0]),
],
closed: true,
})
.collect();
@@ -642,7 +739,12 @@ mod w2d {
} else if sweep < 0.0 {
sweep += std::f64::consts::TAU;
}
raw.push((arc_path(c, radius, radius, 0.0, start, sweep), color, weight, visible));
raw.push((
arc_path(c, radius, radius, 0.0, start, sweep),
color,
weight,
visible,
));
}
b'e' | b'E' => {
// Ellipse: relative centre, major and minor radius, start
@@ -657,7 +759,12 @@ mod w2d {
sweep += std::f64::consts::TAU;
}
let fill = op == b'E';
raw.push((arc_path(c, major, minor, tilt * turn, s * turn, sweep), color, if fill { -1.0 } else { weight }, visible));
raw.push((
arc_path(c, major, minor, tilt * turn, s * turn, sweep),
color,
if fill { -1.0 } else { weight },
visible,
));
}
0x06 => {
// Font: a field mask, then the fields it names in order.
@@ -708,18 +815,34 @@ mod w2d {
.into_iter()
.filter(|(_, _, _, visible)| *visible)
.map(|(subpaths, color, weight, _)| {
let subpaths = subpaths.into_iter().map(|sp| map_subpath(sp, |p| units.model(p))).collect();
let subpaths = subpaths
.into_iter()
.map(|sp| map_subpath(sp, |p| units.model(p)))
.collect();
if weight < 0.0 {
Path { subpaths, stroke: None, fill: Some(color) }
Path {
subpaths,
stroke: None,
fill: Some(color),
}
} else {
Path { subpaths, stroke: Some((color, units.length(weight))), fill: None }
Path {
subpaths,
stroke: Some((color, units.length(weight))),
fill: None,
}
}
})
.collect();
let view = view.map(|v| {
let a = units.model([v[0], v[1]]);
let b = units.model([v[2], v[3]]);
[a[0].min(b[0]), a[1].min(b[1]), a[0].max(b[0]), a[1].max(b[1])]
[
a[0].min(b[0]),
a[1].min(b[1]),
a[0].max(b[0]),
a[1].max(b[1]),
]
});
let texts = texts
.into_iter()
@@ -748,12 +871,11 @@ mod w2d {
Some(String::from_utf8_lossy(&r.b[start..r.i - 1]).to_string())
} else {
let n = r.count()?;
let units: Vec<u16> = r
.b
.get(r.i..r.i + n * 2)?
.chunks_exact(2)
.map(|c| u16::from_le_bytes([c[0], c[1]]))
.collect();
let units: Vec<u16> =
r.b.get(r.i..r.i + n * 2)?
.chunks_exact(2)
.map(|c| u16::from_le_bytes([c[0], c[1]]))
.collect();
r.i += n * 2;
Some(String::from_utf16_lossy(&units))
}
@@ -763,7 +885,13 @@ mod w2d {
fn arc_path(c: [f64; 2], rx: f64, ry: f64, rot: f64, start: f64, sweep: f64) -> Vec<SubPath> {
let pieces = arc_cubics(c, rx, ry, rot, start, sweep);
let closed = (sweep.abs() - std::f64::consts::TAU).abs() < 1e-9;
vec![SubPath { segments: pieces.into_iter().map(|[a, b, cc, d]| Segment::Cubic(a, b, cc, d)).collect(), closed }]
vec![SubPath {
segments: pieces
.into_iter()
.map(|[a, b, cc, d]| Segment::Cubic(a, b, cc, d))
.collect(),
closed,
}]
}
fn map_subpath(sp: SubPath, f: impl Fn([f64; 2]) -> [f64; 2]) -> SubPath {
@@ -800,7 +928,10 @@ mod xps {
}
fn apply(m: Affine, p: [f64; 2]) -> [f64; 2] {
[m[0] * p[0] + m[2] * p[1] + m[4], m[1] * p[0] + m[3] * p[1] + m[5]]
[
m[0] * p[0] + m[2] * p[1] + m[4],
m[1] * p[0] + m[3] * p[1] + m[5],
]
}
fn parse_affine(text: &str) -> Option<Affine> {
@@ -831,7 +962,10 @@ mod xps {
for node in doc.descendants() {
match node.tag_name().name() {
"Units" => {
if let Some(u) = attr(node, "Transform").map(numbers).and_then(|m| Units::from_matrix(&m)) {
if let Some(u) = attr(node, "Transform")
.map(numbers)
.and_then(|m| Units::from_matrix(&m))
{
units = u;
}
}
@@ -859,53 +993,88 @@ mod xps {
};
let mut paths = Vec::new();
let mut texts = Vec::new();
walk(doc.root_element(), [1.0, 0.0, 0.0, 1.0, 0.0, 0.0], &mut |node, m| {
let m = match attr(node, "RenderTransform").and_then(parse_affine) {
Some(local) => mul(m, local),
None => m,
};
if node.tag_name().name() == "Glyphs" {
// Text: its baseline origin and em size in page units (y
// down), drawn in the embedded font's family.
let num = |name: &str| attr(node, name).and_then(|v| v.parse::<f64>().ok());
let (Some(em), Some(text)) = (num("FontRenderingEmSize"), attr(node, "UnicodeString")) else { return };
let o = [num("OriginX").unwrap_or(0.0), num("OriginY").unwrap_or(0.0)];
walk(
doc.root_element(),
[1.0, 0.0, 0.0, 1.0, 0.0, 0.0],
&mut |node, m| {
let m = match attr(node, "RenderTransform").and_then(parse_affine) {
Some(local) => mul(m, local),
None => m,
};
if node.tag_name().name() == "Glyphs" {
// Text: its baseline origin and em size in page units (y
// down), drawn in the embedded font's family.
let num = |name: &str| attr(node, name).and_then(|v| v.parse::<f64>().ok());
let (Some(em), Some(text)) =
(num("FontRenderingEmSize"), attr(node, "UnicodeString"))
else {
return;
};
let o = [num("OriginX").unwrap_or(0.0), num("OriginY").unwrap_or(0.0)];
let map = |p: [f64; 2]| units.model(to_logical(apply(m, p)));
let (a, along, up) = (map(o), map([o[0] + em, o[1]]), map([o[0], o[1] - em]));
let len = |p: [f64; 2]| ((p[0] - a[0]).powi(2) + (p[1] - a[1]).powi(2)).sqrt();
if !text.trim().is_empty() {
texts.push(Text {
text: text.to_string(),
origin: a,
height: len(up) * CAP_PER_EM,
width_factor: if len(up) > 0.0 {
len(along) / len(up)
} else {
1.0
},
rotation: (along[1] - a[1]).atan2(along[0] - a[0]),
color: attr(node, "Fill")
.and_then(parse_color)
.unwrap_or([0, 0, 0]),
font: attr(node, "FontUri")
.and_then(family)
.unwrap_or_else(|| "Arial".to_string()),
});
}
return;
}
let Some(data) = attr(node, "Data") else {
return;
};
let map = |p: [f64; 2]| units.model(to_logical(apply(m, p)));
let (a, along, up) = (map(o), map([o[0] + em, o[1]]), map([o[0], o[1] - em]));
let len = |p: [f64; 2]| ((p[0] - a[0]).powi(2) + (p[1] - a[1]).powi(2)).sqrt();
if !text.trim().is_empty() {
texts.push(Text {
text: text.to_string(),
origin: a,
height: len(up) * CAP_PER_EM,
width_factor: if len(up) > 0.0 { len(along) / len(up) } else { 1.0 },
rotation: (along[1] - a[1]).atan2(along[0] - a[0]),
color: attr(node, "Fill").and_then(parse_color).unwrap_or([0, 0, 0]),
font: attr(node, "FontUri").and_then(family).unwrap_or_else(|| "Arial".to_string()),
let subpaths: Vec<SubPath> = parse_data(data)
.into_iter()
.map(|sp| map_subpath(sp, map))
.collect();
if subpaths.is_empty() {
return;
}
let stroke_scale = (m[0] * m[3] - m[1] * m[2]).abs().sqrt() * px_mm
/ scale[0]
/ units.scale[0].abs();
let stroke = attr(node, "Stroke").and_then(parse_color).map(|c| {
let t = attr(node, "StrokeThickness")
.and_then(|v| v.parse::<f64>().ok())
.unwrap_or(0.0);
(c, t * stroke_scale)
});
let fill = attr(node, "Fill").and_then(parse_color);
if stroke.is_some() || fill.is_some() {
paths.push(Path {
subpaths,
stroke,
fill,
});
}
return;
}
let Some(data) = attr(node, "Data") else { return };
let map = |p: [f64; 2]| units.model(to_logical(apply(m, p)));
let subpaths: Vec<SubPath> = parse_data(data).into_iter().map(|sp| map_subpath(sp, map)).collect();
if subpaths.is_empty() {
return;
}
let stroke_scale = (m[0] * m[3] - m[1] * m[2]).abs().sqrt() * px_mm / scale[0] / units.scale[0].abs();
let stroke = attr(node, "Stroke").and_then(parse_color).map(|c| {
let t = attr(node, "StrokeThickness").and_then(|v| v.parse::<f64>().ok()).unwrap_or(0.0);
(c, t * stroke_scale)
});
let fill = attr(node, "Fill").and_then(parse_color);
if stroke.is_some() || fill.is_some() {
paths.push(Path { subpaths, stroke, fill });
}
}, 0);
},
0,
);
let view = view_log.map(|v: [f64; 4]| {
let a = units.model([v[0], v[1]]);
let b = units.model([v[2], v[3]]);
[a[0].min(b[0]), a[1].min(b[1]), a[0].max(b[0]), a[1].max(b[1])]
[
a[0].min(b[0]),
a[1].min(b[1]),
a[0].max(b[0]),
a[1].max(b[1]),
]
});
Some((paths, view, texts))
}
@@ -961,7 +1130,12 @@ mod xps {
while i < bytes.len() {
let d = bytes[i] as char;
let prev = bytes[i - 1] as char;
if d.is_ascii_digit() || d == '.' || d == 'e' || d == 'E' || ((d == '-' || d == '+') && (prev == 'e' || prev == 'E')) {
if d.is_ascii_digit()
|| d == '.'
|| d == 'e'
|| d == 'E'
|| ((d == '-' || d == '+') && (prev == 'e' || prev == 'E'))
{
i += 1;
} else {
break;
@@ -998,15 +1172,25 @@ mod xps {
}
let cur = pb.current().unwrap_or([0.0, 0.0]);
let relative = cmd.is_ascii_lowercase();
let pt = |x: f64, y: f64| if relative { [cur[0] + x, cur[1] + y] } else { [x, y] };
let pt = |x: f64, y: f64| {
if relative {
[cur[0] + x, cur[1] + y]
} else {
[x, y]
}
};
match cmd.to_ascii_uppercase() {
'M' => {
let (Some(x), Some(y)) = (num(&toks, &mut k), num(&toks, &mut k)) else { break };
let (Some(x), Some(y)) = (num(&toks, &mut k), num(&toks, &mut k)) else {
break;
};
pb.move_to(pt(x, y));
cmd = if relative { 'l' } else { 'L' };
}
'L' => {
let (Some(x), Some(y)) = (num(&toks, &mut k), num(&toks, &mut k)) else { break };
let (Some(x), Some(y)) = (num(&toks, &mut k), num(&toks, &mut k)) else {
break;
};
pb.line_to(pt(x, y));
}
'H' => {
@@ -1019,7 +1203,9 @@ mod xps {
}
'C' => {
let v: Vec<f64> = (0..6).filter_map(|_| num(&toks, &mut k)).collect();
if v.len() < 6 { break }
if v.len() < 6 {
break;
}
let (c1, c2, p) = (pt(v[0], v[1]), pt(v[2], v[3]), pt(v[4], v[5]));
pb.cubic_to(c1, c2, p);
last_ctrl = Some(c2);
@@ -1027,8 +1213,12 @@ mod xps {
}
'S' => {
let v: Vec<f64> = (0..4).filter_map(|_| num(&toks, &mut k)).collect();
if v.len() < 4 { break }
let c1 = last_ctrl.map(|c| [2.0 * cur[0] - c[0], 2.0 * cur[1] - c[1]]).unwrap_or(cur);
if v.len() < 4 {
break;
}
let c1 = last_ctrl
.map(|c| [2.0 * cur[0] - c[0], 2.0 * cur[1] - c[1]])
.unwrap_or(cur);
let (c2, p) = (pt(v[0], v[1]), pt(v[2], v[3]));
pb.cubic_to(c1, c2, p);
last_ctrl = Some(c2);
@@ -1036,17 +1226,35 @@ mod xps {
}
'Q' => {
let v: Vec<f64> = (0..4).filter_map(|_| num(&toks, &mut k)).collect();
if v.len() < 4 { break }
if v.len() < 4 {
break;
}
let (q, p) = (pt(v[0], v[1]), pt(v[2], v[3]));
let c1 = [cur[0] + 2.0 / 3.0 * (q[0] - cur[0]), cur[1] + 2.0 / 3.0 * (q[1] - cur[1])];
let c2 = [p[0] + 2.0 / 3.0 * (q[0] - p[0]), p[1] + 2.0 / 3.0 * (q[1] - p[1])];
let c1 = [
cur[0] + 2.0 / 3.0 * (q[0] - cur[0]),
cur[1] + 2.0 / 3.0 * (q[1] - cur[1]),
];
let c2 = [
p[0] + 2.0 / 3.0 * (q[0] - p[0]),
p[1] + 2.0 / 3.0 * (q[1] - p[1]),
];
pb.cubic_to(c1, c2, p);
}
'A' => {
let v: Vec<f64> = (0..7).filter_map(|_| num(&toks, &mut k)).collect();
if v.len() < 7 { break }
if v.len() < 7 {
break;
}
let end = pt(v[5], v[6]);
for [_, c1, c2, p] in svg_arc(cur, v[0], v[1], v[2].to_radians(), v[3] != 0.0, v[4] != 0.0, end) {
for [_, c1, c2, p] in svg_arc(
cur,
v[0],
v[1],
v[2].to_radians(),
v[3] != 0.0,
v[4] != 0.0,
end,
) {
pb.cubic_to(c1, c2, p);
}
}
@@ -1063,7 +1271,15 @@ mod xps {
}
/// An SVG/XPS endpoint arc as cubic pieces.
fn svg_arc(p0: [f64; 2], rx: f64, ry: f64, phi: f64, large: bool, sweep: bool, p1: [f64; 2]) -> Vec<[[f64; 2]; 4]> {
fn svg_arc(
p0: [f64; 2],
rx: f64,
ry: f64,
phi: f64,
large: bool,
sweep: bool,
p1: [f64; 2],
) -> Vec<[[f64; 2]; 4]> {
let (mut rx, mut ry) = (rx.abs(), ry.abs());
if rx == 0.0 || ry == 0.0 || p0 == p1 {
return vec![[p0, p0, p1, p1]];
@@ -1093,7 +1309,12 @@ mod xps {
a
};
let t1 = ang(1.0, 0.0, (x1 - cx1) / rx, (y1 - cy1) / ry);
let mut dt = ang((x1 - cx1) / rx, (y1 - cy1) / ry, (-x1 - cx1) / rx, (-y1 - cy1) / ry);
let mut dt = ang(
(x1 - cx1) / rx,
(y1 - cy1) / ry,
(-x1 - cx1) / rx,
(-y1 - cy1) / ry,
);
if !sweep && dt > 0.0 {
dt -= std::f64::consts::TAU;
} else if sweep && dt < 0.0 {
@@ -1170,22 +1391,36 @@ mod xps_depth_tests {
s.push_str(&"</r>".repeat(n));
s
};
assert!(super::xml_depth_within(&nest(super::MAX_XML_DEPTH), super::MAX_XML_DEPTH));
assert!(!super::xml_depth_within(&nest(super::MAX_XML_DEPTH + 1), super::MAX_XML_DEPTH));
assert!(super::xml_depth_within(
&nest(super::MAX_XML_DEPTH),
super::MAX_XML_DEPTH
));
assert!(!super::xml_depth_within(
&nest(super::MAX_XML_DEPTH + 1),
super::MAX_XML_DEPTH
));
// 1000 hostile-looking chunks, actual depth 1 each.
let mut s = String::from("<r>");
for _ in 0..1000 {
s.push_str(r#"<n t="</n><n><!-- --> <![CDATA[<b></b>]]>"/>
"#);
s.push_str(
r#"<n t="</n><n><!-- --> <![CDATA[<b></b>]]>"/>
"#,
);
}
s.push_str(&"</r>".repeat(1000));
assert!(super::xml_depth_within(&s, super::MAX_XML_DEPTH));
// Self-closing tags add no depth; only real opens do.
assert!(super::xml_depth_within(&"<a/>".repeat(5000), super::MAX_XML_DEPTH));
assert!(super::xml_depth_within(
&format!("<?xml version='1.0'?><a><!-- {} --></a>", "<b></b>".repeat(1000)),
&"<a/>".repeat(5000),
super::MAX_XML_DEPTH
));
assert!(super::xml_depth_within(
&format!(
"<?xml version='1.0'?><a><!-- {} --></a>",
"<b></b>".repeat(1000)
),
super::MAX_XML_DEPTH
));
}
@@ -1205,13 +1440,9 @@ mod xps_depth_tests {
let xml = r##"<FixedPage xmlns="http://schemas.microsoft.com/xps/2005/08" Width="100" Height="100">
<Path Data="M 10 10 L 50 50" Stroke="#FF0000"/>
</FixedPage>"##;
let (paths, _view, _texts) = xps::read(
xml.as_bytes(),
Some(deep.as_bytes()),
&entry(),
&|_| None,
)
.expect("page parses");
let (paths, _view, _texts) =
xps::read(xml.as_bytes(), Some(deep.as_bytes()), &entry(), &|_| None)
.expect("page parses");
assert_eq!(paths.len(), 1);
}
@@ -1227,10 +1458,7 @@ mod xps_depth_tests {
}
deep.push_str("</Page>");
let mut files = std::collections::HashMap::new();
files.insert(
"3D/descriptor.xml".to_string(),
deep.into_bytes(),
);
files.insert("3D/descriptor.xml".to_string(), deep.into_bytes());
assert!(super::sheets(&files).is_empty());
}
}
+155 -33
View File
@@ -58,14 +58,25 @@ fn raster_cache() -> &'static Mutex<HashMap<Key, Option<Arc<PdfPage>>>> {
/// Forget what was read from `path` (its bytes changed).
pub fn forget(path: &str) {
sheet_cache().lock().unwrap_or_else(|e| e.into_inner()).retain(|k, _| k.0 != path);
raster_cache().lock().unwrap_or_else(|e| e.into_inner()).retain(|k, _| k.0 != path);
vector_cache().lock().unwrap_or_else(|e| e.into_inner()).retain(|k, _| k.0 != path);
sheet_cache()
.lock()
.unwrap_or_else(|e| e.into_inner())
.retain(|k, _| k.0 != path);
raster_cache()
.lock()
.unwrap_or_else(|e| e.into_inner())
.retain(|k, _| k.0 != path);
vector_cache()
.lock()
.unwrap_or_else(|e| e.into_inner())
.retain(|k, _| k.0 != path);
}
/// Layer (DWF) or level (DGN) names of a file; empty when it has none.
pub fn layer_names(kind: UnderlayType, path: &str) -> Vec<String> {
let Some(bytes) = source_bytes(path) else { return Vec::new() };
let Some(bytes) = source_bytes(path) else {
return Vec::new();
};
match kind {
// A plotted DWF carries no layers.
UnderlayType::Dwf | UnderlayType::Pdf => None,
@@ -83,16 +94,27 @@ pub fn sheet(kind: UnderlayType, path: &str, item: &str) -> Option<Arc<Sheet>> {
/// The sheet without the `hidden` layers / levels, on the full sheet's
/// rectangle (turning layers off never moves the underlay), memoised.
pub fn sheet_without(kind: UnderlayType, path: &str, item: &str, hidden: &[String]) -> Option<Arc<Sheet>> {
pub fn sheet_without(
kind: UnderlayType,
path: &str,
item: &str,
hidden: &[String],
) -> Option<Arc<Sheet>> {
let key = (path.to_string(), layered_item(item, hidden));
if let Some(hit) = sheet_cache().lock().unwrap_or_else(|e| e.into_inner()).get(&key) {
if let Some(hit) = sheet_cache()
.lock()
.unwrap_or_else(|e| e.into_inner())
.get(&key)
{
return hit.clone();
}
let read = || -> Option<Sheet> {
let bytes = source_bytes(path)?;
let mut sheet = match kind {
UnderlayType::Dwf => dwf::sheet(&bytes, item)?,
UnderlayType::Dgn if dgn7::is_v7(&bytes) => dgn7::model(&bytes, hidden).unwrap_or_default(),
UnderlayType::Dgn if dgn7::is_v7(&bytes) => {
dgn7::model(&bytes, hidden).unwrap_or_default()
}
UnderlayType::Dgn => dgn8::model(&bytes, item, hidden).unwrap_or_default(),
UnderlayType::Pdf => return None,
};
@@ -112,11 +134,25 @@ pub fn sheet_without(kind: UnderlayType, path: &str, item: &str, hidden: &[Strin
&t.font,
&t.text,
);
let length = strokes.iter().flatten().map(|p| p[0] as f64).fold(0.0, f64::max);
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, t.height], [0.0, 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])];
r = [
r[0].min(p[0]),
r[1].min(p[1]),
r[2].max(p[0]),
r[3].max(p[1]),
];
}
}
sheet.rect = r;
@@ -129,7 +165,10 @@ pub fn sheet_without(kind: UnderlayType, path: &str, item: &str, hidden: &[Strin
Some(sheet)
};
let value = read().map(Arc::new);
sheet_cache().lock().unwrap_or_else(|e| e.into_inner()).insert(key, value.clone());
sheet_cache()
.lock()
.unwrap_or_else(|e| e.into_inner())
.insert(key, value.clone());
value
}
@@ -161,14 +200,24 @@ pub fn dgn_contrast_pivot(
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 = 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),
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;
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)
}
@@ -188,20 +237,38 @@ fn outline_texts(sheet: &mut Sheet) {
let subpaths: Vec<SubPath> = strokes
.iter()
.filter(|s| s.len() >= 2)
.map(|s| SubPath { segments: s.windows(2).map(|w| Segment::Line(at(w[0]), at(w[1]))).collect(), closed: false })
.map(|s| SubPath {
segments: s
.windows(2)
.map(|w| Segment::Line(at(w[0]), at(w[1])))
.collect(),
closed: false,
})
.collect();
if !subpaths.is_empty() {
sheet.paths.push(Path { subpaths, stroke: Some((t.color, -1.0)), fill: None });
sheet.paths.push(Path {
subpaths,
stroke: Some((t.color, -1.0)),
fill: None,
});
}
let triangles: Vec<SubPath> = fills
.chunks_exact(3)
.map(|t| SubPath {
segments: vec![Segment::Line(at(t[0]), at(t[1])), Segment::Line(at(t[1]), at(t[2])), Segment::Line(at(t[2]), at(t[0]))],
segments: vec![
Segment::Line(at(t[0]), at(t[1])),
Segment::Line(at(t[1]), at(t[2])),
Segment::Line(at(t[2]), at(t[0])),
],
closed: true,
})
.collect();
if !triangles.is_empty() {
sheet.paths.push(Path { subpaths: triangles, stroke: None, fill: Some(t.color) });
sheet.paths.push(Path {
subpaths: triangles,
stroke: None,
fill: Some(t.color),
});
}
}
}
@@ -249,13 +316,27 @@ pub fn display_raster(
let side = side.clamp(16.0, RASTER_SIDE).round();
let key = (
path.to_string(),
format!("{}@{side}~{}{}", layered_item(item, hidden), backdrop.max_channel, backdrop.light),
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) {
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, backdrop)).map(Arc::new);
raster_cache().lock().unwrap_or_else(|e| e.into_inner()).insert(key, value.clone());
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
}
@@ -273,16 +354,26 @@ fn rasterize(sheet: &Sheet, side: f64, backdrop: Backdrop) -> Option<PdfPage> {
return None;
}
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 (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)?;
// 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);
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 {
let Some(first) = sp.segments.first() else { continue };
let Some(first) = sp.segments.first() else {
continue;
};
let s = to_px(first.start());
pb.move_to(s.0, s.1);
for seg in &sp.segments {
@@ -307,13 +398,23 @@ fn rasterize(sheet: &Sheet, side: f64, backdrop: Backdrop) -> Option<PdfPage> {
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);
pixmap.fill_path(
&p,
&paint,
tiny_skia::FillRule::Winding,
tiny_skia::Transform::identity(),
None,
);
}
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).
let px = if width < 0.0 { (-width) as f32 } else { ((width * scale) as f32).max(1.0) };
let px = if width < 0.0 {
(-width) as f32
} else {
((width * scale) as f32).max(1.0)
};
let stroke = tiny_skia::Stroke {
width: px,
line_cap: tiny_skia::LineCap::Round,
@@ -350,7 +451,12 @@ fn rasterize(sheet: &Sheet, side: f64, backdrop: Backdrop) -> Option<PdfPage> {
}
}
}
Some(PdfPage { pixels: Arc::new(pixels), width: pw, height: ph, dpi: 0.0 })
Some(PdfPage {
pixels: Arc::new(pixels),
width: pw,
height: ph,
dpi: 0.0,
})
}
/// A path's horizontal and vertical segments, and its slanted and curved
@@ -405,19 +511,32 @@ fn along_axis(a: (f32, f32), b: (f32, f32)) -> bool {
}
fn vector_cache() -> &'static Mutex<HashMap<Key, Option<Arc<super::pdf_vector::PageVectors>>>> {
static CACHE: OnceLock<Mutex<HashMap<Key, Option<Arc<super::pdf_vector::PageVectors>>>>> = OnceLock::new();
static CACHE: OnceLock<Mutex<HashMap<Key, Option<Arc<super::pdf_vector::PageVectors>>>>> =
OnceLock::new();
CACHE.get_or_init(Default::default)
}
/// The sheet's geometry as page vectors for object snaps, in sheet units,
/// memoised.
pub fn page_vectors(kind: UnderlayType, path: &str, item: &str, hidden: &[String]) -> Option<Arc<super::pdf_vector::PageVectors>> {
pub fn page_vectors(
kind: UnderlayType,
path: &str,
item: &str,
hidden: &[String],
) -> Option<Arc<super::pdf_vector::PageVectors>> {
let key = (path.to_string(), layered_item(item, hidden));
if let Some(hit) = vector_cache().lock().unwrap_or_else(|e| e.into_inner()).get(&key) {
if let Some(hit) = vector_cache()
.lock()
.unwrap_or_else(|e| e.into_inner())
.get(&key)
{
return hit.clone();
}
let value = sheet_without(kind, path, item, hidden).map(|s| Arc::new(to_page_vectors(&s)));
vector_cache().lock().unwrap_or_else(|e| e.into_inner()).insert(key, value.clone());
vector_cache()
.lock()
.unwrap_or_else(|e| e.into_inner())
.insert(key, value.clone());
value
}
@@ -446,5 +565,8 @@ fn to_page_vectors(sheet: &Sheet) -> super::pdf_vector::PageVectors {
fill: p.fill,
})
.collect();
pv::PageVectors { paths, ..Default::default() }
pv::PageVectors {
paths,
..Default::default()
}
}
+293 -42
View File
@@ -132,7 +132,10 @@ impl PathBuilder {
fn flush(&mut self, closed: bool) {
if !self.current.is_empty() {
self.subpaths.push(SubPath { segments: std::mem::take(&mut self.current), closed });
self.subpaths.push(SubPath {
segments: std::mem::take(&mut self.current),
closed,
});
}
}
@@ -187,9 +190,19 @@ pub fn arc_cubics(
/// 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 b = [
f64::INFINITY,
f64::INFINITY,
f64::NEG_INFINITY,
f64::NEG_INFINITY,
];
let mut add = |p: [f64; 2]| {
b = [b[0].min(p[0]), b[1].min(p[1]), b[2].max(p[0]), b[3].max(p[1])];
b = [
b[0].min(p[0]),
b[1].min(p[1]),
b[2].max(p[0]),
b[3].max(p[1]),
];
};
for path in paths {
for sp in &path.subpaths {
@@ -216,7 +229,10 @@ pub fn paths_bounds(paths: &[Path]) -> Option<[f64; 4]> {
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]]
[
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.
@@ -247,38 +263,262 @@ fn cubic_extrema(p0: [f64; 2], p1: [f64; 2], p2: [f64; 2], p3: [f64; 2]) -> Vec<
/// 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],
[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
@@ -316,7 +556,12 @@ pub const CAP_PER_EM: f64 = 0.716;
/// Points of a B-spline curve: its order, control points and knot vector
/// (clamped: `order` equal knots at each end).
pub fn bspline_points(order: usize, poles: &[[f64; 2]], knots: &[f64], samples: usize) -> Vec<[f64; 2]> {
pub fn bspline_points(
order: usize,
poles: &[[f64; 2]],
knots: &[f64],
samples: usize,
) -> Vec<[f64; 2]> {
let n = poles.len();
if order < 2 || n < order || knots.len() != n + order {
return poles.to_vec();
@@ -335,8 +580,15 @@ pub fn bspline_points(order: usize, poles: &[[f64; 2]], knots: &[f64], samples:
for j in (r..order).rev() {
let i = j + k + 1 - order;
let den = knots[i + order - r] - knots[i];
let a = if den.abs() < 1e-15 { 0.0 } else { (t - knots[i]) / den };
d[j] = [d[j - 1][0] * (1.0 - a) + d[j][0] * a, d[j - 1][1] * (1.0 - a) + d[j][1] * a];
let a = if den.abs() < 1e-15 {
0.0
} else {
(t - knots[i]) / den
};
d[j] = [
d[j - 1][0] * (1.0 - a) + d[j][0] * a,
d[j - 1][1] * (1.0 - a) + d[j][1] * a,
];
}
}
out.push(d[order - 1]);
@@ -373,9 +625,8 @@ mod arc_cubics_sweep_tests {
/// Ordinary arcs keep their exact piece count, ceil(|sweep| / 90°).
#[test]
fn ordinary_sweeps_keep_their_piece_counts() {
let expect = |sweep: f64| {
((sweep.abs() / std::f64::consts::FRAC_PI_2).ceil() as usize).max(1)
};
let expect =
|sweep: f64| ((sweep.abs() / std::f64::consts::FRAC_PI_2).ceil() as usize).max(1);
for sweep in [
0.0,
1.0,
+36 -10
View File
@@ -19,7 +19,10 @@ pub(crate) fn display_wires(
name: &str,
background: [f32; 4],
) -> Vec<WireModel> {
if def.underlay_type == UnderlayType::Pdf || def.unloaded || !u.flags.contains(UnderlayDisplayFlags::ON) {
if def.underlay_type == UnderlayType::Pdf
|| def.unloaded
|| !u.flags.contains(UnderlayDisplayFlags::ON)
{
return Vec::new();
}
let page = crate::entities::underlay::page_of(def);
@@ -31,10 +34,25 @@ pub(crate) fn display_wires(
let backdrop = Backdrop::of([background[0], background[1], background[2]]);
// A DGN model's fade is in its colours; a DWF sheet's blends it with
// what is under it.
let alpha = if def.underlay_type == UnderlayType::Dgn { 1.0 } else { 1.0 - u.fade.min(100) as f32 / 100.0 };
let alpha = if def.underlay_type == UnderlayType::Dgn {
1.0
} else {
1.0 - u.fade.min(100) as f32 / 100.0
};
let shown = |c: [u8; 3]| -> [f32; 4] {
let c = if sheet.table_colors { model::dgn_table_color(c, backdrop.max_channel, backdrop.light) } else { c };
let rgb = crate::scene::model::pdf_raster::adjust_rgb([c[0] as f32 / 255.0, c[1] as f32 / 255.0, c[2] as f32 / 255.0], &adjust);
let c = if sheet.table_colors {
model::dgn_table_color(c, backdrop.max_channel, backdrop.light)
} else {
c
};
let rgb = crate::scene::model::pdf_raster::adjust_rgb(
[
c[0] as f32 / 255.0,
c[1] as f32 / 255.0,
c[2] as f32 / 255.0,
],
&adjust,
);
[rgb[0], rgb[1], rgb[2], alpha]
};
let world = |p: [f64; 2]| crate::entities::underlay::local_to_world(u, p);
@@ -73,10 +91,12 @@ pub(crate) fn display_wires(
}
if let Some((c, width)) = path.stroke {
let color = shown(c);
let at = *line_at.entry((key(color), width.to_bits())).or_insert_with(|| {
lines.push((color, width, Vec::new()));
lines.len() - 1
});
let at = *line_at
.entry((key(color), width.to_bits()))
.or_insert_with(|| {
lines.push((color, width, Vec::new()));
lines.len() - 1
});
for sp in &path.subpaths {
let mut pts = polyline(&sp.segments, tolerance);
if sp.closed && pts.len() >= 2 && pts.first() != pts.last() {
@@ -130,8 +150,14 @@ pub(crate) fn display_wires(
let w = world(p);
[w[0], w[1]]
});
let lo = [corners.iter().map(|p| p[0]).fold(f64::MAX, f64::min), corners.iter().map(|p| p[1]).fold(f64::MAX, f64::min)];
let hi = [corners.iter().map(|p| p[0]).fold(f64::MIN, f64::max), corners.iter().map(|p| p[1]).fold(f64::MIN, f64::max)];
let lo = [
corners.iter().map(|p| p[0]).fold(f64::MAX, f64::min),
corners.iter().map(|p| p[1]).fold(f64::MAX, f64::min),
];
let hi = [
corners.iter().map(|p| p[0]).fold(f64::MIN, f64::max),
corners.iter().map(|p| p[1]).fold(f64::MIN, f64::max),
];
crate::scene::pick::xclip::inverted_ring(&clip, (lo, hi))
} else {
clip
+2 -7
View File
@@ -1,6 +1,4 @@
use codec::objects::{
ObjectType, VisualStyle, VisualStyleProperty, VisualStylePropertyValue,
};
use codec::objects::{ObjectType, VisualStyle, VisualStyleProperty, VisualStylePropertyValue};
use codec::{CadDocument, EntityType, Handle};
#[derive(Clone, Debug)]
@@ -240,10 +238,7 @@ pub(crate) fn resolve_visual_style_handle(
.map(|(handle, style)| MeshVisualStyle::from_dwg(handle, style))
}
fn visual_style(
document: &CadDocument,
handle: Option<Handle>,
) -> Option<(Handle, &VisualStyle)> {
fn visual_style(document: &CadDocument, handle: Option<Handle>) -> Option<(Handle, &VisualStyle)> {
let handle = handle.filter(|handle| handle.is_valid())?;
match document.objects.get(&handle) {
Some(ObjectType::VisualStyle(style)) => Some((handle, style)),
+204 -76
View File
@@ -79,29 +79,72 @@ impl TangentGeom {
pub fn translated(&self, delta: glam::DVec3) -> Self {
match self {
Self::Line { p1, p2 } => Self::Line {
p1: [p1[0] + delta.x as f32, p1[1] + delta.y as f32, p1[2] + delta.z as f32],
p2: [p2[0] + delta.x as f32, p2[1] + delta.y as f32, p2[2] + delta.z as f32],
p1: [
p1[0] + delta.x as f32,
p1[1] + delta.y as f32,
p1[2] + delta.z as f32,
],
p2: [
p2[0] + delta.x as f32,
p2[1] + delta.y as f32,
p2[2] + delta.z as f32,
],
},
Self::Circle { center, radius } => Self::Circle {
center: [center[0] + delta.x as f32, center[1] + delta.y as f32, center[2] + delta.z as f32],
center: [
center[0] + delta.x as f32,
center[1] + delta.y as f32,
center[2] + delta.z as f32,
],
radius: *radius,
},
Self::PlanarCircle { center, axis_x, axis_y, radius } => Self::PlanarCircle {
center: [center[0] + delta.x, center[1] + delta.y, center[2] + delta.z],
Self::PlanarCircle {
center,
axis_x,
axis_y,
radius,
} => Self::PlanarCircle {
center: [
center[0] + delta.x,
center[1] + delta.y,
center[2] + delta.z,
],
axis_x: *axis_x,
axis_y: *axis_y,
radius: *radius,
},
Self::Arc { center, axis_x, axis_y, radius, start_angle, end_angle } => Self::Arc {
center: [center[0] + delta.x, center[1] + delta.y, center[2] + delta.z],
Self::Arc {
center,
axis_x,
axis_y,
radius,
start_angle,
end_angle,
} => Self::Arc {
center: [
center[0] + delta.x,
center[1] + delta.y,
center[2] + delta.z,
],
axis_x: *axis_x,
axis_y: *axis_y,
radius: *radius,
start_angle: *start_angle,
end_angle: *end_angle,
},
Self::PlanarEllipse { center, major_axis, normal, minor_axis_ratio, start_param, end_param } => Self::PlanarEllipse {
center: [center[0] + delta.x, center[1] + delta.y, center[2] + delta.z],
Self::PlanarEllipse {
center,
major_axis,
normal,
minor_axis_ratio,
start_param,
end_param,
} => Self::PlanarEllipse {
center: [
center[0] + delta.x,
center[1] + delta.y,
center[2] + delta.z,
],
major_axis: *major_axis,
normal: *normal,
minor_axis_ratio: *minor_axis_ratio,
@@ -113,7 +156,8 @@ impl TangentGeom {
pub fn rotated(&self, center: glam::DVec3, axis: glam::DVec3, angle_rad: f64) -> Self {
let quat = glam::DQuat::from_axis_angle(axis.normalize_or(glam::DVec3::Z), angle_rad);
let rot_pt = |p: [f64; 3]| (center + quat * (glam::DVec3::from_array(p) - center)).to_array();
let rot_pt =
|p: [f64; 3]| (center + quat * (glam::DVec3::from_array(p) - center)).to_array();
let rot_vec = |v: [f64; 3]| (quat * glam::DVec3::from_array(v)).to_array();
match self {
Self::Line { p1, p2 } => {
@@ -134,13 +178,25 @@ impl TangentGeom {
radius: *radius,
}
}
Self::PlanarCircle { center: c, axis_x, axis_y, radius } => Self::PlanarCircle {
Self::PlanarCircle {
center: c,
axis_x,
axis_y,
radius,
} => Self::PlanarCircle {
center: rot_pt(*c),
axis_x: rot_vec(*axis_x),
axis_y: rot_vec(*axis_y),
radius: *radius,
},
Self::Arc { center: c, axis_x, axis_y, radius, start_angle, end_angle } => Self::Arc {
Self::Arc {
center: c,
axis_x,
axis_y,
radius,
start_angle,
end_angle,
} => Self::Arc {
center: rot_pt(*c),
axis_x: rot_vec(*axis_x),
axis_y: rot_vec(*axis_y),
@@ -148,7 +204,14 @@ impl TangentGeom {
start_angle: *start_angle,
end_angle: *end_angle,
},
Self::PlanarEllipse { center: c, major_axis, normal, minor_axis_ratio, start_param, end_param } => Self::PlanarEllipse {
Self::PlanarEllipse {
center: c,
major_axis,
normal,
minor_axis_ratio,
start_param,
end_param,
} => Self::PlanarEllipse {
center: rot_pt(*c),
major_axis: rot_vec(*major_axis),
normal: rot_vec(*normal),
@@ -160,7 +223,8 @@ impl TangentGeom {
}
pub fn scaled(&self, center: glam::DVec3, factor: f64) -> Self {
let scale_pt = |p: [f64; 3]| (center + (glam::DVec3::from_array(p) - center) * factor).to_array();
let scale_pt =
|p: [f64; 3]| (center + (glam::DVec3::from_array(p) - center) * factor).to_array();
match self {
Self::Line { p1, p2 } => {
let p1_d = [p1[0] as f64, p1[1] as f64, p1[2] as f64];
@@ -180,13 +244,25 @@ impl TangentGeom {
radius: *radius * factor.abs() as f32,
}
}
Self::PlanarCircle { center: c, axis_x, axis_y, radius } => Self::PlanarCircle {
Self::PlanarCircle {
center: c,
axis_x,
axis_y,
radius,
} => Self::PlanarCircle {
center: scale_pt(*c),
axis_x: *axis_x,
axis_y: *axis_y,
radius: *radius * factor.abs(),
},
Self::Arc { center: c, axis_x, axis_y, radius, start_angle, end_angle } => Self::Arc {
Self::Arc {
center: c,
axis_x,
axis_y,
radius,
start_angle,
end_angle,
} => Self::Arc {
center: scale_pt(*c),
axis_x: *axis_x,
axis_y: *axis_y,
@@ -194,7 +270,14 @@ impl TangentGeom {
start_angle: *start_angle,
end_angle: *end_angle,
},
Self::PlanarEllipse { center: c, major_axis, normal, minor_axis_ratio, start_param, end_param } => Self::PlanarEllipse {
Self::PlanarEllipse {
center: c,
major_axis,
normal,
minor_axis_ratio,
start_param,
end_param,
} => Self::PlanarEllipse {
center: scale_pt(*c),
major_axis: (glam::DVec3::from_array(*major_axis) * factor.abs()).to_array(),
normal: *normal,
@@ -233,7 +316,12 @@ impl TangentGeom {
radius: *radius,
}
}
Self::PlanarCircle { center: c, axis_x, axis_y, radius } => {
Self::PlanarCircle {
center: c,
axis_x,
axis_y,
radius,
} => {
let ax = refl_vec(*axis_x);
let ay = refl_vec(*axis_y);
Self::PlanarCircle {
@@ -243,7 +331,14 @@ impl TangentGeom {
radius: *radius,
}
}
Self::Arc { center: c, axis_x, axis_y, radius, start_angle, end_angle } => {
Self::Arc {
center: c,
axis_x,
axis_y,
radius,
start_angle,
end_angle,
} => {
let ax = refl_vec(*axis_x);
let ay = refl_vec(*axis_y);
Self::Arc {
@@ -255,7 +350,14 @@ impl TangentGeom {
end_angle: -start_angle,
}
}
Self::PlanarEllipse { center: c, major_axis, normal: n, minor_axis_ratio, start_param, end_param } => {
Self::PlanarEllipse {
center: c,
major_axis,
normal: n,
minor_axis_ratio,
start_param,
end_param,
} => {
let norm = refl_vec(*n);
Self::PlanarEllipse {
center: refl_pt(*c),
@@ -479,14 +581,13 @@ pub fn clean_searchable_text(raw: &str) -> String {
}
}
}
i += 2 + (rest.len() - after_plus.len())
+ if pushed { digits.len() } else { 0 };
i +=
2 + (rest.len() - after_plus.len()) + if pushed { digits.len() } else { 0 };
continue;
}
// Decoration / format toggles: drop both the opener and closer.
b'L' | b'l' | b'O' | b'o' | b'K' | b'k' | b'C' | b'c' | b'F'
| b'f' | b'H' | b'h' | b'Q' | b'q' | b'T'
| b't' | b'W' | b'w' | b'A' | b'a' | b'M' | b'm' => {
b'L' | b'l' | b'O' | b'o' | b'K' | b'k' | b'C' | b'c' | b'F' | b'f' | b'H'
| b'h' | b'Q' | b'q' | b'T' | b't' | b'W' | b'w' | b'A' | b'a' | b'M' | b'm' => {
// `\C1;`, `\Farial;` etc. run to the next `;` — skip it.
if let Some(semi) = raw[i + 2..].find(';') {
i += 2 + semi + 1;
@@ -512,8 +613,7 @@ pub fn clean_searchable_text(raw: &str) -> String {
&& bytes[i + 3].is_ascii_digit()
&& bytes[i + 4].is_ascii_digit() =>
{
let digits =
std::str::from_utf8(&bytes[i + 2..i + 5]).unwrap_or("");
let digits = std::str::from_utf8(&bytes[i + 2..i + 5]).unwrap_or("");
if let Ok(code) = digits.parse::<u32>() {
out.push(char::from_u32(code).unwrap_or('?'));
} else {
@@ -891,12 +991,7 @@ impl WireModel {
}
/// Return a clone rotated about an arbitrary world-space axis.
pub fn rotated_about_axis(
&self,
center: glam::Vec3,
axis: glam::Vec3,
angle_rad: f32,
) -> Self {
pub fn rotated_about_axis(&self, center: glam::Vec3, axis: glam::Vec3, angle_rad: f32) -> Self {
let rotation = glam::Quat::from_axis_angle(axis.normalize_or_zero(), angle_rad);
let (s, c) = angle_rad.sin_cos();
let mut out = self.clone();
@@ -927,10 +1022,7 @@ impl WireModel {
let (s, c) = (s as f64, c as f64);
out.text_verts = map_text_verts(&self.text_verts, |x, y, z| {
let v = glam::DVec3::new(x, y, z) - center;
let mapped = center
+ v * c
+ axis.cross(v) * s
+ axis * axis.dot(v) * (1.0 - c);
let mapped = center + v * c + axis.cross(v) * s + axis * axis.dot(v) * (1.0 - c);
(mapped.x, mapped.y, mapped.z)
});
}
@@ -988,22 +1080,17 @@ impl WireModel {
/// outside stay put. Exact for line/polyline vertices (the primary stretch
/// targets); curve tessellation points may deform where a window edge cuts
/// through them, matching the per-vertex nature of the operation.
pub fn stretched(
&self,
win_min: glam::Vec3,
win_max: glam::Vec3,
delta: glam::Vec3,
) -> Self {
self.stretched_windows(&[(win_min, win_max)], delta)
}
pub fn stretched(&self, win_min: glam::Vec3, win_max: glam::Vec3, delta: glam::Vec3) -> Self {
self.stretched_windows(&[(win_min, win_max)], delta)
}
/// Return a clone for a multi-window STRETCH preview. A point moves exactly
/// once when it lies inside any of the crossing windows.
pub fn stretched_windows(
&self,
windows: &[(glam::Vec3, glam::Vec3)],
delta: glam::Vec3,
) -> Self {
/// Return a clone for a multi-window STRETCH preview. A point moves exactly
/// once when it lies inside any of the crossing windows.
pub fn stretched_windows(
&self,
windows: &[(glam::Vec3, glam::Vec3)],
delta: glam::Vec3,
) -> Self {
let mut out = self.clone();
out.name = format!("preview_{}", self.name);
out.color = Self::CYAN;
@@ -1011,10 +1098,7 @@ pub fn stretched_windows(
let inside = |x: f32, y: f32| {
windows.iter().any(|(win_min, win_max)| {
x >= win_min.x
&& x <= win_max.x
&& y >= win_min.y
&& y <= win_max.y
x >= win_min.x && x <= win_max.x && y >= win_min.y && y <= win_max.y
})
};
@@ -1038,8 +1122,7 @@ pub fn stretched_windows(
}
if !out.text_verts.is_empty() {
let (dx, dy, dz) =
(delta.x as f64, delta.y as f64, delta.z as f64);
let (dx, dy, dz) = (delta.x as f64, delta.y as f64, delta.z as f64);
out.text_verts = map_text_verts(&self.text_verts, |x, y, z| {
let inside = windows.iter().any(|(win_min, win_max)| {
@@ -1099,15 +1182,12 @@ pub fn stretched_windows(
let p1 = p1.as_dvec3();
let normal = plane_normal.as_dvec3();
marker.origin -= 2.0 * normal.dot(marker.origin - p1) * normal;
marker.normal =
(marker.normal - 2.0 * normal.dot(marker.normal) * normal)
.normalize_or(glam::DVec3::Z);
marker.axis_x =
(marker.axis_x - 2.0 * normal.dot(marker.axis_x) * normal)
.normalize_or(glam::DVec3::X);
marker.axis_y =
(marker.axis_y - 2.0 * normal.dot(marker.axis_y) * normal)
.normalize_or(glam::DVec3::Y);
marker.normal = (marker.normal - 2.0 * normal.dot(marker.normal) * normal)
.normalize_or(glam::DVec3::Z);
marker.axis_x = (marker.axis_x - 2.0 * normal.dot(marker.axis_x) * normal)
.normalize_or(glam::DVec3::X);
marker.axis_y = (marker.axis_y - 2.0 * normal.dot(marker.axis_y) * normal)
.normalize_or(glam::DVec3::Y);
}
// Glyph quads reflect wholesale (true mirror) — the caller only routes
// text through here for MIRRTEXT-on; MIRRTEXT-off relocates via
@@ -1261,7 +1341,10 @@ pub fn run_rect_overlap(
let (dx, dy) = (c as f64, s as f64);
let (nx, ny) = (-dy, dx);
let corners = [
[origin[0] + nx * -0.3 * height, origin[1] + ny * -0.3 * height],
[
origin[0] + nx * -0.3 * height,
origin[1] + ny * -0.3 * height,
],
[
origin[0] + dx * adv_width + nx * -0.3 * height,
origin[1] + dy * adv_width + ny * -0.3 * height,
@@ -1282,7 +1365,12 @@ pub fn run_rect_overlap(
hi = hi.max(t);
}
let (mut rlo, mut rhi) = (f64::INFINITY, f64::NEG_INFINITY);
for p in [[rect[0], rect[1]], [rect[2], rect[1]], [rect[2], rect[3]], [rect[0], rect[3]]] {
for p in [
[rect[0], rect[1]],
[rect[2], rect[1]],
[rect[2], rect[3]],
[rect[0], rect[3]],
] {
let t = p[0] * ax + p[1] * ay;
rlo = rlo.min(t);
rhi = rhi.max(t);
@@ -1407,7 +1495,14 @@ mod tests {
} else {
panic!("Expected PlanarCircle");
}
if let TangentGeom::Arc { center, radius, start_angle, end_angle, .. } = trans.tangent_geoms[1] {
if let TangentGeom::Arc {
center,
radius,
start_angle,
end_angle,
..
} = trans.tangent_geoms[1]
{
assert!((center[0] - 15.0).abs() < 1e-5);
assert!((center[1] - 15.0).abs() < 1e-5);
assert!((center[2] - 1.0).abs() < 1e-5);
@@ -1417,7 +1512,13 @@ mod tests {
} else {
panic!("Expected Arc");
}
if let TangentGeom::PlanarEllipse { center, major_axis, minor_axis_ratio, .. } = trans.tangent_geoms[2] {
if let TangentGeom::PlanarEllipse {
center,
major_axis,
minor_axis_ratio,
..
} = trans.tangent_geoms[2]
{
assert!((center[0] - 15.0).abs() < 1e-5);
assert!((center[1] - 15.0).abs() < 1e-5);
assert!((center[2] - 1.0).abs() < 1e-5);
@@ -1447,7 +1548,13 @@ mod tests {
// Test rotated
let rot = wire.rotated(glam::Vec3::ZERO, std::f32::consts::FRAC_PI_2);
if let TangentGeom::PlanarCircle { center, axis_x, axis_y, .. } = rot.tangent_geoms[0] {
if let TangentGeom::PlanarCircle {
center,
axis_x,
axis_y,
..
} = rot.tangent_geoms[0]
{
assert!((center[0] - -20.0).abs() < 1e-4);
assert!((center[1] - 10.0).abs() < 1e-4);
assert!((axis_x[1] - 1.0).abs() < 1e-4);
@@ -1457,7 +1564,10 @@ mod tests {
}
// Test mirrored across Y=0 plane (normal = Y)
let mirr = wire.mirrored(glam::Vec3::new(0.0, 0.0, 0.0), glam::Vec3::new(1.0, 0.0, 0.0));
let mirr = wire.mirrored(
glam::Vec3::new(0.0, 0.0, 0.0),
glam::Vec3::new(1.0, 0.0, 0.0),
);
if let TangentGeom::PlanarCircle { center, .. } = mirr.tangent_geoms[0] {
assert!((center[0] - 10.0).abs() < 1e-4);
assert!((center[1] - -20.0).abs() < 1e-4);
@@ -1491,8 +1601,20 @@ mod tests {
#[test]
fn run_rect_overlap_keeps_straddlers_drops_outsiders() {
assert!(run_rect_overlap([10.0, 20.0], 0.0, 8.0, 5.0, [0.0, 0.0, 30.0, 30.0]));
assert!(!run_rect_overlap([10.0, 20.0], 0.0, 8.0, 5.0, [100.0, 100.0, 130.0, 130.0]));
assert!(run_rect_overlap(
[10.0, 20.0],
0.0,
8.0,
5.0,
[0.0, 0.0, 30.0, 30.0]
));
assert!(!run_rect_overlap(
[10.0, 20.0],
0.0,
8.0,
5.0,
[100.0, 100.0, 130.0, 130.0]
));
// A 90° run crossing the rect edge still counts (origin-only culling
// would have dropped it).
assert!(run_rect_overlap(
@@ -1503,7 +1625,13 @@ mod tests {
[0.0, 0.0, 30.0, 30.0]
));
// Degenerate runs never cull (conservative: searchability only).
assert!(run_rect_overlap([500.0, 500.0], 0.0, 0.0, 5.0, [0.0, 0.0, 30.0, 30.0]));
assert!(run_rect_overlap(
[500.0, 500.0],
0.0,
0.0,
5.0,
[0.0, 0.0, 30.0, 30.0]
));
}
#[test]