plangeom 0.1.0a1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ include tests/synth.py
@@ -0,0 +1,85 @@
1
+ Metadata-Version: 2.4
2
+ Name: plangeom
3
+ Version: 0.1.0a1
4
+ Summary: Read construction floor plans from vector PDFs by geometry alone and write a checked JSON description. No model training.
5
+ Keywords: pdf,floor plan,bim,cad,vector,geometry
6
+ Classifier: Development Status :: 3 - Alpha
7
+ Classifier: Intended Audience :: Developers
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Topic :: Scientific/Engineering
10
+ Requires-Python: >=3.10
11
+ Description-Content-Type: text/markdown
12
+ Requires-Dist: numpy>=1.26
13
+ Requires-Dist: scipy>=1.11
14
+ Requires-Dist: shapely>=2.0
15
+ Requires-Dist: opencv-python-headless>=4.8
16
+ Requires-Dist: scikit-image>=0.22
17
+ Requires-Dist: pypdfium2>=4.30
18
+ Provides-Extra: dev
19
+ Requires-Dist: pytest>=8; extra == "dev"
20
+ Requires-Dist: jsonschema>=4; extra == "dev"
21
+ Requires-Dist: reportlab>=4; extra == "dev"
22
+
23
+ # plangeom
24
+
25
+ Reads construction floor plans from vector PDFs by geometry alone and writes a checked JSON description. No model training.
26
+
27
+ ```bash
28
+ pip install plangeom
29
+ plangeom plan.pdf --page 0 -o sheet.json
30
+ ```
31
+
32
+ ```python
33
+ from plangeom import parse_page
34
+ sheet = parse_page("plan.pdf", 0)
35
+ print(sheet["verdict"], sheet["scale"]["mm_per_pt"], len(sheet["doors"]))
36
+ ```
37
+
38
+ A whole set of drawings, one PDF or several volumes of a project:
39
+
40
+ ```bash
41
+ plangeom ar.pdf kr.pdf --all -o building.json
42
+ ```
43
+
44
+ ```python
45
+ from plangeom import parse_document
46
+ result = parse_document(["ar.pdf", "kr.pdf"], workers=4)
47
+ for building in result["buildings"]:
48
+ print(building["parts_named"], building["floors"]["storey_m"], len(building["sheets"]))
49
+ ```
50
+
51
+ ## What it does
52
+
53
+ - **Scale from the axis grid.** Axes are found as circles of paper-fixed size with a long line through the centre. Values printed between axes give millimetres. With no text, axis spacing as multiples of a building module does, with door leaves and round wall thickness settling the harmonic.
54
+ - **Walls** from filled bodies, thickness from opposite edges, centrelines from parallel edge pairs.
55
+ - **Materials** as clusters of hatch geometry, without reading colour. Clusters have no names until a legend or a person names them.
56
+ - **Doors** as a circular arc plus a radial leaf. The sweep angle is deliberately not a criterion.
57
+ - **Rooms** as exact planar faces where walls close them, a watershed from room tags where they do not, and a clearance watershed when the sheet has no text.
58
+ - **Repeated symbols** by D2 shape distribution and size, confirmed by modified Hausdorff distance.
59
+ - **Windows and openings** as gaps between wall ends, in a belt along the slab edge, looking out.
60
+ - **Stairs** as rows of treads, drawn or painted, with flights, landings and the hole in the slab.
61
+ - **Fixtures and cars** by real sizes and shape; a room's type from its name or from what stands in it.
62
+ - **Whole sets.** Stamps give the section, floor, typical floors and level. Sheets on one axis grid under one name are one building; risers, stair wells and lifts run through its floors, and what a floor could not read is taken from the floors around it and marked as taken. Structural sheets judge the wall roles of architectural ones.
63
+
64
+ ## It does not guess silently
65
+
66
+ Every stroke ends in one row of a ledger. Whatever no rule owns goes to `queue`. Checks come from the sheet itself: printed values against axes, room areas against the schedule, thickness and door plausibility. A sheet that fails any of them gets `"verdict": "needs_review"`.
67
+
68
+ ## Status: alpha
69
+
70
+ Tested on synthetic drawings in `tests/`, and by hand on a private corpus of real sheets. Known limits today:
71
+
72
+ - Most real sheets end as `needs_review`: read them with their checks, not as finished models.
73
+ - Room areas agree with schedules at a median error of 4 to 9 % on real projects, and much worse on garages.
74
+ - Stamps are read in Russian.
75
+ - The scale from axes without text can land on a harmonic of the true scale.
76
+ - Scanned pages are not handled: a page with no vector strokes returns `not_a_vector_drawing`.
77
+ - The plan for services in `plangeom.mep` is a first pass with planning defaults, not a design.
78
+
79
+ ## Output
80
+
81
+ JSON Schema: `plangeom.schema.SCHEMA`. Lengths are millimetres in a sheet frame with y down.
82
+
83
+ ## License
84
+
85
+ No license has been granted yet: all rights reserved.
@@ -0,0 +1,63 @@
1
+ # plangeom
2
+
3
+ Reads construction floor plans from vector PDFs by geometry alone and writes a checked JSON description. No model training.
4
+
5
+ ```bash
6
+ pip install plangeom
7
+ plangeom plan.pdf --page 0 -o sheet.json
8
+ ```
9
+
10
+ ```python
11
+ from plangeom import parse_page
12
+ sheet = parse_page("plan.pdf", 0)
13
+ print(sheet["verdict"], sheet["scale"]["mm_per_pt"], len(sheet["doors"]))
14
+ ```
15
+
16
+ A whole set of drawings, one PDF or several volumes of a project:
17
+
18
+ ```bash
19
+ plangeom ar.pdf kr.pdf --all -o building.json
20
+ ```
21
+
22
+ ```python
23
+ from plangeom import parse_document
24
+ result = parse_document(["ar.pdf", "kr.pdf"], workers=4)
25
+ for building in result["buildings"]:
26
+ print(building["parts_named"], building["floors"]["storey_m"], len(building["sheets"]))
27
+ ```
28
+
29
+ ## What it does
30
+
31
+ - **Scale from the axis grid.** Axes are found as circles of paper-fixed size with a long line through the centre. Values printed between axes give millimetres. With no text, axis spacing as multiples of a building module does, with door leaves and round wall thickness settling the harmonic.
32
+ - **Walls** from filled bodies, thickness from opposite edges, centrelines from parallel edge pairs.
33
+ - **Materials** as clusters of hatch geometry, without reading colour. Clusters have no names until a legend or a person names them.
34
+ - **Doors** as a circular arc plus a radial leaf. The sweep angle is deliberately not a criterion.
35
+ - **Rooms** as exact planar faces where walls close them, a watershed from room tags where they do not, and a clearance watershed when the sheet has no text.
36
+ - **Repeated symbols** by D2 shape distribution and size, confirmed by modified Hausdorff distance.
37
+ - **Windows and openings** as gaps between wall ends, in a belt along the slab edge, looking out.
38
+ - **Stairs** as rows of treads, drawn or painted, with flights, landings and the hole in the slab.
39
+ - **Fixtures and cars** by real sizes and shape; a room's type from its name or from what stands in it.
40
+ - **Whole sets.** Stamps give the section, floor, typical floors and level. Sheets on one axis grid under one name are one building; risers, stair wells and lifts run through its floors, and what a floor could not read is taken from the floors around it and marked as taken. Structural sheets judge the wall roles of architectural ones.
41
+
42
+ ## It does not guess silently
43
+
44
+ Every stroke ends in one row of a ledger. Whatever no rule owns goes to `queue`. Checks come from the sheet itself: printed values against axes, room areas against the schedule, thickness and door plausibility. A sheet that fails any of them gets `"verdict": "needs_review"`.
45
+
46
+ ## Status: alpha
47
+
48
+ Tested on synthetic drawings in `tests/`, and by hand on a private corpus of real sheets. Known limits today:
49
+
50
+ - Most real sheets end as `needs_review`: read them with their checks, not as finished models.
51
+ - Room areas agree with schedules at a median error of 4 to 9 % on real projects, and much worse on garages.
52
+ - Stamps are read in Russian.
53
+ - The scale from axes without text can land on a harmonic of the true scale.
54
+ - Scanned pages are not handled: a page with no vector strokes returns `not_a_vector_drawing`.
55
+ - The plan for services in `plangeom.mep` is a first pass with planning defaults, not a design.
56
+
57
+ ## Output
58
+
59
+ JSON Schema: `plangeom.schema.SCHEMA`. Lengths are millimetres in a sheet frame with y down.
60
+
61
+ ## License
62
+
63
+ No license has been granted yet: all rights reserved.
@@ -0,0 +1,37 @@
1
+ [build-system]
2
+ requires = ["setuptools>=70"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "plangeom"
7
+ version = "0.1.0a1"
8
+ description = "Read construction floor plans from vector PDFs by geometry alone and write a checked JSON description. No model training."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ keywords = ["pdf", "floor plan", "bim", "cad", "vector", "geometry"]
12
+ classifiers = [
13
+ "Development Status :: 3 - Alpha",
14
+ "Intended Audience :: Developers",
15
+ "Programming Language :: Python :: 3",
16
+ "Topic :: Scientific/Engineering",
17
+ ]
18
+ dependencies = [
19
+ "numpy>=1.26",
20
+ "scipy>=1.11",
21
+ "shapely>=2.0",
22
+ "opencv-python-headless>=4.8",
23
+ "scikit-image>=0.22",
24
+ "pypdfium2>=4.30",
25
+ ]
26
+
27
+ [project.optional-dependencies]
28
+ dev = ["pytest>=8", "jsonschema>=4", "reportlab>=4"]
29
+
30
+ [project.scripts]
31
+ plangeom = "plangeom.cli:main"
32
+
33
+ [tool.setuptools.packages.find]
34
+ where = ["src"]
35
+
36
+ [tool.pytest.ini_options]
37
+ testpaths = ["tests"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,14 @@
1
+ """plangeom: read construction floor plans from vector PDFs by geometry alone."""
2
+ __version__ = "0.1.0a1"
3
+
4
+
5
+ def parse_page(path, number=0, drop_styles=None):
6
+ """Describe one PDF page as a JSON-ready dict; see plangeom.schema for its shape."""
7
+ from .sheet import parse_page as _p
8
+ return _p(path, number, drop_styles)
9
+
10
+
11
+ def parse_document(path, pages=None, sections=None, workers=2, timeout_s=300.0):
12
+ """Read every plan of a PDF, each sheet in a process of its own, and assemble the buildings: floors, risers, stair wells, what the structure says about the walls."""
13
+ from .document import parse_document as _p
14
+ return _p(path, pages=pages, sections=sections, workers=workers, timeout_s=timeout_s)
@@ -0,0 +1,48 @@
1
+ """plangeom FILE.pdf --page 0 -o sheet.json, or plangeom FILE.pdf --all -o building.json"""
2
+ import argparse
3
+ import json
4
+ import sys
5
+
6
+ from .document import parse_document
7
+ from .sheet import parse_page
8
+
9
+
10
+ def main(argv=None) -> int:
11
+ ap = argparse.ArgumentParser(prog="plangeom", description="Describe a page of a vector PDF drawing as JSON, by geometry alone.")
12
+ ap.add_argument("pdf", nargs="+", help="one file, or with --all several volumes of one project")
13
+ ap.add_argument("--page", type=int, default=0, help="page number from 0")
14
+ ap.add_argument("-o", "--out", help="output JSON file; stdout when omitted")
15
+ ap.add_argument("--all", action="store_true", help="read every plan of the file and assemble the buildings")
16
+ ap.add_argument("--workers", type=int, default=2, help="sheets read at the same time with --all")
17
+ ap.add_argument("--sections", help="with --all: only these sections of the documentation, comma separated, for example АР,КР")
18
+ a = ap.parse_args(argv)
19
+ if a.all:
20
+ def said(n, d):
21
+ print(f"page {n}: {d.get('failed') or d.get('verdict')}", file=sys.stderr)
22
+ res = parse_document(a.pdf, sections=tuple(a.sections.split(",")) if a.sections else None, workers=a.workers, on_sheet=said)
23
+ text = json.dumps(res, ensure_ascii=False, indent=1)
24
+ if a.out:
25
+ with open(a.out, "w", encoding="utf8") as f:
26
+ f.write(text)
27
+ else:
28
+ sys.stdout.write(text + "\n")
29
+ print(f"{res['pages']} pages, {res['read']} read, {len(res['failed'])} failed, {len(res['buildings'])} buildings on their own grids, {len(res['plans_on_no_grid'])} plans on no grid", file=sys.stderr)
30
+ return 0
31
+ if len(a.pdf) > 1:
32
+ ap.error("several files need --all")
33
+ doc = parse_page(a.pdf[0], a.page)
34
+ text = json.dumps(doc, ensure_ascii=False, indent=1)
35
+ if a.out:
36
+ with open(a.out, "w", encoding="utf8") as f:
37
+ f.write(text)
38
+ else:
39
+ sys.stdout.write(text + "\n")
40
+ s = doc["scale"]
41
+ print(f"{doc['verdict']}: scale {s.get('mm_per_pt')} mm/pt by {s.get('method')}, walls {len(doc['walls'])}, doors {len(doc['doors'])}, rooms {len(doc['rooms'])}, symbol classes {len(doc['symbols'])}, unowned {doc['queue']['count']}", file=sys.stderr)
42
+ for c in doc["checks"]:
43
+ print(f" {'ok ' if c['passed'] else 'NO '} {c['name']}: {c['detail']}", file=sys.stderr)
44
+ return 0
45
+
46
+
47
+ if __name__ == "__main__":
48
+ raise SystemExit(main())