feat: v0.6 分割树架构——横梃竖梃完整编辑
- 逆向确认原站设计思路: 基底多边形+mm.sp分割树(polyId指向作用区域), 运行时重放得到最终窗格(254套样本验证重放语义) - 前端 replayShape: 分割树重放还原真实网格(平开1矩形→4-7格) - 中梃编辑: 点击选中高亮/拖动移动/删除(直接操作sp源真值后重放) - 加梃升级: polyId.idx 精确指向被点窗格 - Python报价算料同语义重放: 中梃长度精确到实际作用跨度 - 方案文档§3.3: 原站设计思路+编辑器完成设计
This commit is contained in:
@@ -21,3 +21,103 @@ def parse_drawing(raw: str | dict) -> DrawingData:
|
||||
"""解析 drawing_data(兼容 JSON 字符串或对象)。"""
|
||||
dd = json.loads(raw) if isinstance(raw, str) else raw
|
||||
return dd # type: ignore[return-value]
|
||||
|
||||
|
||||
def _sp_axis(sp: dict) -> str:
|
||||
"""分割线轴向: v=竖梃(恒定x) h=横梃(恒定y)。"""
|
||||
n = (sp.get("line") or {}).get("norm") or {"x": 1, "y": 0}
|
||||
return "v" if abs(n.get("x", 0)) >= abs(n.get("y", 0)) else "h"
|
||||
|
||||
|
||||
def _sp_pos(sp: dict) -> float:
|
||||
p = sp["line"]["pt"]
|
||||
return p["x"] if _sp_axis(sp) == "v" else p["y"]
|
||||
|
||||
|
||||
def _region_bbox(region: list[dict]) -> tuple[float, float, float, float] | None:
|
||||
pts = [seg[k] for seg in region for k in ("ps", "pe") if isinstance(seg.get(k), dict)]
|
||||
if not pts:
|
||||
return None
|
||||
xs = [p["x"] for p in pts]
|
||||
ys = [p["y"] for p in pts]
|
||||
return min(xs), min(ys), max(xs), max(ys)
|
||||
|
||||
|
||||
def _split_poly(region: list[dict], axis: str, pos: float) -> tuple[list[dict], list[dict]] | None:
|
||||
"""两次交叉点法分割闭合多边形段链(凸区域)。"""
|
||||
pts = [seg["ps"] for seg in region if isinstance(seg.get("ps"), dict)]
|
||||
n = len(pts)
|
||||
if n < 3:
|
||||
return None
|
||||
crossings: list[tuple[int, dict]] = []
|
||||
for i in range(n):
|
||||
a, b = pts[i], pts[(i + 1) % n]
|
||||
if axis == "v":
|
||||
da, db = a["x"] - pos, b["x"] - pos
|
||||
else:
|
||||
da, db = a["y"] - pos, b["y"] - pos
|
||||
if (da < 0 < db) or (da > 0 > db):
|
||||
t = da / (da - db)
|
||||
mid = ({"x": pos, "y": a["y"] + (b["y"] - a["y"]) * t} if axis == "v"
|
||||
else {"x": a["x"] + (b["x"] - a["x"]) * t, "y": pos})
|
||||
crossings.append((i, mid))
|
||||
if len(crossings) != 2:
|
||||
return None
|
||||
(i1, c1), (i2, c2) = crossings
|
||||
if i1 == i2:
|
||||
return None
|
||||
|
||||
def collect(fa: int, ca: dict, fb: int, cb: dict) -> list[dict]:
|
||||
ps = [ca]
|
||||
k = (fa + 1) % n
|
||||
while True:
|
||||
ps.append(pts[k])
|
||||
if k == fb:
|
||||
break
|
||||
k = (k + 1) % n
|
||||
ps.append(cb)
|
||||
out = [p for j, p in enumerate(ps)
|
||||
if j == 0 or abs(p["x"] - ps[j - 1]["x"]) > 0.01 or abs(p["y"] - ps[j - 1]["y"]) > 0.01]
|
||||
return out
|
||||
|
||||
def to_segs(ps: list[dict]) -> list[dict]:
|
||||
return [{"ps": ps[i], "pe": ps[(i + 1) % len(ps)]} for i in range(len(ps))]
|
||||
|
||||
poly_a = collect(i1, c1, i2, c2)
|
||||
poly_b = collect(i2, c2, i1, c1)
|
||||
if len(poly_a) < 3 or len(poly_b) < 3:
|
||||
return None
|
||||
return to_segs(poly_a), to_segs(poly_b)
|
||||
|
||||
|
||||
def replay_shape(shape: dict) -> list[list[dict]]:
|
||||
"""分割树重放: 基底多边形 + mm.sp[](polyId.idx 指向作用区域) → 最终窗格列表。
|
||||
|
||||
与前端 replayShape 同语义: idx 越界取 0; 退化/非凸跳过。
|
||||
"""
|
||||
base = shape.get("polygon", {}).get("polygon", []) or []
|
||||
regions = [[{"ps": dict(s["ps"]), "pe": dict(s["pe"])} for s in r] for r in base]
|
||||
if not regions:
|
||||
return regions
|
||||
for sp in (shape.get("mm") or {}).get("sp", []) or []:
|
||||
line = sp.get("line") or {}
|
||||
n, pt = line.get("norm"), line.get("pt")
|
||||
if not (isinstance(n, dict) and isinstance(pt, dict)):
|
||||
continue
|
||||
axis = _sp_axis(sp)
|
||||
pos = _sp_pos(sp)
|
||||
idx = (sp.get("polyId") or {}).get("idx", -1)
|
||||
idx = 0 if (not isinstance(idx, int) or idx < 0 or idx >= len(regions)) else idx
|
||||
bb = _region_bbox(regions[idx])
|
||||
if bb is None:
|
||||
continue
|
||||
x0, y0, x1, y1 = bb
|
||||
lo, hi = (x0, x1) if axis == "v" else (y0, y1)
|
||||
if not (lo + 0.5 < pos < hi - 0.5):
|
||||
continue
|
||||
split = _split_poly(regions[idx], axis, pos)
|
||||
if split is None:
|
||||
continue
|
||||
a, b = split
|
||||
regions[idx:idx + 1] = [a, b]
|
||||
return regions
|
||||
|
||||
@@ -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 []):
|
||||
|
||||
Reference in New Issue
Block a user