diff --git a/src/blenderbim/blenderbim/bim/import_ifc.py b/src/blenderbim/blenderbim/bim/import_ifc.py index 967438af86..a3b714a202 100644 --- a/src/blenderbim/blenderbim/bim/import_ifc.py +++ b/src/blenderbim/blenderbim/bim/import_ifc.py @@ -428,10 +428,12 @@ class IfcImporter: props.has_blender_offset = True def guess_absolute_coordinate(self): - # Civil BIM applications like to work in absolute coordinates, where the ObjectPlacement is 0,0,0 but each - # individual coordinate of the shape representation is in absolute values. + # Civil BIM applications like to work in absolute coordinates, where the + # ObjectPlacement is usually 0,0,0 (but not always, so we'll need to + # check for the actual transformation) but each individual coordinate of + # the shape representation is in absolute values. offset_point = self.get_offset_point() - if not offset_point: + if offset_point is None: return props = bpy.context.scene.BIMGeoreferenceProperties props.blender_eastings = str(offset_point[0]) @@ -441,34 +443,41 @@ class IfcImporter: def get_offset_point(self): elements_checked = 0 - # If more than these points aren't far away, the file probably isn't absolutely positioned - element_checking_threshold = 100 - if self.file.schema == "IFC2X3": - # IFC2X3 does not have IfcCartesianPointList3D - point_lists = [] - else: - point_lists = self.file.by_type("IfcCartesianPointList3D") - for point_list in point_lists: - elements_checked += 1 - if elements_checked > element_checking_threshold: - return - for i, point in enumerate(point_list.CoordList): - if len(point) == 3 and self.is_point_far_away(point, is_meters=False): - return point - - for point in self.file.by_type("IfcCartesianPoint"): - is_used_in_placement = False - for inverse in self.file.get_inverse(point): - if inverse.is_a("IfcAxis2Placement3D"): - is_used_in_placement = True - break - if is_used_in_placement: + # If more than these elements aren't far away, the file probably isn't absolutely positioned + element_checking_threshold = 10 + for element in self.file.by_type("IfcElement"): + if not element.Representation: continue elements_checked += 1 if elements_checked > element_checking_threshold: return - if len(point.Coordinates) == 3 and self.is_point_far_away(point, is_meters=False): - return point.Coordinates + if not self.does_element_likely_have_geometry_far_away(element): + continue + shape = ifcopenshell.geom.create_shape(self.settings, element) + m = shape.transformation.matrix.data + mat = np.array( + ([m[0], m[3], m[6], m[9]], [m[1], m[4], m[7], m[10]], [m[2], m[5], m[8], m[11]], [0, 0, 0, 1]) + ) + point = np.array( + ( + shape.geometry.verts[0] / self.unit_scale, + shape.geometry.verts[1] / self.unit_scale, + shape.geometry.verts[2] / self.unit_scale, + 0.0, + ) + ) + return mat @ point + + def does_element_likely_have_geometry_far_away(self, element): + for representation in element.Representation.Representations: + for subelement in self.file.traverse(representation): + if subelement.is_a("IfcCartesianPointList3D"): + for point in subelement.CoordList: + if len(point) == 3 and self.is_point_far_away(point, is_meters=False): + return True + if subelement.is_a("IfcCartesianPoint"): + if len(subelement.Coordinates) == 3 and self.is_point_far_away(subelement, is_meters=False): + return True def apply_blender_offset_to_matrix_world(self, obj, matrix): props = bpy.context.scene.BIMGeoreferenceProperties @@ -1719,15 +1728,27 @@ class IfcImporter: and geometry.verts and self.is_point_far_away((geometry.verts[0], geometry.verts[1], geometry.verts[2])) ): + m = shape.transformation.matrix.data + mat = np.array( + ([m[0], m[3], m[6], m[9]], [m[1], m[4], m[7], m[10]], [m[2], m[5], m[8], m[11]], [0, 0, 0, 1]) + ) + offset_point = np.linalg.inv(mat) @ np.array( + ( + float(props.blender_eastings), + float(props.blender_northings), + float(props.blender_orthogonal_height), + 0.0, + ) + ) verts = [None] * len(geometry.verts) for i in range(0, len(geometry.verts), 3): verts[i], verts[i + 1], verts[i + 2] = ifcopenshell.util.geolocation.enh2xyz( geometry.verts[i], geometry.verts[i + 1], geometry.verts[i + 2], - float(props.blender_eastings) * self.unit_scale, - float(props.blender_northings) * self.unit_scale, - float(props.blender_orthogonal_height) * self.unit_scale, + offset_point[0] * self.unit_scale, + offset_point[1] * self.unit_scale, + offset_point[2] * self.unit_scale, float(props.blender_x_axis_abscissa), float(props.blender_x_axis_ordinate), ) diff --git a/src/blenderbim/test/bim/feature/project.feature b/src/blenderbim/test/bim/feature/project.feature index 189d244bd1..f88e918f94 100644 --- a/src/blenderbim/test/bim/feature/project.feature +++ b/src/blenderbim/test/bim/feature/project.feature @@ -270,15 +270,28 @@ Scenario: Load project elements - manual offset of object placements Given an empty Blender session And I press "bim.load_project(filepath='{cwd}/test/files/manual-geolocation.ifc', is_advanced=True)" When I set "scene.BIMProjectProperties.should_offset_model" to "True" - And I set "scene.BIMProjectProperties.model_offset_coordinates" to "-268388.5, -5774506.0, -21.899999618530273" + And I set "scene.BIMProjectProperties.model_offset_coordinates" to "268388500, 5774506000, 21900" And I press "bim.load_project_elements" Then the object "IfcPlate/1780 x 270 PRECAST WALL" is at "0,0,0" +Scenario: Load project elements - manual offset of cartesian points + Given an empty Blender session + And I press "bim.load_project(filepath='{cwd}/test/files/manual-geolocation-coords.ifc', is_advanced=True)" + When I set "scene.BIMProjectProperties.should_offset_model" to "True" + And I set "scene.BIMProjectProperties.model_offset_coordinates" to "1990711,5971553,22700" + And I press "bim.load_project_elements" + Then the object "IfcBuildingElementProxy/NAME" is at "0,0,0" + Scenario: Load project elements - auto offset of object placements Given an empty Blender session When I press "bim.load_project(filepath='{cwd}/test/files/auto-geolocation.ifc')" Then the object "IfcPlate/1780 x 270 PRECAST WALL" is at "0,0,0" +Scenario: Load project elements - auto offset of cartesian points + Given an empty Blender session + When I press "bim.load_project(filepath='{cwd}/test/files/manual-geolocation-coords.ifc')" + Then the object "IfcBuildingElementProxy/NAME" is at "0,0,0" + Scenario: Unload project Given an empty Blender session When I press "bim.load_project(filepath='{cwd}/test/files/basic.ifc', is_advanced=True)" diff --git a/src/blenderbim/test/files/manual-geolocation-coords.ifc b/src/blenderbim/test/files/manual-geolocation-coords.ifc new file mode 100644 index 0000000000..c0d843baf7 --- /dev/null +++ b/src/blenderbim/test/files/manual-geolocation-coords.ifc @@ -0,0 +1,122 @@ +ISO-10303-21; +HEADER; +FILE_DESCRIPTION ((''), '2;1'); +FILE_NAME ('', '2022-04-06T07:53:12', (''), (''), 'Xbim File Processor version 4.0.0.0', 'Xbim version 4.0.0.0', ''); +FILE_SCHEMA (('IFC2X3')); +ENDSEC; +DATA; +#1=IFCBUILDINGELEMENTPROXY('0WxtKqfTD7uRQVvprDAkUF',#13,'NAME','PourObject','PourObject',#14,#26,'ID20ef7534-a5d3-47e1-b69f-e73d4d2ae78f',.ELEMENT.); +#2=IFCOWNERHISTORY(#5,#6,$,.ADDED.,$,$,$,0); +#3=IFCPERSON($,'Davies','Tim',$,$,$,$,$); +#4=IFCORGANIZATION($,'ORG1',$,$,$); +#5=IFCPERSONANDORGANIZATION(#3,#4,$); +#7=IFCORGANIZATION($,'ORG2',$,$,$); +#6=IFCAPPLICATION(#7,'1.1','IfcTool','1234567890'); +#8=IFCOWNERHISTORY(#9,#12,$,.NOCHANGE.,$,$,$,1629180000); +#9=IFCPERSONANDORGANIZATION(#10,#11,$); +#10=IFCPERSON('PERSON_NAME','Undefined',$,$,$,$,$,$); +#11=IFCORGANIZATION($,'Trimble Solutions Corporation',$,$,$); +#12=IFCAPPLICATION(#11,'VERSION','APPLICATION','Multi material modeling'); +#13=IFCOWNERHISTORY(#5,#6,$,.MODIFIED.,$,$,$,0); +#14=IFCLOCALPLACEMENT(#15,#24); +#15=IFCLOCALPLACEMENT(#16,#23); +#16=IFCLOCALPLACEMENT(#17,#22); +#17=IFCLOCALPLACEMENT($,#18); +#18=IFCAXIS2PLACEMENT3D(#19,#20,#21); +#19=IFCCARTESIANPOINT((0.,0.,0.)); +#20=IFCDIRECTION((0.,0.,1.)); +#21=IFCDIRECTION((1.,0.,0.)); +#22=IFCAXIS2PLACEMENT3D(#19,#20,#21); +#23=IFCAXIS2PLACEMENT3D(#19,#20,#21); +#24=IFCAXIS2PLACEMENT3D(#19,#25,#21); +#25=IFCDIRECTION((0.,1.,0.)); +#26=IFCPRODUCTDEFINITIONSHAPE($,$,(#27)); +#27=IFCSHAPEREPRESENTATION(#28,'Body','Brep',(#32)); +#28=IFCGEOMETRICREPRESENTATIONSUBCONTEXT('Body','Model',*,*,*,*,#29,$,.MODEL_VIEW.,$); +#29=IFCGEOMETRICREPRESENTATIONCONTEXT($,'Model',3,1.E-05,#30,#31); +#30=IFCAXIS2PLACEMENT3D(#19,#20,#21); +#31=IFCDIRECTION((0.,1.)); +#32=IFCFACETEDBREP(#33); +#33=IFCCLOSEDSHELL((#34,#41,#46,#51,#54,#57)); +#34=IFCFACE((#35)); +#35=IFCFACEOUTERBOUND(#36,.T.); +#36=IFCPOLYLOOP((#37,#38,#39,#40)); +#37=IFCCARTESIANPOINT((1990711.,-22699.9999999999,5971553.)); +#38=IFCCARTESIANPOINT((1990711.,-24899.9999999999,5971553.)); +#39=IFCCARTESIANPOINT((1989711.,-24899.9999999999,5971553.)); +#40=IFCCARTESIANPOINT((1989711.,-22699.9999999999,5971553.)); +#41=IFCFACE((#42)); +#42=IFCFACEOUTERBOUND(#43,.T.); +#43=IFCPOLYLOOP((#38,#44,#45,#39)); +#44=IFCCARTESIANPOINT((1990711.,-24899.9999999999,5972053.)); +#45=IFCCARTESIANPOINT((1989711.,-24899.9999999999,5972053.)); +#46=IFCFACE((#47)); +#47=IFCFACEOUTERBOUND(#48,.T.); +#48=IFCPOLYLOOP((#44,#49,#50,#45)); +#49=IFCCARTESIANPOINT((1990711.,-22699.9999999999,5972053.)); +#50=IFCCARTESIANPOINT((1989711.,-22699.9999999999,5972053.)); +#51=IFCFACE((#52)); +#52=IFCFACEOUTERBOUND(#53,.T.); +#53=IFCPOLYLOOP((#49,#37,#40,#50)); +#54=IFCFACE((#55)); +#55=IFCFACEOUTERBOUND(#56,.T.); +#56=IFCPOLYLOOP((#45,#50,#40,#39)); +#57=IFCFACE((#58)); +#58=IFCFACEOUTERBOUND(#59,.T.); +#59=IFCPOLYLOOP((#49,#44,#38,#37)); +#60=IFCSTYLEDITEM(#32,(#61),$); +#61=IFCPRESENTATIONSTYLEASSIGNMENT((#62)); +#62=IFCSURFACESTYLE($,.BOTH.,(#63)); +#63=IFCSURFACESTYLERENDERING(#64,0.,$,$,$,$,$,$,.NOTDEFINED.); +#64=IFCCOLOURRGB($,0.333333333333333,1.,0.333333333333333); +#65=IFCRELAGGREGATES('3dUdx5Kov6leXMbv_Evj3F',#13,$,$,#72,(#74)); +#66=IFCOWNERHISTORY(#67,#70,$,.ADDED.,$,$,$,1648232845); +#67=IFCPERSONANDORGANIZATION(#68,#69,$); +#68=IFCPERSON($,'',$,$,$,$,$,$); +#69=IFCORGANIZATION($,'Simplebim',$,$,$); +#70=IFCAPPLICATION(#71,'Not Defined','Not Defined','Not Defined'); +#71=IFCORGANIZATION($,'Datacubist',$,$,$); +#72=IFCBUILDING('1cVfOSwd0DwZRaHgXu8a_f',#13,'Undefined',$,'',#16,$,$,.ELEMENT.,$,$,$); +#73=IFCOWNERHISTORY(#9,#12,$,.NOCHANGE.,$,$,$,1648135363); +#74=IFCBUILDINGSTOREY('1PRG1GisebkPvVjTc0dKhA',#13,'Undefined',$,'',#15,$,$,.ELEMENT.,0.); +#75=IFCRELAGGREGATES('0mzX8SC9TE2x9h5brYnb63',#13,$,$,#76,(#72)); +#76=IFCSITE('2AlxJCEVKnYUYG6VRdFCmy',#13,'SITE_NAME',$,'',#17,$,$,.ELEMENT.,(0,0,0),(0,0,0),$,$,$); +#77=IFCRELAGGREGATES('10$gKGL8H5nRoXtsiwnv6O',#13,$,$,#78,(#76)); +#78=IFCPROJECT('2tm8iIrI_$r8NRzOY_fHvI',#13,'PROJECT',$,$,$,$,(#29,#79),#81); +#79=IFCGEOMETRICREPRESENTATIONCONTEXT($,'Plan',3,1.E-05,#80,$); +#80=IFCAXIS2PLACEMENT3D(#19,#20,#21); +#81=IFCUNITASSIGNMENT((#82,#83,#84,#85,#86,#87,#88,#89,#90,#91,#92,#93,#94,#98,#101,#106)); +#82=IFCSIUNIT(*,.PLANEANGLEUNIT.,$,.RADIAN.); +#83=IFCSIUNIT(*,.AREAUNIT.,$,.SQUARE_METRE.); +#84=IFCSIUNIT(*,.LENGTHUNIT.,.MILLI.,.METRE.); +#85=IFCSIUNIT(*,.MASSUNIT.,.KILO.,.GRAM.); +#86=IFCSIUNIT(*,.POWERUNIT.,$,.WATT.); +#87=IFCSIUNIT(*,.PRESSUREUNIT.,.KILO.,.PASCAL.); +#88=IFCSIUNIT(*,.FORCEUNIT.,.KILO.,.NEWTON.); +#89=IFCSIUNIT(*,.ELECTRICVOLTAGEUNIT.,$,.VOLT.); +#90=IFCSIUNIT(*,.ELECTRICCURRENTUNIT.,$,.AMPERE.); +#91=IFCSIUNIT(*,.THERMODYNAMICTEMPERATUREUNIT.,$,.DEGREE_CELSIUS.); +#92=IFCSIUNIT(*,.TIMEUNIT.,$,.SECOND.); +#93=IFCSIUNIT(*,.VOLUMEUNIT.,$,.CUBIC_METRE.); +#94=IFCDERIVEDUNIT((#95,#97),.LINEARVELOCITYUNIT.,$); +#95=IFCDERIVEDUNITELEMENT(#96,1); +#96=IFCSIUNIT(*,.LENGTHUNIT.,$,.METRE.); +#97=IFCDERIVEDUNITELEMENT(#92,-1); +#98=IFCDERIVEDUNIT((#99,#100),.VOLUMETRICFLOWRATEUNIT.,$); +#99=IFCDERIVEDUNITELEMENT(#93,1); +#100=IFCDERIVEDUNITELEMENT(#92,-1); +#101=IFCDERIVEDUNIT((#102,#103,#105),.THERMALTRANSMITTANCEUNIT.,$); +#102=IFCDERIVEDUNITELEMENT(#86,1); +#103=IFCDERIVEDUNITELEMENT(#104,-1); +#104=IFCSIUNIT(*,.THERMODYNAMICTEMPERATUREUNIT.,$,.KELVIN.); +#105=IFCDERIVEDUNITELEMENT(#83,-1); +#106=IFCDERIVEDUNIT((#107),.SOUNDPRESSUREUNIT.,$); +#107=IFCDERIVEDUNITELEMENT(#108,1); +#108=IFCSIUNIT(*,.PRESSUREUNIT.,$,.PASCAL.); +#109=IFCRELCONTAINEDINSPATIALSTRUCTURE('2et5jeJgD1OBJkFAZevcaR',#13,$,$,(#1),#74); +#159=IFCRELDEFINESBYTYPE('0l7quQJkD3hv69HoK6sZxD',#13,$,$,(#1),#160); +#160=IFCBUILDINGELEMENTPROXYTYPE('0wvZ60EKXBihi$_raqzBCm',#13,'PourObject',$,$,$,$,$,$,.NOTDEFINED.); +#161=IFCRELASSOCIATESMATERIAL('1RfoofYsj288$ePu$MZZC_',#13,$,$,(#1),#162); +#162=IFCMATERIAL('Undefined/C40/50'); +ENDSEC; +END-ISO-10303-21;