chore: remove abandoned 3d-printing feature

Remove dead code from abandoned 3D printing initiative:
- Delete jobManager.ts (had unbounded memory growth)
- Delete openscad.worker.ts (unused)
- Delete 3D model files from public/
- Remove openscad-wasm-prebuilt dependency
- Clean up doc references

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Thomas Hallock 2025-12-09 12:48:13 -06:00
parent ae1a0a8e2d
commit f74db216da
8 changed files with 0 additions and 849 deletions

View File

@ -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 <BOSL2/std.scad>` 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 <container-id> 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 <container-id> which openscad
docker exec -it <container-id> openscad --version
```
2. Check if the Debian package install succeeded:
```bash
docker exec -it <container-id> 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 <container-id> ls /usr/share/openscad/libraries/BOSL2/std.scad
```
2. Test include path:
```bash
docker exec -it <container-id> sh -c "cd /tmp && echo 'include <BOSL2/std.scad>; cube(10);' > test.scad && openscad -o test.stl test.scad"
```
### Job Fails with "Permission Denied"
Check tmp directory permissions:
```bash
docker exec -it <container-id> 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 <container-id> | grep "Job"
```

View File

@ -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 | | `InputPhase` | `src/arcade-games/memory-quiz/components/InputPhase.tsx` | Custom numeric keypad + device detection |
| `problemGenerator` | `src/utils/problemGenerator.ts` | Skill-constrained problem generation | | `problemGenerator` | `src/utils/problemGenerator.ts` | Skill-constrained problem generation |
| `AbacusReact` | `@soroban/abacus-react` | On-screen abacus (last resort) | | `AbacusReact` | `@soroban/abacus-react` | On-screen abacus (last resort) |
| 3D Abacus Model | `public/3d-models/simplified.abacus.stl` | Physical abacus recommendation |
### Data Model Extensions ### Data Model Extensions

View File

@ -79,7 +79,6 @@
"next": "^14.2.32", "next": "^14.2.32",
"next-auth": "5.0.0-beta.29", "next-auth": "5.0.0-beta.29",
"next-intl": "^4.4.0", "next-intl": "^4.4.0",
"openscad-wasm-prebuilt": "^1.2.0",
"python-bridge": "^1.1.0", "python-bridge": "^1.1.0",
"qrcode": "^1.5.4", "qrcode": "^1.5.4",
"qrcode.react": "^4.2.0", "qrcode.react": "^4.2.0",

View File

@ -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();
}

View File

@ -1,39 +0,0 @@
include <BOSL2/std.scad>; // 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();
}

View File

@ -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<string, Job>()
// 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<string> {
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<void> {
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<Buffer> {
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<void> {
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<void> {
const now = Date.now()
for (const [jobId, job] of jobs.entries()) {
const age = now - job.createdAt.getTime()
if (age > maxAgeMs) {
await JobManager.cleanupJob(jobId)
}
}
}
}

View File

@ -1,209 +0,0 @@
/// <reference lib="webworker" />
import { createOpenSCAD } from 'openscad-wasm-prebuilt'
declare const self: DedicatedWorkerGlobalScope
let openscad: Awaited<ReturnType<typeof createOpenSCAD>> | null = null
let simplifiedStlData: ArrayBuffer | null = null
let isInitializing = false
let initPromise: Promise<void> | 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<WorkerRequest>) => {
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()