From a8dfac7967b961ecb67d4c0bae41150a8fbfd7e1 Mon Sep 17 00:00:00 2001 From: lodev09 Date: Thu, 5 Mar 2026 02:08:46 +0800 Subject: [PATCH 1/4] feat: add holes support to Polygon component Add `holes` prop to Polygon for rendering interior cutouts. Threads coordinate arrays through Codegen, iOS (Apple Maps + Google Maps), Android, and web. Fixes Apple Maps hit-testing to exclude hole areas using even-odd fill rule. --- .../main/java/com/luggmaps/LuggPolygonView.kt | 7 +++ .../com/luggmaps/LuggPolygonViewManager.kt | 21 ++++++++ .../com/luggmaps/core/GoogleMapProvider.kt | 5 ++ docs/POLYGON.md | 22 ++++++++ example/shared/src/components/Map.tsx | 15 ++++++ ios/LuggPolygonView.h | 1 + ios/LuggPolygonView.mm | 19 +++++++ ios/core/AppleMapProvider.mm | 52 +++++++++++++++++-- ios/core/GoogleMapProvider.mm | 15 ++++++ src/components/Polygon.tsx | 6 +++ src/components/Polygon.web.tsx | 15 ++++-- src/fabric/LuggPolygonViewNativeComponent.ts | 1 + 12 files changed, 171 insertions(+), 8 deletions(-) diff --git a/android/src/main/java/com/luggmaps/LuggPolygonView.kt b/android/src/main/java/com/luggmaps/LuggPolygonView.kt index 91569a7..07064bc 100644 --- a/android/src/main/java/com/luggmaps/LuggPolygonView.kt +++ b/android/src/main/java/com/luggmaps/LuggPolygonView.kt @@ -16,6 +16,9 @@ class LuggPolygonView(context: Context) : ReactViewGroup(context) { var coordinates: List = emptyList() private set + var holes: List> = emptyList() + private set + var strokeColor: Int = Color.BLACK private set @@ -42,6 +45,10 @@ class LuggPolygonView(context: Context) : ReactViewGroup(context) { coordinates = coords } + fun setHoles(value: List>) { + holes = value + } + fun setStrokeColor(color: Int) { strokeColor = color } diff --git a/android/src/main/java/com/luggmaps/LuggPolygonViewManager.kt b/android/src/main/java/com/luggmaps/LuggPolygonViewManager.kt index ed66a8c..94c381d 100644 --- a/android/src/main/java/com/luggmaps/LuggPolygonViewManager.kt +++ b/android/src/main/java/com/luggmaps/LuggPolygonViewManager.kt @@ -47,6 +47,27 @@ class LuggPolygonViewManager : } } + @ReactProp(name = "holes") + override fun setHoles(view: LuggPolygonView, value: ReadableArray?) { + value?.let { array -> + val holesList = mutableListOf>() + for (i in 0 until array.size()) { + val holeArray = array.getArray(i) + val hole = mutableListOf() + if (holeArray != null) { + for (j in 0 until holeArray.size()) { + val coord = holeArray.getMap(j) + val lat = coord?.getDouble("latitude") ?: 0.0 + val lng = coord?.getDouble("longitude") ?: 0.0 + hole.add(LatLng(lat, lng)) + } + } + holesList.add(hole) + } + view.setHoles(holesList) + } + } + @ReactProp(name = "strokeColor", customType = "Color") override fun setStrokeColor(view: LuggPolygonView, value: Int?) { view.setStrokeColor(value ?: android.graphics.Color.BLACK) diff --git a/android/src/main/java/com/luggmaps/core/GoogleMapProvider.kt b/android/src/main/java/com/luggmaps/core/GoogleMapProvider.kt index 08dc587..9fc5486 100644 --- a/android/src/main/java/com/luggmaps/core/GoogleMapProvider.kt +++ b/android/src/main/java/com/luggmaps/core/GoogleMapProvider.kt @@ -540,6 +540,7 @@ class GoogleMapProvider(private val context: Context) : polygonView.polygon?.apply { points = polygonView.coordinates + holes = polygonView.holes fillColor = polygonView.fillColor strokeColor = polygonView.strokeColor strokeWidth = polygonView.strokeWidth.dpToPx() @@ -565,6 +566,10 @@ class GoogleMapProvider(private val context: Context) : .zIndex(polygonView.zIndex) .clickable(true) + for (hole in polygonView.holes) { + options.addHole(hole) + } + val polygon = map.addPolygon(options) polygonView.polygon = polygon polygonToViewMap[polygon] = polygonView diff --git a/docs/POLYGON.md b/docs/POLYGON.md index bd7d633..fd21a01 100644 --- a/docs/POLYGON.md +++ b/docs/POLYGON.md @@ -21,6 +21,27 @@ import { MapView, Polygon } from '@lugg/maps'; strokeWidth={2} onPress={() => console.log('Polygon pressed')} /> + + {/* Polygon with a hole */} + ``` @@ -29,6 +50,7 @@ import { MapView, Polygon } from '@lugg/maps'; | Prop | Type | Default | Description | |------|------|---------|-------------| | `coordinates` | `Coordinate[]` | **required** | Array of coordinates forming the polygon boundary | +| `holes` | `Coordinate[][]` | - | Array of coordinate arrays representing interior holes | | `fillColor` | `ColorValue` | - | Fill color of the polygon | | `strokeColor` | `ColorValue` | - | Stroke (outline) color | | `strokeWidth` | `number` | - | Stroke width in points | diff --git a/example/shared/src/components/Map.tsx b/example/shared/src/components/Map.tsx index 3e76b5f..9ebba0f 100644 --- a/example/shared/src/components/Map.tsx +++ b/example/shared/src/components/Map.tsx @@ -46,6 +46,20 @@ const CIRCLE_COORDS = Array.from({ length: 36 }, (_, i) => { }; }); +const HOLE_RADIUS = 0.0015; +const CIRCLE_HOLES = [ + Array.from({ length: 36 }, (_, i) => { + const angle = (i * 10 * Math.PI) / 180; + return { + latitude: CIRCLE_CENTER.latitude + HOLE_RADIUS * Math.cos(angle), + longitude: + CIRCLE_CENTER.longitude + + (HOLE_RADIUS * Math.sin(angle)) / + Math.cos((CIRCLE_CENTER.latitude * Math.PI) / 180), + }; + }), +]; + const renderMarker = ( marker: MarkerData, onPress?: (event: MarkerPressEvent, marker: MarkerData) => void, @@ -234,6 +248,7 @@ export const Map = forwardRef( *coordinates; +@property(nonatomic, readonly) NSArray *> *holes; @property(nonatomic, readonly) UIColor *strokeColor; @property(nonatomic, readonly) UIColor *fillColor; @property(nonatomic, readonly) CGFloat strokeWidth; diff --git a/ios/LuggPolygonView.mm b/ios/LuggPolygonView.mm index f373f1b..f5f9175 100644 --- a/ios/LuggPolygonView.mm +++ b/ios/LuggPolygonView.mm @@ -17,6 +17,7 @@ @interface LuggPolygonView () @implementation LuggPolygonView { NSArray *_coordinates; + NSArray *> *_holes; UIColor *_strokeColor; UIColor *_fillColor; CGFloat _strokeWidth; @@ -36,6 +37,7 @@ - (instancetype)initWithFrame:(CGRect)frame { _props = defaultProps; _coordinates = @[]; + _holes = @[]; _strokeColor = [UIColor blackColor]; _fillColor = [UIColor colorWithRed:0 green:0 blue:0 alpha:0.3]; _strokeWidth = 1.0; @@ -61,6 +63,19 @@ - (void)updateProps:(Props::Shared const &)props } _coordinates = [coords copy]; + NSMutableArray *> *holesArray = [NSMutableArray array]; + for (const auto &hole : newViewProps.holes) { + NSMutableArray *holeCoords = [NSMutableArray array]; + for (const auto &coord : hole) { + CLLocation *location = + [[CLLocation alloc] initWithLatitude:coord.latitude + longitude:coord.longitude]; + [holeCoords addObject:location]; + } + [holesArray addObject:[holeCoords copy]]; + } + _holes = [holesArray copy]; + if (newViewProps.strokeColor) { UIColor *color = RCTUIColorFromSharedColor(newViewProps.strokeColor); if (color) { @@ -94,6 +109,10 @@ - (void)finalizeUpdates:(RNComponentViewUpdateMask)updateMask { return _coordinates; } +- (NSArray *> *)holes { + return _holes; +} + - (UIColor *)strokeColor { return _strokeColor; } diff --git a/ios/core/AppleMapProvider.mm b/ios/core/AppleMapProvider.mm index 007aa53..873a146 100644 --- a/ios/core/AppleMapProvider.mm +++ b/ios/core/AppleMapProvider.mm @@ -286,7 +286,19 @@ - (LuggPolygonView *)hitTestPolygonAtPoint:(CGPoint)point { } CGPathCloseSubpath(path); - BOOL contains = CGPathContainsPoint(path, NULL, mapPointAsCGP, NO); + for (NSArray *hole in polygonView.holes) { + for (NSUInteger j = 0; j < hole.count; j++) { + MKMapPoint mp = MKMapPointForCoordinate(hole[j].coordinate); + if (j == 0) { + CGPathMoveToPoint(path, NULL, mp.x, mp.y); + } else { + CGPathAddLineToPoint(path, NULL, mp.x, mp.y); + } + } + CGPathCloseSubpath(path); + } + + BOOL contains = CGPathContainsPoint(path, NULL, mapPointAsCGP, YES); CGPathRelease(path); if (contains) @@ -816,8 +828,12 @@ - (void)syncPolygonView:(LuggPolygonView *)polygonView { for (NSUInteger i = 0; i < coordinates.count; i++) { coords[i] = coordinates[i].coordinate; } - MKPolygon *newPolygon = [MKPolygon polygonWithCoordinates:coords - count:coordinates.count]; + NSArray *interiorPolygons = + [self interiorPolygonsFromHoles:polygonView.holes]; + MKPolygon *newPolygon = + [MKPolygon polygonWithCoordinates:coords + count:coordinates.count + interiorPolygons:interiorPolygons]; free(coords); polygonView.polygon = newPolygon; @@ -853,8 +869,12 @@ - (void)addPolygonOverlayToMap:(LuggPolygonView *)polygonView { coords[i] = coordinates[i].coordinate; } - MKPolygon *polygon = [MKPolygon polygonWithCoordinates:coords - count:coordinates.count]; + NSArray *interiorPolygons = + [self interiorPolygonsFromHoles:polygonView.holes]; + MKPolygon *polygon = + [MKPolygon polygonWithCoordinates:coords + count:coordinates.count + interiorPolygons:interiorPolygons]; free(coords); polygonView.polygon = polygon; @@ -862,6 +882,28 @@ - (void)addPolygonOverlayToMap:(LuggPolygonView *)polygonView { [self insertOverlay:polygon withZIndex:polygonView.zIndex]; } +- (NSArray *)interiorPolygonsFromHoles: + (NSArray *> *)holes { + if (holes.count == 0) + return @[]; + + NSMutableArray *interiorPolygons = [NSMutableArray array]; + for (NSArray *hole in holes) { + if (hole.count == 0) + continue; + CLLocationCoordinate2D *holeCoords = (CLLocationCoordinate2D *)malloc( + sizeof(CLLocationCoordinate2D) * hole.count); + for (NSUInteger i = 0; i < hole.count; i++) { + holeCoords[i] = hole[i].coordinate; + } + MKPolygon *interiorPolygon = + [MKPolygon polygonWithCoordinates:holeCoords count:hole.count]; + free(holeCoords); + [interiorPolygons addObject:interiorPolygon]; + } + return [interiorPolygons copy]; +} + - (void)insertOverlay:(id)overlay withZIndex:(NSInteger)zIndex { if (zIndex == 0) { [_mapView addOverlay:overlay]; diff --git a/ios/core/GoogleMapProvider.mm b/ios/core/GoogleMapProvider.mm index eb56b9c..dc401e8 100644 --- a/ios/core/GoogleMapProvider.mm +++ b/ios/core/GoogleMapProvider.mm @@ -562,6 +562,7 @@ - (void)syncPolygonView:(LuggPolygonView *)polygonView { [path addCoordinate:location.coordinate]; } polygon.path = path; + polygon.holes = [self holesPathsFromPolygonView:polygonView]; polygon.fillColor = polygonView.fillColor; polygon.strokeColor = polygonView.strokeColor; polygon.strokeWidth = polygonView.strokeWidth; @@ -569,6 +570,19 @@ - (void)syncPolygonView:(LuggPolygonView *)polygonView { polygon.tappable = polygonView.tappable; } +- (NSArray *)holesPathsFromPolygonView: + (LuggPolygonView *)polygonView { + NSMutableArray *holePaths = [NSMutableArray array]; + for (NSArray *hole in polygonView.holes) { + GMSMutablePath *holePath = [GMSMutablePath path]; + for (CLLocation *location in hole) { + [holePath addCoordinate:location.coordinate]; + } + [holePaths addObject:holePath]; + } + return [holePaths copy]; +} + - (void)processPendingPolygons { if (!_mapView) return; @@ -589,6 +603,7 @@ - (void)addPolygonViewToMap:(LuggPolygonView *)polygonView { } GMSPolygon *polygon = [GMSPolygon polygonWithPath:path]; + polygon.holes = [self holesPathsFromPolygonView:polygonView]; polygon.fillColor = polygonView.fillColor; polygon.strokeColor = polygonView.strokeColor; polygon.strokeWidth = polygonView.strokeWidth; diff --git a/src/components/Polygon.tsx b/src/components/Polygon.tsx index 80fb426..30ff29c 100644 --- a/src/components/Polygon.tsx +++ b/src/components/Polygon.tsx @@ -9,6 +9,10 @@ export interface PolygonProps { * Array of coordinates forming the polygon boundary */ coordinates: Coordinate[]; + /** + * Array of coordinate arrays representing interior holes + */ + holes?: Coordinate[][]; /** * Stroke (outline) color */ @@ -47,6 +51,7 @@ export class Polygon extends React.PureComponent { render() { const { coordinates, + holes, strokeColor, strokeWidth, fillColor, @@ -58,6 +63,7 @@ export class Polygon extends React.PureComponent { ({ + const outerPath = coordinates.map((c) => ({ lat: c.latitude, lng: c.longitude, })); + const paths = [ + outerPath, + ...(holes ?? []).map((hole) => + hole.map((c) => ({ lat: c.latitude, lng: c.longitude })) + ), + ]; + if (polygonRef.current) { - polygonRef.current.setPath(path); + polygonRef.current.setPaths(paths); polygonRef.current.setOptions({ strokeColor: strokeColor as string, strokeWeight: strokeWidth, @@ -87,7 +95,7 @@ export function Polygon({ }); } else { const polygon = new google.maps.Polygon({ - paths: path, + paths, strokeColor: strokeColor as string, strokeWeight: strokeWidth, strokeOpacity: 1, @@ -111,6 +119,7 @@ export function Polygon({ }, [ map, coordinates, + holes, strokeColor, strokeWidth, fillColor, diff --git a/src/fabric/LuggPolygonViewNativeComponent.ts b/src/fabric/LuggPolygonViewNativeComponent.ts index 38cad6a..6a178ac 100644 --- a/src/fabric/LuggPolygonViewNativeComponent.ts +++ b/src/fabric/LuggPolygonViewNativeComponent.ts @@ -12,6 +12,7 @@ export interface Coordinate { export interface NativeProps extends ViewProps { coordinates: ReadonlyArray; + holes?: ReadonlyArray>; strokeColor?: ColorValue; strokeWidth?: Double; fillColor?: ColorValue; From e8b99015693202c1232119629748e26d9ebfb163 Mon Sep 17 00:00:00 2001 From: lodev09 Date: Thu, 5 Mar 2026 02:13:18 +0800 Subject: [PATCH 2/4] fix: reverse hole winding order for web polygon rendering --- src/components/Polygon.web.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Polygon.web.tsx b/src/components/Polygon.web.tsx index 56b2980..bf596fc 100644 --- a/src/components/Polygon.web.tsx +++ b/src/components/Polygon.web.tsx @@ -81,7 +81,7 @@ export function Polygon({ const paths = [ outerPath, ...(holes ?? []).map((hole) => - hole.map((c) => ({ lat: c.latitude, lng: c.longitude })) + [...hole].reverse().map((c) => ({ lat: c.latitude, lng: c.longitude })) ), ]; From 4b640937e1bba0184f34ce02750c417451dea213 Mon Sep 17 00:00:00 2001 From: lodev09 Date: Thu, 5 Mar 2026 02:34:59 +0800 Subject: [PATCH 3/4] feat: add onPress and onLongPress support to web MapView --- example/shared/src/Home.tsx | 11 ++++-- ios/core/AppleMapProvider.mm | 18 ++++----- src/MapView.web.tsx | 73 ++++++++++++++++++++++++++++++++++++ 3 files changed, 88 insertions(+), 14 deletions(-) diff --git a/example/shared/src/Home.tsx b/example/shared/src/Home.tsx index a5b1ad3..67ba56d 100644 --- a/example/shared/src/Home.tsx +++ b/example/shared/src/Home.tsx @@ -134,7 +134,7 @@ function HomeContent() { [] ); - const addMarker = () => { + const addMarker = (coordinate = lastCoordinate.current) => { const type = randomFrom(MARKER_TYPES); const id = Date.now().toString(); @@ -143,7 +143,7 @@ function HomeContent() { { id, name: `marker-${id}`, - coordinate: lastCoordinate.current, + coordinate, type, anchor: { x: 0.5, y: type === 'icon' ? 1 : 0.5 }, text: randomLetter(), @@ -190,7 +190,10 @@ function HomeContent() { userLocationEnabled={locationPermission} onReady={handleMapReady} onPress={(e) => formatPressEvent(e, 'Press')} - onLongPress={(e) => formatPressEvent(e, 'Long press')} + onLongPress={(e) => { + formatPressEvent(e, 'Long press'); + addMarker(e.nativeEvent.coordinate); + }} onCameraMove={(e) => formatCameraEvent(e, false)} onCameraIdle={(e) => formatCameraEvent(e, true)} onMarkerPress={(e, m) => formatPressEvent(e, `Marker(${m.name})`)} @@ -222,7 +225,7 @@ function HomeContent() { > {statusText} -