Try it. Each bar is 10 m long. The order needs the pieces listed below. Click a piece, then click a bar to cut it from that bar — it fits only if there's room. Watch the waste meter track the leftover.
<p class="hint">Bars are <b>10 m</b> each. Pick a required piece, then click a bar to cut it there (it must fit). Use <b>Add bar</b> for a fresh one. Minimize bars and <span class="w">waste</span> — then hit Optimize to compare.</p>
<div class="order">
<span class="lbl">Order:</span>
<span id="pieces"></span>
</div>
<div id="bars" class="bars"></div>
<div class="meter">
<div>Bars used: <b id="nbars">0</b></div>
<div>Waste: <b id="waste" class="w">0 m</b></div>
<div id="msg" class="msg"></div>
</div>
<div class="btns">
<button id="add" type="button">+ Add bar</button>
<button id="opt" type="button">Optimize</button>
<button id="reset" type="button" class="ghost">Reset</button>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .8rem; line-height: 1.45; }
.hint .w { color: #c0392b; font-weight: 700; }
.order { margin-bottom: .8rem; }
.lbl { font: 600 13px system-ui; color: #555; margin-right: .4rem; }
.piece { display: inline-block; background: #457b9d; color: #fff; border: 2px solid #457b9d; border-radius: 6px; padding: .25rem .55rem; margin: .15rem; font: 700 13px ui-monospace, monospace; cursor: pointer; }
.piece.sel { background: #fff; color: #457b9d; box-shadow: 0 0 0 2px #457b9d; }
.piece.done { opacity: .3; cursor: default; text-decoration: line-through; }
.bars { display: flex; flex-direction: column; gap: .5rem; margin-bottom: .8rem; }
.bar { height: 34px; border: 1px solid #bbb; border-radius: 6px; background: #f1f3f5; display: flex; overflow: hidden; cursor: pointer; position: relative; }
.bar:hover { border-color: #457b9d; }
.seg { height: 100%; display: flex; align-items: center; justify-content: center; font: 700 12px ui-monospace, monospace; color: #fff; border-right: 2px solid #fff; }
.seg.s3 { background: #2a9d8f; } .seg.s4 { background: #e76f51; } .seg.s5 { background: #6d597a; }
.seg.s6 { background: #457b9d; } .seg.s7 { background: #b5651d; }
.bar .free { height: 100%; background: repeating-linear-gradient(45deg,#fbeeec,#fbeeec 6px,#f7ded9 6px,#f7ded9 12px); flex: 1; }
.meter { display: flex; gap: 1.2rem; align-items: center; font-size: .95rem; margin-bottom: .7rem; flex-wrap: wrap; }
.meter .w { color: #c0392b; }
.msg { color: #0a7d33; font-weight: 700; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
button { font: 600 14px system-ui, sans-serif; padding: .5rem 1rem; border: 1px solid #457b9d; background: #457b9d; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #457b9d; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Cutting stock: cut fixed-length bars into required pieces with least waste.
// NP-hard (generalizes bin packing). "Optimize" here runs Best-Fit-Decreasing,
// which finds the perfect 4-bar plan for this order.
const BAR = 10;
const ORDER = [6, 4, 5, 5, 7, 3, 4, 3, 3]; // total 40 -> 4 bars is optimal (0 waste)
let placed, bars, sel;
function reset() { placed = ORDER.map(() => false); bars = []; sel = -1; render(); }
function used(bar) { return bar.reduce((s, x) => s + x, 0); }
function renderPieces() {
const el = document.getElementById('pieces');
el.innerHTML = ORDER.map((len, i) =>
fmt('<span class="piece%v%" data-i="%i%">%len%m</span>', { v: placed[i] ? ' done' : (sel === i ? ' sel' : ''), i: i, len: len })).join('');
el.querySelectorAll('.piece').forEach(p => p.addEventListener('click', () => {
const i = +p.dataset.i; if (placed[i]) return; sel = (sel === i ? -1 : i); render();
}));
}
function renderBars() {
const el = document.getElementById('bars');
el.innerHTML = bars.map((bar, b) => {
const segs = bar.map(len => '<div class="seg s' + len + '" style="width:' + (len / BAR * 100) + '%">' + len + '</div>').join('');
const free = BAR - used(bar);
const freeDiv = free > 0 ? '<div class="free" style="flex:' + free + '"></div>' : '';
return '<div class="bar" data-b="' + b + '">' + segs + freeDiv + '</div>';
}).join('') || '<div style="color:#999;font-size:.9rem">No bars yet — Add bar, pick a piece, then click the bar.</div>';
el.querySelectorAll('.bar').forEach(bar => bar.addEventListener('click', () => place(+bar.dataset.b)));
}
function place(b) {
if (sel < 0) { flash('Pick a piece first.'); return; }
const len = ORDER[sel];
if (used(bars[b]) + len > BAR) { flash('Doesn’t fit on that bar.'); return; }
bars[b].push(len); placed[sel] = true; sel = -1; render();
}
let msgTimer = null;
function flash(t) { const m = document.getElementById('msg'); m.style.color = '#c0392b'; m.textContent = t; }
function render() {
renderPieces(); renderBars();
const waste = bars.reduce((s, bar) => s + (BAR - used(bar)), 0);
document.getElementById('nbars').textContent = bars.length;
document.getElementById('waste').textContent = fmt('%waste% m', { waste: waste });
const m = document.getElementById('msg');
const allDone = placed.every(Boolean);
if (allDone) {
m.style.color = '#0a7d33';
m.textContent = bars.length <= 4 ? fmt('✓ Order complete with %length% bars — optimal!', { length: bars.length }) :
fmt('✓ Order complete with %length% bars (4 is possible).', { length: bars.length });
} else if (msgTimer === null) { m.textContent = ''; }
}
// Best-Fit-Decreasing: place each piece (largest first) on the bar that
// leaves the least leftover; open a new bar only if none fits.
function optimize() {
const items = [...ORDER].sort((a, b) => b - a);
const result = [];
for (const len of items) {
let best = -1, bestRem = Infinity;
result.forEach((bar, i) => { const rem = BAR - used(bar) - len; if (rem >= 0 && rem < bestRem) { bestRem = rem; best = i; } });
if (best < 0) result.push([len]); else result[best].push(len);
}
bars = result; placed = ORDER.map(() => true); sel = -1; render();
}
document.getElementById('add').addEventListener('click', () => { bars.push([]); render(); });
document.getElementById('opt').addEventListener('click', optimize);
document.getElementById('reset').addEventListener('click', reset);
reset();
<p class="hint">Bars are <b>10 m</b> each. Pick a required piece, then click a bar to cut it there (it must fit). Use <b>Add bar</b> for a fresh one. Minimize bars and <span class="w">waste</span> — then hit Optimize to compare.</p>
<div class="order">
<span class="lbl">Order:</span>
<span id="pieces"></span>
</div>
<div id="bars" class="bars"></div>
<div class="meter">
<div>Bars used: <b id="nbars">0</b></div>
<div>Waste: <b id="waste" class="w">0 m</b></div>
<div id="msg" class="msg"></div>
</div>
<div class="btns">
<button id="add" type="button">+ Add bar</button>
<button id="opt" type="button">Optimize</button>
<button id="reset" type="button" class="ghost">Reset</button>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .8rem; line-height: 1.45; }
.hint .w { color: #c0392b; font-weight: 700; }
.order { margin-bottom: .8rem; }
.lbl { font: 600 13px system-ui; color: #555; margin-right: .4rem; }
.piece { display: inline-block; background: #457b9d; color: #fff; border: 2px solid #457b9d; border-radius: 6px; padding: .25rem .55rem; margin: .15rem; font: 700 13px ui-monospace, monospace; cursor: pointer; }
.piece.sel { background: #fff; color: #457b9d; box-shadow: 0 0 0 2px #457b9d; }
.piece.done { opacity: .3; cursor: default; text-decoration: line-through; }
.bars { display: flex; flex-direction: column; gap: .5rem; margin-bottom: .8rem; }
.bar { height: 34px; border: 1px solid #bbb; border-radius: 6px; background: #f1f3f5; display: flex; overflow: hidden; cursor: pointer; position: relative; }
.bar:hover { border-color: #457b9d; }
.seg { height: 100%; display: flex; align-items: center; justify-content: center; font: 700 12px ui-monospace, monospace; color: #fff; border-right: 2px solid #fff; }
.seg.s3 { background: #2a9d8f; } .seg.s4 { background: #e76f51; } .seg.s5 { background: #6d597a; }
.seg.s6 { background: #457b9d; } .seg.s7 { background: #b5651d; }
.bar .free { height: 100%; background: repeating-linear-gradient(45deg,#fbeeec,#fbeeec 6px,#f7ded9 6px,#f7ded9 12px); flex: 1; }
.meter { display: flex; gap: 1.2rem; align-items: center; font-size: .95rem; margin-bottom: .7rem; flex-wrap: wrap; }
.meter .w { color: #c0392b; }
.msg { color: #0a7d33; font-weight: 700; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
button { font: 600 14px system-ui, sans-serif; padding: .5rem 1rem; border: 1px solid #457b9d; background: #457b9d; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #457b9d; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Cutting stock: cut fixed-length bars into required pieces with least waste.
// NP-hard (generalizes bin packing). "Optimize" here runs Best-Fit-Decreasing,
// which finds the perfect 4-bar plan for this order.
const BAR = 10;
const ORDER = [6, 4, 5, 5, 7, 3, 4, 3, 3]; // total 40 -> 4 bars is optimal (0 waste)
let placed, bars, sel;
function reset() { placed = ORDER.map(() => false); bars = []; sel = -1; render(); }
function used(bar) { return bar.reduce((s, x) => s + x, 0); }
function renderPieces() {
const el = document.getElementById('pieces');
el.innerHTML = ORDER.map((len, i) =>
fmt('<span class="piece%v%" data-i="%i%">%len%m</span>', { v: placed[i] ? ' done' : (sel === i ? ' sel' : ''), i: i, len: len })).join('');
el.querySelectorAll('.piece').forEach(p => p.addEventListener('click', () => {
const i = +p.dataset.i; if (placed[i]) return; sel = (sel === i ? -1 : i); render();
}));
}
function renderBars() {
const el = document.getElementById('bars');
el.innerHTML = bars.map((bar, b) => {
const segs = bar.map(len => '<div class="seg s' + len + '" style="width:' + (len / BAR * 100) + '%">' + len + '</div>').join('');
const free = BAR - used(bar);
const freeDiv = free > 0 ? '<div class="free" style="flex:' + free + '"></div>' : '';
return '<div class="bar" data-b="' + b + '">' + segs + freeDiv + '</div>';
}).join('') || '<div style="color:#999;font-size:.9rem">No bars yet — Add bar, pick a piece, then click the bar.</div>';
el.querySelectorAll('.bar').forEach(bar => bar.addEventListener('click', () => place(+bar.dataset.b)));
}
function place(b) {
if (sel < 0) { flash('Pick a piece first.'); return; }
const len = ORDER[sel];
if (used(bars[b]) + len > BAR) { flash('Doesn’t fit on that bar.'); return; }
bars[b].push(len); placed[sel] = true; sel = -1; render();
}
let msgTimer = null;
function flash(t) { const m = document.getElementById('msg'); m.style.color = '#c0392b'; m.textContent = t; }
function render() {
renderPieces(); renderBars();
const waste = bars.reduce((s, bar) => s + (BAR - used(bar)), 0);
document.getElementById('nbars').textContent = bars.length;
document.getElementById('waste').textContent = fmt('%waste% m', { waste: waste });
const m = document.getElementById('msg');
const allDone = placed.every(Boolean);
if (allDone) {
m.style.color = '#0a7d33';
m.textContent = bars.length <= 4 ? fmt('✓ Order complete with %length% bars — optimal!', { length: bars.length }) :
fmt('✓ Order complete with %length% bars (4 is possible).', { length: bars.length });
} else if (msgTimer === null) { m.textContent = ''; }
}
// Best-Fit-Decreasing: place each piece (largest first) on the bar that
// leaves the least leftover; open a new bar only if none fits.
function optimize() {
const items = [...ORDER].sort((a, b) => b - a);
const result = [];
for (const len of items) {
let best = -1, bestRem = Infinity;
result.forEach((bar, i) => { const rem = BAR - used(bar) - len; if (rem >= 0 && rem < bestRem) { bestRem = rem; best = i; } });
if (best < 0) result.push([len]); else result[best].push(len);
}
bars = result; placed = ORDER.map(() => true); sel = -1; render();
}
document.getElementById('add').addEventListener('click', () => { bars.push([]); render(); });
document.getElementById('opt').addEventListener('click', optimize);
document.getElementById('reset').addEventListener('click', reset);
reset();
Try to use the fewest bars with the least waste, then hit Optimize to see a tight plan. The natural "biggest piece first" instinct often strands awkward leftovers — exactly the trap that makes cutting stock hard.
Comments
Loading comments...