forked from 1b-agent/opencadstudio-upstream
Merge remote-tracking branch 'origin/main' into merge-test/main-into-constraints
# Conflicts: # src/app/command_driver.rs # src/command.rs # src/ui/ribbon/mod.rs # src/ui/window/options.rs
This commit is contained in:
commit
ad718def3f
124 files changed
+9866
-692
No files matched your search
@@ -24,7 +24,8 @@ pub fn tessellate_sat(
|
||||
color: [f32; 4],
|
||||
facet_res: f64,
|
||||
chordal_deflection: Option<f64>,
|
||||
isolines: usize,
|
||||
isolines: [usize; 2],
|
||||
planar_isolines: bool,
|
||||
) -> Option<MeshLodSet> {
|
||||
let (bodies, loss) = lift(document);
|
||||
if bodies.is_empty() {
|
||||
@@ -114,7 +115,8 @@ pub fn tessellate_sat(
|
||||
max_angle,
|
||||
source_fit * placement_scale,
|
||||
)
|
||||
.with_isolines(isolines);
|
||||
.with_uv_isolines(isolines[0], isolines[1])
|
||||
.with_planar_isolines(planar_isolines);
|
||||
if let Some(deflection) = chordal_deflection {
|
||||
tolerance = tolerance.with_chordal_deflection(deflection);
|
||||
}
|
||||
|
||||
@@ -10,7 +10,8 @@ fn tessellate_acis(
|
||||
color: [f32; 4],
|
||||
facet_res: f64,
|
||||
chordal_deflection: Option<f64>,
|
||||
isolines: usize,
|
||||
isolines: [usize; 2],
|
||||
planar_isolines: bool,
|
||||
) -> Option<MeshLodSet> {
|
||||
crate::scene::convert::acis_kernel::tessellate_sat(
|
||||
sat,
|
||||
@@ -19,6 +20,7 @@ fn tessellate_acis(
|
||||
facet_res,
|
||||
chordal_deflection,
|
||||
isolines,
|
||||
planar_isolines,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -218,7 +220,8 @@ fn finish(
|
||||
color: [f32; 4],
|
||||
facet_res: f64,
|
||||
chordal_deflection: Option<f64>,
|
||||
isolines: usize,
|
||||
isolines: [usize; 2],
|
||||
planar_isolines: bool,
|
||||
acis: &acadrust::entities::AcisData,
|
||||
) -> Option<MeshLodSet> {
|
||||
let mut set = tessellate_acis(
|
||||
@@ -228,6 +231,7 @@ fn finish(
|
||||
facet_res,
|
||||
chordal_deflection,
|
||||
isolines,
|
||||
planar_isolines,
|
||||
)?;
|
||||
remap_acis_material_bindings(&mut set, acis);
|
||||
Some(set)
|
||||
@@ -251,7 +255,8 @@ pub fn tessellate_region(
|
||||
color,
|
||||
facet_res,
|
||||
chordal_deflection,
|
||||
isolines,
|
||||
[isolines; 2],
|
||||
false,
|
||||
®ion.acis_data,
|
||||
)
|
||||
}
|
||||
@@ -274,7 +279,8 @@ pub fn tessellate_body(
|
||||
color,
|
||||
facet_res,
|
||||
chordal_deflection,
|
||||
isolines,
|
||||
[isolines; 2],
|
||||
false,
|
||||
&body.acis_data,
|
||||
)
|
||||
}
|
||||
@@ -284,7 +290,8 @@ pub fn tessellate_surface(
|
||||
color: [f32; 4],
|
||||
facet_res: f64,
|
||||
chordal_deflection: Option<f64>,
|
||||
isolines: usize,
|
||||
isolines: [usize; 2],
|
||||
planar_isolines: bool,
|
||||
) -> Option<MeshLodSet> {
|
||||
let sat = parse_acis(
|
||||
|| surface.parse_sat(),
|
||||
@@ -298,6 +305,7 @@ pub fn tessellate_surface(
|
||||
facet_res,
|
||||
chordal_deflection,
|
||||
isolines,
|
||||
planar_isolines,
|
||||
&surface.acis_data,
|
||||
)
|
||||
}
|
||||
@@ -320,7 +328,8 @@ pub fn tessellate_solid3d(
|
||||
color,
|
||||
facet_res,
|
||||
chordal_deflection,
|
||||
isolines,
|
||||
[isolines; 2],
|
||||
false,
|
||||
&solid.acis_data,
|
||||
)
|
||||
}
|
||||
+286
-44
@@ -215,17 +215,12 @@ fn polyline_segment_widths(entity: &EntityType) -> Vec<(f32, f32)> {
|
||||
(0..seg_count)
|
||||
.map(|i| {
|
||||
let v = &p.vertices[i];
|
||||
let sw = if v.start_width > 1e-9 {
|
||||
v.start_width
|
||||
let (sw, ew) = if v.start_width > 1e-9 || v.end_width > 1e-9 {
|
||||
(v.start_width, v.end_width)
|
||||
} else {
|
||||
c
|
||||
} as f32;
|
||||
let ew = if v.end_width > 1e-9 {
|
||||
v.end_width
|
||||
} else {
|
||||
c
|
||||
} as f32;
|
||||
(sw, ew)
|
||||
(c, c)
|
||||
};
|
||||
(sw as f32, ew as f32)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@@ -244,17 +239,12 @@ fn polyline_segment_widths(entity: &EntityType) -> Vec<(f32, f32)> {
|
||||
(0..seg_count)
|
||||
.map(|i| {
|
||||
let v = &verts[i];
|
||||
let sw = if v.start_width > 1e-9 {
|
||||
v.start_width
|
||||
let (sw, ew) = if v.start_width > 1e-9 || v.end_width > 1e-9 {
|
||||
(v.start_width, v.end_width)
|
||||
} else {
|
||||
def_start
|
||||
} as f32;
|
||||
let ew = if v.end_width > 1e-9 {
|
||||
v.end_width
|
||||
} else {
|
||||
def_end
|
||||
} as f32;
|
||||
(sw, ew)
|
||||
(def_start, def_end)
|
||||
};
|
||||
(sw as f32, ew as f32)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@@ -292,7 +282,53 @@ fn split_mixed_polyline(
|
||||
end_angle,
|
||||
} = *tg
|
||||
{
|
||||
let (sw, ew) = if let Some(&(w0, w1)) = seg_widths.get(i) {
|
||||
(w0, w1)
|
||||
} else if global_world_width > 1e-9 {
|
||||
(global_world_width, global_world_width)
|
||||
} else {
|
||||
(0.0, 0.0)
|
||||
};
|
||||
|
||||
// Check if the CCW start_angle of this TangentGeom::Arc corresponds
|
||||
// to the segment's end vertex (clockwise bulge arc in CAD where
|
||||
// bulge_arc_to_tangent inverted start_angle and end_angle to enforce CCW).
|
||||
let (p_seg_start, p_seg_end) = if i + 1 < key_vertices.len() {
|
||||
(key_vertices[i], key_vertices[i + 1])
|
||||
} else if !key_vertices.is_empty() {
|
||||
(
|
||||
key_vertices[i % key_vertices.len()],
|
||||
key_vertices[(i + 1) % key_vertices.len()],
|
||||
)
|
||||
} else {
|
||||
([0.0; 3], [0.0; 3])
|
||||
};
|
||||
|
||||
let p_sa = [
|
||||
center[0] + radius * (start_angle.cos() * axis_x[0] + start_angle.sin() * axis_y[0]),
|
||||
center[1] + radius * (start_angle.cos() * axis_x[1] + start_angle.sin() * axis_y[1]),
|
||||
center[2] + radius * (start_angle.cos() * axis_x[2] + start_angle.sin() * axis_y[2]),
|
||||
];
|
||||
let is_reversed = if !key_vertices.is_empty() {
|
||||
let d_start_sq = (p_sa[0] - p_seg_start[0]).powi(2)
|
||||
+ (p_sa[1] - p_seg_start[1]).powi(2)
|
||||
+ (p_sa[2] - p_seg_start[2]).powi(2);
|
||||
let d_end_sq = (p_sa[0] - p_seg_end[0]).powi(2)
|
||||
+ (p_sa[1] - p_seg_end[1]).powi(2)
|
||||
+ (p_sa[2] - p_seg_end[2]).powi(2);
|
||||
d_end_sq < d_start_sq
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
let (w_at_sa, w_at_ea) = if is_reversed {
|
||||
(ew, sw)
|
||||
} else {
|
||||
(sw, ew)
|
||||
};
|
||||
|
||||
let mut arc_pts = Vec::with_capacity(17);
|
||||
let mut arc_widths = Vec::with_capacity(17);
|
||||
let n = 16;
|
||||
let sweep = if end_angle >= start_angle {
|
||||
end_angle - start_angle
|
||||
@@ -308,25 +344,12 @@ fn split_mixed_polyline(
|
||||
center[2] + radius * (ang.cos() * axis_x[2] + ang.sin() * axis_y[2]),
|
||||
];
|
||||
arc_pts.push(p);
|
||||
arc_widths.push(w_at_sa + (w_at_ea - w_at_sa) * frac as f32);
|
||||
}
|
||||
let (points, points_low) = points_to_ds(arc_pts);
|
||||
|
||||
let (sw, ew) = if let Some(&(w0, w1)) = seg_widths.get(i) {
|
||||
if w0 > 1e-9 || w1 > 1e-9 {
|
||||
(w0, w1)
|
||||
} else if global_world_width > 1e-9 {
|
||||
(global_world_width, global_world_width)
|
||||
} else {
|
||||
(0.0, 0.0)
|
||||
}
|
||||
} else if global_world_width > 1e-9 {
|
||||
(global_world_width, global_world_width)
|
||||
} else {
|
||||
(0.0, 0.0)
|
||||
};
|
||||
|
||||
let taper_widths = if (sw - ew).abs() > 1e-6 {
|
||||
vec![sw, ew]
|
||||
let taper_widths = if (w_at_sa - w_at_ea).abs() > 1e-6 {
|
||||
arc_widths
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
@@ -377,43 +400,77 @@ fn split_mixed_polyline(
|
||||
|
||||
// Collect straight lines into a line wire
|
||||
let mut straight_pts: Vec<[f64; 3]> = Vec::new();
|
||||
let mut straight_widths: Vec<f32> = Vec::new();
|
||||
let mut straight_tangents: Vec<TangentGeom> = Vec::new();
|
||||
let mut last_end: Option<[f64; 3]> = None;
|
||||
let mut last_end_w: f32 = 0.0;
|
||||
|
||||
for (i, tg) in tangent_geoms.iter().enumerate() {
|
||||
if let TangentGeom::Line { p1, p2 } = tg {
|
||||
straight_tangents.push(tg.clone());
|
||||
let (p_start, p_end) = if i + 1 < key_vertices.len() {
|
||||
(key_vertices[i], key_vertices[i + 1])
|
||||
let p_start = if i + 1 < key_vertices.len()
|
||||
&& (key_vertices[i][0] as f32 - p1[0]).abs() < 1e-3
|
||||
&& (key_vertices[i][1] as f32 - p1[1]).abs() < 1e-3
|
||||
&& (key_vertices[i][2] as f32 - p1[2]).abs() < 1e-3
|
||||
{
|
||||
key_vertices[i]
|
||||
} else {
|
||||
(
|
||||
[p1[0] as f64, p1[1] as f64, p1[2] as f64],
|
||||
[p2[0] as f64, p2[1] as f64, p2[2] as f64],
|
||||
)
|
||||
[p1[0] as f64, p1[1] as f64, p1[2] as f64]
|
||||
};
|
||||
let p_end = if i + 1 < key_vertices.len()
|
||||
&& (key_vertices[i + 1][0] as f32 - p2[0]).abs() < 1e-3
|
||||
&& (key_vertices[i + 1][1] as f32 - p2[1]).abs() < 1e-3
|
||||
&& (key_vertices[i + 1][2] as f32 - p2[2]).abs() < 1e-3
|
||||
{
|
||||
key_vertices[i + 1]
|
||||
} else {
|
||||
[p2[0] as f64, p2[1] as f64, p2[2] as f64]
|
||||
};
|
||||
let (sw, ew) = if let Some(&(w0, w1)) = seg_widths.get(i) {
|
||||
(w0, w1)
|
||||
} else if global_world_width > 1e-9 {
|
||||
(global_world_width, global_world_width)
|
||||
} else {
|
||||
(0.0, 0.0)
|
||||
};
|
||||
if !plinegen {
|
||||
if !straight_pts.is_empty() {
|
||||
straight_pts.push([f64::NAN; 3]);
|
||||
straight_widths.push(0.0);
|
||||
}
|
||||
straight_pts.push(p_start);
|
||||
straight_widths.push(sw);
|
||||
straight_pts.push(p_end);
|
||||
straight_widths.push(ew);
|
||||
} else {
|
||||
if let Some(prev) = last_end {
|
||||
if (prev[0] - p_start[0]).abs() < 1e-7
|
||||
&& (prev[1] - p_start[1]).abs() < 1e-7
|
||||
&& (prev[2] - p_start[2]).abs() < 1e-7
|
||||
&& (last_end_w - sw).abs() < 1e-6
|
||||
{
|
||||
straight_pts.push(p_end);
|
||||
straight_widths.push(ew);
|
||||
} else {
|
||||
straight_pts.push([f64::NAN; 3]);
|
||||
straight_widths.push(0.0);
|
||||
straight_pts.push(p_start);
|
||||
straight_widths.push(sw);
|
||||
straight_pts.push(p_end);
|
||||
straight_widths.push(ew);
|
||||
}
|
||||
} else {
|
||||
if !straight_pts.is_empty() {
|
||||
straight_pts.push([f64::NAN; 3]);
|
||||
straight_widths.push(0.0);
|
||||
}
|
||||
straight_pts.push(p_start);
|
||||
straight_widths.push(sw);
|
||||
straight_pts.push(p_end);
|
||||
straight_widths.push(ew);
|
||||
}
|
||||
last_end = Some(p_end);
|
||||
last_end_w = ew;
|
||||
}
|
||||
} else {
|
||||
last_end = None;
|
||||
@@ -427,12 +484,24 @@ fn split_mixed_polyline(
|
||||
} else {
|
||||
(Vec::new(), Vec::new())
|
||||
};
|
||||
let line_world_width = straight_widths
|
||||
.iter()
|
||||
.copied()
|
||||
.fold(0.0f32, f32::max);
|
||||
let has_line_taper = straight_widths
|
||||
.first()
|
||||
.map_or(false, |&w0| straight_widths.iter().any(|&w| (w - w0).abs() > 1e-6));
|
||||
let taper_widths = if has_line_taper || straight_widths.iter().any(|&w| w > 1e-6) {
|
||||
straight_widths
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
out.push(WireModel {
|
||||
bg_adapt: None,
|
||||
point_marker,
|
||||
taper_widths: Vec::new(),
|
||||
taper_widths,
|
||||
pattern_stations: Vec::new(),
|
||||
world_width: global_world_width,
|
||||
world_width: line_world_width,
|
||||
depth_override: None,
|
||||
display_visible: true,
|
||||
plot_visible: true,
|
||||
@@ -2989,3 +3058,176 @@ pub(crate) fn normalized_or(v: Vec3, fallback: Vec3) -> Vec3 {
|
||||
v.normalize()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_split_mixed_polyline_straight_lines_have_taper_widths() {
|
||||
let tg_line = TangentGeom::Line {
|
||||
p1: [0.0, 0.0, 0.0],
|
||||
p2: [10.0, 0.0, 0.0],
|
||||
};
|
||||
let tg_arc = TangentGeom::Arc {
|
||||
center: [10.0, 5.0, 0.0],
|
||||
axis_x: [1.0, 0.0, 0.0],
|
||||
axis_y: [0.0, 1.0, 0.0],
|
||||
radius: 5.0,
|
||||
start_angle: -std::f64::consts::FRAC_PI_2,
|
||||
end_angle: 0.0,
|
||||
};
|
||||
let tgs = vec![tg_line, tg_arc];
|
||||
let kv = vec![[0.0, 0.0, 0.0], [10.0, 0.0, 0.0], [15.0, 5.0, 0.0]];
|
||||
let seg_widths = vec![(0.0f32, 10.0f32), (10.0f32, 20.0f32)];
|
||||
|
||||
let wires = split_mixed_polyline(
|
||||
&tgs,
|
||||
&kv,
|
||||
"mixed",
|
||||
[1.0, 1.0, 1.0, 1.0],
|
||||
false,
|
||||
0.0,
|
||||
[0.0; 8],
|
||||
1.0,
|
||||
Vec::new(),
|
||||
None,
|
||||
true,
|
||||
&seg_widths,
|
||||
20.0,
|
||||
Vec::new(),
|
||||
Vec::new(),
|
||||
);
|
||||
|
||||
// Arc wire + Line wire
|
||||
assert_eq!(wires.len(), 2);
|
||||
let arc_wire = &wires[0];
|
||||
assert_eq!(arc_wire.taper_widths.first().copied(), Some(10.0));
|
||||
assert_eq!(arc_wire.taper_widths.last().copied(), Some(20.0));
|
||||
|
||||
let line_wire = &wires[1];
|
||||
assert_eq!(line_wire.taper_widths, vec![0.0, 10.0]);
|
||||
assert_eq!(line_wire.world_width, 10.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_split_mixed_polyline_zero_width_segments_after_tapered_segment() {
|
||||
let tg_line = TangentGeom::Line {
|
||||
p1: [0.0, 0.0, 0.0],
|
||||
p2: [10.0, 0.0, 0.0],
|
||||
};
|
||||
let tg_arc1 = TangentGeom::Arc {
|
||||
center: [10.0, 5.0, 0.0],
|
||||
axis_x: [1.0, 0.0, 0.0],
|
||||
axis_y: [0.0, 1.0, 0.0],
|
||||
radius: 5.0,
|
||||
start_angle: -std::f64::consts::FRAC_PI_2,
|
||||
end_angle: 0.0,
|
||||
};
|
||||
let tg_arc2 = TangentGeom::Arc {
|
||||
center: [15.0, 10.0, 0.0],
|
||||
axis_x: [1.0, 0.0, 0.0],
|
||||
axis_y: [0.0, 1.0, 0.0],
|
||||
radius: 5.0,
|
||||
start_angle: -std::f64::consts::FRAC_PI_2,
|
||||
end_angle: 0.0,
|
||||
};
|
||||
let tgs = vec![tg_line, tg_arc1, tg_arc2];
|
||||
let kv = vec![
|
||||
[0.0, 0.0, 0.0],
|
||||
[10.0, 0.0, 0.0],
|
||||
[15.0, 5.0, 0.0],
|
||||
[20.0, 10.0, 0.0],
|
||||
];
|
||||
// Segment 0 has width (2.0, 0.0), segment 1 and 2 have width (0.0, 0.0)
|
||||
let seg_widths = vec![(2.0f32, 0.0f32), (0.0f32, 0.0f32), (0.0f32, 0.0f32)];
|
||||
|
||||
let wires = split_mixed_polyline(
|
||||
&tgs,
|
||||
&kv,
|
||||
"taper_then_zero",
|
||||
[1.0, 1.0, 1.0, 1.0],
|
||||
false,
|
||||
0.0,
|
||||
[0.0; 8],
|
||||
1.0,
|
||||
Vec::new(),
|
||||
None,
|
||||
true,
|
||||
&seg_widths,
|
||||
2.0,
|
||||
Vec::new(),
|
||||
Vec::new(),
|
||||
);
|
||||
|
||||
// 2 arc wires + 1 line wire = 3 wires
|
||||
assert_eq!(wires.len(), 3);
|
||||
let arc1 = &wires[0];
|
||||
let arc2 = &wires[1];
|
||||
let line = &wires[2];
|
||||
|
||||
// Arcs must have world_width = 0.0 and empty taper_widths
|
||||
assert_eq!(arc1.world_width, 0.0);
|
||||
assert!(arc1.taper_widths.is_empty());
|
||||
assert_eq!(arc2.world_width, 0.0);
|
||||
assert!(arc2.taper_widths.is_empty());
|
||||
|
||||
let inst1 = crate::scene::pipeline::circle_gpu::extract_circle_instance(arc1, 0.0)
|
||||
.expect("arc1 instance");
|
||||
assert_eq!(inst1.start_width, 0.0);
|
||||
assert_eq!(inst1.params[3], 0.0);
|
||||
|
||||
// Straight line must have world_width = 2.0 and taper_widths = [2.0, 0.0]
|
||||
assert_eq!(line.world_width, 2.0);
|
||||
assert_eq!(line.taper_widths, vec![2.0, 0.0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_split_mixed_polyline_reversed_arc_aligns_endpoints() {
|
||||
// Clockwise arc from (10.0, 0.0) to (15.0, 5.0).
|
||||
// bulge_arc_to_tangent inverts angles so start_angle is at (15.0, 5.0) and end_angle at (10.0, 0.0).
|
||||
let tg_arc_reversed = TangentGeom::Arc {
|
||||
center: [10.0, 5.0, 0.0],
|
||||
axis_x: [1.0, 0.0, 0.0],
|
||||
axis_y: [0.0, 1.0, 0.0],
|
||||
radius: 5.0,
|
||||
start_angle: 0.0, // at (15.0, 5.0) - physical end!
|
||||
end_angle: -std::f64::consts::FRAC_PI_2, // at (10.0, 0.0) - physical start!
|
||||
};
|
||||
let tgs = vec![tg_arc_reversed];
|
||||
let kv = vec![[10.0, 0.0, 0.0], [15.0, 5.0, 0.0]];
|
||||
let seg_widths = vec![(5.0f32, 25.0f32)]; // sw=5.0 at start, ew=25.0 at end
|
||||
|
||||
let wires = split_mixed_polyline(
|
||||
&tgs,
|
||||
&kv,
|
||||
"arc_rev",
|
||||
[1.0, 1.0, 1.0, 1.0],
|
||||
false,
|
||||
0.0,
|
||||
[0.0; 8],
|
||||
1.0,
|
||||
Vec::new(),
|
||||
None,
|
||||
true,
|
||||
&seg_widths,
|
||||
25.0,
|
||||
Vec::new(),
|
||||
Vec::new(),
|
||||
);
|
||||
|
||||
assert_eq!(wires.len(), 1);
|
||||
let arc_wire = &wires[0];
|
||||
// In CCW shader angles, start_angle is at (15.0, 5.0) which is physical end (ew = 25.0).
|
||||
// end_angle is at (10.0, 0.0) which is physical start (sw = 5.0).
|
||||
assert_eq!(arc_wire.taper_widths.first().copied(), Some(25.0));
|
||||
assert_eq!(arc_wire.taper_widths.last().copied(), Some(5.0));
|
||||
|
||||
let inst = crate::scene::pipeline::circle_gpu::extract_circle_instance(arc_wire, 0.0)
|
||||
.expect("arc instance extracted");
|
||||
// start_width is at start_angle (physical end, 25.0)
|
||||
assert_eq!(inst.start_width, 25.0);
|
||||
// params.w (end_width) is at end_angle (physical start, 5.0)
|
||||
assert_eq!(inst.params[3], 5.0);
|
||||
}
|
||||
}
|
||||
+98
-6
@@ -3159,7 +3159,13 @@ impl Scene {
|
||||
let facet_resolution = self.document.header.facet_resolution;
|
||||
let chordal_deflection =
|
||||
crate::entities::solid3d::display_deflection(&self.document.header, facet_resolution);
|
||||
let isolines = self.document.header.isolines.max(0) as usize;
|
||||
let (isolines, planar_isolines) = match self.document.get_entity(handle) {
|
||||
Some(EntityType::Surface(surface)) => (
|
||||
crate::entities::solid3d::surface_isoline_counts(surface),
|
||||
crate::entities::solid3d::surface_property_state(surface).isolines,
|
||||
),
|
||||
_ => ([self.document.header.isolines.max(0) as usize; 2], false),
|
||||
};
|
||||
let (mut set, wires, center) =
|
||||
crate::scene::model::solid_model::display_from_solid(
|
||||
solid,
|
||||
@@ -3167,6 +3173,7 @@ impl Scene {
|
||||
facet_resolution,
|
||||
chordal_deflection,
|
||||
isolines,
|
||||
planar_isolines,
|
||||
)?;
|
||||
let name = handle.value().to_string();
|
||||
for mesh in &mut set.lods {
|
||||
@@ -5487,8 +5494,12 @@ impl Scene {
|
||||
// changed entities into it, instead of re-cloning + re-sorting the whole
|
||||
// set. Falls back to the full build below when it can't (no cache entry,
|
||||
// journal un-replayable, or a structural assumption violated).
|
||||
if let Some(arc) =
|
||||
self.try_resident_patch(
|
||||
// A live section changes the displayed wires of every intersected
|
||||
// solid, even when the delta journal contains only the section entity.
|
||||
// Rebuild the resident set in that case so an incremental patch cannot
|
||||
// retain the source solid's uncut edge cache.
|
||||
if self.active_live_section(block).is_none() {
|
||||
if let Some(arc) = self.try_resident_patch(
|
||||
key,
|
||||
block,
|
||||
bg,
|
||||
@@ -5497,9 +5508,9 @@ impl Scene {
|
||||
all_visible,
|
||||
frozen_layers,
|
||||
style_viewport,
|
||||
)
|
||||
{
|
||||
return arc;
|
||||
) {
|
||||
return arc;
|
||||
}
|
||||
}
|
||||
// Build once: full tessellation, no cull, no zoom LOD — the resident
|
||||
// set is zoom-independent (GPU analytical circles/arcs/ellipses).
|
||||
@@ -7073,6 +7084,48 @@ impl Scene {
|
||||
Self::section_body(&body, §ion.data, section.slice_depth, None)
|
||||
}
|
||||
|
||||
/// Build a temporary entity whose wire data matches the live-section body.
|
||||
/// The document entity stays unchanged so disabling the section restores
|
||||
/// the complete source geometry and saving never persists the clipped body.
|
||||
fn sectioned_wire_entity(
|
||||
&self,
|
||||
handle: Handle,
|
||||
section: &LiveSection,
|
||||
) -> Result<Option<EntityType>, ()> {
|
||||
let Some(body) = self.sectioned_body(handle, section)? else {
|
||||
return Ok(None);
|
||||
};
|
||||
let (_, wires, center) = self
|
||||
.prepare_solid_model_display(handle, &body)
|
||||
.ok_or(())?;
|
||||
let mut entity = self.document.get_entity(handle).cloned().ok_or(())?;
|
||||
let reference = acadrust::types::Vector3::new(center[0], center[1], center[2]);
|
||||
match &mut entity {
|
||||
EntityType::Solid3D(solid) => {
|
||||
solid.point_of_reference = reference;
|
||||
solid.wires = wires;
|
||||
solid.silhouettes.clear();
|
||||
}
|
||||
EntityType::Surface(surface) => {
|
||||
surface.point_of_reference = reference;
|
||||
surface.wires = wires;
|
||||
surface.silhouettes.clear();
|
||||
}
|
||||
EntityType::Region(region) => {
|
||||
region.point_of_reference = reference;
|
||||
region.wires = wires;
|
||||
region.silhouettes.clear();
|
||||
}
|
||||
EntityType::Body(body) => {
|
||||
body.point_of_reference = reference;
|
||||
body.wires = wires;
|
||||
body.silhouettes.clear();
|
||||
}
|
||||
_ => return Err(()),
|
||||
}
|
||||
Ok(Some(entity))
|
||||
}
|
||||
|
||||
fn section_source_body(
|
||||
&self,
|
||||
handle: Handle,
|
||||
@@ -9786,6 +9839,45 @@ impl Scene {
|
||||
memo_misses = visible_count;
|
||||
out
|
||||
};
|
||||
|
||||
// Normal Wireframe renders the persisted entity wires, whereas shaded
|
||||
// and 3D Wireframe modes render the temporary sectioned mesh. Replace
|
||||
// only the displayed wire run with one tessellated from that same
|
||||
// temporary body so every visual style shows the identical live cut.
|
||||
if let Some(section) = self.active_live_section(block_handle) {
|
||||
let handles: Vec<Handle> = self.meshes.keys().copied().collect();
|
||||
for handle in handles {
|
||||
let Some(source) = self.document.get_entity(handle) else {
|
||||
continue;
|
||||
};
|
||||
if !visibility_ok(source) {
|
||||
continue;
|
||||
}
|
||||
let replacement = match self.sectioned_wire_entity(handle, §ion) {
|
||||
Ok(Some(entity)) => Some(tessellate_entity(
|
||||
doc,
|
||||
sel,
|
||||
avp,
|
||||
bg,
|
||||
anno,
|
||||
annotation_scale_handle,
|
||||
&entity,
|
||||
Some(blk_ref),
|
||||
view_aabb,
|
||||
wpp,
|
||||
paper,
|
||||
)),
|
||||
Ok(None) => Some(Vec::new()),
|
||||
Err(()) => None,
|
||||
};
|
||||
let Some(replacement) = replacement else {
|
||||
continue;
|
||||
};
|
||||
let name = handle.value().to_string();
|
||||
wires.retain(|wire| wire.name != name);
|
||||
wires.extend(replacement);
|
||||
}
|
||||
}
|
||||
let build_ms = crate::perf::elapsed_ms(t_build);
|
||||
|
||||
// Resolve display colours for the live background. Memo hits were
|
||||
|
||||
@@ -120,6 +120,8 @@ pub enum GripShape {
|
||||
Circle,
|
||||
/// Screen-offset menu selector.
|
||||
Dropdown,
|
||||
/// Menu selector placed immediately beside its anchor grip.
|
||||
DropdownAdjacent,
|
||||
}
|
||||
|
||||
/// Describes one grip point for an entity.
|
||||
@@ -202,6 +204,10 @@ pub enum GripMenuAction {
|
||||
RefineVertices,
|
||||
ShowFit,
|
||||
ShowControlVertices,
|
||||
SectionPlane,
|
||||
SectionSlice,
|
||||
SectionBoundary,
|
||||
SectionVolume,
|
||||
MoveWithText,
|
||||
StackText,
|
||||
UnstackText,
|
||||
|
||||
@@ -94,6 +94,14 @@ pub const PROP_LOFT_CLOSED: &str = "solid_history_loft_closed";
|
||||
pub const PROP_LOFT_PERIODIC: &str = "solid_history_loft_periodic";
|
||||
pub const PROP_HISTORY: &str = "solid_history_record";
|
||||
pub const PROP_SHOW_HISTORY: &str = "solid_history_show";
|
||||
pub const PROP_SURFACE_TYPE: &str = "surface_type";
|
||||
pub const PROP_SURFACE_WIREFRAME_TYPE: &str = "srf_wireframe_type";
|
||||
pub const PROP_SURFACE_U_ISOLINES: &str = "srf_u_isolines";
|
||||
pub const PROP_SURFACE_V_ISOLINES: &str = "srf_v_isolines";
|
||||
pub const PROP_SURFACE_MAINTAIN_ASSOCIATIVITY: &str = "srf_maintain_associativity";
|
||||
pub const PROP_SURFACE_SHOW_ASSOCIATIVITY: &str = "srf_show_associativity";
|
||||
pub const PROP_SURFACE_TRIMMED: &str = "srf_trimmed";
|
||||
pub const PROP_SURFACE_TRIMMING_EDGES: &str = "srf_trimming_edges";
|
||||
|
||||
fn history_prop(label: &str, field: &'static str, value: impl ToString) -> Property {
|
||||
Property {
|
||||
@@ -103,6 +111,23 @@ fn history_prop(label: &str, field: &'static str, value: impl ToString) -> Prope
|
||||
}
|
||||
}
|
||||
|
||||
fn compact_surface_number(mut value: String) -> String {
|
||||
if value.ends_with('°') {
|
||||
value.pop();
|
||||
}
|
||||
if let Some(decimal) = value.rfind('.') {
|
||||
if value[decimal + 1..].chars().all(|character| character.is_ascii_digit()) {
|
||||
while value.ends_with('0') {
|
||||
value.pop();
|
||||
}
|
||||
if value.ends_with('.') {
|
||||
value.pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
value
|
||||
}
|
||||
|
||||
fn history_flags(
|
||||
document: &acadrust::CadDocument,
|
||||
handle: acadrust::Handle,
|
||||
@@ -239,9 +264,14 @@ pub fn has_compact_solid_properties(
|
||||
document: &acadrust::CadDocument,
|
||||
handle: acadrust::Handle,
|
||||
) -> bool {
|
||||
document
|
||||
.get_entity(handle)
|
||||
.is_some_and(|entity| matches!(entity, acadrust::EntityType::Solid3D(_)))
|
||||
match document.get_entity(handle) {
|
||||
Some(acadrust::EntityType::Solid3D(_)) => true,
|
||||
Some(acadrust::EntityType::Surface(_)) => matches!(
|
||||
primitive_property_operation(document, handle),
|
||||
Some(SolidHistoryOperation::Extrusion(_))
|
||||
),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reference_point(operation: &SolidHistoryOperation) -> Option<glam::DVec3> {
|
||||
@@ -403,10 +433,6 @@ fn extrusion_properties(
|
||||
|direction| crate::entities::common::format_length(direction[axis]),
|
||||
)),
|
||||
};
|
||||
let (record_history, object_show_history, show_history_mode) =
|
||||
history_flags(document, handle).unwrap_or((false, false, 1));
|
||||
let (show_history, show_history_editable) =
|
||||
displayed_history_state(object_show_history, show_history_mode);
|
||||
let path_length = sweep_path_length(value);
|
||||
let height_value = path_length.unwrap_or(value.end_draft_distance);
|
||||
let height = if value.path_entity.is_some() {
|
||||
@@ -419,6 +445,134 @@ fn extrusion_properties(
|
||||
} else {
|
||||
PropValue::EditText(crate::entities::common::format_angle(value.draft_angle))
|
||||
};
|
||||
if let Some(EntityType::Surface(surface)) = document.get_entity(handle) {
|
||||
let state = crate::entities::solid3d::surface_property_state(surface);
|
||||
let surface_height = if value.path_entity.is_some() {
|
||||
PropValue::ReadOnly(compact_surface_number(
|
||||
crate::entities::common::format_length(height_value),
|
||||
))
|
||||
} else {
|
||||
PropValue::EditText(compact_surface_number(
|
||||
crate::entities::common::format_length(height_value),
|
||||
))
|
||||
};
|
||||
let surface_taper = if value.path_entity.is_some() {
|
||||
PropValue::ReadOnly(compact_surface_number(
|
||||
crate::entities::common::format_angle(value.draft_angle),
|
||||
))
|
||||
} else {
|
||||
PropValue::EditText(compact_surface_number(
|
||||
crate::entities::common::format_angle(value.draft_angle),
|
||||
))
|
||||
};
|
||||
let surface_direction_property =
|
||||
|label: &str, field: &'static str, axis: usize| Property {
|
||||
label: label.to_string(),
|
||||
field,
|
||||
value: PropValue::ReadOnly(direction.map_or_else(
|
||||
|| t!("Unavailable").into_owned(),
|
||||
|direction| {
|
||||
compact_surface_number(crate::entities::common::format_length(
|
||||
direction[axis],
|
||||
))
|
||||
},
|
||||
)),
|
||||
};
|
||||
let yes_no_choice = |selected: bool| PropValue::Choice {
|
||||
selected: if selected { "Yes" } else { "No" }.to_string(),
|
||||
options: vec!["Yes".to_string(), "No".to_string()],
|
||||
};
|
||||
return vec![
|
||||
PropSection {
|
||||
title: t!("Geometry").into_owned(),
|
||||
props: vec![
|
||||
Property {
|
||||
label: t!("Surface Type").into_owned(),
|
||||
field: PROP_SURFACE_TYPE,
|
||||
value: PropValue::ReadOnly(t!("Extrusion").into_owned()),
|
||||
},
|
||||
Property {
|
||||
label: t!("Height").into_owned(),
|
||||
field: PROP_EXTRUSION_HEIGHT,
|
||||
value: surface_height,
|
||||
},
|
||||
Property {
|
||||
label: t!("Taper angle").into_owned(),
|
||||
field: PROP_TAPER_ANGLE,
|
||||
value: surface_taper,
|
||||
},
|
||||
surface_direction_property(
|
||||
t!("Direction X").as_ref(),
|
||||
PROP_EXTRUSION_DIRECTION_X,
|
||||
0,
|
||||
),
|
||||
surface_direction_property(
|
||||
t!("Direction Y").as_ref(),
|
||||
PROP_EXTRUSION_DIRECTION_Y,
|
||||
1,
|
||||
),
|
||||
surface_direction_property(
|
||||
t!("Direction Z").as_ref(),
|
||||
PROP_EXTRUSION_DIRECTION_Z,
|
||||
2,
|
||||
),
|
||||
Property {
|
||||
label: t!("Wireframe type").into_owned(),
|
||||
field: PROP_SURFACE_WIREFRAME_TYPE,
|
||||
value: PropValue::Choice {
|
||||
selected: if state.isolines { "Isolines" } else { "Isoparms" }
|
||||
.to_string(),
|
||||
options: vec!["Isolines".to_string(), "Isoparms".to_string()],
|
||||
},
|
||||
},
|
||||
Property {
|
||||
label: t!("U isolines").into_owned(),
|
||||
field: PROP_SURFACE_U_ISOLINES,
|
||||
value: PropValue::EditText(surface.u_isolines.max(0).to_string()),
|
||||
},
|
||||
Property {
|
||||
label: t!("V isolines").into_owned(),
|
||||
field: PROP_SURFACE_V_ISOLINES,
|
||||
value: PropValue::EditText(surface.v_isolines.max(0).to_string()),
|
||||
},
|
||||
],
|
||||
},
|
||||
PropSection {
|
||||
title: t!("Surface Associativity").into_owned(),
|
||||
props: vec![
|
||||
Property {
|
||||
label: t!("Maintain associativity").into_owned(),
|
||||
field: PROP_SURFACE_MAINTAIN_ASSOCIATIVITY,
|
||||
value: yes_no_choice(state.maintain_associativity),
|
||||
},
|
||||
Property {
|
||||
label: t!("Show associativity").into_owned(),
|
||||
field: PROP_SURFACE_SHOW_ASSOCIATIVITY,
|
||||
value: yes_no_choice(state.show_associativity),
|
||||
},
|
||||
],
|
||||
},
|
||||
PropSection {
|
||||
title: t!("Trims").into_owned(),
|
||||
props: vec![
|
||||
Property {
|
||||
label: t!("Trimmed surface").into_owned(),
|
||||
field: PROP_SURFACE_TRIMMED,
|
||||
value: PropValue::ReadOnly("No".to_string()),
|
||||
},
|
||||
Property {
|
||||
label: t!("Trimming edges").into_owned(),
|
||||
field: PROP_SURFACE_TRIMMING_EDGES,
|
||||
value: PropValue::ReadOnly("0".to_string()),
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
||||
let (record_history, object_show_history, show_history_mode) =
|
||||
history_flags(document, handle).unwrap_or((false, false, 1));
|
||||
let (show_history, show_history_editable) =
|
||||
displayed_history_state(object_show_history, show_history_mode);
|
||||
vec![
|
||||
PropSection {
|
||||
title: t!("Geometry").into_owned(),
|
||||
@@ -1531,6 +1685,15 @@ pub fn is_history_choice(field: &str) -> bool {
|
||||
matches!(field, PROP_HISTORY | PROP_SHOW_HISTORY)
|
||||
}
|
||||
|
||||
pub fn is_surface_property_choice(field: &str) -> bool {
|
||||
matches!(
|
||||
field,
|
||||
PROP_SURFACE_WIREFRAME_TYPE
|
||||
| PROP_SURFACE_MAINTAIN_ASSOCIATIVITY
|
||||
| PROP_SURFACE_SHOW_ASSOCIATIVITY
|
||||
)
|
||||
}
|
||||
|
||||
pub fn is_loft_geometry_choice(field: &str) -> bool {
|
||||
matches!(field, PROP_LOFT_NORMALS | PROP_LOFT_CLOSED | PROP_LOFT_PERIODIC)
|
||||
}
|
||||
@@ -1538,7 +1701,11 @@ pub fn is_loft_geometry_choice(field: &str) -> bool {
|
||||
pub fn is_specialized_property(field: &str) -> bool {
|
||||
matches!(field, "solid_history_type" | PROP_BANK | PROP_SWEEP_LENGTH
|
||||
| PROP_LOFT_TYPE | PROP_LOFT_SECTION_COUNT
|
||||
| PROP_EXTRUSION_DIRECTION_X | PROP_EXTRUSION_DIRECTION_Y | PROP_EXTRUSION_DIRECTION_Z)
|
||||
| PROP_EXTRUSION_DIRECTION_X | PROP_EXTRUSION_DIRECTION_Y | PROP_EXTRUSION_DIRECTION_Z
|
||||
| PROP_SURFACE_TYPE | PROP_SURFACE_WIREFRAME_TYPE
|
||||
| PROP_SURFACE_U_ISOLINES | PROP_SURFACE_V_ISOLINES
|
||||
| PROP_SURFACE_MAINTAIN_ASSOCIATIVITY | PROP_SURFACE_SHOW_ASSOCIATIVITY
|
||||
| PROP_SURFACE_TRIMMED | PROP_SURFACE_TRIMMING_EDGES)
|
||||
|| is_primitive_property(field)
|
||||
|| is_history_choice(field)
|
||||
}
|
||||
@@ -3018,11 +3185,6 @@ fn extrusion_draft_grip(
|
||||
if !inverse.is_finite() {
|
||||
return None;
|
||||
}
|
||||
let profile = value.sweep_entity.as_ref().and_then(embedded_entity)?;
|
||||
let normal = crate::entities::curve::entity_curve(&profile)?.plane.normal()?;
|
||||
let normal = transform
|
||||
.transform_vector3(glam::DVec3::from_array(normal))
|
||||
.try_normalize()?;
|
||||
let center_local = inverse.transform_point3(center);
|
||||
let anchor = profile_grips
|
||||
.into_iter()
|
||||
@@ -3057,7 +3219,7 @@ fn extrusion_draft_grip(
|
||||
value.direction.y,
|
||||
value.direction.z,
|
||||
));
|
||||
let height = direction.dot(normal).abs();
|
||||
let height = direction.length();
|
||||
if !direction.is_finite() || !height.is_finite() || height <= 1e-6 {
|
||||
return None;
|
||||
}
|
||||
@@ -3307,6 +3469,7 @@ pub fn primitive_grips(
|
||||
if let SolidHistoryOperation::Chamfer(value) = operation {
|
||||
return chamfer_distance_grips(document, handle, value);
|
||||
}
|
||||
let is_surface = matches!(document.get_entity(handle), Some(EntityType::Surface(_)));
|
||||
let mut grips = Vec::new();
|
||||
let mut add = |id, transform, point, shape, axis: Option<[f64; 3]>| {
|
||||
if let Some(world) = world_point(transform, point) {
|
||||
@@ -3528,27 +3691,40 @@ pub fn primitive_grips(
|
||||
}
|
||||
SolidHistoryOperation::Extrusion(value) => {
|
||||
let (profile_grips, profile_center) = extrusion_profile_grips(value);
|
||||
grips.extend(profile_grips);
|
||||
if !is_surface {
|
||||
grips.extend(profile_grips);
|
||||
}
|
||||
let direction = [value.direction.x, value.direction.y, value.direction.z];
|
||||
if value.path_entity.is_none() {
|
||||
if let (Some(center), Some(base)) = (profile_center, matrix(value.base.transform)) {
|
||||
let direction = base.transform_vector3(glam::DVec3::from_array(direction));
|
||||
if direction.length_squared() > 1e-12 {
|
||||
grips.push(grip(
|
||||
let mut height_grip = grip(
|
||||
GRIP_HEIGHT,
|
||||
center + direction,
|
||||
GripShape::Triangle,
|
||||
Some(direction.normalize()),
|
||||
));
|
||||
);
|
||||
if is_surface {
|
||||
// Surface construction arrows keep one stable screen
|
||||
// orientation while their world-space axis continues to
|
||||
// constrain the edit.
|
||||
height_grip.dir = None;
|
||||
}
|
||||
grips.push(height_grip);
|
||||
}
|
||||
}
|
||||
if let Some((world, _, radial, _, _)) = extrusion_draft_grip(value) {
|
||||
grips.push(grip(
|
||||
let mut draft_grip = grip(
|
||||
GRIP_DRAFT,
|
||||
world,
|
||||
GripShape::Triangle,
|
||||
Some(radial),
|
||||
));
|
||||
);
|
||||
if is_surface {
|
||||
draft_grip.dir = None;
|
||||
}
|
||||
grips.push(draft_grip);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,8 @@ fn display_tessellation(
|
||||
body: &Body,
|
||||
facet_resolution: f64,
|
||||
chordal_deflection: Option<f64>,
|
||||
isolines: usize,
|
||||
isolines: [usize; 2],
|
||||
planar_isolines: bool,
|
||||
) -> brep::mesh::BodyMesh {
|
||||
let resolution = if facet_resolution.is_finite() && facet_resolution > 0.0 {
|
||||
facet_resolution.clamp(0.01, 10.0)
|
||||
@@ -34,7 +35,8 @@ fn display_tessellation(
|
||||
|_| cadkernel::tessellation::display_angle_for_resolution(resolution),
|
||||
);
|
||||
let mut tolerance = brep::mesh::TessellationTolerance::new(max_angle, TOL)
|
||||
.with_isolines(isolines);
|
||||
.with_uv_isolines(isolines[0], isolines[1])
|
||||
.with_planar_isolines(planar_isolines);
|
||||
if let Some(deflection) = chordal_deflection {
|
||||
tolerance = tolerance.with_chordal_deflection(deflection);
|
||||
}
|
||||
@@ -267,20 +269,42 @@ pub fn edge_wires(body: &Body) -> Vec<acadrust::entities::Wire> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// White wireframe used while a solid-history grip is hot.
|
||||
/// White wireframe used while a solid-history grip is hot. Surface previews
|
||||
/// include the requested construction isolines so their cage follows the edit.
|
||||
///
|
||||
/// This deliberately does not touch the resident solid mesh or its entity
|
||||
/// wires: the selected source stays visible in blue while the candidate body
|
||||
/// is presented as a separate, non-pickable outline until placement.
|
||||
pub fn grip_preview_wires(body: &Body, handle: acadrust::Handle) -> Vec<WireModel> {
|
||||
tessellation(body)
|
||||
pub fn grip_preview_wires(
|
||||
body: &Body,
|
||||
handle: acadrust::Handle,
|
||||
isolines: [usize; 2],
|
||||
planar_isolines: bool,
|
||||
) -> Vec<WireModel> {
|
||||
let tessellation = brep::mesh::tessellate(
|
||||
body,
|
||||
brep::mesh::TessellationTolerance::new(
|
||||
cadkernel::tessellation::DEFAULT_ANGLE,
|
||||
TOL,
|
||||
)
|
||||
.with_uv_isolines(isolines[0], isolines[1])
|
||||
.with_planar_isolines(planar_isolines),
|
||||
);
|
||||
tessellation
|
||||
.edges
|
||||
.into_iter()
|
||||
.filter(|edge| edge.positions.len() >= 2)
|
||||
.map(|edge| {
|
||||
.map(|edge| edge.positions)
|
||||
.chain(
|
||||
tessellation
|
||||
.isolines
|
||||
.into_iter()
|
||||
.map(|isoline| isoline.positions),
|
||||
)
|
||||
.filter(|positions| positions.len() >= 2)
|
||||
.map(|positions| {
|
||||
WireModel::solid_f64(
|
||||
format!("{}-GRIP-PREVIEW", handle.value()),
|
||||
edge.positions,
|
||||
positions,
|
||||
WireModel::WHITE,
|
||||
false,
|
||||
)
|
||||
@@ -453,7 +477,8 @@ pub fn display_from_solid(
|
||||
color: [f32; 4],
|
||||
facet_resolution: f64,
|
||||
chordal_deflection: Option<f64>,
|
||||
isolines: usize,
|
||||
isolines: [usize; 2],
|
||||
planar_isolines: bool,
|
||||
) -> Option<(MeshLodSet, Vec<acadrust::entities::Wire>, [f64; 3])> {
|
||||
use acadrust::types::Vector3;
|
||||
let tessellation = display_tessellation(
|
||||
@@ -461,6 +486,7 @@ pub fn display_from_solid(
|
||||
facet_resolution,
|
||||
chordal_deflection,
|
||||
isolines,
|
||||
planar_isolines,
|
||||
);
|
||||
let center = mesh_center(&tessellation.mesh)?;
|
||||
let wires = tessellation
|
||||
@@ -541,7 +567,7 @@ mod tests {
|
||||
use super::*;
|
||||
|
||||
fn tri_count(body: &Body) -> usize {
|
||||
display_from_solid(body, [0.7, 0.7, 0.7, 1.0], 1.0, None, 0)
|
||||
display_from_solid(body, [0.7, 0.7, 0.7, 1.0], 1.0, None, [0; 2], false)
|
||||
.map(|(m, _, _)| m.lods[0].indices.len() / 3)
|
||||
.unwrap_or(0)
|
||||
}
|
||||
@@ -558,6 +584,23 @@ mod tests {
|
||||
assert!(tri_count(&pyramid_solid(c, 5.0, 9.0, 6).unwrap()) >= 8, "pyramid");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn planar_display_uses_independent_isoline_counts() {
|
||||
let body = box_solid([0.0; 3], 10.0, 10.0, 10.0).unwrap();
|
||||
let display = |counts, planar| {
|
||||
display_from_solid(&body, [0.7, 0.7, 0.7, 1.0], 1.0, None, counts, planar)
|
||||
.unwrap()
|
||||
.0
|
||||
.edge_verts
|
||||
.len()
|
||||
};
|
||||
let boundaries = display([0, 0], false);
|
||||
|
||||
assert_eq!(display([1, 0], true) - boundaries, 12);
|
||||
assert_eq!(display([0, 2], true) - boundaries, 24);
|
||||
assert_eq!(display([2, 2], false), boundaries);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_primitive_is_the_size_it_was_asked_for() {
|
||||
// Triangle counts say a mesh exists; the volume says it is the right
|
||||
|
||||
+13
-1
@@ -1046,9 +1046,21 @@ impl Scene {
|
||||
{
|
||||
return false;
|
||||
}
|
||||
let (isoline_counts, planar_isolines) = match self.document.get_entity(handle) {
|
||||
Some(EntityType::Surface(surface)) => (
|
||||
crate::entities::solid3d::surface_isoline_counts(surface),
|
||||
crate::entities::solid3d::surface_property_state(surface).isolines,
|
||||
),
|
||||
_ => ([0; 2], false),
|
||||
};
|
||||
self.solid_history_preview_wires.insert(
|
||||
handle,
|
||||
crate::scene::model::solid_model::grip_preview_wires(&body, handle),
|
||||
crate::scene::model::solid_model::grip_preview_wires(
|
||||
&body,
|
||||
handle,
|
||||
isoline_counts,
|
||||
planar_isolines,
|
||||
),
|
||||
);
|
||||
true
|
||||
}
|
||||
|
||||
+24
-3
@@ -13,11 +13,18 @@ pub const GRIP_HALF_PX: f32 = 5.0;
|
||||
/// Screen offset for dropdown selectors anchored to a vertex.
|
||||
pub const GRIP_DROPDOWN_OFFSET_X_PX: f32 = 24.0;
|
||||
pub const GRIP_DROPDOWN_OFFSET_Y_PX: f32 = 27.0;
|
||||
pub const GRIP_ADJACENT_DROPDOWN_OFFSET_X_PX: f32 = 12.0;
|
||||
|
||||
fn marker_screen_position(mut point: Point, shape: GripShape) -> Point {
|
||||
if shape == GripShape::Dropdown {
|
||||
point.x += GRIP_DROPDOWN_OFFSET_X_PX;
|
||||
point.y += GRIP_DROPDOWN_OFFSET_Y_PX;
|
||||
match shape {
|
||||
GripShape::Dropdown => {
|
||||
point.x += GRIP_DROPDOWN_OFFSET_X_PX;
|
||||
point.y += GRIP_DROPDOWN_OFFSET_Y_PX;
|
||||
}
|
||||
GripShape::DropdownAdjacent => {
|
||||
point.x += GRIP_ADJACENT_DROPDOWN_OFFSET_X_PX;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
point
|
||||
}
|
||||
@@ -47,6 +54,8 @@ pub struct GripEdit {
|
||||
pub enum GripEditMode {
|
||||
Stretch,
|
||||
Lengthen,
|
||||
Radius,
|
||||
ArcLength,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -85,6 +94,18 @@ impl GripEdit {
|
||||
edit.mode = GripEditMode::Lengthen;
|
||||
edit
|
||||
}
|
||||
|
||||
pub fn radius(handle: Handle, grip_id: usize, world: DVec3) -> Self {
|
||||
let mut edit = Self::single(handle, grip_id, false, world);
|
||||
edit.mode = GripEditMode::Radius;
|
||||
edit
|
||||
}
|
||||
|
||||
pub fn arc_length(handle: Handle, grip_id: usize, world: DVec3) -> Self {
|
||||
let mut edit = Self::single(handle, grip_id, false, world);
|
||||
edit.mode = GripEditMode::ArcLength;
|
||||
edit
|
||||
}
|
||||
}
|
||||
|
||||
// ── Screen-space helpers ───────────────────────────────────────────────────
|
||||
|
||||
@@ -258,7 +258,7 @@ pub fn extract_circle_instance_from_geom_indexed(
|
||||
|
||||
let (start_width, end_width) = if !wire.taper_widths.is_empty() {
|
||||
if wire.taper_widths.len() >= 2 && wire.tangent_geoms.len() == 1 {
|
||||
(wire.taper_widths[0], wire.taper_widths[1])
|
||||
(wire.taper_widths[0], *wire.taper_widths.last().unwrap())
|
||||
} else if wire.taper_widths.len() > geom_index {
|
||||
let sw = wire.taper_widths[geom_index];
|
||||
let ew = wire.taper_widths.get(geom_index + 1).copied().unwrap_or(sw);
|
||||
|
||||
@@ -116,7 +116,7 @@ pub struct WireConst {
|
||||
/// the quad by `world_half_width / world_per_pixel` so the band tracks zoom
|
||||
/// in drawing units.
|
||||
pub world_half_width: f32,
|
||||
pub _pad1: f32,
|
||||
pub is_tapered: f32,
|
||||
pub _pad2: f32,
|
||||
/// Point-marker origin as a double-single pair. `marker_normal_scale.w`
|
||||
/// stores the viewport-height percentage; zero disables marker scaling.
|
||||
@@ -592,7 +592,14 @@ pub(crate) fn emit_wire_packed(
|
||||
let (dists, align_end, align_total) = wire_distances(wire);
|
||||
let (marker_origin_high, marker_origin_low, marker_normal_scale) = marker_metadata(wire);
|
||||
let low = |i: usize| -> [f32; 3] { wire.points_low.get(i).copied().unwrap_or([0.0; 3]) };
|
||||
let tw = |i: usize| -> f32 { wire.taper_widths.get(i).copied().unwrap_or(0.0) * 0.5 };
|
||||
let is_tapered = !wire.taper_widths.is_empty();
|
||||
let tw = |i: usize| -> f32 {
|
||||
if is_tapered {
|
||||
wire.taper_widths.get(i).copied().unwrap_or(0.0) * 0.5
|
||||
} else {
|
||||
-1.0
|
||||
}
|
||||
};
|
||||
let mut instances: Vec<PackedWireInstance> = Vec::with_capacity(seg_count);
|
||||
for i in 0..seg_count {
|
||||
let a = wire.points[i];
|
||||
@@ -636,6 +643,7 @@ pub(crate) fn emit_wire_native(
|
||||
) -> (Vec<WireInstance>, WireConst) {
|
||||
let (dists, align_end, align_total) = wire_distances(wire);
|
||||
let (marker_origin_high, marker_origin_low, marker_normal_scale) = marker_metadata(wire);
|
||||
let is_tapered = !wire.taper_widths.is_empty();
|
||||
let cst = WireConst {
|
||||
color,
|
||||
pat0: [wire.pattern[0], wire.pattern[1], wire.pattern[2], wire.pattern[3]],
|
||||
@@ -646,7 +654,7 @@ pub(crate) fn emit_wire_native(
|
||||
align_end,
|
||||
align_total,
|
||||
world_half_width: wire.world_width * 0.5,
|
||||
_pad1: 0.0,
|
||||
is_tapered: if is_tapered { 1.0 } else { 0.0 },
|
||||
_pad2: 0.0,
|
||||
marker_origin_high,
|
||||
marker_origin_low,
|
||||
@@ -658,18 +666,12 @@ pub(crate) fn emit_wire_native(
|
||||
return (Vec::new(), cst);
|
||||
}
|
||||
let low = |i: usize| -> [f32; 3] { wire.points_low.get(i).copied().unwrap_or([0.0; 3]) };
|
||||
// Store an endpoint/max-width ratio. The shared f32 maximum keeps drawing
|
||||
// units and range out of the packed field; UNORM16 contributes only a
|
||||
// relative error below 1/65535. Preserve zero as the existing constant
|
||||
// width fallback sentinel.
|
||||
let taper_ratio = |i: usize| -> u16 {
|
||||
let width = wire.taper_widths.get(i).copied().unwrap_or(0.0);
|
||||
if width <= 0.0 || wire.world_width <= 0.0 {
|
||||
if !is_tapered || wire.world_width <= 0.0 {
|
||||
0
|
||||
} else {
|
||||
((width / wire.world_width).clamp(0.0, 1.0) * u16::MAX as f32)
|
||||
.round()
|
||||
.max(1.0) as u16
|
||||
let width = wire.taper_widths.get(i).copied().unwrap_or(0.0);
|
||||
((width / wire.world_width).clamp(0.0, 1.0) * u16::MAX as f32).round() as u16
|
||||
}
|
||||
};
|
||||
let mut instances: Vec<WireInstance> = Vec::with_capacity(seg_count);
|
||||
|
||||
Reference in new issue
Block a user