Skip to main content

ifc_structural/
authoring.rs

1//! Selected schema-resolved structural authoring.
2
3use std::collections::HashSet;
4use std::sync::Arc;
5
6use ifc_model::guid::Guid;
7use ifc_model::{Edit, Entity, EntityId, Model, Transaction, Value};
8use ifc_schema::Schema;
9
10use crate::error::{StructuralError, StructuralResult};
11use crate::AnalysisModelType;
12
13mod action;
14mod condition;
15mod item;
16mod load_group;
17mod reaction;
18mod relation;
19mod stiffness;
20
21pub use action::{stage_action, ActionDraft, ActionDraftKind, ProjectedOrTrue};
22pub use condition::{
23    stage_boundary_condition, stage_boundary_condition_in, stage_connection_condition,
24    BoundaryConditionDraft, ConnectionConditionDraft,
25};
26pub use item::{
27    stage_connection, stage_member, ConnectionDraft, ConnectionDraftKind, MemberDraft,
28    MemberDraftKind, MemberPredefinedType, StructuralRootDraft,
29};
30pub use load_group::{stage_load_group, LoadGroupDraft, LoadGroupKind};
31pub use reaction::{
32    stage_reaction, stage_result_group, ReactionDraft, ReactionDraftKind, ResultGroupDraft,
33};
34pub use relation::{
35    stage_activity_assignment, stage_member_connection, ActivityAssignmentDraft,
36    MemberConnectionDraft, RelationshipRootDraft,
37};
38
39/// Staged fields for creating an `IfcStructuralAnalysisModel` via [`stage_analysis_model`].
40#[derive(Debug, Clone)]
41#[non_exhaustive]
42pub struct AnalysisModelDraft {
43    /// `GlobalId`; must parse as a 22-character IFC GUID.
44    pub global_id: String,
45    /// `OwnerHistory`, validated against the model/transaction if present.
46    pub owner_history: Option<EntityId>,
47    /// `Name`.
48    pub name: Option<String>,
49    /// `Description`.
50    pub description: Option<String>,
51    /// `ObjectType`; required non-blank when `predefined_type` is `UserDefined`.
52    pub object_type: Option<String>,
53    /// `PredefinedType`.
54    pub predefined_type: AnalysisModelType,
55    /// `OrientationOf2DPlane`, an `IfcAxis2Placement3D` reference.
56    pub orientation_of_2d_plane: Option<EntityId>,
57    /// `LoadedBy`, `IfcStructuralLoadGroup` references; members must be unique.
58    pub loaded_by: Vec<EntityId>,
59    /// `HasResults`, `IfcStructuralResultGroup` references; members must be unique.
60    pub result_groups: Vec<EntityId>,
61    /// `SharedPlacement`; only staged when the target schema declares the attribute.
62    pub shared_placement: Option<EntityId>,
63}
64
65impl Default for AnalysisModelDraft {
66    fn default() -> Self {
67        Self {
68            global_id: "0000000000000000000000".to_owned(),
69            owner_history: None,
70            name: None,
71            description: None,
72            object_type: None,
73            predefined_type: AnalysisModelType::NotDefined,
74            orientation_of_2d_plane: None,
75            loaded_by: Vec::new(),
76            result_groups: Vec::new(),
77            shared_placement: None,
78        }
79    }
80}
81
82/// Staged fields for creating one core `IfcStructuralLoadStatic` subtype via [`stage_load`].
83#[derive(Debug, Clone, PartialEq)]
84#[non_exhaustive]
85pub enum LoadDraft {
86    /// Stages an `IfcStructuralLoadSingleForce`.
87    SingleForce {
88        /// `Name`.
89        name: Option<String>,
90        /// `ForceX`/`ForceY`/`ForceZ`.
91        force: [Option<f64>; 3],
92        /// `MomentX`/`MomentY`/`MomentZ`.
93        moment: [Option<f64>; 3],
94    },
95    /// Stages an `IfcStructuralLoadLinearForce`.
96    LinearForce {
97        /// `Name`.
98        name: Option<String>,
99        /// `LinearForceX`/`LinearForceY`/`LinearForceZ`.
100        force: [Option<f64>; 3],
101        /// `LinearMomentX`/`LinearMomentY`/`LinearMomentZ`.
102        moment: [Option<f64>; 3],
103    },
104    /// Stages an `IfcStructuralLoadPlanarForce`.
105    PlanarForce {
106        /// `Name`.
107        name: Option<String>,
108        /// `PlanarForceX`/`PlanarForceY`/`PlanarForceZ`.
109        force: [Option<f64>; 3],
110    },
111    /// Stages an `IfcStructuralLoadTemperature`.
112    Temperature {
113        /// `Name`.
114        name: Option<String>,
115        /// Constant, through-Y and through-Z temperature deltas (`DeltaTConstant`/`DeltaTY`/`DeltaTZ`,
116        /// or the underscored equivalents in schemas that name them that way).
117        delta: [Option<f64>; 3],
118    },
119    /// Stages an `IfcStructuralLoadSingleForceWarping`.
120    ///
121    /// Extends the single force with the bimoment that restrained
122    /// warping produces in an open thin-walled section.
123    SingleForceWarping {
124        /// `Name`.
125        name: Option<String>,
126        /// `ForceX`/`ForceY`/`ForceZ`.
127        force: [Option<f64>; 3],
128        /// `MomentX`/`MomentY`/`MomentZ`.
129        moment: [Option<f64>; 3],
130        /// `WarpingMoment`.
131        warping_moment: Option<f64>,
132    },
133    /// Stages an `IfcStructuralLoadSingleDisplacement`.
134    ///
135    /// A prescribed movement of a point: three translations and three
136    /// rotations. The distortion form below extends this with a
137    /// seventh slot; this supertype stops at six.
138    SingleDisplacement {
139        /// `Name`.
140        name: Option<String>,
141        /// `DisplacementX`/`DisplacementY`/`DisplacementZ`.
142        displacement: [Option<f64>; 3],
143        /// `RotationalDisplacementRX`/`RY`/`RZ`.
144        rotation: [Option<f64>; 3],
145    },
146    /// Stages an `IfcStructuralLoadSingleDisplacementDistortion`.
147    ///
148    /// The displacement counterpart of the warping force: a prescribed
149    /// movement rather than an applied load.
150    SingleDisplacementDistortion {
151        /// `Name`.
152        name: Option<String>,
153        /// `DisplacementX`/`DisplacementY`/`DisplacementZ`.
154        displacement: [Option<f64>; 3],
155        /// `RotationalDisplacementRX`/`RY`/`RZ`.
156        rotation: [Option<f64>; 3],
157        /// `Distortion`.
158        distortion: Option<f64>,
159    },
160    /// Stages an `IfcSurfaceReinforcementArea`.
161    ///
162    /// Areas are measures, not coordinates: the schema requires each
163    /// to be non-negative, and requires at least one of the three to
164    /// be present so the record says something.
165    SurfaceReinforcementArea {
166        /// `Name`.
167        name: Option<String>,
168        /// `SurfaceReinforcement1`, two or three entries.
169        surface_1: Option<Vec<f64>>,
170        /// `SurfaceReinforcement2`, two or three entries.
171        surface_2: Option<Vec<f64>>,
172        /// `ShearReinforcement`.
173        shear: Option<f64>,
174    },
175    /// Stages an `IfcStructuralLoadConfiguration`.
176    ///
177    /// A list of loads with optional sample locations. When locations
178    /// are given the schema requires one per value, so the pairing is
179    /// checked here rather than left to a downstream reader.
180    Configuration {
181        /// `Name`.
182        name: Option<String>,
183        /// `Values`: the loads sampled, at least one.
184        values: Vec<EntityId>,
185        /// `Locations`: one coordinate pair per value, or none.
186        locations: Option<Vec<[f64; 2]>>,
187    },
188}
189
190/// Stage an `IfcStructuralAnalysisModel` create edit on `tx`.
191///
192/// Fails with [`StructuralError::InvalidGlobalId`] if `draft.global_id` is not
193/// a valid GUID, [`StructuralError::SemanticViolation`] if `predefined_type`
194/// is `UserDefined` without a non-blank `object_type`, and with reference
195/// validation errors ([`StructuralError::DanglingReference`],
196/// [`StructuralError::WrongReferenceType`], [`StructuralError::InvalidDraftValue`])
197/// if any referenced entity is missing, of the wrong type, or a SET contains
198/// duplicates. Returns the id staged for the new entity.
199pub fn stage_analysis_model(
200    tx: &mut Transaction,
201    model: &Model,
202    schema: &Schema,
203    draft: AnalysisModelDraft,
204) -> StructuralResult<EntityId> {
205    if Guid::parse(&draft.global_id).is_none() {
206        return Err(StructuralError::InvalidGlobalId);
207    }
208    if draft.predefined_type == AnalysisModelType::UserDefined
209        && draft
210            .object_type
211            .as_deref()
212            .is_none_or(|value| value.trim().is_empty())
213    {
214        return Err(StructuralError::SemanticViolation {
215            entity: None,
216            rule: "USERDEFINED analysis model requires ObjectType",
217        });
218    }
219
220    validate_unique_set_members(&draft.loaded_by, "IfcStructuralAnalysisModel", "LoadedBy")?;
221    validate_unique_set_members(
222        &draft.result_groups,
223        "IfcStructuralAnalysisModel",
224        "HasResults",
225    )?;
226    validate_optional_ref(tx, model, schema, draft.owner_history, "IfcOwnerHistory")?;
227    validate_optional_ref(
228        tx,
229        model,
230        schema,
231        draft.orientation_of_2d_plane,
232        "IfcAxis2Placement3D",
233    )?;
234    validate_refs(
235        tx,
236        model,
237        schema,
238        &draft.loaded_by,
239        "IfcStructuralLoadGroup",
240    )?;
241    validate_refs(
242        tx,
243        model,
244        schema,
245        &draft.result_groups,
246        "IfcStructuralResultGroup",
247    )?;
248    validate_optional_ref(
249        tx,
250        model,
251        schema,
252        draft.shared_placement,
253        "IfcObjectPlacement",
254    )?;
255
256    let mut fields = vec![
257        ("GlobalId", Value::Text(Arc::from(draft.global_id))),
258        ("Name", optional_text(draft.name)),
259        ("Description", optional_text(draft.description)),
260        ("ObjectType", optional_text(draft.object_type)),
261        (
262            "PredefinedType",
263            Value::Enum(Arc::from(draft.predefined_type.token())),
264        ),
265        (
266            "OrientationOf2DPlane",
267            optional_ref(draft.orientation_of_2d_plane),
268        ),
269        ("LoadedBy", optional_refs(draft.loaded_by)),
270        ("HasResults", optional_refs(draft.result_groups)),
271    ];
272    if let Some(owner_history) = draft.owner_history {
273        fields.push(("OwnerHistory", Value::Ref(owner_history)));
274    }
275    if schema
276        .attribute_names("IfcStructuralAnalysisModel")
277        .iter()
278        .any(|name| name.eq_ignore_ascii_case("SharedPlacement"))
279    {
280        fields.push(("SharedPlacement", optional_ref(draft.shared_placement)));
281    } else if draft.shared_placement.is_some() {
282        return Err(StructuralError::UnsupportedAttribute {
283            entity_type: "IfcStructuralAnalysisModel".to_owned(),
284            attribute: "SharedPlacement".to_owned(),
285        });
286    }
287
288    let entity = build_named(schema, "IfcStructuralAnalysisModel", fields)?;
289    Ok(tx.create(entity))
290}
291
292/// Stage one core `IfcStructuralLoadStatic` subtype create edit on `tx`.
293///
294/// Fails with [`StructuralError::InvalidDraftValue`] if any numeric component
295/// is set to a non-finite value. Returns the id staged for the new entity.
296pub fn stage_load(
297    tx: &mut Transaction,
298    schema: &Schema,
299    draft: LoadDraft,
300) -> StructuralResult<EntityId> {
301    // Two subtypes carry list-valued attributes and WHERE rules of
302    // their own, so they cannot ride the shared scalar path below.
303
304    let (entity_type, name, attributes, values): (&str, Option<String>, &[&str], Vec<Option<f64>>) =
305        match draft {
306            LoadDraft::SurfaceReinforcementArea {
307                name,
308                surface_1,
309                surface_2,
310                shear,
311            } => return stage_surface_reinforcement(tx, schema, name, surface_1, surface_2, shear),
312            LoadDraft::Configuration {
313                name,
314                values,
315                locations,
316            } => return stage_load_configuration(tx, schema, name, values, locations),
317            LoadDraft::SingleForce {
318                name,
319                force,
320                moment,
321            } => (
322                "IfcStructuralLoadSingleForce",
323                name,
324                &[
325                    "ForceX", "ForceY", "ForceZ", "MomentX", "MomentY", "MomentZ",
326                ],
327                force.into_iter().chain(moment).collect(),
328            ),
329            LoadDraft::LinearForce {
330                name,
331                force,
332                moment,
333            } => (
334                "IfcStructuralLoadLinearForce",
335                name,
336                &[
337                    "LinearForceX",
338                    "LinearForceY",
339                    "LinearForceZ",
340                    "LinearMomentX",
341                    "LinearMomentY",
342                    "LinearMomentZ",
343                ],
344                force.into_iter().chain(moment).collect(),
345            ),
346            LoadDraft::PlanarForce { name, force } => (
347                "IfcStructuralLoadPlanarForce",
348                name,
349                &["PlanarForceX", "PlanarForceY", "PlanarForceZ"],
350                force.to_vec(),
351            ),
352            LoadDraft::Temperature { name, delta } => {
353                let attributes: &[&str] = if schema
354                    .attribute_names("IfcStructuralLoadTemperature")
355                    .iter()
356                    .any(|attribute| attribute.eq_ignore_ascii_case("DeltaTConstant"))
357                {
358                    &["DeltaTConstant", "DeltaTY", "DeltaTZ"]
359                } else {
360                    &["DeltaT_Constant", "DeltaT_Y", "DeltaT_Z"]
361                };
362                (
363                    "IfcStructuralLoadTemperature",
364                    name,
365                    attributes,
366                    delta.to_vec(),
367                )
368            }
369            LoadDraft::SingleForceWarping {
370                name,
371                force,
372                moment,
373                warping_moment,
374            } => (
375                "IfcStructuralLoadSingleForceWarping",
376                name,
377                &[
378                    "ForceX",
379                    "ForceY",
380                    "ForceZ",
381                    "MomentX",
382                    "MomentY",
383                    "MomentZ",
384                    "WarpingMoment",
385                ],
386                force
387                    .into_iter()
388                    .chain(moment)
389                    .chain([warping_moment])
390                    .collect(),
391            ),
392            LoadDraft::SingleDisplacement {
393                name,
394                displacement,
395                rotation,
396            } => (
397                "IfcStructuralLoadSingleDisplacement",
398                name,
399                &[
400                    "DisplacementX",
401                    "DisplacementY",
402                    "DisplacementZ",
403                    "RotationalDisplacementRX",
404                    "RotationalDisplacementRY",
405                    "RotationalDisplacementRZ",
406                ],
407                displacement.into_iter().chain(rotation).collect(),
408            ),
409            LoadDraft::SingleDisplacementDistortion {
410                name,
411                displacement,
412                rotation,
413                distortion,
414            } => (
415                "IfcStructuralLoadSingleDisplacementDistortion",
416                name,
417                &[
418                    "DisplacementX",
419                    "DisplacementY",
420                    "DisplacementZ",
421                    "RotationalDisplacementRX",
422                    "RotationalDisplacementRY",
423                    "RotationalDisplacementRZ",
424                    "Distortion",
425                ],
426                displacement
427                    .into_iter()
428                    .chain(rotation)
429                    .chain([distortion])
430                    .collect(),
431            ),
432        };
433    for (attribute, value) in attributes.iter().zip(&values) {
434        if value.is_some_and(|number| !number.is_finite()) {
435            return Err(StructuralError::InvalidDraftValue {
436                entity_type,
437                attribute,
438                expected: "finite load value or null",
439            });
440        }
441    }
442    let mut fields = vec![("Name", optional_text(name))];
443    fields.extend(
444        attributes
445            .iter()
446            .zip(values)
447            .map(|(name, value)| (*name, value.map_or(Value::Null, Value::Real))),
448    );
449    Ok(tx.create(build_named(schema, entity_type, fields)?))
450}
451
452/// Stage an `IfcSurfaceReinforcementArea`, enforcing its four WHERE rules.
453///
454/// `NonnegativeArea1..3` reject a negative area; `SurfaceAndOrShearArea`
455/// rejects a record that specifies nothing at all.
456fn stage_surface_reinforcement(
457    tx: &mut Transaction,
458    schema: &Schema,
459    name: Option<String>,
460    surface_1: Option<Vec<f64>>,
461    surface_2: Option<Vec<f64>>,
462    shear: Option<f64>,
463) -> StructuralResult<EntityId> {
464    const ENTITY: &str = "IfcSurfaceReinforcementArea";
465
466    if surface_1.is_none() && surface_2.is_none() && shear.is_none() {
467        return Err(StructuralError::InvalidDraftValue {
468            entity_type: ENTITY,
469            attribute: "SurfaceReinforcement1",
470            expected: "at least one reinforcement area to be specified",
471        });
472    }
473
474    for (attribute, area) in [
475        ("SurfaceReinforcement1", surface_1.as_ref()),
476        ("SurfaceReinforcement2", surface_2.as_ref()),
477    ] {
478        let Some(area) = area else { continue };
479        if !(2..=3).contains(&area.len()) {
480            return Err(StructuralError::InvalidDraftValue {
481                entity_type: ENTITY,
482                attribute,
483                expected: "two or three area values",
484            });
485        }
486        if area.iter().any(|value| !value.is_finite() || *value < 0.0) {
487            return Err(StructuralError::InvalidDraftValue {
488                entity_type: ENTITY,
489                attribute,
490                expected: "finite non-negative area values",
491            });
492        }
493    }
494
495    if shear.is_some_and(|value| !value.is_finite() || value < 0.0) {
496        return Err(StructuralError::InvalidDraftValue {
497            entity_type: ENTITY,
498            attribute: "ShearReinforcement",
499            expected: "a finite non-negative area",
500        });
501    }
502
503    let reals = |values: Option<Vec<f64>>| {
504        values.map_or(Value::Null, |values| {
505            Value::List(values.into_iter().map(Value::Real).collect())
506        })
507    };
508    let fields = vec![
509        ("Name", optional_text(name)),
510        ("SurfaceReinforcement1", reals(surface_1)),
511        ("SurfaceReinforcement2", reals(surface_2)),
512        ("ShearReinforcement", shear.map_or(Value::Null, Value::Real)),
513    ];
514    Ok(tx.create(build_named(schema, ENTITY, fields)?))
515}
516
517/// Stage an `IfcStructuralLoadConfiguration`, enforcing `ValidListSize`.
518///
519/// Locations are optional, but when present the schema requires exactly
520/// one per value: a mismatched pair silently misattributes every sample
521/// after the first gap, so it is refused here.
522fn stage_load_configuration(
523    tx: &mut Transaction,
524    schema: &Schema,
525    name: Option<String>,
526    values: Vec<EntityId>,
527    locations: Option<Vec<[f64; 2]>>,
528) -> StructuralResult<EntityId> {
529    const ENTITY: &str = "IfcStructuralLoadConfiguration";
530
531    if values.is_empty() {
532        return Err(StructuralError::InvalidDraftValue {
533            entity_type: ENTITY,
534            attribute: "Values",
535            expected: "at least one load",
536        });
537    }
538    if let Some(locations) = locations.as_ref() {
539        if locations.len() != values.len() {
540            return Err(StructuralError::InvalidDraftValue {
541                entity_type: ENTITY,
542                attribute: "Locations",
543                expected: "one location per value",
544            });
545        }
546        if locations
547            .iter()
548            .any(|pair| pair.iter().any(|value| !value.is_finite()))
549        {
550            return Err(StructuralError::InvalidDraftValue {
551                entity_type: ENTITY,
552                attribute: "Locations",
553                expected: "finite coordinates",
554            });
555        }
556    }
557
558    let fields = vec![
559        ("Name", optional_text(name)),
560        (
561            "Values",
562            Value::List(values.into_iter().map(Value::Ref).collect()),
563        ),
564        (
565            "Locations",
566            locations.map_or(Value::Null, |locations| {
567                Value::List(
568                    locations
569                        .into_iter()
570                        .map(|pair| Value::List(pair.into_iter().map(Value::Real).collect()))
571                        .collect(),
572                )
573            }),
574        ),
575    ];
576    Ok(tx.create(build_named(schema, ENTITY, fields)?))
577}
578
579pub(super) fn build_named(
580    schema: &Schema,
581    entity_type: &str,
582    fields: Vec<(&str, Value)>,
583) -> StructuralResult<Entity> {
584    let attributes = schema.attributes(entity_type);
585    if attributes.is_empty() {
586        return Err(StructuralError::UnsupportedSchema {
587            token: schema.name().to_owned(),
588        });
589    }
590    let mut values = vec![Value::Null; attributes.len()];
591    for (name, value) in fields {
592        let slot = attributes
593            .iter()
594            .position(|attribute| attribute.name.eq_ignore_ascii_case(name))
595            .ok_or_else(|| StructuralError::UnsupportedAttribute {
596                entity_type: entity_type.to_owned(),
597                attribute: name.to_owned(),
598            })?;
599        values[slot] = value;
600    }
601    for (attribute, value) in attributes.iter().zip(&values) {
602        if !attribute.optional && matches!(value, Value::Null) {
603            return Err(StructuralError::MissingRequired {
604                entity_type: entity_type.to_owned(),
605                attribute: attribute.name.clone(),
606            });
607        }
608    }
609    Ok(Entity::new(entity_type.to_ascii_uppercase(), values))
610}
611
612pub(super) fn optional_text(value: Option<String>) -> Value {
613    value.map_or(Value::Null, |value| Value::Text(Arc::from(value)))
614}
615
616pub(super) fn optional_ref(value: Option<EntityId>) -> Value {
617    value.map_or(Value::Null, Value::Ref)
618}
619
620fn optional_refs(values: Vec<EntityId>) -> Value {
621    if values.is_empty() {
622        Value::Null
623    } else {
624        Value::List(values.into_iter().map(Value::Ref).collect())
625    }
626}
627
628pub(super) fn validate_optional_ref(
629    tx: &Transaction,
630    model: &Model,
631    schema: &Schema,
632    target: Option<EntityId>,
633    expected: &'static str,
634) -> StructuralResult<()> {
635    if let Some(target) = target {
636        validate_ref(tx, model, schema, target, expected)?;
637    }
638    Ok(())
639}
640
641fn validate_refs(
642    tx: &Transaction,
643    model: &Model,
644    schema: &Schema,
645    targets: &[EntityId],
646    expected: &'static str,
647) -> StructuralResult<()> {
648    for target in targets {
649        validate_ref(tx, model, schema, *target, expected)?;
650    }
651    Ok(())
652}
653
654fn validate_unique_set_members(
655    targets: &[EntityId],
656    entity_type: &'static str,
657    attribute: &'static str,
658) -> StructuralResult<()> {
659    let mut unique = HashSet::with_capacity(targets.len());
660    if targets.iter().all(|target| unique.insert(*target)) {
661        return Ok(());
662    }
663    Err(StructuralError::InvalidDraftValue {
664        entity_type,
665        attribute,
666        expected: "SET of unique entity references",
667    })
668}
669
670pub(super) fn validate_ref(
671    tx: &Transaction,
672    model: &Model,
673    schema: &Schema,
674    target: EntityId,
675    expected: &'static str,
676) -> StructuralResult<()> {
677    validate_ref_select(tx, model, schema, target, expected, &[expected])
678}
679
680pub(super) fn validate_ref_select(
681    tx: &Transaction,
682    model: &Model,
683    schema: &Schema,
684    target: EntityId,
685    expected: &'static str,
686    members: &[&str],
687) -> StructuralResult<()> {
688    let entity = projected_entity(tx, model, target).ok_or(StructuralError::DanglingReference {
689        entity: EntityId(0),
690        attribute: "draft reference",
691        target,
692    })?;
693    if !members
694        .iter()
695        .any(|member| schema.is_a(&entity.type_name, member))
696    {
697        return Err(StructuralError::WrongReferenceType {
698            entity: EntityId(0),
699            attribute: "draft reference",
700            target,
701            expected,
702            actual: entity.type_name.to_string(),
703        });
704    }
705    Ok(())
706}
707
708pub(super) fn projected_entity(
709    tx: &Transaction,
710    model: &Model,
711    target: EntityId,
712) -> Option<Entity> {
713    let mut projected = model.get(target).cloned();
714    for edit in tx.edits() {
715        match edit {
716            Edit::Create { id, entity } if *id == target => projected = Some(entity.clone()),
717            Edit::SetAttribute { id, slot, value } if *id == target => {
718                let entity = projected.as_mut()?;
719                let attribute = entity.attributes.get_mut(*slot)?;
720                *attribute = value.clone();
721            }
722            Edit::Retype { id, type_name } if *id == target => {
723                projected.as_mut()?.type_name = type_name.clone();
724            }
725            Edit::Remove { id } if *id == target => projected = None,
726            _ => {}
727        }
728    }
729    projected
730}
731
732mod builders;