forked from 1b-agent/opencadstudio-upstream
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:
596 files changed
+69069
-34120
No files matched your search
@@ -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.
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
+273
-316
File diff suppressed because it is too large.
Load diff
@@ -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]
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
|
||||
@@ -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 §ions {
|
||||
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(
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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]
|
||||
|
||||
Reference in new issue
Block a user