Merge pull request #1512 from ramox81/codex/frame-parity

Frames: follow the drawing state, DWF/DGN frames, image frame profile, frame-only picking

The codec [patch] to ramox81/cadcodec@ea5edaa is dropped: that commit landed as opencadcodec#63 and main already pins ad16215, which contains it.
This commit is contained in:
Hakan Seven committed 2026-09-28 13:52:06 +03:00
commit 7694c506aa
70 files changed
+445 -64

No files matched your search

+2
View File
@@ -1750,6 +1750,7 @@ impl Batches {
world_width: b.world_width,
depth_override: b.local_depth,
display_visible: !b.hide_unselected || selected,
snap_only: false,
plot_visible: b.plot_visible,
fill_is_3d: b.fill_is_3d,
fill_is_2d_solid: b.fill_is_2d_solid,
@@ -2516,6 +2517,7 @@ fn emit_wire(
world_width: 0.0,
depth_override: local_depth,
display_visible: !lw.hide_unselected || ctx.selected,
snap_only: false,
plot_visible: lw.plot_visible,
fill_is_3d: false,
fill_is_2d_solid: false,
+6
View File
@@ -931,6 +931,7 @@ fn tessellate_entity_inner(
world_width: 0.0,
depth_override: None,
display_visible: true,
snap_only: false,
plot_visible: true,
fill_is_3d: false,
fill_is_2d_solid: false,
@@ -1176,6 +1177,7 @@ fn tessellate_entity_inner(
world_width: 0.0,
depth_override: None,
display_visible: true,
snap_only: false,
plot_visible: true,
fill_is_3d: false,
fill_is_2d_solid: false,
@@ -1296,6 +1298,8 @@ fn tessellate_entity_inner(
wire.points_low = points_low;
wire.display_visible = false;
wire.plot_visible = false;
// Snaps find it; a click on the page does not select the underlay.
wire.snap_only = true;
if let Some((center, radius)) = piece.circle {
wire.tangent_geoms = vec![crate::scene::model::wire_model::TangentGeom::PlanarCircle {
center,
@@ -1549,6 +1553,7 @@ fn lod_stub_wire(
world_width: 0.0,
depth_override: None,
display_visible: true,
snap_only: false,
plot_visible: true,
fill_is_3d: false,
fill_is_2d_solid: false,
@@ -1645,6 +1650,7 @@ fn lod_stub_wire_3d(
world_width: 0.0,
depth_override: None,
display_visible: true,
snap_only: false,
plot_visible: true,
fill_is_3d: false,
fill_is_2d_solid: false,
+21
View File
@@ -373,6 +373,7 @@ fn split_mixed_polyline(
world_width,
depth_override: None,
display_visible: true,
snap_only: false,
plot_visible: true,
fill_is_3d: false,
fill_is_2d_solid: false,
@@ -508,6 +509,7 @@ fn split_mixed_polyline(
world_width: line_world_width,
depth_override: None,
display_visible: true,
snap_only: false,
plot_visible: true,
fill_is_3d: false,
fill_is_2d_solid: false,
@@ -571,6 +573,7 @@ fn point_cloud_wires(
world_width: 0.0,
depth_override: None,
display_visible: true,
snap_only: false,
plot_visible: true,
fill_is_3d: false,
fill_is_2d_solid: false,
@@ -826,6 +829,7 @@ pub fn tessellate(
world_width: 0.0,
depth_override: None,
display_visible: true,
snap_only: false,
plot_visible: true,
fill_is_3d: false,
fill_is_2d_solid: true,
@@ -990,6 +994,7 @@ pub fn tessellate(
world_width: 0.0,
depth_override: None,
display_visible: true,
snap_only: false,
plot_visible: true,
fill_is_3d: false,
fill_is_2d_solid: false,
@@ -1382,6 +1387,7 @@ pub fn tessellate(
world_width: 0.0,
depth_override: None,
display_visible: true,
snap_only: false,
plot_visible: true,
fill_is_3d: false,
fill_is_2d_solid: false,
@@ -1439,6 +1445,7 @@ pub fn tessellate(
world_width: 0.0,
depth_override: None,
display_visible: true,
snap_only: false,
plot_visible: true,
fill_is_3d: false,
fill_is_2d_solid: false,
@@ -1493,6 +1500,7 @@ pub fn tessellate(
world_width: 0.0,
depth_override: None,
display_visible: true,
snap_only: false,
plot_visible: true,
fill_is_3d: false,
fill_is_2d_solid: false,
@@ -1528,6 +1536,7 @@ pub fn tessellate(
world_width: 0.0,
depth_override: None,
display_visible: true,
snap_only: false,
plot_visible: true,
fill_is_3d: false,
fill_is_2d_solid: false,
@@ -1593,6 +1602,7 @@ pub fn tessellate(
world_width: 0.0,
depth_override: None,
display_visible: true,
snap_only: false,
plot_visible: true,
fill_is_3d: false,
fill_is_2d_solid: false,
@@ -1640,6 +1650,7 @@ pub fn tessellate(
world_width: 0.0,
depth_override: None,
display_visible: true,
snap_only: false,
plot_visible: true,
fill_is_3d: false,
fill_is_2d_solid: false,
@@ -1684,6 +1695,7 @@ pub fn tessellate(
world_width: 0.0,
depth_override: None,
display_visible: true,
snap_only: false,
plot_visible: true,
fill_is_3d: false,
fill_is_2d_solid: false,
@@ -1721,6 +1733,7 @@ pub fn tessellate(
world_width: 0.0,
depth_override: None,
display_visible: true,
snap_only: false,
plot_visible: true,
fill_is_3d: false,
fill_is_2d_solid: false,
@@ -1792,6 +1805,7 @@ pub fn tessellate(
world_width: 0.0,
depth_override: None,
display_visible: true,
snap_only: false,
plot_visible: true,
fill_is_3d: false,
fill_is_2d_solid: false,
@@ -1951,6 +1965,7 @@ pub fn tessellate(
world_width: polyline_band_width(entity, document.header.fill_mode),
depth_override: None,
display_visible: true,
snap_only: false,
plot_visible: true,
fill_is_3d,
fill_is_2d_solid: false,
@@ -2022,6 +2037,7 @@ pub fn tessellate(
render_instance: None,
depth_override: None,
display_visible: true,
snap_only: false,
plot_visible: true,
});
}
@@ -2035,6 +2051,7 @@ pub fn tessellate(
world_width: 0.0,
depth_override: None,
display_visible: true,
snap_only: false,
plot_visible: true,
fill_is_3d: false,
fill_is_2d_solid: false,
@@ -2094,6 +2111,7 @@ pub fn tessellate(
world_width: 0.0,
depth_override: None,
display_visible: true,
snap_only: false,
plot_visible: true,
fill_is_3d: false,
fill_is_2d_solid: false,
@@ -2183,6 +2201,7 @@ pub fn tessellate(
world_width: polyline_band_width(entity, document.header.fill_mode),
depth_override: None,
display_visible: true,
snap_only: false,
plot_visible: true,
fill_is_3d: false,
fill_is_2d_solid: false,
@@ -2261,6 +2280,7 @@ pub fn tessellate(
world_width,
depth_override: None,
display_visible: true,
snap_only: false,
plot_visible: true,
fill_is_3d: false,
fill_is_2d_solid: false,
@@ -2370,6 +2390,7 @@ pub fn tessellate(
world_width: 0.0,
depth_override: None,
display_visible: true,
snap_only: false,
plot_visible: true,
fill_is_3d: false,
fill_is_2d_solid: false,
+57 -13
View File
@@ -5,19 +5,52 @@ use codec::{CadDocument, EntityType, Handle};
pub(crate) enum FrameKind {
Image,
Pdf,
Dwf,
Dgn,
Wipeout,
Xclip,
PointCloudClip,
}
pub(crate) const ALL_KINDS: [FrameKind; 5] = [
pub(crate) const ALL_KINDS: [FrameKind; 7] = [
FrameKind::Image,
FrameKind::Pdf,
FrameKind::Dwf,
FrameKind::Dgn,
FrameKind::Wipeout,
FrameKind::Xclip,
FrameKind::PointCloudClip,
];
/// IMAGEFRAME of the user profile: what a drawing without its own image
/// variables shows, and what a new drawing starts with. Setting IMAGEFRAME
/// updates it, so it carries over to new drawings and later sessions.
static PROFILE_IMAGE_FRAME: std::sync::atomic::AtomicI16 = std::sync::atomic::AtomicI16::new(1);
pub(crate) fn profile_image_mode() -> i16 {
PROFILE_IMAGE_FRAME.load(std::sync::atomic::Ordering::Relaxed)
}
pub(crate) fn set_profile_image_mode(value: i16) {
PROFILE_IMAGE_FRAME.store(value.clamp(0, 2), std::sync::atomic::Ordering::Relaxed);
}
/// The image variables store the frame as bits: 1 shown, 2 not plotted, so
/// "shown but not plotted" is 3. Any other value is not one the variable
/// can hold, and the profile setting applies.
fn image_mode_from_flags(flags: i16) -> Option<i16> {
match flags {
0 => Some(0),
1 => Some(1),
3 => Some(2),
_ => None,
}
}
fn image_flags_from_mode(mode: i16) -> i16 {
if mode == 2 { 3 } else { mode }
}
fn root_entry(document: &CadDocument, name: &str) -> Option<Handle> {
let from_dictionary = |handle| match document.objects.get(&handle) {
Some(ObjectType::Dictionary(dictionary)) => dictionary
@@ -58,12 +91,15 @@ pub(crate) fn mode(document: &CadDocument, kind: FrameKind) -> i16 {
match kind {
FrameKind::Image => raster_variables_handle(document)
.and_then(|handle| match document.objects.get(&handle) {
Some(ObjectType::RasterVariables(value)) => Some(value.display_image_frame),
Some(ObjectType::RasterVariables(value)) => {
image_mode_from_flags(value.display_image_frame)
}
_ => None,
})
.unwrap_or(1)
.clamp(0, 2),
.unwrap_or_else(profile_image_mode),
FrameKind::Pdf => drawing_mode(document, "PDFFRAME", 1),
FrameKind::Dwf => document.header.dwf_frame.clamp(0, 2),
FrameKind::Dgn => document.header.dgn_frame.clamp(0, 2),
FrameKind::Wipeout => crate::io::drawing_variable(document, "WIPEOUTFRAME")
.and_then(|value| value.parse::<i16>().ok())
.or_else(|| {
@@ -115,8 +151,9 @@ fn set_image_mode(document: &mut CadDocument, value: i16) {
});
if let Some(ObjectType::RasterVariables(variables)) = document.objects.get_mut(&handle) {
variables.owner = owner;
variables.display_image_frame = value;
variables.display_image_frame = image_flags_from_mode(value);
}
set_profile_image_mode(value);
attach_root_entry(document, "ACAD_IMAGE_VARS", handle);
}
@@ -145,6 +182,8 @@ pub(crate) fn set_mode(document: &mut CadDocument, kind: FrameKind, value: i16)
match kind {
FrameKind::Image => set_image_mode(document, value),
FrameKind::Pdf => crate::io::set_drawing_variable(document, "PDFFRAME", &value.to_string()),
FrameKind::Dwf => document.header.dwf_frame = value,
FrameKind::Dgn => document.header.dgn_frame = value,
FrameKind::Wipeout => set_wipeout_mode(document, value),
FrameKind::Xclip => document.header.xclip_frame = value,
FrameKind::PointCloudClip => crate::io::set_drawing_variable(
@@ -168,6 +207,8 @@ pub(crate) fn kind_for_name(name: &str) -> Option<FrameKind> {
match name {
"IMAGEFRAME" => Some(FrameKind::Image),
"PDFFRAME" => Some(FrameKind::Pdf),
"DWFFRAME" => Some(FrameKind::Dwf),
"DGNFRAME" => Some(FrameKind::Dgn),
"WIPEOUTFRAME" => Some(FrameKind::Wipeout),
"XCLIPFRAME" => Some(FrameKind::Xclip),
"POINTCLOUDCLIPFRAME" => Some(FrameKind::PointCloudClip),
@@ -178,23 +219,26 @@ pub(crate) fn kind_for_name(name: &str) -> Option<FrameKind> {
pub(crate) fn entity_kind(entity: &EntityType) -> Option<FrameKind> {
match entity {
EntityType::RasterImage(_) => Some(FrameKind::Image),
EntityType::Underlay(underlay)
if matches!(underlay.underlay_type, codec::entities::UnderlayType::Pdf) =>
{
Some(FrameKind::Pdf)
}
EntityType::Underlay(underlay) => Some(underlay_kind(underlay)),
EntityType::Wipeout(_) => Some(FrameKind::Wipeout),
_ => None,
}
}
fn underlay_kind(underlay: &codec::entities::Underlay) -> FrameKind {
match underlay.underlay_type {
codec::entities::UnderlayType::Pdf => FrameKind::Pdf,
codec::entities::UnderlayType::Dwf => FrameKind::Dwf,
codec::entities::UnderlayType::Dgn => FrameKind::Dgn,
}
}
pub(crate) fn affected(entity: &EntityType, kind: FrameKind) -> bool {
match kind {
FrameKind::Image => matches!(entity, EntityType::RasterImage(_)),
FrameKind::Pdf => matches!(
FrameKind::Pdf | FrameKind::Dwf | FrameKind::Dgn => matches!(
entity,
EntityType::Underlay(underlay)
if matches!(underlay.underlay_type, codec::entities::UnderlayType::Pdf)
EntityType::Underlay(underlay) if underlay_kind(underlay) == kind
),
FrameKind::Wipeout => matches!(entity, EntityType::Wipeout(_)),
FrameKind::Xclip => matches!(entity, EntityType::Insert(_)),
+5
View File
@@ -506,6 +506,9 @@ pub struct WireModel {
pub depth_override: Option<f32>,
/// Whether this wire is drawn in the normal viewport pass.
pub display_visible: bool,
/// Geometry that only object snaps use (the content of an underlay):
/// never drawn, plotted or picked.
pub snap_only: bool,
/// Whether this wire is included in plotted output.
pub plot_visible: bool,
/// `true` when [`fill_tris`] is a real 3-D surface (PolyfaceMesh /
@@ -580,6 +583,7 @@ impl WireModel {
world_width: 0.0,
depth_override: None,
display_visible: true,
snap_only: false,
plot_visible: true,
fill_is_3d: false,
fill_is_2d_solid: false,
@@ -1009,6 +1013,7 @@ impl Default for WireModel {
fill_tris_low: Vec::new(),
depth_override: None,
display_visible: true,
snap_only: false,
plot_visible: true,
fill_is_3d: false,
fill_is_2d_solid: false,
+90
View File
@@ -249,6 +249,9 @@ pub fn click_hit<'a, W: WireSource + ?Sized>(
let Some(wire) = wires.source_wire(segment.wire) else {
continue;
};
if wire.snap_only {
continue;
}
if wire.point_marker.is_some() {
continue;
}
@@ -277,6 +280,9 @@ pub fn click_hit<'a, W: WireSource + ?Sized>(
} else {
// Q: lazy projection — no Vec allocation per wire; NaN resets the segment chain.
for wire in wires.iter() {
if wire.snap_only {
continue;
}
if wire.point_marker.is_some() {
continue;
}
@@ -329,6 +335,9 @@ pub fn click_hit<'a, W: WireSource + ?Sized>(
let Some(wire) = wires.source_wire(triangle.wire) else {
continue;
};
if wire.snap_only {
continue;
}
if let Some(depth) = triangle_ref_hit_depth(
cursor,
wire,
@@ -345,6 +354,9 @@ pub fn click_hit<'a, W: WireSource + ?Sized>(
}
} else {
for wire in wires.iter() {
if wire.snap_only {
continue;
}
if wire.fill_tris.is_empty() {
continue;
}
@@ -381,6 +393,9 @@ pub fn click_hit<'a, W: WireSource + ?Sized>(
let Some(wire) = wires.source_wire(triangle.wire) else {
continue;
};
if wire.snap_only {
continue;
}
if let Some(depth) = triangle_ref_hit_depth(
cursor,
wire,
@@ -397,6 +412,9 @@ pub fn click_hit<'a, W: WireSource + ?Sized>(
}
} else {
for wire in wires.iter() {
if wire.snap_only {
continue;
}
if wire.pick_tris.is_empty() {
continue;
}
@@ -438,6 +456,9 @@ pub fn click_hit<'a, W: WireSource + ?Sized>(
let Some(wire) = wires.source_wire(glyph.wire) else {
continue;
};
if wire.snap_only {
continue;
}
let start = glyph.start as usize;
let Some(vertices) = wire.text_verts.get(start..start + 6) else {
continue;
@@ -451,6 +472,9 @@ pub fn click_hit<'a, W: WireSource + ?Sized>(
}
} else {
for wire in wires.iter() {
if wire.snap_only {
continue;
}
if wire.text_verts.is_empty() {
continue;
}
@@ -516,6 +540,9 @@ pub fn click_hits_all<'a, W: WireSource + ?Sized>(
let Some(wire) = wires.source_wire(segment.wire) else {
continue;
};
if wire.snap_only {
continue;
}
if wire.point_marker.is_some() {
continue;
}
@@ -550,6 +577,9 @@ pub fn click_hits_all<'a, W: WireSource + ?Sized>(
}));
} else {
for wire in wires.iter() {
if wire.snap_only {
continue;
}
if wire.point_marker.is_some() {
continue;
}
@@ -593,6 +623,9 @@ pub fn click_hits_all<'a, W: WireSource + ?Sized>(
let Some(wire) = wires.source_wire(triangle.wire) else {
continue;
};
if wire.snap_only {
continue;
}
if hits.iter().any(|&(_, name)| name == wire.name) {
matched.insert(triangle.wire);
continue;
@@ -614,6 +647,9 @@ pub fn click_hits_all<'a, W: WireSource + ?Sized>(
}
} else {
for wire in wires.iter() {
if wire.snap_only {
continue;
}
if wire.fill_tris.is_empty() || hits.iter().any(|&(_, name)| name == wire.name) {
continue;
}
@@ -647,6 +683,9 @@ pub fn click_hits_all<'a, W: WireSource + ?Sized>(
let Some(wire) = wires.source_wire(triangle.wire) else {
continue;
};
if wire.snap_only {
continue;
}
if hits.iter().any(|&(_, name)| name == wire.name) {
matched.insert(triangle.wire);
continue;
@@ -668,6 +707,9 @@ pub fn click_hits_all<'a, W: WireSource + ?Sized>(
}
} else {
for wire in wires.iter() {
if wire.snap_only {
continue;
}
if wire.pick_tris.is_empty() || hits.iter().any(|&(_, name)| name == wire.name) {
continue;
}
@@ -697,6 +739,9 @@ pub fn click_hits_all<'a, W: WireSource + ?Sized>(
let Some(wire) = wires.source_wire(glyph.wire) else {
continue;
};
if wire.snap_only {
continue;
}
if hits.iter().any(|&(_, name)| name == wire.name) {
matched.insert(glyph.wire);
continue;
@@ -712,6 +757,9 @@ pub fn click_hits_all<'a, W: WireSource + ?Sized>(
}
} else {
for wire in wires.iter() {
if wire.snap_only {
continue;
}
if wire.text_verts.is_empty() || hits.iter().any(|&(_, name)| name == wire.name) {
continue;
}
@@ -1252,6 +1300,9 @@ fn indexed_box_crossing_hits<'a, W: WireSource + ?Sized>(
// path below run identical code.
let hit_name = |segment: &SegmentRef| -> Option<&'a str> {
let wire = wires.source_wire(segment.wire)?;
if wire.snap_only {
return None;
}
let start = segment.start as usize;
if start + 1 >= wire.points.len() {
return None;
@@ -1310,6 +1361,9 @@ fn indexed_box_crossing_hits<'a, W: WireSource + ?Sized>(
let Some(wire) = wires.source_wire(triangle.wire) else {
continue;
};
if wire.snap_only {
continue;
}
if already(&wire_hit, triangle.wire) {
continue;
}
@@ -1334,6 +1388,9 @@ fn indexed_box_crossing_hits<'a, W: WireSource + ?Sized>(
let Some(wire) = wires.source_wire(glyph.wire) else {
continue;
};
if wire.snap_only {
continue;
}
if already(&wire_hit, glyph.wire) {
continue;
}
@@ -1355,6 +1412,9 @@ fn indexed_box_crossing_hits<'a, W: WireSource + ?Sized>(
// Degenerate point-only wires have no indexed segment or surface primitive.
for wire in wires.iter() {
if wire.snap_only {
continue;
}
if wire.points.len() >= 2
|| !wire.fill_tris.is_empty()
|| !wire.pick_tris.is_empty()
@@ -1411,6 +1471,9 @@ pub fn poly_fence_hit<'a, W: WireSource + ?Sized>(
let Some(wire) = wires.source_wire(segment.wire) else {
continue;
};
if wire.snap_only {
continue;
}
let start = segment.start as usize;
if start + 1 >= wire.points.len() || seen.contains(wire.name.as_str()) {
continue;
@@ -1442,6 +1505,9 @@ pub fn poly_fence_hit<'a, W: WireSource + ?Sized>(
return out;
}
for wire in wires.iter() {
if wire.snap_only {
continue;
}
if wire.points.len() < 2 {
continue;
}
@@ -1480,6 +1546,9 @@ fn indexed_polygon_crossing_hits<'a, W: WireSource + ?Sized>(
let Some(wire) = wires.source_wire(segment.wire) else {
continue;
};
if wire.snap_only {
continue;
}
let start = segment.start as usize;
if start + 1 >= wire.points.len() {
continue;
@@ -1524,6 +1593,9 @@ fn indexed_polygon_crossing_hits<'a, W: WireSource + ?Sized>(
let Some(wire) = wires.source_wire(triangle.wire) else {
continue;
};
if wire.snap_only {
continue;
}
if seen.contains(wire.name.as_str()) {
continue;
}
@@ -1546,6 +1618,9 @@ fn indexed_polygon_crossing_hits<'a, W: WireSource + ?Sized>(
let Some(wire) = wires.source_wire(glyph.wire) else {
continue;
};
if wire.snap_only {
continue;
}
if seen.contains(wire.name.as_str()) {
continue;
}
@@ -1565,6 +1640,9 @@ fn indexed_polygon_crossing_hits<'a, W: WireSource + ?Sized>(
}
}
for wire in wires.iter() {
if wire.snap_only {
continue;
}
if wire.points.len() >= 2
|| !wire.fill_tris.is_empty()
|| !wire.pick_tris.is_empty()
@@ -1644,6 +1722,9 @@ pub fn box_hit<'a, W: WireSource + ?Sized>(
let mut out = Vec::new();
let mut seen = HashSet::default();
for wire in wires.iter() {
if wire.snap_only {
continue;
}
// Fallback: when wire has no line geometry (e.g. greek text emits
// only fill_tris) treat the AABB rectangle as the hit-test shape
// so low-LOD text stays selectable. See #19.
@@ -1729,6 +1810,9 @@ pub fn box_hit<'a, W: WireSource + ?Sized>(
let mut seen = HashSet::default();
for wire in wires.iter() {
if wire.snap_only {
continue;
}
let name = wire.name.as_str();
if disqualified.contains(name) {
continue;
@@ -1838,6 +1922,9 @@ pub fn poly_hit<'a, W: WireSource + ?Sized>(
let mut out = Vec::new();
let mut seen = HashSet::default();
for wire in wires.iter() {
if wire.snap_only {
continue;
}
// Same AABB fallback as `box_hit`: when a wire has no line
// geometry (e.g. greek-LOD text emits only fill_tris) treat the
// AABB rectangle as the hit-test shape so low-LOD text stays
@@ -1925,6 +2012,9 @@ pub fn poly_hit<'a, W: WireSource + ?Sized>(
let mut seen = HashSet::default();
for wire in wires.iter() {
if wire.snap_only {
continue;
}
let aabb_pts: Vec<[f32; 3]>;
let empty_pts: [[f32; 3]; 0] = [];
let pts: &[[f32; 3]] = if !wire.points.is_empty() {
+1
View File
@@ -551,6 +551,7 @@ pub fn frame_wire(
world_width: 0.0,
depth_override: None,
display_visible: true,
snap_only: false,
plot_visible: true,
fill_is_3d: false,
fill_is_2d_solid: false,
+2
View File
@@ -316,6 +316,7 @@ pub fn apply_along(
world_width: 0.0,
depth_override: None,
display_visible: true,
snap_only: false,
plot_visible: true,
fill_is_3d: false,
fill_is_2d_solid: false,
@@ -365,6 +366,7 @@ pub fn apply_along(
world_width: 0.0,
depth_override: None,
display_visible: true,
snap_only: false,
plot_visible: true,
fill_is_3d: false,
fill_is_2d_solid: false,