2025

Parametrische Modelle mit CadQuery und Three.js

Ein Ansatz, um CadQuery-Modelle serverseitig zu berechnen und die Ergebnisse als Mesh im Browser interaktiv darzustellen — mit Live-Update bei Parameteränderung.

CadQuery als parametrischer Kern

CadQuery ist eine Python-Bibliothek für parametrische CAD-Modellierung, die auf dem OpenCASCADE-Kernel aufbaut. Im Vergleich zum direkten Arbeiten mit OCCT in C++ ist die API deutlich kompakter. Ein einfaches Gehäuse mit Bohrungen lässt sich in wenigen Zeilen beschreiben:

import cadquery as cq

def create_housing(length=80, width=60, height=30,
                   wall=2.5, corner_r=3, hole_d=4.5):
    housing = (
        cq.Workplane("XY")
        .box(length, width, height)
        .edges("|Z").fillet(corner_r)
        .faces(">Z").shell(-wall)
        .faces(">Z").workplane()
        .rect(length - 12, width - 12, forConstruction=True)
        .vertices()
        .hole(hole_d)
    )
    return housing

Das Entscheidende: Jeder Parameter ist eine Variable. Ändert man length, wird das gesamte Modell neu berechnet — einschließlich Verrundungen, Schalenkörper und Bohrungspositionen. Genau diese Eigenschaft macht CadQuery interessant für Web-Tools, in denen Benutzer an Schiebereglern drehen und sofort das Ergebnis sehen wollen.

Architektur: Server berechnet, Browser zeigt an

CadQuery im Browser laufen zu lassen ist theoretisch möglich (Python via Pyodide, OCCT via WASM), aber in der Praxis unpraktisch: Die Kombination aus Python-Runtime und OCCT-Kernel würde über 50 MB an Downloads erzeugen, und die Performance wäre mäßig.

Der pragmatischere Weg: CadQuery läuft auf dem Server, und der Browser bekommt nur das tessellierte Mesh. Die Architektur sieht so aus:

Browser (Three.js)                    Server (Python)
┌─────────────────┐                  ┌─────────────────┐
│                  │   Parameter      │                  │
│  Slider/Input    │ ──── JSON ────→ │  CadQuery         │
│  Three.js Viewer │                  │  Tessellierung   │
│                  │ ←── glTF ────── │  glTF-Export      │
│                  │   Mesh           │                  │
└─────────────────┘                  └─────────────────┘

Der Server nimmt Parameter als JSON entgegen, baut das Modell, tesselliert es und schickt das Ergebnis als glTF oder als rohes Dreiecksmesh zurück. Der Browser aktualisiert die Three.js-Szene.

Der Server: FastAPI + CadQuery

Ein minimaler Server mit FastAPI, der ein parametrisches Modell berechnet und als glTF zurückgibt:

from fastapi import FastAPI
from fastapi.responses import Response
from pydantic import BaseModel
import cadquery as cq
from cadquery import exporters
import io

app = FastAPI()

class HousingParams(BaseModel):
    length: float = 80
    width: float = 60
    height: float = 30
    wall: float = 2.5
    corner_r: float = 3
    hole_d: float = 4.5

@app.post("/api/housing")
def generate_housing(params: HousingParams):
    housing = (
        cq.Workplane("XY")
        .box(params.length, params.width, params.height)
        .edges("|Z").fillet(params.corner_r)
        .faces(">Z").shell(-params.wall)
        .faces(">Z").workplane()
        .rect(params.length - 12, params.width - 12,
              forConstruction=True)
        .vertices()
        .hole(params.hole_d)
    )

    # Als glTF exportieren
    buf = io.BytesIO()
    exporters.export(housing, buf, exportType="GLTF")
    buf.seek(0)

    return Response(
        content=buf.read(),
        media_type="model/gltf-binary",
        headers={"Cache-Control": "no-cache"}
    )

Die Berechnungszeit für ein Modell wie dieses liegt bei 50–200 ms, abhängig von der Komplexität. Das ist schnell genug, um bei jeder Parameteränderung einen neuen Request zu senden — vorausgesetzt, man debounced die Eingabe.

Der Client: Three.js mit GLTFLoader

Auf der Browser-Seite lädt man das glTF-Mesh und ersetzt bei jedem Update die Geometrie in der Szene:

import * as THREE from 'three';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

const loader = new GLTFLoader();
let currentModel = null;

async function updateModel(params) {
    const response = await fetch('/api/housing', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(params),
    });

    const buffer = await response.arrayBuffer();

    loader.parse(buffer, '', (gltf) => {
        if (currentModel) scene.remove(currentModel);

        currentModel = gltf.scene;

        // Material anpassen
        currentModel.traverse((child) => {
            if (child.isMesh) {
                child.material = new THREE.MeshPhongMaterial({
                    color: 0x7799bb,
                    shininess: 60,
                });
            }
        });

        scene.add(currentModel);
    });
}

// Debounced Update bei Slider-Änderung
let timeout;
function onParamChange(params) {
    clearTimeout(timeout);
    timeout = setTimeout(() => updateModel(params), 150);
}

Der Debounce von 150 ms verhindert, dass bei schnellem Slider-Ziehen dutzende Requests gleichzeitig laufen. In der Praxis fühlt sich das flüssig an: Man zieht einen Regler, und nach einer kurzen Verzögerung aktualisiert sich das 3D-Modell.

Optimierungen für die Praxis

Request-Cancellation. Wenn ein neuer Request gestartet wird, bevor der vorherige fertig ist, sollte der alte abgebrochen werden. Mit AbortController ist das trivial:

let controller = null;

async function updateModel(params) {
    if (controller) controller.abort();
    controller = new AbortController();

    try {
        const response = await fetch('/api/housing', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(params),
            signal: controller.signal,
        });
        // ... rest wie oben
    } catch (e) {
        if (e.name !== 'AbortError') throw e;
    }
}

Server-seitiges Caching. Wenn Parameter sich oft wiederholen (z.B. beim Hin- und Herprobieren), lohnt sich ein LRU-Cache auf dem Server. CadQuery-Ergebnisse sind deterministisch — gleiche Parameter ergeben immer das gleiche Modell.

from functools import lru_cache

@lru_cache(maxsize=64)
def compute_housing(length, width, height, wall, corner_r, hole_d):
    # ... CadQuery-Modell bauen ...
    return gltf_bytes

STL statt glTF für Geschwindigkeit. Wenn man kein Material oder Farben braucht, ist STL als Austauschformat schneller zu erzeugen und kleiner. Auf der Three.js-Seite gibt es dafür den STLLoader.

WebSocket statt HTTP. Für noch responsivere Interaktion kann man die Parameter über eine WebSocket-Verbindung senden. Das spart den HTTP-Overhead pro Request und ermöglicht es dem Server, den aktuellen Berechnungsfortschritt zu melden.

Grenzen des Ansatzes

Die Server-seitige Berechnung hat einen offensichtlichen Nachteil: Jede Parameteränderung braucht einen Netzwerk-Roundtrip. Bei einer lokalen Installation oder im Firmennetzwerk ist die Latenz vernachlässigbar (unter 10 ms). Über das Internet kommen je nach Standort 30–100 ms Netzwerklatenz dazu — plus die Berechnungszeit.

Für einfache Modelle ist das kein Problem. Bei komplexen Baugruppen mit vielen Booleschen Operationen kann die Berechnung aber auch mal 1–2 Sekunden dauern. In solchen Fällen hilft ein Indikator im UI ("Berechne...") und eventuell eine niedrigere Tessellierungsqualität für die Vorschau, die nach dem Loslassen des Sliders durch eine hochauflösende Version ersetzt wird.

Wer die Serverabhängigkeit komplett vermeiden will, kann CadQuery theoretisch auch durch OCCT direkt im Browser via WebAssembly ersetzen. Das ist aufwändiger, aber für manche Anwendungsfälle der richtige Weg.