from typing import List, Sequence from .construction_timber_length_types import CutBar, CuttingList def timber_cutting_list(cuts: Sequence[int], stock_length: int, kerf: int) -> CuttingList: """Plan which pieces to cut from which stock length, first-fit decreasing. A bar holds pieces p1..pn when their lengths plus a kerf between each pair fit: sum + kerf * (n - 1) <= stock_length. The last piece may end exactly at the end of the bar, which needs no final cut. Ignoring kerf is the usual mistake: three 1200 mm pieces do not come out of one 3600 mm length. """ if isinstance(stock_length, bool) or not isinstance(stock_length, int) or stock_length <= 0: raise ValueError("stockLength must be a whole number of millimetres greater than 0, received %r" % (stock_length,)) if isinstance(kerf, bool) or not isinstance(kerf, int) or kerf < 0: raise ValueError("kerf must be a whole number of millimetres, 0 or more, received %r" % (kerf,)) for i, cut in enumerate(cuts): if isinstance(cut, bool) or not isinstance(cut, int) or cut <= 0: raise ValueError("cut %d must be a whole number of millimetres greater than 0, received %r" % (i + 1, cut)) if cut > stock_length: raise ValueError("cut %d (%d mm) is longer than the stock length (%d mm)" % (i + 1, cut, stock_length)) # Equal lengths are interchangeable, so a plain descending sort is deterministic. pieces: List[List[int]] = [] used: List[int] = [] for cut in sorted(cuts, reverse=True): for b in range(len(pieces)): if used[b] + kerf + cut <= stock_length: pieces[b].append(cut) used[b] += kerf + cut break else: pieces.append([cut]) used.append(cut) bars = [CutBar(cuts=p, offcut=max(0, stock_length - used[b] - kerf)) for b, p in enumerate(pieces)] return CuttingList(bars=bars, bar_count=len(bars), waste=len(bars) * stock_length - sum(cuts))