feat: plinegen + SAB parsing for Solid3D/Region/Body

LwPolyline PLINEGEN=0 (DXF default): emit SegmentedLines with NaN
separators between vertex-to-vertex segments; wire_gpu resets
arc-length to 0 after each NaN so linetype pattern restarts per vertex.
PLINEGEN=1: existing Contour path unchanged (continuous pattern).
WireModel gains plinegen field propagated through tessellate pipeline.

Solid3D/Region/Body: call SabReader::read() when parse_sat() returns
None due to binary SAB format (DWG files / newer AutoCAD exports).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Hakan Seven 2026-05-06 06:55:28 +03:00
commit 27aca41dd1
39 changed files with 168 additions and 15 deletions

View file

@ -23,6 +23,8 @@ pub enum TruckObject {
Text(Vec<TextStroke>),
/// Pre-computed NaN-separated 3-D point list (leader lines, arrowheads, etc.).
Lines(Vec<[f32; 3]>),
/// Like Lines but linetype pattern restarts at each NaN-separated segment (plinegen=false).
SegmentedLines(Vec<[f32; 3]>),
Volume(Solid),
}

View file

@ -202,6 +202,7 @@ pub fn apply_along(
aci: 0,
key_vertices: vec![],
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
})
.collect()
}

View file

@ -3211,6 +3211,7 @@ fn tessellate_entity(
tangent_geoms: vec![],
key_vertices: vec![],
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
});
return wires;
}

View file

@ -148,7 +148,12 @@ impl WireGpu {
let p = wire.points[i - 1];
let q = wire.points[i];
if !p[0].is_finite() || !q[0].is_finite() {
dists[i] = dists[i - 1];
// plinegen=false: reset to 0 at the first real point after a NaN separator.
dists[i] = if !wire.plinegen && !p[0].is_finite() && q[0].is_finite() {
0.0
} else {
dists[i - 1]
};
} else {
let dx = q[0] - p[0];
let dy = q[1] - p[1];
@ -234,10 +239,13 @@ impl WireGpu {
for i in 1..n {
let p = wire.points[i - 1];
let q = wire.points[i];
// If either point is non-finite (NaN sentinel or ±inf from overflow),
// keep the same distance — the segment will be skipped anyway.
if !p[0].is_finite() || !q[0].is_finite() {
dists[i] = dists[i - 1];
// plinegen=false: reset to 0 at the first real point after a NaN separator.
dists[i] = if !wire.plinegen && !p[0].is_finite() && q[0].is_finite() {
0.0
} else {
dists[i - 1]
};
} else {
let dx = q[0] - p[0];
let dy = q[1] - p[1];

View file

@ -14,8 +14,8 @@ use std::collections::HashSet;
use std::f64::consts::TAU;
use acadrust::entities::acis::{
SatCoedge, SatConeSurface, SatDocument, SatEdge, SatFace, SatLoop, SatPlaneSurface, SatPoint,
SatPointer, SatSphereSurface, SatTorusSurface, SatVertex,
SabReader, SatCoedge, SatConeSurface, SatDocument, SatEdge, SatFace, SatLoop,
SatPlaneSurface, SatPoint, SatPointer, SatSphereSurface, SatTorusSurface, SatVertex,
};
use acadrust::entities::acis::types::Sense;
use acadrust::entities::{Body, Region, Solid3D};
@ -67,16 +67,26 @@ fn tessellate_sat(sat: &SatDocument, name: String, color: [f32; 4]) -> Option<Me
Some(MeshModel { name, verts, normals, indices, color, selected: false })
}
fn parse_acis(sat_fn: impl FnOnce() -> Option<SatDocument>, is_binary: bool, sab_data: &[u8]) -> Option<SatDocument> {
if let Some(doc) = sat_fn() {
return Some(doc);
}
if is_binary && !sab_data.is_empty() {
return SabReader::read(sab_data).ok();
}
None
}
/// Tessellate a `Region` entity (2D planar ACIS body) into a `MeshModel`.
pub fn tessellate_region(region: &Region, color: [f32; 4]) -> Option<MeshModel> {
let sat = region.parse_sat()?;
let sat = parse_acis(|| region.parse_sat(), region.acis_data.is_binary, &region.acis_data.sab_data)?;
let name = region.common.handle.value().to_string();
tessellate_sat(&sat, name, color)
}
/// Tessellate a `Body` entity (3D ACIS body) into a `MeshModel`.
pub fn tessellate_body(body: &Body, color: [f32; 4]) -> Option<MeshModel> {
let sat = body.parse_sat()?;
let sat = parse_acis(|| body.parse_sat(), body.acis_data.is_binary, &body.acis_data.sab_data)?;
let name = body.common.handle.value().to_string();
tessellate_sat(&sat, name, color)
}
@ -86,7 +96,7 @@ pub fn tessellate_body(body: &Body, color: [f32; 4]) -> Option<MeshModel> {
/// Returns `None` when the entity has no parseable SAT data or produces no
/// triangles (e.g. the solid uses only unsupported surface types).
pub fn tessellate_solid3d(solid: &Solid3D, color: [f32; 4]) -> Option<MeshModel> {
let sat = solid.parse_sat()?;
let sat = parse_acis(|| solid.parse_sat(), solid.acis_data.is_binary, &solid.acis_data.sab_data)?;
let name = solid.common.handle.value().to_string();
tessellate_sat(&sat, name, color)
}

View file

@ -104,6 +104,7 @@ pub fn tessellate(
aci: 0,
key_vertices: te.key_vertices,
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
};
}
@ -132,6 +133,7 @@ pub fn tessellate(
aci: 0,
key_vertices: te.key_vertices,
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
};
}
_ => {}
@ -158,6 +160,7 @@ pub fn tessellate(
aci: 0,
key_vertices,
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
};
}
}
@ -182,6 +185,7 @@ pub fn tessellate(
aci: 0,
key_vertices,
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
};
}
}
@ -212,6 +216,34 @@ pub fn tessellate(
aci: 0,
key_vertices,
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
};
}
TruckObject::SegmentedLines(points) => {
let [ox, oy, oz] = world_offset;
let local_pts: Vec<[f32; 3]> = points.into_iter().map(|[x, y, z]| {
if x.is_nan() { [x, y, z] }
else { [x - ox as f32, y - oy as f32, z - oz as f32] }
}).collect();
let snap_pts = offset_snap_pts(te.snap_pts, world_offset);
let key_vertices: Vec<[f32; 3]> = te.key_vertices.into_iter()
.map(|[x, y, z]| [x - ox as f32, y - oy as f32, z - oz as f32])
.collect();
return WireModel {
name,
points: local_pts,
color,
selected,
pattern_length,
pattern,
line_weight_px,
snap_pts,
tangent_geoms: te.tangent_geoms,
aci: 0,
key_vertices,
plinegen: false,
aabb: WireModel::UNBOUNDED_AABB,
};
}
@ -241,6 +273,7 @@ pub fn tessellate(
tangent_geoms,
key_vertices,
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
}
}
@ -296,6 +329,7 @@ pub fn tessellate_dimension(
tangent_geoms: vec![],
key_vertices,
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
}];
if let Some(text) = dimension_text_entity(dim) {
@ -344,6 +378,7 @@ fn tessellate_leader(
aci: 0,
key_vertices: vec![],
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
}];
}
@ -412,6 +447,7 @@ fn tessellate_leader(
tangent_geoms: vec![],
key_vertices,
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
}]
}
@ -521,6 +557,7 @@ fn tessellate_multileader(
aci: 0,
key_vertices: key_verts,
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
}];
// Render text content as MText wire

View file

@ -60,6 +60,9 @@ pub struct WireModel {
/// Preview / interim wires use `UNBOUNDED_AABB` so they are never pre-rejected
/// by the snap world-space filter.
pub aabb: [f32; 4],
/// When false the linetype pattern restarts at each NaN-separated segment
/// (DXF PLINEGEN=0). When true the pattern runs continuously (PLINEGEN=1).
pub plinegen: bool,
}
impl WireModel {
@ -85,6 +88,7 @@ impl WireModel {
tangent_geoms: vec![],
key_vertices: vec![],
aabb: Self::UNBOUNDED_AABB,
plinegen: true,
}
}
@ -184,6 +188,7 @@ impl Default for WireModel {
tangent_geoms: Vec::new(),
key_vertices: Vec::new(),
aabb: Self::UNBOUNDED_AABB,
plinegen: true,
}
}
}