From 2d795e64ede1e2a82db9c94f005b4f0bf80b5ab9 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Wed, 20 May 2026 04:44:07 +0300 Subject: [PATCH] feat(scene): honour render-affecting HeaderVariables (PDMODE/ATTMODE/FILLMODE/LWDISPLAY/MIRRTEXT) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Header system variables that change how the file renders were being ignored. Wire them through the render path: - PDMODE + PDSIZE: Point entities draw the requested glyph shape (dot / + / × / | with optional enclosing circle and square) sized by PDSIZE. Default PDMODE 0 keeps the single-vertex fast path. - ATTMODE: INSERT attribute rendering now respects 0=Off (no attribs), 1=Normal (per-attrib invisible flag), 2=On (force all visible). - FILLMODE: when false, hatch / wipeout / face3d-fill uploads are short-circuited so the renderer draws wireframe only. - LWDISPLAY: when false every entity falls back to the 1-pixel base width, matching AutoCAD's "Show Lineweight" toggle. - MIRRTEXT: when false, MIRROR keeps text / mtext / shape rotation + oblique angle (so text stays right-reading) while still mirroring position. DISPSILH / PLINEGEN were also reviewed: render path already honours the per-polyline plinegen bit and we don't yet render Solid3D silhouettes, so the header values are read for round-trip but produce no extra effect here. Co-Authored-By: Claude Opus 4.7 --- src/entities/point.rs | 117 +++++++++++++++++++++++++++++++++++++++--- src/scene/mod.rs | 60 +++++++++++++++++++++- src/scene/render.rs | 57 ++++++++++++++------ 3 files changed, 210 insertions(+), 24 deletions(-) diff --git a/src/entities/point.rs b/src/entities/point.rs index bee80fcc4..71116c4b3 100644 --- a/src/entities/point.rs +++ b/src/entities/point.rs @@ -9,19 +9,124 @@ use crate::scene::acad_to_truck::{TruckEntity, TruckObject}; use crate::scene::object::{GripApply, GripDef, PropSection}; use crate::scene::wire_model::SnapHint; -fn to_truck(pt: &Point) -> TruckEntity { +/// Resolve PDSIZE (negative = % of viewport, 0 = 5% default, positive = world). +/// We don't know the viewport height at tessellation time, so percentages and +/// the 0 default expand to a small fixed world size as a best-effort fallback. +fn pdsize_world(pdsize: f64) -> f64 { + if pdsize > 0.0 { + pdsize + } else { + // Both PDSIZE = 0 and negative (relative) fall back to a sensible + // visible default until viewport-aware sizing is wired up. + 2.0 + } +} + +fn point_glyph(cx: f64, cy: f64, z: f64, pdmode: i16, pdsize: f64) -> Vec<[f64; 3]> { + // PDMODE bits: + // shape: 0=dot, 1=nothing, 2='+', 3='×', 4='|' + // +32 = enclose in a circle + // +64 = enclose in a square + // (+96 = both) + let shape = (pdmode & 0x0F) as i32; + let circle = (pdmode & 32) != 0; + let square = (pdmode & 64) != 0; + let s = pdsize_world(pdsize) * 0.5; + let nan = [f64::NAN, f64::NAN, f64::NAN]; + let mut pts: Vec<[f64; 3]> = Vec::new(); + let mut push_seg = |a: [f64; 3], b: [f64; 3]| { + if !pts.is_empty() { + pts.push(nan); + } + pts.push(a); + pts.push(b); + }; + match shape { + // 0 = single dot — emit a tiny "+" so it's visible at any zoom. + 0 => { + let d = s * 0.05; + push_seg([cx - d, cy, z], [cx + d, cy, z]); + push_seg([cx, cy - d, z], [cx, cy + d, z]); + } + 1 => {} // explicit nothing + 2 => { + push_seg([cx - s, cy, z], [cx + s, cy, z]); + push_seg([cx, cy - s, z], [cx, cy + s, z]); + } + 3 => { + push_seg([cx - s, cy - s, z], [cx + s, cy + s, z]); + push_seg([cx - s, cy + s, z], [cx + s, cy - s, z]); + } + 4 => { + push_seg([cx, cy - s, z], [cx, cy + s, z]); + } + _ => { + push_seg([cx - s, cy, z], [cx + s, cy, z]); + push_seg([cx, cy - s, z], [cx, cy + s, z]); + } + } + if circle { + // 16-segment polyline circle. + const N: usize = 16; + let mut ring: Vec<[f64; 3]> = Vec::with_capacity(N + 1); + for i in 0..=N { + let a = i as f64 * std::f64::consts::TAU / N as f64; + ring.push([cx + a.cos() * s, cy + a.sin() * s, z]); + } + if !pts.is_empty() { + pts.push(nan); + } + pts.extend(ring); + } + if square { + let p1 = [cx - s, cy - s, z]; + let p2 = [cx + s, cy - s, z]; + let p3 = [cx + s, cy + s, z]; + let p4 = [cx - s, cy + s, z]; + if !pts.is_empty() { + pts.push(nan); + } + pts.extend_from_slice(&[p1, p2, p3, p4, p1]); + } + pts +} + +fn to_truck(pt: &Point, document: &acadrust::CadDocument) -> TruckEntity { let normal = (pt.normal.x, pt.normal.y, pt.normal.z); let (wx, wy, wz) = crate::scene::transform::ocs_point_to_wcs( (pt.location.x, pt.location.y, pt.location.z), normal, ); - let p = Point3::new(wx, wy, wz); let snap = Vec3::new(wx as f32, wy as f32, wz as f32); + let pdmode = document.header.point_display_mode; + let pdsize = document.header.point_display_size; + if pdmode == 0 { + // Default: a single vertex (driver handles the dot pixel). + let p = Point3::new(wx, wy, wz); + return TruckEntity { + object: TruckObject::Point(builder::vertex(p)), + snap_pts: vec![(snap, SnapHint::Node)], + tangent_geoms: vec![], + key_vertices: vec![], + fill_tris: vec![], + }; + } + let pts = point_glyph(wx, wy, wz, pdmode, pdsize); + if pts.is_empty() { + // PDMODE 1 = nothing — emit an empty Lines wire so picking still works. + return TruckEntity { + object: TruckObject::Lines(vec![]), + snap_pts: vec![(snap, SnapHint::Node)], + tangent_geoms: vec![], + key_vertices: vec![[wx, wy, wz]], + fill_tris: vec![], + }; + } TruckEntity { - object: TruckObject::Point(builder::vertex(p)), + object: TruckObject::Lines(pts), snap_pts: vec![(snap, SnapHint::Node)], tangent_geoms: vec![], - key_vertices: vec![], + key_vertices: vec![[wx, wy, wz]], fill_tris: vec![], } } @@ -85,8 +190,8 @@ fn apply_transform(pt: &mut Point, t: &EntityTransform) { } impl TruckConvertible for Point { - fn to_truck(&self, _document: &acadrust::CadDocument) -> Option { - Some(to_truck(self)) + fn to_truck(&self, document: &acadrust::CadDocument) -> Option { + Some(to_truck(self, document)) } } diff --git a/src/scene/mod.rs b/src/scene/mod.rs index 617f62138..a44b8923a 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -3505,6 +3505,34 @@ impl Scene { } else { [0.0; 3] }; + // MIRRTEXT (header.mirror_text): when false AutoCAD positions text / + // mtext / shape by the mirror but keeps the original rotation + + // oblique so the text stays right-reading. Capture before the + // transform and re-apply afterwards. + let preserve_text_orientation = + matches!(t, EntityTransform::Mirror { .. }) && !self.document.header.mirror_text; + let mut text_orient_backup: Vec<(Handle, f64, f64, f64)> = Vec::new(); + if preserve_text_orientation { + for &h in handles { + if let Some(entity) = self.document.get_entity(h) { + match entity { + EntityType::Text(t) => { + text_orient_backup.push((h, t.rotation, t.oblique_angle, 0.0)) + } + EntityType::MText(m) => { + text_orient_backup.push((h, m.rotation, 0.0, 0.0)) + } + EntityType::Shape(s) => text_orient_backup.push(( + h, + s.rotation, + s.oblique_angle, + s.relative_x_scale, + )), + _ => {} + } + } + } + } for &h in handles { if let Some(entity) = self.document.get_entity_mut(h) { dispatch::apply_transform(entity, t); @@ -3521,6 +3549,27 @@ impl Scene { } } } + if preserve_text_orientation { + for (h, rot, oblique, x_scale) in text_orient_backup { + if let Some(entity) = self.document.get_entity_mut(h) { + match entity { + EntityType::Text(t) => { + t.rotation = rot; + t.oblique_angle = oblique; + } + EntityType::MText(m) => { + m.rotation = rot; + } + EntityType::Shape(s) => { + s.rotation = rot; + s.oblique_angle = oblique; + s.relative_x_scale = x_scale; + } + _ => {} + } + } + } + } self.bump_geometry(); } @@ -5150,8 +5199,17 @@ fn append_insert_attribute_wires( if ins.attributes.is_empty() { return; } + // ATTMODE (header.attribute_visibility): + // 0 = Off — every attribute hidden + // 1 = Normal — honour per-attribute `invisible` flag (default) + // 2 = On — every attribute forced visible, ignoring its flag + let attmode = document.header.attribute_visibility; + if attmode == 0 { + return; + } for attr in &ins.attributes { - if attr.common.invisible || attr.flags.invisible { + let per_attr_hidden = attr.common.invisible || attr.flags.invisible; + if attmode == 1 && per_attr_hidden { continue; } let attr_entity = EntityType::AttributeEntity(attr.clone()); diff --git a/src/scene/render.rs b/src/scene/render.rs index b4801e964..2673fe793 100644 --- a/src/scene/render.rs +++ b/src/scene/render.rs @@ -95,6 +95,9 @@ pub struct Primitive { /// Background color used to clear the MSAA buffer at the start of each frame. pub(super) bg_color: [f32; 4], pub(super) show_viewcube: bool, + /// Header.fill_mode (FILLMODE): when false, hatch / wipeout / face3d-fill + /// uploads short-circuit so the renderer draws only wireframe. + pub(super) fill_mode: bool, pub(super) geometry_epoch: u64, /// Camera generation captured when this Primitive was assembled. Paired /// with `geometry_epoch` so the wire buffers re-upload when the view @@ -128,19 +131,30 @@ impl shader::Primitive for Primitive { pipeline.viewcube.ensure_depth_texture(device, full_size); pipeline.upload_uniforms(queue, &self.uniforms); let cur_key = (self.geometry_epoch, self.camera_generation); + let fill_mode = self.fill_mode; if cur_key != pipeline.cached_epoch { // Static buffers (hatches/images/meshes) only need refresh on a // real geometry change, not on every camera tick. if self.geometry_epoch != pipeline.cached_epoch.0 { - pipeline.upload_hatches(device, &self.hatches[..]); - pipeline.upload_wipeouts(device, &self.wipeout_hatches[..]); + if fill_mode { + pipeline.upload_hatches(device, &self.hatches[..]); + pipeline.upload_wipeouts(device, &self.wipeout_hatches[..]); + } else { + pipeline.upload_hatches(device, &[]); + pipeline.upload_wipeouts(device, &[]); + } pipeline.upload_images(device, queue, &self.images[..]); pipeline.upload_meshes(device, &self.meshes[..]); } // Wires re-upload on every camera change because the visible // subset shifts under frustum culling. pipeline.upload_wires(device, &self.wires[..]); - pipeline.upload_face3d(device, &self.face3d_wires[..], &self.wires[..]); + if fill_mode { + pipeline.upload_face3d(device, &self.face3d_wires[..], &self.wires[..]); + } else { + // Edges still need to draw, but no fill_tris are forwarded. + pipeline.upload_face3d(device, &self.face3d_wires[..], &[]); + } pipeline.cached_epoch = cur_key; } pipeline.compute_wire_scissors(self.uniforms.view_proj, clip_size.width, clip_size.height); @@ -239,20 +253,27 @@ pub(super) fn render_style_for( let (pattern_length, pattern) = resolve_pattern(&document.line_types, lt_name, lt_scale); let line_weight_px = { - let ew = &e.common().line_weight; - let resolved = match ew { - LineWeight::ByLayer | LineWeight::ByBlock | LineWeight::Default => document - .layers - .get(layer_name) - .map(|l| &l.line_weight) - .unwrap_or(&LineWeight::Default), - _ => ew, - }; - const MM_TO_PX: f32 = 96.0 / 25.4; - resolved - .millimeters() - .map(|mm| (mm as f32 * MM_TO_PX).max(1.0)) - .unwrap_or(1.0) + // LWDISPLAY (header.lineweight_display): when false, every entity + // draws at the 1-pixel base width regardless of its assigned weight. + // AutoCAD's "Show Lineweight" toggle maps onto this header var. + if !document.header.lineweight_display { + 1.0 + } else { + let ew = &e.common().line_weight; + let resolved = match ew { + LineWeight::ByLayer | LineWeight::ByBlock | LineWeight::Default => document + .layers + .get(layer_name) + .map(|l| &l.line_weight) + .unwrap_or(&LineWeight::Default), + _ => ew, + }; + const MM_TO_PX: f32 = 96.0 / 25.4; + resolved + .millimeters() + .map(|mm| (mm as f32 * MM_TO_PX).max(1.0)) + .unwrap_or(1.0) + } }; (entity_color, pattern_length, pattern, line_weight_px, aci) @@ -360,6 +381,7 @@ impl Scene { hover_region, bg_color, show_viewcube, + fill_mode: self.document.header.fill_mode, geometry_epoch: self.geometry_epoch, camera_generation: self.camera_generation, } @@ -403,6 +425,7 @@ impl Scene { hover_region, bg_color: self.bg_color, show_viewcube: false, + fill_mode: self.document.header.fill_mode, geometry_epoch: self.geometry_epoch, camera_generation: self.camera_generation, }