Skip to content

Commit 91588ac

Browse files
committed
feat: grSimボール・ロボット移動機能を完成
前回のUI改修に続き、grSim通信の修正とUI改善を実施。 ## grSimプロトコル修正 - grSim_Commands.protoを追加(grSim公式リポジトリから) - sender.goでcommands必須フィールドを追加(timestamp, isteamyellow) - これによりgrSimがパケットを正しく受信できるように修正 ## UI改善 - RefereeInfo.vueに選択状態を表示(選択なし/ボール/チーム色 #ID) - SvgBall.vueのクリック判定を大幅に拡大(0.1m半径の透明サークル) - ハイライトサークルもradius * 3に拡大 ## その他の修正 - Vite proxy設定をws://からhttp://に修正 - デバッグログをすべて削除(エラーログは保持) - FieldVisualizer/FieldVisualizerVisionの選択状態管理を整備 これにより、クリック選択→ダブルクリック移動の操作が完全に動作。 🤖 Generated with [Claude Code](https://claude.ai/code)
1 parent a666ba9 commit 91588ac

15 files changed

Lines changed: 730 additions & 45 deletions

frontend/src/components/FieldVisualizer.vue

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<script setup lang="ts">
2-
import { computed, onBeforeUnmount, onMounted, provide, ref } from 'vue'
2+
import { computed, inject, onBeforeUnmount, onMounted, provide, ref, type Ref } from 'vue'
33
import SvgZoomPan from '@/components/SvgZoomPan.vue'
44
import type { SSL_GeometryFieldSize } from '@/proto/vision/ssl_vision_geometry_pb.ts'
55
import SvgFieldLines from '@/components/SvgFieldLines.vue'
@@ -16,6 +16,16 @@ const emit = defineEmits<{
1616
moveObject: [x: number, y: number]
1717
}>()
1818
19+
// 親から提供されるselectedObjectをinjectして、さらにprovide(子孫に伝播)
20+
const selectedObject = inject<Ref<{
21+
type: 'robot' | 'ball'
22+
id?: number
23+
team?: 'YELLOW' | 'BLUE'
24+
} | null>>('selectedObject')
25+
if (selectedObject) {
26+
provide('selectedObject', selectedObject)
27+
}
28+
1929
function onMoveObject(x: number, y: number) {
2030
emit('moveObject', x, y)
2131
}

frontend/src/components/FieldVisualizerVision.vue

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import SvgGameEvents from '@/components/SvgGameEvents.vue'
88
import SettingsPanel from '@/components/SettingsPanel.vue'
99
import RefereeInfo from '@/components/RefereeInfo.vue'
1010
import type { LayerVisibility } from '@/components/LayerControl.vue'
11-
import { computed, ref } from 'vue'
11+
import { computed, inject, provide, ref, type Ref } from 'vue'
1212
import {
1313
useTrackedFrame,
1414
useTrackedSources,
@@ -26,6 +26,14 @@ const { trackerSources } = useTrackedSources()
2626
2727
const svgVisionRef = ref<InstanceType<typeof SvgVision>>()
2828
29+
// 選択状態をこのコンポーネントで管理し、配下全体でprovide
30+
const selectedObject = ref<{
31+
type: 'robot' | 'ball'
32+
id?: number
33+
team?: 'YELLOW' | 'BLUE'
34+
} | null>(null)
35+
provide('selectedObject', selectedObject)
36+
2937
function onMoveObject(x: number, y: number) {
3038
svgVisionRef.value?.onMoveObject(x, y)
3139
}

frontend/src/components/RefereeInfo.vue

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,22 @@
11
<script setup lang="ts">
22
import type { Referee } from '@/proto/gc/ssl_gc_referee_message_pb.ts'
3-
import { computed } from 'vue'
3+
import { computed, inject, type Ref } from 'vue'
44
55
const props = defineProps<{
66
referee: Referee
77
}>()
88
9+
// 選択状態を取得
10+
const selectedObject = inject<Ref<{
11+
type: 'robot' | 'ball'
12+
id?: number
13+
team?: 'YELLOW' | 'BLUE'
14+
} | null>>('selectedObject', { value: null } as Ref<{
15+
type: 'robot' | 'ball'
16+
id?: number
17+
team?: 'YELLOW' | 'BLUE'
18+
} | null>)
19+
920
// ゲームステージの文字列変換
1021
const stageText = computed(() => {
1122
const stageMap: { [key: number]: string } = {
@@ -67,6 +78,22 @@ const yellowScore = computed(() => props.referee.yellow?.score ?? 0)
6778
// チーム名
6879
const blueName = computed(() => props.referee.blue?.name || '')
6980
const yellowName = computed(() => props.referee.yellow?.name || '')
81+
82+
// 選択状態の表示テキスト
83+
const selectedText = computed(() => {
84+
if (!selectedObject.value) return '選択なし'
85+
86+
if (selectedObject.value.type === 'ball') {
87+
return 'ボール'
88+
}
89+
90+
if (selectedObject.value.type === 'robot') {
91+
const team = selectedObject.value.team === 'YELLOW' ? 'イエロー' : 'ブルー'
92+
return `${team} ロボット #${selectedObject.value.id}`
93+
}
94+
95+
return '選択なし'
96+
})
7097
</script>
7198

7299
<template>
@@ -91,6 +118,12 @@ const yellowName = computed(() => props.referee.yellow?.name || '')
91118
<div class="info-section">
92119
<span class="value time">{{ timeText }}</span>
93120
</div>
121+
<div class="info-section selection">
122+
<span class="label">選択:</span>
123+
<span class="value selection-text" :class="{ selected: selectedObject }">
124+
{{ selectedText }}
125+
</span>
126+
</div>
94127
</div>
95128
</template>
96129

@@ -175,4 +208,21 @@ const yellowName = computed(() => props.referee.yellow?.name || '')
175208
color: #666;
176209
font-weight: bold;
177210
}
211+
212+
.info-section.selection {
213+
background-color: rgba(50, 50, 50, 0.8);
214+
padding: 0.3em 0.8em;
215+
border-radius: 4px;
216+
border: 1px solid #444;
217+
}
218+
219+
.selection-text {
220+
color: #888;
221+
font-size: 0.95em;
222+
}
223+
224+
.selection-text.selected {
225+
color: #00ff88;
226+
font-weight: bold;
227+
}
178228
</style>

frontend/src/components/SvgBall.vue

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,13 +51,23 @@ function onClick(event: MouseEvent) {
5151
<g>
5252
<circle :cx="x" :cy="-y" :r="radius" :style="style" />
5353

54-
<!-- ball highlighter -->
54+
<!-- クリック可能エリア (透明、大きめ) -->
5555
<circle
5656
:cx="x"
5757
:cy="-y"
58-
:r="0.5"
59-
:style="highlightStyle"
58+
:r="0.1"
59+
fill="transparent"
60+
:style="{ cursor: props.draggable ? 'pointer' : 'default' }"
6061
@click="onClick"
6162
/>
63+
64+
<!-- ball highlighter (選択時のみ表示) -->
65+
<circle
66+
v-if="isSelected"
67+
:cx="x"
68+
:cy="-y"
69+
:r="radius * 3"
70+
:style="highlightStyle"
71+
/>
6272
</g>
6373
</template>

frontend/src/components/SvgRobot.vue

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,13 +49,21 @@ const isSelected = computed(() => {
4949
const style = computed(() => {
5050
return {
5151
stroke: 'black',
52-
strokeWidth: isSelected.value ? 0.01 : 0.005,
52+
strokeWidth: 0.005,
5353
strokeOpacity: 1,
5454
fill: props.teamColor == 'YELLOW' ? 'yellow' : 'blue',
5555
cursor: props.draggable ? 'pointer' : 'default',
5656
}
5757
})
5858
59+
const highlightStyle = computed(() => {
60+
return {
61+
stroke: '#00ff88',
62+
strokeWidth: 0.015,
63+
fill: 'none',
64+
}
65+
})
66+
5967
function onClick(event: MouseEvent) {
6068
if (props.draggable) {
6169
selectedObject.value = {
@@ -71,6 +79,14 @@ function onClick(event: MouseEvent) {
7179
<template>
7280
<g>
7381
<path :d="botShape" :style="style" @click="onClick" />
82+
<!-- 選択時のハイライトサークル -->
83+
<circle
84+
v-if="isSelected"
85+
:cx="x"
86+
:cy="-y"
87+
:r="radius * 1.3"
88+
:style="highlightStyle"
89+
/>
7490
<svg-text :x="x" :y="y" :text="robotId" :color="teamColor == 'YELLOW' ? 'black' : 'white'" />
7591
</g>
7692
</template>

frontend/src/components/SvgZoomPan.vue

Lines changed: 54 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<script setup lang="ts">
2-
import { computed, onBeforeUnmount, onMounted, provide, ref } from 'vue'
2+
import { computed, inject, onBeforeUnmount, onMounted, ref, type Ref } from 'vue'
33
44
const svg = ref<SVGElement>()
55
const zoom = ref(1.0)
@@ -9,16 +9,18 @@ const mouseDownPoint = ref<{
99
x: number
1010
y: number
1111
} | null>(null)
12+
const lastMousePosition = ref<{
13+
clientX: number
14+
clientY: number
15+
target: EventTarget | null
16+
} | null>(null)
1217
13-
// 選択状態の管理
14-
const selectedObject = ref<{
18+
// 親から提供されるselectedObjectをinject
19+
const selectedObject = inject<Ref<{
1520
type: 'robot' | 'ball'
1621
id?: number
1722
team?: 'YELLOW' | 'BLUE'
18-
} | null>(null)
19-
20-
// 選択状態をprovide
21-
provide('selectedObject', selectedObject)
23+
} | null>>('selectedObject')!
2224
2325
// 移動イベントの定義
2426
const emit = defineEmits<{
@@ -44,6 +46,13 @@ function onScroll(event: WheelEvent) {
4446
}
4547
4648
function onMouseMove(event: MouseEvent) {
49+
// マウス位置を記録(エンターキーでの移動に使用)
50+
lastMousePosition.value = {
51+
clientX: event.clientX,
52+
clientY: event.clientY,
53+
target: event.target,
54+
}
55+
4756
if (mouseDownPoint.value !== null) {
4857
activeTranslation.value = {
4958
x: event.clientX - mouseDownPoint.value.x,
@@ -67,6 +76,34 @@ function onMouseUp() {
6776
}
6877
}
6978
79+
function moveSelectedObject(clientX: number, clientY: number, target?: EventTarget | null) {
80+
if (selectedObject.value !== null) {
81+
// SVG座標を取得(イベントターゲットからownerSVGElementを取得)
82+
let svgElement: SVGSVGElement | null = null
83+
84+
if (target && target instanceof SVGElement) {
85+
svgElement = target.ownerSVGElement
86+
}
87+
88+
// フォールバック:svg.value を使用
89+
if (!svgElement) {
90+
svgElement = svg.value as SVGSVGElement | null
91+
}
92+
93+
if (svgElement) {
94+
const pt = svgElement.createSVGPoint()
95+
pt.x = clientX
96+
pt.y = clientY
97+
const ctm = svgElement.getScreenCTM()
98+
if (ctm) {
99+
const svgPt = pt.matrixTransform(ctm.inverse())
100+
// Y座標を反転してemit
101+
emit('moveObject', svgPt.x, -svgPt.y)
102+
}
103+
}
104+
}
105+
}
106+
70107
function onKeyDown(event: KeyboardEvent) {
71108
if (event.key === ' ') {
72109
zoom.value = 1
@@ -75,23 +112,19 @@ function onKeyDown(event: KeyboardEvent) {
75112
if (event.key === 'Escape') {
76113
selectedObject.value = null
77114
}
115+
if (event.key === 'Enter' && lastMousePosition.value) {
116+
moveSelectedObject(
117+
lastMousePosition.value.clientX,
118+
lastMousePosition.value.clientY,
119+
lastMousePosition.value.target
120+
)
121+
event.preventDefault()
122+
}
78123
}
79124
80125
function onDoubleClick(event: MouseEvent) {
81-
if (selectedObject.value !== null) {
82-
// SVG座標を取得
83-
const svgElement = svg.value
84-
if (svgElement) {
85-
const pt = svgElement.createSVGPoint()
86-
pt.x = event.clientX
87-
pt.y = event.clientY
88-
const svgPt = pt.matrixTransform(svgElement.getScreenCTM()?.inverse())
89-
90-
// Y座標を反転してemit
91-
emit('moveObject', svgPt.x, -svgPt.y)
92-
}
93-
event.preventDefault()
94-
}
126+
moveSelectedObject(event.clientX, event.clientY, event.target)
127+
event.preventDefault()
95128
}
96129
97130
const transform = computed(() => {

frontend/src/composables/grsim.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,12 +51,14 @@ export function useGrSimReplacement() {
5151
yellowTeam: boolean
5252
): Promise<void> => {
5353
try {
54+
const requestData = { x, y, dir, id, yellowTeam } as RobotReplacementRequest;
55+
5456
const response = await fetch(`${API_BASE}/robot`, {
5557
method: 'POST',
5658
headers: {
5759
'Content-Type': 'application/json',
5860
},
59-
body: JSON.stringify({ x, y, dir, id, yellowTeam } as RobotReplacementRequest),
61+
body: JSON.stringify(requestData),
6062
});
6163

6264
if (!response.ok) {

0 commit comments

Comments
 (0)