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