Building Export Systems From Scratch - Complete Beginner's Guide

What is This Chapter About?

This chapter explains how to export shapes from your system to standard file formats. Users need to get their designs out of your system for use in other tools.

Why Do We Need Export Systems? - Much More Detailed Explanation

Your shapes exist as JavaScript objects in memory (in your program). But users need to do much more than just see them on screen! Let's break down why export systems are essential:

The Problem - Explained Simply:

Your shapes are stored in memory like this:

{ type: 'circle', params: { radius: 50 }, position: { x: 100, y: 100 } }

This is great for your program, but it's useless outside your program! Other tools can't read JavaScript objects.

What Users Need to Do with Their Designs:

1. Share Designs with Others:

  • Email: Send design files to colleagues or clients
  • Upload to websites: Share designs online (GitHub, design galleries, etc.)
  • Cloud storage: Save to Google Drive, Dropbox, etc.
  • Real-world analogy: Like saving a photo so you can share it with friends - you need a file format others can open

2. Use Designs in Other Tools:

  • Illustrator (Adobe): Professional graphics software - needs SVG files
  • AutoCAD: Professional CAD software - needs DXF files
  • Inkscape: Free graphics software - needs SVG files
  • Fusion 360: 3D CAD software - needs DXF or other formats
  • Real-world analogy: Like saving a document as PDF so anyone can open it, even if they don't have Word

3. Manufacture Designs:

  • CNC machines: Computer-controlled cutting machines - need DXF files
  • Laser cutters: Cut material with lasers - need DXF or SVG files
  • 3D printers: Create physical objects - need specific file formats
  • Waterjet cutters: Cut materials with high-pressure water - need DXF files
  • Real-world analogy: Like giving a blueprint to a construction worker - they need it in a format they can understand

4. Embed Designs in Web Pages:

  • Show designs on websites: Display designs in HTML pages
  • SVG format: Works directly in web browsers
  • Interactive designs: Make designs clickable, animated, etc.
  • Real-world analogy: Like embedding a YouTube video in a webpage - you need the right format

Export Systems Bridge the Gap:

Export systems convert your internal representation (JavaScript objects) to external file formats (SVG, DXF files).

The Translation Process:

Your System (JavaScript objects):
  { type: 'circle', radius: 50 }
         ↓ (Export system translates)
File Format (SVG):
  <circle cx="100" cy="100" r="50" />
         ↓
Other Tools Can Use It!

Real-World Analogy - Much More Detailed:

Think of export systems like language translators:

Your System = One Language:

  • JavaScript objects (like speaking English)
  • Other programs don't understand this!

Other Tools = Different Languages:

  • SVG format (like Spanish)
  • DXF format (like French)
  • Each tool speaks a different "language"

Export System = Translator:

  • Converts from your language to theirs
  • Like a translator at the UN - translates between languages so everyone understands

Why This Matters:

Without export systems:

  • Designs are trapped in your program
  • Can't share with others
  • Can't use in other tools
  • Can't manufacture
  • Designs are useless outside your program

With export systems:

  • Designs can be shared anywhere
  • Works with professional tools
  • Can be manufactured
  • Designs are useful everywhere!

The Key Insight:

Export systems make your designs portable - they can go anywhere and work with any tool. Without export, your designs are locked in your program forever!

Real-World Analogy:

Think of export systems like language translators:

  • Your system = One language (JavaScript objects)
  • Other tools = Different languages (SVG, DXF files)
  • Export system = Translator (converts between languages)

Why Export Systems Matter - Explained Simply

The Problem:

Your shapes are stored as JavaScript objects in memory:

{ type: 'circle', params: { radius: 50 }, position: { x: 100, y: 100 } }

But other programs can't read JavaScript objects! They need file formats like:

  • SVG files - For web pages and graphics programs
  • DXF files - For CAD programs and manufacturing machines

The Solution:

Export systems convert your internal shapes to these file formats. Think of it like saving a document:

  • You write in Word (your system)
  • You save as PDF (export system converts it)
  • Others can open the PDF (other programs can use your exported files)

The Challenge: Format Conversion - Explained Simply

Why Conversion is Hard:

Each export format has different requirements:

  1. SVG (Scalable Vector Graphics):

    • XML-based (text format)
    • Uses pixels (screen coordinates)
    • Supports fills and strokes (colors, line styles)
    • Needs coordinate transforms (position, rotation, scale)
  2. DXF (Drawing Exchange Format):

    • CAD format (for manufacturing)
    • Uses units (millimeters or inches)
    • Needs precise geometry (exact measurements)
    • Supports layers (organization)

What You Need to Handle:

  1. Coordinate Conversion:

    • Your shapes are in "world coordinates" (real units like mm)
    • SVG uses pixels (screen coordinates)
    • Need to convert between them
  2. Unit Conversion:

    • Your system uses millimeters (mm)
    • SVG uses pixels (px)
    • DXF uses millimeters (mm)
    • Need to convert units correctly
  3. Transform Application:

    • Shapes have position, rotation, scale
    • Need to apply these in the export format
    • SVG uses transform attributes
    • DXF uses entity coordinates
  4. Style Conversion:

    • Shapes have fill colors, stroke colors, line widths
    • Need to convert to format-specific styles
    • SVG: CSS color values (#FF0000, rgb(), etc.)
    • DXF: Color numbers, line types

Visual Example:

Your shape in memory:
  Circle at (100mm, 50mm) with radius 25mm, fill red

SVG export:
  <circle cx="100" cy="50" r="25" fill="#FF0000" />
  (coordinates converted to pixels, color converted to hex)

DXF export:
  CIRCLE
    10 100.0    (x coordinate in mm)
    20 50.0     (y coordinate in mm)
    40 25.0     (radius in mm)
    62 1        (color number)

The Problem

Shapes are in memory as JavaScript objects. You need to convert them to:

  1. SVG - XML format for web/graphics
  2. DXF - CAD format for manufacturing

Both need:

  • Coordinate conversion (world → screen → export units)
  • Unit conversion (AQUI uses mm, SVG uses pixels, DXF uses mm)
  • Transform application (position, rotation, scale)
  • Style conversion (fill, stroke, colors)

Building SVG Export From Scratch

What is SVG?

SVG stands for "Scalable Vector Graphics." It's an XML-based file format for describing 2D graphics. Think of it as a text file that describes drawings.

Why SVG?

SVG is perfect for web and graphics because:

  • Scalable - Can be resized without losing quality
  • Text-based - Human-readable (you can edit it in a text editor)
  • Web-friendly - Browsers can display SVG directly
  • Tool-compatible - Works with Illustrator, Inkscape, etc.

How It Works:

SVG is XML (like HTML but for graphics). You create elements, set attributes, build a string, download it. This section explains how to build the complete SVG export system step by step.

Real-World Analogy:

Think of SVG export like writing a recipe:

  • Shapes = Ingredients (what you have)
  • SVG export = Recipe (instructions for creating the dish)
  • SVG file = The written recipe (file others can use)

How to Build It Step by Step:

Step 1: Create the Main Export Function

This is the main entry point for SVG export. It takes the interpreter (which has all the shapes), the canvas (for dimensions), and an optional filename, then exports everything to an SVG file.

Understanding the Parameters:

  1. interpreter - The interpreter object that has all the shapes

    • Contains interpreter.env.shapes (Map of all shapes)
    • We need this to get the shapes to export
  2. canvas - The HTML canvas element

    • We need this to get canvas dimensions (width, height)
    • Used to set up the SVG viewBox
  3. filename - Name of the exported file (optional)

    • Defaults to "aqui_drawing.svg" if not provided
    • User can specify custom name like "my_design.svg"

Understanding Optional Chaining (?.):

The ?. operator is called "optional chaining":

  • interpreter?.env?.shapes means "if interpreter exists, check env, if env exists, check shapes"
  • If any part is null/undefined, the whole expression returns undefined
  • This prevents errors like "Cannot read property 'env' of undefined"

Why Validate:

If there are no shapes, there's nothing to export. We throw an error early to:

  • Prevent creating an empty SVG file
  • Give clear feedback to the user
  • Avoid wasting time on unnecessary processing
export function exportToSVG(interpreter, canvas, filename = "aqui_drawing.svg") {
  // Validate shapes exist - if not, throw error early
  if (!interpreter?.env?.shapes) {
    throw new Error('No shapes to export');
  }

If you see an error at this step:

Error: TypeError: Cannot read property 'env' of undefined

  • What this means: interpreter is undefined or null
  • Common causes:
    1. interpreter not passed: Called exportToSVG(undefined, canvas)
    2. interpreter not initialized: Interpreter created but not properly set up
    3. Wrong object passed: Passed wrong object instead of interpreter
  • Fix: Check interpreter exists: if (!interpreter) throw new Error('Interpreter required');, verify interpreter has env property, ensure interpreter is initialized before calling export

Error: TypeError: Cannot read property 'shapes' of undefined

  • What this means: interpreter.env is undefined
  • Common causes:
    1. env not initialized: interpreter.env doesn't exist
    2. env is null: interpreter.env is null
    3. Wrong structure: interpreter doesn't have expected structure
  • Fix: Check env exists: if (!interpreter.env) throw new Error('Interpreter.env required');, verify env has shapes property, ensure interpreter structure is correct

Error: Error: No shapes to export

  • What this means: interpreter.env.shapes is empty or doesn't exist
  • Common causes:
    1. No shapes created: User hasn't created any shapes yet
    2. shapes Map is empty: shapes exists but has no entries
    3. shapes is not a Map: shapes exists but is not the expected type
  • Fix: Check if shapes is empty: if (!interpreter.env.shapes || interpreter.env.shapes.size === 0) throw new Error('No shapes to export');, verify shapes is a Map, ensure shapes are created before export

Error: TypeError: canvas is undefined

  • What this means: canvas parameter is undefined
  • Common causes:
    1. Canvas not passed: Called exportToSVG(interpreter, undefined)
    2. Canvas not found: Canvas element doesn't exist in DOM
    3. Wrong element: Passed wrong element instead of canvas
  • Fix: Check canvas exists: if (!canvas) throw new Error('Canvas required');, verify canvas is HTMLCanvasElement, ensure canvas element exists in DOM

Step 2: Get Canvas Dimensions

Gets the canvas dimensions including width, height, and offset (center position). This information is needed to set up the SVG correctly.

What Canvas Dimensions Include:

  • width - Canvas width in pixels
  • height - Canvas height in pixels
  • offsetX - X offset for centering (usually canvas width / 2)
  • offsetY - Y offset for centering (usually canvas height / 2)

Why We Need This:

The SVG needs to match the canvas size and coordinate system:

  • SVG viewBox should match canvas dimensions
  • SVG coordinate system should match canvas coordinate system
  • Shapes are centered in canvas, so SVG needs offset to match

Understanding the Coordinate System:

Your renderer centers shapes at the canvas center:

  • Canvas center is at (width/2, height/2)
  • Shapes are positioned relative to center
  • SVG needs to replicate this centering
  // Get canvas dimensions (width, height, offset for centering)
  // Needed to set up SVG viewBox and coordinate system
  const canvasDims = getCanvasDimensions(canvas);

If you see an error at this step:

Error: TypeError: getCanvasDimensions is not a function

  • What this means: getCanvasDimensions function doesn't exist or not imported
  • Common causes:
    1. Function not defined: getCanvasDimensions() not implemented
    2. Function not imported: Missing import statement
    3. Typo in function name: getCanvasDimension vs getCanvasDimensions
  • Fix: Implement getCanvasDimensions function, check import statement, verify function name spelling

Error: TypeError: Cannot read property 'width' of undefined

  • What this means: getCanvasDimensions returned undefined or doesn't have width property
  • Common causes:
    1. Function returns undefined: getCanvasDimensions() doesn't return object
    2. Wrong return format: Function returns wrong structure
    3. Canvas invalid: Canvas element invalid, function returns undefined
  • Fix: Check getCanvasDimensions returns object: { width, height, offsetX, offsetY }, verify function implementation, ensure canvas is valid HTMLCanvasElement

Error: Canvas dimensions are 0 or NaN

  • What this means: Canvas width/height are invalid
  • Common causes:
    1. Canvas not sized: Canvas element has no width/height attributes
    2. Canvas hidden: Canvas is display:none, dimensions are 0
    3. Invalid canvas: Canvas element is invalid or not in DOM
  • Fix: Check canvas has width/height: canvas.width > 0 && canvas.height > 0, ensure canvas is visible, verify canvas is properly initialized

Step 3: Create SVG Root Element

The SVG root element is the container for all SVG content. It requires the XML namespace and defines the coordinate system. Creating the SVG element correctly is crucial because it establishes the foundation for all content within it.

Understanding XML Namespaces:

XML namespaces prevent naming conflicts when mixing different XML vocabularies. The SVG namespace (http://www.w3.org/2000/svg) tells XML parsers that elements like <svg>, <circle>, <rect> are SVG elements, not some other XML vocabulary.

Why Use createElementNS() Instead of createElement():

  • createElement("svg"): Creates an element but doesn't associate it with a namespace. This can cause issues in some XML contexts.
  • createElementNS(svgNS, "svg"): Creates an element with the proper namespace. This is the correct way to create SVG elements programmatically.

The SVG Namespace URL:

The URL "http://www.w3.org/2000/svg" is not actually accessed - it's just a unique identifier (URI). It identifies the SVG specification version 1.0. Even though it looks like a web address, it's just a namespace identifier that browsers and parsers recognize.

Understanding SVG Attributes:

  1. xmlns Attribute:

    • Stands for "XML Namespace"
    • Declares the default namespace for this element and its children
    • Required for valid SVG
    • Must be set to the SVG namespace URL
  2. width and height Attributes:

    • Specify the intrinsic dimensions of the SVG
    • In pixels (px unit)
    • Used by browsers/systems to know how big to render the SVG
    • We use toFixed(2) to format to 2 decimal places (prevents floating-point precision issues in XML)
  3. viewBox Attribute:

    • Defines the coordinate system for the SVG content
    • Format: "minX minY width height"
    • "0 0 width height" means: "content starts at (0,0) and extends width × height"
    • This matches our canvas coordinate system (0,0 at top-left, extending right and down)

Why These Attributes:

  • xmlns: Required XML namespace for valid SVG. Without it, XML parsers might not recognize SVG elements correctly.
  • width/height: Canvas dimensions in pixels (what the SVG will render at). These tell rendering systems the intended size of the SVG.
  • viewBox: Defines the coordinate system, matching the canvas (0,0 to width,height). This ensures shapes appear in the correct positions.

Understanding toFixed(2):

  • Formats numbers to 2 decimal places
  • Prevents floating-point precision issues (e.g., 100.00000000001 becomes "100.00")
  • Makes XML output cleaner and more readable
  • Example: 100.456789.toFixed(2) = "100.46"

Why Set Attributes After Creating Element:

We create the element first, then set attributes. This is necessary because:

  1. Element must exist before you can set attributes on it
  2. Some attributes might depend on element type
  3. Clear separation of creation and configuration
  // Create SVG element with proper XML namespace (required for valid SVG)
  // The namespace URL identifies this as SVG content (not just any XML)
  const svgNS = "http://www.w3.org/2000/svg";

  // Use createElementNS (not createElement) to create element with namespace
  // This ensures proper XML/SVG parsing and rendering
  const svg = document.createElementNS(svgNS, "svg");

  // Set SVG attributes: namespace, dimensions, and viewBox (coordinate system)
  // xmlns: Required XML namespace declaration (tells parsers this is SVG)
  svg.setAttribute("xmlns", svgNS);

  // width/height: Intrinsic dimensions in pixels (what the SVG should render at)
  // toFixed(2) formats numbers to 2 decimal places (cleaner XML, prevents precision issues)
  svg.setAttribute("width", `${canvasDims.width.toFixed(2)}px`);
  svg.setAttribute("height", `${canvasDims.height.toFixed(2)}px`);

  // viewBox: Defines coordinate system (minX minY width height)
  // "0 0 width height" means content starts at (0,0) and extends width×height
  // This matches the canvas coordinate system
  svg.setAttribute("viewBox", `0 0 ${canvasDims.width.toFixed(2)} ${canvasDims.height.toFixed(2)}`);

Step 4: Create Main Group with Transform

The renderer centers shapes at the canvas center. We need to replicate this in SVG using a transform. The main group applies a translation to center the coordinate system, just like the renderer does.

  // Create main group with transform to center coordinate system (matches renderer)
  const mainGroup = document.createElementNS(svgNS, "g");
  mainGroup.setAttribute("transform", `translate(${canvasDims.offsetX}, ${canvasDims.offsetY})`);
  svg.appendChild(mainGroup);

Step 5: Convert Each Shape

Loop through all shapes in the interpreter's environment and convert each one to an SVG element. Shapes used in boolean operations are marked _consumedByBoolean - these are part of the result shape, not separate entities, so we skip them and only export the result shape.

  // Convert each shape to SVG element
  interpreter.env.shapes.forEach((shape, shapeName) => {
    // Skip shapes consumed by boolean operations (part of result, not separate)
    if (shape._consumedByBoolean) return;

    // Convert shape to SVG (handles unit conversion, styling, transforms)
    const shapeElement = createSVGShapeWithMMConversion(shape, shapeName, svgNS, canvasTransform);

    // Add shape element to main group
    if (shapeElement) {
      mainGroup.appendChild(shapeElement);
    }
  });

Step 6: Serialize and Download

We need to convert the DOM element to an XML string so we can download it as a file. XMLSerializer handles this conversion, including proper XML formatting.

  // Serialize SVG DOM element to XML string
  const serializer = new XMLSerializer();
  const svgString = serializer.serializeToString(svg);

  // Download the SVG file
  downloadSVG(svgString, filename, canvasDims);
}

The Complete Function:

export function exportToSVG(interpreter, canvas, filename = "aqui_drawing.svg") {
  if (!interpreter?.env?.shapes) {
    throw new Error('No shapes to export');
  }

  // Get canvas dimensions
  const canvasDims = getCanvasDimensions(canvas);

  // Create SVG element
  const svgNS = "http://www.w3.org/2000/svg";
  const svg = document.createElementNS(svgNS, "svg");

  svg.setAttribute("xmlns", svgNS);
  svg.setAttribute("width", `${canvasDims.width.toFixed(2)}px`);
  svg.setAttribute("height", `${canvasDims.height.toFixed(2)}px`);
  svg.setAttribute("viewBox", `0 0 ${canvasDims.width.toFixed(2)} ${canvasDims.height.toFixed(2)}`);

  // Create main group (centered, like renderer)
  const mainGroup = document.createElementNS(svgNS, "g");
  mainGroup.setAttribute("transform", `translate(${canvasDims.offsetX}, ${canvasDims.offsetY})`);
  svg.appendChild(mainGroup);

  // Convert each shape
  interpreter.env.shapes.forEach((shape, shapeName) => {
    if (shape._consumedByBoolean) return; // Skip shapes used in boolean ops

    const shapeElement = createSVGShapeWithMMConversion(shape, shapeName, svgNS, canvasTransform);
    if (shapeElement) {
      mainGroup.appendChild(shapeElement);
    }
  });

  // Serialize and download
  const serializer = new XMLSerializer();
  const svgString = serializer.serializeToString(svg);
  downloadSVG(svgString, filename, canvasDims);
}

Building This Step by Step:

  1. Create exportToSVG() function with interpreter, canvas, and filename
  2. Validate shapes exist, throw error if not
  3. Get canvas dimensions (width, height, offset)
  4. Create SVG root element with namespace
  5. Set SVG attributes (xmlns, width, height, viewBox)
  6. Create main group with transform (centers coordinate system)
  7. Loop through shapes, skip consumed ones
  8. Convert each shape to SVG element
  9. Add shape elements to main group
  10. Serialize SVG to XML string
  11. Download the SVG file

Unit Conversion

AQUI uses millimeters (mm) as its internal unit system because it's common in CAD and manufacturing. SVG typically uses pixels (px) because it's web-based. We need to convert between them to ensure shapes appear at the correct size in the exported SVG.

Understanding the Conversion:

The conversion is based on screen resolution (DPI - Dots Per Inch):

  • 96 DPI: Standard web resolution (browsers typically use 96 pixels per inch)
  • 25.4 mm per inch: Standard metric conversion (1 inch = 25.4 millimeters)

Calculating the Conversion Factor:

To convert millimeters to pixels:

  1. First, convert mm to inches: mm ÷ 25.4 = inches
  2. Then, convert inches to pixels: inches × 96 = pixels
  3. Combined: pixels = mm × (96 ÷ 25.4)
  4. Conversion factor: 96 ÷ 25.4 ≈ 3.779527559 pixels per millimeter

Why 96 DPI is Standard:

96 DPI (dots per inch) became the standard because:

  • Historical: Early displays used this resolution
  • CSS Standard: CSS assumes 96 DPI (1 inch = 96px)
  • Web Standard: Browsers and web standards use 96 DPI as default
  • Consistency: Ensures SVG renders consistently across browsers

Other Common DPI Values:

  • 72 DPI: Print resolution (not common in web)
  • 300 DPI: High-quality print resolution
  • 120 DPI: High-DPI displays (Retina, etc.) - browsers scale automatically

For web SVG, 96 DPI is the correct choice because browsers scale SVG appropriately for high-DPI displays automatically.

Mathematical Derivation:

1 inch = 25.4 mm (metric conversion)
1 inch = 96 px (web standard)
Therefore: 96 px = 25.4 mm
Conversion: 1 mm = 96/25.4 px ≈ 3.7795 px

Edge Cases and Validation:

The mmToPx function handles edge cases:

  • Not a number: Returns value as-is (might be a string, already in pixels, etc.)
  • NaN: Returns NaN (invalid input)
  • Infinity: Returns Infinity (handles edge cases)
  • Negative numbers: Works correctly (negative mm → negative px)

Why Check Type:

We check typeof mmValue === 'number' because:

  • Parameters might come from user input (could be strings)
  • Some values might already be in pixels
  • Defensive programming (handles unexpected input gracefully)

Example Conversions:

10 mm → 10 × 3.7795 ≈ 37.8 px
50 mm → 50 × 3.7795 ≈ 189.0 px
100 mm → 100 × 3.7795 ≈ 378.0 px
1 mm → 1 × 3.7795 ≈ 3.78 px (very small, might appear as 1px on screen)

Precision Considerations:

The conversion factor 96/25.4 results in a repeating decimal (approximately 3.779527559...). JavaScript's floating-point arithmetic handles this automatically. For display, we round to reasonable precision. The slight imprecision is acceptable for graphics (sub-pixel differences aren't visible).

Alternative Approaches:

Some systems use different conversion strategies:

  • Direct pixel mapping: 1 unit = 1 pixel (simpler but loses real-world size)
  • Custom DPI: Allow user to specify DPI (more flexible but complex)
  • Unit attributes: Store units in SVG (SVG supports this, but not all tools)

For our use case, 96 DPI is the best choice because it matches web standards and ensures consistent rendering.

// Conversion factor: 96 DPI (standard web resolution), 25.4mm per inch
// Calculation: 96 pixels per inch ÷ 25.4 mm per inch = pixels per mm
// Result: Approximately 3.7795 pixels per millimeter
const MM_TO_PIXELS = 96 / 25.4;

function mmToPx(mmValue) {
  // Convert millimeters to pixels using the conversion factor
  // Type check: Only convert if input is a number
  // This handles edge cases (strings, undefined, etc.) gracefully
  // If not a number, return value as-is (might already be in pixels or invalid)
  return typeof mmValue === 'number' ? mmValue * MM_TO_PIXELS : mmValue;
}

Shape Conversion

Each shape type needs its own converter:

function createSVGShapeWithMMConversion(shape, shapeName, svgNS, canvasTransform) {
  const { type, params, transform } = shape;

  // Convert parameters from MM to pixels
  const convertedParams = convertShapeParams(type, params);

  let element = null;

  switch (type) {
    case 'circle':
      element = createSVGCircleMM(convertedParams, svgNS);
      break;
    case 'rectangle':
      element = createSVGRectangleMM(convertedParams, svgNS);
      break;
    // ... more shape types
  }

  if (element) {
    // Apply styling
    applyShapeStyleMM(element, convertedParams);

    // Apply transform
    applyRendererTransformToSVG(element, transform, canvasTransform);
  }

  return element;
}

Transform Application

Shapes have transforms (position, rotation, scale). You need to apply them in SVG. The transform order matters: translate → rotate → scale.

Why negative rotation? Canvas Y is flipped (positive Y is down). SVG Y is normal (positive Y is up). So you negate the rotation to account for the coordinate system difference.

function applyRendererTransformToSVG(element, transform, canvasTransform) {
  if (!transform) return;

  // Extract transform components
  const worldX = transform.position?.[0] || 0;
  const worldY = transform.position?.[1] || 0;
  const rotation = transform.rotation || 0;
  const scale = transform.scale || [1, 1];

  // Convert world to screen coordinates (matching renderer)
  const screenX = worldX * canvasTransform.scale * canvasTransform.zoomLevel + canvasTransform.panOffset.x;
  const screenY = -worldY * canvasTransform.scale * canvasTransform.zoomLevel + canvasTransform.panOffset.y;

  // Build transform string (order: translate → rotate → scale)
  let transformStr = '';

  if (screenX !== 0 || screenY !== 0) {
    transformStr += `translate(${screenX.toFixed(3)}, ${screenY.toFixed(3)}) `;
  }

  if (rotation !== 0) {
    // Negative rotation because SVG Y is flipped compared to canvas
    transformStr += `rotate(${(-rotation).toFixed(3)}) `;
  }

  if (scale[0] !== 1 || scale[1] !== 1) {
    transformStr += `scale(${scale[0].toFixed(6)}, ${scale[1].toFixed(6)}) `;
  }

  // Apply transform if any components exist
  if (transformStr.trim()) {
    element.setAttribute("transform", transformStr.trim());
  }
}

Style Application

Convert AQUI styles to SVG attributes. SVG uses CSS color values and stroke/fill attributes.

Color Resolution:

  • Check for explicit strokeColor or fillColor
  • Fall back to generic color property
  • Default to black (#000000) if no color specified

Fill Handling:

  • If fill or filled is true, use fill color (or default)
  • If fillColor is specified, use it
  • Otherwise, set fill to "none" (transparent)
function applyShapeStyleMM(element, convertedParams) {
  // Apply stroke (color and width)
  const strokeColor = convertedParams.strokeColor || convertedParams.color || "#000000";
  const strokeWidth = convertedParams.strokeWidth || mmToPx(0.5);
  element.setAttribute("stroke", strokeColor);
  element.setAttribute("stroke-width", strokeWidth.toFixed(3));

  // Apply fill (check fill flag, fillColor, or default to none)
  let fill = "none";
  if (convertedParams.fill === true || convertedParams.filled === true) {
    fill = convertedParams.fillColor || convertedParams.color || "rgba(0, 0, 0, 0.1)";
  } else if (convertedParams.fillColor) {
    fill = convertedParams.fillColor;
  }
  element.setAttribute("fill", fill);

  // Set line caps and joins for smooth appearance
  element.setAttribute("stroke-linecap", "round");
  element.setAttribute("stroke-linejoin", "round");
}

Building SVG Download From Scratch

Create a blob and trigger download. This method handles the browser download mechanism for SVG files.

How to Build It Step by Step:

Step 1: Create Blob from SVG String

A Blob represents file data in memory. We need to create a Blob from the SVG string so we can create a download URL. The MIME type tells the browser it's an SVG file.

Step 2: Create Object URL

URL.createObjectURL() creates a temporary URL that points to the blob. This URL can be used in a download link. The URL is only valid while the page is open.

Step 3: Create Download Link

An anchor element with the download attribute triggers a file download when clicked. We set href to the blob URL and download to the filename.

Step 4: Trigger Download

The link must be in the DOM for the click to work. We add it, click it, then remove it. This is a common pattern for programmatic downloads.

Step 5: Clean Up Object URL

URL.revokeObjectURL() frees up memory by releasing the blob reference. We delay slightly (100ms) to ensure the download has started before revoking the URL.

function downloadSVG(svgString, filename, canvasDims) {
  // Create blob from SVG string (MIME type tells browser it's SVG)
  const blob = new Blob([svgString], { 
    type: 'image/svg+xml;charset=utf-8' 
  });

  // Create temporary URL pointing to blob
  const url = URL.createObjectURL(blob);

  // Create anchor element for download
  const link = document.createElement('a');
  link.href = url;
  link.download = filename;
  link.style.display = 'none';

  // Add to DOM, click to trigger download, then remove
  document.body.appendChild(link);
  link.click();
  document.body.removeChild(link);

  // Revoke URL after delay to free memory (ensure download started first)
  setTimeout(() => URL.revokeObjectURL(url), 100);
}

The Complete Function:

function downloadSVG(svgString, filename, canvasDims) {
  const blob = new Blob([svgString], { 
    type: 'image/svg+xml;charset=utf-8' 
  });

  const url = URL.createObjectURL(blob);
  const link = document.createElement('a');
  link.href = url;
  link.download = filename;
  link.style.display = 'none';

  document.body.appendChild(link);
  link.click();
  document.body.removeChild(link);

  setTimeout(() => URL.revokeObjectURL(url), 100);
}

Building This Step by Step:

  1. Create downloadSVG() function with SVG string, filename, and canvas dimensions
  2. Create Blob from SVG string with proper MIME type
  3. Create object URL from blob
  4. Create anchor element
  5. Set href to object URL
  6. Set download attribute to filename
  7. Hide the link (display: none)
  8. Add link to DOM
  9. Programmatically click the link
  10. Remove link from DOM
  11. Revoke object URL after delay to free memory

Building DXF Export From Scratch

DXF is a text format for CAD. It's more complex than SVG - you need to write headers, tables, entities, etc. This section explains how to build the complete DXF export system step by step.

How to Build It Step by Step:

Step 1: Create the Main Export Function

If there are no shapes, there's nothing to export. Throw an error early to prevent creating an empty DXF file.

Step 2: Initialize DXF Builder

DXF has a specific structure with sections (header, tables, entities). The builder helps us construct it correctly by managing the sections and ensuring proper formatting.

Step 3: Build DXF Sections

DXF requires specific sections in order:

  • Header: Metadata about the file (version, units, etc.)
  • Tables: Style definitions, layers, etc.
  • Entities: The actual shapes (circles, lines, etc.)

Step 4: Convert Shapes to DXF Entities

Shapes used in boolean operations are marked _consumedByBoolean. These are part of the result shape, not separate entities. We only export the result shape, not the individual components.

Step 5: Finalize and Download

The builder assembles all sections into a complete DXF file string. This string can then be downloaded as a file.

export function exportToDXF(interpreter, canvas, filename = "aqui_drawing.dxf") {
  // Validate shapes exist
  if (!interpreter?.env?.shapes) {
    throw new Error('No shapes to export');
  }

  // Get canvas dimensions and create DXF builder
  const canvasDims = getCanvasDimensions(canvas);
  const dxfContent = new DXFBuilder();

  // Build DXF sections (header, tables, entities)
  dxfContent.addHeader(canvasDims);
  dxfContent.addTables();
  dxfContent.startEntities();

  // Convert each shape to DXF entity
  interpreter.env.shapes.forEach((shape, shapeName) => {
    // Skip shapes consumed by boolean operations
    if (shape._consumedByBoolean) return;

    const dxfEntity = createDXFShapeWithMMConversion(shape, shapeName, canvasTransform);
    if (dxfEntity) {
      dxfContent.addEntity(dxfEntity);
    }
  });

  // End entities section, build DXF string, and download
  dxfContent.endEntities();
  const dxfString = dxfContent.build();
  downloadDXF(dxfString, filename, canvasDims);
}

The Complete Function:

export function exportToDXF(interpreter, canvas, filename = "aqui_drawing.dxf") {
  if (!interpreter?.env?.shapes) {
    throw new Error('No shapes to export');
  }

  const canvasDims = getCanvasDimensions(canvas);
  const dxfContent = new DXFBuilder();

  // Build DXF sections
  dxfContent.addHeader(canvasDims);
  dxfContent.addTables();
  dxfContent.startEntities();

  // Convert shapes to DXF entities
  interpreter.env.shapes.forEach((shape, shapeName) => {
    if (shape._consumedByBoolean) return;

    const dxfEntity = createDXFShapeWithMMConversion(shape, shapeName, canvasTransform);
    if (dxfEntity) {
      dxfContent.addEntity(dxfEntity);
    }
  });

  dxfContent.endEntities();

  // Build and download
  const dxfString = dxfContent.build();
  downloadDXF(dxfString, filename, canvasDims);
}

Building This Step by Step:

  1. Create exportToDXF() function with interpreter, canvas, and filename
  2. Validate shapes exist, throw error if not
  3. Get canvas dimensions
  4. Create DXF builder instance
  5. Add header section with metadata
  6. Add tables section (minimal for basic export)
  7. Start entities section
  8. Loop through shapes, skip consumed ones
  9. Convert each shape to DXF entity
  10. Add entities to DXF content
  11. End entities section
  12. Build DXF string from all sections
  13. Download the DXF file

Building DXF Builder From Scratch

DXF has a specific structure. You need a builder to construct it correctly. This section explains how to build the DXF builder step by step.

How to Build It Step by Step:

Step 1: Create the DXFBuilder Class

DXF files are built from sections (header, tables, entities). We store each section as a string in an array, then join them at the end to create the complete file.

Step 2: Add Header Section

The header contains metadata about the file. DXF format uses codes: 0 = start, 2 = name, 9 = variable, etc.

  • AC1015: AutoCAD R2000 format (widely compatible)
  • $INSUNITS = 4: Millimeters (matches AQUI's unit system)
  • The format is: code on one line, value on next line

Step 3: Add Tables Section

For basic export, we can use minimal tables (empty). More complex exports would add layer tables, style tables, etc. The empty tables section is valid and keeps the file structure correct.

Step 4: Start Entities Section

We collect entities in a separate array, then add them all at once when ending the section. This makes it easier to add entities incrementally.

Step 5: Add Entity

Entities are added to the array as they're created. They'll be added to the sections array when we end the entities section.

Step 6: End Entities Section

The spread operator (...) adds all entities to the sections array at once. This is more efficient than pushing them one by one.

Step 7: Build Final DXF String

DXF files must end with EOF (End Of File) marker. This tells CAD software that the file is complete. We join all sections with newlines (DXF format uses newlines to separate codes and values).

class DXFBuilder {
  constructor() {
    // Initialize sections array (stores header, tables, entities as strings)
    this.sections = [];
  }

  addHeader(canvasDims) {
    // Build header section with metadata
    // $ACADVER: AutoCAD version (AC1015 = R2000 format, widely compatible)
    // $INSUNITS: Insertion units (4 = millimeters, matches AQUI)
    this.sections.push(`0
SECTION
2
HEADER
9
$ACADVER
1
AC1015
9
$INSUNITS
70
4
0
ENDSEC`);
  }

  addTables() {
    // Build minimal tables section (empty for basic export)
    this.sections.push(`0
SECTION
2
TABLES
0
ENDSEC`);
  }

  startEntities() {
    // Start entities section and initialize entities array
    this.sections.push(`0
SECTION
2
ENTITIES`);
    this.entities = [];
  }

  addEntity(entity) {
    // Add entity to entities array (will be added to sections when ending)
    this.entities.push(entity);
  }

  endEntities() {
    // Add all entities to sections array (spread operator for efficiency)
    this.sections.push(...this.entities);

    // End entities section
    this.sections.push(`0
ENDSEC`);
  }

  build() {
    // Add EOF (End Of File) marker (required by DXF format)
    this.sections.push(`0
EOF`);

    // Join all sections with newlines (DXF format requirement)
    return this.sections.join('\n');
  }
}

DXF Format Explained: DXF is a text format with codes. Each line is either a code or a value:

  • 0 = start of section/entity
  • 2 = name (section name, entity type, etc.)
  • 9 = variable name (in header)
  • 1, 10, 20, etc. = values (strings, X coordinates, Y coordinates, etc.)

The Complete Class:

class DXFBuilder {
  constructor() {
    this.sections = [];
  }

  addHeader(canvasDims) {
    this.sections.push(`0
SECTION
2
HEADER
9
$ACADVER
1
AC1015
9
$INSUNITS
70
4
0
ENDSEC`);
  }

  addTables() {
    this.sections.push(`0
SECTION
2
TABLES
0
ENDSEC`);
  }

  startEntities() {
    this.sections.push(`0
SECTION
2
ENTITIES`);
    this.entities = [];
  }

  addEntity(entity) {
    this.entities.push(entity);
  }

  endEntities() {
    this.sections.push(...this.entities);
    this.sections.push(`0
ENDSEC`);
  }

  build() {
    this.sections.push(`0
EOF`);
    return this.sections.join('\n');
  }
}

Building This Step by Step:

  1. Create DXFBuilder class
  2. Initialize sections array in constructor
  3. Add addHeader() method with DXF header codes
  4. Add addTables() method with minimal tables
  5. Add startEntities() method to begin entities section
  6. Initialize entities array
  7. Add addEntity() method to collect entities
  8. Add endEntities() method to close entities section
  9. Add build() method to assemble final DXF string
  10. Add EOF marker at the end
  11. Join all sections with newlines

Building DXF Shape Conversion From Scratch

Convert shapes to DXF entities. Each shape type needs its own conversion function that outputs DXF-formatted strings.

How to Build It Step by Step:

Step 1: Create Shape Conversion Router

This function routes shapes to the appropriate converter based on type. It handles parameter conversion and delegates to type-specific converters.

Step 2: Create Circle Converter

DXF format uses codes: code on one line, value on next line.

  • 0: Start of entity
  • CIRCLE: Entity type
  • 5: Handle (unique identifier, auto-generated)
  • 8: Layer (0 = default layer)
  • 10: X coordinate
  • 20: Y coordinate
  • 30: Z coordinate (0.0 for 2D shapes)
  • 40: Radius

Why No Unit Conversion: DXF uses millimeters, and AQUI already uses millimeters. No conversion needed - we can use world coordinates directly.

Why toFixed(6): DXF expects precise coordinates. Using toFixed(6) ensures 6 decimal places, which is sufficient for most CAD applications.

DXF Coordinates Explained: DXF uses 3D coordinates with specific codes:

  • X = code 10
  • Y = code 20
  • Z = code 30

For 2D shapes, Z is always 0.0. The coordinate system matches AQUI's world coordinates (millimeters).

function createDXFShapeWithMMConversion(shape, shapeName, canvasTransform) {
  // Extract shape properties
  const { type, params, transform } = shape;

  // Convert shape parameters (validation/conversion if needed)
  const convertedParams = convertShapeParamsToDXF(type, params);

  // Route to appropriate converter based on shape type
  switch (type) {
    case 'circle':
      return createDXFCircle(convertedParams, transform, canvasTransform);
    case 'line':
      return createDXFLine(convertedParams, transform, canvasTransform);
    // ... more types
  }
}

function createDXFCircle(params, transform, canvasTransform) {
  // Get circle position (DXF uses world coordinates directly, no conversion needed)
  const worldX = (transform?.position?.[0] || 0);
  const worldY = (transform?.position?.[1] || 0);

  // Get circle radius
  const radius = params.radius || 0;

  // Build DXF CIRCLE entity (code on one line, value on next line)
  return `0
CIRCLE
5
${generateHandle()}
8
0
10
${worldX.toFixed(6)}
20
${worldY.toFixed(6)}
30
0.0
40
${radius.toFixed(6)}`;
}

The Complete Functions:

function createDXFShapeWithMMConversion(shape, shapeName, canvasTransform) {
  const { type, params, transform } = shape;
  const convertedParams = convertShapeParamsToDXF(type, params);

  switch (type) {
    case 'circle':
      return createDXFCircle(convertedParams, transform, canvasTransform);
    case 'line':
      return createDXFLine(convertedParams, transform, canvasTransform);
    // ... more types
  }
}

function createDXFCircle(params, transform, canvasTransform) {
  const worldX = (transform?.position?.[0] || 0);
  const worldY = (transform?.position?.[1] || 0);
  const radius = params.radius || 0;

  // DXF uses mm, so no conversion needed (AQUI already uses mm)
  return `0
CIRCLE
5
${generateHandle()}
8
0
10
${worldX.toFixed(6)}
20
${worldY.toFixed(6)}
30
0.0
40
${radius.toFixed(6)}`;
}

Building This Step by Step:

  1. Create createDXFShapeWithMMConversion() router function
  2. Extract shape type, params, and transform
  3. Convert shape parameters if needed
  4. Use switch statement to route to type-specific converter
  5. Create createDXFCircle() function
  6. Get circle position from transform
  7. Get circle radius from params
  8. Build DXF CIRCLE entity string with proper codes
  9. Use toFixed(6) for coordinate precision
  10. Return DXF-formatted string
  11. Repeat for other shape types (line, rectangle, etc.)

Common Gotchas

Gotcha 1: Unit conversion SVG needs pixels, DXF needs mm. Don't forget to convert.

Gotcha 2: Coordinate systems Renderer uses world coordinates with center at origin. SVG/DXF need screen coordinates. Convert properly.

Gotcha 3: Transform order SVG transforms apply right-to-left. Make sure order is correct: translate → rotate → scale.

Gotcha 4: Boolean operations Shapes used in boolean ops are marked _consumedByBoolean. Skip them in export, only export the result.

Gotcha 5: DXF format DXF is picky about format. Missing codes or wrong structure = broken file. Test with CAD software.

How to Build Export Systems - Complete Step-by-Step Guide

This section provides a complete guide for building SVG and DXF export systems from scratch.

Prerequisites

Before building export systems, you need:

  • Understanding of SVG XML structure
  • Understanding of DXF format (for CAD export)
  • Coordinate system conversion knowledge
  • Unit conversion (mm to pixels, etc.)

Part 1: Building SVG Export

Step 1.1: Create Basic SVG Export Function

File: src/svgExport.mjs

What You're Building: The main SVG export function that orchestrates the entire export process. This function takes shapes from the interpreter, converts them to SVG format, and prepares them for download. It handles the SVG document structure, coordinate system setup, and shape conversion.

Why This Function: SVG export requires multiple steps: creating the SVG document, setting up the coordinate system, converting shapes, and serializing. This function coordinates all these steps in the correct order.

How to Build It Step by Step:

Step 1.1.1: Create the Function and Validate Input

export function exportToSVG(interpreter, canvas, filename = "aqui_drawing.svg") {
  // Step 1.1.1.1: Validate shapes exist
  // If there are no shapes, there's nothing to export
  // Return error object instead of throwing (allows graceful handling)
  if (!interpreter?.env?.shapes) {
    return { success: false, error: 'No shapes to export' };
  }

Why Validate: If there are no shapes, there's nothing to export. We return an error object instead of throwing an exception, allowing the caller to handle the error gracefully (e.g., show a user-friendly message).

Step 1.1.2: Get Canvas Dimensions

  // Step 1.1.2.1: Get canvas dimensions
  // This includes width, height, and offset (center position)
  // These are needed to set up the SVG viewBox and coordinate system
  const canvasDims = getCanvasDimensions(canvas);

Why Get Canvas Dimensions: The SVG needs to match the canvas size and coordinate system. We need the width, height, and offset to properly set up the SVG viewBox and transform the coordinate system to match the renderer.

Step 1.1.3: Create SVG Root Element

  // Step 1.1.3.1: Create SVG element with proper namespace
  // SVG requires the XML namespace to be valid XML
  const svgNS = "http://www.w3.org/2000/svg";
  const svg = document.createElementNS(svgNS, "svg");

  // Step 1.1.3.2: Set SVG attributes
  // xmlns: XML namespace (required for valid SVG)
  // width/height: Canvas dimensions in pixels
  // viewBox: Defines the coordinate system (matches canvas)
  svg.setAttribute("xmlns", svgNS);
  svg.setAttribute("width", `${canvasDims.width}px`);
  svg.setAttribute("height", `${canvasDims.height}px`);
  svg.setAttribute("viewBox", `0 0 ${canvasDims.width} ${canvasDims.height}`);

Why These Attributes:

  • xmlns: Required XML namespace for valid SVG. Without it, the SVG won't be recognized as SVG.
  • width/height: Canvas dimensions in pixels. This tells viewers what size the SVG should render at.
  • viewBox: Defines the coordinate system, matching the canvas (0,0 to width,height). This ensures shapes appear in the correct positions.

Step 1.1.4: Create Main Group with Transform

  // Step 1.1.4.1: Create main group (centered like renderer)
  // The renderer centers shapes at the canvas center
  // We need to replicate this in SVG using a transform
  const mainGroup = document.createElementNS(svgNS, "g");
  mainGroup.setAttribute("transform", `translate(${canvasDims.offsetX}, ${canvasDims.offsetY})`);
  svg.appendChild(mainGroup);

  // Convert each shape
  interpreter.env.shapes.forEach((shape, shapeName) => {
    if (shape._consumedByBoolean) return; // Skip consumed shapes

    const shapeElement = createSVGShape(shape, shapeName, svgNS, canvasDims);
    if (shapeElement) {
      mainGroup.appendChild(shapeElement);
    }
  });

  // Serialize and download
  const serializer = new XMLSerializer();
  const svgString = serializer.serializeToString(svg);
  downloadFile(svgString, filename, 'image/svg+xml');

  return { success: true };
}

function getCanvasDimensions(canvas) {
  return {
    width: canvas.width,
    height: canvas.height,
    offsetX: canvas.width / 2,
    offsetY: canvas.height / 2,
    scale: 1  // 1 pixel = 1 mm (adjust as needed)
  };
}

Step 1.2: Implement Shape Conversion

function createSVGShape(shape, shapeName, svgNS, canvasDims) {
  const element = document.createElementNS(svgNS, "g");

  // Apply transform
  const transform = shape.transform || {
    position: [shape.params.x || 0, shape.params.y || 0],
    rotation: shape.params.rotation || 0,
    scale: [1, 1]
  };

  // Convert world coordinates to screen coordinates
  const screenX = (transform.position[0] * canvasDims.scale) + canvasDims.offsetX;
  const screenY = (transform.position[1] * canvasDims.scale) + canvasDims.offsetY;

  // Build transform string
  let transformStr = `translate(${screenX}, ${screenY})`;
  if (transform.rotation) {
    transformStr += ` rotate(${transform.rotation})`;
  }
  if (transform.scale) {
    const scaleX = transform.scale[0] || transform.scale.x || 1;
    const scaleY = transform.scale[1] || transform.scale.y || 1;
    transformStr += ` scale(${scaleX}, ${scaleY})`;
  }
  element.setAttribute("transform", transformStr);

  // Create shape element based on type
  let shapeElement = null;
  switch (shape.type) {
    case 'circle':
      shapeElement = createSVGCircle(shape, svgNS);
      break;
    case 'rectangle':
      shapeElement = createSVGRectangle(shape, svgNS);
      break;
    case 'polygon':
    case 'path':
      shapeElement = createSVGPath(shape, svgNS);
      break;
  }

  if (shapeElement) {
    element.appendChild(shapeElement);
  }

  return element;
}

Step 1.3: Implement Shape-Specific Converters

function createSVGCircle(shape, svgNS) {
  const circle = document.createElementNS(svgNS, "circle");
  const radius = (shape.params.radius || 50) * canvasDims.scale;

  circle.setAttribute("cx", "0");
  circle.setAttribute("cy", "0");
  circle.setAttribute("r", radius.toString());

  applySVGStyle(circle, shape.params);
  return circle;
}

function createSVGRectangle(shape, svgNS) {
  const rect = document.createElementNS(svgNS, "rect");
  const width = (shape.params.width || 100) * canvasDims.scale;
  const height = (shape.params.height || 100) * canvasDims.scale;

  rect.setAttribute("x", (-width / 2).toString());
  rect.setAttribute("y", (-height / 2).toString());
  rect.setAttribute("width", width.toString());
  rect.setAttribute("height", height.toString());

  applySVGStyle(rect, shape.params);
  return rect;
}

function createSVGPath(shape, svgNS) {
  const path = document.createElementNS(svgNS, "path");

  // Get points from shape
  const points = getShapePoints(shape);
  if (points.length === 0) return null;

  // Build path data string
  let pathData = `M ${points[0].x} ${points[0].y}`;
  for (let i = 1; i < points.length; i++) {
    pathData += ` L ${points[i].x} ${points[i].y}`;
  }
  pathData += " Z"; // Close path

  path.setAttribute("d", pathData);
  applySVGStyle(path, shape.params);
  return path;
}

function applySVGStyle(element, params) {
  // Fill
  if (params.fill !== false) {
    const fillColor = resolveColor(params.color || params.fillColor || '#000000');
    element.setAttribute("fill", fillColor);
    if (params.opacity) {
      element.setAttribute("fill-opacity", params.opacity.toString());
    }
  } else {
    element.setAttribute("fill", "none");
  }

  // Stroke
  if (params.stroke !== false) {
    const strokeColor = resolveColor(params.strokeColor || '#000000');
    element.setAttribute("stroke", strokeColor);
    element.setAttribute("stroke-width", (params.strokeWidth || 1).toString());
  } else {
    element.setAttribute("stroke", "none");
  }
}

function resolveColor(color) {
  if (color.startsWith('#')) return color;
  const colorMap = {
    'red': '#FF0000',
    'green': '#008000',
    'blue': '#0000FF',
    // ... add more
  };
  return colorMap[color.toLowerCase()] || color;
}

Step 1.4: Implement Download Function

function downloadFile(content, filename, mimeType) {
  const blob = new Blob([content], { type: mimeType });
  const url = URL.createObjectURL(blob);
  const link = document.createElement('a');
  link.href = url;
  link.download = filename;
  document.body.appendChild(link);
  link.click();
  document.body.removeChild(link);
  URL.revokeObjectURL(url);
}

Part 2: Building DXF Export

Step 2.1: Create Basic DXF Export Function

File: src/dxfExport.mjs

export function exportToDXF(interpreter, canvas, filename = "aqui_drawing.dxf") {
  if (!interpreter?.env?.shapes) {
    return { success: false, error: 'No shapes to export' };
  }

  // DXF uses millimeters (same as AQUI), so no unit conversion needed
  // But we need to convert coordinates from world to DXF space

  const dxfLines = [];

  // DXF Header
  dxfLines.push('0');
  dxfLines.push('SECTION');
  dxfLines.push('2');
  dxfLines.push('HEADER');
  dxfLines.push('9');
  dxfLines.push('$ACADVER');
  dxfLines.push('1');
  dxfLines.push('AC1015');
  dxfLines.push('0');
  dxfLines.push('ENDSEC');

  // DXF Tables
  dxfLines.push('0');
  dxfLines.push('SECTION');
  dxfLines.push('2');
  dxfLines.push('TABLES');
  dxfLines.push('0');
  dxfLines.push('ENDSEC');

  // DXF Entities
  dxfLines.push('0');
  dxfLines.push('SECTION');
  dxfLines.push('2');
  dxfLines.push('ENTITIES');

  // Convert each shape
  interpreter.env.shapes.forEach((shape, shapeName) => {
    if (shape._consumedByBoolean) return;

    const entityLines = createDXFEntity(shape, shapeName);
    dxfLines.push(...entityLines);
  });

  dxfLines.push('0');
  dxfLines.push('ENDSEC');

  // DXF EOF
  dxfLines.push('0');
  dxfLines.push('EOF');

  // Join lines with newlines
  const dxfContent = dxfLines.join('\n');

  // Download
  downloadFile(dxfContent, filename, 'application/dxf');

  return { success: true };
}

Step 2.2: Implement DXF Entity Creation

function createDXFEntity(shape, shapeName) {
  const lines = [];

  switch (shape.type) {
    case 'circle':
      lines.push(...createDXFCircle(shape));
      break;
    case 'rectangle':
      lines.push(...createDXFRectangle(shape));
      break;
    case 'polygon':
    case 'path':
      lines.push(...createDXFPolyline(shape));
      break;
  }

  return lines;
}

function createDXFCircle(shape) {
  const transform = shape.transform || {
    position: [shape.params.x || 0, shape.params.y || 0],
    rotation: 0,
    scale: [1, 1]
  };

  const radius = (shape.params.radius || 50) * (transform.scale[0] || 1);
  const centerX = transform.position[0] || 0;
  const centerY = transform.position[1] || 0;

  return [
    '0',
    'CIRCLE',
    '8',        // Layer
    '0',        // Default layer
    '10',       // Center X
    centerX.toString(),
    '20',       // Center Y
    centerY.toString(),
    '30',       // Center Z
    '0.0',
    '40',       // Radius
    radius.toString()
  ];
}

function createDXFPolyline(shape) {
  const points = getShapePoints(shape);
  if (points.length === 0) return [];

  const transform = shape.transform || {
    position: [0, 0],
    rotation: 0,
    scale: [1, 1]
  };

  const lines = [
    '0',
    'POLYLINE',
    '8',        // Layer
    '0',        // Default layer
    '66',       // Vertices follow flag
    '1',
    '70',       // Polyline flag (closed)
    '1'
  ];

  // Add vertices
  points.forEach(point => {
    const transformed = applyTransform(point, transform);
    lines.push('0');
    lines.push('VERTEX');
    lines.push('8');
    lines.push('0');
    lines.push('10');  // X
    lines.push(transformed.x.toString());
    lines.push('20');  // Y
    lines.push(transformed.y.toString());
    lines.push('30');  // Z
    lines.push('0.0');
  });

  // Close polyline
  lines.push('0');
  lines.push('SEQEND');

  return lines;
}
  • [ ] Transforms are applied correctly

Common Issues and Fixes

Issue: SVG doesn't open

  • Check XML is well-formed
  • Verify namespace is correct
  • Check for invalid characters

Issue: Shapes in wrong position

  • Check coordinate conversion (world → screen)
  • Verify offset is applied correctly
  • Check transform order

Issue: Colors wrong

  • Check color resolution (named → hex)
  • Verify opacity is applied correctly
  • Check fill/stroke flags

Issue: DXF doesn't open in CAD

  • Check DXF format is correct
  • Verify all required codes are present
  • Check coordinate values are valid numbers

results matching ""

    No results matching ""