ifc-properties 0.8.1

Property sets, quantities, and unit resolution. No geometry.
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
//! Authoring the predefined property sets and the property enumeration.

use ifc_model::{Model, Transaction, Value};
use ifc_properties::{
    add_complex_property_template, add_door_lining_properties, add_door_panel_properties,
    add_permeable_covering_properties, add_property_dependency_relationship,
    add_property_enumeration, add_window_lining_properties, add_window_panel_properties,
    DoorLiningDraft, PropertyError, WindowLiningDraft,
};
use ifc_schema::{ifc4, ifc4x3};

const GUID: &str = "0EI0MSHbX9gg8Fxwar7lb8";

fn invalid(err: &PropertyError) -> bool {
    matches!(err, PropertyError::AuthoringInvalid { .. })
}

/// The hard-coded arities match the shipped schemas.
///
/// These writers fill positional slots from constants. That is only safe
/// while the constants agree with the normative schema, so it is asked
/// directly rather than trusted.
#[test]
fn written_arities_match_the_schema() {
    const EXPECTED: &[(&str, usize)] = &[
        ("IfcDoorLiningProperties", 17),
        ("IfcWindowLiningProperties", 16),
        ("IfcDoorPanelProperties", 9),
        ("IfcWindowPanelProperties", 9),
        ("IfcPermeableCoveringProperties", 9),
        ("IfcComplexPropertyTemplate", 7),
        ("IfcPropertyDependencyRelationship", 5),
        ("IfcPropertyEnumeration", 3),
    ];
    for schema in [ifc4(), ifc4x3()] {
        for (entity, arity) in EXPECTED {
            let declared = schema.attributes(entity);
            if declared.is_empty() {
                continue;
            }
            assert_eq!(declared.len(), *arity, "{entity} in {}", schema.name());
        }
    }
}

/// WR31/WR32: a depth without its thickness describes a lining that
/// cannot be built, and both parse fine.
#[test]
fn a_door_depth_without_its_thickness_is_refused() {
    let model = Model::new();
    let mut tx = Transaction::new(&model);

    let err = add_door_lining_properties(&mut tx, GUID, DoorLiningDraft::new().lining_depth(0.1))
        .expect_err("WR31");
    assert!(invalid(&err), "{err}");

    let err =
        add_door_lining_properties(&mut tx, GUID, DoorLiningDraft::new().threshold_depth(0.1))
            .expect_err("WR32");
    assert!(invalid(&err), "{err}");

    add_door_lining_properties(
        &mut tx,
        GUID,
        DoorLiningDraft::new()
            .lining_depth(0.1)
            .lining_thickness(0.02),
    )
    .expect("a depth with its thickness is legal");
}

/// WR33/WR34: the door transom and casing pairs are XOR, both or neither.
#[test]
fn door_transom_and_casing_pairs_are_all_or_nothing() {
    let model = Model::new();
    let mut tx = Transaction::new(&model);

    for draft in [
        DoorLiningDraft::new().transom_offset(0.5),
        DoorLiningDraft::new().transom_thickness(0.02),
        DoorLiningDraft::new().casing_depth(0.1),
        DoorLiningDraft::new().casing_thickness(0.02),
    ] {
        let err = add_door_lining_properties(&mut tx, GUID, draft).expect_err("half a pair");
        assert!(invalid(&err), "{err}");
    }

    add_door_lining_properties(
        &mut tx,
        GUID,
        DoorLiningDraft::new()
            .transom_offset(0.5)
            .transom_thickness(0.02)
            .casing_depth(0.1)
            .casing_thickness(0.02),
    )
    .expect("complete pairs are legal");
}

/// WR32/WR33 on windows are ORDERED, not XOR.
///
/// The contrast with the door rules is the point: a first transom offset
/// alone is perfectly legal on a window, while a door's transom offset
/// alone is not. A writer that shared one rule between the two would
/// wrongly refuse half the legal window files.
#[test]
fn window_offsets_are_ordered_not_paired() {
    let model = Model::new();
    let mut tx = Transaction::new(&model);

    add_window_lining_properties(
        &mut tx,
        GUID,
        WindowLiningDraft::new().first_transom_offset(0.25),
    )
    .expect("a first offset alone is legal on a window");

    for draft in [
        WindowLiningDraft::new().second_transom_offset(0.75),
        WindowLiningDraft::new().second_mullion_offset(0.75),
    ] {
        let err = add_window_lining_properties(&mut tx, GUID, draft)
            .expect_err("a second offset without the first");
        assert!(invalid(&err), "{err}");
    }

    add_window_lining_properties(
        &mut tx,
        GUID,
        WindowLiningDraft::new()
            .first_transom_offset(0.25)
            .second_transom_offset(0.75)
            .first_mullion_offset(0.3)
            .second_mullion_offset(0.6),
    )
    .expect("both offsets in order are legal");
}

/// Window offsets are normalised ratios, not lengths.
#[test]
fn window_offsets_are_bounded_ratios() {
    let model = Model::new();
    let mut tx = Transaction::new(&model);
    let err = add_window_lining_properties(
        &mut tx,
        GUID,
        WindowLiningDraft::new().first_transom_offset(1.5),
    )
    .expect_err("IfcNormalisedRatioMeasure is bounded to [0, 1]");
    assert!(invalid(&err), "{err}");
}

/// Offsets are plain lengths and may be negative; thicknesses may not.
///
/// Collapsing the three length measure kinds into one non-negative
/// check would refuse a legal negative lining offset.
#[test]
fn length_measures_keep_their_own_sign_rules() {
    let model = Model::new();
    let mut tx = Transaction::new(&model);

    add_door_lining_properties(
        &mut tx,
        GUID,
        DoorLiningDraft::new()
            .lining_offset(-0.01)
            .threshold_offset(-0.02),
    )
    .expect("IfcLengthMeasure admits a negative offset");

    for draft in [
        DoorLiningDraft::new()
            .lining_depth(0.0)
            .lining_thickness(0.01),
        DoorLiningDraft::new()
            .lining_depth(0.1)
            .lining_thickness(-0.01),
    ] {
        let err = add_door_lining_properties(&mut tx, GUID, draft)
            .expect_err("a non-positive depth or negative thickness");
        assert!(invalid(&err), "{err}");
    }
}

/// Each panel enumeration is its own closed list.
///
/// The window operation enum has no USERDEFINED member while the door
/// and permeable-covering ones do, so a shared token list would accept
/// a window file the schema rejects.
#[test]
fn panel_enumerations_are_per_entity() {
    let mut model = Model::new();
    let mut tx = Transaction::new(&model);

    let id = add_door_panel_properties(
        &mut tx,
        GUID,
        Some("Leaf"),
        "SWINGING",
        "LEFT",
        (Some(0.04), Some(0.5)),
    )
    .expect("a door panel with legal tokens");
    tx.commit(&mut model).expect("commits");
    let entity = model.get(id).expect("staged");
    assert_eq!(entity.attributes[5], Value::Enum("SWINGING".into()));
    assert_eq!(entity.attributes[7], Value::Enum("LEFT".into()));

    let mut tx = Transaction::new(&model);
    let err = add_window_panel_properties(&mut tx, GUID, None, "USERDEFINED", "TOP", (None, None))
        .expect_err("IfcWindowPanelOperationEnum has no USERDEFINED");
    assert!(invalid(&err), "{err}");

    add_permeable_covering_properties(&mut tx, GUID, None, "USERDEFINED", "TOP", (None, None))
        .expect("the permeable covering enum does have USERDEFINED");

    let err = add_door_panel_properties(&mut tx, GUID, None, "SWINGING", "TOP", (None, None))
        .expect_err("TOP is a window position, not a door one");
    assert!(invalid(&err), "{err}");
}

/// PanelWidth is a fraction of the opening, not a length.
#[test]
fn panel_width_is_a_normalised_ratio() {
    let model = Model::new();
    let mut tx = Transaction::new(&model);
    let err =
        add_door_panel_properties(&mut tx, GUID, None, "SLIDING", "LEFT", (None, Some(900.0)))
            .expect_err("900 is a millimetre reading, not a ratio");
    assert!(invalid(&err), "{err}");
    add_door_panel_properties(&mut tx, GUID, None, "SLIDING", "LEFT", (None, Some(0.9)))
        .expect("a fraction is legal");
}

/// A malformed GlobalId is refused before anything is staged.
#[test]
fn a_malformed_guid_is_refused() {
    let model = Model::new();
    let mut tx = Transaction::new(&model);
    let err = add_door_lining_properties(&mut tx, "nope", DoorLiningDraft::default())
        .expect_err("four characters is not a GUID");
    assert!(invalid(&err), "{err}");
    assert!(tx.is_empty(), "nothing is staged when the GUID fails");
}

/// WR01: every enumeration value must share one measure type.
///
/// TYPEOF compares the declared measure, so an IFCLENGTHMEASURE(2.0) and
/// an IFCPOSITIVELENGTHMEASURE(2.0) are different types even though both
/// print a real. A mixed list makes the enumeration uninterpretable.
#[test]
fn enumeration_values_must_share_one_type() {
    let model = Model::new();
    let mut tx = Transaction::new(&model);

    add_property_enumeration(&mut tx, "Widths", vec![length(0.9), length(1.2)], None)
        .expect("one measure throughout is legal");

    let err = add_property_enumeration(
        &mut tx,
        "Mixed",
        vec![
            length(0.9),
            typed("IFCPOSITIVELENGTHMEASURE", Value::Real(1.2)),
        ],
        None,
    )
    .expect_err("two measures are different types");
    assert!(invalid(&err), "{err}");

    let err = add_property_enumeration(
        &mut tx,
        "Mixed",
        vec![label("a"), typed("IFCINTEGER", Value::Integer(1))],
        None,
    )
    .expect_err("text among integers");
    assert!(invalid(&err), "{err}");
}

/// `EnumerationValues` is `LIST OF IfcValue`, a SELECT: a bare literal
/// cannot say which member it is (#215).
#[test]
fn enumeration_values_are_typed_parameters() {
    let model = Model::new();
    let mut tx = Transaction::new(&model);
    let err = add_property_enumeration(&mut tx, "Mixed", vec![length(0.9), Value::Real(1.2)], None)
        .expect_err("a bare real names no measure");
    assert!(
        matches!(
            err,
            PropertyError::ValueForm {
                attribute: "EnumerationValues",
                typed_required: true,
                ..
            }
        ),
        "{err}"
    );
    assert!(tx.is_empty(), "nothing is staged when a value is bare");
}

/// The value list is UNIQUE and non-empty, and the name is the key.
#[test]
fn enumeration_values_are_unique_and_named() {
    let model = Model::new();
    let mut tx = Transaction::new(&model);
    let integer = |v: i64| typed("IFCINTEGER", Value::Integer(v));

    let err = add_property_enumeration(&mut tx, "Sizes", Vec::new(), None)
        .expect_err("EnumerationValues is LIST [1:?]");
    assert!(invalid(&err), "{err}");

    let err = add_property_enumeration(&mut tx, "  ", vec![integer(1)], None)
        .expect_err("UR1 makes Name the key, so it cannot be blank");
    assert!(invalid(&err), "{err}");

    let err = add_property_enumeration(&mut tx, "Sizes", vec![integer(1), integer(1)], None)
        .expect_err("the list is UNIQUE");
    assert!(invalid(&err), "{err}");

    add_property_enumeration(&mut tx, "Sizes", vec![integer(1), integer(2)], None)
        .expect("distinct values of one type are legal");
}

fn typed(type_name: &str, value: Value) -> Value {
    Value::Typed {
        type_name: type_name.into(),
        value: Box::new(value),
    }
}

fn length(value: f64) -> Value {
    typed("IFCLENGTHMEASURE", Value::Real(value))
}

fn label(text: &str) -> Value {
    typed("IFCLABEL", Value::Text(text.into()))
}

/// NoSelfReference: a property cannot depend on itself.
#[test]
fn a_property_cannot_depend_on_itself() {
    let mut model = Model::new();
    let a = model.push(ifc_model::Entity::new("IFCPROPERTYSINGLEVALUE", vec![]));
    let b = model.push(ifc_model::Entity::new("IFCPROPERTYSINGLEVALUE", vec![]));
    let mut tx = Transaction::new(&model);

    let err = add_property_dependency_relationship(&mut tx, None, None, (a, a), None)
        .expect_err("a self-dependency loops any dependency walk");
    assert!(invalid(&err), "{err}");

    add_property_dependency_relationship(&mut tx, Some("derives"), None, (a, b), Some("x*2"))
        .expect("two distinct properties are legal");
}

/// A complex template refuses a repeated child template name.
#[test]
fn complex_template_children_are_uniquely_named() {
    let mut model = Model::new();
    let a = model.push(ifc_model::Entity::new("IFCSIMPLEPROPERTYTEMPLATE", vec![]));
    let b = model.push(ifc_model::Entity::new("IFCSIMPLEPROPERTYTEMPLATE", vec![]));
    let mut tx = Transaction::new(&model);

    let err = add_complex_property_template(
        &mut tx,
        GUID,
        Some("Assembly"),
        (None, None),
        &[("Width", a), ("Width", b)],
    )
    .expect_err("UniquePropertyTemplateNames");
    assert!(invalid(&err), "{err}");

    let err = add_complex_property_template(
        &mut tx,
        GUID,
        None,
        (None, Some("X_COMPLEX")),
        &[("Width", a)],
    )
    .expect_err("X_COMPLEX is not a member of the template type enum");
    assert!(invalid(&err), "{err}");

    add_complex_property_template(
        &mut tx,
        GUID,
        Some("Assembly"),
        (Some("Leaf"), Some("P_COMPLEX")),
        &[("Width", a), ("Height", b)],
    )
    .expect("distinct child names are legal");
}

/// The window has its own WR31, distinct from the door's.
///
/// Both entities state the rule separately, so each writer needs its
/// own guard; testing only the door leaves the window unprotected.
#[test]
fn a_window_lining_depth_needs_its_thickness() {
    let model = Model::new();
    let mut tx = Transaction::new(&model);
    let err =
        add_window_lining_properties(&mut tx, GUID, WindowLiningDraft::new().lining_depth(0.1))
            .expect_err("WR31 on the window");
    assert!(invalid(&err), "{err}");

    add_window_lining_properties(
        &mut tx,
        GUID,
        WindowLiningDraft::new()
            .lining_depth(0.1)
            .lining_thickness(0.02),
    )
    .expect("a depth with its thickness is legal");
}