├── .DS_Store ├── .gitattributes ├── .vs ├── VSWorkspaceState.json ├── slnx.sqlite └── venv │ └── v16 │ └── .suo ├── .vscode └── launch.json ├── LICENSE ├── README.md ├── __pycache__ └── app.cpython-311.pyc ├── app.py ├── check.py ├── georeference_ifc ├── .DS_Store ├── IFC2X3_Geolocation.ifc ├── __init__.py ├── __pycache__ │ ├── __init__.cpython-311.pyc │ └── main.cpython-311.pyc └── main.py ├── static ├── .DS_Store ├── images │ ├── .DS_Store │ ├── back.png │ ├── chek-logo.png │ ├── conv.png │ ├── eu.jpg │ ├── flavicon.png │ ├── info.png │ ├── loading.png │ ├── logo1.png │ ├── logo2.png │ ├── table.png │ ├── upload.png │ ├── way1.png │ ├── way2.png │ └── way3.png └── main.css ├── templates ├── .DS_Store ├── build │ └── three.module.js ├── chek.html ├── convert.html ├── guid.html ├── jsm │ ├── .DS_Store │ ├── controls │ │ ├── .DS_Store │ │ └── OrbitControls.js │ └── utils │ │ ├── .DS_Store │ │ └── BufferGeometryUtils.js ├── main.css ├── result.html ├── survey.html ├── upload.html ├── view.html └── view3D.html └── uploads ├── .DS_Store ├── CYAA0BF3.gml.ifc └── file.ifc /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tudelft3d/ifcgref/221b05a7bf9171e3b788bb72b7dc23117335a0bf/.DS_Store -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.vs/VSWorkspaceState.json: -------------------------------------------------------------------------------- 1 | { 2 | "ExpandedNodes": [ 3 | "" 4 | ], 5 | "SelectedNode": "\\app.py", 6 | "PreviewInSolutionExplorer": false 7 | } -------------------------------------------------------------------------------- /.vs/slnx.sqlite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tudelft3d/ifcgref/221b05a7bf9171e3b788bb72b7dc23117335a0bf/.vs/slnx.sqlite -------------------------------------------------------------------------------- /.vs/venv/v16/.suo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tudelft3d/ifcgref/221b05a7bf9171e3b788bb72b7dc23117335a0bf/.vs/venv/v16/.suo -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "chrome", 6 | "request": "launch", 7 | "name": "Launch Chrome against localhost", 8 | "url": "http://127.0.0.1:5500", // URL to your live server 9 | "webRoot": "${workspaceFolder}" 10 | } 11 | ] 12 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 3D geoinformation research group at TU Delft 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # IfcGref Web-Based Application 3 | 4 | ![ifcgref-release](https://github.com/tudelft3d/ifcgref/assets/50393714/e335cd23-d063-4f86-8cdf-d9898b6a955a) 5 | 6 | 7 | ## Overview 8 | 9 | This Flask-based application serves the purpose of georeferencing IFC (Industry Foundation Classes) files, which are commonly used in the context of Building Information Modeling (BIM) data exchange. To accomplish georeferencing, the application leverages the **IFCMapConversion** entity in IFC4, which facilitates the updating of data and the conversion from a local Coordinate Reference System (CRS), often referred to as the engineering coordinate system, into the coordinate reference system of the underlying map (Projected CRS). It's accessible at https://ifcgref.bk.tudelft.nl. 10 | 11 | 12 | 13 | ## Prerequisites 14 | 15 | Before running the application, make sure you have the following prerequisites installed on your system: 16 | 17 | - Python 3 18 | - Flask 19 | - ifcopenshell 20 | - pyproj 21 | - pint 22 | - numpy 23 | - scipy 24 | - pandas 25 | - shapely 26 | 27 | You can install these dependencies using pip: 28 | 29 | ```bash 30 | pip install Flask ifcopenshell pyproj pint numpy scipy pandas shapely 31 | ``` 32 | 33 | ## Supported IFC versions 34 | 35 | 36 | Coordinate operations become accessible starting from IFC 4. For earlier versions like the widely utilized IFC2x3, the utilization of Property sets (Pset) is employed to enable georeferencing. The table below outlines the supported versions: 37 | 38 | | Version | Name | 39 | | -------- | ------- | 40 | | 4.3.2.0 | IFC 4.3 ADD2 | 41 | | 4.0.2.0 | IFC4 ADD2 TC1 | 42 | | 4.0.2.1 | IFC4 ADD2 | 43 | | 4.0.2.0 | IFC4 ADD1 | 44 | | 4.0.0.0 | IFC4 | 45 | | 2.3.0.1 | IFC2x3 TC1 | 46 | | 2.3.0.0 | IFC2x3 | 47 | 48 | 49 | ## Usage 50 | 51 | 1. Clone this repository or download the application files to your local machine. 52 | 53 | 2. Navigate to the project directory in your terminal. 54 | 55 | 3. Run the Flask application: 56 | 57 | ```bash 58 | python app.py 59 | ``` 60 | This will start the Flask development server. 61 | 62 | 4. Access the application in your web browser by going to http://localhost:5000/. 63 | 5. Follow the on-screen instructions to upload an IFC file and specify the target EPSG code. 64 | 6. The application will georeference the IFC file and provide details about the process. 65 | 7. You can then visualize the georeferenced IFC file on the map and download it. 66 | 67 | ## File Structure 68 | 69 | - app.py: The main Flask application file. 70 | - static/: Directory to store static files (e.g., GeoJSON output). 71 | - templates/: HTML templates for the web interface. 72 | - uploads/: Directory to temporarily store uploaded IFC files. 73 | - envelop/: Directory to EnvelopExtractor exe files and temporary store shell produced files. 74 | 75 | ## Workflow 76 | 77 | ![Screenshot 2024-02-26 at 17 28 20 (2)](https://github.com/tudelft3d/ifcgref/assets/50393714/3d14b4c7-9652-4b77-bc5b-77bd2a736341) 78 | 79 | ## HTTP request 80 | 81 | For streamlined handling of incoming IFC files by developers, whether they are georeferenced or not, a specialized section called "devs" is available at https://ifcgref.bk.tudelft.nl/devs. Developers can engage with this section by submitting an HTTP request containing the IFC file, and in response, the server provides them with a corresponding response. 82 | 83 | Sample of HTTP request from the devs section using a python script: 84 | 85 | ```bash 86 | import requests 87 | 88 | url = 'https://ifcgref.bk.tudelft.nl/devs' 89 | file_path = './00.ifc' 90 | 91 | 92 | data = { 93 | 'file': ('00.ifc', open(file_path, 'rb')) 94 | } 95 | 96 | 97 | # Make a POST request to the /devs route with the data 98 | response = requests.post(url, files=data) 99 | 100 | # Print the response content 101 | print(response.text) 102 | ``` 103 | 104 | 105 | ## Credits 106 | 107 | - This application uses the Flask web framework for the user interface. 108 | - It leverages the ifcopenshell library for working with IFC files. 109 | - Georeferencing is performed using pyproj for coordinate transformations. 110 | - The optimization is performed using SciPy and in particular scipy.optimize.least_squares function. 111 | - For the vizualization feature IfcEnvelopeExtractor is used for generating the roof-print of the 3D BIM model. 112 | 113 | 114 | 115 | ## Acknowledgments 116 | 117 | This project has received funding from the European Union’s Horizon Europe programme under Grant Agreement No.101058559 (CHEK: Change toolkit for digital building permit). 118 | 119 | 120 | ## References 121 | 122 | BuildingSMART. IFC Specifications database. https://technical.buildingsmart.org/standards/ifc/ifc-schema-specifications/ 123 | 124 | BuildingSMART Australasia (2020). User Guide for Geo-referencing in IFC, version 2.0. https://www.buildingsmart.org/wp-content/uploads/2020/02/User-Guide-for-Geo-referencing-in-IFC-v2.0.pdf 125 | 126 | https://github.com/stijngoedertier/georeference-ifc#readme 127 | -------------------------------------------------------------------------------- /__pycache__/app.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tudelft3d/ifcgref/221b05a7bf9171e3b788bb72b7dc23117335a0bf/__pycache__/app.cpython-311.pyc -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, render_template, request, redirect, url_for, session, make_response, send_from_directory # Import the redirect function 2 | from werkzeug.utils import secure_filename 3 | import os 4 | import ifcopenshell 5 | import ifcopenshell.geom 6 | import georeference_ifc 7 | import re 8 | import pyproj 9 | from pyproj import Transformer 10 | import pint 11 | import numpy as np 12 | import math 13 | from scipy.optimize import leastsq 14 | import pandas as pd 15 | import json 16 | from shapely.geometry import Polygon, mapping 17 | import ifcopenshell.util.placement 18 | import subprocess 19 | import time 20 | 21 | 22 | app = Flask(__name__, static_url_path='/static', static_folder='static') 23 | app.secret_key = '88746898' 24 | app.config['UPLOAD_FOLDER'] = 'uploads' 25 | ALLOWED_EXTENSIONS = {'ifc'} # Define allowed file extensions as a set 26 | 27 | # Function to check if a filename has an allowed extension 28 | def allowed_file(filename): 29 | return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS 30 | 31 | def georef(ifc_file): 32 | geo = False 33 | #check ifc version 34 | version = ifc_file.schema 35 | message = f"IFC version: {version}\n" 36 | # Check the file is georefed or not 37 | mapconversion = None 38 | crs = None 39 | 40 | if ifc_file.schema[:4] == 'IFC4': 41 | project = ifc_file.by_type("IfcProject")[0] 42 | for c in (m for c in project.RepresentationContexts for m in c.HasCoordinateOperation): 43 | mapconversion = c 44 | crs = c.TargetCRS 45 | if mapconversion is not None: 46 | message += "IFC file is georeferenced.\n" 47 | geo = True 48 | if ifc_file.schema == 'IFC2X3': 49 | site = ifc_file.by_type("IfcSite")[0] 50 | psets = ifcopenshell.util.element.get_psets(site) 51 | if 'ePSet_MapConversion' in psets.keys() and 'ePSet_ProjectedCRS' in psets.keys(): 52 | message += "IFC file is georeferenced.\n" 53 | geo = True 54 | return message , geo 55 | 56 | def infoExt(filename , epsgCode): 57 | ureg = pint.UnitRegistry() 58 | ifc_file = fileOpener(filename) 59 | #check ifc version 60 | version = ifc_file.schema 61 | messages = [('IFC version', version)] 62 | ifc_site = ifc_file.by_type("IfcSite") 63 | 64 | 65 | #Find Longtitude and Latitude 66 | RLat = ifc_site[0].RefLatitude 67 | RLon = ifc_site[0].RefLongitude 68 | RElev = ifc_site[0].RefElevation 69 | if RLat is not None and RLon is not None: 70 | x0= (float(RLat[0]) + float(RLat[1])/60 + float(RLat[2]+RLat[3]/1000000)/(60*60)) 71 | y0= (float(RLon[0]) + float(RLon[1])/60 + float(RLon[2]+RLon[3]/1000000)/(60*60)) 72 | session['Refl'] = True 73 | if hasattr(ifc_file.by_type("IfcSite")[0], "ObjectPlacement") and ifc_file.by_type("IfcSite")[0].ObjectPlacement.is_a("IfcLocalPlacement"): 74 | local_placement = ifc_file.by_type("IfcSite")[0].ObjectPlacement.RelativePlacement 75 | # Check if the local placement is an IfcAxis2Placement3D 76 | if local_placement.is_a("IfcAxis2Placement3D"): 77 | local_origin = local_placement.Location.Coordinates 78 | bx,by,bz= local_origin 79 | messages.append(('IFC Local Origin', local_origin)) 80 | else: 81 | errorMessage = "Local placement is not IfcAxis2Placement3D." 82 | return messages, errorMessage 83 | else: 84 | errorMessage = "IfcSite does not have a local placement." 85 | return messages, errorMessage 86 | else: 87 | session['Refl'] = False 88 | messages.append(('RefLatitude or RefLongitude', 'Not available')) 89 | Refl = session.get('Refl') 90 | crs = None 91 | if ifc_file.schema[:4] != 'IFC4' and ifc_file.schema != 'IFC2X3': 92 | errorMessage = "IFC2X3, IFC4, and newer versions are supported.\n" 93 | return messages, errorMessage 94 | 95 | # Find local origin 96 | 97 | 98 | # Target CRS unit name 99 | try: 100 | crs = pyproj.CRS.from_epsg(int(epsgCode)) 101 | except: 102 | errorMessage = "CRS is not available." 103 | return messages, errorMessage 104 | 105 | 106 | crsunit = crs.axis_info[0].unit_name 107 | 108 | if crs.is_projected: 109 | messages.append(('Target CRS Type', 'Projected')) 110 | messages.append(('Target CRS EPSG', epsgCode)) 111 | 112 | else: 113 | errorMessage = "CRS is not projected (geographic)." 114 | return messages, errorMessage 115 | target_epsg = "EPSG:"+str(epsgCode) 116 | transformer = Transformer.from_crs("EPSG:4326", target_epsg) 117 | # IFC length unit name 118 | ifc_units = ifc_file.by_type("IfcUnitAssignment")[0].Units 119 | for ifc_unit in ifc_units: 120 | if ifc_unit.is_a("IfcSIUnit") and ifc_unit.UnitType == "LENGTHUNIT": 121 | if ifc_unit.Prefix is not None: 122 | ifcunit = ifc_unit.Prefix + ifc_unit.Name 123 | else: 124 | ifcunit = ifc_unit.Name 125 | try: 126 | quantity = unitmapper(ifcunit) 127 | ifcmeter = quantity.to(ureg.meter).magnitude 128 | except: 129 | ifcmeter = None 130 | # try: 131 | # if ifcunit in unit_mapping: 132 | # quantity = 1 * unit_mapping[ifcunit] 133 | # ifcmeter = quantity.to(ureg.meter).magnitude 134 | # else: 135 | # ifcmeter = None 136 | # except: 137 | # ifcmeter = None 138 | try: 139 | quantity = unitmapper(crsunit) 140 | crsmeter = quantity.to(ureg.meter).magnitude 141 | except: 142 | crsmeter = None 143 | 144 | # try: 145 | # if crsunit in unit_mapping: 146 | # quantity = 1 * unit_mapping[crsunit] 147 | # crsmeter = quantity.to(ureg.meter).magnitude 148 | # else: 149 | # crsmeter = None 150 | # except: 151 | # crsmeter = None 152 | 153 | if crsmeter is not None and ifcmeter is not None: 154 | coeff= ifcmeter/crsmeter 155 | else: 156 | errorMessage = "IFC/Map unit error" 157 | return messages, errorMessage 158 | if Refl: 159 | messages.append(("Reference Longitude",y0)) 160 | messages.append(("Reference Latitude",x0)) 161 | messages.append(("Reference Elevation",RElev)) 162 | 163 | messages.append(("Target CRS Unit",str.lower(crsunit))) 164 | 165 | session['mapunit'] = str.lower(crsunit) 166 | 167 | if ifcunit: 168 | unit_name = ifcunit 169 | messages.append(("IFC Unit",str.lower(unit_name))) 170 | session['ifcunit'] = str.lower(unit_name) 171 | 172 | else: 173 | errorMessage = "No length unit found in the IFC file." 174 | return messages, errorMessage 175 | messages.append(("Unit Conversion Ratio",coeff)) 176 | errorMessage = "" 177 | session['coeff'] = coeff 178 | if Refl: 179 | x1,y1,z1 = transformer.transform(x0,y0,RElev) 180 | x2= x1*coeff 181 | y2= y1*coeff 182 | session['xt'] = x1 183 | session['yt'] = y1 184 | session['zt'] = z1 185 | session['Longitude'] = y0 186 | session['Latitude'] = x0 187 | 188 | 189 | return messages, errorMessage 190 | 191 | def unitmapper(value): 192 | ureg = pint.UnitRegistry() 193 | unit_mapping = { 194 | "METRE": ureg.meter, 195 | "METER": ureg.meter, 196 | "CENTIMETRE": ureg.centimeter, 197 | "CENTIMETER": ureg.centimeter, 198 | "MILLIMETRE": ureg.millimeter, 199 | "MILLIMETER": ureg.millimeter, 200 | "INCH": ureg.inch, 201 | "FOOT": ureg.foot, 202 | "YARD": ureg.yard, 203 | "MILE": ureg.mile, 204 | "NAUTICAL_MILE": ureg.nautical_mile, 205 | "metre": ureg.meter, 206 | "meter": ureg.meter, 207 | "centimeter": ureg.centimeter, 208 | "centimetre": ureg.centimeter, 209 | "millimeter": ureg.millimeter, 210 | "millimetre": ureg.millimeter, 211 | "inch": ureg.inch, 212 | "foot": ureg.foot, 213 | "yard": ureg.yard, 214 | "mile": ureg.mile, 215 | "nautical_mile": ureg.nautical_mile, 216 | # Add more mappings as needed 217 | } 218 | if value in unit_mapping: 219 | return 1 * unit_mapping[value] 220 | return 221 | 222 | @app.route('/') 223 | def index(): 224 | return render_template('upload.html') 225 | 226 | @app.route('/upload', methods=['POST']) 227 | def upload_file(): 228 | if 'file' not in request.files: 229 | return "No file part" 230 | file = request.files['file'] 231 | if file.filename == '': 232 | return "No selected file" 233 | if file and allowed_file(file.filename): # Check if the file extension is allowed 234 | filename = secure_filename(file.filename) 235 | file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename)) 236 | ifc_file = fileOpener(filename) 237 | 238 | message, geo = georef(ifc_file) 239 | if geo: 240 | IfcMapConversion, IfcProjectedCRS = georeference_ifc.get_mapconversion_crs(ifc_file=ifc_file) 241 | df = pd.DataFrame(list(IfcProjectedCRS.__dict__.items()), columns= ['property', 'value']) 242 | dg = pd.DataFrame(list(IfcMapConversion.__dict__.items()), columns= ['property', 'value']) 243 | html_table_f = df.to_html() 244 | html_table_g = dg.to_html() 245 | IfcMapConversion, IfcProjectedCRS = georeference_ifc.get_mapconversion_crs(ifc_file=ifc_file) 246 | target = IfcProjectedCRS.Name.split(':') 247 | epsg = int(target[1]) 248 | message2 = infoExt(filename,epsg) 249 | coeff = session.get('coeff') 250 | if coeff is None: 251 | return render_template('result.html', filename=filename, table_f=html_table_f, table_g=html_table_g, message=message2) 252 | 253 | if int(coeff)!=1 and IfcMapConversion.Scale is None: 254 | message += "There is a conflict between Scale factor and unit conversion. (Yet to be decided by buildingSmart.)" 255 | session['scaleError']=True 256 | return render_template('result.html', filename=filename, table_f=html_table_f, table_g=html_table_g, message=message) 257 | if int(coeff)!=1 and int(IfcMapConversion.Scale) == 1: 258 | message += "There is a conflict between Scale factor and unit conversion. (Yet to be decided by buildingSmart.)" 259 | session['scaleError']=True 260 | return render_template('result.html', filename=filename, table_f=html_table_f, table_g=html_table_g, message=message) 261 | 262 | return redirect(url_for('convert_crs', filename=filename)) # Redirect to EPSG code input page 263 | else: 264 | return render_template('upload.html', error_message="Invalid file format. Please upload a .ifc file.") 265 | 266 | @app.route('/devs', methods=['GET', 'POST']) 267 | def devs_upload(): 268 | if request.method == 'POST': 269 | if 'file' not in request.files: 270 | return "No file part" 271 | 272 | file = request.files['file'] 273 | 274 | if file.filename == '': 275 | return "No selected file" 276 | 277 | if file and allowed_file(file.filename): 278 | filename = secure_filename(file.filename) 279 | file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename)) 280 | ifc_file = fileOpener(filename) 281 | 282 | # Check if the IFC file is georeferenced 283 | message, geo = georef(ifc_file) 284 | 285 | if geo: 286 | IfcMapConversion, IfcProjectedCRS = georeference_ifc.get_mapconversion_crs(ifc_file=ifc_file) 287 | dg = pd.DataFrame(list(IfcMapConversion.__dict__.items()), columns= ['property', 'value']) 288 | message += "IfcMapconversion:\n\n" + dg.to_string() 289 | return f"Filename: {filename}\nGeoreferenced: YES\n{message}" 290 | else: 291 | message += "For georeferencing the IFC file, please visit the following address in a web browser:\nhttps://ifcgref.bk.tudelft.nl" 292 | return f"Filename: {filename}\nGeoreferenced: NO\n{message}" 293 | 294 | @app.route('/convert/', methods=['GET', 'POST']) 295 | def convert_crs(filename): 296 | if request.method == 'POST': 297 | try: 298 | epsg_code = int(request.form['epsg_code']) 299 | except ValueError: 300 | message = "Invalid EPSG code. Please enter a valid integer." 301 | return render_template('convert.html', filename=filename, message=message) 302 | session['target_epsg'] = epsg_code 303 | # Call the infoExt function and unpack the results 304 | messages, error = infoExt(filename, epsg_code) 305 | if error == "": 306 | # Pass x2, y2, and z1 to the survey_points route 307 | return redirect(url_for('survey_points', filename=filename)) 308 | return render_template('convert.html', filename=filename, message=error) 309 | 310 | return render_template('convert.html', filename=filename) 311 | 312 | @app.route('/survey/', methods=['GET', 'POST']) 313 | def survey_points(filename): 314 | epsg_code = session.get('target_epsg') 315 | messages, error = infoExt(filename, epsg_code) 316 | ifcunit = session.get('ifcunit') 317 | mapunit = session.get('mapunit') 318 | Refl = session.get('Refl') 319 | if request.method == 'POST': 320 | box_number = request.form.get('boxNumber') 321 | if box_number == '3': 322 | session['Refl'] = False 323 | Refl = False 324 | if Refl: 325 | messages , error = local_trans(filename,messages) 326 | Num = [] 327 | if request.method == 'POST': 328 | try: 329 | Num = int(request.form['Num']) 330 | if Num < 0: 331 | error += "Please enter zero or a positive integer." 332 | return render_template('survey.html', filename=filename, messages=messages, error=error) 333 | except ValueError: 334 | error += "Please enter zero or a positive integer." 335 | return render_template('survey.html', filename=filename, messages=messages, error=error) 336 | session['rows'] = Num 337 | if Num == 0: 338 | return redirect(url_for('calculate', filename=filename)) 339 | return render_template('survey.html', filename=filename, messages=messages, Num=Num, ifcunit=ifcunit, mapunit=mapunit, error=error, Refl = Refl) 340 | else: 341 | error += '\nThe model has no surveyed or georeferenced attribute.\nYou need to provide at least one point in local and target CRS.' 342 | error += '\n\nAccuracy of the results improves as you provide more georeferenced points.\nWithout any additional georeferenced points, it is assumed that the model is scaled based on unit conversion and rotation is derived from TrueNorth direction (if availalble).\n' 343 | Num = [] 344 | if request.method == 'POST': 345 | try: 346 | Num = int(request.form['Num']) 347 | if Num <= 0: 348 | error += "Please enter a positive integer." 349 | return render_template('survey.html', filename=filename, error=error) 350 | except ValueError: 351 | error += "Please enter a positive integer." 352 | return render_template('survey.html', filename=filename, error=error) 353 | session['rows'] = Num 354 | return render_template('survey.html', filename=filename, messages=messages, Num=Num, ifcunit=ifcunit, mapunit=mapunit, Refl = Refl) 355 | 356 | 357 | def local_trans(filename , messages): 358 | ifc_file = fileOpener(filename) 359 | xt = session.get('xt') 360 | yt = session.get('yt') 361 | zt = session.get('zt') 362 | bx,by,bz = 0,0,0 363 | error = "" 364 | if hasattr(ifc_file.by_type("IfcSite")[0], "ObjectPlacement") and ifc_file.by_type("IfcSite")[0].ObjectPlacement.is_a("IfcLocalPlacement"): 365 | local_placement = ifc_file.by_type("IfcSite")[0].ObjectPlacement.RelativePlacement 366 | # Check if the local placement is an IfcAxis2Placement3D 367 | if local_placement.is_a("IfcAxis2Placement3D"): 368 | local_origin = local_placement.Location.Coordinates 369 | bx, by, bz = map(float, local_origin) 370 | messages.append(("First Point Local Coordinates",str(local_origin))) 371 | else: 372 | error += "Local placement is not IfcAxis2Placement3D." 373 | else: 374 | error += "IfcSite does not have a local placement." 375 | session['bx'] = bx 376 | session['by'] = by 377 | session['bz'] = bz 378 | 379 | messages.append(("First Point Target coordinates" , ("(" + str(xt) + ", " + str(yt) + ", " + str(zt) + ")"))) 380 | error += '\n\nAccuracy of the results improves as you provide more georeferenced points.\nWithout any additional georeferenced points, it is assumed that the model is scaled based on unit conversion and rotation is derived from TrueNorth direction (if available).\n' 381 | 382 | ifc_file = ifc_file.end_transaction() 383 | return messages, error 384 | 385 | @app.route('/calc/', methods=['GET', 'POST']) 386 | def calculate(filename): 387 | #if request.method == 'POST': 388 | # Access the form data by iterating through the rows 389 | coeff = session.get('coeff') 390 | rows = session.get('rows') 391 | fn = os.path.join(app.config['UPLOAD_FOLDER'], filename) 392 | ifc_file = fileOpener(filename) 393 | data_points = [] 394 | Refl = session.get('Refl') 395 | if Refl: 396 | xt = session.get('xt') 397 | yt = session.get('yt') 398 | zt = session.get('zt') 399 | bx = session.get('bx') 400 | by = session.get('by') 401 | bz = session.get('bz') 402 | data_points.append({"X": bx, "Y": by, "Z": bz, "X_prime": xt, "Y_prime": yt, "Z_prime":zt}) 403 | #seperater 404 | if not Refl and rows == 1: 405 | Rotation_solution = 0 406 | S_solution = coeff 407 | ro = ifc_file.by_type("IfcGeometricRepresentationContext")[0].TrueNorth 408 | if ro is not None and ro.is_a("IfcDirection"): 409 | xord , xabs = round(float(ro[0][0]),6) , round(float(ro[0][1]),6) 410 | else: 411 | xord , xabs = 0 , 1 412 | Rotation_solution = math.atan2(xord,xabs) 413 | A = math.cos(Rotation_solution) 414 | B = math.sin(Rotation_solution) 415 | E_solution = float(request.form[f'x_prime{0}']) - (A*float(request.form[f'x{0}'])*coeff) + (B*float(request.form[f'y{0}'])*coeff) 416 | N_solution = float(request.form[f'y_prime{0}']) - (B*float(request.form[f'x{0}'])*coeff) - (A*float(request.form[f'y{0}'])*coeff) 417 | session['zt'] = float(request.form[f'z_prime{0}']) 418 | session['bz'] = float(request.form[f'z{0}']) 419 | H_solution = float(request.form[f'z_prime{0}']) - (float(request.form[f'z{0}'])*coeff) 420 | 421 | #seperater 422 | else: 423 | if rows == 0: 424 | Rotation_solution = 0 425 | S_solution = coeff 426 | ro = ifc_file.by_type("IfcGeometricRepresentationContext")[0].TrueNorth 427 | if ro is not None and ro.is_a("IfcDirection"): 428 | xord , xabs = round(float(ro[0][0]),6) , round(float(ro[0][1]),6) 429 | else: 430 | xord , xabs = 0 , 1 431 | Rotation_solution = math.atan2(xord,xabs) 432 | A = math.cos(Rotation_solution) 433 | B = math.sin(Rotation_solution) 434 | E_solution = xt - (A*S_solution*bx) + (B*S_solution*by) 435 | N_solution = yt - (B*S_solution*bx) - (A*S_solution*by) 436 | H_solution = zt - (S_solution*bz) 437 | else: 438 | for row in range(rows): 439 | x = request.form[f'x{row}'] 440 | y = request.form[f'y{row}'] 441 | z = request.form[f'z{row}'] 442 | x_prime = request.form[f'x_prime{row}'] 443 | y_prime = request.form[f'y_prime{row}'] 444 | z_prime = request.form[f'z_prime{row}'] 445 | 446 | try: 447 | x = float(x) 448 | y = float(y) 449 | z = float(z) 450 | x_prime = float(x_prime) 451 | y_prime = float(y_prime) 452 | z_prime = float(z_prime) 453 | except ValueError: 454 | message = "Invalid input. Please enter only float values." 455 | Num = rows 456 | return render_template('survey.html', message=message, Num=Num) 457 | 458 | data_points.append({"X": x, "Y": y, "Z":z,"X_prime": x_prime, "Y_prime": y_prime, "Z_prime": z_prime}) 459 | 460 | def equations(variables, data_points): 461 | S, Rotation, E, N , H = variables 462 | eqs = [] 463 | 464 | for data in data_points: 465 | X = data["X"] 466 | Y = data["Y"] 467 | Z = data["Z"] 468 | X_prime = data["X_prime"] 469 | Y_prime = data["Y_prime"] 470 | Z_prime = data["Z_prime"] 471 | 472 | eq1 = S * np.cos(Rotation) * X - S * np.sin(Rotation) * Y + E - X_prime 473 | eq2 = S * np.sin(Rotation) * X + S * np.cos(Rotation) * Y + N - Y_prime 474 | eq3 = S*Z + H - Z_prime 475 | eqs.extend([eq1, eq2, eq3]) 476 | 477 | return eqs 478 | # Initial guess for variables [S, Rotation, E, N] 479 | if Refl: 480 | initial_guess = [coeff, 0, xt, yt, zt] 481 | else: 482 | xg =float(request.form[f'x_prime{0}']) - (float(request.form[f'x{0}'])*coeff) 483 | yg = float(request.form[f'y_prime{0}']) - (float(request.form[f'y{0}'])*coeff) 484 | zg = float(request.form[f'z_prime{0}']) - (float(request.form[f'z{0}'])*coeff) 485 | initial_guess = [coeff,0,xg,yg,zg] 486 | 487 | # Perform the least squares optimization for all data points 488 | result = leastsq(equations, initial_guess, args=(data_points,), full_output=True) 489 | S_solution, Rotation_solution, E_solution, N_solution, H_solution = result[0] 490 | 491 | Rotation_degrees = (180 / math.pi) * Rotation_solution 492 | rDeg = Rotation_degrees - (360*round(Rotation_degrees/360)) 493 | 494 | target_epsg = "EPSG:"+str(session.get('target_epsg')) 495 | georeference_ifc.set_mapconversion_crs(ifc_file=ifc_file, 496 | target_crs_epsg_code=target_epsg, 497 | eastings=E_solution, 498 | northings=N_solution, 499 | orthogonal_height=H_solution, 500 | x_axis_abscissa=math.cos(Rotation_solution), 501 | x_axis_ordinate=math.sin(Rotation_solution), 502 | scale=S_solution) 503 | fn_output = re.sub('\.ifc$','_georeferenced.ifc', fn) 504 | ifc_file.write(fn_output) 505 | IfcMapConversion, IfcProjectedCRS = georeference_ifc.get_mapconversion_crs(ifc_file=ifc_file) 506 | df = pd.DataFrame(list(IfcProjectedCRS.__dict__.items()), columns= ['property', 'value']) 507 | dg = pd.DataFrame(list(IfcMapConversion.__dict__.items()), columns= ['property', 'value']) 508 | dg['value'] = dg['value'].astype(str) 509 | html_table_f = df.to_html() 510 | html_table_g = dg.to_html() 511 | return render_template('result.html', filename=filename, table_f=html_table_f, table_g=html_table_g) 512 | 513 | def fileOpener(filename): 514 | fn = os.path.join(app.config['UPLOAD_FOLDER'], filename) 515 | print("Opening IFC file:", fn) # Add this line for debugging 516 | try: 517 | ifc_file = ifcopenshell.open(fn) 518 | # ifc_schema = ifc_file.schema 519 | # ifc_site = ifc_file.by_type("IfcSite")[0] 520 | # ifc_unit = ifc_file.by_type("IfcUnitAssignment")[0].Units 521 | # ifc_geom = ifc_file.by_type("IfcGeometricRepresentationContext")[0] 522 | # ifc_mapconv, ifc_projcrs = georeference_ifc.get_mapconversion_crs(ifc_file=ifc_file) 523 | # session['ifc_schema'] = ifc_schema 524 | # session['ifc_site'] = pickle.dumps(ifc_site.ObjectPlacement) 525 | # # session['ifc_unit'] = ifc_unit 526 | # # session['ifc_geom'] = ifc_geom 527 | # # session['ifc_mapconv'] = ifc_mapconv 528 | # # session['ifc_projcrs'] = ifc_projcrs 529 | return ifc_file 530 | except Exception as e: 531 | print("Error opening IFC file:", str(e)) # Add this line for debugging 532 | return None 533 | 534 | @app.route('/show/', methods=['POST']) 535 | def visualize(filename): 536 | fn = os.path.join(app.config['UPLOAD_FOLDER'], filename) 537 | fn_output = re.sub('\.ifc$','_georeferenced.ifc', fn) 538 | if not os.path.exists(fn_output): 539 | fn_output = fn 540 | ifc_file = ifcopenshell.open(fn_output) 541 | IfcMapConversion, IfcProjectedCRS = georeference_ifc.get_mapconversion_crs(ifc_file=ifc_file) 542 | target = IfcProjectedCRS.Name.split(':') 543 | org = ifc_file.by_type('IfcProject')[0].RepresentationContexts[0].WorldCoordinateSystem.Location.Coordinates 544 | E = IfcMapConversion.Eastings 545 | N = IfcMapConversion.Northings 546 | S = IfcMapConversion.Scale 547 | if S is None: 548 | S = 1 549 | ortz = IfcMapConversion.OrthogonalHeight 550 | cos = IfcMapConversion.XAxisAbscissa 551 | if cos is None: 552 | cos = 1 553 | sin = IfcMapConversion.XAxisOrdinate 554 | if sin is None: 555 | sin = 0 556 | Rotation_solution = math.atan2(sin,cos) 557 | A = math.cos(Rotation_solution) 558 | B = math.sin(Rotation_solution) 559 | target_epsg = "EPSG:"+ target[1] 560 | transformer2 = Transformer.from_crs(target_epsg,"EPSG:4326") 561 | scaleError = session.get('scaleError') 562 | Gx , Gy = 0 , 0 563 | eff = session.get('coeff') 564 | if scaleError: 565 | saver = S 566 | S = eff 567 | session.pop('scaleError', None) # Corrected line 568 | E=E*S 569 | N = N*S 570 | ortz= ortz*S 571 | xx = S * org[0]* A - S * org[1]*B + E 572 | yy = S * org[1]* A + S * org[1]*B + N 573 | z = S * org[2] + ortz 574 | S = saver 575 | Snew = S 576 | else: 577 | xx = S * org[0]* A - S * org[1]*B + E 578 | yy = S * org[1]* A + S * org[1]*B + N 579 | zz = S * org[2] + ortz 580 | Snew = S/eff 581 | if xx==0 and yy==0: 582 | products = ifc_file.by_type('IfcProduct') 583 | for product in products: 584 | if product.Representation: 585 | placement = product.ObjectPlacement 586 | lpMAat = ifcopenshell.util.placement.get_local_placement(placement) 587 | Gx , Gy = lpMAat[0][3]*eff,lpMAat[1][3]*eff 588 | xx = xx+Gx 589 | yy = yy+Gy 590 | break 591 | x2,y2 = transformer2.transform(xx,yy) 592 | Latitude =x2 593 | Longitude =y2 594 | projstring = pyproj.CRS(target_epsg).to_proj4() 595 | crs = pyproj.CRS(projstring) 596 | alpha_value = crs.to_dict().get('alpha', None) 597 | Scale_value = crs.to_dict().get('k', None) 598 | if Scale_value is None: 599 | Scale_value = 1 600 | # if Scale_value is not None: 601 | # Snew = Snew/Scale_value 602 | # if alpha_value is not None: 603 | transformer3 = Transformer.from_crs(target_epsg, "EPSG:3857", always_xy=True) 604 | x_3857, y_3857 = transformer3.transform(xx, yy) 605 | xn_3857, yn_3857 = transformer3.transform(xx+1000, yy) 606 | dx = -x_3857 + xn_3857 607 | dy = -y_3857 + yn_3857 608 | angle_radians = math.atan2(dy, dx) 609 | angle_degrees = math.degrees(angle_radians) 610 | Rotation_solution = Rotation_solution + angle_radians 611 | length = math.sqrt(dx**2 + dy**2) 612 | # transformer3 = Transformer.from_crs(target_epsg, "EPSG:3857", always_xy=True) 613 | x_3857, y_3857 = transformer3.transform(xx, yy) 614 | xn_3857, yn_3857 = transformer3.transform((xx+50), (yy+50)) 615 | dx = -x_3857 + xn_3857 616 | dy = -y_3857 + yn_3857 617 | length = math.sqrt(dx**2 + dy**2) 618 | min_z_value = float('inf') 619 | 620 | for product in ifc_file.by_type('IfcProduct'): 621 | # Check if the product has a placement 622 | if product.Representation: 623 | placement = product.ObjectPlacement 624 | location = ifcopenshell.util.placement.get_local_placement(placement) 625 | z_value = location[2][3]*eff # Z is usually the 3rd coordinate (0: X, 1: Y, 2: Z) 626 | if z_value < min_z_value: 627 | min_z_value = z_value 628 | return render_template('view3D.html', filename=filename, Latitude=Latitude, Longitude=Longitude, Rotate=Rotation_solution, origin = org, Scale = Snew, ScaleCRS = Scale_value, Gx=Gx, Gy=Gy, LowestLevel = min_z_value) 629 | 630 | @app.route('/download/', methods=['GET']) 631 | def download(filename): 632 | # Define the path to the GeoJSON file 633 | fn = re.sub('\.ifc$','_georeferenced.ifc', filename) 634 | file_path = os.path.join(app.config['UPLOAD_FOLDER'], fn) 635 | 636 | # Ensure the file exists 637 | if os.path.exists(file_path): 638 | # Set the response headers to indicate a file download 639 | response = make_response() 640 | response.headers['Content-Type'] = 'application/octet-stream' 641 | response.headers['Content-Disposition'] = f'attachment; filename={fn}' 642 | 643 | # Read the file content and add it to the response 644 | with open(file_path, 'rb') as file: 645 | response.data = file.read() 646 | 647 | return response 648 | else: 649 | # Return a 404 error if the file doesn't exist 650 | return 'File not found', 404 651 | @app.route('/templates/') 652 | def temp(filename): 653 | return send_from_directory('templates', filename) 654 | 655 | @app.route('/uploads/') 656 | def ups(filename): 657 | return send_from_directory('uploads', filename) 658 | 659 | @app.route('/chek', methods=['GET', 'POST']) 660 | def chs(): 661 | return render_template('chek.html') 662 | 663 | if __name__ == '__main__': 664 | app.run(debug=True) 665 | -------------------------------------------------------------------------------- /check.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | url = 'https://ifcgref.bk.tudelft.nl/devs' 4 | file_path = './01.ifc' 5 | 6 | 7 | data = { 8 | 'file': ('01.ifc', open(file_path, 'rb')) 9 | } 10 | 11 | 12 | # Make a POST request to the /devs route with the data 13 | response = requests.post(url, files=data) 14 | 15 | # Print the response content 16 | print(response.text) 17 | -------------------------------------------------------------------------------- /georeference_ifc/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tudelft3d/ifcgref/221b05a7bf9171e3b788bb72b7dc23117335a0bf/georeference_ifc/.DS_Store -------------------------------------------------------------------------------- /georeference_ifc/IFC2X3_Geolocation.ifc: -------------------------------------------------------------------------------- 1 | ISO-10303-21; 2 | HEADER; 3 | FILE_DESCRIPTION((),'2;1'); 4 | FILE_NAME('IFC2X3_Geolocation.ifc','2020-01-01T00:00:00',(),(),'Sample','Sample',$); 5 | FILE_SCHEMA(('IFC4')); 6 | ENDSEC; 7 | DATA; 8 | #1=IFCPROPERTYSETTEMPLATE('3YdVbPbUXA$QcmIvcLKxCg',$,'EPset_ProjectedCRS','An IFC2x3 convention for georeferencing',.PSET_OCCURRENCEDRIVEN.,'IfcSite',(#2,#3,#4,#5,#6,#7,#8)); 9 | #2=IFCSIMPLEPROPERTYTEMPLATE('2lLZAU4LT7phTIqtaQRX9J',$,'Name','Name by which the coordinate reference system is identified. ',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.); 10 | #3=IFCSIMPLEPROPERTYTEMPLATE('1KDvMgh29EbQRikH8_h4FH',$,'Description','Informal description of this coordinate reference system. ',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.); 11 | #4=IFCSIMPLEPROPERTYTEMPLATE('3lDBscbyLFyvE4xcvYA7Bb',$,'GeodeticDatum','Name by which this datum is identified. The geodetic datum is associated with the coordinate reference system and indicates the shape and size of the rotation ellipsoid and this ellipsoid''s connection and orientation to the actual globe/earth. It needs to be provided, if the Name identifier does not unambiguously define the geodetic datum as well. ',.P_SINGLEVALUE.,'IfcIdentifier',$,$,$,$,$,.READWRITE.); 12 | #5=IFCSIMPLEPROPERTYTEMPLATE('1m3ZjUUVf3tfHK4GD0D$If',$,'VerticalDatum','Name by which the vertical datum is identified. The vertical datum is associated with the height axis of the coordinate reference system and indicates the reference plane and fundamental point defining the origin of a height system. It needs to be provided, if the Name identifier does not unambiguously define the vertical datum as well and if the coordinate reference system is a 3D reference system. ',.P_SINGLEVALUE.,'IfcIdentifier',$,$,$,$,$,.READWRITE.); 13 | #6=IFCSIMPLEPROPERTYTEMPLATE('3sUHnVnqXElRFuVaLpQj_2',$,'MapProjection','Name by which the map projection is identified. ',.P_SINGLEVALUE.,'IfcIdentifier',$,$,$,$,$,.READWRITE.); 14 | #7=IFCSIMPLEPROPERTYTEMPLATE('2uW2Bus$H6bAn9liUM2ui7',$,'MapZone','Name by which the map zone, relating to the MapProjection, is identified. ',.P_SINGLEVALUE.,'IfcIdentifier',$,$,$,$,$,.READWRITE.); 15 | #8=IFCSIMPLEPROPERTYTEMPLATE('0OrG0a0MTCtOrw0$jtMVW3',$,'MapUnit','Unit of the coordinate axes composing the map coordinate system.',.P_SINGLEVALUE.,'IfcIdentifier',$,$,$,$,$,.READWRITE.); 16 | #9=IFCPROPERTYSETTEMPLATE('0TnwG3fwj8RPXQUzCt31Ti',$,'EPset_MapConversion','An IFC2x3 convention for georeferencing',.PSET_OCCURRENCEDRIVEN.,'IfcSite',(#10,#11,#12,#13,#14,#15)); 17 | #10=IFCSIMPLEPROPERTYTEMPLATE('2lLZ9987T7phTIqtaQRX9J',$,'TargetCRS','Name by which the coordinate reference system is identified. ',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.); 18 | #11=IFCSIMPLEPROPERTYTEMPLATE('1UJp$3bALBShK9BBCAmy85',$,'Eastings','Specifies the location along the easting of the coordinate system of the target map coordinate reference system. ',.P_SINGLEVALUE.,'IfcLengthMeasure',$,$,$,$,$,.READWRITE.); 19 | #12=IFCSIMPLEPROPERTYTEMPLATE('3VANBAqv90G8cCyRQ0rFs_',$,'Northings','Specifies the location along the northing of the coordinate system of the target map coordinate reference system. ',.P_SINGLEVALUE.,'IfcLengthMeasure',$,$,$,$,$,.READWRITE.); 20 | #13=IFCSIMPLEPROPERTYTEMPLATE('3W2BUJsh57ZRjcFaF6HDRB',$,'OrthogonalHeight','Orthogonal height relativ to the vertical datum specified. ',.P_SINGLEVALUE.,'IfcLengthMeasure',$,$,$,$,$,.READWRITE.); 21 | #14=IFCSIMPLEPROPERTYTEMPLATE('3SThS3spnEQBfGxK6$FSoS',$,'XAxisAbscissa','Specifies the value along the easing axis of the end point of a vector indicating the position of the local x axis of the engineering coordinate reference system. ',.P_SINGLEVALUE.,'IfcReal',$,$,$,$,$,.READWRITE.); 22 | #15=IFCSIMPLEPROPERTYTEMPLATE('1rwwnE10XA5eME0lfYSR4X',$,'XAxisOrdinate','Specifies the value along the northing axis of the end point of a vector indicating the position of the local x axis of the engineering coordinate reference system. ',.P_SINGLEVALUE.,'IfcReal',$,$,$,$,$,.READWRITE.); 23 | #16=IFCSIMPLEPROPERTYTEMPLATE('2GXSPBbN133egiadM02BGW',$,'Scale','Scale to be used, when the units of the CRS are not identical to the units of the engineering coordinate system. If omited, the value of 1.0 is assumed. ',.P_SINGLEVALUE.,'IfcReal',$,$,$,$,$,.READWRITE.); 24 | ENDSEC; 25 | END-ISO-10303-21; 26 | -------------------------------------------------------------------------------- /georeference_ifc/__init__.py: -------------------------------------------------------------------------------- 1 | from .main import set_mapconversion_crs, set_si_units, get_mapconversion_crs, get_rotation -------------------------------------------------------------------------------- /georeference_ifc/__pycache__/__init__.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tudelft3d/ifcgref/221b05a7bf9171e3b788bb72b7dc23117335a0bf/georeference_ifc/__pycache__/__init__.cpython-311.pyc -------------------------------------------------------------------------------- /georeference_ifc/__pycache__/main.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tudelft3d/ifcgref/221b05a7bf9171e3b788bb72b7dc23117335a0bf/georeference_ifc/__pycache__/main.cpython-311.pyc -------------------------------------------------------------------------------- /georeference_ifc/main.py: -------------------------------------------------------------------------------- 1 | import os 2 | import ifcopenshell 3 | import ifcopenshell.util.unit 4 | import ifcopenshell.api 5 | import ifcopenshell.util.element 6 | import ifcopenshell.util.pset 7 | from ifcopenshell.util.element import get_psets 8 | import math 9 | from collections import OrderedDict 10 | 11 | 12 | 13 | def set_mapconversion_crs(ifc_file: ifcopenshell.file, 14 | target_crs_epsg_code: str, 15 | eastings: float, 16 | northings: float, 17 | orthogonal_height: float, 18 | x_axis_abscissa: float, 19 | x_axis_ordinate: float, 20 | scale: float) -> None: 21 | """ 22 | This method adds IFC map conversion information to an IfcOpenShell file. 23 | IFC map conversion information indicates how the local coordinate reference system of the IFC file 24 | can be converted into a global coordinate reference system. The latter is the IfcProjectedCRS. 25 | 26 | The method detects whether the schema of the IFC file is IFC2X3 or IFC4. 27 | In case of IFC4, an IfcMapConversion and IfcProjectedCRS are created and associated with IfcProject. 28 | For IFC2X3, corresponding property sets are created and associated with IfcSite. 29 | 30 | :param ifc_file: ifcopenshell.file 31 | the IfcOpenShell file object in which IfcMapConversion information is inserted. 32 | :param target_crs_epsg_code: str 33 | the EPSG-code of the target coordinate reference system formatted as a string (e.g. EPSG:2169). 34 | According to the IFC specification, only a projected coordinate reference system 35 | with cartesian coordinates can be used. 36 | :param eastings: float 37 | the coordinate shift (translation) that is added to an x-coordinate to convert it 38 | into the Eastings of the target reference system. 39 | :param northings: float 40 | the coordinate shift (translation) that is added to an y-coordinate to convert it 41 | into the Northings of the target reference system. 42 | :param orthogonal_height: float 43 | the coordinate shift (translation) that is applied to a z-coordinate to convert it 44 | into a height relative to the vertical datum of the target reference system. 45 | :param x_axis_abscissa: float 46 | defines a rotation (together with x_axis_ordinate) around the z-axis of the local coordinate system 47 | to orient it according to the target reference system. 48 | x_axis_abscissa is the component of a unit vector along the x-axis of the local reference system projected 49 | on the Eastings axis of the target reference system. 50 | :param x_axis_ordinate: float 51 | defines a rotation (together with x_axis_abscissa) around the z-axis of the local coordinate system 52 | to orient it according to the target reference system. 53 | x_axis_abscissa is the component of a unit vector along the x-axis of the local reference system projected 54 | on the Northings axis of the target reference system. 55 | :param scale: float, optional 56 | indicates the conversion factor to be used, to convert the units of the local coordinate 57 | system into the units of the target CRS (often expressed in metres). 58 | If omitted, a value of 1.0 is assumed. 59 | """ 60 | if ifc_file.schema[:4] == 'IFC4': 61 | set_mapconversion_crs_ifc4(ifc_file, target_crs_epsg_code, eastings, northings, orthogonal_height, 62 | x_axis_abscissa, 63 | x_axis_ordinate, scale) 64 | if ifc_file.schema == 'IFC2X3': 65 | set_mapconversion_crs_ifc2x3(ifc_file, target_crs_epsg_code, eastings, northings, orthogonal_height, 66 | x_axis_abscissa, x_axis_ordinate, scale) 67 | 68 | 69 | def set_si_units(ifc_file: ifcopenshell.file): 70 | """ 71 | This method adds standardized units to an IFC file. 72 | 73 | :param ifc_file: 74 | """ 75 | lengthunit = ifcopenshell.api.run("unit.add_si_unit", ifc_file, unit_type="LENGTHUNIT", name="METRE", prefix=None) 76 | areaunit = ifcopenshell.api.run("unit.add_si_unit", ifc_file, unit_type="AREAUNIT", name="SQUARE_METRE", 77 | prefix=None) 78 | volumeunit = ifcopenshell.api.run("unit.add_si_unit", ifc_file, unit_type="VOLUMEUNIT", name="CUBIC_METRE", 79 | prefix=None) 80 | ifcopenshell.api.run("unit.assign_unit", ifc_file, units=[lengthunit, areaunit, volumeunit]) 81 | 82 | 83 | def set_mapconversion_crs_ifc4(ifc_file: ifcopenshell.file, 84 | target_crs_epsg_code: str, 85 | eastings: float, 86 | northings: float, 87 | orthogonal_height: float, 88 | x_axis_abscissa: float, 89 | x_axis_ordinate: float, 90 | scale: float) -> None: 91 | # we assume that the IFC file only has one IfcProject entity. 92 | source_crs = ifc_file.by_type('IfcProject')[0].RepresentationContexts[0] 93 | target_crs = ifc_file.createIfcProjectedCRS( 94 | Name=target_crs_epsg_code 95 | ) 96 | ifc_file.createIfcMapConversion( 97 | SourceCRS=source_crs, 98 | TargetCRS=target_crs, 99 | Eastings=eastings, 100 | Northings=northings, 101 | OrthogonalHeight=orthogonal_height, 102 | XAxisAbscissa=x_axis_abscissa, 103 | XAxisOrdinate=x_axis_ordinate, 104 | Scale=scale 105 | ) 106 | 107 | 108 | def set_mapconversion_crs_ifc2x3(ifc_file: ifcopenshell.file, 109 | target_crs_epsg_code: str, 110 | eastings: float, 111 | northings: float, 112 | orthogonal_height: float, 113 | x_axis_abscissa: float, 114 | x_axis_ordinate: float, 115 | scale: float) -> None: 116 | # Open the IFC property set template provided by OSarch.org on https://wiki.osarch.org/index.php?title=File:IFC2X3_Geolocation.ifc 117 | ifc_template = ifcopenshell.open(os.path.join(os.path.dirname(__file__), './IFC2X3_Geolocation.ifc')) 118 | map_conversion_template = \ 119 | [t for t in ifc_template.by_type('IfcPropertySetTemplate') if t.Name == 'EPset_MapConversion'][0] 120 | crs_template = [t for t in ifc_template.by_type('IfcPropertySetTemplate') if t.Name == 'EPset_ProjectedCRS'][0] 121 | 122 | site = ifc_file.by_type("IfcSite")[0] # we assume that the IfcProject only has one IfcSite entity. 123 | pset0 = ifcopenshell.api.run("pset.add_pset", ifc_file, product=site, name="ePset_MapConversion") 124 | ifcopenshell.api.run("pset.edit_pset", ifc_file, pset=pset0, properties={'TargetCRS':target_crs_epsg_code, 125 | 'Eastings': eastings, 126 | 'Northings': northings, 127 | 'OrthogonalHeight': orthogonal_height, 128 | 'XAxisAbscissa': x_axis_abscissa, 129 | 'XAxisOrdinate': x_axis_ordinate, 130 | 'Scale': scale}, 131 | pset_template=map_conversion_template) 132 | pset1 = ifcopenshell.api.run("pset.add_pset", ifc_file, product=site, name="ePset_ProjectedCRS") 133 | ifcopenshell.api.run("pset.edit_pset", ifc_file, pset=pset1, properties={'Name': target_crs_epsg_code}, 134 | pset_template=crs_template) 135 | 136 | def get_mapconversion_crs(ifc_file: ifcopenshell.file) -> (object, object): 137 | class Struct: 138 | def __init__(self, **entries): 139 | self.__dict__.update(entries) 140 | 141 | mapconversion = None 142 | crs = None 143 | 144 | if ifc_file.schema [:4] == 'IFC4': 145 | project = ifc_file.by_type("IfcProject")[0] 146 | for c in (m for c in project.RepresentationContexts for m in c.HasCoordinateOperation): 147 | return c, c.TargetCRS 148 | if ifc_file.schema == 'IFC2X3': 149 | site = ifc_file.by_type("IfcSite")[0] 150 | psets = get_psets(site) 151 | if 'ePset_MapConversion' in psets.keys() and 'ePset_ProjectedCRS' in psets.keys(): 152 | # Move the last property to the first place 153 | mapconversion_properties = OrderedDict(list(psets['ePset_MapConversion'].items())[-1:] + list(psets['ePset_MapConversion'].items())[:-1]) 154 | crs_properties = OrderedDict(list(psets['ePset_ProjectedCRS'].items())[-1:] + list(psets['ePset_ProjectedCRS'].items())[:-1]) 155 | return Struct(**mapconversion_properties), Struct(**crs_properties) 156 | 157 | if 'ePSet_MapConversion' in psets.keys() and 'ePSet_ProjectedCRS' in psets.keys(): 158 | # Move the last property to the first place 159 | mapconversion_properties = OrderedDict(list(psets['ePSet_MapConversion'].items())[-1:] + list(psets['ePSet_MapConversion'].items())[:-1]) 160 | crs_properties = OrderedDict(list(psets['ePSet_ProjectedCRS'].items())[-1:] + list(psets['ePSet_ProjectedCRS'].items())[:-1]) 161 | return Struct(**mapconversion_properties), Struct(**crs_properties) 162 | 163 | return mapconversion, crs 164 | 165 | def get_rotation(mapconversion) -> float: 166 | """ 167 | This method calculates the rotation (in degrees) for a given mapconversion data structure, 168 | from its XAxisAbscissa and XAxisOrdinate properties. 169 | 170 | :returns the rotation in degrees along the Z-axis (the axis orthogonal to the earth's surface). For a right-handed 171 | coordinate reference system (as required) a postitive rotation angle implies a counter-clockwise rotation. 172 | """ 173 | return math.degrees(math.atan2(mapconversion.XAxisOrdinate, mapconversion.XAxisAbscissa)) -------------------------------------------------------------------------------- /static/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tudelft3d/ifcgref/221b05a7bf9171e3b788bb72b7dc23117335a0bf/static/.DS_Store -------------------------------------------------------------------------------- /static/images/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tudelft3d/ifcgref/221b05a7bf9171e3b788bb72b7dc23117335a0bf/static/images/.DS_Store -------------------------------------------------------------------------------- /static/images/back.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tudelft3d/ifcgref/221b05a7bf9171e3b788bb72b7dc23117335a0bf/static/images/back.png -------------------------------------------------------------------------------- /static/images/chek-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tudelft3d/ifcgref/221b05a7bf9171e3b788bb72b7dc23117335a0bf/static/images/chek-logo.png -------------------------------------------------------------------------------- /static/images/conv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tudelft3d/ifcgref/221b05a7bf9171e3b788bb72b7dc23117335a0bf/static/images/conv.png -------------------------------------------------------------------------------- /static/images/eu.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tudelft3d/ifcgref/221b05a7bf9171e3b788bb72b7dc23117335a0bf/static/images/eu.jpg -------------------------------------------------------------------------------- /static/images/flavicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tudelft3d/ifcgref/221b05a7bf9171e3b788bb72b7dc23117335a0bf/static/images/flavicon.png -------------------------------------------------------------------------------- /static/images/info.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tudelft3d/ifcgref/221b05a7bf9171e3b788bb72b7dc23117335a0bf/static/images/info.png -------------------------------------------------------------------------------- /static/images/loading.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tudelft3d/ifcgref/221b05a7bf9171e3b788bb72b7dc23117335a0bf/static/images/loading.png -------------------------------------------------------------------------------- /static/images/logo1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tudelft3d/ifcgref/221b05a7bf9171e3b788bb72b7dc23117335a0bf/static/images/logo1.png -------------------------------------------------------------------------------- /static/images/logo2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tudelft3d/ifcgref/221b05a7bf9171e3b788bb72b7dc23117335a0bf/static/images/logo2.png -------------------------------------------------------------------------------- /static/images/table.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tudelft3d/ifcgref/221b05a7bf9171e3b788bb72b7dc23117335a0bf/static/images/table.png -------------------------------------------------------------------------------- /static/images/upload.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tudelft3d/ifcgref/221b05a7bf9171e3b788bb72b7dc23117335a0bf/static/images/upload.png -------------------------------------------------------------------------------- /static/images/way1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tudelft3d/ifcgref/221b05a7bf9171e3b788bb72b7dc23117335a0bf/static/images/way1.png -------------------------------------------------------------------------------- /static/images/way2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tudelft3d/ifcgref/221b05a7bf9171e3b788bb72b7dc23117335a0bf/static/images/way2.png -------------------------------------------------------------------------------- /static/images/way3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tudelft3d/ifcgref/221b05a7bf9171e3b788bb72b7dc23117335a0bf/static/images/way3.png -------------------------------------------------------------------------------- /static/main.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tudelft3d/ifcgref/221b05a7bf9171e3b788bb72b7dc23117335a0bf/static/main.css -------------------------------------------------------------------------------- /templates/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tudelft3d/ifcgref/221b05a7bf9171e3b788bb72b7dc23117335a0bf/templates/.DS_Store -------------------------------------------------------------------------------- /templates/chek.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | BIMserver Project Contributions 7 | 69 | 70 | 71 |
72 | 73 | 74 |
75 |
76 | Please allow the pop-up, authorize the connection to BIMserver.center , and close the pop-up window afterward. 77 |
78 |
79 |

Select a Project:

80 |
81 | 82 |

Contributions for the Selected Project:

83 |
84 |
85 | 88 | 273 | 274 | 275 | -------------------------------------------------------------------------------- /templates/convert.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | CRS Conversion 7 | 8 | 106 | 107 | 108 |
109 | 110 | 111 |
112 |
113 |

CRS Conversion

114 | 115 | {% if message %} 116 |
{{ message | safe }}
117 | {% endif %} 118 | 119 |
120 | 121 | 122 | 123 |
124 | 125 |
126 | Loading... 127 |
128 |
129 | 130 | 135 | 136 | 137 | -------------------------------------------------------------------------------- /templates/guid.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | IfcGref User Guide 7 | 8 | 153 | 154 | 155 |
156 | 157 | 158 |
159 |
160 |

User Guide for IfcGref

161 |

Welcome to IfcGref! This guide will help you navigate and use the tool effectively to upload, georeference, and visualize IFC files.

162 | 163 |

Table of Contents

164 | 181 | 182 |

Introduction

183 |

This Flask-based tool, accessible at provides a comprehensive solution for designers, engineers, and software developers for georeferencing. IfcGref supports georeferencing operations starting from IFC 4, ensuring backward compatibility with earlier versions. The tool utilizes IfcMapConversion, incorporating attributes like SourceCRS, TargetCRS, and other key parameters to enable accurate coordinate transformations. This guide will walk you through each step of the process.

184 | 185 |

Using the Tool

186 | 187 |

Uploading an IFC File

188 |

1. Navigate to the Upload Page
189 | Go to the home page of IfcGref where you will see the option to upload an IFC file.

190 | 191 |

2. Upload Your File
192 | Drag and drop your `.ifc` file into the designated area or click on the upload area. Once the file is selected, it will automatically start uploading.

193 |
194 | Upload Image 195 |
196 | 197 |

3. Wait for the Upload to Complete
198 | Once the upload is successful, you will be redirected to the CRS conversion page.

199 | 200 |

Converting the CRS

201 |

1. Enter the EPSG Code
202 | On the CRS Conversion page, enter the EPSG code for the target CRS into the input field. EPSG codes are standard codes used to identify specific coordinate systems. IfcGref currently supports only Projected CRSs for the target.
Drag and drop your `.ifc` file into the designated area or click on the upload area. Once the file is selected, it will automatically start uploading.

203 |
204 | Conv Image 205 |
206 |

2. Start the Conversion
207 | Click the "Convert" button to set the EPSG code. You will be taken to a different page that shows BIM and geographical information exiting in the IFC.

208 | 209 |

Georeferencing Options

210 |

In this step, you need to select how to georeference your IFC file and add survey points if necessary. The following options are available:

211 | 212 |
213 |

1. Use existing information

214 |
215 | Option 1 216 |

217 |

In this option your IFC file wil be georeferenced based on the geo information that is already existing in the file such as longtitude, latitude, and reference elevation. By choosing this option, it is assumed that the model is scaled based on unit conversion between source and target CRS and rotation is derived from TrueNorth direction (if availalble)


218 |

219 |
220 | 221 |
222 |

2. Add surveyed points to existing information

223 |
224 | Option 2 225 |

226 |

While exisitng information can compute translation values in IFC files, using additional survey points allows for the calculation of rotation and scale, improving accuracy. The rotation angle can be inferred from the True North direction in BIM models, but discrepancies with the Geographic Coordinate System (GCS) and Projected Coordinate System (PCS) may occur due to map distortions. Therefore, it is advisable to use multiple survey points. The scale factor requires at least two georeferenced points and cannot be determined from the BIM model alone. More surveyed points enhance the accuracy and thoroughness of optimizing conversion values, leading to increased accuracy.


227 |

228 |
229 | 230 |
231 |

3. Add surveyed points and ignore existing information

232 |
233 | Option 3 234 |

235 |

In cases that the existing information is unreliable or the user chooses not to use it, this option to ignores the current data and georeference solely based on the added surveyed points and their relative coordinates in BIM. This allows for a fresh and accurate georeferencing process using only the newly provided survey points.


236 |

237 |
238 | 239 |

Adding Survey Points

240 | Remember to use points as decimal separators. You can copy and paste values from tables in Excel and Google Docs. Ensure that the units for BIM and Map match their original units, as indicated in the header of the input table. Once the table is complete click on the submit button.

241 |
242 | Table Image 243 |
244 |

Viewing Results

245 |

1. View Conversion Results
246 | After the calculation is complete, you will be redirected to the results page. The page will display the georeferencing data in a table format.

247 | 248 |

2. Check for Errors
249 | If any errors occurred during the conversion, they will be displayed on this page.

250 | 251 |

Visualizing on Map

252 |

1. Visualize Data on Map
253 | On the results page, click the "Show on Map" button. You will be redirected to a map view where the converted data is visualized.

254 | 255 |

2. Interact with the Map
256 | Use the map controls to zoom and pan to inspect the converted data. The model will be located based on the georeferencing values. Note that the elevation of BIM models at the local zero level will match the map’s base location.

257 | 258 |

Downloading the Georeferenced File

259 |

If the conversion is successful, you will see a "Download" button on the results page. Click the button to download the converted IFC file to your device. "_georeferenced" will be appended to the original file name to ensure it is distinct and easily searchable.

260 | 261 |

Handling Already Georeferenced Files

262 |

If the uploaded IFC file is georeferenced (IfcMapConversion is present), IfcMapConversion and IfcProjectedCRS values will be displayed. You can visualize the IFC file on the map by clicking the "Show on Map" button.

263 | 264 | 265 |

Troubleshooting

266 |

If you encounter any issues while using IfcGref, here are some common problems and their solutions:

267 | 268 |

File Upload Errors
269 | Ensure the file format is `.ifc`. Check your internet connection. If there is no progress after a while, try reloading the page and upload the file again.

270 | 271 |

Conversion Errors
272 | Ensure EPSG code corresponds to a Projected CRS. Review the error message for specific details and address any issues noted.

273 | 274 |

Visualization Issues
275 | Ensure your browser connects to internet for displaying maps. Refresh the page if the map does not load properly.

276 | 277 |

Contact and Support

278 |

For further assistance, please contact S.Hakim@tudelft.nl or visit our web page.

279 | 280 |

Thank you for using IfcGref! We hope this guide has helped you understand how to use the tool effectively. If you have any feedback or suggestions, feel free to reach out to us.

281 |
282 |
283 | 284 | Back Arrow 285 | 286 |

Back to upload page

287 |
288 | 289 | 290 | -------------------------------------------------------------------------------- /templates/jsm/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tudelft3d/ifcgref/221b05a7bf9171e3b788bb72b7dc23117335a0bf/templates/jsm/.DS_Store -------------------------------------------------------------------------------- /templates/jsm/controls/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tudelft3d/ifcgref/221b05a7bf9171e3b788bb72b7dc23117335a0bf/templates/jsm/controls/.DS_Store -------------------------------------------------------------------------------- /templates/jsm/controls/OrbitControls.js: -------------------------------------------------------------------------------- 1 | import { 2 | EventDispatcher, 3 | MOUSE, 4 | Quaternion, 5 | Spherical, 6 | TOUCH, 7 | Vector2, 8 | Vector3, 9 | Plane, 10 | Ray, 11 | MathUtils 12 | } from 'three'; 13 | 14 | // OrbitControls performs orbiting, dollying (zooming), and panning. 15 | // Unlike TrackballControls, it maintains the "up" direction object.up (+Y by default). 16 | // 17 | // Orbit - left mouse / touch: one-finger move 18 | // Zoom - middle mouse, or mousewheel / touch: two-finger spread or squish 19 | // Pan - right mouse, or left mouse + ctrl/meta/shiftKey, or arrow keys / touch: two-finger move 20 | 21 | const _changeEvent = { type: 'change' }; 22 | const _startEvent = { type: 'start' }; 23 | const _endEvent = { type: 'end' }; 24 | const _ray = new Ray(); 25 | const _plane = new Plane(); 26 | const TILT_LIMIT = Math.cos( 70 * MathUtils.DEG2RAD ); 27 | 28 | class OrbitControls extends EventDispatcher { 29 | 30 | constructor( object, domElement ) { 31 | 32 | super(); 33 | 34 | this.object = object; 35 | this.domElement = domElement; 36 | this.domElement.style.touchAction = 'none'; // disable touch scroll 37 | 38 | // Set to false to disable this control 39 | this.enabled = true; 40 | 41 | // "target" sets the location of focus, where the object orbits around 42 | this.target = new Vector3(); 43 | 44 | // Sets the 3D cursor (similar to Blender), from which the maxTargetRadius takes effect 45 | this.cursor = new Vector3(); 46 | 47 | // How far you can dolly in and out ( PerspectiveCamera only ) 48 | this.minDistance = 0; 49 | this.maxDistance = Infinity; 50 | 51 | // How far you can zoom in and out ( OrthographicCamera only ) 52 | this.minZoom = 0; 53 | this.maxZoom = Infinity; 54 | 55 | // Limit camera target within a spherical area around the cursor 56 | this.minTargetRadius = 0; 57 | this.maxTargetRadius = Infinity; 58 | 59 | // How far you can orbit vertically, upper and lower limits. 60 | // Range is 0 to Math.PI radians. 61 | this.minPolarAngle = 0; // radians 62 | this.maxPolarAngle = Math.PI; // radians 63 | 64 | // How far you can orbit horizontally, upper and lower limits. 65 | // If set, the interval [ min, max ] must be a sub-interval of [ - 2 PI, 2 PI ], with ( max - min < 2 PI ) 66 | this.minAzimuthAngle = - Infinity; // radians 67 | this.maxAzimuthAngle = Infinity; // radians 68 | 69 | // Set to true to enable damping (inertia) 70 | // If damping is enabled, you must call controls.update() in your animation loop 71 | this.enableDamping = false; 72 | this.dampingFactor = 0.05; 73 | 74 | // This option actually enables dollying in and out; left as "zoom" for backwards compatibility. 75 | // Set to false to disable zooming 76 | this.enableZoom = true; 77 | this.zoomSpeed = 1.0; 78 | 79 | // Set to false to disable rotating 80 | this.enableRotate = true; 81 | this.rotateSpeed = 1.0; 82 | 83 | // Set to false to disable panning 84 | this.enablePan = true; 85 | this.panSpeed = 1.0; 86 | this.screenSpacePanning = true; // if false, pan orthogonal to world-space direction camera.up 87 | this.keyPanSpeed = 7.0; // pixels moved per arrow key push 88 | this.zoomToCursor = false; 89 | 90 | // Set to true to automatically rotate around the target 91 | // If auto-rotate is enabled, you must call controls.update() in your animation loop 92 | this.autoRotate = false; 93 | this.autoRotateSpeed = 2.0; // 30 seconds per orbit when fps is 60 94 | 95 | // The four arrow keys 96 | this.keys = { LEFT: 'ArrowLeft', UP: 'ArrowUp', RIGHT: 'ArrowRight', BOTTOM: 'ArrowDown' }; 97 | 98 | // Mouse buttons 99 | this.mouseButtons = { LEFT: MOUSE.ROTATE, MIDDLE: MOUSE.DOLLY, RIGHT: MOUSE.PAN }; 100 | 101 | // Touch fingers 102 | this.touches = { ONE: TOUCH.ROTATE, TWO: TOUCH.DOLLY_PAN }; 103 | 104 | // for reset 105 | this.target0 = this.target.clone(); 106 | this.position0 = this.object.position.clone(); 107 | this.zoom0 = this.object.zoom; 108 | 109 | // the target DOM element for key events 110 | this._domElementKeyEvents = null; 111 | 112 | // 113 | // public methods 114 | // 115 | 116 | this.getPolarAngle = function () { 117 | 118 | return spherical.phi; 119 | 120 | }; 121 | 122 | this.getAzimuthalAngle = function () { 123 | 124 | return spherical.theta; 125 | 126 | }; 127 | 128 | this.getDistance = function () { 129 | 130 | return this.object.position.distanceTo( this.target ); 131 | 132 | }; 133 | 134 | this.listenToKeyEvents = function ( domElement ) { 135 | 136 | domElement.addEventListener( 'keydown', onKeyDown ); 137 | this._domElementKeyEvents = domElement; 138 | 139 | }; 140 | 141 | this.stopListenToKeyEvents = function () { 142 | 143 | this._domElementKeyEvents.removeEventListener( 'keydown', onKeyDown ); 144 | this._domElementKeyEvents = null; 145 | 146 | }; 147 | 148 | this.saveState = function () { 149 | 150 | scope.target0.copy( scope.target ); 151 | scope.position0.copy( scope.object.position ); 152 | scope.zoom0 = scope.object.zoom; 153 | 154 | }; 155 | 156 | this.reset = function () { 157 | 158 | scope.target.copy( scope.target0 ); 159 | scope.object.position.copy( scope.position0 ); 160 | scope.object.zoom = scope.zoom0; 161 | 162 | scope.object.updateProjectionMatrix(); 163 | scope.dispatchEvent( _changeEvent ); 164 | 165 | scope.update(); 166 | 167 | state = STATE.NONE; 168 | 169 | }; 170 | 171 | // this method is exposed, but perhaps it would be better if we can make it private... 172 | this.update = function () { 173 | 174 | const offset = new Vector3(); 175 | 176 | // so camera.up is the orbit axis 177 | const quat = new Quaternion().setFromUnitVectors( object.up, new Vector3( 0, 1, 0 ) ); 178 | const quatInverse = quat.clone().invert(); 179 | 180 | const lastPosition = new Vector3(); 181 | const lastQuaternion = new Quaternion(); 182 | const lastTargetPosition = new Vector3(); 183 | 184 | const twoPI = 2 * Math.PI; 185 | 186 | return function update( deltaTime = null ) { 187 | 188 | const position = scope.object.position; 189 | 190 | offset.copy( position ).sub( scope.target ); 191 | 192 | // rotate offset to "y-axis-is-up" space 193 | offset.applyQuaternion( quat ); 194 | 195 | // angle from z-axis around y-axis 196 | spherical.setFromVector3( offset ); 197 | 198 | if ( scope.autoRotate && state === STATE.NONE ) { 199 | 200 | rotateLeft( getAutoRotationAngle( deltaTime ) ); 201 | 202 | } 203 | 204 | if ( scope.enableDamping ) { 205 | 206 | spherical.theta += sphericalDelta.theta * scope.dampingFactor; 207 | spherical.phi += sphericalDelta.phi * scope.dampingFactor; 208 | 209 | } else { 210 | 211 | spherical.theta += sphericalDelta.theta; 212 | spherical.phi += sphericalDelta.phi; 213 | 214 | } 215 | 216 | // restrict theta to be between desired limits 217 | 218 | let min = scope.minAzimuthAngle; 219 | let max = scope.maxAzimuthAngle; 220 | 221 | if ( isFinite( min ) && isFinite( max ) ) { 222 | 223 | if ( min < - Math.PI ) min += twoPI; else if ( min > Math.PI ) min -= twoPI; 224 | 225 | if ( max < - Math.PI ) max += twoPI; else if ( max > Math.PI ) max -= twoPI; 226 | 227 | if ( min <= max ) { 228 | 229 | spherical.theta = Math.max( min, Math.min( max, spherical.theta ) ); 230 | 231 | } else { 232 | 233 | spherical.theta = ( spherical.theta > ( min + max ) / 2 ) ? 234 | Math.max( min, spherical.theta ) : 235 | Math.min( max, spherical.theta ); 236 | 237 | } 238 | 239 | } 240 | 241 | // restrict phi to be between desired limits 242 | spherical.phi = Math.max( scope.minPolarAngle, Math.min( scope.maxPolarAngle, spherical.phi ) ); 243 | 244 | spherical.makeSafe(); 245 | 246 | 247 | // move target to panned location 248 | 249 | if ( scope.enableDamping === true ) { 250 | 251 | scope.target.addScaledVector( panOffset, scope.dampingFactor ); 252 | 253 | } else { 254 | 255 | scope.target.add( panOffset ); 256 | 257 | } 258 | 259 | // Limit the target distance from the cursor to create a sphere around the center of interest 260 | scope.target.sub( scope.cursor ); 261 | scope.target.clampLength( scope.minTargetRadius, scope.maxTargetRadius ); 262 | scope.target.add( scope.cursor ); 263 | 264 | let zoomChanged = false; 265 | // adjust the camera position based on zoom only if we're not zooming to the cursor or if it's an ortho camera 266 | // we adjust zoom later in these cases 267 | if ( scope.zoomToCursor && performCursorZoom || scope.object.isOrthographicCamera ) { 268 | 269 | spherical.radius = clampDistance( spherical.radius ); 270 | 271 | } else { 272 | 273 | const prevRadius = spherical.radius; 274 | spherical.radius = clampDistance( spherical.radius * scale ); 275 | zoomChanged = prevRadius != spherical.radius; 276 | 277 | } 278 | 279 | offset.setFromSpherical( spherical ); 280 | 281 | // rotate offset back to "camera-up-vector-is-up" space 282 | offset.applyQuaternion( quatInverse ); 283 | 284 | position.copy( scope.target ).add( offset ); 285 | 286 | scope.object.lookAt( scope.target ); 287 | 288 | if ( scope.enableDamping === true ) { 289 | 290 | sphericalDelta.theta *= ( 1 - scope.dampingFactor ); 291 | sphericalDelta.phi *= ( 1 - scope.dampingFactor ); 292 | 293 | panOffset.multiplyScalar( 1 - scope.dampingFactor ); 294 | 295 | } else { 296 | 297 | sphericalDelta.set( 0, 0, 0 ); 298 | 299 | panOffset.set( 0, 0, 0 ); 300 | 301 | } 302 | 303 | // adjust camera position 304 | if ( scope.zoomToCursor && performCursorZoom ) { 305 | 306 | let newRadius = null; 307 | if ( scope.object.isPerspectiveCamera ) { 308 | 309 | // move the camera down the pointer ray 310 | // this method avoids floating point error 311 | const prevRadius = offset.length(); 312 | newRadius = clampDistance( prevRadius * scale ); 313 | 314 | const radiusDelta = prevRadius - newRadius; 315 | scope.object.position.addScaledVector( dollyDirection, radiusDelta ); 316 | scope.object.updateMatrixWorld(); 317 | 318 | zoomChanged = !! radiusDelta; 319 | 320 | } else if ( scope.object.isOrthographicCamera ) { 321 | 322 | // adjust the ortho camera position based on zoom changes 323 | const mouseBefore = new Vector3( mouse.x, mouse.y, 0 ); 324 | mouseBefore.unproject( scope.object ); 325 | 326 | const prevZoom = scope.object.zoom; 327 | scope.object.zoom = Math.max( scope.minZoom, Math.min( scope.maxZoom, scope.object.zoom / scale ) ); 328 | scope.object.updateProjectionMatrix(); 329 | 330 | zoomChanged = prevZoom !== scope.object.zoom; 331 | 332 | const mouseAfter = new Vector3( mouse.x, mouse.y, 0 ); 333 | mouseAfter.unproject( scope.object ); 334 | 335 | scope.object.position.sub( mouseAfter ).add( mouseBefore ); 336 | scope.object.updateMatrixWorld(); 337 | 338 | newRadius = offset.length(); 339 | 340 | } else { 341 | 342 | console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - zoom to cursor disabled.' ); 343 | scope.zoomToCursor = false; 344 | 345 | } 346 | 347 | // handle the placement of the target 348 | if ( newRadius !== null ) { 349 | 350 | if ( this.screenSpacePanning ) { 351 | 352 | // position the orbit target in front of the new camera position 353 | scope.target.set( 0, 0, - 1 ) 354 | .transformDirection( scope.object.matrix ) 355 | .multiplyScalar( newRadius ) 356 | .add( scope.object.position ); 357 | 358 | } else { 359 | 360 | // get the ray and translation plane to compute target 361 | _ray.origin.copy( scope.object.position ); 362 | _ray.direction.set( 0, 0, - 1 ).transformDirection( scope.object.matrix ); 363 | 364 | // if the camera is 20 degrees above the horizon then don't adjust the focus target to avoid 365 | // extremely large values 366 | if ( Math.abs( scope.object.up.dot( _ray.direction ) ) < TILT_LIMIT ) { 367 | 368 | object.lookAt( scope.target ); 369 | 370 | } else { 371 | 372 | _plane.setFromNormalAndCoplanarPoint( scope.object.up, scope.target ); 373 | _ray.intersectPlane( _plane, scope.target ); 374 | 375 | } 376 | 377 | } 378 | 379 | } 380 | 381 | } else if ( scope.object.isOrthographicCamera ) { 382 | 383 | const prevZoom = scope.object.zoom; 384 | scope.object.zoom = Math.max( scope.minZoom, Math.min( scope.maxZoom, scope.object.zoom / scale ) ); 385 | 386 | if ( prevZoom !== scope.object.zoom ) { 387 | 388 | scope.object.updateProjectionMatrix(); 389 | zoomChanged = true; 390 | 391 | } 392 | 393 | } 394 | 395 | scale = 1; 396 | performCursorZoom = false; 397 | 398 | // update condition is: 399 | // min(camera displacement, camera rotation in radians)^2 > EPS 400 | // using small-angle approximation cos(x/2) = 1 - x^2 / 8 401 | 402 | if ( zoomChanged || 403 | lastPosition.distanceToSquared( scope.object.position ) > EPS || 404 | 8 * ( 1 - lastQuaternion.dot( scope.object.quaternion ) ) > EPS || 405 | lastTargetPosition.distanceToSquared( scope.target ) > EPS ) { 406 | 407 | scope.dispatchEvent( _changeEvent ); 408 | 409 | lastPosition.copy( scope.object.position ); 410 | lastQuaternion.copy( scope.object.quaternion ); 411 | lastTargetPosition.copy( scope.target ); 412 | 413 | return true; 414 | 415 | } 416 | 417 | return false; 418 | 419 | }; 420 | 421 | }(); 422 | 423 | this.dispose = function () { 424 | 425 | scope.domElement.removeEventListener( 'contextmenu', onContextMenu ); 426 | 427 | scope.domElement.removeEventListener( 'pointerdown', onPointerDown ); 428 | scope.domElement.removeEventListener( 'pointercancel', onPointerUp ); 429 | scope.domElement.removeEventListener( 'wheel', onMouseWheel ); 430 | 431 | scope.domElement.removeEventListener( 'pointermove', onPointerMove ); 432 | scope.domElement.removeEventListener( 'pointerup', onPointerUp ); 433 | 434 | const document = scope.domElement.getRootNode(); // offscreen canvas compatibility 435 | 436 | document.removeEventListener( 'keydown', interceptControlDown, { capture: true } ); 437 | 438 | if ( scope._domElementKeyEvents !== null ) { 439 | 440 | scope._domElementKeyEvents.removeEventListener( 'keydown', onKeyDown ); 441 | scope._domElementKeyEvents = null; 442 | 443 | } 444 | 445 | //scope.dispatchEvent( { type: 'dispose' } ); // should this be added here? 446 | 447 | }; 448 | 449 | // 450 | // internals 451 | // 452 | 453 | const scope = this; 454 | 455 | const STATE = { 456 | NONE: - 1, 457 | ROTATE: 0, 458 | DOLLY: 1, 459 | PAN: 2, 460 | TOUCH_ROTATE: 3, 461 | TOUCH_PAN: 4, 462 | TOUCH_DOLLY_PAN: 5, 463 | TOUCH_DOLLY_ROTATE: 6 464 | }; 465 | 466 | let state = STATE.NONE; 467 | 468 | const EPS = 0.000001; 469 | 470 | // current position in spherical coordinates 471 | const spherical = new Spherical(); 472 | const sphericalDelta = new Spherical(); 473 | 474 | let scale = 1; 475 | const panOffset = new Vector3(); 476 | 477 | const rotateStart = new Vector2(); 478 | const rotateEnd = new Vector2(); 479 | const rotateDelta = new Vector2(); 480 | 481 | const panStart = new Vector2(); 482 | const panEnd = new Vector2(); 483 | const panDelta = new Vector2(); 484 | 485 | const dollyStart = new Vector2(); 486 | const dollyEnd = new Vector2(); 487 | const dollyDelta = new Vector2(); 488 | 489 | const dollyDirection = new Vector3(); 490 | const mouse = new Vector2(); 491 | let performCursorZoom = false; 492 | 493 | const pointers = []; 494 | const pointerPositions = {}; 495 | 496 | let controlActive = false; 497 | 498 | function getAutoRotationAngle( deltaTime ) { 499 | 500 | if ( deltaTime !== null ) { 501 | 502 | return ( 2 * Math.PI / 60 * scope.autoRotateSpeed ) * deltaTime; 503 | 504 | } else { 505 | 506 | return 2 * Math.PI / 60 / 60 * scope.autoRotateSpeed; 507 | 508 | } 509 | 510 | } 511 | 512 | function getZoomScale( delta ) { 513 | 514 | const normalizedDelta = Math.abs( delta * 0.01 ); 515 | return Math.pow( 0.95, scope.zoomSpeed * normalizedDelta ); 516 | 517 | } 518 | 519 | function rotateLeft( angle ) { 520 | 521 | sphericalDelta.theta -= angle; 522 | 523 | } 524 | 525 | function rotateUp( angle ) { 526 | 527 | sphericalDelta.phi -= angle; 528 | 529 | } 530 | 531 | const panLeft = function () { 532 | 533 | const v = new Vector3(); 534 | 535 | return function panLeft( distance, objectMatrix ) { 536 | 537 | v.setFromMatrixColumn( objectMatrix, 0 ); // get X column of objectMatrix 538 | v.multiplyScalar( - distance ); 539 | 540 | panOffset.add( v ); 541 | 542 | }; 543 | 544 | }(); 545 | 546 | const panUp = function () { 547 | 548 | const v = new Vector3(); 549 | 550 | return function panUp( distance, objectMatrix ) { 551 | 552 | if ( scope.screenSpacePanning === true ) { 553 | 554 | v.setFromMatrixColumn( objectMatrix, 1 ); 555 | 556 | } else { 557 | 558 | v.setFromMatrixColumn( objectMatrix, 0 ); 559 | v.crossVectors( scope.object.up, v ); 560 | 561 | } 562 | 563 | v.multiplyScalar( distance ); 564 | 565 | panOffset.add( v ); 566 | 567 | }; 568 | 569 | }(); 570 | 571 | // deltaX and deltaY are in pixels; right and down are positive 572 | const pan = function () { 573 | 574 | const offset = new Vector3(); 575 | 576 | return function pan( deltaX, deltaY ) { 577 | 578 | const element = scope.domElement; 579 | 580 | if ( scope.object.isPerspectiveCamera ) { 581 | 582 | // perspective 583 | const position = scope.object.position; 584 | offset.copy( position ).sub( scope.target ); 585 | let targetDistance = offset.length(); 586 | 587 | // half of the fov is center to top of screen 588 | targetDistance *= Math.tan( ( scope.object.fov / 2 ) * Math.PI / 180.0 ); 589 | 590 | // we use only clientHeight here so aspect ratio does not distort speed 591 | panLeft( 2 * deltaX * targetDistance / element.clientHeight, scope.object.matrix ); 592 | panUp( 2 * deltaY * targetDistance / element.clientHeight, scope.object.matrix ); 593 | 594 | } else if ( scope.object.isOrthographicCamera ) { 595 | 596 | // orthographic 597 | panLeft( deltaX * ( scope.object.right - scope.object.left ) / scope.object.zoom / element.clientWidth, scope.object.matrix ); 598 | panUp( deltaY * ( scope.object.top - scope.object.bottom ) / scope.object.zoom / element.clientHeight, scope.object.matrix ); 599 | 600 | } else { 601 | 602 | // camera neither orthographic nor perspective 603 | console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - pan disabled.' ); 604 | scope.enablePan = false; 605 | 606 | } 607 | 608 | }; 609 | 610 | }(); 611 | 612 | function dollyOut( dollyScale ) { 613 | 614 | if ( scope.object.isPerspectiveCamera || scope.object.isOrthographicCamera ) { 615 | 616 | scale /= dollyScale; 617 | 618 | } else { 619 | 620 | console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' ); 621 | scope.enableZoom = false; 622 | 623 | } 624 | 625 | } 626 | 627 | function dollyIn( dollyScale ) { 628 | 629 | if ( scope.object.isPerspectiveCamera || scope.object.isOrthographicCamera ) { 630 | 631 | scale *= dollyScale; 632 | 633 | } else { 634 | 635 | console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' ); 636 | scope.enableZoom = false; 637 | 638 | } 639 | 640 | } 641 | 642 | function updateZoomParameters( x, y ) { 643 | 644 | if ( ! scope.zoomToCursor ) { 645 | 646 | return; 647 | 648 | } 649 | 650 | performCursorZoom = true; 651 | 652 | const rect = scope.domElement.getBoundingClientRect(); 653 | const dx = x - rect.left; 654 | const dy = y - rect.top; 655 | const w = rect.width; 656 | const h = rect.height; 657 | 658 | mouse.x = ( dx / w ) * 2 - 1; 659 | mouse.y = - ( dy / h ) * 2 + 1; 660 | 661 | dollyDirection.set( mouse.x, mouse.y, 1 ).unproject( scope.object ).sub( scope.object.position ).normalize(); 662 | 663 | } 664 | 665 | function clampDistance( dist ) { 666 | 667 | return Math.max( scope.minDistance, Math.min( scope.maxDistance, dist ) ); 668 | 669 | } 670 | 671 | // 672 | // event callbacks - update the object state 673 | // 674 | 675 | function handleMouseDownRotate( event ) { 676 | 677 | rotateStart.set( event.clientX, event.clientY ); 678 | 679 | } 680 | 681 | function handleMouseDownDolly( event ) { 682 | 683 | updateZoomParameters( event.clientX, event.clientX ); 684 | dollyStart.set( event.clientX, event.clientY ); 685 | 686 | } 687 | 688 | function handleMouseDownPan( event ) { 689 | 690 | panStart.set( event.clientX, event.clientY ); 691 | 692 | } 693 | 694 | function handleMouseMoveRotate( event ) { 695 | 696 | rotateEnd.set( event.clientX, event.clientY ); 697 | 698 | rotateDelta.subVectors( rotateEnd, rotateStart ).multiplyScalar( scope.rotateSpeed ); 699 | 700 | const element = scope.domElement; 701 | 702 | rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientHeight ); // yes, height 703 | 704 | rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight ); 705 | 706 | rotateStart.copy( rotateEnd ); 707 | 708 | scope.update(); 709 | 710 | } 711 | 712 | function handleMouseMoveDolly( event ) { 713 | 714 | dollyEnd.set( event.clientX, event.clientY ); 715 | 716 | dollyDelta.subVectors( dollyEnd, dollyStart ); 717 | 718 | if ( dollyDelta.y > 0 ) { 719 | 720 | dollyOut( getZoomScale( dollyDelta.y ) ); 721 | 722 | } else if ( dollyDelta.y < 0 ) { 723 | 724 | dollyIn( getZoomScale( dollyDelta.y ) ); 725 | 726 | } 727 | 728 | dollyStart.copy( dollyEnd ); 729 | 730 | scope.update(); 731 | 732 | } 733 | 734 | function handleMouseMovePan( event ) { 735 | 736 | panEnd.set( event.clientX, event.clientY ); 737 | 738 | panDelta.subVectors( panEnd, panStart ).multiplyScalar( scope.panSpeed ); 739 | 740 | pan( panDelta.x, panDelta.y ); 741 | 742 | panStart.copy( panEnd ); 743 | 744 | scope.update(); 745 | 746 | } 747 | 748 | function handleMouseWheel( event ) { 749 | 750 | updateZoomParameters( event.clientX, event.clientY ); 751 | 752 | if ( event.deltaY < 0 ) { 753 | 754 | dollyIn( getZoomScale( event.deltaY ) ); 755 | 756 | } else if ( event.deltaY > 0 ) { 757 | 758 | dollyOut( getZoomScale( event.deltaY ) ); 759 | 760 | } 761 | 762 | scope.update(); 763 | 764 | } 765 | 766 | function handleKeyDown( event ) { 767 | 768 | let needsUpdate = false; 769 | 770 | switch ( event.code ) { 771 | 772 | case scope.keys.UP: 773 | 774 | if ( event.ctrlKey || event.metaKey || event.shiftKey ) { 775 | 776 | rotateUp( 2 * Math.PI * scope.rotateSpeed / scope.domElement.clientHeight ); 777 | 778 | } else { 779 | 780 | pan( 0, scope.keyPanSpeed ); 781 | 782 | } 783 | 784 | needsUpdate = true; 785 | break; 786 | 787 | case scope.keys.BOTTOM: 788 | 789 | if ( event.ctrlKey || event.metaKey || event.shiftKey ) { 790 | 791 | rotateUp( - 2 * Math.PI * scope.rotateSpeed / scope.domElement.clientHeight ); 792 | 793 | } else { 794 | 795 | pan( 0, - scope.keyPanSpeed ); 796 | 797 | } 798 | 799 | needsUpdate = true; 800 | break; 801 | 802 | case scope.keys.LEFT: 803 | 804 | if ( event.ctrlKey || event.metaKey || event.shiftKey ) { 805 | 806 | rotateLeft( 2 * Math.PI * scope.rotateSpeed / scope.domElement.clientHeight ); 807 | 808 | } else { 809 | 810 | pan( scope.keyPanSpeed, 0 ); 811 | 812 | } 813 | 814 | needsUpdate = true; 815 | break; 816 | 817 | case scope.keys.RIGHT: 818 | 819 | if ( event.ctrlKey || event.metaKey || event.shiftKey ) { 820 | 821 | rotateLeft( - 2 * Math.PI * scope.rotateSpeed / scope.domElement.clientHeight ); 822 | 823 | } else { 824 | 825 | pan( - scope.keyPanSpeed, 0 ); 826 | 827 | } 828 | 829 | needsUpdate = true; 830 | break; 831 | 832 | } 833 | 834 | if ( needsUpdate ) { 835 | 836 | // prevent the browser from scrolling on cursor keys 837 | event.preventDefault(); 838 | 839 | scope.update(); 840 | 841 | } 842 | 843 | 844 | } 845 | 846 | function handleTouchStartRotate( event ) { 847 | 848 | if ( pointers.length === 1 ) { 849 | 850 | rotateStart.set( event.pageX, event.pageY ); 851 | 852 | } else { 853 | 854 | const position = getSecondPointerPosition( event ); 855 | 856 | const x = 0.5 * ( event.pageX + position.x ); 857 | const y = 0.5 * ( event.pageY + position.y ); 858 | 859 | rotateStart.set( x, y ); 860 | 861 | } 862 | 863 | } 864 | 865 | function handleTouchStartPan( event ) { 866 | 867 | if ( pointers.length === 1 ) { 868 | 869 | panStart.set( event.pageX, event.pageY ); 870 | 871 | } else { 872 | 873 | const position = getSecondPointerPosition( event ); 874 | 875 | const x = 0.5 * ( event.pageX + position.x ); 876 | const y = 0.5 * ( event.pageY + position.y ); 877 | 878 | panStart.set( x, y ); 879 | 880 | } 881 | 882 | } 883 | 884 | function handleTouchStartDolly( event ) { 885 | 886 | const position = getSecondPointerPosition( event ); 887 | 888 | const dx = event.pageX - position.x; 889 | const dy = event.pageY - position.y; 890 | 891 | const distance = Math.sqrt( dx * dx + dy * dy ); 892 | 893 | dollyStart.set( 0, distance ); 894 | 895 | } 896 | 897 | function handleTouchStartDollyPan( event ) { 898 | 899 | if ( scope.enableZoom ) handleTouchStartDolly( event ); 900 | 901 | if ( scope.enablePan ) handleTouchStartPan( event ); 902 | 903 | } 904 | 905 | function handleTouchStartDollyRotate( event ) { 906 | 907 | if ( scope.enableZoom ) handleTouchStartDolly( event ); 908 | 909 | if ( scope.enableRotate ) handleTouchStartRotate( event ); 910 | 911 | } 912 | 913 | function handleTouchMoveRotate( event ) { 914 | 915 | if ( pointers.length == 1 ) { 916 | 917 | rotateEnd.set( event.pageX, event.pageY ); 918 | 919 | } else { 920 | 921 | const position = getSecondPointerPosition( event ); 922 | 923 | const x = 0.5 * ( event.pageX + position.x ); 924 | const y = 0.5 * ( event.pageY + position.y ); 925 | 926 | rotateEnd.set( x, y ); 927 | 928 | } 929 | 930 | rotateDelta.subVectors( rotateEnd, rotateStart ).multiplyScalar( scope.rotateSpeed ); 931 | 932 | const element = scope.domElement; 933 | 934 | rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientHeight ); // yes, height 935 | 936 | rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight ); 937 | 938 | rotateStart.copy( rotateEnd ); 939 | 940 | } 941 | 942 | function handleTouchMovePan( event ) { 943 | 944 | if ( pointers.length === 1 ) { 945 | 946 | panEnd.set( event.pageX, event.pageY ); 947 | 948 | } else { 949 | 950 | const position = getSecondPointerPosition( event ); 951 | 952 | const x = 0.5 * ( event.pageX + position.x ); 953 | const y = 0.5 * ( event.pageY + position.y ); 954 | 955 | panEnd.set( x, y ); 956 | 957 | } 958 | 959 | panDelta.subVectors( panEnd, panStart ).multiplyScalar( scope.panSpeed ); 960 | 961 | pan( panDelta.x, panDelta.y ); 962 | 963 | panStart.copy( panEnd ); 964 | 965 | } 966 | 967 | function handleTouchMoveDolly( event ) { 968 | 969 | const position = getSecondPointerPosition( event ); 970 | 971 | const dx = event.pageX - position.x; 972 | const dy = event.pageY - position.y; 973 | 974 | const distance = Math.sqrt( dx * dx + dy * dy ); 975 | 976 | dollyEnd.set( 0, distance ); 977 | 978 | dollyDelta.set( 0, Math.pow( dollyEnd.y / dollyStart.y, scope.zoomSpeed ) ); 979 | 980 | dollyOut( dollyDelta.y ); 981 | 982 | dollyStart.copy( dollyEnd ); 983 | 984 | const centerX = ( event.pageX + position.x ) * 0.5; 985 | const centerY = ( event.pageY + position.y ) * 0.5; 986 | 987 | updateZoomParameters( centerX, centerY ); 988 | 989 | } 990 | 991 | function handleTouchMoveDollyPan( event ) { 992 | 993 | if ( scope.enableZoom ) handleTouchMoveDolly( event ); 994 | 995 | if ( scope.enablePan ) handleTouchMovePan( event ); 996 | 997 | } 998 | 999 | function handleTouchMoveDollyRotate( event ) { 1000 | 1001 | if ( scope.enableZoom ) handleTouchMoveDolly( event ); 1002 | 1003 | if ( scope.enableRotate ) handleTouchMoveRotate( event ); 1004 | 1005 | } 1006 | 1007 | // 1008 | // event handlers - FSM: listen for events and reset state 1009 | // 1010 | 1011 | function onPointerDown( event ) { 1012 | 1013 | if ( scope.enabled === false ) return; 1014 | 1015 | if ( pointers.length === 0 ) { 1016 | 1017 | scope.domElement.setPointerCapture( event.pointerId ); 1018 | 1019 | scope.domElement.addEventListener( 'pointermove', onPointerMove ); 1020 | scope.domElement.addEventListener( 'pointerup', onPointerUp ); 1021 | 1022 | } 1023 | 1024 | // 1025 | 1026 | if ( isTrackingPointer( event ) ) return; 1027 | 1028 | // 1029 | 1030 | addPointer( event ); 1031 | 1032 | if ( event.pointerType === 'touch' ) { 1033 | 1034 | onTouchStart( event ); 1035 | 1036 | } else { 1037 | 1038 | onMouseDown( event ); 1039 | 1040 | } 1041 | 1042 | } 1043 | 1044 | function onPointerMove( event ) { 1045 | 1046 | if ( scope.enabled === false ) return; 1047 | 1048 | if ( event.pointerType === 'touch' ) { 1049 | 1050 | onTouchMove( event ); 1051 | 1052 | } else { 1053 | 1054 | onMouseMove( event ); 1055 | 1056 | } 1057 | 1058 | } 1059 | 1060 | function onPointerUp( event ) { 1061 | 1062 | removePointer( event ); 1063 | 1064 | switch ( pointers.length ) { 1065 | 1066 | case 0: 1067 | 1068 | scope.domElement.releasePointerCapture( event.pointerId ); 1069 | 1070 | scope.domElement.removeEventListener( 'pointermove', onPointerMove ); 1071 | scope.domElement.removeEventListener( 'pointerup', onPointerUp ); 1072 | 1073 | scope.dispatchEvent( _endEvent ); 1074 | 1075 | state = STATE.NONE; 1076 | 1077 | break; 1078 | 1079 | case 1: 1080 | 1081 | const pointerId = pointers[ 0 ]; 1082 | const position = pointerPositions[ pointerId ]; 1083 | 1084 | // minimal placeholder event - allows state correction on pointer-up 1085 | onTouchStart( { pointerId: pointerId, pageX: position.x, pageY: position.y } ); 1086 | 1087 | break; 1088 | 1089 | } 1090 | 1091 | } 1092 | 1093 | function onMouseDown( event ) { 1094 | 1095 | let mouseAction; 1096 | 1097 | switch ( event.button ) { 1098 | 1099 | case 0: 1100 | 1101 | mouseAction = scope.mouseButtons.LEFT; 1102 | break; 1103 | 1104 | case 1: 1105 | 1106 | mouseAction = scope.mouseButtons.MIDDLE; 1107 | break; 1108 | 1109 | case 2: 1110 | 1111 | mouseAction = scope.mouseButtons.RIGHT; 1112 | break; 1113 | 1114 | default: 1115 | 1116 | mouseAction = - 1; 1117 | 1118 | } 1119 | 1120 | switch ( mouseAction ) { 1121 | 1122 | case MOUSE.DOLLY: 1123 | 1124 | if ( scope.enableZoom === false ) return; 1125 | 1126 | handleMouseDownDolly( event ); 1127 | 1128 | state = STATE.DOLLY; 1129 | 1130 | break; 1131 | 1132 | case MOUSE.ROTATE: 1133 | 1134 | if ( event.ctrlKey || event.metaKey || event.shiftKey ) { 1135 | 1136 | if ( scope.enablePan === false ) return; 1137 | 1138 | handleMouseDownPan( event ); 1139 | 1140 | state = STATE.PAN; 1141 | 1142 | } else { 1143 | 1144 | if ( scope.enableRotate === false ) return; 1145 | 1146 | handleMouseDownRotate( event ); 1147 | 1148 | state = STATE.ROTATE; 1149 | 1150 | } 1151 | 1152 | break; 1153 | 1154 | case MOUSE.PAN: 1155 | 1156 | if ( event.ctrlKey || event.metaKey || event.shiftKey ) { 1157 | 1158 | if ( scope.enableRotate === false ) return; 1159 | 1160 | handleMouseDownRotate( event ); 1161 | 1162 | state = STATE.ROTATE; 1163 | 1164 | } else { 1165 | 1166 | if ( scope.enablePan === false ) return; 1167 | 1168 | handleMouseDownPan( event ); 1169 | 1170 | state = STATE.PAN; 1171 | 1172 | } 1173 | 1174 | break; 1175 | 1176 | default: 1177 | 1178 | state = STATE.NONE; 1179 | 1180 | } 1181 | 1182 | if ( state !== STATE.NONE ) { 1183 | 1184 | scope.dispatchEvent( _startEvent ); 1185 | 1186 | } 1187 | 1188 | } 1189 | 1190 | function onMouseMove( event ) { 1191 | 1192 | switch ( state ) { 1193 | 1194 | case STATE.ROTATE: 1195 | 1196 | if ( scope.enableRotate === false ) return; 1197 | 1198 | handleMouseMoveRotate( event ); 1199 | 1200 | break; 1201 | 1202 | case STATE.DOLLY: 1203 | 1204 | if ( scope.enableZoom === false ) return; 1205 | 1206 | handleMouseMoveDolly( event ); 1207 | 1208 | break; 1209 | 1210 | case STATE.PAN: 1211 | 1212 | if ( scope.enablePan === false ) return; 1213 | 1214 | handleMouseMovePan( event ); 1215 | 1216 | break; 1217 | 1218 | } 1219 | 1220 | } 1221 | 1222 | function onMouseWheel( event ) { 1223 | 1224 | if ( scope.enabled === false || scope.enableZoom === false || state !== STATE.NONE ) return; 1225 | 1226 | event.preventDefault(); 1227 | 1228 | scope.dispatchEvent( _startEvent ); 1229 | 1230 | handleMouseWheel( customWheelEvent( event ) ); 1231 | 1232 | scope.dispatchEvent( _endEvent ); 1233 | 1234 | } 1235 | 1236 | function customWheelEvent( event ) { 1237 | 1238 | const mode = event.deltaMode; 1239 | 1240 | // minimal wheel event altered to meet delta-zoom demand 1241 | const newEvent = { 1242 | clientX: event.clientX, 1243 | clientY: event.clientY, 1244 | deltaY: event.deltaY, 1245 | }; 1246 | 1247 | switch ( mode ) { 1248 | 1249 | case 1: // LINE_MODE 1250 | newEvent.deltaY *= 16; 1251 | break; 1252 | 1253 | case 2: // PAGE_MODE 1254 | newEvent.deltaY *= 100; 1255 | break; 1256 | 1257 | } 1258 | 1259 | // detect if event was triggered by pinching 1260 | if ( event.ctrlKey && ! controlActive ) { 1261 | 1262 | newEvent.deltaY *= 10; 1263 | 1264 | } 1265 | 1266 | return newEvent; 1267 | 1268 | } 1269 | 1270 | function interceptControlDown( event ) { 1271 | 1272 | if ( event.key === 'Control' ) { 1273 | 1274 | controlActive = true; 1275 | 1276 | 1277 | const document = scope.domElement.getRootNode(); // offscreen canvas compatibility 1278 | 1279 | document.addEventListener( 'keyup', interceptControlUp, { passive: true, capture: true } ); 1280 | 1281 | } 1282 | 1283 | } 1284 | 1285 | function interceptControlUp( event ) { 1286 | 1287 | if ( event.key === 'Control' ) { 1288 | 1289 | controlActive = false; 1290 | 1291 | 1292 | const document = scope.domElement.getRootNode(); // offscreen canvas compatibility 1293 | 1294 | document.removeEventListener( 'keyup', interceptControlUp, { passive: true, capture: true } ); 1295 | 1296 | } 1297 | 1298 | } 1299 | 1300 | function onKeyDown( event ) { 1301 | 1302 | if ( scope.enabled === false || scope.enablePan === false ) return; 1303 | 1304 | handleKeyDown( event ); 1305 | 1306 | } 1307 | 1308 | function onTouchStart( event ) { 1309 | 1310 | trackPointer( event ); 1311 | 1312 | switch ( pointers.length ) { 1313 | 1314 | case 1: 1315 | 1316 | switch ( scope.touches.ONE ) { 1317 | 1318 | case TOUCH.ROTATE: 1319 | 1320 | if ( scope.enableRotate === false ) return; 1321 | 1322 | handleTouchStartRotate( event ); 1323 | 1324 | state = STATE.TOUCH_ROTATE; 1325 | 1326 | break; 1327 | 1328 | case TOUCH.PAN: 1329 | 1330 | if ( scope.enablePan === false ) return; 1331 | 1332 | handleTouchStartPan( event ); 1333 | 1334 | state = STATE.TOUCH_PAN; 1335 | 1336 | break; 1337 | 1338 | default: 1339 | 1340 | state = STATE.NONE; 1341 | 1342 | } 1343 | 1344 | break; 1345 | 1346 | case 2: 1347 | 1348 | switch ( scope.touches.TWO ) { 1349 | 1350 | case TOUCH.DOLLY_PAN: 1351 | 1352 | if ( scope.enableZoom === false && scope.enablePan === false ) return; 1353 | 1354 | handleTouchStartDollyPan( event ); 1355 | 1356 | state = STATE.TOUCH_DOLLY_PAN; 1357 | 1358 | break; 1359 | 1360 | case TOUCH.DOLLY_ROTATE: 1361 | 1362 | if ( scope.enableZoom === false && scope.enableRotate === false ) return; 1363 | 1364 | handleTouchStartDollyRotate( event ); 1365 | 1366 | state = STATE.TOUCH_DOLLY_ROTATE; 1367 | 1368 | break; 1369 | 1370 | default: 1371 | 1372 | state = STATE.NONE; 1373 | 1374 | } 1375 | 1376 | break; 1377 | 1378 | default: 1379 | 1380 | state = STATE.NONE; 1381 | 1382 | } 1383 | 1384 | if ( state !== STATE.NONE ) { 1385 | 1386 | scope.dispatchEvent( _startEvent ); 1387 | 1388 | } 1389 | 1390 | } 1391 | 1392 | function onTouchMove( event ) { 1393 | 1394 | trackPointer( event ); 1395 | 1396 | switch ( state ) { 1397 | 1398 | case STATE.TOUCH_ROTATE: 1399 | 1400 | if ( scope.enableRotate === false ) return; 1401 | 1402 | handleTouchMoveRotate( event ); 1403 | 1404 | scope.update(); 1405 | 1406 | break; 1407 | 1408 | case STATE.TOUCH_PAN: 1409 | 1410 | if ( scope.enablePan === false ) return; 1411 | 1412 | handleTouchMovePan( event ); 1413 | 1414 | scope.update(); 1415 | 1416 | break; 1417 | 1418 | case STATE.TOUCH_DOLLY_PAN: 1419 | 1420 | if ( scope.enableZoom === false && scope.enablePan === false ) return; 1421 | 1422 | handleTouchMoveDollyPan( event ); 1423 | 1424 | scope.update(); 1425 | 1426 | break; 1427 | 1428 | case STATE.TOUCH_DOLLY_ROTATE: 1429 | 1430 | if ( scope.enableZoom === false && scope.enableRotate === false ) return; 1431 | 1432 | handleTouchMoveDollyRotate( event ); 1433 | 1434 | scope.update(); 1435 | 1436 | break; 1437 | 1438 | default: 1439 | 1440 | state = STATE.NONE; 1441 | 1442 | } 1443 | 1444 | } 1445 | 1446 | function onContextMenu( event ) { 1447 | 1448 | if ( scope.enabled === false ) return; 1449 | 1450 | event.preventDefault(); 1451 | 1452 | } 1453 | 1454 | function addPointer( event ) { 1455 | 1456 | pointers.push( event.pointerId ); 1457 | 1458 | } 1459 | 1460 | function removePointer( event ) { 1461 | 1462 | delete pointerPositions[ event.pointerId ]; 1463 | 1464 | for ( let i = 0; i < pointers.length; i ++ ) { 1465 | 1466 | if ( pointers[ i ] == event.pointerId ) { 1467 | 1468 | pointers.splice( i, 1 ); 1469 | return; 1470 | 1471 | } 1472 | 1473 | } 1474 | 1475 | } 1476 | 1477 | function isTrackingPointer( event ) { 1478 | 1479 | for ( let i = 0; i < pointers.length; i ++ ) { 1480 | 1481 | if ( pointers[ i ] == event.pointerId ) return true; 1482 | 1483 | } 1484 | 1485 | return false; 1486 | 1487 | } 1488 | 1489 | function trackPointer( event ) { 1490 | 1491 | let position = pointerPositions[ event.pointerId ]; 1492 | 1493 | if ( position === undefined ) { 1494 | 1495 | position = new Vector2(); 1496 | pointerPositions[ event.pointerId ] = position; 1497 | 1498 | } 1499 | 1500 | position.set( event.pageX, event.pageY ); 1501 | 1502 | } 1503 | 1504 | function getSecondPointerPosition( event ) { 1505 | 1506 | const pointerId = ( event.pointerId === pointers[ 0 ] ) ? pointers[ 1 ] : pointers[ 0 ]; 1507 | 1508 | return pointerPositions[ pointerId ]; 1509 | 1510 | } 1511 | 1512 | // 1513 | 1514 | scope.domElement.addEventListener( 'contextmenu', onContextMenu ); 1515 | 1516 | scope.domElement.addEventListener( 'pointerdown', onPointerDown ); 1517 | scope.domElement.addEventListener( 'pointercancel', onPointerUp ); 1518 | scope.domElement.addEventListener( 'wheel', onMouseWheel, { passive: false } ); 1519 | 1520 | const document = scope.domElement.getRootNode(); // offscreen canvas compatibility 1521 | 1522 | document.addEventListener( 'keydown', interceptControlDown, { passive: true, capture: true } ); 1523 | 1524 | // force an update at start 1525 | 1526 | this.update(); 1527 | 1528 | } 1529 | 1530 | } 1531 | 1532 | export { OrbitControls }; 1533 | -------------------------------------------------------------------------------- /templates/jsm/utils/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tudelft3d/ifcgref/221b05a7bf9171e3b788bb72b7dc23117335a0bf/templates/jsm/utils/.DS_Store -------------------------------------------------------------------------------- /templates/jsm/utils/BufferGeometryUtils.js: -------------------------------------------------------------------------------- 1 | import { 2 | BufferAttribute, 3 | BufferGeometry, 4 | Float32BufferAttribute, 5 | InstancedBufferAttribute, 6 | InterleavedBuffer, 7 | InterleavedBufferAttribute, 8 | TriangleFanDrawMode, 9 | TriangleStripDrawMode, 10 | TrianglesDrawMode, 11 | Vector3, 12 | } from 'three'; 13 | 14 | function computeMikkTSpaceTangents( geometry, MikkTSpace, negateSign = true ) { 15 | 16 | if ( ! MikkTSpace || ! MikkTSpace.isReady ) { 17 | 18 | throw new Error( 'BufferGeometryUtils: Initialized MikkTSpace library required.' ); 19 | 20 | } 21 | 22 | if ( ! geometry.hasAttribute( 'position' ) || ! geometry.hasAttribute( 'normal' ) || ! geometry.hasAttribute( 'uv' ) ) { 23 | 24 | throw new Error( 'BufferGeometryUtils: Tangents require "position", "normal", and "uv" attributes.' ); 25 | 26 | } 27 | 28 | function getAttributeArray( attribute ) { 29 | 30 | if ( attribute.normalized || attribute.isInterleavedBufferAttribute ) { 31 | 32 | const dstArray = new Float32Array( attribute.count * attribute.itemSize ); 33 | 34 | for ( let i = 0, j = 0; i < attribute.count; i ++ ) { 35 | 36 | dstArray[ j ++ ] = attribute.getX( i ); 37 | dstArray[ j ++ ] = attribute.getY( i ); 38 | 39 | if ( attribute.itemSize > 2 ) { 40 | 41 | dstArray[ j ++ ] = attribute.getZ( i ); 42 | 43 | } 44 | 45 | } 46 | 47 | return dstArray; 48 | 49 | } 50 | 51 | if ( attribute.array instanceof Float32Array ) { 52 | 53 | return attribute.array; 54 | 55 | } 56 | 57 | return new Float32Array( attribute.array ); 58 | 59 | } 60 | 61 | // MikkTSpace algorithm requires non-indexed input. 62 | 63 | const _geometry = geometry.index ? geometry.toNonIndexed() : geometry; 64 | 65 | // Compute vertex tangents. 66 | 67 | const tangents = MikkTSpace.generateTangents( 68 | 69 | getAttributeArray( _geometry.attributes.position ), 70 | getAttributeArray( _geometry.attributes.normal ), 71 | getAttributeArray( _geometry.attributes.uv ) 72 | 73 | ); 74 | 75 | // Texture coordinate convention of glTF differs from the apparent 76 | // default of the MikkTSpace library; .w component must be flipped. 77 | 78 | if ( negateSign ) { 79 | 80 | for ( let i = 3; i < tangents.length; i += 4 ) { 81 | 82 | tangents[ i ] *= - 1; 83 | 84 | } 85 | 86 | } 87 | 88 | // 89 | 90 | _geometry.setAttribute( 'tangent', new BufferAttribute( tangents, 4 ) ); 91 | 92 | if ( geometry !== _geometry ) { 93 | 94 | geometry.copy( _geometry ); 95 | 96 | } 97 | 98 | return geometry; 99 | 100 | } 101 | 102 | /** 103 | * @param {Array} geometries 104 | * @param {Boolean} useGroups 105 | * @return {BufferGeometry} 106 | */ 107 | function mergeGeometries( geometries, useGroups = false ) { 108 | 109 | const isIndexed = geometries[ 0 ].index !== null; 110 | 111 | const attributesUsed = new Set( Object.keys( geometries[ 0 ].attributes ) ); 112 | const morphAttributesUsed = new Set( Object.keys( geometries[ 0 ].morphAttributes ) ); 113 | 114 | const attributes = {}; 115 | const morphAttributes = {}; 116 | 117 | const morphTargetsRelative = geometries[ 0 ].morphTargetsRelative; 118 | 119 | const mergedGeometry = new BufferGeometry(); 120 | 121 | let offset = 0; 122 | 123 | for ( let i = 0; i < geometries.length; ++ i ) { 124 | 125 | const geometry = geometries[ i ]; 126 | let attributesCount = 0; 127 | 128 | // ensure that all geometries are indexed, or none 129 | 130 | if ( isIndexed !== ( geometry.index !== null ) ) { 131 | 132 | console.error( 'THREE.BufferGeometryUtils: .mergeGeometries() failed with geometry at index ' + i + '. All geometries must have compatible attributes; make sure index attribute exists among all geometries, or in none of them.' ); 133 | return null; 134 | 135 | } 136 | 137 | // gather attributes, exit early if they're different 138 | 139 | for ( const name in geometry.attributes ) { 140 | 141 | if ( ! attributesUsed.has( name ) ) { 142 | 143 | console.error( 'THREE.BufferGeometryUtils: .mergeGeometries() failed with geometry at index ' + i + '. All geometries must have compatible attributes; make sure "' + name + '" attribute exists among all geometries, or in none of them.' ); 144 | return null; 145 | 146 | } 147 | 148 | if ( attributes[ name ] === undefined ) attributes[ name ] = []; 149 | 150 | attributes[ name ].push( geometry.attributes[ name ] ); 151 | 152 | attributesCount ++; 153 | 154 | } 155 | 156 | // ensure geometries have the same number of attributes 157 | 158 | if ( attributesCount !== attributesUsed.size ) { 159 | 160 | console.error( 'THREE.BufferGeometryUtils: .mergeGeometries() failed with geometry at index ' + i + '. Make sure all geometries have the same number of attributes.' ); 161 | return null; 162 | 163 | } 164 | 165 | // gather morph attributes, exit early if they're different 166 | 167 | if ( morphTargetsRelative !== geometry.morphTargetsRelative ) { 168 | 169 | console.error( 'THREE.BufferGeometryUtils: .mergeGeometries() failed with geometry at index ' + i + '. .morphTargetsRelative must be consistent throughout all geometries.' ); 170 | return null; 171 | 172 | } 173 | 174 | for ( const name in geometry.morphAttributes ) { 175 | 176 | if ( ! morphAttributesUsed.has( name ) ) { 177 | 178 | console.error( 'THREE.BufferGeometryUtils: .mergeGeometries() failed with geometry at index ' + i + '. .morphAttributes must be consistent throughout all geometries.' ); 179 | return null; 180 | 181 | } 182 | 183 | if ( morphAttributes[ name ] === undefined ) morphAttributes[ name ] = []; 184 | 185 | morphAttributes[ name ].push( geometry.morphAttributes[ name ] ); 186 | 187 | } 188 | 189 | if ( useGroups ) { 190 | 191 | let count; 192 | 193 | if ( isIndexed ) { 194 | 195 | count = geometry.index.count; 196 | 197 | } else if ( geometry.attributes.position !== undefined ) { 198 | 199 | count = geometry.attributes.position.count; 200 | 201 | } else { 202 | 203 | console.error( 'THREE.BufferGeometryUtils: .mergeGeometries() failed with geometry at index ' + i + '. The geometry must have either an index or a position attribute' ); 204 | return null; 205 | 206 | } 207 | 208 | mergedGeometry.addGroup( offset, count, i ); 209 | 210 | offset += count; 211 | 212 | } 213 | 214 | } 215 | 216 | // merge indices 217 | 218 | if ( isIndexed ) { 219 | 220 | let indexOffset = 0; 221 | const mergedIndex = []; 222 | 223 | for ( let i = 0; i < geometries.length; ++ i ) { 224 | 225 | const index = geometries[ i ].index; 226 | 227 | for ( let j = 0; j < index.count; ++ j ) { 228 | 229 | mergedIndex.push( index.getX( j ) + indexOffset ); 230 | 231 | } 232 | 233 | indexOffset += geometries[ i ].attributes.position.count; 234 | 235 | } 236 | 237 | mergedGeometry.setIndex( mergedIndex ); 238 | 239 | } 240 | 241 | // merge attributes 242 | 243 | for ( const name in attributes ) { 244 | 245 | const mergedAttribute = mergeAttributes( attributes[ name ] ); 246 | 247 | if ( ! mergedAttribute ) { 248 | 249 | console.error( 'THREE.BufferGeometryUtils: .mergeGeometries() failed while trying to merge the ' + name + ' attribute.' ); 250 | return null; 251 | 252 | } 253 | 254 | mergedGeometry.setAttribute( name, mergedAttribute ); 255 | 256 | } 257 | 258 | // merge morph attributes 259 | 260 | for ( const name in morphAttributes ) { 261 | 262 | const numMorphTargets = morphAttributes[ name ][ 0 ].length; 263 | 264 | if ( numMorphTargets === 0 ) break; 265 | 266 | mergedGeometry.morphAttributes = mergedGeometry.morphAttributes || {}; 267 | mergedGeometry.morphAttributes[ name ] = []; 268 | 269 | for ( let i = 0; i < numMorphTargets; ++ i ) { 270 | 271 | const morphAttributesToMerge = []; 272 | 273 | for ( let j = 0; j < morphAttributes[ name ].length; ++ j ) { 274 | 275 | morphAttributesToMerge.push( morphAttributes[ name ][ j ][ i ] ); 276 | 277 | } 278 | 279 | const mergedMorphAttribute = mergeAttributes( morphAttributesToMerge ); 280 | 281 | if ( ! mergedMorphAttribute ) { 282 | 283 | console.error( 'THREE.BufferGeometryUtils: .mergeGeometries() failed while trying to merge the ' + name + ' morphAttribute.' ); 284 | return null; 285 | 286 | } 287 | 288 | mergedGeometry.morphAttributes[ name ].push( mergedMorphAttribute ); 289 | 290 | } 291 | 292 | } 293 | 294 | return mergedGeometry; 295 | 296 | } 297 | 298 | /** 299 | * @param {Array} attributes 300 | * @return {BufferAttribute} 301 | */ 302 | function mergeAttributes( attributes ) { 303 | 304 | let TypedArray; 305 | let itemSize; 306 | let normalized; 307 | let gpuType = - 1; 308 | let arrayLength = 0; 309 | 310 | for ( let i = 0; i < attributes.length; ++ i ) { 311 | 312 | const attribute = attributes[ i ]; 313 | 314 | if ( TypedArray === undefined ) TypedArray = attribute.array.constructor; 315 | if ( TypedArray !== attribute.array.constructor ) { 316 | 317 | console.error( 'THREE.BufferGeometryUtils: .mergeAttributes() failed. BufferAttribute.array must be of consistent array types across matching attributes.' ); 318 | return null; 319 | 320 | } 321 | 322 | if ( itemSize === undefined ) itemSize = attribute.itemSize; 323 | if ( itemSize !== attribute.itemSize ) { 324 | 325 | console.error( 'THREE.BufferGeometryUtils: .mergeAttributes() failed. BufferAttribute.itemSize must be consistent across matching attributes.' ); 326 | return null; 327 | 328 | } 329 | 330 | if ( normalized === undefined ) normalized = attribute.normalized; 331 | if ( normalized !== attribute.normalized ) { 332 | 333 | console.error( 'THREE.BufferGeometryUtils: .mergeAttributes() failed. BufferAttribute.normalized must be consistent across matching attributes.' ); 334 | return null; 335 | 336 | } 337 | 338 | if ( gpuType === - 1 ) gpuType = attribute.gpuType; 339 | if ( gpuType !== attribute.gpuType ) { 340 | 341 | console.error( 'THREE.BufferGeometryUtils: .mergeAttributes() failed. BufferAttribute.gpuType must be consistent across matching attributes.' ); 342 | return null; 343 | 344 | } 345 | 346 | arrayLength += attribute.count * itemSize; 347 | 348 | } 349 | 350 | const array = new TypedArray( arrayLength ); 351 | const result = new BufferAttribute( array, itemSize, normalized ); 352 | let offset = 0; 353 | 354 | for ( let i = 0; i < attributes.length; ++ i ) { 355 | 356 | const attribute = attributes[ i ]; 357 | if ( attribute.isInterleavedBufferAttribute ) { 358 | 359 | const tupleOffset = offset / itemSize; 360 | for ( let j = 0, l = attribute.count; j < l; j ++ ) { 361 | 362 | for ( let c = 0; c < itemSize; c ++ ) { 363 | 364 | const value = attribute.getComponent( j, c ); 365 | result.setComponent( j + tupleOffset, c, value ); 366 | 367 | } 368 | 369 | } 370 | 371 | } else { 372 | 373 | array.set( attribute.array, offset ); 374 | 375 | } 376 | 377 | offset += attribute.count * itemSize; 378 | 379 | } 380 | 381 | if ( gpuType !== undefined ) { 382 | 383 | result.gpuType = gpuType; 384 | 385 | } 386 | 387 | return result; 388 | 389 | } 390 | 391 | /** 392 | * @param {BufferAttribute} 393 | * @return {BufferAttribute} 394 | */ 395 | export function deepCloneAttribute( attribute ) { 396 | 397 | if ( attribute.isInstancedInterleavedBufferAttribute || attribute.isInterleavedBufferAttribute ) { 398 | 399 | return deinterleaveAttribute( attribute ); 400 | 401 | } 402 | 403 | if ( attribute.isInstancedBufferAttribute ) { 404 | 405 | return new InstancedBufferAttribute().copy( attribute ); 406 | 407 | } 408 | 409 | return new BufferAttribute().copy( attribute ); 410 | 411 | } 412 | 413 | /** 414 | * @param {Array} attributes 415 | * @return {Array} 416 | */ 417 | function interleaveAttributes( attributes ) { 418 | 419 | // Interleaves the provided attributes into an InterleavedBuffer and returns 420 | // a set of InterleavedBufferAttributes for each attribute 421 | let TypedArray; 422 | let arrayLength = 0; 423 | let stride = 0; 424 | 425 | // calculate the length and type of the interleavedBuffer 426 | for ( let i = 0, l = attributes.length; i < l; ++ i ) { 427 | 428 | const attribute = attributes[ i ]; 429 | 430 | if ( TypedArray === undefined ) TypedArray = attribute.array.constructor; 431 | if ( TypedArray !== attribute.array.constructor ) { 432 | 433 | console.error( 'AttributeBuffers of different types cannot be interleaved' ); 434 | return null; 435 | 436 | } 437 | 438 | arrayLength += attribute.array.length; 439 | stride += attribute.itemSize; 440 | 441 | } 442 | 443 | // Create the set of buffer attributes 444 | const interleavedBuffer = new InterleavedBuffer( new TypedArray( arrayLength ), stride ); 445 | let offset = 0; 446 | const res = []; 447 | const getters = [ 'getX', 'getY', 'getZ', 'getW' ]; 448 | const setters = [ 'setX', 'setY', 'setZ', 'setW' ]; 449 | 450 | for ( let j = 0, l = attributes.length; j < l; j ++ ) { 451 | 452 | const attribute = attributes[ j ]; 453 | const itemSize = attribute.itemSize; 454 | const count = attribute.count; 455 | const iba = new InterleavedBufferAttribute( interleavedBuffer, itemSize, offset, attribute.normalized ); 456 | res.push( iba ); 457 | 458 | offset += itemSize; 459 | 460 | // Move the data for each attribute into the new interleavedBuffer 461 | // at the appropriate offset 462 | for ( let c = 0; c < count; c ++ ) { 463 | 464 | for ( let k = 0; k < itemSize; k ++ ) { 465 | 466 | iba[ setters[ k ] ]( c, attribute[ getters[ k ] ]( c ) ); 467 | 468 | } 469 | 470 | } 471 | 472 | } 473 | 474 | return res; 475 | 476 | } 477 | 478 | // returns a new, non-interleaved version of the provided attribute 479 | export function deinterleaveAttribute( attribute ) { 480 | 481 | const cons = attribute.data.array.constructor; 482 | const count = attribute.count; 483 | const itemSize = attribute.itemSize; 484 | const normalized = attribute.normalized; 485 | 486 | const array = new cons( count * itemSize ); 487 | let newAttribute; 488 | if ( attribute.isInstancedInterleavedBufferAttribute ) { 489 | 490 | newAttribute = new InstancedBufferAttribute( array, itemSize, normalized, attribute.meshPerAttribute ); 491 | 492 | } else { 493 | 494 | newAttribute = new BufferAttribute( array, itemSize, normalized ); 495 | 496 | } 497 | 498 | for ( let i = 0; i < count; i ++ ) { 499 | 500 | newAttribute.setX( i, attribute.getX( i ) ); 501 | 502 | if ( itemSize >= 2 ) { 503 | 504 | newAttribute.setY( i, attribute.getY( i ) ); 505 | 506 | } 507 | 508 | if ( itemSize >= 3 ) { 509 | 510 | newAttribute.setZ( i, attribute.getZ( i ) ); 511 | 512 | } 513 | 514 | if ( itemSize >= 4 ) { 515 | 516 | newAttribute.setW( i, attribute.getW( i ) ); 517 | 518 | } 519 | 520 | } 521 | 522 | return newAttribute; 523 | 524 | } 525 | 526 | // deinterleaves all attributes on the geometry 527 | export function deinterleaveGeometry( geometry ) { 528 | 529 | const attributes = geometry.attributes; 530 | const morphTargets = geometry.morphTargets; 531 | const attrMap = new Map(); 532 | 533 | for ( const key in attributes ) { 534 | 535 | const attr = attributes[ key ]; 536 | if ( attr.isInterleavedBufferAttribute ) { 537 | 538 | if ( ! attrMap.has( attr ) ) { 539 | 540 | attrMap.set( attr, deinterleaveAttribute( attr ) ); 541 | 542 | } 543 | 544 | attributes[ key ] = attrMap.get( attr ); 545 | 546 | } 547 | 548 | } 549 | 550 | for ( const key in morphTargets ) { 551 | 552 | const attr = morphTargets[ key ]; 553 | if ( attr.isInterleavedBufferAttribute ) { 554 | 555 | if ( ! attrMap.has( attr ) ) { 556 | 557 | attrMap.set( attr, deinterleaveAttribute( attr ) ); 558 | 559 | } 560 | 561 | morphTargets[ key ] = attrMap.get( attr ); 562 | 563 | } 564 | 565 | } 566 | 567 | } 568 | 569 | /** 570 | * @param {BufferGeometry} geometry 571 | * @return {number} 572 | */ 573 | function estimateBytesUsed( geometry ) { 574 | 575 | // Return the estimated memory used by this geometry in bytes 576 | // Calculate using itemSize, count, and BYTES_PER_ELEMENT to account 577 | // for InterleavedBufferAttributes. 578 | let mem = 0; 579 | for ( const name in geometry.attributes ) { 580 | 581 | const attr = geometry.getAttribute( name ); 582 | mem += attr.count * attr.itemSize * attr.array.BYTES_PER_ELEMENT; 583 | 584 | } 585 | 586 | const indices = geometry.getIndex(); 587 | mem += indices ? indices.count * indices.itemSize * indices.array.BYTES_PER_ELEMENT : 0; 588 | return mem; 589 | 590 | } 591 | 592 | /** 593 | * @param {BufferGeometry} geometry 594 | * @param {number} tolerance 595 | * @return {BufferGeometry} 596 | */ 597 | function mergeVertices( geometry, tolerance = 1e-4 ) { 598 | 599 | tolerance = Math.max( tolerance, Number.EPSILON ); 600 | 601 | // Generate an index buffer if the geometry doesn't have one, or optimize it 602 | // if it's already available. 603 | const hashToIndex = {}; 604 | const indices = geometry.getIndex(); 605 | const positions = geometry.getAttribute( 'position' ); 606 | const vertexCount = indices ? indices.count : positions.count; 607 | 608 | // next value for triangle indices 609 | let nextIndex = 0; 610 | 611 | // attributes and new attribute arrays 612 | const attributeNames = Object.keys( geometry.attributes ); 613 | const tmpAttributes = {}; 614 | const tmpMorphAttributes = {}; 615 | const newIndices = []; 616 | const getters = [ 'getX', 'getY', 'getZ', 'getW' ]; 617 | const setters = [ 'setX', 'setY', 'setZ', 'setW' ]; 618 | 619 | // Initialize the arrays, allocating space conservatively. Extra 620 | // space will be trimmed in the last step. 621 | for ( let i = 0, l = attributeNames.length; i < l; i ++ ) { 622 | 623 | const name = attributeNames[ i ]; 624 | const attr = geometry.attributes[ name ]; 625 | 626 | tmpAttributes[ name ] = new BufferAttribute( 627 | new attr.array.constructor( attr.count * attr.itemSize ), 628 | attr.itemSize, 629 | attr.normalized 630 | ); 631 | 632 | const morphAttr = geometry.morphAttributes[ name ]; 633 | if ( morphAttr ) { 634 | 635 | tmpMorphAttributes[ name ] = new BufferAttribute( 636 | new morphAttr.array.constructor( morphAttr.count * morphAttr.itemSize ), 637 | morphAttr.itemSize, 638 | morphAttr.normalized 639 | ); 640 | 641 | } 642 | 643 | } 644 | 645 | // convert the error tolerance to an amount of decimal places to truncate to 646 | const halfTolerance = tolerance * 0.5; 647 | const exponent = Math.log10( 1 / tolerance ); 648 | const hashMultiplier = Math.pow( 10, exponent ); 649 | const hashAdditive = halfTolerance * hashMultiplier; 650 | for ( let i = 0; i < vertexCount; i ++ ) { 651 | 652 | const index = indices ? indices.getX( i ) : i; 653 | 654 | // Generate a hash for the vertex attributes at the current index 'i' 655 | let hash = ''; 656 | for ( let j = 0, l = attributeNames.length; j < l; j ++ ) { 657 | 658 | const name = attributeNames[ j ]; 659 | const attribute = geometry.getAttribute( name ); 660 | const itemSize = attribute.itemSize; 661 | 662 | for ( let k = 0; k < itemSize; k ++ ) { 663 | 664 | // double tilde truncates the decimal value 665 | hash += `${ ~ ~ ( attribute[ getters[ k ] ]( index ) * hashMultiplier + hashAdditive ) },`; 666 | 667 | } 668 | 669 | } 670 | 671 | // Add another reference to the vertex if it's already 672 | // used by another index 673 | if ( hash in hashToIndex ) { 674 | 675 | newIndices.push( hashToIndex[ hash ] ); 676 | 677 | } else { 678 | 679 | // copy data to the new index in the temporary attributes 680 | for ( let j = 0, l = attributeNames.length; j < l; j ++ ) { 681 | 682 | const name = attributeNames[ j ]; 683 | const attribute = geometry.getAttribute( name ); 684 | const morphAttr = geometry.morphAttributes[ name ]; 685 | const itemSize = attribute.itemSize; 686 | const newarray = tmpAttributes[ name ]; 687 | const newMorphArrays = tmpMorphAttributes[ name ]; 688 | 689 | for ( let k = 0; k < itemSize; k ++ ) { 690 | 691 | const getterFunc = getters[ k ]; 692 | const setterFunc = setters[ k ]; 693 | newarray[ setterFunc ]( nextIndex, attribute[ getterFunc ]( index ) ); 694 | 695 | if ( morphAttr ) { 696 | 697 | for ( let m = 0, ml = morphAttr.length; m < ml; m ++ ) { 698 | 699 | newMorphArrays[ m ][ setterFunc ]( nextIndex, morphAttr[ m ][ getterFunc ]( index ) ); 700 | 701 | } 702 | 703 | } 704 | 705 | } 706 | 707 | } 708 | 709 | hashToIndex[ hash ] = nextIndex; 710 | newIndices.push( nextIndex ); 711 | nextIndex ++; 712 | 713 | } 714 | 715 | } 716 | 717 | // generate result BufferGeometry 718 | const result = geometry.clone(); 719 | for ( const name in geometry.attributes ) { 720 | 721 | const tmpAttribute = tmpAttributes[ name ]; 722 | 723 | result.setAttribute( name, new BufferAttribute( 724 | tmpAttribute.array.slice( 0, nextIndex * tmpAttribute.itemSize ), 725 | tmpAttribute.itemSize, 726 | tmpAttribute.normalized, 727 | ) ); 728 | 729 | if ( ! ( name in tmpMorphAttributes ) ) continue; 730 | 731 | for ( let j = 0; j < tmpMorphAttributes[ name ].length; j ++ ) { 732 | 733 | const tmpMorphAttribute = tmpMorphAttributes[ name ][ j ]; 734 | 735 | result.morphAttributes[ name ][ j ] = new BufferAttribute( 736 | tmpMorphAttribute.array.slice( 0, nextIndex * tmpMorphAttribute.itemSize ), 737 | tmpMorphAttribute.itemSize, 738 | tmpMorphAttribute.normalized, 739 | ); 740 | 741 | } 742 | 743 | } 744 | 745 | // indices 746 | 747 | result.setIndex( newIndices ); 748 | 749 | return result; 750 | 751 | } 752 | 753 | /** 754 | * @param {BufferGeometry} geometry 755 | * @param {number} drawMode 756 | * @return {BufferGeometry} 757 | */ 758 | function toTrianglesDrawMode( geometry, drawMode ) { 759 | 760 | if ( drawMode === TrianglesDrawMode ) { 761 | 762 | console.warn( 'THREE.BufferGeometryUtils.toTrianglesDrawMode(): Geometry already defined as triangles.' ); 763 | return geometry; 764 | 765 | } 766 | 767 | if ( drawMode === TriangleFanDrawMode || drawMode === TriangleStripDrawMode ) { 768 | 769 | let index = geometry.getIndex(); 770 | 771 | // generate index if not present 772 | 773 | if ( index === null ) { 774 | 775 | const indices = []; 776 | 777 | const position = geometry.getAttribute( 'position' ); 778 | 779 | if ( position !== undefined ) { 780 | 781 | for ( let i = 0; i < position.count; i ++ ) { 782 | 783 | indices.push( i ); 784 | 785 | } 786 | 787 | geometry.setIndex( indices ); 788 | index = geometry.getIndex(); 789 | 790 | } else { 791 | 792 | console.error( 'THREE.BufferGeometryUtils.toTrianglesDrawMode(): Undefined position attribute. Processing not possible.' ); 793 | return geometry; 794 | 795 | } 796 | 797 | } 798 | 799 | // 800 | 801 | const numberOfTriangles = index.count - 2; 802 | const newIndices = []; 803 | 804 | if ( drawMode === TriangleFanDrawMode ) { 805 | 806 | // gl.TRIANGLE_FAN 807 | 808 | for ( let i = 1; i <= numberOfTriangles; i ++ ) { 809 | 810 | newIndices.push( index.getX( 0 ) ); 811 | newIndices.push( index.getX( i ) ); 812 | newIndices.push( index.getX( i + 1 ) ); 813 | 814 | } 815 | 816 | } else { 817 | 818 | // gl.TRIANGLE_STRIP 819 | 820 | for ( let i = 0; i < numberOfTriangles; i ++ ) { 821 | 822 | if ( i % 2 === 0 ) { 823 | 824 | newIndices.push( index.getX( i ) ); 825 | newIndices.push( index.getX( i + 1 ) ); 826 | newIndices.push( index.getX( i + 2 ) ); 827 | 828 | } else { 829 | 830 | newIndices.push( index.getX( i + 2 ) ); 831 | newIndices.push( index.getX( i + 1 ) ); 832 | newIndices.push( index.getX( i ) ); 833 | 834 | } 835 | 836 | } 837 | 838 | } 839 | 840 | if ( ( newIndices.length / 3 ) !== numberOfTriangles ) { 841 | 842 | console.error( 'THREE.BufferGeometryUtils.toTrianglesDrawMode(): Unable to generate correct amount of triangles.' ); 843 | 844 | } 845 | 846 | // build final geometry 847 | 848 | const newGeometry = geometry.clone(); 849 | newGeometry.setIndex( newIndices ); 850 | newGeometry.clearGroups(); 851 | 852 | return newGeometry; 853 | 854 | } else { 855 | 856 | console.error( 'THREE.BufferGeometryUtils.toTrianglesDrawMode(): Unknown draw mode:', drawMode ); 857 | return geometry; 858 | 859 | } 860 | 861 | } 862 | 863 | /** 864 | * Calculates the morphed attributes of a morphed/skinned BufferGeometry. 865 | * Helpful for Raytracing or Decals. 866 | * @param {Mesh | Line | Points} object An instance of Mesh, Line or Points. 867 | * @return {Object} An Object with original position/normal attributes and morphed ones. 868 | */ 869 | function computeMorphedAttributes( object ) { 870 | 871 | const _vA = new Vector3(); 872 | const _vB = new Vector3(); 873 | const _vC = new Vector3(); 874 | 875 | const _tempA = new Vector3(); 876 | const _tempB = new Vector3(); 877 | const _tempC = new Vector3(); 878 | 879 | const _morphA = new Vector3(); 880 | const _morphB = new Vector3(); 881 | const _morphC = new Vector3(); 882 | 883 | function _calculateMorphedAttributeData( 884 | object, 885 | attribute, 886 | morphAttribute, 887 | morphTargetsRelative, 888 | a, 889 | b, 890 | c, 891 | modifiedAttributeArray 892 | ) { 893 | 894 | _vA.fromBufferAttribute( attribute, a ); 895 | _vB.fromBufferAttribute( attribute, b ); 896 | _vC.fromBufferAttribute( attribute, c ); 897 | 898 | const morphInfluences = object.morphTargetInfluences; 899 | 900 | if ( morphAttribute && morphInfluences ) { 901 | 902 | _morphA.set( 0, 0, 0 ); 903 | _morphB.set( 0, 0, 0 ); 904 | _morphC.set( 0, 0, 0 ); 905 | 906 | for ( let i = 0, il = morphAttribute.length; i < il; i ++ ) { 907 | 908 | const influence = morphInfluences[ i ]; 909 | const morph = morphAttribute[ i ]; 910 | 911 | if ( influence === 0 ) continue; 912 | 913 | _tempA.fromBufferAttribute( morph, a ); 914 | _tempB.fromBufferAttribute( morph, b ); 915 | _tempC.fromBufferAttribute( morph, c ); 916 | 917 | if ( morphTargetsRelative ) { 918 | 919 | _morphA.addScaledVector( _tempA, influence ); 920 | _morphB.addScaledVector( _tempB, influence ); 921 | _morphC.addScaledVector( _tempC, influence ); 922 | 923 | } else { 924 | 925 | _morphA.addScaledVector( _tempA.sub( _vA ), influence ); 926 | _morphB.addScaledVector( _tempB.sub( _vB ), influence ); 927 | _morphC.addScaledVector( _tempC.sub( _vC ), influence ); 928 | 929 | } 930 | 931 | } 932 | 933 | _vA.add( _morphA ); 934 | _vB.add( _morphB ); 935 | _vC.add( _morphC ); 936 | 937 | } 938 | 939 | if ( object.isSkinnedMesh ) { 940 | 941 | object.applyBoneTransform( a, _vA ); 942 | object.applyBoneTransform( b, _vB ); 943 | object.applyBoneTransform( c, _vC ); 944 | 945 | } 946 | 947 | modifiedAttributeArray[ a * 3 + 0 ] = _vA.x; 948 | modifiedAttributeArray[ a * 3 + 1 ] = _vA.y; 949 | modifiedAttributeArray[ a * 3 + 2 ] = _vA.z; 950 | modifiedAttributeArray[ b * 3 + 0 ] = _vB.x; 951 | modifiedAttributeArray[ b * 3 + 1 ] = _vB.y; 952 | modifiedAttributeArray[ b * 3 + 2 ] = _vB.z; 953 | modifiedAttributeArray[ c * 3 + 0 ] = _vC.x; 954 | modifiedAttributeArray[ c * 3 + 1 ] = _vC.y; 955 | modifiedAttributeArray[ c * 3 + 2 ] = _vC.z; 956 | 957 | } 958 | 959 | const geometry = object.geometry; 960 | const material = object.material; 961 | 962 | let a, b, c; 963 | const index = geometry.index; 964 | const positionAttribute = geometry.attributes.position; 965 | const morphPosition = geometry.morphAttributes.position; 966 | const morphTargetsRelative = geometry.morphTargetsRelative; 967 | const normalAttribute = geometry.attributes.normal; 968 | const morphNormal = geometry.morphAttributes.position; 969 | 970 | const groups = geometry.groups; 971 | const drawRange = geometry.drawRange; 972 | let i, j, il, jl; 973 | let group; 974 | let start, end; 975 | 976 | const modifiedPosition = new Float32Array( positionAttribute.count * positionAttribute.itemSize ); 977 | const modifiedNormal = new Float32Array( normalAttribute.count * normalAttribute.itemSize ); 978 | 979 | if ( index !== null ) { 980 | 981 | // indexed buffer geometry 982 | 983 | if ( Array.isArray( material ) ) { 984 | 985 | for ( i = 0, il = groups.length; i < il; i ++ ) { 986 | 987 | group = groups[ i ]; 988 | 989 | start = Math.max( group.start, drawRange.start ); 990 | end = Math.min( ( group.start + group.count ), ( drawRange.start + drawRange.count ) ); 991 | 992 | for ( j = start, jl = end; j < jl; j += 3 ) { 993 | 994 | a = index.getX( j ); 995 | b = index.getX( j + 1 ); 996 | c = index.getX( j + 2 ); 997 | 998 | _calculateMorphedAttributeData( 999 | object, 1000 | positionAttribute, 1001 | morphPosition, 1002 | morphTargetsRelative, 1003 | a, b, c, 1004 | modifiedPosition 1005 | ); 1006 | 1007 | _calculateMorphedAttributeData( 1008 | object, 1009 | normalAttribute, 1010 | morphNormal, 1011 | morphTargetsRelative, 1012 | a, b, c, 1013 | modifiedNormal 1014 | ); 1015 | 1016 | } 1017 | 1018 | } 1019 | 1020 | } else { 1021 | 1022 | start = Math.max( 0, drawRange.start ); 1023 | end = Math.min( index.count, ( drawRange.start + drawRange.count ) ); 1024 | 1025 | for ( i = start, il = end; i < il; i += 3 ) { 1026 | 1027 | a = index.getX( i ); 1028 | b = index.getX( i + 1 ); 1029 | c = index.getX( i + 2 ); 1030 | 1031 | _calculateMorphedAttributeData( 1032 | object, 1033 | positionAttribute, 1034 | morphPosition, 1035 | morphTargetsRelative, 1036 | a, b, c, 1037 | modifiedPosition 1038 | ); 1039 | 1040 | _calculateMorphedAttributeData( 1041 | object, 1042 | normalAttribute, 1043 | morphNormal, 1044 | morphTargetsRelative, 1045 | a, b, c, 1046 | modifiedNormal 1047 | ); 1048 | 1049 | } 1050 | 1051 | } 1052 | 1053 | } else { 1054 | 1055 | // non-indexed buffer geometry 1056 | 1057 | if ( Array.isArray( material ) ) { 1058 | 1059 | for ( i = 0, il = groups.length; i < il; i ++ ) { 1060 | 1061 | group = groups[ i ]; 1062 | 1063 | start = Math.max( group.start, drawRange.start ); 1064 | end = Math.min( ( group.start + group.count ), ( drawRange.start + drawRange.count ) ); 1065 | 1066 | for ( j = start, jl = end; j < jl; j += 3 ) { 1067 | 1068 | a = j; 1069 | b = j + 1; 1070 | c = j + 2; 1071 | 1072 | _calculateMorphedAttributeData( 1073 | object, 1074 | positionAttribute, 1075 | morphPosition, 1076 | morphTargetsRelative, 1077 | a, b, c, 1078 | modifiedPosition 1079 | ); 1080 | 1081 | _calculateMorphedAttributeData( 1082 | object, 1083 | normalAttribute, 1084 | morphNormal, 1085 | morphTargetsRelative, 1086 | a, b, c, 1087 | modifiedNormal 1088 | ); 1089 | 1090 | } 1091 | 1092 | } 1093 | 1094 | } else { 1095 | 1096 | start = Math.max( 0, drawRange.start ); 1097 | end = Math.min( positionAttribute.count, ( drawRange.start + drawRange.count ) ); 1098 | 1099 | for ( i = start, il = end; i < il; i += 3 ) { 1100 | 1101 | a = i; 1102 | b = i + 1; 1103 | c = i + 2; 1104 | 1105 | _calculateMorphedAttributeData( 1106 | object, 1107 | positionAttribute, 1108 | morphPosition, 1109 | morphTargetsRelative, 1110 | a, b, c, 1111 | modifiedPosition 1112 | ); 1113 | 1114 | _calculateMorphedAttributeData( 1115 | object, 1116 | normalAttribute, 1117 | morphNormal, 1118 | morphTargetsRelative, 1119 | a, b, c, 1120 | modifiedNormal 1121 | ); 1122 | 1123 | } 1124 | 1125 | } 1126 | 1127 | } 1128 | 1129 | const morphedPositionAttribute = new Float32BufferAttribute( modifiedPosition, 3 ); 1130 | const morphedNormalAttribute = new Float32BufferAttribute( modifiedNormal, 3 ); 1131 | 1132 | return { 1133 | 1134 | positionAttribute: positionAttribute, 1135 | normalAttribute: normalAttribute, 1136 | morphedPositionAttribute: morphedPositionAttribute, 1137 | morphedNormalAttribute: morphedNormalAttribute 1138 | 1139 | }; 1140 | 1141 | } 1142 | 1143 | function mergeGroups( geometry ) { 1144 | 1145 | if ( geometry.groups.length === 0 ) { 1146 | 1147 | console.warn( 'THREE.BufferGeometryUtils.mergeGroups(): No groups are defined. Nothing to merge.' ); 1148 | return geometry; 1149 | 1150 | } 1151 | 1152 | let groups = geometry.groups; 1153 | 1154 | // sort groups by material index 1155 | 1156 | groups = groups.sort( ( a, b ) => { 1157 | 1158 | if ( a.materialIndex !== b.materialIndex ) return a.materialIndex - b.materialIndex; 1159 | 1160 | return a.start - b.start; 1161 | 1162 | } ); 1163 | 1164 | // create index for non-indexed geometries 1165 | 1166 | if ( geometry.getIndex() === null ) { 1167 | 1168 | const positionAttribute = geometry.getAttribute( 'position' ); 1169 | const indices = []; 1170 | 1171 | for ( let i = 0; i < positionAttribute.count; i += 3 ) { 1172 | 1173 | indices.push( i, i + 1, i + 2 ); 1174 | 1175 | } 1176 | 1177 | geometry.setIndex( indices ); 1178 | 1179 | } 1180 | 1181 | // sort index 1182 | 1183 | const index = geometry.getIndex(); 1184 | 1185 | const newIndices = []; 1186 | 1187 | for ( let i = 0; i < groups.length; i ++ ) { 1188 | 1189 | const group = groups[ i ]; 1190 | 1191 | const groupStart = group.start; 1192 | const groupLength = groupStart + group.count; 1193 | 1194 | for ( let j = groupStart; j < groupLength; j ++ ) { 1195 | 1196 | newIndices.push( index.getX( j ) ); 1197 | 1198 | } 1199 | 1200 | } 1201 | 1202 | geometry.dispose(); // Required to force buffer recreation 1203 | geometry.setIndex( newIndices ); 1204 | 1205 | // update groups indices 1206 | 1207 | let start = 0; 1208 | 1209 | for ( let i = 0; i < groups.length; i ++ ) { 1210 | 1211 | const group = groups[ i ]; 1212 | 1213 | group.start = start; 1214 | start += group.count; 1215 | 1216 | } 1217 | 1218 | // merge groups 1219 | 1220 | let currentGroup = groups[ 0 ]; 1221 | 1222 | geometry.groups = [ currentGroup ]; 1223 | 1224 | for ( let i = 1; i < groups.length; i ++ ) { 1225 | 1226 | const group = groups[ i ]; 1227 | 1228 | if ( currentGroup.materialIndex === group.materialIndex ) { 1229 | 1230 | currentGroup.count += group.count; 1231 | 1232 | } else { 1233 | 1234 | currentGroup = group; 1235 | geometry.groups.push( currentGroup ); 1236 | 1237 | } 1238 | 1239 | } 1240 | 1241 | return geometry; 1242 | 1243 | } 1244 | 1245 | 1246 | /** 1247 | * Modifies the supplied geometry if it is non-indexed, otherwise creates a new, 1248 | * non-indexed geometry. Returns the geometry with smooth normals everywhere except 1249 | * faces that meet at an angle greater than the crease angle. 1250 | * 1251 | * @param {BufferGeometry} geometry 1252 | * @param {number} [creaseAngle] 1253 | * @return {BufferGeometry} 1254 | */ 1255 | function toCreasedNormals( geometry, creaseAngle = Math.PI / 3 /* 60 degrees */ ) { 1256 | 1257 | const creaseDot = Math.cos( creaseAngle ); 1258 | const hashMultiplier = ( 1 + 1e-10 ) * 1e2; 1259 | 1260 | // reusable vectors 1261 | const verts = [ new Vector3(), new Vector3(), new Vector3() ]; 1262 | const tempVec1 = new Vector3(); 1263 | const tempVec2 = new Vector3(); 1264 | const tempNorm = new Vector3(); 1265 | const tempNorm2 = new Vector3(); 1266 | 1267 | // hashes a vector 1268 | function hashVertex( v ) { 1269 | 1270 | const x = ~ ~ ( v.x * hashMultiplier ); 1271 | const y = ~ ~ ( v.y * hashMultiplier ); 1272 | const z = ~ ~ ( v.z * hashMultiplier ); 1273 | return `${x},${y},${z}`; 1274 | 1275 | } 1276 | 1277 | // BufferGeometry.toNonIndexed() warns if the geometry is non-indexed 1278 | // and returns the original geometry 1279 | const resultGeometry = geometry.index ? geometry.toNonIndexed() : geometry; 1280 | const posAttr = resultGeometry.attributes.position; 1281 | const vertexMap = {}; 1282 | 1283 | // find all the normals shared by commonly located vertices 1284 | for ( let i = 0, l = posAttr.count / 3; i < l; i ++ ) { 1285 | 1286 | const i3 = 3 * i; 1287 | const a = verts[ 0 ].fromBufferAttribute( posAttr, i3 + 0 ); 1288 | const b = verts[ 1 ].fromBufferAttribute( posAttr, i3 + 1 ); 1289 | const c = verts[ 2 ].fromBufferAttribute( posAttr, i3 + 2 ); 1290 | 1291 | tempVec1.subVectors( c, b ); 1292 | tempVec2.subVectors( a, b ); 1293 | 1294 | // add the normal to the map for all vertices 1295 | const normal = new Vector3().crossVectors( tempVec1, tempVec2 ).normalize(); 1296 | for ( let n = 0; n < 3; n ++ ) { 1297 | 1298 | const vert = verts[ n ]; 1299 | const hash = hashVertex( vert ); 1300 | if ( ! ( hash in vertexMap ) ) { 1301 | 1302 | vertexMap[ hash ] = []; 1303 | 1304 | } 1305 | 1306 | vertexMap[ hash ].push( normal ); 1307 | 1308 | } 1309 | 1310 | } 1311 | 1312 | // average normals from all vertices that share a common location if they are within the 1313 | // provided crease threshold 1314 | const normalArray = new Float32Array( posAttr.count * 3 ); 1315 | const normAttr = new BufferAttribute( normalArray, 3, false ); 1316 | for ( let i = 0, l = posAttr.count / 3; i < l; i ++ ) { 1317 | 1318 | // get the face normal for this vertex 1319 | const i3 = 3 * i; 1320 | const a = verts[ 0 ].fromBufferAttribute( posAttr, i3 + 0 ); 1321 | const b = verts[ 1 ].fromBufferAttribute( posAttr, i3 + 1 ); 1322 | const c = verts[ 2 ].fromBufferAttribute( posAttr, i3 + 2 ); 1323 | 1324 | tempVec1.subVectors( c, b ); 1325 | tempVec2.subVectors( a, b ); 1326 | 1327 | tempNorm.crossVectors( tempVec1, tempVec2 ).normalize(); 1328 | 1329 | // average all normals that meet the threshold and set the normal value 1330 | for ( let n = 0; n < 3; n ++ ) { 1331 | 1332 | const vert = verts[ n ]; 1333 | const hash = hashVertex( vert ); 1334 | const otherNormals = vertexMap[ hash ]; 1335 | tempNorm2.set( 0, 0, 0 ); 1336 | 1337 | for ( let k = 0, lk = otherNormals.length; k < lk; k ++ ) { 1338 | 1339 | const otherNorm = otherNormals[ k ]; 1340 | if ( tempNorm.dot( otherNorm ) > creaseDot ) { 1341 | 1342 | tempNorm2.add( otherNorm ); 1343 | 1344 | } 1345 | 1346 | } 1347 | 1348 | tempNorm2.normalize(); 1349 | normAttr.setXYZ( i3 + n, tempNorm2.x, tempNorm2.y, tempNorm2.z ); 1350 | 1351 | } 1352 | 1353 | } 1354 | 1355 | resultGeometry.setAttribute( 'normal', normAttr ); 1356 | return resultGeometry; 1357 | 1358 | } 1359 | 1360 | export { 1361 | computeMikkTSpaceTangents, 1362 | mergeGeometries, 1363 | mergeAttributes, 1364 | interleaveAttributes, 1365 | estimateBytesUsed, 1366 | mergeVertices, 1367 | toTrianglesDrawMode, 1368 | computeMorphedAttributes, 1369 | mergeGroups, 1370 | toCreasedNormals 1371 | }; 1372 | -------------------------------------------------------------------------------- /templates/main.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | background-color: #000; 4 | color: #fff; 5 | font-family: Monospace; 6 | font-size: 13px; 7 | line-height: 24px; 8 | overscroll-behavior: none; 9 | } 10 | 11 | a { 12 | color: #ff0; 13 | text-decoration: none; 14 | } 15 | 16 | a:hover { 17 | text-decoration: underline; 18 | } 19 | 20 | button { 21 | cursor: pointer; 22 | text-transform: uppercase; 23 | } 24 | 25 | #info { 26 | position: absolute; 27 | top: 0px; 28 | width: 100%; 29 | padding: 10px; 30 | box-sizing: border-box; 31 | text-align: center; 32 | -moz-user-select: none; 33 | -webkit-user-select: none; 34 | -ms-user-select: none; 35 | user-select: none; 36 | pointer-events: none; 37 | z-index: 1; /* TODO Solve this in HTML */ 38 | } 39 | 40 | a, button, input, select { 41 | pointer-events: auto; 42 | } 43 | 44 | .lil-gui { 45 | z-index: 2 !important; /* TODO Solve this in HTML */ 46 | } 47 | 48 | @media all and ( max-width: 640px ) { 49 | .lil-gui.root { 50 | right: auto; 51 | top: auto; 52 | max-height: 50%; 53 | max-width: 80%; 54 | bottom: 0; 55 | left: 0; 56 | } 57 | } 58 | 59 | #overlay { 60 | position: absolute; 61 | font-size: 16px; 62 | z-index: 2; 63 | top: 0; 64 | left: 0; 65 | width: 100%; 66 | height: 100%; 67 | display: flex; 68 | align-items: center; 69 | justify-content: center; 70 | flex-direction: column; 71 | background: rgba(0,0,0,0.7); 72 | } 73 | 74 | #overlay button { 75 | background: transparent; 76 | border: 0; 77 | border: 1px solid rgb(255, 255, 255); 78 | border-radius: 4px; 79 | color: #ffffff; 80 | padding: 12px 18px; 81 | text-transform: uppercase; 82 | cursor: pointer; 83 | } 84 | 85 | #notSupported { 86 | width: 50%; 87 | margin: auto; 88 | background-color: #f00; 89 | margin-top: 20px; 90 | padding: 10px; 91 | } 92 | -------------------------------------------------------------------------------- /templates/result.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Results 7 | 8 | 98 | 99 | 100 |
101 | 102 | 103 |
104 |
105 | 106 | {% if message %} 107 |
{{ message | safe }}
108 | {% endif %} 109 |

IFCProjectedCRS Data

110 | {{ table_f | safe }} 111 | 112 |

IFCMapConversion Data

113 | {{ table_g | safe }} 114 | 115 |
116 | 117 |
118 | 119 | {% if not message %} 120 | 121 | 122 | 123 | {% endif %} 124 | 125 |
126 | Loading... 127 |
128 |
129 | 130 | 135 | 136 | 137 | -------------------------------------------------------------------------------- /templates/survey.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | BIM and Geo information 7 | 8 | 143 | 144 | 145 |
146 | 147 | 148 |
149 |
150 |

BIM and Geo Information

151 | 152 | {% if messages %} 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | {% for message, detail in messages %} 162 | 163 | 164 | 165 | 166 | {% endfor %} 167 | 168 |
ItemDetails
{{ message }}{{ detail }}
169 | {% endif %} 170 | 171 | {% if error %} 172 |

{{ error }}

173 | {% endif %} 174 | 175 | {% if not Num %} 176 | {% if Refl %} 177 |

Choose how you want to georeference your IFC file.

178 |
179 | Image 1 180 |
Use existing information
181 |
182 |
183 | Image 2 184 |
Add surveyed points to existing information
185 |
186 |
187 | Image 3 188 |
Add surveyed points and ignore existing information
189 |
190 | {% else %} 191 |
192 | 193 | 194 | 195 |
196 | {% endif %} 197 | {% endif%} 198 | 199 | 200 | 201 | 202 | 203 | 204 | {% if Num %} 205 |
206 |
207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | {% for row in range(Num) %} 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | {% endfor %} 231 |
Source CRS (IFC) [{{ ifcunit }}]Target CRS (MAP) [{{ mapunit }}]
XYZX'Y'Z'
232 |
233 | 234 |
235 | {% else %} 236 | 242 | {% endif %} 243 | 244 |
245 | Loading... 246 |
247 |
248 | 249 | 289 | 290 | -------------------------------------------------------------------------------- /templates/upload.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | IfcGref 7 | 8 | 141 | 142 | 143 |
144 | 145 | 146 |
147 |
148 |

Upload .ifc file

149 | 150 | 151 | {% if error_message %} 152 |

{{ error_message }}

153 | {% endif %} 154 | 155 |
156 |
157 |

Drag & drop your .ifc file here, or click to select it.

158 | 159 |
160 |
161 | 162 |
163 | Loading... 164 |
165 | 166 | 167 |
168 | 169 | 214 | 215 |
216 | User Guide Image 217 |
User Guide
218 |
219 | 229 | 230 | -------------------------------------------------------------------------------- /templates/view.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | IFC Locator 5 | 6 | 7 | 8 | 9 | 10 | 14 | 15 | 16 |
17 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /templates/view3D.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | IfcGref Viewer 5 | 6 | 7 | 8 | 9 | 41 | 42 | 43 | 44 |
45 |
46 | 49 | 52 |
53 | 54 | logo1 55 | logo2 56 | 57 | 58 | 59 | 71 | 72 | 259 | 260 | -------------------------------------------------------------------------------- /uploads/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tudelft3d/ifcgref/221b05a7bf9171e3b788bb72b7dc23117335a0bf/uploads/.DS_Store --------------------------------------------------------------------------------