feat(scene): honour render-affecting HeaderVariables (PDMODE/ATTMODE/FILLMODE/LWDISPLAY/MIRRTEXT)

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 <noreply@anthropic.com>
This commit is contained in:
Hakan Seven 2026-05-20 04:44:07 +03:00
commit 2d795e64ed
3 changed files with 210 additions and 24 deletions

View file

@ -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<TruckEntity> {
Some(to_truck(self))
fn to_truck(&self, document: &acadrust::CadDocument) -> Option<TruckEntity> {
Some(to_truck(self, document))
}
}

View file

@ -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());

View file

@ -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,
}