forked from 1b-agent/opencadstudio-upstream
The codec and kernel repositories and crates are renamed opencadcodec and opencadkernel. Inside OCS they are depended on under short keys, so code reads `codec::`, `kernel::` and the new `graph::` (opencadgraph, the node graph engine). File-format and protocol spellings stay as they were: the ACIS product id `acadrust`, the ACADRUST_* switches and the plugin version keys `acadrust_version` / `acadrust_source`. A button left of MCP on the command line opens a translucent node graph over the viewport, with a palette of object, input, math, logic, list, text and geometry nodes added by click or drag. An object node owns drawing entities and its rows are the Properties panel's own rows for them: the single-object page is extracted from refresh_properties as entity_property_sections, values use the control protocol's property_json, and writes go through set_property_value with a property target override, so the panel, MCP and the graph share one read/write path. A list reaching an object row makes one entity per element. Each graph action is one undo step; the canvas pans and zooms. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1502 lines
53 KiB
Rust
1502 lines
53 KiB
Rust
use codec::entities::{Polyline, Polyline2D, Polyline3D};
|
|
use crate::t;
|
|
|
|
use crate::command::EntityTransform;
|
|
use crate::entities::common::{
|
|
edit_prop as edit, format_area, format_length, parse_f64, ro_prop as ro, square_grip,
|
|
stepper_prop as stepper, VARIES_LABEL,
|
|
};
|
|
use crate::entities::traits::{Grippable, PropertyEditable, Transformable, RenderConvertible};
|
|
use crate::scene::convert::acad_to_render::{extrusion_wall_tris, RenderEntity, RenderObject};
|
|
use crate::scene::model::object::{GripApply, GripDef, PropSection, PropValue, Property};
|
|
use crate::scene::model::wire_model::TangentGeom;
|
|
use kernel::space::{NurbsCurve3, Vec3};
|
|
|
|
// ── Polyline (old-style 3D heavy polyline) ────────────────────────────────────
|
|
|
|
fn tessellate_polyline(pl: &Polyline) -> RenderEntity {
|
|
let pts: Vec<[f64; 3]> = pl
|
|
.vertices
|
|
.iter()
|
|
.map(|v| [v.location.x, v.location.y, v.location.z])
|
|
.collect();
|
|
|
|
let mut points = pts.clone();
|
|
if pl.flags.is_closed() && pts.len() >= 2 {
|
|
points.push(pts[0]);
|
|
}
|
|
|
|
let key_verts = pts.clone();
|
|
RenderEntity {
|
|
pick_tris: Vec::new(),
|
|
object: RenderObject::Lines(points),
|
|
snap_pts: vec![],
|
|
tangent_geoms: vec![],
|
|
key_vertices: key_verts,
|
|
fill_tris: vec![],
|
|
}
|
|
}
|
|
|
|
impl RenderConvertible for Polyline {
|
|
fn to_render(&self, _document: &codec::CadDocument) -> Option<RenderEntity> {
|
|
Some(tessellate_polyline(self))
|
|
}
|
|
}
|
|
|
|
impl Grippable for Polyline {
|
|
fn grips(&self) -> Vec<GripDef> {
|
|
self.vertices
|
|
.iter()
|
|
.enumerate()
|
|
.map(|(i, v)| {
|
|
square_grip(
|
|
i,
|
|
glam::DVec3::new(v.location.x, v.location.y, v.location.z),
|
|
)
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
fn apply_grip(&mut self, grip_id: usize, apply: GripApply) {
|
|
if let Some(v) = self.vertices.get_mut(grip_id) {
|
|
match apply {
|
|
GripApply::Translate(d) => {
|
|
v.location.x += d.x as f64;
|
|
v.location.y += d.y as f64;
|
|
v.location.z += d.z as f64;
|
|
}
|
|
GripApply::Absolute(p) => {
|
|
v.location.x = p.x as f64;
|
|
v.location.y = p.y as f64;
|
|
v.location.z = p.z as f64;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn grip_menu(&self, _grip_id: usize) -> Vec<crate::scene::model::object::GripMenuItem> {
|
|
use crate::scene::model::object::{GripMenuAction, GripMenuItem};
|
|
vec![
|
|
GripMenuItem {
|
|
label: "Stretch",
|
|
action: GripMenuAction::Stretch,
|
|
},
|
|
GripMenuItem {
|
|
label: "Add Vertex",
|
|
action: GripMenuAction::AddVertex,
|
|
},
|
|
GripMenuItem {
|
|
label: "Remove Vertex",
|
|
action: GripMenuAction::RemoveVertex,
|
|
},
|
|
]
|
|
}
|
|
|
|
fn apply_grip_menu(&mut self, grip_id: usize, action: crate::scene::model::object::GripMenuAction) {
|
|
use crate::scene::model::object::GripMenuAction as A;
|
|
let n = self.vertices.len();
|
|
match action {
|
|
A::AddVertex if grip_id < n => {
|
|
if grip_id == n - 1 && !self.is_closed() {
|
|
self.vertices.push(self.vertices[grip_id].clone());
|
|
return;
|
|
}
|
|
let i1 = (grip_id + 1) % n;
|
|
let v0 = self.vertices[grip_id].clone();
|
|
let v1 = self.vertices[i1].clone();
|
|
let mx = (v0.location.x + v1.location.x) * 0.5;
|
|
let my = (v0.location.y + v1.location.y) * 0.5;
|
|
let mz = (v0.location.z + v1.location.z) * 0.5;
|
|
let mut new_v = v0.clone();
|
|
new_v.location.x = mx;
|
|
new_v.location.y = my;
|
|
new_v.location.z = mz;
|
|
let insert_at = (grip_id + 1).min(self.vertices.len());
|
|
self.vertices.insert(insert_at, new_v);
|
|
}
|
|
A::RemoveVertex if grip_id < n && n > 2 => {
|
|
self.vertices.remove(grip_id);
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl PropertyEditable for Polyline {
|
|
fn geometry_properties(&self, _text_style_names: &[String]) -> Vec<PropSection> {
|
|
vec![PropSection {
|
|
title: t!("Geometry").into_owned(),
|
|
props: vec![
|
|
ro(t!("Vertices").as_ref(), "vertices", self.vertices.len().to_string()),
|
|
Property {
|
|
label: t!("Closed").into_owned(),
|
|
field: "pl_closed",
|
|
value: PropValue::BoolToggle {
|
|
field: "pl_closed",
|
|
value: self.flags.is_closed(),
|
|
},
|
|
},
|
|
],
|
|
}]
|
|
}
|
|
|
|
fn apply_geom_prop(&mut self, field: &str, value: &str) {
|
|
if field == "pl_closed" {
|
|
let closed = if value == "toggle" {
|
|
!self.flags.is_closed()
|
|
} else {
|
|
value == "true"
|
|
};
|
|
self.flags.set_closed(closed);
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Transformable for Polyline {
|
|
fn apply_transform(&mut self, t: &EntityTransform) {
|
|
crate::scene::view::transform::apply_standard_entity_transform(self, t, |entity, p1, p2| {
|
|
for v in &mut entity.vertices {
|
|
crate::scene::view::transform::reflect_xy_point(
|
|
&mut v.location.x,
|
|
&mut v.location.y,
|
|
p1,
|
|
p2,
|
|
);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// ── Polyline2D (heavy 2D polyline with bulge) ─────────────────────────────────
|
|
|
|
/// The vertices a curve-/spline-fit 2D polyline actually DRAWS: the
|
|
/// fit-generated points (VertexFlags 8 / 1) plus any unflagged originals,
|
|
/// with the spline-frame CONTROL points (flag 16) dropped — those are editing
|
|
/// scaffolding shown only under SPLFRAME, and chaining through them draws
|
|
/// spokes across the fitted curve (#408). `None` = no fit data, draw the
|
|
/// stored vertices as-is. A fit-flagged polyline whose fit points are missing
|
|
/// falls back to the stored vertices rather than drawing nothing.
|
|
pub fn drawn_vertices2d(
|
|
pl: &Polyline2D,
|
|
) -> Option<Vec<codec::entities::Vertex2D>> {
|
|
use codec::entities::polyline::VertexFlags;
|
|
let has_fit = pl.vertices.iter().any(|v| {
|
|
v.flags.bits()
|
|
& (VertexFlags::SPLINE_VERTEX.bits() | VertexFlags::EXTRA_VERTEX.bits())
|
|
!= 0
|
|
});
|
|
if !has_fit {
|
|
return None;
|
|
}
|
|
let kept: Vec<_> = pl
|
|
.vertices
|
|
.iter()
|
|
.filter(|v| v.flags.bits() & VertexFlags::SPLINE_CONTROL.bits() == 0)
|
|
.cloned()
|
|
.collect();
|
|
(kept.len() >= 2).then_some(kept)
|
|
}
|
|
|
|
fn tessellate_polyline2d(pl: &Polyline2D, fill_mode: bool) -> RenderEntity {
|
|
let filtered = drawn_vertices2d(pl);
|
|
let verts: &[codec::entities::Vertex2D] = filtered.as_deref().unwrap_or(&pl.vertices);
|
|
if verts.is_empty() {
|
|
return RenderEntity {
|
|
pick_tris: Vec::new(),
|
|
object: RenderObject::Lines(vec![]),
|
|
snap_pts: vec![],
|
|
tangent_geoms: vec![],
|
|
key_vertices: vec![],
|
|
fill_tris: vec![],
|
|
};
|
|
}
|
|
|
|
let elev = pl.elevation;
|
|
let normal = (pl.normal.x, pl.normal.y, pl.normal.z);
|
|
let count = verts.len();
|
|
let seg_count = if pl.is_closed() { count } else { count - 1 };
|
|
let mut tangents: Vec<TangentGeom> = Vec::new();
|
|
let mut key_verts: Vec<[f64; 3]> = Vec::new();
|
|
|
|
let to_wcs = |x: f64, y: f64| -> (f64, f64, f64) {
|
|
crate::scene::view::transform::ocs_point_to_wcs((x, y, elev), normal)
|
|
};
|
|
let to_pt = |v: &codec::entities::Vertex2D| -> [f64; 3] {
|
|
let (wx, wy, wz) = to_wcs(v.location.x, v.location.y);
|
|
[wx, wy, wz]
|
|
};
|
|
|
|
if !fill_mode {
|
|
let continuous = pl.flags.bits()
|
|
& codec::entities::PolylineFlags::LINETYPE_CONTINUOUS.bits()
|
|
!= 0;
|
|
let mut boundary = crate::entities::common::wide_band_outline(
|
|
&band_verts_2d(pl),
|
|
pl.is_closed(),
|
|
!continuous,
|
|
&to_wcs,
|
|
);
|
|
if !boundary.points.is_empty() {
|
|
if pl.thickness.abs() > 1e-10 {
|
|
boundary = crate::entities::common::extrude_wide_band_outline(
|
|
boundary,
|
|
[
|
|
pl.thickness * normal.0,
|
|
pl.thickness * normal.1,
|
|
pl.thickness * normal.2,
|
|
],
|
|
);
|
|
}
|
|
let (tangent_geoms, key_vertices) =
|
|
centerline_metadata_2d(verts, pl.is_closed(), &to_wcs, normal);
|
|
return RenderEntity {
|
|
pick_tris: Vec::new(),
|
|
object: RenderObject::BoundaryLines {
|
|
points: boundary.points,
|
|
stations: boundary.stations,
|
|
point_segments: boundary.point_segments,
|
|
station_pieces: boundary.station_pieces,
|
|
source_length: boundary.source_length,
|
|
plinegen: continuous,
|
|
},
|
|
snap_pts: vec![],
|
|
tangent_geoms,
|
|
key_vertices,
|
|
fill_tris: vec![],
|
|
};
|
|
}
|
|
}
|
|
|
|
if pl.thickness.abs() > 1e-10 {
|
|
let (nx, ny, nz) = normal;
|
|
let t = pl.thickness;
|
|
let off = |p: [f64; 3]| -> [f64; 3] { [p[0] + t * nx, p[1] + t * ny, p[2] + t * nz] };
|
|
let to_f32 = |p: [f64; 3]| -> [f32; 3] { [p[0] as f32, p[1] as f32, p[2] as f32] };
|
|
let mut path: Vec<[f64; 3]> = Vec::new();
|
|
let mut kv: Vec<[f64; 3]> = Vec::new();
|
|
let mut tgs: Vec<TangentGeom> = Vec::new();
|
|
let (w0x, w0y, w0z) = to_wcs(verts[0].location.x, verts[0].location.y);
|
|
path.push([w0x, w0y, w0z]);
|
|
kv.push([w0x, w0y, w0z]);
|
|
for i in 0..seg_count {
|
|
let va = &verts[i];
|
|
let vb = &verts[(i + 1) % count];
|
|
let (ox0, oy0) = (va.location.x, va.location.y);
|
|
let (ox1, oy1) = (vb.location.x, vb.location.y);
|
|
let bulge = va.bulge;
|
|
if bulge.abs() < 1e-9 {
|
|
let (wx, wy, wz) = to_wcs(ox1, oy1);
|
|
path.push([wx, wy, wz]);
|
|
let p1_pt = path[path.len() - 2];
|
|
let p2_pt = *path.last().unwrap();
|
|
tgs.push(TangentGeom::Line {
|
|
p1: to_f32(p1_pt),
|
|
p2: to_f32(p2_pt),
|
|
});
|
|
} else if let Some(arc) =
|
|
crate::entities::common::BulgeArc::from_bulge([ox0, oy0], [ox1, oy1], bulge)
|
|
{
|
|
tgs.push(crate::entities::common::bulge_arc_to_tangent(&arc, &to_wcs, normal));
|
|
for s in arc
|
|
.tessellate_angle(kernel::tessellation::DEFAULT_ANGLE)
|
|
.into_iter()
|
|
.skip(1)
|
|
{
|
|
let (wx, wy, wz) = to_wcs(s[0], s[1]);
|
|
path.push([wx, wy, wz]);
|
|
}
|
|
}
|
|
let (wbx, wby, wbz) = to_wcs(ox1, oy1);
|
|
kv.push([wbx, wby, wbz]);
|
|
}
|
|
// A wide Polyline2D extrudes its whole band into a solid tube (walls +
|
|
// caps), same as a thickened LwPolyline; a zero-width one falls through
|
|
// to the centre-line extrusion below.
|
|
let is_wide = pl.start_width > 1e-9
|
|
|| pl.end_width > 1e-9
|
|
|| pl
|
|
.vertices
|
|
.iter()
|
|
.any(|v| v.start_width > 1e-9 || v.end_width > 1e-9);
|
|
if is_wide {
|
|
let (origin, fills) = wide_fills(pl);
|
|
let (fill_tris, lines) =
|
|
crate::entities::common::thick_band_tube(origin, &fills, t, normal, &to_wcs);
|
|
return RenderEntity {
|
|
pick_tris: fill_tris.clone(),
|
|
object: RenderObject::Lines(lines),
|
|
snap_pts: vec![],
|
|
tangent_geoms: tgs,
|
|
key_vertices: kv,
|
|
fill_tris,
|
|
};
|
|
}
|
|
let mut pts: Vec<[f64; 3]> = Vec::with_capacity(path.len() * 2 + kv.len() * 3 + 4);
|
|
pts.extend_from_slice(&path);
|
|
pts.push([f64::NAN; 3]);
|
|
for &p in &path {
|
|
pts.push(off(p));
|
|
}
|
|
if !kv.is_empty() {
|
|
pts.push([f64::NAN; 3]);
|
|
for (i, &pb) in kv.iter().enumerate() {
|
|
pts.push(pb);
|
|
pts.push(off(pb));
|
|
if i + 1 < kv.len() {
|
|
pts.push([f64::NAN; 3]);
|
|
}
|
|
}
|
|
}
|
|
return RenderEntity {
|
|
pick_tris: extrusion_wall_tris(&path, [t * nx, t * ny, t * nz]),
|
|
object: RenderObject::Lines(pts),
|
|
snap_pts: vec![],
|
|
tangent_geoms: tgs,
|
|
key_vertices: kv,
|
|
fill_tris: vec![],
|
|
};
|
|
}
|
|
|
|
for i in 0..seg_count {
|
|
let v0 = &verts[i];
|
|
let v1 = &verts[(i + 1) % count];
|
|
let p0 = to_pt(v0);
|
|
let p1 = to_pt(v1);
|
|
let bulge = v0.bulge;
|
|
|
|
if bulge.abs() < 1e-9 {
|
|
tangents.push(TangentGeom::Line {
|
|
p1: [p0[0] as f32, p0[1] as f32, p0[2] as f32],
|
|
p2: [p1[0] as f32, p1[1] as f32, p1[2] as f32],
|
|
});
|
|
} else if let Some(arc) = crate::entities::common::BulgeArc::from_bulge(
|
|
[v0.location.x, v0.location.y],
|
|
[v1.location.x, v1.location.y],
|
|
bulge,
|
|
) {
|
|
tangents.push(crate::entities::common::bulge_arc_to_tangent(&arc, &to_wcs, normal));
|
|
}
|
|
|
|
if i == 0 {
|
|
key_verts.push([p0[0], p0[1], p0[2]]);
|
|
}
|
|
key_verts.push([p1[0], p1[1], p1[2]]);
|
|
}
|
|
|
|
let band_verts = band_verts_2d(pl);
|
|
let (fill_origin, fills) = wide_fills(pl);
|
|
// A wide Polyline2D whose per-vertex widths VARY renders a smooth taper; a
|
|
// uniform-width one keeps the constant-band Contour.
|
|
let object = match tapered_band_verts_2d(&band_verts) {
|
|
Some(band_verts) => {
|
|
let (pts, widths) = crate::entities::common::tapered_band_points(
|
|
band_verts,
|
|
pl.is_closed(),
|
|
&to_wcs,
|
|
);
|
|
RenderObject::TaperedLines(pts, widths)
|
|
}
|
|
None => RenderObject::Lines(
|
|
crate::entities::curve::polyline2d_curve(pl)
|
|
.map(|planar| crate::entities::curve::curve_points(&planar))
|
|
.unwrap_or_default(),
|
|
),
|
|
};
|
|
RenderEntity {
|
|
pick_tris: crate::entities::common::wide_band_tris(fill_origin, &fills),
|
|
object,
|
|
snap_pts: vec![],
|
|
tangent_geoms: tangents,
|
|
key_vertices: key_verts,
|
|
fill_tris: vec![],
|
|
}
|
|
}
|
|
|
|
/// Effective segment widths for a Polyline2D band.
|
|
fn band_verts_2d(
|
|
pl: &codec::entities::Polyline2D,
|
|
) -> Vec<([f64; 2], f64, f64, f64)> {
|
|
let default_start = pl.start_width;
|
|
let default_end = pl.end_width;
|
|
let filtered = drawn_vertices2d(pl);
|
|
let verts: &[codec::entities::Vertex2D] = filtered.as_deref().unwrap_or(&pl.vertices);
|
|
verts
|
|
.iter()
|
|
.map(|v| {
|
|
let sw = if v.start_width > 1e-9 {
|
|
v.start_width
|
|
} else {
|
|
default_start
|
|
};
|
|
let ew = if v.end_width > 1e-9 {
|
|
v.end_width
|
|
} else {
|
|
default_end
|
|
};
|
|
([v.location.x, v.location.y], v.bulge, sw, ew)
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
fn tapered_band_verts_2d(
|
|
band: &[([f64; 2], f64, f64, f64)],
|
|
) -> Option<&[([f64; 2], f64, f64, f64)]> {
|
|
let w0 = band.first().map_or(0.0, |v| v.2);
|
|
let varies = band
|
|
.iter()
|
|
.any(|&(_, _, sw, ew)| (sw - w0).abs() > 1e-9 || (ew - w0).abs() > 1e-9);
|
|
if varies && w0.max(band.iter().map(|v| v.3).fold(0.0, f64::max)) > 1e-9 {
|
|
Some(band)
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
fn centerline_metadata_2d(
|
|
verts: &[codec::entities::Vertex2D],
|
|
closed: bool,
|
|
to_wcs: &dyn Fn(f64, f64) -> (f64, f64, f64),
|
|
normal: (f64, f64, f64),
|
|
) -> (Vec<TangentGeom>, Vec<[f64; 3]>) {
|
|
let count = verts.len();
|
|
let segment_count = if closed {
|
|
count
|
|
} else {
|
|
count.saturating_sub(1)
|
|
};
|
|
let mut tangents = Vec::with_capacity(segment_count);
|
|
let mut key_vertices = Vec::with_capacity(segment_count + 1);
|
|
for index in 0..segment_count {
|
|
let start = &verts[index];
|
|
let end = &verts[(index + 1) % count];
|
|
let p0 = to_wcs(start.location.x, start.location.y);
|
|
let p1 = to_wcs(end.location.x, end.location.y);
|
|
if start.bulge.abs() < 1e-9 {
|
|
tangents.push(TangentGeom::Line {
|
|
p1: [p0.0 as f32, p0.1 as f32, p0.2 as f32],
|
|
p2: [p1.0 as f32, p1.1 as f32, p1.2 as f32],
|
|
});
|
|
} else if let Some(arc) = crate::entities::common::BulgeArc::from_bulge(
|
|
[start.location.x, start.location.y],
|
|
[end.location.x, end.location.y],
|
|
start.bulge,
|
|
) {
|
|
tangents.push(crate::entities::common::bulge_arc_to_tangent(&arc, to_wcs, normal));
|
|
}
|
|
if index == 0 {
|
|
key_vertices.push([p0.0, p0.1, p0.2]);
|
|
}
|
|
key_vertices.push([p1.0, p1.1, p1.2]);
|
|
}
|
|
(tangents, key_vertices)
|
|
}
|
|
|
|
impl RenderConvertible for Polyline2D {
|
|
fn to_render(&self, document: &codec::CadDocument) -> Option<RenderEntity> {
|
|
Some(tessellate_polyline2d(self, document.header.fill_mode))
|
|
}
|
|
}
|
|
|
|
impl Grippable for Polyline2D {
|
|
fn grips(&self) -> Vec<GripDef> {
|
|
let elev = self.elevation;
|
|
self.vertices
|
|
.iter()
|
|
.enumerate()
|
|
.map(|(i, v)| square_grip(i, glam::DVec3::new(v.location.x, v.location.y, elev)))
|
|
.collect()
|
|
}
|
|
|
|
fn apply_grip(&mut self, grip_id: usize, apply: GripApply) {
|
|
if let Some(v) = self.vertices.get_mut(grip_id) {
|
|
match apply {
|
|
GripApply::Translate(d) => {
|
|
v.location.x += d.x as f64;
|
|
v.location.y += d.y as f64;
|
|
}
|
|
GripApply::Absolute(p) => {
|
|
v.location.x = p.x as f64;
|
|
v.location.y = p.y as f64;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn grip_menu(&self, _grip_id: usize) -> Vec<crate::scene::model::object::GripMenuItem> {
|
|
use crate::scene::model::object::{GripMenuAction, GripMenuItem};
|
|
vec![
|
|
GripMenuItem {
|
|
label: "Stretch",
|
|
action: GripMenuAction::Stretch,
|
|
},
|
|
GripMenuItem {
|
|
label: "Add Vertex",
|
|
action: GripMenuAction::AddVertex,
|
|
},
|
|
GripMenuItem {
|
|
label: "Remove Vertex",
|
|
action: GripMenuAction::RemoveVertex,
|
|
},
|
|
]
|
|
}
|
|
|
|
fn apply_grip_menu(&mut self, grip_id: usize, action: crate::scene::model::object::GripMenuAction) {
|
|
use crate::scene::model::object::GripMenuAction as A;
|
|
let n = self.vertices.len();
|
|
let elev = self.elevation;
|
|
match action {
|
|
A::AddVertex if grip_id < n => {
|
|
if grip_id == n - 1 && !self.is_closed() {
|
|
self.vertices[grip_id].bulge = 0.0;
|
|
let mut new_v = self.vertices[grip_id].clone();
|
|
new_v.bulge = 0.0;
|
|
new_v.id = 0;
|
|
self.vertices.push(new_v);
|
|
return;
|
|
}
|
|
let i1 = (grip_id + 1) % n;
|
|
let v0 = self.vertices[grip_id].clone();
|
|
let v1 = self.vertices[i1].clone();
|
|
let midpoint = if v0.bulge.abs() >= 1e-9 {
|
|
crate::entities::common::BulgeArc::from_bulge(
|
|
[v0.location.x, v0.location.y],
|
|
[v1.location.x, v1.location.y],
|
|
v0.bulge,
|
|
)
|
|
.map(|arc| arc.sample(0.5))
|
|
.unwrap_or([
|
|
(v0.location.x + v1.location.x) * 0.5,
|
|
(v0.location.y + v1.location.y) * 0.5,
|
|
])
|
|
} else {
|
|
[
|
|
(v0.location.x + v1.location.x) * 0.5,
|
|
(v0.location.y + v1.location.y) * 0.5,
|
|
]
|
|
};
|
|
let mut new_v = v0.clone();
|
|
new_v.location.x = midpoint[0];
|
|
new_v.location.y = midpoint[1];
|
|
new_v.location.z = elev;
|
|
new_v.id = 0;
|
|
let effective_start = if v0.start_width > 1e-9 {
|
|
v0.start_width
|
|
} else {
|
|
self.start_width
|
|
};
|
|
let effective_end = if v0.end_width > 1e-9 {
|
|
v0.end_width
|
|
} else {
|
|
self.end_width
|
|
};
|
|
let middle_width = (effective_start + effective_end) * 0.5;
|
|
self.vertices[grip_id].end_width = middle_width;
|
|
new_v.start_width = middle_width;
|
|
if v0.bulge.abs() >= 1e-9 {
|
|
new_v.bulge = (v0.bulge.atan() * 0.5).tan();
|
|
}
|
|
let insert_at = (grip_id + 1).min(self.vertices.len());
|
|
if v0.bulge.abs() >= 1e-9 {
|
|
self.vertices[grip_id].bulge = new_v.bulge;
|
|
}
|
|
self.vertices.insert(insert_at, new_v);
|
|
}
|
|
A::RemoveVertex if grip_id < n && n > 2 => {
|
|
self.vertices.remove(grip_id);
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(crate) fn polyline2d_vertex_segment_widths(
|
|
vertex: &codec::entities::Vertex2D,
|
|
default_start: f64,
|
|
default_end: f64,
|
|
) -> (f64, f64) {
|
|
if vertex.start_width > 1e-9 || vertex.end_width > 1e-9 {
|
|
(vertex.start_width, vertex.end_width)
|
|
} else {
|
|
(default_start, default_end)
|
|
}
|
|
}
|
|
|
|
pub(crate) fn polyline2d_global_width(pline: &Polyline2D) -> Option<f64> {
|
|
let filtered = drawn_vertices2d(pline);
|
|
let verts: &[codec::entities::Vertex2D] =
|
|
filtered.as_deref().unwrap_or(&pline.vertices);
|
|
let count = verts.len();
|
|
let seg_count = if pline.is_closed() {
|
|
count
|
|
} else {
|
|
count.saturating_sub(1)
|
|
};
|
|
if seg_count == 0 {
|
|
if (pline.start_width - pline.end_width).abs() < 1e-6 {
|
|
return Some(pline.start_width);
|
|
} else {
|
|
return None;
|
|
}
|
|
}
|
|
let (w0_start, w0_end) =
|
|
polyline2d_vertex_segment_widths(&verts[0], pline.start_width, pline.end_width);
|
|
if (w0_start - w0_end).abs() > 1e-6 {
|
|
return None;
|
|
}
|
|
let w0 = w0_start;
|
|
for i in 1..seg_count {
|
|
let (sw, ew) =
|
|
polyline2d_vertex_segment_widths(&verts[i], pline.start_width, pline.end_width);
|
|
if (sw - ew).abs() > 1e-6 || (sw - w0).abs() > 1e-6 {
|
|
return None;
|
|
}
|
|
}
|
|
Some(w0)
|
|
}
|
|
|
|
impl PropertyEditable for Polyline2D {
|
|
fn geometry_properties(&self, _text_style_names: &[String]) -> Vec<PropSection> {
|
|
let n = self.vertices.len();
|
|
let mut area = 0.0;
|
|
let mut length = 0.0;
|
|
let seg_count = if self.is_closed() { n } else { n.saturating_sub(1) };
|
|
for i in 0..seg_count {
|
|
let a = &self.vertices[i].location;
|
|
let b = &self.vertices[(i + 1) % n].location;
|
|
area += a.x * b.y - b.x * a.y;
|
|
length += ((b.x - a.x).powi(2) + (b.y - a.y).powi(2)).sqrt();
|
|
}
|
|
area = (area * 0.5).abs();
|
|
|
|
let vi = if n == 0 {
|
|
0
|
|
} else {
|
|
crate::scene::view::dispatch::prop_current_vertex().min(n - 1)
|
|
};
|
|
let v = self.vertices.get(vi);
|
|
let vertex_x = v.map(|v| v.location.x).unwrap_or_default();
|
|
let vertex_y = v.map(|v| v.location.y).unwrap_or_default();
|
|
let (seg_start_w, seg_end_w) = v.map_or((0.0, 0.0), |v| {
|
|
polyline2d_vertex_segment_widths(v, self.start_width, self.end_width)
|
|
});
|
|
let vertex_label = if n == 0 {
|
|
"—".to_string()
|
|
} else {
|
|
format!("{} / {}", vi + 1, n)
|
|
};
|
|
|
|
let global_width_prop = match polyline2d_global_width(self) {
|
|
Some(gw) => edit(t!("Global width").as_ref(), "pl2_start_w", gw),
|
|
None => Property {
|
|
label: t!("Global width").into_owned(),
|
|
field: "pl2_start_w",
|
|
value: PropValue::EditText(VARIES_LABEL.to_string()),
|
|
},
|
|
};
|
|
|
|
vec![
|
|
PropSection {
|
|
title: t!("Geometry").into_owned(),
|
|
props: vec![
|
|
stepper(t!("Current Vertex").as_ref(), "pl2_current_vertex", vertex_label),
|
|
edit(t!("Vertex X").as_ref(), "pl2_vertex_x", vertex_x),
|
|
edit(t!("Vertex Y").as_ref(), "pl2_vertex_y", vertex_y),
|
|
edit(t!("Start segment width").as_ref(), "pl2_seg_start_w", seg_start_w),
|
|
edit(t!("End segment width").as_ref(), "pl2_seg_end_w", seg_end_w),
|
|
global_width_prop,
|
|
edit(t!("Elevation").as_ref(), "pl2_elevation", self.elevation),
|
|
ro(t!("Area").as_ref(), "pl2_area", format_area(area)),
|
|
ro(t!("Length").as_ref(), "pl2_length", format_length(length)),
|
|
],
|
|
},
|
|
PropSection {
|
|
title: t!("Misc").into_owned(),
|
|
props: vec![
|
|
Property {
|
|
label: t!("Closed").into_owned(),
|
|
field: "pl2_closed",
|
|
value: PropValue::BoolToggle {
|
|
field: "pl2_closed",
|
|
value: self.is_closed(),
|
|
},
|
|
},
|
|
Property {
|
|
label: t!("Linetype generation").into_owned(),
|
|
field: "pl2_ltype_gen",
|
|
value: PropValue::BoolToggle {
|
|
field: "pl2_ltype_gen",
|
|
value: self.flags.bits() & 128 != 0,
|
|
},
|
|
},
|
|
],
|
|
},
|
|
]
|
|
}
|
|
|
|
fn apply_geom_prop(&mut self, field: &str, value: &str) {
|
|
// Per-vertex edits target the vertex the panel is focused on.
|
|
let n = self.vertices.len();
|
|
let vi = if n == 0 {
|
|
0
|
|
} else {
|
|
crate::scene::view::dispatch::prop_current_vertex().min(n - 1)
|
|
};
|
|
match field {
|
|
"pl2_closed" => {
|
|
let closed = if value == "toggle" {
|
|
!self.is_closed()
|
|
} else {
|
|
value == "true"
|
|
};
|
|
if closed {
|
|
self.close();
|
|
} else {
|
|
self.flags.set_closed(false);
|
|
}
|
|
}
|
|
"pl2_elevation" => {
|
|
if let Some(v) = parse_f64(value) {
|
|
self.elevation = v;
|
|
}
|
|
}
|
|
"pl2_start_w" => {
|
|
if let Some(v) = parse_f64(value) {
|
|
if v.is_finite() && v >= 0.0 {
|
|
self.start_width = v;
|
|
self.end_width = v;
|
|
for vtx in &mut self.vertices {
|
|
vtx.start_width = v;
|
|
vtx.end_width = v;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
"pl2_vertex_x" => {
|
|
if let (Some(v), Some(vert)) = (parse_f64(value), self.vertices.get_mut(vi)) {
|
|
vert.location.x = v;
|
|
}
|
|
}
|
|
"pl2_vertex_y" => {
|
|
if let (Some(v), Some(vert)) = (parse_f64(value), self.vertices.get_mut(vi)) {
|
|
vert.location.y = v;
|
|
}
|
|
}
|
|
"pl2_seg_start_w" => {
|
|
if let Some(v) = parse_f64(value) {
|
|
if v.is_finite() && v >= 0.0 {
|
|
if self.start_width > 1e-9 || self.end_width > 1e-9 {
|
|
let def_s = self.start_width;
|
|
let def_e = self.end_width;
|
|
for vtx in &mut self.vertices {
|
|
if vtx.start_width <= 1e-9 && vtx.end_width <= 1e-9 {
|
|
vtx.start_width = def_s;
|
|
vtx.end_width = def_e;
|
|
}
|
|
}
|
|
self.start_width = 0.0;
|
|
self.end_width = 0.0;
|
|
}
|
|
if let Some(vert) = self.vertices.get_mut(vi) {
|
|
vert.start_width = v;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
"pl2_seg_end_w" => {
|
|
if let Some(v) = parse_f64(value) {
|
|
if v.is_finite() && v >= 0.0 {
|
|
if self.start_width > 1e-9 || self.end_width > 1e-9 {
|
|
let def_s = self.start_width;
|
|
let def_e = self.end_width;
|
|
for vtx in &mut self.vertices {
|
|
if vtx.start_width <= 1e-9 && vtx.end_width <= 1e-9 {
|
|
vtx.start_width = def_s;
|
|
vtx.end_width = def_e;
|
|
}
|
|
}
|
|
self.start_width = 0.0;
|
|
self.end_width = 0.0;
|
|
}
|
|
if let Some(vert) = self.vertices.get_mut(vi) {
|
|
vert.end_width = v;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
"pl2_ltype_gen" => {
|
|
let on = if value == "toggle" {
|
|
self.flags.bits() & 128 == 0
|
|
} else {
|
|
value == "true"
|
|
};
|
|
let bits = if on {
|
|
self.flags.bits() | 128
|
|
} else {
|
|
self.flags.bits() & !128
|
|
};
|
|
self.flags = codec::entities::polyline::PolylineFlags::from_bits(bits);
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Transformable for Polyline2D {
|
|
fn apply_transform(&mut self, t: &EntityTransform) {
|
|
crate::scene::view::transform::apply_standard_entity_transform(self, t, |entity, p1, p2| {
|
|
for v in &mut entity.vertices {
|
|
crate::scene::view::transform::reflect_xy_point(
|
|
&mut v.location.x,
|
|
&mut v.location.y,
|
|
p1,
|
|
p2,
|
|
);
|
|
// Bulge encodes which side the arc bows to; a reflection
|
|
// reverses it or every curved segment flips to the wrong side.
|
|
v.bulge = -v.bulge;
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// ── Polyline3D ────────────────────────────────────────────────────────────────
|
|
|
|
const POLY3D_SPLINE_POINT: i32 = 8;
|
|
const POLY3D_SPLINE_CONTROL: i32 = 16;
|
|
const POLY3D_VERTEX: i32 = 32;
|
|
|
|
fn polyline3d_control_indices(pl: &Polyline3D) -> Vec<usize> {
|
|
if pl
|
|
.vertices
|
|
.iter()
|
|
.any(|vertex| vertex.flags & POLY3D_SPLINE_CONTROL != 0)
|
|
{
|
|
return pl
|
|
.vertices
|
|
.iter()
|
|
.enumerate()
|
|
.filter_map(|(index, vertex)| {
|
|
(vertex.flags & POLY3D_SPLINE_CONTROL != 0).then_some(index)
|
|
})
|
|
.collect();
|
|
}
|
|
|
|
let controls: Vec<usize> = pl
|
|
.vertices
|
|
.iter()
|
|
.enumerate()
|
|
.filter_map(|(index, vertex)| {
|
|
(vertex.flags & POLY3D_SPLINE_POINT == 0).then_some(index)
|
|
})
|
|
.collect();
|
|
if controls.is_empty() {
|
|
(0..pl.vertices.len()).collect()
|
|
} else {
|
|
controls
|
|
}
|
|
}
|
|
|
|
pub(crate) fn polyline3d_control_vertex_count(pl: &Polyline3D) -> usize {
|
|
polyline3d_control_indices(pl).len()
|
|
}
|
|
|
|
pub(crate) fn polyline3d_controls(pl: &Polyline3D) -> Vec<codec::entities::Vertex3DPolyline> {
|
|
polyline3d_control_indices(pl)
|
|
.into_iter()
|
|
.filter_map(|index| pl.vertices.get(index).cloned())
|
|
.collect()
|
|
}
|
|
|
|
fn polyline3d_wire_points(pl: &Polyline3D) -> Vec<[f64; 3]> {
|
|
let curve: Vec<[f64; 3]> = pl
|
|
.vertices
|
|
.iter()
|
|
.filter(|vertex| vertex.flags & POLY3D_SPLINE_POINT != 0)
|
|
.map(|vertex| [vertex.position.x, vertex.position.y, vertex.position.z])
|
|
.collect();
|
|
if !curve.is_empty() {
|
|
curve
|
|
} else {
|
|
polyline3d_controls(pl)
|
|
.iter()
|
|
.map(|vertex| [vertex.position.x, vertex.position.y, vertex.position.z])
|
|
.collect()
|
|
}
|
|
}
|
|
|
|
fn polyline3d_length(pl: &Polyline3D) -> f64 {
|
|
let points = polyline3d_wire_points(pl);
|
|
let mut length = points
|
|
.windows(2)
|
|
.map(|pair| Vec3::from(pair[0]).distance(Vec3::from(pair[1])))
|
|
.sum::<f64>();
|
|
if pl.is_closed() && points.len() >= 2 {
|
|
length += Vec3::from(points[0])
|
|
.distance(Vec3::from(points[points.len() - 1]));
|
|
}
|
|
length
|
|
}
|
|
|
|
fn polyline3d_spline_samples(
|
|
controls: &[[f64; 3]],
|
|
degree: usize,
|
|
closed: bool,
|
|
) -> Option<Vec<[f64; 3]>> {
|
|
if !controls.iter().flatten().all(|value| value.is_finite()) {
|
|
return None;
|
|
}
|
|
let curve = if closed {
|
|
let mut periodic_controls = controls.to_vec();
|
|
periodic_controls.extend(controls.iter().take(degree).copied());
|
|
let knots = (0..periodic_controls.len() + degree + 1)
|
|
.map(|index| index as f64)
|
|
.collect();
|
|
NurbsCurve3::new(degree, periodic_controls, knots, None)?.with_periodicity(true)
|
|
} else {
|
|
NurbsCurve3::new(degree, controls.to_vec(), Vec::new(), None)?
|
|
};
|
|
let mut samples = curve.tessellate_angle(kernel::tessellation::DEFAULT_ANGLE);
|
|
if closed
|
|
&& samples.len() >= 2
|
|
&& Vec3::from(samples[0]).distance(Vec3::from(samples[samples.len() - 1])) <= 1.0e-9
|
|
{
|
|
samples.pop();
|
|
}
|
|
Some(samples)
|
|
}
|
|
|
|
fn rebuild_polyline3d_fit(pl: &mut Polyline3D) -> bool {
|
|
use codec::entities::polyline3d::SmoothSurfaceType as SST;
|
|
|
|
let mut controls = polyline3d_controls(pl);
|
|
if pl.smooth_type == SST::None {
|
|
for vertex in &mut controls {
|
|
vertex.flags =
|
|
(vertex.flags & !(POLY3D_SPLINE_POINT | POLY3D_SPLINE_CONTROL)) | POLY3D_VERTEX;
|
|
}
|
|
pl.vertices = controls;
|
|
pl.flags.spline_fit = false;
|
|
return true;
|
|
}
|
|
|
|
if pl.smooth_type == SST::Bezier {
|
|
pl.smooth_type = SST::CubicBSpline;
|
|
}
|
|
let degree = match pl.smooth_type {
|
|
SST::QuadraticBSpline => 2,
|
|
SST::CubicBSpline => 3,
|
|
SST::Bezier => unreachable!(),
|
|
SST::None => unreachable!(),
|
|
};
|
|
if controls.len() <= degree {
|
|
return false;
|
|
}
|
|
let points: Vec<[f64; 3]> = controls
|
|
.iter()
|
|
.map(|vertex| [vertex.position.x, vertex.position.y, vertex.position.z])
|
|
.collect();
|
|
let Some(samples) = polyline3d_spline_samples(&points, degree, pl.is_closed()) else {
|
|
return false;
|
|
};
|
|
|
|
for vertex in &mut controls {
|
|
vertex.flags = (vertex.flags & !(POLY3D_SPLINE_POINT | POLY3D_SPLINE_CONTROL))
|
|
| POLY3D_VERTEX
|
|
| POLY3D_SPLINE_CONTROL;
|
|
}
|
|
let vertex_layer = controls
|
|
.first()
|
|
.map(|vertex| vertex.layer.clone())
|
|
.unwrap_or_else(|| "0".to_string());
|
|
let curve_vertices = samples.into_iter().map(|point| {
|
|
let mut vertex = codec::entities::Vertex3DPolyline::from_xyz(
|
|
point[0], point[1], point[2],
|
|
);
|
|
vertex.layer = vertex_layer.clone();
|
|
vertex.flags = POLY3D_VERTEX | POLY3D_SPLINE_POINT;
|
|
vertex
|
|
});
|
|
controls.extend(curve_vertices);
|
|
pl.vertices = controls;
|
|
pl.flags.spline_fit = true;
|
|
true
|
|
}
|
|
|
|
fn tessellate_polyline3d(pl: &Polyline3D) -> RenderEntity {
|
|
let to_pt = |v: &codec::entities::Vertex3DPolyline| -> [f64; 3] {
|
|
[v.position.x, v.position.y, v.position.z]
|
|
};
|
|
|
|
// Spline samples draw the wire; frame vertices remain snap points.
|
|
let spline_curve: Vec<_> = pl
|
|
.vertices
|
|
.iter()
|
|
.filter(|v| v.flags & POLY3D_SPLINE_POINT != 0)
|
|
.collect();
|
|
let ctrl_pts: Vec<_> = pl
|
|
.vertices
|
|
.iter()
|
|
.filter(|v| v.flags & POLY3D_SPLINE_CONTROL != 0)
|
|
.collect();
|
|
|
|
let (wire_pts, key_verts) = if !spline_curve.is_empty() {
|
|
let wire: Vec<[f64; 3]> = spline_curve.iter().map(|v| to_pt(v)).collect();
|
|
let ctrl: Vec<[f64; 3]> = if ctrl_pts.is_empty() {
|
|
polyline3d_controls(pl).iter().map(to_pt).collect()
|
|
} else {
|
|
ctrl_pts.iter().map(|v| to_pt(v)).collect()
|
|
};
|
|
(wire, ctrl)
|
|
} else {
|
|
let pts: Vec<[f64; 3]> = pl.vertices.iter().map(to_pt).collect();
|
|
(pts.clone(), pts)
|
|
};
|
|
|
|
let mut points = wire_pts.clone();
|
|
if pl.is_closed() && wire_pts.len() >= 2 {
|
|
points.push(wire_pts[0]);
|
|
}
|
|
|
|
RenderEntity {
|
|
pick_tris: Vec::new(),
|
|
object: RenderObject::Lines(points),
|
|
snap_pts: vec![],
|
|
tangent_geoms: vec![],
|
|
key_vertices: key_verts,
|
|
fill_tris: vec![],
|
|
}
|
|
}
|
|
|
|
impl RenderConvertible for Polyline3D {
|
|
fn to_render(&self, _document: &codec::CadDocument) -> Option<RenderEntity> {
|
|
Some(tessellate_polyline3d(self))
|
|
}
|
|
}
|
|
|
|
impl Grippable for Polyline3D {
|
|
fn grips(&self) -> Vec<GripDef> {
|
|
polyline3d_control_indices(self)
|
|
.into_iter()
|
|
.filter_map(|index| self.vertices.get(index))
|
|
.enumerate()
|
|
.map(|(i, v)| {
|
|
square_grip(
|
|
i,
|
|
glam::DVec3::new(v.position.x, v.position.y, v.position.z),
|
|
)
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
fn apply_grip(&mut self, grip_id: usize, apply: GripApply) {
|
|
let raw_index = polyline3d_control_indices(self).get(grip_id).copied();
|
|
if let Some(v) = raw_index.and_then(|index| self.vertices.get_mut(index)) {
|
|
match apply {
|
|
GripApply::Translate(d) => {
|
|
v.position.x += d.x as f64;
|
|
v.position.y += d.y as f64;
|
|
v.position.z += d.z as f64;
|
|
}
|
|
GripApply::Absolute(p) => {
|
|
v.position.x = p.x as f64;
|
|
v.position.y = p.y as f64;
|
|
v.position.z = p.z as f64;
|
|
}
|
|
}
|
|
if self.smooth_type
|
|
!= codec::entities::polyline3d::SmoothSurfaceType::None
|
|
{
|
|
let _ = rebuild_polyline3d_fit(self);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn grip_menu(&self, _grip_id: usize) -> Vec<crate::scene::model::object::GripMenuItem> {
|
|
use crate::scene::model::object::{GripMenuAction, GripMenuItem};
|
|
use codec::entities::polyline3d::SmoothSurfaceType as SST;
|
|
|
|
let mut items = vec![
|
|
GripMenuItem {
|
|
label: "Stretch",
|
|
action: GripMenuAction::Stretch,
|
|
},
|
|
GripMenuItem {
|
|
label: "Add Vertex",
|
|
action: GripMenuAction::AddVertex,
|
|
},
|
|
];
|
|
let minimum = match self.smooth_type {
|
|
SST::None if self.is_closed() => 3,
|
|
SST::None => 2,
|
|
SST::QuadraticBSpline => 3,
|
|
SST::CubicBSpline | SST::Bezier => 4,
|
|
};
|
|
if polyline3d_control_vertex_count(self) > minimum {
|
|
items.push(GripMenuItem {
|
|
label: "Remove Vertex",
|
|
action: GripMenuAction::RemoveVertex,
|
|
});
|
|
}
|
|
items
|
|
}
|
|
|
|
fn apply_grip_menu(&mut self, grip_id: usize, action: crate::scene::model::object::GripMenuAction) {
|
|
use crate::scene::model::object::GripMenuAction as A;
|
|
use codec::entities::polyline3d::SmoothSurfaceType as SST;
|
|
|
|
let mut controls = polyline3d_controls(self);
|
|
let n = controls.len();
|
|
match action {
|
|
A::AddVertex if grip_id < n => {
|
|
if grip_id == n - 1 && !self.is_closed() {
|
|
let mut new_v = controls[grip_id].clone();
|
|
if n >= 2 {
|
|
let previous = &controls[n - 2].position;
|
|
let last = &controls[n - 1].position;
|
|
let next = Vec3::new(previous.x, previous.y, previous.z).lerp(
|
|
Vec3::new(last.x, last.y, last.z),
|
|
2.0,
|
|
);
|
|
new_v.position.x = next.x;
|
|
new_v.position.y = next.y;
|
|
new_v.position.z = next.z;
|
|
}
|
|
new_v.handle = codec::Handle::NULL;
|
|
controls.push(new_v);
|
|
} else {
|
|
let i1 = (grip_id + 1) % n;
|
|
let v0 = &controls[grip_id];
|
|
let v1 = &controls[i1];
|
|
let midpoint = Vec3::new(v0.position.x, v0.position.y, v0.position.z).lerp(
|
|
Vec3::new(v1.position.x, v1.position.y, v1.position.z),
|
|
0.5,
|
|
);
|
|
let mut new_v = v0.clone();
|
|
new_v.position.x = midpoint.x;
|
|
new_v.position.y = midpoint.y;
|
|
new_v.position.z = midpoint.z;
|
|
new_v.handle = codec::Handle::NULL;
|
|
controls.insert(grip_id + 1, new_v);
|
|
}
|
|
self.vertices = controls;
|
|
let _ = rebuild_polyline3d_fit(self);
|
|
}
|
|
A::RemoveVertex if grip_id < n => {
|
|
let minimum = match self.smooth_type {
|
|
SST::None if self.is_closed() => 3,
|
|
SST::None => 2,
|
|
SST::QuadraticBSpline => 3,
|
|
SST::CubicBSpline | SST::Bezier => 4,
|
|
};
|
|
if n > minimum {
|
|
controls.remove(grip_id);
|
|
self.vertices = controls;
|
|
let _ = rebuild_polyline3d_fit(self);
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl PropertyEditable for Polyline3D {
|
|
fn geometry_properties(&self, _text_style_names: &[String]) -> Vec<PropSection> {
|
|
use codec::entities::polyline3d::SmoothSurfaceType as SST;
|
|
let control_indices = polyline3d_control_indices(self);
|
|
let n = control_indices.len();
|
|
let vi = if n == 0 {
|
|
0
|
|
} else {
|
|
crate::scene::view::dispatch::prop_current_vertex().min(n - 1)
|
|
};
|
|
let vertex = control_indices
|
|
.get(vi)
|
|
.and_then(|index| self.vertices.get(*index));
|
|
let vertex_x = vertex.map(|v| v.position.x).unwrap_or_default();
|
|
let vertex_y = vertex.map(|v| v.position.y).unwrap_or_default();
|
|
let vertex_z = vertex.map(|v| v.position.z).unwrap_or_default();
|
|
let vertex_label = if n == 0 {
|
|
"—".to_string()
|
|
} else {
|
|
format!("{} / {}", vi + 1, n)
|
|
};
|
|
let fit_smooth = match self.smooth_type {
|
|
SST::None => "None",
|
|
SST::QuadraticBSpline => "Quadratic",
|
|
SST::CubicBSpline | SST::Bezier => "Cubic",
|
|
};
|
|
let mut smooth_options = vec!["None".to_string()];
|
|
if n >= 3 {
|
|
smooth_options.push("Quadratic".to_string());
|
|
}
|
|
if n >= 4 {
|
|
smooth_options.push("Cubic".to_string());
|
|
}
|
|
|
|
vec![
|
|
PropSection {
|
|
title: t!("Geometry").into_owned(),
|
|
props: vec![
|
|
stepper(
|
|
t!("Current Vertex").as_ref(),
|
|
"pl3_current_vertex",
|
|
vertex_label,
|
|
),
|
|
edit(t!("Vertex X").as_ref(), "pl3_vertex_x", vertex_x),
|
|
edit(t!("Vertex Y").as_ref(), "pl3_vertex_y", vertex_y),
|
|
edit(t!("Vertex Z").as_ref(), "pl3_vertex_z", vertex_z),
|
|
ro(
|
|
t!("Length").as_ref(),
|
|
"pl3_length",
|
|
format_length(polyline3d_length(self)),
|
|
),
|
|
],
|
|
},
|
|
PropSection {
|
|
title: t!("Misc").into_owned(),
|
|
props: vec![
|
|
Property {
|
|
label: t!("Fit/Smooth").into_owned(),
|
|
field: "pl3_smooth",
|
|
value: PropValue::Choice {
|
|
selected: fit_smooth.to_string(),
|
|
options: smooth_options,
|
|
},
|
|
},
|
|
Property {
|
|
label: t!("Closed").into_owned(),
|
|
field: "pl3_closed",
|
|
value: if n >= 3 {
|
|
PropValue::BoolToggle {
|
|
field: "pl3_closed",
|
|
value: self.is_closed(),
|
|
}
|
|
} else {
|
|
PropValue::ReadOnly("No".to_string())
|
|
},
|
|
},
|
|
],
|
|
},
|
|
]
|
|
}
|
|
|
|
fn apply_geom_prop(&mut self, field: &str, value: &str) {
|
|
match field {
|
|
"pl3_closed" => {
|
|
let closed = if value == "toggle" {
|
|
!self.is_closed()
|
|
} else {
|
|
value == "true"
|
|
};
|
|
if closed && polyline3d_control_vertex_count(self) < 3 {
|
|
return;
|
|
}
|
|
if closed {
|
|
self.close();
|
|
} else {
|
|
self.open();
|
|
}
|
|
if self.smooth_type
|
|
!= codec::entities::polyline3d::SmoothSurfaceType::None
|
|
{
|
|
let _ = rebuild_polyline3d_fit(self);
|
|
}
|
|
}
|
|
"pl3_smooth" => {
|
|
use codec::entities::polyline3d::SmoothSurfaceType as SST;
|
|
let next = match value {
|
|
"None" => SST::None,
|
|
"Quadratic" => SST::QuadraticBSpline,
|
|
"Cubic" => SST::CubicBSpline,
|
|
_ => return,
|
|
};
|
|
let previous = self.smooth_type;
|
|
self.smooth_type = next;
|
|
if !rebuild_polyline3d_fit(self) {
|
|
self.smooth_type = previous;
|
|
}
|
|
}
|
|
"pl3_vertex_x" => {
|
|
let vi = crate::scene::view::dispatch::prop_current_vertex();
|
|
let raw_index = polyline3d_control_indices(self).get(vi).copied();
|
|
if let (Some(v), Some(vert)) = (
|
|
parse_f64(value).filter(|value| value.is_finite()),
|
|
raw_index.and_then(|index| self.vertices.get_mut(index)),
|
|
) {
|
|
vert.position.x = v;
|
|
let _ = rebuild_polyline3d_fit(self);
|
|
}
|
|
}
|
|
"pl3_vertex_y" => {
|
|
let vi = crate::scene::view::dispatch::prop_current_vertex();
|
|
let raw_index = polyline3d_control_indices(self).get(vi).copied();
|
|
if let (Some(v), Some(vert)) = (
|
|
parse_f64(value).filter(|value| value.is_finite()),
|
|
raw_index.and_then(|index| self.vertices.get_mut(index)),
|
|
) {
|
|
vert.position.y = v;
|
|
let _ = rebuild_polyline3d_fit(self);
|
|
}
|
|
}
|
|
"pl3_vertex_z" => {
|
|
let vi = crate::scene::view::dispatch::prop_current_vertex();
|
|
let raw_index = polyline3d_control_indices(self).get(vi).copied();
|
|
if let (Some(v), Some(vert)) = (
|
|
parse_f64(value).filter(|value| value.is_finite()),
|
|
raw_index.and_then(|index| self.vertices.get_mut(index)),
|
|
) {
|
|
vert.position.z = v;
|
|
let _ = rebuild_polyline3d_fit(self);
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Transformable for Polyline3D {
|
|
fn apply_transform(&mut self, t: &EntityTransform) {
|
|
crate::scene::view::transform::apply_standard_entity_transform(self, t, |entity, p1, p2| {
|
|
for v in &mut entity.vertices {
|
|
crate::scene::view::transform::reflect_xy_point(
|
|
&mut v.position.x,
|
|
&mut v.position.y,
|
|
p1,
|
|
p2,
|
|
);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
/// Generate solid-fill boundary polygons for each wide segment of a Polyline2D.
|
|
/// Solid-fill bands for a wide Polyline2D, plus the `world_origin` they are
|
|
/// relative to (the first vertex). See `lwpolyline::wide_fills` — offsets are
|
|
/// f32 from `origin` so the band stays precise at UTM-scale coordinates.
|
|
pub(crate) fn wide_fills(pl: &codec::entities::Polyline2D) -> ([f64; 2], Vec<Vec<[f32; 2]>>) {
|
|
// The stored widths are the band's FULL width and `polyline_segment_fill`
|
|
// offsets ±hw about the centreline — halve them. See `lwpolyline::wide_fills`.
|
|
let hw_default_start = pl.start_width as f32 * 0.5;
|
|
let hw_default_end = pl.end_width as f32 * 0.5;
|
|
let filtered = drawn_vertices2d(pl);
|
|
let verts: &[codec::entities::Vertex2D] = filtered.as_deref().unwrap_or(&pl.vertices);
|
|
let n = verts.len();
|
|
if n < 2 {
|
|
return ([0.0; 2], vec![]);
|
|
}
|
|
let origin = [verts[0].location.x, verts[0].location.y];
|
|
let seg_count = if pl.is_closed() { n } else { n - 1 };
|
|
let mut out = Vec::new();
|
|
for i in 0..seg_count {
|
|
let v0 = &verts[i];
|
|
let v1 = &verts[(i + 1) % n];
|
|
let hw0 = if v0.start_width > 1e-9 {
|
|
v0.start_width as f32 * 0.5
|
|
} else {
|
|
hw_default_start
|
|
};
|
|
let hw1 = if v0.end_width > 1e-9 {
|
|
v0.end_width as f32 * 0.5
|
|
} else {
|
|
hw_default_end
|
|
};
|
|
if hw0 < 1e-6 && hw1 < 1e-6 {
|
|
continue;
|
|
}
|
|
let p0 = [
|
|
(v0.location.x - origin[0]) as f32,
|
|
(v0.location.y - origin[1]) as f32,
|
|
];
|
|
let p1 = [
|
|
(v1.location.x - origin[0]) as f32,
|
|
(v1.location.y - origin[1]) as f32,
|
|
];
|
|
if let Some(poly) =
|
|
crate::entities::common::polyline_segment_fill(p0, p1, hw0, hw1, v0.bulge as f32)
|
|
{
|
|
out.push(poly);
|
|
}
|
|
}
|
|
(origin, out)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::entities::traits::PropertyEditable;
|
|
use codec::entities::{Polyline2D, Vertex2D};
|
|
use codec::Vector3;
|
|
|
|
fn make_test_polyline2d(count: usize, start_w: f64, end_w: f64) -> Polyline2D {
|
|
let mut pl = Polyline2D::default();
|
|
pl.start_width = start_w;
|
|
pl.end_width = end_w;
|
|
for i in 0..count {
|
|
pl.vertices.push(Vertex2D::new(Vector3::new(i as f64 * 10.0, 0.0, 0.0)));
|
|
}
|
|
pl
|
|
}
|
|
|
|
#[test]
|
|
fn test_polyline2d_uniform_width_shows_global_width() {
|
|
let pl = make_test_polyline2d(3, 4.0, 4.0);
|
|
assert_eq!(polyline2d_global_width(&pl), Some(4.0));
|
|
|
|
let props = pl.geometry_properties(&[]);
|
|
let geom_props = &props[0].props;
|
|
let gw = geom_props.iter().find(|p| p.field == "pl2_start_w").unwrap();
|
|
match &gw.value {
|
|
PropValue::EditText(val) => assert_eq!(parse_f64(val), Some(4.0)),
|
|
_ => panic!("expected EditText"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_polyline2d_varying_vertex_width_shows_varies() {
|
|
let mut pl = make_test_polyline2d(3, 4.0, 4.0);
|
|
pl.vertices[0].start_width = 1.0;
|
|
pl.vertices[0].end_width = 2.0;
|
|
|
|
assert_eq!(polyline2d_global_width(&pl), None);
|
|
|
|
let props = pl.geometry_properties(&[]);
|
|
let geom_props = &props[0].props;
|
|
let gw = geom_props.iter().find(|p| p.field == "pl2_start_w").unwrap();
|
|
match &gw.value {
|
|
PropValue::EditText(val) => assert_eq!(val, VARIES_LABEL),
|
|
_ => panic!("expected EditText with VARIES_LABEL"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_polyline2d_modifying_global_width_sets_all_vertex_widths() {
|
|
let mut pl = make_test_polyline2d(3, 4.0, 4.0);
|
|
pl.vertices[0].start_width = 1.0;
|
|
pl.vertices[0].end_width = 2.0;
|
|
|
|
pl.apply_geom_prop("pl2_start_w", "8.0");
|
|
|
|
assert_eq!(pl.start_width, 8.0);
|
|
assert_eq!(pl.end_width, 8.0);
|
|
for v in &pl.vertices {
|
|
assert_eq!(v.start_width, 8.0);
|
|
assert_eq!(v.end_width, 8.0);
|
|
}
|
|
assert_eq!(polyline2d_global_width(&pl), Some(8.0));
|
|
}
|
|
|
|
#[test]
|
|
fn test_polyline2d_modifying_segment_width_materializes_default_width() {
|
|
let mut pl = make_test_polyline2d(3, 4.0, 4.0);
|
|
assert_eq!(pl.vertices[0].start_width, 0.0);
|
|
|
|
pl.apply_geom_prop("pl2_seg_start_w", "1.5");
|
|
|
|
assert_eq!(pl.vertices[0].start_width, 1.5);
|
|
assert_eq!(pl.vertices[0].end_width, 4.0);
|
|
assert_eq!(pl.vertices[1].start_width, 4.0);
|
|
assert_eq!(pl.vertices[1].end_width, 4.0);
|
|
|
|
assert_eq!(polyline2d_global_width(&pl), None);
|
|
}
|
|
}
|