/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ /** * Carry an element's Pset / Qto / classification / material / type * relationships from a source IfcProduct onto one or more target * products. Used when an authoring operation produces siblings of * a source entity (split, duplicate, etc.) — both halves of a split * wall should keep the source's FireRating, etc. * * Strategy: append the target id(s) to each relationship's * `RelatedObjects` list rather than cloning the relationship entity * itself. Cheaper, less overlay growth, and the IFC schema allows * many objects to share a single rel. Downstream tooling (IDS, Pset * dumps, exports) reads each rel's RelatedObjects list, so the * targets become first-class members. * * The relationships we touch: * * IfcRelDefinesByProperties — Psets / Qtos (the big one) * IfcRelDefinesByType — occurrence → type binding * IfcRelAssociatesClassification * IfcRelAssociatesMaterial * IfcRelContainedInSpatialStructure — storey containment * IfcRelAggregates / IfcRelNests — element assemblies * * Containment is handled separately by callers that already use * `addWallToStore` / `addSlabToStore` etc. (those builders emit a * fresh `IfcRelContainedInSpatialStructure` for the new entity), so * this helper deliberately skips it. * * Returns the number of relationships touched so callers can log * useful telemetry. Returns 0 silently when the source has no * relationships — that's a valid case. */ import type { IfcDataStore } from '@ifc-lite/parser'; import type { IfcAttributeValue, MutablePropertyView, StoreEditor } from '@ifc-lite/mutations'; import { asExpressIdRef, readAttributes } from './placement-core.js'; /** Relationship entity types we touch (case follows STEP storage form). */ const RELATIONSHIPS_TO_CLONE = [ // [type, related-objects-attribute-index, related-object-is-list-of-refs?] ['IFCRELDEFINESBYPROPERTIES', 4], ['IFCRELDEFINESBYTYPE', 4], ['IFCRELASSOCIATESCLASSIFICATION', 4], ['IFCRELASSOCIATESMATERIAL', 4], ['IFCRELAGGREGATES', 5], ['IFCRELNESTS', 5], ] as const; export interface CloneMetadataResult { /** Number of relationships that gained at least one target. */ relationshipsTouched: number; } /** * Coerce a raw RelatedObjects attribute into an array of express ids. * Overlay-stored refs are `#X` strings; parsed source refs are bare * numbers (see `asExpressIdRef`). */ function relatedObjectIds(raw: unknown): number[] { if (!Array.isArray(raw)) return []; const ids: number[] = []; for (const v of raw) { const id = asExpressIdRef(v); if (id !== null) ids.push(id); } return ids; } /** * Append target ids to the raw RelatedObjects list while preserving * the source's ref form (`#X` strings if any source ref is a string, * raw numbers otherwise). Avoids accidentally normalising overlay * refs into bare numbers, which the exporter wouldn't recognise. */ function appendTargetsToList( raw: unknown, targets: number[], ): unknown[] { const list: unknown[] = Array.isArray(raw) ? raw.slice() : []; const usesStrings = list.some((v) => typeof v === 'string'); for (const t of targets) { list.push(usesStrings ? `#${t}` : t); } return list; } export function cloneElementMetadata( dataStore: IfcDataStore, view: MutablePropertyView, editor: StoreEditor, sourceExpressId: number, targetExpressIds: number[], ): CloneMetadataResult { if (targetExpressIds.length === 0) return { relationshipsTouched: 0 }; // Dedupe caller-side input so a target listed twice doesn't // produce two appends. The per-rel `currentRelated.includes` // guard below would catch it on the second pass, but on the // first pass both copies would land in the new list. const uniqueTargets = Array.from(new Set(targetExpressIds)); let touched = 0; for (const [type, index] of RELATIONSHIPS_TO_CLONE) { const relIds = dataStore.entityIndex.byType.get(type) ?? []; for (const relId of relIds) { const attrs = readAttributes(dataStore, view, editor, relId); if (!attrs) continue; const currentRelated = relatedObjectIds(attrs[index]); if (!currentRelated.includes(sourceExpressId)) continue; // Skip targets that are already in the list — avoids duplicates // when this is called twice for the same operation (idempotent). const additions = uniqueTargets.filter((t) => !currentRelated.includes(t)); if (additions.length === 0) continue; const newList = appendTargetsToList(attrs[index], additions); editor.setPositionalAttribute(relId, index, newList as IfcAttributeValue); touched++; } } return { relationshipsTouched: touched }; }