Skip to main content

ifc_geometry/authoring/
swept.rs

1//! The swept solids beyond plain extrusion and revolution.
2//!
3//! Three shapes of sweep, and the slot layouts do not generalise:
4//!
5//! - **Tapered** extrusion and revolution add `EndSweptArea`, so the
6//!   profile changes along the sweep. Both profiles are references and
7//!   neither is evaluated here.
8//! - **Directrix-driven** sweeps (`IfcSurfaceCurveSweptAreaSolid`,
9//!   `IfcFixedReferenceSweptAreaSolid`) share slots 0-4 and differ only
10//!   at slot 5, where one names a reference surface and the other a
11//!   fixed direction.
12//! - **`IfcSweptDiskSolid`** subtypes `IfcSolidModel` *directly*. It has
13//!   no inherited `SweptArea`, so `Directrix` is slot 0, not slot 2.
14//!   That break in the family resemblance is the easiest thing to get
15//!   wrong here.
16//!
17//! The one invariant worth enforcing is the disk solid's
18//! `InnerRadius < Radius`: a hollow tube whose bore is wider than the
19//! tube is not a solid, and that is arithmetic rather than geometry.
20
21use ifc_model::{Entity, EntityId, Transaction, Value};
22
23use crate::error::GeometryError;
24use crate::solid::swept::{
25    directrix_slot, disk_slot, extruded_slot, revolved_slot, swept_area_slot,
26};
27
28use super::std_profile::positive;
29use super::{invalid, refs, require_finite};
30
31/// Optional trim parameters shared by the directrix-driven sweeps.
32///
33/// Absent means the sweep runs the whole directrix. Both are
34/// `IfcParameterValue`, so they carry their measure type when written.
35#[derive(Debug, Default, Clone, Copy)]
36pub struct SweepTrim {
37    /// `StartParam`.
38    pub start: Option<f64>,
39    /// `EndParam`.
40    pub end: Option<f64>,
41}
42
43/// Write an optional `IfcParameterValue` into its slot.
44fn put_param(
45    attrs: &mut [Value],
46    index: usize,
47    value: Option<f64>,
48    type_name: &'static str,
49    attribute: &'static str,
50) -> Result<(), GeometryError> {
51    if let Some(value) = value {
52        require_finite(type_name, attribute, &[value])?;
53        attrs[index] = Value::Typed {
54            type_name: "IFCPARAMETERVALUE".into(),
55            value: Box::new(Value::Real(value)),
56        };
57    }
58    Ok(())
59}
60
61/// Stage an `IfcExtrudedAreaSolidTapered`.
62///
63/// # Errors
64///
65/// Refuses a non-positive or non-finite depth.
66pub fn extruded_area_solid_tapered(
67    tx: &mut Transaction,
68    swept_area: EntityId,
69    position: Option<EntityId>,
70    extruded_direction: EntityId,
71    depth: f64,
72    end_swept_area: EntityId,
73) -> Result<EntityId, GeometryError> {
74    const T: &str = "IFCEXTRUDEDAREASOLIDTAPERED";
75    positive(T, "Depth", depth)?;
76    let mut attrs = vec![Value::Null; 5];
77    attrs[swept_area_slot::SWEPT_AREA] = Value::Ref(swept_area);
78    attrs[swept_area_slot::POSITION] = position.map_or(Value::Null, Value::Ref);
79    attrs[extruded_slot::EXTRUDED_DIRECTION] = Value::Ref(extruded_direction);
80    attrs[extruded_slot::DEPTH] = Value::Real(depth);
81    attrs[extruded_slot::END_SWEPT_AREA] = Value::Ref(end_swept_area);
82    Ok(tx.create(Entity::new(T, attrs)))
83}
84
85/// Stage an `IfcRevolvedAreaSolidTapered`.
86///
87/// `angle` is in the model's plane-angle unit, very often degrees. The
88/// writer does not convert: it has no unit context and inventing one
89/// would silently rescale the solid.
90///
91/// # Errors
92///
93/// Refuses a non-finite angle.
94pub fn revolved_area_solid_tapered(
95    tx: &mut Transaction,
96    swept_area: EntityId,
97    position: Option<EntityId>,
98    axis: EntityId,
99    angle: f64,
100    end_swept_area: EntityId,
101) -> Result<EntityId, GeometryError> {
102    const T: &str = "IFCREVOLVEDAREASOLIDTAPERED";
103    require_finite(T, "Angle", &[angle])?;
104    let mut attrs = vec![Value::Null; 5];
105    attrs[swept_area_slot::SWEPT_AREA] = Value::Ref(swept_area);
106    attrs[swept_area_slot::POSITION] = position.map_or(Value::Null, Value::Ref);
107    attrs[revolved_slot::AXIS] = Value::Ref(axis);
108    attrs[revolved_slot::ANGLE] = Value::Real(angle);
109    attrs[revolved_slot::END_SWEPT_AREA] = Value::Ref(end_swept_area);
110    Ok(tx.create(Entity::new(T, attrs)))
111}
112
113/// Stage an `IfcSurfaceCurveSweptAreaSolid`.
114///
115/// The profile is swept along `directrix` while staying on
116/// `reference_surface`, which is what fixes its orientation.
117///
118/// # Errors
119///
120/// Refuses a non-finite trim parameter.
121pub fn surface_curve_swept_area_solid(
122    tx: &mut Transaction,
123    swept_area: EntityId,
124    position: Option<EntityId>,
125    directrix: EntityId,
126    trim: SweepTrim,
127    reference_surface: EntityId,
128) -> Result<EntityId, GeometryError> {
129    const T: &str = "IFCSURFACECURVESWEPTAREASOLID";
130    let mut attrs = directrix_attrs(T, swept_area, position, directrix, trim)?;
131    attrs[directrix_slot::REFERENCE_SURFACE] = Value::Ref(reference_surface);
132    Ok(tx.create(Entity::new(T, attrs)))
133}
134
135/// Stage an `IfcFixedReferenceSweptAreaSolid`.
136///
137/// Like the surface-curve sweep, but the profile's orientation is fixed
138/// by a direction rather than by a surface.
139///
140/// # Errors
141///
142/// Refuses a non-finite trim parameter.
143pub fn fixed_reference_swept_area_solid(
144    tx: &mut Transaction,
145    swept_area: EntityId,
146    position: Option<EntityId>,
147    directrix: EntityId,
148    trim: SweepTrim,
149    fixed_reference: EntityId,
150) -> Result<EntityId, GeometryError> {
151    const T: &str = "IFCFIXEDREFERENCESWEPTAREASOLID";
152    let mut attrs = directrix_attrs(T, swept_area, position, directrix, trim)?;
153    attrs[directrix_slot::FIXED_REFERENCE] = Value::Ref(fixed_reference);
154    Ok(tx.create(Entity::new(T, attrs)))
155}
156
157/// The six slots the two directrix-driven sweeps share.
158///
159/// Slot 5 is left for the caller: it is `ReferenceSurface` on one
160/// subtype and `FixedReference` on the other.
161fn directrix_attrs(
162    type_name: &'static str,
163    swept_area: EntityId,
164    position: Option<EntityId>,
165    directrix: EntityId,
166    trim: SweepTrim,
167) -> Result<Vec<Value>, GeometryError> {
168    let mut attrs = vec![Value::Null; 6];
169    attrs[swept_area_slot::SWEPT_AREA] = Value::Ref(swept_area);
170    attrs[swept_area_slot::POSITION] = position.map_or(Value::Null, Value::Ref);
171    attrs[directrix_slot::DIRECTRIX] = Value::Ref(directrix);
172    put_param(
173        &mut attrs,
174        directrix_slot::START_PARAM,
175        trim.start,
176        type_name,
177        "StartParam",
178    )?;
179    put_param(
180        &mut attrs,
181        directrix_slot::END_PARAM,
182        trim.end,
183        type_name,
184        "EndParam",
185    )?;
186    Ok(attrs)
187}
188
189/// Stage an `IfcSweptDiskSolid`: a disk swept along a curve.
190///
191/// Note the slot layout: this subtypes `IfcSolidModel` directly, so
192/// `Directrix` is slot 0 and there is no `SweptArea` or `Position`.
193///
194/// # Errors
195///
196/// Refuses a non-positive radius, a non-positive inner radius, a
197/// non-finite trim parameter, or an inner radius that is not smaller
198/// than the outer one -- a bore wider than its tube leaves no solid.
199pub fn swept_disk_solid(
200    tx: &mut Transaction,
201    directrix: EntityId,
202    radius: f64,
203    inner_radius: Option<f64>,
204    trim: SweepTrim,
205) -> Result<EntityId, GeometryError> {
206    const T: &str = "IFCSWEPTDISKSOLID";
207    let attrs = disk_attrs(T, directrix, radius, inner_radius, trim)?;
208    Ok(tx.create(Entity::new(T, attrs)))
209}
210
211/// Stage an `IfcSweptDiskSolidPolygonal`.
212///
213/// The polygonal form approximates the directrix with straight
214/// segments; `fillet_radius` rounds the joints between them.
215///
216/// # Errors
217///
218/// Everything [`swept_disk_solid`] refuses, plus a negative fillet
219/// radius. Zero is legal: `IfcNonNegativeLengthMeasure` means a sharp
220/// joint, not an error.
221pub fn swept_disk_solid_polygonal(
222    tx: &mut Transaction,
223    directrix: EntityId,
224    radius: f64,
225    inner_radius: Option<f64>,
226    trim: SweepTrim,
227    fillet_radius: Option<f64>,
228) -> Result<EntityId, GeometryError> {
229    const T: &str = "IFCSWEPTDISKSOLIDPOLYGONAL";
230    let mut attrs = disk_attrs(T, directrix, radius, inner_radius, trim)?;
231    attrs.push(Value::Null);
232    if let Some(fillet) = fillet_radius {
233        require_finite(T, "FilletRadius", &[fillet])?;
234        if fillet < 0.0 {
235            return Err(invalid(
236                T,
237                "FilletRadius",
238                format!("expected a non-negative length, got {fillet}"),
239            ));
240        }
241        attrs[disk_slot::FILLET_RADIUS] = Value::Real(fillet);
242    }
243    Ok(tx.create(Entity::new(T, attrs)))
244}
245
246/// The five slots both swept disk solids share.
247fn disk_attrs(
248    type_name: &'static str,
249    directrix: EntityId,
250    radius: f64,
251    inner_radius: Option<f64>,
252    trim: SweepTrim,
253) -> Result<Vec<Value>, GeometryError> {
254    positive(type_name, "Radius", radius)?;
255    let mut attrs = vec![Value::Null; 5];
256    attrs[disk_slot::DIRECTRIX] = Value::Ref(directrix);
257    attrs[disk_slot::RADIUS] = Value::Real(radius);
258    if let Some(inner) = inner_radius {
259        positive(type_name, "InnerRadius", inner)?;
260        // A bore at least as wide as the tube leaves nothing solid. The
261        // schema types both as positive lengths but does not relate them,
262        // so this is the writer's to catch.
263        if inner >= radius {
264            return Err(invalid(
265                type_name,
266                "InnerRadius",
267                format!("{inner} is not below the outer radius {radius}"),
268            ));
269        }
270        attrs[disk_slot::INNER_RADIUS] = Value::Real(inner);
271    }
272    put_param(
273        &mut attrs,
274        disk_slot::START_PARAM,
275        trim.start,
276        type_name,
277        "StartParam",
278    )?;
279    put_param(
280        &mut attrs,
281        disk_slot::END_PARAM,
282        trim.end,
283        type_name,
284        "EndParam",
285    )?;
286    Ok(attrs)
287}
288
289/// Stage an `IfcSurfaceOfLinearExtrusion`: a surface, not a solid.
290///
291/// `SweptCurve` is an `IfcProfileDef`, and `Depth` is a plain
292/// `IfcLengthMeasure` here -- negative extrusion is legal, unlike the
293/// solid form which requires a positive depth.
294///
295/// # Errors
296///
297/// Refuses a non-finite depth.
298pub fn surface_of_linear_extrusion(
299    tx: &mut Transaction,
300    swept_curve: EntityId,
301    position: Option<EntityId>,
302    extruded_direction: EntityId,
303    depth: f64,
304) -> Result<EntityId, GeometryError> {
305    const T: &str = "IFCSURFACEOFLINEAREXTRUSION";
306    require_finite(T, "Depth", &[depth])?;
307    let attrs = vec![
308        Value::Ref(swept_curve),
309        position.map_or(Value::Null, Value::Ref),
310        Value::Ref(extruded_direction),
311        Value::Real(depth),
312    ];
313    Ok(tx.create(Entity::new(T, attrs)))
314}
315
316/// Stage an `IfcSurfaceOfRevolution`.
317///
318/// There is no angle: the surface is the full revolution of the swept
319/// curve about `axis_position`.
320pub fn surface_of_revolution(
321    tx: &mut Transaction,
322    swept_curve: EntityId,
323    position: Option<EntityId>,
324    axis_position: EntityId,
325) -> EntityId {
326    let attrs = vec![
327        Value::Ref(swept_curve),
328        position.map_or(Value::Null, Value::Ref),
329        Value::Ref(axis_position),
330    ];
331    tx.create(Entity::new("IFCSURFACEOFREVOLUTION", attrs))
332}
333
334/// Stage an `IfcPlane`.
335pub fn plane(tx: &mut Transaction, position: EntityId) -> EntityId {
336    tx.create(Entity::new("IFCPLANE", vec![Value::Ref(position)]))
337}
338
339/// Stage an `IfcCylindricalSurface`.
340///
341/// # Errors
342///
343/// Refuses a non-positive or non-finite radius.
344pub fn cylindrical_surface(
345    tx: &mut Transaction,
346    position: EntityId,
347    radius: f64,
348) -> Result<EntityId, GeometryError> {
349    const T: &str = "IFCCYLINDRICALSURFACE";
350    positive(T, "Radius", radius)?;
351    let attrs = vec![Value::Ref(position), Value::Real(radius)];
352    Ok(tx.create(Entity::new(T, attrs)))
353}
354
355/// Stage an `IfcAxis1Placement`: a point and an optional axis direction.
356///
357/// Revolutions need one, and it is the only placement form the other
358/// authoring modules do not already cover.
359pub fn axis1_placement(
360    tx: &mut Transaction,
361    location: EntityId,
362    axis: Option<EntityId>,
363) -> EntityId {
364    let attrs = vec![Value::Ref(location), axis.map_or(Value::Null, Value::Ref)];
365    tx.create(Entity::new("IFCAXIS1PLACEMENT", attrs))
366}
367
368/// Stage an `IfcDirectrixDerivedReferenceSweptAreaSolid`.
369///
370/// Shares the fixed-reference layout exactly, but the reference is
371/// *derived from* the directrix rather than held constant: the profile
372/// rotates with the curve as it sweeps. Same six slots, different
373/// meaning, so it is its own entity rather than a flag.
374///
375/// # Errors
376///
377/// Refuses a non-finite trim parameter.
378pub fn directrix_derived_reference_swept_area_solid(
379    tx: &mut Transaction,
380    swept_area: EntityId,
381    position: Option<EntityId>,
382    directrix: EntityId,
383    trim: SweepTrim,
384    fixed_reference: EntityId,
385) -> Result<EntityId, GeometryError> {
386    const T: &str = "IFCDIRECTRIXDERIVEDREFERENCESWEPTAREASOLID";
387    let mut attrs = directrix_attrs(T, swept_area, position, directrix, trim)?;
388    attrs[directrix_slot::FIXED_REFERENCE] = Value::Ref(fixed_reference);
389    Ok(tx.create(Entity::new(T, attrs)))
390}
391
392/// Which sectioned entity to stage.
393///
394/// The two carry the same three attributes but in *different slot
395/// order*: the solid is Directrix, CrossSections, CrossSectionPositions
396/// while the surface is Directrix, CrossSectionPositions, CrossSections.
397/// Writing one layout under the other type name produces a record that
398/// parses and is wrong, so the order is selected here rather than left
399/// to the caller.
400#[derive(Debug, Clone, Copy, PartialEq, Eq)]
401pub enum SectionedKind {
402    /// `IfcSectionedSolidHorizontal`.
403    SolidHorizontal,
404    /// `IfcSectionedSurface`.
405    Surface,
406}
407
408/// Stage an `IfcSectionedSolidHorizontal` or `IfcSectionedSurface`.
409///
410/// Both sweep a series of cross-sections along a directrix, each
411/// positioned by an `IfcAxis2PlacementLinear`. Both require at least
412/// two sections and one position per section.
413///
414/// # Errors
415///
416/// Refuses fewer than two cross-sections or positions (`LIST [2:?]`)
417/// and a count mismatch between them
418/// (`CorrespondingSectionPositions`).
419pub fn sectioned(
420    tx: &mut Transaction,
421    kind: SectionedKind,
422    directrix: EntityId,
423    cross_sections: &[EntityId],
424    cross_section_positions: &[EntityId],
425) -> Result<EntityId, GeometryError> {
426    let entity = match kind {
427        SectionedKind::SolidHorizontal => "IFCSECTIONEDSOLIDHORIZONTAL",
428        SectionedKind::Surface => "IFCSECTIONEDSURFACE",
429    };
430    if cross_sections.len() < 2 {
431        return Err(invalid(entity, "CrossSections", "expected LIST [2:?]"));
432    }
433    if cross_section_positions.len() < 2 {
434        return Err(invalid(
435            entity,
436            "CrossSectionPositions",
437            "expected LIST [2:?]",
438        ));
439    }
440    if cross_sections.len() != cross_section_positions.len() {
441        return Err(invalid(
442            entity,
443            "CrossSectionPositions",
444            format!(
445                "expected one position per section: {} sections, {} positions",
446                cross_sections.len(),
447                cross_section_positions.len()
448            ),
449        ));
450    }
451    let attrs = match kind {
452        SectionedKind::SolidHorizontal => vec![
453            Value::Ref(directrix),
454            refs(cross_sections),
455            refs(cross_section_positions),
456        ],
457        SectionedKind::Surface => vec![
458            Value::Ref(directrix),
459            refs(cross_section_positions),
460            refs(cross_sections),
461        ],
462    };
463    Ok(tx.create(Entity::new(entity, attrs)))
464}