diff --git a/apps/web/.claude/3D_PRINTING_DOCKER.md b/apps/web/.claude/3D_PRINTING_DOCKER.md deleted file mode 100644 index ff0931fd..00000000 --- a/apps/web/.claude/3D_PRINTING_DOCKER.md +++ /dev/null @@ -1,335 +0,0 @@ -# 3D Printing Docker Setup - -## Summary - -The 3D printable abacus customization feature is fully containerized with optimized Docker multi-stage builds. - -**Key Technologies:** - -- OpenSCAD 2021.01 (for rendering STL/3MF from .scad files) -- BOSL2 v2.0.0 (minimized library, .scad files only) -- Typst v0.11.1 (pre-built binary) - -**Image Size:** ~257MB (optimized with multi-stage builds, saved ~38MB) - -**Build Stages:** 7 total (base → builder → deps → typst-builder → bosl2-builder → runner) - -## Overview - -The 3D printable abacus customization feature requires OpenSCAD and the BOSL2 library to be available in the Docker container. - -## Size Optimization Strategy - -The Dockerfile uses **multi-stage builds** to minimize the final image size: - -1. **typst-builder stage** - Downloads and extracts typst, discards wget/xz-utils -2. **bosl2-builder stage** - Clones BOSL2 and removes unnecessary files (tests, docs, examples, images) -3. **runner stage** - Only copies final binaries and minimized libraries - -### Size Reductions - -- **Removed from runner**: git, wget, curl, xz-utils (~40MB) -- **BOSL2 minimized**: Removed .git, tests, tutorials, examples, images, markdown files (~2-3MB savings) -- **Kept only .scad files** in BOSL2 library - -## Dockerfile Changes - -### Build Stages Overview - -The Dockerfile now has **7 stages**: - -1. **base** (Alpine) - Install build tools and dependencies -2. **builder** (Alpine) - Build Next.js application -3. **deps** (Alpine) - Install production node_modules -4. **typst-builder** (Debian) - Download and extract typst binary -5. **bosl2-builder** (Debian) - Clone and minimize BOSL2 library -6. **runner** (Debian) - Final production image - -### Stage 1-3: Base, Builder, Deps (unchanged) - -Uses Alpine Linux for building the application (smaller and faster builds). - -### Stage 4: Typst Builder (lines 68-87) - -```dockerfile -FROM node:18-slim AS typst-builder -RUN apt-get update && apt-get install -y --no-install-recommends \ - wget \ - xz-utils \ - ca-certificates \ - && rm -rf /var/lib/apt/lists/* - -RUN ARCH=$(uname -m) && \ - ... download and install typst from GitHub releases -``` - -**Purpose:** Download typst binary in isolation, then discard build tools (wget, xz-utils). - -**Result:** Only the typst binary is copied to runner stage (line 120). - -### Stage 5: BOSL2 Builder (lines 90-103) - -```dockerfile -FROM node:18-slim AS bosl2-builder -RUN apt-get update && apt-get install -y --no-install-recommends \ - git \ - ca-certificates \ - && rm -rf /var/lib/apt/lists/* - -RUN mkdir -p /bosl2 && \ - cd /bosl2 && \ - git clone --depth 1 --branch v2.0.0 https://github.com/BelfrySCAD/BOSL2.git . && \ - # Remove unnecessary files to minimize size - rm -rf .git .github tests tutorials examples images *.md CONTRIBUTING* LICENSE* && \ - # Keep only .scad files and essential directories - find . -type f ! -name "*.scad" -delete && \ - find . -type d -empty -delete -``` - -**Purpose:** Clone BOSL2 and aggressively minimize by removing: - -- `.git` directory -- Tests, tutorials, examples -- Documentation (markdown files) -- Images -- All non-.scad files - -**Result:** Minimized BOSL2 library (~1-2MB instead of ~5MB) copied to runner (line 124). - -### Stage 6: Runner - Production Image (lines 106-177) - -**Base Image:** `node:18-slim` (Debian) - Required for OpenSCAD availability - -**Runtime Dependencies (lines 111-117):** - -```dockerfile -RUN apt-get update && apt-get install -y --no-install-recommends \ - python3 \ - python3-pip \ - qpdf \ - openscad \ - ca-certificates \ - && rm -rf /var/lib/apt/lists/* -``` - -**Removed from runner:** - -- ❌ git (only needed in bosl2-builder) -- ❌ wget (only needed in typst-builder) -- ❌ curl (not needed at runtime) -- ❌ xz-utils (only needed in typst-builder) - -**Artifacts Copied from Other Stages:** - -```dockerfile -# From typst-builder (line 120) -COPY --from=typst-builder /usr/local/bin/typst /usr/local/bin/typst - -# From bosl2-builder (line 124) -COPY --from=bosl2-builder /bosl2 /usr/share/openscad/libraries/BOSL2 - -# From builder (lines 131-159) -# Next.js app, styled-system, server files, etc. - -# From deps (lines 145-146) -# Production node_modules only -``` - -BOSL2 v2.0.0 (minimized) is copied to `/usr/share/openscad/libraries/BOSL2/`, which is OpenSCAD's default library search path. This allows `include ` to work in the abacus.scad file. - -### Temp Directory for Job Outputs (line 168) - -```dockerfile -RUN mkdir -p tmp/3d-jobs && chown nextjs:nodejs tmp -``` - -Creates the directory where JobManager stores generated 3D files. - -## Files Included in Docker Image - -The following files are automatically included via the `COPY` command at line 132: - -``` -apps/web/public/3d-models/ -├── abacus.scad (parametric OpenSCAD source) -└── simplified.abacus.stl (base model, 4.8MB) -``` - -These files are NOT excluded by `.dockerignore`. - -## Testing the Docker Build - -### Local Testing - -1. **Build the Docker image:** - - ```bash - docker build -t soroban-abacus-test . - ``` - -2. **Run the container:** - - ```bash - docker run -p 3000:3000 soroban-abacus-test - ``` - -3. **Test OpenSCAD inside the container:** - - ```bash - docker exec -it sh - openscad --version - ls /usr/share/openscad/libraries/BOSL2 - ``` - -4. **Test the 3D printing endpoint:** - - Visit http://localhost:3000/3d-print - - Adjust parameters and generate a file - - Monitor job progress - - Download the result - -### Verify BOSL2 Installation - -Inside the running container: - -```bash -# Check OpenSCAD version -openscad --version - -# Verify BOSL2 library exists -ls -la /usr/share/openscad/libraries/BOSL2/ - -# Test rendering a simple file -cd /app/apps/web/public/3d-models -openscad -o /tmp/test.stl abacus.scad -``` - -## Production Deployment - -### Environment Variables - -No additional environment variables are required for the 3D printing feature. - -### Volume Mounts (Optional) - -For better performance and to avoid rebuilding the image when updating 3D models: - -```bash -docker run -p 3000:3000 \ - -v $(pwd)/apps/web/public/3d-models:/app/apps/web/public/3d-models:ro \ - soroban-abacus-test -``` - -### Disk Space Considerations - -- **BOSL2 library**: ~5MB (cloned during build) -- **Base STL file**: 4.8MB (in public/3d-models/) -- **Generated files**: Vary by parameters, typically 1-10MB each -- **Job cleanup**: Old jobs are automatically cleaned up after 1 hour - -## Image Size - -The final image is Debian-based (required for OpenSCAD), but optimized using multi-stage builds: - -**Before optimization (original Debian approach):** - -- Base runner: ~250MB -- With all build tools (git, wget, curl, xz-utils): ~290MB -- With BOSL2 (full): ~295MB -- **Total: ~295MB** - -**After optimization (current multi-stage approach):** - -- Base runner: ~250MB -- Runtime deps only (no build tools): ~250MB -- BOSL2 (minimized, .scad only): ~252MB -- 3D models (STL): ~257MB -- **Total: ~257MB** - -**Savings: ~38MB (~13% reduction)** - -### What Was Removed - -- ❌ git (~15MB) -- ❌ wget (~2MB) -- ❌ curl (~5MB) -- ❌ xz-utils (~1MB) -- ❌ BOSL2 .git directory (~1MB) -- ❌ BOSL2 tests, examples, tutorials (~10MB) -- ❌ BOSL2 images and docs (~4MB) - -**Total removed: ~38MB** - -This trade-off (Debian vs Alpine) is necessary for OpenSCAD availability, but the multi-stage approach minimizes the size impact. - -## Troubleshooting - -### OpenSCAD Not Found - -If you see "openscad: command not found" in logs: - -1. Verify OpenSCAD is installed: - - ```bash - docker exec -it which openscad - docker exec -it openscad --version - ``` - -2. Check if the Debian package install succeeded: - ```bash - docker exec -it dpkg -l | grep openscad - ``` - -### BOSL2 Include Error - -If OpenSCAD reports "Can't open library 'BOSL2/std.scad'": - -1. Check BOSL2 exists: - - ```bash - docker exec -it ls /usr/share/openscad/libraries/BOSL2/std.scad - ``` - -2. Test include path: - ```bash - docker exec -it sh -c "cd /tmp && echo 'include ; cube(10);' > test.scad && openscad -o test.stl test.scad" - ``` - -### Job Fails with "Permission Denied" - -Check tmp directory permissions: - -```bash -docker exec -it ls -la /app/apps/web/tmp -# Should show: drwxr-xr-x ... nextjs nodejs ... 3d-jobs -``` - -### Large File Generation Timeout - -Jobs timeout after 60 seconds. For complex models, increase the timeout in `jobManager.ts:138`: - -```typescript -timeout: 120000, // 2 minutes instead of 60 seconds -``` - -## Performance Notes - -- **Cold start**: First generation takes ~5-10 seconds (OpenSCAD initialization) -- **Warm generations**: Subsequent generations take ~3-5 seconds -- **STL size**: Typically 5-15MB depending on scale parameters -- **3MF size**: Similar to STL (no significant compression) -- **SCAD size**: ~1KB (just text parameters) - -## Monitoring - -Job processing is logged to stdout: - -``` -Executing: openscad -o /app/apps/web/tmp/3d-jobs/abacus-abc123.stl ... -Job abc123 completed successfully -``` - -Check logs with: - -```bash -docker logs | grep "Job" -``` diff --git a/apps/web/docs/DAILY_PRACTICE_SYSTEM.md b/apps/web/docs/DAILY_PRACTICE_SYSTEM.md index fde6f86b..c28528d5 100644 --- a/apps/web/docs/DAILY_PRACTICE_SYSTEM.md +++ b/apps/web/docs/DAILY_PRACTICE_SYSTEM.md @@ -676,8 +676,6 @@ The practice experience is the actual problem-solving interface where the studen │ │ ● ● ● ● ○ ○ ○ ○ ○ │ │ │ └───────────────────────┘ │ │ │ -│ 3D Model: public/3d-models/simplified.abacus.stl │ -│ │ └─────────────────────────────────────────────────────────────────┘ ``` @@ -896,7 +894,6 @@ const constraints = { | `InputPhase` | `src/arcade-games/memory-quiz/components/InputPhase.tsx` | Custom numeric keypad + device detection | | `problemGenerator` | `src/utils/problemGenerator.ts` | Skill-constrained problem generation | | `AbacusReact` | `@soroban/abacus-react` | On-screen abacus (last resort) | -| 3D Abacus Model | `public/3d-models/simplified.abacus.stl` | Physical abacus recommendation | ### Data Model Extensions diff --git a/apps/web/package.json b/apps/web/package.json index e678ab8c..48d228d9 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -79,7 +79,6 @@ "next": "^14.2.32", "next-auth": "5.0.0-beta.29", "next-intl": "^4.4.0", - "openscad-wasm-prebuilt": "^1.2.0", "python-bridge": "^1.1.0", "qrcode": "^1.5.4", "qrcode.react": "^4.2.0", diff --git a/apps/web/public/3d-models/abacus-inline.scad b/apps/web/public/3d-models/abacus-inline.scad deleted file mode 100644 index ea592ed9..00000000 --- a/apps/web/public/3d-models/abacus-inline.scad +++ /dev/null @@ -1,47 +0,0 @@ -// Inline version of abacus.scad that doesn't require BOSL2 -// This version uses a hardcoded bounding box size instead of the bounding_box() function - -// ---- USER CUSTOMIZABLE PARAMETERS ---- -// These can be overridden via command line: -D 'columns=7' etc. -columns = 13; // Total number of columns (1-13, mirrored book design) -scale_factor = 1.5; // Overall size scale (preserves aspect ratio) -// ----------------------------------------- - -stl_path = "/3d-models/simplified.abacus.stl"; - -// Known bounding box dimensions of the simplified.abacus.stl file -// These were measured from the original file -bbox_size = [186, 60, 120]; // [width, depth, height] in STL units - -// Calculate parameters based on column count -// The full STL has 13 columns. We want columns/2 per side (mirrored). -total_columns_in_stl = 13; -columns_per_side = columns / 2; -width_scale = columns_per_side / total_columns_in_stl; - -// Column spacing: distance between mirrored halves -units_per_column = bbox_size[0] / total_columns_in_stl; // ~14.3 units per column -column_spacing = columns_per_side * units_per_column; - -// --- actual model --- -module imported() { - import(stl_path, convexity = 10); -} - -// Create a bounding box manually instead of using BOSL2's bounding_box() -module bounding_box_manual() { - translate([-bbox_size[0]/2, -bbox_size[1]/2, -bbox_size[2]/2]) - cube(bbox_size); -} - -module half_abacus() { - intersection() { - scale([width_scale, 1, 1]) bounding_box_manual(); - imported(); - } -} - -scale([scale_factor, scale_factor, scale_factor]) { - translate([column_spacing, 0, 0]) mirror([1,0,0]) half_abacus(); - half_abacus(); -} diff --git a/apps/web/public/3d-models/abacus.scad b/apps/web/public/3d-models/abacus.scad deleted file mode 100644 index a4a2e77b..00000000 --- a/apps/web/public/3d-models/abacus.scad +++ /dev/null @@ -1,39 +0,0 @@ -include ; // BOSL2 v2.0 or newer - -// ---- USER CUSTOMIZABLE PARAMETERS ---- -// These can be overridden via command line: -D 'columns=7' etc. -columns = 13; // Total number of columns (1-13, mirrored book design) -scale_factor = 1.5; // Overall size scale (preserves aspect ratio) -// ----------------------------------------- - -stl_path = "./simplified.abacus.stl"; - -// Calculate parameters based on column count -// The full STL has 13 columns. We want columns/2 per side (mirrored). -// The original bounding box intersection: scale([35/186, 1, 1]) -// 35/186 ≈ 0.188 = ~2.44 columns, so 186 units ≈ 13 columns, ~14.3 units per column -total_columns_in_stl = 13; -columns_per_side = columns / 2; -width_scale = columns_per_side / total_columns_in_stl; - -// Column spacing: distance between mirrored halves -// Original spacing of 69 for ~2.4 columns/side -// Calculate proportional spacing based on columns -units_per_column = 186 / total_columns_in_stl; // ~14.3 units per column -column_spacing = columns_per_side * units_per_column; - -// --- actual model --- -module imported() - import(stl_path, convexity = 10); - -module half_abacus() { - intersection() { - scale([width_scale, 1, 1]) bounding_box() imported(); - imported(); - } -} - -scale([scale_factor, scale_factor, scale_factor]) { - translate([column_spacing, 0, 0]) mirror([1,0,0]) half_abacus(); - half_abacus(); -} diff --git a/apps/web/public/3d-models/simplified.abacus.stl b/apps/web/public/3d-models/simplified.abacus.stl deleted file mode 100644 index 237dd73b..00000000 Binary files a/apps/web/public/3d-models/simplified.abacus.stl and /dev/null differ diff --git a/apps/web/src/lib/3d-printing/jobManager.ts b/apps/web/src/lib/3d-printing/jobManager.ts deleted file mode 100644 index 30798cd3..00000000 --- a/apps/web/src/lib/3d-printing/jobManager.ts +++ /dev/null @@ -1,215 +0,0 @@ -import { exec } from 'node:child_process' -import { randomBytes } from 'node:crypto' -import { mkdir, readFile, rm, writeFile } from 'node:fs/promises' -import { join } from 'node:path' -import { promisify } from 'node:util' - -const execAsync = promisify(exec) - -export type JobStatus = 'pending' | 'processing' | 'completed' | 'failed' - -export interface Job { - id: string - status: JobStatus - params: AbacusParams - error?: string - outputPath?: string - createdAt: Date - completedAt?: Date - progress?: string -} - -export interface AbacusParams { - columns: number // Number of columns (1-13) - scaleFactor: number // Overall size multiplier - widthMm?: number // Optional: desired width in mm (overrides scaleFactor) - format: 'stl' | '3mf' | 'scad' - // 3MF color options - frameColor?: string - heavenBeadColor?: string - earthBeadColor?: string - decorationColor?: string -} - -// In-memory job storage (can be upgraded to Redis later) -const jobs = new Map() - -// Temporary directory for generated files -const TEMP_DIR = join(process.cwd(), 'tmp', '3d-jobs') - -export class JobManager { - static generateJobId(): string { - return randomBytes(16).toString('hex') - } - - static async createJob(params: AbacusParams): Promise { - const jobId = JobManager.generateJobId() - const job: Job = { - id: jobId, - status: 'pending', - params, - createdAt: new Date(), - } - - jobs.set(jobId, job) - - // Start processing in background - JobManager.processJob(jobId).catch((error) => { - console.error(`Job ${jobId} failed:`, error) - const job = jobs.get(jobId) - if (job) { - job.status = 'failed' - job.error = error.message - job.completedAt = new Date() - } - }) - - return jobId - } - - static getJob(jobId: string): Job | undefined { - return jobs.get(jobId) - } - - static async processJob(jobId: string): Promise { - const job = jobs.get(jobId) - if (!job) throw new Error('Job not found') - - job.status = 'processing' - job.progress = 'Preparing workspace...' - - // Create temp directory - await mkdir(TEMP_DIR, { recursive: true }) - - const outputFileName = `abacus-${jobId}.${job.params.format}` - const outputPath = join(TEMP_DIR, outputFileName) - - try { - // Build OpenSCAD command - const scadPath = join(process.cwd(), 'public', '3d-models', 'abacus.scad') - const stlPath = join(process.cwd(), 'public', '3d-models', 'simplified.abacus.stl') - - // If format is 'scad', just copy the file with custom parameters - if (job.params.format === 'scad') { - job.progress = 'Generating OpenSCAD file...' - const scadContent = await readFile(scadPath, 'utf-8') - const customizedScad = scadContent - .replace(/columns = \d+\.?\d*/, `columns = ${job.params.columns}`) - .replace(/scale_factor = \d+\.?\d*/, `scale_factor = ${job.params.scaleFactor}`) - - await writeFile(outputPath, customizedScad) - job.outputPath = outputPath - job.status = 'completed' - job.completedAt = new Date() - job.progress = 'Complete!' - return - } - - job.progress = 'Rendering 3D model...' - - // Build command with parameters - const cmd = [ - 'openscad', - '-o', - outputPath, - '-D', - `'columns=${job.params.columns}'`, - '-D', - `'scale_factor=${job.params.scaleFactor}'`, - scadPath, - ].join(' ') - - console.log(`Executing: ${cmd}`) - - // Execute OpenSCAD (with 60s timeout) - // Note: OpenSCAD may exit with non-zero status due to CGAL warnings - // but still produce valid output. We'll check file existence afterward. - try { - await execAsync(cmd, { - timeout: 60000, - cwd: join(process.cwd(), 'public', '3d-models'), - }) - } catch (execError) { - // Log the error but don't throw yet - check if output was created - console.warn(`OpenSCAD reported errors, but checking if output was created:`, execError) - - // Check if output file exists despite the error - try { - await readFile(outputPath) - console.log(`Output file created despite OpenSCAD warnings - proceeding`) - } catch (readError) { - // File doesn't exist, this is a real failure - console.error(`OpenSCAD execution failed and no output file created:`, execError) - if (execError instanceof Error) { - throw new Error(`OpenSCAD error: ${execError.message}`) - } - throw execError - } - } - - job.progress = 'Finalizing...' - - // Verify output exists and check file size - const fileBuffer = await readFile(outputPath) - const fileSizeMB = fileBuffer.length / (1024 * 1024) - - // Maximum file size: 100MB (to prevent memory issues) - const MAX_FILE_SIZE_MB = 100 - if (fileSizeMB > MAX_FILE_SIZE_MB) { - throw new Error( - `Generated file is too large (${fileSizeMB.toFixed(1)}MB). Maximum allowed is ${MAX_FILE_SIZE_MB}MB. Try reducing scale parameters.` - ) - } - - console.log(`Generated STL file size: ${fileSizeMB.toFixed(2)}MB`) - - job.outputPath = outputPath - job.status = 'completed' - job.completedAt = new Date() - job.progress = 'Complete!' - - console.log(`Job ${jobId} completed successfully`) - } catch (error) { - console.error(`Job ${jobId} failed:`, error) - job.status = 'failed' - job.error = error instanceof Error ? error.message : 'Unknown error occurred' - job.completedAt = new Date() - throw error - } - } - - static async getJobOutput(jobId: string): Promise { - const job = jobs.get(jobId) - if (!job) throw new Error('Job not found') - if (job.status !== 'completed') throw new Error(`Job is ${job.status}, not completed`) - if (!job.outputPath) throw new Error('Output path not set') - - return await readFile(job.outputPath) - } - - static async cleanupJob(jobId: string): Promise { - const job = jobs.get(jobId) - if (!job) return - - if (job.outputPath) { - try { - await rm(job.outputPath) - } catch (error) { - console.error(`Failed to cleanup job ${jobId}:`, error) - } - } - - jobs.delete(jobId) - } - - // Cleanup old jobs (should be called periodically) - static async cleanupOldJobs(maxAgeMs = 3600000): Promise { - const now = Date.now() - for (const [jobId, job] of jobs.entries()) { - const age = now - job.createdAt.getTime() - if (age > maxAgeMs) { - await JobManager.cleanupJob(jobId) - } - } - } -} diff --git a/apps/web/src/workers/openscad.worker.ts b/apps/web/src/workers/openscad.worker.ts deleted file mode 100644 index 1a08a049..00000000 --- a/apps/web/src/workers/openscad.worker.ts +++ /dev/null @@ -1,209 +0,0 @@ -/// - -import { createOpenSCAD } from 'openscad-wasm-prebuilt' - -declare const self: DedicatedWorkerGlobalScope - -let openscad: Awaited> | null = null -let simplifiedStlData: ArrayBuffer | null = null -let isInitializing = false -let initPromise: Promise | null = null - -// Message types -interface RenderRequest { - type: 'render' - columns: number - scaleFactor: number -} - -interface InitRequest { - type: 'init' -} - -type WorkerRequest = RenderRequest | InitRequest - -// Initialize OpenSCAD instance and load base STL file -async function initialize() { - if (openscad) return // Already initialized - if (isInitializing) return initPromise // Already initializing, return existing promise - - isInitializing = true - initPromise = (async () => { - try { - console.log('[OpenSCAD Worker] Initializing...') - - // Create OpenSCAD instance - openscad = await createOpenSCAD() - console.log('[OpenSCAD Worker] OpenSCAD WASM loaded') - - // Fetch the simplified STL file once - const stlResponse = await fetch('/3d-models/simplified.abacus.stl') - if (!stlResponse.ok) { - throw new Error(`Failed to fetch STL: ${stlResponse.statusText}`) - } - simplifiedStlData = await stlResponse.arrayBuffer() - console.log('[OpenSCAD Worker] Simplified STL loaded', simplifiedStlData.byteLength, 'bytes') - - self.postMessage({ type: 'ready' }) - } catch (error) { - console.error('[OpenSCAD Worker] Initialization failed:', error) - self.postMessage({ - type: 'error', - error: error instanceof Error ? error.message : 'Initialization failed', - }) - throw error - } finally { - isInitializing = false - } - })() - - return initPromise -} - -async function render(columns: number, scaleFactor: number) { - // Wait for initialization if not ready - if (!openscad || !simplifiedStlData) { - await initialize() - } - - if (!openscad || !simplifiedStlData) { - throw new Error('Worker not initialized') - } - - try { - console.log(`[OpenSCAD Worker] Rendering with columns=${columns}, scaleFactor=${scaleFactor}`) - - // Get low-level instance for filesystem access - const instance = openscad.getInstance() - - // Create directory if it doesn't exist - try { - instance.FS.mkdir('/3d-models') - console.log('[OpenSCAD Worker] Created /3d-models directory') - } catch (e: any) { - // Check if it's EEXIST (directory already exists) - errno 20 - if (e.errno === 20) { - console.log('[OpenSCAD Worker] /3d-models directory already exists') - } else { - console.error('[OpenSCAD Worker] Failed to create directory:', e) - throw new Error(`Failed to create /3d-models directory: ${e.message || e}`) - } - } - - // Write STL file - instance.FS.writeFile('/3d-models/simplified.abacus.stl', new Uint8Array(simplifiedStlData)) - console.log('[OpenSCAD Worker] Wrote simplified STL to filesystem') - - // Generate the SCAD code with parameters - const scadCode = ` -// Inline version of abacus.scad that doesn't require BOSL2 -columns = ${columns}; -scale_factor = ${scaleFactor}; - -stl_path = "/3d-models/simplified.abacus.stl"; - -// Known bounding box dimensions -bbox_size = [186, 60, 120]; - -// Calculate parameters -total_columns_in_stl = 13; -columns_per_side = columns / 2; -width_scale = columns_per_side / total_columns_in_stl; - -units_per_column = bbox_size[0] / total_columns_in_stl; -column_spacing = columns_per_side * units_per_column; - -// Model modules -module imported() { - import(stl_path, convexity = 10); -} - -module bounding_box_manual() { - translate([-bbox_size[0]/2, -bbox_size[1]/2, -bbox_size[2]/2]) - cube(bbox_size); -} - -module half_abacus() { - intersection() { - scale([width_scale, 1, 1]) bounding_box_manual(); - imported(); - } -} - -scale([scale_factor, scale_factor, scale_factor]) { - translate([column_spacing, 0, 0]) mirror([1,0,0]) half_abacus(); - half_abacus(); -} -` - - // Use high-level renderToStl API - console.log('[OpenSCAD Worker] Calling renderToStl...') - const stlBuffer = await openscad.renderToStl(scadCode) - console.log('[OpenSCAD Worker] Rendering complete:', stlBuffer.byteLength, 'bytes') - - // Send the result back - self.postMessage( - { - type: 'result', - stl: stlBuffer, - }, - [stlBuffer] - ) // Transfer ownership of the buffer - - // Clean up STL file - try { - instance.FS.unlink('/3d-models/simplified.abacus.stl') - } catch (e) { - // Ignore cleanup errors - } - } catch (error) { - console.error('[OpenSCAD Worker] Rendering failed:', error) - - // Try to get more error details - let errorMessage = 'Rendering failed' - if (error instanceof Error) { - errorMessage = error.message - console.error('[OpenSCAD Worker] Error stack:', error.stack) - } - - // Check if it's an Emscripten FS error - if (error && typeof error === 'object' && 'errno' in error) { - console.error('[OpenSCAD Worker] FS errno:', (error as any).errno) - console.error('[OpenSCAD Worker] FS error details:', error) - } - - self.postMessage({ - type: 'error', - error: errorMessage, - }) - } -} - -// Message handler -self.onmessage = async (event: MessageEvent) => { - const { data } = event - - try { - switch (data.type) { - case 'init': - await initialize() - break - - case 'render': - await render(data.columns, data.scaleFactor) - break - - default: - console.error('[OpenSCAD Worker] Unknown message type:', data) - } - } catch (error) { - console.error('[OpenSCAD Worker] Message handler error:', error) - self.postMessage({ - type: 'error', - error: error instanceof Error ? error.message : 'Unknown error', - }) - } -} - -// Auto-initialize on worker start -initialize()