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
2 changes: 1 addition & 1 deletion BACKLOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ Description formats:
| 027 | Infrastructure | [Add automated screenshot capture for Storybook stories](docs/ideas/027-automated-screenshots.md) | 3 | 4 | 4 | 11 | Medium | approved |
| 088 | Feature | [Custom editor provider for result datasets](docs/ideas/E04-results-visualization.md) [E04] — opens results as editor tabs, supports drag-to-float via auxiliary windows (requires #085) | 3 | 4 | 4 | 11 | Medium | approved |
| 005 | Tech Debt | Add cross-service end-to-end workflow tests (io -> stac -> calc) | 4 | 2 | 5 | 11 | Low | approved |
| 057 | Feature | [Add enlarge shape tool spec](docs/ideas/057-enlarge-shape.md) (requires #049) | 3 | 3 | 5 | 11 | Low | approved |
| 057 | Feature | [Add enlarge shape tool spec](specs/057-enlarge-shape/spec.md) (requires #049) | 3 | 3 | 5 | 11 | Low | specified |
| 078 | Feature | [Implement generate-reference-points tool](docs/ideas/E03-buffer-zone-analysis-demo.md) [E03] — creates grid/scatter of reference points on plot (requires #049) | 3 | 3 | 5 | 11 | Low | approved |
| 008 | Feature | Design and implement extension discovery mechanism for contrib packages | 4 | 3 | 3 | 10 | High | approved |
| 090 | Infrastructure | [E04 sample data workshop: identify realistic generators for all result types](docs/ideas/090-e04-sample-data-workshop.md) [E04] — collaborative workshop using E03 demo scenario; pseudocode + golden fixtures for all result categories (prerequisite for #085) | 5 | 3 | 2 | 10 | Low | approved |
Expand Down
197 changes: 197 additions & 0 deletions apps/vscode/src/tools/shape/manipulation/enlargeShape.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
/**
* Enlarge Shape tool implementation.
* Scales annotation shapes by a multiplicative factor relative to an origin point.
*
* Spec: shared/tools/shape/manipulation/enlarge-shape.1.0.md
*/

import type { MCPToolDefinition } from '../../../types/tool';

export interface EnlargeShapeParams {
scale_factor?: number;
origin?: number[] | null;
}

export const toolDefinition: MCPToolDefinition = {
name: 'enlarge-shape',
description:
'Scales annotation shapes by a multiplicative factor relative to an origin point. Uses linear interpolation of geographic coordinate differences.',
inputSchema: {
type: 'object',
properties: {
features: { type: 'array', items: { type: 'object' } },
params: {
type: 'object',
properties: {
scale_factor: {
type: 'number',
description: 'Multiplicative scaling factor (>1 enlarges, <1 shrinks, 1 is no-op)',
default: 3.0,
},
origin: {
type: 'array',
description: 'Scaling origin as [longitude, latitude]. Default: geometric centroid.',
items: { type: 'number' },
},
},
},
},
},
annotations: {
'debrief:selectionRequirements': [
{ kind: 'CIRCLE', min: 1 },
{ kind: 'RECTANGLE', min: 1 },
{ kind: 'LINE', min: 1 },
{ kind: 'TEXT', min: 1 },
{ kind: 'VECTOR', min: 1 },
],
'debrief:category': 'shape/manipulation',
'debrief:version': '1.0.0',
'debrief:outputKind': 'mutation/shape/scaled',
},
};

interface GeoJSONFeature {
type: 'Feature';
id?: string;
geometry: { type: string; coordinates: unknown };
properties: Record<string, unknown>;
}

const ANNOTATION_KINDS = new Set(['CIRCLE', 'RECTANGLE', 'LINE', 'TEXT', 'VECTOR']);

/**
* Compute arithmetic mean of vertices as the geometric centroid.
*/
function computeCentroid(geometry: { type: string; coordinates: unknown }): number[] {
const geoType = geometry.type;
const coords = geometry.coordinates;

if (geoType === 'Point') {
return [...(coords as number[])];
}

let vertices: number[][];

if (geoType === 'Polygon') {
const ring = (coords as number[][][])[0]!;
// Exclude closing vertex if first == last
if (
ring.length > 1 &&
ring[0]![0] === ring[ring.length - 1]![0] &&
ring[0]![1] === ring[ring.length - 1]![1]
) {
vertices = ring.slice(0, -1);
} else {
vertices = ring;
}
} else if (geoType === 'LineString') {
vertices = coords as number[][];
} else {
vertices = coords as number[][];
}

const n = vertices.length;
if (n === 0) return [0, 0];

let sumLon = 0;
let sumLat = 0;
for (const v of vertices) {
sumLon += v[0]!;
sumLat += v[1]!;
}

return [sumLon / n, sumLat / n];
}

/**
* Scale a single [lon, lat] coordinate relative to an origin.
*/
function scaleCoordinate(
coord: number[],
origin: number[],
scaleFactor: number,
): number[] {
let newLon = origin[0]! + (coord[0]! - origin[0]!) * scaleFactor;
let newLat = origin[1]! + (coord[1]! - origin[1]!) * scaleFactor;

// Clamp latitude to [-90, 90]
newLat = Math.max(-90, Math.min(90, newLat));

// Normalise longitude to [-180, 180]
newLon = ((newLon + 180) % 360) - 180;

return [newLon, newLat];
}

function scaleCoordsList(
coords: number[][],
origin: number[],
scaleFactor: number,
): number[][] {
return coords.map((c) => scaleCoordinate(c, origin, scaleFactor));
}

export function execute(
features: GeoJSONFeature[],
params: EnlargeShapeParams,
): GeoJSONFeature[] {
const scaleFactor = params.scale_factor ?? 3.0;
const origin = params.origin ?? null;

if (scaleFactor < 0) {
throw new Error('scale_factor must be >= 0');
}

const modified: GeoJSONFeature[] = [];

for (const feature of features) {
const props = feature.properties ?? {};
const kind = props.kind as string;

if (!ANNOTATION_KINDS.has(kind)) {
continue;
}

const geometry = feature.geometry;

// Determine scaling origin
const scalingOrigin = origin ?? computeCentroid(geometry);

// Scale factor of 1.0 is a no-op
if (scaleFactor === 1.0) {
modified.push(feature);
continue;
}

const coords = geometry.coordinates;

if (kind === 'CIRCLE' || kind === 'RECTANGLE') {
// Polygon: scale all rings
const polyCoords = coords as number[][][];
geometry.coordinates = polyCoords.map((ring) =>
scaleCoordsList(ring, scalingOrigin, scaleFactor),
);
if (kind === 'CIRCLE' && props.center) {
props.center = scaleCoordinate(props.center as number[], scalingOrigin, scaleFactor);
}
} else if (kind === 'LINE') {
geometry.coordinates = scaleCoordsList(coords as number[][], scalingOrigin, scaleFactor);
} else if (kind === 'TEXT') {
geometry.coordinates = scaleCoordinate(coords as number[], scalingOrigin, scaleFactor);
} else if (kind === 'VECTOR') {
geometry.coordinates = scaleCoordsList(coords as number[][], scalingOrigin, scaleFactor);
if (props.origin) {
props.origin = scaleCoordinate(props.origin as number[], scalingOrigin, scaleFactor);
}
}

modified.push(feature);
}

if (modified.length === 0) {
throw new Error('No annotation features found in input');
}

return modified;
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
"""Shape manipulation tools."""

from debrief_calc.tools.shape.manipulation.enlarge_shape import enlarge_shape
from debrief_calc.tools.shape.manipulation.move_shape import move_shape

__all__ = [
"enlarge_shape",
"move_shape",
]
Loading
Loading