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.

El modelo de datos es un string de taller canónico "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

Vista general con el chip de material en una fila
01 Chip de material en cada fila. El primer chip del grupo acabado es un cuadrado de 26×26 px con un dot de color (swatch del material asignado). Sin asignar, queda apagado con el ícono del material genérico.

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

Picker de materiales
02 Picker modal. Tres secciones: Proyecto (los materiales ya usados en otras piezas del modelo cargado), Recientes (MRU 30 que tocaste), Catálogo (built-ins + customs del taller, agrupados por tipo de tablero). Buscador arriba con 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ónOrigenLímite
ProyectoprojectMaterialsFromParts(parts) — los materiales únicos ya asignados en el modelo cargado.sin límite explícito
RecienteslocalStorage[despiece.materials.v1].recent (MRU).30 (MAX_RECENT)
CatálogolistMaterialDefs() = 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).

Click afuera del picker (scrim) o Esc lo cierra sin aplicar nada (devuelve 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

Form Otro con toggle Color|Textura abierto
02b Crear material al vuelo. El botón + 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

Asignar material a una pieza individual
03 Asignación individual. Click en el chip → picker → elegir. Se aplica con un único 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

Asignar material a múltiples piezas seleccionadas
04 Asignación bulk. Con ≥ 2 piezas seleccionadas, click en cualquiera de sus chips de material abre el picker una sola vez; el resultado se aplica a todas. Equivalente: botón derecho del sidebar > «Material…».

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.

Para limpiar el material de la selección, botón derecho > «Limpiar material». Internamente clearMaterialFromSelection setea part.material = '' en una sola entrada de history.

06 Configuración → Materiales

Tab Materiales de Configuración
05 Tab Materiales del taller. Lista unificada de customs + built-ins (sin duplicar), con swatch + simil + tipo. Cada fila custom tiene botones y ×; las built-ins muestran un badge fijo. Debajo: Recientes con botón Vaciar.

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:

ZonaSelectorFunción
Hint#stg-materials > .stg-hintRecordatorio de cómo armar un material: tipo + simil + color (con sample Faplac).
+ Agregar material#stg-mat-addAbre el form inline (#stg-mat-form) oculto por default.
Lista#stg-mat-listCada fila: swatch + simil + tipo + acciones. Customs con /×; built-ins con badge fijo.
Recientes#stg-mat-recentMRU 30 (chips clickeables). Vaciar los borra todos (clearRecentMaterials).
Los customs persisten en 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

Lista de materiales built-in
06 26 built-ins. 11 melamina MDP + 4 melamina MDF + 1 MDF crudo + 2 fondo + 9 listonado macizo (Pino FK / Eucalipto / Paraíso en 18 y 24 mm). Cada uno con swatch de referencia y persistencia «fijo» (no editable).

BUILTIN_MATERIAL_DEFS en src/materials/catalog.ts lleva 26 entradas:

TipoBuilt-insRole default
melamina_mdp11 (Blanco TX / mate / brillo / Gris perla / lino / cemento / Negro / Roble / Nogal / Petiribí / Wengue)face
melamina_mdf4 (Blanco TX / Gris lino / Nogal / Roble)face
mdf_crudo1 (Crudo)face
fondo2 (Blanco / Negro)fondo
maciza_pino_fk2 (Listonado 18 / 24 mm)core
maciza_eucalipto2 (Listonado 18 / 24 mm)core
maciza_paraiso2 (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)

Form de creación de material
07 Form inline. Selector de tipo de tablero + input simil + color picker (default #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:

CampoSelectorValidación
Tipo de tablero#stg-mat-kinduno de los 7 BoardKind built-in; melamina_mdp por default.
Simil / acabado#stg-mat-finishtexto, máx 80 chars, trim + colapsar whitespace, no vacío.
Color#stg-mat-swatchcolor picker HTML5, default #cccccc; se normaliza a hex6 minúsculas.
Costo $/m² (opcional)#stg-mat-costnú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).

Si intentás guardar un simil que matchea un built-in Faplac (ej. Blanco TX en melamina_mdp), el form rechaza con Ese material ya está en el catálogo fijo (chequeo isBuiltinMaterialName).

09 Renombrar / borrar custom

Editar y borrar materiales custom
08 Editar (✎) y borrar (×). Editar abre el mismo form precargado con #stg-mat-edit-id populated. Borrar quita del catálogo y remapea piezas que apuntaban a su label anterior.

Dos botones por fila custom:

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);
  }
}
El rename entra como una sola entrada de history (no dos separadas), así un undo revierte tanto el catálogo como las N piezas en un paso.

10 Recientes y persistencia

Sección de recientes
09 Recientes. MRU 30. Cada vez que elegís un material en el picker se inserta al tope; las apariciones duplicadas se mueven (no se duplican). Vaciar borra toda la lista (no toca customs ni el modelo).

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:

  1. Trim + colapsar whitespace al display string.
  2. Buscar key lowercase en recent; si existe, sacarlo.
  3. Insertar al tope; truncar a MAX_RECENT = 30.
  4. Persistir.

Vaciar (#stg-mat-clear-recent) llama clearRecentMaterials que setea recent: []. No toca favorites ni custom.

Como favorites es un campo reservado del schema pero todavía sin UI, los customs son la forma principal de «fijar» un material: aparece siempre en el picker y no se va con Vaciar recientes.

11 Integración con Nest · BOM · Etiquetas

Hoja de atajos
10 Hoja de atajos. 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:

DownstreamComportamientoMó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:

AtajoAcción
Click en chip de materialAbrir picker (1 pieza).
RMB en chip / filaMenú contextual con Material… y Limpiar material.
F / HomeFit / Fit + iso del viewport 3D.
? / F1Abrir la hoja de atajos completa.

En Mac el modificador de sistema es ; en otros sistemas Ctrl.


Ver también