fix(rect): merge current main

Preserve both Point and Rectangle property layouts where the two pull requests changed the same section.
This commit is contained in:
Hakan Seven committed 2026-08-22 23:03:26 +03:00
commit e74d5d78fc
98 files changed
+1167 -268

No files matched your search

+5
View File
@@ -2066,6 +2066,7 @@ fn tessellate_dimension_inner(
let (ext1, ext2) = split_ext_lines(&geom.ext_lines);
if !ext1.is_empty() {
wires.push(WireModel {
point_marker: None,
taper_widths: Vec::new(),
world_width: 0.0,
depth_override: None,
@@ -2099,6 +2100,7 @@ fn tessellate_dimension_inner(
}
if !ext2.is_empty() {
wires.push(WireModel {
point_marker: None,
taper_widths: Vec::new(),
world_width: 0.0,
depth_override: None,
@@ -2132,6 +2134,7 @@ fn tessellate_dimension_inner(
}
} else {
wires.push(WireModel {
point_marker: None,
taper_widths: Vec::new(),
world_width: 0.0,
depth_override: None,
@@ -2166,6 +2169,7 @@ fn tessellate_dimension_inner(
}
wires.push(WireModel {
point_marker: None,
taper_widths: Vec::new(),
world_width: 0.0,
depth_override: None,
@@ -2216,6 +2220,7 @@ fn tessellate_dimension_inner(
aci_to_rgba(&c)
};
wires.push(WireModel {
point_marker: None,
taper_widths: Vec::new(),
world_width: 0.0,
depth_override: None,
+2
View File
@@ -674,6 +674,7 @@ impl LeaderTess for Leader {
if verts.len() < 2 {
return WireModel {
point_marker: None,
taper_widths: Vec::new(),
world_width: 0.0,
depth_override: None,
@@ -852,6 +853,7 @@ impl LeaderTess for Leader {
crate::scene::convert::tessellate::points_to_ds(fill_tris);
WireModel {
point_marker: None,
taper_widths: Vec::new(),
world_width: 0.0,
depth_override: None,
+5
View File
@@ -1503,6 +1503,7 @@ impl MultiLeaderTess for MultiLeader {
// WireModels so the renderer respects per-piece coloring.
let mut wires: Vec<WireModel> = Vec::new();
wires.push(WireModel {
point_marker: None,
taper_widths: Vec::new(),
world_width: 0.0,
depth_override: None,
@@ -1842,6 +1843,7 @@ impl MultiLeaderTess for MultiLeader {
xy = xy.max(p[1] as f64);
}
wires.push(WireModel {
point_marker: None,
taper_widths: Vec::new(),
world_width: 0.0,
depth_override: None,
@@ -1914,6 +1916,7 @@ impl MultiLeaderTess for MultiLeader {
pts.push([bx, by, z]);
}
wires.push(WireModel {
point_marker: None,
taper_widths: Vec::new(),
world_width: 0.0,
depth_override: None,
@@ -2001,6 +2004,7 @@ impl MultiLeaderTess for MultiLeader {
wcs_corners[3],
];
wires.push(WireModel {
point_marker: None,
taper_widths: Vec::new(),
world_width: 0.0,
depth_override: None,
@@ -2043,6 +2047,7 @@ impl MultiLeaderTess for MultiLeader {
wcs_corners[0],
];
wires.push(WireModel {
point_marker: None,
taper_widths: Vec::new(),
world_width: 0.0,
depth_override: None,
+121 -70
View File
@@ -1,23 +1,18 @@
use acadrust::entities::Point;
use acadrust::EntityType;
use crate::t;
use cadkernel::geom2d::{Circle, Curve, Line};
use cadkernel::space::{curve::bezier_points, PlanarCurve, Plane, Vec3};
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;
/// 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;
use crate::scene::model::wire_model::{PointMarker, SnapHint};
/// 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 +30,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 +43,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 +60,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<f32>,
_wpp: Option<f32>,
) -> Option<RenderEntity> {
let EntityType::Point(pt) = entity else {
return None;
@@ -86,20 +85,69 @@ 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<PointMarker> {
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 plane = point_plane(pt);
let normal = Vec3::from(plane.normal().unwrap_or([0.0, 0.0, 1.0]));
Some(PointMarker {
origin: glam::DVec3::new(pt.location.x, pt.location.y, pt.location.z),
normal: glam::DVec3::new(normal.x, normal.y, normal.z),
axis_x: glam::DVec3::from_array(plane.x_axis),
axis_y: glam::DVec3::from_array(plane.y_axis),
viewport_percent: if size == 0.0 { 5.0 } else { -size as f32 },
})
}
fn point_normal(pt: &Point) -> glam::DVec3 {
let normal = Vec3::new(pt.normal.x, pt.normal.y, pt.normal.z)
.normalize()
.unwrap_or(Vec3::Z);
glam::DVec3::new(normal.x, normal.y, normal.z)
}
fn point_plane(pt: &Point) -> Plane {
let origin = [pt.location.x, pt.location.y, pt.location.z];
let normal = Vec3::new(pt.normal.x, pt.normal.y, pt.normal.z)
.normalize()
.unwrap_or(Vec3::Z);
let x_seed = if normal.x.abs() < 1.0 / 64.0 && normal.y.abs() < 1.0 / 64.0 {
Vec3::Y.cross(normal)
} else {
Vec3::Z.cross(normal)
};
let base = Plane::orthonormal(origin, x_seed.to_array(), normal.to_array())
.unwrap_or(Plane::XY);
let (sin, cos) = pt.x_axis_angle.sin_cos();
Plane::from_axes(
origin,
base.vector_at([cos, sin]),
base.vector_at([-sin, cos]),
)
}
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
@@ -112,65 +160,68 @@ fn point_glyph(cx: f64, cy: f64, z: f64, pdmode: i16, s_half: f64) -> Vec<[f64;
// 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 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);
};
let mut curves = Vec::new();
let line = |start, end| Curve::Line(Line { start, end });
match shape {
// 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]);
curves.push(line([-d, 0.0], [d, 0.0]));
curves.push(line([0.0, -d], [0.0, d]));
}
1 => {} // explicit nothing
1 => {}
2 => {
push_seg([cx - arm, cy, z], [cx + arm, cy, z]);
push_seg([cx, cy - arm, z], [cx, cy + arm, z]);
curves.push(line([-arm, 0.0], [arm, 0.0]));
curves.push(line([0.0, -arm], [0.0, arm]));
}
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]);
curves.push(line([-arm, -arm], [arm, arm]));
curves.push(line([-arm, arm], [arm, -arm]));
}
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]);
curves.push(line([0.0, 0.0], [0.0, s]));
}
_ => {
push_seg([cx - s, cy, z], [cx + s, cy, z]);
push_seg([cx, cy - s, z], [cx, cy + s, z]);
curves.push(line([-s, 0.0], [s, 0.0]));
curves.push(line([0.0, -s], [0.0, s]));
}
}
if circle {
// 16-segment polyline circle.
const N: usize = 16;
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]);
}
if !pts.is_empty() {
pts.push(nan);
}
pts.extend(ring);
curves.push(Curve::Circle(Circle {
centre: [0.0, 0.0],
radius: s,
}));
}
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];
if !pts.is_empty() {
pts.push(nan);
}
pts.extend_from_slice(&[p1, p2, p3, p4, p1]);
curves.push(line([-s, -s], [s, -s]));
curves.push(line([s, -s], [s, s]));
curves.push(line([s, s], [-s, s]));
curves.push(line([-s, s], [-s, -s]));
}
pts
let plane = point_plane(pt);
let nan = [f64::NAN; 3];
let mut paths: Vec<Vec<[f64; 3]>> = curves
.iter()
.map(|curve| PlanarCurve::new(plane, curve.clone()).tessellate(64.0 / std::f64::consts::TAU))
.collect();
if pt.thickness.abs() > 1.0e-10 && !curves.is_empty() {
let normal = Vec3::from(plane.normal().unwrap_or([0.0, 0.0, 1.0]));
let top_origin = (Vec3::from(plane.origin) + normal * pt.thickness).to_array();
let top_plane = Plane::from_axes(top_origin, plane.x_axis, plane.y_axis);
paths.extend(curves.iter().map(|curve| {
PlanarCurve::new(top_plane, curve.clone())
.tessellate(64.0 / std::f64::consts::TAU)
}));
paths.push(bezier_points(&[plane.origin, top_origin], 1));
}
let mut points = Vec::new();
for path in paths {
if !points.is_empty() {
points.push(nan);
}
points.extend(path);
}
points
}
fn is_defpoints_layer(layer: &str) -> bool {
+1
View File
@@ -1667,6 +1667,7 @@ pub fn tessellate_table(
let mk =
|color: [f32; 4], points: Vec<[f32; 3]>, fill_tris: Vec<[f32; 3]>, lw: f32| -> WireModel {
WireModel {
point_marker: None,
taper_widths: Vec::new(),
world_width: 0.0,
depth_override: None,