From dc4e3c302827dbe6da7b87eb8619107f9812ebe3 Mon Sep 17 00:00:00 2001 From: daywithmin Date: Sat, 6 Sep 2025 01:41:05 +0900 Subject: [PATCH] build 3d wall --- build_3d.gh | Bin 12482 -> 17171 bytes rhino_packages/build_3d.py | 173 ++++++++++++++++++++++++++++++++++++ rhino_packages/constants.py | 13 +++ rhino_packages/parsing.py | 86 ++++++++++++++++++ rhino_packages/utils.py | 73 +++++++++++++++ 5 files changed, 345 insertions(+) create mode 100644 rhino_packages/build_3d.py create mode 100644 rhino_packages/constants.py create mode 100644 rhino_packages/parsing.py create mode 100644 rhino_packages/utils.py diff --git a/build_3d.gh b/build_3d.gh index b36e3ce9af9023ebb15c599e2590ce37cd0c1304..d6155cb02c5b1e62a5e7ada555784baa134fee0e 100644 GIT binary patch literal 17171 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 )C{miH;-6aG)kO2dWf;E>%t< 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 azgFoM6wVNGrJT1}1BU;900030{{sMzV%iq~ literal 12482 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 M>;D4)0RR630N=p~_W%F@ diff --git a/rhino_packages/build_3d.py b/rhino_packages/build_3d.py new file mode 100644 index 0000000..48f50d7 --- /dev/null +++ b/rhino_packages/build_3d.py @@ -0,0 +1,173 @@ +import Rhino.Geometry as geo +from .constants import * +from .utils import set_clockwise, offset_closed_crv_inside, offset_closed_crv_outside, extrude_crv, brep_boolean_difference +from typing import List, Optional, Tuple + +def make_door_from_open_curve(d_crv, height, thickness): + if not d_crv: + return None + + offset1 = d_crv.Offset(geo.Plane.WorldXY, thickness/2, 0.01, geo.CurveOffsetCornerStyle.Sharp) + offset2 = d_crv.Offset(geo.Plane.WorldXY, -thickness/2, 0.01, geo.CurveOffsetCornerStyle.Sharp) + if not offset1 or not offset2: + return None + + c1, c2 = offset1[0], offset2[0] + + # 양 끝 연결 + side1 = geo.Line(c1.PointAtStart, c2.PointAtStart).ToNurbsCurve() + side2 = geo.Line(c1.PointAtEnd, c2.PointAtEnd).ToNurbsCurve() + + # 닫힌 경계 + curves = [c1, side2, c2, side1] + joined = geo.Curve.JoinCurves(curves) + if not joined or len(joined) == 0: + return None + + profile = joined[0] + if not profile.IsClosed: + return None + + # Extrude (높이) + extrusion = geo.Extrusion.Create(profile, height, True) + if not extrusion: + raise ValueError("not door extrusion") + return extrusion.ToBrep() + +def make_window_from_open_curve(w_crv, thickness, height, height_from_bottom): + if not w_crv: + return None + + # 1. Offset (양쪽) + offset1 = w_crv.Offset(geo.Plane.WorldXY, thickness/2, 0.01, geo.CurveOffsetCornerStyle.Sharp) + offset2 = w_crv.Offset(geo.Plane.WorldXY, -thickness/2, 0.01, geo.CurveOffsetCornerStyle.Sharp) + if not offset1 or not offset2: + return None + + c1, c2 = offset1[0], offset2[0] + + # 2. 양 끝점 잇기 + side1 = geo.Line(c1.PointAtStart, c2.PointAtStart).ToNurbsCurve() + side2 = geo.Line(c1.PointAtEnd, c2.PointAtEnd).ToNurbsCurve() + + # 3. 닫힌 경계 만들기 + curves = [c1, side2, c2, side1] + joined = geo.Curve.JoinCurves(curves) + if not joined or len(joined) == 0: + return None + + profile = joined[0] + if not profile.IsClosed: + return None + + # 4. 창 하단 높이만큼 Z 방향으로 이동 + move = geo.Transform.Translation(0, 0, height_from_bottom) + profile_dup = profile.DuplicateCurve() + profile_dup.Transform(move) + + # 5. Extrude (창 높이) + extrusion = geo.Extrusion.Create(profile_dup, height, True) + if not extrusion: + raise ValueError("not window extrusion") + return extrusion.ToBrep() + +def get_room_wall(segs, thickness, height): + # type: (list[geo.Curve], float, float) -> geo.Brep + """ + 벽들의 단일 세그먼트로부터, 두께와 높이만큼 벽을 만든다. + """ + segs = set_clockwise(segs) + # TODO: segs에 순서대로 내부 세그, 외부 세그 나오도록 설정 + walls = [] # type: list[geo.Brep] + + for seg in segs: + inner_offset = offset_closed_crv_inside(seg, thickness/2) + outer_offset = offset_closed_crv_outside(seg, thickness/2) + + inner_extrude = extrude_crv(inner_offset, height) + outer_extrude = extrude_crv(outer_offset, height) + + wall = brep_boolean_difference(outer_extrude, inner_extrude) + walls.append(wall) + wall_unions = geo.Brep.CreateBooleanUnion(walls, 0.01) + + if len(wall_unions) > 1: + raise ValueError("벽이 연결되어 있지 않습니다.") + wall_union = wall_unions[0] + return wall_union + +def get_doors(segs, thickness, height): + # type: (list[geo.Curve], float, float) -> list[geo.Brep] + """ + 문들의 단일 세그먼트로부터, 두께와 높이만큼 문을 만든다. + """ + doors = [] # type: list[geo.Brep] + for seg in segs: + door = make_door_from_open_curve(seg, thickness, height) + doors.append(door) + +def get_windows(segs, thickness, height, height_from_bottom): + # type: (list[geo.Curve], float, float, float) -> list[geo.Brep] + """ + 창문들의 세그먼트로부터, 바닥의 높이에서 창문의 높이, 두께만큼 만든다. + """ + windows = [] # type: list[geo.Brep] + for seg in segs: + window = make_window_from_open_curve(seg, thickness, height, height_from_bottom) + windows.append(window) + return windows + +def get_bottom_slab(wall_region, thickness): + # type: (geo.Curve, float) -> geo.Brep + """ + 천장 슬래브 + """ + outside_offset_crv = offset_closed_crv_outside(wall_region, thickness/2) + extrusion = geo.Extrusion.Create(outside_offset_crv, -thickness, True) + if not extrusion: + raise ValueError("not bottom extrusion") + return extrusion.ToBrep() + +def get_top_slab(wall_region, thickness, wall_height): + # type: (geo.Curve, float, float) -> geo.Brep + """ + 바닥 슬래브 + """ + outside_offset_crv = offset_closed_crv_outside(wall_region, thickness/2) + base = outside_offset_crv.DuplicateCurve() + move = geo.Transform.Translation(0, 0, wall_height) # 벽 높이만큼 위로 이동 + base.Transform(move) + + extrusion = geo.Extrusion.Create(base, thickness, True) + if not extrusion: + raise ValueError("not top extrusion") + return extrusion.ToBrep() + +def build(wall_regions, door_segs, window_segs): + # type: (list[geo.Curve], list[geo.Curve], list[geo.Curve]) -> tuple[list[geo.Brep], list[geo.Brep], list[geo.Brep]] + """ + 문과 창문을 제외한 벽체를 생성한다. + """ + + wall = get_room_wall(wall_regions, wall_thickness, wall_height) + doors = get_doors(door_segs, 300, 3000) + windows = get_windows(window_segs, wall_thickness, window_height, window_height_from_bottom) + + print(doors) + print(windows) + + if not doors: + doors = [] + if not windows: + windows = [] + + cutters = doors + windows + refined_wall = geo.Brep.CreateBooleanDifference([wall], cutters, TOL) + + bottom_slabs = [get_bottom_slab(wr, slab_thickness) for wr in wall_regions] + top_slabs = [get_top_slab(wr, slab_thickness, wall_height) for wr in wall_regions] + + bottom_slab = geo.Brep.CreateBooleanUnion(bottom_slabs, 0.01) + top_slab = geo.Brep.CreateBooleanUnion(top_slabs, 0.01) + + return refined_wall, bottom_slab, top_slab \ No newline at end of file diff --git a/rhino_packages/constants.py b/rhino_packages/constants.py new file mode 100644 index 0000000..220316c --- /dev/null +++ b/rhino_packages/constants.py @@ -0,0 +1,13 @@ +TOL = 0.01 + +# 벽 두께, 벽 높이 +wall_thickness = 200 +wall_height = 3000 + +# 바닥에서 창문까지 높이 +window_height_from_bottom = 500 +# 창문 높이 +window_height = 500 + +# 슬래브 두께 +slab_thickness = 300 \ No newline at end of file diff --git a/rhino_packages/parsing.py b/rhino_packages/parsing.py new file mode 100644 index 0000000..90c626e --- /dev/null +++ b/rhino_packages/parsing.py @@ -0,0 +1,86 @@ +import Rhino.Geometry as geo +from typing import Union, Optional, List, Dict, Any + + +class JsonToGeometry: + """ + JSON 구조(rooms, doors, windows)를 받아 + Rhino.Geometry Curve 객체로 변환하는 클래스 + """ + + def __init__(self, data: Dict[str, Any]): + self.data = data + self.rooms: Dict[str, geo.Curve] = {} + self.doors: List[geo.Curve] = [] + self.windows: List[geo.Curve] = [] + self._parse_all() + + # --- 내부 유틸 --- + def _parse_coords(self, coord_str: str) -> List[geo.Point3d]: + pts: List[geo.Point3d] = [] + for c in coord_str.split(","): + nums = list(map(float, c.strip().split())) + if len(nums) == 2: # x, y만 있는 경우 → z=0 + x, y = nums + pts.append(geo.Point3d(x, y, 0.0)) + elif len(nums) == 3: + x, y, z = nums + pts.append(geo.Point3d(x, y, z)) + else: + raise ValueError(f"좌표 포맷 오류: {nums}") + return pts + + def parse_polygon(self, wkt: str) -> geo.Curve: + coords = wkt.replace("POLYGON ((", "").replace("))", "") + pts = self._parse_coords(coords) + return geo.Polyline(pts).ToPolylineCurve() + + def parse_linestring(self, wkt: str) -> geo.Curve: + coords = wkt.replace("LINESTRING (", "").replace(")", "") + pts = self._parse_coords(coords) + if len(pts) == 2: + return geo.Line(pts[0], pts[1]).ToNurbsCurve() + return geo.Polyline(pts).ToPolylineCurve() + + def parse_point(self, wkt: str) -> geo.Point3d: + coord = wkt.replace("POINT (", "").replace(")", "") + nums = list(map(float, coord.strip().split())) + if len(nums) == 2: + x, y = nums + return geo.Point3d(x, y, 0.0) + elif len(nums) == 3: + x, y, z = nums + return geo.Point3d(x, y, z) + else: + raise ValueError(f"좌표 포맷 오류: {nums}") + + def parse(self, wkt: str) -> Optional[Union[geo.Curve, geo.Point3d]]: + if wkt.startswith("POLYGON"): + return self.parse_polygon(wkt) + elif wkt.startswith("LINESTRING"): + return self.parse_linestring(wkt) + elif wkt.startswith("POINT"): + return self.parse_point(wkt) + return None + + # --- 메인 파서 --- + def _parse_all(self) -> None: + """JSON 구조 전체 파싱""" + # rooms + for r in self.data.get("rooms", []): + geom = self.parse(r["geom"]) + if isinstance(geom, geo.Curve): + self.rooms[r["room_name"]] = geom + + # doors + for d in self.data.get("doors", []): + geom = self.parse(d["geom"]) + if isinstance(geom, geo.Curve): + self.doors.append(geom) + + # windows + for w in self.data.get("windows", []): + geom = self.parse(w["geom"]) + if isinstance(geom, geo.Curve): + self.windows.append(geom) + diff --git a/rhino_packages/utils.py b/rhino_packages/utils.py new file mode 100644 index 0000000..f67b37e --- /dev/null +++ b/rhino_packages/utils.py @@ -0,0 +1,73 @@ +import Rhino.Geometry as geo + +def set_clockwise(crvs): + update_crvs = [] # type: list[geo.Curve] + for crv in crvs: + orientation = crv.ClosedCurveOrientation(geo.Plane.WorldXY) + if orientation == geo.CurveOrientation.CounterClockwise: + update_crvs.append(crv) + else: + crv.Reverse() + update_crvs.append(crv) + return update_crvs + +def is_crv_clockwise(crv): + orientation = crv.ClosedCurveOrientation(geo.Plane.WorldXY) + if orientation == geo.CurveOrientation.CounterClockwise: + return True + elif orientation == geo.CurveOrientation.Clockwise: + return False + else: + return None + +def _try_get_plane(crv): + plane = geo.Plane.Unset + + ok, plane = crv.TryGetPlane() + return (ok, plane) + +def offset_closed_crv_inside(crv, dist): + ok, plane = _try_get_plane(crv) + if not ok or not crv.IsClosed: + return None + if is_crv_clockwise(crv): + dist = -dist + segs = crv.Offset(plane, dist, 0.01, geo.CurveOffsetCornerStyle.Sharp) + if not segs: return None + joined = geo.Curve.JoinCurves(segs, 0.01) + if len(joined) != 1: return None + return joined[0] + +def offset_closed_crv_outside(crv, dist): + + ok, plane = _try_get_plane(crv) + if not ok or not crv.IsClosed: + return None + if not is_crv_clockwise(crv): + dist = -dist + segs = crv.Offset(plane, dist, 0.01, geo.CurveOffsetCornerStyle.Sharp) + if not segs: return None + joined = geo.Curve.JoinCurves(segs, 0.01) + if len(joined) != 1: return None + return joined[0] + +def extrude_crv(crv, height): + if not crv: return None + ext = geo.Extrusion.Create(crv, height, True) + return ext.ToBrep() if ext else None + +def brep_boolean_difference(base_brep, cutter_brep): + if not base_brep or not cutter_brep: + return None + if isinstance(base_brep, list) and len(base_brep) > 0: + base_brep = base_brep[0] + if isinstance(cutter_brep, list) and len(cutter_brep) > 0: + cutter_brep = cutter_brep[0] + try: + tol = Rhino.RhinoDoc.ActiveDoc.ModelAbsoluteTolerance + except: + tol = 1e-3 + result = geo.Brep.CreateBooleanDifference(base_brep, cutter_brep, tol) + if result and len(result) > 0: + return result[0] + return None \ No newline at end of file