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>
This commit is contained in:
Viktor Vaczi 2025-11-26 17:39:53 +01:00
commit 92faf7c082
17 changed files with 1068 additions and 0 deletions

3
core/.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
# Build directories
build/
build-wasm/

177
core/CMakeLists.txt Normal file
View file

@ -0,0 +1,177 @@
cmake_minimum_required(VERSION 3.16)
project(kicad_core VERSION 1.0 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# Path to KiCad source (submodule)
set(KICAD_SOURCE "${CMAKE_SOURCE_DIR}/../kicad" CACHE PATH "Path to KiCad source")
# Verify KiCad source exists
if(NOT EXISTS "${KICAD_SOURCE}/libs/kimath")
message(FATAL_ERROR "KiCad source not found at ${KICAD_SOURCE}")
endif()
message(STATUS "Using KiCad source at: ${KICAD_SOURCE}")
# =============================================================================
# Options
# =============================================================================
option(KICAD_CORE_VERBOSE "Enable verbose logging in core library" OFF)
# =============================================================================
# Clipper2 library (for polygon boolean operations)
# =============================================================================
set(CLIPPER2_DIR "${KICAD_SOURCE}/thirdparty/clipper2")
add_library(clipper2 STATIC
${CLIPPER2_DIR}/Clipper2Lib/src/clipper.engine.cpp
${CLIPPER2_DIR}/Clipper2Lib/src/clipper.offset.cpp
${CLIPPER2_DIR}/Clipper2Lib/src/clipper.rectclip.cpp
)
target_include_directories(clipper2 PUBLIC
${CLIPPER2_DIR}/Clipper2Lib/include
)
target_compile_definitions(clipper2 PUBLIC USINGZ)
# =============================================================================
# RTree library (header-only spatial index)
# =============================================================================
add_library(rtree INTERFACE)
target_include_directories(rtree INTERFACE
${KICAD_SOURCE}/thirdparty/rtree
)
# =============================================================================
# Core utilities library (minimal version without wx_stl_compat)
# =============================================================================
add_library(kicad_core_utils STATIC
${KICAD_SOURCE}/libs/core/base64.cpp
${KICAD_SOURCE}/libs/core/observable.cpp
${KICAD_SOURCE}/libs/core/profile.cpp
# Skip utf8.cpp and wx_stl_compat.cpp - they have heavy wx dependencies
)
target_include_directories(kicad_core_utils PUBLIC
${KICAD_SOURCE}/libs/core/include
${CMAKE_CURRENT_SOURCE_DIR}/include # For wx_shim.h
)
# =============================================================================
# S-expression library (parser for .kicad_pcb files)
# =============================================================================
add_library(sexpr STATIC
${KICAD_SOURCE}/libs/sexpr/sexpr.cpp
${KICAD_SOURCE}/libs/sexpr/sexpr_parser.cpp
)
target_include_directories(sexpr PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/include # wx_shim.h - FIRST!
${KICAD_SOURCE}/libs/sexpr/include
${KICAD_SOURCE}/libs/core/include
)
target_link_libraries(sexpr PUBLIC kicad_core_utils)
# =============================================================================
# KiMath library (geometry and math)
# =============================================================================
set(KIMATH_SRCS
${KICAD_SOURCE}/libs/kimath/src/bezier_curves.cpp
${KICAD_SOURCE}/libs/kimath/src/convert_basic_shapes_to_polygon.cpp
${KICAD_SOURCE}/libs/kimath/src/md5_hash.cpp
${KICAD_SOURCE}/libs/kimath/src/transform.cpp
${KICAD_SOURCE}/libs/kimath/src/trigo.cpp
${KICAD_SOURCE}/libs/kimath/src/geometry/corner_operations.cpp
${KICAD_SOURCE}/libs/kimath/src/geometry/distribute.cpp
${KICAD_SOURCE}/libs/kimath/src/geometry/eda_angle.cpp
${KICAD_SOURCE}/libs/kimath/src/geometry/ellipse.cpp
${KICAD_SOURCE}/libs/kimath/src/geometry/circle.cpp
${KICAD_SOURCE}/libs/kimath/src/geometry/convex_hull.cpp
${KICAD_SOURCE}/libs/kimath/src/geometry/direction_45.cpp
${KICAD_SOURCE}/libs/kimath/src/geometry/geometry_utils.cpp
${KICAD_SOURCE}/libs/kimath/src/geometry/half_line.cpp
${KICAD_SOURCE}/libs/kimath/src/geometry/intersection.cpp
${KICAD_SOURCE}/libs/kimath/src/geometry/line.cpp
${KICAD_SOURCE}/libs/kimath/src/geometry/nearest.cpp
${KICAD_SOURCE}/libs/kimath/src/geometry/oval.cpp
${KICAD_SOURCE}/libs/kimath/src/geometry/roundrect.cpp
${KICAD_SOURCE}/libs/kimath/src/geometry/seg.cpp
${KICAD_SOURCE}/libs/kimath/src/geometry/shape.cpp
${KICAD_SOURCE}/libs/kimath/src/geometry/shape_arc.cpp
${KICAD_SOURCE}/libs/kimath/src/geometry/shape_collisions.cpp
${KICAD_SOURCE}/libs/kimath/src/geometry/shape_compound.cpp
${KICAD_SOURCE}/libs/kimath/src/geometry/shape_file_io.cpp
${KICAD_SOURCE}/libs/kimath/src/geometry/shape_line_chain.cpp
${KICAD_SOURCE}/libs/kimath/src/geometry/shape_nearest_points.cpp
${KICAD_SOURCE}/libs/kimath/src/geometry/shape_poly_set.cpp
${KICAD_SOURCE}/libs/kimath/src/geometry/shape_rect.cpp
${KICAD_SOURCE}/libs/kimath/src/geometry/shape_segment.cpp
${KICAD_SOURCE}/libs/kimath/src/geometry/vector_utils.cpp
${KICAD_SOURCE}/libs/kimath/src/geometry/shape_utils.cpp
${KICAD_SOURCE}/libs/kimath/src/geometry/vertex_set.cpp
${KICAD_SOURCE}/libs/kimath/src/math/vector2.cpp
${KICAD_SOURCE}/libs/kimath/src/math/util.cpp
)
add_library(kimath STATIC ${KIMATH_SRCS})
target_include_directories(kimath PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/include # wx_shim.h and wx/ stubs - FIRST!
${KICAD_SOURCE}/libs/kimath/include
${KICAD_SOURCE}/libs/core/include
${KICAD_SOURCE}/include # Main KiCad includes (units, etc.)
)
target_link_libraries(kimath PUBLIC
kicad_core_utils
clipper2
rtree
)
# Define KICAD_CORE_ONLY to enable any conditional compilation
target_compile_definitions(kimath PRIVATE
KICAD_CORE_ONLY=1
)
if(KICAD_CORE_VERBOSE)
target_compile_definitions(kimath PRIVATE KICAD_CORE_VERBOSE=1)
endif()
# =============================================================================
# Combined kicad_core library
# =============================================================================
add_library(kicad_core INTERFACE)
target_link_libraries(kicad_core INTERFACE
kimath
sexpr
kicad_core_utils
clipper2
rtree
)
# =============================================================================
# Test executable (optional)
# =============================================================================
option(BUILD_TESTS "Build test executables" ON)
if(BUILD_TESTS)
add_executable(test_kimath
${CMAKE_CURRENT_SOURCE_DIR}/src/test_kimath.cpp
)
target_link_libraries(test_kimath PRIVATE kimath sexpr)
endif()
# =============================================================================
# Summary
# =============================================================================
message(STATUS "")
message(STATUS "kicad_core configuration:")
message(STATUS " KiCad source: ${KICAD_SOURCE}")
message(STATUS " Verbose logging: ${KICAD_CORE_VERBOSE}")
message(STATUS " Build tests: ${BUILD_TESTS}")
message(STATUS "")

View file

@ -0,0 +1,70 @@
/*
* advanced_config.h - Stub for kicad-core standalone build
*
* Provides default configuration values without wx dependencies
*/
#ifndef KICAD_CORE_ADVANCED_CONFIG_H
#define KICAD_CORE_ADVANCED_CONFIG_H
// Stub ADVANCED_CFG class with default values
class ADVANCED_CFG {
public:
// Triangulation settings (defaults from KiCad)
int m_TriangulateSimplificationLevel = 50;
double m_TriangulateMinimumArea = 1000.0;
// Other settings that might be accessed
bool m_EnableLibWithText = false;
bool m_EnableEeschemaPrintCairo = false;
int m_UpdateUIEventInterval = 0;
double m_DrawArcAccuracy = 0.005;
double m_DrawArcCenterMaxAngle = 50.0;
int m_MaxUndoItems = 0;
double m_MinPlotPenWidth = 0.0;
int m_3DRT_BevelExtentFactor = 1;
int m_3DRT_BevelHeight_um = 30;
bool m_ShowRepairSchematic = false;
bool m_ShowPropertiesPanel = true;
bool m_ShowEventCounters = false;
bool m_AllowManualCanvasScale = false;
double m_CanvasScale = 1.0;
bool m_CompactSave = false;
int m_CoroutineStackSize = 0;
int m_DrawBoundingBoxes = 0;
bool m_ShowPcbnewExportNetlist = false;
bool m_Skip3DModelFileCache = false;
bool m_Skip3DModelMemoryCache = false;
bool m_HideVersionFromTitle = false;
bool m_TraceMasks = false;
bool m_ShowRouterDebugGraphics = false;
bool m_ExtraZoneDisplayModes = false;
double m_MinClrDistance = 0.0;
bool m_DebugZoneFiller = false;
bool m_DebugPDFWriter = false;
int m_HotkeysDumper = 0;
bool m_DrawTriangulationOutlines = false;
bool m_StrokeTriangulation = false;
bool m_ExtraClearance = false;
double m_SmallDrillMarkSize = 0.0;
int m_HoleijWallThickness = 0;
int m_MaxTangentAngleDeviation = 1;
int m_MaxClearanceDistanceFactor = 2;
int m_ViaijFillMinRatio = 0;
bool m_RealtimeConnectivity = true;
bool m_EnableCacheFriendlyFracture = true;
double m_FontErrorSize = 0.0;
double m_OcctVerbosity = 0.0;
bool m_Use3DConnexionDriver = false;
bool m_IncrementalConnectivity = true;
static ADVANCED_CFG& GetCfg() {
static ADVANCED_CFG instance;
return instance;
}
private:
ADVANCED_CFG() = default;
};
#endif // KICAD_CORE_ADVANCED_CONFIG_H

31
core/include/config.h Normal file
View file

@ -0,0 +1,31 @@
/*
* config.h - Minimal config for kicad-core standalone build
*
* This replaces the auto-generated config.h from KiCad's CMake
*/
#ifndef KICAD_CORE_CONFIG_H
#define KICAD_CORE_CONFIG_H
// Platform detection for timing functions
#if defined(_WIN32)
// Windows uses GetSystemTimeAsFileTime
#elif defined(__APPLE__) || defined(__linux__) || defined(__unix__)
#define HAVE_CLOCK_GETTIME 1
#else
#define HAVE_GETTIMEOFDAY_FUNC 1
#endif
// Version info
#define KICAD_MAJOR_VERSION 8
#define KICAD_MINOR_VERSION 0
#define KICAD_PATCH_VERSION 0
#define KICAD_VERSION_FULL "8.0.0-wasm"
// Feature flags - all disabled for core-only build
#define KICAD_USE_CURL 0
#define KICAD_USE_GIT 0
#define KICAD_USE_OCC 0
#define KICAD_USE_NGSPICE 0
#endif // KICAD_CORE_CONFIG_H

View file

@ -0,0 +1,19 @@
/*
* string_utils.h - Minimal stub for kicad-core
*
* The sexpr library only uses From_UTF8() which is defined in wx_shim.h
*/
#ifndef KICAD_CORE_STRING_UTILS_H
#define KICAD_CORE_STRING_UTILS_H
#include "wx_shim.h"
// From_UTF8 is already defined in wx_shim.h
// Additional string utilities that might be needed
inline std::string To_UTF8(const wxString& str) {
return str;
}
#endif // KICAD_CORE_STRING_UTILS_H

View file

@ -0,0 +1,27 @@
// Stub wx/confbase.h - minimal config base for kicad-core
#ifndef _WX_CONFBASE_H_
#define _WX_CONFBASE_H_
#include "wx_shim.h"
// Minimal wxConfigBase stub
class wxConfigBase {
public:
virtual ~wxConfigBase() = default;
// Minimal interface - returns false/empty for everything
virtual bool Read(const wxString& key, wxString* str) const { return false; }
virtual bool Read(const wxString& key, long* val) const { return false; }
virtual bool Read(const wxString& key, double* val) const { return false; }
virtual bool Read(const wxString& key, bool* val) const { return false; }
virtual bool Write(const wxString& key, const wxString& value) { return false; }
virtual bool Write(const wxString& key, long value) { return false; }
virtual bool Write(const wxString& key, double value) { return false; }
virtual bool Write(const wxString& key, bool value) { return false; }
static wxConfigBase* Get(bool createOnDemand = true) { return nullptr; }
static wxConfigBase* Set(wxConfigBase* config) { return nullptr; }
};
#endif // _WX_CONFBASE_H_

5
core/include/wx/debug.h Normal file
View file

@ -0,0 +1,5 @@
// Stub wx/debug.h - redirects to wx_shim.h
#ifndef _WX_DEBUG_H_
#define _WX_DEBUG_H_
#include "wx_shim.h"
#endif

5
core/include/wx/ffile.h Normal file
View file

@ -0,0 +1,5 @@
// Stub wx/ffile.h - redirects to wx_shim.h
#ifndef _WX_FFILE_H_
#define _WX_FFILE_H_
#include "wx_shim.h"
#endif

5
core/include/wx/file.h Normal file
View file

@ -0,0 +1,5 @@
// Stub wx/file.h - redirects to wx_shim.h
#ifndef _WX_FILE_H_
#define _WX_FILE_H_
#include "wx_shim.h"
#endif

5
core/include/wx/log.h Normal file
View file

@ -0,0 +1,5 @@
// Stub wx/log.h - redirects to wx_shim.h
#ifndef _WX_LOG_H_
#define _WX_LOG_H_
#include "wx_shim.h"
#endif

5
core/include/wx/string.h Normal file
View file

@ -0,0 +1,5 @@
// Stub wx/string.h - redirects to wx_shim.h
#ifndef _WX_STRING_H_
#define _WX_STRING_H_
#include "wx_shim.h"
#endif

264
core/include/wx_shim.h Normal file
View file

@ -0,0 +1,264 @@
/*
* wx_shim.h - Minimal wxWidgets compatibility layer for kicad-core
*
* Provides drop-in replacements for wx utility macros/functions used in
* KiCad's core computation libraries. This is NOT a port of wxWidgets -
* just ~100 lines of standard C++ that replaces debug/logging utilities.
*
* Used macros in kimath:
* - wxASSERT, wxASSERT_MSG
* - wxCHECK, wxCHECK_MSG, wxCHECK2_MSG, wxCHECK_RET
* - wxFAIL_MSG
* - wxLogTrace, wxLogDebug, wxLogWarning
* - wxString, wxString::Format
* - wxT()
*/
#ifndef KICAD_WX_SHIM_H
#define KICAD_WX_SHIM_H
#include <cassert>
#include <string>
#include <cstdio>
#include <cstdarg>
#include <sstream>
// =============================================================================
// Assertions
// =============================================================================
#define wxASSERT(cond) assert(cond)
#define wxASSERT_MSG(cond, msg) assert(cond) // msg ignored in release
// wxCHECK variants - check condition, return if false
#define wxCHECK(cond, ret) do { if(!(cond)) return ret; } while(0)
#define wxCHECK_MSG(cond, ret, msg) do { if(!(cond)) return ret; } while(0)
#define wxCHECK_RET(cond, msg) do { if(!(cond)) return; } while(0)
#define wxCHECK2(cond, op) do { if(!(cond)) { op; } } while(0)
#define wxCHECK2_MSG(cond, op, msg) do { if(!(cond)) { op; } } while(0)
#define wxFAIL_MSG(msg) assert(false) // msg ignored in standalone build
// =============================================================================
// Logging - mostly no-ops for core library
// =============================================================================
// Trace logging - typically disabled in release builds anyway
#define wxLogTrace(...) ((void)0)
#define wxLogDebug(...) ((void)0)
// Warnings - optionally print to stderr
#ifdef KICAD_CORE_VERBOSE
#define wxLogWarning(fmt, ...) fprintf(stderr, "Warning: " fmt "\n", ##__VA_ARGS__)
#else
#define wxLogWarning(...) ((void)0)
#endif
// Variable argument version
inline void wxVLogWarning(const char* format, va_list args) {
#ifdef KICAD_CORE_VERBOSE
vfprintf(stderr, format, args);
fprintf(stderr, "\n");
#else
(void)format;
(void)args;
#endif
}
// Log level checking - always returns false (logging disabled)
namespace wxLog {
inline bool IsLevelEnabled(int, const std::string&) { return false; }
inline void EnableLogging(bool enable = true) { (void)enable; }
inline bool IsLoggingEnabled() { return false; }
}
// Log level constants
constexpr int wxLOG_Debug = 0;
// Log component macro
#ifndef wxLOG_COMPONENT
#define wxLOG_COMPONENT "kicad-core"
#endif
// =============================================================================
// String utilities
// =============================================================================
// wxChar type
using wxChar = char;
// wxT() macro - pass through (we're always using UTF-8)
#define wxT(x) x
// wxASCII_STR for older wx compatibility
#define wxASCII_STR(s) std::string(s)
// Helper to convert args for snprintf - strings need c_str()
template<typename T>
struct FormatArg {
static auto convert(const T& arg) { return arg; }
};
template<>
struct FormatArg<std::string> {
static const char* convert(const std::string& arg) { return arg.c_str(); }
};
// Forward declaration for wxString specialization
class wxString;
template<>
struct FormatArg<wxString> {
static const char* convert(const wxString& arg);
};
// wxString class with static Format method
class wxString : public std::string {
public:
// Inherit constructors
using std::string::string;
// Additional constructors for compatibility
wxString() : std::string() {}
wxString(const std::string& s) : std::string(s) {}
wxString(const char* s) : std::string(s ? s : "") {}
// Static Format method - converts string args to c_str()
template<typename... Args>
static wxString Format(const char* fmt, Args... args) {
char buf[2048];
snprintf(buf, sizeof(buf), fmt, FormatArg<Args>::convert(args)...);
return wxString(buf);
}
static wxString Format(const wxString& fmt) {
return fmt;
}
// FromAscii static method
static wxString FromAscii(const char* s) {
return wxString(s ? s : "");
}
// c_str() for compatibility (inherited from std::string but explicit)
const char* c_str() const { return std::string::c_str(); }
// RemoveLast - removes last n characters
void RemoveLast(size_t n = 1) {
if (n <= size()) {
resize(size() - n);
} else {
clear();
}
}
// Truncate - truncate to given length
void Truncate(size_t len) {
if (len < size()) {
resize(len);
}
}
};
// Define the wxString FormatArg specialization now that wxString is defined
inline const char* FormatArg<wxString>::convert(const wxString& arg) {
return arg.c_str();
}
// Empty string constant
inline const wxString wxEmptyString = wxString("");
// =============================================================================
// File I/O utilities
// =============================================================================
#include <fstream>
// wxFFile - File wrapper class
class wxFFile {
FILE* m_fp = nullptr;
bool m_close = false;
public:
wxFFile() = default;
wxFFile(const wxString& filename, const char* mode = "r") {
Open(filename, mode);
}
~wxFFile() { Close(); }
bool Open(const wxString& filename, const char* mode = "r") {
Close();
m_fp = fopen(filename.c_str(), mode);
m_close = (m_fp != nullptr);
return m_fp != nullptr;
}
bool IsOpened() const { return m_fp != nullptr; }
void Close() {
if (m_fp && m_close) {
fclose(m_fp);
}
m_fp = nullptr;
m_close = false;
}
size_t Read(void* buffer, size_t count) {
if (!m_fp) return 0;
return fread(buffer, 1, count, m_fp);
}
size_t Write(const void* buffer, size_t count) {
if (!m_fp) return 0;
return fwrite(buffer, 1, count, m_fp);
}
bool ReadAll(wxString* str) {
if (!m_fp || !str) return false;
// Get file size
long pos = ftell(m_fp);
fseek(m_fp, 0, SEEK_END);
long size = ftell(m_fp);
fseek(m_fp, pos, SEEK_SET);
// Read content
str->resize(size);
size_t read = fread(&(*str)[0], 1, size, m_fp);
str->resize(read);
return read > 0;
}
size_t Length() const {
if (!m_fp) return 0;
long pos = ftell(m_fp);
fseek(m_fp, 0, SEEK_END);
long size = ftell(m_fp);
fseek(m_fp, pos, SEEK_SET);
return static_cast<size_t>(size);
}
bool Eof() const {
return m_fp ? feof(m_fp) != 0 : true;
}
bool Seek(long offset, int origin = SEEK_SET) {
return m_fp ? fseek(m_fp, offset, origin) == 0 : false;
}
long Tell() const {
return m_fp ? ftell(m_fp) : -1;
}
};
// From_UTF8 - passthrough for UTF-8 strings
inline wxString From_UTF8(const char* s) {
return wxString(s ? s : "");
}
// =============================================================================
// Replacement for wx headers
// =============================================================================
// Empty stubs for wx includes - these get included but do nothing
// Create empty headers in core/include/wx/
#endif // KICAD_WX_SHIM_H

57
core/src/test_kimath.cpp Normal file
View file

@ -0,0 +1,57 @@
/*
* test_kimath.cpp - Test kimath and sexpr without wxWidgets
*/
#include <iostream>
#include <math/vector2d.h>
#include <geometry/seg.h>
#include <geometry/shape_poly_set.h>
#include <sexpr/sexpr.h>
#include <sexpr/sexpr_parser.h>
int main() {
std::cout << "Testing kimath standalone build...\n";
// Test VECTOR2I
VECTOR2I p1(0, 0);
VECTOR2I p2(100, 100);
std::cout << "Created VECTOR2I: (" << p1.x << "," << p1.y << ") and ("
<< p2.x << "," << p2.y << ")\n";
// Test SEG
SEG segment(p1, p2);
int length = segment.Length();
std::cout << "SEG length: " << length << "\n";
// Test SHAPE_POLY_SET
SHAPE_POLY_SET poly;
poly.NewOutline();
poly.Append(0, 0);
poly.Append(1000, 0);
poly.Append(1000, 1000);
poly.Append(0, 1000);
std::cout << "Created polygon with " << poly.OutlineCount() << " outline(s)\n";
std::cout << "Polygon area: " << poly.Area() << "\n";
// Test S-expression parser
std::cout << "\nTesting S-expression parser...\n";
std::string test_sexpr = "(kicad_pcb (version 20231014) (generator \"test\") (layer F.Cu front copper))";
SEXPR::PARSER parser;
std::unique_ptr<SEXPR::SEXPR> parsed = parser.Parse(test_sexpr);
if (parsed && parsed->IsList()) {
const SEXPR::SEXPR_VECTOR* list = parsed->GetChildren();
std::cout << "Parsed S-expr with " << list->size() << " elements\n";
if (!list->empty() && (*list)[0]->IsSymbol()) {
std::cout << "Root element: " << (*list)[0]->GetSymbol() << "\n";
}
} else {
std::cout << "Failed to parse S-expression\n";
return 1;
}
std::cout << "\nkimath + sexpr standalone build: SUCCESS!\n";
return 0;
}

View file

@ -0,0 +1,395 @@
# 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)