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