use super::entity::{set_wall, ArchKind, StraightWallGeometry};
use super::geometry::{point_is_finite, positive};
use crate::entities::{EntityType, ExtendedEntityData};
use crate::types::{Handle, Vector3};
use crate::CadDocument;
const ENDPOINT_TOLERANCE: f64 = 1e-6;
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ContinuousWall {
pub handles: Vec<Handle>,
pub points: Vec<Vector3>,
pub width: f64,
pub height: f64,
}
pub fn continuous_walls(document: &CadDocument) -> Vec<ContinuousWall> {
let mut segments = Vec::new();
for entity in document.entities() {
let EntityType::Extended(entity) = entity else {
continue;
};
let ExtendedEntityData::Arch(data) = &entity.data else {
continue;
};
if data.kind != ArchKind::Wall {
continue;
}
let Some(geometry) = data.straight_wall_geometry() else {
continue;
};
segments.push((entity.common.handle, geometry));
}
let mut unused = vec![true; segments.len()];
let mut chains = Vec::new();
for index in 0..segments.len() {
if !unused[index] {
continue;
}
unused[index] = false;
let mut handles = vec![segments[index].0];
let mut points = vec![segments[index].1.start, segments[index].1.end];
let width = segments[index].1.width;
let height = segments[index].1.height;
loop {
let mut extended = false;
for (candidate, free) in segments.iter().zip(unused.iter_mut()) {
if !*free {
continue;
}
if !super::metrics::near(candidate.1.width, width)
|| !super::metrics::near(candidate.1.height, height)
{
continue;
}
if let Some(joined) = join(&mut points, &candidate.1) {
*free = false;
if joined {
handles.push(candidate.0);
} else {
handles.insert(0, candidate.0);
}
extended = true;
break;
}
}
if !extended {
break;
}
}
chains.push(ContinuousWall {
handles,
points,
width,
height,
});
}
chains
}
pub fn set_continuous_wall(
document: &mut CadDocument,
chain: &ContinuousWall,
points: &[Vector3],
width: f64,
height: f64,
) -> Result<(), super::entity::ArchEditError> {
if !positive(width) || !positive(height) {
return Err(super::entity::ArchEditError::InvalidValue);
}
if points.len() != chain.handles.len() + 1
|| points.len() < 2
|| points
.iter()
.any(|point| !point_is_finite(*point) || point.z.abs() > 1e-12)
|| points
.windows(2)
.any(|pair| !positive((pair[1] - pair[0]).length()))
{
return Err(super::entity::ArchEditError::InvalidValue);
}
let mut changed = Vec::with_capacity(chain.handles.len());
for (index, handle) in chain.handles.iter().enumerate() {
if chain.handles[..index].contains(handle) {
return Err(super::entity::ArchEditError::Rejected);
}
let Some(entity) = document.get_entity(*handle) else {
return Err(super::entity::ArchEditError::UnsupportedLayout);
};
let mut entity = entity.clone();
let EntityType::Extended(extended) = &mut entity else {
return Err(super::entity::ArchEditError::UnsupportedLayout);
};
let ExtendedEntityData::Arch(data) = &mut extended.data else {
return Err(super::entity::ArchEditError::UnsupportedLayout);
};
set_wall(
data,
&mut extended.common.graphic_data,
points[index],
points[index + 1],
width,
height,
)?;
changed.push((*handle, entity));
}
for (handle, entity) in changed {
*document
.get_entity_mut(handle)
.expect("preflight checked every handle") = entity;
}
Ok(())
}
pub fn continuous_wall_outline(points: &[Vector3], width: f64) -> Option<Vec<Vector3>> {
if points.len() < 2
|| !positive(width)
|| points
.iter()
.any(|point| !point_is_finite(*point) || point.z.abs() > 1e-12)
{
return None;
}
let half = width * 0.5;
let mut left = Vec::with_capacity(points.len());
let mut right = Vec::with_capacity(points.len());
for index in 0..points.len() {
let (left_point, right_point) = offset_vertex(points, index, half)?;
left.push(left_point);
right.push(right_point);
}
let mut outline = left;
outline.extend(right.into_iter().rev());
outline.push(outline[0]);
outline
.iter()
.all(|point| point_is_finite(*point))
.then_some(outline)
}
fn join(points: &mut Vec<Vector3>, segment: &StraightWallGeometry) -> Option<bool> {
let end = *points.last()?;
let start = *points.first()?;
if near(end, segment.start) {
points.push(segment.end);
Some(true)
} else if near(end, segment.end) {
points.push(segment.start);
Some(true)
} else if near(start, segment.end) {
points.insert(0, segment.start);
Some(false)
} else if near(start, segment.start) {
points.insert(0, segment.end);
Some(false)
} else {
None
}
}
fn near(left: Vector3, right: Vector3) -> bool {
(left - right).length() <= ENDPOINT_TOLERANCE
}
fn unit(from: Vector3, to: Vector3) -> Option<Vector3> {
let edge = to - from;
let length = edge.length();
positive(length).then_some(edge * (1.0 / length))
}
fn left_normal(direction: Vector3) -> Vector3 {
Vector3::new(-direction.y, direction.x, 0.0)
}
fn offset_vertex(points: &[Vector3], index: usize, half: f64) -> Option<(Vector3, Vector3)> {
if index == 0 {
let direction = unit(points[0], points[1])?;
let normal = left_normal(direction) * half;
return Some((points[0] + normal, points[0] - normal));
}
if index + 1 == points.len() {
let direction = unit(points[index - 1], points[index])?;
let normal = left_normal(direction) * half;
return Some((points[index] + normal, points[index] - normal));
}
let incoming = unit(points[index - 1], points[index])?;
let outgoing = unit(points[index], points[index + 1])?;
let point = points[index];
let left_in = left_normal(incoming) * half;
let left_out = left_normal(outgoing) * half;
let left = line_intersect(
point + left_in,
point + left_in + incoming,
point + left_out,
point + left_out + outgoing,
)
.unwrap_or(point + left_in);
let right = line_intersect(
point - left_in,
point - left_in + incoming,
point - left_out,
point - left_out + outgoing,
)
.unwrap_or(point - left_in);
Some((left, right))
}
fn line_intersect(
origin: Vector3,
through: Vector3,
other: Vector3,
other_through: Vector3,
) -> Option<Vector3> {
let first = through - origin;
let second = other_through - other;
let cross = first.x * second.y - first.y * second.x;
if cross.abs() < 1e-9 {
return None;
}
let delta = other - origin;
let parameter = (delta.x * second.y - delta.y * second.x) / cross;
Some(origin + first * parameter)
}