200 lines
7.4 KiB
TypeScript
200 lines
7.4 KiB
TypeScript
import { ArrowRight, ArrowDown } from 'lucide-react';
|
|
import type { Project, Room, FlooringSettings, SkirtingSettings } from '../lib/types';
|
|
import { NumberField } from './Fields';
|
|
import type { FlooringOptions } from '../lib/flooring';
|
|
import { mm } from '../lib/format';
|
|
|
|
interface Props {
|
|
project: Project;
|
|
room: Room;
|
|
flooringOptions: FlooringOptions;
|
|
setFlooring: (patch: Partial<FlooringSettings>) => void;
|
|
setSkirting: (patch: Partial<SkirtingSettings>) => void;
|
|
changeRoom: (fn: (r: Room) => Room) => void;
|
|
}
|
|
|
|
const kerfHelp =
|
|
'Width of material removed by the saw blade. A 3 mm blade turns a 1,000 mm board into 600 mm + 397 mm after one cut.';
|
|
|
|
export function MaterialsEditor({
|
|
project,
|
|
room,
|
|
flooringOptions,
|
|
setFlooring,
|
|
setSkirting,
|
|
changeRoom,
|
|
}: Props) {
|
|
const f = project.flooring;
|
|
const s = project.skirting;
|
|
const floor = <K extends keyof FlooringSettings>(key: K, value: FlooringSettings[K]) =>
|
|
setFlooring({ [key]: value });
|
|
const skirt = (key: keyof SkirtingSettings, value: number) => setSkirting({ [key]: value });
|
|
|
|
return (
|
|
<>
|
|
<div className="editor-heading">
|
|
<h2>Materials</h2>
|
|
<span>Shared across rooms</span>
|
|
</div>
|
|
<section className="material-section flooring" aria-labelledby="flooring-materials">
|
|
<h3 id="flooring-materials" className="material-heading">
|
|
Floorboards
|
|
</h3>
|
|
<p className="helper material-help">
|
|
Use the length and width printed on your flooring pack.
|
|
</p>
|
|
<div className="field-grid">
|
|
<NumberField label="Board length" value={f.length} onChange={(n) => floor('length', n)} />
|
|
<NumberField label="Board width" value={f.width} onChange={(n) => floor('width', n)} />
|
|
</div>
|
|
<div className="field">
|
|
<span>Laying pattern</span>
|
|
<div className="segmented" role="group" aria-label="Laying pattern">
|
|
<button
|
|
className={f.pattern === 'random' ? 'active' : ''}
|
|
aria-pressed={f.pattern === 'random'}
|
|
onClick={() => floor('pattern', 'random')}
|
|
>
|
|
Random
|
|
</button>
|
|
<button
|
|
className={f.pattern === 'brick' ? 'active' : ''}
|
|
aria-pressed={f.pattern === 'brick'}
|
|
onClick={() => floor('pattern', 'brick')}
|
|
>
|
|
Brick
|
|
</button>
|
|
</div>
|
|
<p className="helper">
|
|
{f.pattern === 'random'
|
|
? 'Maximise randomness spreads out joints to avoid H shapes. Minimise cuts can repeat joint positions.'
|
|
: 'A regular pattern with each row staggered by half a board.'}
|
|
</p>
|
|
</div>
|
|
{f.pattern === 'random' && (
|
|
<div className="field">
|
|
<span>What matters most? · all rooms</span>
|
|
<div className="optimisation-options" role="group" aria-label="Flooring optimisation">
|
|
{(['cuts', 'random'] as const).map((priority) => {
|
|
const plan = flooringOptions[priority];
|
|
const valid = !plan.issues.some((issue) => issue.severity === 'error');
|
|
const selected = f.optimisation === priority;
|
|
return (
|
|
<button
|
|
key={priority}
|
|
type="button"
|
|
className={selected ? 'active' : ''}
|
|
aria-pressed={selected}
|
|
onClick={() => floor('optimisation', priority)}
|
|
>
|
|
<span>{priority === 'cuts' ? 'Minimise cuts' : 'Maximise randomness'}</span>
|
|
<small>
|
|
{valid
|
|
? `${plan.cuts} cuts · ${mm(plan.wastePercent)}% waste`
|
|
: 'Check measurements'}
|
|
</small>
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
)}
|
|
<div className="field">
|
|
<span>Board direction · {room.name}</span>
|
|
<div className="segmented" role="group" aria-label="Board direction">
|
|
<button
|
|
className={room.direction === 0 ? 'active' : ''}
|
|
aria-pressed={room.direction === 0}
|
|
onClick={() => changeRoom((r) => ({ ...r, direction: 0 }))}
|
|
>
|
|
<ArrowRight size={16} aria-hidden="true" /> Along first wall
|
|
</button>
|
|
<button
|
|
className={room.direction === 90 ? 'active' : ''}
|
|
aria-pressed={room.direction === 90}
|
|
onClick={() => changeRoom((r) => ({ ...r, direction: 90 }))}
|
|
>
|
|
<ArrowDown size={16} aria-hidden="true" /> Across first wall
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<NumberField
|
|
label="Wall spacer"
|
|
value={f.gap}
|
|
onChange={(n) => floor('gap', n)}
|
|
hint="Expansion gap between the floorboards and walls. Check your flooring instructions."
|
|
/>
|
|
<details className="advanced-settings">
|
|
<summary>Advanced flooring settings</summary>
|
|
<p className="helper material-help">
|
|
Adjust the saw blade width, joint spacing and smallest pieces to suit your flooring.
|
|
</p>
|
|
<div className="field-grid">
|
|
<NumberField
|
|
label="Saw kerf"
|
|
value={f.kerf}
|
|
onChange={(n) => floor('kerf', n)}
|
|
tooltip={kerfHelp}
|
|
/>
|
|
<NumberField
|
|
label="Minimum joint offset"
|
|
value={f.minOffset}
|
|
onChange={(n) => floor('minOffset', n)}
|
|
/>
|
|
<NumberField
|
|
label="Maximum joint offset"
|
|
value={f.maxOffset}
|
|
onChange={(n) => floor('maxOffset', n)}
|
|
/>
|
|
<NumberField
|
|
label="Shortest end piece"
|
|
value={f.minLength}
|
|
onChange={(n) => floor('minLength', n)}
|
|
/>
|
|
<NumberField
|
|
label="Narrowest edge row"
|
|
value={f.minWidth}
|
|
onChange={(n) => floor('minWidth', n)}
|
|
/>
|
|
</div>
|
|
</details>
|
|
</section>
|
|
<section className="material-section skirting" aria-labelledby="skirting-materials">
|
|
<h3 id="skirting-materials" className="material-heading">
|
|
Skirting boards
|
|
</h3>
|
|
<p className="helper material-help">
|
|
Enter one purchased board length. Door openings are left clear automatically.
|
|
</p>
|
|
<div className="field-grid">
|
|
<NumberField
|
|
label="Skirting board length"
|
|
value={s.stockLength}
|
|
onChange={(n) => skirt('stockLength', n)}
|
|
/>
|
|
<NumberField
|
|
label="Skirting thickness"
|
|
value={s.thickness}
|
|
onChange={(n) => skirt('thickness', n)}
|
|
/>
|
|
</div>
|
|
<details className="advanced-settings">
|
|
<summary>Advanced skirting settings</summary>
|
|
<div className="field-grid">
|
|
<NumberField
|
|
label="Minimum piece"
|
|
value={s.minLength}
|
|
onChange={(n) => skirt('minLength', n)}
|
|
/>
|
|
<NumberField
|
|
label="Skirting saw kerf"
|
|
value={s.kerf}
|
|
onChange={(n) => skirt('kerf', n)}
|
|
tooltip={kerfHelp}
|
|
/>
|
|
</div>
|
|
</details>
|
|
</section>
|
|
</>
|
|
);
|
|
}
|