fix(thickness): edit persists + wide polyline extrudes as a solid tube
Entering a thickness (DXF 39 extrusion) on a wide polyline now works end to end: - The properties Thickness field is a General-group common prop, so it is routed to apply_common_prop (set_entity_thickness) instead of the geometry setter, which ignored it — the value was silently dropped and reverted to 0. - A wide polyline extrudes its whole band into a 3-D tube (outer + inner walls with top and bottom caps), not just its centreline. - The tube is real 3-D geometry, not a flat draw-order hatch: it renders solid in every view mode with true depth, so nothing shows through the walls. The flat 2-D band fill is skipped for a thickened polyline. - A fill whose triangles span depth keeps its real depth (no draw-order bias), so the extrusion's own edge wires stay in front of the surface and remain visible instead of only the silhouette. - The corner and cap edge lines read black, framing the coloured tube. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
626e8091f7
commit
3d37e1f8d8
5 changed files with 124 additions and 3 deletions
|
|
@ -1576,7 +1576,13 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"linetype_scale" | "transparency" => {
|
"linetype_scale" | "transparency" | "thickness" => {
|
||||||
|
// Thickness (DXF 39 extrusion) is a General-
|
||||||
|
// group common prop, not a geometry-group one:
|
||||||
|
// route it to apply_common_prop (which handles
|
||||||
|
// it via set_entity_thickness). apply_geom_prop
|
||||||
|
// ignores it, so the edit would otherwise be a
|
||||||
|
// silent no-op and revert to the old value.
|
||||||
if let Some(entity) =
|
if let Some(entity) =
|
||||||
self.tabs[i].scene.document.get_entity_mut(handle)
|
self.tabs[i].scene.document.get_entity_mut(handle)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -100,6 +100,83 @@ fn thick_segments(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Extrude a *wide* LwPolyline's band into a 3-D tube: each band segment's outer
|
||||||
|
/// and inner boundary rises by `thickness` (along the normal) into a wall,
|
||||||
|
/// leaving the two radial segment-end edges open (they are internal to the
|
||||||
|
/// band). The flat bottom band is already drawn by the wide-fill path; this
|
||||||
|
/// adds the vertical walls and their outline. Used instead of `thick_segments`
|
||||||
|
/// (which extrudes only the centreline) when the polyline carries a width.
|
||||||
|
fn thick_wide_band(
|
||||||
|
pl: &LwPolyline,
|
||||||
|
thickness: f64,
|
||||||
|
to_wcs: &dyn Fn(f64, f64) -> (f64, f64, f64),
|
||||||
|
normal: (f64, f64, f64),
|
||||||
|
key_verts: Vec<[f64; 3]>,
|
||||||
|
tangents: Vec<TangentGeom>,
|
||||||
|
) -> TruckEntity {
|
||||||
|
let (origin, fills) = wide_fills(pl);
|
||||||
|
let (nx, ny, nz) = normal;
|
||||||
|
let t = thickness;
|
||||||
|
let off = |p: [f64; 3]| -> [f64; 3] { [p[0] + t * nx, p[1] + t * ny, p[2] + t * nz] };
|
||||||
|
let push_seg = |lines: &mut Vec<[f64; 3]>, a: [f64; 3], b: [f64; 3]| {
|
||||||
|
lines.push(a);
|
||||||
|
lines.push(b);
|
||||||
|
lines.push([f64::NAN; 3]);
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut lines: Vec<[f64; 3]> = Vec::new();
|
||||||
|
// Shaded surfaces: the vertical walls plus the top cap. The flat bottom cap
|
||||||
|
// is already drawn by the wide-fill hatch path, so it is not repeated here.
|
||||||
|
let mut fill_tris: Vec<[f64; 3]> = Vec::new();
|
||||||
|
for poly in &fills {
|
||||||
|
let n = poly.len();
|
||||||
|
if n < 4 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// `polyline_segment_fill` returns the band loop as the outer boundary
|
||||||
|
// forward then the inner boundary back, so the two transition edges
|
||||||
|
// (half-1 → half and n-1 → 0) are the radial segment ends.
|
||||||
|
let half = n / 2;
|
||||||
|
let bot: Vec<[f64; 3]> = poly
|
||||||
|
.iter()
|
||||||
|
.map(|&[x, y]| {
|
||||||
|
let (wx, wy, wz) = to_wcs(origin[0] + x as f64, origin[1] + y as f64);
|
||||||
|
[wx, wy, wz]
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let top: Vec<[f64; 3]> = bot.iter().map(|&p| off(p)).collect();
|
||||||
|
for k in 0..n {
|
||||||
|
// Vertical at every boundary point.
|
||||||
|
push_seg(&mut lines, bot[k], top[k]);
|
||||||
|
// Skip the two radial cap edges — they stay inside the band.
|
||||||
|
if k == half - 1 || k == n - 1 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let kn = (k + 1) % n;
|
||||||
|
push_seg(&mut lines, bot[k], bot[kn]); // bottom boundary
|
||||||
|
push_seg(&mut lines, top[k], top[kn]); // top boundary
|
||||||
|
// Wall quad (two triangles).
|
||||||
|
fill_tris.extend_from_slice(&[
|
||||||
|
bot[k], bot[kn], top[kn], bot[k], top[kn], top[k],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
// Bottom + top caps — the band ring triangulated at each plane. The
|
||||||
|
// bottom cap is a real 3-D surface (with depth) so the walls occlude it
|
||||||
|
// correctly; the flat 2-D band fill is skipped for thickened polylines.
|
||||||
|
fill_tris.extend(crate::entities::mesh::triangulate_planar(&bot));
|
||||||
|
fill_tris.extend(crate::entities::mesh::triangulate_planar(&top));
|
||||||
|
}
|
||||||
|
|
||||||
|
TruckEntity {
|
||||||
|
pick_tris: fill_tris.clone(),
|
||||||
|
object: TruckObject::Lines(lines),
|
||||||
|
snap_pts: vec![],
|
||||||
|
tangent_geoms: tangents,
|
||||||
|
key_vertices: key_verts,
|
||||||
|
fill_tris,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn to_truck(pline: &LwPolyline) -> TruckEntity {
|
fn to_truck(pline: &LwPolyline) -> TruckEntity {
|
||||||
let verts = &pline.vertices;
|
let verts = &pline.vertices;
|
||||||
if verts.is_empty() {
|
if verts.is_empty() {
|
||||||
|
|
@ -172,6 +249,16 @@ fn to_truck(pline: &LwPolyline) -> TruckEntity {
|
||||||
kv.push([wbx, wby, wbz]);
|
kv.push([wbx, wby, wbz]);
|
||||||
seg_data.push((ox0, oy0, ox1, oy1));
|
seg_data.push((ox0, oy0, ox1, oy1));
|
||||||
}
|
}
|
||||||
|
// A wide polyline extrudes its whole band into a tube (outer + inner
|
||||||
|
// walls); a zero-width one just extrudes its centreline.
|
||||||
|
let is_wide = pline.constant_width > 1e-9
|
||||||
|
|| pline
|
||||||
|
.vertices
|
||||||
|
.iter()
|
||||||
|
.any(|v| v.start_width > 1e-9 || v.end_width > 1e-9);
|
||||||
|
if is_wide {
|
||||||
|
return thick_wide_band(pline, pline.thickness, &to_wcs, normal, kv, tgs);
|
||||||
|
}
|
||||||
return thick_segments(&seg_data, &path, pline.thickness, normal, kv, tgs);
|
return thick_segments(&seg_data, &path, pline.thickness, normal, kv, tgs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1116,6 +1116,18 @@ pub fn tessellate(
|
||||||
let mut out = Vec::new();
|
let mut out = Vec::new();
|
||||||
let mut is_first = true;
|
let mut is_first = true;
|
||||||
|
|
||||||
|
// A thickened wide polyline's extrusion — its corner / cap edges
|
||||||
|
// frame the solid tube and read black (like solid-with-edges
|
||||||
|
// outlines), while the tube fill keeps the entity colour.
|
||||||
|
let edge_color = if matches!(
|
||||||
|
entity,
|
||||||
|
EntityType::LwPolyline(p) if p.thickness.abs() > 1e-10
|
||||||
|
) {
|
||||||
|
[0.0, 0.0, 0.0, 1.0]
|
||||||
|
} else {
|
||||||
|
color
|
||||||
|
};
|
||||||
|
|
||||||
if !local_pts.is_empty() {
|
if !local_pts.is_empty() {
|
||||||
let (snap, keys, tangents) = if is_first {
|
let (snap, keys, tangents) = if is_first {
|
||||||
is_first = false;
|
is_first = false;
|
||||||
|
|
@ -1137,7 +1149,7 @@ pub fn tessellate(
|
||||||
name: name.clone(),
|
name: name.clone(),
|
||||||
points: local_pts,
|
points: local_pts,
|
||||||
points_low: local_pts_low,
|
points_low: local_pts_low,
|
||||||
color,
|
color: edge_color,
|
||||||
selected,
|
selected,
|
||||||
pattern_length: 0.0,
|
pattern_length: 0.0,
|
||||||
pattern: [0.0; 8],
|
pattern: [0.0; 8],
|
||||||
|
|
|
||||||
|
|
@ -691,6 +691,11 @@ impl Scene {
|
||||||
// Wide LWPolyline and Polyline2D fills
|
// Wide LWPolyline and Polyline2D fills
|
||||||
for entity in self.document.entities() {
|
for entity in self.document.entities() {
|
||||||
let (common, fill_origin, fills) = match entity {
|
let (common, fill_origin, fills) = match entity {
|
||||||
|
// A thickened wide LwPolyline becomes a 3-D tube: its bottom cap
|
||||||
|
// is drawn as a real depth-tested surface by the tessellator, not
|
||||||
|
// as this flat 2-D band (which, having only draw-order depth,
|
||||||
|
// would show through the tube walls in a 3-D view).
|
||||||
|
EntityType::LwPolyline(pl) if pl.thickness.abs() > 1e-10 => continue,
|
||||||
EntityType::LwPolyline(pl) => {
|
EntityType::LwPolyline(pl) => {
|
||||||
let (o, f) = crate::entities::lwpolyline::wide_fills(pl);
|
let (o, f) = crate::entities::lwpolyline::wide_fills(pl);
|
||||||
(&pl.common, o, f)
|
(&pl.common, o, f)
|
||||||
|
|
|
||||||
|
|
@ -211,7 +211,18 @@ impl Face3DGpu {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let fill_color = [r, g, b, a];
|
let fill_color = [r, g, b, a];
|
||||||
let depth = depth_of(wire);
|
// A fill whose triangles span depth is a genuine 3-D surface (an
|
||||||
|
// extruded polyline tube), not a flat coplanar overlay (2-D SOLID,
|
||||||
|
// greek text, dimension background). Keep its real depth so the
|
||||||
|
// fill occludes correctly and its own edge wires — coincident with
|
||||||
|
// the surface — win the depth test and stay visible. A flat overlay
|
||||||
|
// keeps the draw-order bias that layers it in screen order.
|
||||||
|
let (mut zmin, mut zmax) = (f32::INFINITY, f32::NEG_INFINITY);
|
||||||
|
for p in &wire.fill_tris {
|
||||||
|
zmin = zmin.min(p[2]);
|
||||||
|
zmax = zmax.max(p[2]);
|
||||||
|
}
|
||||||
|
let depth = if zmax - zmin > 1e-4 { 0.0 } else { depth_of(wire) };
|
||||||
for (i, &position) in wire.fill_tris.iter().enumerate() {
|
for (i, &position) in wire.fill_tris.iter().enumerate() {
|
||||||
verts_2d.push(Face3DVertex {
|
verts_2d.push(Face3DVertex {
|
||||||
position,
|
position,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue