All files / web/src/components/create/abacus abacus-ticket.ts

100% Statements 77/77
100% Branches 11/11
100% Functions 1/1
100% Lines 77/77

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 781x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 6x 6x 6x 1x 1x 1x 1x 6x 1x 1x 4x 4x 4x 4x 6x 9x 9x 8x 8x 9x 1x 1x 7x 7x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x  
/**
 * v2 print-ticket assembly for the abacus (Phase 2b, Gitea #9).
 *
 * Builds the `PrintTicketV2` that rides the multipart submit next to the 3MF.
 * The filament list mirrors the model's extruder assignment: `meshesToThreeMf`
 * groups same-colour bodies onto one extruder in first-appearance order, so the
 * ticket lists one spool per DISTINCT body colour, in that same order — a spool
 * per extruder, never a spool per role.
 *
 * The style block is the settings editor's controlled value, passed through
 * verbatim (v2 discipline: nothing here injects or defaults process keys).
 * Only a THH-backed catalog can produce a ticket — its spool ids are the real
 * AMS `slotId`s the service resolves; the params stand-in catalog has no
 * physical slots to name.
 */
import type {
  PrintTicketV2,
  TicketFilament,
  TicketSource,
  TicketStartPolicy,
  TicketStyle,
} from '@eink/print-dialog'
import { PRINT_SOURCE_APP } from '@/lib/abacus/print/source-app'
import type { SpoolBodySummary } from './abacus-3mf'
import type { FilamentCatalog } from './abacus-catalog'
 
export interface AbacusTicketArgs {
  /** Job name shown on the service, e.g. "Abacus — 13 columns". */
  name: string
  /** Provenance minus `app`, which this module owns. */
  source: Omit<TicketSource, 'app'>
  /** The bodies that actually went into the 3MF (ascending slot order). */
  bodies: readonly SpoolBodySummary[]
  /** Must be a 'thh-ams' catalog — spool ids are the AMS slotIds. */
  catalog: FilamentCatalog
  /** The settings editor's controlled value, verbatim. */
  style: TicketStyle
  startPolicy: TicketStartPolicy
  /** Dedup key — mint one per submit intent, reuse across retries. */
  idempotencyKey: string
}
 
export function buildAbacusTicket(args: AbacusTicketArgs): PrintTicketV2 {
  const { name, source, bodies, catalog, style, startPolicy, idempotencyKey } = args
 
  if (catalog.source !== 'thh-ams') {
    throw new Error(
      'print submission needs the AMS filament roster — the params catalog has no real slot ids'
    )
  }
  if (bodies.length === 0) {
    throw new Error('nothing to print — the 3MF has no bodies')
  }
 
  // One filament per distinct body colour, in body (= extruder) order.
  const seenColors = new Set<string>()
  const filaments: TicketFilament[] = []
  for (const body of bodies) {
    const color = body.colorHex.toLowerCase()
    if (seenColors.has(color)) continue
    seenColors.add(color)
    const spool = catalog.spools[body.slot]
    if (!spool) {
      throw new Error(`3MF body "${body.label}" references slot ${body.slot}, not in the catalog`)
    }
    filaments.push({ slotId: spool.id })
  }
 
  return {
    name,
    source: { ...source, app: PRINT_SOURCE_APP },
    filaments,
    style,
    start: { policy: startPolicy },
    idempotencyKey,
  }
}