ifc-geometry 0.4.2

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
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
//! 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.

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::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
/// `IfcParameterValue`, so they carry their measure type when written.
#[derive(Debug, Default, Clone, Copy)]
pub struct SweepTrim {
    /// `StartParam`.
    pub start: Option<f64>,
    /// `EndParam`.
    pub end: Option<f64>,
}

/// Write an optional `IfcParameterValue` into its slot.
fn put_param(
    attrs: &mut [Value],
    index: usize,
    value: Option<f64>,
    type_name: &'static str,
    attribute: &'static str,
) -> Result<(), GeometryError> {
    if let Some(value) = value {
        require_finite(type_name, attribute, &[value])?;
        attrs[index] = 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)))
}

/// Stage an `IfcSurfaceCurveSweptAreaSolid`.
///
/// The profile is swept along `directrix` while staying on
/// `reference_surface`, which is what fixes its orientation.
///
/// # Errors
///
/// Refuses a non-finite trim parameter.
pub fn surface_curve_swept_area_solid(
    tx: &mut Transaction,
    swept_area: EntityId,
    position: Option<EntityId>,
    directrix: EntityId,
    trim: SweepTrim,
    reference_surface: EntityId,
) -> Result<EntityId, GeometryError> {
    const T: &str = "IFCSURFACECURVESWEPTAREASOLID";
    let mut attrs = directrix_attrs(T, swept_area, position, directrix, trim)?;
    attrs[directrix_slot::REFERENCE_SURFACE] = Value::Ref(reference_surface);
    Ok(tx.create(Entity::new(T, attrs)))
}

/// Stage an `IfcFixedReferenceSweptAreaSolid`.
///
/// Like the surface-curve sweep, but the profile's orientation is fixed
/// by a direction rather than by a surface.
///
/// # Errors
///
/// Refuses a non-finite trim parameter.
pub fn fixed_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 = "IFCFIXEDREFERENCESWEPTAREASOLID";
    let mut attrs = directrix_attrs(T, swept_area, position, directrix, trim)?;
    attrs[directrix_slot::FIXED_REFERENCE] = Value::Ref(fixed_reference);
    Ok(tx.create(Entity::new(T, attrs)))
}

/// The six slots the two directrix-driven sweeps share.
///
/// Slot 5 is left for the caller: it is `ReferenceSurface` on one
/// subtype and `FixedReference` on the other.
fn directrix_attrs(
    type_name: &'static str,
    swept_area: EntityId,
    position: Option<EntityId>,
    directrix: EntityId,
    trim: SweepTrim,
) -> 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,
        type_name,
        "StartParam",
    )?;
    put_param(
        &mut attrs,
        directrix_slot::END_PARAM,
        trim.end,
        type_name,
        "EndParam",
    )?;
    Ok(attrs)
}

/// Stage an `IfcSweptDiskSolid`: a disk swept along a curve.
///
/// Note the slot layout: this subtypes `IfcSolidModel` directly, so
/// `Directrix` is slot 0 and there is no `SweptArea` or `Position`.
///
/// # Errors
///
/// Refuses a non-positive radius, a non-positive inner radius, a
/// non-finite trim parameter, or an inner radius that is not smaller
/// than the outer one -- a bore wider than its tube leaves no solid.
pub fn swept_disk_solid(
    tx: &mut Transaction,
    directrix: EntityId,
    radius: f64,
    inner_radius: Option<f64>,
    trim: SweepTrim,
) -> Result<EntityId, GeometryError> {
    const T: &str = "IFCSWEPTDISKSOLID";
    let attrs = disk_attrs(T, directrix, radius, inner_radius, trim)?;
    Ok(tx.create(Entity::new(T, attrs)))
}

/// Stage an `IfcSweptDiskSolidPolygonal`.
///
/// The polygonal form approximates the directrix with straight
/// segments; `fillet_radius` rounds the joints between them.
///
/// # Errors
///
/// Everything [`swept_disk_solid`] refuses, 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)?;
    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.
fn disk_attrs(
    type_name: &'static str,
    directrix: EntityId,
    radius: f64,
    inner_radius: Option<f64>,
    trim: SweepTrim,
) -> 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);
    }
    put_param(
        &mut attrs,
        disk_slot::START_PARAM,
        trim.start,
        type_name,
        "StartParam",
    )?;
    put_param(
        &mut attrs,
        disk_slot::END_PARAM,
        trim.end,
        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.
///
/// # 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)?;
    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)))
}