03 - Design Generator (2D SVG Preview)
Generate 2D CAD-style drawings from ML parameters (45 min)
2D Design Preview Component
Create src/app/(core)/cad-cam-demo/components/DesignPreview.tsx:
"use client";
import { DesignParams } from "../utils/types";
interface Props {
params: DesignParams | null;
}
export function DesignPreview({ params }: Props) {
if (!params) {
return (
<div className="bg-white rounded-xl shadow-md p-6">
<h3 className="text-lg font-semibold mb-4">2D Design Preview</h3>
<div className="aspect-square bg-gray-100 rounded-lg flex items-center justify-center">
<span className="text-gray-400">Waiting for design parameters...</span>
</div>
</div>
);
}
// Scale factor for SVG (1m = 100px)
const scale = 80;
const cx = 250; // Center X
const cy = 200; // Center Y
const radius = (params.tankDiameter / 2) * scale;
const pipeRadius = (params.inletDiameter / 2000) * scale; // mm to m to px
// Generate baffle positions
const baffles = Array.from({ length: params.baffleCount }, (_, i) => {
const angle = (i * 360) / params.baffleCount;
return angle;
});
// Generate aeration slot positions
const aerationSlots = Array.from({ length: params.aerationSlots }, (_, i) => {
const angle = (i * 360) / params.aerationSlots;
const r = radius * 0.6;
return {
x: cx + r * Math.cos((angle * Math.PI) / 180),
y: cy + r * Math.sin((angle * Math.PI) / 180),
};
});
return (
<div className="bg-white rounded-xl shadow-md p-6">
<h3 className="text-lg font-semibold mb-4">2D Design Preview (Top View)</h3>
<svg viewBox="0 0 500 400" className="w-full border rounded-lg bg-slate-900" style={{ maxHeight: "400px" }}>
{/* Grid */}
<defs>
<pattern id="grid" width="20" height="20" patternUnits="userSpaceOnUse">
<path d="M 20 0 L 0 0 0 20" fill="none" stroke="#334155" strokeWidth="0.5" />
</pattern>
</defs>
<rect width="100%" height="100%" fill="url(#grid)" />
{/* Tank outline */}
<circle cx={cx} cy={cy} r={radius} fill="none" stroke="#3b82f6" strokeWidth="3" />
{/* Inner wall */}
<circle
cx={cx}
cy={cy}
r={radius - (params.wallThickness / 1000) * scale}
fill="none"
stroke="#60a5fa"
strokeWidth="1"
strokeDasharray="5,5"
/>
{/* Baffles */}
{baffles.map((angle, i) => {
const rad = (angle * Math.PI) / 180;
const innerR = radius * 0.3;
const outerR = radius * 0.9;
return (
<line
key={`baffle-${i}`}
x1={cx + innerR * Math.cos(rad)}
y1={cy + innerR * Math.sin(rad)}
x2={cx + outerR * Math.cos(rad)}
y2={cy + outerR * Math.sin(rad)}
stroke="#f59e0b"
strokeWidth="2"
/>
);
})}
{/* Aeration slots */}
{aerationSlots.map((pos, i) => (
<circle key={`aeration-${i}`} cx={pos.x} cy={pos.y} r="4" fill="#22c55e" />
))}
{/* Inlet pipe (top) */}
<g>
<circle cx={cx} cy={cy - radius} r={pipeRadius * 3} fill="none" stroke="#ef4444" strokeWidth="2" />
<line x1={cx} y1={cy - radius - pipeRadius * 3} x2={cx} y2={cy - radius - 40} stroke="#ef4444" strokeWidth="2" />
<text x={cx + 15} y={cy - radius - 30} fill="#ef4444" fontSize="12">
INLET
</text>
</g>
{/* Outlet pipe (bottom) */}
<g>
<circle cx={cx} cy={cy + radius} r={pipeRadius * 3} fill="none" stroke="#10b981" strokeWidth="2" />
<line x1={cx} y1={cy + radius + pipeRadius * 3} x2={cx} y2={cy + radius + 40} stroke="#10b981" strokeWidth="2" />
<text x={cx + 15} y={cy + radius + 35} fill="#10b981" fontSize="12">
OUTLET
</text>
</g>
{/* Center mark */}
<circle cx={cx} cy={cy} r="3" fill="#94a3b8" />
<line x1={cx - 10} y1={cy} x2={cx + 10} y2={cy} stroke="#94a3b8" strokeWidth="1" />
<line x1={cx} y1={cy - 10} x2={cx} y2={cy + 10} stroke="#94a3b8" strokeWidth="1" />
{/* Dimension: Diameter */}
<g>
<line x1={cx - radius} y1={cy + radius + 60} x2={cx + radius} y2={cy + radius + 60} stroke="#fff" strokeWidth="1" />
<line x1={cx - radius} y1={cy + radius + 55} x2={cx - radius} y2={cy + radius + 65} stroke="#fff" strokeWidth="1" />
<line x1={cx + radius} y1={cy + radius + 55} x2={cx + radius} y2={cy + radius + 65} stroke="#fff" strokeWidth="1" />
<text x={cx} y={cy + radius + 75} fill="#fff" fontSize="11" textAnchor="middle">
Ø {params.tankDiameter}m
</text>
</g>
{/* Legend */}
<g transform="translate(10, 10)">
<rect width="120" height="90" fill="#1e293b" rx="4" />
<circle cx="15" cy="20" r="5" fill="#3b82f6" />
<text x="30" y="24" fill="#fff" fontSize="10">
Tank Wall
</text>
<line x1="10" y1="40" x2="20" y2="40" stroke="#f59e0b" strokeWidth="2" />
<text x="30" y="44" fill="#fff" fontSize="10">
Baffles ({params.baffleCount})
</text>
<circle cx="15" cy="60" r="4" fill="#22c55e" />
<text x="30" y="64" fill="#fff" fontSize="10">
Aeration ({params.aerationSlots})
</text>
<text x="10" y="82" fill="#94a3b8" fontSize="9">
Vol: {params.tankVolume}m³
</text>
</g>
</svg>
{/* Export buttons */}
<div className="flex gap-2 mt-4">
<button onClick={() => exportSVG(params)} className="px-4 py-2 bg-blue-500 text-white rounded-lg text-sm hover:bg-blue-600">
Export SVG
</button>
<button onClick={() => exportDXF(params)} className="px-4 py-2 bg-gray-500 text-white rounded-lg text-sm hover:bg-gray-600">
Export DXF (Mock)
</button>
</div>
</div>
);
}
// Export SVG helper
function exportSVG(params: DesignParams) {
const svgElement = document.querySelector("svg");
if (!svgElement) return;
const svgData = new XMLSerializer().serializeToString(svgElement);
const blob = new Blob([svgData], { type: "image/svg+xml" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `tank_design_${params.tankVolume}m3.svg`;
a.click();
URL.revokeObjectURL(url);
}
// Mock DXF export (shows what would happen)
function exportDXF(params: DesignParams) {
const dxfContent = `
0
SECTION
2
HEADER
0
ENDSEC
0
SECTION
2
ENTITIES
0
CIRCLE
8
TANK
10
0.0
20
0.0
40
${params.tankDiameter / 2}
0
ENDSEC
0
EOF
`.trim();
const blob = new Blob([dxfContent], { type: "application/dxf" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `tank_design_${params.tankVolume}m3.dxf`;
a.click();
URL.revokeObjectURL(url);
}
Side View Component (Optional)
Create src/app/(core)/cad-cam-demo/components/SideViewPreview.tsx:
"use client";
import { DesignParams } from "../utils/types";
interface Props {
params: DesignParams | null;
}
export function SideViewPreview({ params }: Props) {
if (!params) return null;
const scale = 60;
const width = params.tankDiameter * scale;
const height = params.tankHeight * scale;
const wallT = (params.wallThickness / 1000) * scale;
const startX = 150;
const startY = 30;
return (
<div className="bg-white rounded-xl shadow-md p-6">
<h3 className="text-lg font-semibold mb-4">Side View</h3>
<svg viewBox="0 0 500 350" className="w-full border rounded-lg bg-slate-900">
{/* Tank body */}
<rect x={startX} y={startY} width={width} height={height} fill="none" stroke="#3b82f6" strokeWidth="3" />
{/* Inner wall */}
<rect
x={startX + wallT}
y={startY + wallT}
width={width - wallT * 2}
height={height - wallT * 2}
fill="none"
stroke="#60a5fa"
strokeWidth="1"
strokeDasharray="5,5"
/>
{/* Water level indicator */}
<rect x={startX + wallT} y={startY + wallT + height * 0.1} width={width - wallT * 2} height={height * 0.8} fill="#3b82f620" />
<text x={startX + width / 2} y={startY + height / 2} fill="#60a5fa" fontSize="10" textAnchor="middle">
WATER
</text>
{/* Inlet */}
<rect x={startX - 30} y={startY + 20} width="30" height="20" fill="none" stroke="#ef4444" strokeWidth="2" />
<text x={startX - 45} y={startY + 15} fill="#ef4444" fontSize="10">
IN
</text>
{/* Outlet */}
<rect x={startX + width} y={startY + height - 40} width="30" height="20" fill="none" stroke="#10b981" strokeWidth="2" />
<text x={startX + width + 35} y={startY + height - 25} fill="#10b981" fontSize="10">
OUT
</text>
{/* Height dimension */}
<line x1={startX + width + 50} y1={startY} x2={startX + width + 50} y2={startY + height} stroke="#fff" />
<text
x={startX + width + 60}
y={startY + height / 2}
fill="#fff"
fontSize="10"
transform={`rotate(90, ${startX + width + 60}, ${startY + height / 2})`}
>
H: {params.tankHeight}m
</text>
</svg>
</div>
);
}
Next: 04-VISUALIZATION.md
Add 3D visualization with Three.js →