pcbjam/docs/02-PHASE2-CORE-EXTRACTION.md
Viktor Vaczi 92faf7c082 Add standalone core library with kimath and sexpr (Phase 2 Step 1)
Create standalone build system for KiCad's core computation libraries
that compiles without wxWidgets, targeting both native and WebAssembly.

Key additions:
- wx_shim.h: ~250 lines replacing wx utilities with standard C++
  - wxString class with Format() method
  - wxFFile for file I/O
  - wxASSERT, wxCHECK, wxFAIL_MSG macros
  - wxLog stubs
- Stub headers: wx/debug.h, wx/log.h, wx/string.h, wx/file.h, etc.
- config.h, advanced_config.h: Platform and triangulation config
- CMakeLists.txt: Builds kimath, sexpr, clipper2, rtree

Build results:
- Native: libkimath.a (1.4MB), libsexpr.a, test passes
- WASM: test_kimath.wasm (867KB), runs in Node.js

Test verifies:
- VECTOR2I, SEG, SHAPE_POLY_SET geometry operations
- S-expression parsing of .kicad_pcb format

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-26 17:39:53 +01:00

395 lines
12 KiB
Markdown

# Phase 2: Core Library Extraction
## Overview
Extract KiCad's core computation code into a standalone library that compiles to both native and WebAssembly. This enables running the native wxWidgets GUI while delegating computation to a WASM module.
## Architecture: Native GUI + WASM Core
```
┌─────────────────────────────────────────────────────────┐
│ Native GUI (wxWidgets) │
│ - PCB Editor canvas, menus, dialogs │
│ - File dialogs, clipboard │
│ - User interaction │
└─────────────────────┬───────────────────────────────────┘
│ S-expression serialization
┌─────────────────────────────────────────────────────────┐
│ WASM Bridge │
│ - Wasmtime/wasm3 runtime (future) │
│ - Serialize board → S-expr → WASM │
│ - Deserialize results ← S-expr ← WASM │
└─────────────────────┬───────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ kicad_core.wasm │
│ - libs/kimath (geometry) │
│ - libs/sexpr (parsing) │
│ - Board data model │
│ - DRC engine │
│ - Router (Push & Shove) │
└─────────────────────────────────────────────────────────┘
```
## Why This Works: wxWidgets is NOT Required for Core
**Important**: The core libs don't use wxWidgets for GUI - they just use utility macros that happen to come from wx. We provide drop-in replacements via a ~50 line shim header:
| Library | wx Usage | Count | Shim Solution |
|---------|----------|-------|---------------|
| libs/kimath | wxASSERT, wxLogTrace | ~34 | Macros → assert/no-op |
| libs/sexpr | wxFFile, wxString | ~4 | Classes → std equivalents |
| libs/core | wxString | ~17 | typedef → std::string |
### wx_shim.h
```cpp
// core/include/wx_shim.h
#ifndef KICAD_WX_SHIM_H
#define KICAD_WX_SHIM_H
#include <cassert>
#include <string>
#include <cstdio>
// Assertions - just use standard assert
#define wxASSERT(x) assert(x)
#define wxASSERT_MSG(x, msg) assert((x) && (msg))
#define wxCHECK(x, ret) do { if(!(x)) return ret; } while(0)
#define wxCHECK_MSG(x, ret, msg) do { if(!(x)) return ret; } while(0)
#define wxFAIL_MSG(msg) assert(false && (msg))
// Logging - no-op or stderr
#define wxLogTrace(...) ((void)0)
#define wxLogDebug(...) ((void)0)
#define wxLogWarning(...) fprintf(stderr, __VA_ARGS__)
// String - just use std::string
using wxString = std::string;
using wxChar = char;
// File I/O - use standard C++
#include <fstream>
class wxFFile {
std::ifstream m_file;
public:
bool Open(const std::string& name) { m_file.open(name); return m_file.is_open(); }
bool IsOpened() const { return m_file.is_open(); }
size_t Read(void* buf, size_t count) { m_file.read((char*)buf, count); return m_file.gcount(); }
bool Eof() const { return m_file.eof(); }
};
#endif
```
## Directory Structure
```
kicad-wasm/
├── core/
│ ├── CMakeLists.txt # Standalone build without wxWidgets
│ ├── include/
│ │ ├── wx_shim.h # wx compatibility layer (~50 lines)
│ │ └── kicad_core_api.h # C API for WASM
│ ├── src/
│ │ └── api.cpp # API implementation
│ └── wasm/
│ └── CMakeLists.txt # Emscripten-specific settings
├── test/
│ ├── test_geometry.cpp # Test kimath without wx
│ ├── test_board_io.cpp # Test board load/save
│ └── test.kicad_pcb # Sample board file
```
## Implementation Steps
### Step 1: Foundation + Shim Layer
- Create `core/` directory structure
- Create `wx_shim.h` with standard C++ replacements
- Create CMakeLists.txt that builds kimath, core, sexpr
- **Test**: Compiles without wxWidgets
### Step 2: Board Model Extraction
- Identify minimal BOARD dependencies
- Create C API: `kicad_load_board()`, `kicad_save_board()`
- Handle S-expression serialization
- **Test**: Load a .kicad_pcb file via API
### Step 3: DRC Engine Extraction
- Extract DRC_ENGINE and test providers
- Create C API: `kicad_drc_run()` returns violations as JSON
- **Test**: Run DRC on test board
### Step 4: Router Extraction
- Extract PNS::ROUTER and supporting classes
- Create minimal ROUTER_IFACE implementation
- Create C API: `kicad_router_start()`, `_move()`, `_commit()`
- **Test**: Route traces via API
### Step 5: Emscripten Build
- Set up emsdk toolchain
- Build `kicad_core.wasm`
- Create JavaScript bindings
- **Test**: Load board in Node.js, run DRC, route traces
### Step 6: Browser Demo (Future)
- Simple HTML page with file upload
- Load .kicad_pcb, display stats
- Run DRC, show violations
## C API Design
### Board I/O
```cpp
extern "C" {
// Load board from S-expression string
void* kicad_load_board(const char* sexpr_data, size_t len);
// Serialize board to S-expression
char* kicad_save_board(void* board);
// Free memory
void kicad_free_board(void* board);
void kicad_free_string(char* str);
// Query operations
int kicad_get_track_count(void* board);
int kicad_get_footprint_count(void* board);
}
```
### DRC Engine
```cpp
extern "C" {
// Initialize DRC with rules
void* kicad_drc_create(void* board, const char* rules_sexpr);
// Run DRC, returns JSON array of violations
char* kicad_drc_run(void* drc_engine);
// Query specific clearance
int kicad_drc_query_clearance(void* drc, int item_a, int item_b);
void kicad_drc_free(void* drc);
}
```
### Router
```cpp
extern "C" {
// Create router with board data
void* kicad_router_create(void* board);
// Start routing from point
int kicad_router_start(void* router, int x, int y, int layer);
// Move to point, returns preview geometry as S-expr
char* kicad_router_move(void* router, int x, int y);
// Commit route
char* kicad_router_commit(void* router);
void kicad_router_free(void* router);
}
```
## Emscripten Build
```bash
source /path/to/emsdk/emsdk_env.sh
cd kicad-wasm
mkdir build-wasm && cd build-wasm
emcmake cmake ../core \
-DCMAKE_BUILD_TYPE=Release \
-DKICAD_WASM_BUILD=ON
emmake make
```
### Emscripten CMake Settings
```cmake
if(EMSCRIPTEN)
set_target_properties(kicad_core PROPERTIES
LINK_FLAGS "-s EXPORTED_FUNCTIONS='[_kicad_load_board,_kicad_save_board,...]' \
-s EXPORTED_RUNTIME_METHODS='[ccall,cwrap,UTF8ToString]' \
-s MODULARIZE=1 \
-s EXPORT_NAME='KicadCore' \
-s ALLOW_MEMORY_GROWTH=1"
)
endif()
```
## Key KiCad Source Files
**Libraries to include (via shim, no modification):**
- `kicad/libs/kimath/src/**/*.cpp` - 17.5k lines, geometry
- `kicad/libs/core/*.cpp` - 870 lines, utilities
- `kicad/libs/sexpr/*.cpp` - 734 lines, parser
- `kicad/pcbnew/board*.cpp` - Board data model
- `kicad/pcbnew/pcb_io/kicad_sexpr/*.cpp` - S-expr I/O
- `kicad/pcbnew/drc/*.cpp` - DRC engine
- `kicad/pcbnew/router/pns_*.cpp` - Router
**Key headers:**
- `kicad/libs/kimath/include/geometry/shape_poly_set.h` - Polygon ops
- `kicad/pcbnew/board.h` - BOARD class (1510 lines)
- `kicad/pcbnew/drc/drc_engine.h` - DRC entry point
- `kicad/pcbnew/router/pns_router.h` - Router entry point
## Progress
### ✅ Step 1: Foundation + Shim Layer (COMPLETE)
**Date**: 2025-11-26
Successfully compiled kimath standalone without wxWidgets:
```
core/
├── include/
│ ├── wx_shim.h # ~170 lines (more than expected, but still minimal)
│ ├── config.h # Platform configuration
│ ├── advanced_config.h # Default values for triangulation etc.
│ └── wx/ # Stub wx headers
│ ├── debug.h
│ ├── log.h
│ ├── string.h
│ └── confbase.h
├── src/
│ └── test_kimath.cpp
├── CMakeLists.txt
└── build/
├── libkimath.a # 1.4 MB static library
├── libclipper2.a
├── libkicad_core_utils.a
└── test_kimath # Working test executable
```
**Key learnings:**
- wx_shim.h needed to be ~170 lines, not ~50, due to:
- `wxString::Format()` with varargs required a proper class with template Format method
- `FormatArg<T>` template needed to convert string args to `c_str()` for snprintf
- `wxLog::EnableLogging()` used in polygon_triangulation.h
- `wxString::RemoveLast()` used for string manipulation
- `ADVANCED_CFG` class needed for triangulation settings
- C++20 required (not C++17) due to KiCad's use of concepts
- Build order: Our `core/include/` must come FIRST in include paths
**Test output:**
```
Testing kimath standalone build...
Created VECTOR2I: (0,0) and (100,100)
SEG length: 141
Created polygon with 1 outline(s)
Polygon area: 1e+06
kimath standalone build: SUCCESS!
```
---
### ✅ Step 5 (partial): Emscripten Build (COMPLETE)
**Date**: 2025-11-26
Successfully compiled kimath to WebAssembly:
```bash
$ emcmake cmake .. && emmake make
$ node test_kimath.js
Testing kimath standalone build...
Created VECTOR2I: (0,0) and (100,100)
SEG length: 141
Created polygon with 1 outline(s)
Polygon area: 1e+06
kimath standalone build: SUCCESS!
```
**Build artifacts:**
```
build-wasm/
├── test_kimath.wasm # 845KB - WASM module
├── test_kimath.js # 154KB - JS glue code
├── libkimath.a # 5.1MB - Static WASM library
├── libclipper2.a # 1.2MB
└── libkicad_core_utils.a # 106KB
```
**Key findings:**
- No code changes needed between native and WASM builds
- Same wx_shim.h works for both targets
- WASM module runs identically to native in Node.js
---
### ✅ Step 2 (partial): S-expression Parser (COMPLETE)
**Date**: 2025-11-26
Added libs/sexpr to the standalone build:
**Additional stubs needed:**
- `wx/file.h`, `wx/ffile.h` - file I/O stubs
- `wxFFile` class in wx_shim.h (~80 lines)
- `string_utils.h` - minimal stub for `From_UTF8()`
**Test output (both native and WASM):**
```
Testing S-expression parser...
Parsed S-expr with 4 elements
Root element: kicad_pcb
```
**WASM sizes:**
```
test_kimath.wasm - 867KB (geometry + sexpr parser)
libkimath.a - 5.1MB
libsexpr.a - 159KB
libclipper2.a - 1.2MB
```
---
## Success Criteria
- [x] wx_shim.h provides all needed wx replacements
- [x] libs/kimath compiles with shim (no wxWidgets linked)
- [x] kicad_core.wasm builds with Emscripten (kimath portion)
- [x] libs/sexpr compiles with shim (added wxFFile, string_utils stubs)
- [x] S-expression parser can parse .kicad_pcb format strings
- [ ] S-expression parser can load .kicad_pcb from string
- [ ] Board data model extracts cleanly
- [ ] C API wrapper builds as native static library
- [ ] kicad_core.wasm builds with Emscripten
- [ ] Node.js can load a .kicad_pcb file via WASM
- [ ] DRC runs in WASM, outputs violation list
- [ ] Router API works in WASM
## First Concrete Step
Start with Step 1: Create `core/` directory with `wx_shim.h` and attempt to compile just `libs/kimath` standalone. This proves the shim approach works before tackling the larger board model.
## Dependencies
**Must include in WASM build:**
- Clipper2 library (polygon boolean operations) - pure C++
- RTree (spatial indexing) - header-only
**Not needed:**
- wxWidgets (replaced by shim)
- Boost (only header-only templates used)
- OpenCASCADE, ngspice, curl, libgit2 (already disabled in Phase 1)