こちらの続きです。
水溜り3 - C#ATIA
そろそろUI作ろうと思った矢先に、結構なバグが見つかり解決に時間がかかりました。
で、解決したのでUIを付けての動作はこんな感じです。
赤の矢印は重力方向です。エッジや直線を選択する事で任意の方向を重力方向と
指定出来るようにしています。指定なしはZマイナス方向です。
十分使えそうなレベルです。
こちらの続きです。
水溜り3 - C#ATIA
そろそろUI作ろうと思った矢先に、結構なバグが見つかり解決に時間がかかりました。
で、解決したのでUIを付けての動作はこんな感じです。
赤の矢印は重力方向です。エッジや直線を選択する事で任意の方向を重力方向と
指定出来るようにしています。指定なしはZマイナス方向です。
十分使えそうなレベルです。
初めて見たのは少し前だったのですが、こちらすごいですね。
https://www.youtube.com/watch?v=Fez2tIUCWRk
興味のある内容で、こんなの来たら騙されちゃいますね。恐ろしい。
この方、これを作成するサービスを利用して田口さんを再現してます。
サービスを紹介している動画は削除したようですが(賢明だと思います)
えの~ を期待してる人も多いw
こちらの続きです。
水溜り2 - C#ATIA
今回は、
・Numpyの導入
・複数ボディの対応
・水マテリアルの利用
です。
形状はこんなボディです。

片方は一個の時には水が溜まらず、二個並んだ時には水が溜まるような形状となっています。

動作はこんな感じです。
FusionAPIでNumpyを利用するのはハードルが高いので避けていたのですが、
Claude(かCodexか忘れました)に相談した所あっさり対応してくれました。
が、処理時間が遅くなりました。まぁきっと大きなデータの時に影響出るかな?
後はUI付けてアドイン化かな?
こちらの続きです。
水溜り1 - C#ATIA
前回はカスタムグラフィックスだったので、処理は早かったのですが恐らく
欲しいものではないです。
その為、ソリッドとして取得出来るように改修しました。
あっもうちょっと進めたいアイデアがあるので、コードは今回無しで・・・

ちゃんとボディで取得で来ています。あぁBody名が体積の方が良かったかも。

最初は淵がガタガタだったり、不要な部分が出来上がったりしていたのですが、
何度か修正した(Claudeが)結果、滑らかになりました。
やっとスタートに立った感じですよ!
業務で何度か部品の水のたまる部分が分かるマクロについて話をされたことがあります。
(細かな用途は良く分かっていませんが・・)
実際に作成したことも複数回あるのですが、Fusionの場合はどうやって処理するのだろうか?
と前から興味があったので、ちょっと作成してみました。

くたびれたウレタンマットの様な形状を作成しました。ウレタンマットは吸水するんじゃないかって?
まぁまぁそれは置いといて・・・
こちらは作成したスクリプトです。
エントリーポイントで、ルートコンポーネントの1個目のボディとZのマイナス方向を重力としています。
# Fusion360API Python script import importlib import os import sys import traceback import adsk.core as core import adsk.fusion as fusion _DIR = os.path.dirname(os.path.abspath(__file__)) if _DIR not in sys.path: sys.path.append(_DIR) import water_pool # noqa: E402 importlib.reload(water_pool) def run(context): ui: core.UserInterface = None try: app: core.Application = core.Application.get() ui = app.userInterface des: fusion.Design = app.activeProduct root: fusion.Component = des.rootComponent body: fusion.BRepBody = root.bRepBodies[0] # --- 一時的な切り分け用 (原因特定後に削除する) --- # ui.messageBox(water_pool.debug_report(body)) # ------------------------------------------------ # bodyに上から水をかけた時に、水が溜まる部分を探し出す gravity: core.Vector3D = core.Vector3D.create(0.0, 0.0, -1.0) pools: list = water_pool.find_water_pools( body, gravity=gravity, include_overhang=False, ) if not pools: ui.messageBox("水が溜まる部分はありません") return water_pool.draw_pools(pools, root) lines = ["水たまり: {} 箇所".format(len(pools))] for i, pool in enumerate(pools): lines.append( " [{}] 体積 {:.3f} cm^3 / 最大水深 {:.3f} cm / 水面積 {:.3f} cm^2".format( i, pool.volume, pool.max_depth, pool.area ) ) ui.messageBox("\n".join(lines)) except Exception: if ui: ui.messageBox("Failed:\n{}".format(traceback.format_exc()))
続いて実際に水溜りを見つける "water_pool.py" です。
# Fusion360API Python module """上から水をかけたときに body に水が溜まる領域を求める。 アルゴリズムの核は Priority-Flood (Wang & Liu 法)。 各ノード v の高さを z(v) とし、外部へ至る全経路のうち「経路上の最大高さ」が 最小になる値を level(v) とすると、level(v) > z(v) のとき v は水没している。 実装は Dijkstra と同型で、ノード集合と近傍の定義を差し替えるだけで 2.5D 高さ場と 3D ボクセルの両方に使える。 Fusion 360 同梱の Python には numpy が含まれないため、純 Python で実装している。 """ import heapq import json import math import os from dataclasses import dataclass, field import adsk.core as core import adsk.fusion as fusion _INF = float("inf") # 既定の分割数 (bbox 最長辺方向)。3D は O(n^3) なので別値。 _DEF_RES_25D = 200 _DEF_RES_3D = 80 @dataclass class PoolRegion: """水が溜まる領域 1 つ分。""" water_level: float # 水面の高さ (ローカル系 / 重力の逆方向, cm) water_plane_origin: core.Point3D # 水面上のワールド座標の点 up: core.Vector3D # 重力の逆方向 (単位ベクトル, ワールド) seed_point: core.Point3D # 確実に水中にあるワールド座標の点 lowest_point: core.Point3D # 最深部 (床) のワールド座標 max_depth: float # 最大水深 (cm) area: float # 水面の概算面積 (cm^2) volume: float # 概算体積 (cm^3) surface_mesh: tuple = field(default_factory=lambda: ([], [])) """水面を描画するための (ワールド座標の平坦な float 配列, 三角形インデックス)。""" # -------------------------------------------------------------------------- # 公開 API # -------------------------------------------------------------------------- def find_water_pools( body: fusion.BRepBody, gravity: core.Vector3D = None, resolution: int = 0, include_overhang: bool = False, min_depth: float = 0.01, ) -> list: """body に上から水をかけたときに水が溜まる領域を返す。 Args: body: 対象ボディ。 gravity: 重力方向ベクトル。None なら (0, 0, -1)。 resolution: bbox 最長辺の分割数。0 なら自動 (2.5D:200 / 3D:80)。 include_overhang: True なら 3D ボクセルで庇の下の窪みも検出する (低速)。 min_depth: これ未満の最大水深の領域はノイズとして捨てる (cm)。 Returns: PoolRegion のリスト。体積の大きい順。 """ if gravity is None: gravity = core.Vector3D.create(0.0, 0.0, -1.0) ex, ey, ez = _basis(gravity) verts, tris = _get_mesh(body, ex, ey, ez) if not tris: return [] if resolution <= 0: resolution = _DEF_RES_3D if include_overhang else _DEF_RES_25D grid = _Grid(verts, resolution) columns = _build_columns(verts, tris, grid) if include_overhang: raw = _pools_3d(grid, columns, min_depth) else: raw = _pools_25d(grid, columns, min_depth) up = core.Vector3D.create(ez[0], ez[1], ez[2]) pools = [_to_pool_region(r, ex, ey, ez, up) for r in raw] pools.sort(key=lambda p: p.volume, reverse=True) return pools def draw_pools( pools: list, comp: fusion.Component = None, color: tuple = (0, 130, 255), opacity: float = 0.55, ) -> fusion.CustomGraphicsGroup: """水面を CustomGraphics で描画する。モデル・履歴は一切変更しない。 color の opacity 成分は CustomGraphics では無視されるため、 透明度はグループの setOpacity() で指定する。 """ app: core.Application = core.Application.get() if comp is None: comp = app.activeProduct.rootComponent group: fusion.CustomGraphicsGroup = comp.customGraphicsGroups.add() effect = fusion.CustomGraphicsSolidColorEffect.create( core.Color.create(color[0], color[1], color[2], 255) ) for pool in pools: coords, indices = pool.surface_mesh if not indices: continue cg_coords = fusion.CustomGraphicsCoordinates.create(coords) mesh = group.addMesh(cg_coords, indices, [], []) mesh.color = effect group.setOpacity(opacity, True) app.activeViewport.refresh() return group # -------------------------------------------------------------------------- # 座標系 # -------------------------------------------------------------------------- def _basis(gravity: core.Vector3D): """重力方向から正規直交基底を作る。ez が「上」(重力の逆)。""" g = gravity.copy() if not g.normalize(): raise ValueError("gravity ベクトルの長さが 0 です") ez = (-g.x, -g.y, -g.z) helper = (1.0, 0.0, 0.0) if abs(ez[0]) < 0.9 else (0.0, 1.0, 0.0) ex = _normalized(_cross(helper, ez)) ey = _cross(ez, ex) return ex, ey, ez def _cross(a, b): return ( a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0], ) def _normalized(v): n = math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]) return (v[0] / n, v[1] / n, v[2] / n) def _world_point(p, ex, ey, ez) -> core.Point3D: """ローカル座標 (x, y, z) をワールドの Point3D へ戻す。""" x, y, z = p return core.Point3D.create( x * ex[0] + y * ey[0] + z * ez[0], x * ex[1] + y * ey[1] + z * ez[1], x * ex[2] + y * ey[2] + z * ez[2], ) # -------------------------------------------------------------------------- # メッシュ取得 # -------------------------------------------------------------------------- def _calc_mesh(ent): calc = ent.meshManager.createMeshCalculator() calc.setQuality(fusion.TriangleMeshQualityOptions.HighQualityTriangleMesh) return calc.calculate() def _get_mesh(body: fusion.BRepBody, ex, ey, ez): """ボディの三角形メッシュをローカル系 (ez が上) で返す。 body 全体の meshManager は面が丸ごと欠落したメッシュを返すことがある (実測で傾斜面 1 枚が欠けた)。メッシュに穴があるとその柱の交差数が奇数になり、 排水口とみなされて窪みの水が丸ごと抜けてしまうため、面ごとにメッシュ化して 連結し、全面が必ず含まれるようにする。 """ verts = [] tris = [] def collect(mesh): if mesh is None: return base = len(verts) for p in mesh.nodeCoordinates: x, y, z = p.x, p.y, p.z verts.append( ( x * ex[0] + y * ex[1] + z * ex[2], x * ey[0] + y * ey[1] + z * ey[2], x * ez[0] + y * ez[1] + z * ez[2], ) ) idx = mesh.nodeIndices for i in range(0, len(idx) - 2, 3): tris.append((base + idx[i], base + idx[i + 1], base + idx[i + 2])) for face in body.faces: collect(_calc_mesh(face)) if not tris: # 面単位が使えない場合のフォールバック collect(_calc_mesh(body)) return verts, tris # -------------------------------------------------------------------------- # グリッド # -------------------------------------------------------------------------- class _Grid: def __init__(self, verts, resolution, pad_cells=2): xs = [v[0] for v in verts] ys = [v[1] for v in verts] zs = [v[2] for v in verts] min_x, max_x = min(xs), max(xs) min_y, max_y = min(ys), max(ys) self.min_z, self.max_z = min(zs), max(zs) span = max(max_x - min_x, max_y - min_y, 1e-6) self.cs = span / max(1, resolution) cs = self.cs self.pad = pad_cells self.nx = int(math.ceil((max_x - min_x) / cs)) + 2 * pad_cells + 1 self.ny = int(math.ceil((max_y - min_y) / cs)) + 2 * pad_cells + 1 # CAD の平面とセル中心が正確に一致して内外判定が不安定になるのを避ける微小オフセット self.ox = min_x - pad_cells * cs + cs * 0.0137 self.oy = min_y - pad_cells * cs + cs * 0.0271 self.base_z = self.min_z - cs self.top_z = self.max_z + cs def cx(self, i): return self.ox + (i + 0.5) * self.cs def cy(self, j): return self.oy + (j + 0.5) * self.cs # -------------------------------------------------------------------------- # 柱モデル (共通の土台) # -------------------------------------------------------------------------- def _build_columns(verts, tris, grid: _Grid): """各 (x, y) セルについて (ソリッド区間 [(z_bottom, z_top), ...], 信頼フラグ) を返す。""" nx, ny, cs, ox, oy = grid.nx, grid.ny, grid.cs, grid.ox, grid.oy crossings = [[] for _ in range(nx * ny)] for ia, ib, ic in tris: a = verts[ia] b = verts[ib] c = verts[ic] ux, uy, uz = b[0] - a[0], b[1] - a[1], b[2] - a[2] vx, vy, vz = c[0] - a[0], c[1] - a[1], c[2] - a[2] n_x = uy * vz - uz * vy n_y = uz * vx - ux * vz n_z = ux * vy - uy * vx # 垂直な面 (XY への射影面積がゼロ) は垂直レイと交差しない。 # 三角形の大きさに依らない判定にするため法線長に対する相対しきい値を使う。 n_len = math.sqrt(n_x * n_x + n_y * n_y + n_z * n_z) if n_len <= 0.0 or abs(n_z) < 1e-9 * n_len: continue # 内外判定用に CCW へ揃える (平面式は元の法線のままで正しい) ra, rb, rc = (a, b, c) if n_z > 0 else (a, c, b) # 共有辺でセル中心を取りこぼすと床に穴が空き、盆地が丸ごと排水されてしまう。 # 境界は両側の三角形で拾っておき、生じた重複交差は _intervals() で潰す。 tol = abs(n_z) * 1e-9 i0 = max(0, int(math.ceil((min(a[0], b[0], c[0]) - ox) / cs - 0.5))) i1 = min(nx - 1, int(math.floor((max(a[0], b[0], c[0]) - ox) / cs - 0.5))) j0 = max(0, int(math.ceil((min(a[1], b[1], c[1]) - oy) / cs - 0.5))) j1 = min(ny - 1, int(math.floor((max(a[1], b[1], c[1]) - oy) / cs - 0.5))) if i0 > i1 or j0 > j1: continue for j in range(j0, j1 + 1): py = oy + (j + 0.5) * cs row = j * nx for i in range(i0, i1 + 1): px = ox + (i + 0.5) * cs w0 = (rc[0] - rb[0]) * (py - rb[1]) - (rc[1] - rb[1]) * (px - rb[0]) if w0 < -tol: continue w1 = (ra[0] - rc[0]) * (py - rc[1]) - (ra[1] - rc[1]) * (px - rc[0]) if w1 < -tol: continue w2 = (rb[0] - ra[0]) * (py - ra[1]) - (rb[1] - ra[1]) * (px - ra[0]) if w2 < -tol: continue z = a[2] - (n_x * (px - a[0]) + n_y * (py - a[1])) / n_z crossings[row + i].append(z) z_tol = cs * 1e-6 return [_intervals(cl, z_tol) for cl in crossings] def _intervals(crossing_list, z_tol): """交差 z のリストをソリッド区間に組み上げる。 三角形の巻き方向 (winding) に依存しないよう even-odd 規則を使う。 閉じたソリッドなら垂直レイの交差数は必ず偶数になるので、z 昇順に 2 つずつ組にすれば中空ボディ (外殻 + 内殻) でも正しく区間が得られる。 Returns: (区間リスト, 信頼フラグ)。交差数が奇数の柱は数値的に壊れているので 信頼フラグを False にし、呼び出し側で排水口扱いしないようにする。 """ if not crossing_list: return [], True crossing_list.sort() # 共有辺・共有頂点を複数の三角形で拾った重複を 1 つに畳む。 zs = [crossing_list[0]] for z in crossing_list: if z - zs[-1] > z_tol: zs.append(z) ok = len(zs) % 2 == 0 out = [] for k in range(0, len(zs) - 1, 2): if zs[k + 1] > zs[k]: out.append((zs[k], zs[k + 1])) return out, ok # -------------------------------------------------------------------------- # Priority-Flood (2.5D 高さ場) # -------------------------------------------------------------------------- def _pools_25d(grid: _Grid, columns, min_depth): nx, ny, cs = grid.nx, grid.ny, grid.cs n = nx * ny height = [grid.base_z] * n solid = bytearray(n) reliable = bytearray(n) for k, (ivs, ok) in enumerate(columns): if ok: reliable[k] = 1 if ivs: height[k] = ivs[-1][1] # 最上面 solid[k] = 1 # ラスタライズ由来の孤立した穴を埋める。 # 床に 1 セルでも穴があると、そこが排水口になり盆地が丸ごと消えてしまう。 filled = bytes(solid) for k in range(n): if filled[k]: continue i = k % nx j = k // nx if i == 0 or j == 0 or i == nx - 1 or j == ny - 1: continue nb = (k + 1, k - 1, k + nx, k - nx) if all(filled[t] for t in nb): height[k] = min(height[t] for t in nb) solid[k] = 1 level = [_INF] * n heap = [] def seed(k): if level[k] == _INF: level[k] = height[k] heapq.heappush(heap, (height[k], k)) # 外周セル + 空の柱 (床が無いので水は素通りする = 排水口) for i in range(nx): seed(i) seed((ny - 1) * nx + i) for j in range(ny): seed(j * nx) seed(j * nx + nx - 1) for k in range(n): # 信頼できない柱 (交差数が奇数) は排水口とみなさない。 # 水位も与えないので、障壁にも排水口にもならず素通りするだけになる。 if not solid[k] and reliable[k]: seed(k) while heap: lv, k = heapq.heappop(heap) i = k % nx j = k // nx for ii, jj in ((i + 1, j), (i - 1, j), (i, j + 1), (i, j - 1)): if 0 <= ii < nx and 0 <= jj < ny: kk = jj * nx + ii if level[kk] == _INF: lv2 = lv if lv > height[kk] else height[kk] level[kk] = lv2 heapq.heappush(heap, (lv2, kk)) wet = bytearray(n) for k in range(n): if solid[k] and level[k] > height[k]: wet[k] = 1 cell_area = cs * cs pools = [] seen = bytearray(n) for k0 in range(n): if not wet[k0] or seen[k0]: continue # 連結成分 (4 近傍) comp = [] seen[k0] = 1 stack = [k0] while stack: k = stack.pop() comp.append(k) i = k % nx j = k // nx for ii, jj in ((i + 1, j), (i - 1, j), (i, j + 1), (i, j - 1)): if 0 <= ii < nx and 0 <= jj < ny: kk = jj * nx + ii if wet[kk] and not seen[kk]: seen[kk] = 1 stack.append(kk) water_level = max(level[k] for k in comp) deepest = min(comp, key=lambda k: height[k]) max_depth = water_level - height[deepest] if max_depth <= min_depth: continue volume = sum((water_level - height[k]) for k in comp) * cell_area quads = [(grid.cx(k % nx), grid.cy(k // nx), water_level) for k in comp] pools.append( { "water_level": water_level, "max_depth": max_depth, "area": len(comp) * cell_area, "volume": volume, "floor": ( grid.cx(deepest % nx), grid.cy(deepest // nx), height[deepest], ), "quad_centers": quads, "cs": cs, } ) return pools # -------------------------------------------------------------------------- # Priority-Flood (3D ボクセル / オーバーハング対応) # -------------------------------------------------------------------------- def _pools_3d(grid: _Grid, columns, min_depth): nx, ny, cs, base_z = grid.nx, grid.ny, grid.cs, grid.base_z nz = int(math.ceil((grid.top_z - base_z) / cs)) + 1 layer = nx * ny n = layer * nz solid = bytearray(n) for k, (ivs, _ok) in enumerate(columns): for z0, z1 in ivs: kz0 = max(0, int(math.ceil((z0 - base_z) / cs - 0.5))) kz1 = min(nz - 1, int(math.floor((z1 - base_z) / cs - 0.5))) if kz0 > kz1: # 1 セルより薄い区間。そのままだと床が抜けるので中心の 1 ボクセルを立てる。 kzc = int(round(((z0 + z1) * 0.5 - base_z) / cs - 0.5)) solid[min(nz - 1, max(0, kzc)) * layer + k] = 1 continue for kz in range(kz0, kz1 + 1): solid[kz * layer + k] = 1 def vz(kz): return base_z + (kz + 0.5) * cs level = [_INF] * n heap = [] def seed(idx, kz): if not solid[idx] and level[idx] == _INF: z = vz(kz) level[idx] = z heapq.heappush(heap, (z, idx)) # bbox 表面の空隙ボクセル。ここから到達できない空隙 (= 内部空洞) は未訪問のまま残る。 for kz in range(nz): off = kz * layer for i in range(nx): seed(off + i, kz) seed(off + (ny - 1) * nx + i, kz) for j in range(ny): seed(off + j * nx, kz) seed(off + j * nx + nx - 1, kz) for k in range(layer): seed(k, 0) seed((nz - 1) * layer + k, nz - 1) while heap: lv, idx = heapq.heappop(heap) kz, rem = divmod(idx, layer) j, i = divmod(rem, nx) for ii, jj, kk in ( (i + 1, j, kz), (i - 1, j, kz), (i, j + 1, kz), (i, j - 1, kz), (i, j, kz + 1), (i, j, kz - 1), ): if 0 <= ii < nx and 0 <= jj < ny and 0 <= kk < nz: nidx = kk * layer + jj * nx + ii if solid[nidx] or level[nidx] != _INF: continue z = vz(kk) lv2 = lv if lv > z else z level[nidx] = lv2 heapq.heappush(heap, (lv2, nidx)) wet = bytearray(n) for idx in range(n): lv = level[idx] if lv != _INF and lv > vz(idx // layer): wet[idx] = 1 cell_vol = cs * cs * cs cell_area = cs * cs pools = [] seen = bytearray(n) for i0 in range(n): if not wet[i0] or seen[i0]: continue comp = [] seen[i0] = 1 stack = [i0] while stack: idx = stack.pop() comp.append(idx) kz, rem = divmod(idx, layer) j, i = divmod(rem, nx) for ii, jj, kk in ( (i + 1, j, kz), (i - 1, j, kz), (i, j + 1, kz), (i, j - 1, kz), (i, j, kz + 1), (i, j, kz - 1), ): if 0 <= ii < nx and 0 <= jj < ny and 0 <= kk < nz: nidx = kk * layer + jj * nx + ii if wet[nidx] and not seen[nidx]: seen[nidx] = 1 stack.append(nidx) water_level = max(level[idx] for idx in comp) deepest = min(comp, key=lambda idx: idx // layer) floor_z = vz(deepest // layer) - cs * 0.5 max_depth = water_level - floor_z if max_depth <= min_depth: continue # 水面の描画用: (i, j) 柱ごとの最上位ボクセル tops = {} for idx in comp: kz, rem = divmod(idx, layer) if tops.get(rem, -1) < kz: tops[rem] = kz quads = [(grid.cx(rem % nx), grid.cy(rem // nx), water_level) for rem in tops] d_rem = deepest % layer pools.append( { "water_level": water_level, "max_depth": max_depth, "area": len(tops) * cell_area, "volume": len(comp) * cell_vol, "floor": (grid.cx(d_rem % nx), grid.cy(d_rem // nx), floor_z), "quad_centers": quads, "cs": cs, } ) return pools # -------------------------------------------------------------------------- # ワールド座標への変換 # -------------------------------------------------------------------------- def _to_pool_region(raw, ex, ey, ez, up) -> PoolRegion: cs = raw["cs"] half = cs * 0.5 level = raw["water_level"] fx, fy, fz = raw["floor"] coords = [] indices = [] for cx, cy, cz in raw["quad_centers"]: base = len(coords) // 3 for dx, dy in ((-half, -half), (half, -half), (half, half), (-half, half)): p = _world_point((cx + dx, cy + dy, cz), ex, ey, ez) coords.extend((p.x, p.y, p.z)) indices.extend((base, base + 1, base + 2, base, base + 2, base + 3)) return PoolRegion( water_level=level, water_plane_origin=_world_point((fx, fy, level), ex, ey, ez), up=up, seed_point=_world_point((fx, fy, (fz + level) * 0.5), ex, ey, ez), lowest_point=_world_point((fx, fy, fz), ex, ey, ez), max_depth=raw["max_depth"], area=raw["area"], volume=raw["volume"], surface_mesh=(coords, indices), ) # -------------------------------------------------------------------------- # 切り分け用の診断 # -------------------------------------------------------------------------- def _boundary_edges(verts, tris) -> int: """メッシュの境界辺 (2 枚の三角形で共有されていない辺) の本数を数える。 0 なら水密。0 でなければメッシュに穴があり、その位置の柱が排水口として 誤検出されるため、水たまりが丸ごと消える。 """ ids = [] key = {} for p in verts: k = (round(p[0], 6), round(p[1], 6), round(p[2], 6)) i = key.get(k) if i is None: i = len(key) key[k] = i ids.append(i) counts = {} for a, b, c in tris: for x, y in ((a, b), (b, c), (c, a)): e = (ids[x], ids[y]) if ids[x] < ids[y] else (ids[y], ids[x]) counts[e] = counts.get(e, 0) + 1 return sum(1 for n in counts.values() if n != 2) def debug_report( body: fusion.BRepBody, gravity: core.Vector3D = None, resolution: int = 0, ) -> str: """検出結果がおかしいときに、どの段階で壊れているかを調べるレポートを返す。 メッシュ取得 → 柱 (ソリッド区間) 抽出 → Priority-Flood の順に数値を出すので、 「柱が 0 本」ならラスタライズ側、「柱はあるが pools が 0」なら浸水判定側と切り分けられる。 """ if gravity is None: gravity = core.Vector3D.create(0.0, 0.0, -1.0) ex, ey, ez = _basis(gravity) verts, tris = _get_mesh(body, ex, ey, ez) lines = ["verts: {} / tris: {}".format(len(verts), len(tris))] if not tris: lines.append("!! メッシュが取得できていません") return "\n".join(lines) lines.append( "faces: {} / isSolid: {}".format(body.faces.count, body.isSolid) ) lines.append("boundary edges (0 なら水密): {}".format(_boundary_edges(verts, tris))) # 手元で完全に再現できるようメッシュをそのまま書き出す。 dump = os.path.join(os.path.dirname(os.path.abspath(__file__)), "mesh_dump.json") with open(dump, "w") as f: json.dump({"verts": verts, "tris": tris}, f) lines.append("mesh dump: {}".format(dump)) if resolution <= 0: resolution = _DEF_RES_25D grid = _Grid(verts, resolution) columns = _build_columns(verts, tris, grid) n = grid.nx * grid.ny n_solid = sum(1 for ivs, _ok in columns if ivs) n_bad = sum(1 for _ivs, ok in columns if not ok) lines.append( "grid: nx={} ny={} cells={} cs={:.6f}".format(grid.nx, grid.ny, n, grid.cs) ) lines.append("z range: {:.4f} .. {:.4f}".format(grid.min_z, grid.max_z)) lines.append("solid columns: {} / {}".format(n_solid, n)) lines.append("unreliable columns (交差数が奇数): {}".format(n_bad)) if n_solid == 0: lines.append("!! 柱が 1 本も作れていません (ラスタライズ側の問題)") def _dump(tag, raw): raw.sort(key=lambda r: r["volume"], reverse=True) lines.append("{} pools (min_depth=0): {}".format(tag, len(raw))) for i, r in enumerate(raw[:5]): lines.append( " [{}] level={:.4f} depth={:.4f} vol={:.4f}".format( i, r["water_level"], r["max_depth"], r["volume"] ) ) _dump("2.5D", _pools_25d(grid, columns, 0.0)) # 2.5D は「最上面の高さ場」しか見ないので、庇や天井の下の窪みは原理的に見えない。 # 3D ボクセルでだけ見つかるなら、それがオーバーハング形状である証拠になる。 grid3 = _Grid(verts, _DEF_RES_3D) columns3 = _build_columns(verts, tris, grid3) _dump("3D (include_overhang)", _pools_3d(grid3, columns3, 0.0)) return "\n".join(lines)
やっちゃいましたね。ブログで出すボリュームじゃないです。はい、反省してます。
実際に実行した結果はこちらです。

正しく処理出来ているようです。
念のためお伝えすると、
・上から水をかけた際の結果ではなく、一旦水に部品を浸けた後に持ち上げて少し揺らした後の様な状態です。
(上がカバーされている凹みにも水が溜まり、水平部分は水が溜まらない と言った意味です)
・出来上がりの水溜まりはカスタムグラフィックスで作成されている為、APIで削除するかファイルの再オープン
するしか削除方法がありません
EEのtomoさん、APIやってないと思っていたのですが、ガッツリ調べていました。
AI時代のFusion MCPを全解剖してわかったこと — 他のCG&CAD MCPとは前提から違う|神原 友徳 tomo1230
Autodesk Fusion 最新アップデートのAPIをMCPで検証したら、書いていないことの方が多かった|神原 友徳 tomo1230
面白そうなので試したいのですが、時間が無い・・・
テキストコマンドは間違いなく強力なのですが、Update時に動作するかどうかの保証が無いので
微妙は微妙なのですが、強力です!
11月ぐらいからなら時間作れるかなぁ。
こちらの続きです。
カスタムテーブルにアクセスする - C#ATIA
前回のコードを試したところ、正しいカスタムテーブルの数を返してくれませんでしたが、
今回試したところ、正しい値を返すようになりました。
そこで今回はもう一歩進めます。

カスタムテーブルを1個作成し、適当にセルに値を入力しておきます。
続いて、セルの値を出力するためのコードを作成しました。
# Fusion360API Python script import traceback import adsk.core as core import adsk.drawing as drawing def run(context): ui: core.UserInterface = None try: app: core.Application = core.Application.get() ui = app.userInterface drawDoc: drawing.DrawingDocument = drawing.DrawingDocument.cast( app.activeDocument ) if not drawDoc: return draw: drawing.Drawing = drawDoc.drawing sheet: drawing.Sheet = draw.activeSheet table: drawing.CustomTable = sheet.customTables[0] for row in range(table.rowCount): for col in range(table.columnCount): cell_data = table.getCellData(row, col) print(f"[{row}, {col}] = {cell_data}") except Exception: if ui: ui.messageBox("Failed:\n{}".format(traceback.format_exc()))
実行結果は、この様になりました。
[0, 0] = [0, 1] = [0, 2] = [0, 3] = [1, 0] = [1, 1] = [1, 2] = [1, 3] = [2, 0] = [2, 1] = [2, 2] = [2, 3] = [3, 0] = [3, 1] = [3, 2] = [3, 3] = [4, 0] = [4, 1] = [4, 2] = [4, 3] =
どうも、未だに機能していないようです・・・。書き込みのsetCellDataメソッドも機能していませんでした。