building_composition.py 4.0 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788
  1. """Conservative adjacency candidates for narrow surveyed outlines; no polygon union."""
  2. import math
  3. def signed_area(ring):
  4. ox, oy = ring[0]
  5. return sum((a[0]-ox)*(b[1]-oy)-(b[0]-ox)*(a[1]-oy)
  6. for a,b in zip(ring,ring[1:]+ring[:1])) / 2
  7. def shared_boundary_length(first, second, tolerance=.06):
  8. """Measure overlapping opposite-facing edges, merging duplicate overlap intervals."""
  9. a = first if signed_area(first)>0 else list(reversed(first))
  10. b = second if signed_area(second)>0 else list(reversed(second))
  11. total = 0
  12. for p,q in zip(a,a[1:]+a[:1]):
  13. dx,dy = q[0]-p[0],q[1]-p[1]
  14. length = math.hypot(dx,dy)
  15. if length <= tolerance:
  16. continue
  17. ux,uy = dx/length,dy/length
  18. intervals = []
  19. for r,s in zip(b,b[1:]+b[:1]):
  20. if (s[0]-r[0])*ux+(s[1]-r[1])*uy >= 0:
  21. continue # CCW neighbors run opposite ways along a shared boundary
  22. distances = [abs((v[0]-p[0])*uy-(v[1]-p[1])*ux) for v in (r,s)]
  23. if max(distances)>tolerance:
  24. continue
  25. t = [(v[0]-p[0])*ux+(v[1]-p[1])*uy for v in (r,s)]
  26. low,high = max(0,min(t)),min(length,max(t))
  27. if high>low:
  28. intervals.append((low,high))
  29. end = -math.inf
  30. for low,high in sorted(intervals):
  31. total += max(0,high-max(end,low))
  32. end = max(end,high)
  33. return total
  34. def attachment_candidates(strip, buildings):
  35. """Geometry evidence only; ambiguity must be resolved before assigning an export role."""
  36. short,long = sorted((strip['width'],strip['depth']))
  37. if strip['kind']!='massing' or short>2 or short<=0 or long<4 or long/short<4:
  38. return []
  39. area = abs(signed_area(strip['footprintENU']))
  40. candidates = []
  41. for host in buildings:
  42. if host['id']==strip['id'] or min(host['width'],host['depth'])<4:
  43. continue
  44. if abs(signed_area(host['footprintENU']))<area*4:
  45. continue
  46. shared = shared_boundary_length(strip['footprintENU'],host['footprintENU'])
  47. if shared>=long*.7:
  48. candidates.append({'primaryId':host['id'],'sharedBoundaryM':round(shared,4),
  49. 'sharedToLongSideRatio':round(shared/long,4)})
  50. return sorted(candidates,key=lambda row:(-row['sharedBoundaryM'],row['primaryId']))
  51. def validate_composition_scopes(buildings, profile):
  52. pairs = {}
  53. for building in buildings:
  54. scope = building.get('massingFacadeScope', 'building')
  55. if scope not in ('building', 'shell-only'):
  56. raise ValueError('unsupported facade scope')
  57. if scope == 'shell-only':
  58. composition = building.get('composition', {})
  59. if profile != 'compact' or composition.get('role') != 'attached-narrow-outline':
  60. raise ValueError('shell-only requires an explicit compact attachment role')
  61. pairs[building['id']] = composition.get('primaryId')
  62. if pairs:
  63. annotate_reviewed_attachments(buildings, pairs)
  64. def annotate_reviewed_attachments(buildings, reviewed_pairs):
  65. """Annotate an isolated plan; reject missing hosts, ambiguity or incorrect pairs."""
  66. by_id = {b['id']:b for b in buildings}
  67. if len(by_id)!=len(buildings):
  68. raise ValueError('duplicate building IDs')
  69. for strip_id,primary_id in reviewed_pairs.items():
  70. if strip_id not in by_id or primary_id not in by_id or primary_id in reviewed_pairs:
  71. raise ValueError('missing or invalid attachment primary')
  72. candidates = attachment_candidates(by_id[strip_id],buildings)
  73. if len(candidates)!=1 or candidates[0]['primaryId']!=primary_id:
  74. raise ValueError('ambiguous or unsupported attachment: '+strip_id)
  75. return [{**building, 'massingFacadeScope':'shell-only',
  76. 'composition':{'role':'attached-narrow-outline', 'primaryId':reviewed_pairs[building['id']],
  77. 'geometryUnchanged':True}}
  78. if building['id'] in reviewed_pairs else dict(building) for building in buildings]