Herrajes (Pedido · Catálogo · Reglas)

Tutorial paso a paso del panel de Herrajes de Despiece: armar el pedido con su catálogo de SKUs y un motor de reglas que mapea rol de pieza × intervalo mm a cantidad automática. Se abre desde el botón Herraje de la topbar (no tiene atajo de teclado propio — la callback onHerraje solo está cableada al #tb-herraje y al item de menú correspondiente).

El modelo de datos vive en dos localStorage aparte del proyecto (despiece.herrajeria.v1 con 29 SKUs default + customs, despiece.hardware-rules.v1 con 7 reglas default). El pedido por proyecto está en parts.hardwareList con líneas {skuCode, qty, origin: 'manual' | 'auto'}. Aplicar reglas regenera solo las líneas auto; las manual quedan intactas.

§ 0 Modelo mental: tres capas para responder "¿cuántas bisagras tengo que pedir?"

Antes de meternos en la UI, vale la pena tener clara la pregunta de negocio. Cuando un taller importa un modelo con 12 puertas y 6 estantes y abre Excel paralelo, lo que está haciendo a mano es:

  1. Recorrer las puertas una por una, medir la altura y ver en qué tramo cae (2 bisagras hasta 1500 mm, 3 hasta 2200 mm, 4 por encima) para saber cuántas bisagras lleva cada una.
  2. Sumar un tirador por puerta, sumar 4 pines por estante, etc.

El módulo de Herrajes hace exactamente eso, pero con datos configurables en vez de a ojo. Hay tres capas independientes:

┌─────────────────────────────────┐   ┌──────────────────────────────────┐
│ Catálogo (localStorage taller)  │   │ Reglas (localStorage taller)     │
│  HerrajeItem.code/name/cost/cat │   │  HardwareRule.role × [min,max)   │
│                                 │   │              → sku × qtyPerPart  │
└────────────┬────────────────────┘   └─────────────┬────────────────────┘
             │                                      │
             └──────────────────┬───────────────────┘
                                ▼
              applyHardwareRules() ← parts.partRole + dims
                                │
                                ▼
              parts.hardwareList: HardwareLine[]
                                │
        ┌───────────┬───────────┼───────────┬─────────────┐
        ▼           ▼           ▼           ▼             ▼
     Pedido    Etiquetas   Presupuesto  Export CSV   Export XLSX

Las tres capas viven en lugares distintos (catálogo y reglas son del taller; pedido es del proyecto). Eso permite que cambies un precio en el catálogo y se recalculen subtotales sin tocar reglas, y que cambies una regla y se regenere el pedido sin tocar el catálogo.

01 Cargar el modelo y abrir Herraje

Vista general con el botón Herraje en la topbar
01 Botón Herraje en la topbar. Grupo 5 (Cómputo): el segundo icono desde la izquierda. Queda deshabilitado hasta cargar un modelo. No hay atajo de teclado propio — solo se abre desde el botón o desde el menú hamburguesa.

Al importar el STEP/IGES/BREP/glTF/GLB, el botón Herraje de la topbar se habilita. La primera vez que abrís Herraje en este proyecto, el panel se inicializa con:

Cuándo se inicializan catálogo y reglas: la primera vez que abrís el panel en una sesión, loadHerrajeriaCatalog() y loadHardwareRules() leen localStorage; si está vacío, devuelven los defaults sin red ni sincronización. Una vez cargado, todo lo que edites ahí se persiste en este navegador.

El panel es full-screen modal con shell .herraje-sheet y tres vistas segmentadas Pedido · Catálogo · Reglas. Las vistas son exclusivas — cambiar de tab destruye y re-monta el contenido de la vista target (en vez de tenerlas apiladas).

02 Vista Pedido: lista del proyecto

Vista Pedido con líneas del proyecto
02 Vista Pedido (default). Toolbar arriba: buscador + autocomplete, + Línea, Aplicar reglas, switch Plano / Por mueble. Abajo, tabla con SKU · Nombre · Cant · Unidad · Costo · Subtotal · Acciones.

La vista Pedido muestra state.project.hardwareList con columnas SKU · Nombre · Cant · Unidad · Costo unit · Subtotal · Notas. La toolbar tiene:

Botón / selectorAcción
#herraje-list-searchBuscador live con autocomplete contra el catálogo (código + nombre).
data-action="add"+ Línea: inserta una fila nueva vacía para que escribas el SKU manualmente.
data-action="apply-rules"Aplicar reglas (paso 3).
data-action="groupby-flat" | "groupby-assembly"Switch Plano / Por mueble (subtotal por assembly). En Por mueble, las filas de una línea auto repartida entre dos o más muebles son de sólo lectura.

Cada línea tiene una provenance badge: auto (lila / teal, según el tema) indica que la línea fue generada por aplicar reglas; manual (gris) indica que la escribiste a mano. Las dos conviven en la misma tabla sin separarse.

Arriba de la tabla puede aparecer un aviso: el listado guardado ya no coincide con las reglas o las piezas actuales. Es el recordatorio de volver a apretar Aplicar reglas para regenerar las líneas auto.

2.1 · Anatomía de una HardwareLine

Detrás del row visible hay un objeto HardwareLine con estos campos:

CampoTipoOrigenQuién lo edita
iduuid v4generado al crearinmutable
skuCodeFK string al HerrajeItem.codecatálogo (FK) o tipeadomanual al crear
qtynumber ≥ 1manual o automanual + Aplicar reglas
origin'manual' | 'auto'sistemasólo Apply lo cambia
notesstring opcionalmanualmanual de la línea
unitCostnumber opcional (override)manualmanual de la línea
Por qué el badge auto no es decorativo: es el dato que usa applyHardwareRules para saber qué líneas descartar antes de regenerar (apply-rules.ts:112-116: existing.filter(l => l.origin !== 'auto')). Si ves un row auto mal en tu CSV de proveedor, la solución no es borrarlo a mano — es ajustar la regla, volver a aplicar y dejar que el motor lo regenere.

03 Aplicar reglas — el motor de 7 fases

Aplicar reglas regenera las líneas auto
03 Aplicar reglas. Botón Aplicar reglas de la toolbar → regenera líneas auto a partir de las piezas del modelo + las reglas del catálogo. Líneas manual se preservan intactas.

El botón llama runApplyHardwareRules() (src/herrajerie/run-apply.ts:24-49) que termina en applyHardwareRules(...) en src/herrajerie/apply-rules.ts. El motor recorre 7 fases, cada una con un rol claro.

3.1 · Clamp del multiplicador (apply-rules.ts:108-110)

const qtyMul = quantityMultiplier
  && Number.isFinite(quantityMultiplier)
  && quantityMultiplier > 1
  ? Math.floor(quantityMultiplier)
  : 1;

Defensivo: aunque el caller pase 0, NaN, 2.7 o negativo, el motor trata todo lo "no es entero ≥ 2" como 1 (sin escalado). Esto preserva la compat con proyectos y tests pre-multiplier (ver spec 2026-07-17-herraje-multiplier.md).

3.2 · Loop exterior: por cada pieza (apply-rules.ts:132-182)

Filtros en orden, cortocircuitando en cuanto uno falla:

  1. visibleOnly && part.object?.visible === false → skip (respeta el flag de Settings → Export).
  2. role = part.partRole ?? 'none'; si es 'none' o vacío → skip (la pieza no aporta nada al herraje).

3.3 · Cómputo de X (apply-rules.ts:138)

const x = boardDimensions(
  part.dimXmm, part.dimYmm, part.dimZmm
).x;

x es la cara mayor del AABB de la pieza (boardDimensions() en src/export-bom/board-dimensions.ts). Una puerta 600×1800×18 → X=1800, Y=600. Justificación de diseño: las reglas de bisagras son por altura de puerta, y X resulta ser la dimensión mayor de la cara. Limitación documentada: la spec v2 sólo cubre X; una futura v3 podría cubrir Y (ancho) y Z (espesor).

3.4 · Loop interior: por cada regla (apply-rules.ts:142-179)

Para cada pieza, se iteran las reglas aplicando filtros en orden:

  1. !rule.enabled → skip. Las reglas deshabilitadas ni aportan qty ni participan en el overlap check (sólo "reservan" rangos futuros).
  2. role !== rule.role → skip. La pieza no es del rol que la regla matchea.
  3. seenSku.has(rule.skuCode) → skip. Defensa runtime: si dos reglas enabled matchean el mismo SKU en la misma pieza (no debería pasar si la validación de solapes está OK), gana la primera en orden del array.
  4. !inRange(x, rule.minMm, rule.maxMm) → skip. Intervalo half-open [min, max) (semi-abierto: minMm ≤ x < maxMm).
  5. !knownSkuCodes.has(rule.skuCode)skippedOrphanSku++, continuar. Una regla que apunta a un SKU borrado del catálogo no rompe la corrida, sólo se cuenta aparte.

Por qué intervalos semi-abiertos [0, 1500) ∪ [1500, 2200) ∪ [2200, ∞)

Una pieza de X=1500 mm cae en la regla [1500, 2200), NO en [0, 1500). Si los rangos fueran cerrados en ambos lados ([0, 1500] ∪ [1500, 2200]), los bordes se solaparían en 1500 y la validación rechazaría las reglas por overlap. Definición formal en rules-interval.ts:7-11:

export function inRange(x, minMm, maxMm) {
  if (minMm !== undefined && x < minMm) return false;
  if (maxMm !== undefined && x >= maxMm) return false;
  return true;
}

El helper rangesOverlap() (rules-interval.ts:17-27) usa la condición aLo < bHi && bLo < aHi para detectar solapes; los adyacentes (que tocan en el extremo compartido) NO se consideran solapados gracias al half-open.

3.5 · Acumulación (apply-rules-match.ts + apply-rules-emit.ts)

Si todos los filtros pasan:

// apply-rules-match.ts (1)
stats.pieces++;
partsBySku.set(
  rule.skuCode,
  (partsBySku.get(rule.skuCode) ?? 0) + 1
);
seenSku.add(rule.skuCode);
matchedThisPart = true;

// apply-rules-emit.ts (2)
const groups = Math.floor(stats.pieces / (rule.perPieces ?? 1));
if (groups > 0) qtyBySku.set(
  rule.skuCode,
  (qtyBySku.get(rule.skuCode) ?? 0) + groups * rule.qtyPerPart * qtyMul
);
El multiplicador (qtyMul) entra acá — escala el cómputo, NO la regla. La regla sigue diciendo "2 bisagras por puerta"; lo que cambia es el factor de la obra.

Con perPieces la suma no es por pieza: el motor cuenta primero cuántas piezas matchea cada regla y sólo después emite floor(piezas / perPieces) × qtyPerPart × qtyMul. Un resto incompleto no emite nada, y un SKU que queda en 0 no emite línea. Son dos pases —apply-rules-match.ts cuenta, apply-rules-emit.ts emite— porque la cantidad no existe hasta terminar de recorrer el modelo.

3.6 · Breakdown por ensamblaje (opt-in, apply-rules.ts:165-178)

Cuando withAssemblyBreakdown: true y partAncestors están poblados (cómputo en computeAssemblyGroups + tree-builder + findAncestorAssemblyIds):

3.7 · Reemplazo selectivo (apply-rules.ts:112-116, 184-196)

const kept = qtyMul > 1
  ? existing.filter(l => l.origin !== 'auto')
              .map(l => ({ ...l, qty: l.qty * qtyMul }))
  : existing.filter(l => l.origin !== 'auto');

const autoLines = [];
for (const [skuCode, qty] of qtyBySku) {
  autoLines.push({
    id: uuid(),
    skuCode,
    qty,
    origin: 'auto',
    notes: `auto · ${n} piezas`,
  });
}

return { nextList: [...kept, ...autoLines], summary };

nextList = [...manual (× qtyMul), ...autoLines (× qtyMul)]. Las líneas manual no se tocan salvo por el escalado del multiplicador. Las auto se descartan y se regeneran limpias en cada Apply. Re-aplicar no duplica auto — siempre una línea por SKU, no por pieza.

3.8 · Summary devuelto (apply-rules.ts:198-223)

partsMatched     → piezas que aportaron ≥1 regla
linesAuto        → líneas auto emitidas (1 por SKU)
qtyTotal         → suma qty de las auto (post-multiplier)
skippedOrphanSku → matches omitidos por SKU fuera de catálogo
byAssembly?      → opt-in; Map<skuCode, Map<asmId, { partIds, qty }>>

3.9 · Worked example end-to-end

Proyecto CocinaV9: 3 puertas con role puerta, ninguna asignada a mano aún.

pieza A: 600×500×18   → X=600   → rule-puerta-bis-0  [0,1500)     → BIS-35 × 2
pieza B: 1800×600×18  → X=1800  → rule-puerta-bis-1  [1500,2200)  → BIS-35 × 3
pieza C: 2400×900×18  → X=2400  → rule-puerta-bis-2  [2200,+∞)    → BIS-35 × 4

+ cada puerta también matchea rule-puerta-tir (sin rango, cualquier alto) → TIR-J × 1

Aplicar reglas (multiplier=1) produce:

partsMatched:    3
linesAuto:       2   (BIS-35 + TIR-J)
qtyTotal:       12   (9 + 3)
skippedOrphanSku: 0

state.project.hardwareList =
  { skuCode: 'BIS-35', qty: 9, origin: 'auto', notes: 'auto · 3 piezas' }
  { skuCode: 'TIR-J',  qty: 3, origin: 'auto', notes: 'auto · 3 piezas' }

Si activás herrajeMultiplierEnabled + projectMultiplier=3 (mismo proyecto, "3 cocinas iguales"), el mismo input produce qtyTotal: 36 con las mismas 2 líneas, sin editar reglas.

04 Vista Catálogo: 29 SKUs precargados

Vista Catálogo con SKUs y CRUD inline
04 Vista Catálogo. Tabla con Código · Nombre · Categoría · Unidad · Costo unitario · Medida (mm) · Espesor (mm) · Roles sugeridos · Acciones. Toolbar: + Nuevo SKU, Importar JSON, Exportar JSON.

La vista Catálogo muestra loadHerrajeriaCatalog() con las 9 categorías:

CategoríaBuilt-insEjemplos
bisagra5BIS-35, BIS-PREMIUM, BIS-PIV, BIS-VIDRIO-LO, TIP-ON
corredera6COR-TEL-30/35/40/45/50, COR-TEL-CUSTOM
tirador4TIR-J, TIR-U, TIR-EMB, TIR-BOTON
union4GRP-UNI, GRP-CONF, TAR-DOWEL, TORN-30
soporte3PIN-EST-5, PIN-EST-7, SOP-PERCH
cerradura2LOCK-CAJ, LOCK-PTA-EMB
pata2PATA-REG, RUEDA-GIR
kit1KIT-COR (riel + 2 roldanas)
otro2PERCHERO-TUB, ZAPATERO (los dos a medida)

Los tres últimos (SOP-PERCH, PERCHERO-TUB, ZAPATERO) entraron para que add_clothes_rail y add_shoe_rack no emitan códigos que no existen en el catálogo.

Las 9 categorías viven en src/herrajerie/catalog-defs.ts:HERRAJE_CATEGORY_DEFS con sus aliases por locale. Las 3 unidades son unidad | par | juego. El CRUD es inline (celda por celda con autocomplete) — no hay modal de edición.

El code es slug-like, mayúsculas, guiones (la normalización vive en normalizeCode() en types.ts:136-138: trim + uppercase + colapsa espacios a -). Es la FK que las reglas y el pedido referencian — si lo cambiás, tenés que remapear las reglas y líneas a mano.

Con el SKU en uso por el proyecto abierto, el code es el único campo bloqueado: precio, nombre, categoría, unidad, familia, notas y medidas se editan igual. Al cambiar el cost, las líneas sin override propio toman el precio nuevo; las que tienen override lo conservan. Borrar el SKU sí sigue bloqueado mientras haya líneas usándolo.

4.1 · Por qué catálogo ≠ reglas

El catálogo es la fuente de verdad de precios: cambiar cost de BIS-35 recalcula subtotales en todo el proyecto automáticamente, sin tocar las reglas. Las reglas son la fuente de verdad de cantidades: cambiar una regla regenera líneas auto sin tocar el catálogo. Mantener estas dos dimensiones separadas es lo que permite ajustar el precio de tu proveedor sin invalidar tu conocimiento de taller.

05 Roles sugeridos por SKU

Popover de roles sugeridos en una fila del catálogo
05 Popover de roles. Click en la celda Roles sugeridos abre un popover con checkboxes (built-ins + customs, sin sentinel none). Togglea y el commit es inmediato (fila existente) o marca la lista para la fila nueva.

Cada HerrajeItem carga un array roles: string[] con los roles sugeridos (qué roles de pieza típicamente usan este herraje). Por ejemplo, BIS-35 arranca con ['puerta'], PIN-EST-5 con ['estante'], PATA-REG con ['base'].

El catálogo herraje es la fuente de verdad para el popover: los roles son los mismos del módulo Roles (built-ins + customs de getRoleDefs()). El sentinel 'none' se filtra.

5.1 · Distinción clave: sugerencia vs dato real

Tres campos con nombres similares pero responsabilidades distintas:

HerrajeItem.roles: ['puerta']      ← sugerencia VISUAL (catálogo)
part.partRole: 'puerta'           ← dato REAL de la pieza (modelo)
HardwareRule.role: 'puerta'       ← filtro de la regla (regla)

El motor lee part.partRole        (apply-rules.ts:135)
El catálogo sugiere.              (UI badge "sugerido para puerta")
La pieza decide.                  (Shift+B, edición manual, auto-detect)
La regla filtra.                  (overlap check, half-open interval)
¿Cómo se conecta con las reglas? Las reglas de Rol → SKU × intervalo no leen los roles del catálogo — leen el part.partRole de cada pieza. Los roles del catálogo son sólo sugerencia visual para que el taller sepa "este herraje va con piezas de rol X". Si tu pieza tiene rol puerta y existe la regla (puerta, BIS-35, 0–1500 mm, 2 u.), la pieza suma 2 a BIS-35 al aplicar reglas.

06 Vista Reglas: rol → SKU × intervalo mm

Vista Reglas con las 7 reglas default
06 Vista Reglas. Tabla con id · rol · SKU · qtyPorPieza · Cada N piezas (perPieces) · desde mm · hasta mm · enabled · notas · acciones. Las 7 default del taller arrancan pre-cargadas.

La vista Reglas muestra loadHardwareRules() con 7 reglas default del taller (definidas en src/herrajerie/rules-defaults.ts):

El campo x (eje mayor de la pieza en mm) cae en cada regla vía inRange(x, minMm, maxMm). Los bordes son semi-abiertos (minMm <= x < maxMm) y minMm/maxMm son opcionales: sin minMm el match arranca en 0; sin maxMm el match va a infinito. La tercera regla puerta (≥ 2200 mm) tiene minMm: 2200 y maxMm indefinido — cubre cualquier pieza arriba de 2200 mm.

6.1 · Función conceptual de una regla

Si tenemos que reducir mentalmente "Aplicar reglas" a una sola función, es:

f(role, X) → qty del SKU
       ↑   ↑
       │   └─ boardDimensions(p).x   (cara mayor)
       └───── part.partRole ?? 'none'

Una regla es una fila de la "tabla de despacho": si el rol matchea y el alto cae en el rango, suma qtyPerPart al acumulador del SKU. La pieza pasa por todas las reglas activas; cada SKU que matchea suma una vez por pieza (no se duplica si dos reglas enabled solapan, ver §3.4 punto 3).

6.2 · Cobertura de las 7 default y por qué esos números

6.3 · Cómo agregar una regla

Para "puerta alta de cocina, lleva 5 bisagras a partir de 2600 mm", crear:

role: 'puerta'  sku: 'BIS-35'  minMm: 2600  maxMm: (vacío = ∞)  qty: 5

La cobertura [0, ∞) de las 3 default queda: [0, 1500) ∪ [1500, 2200) ∪ [2200, +∞). La nueva [2600, +∞) se solapa con la última puerta BIS-35 ([2200, +∞)); al guardar, la validación va a disparar overlap salvo que deshabilites una de las dos. Diseñá reglas disjuntas (half-open adyacentes o none).

07 Crear regla custom

Form de creación de regla
07 + Nueva regla. Aparece como fila draft al final de la tabla: id (slug), rol, SKU (autocomplete), qtyPorPieza, Cada N piezas (perPieces), desde mm, hasta mm, enabled. La validación bloquea el enabled hasta que rol y SKU estén completos.

+ Nueva regla agrega una fila con id vacío (addHerrajeriaRule() en src/herrajerie/rules-store.ts). La fila draft está forzada a enabled: false hasta completar rol y SKU; el resto se puede dejar incompleto mientras tipeás.

Cuando completás los campos y guardás, validateHardwareRules corre antes de persistir (rules-validate.ts:140-151):

Código de errorCuándoEjemplo disparador
invalid_idEl id está vacío o sólo whitespace.Draft recién agregado antes de tocar nada
invalid_roleRol no está en el catálogo de roles conocidos (built-ins + customs), o es 'none'.rol puert (typo, falta la a)
unknown_skuEl SKU no está en el catálogo de herrajes del taller.BIS-99 que el taller borró del catálogo
invalid_qtyqtyPerPart no es entero 1–999.2.5 (decimal), 0 (en regla enabled), 1000 (fuera de rango)
invalid_per_piecesperPieces no es entero ≥ 1.0, 2.5, -1
invalid_min / invalid_maxBorde negativo o no finito.minMm: -5, maxMm: NaN
invalid_rangemaxMm ≤ minMm.min: 600, max: 600 (punto único)
overlapDos reglas enabled con mismo (rol, sku) y rangos solapados.[0, 600) + [500, 700) con mismo rol+sku
Draft vs persistente: los drafts incompletos se persisten (forceWriteHardwareRules en rules-store.ts:131) para que no se pierdan al recargar. Sólo se bloquea enabled; si tenés 6 reglas OK y agregás una draft rota, no se pierden las 6 — sólo la nueva queda enabled: false hasta que la termines. Esto permite agregar una fila "placeholder" sin invalidar el resto del taller.

08 Multiplicador de obra (×N cocinas)

Multiplicador de obra escalando el pedido
08 Multiplicador ×N. El flag settings.herrajeMultiplierEnabled + projectMultiplier (1–99) escala TODAS las líneas (manual + auto) sin tocar las reglas. Útil para cotizar un lote de N cocinas iguales sin duplicar el modelo.

El multiplicador de obra vive en dos settings:

Cuando el flag está activo, runApplyHardwareRules escala la qty de todas las líneas resultantes (manual + auto) por el multiplicador. La separación conceptual es clave:

SIN multiplier ×3              CON multiplier ×3
─────────────────────          ─────────────────────
qtyBySku  = Σ qtyPerPart       qtyBySku  = Σ qtyPerPart × 3
manual[i].qty = N              manual[i].qty = N × 3
rule.qtyPerPart = 2            rule.qtyPerPart = 2  (sin tocar)
// apply-rules.ts (con multiplier)
const qtyMul = quantityMultiplier && quantityMultiplier > 1
  ? Math.floor(quantityMultiplier)
  : 1;

const kept = qtyMul > 1
  ? existing.filter(l => l.origin !== 'auto')
              .map(l => ({ ...l, qty: l.qty * qtyMul }))
  : existing.filter(l => l.origin !== 'auto');

// también: qtyBySku acumula con rule.qtyPerPart * qtyMul
Por qué separar cómputo y regla: pasar de ×2 a ×3 no debe romper las reglas custom del taller. Cambiar el multiplier edita el factor de la obra; cambiar las reglas edita "qué cantidad va por pieza". Dos dimensiones ortogonales, dos settings separados.

NO escala qtyPerPart de las reglas — la regla sigue siendo «por pieza». Lo que se escala es el cómputo. Así un cambio posterior en el multiplicador (ej. pasar de ×2 a ×3) no rompe las reglas custom del taller.

Default off: la spec (2026-07-17-herraje-multiplier.md) fija el default en false para preservar el comportamiento histórico (proyectos y tests existentes NO multiplican). Cuando agregás projectMultiplier>1 a un proyecto viejo, el herraje también se multiplica sólo si activás explícitamente el flag en Settings.

09 Settings: Export / Import JSON · library share

Botones Importar / Exportar JSON del panel
09 I/O del herraje. Botón Exportar JSON del panel → archivo herrajeria.json con catálogo + recientes. Importar JSON lo reemplaza. Compartir enlace… encodea el catálogo a base64+envelope despiece-library (cap 65 KB) para pasarlo por WhatsApp.

Hay tres niveles de I/O para el herraje:

NivelSalidaComando
Per-panel JSONherrajeria.json (catálogo + recientes)Catálogo → Exportar JSON / Importar JSON
Library shareURL con catálogo encodeado (despiece-library envelope v1, gzip+base64)Catálogo → ⋯ → Compartir enlace / Pegar enlace
Configuración I/Oconfig.zip con TODO (catálogo + reglas + fotos + materiales + roles + máquinas + stock + glue-up + lineales)Settings → Exportar / Importar

El library share tiene cap 65 KB por la limitación de URL — si tu catálogo supera eso, va como .zip con las fotos adjuntas. Cada SKU puede tener una foto en localStorage[despiece.herraje-images.v1] que se incluye en el I/O.

9.1 · Mapa de persistencia por capa

DatoDóndeBump PROJECT_FORMAT_VERSION
Catálogo de herrajeslocalStorage[despiece.herrajeria.v1]N/A
Reglas de tallerlocalStorage[despiece.hardware-rules.v1]N/A
Fotos de herrajeslocalStorage[despiece.herraje-images.v1]N/A
Settings (incl. herrajeMultiplierEnabled)localStorage[despiece.settings.v1]N/A
Pedido del proyecto (parts.hardwareList[]).despiece (per-proyecto)no (soft)
parts.hardwareRulesMode / hardwareRules.despiece (legacy, soft)no
Soft fields: el catálogo (herrajeriaCatalog), las reglas (hardwareRules) y la lista de herraje (hardwareList) no bumpan PROJECT_FORMAT_VERSION — son soft fields que se migran con sane defaults si el modelo tiene un valor legacy.

10 Atajos de teclado

Hoja de atajos
10 Hoja de atajos. Help (botón de la topbar o ?) abre la hoja completa. Herraje no tiene atajo propio — todo va por la UI.

Atajos del flujo de herraje:

AtajoAcción
Click en #tb-herrajeAbrir / cerrar el panel de Herraje (toggleHerrajePanel).
EscCerrar el panel (no bloquea si hay un export en curso).
En el buscadorType-ahead con autocomplete; Enter agrega el primer resultado o el SKU tipeado.
Click en SKU de catálogoSelecciona + resalta su fila (drag & inline edit).
Click en Aplicar reglasRegenera las líneas auto (sin tocar las manual).
? / F1Abrir la hoja de atajos completa.
F / HomeFit / Fit + iso del viewport 3D.

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

11 Por conjunto (assembly breakdown)

Además del modo Plano, el switch groupby-assembly en la toolbar de Pedido abre la vista Por mueble: las líneas auto se reparten por ensamblaje del product tree y muestran subtotal por grupo.

Para obras con varios muebles (ej. una cocina con bajo-mesada + alacena + cajonera), este modo te dice qué bisagras van a la alacena y cuáles al bajo-mesada:

Cuándo usarlo: cuando el cliente te pide "cuánto herraje lleva la cocina + cuánto lleva la alacena por separado" para facturar parcialmente. Cuándo NO usarlo: si tenés un solo mueble en el proyecto, el modo Plano es más directo.

12 Pipeline integrado

El herraje no termina en el panel: alimenta varios consumidores aguas abajo, todos leyendo state.project.hardwareList o sus derivados:

ConsumidorQué leePara qué
Pedido de materiales (hardwareCost)buildHardwareRows(list, catalog)Suma de subtotales herraje en el pedido al proveedor
Etiquetas (label-section)state.project.hardwareList agrupado por (sku, asm)Breakdown por mueble en etiquetas
Presupuesto al clientestate.project.hardwareList con unitCostSección "Herraje" del PDF branded
Export CSV heredadofilas planas del listadoDescarga CSV
Export XLSXfilas + estilos por categoríaPlanilla editable
Export PDFfilas agrupadas por categoríaPedido al proveedor
Implicancia del multiplier: como runApplyHardwareRules pre-escala las qty (manual + auto), todos los consumidores leen qty ya multiplicada — no requieren enterarse del flag. Cambiar herrajeMultiplierEnabled retroactivo requiere re-aplicar reglas para que las auto vuelvan a escalar; las manuales se editan a mano (decisión de spec).

Ver también