IU Tips

ゾーン名に通し番号を一括で付ける方法②【Archicad】【Python】

2024.02.21

こんにちは。
IU BIM STUDIOの原田です。
前回、室名に連番を振る方法を紹介しました。
今回はその続きになります。

前回の方法ではきれいに採番されませんでした。
今回はテストモデルを作成してきれいに採番する方法を考えたいと思います。

テストモデルではゾーンを作成する順番をバラバラにしました。
実際にはゾーンを作る順番なんて意識しないですからね。

そのモデルに前回のコードで一括採番した状態が次の画像です。

番号はバラバラですね。
ではこれをきれいに並べる方法を考えていきましょう。

階ごとに分類する

まずは階ごとに分けてみましょう。
階ごとで分けようと思ったら、なんとPython APIでは配置フロアを取得できないようです。
仕方ないので代わりに高度を取得します。

# プロパティのGUIDを取得
property_name = ["Zone_ZoneName", "General_ElevationToProjectZero"]
property_id = [acu.GetBuiltInPropertyId(i) for i in property_name]

前回のような感じで、ゾーン名と高度を取得します。

そして、プロパティ値を取得し配列に入れます。

# ゾーンを全て取得
zone_ids = acc.GetElementsByType("Zone")

# ゾーン名を取得
element_properties = acc.GetPropertyValuesOfElements(zone_ids, property_id)

# ゾーンID、ゾーン名、高度をリストに入れる
zones = []
zone_names = []
for zone_id, values in zip(zone_ids, element_properties):
    zone_name = values.propertyValues[0].propertyValue.value
    elevation = values.propertyValues[1].propertyValue.value

    zones.append([zone_id, zone_name, elevation])
    zone_names.append(zone_name)

前回同様、ゾーン名が複数個あるかの判定のために別でzone_namesリストを作っておきます。
配列ができたら高度で配列を並び替えします。

# ゾーンを高度で並び替え
sorted_zones = sorted(zones, key=lambda x: x[2])

sorted関数はkeyを指定することで何を基準で並び替えるかを決めることができるので、ラムダ式でkeyを指定します。
今回は高度なので配列の2列目で並び替えます。

あとはほとんど前回と同じなので、説明は省きます。
コード全体は最後に載せておきますので参考にしてください。
結果は画像のようになります。

順番に番号を振る

階ごとに順番にはなりましたが、それでもまだきれいとは言い難いですね。
並んでいる順番に番号が振られる方法を考えてみましょう。

Archicad Python APIでは基本的にはジオメトリ情報は扱えませんが、バウンディングボックスを取得することができます。

バウンディングボックスとは要素を囲むことができる最小の直方体(2Dの場合は長方形)のことです。
直径1mの球の場合、バウンディングボックスは1辺1mの立方体になります。

では、ゾーン名とバウンディングボックスを取得します。

# プロパティのGUIDを取得
property_id = acu.GetBuiltInPropertyId("Zone_ZoneName")

# ゾーンを全て取得
zone_ids = acc.GetElementsByType("Zone")

# バウンディングボックスを取得
bounding_boxes = acc.Get3DBoundingBoxes(zone_ids)

# ゾーン名を取得
property_values = acc.GetPropertyValuesOfElements(zone_ids, [property_id])

取得したら、ゾーンと座標情報をリストに入れます。
バウンディングボックスはx,y,zの最大値と最小値という形式で取得されるので今回は最小値を使います。

# ゾーンID、ゾーン名、高度をリストに入れる
zones = []
zone_names = []
for zone_id, values, bounding_box in zip(zone_ids, property_values, bounding_boxes):

    zone_name = values.propertyValues[0].propertyValue.value
    x = bounding_box.boundingBox3D.xMin
    y = bounding_box.boundingBox3D.yMin
    z = bounding_box.boundingBox3D.zMin

    zones.append([zone_id, zone_name, x, y, z])
    zone_names.append(zone_name)

# ゾーンを高度で並び替え
sorted_zones = sorted(zones, key=lambda x: (x[4], x[3], x[2]))

配列を作り終わったら、Z,Y,Xの優先度で並べます。
sorted関数は複数キーで並べ替えることも可能です。
出来上がりはこんな感じです。

時計回りに並べる

きれいに番号を振ることができましたが、時計回りで番号振りたいときとかありますよね。
そんな場合の方法も考えていきたいと思います。

先ほど同様、ゾーンとゾーン名、バウンディングボックスを取得します。

# プロパティのGUIDを取得
property_id = acu.GetBuiltInPropertyId("Zone_ZoneName")

# ゾーンを全て取得
zone_ids = acc.GetElementsByType("Zone")

# バウンディングボックスを取得
bounding_boxes = acc.Get3DBoundingBoxes(zone_ids)

# ゾーン名を取得
property_values = acc.GetPropertyValuesOfElements(zone_ids, [property_id])

次に先ほど同様リストに入れ並び替えます。

# ゾーンID、ゾーン名、高度をリストに入れる
zones = []
y_values = []
zone_names = []
for zone_id, values, bounding_box in zip(zone_ids, property_values, bounding_boxes):

    zone_name = values.propertyValues[0].propertyValue.value
    x = bounding_box.boundingBox3D.xMin
    y = bounding_box.boundingBox3D.yMin
    z = bounding_box.boundingBox3D.zMin

    zones.append([zone_id, zone_name, x, y, z])
    zone_names.append(zone_name)
    y_values.append(y)

# ゾーンを一旦並び替え
zones.sort(key=lambda x: (x[4], -x[3], x[2])) 

後で使うのでY座標のみのリストを作っておきます。
データの方はYの並び順を逆にしています。-をつけると逆順にソートできるらしいです。

How to sort a list with two keys but one in reverse order?
https://stackoverflow.com/questions/37693373/how-to-sort-a-list-with-two-keys-but-one-in-reverse-order

今回はYの値が中間より下の場合は並びを逆にします。

# ゾーンを並び替え
prev_z = zones[0][4]
prev_y = zones[0][3]
average_y = (max(y_values) + min(y_values)) / 2
sorted_zones = []
is_reverse_order = False
temp_zones = []

for zone in zones:
    current_z = zone[4]
    current_y = zone[3]

    if not math.isclose(current_z, prev_z):
        is_reverse_order = False  # 階が変わったら逆順を戻す
        sorted_zones.extend(temp_zones)  # 一時リストを最終的なリスト追加し、空にする
        temp_zones = []

    if current_y < average_y:
        is_reverse_order = True  # Y座標の半分より下は逆順

    if is_reverse_order:
        temp_zones.insert(0, zone)  # 逆順は一時リストの先頭に追加
    else:
        sorted_zones.append(zone)

    prev_y = current_y
    prev_z = current_z

sorted_zones.extend(temp_zones)

is_reverse_orderがtrueの時はゾーンを先頭に追加していき、falseの時は末尾に追加しています。

階が変わったときにis_reverse_orderを元に戻したいのですが、浮動小数点数の誤差があるためにz座標の値が前のものと違うということを単純に比較できないようでした。
そのためにmath.iscloseを使っています。

これで時計回りに採番できました。
結果は次のようになります。

今回のまとめ

今回いろいろやって、きれいに並べることができましたが、必ずしもすべてのケースに対応できるわけではありません。
すべてのケースに対応できるスクリプトというのは非常に難しい場合が多いです。
ケースに応じて微調整できるスキルが重要ですね。

では、今回はこれにて終了です。
最後までお読みいただきありがとうございました!
最後にコードを載せておきますので参考にしてください。

階ごとに並び替え

from archicad import ACConnection

conn = ACConnection.connect()
assert conn

acc = conn.commands
act = conn.types
acu = conn.utilities

# プロパティのGUIDを取得
property_name = ["Zone_ZoneName", "General_ElevationToProjectZero"]
property_id = [acu.GetBuiltInPropertyId(i) for i in property_name]

# ゾーンを全て取得
zone_ids = acc.GetElementsByType("Zone")

# ゾーン名を取得
element_properties = acc.GetPropertyValuesOfElements(zone_ids, property_id)

# ゾーンID、ゾーン名、高度をリストに入れる
zones = []
zone_names = []
for zone_id, values in zip(zone_ids, element_properties):
    zone_name = values.propertyValues[0].propertyValue.value
    elevation = values.propertyValues[1].propertyValue.value

    zones.append([zone_id, zone_name, elevation])
    zone_names.append(zone_name)

# ゾーンを高度で並び替え
sorted_zones = sorted(zones, key=lambda x: x[2])


property_values = []
zone_number_dict = {}

for zone, zone_name, _ in sorted_zones:
    # ゾーン名が1つの場合は枝番を付けない
    if zone_names.count(zone_name) <= 1:
        continue

    # 通し番号を更新
    if zone_name in zone_number_dict:
        zone_number_dict[zone_name] += 1
    else:
        zone_number_dict[zone_name] = 1

    # ゾーン名の文字列を作成
    zone_number = zone_number_dict[zone_name]
    new_zone_name = act.NormalStringPropertyValue(f"{zone_name}_{zone_number}")

    # 変更内容のリストを作成
    property_value = act.ElementPropertyValue(
        zone.elementId, property_id[0], new_zone_name
    )
    property_values.append(property_value)

# 作成した変更をゾーンに適用
acc.SetPropertyValuesOfElements(property_values)

順番に並び替え

from archicad import ACConnection

conn = ACConnection.connect()
assert conn

acc = conn.commands
act = conn.types
acu = conn.utilities

# プロパティのGUIDを取得
property_id = acu.GetBuiltInPropertyId("Zone_ZoneName")

# ゾーンを全て取得
zone_ids = acc.GetElementsByType("Zone")


# バウンディングボックスを取得
bounding_boxes = acc.Get3DBoundingBoxes(zone_ids)

# ゾーン名を取得
property_values = acc.GetPropertyValuesOfElements(zone_ids, [property_id])

# ゾーンID、ゾーン名、高度をリストに入れる
zones = []
zone_names = []
for zone_id, values, bounding_box in zip(zone_ids, property_values, bounding_boxes):

    zone_name = values.propertyValues[0].propertyValue.value
    x = bounding_box.boundingBox3D.xMin
    y = bounding_box.boundingBox3D.yMin
    z = bounding_box.boundingBox3D.zMin

    zones.append([zone_id, zone_name, x, y, z])
    zone_names.append(zone_name)

# ゾーンを高度で並び替え
sorted_zones = sorted(zones, key=lambda x: (x[4], x[3], x[2]))


property_values = []
zone_number_dict = {}

for zone, zone_name, *_ in sorted_zones:
    # ゾーン名が1つの場合は枝番を付けない
    if zone_names.count(zone_name) <= 1:
        continue

    # 通し番号を更新
    if zone_name in zone_number_dict:
        zone_number_dict[zone_name] += 1
    else:
        zone_number_dict[zone_name] = 1

    # ゾーン名の文字列を作成
    zone_number = zone_number_dict[zone_name]
    new_zone_name = act.NormalStringPropertyValue(f"{zone_name}_{zone_number}")

    # 変更内容のリストを作成
    property_value = act.ElementPropertyValue(
        zone.elementId, property_id, new_zone_name
    )
    property_values.append(property_value)

# 作成した変更をゾーンに適用
acc.SetPropertyValuesOfElements(property_values)

時計回りに並び替え

from archicad import ACConnection
import math

conn = ACConnection.connect()
assert conn

acc = conn.commands
act = conn.types
acu = conn.utilities

# プロパティのGUIDを取得
property_id = acu.GetBuiltInPropertyId("Zone_ZoneName")

# ゾーンを全て取得
zone_ids = acc.GetElementsByType("Zone")


# バウンディングボックスを取得
bounding_boxes = acc.Get3DBoundingBoxes(zone_ids)

# ゾーン名を取得
property_values = acc.GetPropertyValuesOfElements(zone_ids, [property_id])

# ゾーンID、ゾーン名、高度をリストに入れる
zones = []
y_values = []
zone_names = []
for zone_id, values, bounding_box in zip(zone_ids, property_values, bounding_boxes):

    zone_name = values.propertyValues[0].propertyValue.value
    x = bounding_box.boundingBox3D.xMin
    y = bounding_box.boundingBox3D.yMin
    z = bounding_box.boundingBox3D.zMin

    zones.append([zone_id, zone_name, x, y, z])
    zone_names.append(zone_name)
    y_values.append(y)

# ゾーンを一旦並び替え
zones.sort(key=lambda x: (x[4], -x[3], x[2]))  # マイナスをつけると逆順らしい

# ゾーンを並び替え
prev_z = zones[0][4]
prev_y = zones[0][3]
average_y = (max(y_values) + min(y_values)) / 2
sorted_zones = []
is_reverse_order = False
temp_zones = []

for zone in zones:
    current_z = zone[4]
    current_y = zone[3]

    if not math.isclose(current_z, prev_z):
        is_reverse_order = False  # 階が変わったら逆順を戻す
        sorted_zones.extend(temp_zones)  # 一時的な配列を追加し、空にする
        temp_zones = []

    if current_y < average_y:
        is_reverse_order = True  # Y座標の半分より下は逆順

    if is_reverse_order:
        temp_zones.insert(0, zone)  # 逆順は一時的な配列の先頭に追加
    else:
        sorted_zones.append(zone)

    prev_y = current_y
    prev_z = current_z

sorted_zones.extend(temp_zones)

# プロパティを設定

property_values = []
zone_number_dict = {}

for zone, zone_name, *_ in sorted_zones:
    # ゾーン名が1つの場合は枝番を付けない
    if zone_names.count(zone_name) <= 1:
        continue

    # 通し番号を更新
    if zone_name in zone_number_dict:
        zone_number_dict[zone_name] += 1
    else:
        zone_number_dict[zone_name] = 1

    # ゾーン名の文字列を作成
    zone_number = zone_number_dict[zone_name]
    new_zone_name = act.NormalStringPropertyValue(f"{zone_name}_{zone_number}")

    # 変更内容のリストを作成
    property_value = act.ElementPropertyValue(
        zone.elementId, property_id, new_zone_name
    )
    property_values.append(property_value)

# 作成した変更をゾーンに適用
acc.SetPropertyValuesOfElements(property_values)