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