# ===================================================== # PVC SCH40 FITTING GENERATOR FOR BLENDER/BONSAI # GENERADOR DE ACCESORIOS PVC SCH40 PARA BLENDER/BONSAI # ===================================================== # Version: V0.8 # Author: Javier Fonseca # Email: jafocol@gmail.com # Web: www.rojasfonseca.com # ===================================================== # # --- ENGLISH DOCUMENTATION --- # This script generates parametric PVC SCH40 pipe fittings # as IFC objects inside Blender using the Bonsai (BlenderBIM) add-on. # # 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 diameters and special equipment you need # 5. Adjust curve segments (6-64) for geometry detail # 6. Click "Create PVC Fittings" button # 7. Fittings that already exist in the IFC will be SKIPPED # (they are never overwritten to protect placed occurrences) # # FITTINGS GENERATED PER DIAMETER: # - 90 Elbow (IfcPipeFittingType BEND) # - Tee (IfcPipeFittingType JUNCTION) # - Male Adapter (IfcPipeFittingType TRANSITION) # - Female Adapter (IfcPipeFittingType TRANSITION) # - Ball Valve (IfcValveType ISOLATING) # - Cap (IfcPipeFittingType CONNECTOR) # - Reducers/Bushings (IfcPipeFittingType TRANSITION) # # SPECIAL EQUIPMENT: # - Washing Machine Box 23x15x8cm (IfcSanitaryTerminalType) # - Water Heater 12Lt (IfcBoilerType WATER) # # PORT FLOW DIRECTION CONVENTION: # - Symmetric fittings (elbow, tee, cap, reducers, adapters): # SOURCEANDSINK (bidirectional, flow depends on system) # - 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 # como objetos IFC dentro de Blender usando el add-on Bonsai. # # REQUISITOS: # 1. Blender 4.x o 5.x con add-on Bonsai instalado # 2. Se debe crear y GUARDAR un proyecto IFC con nombre # ANTES de ejecutar este script (File > Save As IFC) # 3. El archivo IFC debe tener una ruta valida (no "Unsaved") # # COMO USAR: # 1. Abrir su proyecto IFC en Blender/Bonsai # 2. Ejecutar este script (Alt+P o pegar en Scripting) # 3. Abrir el N-panel (tecla N) > pestana "Accesorios"/"Fittings" # 4. Seleccionar los diametros y equipos especiales # 5. Ajustar segmentos de curva (6-64) para detalle # 6. Click en "Crear Accesorios PVC" # 7. Los accesorios que ya existan en el IFC se OMITEN # (nunca se sobrescriben para proteger ocurrencias) # # ACCESORIOS GENERADOS POR DIAMETRO: # - Codo 90 (IfcPipeFittingType BEND) # - Te (IfcPipeFittingType JUNCTION) # - Terminal Macho (IfcPipeFittingType TRANSITION) # - Adaptador Hembra (IfcPipeFittingType TRANSITION) # - Registro Agua Fria (IfcValveType ISOLATING) # - Cap/Tapon (IfcPipeFittingType CONNECTOR) # - Reducciones/Bushing (IfcPipeFittingType TRANSITION) # # EQUIPOS ESPECIALES: # - Caja Lavadora 23x15x8cm (IfcSanitaryTerminalType) # - Calentador 12Lt (IfcBoilerType WATER) # # CONVENCION FLOWDIRECTION DE PUERTOS: # - Accesorios simetricos (codo, te, cap, reducciones, adaptadores): # SOURCEANDSINK (bidireccional, el flujo depende del sistema) # - Valvula: Entrada=SINK, Salida=SOURCE # - Calentador: Gas/Agua entrada=SINK, AC salida/Chimenea=SOURCE # - Caja Lavadora: Agua entrada=SINK, Sanitaria=SOURCE # # ===================================================== # 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, # info panel with IFC requirements # ===================================================== 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 = ["CodoPVC_","TePVC_","TermMachoPVC_","AdaptHembraPVC_","RegistroAF_", "CapPVC_","RedPVC_","CajaLavadora_","Calentador_"] # ===================================================== # MATERIALES # ===================================================== def get_blue_material(): name = "PVC_AguaFria_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.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 apply_blue(obj): mat = get_blue_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) # ===================================================== # 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"): port = ifcopenshell.api.run("system.add_port", ifc, element=element) port.Name = name port.FlowDirection = flow port.SystemType = "DOMESTICCOLDWATER" try: ifcopenshell.api.run("geometry.edit_object_placement", ifc, product=port, matrix=port_mat(loc, direction)) print(" Puerto [" + name + "] OK") 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): # 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 = "CodoPVC_" + size + "_SCH40" 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) ifc, el = assign_ifc(torus, "IfcPipeFittingType", "BEND", name_codo, "Codo 90 PVC SCH40 " + dc["desc"] + " sin rosca") add_port(ifc, el, "Boca_X", (-L_c - radio_c, 0, 0), (-1, 0, 0), "SOURCEANDSINK") add_port(ifc, el, "Boca_Y", (0, -L_c - radio_c, 0), ( 0,-1, 0), "SOURCEANDSINK") 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 = "TePVC_" + size + "_SCH40" 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) ifc, el = assign_ifc(run_cyl, "IfcPipeFittingType", "JUNCTION", name_te, "Te PVC SCH40 " + dt["desc"] + " SxSxS Part 2400 sin rosca") add_port(ifc, el, "Run_A", (0, 0, -L_t/2), (0, 0,-1), "SOURCEANDSINK") add_port(ifc, el, "Run_B", (0, 0, L_t/2), (0, 0, 1), "SOURCEANDSINK") add_port(ifc, el, "Ramal", (0, H_t, 0), (0, 1, 0), "SOURCEANDSINK") 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 = "TermMachoPVC_" + size + "_SCH40" 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) ifc, el = assign_ifc(body, "IfcPipeFittingType", "TRANSITION", name_tm, "Terminal Macho PVC SCH40 " + dm["desc"] + " sin rosca", "MALE_ADAPTER") add_port(ifc, el, "Socket", (0, 0, 0), (0, 0,-1), "SOURCEANDSINK") add_port(ifc, el, "Espiga", (0, 0, H_m+G_m), (0, 0, 1), "SOURCEANDSINK") 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 = "AdaptHembraPVC_" + size + "_SCH40" 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) ifc, el = assign_ifc(body_af, "IfcPipeFittingType", "TRANSITION", name_af, "Adaptador Hembra PVC SCH40 " + da["desc"] + " sin rosca", "FEMALE_ADAPTER") add_port(ifc, el, "Socket_A", (0, 0, 0), (0, 0,-1), "SOURCEANDSINK") add_port(ifc, el, "Socket_B", (0, 0, H_a), (0, 0, 1), "SOURCEANDSINK") 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 = "RegistroAF_" + size + "_SCH40" 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) 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) bu(valve, handle) valve.name = name_reg set_origin(valve, (0, 0, 0)) apply_transforms(valve) apply_blue(valve) ifc, el = assign_ifc(valve, "IfcValveType", "ISOLATING", name_reg, "Registro Agua Fria PVC SCH40 " + dr["desc"] + " valvula de bola SOLDAR", "BALL_VALVE") add_port(ifc, el, "Entrada", (0, 0, 0), (0,-1, 0), "SINK") add_port(ifc, el, "Salida", (0, B_r, 0), (0, 1, 0), "SOURCE") 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 = "CapPVC_" + size + "_SCH40" 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) ifc, el = assign_ifc(cap_body, "IfcPipeFittingType", "CONNECTOR", name_cap, "Cap/Tapon PVC SCH40 " + dcp["desc"] + " sin rosca", "CAP") add_port(ifc, el, "Socket", (0, 0, 0), (0, 0, -1), "SOURCEANDSINK") 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 = "RedPVC_" + red_key + "_SCH40" 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) ifc, el = assign_ifc(red_body, "IfcPipeFittingType", "TRANSITION", name_red, "Reduccion/Bushing PVC SCH40 " + dr_red["desc"] + " SxS sin rosca", "REDUCER") add_port(ifc, el, "Grande", (0, 0, 0), (0, 0, -1), "SOURCEANDSINK") add_port(ifc, el, "Chico", (0, 0, L_red), (0, 0, 1), "SOURCEANDSINK") 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 PVC SCH40 " + 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 SCH40", "select_sizes": "Seleccione diametros:", "specials": "Especiales:", "caja_lav": "Caja Lavadora 23x15x8", "calentador": "Calentador 12Lt", "geometry": "Geometria:", "segments": "Segmentos", "btn_create": "Crear Accesorios PVC", "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: ", }, "en": { "panel_title": "PVC SCH40 Fittings", "select_sizes": "Select diameters:", "specials": "Special equipment:", "caja_lav": "Washing Machine Box 23x15x8", "calentador": "Water Heater 12Lt", "geometry": "Geometry:", "segments": "Segments", "btn_create": "Create PVC 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: ", }, } 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): 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 Fittings" bl_description = "Generate SCH40 fittings for selected diameters" bl_options = {'REGISTER', 'UNDO'} def execute(self, context): props = context.scene.pvc_accesorios lados = props.segmentos 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) 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)) creados = [] if sizes_to_create: creados.append("PVC: " + ", ".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 SCH40 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() # 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 SCH40 FITTINGS V0.8 - Javier Fonseca") print(" jafocol@gmail.com | www.rojasfonseca.com") print(" N-panel > 'Accesorios' tab") print("="*60)