Materiales del taller
Tutorial paso a paso del catálogo de materiales de Despiece: el chip de la fila plana, el picker con proyecto / recientes / catálogo, el tab Materiales del taller en Configuración (catálogo built-in + customs + recientes), y cómo alimenta al Nest / BOM / Pedido / Etiquetas.
"Melamina MDP · Petiribí" (tipo + simil) por pieza,
persistido en parts.materials?: Record<partId, string>.
El catálogo vive en localStorage[despiece.materials.v1]
con recent (MRU 30), favorites y custom
(≤ 80). Matching por normalizeMaterialKey (trim +
colapsar whitespace + lowercase).
01 Cargar el modelo y el chip de material
Al importar el STEP/IGES/BREP/glTF/GLB, la sidebar izquierda muestra el
árbol jerárquico. En la lista plana, cada pieza expone un chip de
material (selector .sb-material-btn) en la misma línea
que el de rol, veta y cantos.
Internamente el chip vive en src/ui/product-tree/row-chips.ts:
buildMaterialButton(part) lee
part.material, consulta swatchForMaterial()
para el color, y renderiza un dot del color del catálogo. Si no
hay material asignado, el chip queda vacío (sin dot) y se ve el
ícono de la paleta en gris.
02 Picker: proyecto · recientes · catálogo
suggestMaterials.
Click en el chip de material abre el picker (handler en
src/ui/material-picker.ts:openMaterialPicker). El picker
arma tres listas con listMaterialChoices():
| Sección | Origen | Límite |
|---|---|---|
| Proyecto | projectMaterialsFromParts(parts) — los materiales únicos ya asignados en el modelo cargado. | sin límite explícito |
| Recientes | localStorage[despiece.materials.v1].recent (MRU). | 30 (MAX_RECENT) |
| Catálogo | listMaterialDefs() = customs + 26 built-ins (Faplac-style) no overridden. | 80 customs (MAX_CUSTOM) + 26 built-ins |
El buscador usa suggestMaterials(q) que ordena por
score (exacto 300, prefix 200, contains 100, sólo finish 90/180/250).
Cuando elegís un material, el picker llama canonicalizeMaterial
para normalizar el string (a veces un built-in tiene un whitespace
distinto del custom).
null). El handler en
apply-material.ts:pickAndApplyMaterial detecta esa
cancelación y no toca el modelo.
03 Otro: crear material al vuelo con toggle Color | Textura
+ Otro… del picker expande un form inline con toggle Color | Textura (XOR). El mismo form vive en Configuración → Materiales; los customs creados desde cualquiera de los dos aparecen inmediatamente en el catálogo y en Recientes.
04 Asignar a una pieza
withHistory('set-material'). La pieza queda con su swatch en el dot y el material aparece arriba en Recientes del picker.
El flujo de una sola pieza vive en
src/ui/apply-material.ts:pickAndApplyMaterial:
// Click en el chip → const next = await openMaterialPicker({ current: primary.material ?? '', projectMaterials: projectMaterialsFromParts(parts), applyCount: targets.length, }); if (next === null) return; // cancelado applyMaterialToIds(targets, next);
applyMaterialToIds arma un solo entry de
history (withHistory('set-material')), así un undo
deshace toda la asignación de una. Internamente
setPartMaterial(id, material) actualiza
state.model.parts[i].material (string) y dispara
la re-render del árbol.
El picker también hace pushRecentMaterial(display)
automáticamente — el material elegido sube al tope de
Recientes en la próxima apertura.
05 Asignar a toda la selección
El mismo pickAndApplyMaterial detecta si la pieza clickeada
es parte de la selección múltiple y abre el picker con todos los
ids seleccionados como target:
const selected = state.selectedPartIds.filter(...); const targets = selected.length > 1 && selected.includes(primaryId) ? selected : [primaryId]; // picker.applyCount = targets.length → muestra "Aplicar a N piezas"
Si tu selección es de un color distinto al de la primera pieza, el
picker muestra el current del primary (no un mixto) —
pero el material que elijas reemplaza los N strings en una sola
operación atómica de history.
clearMaterialFromSelection setea
part.material = '' en una sola entrada de history.
06 Configuración → Materiales
El botón Configuración de la topbar (icono engranaje) abre
el panel lateral. La primera sección del tab Materiales del
taller (id #stg-materials) está implementada en
src/ui/settings-panel/materials-ui.ts.
El tab tiene tres zonas:
| Zona | Selector | Función |
|---|---|---|
| Hint | #stg-materials > .stg-hint | Recordatorio de cómo armar un material: tipo + simil + color (con sample Faplac). |
| + Agregar material | #stg-mat-add | Abre el form inline (#stg-mat-form) oculto por default. |
| Lista | #stg-mat-list | Cada fila: swatch + simil + tipo + acciones. Customs con ✎/×; built-ins con badge fijo. |
| Recientes | #stg-mat-recent | MRU 30 (chips clickeables). Vaciar los borra todos (clearRecentMaterials). |
localStorage[despiece.materials.v1]
(version: 3) y se incluyen en Configuración → Exportar
(zip/json). El logo del taller también se incluye ahí.
Importar otro equipo no pisa los customs locales — solo añade.
07 Catálogo built-in Faplac-style
BUILTIN_MATERIAL_DEFS en src/materials/catalog.ts
lleva 26 entradas:
| Tipo | Built-ins | Role default |
|---|---|---|
melamina_mdp | 11 (Blanco TX / mate / brillo / Gris perla / lino / cemento / Negro / Roble / Nogal / Petiribí / Wengue) | face |
melamina_mdf | 4 (Blanco TX / Gris lino / Nogal / Roble) | face |
mdf_crudo | 1 (Crudo) | face |
fondo | 2 (Blanco / Negro) | fondo |
maciza_pino_fk | 2 (Listonado 18 / 24 mm) | core |
maciza_eucalipto | 2 (Listonado 18 / 24 mm) | core |
maciza_paraiso | 2 (Listonado 18 / 24 mm) | core |
Los built-ins viven en el código (src/materials/catalog.ts),
no en localStorage — son inamovibles. Si querés un simil
custom (Roble español, Blanco nórdico, etc.), agregalo
como custom (paso 7). El badge fijo en la fila lo deja claro.
08 Crear un material custom (con toggle Color|Textura)
#cccccc) + número costo $/m² (opcional). El error inline (duplicate / empty / limit) aparece arriba de los botones Cancelar / Guardar.
+ Agregar material abre el form inline con cuatro campos:
| Campo | Selector | Validación |
|---|---|---|
| Tipo de tablero | #stg-mat-kind | uno de los 7 BoardKind built-in; melamina_mdp por default. |
| Simil / acabado | #stg-mat-finish | texto, máx 80 chars, trim + colapsar whitespace, no vacío. |
| Color | #stg-mat-swatch | color picker HTML5, default #cccccc; se normaliza a hex6 minúsculas. |
| Costo $/m² (opcional) | #stg-mat-cost | número decimal, ≥ 0; vacío = sin costo (no bloquea). |
Guardar llama addCustomMaterial({kind, finish, swatch, costPerM2})
en src/materials/store.ts. La función devuelve
{ok:false, error:'duplicate' | 'empty' | 'limit'} si
algo no cierra (límite 80 customs; duplicado por kind+finish
case-insensitive; simil vacío).
melamina_mdp), el form
rechaza con Ese material ya está en el catálogo fijo
(chequeo isBuiltinMaterialName).
09 Renombrar / borrar custom
#stg-mat-edit-id populated. Borrar quita del catálogo y remapea piezas que apuntaban a su label anterior.
Dos botones por fila custom:
- ✎ (
data-mat-edit) →showForm(root, def); el form se abre con todos los campos pre-poblados y#stg-mat-edit-idcon el id del def. Guardar en este caso llamaupdateCustomMaterial(id, patch), que valida duplicate / empty y vuelve a persistir. - × (
data-mat-del) →removeCustomMaterial(id): saca del catálogo y remapea cualquier entrada derecentofavoritesque apuntaba al label viejo.
Lo importante: al renombrar (cambiar simil o tipo) el sistema reescribe automáticamente todas las piezas del modelo cargado que tenían el label anterior. Esto evita huérfanos silenciosos:
// src/ui/apply-material.ts if (r.previousName !== r.def.name) { const n = rewriteMaterialsInLoadedModel(r.previousName, r.def.name); if (n > 0) { setTransient(`${n} piezas actualizadas con el material renombrado`, 3000); } }
10 Recientes y persistencia
El catálogo se persiste en localStorage bajo la
clave despiece.materials.v1 con la forma:
interface MaterialCatalogV3 { version: 3; recent: string[]; // MRU 30, string canonico "Melamina MDP · Petiribí" favorites: string[]; // reservado (sin UI v1) custom: MaterialDef[]; // hasta 80, editable }
La función pushRecentMaterial(display) en
src/materials/store.ts hace lo siguiente:
- Trim + colapsar whitespace al display string.
- Buscar key lowercase en
recent; si existe, sacarlo. - Insertar al tope; truncar a
MAX_RECENT = 30. - Persistir.
Vaciar (#stg-mat-clear-recent) llama
clearRecentMaterials que setea recent: [].
No toca favorites ni custom.
11 Integración con Nest · BOM · Etiquetas
Help (botón de la topbar o ?) abre la hoja completa. Materiales no tiene atajo dedicado — todo va por el chip de la fila o Configuración.
El material asignado a cada pieza fluye hacia abajo de tres formas distintas. Es importante entender las tres para no mezclar colores en el nest sin querer:
| Downstream | Comportamiento | Módulo |
|---|---|---|
| Nest (multi-restart) | partsToNestItems agrupa por material × espesor y crea tableros separados por color. La pieza sin material va a Sin material y queda suelta. | src/nesting/pack-async.ts |
| BOM (CSV / XLSX / PDF) | Agrupa piezas por material + espesor + dimensiones; incluye columna modelo con el label canónico. | src/export-bom.ts |
| Pedido de materiales (PDF / CSV) | Si el catálogo tiene costPerM2, lo usa para calcular costo total por material. Sin costo → columna vacía. | src/export-purchase.ts |
| Etiquetas PDF (cantos) | Header de la etiqueta incluye material del catálogo (lookup canonicalizeMaterial). | src/export-labels.ts |
Atajos del flujo de materiales:
| Atajo | Acción |
|---|---|
| Click en chip de material | Abrir picker (1 pieza). |
| RMB en chip / fila | Menú contextual con Material… y Limpiar material. |
| F / Home | Fit / Fit + iso del viewport 3D. |
| ? / F1 | Abrir la hoja de atajos completa. |
En Mac el modificador de sistema es ⌘; en otros sistemas Ctrl.
Ver también
- Tutorial · Cantos y auto-cantos
- Tutorial · Roles y auto-roles
- Tutorial · Optimización de corte
- Tutorial · Plano de taller
- Spec 2026-07-15-material-purchase-labels.md — modelo de material + lista de compra + etiquetas.