From c5fe70627a0783669aef5c759ba62a18a8b66d26 Mon Sep 17 00:00:00 2001 From: ramox81 <184937705+ramox81@users.noreply.github.com> Date: Sat, 22 Aug 2026 17:18:30 +0300 Subject: [PATCH] Honor point display geometry and sizing --- src/app/commands/styleprops.rs | 2 +- src/app/update/viewport.rs | 3 +- src/entities/point.rs | 139 ++++++++++++++++++++++---------- src/locale_catalog.rs | 1 - src/scene/convert/tessellate.rs | 15 +++- src/scene/pipeline/wire_gpu.rs | 60 +++++++++++++- src/shaders/block_wire.wgsl | 32 +++++++- src/shaders/wire.wgsl | 22 ++++- src/shaders/wire_indexed.wgsl | 22 ++++- src/ui/style/point_style.rs | 2 +- 10 files changed, 238 insertions(+), 60 deletions(-) diff --git a/src/app/commands/styleprops.rs b/src/app/commands/styleprops.rs index de326abb0..774f5cc8e 100644 --- a/src/app/commands/styleprops.rs +++ b/src/app/commands/styleprops.rs @@ -2309,7 +2309,7 @@ impl OpenCADStudio { use crate::command::ValuePromptCommand; let c = ValuePromptCommand::new( "PDSIZE", - "PDSIZE new point size (0 = 5% of viewport, <0 = absolute):", + "PDSIZE new point size (0 = 5% of viewport, >0 = absolute, <0 = viewport percentage):", ); self.command_line.push_info(&c.prompt()); self.tabs[i].active_cmd = Some(Box::new(c)); diff --git a/src/app/update/viewport.rs b/src/app/update/viewport.rs index ef0015f45..739b0748d 100644 --- a/src/app/update/viewport.rs +++ b/src/app/update/viewport.rs @@ -341,8 +341,9 @@ impl OpenCADStudio { .abs(); if !self.point_size_relative && mag == 0.0 { let wpp = self.tabs[i].scene.world_per_pixel().unwrap_or(0.0); + let viewport_height = self.tabs[i].scene.selection.borrow().vp_size.1; mag = if wpp > 0.0 { - crate::entities::point::relative_world_size(0.0, wpp) + crate::entities::point::relative_world_size(0.0, wpp, viewport_height) } else { 1.0 }; diff --git a/src/entities/point.rs b/src/entities/point.rs index 2b8313450..2d4894d69 100644 --- a/src/entities/point.rs +++ b/src/entities/point.rs @@ -9,15 +9,8 @@ use crate::scene::convert::acad_to_render::{RenderEntity, RenderObject}; use crate::scene::model::object::{GripApply, GripDef, PropSection}; use crate::scene::model::wire_model::SnapHint; -/// Nominal viewport height (px) used to turn a relative PDSIZE percentage into -/// an on-screen pixel size. The exact viewport height isn't threaded into -/// tessellation; this reference keeps relative points a roughly constant -/// fraction of the screen across zoom levels. -const REL_REF_PX: f64 = 600.0; - /// Resolve a positive (absolute) PDSIZE to a world size. Relative/zero PDSIZE -/// is handled by [`relative_render`] when a world-per-pixel factor is available; -/// without one it falls back to a small fixed world size. +/// is handled by [`relative_render`]. fn pdsize_world(pdsize: f64) -> f64 { if pdsize > 0.0 { pdsize @@ -35,7 +28,9 @@ fn point_render(pt: &Point, pdmode: i16, s: f64) -> RenderEntity { // mirrored points (normal 0,0,-1) to the wrong side of the drawing. let (wx, wy, wz) = (pt.location.x, pt.location.y, pt.location.z); let snap = glam::DVec3::new(wx, wy, wz); - if pdmode == 0 { + let normal = point_normal(pt); + let top = snap + normal * pt.thickness; + if pdmode == 0 && pt.thickness.abs() <= 1.0e-10 { // Default: a single position (the driver sizes the dot in pixels). return RenderEntity { pick_tris: Vec::new(), @@ -46,7 +41,7 @@ fn point_render(pt: &Point, pdmode: i16, s: f64) -> RenderEntity { fill_tris: vec![], }; } - let pts = point_glyph(wx, wy, wz, pdmode, s); + let pts = point_glyph(pt, pdmode, s); if pts.is_empty() { // PDMODE 1 = nothing — emit an empty Lines wire so picking still works. return RenderEntity { @@ -63,20 +58,22 @@ fn point_render(pt: &Point, pdmode: i16, s: f64) -> RenderEntity { object: RenderObject::Lines(pts), snap_pts: vec![(snap, SnapHint::Node)], tangent_geoms: vec![], - key_vertices: vec![[wx, wy, wz]], + key_vertices: if pt.thickness.abs() > 1.0e-10 { + vec![[wx, wy, wz], [top.x, top.y, top.z]] + } else { + vec![[wx, wy, wz]] + }, fill_tris: vec![], } } -/// Viewport-aware override for a relative (≤ 0) PDSIZE: size the glyph from the -/// world-per-pixel factor so the point stays a roughly constant on-screen size -/// across zoom. Returns `None` for an absolute PDSIZE, the size-independent -/// default dot (PDMODE 0), or when no `wpp` is available — the caller then uses -/// the normal header-driven path. +/// Build a unit-sized glyph for a relative (≤ 0) PDSIZE. The wire shader scales +/// its planar displacement from the point origin using the live viewport size, +/// so zooming and resizing do not require retessellation. pub fn relative_render( entity: &EntityType, document: &acadrust::CadDocument, - wpp: Option, + _wpp: Option, ) -> Option { let EntityType::Point(pt) = entity else { return None; @@ -86,20 +83,55 @@ pub fn relative_render( if pdsize > 0.0 || pdmode == 0 { return None; } - let wpp = wpp.filter(|w| *w > 0.0)?; - Some(point_render(pt, pdmode, relative_world_size(pdsize, wpp) * 0.5)) + Some(point_render(pt, pdmode, 0.5)) } -/// Full on-screen glyph size (world units) for a relative (≤ 0) PDSIZE at the -/// given world-per-pixel factor. PDSIZE 0 is the 5% default; negative is the -/// percentage. Used both for rendering and to seed an absolute size when the -/// user switches the Point Style dialog to absolute units. -pub fn relative_world_size(pdsize: f64, wpp: f32) -> f64 { +/// Full glyph size in world units for a relative PDSIZE at the current +/// viewport height. Used when the style dialog converts a relative value to an +/// absolute one. +pub fn relative_world_size(pdsize: f64, wpp: f32, viewport_height_px: f32) -> f64 { let pct = if pdsize == 0.0 { 5.0 } else { -pdsize }; - (pct / 100.0) * REL_REF_PX * wpp as f64 + (pct / 100.0) * viewport_height_px.max(1.0) as f64 * wpp as f64 } -fn point_glyph(cx: f64, cy: f64, z: f64, pdmode: i16, s_half: f64) -> Vec<[f64; 3]> { +/// Percentage and plane normal encoded into the point wire for live GPU +/// viewport scaling. A zero PDSIZE means the standard five-percent size. +pub fn relative_marker_spec( + entity: &EntityType, + document: &acadrust::CadDocument, +) -> Option<(f32, [f32; 3])> { + let EntityType::Point(pt) = entity else { + return None; + }; + let size = document.header.point_display_size; + if size > 0.0 || effective_pdmode(pt, document.header.point_display_mode) == 0 { + return None; + } + let n = point_normal(pt); + Some(( + if size == 0.0 { 5.0 } else { -size as f32 }, + [n.x as f32, n.y as f32, n.z as f32], + )) +} + +fn point_normal(pt: &Point) -> glam::DVec3 { + glam::DVec3::new(pt.normal.x, pt.normal.y, pt.normal.z).normalize_or(glam::DVec3::Z) +} + +fn point_axes(pt: &Point) -> (glam::DVec3, glam::DVec3, glam::DVec3) { + let n = point_normal(pt); + let (ax, ay) = crate::scene::view::transform::ocs_axes((n.x, n.y, n.z)); + let base_x = glam::DVec3::new(ax.0, ax.1, ax.2); + let base_y = glam::DVec3::new(ay.0, ay.1, ay.2); + let (sin, cos) = pt.x_axis_angle.to_radians().sin_cos(); + ( + base_x * cos + base_y * sin, + -base_x * sin + base_y * cos, + n, + ) +} + +fn point_glyph(pt: &Point, pdmode: i16, s_half: f64) -> Vec<[f64; 3]> { // PDMODE bits: // shape: 0=dot, 1=nothing, 2='+', 3='×', 4='|' // +32 = enclose in a circle @@ -113,6 +145,12 @@ fn point_glyph(cx: f64, cy: f64, z: f64, pdmode: i16, s_half: f64) -> Vec<[f64; // cross pokes out past any enclosing circle/square, which sit at the radius. let arm = 2.0 * s_half; let nan = [f64::NAN, f64::NAN, f64::NAN]; + let center = glam::DVec3::new(pt.location.x, pt.location.y, pt.location.z); + let (axis_x, axis_y, normal) = point_axes(pt); + let map = |x: f64, y: f64, height: f64| { + let p = center + axis_x * x + axis_y * y + normal * height; + [p.x, p.y, p.z] + }; let mut pts: Vec<[f64; 3]> = Vec::new(); let mut push_seg = |a: [f64; 3], b: [f64; 3]| { if !pts.is_empty() { @@ -125,35 +163,34 @@ fn point_glyph(cx: f64, cy: f64, z: f64, pdmode: i16, s_half: f64) -> Vec<[f64; // 0 = single dot — emit a tiny "+" so it's visible at any zoom. 0 => { let d = s * 0.05; - push_seg([cx - d, cy, z], [cx + d, cy, z]); - push_seg([cx, cy - d, z], [cx, cy + d, z]); + push_seg(map(-d, 0.0, 0.0), map(d, 0.0, 0.0)); + push_seg(map(0.0, -d, 0.0), map(0.0, d, 0.0)); } 1 => {} // explicit nothing 2 => { - push_seg([cx - arm, cy, z], [cx + arm, cy, z]); - push_seg([cx, cy - arm, z], [cx, cy + arm, z]); + push_seg(map(-arm, 0.0, 0.0), map(arm, 0.0, 0.0)); + push_seg(map(0.0, -arm, 0.0), map(0.0, arm, 0.0)); } 3 => { - push_seg([cx - arm, cy - arm, z], [cx + arm, cy + arm, z]); - push_seg([cx - arm, cy + arm, z], [cx + arm, cy - arm, z]); + push_seg(map(-arm, -arm, 0.0), map(arm, arm, 0.0)); + push_seg(map(-arm, arm, 0.0), map(arm, -arm, 0.0)); } 4 => { // Upward tick rising from the point (length = PDSIZE/2), not a // vertical line centred on it. - push_seg([cx, cy, z], [cx, cy + s, z]); + push_seg(map(0.0, 0.0, 0.0), map(0.0, s, 0.0)); } _ => { - push_seg([cx - s, cy, z], [cx + s, cy, z]); - push_seg([cx, cy - s, z], [cx, cy + s, z]); + push_seg(map(-s, 0.0, 0.0), map(s, 0.0, 0.0)); + push_seg(map(0.0, -s, 0.0), map(0.0, s, 0.0)); } } if circle { - // 16-segment polyline circle. - const N: usize = 16; + const N: usize = 64; let mut ring: Vec<[f64; 3]> = Vec::with_capacity(N + 1); for i in 0..=N { let a = i as f64 * std::f64::consts::TAU / N as f64; - ring.push([cx + a.cos() * s, cy + a.sin() * s, z]); + ring.push(map(a.cos() * s, a.sin() * s, 0.0)); } if !pts.is_empty() { pts.push(nan); @@ -161,15 +198,33 @@ fn point_glyph(cx: f64, cy: f64, z: f64, pdmode: i16, s_half: f64) -> Vec<[f64; pts.extend(ring); } if square { - let p1 = [cx - s, cy - s, z]; - let p2 = [cx + s, cy - s, z]; - let p3 = [cx + s, cy + s, z]; - let p4 = [cx - s, cy + s, z]; + let p1 = map(-s, -s, 0.0); + let p2 = map(s, -s, 0.0); + let p3 = map(s, s, 0.0); + let p4 = map(-s, s, 0.0); if !pts.is_empty() { pts.push(nan); } pts.extend_from_slice(&[p1, p2, p3, p4, p1]); } + if pt.thickness.abs() > 1.0e-10 && !pts.is_empty() { + let base = pts.clone(); + pts.push(nan); + pts.extend(base.iter().map(|p| { + if p[0].is_finite() { + [ + p[0] + normal.x * pt.thickness, + p[1] + normal.y * pt.thickness, + p[2] + normal.z * pt.thickness, + ] + } else { + nan + } + })); + pts.push(nan); + pts.push(map(0.0, 0.0, 0.0)); + pts.push(map(0.0, 0.0, pt.thickness)); + } pts } diff --git a/src/locale_catalog.rs b/src/locale_catalog.rs index 249e2234e..73422d22b 100644 --- a/src/locale_catalog.rs +++ b/src/locale_catalog.rs @@ -1842,7 +1842,6 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static "PDMODE new value [0=dot 1=none 2=+ 3=x 4=tick; +32 circle +64 square]:" => Some(("styles", "pdmode-new-value-0-dot-1-none-2-3-x-4-tick-32")), "PDMODE = {v}" => Some(("styles", "pdmode-v")), "PDMODE set to {v}" => Some(("styles", "pdmode-set-to-v")), - "PDSIZE new point size (0 = 5% of viewport, <0 = absolute):" => Some(("styles", "pdsize-new-point-size-0-5-of-viewport-0-absolu")), "PDSIZE = {v:.4}" => Some(("styles", "pdsize-v-4")), "PDSIZE set to {v:.4}" => Some(("styles", "pdsize-set-to-v-4")), "PEDIT Enter option:" => Some(("modify", "pedit-enter-option")), diff --git a/src/scene/convert/tessellate.rs b/src/scene/convert/tessellate.rs index 1c4dfa837..243958bbd 100644 --- a/src/scene/convert/tessellate.rs +++ b/src/scene/convert/tessellate.rs @@ -1310,9 +1310,20 @@ pub fn tessellate( } else { (Vec::new(), Vec::new(), Vec::new()) }; + let relative_marker = + crate::entities::point::relative_marker_spec(entity, document); + let (taper_widths, world_width) = if let Some((percent, normal)) = relative_marker { + // A negative width is reserved for viewport-relative + // point markers. The first three taper entries carry + // the marker-plane normal; normal taper processing is + // disabled for the sentinel by the wire uploader. + (normal.to_vec(), -percent) + } else { + (Vec::new(), polyline_band_width(entity)) + }; out.push(WireModel { - taper_widths: Vec::new(), - world_width: polyline_band_width(entity), + taper_widths, + world_width, depth_override: None, display_visible: true, plot_visible: true, diff --git a/src/scene/pipeline/wire_gpu.rs b/src/scene/pipeline/wire_gpu.rs index 2de66884f..3f3efb798 100644 --- a/src/scene/pipeline/wire_gpu.rs +++ b/src/scene/pipeline/wire_gpu.rs @@ -111,7 +111,7 @@ impl WireInstance { } /// Per-wire constants shared by every segment of a wire (storage path). std430 -/// layout: three vec4 then eight scalars = 80 B, matching `WireConst` in +/// layout: three vec4, eight scalars, then three vec4 = 128 B, matching `WireConst` in /// wire_indexed.wgsl. `align_end` / `align_total` carry the "A"-type endpoint /// alignment (see `wire_distances`); 0.0 total = no alignment. #[repr(C)] @@ -132,6 +132,11 @@ pub struct WireConst { pub world_half_width: f32, pub _pad1: f32, pub _pad2: f32, + /// Point-marker origin as a double-single pair. `marker_normal_scale.w` + /// stores the viewport-height percentage; zero disables marker scaling. + pub marker_origin_high: [f32; 4], + pub marker_origin_low: [f32; 4], + pub marker_normal_scale: [f32; 4], } impl WireConst { @@ -189,6 +194,9 @@ pub struct PackedWireInstance { /// `world_half_width`). The shader interpolates across the segment. pub world_hw_a: f32, pub world_hw_b: f32, + pub marker_origin_high: [f32; 4], + pub marker_origin_low: [f32; 4], + pub marker_normal_scale: [f32; 4], } impl PackedWireInstance { @@ -213,6 +221,9 @@ impl PackedWireInstance { wgpu::VertexAttribute { offset: std::mem::offset_of!(PackedWireInstance, pos_b_low) as u64, shader_location: 8, format: wgpu::VertexFormat::Float32x3 }, // taper = (world_hw_a, world_hw_b) wgpu::VertexAttribute { offset: std::mem::offset_of!(PackedWireInstance, world_hw_a) as u64, shader_location: 9, format: wgpu::VertexFormat::Float32x2 }, + wgpu::VertexAttribute { offset: std::mem::offset_of!(PackedWireInstance, marker_origin_high) as u64, shader_location: 10, format: wgpu::VertexFormat::Float32x4 }, + wgpu::VertexAttribute { offset: std::mem::offset_of!(PackedWireInstance, marker_origin_low) as u64, shader_location: 11, format: wgpu::VertexFormat::Float32x4 }, + wgpu::VertexAttribute { offset: std::mem::offset_of!(PackedWireInstance, marker_normal_scale) as u64, shader_location: 12, format: wgpu::VertexFormat::Float32x4 }, ]; wgpu::VertexBufferLayout { array_stride: std::mem::size_of::() as u64, @@ -509,6 +520,33 @@ fn finite3(p: [f32; 3]) -> bool { p[0].is_finite() && p[1].is_finite() && p[2].is_finite() } +fn marker_metadata(wire: &WireModel) -> ([f32; 4], [f32; 4], [f32; 4]) { + if wire.world_width >= 0.0 { + return ([0.0; 4], [0.0; 4], [0.0; 4]); + } + let origin = wire + .snap_pts + .iter() + .find_map(|(point, hint)| { + matches!(hint, crate::scene::model::wire_model::SnapHint::Node).then_some(*point) + }) + .unwrap_or(glam::DVec3::ZERO); + let (hx, lx) = WireModel::split_ds(origin.x); + let (hy, ly) = WireModel::split_ds(origin.y); + let (hz, lz) = WireModel::split_ds(origin.z); + let normal = glam::Vec3::new( + wire.taper_widths.first().copied().unwrap_or(0.0), + wire.taper_widths.get(1).copied().unwrap_or(0.0), + wire.taper_widths.get(2).copied().unwrap_or(1.0), + ) + .normalize_or(glam::Vec3::Z); + ( + [hx, hy, hz, 0.0], + [lx, ly, lz, 0.0], + [normal.x, normal.y, normal.z, -wire.world_width], + ) +} + /// Emit packed per-segment instances (each carries the wire's constants). pub(crate) fn emit_wire_packed( wire: &WireModel, @@ -525,8 +563,15 @@ pub(crate) fn emit_wire_packed( return Vec::new(); } let (dists, align_end, align_total) = wire_distances(wire); + let (marker_origin_high, marker_origin_low, marker_normal_scale) = marker_metadata(wire); let low = |i: usize| -> [f32; 3] { wire.points_low.get(i).copied().unwrap_or([0.0; 3]) }; - let tw = |i: usize| -> f32 { wire.taper_widths.get(i).copied().unwrap_or(0.0) * 0.5 }; + let tw = |i: usize| -> f32 { + if wire.world_width < 0.0 { + 0.0 + } else { + wire.taper_widths.get(i).copied().unwrap_or(0.0) * 0.5 + } + }; let mut instances: Vec = Vec::with_capacity(seg_count); for i in 0..seg_count { let a = wire.points[i]; @@ -549,9 +594,12 @@ pub(crate) fn emit_wire_packed( draw_depth, align_end, align_total, - world_half_width: wire.world_width * 0.5, + world_half_width: wire.world_width.max(0.0) * 0.5, world_hw_a: tw(i), world_hw_b: tw(i + 1), + marker_origin_high, + marker_origin_low, + marker_normal_scale, }); } instances @@ -566,6 +614,7 @@ pub(crate) fn emit_wire_native( draw_depth: f32, ) -> (Vec, WireConst) { let (dists, align_end, align_total) = wire_distances(wire); + let (marker_origin_high, marker_origin_low, marker_normal_scale) = marker_metadata(wire); let cst = WireConst { color, pat0: [wire.pattern[0], wire.pattern[1], wire.pattern[2], wire.pattern[3]], @@ -575,9 +624,12 @@ pub(crate) fn emit_wire_native( draw_depth, align_end, align_total, - world_half_width: wire.world_width * 0.5, + world_half_width: wire.world_width.max(0.0) * 0.5, _pad1: 0.0, _pad2: 0.0, + marker_origin_high, + marker_origin_low, + marker_normal_scale, }; let n = wire.points.len(); let seg_count = n.saturating_sub(1); diff --git a/src/shaders/block_wire.wgsl b/src/shaders/block_wire.wgsl index c33c9fa13..b357e7846 100644 --- a/src/shaders/block_wire.wgsl +++ b/src/shaders/block_wire.wgsl @@ -26,6 +26,9 @@ struct WireConst { world_half_width: f32, _pad1: f32, _pad2: f32, + marker_origin_high: vec4, + marker_origin_low: vec4, + marker_normal_scale: vec4, } @group(1) @binding(0) var wire_const: WireConst; @@ -67,6 +70,29 @@ fn resolve_hw(taper_ratio: f32, world_hw: f32, px_hw: f32) -> f32 { return select(0.5, px_hw, u.lwdisplay_enable > 0.5); } +fn marker_relative( + position_high: vec3, + position_low: vec3, + translation: vec3, + translation_low: vec3, +) -> vec3 { + if wire_const.marker_normal_scale.w <= 0.0 { + return (position_high + translation - u.eye_high) + + (position_low + translation_low - u.eye_low); + } + let origin_high = wire_const.marker_origin_high.xyz; + let origin_low = wire_const.marker_origin_low.xyz; + let origin_relative = (origin_high + translation - u.eye_high) + + (origin_low + translation_low - u.eye_low); + let delta = (position_high - origin_high) + (position_low - origin_low); + let normal = normalize(wire_const.marker_normal_scale.xyz); + let axial = normal * dot(delta, normal); + let planar = delta - axial; + let world_size = wire_const.marker_normal_scale.w * 0.01 + * u.world_per_pixel * u.viewport_size.y; + return origin_relative + axial + planar * world_size; +} + @vertex fn vs_main(@builtin(vertex_index) vertex_index: u32, in: VertexIn) -> VertexOut { let corner = vertex_index % 6u; @@ -75,10 +101,8 @@ fn vs_main(@builtin(vertex_index) vertex_index: u32, in: VertexIn) -> VertexOut let which_end = which_end_arr[corner]; let side = side_arr[corner]; - let rel_a = (in.pos_a + in.translation - u.eye_high) - + (in.pos_a_low + in.translation_low - u.eye_low); - let rel_b = (in.pos_b + in.translation - u.eye_high) - + (in.pos_b_low + in.translation_low - u.eye_low); + let rel_a = marker_relative(in.pos_a, in.pos_a_low, in.translation, in.translation_low); + let rel_b = marker_relative(in.pos_b, in.pos_b_low, in.translation, in.translation_low); let clip_a = u.view_rot * vec4(rel_a, 1.0); let clip_b = u.view_rot * vec4(rel_b, 1.0); let screen_a = clip_a.xy / clip_a.w * u.viewport_size * 0.5; diff --git a/src/shaders/wire.wgsl b/src/shaders/wire.wgsl index c684aef56..2a974c46e 100644 --- a/src/shaders/wire.wgsl +++ b/src/shaders/wire.wgsl @@ -62,6 +62,9 @@ struct InstanceIn { @location(8) pos_b_low: vec3, // Per-endpoint world half-width for a tapered band (0 = use the constant). @location(9) taper: vec2, + @location(10) marker_origin_high: vec4, + @location(11) marker_origin_low: vec4, + @location(12) marker_normal_scale: vec4, } // Draw-order depth bias: shifts clip-space z so 2D entities of different @@ -102,6 +105,21 @@ fn resolve_hw(taper: f32, world_hw: f32, px_hw: f32) -> f32 { return select(0.5, px_hw, u.lwdisplay_enable > 0.5); } +fn marker_relative(position_high: vec3, position_low: vec3, instance: InstanceIn) -> vec3 { + if instance.marker_normal_scale.w <= 0.0 { + return (position_high - u.eye_high) + (position_low - u.eye_low); + } + let origin_high = instance.marker_origin_high.xyz; + let origin_low = instance.marker_origin_low.xyz; + let origin_relative = (origin_high - u.eye_high) + (origin_low - u.eye_low); + let delta = (position_high - origin_high) + (position_low - origin_low); + let normal = normalize(instance.marker_normal_scale.xyz); + let axial = normal * dot(delta, normal); + let planar = delta - axial; + let world_size = instance.marker_normal_scale.w * 0.01 * u.world_per_pixel * u.viewport_size.y; + return origin_relative + axial + planar * world_size; +} + @vertex fn vs_main(@builtin(vertex_index) vid: u32, in: InstanceIn) -> VertexOut { // Two-triangle quad corner table: // vid 0,1,2 = (A,-1) (B,-1) (B,+1) @@ -117,8 +135,8 @@ fn resolve_hw(taper: f32, world_hw: f32, px_hw: f32) -> f32 { // operands (Sterbenz); adding (pos_low − eye_low) restores the residual both // the vertex and the eye would otherwise lose — so geometry stays put at // UTM-scale coordinates and after a cross-drawing paste, with no jitter. - let rel_a = (in.pos_a - u.eye_high) + (in.pos_a_low - u.eye_low); - let rel_b = (in.pos_b - u.eye_high) + (in.pos_b_low - u.eye_low); + let rel_a = marker_relative(in.pos_a, in.pos_a_low, in); + let rel_b = marker_relative(in.pos_b, in.pos_b_low, in); let clip_a = u.view_rot * vec4(rel_a, 1.0); let clip_b = u.view_rot * vec4(rel_b, 1.0); diff --git a/src/shaders/wire_indexed.wgsl b/src/shaders/wire_indexed.wgsl index 9e1bd1748..b638a5d74 100644 --- a/src/shaders/wire_indexed.wgsl +++ b/src/shaders/wire_indexed.wgsl @@ -34,6 +34,9 @@ struct WireConst { world_half_width: f32, _pad1: f32, _pad2: f32, + marker_origin_high: vec4, + marker_origin_low: vec4, + marker_normal_scale: vec4, } @group(1) @binding(0) var wire_consts: array; @@ -81,6 +84,21 @@ fn resolve_hw(taper_ratio: f32, world_hw: f32, px_hw: f32) -> f32 { return select(0.5, px_hw, u.lwdisplay_enable > 0.5); } +fn marker_relative(position_high: vec3, position_low: vec3, c: WireConst) -> vec3 { + if c.marker_normal_scale.w <= 0.0 { + return (position_high - u.eye_high) + (position_low - u.eye_low); + } + let origin_high = c.marker_origin_high.xyz; + let origin_low = c.marker_origin_low.xyz; + let origin_relative = (origin_high - u.eye_high) + (origin_low - u.eye_low); + let delta = (position_high - origin_high) + (position_low - origin_low); + let normal = normalize(c.marker_normal_scale.xyz); + let axial = normal * dot(delta, normal); + let planar = delta - axial; + let world_size = c.marker_normal_scale.w * 0.01 * u.world_per_pixel * u.viewport_size.y; + return origin_relative + axial + planar * world_size; +} + @vertex fn vs_main(@builtin(vertex_index) vid: u32, in: InstanceIn) -> VertexOut { let c = wire_consts[in.wire_id]; @@ -89,8 +107,8 @@ fn resolve_hw(taper_ratio: f32, world_hw: f32, px_hw: f32) -> f32 { let which_end = which_end_arr[vid]; let side = side_arr[vid]; - let rel_a = (in.pos_a - u.eye_high) + (in.pos_a_low - u.eye_low); - let rel_b = (in.pos_b - u.eye_high) + (in.pos_b_low - u.eye_low); + let rel_a = marker_relative(in.pos_a, in.pos_a_low, c); + let rel_b = marker_relative(in.pos_b, in.pos_b_low, c); let clip_a = u.view_rot * vec4(rel_a, 1.0); let clip_b = u.view_rot * vec4(rel_b, 1.0); diff --git a/src/ui/style/point_style.rs b/src/ui/style/point_style.rs index ee3607cd7..14091e40d 100644 --- a/src/ui/style/point_style.rs +++ b/src/ui/style/point_style.rs @@ -63,7 +63,7 @@ impl canvas::Program for GlyphCanvas { stroke.clone(), ); } - 4 => frame.stroke(&line(Point::new(cx, cy - r), Point::new(cx, cy + r)), stroke.clone()), + 4 => frame.stroke(&line(Point::new(cx, cy), Point::new(cx, cy - r)), stroke.clone()), _ => {} } if self.mode & 32 != 0 {