Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,8 @@
__pycache__/
*.pyc
*.pyo
.venv
.venv

# Frontend
app/frontend/node_modules/
app/frontend/dist/
10 changes: 10 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,7 +1,17 @@
# ── Stage 1: Build React frontend ─────────────────────────────────────────────
FROM node:20-slim AS frontend-build
WORKDIR /frontend
COPY app/frontend/package.json app/frontend/package-lock.json ./
RUN npm ci --legacy-peer-deps
COPY app/frontend/ .
RUN npm run build

# ── Stage 2: Python backend ──────────────────────────────────────────────────
FROM python:3.11-slim
WORKDIR /app
RUN apt-get update && apt-get install -y --no-install-recommends gcc libc6-dev && rm -rf /var/lib/apt/lists/*
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY ./app /app/app
COPY --from=frontend-build /frontend/dist /app/app/frontend/dist
CMD ["sh", "-c", "uvicorn app.main:app --host 0.0.0.0 --port 8000 ${BASE_PATH:+--root-path $BASE_PATH}"]
112 changes: 110 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Integrated toolpath optimization platform for CNC pen plotters. Converts vector
- Geometric line overlap detection and filtering
- WebSocket-driven progress monitoring

**TECH STACK:** FastAPI | Python 3.11 | Vanilla JS | Canvas API
**TECH STACK:** FastAPI | Python 3.11 | React + TypeScript + Vite | Canvas API

## BUILD PROCEDURE

Expand All @@ -27,6 +27,9 @@ docker compose up -d --build

Service available at `http://localhost:8080`

- Legacy UI: `http://localhost:8080/`
- React v2 UI: `http://localhost:8080/v2`

### SUBPATH HOSTING

For reverse-proxy deployments under dedicated routes (e.g., `domain.com/plotter-tool`):
Expand Down Expand Up @@ -59,20 +62,125 @@ location /plotter-tool {
### LOCAL DEVELOPMENT

```bash
# Backend
pip install -r requirements.txt
uvicorn app.main:app --reload

# Frontend (separate terminal)
cd app/frontend
npm install --legacy-peer-deps
npm run dev # Dev server with hot-reload (proxied to backend)
npm run build # Production build → app/frontend/dist/
npm test # Run Vitest unit tests
```

Optional: `BASE_PATH=/plotter-tool uvicorn app.main:app --reload`

## FRONTEND ARCHITECTURE

The v2 frontend is a React + TypeScript SPA built with Vite, using a feature-sliced structure:

```
app/frontend/src/
├── api/ # Typed API client for v2 endpoints
├── store/ # Zustand state management
├── features/
│ ├── settings/ # Schema-driven settings panel (debounced, optimistic)
│ ├── path-preview/ # Canvas-based path visualization with layer toggling
│ └── upload/ # File upload, optimization trigger, download controls
├── components/ # Shared UI components (Terminal, StatusBar)
├── types.ts # Shared TypeScript types matching backend schemas
└── __tests__/ # Unit tests (store reducers, event handling)
```

### Preview Layers

The path preview engine renders multiple visualization layers that users can toggle:

| Layer | Color | Description |
|-------|-------|-------------|
| `original` | Grey | Initial parsed paths |
| `filtered` | Red | Paths removed by pen-width filter |
| `greedy` | Blue | After nearest-neighbor sort |
| `merged` | Green | After path merging |
| `twoopt` | Yellow | After 2-opt refinement |
| `final` | White | Final optimized paths + travel moves |

## API REFERENCE

### Legacy Endpoints (v1)

| Method | Endpoint | Description |
|--------|----------|-------------|
| `POST` | `/upload` | Upload G-code file |
| `POST` | `/upload-svg` | Upload SVG file |
| `WS` | `/ws/{job_id}` | Optimization WebSocket |
| `GET` | `/download/{job_id}` | Download optimized G-code |

### v2 Endpoints

| Method | Endpoint | Description |
|--------|----------|-------------|
| `POST` | `/api/v2/upload` | Upload G-code (typed response) |
| `POST` | `/api/v2/upload-svg` | Upload SVG (typed response) |
| `GET` | `/api/v2/job/{job_id}` | Get job status |
| `GET` | `/api/v2/download/{job_id}` | Download optimized G-code |
| `GET` | `/api/v2/settings/schema` | Get settings JSON Schema |
| `WS` | `/api/v2/ws/{job_id}` | Optimization WebSocket (typed events) |

### WebSocket Event Types

Events sent during optimization over the v2 WebSocket:

| Event | Phase | Key Fields |
|-------|-------|------------|
| `log` | Any | `msg` |
| `filter_start` | Filter | `path_count`, `pen_width`, `visibility_threshold` |
| `filter_result` | Filter | `removed_count`, `kept_count`, `removed_indices` |
| `greedy_result` | Greedy | `paths`, `progress_history`, `original_dist`, `phase1_dist` |
| `merge_result` | Merge | `paths`, `merge_count`, `post_merge_dist` |
| `twoopt_start` | 2-OPT | `estimated_paths` |
| `phase2_result` | 2-OPT | `paths`, `iterations`, `dist_history`, `final_dist` |
| `complete` | Done | `job_id` |
| `ping` | Any | Keep-alive during long operations |
| `phase_progress` | Any | `phase`, `progress` (0–100) |

### Settings Schema

All settings are defined in `app/models.py` as a Pydantic `OptimizerSettings` model. The JSON Schema is available at `GET /api/v2/settings/schema`.

| Setting | Type | Default | Description |
|---------|------|---------|-------------|
| `z_up` | float | 2.0 | Pen lift height (mm) |
| `z_down` | float | 0.0 | Pen down height (mm) |
| `feedrate` | float | 1000 | Draw speed (mm/min) |
| `travel_speed` | float\|null | 3000 | Travel speed (mm/min) |
| `z_speed` | float\|null | 500 | Z-axis speed (mm/min) |
| `curve_tolerance` | float | 0.1 | SVG curve discretization (mm) |
| `pen_width` | float | 0 | Pen width for filter (0=disabled) |
| `visibility_threshold` | float | 50 | Min visibility % to keep line |
| `offset_closed_paths` | bool | false | Offset closed paths inward |
| `merge_threshold` | float | 0.5 | Max gap to merge paths (mm) |
| `gcode_header` | string | "G28" | Startup G-code commands |
| `gcode_footer` | string | "G0 Z5\nG0 X10 Y10\nM84" | Shutdown G-code commands |

## CONFIGURATION

All operational parameters (feed rates, Z-axis control, curve tolerance, pen width filtering) are configurable via the web interface. Settings persist in browser local storage.
All operational parameters are configurable via either the legacy or v2 web interface. Settings persist in browser local storage under the key `cyberplotter_settings`.

## DEPENDENCIES

This system incorporates [svg2gcode](https://github.com/sameer/svg2gcode) for SVG decomposition and G-code generation. Path optimization and overlap filtering are implemented internally.

## EXTENSION GUIDELINES

The v2 frontend is designed for future extension:

- **New settings**: Add fields to `OptimizerSettings` in `app/models.py` and `OptimizerSettings` in `src/types.ts`. The settings panel renders them automatically.
- **New preview layers**: Add a key to `PreviewLayers` in `src/types.ts` and a colour entry in `PathPreview.tsx`.
- **New WS events**: Add the event type to `WSEvent` union in `src/types.ts` and handle it in `appStore.ts` `handleWSEvent`.
- **Vectorization**: The settings panel and preview architecture support future vectorization features without refactor. Add vectorization-specific settings and preview layers when ready.

## DISCLAIMER

This application was shamelessly vibe-coded by an individual whose close-to-zero coding skills are a source of deep, profound shame. The code quality reflects this unfortunate reality. Proceed with appropriate caution.
Expand Down
12 changes: 12 additions & 0 deletions app/frontend/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>PLOTTERTOOL // SVG & G-CODE TOOLSET v2</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
Loading