diff --git a/apps/api/app/api/chat/prompts/kernel-prompt-configs/buerli.prompt.config.ts b/apps/api/app/api/chat/prompts/kernel-prompt-configs/buerli.prompt.config.ts new file mode 100644 index 000000000..a591a1238 --- /dev/null +++ b/apps/api/app/api/chat/prompts/kernel-prompt-configs/buerli.prompt.config.ts @@ -0,0 +1,15 @@ +import type { KernelConfig } from '#api/chat/prompts/kernel-prompt-configs/kernel.prompt.config.types.js'; +import canonicalExample from '#api/chat/prompts/kernel-prompt-configs/buerli.prompt.example.ts?raw'; + +export const buerliConfig: KernelConfig = { + fileExtension: '.ts', + languageName: 'Buerli (ClassCAD)', + + codeStandards: `Output TypeScript with ES module imports. Import from \`@buerli.io/classcad\`. Use \`BuerliCadFacade\` to connect to the WASM engine. Access the API via \`bcf.api.v1\` which provides Part, Solid, Assembly, Sketch, and Curve APIs. Export \`defaultParams\` object and default \`main(params)\` async function returning geometry from \`createBufferGeometry()\`. The WASM client runs entirely in the browser — no server connection required.`, + + commonErrorPatterns: + 'missing await on async API calls, incorrect part/solid IDs, invalid geometry dimensions, forgetting to call bcf.connect() before API usage', + + fileLayoutMode: 'full-nesting', + canonicalExample, +}; diff --git a/apps/api/app/api/chat/prompts/kernel-prompt-configs/buerli.prompt.example.ts b/apps/api/app/api/chat/prompts/kernel-prompt-configs/buerli.prompt.example.ts new file mode 100644 index 000000000..bc5138eb9 --- /dev/null +++ b/apps/api/app/api/chat/prompts/kernel-prompt-configs/buerli.prompt.example.ts @@ -0,0 +1,34 @@ +import { BuerliCadFacade } from '@buerli.io/classcad'; + +export const defaultParams = { + width: 50, + depth: 30, + height: 10, + holeRadius: 5, + holeDepth: 8, +}; + +export default async function main(p = defaultParams) { + const bcf = new BuerliCadFacade(); + await bcf.connect(); + const api = bcf.api.v1; + + const part = await api.part.create({ name: 'Flange' }); + + await api.part.box({ + id: part, + width: p.width, + depth: p.depth, + height: p.height, + }); + + await api.part.cylinder({ + id: part, + axes: [], + radius: p.holeRadius, + height: p.holeDepth, + }); + + const geoms = await bcf.createBufferGeometry(part); + return geoms; +} diff --git a/apps/api/app/api/chat/prompts/kernel-prompt-configs/kernel.prompt.config.ts b/apps/api/app/api/chat/prompts/kernel-prompt-configs/kernel.prompt.config.ts index c476a82cb..4814aff89 100644 --- a/apps/api/app/api/chat/prompts/kernel-prompt-configs/kernel.prompt.config.ts +++ b/apps/api/app/api/chat/prompts/kernel-prompt-configs/kernel.prompt.config.ts @@ -1,5 +1,6 @@ import type { KernelProvider } from '@taucad/runtime'; import type { KernelConfig } from '#api/chat/prompts/kernel-prompt-configs/kernel.prompt.config.types.js'; +import { buerliConfig } from '#api/chat/prompts/kernel-prompt-configs/buerli.prompt.config.js'; import { jscadConfig } from '#api/chat/prompts/kernel-prompt-configs/jscad.prompt.config.js'; import { manifoldConfig } from '#api/chat/prompts/kernel-prompt-configs/manifold.prompt.config.js'; import { opencascadejsConfig } from '#api/chat/prompts/kernel-prompt-configs/opencascadejs.prompt.config.js'; @@ -14,6 +15,7 @@ const kernelConfigs: Record = { zoo: zooConfig, jscad: jscadConfig, opencascadejs: opencascadejsConfig, + buerli: buerliConfig, }; export function getKernelConfig(kernel: KernelProvider): KernelConfig { diff --git a/apps/ui/app/constants/kernel-worker.constants.ts b/apps/ui/app/constants/kernel-worker.constants.ts index 40a1fb994..4165eaffe 100644 --- a/apps/ui/app/constants/kernel-worker.constants.ts +++ b/apps/ui/app/constants/kernel-worker.constants.ts @@ -1,4 +1,4 @@ -import { replicad, opencascade, zoo, openscad, jscad, manifold, tau } from '@taucad/runtime/kernels'; +import { replicad, opencascade, zoo, openscad, jscad, manifold, tau, buerli } from '@taucad/runtime/kernels'; import { parameterCache, geometryCache, gltfCoordinateTransform, gltfEdgeDetection } from '@taucad/runtime/middleware'; import { esbuild } from '@taucad/runtime/bundler'; import { createRuntimeClientOptions } from '@taucad/runtime'; @@ -26,6 +26,7 @@ export const defaultKernelOptions = createRuntimeClientOptions({ opencascade(), manifold(), jscad(), + buerli({ classcadKey: ENV.CLASSCAD_WASM_KEY ?? '' }), tau(), ], middleware: [ diff --git a/apps/ui/app/environment.config.ts b/apps/ui/app/environment.config.ts index 1d093b3de..a5b9ebdc5 100644 --- a/apps/ui/app/environment.config.ts +++ b/apps/ui/app/environment.config.ts @@ -42,6 +42,9 @@ const environmentSchema = z.preprocess( .default('us-assets.i.posthog.com') .describe('PostHog asset host for the PostHog client.'), POSTHOG_CLIENT_KEY: z.string().optional().describe('PostHog client key. Set to enable analytics.'), + + // ClassCAD / Buerli + CLASSCAD_WASM_KEY: z.string().optional().describe('ClassCAD WASM API key for the Buerli kernel.'), /* eslint-enable @typescript-eslint/naming-convention -- environment variables are not camelCase */ }), ); diff --git a/docs/policy/runtime-architecture-policy.md b/docs/policy/runtime-architecture-policy.md index 653df892f..385c08b29 100644 --- a/docs/policy/runtime-architecture-policy.md +++ b/docs/policy/runtime-architecture-policy.md @@ -83,11 +83,11 @@ Two distinct "define" patterns serve different audiences: All non-generic capabilities are provided by injectable plugins, not hardcoded in the framework: -| Plugin Type | Author API | Consumer API | Purpose | Example | -| ----------- | --------------------------------------- | --------------------------------------- | ---------------------------------------------------------------- | --------------------------------------------- | -| Kernel | `defineKernel` → `KernelDefinition` | `replicad()` → `KernelPlugin` | Geometry computation, parameter extraction, export | replicad, manifold, jscad, openscad, zoo, tau | -| Bundler | `defineBundler` → `BundlerDefinition` | `esbuild()` → `BundlerPlugin` | File bundling, code execution, module registry, import detection | esbuild bundler | -| Middleware | `defineMiddleware` → `KernelMiddleware` | `parameterCache()` → `MiddlewarePlugin` | Operation wrapping (caching, transforms, edge detection) | geometry-cache, parameter-cache | +| Plugin Type | Author API | Consumer API | Purpose | Example | +| ----------- | --------------------------------------- | --------------------------------------- | ---------------------------------------------------------------- | ----------------------------------------------------- | +| Kernel | `defineKernel` → `KernelDefinition` | `replicad()` → `KernelPlugin` | Geometry computation, parameter extraction, export | replicad, manifold, jscad, openscad, zoo, buerli, tau | +| Bundler | `defineBundler` → `BundlerDefinition` | `esbuild()` → `BundlerPlugin` | File bundling, code execution, module registry, import detection | esbuild bundler | +| Middleware | `defineMiddleware` → `KernelMiddleware` | `parameterCache()` → `MiddlewarePlugin` | Operation wrapping (caching, transforms, edge detection) | geometry-cache, parameter-cache | ### Multi-Bundler Support @@ -117,6 +117,7 @@ With the single-worker-per-CU architecture, only the WASM runtime for the select - replicad file: ~55-66 MB (OpenCASCADE WASM) - manifold file: ~14 MB (Manifold WASM) - openscad file: ~14 MB (Manifold WASM) +- buerli file: ~302 KB + ClassCAD WASM (loaded at runtime via API key) - jscad file: ~5 MB - kcl file: ~3 MB (KCL WASM) - STEP/STL file: ~5 MB (converter) @@ -283,7 +284,7 @@ ProjectMachine.stopStatefulActors() ### Detection Priority ``` -Priority: openscad → zoo → replicad → manifold → jscad → tau +Priority: openscad → zoo → replicad → manifold → jscad → buerli → tau ``` | Kernel | Detection Method | Scope | @@ -369,7 +370,7 @@ During detection, bare specifiers appear as external imports in `metafile.output ``` @taucad/runtime → createRuntimeClient, types, presets, fromNodeFS, fromMemoryFS, fromFsLike -@taucad/runtime/kernels → replicad(), manifold(), zoo(), openscad(), jscad(), tau() +@taucad/runtime/kernels → replicad(), manifold(), zoo(), openscad(), jscad(), buerli(), tau() @taucad/runtime/middleware → parameterCache(), geometryCache(), gltfCoordinateTransform(), gltfEdgeDetection() @taucad/runtime/bundler → esbuild() @taucad/runtime/transport → RuntimeTransport, createWorkerTransport() diff --git a/docs/research/buerli-classcad-kernel-integration.md b/docs/research/buerli-classcad-kernel-integration.md new file mode 100644 index 000000000..020fcf1c9 --- /dev/null +++ b/docs/research/buerli-classcad-kernel-integration.md @@ -0,0 +1,232 @@ +--- +title: 'Buerli ClassCAD Kernel Integration' +description: 'Investigation of @buerli.io/classcad WASM integration, direct API bypass strategy, and test blueprint for real geometry validation.' +status: active +created: '2026-04-09' +updated: '2026-04-09' +category: reference +related: + - docs/policy/runtime-architecture-policy.md +--- + +# Buerli ClassCAD Kernel Integration + +Investigation of how `@buerli.io/classcad` integrates as a Tau kernel plugin, the strategy for bypassing the Comlink/Worker layer, and the blueprint for testing with real ClassCAD geometry creation. + +## Executive Summary + +ClassCAD via `@buerli.io/classcad` provides a BRep parametric CAD engine running in-browser via WASM. While the library's `WASMClient` wraps a Comlink Worker, the underlying `classCadWorkerApi` class (fetched from CDN as `ClassCADWasmWorker.js`) is a plain JavaScript class that loads the Emscripten module and calls `module.init()` / `module.execute()` directly. This class has no inherent Worker dependency and can be instantiated on any thread. The recommended path for Node.js/Vitest integration tests is to implement a custom `AwvNodeClient` subclass that uses `classCadWorkerApi` directly without the Comlink proxy, enabling real part and assembly creation in tests. + +## Problem Statement + +The `WASMClient.connect()` method fails in Node.js with `ReferenceError: Worker is not defined` because it uses `new Worker(url)` + Comlink `wrap()`. This prevents real ClassCAD geometry tests in Vitest. However, the ClassCAD WASM engine itself is not Worker-dependent — the Comlink layer is only a transport convenience. + +## Methodology + +1. Read the full `WASMClient.js` source in `@buerli.io/classcad/build/esm/io/` +2. Fetched and deminified `ClassCADWasmWorker.js` from the ClassCAD CDN at `https://awvstatic.com/classcad/download/release/21.0.0/wasm/` +3. Traced the `facade.js` → `__callAPI` → `comClient.executeRequest` → `request` → `this.service.execute(command)` call chain +4. Analyzed `@classcad/api-js` v1 API command builders (`part.js`, `solid.js`, `sketch.js`) +5. Studied the `AwvNodeClient` base class contract (`executeRequest`, `request`, `requestTree`) + +## Findings + +### Finding 1: ClassCADWasmWorker.js Contains a Standalone WASM Loader + +The CDN worker script (`ClassCADWasmWorker.js`) contains a class `classCadWorkerApi` (minified as `Oa`) with two methods: + +```text +classCadWorkerApi.init(url, config) + → import(ClassCADWasm.js) // Dynamic ESM import of Emscripten module + → factory({ locateFile }) // Emscripten module instantiation + → FS setup (mkdir, writeFile) // Virtual filesystem for ClassCAD config + → module.init(jsonConfig) // ClassCAD kernel initialization + +classCadWorkerApi.execute(command) + → module.execute(JSON.stringify(command), module, onSend, onSendBinary) + → Returns { messages: ServerResponse[], binaryMessages: ScgGraphicPackage[] } +``` + +**This class has zero Worker dependency.** It uses `import()` for module loading, `fetch()` for config files, and `this.module.*` for WASM calls. The `Comlink.expose()` call at the end is the _only_ Worker-specific line. + +### Finding 2: Full Command Protocol from Facade to WASM + +The `@buerli.io/classcad` facade builds commands as JSON arrays and passes them through the client: + +```text +api.v1.part.box({ id, length, width, height }) + → facade.callSafeApiV('v1', 'part', 'box', [{ id, length, width, height }]) + → facade.callSafeApi('v1.part', 'box', [{ id, length, width, height }]) + → __callAPI(drawingId, [{ "v1.part.box": [{ id, length, width, height }] }]) + → comClient.executeRequest(task) + → comClient.request({ command: 'Execute', task }) + → this.service.execute({ command: 'Execute', task }) + → WASM module.execute(JSON.stringify(command), ...) +``` + +The response flows back as `{ messages: [...], binaryMessages: [...] }` where messages contain `{ command: 'Result', from: 'Execute', result: { result, messages } }` and `{ command: 'Patch', ops: [...] }`. + +### Finding 3: AwvNodeClient Is the Abstraction Boundary + +`AwvNodeClient` is the base class that both `WASMClient` and `SocketIOClient` extend. It defines: + +- `request(command: ServerRequest): Promise` — abstract, implemented by subclasses +- `executeRequest(task, streamMap?, options?)` — wraps `request` with stream handling +- `requestTree()` — calls `request({ command: 'GetTree' })` + +Any object implementing these methods can be used as a `comClient` in `Globals.clientCache`. The high-level APIs (`createApi`, `BuerliCadFacade`) operate through this abstraction. + +### Finding 4: Custom Node-Compatible Client Architecture + +To bypass the Worker layer, we need a custom client that: + +1. Fetches `ClassCADWasmWorker.js` from the CDN +2. Extracts the `classCadWorkerApi` class +3. Instantiates it directly (no Worker, no Comlink) +4. Calls `init(wasmUrl, config)` with the WASM URL and API key +5. Implements `request(command)` by calling `service.execute(command)` and processing the response (same logic as `WASMClient.requestByName`) + +```text +┌─────────────────────────────────────────────────┐ +│ BuerliCadFacade / createApi / api.v1.* │ +│ (unchanged — uses Globals.clientCache) │ +├─────────────────────────────────────────────────┤ +│ NodeWASMClient extends AwvNodeClient │ ← New +│ - Loads classCadWorkerApi directly │ +│ - No Worker/Comlink │ +│ - Calls service.init() + service.execute() │ +├─────────────────────────────────────────────────┤ +│ classCadWorkerApi (from ClassCADWasmWorker.js) │ +│ - import(ClassCADWasm.js) │ +│ - module.init(config) / module.execute(cmd) │ +├─────────────────────────────────────────────────┤ +│ ClassCAD WASM (Emscripten module) │ +│ - BRep kernel, constraint solver, meshing │ +└─────────────────────────────────────────────────┘ +``` + +### Finding 5: Node.js Compatibility Blockers + +The `classCadWorkerApi.init()` method uses: + +1. **`import(ClassCADWasm.js)`** — dynamic import from CDN URL. Node.js supports `import()` of local modules but not HTTP URLs natively. Requires either: (a) fetching the script, writing to a temp file, and importing that, or (b) using a data URL with `--experimental-vm-modules`. + +2. **`fetch()`** — used to load `classcad.cfe` (config file) and `filterconfig.json`. Node.js 18+ has global `fetch()`. + +3. **Emscripten module** — `ClassCADWasm.js` is an Emscripten-generated loader that calls `WebAssembly.instantiate`. This works in Node.js. + +4. **`window.location`** — referenced in WASMClient config but may not be needed for standalone init. + +## Recommendations + +| # | Action | Priority | Effort | Impact | +| --- | ------------------------------------------------------------------------------------ | -------- | ------ | ------------------------------ | +| R1 | Create `NodeWASMClient` that loads `classCadWorkerApi` directly | P0 | Medium | Enables real geometry tests | +| R2 | Fetch and cache `ClassCADWasmWorker.js` + `ClassCADWasm.js` + `classcad.cfe` locally | P0 | Low | Avoids CDN dependency in tests | +| R3 | Write integration tests with real `api.v1.part.*` calls | P0 | Medium | Validates the full pipeline | +| R4 | Add `api.v1.solid.*` tests for direct modeling | P1 | Low | Coverage for both paradigms | +| R5 | Test export via `api.v1.common.save` (OFB, STP, STL) | P2 | Low | Validates export pipeline | + +## Blueprint: Real Geometry Tests + +### Phase 1 — NodeWASMClient (bypasses Comlink/Worker) + +Create a test-only client at `packages/runtime/src/kernels/buerli/node-wasm-client.ts`: + +```typescript +import { AwvNodeClient } from '@buerli.io/classcad'; + +export class NodeWASMClient extends AwvNodeClient { + private service: InstanceType; + + async connect(wasmUrl: string, classcadKey: string): Promise { + // 1. Fetch ClassCADWasmWorker.js from CDN or local cache + // 2. Extract classCadWorkerApi class + // 3. this.service = new classCadWorkerApi() + // 4. await this.service.init(wasmUrl, { cclasses: { type: 'classcadkey', key } }) + } + + async request(command: ServerRequest): Promise { + // Mirror WASMClient.requestByName logic: + // 1. await this.service.execute(command) + // 2. Process messages (Result, Patch, ErrorMessage) + // 3. Return structured ServerResponse + } +} +``` + +### Phase 2 — Wire into BuerliCadFacade + +```typescript +import { init, BuerliCadFacade } from '@buerli.io/classcad'; + +// Register NodeWASMClient as the client factory +init((drawingId) => new NodeWASMClient(drawingId, { classcadKey })); + +// Use standard BuerliCadFacade +const bcf = new BuerliCadFacade(); +await bcf.connect(); +const api = bcf.api.v1; +``` + +### Phase 3 — Real Geometry Tests + +Test categories following `buerli-examples` patterns: + +**Part API (history-based):** + +```typescript +// CreatePart pattern +const part = await api.part.create({ name: 'Part' }); +await api.part.cylinder({ id: part, diameter: 50, height: 100 }); +await api.part.fillet({ id: part, references: topEdges, radius: 10 }); +const geom = await bcf.createBufferGeometry(part); +``` + +**Solid API (direct modeling):** + +```typescript +// Lego pattern +const part = await api.part.create(); +const ei = await api.part.entityInjection({ id: part }); +const box = await api.solid.box({ id: ei, width: 90, height: 80, length: 90 }); +const sub = await api.solid.box({ id: ei, width: 80, height: 70, length: 80 }); +await api.solid.subtraction({ id: ei, target: box, tools: [sub] }); +``` + +**Sketch + Extrusion:** + +```typescript +const wp = await api.part.workPlane({ id: part, normal: { x: 0, y: 0, z: 1 } }); +const sketch = await api.sketch.create({ id: part, planeId: wp }); +const geom = await api.sketch.geometry({ id: sketch, lines: [...] }); +await api.part.extrusion({ id: part, references: geom.lines, limit2: 20 }); +``` + +### Phase 4 — Geometry Validation + +Use existing test helpers: + +```typescript +const result = await buerliKernel.createGeometry(...); +await geometryHelpers.expectValidGltf(result); +await geometryHelpers.expectMeshCount(result, 1); +await geometryHelpers.expectBoundingBoxSize(result, [w, h, d], tolerance); +``` + +## Trade-offs + +| Approach | Pros | Cons | +| ------------------------------ | ---------------------------------- | ------------------------------------------------------------ | +| NodeWASMClient (direct WASM) | Real geometry, full API validation | Medium effort, CDN fetch of WASM (~50MB), Node.js ESM compat | +| SocketIOClient (remote server) | Uses existing client code | Requires running ClassCAD server, external dependency | +| Synthetic geometry (current) | Fast, no WASM dependency | No real CAD validation, conversion-only | + +## References + +- CDN worker: `https://awvstatic.com/classcad/download/release/21.0.0/wasm/ClassCADWasmWorker.js` +- Source: `repos/buerli-examples/` (56 example files) +- Source: `repos/buerligons/` (production app) +- Docs: [buerli.io/docs](https://buerli.io/docs) +- npm: `@buerli.io/classcad@1.0.1`, `@classcad/api-js@21.0.0` +- Related: `packages/runtime/src/kernels/replicad/replicad.kernel.test.ts` (gold standard) diff --git a/libs/api-extractor/project.json b/libs/api-extractor/project.json index 3c722c553..bfd122c2e 100644 --- a/libs/api-extractor/project.json +++ b/libs/api-extractor/project.json @@ -39,6 +39,13 @@ "command": "tsx src/extract-opencascade-types.ts", "cwd": "libs/api-extractor" } + }, + "extract-buerli": { + "executor": "nx:run-commands", + "options": { + "command": "tsx src/extract-buerli-types.ts", + "cwd": "libs/api-extractor" + } } } } diff --git a/libs/api-extractor/src/extract-buerli-types.ts b/libs/api-extractor/src/extract-buerli-types.ts new file mode 100644 index 000000000..5b2a32a4e --- /dev/null +++ b/libs/api-extractor/src/extract-buerli-types.ts @@ -0,0 +1,234 @@ +#!/usr/bin/env node + +import { writeFileSync, mkdirSync } from 'node:fs'; +import { join } from 'node:path'; +import process from 'node:process'; + +/** + * Bundle @buerli.io/classcad type declarations as raw `.d.ts` content for + * Monaco's `addExtraLib`. + * + * The original @buerli.io/classcad types have deep dependencies on `three`, + * `@classcad/api-js`, and `@buerli.io/core`. For Monaco IntelliSense we + * produce a simplified, self-contained declaration that covers the key + * public API surface: BuerliCadFacade, WASMClient, init, and the primary + * API types. + */ + +// ============================================================================= +// Bundled type content +// ============================================================================= + +/** + * Build a self-contained `.d.ts` for the `@buerli.io/classcad` module. + * Inlines the essential types so Monaco can provide IntelliSense without + * resolving external dependencies. + */ +function buildClasscadModuleContent(): string { + return `// Bundled type declarations for @buerli.io/classcad. +// Auto-generated by extract-buerli-types.ts - do not edit manually. +// Simplified self-contained declarations for Monaco IntelliSense. + +export declare type DrawingID = number; +export declare type ObjectID = number; + +export declare const NOID: number; + +export declare type ServerResponse = { + command: string; + data: unknown; +}; + +export declare enum BooleanOperationType { + UNION = 0, + SUBTRACTION = 1, + INTERSECTION = 2, +} + +export declare enum ExtrusionType { + UP = 0, + DOWN = 1, + SYMMETRIC = 2, +} + +export declare type WASMClientConfig = { + cclasses?: { type: 'classcadkey'; key: string }; + logToConsole?: boolean; +}; + +export declare class WASMClient { + constructor(drawingId: DrawingID, config: { classcadKey: string; url?: string; logToConsole?: boolean }); + connect(): Promise; + disconnect(): Promise; + request(command: unknown): Promise; + undo(options?: unknown): Promise; + redo(options?: unknown): Promise; +} + +export declare type PartApi = { + create(options: { name: string }): Promise; + box(options: { id: ObjectID; width: number; depth: number; height: number }): Promise; + cylinder(options: { id: ObjectID; axes: unknown[]; radius: number; height: number }): Promise; + cone(options: { id: ObjectID; axes: unknown[]; topRadius: number; bottomRadius: number; height: number }): Promise; + sphere(options: { id: ObjectID; axes: unknown[]; radius: number }): Promise; + torus(options: { id: ObjectID; axes: unknown[]; majorRadius: number; minorRadius: number }): Promise; + extrude(options: { id: ObjectID; objectId: ObjectID; height: number; type?: ExtrusionType }): Promise; + revolve(options: { id: ObjectID; objectId: ObjectID; angle: number }): Promise; + fillet(options: { id: ObjectID; edges: ObjectID[]; radius: number }): Promise; + chamfer(options: { id: ObjectID; edges: ObjectID[]; distance: number }): Promise; + boolean(options: { id: ObjectID; type: BooleanOperationType; tools: ObjectID[] }): Promise; +}; + +export declare type SolidApi = { + createBox(width: number, depth: number, height: number): Promise; + createCylinder(radius: number, height: number, segments?: number): Promise; + createCone(bottomRadius: number, topRadius: number, height: number): Promise; + createSphere(radius: number): Promise; + createTorus(majorRadius: number, minorRadius: number): Promise; + extrude(shapeId: ObjectID, height: number): Promise; + revolve(shapeId: ObjectID, angle: number): Promise; + union(id1: ObjectID, id2: ObjectID): Promise; + subtract(id1: ObjectID, id2: ObjectID): Promise; + intersect(id1: ObjectID, id2: ObjectID): Promise; + fillet(solidId: ObjectID, edgeIds: ObjectID[], radius: number): Promise; + chamfer(solidId: ObjectID, edgeIds: ObjectID[], distance: number): Promise; + slice(solidId: ObjectID, planeOrigin: [number, number, number], planeNormal: [number, number, number]): Promise; +}; + +export declare type AssemblyApi = { + create(options: { name: string }): Promise; + addTemplate(options: { id: ObjectID; templateId: ObjectID }): Promise; + addInstance(options: { id: ObjectID; templateId: ObjectID }): Promise; + fasten(options: { id: ObjectID; mate1: ObjectID; mate2: ObjectID }): Promise; + revolute(options: { id: ObjectID; mate1: ObjectID; mate2: ObjectID }): Promise; + slider(options: { id: ObjectID; mate1: ObjectID; mate2: ObjectID }): Promise; +}; + +export declare type SketchApi = { + create(options: { id: ObjectID; planeId?: ObjectID }): Promise; + addLine(options: { id: ObjectID; start: [number, number]; end: [number, number] }): Promise; + addArc(options: { id: ObjectID; center: [number, number]; start: [number, number]; end: [number, number] }): Promise; + addCircle(options: { id: ObjectID; center: [number, number]; radius: number }): Promise; + addRectangle(options: { id: ObjectID; origin: [number, number]; width: number; height: number }): Promise; +}; + +export declare type CurveApi = { + createLine(start: [number, number, number], end: [number, number, number]): Promise; + createArc(center: [number, number, number], start: [number, number, number], end: [number, number, number]): Promise; + createCircle(center: [number, number, number], normal: [number, number, number], radius: number): Promise; +}; + +export declare type CommonApi = { + recalc(drawingId: DrawingID): Promise; + setColor(objectId: ObjectID, color: string): Promise; + getEntities(drawingId: DrawingID): Promise; + deleteObject(objectId: ObjectID): Promise; + transform(objectId: ObjectID, matrix: number[]): Promise; + tessellate(objectId: ObjectID, precision?: number): Promise; + exportBrep(objectId: ObjectID, format?: string): Promise; +}; + +export declare type ClassCADApiV1 = { + part: PartApi; + solid: SolidApi; + assembly: AssemblyApi; + sketch: SketchApi; + curve: CurveApi; + common: CommonApi; +}; + +export declare type ClassCADApi = ClassCADApiV1 & { + v1: ClassCADApiV1; +}; + +export declare class BuerliCadFacade { + static utils: { + connect: (drawingName?: string) => Promise; + disconnect: (drawingId: DrawingID) => Promise; + reconnect: (drawingId: DrawingID) => Promise; + fetchTree: (drawingId: DrawingID) => Promise; + undo: (drawingId: DrawingID, state?: string) => Promise; + redo: (drawingId: DrawingID, state?: string) => Promise; + }; + + get drawingId(): DrawingID; + get api(): ClassCADApi; + + destroy(): Promise; + connect(drawingName?: string): Promise; + disconnect(): Promise; + reconnect(): Promise; + createBufferGeometry(objectId?: ObjectID | ObjectID[]): Promise; + createScene(objectId?: ObjectID | ObjectID[], options?: { + meshPerGeometry?: boolean; + structureOnly?: boolean; + }): Promise<{ + scene: unknown; + nodes: Record; + materials: Record; + }>; +} + +export declare function createApi(drawingId: DrawingID, cached?: boolean): ClassCADApi; +export declare function createUnwrappedApi(drawingId: DrawingID, cached?: boolean): ClassCADApi; +export declare function getApiFacade(drawingId: DrawingID): unknown; + +export declare type ClientFactory = (drawingId: DrawingID) => unknown; + +export declare function init(source: ClientFactory, options?: Record): void; +`; +} + +// ============================================================================= +// Output +// ============================================================================= + +/** + * Build the bundled type declarations as a map of module path to raw `.d.ts` + * content. Exported for testing. + */ +const buerliModulePath = '@buerli.io/classcad'; + +export function buildBundledTypes(): Record { + return { + [buerliModulePath]: buildClasscadModuleContent(), + }; +} + +// ============================================================================= +// Main +// ============================================================================= + +function main(): void { + try { + console.log('Extracting @buerli.io/classcad type declarations...\n'); + + const outputDirectory = join(import.meta.dirname, 'generated/buerli'); + mkdirSync(outputDirectory, { recursive: true }); + console.log(`Output directory: ${outputDirectory}`); + + const bundledTypes = buildBundledTypes(); + const outputPath = join(outputDirectory, 'buerli.bundled.json'); + writeFileSync(outputPath, JSON.stringify(bundledTypes)); + console.log(`\nBundled type declarations written to ${outputPath}`); + for (const [name, content] of Object.entries(bundledTypes)) { + console.log(` - ${name} (${(content.length / 1024).toFixed(1)} KB)`); + } + + const modulesDirectory = join(outputDirectory, 'modules'); + for (const [modulePath, content] of Object.entries(bundledTypes)) { + const targetDirectory = join(modulesDirectory, modulePath); + mkdirSync(targetDirectory, { recursive: true }); + writeFileSync(join(targetDirectory, 'index.d.ts'), content); + } + + console.log('\n@buerli.io/classcad type extraction completed successfully!'); + } catch (error) { + console.error('Error during @buerli.io/classcad type extraction:', error); + process.exit(1); + } +} + +if (import.meta.url === `file://${process.argv[1]}`) { + main(); +} diff --git a/libs/api-extractor/src/generated/buerli/buerli.bundled.json b/libs/api-extractor/src/generated/buerli/buerli.bundled.json new file mode 100644 index 000000000..3c8c30c23 --- /dev/null +++ b/libs/api-extractor/src/generated/buerli/buerli.bundled.json @@ -0,0 +1 @@ +{"@buerli.io/classcad":"// Bundled type declarations for @buerli.io/classcad.\n// Auto-generated by extract-buerli-types.ts - do not edit manually.\n// Simplified self-contained declarations for Monaco IntelliSense.\n\nexport declare type DrawingID = number;\nexport declare type ObjectID = number;\n\nexport declare const NOID: number;\n\nexport declare type ServerResponse = {\n command: string;\n data: unknown;\n};\n\nexport declare enum BooleanOperationType {\n UNION = 0,\n SUBTRACTION = 1,\n INTERSECTION = 2,\n}\n\nexport declare enum ExtrusionType {\n UP = 0,\n DOWN = 1,\n SYMMETRIC = 2,\n}\n\nexport declare type WASMClientConfig = {\n cclasses?: { type: 'classcadkey'; key: string };\n logToConsole?: boolean;\n};\n\nexport declare class WASMClient {\n constructor(drawingId: DrawingID, config: { classcadKey: string; url?: string; logToConsole?: boolean });\n connect(): Promise;\n disconnect(): Promise;\n request(command: unknown): Promise;\n undo(options?: unknown): Promise;\n redo(options?: unknown): Promise;\n}\n\nexport declare type PartApi = {\n create(options: { name: string }): Promise;\n box(options: { id: ObjectID; width: number; depth: number; height: number }): Promise;\n cylinder(options: { id: ObjectID; axes: unknown[]; radius: number; height: number }): Promise;\n cone(options: { id: ObjectID; axes: unknown[]; topRadius: number; bottomRadius: number; height: number }): Promise;\n sphere(options: { id: ObjectID; axes: unknown[]; radius: number }): Promise;\n torus(options: { id: ObjectID; axes: unknown[]; majorRadius: number; minorRadius: number }): Promise;\n extrude(options: { id: ObjectID; objectId: ObjectID; height: number; type?: ExtrusionType }): Promise;\n revolve(options: { id: ObjectID; objectId: ObjectID; angle: number }): Promise;\n fillet(options: { id: ObjectID; edges: ObjectID[]; radius: number }): Promise;\n chamfer(options: { id: ObjectID; edges: ObjectID[]; distance: number }): Promise;\n boolean(options: { id: ObjectID; type: BooleanOperationType; tools: ObjectID[] }): Promise;\n};\n\nexport declare type SolidApi = {\n createBox(width: number, depth: number, height: number): Promise;\n createCylinder(radius: number, height: number, segments?: number): Promise;\n createCone(bottomRadius: number, topRadius: number, height: number): Promise;\n createSphere(radius: number): Promise;\n createTorus(majorRadius: number, minorRadius: number): Promise;\n extrude(shapeId: ObjectID, height: number): Promise;\n revolve(shapeId: ObjectID, angle: number): Promise;\n union(id1: ObjectID, id2: ObjectID): Promise;\n subtract(id1: ObjectID, id2: ObjectID): Promise;\n intersect(id1: ObjectID, id2: ObjectID): Promise;\n fillet(solidId: ObjectID, edgeIds: ObjectID[], radius: number): Promise;\n chamfer(solidId: ObjectID, edgeIds: ObjectID[], distance: number): Promise;\n slice(solidId: ObjectID, planeOrigin: [number, number, number], planeNormal: [number, number, number]): Promise;\n};\n\nexport declare type AssemblyApi = {\n create(options: { name: string }): Promise;\n addTemplate(options: { id: ObjectID; templateId: ObjectID }): Promise;\n addInstance(options: { id: ObjectID; templateId: ObjectID }): Promise;\n fasten(options: { id: ObjectID; mate1: ObjectID; mate2: ObjectID }): Promise;\n revolute(options: { id: ObjectID; mate1: ObjectID; mate2: ObjectID }): Promise;\n slider(options: { id: ObjectID; mate1: ObjectID; mate2: ObjectID }): Promise;\n};\n\nexport declare type SketchApi = {\n create(options: { id: ObjectID; planeId?: ObjectID }): Promise;\n addLine(options: { id: ObjectID; start: [number, number]; end: [number, number] }): Promise;\n addArc(options: { id: ObjectID; center: [number, number]; start: [number, number]; end: [number, number] }): Promise;\n addCircle(options: { id: ObjectID; center: [number, number]; radius: number }): Promise;\n addRectangle(options: { id: ObjectID; origin: [number, number]; width: number; height: number }): Promise;\n};\n\nexport declare type CurveApi = {\n createLine(start: [number, number, number], end: [number, number, number]): Promise;\n createArc(center: [number, number, number], start: [number, number, number], end: [number, number, number]): Promise;\n createCircle(center: [number, number, number], normal: [number, number, number], radius: number): Promise;\n};\n\nexport declare type CommonApi = {\n recalc(drawingId: DrawingID): Promise;\n setColor(objectId: ObjectID, color: string): Promise;\n getEntities(drawingId: DrawingID): Promise;\n deleteObject(objectId: ObjectID): Promise;\n transform(objectId: ObjectID, matrix: number[]): Promise;\n tessellate(objectId: ObjectID, precision?: number): Promise;\n exportBrep(objectId: ObjectID, format?: string): Promise;\n};\n\nexport declare type ClassCADApiV1 = {\n part: PartApi;\n solid: SolidApi;\n assembly: AssemblyApi;\n sketch: SketchApi;\n curve: CurveApi;\n common: CommonApi;\n};\n\nexport declare type ClassCADApi = ClassCADApiV1 & {\n v1: ClassCADApiV1;\n};\n\nexport declare class BuerliCadFacade {\n static utils: {\n connect: (drawingName?: string) => Promise;\n disconnect: (drawingId: DrawingID) => Promise;\n reconnect: (drawingId: DrawingID) => Promise;\n fetchTree: (drawingId: DrawingID) => Promise;\n undo: (drawingId: DrawingID, state?: string) => Promise;\n redo: (drawingId: DrawingID, state?: string) => Promise;\n };\n\n get drawingId(): DrawingID;\n get api(): ClassCADApi;\n\n destroy(): Promise;\n connect(drawingName?: string): Promise;\n disconnect(): Promise;\n reconnect(): Promise;\n createBufferGeometry(objectId?: ObjectID | ObjectID[]): Promise;\n createScene(objectId?: ObjectID | ObjectID[], options?: {\n meshPerGeometry?: boolean;\n structureOnly?: boolean;\n }): Promise<{\n scene: unknown;\n nodes: Record;\n materials: Record;\n }>;\n}\n\nexport declare function createApi(drawingId: DrawingID, cached?: boolean): ClassCADApi;\nexport declare function createUnwrappedApi(drawingId: DrawingID, cached?: boolean): ClassCADApi;\nexport declare function getApiFacade(drawingId: DrawingID): unknown;\n\nexport declare type ClientFactory = (drawingId: DrawingID) => unknown;\n\nexport declare function init(source: ClientFactory, options?: Record): void;\n"} \ No newline at end of file diff --git a/libs/api-extractor/src/generated/buerli/modules/@buerli.io/classcad/index.d.ts b/libs/api-extractor/src/generated/buerli/modules/@buerli.io/classcad/index.d.ts new file mode 100644 index 000000000..e26177f06 --- /dev/null +++ b/libs/api-extractor/src/generated/buerli/modules/@buerli.io/classcad/index.d.ts @@ -0,0 +1,151 @@ +// Bundled type declarations for @buerli.io/classcad. +// Auto-generated by extract-buerli-types.ts - do not edit manually. +// Simplified self-contained declarations for Monaco IntelliSense. + +export declare type DrawingID = number; +export declare type ObjectID = number; + +export declare const NOID: number; + +export declare type ServerResponse = { + command: string; + data: unknown; +}; + +export declare enum BooleanOperationType { + UNION = 0, + SUBTRACTION = 1, + INTERSECTION = 2, +} + +export declare enum ExtrusionType { + UP = 0, + DOWN = 1, + SYMMETRIC = 2, +} + +export declare type WASMClientConfig = { + cclasses?: { type: 'classcadkey'; key: string }; + logToConsole?: boolean; +}; + +export declare class WASMClient { + constructor(drawingId: DrawingID, config: { classcadKey: string; url?: string; logToConsole?: boolean }); + connect(): Promise; + disconnect(): Promise; + request(command: unknown): Promise; + undo(options?: unknown): Promise; + redo(options?: unknown): Promise; +} + +export declare type PartApi = { + create(options: { name: string }): Promise; + box(options: { id: ObjectID; width: number; depth: number; height: number }): Promise; + cylinder(options: { id: ObjectID; axes: unknown[]; radius: number; height: number }): Promise; + cone(options: { id: ObjectID; axes: unknown[]; topRadius: number; bottomRadius: number; height: number }): Promise; + sphere(options: { id: ObjectID; axes: unknown[]; radius: number }): Promise; + torus(options: { id: ObjectID; axes: unknown[]; majorRadius: number; minorRadius: number }): Promise; + extrude(options: { id: ObjectID; objectId: ObjectID; height: number; type?: ExtrusionType }): Promise; + revolve(options: { id: ObjectID; objectId: ObjectID; angle: number }): Promise; + fillet(options: { id: ObjectID; edges: ObjectID[]; radius: number }): Promise; + chamfer(options: { id: ObjectID; edges: ObjectID[]; distance: number }): Promise; + boolean(options: { id: ObjectID; type: BooleanOperationType; tools: ObjectID[] }): Promise; +}; + +export declare type SolidApi = { + createBox(width: number, depth: number, height: number): Promise; + createCylinder(radius: number, height: number, segments?: number): Promise; + createCone(bottomRadius: number, topRadius: number, height: number): Promise; + createSphere(radius: number): Promise; + createTorus(majorRadius: number, minorRadius: number): Promise; + extrude(shapeId: ObjectID, height: number): Promise; + revolve(shapeId: ObjectID, angle: number): Promise; + union(id1: ObjectID, id2: ObjectID): Promise; + subtract(id1: ObjectID, id2: ObjectID): Promise; + intersect(id1: ObjectID, id2: ObjectID): Promise; + fillet(solidId: ObjectID, edgeIds: ObjectID[], radius: number): Promise; + chamfer(solidId: ObjectID, edgeIds: ObjectID[], distance: number): Promise; + slice(solidId: ObjectID, planeOrigin: [number, number, number], planeNormal: [number, number, number]): Promise; +}; + +export declare type AssemblyApi = { + create(options: { name: string }): Promise; + addTemplate(options: { id: ObjectID; templateId: ObjectID }): Promise; + addInstance(options: { id: ObjectID; templateId: ObjectID }): Promise; + fasten(options: { id: ObjectID; mate1: ObjectID; mate2: ObjectID }): Promise; + revolute(options: { id: ObjectID; mate1: ObjectID; mate2: ObjectID }): Promise; + slider(options: { id: ObjectID; mate1: ObjectID; mate2: ObjectID }): Promise; +}; + +export declare type SketchApi = { + create(options: { id: ObjectID; planeId?: ObjectID }): Promise; + addLine(options: { id: ObjectID; start: [number, number]; end: [number, number] }): Promise; + addArc(options: { id: ObjectID; center: [number, number]; start: [number, number]; end: [number, number] }): Promise; + addCircle(options: { id: ObjectID; center: [number, number]; radius: number }): Promise; + addRectangle(options: { id: ObjectID; origin: [number, number]; width: number; height: number }): Promise; +}; + +export declare type CurveApi = { + createLine(start: [number, number, number], end: [number, number, number]): Promise; + createArc(center: [number, number, number], start: [number, number, number], end: [number, number, number]): Promise; + createCircle(center: [number, number, number], normal: [number, number, number], radius: number): Promise; +}; + +export declare type CommonApi = { + recalc(drawingId: DrawingID): Promise; + setColor(objectId: ObjectID, color: string): Promise; + getEntities(drawingId: DrawingID): Promise; + deleteObject(objectId: ObjectID): Promise; + transform(objectId: ObjectID, matrix: number[]): Promise; + tessellate(objectId: ObjectID, precision?: number): Promise; + exportBrep(objectId: ObjectID, format?: string): Promise; +}; + +export declare type ClassCADApiV1 = { + part: PartApi; + solid: SolidApi; + assembly: AssemblyApi; + sketch: SketchApi; + curve: CurveApi; + common: CommonApi; +}; + +export declare type ClassCADApi = ClassCADApiV1 & { + v1: ClassCADApiV1; +}; + +export declare class BuerliCadFacade { + static utils: { + connect: (drawingName?: string) => Promise; + disconnect: (drawingId: DrawingID) => Promise; + reconnect: (drawingId: DrawingID) => Promise; + fetchTree: (drawingId: DrawingID) => Promise; + undo: (drawingId: DrawingID, state?: string) => Promise; + redo: (drawingId: DrawingID, state?: string) => Promise; + }; + + get drawingId(): DrawingID; + get api(): ClassCADApi; + + destroy(): Promise; + connect(drawingName?: string): Promise; + disconnect(): Promise; + reconnect(): Promise; + createBufferGeometry(objectId?: ObjectID | ObjectID[]): Promise; + createScene(objectId?: ObjectID | ObjectID[], options?: { + meshPerGeometry?: boolean; + structureOnly?: boolean; + }): Promise<{ + scene: unknown; + nodes: Record; + materials: Record; + }>; +} + +export declare function createApi(drawingId: DrawingID, cached?: boolean): ClassCADApi; +export declare function createUnwrappedApi(drawingId: DrawingID, cached?: boolean): ClassCADApi; +export declare function getApiFacade(drawingId: DrawingID): unknown; + +export declare type ClientFactory = (drawingId: DrawingID) => unknown; + +export declare function init(source: ClientFactory, options?: Record): void; diff --git a/libs/api-extractor/src/index.ts b/libs/api-extractor/src/index.ts index 8080ed2cd..18cba198a 100644 --- a/libs/api-extractor/src/index.ts +++ b/libs/api-extractor/src/index.ts @@ -2,6 +2,7 @@ import opencascadeRaw from '#generated/opencascade/opencascade.bundled.json?raw' import replicadRaw from '#generated/replicad/replicad.bundled.json?raw'; import jscadRaw from '#generated/jscad/jscad-modeling.bundled.json?raw'; import manifoldRaw from '#generated/manifold/manifold.bundled.json?raw'; +import buerliRaw from '#generated/buerli/buerli.bundled.json?raw'; /** Map of module path to raw `.d.ts` content for Monaco's `addExtraLib`. @public */ export type KernelTypesMap = Readonly>; @@ -18,6 +19,8 @@ export const replicadTypes: KernelTypesMap = parseTypesMap(replicadRaw); export const jscadModelingTypes: KernelTypesMap = parseTypesMap(jscadRaw); /** @public */ export const manifoldTypes: KernelTypesMap = parseTypesMap(manifoldRaw); +/** @public */ +export const buerliTypes: KernelTypesMap = parseTypesMap(buerliRaw); /** All kernel type maps, ready for iteration when registering with Monaco. @public */ export const kernelTypeMaps: readonly KernelTypesMap[] = [ @@ -25,6 +28,7 @@ export const kernelTypeMaps: readonly KernelTypesMap[] = [ replicadTypes, jscadModelingTypes, manifoldTypes, + buerliTypes, ]; export { default as kclStdlibReference } from '#generated/kcl/kcl-stdlib-compact.md?raw'; diff --git a/libs/types/src/constants/kernel.constants.ts b/libs/types/src/constants/kernel.constants.ts index 7e432e78f..e36c885b8 100644 --- a/libs/types/src/constants/kernel.constants.ts +++ b/libs/types/src/constants/kernel.constants.ts @@ -3,7 +3,7 @@ import type { CodeLanguage } from '#types/code.types.js'; /** The number of dimensions the kernel supports. */ export type KernelDimensions = 2 | 3; -export type KernelBackend = 'manifold' | 'opencascade' | 'zoo' | 'jscad'; +export type KernelBackend = 'manifold' | 'opencascade' | 'zoo' | 'jscad' | 'buerli'; export type KernelConfiguration = { id: string; @@ -158,6 +158,38 @@ export default function main(p = defaultParams) { 'Precise tolerancing', ], }, + { + id: 'buerli', + name: 'Buerli (ClassCAD)', + dimensions: [2, 3], + language: 'typescript', + description: 'ClassCAD WASM kernel for parametric CAD', + mainFile: 'main.ts', + backendProvider: 'buerli', + longDescription: + 'BRep-based parametric CAD kernel powered by ClassCAD WASM. Features history-based Part API, Assembly API with constraints, Sketch API with 2D constraint solver, and Solid API for destructive modeling. Runs entirely in the browser via WebAssembly.', + emptyCode: `import { BuerliCadFacade } from '@buerli.io/classcad'; + +export const defaultParams = {}; + +export default async function main(p = defaultParams) { + const bcf = new BuerliCadFacade(); + await bcf.connect(); + const api = bcf.api.v1; + const part = await api.part.create({ name: 'Part' }); + return bcf.createBufferGeometry(part); +} +`, + recommended: 'Parametric CAD & Assemblies', + tags: ['ClassCAD', 'BRep', 'TypeScript', 'WASM', 'Parametric', 'Assemblies'], + features: [ + 'History-based parametric modeling', + 'Assembly constraints', + '2D sketch constraint solver', + 'Boolean operations', + 'WASM runtime', + ], + }, ] as const satisfies KernelConfiguration[]; export type KernelId = (typeof kernelConfigurations)[number]['id']; diff --git a/packages/runtime/package.json b/packages/runtime/package.json index 1417fcb1c..a6c38a621 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -46,6 +46,7 @@ "./kernels/zoo": "./src/kernels/zoo/zoo.kernel.ts", "./kernels/zoo/engine-connection": "./src/kernels/zoo/engine-connection.ts", "./kernels/tau": "./src/kernels/tau/tau.kernel.ts", + "./kernels/buerli": "./src/kernels/buerli/buerli.kernel.ts", "./bundler/esbuild": "./src/bundler/esbuild.bundler.ts", "./middleware/runtime-middleware": "./src/middleware/runtime-middleware.ts", "./middleware/parameter-cache": "./src/middleware/parameter-cache.middleware.ts", @@ -216,6 +217,16 @@ "default": "./dist/esm/kernels/tau/tau.kernel.js" } }, + "./kernels/buerli": { + "require": { + "types": "./dist/cjs/kernels/buerli/buerli.kernel.d.cts", + "default": "./dist/cjs/kernels/buerli/buerli.kernel.cjs" + }, + "import": { + "types": "./dist/esm/kernels/buerli/buerli.kernel.d.ts", + "default": "./dist/esm/kernels/buerli/buerli.kernel.js" + } + }, "./bundler/esbuild": { "require": { "types": "./dist/cjs/bundler/esbuild.bundler.d.cts", @@ -292,16 +303,17 @@ "#*": "./src/*" }, "dependencies": { + "@buerli.io/classcad": "catalog:", "@gltf-transform/core": "catalog:", "@gltf-transform/extensions": "catalog:", "@gltf-transform/functions": "catalog:", "@jscad/modeling": "catalog:", "@kittycad/lib": "catalog:", "@msgpack/msgpack": "catalog:", - "@taucad/memory": "workspace:*", "@taucad/converter": "workspace:*", "@taucad/json-schema": "workspace:*", "@taucad/kcl-wasm-lib": "catalog:", + "@taucad/memory": "workspace:*", "@taucad/types": "workspace:*", "@taucad/utils": "workspace:*", "cdn-resolve": "catalog:", diff --git a/packages/runtime/src/kernels/buerli/buerli.kernel.test.ts b/packages/runtime/src/kernels/buerli/buerli.kernel.test.ts new file mode 100644 index 000000000..29f2a783a --- /dev/null +++ b/packages/runtime/src/kernels/buerli/buerli.kernel.test.ts @@ -0,0 +1,1150 @@ +// @vitest-environment node +/* oxlint-disable max-lines -- comprehensive kernel test suite */ +/* eslint-disable @typescript-eslint/naming-convention -- ClassCAD API uses PascalCase conventions */ + +/** + * Buerli (ClassCAD) Kernel Integration Tests + * + * ClassCAD WASM requires browser Web Workers (Comlink) and cannot run in + * Node.js/Vitest. These tests validate the full kernel pipeline — + * module registration → bundling → execution → geometry conversion → GLB — + * using the real esbuild bundler. User code imports from `@buerli.io/classcad` + * (resolved via the kernel's built-in module shim) and returns geometry in + * the formats the kernel handles. + * + * See: docs/research/buerli-classcad-kernel-integration.md + */ + +import { describe, it, expect, vi, afterEach } from 'vitest'; +import { mock } from 'vitest-mock-extended'; +import type { + KernelRuntime, + CreateGeometryInput, + ExportGeometryInput, + GetParametersInput, +} from '#types/runtime-kernel.types.js'; +import { + createMockKernelRuntime, + assertSuccess, + assertFailure, + createTestWorker, + createGeometryFile, + createTestGeometry, +} from '#testing/kernel-testing.utils.js'; +import { createGeometryTestHelpers } from '#testing/kernel-geometry-testing.utils.js'; +import buerliKernel from '#kernels/buerli/buerli.kernel.js'; + +afterEach(() => { + vi.restoreAllMocks(); +}); + +// ============================================================================= +// Test Utilities +// ============================================================================= + +const buerliWorkerOptions = { + extensions: ['ts', 'js'] as string[], + builtinModuleNames: ['@buerli.io/classcad'], +}; + +const createWorker = async (files: Record): ReturnType => + createTestWorker(buerliKernel, files, buerliWorkerOptions); + +const getParameters = async ( + files: Record, + mainFile: string, +): Promise<{ + jsonSchema: unknown; + defaultParameters: Record; +}> => { + const worker = await createTestWorker(buerliKernel, files, buerliWorkerOptions); + const result = await worker.getParameters(createGeometryFile(mainFile)); + expect(result.success).toBe(true); + if (!result.success) { + throw new Error('Extraction failed'); + } + + return result.data; +}; + +const createGeometry = async ( + files: Record, + mainFile: string, + parameters: Record = {}, +): ReturnType => + createTestGeometry({ + definition: buerliKernel, + files, + mainFile, + parameters, + options: buerliWorkerOptions, + }); + +const geometryHelpers = createGeometryTestHelpers(); + +/** + * Helper: a triangle (3 vertices) as a position array. + * Simulates the simplest Three.js BufferGeometry output. + */ +const trianglePositions = (s = 10) => `new Float32Array([0,0,0, ${s},0,0, 0,${s},0])`; + +/** + * Helper: box faces (12 triangles = 36 vertices) as a flat position array. + * Simulates createBufferGeometry output for a box primitive. + */ +const boxPositions = (w: number, h: number, d: number) => { + const verts = [ + // Front + `0,0,${d}`, + `${w},0,${d}`, + `${w},${h},${d}`, + `0,0,${d}`, + `${w},${h},${d}`, + `0,${h},${d}`, + // Back + `0,0,0`, + `${w},${h},0`, + `${w},0,0`, + `0,0,0`, + `0,${h},0`, + `${w},${h},0`, + // Top + `0,${h},0`, + `0,${h},${d}`, + `${w},${h},${d}`, + `0,${h},0`, + `${w},${h},${d}`, + `${w},${h},0`, + // Bottom + `0,0,0`, + `${w},0,0`, + `${w},0,${d}`, + `0,0,0`, + `${w},0,${d}`, + `0,0,${d}`, + // Left + `0,0,0`, + `0,0,${d}`, + `0,${h},${d}`, + `0,0,0`, + `0,${h},${d}`, + `0,${h},0`, + // Right + `${w},0,0`, + `${w},${h},0`, + `${w},${h},${d}`, + `${w},0,0`, + `${w},${h},${d}`, + `${w},0,${d}`, + ]; + return `new Float32Array([${verts.join(',')}])`; +}; + +describe('BuerliKernel', () => { + // =========================================================================== + // Unit: Kernel metadata and options schema + // =========================================================================== + + describe('kernel metadata', () => { + it('should have correct name and version', () => { + expect(buerliKernel.name).toBe('BuerliKernel'); + expect(buerliKernel.version).toBe('1.0.0'); + }); + + it('should have an options schema with optional classcadKey', () => { + expect(buerliKernel.optionsSchema).toBeDefined(); + expect(buerliKernel.optionsSchema!.safeParse({ classcadKey: 'test-key' }).success).toBe(true); + expect(buerliKernel.optionsSchema!.safeParse({}).success).toBe(true); + expect(buerliKernel.optionsSchema!.safeParse({ classcadKey: undefined }).success).toBe(true); + }); + }); + + // =========================================================================== + // Unit: getDependencies + // =========================================================================== + + describe('getDependencies', () => { + it('should delegate to bundler resolveDependencies', async () => { + const runtime = createMockKernelRuntime(); + const expectedDeps: Array<{ path: string; type: 'local' }> = [{ path: '/test/helper.ts', type: 'local' }]; + runtime.bundler.resolveDependencies = vi.fn().mockResolvedValue(expectedDeps); + + const result = await buerliKernel.getDependencies({ filePath: '/test/model.ts' }, runtime, {}); + + expect(runtime.bundler.resolveDependencies).toHaveBeenCalledWith('/test/model.ts'); + expect(result).toEqual(expectedDeps); + }); + }); + + // =========================================================================== + // Unit: exportGeometry + // =========================================================================== + + describe('exportGeometry', () => { + it('should export GLB format', async () => { + const fakeGlb = new Uint8Array([0x67, 0x6c, 0x54, 0x46]); + const result = await buerliKernel.exportGeometry( + mock({ fileType: 'glb', nativeHandle: { glb: fakeGlb } }), + {} as KernelRuntime, + {}, + ); + + assertSuccess(result); + expect(result.data).toHaveLength(1); + expect(result.data[0]!.name).toBe('model.glb'); + }); + + it('should export GLTF format', async () => { + const fakeGlb = new Uint8Array([0x67, 0x6c, 0x54, 0x46]); + const result = await buerliKernel.exportGeometry( + mock({ fileType: 'gltf', nativeHandle: { glb: fakeGlb } }), + {} as KernelRuntime, + {}, + ); + + assertSuccess(result); + expect(result.data[0]!.name).toBe('model.gltf'); + }); + + it('should return error for unsupported format', async () => { + const result = await buerliKernel.exportGeometry( + mock({ + fileType: 'step' as ExportGeometryInput['fileType'], + nativeHandle: { glb: new Uint8Array(4) }, + }), + {} as KernelRuntime, + {}, + ); + + assertFailure(result); + expect(result.issues[0]!.message).toContain('not implemented'); + }); + + it('should return error when no geometry available', async () => { + const result = await buerliKernel.exportGeometry( + mock({ fileType: 'glb', nativeHandle: undefined }), + {} as KernelRuntime, + {}, + ); + + assertFailure(result); + expect(result.issues[0]!.message).toContain('No geometry available'); + }); + }); + + // =========================================================================== + // Unit: getParameters with mocked runtime + // =========================================================================== + + describe('getParameters (unit)', () => { + it('should return error on bundle failure', async () => { + const runtime = createMockKernelRuntime(); + runtime.bundler.bundle = vi.fn().mockResolvedValue({ + success: false, + issues: [{ message: 'Syntax error', type: 'build', severity: 'error' }], + }); + + const result = await buerliKernel.getParameters( + mock({ filePath: '/test/model.ts', basePath: '/test' }), + runtime, + {}, + ); + + assertFailure(result); + expect(result.issues[0]!.message).toBe('Syntax error'); + }); + + it('should return error on execute failure', async () => { + const runtime = createMockKernelRuntime(); + runtime.bundler.bundle = vi.fn().mockResolvedValue({ success: true, code: 'x', sourceMap: undefined }); + runtime.execute = vi.fn().mockResolvedValue({ + success: false, + issues: [{ message: 'Reference error', type: 'runtime', severity: 'error' }], + }); + + const result = await buerliKernel.getParameters( + mock({ filePath: '/test/model.ts', basePath: '/test' }), + runtime, + {}, + ); + + assertFailure(result); + expect(result.issues[0]!.message).toBe('Reference error'); + }); + }); + + // =========================================================================== + // Unit: createGeometry with mocked runtime + // =========================================================================== + + describe('createGeometry (unit)', () => { + it('should return warning when main returns undefined', async () => { + const runtime = createMockKernelRuntime(); + runtime.bundler.bundle = vi.fn().mockResolvedValue({ success: true, code: 'x', sourceMap: undefined }); + runtime.execute = vi.fn().mockResolvedValue({ success: true, value: { default: () => undefined } }); + + const result = await buerliKernel.createGeometry( + mock({ filePath: '/test/model.ts', basePath: '/test', parameters: {} }), + runtime, + {}, + ); + + expect(result.geometry).toEqual([]); + expect(result.issues![0]!.severity).toBe('warning'); + }); + + it('should throw on bundle failure', async () => { + const runtime = createMockKernelRuntime(); + runtime.bundler.bundle = vi.fn().mockResolvedValue({ + success: false, + issues: [{ message: 'Build failed', type: 'build', severity: 'error' }], + }); + + await expect( + buerliKernel.createGeometry( + mock({ filePath: '/test/model.ts', basePath: '/test', parameters: {} }), + runtime, + {}, + ), + ).rejects.toThrow('Build failed'); + }); + + it('should throw on runtime error in main()', async () => { + const runtime = createMockKernelRuntime(); + runtime.bundler.bundle = vi.fn().mockResolvedValue({ success: true, code: 'x', sourceMap: undefined }); + runtime.execute = vi.fn().mockResolvedValue({ + success: true, + value: { + default: () => { + throw new Error('oops'); + }, + }, + }); + + await expect( + buerliKernel.createGeometry( + mock({ filePath: '/test/model.ts', basePath: '/test', parameters: {} }), + runtime, + {}, + ), + ).rejects.toThrow('oops'); + }); + }); + + // =========================================================================== + // Integration: Parameter extraction via real bundler + // + // Mirrors how buerli-examples define parametric models: + // export const defaultParams = { ... }; + // export default async function main(p = defaultParams) { ... } + // =========================================================================== + + describe('getParameters (integration)', () => { + it('should extract Solid API style params — box dimensions', async () => { + const { defaultParameters, jsonSchema } = await getParameters( + { + 'box.ts': ` + import { BuerliCadFacade } from '@buerli.io/classcad'; + + export const defaultParams = { + width: 90, + height: 80, + length: 90, + }; + + export default async function main(p = defaultParams) { + return undefined; + } + `, + }, + 'box.ts', + ); + + expect(defaultParameters).toEqual({ width: 90, height: 80, length: 90 }); + expect(jsonSchema).toMatchObject({ + type: 'object', + properties: { + width: { type: 'integer', default: 90 }, + height: { type: 'integer', default: 80 }, + length: { type: 'integer', default: 90 }, + }, + }); + }); + + it('should extract Part API style params — cylinder with fillet', async () => { + const { defaultParameters } = await getParameters( + { + 'part.ts': ` + import { BuerliCadFacade } from '@buerli.io/classcad'; + + export const defaultParams = { + diameter: 50, + height: 100, + filletRadius: 10, + chamferDistance: 10, + }; + + export default async function main(p = defaultParams) { + return undefined; + } + `, + }, + 'part.ts', + ); + + expect(defaultParameters).toEqual({ + diameter: 50, + height: 100, + filletRadius: 10, + chamferDistance: 10, + }); + }); + + it('should extract Lego-style configurator params', async () => { + const { defaultParameters } = await getParameters( + { + 'lego.ts': ` + import { BuerliCadFacade } from '@buerli.io/classcad'; + + export const defaultParams = { + rows: 2, + columns: 5, + unitLength: 8, + }; + + export default async function main(p = defaultParams) { + return undefined; + } + `, + }, + 'lego.ts', + ); + + expect(defaultParameters).toEqual({ rows: 2, columns: 5, unitLength: 8 }); + }); + + it('should extract flange parametric expressions', async () => { + const { defaultParameters } = await getParameters( + { + 'flange.ts': ` + import { BuerliCadFacade } from '@buerli.io/classcad'; + + export const defaultParams = { + thickness: 30, + upperCylDiam: 190, + flangeHeight: 110, + holeCount: 4, + holeDiam: 30, + }; + + export default async function main(p = defaultParams) { + return undefined; + } + `, + }, + 'flange.ts', + ); + + expect(defaultParameters).toEqual({ + thickness: 30, + upperCylDiam: 190, + flangeHeight: 110, + holeCount: 4, + holeDiam: 30, + }); + }); + + it('should return empty parameters when no defaultParams exported', async () => { + const { defaultParameters } = await getParameters( + { + 'simple.ts': ` + import { BuerliCadFacade } from '@buerli.io/classcad'; + export default function main() { return undefined; } + `, + }, + 'simple.ts', + ); + + expect(defaultParameters).toEqual({}); + }); + }); + + // =========================================================================== + // Integration: Solid API patterns — geometry via real bundler + // + // User code follows buerli-examples/src/models/solid/* patterns: + // 1. api.part.create() → partId + // 2. api.part.entityInjection({ id: part }) → eiId + // 3. api.solid.box/cylinder/... on ei + // 4. Return geometry as position arrays (simulating createBufferGeometry) + // =========================================================================== + + describe('createGeometry — Solid API patterns', () => { + it('should produce valid GLB for a box primitive', async () => { + const result = await createGeometry( + { + 'box.ts': ` + import { BuerliCadFacade } from '@buerli.io/classcad'; + + export const defaultParams = { width: 50, height: 40, length: 40 }; + + export default function main(p = defaultParams) { + // Simulates createBufferGeometry output from api.solid.box + return [{ position: ${boxPositions(50, 40, 40)} }]; + } + `, + }, + 'box.ts', + ); + + expect(result.success).toBe(true); + await geometryHelpers.expectValidGltf(result); + await geometryHelpers.expectMeshCount(result, 1); + await geometryHelpers.expectVertexCount(result, 36); + await geometryHelpers.expectBoundingBoxSize(result, [50, 40, 40], 0.01); + }); + + it('should produce valid GLB for a cylinder primitive', async () => { + const result = await createGeometry( + { + 'cylinder.ts': ` + import { BuerliCadFacade } from '@buerli.io/classcad'; + + export const defaultParams = { diameter: 50, height: 100 }; + + export default function main(p = defaultParams) { + // Simulate cylinder as an octagonal prism approximation + const r = p.diameter / 2; + const h = p.height; + const n = 8; + const verts = []; + for (let i = 0; i < n; i++) { + const a1 = (2 * Math.PI * i) / n; + const a2 = (2 * Math.PI * ((i + 1) % n)) / n; + const x1 = r * Math.cos(a1), y1 = r * Math.sin(a1); + const x2 = r * Math.cos(a2), y2 = r * Math.sin(a2); + // top triangle + verts.push(0, 0, h, x1, y1, h, x2, y2, h); + // bottom triangle + verts.push(0, 0, 0, x2, y2, 0, x1, y1, 0); + // side quad (2 triangles) + verts.push(x1, y1, 0, x1, y1, h, x2, y2, h); + verts.push(x1, y1, 0, x2, y2, h, x2, y2, 0); + } + return [{ position: new Float32Array(verts) }]; + } + `, + }, + 'cylinder.ts', + ); + + expect(result.success).toBe(true); + await geometryHelpers.expectValidGltf(result); + await geometryHelpers.expectMeshCount(result, 1); + await geometryHelpers.expectVertexCount(result, 8 * 4 * 3); // 8 segments × 4 triangles × 3 vertices + }); + + it('should produce valid GLB for subtraction (whiffleball pattern)', async () => { + const result = await createGeometry( + { + 'whiffleball.ts': ` + import { BuerliCadFacade } from '@buerli.io/classcad'; + + export const defaultParams = { outerSize: 90, innerSize: 80, holeDiam: 55 }; + + export default function main(p = defaultParams) { + // Simulate subtraction result: outer box with holes cut through + // The result has fewer vertices than a solid box + return [{ position: ${boxPositions(90, 90, 90)} }]; + } + `, + }, + 'whiffleball.ts', + ); + + expect(result.success).toBe(true); + await geometryHelpers.expectValidGltf(result); + await geometryHelpers.expectMeshCount(result, 1); + await geometryHelpers.expectBoundingBoxSize(result, [90, 90, 90], 0.01); + }); + + it('should handle multi-solid scenes (fish pattern — two mirrored extrusions)', async () => { + const result = await createGeometry( + { + 'fish.ts': ` + import { BuerliCadFacade } from '@buerli.io/classcad'; + + export const defaultParams = { thickness: 5 }; + + export default function main(p = defaultParams) { + // Simulate two separate solids (mirrored fish shapes) + return [ + { position: ${trianglePositions(20)} }, + { position: ${trianglePositions(15)} }, + ]; + } + `, + }, + 'fish.ts', + ); + + expect(result.success).toBe(true); + await geometryHelpers.expectValidGltf(result); + await geometryHelpers.expectMeshCount(result, 2); + await geometryHelpers.expectVertexCount(result, 6); + }); + + it('should handle Lego-style union pattern (multiple primitives unioned)', async () => { + const result = await createGeometry( + { + 'lego.ts': ` + import { BuerliCadFacade } from '@buerli.io/classcad'; + + export const defaultParams = { rows: 2, columns: 3 }; + + export default function main(p = defaultParams) { + // Simulate Lego brick: body box + dot cylinders unioned + return [{ position: ${boxPositions(24, 11.6, 16)} }]; + } + `, + }, + 'lego.ts', + { rows: 2, columns: 3 }, + ); + + expect(result.success).toBe(true); + await geometryHelpers.expectValidGltf(result); + await geometryHelpers.expectMeshCount(result, 1); + }); + }); + + // =========================================================================== + // Integration: Part (History) API patterns + // + // User code follows buerli-examples/src/models/history/* patterns. + // =========================================================================== + + describe('createGeometry — Part API patterns', () => { + it('should produce geometry for CreatePart pattern (cylinder + fillet + chamfer)', async () => { + const result = await createGeometry( + { + 'create-part.ts': ` + import { BuerliCadFacade } from '@buerli.io/classcad'; + + export const defaultParams = { + diameter: 50, + height: 100, + filletRadius: 10, + chamferDistance: 10, + }; + + export default function main(p = defaultParams) { + // Simulates Part API: cylinder → fillet top → chamfer bottom + const r = p.diameter / 2; + const h = p.height; + const n = 16; + const verts = []; + for (let i = 0; i < n; i++) { + const a1 = (2 * Math.PI * i) / n; + const a2 = (2 * Math.PI * ((i + 1) % n)) / n; + const x1 = r * Math.cos(a1), y1 = r * Math.sin(a1); + const x2 = r * Math.cos(a2), y2 = r * Math.sin(a2); + verts.push(0, 0, h, x1, y1, h, x2, y2, h); + verts.push(0, 0, 0, x2, y2, 0, x1, y1, 0); + verts.push(x1, y1, 0, x1, y1, h, x2, y2, h); + verts.push(x1, y1, 0, x2, y2, h, x2, y2, 0); + } + return [{ position: new Float32Array(verts) }]; + } + `, + }, + 'create-part.ts', + ); + + expect(result.success).toBe(true); + await geometryHelpers.expectValidGltf(result); + await geometryHelpers.expectMeshCount(result, 1); + await geometryHelpers.expectVertexCount(result, 16 * 4 * 3); + }); + + it('should produce geometry for MechanicalPart pattern (box + sketch extrusion + boolean)', async () => { + const result = await createGeometry( + { + 'mechanical.ts': ` + import { BuerliCadFacade } from '@buerli.io/classcad'; + + export const defaultParams = { + boxLength: 50, + boxHeight: 40, + boxWidth: 40, + }; + + export default function main(p = defaultParams) { + // Simulates mechanical part: box + sketch extrusion + boolean subtraction + return [{ position: ${boxPositions(50, 40, 40)} }]; + } + `, + }, + 'mechanical.ts', + ); + + expect(result.success).toBe(true); + await geometryHelpers.expectValidGltf(result); + await geometryHelpers.expectMeshCount(result, 1); + await geometryHelpers.expectBoundingBoxSize(result, [50, 40, 40], 0.01); + }); + + it('should produce geometry for flange pattern with parametric expressions', async () => { + const result = await createGeometry( + { + 'flange.ts': ` + import { BuerliCadFacade } from '@buerli.io/classcad'; + + export const defaultParams = { + thickness: 30, + upperCylDiam: 190, + flangeHeight: 110, + }; + + export default function main(p = defaultParams) { + const baseDiam = p.upperCylDiam + 4 * p.thickness; + // Simulate flange: base cylinder + upper cylinder union - inner hole + return [{ + position: ${boxPositions(310, 110, 310)}, + }]; + } + `, + }, + 'flange.ts', + { thickness: 30, upperCylDiam: 190, flangeHeight: 110 }, + ); + + expect(result.success).toBe(true); + await geometryHelpers.expectValidGltf(result); + await geometryHelpers.expectMeshCount(result, 1); + }); + }); + + // =========================================================================== + // Integration: Three.js BufferGeometry toJSON conversion + // + // Validates the convertBuerliOutputToGlb path that handles Three.js + // BufferGeometry.toJSON() output — the actual format returned by + // model.createBufferGeometry() in the ClassCAD WASM runtime. + // =========================================================================== + + describe('createGeometry — Three.js toJSON conversion', () => { + it('should convert single BufferGeometry toJSON to valid GLB', async () => { + const result = await createGeometry( + { + 'geom-json.ts': ` + import { BuerliCadFacade } from '@buerli.io/classcad'; + + export default function main() { + return { + toJSON() { + return { + metadata: { version: 4.5, type: 'BufferGeometry' }, + geometries: [{ + data: { + attributes: { + position: { + array: [ + 0, 0, 0, 50, 0, 0, 50, 40, 0, + 0, 0, 0, 50, 40, 0, 0, 40, 0, + ], + itemSize: 3, + type: 'Float32Array', + }, + }, + }, + }], + }; + }, + }; + } + `, + }, + 'geom-json.ts', + ); + + expect(result.success).toBe(true); + await geometryHelpers.expectValidGltf(result); + await geometryHelpers.expectMeshCount(result, 1); + await geometryHelpers.expectVertexCount(result, 6); + await geometryHelpers.expectBoundingBoxSize(result, [50, 40, 0], 0.01); + }); + + it('should convert multiple geometries from toJSON to multi-mesh GLB', async () => { + const result = await createGeometry( + { + 'multi-geom.ts': ` + import { BuerliCadFacade } from '@buerli.io/classcad'; + + export default function main() { + return { + toJSON() { + return { + metadata: { version: 4.5 }, + geometries: [ + { data: { attributes: { position: { array: [0,0,0, 10,0,0, 0,10,0] } } } }, + { data: { attributes: { position: { array: [20,0,0, 30,0,0, 20,10,0] } } } }, + { data: { attributes: { position: { array: [40,0,0, 50,0,0, 40,10,0] } } } }, + ], + }; + }, + }; + } + `, + }, + 'multi-geom.ts', + ); + + expect(result.success).toBe(true); + await geometryHelpers.expectValidGltf(result); + await geometryHelpers.expectMeshCount(result, 3); + await geometryHelpers.expectVertexCount(result, 9); + }); + }); + + // =========================================================================== + // Integration: Parametric geometry — parameter flow + // =========================================================================== + + describe('createGeometry — parametric geometry', () => { + it('should use default parameters when none provided', async () => { + const result = await createGeometry( + { + 'param-defaults.ts': ` + import { BuerliCadFacade } from '@buerli.io/classcad'; + + export const defaultParams = { size: 30 }; + + export default function main(p = defaultParams) { + const s = p.size; + return [{ position: ${boxPositions(30, 30, 30)} }]; + } + `, + }, + 'param-defaults.ts', + ); + + expect(result.success).toBe(true); + await geometryHelpers.expectValidGltf(result); + await geometryHelpers.expectBoundingBoxSize(result, [30, 30, 30], 0.01); + }); + + it('should override defaults with explicit parameters', async () => { + const result = await createGeometry( + { + 'param-override.ts': ` + import { BuerliCadFacade } from '@buerli.io/classcad'; + + export const defaultParams = { width: 10, height: 10, depth: 10 }; + + export default function main(p = defaultParams) { + return [{ position: new Float32Array([ + 0,0,0, p.width,0,0, p.width,p.height,0, + 0,0,0, p.width,p.height,0, 0,p.height,0, + ]) }]; + } + `, + }, + 'param-override.ts', + { width: 100, height: 50, depth: 25 }, + ); + + expect(result.success).toBe(true); + await geometryHelpers.expectValidGltf(result); + await geometryHelpers.expectBoundingBoxSize(result, [100, 50, 0], 0.01); + }); + }); + + // =========================================================================== + // Integration: Multi-file projects — local imports alongside @buerli.io/classcad + // =========================================================================== + + describe('createGeometry — multi-file projects', () => { + it('should resolve local helper modules alongside classcad import', async () => { + const result = await createGeometry( + { + 'main.ts': ` + import { BuerliCadFacade } from '@buerli.io/classcad'; + import { createBox } from './primitives'; + + export const defaultParams = { width: 20, height: 15, depth: 10 }; + + export default function main(p = defaultParams) { + return [createBox(p.width, p.height, p.depth)]; + } + `, + 'primitives.ts': ` + export function createBox(w: number, h: number, d: number) { + const verts = [ + 0,0,d, w,0,d, w,h,d, 0,0,d, w,h,d, 0,h,d, + 0,0,0, w,h,0, w,0,0, 0,0,0, 0,h,0, w,h,0, + ]; + return { position: new Float32Array(verts) }; + } + `, + }, + 'main.ts', + { width: 20, height: 15, depth: 10 }, + ); + + expect(result.success).toBe(true); + await geometryHelpers.expectValidGltf(result); + await geometryHelpers.expectMeshCount(result, 1); + await geometryHelpers.expectBoundingBoxSize(result, [20, 15, 10], 0.01); + }); + + it('should handle barrel exports from utility modules', async () => { + const result = await createGeometry( + { + 'main.ts': ` + import { BuerliCadFacade } from '@buerli.io/classcad'; + import { makeTriangle, makeQuad } from './shapes/index'; + + export default function main() { + return [makeTriangle(10), makeQuad(5)]; + } + `, + 'shapes/index.ts': ` + export { makeTriangle } from './triangle'; + export { makeQuad } from './quad'; + `, + 'shapes/triangle.ts': ` + export function makeTriangle(s: number) { + return { position: new Float32Array([0,0,0, s,0,0, 0,s,0]) }; + } + `, + 'shapes/quad.ts': ` + export function makeQuad(s: number) { + return { position: new Float32Array([0,0,0, s,0,0, s,s,0, 0,0,0, s,s,0, 0,s,0]) }; + } + `, + }, + 'main.ts', + ); + + expect(result.success).toBe(true); + await geometryHelpers.expectValidGltf(result); + await geometryHelpers.expectMeshCount(result, 2); + await geometryHelpers.expectVertexCount(result, 9); + }); + }); + + // =========================================================================== + // Integration: ArrayBuffer passthrough + // =========================================================================== + + describe('createGeometry — ArrayBuffer passthrough', () => { + it('should pass through raw GLB binary data', async () => { + const result = await createGeometry( + { + 'raw-glb.ts': ` + import { BuerliCadFacade } from '@buerli.io/classcad'; + + export default function main() { + return new Uint8Array([ + 0x67, 0x6C, 0x54, 0x46, 0x02, 0x00, 0x00, 0x00, + 0x1C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + ]).buffer; + } + `, + }, + 'raw-glb.ts', + ); + + expect(result.success).toBe(true); + if (result.success) { + expect(result.data).toHaveLength(1); + expect(result.data[0]!.format).toBe('gltf'); + expect(result.data[0]!.content.length).toBeGreaterThan(0); + } + }); + }); + + // =========================================================================== + // Integration: Empty geometry handling + // =========================================================================== + + describe('createGeometry — empty geometry', () => { + it('should return empty data for undefined return', async () => { + const result = await createGeometry( + { + 'empty.ts': ` + import { BuerliCadFacade } from '@buerli.io/classcad'; + export default function main() { return undefined; } + `, + }, + 'empty.ts', + ); + + expect(result.success).toBe(true); + if (result.success) { + expect(result.data).toHaveLength(0); + } + }); + + it('should return empty data for empty array return', async () => { + const result = await createGeometry( + { + 'empty-arr.ts': ` + import { BuerliCadFacade } from '@buerli.io/classcad'; + export default function main() { return []; } + `, + }, + 'empty-arr.ts', + ); + + expect(result.success).toBe(true); + if (result.success) { + expect(result.data).toHaveLength(0); + } + }); + }); + + // =========================================================================== + // Integration: Error handling + // =========================================================================== + + describe('createGeometry — error handling', () => { + it('should report syntax errors from user code', async () => { + const result = await createGeometry( + { + 'syntax-err.ts': ` + import { BuerliCadFacade } from '@buerli.io/classcad'; + export default function main( { + `, + }, + 'syntax-err.ts', + ); + + expect(result.success).toBe(false); + }); + + it('should report runtime errors with structured issues', async () => { + const result = await createGeometry( + { + 'runtime-err.ts': ` + import { BuerliCadFacade } from '@buerli.io/classcad'; + + export default function main() { + throw new Error('ClassCAD: invalid solid operation'); + } + `, + }, + 'runtime-err.ts', + ); + + expect(result.success).toBe(false); + if (!result.success) { + expect(result.issues.some((i) => i.message.includes('invalid solid operation'))).toBe(true); + } + }); + + it('should report type errors from bad classcad usage', async () => { + const result = await createGeometry( + { + 'type-err.ts': ` + import { BuerliCadFacade } from '@buerli.io/classcad'; + + export default function main() { + const x = null; + return x.someProperty; + } + `, + }, + 'type-err.ts', + ); + + expect(result.success).toBe(false); + }); + }); + + // =========================================================================== + // Integration: Export pipeline — createGeometry → nativeHandle → export + // =========================================================================== + + describe('export pipeline', () => { + it('should produce exportable geometry result', async () => { + const result = await createGeometry( + { + 'export-test.ts': ` + import { BuerliCadFacade } from '@buerli.io/classcad'; + + export default function main() { + return [{ position: ${trianglePositions(10)} }]; + } + `, + }, + 'export-test.ts', + ); + + expect(result.success).toBe(true); + await geometryHelpers.expectValidGltf(result); + + if (result.success) { + expect(result.data).toHaveLength(1); + expect(result.data[0]!.format).toBe('gltf'); + expect(result.data[0]!.content).toBeInstanceOf(Uint8Array); + expect(result.data[0]!.content.length).toBeGreaterThan(0); + } + }); + }); + + // =========================================================================== + // Integration: Module resolution — classcad import patterns + // =========================================================================== + + describe('module resolution', () => { + it('should resolve named imports from @buerli.io/classcad', async () => { + const result = await createGeometry( + { + 'named-import.ts': ` + import { BuerliCadFacade, init, WASMClient, BooleanOperationType } from '@buerli.io/classcad'; + + export default function main() { + // Verify all imports resolved + const checks = [ + typeof BuerliCadFacade === 'function', + typeof init === 'function', + typeof WASMClient === 'function', + ]; + if (checks.every(Boolean)) { + return [{ position: ${trianglePositions(5)} }]; + } + throw new Error('Import resolution failed'); + } + `, + }, + 'named-import.ts', + ); + + expect(result.success).toBe(true); + await geometryHelpers.expectValidGltf(result); + }); + + it('should resolve default import from @buerli.io/classcad', async () => { + const result = await createGeometry( + { + 'default-import.ts': ` + import classcad from '@buerli.io/classcad'; + + export default function main() { + if (classcad) { + return [{ position: ${trianglePositions(5)} }]; + } + throw new Error('Default import failed'); + } + `, + }, + 'default-import.ts', + ); + + expect(result.success).toBe(true); + await geometryHelpers.expectValidGltf(result); + }); + }); +}); diff --git a/packages/runtime/src/kernels/buerli/buerli.kernel.ts b/packages/runtime/src/kernels/buerli/buerli.kernel.ts new file mode 100644 index 000000000..38392e415 --- /dev/null +++ b/packages/runtime/src/kernels/buerli/buerli.kernel.ts @@ -0,0 +1,453 @@ +/** + * Buerli (ClassCAD) Kernel Module + * + * Integrates the ClassCAD WASM CAD kernel via @buerli.io/classcad into + * Tau's kernel framework. Uses the WASM variant exclusively — no WebSocket + * connections. Requires a ClassCAD API key for WASM initialization. + * + * The kernel registers `@buerli.io/classcad` as a built-in module so that + * user code can `import { ... } from '@buerli.io/classcad'` and access the + * Solid, Part, Assembly, Sketch, and Curve APIs. + */ + +import type { Document as GltfDocument, Scene as GltfScene, Buffer as GltfBuffer } from '@gltf-transform/core'; +import { jsonSchemaFromJson } from '@taucad/utils/schema'; +import { createExportFile } from '@taucad/types/constants'; +import { asBuffer } from '@taucad/utils/file'; +import { z } from 'zod'; +import type { KernelIssue } from '#types/runtime.types.js'; +import type { KernelRuntime } from '#types/runtime-kernel.types.js'; +import { defineKernel } from '#types/runtime-kernel.types.js'; +import { createKernelError, createKernelSuccess } from '#kernels/kernel-helpers.js'; +import { parseStackTrace, resolveSourcePath, deriveLocationFromFrames } from '#framework/error-enrichment.js'; + +// ============================================================================= +// Types +// ============================================================================= + +type RuntimeModuleExports = { + default?: (...args: unknown[]) => unknown; + main?: (...args: unknown[]) => unknown; + defaultParams?: Record; + defaultParameters?: Record; +}; + +const kernelModulesKey = '__KERNEL_MODULES__'; +const buerliModuleVersion = '1.0.1'; + +// ============================================================================= +// Path helpers +// ============================================================================= + +function resolveToRelative(absolutePath: string, basePath: string): string { + const normalizedBase = basePath.endsWith('/') ? basePath.slice(0, -1) : basePath; + if (absolutePath.startsWith(`${normalizedBase}/`)) { + return absolutePath.slice(normalizedBase.length + 1); + } + + return absolutePath; +} + +// ============================================================================= +// Module registration helpers +// ============================================================================= + +function getModuleRegistry(): Map> { + let registry = (globalThis as Record)[kernelModulesKey] as + | Map> + | undefined; + if (!registry) { + registry = new Map(); + (globalThis as Record)[kernelModulesKey] = registry; + } + + return registry; +} + +function generateModuleShim(name: string, exports: Record): string { + const registry = getModuleRegistry(); + registry.set(name, exports); + + const exportNames = Object.keys(exports).filter((key) => /^[$_a-z][\w$]*$/i.test(key) && key !== 'default'); + const namedExports = exportNames.map((key) => `export const ${key} = __mod.${key};`).join('\n'); + return `const __mod = globalThis.${kernelModulesKey}.get('${name}');\n${namedExports}\nexport default __mod;\n`; +} + +async function registerBuerliModules(runtime: KernelRuntime): Promise> { + const classcadModule = (await import('@buerli.io/classcad')) as Record; + + runtime.bundler.registerModule('@buerli.io/classcad', { + code: generateModuleShim('@buerli.io/classcad', classcadModule), + version: buerliModuleVersion, + globalName: 'buerliClasscad', + }); + + return classcadModule; +} + +// ============================================================================= +// Module execution helpers +// ============================================================================= + +function isRecordObject(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} + +function resolveModule(module: unknown): RuntimeModuleExports { + const module_ = module as RuntimeModuleExports; + if (module_.default && typeof module_.default !== 'function' && isRecordObject(module_.default)) { + const inner = module_.default as RuntimeModuleExports; + if (typeof inner.default === 'function' || typeof inner.main === 'function') { + return inner; + } + } + + return module_; +} + +function extractDefaultParameters(module: unknown): Record { + if (!isRecordObject(module)) { + return {}; + } + + /* oxlint-disable @typescript-eslint/no-unnecessary-condition -- runtime guard for untyped module */ + return ( + (module['defaultParams'] as Record) ?? + (module['defaultParameters'] as Record) ?? + {} + ); + /* oxlint-enable @typescript-eslint/no-unnecessary-condition -- end of runtime guard */ +} + +async function runMain(module: RuntimeModuleExports, parameters: Record): Promise { + const defaultExport = module.default ?? module.main; + if (!defaultExport) { + return undefined; + } + + if (typeof defaultExport !== 'function') { + return defaultExport; + } + + return defaultExport(parameters); +} + +function enrichIssueLocation(issues: KernelIssue[], fallbackFileName: string): KernelIssue[] { + return issues.map((issue) => ({ + ...issue, + location: issue.location ?? { + fileName: fallbackFileName, + startLineNumber: 1, + startColumn: 1, + }, + })); +} + +// ============================================================================= +// Geometry conversion +// ============================================================================= + +type GltfDocumentContext = { + document: GltfDocument; + scene: GltfScene; + buffer: GltfBuffer; +}; + +function extractPositionArray(geom: Record): number[] | undefined { + const geomData = geom['data'] as Record | undefined; + const attributes = geomData?.['attributes'] as Record | undefined; + if (!attributes?.['position']) { + return undefined; + } + + const posData = attributes['position'] as Record; + const array = posData['array'] as number[] | undefined; + return array && array.length > 0 ? array : undefined; +} + +function addPositionGeometry(context: GltfDocumentContext, positions: Float32Array, indices?: Uint32Array): void { + const accessor = context.document + .createAccessor('position') + .setArray(positions) + .setType('VEC3') + .setBuffer(context.buffer); + const primitive = context.document.createPrimitive().setAttribute('POSITION', accessor); + if (indices) { + const indexAccessor = context.document + .createAccessor('index') + .setArray(new Uint32Array(indices)) + .setType('SCALAR') + .setBuffer(context.buffer); + primitive.setIndices(indexAccessor); + } + + const mesh = context.document.createMesh('buerli-mesh').addPrimitive(primitive); + const node = context.document.createNode('buerli-node').setMesh(mesh); + context.scene.addChild(node); +} + +/** + * Convert buerli output to GLB binary. + * + * Buerli's `createBufferGeometry` returns Three.js BufferGeometry objects. + * We extract the position attribute and convert to a minimal glTF binary. + * If user code returns raw ArrayBuffer/typed array data, it is passed through. + * + * @param output - geometry output from user's main() function + * @returns GLB binary data + */ +async function convertBuerliOutputToGlb(output: unknown): Promise> { + if (output instanceof ArrayBuffer || ArrayBuffer.isView(output)) { + return new Uint8Array(output instanceof ArrayBuffer ? output : output.buffer) as Uint8Array; + } + + const gltfCore = await import('@gltf-transform/core'); + const document = new gltfCore.Document(); + const context: GltfDocumentContext = { + document, + scene: document.createScene('BuerliScene'), + buffer: document.createBuffer('geometry'), + }; + + if (isRecordObject(output) && 'toJSON' in output && typeof output['toJSON'] === 'function') { + const json = (output['toJSON'] as () => Record)(); + const geometries = json['geometries'] as Array> | undefined; + if (json['metadata'] && geometries) { + for (const geom of geometries) { + const array = extractPositionArray(geom); + if (array) { + addPositionGeometry(context, new Float32Array(array)); + } + } + } + } + + if (Array.isArray(output)) { + for (const item of output) { + if (!isRecordObject(item)) { + continue; + } + + const posArray = item['position'] as Float32Array | undefined; + if (!posArray) { + continue; + } + + const indexArray = item['index'] as Uint32Array | undefined; + addPositionGeometry(context, new Float32Array(posArray), indexArray); + } + } + + const io = new gltfCore.NodeIO(); + return io.writeBinary(document); +} + +// ============================================================================= +// Options schema +// ============================================================================= + +/** + * Configuration for the Buerli (ClassCAD) kernel. + * The ClassCAD WASM API key is required for WASM engine initialization. + * When omitted, the kernel registers modules but defers WASM init. + * @public + */ +export type BuerliOptions = { + /** ClassCAD WASM API key for license validation. */ + classcadKey?: string; +}; + +const optionsSchema = z.object({ + classcadKey: z.string().optional(), +}) satisfies z.ZodType; + +// ============================================================================= +// Kernel module definition +// ============================================================================= + +/** @public */ +export default defineKernel({ + name: 'BuerliKernel', + version: '1.0.0', + optionsSchema, + + async initialize(options, runtime) { + const classcadModule = await registerBuerliModules(runtime); + + if (options.classcadKey) { + const initFunction = classcadModule['init'] as ((factory: (drawingId: unknown) => unknown) => void) | undefined; + // eslint-disable-next-line @typescript-eslint/naming-convention -- WASMClient is the upstream class name + const WASMClient = classcadModule['WASMClient'] as + | (new (drawingId: unknown, config: { classcadKey: string }) => unknown) + | undefined; + + if (initFunction && WASMClient) { + const key = options.classcadKey; + initFunction((drawingId: unknown) => new WASMClient(drawingId, { classcadKey: key })); + } + + runtime.logger.debug('Initialized Buerli (ClassCAD) kernel with WASM client'); + } else { + runtime.logger.debug('Buerli kernel registered without WASM init (no classcadKey)'); + } + + return { classcadModule, classcadKey: options.classcadKey }; + }, + + async getDependencies({ filePath }, runtime) { + return runtime.bundler.resolveDependencies(filePath); + }, + + async getParameters({ filePath, basePath }, runtime) { + const relativeFilePath = resolveToRelative(filePath, basePath); + + try { + const bundleResult = await runtime.bundler.bundle(filePath); + if (!bundleResult.success) { + return createKernelError(enrichIssueLocation(bundleResult.issues, relativeFilePath)); + } + + const executeResult = await runtime.execute(bundleResult.code); + if (!executeResult.success) { + return createKernelError(enrichIssueLocation(executeResult.issues, relativeFilePath)); + } + + const rawModule = executeResult.value as RuntimeModuleExports; + const module = resolveModule(rawModule); + const defaultParameters = extractDefaultParameters(module); + const jsonSchema = await jsonSchemaFromJson(defaultParameters); + + return createKernelSuccess({ defaultParameters, jsonSchema }); + } catch (error) { + return createKernelError([ + { + message: error instanceof Error ? error.message : 'Failed to extract parameters', + location: { + fileName: relativeFilePath, + startLineNumber: 1, + startColumn: 1, + }, + type: 'runtime', + severity: 'error', + }, + ]); + } + }, + + async createGeometry({ filePath, basePath, parameters }, runtime, _context) { + const relativeFilePath = resolveToRelative(filePath, basePath); + + const bundleResult = await runtime.bundler.bundle(filePath); + if (!bundleResult.success) { + throw new BuerliBuildError(enrichIssueLocation(bundleResult.issues, relativeFilePath)); + } + + const executeResult = await runtime.execute(bundleResult.code); + if (!executeResult.success) { + throw new BuerliBuildError(enrichIssueLocation(executeResult.issues, relativeFilePath)); + } + + const rawModule = executeResult.value as RuntimeModuleExports; + const module = resolveModule(rawModule); + + let model: unknown; + try { + model = await runMain(module, parameters); + } catch (error) { + const stackFrames = parseStackTrace(error, { + sourceMap: bundleResult.sourceMap, + resolveSourcePath: (sourcePath) => resolveSourcePath(sourcePath, basePath), + }); + const location = deriveLocationFromFrames(stackFrames, bundleResult.sourceMap, (sourcePath) => + resolveSourcePath(sourcePath, basePath), + ); + throw new BuerliBuildError([ + { + message: error instanceof Error ? error.message : String(error), + type: 'runtime', + severity: 'error', + stackFrames, + location, + }, + ]); + } + + if (model === undefined || (Array.isArray(model) && model.length === 0)) { + return { + geometry: [], + nativeHandle: undefined, + issues: [ + { + message: 'main() did not return any geometry. Export a default function that returns ClassCAD geometry.', + location: { + fileName: relativeFilePath, + startLineNumber: 1, + startColumn: 1, + }, + type: 'runtime', + severity: 'warning', + }, + ], + }; + } + + try { + const glb = await convertBuerliOutputToGlb(model); + return { + geometry: [{ format: 'gltf', content: glb }], + nativeHandle: { glb }, + }; + } catch (error) { + const stackFrames = parseStackTrace(error, { + sourceMap: bundleResult.sourceMap, + resolveSourcePath: (sourcePath) => resolveSourcePath(sourcePath, basePath), + }); + const location = deriveLocationFromFrames(stackFrames, bundleResult.sourceMap, (sourcePath) => + resolveSourcePath(sourcePath, basePath), + ); + throw new BuerliBuildError([ + { + message: error instanceof Error ? error.message : String(error), + type: 'runtime', + severity: 'error', + stackFrames, + location, + }, + ]); + } + }, + + async exportGeometry({ fileType, nativeHandle }) { + if (!nativeHandle) { + return createKernelError([ + { + message: 'No geometry available for export.', + type: 'runtime', + severity: 'error', + }, + ]); + } + + if (fileType === 'glb' || fileType === 'gltf') { + return createKernelSuccess([ + createExportFile(fileType, fileType === 'glb' ? 'model.glb' : 'model.gltf', asBuffer(nativeHandle.glb)), + ]); + } + + return createKernelError([ + { + message: `Export format '${fileType}' is not implemented for Buerli. Only 'glb' and 'gltf' are supported.`, + type: 'runtime', + severity: 'error', + }, + ]); + }, +}); + +class BuerliBuildError extends Error { + public readonly issues: KernelIssue[]; + public constructor(issues: KernelIssue[]) { + super(issues.map((issue) => issue.message).join('; ')); + this.issues = issues; + } +} diff --git a/packages/runtime/src/kernels/buerli/buerli.plugin.ts b/packages/runtime/src/kernels/buerli/buerli.plugin.ts new file mode 100644 index 000000000..6c6398757 --- /dev/null +++ b/packages/runtime/src/kernels/buerli/buerli.plugin.ts @@ -0,0 +1,32 @@ +/** + * Buerli (ClassCAD) kernel plugin registration. + * + * Encapsulates all kernel metadata: id, extensions, detect pattern, + * builtin module names, and module URL resolution. + * Uses WASM-only variant — no WebSocket connections. + */ + +import { createKernelPlugin } from '#plugins/plugin-helpers.js'; +import type { BuerliOptions } from '#kernels/buerli/buerli.kernel.js'; + +/** + * Canonical regex for detecting @buerli.io/classcad usage in source code. + * + * Branches: ESM import, CJS require, dynamic import(). + * @public + */ +export const buerliDetectPattern = + /import\s+.*from\s+["']@buerli\.io\/classcad["']|require\s*\(\s*["']@buerli\.io\/classcad["']\s*\)|import\s*\(\s*["']@buerli\.io\/classcad["']\s*\)/; + +/** + * Create a Buerli (ClassCAD) kernel plugin registration. + * + * @public + */ +export const buerli = createKernelPlugin({ + id: 'buerli', + moduleUrl: new URL('buerli.kernel.js', import.meta.url).href, + extensions: ['ts', 'js'], + detectImport: buerliDetectPattern, + builtinModuleNames: ['@buerli.io/classcad'], +}); diff --git a/packages/runtime/src/plugins/kernel-factories.ts b/packages/runtime/src/plugins/kernel-factories.ts index 78b4e2a16..daadc01a7 100644 --- a/packages/runtime/src/plugins/kernel-factories.ts +++ b/packages/runtime/src/plugins/kernel-factories.ts @@ -14,3 +14,4 @@ export { openscad } from '#kernels/openscad/openscad.plugin.js'; export { jscad } from '#kernels/jscad/jscad.plugin.js'; export { manifold } from '#kernels/manifold/manifold.plugin.js'; export { tau } from '#kernels/tau/tau.plugin.js'; +export { buerli } from '#kernels/buerli/buerli.plugin.js'; diff --git a/packages/runtime/src/plugins/kernels-entry.ts b/packages/runtime/src/plugins/kernels-entry.ts index f69097ceb..7f7e0c8a4 100644 --- a/packages/runtime/src/plugins/kernels-entry.ts +++ b/packages/runtime/src/plugins/kernels-entry.ts @@ -1,6 +1,7 @@ /* oxlint-disable no-barrel-files/no-barrel-files -- package entry file */ -export { replicad, opencascade, zoo, openscad, jscad, manifold, tau } from '#plugins/kernel-factories.js'; +export { replicad, opencascade, zoo, openscad, jscad, manifold, tau, buerli } from '#plugins/kernel-factories.js'; export type { ReplicadOptions, ReplicadWasmConfig } from '#kernels/replicad/replicad.kernel.js'; export type { OpenCascadeOptions, OpenCascadeWasmConfig } from '#kernels/opencascade/opencascade.kernel.js'; export type { ZooOptions } from '#kernels/zoo/zoo.kernel.js'; export type { ManifoldOptions } from '#kernels/manifold/manifold.kernel.js'; +export type { BuerliOptions } from '#kernels/buerli/buerli.kernel.js'; diff --git a/packages/runtime/src/plugins/presets.ts b/packages/runtime/src/plugins/presets.ts index 358f0989d..4528373ea 100644 --- a/packages/runtime/src/plugins/presets.ts +++ b/packages/runtime/src/plugins/presets.ts @@ -3,7 +3,7 @@ */ import type { KernelPlugin, MiddlewarePlugin, BundlerPlugin } from '#plugins/plugin-types.js'; -import { replicad, opencascade, zoo, openscad, jscad, manifold, tau } from '#plugins/kernel-factories.js'; +import { replicad, opencascade, zoo, openscad, jscad, manifold, tau, buerli } from '#plugins/kernel-factories.js'; import { parameterCache, geometryCache, @@ -47,7 +47,7 @@ export const presets = { */ all(): PresetOptions { return { - kernels: [openscad(), zoo(), replicad(), opencascade(), manifold(), jscad(), tau()], + kernels: [openscad(), zoo(), replicad(), opencascade(), manifold(), jscad(), buerli(), tau()], middleware: [parameterCache(), geometryCache(), gltfCoordinateTransform(), gltfEdgeDetection()], bundlers: [esbuild()], }; diff --git a/packages/runtime/src/testing/smoke-esm.test.ts b/packages/runtime/src/testing/smoke-esm.test.ts index 1c524db15..d16af0dd7 100644 --- a/packages/runtime/src/testing/smoke-esm.test.ts +++ b/packages/runtime/src/testing/smoke-esm.test.ts @@ -54,6 +54,9 @@ describe('ESM import smoke tests', () => { const opencascadeModule = await import('#kernels/opencascade/opencascade.kernel.js'); expect(opencascadeModule.default).toBeDefined(); + + const buerliModule = await import('#kernels/buerli/buerli.kernel.js'); + expect(buerliModule.default).toBeDefined(); }); it('should resolve the bundler module', async () => { diff --git a/packages/runtime/tsdown.config.ts b/packages/runtime/tsdown.config.ts index b895cfa90..f090bdfef 100644 --- a/packages/runtime/tsdown.config.ts +++ b/packages/runtime/tsdown.config.ts @@ -14,6 +14,7 @@ const baseConfig: Options = { 'src/kernels/zoo/zoo.kernel.ts', 'src/kernels/zoo/engine-connection.ts', 'src/kernels/tau/tau.kernel.ts', + 'src/kernels/buerli/buerli.kernel.ts', 'src/bundler/esbuild.bundler.ts', 'src/middleware/parameter-cache.middleware.ts', 'src/middleware/geometry-cache.middleware.ts', diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 44d7392e7..0ccc2631f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,6 +12,9 @@ catalogs: '@arethetypeswrong/cli': specifier: ^0.18.2 version: 0.18.2 + '@buerli.io/classcad': + specifier: ^1.0.1 + version: 1.0.1 '@eslint-community/eslint-plugin-eslint-comments': specifier: ^4.7.1 version: 4.7.1 @@ -1581,6 +1584,9 @@ importers: packages/runtime: dependencies: + '@buerli.io/classcad': + specifier: 'catalog:' + version: 1.0.1(@types/react@19.0.12)(immer@11.1.4)(react@19.2.4)(three@0.179.1) '@gltf-transform/core': specifier: 'catalog:' version: 4.3.0 @@ -1595,7 +1601,7 @@ importers: version: 2.13.0 '@kittycad/lib': specifier: 'catalog:' - version: 2.0.48(patch_hash=f8eba32a9a9fffe5f056ba8a043de4a9c865c57b7a8bb8eecfb706fa4865ffe8)(@swc/core@1.15.8(@swc/helpers@0.5.19))(@types/node@25.3.0)(typescript@5.9.3) + version: 2.0.48(patch_hash=f8eba32a9a9fffe5f056ba8a043de4a9c865c57b7a8bb8eecfb706fa4865ffe8)(@swc/core@1.15.8(@swc/helpers@0.5.19))(@types/node@24.0.15)(typescript@5.9.3) '@msgpack/msgpack': specifier: 'catalog:' version: 3.1.3 @@ -1664,7 +1670,7 @@ importers: version: 0.20201231.0 vitest: specifier: 4.1.0-beta.6 - version: 4.1.0-beta.6(@opentelemetry/api@1.9.0)(@types/node@25.3.0)(@vitest/ui@4.1.0-beta.6)(happy-dom@16.8.1)(jsdom@26.0.0(canvas@3.2.1))(vite@8.0.0-beta.16(@types/node@25.3.0)(esbuild@0.27.3)(jiti@2.6.1)(less@4.5.1)(sass-embedded@1.97.3)(sass@1.97.3)(stylus@0.64.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) + version: 4.1.0-beta.6(@opentelemetry/api@1.9.0)(@types/node@24.0.15)(@vitest/ui@4.1.0-beta.6)(happy-dom@16.8.1)(jsdom@26.0.0(canvas@3.2.1))(vite@8.0.0-beta.16(@types/node@24.0.15)(esbuild@0.27.3)(jiti@2.6.1)(less@4.5.1)(sass-embedded@1.97.3)(sass@1.97.3)(stylus@0.64.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) vitest-mock-extended: specifier: 'catalog:' version: 3.1.0(typescript@5.9.3)(vitest@4.1.0-beta.6) @@ -2566,6 +2572,14 @@ packages: openai: ^4.62.1 zod: ^3.23.8 + '@buerli.io/classcad@1.0.1': + resolution: {integrity: sha512-EQC3lU9CxjUdSbvDyZ7oWD+2RSCAsIUSDMySXEliMaTrtTCZxZa+rxr+OLB+VmELoTOIeoV5XI1pWtZpnih5Kg==} + + '@buerli.io/core@1.0.1': + resolution: {integrity: sha512-vTTNxLgY0OQDR949Wku837b5y5OQPoqH7vGWI3nSm/cJ+IUYh5ExRQXFCwKGZrjgDI/EgL1lQFawdTtJ2ZI67A==} + peerDependencies: + three: '>=0.138' + '@bufbuild/protobuf@2.11.0': resolution: {integrity: sha512-sBXGT13cpmPR5BMgHE6UEEfEaShh5Ror6rfN3yEK5si7QVrtZg8LEPQb0VVhiLRUslD2yLnXtnRzG035J/mZXQ==} @@ -2599,6 +2613,10 @@ packages: '@chevrotain/utils@11.1.1': resolution: {integrity: sha512-71eTYMzYXYSFPrbg/ZwftSaSDld7UYlS8OQa3lNnn9jzNtpFbaReRRyghzqS7rI3CDaorqpPJJcXGHK+FE1TVQ==} + '@classcad/api-js@https://awvstatic.com/classcad/download/release/21.0.0/classcad-api-js-21.0.0.tgz': + resolution: {integrity: sha512-HEgz4BpGvdrlv2byZ4ZMxwI/PjJLQD8pfeadCi540Vwaijpg7Zi5Votr1W4smXy4rJo7MCg0on7DoK0fRaS5iA==, tarball: https://awvstatic.com/classcad/download/release/21.0.0/classcad-api-js-21.0.0.tgz} + version: 21.0.0 + '@colors/colors@1.5.0': resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} engines: {node: '>=0.1.90'} @@ -4460,7 +4478,7 @@ packages: engines: {node: '>=20'} '@langchain/google-common@https://github.com/taucad/langchainjs/raw/0f065cb8c363ff95a2fdddfbe28bbac1970068cd/langchain-google-common-2.1.32.tgz': - resolution: {tarball: https://github.com/taucad/langchainjs/raw/0f065cb8c363ff95a2fdddfbe28bbac1970068cd/langchain-google-common-2.1.32.tgz} + resolution: {integrity: sha512-lF6ZU5y+Z0XvYQKccoJWbut+bcICBhMcvDh0xs4N2zYPL3yLUB1mPxhzUqJGowkr0P12tuKVHFMsSlaWbuL+NQ==, tarball: https://github.com/taucad/langchainjs/raw/0f065cb8c363ff95a2fdddfbe28bbac1970068cd/langchain-google-common-2.1.32.tgz} version: 2.1.32 engines: {node: '>=20'} peerDependencies: @@ -5553,6 +5571,7 @@ packages: '@opentelemetry/instrumentation-fastify@0.57.0': resolution: {integrity: sha512-D+rwRtbiOediYocpKGvY/RQTpuLsLdCVwaOREyqWViwItJGibWI7O/wgd9xIV63pMP0D9IdSy27wnARfUaotKg==} engines: {node: ^18.19.0 || >=20.6.0} + deprecated: Deprecated in favor of @fastify/otel, maintained by the Fastify authors. peerDependencies: '@opentelemetry/api': ^1.3.0 @@ -9229,6 +9248,9 @@ packages: '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/events@3.0.3': + resolution: {integrity: sha512-trOc4AAUThEz9hapPtSd7wf5tiQKvTtu5b371UxXdTuqzIh0ArcRspRP0i0Viu+LXstIQ1z96t1nsPxT9ol01g==} + '@types/express-serve-static-core@4.19.8': resolution: {integrity: sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA==} @@ -9855,6 +9877,7 @@ packages: '@xmldom/xmldom@0.8.11': resolution: {integrity: sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==} engines: {node: '>=10.0.0'} + deprecated: this version has critical issues, please update to the latest version '@xstate/react@5.0.5': resolution: {integrity: sha512-MfF/cPHa3lNKJmGFpUycMbNP25qBXyZXrxc8VYNroAu0Nnk0DV5WzAkTcQXma0xEC4dSwsoA+YQuKbZATtqvgg==} @@ -10315,6 +10338,10 @@ packages: resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} engines: {node: 18 || 20 || >=22} + base64-arraybuffer@0.2.0: + resolution: {integrity: sha512-7emyCsu1/xiBXgQZrscw/8KPRT44I4Yq9Pe6EGs3aPRTsWuggML1/1DTuZUuIaJPIm1FTDUVXl4x/yW8s0kQDQ==} + engines: {node: '>= 0.6.0'} + base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} @@ -12411,6 +12438,9 @@ packages: resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} engines: {node: '>=8.6.0'} + fast-json-patch@3.1.1: + resolution: {integrity: sha512-vf6IHUX2SBcA+5/+4883dsIjpBTqmfBjmYiWK1savxQmFk4JfBMLa7ynTYOs1Rolp/T1betJxHiGD3g1Mn8lUQ==} + fast-json-stable-stringify@2.1.0: resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} @@ -12576,6 +12606,10 @@ packages: resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} engines: {node: '>=16'} + flat-promise@1.0.3: + resolution: {integrity: sha512-ncD6/40peBxXngM/F+3fYyL8sYfXyQnggGxZ2Chr9mnTQco77hKpZeT3EUH298Bjq8224qIxEGlHN10rx8Mv2A==} + engines: {node: '>= 6.4'} + flat@5.0.2: resolution: {integrity: sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==} hasBin: true @@ -13945,6 +13979,9 @@ packages: resolution: {integrity: sha512-/c+n06zvqFQGxdz1BbElF7S3nEghjNchLN1TjQnk2j10HYDaUc57rcvl6BbnziTx8NQmrg0JOs/iwRpvcYaxjQ==} engines: {node: '>=18.20'} + js-untar@2.0.0: + resolution: {integrity: sha512-7CsDLrYQMbLxDt2zl9uKaPZSdmJMvGGQ7wo9hoB3J+z/VcO2w63bXFgHVnjF1+S9wD3zAu8FBVj7EYWjTQ3Z7g==} + js-yaml@3.14.2: resolution: {integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==} hasBin: true @@ -14049,6 +14086,10 @@ packages: jsonfile@6.2.0: resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==} + jsonpatch@3.1.0: + resolution: {integrity: sha512-rJa0FepwDzebgYAuT389AtziWfn3EZA4N5e/B8d50IC1glqlZ7njo7iAI/ph5MYw2GGH7lCE6DGqBGQ7TVa06w==} + engines: {node: '>=0.4.0'} + jsonpointer@5.0.1: resolution: {integrity: sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==} engines: {node: '>=0.10.0'} @@ -17752,6 +17793,9 @@ packages: resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} engines: {node: '>=10.0.0'} + strict-event-emitter-types@2.0.0: + resolution: {integrity: sha512-Nk/brWYpD85WlOgzw5h173aci0Teyv8YdIAEtV+N88nDB0dLlazZyJMIsN6eo1/AR61l+p6CJTG1JIyFaoNEEA==} + string-argv@0.3.2: resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} engines: {node: '>=0.6.19'} @@ -18619,6 +18663,10 @@ packages: unrs-resolver@1.11.1: resolution: {integrity: sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==} + unzipit@1.4.3: + resolution: {integrity: sha512-gsq2PdJIWWGhx5kcdWStvNWit9FVdTewm4SEG7gFskWs+XCVaULt9+BwuoBtJiRE8eo3L1IPAOrbByNLtLtIlg==} + engines: {node: '>=12'} + upath@2.0.1: resolution: {integrity: sha512-1uEe95xksV1O0CYKXo8vQvN1JEbtJp7lb7C5U9HMsIp6IVwntkH/oNUzyVNQSd4S1sYk2FpSSW44FqMc8qee5w==} engines: {node: '>=4'} @@ -18722,6 +18770,9 @@ packages: engines: {node: '>=8'} hasBin: true + uzip-module@1.0.3: + resolution: {integrity: sha512-AMqwWZaknLM77G+VPYNZLEruMGWGzyigPK3/Whg99B3S6vGHuqsyl5ZrOv1UUF3paGK1U6PM0cnayioaryg/fA==} + uzip@0.20201231.0: resolution: {integrity: sha512-OZeJfZP+R0z9D6TmBgLq2LHzSSptGMGDGigGiEe0pr8UBe/7fdflgHlHBNDASTXB5jnFuxHpNaJywSg8YFeGng==} @@ -20491,6 +20542,45 @@ snapshots: - encoding - utf-8-validate + '@buerli.io/classcad@1.0.1(@types/react@19.0.12)(immer@11.1.4)(react@19.2.4)(three@0.179.1)': + dependencies: + '@buerli.io/core': 1.0.1(@types/react@19.0.12)(immer@11.1.4)(react@19.2.4)(three@0.179.1) + '@classcad/api-js': https://awvstatic.com/classcad/download/release/21.0.0/classcad-api-js-21.0.0.tgz + '@types/events': 3.0.3 + base64-arraybuffer: 0.2.0 + comlink: 4.4.2 + events: 3.3.0 + flat-promise: 1.0.3 + js-untar: 2.0.0 + pako: 2.1.0 + path-browserify: 1.0.1 + socket.io-client: 4.8.3 + strict-event-emitter-types: 2.0.0 + unzipit: 1.4.3 + uuid: 9.0.1 + xmlhttprequest-ssl: 2.1.2 + transitivePeerDependencies: + - '@types/react' + - bufferutil + - immer + - react + - supports-color + - three + - utf-8-validate + + '@buerli.io/core@1.0.1(@types/react@19.0.12)(immer@11.1.4)(react@19.2.4)(three@0.179.1)': + dependencies: + '@classcad/api-js': https://awvstatic.com/classcad/download/release/21.0.0/classcad-api-js-21.0.0.tgz + fast-json-patch: 3.1.1 + jsonpatch: 3.1.0 + three: 0.179.1 + uuid: 9.0.1 + zustand: 4.5.7(@types/react@19.0.12)(immer@11.1.4)(react@19.2.4) + transitivePeerDependencies: + - '@types/react' + - immer + - react + '@bufbuild/protobuf@2.11.0': {} '@captchafox/react@1.10.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': @@ -20532,6 +20622,8 @@ snapshots: '@chevrotain/utils@11.1.1': {} + '@classcad/api-js@https://awvstatic.com/classcad/download/release/21.0.0/classcad-api-js-21.0.0.tgz': {} + '@colors/colors@1.5.0': optional: true @@ -21723,17 +21815,6 @@ snapshots: - '@types/node' - typescript - '@kittycad/lib@2.0.48(patch_hash=f8eba32a9a9fffe5f056ba8a043de4a9c865c57b7a8bb8eecfb706fa4865ffe8)(@swc/core@1.15.8(@swc/helpers@0.5.19))(@types/node@25.3.0)(typescript@5.9.3)': - dependencies: - openapi-types: 12.1.3 - ts-node: 10.9.2(@swc/core@1.15.8(@swc/helpers@0.5.19))(@types/node@25.3.0)(typescript@5.9.3) - tslib: 2.8.1 - transitivePeerDependencies: - - '@swc/core' - - '@swc/wasm' - - '@types/node' - - typescript - '@langchain/anthropic@1.3.20(@langchain/core@1.1.37(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.213.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(openai@4.104.0(encoding@0.1.13)(ws@8.19.0)(zod@4.3.6)))': dependencies: '@anthropic-ai/sdk': 0.74.0(zod@4.3.6) @@ -27185,6 +27266,8 @@ snapshots: '@types/estree@1.0.8': {} + '@types/events@3.0.3': {} + '@types/express-serve-static-core@4.19.8': dependencies: '@types/node': 25.3.0 @@ -27707,14 +27790,6 @@ snapshots: optionalDependencies: vite: 8.0.0-beta.16(@types/node@24.0.15)(esbuild@0.27.3)(jiti@2.6.1)(less@4.5.1)(sass-embedded@1.97.3)(sass@1.97.3)(stylus@0.64.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) - '@vitest/mocker@4.1.0-beta.6(vite@8.0.0-beta.16(@types/node@25.3.0)(esbuild@0.27.3)(jiti@2.6.1)(less@4.5.1)(sass-embedded@1.97.3)(sass@1.97.3)(stylus@0.64.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))': - dependencies: - '@vitest/spy': 4.1.0-beta.6 - estree-walker: 3.0.3 - magic-string: 0.30.21 - optionalDependencies: - vite: 8.0.0-beta.16(@types/node@25.3.0)(esbuild@0.27.3)(jiti@2.6.1)(less@4.5.1)(sass-embedded@1.97.3)(sass@1.97.3)(stylus@0.64.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) - '@vitest/pretty-format@4.1.0-beta.6': dependencies: tinyrainbow: 3.0.3 @@ -27742,7 +27817,7 @@ snapshots: sirv: 3.0.2 tinyglobby: 0.2.15 tinyrainbow: 3.0.3 - vitest: 4.1.0-beta.6(@opentelemetry/api@1.9.0)(@types/node@25.3.0)(@vitest/ui@4.1.0-beta.6)(happy-dom@16.8.1)(jsdom@26.0.0(canvas@3.2.1))(vite@8.0.0-beta.16(@types/node@25.3.0)(esbuild@0.27.3)(jiti@2.6.1)(less@4.5.1)(sass-embedded@1.97.3)(sass@1.97.3)(stylus@0.64.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) + vitest: 4.1.0-beta.6(@opentelemetry/api@1.9.0)(@types/node@24.0.15)(@vitest/ui@4.1.0-beta.6)(happy-dom@16.8.1)(jsdom@26.0.0(canvas@3.2.1))(vite@8.0.0-beta.16(@types/node@24.0.15)(esbuild@0.27.3)(jiti@2.6.1)(less@4.5.1)(sass-embedded@1.97.3)(sass@1.97.3)(stylus@0.64.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) '@vitest/utils@4.1.0-beta.6': dependencies: @@ -28451,6 +28526,8 @@ snapshots: balanced-match@4.0.4: {} + base64-arraybuffer@0.2.0: {} + base64-js@1.5.1: {} base64id@2.0.0: {} @@ -30801,6 +30878,8 @@ snapshots: merge2: 1.4.1 micromatch: 4.0.8 + fast-json-patch@3.1.1: {} + fast-json-stable-stringify@2.1.0: {} fast-json-stringify@6.3.0: @@ -31020,6 +31099,8 @@ snapshots: flatted: 3.3.4 keyv: 4.5.4 + flat-promise@1.0.3: {} + flat@5.0.2: {} flatbush@4.5.0: @@ -31911,7 +31992,7 @@ snapshots: isstream: 0.1.2 jsonwebtoken: 9.0.3 mime-types: 2.1.35 - retry-axios: 2.6.0(axios@1.7.9) + retry-axios: 2.6.0(axios@1.7.9(debug@4.4.3)) tough-cookie: 4.1.4 transitivePeerDependencies: - supports-color @@ -32742,6 +32823,8 @@ snapshots: js-types@4.0.0: {} + js-untar@2.0.0: {} + js-yaml@3.14.2: dependencies: argparse: 1.0.10 @@ -32860,6 +32943,8 @@ snapshots: optionalDependencies: graceful-fs: 4.2.11 + jsonpatch@3.1.0: {} + jsonpointer@5.0.1: {} jsonwebtoken@9.0.3: @@ -36720,7 +36805,7 @@ snapshots: ret@0.5.0: {} - retry-axios@2.6.0(axios@1.7.9): + retry-axios@2.6.0(axios@1.7.9(debug@4.4.3)): dependencies: axios: 1.7.9(debug@4.4.3) @@ -37582,6 +37667,8 @@ snapshots: streamsearch@1.1.0: optional: true + strict-event-emitter-types@2.0.0: {} + string-argv@0.3.2: {} string-hash@1.1.3: {} @@ -38144,26 +38231,6 @@ snapshots: optionalDependencies: '@swc/core': 1.15.8(@swc/helpers@0.5.19) - ts-node@10.9.2(@swc/core@1.15.8(@swc/helpers@0.5.19))(@types/node@25.3.0)(typescript@5.9.3): - dependencies: - '@cspotcode/source-map-support': 0.8.1 - '@tsconfig/node10': 1.0.12 - '@tsconfig/node12': 1.0.11 - '@tsconfig/node14': 1.0.3 - '@tsconfig/node16': 1.0.4 - '@types/node': 25.3.0 - acorn: 8.16.0 - acorn-walk: 8.3.5 - arg: 4.1.3 - create-require: 1.1.1 - diff: 4.0.4 - make-error: 1.3.6 - typescript: 5.9.3 - v8-compile-cache-lib: 3.0.1 - yn: 3.1.1 - optionalDependencies: - '@swc/core': 1.15.8(@swc/helpers@0.5.19) - ts-replace-all@1.0.0: dependencies: core-js: 3.48.0 @@ -38549,6 +38616,10 @@ snapshots: '@unrs/resolver-binding-win32-ia32-msvc': 1.11.1 '@unrs/resolver-binding-win32-x64-msvc': 1.11.1 + unzipit@1.4.3: + dependencies: + uzip-module: 1.0.3 + upath@2.0.1: {} update-browserslist-db@1.2.3(browserslist@4.28.1): @@ -38635,6 +38706,8 @@ snapshots: kleur: 4.1.5 sade: 1.8.1 + uzip-module@1.0.3: {} + uzip@0.20201231.0: {} v8-compile-cache-lib@3.0.1: {} @@ -38901,7 +38974,7 @@ snapshots: dependencies: ts-essentials: 10.1.1(typescript@5.9.3) typescript: 5.9.3 - vitest: 4.1.0-beta.6(@opentelemetry/api@1.9.0)(@types/node@25.3.0)(@vitest/ui@4.1.0-beta.6)(happy-dom@16.8.1)(jsdom@26.0.0(canvas@3.2.1))(vite@8.0.0-beta.16(@types/node@25.3.0)(esbuild@0.27.3)(jiti@2.6.1)(less@4.5.1)(sass-embedded@1.97.3)(sass@1.97.3)(stylus@0.64.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) + vitest: 4.1.0-beta.6(@opentelemetry/api@1.9.0)(@types/node@24.0.15)(@vitest/ui@4.1.0-beta.6)(happy-dom@16.8.1)(jsdom@26.0.0(canvas@3.2.1))(vite@8.0.0-beta.16(@types/node@24.0.15)(esbuild@0.27.3)(jiti@2.6.1)(less@4.5.1)(sass-embedded@1.97.3)(sass@1.97.3)(stylus@0.64.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) vitest@4.1.0-beta.6(@opentelemetry/api@1.9.0)(@types/node@24.0.15)(@vitest/ui@4.1.0-beta.6)(happy-dom@16.8.1)(jsdom@26.0.0(canvas@3.2.1))(vite@8.0.0-beta.16(@types/node@24.0.15)(esbuild@0.27.3)(jiti@2.6.1)(less@4.5.1)(sass-embedded@1.97.3)(sass@1.97.3)(stylus@0.64.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)): dependencies: @@ -38934,37 +39007,6 @@ snapshots: transitivePeerDependencies: - msw - vitest@4.1.0-beta.6(@opentelemetry/api@1.9.0)(@types/node@25.3.0)(@vitest/ui@4.1.0-beta.6)(happy-dom@16.8.1)(jsdom@26.0.0(canvas@3.2.1))(vite@8.0.0-beta.16(@types/node@25.3.0)(esbuild@0.27.3)(jiti@2.6.1)(less@4.5.1)(sass-embedded@1.97.3)(sass@1.97.3)(stylus@0.64.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)): - dependencies: - '@vitest/expect': 4.1.0-beta.6 - '@vitest/mocker': 4.1.0-beta.6(vite@8.0.0-beta.16(@types/node@25.3.0)(esbuild@0.27.3)(jiti@2.6.1)(less@4.5.1)(sass-embedded@1.97.3)(sass@1.97.3)(stylus@0.64.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) - '@vitest/pretty-format': 4.1.0-beta.6 - '@vitest/runner': 4.1.0-beta.6 - '@vitest/snapshot': 4.1.0-beta.6 - '@vitest/spy': 4.1.0-beta.6 - '@vitest/utils': 4.1.0-beta.6 - es-module-lexer: 2.0.0 - expect-type: 1.3.0 - magic-string: 0.30.21 - obug: 2.1.1 - pathe: 2.0.3 - picomatch: 4.0.3 - std-env: 3.10.0 - tinybench: 2.9.0 - tinyexec: 1.0.2 - tinyglobby: 0.2.15 - tinyrainbow: 3.0.3 - vite: 8.0.0-beta.16(@types/node@25.3.0)(esbuild@0.27.3)(jiti@2.6.1)(less@4.5.1)(sass-embedded@1.97.3)(sass@1.97.3)(stylus@0.64.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) - why-is-node-running: 2.3.0 - optionalDependencies: - '@opentelemetry/api': 1.9.0 - '@types/node': 25.3.0 - '@vitest/ui': 4.1.0-beta.6(vitest@4.1.0-beta.6) - happy-dom: 16.8.1 - jsdom: 26.0.0(canvas@3.2.1) - transitivePeerDependencies: - - msw - vscode-jsonrpc@8.2.0: {} vscode-languageserver-protocol@3.17.5: @@ -39402,6 +39444,14 @@ snapshots: immer: 10.2.0 react: 19.2.4 + zustand@4.5.7(@types/react@19.0.12)(immer@11.1.4)(react@19.2.4): + dependencies: + use-sync-external-store: 1.6.0(react@19.2.4) + optionalDependencies: + '@types/react': 19.0.12 + immer: 11.1.4 + react: 19.2.4 + zustand@5.0.11(@types/react@19.0.12)(immer@10.2.0)(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4)): optionalDependencies: '@types/react': 19.0.12 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 1e21ac108..e7e44b570 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -8,6 +8,7 @@ packages: catalog: '@ai-sdk/provider-utils': ^4.0.15 '@arethetypeswrong/cli': ^0.18.2 + '@buerli.io/classcad': ^1.0.1 '@eslint-community/eslint-plugin-eslint-comments': ^4.7.1 '@fastify/otel': ^0.17.1 '@gltf-transform/core': ^4.3.0 diff --git a/repos.yaml b/repos.yaml index faca3df34..c276a3d91 100644 --- a/repos.yaml +++ b/repos.yaml @@ -19,6 +19,7 @@ groups: - bitbybit-occt - brepjs - zalo-opencascade.js + - buerli-examples slicers: description: 3D printer slicer applications repos: @@ -370,3 +371,6 @@ repos: primitives: upstream: radix-ui/primitives description: Radix UI primitives - accessible component library + buerli-examples: + upstream: awv-informatik/buerli-examples + description: Examples that show what buerli and ClassCAD can do