@@ -129,8 +129,10 @@ export async function preloadTiles(pointsLV95, onProgress) {
129129}
130130
131131// =============================================
132- // Grid creation
132+ // Grid creation (orientation-aligned)
133133// =============================================
134+
135+ /** Ray-casting point-in-polygon test */
134136function pointInPolygon ( x , y , polygon ) {
135137 let inside = false ;
136138 for ( let i = 0 , j = polygon . length - 1 ; i < polygon . length ; j = i ++ ) {
@@ -143,33 +145,129 @@ function pointInPolygon(x, y, polygon) {
143145 return inside ;
144146}
145147
148+ /** Cross product of vectors OA and OB (for convex hull) */
149+ function cross ( o , a , b ) {
150+ return ( a [ 0 ] - o [ 0 ] ) * ( b [ 1 ] - o [ 1 ] ) - ( a [ 1 ] - o [ 1 ] ) * ( b [ 0 ] - o [ 0 ] ) ;
151+ }
152+
153+ /** Monotone chain convex hull — returns points in CCW order, no duplicates */
154+ function convexHull ( points ) {
155+ const pts = points . slice ( ) . sort ( ( a , b ) => a [ 0 ] - b [ 0 ] || a [ 1 ] - b [ 1 ] ) ;
156+ const n = pts . length ;
157+ if ( n <= 2 ) return pts . slice ( ) ;
158+
159+ const lower = [ ] ;
160+ for ( const p of pts ) {
161+ while ( lower . length >= 2 && cross ( lower [ lower . length - 2 ] , lower [ lower . length - 1 ] , p ) <= 0 ) lower . pop ( ) ;
162+ lower . push ( p ) ;
163+ }
164+ const upper = [ ] ;
165+ for ( let i = n - 1 ; i >= 0 ; i -- ) {
166+ while ( upper . length >= 2 && cross ( upper [ upper . length - 2 ] , upper [ upper . length - 1 ] , pts [ i ] ) <= 0 ) upper . pop ( ) ;
167+ upper . push ( pts [ i ] ) ;
168+ }
169+ lower . pop ( ) ;
170+ upper . pop ( ) ;
171+ return lower . concat ( upper ) ;
172+ }
173+
146174/**
147- * Create a grid of sample points inside a polygon (LV95 coordinates).
148- * Uses axis-aligned grid at GRID_SPACING (1m).
175+ * Minimum area bounding rectangle via rotating calipers on the convex hull.
176+ * Returns the rotation angle (radians) of the longest edge.
177+ */
178+ function getBuildingAngle ( coordsLV95 ) {
179+ const hull = convexHull ( coordsLV95 ) ;
180+ if ( hull . length < 3 ) return 0 ;
181+
182+ let bestArea = Infinity ;
183+ let bestAngle = 0 ;
184+ let bestLongestEdgeAngle = 0 ;
185+
186+ for ( let i = 0 ; i < hull . length ; i ++ ) {
187+ const j = ( i + 1 ) % hull . length ;
188+ const edgeAngle = Math . atan2 ( hull [ j ] [ 1 ] - hull [ i ] [ 1 ] , hull [ j ] [ 0 ] - hull [ i ] [ 0 ] ) ;
189+ const cos = Math . cos ( - edgeAngle ) , sin = Math . sin ( - edgeAngle ) ;
190+
191+ // Rotate all hull points to align this edge with x-axis
192+ let rxMin = Infinity , rxMax = - Infinity , ryMin = Infinity , ryMax = - Infinity ;
193+ for ( const p of hull ) {
194+ const rx = p [ 0 ] * cos - p [ 1 ] * sin ;
195+ const ry = p [ 0 ] * sin + p [ 1 ] * cos ;
196+ if ( rx < rxMin ) rxMin = rx ;
197+ if ( rx > rxMax ) rxMax = rx ;
198+ if ( ry < ryMin ) ryMin = ry ;
199+ if ( ry > ryMax ) ryMax = ry ;
200+ }
201+
202+ const area = ( rxMax - rxMin ) * ( ryMax - ryMin ) ;
203+ if ( area < bestArea ) {
204+ bestArea = area ;
205+ bestAngle = edgeAngle ;
206+ // Determine longest edge of this bounding rectangle
207+ const w = rxMax - rxMin , h = ryMax - ryMin ;
208+ bestLongestEdgeAngle = w >= h ? edgeAngle : edgeAngle + Math . PI / 2 ;
209+ }
210+ }
211+
212+ return bestLongestEdgeAngle ;
213+ }
214+
215+ /**
216+ * Create grid points aligned to the building's orientation.
217+ *
218+ * 1. Compute orientation from minimum area bounding rectangle
219+ * 2. Rotate polygon to align with axes
220+ * 3. Generate regular grid in rotated space
221+ * 4. Filter points inside the polygon
222+ * 5. Rotate points back to original orientation
149223 */
150224export function createGridPoints ( coordsLV95 , spacing = GRID_SPACING ) {
225+ // Compute centroid
226+ let cx = 0 , cy = 0 ;
227+ for ( const pt of coordsLV95 ) { cx += pt [ 0 ] ; cy += pt [ 1 ] ; }
228+ cx /= coordsLV95 . length ;
229+ cy /= coordsLV95 . length ;
230+
231+ // Get building orientation angle
232+ const angle = getBuildingAngle ( coordsLV95 ) ;
233+ const cosA = Math . cos ( - angle ) , sinA = Math . sin ( - angle ) ;
234+
235+ // Rotate polygon to align with axes (around centroid)
236+ const rotated = coordsLV95 . map ( ( p ) => {
237+ const dx = p [ 0 ] - cx , dy = p [ 1 ] - cy ;
238+ return [ cx + dx * cosA - dy * sinA , cy + dx * sinA + dy * cosA ] ;
239+ } ) ;
240+
241+ // Bounding box of rotated polygon, snapped to grid
151242 let minX = Infinity , minY = Infinity , maxX = - Infinity , maxY = - Infinity ;
152- for ( const pt of coordsLV95 ) {
243+ for ( const pt of rotated ) {
153244 if ( pt [ 0 ] < minX ) minX = pt [ 0 ] ;
154245 if ( pt [ 0 ] > maxX ) maxX = pt [ 0 ] ;
155246 if ( pt [ 1 ] < minY ) minY = pt [ 1 ] ;
156247 if ( pt [ 1 ] > maxY ) maxY = pt [ 1 ] ;
157248 }
249+ minX = Math . floor ( minX / spacing ) * spacing ;
250+ minY = Math . floor ( minY / spacing ) * spacing ;
251+ maxX = Math . ceil ( maxX / spacing ) * spacing ;
252+ maxY = Math . ceil ( maxY / spacing ) * spacing ;
158253
254+ // Generate grid in rotated space, filter by polygon containment
255+ const cosB = Math . cos ( angle ) , sinB = Math . sin ( angle ) ;
159256 const points = [ ] ;
257+
160258 for ( let gx = minX + spacing / 2 ; gx < maxX ; gx += spacing ) {
161259 for ( let gy = minY + spacing / 2 ; gy < maxY ; gy += spacing ) {
162- if ( pointInPolygon ( gx , gy , coordsLV95 ) ) {
163- points . push ( [ gx , gy ] ) ;
260+ if ( pointInPolygon ( gx , gy , rotated ) ) {
261+ // Rotate back to original orientation
262+ const dx = gx - cx , dy = gy - cy ;
263+ points . push ( [ cx + dx * cosB - dy * sinB , cy + dx * sinB + dy * cosB ] ) ;
164264 }
165265 }
166266 }
167267
168268 // Fallback: centroid if grid is empty (very small footprint)
169269 if ( points . length === 0 ) {
170- let cx = 0 , cy = 0 ;
171- for ( const pt of coordsLV95 ) { cx += pt [ 0 ] ; cy += pt [ 1 ] ; }
172- points . push ( [ cx / coordsLV95 . length , cy / coordsLV95 . length ] ) ;
270+ points . push ( [ cx , cy ] ) ;
173271 }
174272 return points ;
175273}
@@ -198,6 +296,7 @@ export function polygonAreaLV95(ring) {
198296 * @returns {object|null } Volume result or null if no data
199297 */
200298export function computeVolumeSync ( coordsLV95 ) {
299+ const gridAngle = getBuildingAngle ( coordsLV95 ) ;
201300 const gridPoints = createGridPoints ( coordsLV95 ) ;
202301 if ( gridPoints . length === 0 ) return null ;
203302
@@ -278,6 +377,7 @@ export function computeVolumeSync(coordsLV95) {
278377 grid_points : dtmValues . length ,
279378 grid_cells : cells ,
280379 grid_spacing : GRID_SPACING ,
380+ grid_angle : gridAngle ,
281381 } ;
282382}
283383
0 commit comments