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 committed 2026-08-24 22:18:44 +03:00
commit 4209c35ba3
60 files changed
+1620 -575

No files matched your search

+203 -9
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 {
transformed_wire_length_scale(lw, accum_xform)
} else {
1.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);
}
}
}
+9
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
+4
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,
+81 -12
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;
+70
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(),
+89 -7
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
+1 -2
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 ───────────────────────────
+32 -27
View File
@@ -406,40 +406,45 @@ 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 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).
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;
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).
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;
if !wire.plinegen && !p[0].is_finite() && q[0].is_finite() {
acc = 0.0;
}
dists[i] = acc as f32;
} else {
let pl = wire.points_low.get(i - 1).copied().unwrap_or([0.0; 3]);
let ql = wire.points_low.get(i).copied().unwrap_or([0.0; 3]);
let dx = (q[0] as f64 - p[0] as f64) + (ql[0] as f64 - pl[0] as f64);
let dy = (q[1] as f64 - p[1] as f64) + (ql[1] as f64 - pl[1] as f64);
let dz = (q[2] as f64 - p[2] as f64) + (ql[2] as f64 - pl[2] as f64);
acc += (dx * dx + dy * dy + dz * dz).sqrt();
dists[i] = acc as f32;
}
dists[i] = acc as f32;
} else {
let pl = wire.points_low.get(i - 1).copied().unwrap_or([0.0; 3]);
let ql = wire.points_low.get(i).copied().unwrap_or([0.0; 3]);
let dx = (q[0] as f64 - p[0] as f64) + (ql[0] as f64 - pl[0] as f64);
let dy = (q[1] as f64 - p[1] as f64) + (ql[1] as f64 - pl[1] as f64);
let dz = (q[2] as f64 - p[2] as f64) + (ql[2] as f64 - pl[2] as f64);
acc += (dx * dx + dy * dy + dz * dz).sqrt();
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);
}
+58 -4
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)
}
+2
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,
-1
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()