fix(polyline): render fill mode boundaries

Use kernel-produced band outlines when FILLMODE is off. Preserve centerline linetype stations through inserts, clipping, projection, and PDF export.
This commit is contained in:
Hakan Seven 2026-08-24 22:18:44 +03:00
commit 4209c35ba3
60 changed files with 1620 additions and 575 deletions

2
Cargo.lock generated
View file

@ -878,7 +878,7 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
[[package]]
name = "cadkernel"
version = "0.1.0"
source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=76de3b3#76de3b35e55a45f3ffb813c499cf3affe176351d"
source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=b2b1d4b#b2b1d4b0cd7def2d26a0d519991fc89a89f916d3"
dependencies = [
"acadrust",
"cavalier_contours",

View file

@ -28,7 +28,7 @@ rfd = "0.17"
clap = { version = "4", features = ["derive"] }
env_logger = "0.11"
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "94df2c3", features = ["serde"] }
cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "76de3b3", features = ["acis", "offset"] }
cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "b2b1d4b", features = ["acis", "offset"] }
dwg-thumbnailer = { path = "crates/dwg-thumbnailer" }
flate2 = "1"
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp", "tiff"] }

File diff suppressed because it is too large Load diff

View file

@ -1271,6 +1271,8 @@ draw =
.donut-specify-center-of-donut-enter-to-exit = DONUT Specify center of donut (Enter to exit):
.donut-specify-inside-diameter-0 = DONUT Specify inside diameter <0>:
.donut-specify-outside-diameter = DONUT Specify outside diameter:
.donut-specify-second-point-for-inside-diameter = DONUT Specify second point for inside diameter:
.donut-specify-second-point-for-outside-diameter = DONUT Specify second point for outside diameter:
.draw-manually = Draw manually
.ellipse-specify-center = ELLIPSE Specify center:
.ellipse-specify-major-axis-endpoint = ELLIPSE Specify major axis endpoint:
@ -2573,6 +2575,7 @@ properties =
.ext-line-lineweight = Ext line lineweight
.ext-line-offset = Ext line offset
.extended-entity = Extended Entity
.extended-data = Extended Data
.extended-lighting = Extended Lighting
.extents-max = Extents Max
.extents-min = Extents Min

View file

@ -1253,6 +1253,8 @@ draw =
.donut-specify-center-of-donut-enter-to-exit = DONUT Halkanın merkezini belirtin (çıkmak için Enter):
.donut-specify-inside-diameter-0 = DONUT İç çapı belirtin <0>:
.donut-specify-outside-diameter = DONUT Dış çapı belirtin:
.donut-specify-second-point-for-inside-diameter = DONUT İç çap için ikinci noktayı belirtin:
.donut-specify-second-point-for-outside-diameter = DONUT Dış çap için ikinci noktayı belirtin:
.draw-manually = Elle çiz
.ellipse-specify-center = ELLIPSE Merkezi belirtin:
.ellipse-specify-major-axis-endpoint = ELLIPSE Büyük eksen uç noktasını belirtin:

View file

@ -935,6 +935,8 @@ impl OpenCADStudio {
| "SKETCHINC"
| "SKPOLY"
| "SKTOLERANCE"
| "DONUTID"
| "DONUTOD"
| "CENTEREXE"
| "CENTERLAYER"
| "CENTERLTYPE"
@ -965,7 +967,7 @@ impl OpenCADStudio {
let value = it.next().map(|s| s.trim().to_string());
if name.is_empty() || name == "?" {
self.command_line.push_info(
"SETVAR: LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR CURSORSIZE PICKBOX CURSORTYPE SNAPANG ATTREQ ATTDIA DIMASSOC ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE | CLAYER CELTYPE TEXTSTYLE (read-only)",
"SETVAR: LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR CURSORSIZE PICKBOX CURSORTYPE SNAPANG ATTREQ ATTDIA DIMASSOC ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE | CLAYER CELTYPE TEXTSTYLE (read-only)",
);
} else {
let frame_kind = crate::scene::frame::kind_for_name(&name);
@ -1081,6 +1083,49 @@ impl OpenCADStudio {
}
return Some(self.finish_dispatch(cmd));
}
if matches!(name.as_str(), "DONUTID" | "DONUTOD") {
let current = if name == "DONUTID" {
crate::modules::draw::defaults::get_donut_inner_diameter()
} else {
crate::modules::draw::defaults::get_donut_outer_diameter()
};
if let Some(value) = &value {
let parsed = value
.trim()
.replace(',', ".")
.parse::<f64>()
.ok()
.filter(|number| number.is_finite());
let valid = parsed.filter(|number| {
if name == "DONUTID" {
*number >= 0.0
} else {
*number > 0.0
}
});
if let Some(number) = valid {
if name == "DONUTID" {
crate::modules::draw::defaults::set_donut_inner_diameter(number);
} else {
crate::modules::draw::defaults::set_donut_outer_diameter(number);
}
self.command_line
.push_output(&format!("{name} = {number}"));
} else {
self.command_line.push_error(if name == "DONUTID" {
"SETVAR: DONUTID requires a finite value greater than or equal to zero."
} else {
"SETVAR: DONUTOD requires a finite value greater than zero."
});
}
} else {
self.command_line.push_output(crate::tf!(
"Enter new value for {name} <{current}>:"
).as_ref());
self.pending_setvar = Some(name.clone());
}
return Some(self.finish_dispatch(cmd));
}
// Parse a boolean given as 0/1 or ON/OFF.
let parse_bool = |s: &str| match s.to_uppercase().as_str() {
"1" | "ON" | "TRUE" => Some(true),
@ -1849,6 +1894,9 @@ impl OpenCADStudio {
} else {
self.tabs[i].dirty = true;
}
if name == "FILLMODE" {
self.tabs[i].scene.bump_geometry();
}
self.command_line.push_output(&msg);
} else {
// Queried with no value (`changed == false`):

View file

@ -671,6 +671,31 @@ impl OpenCADStudio {
entity,
));
{
use crate::entities::common::ro_prop;
let xd = &source_entity.common().extended_data;
if !xd.is_empty() {
let mut xd_props = Vec::new();
for rec in xd.records() {
let value_text = rec
.values
.iter()
.map(|value| format!("{value:?}"))
.collect::<Vec<_>>()
.join(", ");
xd_props.push(ro_prop(
t!("Application").as_ref(),
"xdata_value",
format!("{}: {value_text}", rec.application_name),
));
}
sections.push(crate::scene::model::object::PropSection {
title: t!("Extended Data").into_owned(),
props: xd_props,
});
}
}
// Uniform-scale checkbox for block references (#427):
// while the three scale factors are equal (and the user
// hasn't opted into per-axis editing) the panel shows one

View file

@ -2242,6 +2242,7 @@ impl OpenCADStudio {
let guide = crate::scene::WireModel {
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
world_width: 0.0,
depth_override: None,
display_visible: true,

View file

@ -1,6 +1,8 @@
use std::cell::Cell;
use std::collections::HashSet;
use crate::scene::model::object::{GripDef, GripShape, PropValue, Property};
use crate::scene::model::wire_model::PatternStationPiece;
/// Linear / angular unit format pulled from the document header so the
/// per-thread properties pipeline can format values consistently without
@ -789,6 +791,158 @@ pub(crate) fn tapered_band_points(
(pts, widths)
}
pub(crate) struct WideBandOutline {
pub points: Vec<[f64; 3]>,
pub stations: Vec<f32>,
pub point_segments: Vec<i32>,
pub station_pieces: Vec<PatternStationPiece>,
pub source_length: f32,
}
/// Builds the boundary of a variable-width band through the kernel.
pub(crate) fn wide_band_outline(
verts: &[([f64; 2], f64, f64, f64)],
is_closed: bool,
restart_per_segment: bool,
to_wcs: &dyn Fn(f64, f64) -> (f64, f64, f64),
) -> WideBandOutline {
let source = cadkernel::geom2d::Polyline {
vertices: verts
.iter()
.map(|(position, bulge, _, _)| cadkernel::geom2d::PolylineVertex {
position: *position,
bulge: *bulge,
})
.collect(),
closed: is_closed,
};
let segment_count = if is_closed {
verts.len()
} else {
verts.len().saturating_sub(1)
};
let widths: Vec<[f64; 2]> = verts
.iter()
.take(segment_count)
.map(|(_, _, start, end)| [*start, *end])
.collect();
let boundary = cadkernel::geom2d::polyline_band_boundary(
&source,
&widths,
cadkernel::tessellation::DEFAULT_ANGLE,
);
let mut points = Vec::new();
let mut stations = Vec::new();
let mut point_segments = Vec::new();
for edge in boundary.edges {
if !points.is_empty() {
points.push([f64::NAN; 3]);
stations.push(0.0);
point_segments.push(-1);
}
points.extend(edge.points.into_iter().map(|point| {
let (x, y, z) = to_wcs(point[0], point[1]);
[x, y, z]
}));
stations.extend(
if restart_per_segment {
edge.segment_distances
} else {
edge.source_distances
}
.map(|distance| distance as f32),
);
point_segments.extend([edge.source_segment as i32; 2]);
}
let station_pieces = boundary
.station_pieces
.into_iter()
.map(|piece| {
let start = to_wcs(piece.points[0][0], piece.points[0][1]);
let end = to_wcs(piece.points[1][0], piece.points[1][1]);
PatternStationPiece {
source_segment: piece.source_segment as u32,
source_distances: piece.source_distances.map(|distance| distance as f32),
segment_distances: piece.segment_distances.map(|distance| distance as f32),
vector: [
(end.0 - start.0) as f32,
(end.1 - start.1) as f32,
(end.2 - start.2) as f32,
],
}
})
.collect();
WideBandOutline {
points,
stations,
point_segments,
station_pieces,
source_length: boundary.source_length as f32,
}
}
pub(crate) fn extrude_wide_band_outline(
outline: WideBandOutline,
extrusion: [f64; 3],
) -> WideBandOutline {
let base_points = outline.points;
let base_stations = outline.stations;
let base_segments = outline.point_segments;
let mut points = base_points.clone();
let mut stations = base_stations.clone();
let mut point_segments = base_segments.clone();
points.push([f64::NAN; 3]);
stations.push(0.0);
point_segments.push(-1);
points.extend(base_points.iter().map(|point| {
if point[0].is_finite() {
[
point[0] + extrusion[0],
point[1] + extrusion[1],
point[2] + extrusion[2],
]
} else {
*point
}
}));
stations.extend_from_slice(&base_stations);
point_segments.extend_from_slice(&base_segments);
let mut emitted = HashSet::new();
for ((&point, &station), &segment) in base_points
.iter()
.zip(&base_stations)
.zip(&base_segments)
{
if !point[0].is_finite() {
continue;
}
let key = (point.map(f64::to_bits), station.to_bits(), segment);
if !emitted.insert(key) {
continue;
}
points.push([f64::NAN; 3]);
stations.push(0.0);
point_segments.push(-1);
points.push(point);
points.push([
point[0] + extrusion[0],
point[1] + extrusion[1],
point[2] + extrusion[2],
]);
stations.push(station);
stations.push(station);
point_segments.extend([segment; 2]);
}
WideBandOutline {
points,
stations,
point_segments,
station_pieces: outline.station_pieces,
source_length: outline.source_length,
}
}
/// Compute the filled boundary polygon for one polyline segment.
/// For straight segments: a rectangle/trapezoid.
/// For arc segments: an arc band (outer arc + reversed inner arc).

View file

@ -2068,6 +2068,7 @@ fn tessellate_dimension_inner(
wires.push(WireModel {
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
world_width: 0.0,
depth_override: None,
display_visible: true,
@ -2102,6 +2103,7 @@ fn tessellate_dimension_inner(
wires.push(WireModel {
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
world_width: 0.0,
depth_override: None,
display_visible: true,
@ -2136,6 +2138,7 @@ fn tessellate_dimension_inner(
wires.push(WireModel {
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
world_width: 0.0,
depth_override: None,
display_visible: true,
@ -2171,6 +2174,7 @@ fn tessellate_dimension_inner(
wires.push(WireModel {
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
world_width: 0.0,
depth_override: None,
display_visible: true,
@ -2222,6 +2226,7 @@ fn tessellate_dimension_inner(
wires.push(WireModel {
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
world_width: 0.0,
depth_override: None,
display_visible: true,

View file

@ -676,6 +676,7 @@ impl LeaderTess for Leader {
return WireModel {
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
world_width: 0.0,
depth_override: None,
display_visible: true,
@ -855,6 +856,7 @@ impl LeaderTess for Leader {
WireModel {
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
world_width: 0.0,
depth_override: None,
display_visible: true,

View file

@ -16,6 +16,15 @@ use crate::scene::model::wire_model::TangentGeom;
const TAU: f64 = std::f64::consts::TAU;
const REVCLOUD_BULGE: f64 = 0.5;
const MAX_REVCLOUD_VERTICES: usize = 100_000;
const WIDTH_EPSILON: f64 = 1.0e-9;
fn effective_width(width: f64, constant_width: f64) -> f64 {
if width > WIDTH_EPSILON {
width
} else {
constant_width
}
}
/// Midpoint position on an arc segment defined by its bulge.
fn arc_midpoint(p0: [f64; 2], p1: [f64; 2], bulge: f64) -> [f64; 2] {
@ -251,7 +260,7 @@ fn thick_wide_band(
}
}
fn to_render(pline: &LwPolyline) -> RenderEntity {
fn to_render(pline: &LwPolyline, fill_mode: bool) -> RenderEntity {
let verts = &pline.vertices;
if verts.is_empty() {
return RenderEntity {
@ -268,9 +277,6 @@ fn to_render(pline: &LwPolyline) -> RenderEntity {
let normal = (pline.normal.x, pline.normal.y, pline.normal.z);
let count = verts.len();
let seg_count = if pline.is_closed { count } else { count - 1 };
let mut tangents: Vec<TangentGeom> = Vec::new();
let mut key_verts: Vec<[f64; 3]> = Vec::new();
// Convert OCS (x, y, elevation) to a WCS point.
let to_wcs = |x: f64, y: f64| -> (f64, f64, f64) {
crate::scene::view::transform::ocs_point_to_wcs((x, y, elev), normal)
@ -280,6 +286,44 @@ fn to_render(pline: &LwPolyline) -> RenderEntity {
[wx, wy, wz]
};
let band_verts = band_verts(pline);
if !fill_mode {
let mut boundary = crate::entities::common::wide_band_outline(
&band_verts,
pline.is_closed,
!pline.plinegen,
&to_wcs,
);
if !boundary.points.is_empty() {
if pline.thickness.abs() > 1e-10 {
boundary = crate::entities::common::extrude_wide_band_outline(
boundary,
[
pline.thickness * normal.0,
pline.thickness * normal.1,
pline.thickness * normal.2,
],
);
}
let (tangent_geoms, key_vertices) = centerline_metadata(pline, &to_wcs);
return RenderEntity {
pick_tris: Vec::new(),
object: RenderObject::BoundaryLines {
points: boundary.points,
stations: boundary.stations,
point_segments: boundary.point_segments,
station_pieces: boundary.station_pieces,
source_length: boundary.source_length,
plinegen: pline.plinegen,
},
snap_pts: vec![],
tangent_geoms,
key_vertices,
fill_tris: vec![],
};
}
}
if pline.thickness.abs() > 1e-10 {
let mut path: Vec<[f64; 3]> = Vec::new();
let mut kv: Vec<[f64; 3]> = Vec::new();
@ -341,7 +385,7 @@ fn to_render(pline: &LwPolyline) -> RenderEntity {
// A wide polyline whose per-vertex widths VARY renders a smooth taper —
// handled here, before the PLINEGEN split, so both cases get it. A
// uniform-width polyline falls through to the constant-band paths below.
if let Some(band_verts) = tapered_band_verts(pline) {
if tapered_band_verts(&band_verts).is_some() {
let mut kv: Vec<[f64; 3]> = Vec::new();
let mut tgs: Vec<TangentGeom> = Vec::new();
for i in 0..seg_count {
@ -370,8 +414,11 @@ fn to_render(pline: &LwPolyline) -> RenderEntity {
});
}
}
let (pts, widths) =
crate::entities::common::tapered_band_points(&band_verts, pline.is_closed, &to_wcs);
let (pts, widths) = crate::entities::common::tapered_band_points(
&band_verts,
pline.is_closed,
&to_wcs,
);
let (fill_origin, fills) = wide_fills(pline);
return RenderEntity {
pick_tris: crate::entities::common::wide_band_tris(fill_origin, &fills),
@ -444,36 +491,7 @@ fn to_render(pline: &LwPolyline) -> RenderEntity {
};
}
for i in 0..seg_count {
let v0 = &verts[i];
let v1 = &verts[(i + 1) % count];
let p0 = to_pt(v0);
let p1 = to_pt(v1);
let bulge = v0.bulge;
if bulge.abs() < 1e-9 {
tangents.push(TangentGeom::Line {
p1: [p0[0] as f32, p0[1] as f32, p0[2] as f32],
p2: [p1[0] as f32, p1[1] as f32, p1[2] as f32],
});
} else if let Some(arc) = crate::entities::common::BulgeArc::from_bulge(
[v0.location.x, v0.location.y],
[v1.location.x, v1.location.y],
bulge as f64,
) {
let (wcx, wcy, wcz) = to_wcs(arc.center[0], arc.center[1]);
tangents.push(TangentGeom::Circle {
center: [wcx as f32, wcy as f32, wcz as f32],
radius: arc.radius as f32,
});
}
if i == 0 {
key_verts.push([p0[0], p0[1], p0[2]]);
}
key_verts.push([p1[0], p1[1], p1[2]]);
}
let (tangents, key_verts) = centerline_metadata(pline, &to_wcs);
let (fill_origin, fills) = wide_fills(pline);
RenderEntity {
pick_tris: crate::entities::common::wide_band_tris(fill_origin, &fills),
@ -491,20 +509,23 @@ fn to_render(pline: &LwPolyline) -> RenderEntity {
}
}
/// The per-vertex `(location, bulge, start_width, end_width)` band description
/// for a wide LwPolyline whose width VARIES — `None` when the width is uniform
/// (a constant band) so the caller keeps the cheaper constant-width path.
fn tapered_band_verts(pline: &LwPolyline) -> Option<Vec<([f64; 2], f64, f64, f64)>> {
/// Effective segment widths for an LwPolyline band.
fn band_verts(pline: &LwPolyline) -> Vec<([f64; 2], f64, f64, f64)> {
let c = pline.constant_width;
let band: Vec<([f64; 2], f64, f64, f64)> = pline
pline
.vertices
.iter()
.map(|v| {
let sw = if v.start_width > 1e-9 { v.start_width } else { c };
let ew = if v.end_width > 1e-9 { v.end_width } else { c };
let sw = effective_width(v.start_width, c);
let ew = effective_width(v.end_width, c);
([v.location.x, v.location.y], v.bulge, sw, ew)
})
.collect();
.collect()
}
fn tapered_band_verts(
band: &[([f64; 2], f64, f64, f64)],
) -> Option<&[([f64; 2], f64, f64, f64)]> {
let w0 = band.first().map_or(0.0, |v| v.2);
let varies = band
.iter()
@ -517,6 +538,47 @@ fn tapered_band_verts(pline: &LwPolyline) -> Option<Vec<([f64; 2], f64, f64, f64
}
}
fn centerline_metadata(
pline: &LwPolyline,
to_wcs: &dyn Fn(f64, f64) -> (f64, f64, f64),
) -> (Vec<TangentGeom>, Vec<[f64; 3]>) {
let count = pline.vertices.len();
let segment_count = if pline.is_closed {
count
} else {
count.saturating_sub(1)
};
let mut tangents = Vec::with_capacity(segment_count);
let mut key_vertices = Vec::with_capacity(segment_count + 1);
for index in 0..segment_count {
let start = &pline.vertices[index];
let end = &pline.vertices[(index + 1) % count];
let p0 = to_wcs(start.location.x, start.location.y);
let p1 = to_wcs(end.location.x, end.location.y);
if start.bulge.abs() < 1e-9 {
tangents.push(TangentGeom::Line {
p1: [p0.0 as f32, p0.1 as f32, p0.2 as f32],
p2: [p1.0 as f32, p1.1 as f32, p1.2 as f32],
});
} else if let Some(arc) = crate::entities::common::BulgeArc::from_bulge(
[start.location.x, start.location.y],
[end.location.x, end.location.y],
start.bulge,
) {
let center = to_wcs(arc.center[0], arc.center[1]);
tangents.push(TangentGeom::Circle {
center: [center.0 as f32, center.1 as f32, center.2 as f32],
radius: arc.radius as f32,
});
}
if index == 0 {
key_vertices.push([p0.0, p0.1, p0.2]);
}
key_vertices.push([p1.0, p1.1, p1.2]);
}
(tangents, key_vertices)
}
/// Split at vertex `idx`: a closed polyline re-opens there (one piece); an
/// open one splits into two (interior vertices only). `None` when invalid.
pub(crate) fn break_at_vertex(
@ -852,8 +914,12 @@ fn properties(pline: &LwPolyline) -> Vec<PropSection> {
let v = pline.vertices.get(vi);
let vx = v.map_or(0.0, |v| v.location.x);
let vy = v.map_or(0.0, |v| v.location.y);
let start_w = v.map_or(0.0, |v| v.start_width);
let end_w = v.map_or(0.0, |v| v.end_width);
let start_w = v.map_or(0.0, |v| {
effective_width(v.start_width, pline.constant_width)
});
let end_w = v.map_or(0.0, |v| {
effective_width(v.end_width, pline.constant_width)
});
let mp = <LwPolyline as crate::entities::traits::MassPropsCalc>::mass_props(pline);
let cloud_arc_length = revision_cloud_arc_length(pline);
let vertex_label = if n == 0 {
@ -964,16 +1030,7 @@ fn apply_geom_prop(pline: &mut LwPolyline, field: &str, value: &str) {
};
match field {
"elevation" => pline.elevation = v,
"global_width" if v.is_finite() && v >= 0.0 => {
pline.constant_width = v;
// Global width replaces every segment override. Keeping old
// start/end values would make this editable row appear to work
// while the renderer continued using the stale per-segment data.
for vertex in &mut pline.vertices {
vertex.start_width = 0.0;
vertex.end_width = 0.0;
}
}
"global_width" if v.is_finite() && v >= 0.0 => pline.constant_width = v,
"vertex_x" => {
if let Some(vtx) = pline.vertices.get_mut(vi) {
vtx.location.x = v;
@ -985,27 +1042,11 @@ fn apply_geom_prop(pline: &mut LwPolyline, field: &str, value: &str) {
}
}
"start_width" if v.is_finite() && v >= 0.0 => {
if pline.constant_width > 0.0 {
let width = pline.constant_width;
for vertex in &mut pline.vertices {
vertex.start_width = width;
vertex.end_width = width;
}
pline.constant_width = 0.0;
}
if let Some(vtx) = pline.vertices.get_mut(vi) {
vtx.start_width = v;
}
}
"end_width" if v.is_finite() && v >= 0.0 => {
if pline.constant_width > 0.0 {
let width = pline.constant_width;
for vertex in &mut pline.vertices {
vertex.start_width = width;
vertex.end_width = width;
}
pline.constant_width = 0.0;
}
if let Some(vtx) = pline.vertices.get_mut(vi) {
vtx.end_width = v;
}
@ -1104,8 +1145,8 @@ fn apply_transform(pline: &mut LwPolyline, t: &EntityTransform) {
}
impl RenderConvertible for LwPolyline {
fn to_render(&self, _document: &acadrust::CadDocument) -> Option<RenderEntity> {
Some(to_render(self))
fn to_render(&self, document: &acadrust::CadDocument) -> Option<RenderEntity> {
Some(to_render(self, document.header.fill_mode))
}
}
@ -1226,16 +1267,8 @@ impl crate::entities::traits::Grippable for LwPolyline {
new_v.location.x = midpoint[0];
new_v.location.y = midpoint[1];
new_v.vertex_id = 0;
let effective_start = if v0.start_width > 1e-9 {
v0.start_width
} else {
self.constant_width
};
let effective_end = if v0.end_width > 1e-9 {
v0.end_width
} else {
self.constant_width
};
let effective_start = effective_width(v0.start_width, self.constant_width);
let effective_end = effective_width(v0.end_width, self.constant_width);
let middle_width = (effective_start + effective_end) * 0.5;
self.vertices[i0].end_width = middle_width;
new_v.start_width = middle_width;
@ -1296,7 +1329,6 @@ pub(crate) fn wide_fills(pl: &acadrust::entities::LwPolyline) -> ([f64; 2], Vec<
// Feeding the stored width in whole draws every wide polyline twice as wide
// as the file asks for, which on a donut (a closed 2-vertex bulge-1 polyline)
// shows up as a disc 1.5× its real radius.
let hw_const = pl.constant_width as f32 * 0.5;
let verts = &pl.vertices;
let n = verts.len();
if n < 2 {
@ -1308,16 +1340,8 @@ pub(crate) fn wide_fills(pl: &acadrust::entities::LwPolyline) -> ([f64; 2], Vec<
for i in 0..seg_count {
let v0 = &verts[i];
let v1 = &verts[(i + 1) % n];
let hw0 = if v0.start_width > 1e-9 {
v0.start_width as f32 * 0.5
} else {
hw_const
};
let hw1 = if v0.end_width > 1e-9 {
v0.end_width as f32 * 0.5
} else {
hw_const
};
let hw0 = effective_width(v0.start_width, pl.constant_width) as f32 * 0.5;
let hw1 = effective_width(v0.end_width, pl.constant_width) as f32 * 0.5;
if hw0 < 1e-6 && hw1 < 1e-6 {
continue;
}

View file

@ -1505,6 +1505,7 @@ impl MultiLeaderTess for MultiLeader {
wires.push(WireModel {
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
world_width: 0.0,
depth_override: None,
display_visible: true,
@ -1845,6 +1846,7 @@ impl MultiLeaderTess for MultiLeader {
wires.push(WireModel {
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
world_width: 0.0,
depth_override: None,
display_visible: true,
@ -1918,6 +1920,7 @@ impl MultiLeaderTess for MultiLeader {
wires.push(WireModel {
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
world_width: 0.0,
depth_override: None,
display_visible: true,
@ -2006,6 +2009,7 @@ impl MultiLeaderTess for MultiLeader {
wires.push(WireModel {
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
world_width: 0.0,
depth_override: None,
display_visible: true,
@ -2049,6 +2053,7 @@ impl MultiLeaderTess for MultiLeader {
wires.push(WireModel {
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
world_width: 0.0,
depth_override: None,
display_visible: true,

View file

@ -195,7 +195,7 @@ pub fn drawn_vertices2d(
(kept.len() >= 2).then_some(kept)
}
fn tessellate_polyline2d(pl: &Polyline2D) -> RenderEntity {
fn tessellate_polyline2d(pl: &Polyline2D, fill_mode: bool) -> RenderEntity {
let filtered = drawn_vertices2d(pl);
let verts: &[acadrust::entities::Vertex2D] = filtered.as_deref().unwrap_or(&pl.vertices);
if verts.is_empty() {
@ -224,6 +224,47 @@ fn tessellate_polyline2d(pl: &Polyline2D) -> RenderEntity {
[wx, wy, wz]
};
if !fill_mode {
let continuous = pl.flags.bits()
& acadrust::entities::PolylineFlags::LINETYPE_CONTINUOUS.bits()
!= 0;
let mut boundary = crate::entities::common::wide_band_outline(
&band_verts_2d(pl),
pl.is_closed(),
!continuous,
&to_wcs,
);
if !boundary.points.is_empty() {
if pl.thickness.abs() > 1e-10 {
boundary = crate::entities::common::extrude_wide_band_outline(
boundary,
[
pl.thickness * normal.0,
pl.thickness * normal.1,
pl.thickness * normal.2,
],
);
}
let (tangent_geoms, key_vertices) =
centerline_metadata_2d(verts, pl.is_closed(), &to_wcs);
return RenderEntity {
pick_tris: Vec::new(),
object: RenderObject::BoundaryLines {
points: boundary.points,
stations: boundary.stations,
point_segments: boundary.point_segments,
station_pieces: boundary.station_pieces,
source_length: boundary.source_length,
plinegen: continuous,
},
snap_pts: vec![],
tangent_geoms,
key_vertices,
fill_tris: vec![],
};
}
}
if pl.thickness.abs() > 1e-10 {
let (nx, ny, nz) = normal;
let t = pl.thickness;
@ -348,13 +389,14 @@ fn tessellate_polyline2d(pl: &Polyline2D) -> RenderEntity {
key_verts.push([p1[0], p1[1], p1[2]]);
}
let band_verts = band_verts_2d(pl);
let (fill_origin, fills) = wide_fills(pl);
// A wide Polyline2D whose per-vertex widths VARY renders a smooth taper; a
// uniform-width one keeps the constant-band Contour.
let object = match tapered_band_verts_2d(pl) {
let object = match tapered_band_verts_2d(&band_verts) {
Some(band_verts) => {
let (pts, widths) = crate::entities::common::tapered_band_points(
&band_verts,
band_verts,
pl.is_closed(),
&to_wcs,
);
@ -376,16 +418,15 @@ fn tessellate_polyline2d(pl: &Polyline2D) -> RenderEntity {
}
}
/// Per-vertex `(location, bulge, start_width, end_width)` band description for a
/// wide Polyline2D whose width VARIES — `None` when the width is uniform.
fn tapered_band_verts_2d(
/// Effective segment widths for a Polyline2D band.
fn band_verts_2d(
pl: &acadrust::entities::Polyline2D,
) -> Option<Vec<([f64; 2], f64, f64, f64)>> {
) -> Vec<([f64; 2], f64, f64, f64)> {
let default_start = pl.start_width;
let default_end = pl.end_width;
let filtered = drawn_vertices2d(pl);
let verts: &[acadrust::entities::Vertex2D] = filtered.as_deref().unwrap_or(&pl.vertices);
let band: Vec<([f64; 2], f64, f64, f64)> = verts
verts
.iter()
.map(|v| {
let sw = if v.start_width > 1e-9 {
@ -400,7 +441,12 @@ fn tapered_band_verts_2d(
};
([v.location.x, v.location.y], v.bulge, sw, ew)
})
.collect();
.collect()
}
fn tapered_band_verts_2d(
band: &[([f64; 2], f64, f64, f64)],
) -> Option<&[([f64; 2], f64, f64, f64)]> {
let w0 = band.first().map_or(0.0, |v| v.2);
let varies = band
.iter()
@ -412,9 +458,51 @@ fn tapered_band_verts_2d(
}
}
fn centerline_metadata_2d(
verts: &[acadrust::entities::Vertex2D],
closed: bool,
to_wcs: &dyn Fn(f64, f64) -> (f64, f64, f64),
) -> (Vec<TangentGeom>, Vec<[f64; 3]>) {
let count = verts.len();
let segment_count = if closed {
count
} else {
count.saturating_sub(1)
};
let mut tangents = Vec::with_capacity(segment_count);
let mut key_vertices = Vec::with_capacity(segment_count + 1);
for index in 0..segment_count {
let start = &verts[index];
let end = &verts[(index + 1) % count];
let p0 = to_wcs(start.location.x, start.location.y);
let p1 = to_wcs(end.location.x, end.location.y);
if start.bulge.abs() < 1e-9 {
tangents.push(TangentGeom::Line {
p1: [p0.0 as f32, p0.1 as f32, p0.2 as f32],
p2: [p1.0 as f32, p1.1 as f32, p1.2 as f32],
});
} else if let Some(arc) = crate::entities::common::BulgeArc::from_bulge(
[start.location.x, start.location.y],
[end.location.x, end.location.y],
start.bulge,
) {
let center = to_wcs(arc.center[0], arc.center[1]);
tangents.push(TangentGeom::Circle {
center: [center.0 as f32, center.1 as f32, center.2 as f32],
radius: arc.radius as f32,
});
}
if index == 0 {
key_vertices.push([p0.0, p0.1, p0.2]);
}
key_vertices.push([p1.0, p1.1, p1.2]);
}
(tangents, key_vertices)
}
impl RenderConvertible for Polyline2D {
fn to_render(&self, _document: &acadrust::CadDocument) -> Option<RenderEntity> {
Some(tessellate_polyline2d(self))
fn to_render(&self, document: &acadrust::CadDocument) -> Option<RenderEntity> {
Some(tessellate_polyline2d(self, document.header.fill_mode))
}
}

View file

@ -1669,6 +1669,7 @@ pub fn tessellate_table(
WireModel {
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
world_width: 0.0,
depth_override: None,
display_visible: true,

View file

@ -593,6 +593,43 @@ fn append_pdf_page(
// Linetype dash pattern. Without this every wire exported as a solid
// line regardless of its linetype (dashed / centre / dash-dot). (#155)
let dash_arr = dash_array_from_pattern(wire.pattern_length, &wire.pattern, MM_TO_PT);
let stationed =
!dash_arr.is_empty() && wire.pattern_stations.len() > wire.points.len();
if stationed {
if last_dash.as_ref().is_none_or(|dash| !dash.is_empty()) {
ops.push(Op::SetLineDashPattern {
dash: LineDashPattern::default(),
});
last_dash = Some(Vec::new());
}
for index in 0..wire.points.len().saturating_sub(1) {
if !wire.points[index][0].is_finite()
|| !wire.points[index + 1][0].is_finite()
{
continue;
}
let start = wire.point_world(index, paper_h as f64 / scale.max(1e-6) as f64);
let end = wire.point_world(index + 1, paper_h as f64 / scale.max(1e-6) as f64);
for [from, to] in visible_station_ranges(
wire.pattern_stations[index],
wire.pattern_stations[index + 1],
wire.pattern_length,
&wire.pattern,
) {
let point = |t: f32| {
LinePoint {
p: Point::new(
Mm((start.x + (end.x - start.x) * t as f64 + ox) as f32),
Mm((start.y + (end.y - start.y) * t as f64 + oy) as f32),
),
bezier: false,
}
};
flush_line(&mut ops, &[point(from), point(to)], None);
}
}
continue;
}
if last_dash.as_deref() != Some(dash_arr.as_slice()) {
let dash = if dash_arr.is_empty() {
LineDashPattern::default()
@ -600,7 +637,7 @@ fn append_pdf_page(
LineDashPattern::from_array(&dash_arr, 0)
};
ops.push(Op::SetLineDashPattern { dash });
last_dash = Some(dash_arr);
last_dash = Some(dash_arr.clone());
}
// Emit segments (NaN = pen-up). Points are the "high" half of a
@ -670,6 +707,76 @@ fn dash_array_from_pattern(pattern_length: f32, pattern: &[f32; 8], mm_to_pt: f3
.collect()
}
#[cfg(not(target_arch = "wasm32"))]
fn visible_station_ranges(
start: f32,
end: f32,
pattern_length: f32,
pattern: &[f32; 8],
) -> Vec<[f32; 2]> {
let count = pattern.iter().rposition(|value| *value != 0.0).map_or(0, |i| i + 1);
if count == 0 || pattern_length <= 1e-6 {
return vec![[0.0, 1.0]];
}
let dot = 1.0 / MM_TO_PT;
let mut elements: Vec<(f32, bool)> = pattern[..count]
.iter()
.map(|value| (if *value == 0.0 { dot } else { value.abs() }, *value >= 0.0))
.collect();
let total: f32 = elements.iter().map(|(length, _)| *length).sum();
if total <= 1e-6 {
return vec![[0.0, 1.0]];
}
let factor = pattern_length / total;
for (length, _) in &mut elements {
*length *= factor;
}
let delta = end - start;
let state = |station: f32, forward: bool| {
let mut phase = station.rem_euclid(pattern_length);
if !forward && phase <= 1e-6 {
phase = pattern_length;
}
let mut offset = 0.0;
if forward {
for &(length, drawn) in &elements {
let end = offset + length;
if phase < end - 1e-6 {
return (drawn, end - phase);
}
offset = end;
}
(elements[0].1, elements[0].0)
} else {
offset = pattern_length;
for &(length, drawn) in elements.iter().rev() {
offset -= length;
if phase > offset + 1e-6 {
return (drawn, phase - offset);
}
}
let last = elements[elements.len() - 1];
(last.1, last.0)
}
};
if delta.abs() <= 1e-6 {
return state(start, true).0.then_some([0.0, 1.0]).into_iter().collect();
}
let mut ranges = Vec::new();
let mut t = 0.0;
while t < 1.0 - 1e-6 {
let station = start + delta * t;
let (drawn, remaining) = state(station, delta > 0.0);
let next = (t + remaining / delta.abs()).clamp(t + 1e-6, 1.0);
if drawn {
ranges.push([t, next]);
}
t = next;
}
ranges
}
#[cfg(not(target_arch = "wasm32"))]
fn flush_line(ops: &mut Vec<Op>, pts: &[LinePoint], dot_radius: Option<Pt>) {
if pts.len() < 2 {

View file

@ -786,6 +786,8 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static
"DONUT Specify center of donut (Enter to exit):" => Some(("draw", "donut-specify-center-of-donut-enter-to-exit")),
"DONUT Specify inside diameter <0>:" => Some(("draw", "donut-specify-inside-diameter-0")),
"DONUT Specify outside diameter:" => Some(("draw", "donut-specify-outside-diameter")),
"DONUT Specify second point for inside diameter:" => Some(("draw", "donut-specify-second-point-for-inside-diameter")),
"DONUT Specify second point for outside diameter:" => Some(("draw", "donut-specify-second-point-for-outside-diameter")),
"DRAWORDER Select reference object (move selection above):" => Some(("view", "draworder-select-reference-object-move-selection-above")),
"DRAWORDER Select reference object (move selection under):" => Some(("view", "draworder-select-reference-object-move-selection-under")),
"DRAWORDER [Front / Back] (Above/Under <handle> by typing):" => Some(("view", "draworder-front-back-above-under-handle-by-typ")),
@ -1015,6 +1017,7 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static
"Ext line offset" => Some(("properties", "ext-line-offset")),
"Ext line weight (DIMLWE)" => Some(("draw", "ext-line-weight-dimlwe")),
"Extend" => Some(("modify", "extend")),
"Extended Data" => Some(("properties", "extended-data")),
"Extended Entity" => Some(("properties", "extended-entity")),
"Extended Lighting" => Some(("properties", "extended-lighting")),
"Extension" => Some(("common", "extension")),

View file

@ -222,6 +222,7 @@ impl CadCommand for AlignedDimensionCommand {
Some(WireModel {
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
world_width: 0.0,
depth_override: None,
display_visible: true,
@ -281,6 +282,7 @@ fn preview_aligned(p1: DVec3, p2: DVec3, dim_pt: DVec3) -> WireModel {
WireModel {
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
world_width: 0.0,
depth_override: None,
display_visible: true,

View file

@ -163,6 +163,7 @@ fn preview_wire(points: Vec<Vec3>) -> WireModel {
WireModel {
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
world_width: 0.0,
depth_override: None,
display_visible: true,

View file

@ -194,6 +194,7 @@ fn preview_line(a: Vec3, b: Vec3) -> WireModel {
WireModel {
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
world_width: 0.0,
depth_override: None,
display_visible: true,

View file

@ -182,6 +182,7 @@ impl CadCommand for DimBaselineCommand {
Some(WireModel {
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
world_width: 0.0,
depth_override: None,
display_visible: true,

View file

@ -162,6 +162,7 @@ impl CadCommand for DimContinueCommand {
Some(WireModel {
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
world_width: 0.0,
depth_override: None,
display_visible: true,

View file

@ -88,6 +88,7 @@ impl CadCommand for DimJogLineCommand {
Some(WireModel {
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
world_width: 0.0,
depth_override: None,
display_visible: true,

View file

@ -105,6 +105,7 @@ impl CadCommand for DimTeditCommand {
Some(WireModel {
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
world_width: 0.0,
depth_override: None,
display_visible: true,

View file

@ -302,6 +302,7 @@ fn preview_wire(pts: &[Vec3], arrow_size: f32) -> WireModel {
WireModel {
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
world_width: 0.0,
depth_override: None,
display_visible: true,

View file

@ -239,6 +239,7 @@ fn preview_wire(points: Vec<DVec3>) -> WireModel {
WireModel {
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
world_width: 0.0,
depth_override: None,
display_visible: true,

View file

@ -229,6 +229,7 @@ fn preview_wire(pts: &[Vec3], arrow_size: f32) -> WireModel {
WireModel {
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
world_width: 0.0,
depth_override: None,
display_visible: true,

View file

@ -464,6 +464,7 @@ fn preview_wire(pts: &[Vec3]) -> WireModel {
WireModel {
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
world_width: 0.0,
depth_override: None,
display_visible: true,

View file

@ -139,6 +139,7 @@ fn preview_wire(points: Vec<Vec3>) -> WireModel {
WireModel {
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
world_width: 0.0,
depth_override: None,
display_visible: true,

View file

@ -95,6 +95,7 @@ impl CadCommand for QdimCommand {
Some(WireModel {
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
world_width: 0.0,
depth_override: None,
display_visible: true,

View file

@ -199,6 +199,7 @@ fn preview_wire(points: Vec<Vec3>) -> WireModel {
WireModel {
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
world_width: 0.0,
depth_override: None,
display_visible: true,

View file

@ -201,6 +201,7 @@ impl CadCommand for TableCommand {
Some(WireModel {
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
world_width: 0.0,
depth_override: None,
display_visible: true,

View file

@ -105,6 +105,7 @@ impl CadCommand for ToleranceCommand {
Some(WireModel {
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
world_width: 0.0,
depth_override: None,
display_visible: true,

View file

@ -26,6 +26,8 @@ thread_local! {
static RECT_CHAMFER1: Cell<f64> = Cell::new(0.0);
static RECT_CHAMFER2: Cell<f64> = Cell::new(0.0);
static RECT_FILLET: Cell<f64> = Cell::new(0.0);
static DONUT_INNER_DIAMETER: Cell<f64> = Cell::new(0.5);
static DONUT_OUTER_DIAMETER: Cell<f64> = Cell::new(1.0);
}
macro_rules! accessors {
@ -68,3 +70,13 @@ accessors!(get_rect_rotation, set_rect_rotation, RECT_ROTATION);
accessors!(get_rect_chamfer1, set_rect_chamfer1, RECT_CHAMFER1);
accessors!(get_rect_chamfer2, set_rect_chamfer2, RECT_CHAMFER2);
accessors!(get_rect_fillet, set_rect_fillet, RECT_FILLET);
accessors!(
get_donut_inner_diameter,
set_donut_inner_diameter,
DONUT_INNER_DIAMETER
);
accessors!(
get_donut_outer_diameter,
set_donut_outer_diameter,
DONUT_OUTER_DIAMETER
);

View file

@ -203,6 +203,7 @@ impl CadCommand for AttdefCommand {
Some(WireModel {
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
world_width: 0.0,
depth_override: None,
display_visible: true,

View file

@ -1,21 +1,19 @@
// DONUT command — create a filled circular ring (thick LwPolyline).
//
// A donut is an LwPolyline with:
// - 2 vertices at (cx ± r_avg, 0), both with bulge = 1.0 (two 180° CCW arcs)
// - start_width = end_width = (outer - inner) / 2
// - is_closed = true
//
// Workflow:
// 1. Type inner diameter (or 0 for a filled circle)
// 2. Type outer diameter
// 3. Click center point(s); Enter to finish
// DONUT creates a ring as a closed, constant-width LwPolyline.
use acadrust::entities::{LwPolyline, LwVertex};
use acadrust::EntityType;
use cadkernel::geom2d::{
Circle as KernelCircle, Curve as KernelCurve, Vec2 as KernelVec2,
};
use glam::DVec3;
use crate::t;
use crate::command::{CadCommand, CmdResult, WorkingPlane};
use crate::modules::draw::defaults;
use crate::scene::model::wire_model::WireModel;
const TAU: f64 = std::f64::consts::TAU;
pub struct DonutCommand {
state: DonutState,
@ -24,21 +22,64 @@ pub struct DonutCommand {
plane: WorkingPlane,
}
#[derive(Clone, Copy)]
enum DonutState {
AskInner,
AskInnerSecond(DVec3),
AskOuter,
AskOuterSecond(DVec3),
PlaceCenter,
}
impl DonutCommand {
pub fn new() -> Self {
let mut inner_diameter = defaults::get_donut_inner_diameter();
let mut outer_diameter = defaults::get_donut_outer_diameter();
if inner_diameter > outer_diameter {
std::mem::swap(&mut inner_diameter, &mut outer_diameter);
}
Self {
state: DonutState::AskInner,
inner_r: 0.0,
outer_r: 1.0,
inner_r: inner_diameter * 0.5,
outer_r: outer_diameter * 0.5,
plane: WorkingPlane::default(),
}
}
fn set_inner_diameter(&mut self, diameter: f64) {
self.inner_r = diameter * 0.5;
self.state = DonutState::AskOuter;
}
fn set_outer_diameter(&mut self, diameter: f64) {
self.outer_r = diameter * 0.5;
if self.inner_r > self.outer_r {
std::mem::swap(&mut self.inner_r, &mut self.outer_r);
}
defaults::set_donut_inner_diameter(self.inner_r * 2.0);
defaults::set_donut_outer_diameter(self.outer_r * 2.0);
self.state = DonutState::PlaceCenter;
}
fn point_distance(&self, first: DVec3, second: DVec3) -> f64 {
let first = self.plane.to_local(first);
let second = self.plane.to_local(second);
KernelVec2::new(first.x, first.y).distance(KernelVec2::new(second.x, second.y))
}
fn project_to_plane(&self, point: DVec3) -> DVec3 {
let local = self.plane.to_local(point);
self.plane.to_world(DVec3::new(local.x, local.y, 0.0))
}
}
fn prompt_with_default(prompt: &str, value: f64) -> String {
match prompt.char_indices().next_back() {
Some((index, suffix @ (':' | ''))) => {
format!("{} <{value:.4}>{suffix}", &prompt[..index])
}
_ => format!("{prompt} <{value:.4}>"),
}
}
impl CadCommand for DonutCommand {
@ -51,9 +92,19 @@ impl CadCommand for DonutCommand {
}
fn prompt(&self) -> String {
match &self.state {
DonutState::AskInner => t!("DONUT Specify inside diameter <0>:").into_owned(),
DonutState::AskOuter => t!("DONUT Specify outside diameter:").into_owned(),
match self.state {
DonutState::AskInner => t!("DONUT Specify inside diameter <0>:")
.replace("<0>", &format!("<{:.4}>", self.inner_r * 2.0)),
DonutState::AskInnerSecond(_) => {
t!("DONUT Specify second point for inside diameter:").into_owned()
}
DonutState::AskOuter => prompt_with_default(
t!("DONUT Specify outside diameter:").as_ref(),
self.outer_r * 2.0,
),
DonutState::AskOuterSecond(_) => {
t!("DONUT Specify second point for outside diameter:").into_owned()
}
DonutState::PlaceCenter => {
t!("DONUT Specify center of donut (Enter to exit):").into_owned()
}
@ -78,22 +129,20 @@ impl CadCommand for DonutCommand {
.replace(',', ".")
.parse()
.ok()
.filter(|&v: &f64| v >= 0.0)?;
match &self.state {
.filter(|v: &f64| v.is_finite())?;
match self.state {
DonutState::AskInner => {
self.inner_r = val / 2.0;
self.state = DonutState::AskOuter;
if val < 0.0 {
return Some(CmdResult::NeedPoint);
}
self.set_inner_diameter(val);
Some(CmdResult::NeedPoint)
}
DonutState::AskOuter => {
if val <= 0.0 {
return None;
return Some(CmdResult::NeedPoint);
}
self.outer_r = val / 2.0;
if self.inner_r > self.outer_r {
std::mem::swap(&mut self.inner_r, &mut self.outer_r);
}
self.state = DonutState::PlaceCenter;
self.set_outer_diameter(val);
Some(CmdResult::NeedPoint)
}
_ => None,
@ -101,29 +150,107 @@ impl CadCommand for DonutCommand {
}
fn on_point(&mut self, pt: DVec3) -> CmdResult {
match &self.state {
match self.state {
DonutState::AskInner => {
self.state = DonutState::AskInnerSecond(self.project_to_plane(pt));
CmdResult::NeedPoint
}
DonutState::AskInnerSecond(first) => {
let diameter = self.point_distance(first, pt);
if diameter <= 1.0e-9 {
return CmdResult::NeedPoint;
}
self.set_inner_diameter(diameter);
CmdResult::NeedPoint
}
DonutState::AskOuter => {
self.state = DonutState::AskOuterSecond(self.project_to_plane(pt));
CmdResult::NeedPoint
}
DonutState::AskOuterSecond(first) => {
let diameter = self.point_distance(first, pt);
if diameter <= 1.0e-9 {
return CmdResult::NeedPoint;
}
self.set_outer_diameter(diameter);
CmdResult::NeedPoint
}
DonutState::PlaceCenter => {
let center = self.plane.to_local(pt);
let entity = make_donut(center.x, center.y, center.z, self.inner_r, self.outer_r);
// Keep command active so user can place more donuts.
CmdResult::CommitEntity(self.plane.place_entity(entity))
}
_ => CmdResult::NeedPoint,
}
}
fn on_enter(&mut self) -> CmdResult {
match &self.state {
match self.state {
DonutState::AskInner => {
// Accept default 0 for inner diameter
self.inner_r = 0.0;
self.state = DonutState::AskOuter;
CmdResult::NeedPoint
}
DonutState::AskOuter => {
self.set_outer_diameter(self.outer_r * 2.0);
CmdResult::NeedPoint
}
DonutState::PlaceCenter => CmdResult::Cancel,
_ => CmdResult::Cancel,
}
}
fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> {
match self.state {
DonutState::AskInnerSecond(first) | DonutState::AskOuterSecond(first) => {
let point = self.project_to_plane(pt);
Some(WireModel::solid_f64(
"rubber_band".into(),
vec![first.to_array(), point.to_array()],
WireModel::CYAN,
false,
))
}
DonutState::PlaceCenter => Some(donut_wire(
pt,
self.inner_r,
self.outer_r,
self.plane,
)),
_ => None,
}
}
}
fn donut_wire(
center: DVec3,
inner_r: f64,
outer_r: f64,
plane: WorkingPlane,
) -> WireModel {
let local_center = plane.to_local(center);
let mut points = Vec::new();
for radius in [outer_r, inner_r] {
if radius <= 1.0e-9 {
continue;
}
if !points.is_empty() {
points.push([f64::NAN; 3]);
}
points.extend(
KernelCurve::Circle(KernelCircle {
centre: [local_center.x, local_center.y],
radius,
})
.tessellate_angle(TAU / 64.0)
.into_iter()
.map(|point| {
plane
.to_world(DVec3::new(point[0], point[1], local_center.z))
.to_array()
}),
);
}
WireModel::solid_f64("rubber_band".into(), points, WireModel::CYAN, false)
}
fn make_donut(cx: f64, cy: f64, elevation: f64, inner_r: f64, outer_r: f64) -> EntityType {
@ -136,17 +263,12 @@ fn make_donut(cx: f64, cy: f64, elevation: f64, inner_r: f64, outer_r: f64) -> E
p.constant_width = width;
p.elevation = elevation;
// Vertex at (cx - r, cy) with bulge 1.0 (180° CCW arc to next vertex)
// Segment widths inherit the polyline's constant width.
let mut v0 = LwVertex::new(Vector2::new(cx - r_avg, cy));
v0.bulge = 1.0;
v0.start_width = width;
v0.end_width = width;
// Vertex at (cx + r, cy) with bulge 1.0 (second 180° arc back to v0)
let mut v1 = LwVertex::new(Vector2::new(cx + r_avg, cy));
v1.bulge = 1.0;
v1.start_width = width;
v1.end_width = width;
p.vertices = vec![v0, v1];
EntityType::LwPolyline(p)

View file

@ -182,6 +182,7 @@ impl MlineCommand {
Some(WireModel {
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
world_width: 0.0,
depth_override: None,
display_visible: true,

View file

@ -125,6 +125,7 @@ impl CadCommand for ImageCommand {
Some(WireModel {
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
world_width: 0.0,
depth_override: None,
display_visible: true,

View file

@ -87,6 +87,7 @@ impl CadCommand for RayCommand {
Some(WireModel {
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
world_width: 0.0,
depth_override: None,
display_visible: true,
@ -190,6 +191,7 @@ impl CadCommand for XLineCommand {
Some(WireModel {
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
world_width: 0.0,
depth_override: None,
display_visible: true,

View file

@ -364,6 +364,7 @@ impl CadCommand for AreaCommand {
Some(WireModel {
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
world_width: 0.0,
depth_override: None,
display_visible: true,

View file

@ -87,6 +87,7 @@ impl CadCommand for DistCommand {
Some(WireModel {
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
world_width: 0.0,
depth_override: None,
display_visible: true,

View file

@ -72,6 +72,7 @@ impl MeasureGeomCommand {
WireModel {
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
world_width: 0.0,
depth_override: None,
display_visible: true,

View file

@ -3721,6 +3721,7 @@ fn preview_wire(points: Vec<[f32; 3]>, color: [f32; 4], name: &str) -> WireModel
WireModel {
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
world_width: 0.0,
depth_override: None,
display_visible: true,

View file

@ -541,6 +541,7 @@ fn wire(name: &str, points: Vec<[f32; 3]>) -> WireModel {
WireModel {
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
world_width: 0.0,
depth_override: None,
display_visible: true,

View file

@ -56,6 +56,7 @@ impl CadCommand for PlotWindowCommand {
Some(WireModel {
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
world_width: 0.0,
depth_override: None,
display_visible: true,

View file

@ -138,6 +138,7 @@ impl CadCommand for ZoomWindowCommand {
Some(WireModel {
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
world_width: 0.0,
depth_override: None,
display_visible: true,

View file

@ -23,7 +23,10 @@ use acadrust::{CadDocument, EntityType, Handle};
use cadkernel::space::{Plane as KernelPlane, Vec3 as KernelVec3};
use crate::scene::convert::tessellate;
use crate::scene::model::wire_model::{PointMarker, SnapHint, TangentGeom, WireModel};
use crate::scene::model::wire_model::{
decode_pattern_station_map, encode_pattern_stations, pattern_station_values, PatternStationMap,
PatternStationPiece, PointMarker, SnapHint, TangentGeom, WireModel,
};
const MAX_NESTING_DEPTH: usize = 32;
/// Skip wires whose world-AABB projects to fewer than this many pixels in
@ -70,6 +73,7 @@ pub struct LocalWire {
pub pattern_length: f32,
pub pattern: [f32; 8],
pub line_weight_px: f32,
pub pattern_stations: Vec<f32>,
/// World-space band width for a wide polyline (see `WireModel.world_width`).
/// Block-local; the expand-time transform scales it by the insert so the
/// shader band grows with a scaled insert. `0.0` = a normal wire.
@ -865,6 +869,7 @@ fn tessellate_sub_local(
pattern_length: pat_len,
pattern: pat,
line_weight_px: lw_px,
pattern_stations: wire.pattern_stations,
world_width: wire.world_width,
plinegen: wire.plinegen,
plot_visible: frame_mode.is_none_or(|mode| mode == 1)
@ -1433,6 +1438,7 @@ struct StyleKey {
pattern_length: u32,
pattern: [u32; 8],
line_weight_px: u32,
source_length: u32,
/// Wide-polyline band width (bit-cast). Keeps bands of different widths — and
/// bands vs thin wires of the same colour/style — in separate batches so the
/// finalized WireModel carries one correct `world_width`.
@ -1475,6 +1481,10 @@ struct BatchEntry {
pattern_length: f32,
pattern: [f32; 8],
line_weight_px: f32,
pattern_stations: Vec<f32>,
pattern_station_segments: Vec<i32>,
pattern_station_pieces: Vec<PatternStationPiece>,
source_length: f32,
world_width: f32,
point_marker: Option<PointMarker>,
aci: u8,
@ -1555,6 +1565,7 @@ impl BatchEntry {
pat_len: f32,
pat: [f32; 8],
lw_px: f32,
source_length: f32,
world_width: f32,
point_marker: Option<PointMarker>,
aci: u8,
@ -1578,6 +1589,7 @@ impl BatchEntry {
pattern_length: pat_len,
pattern: pat,
line_weight_px: lw_px,
source_length,
world_width,
point_marker,
aci,
@ -1606,6 +1618,9 @@ impl Batches {
if b.hide_unselected && !selected {
b.points.clear();
b.points_low.clear();
b.pattern_stations.clear();
b.pattern_station_segments.clear();
b.pattern_station_pieces.clear();
}
let aabb = if b.min_x.is_infinite() {
WireModel::UNBOUNDED_AABB
@ -1628,9 +1643,18 @@ impl Batches {
);
}
}
if !b.pattern_stations.is_empty() {
b.pattern_stations = encode_pattern_stations(
std::mem::take(&mut b.pattern_stations),
b.source_length,
&b.pattern_station_segments,
&b.pattern_station_pieces,
);
}
WireModel {
point_marker: b.point_marker,
taper_widths: Vec::new(),
pattern_stations: b.pattern_stations,
world_width: b.world_width,
depth_override: b.local_depth,
display_visible: !b.hide_unselected || selected,
@ -1681,6 +1705,7 @@ fn style_key(
pat_len: f32,
pat: [f32; 8],
lw_px: f32,
source_length: f32,
world_width: f32,
point_marker: Option<PointMarker>,
aci: u8,
@ -1714,6 +1739,7 @@ fn style_key(
pat[7].to_bits(),
],
line_weight_px: lw_px.to_bits(),
source_length: source_length.to_bits(),
world_width: world_width.to_bits(),
point_marker: point_marker.map(|marker| PointMarkerKey {
origin: marker.origin.to_array().map(f64::to_bits),
@ -2060,6 +2086,116 @@ fn transformed_wire_length_scale(lw: &LocalWire, xform: &Transform) -> f32 {
}
}
struct TransformedStationData {
values: Vec<f32>,
source_length: f32,
point_segments: Vec<i32>,
pieces: Vec<PatternStationPiece>,
}
fn transformed_station_data(
values: &[f32],
map: &PatternStationMap,
plinegen: bool,
xform: &Transform,
) -> Option<TransformedStationData> {
if values.len() != map.point_segments.len() || map.pieces.is_empty() {
return None;
}
let mut pieces = Vec::with_capacity(map.pieces.len());
let mut global = 0.0_f32;
let mut local = 0.0_f32;
let mut current_segment = None;
for piece in &map.pieces {
if current_segment != Some(piece.source_segment) {
current_segment = Some(piece.source_segment);
local = 0.0;
}
let vector = Vector3::new(
piece.vector[0] as f64,
piece.vector[1] as f64,
piece.vector[2] as f64,
);
let mapped = xform.matrix.transform_direction(vector);
let chord = vector.length();
let mapped_chord = mapped.length();
let original_length = (piece.source_distances[1] - piece.source_distances[0]).abs();
let length = if chord > 1e-12 && mapped_chord.is_finite() {
original_length * (mapped_chord / chord) as f32
} else {
original_length
};
pieces.push(PatternStationPiece {
source_segment: piece.source_segment,
source_distances: [global, global + length],
segment_distances: [local, local + length],
vector: [mapped.x as f32, mapped.y as f32, mapped.z as f32],
});
global += length;
local += length;
}
let fallback_scale = map
.pieces
.last()
.map(|piece| piece.source_distances[1])
.filter(|length| length.abs() > 1e-12)
.map_or(1.0, |length| global / length);
let mapped_values = values
.iter()
.zip(&map.point_segments)
.map(|(&value, &segment)| {
if segment < 0 {
return 0.0;
}
let segment = segment as u32;
let range = |piece: &PatternStationPiece| {
if plinegen {
piece.source_distances
} else {
piece.segment_distances
}
};
let distance_to = |piece: &PatternStationPiece| {
let [start, end] = range(piece);
if value < start {
start - value
} else if value > end {
value - end
} else {
0.0
}
};
let Some((old_piece, new_piece)) = map
.pieces
.iter()
.zip(&pieces)
.filter(|(piece, _)| piece.source_segment == segment)
.min_by(|(a, _), (b, _)| distance_to(a).total_cmp(&distance_to(b)))
else {
return value * fallback_scale;
};
let old = range(old_piece);
let new = range(new_piece);
let span = old[1] - old[0];
let t = if span.abs() > 1e-12 {
((value - old[0]) / span).clamp(0.0, 1.0)
} else {
0.0
};
new[0] + (new[1] - new[0]) * t
})
.collect();
Some(TransformedStationData {
values: mapped_values,
source_length: global,
point_segments: map.point_segments.clone(),
pieces,
})
}
fn transformed_point_marker(marker: PointMarker, xform: &Transform) -> (PointMarker, f64) {
let origin = xform.apply(Vector3::new(marker.origin.x, marker.origin.y, marker.origin.z));
let map_direction = |direction: glam::DVec3| {
@ -2163,16 +2299,38 @@ fn emit_wire(
lw.line_weight_px
};
// Pattern distances are stored in block-local units. Scale them by the
// wire's actual path-length ratio so uniform and non-uniform INSERTs both
// stay dimensionally consistent with their transformed geometry.
let pattern_scale = if final_pat_len > 0.0 {
let station_values = pattern_station_values(&lw.pattern_stations, lw.points.len());
let station_map = decode_pattern_station_map(&lw.pattern_stations, lw.points.len());
let transformed_stations = station_values.and_then(|(values, _)| {
station_map.as_ref().and_then(|map| {
transformed_station_data(values, map, lw.plinegen, accum_xform)
})
});
let has_stations = station_values.is_some();
let pattern_scale = transformed_stations.as_ref().map_or_else(
|| {
if final_pat_len > 0.0 || has_stations {
transformed_wire_length_scale(lw, accum_xform)
} else {
1.0
};
}
},
|data| {
station_values.map_or(1.0, |(_, source_length)| {
if source_length.abs() > 1e-12 {
data.source_length / source_length
} else {
1.0
}
})
},
);
let final_pat_len = final_pat_len * ctx.pslt_factor * pattern_scale;
let final_pat = final_pat.map(|v| v * ctx.pslt_factor * pattern_scale);
let source_length = transformed_stations.as_ref().map_or_else(
|| station_values.map_or(0.0, |(_, length)| length * pattern_scale),
|data| data.source_length,
);
// A wide polyline's band width is baked in block-local units; scale it by
// the insert transform so the shader band matches the scaled geometry.
@ -2211,6 +2369,7 @@ fn emit_wire(
final_pat_len,
final_pat,
final_lw_px,
source_length,
final_world_width,
point_marker,
final_aci,
@ -2223,6 +2382,12 @@ fn emit_wire(
local_depth,
);
if transformed_stations.is_some() {
if let Some(closed) = out.by_style.remove(&key) {
out.closed.push(closed);
}
}
// If the open batch for this style would exceed wgpu's per-buffer limit
// after appending this wire, finalize it now and start a fresh batch.
if let Some(existing) = out.by_style.get(&key) {
@ -2241,6 +2406,7 @@ fn emit_wire(
final_pat_len,
final_pat,
final_lw_px,
source_length,
final_world_width,
point_marker,
final_aci,
@ -2253,6 +2419,9 @@ fn emit_wire(
)
});
entry.local_depth = local_depth;
if let Some(data) = &transformed_stations {
entry.pattern_station_pieces.clone_from(&data.pieces);
}
// NaN separator between previously-appended geometry and this wire so the
// GPU shader treats them as disconnected polylines within one buffer.
@ -2263,14 +2432,33 @@ fn emit_wire(
if needs_sep {
entry.points.push([f32::NAN; 3]);
entry.points_low.push([0.0; 3]);
if has_stations {
entry.pattern_stations.push(0.0);
}
if transformed_stations.is_some() {
entry.pattern_station_segments.push(-1);
}
}
// Iterate paired with the matching low residual so the GPU keeps
// sub-f32 precision once the INSERT transform lands the wire in
// world space at UTM-scale coordinates.
for (idx, p) in lw.points.iter().enumerate() {
let station = transformed_stations
.as_ref()
.map(|data| data.values[idx])
.or_else(|| has_stations.then(|| lw.pattern_stations[idx] * pattern_scale));
let station_segment = transformed_stations
.as_ref()
.map(|data| data.point_segments[idx]);
if p[0].is_nan() {
entry.points.push([f32::NAN; 3]);
entry.points_low.push([0.0; 3]);
if let Some(station) = station {
entry.pattern_stations.push(station);
}
if let Some(segment) = station_segment {
entry.pattern_station_segments.push(segment);
}
continue;
}
let pl = lw.points_low.get(idx).copied().unwrap_or([0.0; 3]);
@ -2311,6 +2499,12 @@ fn emit_wire(
}
entry.points.push(q);
entry.points_low.push([qx_l, qy_l, qz_l]);
if let Some(station) = station {
entry.pattern_stations.push(station);
}
if let Some(segment) = station_segment {
entry.pattern_station_segments.push(segment);
}
}
}

View file

@ -61,6 +61,15 @@ pub enum RenderObject {
Lines(Vec<[f64; 3]>),
/// Like Lines but linetype pattern restarts at each NaN-separated segment (plinegen=false).
SegmentedLines(Vec<[f64; 3]>),
/// Kernel band boundary with centreline-based linetype stations.
BoundaryLines {
points: Vec<[f64; 3]>,
stations: Vec<f32>,
point_segments: Vec<i32>,
station_pieces: Vec<crate::scene::model::wire_model::PatternStationPiece>,
source_length: f32,
plinegen: bool,
},
/// A wide polyline whose band width VARIES (a taper): a continuous WCS point
/// list paired index-for-index with a per-point full band width. The wire
/// shader interpolates the two endpoint widths of each segment so the band

View file

@ -750,6 +750,7 @@ fn tessellate_entity_inner(
return vec![WireModel {
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
world_width: 0.0,
depth_override: None,
display_visible: true,
@ -1110,6 +1111,7 @@ fn tessellate_entity_inner(
let marker = WireModel {
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
world_width: 0.0,
depth_override: None,
display_visible: true,
@ -1508,6 +1510,7 @@ fn lod_stub_wire(
WireModel {
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
world_width: 0.0,
depth_override: None,
display_visible: true,
@ -1602,6 +1605,7 @@ fn lod_stub_wire_3d(
WireModel {
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
world_width: 0.0,
depth_override: None,
display_visible: true,

View file

@ -147,6 +147,7 @@ fn point_cloud_wires(
let mut wires = vec![WireModel {
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
world_width: 0.0,
depth_override: None,
display_visible: true,
@ -400,6 +401,7 @@ pub fn tessellate(
out.push(WireModel {
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
world_width: 0.0,
depth_override: None,
display_visible: true,
@ -562,6 +564,7 @@ pub fn tessellate(
elem_wires.push(WireModel {
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
world_width: 0.0,
depth_override: None,
display_visible: true,
@ -870,6 +873,7 @@ pub fn tessellate(
wires.push(WireModel {
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
world_width: 0.0,
depth_override: None,
display_visible: true,
@ -920,6 +924,7 @@ pub fn tessellate(
wires.push(WireModel {
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
world_width: 0.0,
depth_override: None,
display_visible: true,
@ -972,6 +977,7 @@ pub fn tessellate(
wires.push(WireModel {
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
world_width: 0.0,
depth_override: None,
display_visible: true,
@ -1005,6 +1011,7 @@ pub fn tessellate(
wires.push(WireModel {
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
world_width: 0.0,
depth_override: None,
display_visible: true,
@ -1068,6 +1075,7 @@ pub fn tessellate(
out.push(WireModel {
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
world_width: 0.0,
depth_override: None,
display_visible: true,
@ -1113,6 +1121,7 @@ pub fn tessellate(
out.push(WireModel {
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
world_width: 0.0,
depth_override: None,
display_visible: true,
@ -1155,6 +1164,7 @@ pub fn tessellate(
out.push(WireModel {
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
world_width: 0.0,
depth_override: None,
display_visible: true,
@ -1190,6 +1200,7 @@ pub fn tessellate(
out.push(WireModel {
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
world_width: 0.0,
depth_override: None,
display_visible: true,
@ -1259,6 +1270,7 @@ pub fn tessellate(
return vec![WireModel {
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
world_width: 0.0,
depth_override: None,
display_visible: true,
@ -1355,8 +1367,7 @@ pub fn tessellate(
} else {
color
};
// Basic curve entities use the Lines path for large-coordinate precision,
// but they must still preserve the entity's resolved linetype pattern.
// Basic curves keep their resolved linetype.
let (edge_pattern_length, edge_pattern) =
if matches!(
entity,
@ -1385,7 +1396,8 @@ pub fn tessellate(
out.push(WireModel {
point_marker,
taper_widths: Vec::new(),
world_width: polyline_band_width(entity),
pattern_stations: Vec::new(),
world_width: polyline_band_width(entity, document.header.fill_mode),
depth_override: None,
display_visible: true,
plot_visible: true,
@ -1429,6 +1441,7 @@ pub fn tessellate(
out.push(WireModel {
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
world_width: 0.0,
pick_tris: Vec::new(),
pick_tris_low: Vec::new(),
@ -1464,6 +1477,7 @@ pub fn tessellate(
out.push(WireModel {
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
world_width: 0.0,
depth_override: None,
display_visible: true,
@ -1498,6 +1512,61 @@ pub fn tessellate(
return out;
}
RenderObject::BoundaryLines {
points,
stations,
point_segments,
station_pieces,
source_length,
plinegen,
} => {
let (local_pts, local_pts_low) = points_to_ds(points);
let key_vertices = te
.key_vertices
.into_iter()
.map(|[x, y, z]| [x, y, z])
.collect();
let station_data = crate::scene::model::wire_model::encode_pattern_stations(
stations,
source_length,
&point_segments,
&station_pieces,
);
return vec![WireModel {
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: station_data,
world_width: 0.0,
depth_override: None,
display_visible: true,
plot_visible: true,
fill_is_3d: false,
fill_is_2d_solid: false,
render_instance: None,
pick_tris: Vec::new(),
pick_tris_low: Vec::new(),
dash_from_start: false,
dash_align_end: None,
text_verts: Vec::new(),
name,
points: local_pts,
points_low: local_pts_low,
color,
selected,
pattern_length,
pattern,
line_weight_px,
snap_pts: te.snap_pts,
tangent_geoms: te.tangent_geoms,
aci: 0,
key_vertices,
plinegen,
aabb: WireModel::UNBOUNDED_AABB,
fill_tris: Vec::new(),
fill_tris_low: Vec::new(),
}];
}
RenderObject::SegmentedLines(points) => {
let (local_pts, local_pts_low) = points_to_ds(points);
let snap_pts = te.snap_pts;
@ -1512,7 +1581,8 @@ pub fn tessellate(
return vec![WireModel {
point_marker: None,
taper_widths: Vec::new(),
world_width: polyline_band_width(entity),
pattern_stations: Vec::new(),
world_width: polyline_band_width(entity, document.header.fill_mode),
depth_override: None,
display_visible: true,
plot_visible: true,
@ -1561,6 +1631,7 @@ pub fn tessellate(
return vec![WireModel {
point_marker: None,
taper_widths: widths,
pattern_stations: Vec::new(),
world_width,
depth_override: None,
display_visible: true,
@ -1683,6 +1754,7 @@ pub fn tessellate(
vec![WireModel {
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
world_width: 0.0,
depth_override: None,
display_visible: true,
@ -2140,14 +2212,11 @@ pub(crate) fn entity_z(entity: &EntityType) -> f32 {
}
}
/// Band width (drawing units) of a wide polyline whose flat band is drawn by
/// expanding its centre-line wire in the shader. `0.0` = a zero-width polyline,
/// a non-polyline (a normal screen-pixel wire), or a thickness-extruded one — an
/// LwPolyline thickens into a 3-D tube (`thick_wide_band`) and a Polyline2D
/// keeps its hatch band (only its centre-line extrudes; no tube yet), so neither
/// takes the flat shader band. A tapering polyline (per-vertex start/end widths)
/// collapses to its widest edge — the shader carries one width per wire.
fn polyline_band_width(entity: &EntityType) -> f32 {
/// Shader band width, or zero while FILLMODE draws the kernel boundary.
fn polyline_band_width(entity: &EntityType, fill_mode: bool) -> f32 {
if !fill_mode {
return 0.0;
}
let w = match entity {
EntityType::LwPolyline(p) if p.thickness.abs() <= 1e-10 => {
let mut w = p.constant_width;

View file

@ -59,6 +59,72 @@ pub struct PointMarker {
pub viewport_percent: f32,
}
#[derive(Clone, Copy, Debug)]
pub struct PatternStationPiece {
pub source_segment: u32,
pub source_distances: [f32; 2],
pub segment_distances: [f32; 2],
pub vector: [f32; 3],
}
#[derive(Clone, Debug)]
pub(crate) struct PatternStationMap {
pub point_segments: Vec<i32>,
pub pieces: Vec<PatternStationPiece>,
}
pub(crate) fn pattern_station_values(data: &[f32], point_count: usize) -> Option<(&[f32], f32)> {
(data.len() >= point_count + 1).then(|| (&data[..point_count], data[point_count]))
}
pub(crate) fn encode_pattern_stations(
mut stations: Vec<f32>,
source_length: f32,
point_segments: &[i32],
pieces: &[PatternStationPiece],
) -> Vec<f32> {
let point_count = stations.len();
stations.push(source_length);
if point_segments.len() != point_count || pieces.is_empty() {
return stations;
}
stations.extend(point_segments.iter().map(|segment| *segment as f32));
for piece in pieces {
stations.push(piece.source_segment as f32);
stations.extend(piece.source_distances);
stations.extend(piece.segment_distances);
stations.extend(piece.vector);
}
stations
}
pub(crate) fn decode_pattern_station_map(
data: &[f32],
point_count: usize,
) -> Option<PatternStationMap> {
let metadata_start = point_count.checked_mul(2)?.checked_add(1)?;
if data.len() < metadata_start || (data.len() - metadata_start) % 8 != 0 {
return None;
}
let point_segments = data[point_count + 1..metadata_start]
.iter()
.map(|value| *value as i32)
.collect();
let pieces = data[metadata_start..]
.chunks_exact(8)
.map(|values| PatternStationPiece {
source_segment: values[0] as u32,
source_distances: [values[1], values[2]],
segment_distances: [values[3], values[4]],
vector: [values[5], values[6], values[7]],
})
.collect();
Some(PatternStationMap {
point_segments,
pieces,
})
}
impl PointMarker {
pub fn resolve(self, point: glam::DVec3, view_height: f64) -> glam::DVec3 {
let normal = self.normal.normalize_or(glam::DVec3::Z);
@ -111,6 +177,8 @@ pub struct WireModel {
/// constant band of `world_width`. The wire shader reads the two endpoint
/// widths of each segment and interpolates, so the band tapers smoothly.
pub taper_widths: Vec<f32>,
/// Centreline stations and optional source-piece transform metadata.
pub pattern_stations: Vec<f32>,
/// ACI color index (1-255). 0 means true-color or unknown (no CTB lookup).
pub aci: u8,
/// Pre-baked snap candidates (Center, Node, Quadrant, Insertion).
@ -249,6 +317,7 @@ impl WireModel {
Self {
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
world_width: 0.0,
depth_override: None,
display_visible: true,
@ -625,6 +694,7 @@ impl Default for WireModel {
line_weight_px: 1.0,
world_width: 0.0,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
aci: 0,
snap_pts: Vec::new(),
tangent_geoms: Vec::new(),

View file

@ -16,7 +16,9 @@ use acadrust::types::{Handle, Transform, Vector3};
use acadrust::CadDocument;
use cadkernel::geom2d::{contains, Curve, Line, Tolerance};
use crate::scene::model::wire_model::WireModel;
use crate::scene::model::wire_model::{
decode_pattern_station_map, encode_pattern_stations, pattern_station_values, WireModel,
};
const NAN3: [f32; 3] = [f32::NAN, f32::NAN, f32::NAN];
@ -144,6 +146,7 @@ pub fn clip_wires(wires: &mut Vec<WireModel>, poly: &[[f64; 2]]) {
if keep_marker == Some(false) {
w.points.clear();
w.points_low.clear();
w.pattern_stations.clear();
w.point_marker = None;
} else if keep_marker.is_none() && !w.points.is_empty() {
// Absolute → local f32 (NaN separators preserved).
@ -158,7 +161,15 @@ pub fn clip_wires(wires: &mut Vec<WireModel>, poly: &[[f64; 2]]) {
[(a[0] - rx) as f32, (a[1] - ry) as f32, a[2] as f32]
})
.collect();
let clipped = clip_polyline(&local, &lpoly);
let station_data = pattern_station_values(&w.pattern_stations, w.points.len());
let source_length = station_data.map(|(_, source_length)| source_length);
let station_map = decode_pattern_station_map(&w.pattern_stations, w.points.len());
let (clipped, clipped_stations, clipped_segments) = clip_polyline_stationed(
&local,
&lpoly,
station_data.map(|(stations, _)| stations),
station_map.as_ref().map(|map| map.point_segments.as_slice()),
);
// Local → absolute, re-split into double-single high/low.
let mut hi = Vec::with_capacity(clipped.len());
let mut lo = Vec::with_capacity(clipped.len());
@ -176,6 +187,16 @@ pub fn clip_wires(wires: &mut Vec<WireModel>, poly: &[[f64; 2]]) {
}
w.points = hi;
w.points_low = lo;
if let Some(source_length) = source_length {
w.pattern_stations = encode_pattern_stations(
clipped_stations,
source_length,
&clipped_segments,
station_map.as_ref().map_or(&[], |map| map.pieces.as_slice()),
);
} else {
w.pattern_stations.clear();
}
}
if !w.fill_tris.is_empty() {
let local: Vec<[f32; 3]> = (0..w.fill_tris.len())
@ -277,6 +298,7 @@ pub fn frame_wire(
WireModel {
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
world_width: 0.0,
depth_override: None,
display_visible: true,
@ -438,8 +460,20 @@ fn point_in_poly(x: f32, y: f32, poly: &[[f32; 2]]) -> bool {
/// Clip a NaN-separated polyline to `poly`, returning a NaN-separated polyline
/// of only the inside portions.
#[cfg(test)]
fn clip_polyline(pts: &[[f32; 3]], poly: &[[f32; 2]]) -> Vec<[f32; 3]> {
clip_polyline_stationed(pts, poly, None, None).0
}
fn clip_polyline_stationed(
pts: &[[f32; 3]],
poly: &[[f32; 2]],
stations: Option<&[f32]>,
point_segments: Option<&[i32]>,
) -> (Vec<[f32; 3]>, Vec<f32>, Vec<i32>) {
let mut out: Vec<[f32; 3]> = Vec::new();
let mut out_stations = Vec::new();
let mut out_segments = Vec::new();
let mut i = 0;
while i < pts.len() {
if !pts[i][0].is_finite() || !pts[i][1].is_finite() {
@ -453,32 +487,80 @@ fn clip_polyline(pts: &[[f32; 3]], poly: &[[f32; 2]]) -> Vec<[f32; 3]> {
let seg = &pts[start..i];
let mut last: Option<[f32; 3]> = None;
for j in 0..seg.len().saturating_sub(1) {
for (a, b) in clip_segment(seg[j], seg[j + 1], poly) {
for (ta, tb) in clip_segment_params(seg[j], seg[j + 1], poly) {
let lerp_point = |t: f32| {
[
seg[j][0] + (seg[j + 1][0] - seg[j][0]) * t,
seg[j][1] + (seg[j + 1][1] - seg[j][1]) * t,
seg[j][2] + (seg[j + 1][2] - seg[j][2]) * t,
]
};
let a = lerp_point(ta);
let b = lerp_point(tb);
let station = |t: f32| {
stations.map_or(0.0, |values| {
let a = values[start + j];
a + (values[start + j + 1] - a) * t
})
};
let contiguous = last.is_some_and(|l| {
(l[0] - a[0]).abs() <= 1e-4 && (l[1] - a[1]).abs() <= 1e-4
});
if !contiguous {
if !out.is_empty() {
out.push(NAN3);
if stations.is_some() {
out_stations.push(0.0);
}
if point_segments.is_some() {
out_segments.push(-1);
}
}
out.push(a);
if stations.is_some() {
out_stations.push(station(ta));
}
if let Some(segments) = point_segments {
out_segments.push(segments[start + j]);
}
}
out.push(b);
if stations.is_some() {
out_stations.push(station(tb));
}
if let Some(segments) = point_segments {
out_segments.push(segments[start + j]);
}
last = Some(b);
}
}
}
out
(out, out_stations, out_segments)
}
/// Return the inside-the-polygon sub-segments of `p0`→`p1` as endpoint pairs.
/// Handles convex and concave boundaries by testing the midpoint of every
/// interval between consecutive boundary crossings.
#[cfg(test)]
fn clip_segment(
p0: [f32; 3],
p1: [f32; 3],
poly: &[[f32; 2]],
) -> Vec<([f32; 3], [f32; 3])> {
let lerp = |t: f32| {
[
p0[0] + (p1[0] - p0[0]) * t,
p0[1] + (p1[1] - p0[1]) * t,
p0[2] + (p1[2] - p0[2]) * t,
]
};
clip_segment_params(p0, p1, poly)
.into_iter()
.map(|(a, b)| (lerp(a), lerp(b)))
.collect()
}
fn clip_segment_params(p0: [f32; 3], p1: [f32; 3], poly: &[[f32; 2]]) -> Vec<(f32, f32)> {
let mut ts: Vec<f32> = vec![0.0, 1.0];
let n = poly.len();
let mut j = n - 1;
@ -493,7 +575,7 @@ fn clip_segment(
ts.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
ts.dedup_by(|a, b| (*a - *b).abs() < 1e-7);
let lerp = |t: f32| {
let point = |t: f32| {
[
p0[0] + (p1[0] - p0[0]) * t,
p0[1] + (p1[1] - p0[1]) * t,
@ -502,9 +584,9 @@ fn clip_segment(
};
let mut out = Vec::new();
for w in ts.windows(2) {
let mid = lerp(0.5 * (w[0] + w[1]));
let mid = point(0.5 * (w[0] + w[1]));
if point_in_poly(mid[0], mid[1], poly) {
out.push((lerp(w[0]), lerp(w[1])));
out.push((w[0], w[1]));
}
}
out

View file

@ -25,8 +25,7 @@ pub struct Uniforms {
/// zooming inside MSPACE changes one scalar instead of re-tessellating and
/// re-uploading every dashed wire.
pub linetype_scale: f32,
/// FILLMODE toggle. Wire shaders use this slot to draw wide polylines as
/// boundary outlines when fills are disabled.
/// Pads the struct to the uniform alignment required by wgpu.
pub _pad: f32,
// ── Relative-to-eye (double-single) additions ───────────────────────────

View file

@ -406,20 +406,23 @@ fn pack_color(color: [f32; 4]) -> [u8; 4] {
/// non-dash-first patterns keep the legacy centred phase.
fn wire_distances(wire: &WireModel) -> (Vec<f32>, f32, f32) {
let n = wire.points.len();
let explicit = wire.pattern_stations.len() >= n + 1;
let (mut dists, has_break, total) = if explicit {
(
wire.pattern_stations[..n].to_vec(),
!wire.plinegen,
wire.pattern_stations[n],
)
} else {
let mut dists = vec![0.0_f32; n];
let mut has_break = false;
// Accumulate arc-length in f64 from double-single deltas (high + low). An
// f32-high-only delta `q[0] - p[0]` quantises ~0.1 at UTM coordinates
// (1.2M), which shifts the dash phase and drifts parallel lines out of sync
// — the reason MLINE elements were CPU-dashed. `points_low` may be empty for
// non-RTE wires → treat the low half as zero (same as the old behaviour).
// Accumulate arc-length in f64 from double-single deltas (high + low).
let mut acc = 0.0_f64;
for i in 1..n {
let p = wire.points[i - 1];
let q = wire.points[i];
if !p[0].is_finite() || !q[0].is_finite() {
has_break = true;
// plinegen=false: reset to 0 at the first real point after a NaN separator.
if !wire.plinegen && !p[0].is_finite() && q[0].is_finite() {
acc = 0.0;
}
@ -434,12 +437,14 @@ fn wire_distances(wire: &WireModel) -> (Vec<f32>, f32, f32) {
dists[i] = acc as f32;
}
}
let total = dists.last().copied().unwrap_or(0.0);
(dists, has_break, total)
};
let pat_len = wire.pattern_length;
if pat_len <= 1e-6 || has_break || n < 2 {
return (dists, 0.0, 0.0);
}
let total = dists[n - 1];
if total <= 1e-6 {
return (dists, 0.0, 0.0);
}

View file

@ -246,8 +246,17 @@ impl Scene {
continue;
}
let clipped =
clip_polyline_to_rect(&projected_pts, vp_x0, vp_y0, vp_x1, vp_y1, pcz);
let stationed = wire.pattern_stations.len() > wire.points.len();
let source_length = stationed.then(|| wire.pattern_stations[wire.points.len()]);
let (clipped, mut clipped_stations) = clip_polyline_to_rect(
&projected_pts,
stationed.then_some(&wire.pattern_stations[..wire.points.len()]),
vp_x0,
vp_y0,
vp_x1,
vp_y1,
pcz,
);
if clipped.is_empty() && projected_text.is_empty() {
continue;
}
@ -312,6 +321,15 @@ impl Scene {
}
let mut out = wire.clone();
out.points = clipped;
if let Some(source_length) = source_length {
for station in &mut clipped_stations {
*station *= scale;
}
clipped_stations.push(source_length * scale);
out.pattern_stations = clipped_stations;
} else {
out.pattern_stations.clear();
}
out.text_verts = projected_text;
// Paper coordinates are small sheet units — no relative-to-eye
// residual is needed, and keeping the model wire's points_low
@ -980,14 +998,16 @@ fn cs_clip(
/// Returns a new points vec with proper NaN separators at clip boundaries.
fn clip_polyline_to_rect(
pts: &[[f32; 3]],
stations: Option<&[f32]>,
xmin: f32,
ymin: f32,
xmax: f32,
ymax: f32,
z: f32,
) -> Vec<[f32; 3]> {
) -> (Vec<[f32; 3]>, Vec<f32>) {
const NAN3: [f32; 3] = [f32::NAN, f32::NAN, f32::NAN];
let mut result: Vec<[f32; 3]> = Vec::new();
let mut result_stations = Vec::new();
let mut i = 0;
while i < pts.len() {
@ -1016,19 +1036,50 @@ fn clip_polyline_to_rect(
pen_down = false;
}
Some((cx0, cy0, cx1, cy1)) => {
let parameter = |x: f32, y: f32| {
let dx = x1 - x0;
let dy = y1 - y0;
if dx.abs() >= dy.abs() && dx.abs() > 1e-12 {
(x - x0) / dx
} else if dy.abs() > 1e-12 {
(y - y0) / dy
} else {
0.0
}
};
let station = |t: f32| {
stations.map_or(0.0, |values| {
let start_station = values[start + j];
start_station
+ (values[start + j + 1] - start_station) * t
})
};
if !pen_down {
if !result.is_empty() {
result.push(NAN3);
if stations.is_some() {
result_stations.push(0.0);
}
}
result.push([cx0, cy0, z]);
if stations.is_some() {
result_stations.push(station(parameter(cx0, cy0)));
}
pen_down = true;
} else if let Some(&[lx, ly, _]) = result.last() {
if (lx - cx0).abs() > 1e-4 || (ly - cy0).abs() > 1e-4 {
result.push(NAN3);
result.push([cx0, cy0, z]);
if stations.is_some() {
result_stations.push(0.0);
result_stations.push(station(parameter(cx0, cy0)));
}
}
}
result.push([cx1, cy1, z]);
if stations.is_some() {
result_stations.push(station(parameter(cx1, cy1)));
}
// If the exit point was clipped, lift pen.
if (cx1 - x1).abs() > 1e-4 || (cy1 - y1).abs() > 1e-4 {
pen_down = false;
@ -1044,6 +1095,9 @@ fn clip_polyline_to_rect(
.unwrap_or(false)
{
result.pop();
if stations.is_some() {
result_stations.pop();
}
result
}
(result, result_stations)
}

View file

@ -311,6 +311,7 @@ pub fn apply_along(
.map(|pts| WireModel {
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
world_width: 0.0,
depth_override: None,
display_visible: true,
@ -358,6 +359,7 @@ pub fn apply_along(
out.push(WireModel {
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
world_width: 0.0,
depth_override: None,
display_visible: true,

View file

@ -3527,7 +3527,6 @@ impl Scene {
};
let mut uniforms =
Uniforms::new(&inst.camera, full_bounds, self.document.header.lineweight_display);
uniforms._pad = if self.document.header.fill_mode { 1.0 } else { 0.0 };
if self.document.header.paper_space_linetype_scaling
&& !inst.paper_sheet
&& inst.tile_idx.is_none()

View file

@ -5,7 +5,7 @@ struct Uniforms {
flat_shade: f32,
transparency_enable: f32,
linetype_scale: f32,
fillmode_enable: f32,
_pad: f32,
view_rot: mat4x4<f32>,
eye_high: vec3<f32>,
_pad_eh: f32,
@ -58,7 +58,6 @@ struct VertexOut {
@location(7) @interpolate(flat) align_total: f32,
@location(8) cap: vec2<f32>,
@location(9) @interpolate(flat) cap_ends: vec3<f32>,
@location(10) @interpolate(flat) world_band: f32,
}
fn resolve_hw(taper_ratio: f32, world_hw: f32, px_hw: f32) -> f32 {
@ -176,11 +175,6 @@ fn vs_main(@builtin(vertex_index) vertex_index: u32, in: VertexIn) -> VertexOut
half_width * side,
);
out.cap_ends = vec3<f32>(segment_length, half_width_a, half_width_b);
out.world_band = select(
0.0,
1.0,
wire_const.world_half_width > 0.0 || in.taper_ratio.x > 0.0 || in.taper_ratio.y > 0.0
);
return out;
}
@ -250,28 +244,10 @@ fn clipped_cap(cap: vec2<f32>, ends: vec3<f32>) -> bool {
return false;
}
fn band_interior_clipped(in: VertexOut) -> bool {
if u.fillmode_enable > 0.5 || in.world_band < 0.5 {
return false;
}
if in.cap.x < 0.0 || in.cap.x > in.cap_ends.x {
return true;
}
var along = 0.0;
if in.cap_ends.x > 1e-4 {
along = clamp(in.cap.x / in.cap_ends.x, 0.0, 1.0);
}
let half_width = mix(in.cap_ends.y, in.cap_ends.z, along);
return abs(in.cap.y) < max(half_width - 1.0, 0.0);
}
fn visible_fragment(in: VertexOut) -> bool {
if clipped_cap(in.cap, in.cap_ends) {
return false;
}
if band_interior_clipped(in) {
return false;
}
if in.pattern_length > 0.0 && in.min_elem >= u.world_per_pixel {
return in_dash(
in.distance,

View file

@ -29,7 +29,7 @@ struct Uniforms {
// every line opaque.
transparency_enable: f32,
linetype_scale: f32,
fillmode_enable: f32,
_pad: f32,
// Relative-to-eye (double-single)
// view_rot is the rotation-only view-projection; vertices subtract the eye
// (eye_high + eye_low, two f32 emulating f64) before transforming, so the
@ -94,7 +94,6 @@ struct VertexOut {
@location(8) cap: vec2<f32>,
// (segment pixel length, end half-width at A, end half-width at B).
@location(9) @interpolate(flat) cap_ends: vec3<f32>,
@location(10) @interpolate(flat) world_band: f32,
}
// Half-width of one segment end: a tapered band's own end width wins, then a
@ -216,7 +215,6 @@ fn marker_relative(position_high: vec3<f32>, position_low: vec3<f32>, instance:
+ ext * hw * u.world_per_pixel;
out.cap = vec2<f32>(which_end * seg_len + ext * hw, hw * side);
out.cap_ends = vec3<f32>(seg_len, hw_a, hw_b);
out.world_band = select(0.0, 1.0, in.misc.w > 0.0 || in.taper.x > 0.0 || in.taper.y > 0.0);
out.pattern_length = in.dists.w * lt_scale;
out.pat0 = in.pat0 * lt_scale;
out.pat1 = in.pat1 * lt_scale;
@ -289,24 +287,6 @@ fn cap_clipped(cap: vec2<f32>, cap_ends: vec3<f32>) -> bool {
return false;
}
fn band_interior_clipped(in: VertexOut) -> bool {
if u.fillmode_enable > 0.5 || in.world_band < 0.5 {
return false;
}
// Wide-polyline fill off: retain a one-pixel line at both band edges.
// Segment cap overhangs are discarded; adjacent sampled segments meet at
// their shared endpoint without drawing an interior seam across the band.
if in.cap.x < 0.0 || in.cap.x > in.cap_ends.x {
return true;
}
var along = 0.0;
if in.cap_ends.x > 1e-4 {
along = clamp(in.cap.x / in.cap_ends.x, 0.0, 1.0);
}
let half_width = mix(in.cap_ends.y, in.cap_ends.z, along);
return abs(in.cap.y) < max(half_width - 1.0, 0.0);
}
@fragment fn fs_main(in: VertexOut) -> @location(0) vec4<f32> {
// Negative pattern length is the persistent-arena tombstone sentinel.
// Discard before cap/alpha work so deleted slabs cannot write color/depth.
@ -316,9 +296,6 @@ fn band_interior_clipped(in: VertexOut) -> bool {
if cap_clipped(in.cap, in.cap_ends) {
discard;
}
if band_interior_clipped(in) {
discard;
}
if in.pattern_length > 0.0 {
// LOD: once the pattern's smallest feature drops below ~1 px
// on screen, dash gaps alias / shimmer (or vanish completely)
@ -346,9 +323,6 @@ fn band_interior_clipped(in: VertexOut) -> bool {
if cap_clipped(in.cap, in.cap_ends) {
discard;
}
if band_interior_clipped(in) {
discard;
}
if in.pattern_length > 0.0 {
if in.min_elem >= u.world_per_pixel {
if !in_dash(in.distance, in.pattern_length, in.pat0, in.pat1, in.align_end, in.align_total) {

View file

@ -12,7 +12,7 @@ struct Uniforms {
flat_shade: f32,
transparency_enable: f32,
linetype_scale: f32,
fillmode_enable: f32,
_pad: f32,
view_rot: mat4x4<f32>,
eye_high: vec3<f32>,
_pad_eh: f32,
@ -71,7 +71,6 @@ struct VertexOut {
@location(8) cap: vec2<f32>,
// (segment pixel length, end half-width at A, end half-width at B).
@location(9) @interpolate(flat) cap_ends: vec3<f32>,
@location(10) @interpolate(flat) world_band: f32,
}
// Half-width of one segment end: a tapered band's own end width wins, then a
@ -182,7 +181,6 @@ fn marker_relative(position_high: vec3<f32>, position_low: vec3<f32>, c: WireCon
+ ext * hw * u.world_per_pixel;
out.cap = vec2<f32>(which_end * seg_len + ext * hw, hw * side);
out.cap_ends = vec3<f32>(seg_len, hw_a, hw_b);
out.world_band = select(0.0, 1.0, c.world_half_width > 0.0 || in.taper_ratio.x > 0.0 || in.taper_ratio.y > 0.0);
out.pattern_length = c.pattern_length * lt_scale;
out.pat0 = c.pat0 * lt_scale;
out.pat1 = c.pat1 * lt_scale;
@ -246,21 +244,6 @@ fn cap_clipped(cap: vec2<f32>, cap_ends: vec3<f32>) -> bool {
return false;
}
fn band_interior_clipped(in: VertexOut) -> bool {
if u.fillmode_enable > 0.5 || in.world_band < 0.5 {
return false;
}
if in.cap.x < 0.0 || in.cap.x > in.cap_ends.x {
return true;
}
var along = 0.0;
if in.cap_ends.x > 1e-4 {
along = clamp(in.cap.x / in.cap_ends.x, 0.0, 1.0);
}
let half_width = mix(in.cap_ends.y, in.cap_ends.z, along);
return abs(in.cap.y) < max(half_width - 1.0, 0.0);
}
@fragment fn fs_main(in: VertexOut) -> @location(0) vec4<f32> {
// Negative pattern length is the persistent-arena tombstone sentinel.
// Discard before cap/alpha work so deleted slabs cannot write color/depth.
@ -270,9 +253,6 @@ fn band_interior_clipped(in: VertexOut) -> bool {
if cap_clipped(in.cap, in.cap_ends) {
discard;
}
if band_interior_clipped(in) {
discard;
}
if in.pattern_length > 0.0 {
if in.min_elem >= u.world_per_pixel {
if !in_dash(in.distance, in.pattern_length, in.pat0, in.pat1, in.align_end, in.align_total) {
@ -294,9 +274,6 @@ fn band_interior_clipped(in: VertexOut) -> bool {
if cap_clipped(in.cap, in.cap_ends) {
discard;
}
if band_interior_clipped(in) {
discard;
}
if in.pattern_length > 0.0 {
if in.min_elem >= u.world_per_pixel {
if !in_dash(in.distance, in.pattern_length, in.pat0, in.pat1, in.align_end, in.align_total) {