Skip to content

Commit 4dc93be

Browse files
BIMvoicelouistrue
andauthored
fix(server): extract IfcRelAssignsToGroup(ByFactor)/Nests/ConnectsPathElements (#3969)
apps/server/src/services/data_model/relationships.rs only extracted 9 of the ~19 IfcRel* types the TS/WASM path resolves, so a server-parsed model's relationship graph silently dropped edges the client is fully wired to consume. Confirmed live consumer chains for four of the ten missing types and added them: - IFCRELASSIGNSTOGROUP / IFCRELASSIGNSTOGROUPBYFACTOR: the viewer's Groups panel, "By Zone" lens, and IDS partOf all read RelationshipType.AssignsToGroup; without this a server-parsed model's Groups tab renders empty even when the model has real IfcSystem/ IfcZone/IfcGroup memberships. - IFCRELNESTS: packages/ids/src/bridge/data-accessor.ts maps it onto the same edge bucket as IfcRelAggregates for IDS partOf checks. - IFCRELCONNECTSPATHELEMENTS: the Properties panel's "connected walls" (extractRelationshipsOnDemand) reads this edge. Left out IFCRELASSIGNSTOPRODUCT, IFCRELCONNECTSELEMENTS, IFCRELCONNECTSPORTTOELEMENT, IFCRELCONNECTSPORTS, IFCRELSPACEBOUNDARY, and IFCRELREFERENCEDINSPATIALSTRUCTURE: grepping the app/server/mcp/cli/ ids trees found no consumer for any of them beyond generic cache/export/ query plumbing and parser-level tests, so adding them now would be dead weight with no user-visible fix. Orientation was verified per added type against the TS `extractRelFast`/`REL_TYPE_MAP` byte-level scanner (not assumed): AssignsToGroup(ByFactor) reads RelatingGroup at attr 6 (after the RelatedObjects list at attr 4 and the RelatedObjectsType enum at attr 5); ConnectsPathElements reads RelatingElement/RelatedElement as two SINGLE refs at attrs 5/6 (an optional ConnectionGeometry sits at attr 4), the same failure mode class as #1751 if handled as lists; Nests shares the existing default (4,5) arm already used for Aggregates. RED-GREEN'd against a synthetic STEP fixture, with a mutation removing IFCRELNESTS from the type list and a second flipping the ConnectsPathElements orientation, both confirmed caught by the new tests. All 9 pre-existing relationship types and a fixture with none of the new types were re-verified unaffected (274/274 `-p ifc-lite-server` tests pass); the browser/WASM path is untouched. Closes #3964 Claude-Session: https://claude.ai/code/session_01QPHChk3Ve9N519A4kY7436 Co-authored-by: Louis Trümpler <78563314+louistrue@users.noreply.github.com>
1 parent 95f25d6 commit 4dc93be

2 files changed

Lines changed: 152 additions & 0 deletions

File tree

apps/server/src/services/data_model/relationships.rs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,19 @@ pub(super) fn extract_relationships(
2626
"IFCRELASSOCIATESDOCUMENT",
2727
"IFCRELVOIDSELEMENT",
2828
"IFCRELFILLSELEMENT",
29+
// Added for issue #3964. Each has a live consumer on the TS/WASM
30+
// side (see the PR description for the full audit of what was and
31+
// wasn't added):
32+
// - IFCRELASSIGNSTOGROUP / IFCRELASSIGNSTOGROUPBYFACTOR: the
33+
// viewer's Groups panel, "By Zone" lens, and IDS `partOf`.
34+
// - IFCRELNESTS: IDS `partOf` maps it onto the same edge bucket as
35+
// IfcRelAggregates (packages/ids/src/bridge/data-accessor.ts).
36+
// - IFCRELCONNECTSPATHELEMENTS: the Properties panel's "connected
37+
// walls" (extractRelationshipsOnDemand).
38+
"IFCRELASSIGNSTOGROUP",
39+
"IFCRELASSIGNSTOGROUPBYFACTOR",
40+
"IFCRELNESTS",
41+
"IFCRELCONNECTSPATHELEMENTS",
2942
];
3043

3144
let rel_jobs: Vec<_> = jobs
@@ -138,6 +151,24 @@ fn extract_relationship(
138151
}]);
139152
}
140153

154+
// IfcRelConnectsElements (and its subtype IfcRelConnectsPathElements)
155+
// carries an OPTIONAL ConnectionGeometry at attr 4, then RelatingElement
156+
// at attr 5 and RelatedElement at attr 6 — both SINGLE refs, not lists
157+
// (mirrors the TS `extractRelFast` ConnectsElements/ConnectsPathElements
158+
// branch in columnar-parser-relationships.ts). The list-based path below
159+
// would call `get_list(6)` on a single entity ref, get `None`, and
160+
// silently drop the relationship, same failure mode as #1751.
161+
if type_upper == "IFCRELCONNECTSPATHELEMENTS" {
162+
let relating_id = entity.get_ref(5)?;
163+
let related_id = entity.get_ref(6)?;
164+
return Some(vec![Relationship {
165+
rel_type: type_name.to_string(),
166+
rel_id,
167+
relating_id,
168+
related_id,
169+
}]);
170+
}
171+
141172
let (relating_idx, related_idx) = match type_upper.as_str() {
142173
"IFCRELDEFINESBYPROPERTIES" => (5, 4), // RelatingPropertyDefinition at 5, RelatedObjects at 4
143174
// RelatingType (single ref) at 5, RelatedObjects (list) at 4 — same
@@ -151,6 +182,15 @@ fn extract_relationship(
151182
"IFCRELASSOCIATESMATERIAL"
152183
| "IFCRELASSOCIATESCLASSIFICATION"
153184
| "IFCRELASSOCIATESDOCUMENT" => (5, 4),
185+
// IfcRelAssigns base attrs: RelatedObjects(4), RelatedObjectsType(5,
186+
// an enum, not a ref), then IfcRelAssignsToGroup adds RelatingGroup(6).
187+
// IfcRelAssignsToGroupByFactor is a subtype (adds a trailing Factor
188+
// we don't read) with the identical RelatedObjects/RelatingGroup
189+
// layout, so it shares this arm.
190+
"IFCRELASSIGNSTOGROUP" | "IFCRELASSIGNSTOGROUPBYFACTOR" => (6, 4),
191+
// IFCRELNESTS (IfcRelDecomposes): RelatingObject(4), RelatedObjects(5)
192+
// — identical layout to IFCRELAGGREGATES, so it falls through to the
193+
// default arm below; listed here only for discoverability.
154194
_ => (4, 5), // Standard: RelatingObject at 4, RelatedObjects at 5
155195
};
156196

apps/server/src/services/data_model/tests.rs

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -733,3 +733,115 @@ fn voids_and_fills_carry_the_ifcrel_express_id() {
733733
assert_eq!(rel_id_of("IFCRELVOIDSELEMENT"), 40);
734734
assert_eq!(rel_id_of("IFCRELFILLSELEMENT"), 50);
735735
}
736+
737+
/// Fixture for issue #3964: a `IfcSystem` grouping a wall via
738+
/// `IfcRelAssignsToGroup`, an `IfcZone` grouping the same wall via
739+
/// `IfcRelAssignsToGroupByFactor` (adds a proportional Factor, e.g. zone
740+
/// occupancy share, but shares the same RelatedObjects/RelatingGroup
741+
/// membership semantics as its supertype), a door decomposed into a panel via
742+
/// `IfcRelNests` (a decomposition edge some IFC4 exporters use instead of
743+
/// `IfcRelAggregates`, e.g. feature/fastener nesting), and two walls joined
744+
/// end-to-end via `IfcRelConnectsPathElements` (the "connected walls" edge the
745+
/// Properties panel reads via `extractRelationshipsOnDemand`). None of these
746+
/// four types were extracted before this fix.
747+
const NEW_REL_TYPES_IFC: &str = r#"ISO-10303-21;
748+
HEADER;
749+
FILE_SCHEMA(('IFC4'));
750+
ENDSEC;
751+
DATA;
752+
#1=IFCPROJECT('0$ScRe4drECQ4DMSqUjd6d',$,'P',$,$,$,$,$,$);
753+
#10=IFCWALL('Wall00000000000000001',$,'W1',$,$,$,$,$,$);
754+
#11=IFCWALL('Wall00000000000000002',$,'W2',$,$,$,$,$,$);
755+
#20=IFCSYSTEM('Sys0000000000000000001',$,'HVAC-1',$,$);
756+
#21=IFCRELASSIGNSTOGROUP('Grp0000000000000000001',$,$,$,(#10),$,#20);
757+
#25=IFCZONE('Zon0000000000000000001',$,'Zone-A',$,$);
758+
#26=IFCRELASSIGNSTOGROUPBYFACTOR('Grf0000000000000000001',$,$,$,(#10),$,#25,0.5);
759+
#30=IFCDOOR('Doo0000000000000000001',$,'D1',$,$,$,$,$,$);
760+
#31=IFCDOOR('Doo0000000000000000002',$,'D2',$,$,$,$,$,$);
761+
#32=IFCRELNESTS('Nst0000000000000000001',$,$,$,#30,(#31));
762+
#40=IFCRELCONNECTSPATHELEMENTS('Con0000000000000000001',$,$,$,$,#10,#11,$,$,.ATEND.,.ATSTART.);
763+
ENDSEC;
764+
END-ISO-10303-21;
765+
"#;
766+
767+
#[test]
768+
fn extracts_assigns_to_group_relationship_orientation() {
769+
let dm = extract_data_model(NEW_REL_TYPES_IFC);
770+
// RelatingGroup=#20 (IfcSystem), RelatedObjects=(#10) (the wall).
771+
assert!(
772+
dm.relationships.iter().any(|r| {
773+
r.rel_type.eq_ignore_ascii_case("IFCRELASSIGNSTOGROUP")
774+
&& r.relating_id == 20
775+
&& r.related_id == 10
776+
}),
777+
"IFCRELASSIGNSTOGROUP (system -> wall) missing or misoriented: {:?}",
778+
dm.relationships
779+
);
780+
}
781+
782+
#[test]
783+
fn extracts_assigns_to_group_by_factor_relationship_orientation() {
784+
let dm = extract_data_model(NEW_REL_TYPES_IFC);
785+
// RelatingGroup=#25 (IfcZone), RelatedObjects=(#10) (the wall).
786+
assert!(
787+
dm.relationships.iter().any(|r| {
788+
r.rel_type.eq_ignore_ascii_case("IFCRELASSIGNSTOGROUPBYFACTOR")
789+
&& r.relating_id == 25
790+
&& r.related_id == 10
791+
}),
792+
"IFCRELASSIGNSTOGROUPBYFACTOR (zone -> wall) missing or misoriented: {:?}",
793+
dm.relationships
794+
);
795+
}
796+
797+
#[test]
798+
fn extracts_nests_relationship_orientation() {
799+
let dm = extract_data_model(NEW_REL_TYPES_IFC);
800+
// RelatingObject=#30 (door), RelatedObjects=(#31) (the nested panel).
801+
assert!(
802+
dm.relationships.iter().any(|r| {
803+
r.rel_type.eq_ignore_ascii_case("IFCRELNESTS")
804+
&& r.relating_id == 30
805+
&& r.related_id == 31
806+
}),
807+
"IFCRELNESTS (door -> panel) missing or misoriented: {:?}",
808+
dm.relationships
809+
);
810+
}
811+
812+
#[test]
813+
fn extracts_connects_path_elements_relationship_orientation() {
814+
let dm = extract_data_model(NEW_REL_TYPES_IFC);
815+
// RelatingElement=#10 (wall 1), RelatedElement=#11 (wall 2).
816+
assert!(
817+
dm.relationships.iter().any(|r| {
818+
r.rel_type.eq_ignore_ascii_case("IFCRELCONNECTSPATHELEMENTS")
819+
&& r.relating_id == 10
820+
&& r.related_id == 11
821+
}),
822+
"IFCRELCONNECTSPATHELEMENTS (wall -> wall) missing or misoriented: {:?}",
823+
dm.relationships
824+
);
825+
}
826+
827+
/// Control: a fixture with none of the four new types must extract exactly as
828+
/// before — none of them should ever appear for a model that never wrote
829+
/// them, so a future change to the new-type match arms can't silently start
830+
/// matching an unrelated type.
831+
#[test]
832+
fn fixture_without_new_types_is_unaffected() {
833+
let dm = extract_data_model(ASSOCIATIONS_IFC);
834+
assert!(
835+
!dm.relationships.iter().any(|r| {
836+
matches!(
837+
r.rel_type.to_uppercase().as_str(),
838+
"IFCRELASSIGNSTOGROUP"
839+
| "IFCRELASSIGNSTOGROUPBYFACTOR"
840+
| "IFCRELNESTS"
841+
| "IFCRELCONNECTSPATHELEMENTS"
842+
)
843+
}),
844+
"fixture has none of the new types, but one was extracted: {:?}",
845+
dm.relationships
846+
);
847+
}

0 commit comments

Comments
 (0)