Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ function App() {
0.5,
0,
0.25,
0.125,
"in",
"LR4",
));
Expand All @@ -33,7 +34,7 @@ function App() {
<div className="inline-flex flex-col h-screen max-h-screen">
<div className="inline-block">
<TableEditor table={table} updateTable={setTable} />
<SVGDownloadButton className="real-size-layout" />
<SVGDownloadButton className="real-size-layout" units={table.units} />
</div>
<p>Calibration square is {table.calibrationSquareSize} {table.units}</p>
<hr className="p-3 w-screen"/>
Expand Down
49 changes: 35 additions & 14 deletions src/components/SVGDownloadButton.tsx
Original file line number Diff line number Diff line change
@@ -1,33 +1,54 @@
import { Units } from '../models/Table';

export default function SVGDownloadButton(props: {className: string}) {
export default function SVGDownloadButton(props: {className: string, units: Units}) {
const handleDownload = () => {
// Find the SVG element
const svgElement = document.getElementById(props.className);
if (!svgElement) {
// Find the container holding the on-screen SVG.
const container = document.getElementById(props.className);

if (!container) {
console.error('SVG element not found');
return;
}

// Get the SVG content
const svgContent = svgElement.innerHTML;

const svg = container.querySelector('svg');

if (!svg) {
console.error('SVG element not found');
return;
}

// Clone so we can give the exported file a real-world size without
// touching the responsive on-screen version. Setting width/height with
// the unit suffix makes 1 user unit = 1 unit of length, so drawings
// exported in mm come out at 1px = 1mm.
const clone = svg.cloneNode(true) as SVGSVGElement;
const viewBox = clone.getAttribute('viewBox');
if (viewBox) {
const [, , width, height] = viewBox.split(/\s+/).map(Number);
clone.setAttribute('width', `${width}${props.units}`);
clone.setAttribute('height', `${height}${props.units}`);
}
clone.removeAttribute('class');

// Serialize the cleaned-up SVG.
const svgContent = new XMLSerializer().serializeToString(clone);

// Create a Blob from the SVG content
const blob = new Blob([svgContent], { type: 'image/svg+xml' });

// Create a URL for the Blob
const url = URL.createObjectURL(blob);

// Create a temporary link element
const link = document.createElement('a');
link.href = url;
link.download = 'layout.svg';

// Append link to body, click it, and remove it
document.body.appendChild(link);
link.click();
document.body.removeChild(link);

// Clean up the URL object
URL.revokeObjectURL(url);
};
Expand All @@ -40,8 +61,8 @@ export default function SVGDownloadButton(props: {className: string}) {
onClick={handleDownload}>
Download SVG
</button>
<a
className="block font-medium text-blue-600 dark:text-blue-500 hover:underline"
<a
className="block font-medium text-blue-600 dark:text-blue-500 hover:underline"
href="https://github.com/3ach/table/issues"
>
Report a bug or request a feature
Expand Down
7 changes: 7 additions & 0 deletions src/components/TableEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ function propertyNameToLabel(name: keyof TableEditable): string {
case "flatOutsideBuffer": return "Flat rail outside buffer"
case "railInsideBuffer": return "Tube rail inside buffer"
case "railOutsideBuffer": return "Tube rail outside buffer"
case "bitDiameter": return "CNC bit diameter"
}
}

Expand Down Expand Up @@ -94,6 +95,12 @@ export default function TableEditor(props: TableEditorProps) {
table={props.table}
updateTable={props.updateTable}
/>
<TablePropEditor
itemName={propertyNameToLabel("bitDiameter")}
propName="bitDiameter"
table={props.table}
updateTable={props.updateTable}
/>
</div>
<div className="inline-block p-1.5">
<TablePropEditor
Expand Down
1 change: 1 addition & 0 deletions src/components/TablePropEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export default function TablePropEditor(props: TablePropEditorProps) {
props.table.flatInsideBuffer,
props.table.railOutsideBuffer,
props.table.railInsideBuffer,
props.table.bitDiameter,
props.table.units,
props.table.configuration,
);
Expand Down
12 changes: 8 additions & 4 deletions src/components/TestParts.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { SVG } from '@svgdotjs/svg.js';
import { SVGComponent, SVGProps } from './SVGComponent';
import { Table } from '../models/Table';
import { dogBoneCorner } from '../lib/dogbone';

interface TestPartsProps extends SVGProps {
table: Table,
Expand All @@ -10,21 +11,24 @@ export default class TestParts extends SVGComponent<TestPartsProps> {
svg() {
const thickness = this.props.table.thickness;
const material = this.props.table.material;
const r = this.props.table.dogBoneRadius;
const upDepth = (thickness / 2) - (thickness / 50);
const downDepth = (thickness / 2) + (thickness / 50);

let pathstr = `M 0 0`
pathstr += `L ${material * 3} 0`
pathstr += `L ${material * 3} ${thickness}`
pathstr += `L ${material * 2} ${thickness}`
pathstr += `L ${material * 2} ${(thickness / 2) - (thickness / 50)}`
pathstr += `L ${material} ${(thickness / 2) - (thickness / 50)}`
pathstr += dogBoneCorner(material * 2, upDepth, 0, -1, -1, 0, r)
pathstr += dogBoneCorner(material, upDepth, -1, 0, 0, 1, r)
pathstr += `L ${material} ${thickness}`
pathstr += `L 0 ${thickness}`
pathstr += 'z'

pathstr += `M ${material * 3.5} 0`
pathstr += `L ${material * 4.5} 0`
pathstr += `L ${material * 4.5} ${(thickness / 2) + (thickness / 50)}`
pathstr += `L ${material * 5.5} ${(thickness / 2) + (thickness / 50)}`
pathstr += dogBoneCorner(material * 4.5, downDepth, 0, 1, 1, 0, r)
pathstr += dogBoneCorner(material * 5.5, downDepth, 1, 0, 0, -1, r)
pathstr += `L ${material * 5.5} 0`
pathstr += `L ${material * 6.5} 0`
pathstr += `L ${material * 6.5} ${thickness}`
Expand Down
18 changes: 13 additions & 5 deletions src/components/XSpar.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { SVG } from '@svgdotjs/svg.js';
import { SVGComponent, SVGProps } from './SVGComponent';
import { Table } from '../models/Table';
import { dogBoneCorner } from '../lib/dogbone';

interface XSparProps extends SVGProps {
table: Table,
Expand All @@ -19,6 +20,8 @@ export default class XSpar extends SVGComponent<XSparProps> {
const yMortises = this.props.table.ySparCount;
const overhang = this.props.table.overhang;
const yGap = this.props.table.ySparGap;
const r = this.props.table.dogBoneRadius;
const depth = (thickness / 2) + (thickness / 50);

let pathstr = '';
const start = flatBuffer + flatOutsideBuffer + overhang - (xShrink / 2);
Expand All @@ -31,13 +34,18 @@ export default class XSpar extends SVGComponent<XSparProps> {
for (let mortise = 0; mortise < yMortises; mortise++) {
const x = (mortise * yGap) + start;
if (x != 0) {
pathstr += `L ${x} 0`;
pathstr += `L ${x} 0`;
// bottom-left corner: down the left wall, relieve, across the base
pathstr += dogBoneCorner(x, depth, 0, 1, 1, 0, r);
} else {
pathstr += `L ${x} ${depth}`;
}
pathstr += `L ${x} ${(thickness / 2) + (thickness / 50)}`;
pathstr += `L ${x + material} ${(thickness / 2) + (thickness / 50)}`;

if (x + material != xCut) {
pathstr += `L ${x + material} 0`;
// bottom-right corner: across the base, relieve, up the right wall
pathstr += dogBoneCorner(x + material, depth, 1, 0, 0, -1, r);
pathstr += `L ${x + material} 0`;
} else {
pathstr += `L ${x + material} ${depth}`;
}
}

Expand Down
17 changes: 11 additions & 6 deletions src/components/YSpar.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { SVG } from '@svgdotjs/svg.js';
import { SVGComponent, SVGProps } from './SVGComponent';
import { Table } from '../models/Table';
import { dogBoneCorner } from '../lib/dogbone';

interface YSparProps extends SVGProps {
table: Table,
Expand All @@ -14,17 +15,21 @@ export default class YSpar extends SVGComponent<YSparProps> {
const material = this.props.table.material;
const xMortises = this.props.table.xSparCount - 1;
const xGap = this.props.table.xSparGap;
const r = this.props.table.dogBoneRadius;
const depth = (thickness / 2) - (thickness / 50);

let pathstr = `M 0 0`
pathstr += `L 0 ${(thickness / 2) - (thickness / 50)}`;
pathstr += `L 0 ${depth}`;
for (let mortise = 0; mortise < xMortises; mortise++) {
const x = (mortise * xGap);
pathstr += `L ${x + material} ${(thickness / 2) - (thickness / 50)}`;
pathstr += `L ${x + material} ${thickness}`;
pathstr += `L ${x + xGap} ${thickness}`;
pathstr += `L ${x + xGap} ${(thickness / 2) - (thickness / 50)}`;
// floor -> right corner -> down the wall
pathstr += dogBoneCorner(x + material, depth, 1, 0, 0, 1, r);
pathstr += `L ${x + material} ${thickness}`;
pathstr += `L ${x + xGap} ${thickness}`;
// up the wall -> left corner -> floor
pathstr += dogBoneCorner(x + xGap, depth, 0, -1, 1, 0, r);
}
pathstr += `L ${yCut} ${(thickness / 2) - (thickness / 50)}`
pathstr += `L ${yCut} ${depth}`
pathstr += `L ${yCut} 0`
pathstr += 'z'

Expand Down
33 changes: 33 additions & 0 deletions src/lib/dogbone.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Emits the path commands for one inside corner at vertex (vx, vy), integrating
// a minimal 45deg dog-bone (per https://fablab.ruc.dk/more-elegant-cnc-dogbones/):
// a circle of the bit radius r is centred r into the slot along the corner
// bisector, so it passes exactly through the corner point. The outline joins the
// circle sqrt(2)*r before the corner, sweeps the semicircle through the corner
// point, and rejoins the other wall sqrt(2)*r past it. The relief bulges only
// ~0.29*r past each wall - the least material removed while letting the mating
// square corner seat fully.
//
// (inDx, inDy) is the unit direction the outline travels *into* the corner;
// (outDx, outDy) is the unit direction it leaves along.
//
// r <= 0 returns a plain corner (`L vx vy`), so dog-bones can be disabled.
export function dogBoneCorner(
vx: number, vy: number,
inDx: number, inDy: number,
outDx: number, outDy: number,
r: number,
): string {
if (r <= 0) return `L ${vx} ${vy} `;

// The circle through the corner crosses each wall sqrt(2)*r from the vertex.
const setback = Math.SQRT2 * r;
const ax = vx - inDx * setback;
const ay = vy - inDy * setback;
const bx = vx + outDx * setback;
const by = vy + outDy * setback;

// Semicircle through the corner point, swept towards the material side.
const sweep = (inDx * outDy - inDy * outDx) > 0 ? 1 : 0;

return `L ${ax} ${ay} A ${r} ${r} 0 0 ${sweep} ${bx} ${by} `;
}
13 changes: 12 additions & 1 deletion src/models/Table.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export interface TableEditable {
flatInsideBuffer: number;
railOutsideBuffer: number;
railInsideBuffer: number;
bitDiameter: number;
}

export class Table implements TableEditable {
Expand All @@ -33,6 +34,7 @@ export class Table implements TableEditable {
flatInsideBuffer: number;
railOutsideBuffer: number;
railInsideBuffer: number;
bitDiameter: number;
units: Units;
configuration: Configuration;

Expand All @@ -51,7 +53,8 @@ export class Table implements TableEditable {
flatInsideBuffer: number,
railOutsideBuffer: number,
railInsideBuffer: number,
units: Units,
bitDiameter: number,
units: Units,
configuration: Configuration
) {
this.xCut = xCut;
Expand All @@ -68,6 +71,7 @@ export class Table implements TableEditable {
this.flatInsideBuffer = flatInsideBuffer;
this.railOutsideBuffer = railOutsideBuffer;
this.railInsideBuffer = railInsideBuffer;
this.bitDiameter = bitDiameter;
this.units = units;
this.configuration = configuration;
}
Expand Down Expand Up @@ -220,6 +224,10 @@ export class Table implements TableEditable {
}[this.units];
}

get dogBoneRadius(): number {
return this.bitDiameter / 2;
}

get inMillimeters(): Table {
const convert = {
"mm": (x: number) => x,
Expand All @@ -242,6 +250,7 @@ export class Table implements TableEditable {
convert(this.flatInsideBuffer),
convert(this.railOutsideBuffer),
convert(this.railInsideBuffer),
convert(this.bitDiameter),
"mm",
this.configuration,
)
Expand Down Expand Up @@ -269,6 +278,7 @@ export class Table implements TableEditable {
convert(this.flatInsideBuffer),
convert(this.railOutsideBuffer),
convert(this.railInsideBuffer),
convert(this.bitDiameter),
"cm",
this.configuration,
)
Expand Down Expand Up @@ -296,6 +306,7 @@ export class Table implements TableEditable {
convert(this.flatInsideBuffer),
convert(this.railOutsideBuffer),
convert(this.railInsideBuffer),
convert(this.bitDiameter),
"in",
this.configuration,
)
Expand Down