Adding a New CAD Kernel

Step-by-step guide to integrating a new CAD kernel into Tau.

This guide explains how to add a new CAD kernel to Tau. It's based on the JSCAD kernel integration and serves as a template for future kernels.

Overview

Tau's multi-kernel architecture allows different CAD engines to be plugged in as Web Workers. Each kernel follows the same interface pattern, making it straightforward to add new ones.

What You'll Create

  1. Worker — Runs in a Web Worker, handles geometry computation
  2. Type Export — Prevents Vite from bundling the worker twice
  3. Kernel Registry Entry — Configuration in the types library
  4. Machine Wiring — Integration with the kernel state machine
  5. Examples (optional) — Sample builds to showcase the kernel
  6. AI Configuration (optional) — Prompts for AI-assisted modeling

Step 1: Create the Worker

Create your worker at:

apps/ui/app/components/geometry/kernel/<kernel>/<kernel>.worker.ts

Extend KernelWorker

Import and extend the base KernelWorker class:

import { expose } from 'comlink';
import type { ComputeGeometryResult, ExportFormat, ExportGeometryResult, ExtractParametersResult } from '@taucad/types';
import { createKernelSuccess, createKernelError } from '@taucad/types/guards';
import { KernelWorker } from '#components/geometry/kernel/utils/kernel-worker.js';

class MyKernelWorker extends KernelWorker {
  protected readonly name = 'MyKernelWorker';

  // Define supported export formats
  protected static override readonly supportedExportFormats: ExportFormat[] = ['stl', 'glb'];

  // Implement required abstract methods...
}

// Expose via Comlink
const service = new MyKernelWorker();
expose(service);

export type MyKernelWorkerInterface = typeof service;

Implement Required Methods

canHandle(filename, extension)

Quick check to determine if this worker can process the file. Must be fast — don't initialize heavy runtimes here.

protected async canHandle(filename: string, extension: string): Promise<boolean> {
  // Check file extension
  if (extension !== 'myext') {
    return false;
  }

  // Optionally check file content for specific patterns
  const code = await this.readFile(filename, 'utf8');
  return code.includes('mykernel-signature');
}

extractParameters(path)

Parse the file and extract parameters with their JSON Schema.

protected async extractParameters(path: string): Promise<ExtractParametersResult> {
  try {
    const code = await this.readFile(path, 'utf8');
    const { defaultParameters, jsonSchema } = parseParameters(code);

    return createKernelSuccess({
      defaultParameters,
      jsonSchema,
    });
  } catch (error) {
    return createKernelError({
      message: `Failed to extract parameters: ${error}`,
      type: 'code',
    });
  }
}

computeGeometry(path, parameters, geometryId?)

Execute the code and generate geometry (typically as glTF).

protected async computeGeometry(
  path: string,
  parameters: Record<string, unknown>,
  geometryId?: string,
): Promise<ComputeGeometryResult> {
  try {
    const code = await this.readFile(path, 'utf8');
    const geometry = await executeCode(code, parameters);
    const gltfBlob = await convertToGltf(geometry);

    return createKernelSuccess({
      geometries: [{
        id: geometryId ?? 'default',
        type: 'model/gltf-binary',
        blob: gltfBlob,
      }],
    });
  } catch (error) {
    return createKernelError({
      message: `Geometry computation failed: ${error}`,
      type: 'kernel',
    });
  }
}

exportGeometry(fileType, geometryId?, meshConfig?)

Export geometry to the requested format.

protected async exportGeometry(
  fileType: ExportFormat,
  geometryId?: string,
  meshConfig?: { linearTolerance: number; angularTolerance: number },
): Promise<ExportGeometryResult> {
  // Export from cached geometry
  const blob = await this.exportToFormat(fileType);

  return createKernelSuccess({
    files: [{ name: `model.${fileType}`, blob }],
  });
}

Create the Types File

Create a sibling file to prevent Vite double-bundling:

apps/ui/app/components/geometry/kernel/<kernel>/<kernel>.worker.types.ts
// eslint-disable-next-line no-barrel-files/no-barrel-files -- Type re-export prevents Vite from bundling worker twice
export type { MyKernelWorkerInterface } from '#components/geometry/kernel/<kernel>/<kernel>.worker.js';

Step 2: Register in Kernel Machine

Edit apps/ui/app/machines/kernel.machine.ts:

Add to Provider Union

type KernelProvider = CadKernelProvider | 'tau' | 'jscad' | 'mykernel';

Import the Worker

import type { MyKernelWorkerInterface as MyKernelWorker } from '#components/geometry/kernel/mykernel/mykernel.worker.types.js';
import MyKernelBuilderWorker from '#components/geometry/kernel/mykernel/mykernel.worker.js?worker';

Add to Workers Map

const workers = {
  // ... existing workers
  mykernel: MyKernelBuilderWorker,
} as const satisfies Partial<Record<KernelProvider, new () => Worker>>;

Add to Priority Array

const workerPriority: KernelProvider[] = ['openscad', 'zoo', 'replicad', 'jscad', 'mykernel', 'tau'];

Wire Up in createWorkersActor

Add worker creation, wrapping, and initialization in the createWorkersActor promise actor.

Add Cleanup in destroyWorkers

Ensure the worker is terminated in the cleanup action.

Step 3: Add Kernel Configuration

Edit libs/types/src/constants/kernel.constants.ts:

export const kernelConfigurations = [
  // ... existing kernels
  {
    id: 'mykernel',
    name: 'My Kernel',
    dimensions: [3],
    language: 'typescript', // or 'javascript', 'openscad', 'kcl', etc.
    description: 'Brief description for UI',
    mainFile: 'main.ts',
    backendProvider: 'mybackend',
    longDescription: 'Detailed description explaining the kernel strengths and use cases.',
    emptyCode: `// Starter template
export default function main() {
  return createGeometry();
}
`,
    recommended: 'Use Case Category',
    tags: ['Tag1', 'Tag2'],
    features: ['Feature 1', 'Feature 2'],
  },
] as const satisfies KernelConfiguration[];

Step 4: Add Examples (Optional)

Create Examples in Library

Edit libs/tau-examples/src/build.examples.ts:

export const myKernelExamples = [
  {
    id: 'my-example-1',
    name: 'Example Name',
    code: `// Example code...`,
    thumbnail: 'data:image/png;base64,...', // Optional
  },
];

Export from Index

Edit libs/tau-examples/src/index.ts:

export { myKernelExamples } from './build.examples.js';

Add to Sample Builds

Edit apps/ui/app/constants/build-examples.ts:

import { myKernelExamples } from '@taucad/tau-examples';

// Map to Build objects and add to sampleBuilds

Step 5: Configure AI Prompts (Optional)

Edit apps/api/app/api/chat/prompts/chat-prompt-cad.ts:

Add a KernelConfig entry to cadKernelConfigs with:

  • fileExtension — File extension (e.g., .ts, .scad)
  • languageName — Human-readable name
  • roleDescription — Brief purpose description
  • technicalContext — Strengths and use cases
  • codeStandards — Syntax and formatting rules
  • modelingStrategy — Design philosophy
  • technicalResources — Available APIs and examples
  • commonErrorPatterns — Typical issues and solutions

Step 6: Verify

Run the following Nx tasks to verify your integration:

# Typecheck
pnpm nx typecheck ui

# Lint
pnpm nx lint ui

# Test
pnpm nx test ui --watch=false

Best Practices

Result Pattern

Always return result objects using the helpers:

import { createKernelSuccess, createKernelError } from '@taucad/types/guards';

// Success
return createKernelSuccess({ data });

// Error
return createKernelError({
  message: 'Human-readable error',
  type: 'code' | 'kernel', // code = syntax error, kernel = runtime error
});

Keep canHandle Fast

The canHandle method is called for every file to determine which kernel to use. Don't:

  • Initialize heavy runtimes
  • Parse entire files
  • Make network requests

Do:

  • Check file extensions
  • Look for quick signature patterns

Export Formats

Define supported export formats as a static property:

protected static override readonly supportedExportFormats: ExportFormat[] = ['stl', 'glb', 'step'];

Use glTF-Transform for Output

For consistent 3D output, use the @gltf-transform/core library:

import { Document, NodeIO } from '@gltf-transform/core';

const document = new Document();
// Build scene...
const io = new NodeIO();
const glb = await io.writeBinary(document);

File System Access

Use the provided fileReader for filesystem operations:

// Read file content
const content = await this.readFile('path/to/file.txt', 'utf8');

// Check if file exists
const exists = await this.exists('path/to/file.txt');

// List directory
const files = await this.readdir('path/to/dir');

Logging

Use the built-in logging methods:

this.debug('Processing file', { operation: 'computeGeometry' });
this.warn('Deprecated feature used');
this.error('Failed to parse', { data: { line: 42 } });

Existing Kernels Reference

KernelLanguageBackendDescription
openscadOpenSCADManifoldCSG for 3D printing
replicadTypeScriptOpenCascadePrecise BRep engineering
zooKCLZooCloud-native CAD with AI
jscadTypeScriptJSCADParametric CSG modeling
tauTypeScriptReplicadFile conversion kernel

See the existing implementations in apps/ui/app/components/geometry/kernel/ for reference.