⚠ This page is served via a proxy. Original site: https://github.com
This service does not collect credentials or authentication data.
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 101 additions & 21 deletions client/dive-common/components/DeleteControls.vue
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,15 @@ export default Vue.extend({
}
return false;
},
isPolygonMode(): boolean {
return this.editingMode === 'Polygon';
},
editModeIcon(): string {
if (this.editingMode === 'Polygon') return 'mdi-vector-polygon';
if (this.editingMode === 'LineString') return 'mdi-vector-line';
if (this.editingMode === 'rectangle') return 'mdi-vector-square';
return 'mdi-shape';
},
},

methods: {
Expand All @@ -39,33 +48,104 @@ export default Vue.extend({
this.$emit('delete-annotation');
}
},
addHole() {
this.$emit('add-hole');
},
addPolygon() {
this.$emit('add-polygon');
},
},
});
</script>

<template>
<span class="mx-1">
<v-btn
<span class="mx-1 d-flex align-center">
<!-- Add Polygon button - shown in polygon edit mode -->
<v-tooltip
v-if="isPolygonMode"
bottom
>
<template #activator="{ on, attrs }">
<v-btn
v-bind="attrs"
color="primary"
depressed
small
class="mr-1"
v-on="on"
@click="addPolygon"
>
<v-icon small>
mdi-vector-polygon
</v-icon>
<v-icon
x-small
class="ml-n1"
>
mdi-plus-circle-outline
</v-icon>
</v-btn>
</template>
<span>Add another polygon</span>
</v-tooltip>

<!-- Add Hole button - shown in polygon edit mode -->
<v-tooltip
v-if="isPolygonMode"
bottom
>
<template #activator="{ on, attrs }">
<v-btn
v-bind="attrs"
color="primary"
depressed
small
class="mr-1"
v-on="on"
@click="addHole"
>
<v-icon small>
mdi-vector-polygon
</v-icon>
<v-icon
x-small
class="ml-n1"
>
mdi-minus-circle-outline
</v-icon>
</v-btn>
</template>
<span>Add hole to polygon</span>
</v-tooltip>

<!-- Delete button -->
<v-tooltip
v-if="!disabled"
color="error"
depressed
small
@click="deleteSelected"
bottom
>
<pre class="mr-1 text-body-2">del</pre>
<span v-if="selectedFeatureHandle >= 0">
point {{ selectedFeatureHandle }}
</span>
<span v-else-if="editingMode">
{{ editingMode }}
</span>
<span v-else>unselected</span>
<v-icon
small
class="ml-2"
>
mdi-delete
</v-icon>
</v-btn>
<template #activator="{ on, attrs }">
<v-btn
v-bind="attrs"
color="error"
depressed
small
v-on="on"
@click="deleteSelected"
>
<span class="mr-1 text-body-2 font-weight-bold">DEL</span>
<span v-if="selectedFeatureHandle >= 0">
pt{{ selectedFeatureHandle }}
</span>
<v-icon
v-else
small
>
{{ editModeIcon }}
</v-icon>
</v-btn>
</template>
<span v-if="selectedFeatureHandle >= 0">Delete point {{ selectedFeatureHandle }}</span>
<span v-else-if="editingMode">Delete {{ editingMode }}</span>
</v-tooltip>
</span>
</template>
2 changes: 2 additions & 0 deletions client/dive-common/components/Viewer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -1010,6 +1010,8 @@ export default defineComponent({
class="mr-2"
@delete-point="handler.removePoint"
@delete-annotation="handler.removeAnnotation"
@add-hole="handler.addHole"
@add-polygon="handler.addPolygon"
/>
</template>
<template
Expand Down
201 changes: 196 additions & 5 deletions client/dive-common/recipes/polygonbase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,49 @@ import { EditAnnotationTypes } from 'vue-media-annotator/layers';

const EmptyResponse = { data: {}, union: [], unionWithoutBounds: [] };

/**
* Check if a point is inside a polygon using ray casting algorithm
* @param point [x, y] coordinates
* @param polygon array of [x, y] coordinates forming the polygon (outer ring only)
* @returns true if point is inside polygon
*/
function isPointInsidePolygon(point: [number, number], polygon: GeoJSON.Position[]): boolean {
const [x, y] = point;
let inside = false;

for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i, i += 1) {
const xi = polygon[i][0];
const yi = polygon[i][1];
const xj = polygon[j][0];
const yj = polygon[j][1];

const intersect = ((yi > y) !== (yj > y))
&& (x < ((xj - xi) * (y - yi)) / (yj - yi) + xi);

if (intersect) {
inside = !inside;
}
}

return inside;
}

/**
* Check if all vertices of polygon P are inside polygon E
* @param innerPolygon polygon to check if it's inside
* @param outerPolygon polygon to check if it contains the inner polygon
* @returns true if all vertices of innerPolygon are inside outerPolygon
*/
function isPolygonInsidePolygon(
innerPolygon: GeoJSON.Position[],
outerPolygon: GeoJSON.Position[],
): boolean {
// Check if all vertices of the inner polygon are inside the outer polygon
return innerPolygon.every(
(vertex) => isPointInsidePolygon([vertex[0], vertex[1]], outerPolygon),
);
}

export default class PolygonBoundsExpand implements Recipe {
active: Ref<boolean>;

Expand All @@ -19,12 +62,41 @@ export default class PolygonBoundsExpand implements Recipe {

bus: Vue;

// Mode for adding polygons: 'normal', 'hole', or 'newPolygon'
addingMode: Ref<'normal' | 'hole' | 'newPolygon'>;

constructor() {
this.bus = new Vue();
this.active = ref(false);
this.name = 'PolygonBase';
this.toggleable = ref(true);
this.icon = ref('mdi-vector-polygon');
this.addingMode = ref('normal');
}

setAddingHole() {
this.addingMode.value = 'hole';
// Emit activate event with special key to trigger creation mode
// The special key ensures no geometry matches, forcing creation mode
this.bus.$emit('activate', {
editing: 'Polygon' as EditAnnotationTypes,
key: '__adding_hole__',
recipeName: this.name,
});
}

setAddingPolygon(newKey: string) {
this.addingMode.value = 'newPolygon';
// Emit activate event with new key to trigger creation mode
this.bus.$emit('activate', {
editing: 'Polygon' as EditAnnotationTypes,
key: newKey,
recipeName: this.name,
});
}

resetAddingMode() {
this.addingMode.value = 'normal';
}

update(
Expand All @@ -37,32 +109,151 @@ export default class PolygonBoundsExpand implements Recipe {
if (data.length === 1 && mode === 'editing' && this.active.value) {
const poly = data[0].geometry;
if (poly.type === 'Polygon') {
const newPolyCoords = poly.coordinates[0] as GeoJSON.Position[];
const currentMode = this.addingMode.value;

// Reset adding mode after processing
this.resetAddingMode();

if (currentMode === 'hole' || key === '__adding_hole__') {
// Adding a hole - find the first polygon and add hole to it
const existingPolygons = track.getPolygonFeatures(frameNum);
if (existingPolygons.length > 0) {
// Add as hole to the first (default) polygon
const targetPoly = existingPolygons[0];
// Create updated polygon geometry with the hole added
const updatedCoordinates = [
...targetPoly.geometry.coordinates,
newPolyCoords,
];
const updatedPolygon: GeoJSON.Polygon = {
type: 'Polygon',
coordinates: updatedCoordinates,
};
const updatedFeature: GeoJSON.Feature<GeoJSON.Polygon> = {
type: 'Feature',
properties: { key: targetPoly.key },
geometry: updatedPolygon,
};
// Return data like add polygon mode so right-click behavior is consistent
return {
data: {
[targetPoly.key]: [updatedFeature],
},
union: [],
done: true,
unionWithoutBounds: [],
newSelectedKey: targetPoly.key, // Set to target polygon's key for proper mode transition
};
}
// No existing polygon, treat as normal (create first polygon)
return {
data: {
'': data,
},
union: [],
done: true,
unionWithoutBounds: [poly],
newSelectedKey: '',
};
}

if (currentMode === 'newPolygon') {
// Adding a new separate polygon - key should already be set to new value
const useKey = key || track.getNextPolygonKey(frameNum);
const newFeature: GeoJSON.Feature<GeoJSON.Polygon> = {
type: 'Feature',
properties: { key: useKey },
geometry: poly,
};
return {
data: {
[useKey]: [newFeature],
},
union: [poly], // Use union to EXPAND bounds, not replace them
done: true,
unionWithoutBounds: [],
newSelectedKey: '', // Reset to default polygon for future edits
};
}

// Standard case: save polygon with the given key
// Calculate bounds from ALL polygons in the detection, not just the edited one
const allPolygons = track.getPolygonFeatures(frameNum);
const otherPolygons: GeoJSON.Polygon[] = allPolygons
.filter((p) => p.key !== (key || ''))
.map((p) => p.geometry);

return {
data: {
[key || '']: data,
},
union: [],
// Use union with other polygons to ensure bounds encompass all
union: otherPolygons,
done: true,
// The edited polygon replaces the base bounds
unionWithoutBounds: [poly],
};
}
}
return EmptyResponse;
}

/**
* Add a polygon as a hole to an existing polygon, or as a new separate polygon.
* Call this method explicitly when auto-hole detection is desired.
*/
// eslint-disable-next-line class-methods-use-this
addPolygonWithHoleDetection(
frameNum: number,
track: Track,
poly: GeoJSON.Polygon,
key?: string,
) {
const newPolyCoords = poly.coordinates[0] as GeoJSON.Position[];

// Get existing polygons for this frame
const existingPolygons = track.getPolygonFeatures(frameNum);

// Check if this is an edit to an existing polygon (key matches)
const isExistingEdit = existingPolygons.some((p) => p.key === (key || ''));

if (!isExistingEdit && existingPolygons.length > 0) {
// This is a new polygon - check if it should be a hole in an existing polygon
const containingPoly = existingPolygons.find((existingPoly) => {
const outerRing = existingPoly.geometry.coordinates[0] as GeoJSON.Position[];
return isPolygonInsidePolygon(newPolyCoords, outerRing);
});

if (containingPoly) {
// New polygon is inside existing polygon - add as hole
track.addHoleToPolygon(frameNum, containingPoly.key, newPolyCoords);
return { isHole: true, key: containingPoly.key };
}

// Not inside any existing polygon - add as new separate polygon with auto-key
const newKey = track.getNextPolygonKey(frameNum);
return { isHole: false, key: newKey };
}

// Standard case: use provided key or default
return { isHole: false, key: key || '' };
}

// eslint-disable-next-line class-methods-use-this
delete(frame: number, track: Track, key: string, type: EditAnnotationTypes) {
if (key === '' && type === 'Polygon') {
track.removeFeatureGeometry(frame, { key: '', type: 'Polygon' });
if (type === 'Polygon') {
// Remove polygon with the specified key (supports multiple polygons)
track.removeFeatureGeometry(frame, { key, type: 'Polygon' });
}
}

// eslint-disable-next-line class-methods-use-this
deletePoint(frame: number, track: Track, idx: number, key: string, type: EditAnnotationTypes) {
if (key === '' && type === 'Polygon') {
if (type === 'Polygon') {
const geoJsonFeatures = track.getFeatureGeometry(frame, {
type: 'Polygon',
key: '',
key,
});
if (geoJsonFeatures.length === 0) return;
const clone = cloneDeep(geoJsonFeatures[0]);
Expand Down
Loading
Loading