# ===================================================== # PVC/CPVC FITTING GENERATOR FOR BLENDER/BONSAI # GENERADOR DE ACCESORIOS PVC/CPVC PARA BLENDER/BONSAI # ===================================================== # Version: V0.9 # Author: Javier Fonseca # Email: jafocol@gmail.com # Web: www.rojasfonseca.com # ===================================================== # # --- ENGLISH DOCUMENTATION --- # This script generates parametric PVC SCH40 and CPVC CTS # pipe fittings as IFC objects inside Blender/Bonsai. # # SYSTEMS SUPPORTED: # AF (Cold Water): PVC-U SCH40, NTC 382/NTC 1339, blue # AC (Hot Water): CPVC CTS SDR11, NTC 1062/ASTM D2846, cream # Both use the same SCH40 geometry for simplicity. # # REQUIREMENTS: # 1. Blender 4.x or 5.x with Bonsai add-on installed # 2. An IFC project must be created and SAVED with a filename # BEFORE running this script (File > Save As IFC) # 3. The IFC file must have a valid path (not "Unsaved") # # HOW TO USE: # 1. Open your IFC project in Blender/Bonsai # 2. Run this script (Alt+P or paste in Scripting tab) # 3. Open the N-panel (press N) > "Accesorios" / "Fittings" tab # 4. Select the system: AF (cold) or AC (hot) # 5. Select the diameters and special equipment you need # 6. Click "Create Fittings" button # 7. Fittings already in IFC will be SKIPPED # # IFC MATERIALS ASSIGNED: # AF: IfcMaterial "PVC-U SCH40" Category="Plastic" # Pset: NTC 382, NTC 1339, NTC 576, 150 PSI @23C # AC: IfcMaterial "CPVC CTS SDR11" Category="Plastic" # Pset: NTC 1062, ASTM D2846, 100 PSI @82C # # PROPERTY SETS (Psets) ADDED: # - Pset_PipeFittingTypeCommon: PressureRating, Standard, Material # - Pset_PipeFittingTypeCommon: PressureRating, NominalDiameter # - Pset_ValveTypeCommon: PressureRating (for valves) # - IfcMaterial with Name and Category # # PORT FLOW DIRECTION CONVENTION: # - Symmetric fittings: SOURCEANDSINK (bidirectional) # - Valve: Inlet=SINK, Outlet=SOURCE # - Water Heater: Gas/Water in=SINK, Hot out/Flue=SOURCE # - Washing Machine Box: Water in=SINK, Drain=SOURCE # # --- DOCUMENTACION EN ESPAÑOL --- # Este script genera accesorios parametricos PVC SCH40 y CPVC CTS # como objetos IFC dentro de Blender/Bonsai. # # SISTEMAS SOPORTADOS: # AF (Agua Fria): PVC-U SCH40, NTC 382/NTC 1339, azul # AC (Agua Caliente): CPVC CTS SDR11, NTC 1062/ASTM D2846, crema # Ambos usan la misma geometria SCH40 por simplicidad. # # REQUISITOS: # 1. Blender 4.x o 5.x con add-on Bonsai instalado # 2. Crear y GUARDAR un proyecto IFC con nombre # ANTES de ejecutar (File > Save As IFC) # # MATERIALES IFC ASIGNADOS: # AF: IfcMaterial "PVC-U SCH40" Categoria="Plastic" # Pset: NTC 382, NTC 1339, NTC 576, 150 PSI @23C # AC: IfcMaterial "CPVC CTS SDR11" Categoria="Plastic" # Pset: NTC 1062, ASTM D2846, 100 PSI @82C # # PROPERTY SETS (Psets) AGREGADOS: # - Pset_PipeFittingTypeCommon: Presion, Norma, Material # - Pset_PipeFittingTypeCommon: Presion, DiametroNominal # - Pset_ValveTypeCommon: Presion (para valvulas) # - IfcMaterial con Nombre y Categoria # # ===================================================== # VERSION HISTORY / HISTORIAL DE VERSIONES: # V0.1 - Base fittings / Accesorios base # V0.2 - Cap, Reducers, Washing Machine Box, UI Panel # V0.3 - Cap dome, box fix, segments control, credits # V0.4 - Cap simplified, robust IFC save, compact credits # V0.5 - Water Heater 12Lt, IfcBoilerType WATER # V0.6 - Independent functions, checkbox validation # V0.7 - Occurrence protection: already_exists() # V0.8 - Bilingual UI (ES/EN), FlowDirection fix, # try/except on booleans, code documentation # V0.9 - CPVC Hot Water system (AC), IfcMaterial assignment, # Pset properties (manufacturer, pressure, standard), # System selector [AF][AC], cream color + red handle, # DOMESTICHOTWATER port SystemType for AC # ===================================================== import bpy import math import mathutils import numpy as np import ifcopenshell import ifcopenshell.api from bonsai.bim.ifc import IfcStore # ===================================================== # TABLAS SCH40 # ===================================================== I = 0.0254 # pulgadas → metros C = 0.01 # cm → metros codo_data = { "0.5": {"OD_pipe":0.02134,"OD_fitting":0.027, "H":1.25*I, "G":0.50*I, "desc":"1/2\""}, "0.75": {"OD_pipe":0.02667,"OD_fitting":0.033, "H":1.5625*I, "G":0.5625*I, "desc":"3/4\""}, "1.0": {"OD_pipe":0.03340,"OD_fitting":0.041, "H":1.5625*I, "G":0.6875*I, "desc":"1\""}, "1.25": {"OD_pipe":0.04216,"OD_fitting":0.052, "H":2.125*I, "G":0.875*I, "desc":"1-1/4\""}, "1.5": {"OD_pipe":0.04826,"OD_fitting":0.060, "H":2.3125*I, "G":1.0*I, "desc":"1-1/2\""}, "2.0": {"OD_pipe":0.06033,"OD_fitting":0.073, "H":2.40625*I, "G":1.25*I, "desc":"2\""}, } tee_data = { "0.5": {"OD_pipe":0.02134,"OD_fitting":0.027, "L":2.5*I, "H":1.25*I, "G":0.5*I, "desc":"1/2\""}, "0.75": {"OD_pipe":0.02667,"OD_fitting":0.033, "L":3.0*I, "H":1.5*I, "G":0.5625*I, "desc":"3/4\""}, "1.0": {"OD_pipe":0.03340,"OD_fitting":0.041, "L":3.125*I, "H":1.5625*I, "G":0.6875*I, "desc":"1\""}, "1.25": {"OD_pipe":0.04216,"OD_fitting":0.052, "L":4.25*I, "H":2.125*I, "G":0.875*I, "desc":"1-1/4\""}, "1.5": {"OD_pipe":0.04826,"OD_fitting":0.060, "L":4.625*I, "H":2.3125*I, "G":1.0*I, "desc":"1-1/2\""}, "2.0": {"OD_pipe":0.06033,"OD_fitting":0.073, "L":4.8125*I, "H":2.40625*I, "G":1.25*I, "desc":"2\""}, } term_data = codo_data adapt_data = codo_data # Registro agua fria SOLDAR: A=Ancho(X), B=Largo/flujo(Y), C=Alto(Z) reg_data = { "0.5": {"OD_f":0.027, "A":3.0*C, "B":8.0*C, "C":6.0*C, "G":0.50*I, "desc":"1/2\""}, "0.75": {"OD_f":0.033, "A":4.5*C, "B":9.0*C, "C":7.5*C, "G":0.5625*I,"desc":"3/4\""}, "1.0": {"OD_f":0.041, "A":5.0*C, "B":10.0*C, "C":8.0*C, "G":0.6875*I,"desc":"1\""}, "1.25": {"OD_f":0.052, "A":6.4*C, "B":10.7*C, "C":9.6*C, "G":0.875*I, "desc":"1-1/4\""}, "1.5": {"OD_f":0.060, "A":7.2*C, "B":13.1*C, "C":10.6*C, "G":1.0*I, "desc":"1-1/2\""}, "2.0": {"OD_f":0.073, "A":8.4*C, "B":13.0*C, "C":12.0*C, "G":1.25*I, "desc":"2\""}, } # Cap (Tapon) PVC SCH40: OD_fitting, H=profundidad total, G=profundidad socket cap_data = { "0.5": {"OD_fitting":0.027, "H":0.875*I, "G":0.50*I, "wall":0.003, "desc":"1/2\""}, "0.75": {"OD_fitting":0.033, "H":1.0*I, "G":0.5625*I, "wall":0.004, "desc":"3/4\""}, "1.0": {"OD_fitting":0.041, "H":1.125*I, "G":0.6875*I, "wall":0.005, "desc":"1\""}, "1.25": {"OD_fitting":0.052, "H":1.375*I, "G":0.875*I, "wall":0.005, "desc":"1-1/4\""}, "1.5": {"OD_fitting":0.060, "H":1.5*I, "G":1.0*I, "wall":0.006, "desc":"1-1/2\""}, "2.0": {"OD_fitting":0.073, "H":1.625*I, "G":1.25*I, "wall":0.007, "desc":"2\""}, } # Reduccion (Bushing) PVC SCH40: SxS (socket x socket) # Cada entrada: OD grande, OD chico, longitud total, profundidad socket grande, desc red_data = { "1.0x0.75": {"OD_big":0.041, "OD_small":0.033, "L":1.5*I, "G_big":0.6875*I, "G_small":0.5625*I, "desc":"1\" x 3/4\""}, "1.0x0.5": {"OD_big":0.041, "OD_small":0.027, "L":1.5*I, "G_big":0.6875*I, "G_small":0.50*I, "desc":"1\" x 1/2\""}, "0.75x0.5": {"OD_big":0.033, "OD_small":0.027, "L":1.25*I, "G_big":0.5625*I, "G_small":0.50*I, "desc":"3/4\" x 1/2\""}, } # Caja para lavadora plastica Colombia: 23x15x8 cm # 3 puertos por abajo: AF (1/2"), AC (1/2"), Sanitaria (1-1/2") # Tipo: Terminal Sanitaria (IfcSanitaryTerminalType - válido IFC4) caja_lav_data = { "standard": { "ancho": 23.0*C, # X (23 cm) "alto": 15.0*C, # Z (15 cm) "prof": 8.0*C, # Y (8 cm profundidad empotrar) "wall": 0.3*C, # espesor pared caja "OD_af": 0.027, # 1/2" agua fria "OD_ac": 0.027, # 1/2" agua caliente "OD_san": 0.060, # 1-1/2" sanitaria (drenaje) "G_af": 0.50*I, # profundidad socket AF "G_ac": 0.50*I, # profundidad socket AC "G_san": 1.0*I, # profundidad socket sanitaria "desc": "23x15x8cm 1/2\" AF + 1/2\" AC + 1-1/2\" San", }, } # Calentador de paso 12 Lt (tipo Challenger) # Dimensiones del plano: 35x18.5x61 cm # Puertos por abajo: Entrada Gas (1/2"), Salida Agua (1/2"), Entrada Agua (1/2") # Chimenea arriba: diametro ~11cm calentador_data = { "12lt": { "ancho": 35.0*C, # X (35 cm frontal) "prof": 18.5*C, # Y (18.5 cm profundidad) "alto": 61.0*C, # Z (61 cm alto) "chim_d": 11.0*C, # diametro chimenea "chim_h": 5.0*C, # altura chimenea sobre el cuerpo "OD_gas": 0.027, # 1/2" gas "OD_sal": 0.027, # 1/2" salida agua caliente "OD_ent": 0.027, # 1/2" entrada agua fria "G_port": 0.50*I, # profundidad niple # Posiciones X de puertos (vista inferior, segun plano) # Gas izquierda, Salida centro, Entrada derecha "px_gas": -10.0*C, # -10cm desde centro "px_sal": 0.0*C, # centro "px_ent": 10.0*C, # +10cm desde centro "desc": "Calentador 12Lt 35x18.5x61cm", }, } AVAILABLE_SIZES = ["0.5", "0.75", "1.0", "1.25", "1.5", "2.0"] DEFAULT_SEGMENTS = 16 PREFIX_AF = ["CodoPVC_","TePVC_","TermMachoPVC_","AdaptHembraPVC_","RegistroAF_", "CapPVC_","RedPVC_","CajaLavadora_","Calentador_"] PREFIX_AC = ["CodoCPVC_","TeCPVC_","TermMachoCPVC_","AdaptHembraCPVC_","RegistroAC_", "CapCPVC_","RedCPVC_"] # Keep old PREFIX for backward compat PREFIX = PREFIX_AF # ===================================================== # SYSTEM CONFIGURATION / CONFIGURACION DE SISTEMAS # ===================================================== SYSTEM_CONFIG = { "AF": { "mat_name": "PVC-U SCH40", "mat_category": "Plastic", "standard": "NTC 382 / NTC 1339 / NTC 576", "pressure": "150 PSI @ 23C (600 kPa)", "system_type": "DOMESTICCOLDWATER", "prefix_codo": "CodoPVC_", "prefix_te": "TePVC_", "prefix_term": "TermMachoPVC_", "prefix_adapt": "AdaptHembraPVC_", "prefix_reg": "RegistroAF_", "prefix_cap": "CapPVC_", "prefix_red": "RedPVC_", "suffix": "_SCH40", "desc_mat": "PVC SCH40", "desc_reg": "Registro Agua Fria PVC SCH40", }, "AC": { "mat_name": "CPVC CTS SDR11", "mat_category": "Plastic", "standard": "NTC 1062 / ASTM D2846", "pressure": "100 PSI @ 82C (689 kPa)", "system_type": "DOMESTICHOTWATER", "prefix_codo": "CodoCPVC_", "prefix_te": "TeCPVC_", "prefix_term": "TermMachoCPVC_", "prefix_adapt": "AdaptHembraCPVC_", "prefix_reg": "RegistroAC_", "prefix_cap": "CapCPVC_", "prefix_red": "RedCPVC_", "suffix": "_CTS", "desc_mat": "CPVC CTS SDR11", "desc_reg": "Registro Agua Caliente CPVC CTS", }, } # ===================================================== # MATERIALES / MATERIALS # ===================================================== def get_white_material(): """PVC white color for cold water fittings (Colombian standard).""" name = "PVC_AguaFria_Blanco" mat = bpy.data.materials.get(name) if mat is None: mat = bpy.data.materials.new(name=name) mat.use_nodes = True bsdf = mat.node_tree.nodes.get("Principled BSDF") if bsdf: bsdf.inputs["Base Color"].default_value = (0.92, 0.92, 0.90, 1.0) bsdf.inputs["Roughness"].default_value = 0.3 return mat def get_dark_material(): name = "Calentador_Negro" mat = bpy.data.materials.get(name) if mat is None: mat = bpy.data.materials.new(name=name) mat.use_nodes = True bsdf = mat.node_tree.nodes.get("Principled BSDF") if bsdf: bsdf.inputs["Base Color"].default_value = (0.05, 0.05, 0.05, 1.0) bsdf.inputs["Roughness"].default_value = 0.15 bsdf.inputs["Metallic"].default_value = 0.1 return mat def get_cream_material(): """CPVC cream/beige color for hot water fittings.""" name = "CPVC_AguaCaliente_Crema" mat = bpy.data.materials.get(name) if mat is None: mat = bpy.data.materials.new(name=name) mat.use_nodes = True bsdf = mat.node_tree.nodes.get("Principled BSDF") if bsdf: bsdf.inputs["Base Color"].default_value = (0.85, 0.78, 0.65, 1.0) bsdf.inputs["Roughness"].default_value = 0.35 return mat def get_blue_handle_material(): """Blue handle material for PVC AF ball valves.""" name = "PVC_Manija_Azul" mat = bpy.data.materials.get(name) if mat is None: mat = bpy.data.materials.new(name=name) mat.use_nodes = True bsdf = mat.node_tree.nodes.get("Principled BSDF") if bsdf: bsdf.inputs["Base Color"].default_value = (0.0, 0.35, 0.85, 1.0) bsdf.inputs["Roughness"].default_value = 0.35 return mat def get_red_handle_material(): """Red handle material for CPVC AC ball valves.""" name = "CPVC_Manija_Roja" mat = bpy.data.materials.get(name) if mat is None: mat = bpy.data.materials.new(name=name) mat.use_nodes = True bsdf = mat.node_tree.nodes.get("Principled BSDF") if bsdf: bsdf.inputs["Base Color"].default_value = (0.75, 0.08, 0.08, 1.0) bsdf.inputs["Roughness"].default_value = 0.4 return mat def apply_white(obj): mat = get_white_material() if obj.data.materials: obj.data.materials[0] = mat else: obj.data.materials.append(mat) def apply_dark(obj): mat = get_dark_material() if obj.data.materials: obj.data.materials[0] = mat else: obj.data.materials.append(mat) def apply_cream(obj): mat = get_cream_material() if obj.data.materials: obj.data.materials[0] = mat else: obj.data.materials.append(mat) def apply_system_color(obj, sys="AF"): """Apply the correct Blender material based on system.""" if sys == "AC": apply_cream(obj) else: apply_white(obj) # ===================================================== # IFC MATERIAL & PSET ASSIGNMENT # ===================================================== def assign_ifc_material(ifc, element, sys="AF"): """Create or find IfcMaterial and assign it to the element.""" cfg = SYSTEM_CONFIG[sys] mat_name = cfg["mat_name"] # Check if material already exists existing = None for m in ifc.by_type("IfcMaterial"): if m.Name == mat_name: existing = m break if existing is None: existing = ifcopenshell.api.run("material.add_material", ifc, name=mat_name, category=cfg["mat_category"]) try: ifcopenshell.api.run("material.assign_material", ifc, products=[element], material=existing) print(" Material [" + mat_name + "] asignado OK") except Exception as e: print(" WARN material: " + str(e)) def assign_pset_fitting(ifc, element, sys="AF", nominal_dn=""): """Add Pset with standard, pressure and diameter info.""" cfg = SYSTEM_CONFIG[sys] try: pset = ifcopenshell.api.run("pset.add_pset", ifc, product=element, name="Pset_PipeFittingTypeCommon") ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={ "PressureRating": cfg["pressure"], "NominalDiameter": nominal_dn, "Standard": cfg["standard"], "Material": cfg["mat_name"], }) print(" Pset_PipeFitting OK") except Exception as e: print(" WARN pset fitting: " + str(e)) def assign_pset_valve(ifc, element, sys="AF", nominal_dn=""): """Add Pset for valves with standard and pressure.""" cfg = SYSTEM_CONFIG[sys] try: pset = ifcopenshell.api.run("pset.add_pset", ifc, product=element, name="Pset_ValveTypeCommon") ifcopenshell.api.run("pset.edit_pset", ifc, pset=pset, properties={ "PressureRating": cfg["pressure"], "NominalDiameter": nominal_dn, "ValvePattern": "SINGLEPORT", "ValveMechanism": "BALL", "Standard": cfg["standard"], "Material": cfg["mat_name"], }) print(" Pset_Valve OK") except Exception as e: print(" WARN pset valve: " + str(e)) # ===================================================== # HELPERS GEOMETRIA # ===================================================== def apply_transforms(obj): bpy.context.view_layer.objects.active = obj obj.select_set(True) bpy.ops.object.transform_apply(location=False, rotation=True, scale=True) def bu(target, tool_obj): try: m = target.modifiers.new("u", 'BOOLEAN') m.operation = 'UNION' m.object = tool_obj bpy.context.view_layer.objects.active = target bpy.ops.object.modifier_apply(modifier="u") bpy.data.objects.remove(tool_obj, do_unlink=True) except Exception as e: print(" WARN boolean union: " + str(e)) try: bpy.data.objects.remove(tool_obj, do_unlink=True) except: pass def bi(target, tool_obj): try: m = target.modifiers.new("i", 'BOOLEAN') m.operation = 'INTERSECT' m.object = tool_obj bpy.context.view_layer.objects.active = target bpy.ops.object.modifier_apply(modifier="i") bpy.data.objects.remove(tool_obj, do_unlink=True) except Exception as e: print(" WARN boolean intersect: " + str(e)) try: bpy.data.objects.remove(tool_obj, do_unlink=True) except: pass def set_origin(obj, world_loc): bpy.context.scene.cursor.location = world_loc bpy.context.view_layer.objects.active = obj obj.select_set(True) bpy.ops.object.origin_set(type='ORIGIN_CURSOR') bpy.ops.object.mode_set(mode='OBJECT') bpy.context.view_layer.update() def deselect_all(): bpy.ops.object.select_all(action='DESELECT') # ===================================================== # HELPERS IFC # ===================================================== def port_mat(loc, out_dir): z = mathutils.Vector(out_dir).normalized() ref = mathutils.Vector((0,0,1)) if abs(z.z) < 0.9 else mathutils.Vector((1,0,0)) x = ref.cross(z).normalized() y = z.cross(x).normalized() return np.array([ [x.x, y.x, z.x, loc[0]], [x.y, y.y, z.y, loc[1]], [x.z, y.z, z.z, loc[2]], [0, 0, 0, 1 ] ], dtype=float) def add_port(ifc, element, name, loc, direction, flow="NOTDEFINED", sys="AF"): port = ifcopenshell.api.run("system.add_port", ifc, element=element) port.Name = name port.FlowDirection = flow port.SystemType = SYSTEM_CONFIG.get(sys, SYSTEM_CONFIG["AF"])["system_type"] try: ifcopenshell.api.run("geometry.edit_object_placement", ifc, product=port, matrix=port_mat(loc, direction)) print(" Puerto [" + name + "] OK (" + port.SystemType + ")") except Exception as e: print(" WARN puerto: " + str(e)) return port def assign_ifc(obj, ifc_class, predef, name, desc_text, obj_type=None): deselect_all() obj.select_set(True) bpy.context.view_layer.objects.active = obj try: bpy.ops.bim.assign_class(ifc_class=ifc_class, predefined_type=predef) except TypeError: bpy.ops.bim.assign_class(ifc_class=ifc_class) ifc = IfcStore.get_file() ifc_id = obj.BIMObjectProperties.ifc_definition_id el = ifc.by_id(ifc_id) el.Name = name el.Description = desc_text try: el.PredefinedType = predef except: pass if obj_type: try: el.ObjectType = obj_type except: pass return ifc, el # ===================================================== # VERIFICAR SI UN TYPE YA EXISTE EN EL IFC # Si ya existe, NO se toca — protege ocurrencias ya colocadas # ===================================================== def type_exists_in_ifc(name): """Retorna True si ya existe un TypeProduct con ese Name en el IFC.""" ifc = IfcStore.get_file() if not ifc: return False for t in ifc.by_type("IfcTypeProduct"): if hasattr(t, "Name") and t.Name == name: print(" [SKIP] '" + name + "' ya existe en IFC (id=" + str(t.id()) + ") - no se sobrescribe") return True return False def obj_exists_in_blender(name): """Retorna True si ya existe un objeto Blender con ese nombre y tiene IFC valido.""" obj = bpy.data.objects.get(name) if obj is None: return False ifc_id = obj.BIMObjectProperties.ifc_definition_id if ifc_id and ifc_id > 0: ifc = IfcStore.get_file() if ifc: try: ifc.by_id(ifc_id) return True except: return False return False def already_exists(name): """Retorna True si el accesorio ya existe en IFC o Blender con datos validos.""" return type_exists_in_ifc(name) or obj_exists_in_blender(name) # ===================================================== # FUNCION PRINCIPAL: genera todos los accesorios para un size # Solo crea los que NO existen aun en el IFC # ===================================================== def generate_for_size(size, lados=DEFAULT_SEGMENTS, sys="AF"): cfg = SYSTEM_CONFIG[sys] # Solo limpiar IDs invalidos (huerfanos de runs fallidos anteriores) ifc = IfcStore.get_file() if ifc: for obj in list(bpy.data.objects): ifc_id = obj.BIMObjectProperties.ifc_definition_id if ifc_id and ifc_id > 0: try: ifc.by_id(ifc_id) except Exception: print("Limpieza - eliminando ID invalido: " + obj.name + " #" + str(ifc_id)) try: bpy.data.objects.remove(obj, do_unlink=True) except: pass # ===================================================== # ACCESORIO 1: CODO 90° # ===================================================== dc = codo_data[size] radio_c = dc["OD_fitting"] / 2 L_c = dc["H"] - dc["G"] name_codo = cfg["prefix_codo"] + size + cfg["suffix"] if not already_exists(name_codo): print("\n--- " + name_codo + " ---") bpy.ops.mesh.primitive_torus_add( major_radius=radio_c, minor_radius=radio_c, major_segments=lados, minor_segments=lados, location=(0,0,0)) torus = bpy.context.object cut = dc["OD_fitting"] * 3 bpy.ops.mesh.primitive_cube_add(size=cut, location=(cut/2, cut/2, 0)) bi(torus, bpy.context.object) bpy.ops.mesh.primitive_cylinder_add( vertices=lados, radius=radio_c, depth=L_c, location=(-L_c/2, radio_c, 0), rotation=(0, math.radians(90), 0)) bu(torus, bpy.context.object) bpy.ops.mesh.primitive_cylinder_add( vertices=lados, radius=radio_c, depth=L_c, location=(radio_c, -L_c/2, 0), rotation=(math.radians(90), 0, 0)) bu(torus, bpy.context.object) torus.name = name_codo set_origin(torus, (radio_c, radio_c, 0)) apply_transforms(torus) apply_system_color(torus, sys) ifc, el = assign_ifc(torus, "IfcPipeFittingType", "BEND", name_codo, "Codo 90 " + cfg["desc_mat"] + " " + dc["desc"] + " sin rosca") add_port(ifc, el, "Boca_X", (-L_c - radio_c, 0, 0), (-1, 0, 0), "SOURCEANDSINK", sys) add_port(ifc, el, "Boca_Y", (0, -L_c - radio_c, 0), ( 0,-1, 0), "SOURCEANDSINK", sys) assign_ifc_material(ifc, el, sys) assign_pset_fitting(ifc, el, sys, dc["desc"]) bpy.ops.bim.update_representation() print("OK " + name_codo) # ===================================================== # ACCESORIO 2: TE PVC 2400 SxSxS # ===================================================== dt = tee_data[size] radio_t = dt["OD_fitting"] / 2 L_t = dt["L"] H_t = dt["H"] name_te = cfg["prefix_te"] + size + cfg["suffix"] if not already_exists(name_te): print("\n--- " + name_te + " ---") bpy.ops.mesh.primitive_cylinder_add( vertices=lados, radius=radio_t, depth=L_t, location=(0, 0, 0)) run_cyl = bpy.context.object bpy.ops.mesh.primitive_cylinder_add( vertices=lados, radius=radio_t, depth=H_t, location=(0, H_t/2, 0), rotation=(math.radians(90), 0, 0)) bu(run_cyl, bpy.context.object) run_cyl.name = name_te set_origin(run_cyl, (0, 0, 0)) apply_transforms(run_cyl) apply_system_color(run_cyl, sys) ifc, el = assign_ifc(run_cyl, "IfcPipeFittingType", "JUNCTION", name_te, "Te " + cfg["desc_mat"] + " " + dt["desc"] + " SxSxS Part 2400 sin rosca") add_port(ifc, el, "Run_A", (0, 0, -L_t/2), (0, 0,-1), "SOURCEANDSINK", sys) add_port(ifc, el, "Run_B", (0, 0, L_t/2), (0, 0, 1), "SOURCEANDSINK", sys) add_port(ifc, el, "Ramal", (0, H_t, 0), (0, 1, 0), "SOURCEANDSINK", sys) assign_ifc_material(ifc, el, sys) assign_pset_fitting(ifc, el, sys, dt["desc"]) bpy.ops.bim.update_representation() print("OK " + name_te) # ===================================================== # ACCESORIO 3: TERMINAL MACHO (sin rosca) # ===================================================== dm = term_data[size] radio_m = dm["OD_fitting"] / 2 rp_m = dm["OD_pipe"] / 2 H_m = dm["H"] G_m = dm["G"] name_tm = cfg["prefix_term"] + size + cfg["suffix"] if not already_exists(name_tm): print("\n--- " + name_tm + " ---") bpy.ops.mesh.primitive_cylinder_add( vertices=lados, radius=radio_m, depth=H_m, location=(0, 0, H_m/2)) body = bpy.context.object bpy.ops.mesh.primitive_cylinder_add( vertices=lados, radius=rp_m, depth=G_m, location=(0, 0, H_m + G_m/2)) bu(body, bpy.context.object) body.name = name_tm set_origin(body, (0, 0, 0)) apply_transforms(body) apply_system_color(body, sys) ifc, el = assign_ifc(body, "IfcPipeFittingType", "TRANSITION", name_tm, "Terminal Macho " + cfg["desc_mat"] + " " + dm["desc"] + " sin rosca", "MALE_ADAPTER") add_port(ifc, el, "Socket", (0, 0, 0), (0, 0,-1), "SOURCEANDSINK", sys) add_port(ifc, el, "Espiga", (0, 0, H_m+G_m), (0, 0, 1), "SOURCEANDSINK", sys) assign_ifc_material(ifc, el, sys) assign_pset_fitting(ifc, el, sys, dm["desc"]) bpy.ops.bim.update_representation() print("OK " + name_tm) # ===================================================== # ACCESORIO 4: ADAPTADOR HEMBRA (sin rosca) # ===================================================== da = adapt_data[size] radio_a = da["OD_fitting"] / 2 H_a = da["H"] collar_h = H_a * 0.35 collar_r = radio_a * 1.15 ring_h = H_a * 0.12 ring_r = radio_a * 1.08 name_af = cfg["prefix_adapt"] + size + cfg["suffix"] if not already_exists(name_af): print("\n--- " + name_af + " ---") bpy.ops.mesh.primitive_cylinder_add( vertices=lados, radius=radio_a, depth=H_a, location=(0, 0, H_a/2)) body_af = bpy.context.object bpy.ops.mesh.primitive_cylinder_add( vertices=lados, radius=collar_r, depth=collar_h, location=(0, 0, H_a - collar_h/2)) bu(body_af, bpy.context.object) bpy.ops.mesh.primitive_cylinder_add( vertices=lados, radius=ring_r, depth=ring_h, location=(0, 0, ring_h/2)) bu(body_af, bpy.context.object) body_af.name = name_af set_origin(body_af, (0, 0, 0)) apply_transforms(body_af) apply_system_color(body_af, sys) ifc, el = assign_ifc(body_af, "IfcPipeFittingType", "TRANSITION", name_af, "Adaptador Hembra " + cfg["desc_mat"] + " " + da["desc"] + " sin rosca", "FEMALE_ADAPTER") add_port(ifc, el, "Socket_A", (0, 0, 0), (0, 0,-1), "SOURCEANDSINK", sys) add_port(ifc, el, "Socket_B", (0, 0, H_a), (0, 0, 1), "SOURCEANDSINK", sys) assign_ifc_material(ifc, el, sys) assign_pset_fitting(ifc, el, sys, da["desc"]) bpy.ops.bim.update_representation() print("OK " + name_af) # ===================================================== # ACCESORIO 5: REGISTRO AGUA FRIA (valvula de bola) # ===================================================== dr = reg_data[size] OD_f_r = dr["OD_f"] A_r = dr["A"] B_r = dr["B"] C_r = dr["C"] radio_r = OD_f_r / 2 sock_l_r = B_r * 0.27 body_l_r = B_r - 2 * sock_l_r handle_w = B_r * 0.75 handle_t = A_r * 0.60 handle_h = C_r * 0.35 handle_bot_z = -radio_r + 0.65 * C_r handle_cz = handle_bot_z + handle_h / 2 vastago_h = handle_bot_z vastago_r = radio_r * 0.75 vastago_cz = vastago_h / 2 name_reg = cfg["prefix_reg"] + size + cfg["suffix"] if not already_exists(name_reg): print("\n--- " + name_reg + " ---") bpy.ops.mesh.primitive_cylinder_add( vertices=lados, radius=radio_r, depth=sock_l_r, location=(0, sock_l_r/2, 0), rotation=(math.radians(90), 0, 0)) valve = bpy.context.object bpy.ops.mesh.primitive_cylinder_add( vertices=lados, radius=radio_r, depth=body_l_r, location=(0, B_r/2, 0), rotation=(math.radians(90), 0, 0)) bu(valve, bpy.context.object) bpy.ops.mesh.primitive_cylinder_add( vertices=lados, radius=radio_r, depth=sock_l_r, location=(0, B_r - sock_l_r/2, 0), rotation=(math.radians(90), 0, 0)) bu(valve, bpy.context.object) bpy.ops.mesh.primitive_cylinder_add( vertices=lados, radius=vastago_r, depth=vastago_h, location=(0, B_r/2, vastago_cz)) bu(valve, bpy.context.object) # Apply body color BEFORE merging handle apply_system_color(valve, sys) # Create handle with its own color material bpy.ops.mesh.primitive_cube_add(size=1, location=(0, B_r/2, handle_cz)) handle = bpy.context.object handle.scale = (handle_w/2, handle_t/2, handle_h/2) bpy.context.view_layer.objects.active = handle bpy.ops.object.transform_apply(scale=True) # Assign handle color: blue for AF, red for AC handle_mat = get_blue_handle_material() if sys == "AF" else get_red_handle_material() handle.data.materials.append(handle_mat) # Add handle material slot to valve body before union valve.data.materials.append(handle_mat) bu(valve, handle) valve.name = name_reg set_origin(valve, (0, 0, 0)) apply_transforms(valve) ifc, el = assign_ifc(valve, "IfcValveType", "ISOLATING", name_reg, cfg["desc_reg"] + " " + dr["desc"] + " valvula de bola SOLDAR", "BALL_VALVE") add_port(ifc, el, "Entrada", (0, 0, 0), (0,-1, 0), "SINK", sys) add_port(ifc, el, "Salida", (0, B_r, 0), (0, 1, 0), "SOURCE", sys) assign_ifc_material(ifc, el, sys) assign_pset_valve(ifc, el, sys, dr["desc"]) bpy.ops.bim.update_representation() print("OK " + name_reg) # ===================================================== # ACCESORIO 6: CAP / TAPON PVC SCH40 # Cilindro socket + cono truncado achatado (sin tapa superior) # ===================================================== dcp = cap_data[size] radio_cap = dcp["OD_fitting"] / 2 H_cap = dcp["H"] G_cap = dcp["G"] wall_cap = dcp["wall"] name_cap = cfg["prefix_cap"] + size + cfg["suffix"] if not already_exists(name_cap): print("\n--- " + name_cap + " ---") bpy.ops.mesh.primitive_cylinder_add( vertices=lados, radius=radio_cap, depth=H_cap, location=(0, 0, H_cap/2)) cap_body = bpy.context.object dome_h = radio_cap * 0.25 dome_r_top = radio_cap * 0.70 bpy.ops.mesh.primitive_cone_add( vertices=lados, radius1=radio_cap, radius2=dome_r_top, depth=dome_h, location=(0, 0, H_cap + dome_h/2)) bu(cap_body, bpy.context.object) cap_body.name = name_cap set_origin(cap_body, (0, 0, 0)) apply_transforms(cap_body) apply_system_color(cap_body, sys) ifc, el = assign_ifc(cap_body, "IfcPipeFittingType", "CONNECTOR", name_cap, "Cap/Tapon " + cfg["desc_mat"] + " " + dcp["desc"] + " sin rosca", "CAP") add_port(ifc, el, "Socket", (0, 0, 0), (0, 0, -1), "SOURCEANDSINK", sys) assign_ifc_material(ifc, el, sys) assign_pset_fitting(ifc, el, sys, dcp["desc"]) bpy.ops.bim.update_representation() print("OK " + name_cap) # ===================================================== # ACCESORIO 7: REDUCCIONES / BUSHING PVC SCH40 # ===================================================== # Solo crear reducciones que involucren este size for red_key, dr_red in red_data.items(): parts = red_key.split("x") if size not in parts: continue OD_big = dr_red["OD_big"] OD_small = dr_red["OD_small"] L_red = dr_red["L"] G_big = dr_red["G_big"] G_small = dr_red["G_small"] r_big = OD_big / 2 r_small = OD_small / 2 name_red = cfg["prefix_red"] + red_key + cfg["suffix"] if already_exists(name_red): continue print("\n--- " + name_red + " ---") bpy.ops.mesh.primitive_cylinder_add( vertices=lados, radius=r_big, depth=G_big, location=(0, 0, G_big/2)) red_body = bpy.context.object trans_h = L_red - G_big - G_small if trans_h > 0.0005: trans_cz = G_big + trans_h/2 bpy.ops.mesh.primitive_cone_add( vertices=lados, radius1=r_big, radius2=r_small, depth=trans_h, location=(0, 0, trans_cz)) bu(red_body, bpy.context.object) bpy.ops.mesh.primitive_cylinder_add( vertices=lados, radius=r_small, depth=G_small, location=(0, 0, L_red - G_small/2)) bu(red_body, bpy.context.object) red_body.name = name_red set_origin(red_body, (0, 0, 0)) apply_transforms(red_body) apply_system_color(red_body, sys) ifc, el = assign_ifc(red_body, "IfcPipeFittingType", "TRANSITION", name_red, "Reduccion/Bushing " + cfg["desc_mat"] + " " + dr_red["desc"] + " SxS sin rosca", "REDUCER") add_port(ifc, el, "Grande", (0, 0, 0), (0, 0, -1), "SOURCEANDSINK", sys) add_port(ifc, el, "Chico", (0, 0, L_red), (0, 0, 1), "SOURCEANDSINK", sys) assign_ifc_material(ifc, el, sys) assign_pset_fitting(ifc, el, sys, dr_red["desc"]) bpy.ops.bim.update_representation() print("OK " + name_red) # ===================================================== # VERIFICACION FINAL # ===================================================== ifc = IfcStore.get_file() if ifc: n = 0 for obj in list(bpy.data.objects): ifc_id = obj.BIMObjectProperties.ifc_definition_id if ifc_id and ifc_id > 0: try: ifc.by_id(ifc_id) except Exception: n += 1 print("PREFINAL - eliminando huerfano: " + obj.name) try: bpy.data.objects.remove(obj, do_unlink=True) except: pass print("Verificacion final: " + str(n) + " huerfanos eliminados") print("\nAccesorios " + cfg["desc_mat"] + " " + size + " pulg CREADOS OK") # ===================================================== # FUNCION: Caja Lavadora Plastica Colombia # 23x15x8 cm, 3 puertos por abajo: AF, AC, Sanitaria # ===================================================== def generate_caja_lavadora(lados=DEFAULT_SEGMENTS): dcl = caja_lav_data["standard"] ancho_cl = dcl["ancho"] alto_cl = dcl["alto"] prof_cl = dcl["prof"] wall_cl = dcl["wall"] OD_af_cl = dcl["OD_af"] OD_ac_cl = dcl["OD_ac"] OD_san_cl = dcl["OD_san"] G_af_cl = dcl["G_af"] G_ac_cl = dcl["G_ac"] G_san_cl = dcl["G_san"] name_caja = "CajaLavadora_Std" if already_exists(name_caja): return print("\n--- " + name_caja + " ---") # Caja exterior solida bpy.ops.mesh.primitive_cube_add(size=2, location=(0, prof_cl/2, alto_cl/2)) caja = bpy.context.object caja.scale = (ancho_cl/2, prof_cl/2, alto_cl/2) bpy.context.view_layer.objects.active = caja bpy.ops.object.transform_apply(scale=True) # Cavidad interior inner_x = (ancho_cl - 2*wall_cl) / 2 inner_y = (prof_cl - wall_cl) / 2 inner_z = (alto_cl - 2*wall_cl) / 2 bpy.ops.mesh.primitive_cube_add(size=2, location=(0, (prof_cl - wall_cl)/2, alto_cl/2)) cavity = bpy.context.object cavity.scale = (inner_x, inner_y, inner_z) bpy.context.view_layer.objects.active = cavity bpy.ops.object.transform_apply(scale=True) m_diff = caja.modifiers.new("diff", 'BOOLEAN') m_diff.operation = 'DIFFERENCE' m_diff.object = cavity bpy.context.view_layer.objects.active = caja bpy.ops.object.modifier_apply(modifier="diff") bpy.data.objects.remove(cavity, do_unlink=True) # Abrir cara frontal bpy.ops.mesh.primitive_cube_add(size=2, location=(0, 0, alto_cl/2)) front_cut = bpy.context.object front_cut.scale = (inner_x, wall_cl*2, inner_z) bpy.context.view_layer.objects.active = front_cut bpy.ops.object.transform_apply(scale=True) m_diff2 = caja.modifiers.new("front", 'BOOLEAN') m_diff2.operation = 'DIFFERENCE' m_diff2.object = front_cut bpy.context.view_layer.objects.active = caja bpy.ops.object.modifier_apply(modifier="front") bpy.data.objects.remove(front_cut, do_unlink=True) # Niples pegados a la base af_x = -ancho_cl * 0.30 af_y = prof_cl * 0.50 r_af = OD_af_cl / 2 bpy.ops.mesh.primitive_cylinder_add( vertices=lados, radius=r_af, depth=G_af_cl, location=(af_x, af_y, -G_af_cl/2)) bu(caja, bpy.context.object) ac_x = ancho_cl * 0.30 ac_y = prof_cl * 0.50 r_ac = OD_ac_cl / 2 bpy.ops.mesh.primitive_cylinder_add( vertices=lados, radius=r_ac, depth=G_ac_cl, location=(ac_x, ac_y, -G_ac_cl/2)) bu(caja, bpy.context.object) san_y = prof_cl * 0.50 r_san = OD_san_cl / 2 bpy.ops.mesh.primitive_cylinder_add( vertices=lados, radius=r_san, depth=G_san_cl, location=(0, san_y, -G_san_cl/2)) bu(caja, bpy.context.object) caja.name = name_caja set_origin(caja, (0, 0, 0)) apply_transforms(caja) ifc, el = assign_ifc(caja, "IfcSanitaryTerminalType", "USERDEFINED", name_caja, "Caja Lavadora Plastica " + dcl["desc"] + " Colombia - entradas por abajo", "CAJA_LAVADORA") add_port(ifc, el, "AguaFria", (af_x, af_y, -G_af_cl), (0, 0, -1), "SINK") add_port(ifc, el, "AguaCaliente", (ac_x, ac_y, -G_ac_cl), (0, 0, -1), "SINK") add_port(ifc, el, "Sanitaria", (0, san_y, -G_san_cl), (0, 0, -1), "SOURCE") bpy.ops.bim.update_representation() print("OK " + name_caja) # ===================================================== # FUNCION: Calentador de paso 12 Lt # Cuerpo rectangular + chimenea cilindrica arriba # 3 puertos por abajo: Gas, Salida AC, Entrada AF # ===================================================== def generate_calentador(lados=DEFAULT_SEGMENTS): dc = calentador_data["12lt"] ancho = dc["ancho"] # X = 0.35 prof = dc["prof"] # Y = 0.185 alto = dc["alto"] # Z = 0.61 chim_r = dc["chim_d"] / 2 chim_h = dc["chim_h"] G_port = dc["G_port"] name_cal = "Calentador_12Lt" if already_exists(name_cal): return print("\n--- " + name_cal + " ---") # Cuerpo principal: caja rectangular bpy.ops.mesh.primitive_cube_add(size=2, location=(0, prof/2, alto/2)) cal = bpy.context.object cal.scale = (ancho/2, prof/2, alto/2) bpy.context.view_layer.objects.active = cal bpy.ops.object.transform_apply(scale=True) # Chimenea cilindrica arriba-centro bpy.ops.mesh.primitive_cylinder_add( vertices=lados, radius=chim_r, depth=chim_h, location=(0, prof * 0.35, alto + chim_h/2)) bu(cal, bpy.context.object) # Niples por abajo (z=0 hacia abajo) # Entrada Gas: izquierda gas_x = dc["px_gas"] gas_y = prof * 0.50 r_port = dc["OD_gas"] / 2 bpy.ops.mesh.primitive_cylinder_add( vertices=lados, radius=r_port, depth=G_port, location=(gas_x, gas_y, -G_port/2)) bu(cal, bpy.context.object) # Salida Agua Caliente: centro sal_x = dc["px_sal"] sal_y = prof * 0.50 r_sal = dc["OD_sal"] / 2 bpy.ops.mesh.primitive_cylinder_add( vertices=lados, radius=r_sal, depth=G_port, location=(sal_x, sal_y, -G_port/2)) bu(cal, bpy.context.object) # Entrada Agua Fria: derecha ent_x = dc["px_ent"] ent_y = prof * 0.50 r_ent = dc["OD_ent"] / 2 bpy.ops.mesh.primitive_cylinder_add( vertices=lados, radius=r_ent, depth=G_port, location=(ent_x, ent_y, -G_port/2)) bu(cal, bpy.context.object) cal.name = name_cal set_origin(cal, (0, 0, 0)) apply_transforms(cal) apply_dark(cal) # IFC: IfcBoilerType WATER (clase concreta en IFC4 para calentador de agua) ifc, el = assign_ifc(cal, "IfcBoilerType", "WATER", name_cal, "Calentador de paso 12Lt " + dc["desc"], "WATER_HEATER") add_port(ifc, el, "EntradaGas", (gas_x, gas_y, -G_port), (0, 0, -1), "SINK") add_port(ifc, el, "SalidaAC", (sal_x, sal_y, -G_port), (0, 0, -1), "SOURCE") add_port(ifc, el, "EntradaAF", (ent_x, ent_y, -G_port), (0, 0, -1), "SINK") add_port(ifc, el, "Chimenea", (0, prof*0.35, alto + chim_h), (0, 0, 1), "SOURCE") bpy.ops.bim.update_representation() print("OK " + name_cal) # ===================================================== # BILINGUAL LABELS / ETIQUETAS BILINGUES # ===================================================== LABELS = { "es": { "panel_title": "Accesorios PVC/CPVC", "system_label": "Sistema:", "select_sizes": "Seleccione diametros:", "specials": "Especiales:", "caja_lav": "Caja Lavadora 23x15x8", "calentador": "Calentador 12Lt", "geometry": "Geometria:", "segments": "Segmentos", "btn_create": "Crear Accesorios", "info_title": "Requisitos:", "info_1": "Tener un archivo IFC guardado", "info_2": "con nombre (File > Save As IFC)", "info_3": "antes de crear accesorios.", "lang_label": "Idioma:", "warn_none": "Seleccione al menos un diametro o equipo", "created": "Creados y guardados: ", "created_ok": "Creados OK. Guardar: File > Save As IFC", "err_save": "Creados. Error al guardar: ", "sys_af": "AF - PVC Agua Fria", "sys_ac": "AC - CPVC Agua Caliente", }, "en": { "panel_title": "PVC/CPVC Fittings", "system_label": "System:", "select_sizes": "Select diameters:", "specials": "Special equipment:", "caja_lav": "Washing Machine Box 23x15x8", "calentador": "Water Heater 12Lt", "geometry": "Geometry:", "segments": "Segments", "btn_create": "Create Fittings", "info_title": "Requirements:", "info_1": "Have a saved IFC file", "info_2": "with a name (File > Save As IFC)", "info_3": "before creating fittings.", "lang_label": "Language:", "warn_none": "Select at least one diameter or equipment", "created": "Created and saved: ", "created_ok": "Created OK. Save: File > Save As IFC", "err_save": "Created. Save error: ", "sys_af": "CW - PVC Cold Water", "sys_ac": "HW - CPVC Hot Water", }, } def L(key): """Get label for current language.""" try: lang = bpy.context.scene.pvc_accesorios.idioma except: lang = "es" return LABELS.get(lang, LABELS["es"]).get(key, key) # ===================================================== # PANEL UI # ===================================================== class PVC_AccesoriosProps(bpy.types.PropertyGroup): sistema: bpy.props.EnumProperty( name="Sistema", items=[("AF", "AF", "PVC Agua Fria / Cold Water"), ("AC", "AC", "CPVC Agua Caliente / Hot Water")], default="AF") size_05: bpy.props.BoolProperty(name='1/2"', default=False) size_075: bpy.props.BoolProperty(name='3/4"', default=True) size_10: bpy.props.BoolProperty(name='1"', default=False) size_125: bpy.props.BoolProperty(name='1-1/4"', default=False) size_15: bpy.props.BoolProperty(name='1-1/2"', default=False) size_20: bpy.props.BoolProperty(name='2"', default=False) caja_lav: bpy.props.BoolProperty(name='Caja Lavadora', default=False) calentador: bpy.props.BoolProperty(name='Calentador 12Lt', default=False) segmentos: bpy.props.IntProperty( name="Segmentos", default=DEFAULT_SEGMENTS, min=6, max=64, step=2) idioma: bpy.props.EnumProperty( name="Idioma", items=[("es", "ES", "Español"), ("en", "EN", "English")], default="es") PROP_MAP = [ ("size_05", "0.5"), ("size_075", "0.75"), ("size_10", "1.0"), ("size_125", "1.25"), ("size_15", "1.5"), ("size_20", "2.0"), ] class PVC_OT_CrearAccesorios(bpy.types.Operator): bl_idname = "pvc.crear_accesorios" bl_label = "Create PVC/CPVC Fittings" bl_description = "Generate fittings for selected system and diameters" bl_options = {'REGISTER', 'UNDO'} def execute(self, context): props = context.scene.pvc_accesorios lados = props.segmentos sys = props.sistema sizes_to_create = [] for prop_name, size_key in PROP_MAP: if getattr(props, prop_name): sizes_to_create.append(size_key) if not sizes_to_create and not props.caja_lav and not props.calentador: self.report({'WARNING'}, L("warn_none")) return {'CANCELLED'} for sz in sizes_to_create: try: generate_for_size(sz, lados, sys) except Exception as e: self.report({'ERROR'}, "Error size " + sz + ": " + str(e)) print("ERROR size " + sz + ": " + str(e)) if props.caja_lav: try: generate_caja_lavadora(lados) except Exception as e: self.report({'ERROR'}, "Error caja: " + str(e)) if props.calentador: try: generate_calentador(lados) except Exception as e: self.report({'ERROR'}, "Error calentador: " + str(e)) sys_name = L("sys_af") if sys == "AF" else L("sys_ac") creados = [sys_name] if sizes_to_create: creados.append(", ".join(sizes_to_create)) if props.caja_lav: creados.append(L("caja_lav")) if props.calentador: creados.append(L("calentador")) try: ifc_path = bpy.context.scene.BIMProperties.ifc_file if ifc_path and ifc_path.strip() and ifc_path.strip() != ".ifc": bpy.ops.bim.save_project(filepath=ifc_path) self.report({'INFO'}, L("created") + " | ".join(creados)) else: self.report({'WARNING'}, L("created_ok")) except Exception as e: self.report({'WARNING'}, L("err_save") + str(e)) return {'FINISHED'} class PVC_PT_AccesoriosPanel(bpy.types.Panel): bl_label = "PVC/CPVC Fittings" bl_idname = "PVC_PT_accesorios" bl_space_type = 'VIEW_3D' bl_region_type = 'UI' bl_category = "Accesorios" def draw(self, context): layout = self.layout props = context.scene.pvc_accesorios # Language selector row_lang = layout.row(align=True) row_lang.label(text=L("lang_label"), icon='WORLD') row_lang.prop(props, "idioma", expand=True) layout.separator() # System selector [AF] [AC] box_sys = layout.box() box_sys.label(text=L("system_label"), icon='MODIFIER') box_sys.prop(props, "sistema", expand=True) layout.separator() # Diameters box = layout.box() box.label(text=L("select_sizes"), icon='MESH_CYLINDER') col = box.column(align=True) row = col.row(align=True) row.prop(props, "size_05", toggle=True) row.prop(props, "size_075", toggle=True) row = col.row(align=True) row.prop(props, "size_10", toggle=True) row.prop(props, "size_125", toggle=True) row = col.row(align=True) row.prop(props, "size_15", toggle=True) row.prop(props, "size_20", toggle=True) layout.separator() # Specials box2 = layout.box() box2.label(text=L("specials"), icon='HOME') box2.prop(props, "caja_lav", text=L("caja_lav"), icon='CHECKBOX_HLT' if props.caja_lav else 'CHECKBOX_DEHLT') box2.prop(props, "calentador", text=L("calentador"), icon='CHECKBOX_HLT' if props.calentador else 'CHECKBOX_DEHLT') layout.separator() # Segments box3 = layout.box() box3.label(text=L("geometry"), icon='MOD_SUBSURF') box3.prop(props, "segmentos", text=L("segments"), slider=True) # Create button layout.separator() row = layout.row() row.scale_y = 1.8 row.operator("pvc.crear_accesorios", text=L("btn_create"), icon='PLAY') # Info box (compact) layout.separator() info = layout.box() col_i = info.column(align=True) col_i.scale_y = 0.8 col_i.label(text=L("info_title"), icon='ERROR') col_i.label(text=L("info_1")) col_i.label(text=L("info_2")) col_i.label(text=L("info_3")) # ===================================================== # REGISTER / REGISTRO # ===================================================== classes = [ PVC_AccesoriosProps, PVC_OT_CrearAccesorios, PVC_PT_AccesoriosPanel, ] def register(): for cls in classes: try: bpy.utils.unregister_class(cls) except: pass for cls in classes: bpy.utils.register_class(cls) bpy.types.Scene.pvc_accesorios = bpy.props.PointerProperty(type=PVC_AccesoriosProps) def unregister(): for cls in reversed(classes): try: bpy.utils.unregister_class(cls) except: pass try: del bpy.types.Scene.pvc_accesorios except: pass register() print("\n" + "="*60) print("PVC/CPVC FITTINGS V0.9 - Javier Fonseca") print(" jafocol@gmail.com | www.rojasfonseca.com") print(" N-panel > 'Accesorios' tab") print(" Systems: AF (PVC Cold) | AC (CPVC Hot)") print("="*60)