cad-bim-probe.py 2.8 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485
  1. """Diagnose the massing prism volume check for one building. Run with Blender:
  2. blender --background --factory-startup --python-exit-code 1 --python <this> -- <plan.json> <building-id>
  3. """
  4. import json
  5. import sys
  6. from pathlib import Path
  7. import bpy
  8. import bmesh
  9. from mathutils import Vector
  10. argv = sys.argv[sys.argv.index('--') + 1:]
  11. plan = json.loads(Path(argv[0]).read_text(encoding='utf-8'))
  12. target = argv[1] if len(argv) > 1 else 'cad-210'
  13. building = next(b for b in plan['buildings'] if b['id'] == target)
  14. def ring_area(points):
  15. total = 0.0
  16. for index in range(len(points)):
  17. x1, y1 = points[index]
  18. x2, y2 = points[(index + 1) % len(points)]
  19. total += x1 * y2 - x2 * y1
  20. return total / 2.0
  21. def as_ccw(points):
  22. return list(reversed(points)) if ring_area(points) < 0 else list(points)
  23. def volume_from(obj, anchor):
  24. total = 0.0
  25. for polygon in obj.data.polygons:
  26. pts = [obj.data.vertices[i].co - anchor for i in polygon.vertices]
  27. for k in range(1, len(pts) - 1):
  28. total += pts[0].cross(pts[k]).dot(pts[k + 1])
  29. return total
  30. outline = [(float(x), float(y)) for x, y in building['footprintENU']]
  31. print('raw ring area ', round(ring_area(outline), 3))
  32. outline = as_ccw(outline)
  33. print('ccw ring area ', round(ring_area(outline), 3))
  34. print('positionENU ', building['positionENU'])
  35. print('foundationBottom', building['foundationBottom'], 'heightM', building['heightM'])
  36. bottom = building['foundationBottom']
  37. top = building['positionENU'][2] + building['heightM']
  38. ax, ay, az = building['positionENU']
  39. n = len(outline)
  40. vertices = [(x - ax, y - ay, bottom - az) for x, y in outline] + [(x - ax, y - ay, top - az) for x, y in outline]
  41. bpy.ops.wm.read_factory_settings(use_empty=True)
  42. faces = [(i, (i + 1) % n, n + (i + 1) % n, n + i) for i in range(n)]
  43. faces += [tuple(range(n)), tuple(reversed(range(n, 2 * n)))]
  44. faces += [(n + i, n + (i + 1) % n, (i + 1) % n, i) for i in range(n)]
  45. print('faces ', faces)
  46. mesh = bpy.data.meshes.new('probe')
  47. mesh.from_pydata(vertices, [], faces)
  48. mesh.update()
  49. obj = bpy.data.objects.new('probe', mesh)
  50. bpy.context.scene.collection.objects.link(obj)
  51. anchor = Vector((0.0, 0.0, 0.0))
  52. for vertex in obj.data.vertices:
  53. anchor += vertex.co
  54. anchor /= len(obj.data.vertices)
  55. print('local anchor ', tuple(round(v, 3) for v in anchor))
  56. print('local verts ', [tuple(round(c, 2) for c in v.co) for v in obj.data.vertices])
  57. print('volume pre-recalc ', round(volume_from(obj, anchor), 3))
  58. edit = bmesh.new()
  59. edit.from_mesh(mesh)
  60. bmesh.ops.recalc_face_normals(edit, faces=edit.faces)
  61. edit.to_mesh(mesh)
  62. edit.free()
  63. mesh.update()
  64. print('volume post-recalc', round(volume_from(obj, anchor), 3))
  65. print('polygons ', len(mesh.polygons), 'expected', n + 2)
  66. bm = bmesh.new()
  67. bm.from_mesh(mesh)
  68. print('non-manifold edges', [e.index for e in bm.edges if len(e.link_faces) != 2])
  69. bm.free()