Skip to content

Commit 3a64cd7

Browse files
committed
Merge branch 'feat/dxf-import'
2 parents 9bd1f8b + 607b5db commit 3a64cd7

File tree

10 files changed

+288
-7
lines changed

10 files changed

+288
-7
lines changed

package-lock.json

Lines changed: 38 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,14 @@
3030
"teenyicons": "^0.4.1",
3131
"undo-stacker": "^0.2.1",
3232
"use-local-storage-state": "^19.5.0",
33+
"dxf": "^5.2.0",
3334
"xstate": "^5.18.1"
3435
},
3536
"devDependencies": {
3637
"@biomejs/biome": "^1.9.4",
3738
"@tailwindcss/postcss": "^4.1.3",
3839
"@types/blob-stream": "^0.1.33",
40+
"@types/dxf": "^4.6.10",
3941
"@types/file-saver": "^2.0.7",
4042
"@types/node": "^22.9.1",
4143
"@types/react": "^18.3.3",

src/components/Toolbar.tsx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {exportEntitiesToSvgFile} from '../helpers/import-export-handlers/export-
1010
import {importEntitiesFromJsonFile} from '../helpers/import-export-handlers/import-entities-from-json';
1111
import {importEntitiesFromSvgFile} from '../helpers/import-export-handlers/import-entities-from-svg.ts';
1212
import {importImageFromFile} from '../helpers/import-export-handlers/import-image-from-file';
13+
import {importEntitiesFromDxfFile} from '../helpers/import-export-handlers/import-entities-from-dxf';
1314
import {times} from '../helpers/times';
1415
import {
1516
getActiveLayerId,
@@ -516,6 +517,24 @@ export const Toolbar: FC = () => {
516517
}}
517518
/>
518519
</Button>
520+
<Button
521+
className="relative w-full"
522+
title="Load from DXF file"
523+
dataId="dxf-open-button"
524+
iconName={IconName.VectorDocumentSolid}
525+
onClick={noopClickHandler}
526+
label="DXF"
527+
>
528+
<input
529+
className="absolute inset-0 opacity-0"
530+
type="file"
531+
accept="*.dxf"
532+
onChange={async (evt) => {
533+
await importEntitiesFromDxfFile(evt.target.files?.[0]);
534+
evt.target.files = null;
535+
}}
536+
/>
537+
</Button>
519538
<Button
520539
className="relative w-full"
521540
title="Load from JSON file"
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import {Point} from '@flatten-js/core';
2+
import type {Entities as DxfEntities, Helper} from 'dxf';
3+
import {isNil, uniqBy} from 'es-toolkit';
4+
import {toast} from 'react-toastify';
5+
import {CircleEntity} from '../../entities/CircleEntity.ts';
6+
import type {Entity} from '../../entities/Entity.ts';
7+
import {LineEntity} from '../../entities/LineEntity.ts';
8+
import {getActiveLayerId, getActiveLineColor, getActiveLineWidth, getEntities, setEntities,} from '../../state';
9+
import {toHex} from '../rgb-to-hex-color.ts';
10+
11+
function getDxfLineColor(dxfColor: [number, number, number] | undefined): string {
12+
if (isNil(dxfColor)) {
13+
return getActiveLineColor();
14+
}
15+
return toHex(dxfColor[0], dxfColor[1], dxfColor[2], 1);
16+
}
17+
18+
/**
19+
* Imports entities from a DXF file.
20+
* @param file - The DXF file to import.
21+
*/
22+
export const importEntitiesFromDxfFile = async (file?: File): Promise<void> => {
23+
if (!file) {
24+
toast.warn('No DXF file selected.');
25+
return;
26+
}
27+
28+
console.log(`Attempting to import DXF file: ${file.name}`);
29+
const reader = new FileReader();
30+
31+
reader.onload = async (event) => {
32+
if (!event.target?.result) {
33+
toast.error('Failed to read DXF file content.');
34+
console.error('FileReader event.target.result is null or undefined.');
35+
return;
36+
}
37+
38+
const fileContent = event.target.result as string;
39+
40+
try {
41+
console.log('DXF file content loaded, attempting to parse...');
42+
// Dynamically import the dxf library
43+
const dxf = await import('dxf');
44+
const parsedDxf = new dxf.Helper(fileContent) as Helper;
45+
46+
if (!parsedDxf || !parsedDxf.denormalised) {
47+
toast.error('Failed to parse DXF file. No entities found or invalid format.');
48+
console.error('Parsed DXF data is invalid or contains no entities:', parsedDxf);
49+
return;
50+
}
51+
52+
console.log(`Successfully parsed DXF. Found ${parsedDxf.denormalised.length} entities.`);
53+
const newEntities: Entity[] = [];
54+
const currentLayerId = getActiveLayerId();
55+
const defaultWidth = getActiveLineWidth();
56+
57+
for (const entity of parsedDxf.denormalised) {
58+
// TODO: Coordinate transformation for Y-axis (DXF Y is usually up, canvas Y is down)
59+
// For now, we assume positive Y is down for simplicity matching canvas.
60+
// If DXF typically has Y up, then Y coordinates from DXF might need to be negated or subtracted from canvas height.
61+
if (entity.type === 'LINE') {
62+
const dxfLine = entity as DxfEntities.Line;
63+
if (dxfLine.start && dxfLine.end) {
64+
const startPoint: Point = new Point(dxfLine.start.x, dxfLine.start.y);
65+
const endPoint: Point = new Point(dxfLine.end.x, dxfLine.end.y);
66+
const line = new LineEntity(currentLayerId, startPoint, endPoint);
67+
line.lineColor = getDxfLineColor(dxfLine.colorNumber);
68+
line.lineWidth = dxfLine.thickness || defaultWidth;
69+
line.lineDash = undefined;
70+
newEntities.push(line);
71+
} else {
72+
console.warn('Skipping DXF LINE due to missing or insufficient vertices:', dxfLine);
73+
}
74+
} else if (entity.type === 'CIRCLE') {
75+
const dxfCircle = entity as DxfEntities.Circle;
76+
if (dxfCircle.x && dxfCircle.y && dxfCircle.r) {
77+
const centerPoint = new Point(dxfCircle.x, dxfCircle.y);
78+
const circle = new CircleEntity(currentLayerId, centerPoint, dxfCircle.r);
79+
circle.lineColor = getDxfLineColor(dxfCircle.colorNumber);
80+
circle.lineWidth = defaultWidth;
81+
circle.lineDash = undefined;
82+
newEntities.push(circle);
83+
} else {
84+
console.warn('Skipping DXF CIRCLE due to missing center or radius:', dxfCircle);
85+
}
86+
} else {
87+
console.log(`Unsupported DXF type: ${entity.type}. Skipping.`);
88+
}
89+
}
90+
91+
if (newEntities.length > 0) {
92+
const uniqueEntities = uniqBy(
93+
newEntities,
94+
(entity) =>
95+
`${JSON.stringify(entity.getShape())}|${entity.lineColor}|${entity.lineWidth}|${entity.lineDash}`
96+
);
97+
setEntities([...getEntities(), ...uniqueEntities]);
98+
toast.success(`${uniqueEntities.length} entities imported successfully from DXF!`);
99+
} else {
100+
toast.info('No supported entities found in the DXF file.');
101+
}
102+
} catch (error) {
103+
console.error('Error parsing DXF file:', error);
104+
toast.error('An error occurred while parsing the DXF file. See console for details.');
105+
}
106+
};
107+
108+
reader.onerror = () => {
109+
console.error('FileReader error:', reader.error);
110+
toast.error('Failed to read the DXF file.');
111+
};
112+
113+
reader.readAsText(file);
114+
};

src/helpers/rgb-to-hex-color.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export function toHex(red: number, green: number, blue: number, alpha: number): string {
2+
return (blue | (green << 8) | (red << 16) | (1 << 24)).toString(16).slice(1) + alpha;
3+
}

test/mocks/dxf/circle.dxf

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
0
2+
SECTION
3+
2
4+
ENTITIES
5+
0
6+
CIRCLE
7+
8
8+
0
9+
10
10+
30.0
11+
20
12+
30.0
13+
30
14+
0.0
15+
40
16+
5.0
17+
0
18+
ENDSEC
19+
0
20+
EOF

test/mocks/dxf/empty.dxf

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
0
2+
SECTION
3+
2
4+
ENTITIES
5+
0
6+
ENDSEC
7+
0
8+
EOF

test/mocks/dxf/line-and-circle.dxf

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
0
2+
SECTION
3+
2
4+
ENTITIES
5+
0
6+
LINE
7+
8
8+
0
9+
10
10+
10.0
11+
20
12+
10.0
13+
30
14+
0.0
15+
11
16+
20.0
17+
21
18+
20.0
19+
31
20+
0.0
21+
0
22+
CIRCLE
23+
8
24+
0
25+
10
26+
30.0
27+
20
28+
30.0
29+
30
30+
0.0
31+
40
32+
5.0
33+
0
34+
ENDSEC
35+
0
36+
EOF

test/mocks/dxf/line.dxf

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
0
2+
SECTION
3+
2
4+
ENTITIES
5+
0
6+
LINE
7+
8
8+
0
9+
10
10+
10.0
11+
20
12+
10.0
13+
30
14+
0.0
15+
11
16+
20.0
17+
21
18+
20.0
19+
31
20+
0.0
21+
0
22+
ENDSEC
23+
0
24+
EOF

test/mocks/dxf/unsupported.dxf

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
0
2+
SECTION
3+
2
4+
ENTITIES
5+
0
6+
ARC
7+
8
8+
0
9+
10
10+
50.0
11+
20
12+
50.0
13+
30
14+
0.0
15+
40
16+
10.0
17+
50
18+
0.0
19+
51
20+
90.0
21+
0
22+
ENDSEC
23+
0
24+
EOF

0 commit comments

Comments
 (0)