updated player angle
This commit is contained in:
parent
c10bd74b70
commit
02b2a8efc8
@ -4,28 +4,69 @@
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import LoadingSpinner from './LoadingSpinner'
|
||||
|
||||
const RAD2DEG = 180 / Math.PI;
|
||||
function normalizeDeg(d: number) { d = d % 360; return d < 0 ? d + 360 : d; }
|
||||
function toYawDegMaybe(raw: any): number | null {
|
||||
const v = Number(raw);
|
||||
if (!Number.isFinite(v)) return null;
|
||||
if (Math.abs(v) <= 2 * Math.PI + 1e-3) return v * RAD2DEG; // radians
|
||||
return v; // degrees
|
||||
}
|
||||
function deriveYawDeg(raw: any, prev: PlayerState | undefined, x: number, y: number): number {
|
||||
const fromRaw = toYawDegMaybe(raw);
|
||||
if (fromRaw != null && Math.abs(fromRaw) > 1e-6 && Math.abs(fromRaw) < 1e6) return normalizeDeg(fromRaw);
|
||||
if (prev) {
|
||||
const dx = x - prev.x, dy = y - prev.y;
|
||||
if (Math.hypot(dx, dy) > 1) return normalizeDeg(Math.atan2(dy, dx) * RAD2DEG);
|
||||
if (Number.isFinite(prev.yaw)) return prev.yaw;
|
||||
// ---------- Konfiguration (UI & Verhalten) ----------
|
||||
const UI = {
|
||||
player: {
|
||||
minRadiusPx: 4,
|
||||
radiusRel: 0.008, // Radius relativ zur kleineren Bildkante
|
||||
dirLenRel: 0.70, // Anteil des Radius, Linie bleibt im Kreis
|
||||
dirMinLenPx: 6,
|
||||
lineWidthRel: 0.25, // Linienbreite relativ zum Radius
|
||||
stroke: '#ffffff',
|
||||
fillCT: '#3b82f6',
|
||||
fillT: '#f59e0b',
|
||||
// 'auto' = automatisch kontrastierend zum Kreis, sonst fixe Farbe wie '#fff'
|
||||
dirColor: 'auto' as 'auto' | string,
|
||||
},
|
||||
effects: {
|
||||
smokeIconScale: 1.6,
|
||||
fireIconScale: 1.45,
|
||||
smokeOpacity: 0.95,
|
||||
smokeFillOpacity: 0.70,
|
||||
fireOpacity: 1,
|
||||
smokeFadeMs: 3000,
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
const RAD2DEG = 180 / Math.PI
|
||||
|
||||
function normalizeDeg(d: number) {
|
||||
d = d % 360
|
||||
return d < 0 ? d + 360 : d
|
||||
}
|
||||
|
||||
function shortestAngleDeltaDeg(a: number, b: number) {
|
||||
// delta in [-180, 180)
|
||||
return ((b - a + 540) % 360) - 180
|
||||
}
|
||||
|
||||
function lerpAngleDeg(a: number, b: number, t: number) {
|
||||
return normalizeDeg(a + shortestAngleDeltaDeg(a, b) * t)
|
||||
}
|
||||
|
||||
// Yaw-Quelle parsen (Server liefert Grad)
|
||||
function toYawDegMaybe(raw: any): number | null {
|
||||
const v = Number(raw)
|
||||
return Number.isFinite(v) ? v : null
|
||||
}
|
||||
|
||||
// Fallback, wenn keine yaw übermittelt wird
|
||||
function deriveYawDeg(raw: any, prev: PlayerState | undefined, x: number, y: number): number {
|
||||
const fromRaw = toYawDegMaybe(raw)
|
||||
if (fromRaw != null) return normalizeDeg(fromRaw)
|
||||
|
||||
if (prev) {
|
||||
const dx = x - prev.x, dy = y - prev.y
|
||||
if (Math.hypot(dx, dy) > 1) return normalizeDeg(Math.atan2(dy, dx) * RAD2DEG)
|
||||
if (Number.isFinite(prev.yaw)) return prev.yaw
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
function mapTeam(t: any): 'T' | 'CT' | string {
|
||||
if (t === 2 || t === 'T' || t === 't') return 'T';
|
||||
if (t === 3 || t === 'CT' || t === 'ct') return 'CT';
|
||||
return String(t ?? '');
|
||||
if (t === 2 || t === 'T' || t === 't') return 'T'
|
||||
if (t === 3 || t === 'CT' || t === 'ct') return 'CT'
|
||||
return String(t ?? '')
|
||||
}
|
||||
|
||||
type Props = { matchId: string }
|
||||
@ -62,16 +103,15 @@ type Effect = {
|
||||
z: number
|
||||
startMs: number
|
||||
ttlMs: number
|
||||
// neu:
|
||||
ending?: boolean
|
||||
fadeUntil?: number
|
||||
}
|
||||
|
||||
// --- Nade-Pfade -------------------------------------------------------------
|
||||
// --- Nade-Pfade --------------------------------------------------------------
|
||||
type NadePoint = { x: number; y: number; z?: number; t?: number; s?: number }
|
||||
type NadePath = {
|
||||
id: string
|
||||
kind: string // 'smoke' | 'flash' | 'he' | 'molotov' | ...
|
||||
kind: string
|
||||
points: NadePoint[]
|
||||
startedMs: number
|
||||
endedMs?: number
|
||||
@ -81,14 +121,24 @@ const NADE_PATH_TTL = 6000 // Pfad noch 6s nach Detonation zeigen
|
||||
|
||||
function nadeColor(kind: string) {
|
||||
const k = String(kind || '').toLowerCase()
|
||||
if (k.includes('smoke')) return '#94a3b8' // smoke: slate-grau
|
||||
if (k.includes('flash')) return '#fbbf24' // flash: gelb
|
||||
if (k.includes('molotov') || k.includes('incen') || k === 'fire') return '#f97316' // molly
|
||||
if (k.includes('he') || k.includes('frag')) return '#ef4444' // HE
|
||||
if (k.includes('decoy')) return '#22c55e' // decoy
|
||||
if (k.includes('smoke')) return '#94a3b8'
|
||||
if (k.includes('flash')) return '#fbbf24'
|
||||
if (k.includes('molotov') || k.includes('incen') || k === 'fire') return '#f97316'
|
||||
if (k.includes('he') || k.includes('frag')) return '#ef4444'
|
||||
if (k.includes('decoy')) return '#22c55e'
|
||||
return '#a3a3a3'
|
||||
}
|
||||
|
||||
function contrastStroke(hex: string) {
|
||||
const h = hex.replace('#','')
|
||||
const r = parseInt(h.slice(0,2),16)/255
|
||||
const g = parseInt(h.slice(2,4),16)/255
|
||||
const b = parseInt(h.slice(4,6),16)/255
|
||||
const toL = (c:number) => (c<=0.03928 ? c/12.92 : Math.pow((c+0.055)/1.055, 2.4))
|
||||
const L = 0.2126*toL(r) + 0.7152*toL(g) + 0.0722*toL(b)
|
||||
return L > 0.6 ? '#111111' : '#ffffff'
|
||||
}
|
||||
|
||||
export default function LiveRadar({ matchId }: Props) {
|
||||
// ---- MapVote (Backup)
|
||||
const [loading, setLoading] = useState(true)
|
||||
@ -116,29 +166,32 @@ export default function LiveRadar({ matchId }: Props) {
|
||||
}
|
||||
}, [])
|
||||
|
||||
// SMOKE + FIRE:
|
||||
const SMOKE_PATH = "M32 400C32 479.5 96.5 544 176 544L480 544C550.7 544 608 486.7 608 416C608 364.4 577.5 319.9 533.5 299.7C540.2 286.6 544 271.7 544 256C544 203 501 160 448 160C430.3 160 413.8 164.8 399.6 173.1C375.5 127.3 327.4 96 272 96C192.5 96 128 160.5 128 240C128 248 128.7 255.9 129.9 263.5C73 282.7 32 336.6 32 400z";
|
||||
const FIRE_PATH = "M256.5 37.6C265.8 29.8 279.5 30.1 288.4 38.5C300.7 50.1 311.7 62.9 322.3 75.9C335.8 92.4 352 114.2 367.6 140.1C372.8 133.3 377.6 127.3 381.8 122.2C382.9 120.9 384 119.5 385.1 118.1C393 108.3 402.8 96 415.9 96C429.3 96 438.7 107.9 446.7 118.1C448 119.8 449.3 121.4 450.6 122.9C460.9 135.3 474.6 153.2 488.3 175.3C515.5 219.2 543.9 281.7 543.9 351.9C543.9 475.6 443.6 575.9 319.9 575.9C196.2 575.9 96 475.7 96 352C96 260.9 137.1 182 176.5 127C196.4 99.3 216.2 77.1 231.1 61.9C239.3 53.5 247.6 45.2 256.6 37.7zM321.7 480C347 480 369.4 473 390.5 459C432.6 429.6 443.9 370.8 418.6 324.6C414.1 315.6 402.6 315 396.1 322.6L370.9 351.9C364.3 359.5 352.4 359.3 346.2 351.4C328.9 329.3 297.1 289 280.9 268.4C275.5 261.5 265.7 260.4 259.4 266.5C241.1 284.3 207.9 323.3 207.9 370.8C207.9 439.4 258.5 480 321.6 480z";
|
||||
// ---- Effekt-SVG-Paths
|
||||
const SMOKE_PATH = "M32 400C32 479.5 96.5 544 176 544L480 544C550.7 544 608 486.7 608 416C608 364.4 577.5 319.9 533.5 299.7C540.2 286.6 544 271.7 544 256C544 203 501 160 448 160C430.3 160 413.8 164.8 399.6 173.1C375.5 127.3 327.4 96 272 96C192.5 96 128 160.5 128 240C128 248 128.7 255.9 129.9 263.5C73 282.7 32 336.6 32 400z"
|
||||
const FIRE_PATH = "M256.5 37.6C265.8 29.8 279.5 30.1 288.4 38.5C300.7 50.1 311.7 62.9 322.3 75.9C335.8 92.4 352 114.2 367.6 140.1C372.8 133.3 377.6 127.3 381.8 122.2C382.9 120.9 384 119.5 385.1 118.1C393 108.3 402.8 96 415.9 96C429.3 96 438.7 107.9 446.7 118.1C448 119.8 449.3 121.4 450.6 122.9C460.9 135.3 474.6 153.2 488.3 175.3C515.5 219.2 543.9 281.7 543.9 351.9C543.9 475.6 443.6 575.9 319.9 575.9C196.2 575.9 96 475.7 96 352C96 260.9 137.1 182 176.5 127C196.4 99.3 216.2 77.1 231.1 61.9C239.3 53.5 247.6 45.2 256.6 37.7zM321.7 480C347 480 369.4 473 390.5 459C432.6 429.6 443.9 370.8 418.6 324.6C414.1 315.6 402.6 315 396.1 322.6L370.9 351.9C364.3 359.5 352.4 359.3 346.2 351.4C328.9 329.3 297.1 289 280.9 268.4C275.5 261.5 265.7 260.4 259.4 266.5C241.1 284.3 207.9 323.3 207.9 370.8C207.9 439.4 258.5 480 321.6 480z"
|
||||
|
||||
const SMOKE_FADE_MS = 3000; // 3s ausfaden
|
||||
|
||||
// NEU – Größe & Opazität der Icons
|
||||
const SMOKE_ICON_SCALE = 1.6; // >1 = größer
|
||||
const FIRE_ICON_SCALE = 1.45;
|
||||
|
||||
const SMOKE_GROUP_OPACITY = 0.95; // weniger transparent
|
||||
const SMOKE_FILL_OPACITY = 0.70;
|
||||
|
||||
const FIRE_GROUP_OPACITY = 1.0; // fast/komplett deckend
|
||||
|
||||
// ---- Nade-Pfade ------------------------------------------------------------
|
||||
// ---- Nade-Pfade & Effekte -------------------------------------------------
|
||||
const nadePathsRef = useRef<Map<string, NadePath>>(new Map())
|
||||
const [nadePaths, setNadePaths] = useState<NadePath[]>([])
|
||||
const syncNadePaths = () => setNadePaths(Array.from(nadePathsRef.current.values()))
|
||||
|
||||
const effectsRef = useRef<Map<string, Effect>>(new Map())
|
||||
const [effects, setEffects] = useState<Effect[]>([])
|
||||
const syncEffects = () => setEffects(Array.from(effectsRef.current.values()))
|
||||
|
||||
// ---- Spieler (throttled) --------------------------------------------------
|
||||
const playersRef = useRef<Map<string, PlayerState>>(new Map())
|
||||
const [players, setPlayers] = useState<PlayerState[]>([])
|
||||
const flushTimer = useRef<number | null>(null)
|
||||
const scheduleFlush = () => {
|
||||
if (flushTimer.current != null) return
|
||||
flushTimer.current = window.setTimeout(() => {
|
||||
flushTimer.current = null
|
||||
setPlayers(Array.from(playersRef.current.values()))
|
||||
}, 66)
|
||||
}
|
||||
|
||||
// ---- 1) MapVote laden
|
||||
// ---- MapVote laden --------------------------------------------------------
|
||||
useEffect(() => {
|
||||
let cancel = false
|
||||
;(async () => {
|
||||
@ -161,37 +214,7 @@ export default function LiveRadar({ matchId }: Props) {
|
||||
return () => { cancel = true }
|
||||
}, [matchId])
|
||||
|
||||
// ---- Spieler-Overlay (ge-throttled)
|
||||
const playersRef = useRef<Map<string, PlayerState>>(new Map())
|
||||
const [players, setPlayers] = useState<PlayerState[]>([])
|
||||
const flushTimer = useRef<number | null>(null)
|
||||
const scheduleFlush = () => {
|
||||
if (flushTimer.current != null) return
|
||||
flushTimer.current = window.setTimeout(() => {
|
||||
flushTimer.current = null
|
||||
setPlayers(Array.from(playersRef.current.values()))
|
||||
}, 66)
|
||||
}
|
||||
|
||||
// ---- Effekte-State (seltenere Updates, kein Throttle nötig – aber Map + Sync)
|
||||
const effectsRef = useRef<Map<string, Effect>>(new Map())
|
||||
const [effects, setEffects] = useState<Effect[]>([])
|
||||
const syncEffects = () => setEffects(Array.from(effectsRef.current.values()))
|
||||
|
||||
function parsePlayer(p: any): PlayerState | null {
|
||||
if (!p) return null
|
||||
const id = p.steamId || p.steam_id || p.userId || p.playerId || p.id || p.name
|
||||
if (!id) return null
|
||||
const pos = p.pos || p.position || p.location || p.coordinates
|
||||
const x = Number(p.x ?? pos?.x ?? (Array.isArray(pos) ? pos?.[0] : undefined))
|
||||
const y = Number(p.y ?? pos?.y ?? (Array.isArray(pos) ? pos?.[1] : undefined))
|
||||
const z = Number(p.z ?? pos?.z ?? (Array.isArray(pos) ? pos?.[2] : 0))
|
||||
if (!Number.isFinite(x) || !Number.isFinite(y)) return null
|
||||
const yaw = Number(p.yaw ?? p.ang?.y ?? p.angles?.y ?? p.rotation?.yaw ?? p.view?.yaw ?? 0)
|
||||
return { id: String(id), name: p.name, team: p.team, x, y, z, yaw, alive: p.alive }
|
||||
}
|
||||
|
||||
// ---- Aktive Map (WS hat Vorrang)
|
||||
// ---- Aktive Map bestimmen -------------------------------------------------
|
||||
const voteMapKey = useMemo(() => {
|
||||
const chosen = (voteData?.steps ?? []).filter(s => (s.action === 'pick' || s.action === 'decider') && s.map)
|
||||
return chosen[0]?.map ?? null
|
||||
@ -202,7 +225,7 @@ export default function LiveRadar({ matchId }: Props) {
|
||||
if (!activeMapKey && voteMapKey) setActiveMapKey(voteMapKey)
|
||||
}, [voteMapKey, activeMapKey])
|
||||
|
||||
// ---- 2) WS verbinden: Map + Players + Effekte
|
||||
// ---- WebSocket verbinden --------------------------------------------------
|
||||
useEffect(() => {
|
||||
if (typeof window === 'undefined') return
|
||||
const explicit = process.env.NEXT_PUBLIC_CS2_WS_URL
|
||||
@ -216,14 +239,14 @@ export default function LiveRadar({ matchId }: Props) {
|
||||
let ws: WebSocket | null = null
|
||||
let retry: number | null = null
|
||||
|
||||
// Hilfsfunktionen für Effekte
|
||||
// Effekte hinzufügen/entfernen
|
||||
const addEffect = (type: EffectType, m: any) => {
|
||||
const pos = m.pos ?? m.position
|
||||
const x = Number(m.x ?? pos?.x), y = Number(m.y ?? pos?.y), z = Number(m.z ?? pos?.z ?? 0)
|
||||
if (!Number.isFinite(x) || !Number.isFinite(y)) return
|
||||
const serverId = m.id ?? m.entityId ?? m.grenadeId ?? m.guid
|
||||
const id = String(serverId ?? `${type}:${Math.round(x)}:${Math.round(y)}:${Math.round(m.t ?? Date.now())}`)
|
||||
const ttlMs = type === 'smoke' ? 19000 : 7000 // Smoke ~19s, Molotov ~7s (Fallback)
|
||||
const ttlMs = type === 'smoke' ? 19000 : 7000
|
||||
effectsRef.current.set(id, { id, type, x, y, z, startMs: Date.now(), ttlMs })
|
||||
syncEffects()
|
||||
}
|
||||
@ -235,12 +258,10 @@ export default function LiveRadar({ matchId }: Props) {
|
||||
const e = effectsRef.current.get(key)
|
||||
if (e) {
|
||||
if (type === 'smoke') {
|
||||
// statt löschen: Fade markieren
|
||||
e.ending = true
|
||||
e.fadeUntil = Date.now() + SMOKE_FADE_MS
|
||||
e.fadeUntil = Date.now() + UI.effects.smokeFadeMs
|
||||
effectsRef.current.set(key, e)
|
||||
} else {
|
||||
// fire weiterhin sofort entfernen
|
||||
effectsRef.current.delete(key)
|
||||
}
|
||||
syncEffects()
|
||||
@ -248,9 +269,8 @@ export default function LiveRadar({ matchId }: Props) {
|
||||
return
|
||||
}
|
||||
|
||||
// Fallback: nächster gleicher Typ in der Nähe
|
||||
// Fallback: nächster gleicher Typ in der Nähe (ohne serverId)
|
||||
const pos = m.pos ?? m.position
|
||||
|
||||
const x = Number(m.x ?? pos?.x), y = Number(m.y ?? pos?.y)
|
||||
if (Number.isFinite(x) && Number.isFinite(y)) {
|
||||
let bestKey: string | null = null, bestD = Infinity
|
||||
@ -263,7 +283,7 @@ export default function LiveRadar({ matchId }: Props) {
|
||||
if (type === 'smoke') {
|
||||
const e = effectsRef.current.get(bestKey)!
|
||||
e.ending = true
|
||||
e.fadeUntil = Date.now() + SMOKE_FADE_MS
|
||||
e.fadeUntil = Date.now() + UI.effects.smokeFadeMs
|
||||
effectsRef.current.set(bestKey, e)
|
||||
} else {
|
||||
effectsRef.current.delete(bestKey)
|
||||
@ -273,19 +293,14 @@ export default function LiveRadar({ matchId }: Props) {
|
||||
}
|
||||
}
|
||||
|
||||
const connect = () => {
|
||||
if (!alive) return
|
||||
setWsStatus('connecting')
|
||||
ws = new WebSocket(url)
|
||||
// Nade-Trace verarbeiten
|
||||
const upsertNadeTrace = (tr: any) => {
|
||||
const now = Date.now()
|
||||
const id = String(tr?.id ?? tr?.guid ?? tr?.entityId ?? tr?.grenadeId ?? '')
|
||||
const kind = String(tr?.nade ?? tr?.kind ?? tr?.type ?? tr?.weapon ?? '').toLowerCase()
|
||||
if (!id) return
|
||||
|
||||
// ---- helper: trace-objekt in NadePath mergen + Effekte erzeugen/entfernen
|
||||
const upsertNadeTrace = (tr: any) => {
|
||||
const now = Date.now()
|
||||
const id = String(tr?.id ?? tr?.guid ?? tr?.entityId ?? tr?.grenadeId ?? '')
|
||||
const kind = String(tr?.nade ?? tr?.kind ?? tr?.type ?? tr?.weapon ?? '').toLowerCase()
|
||||
if (!id) return
|
||||
|
||||
const addPoint = (pt: any) => {
|
||||
const addPoint = (pt: any) => {
|
||||
const px = Number(pt?.x ?? pt?.pos?.x)
|
||||
const py = Number(pt?.y ?? pt?.pos?.y)
|
||||
const pz = Number(pt?.z ?? pt?.pos?.z ?? 0)
|
||||
@ -300,7 +315,6 @@ export default function LiveRadar({ matchId }: Props) {
|
||||
const last = cur.points[cur.points.length - 1]
|
||||
const seg = Number.isFinite(s0) ? s0 : (last?.s ?? 0)
|
||||
|
||||
// einfache Dedupe (Distanz + Zeit)
|
||||
const tooClose = last && Math.hypot(last.x - px, last.y - py) <= 1
|
||||
const sameTime = last && (last.t ?? 0) === t
|
||||
if (tooClose && sameTime) return
|
||||
@ -309,52 +323,51 @@ export default function LiveRadar({ matchId }: Props) {
|
||||
nadePathsRef.current.set(id, cur)
|
||||
}
|
||||
|
||||
// Punkte anhängen (einzeln oder als Liste)
|
||||
if (Array.isArray(tr?.points)) tr.points.forEach(addPoint)
|
||||
else if (tr?.pos || (tr?.x != null && tr?.y != null)) addPoint(tr.pos ?? tr)
|
||||
if (Array.isArray(tr?.points)) tr.points.forEach(addPoint)
|
||||
else if (tr?.pos || (tr?.x != null && tr?.y != null)) addPoint(tr.pos ?? tr)
|
||||
|
||||
// Status auswerten
|
||||
const state = String(tr?.state ?? tr?.sub ?? tr?.phase ?? '').toLowerCase()
|
||||
const markEnded = () => {
|
||||
const cur = nadePathsRef.current.get(id)
|
||||
if (cur && !cur.endedMs) { cur.endedMs = now; nadePathsRef.current.set(id, cur) }
|
||||
}
|
||||
|
||||
// Detonation -> Pfad beenden + Effekt starten
|
||||
if (state === 'detonate' || state === 'detonated' || tr?.detonate || tr?.done) {
|
||||
markEnded()
|
||||
const pos = tr?.pos ?? tr
|
||||
if (kind.includes('smoke')) addEffect('smoke', { ...pos, id })
|
||||
if (kind.includes('molotov') || kind.includes('incen') || kind.includes('fire')) addEffect('fire', { ...pos, id })
|
||||
}
|
||||
|
||||
// Ende (explizit) -> Effekt entfernen (Smoke mit Fade, Fire sofort wie gehabt)
|
||||
if (state === 'end' || state === 'expired') {
|
||||
if (kind.includes('smoke')) removeEffect('smoke', { id })
|
||||
if (kind.includes('molotov') || kind.includes('incen') || kind.includes('fire')) removeEffect('fire', { id })
|
||||
markEnded()
|
||||
}
|
||||
const state = String(tr?.state ?? tr?.sub ?? tr?.phase ?? '').toLowerCase()
|
||||
const markEnded = () => {
|
||||
const cur = nadePathsRef.current.get(id)
|
||||
if (cur && !cur.endedMs) { cur.endedMs = now; nadePathsRef.current.set(id, cur) }
|
||||
}
|
||||
|
||||
ws.onopen = () => { setWsStatus('open'); console.info('[cs2-ws] connected →', url) }
|
||||
if (state === 'detonate' || state === 'detonated' || tr?.detonate || tr?.done) {
|
||||
markEnded()
|
||||
const pos = tr?.pos ?? tr
|
||||
if (kind.includes('smoke')) addEffect('smoke', { ...pos, id })
|
||||
if (kind.includes('molotov') || kind.includes('incen') || kind.includes('fire')) addEffect('fire', { ...pos, id })
|
||||
}
|
||||
|
||||
if (state === 'end' || state === 'expired') {
|
||||
if (kind.includes('smoke')) removeEffect('smoke', { id })
|
||||
if (kind.includes('molotov') || kind.includes('incen') || kind.includes('fire')) removeEffect('fire', { id })
|
||||
markEnded()
|
||||
}
|
||||
}
|
||||
|
||||
const connect = () => {
|
||||
if (!alive) return
|
||||
setWsStatus('connecting')
|
||||
ws = new WebSocket(url)
|
||||
|
||||
ws.onopen = () => setWsStatus('open')
|
||||
|
||||
ws.onmessage = (ev) => {
|
||||
let msg: any = null
|
||||
try { msg = JSON.parse(ev.data as string) } catch {}
|
||||
|
||||
const tickTs = Number(msg?.t ?? Date.now())
|
||||
|
||||
const handleEvent = (e: any) => {
|
||||
if (!e) return
|
||||
|
||||
// 1) Map-Event
|
||||
// Map wechseln
|
||||
if (e.type === 'map' || e.type === 'level' || e.map) {
|
||||
const key = e.name || e.map || e.level
|
||||
if (typeof key === 'string' && key) setActiveMapKey(key)
|
||||
return
|
||||
}
|
||||
|
||||
// 2) Effekte -> NICHT als Player behandeln
|
||||
// Effekte
|
||||
if (e.type === 'smoke' || e.type === 'fire') {
|
||||
const t = e.type as EffectType
|
||||
if (e.state === 'start') addEffect(t, e)
|
||||
@ -362,64 +375,12 @@ export default function LiveRadar({ matchId }: Props) {
|
||||
return
|
||||
}
|
||||
|
||||
if (e.type === 'nade') {
|
||||
const id = String(e.id ?? '')
|
||||
const kind = String(e.nade ?? e.weapon ?? '')
|
||||
if (!id) return
|
||||
|
||||
if (e.sub === 'thrown') {
|
||||
const p = e.throwPos ?? e.pos ?? {}
|
||||
const x = Number(p.x), y = Number(p.y), z = Number(p.z ?? 0)
|
||||
if (!Number.isFinite(x) || !Number.isFinite(y)) return
|
||||
nadePathsRef.current.set(id, {
|
||||
id, kind,
|
||||
points: [{ x, y, z, t: e.t }],
|
||||
startedMs: Date.now()
|
||||
})
|
||||
syncNadePaths()
|
||||
return
|
||||
}
|
||||
|
||||
if (e.sub === 'path') {
|
||||
const cur = nadePathsRef.current.get(id)
|
||||
const pts = Array.isArray(e.points) ? e.points as NadePoint[] : []
|
||||
if (!cur) {
|
||||
if (pts.length) {
|
||||
nadePathsRef.current.set(id, { id, kind, points: pts.slice(0, 200), startedMs: Date.now() })
|
||||
syncNadePaths()
|
||||
}
|
||||
return
|
||||
}
|
||||
// Punkte anhängen (ein wenig deduplizieren)
|
||||
for (const pt of pts) {
|
||||
const x = Number(pt.x), y = Number(pt.y)
|
||||
if (!Number.isFinite(x) || !Number.isFinite(y)) continue
|
||||
const last = cur.points[cur.points.length - 1]
|
||||
if (!last || Math.hypot(last.x - x, last.y - y) > 1) cur.points.push({ x, y, z: pt.z, t: pt.t })
|
||||
}
|
||||
// Länge begrenzen
|
||||
if (cur.points.length > 300) cur.points.splice(0, cur.points.length - 300)
|
||||
nadePathsRef.current.set(id, cur)
|
||||
syncNadePaths()
|
||||
return
|
||||
}
|
||||
|
||||
if (e.sub === 'detonate') {
|
||||
const cur = nadePathsRef.current.get(id)
|
||||
if (cur && !cur.endedMs) {
|
||||
// Endpunkt (falls vorhanden) hinzufügen
|
||||
const p = e.pos ?? {}
|
||||
const x = Number(p.x), y = Number(p.y)
|
||||
if (Number.isFinite(x) && Number.isFinite(y)) cur.points.push({ x, y, z: Number(p.z ?? 0), t: e.t })
|
||||
cur.endedMs = Date.now()
|
||||
nadePathsRef.current.set(id, cur)
|
||||
syncNadePaths()
|
||||
}
|
||||
return
|
||||
}
|
||||
// Grenade-Traces
|
||||
if (e.type === 'nade' || e.kind || e.nade || e.weapon) {
|
||||
upsertNadeTrace(e)
|
||||
}
|
||||
|
||||
// 3) Player-ähnliche Events
|
||||
// Spieler
|
||||
if (!(e.steamId || e.steam_id || e.pos || e.position)) return
|
||||
|
||||
const id = String(e.steamId ?? e.steam_id ?? e.userId ?? e.playerId ?? e.id ?? e.name ?? '')
|
||||
@ -432,8 +393,20 @@ export default function LiveRadar({ matchId }: Props) {
|
||||
if (!Number.isFinite(x) || !Number.isFinite(y)) return
|
||||
|
||||
const prev = playersRef.current.get(id)
|
||||
const rawYaw = e.yaw ?? e.ang?.y ?? e.angles?.y ?? e.rotation?.yaw ?? e.view?.yaw
|
||||
const yawDeg = deriveYawDeg(rawYaw, prev, x, y)
|
||||
|
||||
const rawYaw =
|
||||
e.viewAngle?.yaw ??
|
||||
e.view?.yaw ??
|
||||
e.aim?.yaw ??
|
||||
e.yaw ??
|
||||
e.ang?.y ??
|
||||
e.angles?.y ??
|
||||
e.rotation?.yaw
|
||||
|
||||
const yawDegRaw = deriveYawDeg(rawYaw, prev, x, y)
|
||||
// Sanftes Smoothing, damit kleine Änderungen sichtbar & stabil sind
|
||||
const YAW_SMOOTH = 0.01
|
||||
const yawDeg = prev ? lerpAngleDeg(prev.yaw, yawDegRaw, YAW_SMOOTH) : yawDegRaw
|
||||
|
||||
const p: PlayerState = {
|
||||
id,
|
||||
@ -454,39 +427,10 @@ export default function LiveRadar({ matchId }: Props) {
|
||||
handleEvent(msg)
|
||||
}
|
||||
|
||||
if (msg?.nades?.trace && Array.isArray(msg.nades.trace)) {
|
||||
for (const tr of msg.nades.trace) upsertNadeTrace(tr)
|
||||
syncNadePaths() // sichtbaren State aktualisieren
|
||||
}
|
||||
|
||||
if (Array.isArray(msg?.nades?.active)) {
|
||||
for (const a of msg.nades.active) {
|
||||
const id = String(a?.id ?? '')
|
||||
if (!id) continue
|
||||
const kind = String(a?.nade ?? a?.kind ?? '').toLowerCase()
|
||||
const p = a.pos ?? {}
|
||||
const x = Number(p.x), y = Number(p.y), z = Number(p.z ?? 0)
|
||||
if (!Number.isFinite(x) || !Number.isFinite(y)) continue
|
||||
|
||||
let cur = nadePathsRef.current.get(id)
|
||||
if (!cur) cur = { id, kind, points: [], startedMs: Date.now() }
|
||||
else if (!cur.kind && kind) cur.kind = kind
|
||||
|
||||
const last = cur.points[cur.points.length - 1]
|
||||
const seg = last?.s ?? 0
|
||||
// IMMER anhängen (pro Tick ein Punkt)
|
||||
cur.points.push({ x, y, z, t: tickTs, s: seg })
|
||||
if (cur.points.length > 500) cur.points.splice(0, cur.points.length - 500)
|
||||
|
||||
nadePathsRef.current.set(id, cur)
|
||||
}
|
||||
syncNadePaths() // sofort zeichnen
|
||||
}
|
||||
// Spieler-Flush (wenn sich etwas geändert hat)
|
||||
scheduleFlush()
|
||||
}
|
||||
|
||||
ws.onerror = (e) => { setWsStatus('error'); console.error('[cs2-ws] error:', e) }
|
||||
ws.onerror = () => setWsStatus('error')
|
||||
ws.onclose = () => {
|
||||
setWsStatus('closed')
|
||||
if (alive) retry = window.setTimeout(connect, 2000)
|
||||
@ -501,7 +445,7 @@ export default function LiveRadar({ matchId }: Props) {
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Automatisches Aufräumen von abgelaufenen Effekten (TTL)
|
||||
// ---- Aufräumen (TTL) ------------------------------------------------------
|
||||
useEffect(() => {
|
||||
const iv = window.setInterval(() => {
|
||||
const now = Date.now()
|
||||
@ -510,40 +454,29 @@ export default function LiveRadar({ matchId }: Props) {
|
||||
|
||||
for (const [k, e] of effectsRef.current) {
|
||||
haveAny = true
|
||||
// wenn beendet & Fade vorbei -> löschen
|
||||
if (e.ending && e.fadeUntil && now >= e.fadeUntil) {
|
||||
effectsRef.current.delete(k)
|
||||
changed = true
|
||||
continue
|
||||
effectsRef.current.delete(k); changed = true; continue
|
||||
}
|
||||
// Sicherheit: TTL nur anwenden, wenn nicht bereits im Fade
|
||||
if (!e.ending && now - e.startMs > e.ttlMs) {
|
||||
effectsRef.current.delete(k)
|
||||
changed = true
|
||||
effectsRef.current.delete(k); changed = true
|
||||
}
|
||||
}
|
||||
|
||||
// --- Nade-Pfade nach TTL entfernen ---
|
||||
for (const [k, np] of nadePathsRef.current) {
|
||||
if (np.endedMs && Date.now() - np.endedMs > NADE_PATH_TTL) {
|
||||
nadePathsRef.current.delete(k)
|
||||
changed = true
|
||||
if (np.endedMs && now - np.endedMs > NADE_PATH_TTL) {
|
||||
nadePathsRef.current.delete(k); changed = true
|
||||
}
|
||||
}
|
||||
if (changed) setNadePaths(Array.from(nadePathsRef.current.values()))
|
||||
|
||||
// Repaint erzwingen, damit Fade-Opacity sichtbar animiert
|
||||
if (changed || haveAny) {
|
||||
setEffects(Array.from(effectsRef.current.values()))
|
||||
}
|
||||
}, 100) // ~10 FPS reicht für sanftes Fade
|
||||
if (changed || haveAny) setEffects(Array.from(effectsRef.current.values()))
|
||||
}, 100)
|
||||
return () => window.clearInterval(iv)
|
||||
}, [])
|
||||
|
||||
// ---- 3) Overview laden (JSON ODER Valve-KV)
|
||||
// ---- Overview laden -------------------------------------------------------
|
||||
const [overview, setOverview] = useState<Overview | null>(null)
|
||||
|
||||
function overviewCandidates(mapKey: string) {
|
||||
const overviewCandidates = (mapKey: string) => {
|
||||
const base = mapKey
|
||||
return [
|
||||
`/assets/resource/overviews/${base}.json`,
|
||||
@ -553,6 +486,7 @@ export default function LiveRadar({ matchId }: Props) {
|
||||
`/assets/resource/overviews/${base}_s2.json`,
|
||||
]
|
||||
}
|
||||
|
||||
const parseOverviewJson = (j: any): Overview | null => {
|
||||
const posX = Number(j?.posX ?? j?.pos_x)
|
||||
const posY = Number(j?.posY ?? j?.pos_y)
|
||||
@ -561,6 +495,7 @@ export default function LiveRadar({ matchId }: Props) {
|
||||
if (![posX, posY, scale].every(Number.isFinite)) return null
|
||||
return { posX, posY, scale, rotate }
|
||||
}
|
||||
|
||||
const parseValveKvOverview = (txt: string): Overview | null => {
|
||||
const clean = txt.replace(/\/\/.*$/gm, '')
|
||||
const pick = (k: string) => { const m = clean.match(new RegExp(`"\\s*${k}\\s*"\\s*"([^"]+)"`)); return m ? Number(m[1]) : NaN }
|
||||
@ -569,6 +504,7 @@ export default function LiveRadar({ matchId }: Props) {
|
||||
if (![posX, posY, scale].every(Number.isFinite)) return null
|
||||
return { posX, posY, scale, rotate }
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
let cancel = false
|
||||
;(async () => {
|
||||
@ -588,14 +524,7 @@ export default function LiveRadar({ matchId }: Props) {
|
||||
return () => { cancel = true }
|
||||
}, [activeMapKey])
|
||||
|
||||
// ---- 4) Radarbild-Pfade
|
||||
const mapLabel = useMemo(() => {
|
||||
if (activeMapKey && voteData?.mapVisuals?.[activeMapKey]?.label)
|
||||
return voteData.mapVisuals[activeMapKey].label
|
||||
if (activeMapKey) return activeMapKey.replace(/^de_/, '').replace(/_/g, ' ').toUpperCase()
|
||||
return 'Unbekannte Map'
|
||||
}, [activeMapKey, voteData?.mapVisuals])
|
||||
|
||||
// ---- Radarbild-Pfade ------------------------------------------------------
|
||||
const { folderKey, imageCandidates } = useMemo(() => {
|
||||
if (!activeMapKey) return { folderKey: null as string | null, imageCandidates: [] as string[] }
|
||||
const short = activeMapKey.startsWith('de_') ? activeMapKey.slice(3) : activeMapKey
|
||||
@ -615,10 +544,10 @@ export default function LiveRadar({ matchId }: Props) {
|
||||
useEffect(() => { setSrcIdx(0) }, [folderKey])
|
||||
const currentSrc = imageCandidates[srcIdx]
|
||||
|
||||
// ---- 5) Bildgröße
|
||||
// ---- Bildgröße ------------------------------------------------------------
|
||||
const [imgSize, setImgSize] = useState<{ w: number; h: number } | null>(null)
|
||||
|
||||
// ---- 6) Welt→Pixel + Einheiten→Pixel -------------------------------------
|
||||
// ---- Welt→Pixel & Einheiten→Pixel ----------------------------------------
|
||||
type Mapper = (xw: number, yw: number) => { x: number; y: number }
|
||||
|
||||
const worldToPx: Mapper = useMemo(() => {
|
||||
@ -631,8 +560,7 @@ export default function LiveRadar({ matchId }: Props) {
|
||||
return { x: imgSize.w / 2 + xw * k, y: imgSize.h / 2 - yw * k }
|
||||
}
|
||||
}
|
||||
const { posX, posY, scale } = overview
|
||||
const rotDeg = overview.rotate ?? 0
|
||||
const { posX, posY, scale, rotate = 0 } = overview
|
||||
const w = imgSize.w, h = imgSize.h
|
||||
const cx = w / 2, cy = h / 2
|
||||
|
||||
@ -646,10 +574,10 @@ export default function LiveRadar({ matchId }: Props) {
|
||||
const candidates: Mapper[] = []
|
||||
for (const base of bases) {
|
||||
for (const s of rotSigns) {
|
||||
const theta = (rotDeg * s * Math.PI) / 180
|
||||
const theta = (rotate * s * Math.PI) / 180
|
||||
candidates.push((xw, yw) => {
|
||||
const p = base(xw, yw)
|
||||
if (rotDeg === 0) return p
|
||||
if (rotate === 0) return p
|
||||
const dx = p.x - cx, dy = p.y - cy
|
||||
const xr = dx * Math.cos(theta) - dy * Math.sin(theta)
|
||||
const yr = dx * Math.sin(theta) + dy * Math.cos(theta)
|
||||
@ -686,7 +614,7 @@ export default function LiveRadar({ matchId }: Props) {
|
||||
return (u: number) => u * k
|
||||
}, [imgSize, overview])
|
||||
|
||||
// ---- Status-Badge
|
||||
// ---- Status-Badge ---------------------------------------------------------
|
||||
const WsDot = ({ status }: { status: typeof wsStatus }) => {
|
||||
const color =
|
||||
status === 'open' ? 'bg-green-500' :
|
||||
@ -706,32 +634,7 @@ export default function LiveRadar({ matchId }: Props) {
|
||||
)
|
||||
}
|
||||
|
||||
function splitSegments(points: NadePoint[], jumpWorld = 64): NadePoint[][] {
|
||||
const out: NadePoint[][] = []
|
||||
let cur: NadePoint[] = []
|
||||
let lastS = points.length ? (points[0].s ?? 0) : 0
|
||||
let last: NadePoint | null = null
|
||||
|
||||
for (const p of points) {
|
||||
const s = p.s ?? lastS
|
||||
const jump = last ? Math.hypot(p.x - last.x, p.y - last.y) > jumpWorld : false
|
||||
const breakHere = (s !== lastS) || jump
|
||||
|
||||
if (breakHere) {
|
||||
if (cur.length >= 2) out.push(cur)
|
||||
cur = [p]
|
||||
} else {
|
||||
cur.push(p)
|
||||
}
|
||||
|
||||
last = p
|
||||
lastS = s
|
||||
}
|
||||
if (cur.length >= 2) out.push(cur)
|
||||
return out
|
||||
}
|
||||
|
||||
// ---- Render
|
||||
// ---- Render ---------------------------------------------------------------
|
||||
return (
|
||||
<div className="p-4">
|
||||
<div ref={headerRef} className="mb-4 flex items-center justify-between">
|
||||
@ -785,7 +688,7 @@ export default function LiveRadar({ matchId }: Props) {
|
||||
viewBox={`0 0 ${imgSize.w} ${imgSize.h}`}
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
>
|
||||
{/* SVG-Defs für Filter */}
|
||||
{/* SVG-Defs */}
|
||||
<defs>
|
||||
<filter id="smoke-blur" x="-50%" y="-50%" width="200%" height="200%">
|
||||
<feGaussianBlur stdDeviation="8" />
|
||||
@ -800,7 +703,7 @@ export default function LiveRadar({ matchId }: Props) {
|
||||
</radialGradient>
|
||||
</defs>
|
||||
|
||||
{/* --- Effekte unter den Spielern (SVG-Icon-basiert) --- */}
|
||||
{/* Effekte */}
|
||||
{effects.map(e => {
|
||||
const { x, y } = worldToPx(e.x, e.y)
|
||||
if (!Number.isFinite(x) || !Number.isFinite(y)) return null
|
||||
@ -808,107 +711,116 @@ export default function LiveRadar({ matchId }: Props) {
|
||||
const R_WORLD = e.type === 'smoke' ? 170 : 110
|
||||
const halfPx = Math.max(12, unitsToPx(R_WORLD))
|
||||
const sBase = (halfPx * 2) / 640
|
||||
const s = sBase * (e.type === 'smoke' ? SMOKE_ICON_SCALE : FIRE_ICON_SCALE)
|
||||
const s = sBase * (e.type === 'smoke' ? UI.effects.smokeIconScale : UI.effects.fireIconScale)
|
||||
const baseT = `translate(${x},${y}) scale(${s}) translate(-320,-320)`
|
||||
|
||||
let fadeAlpha = 1
|
||||
if (e.type === 'smoke' && e.fadeUntil) {
|
||||
const remain = Math.max(0, e.fadeUntil - Date.now())
|
||||
fadeAlpha = Math.min(1, remain / SMOKE_FADE_MS)
|
||||
fadeAlpha = Math.min(1, remain / UI.effects.smokeFadeMs)
|
||||
}
|
||||
|
||||
if (e.type === 'smoke') {
|
||||
return (
|
||||
<g filter="url(#smoke-blur)" opacity={SMOKE_GROUP_OPACITY * fadeAlpha} transform={baseT}>
|
||||
<path d={SMOKE_PATH} fill="#949494" fillOpacity={SMOKE_FILL_OPACITY} />
|
||||
<g filter="url(#smoke-blur)" opacity={UI.effects.smokeOpacity * fadeAlpha} transform={baseT}>
|
||||
<path d={SMOKE_PATH} fill="#949494" fillOpacity={UI.effects.smokeFillOpacity} />
|
||||
</g>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<g filter="url(#fire-blur)" opacity={FIRE_GROUP_OPACITY} transform={baseT}>
|
||||
<g filter="url(#fire-blur)" opacity={UI.effects.fireOpacity} transform={baseT}>
|
||||
<path d={FIRE_PATH} fill="url(#fire-grad)" />
|
||||
</g>
|
||||
)
|
||||
})}
|
||||
|
||||
{/* --- Spieler darüber --- */}
|
||||
{/* Spieler */}
|
||||
{players
|
||||
.filter(p => p.team === 'CT' || p.team === 'T') // <— nur CT/T
|
||||
.filter(p => p.team === 'CT' || p.team === 'T')
|
||||
.map((p) => {
|
||||
const A = worldToPx(p.x, p.y)
|
||||
const base = Math.min(imgSize.w, imgSize.h)
|
||||
const r = Math.max(4, base * 0.008)
|
||||
const dirLenPx = Math.max(18, base * 0.025)
|
||||
const stroke = '#fff'
|
||||
const strokeW = Math.max(1.5, r * 0.35)
|
||||
const color = p.team === 'CT' ? '#3b82f6' : '#f59e0b'
|
||||
const r = Math.max(UI.player.minRadiusPx, base * UI.player.radiusRel)
|
||||
|
||||
const yawRad = (p.yaw * Math.PI) / 180
|
||||
const dirLenPx = Math.max(UI.player.dirMinLenPx, r * UI.player.dirLenRel)
|
||||
const stroke = UI.player.stroke
|
||||
const strokeW = Math.max(1, r * UI.player.lineWidthRel)
|
||||
const fillColor = p.team === 'CT' ? UI.player.fillCT : UI.player.fillT
|
||||
const dirColor = UI.player.dirColor === 'auto' ? contrastStroke(fillColor) : UI.player.dirColor
|
||||
|
||||
const yawRad = Number.isFinite(p.yaw) ? (p.yaw * Math.PI) / 180 : 0
|
||||
|
||||
// Richtung als Welt-Schritt (respektiert Overview-Rotation)
|
||||
const STEP_WORLD = 200
|
||||
const B = worldToPx(
|
||||
p.x + Math.cos(yawRad) * STEP_WORLD,
|
||||
p.y + Math.sin(yawRad) * STEP_WORLD
|
||||
)
|
||||
|
||||
let dxp = B.x - A.x, dyp = B.y - A.y
|
||||
if (!Number.isFinite(dxp) || !Number.isFinite(dyp)) { dxp = STEP_WORLD; dyp = 0 }
|
||||
|
||||
const cur = Math.hypot(dxp, dyp) || 1
|
||||
dxp *= dirLenPx / cur
|
||||
dyp *= dirLenPx / cur
|
||||
|
||||
return (
|
||||
<g key={p.id}>
|
||||
<line x1={A.x} y1={A.y} x2={A.x + dxp} y2={A.y + dyp}
|
||||
stroke={color} strokeWidth={strokeW} strokeLinecap="round"
|
||||
opacity={p.alive === false ? 0.5 : 1}/>
|
||||
<circle cx={A.x} cy={A.y} r={r}
|
||||
fill={color} stroke={stroke}
|
||||
strokeWidth={Math.max(1, r*0.3)}
|
||||
opacity={p.alive === false ? 0.6 : 1}/>
|
||||
{/* Kreis zuerst */}
|
||||
<circle
|
||||
cx={A.x} cy={A.y} r={r}
|
||||
fill={fillColor} stroke={stroke}
|
||||
strokeWidth={Math.max(1, r*0.3)}
|
||||
opacity={p.alive === false ? 0.6 : 1}
|
||||
/>
|
||||
{/* Linie darüber (sichtbar) */}
|
||||
<line
|
||||
x1={A.x} y1={A.y} x2={A.x + dxp} y2={A.y + dyp}
|
||||
stroke={dirColor} strokeWidth={strokeW} strokeLinecap="round"
|
||||
opacity={p.alive === false ? 0.5 : 1}
|
||||
/>
|
||||
</g>
|
||||
)
|
||||
})}
|
||||
|
||||
{/* --- Nade-Pfade (unter Effekten & Spielern) --- */}
|
||||
<g opacity={0.95}>
|
||||
{nadePaths.map(np => {
|
||||
const col = nadeColor(np.kind)
|
||||
const wBase = Math.min(imgSize.w, imgSize.h)
|
||||
const strokeW = Math.max(2, wBase * 0.004)
|
||||
const dotR = Math.max(1.5, wBase * 0.0025)
|
||||
{/* Nade-Pfade */}
|
||||
<g opacity={0.95}>
|
||||
{nadePaths.map(np => {
|
||||
const col = nadeColor(np.kind)
|
||||
const wBase = Math.min(imgSize.w, imgSize.h)
|
||||
const strokeW = Math.max(2, wBase * 0.004)
|
||||
const dotR = Math.max(1.5, wBase * 0.0025)
|
||||
let alpha = 1
|
||||
if (np.endedMs) alpha = Math.max(0, 1 - (Date.now() - np.endedMs) / NADE_PATH_TTL)
|
||||
|
||||
let alpha = 1
|
||||
if (np.endedMs) alpha = Math.max(0, 1 - (Date.now() - np.endedMs) / NADE_PATH_TTL)
|
||||
const pts = np.points
|
||||
.map(p => worldToPx(p.x, p.y))
|
||||
.filter(p => Number.isFinite(p.x) && Number.isFinite(p.y))
|
||||
if (pts.length === 0) return null
|
||||
|
||||
const pts = np.points
|
||||
.map(p => worldToPx(p.x, p.y))
|
||||
.filter(p => Number.isFinite(p.x) && Number.isFinite(p.y))
|
||||
const d = pts.length >= 2
|
||||
? `M ${pts[0].x} ${pts[0].y} ` + pts.slice(1).map(p => `L ${p.x} ${p.y}`).join(' ')
|
||||
: null
|
||||
|
||||
if (pts.length === 0) return null
|
||||
|
||||
const d = pts.length >= 2
|
||||
? `M ${pts[0].x} ${pts[0].y} ` + pts.slice(1).map(p => `L ${p.x} ${p.y}`).join(' ')
|
||||
: null
|
||||
|
||||
return (
|
||||
<g key={np.id} opacity={alpha}>
|
||||
{d && (
|
||||
<path
|
||||
d={d}
|
||||
fill="none"
|
||||
stroke={col}
|
||||
strokeWidth={strokeW}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
)}
|
||||
{/* Punkte anzeigen, damit sofort etwas sichtbar ist */}
|
||||
{pts.map((p, i) => (
|
||||
<circle key={`${np.id}:pt:${i}`} cx={p.x} cy={p.y} r={dotR} fill={col} opacity={0.9} />
|
||||
))}
|
||||
</g>
|
||||
)
|
||||
})}
|
||||
</g>
|
||||
return (
|
||||
<g key={np.id} opacity={alpha}>
|
||||
{d && (
|
||||
<path
|
||||
d={d}
|
||||
fill="none"
|
||||
stroke={col}
|
||||
strokeWidth={strokeW}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
)}
|
||||
{pts.map((p, i) => (
|
||||
<circle key={`${np.id}:pt:${i}`} cx={p.x} cy={p.y} r={dotR} fill={col} opacity={0.9} />
|
||||
))}
|
||||
</g>
|
||||
)
|
||||
})}
|
||||
</g>
|
||||
</svg>
|
||||
)}
|
||||
</>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user