floorplan-scene 0.1.0__py3-none-any.whl

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,20 @@
1
+ """Floor plan data structures and format conversion; no application services."""
2
+ from .fpi import FpiImport, FpiSupplement, export_fpi, import_fpi
3
+ from .model import FloorplanScene, read_json, scene_from_dict, scene_to_dict
4
+ from .rich_svg import enrich_svg, scene_to_rich_svg
5
+ from .svg import scene_to_svg, svg_to_scene
6
+
7
+ __all__ = [
8
+ "FloorplanScene",
9
+ "FpiImport",
10
+ "FpiSupplement",
11
+ "enrich_svg",
12
+ "export_fpi",
13
+ "import_fpi",
14
+ "read_json",
15
+ "scene_from_dict",
16
+ "scene_to_dict",
17
+ "scene_to_rich_svg",
18
+ "scene_to_svg",
19
+ "svg_to_scene",
20
+ ]
@@ -0,0 +1,16 @@
1
+ {
2
+ "unit": "mm",
3
+ "source": "kam-service Constants.java and AbstractFpiBayWindowSillStrategy.java",
4
+ "bay_window_sill": {
5
+ "windowHeight": 1700,
6
+ "offset": 450,
7
+ "upperFloorThickness": 120,
8
+ "lowerFloorThickness": 120,
9
+ "wallThickness": 120,
10
+ "beamThickness": 120
11
+ },
12
+ "beam": {
13
+ "height": 600
14
+ },
15
+ "beamSource": "kam-service@bb34fd64d82800b6a507006a045891086295d074 kammodel/constrants/Constant.java DEFAULT_BEAM_HEIGHT"
16
+ }
@@ -0,0 +1,11 @@
1
+ """Bundled component defaults; explicit source values always take precedence."""
2
+
3
+ import json
4
+ from functools import lru_cache
5
+ from importlib.resources import files
6
+ from typing import Any
7
+
8
+
9
+ @lru_cache(maxsize=1)
10
+ def component_defaults() -> dict[str, Any]:
11
+ return dict(json.loads(files(__package__).joinpath("component-defaults.json").read_text()))
@@ -0,0 +1,140 @@
1
+ """Dimensions that make the current Scene's geometric scale readable."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+ from dataclasses import replace
7
+ from statistics import median
8
+ from typing import Any
9
+
10
+ from shapely.errors import GEOSException # type: ignore[import-untyped]
11
+ from shapely.geometry import LineString # type: ignore[import-untyped]
12
+ from shapely.geometry import Point as ShapelyPoint
13
+ from shapely.ops import unary_union # type: ignore[import-untyped]
14
+
15
+ from .geometry import polygon_parts
16
+ from .model import DimensionAnnotation, FloorplanScene, Wall
17
+ from .shape_geometry import geometry_shape, wall_shape
18
+ from .shapes import LineGeometry, Point
19
+
20
+
21
+ def scene_shell(scene: FloorplanScene) -> Any | None:
22
+ """Return the largest connected room-and-wall shell used for exterior checks."""
23
+ shapes = [geometry_shape(room.geometry) for room in scene.rooms]
24
+ shapes.extend(wall_shape(wall) for wall in scene.walls)
25
+ # A crossed outline has no reliable exterior. Keep the source geometry visible
26
+ # and use the existing no-shell label placement instead of failing the preview.
27
+ if any(not shape.is_valid for shape in shapes):
28
+ return None
29
+ try:
30
+ polygons = polygon_parts(unary_union(shapes).buffer(0)) if shapes else []
31
+ except GEOSException:
32
+ return None
33
+ return max(polygons, key=lambda polygon: polygon.area, default=None)
34
+
35
+
36
+ def _typical_thickness(scene: FloorplanScene) -> float:
37
+ values = [wall.thickness for wall in scene.walls if wall.thickness is not None and wall.thickness > 0]
38
+ return float(median(values)) if values else 200.0
39
+
40
+
41
+ def _wall_outward_normal(shell: Any, wall: Wall, fallback_thickness: float) -> tuple[Point, int] | None:
42
+ if not isinstance(wall.geometry, LineGeometry):
43
+ return None
44
+ start, end = wall.geometry.start, wall.geometry.end
45
+ dx, dy = end[0] - start[0], end[1] - start[1]
46
+ length = math.hypot(dx, dy)
47
+ if length < 0.5:
48
+ return None
49
+ normal = (-dy / length, dx / length)
50
+ half_thickness = (wall.thickness or fallback_thickness) / 2
51
+ if LineString((start, end)).distance(shell.exterior) > half_thickness + 10:
52
+ return None
53
+
54
+ outside_signs: list[int] = []
55
+ distance = half_thickness + 5
56
+ for ratio in (0.25, 0.5, 0.75):
57
+ point = (start[0] + dx * ratio, start[1] + dy * ratio)
58
+ plus = shell.covers(
59
+ ShapelyPoint(point[0] + normal[0] * distance, point[1] + normal[1] * distance)
60
+ )
61
+ minus = shell.covers(
62
+ ShapelyPoint(point[0] - normal[0] * distance, point[1] - normal[1] * distance)
63
+ )
64
+ if plus != minus:
65
+ outside_signs.append(1 if not plus else -1)
66
+ if len(outside_signs) < 2 or abs(sum(outside_signs)) != len(outside_signs):
67
+ return None
68
+ sign = outside_signs[0]
69
+ return (normal[0] * sign, normal[1] * sign), len(outside_signs)
70
+
71
+
72
+ def dimension_outward_normal(scene: FloorplanScene, dimension: DimensionAnnotation) -> Point | None:
73
+ """Prefer the referenced exterior wall; otherwise face away from the plan center."""
74
+ wall = next((item for item in scene.walls if item.id == dimension.refId), None)
75
+ shell = scene_shell(scene)
76
+ if wall is not None and shell is not None:
77
+ result = _wall_outward_normal(shell, wall, _typical_thickness(scene))
78
+ if result is not None:
79
+ return result[0]
80
+ start, end = dimension.geometry.start, dimension.geometry.end
81
+ dx, dy = end[0] - start[0], end[1] - start[1]
82
+ length = math.hypot(dx, dy)
83
+ if length == 0:
84
+ return None
85
+ normal = (-dy / length, dx / length)
86
+ if shell is not None:
87
+ center = shell.centroid
88
+ projection = ((start[0] + end[0]) / 2 - center.x) * normal[0] + (
89
+ (start[1] + end[1]) / 2 - center.y
90
+ ) * normal[1]
91
+ if projection < 0:
92
+ normal = (-normal[0], -normal[1])
93
+ return normal
94
+
95
+
96
+ def with_default_dimension(scene: FloorplanScene) -> FloorplanScene:
97
+ """Add one useful exterior-wall dimension when the Scene has none."""
98
+ if scene.dimensionAnnotations:
99
+ return scene
100
+ shell = scene_shell(scene)
101
+ if shell is None:
102
+ return scene
103
+ fallback_thickness = _typical_thickness(scene)
104
+ candidates: list[tuple[float, int, int, Wall]] = []
105
+ for index, wall in enumerate(scene.walls):
106
+ result = _wall_outward_normal(shell, wall, fallback_thickness)
107
+ if result is None or not isinstance(wall.geometry, LineGeometry):
108
+ continue
109
+ length = math.dist(wall.geometry.start, wall.geometry.end)
110
+ if round(length) > 0:
111
+ candidates.append((length, result[1], -index, wall))
112
+ if not candidates:
113
+ return scene
114
+
115
+ wall = max(candidates)[3]
116
+ assert isinstance(wall.geometry, LineGeometry)
117
+ used_ids = {
118
+ item.id
119
+ for collection in (
120
+ scene.rooms,
121
+ scene.walls,
122
+ scene.separators,
123
+ scene.openings,
124
+ scene.buildings,
125
+ scene.floorOpenings,
126
+ scene.furniture,
127
+ scene.zones,
128
+ )
129
+ for item in collection
130
+ }
131
+ number = 1
132
+ while f"D{number}" in used_ids:
133
+ number += 1
134
+ dimension = DimensionAnnotation(
135
+ f"D{number}",
136
+ round(math.dist(wall.geometry.start, wall.geometry.end)),
137
+ wall.geometry,
138
+ wall.id,
139
+ )
140
+ return replace(scene, dimensionAnnotations=(dimension,))