En este artículo abordamos la resolución geométrica de un problema habitual en reformas y obras: la colocación de baldosas rectangulares en patrón de espiga a 45°.
El trabajo se divide en dos fases o scripts interconectados:
**Fase 1 (Centrado y Simetría Geométrica): Define los parámetros de la pared y la pieza, calcula el desfase para centrar el patrón respecto al eje medio vertical de la pared y genera la malla recortada.
**Fase 2 (Análisis de Despiece y Catálogo de Taller): Toma como entrada los datos procesados en la Fase 1 (interseccion), clasifica las piezas cortadas por dimensiones y genera el plano codificado final para el alicatador.
El código está diseñado para ser totalmente configurable. Para adaptarlo a cualquier otra obra, basta con modificar los siguientes parámetros iniciales:
ancho_pared: Ancho total del paño de pared en cm (ej. 154.0 cm).
alto_pared: Alto total del paño de pared en cm (ej. 250.0 cm).
L: Largo de la baldosa en cm (ej. 28.0 cm).
W: Ancho de la baldosa en cm (ej. 7.0 cm).
correccion_manual: Ajuste fino manual opcional en cm (por defecto 0).
El objetivo prioritario de este primer script no es solo calcular cuántas piezas se usan, sino garantizar una geometría perfectamente simétrica: que los cortes de los extremos izquierdo y derecho sean exactamente idénticos y equilibrados estéticamente.
Al finalizar, este script genera el objeto de datos espaciales interseccion (y exporta el archivo despiece_cortes.csv), que servirá como fichero/variable de entrada para la Parte 2.
graphics.off()
# ==============================================================================
# 1. PARÁMETROS DE ENTRADA (Modificables según cada obra)
# ==============================================================================
ancho_pared <- 154.0
alto_pared <- 250.0
L <- 28.0
W <- 7.0
correccion_manual <- 0
# Carga de paquetes
if (!require("ggplot2")) install.packages("ggplot2")
if (!require("sf")) install.packages("sf")
library(ggplot2)
library(sf)
# ==============================================================================
# 2. GEOMETRÍA BASE Y CÁLCULO DE CENTRADO
# ==============================================================================
centro_pared <- ancho_pared / 2
# Función para rotar vértices
rotar_pieza <- function(vertices, angulo_grados) {
rad <- angulo_grados * pi / 180
R <- matrix(c(cos(rad), -sin(rad), sin(rad), cos(rad)), ncol = 2, byrow = TRUE)
vertices %*% t(R)
}
# Piezas base (Espiga V1 y V2)
vertices_base <- matrix(c(0,0, L,0, L,W, 0,W), ncol = 2, byrow = TRUE)
b1_base <- rotar_pieza(vertices_base, 45)
tx_b2 <- (L - W) * cos(pi/4)
ty_b2 <- (L + W) * cos(pi/4)
b2_base <- rotar_pieza(vertices_base, -45)
b2_base[, 1] <- b2_base[, 1] + tx_b2
b2_base[, 2] <- b2_base[, 2] + ty_b2
# Desplazamientos del patrón
distancia_horizontal <- b2_base[2, 1] - b1_base[4, 1]
paso_vertical_x <- b2_base[4, 1] - b1_base[2, 1]
paso_vertical_y <- b2_base[4, 2] - b1_base[2, 2]
# Centrado por pico para asegurar simetría exacta
x_pico_izq <- b1_base[2, 1]
x_pico_der <- b2_base[1, 1]
x_pico_medio <- (x_pico_izq + x_pico_der) / 2
ajuste_centrado <- centro_pared - x_pico_medio + correccion_manual
# ==============================================================================
# 3. GENERACIÓN DE LA MALLA DE BALDOSAS
# ==============================================================================
margen <- 4
n_horizontal <- ceiling(ancho_pared / abs(distancia_horizontal)) + margen
n_vertical <- ceiling(alto_pared / abs(paso_vertical_y)) + margen
lista_piezas <- list()
id_pieza <- 1
for (v in (-margen):n_vertical) {
for (h in (-n_horizontal):n_horizontal) {
dx <- h * distancia_horizontal + ajuste_centrado + v * paso_vertical_x
dy <- v * paso_vertical_y
# Pieza 1
lista_piezas[[length(lista_piezas) + 1]] <- data.frame(
x = b1_base[, 1] + dx, y = b1_base[, 2] + dy, grupo = paste0("P", id_pieza)
)
id_pieza <- id_pieza + 1
# Pieza 2
lista_piezas[[length(lista_piezas) + 1]] <- data.frame(
x = b2_base[, 1] + dx, y = b2_base[, 2] + dy, grupo = paste0("P", id_pieza)
)
id_pieza <- id_pieza + 1
}
}
pared_completa <- do.call(rbind, lista_piezas)
# ==============================================================================
# 4. INTERSECCIÓN Y ANÁLISIS ESPACIAL (sf)
# ==============================================================================
grupos <- unique(pared_completa$grupo)
lista_sf <- lapply(grupos, function(g) {
datos <- pared_completa[pared_completa$grupo == g, ]
coords <- rbind(as.matrix(datos[, c("x", "y")]), as.matrix(datos[1, c("x", "y")]))
st_sf(grupo = g, geometry = st_sfc(st_polygon(list(coords))))
})
baldosas_sf <- do.call(rbind, lista_sf)
# Polígono de la pared
pared_poligono <- st_polygon(list(matrix(c(
0, 0,
ancho_pared, 0,
ancho_pared, alto_pared,
0, alto_pared,
0, 0
), ncol = 2, byrow = TRUE)))
pared_sf <- st_sf(geometry = st_sfc(pared_poligono))
# Recorte exacto
interseccion <- st_intersection(baldosas_sf, pared_sf)
# Clasificación y métricas
area_original <- L * W
interseccion$area_visible <- as.numeric(st_area(interseccion))
interseccion$estado <- ifelse(abs(interseccion$area_visible - area_original) < 0.1, "Completa", "Cortada")
# Métricas acumuladas
num_completas <- sum(interseccion$estado == "Completa")
num_cortadas <- sum(interseccion$estado == "Cortada")
baldosas_usadas <- nrow(interseccion)
porcentaje_completas <- 100 * num_completas / baldosas_usadas
area_total_visible <- sum(interseccion$area_visible)
equivalente_baldosas <- area_total_visible / area_original
desperdicio_total <- sum(area_original - interseccion$area_visible[interseccion$estado == "Cortada"])
# Visibilidad horizontal
dentro <- subset(pared_completa, x >= 0 & x <= ancho_pared)
xmin_visible <- min(dentro$x)
xmax_visible <- max(dentro$x)
centro_visible <- (xmin_visible + xmax_visible) / 2
# ==============================================================================
# 5. DESPIECE Y EXPORTACIÓN CSV (Fichero puente entre scripts)
# ==============================================================================
interseccion$codigo <- ""
cortes_idx <- which(interseccion$estado == "Cortada")
interseccion$codigo[cortes_idx] <- paste0("C", seq_along(cortes_idx))
if (length(cortes_idx) > 0) {
despiece_list <- lapply(cortes_idx, function(i) {
bb <- st_bbox(interseccion[i, ])
data.frame(
Codigo = interseccion$codigo[i],
Area_cm2 = round(interseccion$area_visible[i], 1),
Ancho_Max_cm = round(as.numeric(bb['xmax'] - bb['xmin']), 1),
Alto_Max_cm = round(as.numeric(bb['ymax'] - bb['ymin']), 1)
)
})
despiece_df <- do.call(rbind, despiece_list)
write.csv(despiece_df, 'despiece_cortes.csv', row.names = FALSE)
}
En la siguiente figura se puede apreciar el eje central de la pared marcado con una línea azul discontinua. Se comprueba visualmente la perfecta simetría del patrón respecto al centro de la pared:
Una vez generada la geometría centrada, la Parte 2 utiliza el objeto interseccion (generado en la Parte 1).En esta fase:Filtramos las micropuntas o retales inapreciables (\(< 2\text{ cm}^2\)).Clasificamos las piezas por dimensiones (\(A \times H\)).Separamos las tiras laterales en dos tipologías independientes (izquierda y derecha) para respetar la orientación de la cara vista esmaltada del material.
if (!require("pacman")) install.packages("pacman")
pacman::p_load(ggplot2, sf, dplyr)
cortadas_sf <- interseccion %>%
filter(estado == "Cortada", area_visible > 2.0)
bboxes <- do.call(rbind, lapply(st_geometry(cortadas_sf), st_bbox))
cortadas_sf <- cortadas_sf %>%
mutate(
Ancho_Max = round(bboxes[, "xmax"] - bboxes[, "xmin"], 1),
Alto_Max = round(bboxes[, "ymax"] - bboxes[, "ymin"], 1),
Area_cm2 = round(area_visible, 1),
X_Centro = (bboxes[, "xmin"] + bboxes[, "xmax"]) / 2,
Es_Lateral = (Ancho_Max > 19 & Ancho_Max < 21 & Alto_Max > 24 & Alto_Max < 26),
Lado = ifelse(Es_Lateral & X_Centro < 77, "Izq",
ifelse(Es_Lateral & X_Centro >= 77, "Der", "Ambos"))
)
tipologias <- cortadas_sf %>%
st_drop_geometry() %>%
group_by(Area_cm2, Ancho_Max, Alto_Max, Lado) %>%
summarise(Cantidad = n(), .groups = "drop") %>%
arrange(desc(Cantidad), Lado) %>%
mutate(
Num_Tipo = row_number(),
Tipo_ID = sprintf("TIPO %02d", Num_Tipo),
Tipo_Corto = paste0("T", Num_Tipo),
Etiqueta_Leyenda = paste0(Tipo_ID, " (", Cantidad, " ud) [", Ancho_Max, "x", Alto_Max, " cm | ", Area_cm2, " cm²]")
)
cortadas_sf <- cortadas_sf %>%
inner_join(
tipologias %>% select(Area_cm2, Ancho_Max, Alto_Max, Lado, Tipo_ID, Tipo_Corto, Etiqueta_Leyenda, Num_Tipo),
by = c("Area_cm2", "Ancho_Max", "Alto_Max", "Lado")
)
niveles_ordenados <- tipologias$Etiqueta_Leyenda[order(tipologias$Num_Tipo)]
cortadas_sf$Etiqueta_Leyenda <- factor(cortadas_sf$Etiqueta_Leyenda, levels = niveles_ordenados)
centroides <- st_centroid(cortadas_sf)
coords_cent <- st_coordinates(centroides)
cortadas_sf$x_cent <- coords_cent[, 1]
cortadas_sf$y_cent <- coords_cent[, 2]
completas_sf <- interseccion %>% filter(estado == "Completa")
| Tipología | Unidades | Ancho (cm) | Alto (cm) | Área (cm²) |
|---|---|---|---|---|
| TIPO 01 | 23 | 20.1 | 24.7 | 174.2 |
| TIPO 02 | 23 | 20.1 | 24.7 | 174.2 |
| TIPO 03 | 4 | 5.0 | 2.5 | 6.3 |
| TIPO 04 | 4 | 12.4 | 7.5 | 49.4 |
| TIPO 05 | 4 | 17.4 | 12.4 | 98.4 |
| TIPO 06 | 3 | 9.9 | 4.9 | 24.5 |
| TIPO 07 | 3 | 14.8 | 9.9 | 73.5 |
| TIPO 08 | 3 | 19.8 | 14.8 | 122.5 |
| TIPO 09 | 3 | 22.3 | 17.4 | 147.4 |
| TIPO 10 | 3 | 24.7 | 19.8 | 171.5 |
| TIPO 11 | 3 | 24.7 | 22.3 | 190.1 |
| TIPO 12 | 1 | 5.2 | 4.9 | 13.6 |
| TIPO 13 | 1 | 10.2 | 9.9 | 51.7 |
| TIPO 14 | 1 | 15.1 | 14.8 | 100.7 |
| TIPO 15 | 1 | 20.1 | 17.4 | 144.9 |
| TIPO 16 | 1 | 20.1 | 19.8 | 149.7 |
| TIPO 17 | 1 | 20.1 | 22.3 | 170.6 |
paleta_18 <- c(
"#E53935", "#388E3C", "#F57C00", "#FBC02D", "#00ACC1",
"#1E88E5", "#3949AB", "#8E24AA", "#D81B60", "#6D4C41",
"#757575", "#546E7A", "#FFB300", "#C0CA33", "#00E676",
"#18FFFF", "#E040FB", "#FF5252"
)
names(paleta_18) <- niveles_ordenados
ggplot() +
geom_sf(data = completas_sf, fill = "#F0F0F0", color = "gray85", linewidth = 0.1) +
geom_sf(data = cortadas_sf, aes(fill = Etiqueta_Leyenda), color = "black", linewidth = 0.25) +
geom_label(
data = cortadas_sf,
aes(x = x_cent, y = y_cent, label = Tipo_Corto),
size = 2.2, fontface = "bold", label.padding = unit(0.08, "lines"),
linewidth = 0, fill = alpha("white", 0.85)
) +
scale_fill_manual(values = paleta_18) +
guides(fill = guide_legend(ncol = 2, byrow = TRUE)) +
theme_minimal() +
labs(
title = "Plano de Despiece de Obra",
subtitle = paste0("Total: ", nrow(cortadas_sf), " piezas cortadas distribuidas en ", nrow(tipologias), " tipos\n"),
fill = "Tipología:"
) +
theme(
legend.position = "bottom",
legend.title = element_text(face = "bold", size = 9),
legend.text = element_text(size = 8),
plot.title = element_text(face = "bold", size = 14),
plot.subtitle = element_text(size = 10)
)
Plano de despiece codificado para alicatador