From 21a232ef8d86e639b411de9d756f5dda7bb61ab5 Mon Sep 17 00:00:00 2001 From: ramox81 <184937705+ramox81@users.noreply.github.com> Date: Fri, 11 Sep 2026 17:11:52 +0300 Subject: [PATCH 1/2] Fix XLine and wipeout properties --- src/entities/raster_image.rs | 74 +++++++++++++++++++++++++++----- src/entities/ray.rs | 36 ++++++++-------- src/modules/draw/draw/wipeout.rs | 22 ++++++---- src/scene/cache/properties.rs | 1 + src/scene/convert/tess.rs | 1 + src/scene/model/solid_history.rs | 6 +++ 6 files changed, 104 insertions(+), 36 deletions(-) diff --git a/src/entities/raster_image.rs b/src/entities/raster_image.rs index c91aaf793..ead30891b 100644 --- a/src/entities/raster_image.rs +++ b/src/entities/raster_image.rs @@ -2,7 +2,10 @@ use acadrust::entities::{RasterImage, Wipeout}; use crate::t; use crate::command::EntityTransform; -use crate::entities::common::{center_grip, edit_prop as edit, ro_prop as ro, square_grip}; +use crate::entities::common::{ + center_grip, edit_angle_prop as edit_angle, edit_prop as edit, parse_angle_deg, parse_f64, + ro_prop as ro, square_grip, +}; use crate::entities::text_support::{resolve_text_style, text_local_bounds}; use crate::entities::traits::{Grippable, PropertyEditable, Transformable, RenderConvertible}; use crate::scene::convert::acad_to_render::{GlyphRun, TextStroke, RenderEntity, RenderObject}; @@ -473,6 +476,10 @@ fn wipeout_boundary(wipeout: &Wipeout) -> Vec<[f64; 3]> { } } +fn wipeout_axis_length(axis: &acadrust::types::Vector3, size: f64) -> f64 { + (axis.x * axis.x + axis.y * axis.y + axis.z * axis.z).sqrt() * size.abs() +} + fn wipeout_pick_rings(wipeout: &Wipeout) -> Vec> { let boundary = wipeout_boundary(wipeout); if wipeout_is_polygonal(wipeout) @@ -672,13 +679,28 @@ impl PropertyEditable for Wipeout { let bg_transparency = self .flags .contains(acadrust::entities::WipeoutDisplayFlags::TRANSPARENCY_ON); + let width = wipeout_axis_length(&self.u_vector, self.size.x); + let height = wipeout_axis_length(&self.v_vector, self.size.y); + let rotation = self.u_vector.y.atan2(self.u_vector.x).to_degrees(); vec![ + PropSection { + title: t!("Image Adjust").into_owned(), + props: vec![ + ro(t!("Brightness").as_ref(), "wo_brightness", self.brightness.to_string()), + ro(t!("Contrast").as_ref(), "wo_contrast", self.contrast.to_string()), + ro(t!("Fade").as_ref(), "wo_fade", self.fade.to_string()), + ], + }, PropSection { title: t!("Geometry").into_owned(), props: vec![ edit(t!("Position X").as_ref(), "wo_ox", self.insertion_point.x), edit(t!("Position Y").as_ref(), "wo_oy", self.insertion_point.y), edit(t!("Position Z").as_ref(), "wo_oz", self.insertion_point.z), + edit_angle(t!("Rotation").as_ref(), "wo_rotation", rotation), + edit(t!("Width").as_ref(), "wo_width", width), + edit(t!("Height").as_ref(), "wo_height", height), + ro(t!("Scale").as_ref(), "wo_scale", "1"), ], }, PropSection { @@ -689,25 +711,57 @@ impl PropertyEditable for Wipeout { ro(t!("Background transparency").as_ref(), "wo_bg_transparency", if bg_transparency { t!("Yes") } else { t!("No") }), ], }, - PropSection { - title: t!("Image Adjust").into_owned(), - props: vec![ - ro(t!("Brightness").as_ref(), "wo_brightness", self.brightness.to_string()), - ro(t!("Contrast").as_ref(), "wo_contrast", self.contrast.to_string()), - ro(t!("Fade").as_ref(), "wo_fade", self.fade.to_string()), - ], - }, ] } fn apply_geom_prop(&mut self, field: &str, value: &str) { - let Ok(v) = value.trim().parse::() else { + if field == "wo_rotation" { + let Some(target_degrees) = parse_angle_deg(value).filter(|value| value.is_finite()) else { + return; + }; + let u = glam::DVec3::new(self.u_vector.x, self.u_vector.y, self.u_vector.z); + let v = glam::DVec3::new(self.v_vector.x, self.v_vector.y, self.v_vector.z); + let normal = u.cross(v).normalize_or_zero(); + if normal.length_squared() <= 1e-18 { + return; + } + let current_radians = u.y.atan2(u.x); + let rotation = glam::DQuat::from_axis_angle( + normal, + target_degrees.to_radians() - current_radians, + ); + let u = rotation * u; + let v = rotation * v; + self.u_vector = acadrust::types::Vector3::new(u.x, u.y, u.z); + self.v_vector = acadrust::types::Vector3::new(v.x, v.y, v.z); + return; + } + + let Some(v) = parse_f64(value).filter(|value| value.is_finite()) else { return; }; match field { "wo_ox" => self.insertion_point.x = v, "wo_oy" => self.insertion_point.y = v, "wo_oz" => self.insertion_point.z = v, + "wo_width" if v > 0.0 => { + let current = wipeout_axis_length(&self.u_vector, self.size.x); + if current > 1e-9 { + let scale = v / current; + self.u_vector.x *= scale; + self.u_vector.y *= scale; + self.u_vector.z *= scale; + } + } + "wo_height" if v > 0.0 => { + let current = wipeout_axis_length(&self.v_vector, self.size.y); + if current > 1e-9 { + let scale = v / current; + self.v_vector.x *= scale; + self.v_vector.y *= scale; + self.v_vector.z *= scale; + } + } _ => {} } } diff --git a/src/entities/ray.rs b/src/entities/ray.rs index 620af15fa..04aeb5bb1 100644 --- a/src/entities/ray.rs +++ b/src/entities/ray.rs @@ -2,8 +2,10 @@ use acadrust::entities::{Ray, XLine}; use crate::t; use crate::command::EntityTransform; -use crate::entities::common::{center_grip, edit_prop as edit, square_grip}; -use crate::entities::curve::{point_along, unit_direction, unit_vector}; +use crate::entities::common::{ + center_grip, edit_prop as edit, format_length, ro_prop as ro, square_grip, +}; +use crate::entities::curve::{point_along, unit_direction}; use crate::entities::traits::{Grippable, PropertyEditable, Transformable, RenderConvertible}; use crate::scene::convert::acad_to_render::{RenderEntity, RenderObject}; use crate::scene::model::object::{GripApply, GripDef, PropSection}; @@ -240,9 +242,21 @@ impl PropertyEditable for XLine { edit(t!("Second X").as_ref(), "xl_sx", second_point.x), edit(t!("Second Y").as_ref(), "xl_sy", second_point.y), edit(t!("Second Z").as_ref(), "xl_sz", second_point.z), - edit(t!("Direction vector X").as_ref(), "xl_dx", self.direction.x), - edit(t!("Direction vector Y").as_ref(), "xl_dy", self.direction.y), - edit(t!("Direction vector Z").as_ref(), "xl_dz", self.direction.z), + ro( + t!("Direction vector X").as_ref(), + "xl_dx", + format_length(self.direction.x), + ), + ro( + t!("Direction vector Y").as_ref(), + "xl_dy", + format_length(self.direction.y), + ), + ro( + t!("Direction vector Z").as_ref(), + "xl_dz", + format_length(self.direction.z), + ), ], }] } @@ -267,18 +281,6 @@ impl PropertyEditable for XLine { self.direction = direction; } } - "xl_dx" | "xl_dy" | "xl_dz" => { - let mut direction = self.direction; - match field { - "xl_dx" => direction.x = v, - "xl_dy" => direction.y = v, - "xl_dz" => direction.z = v, - _ => unreachable!(), - } - if let Some(direction) = unit_vector(direction) { - self.direction = direction; - } - } _ => {} } } diff --git a/src/modules/draw/draw/wipeout.rs b/src/modules/draw/draw/wipeout.rs index 23c4e568e..994daaf52 100644 --- a/src/modules/draw/draw/wipeout.rs +++ b/src/modules/draw/draw/wipeout.rs @@ -335,7 +335,7 @@ impl CadCommand for WipeoutCommand { let mut preview = self.points.clone(); preview.push(point); preview.push(first); - Some(WireModel::solid_f64( + let mut wire = WireModel::solid_f64( "wipeout_preview".into(), preview .iter() @@ -343,7 +343,9 @@ impl CadCommand for WipeoutCommand { .collect(), WireModel::CYAN, false, - )) + ); + wire.line_weight_px = 1.5; + Some(wire) } WipeoutMode::Rectangular => { let first = self.first?; @@ -359,12 +361,14 @@ impl CadCommand for WipeoutCommand { ] .map(|corner| self.plane.to_world(corner)) }; - Some(WireModel::solid_f64( + let mut wire = WireModel::solid_f64( "wipeout_preview".into(), corners.iter().map(|p| [p.x, p.y, p.z]).collect(), WireModel::CYAN, false, - )) + ); + wire.line_weight_px = 1.5; + Some(wire) } WipeoutMode::Polyline | WipeoutMode::Frames | WipeoutMode::ErasePolyline => None, } @@ -409,11 +413,11 @@ fn make_rect_wipeout(first: DVec3, second: DVec3, plane: Plane) -> Option Option { plane.normal()?; let frame = polygon_frame(points, Tolerance::default())?; - let [width, height] = frame.size; + let extent = frame.size[0].max(frame.size[1]); let mut wipeout = Wipeout::new(); wipeout.insertion_point = vector(plane.point_at(frame.origin)); - wipeout.u_vector = vector(plane.x_axis) * width; - wipeout.v_vector = vector(plane.y_axis) * height; + wipeout.u_vector = vector(plane.x_axis) * extent; + wipeout.v_vector = vector(plane.y_axis) * extent; wipeout.size = Vector2::new(1.0, 1.0); wipeout.clip_type = WipeoutClipType::Polygonal; wipeout.clip_boundary_vertices = frame @@ -421,8 +425,8 @@ fn make_poly_wipeout(points: &[[f64; 2]], plane: Plane) -> Option { .iter() .map(|point| { Vector2::new( - (point[0] - frame.origin[0]) / width - 0.5, - 0.5 - (point[1] - frame.origin[1]) / height, + (point[0] - frame.origin[0]) / extent - 0.5, + 0.5 - (point[1] - frame.origin[1]) / extent, ) }) .collect(); diff --git a/src/scene/cache/properties.rs b/src/scene/cache/properties.rs index a2b0b3f73..146704d63 100644 --- a/src/scene/cache/properties.rs +++ b/src/scene/cache/properties.rs @@ -120,6 +120,7 @@ pub fn visualization_section(entity: &EntityType) -> Option { | EntityType::BlockEnd(_) | EntityType::Seqend(_) | EntityType::Leader(_) + | EntityType::Wipeout(_) | EntityType::Unknown(_) // Non-plotting drawing-view border: never rendered, no properties. | EntityType::ViewBorder(_) diff --git a/src/scene/convert/tess.rs b/src/scene/convert/tess.rs index 10ad57487..ef5da490d 100644 --- a/src/scene/convert/tess.rs +++ b/src/scene/convert/tess.rs @@ -1282,6 +1282,7 @@ fn tessellate_entity_inner( } if matches!(e, EntityType::Wipeout(_)) { b.depth_override = Some(0.5); + b.line_weight_px = b.line_weight_px.max(1.5); } } diff --git a/src/scene/model/solid_history.rs b/src/scene/model/solid_history.rs index 45eafe9e7..45bcde0d1 100644 --- a/src/scene/model/solid_history.rs +++ b/src/scene/model/solid_history.rs @@ -1609,6 +1609,12 @@ pub fn primitive_properties( document: &acadrust::CadDocument, handle: acadrust::Handle, ) -> Vec { + if !matches!( + document.get_entity(handle), + Some(acadrust::EntityType::Solid3D(_) | acadrust::EntityType::Surface(_)) + ) { + return Vec::new(); + } let Some(operation) = primitive_property_operation(document, handle) else { return brep_properties(document, handle); }; From 61e5271055c0a7924c2598c57a545bc1fe92962c Mon Sep 17 00:00:00 2001 From: ramox81 <184937705+ramox81@users.noreply.github.com> Date: Fri, 11 Sep 2026 17:27:34 +0300 Subject: [PATCH 2/2] Keep wipeout frames continuous --- src/modules/draw/draw/wipeout.rs | 4 ++-- src/scene/cache/block_cache.rs | 2 ++ src/scene/convert/tess.rs | 2 +- src/scene/model/wire_model.rs | 15 ++++++++++----- src/shaders/wire.wgsl | 3 ++- src/shaders/wire_indexed.wgsl | 3 ++- 6 files changed, 19 insertions(+), 10 deletions(-) diff --git a/src/modules/draw/draw/wipeout.rs b/src/modules/draw/draw/wipeout.rs index 994daaf52..56f60ca12 100644 --- a/src/modules/draw/draw/wipeout.rs +++ b/src/modules/draw/draw/wipeout.rs @@ -344,7 +344,7 @@ impl CadCommand for WipeoutCommand { WireModel::CYAN, false, ); - wire.line_weight_px = 1.5; + wire.set_fixed_screen_width(2.0); Some(wire) } WipeoutMode::Rectangular => { @@ -367,7 +367,7 @@ impl CadCommand for WipeoutCommand { WireModel::CYAN, false, ); - wire.line_weight_px = 1.5; + wire.set_fixed_screen_width(2.0); Some(wire) } WipeoutMode::Polyline | WipeoutMode::Frames | WipeoutMode::ErasePolyline => None, diff --git a/src/scene/cache/block_cache.rs b/src/scene/cache/block_cache.rs index 307a9a526..125c0dee8 100644 --- a/src/scene/cache/block_cache.rs +++ b/src/scene/cache/block_cache.rs @@ -2485,6 +2485,8 @@ fn emit_wire( let sx = ((ax.x - o.x).powi(2) + (ax.y - o.y).powi(2) + (ax.z - o.z).powi(2)).sqrt(); let sy = ((ay.x - o.x).powi(2) + (ay.y - o.y).powi(2) + (ay.z - o.z).powi(2)).sqrt(); lw.world_width * ((sx + sy) * 0.5) as f32 + } else if lw.world_width < 0.0 { + lw.world_width } else { 0.0 }; diff --git a/src/scene/convert/tess.rs b/src/scene/convert/tess.rs index ef5da490d..52bd87f11 100644 --- a/src/scene/convert/tess.rs +++ b/src/scene/convert/tess.rs @@ -1282,7 +1282,7 @@ fn tessellate_entity_inner( } if matches!(e, EntityType::Wipeout(_)) { b.depth_override = Some(0.5); - b.line_weight_px = b.line_weight_px.max(1.5); + b.set_fixed_screen_width(2.0); } } diff --git a/src/scene/model/wire_model.rs b/src/scene/model/wire_model.rs index 30905d7a3..deec10db2 100644 --- a/src/scene/model/wire_model.rs +++ b/src/scene/model/wire_model.rs @@ -407,11 +407,10 @@ pub struct WireModel { pub pattern: [f32; 8], /// Rendered line width in screen pixels (half-width = line_weight_px / 2). pub line_weight_px: f32, - /// World-space band width (drawing units). `0.0` = a normal wire whose - /// width comes from `line_weight_px` (screen pixels). Non-zero = a wide - /// polyline: the shader expands this centre-line to `world_width` world - /// units (scaling with zoom) so the band IS the wire — the linetype dash - /// pattern then applies to the band instead of a separate hatch fill. + /// World-space band width (drawing units). Positive values create a wide + /// polyline band that scales with zoom. Zero uses `line_weight_px` and the + /// drawing's lineweight display setting. A negative value encodes a fixed + /// screen-pixel width whose absolute value bypasses that display setting. pub world_width: f32, /// Per-point full band width (drawing units), aligned index-for-index with /// [`points`], for a polyline whose width VARIES (a taper). Empty = a @@ -553,6 +552,12 @@ impl WireModel { w } + pub fn set_fixed_screen_width(&mut self, width_px: f32) { + let width_px = width_px.max(1.0); + self.line_weight_px = width_px; + self.world_width = -width_px; + } + /// Create a solid wire (no dash pattern, 1px weight). pub fn solid(name: String, points: Vec<[f32; 3]>, color: [f32; 4], selected: bool) -> Self { Self { diff --git a/src/shaders/wire.wgsl b/src/shaders/wire.wgsl index f1622b691..6a458b1dd 100644 --- a/src/shaders/wire.wgsl +++ b/src/shaders/wire.wgsl @@ -104,6 +104,7 @@ struct VertexOut { fn resolve_hw(taper: f32, world_hw: f32, px_hw: f32) -> f32 { if taper >= 0.0 { return max(taper / u.world_per_pixel, 0.5); } if world_hw > 0.0 { return max(world_hw / u.world_per_pixel, 0.5); } + if world_hw < 0.0 { return max(-world_hw, 0.5); } var display_hw = max(px_hw * u.lineweight_scale, 0.5); if u.lineweight_scale < 0.0 { let scale = -u.lineweight_scale; @@ -227,7 +228,7 @@ fn marker_relative(position_high: vec3, position_low: vec3, instance: let clip_pos = mix(clip_a, clip_b, which_end); - let hw = resolve_hw(0.0, 0.0, in.dists.z); + let hw = resolve_hw(-1.0, in.misc.w, in.dists.z); let ext = which_end * 2.0 - 1.0; let offset_px = perp * hw * side + dir * hw * ext; let ndc_offset = offset_px / (u.viewport_size * 0.5); diff --git a/src/shaders/wire_indexed.wgsl b/src/shaders/wire_indexed.wgsl index a18ee51b6..8a3d099e3 100644 --- a/src/shaders/wire_indexed.wgsl +++ b/src/shaders/wire_indexed.wgsl @@ -83,6 +83,7 @@ fn resolve_hw(taper_ratio: f32, world_hw: f32, px_hw: f32) -> f32 { return max((taper_ratio * world_hw) / u.world_per_pixel, 0.5); } if world_hw > 0.0 { return max(world_hw / u.world_per_pixel, 0.5); } + if world_hw < 0.0 { return max(-world_hw, 0.5); } var display_hw = max(px_hw * u.lineweight_scale, 0.5); if u.lineweight_scale < 0.0 { let scale = -u.lineweight_scale; @@ -200,7 +201,7 @@ fn marker_relative(position_high: vec3, position_low: vec3, c: WireCon let clip_pos = mix(clip_a, clip_b, which_end); - let hw = resolve_hw(0.0, 0.0, c.half_width); + let hw = resolve_hw(0.0, c.world_half_width, c.half_width); let ext = which_end * 2.0 - 1.0; let offset_px = perp * hw * side + dir * hw * ext; let ndc_offset = offset_px / (u.viewport_size * 0.5);