cad-editor/src/entities/point.rs
2026-08-22 17:18:30 +03:00

364 lines
12 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

use acadrust::entities::Point;
use acadrust::EntityType;
use crate::t;
use crate::command::EntityTransform;
use crate::entities::common::{edit_prop as edit, parse_f64, square_grip};
use crate::entities::traits::RenderConvertible;
use crate::scene::convert::acad_to_render::{RenderEntity, RenderObject};
use crate::scene::model::object::{GripApply, GripDef, PropSection};
use crate::scene::model::wire_model::SnapHint;
/// Resolve a positive (absolute) PDSIZE to a world size. Relative/zero PDSIZE
/// is handled by [`relative_render`].
fn pdsize_world(pdsize: f64) -> f64 {
if pdsize > 0.0 {
pdsize
} else {
2.0
}
}
/// Build the render entity for a point given the glyph half-size `s` in world
/// units. Shared by the header-driven path ([`to_render`]) and the
/// viewport-aware relative path ([`relative_render`]).
fn point_render(pt: &Point, pdmode: i16, s: f64) -> RenderEntity {
// POINT location is stored in WCS (the extrusion normal only orients the
// glyph/thickness) — remapping it through the arbitrary-axis OCS moved
// 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);
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(),
object: RenderObject::Dot([wx, wy, wz]),
snap_pts: vec![(snap, SnapHint::Node)],
tangent_geoms: vec![],
key_vertices: vec![],
fill_tris: vec![],
};
}
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 {
pick_tris: Vec::new(),
object: RenderObject::Lines(vec![]),
snap_pts: vec![(snap, SnapHint::Node)],
tangent_geoms: vec![],
key_vertices: vec![[wx, wy, wz]],
fill_tris: vec![],
};
}
RenderEntity {
pick_tris: Vec::new(),
object: RenderObject::Lines(pts),
snap_pts: vec![(snap, SnapHint::Node)],
tangent_geoms: vec![],
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![],
}
}
/// 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<f32>,
) -> Option<RenderEntity> {
let EntityType::Point(pt) = entity else {
return None;
};
let pdsize = document.header.point_display_size;
let pdmode = effective_pdmode(pt, document.header.point_display_mode);
if pdsize > 0.0 || pdmode == 0 {
return None;
}
Some(point_render(pt, pdmode, 0.5))
}
/// 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) * viewport_height_px.max(1.0) as f64 * wpp as f64
}
/// 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
// +64 = enclose in a square
// (+96 = both)
let shape = (pdmode & 0x0F) as i32;
let circle = (pdmode & 32) != 0;
let square = (pdmode & 64) != 0;
let s = s_half;
// The '+' and '×' arms reach the full PDSIZE (twice the radius), so the
// 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() {
pts.push(nan);
}
pts.push(a);
pts.push(b);
};
match shape {
// 0 = single dot — emit a tiny "+" so it's visible at any zoom.
0 => {
let d = s * 0.05;
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(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(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(map(0.0, 0.0, 0.0), map(0.0, s, 0.0));
}
_ => {
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 {
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(map(a.cos() * s, a.sin() * s, 0.0));
}
if !pts.is_empty() {
pts.push(nan);
}
pts.extend(ring);
}
if square {
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
}
fn is_defpoints_layer(layer: &str) -> bool {
let name = layer.rsplit_once('|').map_or(layer, |(_, name)| name);
name.eq_ignore_ascii_case("DEFPOINTS")
}
/// Definition points always use the dot style.
fn effective_pdmode(pt: &Point, pdmode: i16) -> i16 {
if is_defpoints_layer(&pt.common.layer) {
0
} else {
pdmode
}
}
fn to_render(pt: &Point, document: &acadrust::CadDocument) -> RenderEntity {
let pdmode = effective_pdmode(pt, document.header.point_display_mode);
let s = pdsize_world(document.header.point_display_size) * 0.5;
point_render(pt, pdmode, s)
}
fn grips(pt: &Point) -> Vec<GripDef> {
let p = glam::DVec3::new(pt.location.x, pt.location.y, pt.location.z);
vec![square_grip(0, p)]
}
fn properties(pt: &Point) -> Vec<PropSection> {
vec![PropSection {
title: t!("Geometry").into_owned(),
props: vec![
edit(t!("Position X").as_ref(), "loc_x", pt.location.x),
edit(t!("Position Y").as_ref(), "loc_y", pt.location.y),
edit(t!("Position Z").as_ref(), "loc_z", pt.location.z),
],
}]
}
fn apply_geom_prop(pt: &mut Point, field: &str, value: &str) {
let Some(v) = parse_f64(value) else {
return;
};
match field {
"loc_x" => pt.location.x = v,
"loc_y" => pt.location.y = v,
"loc_z" => pt.location.z = v,
_ => {}
}
}
fn apply_grip(pt: &mut Point, _grip_id: usize, apply: GripApply) {
match apply {
GripApply::Absolute(p) => {
pt.location.x = p.x as f64;
pt.location.y = p.y as f64;
pt.location.z = p.z as f64;
}
GripApply::Translate(d) => {
pt.location.x += d.x as f64;
pt.location.y += d.y as f64;
pt.location.z += d.z as f64;
}
}
}
fn apply_transform(pt: &mut Point, t: &EntityTransform) {
crate::scene::view::transform::apply_standard_entity_transform(pt, t, |entity, p1, p2| {
crate::scene::view::transform::reflect_xy_point(
&mut entity.location.x,
&mut entity.location.y,
p1,
p2,
);
});
}
impl RenderConvertible for Point {
fn to_render(&self, document: &acadrust::CadDocument) -> Option<RenderEntity> {
Some(to_render(self, document))
}
}
crate::impl_entity_basics!(Point);
#[cfg(test)]
mod tests {
use super::*;
fn point_on(layer: &str) -> Point {
let mut pt = Point::default();
pt.common.layer = layer.to_string();
pt.location = acadrust::types::Vector3::new(1.0, 2.0, 0.0);
pt
}
#[test]
fn definition_points_ignore_the_point_style() {
let mut doc = acadrust::CadDocument::new();
doc.header.point_display_mode = 34;
doc.header.point_display_size = 2.0;
let drawn = to_render(&point_on("0"), &doc);
assert!(
matches!(drawn.object, RenderObject::Lines(ref pts) if !pts.is_empty()),
"a point the user placed still follows PDMODE"
);
for layer in ["Defpoints", "DEFPOINTS", "defpoints", "xref|Defpoints"] {
let defpoint = to_render(&point_on(layer), &doc);
assert!(
matches!(defpoint.object, RenderObject::Dot(_)),
"a definition point on {layer} must stay a dot"
);
}
}
#[test]
fn definition_points_ignore_the_point_style_at_relative_pdsize() {
let mut doc = acadrust::CadDocument::new();
doc.header.point_display_mode = 34;
doc.header.point_display_size = -5.0;
let drawn = relative_render(&EntityType::Point(point_on("0")), &doc, Some(0.01));
assert!(drawn.is_some(), "a user point still takes the viewport-aware path");
let defpoint = relative_render(
&EntityType::Point(point_on("xref|Defpoints")),
&doc,
Some(0.01),
);
assert!(
defpoint.is_none(),
"a definition point falls through to the dot instead of a sized glyph"
);
}
}