From 3d37e1f8d8d1909199cc1cd380d2b36c4a8ecb23 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Mon, 20 Jul 2026 13:40:12 +0300 Subject: [PATCH] fix(thickness): edit persists + wide polyline extrudes as a solid tube MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/app/update/command.rs | 8 ++- src/entities/lwpolyline.rs | 87 ++++++++++++++++++++++++++++++++ src/scene/convert/tessellate.rs | 14 ++++- src/scene/entity.rs | 5 ++ src/scene/pipeline/face3d_gpu.rs | 13 ++++- 5 files changed, 124 insertions(+), 3 deletions(-) diff --git a/src/app/update/command.rs b/src/app/update/command.rs index ddc646dba..7de37ca48 100644 --- a/src/app/update/command.rs +++ b/src/app/update/command.rs @@ -1576,7 +1576,13 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { ); } } - "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) = self.tabs[i].scene.document.get_entity_mut(handle) { diff --git a/src/entities/lwpolyline.rs b/src/entities/lwpolyline.rs index b326cf1ac..ae5f295c0 100644 --- a/src/entities/lwpolyline.rs +++ b/src/entities/lwpolyline.rs @@ -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, +) -> 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 { let verts = &pline.vertices; if verts.is_empty() { @@ -172,6 +249,16 @@ fn to_truck(pline: &LwPolyline) -> TruckEntity { kv.push([wbx, wby, wbz]); 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); } diff --git a/src/scene/convert/tessellate.rs b/src/scene/convert/tessellate.rs index 2d120dc40..d72c71efb 100644 --- a/src/scene/convert/tessellate.rs +++ b/src/scene/convert/tessellate.rs @@ -1116,6 +1116,18 @@ pub fn tessellate( let mut out = Vec::new(); 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() { let (snap, keys, tangents) = if is_first { is_first = false; @@ -1137,7 +1149,7 @@ pub fn tessellate( name: name.clone(), points: local_pts, points_low: local_pts_low, - color, + color: edge_color, selected, pattern_length: 0.0, pattern: [0.0; 8], diff --git a/src/scene/entity.rs b/src/scene/entity.rs index 4c598bb2a..a467ca6bc 100644 --- a/src/scene/entity.rs +++ b/src/scene/entity.rs @@ -691,6 +691,11 @@ impl Scene { // Wide LWPolyline and Polyline2D fills for entity in self.document.entities() { 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) => { let (o, f) = crate::entities::lwpolyline::wide_fills(pl); (&pl.common, o, f) diff --git a/src/scene/pipeline/face3d_gpu.rs b/src/scene/pipeline/face3d_gpu.rs index 79153b522..4b23d7a6b 100644 --- a/src/scene/pipeline/face3d_gpu.rs +++ b/src/scene/pipeline/face3d_gpu.rs @@ -211,7 +211,18 @@ impl Face3DGpu { } } else { 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() { verts_2d.push(Face3DVertex { position,