forked from 1b-agent/opencadstudio-upstream
Merge updated origin/main
This commit is contained in:
commit
e51c4d0932
25 files changed
+2516
-136
No files matched your search
@@ -259,7 +259,7 @@ pub unsafe extern "C" fn dwg_thumbnail_png(
|
|||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub unsafe extern "C" fn dwg_thumbnail_free(ptr: *mut u8, len: usize) {
|
pub unsafe extern "C" fn dwg_thumbnail_free(ptr: *mut u8, len: usize) {
|
||||||
if !ptr.is_null() && len != 0 {
|
if !ptr.is_null() && len != 0 {
|
||||||
drop(Box::from_raw(std::slice::from_raw_parts_mut(ptr, len)));
|
drop(Box::from_raw(std::ptr::slice_from_raw_parts_mut(ptr, len)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -450,10 +450,7 @@ fn profile_curves<B: DocApiBackend>(
|
|||||||
b.profile_curves(profile)
|
b.profile_curves(profile)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Validate a `Curve2Spec`'s fallible conditions BEFORE mutation (the same rules
|
/// Validate that an entity exists before performing an operation.
|
||||||
/// the host's `curve_spec_to_entity` enforces), so `CreateMany` stays all-or-nothing.
|
|
||||||
/// Currently: ellipse minor/major ratio must be in (0, 1].
|
|
||||||
|
|
||||||
fn require_exists<B: DocApiBackend>(b: &B, id: ObjectId, op: &'static str) -> ApiResult<()> {
|
fn require_exists<B: DocApiBackend>(b: &B, id: ObjectId, op: &'static str) -> ApiResult<()> {
|
||||||
if b.entity_exists(id) {
|
if b.entity_exists(id) {
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -746,10 +746,13 @@ mod repl {
|
|||||||
error: Option<String>,
|
error: Option<String>,
|
||||||
error_type: Option<String>,
|
error_type: Option<String>,
|
||||||
traceback: Option<String>,
|
traceback: Option<String>,
|
||||||
line_number: Option<u32>,
|
position: Option<(u32, u32)>,
|
||||||
column_number: Option<u32>,
|
|
||||||
duration_ms: f64,
|
duration_ms: f64,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
|
let (line_number, column_number) = match position {
|
||||||
|
Some((l, c)) => (Some(l), Some(c)),
|
||||||
|
None => (None, None),
|
||||||
|
};
|
||||||
Self {
|
Self {
|
||||||
success,
|
success,
|
||||||
output,
|
output,
|
||||||
|
|||||||
@@ -261,20 +261,13 @@ pub fn run_request_proxy(
|
|||||||
token: [u8; PROXY_TOKEN_LEN],
|
token: [u8; PROXY_TOKEN_LEN],
|
||||||
) -> std::io::Result<()> {
|
) -> std::io::Result<()> {
|
||||||
for stream in listener.incoming() {
|
for stream in listener.incoming() {
|
||||||
let mut stream = match stream {
|
let mut stream = stream?;
|
||||||
Ok(s) => s,
|
|
||||||
Err(e) => return Err(e),
|
|
||||||
};
|
|
||||||
if !verify_token(&mut stream, &token) {
|
if !verify_token(&mut stream, &token) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let sender = Arc::clone(&sender);
|
let sender = Arc::clone(&sender);
|
||||||
std::thread::spawn(move || {
|
std::thread::spawn(move || {
|
||||||
loop {
|
while let Ok(req) = recv_framed(&mut stream) {
|
||||||
let req: PluginRequest = match recv_framed(&mut stream) {
|
|
||||||
Ok(r) => r,
|
|
||||||
Err(_) => break,
|
|
||||||
};
|
|
||||||
let resp = match sender.request(req) {
|
let resp = match sender.request(req) {
|
||||||
Ok(r) => r,
|
Ok(r) => r,
|
||||||
Err(e) => PluginResponse::Error(e.to_string()),
|
Err(e) => PluginResponse::Error(e.to_string()),
|
||||||
|
|||||||
@@ -75,10 +75,12 @@ fn request_kind(req: &HostRequest) -> &'static str {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type DeferredRequest = (u64, Option<u64>, Box<PluginRequest>);
|
||||||
|
|
||||||
pub(crate) struct V4Connection {
|
pub(crate) struct V4Connection {
|
||||||
shared: Arc<V4HostShared>,
|
shared: Arc<V4HostShared>,
|
||||||
incoming: Mutex<mpsc::Receiver<HostIncoming>>,
|
incoming: Mutex<mpsc::Receiver<HostIncoming>>,
|
||||||
deferred: Mutex<VecDeque<(u64, Option<u64>, Box<PluginRequest>)>>,
|
deferred: Mutex<VecDeque<DeferredRequest>>,
|
||||||
call_lock: Mutex<()>,
|
call_lock: Mutex<()>,
|
||||||
next_id: AtomicU64,
|
next_id: AtomicU64,
|
||||||
reader_handle: Mutex<Option<std::thread::JoinHandle<()>>>,
|
reader_handle: Mutex<Option<std::thread::JoinHandle<()>>>,
|
||||||
@@ -199,7 +201,7 @@ impl V4Connection {
|
|||||||
tab_id,
|
tab_id,
|
||||||
payload,
|
payload,
|
||||||
}) => {
|
}) => {
|
||||||
if tab_id.map_or(true, |request_tab| request_tab == host.tab_id()) {
|
if tab_id.is_none_or(|request_tab| request_tab == host.tab_id()) {
|
||||||
self.respond_to_plugin_request(
|
self.respond_to_plugin_request(
|
||||||
host,
|
host,
|
||||||
rid,
|
rid,
|
||||||
@@ -251,7 +253,7 @@ impl V4Connection {
|
|||||||
let mut deferred = self.deferred.lock().unwrap_or_else(|e| e.into_inner());
|
let mut deferred = self.deferred.lock().unwrap_or_else(|e| e.into_inner());
|
||||||
let mut waiting = VecDeque::new();
|
let mut waiting = VecDeque::new();
|
||||||
while let Some((id, tab_id, payload)) = deferred.pop_front() {
|
while let Some((id, tab_id, payload)) = deferred.pop_front() {
|
||||||
if tab_id.map_or(true, |request_tab| request_tab == current_tab_id) {
|
if tab_id.is_none_or(|request_tab| request_tab == current_tab_id) {
|
||||||
ready.push_back((id, payload));
|
ready.push_back((id, payload));
|
||||||
} else {
|
} else {
|
||||||
waiting.push_back((id, tab_id, payload));
|
waiting.push_back((id, tab_id, payload));
|
||||||
@@ -271,7 +273,7 @@ impl V4Connection {
|
|||||||
tab_id,
|
tab_id,
|
||||||
payload,
|
payload,
|
||||||
}) => {
|
}) => {
|
||||||
if tab_id.map_or(true, |request_tab| request_tab == current_tab_id) {
|
if tab_id.is_none_or(|request_tab| request_tab == current_tab_id) {
|
||||||
self.respond_to_plugin_request(
|
self.respond_to_plugin_request(
|
||||||
host,
|
host,
|
||||||
id,
|
id,
|
||||||
|
|||||||
+1
-9
@@ -999,14 +999,6 @@ fn kernel_placement(p: &PlacementSpec) -> cadkernel::brep::Placement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Signed volume and centroid of a closed triangle mesh via the divergence
|
|
||||||
/// theorem: V = Σ v0·(v1×v2)/6, C = Σ (v0+v1+v2)·tetra_vol / (4V). Single source
|
|
||||||
/// of truth for both (used by the cold-cache `volume`/`centroid` fallback).
|
|
||||||
fn mesh_volume_centroid(mesh: &cadkernel::brep::Mesh) -> (f64, [f64; 3]) {
|
|
||||||
// Delegate to the crate's single source of truth (divergence theorem).
|
|
||||||
ocs_doc_api::geom::mesh_volume_centroid(mesh)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -3235,7 +3227,7 @@ mod tests {
|
|||||||
let body = crate::scene::convert::solid3d_tess::kernel_body(solid)
|
let body = crate::scene::convert::solid3d_tess::kernel_body(solid)
|
||||||
.expect("re-lift reloaded solid failed");
|
.expect("re-lift reloaded solid failed");
|
||||||
let mesh = cadkernel::brep::mesh_body(&body, 0.5, 1e-3);
|
let mesh = cadkernel::brep::mesh_body(&body, 0.5, 1e-3);
|
||||||
let vol = mesh_volume_centroid(&mesh).0;
|
let vol = ocs_doc_api::geom::mesh_volume_centroid(&mesh).0;
|
||||||
assert!(
|
assert!(
|
||||||
(vol - 125.0).abs() < 1.0,
|
(vol - 125.0).abs() < 1.0,
|
||||||
"reloaded intersection volume {vol}"
|
"reloaded intersection volume {vol}"
|
||||||
|
|||||||
+145
-3
@@ -10,7 +10,39 @@ use crate::scene::convert::acad_to_render::{extrusion_wall_tris, RenderEntity, R
|
|||||||
use crate::scene::model::object::{GripApply, GripDef, PropSection};
|
use crate::scene::model::object::{GripApply, GripDef, PropSection};
|
||||||
use crate::scene::model::wire_model::TangentGeom;
|
use crate::scene::model::wire_model::TangentGeom;
|
||||||
|
|
||||||
fn to_render(circle: &Circle) -> RenderEntity {
|
/// The number of segments to tessellate a circle into, based on its radius and the
|
||||||
|
/// current viewport's world units per pixel (`wpp`).
|
||||||
|
///
|
||||||
|
/// Targets a maximum chord sagitta of ~0.25 pixels on screen so circles stay visually
|
||||||
|
/// round at any zoom level, while bounded between a minimum of 48 (preventing
|
||||||
|
/// coarse polygons when small or far away) and a maximum of 4096 (capping buffer size
|
||||||
|
/// on extreme zoom).
|
||||||
|
pub fn circle_segments(radius: f64, wpp: Option<f32>) -> usize {
|
||||||
|
const MIN_SEGMENTS: usize = 48;
|
||||||
|
const MAX_SEGMENTS: usize = 4096;
|
||||||
|
let Some(w) = wpp.filter(|&w| w.is_finite() && w > 0.0) else {
|
||||||
|
return MIN_SEGMENTS;
|
||||||
|
};
|
||||||
|
let r = radius.abs();
|
||||||
|
if !r.is_finite() || r <= 0.0 {
|
||||||
|
return MIN_SEGMENTS;
|
||||||
|
}
|
||||||
|
// Target chord sagitta of ~0.25 px on screen:
|
||||||
|
// sag = r * (1 - cos(theta / 2)) <= tolerance
|
||||||
|
let tolerance = (w as f64) * 0.25;
|
||||||
|
if r <= tolerance {
|
||||||
|
return MIN_SEGMENTS;
|
||||||
|
}
|
||||||
|
let cos_half = (1.0 - tolerance / r).clamp(-1.0, 1.0);
|
||||||
|
let step = 2.0 * cos_half.acos();
|
||||||
|
if step <= 0.0 {
|
||||||
|
return MAX_SEGMENTS;
|
||||||
|
}
|
||||||
|
let n = (std::f64::consts::TAU / step).ceil() as usize;
|
||||||
|
n.clamp(MIN_SEGMENTS, MAX_SEGMENTS)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_render_with_wpp(circle: &Circle, wpp: Option<f32>) -> RenderEntity {
|
||||||
let cx = circle.center.x;
|
let cx = circle.center.x;
|
||||||
let cy = circle.center.y;
|
let cy = circle.center.y;
|
||||||
let cz = circle.center.z;
|
let cz = circle.center.z;
|
||||||
@@ -31,10 +63,12 @@ fn to_render(circle: &Circle) -> RenderEntity {
|
|||||||
radius: r,
|
radius: r,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let n = circle_segments(r, wpp);
|
||||||
|
|
||||||
if circle.thickness.abs() > 1e-10 {
|
if circle.thickness.abs() > 1e-10 {
|
||||||
let t = circle.thickness;
|
let t = circle.thickness;
|
||||||
let (nx, ny, nz) = normal;
|
let (nx, ny, nz) = normal;
|
||||||
let n = 64usize;
|
let n = n.max(64);
|
||||||
let tau = std::f64::consts::TAU;
|
let tau = std::f64::consts::TAU;
|
||||||
let circ_pt = |a: f64| -> (f64, f64, f64) {
|
let circ_pt = |a: f64| -> (f64, f64, f64) {
|
||||||
let (c, s) = (a.cos(), a.sin());
|
let (c, s) = (a.cos(), a.sin());
|
||||||
@@ -76,7 +110,8 @@ fn to_render(circle: &Circle) -> RenderEntity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Keep tessellation on the entity curve for large-coordinate precision.
|
// Keep tessellation on the entity curve for large-coordinate precision.
|
||||||
let pts = crate::entities::curve::curve_points(&curve);
|
let max_angle = std::f64::consts::TAU / (n as f64);
|
||||||
|
let pts = curve.tessellate_angle(max_angle);
|
||||||
|
|
||||||
RenderEntity {
|
RenderEntity {
|
||||||
pick_tris: Vec::new(),
|
pick_tris: Vec::new(),
|
||||||
@@ -88,6 +123,22 @@ fn to_render(circle: &Circle) -> RenderEntity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn to_render(circle: &Circle) -> RenderEntity {
|
||||||
|
to_render_with_wpp(circle, None)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Zoom-aware render converter for EntityType::Circle.
|
||||||
|
pub fn relative_render(
|
||||||
|
entity: &acadrust::EntityType,
|
||||||
|
_document: &acadrust::CadDocument,
|
||||||
|
wpp: Option<f32>,
|
||||||
|
) -> Option<RenderEntity> {
|
||||||
|
let acadrust::EntityType::Circle(circle) = entity else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
Some(to_render_with_wpp(circle, wpp))
|
||||||
|
}
|
||||||
|
|
||||||
fn grips(circle: &Circle) -> Vec<GripDef> {
|
fn grips(circle: &Circle) -> Vec<GripDef> {
|
||||||
let center = circle.center_wcs();
|
let center = circle.center_wcs();
|
||||||
let (axis_x, axis_y) = circle.axes_wcs();
|
let (axis_x, axis_y) = circle.axes_wcs();
|
||||||
@@ -220,3 +271,94 @@ impl crate::entities::traits::MassPropsCalc for Circle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_circle_segments_scales_with_zoom() {
|
||||||
|
let r = 10.0;
|
||||||
|
// Default / invalid cases return baseline floor of 48.
|
||||||
|
assert_eq!(circle_segments(r, None), 48);
|
||||||
|
assert_eq!(circle_segments(r, Some(-1.0)), 48);
|
||||||
|
assert_eq!(circle_segments(r, Some(0.0)), 48);
|
||||||
|
assert_eq!(circle_segments(r, Some(f32::NAN)), 48);
|
||||||
|
assert_eq!(circle_segments(r, Some(f32::INFINITY)), 48);
|
||||||
|
|
||||||
|
// Zoomed far out: screen radius is tiny, capped at floor 48.
|
||||||
|
assert_eq!(circle_segments(r, Some(10.0)), 48);
|
||||||
|
assert_eq!(circle_segments(r, Some(2.0)), 48);
|
||||||
|
|
||||||
|
// Zooming closer increases segments monotonically.
|
||||||
|
let seg_0_1 = circle_segments(r, Some(0.1));
|
||||||
|
let seg_0_01 = circle_segments(r, Some(0.01));
|
||||||
|
let seg_0_001 = circle_segments(r, Some(0.001));
|
||||||
|
let seg_0_0001 = circle_segments(r, Some(0.0001));
|
||||||
|
|
||||||
|
assert!(seg_0_1 >= 48, "seg_0_1 was {seg_0_1}");
|
||||||
|
assert!(seg_0_01 > seg_0_1, "seg_0_01 ({seg_0_01}) <= seg_0_1 ({seg_0_1})");
|
||||||
|
assert!(seg_0_001 > seg_0_01, "seg_0_001 ({seg_0_001}) <= seg_0_01 ({seg_0_01})");
|
||||||
|
assert!(seg_0_0001 > seg_0_001, "seg_0_0001 ({seg_0_0001}) <= seg_0_001 ({seg_0_001})");
|
||||||
|
|
||||||
|
// Extreme zoom in hits maximum cap of 4096.
|
||||||
|
assert_eq!(circle_segments(r, Some(1e-9)), 4096);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_circle_segments_scales_with_radius() {
|
||||||
|
let wpp = Some(0.1);
|
||||||
|
let seg_small = circle_segments(1.0, wpp);
|
||||||
|
let seg_med = circle_segments(20.0, wpp);
|
||||||
|
let seg_large = circle_segments(100.0, wpp);
|
||||||
|
let seg_huge = circle_segments(500.0, wpp);
|
||||||
|
|
||||||
|
assert_eq!(seg_small, 48);
|
||||||
|
assert!(seg_med > seg_small, "{seg_med} <= {seg_small}");
|
||||||
|
assert!(seg_large > seg_med, "{seg_large} <= {seg_med}");
|
||||||
|
assert!(seg_huge > seg_large, "{seg_huge} <= {seg_large}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_circle_to_render_points_increase_with_zoom() {
|
||||||
|
let mut circle = Circle::default();
|
||||||
|
circle.radius = 10.0;
|
||||||
|
|
||||||
|
let far_render = to_render_with_wpp(&circle, Some(1.0));
|
||||||
|
let close_render = to_render_with_wpp(&circle, Some(0.01));
|
||||||
|
|
||||||
|
let far_count = match far_render.object {
|
||||||
|
RenderObject::Lines(pts) => pts.len(),
|
||||||
|
_ => panic!("Expected RenderObject::Lines"),
|
||||||
|
};
|
||||||
|
let close_count = match close_render.object {
|
||||||
|
RenderObject::Lines(pts) => pts.len(),
|
||||||
|
_ => panic!("Expected RenderObject::Lines"),
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(far_count, 49); // 48 segments + 1 closing vertex
|
||||||
|
assert!(close_count > far_count, "close_count was {close_count} vs far_count {far_count}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_thick_circle_to_render_points_increase_with_zoom() {
|
||||||
|
let mut circle = Circle::default();
|
||||||
|
circle.radius = 10.0;
|
||||||
|
circle.thickness = 2.0;
|
||||||
|
|
||||||
|
let far_render = to_render_with_wpp(&circle, Some(1.0));
|
||||||
|
let close_render = to_render_with_wpp(&circle, Some(0.01));
|
||||||
|
|
||||||
|
let far_count = match far_render.object {
|
||||||
|
RenderObject::Lines(pts) => pts.len(),
|
||||||
|
_ => panic!("Expected RenderObject::Lines"),
|
||||||
|
};
|
||||||
|
let close_count = match close_render.object {
|
||||||
|
RenderObject::Lines(pts) => pts.len(),
|
||||||
|
_ => panic!("Expected RenderObject::Lines"),
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(close_count > far_count, "{close_count} <= {far_count}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -702,6 +702,30 @@ pub fn parse_angle_deg(value: &str) -> Option<f64> {
|
|||||||
/// there is one obvious place to see that the maths moved out.
|
/// there is one obvious place to see that the maths moved out.
|
||||||
pub use cadkernel::geom2d::BulgeArc;
|
pub use cadkernel::geom2d::BulgeArc;
|
||||||
|
|
||||||
|
/// Convert a 2D BulgeArc into a 3D TangentGeom::Arc with its world center,
|
||||||
|
/// plane axes, radius, and counter-clockwise start/end sweep angles.
|
||||||
|
pub fn bulge_arc_to_tangent(
|
||||||
|
arc: &BulgeArc,
|
||||||
|
to_wcs: &dyn Fn(f64, f64) -> (f64, f64, f64),
|
||||||
|
normal: (f64, f64, f64),
|
||||||
|
) -> crate::scene::model::wire_model::TangentGeom {
|
||||||
|
let (cwx, cwy, cwz) = to_wcs(arc.center[0], arc.center[1]);
|
||||||
|
let (ax, ay) = crate::scene::view::transform::ocs_axes(normal);
|
||||||
|
let (sa, ea) = if arc.sweep >= 0.0 {
|
||||||
|
(arc.start_angle, arc.end_angle)
|
||||||
|
} else {
|
||||||
|
(arc.end_angle, arc.start_angle)
|
||||||
|
};
|
||||||
|
crate::scene::model::wire_model::TangentGeom::Arc {
|
||||||
|
center: [cwx, cwy, cwz],
|
||||||
|
axis_x: [ax.0, ax.1, ax.2],
|
||||||
|
axis_y: [ay.0, ay.1, ay.2],
|
||||||
|
radius: arc.radius,
|
||||||
|
start_angle: sa,
|
||||||
|
end_angle: ea,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Triangulate the solid bands a `wide_fills` returns into the flat WCS f64
|
/// Triangulate the solid bands a `wide_fills` returns into the flat WCS f64
|
||||||
/// triangle list `RenderEntity::pick_tris` carries, so a wide polyline is
|
/// triangle list `RenderEntity::pick_tris` carries, so a wide polyline is
|
||||||
/// selectable across the band it draws and not just along its centreline.
|
/// selectable across the band it draws and not just along its centreline.
|
||||||
|
|||||||
@@ -5408,6 +5408,21 @@ fn map_wire_ocs_to_wcs(wire: &mut WireModel, normal: Vector3) {
|
|||||||
*axis_x = map_vector(*axis_x);
|
*axis_x = map_vector(*axis_x);
|
||||||
*axis_y = map_vector(*axis_y);
|
*axis_y = map_vector(*axis_y);
|
||||||
}
|
}
|
||||||
|
TangentGeom::PlanarEllipse {
|
||||||
|
center,
|
||||||
|
major_axis,
|
||||||
|
normal,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
let mapped = map(center[0], center[1], center[2]);
|
||||||
|
let map_vector = |axis: [f64; 3]| {
|
||||||
|
let mapped = map(axis[0], axis[1], axis[2]);
|
||||||
|
[mapped.0, mapped.1, mapped.2]
|
||||||
|
};
|
||||||
|
*center = [mapped.0, mapped.1, mapped.2];
|
||||||
|
*major_axis = map_vector(*major_axis);
|
||||||
|
*normal = map_vector(*normal);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
wire.aabb = WireModel::UNBOUNDED_AABB;
|
wire.aabb = WireModel::UNBOUNDED_AABB;
|
||||||
|
|||||||
+19
-9
@@ -15,6 +15,7 @@ use crate::entities::common::{
|
|||||||
use crate::entities::traits::RenderConvertible;
|
use crate::entities::traits::RenderConvertible;
|
||||||
use crate::scene::convert::acad_to_render::{RenderEntity, RenderObject};
|
use crate::scene::convert::acad_to_render::{RenderEntity, RenderObject};
|
||||||
use crate::scene::model::object::{GripApply, GripDef, PropSection};
|
use crate::scene::model::object::{GripApply, GripDef, PropSection};
|
||||||
|
use crate::scene::model::wire_model::TangentGeom;
|
||||||
|
|
||||||
fn to_render(ell: &Ellipse) -> RenderEntity {
|
fn to_render(ell: &Ellipse) -> RenderEntity {
|
||||||
// ELLIPSE is one of the few WCS entities in DXF: `center` (code 10) and
|
// ELLIPSE is one of the few WCS entities in DXF: `center` (code 10) and
|
||||||
@@ -29,6 +30,15 @@ fn to_render(ell: &Ellipse) -> RenderEntity {
|
|||||||
.map(crate::entities::curve::snap_from)
|
.map(crate::entities::curve::snap_from)
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
let tangent = TangentGeom::PlanarEllipse {
|
||||||
|
center: [ell.center.x, ell.center.y, ell.center.z],
|
||||||
|
major_axis: [ell.major_axis.x, ell.major_axis.y, ell.major_axis.z],
|
||||||
|
normal: [ell.normal.x, ell.normal.y, ell.normal.z],
|
||||||
|
minor_axis_ratio: ell.minor_axis_ratio,
|
||||||
|
start_param: ell.start_parameter,
|
||||||
|
end_param: ell.end_parameter,
|
||||||
|
};
|
||||||
|
|
||||||
// The points come from the entity's own kernel curve and angular policy.
|
// The points come from the entity's own kernel curve and angular policy.
|
||||||
//
|
//
|
||||||
// What does not change is the shape of the object. EXTRUDE, REVOLVE and
|
// What does not change is the shape of the object. EXTRUDE, REVOLVE and
|
||||||
@@ -39,7 +49,7 @@ fn to_render(ell: &Ellipse) -> RenderEntity {
|
|||||||
pick_tris: Vec::new(),
|
pick_tris: Vec::new(),
|
||||||
object: RenderObject::Lines(Vec::new()),
|
object: RenderObject::Lines(Vec::new()),
|
||||||
snap_pts: snap.snap_pts,
|
snap_pts: snap.snap_pts,
|
||||||
tangent_geoms: vec![],
|
tangent_geoms: vec![tangent.clone()],
|
||||||
key_vertices: vec![],
|
key_vertices: vec![],
|
||||||
fill_tris: vec![],
|
fill_tris: vec![],
|
||||||
};
|
};
|
||||||
@@ -57,7 +67,7 @@ fn to_render(ell: &Ellipse) -> RenderEntity {
|
|||||||
pick_tris: Vec::new(),
|
pick_tris: Vec::new(),
|
||||||
object,
|
object,
|
||||||
snap_pts: snap.snap_pts,
|
snap_pts: snap.snap_pts,
|
||||||
tangent_geoms: vec![],
|
tangent_geoms: vec![tangent],
|
||||||
key_vertices: vec![],
|
key_vertices: vec![],
|
||||||
fill_tris: vec![],
|
fill_tris: vec![],
|
||||||
}
|
}
|
||||||
@@ -284,16 +294,16 @@ fn apply_axis_grip(ell: &mut Ellipse, grip_id: usize, point: glam::DVec3) {
|
|||||||
fn apply_grip(ell: &mut Ellipse, grip_id: usize, apply: GripApply) {
|
fn apply_grip(ell: &mut Ellipse, grip_id: usize, apply: GripApply) {
|
||||||
match (grip_id, apply) {
|
match (grip_id, apply) {
|
||||||
(0, GripApply::Translate(d)) => {
|
(0, GripApply::Translate(d)) => {
|
||||||
ell.center.x += d.x as f64;
|
ell.center.x += d.x;
|
||||||
ell.center.y += d.y as f64;
|
ell.center.y += d.y;
|
||||||
ell.center.z += d.z as f64;
|
ell.center.z += d.z;
|
||||||
}
|
}
|
||||||
(0, GripApply::Absolute(p)) => {
|
(0, GripApply::Absolute(p)) => {
|
||||||
ell.center.x = p.x as f64;
|
ell.center.x = p.x;
|
||||||
ell.center.y = p.y as f64;
|
ell.center.y = p.y;
|
||||||
ell.center.z = p.z as f64;
|
ell.center.z = p.z;
|
||||||
}
|
}
|
||||||
(1 | 2 | 3 | 4, GripApply::Absolute(p)) => apply_axis_grip(ell, grip_id, p),
|
(1..=4, GripApply::Absolute(p)) => apply_axis_grip(ell, grip_id, p),
|
||||||
(5 | 6, GripApply::Absolute(p)) => {
|
(5 | 6, GripApply::Absolute(p)) => {
|
||||||
let Some(parameter) = parameter_at_point(ell, p) else {
|
let Some(parameter) = parameter_at_point(ell, p) else {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -351,11 +351,7 @@ fn to_render(pline: &LwPolyline, fill_mode: bool) -> RenderEntity {
|
|||||||
} else if let Some(arc) =
|
} else if let Some(arc) =
|
||||||
crate::entities::common::BulgeArc::from_bulge([ox0, oy0], [ox1, oy1], bulge)
|
crate::entities::common::BulgeArc::from_bulge([ox0, oy0], [ox1, oy1], bulge)
|
||||||
{
|
{
|
||||||
let (wcx, wcy, wcz) = to_wcs(arc.center[0], arc.center[1]);
|
tgs.push(crate::entities::common::bulge_arc_to_tangent(&arc, &to_wcs, normal));
|
||||||
tgs.push(TangentGeom::Circle {
|
|
||||||
center: [wcx as f32, wcy as f32, wcz as f32],
|
|
||||||
radius: arc.radius as f32,
|
|
||||||
});
|
|
||||||
for s in arc
|
for s in arc
|
||||||
.tessellate_angle(cadkernel::tessellation::DEFAULT_ANGLE)
|
.tessellate_angle(cadkernel::tessellation::DEFAULT_ANGLE)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@@ -407,11 +403,7 @@ fn to_render(pline: &LwPolyline, fill_mode: bool) -> RenderEntity {
|
|||||||
[v1.location.x, v1.location.y],
|
[v1.location.x, v1.location.y],
|
||||||
v0.bulge,
|
v0.bulge,
|
||||||
) {
|
) {
|
||||||
let (wcx, wcy, wcz) = to_wcs(arc.center[0], arc.center[1]);
|
tgs.push(crate::entities::common::bulge_arc_to_tangent(&arc, &to_wcs, normal));
|
||||||
tgs.push(TangentGeom::Circle {
|
|
||||||
center: [wcx as f32, wcy as f32, wcz as f32],
|
|
||||||
radius: arc.radius as f32,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let (pts, widths) = crate::entities::common::tapered_band_points(
|
let (pts, widths) = crate::entities::common::tapered_band_points(
|
||||||
@@ -470,11 +462,7 @@ fn to_render(pline: &LwPolyline, fill_mode: bool) -> RenderEntity {
|
|||||||
}
|
}
|
||||||
let (wx1, wy1, wz1) = to_wcs(ox1, oy1);
|
let (wx1, wy1, wz1) = to_wcs(ox1, oy1);
|
||||||
kv.push([wx1, wy1, wz1]);
|
kv.push([wx1, wy1, wz1]);
|
||||||
let (wcx, wcy, wcz) = to_wcs(arc.center[0], arc.center[1]);
|
tgs.push(crate::entities::common::bulge_arc_to_tangent(&arc, &to_wcs, normal));
|
||||||
tgs.push(TangentGeom::Circle {
|
|
||||||
center: [wcx as f32, wcy as f32, wcz as f32],
|
|
||||||
radius: arc.radius as f32,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
if i + 1 < seg_count {
|
if i + 1 < seg_count {
|
||||||
pts.push([f64::NAN; 3]);
|
pts.push([f64::NAN; 3]);
|
||||||
@@ -565,11 +553,8 @@ fn centerline_metadata(
|
|||||||
[end.location.x, end.location.y],
|
[end.location.x, end.location.y],
|
||||||
start.bulge,
|
start.bulge,
|
||||||
) {
|
) {
|
||||||
let center = to_wcs(arc.center[0], arc.center[1]);
|
let normal = (pline.normal.x, pline.normal.y, pline.normal.z);
|
||||||
tangents.push(TangentGeom::Circle {
|
tangents.push(crate::entities::common::bulge_arc_to_tangent(&arc, to_wcs, normal));
|
||||||
center: [center.0 as f32, center.1 as f32, center.2 as f32],
|
|
||||||
radius: arc.radius as f32,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
if index == 0 {
|
if index == 0 {
|
||||||
key_vertices.push([p0.0, p0.1, p0.2]);
|
key_vertices.push([p0.0, p0.1, p0.2]);
|
||||||
|
|||||||
@@ -248,7 +248,7 @@ fn tessellate_polyline2d(pl: &Polyline2D, fill_mode: bool) -> RenderEntity {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
let (tangent_geoms, key_vertices) =
|
let (tangent_geoms, key_vertices) =
|
||||||
centerline_metadata_2d(verts, pl.is_closed(), &to_wcs);
|
centerline_metadata_2d(verts, pl.is_closed(), &to_wcs, normal);
|
||||||
return RenderEntity {
|
return RenderEntity {
|
||||||
pick_tris: Vec::new(),
|
pick_tris: Vec::new(),
|
||||||
object: RenderObject::BoundaryLines {
|
object: RenderObject::BoundaryLines {
|
||||||
@@ -296,11 +296,7 @@ fn tessellate_polyline2d(pl: &Polyline2D, fill_mode: bool) -> RenderEntity {
|
|||||||
} else if let Some(arc) =
|
} else if let Some(arc) =
|
||||||
crate::entities::common::BulgeArc::from_bulge([ox0, oy0], [ox1, oy1], bulge)
|
crate::entities::common::BulgeArc::from_bulge([ox0, oy0], [ox1, oy1], bulge)
|
||||||
{
|
{
|
||||||
let (wcx, wcy, wcz) = to_wcs(arc.center[0], arc.center[1]);
|
tgs.push(crate::entities::common::bulge_arc_to_tangent(&arc, &to_wcs, normal));
|
||||||
tgs.push(TangentGeom::Circle {
|
|
||||||
center: [wcx as f32, wcy as f32, wcz as f32],
|
|
||||||
radius: arc.radius as f32,
|
|
||||||
});
|
|
||||||
for s in arc
|
for s in arc
|
||||||
.tessellate_angle(cadkernel::tessellation::DEFAULT_ANGLE)
|
.tessellate_angle(cadkernel::tessellation::DEFAULT_ANGLE)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@@ -378,11 +374,7 @@ fn tessellate_polyline2d(pl: &Polyline2D, fill_mode: bool) -> RenderEntity {
|
|||||||
[v1.location.x, v1.location.y],
|
[v1.location.x, v1.location.y],
|
||||||
bulge,
|
bulge,
|
||||||
) {
|
) {
|
||||||
let (wcx, wcy, wcz) = to_wcs(arc.center[0], arc.center[1]);
|
tangents.push(crate::entities::common::bulge_arc_to_tangent(&arc, &to_wcs, normal));
|
||||||
tangents.push(TangentGeom::Circle {
|
|
||||||
center: [wcx as f32, wcy as f32, wcz as f32],
|
|
||||||
radius: arc.radius as f32,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if i == 0 {
|
if i == 0 {
|
||||||
@@ -464,6 +456,7 @@ fn centerline_metadata_2d(
|
|||||||
verts: &[acadrust::entities::Vertex2D],
|
verts: &[acadrust::entities::Vertex2D],
|
||||||
closed: bool,
|
closed: bool,
|
||||||
to_wcs: &dyn Fn(f64, f64) -> (f64, f64, f64),
|
to_wcs: &dyn Fn(f64, f64) -> (f64, f64, f64),
|
||||||
|
normal: (f64, f64, f64),
|
||||||
) -> (Vec<TangentGeom>, Vec<[f64; 3]>) {
|
) -> (Vec<TangentGeom>, Vec<[f64; 3]>) {
|
||||||
let count = verts.len();
|
let count = verts.len();
|
||||||
let segment_count = if closed {
|
let segment_count = if closed {
|
||||||
@@ -488,11 +481,7 @@ fn centerline_metadata_2d(
|
|||||||
[end.location.x, end.location.y],
|
[end.location.x, end.location.y],
|
||||||
start.bulge,
|
start.bulge,
|
||||||
) {
|
) {
|
||||||
let center = to_wcs(arc.center[0], arc.center[1]);
|
tangents.push(crate::entities::common::bulge_arc_to_tangent(&arc, to_wcs, normal));
|
||||||
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 {
|
if index == 0 {
|
||||||
key_vertices.push([p0.0, p0.1, p0.2]);
|
key_vertices.push([p0.0, p0.1, p0.2]);
|
||||||
|
|||||||
Vendored
+180
-10
@@ -1108,7 +1108,7 @@ pub fn expand_insert(
|
|||||||
);
|
);
|
||||||
let mut first = first_batches.finalize(&name, selected, bg_color);
|
let mut first = first_batches.finalize(&name, selected, bg_color);
|
||||||
for wire in &mut first {
|
for wire in &mut first {
|
||||||
if wire.render_instance.is_none() {
|
if !is_standalone_analytical_curve(wire) && wire.render_instance.is_none() {
|
||||||
wire.render_instance = Some(
|
wire.render_instance = Some(
|
||||||
crate::scene::model::instance_model::RenderInstance {
|
crate::scene::model::instance_model::RenderInstance {
|
||||||
source_id: crate::scene::model::instance_model::next_source_id(),
|
source_id: crate::scene::model::instance_model::next_source_id(),
|
||||||
@@ -1149,7 +1149,7 @@ pub fn expand_insert(
|
|||||||
if let Some(guard) = prototype_guard.as_mut() {
|
if let Some(guard) = prototype_guard.as_mut() {
|
||||||
let translation = transform_translation(&xform);
|
let translation = transform_translation(&xform);
|
||||||
for wire in &mut wires {
|
for wire in &mut wires {
|
||||||
if wire.render_instance.is_none() {
|
if !is_standalone_analytical_curve(wire) && wire.render_instance.is_none() {
|
||||||
wire.render_instance = Some(crate::scene::model::instance_model::RenderInstance {
|
wire.render_instance = Some(crate::scene::model::instance_model::RenderInstance {
|
||||||
source_id: crate::scene::model::instance_model::next_source_id(),
|
source_id: crate::scene::model::instance_model::next_source_id(),
|
||||||
translation,
|
translation,
|
||||||
@@ -1268,7 +1268,8 @@ fn translated_prototype_wire(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
TangentGeom::PlanarCircle { center, .. }
|
TangentGeom::PlanarCircle { center, .. }
|
||||||
| TangentGeom::Arc { center, .. } => {
|
| TangentGeom::Arc { center, .. }
|
||||||
|
| TangentGeom::PlanarEllipse { center, .. } => {
|
||||||
for axis in 0..3 {
|
for axis in 0..3 {
|
||||||
center[axis] += delta[axis];
|
center[axis] += delta[axis];
|
||||||
}
|
}
|
||||||
@@ -1313,6 +1314,20 @@ fn aabb_pixel_size(local_aabb: [f32; 4], world_per_pixel: f32) -> f32 {
|
|||||||
w.max(h) / world_per_pixel
|
w.max(h) / world_per_pixel
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_standalone_analytical_curve(wire: &WireModel) -> bool {
|
||||||
|
wire.tangent_geoms.len() == 1
|
||||||
|
&& wire.fill_tris.is_empty()
|
||||||
|
&& wire.pick_tris.is_empty()
|
||||||
|
&& wire.text_verts.is_empty()
|
||||||
|
&& matches!(
|
||||||
|
wire.tangent_geoms[0],
|
||||||
|
TangentGeom::Circle { .. }
|
||||||
|
| TangentGeom::PlanarCircle { .. }
|
||||||
|
| TangentGeom::Arc { .. }
|
||||||
|
| TangentGeom::PlanarEllipse { .. }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
struct ExpandCtx<'a> {
|
struct ExpandCtx<'a> {
|
||||||
cache: &'a BlockCache,
|
cache: &'a BlockCache,
|
||||||
ins_color: [f32; 4],
|
ins_color: [f32; 4],
|
||||||
@@ -1930,12 +1945,14 @@ fn expand_defn(
|
|||||||
.collect();
|
.collect();
|
||||||
crate::scene::pick::xclip::clip_wires(&mut wires, &world_poly);
|
crate::scene::pick::xclip::clip_wires(&mut wires, &world_poly);
|
||||||
for wire in &mut wires {
|
for wire in &mut wires {
|
||||||
wire.render_instance = Some(
|
if !is_standalone_analytical_curve(wire) {
|
||||||
crate::scene::model::instance_model::RenderInstance {
|
wire.render_instance = Some(
|
||||||
source_id: crate::scene::model::instance_model::next_source_id(),
|
crate::scene::model::instance_model::RenderInstance {
|
||||||
translation,
|
source_id: crate::scene::model::instance_model::next_source_id(),
|
||||||
},
|
translation,
|
||||||
);
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
out.extra_wires.extend(wires.iter().cloned());
|
out.extra_wires.extend(wires.iter().cloned());
|
||||||
first = Some((wires, translation));
|
first = Some((wires, translation));
|
||||||
@@ -1983,7 +2000,7 @@ fn expand_defn(
|
|||||||
);
|
);
|
||||||
let mut wires = sub.finalize("", ctx.selected, ctx.bg_color);
|
let mut wires = sub.finalize("", ctx.selected, ctx.bg_color);
|
||||||
for wire in &mut wires {
|
for wire in &mut wires {
|
||||||
if wire.render_instance.is_none() {
|
if !is_standalone_analytical_curve(wire) && wire.render_instance.is_none() {
|
||||||
wire.render_instance = Some(
|
wire.render_instance = Some(
|
||||||
crate::scene::model::instance_model::RenderInstance {
|
crate::scene::model::instance_model::RenderInstance {
|
||||||
source_id: crate::scene::model::instance_model::next_source_id(),
|
source_id: crate::scene::model::instance_model::next_source_id(),
|
||||||
@@ -2296,6 +2313,134 @@ fn emit_wire(
|
|||||||
lw.line_weight_px
|
lw.line_weight_px
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Analytical GPU circle / arc / ellipse fast path:
|
||||||
|
// If this LocalWire is a pure analytical curve (single tangent geometry, no fills/text/pick),
|
||||||
|
// and transforms cleanly with accum_xform into an analytical curve, emit it as a standalone
|
||||||
|
// wire in extra_wires. This lets partition_wires extract it into CircleGpu / EllipseGpu for
|
||||||
|
// pixel-perfect analytical rendering on the GPU instead of segmented lines.
|
||||||
|
if !lw.tangent_geoms.is_empty()
|
||||||
|
&& lw.fill_tris.is_empty()
|
||||||
|
&& lw.pick_tris.is_empty()
|
||||||
|
&& lw.text_verts.is_empty()
|
||||||
|
{
|
||||||
|
let transformed_tangents: Option<Vec<TangentGeom>> = lw
|
||||||
|
.tangent_geoms
|
||||||
|
.iter()
|
||||||
|
.map(|tg| transform_tangent(tg, accum_xform))
|
||||||
|
.collect();
|
||||||
|
if let Some(tangents) = transformed_tangents {
|
||||||
|
if tangents.iter().all(|tangent| {
|
||||||
|
matches!(
|
||||||
|
tangent,
|
||||||
|
TangentGeom::Circle { .. }
|
||||||
|
| TangentGeom::PlanarCircle { .. }
|
||||||
|
| TangentGeom::Arc { .. }
|
||||||
|
| TangentGeom::PlanarEllipse { .. }
|
||||||
|
)
|
||||||
|
}) {
|
||||||
|
let contrast_bg = lw.contrast_bg.unwrap_or(ctx.bg_color);
|
||||||
|
let color = if lw.canvas_color {
|
||||||
|
ctx.bg_color
|
||||||
|
} else if lw.preserve_color {
|
||||||
|
final_color
|
||||||
|
} else {
|
||||||
|
crate::scene::view::render::adapt_to_bg(final_color, contrast_bg)
|
||||||
|
};
|
||||||
|
let bg_adapt: crate::scene::model::wire_model::BgAdapt =
|
||||||
|
(lw.canvas_color || !lw.preserve_color).then(|| {
|
||||||
|
Box::new(crate::scene::model::wire_model::BgAdaptInputs {
|
||||||
|
raw_color: final_color,
|
||||||
|
text_raw_colors: Vec::new(),
|
||||||
|
contrast_bg: lw.contrast_bg,
|
||||||
|
canvas_color: lw.canvas_color,
|
||||||
|
preserve_color: lw.preserve_color,
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut points = Vec::with_capacity(lw.points.len());
|
||||||
|
let mut points_low = Vec::with_capacity(lw.points.len());
|
||||||
|
let mut min_x = f32::INFINITY;
|
||||||
|
let mut min_y = f32::INFINITY;
|
||||||
|
let mut max_x = f32::NEG_INFINITY;
|
||||||
|
let mut max_y = f32::NEG_INFINITY;
|
||||||
|
for (idx, p) in lw.points.iter().enumerate() {
|
||||||
|
let pl = lw.points_low.get(idx).copied().unwrap_or([0.0; 3]);
|
||||||
|
let point = accum_xform.apply(Vector3::new(
|
||||||
|
p[0] as f64 + pl[0] as f64,
|
||||||
|
p[1] as f64 + pl[1] as f64,
|
||||||
|
p[2] as f64 + pl[2] as f64,
|
||||||
|
));
|
||||||
|
let (hx, lx) = WireModel::split_ds(point.x);
|
||||||
|
let (hy, ly) = WireModel::split_ds(point.y);
|
||||||
|
let (hz, lz) = WireModel::split_ds(point.z);
|
||||||
|
if hx < min_x { min_x = hx; }
|
||||||
|
if hy < min_y { min_y = hy; }
|
||||||
|
if hx > max_x { max_x = hx; }
|
||||||
|
if hy > max_y { max_y = hy; }
|
||||||
|
points.push([hx, hy, hz]);
|
||||||
|
points_low.push([lx, ly, lz]);
|
||||||
|
}
|
||||||
|
let aabb = if min_x.is_infinite() {
|
||||||
|
WireModel::UNBOUNDED_AABB
|
||||||
|
} else {
|
||||||
|
[min_x, min_y, max_x, max_y]
|
||||||
|
};
|
||||||
|
let mut snap_pts = Vec::with_capacity(lw.snap_pts.len());
|
||||||
|
for (p, hint) in &lw.snap_pts {
|
||||||
|
let v = accum_xform.apply(Vector3::new(p.x, p.y, p.z));
|
||||||
|
snap_pts.push((glam::DVec3::new(v.x, v.y, v.z), *hint));
|
||||||
|
}
|
||||||
|
let mut key_vertices = Vec::with_capacity(lw.key_vertices.len());
|
||||||
|
for p in &lw.key_vertices {
|
||||||
|
let v = accum_xform.apply(Vector3::new(p[0], p[1], p[2]));
|
||||||
|
key_vertices.push([v.x, v.y, v.z]);
|
||||||
|
}
|
||||||
|
let local_depth = if d_range != (0.0, 1.0) {
|
||||||
|
Some(d_range.0)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let wire = WireModel {
|
||||||
|
bg_adapt,
|
||||||
|
point_marker: lw.point_marker,
|
||||||
|
taper_widths: Vec::new(),
|
||||||
|
pattern_stations: Vec::new(),
|
||||||
|
world_width: 0.0,
|
||||||
|
depth_override: local_depth,
|
||||||
|
display_visible: !lw.hide_unselected || ctx.selected,
|
||||||
|
plot_visible: lw.plot_visible,
|
||||||
|
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: String::new(),
|
||||||
|
points,
|
||||||
|
points_low,
|
||||||
|
color,
|
||||||
|
selected: ctx.selected,
|
||||||
|
pattern_length: final_pat_len,
|
||||||
|
pattern: final_pat,
|
||||||
|
line_weight_px: final_lw_px,
|
||||||
|
aci: final_aci,
|
||||||
|
snap_pts,
|
||||||
|
tangent_geoms: tangents,
|
||||||
|
key_vertices,
|
||||||
|
aabb,
|
||||||
|
plinegen: lw.plinegen,
|
||||||
|
fill_tris: Vec::new(),
|
||||||
|
fill_tris_low: Vec::new(),
|
||||||
|
};
|
||||||
|
out.extra_wires.push(wire);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let station_values = pattern_station_values(&lw.pattern_stations, lw.points.len());
|
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 station_map = decode_pattern_station_map(&lw.pattern_stations, lw.points.len());
|
||||||
let transformed_stations = station_values.and_then(|(values, _)| {
|
let transformed_stations = station_values.and_then(|(values, _)| {
|
||||||
@@ -2727,6 +2872,31 @@ fn transform_tangent(
|
|||||||
radius: radius * ((sx + sy) * 0.5),
|
radius: radius * ((sx + sy) * 0.5),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
TangentGeom::PlanarEllipse {
|
||||||
|
center,
|
||||||
|
major_axis,
|
||||||
|
normal,
|
||||||
|
minor_axis_ratio,
|
||||||
|
start_param,
|
||||||
|
end_param,
|
||||||
|
} => {
|
||||||
|
let c = t.apply(Vector3::new(center[0], center[1], center[2]));
|
||||||
|
let m = t.apply_rotation(Vector3::new(major_axis[0], major_axis[1], major_axis[2]));
|
||||||
|
let n = t.apply_rotation(Vector3::new(normal[0], normal[1], normal[2]));
|
||||||
|
let n_len = n.length();
|
||||||
|
if n_len <= 1.0e-12 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let n = n / n_len;
|
||||||
|
Some(TangentGeom::PlanarEllipse {
|
||||||
|
center: [c.x, c.y, c.z],
|
||||||
|
major_axis: [m.x, m.y, m.z],
|
||||||
|
normal: [n.x, n.y, n.z],
|
||||||
|
minor_axis_ratio: *minor_axis_ratio,
|
||||||
|
start_param: *start_param,
|
||||||
|
end_param: *end_param,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -707,9 +707,11 @@ pub fn tessellate(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── Try the kernel path first ───────────────────────────────────────────
|
// ── Try the kernel path first ───────────────────────────────────────────
|
||||||
|
// Zoom-adaptive circles scale their segment count from the current zoom level and radius.
|
||||||
// Relative-PDSIZE points size their glyph from the current zoom so they
|
// Relative-PDSIZE points size their glyph from the current zoom so they
|
||||||
// stay a roughly constant on-screen size; otherwise the header-driven path.
|
// stay a roughly constant on-screen size; otherwise the header-driven path.
|
||||||
let te = crate::entities::point::relative_render(entity, document, world_per_pixel)
|
let te = crate::entities::circle::relative_render(entity, document, world_per_pixel)
|
||||||
|
.or_else(|| crate::entities::point::relative_render(entity, document, world_per_pixel))
|
||||||
.or_else(|| crate::entities::light::relative_render(entity, document, world_per_pixel))
|
.or_else(|| crate::entities::light::relative_render(entity, document, world_per_pixel))
|
||||||
.or_else(|| match entity {
|
.or_else(|| match entity {
|
||||||
EntityType::Text(text) => Some(crate::entities::text::to_render_at_scale(
|
EntityType::Text(text) => Some(crate::entities::text::to_render_at_scale(
|
||||||
|
|||||||
+300
-4
@@ -5275,8 +5275,8 @@ impl Scene {
|
|||||||
{
|
{
|
||||||
return arc;
|
return arc;
|
||||||
}
|
}
|
||||||
// Build once: full tessellation, no cull (region = None), no zoom LOD
|
// Build once: full tessellation, no cull, no zoom LOD — the resident
|
||||||
// (wpp = None) — for every space, exactly like the Model static-hold.
|
// set is zoom-independent (GPU analytical circles/arcs/ellipses).
|
||||||
let t_tess = iced::time::Instant::now();
|
let t_tess = iced::time::Instant::now();
|
||||||
let mut wires = self.wires_for_block_culled(
|
let mut wires = self.wires_for_block_culled(
|
||||||
block,
|
block,
|
||||||
@@ -5327,6 +5327,9 @@ impl Scene {
|
|||||||
// spaces or re-scaling a viewport can't accumulate dead full sets.
|
// spaces or re-scaling a viewport can't accumulate dead full sets.
|
||||||
let cur_epoch = self.geometry_epoch;
|
let cur_epoch = self.geometry_epoch;
|
||||||
sets.retain(|_, set| set.epoch == cur_epoch);
|
sets.retain(|_, set| set.epoch == cur_epoch);
|
||||||
|
if sets.len() > 16 {
|
||||||
|
sets.clear();
|
||||||
|
}
|
||||||
sets.insert(
|
sets.insert(
|
||||||
key,
|
key,
|
||||||
ResidentWireSet {
|
ResidentWireSet {
|
||||||
@@ -5362,6 +5365,9 @@ impl Scene {
|
|||||||
mix(annotation_scale_handle.map(|handle| handle.value()).unwrap_or(0));
|
mix(annotation_scale_handle.map(|handle| handle.value()).unwrap_or(0));
|
||||||
mix(all_visible as u64);
|
mix(all_visible as u64);
|
||||||
mix(self.viewport_style_key(style_viewport));
|
mix(self.viewport_style_key(style_viewport));
|
||||||
|
// quantized_wpp removed: GPU analytical rendering for circles/arcs/
|
||||||
|
// ellipses makes tessellation zoom-independent, so the resident set is
|
||||||
|
// shared across all zoom levels.
|
||||||
match frozen_layers {
|
match frozen_layers {
|
||||||
Some(frozen) => {
|
Some(frozen) => {
|
||||||
let mut signature = 0u64;
|
let mut signature = 0u64;
|
||||||
@@ -5376,6 +5382,18 @@ impl Scene {
|
|||||||
key
|
key
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Quantize world units per pixel (`wpp`) into half-octave bands (~1.414x zoom ratio).
|
||||||
|
///
|
||||||
|
/// This provides discrete, stable zoom bands so that panning (where wpp is constant)
|
||||||
|
/// and small subpixel viewport drifts or mouse-wheel motions within a band do not
|
||||||
|
/// cause re-tessellation or GPU buffer re-uploads, while zooming past a band threshold
|
||||||
|
/// smoothly scales circle tessellation detail.
|
||||||
|
pub fn quantize_wpp(wpp: Option<f32>) -> Option<f32> {
|
||||||
|
let w = wpp.filter(|&w| w.is_finite() && w > 0.0)?;
|
||||||
|
let step = (w.log2() * 2.0).floor() * 0.5;
|
||||||
|
Some(2.0f32.powf(step))
|
||||||
|
}
|
||||||
|
|
||||||
/// The GPU wire-arena handoff for a viewport whose content id is `gen`:
|
/// The GPU wire-arena handoff for a viewport whose content id is `gen`:
|
||||||
/// `(prev_gen, changed handles)` when the Model set reached `gen` via an
|
/// `(prev_gen, changed handles)` when the Model set reached `gen` via an
|
||||||
/// incremental resident patch, else `None`. Read (not consumed) so every
|
/// incremental resident patch, else `None`. Read (not consumed) so every
|
||||||
@@ -9061,7 +9079,7 @@ impl Scene {
|
|||||||
static EN: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
|
static EN: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
|
||||||
*EN.get_or_init(|| std::env::var("OCS_NO_RESIDENT_MEMO").is_err())
|
*EN.get_or_init(|| std::env::var("OCS_NO_RESIDENT_MEMO").is_err())
|
||||||
}
|
}
|
||||||
let resident = base_ok && view_aabb.is_none() && wpp.is_none() && resident_memo_enabled();
|
let resident = base_ok && view_aabb.is_none() && resident_memo_enabled();
|
||||||
let memo_active = base_ok && (view_aabb.is_some() || resident);
|
let memo_active = base_ok && (view_aabb.is_some() || resident);
|
||||||
// Reported in one line below. `tess_ms` / `memo_misses` are assigned by
|
// Reported in one line below. `tess_ms` / `memo_misses` are assigned by
|
||||||
// both branches; the rest exist only on the memo path and stay zero.
|
// both branches; the rest exist only on the memo path and stay zero.
|
||||||
@@ -9082,7 +9100,10 @@ impl Scene {
|
|||||||
let guard = {
|
let guard = {
|
||||||
let mut g: u64 = 0xcbf2_9ce4_8422_2325;
|
let mut g: u64 = 0xcbf2_9ce4_8422_2325;
|
||||||
let mut mix = |x: u64| g = g.rotate_left(13) ^ x;
|
let mut mix = |x: u64| g = g.rotate_left(13) ^ x;
|
||||||
mix(wpp.map(|w| w.to_bits() as u64).unwrap_or(u64::MAX));
|
// wpp removed from guard: GPU analytical rendering handles
|
||||||
|
// circles/arcs/ellipses, Point ignores wpp, and Light (the
|
||||||
|
// only remaining wpp consumer) is rare enough that its stale
|
||||||
|
// glyphs don't justify clearing every memoized entity on zoom.
|
||||||
if let Some(v) = view_aabb {
|
if let Some(v) = view_aabb {
|
||||||
for c in v {
|
for c in v {
|
||||||
mix(c.to_bits() as u64);
|
mix(c.to_bits() as u64);
|
||||||
@@ -11226,4 +11247,279 @@ mod layout_cache_tests {
|
|||||||
assert!(!source.is_empty());
|
assert!(!source.is_empty());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resident_wires_are_zoom_independent_and_gpu_analytical() {
|
||||||
|
let mut s = Scene::new();
|
||||||
|
let mut circle = acadrust::entities::Circle::default();
|
||||||
|
circle.radius = 100.0;
|
||||||
|
let handle = s.add_entity(EntityType::Circle(circle));
|
||||||
|
|
||||||
|
// Far camera: distance is large
|
||||||
|
let mut cam_far = Camera::default();
|
||||||
|
cam_far.distance = 1000.0;
|
||||||
|
|
||||||
|
// Close camera: distance is small
|
||||||
|
let mut cam_close = Camera::default();
|
||||||
|
cam_close.distance = 1.0;
|
||||||
|
|
||||||
|
let wires_far = s.model_tile_wires_arc(0, &cam_far, 1.0, 1000.0);
|
||||||
|
let circle_wire_far = wires_far
|
||||||
|
.iter()
|
||||||
|
.find(|w| w.name == handle.value().to_string())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let wires_close = s.model_tile_wires_arc(0, &cam_close, 1.0, 1000.0);
|
||||||
|
|
||||||
|
// Zooming must reuse the exact same resident wire set (zero re-tessellation)
|
||||||
|
assert!(
|
||||||
|
Arc::ptr_eq(&wires_far, &wires_close),
|
||||||
|
"Resident wires must be identical Arc across zoom levels"
|
||||||
|
);
|
||||||
|
// Circle must carry TangentGeom for GPU analytical rendering
|
||||||
|
assert_eq!(circle_wire_far.tangent_geoms.len(), 1);
|
||||||
|
assert!(matches!(
|
||||||
|
circle_wire_far.tangent_geoms[0],
|
||||||
|
crate::scene::model::wire_model::TangentGeom::PlanarCircle { .. }
|
||||||
|
| crate::scene::model::wire_model::TangentGeom::Circle { .. }
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn block_circles_and_arcs_extract_as_analytical_gpu_instances() {
|
||||||
|
let mut s = Scene::new();
|
||||||
|
// Create initial entities
|
||||||
|
let mut circle = acadrust::entities::Circle::default();
|
||||||
|
circle.radius = 50.0;
|
||||||
|
let c_h = s.add_entity(EntityType::Circle(circle));
|
||||||
|
|
||||||
|
let mut arc = acadrust::entities::Arc::default();
|
||||||
|
arc.radius = 25.0;
|
||||||
|
arc.start_angle = 0.0;
|
||||||
|
arc.end_angle = 3.14159;
|
||||||
|
let a_h = s.add_entity(EntityType::Arc(arc));
|
||||||
|
|
||||||
|
let line = acadrust::entities::Line::from_points(
|
||||||
|
acadrust::types::Vector3::new(0.0, 0.0, 0.0),
|
||||||
|
acadrust::types::Vector3::new(100.0, 100.0, 0.0),
|
||||||
|
);
|
||||||
|
let l_h = s.add_entity(EntityType::Line(line));
|
||||||
|
|
||||||
|
// Create block and place insert at (200, 300, 0)
|
||||||
|
let xform = acadrust::types::Transform::from_translation(acadrust::types::Vector3::new(200.0, 300.0, 0.0));
|
||||||
|
let _ = s.create_block_from_entities(
|
||||||
|
&[c_h, a_h, l_h],
|
||||||
|
"TEST_ANALYTICAL_BLOCK",
|
||||||
|
&acadrust::types::Transform::identity(),
|
||||||
|
&xform,
|
||||||
|
).unwrap();
|
||||||
|
|
||||||
|
let wires = s.model_tile_wires_arc(0, &Camera::default(), 1.0, 1000.0);
|
||||||
|
let depths = rustc_hash::FxHashMap::default();
|
||||||
|
let partitioned = crate::scene::pipeline::wire_arena::partition_wires(&wires, &depths);
|
||||||
|
|
||||||
|
// Both the circle and the arc inside the block must be extracted as analytical GPU circle instances!
|
||||||
|
assert!(
|
||||||
|
partitioned.circle_instances.len() >= 2,
|
||||||
|
"Expected at least 2 analytical circle instances from block, got {}",
|
||||||
|
partitioned.circle_instances.len()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn single_bulge_polyline_extracts_as_analytical_gpu_arc() {
|
||||||
|
let mut s = Scene::new();
|
||||||
|
let mut pline = acadrust::entities::LwPolyline::new();
|
||||||
|
pline.vertices = vec![
|
||||||
|
acadrust::entities::LwVertex {
|
||||||
|
location: acadrust::types::Vector2::new(0.0, 0.0),
|
||||||
|
bulge: 1.0, // semicircle
|
||||||
|
start_width: 0.0,
|
||||||
|
end_width: 0.0,
|
||||||
|
vertex_id: 0,
|
||||||
|
},
|
||||||
|
acadrust::entities::LwVertex {
|
||||||
|
location: acadrust::types::Vector2::new(100.0, 0.0),
|
||||||
|
bulge: 0.0,
|
||||||
|
start_width: 0.0,
|
||||||
|
end_width: 0.0,
|
||||||
|
vertex_id: 0,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
let _ = s.add_entity(EntityType::LwPolyline(pline));
|
||||||
|
|
||||||
|
let wires = s.model_tile_wires_arc(0, &Camera::default(), 1.0, 1000.0);
|
||||||
|
let depths = rustc_hash::FxHashMap::default();
|
||||||
|
let partitioned = crate::scene::pipeline::wire_arena::partition_wires(&wires, &depths);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
partitioned.circle_instances.len(),
|
||||||
|
1,
|
||||||
|
"Single bulge arc polyline must be extracted as analytical GPU arc instance"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn multi_bulge_polyline_extracts_as_analytical_gpu_arcs() {
|
||||||
|
let mut s = Scene::new();
|
||||||
|
let mut pline = acadrust::entities::LwPolyline::new();
|
||||||
|
// A circle represented as a closed 2-vertex polyline with two semicircle bulges (standard CAD polyline circle)
|
||||||
|
pline.is_closed = true;
|
||||||
|
pline.vertices = vec![
|
||||||
|
acadrust::entities::LwVertex {
|
||||||
|
location: acadrust::types::Vector2::new(0.0, 0.0),
|
||||||
|
bulge: 1.0,
|
||||||
|
start_width: 0.0,
|
||||||
|
end_width: 0.0,
|
||||||
|
vertex_id: 0,
|
||||||
|
},
|
||||||
|
acadrust::entities::LwVertex {
|
||||||
|
location: acadrust::types::Vector2::new(100.0, 0.0),
|
||||||
|
bulge: 1.0,
|
||||||
|
start_width: 0.0,
|
||||||
|
end_width: 0.0,
|
||||||
|
vertex_id: 1,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
let _ = s.add_entity(EntityType::LwPolyline(pline));
|
||||||
|
|
||||||
|
let wires = s.model_tile_wires_arc(0, &Camera::default(), 1.0, 1000.0);
|
||||||
|
let depths = rustc_hash::FxHashMap::default();
|
||||||
|
let partitioned = crate::scene::pipeline::wire_arena::partition_wires(&wires, &depths);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
partitioned.circle_instances.len(),
|
||||||
|
2,
|
||||||
|
"Multi-bulge closed polyline circle must be extracted as 2 analytical GPU arc instances"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
partitioned.regular.len(),
|
||||||
|
0,
|
||||||
|
"Multi-bulge pure arc polyline must not be sent to regular line arena"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn bench_analytical_rendering() {
|
||||||
|
use std::time::Instant;
|
||||||
|
use acadrust::entities::{Arc as AcadArc, Circle, Ellipse, LwPolyline, LwVertex};
|
||||||
|
use acadrust::types::{Vector2, Vector3};
|
||||||
|
|
||||||
|
let n_circles = 5000;
|
||||||
|
let n_arcs = 5000;
|
||||||
|
let n_ellipses = 2000;
|
||||||
|
let n_polylines = 2000;
|
||||||
|
let total_entities = n_circles + n_arcs + n_ellipses + n_polylines;
|
||||||
|
|
||||||
|
let mut scene = Scene::new();
|
||||||
|
for i in 0..n_circles {
|
||||||
|
let x = (i % 100) as f64 * 50.0;
|
||||||
|
let y = (i / 100) as f64 * 50.0;
|
||||||
|
let mut circle = Circle::default();
|
||||||
|
circle.center = Vector3::new(x, y, 0.0);
|
||||||
|
circle.radius = 10.0 + (i % 20) as f64;
|
||||||
|
scene.add_entity(EntityType::Circle(circle));
|
||||||
|
}
|
||||||
|
|
||||||
|
for i in 0..n_arcs {
|
||||||
|
let x = (i % 100) as f64 * 50.0 + 25.0;
|
||||||
|
let y = (i / 100) as f64 * 50.0;
|
||||||
|
let mut arc = AcadArc::default();
|
||||||
|
arc.center = Vector3::new(x, y, 0.0);
|
||||||
|
arc.radius = 15.0;
|
||||||
|
arc.start_angle = ((i % 8) as f64) * 0.25 * std::f64::consts::PI;
|
||||||
|
arc.end_angle = arc.start_angle + 0.75 * std::f64::consts::PI;
|
||||||
|
scene.add_entity(EntityType::Arc(arc));
|
||||||
|
}
|
||||||
|
|
||||||
|
for i in 0..n_ellipses {
|
||||||
|
let x = (i % 50) as f64 * 100.0;
|
||||||
|
let y = (i / 50) as f64 * 100.0 + 5000.0;
|
||||||
|
let mut el = Ellipse::default();
|
||||||
|
el.center = Vector3::new(x, y, 0.0);
|
||||||
|
el.major_axis = Vector3::new(20.0, 0.0, 0.0);
|
||||||
|
el.minor_axis_ratio = 0.5;
|
||||||
|
el.start_parameter = 0.0;
|
||||||
|
el.end_parameter = std::f64::consts::TAU;
|
||||||
|
scene.add_entity(EntityType::Ellipse(el));
|
||||||
|
}
|
||||||
|
|
||||||
|
for i in 0..n_polylines {
|
||||||
|
let x = (i % 50) as f64 * 100.0 + 50.0;
|
||||||
|
let y = (i / 50) as f64 * 100.0 + 5000.0;
|
||||||
|
let mut pline = LwPolyline::new();
|
||||||
|
pline.is_closed = true;
|
||||||
|
pline.vertices = vec![
|
||||||
|
LwVertex {
|
||||||
|
location: Vector2::new(x, y),
|
||||||
|
bulge: 1.0,
|
||||||
|
start_width: 0.0,
|
||||||
|
end_width: 0.0,
|
||||||
|
vertex_id: 0,
|
||||||
|
},
|
||||||
|
LwVertex {
|
||||||
|
location: Vector2::new(x + 30.0, y),
|
||||||
|
bulge: 1.0,
|
||||||
|
start_width: 0.0,
|
||||||
|
end_width: 0.0,
|
||||||
|
vertex_id: 1,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
scene.add_entity(EntityType::LwPolyline(pline));
|
||||||
|
}
|
||||||
|
|
||||||
|
let cam = Camera::default();
|
||||||
|
let t_tess_start = Instant::now();
|
||||||
|
let wires = scene.model_tile_wires_arc(0, &cam, 1.0, 1000.0);
|
||||||
|
let tess_duration = t_tess_start.elapsed();
|
||||||
|
|
||||||
|
let total_wires = wires.len();
|
||||||
|
let mut chord_points = 0usize;
|
||||||
|
let mut analytical_tangents = 0usize;
|
||||||
|
for w in wires.iter() {
|
||||||
|
chord_points += w.points.len();
|
||||||
|
analytical_tangents += w.tangent_geoms.len();
|
||||||
|
}
|
||||||
|
|
||||||
|
let depths = rustc_hash::FxHashMap::default();
|
||||||
|
let t_part_start = Instant::now();
|
||||||
|
let iters = 100;
|
||||||
|
let mut part = None;
|
||||||
|
for _ in 0..iters {
|
||||||
|
part = Some(crate::scene::pipeline::wire_arena::partition_wires(&wires, &depths));
|
||||||
|
}
|
||||||
|
let part_duration = t_part_start.elapsed() / (iters as u32);
|
||||||
|
let p = part.unwrap();
|
||||||
|
|
||||||
|
// Zoom simulation
|
||||||
|
let zoom_levels = [0.1f32, 0.2, 0.5, 1.0, 2.0, 5.0, 10.0, 50.0, 100.0, 1000.0];
|
||||||
|
let t_zoom_start = Instant::now();
|
||||||
|
let mut dynamic_cam = Camera::default();
|
||||||
|
for &zoom in zoom_levels.iter().cycle().take(100) {
|
||||||
|
dynamic_cam.distance = 1000.0 / zoom;
|
||||||
|
let _w = scene.model_tile_wires_arc(0, &dynamic_cam, 1.0, 1000.0);
|
||||||
|
}
|
||||||
|
let zoom_100_duration = t_zoom_start.elapsed();
|
||||||
|
let per_zoom_ms = zoom_100_duration.as_secs_f64() * 1000.0 / 100.0;
|
||||||
|
|
||||||
|
let line_vram_mb = (chord_points * 36) as f64 / (1024.0 * 1024.0);
|
||||||
|
let inst_vram_mb = (p.circle_instances.len() * 128 + p.ellipse_instances.len() * 144) as f64 / (1024.0 * 1024.0);
|
||||||
|
|
||||||
|
println!("\n=== BENCHMARK REPORT: CURRENT HEAD (WITH GPU ANALYTICAL & OPTIMIZATIONS) ===");
|
||||||
|
println!("Curved Entities Tested: {total_entities}");
|
||||||
|
println!("Initial Wire Build Time: {:.2?}", tess_duration);
|
||||||
|
println!("Total Wires: {total_wires}");
|
||||||
|
println!("Tessellated Chord Vertices: {chord_points}");
|
||||||
|
println!("Analytical Tangent Geometries: {analytical_tangents}");
|
||||||
|
println!("Partitioning Time (per frame): {:.3?}", part_duration);
|
||||||
|
println!("Regular Line Wires: {}", p.regular.len());
|
||||||
|
println!("GPU Circle/Arc Instances: {}", p.circle_instances.len());
|
||||||
|
println!("GPU Ellipse Instances: {}", p.ellipse_instances.len());
|
||||||
|
println!("Line Arena VRAM (chord lines): {:.2} MB", line_vram_mb);
|
||||||
|
println!("GPU Analytical VRAM: {:.2} MB", inst_vram_mb);
|
||||||
|
println!("VRAM Reduction Ratio: {:.1}x", if inst_vram_mb > 0.0 { line_vram_mb / inst_vram_mb } else { 0.0 });
|
||||||
|
println!("100 Camera Zoom Navigations: {:.2?}", zoom_100_duration);
|
||||||
|
println!("Zoom Frame Overhead: {:.3} ms ({:.0} FPS)", per_zoom_ms, 1000.0 / per_zoom_ms.max(0.001));
|
||||||
|
println!("============================================================================\n");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -47,6 +47,15 @@ pub enum TangentGeom {
|
|||||||
start_angle: f64,
|
start_angle: f64,
|
||||||
end_angle: f64,
|
end_angle: f64,
|
||||||
},
|
},
|
||||||
|
/// Ellipse or elliptical arc in an arbitrary world-space plane.
|
||||||
|
PlanarEllipse {
|
||||||
|
center: [f64; 3],
|
||||||
|
major_axis: [f64; 3],
|
||||||
|
normal: [f64; 3],
|
||||||
|
minor_axis_ratio: f64,
|
||||||
|
start_param: f64,
|
||||||
|
end_param: f64,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Viewport-relative display transform for a point marker.
|
/// Viewport-relative display transform for a point marker.
|
||||||
|
|||||||
@@ -197,6 +197,9 @@ pub fn clip_wires(wires: &mut Vec<WireModel>, poly: &[[f64; 2]]) {
|
|||||||
} else {
|
} else {
|
||||||
w.pattern_stations.clear();
|
w.pattern_stations.clear();
|
||||||
}
|
}
|
||||||
|
if !w.tangent_geoms.is_empty() {
|
||||||
|
w.tangent_geoms.clear();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if !w.fill_tris.is_empty() {
|
if !w.fill_tris.is_empty() {
|
||||||
let local: Vec<[f32; 3]> = (0..w.fill_tris.len())
|
let local: Vec<[f32; 3]> = (0..w.fill_tris.len())
|
||||||
|
|||||||
@@ -0,0 +1,413 @@
|
|||||||
|
//! GPU-instanced analytical circle buffer & pipeline layout.
|
||||||
|
//!
|
||||||
|
//! Renders 2D/planar circles as analytical screen/plane-aligned quads with
|
||||||
|
//! screen-space derivative anti-aliasing. Each circle is a single instance
|
||||||
|
//! (6 vertices for a 2-triangle quad), eliminating CPU re-tessellation on zoom
|
||||||
|
//! and providing infinite visual smoothness.
|
||||||
|
|
||||||
|
use iced::wgpu;
|
||||||
|
|
||||||
|
#[repr(C)]
|
||||||
|
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
|
||||||
|
pub struct CircleInstance {
|
||||||
|
pub center_high: [f32; 3],
|
||||||
|
pub _pad0: f32,
|
||||||
|
pub center_low: [f32; 3],
|
||||||
|
pub radius: f32,
|
||||||
|
pub axis_x: [f32; 3],
|
||||||
|
pub start_angle: f32,
|
||||||
|
pub axis_y: [f32; 3],
|
||||||
|
pub end_angle: f32,
|
||||||
|
pub color: [f32; 4],
|
||||||
|
/// `[half_width, pattern_length, draw_depth, padding]`
|
||||||
|
pub params: [f32; 4],
|
||||||
|
pub pat0: [f32; 4],
|
||||||
|
pub pat1: [f32; 4],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CircleInstance {
|
||||||
|
pub fn layout<'a>() -> wgpu::VertexBufferLayout<'a> {
|
||||||
|
const ATTRS: &[wgpu::VertexAttribute] = &[
|
||||||
|
wgpu::VertexAttribute {
|
||||||
|
offset: std::mem::offset_of!(CircleInstance, center_high) as u64,
|
||||||
|
shader_location: 0,
|
||||||
|
format: wgpu::VertexFormat::Float32x4,
|
||||||
|
},
|
||||||
|
wgpu::VertexAttribute {
|
||||||
|
offset: std::mem::offset_of!(CircleInstance, center_low) as u64,
|
||||||
|
shader_location: 1,
|
||||||
|
format: wgpu::VertexFormat::Float32x4,
|
||||||
|
},
|
||||||
|
wgpu::VertexAttribute {
|
||||||
|
offset: std::mem::offset_of!(CircleInstance, axis_x) as u64,
|
||||||
|
shader_location: 2,
|
||||||
|
format: wgpu::VertexFormat::Float32x4,
|
||||||
|
},
|
||||||
|
wgpu::VertexAttribute {
|
||||||
|
offset: std::mem::offset_of!(CircleInstance, axis_y) as u64,
|
||||||
|
shader_location: 3,
|
||||||
|
format: wgpu::VertexFormat::Float32x4,
|
||||||
|
},
|
||||||
|
wgpu::VertexAttribute {
|
||||||
|
offset: std::mem::offset_of!(CircleInstance, color) as u64,
|
||||||
|
shader_location: 4,
|
||||||
|
format: wgpu::VertexFormat::Float32x4,
|
||||||
|
},
|
||||||
|
wgpu::VertexAttribute {
|
||||||
|
offset: std::mem::offset_of!(CircleInstance, params) as u64,
|
||||||
|
shader_location: 5,
|
||||||
|
format: wgpu::VertexFormat::Float32x4,
|
||||||
|
},
|
||||||
|
wgpu::VertexAttribute {
|
||||||
|
offset: std::mem::offset_of!(CircleInstance, pat0) as u64,
|
||||||
|
shader_location: 6,
|
||||||
|
format: wgpu::VertexFormat::Float32x4,
|
||||||
|
},
|
||||||
|
wgpu::VertexAttribute {
|
||||||
|
offset: std::mem::offset_of!(CircleInstance, pat1) as u64,
|
||||||
|
shader_location: 7,
|
||||||
|
format: wgpu::VertexFormat::Float32x4,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
wgpu::VertexBufferLayout {
|
||||||
|
array_stride: std::mem::size_of::<CircleInstance>() as u64,
|
||||||
|
step_mode: wgpu::VertexStepMode::Instance,
|
||||||
|
attributes: ATTRS,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct CircleGpu {
|
||||||
|
pub instance_buffer: wgpu::Buffer,
|
||||||
|
pub instance_count: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CircleGpu {
|
||||||
|
pub fn from_instances(
|
||||||
|
device: &wgpu::Device,
|
||||||
|
queue: &wgpu::Queue,
|
||||||
|
label: &str,
|
||||||
|
instances: &[CircleInstance],
|
||||||
|
) -> Self {
|
||||||
|
let instance_buffer = super::gpu_upload::upload_buffer(
|
||||||
|
device,
|
||||||
|
queue,
|
||||||
|
label,
|
||||||
|
instances,
|
||||||
|
wgpu::BufferUsages::VERTEX,
|
||||||
|
);
|
||||||
|
Self {
|
||||||
|
instance_buffer,
|
||||||
|
instance_count: instances.len() as u32,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build the base and xray circle pipelines.
|
||||||
|
pub fn create_pipelines(
|
||||||
|
device: &wgpu::Device,
|
||||||
|
frame_bgl: &wgpu::BindGroupLayout,
|
||||||
|
color_format: wgpu::TextureFormat,
|
||||||
|
sample_count: u32,
|
||||||
|
content_stencil: &wgpu::StencilState,
|
||||||
|
) -> (wgpu::RenderPipeline, wgpu::RenderPipeline) {
|
||||||
|
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||||
|
label: Some("circle.wgsl"),
|
||||||
|
source: wgpu::ShaderSource::Wgsl(include_str!("../../shaders/circle.wgsl").into()),
|
||||||
|
});
|
||||||
|
let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||||
|
label: Some("circle.pipeline.layout"),
|
||||||
|
bind_group_layouts: &[Some(frame_bgl)],
|
||||||
|
immediate_size: 0,
|
||||||
|
});
|
||||||
|
let circle_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||||
|
label: Some("circle.pipeline"),
|
||||||
|
layout: Some(&layout),
|
||||||
|
vertex: wgpu::VertexState {
|
||||||
|
module: &shader,
|
||||||
|
entry_point: Some("vs_main"),
|
||||||
|
buffers: &[CircleInstance::layout()],
|
||||||
|
compilation_options: Default::default(),
|
||||||
|
},
|
||||||
|
fragment: Some(wgpu::FragmentState {
|
||||||
|
module: &shader,
|
||||||
|
entry_point: Some("fs_main"),
|
||||||
|
targets: &[Some(wgpu::ColorTargetState {
|
||||||
|
format: color_format,
|
||||||
|
blend: Some(wgpu::BlendState::ALPHA_BLENDING),
|
||||||
|
write_mask: wgpu::ColorWrites::ALL,
|
||||||
|
})],
|
||||||
|
compilation_options: Default::default(),
|
||||||
|
}),
|
||||||
|
primitive: wgpu::PrimitiveState {
|
||||||
|
topology: wgpu::PrimitiveTopology::TriangleList,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
depth_stencil: Some(wgpu::DepthStencilState {
|
||||||
|
format: wgpu::TextureFormat::Depth24PlusStencil8,
|
||||||
|
depth_write_enabled: Some(true),
|
||||||
|
depth_compare: Some(wgpu::CompareFunction::LessEqual),
|
||||||
|
stencil: content_stencil.clone(),
|
||||||
|
bias: wgpu::DepthBiasState::default(),
|
||||||
|
}),
|
||||||
|
multisample: wgpu::MultisampleState {
|
||||||
|
count: sample_count,
|
||||||
|
mask: !0,
|
||||||
|
alpha_to_coverage_enabled: false,
|
||||||
|
},
|
||||||
|
multiview_mask: None,
|
||||||
|
cache: None,
|
||||||
|
});
|
||||||
|
let circle_xray_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||||
|
label: Some("circle_xray.pipeline"),
|
||||||
|
layout: Some(&layout),
|
||||||
|
vertex: wgpu::VertexState {
|
||||||
|
module: &shader,
|
||||||
|
entry_point: Some("vs_main"),
|
||||||
|
buffers: &[CircleInstance::layout()],
|
||||||
|
compilation_options: Default::default(),
|
||||||
|
},
|
||||||
|
fragment: Some(wgpu::FragmentState {
|
||||||
|
module: &shader,
|
||||||
|
entry_point: Some("fs_main"),
|
||||||
|
targets: &[Some(wgpu::ColorTargetState {
|
||||||
|
format: color_format,
|
||||||
|
blend: Some(wgpu::BlendState::ALPHA_BLENDING),
|
||||||
|
write_mask: wgpu::ColorWrites::ALL,
|
||||||
|
})],
|
||||||
|
compilation_options: Default::default(),
|
||||||
|
}),
|
||||||
|
primitive: wgpu::PrimitiveState {
|
||||||
|
topology: wgpu::PrimitiveTopology::TriangleList,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
depth_stencil: Some(wgpu::DepthStencilState {
|
||||||
|
format: wgpu::TextureFormat::Depth24PlusStencil8,
|
||||||
|
depth_write_enabled: Some(false),
|
||||||
|
depth_compare: Some(wgpu::CompareFunction::Always),
|
||||||
|
stencil: content_stencil.clone(),
|
||||||
|
bias: wgpu::DepthBiasState::default(),
|
||||||
|
}),
|
||||||
|
multisample: wgpu::MultisampleState {
|
||||||
|
count: sample_count,
|
||||||
|
mask: !0,
|
||||||
|
alpha_to_coverage_enabled: false,
|
||||||
|
},
|
||||||
|
multiview_mask: None,
|
||||||
|
cache: None,
|
||||||
|
});
|
||||||
|
(circle_pipeline, circle_xray_pipeline)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper to extract an analytical circle or circular arc instance from a single `TangentGeom`.
|
||||||
|
pub fn extract_circle_instance_from_geom(
|
||||||
|
geom: &crate::scene::model::wire_model::TangentGeom,
|
||||||
|
wire: &crate::scene::WireModel,
|
||||||
|
draw_depth: f32,
|
||||||
|
) -> Option<CircleInstance> {
|
||||||
|
let (center, axis_x, axis_y, radius, start_angle, end_angle) = match *geom {
|
||||||
|
crate::scene::model::wire_model::TangentGeom::Circle { center, radius } => (
|
||||||
|
[center[0] as f64, center[1] as f64, center[2] as f64],
|
||||||
|
[1.0, 0.0, 0.0],
|
||||||
|
[0.0, 1.0, 0.0],
|
||||||
|
radius as f64,
|
||||||
|
0.0f32,
|
||||||
|
std::f32::consts::TAU,
|
||||||
|
),
|
||||||
|
crate::scene::model::wire_model::TangentGeom::PlanarCircle {
|
||||||
|
center,
|
||||||
|
axis_x,
|
||||||
|
axis_y,
|
||||||
|
radius,
|
||||||
|
} => (center, axis_x, axis_y, radius, 0.0f32, std::f32::consts::TAU),
|
||||||
|
crate::scene::model::wire_model::TangentGeom::Arc {
|
||||||
|
center,
|
||||||
|
axis_x,
|
||||||
|
axis_y,
|
||||||
|
radius,
|
||||||
|
start_angle,
|
||||||
|
end_angle,
|
||||||
|
} => (
|
||||||
|
center,
|
||||||
|
axis_x,
|
||||||
|
axis_y,
|
||||||
|
radius,
|
||||||
|
start_angle as f32,
|
||||||
|
end_angle as f32,
|
||||||
|
),
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
if radius <= 0.0 || !radius.is_finite() || !start_angle.is_finite() || !end_angle.is_finite() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let (ch, cl) = split_ds_xyz(center[0], center[1], center[2]);
|
||||||
|
let hw = wire.line_weight_px * 0.5;
|
||||||
|
let pat0 = [wire.pattern[0], wire.pattern[1], wire.pattern[2], wire.pattern[3]];
|
||||||
|
let pat1 = [wire.pattern[4], wire.pattern[5], wire.pattern[6], wire.pattern[7]];
|
||||||
|
|
||||||
|
Some(CircleInstance {
|
||||||
|
center_high: ch,
|
||||||
|
_pad0: 0.0,
|
||||||
|
center_low: cl,
|
||||||
|
radius: radius as f32,
|
||||||
|
axis_x: [axis_x[0] as f32, axis_x[1] as f32, axis_x[2] as f32],
|
||||||
|
start_angle,
|
||||||
|
axis_y: [axis_y[0] as f32, axis_y[1] as f32, axis_y[2] as f32],
|
||||||
|
end_angle,
|
||||||
|
color: wire.color,
|
||||||
|
params: [hw, wire.pattern_length, draw_depth, 0.0],
|
||||||
|
pat0,
|
||||||
|
pat1,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper to extract all analytical circle/arc instances from a `WireModel`.
|
||||||
|
///
|
||||||
|
/// Returns `Some(instances)` if the wire is non-empty, contains solely analytical
|
||||||
|
/// circle/arc tangent geometries, and has no mesh/pick fills, text, or block instance.
|
||||||
|
pub fn extract_circle_instances(
|
||||||
|
wire: &crate::scene::WireModel,
|
||||||
|
draw_depth: f32,
|
||||||
|
) -> Option<Vec<CircleInstance>> {
|
||||||
|
if wire.tangent_geoms.is_empty()
|
||||||
|
|| !wire.fill_tris.is_empty()
|
||||||
|
|| !wire.pick_tris.is_empty()
|
||||||
|
|| !wire.text_verts.is_empty()
|
||||||
|
|| wire.render_instance.is_some()
|
||||||
|
{
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut instances = Vec::with_capacity(wire.tangent_geoms.len());
|
||||||
|
for geom in &wire.tangent_geoms {
|
||||||
|
if let Some(inst) = extract_circle_instance_from_geom(geom, wire, draw_depth) {
|
||||||
|
instances.push(inst);
|
||||||
|
} else {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(instances)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper to extract a single analytical circle or circular arc instance from a `WireModel`.
|
||||||
|
pub fn extract_circle_instance(
|
||||||
|
wire: &crate::scene::WireModel,
|
||||||
|
draw_depth: f32,
|
||||||
|
) -> Option<CircleInstance> {
|
||||||
|
if wire.tangent_geoms.len() == 1 {
|
||||||
|
extract_circle_instances(wire, draw_depth).and_then(|mut v| v.pop())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn split_ds_xyz(x: f64, y: f64, z: f64) -> ([f32; 3], [f32; 3]) {
|
||||||
|
let hx = x as f32;
|
||||||
|
let lx = (x - hx as f64) as f32;
|
||||||
|
let hy = y as f32;
|
||||||
|
let ly = (y - hy as f64) as f32;
|
||||||
|
let hz = z as f32;
|
||||||
|
let lz = (z - hz as f64) as f32;
|
||||||
|
([hx, hy, hz], [lx, ly, lz])
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn circle_instance_size_matches_stride() {
|
||||||
|
assert_eq!(std::mem::size_of::<CircleInstance>(), 128);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn circle_shader_validates_with_naga() {
|
||||||
|
let source = include_str!("../../shaders/circle.wgsl");
|
||||||
|
let module = naga::front::wgsl::parse_str(source).expect("circle.wgsl parses cleanly");
|
||||||
|
let mut validator = naga::valid::Validator::new(
|
||||||
|
naga::valid::ValidationFlags::all(),
|
||||||
|
naga::valid::Capabilities::all(),
|
||||||
|
);
|
||||||
|
validator
|
||||||
|
.validate(&module)
|
||||||
|
.expect("circle.wgsl validates cleanly with naga");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn extract_circle_instance_planar_success() {
|
||||||
|
let mut wire = crate::scene::WireModel::default();
|
||||||
|
wire.tangent_geoms.push(crate::scene::model::wire_model::TangentGeom::PlanarCircle {
|
||||||
|
center: [100.0, 200.0, 300.0],
|
||||||
|
axis_x: [1.0, 0.0, 0.0],
|
||||||
|
axis_y: [0.0, 1.0, 0.0],
|
||||||
|
radius: 50.0,
|
||||||
|
});
|
||||||
|
wire.line_weight_px = 2.0;
|
||||||
|
wire.color = [1.0, 0.0, 0.0, 1.0];
|
||||||
|
|
||||||
|
let inst = extract_circle_instance(&wire, 1.5).expect("should extract planar circle");
|
||||||
|
assert_eq!(inst.radius, 50.0);
|
||||||
|
assert_eq!(inst.start_angle, 0.0);
|
||||||
|
assert_eq!(inst.end_angle, std::f32::consts::TAU);
|
||||||
|
assert_eq!(inst.center_high, [100.0, 200.0, 300.0]);
|
||||||
|
assert_eq!(inst.color, [1.0, 0.0, 0.0, 1.0]);
|
||||||
|
assert_eq!(inst.params[0], 1.0); // half lineweight
|
||||||
|
assert_eq!(inst.params[2], 1.5); // draw depth
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn extract_circle_instance_arc_success() {
|
||||||
|
let mut wire = crate::scene::WireModel::default();
|
||||||
|
wire.tangent_geoms.push(crate::scene::model::wire_model::TangentGeom::Arc {
|
||||||
|
center: [10.0, 20.0, 30.0],
|
||||||
|
axis_x: [1.0, 0.0, 0.0],
|
||||||
|
axis_y: [0.0, 1.0, 0.0],
|
||||||
|
radius: 25.0,
|
||||||
|
start_angle: 0.5,
|
||||||
|
end_angle: 2.5,
|
||||||
|
});
|
||||||
|
wire.line_weight_px = 3.0;
|
||||||
|
wire.color = [0.0, 1.0, 0.0, 1.0];
|
||||||
|
|
||||||
|
let inst = extract_circle_instance(&wire, 0.5).expect("should extract circular arc");
|
||||||
|
assert_eq!(inst.radius, 25.0);
|
||||||
|
assert_eq!(inst.start_angle, 0.5);
|
||||||
|
assert_eq!(inst.end_angle, 2.5);
|
||||||
|
assert_eq!(inst.center_high, [10.0, 20.0, 30.0]);
|
||||||
|
assert_eq!(inst.color, [0.0, 1.0, 0.0, 1.0]);
|
||||||
|
assert_eq!(inst.params[0], 1.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn extract_circle_instance_rejects_non_planar_or_complex() {
|
||||||
|
let mut wire = crate::scene::WireModel::default();
|
||||||
|
wire.tangent_geoms.push(crate::scene::model::wire_model::TangentGeom::PlanarCircle {
|
||||||
|
center: [0.0, 0.0, 0.0],
|
||||||
|
axis_x: [1.0, 0.0, 0.0],
|
||||||
|
axis_y: [0.0, 1.0, 0.0],
|
||||||
|
radius: 10.0,
|
||||||
|
});
|
||||||
|
// 1. With fill_tris (e.g. 3D solid or hatch)
|
||||||
|
wire.fill_tris = vec![[0.0; 3]];
|
||||||
|
assert!(extract_circle_instance(&wire, 0.0).is_none());
|
||||||
|
|
||||||
|
// 2. Clear fill_tris, add render_instance (block instance)
|
||||||
|
wire.fill_tris.clear();
|
||||||
|
wire.render_instance = Some(crate::scene::model::instance_model::RenderInstance {
|
||||||
|
source_id: 1,
|
||||||
|
translation: [0.0; 3],
|
||||||
|
});
|
||||||
|
assert!(extract_circle_instance(&wire, 0.0).is_none());
|
||||||
|
|
||||||
|
// 3. Invalid radius
|
||||||
|
wire.render_instance = None;
|
||||||
|
wire.tangent_geoms[0] = crate::scene::model::wire_model::TangentGeom::PlanarCircle {
|
||||||
|
center: [0.0, 0.0, 0.0],
|
||||||
|
axis_x: [1.0, 0.0, 0.0],
|
||||||
|
axis_y: [0.0, 1.0, 0.0],
|
||||||
|
radius: -5.0,
|
||||||
|
};
|
||||||
|
assert!(extract_circle_instance(&wire, 0.0).is_none());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,450 @@
|
|||||||
|
//! GPU-instanced analytical ellipse buffer & pipeline layout.
|
||||||
|
//!
|
||||||
|
//! Renders 2D/planar ellipses and elliptical arcs as analytical screen/plane-aligned quads
|
||||||
|
//! with sub-pixel screen-space derivative anti-aliasing. Each ellipse is a single instance
|
||||||
|
//! (6 vertices for a 2-triangle quad), eliminating CPU re-tessellation on zoom and providing
|
||||||
|
//! mathematically exact curvature at any magnification.
|
||||||
|
|
||||||
|
use iced::wgpu;
|
||||||
|
|
||||||
|
#[repr(C)]
|
||||||
|
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
|
||||||
|
pub struct EllipseInstance {
|
||||||
|
pub center_high: [f32; 3],
|
||||||
|
pub _pad0: f32,
|
||||||
|
pub center_low: [f32; 3],
|
||||||
|
pub minor_axis_ratio: f32,
|
||||||
|
pub major_axis: [f32; 3],
|
||||||
|
pub start_param: f32,
|
||||||
|
pub normal: [f32; 3],
|
||||||
|
pub end_param: f32,
|
||||||
|
pub color: [f32; 4],
|
||||||
|
/// `[half_width, pattern_length, draw_depth, padding]`
|
||||||
|
pub params: [f32; 4],
|
||||||
|
pub pat0: [f32; 4],
|
||||||
|
pub pat1: [f32; 4],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EllipseInstance {
|
||||||
|
pub fn layout<'a>() -> wgpu::VertexBufferLayout<'a> {
|
||||||
|
const ATTRS: &[wgpu::VertexAttribute] = &[
|
||||||
|
wgpu::VertexAttribute {
|
||||||
|
offset: std::mem::offset_of!(EllipseInstance, center_high) as u64,
|
||||||
|
shader_location: 0,
|
||||||
|
format: wgpu::VertexFormat::Float32x4,
|
||||||
|
},
|
||||||
|
wgpu::VertexAttribute {
|
||||||
|
offset: std::mem::offset_of!(EllipseInstance, center_low) as u64,
|
||||||
|
shader_location: 1,
|
||||||
|
format: wgpu::VertexFormat::Float32x4,
|
||||||
|
},
|
||||||
|
wgpu::VertexAttribute {
|
||||||
|
offset: std::mem::offset_of!(EllipseInstance, major_axis) as u64,
|
||||||
|
shader_location: 2,
|
||||||
|
format: wgpu::VertexFormat::Float32x4,
|
||||||
|
},
|
||||||
|
wgpu::VertexAttribute {
|
||||||
|
offset: std::mem::offset_of!(EllipseInstance, normal) as u64,
|
||||||
|
shader_location: 3,
|
||||||
|
format: wgpu::VertexFormat::Float32x4,
|
||||||
|
},
|
||||||
|
wgpu::VertexAttribute {
|
||||||
|
offset: std::mem::offset_of!(EllipseInstance, color) as u64,
|
||||||
|
shader_location: 4,
|
||||||
|
format: wgpu::VertexFormat::Float32x4,
|
||||||
|
},
|
||||||
|
wgpu::VertexAttribute {
|
||||||
|
offset: std::mem::offset_of!(EllipseInstance, params) as u64,
|
||||||
|
shader_location: 5,
|
||||||
|
format: wgpu::VertexFormat::Float32x4,
|
||||||
|
},
|
||||||
|
wgpu::VertexAttribute {
|
||||||
|
offset: std::mem::offset_of!(EllipseInstance, pat0) as u64,
|
||||||
|
shader_location: 6,
|
||||||
|
format: wgpu::VertexFormat::Float32x4,
|
||||||
|
},
|
||||||
|
wgpu::VertexAttribute {
|
||||||
|
offset: std::mem::offset_of!(EllipseInstance, pat1) as u64,
|
||||||
|
shader_location: 7,
|
||||||
|
format: wgpu::VertexFormat::Float32x4,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
wgpu::VertexBufferLayout {
|
||||||
|
array_stride: std::mem::size_of::<EllipseInstance>() as u64,
|
||||||
|
step_mode: wgpu::VertexStepMode::Instance,
|
||||||
|
attributes: ATTRS,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct EllipseGpu {
|
||||||
|
pub instance_buffer: wgpu::Buffer,
|
||||||
|
pub instance_count: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EllipseGpu {
|
||||||
|
pub fn from_instances(
|
||||||
|
device: &wgpu::Device,
|
||||||
|
queue: &wgpu::Queue,
|
||||||
|
label: &str,
|
||||||
|
instances: &[EllipseInstance],
|
||||||
|
) -> Self {
|
||||||
|
let instance_buffer = super::gpu_upload::upload_buffer(
|
||||||
|
device,
|
||||||
|
queue,
|
||||||
|
label,
|
||||||
|
instances,
|
||||||
|
wgpu::BufferUsages::VERTEX,
|
||||||
|
);
|
||||||
|
Self {
|
||||||
|
instance_buffer,
|
||||||
|
instance_count: instances.len() as u32,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build the base and xray ellipse pipelines.
|
||||||
|
pub fn create_pipelines(
|
||||||
|
device: &wgpu::Device,
|
||||||
|
frame_bgl: &wgpu::BindGroupLayout,
|
||||||
|
color_format: wgpu::TextureFormat,
|
||||||
|
sample_count: u32,
|
||||||
|
content_stencil: &wgpu::StencilState,
|
||||||
|
) -> (wgpu::RenderPipeline, wgpu::RenderPipeline) {
|
||||||
|
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||||
|
label: Some("ellipse.wgsl"),
|
||||||
|
source: wgpu::ShaderSource::Wgsl(include_str!("../../shaders/ellipse.wgsl").into()),
|
||||||
|
});
|
||||||
|
let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||||
|
label: Some("ellipse.pipeline.layout"),
|
||||||
|
bind_group_layouts: &[Some(frame_bgl)],
|
||||||
|
immediate_size: 0,
|
||||||
|
});
|
||||||
|
let ellipse_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||||
|
label: Some("ellipse.pipeline"),
|
||||||
|
layout: Some(&layout),
|
||||||
|
vertex: wgpu::VertexState {
|
||||||
|
module: &shader,
|
||||||
|
entry_point: Some("vs_main"),
|
||||||
|
buffers: &[EllipseInstance::layout()],
|
||||||
|
compilation_options: Default::default(),
|
||||||
|
},
|
||||||
|
fragment: Some(wgpu::FragmentState {
|
||||||
|
module: &shader,
|
||||||
|
entry_point: Some("fs_main"),
|
||||||
|
targets: &[Some(wgpu::ColorTargetState {
|
||||||
|
format: color_format,
|
||||||
|
blend: Some(wgpu::BlendState::ALPHA_BLENDING),
|
||||||
|
write_mask: wgpu::ColorWrites::ALL,
|
||||||
|
})],
|
||||||
|
compilation_options: Default::default(),
|
||||||
|
}),
|
||||||
|
primitive: wgpu::PrimitiveState {
|
||||||
|
topology: wgpu::PrimitiveTopology::TriangleList,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
depth_stencil: Some(wgpu::DepthStencilState {
|
||||||
|
format: wgpu::TextureFormat::Depth24PlusStencil8,
|
||||||
|
depth_write_enabled: Some(true),
|
||||||
|
depth_compare: Some(wgpu::CompareFunction::LessEqual),
|
||||||
|
stencil: content_stencil.clone(),
|
||||||
|
bias: wgpu::DepthBiasState::default(),
|
||||||
|
}),
|
||||||
|
multisample: wgpu::MultisampleState {
|
||||||
|
count: sample_count,
|
||||||
|
mask: !0,
|
||||||
|
alpha_to_coverage_enabled: false,
|
||||||
|
},
|
||||||
|
multiview_mask: None,
|
||||||
|
cache: None,
|
||||||
|
});
|
||||||
|
let ellipse_xray_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||||
|
label: Some("ellipse_xray.pipeline"),
|
||||||
|
layout: Some(&layout),
|
||||||
|
vertex: wgpu::VertexState {
|
||||||
|
module: &shader,
|
||||||
|
entry_point: Some("vs_main"),
|
||||||
|
buffers: &[EllipseInstance::layout()],
|
||||||
|
compilation_options: Default::default(),
|
||||||
|
},
|
||||||
|
fragment: Some(wgpu::FragmentState {
|
||||||
|
module: &shader,
|
||||||
|
entry_point: Some("fs_main"),
|
||||||
|
targets: &[Some(wgpu::ColorTargetState {
|
||||||
|
format: color_format,
|
||||||
|
blend: Some(wgpu::BlendState::ALPHA_BLENDING),
|
||||||
|
write_mask: wgpu::ColorWrites::ALL,
|
||||||
|
})],
|
||||||
|
compilation_options: Default::default(),
|
||||||
|
}),
|
||||||
|
primitive: wgpu::PrimitiveState {
|
||||||
|
topology: wgpu::PrimitiveTopology::TriangleList,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
depth_stencil: Some(wgpu::DepthStencilState {
|
||||||
|
format: wgpu::TextureFormat::Depth24PlusStencil8,
|
||||||
|
depth_write_enabled: Some(false),
|
||||||
|
depth_compare: Some(wgpu::CompareFunction::Always),
|
||||||
|
stencil: content_stencil.clone(),
|
||||||
|
bias: wgpu::DepthBiasState::default(),
|
||||||
|
}),
|
||||||
|
multisample: wgpu::MultisampleState {
|
||||||
|
count: sample_count,
|
||||||
|
mask: !0,
|
||||||
|
alpha_to_coverage_enabled: false,
|
||||||
|
},
|
||||||
|
multiview_mask: None,
|
||||||
|
cache: None,
|
||||||
|
});
|
||||||
|
(ellipse_pipeline, ellipse_xray_pipeline)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper to extract an analytical ellipse instance from a single `TangentGeom`.
|
||||||
|
pub fn extract_ellipse_instance_from_geom(
|
||||||
|
geom: &crate::scene::model::wire_model::TangentGeom,
|
||||||
|
wire: &crate::scene::WireModel,
|
||||||
|
draw_depth: f32,
|
||||||
|
) -> Option<EllipseInstance> {
|
||||||
|
let crate::scene::model::wire_model::TangentGeom::PlanarEllipse {
|
||||||
|
center,
|
||||||
|
major_axis,
|
||||||
|
normal,
|
||||||
|
minor_axis_ratio,
|
||||||
|
start_param,
|
||||||
|
end_param,
|
||||||
|
} = *geom else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
|
||||||
|
let major_len_sq = major_axis[0] * major_axis[0] + major_axis[1] * major_axis[1] + major_axis[2] * major_axis[2];
|
||||||
|
let norm_len_sq = normal[0] * normal[0] + normal[1] * normal[1] + normal[2] * normal[2];
|
||||||
|
if major_len_sq <= 1e-12 || !major_len_sq.is_finite()
|
||||||
|
|| norm_len_sq <= 1e-12 || !norm_len_sq.is_finite()
|
||||||
|
|| minor_axis_ratio <= 1e-6 || !minor_axis_ratio.is_finite()
|
||||||
|
|| !start_param.is_finite() || !end_param.is_finite()
|
||||||
|
{
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let (ch, cl) = split_ds_xyz(center[0], center[1], center[2]);
|
||||||
|
let hw = wire.line_weight_px * 0.5;
|
||||||
|
let pat0 = [wire.pattern[0], wire.pattern[1], wire.pattern[2], wire.pattern[3]];
|
||||||
|
let pat1 = [wire.pattern[4], wire.pattern[5], wire.pattern[6], wire.pattern[7]];
|
||||||
|
|
||||||
|
Some(EllipseInstance {
|
||||||
|
center_high: ch,
|
||||||
|
_pad0: 0.0,
|
||||||
|
center_low: cl,
|
||||||
|
minor_axis_ratio: minor_axis_ratio.min(1.0) as f32,
|
||||||
|
major_axis: [major_axis[0] as f32, major_axis[1] as f32, major_axis[2] as f32],
|
||||||
|
start_param: start_param as f32,
|
||||||
|
normal: [normal[0] as f32, normal[1] as f32, normal[2] as f32],
|
||||||
|
end_param: end_param as f32,
|
||||||
|
color: wire.color,
|
||||||
|
params: [hw, wire.pattern_length, draw_depth, 0.0],
|
||||||
|
pat0,
|
||||||
|
pat1,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper to extract all analytical ellipse instances from a `WireModel`.
|
||||||
|
pub fn extract_ellipse_instances(
|
||||||
|
wire: &crate::scene::WireModel,
|
||||||
|
draw_depth: f32,
|
||||||
|
) -> Option<Vec<EllipseInstance>> {
|
||||||
|
if wire.tangent_geoms.is_empty()
|
||||||
|
|| !wire.fill_tris.is_empty()
|
||||||
|
|| !wire.pick_tris.is_empty()
|
||||||
|
|| !wire.text_verts.is_empty()
|
||||||
|
|| wire.render_instance.is_some()
|
||||||
|
{
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut instances = Vec::with_capacity(wire.tangent_geoms.len());
|
||||||
|
for geom in &wire.tangent_geoms {
|
||||||
|
if let Some(inst) = extract_ellipse_instance_from_geom(geom, wire, draw_depth) {
|
||||||
|
instances.push(inst);
|
||||||
|
} else {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(instances)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper to extract a single analytical ellipse instance from a `WireModel`.
|
||||||
|
pub fn extract_ellipse_instance(
|
||||||
|
wire: &crate::scene::WireModel,
|
||||||
|
draw_depth: f32,
|
||||||
|
) -> Option<EllipseInstance> {
|
||||||
|
if wire.tangent_geoms.len() == 1 {
|
||||||
|
extract_ellipse_instances(wire, draw_depth).and_then(|mut v| v.pop())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn split_ds_xyz(x: f64, y: f64, z: f64) -> ([f32; 3], [f32; 3]) {
|
||||||
|
let hx = x as f32;
|
||||||
|
let lx = (x - hx as f64) as f32;
|
||||||
|
let hy = y as f32;
|
||||||
|
let ly = (y - hy as f64) as f32;
|
||||||
|
let hz = z as f32;
|
||||||
|
let lz = (z - hz as f64) as f32;
|
||||||
|
([hx, hy, hz], [lx, ly, lz])
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ellipse_instance_size_matches_stride() {
|
||||||
|
assert_eq!(std::mem::size_of::<EllipseInstance>(), 128);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ellipse_shader_validates_with_naga() {
|
||||||
|
let source = include_str!("../../shaders/ellipse.wgsl");
|
||||||
|
let module = naga::front::wgsl::parse_str(source).expect("ellipse.wgsl parses cleanly");
|
||||||
|
let mut validator = naga::valid::Validator::new(
|
||||||
|
naga::valid::ValidationFlags::all(),
|
||||||
|
naga::valid::Capabilities::all(),
|
||||||
|
);
|
||||||
|
validator
|
||||||
|
.validate(&module)
|
||||||
|
.expect("ellipse.wgsl validates cleanly with naga");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn extract_ellipse_instance_success() {
|
||||||
|
let mut wire = crate::scene::WireModel::default();
|
||||||
|
wire.tangent_geoms.push(crate::scene::model::wire_model::TangentGeom::PlanarEllipse {
|
||||||
|
center: [100.0, 200.0, 300.0],
|
||||||
|
major_axis: [50.0, 0.0, 0.0],
|
||||||
|
normal: [0.0, 0.0, 1.0],
|
||||||
|
minor_axis_ratio: 0.5,
|
||||||
|
start_param: 0.0,
|
||||||
|
end_param: std::f64::consts::TAU,
|
||||||
|
});
|
||||||
|
wire.line_weight_px = 2.0;
|
||||||
|
wire.color = [1.0, 0.5, 0.0, 1.0];
|
||||||
|
|
||||||
|
let inst = extract_ellipse_instance(&wire, 1.0).expect("should extract ellipse");
|
||||||
|
assert_eq!(inst.center_high, [100.0, 200.0, 300.0]);
|
||||||
|
assert_eq!(inst.major_axis, [50.0, 0.0, 0.0]);
|
||||||
|
assert_eq!(inst.minor_axis_ratio, 0.5);
|
||||||
|
assert_eq!(inst.start_param, 0.0);
|
||||||
|
assert_eq!(inst.end_param, std::f32::consts::TAU);
|
||||||
|
assert_eq!(inst.color, [1.0, 0.5, 0.0, 1.0]);
|
||||||
|
assert_eq!(inst.params[0], 1.0);
|
||||||
|
assert_eq!(inst.params[2], 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn extract_elliptical_arc_instance_success() {
|
||||||
|
let mut wire = crate::scene::WireModel::default();
|
||||||
|
wire.tangent_geoms.push(crate::scene::model::wire_model::TangentGeom::PlanarEllipse {
|
||||||
|
center: [0.0, 0.0, 0.0],
|
||||||
|
major_axis: [10.0, 0.0, 0.0],
|
||||||
|
normal: [0.0, 0.0, 1.0],
|
||||||
|
minor_axis_ratio: 0.8,
|
||||||
|
start_param: 0.5,
|
||||||
|
end_param: 2.5,
|
||||||
|
});
|
||||||
|
wire.line_weight_px = 1.0;
|
||||||
|
wire.color = [0.0, 1.0, 0.0, 1.0];
|
||||||
|
|
||||||
|
let inst = extract_ellipse_instance(&wire, 0.5).expect("should extract elliptical arc");
|
||||||
|
assert_eq!(inst.minor_axis_ratio, 0.8);
|
||||||
|
assert_eq!(inst.start_param, 0.5);
|
||||||
|
assert_eq!(inst.end_param, 2.5);
|
||||||
|
assert_eq!(inst.params[0], 0.5); // half-width
|
||||||
|
assert_eq!(inst.params[2], 0.5); // draw-depth
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rust_closest_point_ellipse(p_in: [f32; 2], ab_in: [f32; 2]) -> [f32; 2] {
|
||||||
|
let px = p_in[0].abs();
|
||||||
|
let py = p_in[1].abs();
|
||||||
|
let a = ab_in[0];
|
||||||
|
let b = ab_in[1];
|
||||||
|
if (a - b).abs() < 1e-6 {
|
||||||
|
let len = (px * px + py * py).sqrt();
|
||||||
|
let q = if len > 1e-6 {
|
||||||
|
[px / len * a, py / len * a]
|
||||||
|
} else {
|
||||||
|
[a, 0.0]
|
||||||
|
};
|
||||||
|
return [
|
||||||
|
if p_in[0] >= 0.0 { q[0] } else { -q[0] },
|
||||||
|
if p_in[1] >= 0.0 { q[1] } else { -q[1] },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initial estimate from eccentric anomaly of radial direction
|
||||||
|
let mut t = (py * a).atan2(px * b);
|
||||||
|
let k = a * a - b * b;
|
||||||
|
|
||||||
|
// 3 iterations of Newton-Raphson
|
||||||
|
for _ in 0..3 {
|
||||||
|
let (st, ct) = t.sin_cos();
|
||||||
|
let f = k * st * ct - px * a * st + py * b * ct;
|
||||||
|
let f_prime = k * (ct * ct - st * st) - px * a * ct - py * b * st;
|
||||||
|
if f_prime.abs() > 1e-7 {
|
||||||
|
t -= f / f_prime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t = t.clamp(0.0, std::f32::consts::FRAC_PI_2);
|
||||||
|
let q = [a * t.cos(), b * t.sin()];
|
||||||
|
[
|
||||||
|
if p_in[0] >= 0.0 { q[0] } else { -q[0] },
|
||||||
|
if p_in[1] >= 0.0 { q[1] } else { -q[1] },
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn closest_point_ellipse_accuracy() {
|
||||||
|
let a = 100.0f32;
|
||||||
|
let b = 40.0f32;
|
||||||
|
// Test points directly on the ellipse: closest point should be itself!
|
||||||
|
for i in 0..36 {
|
||||||
|
let angle = (i as f32) * std::f32::consts::TAU / 36.0;
|
||||||
|
let px = a * angle.cos();
|
||||||
|
let py = b * angle.sin();
|
||||||
|
let q = rust_closest_point_ellipse([px, py], [a, b]);
|
||||||
|
let dist = ((q[0] - px).powi(2) + (q[1] - py).powi(2)).sqrt();
|
||||||
|
assert!(
|
||||||
|
dist < 0.2,
|
||||||
|
"Point on ellipse at angle {angle} failed: px={px}, py={py}, q={:?}, dist={dist}",
|
||||||
|
q
|
||||||
|
);
|
||||||
|
// Verify q is on the ellipse (x/a)^2 + (y/b)^2 == 1
|
||||||
|
let eq = (q[0] / a).powi(2) + (q[1] / b).powi(2);
|
||||||
|
assert!((eq - 1.0).abs() < 0.01, "eq={eq} for angle {angle}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test points offset from ellipse (inside and outside)
|
||||||
|
for i in 0..36 {
|
||||||
|
let angle = (i as f32) * std::f32::consts::TAU / 36.0;
|
||||||
|
for offset in [-5.0f32, -1.0, 1.0, 5.0] {
|
||||||
|
let norm = [angle.cos() / a, angle.sin() / b];
|
||||||
|
let norm_len = (norm[0] * norm[0] + norm[1] * norm[1]).sqrt();
|
||||||
|
let un = [norm[0] / norm_len, norm[1] / norm_len];
|
||||||
|
let px = a * angle.cos() + offset * un[0];
|
||||||
|
let py = b * angle.sin() + offset * un[1];
|
||||||
|
let q = rust_closest_point_ellipse([px, py], [a, b]);
|
||||||
|
let eq = (q[0] / a).powi(2) + (q[1] / b).powi(2);
|
||||||
|
assert!(
|
||||||
|
(eq - 1.0).abs() < 0.05,
|
||||||
|
"Offset point at angle {angle}, offset {offset}: q={:?}, eq={eq}",
|
||||||
|
q
|
||||||
|
);
|
||||||
|
let measured_dist = ((q[0] - px).powi(2) + (q[1] - py).powi(2)).sqrt();
|
||||||
|
assert!(
|
||||||
|
(measured_dist - offset.abs()).abs() < 0.2,
|
||||||
|
"Distance error at angle {angle}, offset {offset}: measured={measured_dist}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+282
-31
@@ -1,6 +1,8 @@
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod gpu_tests;
|
mod gpu_tests;
|
||||||
mod device_capabilities;
|
mod device_capabilities;
|
||||||
|
pub mod circle_gpu;
|
||||||
|
pub mod ellipse_gpu;
|
||||||
pub mod face3d_gpu;
|
pub mod face3d_gpu;
|
||||||
pub mod gpu_budget;
|
pub mod gpu_budget;
|
||||||
pub mod gpu_upload;
|
pub mod gpu_upload;
|
||||||
@@ -20,6 +22,8 @@ pub mod wire_gpu;
|
|||||||
use iced::wgpu;
|
use iced::wgpu;
|
||||||
use iced::{Rectangle, Size};
|
use iced::{Rectangle, Size};
|
||||||
|
|
||||||
|
pub use circle_gpu::{CircleGpu, CircleInstance};
|
||||||
|
pub use ellipse_gpu::{EllipseGpu, EllipseInstance};
|
||||||
pub use face3d_gpu::Face3DGpu;
|
pub use face3d_gpu::Face3DGpu;
|
||||||
pub use wipeout_gpu::WipeoutGpu;
|
pub use wipeout_gpu::WipeoutGpu;
|
||||||
pub use image_gpu::ImageGpu;
|
pub use image_gpu::ImageGpu;
|
||||||
@@ -79,6 +83,8 @@ pub struct Pipeline {
|
|||||||
shadow_pipeline: wgpu::RenderPipeline,
|
shadow_pipeline: wgpu::RenderPipeline,
|
||||||
shadow_plain_pipeline: wgpu::RenderPipeline,
|
shadow_plain_pipeline: wgpu::RenderPipeline,
|
||||||
wire_pipeline: wgpu::RenderPipeline,
|
wire_pipeline: wgpu::RenderPipeline,
|
||||||
|
circle_pipeline: wgpu::RenderPipeline,
|
||||||
|
ellipse_pipeline: wgpu::RenderPipeline,
|
||||||
block_wire_pipeline: wgpu::RenderPipeline,
|
block_wire_pipeline: wgpu::RenderPipeline,
|
||||||
/// Stamps clip-boundary polygons into the stencil buffer (viewports + XCLIP).
|
/// Stamps clip-boundary polygons into the stencil buffer (viewports + XCLIP).
|
||||||
clip_mask_pipeline: wgpu::RenderPipeline,
|
clip_mask_pipeline: wgpu::RenderPipeline,
|
||||||
@@ -89,6 +95,8 @@ pub struct Pipeline {
|
|||||||
/// Same shader as wire_pipeline but depth_compare=Greater, depth_write_enabled=false.
|
/// Same shader as wire_pipeline but depth_compare=Greater, depth_write_enabled=false.
|
||||||
/// Used to draw ghost copies of selected wires through occluding geometry.
|
/// Used to draw ghost copies of selected wires through occluding geometry.
|
||||||
wire_xray_pipeline: wgpu::RenderPipeline,
|
wire_xray_pipeline: wgpu::RenderPipeline,
|
||||||
|
circle_xray_pipeline: wgpu::RenderPipeline,
|
||||||
|
ellipse_xray_pipeline: wgpu::RenderPipeline,
|
||||||
block_wire_xray_pipeline: wgpu::RenderPipeline,
|
block_wire_xray_pipeline: wgpu::RenderPipeline,
|
||||||
/// Layout for the per-wire `WireConst` storage buffer (group 1 of the wire /
|
/// Layout for the per-wire `WireConst` storage buffer (group 1 of the wire /
|
||||||
/// xray pipelines). `Some` on any storage-capable device; `None` in packed
|
/// xray pipelines). `Some` on any storage-capable device; `None` in packed
|
||||||
@@ -225,6 +233,8 @@ pub struct Pipeline {
|
|||||||
/// replace this thin draw-range list on camera changes without touching the
|
/// replace this thin draw-range list on camera changes without touching the
|
||||||
/// shared resident buffer.
|
/// shared resident buffer.
|
||||||
pub(crate) gpu_wires: std::sync::Arc<Vec<WireGpu>>,
|
pub(crate) gpu_wires: std::sync::Arc<Vec<WireGpu>>,
|
||||||
|
pub(crate) gpu_circles: std::sync::Arc<Vec<CircleGpu>>,
|
||||||
|
pub(crate) gpu_ellipses: std::sync::Arc<Vec<EllipseGpu>>,
|
||||||
pub(crate) gpu_block_wires: std::sync::Arc<Vec<BlockWireGpu>>,
|
pub(crate) gpu_block_wires: std::sync::Arc<Vec<BlockWireGpu>>,
|
||||||
/// Persistent per-entity wire instance arena (capability-selected format).
|
/// Persistent per-entity wire instance arena (capability-selected format).
|
||||||
/// When active, `gpu_wires` is a thin wrapper over this arena's buffers and an
|
/// When active, `gpu_wires` is a thin wrapper over this arena's buffers and an
|
||||||
@@ -256,6 +266,8 @@ pub struct Pipeline {
|
|||||||
clip_boundary: Option<(wgpu::Buffer, u32)>,
|
clip_boundary: Option<(wgpu::Buffer, u32)>,
|
||||||
/// Ghost copies (25% alpha) of selected wires for the X-ray depth pass.
|
/// Ghost copies (25% alpha) of selected wires for the X-ray depth pass.
|
||||||
gpu_selected_wires: Vec<WireGpu>,
|
gpu_selected_wires: Vec<WireGpu>,
|
||||||
|
gpu_selected_circles: Vec<CircleGpu>,
|
||||||
|
gpu_selected_ellipses: Vec<EllipseGpu>,
|
||||||
gpu_selected_block_wires: Vec<BlockWireGpu>,
|
gpu_selected_block_wires: Vec<BlockWireGpu>,
|
||||||
/// Command-preview / interim / grip-drag overlay wires. Re-uploaded every
|
/// Command-preview / interim / grip-drag overlay wires. Re-uploaded every
|
||||||
/// frame they are present (small), drawn on top of the base wire pass — so
|
/// frame they are present (small), drawn on top of the base wire pass — so
|
||||||
@@ -2083,6 +2095,22 @@ impl Pipeline {
|
|||||||
&content_stencil,
|
&content_stencil,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let (circle_pipeline, circle_xray_pipeline) = circle_gpu::create_pipelines(
|
||||||
|
device,
|
||||||
|
&frame_bgl,
|
||||||
|
format,
|
||||||
|
MSAA_SAMPLES,
|
||||||
|
&content_stencil,
|
||||||
|
);
|
||||||
|
|
||||||
|
let (ellipse_pipeline, ellipse_xray_pipeline) = ellipse_gpu::create_pipelines(
|
||||||
|
device,
|
||||||
|
&frame_bgl,
|
||||||
|
format,
|
||||||
|
MSAA_SAMPLES,
|
||||||
|
&content_stencil,
|
||||||
|
);
|
||||||
|
|
||||||
let viewcube = ViewCubePipeline::new(device, queue, format);
|
let viewcube = ViewCubePipeline::new(device, queue, format);
|
||||||
|
|
||||||
let init_size = Size::new(1, 1);
|
let init_size = Size::new(1, 1);
|
||||||
@@ -2223,6 +2251,10 @@ impl Pipeline {
|
|||||||
wire_black_pipeline,
|
wire_black_pipeline,
|
||||||
block_wire_black_pipeline,
|
block_wire_black_pipeline,
|
||||||
wire_xray_pipeline,
|
wire_xray_pipeline,
|
||||||
|
circle_pipeline,
|
||||||
|
circle_xray_pipeline,
|
||||||
|
ellipse_pipeline,
|
||||||
|
ellipse_xray_pipeline,
|
||||||
block_wire_xray_pipeline,
|
block_wire_xray_pipeline,
|
||||||
wire_const_bgl,
|
wire_const_bgl,
|
||||||
block_wire_const_bgl,
|
block_wire_const_bgl,
|
||||||
@@ -2295,6 +2327,8 @@ impl Pipeline {
|
|||||||
blit_uniform_buffer,
|
blit_uniform_buffer,
|
||||||
surface_format: format,
|
surface_format: format,
|
||||||
gpu_wires: std::sync::Arc::new(vec![]),
|
gpu_wires: std::sync::Arc::new(vec![]),
|
||||||
|
gpu_circles: std::sync::Arc::new(vec![]),
|
||||||
|
gpu_ellipses: std::sync::Arc::new(vec![]),
|
||||||
gpu_block_wires: std::sync::Arc::new(vec![]),
|
gpu_block_wires: std::sync::Arc::new(vec![]),
|
||||||
wire_arena: None,
|
wire_arena: None,
|
||||||
wire_arena_mesh: None,
|
wire_arena_mesh: None,
|
||||||
@@ -2308,6 +2342,8 @@ impl Pipeline {
|
|||||||
silhouette_key: (usize::MAX, u64::MAX, [u32::MAX; 3], false),
|
silhouette_key: (usize::MAX, u64::MAX, [u32::MAX; 3], false),
|
||||||
clip_boundary: None,
|
clip_boundary: None,
|
||||||
gpu_selected_wires: vec![],
|
gpu_selected_wires: vec![],
|
||||||
|
gpu_selected_circles: vec![],
|
||||||
|
gpu_selected_ellipses: vec![],
|
||||||
gpu_selected_block_wires: vec![],
|
gpu_selected_block_wires: vec![],
|
||||||
gpu_preview_wires: vec![],
|
gpu_preview_wires: vec![],
|
||||||
gpu_wipeouts: vec![],
|
gpu_wipeouts: vec![],
|
||||||
@@ -2371,6 +2407,86 @@ impl Pipeline {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Upload GPU-instanced analytical circles from a wire slice.
|
||||||
|
pub fn upload_circles(
|
||||||
|
&mut self,
|
||||||
|
device: &wgpu::Device,
|
||||||
|
queue: &wgpu::Queue,
|
||||||
|
wires: &[WireModel],
|
||||||
|
depth_map: &rustc_hash::FxHashMap<u64, [f32; 2]>,
|
||||||
|
) -> Vec<CircleGpu> {
|
||||||
|
let mut instances: Vec<CircleInstance> = Vec::new();
|
||||||
|
for wire in wires {
|
||||||
|
if !wire.display_visible {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let depth = wire_gpu::wire_draw_depth(wire, depth_map);
|
||||||
|
if let Some(insts) = circle_gpu::extract_circle_instances(wire, depth) {
|
||||||
|
instances.extend(insts);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.upload_circles_from_instances(device, queue, &instances)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Upload GPU-instanced analytical circles from pre-extracted instances.
|
||||||
|
pub fn upload_circles_from_instances(
|
||||||
|
&mut self,
|
||||||
|
device: &wgpu::Device,
|
||||||
|
queue: &wgpu::Queue,
|
||||||
|
instances: &[CircleInstance],
|
||||||
|
) -> Vec<CircleGpu> {
|
||||||
|
if instances.is_empty() {
|
||||||
|
vec![]
|
||||||
|
} else {
|
||||||
|
vec![CircleGpu::from_instances(
|
||||||
|
device,
|
||||||
|
queue,
|
||||||
|
"viewer.circles",
|
||||||
|
instances,
|
||||||
|
)]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Upload GPU-instanced analytical ellipses from a wire slice.
|
||||||
|
pub fn upload_ellipses(
|
||||||
|
&mut self,
|
||||||
|
device: &wgpu::Device,
|
||||||
|
queue: &wgpu::Queue,
|
||||||
|
wires: &[WireModel],
|
||||||
|
depth_map: &rustc_hash::FxHashMap<u64, [f32; 2]>,
|
||||||
|
) -> Vec<EllipseGpu> {
|
||||||
|
let mut instances: Vec<EllipseInstance> = Vec::new();
|
||||||
|
for wire in wires {
|
||||||
|
if !wire.display_visible {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let depth = wire_gpu::wire_draw_depth(wire, depth_map);
|
||||||
|
if let Some(insts) = ellipse_gpu::extract_ellipse_instances(wire, depth) {
|
||||||
|
instances.extend(insts);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.upload_ellipses_from_instances(device, queue, &instances)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Upload GPU-instanced analytical ellipses from pre-extracted instances.
|
||||||
|
pub fn upload_ellipses_from_instances(
|
||||||
|
&mut self,
|
||||||
|
device: &wgpu::Device,
|
||||||
|
queue: &wgpu::Queue,
|
||||||
|
instances: &[EllipseInstance],
|
||||||
|
) -> Vec<EllipseGpu> {
|
||||||
|
if instances.is_empty() {
|
||||||
|
vec![]
|
||||||
|
} else {
|
||||||
|
vec![EllipseGpu::from_instances(
|
||||||
|
device,
|
||||||
|
queue,
|
||||||
|
"viewer.ellipses",
|
||||||
|
instances,
|
||||||
|
)]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Build resident batches and a handle index shared across viewport slots.
|
/// Build resident batches and a handle index shared across viewport slots.
|
||||||
pub fn build_wire_buffers(
|
pub fn build_wire_buffers(
|
||||||
&mut self,
|
&mut self,
|
||||||
@@ -2383,6 +2499,8 @@ impl Pipeline {
|
|||||||
std::sync::Arc<Vec<WireGpu>>,
|
std::sync::Arc<Vec<WireGpu>>,
|
||||||
std::sync::Arc<Vec<BlockWireGpu>>,
|
std::sync::Arc<Vec<BlockWireGpu>>,
|
||||||
std::sync::Arc<rustc_hash::FxHashMap<u64, Vec<u32>>>,
|
std::sync::Arc<rustc_hash::FxHashMap<u64, Vec<u32>>>,
|
||||||
|
std::sync::Arc<Vec<CircleGpu>>,
|
||||||
|
std::sync::Arc<Vec<EllipseGpu>>,
|
||||||
) {
|
) {
|
||||||
// Batch the wire pass: instead of one GPU buffer + one draw call per
|
// Batch the wire pass: instead of one GPU buffer + one draw call per
|
||||||
// WireModel (tens of thousands on a large drawing), merge maximal runs
|
// WireModel (tens of thousands on a large drawing), merge maximal runs
|
||||||
@@ -2392,40 +2510,42 @@ impl Pipeline {
|
|||||||
// wire order — already sorted by draw order — is preserved; depth bias
|
// wire order — already sorted by draw order — is preserved; depth bias
|
||||||
// and alpha blending both depend on it. Scissor and mesh-edge stay
|
// and alpha blending both depend on it. Scissor and mesh-edge stay
|
||||||
// grouping keys because the draw loop sets one scissor per batch and
|
// grouping keys because the draw loop sets one scissor per batch and
|
||||||
// skips whole mesh-edge batches in shaded modes.
|
|
||||||
let is_mesh_edge = |w: &WireModel| !w.points.is_empty() && w.fill_is_3d;
|
|
||||||
let mut batches: Vec<WireGpu> = Vec::new();
|
let mut batches: Vec<WireGpu> = Vec::new();
|
||||||
let mut block_wires: Vec<&WireModel> = Vec::new();
|
let mut block_wires: Vec<&WireModel> = Vec::new();
|
||||||
let mut i = 0;
|
let mut i = 0;
|
||||||
while i < wires.len() {
|
while i < wires.len() {
|
||||||
if !wires[i].display_visible {
|
let kind = wire_arena::classify_wire(&wires[i]);
|
||||||
i += 1;
|
match kind {
|
||||||
continue;
|
wire_arena::WireKind::Invisible
|
||||||
|
| wire_arena::WireKind::Circle
|
||||||
|
| wire_arena::WireKind::Ellipse
|
||||||
|
| wire_arena::WireKind::Empty => {
|
||||||
|
i += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
wire_arena::WireKind::Block => {
|
||||||
|
block_wires.push(&wires[i]);
|
||||||
|
i += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
wire_arena::WireKind::Regular | wire_arena::WireKind::MeshEdge => {
|
||||||
|
let mesh_edge = kind == wire_arena::WireKind::MeshEdge;
|
||||||
|
let mut j = i + 1;
|
||||||
|
while j < wires.len() && wire_arena::classify_wire(&wires[j]) == kind {
|
||||||
|
j += 1;
|
||||||
|
}
|
||||||
|
let refs: Vec<&WireModel> = wires[i..j].iter().collect();
|
||||||
|
batches.extend(WireGpu::from_run_refs(
|
||||||
|
device,
|
||||||
|
queue,
|
||||||
|
&refs,
|
||||||
|
depth_map,
|
||||||
|
mesh_edge,
|
||||||
|
self.wire_const_bgl.as_ref(),
|
||||||
|
));
|
||||||
|
i = j;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if wires[i].render_instance.is_some() {
|
|
||||||
block_wires.push(&wires[i]);
|
|
||||||
i += 1;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let mesh_edge = is_mesh_edge(&wires[i]);
|
|
||||||
let mut j = i + 1;
|
|
||||||
while j < wires.len()
|
|
||||||
&& wires[j].display_visible
|
|
||||||
&& wires[j].render_instance.is_none()
|
|
||||||
&& is_mesh_edge(&wires[j]) == mesh_edge
|
|
||||||
{
|
|
||||||
j += 1;
|
|
||||||
}
|
|
||||||
let refs: Vec<&WireModel> = wires[i..j].iter().collect();
|
|
||||||
batches.extend(WireGpu::from_run_refs(
|
|
||||||
device,
|
|
||||||
queue,
|
|
||||||
&refs,
|
|
||||||
depth_map,
|
|
||||||
mesh_edge,
|
|
||||||
self.wire_const_bgl.as_ref(),
|
|
||||||
));
|
|
||||||
i = j;
|
|
||||||
}
|
}
|
||||||
let block_batches = BlockWireGpu::from_wires(
|
let block_batches = BlockWireGpu::from_wires(
|
||||||
device,
|
device,
|
||||||
@@ -2462,10 +2582,14 @@ impl Pipeline {
|
|||||||
wires.len(),
|
wires.len(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
let circle_batches = self.upload_circles(device, queue, wires, depth_map);
|
||||||
|
let ellipse_batches = self.upload_ellipses(device, queue, wires, depth_map);
|
||||||
(
|
(
|
||||||
std::sync::Arc::new(batches),
|
std::sync::Arc::new(batches),
|
||||||
std::sync::Arc::new(block_batches),
|
std::sync::Arc::new(block_batches),
|
||||||
std::sync::Arc::new(index),
|
std::sync::Arc::new(index),
|
||||||
|
std::sync::Arc::new(circle_batches),
|
||||||
|
std::sync::Arc::new(ellipse_batches),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2492,6 +2616,8 @@ impl Pipeline {
|
|||||||
if selected.is_empty() && hovered.is_empty() && annotation_context_wires.is_empty() {
|
if selected.is_empty() && hovered.is_empty() && annotation_context_wires.is_empty() {
|
||||||
self.gpu_selected_wires = vec![];
|
self.gpu_selected_wires = vec![];
|
||||||
self.gpu_selected_block_wires = vec![];
|
self.gpu_selected_block_wires = vec![];
|
||||||
|
self.gpu_selected_circles = vec![];
|
||||||
|
self.gpu_selected_ellipses = vec![];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Gather borrowed highlighted wires via the prebuilt index —
|
// Gather borrowed highlighted wires via the prebuilt index —
|
||||||
@@ -2527,15 +2653,87 @@ impl Pipeline {
|
|||||||
hover_wires.push(wire);
|
hover_wires.push(wire);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
let mut selected_circles: Vec<CircleInstance> = Vec::new();
|
||||||
|
for &wire in &selected_wires {
|
||||||
|
let depth = wire_gpu::wire_draw_depth(wire, depth_map);
|
||||||
|
if let Some(insts) = circle_gpu::extract_circle_instances(wire, depth) {
|
||||||
|
for mut inst in insts {
|
||||||
|
if let Some(tint) = selected_tint {
|
||||||
|
inst.color = tint;
|
||||||
|
}
|
||||||
|
selected_circles.push(inst);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for &wire in &hover_wires {
|
||||||
|
let depth = wire_gpu::wire_draw_depth(wire, depth_map);
|
||||||
|
if let Some(insts) = circle_gpu::extract_circle_instances(wire, depth) {
|
||||||
|
for mut inst in insts {
|
||||||
|
inst.color = WireModel::HOVER;
|
||||||
|
selected_circles.push(inst);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.gpu_selected_circles = if selected_circles.is_empty() {
|
||||||
|
vec![]
|
||||||
|
} else {
|
||||||
|
vec![CircleGpu::from_instances(
|
||||||
|
device,
|
||||||
|
queue,
|
||||||
|
"viewer.selected_circles",
|
||||||
|
&selected_circles,
|
||||||
|
)]
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut selected_ellipses: Vec<EllipseInstance> = Vec::new();
|
||||||
|
for &wire in &selected_wires {
|
||||||
|
let depth = wire_gpu::wire_draw_depth(wire, depth_map);
|
||||||
|
if let Some(insts) = ellipse_gpu::extract_ellipse_instances(wire, depth) {
|
||||||
|
for mut inst in insts {
|
||||||
|
if let Some(tint) = selected_tint {
|
||||||
|
inst.color = tint;
|
||||||
|
}
|
||||||
|
selected_ellipses.push(inst);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for &wire in &hover_wires {
|
||||||
|
let depth = wire_gpu::wire_draw_depth(wire, depth_map);
|
||||||
|
if let Some(insts) = ellipse_gpu::extract_ellipse_instances(wire, depth) {
|
||||||
|
for mut inst in insts {
|
||||||
|
inst.color = WireModel::HOVER;
|
||||||
|
selected_ellipses.push(inst);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.gpu_selected_ellipses = if selected_ellipses.is_empty() {
|
||||||
|
vec![]
|
||||||
|
} else {
|
||||||
|
vec![EllipseGpu::from_instances(
|
||||||
|
device,
|
||||||
|
queue,
|
||||||
|
"viewer.selected_ellipses",
|
||||||
|
&selected_ellipses,
|
||||||
|
)]
|
||||||
|
};
|
||||||
|
|
||||||
let selected_regular: Vec<&WireModel> = selected_wires
|
let selected_regular: Vec<&WireModel> = selected_wires
|
||||||
.iter()
|
.iter()
|
||||||
.copied()
|
.copied()
|
||||||
.filter(|wire| wire.render_instance.is_none())
|
.filter(|wire| {
|
||||||
|
wire.render_instance.is_none()
|
||||||
|
&& circle_gpu::extract_circle_instances(wire, 0.0).is_none()
|
||||||
|
&& ellipse_gpu::extract_ellipse_instances(wire, 0.0).is_none()
|
||||||
|
})
|
||||||
.collect();
|
.collect();
|
||||||
let hover_regular: Vec<&WireModel> = hover_wires
|
let hover_regular: Vec<&WireModel> = hover_wires
|
||||||
.iter()
|
.iter()
|
||||||
.copied()
|
.copied()
|
||||||
.filter(|wire| wire.render_instance.is_none())
|
.filter(|wire| {
|
||||||
|
wire.render_instance.is_none()
|
||||||
|
&& circle_gpu::extract_circle_instances(wire, 0.0).is_none()
|
||||||
|
&& ellipse_gpu::extract_ellipse_instances(wire, 0.0).is_none()
|
||||||
|
})
|
||||||
.collect();
|
.collect();
|
||||||
let selected_blocks: Vec<&WireModel> = selected_wires
|
let selected_blocks: Vec<&WireModel> = selected_wires
|
||||||
.iter()
|
.iter()
|
||||||
@@ -4373,6 +4571,26 @@ impl Pipeline {
|
|||||||
}
|
}
|
||||||
bind_and_draw_block_wire(&mut pass, wire);
|
bind_and_draw_block_wire(&mut pass, wire);
|
||||||
}
|
}
|
||||||
|
// Analytical GPU circles (instanced screen-space quads)
|
||||||
|
if self.gpu_circles.iter().any(|cg| cg.instance_count > 0) {
|
||||||
|
pass.set_pipeline(&self.circle_pipeline);
|
||||||
|
for cg in self.gpu_circles.iter() {
|
||||||
|
if cg.instance_count > 0 {
|
||||||
|
pass.set_vertex_buffer(0, cg.instance_buffer.slice(..));
|
||||||
|
pass.draw(0..6, 0..cg.instance_count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Analytical GPU ellipses (instanced screen-space quads)
|
||||||
|
if self.gpu_ellipses.iter().any(|eg| eg.instance_count > 0) {
|
||||||
|
pass.set_pipeline(&self.ellipse_pipeline);
|
||||||
|
for eg in self.gpu_ellipses.iter() {
|
||||||
|
if eg.instance_count > 0 {
|
||||||
|
pass.set_vertex_buffer(0, eg.instance_buffer.slice(..));
|
||||||
|
pass.draw(0..6, 0..eg.instance_count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
// Live overlay wires (command preview / interim / grip drag) always
|
// Live overlay wires (command preview / interim / grip drag) always
|
||||||
// on top: the xray pipeline (depth_compare=Always, no depth write)
|
// on top: the xray pipeline (depth_compare=Always, no depth write)
|
||||||
// keeps them visible through any occluding geometry — a 3D solid, or
|
// keeps them visible through any occluding geometry — a 3D solid, or
|
||||||
@@ -4563,6 +4781,32 @@ impl Pipeline {
|
|||||||
bind_and_draw_block_wire(&mut pass, wire);
|
bind_and_draw_block_wire(&mut pass, wire);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if !self.gpu_selected_circles.is_empty() {
|
||||||
|
pass.set_pipeline(if hidden_line {
|
||||||
|
&self.circle_pipeline
|
||||||
|
} else {
|
||||||
|
&self.circle_xray_pipeline
|
||||||
|
});
|
||||||
|
for cg in &self.gpu_selected_circles {
|
||||||
|
if cg.instance_count > 0 {
|
||||||
|
pass.set_vertex_buffer(0, cg.instance_buffer.slice(..));
|
||||||
|
pass.draw(0..6, 0..cg.instance_count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !self.gpu_selected_ellipses.is_empty() {
|
||||||
|
pass.set_pipeline(if hidden_line {
|
||||||
|
&self.ellipse_pipeline
|
||||||
|
} else {
|
||||||
|
&self.ellipse_xray_pipeline
|
||||||
|
});
|
||||||
|
for eg in &self.gpu_selected_ellipses {
|
||||||
|
if eg.instance_count > 0 {
|
||||||
|
pass.set_vertex_buffer(0, eg.instance_buffer.slice(..));
|
||||||
|
pass.draw(0..6, 0..eg.instance_count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if let Some(atlas) = &self.text_atlas_gpu {
|
if let Some(atlas) = &self.text_atlas_gpu {
|
||||||
if !self.text_highlight_gpu.is_empty() {
|
if !self.text_highlight_gpu.is_empty() {
|
||||||
pass.set_pipeline(&self.text_highlight_pipeline);
|
pass.set_pipeline(&self.text_highlight_pipeline);
|
||||||
@@ -4711,6 +4955,8 @@ impl Pipeline {
|
|||||||
|
|
||||||
self.gpu_wires = std::sync::Arc::new(Vec::new());
|
self.gpu_wires = std::sync::Arc::new(Vec::new());
|
||||||
self.gpu_block_wires = std::sync::Arc::new(Vec::new());
|
self.gpu_block_wires = std::sync::Arc::new(Vec::new());
|
||||||
|
self.gpu_circles = std::sync::Arc::new(Vec::new());
|
||||||
|
self.gpu_ellipses = std::sync::Arc::new(Vec::new());
|
||||||
self.wire_handle_index = std::sync::Arc::new(rustc_hash::FxHashMap::default());
|
self.wire_handle_index = std::sync::Arc::new(rustc_hash::FxHashMap::default());
|
||||||
self.wire_arena = None;
|
self.wire_arena = None;
|
||||||
self.wire_arena_mesh = None;
|
self.wire_arena_mesh = None;
|
||||||
@@ -4721,6 +4967,8 @@ impl Pipeline {
|
|||||||
|
|
||||||
self.gpu_selected_wires.clear();
|
self.gpu_selected_wires.clear();
|
||||||
self.gpu_selected_block_wires.clear();
|
self.gpu_selected_block_wires.clear();
|
||||||
|
self.gpu_selected_circles.clear();
|
||||||
|
self.gpu_selected_ellipses.clear();
|
||||||
self.gpu_preview_wires.clear();
|
self.gpu_preview_wires.clear();
|
||||||
self.hatch_gpu.clear();
|
self.hatch_gpu.clear();
|
||||||
self.gpu_wipeouts.clear();
|
self.gpu_wipeouts.clear();
|
||||||
@@ -4990,6 +5238,7 @@ impl MultiPipeline {
|
|||||||
self.wire_buffer_cache.retain(|_, entry| {
|
self.wire_buffer_cache.retain(|_, entry| {
|
||||||
std::sync::Arc::strong_count(&entry.0) > 1
|
std::sync::Arc::strong_count(&entry.0) > 1
|
||||||
|| std::sync::Arc::strong_count(&entry.1) > 1
|
|| std::sync::Arc::strong_count(&entry.1) > 1
|
||||||
|
|| std::sync::Arc::strong_count(&entry.3) > 1
|
||||||
});
|
});
|
||||||
self.block_geometry.retain(|_, chunks| {
|
self.block_geometry.retain(|_, chunks| {
|
||||||
chunks.iter().any(|chunk| std::sync::Arc::strong_count(&chunk.bind_group) > 1)
|
chunks.iter().any(|chunk| std::sync::Arc::strong_count(&chunk.bind_group) > 1)
|
||||||
@@ -5140,6 +5389,8 @@ pub struct MultiPipeline {
|
|||||||
std::sync::Arc<Vec<WireGpu>>,
|
std::sync::Arc<Vec<WireGpu>>,
|
||||||
std::sync::Arc<Vec<BlockWireGpu>>,
|
std::sync::Arc<Vec<BlockWireGpu>>,
|
||||||
std::sync::Arc<rustc_hash::FxHashMap<u64, Vec<u32>>>,
|
std::sync::Arc<rustc_hash::FxHashMap<u64, Vec<u32>>>,
|
||||||
|
std::sync::Arc<Vec<CircleGpu>>,
|
||||||
|
std::sync::Arc<Vec<EllipseGpu>>,
|
||||||
),
|
),
|
||||||
>,
|
>,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -120,15 +120,131 @@ fn handle_of(w: &WireModel) -> Option<Handle> {
|
|||||||
crate::scene::Scene::handle_from_wire_name(&w.name)
|
crate::scene::Scene::handle_from_wire_name(&w.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Classification of a wire model for rendering pipelines.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub enum WireKind {
|
||||||
|
Invisible,
|
||||||
|
Block,
|
||||||
|
Circle,
|
||||||
|
Ellipse,
|
||||||
|
Regular,
|
||||||
|
MeshEdge,
|
||||||
|
Empty,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fast, branch-predicted classification of a wire model.
|
||||||
|
///
|
||||||
|
/// Bypasses analytical circle/ellipse extraction for the 95%+ of wires that have
|
||||||
|
/// no tangent geometry or multiple tangent primitives, reducing overhead to ~2 ns.
|
||||||
|
#[inline]
|
||||||
|
pub fn classify_wire(wire: &WireModel) -> WireKind {
|
||||||
|
if !wire.display_visible {
|
||||||
|
return WireKind::Invisible;
|
||||||
|
}
|
||||||
|
if wire.render_instance.is_some() {
|
||||||
|
return WireKind::Block;
|
||||||
|
}
|
||||||
|
if !wire.tangent_geoms.is_empty()
|
||||||
|
&& wire.fill_tris.is_empty()
|
||||||
|
&& wire.pick_tris.is_empty()
|
||||||
|
&& wire.text_verts.is_empty()
|
||||||
|
{
|
||||||
|
if super::circle_gpu::extract_circle_instances(wire, 0.0).is_some() {
|
||||||
|
return WireKind::Circle;
|
||||||
|
}
|
||||||
|
if super::ellipse_gpu::extract_ellipse_instances(wire, 0.0).is_some() {
|
||||||
|
return WireKind::Ellipse;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !wire.points.is_empty() {
|
||||||
|
if wire.fill_is_3d {
|
||||||
|
WireKind::MeshEdge
|
||||||
|
} else {
|
||||||
|
WireKind::Regular
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
WireKind::Empty
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Result of a single-pass partitioning of viewport wires.
|
||||||
|
pub struct PartitionedWires<'a> {
|
||||||
|
pub regular: Vec<&'a WireModel>,
|
||||||
|
pub mesh: Vec<&'a WireModel>,
|
||||||
|
pub instanced: Vec<&'a WireModel>,
|
||||||
|
pub circle_instances: Vec<super::circle_gpu::CircleInstance>,
|
||||||
|
pub ellipse_instances: Vec<super::ellipse_gpu::EllipseInstance>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Single-pass classification and extraction of all viewport wire categories.
|
||||||
|
///
|
||||||
|
/// Traverses the wire slice exactly once, eliminating redundant passes and duplicate
|
||||||
|
/// analytical extractions across split_wires, upload_block_wires, upload_circles,
|
||||||
|
/// and upload_ellipses.
|
||||||
|
pub fn partition_wires<'a>(
|
||||||
|
wires: &'a [WireModel],
|
||||||
|
depth_map: &rustc_hash::FxHashMap<u64, [f32; 2]>,
|
||||||
|
) -> PartitionedWires<'a> {
|
||||||
|
let mut regular = Vec::new();
|
||||||
|
let mut mesh = Vec::new();
|
||||||
|
let mut instanced = Vec::new();
|
||||||
|
let mut circle_instances = Vec::new();
|
||||||
|
let mut ellipse_instances = Vec::new();
|
||||||
|
|
||||||
|
for wire in wires {
|
||||||
|
if !wire.display_visible {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if wire.render_instance.is_some() {
|
||||||
|
instanced.push(wire);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let depth = super::wire_gpu::wire_draw_depth(wire, depth_map);
|
||||||
|
if !wire.tangent_geoms.is_empty()
|
||||||
|
&& wire.fill_tris.is_empty()
|
||||||
|
&& wire.pick_tris.is_empty()
|
||||||
|
&& wire.text_verts.is_empty()
|
||||||
|
{
|
||||||
|
if let Some(insts) = super::circle_gpu::extract_circle_instances(wire, depth) {
|
||||||
|
circle_instances.extend(insts);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if let Some(insts) = super::ellipse_gpu::extract_ellipse_instances(wire, depth) {
|
||||||
|
ellipse_instances.extend(insts);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !wire.points.is_empty() {
|
||||||
|
if wire.fill_is_3d {
|
||||||
|
mesh.push(wire);
|
||||||
|
} else {
|
||||||
|
regular.push(wire);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PartitionedWires {
|
||||||
|
regular,
|
||||||
|
mesh,
|
||||||
|
instanced,
|
||||||
|
circle_instances,
|
||||||
|
ellipse_instances,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Use the same regular/mesh partition for full uploads and changed runs.
|
/// Use the same regular/mesh partition for full uploads and changed runs.
|
||||||
/// Instanced blocks are uploaded separately by `BlockWireGpu`.
|
/// Instanced blocks are uploaded separately by `BlockWireGpu`.
|
||||||
pub fn split_wires(wires: &[WireModel]) -> (Vec<&WireModel>, Vec<&WireModel>) {
|
pub fn split_wires(wires: &[WireModel]) -> (Vec<&WireModel>, Vec<&WireModel>) {
|
||||||
wires
|
let mut regular = Vec::new();
|
||||||
.iter()
|
let mut mesh = Vec::new();
|
||||||
.filter(|wire| {
|
for wire in wires {
|
||||||
wire.display_visible && !wire.points.is_empty() && wire.render_instance.is_none()
|
match classify_wire(wire) {
|
||||||
})
|
WireKind::Regular => regular.push(wire),
|
||||||
.partition(|wire| !wire.fill_is_3d)
|
WireKind::MeshEdge => mesh.push(wire),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(regular, mesh)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// True when appending a new entity at the tail could change the image, so the
|
/// True when appending a new entity at the tail could change the image, so the
|
||||||
|
|||||||
@@ -839,20 +839,31 @@ impl shader::Primitive for Primitive {
|
|||||||
gpus.extend(arena.wire_gpus());
|
gpus.extend(arena.wire_gpus());
|
||||||
}
|
}
|
||||||
inner.gpu_wires = std::sync::Arc::new(gpus);
|
inner.gpu_wires = std::sync::Arc::new(gpus);
|
||||||
// Blocks keep their own cached geometry and placements.
|
let partitioned =
|
||||||
let instanced: Vec<&crate::scene::WireModel> = vp_wires
|
wire_arena::partition_wires(&vp_wires, &draw_depths);
|
||||||
.iter()
|
|
||||||
.filter(|w| w.render_instance.is_some())
|
|
||||||
.collect();
|
|
||||||
inner.gpu_block_wires = std::sync::Arc::new(
|
inner.gpu_block_wires = std::sync::Arc::new(
|
||||||
inner.upload_block_wires(
|
inner.upload_block_wires(
|
||||||
device,
|
device,
|
||||||
queue,
|
queue,
|
||||||
&instanced,
|
&partitioned.instanced,
|
||||||
&draw_depths,
|
&draw_depths,
|
||||||
&mut pipeline.block_geometry,
|
&mut pipeline.block_geometry,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
inner.gpu_circles = std::sync::Arc::new(
|
||||||
|
inner.upload_circles_from_instances(
|
||||||
|
device,
|
||||||
|
queue,
|
||||||
|
&partitioned.circle_instances,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
inner.gpu_ellipses = std::sync::Arc::new(
|
||||||
|
inner.upload_ellipses_from_instances(
|
||||||
|
device,
|
||||||
|
queue,
|
||||||
|
&partitioned.ellipse_instances,
|
||||||
|
),
|
||||||
|
);
|
||||||
if _patched {
|
if _patched {
|
||||||
wire_arena::patch_handle_index(
|
wire_arena::patch_handle_index(
|
||||||
&mut inner.wire_handle_index,
|
&mut inner.wire_handle_index,
|
||||||
@@ -911,10 +922,14 @@ impl shader::Primitive for Primitive {
|
|||||||
// Other panes retain their own references to shared geometry.
|
// Other panes retain their own references to shared geometry.
|
||||||
inner.gpu_wires = std::sync::Arc::new(Vec::new());
|
inner.gpu_wires = std::sync::Arc::new(Vec::new());
|
||||||
inner.gpu_block_wires = std::sync::Arc::new(Vec::new());
|
inner.gpu_block_wires = std::sync::Arc::new(Vec::new());
|
||||||
|
inner.gpu_circles = std::sync::Arc::new(Vec::new());
|
||||||
|
inner.gpu_ellipses = std::sync::Arc::new(Vec::new());
|
||||||
let held_before = pipeline.wire_buffer_cache.len();
|
let held_before = pipeline.wire_buffer_cache.len();
|
||||||
pipeline.wire_buffer_cache.retain(|_, (w, b, _)| {
|
pipeline.wire_buffer_cache.retain(|_, (w, b, _, c, e)| {
|
||||||
std::sync::Arc::strong_count(w) > 1
|
std::sync::Arc::strong_count(w) > 1
|
||||||
|| std::sync::Arc::strong_count(b) > 1
|
|| std::sync::Arc::strong_count(b) > 1
|
||||||
|
|| std::sync::Arc::strong_count(c) > 1
|
||||||
|
|| std::sync::Arc::strong_count(e) > 1
|
||||||
});
|
});
|
||||||
if _perf {
|
if _perf {
|
||||||
crate::perf_record!(
|
crate::perf_record!(
|
||||||
@@ -959,6 +974,8 @@ impl shader::Primitive for Primitive {
|
|||||||
inner.gpu_wires = built.0;
|
inner.gpu_wires = built.0;
|
||||||
inner.gpu_block_wires = built.1;
|
inner.gpu_block_wires = built.1;
|
||||||
inner.wire_handle_index = built.2;
|
inner.wire_handle_index = built.2;
|
||||||
|
inner.gpu_circles = built.3;
|
||||||
|
inner.gpu_ellipses = built.4;
|
||||||
} // end !arena_served
|
} // end !arena_served
|
||||||
inner.cached_wire_id = vp.wire_content_id;
|
inner.cached_wire_id = vp.wire_content_id;
|
||||||
if _perf {
|
if _perf {
|
||||||
|
|||||||
@@ -0,0 +1,224 @@
|
|||||||
|
// GPU-instanced analytical circle shader — renders 2D/planar circles as screen/plane-aligned quads.
|
||||||
|
// Topology: TriangleList, 6 vertices drawn per INSTANCE.
|
||||||
|
//
|
||||||
|
// One instance = one complete circle. The six vertex IDs map to the corners of a
|
||||||
|
// two-triangle quad covering the circle's bounding box (+ lineweight margin).
|
||||||
|
//
|
||||||
|
// The fragment shader analytically computes the exact distance to the circle perimeter:
|
||||||
|
// d_world = abs(length(local_xy) - radius)
|
||||||
|
// and uses screen-space derivatives (fwidth) to achieve perfect sub-pixel anti-aliasing
|
||||||
|
// at any zoom level, without any polygon faceting or CPU re-tessellation.
|
||||||
|
|
||||||
|
struct Uniforms {
|
||||||
|
viewport_size: vec2<f32>,
|
||||||
|
world_per_pixel: f32,
|
||||||
|
lwdisplay_enable: f32,
|
||||||
|
flat_shade: f32,
|
||||||
|
transparency_enable: f32,
|
||||||
|
linetype_scale: f32,
|
||||||
|
lineweight_scale: f32,
|
||||||
|
// Relative-to-eye (double-single)
|
||||||
|
view_rot: mat4x4<f32>,
|
||||||
|
eye_high: vec3<f32>,
|
||||||
|
_pad_eh: f32,
|
||||||
|
eye_low: vec3<f32>,
|
||||||
|
_pad_el: f32,
|
||||||
|
}
|
||||||
|
@group(0) @binding(0) var<uniform> u: Uniforms;
|
||||||
|
|
||||||
|
const DRAW_ORDER_BIAS: f32 = 0.001;
|
||||||
|
const MODEL_LINEWEIGHT_BOOST: f32 = 2.0;
|
||||||
|
const MODEL_LINEWEIGHT_MAX_PX: f32 = 10.0;
|
||||||
|
const TAU: f32 = 6.283185307179586;
|
||||||
|
|
||||||
|
struct InstanceIn {
|
||||||
|
@location(0) center_high: vec4<f32>, // xyz = center_high, w = unused
|
||||||
|
@location(1) center_low: vec4<f32>, // xyz = center_low, w = radius
|
||||||
|
@location(2) axis_x: vec4<f32>, // xyz = axis_x, w = start_angle
|
||||||
|
@location(3) axis_y: vec4<f32>, // xyz = axis_y, w = end_angle
|
||||||
|
@location(4) color: vec4<f32>, // rgba
|
||||||
|
@location(5) params: vec4<f32>, // half_width_px, pattern_length, draw_depth, unused
|
||||||
|
@location(6) pat0: vec4<f32>,
|
||||||
|
@location(7) pat1: vec4<f32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct VertexOut {
|
||||||
|
@builtin(position) clip_pos: vec4<f32>,
|
||||||
|
@location(0) local_pos: vec2<f32>,
|
||||||
|
@location(1) radius: f32,
|
||||||
|
@location(2) hw_px: f32,
|
||||||
|
@location(3) color: vec4<f32>,
|
||||||
|
@location(4) pattern_length: f32,
|
||||||
|
@location(5) pat0: vec4<f32>,
|
||||||
|
@location(6) pat1: vec4<f32>,
|
||||||
|
@location(7) @interpolate(flat) min_elem: f32,
|
||||||
|
@location(8) @interpolate(flat) start_angle: f32,
|
||||||
|
@location(9) @interpolate(flat) end_angle: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_hw(px_hw: f32) -> f32 {
|
||||||
|
var display_hw = max(px_hw * u.lineweight_scale, 0.5);
|
||||||
|
if u.lineweight_scale < 0.0 {
|
||||||
|
let scale = -u.lineweight_scale;
|
||||||
|
let base_hw = select(
|
||||||
|
min(px_hw * MODEL_LINEWEIGHT_BOOST, MODEL_LINEWEIGHT_MAX_PX * 0.5),
|
||||||
|
0.5,
|
||||||
|
px_hw <= 0.5,
|
||||||
|
);
|
||||||
|
display_hw = max(base_hw * scale, 0.5);
|
||||||
|
}
|
||||||
|
return select(0.5, display_hw, u.lwdisplay_enable > 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
@vertex fn vs_main(@builtin(vertex_index) vid: u32, in: InstanceIn) -> VertexOut {
|
||||||
|
// Two-triangle unit quad corner table:
|
||||||
|
// vid 0,1,2 = (-1,-1) ( 1,-1) ( 1, 1)
|
||||||
|
// vid 3,4,5 = (-1,-1) ( 1, 1) (-1, 1)
|
||||||
|
let u_arr = array<f32, 6>(-1.0, 1.0, 1.0, -1.0, 1.0, -1.0);
|
||||||
|
let v_arr = array<f32, 6>(-1.0, -1.0, 1.0, -1.0, 1.0, 1.0);
|
||||||
|
let u_val = u_arr[vid];
|
||||||
|
let v_val = v_arr[vid];
|
||||||
|
|
||||||
|
let center_high = in.center_high.xyz;
|
||||||
|
let center_low = in.center_low.xyz;
|
||||||
|
let radius = in.center_low.w;
|
||||||
|
let axis_x = in.axis_x.xyz;
|
||||||
|
let axis_y = in.axis_y.xyz;
|
||||||
|
let color = in.color;
|
||||||
|
let hw_px = resolve_hw(in.params.x);
|
||||||
|
let pattern_length = in.params.y;
|
||||||
|
let draw_depth = in.params.z;
|
||||||
|
|
||||||
|
// Expand bounding quad so anti-aliasing margin and lineweight never clip
|
||||||
|
let margin_world = (hw_px + 2.0) * max(u.world_per_pixel, 1e-6);
|
||||||
|
let ext = radius + margin_world;
|
||||||
|
|
||||||
|
let center_rel = (center_high - u.eye_high) + (center_low - u.eye_low);
|
||||||
|
let world_pos_rel = center_rel + (u_val * axis_x + v_val * axis_y) * ext;
|
||||||
|
|
||||||
|
var clip_pos = u.view_rot * vec4<f32>(world_pos_rel, 1.0);
|
||||||
|
clip_pos.z = clip_pos.z - draw_depth * DRAW_ORDER_BIAS * clip_pos.w;
|
||||||
|
|
||||||
|
var out: VertexOut;
|
||||||
|
out.clip_pos = clip_pos;
|
||||||
|
out.local_pos = vec2<f32>(u_val * ext, v_val * ext);
|
||||||
|
out.radius = radius;
|
||||||
|
out.hw_px = hw_px;
|
||||||
|
out.color = color;
|
||||||
|
out.pattern_length = pattern_length;
|
||||||
|
out.pat0 = in.pat0;
|
||||||
|
out.pat1 = in.pat1;
|
||||||
|
|
||||||
|
// Compute smallest non-zero pattern element for LOD short-circuiting
|
||||||
|
let elems = array<f32, 8>(in.pat0.x, in.pat0.y, in.pat0.z, in.pat0.w, in.pat1.x, in.pat1.y, in.pat1.z, in.pat1.w);
|
||||||
|
var min_elem = 1e9;
|
||||||
|
for (var i = 0u; i < 8u; i++) {
|
||||||
|
let e = abs(elems[i]);
|
||||||
|
if e > 0.0 && e < min_elem {
|
||||||
|
min_elem = e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out.min_elem = select(0.0, min_elem, min_elem < 1e8);
|
||||||
|
|
||||||
|
out.start_angle = in.axis_x.w;
|
||||||
|
out.end_angle = in.axis_y.w;
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mod_tau(val: f32) -> f32 {
|
||||||
|
let m = val % TAU;
|
||||||
|
return select(m, m + TAU, m < 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn in_dash(dist: f32, pat_len: f32, p0: vec4<f32>, p1: vec4<f32>) -> bool {
|
||||||
|
let elems = array<f32, 8>(p0.x, p0.y, p0.z, p0.w, p1.x, p1.y, p1.z, p1.w);
|
||||||
|
var count = 0u;
|
||||||
|
for (var i = 0u; i < 8u; i++) {
|
||||||
|
if elems[i] != 0.0 { count = i + 1u; }
|
||||||
|
}
|
||||||
|
if count == 0u {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let d = ((dist % pat_len) + pat_len) % pat_len;
|
||||||
|
var pos = 0.0f;
|
||||||
|
let dot_half = u.world_per_pixel * 0.75;
|
||||||
|
for (var i = 0u; i < count; i++) {
|
||||||
|
let elem = elems[i];
|
||||||
|
if elem == 0.0 {
|
||||||
|
let dd = abs(d - pos);
|
||||||
|
if min(dd, pat_len - dd) <= dot_half { return true; }
|
||||||
|
} else if elem > 0.0 {
|
||||||
|
if d >= pos && d < pos + elem { return true; }
|
||||||
|
pos += elem;
|
||||||
|
} else {
|
||||||
|
pos += -elem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@fragment fn fs_main(in: VertexOut) -> @location(0) vec4<f32> {
|
||||||
|
let r = length(in.local_pos);
|
||||||
|
|
||||||
|
// Screen-space derivative anti-aliasing:
|
||||||
|
// Use the gradient of the monotonic radial distance `r` instead of `abs(r - radius)`.
|
||||||
|
// Taking derivatives across the cusp of abs() cancels out at scanline crossings,
|
||||||
|
// which previously collapsed fw to near-zero and punched periodic 1-2px holes (false dashes).
|
||||||
|
let grad = vec2<f32>(dpdx(r), dpdy(r));
|
||||||
|
let grad_len = length(grad);
|
||||||
|
let fw = max(select(u.world_per_pixel, grad_len, grad_len > 1e-6), 1e-6);
|
||||||
|
|
||||||
|
let sa = in.start_angle;
|
||||||
|
let ea = in.end_angle;
|
||||||
|
var sweep = mod_tau(ea - sa);
|
||||||
|
if sweep <= 1e-5 && abs(ea - sa) > 1e-5 {
|
||||||
|
sweep = TAU;
|
||||||
|
}
|
||||||
|
|
||||||
|
let angle = atan2(in.local_pos.y, in.local_pos.x);
|
||||||
|
let theta = select(angle, angle + TAU, angle < 0.0);
|
||||||
|
let d_theta = mod_tau(theta - sa);
|
||||||
|
|
||||||
|
var d_world: f32;
|
||||||
|
var arc_dist: f32;
|
||||||
|
|
||||||
|
if sweep >= TAU - 1e-5 || d_theta <= sweep {
|
||||||
|
// Inside arc sweep or full circle
|
||||||
|
d_world = abs(r - in.radius);
|
||||||
|
arc_dist = in.radius * d_theta;
|
||||||
|
} else {
|
||||||
|
// Outside sweep: measure distance to start and end endpoints for round end caps
|
||||||
|
let p_start = in.radius * vec2<f32>(cos(sa), sin(sa));
|
||||||
|
let p_end = in.radius * vec2<f32>(cos(ea), sin(ea));
|
||||||
|
let d_start = length(in.local_pos - p_start);
|
||||||
|
let d_end = length(in.local_pos - p_end);
|
||||||
|
if d_start < d_end {
|
||||||
|
d_world = d_start;
|
||||||
|
arc_dist = 0.0;
|
||||||
|
} else {
|
||||||
|
d_world = d_end;
|
||||||
|
arc_dist = in.radius * sweep;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let d_px = d_world / fw;
|
||||||
|
let alpha_cov = clamp(0.5 + in.hw_px - d_px, 0.0, 1.0);
|
||||||
|
if alpha_cov <= 0.0 {
|
||||||
|
discard;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Linetype dash pattern test
|
||||||
|
if in.pattern_length > 0.0 {
|
||||||
|
let pat_len = in.pattern_length * u.linetype_scale;
|
||||||
|
if in.min_elem * u.linetype_scale >= u.world_per_pixel && pat_len > 0.0 {
|
||||||
|
if !in_dash(arc_dist, pat_len, in.pat0, in.pat1) {
|
||||||
|
discard;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let alpha = select(1.0, in.color.a, u.transparency_enable > 0.5) * alpha_cov;
|
||||||
|
return vec4<f32>(in.color.rgb, alpha);
|
||||||
|
}
|
||||||
@@ -0,0 +1,275 @@
|
|||||||
|
// GPU-instanced analytical ellipse shader — renders 2D/planar ellipses and elliptical arcs as quads.
|
||||||
|
// Topology: TriangleList, 6 vertices drawn per INSTANCE.
|
||||||
|
//
|
||||||
|
// Evaluates Inigo Quilez's exact 2D Ellipse Signed Distance Function (quartic closest-point solver)
|
||||||
|
// with sub-pixel anti-aliasing and parametric arc endpoint clamping for round end caps.
|
||||||
|
|
||||||
|
struct Uniforms {
|
||||||
|
viewport_size: vec2<f32>,
|
||||||
|
world_per_pixel: f32,
|
||||||
|
lwdisplay_enable: f32,
|
||||||
|
flat_shade: f32,
|
||||||
|
transparency_enable: f32,
|
||||||
|
linetype_scale: f32,
|
||||||
|
lineweight_scale: f32,
|
||||||
|
// Relative-to-eye (double-single)
|
||||||
|
view_rot: mat4x4<f32>,
|
||||||
|
eye_high: vec3<f32>,
|
||||||
|
_pad_eh: f32,
|
||||||
|
eye_low: vec3<f32>,
|
||||||
|
_pad_el: f32,
|
||||||
|
}
|
||||||
|
@group(0) @binding(0) var<uniform> u: Uniforms;
|
||||||
|
|
||||||
|
const DRAW_ORDER_BIAS: f32 = 0.001;
|
||||||
|
const MODEL_LINEWEIGHT_BOOST: f32 = 2.0;
|
||||||
|
const MODEL_LINEWEIGHT_MAX_PX: f32 = 10.0;
|
||||||
|
const TAU: f32 = 6.283185307179586;
|
||||||
|
|
||||||
|
struct InstanceIn {
|
||||||
|
@location(0) center_high: vec4<f32>, // xyz = center_high, w = unused
|
||||||
|
@location(1) center_low: vec4<f32>, // xyz = center_low, w = minor_axis_ratio
|
||||||
|
@location(2) major_axis: vec4<f32>, // xyz = major_axis (semi-major vector), w = start_param
|
||||||
|
@location(3) normal: vec4<f32>, // xyz = normal, w = end_param
|
||||||
|
@location(4) color: vec4<f32>, // rgba
|
||||||
|
@location(5) params: vec4<f32>, // half_width_px, pattern_length, draw_depth, unused
|
||||||
|
@location(6) pat0: vec4<f32>,
|
||||||
|
@location(7) pat1: vec4<f32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct VertexOut {
|
||||||
|
@builtin(position) clip_pos: vec4<f32>,
|
||||||
|
@location(0) local_pos: vec2<f32>,
|
||||||
|
@location(1) semi_axes: vec2<f32>,
|
||||||
|
@location(2) hw_px: f32,
|
||||||
|
@location(3) color: vec4<f32>,
|
||||||
|
@location(4) pattern_length: f32,
|
||||||
|
@location(5) pat0: vec4<f32>,
|
||||||
|
@location(6) pat1: vec4<f32>,
|
||||||
|
@location(7) @interpolate(flat) min_elem: f32,
|
||||||
|
@location(8) @interpolate(flat) start_param: f32,
|
||||||
|
@location(9) @interpolate(flat) end_param: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_hw(px_hw: f32) -> f32 {
|
||||||
|
var display_hw = max(px_hw * u.lineweight_scale, 0.5);
|
||||||
|
if u.lineweight_scale < 0.0 {
|
||||||
|
let scale = -u.lineweight_scale;
|
||||||
|
let base_hw = select(
|
||||||
|
min(px_hw * MODEL_LINEWEIGHT_BOOST, MODEL_LINEWEIGHT_MAX_PX * 0.5),
|
||||||
|
0.5,
|
||||||
|
px_hw <= 0.5,
|
||||||
|
);
|
||||||
|
display_hw = max(base_hw * scale, 0.5);
|
||||||
|
}
|
||||||
|
return select(0.5, display_hw, u.lwdisplay_enable > 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
@vertex fn vs_main(@builtin(vertex_index) vid: u32, in: InstanceIn) -> VertexOut {
|
||||||
|
let u_arr = array<f32, 6>(-1.0, 1.0, 1.0, -1.0, 1.0, -1.0);
|
||||||
|
let v_arr = array<f32, 6>(-1.0, -1.0, 1.0, -1.0, 1.0, 1.0);
|
||||||
|
let u_val = u_arr[vid];
|
||||||
|
let v_val = v_arr[vid];
|
||||||
|
|
||||||
|
let center_high = in.center_high.xyz;
|
||||||
|
let center_low = in.center_low.xyz;
|
||||||
|
let major_vec = in.major_axis.xyz;
|
||||||
|
let norm_vec = in.normal.xyz;
|
||||||
|
|
||||||
|
let a = length(major_vec);
|
||||||
|
let u_axis = select(vec3<f32>(1.0, 0.0, 0.0), major_vec / a, a > 1e-6);
|
||||||
|
let n_len = length(norm_vec);
|
||||||
|
let n_axis = select(vec3<f32>(0.0, 0.0, 1.0), norm_vec / n_len, n_len > 1e-6);
|
||||||
|
let v_axis = normalize(cross(n_axis, u_axis));
|
||||||
|
|
||||||
|
let ratio = clamp(in.center_low.w, 1e-6, 1.0);
|
||||||
|
let b = a * ratio;
|
||||||
|
|
||||||
|
let color = in.color;
|
||||||
|
let hw_px = resolve_hw(in.params.x);
|
||||||
|
let pattern_length = in.params.y;
|
||||||
|
let draw_depth = in.params.z;
|
||||||
|
|
||||||
|
let margin_world = (hw_px + 2.0) * max(u.world_per_pixel, 1e-6);
|
||||||
|
let ext_u = a + margin_world;
|
||||||
|
let ext_v = b + margin_world;
|
||||||
|
|
||||||
|
let center_rel = (center_high - u.eye_high) + (center_low - u.eye_low);
|
||||||
|
let world_pos_rel = center_rel + (u_val * ext_u) * u_axis + (v_val * ext_v) * v_axis;
|
||||||
|
|
||||||
|
var clip_pos = u.view_rot * vec4<f32>(world_pos_rel, 1.0);
|
||||||
|
clip_pos.z = clip_pos.z - draw_depth * DRAW_ORDER_BIAS * clip_pos.w;
|
||||||
|
|
||||||
|
var out: VertexOut;
|
||||||
|
out.clip_pos = clip_pos;
|
||||||
|
out.local_pos = vec2<f32>(u_val * ext_u, v_val * ext_v);
|
||||||
|
out.semi_axes = vec2<f32>(a, b);
|
||||||
|
out.hw_px = hw_px;
|
||||||
|
out.color = color;
|
||||||
|
out.pattern_length = pattern_length;
|
||||||
|
out.pat0 = in.pat0;
|
||||||
|
out.pat1 = in.pat1;
|
||||||
|
out.start_param = in.major_axis.w;
|
||||||
|
out.end_param = in.normal.w;
|
||||||
|
|
||||||
|
let elems = array<f32, 8>(in.pat0.x, in.pat0.y, in.pat0.z, in.pat0.w, in.pat1.x, in.pat1.y, in.pat1.z, in.pat1.w);
|
||||||
|
var min_elem = 1e9;
|
||||||
|
for (var i = 0u; i < 8u; i++) {
|
||||||
|
let e = abs(elems[i]);
|
||||||
|
if e > 0.0 && e < min_elem {
|
||||||
|
min_elem = e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out.min_elem = select(0.0, min_elem, min_elem < 1e8);
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mod_tau(val: f32) -> f32 {
|
||||||
|
let m = val % TAU;
|
||||||
|
return select(m, m + TAU, m < 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ClosestResult {
|
||||||
|
q: vec2<f32>,
|
||||||
|
t: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn closest_point_ellipse(p_in: vec2<f32>, ab: vec2<f32>) -> ClosestResult {
|
||||||
|
let px = abs(p_in.x);
|
||||||
|
let py = abs(p_in.y);
|
||||||
|
let a = ab.x;
|
||||||
|
let b = ab.y;
|
||||||
|
if abs(a - b) < 1e-5 {
|
||||||
|
let len = length(p_in);
|
||||||
|
let q_circ = select(p_in, normalize(p_in) * a, len > 1e-6);
|
||||||
|
let ang = atan2(q_circ.y, q_circ.x);
|
||||||
|
let full_t = select(ang, ang + TAU, ang < 0.0);
|
||||||
|
return ClosestResult(q_circ, full_t);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initial estimate from eccentric anomaly of radial projection
|
||||||
|
var t = atan2(py * a, px * b);
|
||||||
|
let k = a * a - b * b;
|
||||||
|
|
||||||
|
// 3 iterations of Newton-Raphson for machine precision
|
||||||
|
for (var i = 0u; i < 3u; i++) {
|
||||||
|
let st = sin(t);
|
||||||
|
let ct = cos(t);
|
||||||
|
let f = k * st * ct - px * a * st + py * b * ct;
|
||||||
|
let f_prime = k * (ct * ct - st * st) - px * a * ct - py * b * st;
|
||||||
|
if abs(f_prime) > 1e-7 {
|
||||||
|
t -= f / f_prime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t = clamp(t, 0.0, 1.5707963267948966);
|
||||||
|
|
||||||
|
let q_quad = vec2<f32>(a * cos(t), b * sin(t));
|
||||||
|
let q = vec2<f32>(
|
||||||
|
select(-q_quad.x, q_quad.x, p_in.x >= 0.0),
|
||||||
|
select(-q_quad.y, q_quad.y, p_in.y >= 0.0)
|
||||||
|
);
|
||||||
|
|
||||||
|
var full_t = t;
|
||||||
|
if (p_in.x < 0.0) {
|
||||||
|
full_t = 3.141592653589793 - t;
|
||||||
|
}
|
||||||
|
if (p_in.y < 0.0) {
|
||||||
|
full_t = select(TAU - t, 3.141592653589793 + t, p_in.x < 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ClosestResult(q, full_t);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn in_dash(dist: f32, pat_len: f32, p0: vec4<f32>, p1: vec4<f32>) -> bool {
|
||||||
|
let elems = array<f32, 8>(p0.x, p0.y, p0.z, p0.w, p1.x, p1.y, p1.z, p1.w);
|
||||||
|
var count = 0u;
|
||||||
|
for (var i = 0u; i < 8u; i++) {
|
||||||
|
if elems[i] != 0.0 { count = i + 1u; }
|
||||||
|
}
|
||||||
|
if count == 0u {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let d = ((dist % pat_len) + pat_len) % pat_len;
|
||||||
|
var pos = 0.0f;
|
||||||
|
let dot_half = u.world_per_pixel * 0.75;
|
||||||
|
for (var i = 0u; i < count; i++) {
|
||||||
|
let elem = elems[i];
|
||||||
|
if elem == 0.0 {
|
||||||
|
let dd = abs(d - pos);
|
||||||
|
if min(dd, pat_len - dd) <= dot_half { return true; }
|
||||||
|
} else if elem > 0.0 {
|
||||||
|
if d >= pos && d < pos + elem { return true; }
|
||||||
|
pos += elem;
|
||||||
|
} else {
|
||||||
|
pos += -elem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@fragment fn fs_main(in: VertexOut) -> @location(0) vec4<f32> {
|
||||||
|
let ab = in.semi_axes;
|
||||||
|
let res = closest_point_ellipse(in.local_pos, ab);
|
||||||
|
let q = res.q;
|
||||||
|
let t = res.t;
|
||||||
|
|
||||||
|
let sa = in.start_param;
|
||||||
|
let ea = in.end_param;
|
||||||
|
var sweep = mod_tau(ea - sa);
|
||||||
|
if sweep <= 1e-5 && abs(ea - sa) > 1e-5 {
|
||||||
|
sweep = TAU;
|
||||||
|
}
|
||||||
|
|
||||||
|
let d_t = mod_tau(t - sa);
|
||||||
|
|
||||||
|
var d_world: f32;
|
||||||
|
var arc_dist: f32;
|
||||||
|
|
||||||
|
// Ramanujan approximation of ellipse perimeter for dash scaling
|
||||||
|
let h_val = ((ab.x - ab.y) * (ab.x - ab.y)) / max((ab.x + ab.y) * (ab.x + ab.y), 1e-6);
|
||||||
|
let perimeter = 3.141592653589793 * (ab.x + ab.y) * (1.0 + (3.0 * h_val) / max(10.0 + sqrt(max(4.0 - 3.0 * h_val, 0.0)), 1e-6));
|
||||||
|
|
||||||
|
if sweep >= TAU - 1e-5 || d_t <= sweep {
|
||||||
|
d_world = length(in.local_pos - q);
|
||||||
|
arc_dist = perimeter * (d_t / TAU);
|
||||||
|
} else {
|
||||||
|
let p_start = vec2<f32>(ab.x * cos(sa), ab.y * sin(sa));
|
||||||
|
let p_end = vec2<f32>(ab.x * cos(ea), ab.y * sin(ea));
|
||||||
|
let d_start = length(in.local_pos - p_start);
|
||||||
|
let d_end = length(in.local_pos - p_end);
|
||||||
|
if d_start < d_end {
|
||||||
|
d_world = d_start;
|
||||||
|
arc_dist = 0.0;
|
||||||
|
} else {
|
||||||
|
d_world = d_end;
|
||||||
|
arc_dist = perimeter * (sweep / TAU);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Isotropic quad screen-space filter width:
|
||||||
|
// Deriving from interpolated linear coordinates eliminates scanline derivative cancellations
|
||||||
|
let dx = dpdx(in.local_pos);
|
||||||
|
let dy = dpdy(in.local_pos);
|
||||||
|
let fw_len = sqrt(0.5 * (dot(dx, dx) + dot(dy, dy)));
|
||||||
|
let fw = max(select(u.world_per_pixel, fw_len, fw_len > 1e-6), 1e-6);
|
||||||
|
|
||||||
|
let d_px = d_world / fw;
|
||||||
|
let alpha_cov = clamp(0.5 + in.hw_px - d_px, 0.0, 1.0);
|
||||||
|
if alpha_cov <= 0.0 {
|
||||||
|
discard;
|
||||||
|
}
|
||||||
|
|
||||||
|
if in.pattern_length > 0.0 {
|
||||||
|
let pat_len = in.pattern_length * u.linetype_scale;
|
||||||
|
if in.min_elem * u.linetype_scale >= u.world_per_pixel && pat_len > 0.0 {
|
||||||
|
if !in_dash(arc_dist, pat_len, in.pat0, in.pat1) {
|
||||||
|
discard;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let alpha = select(1.0, in.color.a, u.transparency_enable > 0.5) * alpha_cov;
|
||||||
|
return vec4<f32>(in.color.rgb, alpha);
|
||||||
|
}
|
||||||
@@ -1778,6 +1778,7 @@ impl Snapper {
|
|||||||
.fold(f32::INFINITY, f32::min);
|
.fold(f32::INFINITY, f32::min);
|
||||||
(world, edge_d2)
|
(world, edge_d2)
|
||||||
}
|
}
|
||||||
|
TangentGeom::PlanarEllipse { .. } => continue,
|
||||||
};
|
};
|
||||||
let (tier, sub) = (
|
let (tier, sub) = (
|
||||||
snap_tier(SnapType::Tangent),
|
snap_tier(SnapType::Tangent),
|
||||||
@@ -1814,6 +1815,7 @@ impl Snapper {
|
|||||||
radius: *radius,
|
radius: *radius,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
TangentGeom::PlanarEllipse { .. } => unreachable!(),
|
||||||
};
|
};
|
||||||
best = Some(SnapResult {
|
best = Some(SnapResult {
|
||||||
world: world_pt,
|
world: world_pt,
|
||||||
|
|||||||
Reference in new issue
Block a user