This commit is contained in:
Andrej730
2025-09-08 14:42:36 +05:00
parent 9aafa32008
commit a4a04e6941
7 changed files with 173 additions and 169 deletions
@@ -1453,7 +1453,7 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
return return
# --- tolerance check --- # --- tolerance check ---
tolerance = 1e-5 #to provide a little wiggle room tolerance = 1e-5 # to provide a little wiggle room
if layer2_bases and (max(layer2_bases) - min(layer2_bases)) > tolerance: if layer2_bases and (max(layer2_bases) - min(layer2_bases)) > tolerance:
min_base = min(layer2_bases) min_base = min(layer2_bases)
max_base = max(layer2_bases) max_base = max(layer2_bases)
+24 -40
View File
@@ -137,16 +137,11 @@ class IfcTesterNamespace(socketio.AsyncNamespace):
ids_string = data.get("ids") ids_string = data.get("ids")
if not request_id: if not request_id:
await self.emit("error", { await self.emit("error", {"error": "No request ID provided"}, room=sid)
"error": "No request ID provided"
}, room=sid)
return return
if not ids_string: if not ids_string:
await self.emit("error", { await self.emit("error", {"id": request_id, "error": "No IDS XML string provided"}, room=sid)
"id": request_id,
"error": "No IDS XML string provided"
}, room=sid)
return return
print(f"Processing IDS audit request {request_id}") print(f"Processing IDS audit request {request_id}")
@@ -154,30 +149,23 @@ class IfcTesterNamespace(socketio.AsyncNamespace):
# Check if IFC is loaded in Bonsai # Check if IFC is loaded in Bonsai
ifc = tool.Ifc.get() ifc = tool.Ifc.get()
if not ifc: if not ifc:
await self.emit("error", { await self.emit(
"id": request_id, "error", {"id": request_id, "error": "No IFC model is currently loaded in Bonsai"}, room=sid
"error": "No IFC model is currently loaded in Bonsai" )
}, room=sid)
return return
# Parse IDS from string # Parse IDS from string
try: try:
ids = ifctester.ids.from_string(ids_string) ids = ifctester.ids.from_string(ids_string)
except Exception as e: except Exception as e:
await self.emit("error", { await self.emit("error", {"id": request_id, "error": f"Failed to parse IDS XML: {str(e)}"}, room=sid)
"id": request_id,
"error": f"Failed to parse IDS XML: {str(e)}"
}, room=sid)
return return
# Validate IFC against IDS # Validate IFC against IDS
try: try:
ids.validate(ifc) ids.validate(ifc)
except Exception as e: except Exception as e:
await self.emit("error", { await self.emit("error", {"id": request_id, "error": f"Validation failed: {str(e)}"}, room=sid)
"id": request_id,
"error": f"Validation failed: {str(e)}"
}, room=sid)
return return
# Generate reports # Generate reports
@@ -193,28 +181,21 @@ class IfcTesterNamespace(socketio.AsyncNamespace):
html_report = html_reporter.to_string() html_report = html_reporter.to_string()
# Send results back # Send results back
await self.emit("audit_result", { await self.emit(
"id": request_id, "audit_result", {"id": request_id, "json_report": json_report, "html_report": html_report}, room=sid
"json_report": json_report, )
"html_report": html_report
}, room=sid)
print(f"Successfully processed IDS audit request {request_id}") print(f"Successfully processed IDS audit request {request_id}")
except Exception as e: except Exception as e:
await self.emit("error", { await self.emit("error", {"id": request_id, "error": f"Failed to generate reports: {str(e)}"}, room=sid)
"id": request_id,
"error": f"Failed to generate reports: {str(e)}"
}, room=sid)
except Exception as e: except Exception as e:
print(f"Error processing audit request: {str(e)}") print(f"Error processing audit request: {str(e)}")
import traceback import traceback
print(traceback.format_exc()) print(traceback.format_exc())
await self.emit("error", { await self.emit("error", {"id": request_id, "error": f"Internal server error: {str(e)}"}, room=sid)
"id": request_id,
"error": f"Internal server error: {str(e)}"
}, room=sid)
async def on_ping(self, sid, data): async def on_ping(self, sid, data):
await self.emit("pong", {"timestamp": data.get("timestamp")}, room=sid) await self.emit("pong", {"timestamp": data.get("timestamp")}, room=sid)
@@ -328,7 +309,9 @@ class StartIfcTesterWebapp(bpy.types.Operator):
try: try:
import ifctester.webapp.serve import ifctester.webapp.serve
except ImportError: except ImportError:
self.report({"ERROR"}, "IfcTester webapp not available. Please ensure the latest version of ifctester is installed.") self.report(
{"ERROR"}, "IfcTester webapp not available. Please ensure the latest version of ifctester is installed."
)
return {"CANCELLED"} return {"CANCELLED"}
webapp_port = self.find_free_port() webapp_port = self.find_free_port()
@@ -355,11 +338,9 @@ class StartIfcTesterWebapp(bpy.types.Operator):
websocket_server_thread.start() websocket_server_thread.start()
# Start the Flask server as subprocess # Start the Flask server as subprocess
webapp_process = subprocess.Popen([ webapp_process = subprocess.Popen(
sys.executable, webapp_serve_path, [sys.executable, webapp_serve_path, "--host", "127.0.0.1", "--port", str(webapp_port)]
"--host", "127.0.0.1", )
"--port", str(webapp_port)
])
# Update properties # Update properties
props.webapp_server_port = webapp_port props.webapp_server_port = webapp_port
@@ -369,13 +350,16 @@ class StartIfcTesterWebapp(bpy.types.Operator):
# Wait a moment for servers to start, then open browser # Wait a moment for servers to start, then open browser
def delayed_open_browser(): def delayed_open_browser():
import time import time
time.sleep(1.5) time.sleep(1.5)
webbrowser.open(f"http://127.0.0.1:{webapp_port}?bonsai_server={websocket_port}") webbrowser.open(f"http://127.0.0.1:{webapp_port}?bonsai_server={websocket_port}")
browser_thread = threading.Thread(target=delayed_open_browser, daemon=True) browser_thread = threading.Thread(target=delayed_open_browser, daemon=True)
browser_thread.start() browser_thread.start()
self.report({"INFO"}, f"IfcTester webapp started at http://127.0.0.1:{webapp_port} (Websocket: {websocket_port})") self.report(
{"INFO"}, f"IfcTester webapp started at http://127.0.0.1:{webapp_port} (Websocket: {websocket_port})"
)
return {"FINISHED"} return {"FINISHED"}
except Exception as e: except Exception as e:
@@ -393,7 +377,7 @@ class StartIfcTesterWebapp(bpy.types.Operator):
def find_free_port(self): def find_free_port(self):
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind(('', 0)) s.bind(("", 0))
s.listen(1) s.listen(1)
port = s.getsockname()[1] port = s.getsockname()[1]
return port return port
+1 -1
View File
@@ -1,3 +1,3 @@
from .serve import app from .serve import app
__all__ = ['app'] __all__ = ["app"]
+56 -43
View File
@@ -9,6 +9,7 @@ from ifctester.ids import Ids, IdsXmlValidationError, get_schema
# https://github.com/buildingSMART/IDS/blob/9914d568c7ac037acd97e58a0d16e9f93c3e3416/Schema/ids.xsd#L232 # https://github.com/buildingSMART/IDS/blob/9914d568c7ac037acd97e58a0d16e9f93c3e3416/Schema/ids.xsd#L232
ifc_schemas = ["IFC2X3", "IFC4", "IFC4X3_ADD2"] ifc_schemas = ["IFC2X3", "IFC4", "IFC4X3_ADD2"]
def get_predefined_types_for_entity(schema_name, entity_name): def get_predefined_types_for_entity(schema_name, entity_name):
"""Get a list of predefined types for a given entity.""" """Get a list of predefined types for a given entity."""
@@ -44,6 +45,7 @@ def get_predefined_types_for_entity(schema_name, entity_name):
return [] return []
def get_all_entity_classes(schema_name): def get_all_entity_classes(schema_name):
"""Get all IFC entity classes in the given schema.""" """Get all IFC entity classes in the given schema."""
@@ -57,11 +59,17 @@ def get_all_entity_classes(schema_name):
entities.sort() entities.sort()
return entities return entities
def get_all_data_types(schema_name): def get_all_data_types(schema_name):
"""Get all data types in the given schema.""" """Get all data types in the given schema."""
schema = ifcopenshell.schema_by_name(schema_name) schema = ifcopenshell.schema_by_name(schema_name)
return {d.name(): ifcopenshell.util.attribute.get_primitive_type(d) for d in schema.declarations() if d.as_type_declaration()} return {
d.name(): ifcopenshell.util.attribute.get_primitive_type(d)
for d in schema.declarations()
if d.as_type_declaration()
}
def get_entity_attributes(schema_name, entity_name): def get_entity_attributes(schema_name, entity_name):
"""Get all attributes for a given entity.""" """Get all attributes for a given entity."""
@@ -80,14 +88,17 @@ def get_entity_attributes(schema_name, entity_name):
entity = entity.as_entity() entity = entity.as_entity()
attributes = [] attributes = []
for attr in entity.all_attributes(): for attr in entity.all_attributes():
attributes.append({ attributes.append(
"name": attr.name(), {
# "type": attr.type_of_attribute() # TODO Types of attribute "name": attr.name(),
}) # "type": attr.type_of_attribute() # TODO Types of attribute
}
)
return attributes return attributes
def get_applicable_psets(schema_name, entity_name, predefined_type = ""):
def get_applicable_psets(schema_name, entity_name, predefined_type=""):
"""Get all applicable property and quantity sets for a given entity.""" """Get all applicable property and quantity sets for a given entity."""
pset_qto = ifcopenshell.util.pset.PsetQto(schema_name) pset_qto = ifcopenshell.util.pset.PsetQto(schema_name)
@@ -95,6 +106,7 @@ def get_applicable_psets(schema_name, entity_name, predefined_type = ""):
return pset_names return pset_names
def get_all_psets(schema_name): def get_all_psets(schema_name):
"""Get all property sets and quantity sets defined in an IFC schema""" """Get all property sets and quantity sets defined in an IFC schema"""
@@ -121,7 +133,7 @@ def get_all_psets(schema_name):
else: else:
prop_info["type"] = str(prop_template.TemplateType) prop_info["type"] = str(prop_template.TemplateType)
elif prop_template.is_a("IfcComplexPropertyTemplate"): elif prop_template.is_a("IfcComplexPropertyTemplate"):
prop_info["type"] = None # Complex properties are not supported prop_info["type"] = None # Complex properties are not supported
else: else:
prop_info["type"] = None prop_info["type"] = None
@@ -130,51 +142,52 @@ def get_all_psets(schema_name):
return result return result
def get_material_categories(): def get_material_categories():
return ['concrete', 'steel', 'aluminium', 'block', 'brick', 'stone', 'wood', 'glass', 'gypsum', 'plastic', 'earth'] return ["concrete", "steel", "aluminium", "block", "brick", "stone", "wood", "glass", "gypsum", "plastic", "earth"]
def get_standard_classification_systems(): def get_standard_classification_systems():
return { return {
'BB/SfB (3/4 cijfers)': {'source': 'Regie der Gebouwen', 'tokens': ['.']}, "BB/SfB (3/4 cijfers)": {"source": "Regie der Gebouwen", "tokens": ["."]},
'BIMTypeCode': {'source': 'BIMStockholm', 'tokens': None}, "BIMTypeCode": {"source": "BIMStockholm", "tokens": None},
'Common Arrangement of Work Sections (CAWS)': {'source': 'NBS', 'tokens': ['/']}, "Common Arrangement of Work Sections (CAWS)": {"source": "NBS", "tokens": ["/"]},
'CBI Classification - Level 2': {'source': 'Masterspec', 'tokens': None}, "CBI Classification - Level 2": {"source": "Masterspec", "tokens": None},
'CBI Classification - Level 4': {'source': 'Masterspec', 'tokens': None}, "CBI Classification - Level 4": {"source": "Masterspec", "tokens": None},
'Rumsfunktionskoder CC001 - 001': {'source': 'BIMAlliance', 'tokens': ['-']}, "Rumsfunktionskoder CC001 - 001": {"source": "BIMAlliance", "tokens": ["-"]},
'CCS': {'source': 'Molio', 'tokens': None}, "CCS": {"source": "Molio", "tokens": None},
'CCTB': {'source': 'CCT-Bâtiments', 'tokens': ['.']}, "CCTB": {"source": "CCT-Bâtiments", "tokens": ["."]},
'Funktionskoder Regionservice CD001 - 001': {'source': 'BIMAlliance', 'tokens': None}, "Funktionskoder Regionservice CD001 - 001": {"source": "BIMAlliance", "tokens": None},
'Rumsfunktion Blekinge CD002 - 001': {'source': 'BIMAlliance', 'tokens': None}, "Rumsfunktion Blekinge CD002 - 001": {"source": "BIMAlliance", "tokens": None},
'EcoQuaestor Codetabel': {'source': 'EcoQuaestor', 'tokens': ['.', '-']}, "EcoQuaestor Codetabel": {"source": "EcoQuaestor", "tokens": [".", "-"]},
'GuBIMclass CA': {'source': 'GuBIMClass', 'tokens': ['.']}, "GuBIMclass CA": {"source": "GuBIMClass", "tokens": ["."]},
'GuBIMclass ES': {'source': 'GuBIMClass', 'tokens': ['.']}, "GuBIMclass ES": {"source": "GuBIMClass", "tokens": ["."]},
'MasterFormat': {'source': 'CSI', 'tokens': [' ', '.']}, "MasterFormat": {"source": "CSI", "tokens": [" ", "."]},
'NATSPEC Worksections': {'source': 'NATSPEC', 'tokens': None}, "NATSPEC Worksections": {"source": "NATSPEC", "tokens": None},
'NBS Create': {'source': 'NBS', 'tokens': ['_', '/']}, "NBS Create": {"source": "NBS", "tokens": ["_", "/"]},
'NL/SfB (4 cijfers)': {'source': 'BIMLoket', 'tokens': ['.']}, "NL/SfB (4 cijfers)": {"source": "BIMLoket", "tokens": ["."]},
'NS 3451 - Bygningsdelstabell': {'source': 'Standard Norge', 'tokens': None}, "NS 3451 - Bygningsdelstabell": {"source": "Standard Norge", "tokens": None},
'OmniClass': {'source': 'OmniClass', 'tokens': ['-', ' ']}, "OmniClass": {"source": "OmniClass", "tokens": ["-", " "]},
'ÖNORM 6241-2': {'source': 'freeBIM 2', 'tokens': None}, "ÖNORM 6241-2": {"source": "freeBIM 2", "tokens": None},
'RICS NRM1': {'source': 'RICS', 'tokens': ['.']}, "RICS NRM1": {"source": "RICS", "tokens": ["."]},
'RICS NRM3': {'source': 'RICS', 'tokens': ['.']}, "RICS NRM3": {"source": "RICS", "tokens": ["."]},
'SFG20': {'source': 'SFG20', 'tokens': ['-']}, "SFG20": {"source": "SFG20", "tokens": ["-"]},
'SINAPI': {'source': 'Caixa', 'tokens': ['/']}, "SINAPI": {"source": "Caixa", "tokens": ["/"]},
'STABU-Element': {'source': 'STABU', 'tokens': ['.']}, "STABU-Element": {"source": "STABU", "tokens": ["."]},
'TALO 2000 Building Component Classification': {'source': 'Rakennustieto', 'tokens': ['.']}, "TALO 2000 Building Component Classification": {"source": "Rakennustieto", "tokens": ["."]},
'TALO 2000 Hankenimikkeistö': {'source': 'Rakennustieto', 'tokens': ['.']}, "TALO 2000 Hankenimikkeistö": {"source": "Rakennustieto", "tokens": ["."]},
'Uniclass': {'source': 'RIBA Enterprises Ltd', 'tokens': ['_']}, "Uniclass": {"source": "RIBA Enterprises Ltd", "tokens": ["_"]},
'Uniclass 2015': {'source': 'RIBA Enterprises Ltd', 'tokens': ['_']}, "Uniclass 2015": {"source": "RIBA Enterprises Ltd", "tokens": ["_"]},
'UniFormat': {'source': 'UniFormat', 'tokens': ['.']}, "UniFormat": {"source": "UniFormat", "tokens": ["."]},
'Uniformat': {'source': 'UniFormat', 'tokens': ['.']}, "Uniformat": {"source": "UniFormat", "tokens": ["."]},
'VMSW': {'source': 'VMSW', 'tokens': ['.']} "VMSW": {"source": "VMSW", "tokens": ["."]},
} }
def ids_from_xml_string(xml: str, validate: bool = False) -> Ids: def ids_from_xml_string(xml: str, validate: bool = False) -> Ids:
try: try:
decode = get_schema().decode( decode = get_schema().decode(
xml, strip_namespaces=True, namespaces={ xml, strip_namespaces=True, namespaces={"": "http://standards.buildingsmart.org/IDS"}
"": "http://standards.buildingsmart.org/IDS"
}
) )
except XMLSchemaValidationError as e: except XMLSchemaValidationError as e:
raise IdsXmlValidationError(e, "Provided XML appears to be invalid. See details above.") raise IdsXmlValidationError(e, "Provided XML appears to be invalid. See details above.")
+21 -14
View File
@@ -22,10 +22,11 @@ from flask import Flask, send_from_directory, send_file
app = Flask(__name__) app = Flask(__name__)
def get_static_folder(): def get_static_folder():
base_dir = os.path.dirname(__file__) base_dir = os.path.dirname(__file__)
dist_dir = os.path.join(base_dir, 'dist') dist_dir = os.path.join(base_dir, "dist")
www_dir = os.path.join(base_dir, 'www') www_dir = os.path.join(base_dir, "www")
if os.path.exists(dist_dir) and os.path.isdir(dist_dir): if os.path.exists(dist_dir) and os.path.isdir(dist_dir):
return dist_dir return dist_dir
@@ -34,30 +35,36 @@ def get_static_folder():
else: else:
return dist_dir return dist_dir
STATIC_FOLDER = get_static_folder() STATIC_FOLDER = get_static_folder()
@app.route('/')
def index():
return send_file(os.path.join(STATIC_FOLDER, 'index.html'))
@app.route('/<path:filename>') @app.route("/")
def index():
return send_file(os.path.join(STATIC_FOLDER, "index.html"))
@app.route("/<path:filename>")
def static_files(filename): def static_files(filename):
return send_from_directory(STATIC_FOLDER, filename) return send_from_directory(STATIC_FOLDER, filename)
@app.route('/assets/<path:filename>')
@app.route("/assets/<path:filename>")
def assets(filename): def assets(filename):
return send_from_directory(os.path.join(STATIC_FOLDER, 'assets'), filename) return send_from_directory(os.path.join(STATIC_FOLDER, "assets"), filename)
@app.errorhandler(404) @app.errorhandler(404)
def not_found(error): def not_found(error):
return send_file(os.path.join(STATIC_FOLDER, 'index.html')) return send_file(os.path.join(STATIC_FOLDER, "index.html"))
if __name__ == "__main__": if __name__ == "__main__":
parser = argparse.ArgumentParser(description='Start IfcTester webapp') parser = argparse.ArgumentParser(description="Start IfcTester webapp")
parser.add_argument('--host', default='127.0.0.1', help='Host to bind to (default: 127.0.0.1)') parser.add_argument("--host", default="127.0.0.1", help="Host to bind to (default: 127.0.0.1)")
parser.add_argument('--port', type=int, default=5000, help='Port to bind to (default: 5000)') parser.add_argument("--port", type=int, default=5000, help="Port to bind to (default: 5000)")
parser.add_argument('--debug', action='store_true', help='Enable debug mode') parser.add_argument("--debug", action="store_true", help="Enable debug mode")
parser.add_argument('--dist-dir', default=STATIC_FOLDER, help='Directory containing built files') parser.add_argument("--dist-dir", default=STATIC_FOLDER, help="Directory containing built files")
args = parser.parse_args() args = parser.parse_args()