use crate::partition_element_records::PartitionElementRecord;
use std::collections::{BTreeMap, BTreeSet};
pub const WALL_BODY_SOURCE_FIELD: &str = "m_wall_body_source";
pub const WALL_BODY_JOIN_TRIMMED: &str = "partition_element_record_join_trimmed";
pub const WALL_THICKNESS_FIELD: &str = "m_wall_thickness";
pub const WALL_TRIM_START_FIELD: &str = "m_wall_trim_start";
pub const WALL_TRIM_END_FIELD: &str = "m_wall_trim_end";
pub const WALL_THICKNESS_SOURCE: &str = "partition_element_record_bbox_thin_axis";
pub const JOIN_EPS_FEET: f64 = 1e-6;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct WallRun {
pub element_id: u32,
pub axis: usize,
pub thickness_feet: f64,
pub centre_feet: f64,
pub start_feet: f64,
pub end_feet: f64,
pub base_feet: f64,
pub top_feet: f64,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct WallJoinTrim {
pub axis: usize,
pub thickness_feet: f64,
pub start_feet: f64,
pub end_feet: f64,
}
impl WallJoinTrim {
pub fn is_trimmed(&self) -> bool {
self.start_feet > 0.0 || self.end_feet > 0.0
}
}
pub fn wall_run(record: &PartitionElementRecord) -> Option<WallRun> {
let (dx, dy, _) = record.extents_feet();
if !dx.is_finite() || !dy.is_finite() {
return None;
}
if (dx - dy).abs() <= JOIN_EPS_FEET {
return None;
}
let axis = usize::from(dy > dx);
let across = 1 - axis;
let thickness_feet = if axis == 0 { dy } else { dx };
if thickness_feet <= 0.0 {
return None;
}
Some(WallRun {
element_id: record.element_id,
axis,
thickness_feet,
centre_feet: (record.bbox_feet[across] + record.bbox_feet[across + 3]) * 0.5,
start_feet: record.bbox_feet[axis],
end_feet: record.bbox_feet[axis + 3],
base_feet: record.bbox_feet[2],
top_feet: record.bbox_feet[5],
})
}
pub fn joined_walls(record: &PartitionElementRecord, wall_ids: &BTreeSet<u32>) -> BTreeSet<u32> {
record
.references
.iter()
.filter(|slot| **slot <= u64::from(u32::MAX))
.map(|slot| *slot as u32)
.filter(|id| *id != record.element_id && wall_ids.contains(id))
.collect()
}
struct Solver<'a> {
runs: &'a [WallRun],
joined: &'a BTreeMap<u32, BTreeSet<u32>>,
}
impl<'a> Solver<'a> {
fn names(&self, wall: &WallRun, other: &WallRun) -> bool {
self.joined
.get(&wall.element_id)
.is_some_and(|set| set.contains(&other.element_id))
}
fn candidates(&self, wall: &WallRun, coord: f64, skip_centre: Option<f64>) -> Vec<&'a WallRun> {
self.runs
.iter()
.filter(|other| other.element_id != wall.element_id && other.axis != wall.axis)
.filter(|other| (other.centre_feet - coord).abs() <= JOIN_EPS_FEET)
.filter(|other| self.names(wall, other))
.filter(|other| {
skip_centre.is_none_or(|line| (other.centre_feet - line).abs() > JOIN_EPS_FEET)
})
.filter(|other| {
wall.centre_feet >= other.start_feet - JOIN_EPS_FEET
&& wall.centre_feet <= other.end_feet + JOIN_EPS_FEET
})
.filter(|other| {
wall.top_feet.min(other.top_feet) - wall.base_feet.max(other.base_feet)
> JOIN_EPS_FEET
})
.collect()
}
fn half_thickness(candidates: &[&WallRun]) -> Option<f64> {
let mut thickness: Option<f64> = None;
for other in candidates {
match thickness {
None => thickness = Some(other.thickness_feet),
Some(held) if (held - other.thickness_feet).abs() <= JOIN_EPS_FEET => {}
Some(_) => return None,
}
}
Some(thickness.map_or(0.0, |value| value * 0.5))
}
fn reduced_run(&self, candidate: &WallRun, line: f64) -> (f64, f64) {
let start =
Self::half_thickness(&self.candidates(candidate, candidate.start_feet, Some(line)))
.unwrap_or(0.0);
let end = Self::half_thickness(&self.candidates(candidate, candidate.end_feet, Some(line)))
.unwrap_or(0.0);
(candidate.start_feet + start, candidate.end_feet - end)
}
fn trim_at(&self, wall: &WallRun, coord: f64) -> Option<f64> {
let reaching: Vec<&WallRun> = self
.candidates(wall, coord, None)
.into_iter()
.filter(|candidate| {
let (start, end) = self.reduced_run(candidate, wall.centre_feet);
wall.centre_feet >= start - JOIN_EPS_FEET && wall.centre_feet <= end + JOIN_EPS_FEET
})
.collect();
Self::half_thickness(&reaching)
}
}
pub fn join_trims(records: &[PartitionElementRecord]) -> BTreeMap<u32, WallJoinTrim> {
let wall_ids: BTreeSet<u32> = records.iter().map(|record| record.element_id).collect();
let joined: BTreeMap<u32, BTreeSet<u32>> = records
.iter()
.map(|record| (record.element_id, joined_walls(record, &wall_ids)))
.collect();
let runs: Vec<WallRun> = records.iter().filter_map(wall_run).collect();
let solver = Solver {
runs: &runs,
joined: &joined,
};
let mut out = BTreeMap::new();
for wall in &runs {
let (Some(start), Some(end)) = (
solver.trim_at(wall, wall.start_feet),
solver.trim_at(wall, wall.end_feet),
) else {
continue;
};
if wall.end_feet - end - (wall.start_feet + start) <= JOIN_EPS_FEET {
continue;
}
out.insert(
wall.element_id,
WallJoinTrim {
axis: wall.axis,
thickness_feet: wall.thickness_feet,
start_feet: start,
end_feet: end,
},
);
}
out
}
pub const PERPENDICULAR_EPS: f64 = 1e-6;
pub const ANGLED_JOIN_MAX_COSINE: f64 = std::f64::consts::FRAC_1_SQRT_2;
#[derive(Debug, Clone, PartialEq)]
pub struct WallLine {
pub element_id: u32,
pub start: [f64; 2],
pub end: [f64; 2],
pub thickness_feet: f64,
pub layers: Vec<(f64, u32)>,
pub exterior: [f64; 2],
pub base_feet: f64,
pub top_feet: f64,
}
#[derive(Debug, Clone, PartialEq)]
pub struct ButtJoin {
pub partner: u32,
pub runs_through: bool,
pub reach_feet: Option<f64>,
pub layer_reach_feet: Option<Vec<f64>>,
pub layer_edge_reach_feet: Option<Vec<[f64; 2]>>,
}
pub fn butt_joins(
walls: &[WallLine],
partners: &BTreeMap<u32, BTreeSet<u32>>,
) -> BTreeMap<u32, [Option<ButtJoin>; 2]> {
let length = |wall: &WallLine| (wall.end[0] - wall.start[0]).hypot(wall.end[1] - wall.start[1]);
let lines: Vec<&WallLine> = walls
.iter()
.filter(|wall| length(wall).is_finite() && length(wall) > 1e-9)
.collect();
let mut ends: Vec<([f64; 2], usize)> = lines
.iter()
.enumerate()
.flat_map(|(index, wall)| [(wall.start, index), (wall.end, index)])
.collect();
ends.sort_by(|a, b| a.0[0].total_cmp(&b.0[0]));
let names = |from: &WallLine, to: &WallLine| {
partners
.get(&from.element_id)
.is_some_and(|named| named.contains(&to.element_id))
};
let mut out = BTreeMap::new();
for (index, wall) in lines.iter().enumerate() {
let run = length(wall);
let along = [
(wall.end[0] - wall.start[0]) / run,
(wall.end[1] - wall.start[1]) / run,
];
let mut joins = [None, None];
for (slot, point) in [wall.start, wall.end].into_iter().enumerate() {
let from = ends.partition_point(|(p, _)| p[0] < point[0] - JOIN_EPS_FEET);
let mates: BTreeSet<usize> = ends[from..]
.iter()
.take_while(|(p, _)| p[0] <= point[0] + JOIN_EPS_FEET)
.filter(|(p, other)| *other != index && (p[1] - point[1]).abs() <= JOIN_EPS_FEET)
.map(|(_, other)| *other)
.filter(|other| {
let other = lines[*other];
wall.top_feet.min(other.top_feet) - wall.base_feet.max(other.base_feet)
> JOIN_EPS_FEET
})
.collect();
let into = if slot == 0 {
along
} else {
[-along[0], -along[1]]
};
if mates.is_empty() {
joins[slot] = tee_join(wall, &lines, point, into);
continue;
}
let [mate] = mates.into_iter().collect::<Vec<_>>()[..] else {
continue;
};
let other = lines[mate];
let other_run = length(other);
let cosine = (along[0] * (other.end[0] - other.start[0])
+ along[1] * (other.end[1] - other.start[1]))
/ other_run;
if cosine.abs() > PERPENDICULAR_EPS && cosine.abs() > ANGLED_JOIN_MAX_COSINE {
continue;
}
let runs_through = match (names(wall, other), names(other, wall)) {
(true, false) => true,
(false, true) => false,
_ => continue,
};
if cosine.abs() > PERPENDICULAR_EPS {
joins[slot] = Some(ButtJoin {
partner: other.element_id,
runs_through,
reach_feet: None,
layer_reach_feet: None,
layer_edge_reach_feet: angled_layer_edges(
wall,
other,
point,
into,
runs_through,
),
});
continue;
}
let half = other.thickness_feet * 0.5;
let single = wall.layers.len() == 1 && other.layers.len() == 1;
joins[slot] = Some(ButtJoin {
partner: other.element_id,
runs_through,
reach_feet: single.then_some(if runs_through { half } else { -half }),
layer_reach_feet: layer_reaches(wall, other, point, into, runs_through),
layer_edge_reach_feet: None,
});
}
if joins.iter().any(Option::is_some) {
out.insert(wall.element_id, joins);
}
}
out
}
pub fn tee_join(
wall: &WallLine,
lines: &[&WallLine],
point: [f64; 2],
into: [f64; 2],
) -> Option<ButtJoin> {
let dot = |a: [f64; 2], b: [f64; 2]| a[0] * b[0] + a[1] * b[1];
let hosts: Vec<&WallLine> = lines
.iter()
.copied()
.filter(|other| other.element_id != wall.element_id)
.filter(|other| {
wall.top_feet.min(other.top_feet) - wall.base_feet.max(other.base_feet) > JOIN_EPS_FEET
})
.filter(|other| {
let run = (other.end[0] - other.start[0]).hypot(other.end[1] - other.start[1]);
let axis = [
(other.end[0] - other.start[0]) / run,
(other.end[1] - other.start[1]) / run,
];
let offset = [point[0] - other.start[0], point[1] - other.start[1]];
let at = dot(offset, axis);
let across = dot(offset, [-axis[1], axis[0]]);
let sine = (1.0 - dot(into, axis).powi(2)).max(0.0).sqrt();
if sine <= 0.0 {
return false;
}
let clear = wall.thickness_feet * 0.5 / sine - JOIN_EPS_FEET;
at > clear && at < run - clear && across.abs() <= JOIN_EPS_FEET
})
.collect();
let [host] = hosts[..] else {
return None;
};
let run = (host.end[0] - host.start[0]).hypot(host.end[1] - host.start[1]);
let cosine = dot(
into,
[
(host.end[0] - host.start[0]) / run,
(host.end[1] - host.start[1]) / run,
],
);
if cosine.abs() > ANGLED_JOIN_MAX_COSINE || wall.layers.is_empty() || host.layers.is_empty() {
return None;
}
if cosine.abs() > PERPENDICULAR_EPS {
return Some(ButtJoin {
partner: host.element_id,
runs_through: false,
reach_feet: None,
layer_reach_feet: None,
layer_edge_reach_feet: Some(angled_tee_edges(wall, host, point, into)?),
});
}
let half = host.thickness_feet * 0.5;
if wall.layers.len() == 1 {
return Some(ButtJoin {
partner: host.element_id,
runs_through: false,
reach_feet: Some(-half),
layer_reach_feet: None,
layer_edge_reach_feet: None,
});
}
let near_first: Vec<(f64, u32)> = if dot(host.exterior, into) > 0.0 {
host.layers.clone()
} else {
host.layers.iter().rev().copied().collect()
};
let reaches: Vec<f64> = wall
.layers
.iter()
.map(|(_, function)| {
-half
+ near_first
.iter()
.take_while(|(_, other)| other > function)
.map(|(width, _)| width)
.sum::<f64>()
})
.collect();
Some(ButtJoin {
partner: host.element_id,
runs_through: false,
reach_feet: None,
layer_reach_feet: Some(reaches),
layer_edge_reach_feet: None,
})
}
fn meet(from: [f64; 2], direction: [f64; 2], on: [f64; 2], along: [f64; 2]) -> Option<f64> {
let cross = direction[0] * along[1] - direction[1] * along[0];
if cross.abs() < 1e-12 {
return None;
}
let gap = [on[0] - from[0], on[1] - from[1]];
Some((gap[0] * along[1] - gap[1] * along[0]) / cross)
}
fn edge_reaches(
wall: &WallLine,
point: [f64; 2],
into: [f64; 2],
targets: &[([f64; 2], [f64; 2])],
) -> Option<Vec<[f64; 2]>> {
if targets.len() != wall.layers.len() {
return None;
}
let mut edge = wall.thickness_feet * 0.5;
let mut out = Vec::with_capacity(targets.len());
for ((width, _), (on, along)) in wall.layers.iter().zip(targets) {
let mut pair = [0.0; 2];
for (side, offset) in [edge, edge - width].into_iter().enumerate() {
let from = [
point[0] + wall.exterior[0] * offset,
point[1] + wall.exterior[1] * offset,
];
let reach = -meet(from, into, *on, *along)?;
if !reach.is_finite() {
return None;
}
pair[side] = reach;
}
out.push(pair);
edge -= width;
}
Some(out)
}
fn boundaries_from(other: &WallLine, side_normal: [f64; 2]) -> (Vec<f64>, Vec<(f64, u32)>) {
let layers: Vec<(f64, u32)> =
if other.exterior[0] * side_normal[0] + other.exterior[1] * side_normal[1] > 0.0 {
other.layers.clone()
} else {
other.layers.iter().rev().copied().collect()
};
let mut at = other.thickness_feet * 0.5;
let mut offsets = vec![at];
for (width, _) in &layers {
at -= width;
offsets.push(at);
}
(offsets, layers)
}
pub fn angled_layer_edges(
wall: &WallLine,
other: &WallLine,
point: [f64; 2],
into: [f64; 2],
runs_through: bool,
) -> Option<Vec<[f64; 2]>> {
let dot = |a: [f64; 2], b: [f64; 2]| a[0] * b[0] + a[1] * b[1];
let far = if (other.start[0] - point[0]).hypot(other.start[1] - point[1]) <= JOIN_EPS_FEET {
other.end
} else {
other.start
};
let run = (far[0] - point[0]).hypot(far[1] - point[1]);
if !(run.is_finite() && run > 1e-9) {
return None;
}
let away = [(far[0] - point[0]) / run, (far[1] - point[1]) / run];
let normal = [-away[1], away[0]];
let outer = if dot(normal, into) < 0.0 {
normal
} else {
[-normal[0], -normal[1]]
};
let (offsets, theirs) = boundaries_from(other, outer);
let line = |offset: f64| {
(
[point[0] + outer[0] * offset, point[1] + outer[1] * offset],
away,
)
};
let exterior_outside = dot(wall.exterior, away) < 0.0;
let targets: Vec<([f64; 2], [f64; 2])> = if wall.layers.len() == 1 && other.layers.len() == 1 {
vec![line(if runs_through { offsets[0] } else { offsets[1] })]
} else {
if wall.layers.len() < 2
|| other.layers.len() != wall.layers.len()
|| (wall.base_feet - other.base_feet).abs() > LAYER_JOIN_HEIGHT_EPS_FEET
|| (wall.top_feet - other.top_feet).abs() > LAYER_JOIN_HEIGHT_EPS_FEET
{
return None;
}
let ours: Vec<u32> = if exterior_outside {
wall.layers.iter().map(|(_, f)| *f).collect()
} else {
wall.layers.iter().rev().map(|(_, f)| *f).collect()
};
let theirs: Vec<u32> = theirs.iter().map(|(_, f)| *f).collect();
if ours != theirs {
return None;
}
let from_corner: Vec<([f64; 2], [f64; 2])> = (0..wall.layers.len())
.map(|rank| line(offsets[if runs_through { rank } else { rank + 1 }]))
.collect();
if exterior_outside {
from_corner
} else {
from_corner.into_iter().rev().collect()
}
};
edge_reaches(wall, point, into, &targets)
}
pub fn angled_tee_edges(
wall: &WallLine,
host: &WallLine,
point: [f64; 2],
into: [f64; 2],
) -> Option<Vec<[f64; 2]>> {
let run = (host.end[0] - host.start[0]).hypot(host.end[1] - host.start[1]);
let axis = [
(host.end[0] - host.start[0]) / run,
(host.end[1] - host.start[1]) / run,
];
let normal = [-axis[1], axis[0]];
let near = if normal[0] * into[0] + normal[1] * into[1] > 0.0 {
normal
} else {
[-normal[0], -normal[1]]
};
let (offsets, near_first) = boundaries_from(host, near);
let targets: Vec<([f64; 2], [f64; 2])> = wall
.layers
.iter()
.map(|(_, function)| {
let passed = if wall.layers.len() == 1 {
0
} else {
near_first
.iter()
.take_while(|(_, other)| other > function)
.count()
};
(
[
point[0] + near[0] * offsets[passed],
point[1] + near[1] * offsets[passed],
],
axis,
)
})
.collect();
edge_reaches(wall, point, into, &targets)
}
pub fn layer_reaches(
wall: &WallLine,
other: &WallLine,
point: [f64; 2],
into: [f64; 2],
runs_through: bool,
) -> Option<Vec<f64>> {
if wall.layers.len() < 2
|| other.layers.len() != wall.layers.len()
|| (wall.base_feet - other.base_feet).abs() > LAYER_JOIN_HEIGHT_EPS_FEET
|| (wall.top_feet - other.top_feet).abs() > LAYER_JOIN_HEIGHT_EPS_FEET
{
return None;
}
let dot = |a: [f64; 2], b: [f64; 2]| a[0] * b[0] + a[1] * b[1];
let far = if (other.start[0] - point[0]).hypot(other.start[1] - point[1]) <= JOIN_EPS_FEET {
other.end
} else {
other.start
};
let partner_on_exterior = dot([far[0] - point[0], far[1] - point[1]], wall.exterior) > 0.0;
let outward = if dot(other.exterior, [-into[0], -into[1]]) >= 0.0 {
1.0
} else {
-1.0
};
let mut bound = other.thickness_feet * 0.5;
let mut bounds = vec![bound * outward];
for (width, _) in &other.layers {
bound -= width;
bounds.push(bound * outward);
}
let ours: Vec<u32> = if partner_on_exterior {
wall.layers.iter().rev().map(|(_, f)| *f).collect()
} else {
wall.layers.iter().map(|(_, f)| *f).collect()
};
let partner_outside_first = outward > 0.0;
let theirs: Vec<u32> = if partner_outside_first {
other.layers.iter().map(|(_, f)| *f).collect()
} else {
other.layers.iter().rev().map(|(_, f)| *f).collect()
};
if ours != theirs {
return None;
}
bounds.sort_by(|a, b| b.total_cmp(a));
let reaches: Vec<f64> = (0..wall.layers.len())
.map(|rank| bounds[if runs_through { rank } else { rank + 1 }])
.collect();
Some(if partner_on_exterior {
reaches.into_iter().rev().collect()
} else {
reaches
})
}
pub const LAYER_JOIN_HEIGHT_EPS_FEET: f64 = 1e-3;
#[cfg(test)]
mod tests {
use super::*;
use crate::partition_element_records as per;
fn wall(element_id: u32, bbox_feet: [f64; 6], joins: &[u32]) -> PartitionElementRecord {
let mut references: Vec<u64> = vec![3, 17328, 20307, 20308];
references.extend(joins.iter().map(|id| u64::from(*id)));
references.push(u64::from(element_id));
references.sort_unstable();
PartitionElementRecord {
stream: "Partitions/59".into(),
offset: element_id as usize,
element_id,
flags: 0x0121,
builtin_category: per::OST_WALLS,
container: per::CONTAINER_NONE,
placement_kind: per::PLACEMENT_KIND_INSTANCE,
class_tag: 0,
bbox_feet,
preceding_reference: None,
owner_reference: None,
references,
id_from_enclosing_record: false,
design_option: None,
}
}
fn core_interior_corner() -> Vec<PartitionElementRecord> {
vec![
wall(
20796,
[47.75, 80.66667, 76.0, 75.0, 81.33333, 91.0],
&[20797, 20799],
),
wall(20797, [47.75, 57.66667, 76.0, 48.25, 81.0, 91.0], &[20796]),
wall(
20799,
[74.66667, 58.0, 76.0, 75.33333, 81.33333, 91.0],
&[20796],
),
]
}
#[test]
fn a_wall_is_cut_back_by_half_the_wall_it_ends_on() {
let trims = join_trims(&core_interior_corner());
let t = trims[&20796];
assert_eq!(t.axis, 0);
assert!((t.thickness_feet - 0.66666).abs() < 1e-4);
assert!((t.start_feet - 0.0).abs() < 1e-9, "47.75 joins nothing");
assert!((t.end_feet - 0.33333).abs() < 1e-4, "half of 20799");
}
#[test]
fn the_perpendicular_wall_is_cut_by_the_one_it_ends_on() {
let trims = join_trims(&core_interior_corner());
let t = trims[&20797];
assert_eq!(t.axis, 1);
assert!((t.start_feet - 0.0).abs() < 1e-9);
assert!((t.end_feet - 0.33333).abs() < 1e-4, "half of 20796");
}
#[test]
fn a_wall_that_ends_short_of_a_centreline_is_not_cut() {
let trims = join_trims(&core_interior_corner());
let t = trims[&20799];
assert!((t.end_feet - 0.0).abs() < 1e-9);
assert!((t.start_feet - 0.0).abs() < 1e-9, "58.0 joins nothing");
}
#[test]
fn a_candidate_out_of_elevation_range_does_not_cut() {
let mut records = core_interior_corner();
records[2].bbox_feet[2] = 106.0;
records[2].bbox_feet[5] = 121.0;
let trims = join_trims(&records);
assert!((trims[&20796].end_feet - 0.0).abs() < 1e-9);
}
#[test]
fn candidates_that_disagree_on_thickness_decline_the_element() {
let mut records = core_interior_corner();
records.push(wall(
21000,
[74.75, 58.0, 76.0, 75.25, 81.33333, 91.0],
&[20796],
));
records[0].references.push(21000);
records[0].references.sort_unstable();
let trims = join_trims(&records);
assert!(!trims.contains_key(&20796), "ambiguous join declines");
assert!(trims.contains_key(&20797), "other walls are unaffected");
}
#[test]
fn a_square_plan_box_has_no_wall_axis() {
let records = vec![wall(30000, [0.0, 0.0, 0.0, 2.0, 2.0, 10.0], &[])];
assert!(join_trims(&records).is_empty());
}
#[test]
fn a_trim_that_would_collapse_the_run_is_declined() {
let records = vec![
wall(
31000,
[0.0, 9.66667, 0.0, 1.0, 10.33333, 10.0],
&[31001, 31002],
),
wall(31001, [-1.0, 0.0, 0.0, 1.0, 20.0, 10.0], &[31000]),
wall(31002, [0.0, 0.0, 0.0, 2.0, 20.0, 10.0], &[31000]),
];
let trims = join_trims(&records);
assert!(!trims.contains_key(&31000));
}
fn unjoined_span() -> Vec<PartitionElementRecord> {
vec![
wall(20826, [119.5, 67.41667, 76.0, 129.0, 68.08333, 91.0], &[]),
wall(20825, [119.16667, 58.0, 76.0, 119.83333, 81.0, 91.0], &[]),
wall(20804, [128.66667, 58.0, 76.0, 129.33333, 81.0, 91.0], &[]),
]
}
#[test]
fn a_wall_that_names_no_join_partner_is_not_trimmed() {
let trims = join_trims(&unjoined_span());
let t = trims[&20826];
assert!((t.start_feet - 0.0).abs() < 1e-9, "119.5 is not cut");
assert!((t.end_feet - 0.0).abs() < 1e-9, "129.0 is not cut");
}
#[test]
fn naming_the_same_two_walls_restores_the_trim() {
let mut records = unjoined_span();
records[0] = wall(
20826,
[119.5, 67.41667, 76.0, 129.0, 68.08333, 91.0],
&[20804, 20825],
);
let t = join_trims(&records)[&20826];
assert!((t.start_feet - 0.33333).abs() < 1e-4);
assert!((t.end_feet - 0.33333).abs() < 1e-4);
}
fn cut_back_candidate() -> Vec<PartitionElementRecord> {
vec![
wall(
20800,
[85.5, 80.66667, 76.0, 137.25, 81.33333, 91.0],
&[20803, 20805, 20816],
),
wall(
20805,
[85.16667, 58.0, 76.0, 85.83333, 81.33333, 91.0],
&[20800],
),
wall(
20816,
[136.91667, 58.0, 76.0, 137.58333, 81.0, 91.0],
&[20800],
),
wall(20803, [136.83333, 81.0, 76.0, 137.5, 87.5, 91.0], &[20800]),
]
}
#[test]
fn a_candidate_cut_back_short_of_the_line_does_not_trim() {
let trims = join_trims(&cut_back_candidate());
assert!(
(trims[&20800].end_feet - 0.33333).abs() < 1e-4,
"20800 is still cut by 20816"
);
assert!(
(trims[&20803].start_feet - 0.0).abs() < 1e-9,
"20800 no longer reaches 137.1667"
);
}
#[test]
fn the_surviving_side_of_a_true_l_corner_is_still_over_trimmed() {
let trims = join_trims(&cut_back_candidate());
assert!(
(trims[&20816].end_feet - 0.33333).abs() < 1e-4,
"Revit leaves 20816 at 81.0; the solver cuts it"
);
}
#[test]
fn a_cut_the_resolved_line_imposed_does_not_disqualify_the_candidate() {
let records = vec![
wall(
20798,
[48.0, 57.66667, 76.0, 100.25, 58.33333, 91.0],
&[20797, 20817],
),
wall(20817, [47.75, 51.5, 76.0, 48.25, 58.0, 91.0], &[20798]),
wall(20797, [47.75, 57.66667, 76.0, 48.25, 81.0, 91.0], &[20798]),
];
let trims = join_trims(&records);
assert!(
(trims[&20798].start_feet - 0.25).abs() < 1e-4,
"20798 is cut by the x = 48 line"
);
assert!(
(trims[&20817].end_feet - 0.33333).abs() < 1e-4,
"and 20817 is still cut by 20798"
);
}
#[test]
fn joined_walls_keeps_only_recovered_wall_ids() {
let record = wall(
20796,
[47.75, 80.66667, 76.0, 75.0, 81.33333, 91.0],
&[20797],
);
let ids = [20796u32, 20797].into_iter().collect();
let joined = joined_walls(&record, &ids);
assert_eq!(joined.iter().copied().collect::<Vec<u32>>(), vec![20797]);
}
}