feat(agent): share Web UI compatible drawing builders between MCP and copilot

The text drawing tool was MCP-only; the copilot needs the same
capability and its area-annotation helper carried the same Web UI
compat bugs. Split into three layers with a single implementation:

- gns3server/utils/drawings.py: pure SVG builders (text, rectangle,
  ellipse) with full input validation (ValueError) — the Web UI
  format knowledge lives in core utils next to picture.py
- api_handlers.py: shared REST handlers (create_text_drawing_handler
  moved here, create_rectangle/ellipse_drawing_handler added) — the
  layer both consumers already share
- thin shells: MCP tools (drawing_create_text unchanged, plus new
  drawing_create_rectangle/_ellipse) and three LangChain tools
  (tools_v2/gns3_create_drawing.py) registered in both copilot mode
  lists

Shape builder details:

- rectangle: solid fill or "none", stroke block emitted only when a
  stroke is given, rx corner radius, dasharray validated against the
  Web UI's Qt remap table (QtDasharrayFixer rewrites those patterns)
- ellipse: bounding-box API; cx/cy derived as offsets inside the
  drawing box (the Web UI treats them as box-relative, not canvas
  coordinates); odd sizes round down and the actual size is returned

gns3_drawing_utils now delegates to the shared builders, fixing the
px-blind sizing, the ignored text-anchor/x/y centering and the
missing XML escaping in the area-annotation path.
This commit is contained in:
YueGuobin committed 2026-10-09 11:12:18 +08:00
1 parent 73cbb0387c
commit 5e8272fdd8
13 files changed
+1003 -265

No files matched your search

@@ -33,6 +33,8 @@ Supports ellipse and rectangle shapes for two-node annotations.
import math
from typing import Any, Literal
from gns3server.utils.drawings import build_ellipse_svg, build_rectangle_svg, build_text_svg
# Default parameters
DEFAULT_DEVICE_WIDTH = 50
DEFAULT_DEVICE_HEIGHT = 50
@@ -144,10 +146,16 @@ def calculate_two_node_shape(
center_y = (node1_center_y + node2_center_y) / 2
color_scheme = _get_color_scheme(area_name)
text_svg = generate_text_svg(area_name, color_scheme)
text_svg_width = len(area_name) * 8 + 20
text_svg_height = DEFAULT_FONT_SIZE + 16
# Shared builder: Web UI compatible SVG plus the computed label size
# (the old len * 8 + 20 estimate ignored the Web UI's pt-based rendering).
text_svg, text_svg_width, text_svg_height, _ = build_text_svg(
area_name,
font_size=DEFAULT_FONT_SIZE,
color=color_scheme["stroke"],
bold=True,
font_family="TypeWriter",
)
if shape_type == "ellipse":
rx = distance / 2
@@ -236,8 +244,11 @@ def generate_ellipse_svg(
svg_width: int,
svg_height: int,
) -> str:
"""Generate SVG for ellipse."""
return f"""<svg width="{svg_width}" height="{svg_height}"><ellipse cx="{rx}" cy="{ry}" rx="{rx}" ry="{ry}" fill="{color_scheme["fill"]}" fill-opacity="{color_scheme["fill_opacity"]}"/></svg>"""
"""Generate SVG for ellipse (delegates to the shared Web UI compatible builder)."""
svg, _, _ = build_ellipse_svg(
rx * 2, ry * 2, fill=color_scheme["fill"], fill_opacity=color_scheme["fill_opacity"]
)
return svg
def generate_rectangle_svg(
@@ -245,16 +256,23 @@ def generate_rectangle_svg(
height: int,
color_scheme: dict[str, Any],
) -> str:
"""Generate SVG for rectangle."""
return f"""<svg width="{width}" height="{height}"><rect x="0" y="0" width="{width}" height="{height}" fill="{color_scheme["fill"]}" fill-opacity="{color_scheme["fill_opacity"]}"/></svg>"""
"""Generate SVG for rectangle (delegates to the shared Web UI compatible builder)."""
svg, _, _ = build_rectangle_svg(
width, height, fill=color_scheme["fill"], fill_opacity=color_scheme["fill_opacity"]
)
return svg
def generate_text_svg(text: str, color_scheme: dict[str, Any]) -> str:
"""Generate SVG for text label."""
text_width = len(text) * 8 + 20
text_height = DEFAULT_FONT_SIZE + 16
return f"""<svg width="{text_width}" height="{text_height}"><text font-family="TypeWriter" font-size="{DEFAULT_FONT_SIZE}.0" font-weight="bold" fill="{color_scheme["stroke"]}" text-anchor="middle" x="{text_width / 2}" y="{text_height / 2 + 4}">{text}</text></svg>"""
"""Generate SVG for text label (delegates to the shared Web UI compatible builder)."""
svg, _, _, _ = build_text_svg(
text,
font_size=DEFAULT_FONT_SIZE,
color=color_scheme["stroke"],
bold=True,
font_family="TypeWriter",
)
return svg
def _hsv_to_hex(h: int, s: int, v: int) -> str: