pyFit

pyFit

Enables 2D irregular polygon nesting (bin-packing) with tools to design, preview, get reports, and export DXF files for laser cutting or CNC routing.

Category
访问服务器

README

pyFit - Agentic Irregular 2D Polygon Nesting (Bin-Packing) Tool

CI License: MIT Python 3.9+ Development Status: Beta

<img src="blog-posts/images/nest_sheet1_pylair_triangles.png" alt="nesting-example" width="416" height="312">

A general-purpose 2D irregular-polygon nesting (bin-packing) tool: given a set of 2D shapes and how many of each you need, arranges them onto rectangular sheet stock (plywood, acrylic, sheet metal, ...) with minimal wasted material, and writes out a ready-to-cut DXF file per sheet (plus a JSON placement report) for your laser cutter or CNC router.

This started as a follow-on to pyLair — pyLair's Bill of Materials tells you exactly which triangular panel shapes a geodesic dome needs and how many of each, but has nothing to say about how to lay them out on actual material. pyfit is deliberately not a pyLair module, though — it's a standalone tool that happens to read the DXF files pyLair (or any other CAD tool) can produce, so it's equally usable for unrelated 2D cutting/fabrication problems.

For the fuller story behind this project — why it exists, the algorithm design decisions, a real bug caught and fixed along the way, and how it was built — see the introductory blog post.

Installation

pip install pyfit-agentic-polygon-nesting

The distribution name is pyfit-agentic-polygon-nesting (pyfit was already taken on PyPI by an unrelated project), but the importable package, CLI command, and MCP console script are all still pyfit/pyfit-mcp — see "Naming note" in AGENTS.md for the full history.

For local development, install from a checkout instead:

pip install -e .

For running the test suite:

pip install -e ".[test]"
pytest

With coverage:

pytest --cov=pyfit --cov-report=term-missing

For linting and type checking (what CI runs):

pip install -e ".[lint]"
ruff check pyfit tests
mypy

Usage

pyfit -j job.json -o output/nest

writes output/nest_sheet1.dxf, output/nest_sheet2.dxf, ... (one file per sheet actually used) plus output/nest_report.json, and prints the same report to stdout. Add -P/--preview to also write output/nest_sheet1.png, ... — a quick 2D render of each sheet's layout (boundary plus every placed part's outline) for a fast sanity check without opening a DXF viewer.

While a large job packs, pyfit prints a placed N/M parts (checking sheet K)... heartbeat to stderr every couple of seconds, so a slow run doesn't look frozen (stdout stays clean JSON either way). Pass -q/--quiet to suppress it.

A job spec is JSON describing the sheet size and the parts to nest:

{
  "sheet": {"width": 96, "height": 48},
  "parts": [
    {"name": "shapeA", "dxf": "facetype1.dxf", "quantity": 120, "allow_mirror": true},
    {"name": "shapeB", "polygon": [[0, 0], [1, 0], [0, 1]], "quantity": 10, "allow_mirror": false}
  ]
}

Each part gives its outline either as "dxf" (a path to a DXF file containing exactly one closed loop) or "polygon" (an inline list of [x, y] points), a "quantity", and optionally "allow_mirror" (default true; set false for chirality-sensitive material, where flipping a shape over isn't the same as another copy of it — e.g. wood grain, a printed pattern, or a one-sided finish).

Using pyLair's panel templates as input

pyLair's -T/--face-templates flag writes one DXF cutting template per unique panel shape (<output>_facetype1.dxf, ...) and reports a panel_count for each in its Bill of Materials. Point a pyfit job spec's "dxf" fields at those files and their "quantity" at the reported counts, and pyfit will figure out how to arrange them on your actual stock. There's no code coupling between the two projects — pyLair writes plain DXF files, and pyfit's DXF importer doesn't know or care where a shape came from, the same way any CAD tool could read pyLair's output.

MCP interface

For agentic use (an LLM assistant interactively nesting parts), install the mcp extra:

pip install "pyfit-agentic-polygon-nesting[mcp]"

This provides a pyfit-mcp console command: an MCP server (stdio transport) exposing four tools, all sharing one parameter schema (sheet_width, sheet_height, parts — a list of job-spec part dicts, see above — and rotation_step_degrees):

Tool Purpose
design_nest Packs the parts (no files written) and returns sheets-used and per-sheet utilization — a cheap way to try job specs.
preview_nest Renders each sheet's 2D layout and returns them as inline images, so a result can be seen in-conversation before any file is written.
get_nest_report Returns the full placement report (sheet index, position, rotation, mirror flag per part instance) as structured data, no files.
export_nest Writes one DXF per sheet actually used to disk (optionally a preview PNG too, via preview=True). Returns the paths written plus the full placement report.

Configure it in an MCP client (e.g. Claude Code/Desktop) by pointing at the pyfit-mcp command. All four tools share the same validation as the CLI (pyfit/api.py:run_nest/load_part) — a malformed job spec or an out-of-range rotation step raises a clear error rather than producing a bad or silent result.

All four tools also report MCP progress notifications on a large job, if the calling client requested them (i.e. sent a progress token): a heartbeat each time the packer tries a sheet for the current part instance, so a slow call doesn't look frozen mid-request. This works because packing itself runs in a worker thread while the tool awaits it, keeping the server's event loop free to actually send those notifications out. With no progress token (or when calling the tool functions directly, e.g. in tests), this is simply a no-op.

OpenClaw skill

For OpenClaw users, this repo ships a skill at skills/pyfit/SKILL.md that teaches an OpenClaw agent when and how to invoke the pyfit CLI (job spec format, flags, output files) without any MCP setup. To use it:

  1. Install pyfit itself (see "Installation" above) — the skill gates on the pyfit command being on PATH.
  2. Merge openclaw.config.snippet.jsonc into your ~/.openclaw/openclaw.json, pointing skills.load.extraDirs at this repo's skills/ directory.

OpenClaw will then discover and enable the skill on its next run. This is a separate integration path from the MCP server above — the skill is for an OpenClaw agent shelling out to the pyfit CLI directly; pyfit-mcp is for any MCP-capable client (including OpenClaw, if it supports MCP servers) making structured tool calls instead.

Knowledge graph

This repo also carries a graphify-generated knowledge graph of its own codebase and docs, tracked at graphify-out/graph.html (interactive, open in any browser — no server needed), graphify-out/GRAPH_REPORT.md (audit report: god nodes, cross-community bridges, surprising cross-doc connections), and graphify-out/graph.json (raw graph data). It's a snapshot, not auto-regenerated — re-run /graphify (see AGENTS.md) if the codebase has since changed significantly and you want it current again.

How it works

A closed 2D shape's set of legal (non-overlapping) placements relative to another fixed shape is described by their no-fit-polygon (NFP): the region a moving shape's reference point must stay outside of to avoid overlapping the stationary one. pyfit computes NFPs via pyclipper (Python bindings to the mature Clipper library) using the standard Minkowski-sum technique, verified against a hand-computable case (the NFP of two unit squares is exactly the 2×2 square from (-1,-1) to (1,1)) before anything was built on top of it.

On top of that primitive, pyfit runs a bottom-left-fill heuristic: place the largest parts first, and for each part instance, try a range of rotation angles (and mirrored orientations, unless allow_mirror is false) at a configurable step size (-R/--rotation-step, default 15°), computing the combined set of valid positions against every already-placed part plus the sheet boundary, and picking the leftmost-then-bottommost one. Each part instance tries every already-opened sheet in order, earliest first, before a new one gets opened — so leftover scrap on an earlier sheet gets reused instead of every miss immediately starting a fresh sheet.

This is a heuristic, not a globally optimal solver — irregular 2D bin-packing is NP-hard, and this is the same family of approach used by tools like SVGnest/DeepNest. A refinement pass (simulated annealing or genetic reordering on top of this base placement) would be a natural future improvement, not something this MVP attempts.

Complexity. For one part instance trying one sheet, the candidate-point search costs O(P) no-fit-polygon computations plus O(P²) pairwise NFP-ring intersection checks, where P is the number of parts already placed on that sheet. That repeats once per candidate orientation (O(360 / rotation_step_degrees) of them) and, when scrap reuse forces trying more than one already-opened sheet, once per sheet tried — so a full run of N part instances costs on the order of N × orientations × P² geometry operations in the worst case. This is exactly why a finer -R/--rotation-step and heavier scrap reuse (more, fuller sheets to search) directly trade off against wall-clock time, as measured below.

A real gotcha worth knowing about

pyclipper.MinkowskiSum on a closed path doesn't return a single resolved polygon — it returns the raw sweep contours, which for a small pattern swept around a larger path includes an inner contour that looks like a hole but isn't one (the Minkowski sum of two convex filled shapes is always itself convex, hence never has a hole — verified against an independent ground truth, the convex hull of all pairwise vertex sums, on a case where the raw Clipper output was misleading). The fix, in pyfit/nfp.py, is to treat every returned contour as an independent solid region and take their union via shapely rather than trusting Clipper's own winding-direction-implied fill rule. This assumes the true NFP has no legitimate holes, which holds for the convex/simple shapes this package targets, but would be wrong for a genuinely non-convex shape with a real unreachable pocket — out of scope here.

Known limitations

  • Candidate placement points aren't fully exhaustive. The bottom-left-fill search considers the sheet's own corners, every NFP vertex, every NFP-vs-sheet-boundary crossing, and every NFP-vs-NFP crossing between different already-placed parts — this is what lets, for example, six unit squares tile a 3×2 sheet perfectly rather than needlessly spilling onto a second sheet. It still isn't full NFP-boundary tracing, so it can occasionally miss an even tighter placement. Every candidate is explicitly re-validated for overlap and sheet containment before being accepted, though, so this can only produce a non-optimal placement, never an invalid one.
  • Rectangular sheets only. No support (yet) for irregular stock outlines or offcuts with existing cutouts.
  • Reusing scrap across sheets costs real search time on large, many-sheet jobs. Every part instance now tries each already-opened sheet in turn, and each try means a full NFP-based candidate search against everything already placed there. A cheap area check skips sheets with too little aggregate remaining area to possibly fit (an exact, safe filter — it never wrongly skips a sheet that could actually fit), but a sheet can still have plenty of leftover area in a shape nothing fits, and that case still costs a full search per miss. Measured on a real 40-panel, 3-sheet job of small pyLair triangles: about 3x slower than the previous (no-reuse) behavior at the default rotation step, but back to roughly the same speed at a coarser one. -R/--rotation-step is the direct lever for this trade-off (fewer tried orientations means fewer NFP computations per sheet-miss) — widen it for a large job if packing feels slow, at the cost of a somewhat coarser search.

Project structure

File Responsibility
pyfit/geometry.py Part/Sheet/Placement/NestResult data model, plus polygon transforms (rotate/mirror/translate about a local origin) and area/bounding-box helpers.
pyfit/nfp.py No-fit-polygon computation via pyclipper, with the Minkowski-sum union fix described above.
pyfit/packer.py The bottom-left-fill placement heuristic, including trying every already-opened sheet in order before starting a new one, plus an optional on_progress callback (a heartbeat, fired per sheet tried per part instance) for the CLI/MCP progress reporting described above.
pyfit/sheet.py Sheet-boundary containment (inner_fit_bounds, exact for a rectangular sheet via bounding-box math, no NFP needed) and utilization reporting.
pyfit/io_dxf.py A minimal hand-written raw DXF reader (reconstructs closed loops from independent LINE entities, e.g. pyLair's face templates) and writer (one file per sheet), with no DXF library dependency — matching pyLair's own writers.
pyfit/preview.py Renders a quick 2D preview PNG (-P/--preview) of a sheet's layout (boundary plus every placed part's outline), the same role as pyLair's own preview module.
pyfit/api.py Programmatic entry point shared by the CLI and the MCP server: run_nest(...)/load_part(...) (job-spec parsing and packing) and nest_result_report(...)/write_nest_files(...) (structured report and file output), all ValueError-raising on bad input rather than printing and exiting.
pyfit/mcp_server.py MCP server (pyfit-mcp console command, optional mcp extra): design_nest/preview_nest/get_nest_report/export_nest tools built on pyfit/api.py.
pyfit/cli.py argparse-based command-line entry point, built on pyfit/api.py.
tests/ pytest suite: unit tests per module, plus subprocess-level CLI integration tests.
skills/pyfit/SKILL.md OpenClaw skill teaching an agent when/how to invoke the pyfit CLI. See "OpenClaw skill" above.
openclaw.config.snippet.jsonc A snippet to merge into ~/.openclaw/openclaw.json to register the above skill.
graphify-out/ Tracked: graph.html/GRAPH_REPORT.md/graph.json, a knowledge-graph snapshot of this repo. See "Knowledge graph" above. Everything else in this directory (extraction cache, cost log, community labels) is gitignored local/derived state.

License

MIT.

推荐服务器

Baidu Map

Baidu Map

百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。

官方
精选
JavaScript
Playwright MCP Server

Playwright MCP Server

一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。

官方
精选
TypeScript
Magic Component Platform (MCP)

Magic Component Platform (MCP)

一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。

官方
精选
本地
TypeScript
Audiense Insights MCP Server

Audiense Insights MCP Server

通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。

官方
精选
本地
TypeScript
VeyraX

VeyraX

一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。

官方
精选
本地
graphlit-mcp-server

graphlit-mcp-server

模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。

官方
精选
TypeScript
Kagi MCP Server

Kagi MCP Server

一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。

官方
精选
Python
e2b-mcp-server

e2b-mcp-server

使用 MCP 通过 e2b 运行代码。

官方
精选
Neon MCP Server

Neon MCP Server

用于与 Neon 管理 API 和数据库交互的 MCP 服务器

官方
精选
Exa MCP Server

Exa MCP Server

模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。

官方
精选