"""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