| name | crosstech-errors-coordinate-mismatch |
| description | Use when BIM models appear at the wrong location, wrong scale, or wrong orientation in GIS or web viewers. Provides a diagnostic decision tree for coordinate-related errors: Y-up vs Z-up axis swap, unit mismatches (mm/m/ft), missing georeferencing, CRS mismatches, and datum transformation errors. Covers coordinate debugging across Blender, Three.js, QGIS, IFC, Revit, and FreeCAD. Keywords: wrong location, coordinate mismatch, Y-up Z-up, scale error, EPSG, georeferencing missing, axis swap, unit mismatch, CRS error, datum shift, model in wrong place, building too small, upside down, rotated model.
|
| license | MIT |
| compatibility | Designed for Claude Code. Covers all AEC tools with coordinate systems. |
| metadata | {"author":"OpenAEC-Foundation","version":"1.0"} |
crosstech-errors-coordinate-mismatch
Quick Reference
| Symptom | Likely Cause | Jump To |
|---|
| Model rotated 90° / lying on side | Y-up vs Z-up axis swap | Failure Mode 1 |
| Model 1000x too large or too small | Unit mismatch (mm/m/ft) | Failure Mode 2 |
| Model at (0°N, 0°E) in Gulf of Guinea | Missing georeferencing | Failure Mode 3 |
| Model offset by hundreds of km | Wrong CRS / EPSG code | Failure Mode 4 |
| Building rotated on map, footprint correct | True North rotation error | Failure Mode 5 |
| Coordinates jump at project boundary | UTM zone boundary | Failure Mode 6 |
| Building floating ~43m above terrain | Vertical datum confusion | Failure Mode 7 |
Axis Convention Table
| Tool | Up Axis | Handedness | Default Units | Coordinate Order |
|---|
| Blender | Z-up | Right-handed | Meters | (X, Y, Z) |
| Three.js | Y-up | Right-handed | Unitless | (X, Y, Z) |
| QGIS | Z-up (2.5D) | Right-handed | Map units (m/deg) | (X=East, Y=North) |
| IFC | Z-up | Right-handed | Project units (mm/m) | (X, Y, Z) |
| Revit | Z-up | Right-handed | Internal: feet | (X, Y, Z) |
| FreeCAD | Z-up | Right-handed | Internal: mm | (X, Y, Z) |
| Speckle | Z-up | Right-handed | Meters | (X, Y, Z) |
| web-ifc | Z-up | Right-handed | IFC project units | (X, Y, Z) |
Key boundary: ALL BIM/GIS tools use Z-up. ONLY web 3D renderers (Three.js, Babylon.js) use Y-up.
Diagnostic Decision Tree
START: Model appears wrong in target application
│
├─ Is the model rotated 90°?
│ YES → Failure Mode 1 (Y/Z axis swap)
│
├─ Is the model at the correct location but wrong size?
│ ├─ ~1000x too large → IFC in mm, target expects m (Scale missing)
│ ├─ ~3.28x too large → IFC in feet, target expects m
│ ├─ ~0.3x too small → IFC in m, target expects feet
│ └─ YES → Failure Mode 2 (unit mismatch)
│
├─ Is the model near (0, 0) / Gulf of Guinea?
│ YES → Failure Mode 3 (missing georeferencing)
│
├─ Is the model offset by >1 km from expected position?
│ ├─ Offset ~100–500 km → Failure Mode 4 (wrong CRS)
│ ├─ Offset ~500,000 m in easting → Failure Mode 6 (UTM zone)
│ └─ Offset < 5 m → Check datum transformation accuracy
│
├─ Is the model at the right position but rotated?
│ YES → Failure Mode 5 (True North rotation)
│
├─ Is the model at correct X/Y but wrong height (~40-44m off)?
│ YES → Failure Mode 7 (vertical datum)
│
└─ None of the above → Check for compound errors (multiple failures)
Failure Mode 1: Y-Up vs Z-Up Axis Swap
Symptom
Model appears rotated 90° — buildings lie on their side. Walls are horizontal, floors are vertical.
Cause
BIM tools (IFC, Blender, Revit, QGIS) use Z-up. Web 3D renderers (Three.js, Babylon.js) use Y-up. Importing BIM data into a web viewer without axis transformation produces a 90° rotation.
Detection
const box = new THREE.Box3().setFromObject(model);
const size = box.getSize(new THREE.Vector3());
if (size.y < 0.1 && size.z > 1.0) {
console.error("Y/Z axis swap detected: model is lying flat");
}
Fix
function bimToThreeJs(x, y, z) {
return { x: x, y: z, z: -y };
}
The negation of y → -z preserves right-handedness. Without negation, the model appears mirrored.
ALWAYS apply this transform when loading IFC/BIM data into Three.js or Babylon.js.
NEVER apply this transform when loading into Blender, QGIS, or other Z-up tools.
Tool-Specific Notes
- web-ifc / ThatOpen Components: Handles Y/Z swap automatically when using
FragmentsManager. Manual loading via IfcLoader requires explicit swap.
- Three.js GLTFLoader: glTF is Y-up by spec — no swap needed for glTF files. Only swap for raw IFC coordinate data.
- Speckle: Converts axes automatically per target application on receive.
Failure Mode 2: Unit Mismatch (mm/m/ft)
Symptom
Model appears at the correct location but is absurdly large or small. A 10m building spans 10km on the map (mm→m) or appears as a 3m speck (feet→m without conversion).
Detection
import ifcopenshell
ifc = ifcopenshell.open("model.ifc")
units = ifc.by_type("IfcUnitAssignment")[0]
for unit in units.Units:
if hasattr(unit, "UnitType") and unit.UnitType == "LENGTHUNIT":
if hasattr(unit, "Prefix") and unit.Prefix == "MILLI":
project_unit = "mm"
expected_scale = 0.001
elif hasattr(unit, "Name") and unit.Name == "FOOT":
project_unit = "ft"
expected_scale = 0.3048
else:
project_unit = "m"
expected_scale = 1.0
map_conv = ifc.by_type("IfcMapConversion")
if map_conv:
actual_scale = map_conv[0].Scale or 1.0
if abs(actual_scale - expected_scale) > 0.0001:
print(f"SCALE MISMATCH: project={project_unit}, "
f"MapConversion.Scale={actual_scale}, expected={expected_scale}")
Common Scale Factors
| From | To | Scale Factor |
|---|
| mm | m | 0.001 |
| m | m | 1.0 |
| ft | m | 0.3048 |
| in | m | 0.0254 |
Fix
Set IfcMapConversion.Scale to the correct conversion factor from project units to CRS map units.
ALWAYS check IfcUnitAssignment before interpreting IfcMapConversion.Scale.
NEVER assume project units are meters — Revit uses feet internally, FreeCAD uses mm.
Failure Mode 3: Missing Georeferencing
Symptom
Model appears at latitude 0°, longitude 0° (Gulf of Guinea, West Africa) or at the CRS origin point. This is the single most common BIM↔GIS integration failure.
Detection
import ifcopenshell
ifc = ifcopenshell.open("model.ifc")
has_map_conv = len(ifc.by_type("IfcMapConversion")) > 0
has_crs = len(ifc.by_type("IfcProjectedCRS")) > 0
if not has_map_conv:
print("MISSING: IfcMapConversion — no coordinate transformation defined")
if not has_crs:
print("MISSING: IfcProjectedCRS — no target CRS defined")
Fix
Add georeferencing using IfcOpenShell:
import ifcopenshell
import ifcopenshell.api
ifc = ifcopenshell.open("model.ifc")
ifcopenshell.api.run("georeference.add_georeferencing", ifc)
ifcopenshell.api.run("georeference.edit_georeferencing", ifc,
projected_crs={"Name": "EPSG:28992"},
coordinate_operation={
"Eastings": 155000.0,
"Northings": 463000.0,
"OrthogonalHeight": 0.0,
})
ifc.write("model_georef.ifc")
ALWAYS verify Eastings/Northings fall within the valid range of the target CRS.
NEVER leave IfcProjectedCRS.Name empty — downstream tools rely on this field to resolve the CRS.
Failure Mode 4: Wrong CRS / EPSG Code
Symptom
Model appears offset by tens to hundreds of kilometers from the expected position.
Detection
Validate that coordinate values are plausible for the assigned CRS:
| CRS | Valid Easting Range | Valid Northing Range |
|---|
| EPSG:28992 (RD New) | -7,000 to 300,000 | 289,000 to 629,000 |
| EPSG:32631 (UTM 31N) | 166,000 to 834,000 | 0 to 9,400,000 |
| EPSG:32632 (UTM 32N) | 166,000 to 834,000 | 0 to 9,400,000 |
import ifcopenshell
ifc = ifcopenshell.open("model.ifc")
map_conv = ifc.by_type("IfcMapConversion")
crs = ifc.by_type("IfcProjectedCRS")
if map_conv and crs:
e = map_conv[0].Eastings
n = map_conv[0].Northings
crs_name = crs[0].Name
if "28992" in (crs_name or ""):
if not (-7000 <= e <= 300000 and 289000 <= n <= 629000):
print(f"COORDINATES OUT OF RANGE for {crs_name}: "
f"E={e}, N={n}")
Fix
- Determine the correct CRS from project documentation or site survey data
- Verify with a known reference point (e.g., a building corner with known GPS coordinates)
- Use pyproj to cross-check:
from pyproj import Transformer
t = Transformer.from_crs("EPSG:4326", "EPSG:28992", always_xy=True)
expected_e, expected_n = t.transform(lon, lat)
print(f"Expected: E={expected_e:.1f}, N={expected_n:.1f}")
print(f"Actual: E={map_conv[0].Eastings}, N={map_conv[0].Northings}")
ALWAYS use always_xy=True with pyproj — without it, axis order depends on CRS definition and causes silent coordinate swaps.
Failure Mode 5: True North Rotation Error
Symptom
Building footprint is at the correct map location but the building is rotated. Walls that should be parallel to streets appear at an angle.
Detection
import math
import ifcopenshell
ifc = ifcopenshell.open("model.ifc")
map_conv = ifc.by_type("IfcMapConversion")
if map_conv:
xa = map_conv[0].XAxisAbscissa or 1.0
xo = map_conv[0].XAxisOrdinate or 0.0
angle_rad = math.atan2(xo, xa)
angle_deg = math.degrees(angle_rad)
vec_length = math.sqrt(xa**2 + xo**2)
print(f"True North rotation: {angle_deg:.2f}°")
if abs(vec_length - 1.0) > 0.001:
print(f"WARNING: direction vector not normalized (length={vec_length:.4f})")
Fix
Set XAxisAbscissa = cos(angle) and XAxisOrdinate = sin(angle) where angle is the anti-clockwise rotation from grid north (CRS Y-axis) to project north.
ALWAYS verify the direction vector is normalized (length = 1.0).
NEVER confuse the sign convention — positive angle = anti-clockwise rotation in IFC.
Failure Mode 6: UTM Zone Boundary
Symptom
Project straddles a UTM zone boundary. Coordinates jump by ~500,000m in easting, or measurements near the boundary show 0.04% distortion.
Detection
from pyproj import CRS
def utm_zone_from_longitude(lon):
"""Return UTM zone number for a given longitude."""
return int((lon + 180) / 6) + 1
western_nl_zone = utm_zone_from_longitude(4.5)
eastern_nl_zone = utm_zone_from_longitude(6.5)
Fix
NEVER mix coordinates from different UTM zones in one project.
Use a single CRS that covers the entire project area:
- Netherlands: EPSG:28992 (RD New) covers the entire country
- Europe-wide: EPSG:3035 (ETRS89 / LAEA) for pan-European projects
- If UTM is required, pick the zone containing the project centroid
Failure Mode 7: Vertical Datum Confusion
Symptom
Model floats ~42–44 meters above the terrain (Netherlands) or is buried below ground. Horizontal position is correct.
Cause
Mixing NAP (Normaal Amsterdams Peil) orthometric heights with WGS84 ellipsoidal heights. In the Netherlands, the geoid undulation (difference between ellipsoid and geoid) is approximately 42–44 meters.
Detection
import ifcopenshell
ifc = ifcopenshell.open("model.ifc")
crs = ifc.by_type("IfcProjectedCRS")
map_conv = ifc.by_type("IfcMapConversion")
if crs:
vert_datum = crs[0].VerticalDatum
if vert_datum is None:
print("WARNING: No vertical datum specified — height interpretation ambiguous")
if map_conv:
h = map_conv[0].OrthogonalHeight
if abs(h) > 40 and abs(h) < 50:
print(f"SUSPICIOUS: OrthogonalHeight={h}m — possible NAP↔ellipsoid confusion")
Fix
from pyproj import Transformer
t = Transformer.from_crs(
"EPSG:7415",
"EPSG:4979",
always_xy=True
)
lon, lat, h_ellipsoid = t.transform(easting, northing, h_nap)
geoid_undulation = h_ellipsoid - h_nap
ALWAYS specify IfcProjectedCRS.VerticalDatum (e.g., "NAP") to make height interpretation unambiguous.
NEVER assume heights are ellipsoidal — most BIM models use orthometric (above sea level) heights.
Critical Rules
- ALWAYS check axis conventions before crossing a tool boundary — Z-up to Y-up swaps are the most common error.
- ALWAYS verify
IfcMapConversion.Scale matches the ratio of project units to CRS units.
- ALWAYS use
always_xy=True when creating pyproj Transformers.
- ALWAYS validate coordinate ranges against the assigned CRS before declaring georeferencing correct.
- NEVER assume georeferencing exists — explicitly check for
IfcMapConversion and IfcProjectedCRS.
- NEVER mix coordinates from different UTM zones in a single project.
- NEVER assume heights are ellipsoidal — BIM models use orthometric heights by default.
- NEVER skip the direction vector normalization check for True North rotation.
- ALWAYS check for compound errors — multiple failure modes can occur simultaneously.
- ALWAYS verify fixes with a known reference point before declaring the issue resolved.
Reference Links
Official Sources