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

100% Statements 107/107
100% Branches 8/8
100% Functions 3/3
100% Lines 107/107

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 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 1081x 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 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 1x 1x 1x 1x 1x 1x 1x 141x 141x 141x 141x 141x 1x 1x 1x 1x 1x 270x 270x 6x 264x 270x  
// Abacus Studio — user-facing infill for the frame and the beads.
//
// WHY THIS IS A DESIGN KNOB AND NOT A PRINT SETTING. Infill is the one slicer
// number that changes how the finished abacus FEELS — a light frame flexes under
// a thumb, heavy beads click differently — so it belongs beside the geometry that
// decides the rest of the feel, not in the printer's settings sheet. It also has
// to survive the trip: THH's `--load-settings` replaces the project's global
// settings wholesale, so a plate-wide `sparse_infill_density` written into
// project_settings.config is gone by the time the job slices. Per-part config is
// the one channel that reaches a submitted print untouched (see `AssemblyBody.process`),
// so every body carries its own density and nothing can overwrite it.
//
// THE FEET ARE NOT HERE. Printed feet are always solid, for a mechanical reason
// rather than a taste one: Stage B's first layer lands on whatever infill the feet
// have, and a sparse foot is a sparse bed for the seam. That stays a constant
// (`FEET_PART_PROCESS` in abacus-3mf-assembly.ts) and is deliberately not exposed.
//
// THE SHARED-SLOT RULE. Bodies are per FILAMENT SLOT, not per role, and the plan's
// quantizer is free to land the frame and a bead role on the same slot (the
// monochrome scheme always does). A body can therefore only have one density, so
// the frame's wins: it is the structural part, and it is the one the user was
// thinking about when they moved the knob. The rail says so out loud when the
// scheme makes it certain.
 
import type { Params } from './abacus-model'
 
export type InfillLevel = 'light' | 'standard' | 'sturdy' | 'solid'
 
export interface InfillOption {
  id: InfillLevel
  label: string
  /** Orca `sparse_infill_density`, as a percentage number (serialized `"15%"`). */
  densityPct: number
  /** What this level does to the frame — the structural half. */
  frame: string
  /** What it does to the beads — weight and click. */
  beads: string
  /** What it costs in print time. */
  time: string
}
 
/** Ascending density. The rail renders this list in order, so it doubles as the menu. */
export const INFILL_OPTIONS: readonly InfillOption[] = [
  {
    id: 'light',
    label: 'Light',
    densityPct: 10,
    frame: 'Lightest frame. Fine on a desk; flexes a little under a hard press.',
    beads: 'Light beads with a quieter, tappier click.',
    time: 'Fastest print.',
  },
  {
    id: 'standard',
    label: 'Standard',
    densityPct: 15,
    frame: "The printer's usual density. Rigid enough for everyday use.",
    beads: 'Ordinary bead weight and click.',
    time: 'The baseline print time.',
  },
  {
    id: 'sturdy',
    label: 'Sturdy',
    densityPct: 30,
    frame: 'Noticeably stiffer; shrugs off drops and knocks.',
    beads: 'Heavier beads with a firmer, more satisfying click.',
    time: "Adds roughly a quarter to the frame's print time; beads are small and add little.",
  },
  {
    id: 'solid',
    label: 'Solid',
    densityPct: 100,
    frame: 'Heavy and as rigid as it gets; feels like one solid object.',
    beads: 'The heaviest beads: the fullest click and the most carry in a flick.',
    time: "Roughly doubles the frame's print time; beads still add little.",
  },
]
 
const BY_ID = Object.fromEntries(INFILL_OPTIONS.map((o) => [o.id, o])) as Record<
  InfillLevel,
  InfillOption
>
 
/** The option row for a level — the label/explanation lookup the rail and the
 *  print panel share, so neither restates the copy. */
export const infillOption = (level: InfillLevel): InfillOption => BY_ID[level]
 
/** The two densities a design actually prints at. `infill_linked` (the default)
 *  means the beads follow the frame and `infill_beads` is ignored here; the rail
 *  mirrors the frame into the stored bead value while linked, so a stale
 *  `infill_beads` can only come from a hand-edited snapshot — and even then it
 *  is inert until the user separates the two. */
export function resolveInfill(
  params: Pick<Params, 'infill_frame' | 'infill_beads' | 'infill_linked'>
): { frame: InfillLevel; beads: InfillLevel } {
  const frame = params.infill_frame
  return { frame, beads: params.infill_linked ? frame : params.infill_beads }
}
 
/** The per-part process keys a body at this level carries. Solid also pins the
 *  pattern, for the same reason `FEET_PART_PROCESS` does: at 100 % a gyroid is a
 *  slow way to draw a solid layer. */
export function infillPartProcess(level: InfillLevel): Readonly<Record<string, string>> {
  const density = `${infillOption(level).densityPct}%`
  return level === 'solid'
    ? { sparse_infill_density: density, sparse_infill_pattern: 'rectilinear' }
    : { sparse_infill_density: density }
}