feat: v0.6 分割树架构——横梃竖梃完整编辑

- 逆向确认原站设计思路: 基底多边形+mm.sp分割树(polyId指向作用区域),
  运行时重放得到最终窗格(254套样本验证重放语义)
- 前端 replayShape: 分割树重放还原真实网格(平开1矩形→4-7格)
- 中梃编辑: 点击选中高亮/拖动移动/删除(直接操作sp源真值后重放)
- 加梃升级: polyId.idx 精确指向被点窗格
- Python报价算料同语义重放: 中梃长度精确到实际作用跨度
- 方案文档§3.3: 原站设计思路+编辑器完成设计
This commit is contained in:
HHY
2026-09-20 11:40:21 -07:00
parent 6ec30c198f
commit 78563267c4
6 changed files with 463 additions and 162 deletions
+29 -14
View File
@@ -8,7 +8,7 @@
"""
from dataclasses import dataclass, field
from huamenchuang.core.drawing import DrawingData, parse_drawing
from huamenchuang.core.drawing import parse_drawing, replay_shape
@dataclass
@@ -45,18 +45,23 @@ def _region_bbox(region_pts):
return min(xs), min(ys), max(xs), max(ys)
def takeoff(dd: DrawingData) -> TakeoffResult:
"""从解析后的 drawing_data 提取算料清单。"""
def takeoff(dd) -> TakeoffResult:
"""从解析后的 drawing_data 提取算料清单(基于分割树重放后的真实窗格)。"""
result = TakeoffResult()
xs, ys = [], []
for shape in dd.get("sm", []):
regions = [[(seg["ps"]["x"], seg["ps"]["y"]) for seg in r]
for r in ((shape.get("polygon") or {}).get("polygon") or [])]
# 重放分割树得到真实窗格; 空区域基底的形状跳过
regions = replay_shape(shape)
if not regions:
continue
region_pts = [[(seg["ps"]["x"], seg["ps"]["y"]) for seg in r] for r in regions]
region_bbs = []
for pts in region_pts:
xs_ = [p[0] for p in pts]; ys_ = [p[1] for p in pts]
region_bbs.append((min(xs_), min(ys_), max(xs_), max(ys_)))
# 形状外框 bbox
all_pts = [p for r in regions for p in r]
all_pts = [p for r in region_pts for p in r]
x0, y0, x1, y1 = _region_bbox(all_pts)
w, h = round(x1 - x0), round(y1 - y0)
xs.extend([x0, x1])
@@ -64,16 +69,26 @@ def takeoff(dd: DrawingData) -> TakeoffResult:
result.bars.append(BarItem("frame", w, 2, "外框-宽"))
result.bars.append(BarItem("frame", h, 2, "外框-高"))
# 中梃:投影长度(竖梃=框高,横梃=框宽)
# 中梃: 分割线在其作用区域 bbox 内的投影长度(精确到该梃实际跨度)
for sp in ((shape.get("mm") or {}).get("sp") or []):
norm = sp["line"]["norm"]
if abs(norm["x"]) >= abs(norm["y"]):
result.bars.append(BarItem("mullion", h, 1, "竖梃"))
else:
result.bars.append(BarItem("mullion", w, 1, "横梃"))
line = sp.get("line") or {}
n, pt = line.get("norm"), line.get("pt")
if not (isinstance(n, dict) and isinstance(pt, dict)):
continue
axis = "v" if abs(n.get("x", 0)) >= abs(n.get("y", 0)) else "h"
pos = pt.get("x") if axis == "v" else pt.get("y")
# 取该梃与重放区域相交的最长投影
best = 0.0
for bb in region_bbs:
bx0, by0, bx1, by1 = bb
lo, hi = (bx0, bx1) if axis == "v" else (by0, by1)
if lo - 1 <= pos <= hi + 1:
best = max(best, hi - lo)
if best > 0:
result.bars.append(BarItem("mullion", round(best), 1, "竖梃" if axis == "v" else "横梃"))
# 玻璃面积
result.glass_area_m2 += sum(_shoelace_area(r) for r in regions) / 1_000_000
# 玻璃面积(重放后各窗格之和)
result.glass_area_m2 += sum(_shoelace_area(r) for r in region_pts) / 1_000_000
# 扇框
for sub in (shape.get("sm") or []):