j138 draw-only: yellow triangle (enemy-heading other point, enemy, us) in the geometry overlay
This commit is contained in:
@@ -12,7 +12,15 @@
|
|||||||
## through a point that is ON the circle meets it twice — the bot itself and
|
## through a point that is ON the circle meets it twice — the bot itself and
|
||||||
## ONE OTHER point — and the OTHER point is drawn visually distinct (that is
|
## ONE OTHER point — and the OTHER point is drawn visually distinct (that is
|
||||||
## the one worth looking at). A tangent line's two points coincide; we draw
|
## the one worth looking at). A tangent line's two points coincide; we draw
|
||||||
## what the maths gives.
|
## what the maths gives;
|
||||||
|
## * the TRIANGLE whose corners are the ENEMY heading line's OTHER
|
||||||
|
## intersection, the enemy, and our bot — three vertices the rest of the
|
||||||
|
## overlay already computes. Drawn YELLOW (#FFFF00), stroke width 1.5,
|
||||||
|
## with a small yellow hollow ring (r=6) on each distinct corner, so it is
|
||||||
|
## not confused with the grey circle / cyan / orange heading lines. A
|
||||||
|
## zero-length edge (a tangent enemy heading puts corner 1 on top of
|
||||||
|
## corner 2; or corner 1 on top of corner 3 when the enemy heading points
|
||||||
|
## straight at us) is SKIPPED rather than emitted as a degenerate line.
|
||||||
##
|
##
|
||||||
## Env knobs (read once at boot by ModularBot, registered in env_report.nim):
|
## Env knobs (read once at boot by ModularBot, registered in env_report.nim):
|
||||||
## TR_GEO_DEBUG presence/1 = on. default OFF.
|
## TR_GEO_DEBUG presence/1 = on. default OFF.
|
||||||
@@ -103,6 +111,8 @@ type
|
|||||||
gpLineEnemy ## line along the ENEMY heading
|
gpLineEnemy ## line along the ENEMY heading
|
||||||
gpMarkSelf ## intersection that coincides with the bot (hollow ring)
|
gpMarkSelf ## intersection that coincides with the bot (hollow ring)
|
||||||
gpMarkOther ## the OTHER intersection (filled, distinct)
|
gpMarkOther ## the OTHER intersection (filled, distinct)
|
||||||
|
gpTriangleEdge ## one edge of the enemy-heading triangle
|
||||||
|
gpTriangleCorner ## a DISTINCT corner of that triangle (hollow ring)
|
||||||
|
|
||||||
GeoPrim* = object
|
GeoPrim* = object
|
||||||
kind*: GeoPrimKind
|
kind*: GeoPrimKind
|
||||||
@@ -117,11 +127,17 @@ const
|
|||||||
GeoEnemyLineColor* = "#FF8000" # orange— ENEMY heading line
|
GeoEnemyLineColor* = "#FF8000" # orange— ENEMY heading line
|
||||||
GeoSelfMarkColor* = "#FFFFFF" # white — the bot itself (hollow ring)
|
GeoSelfMarkColor* = "#FFFFFF" # white — the bot itself (hollow ring)
|
||||||
GeoOtherMarkColor* = "#FF00FF" # magenta — the OTHER intersection (filled)
|
GeoOtherMarkColor* = "#FF00FF" # magenta — the OTHER intersection (filled)
|
||||||
|
GeoTriangleColor* = "#FFFF00" # yellow — the requested triangle (edges+corner rings)
|
||||||
GeoCircleWidth* = 1.0
|
GeoCircleWidth* = 1.0
|
||||||
GeoLineWidth* = 1.0
|
GeoLineWidth* = 1.0
|
||||||
GeoMarkWidth* = 1.5
|
GeoMarkWidth* = 1.5
|
||||||
|
GeoTriangleWidth* = 1.5
|
||||||
GeoSelfMarkRadius* = 4.0
|
GeoSelfMarkRadius* = 4.0
|
||||||
GeoOtherMarkRadius* = 4.0
|
GeoOtherMarkRadius* = 4.0
|
||||||
|
GeoTriangleCornerRadius* = 6.0
|
||||||
|
GeoZeroLen* = 1e-6
|
||||||
|
## Below this two triangle corners are the SAME point: the edge between
|
||||||
|
## them is degenerate (skipped) and they share a single corner ring.
|
||||||
GeoLineMargin* = 10.0
|
GeoLineMargin* = 10.0
|
||||||
## Extra length (px) the heading segment extends beyond the far
|
## Extra length (px) the heading segment extends beyond the far
|
||||||
## intersection; a segment of half-length 2r + margin always contains both
|
## intersection; a segment of half-length 2r + margin always contains both
|
||||||
@@ -157,6 +173,36 @@ proc headingSegment(px, py, headingDeg, halfLen: float): tuple[x1, y1, x2, y2: f
|
|||||||
let dy = sin(h) * halfLen
|
let dy = sin(h) * halfLen
|
||||||
(px - dx, py - dy, px + dx, py + dy)
|
(px - dx, py - dy, px + dx, py + dy)
|
||||||
|
|
||||||
|
proc samePoint(a, b: GeomPoint): bool {.inline.} =
|
||||||
|
hypot(b.x - a.x, b.y - a.y) < GeoZeroLen
|
||||||
|
|
||||||
|
proc trianglePrims*(enemyOther, enemyPos, selfPos: GeomPoint): seq[GeoPrim] =
|
||||||
|
## DRAW-ONLY: the owner-requested triangle whose three corners are, in order,
|
||||||
|
## the ENEMY heading line's OTHER intersection with the diameter circle, the
|
||||||
|
## enemy itself, and our bot. Returns its three edges plus one hollow corner
|
||||||
|
## ring per DISTINCT corner, all in `GeoTriangleColor` (yellow, width 1.5).
|
||||||
|
##
|
||||||
|
## Degenerate handling, no new geometry is derived:
|
||||||
|
## * a zero-length edge is SKIPPED instead of drawn as a degenerate line.
|
||||||
|
## This happens when the enemy heading is TANGENT to the circle (corner 1
|
||||||
|
## coincides with corner 2) or points straight at us (corner 1 coincides
|
||||||
|
## with corner 3);
|
||||||
|
## * coincident corners share a single ring, so no zero-radius garbage.
|
||||||
|
let vs = [enemyOther, enemyPos, selfPos]
|
||||||
|
for i in 0 ..< 3:
|
||||||
|
let a = vs[i]
|
||||||
|
let b = vs[(i + 1) mod 3]
|
||||||
|
if samePoint(a, b): continue
|
||||||
|
result.add GeoPrim(kind: gpTriangleEdge, x1: a.x, y1: a.y, x2: b.x, y2: b.y,
|
||||||
|
color: GeoTriangleColor)
|
||||||
|
for i in 0 ..< 3:
|
||||||
|
var dup = false
|
||||||
|
for j in 0 ..< i:
|
||||||
|
if samePoint(vs[i], vs[j]): dup = true; break
|
||||||
|
if dup: continue
|
||||||
|
result.add GeoPrim(kind: gpTriangleCorner, cx: vs[i].x, cy: vs[i].y,
|
||||||
|
r: GeoTriangleCornerRadius, color: GeoTriangleColor)
|
||||||
|
|
||||||
proc collectGeoPrims*(selfX, selfY, selfHeading,
|
proc collectGeoPrims*(selfX, selfY, selfHeading,
|
||||||
enemyX, enemyY, enemyHeading: float): seq[GeoPrim] =
|
enemyX, enemyY, enemyHeading: float): seq[GeoPrim] =
|
||||||
## Pure: the primitives the overlay draws. Empty in the degenerate case where
|
## Pure: the primitives the overlay draws. Empty in the degenerate case where
|
||||||
@@ -194,6 +240,14 @@ proc collectGeoPrims*(selfX, selfY, selfHeading,
|
|||||||
cy: soEnemy.other.y, r: GeoOtherMarkRadius,
|
cy: soEnemy.other.y, r: GeoOtherMarkRadius,
|
||||||
color: GeoOtherMarkColor)
|
color: GeoOtherMarkColor)
|
||||||
|
|
||||||
|
# The owner-requested TRIANGLE: the enemy heading's OTHER intersection,
|
||||||
|
# the enemy, and our bot. All three corners were already computed just
|
||||||
|
# above — nothing is re-derived here.
|
||||||
|
for p in trianglePrims(soEnemy.other,
|
||||||
|
GeomPoint(x: enemyX, y: enemyY),
|
||||||
|
GeomPoint(x: selfX, y: selfY)):
|
||||||
|
result.add p
|
||||||
|
|
||||||
# ── emit to the GUI debug-graphics buffer ────────────────────────────────────
|
# ── emit to the GUI debug-graphics buffer ────────────────────────────────────
|
||||||
|
|
||||||
proc emitGeoPrims*(prims: seq[GeoPrim]): int =
|
proc emitGeoPrims*(prims: seq[GeoPrim]): int =
|
||||||
@@ -215,6 +269,14 @@ proc emitGeoPrims*(prims: seq[GeoPrim]): int =
|
|||||||
of gpMarkOther:
|
of gpMarkOther:
|
||||||
setFillColor(p.color)
|
setFillColor(p.color)
|
||||||
fillCircle(p.cx, p.cy, p.r)
|
fillCircle(p.cx, p.cy, p.r)
|
||||||
|
of gpTriangleEdge:
|
||||||
|
setStrokeColor(p.color)
|
||||||
|
setStrokeWidth(GeoTriangleWidth)
|
||||||
|
drawLine(p.x1, p.y1, p.x2, p.y2)
|
||||||
|
of gpTriangleCorner:
|
||||||
|
setStrokeColor(p.color)
|
||||||
|
setStrokeWidth(GeoTriangleWidth)
|
||||||
|
drawCircle(p.cx, p.cy, p.r)
|
||||||
result = prims.len
|
result = prims.len
|
||||||
|
|
||||||
proc drawGeoOverlay*(selfX, selfY, selfHeading,
|
proc drawGeoOverlay*(selfX, selfY, selfHeading,
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
##
|
##
|
||||||
## NO battle, NO Java, NO server: fixed positions/headings, then assert the
|
## NO battle, NO Java, NO server: fixed positions/headings, then assert the
|
||||||
## circle/diameter property, the two line-circle intersections per heading line,
|
## circle/diameter property, the two line-circle intersections per heading line,
|
||||||
## and the hand-computed "other" points. Also pins the two env switches and the
|
## the hand-computed "other" points, and the owner-requested triangle whose
|
||||||
## SVG emission (2 line segments + 5 circles: 1 outline + 2 hollow self marks +
|
## corners are the enemy-heading OTHER point, the enemy and our bot. Also pins
|
||||||
## 2 filled other marks).
|
## the two env switches and the SVG emission.
|
||||||
##
|
##
|
||||||
## Run: nim c -r ModularBot_garage/tests/test_geo_overlay.nim
|
## Run: nim c -r ModularBot_garage/tests/test_geo_overlay.nim
|
||||||
|
|
||||||
@@ -135,31 +135,144 @@ proc testDegenerate() =
|
|||||||
let prims = collectGeoPrims(50.0, 50.0, 0.0, 50.0, 50.0, 180.0)
|
let prims = collectGeoPrims(50.0, 50.0, 0.0, 50.0, 50.0, 180.0)
|
||||||
check "coincident bots: nothing drawn (no divide by zero)", prims.len == 0
|
check "coincident bots: nothing drawn (no divide by zero)", prims.len == 0
|
||||||
|
|
||||||
|
# ── the owner-requested triangle ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
proc triEdges(prims: seq[GeoPrim]): seq[GeoPrim] =
|
||||||
|
for p in prims:
|
||||||
|
if p.kind == gpTriangleEdge: result.add p
|
||||||
|
|
||||||
|
proc triRings(prims: seq[GeoPrim]): seq[GeoPrim] =
|
||||||
|
for p in prims:
|
||||||
|
if p.kind == gpTriangleCorner: result.add p
|
||||||
|
|
||||||
|
proc edgeLen(p: GeoPrim): float = hypot(p.x2 - p.x1, p.y2 - p.y1)
|
||||||
|
|
||||||
|
proc hasVertex(prims: seq[GeoPrim], x, y: float): bool =
|
||||||
|
## The closed edge loop covers every corner, so scan the edge endpoints.
|
||||||
|
for p in triEdges(prims):
|
||||||
|
if (approx(p.x1, x) and approx(p.y1, y)) or
|
||||||
|
(approx(p.x2, x) and approx(p.y2, y)): return true
|
||||||
|
false
|
||||||
|
|
||||||
|
proc testTriangleCaseB() =
|
||||||
|
## The EXISTING case: self (0,0) heading 45, enemy (100,0) heading 0.
|
||||||
|
## The enemy heading (east) lies ALONG the diameter, so the enemy heading's
|
||||||
|
## OTHER intersection is hand-computed to be our own bot (0,0). The triangle
|
||||||
|
## is therefore FLAT: corner 1 == corner 3.
|
||||||
|
let selfX = 0.0; let selfY = 0.0
|
||||||
|
let enX = 100.0; let enY = 0.0; let enH = 0.0
|
||||||
|
let c = makeDiameterCircle(selfX, selfY, enX, enY)
|
||||||
|
let soEnemy = pickSelfOther(lineCircleIntersections(enX, enY, enH, c), enX, enY)
|
||||||
|
check "T/B: enemy-heading other point is our bot (0,0)",
|
||||||
|
soEnemy.hasOther and approx(soEnemy.other.x, 0.0) and
|
||||||
|
approx(soEnemy.other.y, 0.0)
|
||||||
|
|
||||||
|
let prims = trianglePrims(soEnemy.other, GeomPoint(x: enX, y: enY),
|
||||||
|
GeomPoint(x: selfX, y: selfY))
|
||||||
|
check "T/B: triangle corners are (0,0), enemy (100,0), us (0,0)",
|
||||||
|
hasVertex(prims, 0.0, 0.0) and hasVertex(prims, 100.0, 0.0)
|
||||||
|
check "T/B: the collapsed triangle has 2 edges (v3->v1 skipped)",
|
||||||
|
triEdges(prims).len == 2
|
||||||
|
check "T/B: coincident corners share one ring (2 not 3)",
|
||||||
|
triRings(prims).len == 2
|
||||||
|
check "T/B: no zero-length edge is emitted",
|
||||||
|
(block:
|
||||||
|
var ok = true
|
||||||
|
for p in triEdges(prims):
|
||||||
|
if edgeLen(p) < GeoZeroLen: ok = false
|
||||||
|
ok)
|
||||||
|
check "T/B: all three corners lie ON the circle",
|
||||||
|
onCircle(soEnemy.other, c) and
|
||||||
|
onCircle(GeomPoint(x: enX, y: enY), c) and
|
||||||
|
onCircle(GeomPoint(x: selfX, y: selfY), c)
|
||||||
|
|
||||||
|
proc testTriangleCaseD() =
|
||||||
|
## Non-degenerate: self (0,0), enemy (100,0) heading 135. The enemy heading
|
||||||
|
## line meets the circle again at the hand-computed (50,50).
|
||||||
|
let selfX = 0.0; let selfY = 0.0
|
||||||
|
let enX = 100.0; let enY = 0.0; let enH = 135.0
|
||||||
|
let c = makeDiameterCircle(selfX, selfY, enX, enY)
|
||||||
|
let soEnemy = pickSelfOther(lineCircleIntersections(enX, enY, enH, c), enX, enY)
|
||||||
|
check "T/D: enemy-heading other point is hand-computed (50,50)",
|
||||||
|
soEnemy.hasOther and approx(soEnemy.other.x, 50.0, 1e-5) and
|
||||||
|
approx(soEnemy.other.y, 50.0, 1e-5)
|
||||||
|
|
||||||
|
let prims = trianglePrims(soEnemy.other, GeomPoint(x: enX, y: enY),
|
||||||
|
GeomPoint(x: selfX, y: selfY))
|
||||||
|
check "T/D: triangle corners are (50,50), enemy (100,0), us (0,0)",
|
||||||
|
hasVertex(prims, 50.0, 50.0) and hasVertex(prims, 100.0, 0.0) and
|
||||||
|
hasVertex(prims, 0.0, 0.0)
|
||||||
|
check "T/D: 3 edges and 3 distinct corner rings",
|
||||||
|
triEdges(prims).len == 3 and triRings(prims).len == 3
|
||||||
|
check "T/D: every edge has non-zero length",
|
||||||
|
(block:
|
||||||
|
var ok = true
|
||||||
|
for p in triEdges(prims):
|
||||||
|
if edgeLen(p) < GeoZeroLen: ok = false
|
||||||
|
ok)
|
||||||
|
check "T/D: all three corners lie ON the circle",
|
||||||
|
onCircle(soEnemy.other, c) and
|
||||||
|
onCircle(GeomPoint(x: enX, y: enY), c) and
|
||||||
|
onCircle(GeomPoint(x: selfX, y: selfY), c)
|
||||||
|
check "T/D: triangle colour differs from circle / heading lines / marks",
|
||||||
|
GeoTriangleColor != GeoCircleColor and
|
||||||
|
GeoTriangleColor != GeoSelfLineColor and
|
||||||
|
GeoTriangleColor != GeoEnemyLineColor and
|
||||||
|
GeoTriangleColor != GeoOtherMarkColor
|
||||||
|
|
||||||
|
proc testTriangleTangent() =
|
||||||
|
## TANGENT: enemy (100,0) heading 90 (north) — the vertical line x=100 is
|
||||||
|
## tangent to the circle at the enemy, so corner 1 coincides with corner 2.
|
||||||
|
## The zero-length edge must be skipped, not emitted.
|
||||||
|
let selfX = 0.0; let selfY = 0.0
|
||||||
|
let enX = 100.0; let enY = 0.0; let enH = 90.0
|
||||||
|
let c = makeDiameterCircle(selfX, selfY, enX, enY)
|
||||||
|
let soEnemy = pickSelfOther(lineCircleIntersections(enX, enY, enH, c), enX, enY)
|
||||||
|
check "T/tangent: other point coincides with the enemy (100,0)",
|
||||||
|
soEnemy.hasOther and approx(soEnemy.other.x, 100.0) and
|
||||||
|
approx(soEnemy.other.y, 0.0)
|
||||||
|
let prims = trianglePrims(soEnemy.other, GeomPoint(x: enX, y: enY),
|
||||||
|
GeomPoint(x: selfX, y: selfY))
|
||||||
|
check "T/tangent: zero-length edge SKIPPED (2 edges, not 3)",
|
||||||
|
triEdges(prims).len == 2
|
||||||
|
check "T/tangent: no emitted edge has zero length",
|
||||||
|
(block:
|
||||||
|
var ok = true
|
||||||
|
for p in triEdges(prims):
|
||||||
|
if edgeLen(p) < GeoZeroLen: ok = false
|
||||||
|
ok)
|
||||||
|
check "T/tangent: coincident corners share one ring (2 not 3)",
|
||||||
|
triRings(prims).len == 2
|
||||||
|
|
||||||
# ── primitive collection + emission ──────────────────────────────────────────
|
# ── primitive collection + emission ──────────────────────────────────────────
|
||||||
|
|
||||||
proc testPrimsAndEmit() =
|
proc testPrimsAndEmit() =
|
||||||
let prims = collectGeoPrims(100.0, 100.0, 0.0, 300.0, 100.0, 180.0)
|
let prims = collectGeoPrims(100.0, 100.0, 0.0, 300.0, 100.0, 180.0)
|
||||||
check "non-degenerate case draws 1 circle + 2 lines + 4 marks = 7",
|
check "non-degenerate case draws 1 circle + 2 lines + 4 marks + 4 triangle = 11",
|
||||||
prims.len == 7
|
prims.len == 11
|
||||||
var circles, lines, selfMarks, otherMarks = 0
|
var circles, lines, selfMarks, otherMarks, triEdge, triCorner = 0
|
||||||
for p in prims:
|
for p in prims:
|
||||||
case p.kind
|
case p.kind
|
||||||
of gpCircle: inc circles
|
of gpCircle: inc circles
|
||||||
of gpLineSelf, gpLineEnemy: inc lines
|
of gpLineSelf, gpLineEnemy: inc lines
|
||||||
of gpMarkSelf: inc selfMarks
|
of gpMarkSelf: inc selfMarks
|
||||||
of gpMarkOther: inc otherMarks
|
of gpMarkOther: inc otherMarks
|
||||||
check "primitive mix is (1 circle, 2 lines, 2 self marks, 2 other marks)",
|
of gpTriangleEdge: inc triEdge
|
||||||
circles == 1 and lines == 2 and selfMarks == 2 and otherMarks == 2
|
of gpTriangleCorner: inc triCorner
|
||||||
|
check "primitive mix is (1 circle, 2 lines, 2 self, 2 other, 2 tri edge, 2 tri ring)",
|
||||||
|
circles == 1 and lines == 2 and selfMarks == 2 and otherMarks == 2 and
|
||||||
|
triEdge == 2 and triCorner == 2
|
||||||
check "self and other marks use distinct colours",
|
check "self and other marks use distinct colours",
|
||||||
GeoSelfMarkColor != GeoOtherMarkColor
|
GeoSelfMarkColor != GeoOtherMarkColor
|
||||||
|
|
||||||
clearGraphics()
|
clearGraphics()
|
||||||
let n = drawGeoOverlay(100.0, 100.0, 0.0, 300.0, 100.0, 180.0)
|
let n = drawGeoOverlay(100.0, 100.0, 0.0, 300.0, 100.0, 180.0)
|
||||||
let svg = svgOutput()
|
let svg = svgOutput()
|
||||||
check "drawGeoOverlay returns the primitive count (7)", n == 7
|
check "drawGeoOverlay returns the primitive count (11)", n == 11
|
||||||
check "SVG carries 2 line segments", svg.count("<line") == 2
|
check "SVG carries 4 line segments (2 heading + 2 triangle edges)",
|
||||||
check "SVG carries 5 circles (1 outline + 2 hollow + 2 filled)",
|
svg.count("<line") == 4
|
||||||
svg.count("<circle") == 5
|
check "SVG carries 7 circles (1 outline + 2 hollow + 2 filled + 2 tri rings)",
|
||||||
|
svg.count("<circle") == 7
|
||||||
check "SVG is non-empty and wrapped", svg.startsWith("<svg") and
|
check "SVG is non-empty and wrapped", svg.startsWith("<svg") and
|
||||||
svg.endsWith("</svg>")
|
svg.endsWith("</svg>")
|
||||||
|
|
||||||
@@ -168,6 +281,9 @@ testCaseA()
|
|||||||
testCaseB()
|
testCaseB()
|
||||||
testCaseC()
|
testCaseC()
|
||||||
testDegenerate()
|
testDegenerate()
|
||||||
|
testTriangleCaseB()
|
||||||
|
testTriangleCaseD()
|
||||||
|
testTriangleTangent()
|
||||||
testPrimsAndEmit()
|
testPrimsAndEmit()
|
||||||
|
|
||||||
if failures > 0:
|
if failures > 0:
|
||||||
|
|||||||
Reference in New Issue
Block a user