ここは Structure Layer → Exterior Layer の中でも最難関のひとつ
でも OS 的に分解すれば、破綻なく・実装しやすく・Roblox に渡しやすい「投影アルゴリズム」が作れる。

ここでは 「room+direction → 外周セグメント」へのマッピングロジック
完全に実装可能な形でまとめるね。


🧭 全体の考え方(OSレイヤーで整理)

外周ポリゴンはすでに得られている:

outline = [(x1,y1), (x2,y2), ...]

部屋の bounds もある:

room.bounds = {x, y, width, depth}

opening(窓・ドア)は:

{
  "room": "r1",
  "direction": "south",
  "offset": 3.0,
  "width": 2.0
}

やるべきことは:

  1. opening の属する部屋の「該当壁の絶対座標」を求める
  2. その壁が外周ポリゴンのどのセグメントと一致するか判定
  3. offset を外周セグメント上の座標に変換
  4. opening を外周セグメントにマッピングして返す

🟦 Step 1:部屋の壁を絶対座標で求める

部屋の bounds から 4 つの壁を求める:

north: (x, y+depth) → (x+width, y+depth)
south: (x, y)       → (x+width, y)
east:  (x+width, y) → (x+width, y+depth)
west:  (x, y)       → (x, y+depth)

🟧 Step 2:外周ポリゴンの各セグメントと一致するか判定

外周ポリゴンのセグメント:

[(x1,y1),(x2,y2)], [(x2,y2),(x3,y3)], ...

部屋の壁と外周セグメントが一致する条件:

  • 線分が完全に重なる(colinear)
  • 重なり部分が存在する(overlap)

これは Shapely を使うと簡単:

from shapely.geometry import LineString

room_wall = LineString([(x1,y1),(x2,y2)])
seg = LineString([(sx1,sy1),(sx2,sy2)])

if room_wall.intersects(seg):
    # 外周に面している壁

🟨 Step 3:offset を外周セグメント上の座標に変換

opening の offset は「部屋の壁の左端からの距離」。

外周セグメントと部屋の壁が一致したら:

opening_start = seg_start + (offset / seg_length) * (seg_vector)
opening_end   = opening_start + (width / seg_length) * (seg_vector)

🟪 Step 4:opening を外周セグメントにマッピングして返す

最終的に:

{
  "id": "w1",
  "wall_index": 3,
  "from": [xA, yA],
  "to":   [xB, yB]
}

🧩 完全実装(Flask + Shapely)

from shapely.geometry import LineString

def get_room_wall_coords(room, direction):
    b = room["bounds"]
    x, y = b["x"], b["y"]
    w, d = b["width"], b["depth"]

    if direction == "south":
        return (x, y), (x + w, y)
    if direction == "north":
        return (x, y + d), (x + w, y + d)
    if direction == "east":
        return (x + w, y), (x + w, y + d)
    if direction == "west":
        return (x, y), (x, y + d)

    raise ValueError("invalid direction")


def project_openings_to_exterior(rooms, outline, openings):
    # room_id → room データ
    room_map = {r["id"]: r for r in rooms}

    exterior_segments = []
    n = len(outline)
    for i in range(n):
        p1 = outline[i]
        p2 = outline[(i + 1) % n]
        exterior_segments.append(LineString([p1, p2]))

    projected = []

    for op in openings:
        room = room_map[op["room"]]
        (rx1, ry1), (rx2, ry2) = get_room_wall_coords(room, op["direction"])
        room_wall = LineString([(rx1, ry1), (rx2, ry2)])

        # offset / width
        offset = op["offset"]
        width = op["width"]

        for idx, seg in enumerate(exterior_segments):
            if not room_wall.intersects(seg):
                continue

            # 外周セグメント上の位置を計算
            seg_len = seg.length
            if seg_len == 0:
                continue

            # セグメントの始点・方向ベクトル
            sx, sy = seg.coords[0]
            ex, ey = seg.coords[1]
            dx = ex - sx
            dy = ey - sy

            # offset → opening start
            t1 = offset / seg_len
            t2 = (offset + width) / seg_len

            ox1 = sx + dx * t1
            oy1 = sy + dy * t1
            ox2 = sx + dx * t2
            oy2 = sy + dy * t2

            projected.append({
                "id": op["id"],
                "wall_index": idx,
                "from": [ox1, oy1],
                "to": [ox2, oy2]
            })

            break

    return projected

🟦 これを Flask の /generate_exterior に統合すると:

projected_openings = project_openings_to_exterior(
    rooms,
    outline_coords,
    openings["windows"] + openings["doors"]
)

exterior = {
    "outline": outline_coords,
    "walls": walls,
    "openings": projected_openings
}

🎯 これで完成するもの

  • Structure Layer の room.bounds
  • direction(north/south/east/west)
  • offset / width

から、

外周ポリゴン上の正確な opening 座標が自動生成される。

Roblox 側は:

  • wall_index
  • from/to 座標
  • height

を受け取れば、そのまま Window/ Door を配置できる。


この次はどっちを進める?

  • Roblox 側の「外壁+opening を Mesh/Part に変換するコード」
  • Structure Layer の JSON スキーマを正式に定義
  • 3D 化のための「壁厚」「外壁 offset」アルゴリズム
  • 2階以上の opening の扱い(Z 軸の導入)

どれでもいけるよ。