| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798 |
- """Skip facade details only on explicitly reviewed, fully shared massing edges.
- No walls, roof caps, terrain coordinates or heights are changed. Partial-edge
- contacts stay untouched, and higher exposed storeys are never hidden by a low neighbor.
- """
- import math
- from building_composition import signed_area
- def _covers_edge(a, b, ring, tolerance=.011):
- dx,dy=b[0]-a[0],b[1]-a[1]
- length=math.hypot(dx,dy)
- if length<=tolerance:
- return False
- ux,uy=dx/length,dy/length
- intervals=[]
- for p,q in zip(ring,ring[1:]+ring[:1]):
- if (q[0]-p[0])*ux+(q[1]-p[1])*uy>=0:
- continue
- if max(abs((v[0]-a[0])*uy-(v[1]-a[1])*ux) for v in (p,q))>tolerance:
- continue
- along=[(v[0]-a[0])*ux+(v[1]-a[1])*uy for v in (p,q)]
- low,high=max(0,min(along)),min(length,max(along))
- if high>low:
- intervals.append((low,high))
- end=0
- for low,high in sorted(intervals):
- if low>end+tolerance:
- return False
- end=max(end,high)
- return end>=length-tolerance
- def party_wall_masks(buildings, normalize, profile):
- """Derive per-level masks from real neighbors; never trust serialized edge indices."""
- by_id={b['id']:b for b in buildings}
- if len(by_id)!=len(buildings):
- raise ValueError('duplicate party-wall building IDs')
- specified=[b for b in buildings if b.get('partyWallNeighbors')]
- if specified and profile!='compact':
- raise ValueError('reviewed party walls require compact profile')
- outlines={b['id']:normalize(b['footprintENU']) for b in buildings}
- masks={}
- for building in specified:
- ids=building['partyWallNeighbors']
- if not isinstance(ids,list) or not ids or len(set(ids))!=len(ids):
- raise ValueError('invalid party-wall neighbor IDs')
- if building['kind']!='massing' or building.get('massingFacadeScope','building')!='building':
- raise ValueError('party-wall details require an ordinary surveyed massing shell')
- ring=outlines[building['id']]
- hosts=[]
- for neighbor_id in ids:
- neighbor=by_id.get(neighbor_id)
- if neighbor is None or neighbor is building or neighbor['kind']!='massing':
- raise ValueError('party-wall neighbor missing or not surveyed massing')
- if building['id'] not in neighbor.get('partyWallNeighbors',[]):
- raise ValueError('party-wall review must be reciprocal')
- other=outlines[neighbor_id]
- if len(ring)<3 or len(other)<3 or signed_area(other)<=0:
- raise ValueError('invalid party-wall outline')
- edges=[i for i,a in enumerate(ring) if _covers_edge(a,ring[(i+1)%len(ring)],other)]
- if not edges:
- raise ValueError('no fully shared edge for reviewed party wall')
- low,high=neighbor['foundationBottom'],neighbor['positionENU'][2]+neighbor['heightM']
- if not math.isfinite(low) or not math.isfinite(high) or high<=low:
- raise ValueError('invalid party-wall neighbor heights')
- hosts.append((edges,low,high))
- base=max(building['foundationBottom'],building['positionENU'][2])
- top=building['positionENU'][2]+building['heightM']
- floors=building['floors']
- if floors<1 or top<=base or not math.isfinite(top):
- raise ValueError('invalid party-wall building heights')
- floor_height=max(2.4,(top-base)/floors)
- # Match compact's deterministic longest-edge tie break. Door masks and
- # actual reservation must refer to the same surveyed wall after ring rotation.
- longest=min(range(len(ring)),key=lambda i:(
- -round(math.dist(ring[i],ring[(i+1)%len(ring)]),6),
- tuple(sorted((tuple(ring[i]),tuple(ring[(i+1)%len(ring)]))))))
- def hidden(edge,low,high):
- return high>low and any(edge in indices and low>=z0 and high<=z1
- for indices,z0,z1 in hosts)
- levels={}
- for level in ('lod0','lod1'):
- compact=level=='lod0'
- windows=set()
- for i in range(len(ring)):
- for floor in range(floors):
- low=base+floor*floor_height+floor_height*(.30 if compact else .34)
- high=min(top-.08,low+floor_height*(.45 if compact else .36))
- if hidden(i,low,high):
- windows.add((i,floor))
- bands={(i,floor) for i in range(len(ring)) for floor in range(1,floors)
- if hidden(i,base+floor*floor_height-.045,base+floor*floor_height+.045)}
- door_low=base+.08
- door_high=min(top-.05,base+min(2.2,max(1.8,floor_height*.76)))
- levels[level]={'windows':windows,'bands':bands,'door':hidden(longest,door_low,door_high)}
- masks[building['id']]=levels
- return masks
|