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::elevation::elevation_law;
24use crate::curve::seam::{check_position, HorizontalSeam, SeamCheck};
25use crate::curve::spiral::{is_exactly_lowerable, refuse_unlowerable, spiral_curve};
26use crate::curve::terminal::split_closing;
27use crate::error::{AlignmentError, AlignmentResult};
28use crate::horizontal::{
29 read_horizontal_segment, AlignmentUnits, HorizontalSegment, HorizontalSegmentType,
30};
31use crate::vertical::{read_vertical_segment, VerticalSegment};
32use crate::view::AlignmentView;
33
34#[derive(Debug, Clone, PartialEq)]
36#[non_exhaustive]
37pub struct LoweredAlignmentCurve {
38 pub graph: GeometryGraph,
41 pub root: NodeId,
43 pub sources: Vec<EntityId>,
45 pub seams: Vec<HorizontalSeam>,
49}
50
51fn seam_transition(seam: &HorizontalSeam) -> Transition {
61 match seam.position {
62 SeamCheck::Verified => Transition::Continuous,
63 _ => Transition::Discontinuous,
64 }
65}
66
67#[derive(Debug, Clone, PartialEq)]
73#[non_exhaustive]
74pub struct RefusedSegment {
75 pub entity: EntityId,
77 pub type_name: String,
79 pub reason: AlignmentError,
81}
82
83#[derive(Debug, Clone, PartialEq)]
97#[non_exhaustive]
98pub struct PartialHorizontalLayout {
99 pub runs: Vec<LoweredAlignmentCurve>,
102 pub refused: Vec<RefusedSegment>,
104 pub segment_count: usize,
106}
107
108impl PartialHorizontalLayout {
109 #[must_use]
115 pub fn is_complete(&self) -> bool {
116 self.refused.is_empty()
117 }
118
119 #[must_use]
121 pub fn lowered_count(&self) -> usize {
122 self.segment_count - self.refused.len()
123 }
124}
125
126pub fn lower_horizontal_segment(
136 model: &Model,
137 id: EntityId,
138 units: AlignmentUnits,
139) -> AlignmentResult<LoweredAlignmentCurve> {
140 let segment = read_horizontal_segment(model, id, units)?;
141 if segment.segment_length == 0.0 {
142 return Err(no_geometry(id));
143 }
144 let mut builder = GeometryGraphBuilder::new();
145 let root = match &segment.segment_type {
146 HorizontalSegmentType::Line => push_line(&mut builder, &segment)?,
147 HorizontalSegmentType::CircularArc => push_arc(&mut builder, &segment)?,
148 HorizontalSegmentType::Transition(name) if is_exactly_lowerable(name, false) => {
149 push_spiral(&mut builder, &segment, name, None, 0.0)?
150 }
151 _ => return Err(refuse_unlowerable(&segment)),
152 };
153 finish(builder, root, vec![id])
154}
155
156pub fn lower_vertical_segment(
180 model: &Model,
181 id: EntityId,
182 units: AlignmentUnits,
183) -> AlignmentResult<LoweredAlignmentCurve> {
184 let segment = read_vertical_segment(model, id, units)?;
185 if segment.horizontal_length == 0.0 {
186 return Err(no_geometry(id));
187 }
188 let law = elevation_law(&segment)?;
189 let mut builder = GeometryGraphBuilder::new();
190 let root = push_vertical_law(&mut builder, &segment, &law)?;
191 finish(builder, root, vec![id])
192}
193
194fn push_line(
196 builder: &mut GeometryGraphBuilder,
197 segment: &HorizontalSegment,
198) -> AlignmentResult<NodeId> {
199 if segment.start_radius != 0.0 || segment.end_radius != 0.0 {
200 return Err(AlignmentError::InvalidSegment {
201 entity: segment.entity,
202 detail: "LINE requires zero start and end radii",
203 });
204 }
205 let direction = Vec2::new(segment.start_direction.cos(), segment.start_direction.sin());
206 let basis = push(
207 builder,
208 GeometryNode::Curve2(Curve2::Line(Line2 {
209 origin: segment.start_point,
210 direction,
211 })),
212 )?;
213 push(
214 builder,
215 GeometryNode::CurveRelation(CurveRelation::Trimmed {
216 basis,
217 start: vec![TrimSelector::Parameter(0.0)],
218 end: vec![TrimSelector::Parameter(segment.segment_length)],
219 sense_agreement: true,
220 preference: TrimmingPreference::Parameter,
221 }),
222 )
223}
224
225fn push_arc(
228 builder: &mut GeometryGraphBuilder,
229 segment: &HorizontalSegment,
230) -> AlignmentResult<NodeId> {
231 if segment.start_radius == 0.0
232 || segment.start_radius != segment.end_radius
233 || !segment.start_radius.is_finite()
234 {
235 return Err(AlignmentError::InvalidSegment {
236 entity: segment.entity,
237 detail: "CIRCULARARC requires equal, finite, non-zero start and end radii",
238 });
239 }
240 let direction = Vec2::new(segment.start_direction.cos(), segment.start_direction.sin());
241 let left = Vec2::new(-direction.y, direction.x);
242 let signed_radius = segment.start_radius;
243 let radius = signed_radius.abs();
244 let centre = segment.start_point + left * signed_radius;
245 let x = left * -signed_radius.signum();
249 let y = Vec2::new(-x.y, x.x);
252 let sweep = segment.segment_length / signed_radius;
253 if [centre.x, centre.y, x.x, x.y, y.x, y.y, radius, sweep]
254 .iter()
255 .any(|value| !value.is_finite())
256 {
257 return Err(AlignmentError::InvalidSegment {
258 entity: segment.entity,
259 detail: "CIRCULARARC derived frame and trim parameters must be finite",
260 });
261 }
262 let basis = push(
263 builder,
264 GeometryNode::Curve2(Curve2::Circle(Circle2 {
265 frame: Frame2 {
266 origin: centre,
267 x,
268 y,
269 },
270 radius,
271 })),
272 )?;
273 push(
274 builder,
275 GeometryNode::CurveRelation(CurveRelation::Trimmed {
276 basis,
277 start: vec![TrimSelector::Parameter(0.0)],
278 end: vec![TrimSelector::Parameter(sweep)],
279 sense_agreement: true,
280 preference: TrimmingPreference::Parameter,
281 }),
282 )
283}
284
285fn push_vertical_law(
288 builder: &mut GeometryGraphBuilder,
289 segment: &VerticalSegment,
290 law: &ElevationLaw,
291) -> AlignmentResult<NodeId> {
292 let unsupported = |detail| AlignmentError::Unsupported {
293 entity: segment.entity,
294 type_name: segment.predefined_type.source_name().to_owned(),
295 detail,
296 };
297 let ElevationLaw::Polynomial { coefficients } = law else {
298 return Err(unsupported(
299 "a single vertical segment lowers from one polynomial piece",
300 ));
301 };
302 let start = segment.start_dist_along;
303 let length = segment.horizontal_length;
304 let coefficient = |power: usize| coefficients.get(power).copied().unwrap_or(0.0);
305 let curve = match coefficients.len() {
306 0..=2 => Curve2::Line(Line2 {
307 origin: Point2::new(start, coefficient(0)),
308 direction: Vec2::new(1.0, coefficient(1)),
309 }),
310 3 => {
311 let (z0, g0, c2) = (coefficient(0), coefficient(1), coefficient(2));
312 let control_points = vec![
313 Point2::new(start, z0),
314 Point2::new(start + 0.5 * length, z0 + 0.5 * g0 * length),
315 Point2::new(start + length, z0 + g0 * length + c2 * length * length),
316 ];
317 if control_points
318 .iter()
319 .any(|p| !p.x.is_finite() || !p.y.is_finite())
320 {
321 return Err(AlignmentError::InvalidSegment {
322 entity: segment.entity,
323 detail: "vertical control points must be finite",
324 });
325 }
326 Curve2::BSpline(BSplineCurve2 {
327 degree: 2,
328 control_points,
329 knots: vec![0.0, length],
330 multiplicities: vec![3, 3],
331 weights: None,
332 closed: false,
333 self_intersect: Some(false),
334 knot_spec: KnotSpec::PiecewiseBezier,
335 })
336 }
337 _ => {
338 return Err(unsupported(
339 "the vertical law has no exact neutral curve of its degree",
340 ))
341 }
342 };
343 let basis = push(builder, GeometryNode::Curve2(curve))?;
344 push(
345 builder,
346 GeometryNode::CurveRelation(CurveRelation::Trimmed {
347 basis,
348 start: vec![TrimSelector::Parameter(0.0)],
349 end: vec![TrimSelector::Parameter(length)],
350 sense_agreement: true,
351 preference: TrimmingPreference::Parameter,
352 }),
353 )
354}
355
356fn push_spiral(
359 builder: &mut GeometryGraphBuilder,
360 segment: &HorizontalSegment,
361 name: &str,
362 cant: Option<&CantLayout>,
363 start_distance: f64,
364) -> AlignmentResult<NodeId> {
365 let curve = spiral_curve(segment, name, cant, start_distance)?;
366 let basis = push(builder, GeometryNode::Curve2(curve))?;
367 push(
368 builder,
369 GeometryNode::CurveRelation(CurveRelation::Trimmed {
370 basis,
371 start: vec![TrimSelector::Parameter(0.0)],
372 end: vec![TrimSelector::Parameter(segment.segment_length)],
373 sense_agreement: true,
374 preference: TrimmingPreference::Parameter,
375 }),
376 )
377}
378
379fn no_geometry(entity: EntityId) -> AlignmentError {
382 AlignmentError::InvalidSegment {
383 entity,
384 detail: "a zero-length segment has no geometry to lower; IFC4.3 allows it only as the \
385 closing segment of a layout",
386 }
387}
388
389fn push(builder: &mut GeometryGraphBuilder, node: GeometryNode) -> AlignmentResult<NodeId> {
390 builder.push(node).map_err(|error| AlignmentError::Graph {
391 detail: error.to_string(),
392 })
393}
394
395pub(super) fn finish(
396 builder: GeometryGraphBuilder,
397 root: NodeId,
398 sources: Vec<EntityId>,
399) -> AlignmentResult<LoweredAlignmentCurve> {
400 let graph = builder
401 .finish(vec![root])
402 .map_err(|error| AlignmentError::Graph {
403 detail: error.to_string(),
404 })?;
405 Ok(LoweredAlignmentCurve {
406 graph,
407 root,
408 sources,
409 seams: Vec::new(),
410 })
411}
412
413pub fn lower_horizontal_layout(
427 model: &Model,
428 entity: EntityId,
429 units: AlignmentUnits,
430 cant: Option<&CantLayout>,
431) -> AlignmentResult<LoweredAlignmentCurve> {
432 let view = AlignmentView::for_model(model)?;
433 let horizontal_entity = model
434 .get(entity)
435 .ok_or(AlignmentError::MissingEntity { entity })?;
436 if !view
437 .schema
438 .is_a(&horizontal_entity.type_name, "IfcAlignmentHorizontal")
439 {
440 return Err(AlignmentError::WrongType {
441 entity,
442 expected: "IfcAlignmentHorizontal",
443 actual: horizontal_entity.type_name.to_string(),
444 });
445 }
446 let ids = view.segment_chain(entity, "IfcAlignmentHorizontalSegment")?;
447 if ids.is_empty() {
448 return Err(AlignmentError::SemanticViolation {
449 entity: Some(entity),
450 rule: "IfcAlignmentHorizontal must nest at least one IfcAlignmentSegment",
451 });
452 }
453
454 let mut segments = Vec::with_capacity(ids.len());
455 for id in &ids {
456 segments.push(read_horizontal_segment(model, *id, units)?);
457 }
458
459 let (body, closing) = split_closing(&segments, |s| s.segment_length, |s| s.entity)?;
460
461 let mut builder = GeometryGraphBuilder::new();
462 let mut composite_segments = Vec::with_capacity(body.len());
463 let mut seams = Vec::with_capacity(segments.len().saturating_sub(1));
464 let mut station = 0.0_f64;
465 for (index, segment) in body.iter().enumerate() {
466 let curve = match &segment.segment_type {
467 HorizontalSegmentType::Line => push_line(&mut builder, segment)?,
468 HorizontalSegmentType::CircularArc => push_arc(&mut builder, segment)?,
469 HorizontalSegmentType::Transition(name)
470 if is_exactly_lowerable(name, cant.is_some()) =>
471 {
472 push_spiral(&mut builder, segment, name, cant, station)?
473 }
474 _ => return Err(refuse_unlowerable(segment)),
475 };
476 let transition = if index == 0 {
477 Transition::Discontinuous
478 } else {
479 let seam = check_position(&body[index - 1], segment, station)?;
480 let transition = seam_transition(&seam);
481 seams.push(seam);
482 transition
483 };
484 composite_segments.push(CurveSegment {
485 curve,
486 same_sense: true,
487 transition,
488 });
489 station += segment.segment_length;
490 }
491 if let (Some(closing), Some(last)) = (closing, body.last()) {
494 seams.push(check_position(last, closing, station)?);
495 }
496
497 let root = push(
498 &mut builder,
499 GeometryNode::CurveRelation(CurveRelation::Composite {
500 segments: composite_segments,
501 }),
502 )?;
503 let mut lowered = finish(builder, root, ids)?;
504 lowered.seams = seams;
505 Ok(lowered)
506}
507
508pub fn lower_horizontal_layout_partial(
531 model: &Model,
532 entity: EntityId,
533 units: AlignmentUnits,
534 cant: Option<&CantLayout>,
535) -> AlignmentResult<PartialHorizontalLayout> {
536 let view = AlignmentView::for_model(model)?;
537 let horizontal_entity = model
538 .get(entity)
539 .ok_or(AlignmentError::MissingEntity { entity })?;
540 if !view
541 .schema
542 .is_a(&horizontal_entity.type_name, "IfcAlignmentHorizontal")
543 {
544 return Err(AlignmentError::WrongType {
545 entity,
546 expected: "IfcAlignmentHorizontal",
547 actual: horizontal_entity.type_name.to_string(),
548 });
549 }
550 let ids = view.segment_chain(entity, "IfcAlignmentHorizontalSegment")?;
551 if ids.is_empty() {
552 return Err(AlignmentError::SemanticViolation {
553 entity: Some(entity),
554 rule: "IfcAlignmentHorizontal must nest at least one IfcAlignmentSegment",
555 });
556 }
557
558 let mut segments = Vec::with_capacity(ids.len());
559 for id in &ids {
560 segments.push(read_horizontal_segment(model, *id, units)?);
561 }
562
563 let (body, closing) = split_closing(&segments, |s| s.segment_length, |s| s.entity)?;
564
565 let mut runs = Vec::new();
566 let mut refused = Vec::new();
567 let mut builder = GeometryGraphBuilder::new();
570 let mut pending: Vec<(usize, CurveSegment)> = Vec::new();
571 let mut pending_ids: Vec<EntityId> = Vec::new();
572 let mut pending_seams: Vec<HorizontalSeam> = Vec::new();
573 let mut station = 0.0_f64;
574
575 for (index, segment) in body.iter().enumerate() {
576 let lowered = match &segment.segment_type {
577 HorizontalSegmentType::Line => push_line(&mut builder, segment),
578 HorizontalSegmentType::CircularArc => push_arc(&mut builder, segment),
579 HorizontalSegmentType::Transition(name)
580 if is_exactly_lowerable(name, cant.is_some()) =>
581 {
582 push_spiral(&mut builder, segment, name, cant, station)
583 }
584 _ => Err(refuse_unlowerable(segment)),
585 };
586 let start_station = station;
587 station += segment.segment_length;
590 let curve = match lowered {
591 Ok(curve) => curve,
592 Err(reason) => {
593 flush_run(
596 &mut runs,
597 &mut builder,
598 &mut pending,
599 &mut pending_ids,
600 &mut pending_seams,
601 )?;
602 refused.push(RefusedSegment {
603 entity: segment.entity,
604 type_name: segment.segment_type.source_name().to_owned(),
605 reason,
606 });
607 continue;
608 }
609 };
610 let transition = match pending.last() {
613 None => Transition::Discontinuous,
614 Some((previous_index, _)) => {
615 let seam = check_position(&body[*previous_index], segment, start_station)?;
616 let transition = seam_transition(&seam);
617 pending_seams.push(seam);
618 transition
619 }
620 };
621 pending.push((
622 index,
623 CurveSegment {
624 curve,
625 same_sense: true,
626 transition,
627 },
628 ));
629 pending_ids.push(segment.entity);
630 }
631 if let (Some(closing), Some((previous_index, _))) = (closing, pending.last()) {
634 pending_seams.push(check_position(&body[*previous_index], closing, station)?);
635 }
636 flush_run(
637 &mut runs,
638 &mut builder,
639 &mut pending,
640 &mut pending_ids,
641 &mut pending_seams,
642 )?;
643
644 Ok(PartialHorizontalLayout {
645 runs,
646 refused,
647 segment_count: segments.len(),
648 })
649}
650
651fn flush_run(
656 runs: &mut Vec<LoweredAlignmentCurve>,
657 builder: &mut GeometryGraphBuilder,
658 pending: &mut Vec<(usize, CurveSegment)>,
659 pending_ids: &mut Vec<EntityId>,
660 pending_seams: &mut Vec<HorizontalSeam>,
661) -> AlignmentResult<()> {
662 if pending.is_empty() {
663 *builder = GeometryGraphBuilder::new();
666 pending_ids.clear();
667 pending_seams.clear();
668 return Ok(());
669 }
670 let mut finished = GeometryGraphBuilder::new();
671 core::mem::swap(builder, &mut finished);
672 let composite_segments: Vec<CurveSegment> =
673 pending.drain(..).map(|(_, segment)| segment).collect();
674 let root = push(
675 &mut finished,
676 GeometryNode::CurveRelation(CurveRelation::Composite {
677 segments: composite_segments,
678 }),
679 )?;
680 let sources = core::mem::take(pending_ids);
681 let mut run = finish(finished, root, sources)?;
682 run.seams = core::mem::take(pending_seams);
683 runs.push(run);
684 Ok(())
685}