TutorialLineales · 1D listones, perfiles y varillas
Tutorial paso a paso del módulo Lineales de Despiece: el
toggle de activación en Configuración → Lineales, los
defaults del taller (kerf, espesor de disco, perfil y material),
el editor de keywords para auto-detectar cortes a partir de los
nombres del CAD, el panel [L] con sus vistas
Stock | Catálogo, el wizard + Listón, el
importador CSV bilingüe (Despiece + OptiCut), el packer FFD
1D con multi-restart y la integración con BOM · Pedido ·
Etiquetas.
Las lineales son piezas 1D (no tableros 2D): listones de madera
maciza, machimbres, perfiles de aluminio, zócalos PVC, varillas
de placard Ø25/30, tirantes, alfajías y tapacantos. Se cortan de
barras nominales del catálogo y se empacan con un bin-packer
FFD + multi-restart (primer ajuste decreciente con
32 variantes semilladas). El módulo está desactivado por
default para no pagar el costo del UI extra en apps que sólo
trabajan con tableros crudos. La activación se persiste en
state.settings.linealesEnabled y sobrevive a
.despiece recargados: los datos lineales se guardan
como soft fields (linearItems,
linearResult) y se recuperan al reactivar. El modelo
de ejemplo es CocinaV9.despiece.
1 · Activar el módulo Lineales
El módulo vive detrás de un toggle global porque las apps que sólo trabajan con tableros crudos no necesitan el UI extra. La activación se hace una sola vez por taller.
Configuración (icono engranaje de la topbar) → tab
Listones (cortado 1D) (id lineal, registrado en
src/ui/settings-panel/tabs.ts:26). El primer row,
Activar módulo Lineales
(#stg-lineales-enabled, persistido vía
setSettings({ linealesEnabled }) desde
src/ui/settings-panel/lineal-wiring.ts:34) prende o
apaga todo lo demás.
Cuando el toggle está apagado:
| Superficie | Estado |
|---|---|
Botón [L] del topbar (#tb-lineales) | hidden (no se renderiza) |
| Atajo Shift+L | No-op + toast "Módulo Lineales desactivado · activalo en Settings → Lineales" |
Botón [L] del menú Exportar (4 items) | hidden vía state-apply.ts:146-166 |
Items linearItems guardados en disco | Se conservan tal cual — al reactivar se recuperan |
Resultado linearResult previo | Se conserva; al reactivar se re-puede abrir |
Esto significa que podés apagar el módulo para limpiar la UI
sin perder trabajo: las marcas quedan en el
.despiece y se reactivan al volver a prender.
2 · Settings → Lineales · defaults del taller
El tab tiene cuatro inputs numéricos / texto que aplican
tanto al wizard + Listón como al botón Calcular
barras (FFP) y al botón Auto-detectar del modelo.
Todos se persisten en
localStorage[despiece.lineales.defaults.v1] y se
sanitizan con sanitizeLinearDefaults() (cae a
per-field default si el valor está corrupto, nunca tira):
| Campo | Selector | Default | Rango | Función |
|---|---|---|---|---|
| Kerf | [data-lineal="kerfMm"] |
3 mm |
0 – 20 (step 0.5) | Separación entre cortes consecutivos en la misma barra. El packer lo aplica con kerfMm = 0 en el primer corte de cada barra y suma kerfMm entre cortes del mismo slot. |
| Espesor de disco | [data-lineal="bladeThicknessMm"] |
3 mm |
0 – 10 (step 0.5) | Informativo; el packer no lo usa. Reservado para cutter-compensation futuro. |
| Perfil por defecto | [data-lineal="defaultProfileId"] |
pino-2x3 |
id del catálogo | ID del LinearStockProfile que arranca el wizard (el selector cambia a este perfil). |
| Material por defecto | [data-lineal="defaultMaterialId"] |
maciza_pino_fk |
id del catálogo | Material que se asigna al item recién creado si el perfil no lo trae. |
El packer hardcodea allowRotate: false
(DEFAULT_LINEAL_DEFAULTS y
types.ts:125). Las lineales no rotan: el corte
siempre se mide sobre el largo nominal de la barra
(spec.lengthMm). Esto es por dominio — una varilla
de placard Ø25 sigue siendo Ø25 en cualquier posición, pero el
packer no intenta cortar transversal.
3 · Auto-detección por nombre (keywords)
El botón Auto-detectar del modelo del panel [L]
usa un diccionario de palabras clave para proponer piezas como
lineales a partir del nombre del CAD (sin mutar el modelo — solo
agrega items a state.project.linearItems). La regla
es longest-keyword-wins: si un part se llama Estante
listonado reforzado, matchea listonado (9 chars)
por encima de listón (6) o estante (7).
El editor (#stg-lineales-keywords-list) tiene tres filas:
| Fila | Función |
|---|---|
| Lista de keywords (existente) | Cada row expone palabra + perfil editable + × para borrar. Cambiar el perfil persiste sin re-render completo. |
| Form + Agregar | Input palabra + select perfil + botón. Trim + lowercase en el key (ver sanitizeLinearKeywords). |
| Restablecer por defecto | Vuelve al set built-in (resetLinearKeywords). |
El matcher (matchLinealKeyword en
src/lineales/auto-lineales.ts:38) combina
folding de acentos (Listón → liston)
con token-AND para multi-token: si el keyword es
tirante pine, todos sus tokens tienen que aparecer
como tokens del nombre (en cualquier orden).
Keywords built-in (src/lineales/types.ts:168):
| Keyword | Perfil sugerido | Uso típico |
|---|---|---|
listón · listones · liston | pino-2x3 | Listones de pino genéricos. |
machimbre | machimbre-1x4 | Machimbre 1×4 para cielorrasos / frisos. |
perfil | aluminio-l-1 | Perfiles de aluminio en L. |
zócalo · zocalo | pvc-zocalo-100 | Zócalos PVC 100 mm. |
varilla · varilla 25 · varilla 30 | varilla-placard-25 / varilla-placard-30 | Varillas de placard. |
tirante · alfajía · alfajia | pino-2x3 | Tirantes y alfajías de techo. |
tapacanto | aluminio-tapacanto-22 | Tapacantos de aluminio. |
Cada keyword tiene también aliases localizados en
aliasesByLocale (es-AR, en-US,
pt-BR, fr-FR, de-DE,
it-IT) para que varilla matchee también
closet rod / cabideiro /
tringle penderie / Kleiderstange /
asta armadio. El matching sigue siendo
longest-key + folding; los aliases viven en los perfiles, no en
las keywords.
4 · Catálogo built-in (vista Catálogo del panel)
El panel [L] tiene una vista Catálogo
(segmented al lado de Stock) que muestra los perfiles
disponibles en modo read-only. La edición se hace en
Configuración → Materiales (customs) o desde el catálogo
lineal (src/lineales/stock-store.ts).
Los 11 perfiles built-in viven en src/lineales/types.ts:136-146:
| ID | Familia | Etiqueta | Perfil | Largo nominal | Material |
|---|---|---|---|---|---|
pino-2x2 | madera | Pino FK 2×2″ | 50×50 mm | 2400 mm | maciza_pino_fk |
pino-2x3 | madera | Pino FK 2×3″ | 50×75 mm | 2400 mm | maciza_pino_fk |
pino-3x3 | madera | Pino FK 3×3″ | 75×75 mm | 2400 mm | maciza_pino_fk |
eucalipto-2x3 | madera | Eucalipto 2×3″ | 50×75 mm | 2400 mm | maciza_eucalipto |
paraiso-2x3 | madera | Paraíso 2×3″ | 50×75 mm | 2400 mm | maciza_paraiso |
machimbre-1x4 | machimbre | Machimbre 1×4″ | 25×100 mm | 2400 mm | maciza_machimbre |
aluminio-l-1 | aluminio | Aluminio L 1″ | 25×25 mm | 3000 mm | aluminio_perfil |
aluminio-tapacanto-22 | aluminio | Tapacanto aluminio 22 mm | 22×600 mm | 3000 mm | aluminio_tapacanto |
pvc-zocalo-100 | pvc | Zócalo PVC 100 mm | 100×15 mm | 2400 mm | pvc_zocalo |
varilla-placard-25 | otro | Varilla placard Ø25 | 25×25 mm | 2400 mm | varilla_placard |
varilla-placard-30 | otro | Varilla placard Ø30 | 30×30 mm | 2400 mm | varilla_placard |
Los built-ins son inamovibles — viven en el código, no en
localStorage. Si necesitás un perfil custom (ej.
Pino tratado 2×4″), agregalo vía addLinealProfile
desde el panel de stock catalogado (cap 200 customs en
stock-store.ts:13).
5 · Abrir el panel [L] (Shift+L)
[L] recién abierto, vista Stock con el wizard + Listón.
Cargar un modelo CAD (cualquier STEP/IGES/BREP/glTF/GLB), pulsar
Shift+L o click en el botón [L]
del topbar (id #tb-lineales). El panel se abre como
un sheet full-screen (min(1100px, 92vw) × 86vh) con
un segmented Stock | Catálogo.
El shell vive en src/ui/lineales-panel.ts. La vista
por default es Stock; el switcher es el
nav.lineales-seg con dos botones
.lineales-seg-btn[data-view="stock|catalogo"]. El
header expone:
titleconkicker"Listones" + nombre del proyecto activo.metacon el contador de piezas y barras ({count} · Barras: {bars}).- Exportar (CSV/XLSX/PDF) y la
×de cerrar.
Si el módulo está apagado en Settings, el botón [L]
no se renderiza y el atajo Shift+L muestra
un transient:
Módulo Lineales desactivado · activalo en Settings → Lineales
6 · + Listón · wizard manual
El título del wizard es + Listón y su botón de submit
(#lineales-add, etiqueta "Agregar") toma cuatro campos
del #lineales-w-* form. La validación
(stock-view.ts:70):
if (!profile || !Number.isFinite(length) || length <= 0
|| !Number.isFinite(qty) || qty <= 0) return;
| Campo | Selector | Validación |
|---|---|---|
| Nombre | #lineales-w-name |
trim, no vacío; si está vacío cae al profileId. |
| Perfil | .herraje-select[data-field="lineales-w-profile"] |
id del catálogo (11 built-in + customs). Default = defaultProfileId de Settings. |
| Largo (mm) | #lineales-w-length |
número entero > 0. Math.round(length) antes de guardar. |
| Cantidad | #lineales-w-qty |
entero ≥ 1. Math.floor(qty) antes de guardar. |
Al click Agregar se llama
addLinearItem({...source: 'wizard'}) en
src/state/lineales-slice.ts:22. La función genera
id = uuid() si falta, valida con
isLinearStockLine (defensa contra shape inválido), y
bumpea linearEpoch (la vista Stock se re-renderiza
vía on(state)).
Internamente el item nuevo es un LinearStockLine:
interface LinearStockLine {
id: string;
parentPartId?: string;
name: string;
profileMm: { w: number; h: number }; // snapshot del perfil
lengthMm: number; // largo del corte pedido
materialId: string; // snapshot del material
qty: number;
source: 'cad' | 'wizard' | 'csv' | 'auto-detect';
}
El item no clona el perfil — copia profileMm
por valor y referencia materialId por id. Si después
se renombra el material en Configuración → Materiales,
materials/rewrite-refs.ts:124 reescribe el
materialId en todos los lineales al cargar el
proyecto (consistencia con el resto de la app).
7 · Importar CSV (Despiece-lineal y OptiCut-lineal)
El botón Importar CSV… (#lineales-import-csv)
abre un picker nativo y acepta .csv / .tsv.
El parser (src/lineales/parse-csv.ts) detecta
dos dialectos mirando el header (case-insensitive,
lowercase) y maneja BOM UTF-8:
| Dialecto | Header | Detección |
|---|---|---|
despiece-lineal |
cantidad;nombre;profile_mm;largo_mm;material |
header contiene profile_mm o largo_mm |
opticut-lineal |
quantità;descrizione;lunghezza_mm;materiale |
header contiene descrizione o lunghezza |
unknown |
(cualquier otro) | igual parsea con alias genéricos si las columnas matchean |
El separador también se autodetecta (detectSep línea
37): si el header tiene ;, sep = ;, si
no ,. La columna profile_mm acepta
, o . como decimal y x o
× como separador, vía regex
^(\d+(?:[.,]\d+)?)\s*[x×]\s*(\d+(?:[.,]\d+)?)$/i
(línea 57).
Ejemplo válido (/tmp/lineales-demo.csv):
cantidad;nombre;profile_mm;largo_mm;material
4;Listón pino 2×3 refuerzo;50×75;1800;maciza_pino_fk
2;Machimbre 1×4 cielorraso;25×100;2200;maciza_machimbre
8;Varilla placard 25 closet;25×25;1200;varilla_placard
Cada fila se agrega con addLinearItem({..., source: 'csv'}).
Las filas inválidas (qty ≤ 0, profile sin match,
length no numérico) van a errors[] y no rompen el
parse — el resto del archivo entra.
| Campo | Aliases de columnas (lowercase) |
|---|---|
qty | cant, qty, quantità, quantita |
name | nombre, name, descrizione, desc |
profile | profile_mm, profile, profilo_mm, profilo |
length | largo_mm, largo, length_mm, lunghezza_mm, lunghezza |
Si falta alguna columna obligatoria, el parser devuelve
errors: ['Faltan columnas: cant/qty, nombre/name, profile_mm, largo_mm']
y no agrega nada. La columna material del CSV es
opcional — si está vacía, el item se guarda con
materialId = 'maciza_pino_fk' (fallback de pino FK
para no romper el lookup downstream).
8 · Auto-detectar del modelo
El botón Auto-detectar del modelo (#lineales-auto)
corre selectLinealMatches(parts, keywords) y agrega
un item por cada match con source: 'auto-detect'.
La pieza original del CAD no se muta — solo se crea un
LinearStockLine con
parentPartId: m.part.id y
lengthMm = Math.round(max(profile.nominalLengthMm, 100) / 4)
(un cuarto del largo nominal como heurística de corte inicial).
El cuarto de largo nominal es solo un starting point — el
usuario edita el lengthMm directamente en la fila
de stock. Para CocinaV9 el catálogo de nombres es estándar
(puerta, estante, cajón) y el auto-detect
no agrega items porque ninguno matchea las keywords.
El flag addLinearItem bumpea linearEpoch;
la vista Stock se re-renderiza mostrando los items nuevos. Si la
pieza ya tenía un item lineal previo (mismo
parentPartId), el nuevo se acumula — no se
deduplica (decisión del spec §3.13: auto-detect es
sugerir, no sincronizar).
9 · Calcular barras (packer FFD 1D + multi-restart)
El botón Calcular barras (#lineales-pack)
corre packLinealStock(items, spec)
(src/lineales/packer.ts:164) sobre la lista actual.
El algoritmo es:
- Expand cada
LinearStockLinepor suqty→ flat array deExpandedCut[](línea 147). - FFD inicial =
sort((a,b) => b.lengthMm - a.lengthMm)— primer ajuste decreciente (línea 171). - Multi-restart hasta 32 variantes o 250 ms. La RNG es
makeRng(Mulberry32-like enpacker.ts:51) y el barajado se aplica conshuffleSeeded(packer.ts:61). - Greedy placement (
placeRunslínea 77): mantiene unslot[]por barra viva con{barId, nextStart}; para cada cut busca el primer slot dondenextStart + cut.lengthMm + kerfMm ≤ spec.lengthMm. Si no entra, abre una barra nueva con kerf = 0 en el primer cut. - Best of N — elige la variante con menor waste total; desempata por menor
totalBars(líneas 192-201). - Output inmutable:
LinearResultconbars,cuts,unusedByBar,wasteMmByBar,totalMl,totalBars,cfg.
El spec.bar se construye a partir del primer item:
profileId + lengthMm = profile.nominalLengthMm
(largo nominal del catálogo). Todos los items del pack deben
compartir perfil — si mezclás listones de 50×75 con varillas
de 25×25, el packer aplica el perfil del primero a todos (con un
warning por consola si hay mismatch).
Workbench (src/ui/lineales-panel/workbench.ts):
- Una banda horizontal por barra, con
width = (lengthMm / maxLength) × 100%. - Los cortes son bloques cobalt (
var(--cobalt)) con texto embebido (name · lengthMm). - El remanente es un gradiente rayado 45° sobre el tramo no ocupado (entre el último corte y
bar.lengthMm). - El label arriba (
bar-N · 50×75 · 2400 mm) y el stats abajo (N cortes · total mm usado · M mm retazo).
El remanente está representado visualmente pero no se
trackea como remnantOfBarId en el output — solo
queda en unusedByBar[barId] como
{startMm, lengthMm} para futuro uso (reutilización
de retazos entre obras). El spec §3.10 lo marca como pendiente.
10 · Exportar (CSV · XLSX · PDF) + atajos
El botón Exportar ([data-action="export-toggle"])
del header abre un popover con tres opciones. Cada handler vive
en src/export/actions-linear.ts y se cablea vía
setLinealesHandlers({ onExport }) en
src/ui/lineales-panel.ts:33:
| Formato | Columnas / output | Notas |
|---|---|---|
| CSV | cantidad;nombre;profile_mm;largo_mm;material;ml |
UTF-8 BOM + CRLF. Sin header en TSV de corte. ml = lengthMm × qty / 1000. |
| XLSX | Segunda hoja "Lineales" del BOM | exceljs dynamic import (no entra al bundle inicial). |
| Etiquetas de barras | Toggle linearLabelsBreakdown decide si es 1 etiqueta por barra con desglose de cortes, o 1 por corte. |
El export respeta state.settings.exportVisibleOnly
igual que el resto de la app (default true). Si el
módulo está apagado, los 4 items del menú Export se ocultan vía
src/ui/topbar/sections/state-apply.ts:146-166.
Atajos del flujo
| Atajo | Acción |
|---|---|
| Shift+L | Abrir/cerrar el panel [L] (Mac = metaKey, otros = ctrlKey) |
| Esc dentro del panel | Cerrar el sheet (vuelve al viewport) |
| ? / F1 | Abrir la hoja de atajos completa (incluye el link a este tutorial) |
| ⌘+E / Ctrl+E | Menú Exportar (CSV / XLSX / PDF · BOM · Pedido · Etiquetas · Listones CSV/XLSX/PDF) |
| F / Home | Fit / Fit + iso del viewport 3D |
El panel [L] no captura el atajo L
simple (reservado para futuro List view del sidebar). El
modificador Shift lo desambigua y deja el shortcut
simple libre.
Pipeline integrado
El LinearResult se cablea hacia abajo en cuatro
destinos:
| Downstream | Comportamiento | Módulo |
|---|---|---|
| BOM CSV / XLSX / PDF | buildLinearBomRows agrega las filas de lineales con cantidad;nombre;profile_mm;largo_mm;material;ml. En XLSX es la segunda hoja Lineales. |
src/export-bom/build-bom-rows.ts |
| Pedido de materiales | buildLinearPurchaseRows suma costPerBar × totalBars por perfil × material. Aparece como bloque Barras lineales al final del PDF/CSV de pedido. |
src/export-purchase/linear-purchase-rows.ts:17 |
| Etiquetas PDF | export-labels.ts chequea linearLabelsBreakdown; cuando está activado, una etiqueta por barra con desglose de cortes pegados; cuando está apagado, una etiqueta por corte. |
src/export-labels.ts |
| Total de obra | El panel [L] muestra ml lineal en el header (suma de lengthMm × qty / 1000). El multiplicador ×N aplica antes del pack (clamp 1–99). |
recalculateIfResult() |
Multiplicador de obra ×N: si
state.settings.projectMultiplier = 3 y tenés 4
listones ×2400, el packer recibe 12 items en lugar de 4 (no
clona meshes 3D — las lineales no tienen). El contador de barras
se recalcula automáticamente vía recalculateIfResult()
(lineales-panel.ts:174), que re-corre el pack si hay
linearResult previo en disco.
En Mac el modificador de sistema es ⌘; en otros sistemas Ctrl.
Ver también
- Tutorial · Glue-up y auto-glue-up — vecino del grupo Auto-fill (mismo flujo B / Shift+B / Shift+G / Shift+L).
- Tutorial · Materiales — el catálogo de materiales con el que se arman listones, almas y tapas.
- Tutorial · Roles y auto-roles — el otro discriminador de pieza que también se aplica vía nombre.
- Manual § Lineales (
docs/manual/src/09-panel-lineales.md) — referencia corta del módulo. - Spec
2026-07-18-lineales-dimensioned-lumber.md— modelo de datos y decisiones cerradas del módulo.