ifc-geometry 0.5.0

IFC semantic views lowered into the format-neutral geometry DAG.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
//! The swept solids beyond plain extrusion and revolution.
//!
//! Three shapes of sweep, and the slot layouts do not generalise:
//!
//! - **Tapered** extrusion and revolution add `EndSweptArea`, so the
//!   profile changes along the sweep. Both profiles are references and
//!   neither is evaluated here.
//! - **Directrix-driven** sweeps (`IfcSurfaceCurveSweptAreaSolid`,
//!   `IfcFixedReferenceSweptAreaSolid`) share slots 0-4 and differ only
//!   at slot 5, where one names a reference surface and the other a
//!   fixed direction.
//! - **`IfcSweptDiskSolid`** subtypes `IfcSolidModel` *directly*. It has
//!   no inherited `SweptArea`, so `Directrix` is slot 0, not slot 2.
//!   That break in the family resemblance is the easiest thing to get
//!   wrong here.
//!
//! The one invariant worth enforcing is the disk solid's
//! `InnerRadius < Radius`: a hollow tube whose bore is wider than the
//! tube is not a solid, and that is arithmetic rather than geometry.
//!
//! The directrix sweeps and the plain swept disk are written by the
//! release-bound writers in `swept_in.rs`, because their trim parameters
//! change form or optionality between releases (#200, #210). This module
//! keeps the shared slot layouts and the writers whose record is the same
//! in every release that declares the entity.

use ifc_model::{Entity, EntityId, Transaction, Value};

use crate::error::GeometryError;
use crate::solid::swept::{
    directrix_slot, disk_slot, extruded_slot, revolved_slot, swept_area_slot,
};

use super::release::Release;
use super::std_profile::positive;
use super::{invalid, refs, require_finite};

/// Optional trim parameters shared by the directrix-driven sweeps.
///
/// Absent means the sweep runs the whole directrix. Both are parameter
/// values on the directrix. How they are written depends on the declared
/// type of the slot (#200): bare where it is `IfcParameterValue` (the swept
/// disks in every release, the other sweeps in IFC2X3 and IFC4), and
/// `IFCPARAMETERVALUE(..)` where it is the IFC4X3 SELECT
/// `IfcCurveMeasureSelect`. Whether absent is allowed depends on the release
/// too: IFC2X3 requires both on every sweep that declares them, which is why
/// the writers of those sweeps take the model (#210).
#[derive(Debug, Default, Clone, Copy)]
pub struct SweepTrim {
    /// `StartParam`.
    pub start: Option<f64>,
    /// `EndParam`.
    pub end: Option<f64>,
}

/// How a trim parameter is written.
#[derive(Debug, Clone, Copy)]
pub(super) enum ParamForm {
    /// The slot is declared `IfcParameterValue`, a defined type: bare.
    Bare,
    /// The slot is declared `IfcCurveMeasureSelect`, a SELECT: typed.
    Select,
    /// The form the model's declared release requires.
    Release(Release),
}

/// Write an optional trim parameter into its slot in `form`.
fn put_param(
    attrs: &mut [Value],
    index: usize,
    value: Option<f64>,
    form: ParamForm,
    type_name: &'static str,
    attribute: &'static str,
) -> Result<(), GeometryError> {
    if let Some(value) = value {
        require_finite(type_name, attribute, &[value])?;
    }
    attrs[index] = match (form, value) {
        (ParamForm::Release(release), value) => release.parameter(type_name, attribute, value)?,
        (_, None) => Value::Null,
        (ParamForm::Bare, Some(value)) => Value::Real(value),
        (ParamForm::Select, Some(value)) => Value::Typed {
            type_name: "IFCPARAMETERVALUE".into(),
            value: Box::new(Value::Real(value)),
        },
    };
    Ok(())
}

/// Stage an `IfcExtrudedAreaSolidTapered`.
///
/// # Errors
///
/// Refuses a non-positive or non-finite depth.
pub fn extruded_area_solid_tapered(
    tx: &mut Transaction,
    swept_area: EntityId,
    position: Option<EntityId>,
    extruded_direction: EntityId,
    depth: f64,
    end_swept_area: EntityId,
) -> Result<EntityId, GeometryError> {
    const T: &str = "IFCEXTRUDEDAREASOLIDTAPERED";
    positive(T, "Depth", depth)?;
    let mut attrs = vec![Value::Null; 5];
    attrs[swept_area_slot::SWEPT_AREA] = Value::Ref(swept_area);
    attrs[swept_area_slot::POSITION] = position.map_or(Value::Null, Value::Ref);
    attrs[extruded_slot::EXTRUDED_DIRECTION] = Value::Ref(extruded_direction);
    attrs[extruded_slot::DEPTH] = Value::Real(depth);
    attrs[extruded_slot::END_SWEPT_AREA] = Value::Ref(end_swept_area);
    Ok(tx.create(Entity::new(T, attrs)))
}

/// Stage an `IfcRevolvedAreaSolidTapered`.
///
/// `angle` is in the model's plane-angle unit, very often degrees. The
/// writer does not convert: it has no unit context and inventing one
/// would silently rescale the solid.
///
/// # Errors
///
/// Refuses a non-finite angle.
pub fn revolved_area_solid_tapered(
    tx: &mut Transaction,
    swept_area: EntityId,
    position: Option<EntityId>,
    axis: EntityId,
    angle: f64,
    end_swept_area: EntityId,
) -> Result<EntityId, GeometryError> {
    const T: &str = "IFCREVOLVEDAREASOLIDTAPERED";
    require_finite(T, "Angle", &[angle])?;
    let mut attrs = vec![Value::Null; 5];
    attrs[swept_area_slot::SWEPT_AREA] = Value::Ref(swept_area);
    attrs[swept_area_slot::POSITION] = position.map_or(Value::Null, Value::Ref);
    attrs[revolved_slot::AXIS] = Value::Ref(axis);
    attrs[revolved_slot::ANGLE] = Value::Real(angle);
    attrs[revolved_slot::END_SWEPT_AREA] = Value::Ref(end_swept_area);
    Ok(tx.create(Entity::new(T, attrs)))
}

/// The six slots the directrix-driven sweeps share, trim parameters in
/// `form`.
///
/// Slot 5 is left for the caller: it is `ReferenceSurface` on one
/// subtype and `FixedReference` on the others.
pub(super) fn directrix_attrs(
    type_name: &'static str,
    swept_area: EntityId,
    position: Option<EntityId>,
    directrix: EntityId,
    trim: SweepTrim,
    form: ParamForm,
) -> Result<Vec<Value>, GeometryError> {
    let mut attrs = vec![Value::Null; 6];
    attrs[swept_area_slot::SWEPT_AREA] = Value::Ref(swept_area);
    attrs[swept_area_slot::POSITION] = position.map_or(Value::Null, Value::Ref);
    attrs[directrix_slot::DIRECTRIX] = Value::Ref(directrix);
    put_param(
        &mut attrs,
        directrix_slot::START_PARAM,
        trim.start,
        form,
        type_name,
        "StartParam",
    )?;
    put_param(
        &mut attrs,
        directrix_slot::END_PARAM,
        trim.end,
        form,
        type_name,
        "EndParam",
    )?;
    Ok(attrs)
}

/// Stage an `IfcSweptDiskSolidPolygonal`.
///
/// The polygonal form approximates the directrix with straight
/// segments; `fillet_radius` rounds the joints between them.
///
/// Only IFC4 and later declare it, and there `StartParam`/`EndParam` are
/// `OPTIONAL IfcParameterValue` in every release, so the record does not
/// depend on the release and this writer needs no model.
///
/// # Errors
///
/// Everything [`swept_disk_solid_in`](super::swept_disk_solid_in) refuses
/// for the radii and trim values, plus a negative fillet radius. Zero is
/// legal: `IfcNonNegativeLengthMeasure` means a sharp joint, not an error.
pub fn swept_disk_solid_polygonal(
    tx: &mut Transaction,
    directrix: EntityId,
    radius: f64,
    inner_radius: Option<f64>,
    trim: SweepTrim,
    fillet_radius: Option<f64>,
) -> Result<EntityId, GeometryError> {
    const T: &str = "IFCSWEPTDISKSOLIDPOLYGONAL";
    let mut attrs = disk_attrs(T, directrix, radius, inner_radius, trim, ParamForm::Bare)?;
    attrs.push(Value::Null);
    if let Some(fillet) = fillet_radius {
        require_finite(T, "FilletRadius", &[fillet])?;
        if fillet < 0.0 {
            return Err(invalid(
                T,
                "FilletRadius",
                format!("expected a non-negative length, got {fillet}"),
            ));
        }
        attrs[disk_slot::FILLET_RADIUS] = Value::Real(fillet);
    }
    Ok(tx.create(Entity::new(T, attrs)))
}

/// The five slots both swept disk solids share, trim parameters in `form`.
pub(super) fn disk_attrs(
    type_name: &'static str,
    directrix: EntityId,
    radius: f64,
    inner_radius: Option<f64>,
    trim: SweepTrim,
    form: ParamForm,
) -> Result<Vec<Value>, GeometryError> {
    positive(type_name, "Radius", radius)?;
    let mut attrs = vec![Value::Null; 5];
    attrs[disk_slot::DIRECTRIX] = Value::Ref(directrix);
    attrs[disk_slot::RADIUS] = Value::Real(radius);
    if let Some(inner) = inner_radius {
        positive(type_name, "InnerRadius", inner)?;
        // A bore at least as wide as the tube leaves nothing solid. The
        // schema types both as positive lengths but does not relate them,
        // so this is the writer's to catch.
        if inner >= radius {
            return Err(invalid(
                type_name,
                "InnerRadius",
                format!("{inner} is not below the outer radius {radius}"),
            ));
        }
        attrs[disk_slot::INNER_RADIUS] = Value::Real(inner);
    }
    // `IfcParameterValue` on both swept disks in every release: bare (#200).
    put_param(
        &mut attrs,
        disk_slot::START_PARAM,
        trim.start,
        form,
        type_name,
        "StartParam",
    )?;
    put_param(
        &mut attrs,
        disk_slot::END_PARAM,
        trim.end,
        form,
        type_name,
        "EndParam",
    )?;
    Ok(attrs)
}

/// Stage an `IfcSurfaceOfLinearExtrusion`: a surface, not a solid.
///
/// `SweptCurve` is an `IfcProfileDef`, and `Depth` is a plain
/// `IfcLengthMeasure` here -- negative extrusion is legal, unlike the
/// solid form which requires a positive depth.
///
/// # Errors
///
/// Refuses a non-finite depth.
pub fn surface_of_linear_extrusion(
    tx: &mut Transaction,
    swept_curve: EntityId,
    position: Option<EntityId>,
    extruded_direction: EntityId,
    depth: f64,
) -> Result<EntityId, GeometryError> {
    const T: &str = "IFCSURFACEOFLINEAREXTRUSION";
    require_finite(T, "Depth", &[depth])?;
    let attrs = vec![
        Value::Ref(swept_curve),
        position.map_or(Value::Null, Value::Ref),
        Value::Ref(extruded_direction),
        Value::Real(depth),
    ];
    Ok(tx.create(Entity::new(T, attrs)))
}

/// Stage an `IfcSurfaceOfRevolution`.
///
/// There is no angle: the surface is the full revolution of the swept
/// curve about `axis_position`.
pub fn surface_of_revolution(
    tx: &mut Transaction,
    swept_curve: EntityId,
    position: Option<EntityId>,
    axis_position: EntityId,
) -> EntityId {
    let attrs = vec![
        Value::Ref(swept_curve),
        position.map_or(Value::Null, Value::Ref),
        Value::Ref(axis_position),
    ];
    tx.create(Entity::new("IFCSURFACEOFREVOLUTION", attrs))
}

/// Stage an `IfcPlane`.
pub fn plane(tx: &mut Transaction, position: EntityId) -> EntityId {
    tx.create(Entity::new("IFCPLANE", vec![Value::Ref(position)]))
}

/// Stage an `IfcCylindricalSurface`.
///
/// # Errors
///
/// Refuses a non-positive or non-finite radius.
pub fn cylindrical_surface(
    tx: &mut Transaction,
    position: EntityId,
    radius: f64,
) -> Result<EntityId, GeometryError> {
    const T: &str = "IFCCYLINDRICALSURFACE";
    positive(T, "Radius", radius)?;
    let attrs = vec![Value::Ref(position), Value::Real(radius)];
    Ok(tx.create(Entity::new(T, attrs)))
}

/// Stage an `IfcAxis1Placement`: a point and an optional axis direction.
///
/// Revolutions need one, and it is the only placement form the other
/// authoring modules do not already cover.
pub fn axis1_placement(
    tx: &mut Transaction,
    location: EntityId,
    axis: Option<EntityId>,
) -> EntityId {
    let attrs = vec![Value::Ref(location), axis.map_or(Value::Null, Value::Ref)];
    tx.create(Entity::new("IFCAXIS1PLACEMENT", attrs))
}

/// Stage an `IfcDirectrixDerivedReferenceSweptAreaSolid`.
///
/// Shares the fixed-reference layout exactly, but the reference is
/// *derived from* the directrix rather than held constant: the profile
/// rotates with the curve as it sweeps. Same six slots, different
/// meaning, so it is its own entity rather than a flag.
///
/// Only IFC4X3 declares it, and there the trim parameters are the SELECT
/// `IfcCurveMeasureSelect`, so they are written `IFCPARAMETERVALUE(..)`.
///
/// # Errors
///
/// Refuses a non-finite trim parameter.
pub fn directrix_derived_reference_swept_area_solid(
    tx: &mut Transaction,
    swept_area: EntityId,
    position: Option<EntityId>,
    directrix: EntityId,
    trim: SweepTrim,
    fixed_reference: EntityId,
) -> Result<EntityId, GeometryError> {
    const T: &str = "IFCDIRECTRIXDERIVEDREFERENCESWEPTAREASOLID";
    let mut attrs = directrix_attrs(T, swept_area, position, directrix, trim, ParamForm::Select)?;
    attrs[directrix_slot::FIXED_REFERENCE] = Value::Ref(fixed_reference);
    Ok(tx.create(Entity::new(T, attrs)))
}

/// Which sectioned entity to stage.
///
/// The two carry the same three attributes but in *different slot
/// order*: the solid is Directrix, CrossSections, CrossSectionPositions
/// while the surface is Directrix, CrossSectionPositions, CrossSections.
/// Writing one layout under the other type name produces a record that
/// parses and is wrong, so the order is selected here rather than left
/// to the caller.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SectionedKind {
    /// `IfcSectionedSolidHorizontal`.
    SolidHorizontal,
    /// `IfcSectionedSurface`.
    Surface,
}

/// Stage an `IfcSectionedSolidHorizontal` or `IfcSectionedSurface`.
///
/// Both sweep a series of cross-sections along a directrix, each
/// positioned by an `IfcAxis2PlacementLinear`. Both require at least
/// two sections and one position per section.
///
/// # Errors
///
/// Refuses fewer than two cross-sections or positions (`LIST [2:?]`)
/// and a count mismatch between them
/// (`CorrespondingSectionPositions`).
pub fn sectioned(
    tx: &mut Transaction,
    kind: SectionedKind,
    directrix: EntityId,
    cross_sections: &[EntityId],
    cross_section_positions: &[EntityId],
) -> Result<EntityId, GeometryError> {
    let entity = match kind {
        SectionedKind::SolidHorizontal => "IFCSECTIONEDSOLIDHORIZONTAL",
        SectionedKind::Surface => "IFCSECTIONEDSURFACE",
    };
    if cross_sections.len() < 2 {
        return Err(invalid(entity, "CrossSections", "expected LIST [2:?]"));
    }
    if cross_section_positions.len() < 2 {
        return Err(invalid(
            entity,
            "CrossSectionPositions",
            "expected LIST [2:?]",
        ));
    }
    if cross_sections.len() != cross_section_positions.len() {
        return Err(invalid(
            entity,
            "CrossSectionPositions",
            format!(
                "expected one position per section: {} sections, {} positions",
                cross_sections.len(),
                cross_section_positions.len()
            ),
        ));
    }
    let attrs = match kind {
        SectionedKind::SolidHorizontal => vec![
            Value::Ref(directrix),
            refs(cross_sections),
            refs(cross_section_positions),
        ],
        SectionedKind::Surface => vec![
            Value::Ref(directrix),
            refs(cross_section_positions),
            refs(cross_sections),
        ],
    };
    Ok(tx.create(Entity::new(entity, attrs)))
}