diff --git a/build_3d.gh b/build_3d.gh index b36e3ce..d6155cb 100644 Binary files a/build_3d.gh and b/build_3d.gh differ 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