OpenSKP Documentation
The open-source SketchUp (.skp) file toolkit — parse, write, and convert .skp files, natively in Python, TypeScript, .NET, Dart, and C++. Convert SketchUp models to glTF (GLB), OBJ, STL, PLY, DXF, IFC4, and JSON with no SketchUp SDK, no license, both the modern VFF (2021+) and classic MFC (2013–2020) formats supported. This page is the detailed, verified developer reference; the full write-ups live in docs/ on GitHub.
5
Languages
7
Convert-to formats
179
Tests passing
620MB
Largest file verified
2
Container formats (VFF + legacy MFC)
Installation
Pick your language — the API is equivalent across all five
bash
pip install openskp
python
from openskp import SkpFile
model = SkpFile.open("my_model.skp").parse()
print(model.version, len(model.layers))
# Opt-in: full placed scene graph, triangulated, world-space, GLB-ready
scene = SkpFile.open("my_model.skp").build_scene()
print(len(scene.glb_primitives), "mesh primitives")
bash
npm install openskp
typescript
import { SkpFile, toGLB } from 'openskp'
// Node.js
const model = SkpFile.open('my_model.skp').parse()
// Browser - same package, isomorphic
// const buffer = await fetch('my_model.skp').then(r => r.arrayBuffer())
// const model = parseSkp(buffer)
const scene = SkpFile.open('my_model.skp').buildScene()
const glb = toGLB(scene) // ready to write to a .glb file
bash
dotnet add package OpenSkp
csharp
using OpenSkp;
var model = SkpFile.Open("my_model.skp");
Console.WriteLine($"{model.Version} - {model.Layers.Count} layers");
var scene = SkpFile.BuildScene("my_model.skp");
Console.WriteLine($"{scene.GlbPrimitives.Count} mesh primitives");
bash
dart pub add openskp
dart
import 'package:openskp/openskp.dart';
final model = SkpFile.open('my_model.skp').parse();
print('${model.version} - ${model.layers.length} layers');
final scene = SkpFile.open('my_model.skp').buildScene();
print('${scene.glbPrimitives.length} mesh primitives');
cmake
find_package(OpenSkp CONFIG REQUIRED)
target_link_libraries(your_target PRIVATE OpenSkp::OpenSkp)
cpp
#include <openskp/openskp.hpp>
auto skp = openskp::SkpFile::open("my_model.skp");
auto model = skp.parse();
auto scene = skp.build_scene();
auto glb = openskp::to_glb(scene);
openskp::export_glb(scene, "my_model.glb");
std::cout << scene.glb_primitives.size() << " mesh primitives\n";
No package registry today - build/install from source (or download a release tarball) via CMake, then
find_package(OpenSkp CONFIG REQUIRED). See GitHub Releases for tagged source packages.Core Concepts
Two entry points, and why they're separate
parse()
Light, defaultReads each component/group definition's geometry exactly once — vertices, edges, faces, and the un-resolved instance placements (which definition, what transform). No scene-graph walking, no triangulation. This is what you want for metadata inspection, custom geometry processing, or anything that doesn't need a renderable mesh.
buildScene()
Opt-in, heavierWalks the entire placed scene graph: every instance of every component, nested arbitrarily deep, each with its transform resolved to world space, each face triangulated (a ported earcut — the same algorithm in all five languages) and grouped by resolved color into GLB-ready mesh primitives. For a file that reuses a handful of definitions across many thousands of placements, this can produce far more data than the file's raw geometry — that's why it's a separate, opt-in call.
Calling both
parse() and buildScene() re-parses the raw TLV data twice — a deliberate trade of extra CPU time for guaranteeing parse() alone never pays for scene-baking's cost. They're independent, not layered.Creating Files
create() — build a new .skp file from nothing, all five languagesOpenSKP can also go the other direction:
create() returns an SkpBuilder that assembles a genuine legacy MFC CArchive-format .skp file (SketchUp 2013–2020) from nothing — geometry, materials, layers, component definitions, and groups — with no SketchUp SDK involved at import, build, or save time. It works by inverting this project's own reader logic against a small bundled blank-document scaffold.Every feature on this page has been validated feature-by-feature against the real SketchUp SDK (
SketchUpAPI.dll), not just against this project's own reader — and it holds up rebuilding complex, real architectural models, not only synthetic test fixtures.Landed in Python first, then ported to TypeScript, .NET, Dart, and C++ — the identical feature set is now available in all five languages (1.1.0). Combined with the export formats below, this makes OpenSKP a genuine SketchUp file converter in both directions this project targets: build a
.skp from nothing, or convert an existing one to glTF/OBJ/STL/PLY/DXF/IFC4/JSON.Materials, layers & geometry
Solid-color and PNG/JPEG-textured materials, and named layers with their own color and default visibility — reused by name across every face, instance, and group you place.
python
from openskp import create
builder = create()
red = builder.add_material("Red", (255, 0, 0))
brick = builder.add_texture_material("Brick", "brick.png")
roof = builder.add_layer("Roof", color=(180, 60, 40))
builder.add_face(
[(0, 0, 0), (200, 0, 0), (200, 150, 0), (0, 150, 0)],
material=red, layer=roof,
)
builder.save("output.skp")
Definitions, instances & groups
Reusable component definitions with multiple positioned instances, one-off groups, and nesting to any depth — a definition can place instances or groups of its own already-built sub-parts, the same way a real SketchUp assembly does.
python
with builder.add_component_definition("Wheel") as wheel:
wheel.add_face([(0, 0, 0), (10, 0, 0), (10, 10, 0), (0, 10, 0)])
with builder.add_component_definition("Car") as car:
car.add_instance(wheel, translation=(0, 0, 0))
car.add_instance(wheel, translation=(100, 0, 0))
builder.add_instance(car, translation=(0, 0, 0))
with builder.add_group("Table", translation=(200, 0, 0)) as table:
table.add_face([(0, 0, 0), (60, 0, 0), (60, 40, 0), (0, 40, 0)])
One ordering rule falls out of the format's own slot numbering: every
add_component_definition/add_group call must happen before any add_face/add_instance call on the builder itself.Rotation & visibility
rotation=(axis, angle_radians) is a shortcut for a hand-derived matrix3x3, and every placement — instance or group — can be created hidden=True (its contents still exist in the file, just not shown by default).python
import math
builder.add_instance(wheel, translation=(0, 0, 0), rotation=((0, 0, 1), math.radians(90)))
builder.add_instance(wheel, translation=(100, 0, 0), hidden=True)
Curved geometry
add_circle/add_arc write a genuine, editable-by-radius CArcCurve entity — not disconnected straight edges that merely trace the shape. add_polyline groups an arbitrary edge chain into one real CCurve entity, the same grouping SketchUp's own Freehand tool produces.python
builder.add_circle((100, 75, 0), normal=(0, 0, 1), radius=30, num_segments=24)
builder.add_arc((100, 75, 0), normal=(0, 0, 1), radius=30, start_angle=0, end_angle=math.pi / 2)
builder.add_polyline([(0, 0, 0), (10, 10, 0), (20, 0, 0), (30, 10, 0)])
Faces: holes & non-planar input
A face can have one or more holes cut out of it (a window opening in a wall) via
holes=, and auto_triangulate=True fan-triangulates non-planar input instead of raising — the same silent fallback real SketchUp's own UI applies to a not-quite-flat quad.python
wall = [(0, 0, 0), (200, 0, 0), (200, 100, 0), (0, 100, 0)]
window = [(80, 30, 0), (120, 30, 0), (120, 70, 0), (80, 70, 0)]
builder.add_face(wall, holes=[window])
warped_quad = [(0, 0, 0), (10, 0, 0), (10, 10, 0), (0, 10, 5)]
builder.add_face(warped_quad, auto_triangulate=True) # -> 2 triangular faces
Texture positioning & custom attributes
A face's texture can be explicitly positioned (scaled, rotated, sheared, offset — independently per side) via 3 world-point/UV correspondences, on a face of any orientation. Component definitions, instances, and faces can also carry custom key/value metadata — the same mechanism SketchUp's own "dynamic component" attributes use.
python
builder.add_face(
[(0, 0, 0), (100, 0, 0), (100, 100, 0), (0, 100, 0)],
material=brick,
front_uv=[((0, 0, 0), (0.0, 0.0)), ((50, 0, 0), (1.0, 0.0)), ((0, 50, 0), (0.0, 1.0))],
)
with builder.add_component_definition("Chair", attributes={"sku": "CH-100", "price": 49.99}) as chair:
chair.add_face([(0, 0, 0), (20, 0, 0), (20, 20, 0), (0, 20, 0)])
Scope at a glance
| Feature | Status |
|---|---|
| Solid + PNG/JPEG-textured materials | ✓ |
| Layers with color & default visibility | ✓ |
| Component definitions, multi-instance, groups, nesting | ✓ |
| Instance/group rotation & hidden state | ✓ |
| Circular/arc curves & freeform polylines | ✓ |
| Faces with holes & auto-triangulation | ✓ |
| Explicit per-side texture positioning | ✓ |
| Custom attributes on definitions/instances/faces | ✓ |
| Attributes on groups | ✗ not yet |
| Modern VFF (2021+) output | ✗ legacy format only |
See
openskp/create.py for the full, current scope notes.Editing Existing Files
openskp.open_existing() — load a file that already exists, and extend itopenskp.create() only ever starts from its own blank scaffold. Real SketchUp never patches a file in place either — it fully re-serializes the whole document on every save — so there's no stable byte region to append to for an arbitrary existing file. open_existing() takes the same approach real SketchUp effectively does: parse → replay → extend → save. It fully parses the source file with this project's own reader, then replays everything it understood — materials, layers, every component definition, all root-level geometry and instances — back through the writer's own public API, producing a brand-new file with equivalent content that more geometry can still be added to.python
from openskp import open_existing
builder, warnings, definitions = open_existing("building.skp")
for w in warnings:
print("not fully reproduced:", w)
# Every material/layer the source had is already reusable, no separate lookup:
roof = builder.materials_by_name.get("Roofing")
builder.add_circle((0, 0, 100), (0, 0, 1), radius=50, material=roof)
# definitions maps each replayed component's own name to its builder:
builder.add_instance(definitions["Window"], translation=(0, 300, 0))
builder.save("building_edited.skp")
What can and can't be added afterward
The returned builder is ready for more
add_face/add_circle/add_instance/etc. calls, reusing every material and layer the source already had. What it can no longer do is register a genuinely new material, layer, or component definition/group — by the time replay finishes writing the source's own root-level geometry, this writer's usual file-format ordering requirement (materials/layers/definitions must be finalized before any geometry) is already satisfied.add_material, add_layer, add_component_definition, and add_group all raise on a builder returned by open_existing() — build anything genuinely new into a separate create() call instead.Known fidelity gaps
The returned
warnings list is the honest, per-file account of what couldn't be faithfully reproduced. Only a legacy-format (2013–2020) source is accepted, for the same reason the writer only ever produces that format. Round-trip-validated against real, non-writer-authored architectural models, not just files this project's own writer produced.| Gap | Detail |
|---|---|
| Per-edge flags | hidden/soft/smooth are applied per-face, not per-edge — an "any edge in this boundary has the flag" approximation |
| Projective textures | positioned textures replay via a 3-point affine fit — exact at those points, but a genuinely projective/distorted source mapping won't interpolate identically. A draped/projected texture falls back to the default projection |
| Texture tile size & tint | a material's original texture scale isn't preserved yet, and a colorized (tinted) material replays as its plain source texture |
| Per-face layer painting | only a face's front/back material is replayed — the reader doesn't expose a per-face layer assignment |
| Group vs. instance | every placed thing replays as a plain component instance — visually identical, but no longer shows as a "Group" in SketchUp's Outliner |
| Section planes, text, dimensions | not carried over at all — the writer has no support for these entity types |
| Curve grouping | a circle/arc/polyline's original curve grouping is lost — the reader doesn't preserve it, so it round-trips as a plain straight-edged face |
| Definition/face attributes | not reproduced — the reader's public model doesn't expose either (only an instance's own properties are) |
See
openskp/edit.py's own module docstring for the complete, itemized list and the reasoning behind each one.AI-Generated Models
Why the writer above works well as an AI coding-agent target
The writer's API is generic on purpose - no
add_chair() or add_staircase() helpers, just materials, faces, components, and instances. That turns out to make it an unusually good target for AI coding agents: no object-specific primitives library is needed for an agent to compose arbitrary shapes, since it already knows how to turn a description into geometry once the API is in context.Every model below was generated from a natural-language (or reference-photo) prompt, by two independent AI agents, using nothing but the raw
create() API documented on this page - no primitives library, no hand-authored geometry.
Armchair + side table - tapered legs, curved tessellated backrest
Executive desk - 8 nested component definitions
Smartphone - modeled directly from a product reference photo
Two more real models - a mid-century dining chair + accent table (9 component definitions, 38 meshes, 4 materials) and a small gable cottage (6 materials including translucent glass) - were generated the same way but don't have a saved render yet; both are documented with their exact stats and a real code excerpt in the full write-up below.
This isn't a roadmap item - it works today. Point your AI coding agent at this page (or any package README's Writing section) and a concrete goal in plain language, and it can write and run real
create() code against the same API documented above. See docs/AI_MODELING.md on GitHub for the full write-up, including a real code excerpt and open directions for contributors.Data Model
Structurally equivalent output across all five languages
All five languages produce the same shape for the same file — cross-validated directly against each other on real fixtures, not just against each language's own idea of what the format means. Coordinates are inches, Z-up (SketchUp's native units) in
parse()'s result; buildScene()'s output converts to meters, Y-up (glTF convention).| Concept | Python | TypeScript | .NET | Dart | C++ |
|---|---|---|---|---|---|
| definitions | dict[int|str, Definition] | Map<number, Definition> | Dictionary<long, Definition> | Map<int, Definition> | std::map<EntityId, Definition> |
| Vertex | id, x, y, z | {id, x, y, z} | Id, X, Y, Z | id, x, y, z | id, x, y, z |
| Edge | id, v1_id, v2_id, soft, smooth, hidden | camelCase | PascalCase | camelCase | snake_case (matches Python) |
| Face | id, loops, normal, material_id, back_material_id, uv_transform | camelCase | PascalCase | camelCase | snake_case (matches Python) |
| Layer | name, color_r, color_g, color_b | name, color:{r,g,b} | Name, ColorR/G/B | name, colorR/G/B | name, color (std::array<uint8_t,3>) |
The root definition
Every
.skp file has an implicit top-level definition — geometry drawn directly in the model (not inside any component/group) and the top-level placed instances. How each language exposes it is not currently uniform — see Known Differences before assuming one language's shape applies to another's.Legacy Format Support
SketchUp 2013–2020, classic MFC container
SketchUp 2021 switched
.skp's container from a classic MFC CArchive object-graph serialization (versions 8 through 2020, internally versions 13–20) to the modern VFF/ZIP container. OpenSKP reads both, transparently — SkpFile.open()/.parse() auto-detects which era a file uses by its header bytes and routes to the matching walker. There is no separate API to call for old files, and the resulting SkpModel/Scene shape is identical either way.The legacy walker was reverse-engineered independently of the public "2017 format notes" — several details (edge/loop record ordering, entity preamble structure, per-version byte-count differences between v16 and v17+) were established by clean-room analysis and cross-validated against the same models re-saved as VFF, matching face/edge counts, surface area, and bounding boxes exactly.
Legacy files cost more CPU per byte than modern VFF files (the MFC object-graph format requires resolving a shared, order-dependent slot table rather than a self-describing TLV tree) — but the same lazy, streaming architecture applies. See Performance.
Performance & Memory
The architecture change that made large real files actually work
The memory architecture
Real production
.skp files can have well over 100,000 separate component definitions. The naive approach — parse the entire file into one in-memory tree, then walk it — means peak memory scales with the whole file's node count, which is what made large files crash outright before this was fixed.All five languages now parse one top-level record at a time: a cheap flat header scan (O(sibling count), not O(total node count)) finds each top-level definition/layer-manager/material-manager/root block, fully builds only that one record's subtree, hands it to the caller, and lets it be garbage-collected before the next one is built. Peak memory during the walk is bounded by the size of the single largest top-level record, not the file's total size. The per-tag extraction logic was untouched — only the orchestration loop changed.
.NET's additional fix: the CLR's array and
MemoryStream types have a hard ~2.1GB ceiling regardless of GC settings, and a decompressed model.dat can exceed that (SketchUp's format commonly compresses at ~10x). This needed a genuine architecture addition: ChunkedBuffer (a multi-segment byte buffer) plus widening every TLV offset from int to long. As a result, .NET has no practical file-size ceiling today — verified against a 620MB real file (153,586 definitions) with zero special configuration.Verified numbers (real files)
| Language | File size | Definitions | Config needed | Time |
|---|---|---|---|---|
| .NET | 620 MB | 153,586 | none | ~230–270s parse, ~17s scene build |
| Python | 294 MB | 336,254 | none | ~400s |
| Dart | 294 MB | 336,253 | --old_gen_heap_size=4096 | ~82s |
| TypeScript | 18.5 MB | 1,264 | none | ~4s |
| TypeScript | 113 MB | 132,879 | --max-old-space-size=16384 | ~34s |
| TypeScript | 294 MB | 336,254 | — | fails even at 16GB heap |
Python and .NET need no configuration regardless of file size in the files tested. Dart and TypeScript run on their own VM/engine heap, which defaults to a few GB — for files past roughly 50–100MB, raise it:
bash
# Dart
DART_VM_OPTIONS="--old_gen_heap_size=4096" dart run your_script.dart
# Node.js
node --max-old-space-size=8192 your-script.js
TypeScript's ceiling is a real, currently open limitation — not just "needs a bigger flag." A 113MB file needed somewhere between 8GB and 16GB of heap, and a 294MB file failed even at 16GB. This is very likely V8's per-object memory overhead on the millions of individual small
{id,x,y,z}-shaped objects a large file's vertices/edges/faces become. A browser tab has no equivalent of --max-old-space-size — the practical ceiling there is lower still (confirmed directly: a 113MB file hangs a browser tab outright). Practical guidance: TypeScript is solid up to the tens-of-MB / low-hundreds-of-thousands-of-definitions range; for larger files, prefer Python or .NET, or process server-side rather than in a browser tab.Observability
Opt-in progress reporting and structured, location-carrying errors — silent by default
Every port exposes the same two things about a parse or scene-bake in progress: progress (how far through the file the walk has gotten) and structured errors (exactly where a failure happened, if one does). Neither is on by default — OpenSKP never prints or logs anything unless you ask, and you wire it into whatever logging/monitoring your own application already uses.
| Stage | Meaning |
|---|---|
| header | File doesn't start with the VFF magic marker — not a .skp file, or unrecognized format |
| zip_extract | Valid header but no embedded ZIP found, or no model.dat entry (VFF path only) |
| tlv_walk | Failure walking model.dat's top-level records (modern VFF path). Carries recordIndex/totalRecords/tag |
| legacy_walk | Failure walking the classic MFC CArchive object stream |
| legacy_defs | Failure converting walked legacy objects into definitions. Carries definitionId |
| build_scene | Failure baking placed instances into a scene — almost always a triangulation failure. Carries definitionId |
Per-language mechanism
Python uses the standard
logging module (logging.getLogger("openskp")) — progress is reported as DEBUG-level log records rather than a second parallel mechanism, matching Python's ecosystem convention. TypeScript/.NET/Dart don't have an equivalent stdlib-blessed logging façade, so they use an explicit options object with onProgress/onLog callbacks (IProgress<T>-based in .NET).python
import logging
logging.basicConfig(level=logging.DEBUG)
logging.getLogger("openskp").setLevel(logging.DEBUG)
model = SkpFile.open("model.skp").parse() # now logs progress/stages
typescript
const model = SkpFile.open("model.skp").parse({
onProgress: (info) => console.log(`${info.stage}: ${info.current}/${info.total}`),
onLog: (level, message) => console.log(`[${level}] ${message}`),
})
csharp
var options = new SkpParseOptions {
Progress = new Progress<SkpParseProgress>(p => Console.WriteLine($"{p.Stage}: {p.Current}/{p.Total}")),
OnLog = (level, msg) => Console.WriteLine($"[{level}] {msg}"),
};
var model = SkpFile.Open("model.skp", options);
dart
final model = SkpFile.open("model.skp").parse(ParseOptions(
onProgress: (info) => print('${info.stage}: ${info.current}/${info.total}'),
onLog: (level, message) => print('[$level] $message'),
));
Progress fires every 500 units (records/definitions/instances), plus once more at the very last unit — coarse enough to cost nothing on 100,000+ definition files, frequent enough to catch a stuck pipeline well before a human gives up waiting. See the full Observability guide on GitHub for the complete field reference and design rationale.
Error Handling
Structured, location-carrying — never a bare string
SkpParseError / SkpParseException
Every failure anywhere in the parse or scene-build path raises/throws this structured type. The original error is always preserved — Python's
__cause__ (via raise ... from exc), TypeScript's .cause, .NET's .InnerException, Dart's .cause — so adding location context never means losing the stack trace that actually explains the bug.| Field | Set for | Meaning |
|---|---|---|
| stage | always | One of the six stages above |
| recordIndex / totalRecords | tlv_walk | 0-based index / total count — "N of M" position |
| tag | tlv_walk | The TLV tag hex string (e.g. "7C15") being processed |
| definitionId | legacy_defs, build_scene | The definition being built when the failure happened |
python
from openskp import SkpFile, SkpParseError
try:
model = SkpFile.open("model.skp").parse()
except SkpParseError as e:
print(f"parse failed: {e}") # includes stage=... record=.../... etc.
print(f"caused by: {e.__cause__}")
Convert & Export Capabilities
Convert a .skp file to 7 formats, natively, in all five languages
This is where OpenSKP becomes a real SketchUp file converter, not just a reader:
buildScene()'s result (Scene, GlbPrimitive[], gltfMaterials) is already exactly the data a converter needs — triangulated, world-space, grouped by material — and every language ships native, from-scratch writers on top of it for every format below. No third-party CAD/BIM SDK is involved for any of them.| Format | Extension | Ships in |
|---|---|---|
| glTF (binary GLB) | .glb | ✓ all 5 languages |
| Wavefront OBJ + MTL | .obj | ✓ all 5 languages |
| STL (3D printing) | .stl | ✓ all 5 languages |
| PLY (Stanford Triangle Format) | .ply | ✓ all 5 languages |
| DXF 3D (AutoCAD Polyface Mesh / 3DFACE) | .dxf | ✓ all 5 languages |
| IFC4 (BIM / ISO 10303-21 STEP) | .ifc | ✓ all 5 languages |
| Full metadata JSON | .json | ✓ all 5 languages |
Python's converters, in
openskp.export:from openskp import SkpFile
from openskp.export import glb, obj, stl, ply, dxf, ifc, json_export
skp = SkpFile.open("model.skp")
model = skp.parse()
scene = skp.build_scene()
glb.export(skp, "output.glb")
obj.export(scene, "output.obj")
stl.export(scene, "output.stl")
ply.export(scene, "output.ply")
dxf.export(scene, "output.dxf")
ifc.export(scene, "output.ifc")
json_export.export(model, "output.json", scene=scene)
TypeScript:
toGLB/toOBJ/toSTLAscii/toSTLBinary/toPLYAscii/toPLYBinary/toDXF/toIFC/toJSON, plus Node-only exportOBJ/exportSTL/exportPLY/exportDXF/exportIFC file writers. .NET: GlbExport/ObjExport/StlExport/PlyExport/DxfExport/IfcExport/JsonExport, each with a .Export* file-writing method. Dart: toGlb/exportGlb/toObj/exportObj/toStlAscii/toStlBinary/exportStl/toPlyAscii/toPlyBinary/exportPly/toDxf/exportDxf/toIfc/exportIfc/toJson. C++: to_glb/export_glb/to_obj/export_obj/to_stl_ascii/to_stl_binary/export_stl/to_ply_ascii/to_ply_binary/export_ply/to_dxf/export_dxf/to_ifc/export_ifc/to_json/export_json. TinyGLTF and miniz are private to OpenSKP's C++ package, not consumer dependencies.The DXF converter specifically is verified against real desktop AutoCAD, not just lenient DXF readers — see the Changelog for the exact compatibility issues that surfaced and were fixed.
Web Viewer
Drag-and-drop 3D viewer, built on the TypeScript package
The live web viewer is a full drag-and-drop 3D viewer built on the TypeScript package and Three.js. It calls both
parseSkp() (for version/layers/materials metadata) and buildScene() (for renderable meshes) on the same buffer.The viewer runs entirely in your browser tab, which has a fixed JavaScript memory limit that can't be raised the way Node's
--max-old-space-size can. Files at or above 50MB show an explicit warning before attempting to load, since a load that exceeds the tab's heap can freeze it with no recoverable error. For large files, use the Python/.NET/Dart packages directly instead.Known Cross-Language Differences
Honest list — none are "wrong," but code written for one language won't port directly
Root-level definition access
Python:
model.definitions is a single dict that includes a 'ROOT' string key alongside integer definition IDs — no separate .root attribute. TypeScript, .NET, Dart: model.definitions is strictly numeric-keyed; root is a separate model.root/model.Root property with the same Definition shape. C++: same numeric-keyed split, but root is a model.root() accessor method rather than a plain field.TypeScript memory scaling
See Performance & Memory above — TypeScript needs significantly more heap than the other four languages for the same file, with no config-based workaround for files above roughly 250MB today.
.NET static
SkpFile API shapeThe .NET port exposes
SkpFile as a static class with factory methods (SkpFile.Parse, SkpFile.BuildScene, SkpFile.Open), rather than requiring an instantiated file handle object before calling .Parse() — a deliberate C# idiom matching standard .NET framework designs like System.IO.File.C++
materials_by_id() helperSkpModel::materials_by_id() returns a std::map<EntityId, Material*> (a method, not a plain field) — matching the enumerable dictionary/map the other four languages expose as materials_by_id/materialsById/MaterialsById.Contributing
Every contribution matters — bug fixes, features, docs, new platforms
Report an Issue
Found a bug, or a file that doesn't parse right? Open an issue with a repro if you can.
Pull Request
Fork, add tests, submit a PR. See CONTRIBUTING.md for the per-language setup.
Full Docs on GitHub
Developer Guide, Observability Guide, Architecture, and the raw binary format spec.
bash
# Clone and set up
git clone https://github.com/iamahsanmehmood/openskp.git
cd openskp
# Python
cd packages/python && python -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]" && pytest
# TypeScript
cd packages/typescript && npm install && npm test
# .NET
cd packages/dotnet/OpenSkp.Tests && dotnet test
# Dart
cd packages/dart && dart pub get && dart test
# C++
cmake -S packages/cpp -B build/cpp -DOPENSKP_BUILD_TESTS=ON
cmake --build build/cpp && ctest --test-dir build/cpp --output-on-failure