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.
- plangeom-0.1.0a1/MANIFEST.in +1 -0
- plangeom-0.1.0a1/PKG-INFO +85 -0
- plangeom-0.1.0a1/README.md +63 -0
- plangeom-0.1.0a1/pyproject.toml +37 -0
- plangeom-0.1.0a1/setup.cfg +4 -0
- plangeom-0.1.0a1/src/plangeom/__init__.py +14 -0
- plangeom-0.1.0a1/src/plangeom/cli.py +48 -0
- plangeom-0.1.0a1/src/plangeom/document.py +402 -0
- plangeom-0.1.0a1/src/plangeom/elements.py +458 -0
- plangeom-0.1.0a1/src/plangeom/fixtures.py +376 -0
- plangeom-0.1.0a1/src/plangeom/floor.py +582 -0
- plangeom-0.1.0a1/src/plangeom/linework.py +799 -0
- plangeom-0.1.0a1/src/plangeom/mep.py +156 -0
- plangeom-0.1.0a1/src/plangeom/openings.py +248 -0
- plangeom-0.1.0a1/src/plangeom/primitives.py +278 -0
- plangeom-0.1.0a1/src/plangeom/rooms.py +374 -0
- plangeom-0.1.0a1/src/plangeom/scale.py +411 -0
- plangeom-0.1.0a1/src/plangeom/schema.py +61 -0
- plangeom-0.1.0a1/src/plangeom/sheet.py +586 -0
- plangeom-0.1.0a1/src/plangeom/stack.py +250 -0
- plangeom-0.1.0a1/src/plangeom.egg-info/PKG-INFO +85 -0
- plangeom-0.1.0a1/src/plangeom.egg-info/SOURCES.txt +28 -0
- plangeom-0.1.0a1/src/plangeom.egg-info/dependency_links.txt +1 -0
- plangeom-0.1.0a1/src/plangeom.egg-info/entry_points.txt +2 -0
- plangeom-0.1.0a1/src/plangeom.egg-info/requires.txt +11 -0
- plangeom-0.1.0a1/src/plangeom.egg-info/top_level.txt +1 -0
- plangeom-0.1.0a1/tests/synth.py +251 -0
- plangeom-0.1.0a1/tests/test_building_logic.py +316 -0
- plangeom-0.1.0a1/tests/test_engineering.py +179 -0
- plangeom-0.1.0a1/tests/test_synthetic.py +177 -0
|
@@ -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,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())
|