1use axiolid_core::{Frame2, Point2, Vec2};
15use axiolid_curve::{BSplineCurve2, Circle2, Curve2, ElevationLaw, KnotSpec, Line2};
16use axiolid_model::{
17 CurveRelation, CurveSegment, GeometryGraph, GeometryGraphBuilder, GeometryNode, NodeId,
18 Transition, TrimSelector, TrimmingPreference,
19};
20use ifc_model::{EntityId, Model};
21
22use crate::cant::CantLayout;
23use crate::curve::cubic::{cubic_curve, start_frame, CUBIC};
24use crate::curve::elevation::elevation_law;
25use crate::curve::seam::{check_position, HorizontalSeam, SeamCheck};
26use crate::curve::spiral::{is_exactly_lowerable, refuse_unlowerable, spiral_curve};
27use crate::curve::terminal::split_closing;
28use crate::error::{AlignmentError, AlignmentResult};
29use crate::horizontal::{
30 read_horizontal_segment, AlignmentUnits, HorizontalSegment, HorizontalSegmentType,
31};
32use crate::vertical::{read_vertical_segment, VerticalSegment};
33use crate::view::AlignmentView;
34
35#[derive(Debug, Clone, PartialEq)]
37#[non_exhaustive]
38pub struct LoweredAlignmentCurve {
39 pub graph: GeometryGraph,
42 pub root: NodeId,
44 pub sources: Vec<EntityId>,
46 pub seams: Vec<HorizontalSeam>,
50}
51
52fn seam_transition(seam: &HorizontalSeam) -> Transition {
62 match seam.position {
63 SeamCheck::Verified => Transition::Continuous,
64 _ => Transition::Discontinuous,
65 }
66}
67
68#[derive(Debug, Clone, PartialEq)]
74#[non_exhaustive]
75pub struct RefusedSegment {
76 pub entity: EntityId,
78 pub type_name: String,
80 pub reason: AlignmentError,
82}
83
84#[derive(Debug, Clone, PartialEq)]
98#[non_exhaustive]
99pub struct PartialHorizontalLayout {
100 pub runs: Vec<LoweredAlignmentCurve>,
103 pub refused: Vec<RefusedSegment>,
105 pub segment_count: usize,
107}
108
109impl PartialHorizontalLayout {
110 #[must_use]
116 pub fn is_complete(&self) -> bool {
117 self.refused.is_empty()
118 }
119
120 #[must_use]
122 pub fn lowered_count(&self) -> usize {
123 self.segment_count - self.refused.len()
124 }
125}
126
127pub fn lower_horizontal_segment(
140 model: &Model,
141 id: EntityId,
142 units: AlignmentUnits,
143) -> AlignmentResult<LoweredAlignmentCurve> {
144 let segment = read_horizontal_segment(model, id, units)?;
145 if segment.segment_length == 0.0 {
146 return Err(no_geometry(id));
147 }
148 let mut builder = GeometryGraphBuilder::new();
149 let root = match &segment.segment_type {
150 HorizontalSegmentType::Line => push_line(&mut builder, &segment)?,
151 HorizontalSegmentType::CircularArc => push_arc(&mut builder, &segment)?,
152 HorizontalSegmentType::Transition(name) if name == CUBIC => {
153 push_cubic(&mut builder, &segment)?
154 }
155 HorizontalSegmentType::Transition(name) if is_exactly_lowerable(name, false) => {
156 push_spiral(&mut builder, &segment, name, None, 0.0)?
157 }
158 _ => return Err(refuse_unlowerable(&segment)),
159 };
160 finish(builder, root, vec![id])
161}
162
163pub fn lower_vertical_segment(
191 model: &Model,
192 id: EntityId,
193 units: AlignmentUnits,
194) -> AlignmentResult<LoweredAlignmentCurve> {
195 let segment = read_vertical_segment(model, id, units)?;
196 if segment.horizontal_length == 0.0 {
197 return Err(no_geometry(id));
198 }
199 let law = elevation_law(&segment)?;
200 let mut builder = GeometryGraphBuilder::new();
201 let root = push_vertical_law(&mut builder, &segment, &law)?;
202 finish(builder, root, vec![id])
203}
204
205fn push_line(
207 builder: &mut GeometryGraphBuilder,
208 segment: &HorizontalSegment,
209) -> AlignmentResult<NodeId> {
210 if segment.start_radius != 0.0 || segment.end_radius != 0.0 {
211 return Err(AlignmentError::InvalidSegment {
212 entity: segment.entity,
213 detail: "LINE requires zero start and end radii",
214 });
215 }
216 let direction = Vec2::new(segment.start_direction.cos(), segment.start_direction.sin());
217 let basis = push(
218 builder,
219 GeometryNode::Curve2(Curve2::Line(Line2 {
220 origin: segment.start_point,
221 direction,
222 })),
223 )?;
224 push(
225 builder,
226 GeometryNode::CurveRelation(CurveRelation::Trimmed {
227 basis,
228 start: vec![TrimSelector::Parameter(0.0)],
229 end: vec![TrimSelector::Parameter(segment.segment_length)],
230 sense_agreement: true,
231 preference: TrimmingPreference::Parameter,
232 }),
233 )
234}
235
236fn push_arc(
239 builder: &mut GeometryGraphBuilder,
240 segment: &HorizontalSegment,
241) -> AlignmentResult<NodeId> {
242 if segment.start_radius == 0.0
243 || segment.start_radius != segment.end_radius
244 || !segment.start_radius.is_finite()
245 {
246 return Err(AlignmentError::InvalidSegment {
247 entity: segment.entity,
248 detail: "CIRCULARARC requires equal, finite, non-zero start and end radii",
249 });
250 }
251 let direction = Vec2::new(segment.start_direction.cos(), segment.start_direction.sin());
252 let left = Vec2::new(-direction.y, direction.x);
253 let signed_radius = segment.start_radius;
254 let radius = signed_radius.abs();
255 let centre = segment.start_point + left * signed_radius;
256 let x = left * -signed_radius.signum();
260 let y = Vec2::new(-x.y, x.x);
263 let sweep = segment.segment_length / signed_radius;
264 if [centre.x, centre.y, x.x, x.y, y.x, y.y, radius, sweep]
265 .iter()
266 .any(|value| !value.is_finite())
267 {
268 return Err(AlignmentError::InvalidSegment {
269 entity: segment.entity,
270 detail: "CIRCULARARC derived frame and trim parameters must be finite",
271 });
272 }
273 let basis = push(
274 builder,
275 GeometryNode::Curve2(Curve2::Circle(Circle2 {
276 frame: Frame2 {
277 origin: centre,
278 x,
279 y,
280 },
281 radius,
282 })),
283 )?;
284 push(
285 builder,
286 GeometryNode::CurveRelation(CurveRelation::Trimmed {
287 basis,
288 start: vec![TrimSelector::Parameter(0.0)],
289 end: vec![TrimSelector::Parameter(sweep)],
290 sense_agreement: true,
291 preference: TrimmingPreference::Parameter,
292 }),
293 )
294}
295
296fn push_vertical_law(
299 builder: &mut GeometryGraphBuilder,
300 segment: &VerticalSegment,
301 law: &ElevationLaw,
302) -> AlignmentResult<NodeId> {
303 let unsupported = |detail| AlignmentError::Unsupported {
304 entity: segment.entity,
305 type_name: segment.predefined_type.source_name().to_owned(),
306 detail,
307 };
308 let coefficients = match law {
309 ElevationLaw::Polynomial { coefficients } => coefficients,
310 ElevationLaw::CircularArc { grade, radius, .. } => {
311 return push_vertical_arc(builder, segment, *grade, *radius)
312 }
313 _ => {
314 return Err(unsupported(
315 "a single vertical segment lowers from one polynomial or circular piece",
316 ))
317 }
318 };
319 let start = segment.start_dist_along;
320 let length = segment.horizontal_length;
321 let coefficient = |power: usize| coefficients.get(power).copied().unwrap_or(0.0);
322 let curve = match coefficients.len() {
323 0..=2 => Curve2::Line(Line2 {
324 origin: Point2::new(start, coefficient(0)),
325 direction: Vec2::new(1.0, coefficient(1)),
326 }),
327 3 => {
328 let (z0, g0, c2) = (coefficient(0), coefficient(1), coefficient(2));
329 let control_points = vec![
330 Point2::new(start, z0),
331 Point2::new(start + 0.5 * length, z0 + 0.5 * g0 * length),
332 Point2::new(start + length, z0 + g0 * length + c2 * length * length),
333 ];
334 if control_points
335 .iter()
336 .any(|p| !p.x.is_finite() || !p.y.is_finite())
337 {
338 return Err(AlignmentError::InvalidSegment {
339 entity: segment.entity,
340 detail: "vertical control points must be finite",
341 });
342 }
343 Curve2::BSpline(BSplineCurve2 {
344 degree: 2,
345 control_points,
346 knots: vec![0.0, length],
347 multiplicities: vec![3, 3],
348 weights: None,
349 closed: false,
350 self_intersect: Some(false),
351 knot_spec: KnotSpec::PiecewiseBezier,
352 })
353 }
354 _ => {
355 return Err(unsupported(
356 "the vertical law has no exact neutral curve of its degree",
357 ))
358 }
359 };
360 let basis = push(builder, GeometryNode::Curve2(curve))?;
361 push(
362 builder,
363 GeometryNode::CurveRelation(CurveRelation::Trimmed {
364 basis,
365 start: vec![TrimSelector::Parameter(0.0)],
366 end: vec![TrimSelector::Parameter(length)],
367 sense_agreement: true,
368 preference: TrimmingPreference::Parameter,
369 }),
370 )
371}
372
373fn push_vertical_arc(
382 builder: &mut GeometryGraphBuilder,
383 segment: &VerticalSegment,
384 grade: f64,
385 radius: f64,
386) -> AlignmentResult<NodeId> {
387 let norm = grade.hypot(1.0);
389 let (sin0, cos0) = (grade / norm, 1.0 / norm);
390 let left = Vec2::new(-sin0, cos0);
391 let start = Point2::new(segment.start_dist_along, segment.start_height);
392 let centre = start + left * radius;
393 let x = left * -radius.signum();
394 let y = Vec2::new(-x.y, x.x);
395 let sin1 = sin0 + segment.horizontal_length / radius;
396 let sweep = sin1.asin() - sin0.atan2(cos0);
397 if [centre.x, centre.y, sweep].iter().any(|v| !v.is_finite()) || sin1.abs() >= 1.0 {
398 return Err(AlignmentError::InvalidSegment {
399 entity: segment.entity,
400 detail: "CIRCULARARC derived frame and trim angle must be finite",
401 });
402 }
403 let basis = push(
404 builder,
405 GeometryNode::Curve2(Curve2::Circle(Circle2 {
406 frame: Frame2 {
407 origin: centre,
408 x,
409 y,
410 },
411 radius: radius.abs(),
412 })),
413 )?;
414 push(
415 builder,
416 GeometryNode::CurveRelation(CurveRelation::Trimmed {
417 basis,
418 start: vec![TrimSelector::Parameter(0.0)],
419 end: vec![TrimSelector::Parameter(sweep)],
420 sense_agreement: true,
421 preference: TrimmingPreference::Parameter,
422 }),
423 )
424}
425
426fn push_spiral(
429 builder: &mut GeometryGraphBuilder,
430 segment: &HorizontalSegment,
431 name: &str,
432 cant: Option<&CantLayout>,
433 start_distance: f64,
434) -> AlignmentResult<NodeId> {
435 let curve = spiral_curve(segment, name, cant, start_distance)?;
436 let basis = push(builder, GeometryNode::Curve2(curve))?;
437 push(
438 builder,
439 GeometryNode::CurveRelation(CurveRelation::Trimmed {
440 basis,
441 start: vec![TrimSelector::Parameter(0.0)],
442 end: vec![TrimSelector::Parameter(segment.segment_length)],
443 sense_agreement: true,
444 preference: TrimmingPreference::Parameter,
445 }),
446 )
447}
448
449fn push_cubic(
456 builder: &mut GeometryGraphBuilder,
457 segment: &HorizontalSegment,
458) -> AlignmentResult<NodeId> {
459 let curve = cubic_curve(segment, start_frame(segment))?;
460 let basis = push(builder, GeometryNode::Curve2(curve))?;
461 push(
462 builder,
463 GeometryNode::CurveRelation(CurveRelation::Trimmed {
464 basis,
465 start: vec![TrimSelector::Parameter(0.0)],
466 end: vec![TrimSelector::ArcLength(segment.segment_length)],
467 sense_agreement: true,
468 preference: TrimmingPreference::Parameter,
469 }),
470 )
471}
472
473fn no_geometry(entity: EntityId) -> AlignmentError {
476 AlignmentError::InvalidSegment {
477 entity,
478 detail: "a zero-length segment has no geometry to lower; IFC4.3 allows it only as the \
479 closing segment of a layout",
480 }
481}
482
483fn push(builder: &mut GeometryGraphBuilder, node: GeometryNode) -> AlignmentResult<NodeId> {
484 builder.push(node).map_err(|error| AlignmentError::Graph {
485 detail: error.to_string(),
486 })
487}
488
489pub(super) fn finish(
490 builder: GeometryGraphBuilder,
491 root: NodeId,
492 sources: Vec<EntityId>,
493) -> AlignmentResult<LoweredAlignmentCurve> {
494 let graph = builder
495 .finish(vec![root])
496 .map_err(|error| AlignmentError::Graph {
497 detail: error.to_string(),
498 })?;
499 Ok(LoweredAlignmentCurve {
500 graph,
501 root,
502 sources,
503 seams: Vec::new(),
504 })
505}
506
507pub fn lower_horizontal_layout(
521 model: &Model,
522 entity: EntityId,
523 units: AlignmentUnits,
524 cant: Option<&CantLayout>,
525) -> AlignmentResult<LoweredAlignmentCurve> {
526 let view = AlignmentView::for_model(model)?;
527 let horizontal_entity = model
528 .get(entity)
529 .ok_or(AlignmentError::MissingEntity { entity })?;
530 if !view
531 .schema
532 .is_a(&horizontal_entity.type_name, "IfcAlignmentHorizontal")
533 {
534 return Err(AlignmentError::WrongType {
535 entity,
536 expected: "IfcAlignmentHorizontal",
537 actual: horizontal_entity.type_name.to_string(),
538 });
539 }
540 let ids = view.segment_chain(entity, "IfcAlignmentHorizontalSegment")?;
541 if ids.is_empty() {
542 return Err(AlignmentError::SemanticViolation {
543 entity: Some(entity),
544 rule: "IfcAlignmentHorizontal must nest at least one IfcAlignmentSegment",
545 });
546 }
547
548 let mut segments = Vec::with_capacity(ids.len());
549 for id in &ids {
550 segments.push(read_horizontal_segment(model, *id, units)?);
551 }
552
553 let (body, closing) = split_closing(&segments, |s| s.segment_length, |s| s.entity)?;
554
555 let mut builder = GeometryGraphBuilder::new();
556 let mut composite_segments = Vec::with_capacity(body.len());
557 let mut seams = Vec::with_capacity(segments.len().saturating_sub(1));
558 let mut station = 0.0_f64;
559 for (index, segment) in body.iter().enumerate() {
560 let curve = match &segment.segment_type {
561 HorizontalSegmentType::Line => push_line(&mut builder, segment)?,
562 HorizontalSegmentType::CircularArc => push_arc(&mut builder, segment)?,
563 HorizontalSegmentType::Transition(name) if name == CUBIC => {
564 push_cubic(&mut builder, segment)?
565 }
566 HorizontalSegmentType::Transition(name)
567 if is_exactly_lowerable(name, cant.is_some()) =>
568 {
569 push_spiral(&mut builder, segment, name, cant, station)?
570 }
571 _ => return Err(refuse_unlowerable(segment)),
572 };
573 let transition = if index == 0 {
574 Transition::Discontinuous
575 } else {
576 let seam = check_position(&body[index - 1], segment, station)?;
577 let transition = seam_transition(&seam);
578 seams.push(seam);
579 transition
580 };
581 composite_segments.push(CurveSegment {
582 curve,
583 same_sense: true,
584 transition,
585 });
586 station += segment.segment_length;
587 }
588 if let (Some(closing), Some(last)) = (closing, body.last()) {
591 seams.push(check_position(last, closing, station)?);
592 }
593
594 let root = push(
595 &mut builder,
596 GeometryNode::CurveRelation(CurveRelation::Composite {
597 segments: composite_segments,
598 }),
599 )?;
600 let mut lowered = finish(builder, root, ids)?;
601 lowered.seams = seams;
602 Ok(lowered)
603}
604
605pub fn lower_horizontal_layout_partial(
628 model: &Model,
629 entity: EntityId,
630 units: AlignmentUnits,
631 cant: Option<&CantLayout>,
632) -> AlignmentResult<PartialHorizontalLayout> {
633 let view = AlignmentView::for_model(model)?;
634 let horizontal_entity = model
635 .get(entity)
636 .ok_or(AlignmentError::MissingEntity { entity })?;
637 if !view
638 .schema
639 .is_a(&horizontal_entity.type_name, "IfcAlignmentHorizontal")
640 {
641 return Err(AlignmentError::WrongType {
642 entity,
643 expected: "IfcAlignmentHorizontal",
644 actual: horizontal_entity.type_name.to_string(),
645 });
646 }
647 let ids = view.segment_chain(entity, "IfcAlignmentHorizontalSegment")?;
648 if ids.is_empty() {
649 return Err(AlignmentError::SemanticViolation {
650 entity: Some(entity),
651 rule: "IfcAlignmentHorizontal must nest at least one IfcAlignmentSegment",
652 });
653 }
654
655 let mut segments = Vec::with_capacity(ids.len());
656 for id in &ids {
657 segments.push(read_horizontal_segment(model, *id, units)?);
658 }
659
660 let (body, closing) = split_closing(&segments, |s| s.segment_length, |s| s.entity)?;
661
662 let mut runs = Vec::new();
663 let mut refused = Vec::new();
664 let mut builder = GeometryGraphBuilder::new();
667 let mut pending: Vec<(usize, CurveSegment)> = Vec::new();
668 let mut pending_ids: Vec<EntityId> = Vec::new();
669 let mut pending_seams: Vec<HorizontalSeam> = Vec::new();
670 let mut station = 0.0_f64;
671
672 for (index, segment) in body.iter().enumerate() {
673 let lowered = match &segment.segment_type {
674 HorizontalSegmentType::Line => push_line(&mut builder, segment),
675 HorizontalSegmentType::CircularArc => push_arc(&mut builder, segment),
676 HorizontalSegmentType::Transition(name) if name == CUBIC => {
677 push_cubic(&mut builder, segment)
678 }
679 HorizontalSegmentType::Transition(name)
680 if is_exactly_lowerable(name, cant.is_some()) =>
681 {
682 push_spiral(&mut builder, segment, name, cant, station)
683 }
684 _ => Err(refuse_unlowerable(segment)),
685 };
686 let start_station = station;
687 station += segment.segment_length;
690 let curve = match lowered {
691 Ok(curve) => curve,
692 Err(reason) => {
693 flush_run(
696 &mut runs,
697 &mut builder,
698 &mut pending,
699 &mut pending_ids,
700 &mut pending_seams,
701 )?;
702 refused.push(RefusedSegment {
703 entity: segment.entity,
704 type_name: segment.segment_type.source_name().to_owned(),
705 reason,
706 });
707 continue;
708 }
709 };
710 let transition = match pending.last() {
713 None => Transition::Discontinuous,
714 Some((previous_index, _)) => {
715 let seam = check_position(&body[*previous_index], segment, start_station)?;
716 let transition = seam_transition(&seam);
717 pending_seams.push(seam);
718 transition
719 }
720 };
721 pending.push((
722 index,
723 CurveSegment {
724 curve,
725 same_sense: true,
726 transition,
727 },
728 ));
729 pending_ids.push(segment.entity);
730 }
731 if let (Some(closing), Some((previous_index, _))) = (closing, pending.last()) {
734 pending_seams.push(check_position(&body[*previous_index], closing, station)?);
735 }
736 flush_run(
737 &mut runs,
738 &mut builder,
739 &mut pending,
740 &mut pending_ids,
741 &mut pending_seams,
742 )?;
743
744 Ok(PartialHorizontalLayout {
745 runs,
746 refused,
747 segment_count: segments.len(),
748 })
749}
750
751fn flush_run(
756 runs: &mut Vec<LoweredAlignmentCurve>,
757 builder: &mut GeometryGraphBuilder,
758 pending: &mut Vec<(usize, CurveSegment)>,
759 pending_ids: &mut Vec<EntityId>,
760 pending_seams: &mut Vec<HorizontalSeam>,
761) -> AlignmentResult<()> {
762 if pending.is_empty() {
763 *builder = GeometryGraphBuilder::new();
766 pending_ids.clear();
767 pending_seams.clear();
768 return Ok(());
769 }
770 let mut finished = GeometryGraphBuilder::new();
771 core::mem::swap(builder, &mut finished);
772 let composite_segments: Vec<CurveSegment> =
773 pending.drain(..).map(|(_, segment)| segment).collect();
774 let root = push(
775 &mut finished,
776 GeometryNode::CurveRelation(CurveRelation::Composite {
777 segments: composite_segments,
778 }),
779 )?;
780 let sources = core::mem::take(pending_ids);
781 let mut run = finish(finished, root, sources)?;
782 run.seams = core::mem::take(pending_seams);
783 runs.push(run);
784 Ok(())
785}