Skip to content

Commit b77b68c

Browse files
authored
Spawn Location Configuration [SYNTH-235] (#1415)
2 parents 9d87e1f + c0c6065 commit b77b68c

15 files changed

Lines changed: 535 additions & 61 deletions

fission/src/mirabuf/MirabufSceneObject.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -378,7 +378,7 @@ class MirabufSceneObject extends SceneObject implements ContextSupplier {
378378
? defaultFieldSpawnLocation()
379379
: (this.robotSpawnPosition(referencePos) ?? defaultFieldSpawnLocation())
380380

381-
this.setObjectPosition(pos)
381+
this.setObjectPosition(pos, referencePos)
382382
}
383383

384384
private robotSpawnPosition(referencePos: THREE.Vector3): SpawnLocation | undefined {
@@ -390,8 +390,8 @@ class MirabufSceneObject extends SceneObject implements ContextSupplier {
390390
? fieldLocations[this.alliance][this.station]
391391
: fieldLocations?.default
392392

393-
// TODO
394-
// Why are we calling this?
393+
// Mutates referencePos in place (Box3.getCenter side effect) so setObjectPosition can offset
394+
// this spawn location by the field's own transform instead of treating it as a world-space position.
395395
field?.getXZPositionTransform(referencePos)
396396

397397
return pos
Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
export type JoltBodyIndexAndSequence = number
22

33
export const PAUSE_REF_ASSEMBLY_SPAWNING = "assembly-spawning"
4-
export const PAUSE_REF_ASSEMBLY_CONFIG = "assembly-config"
54
export const PAUSE_REF_ASSEMBLY_MOVE = "assembly-move"

fission/src/systems/preferences/PreferenceTypes.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -210,9 +210,11 @@ export type MotorPreferences = {
210210
maxAcceleration: number
211211
}
212212

213-
export type Alliance = "red" | "blue"
213+
export const ALLIANCES = ["red", "blue"] as const
214+
export type Alliance = (typeof ALLIANCES)[number]
214215

215-
export type Station = 1 | 2 | 3
216+
export const STATIONS = [1, 2, 3] as const
217+
export type Station = (typeof STATIONS)[number]
216218

217219
export type ZonePreferencesShared = {
218220
name: string
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import { renderHook } from "@testing-library/react"
2+
import * as THREE from "three"
3+
import { beforeEach, describe, expect, test, vi } from "vitest"
4+
import type MirabufSceneObject from "@/mirabuf/MirabufSceneObject"
5+
import type GizmoSceneObject from "@/systems/scene/GizmoSceneObject"
6+
import {
7+
type FieldPointMarker,
8+
useFieldPointMarkers,
9+
useFieldRelativeGizmoPosition,
10+
} from "@/ui/panels/configuring/assembly-config/interfaces/FieldPointEditing"
11+
12+
let scene: THREE.Scene
13+
14+
vi.mock("@/systems/World", () => ({
15+
default: {
16+
get sceneRenderer() {
17+
return { scene }
18+
},
19+
},
20+
}))
21+
22+
function fieldAt(x: number, y: number, z: number): MirabufSceneObject {
23+
return {
24+
getXZPositionTransform: (vec: THREE.Vector3) => vec.set(x, y, z),
25+
} as unknown as MirabufSceneObject
26+
}
27+
28+
function emptyGizmo(): GizmoSceneObject {
29+
return { obj: new THREE.Object3D() } as unknown as GizmoSceneObject
30+
}
31+
32+
function tracksDisposal(target: THREE.Material | THREE.BufferGeometry): () => boolean {
33+
let disposed = false
34+
target.addEventListener("dispose", () => {
35+
disposed = true
36+
})
37+
return () => disposed
38+
}
39+
40+
beforeEach(() => {
41+
scene = new THREE.Scene()
42+
})
43+
44+
describe("useFieldRelativeGizmoPosition", () => {
45+
test("places a new gizmo at the field-relative offset", () => {
46+
const { result } = renderHook(() => useFieldRelativeGizmoPosition(fieldAt(10, 0, -5), [1, 2, 3]))
47+
const gizmo = emptyGizmo()
48+
49+
result.current.postGizmoCreation(gizmo)
50+
51+
expect(gizmo.obj.position.toArray()).toEqual([11, 2, -2])
52+
})
53+
54+
test("reads a dragged gizmo back as field-relative coordinates", () => {
55+
const { result } = renderHook(() => useFieldRelativeGizmoPosition(fieldAt(10, 0, -5), [1, 2, 3]))
56+
const gizmo = emptyGizmo()
57+
result.current.postGizmoCreation(gizmo)
58+
result.current.gizmoRef.current = gizmo
59+
60+
expect(result.current.readFieldRelativePosition()).toEqual([1, 2, 3])
61+
62+
gizmo.obj.position.x += 4
63+
64+
expect(result.current.readFieldRelativePosition()).toEqual([5, 2, 3])
65+
})
66+
})
67+
68+
describe("useFieldPointMarkers", () => {
69+
test("adds a marker per point and removes them on unmount", () => {
70+
const points: FieldPointMarker[] = [{ pos: [1, 0, 2], yaw: Math.PI / 2 }, { pos: [-1, 0, 0] }]
71+
72+
const { unmount } = renderHook(() => useFieldPointMarkers(fieldAt(10, 0, -5), points))
73+
74+
expect(scene.children).toHaveLength(2)
75+
76+
const [oriented, plain] = scene.children
77+
expect(oriented.position.toArray()).toEqual([11, 0, -3])
78+
expect(oriented.rotation.y).toBeCloseTo(Math.PI / 2)
79+
expect(oriented.children).toHaveLength(2) // dot + direction cone
80+
expect(plain.children).toHaveLength(1) // dot only, no yaw to indicate
81+
82+
const geometryDisposed = tracksDisposal((oriented.children[0] as THREE.Mesh).geometry)
83+
84+
unmount()
85+
86+
expect(scene.children).toHaveLength(0)
87+
expect(geometryDisposed()).toBe(true)
88+
})
89+
})

fission/src/ui/panels/configuring/assembly-config/ConfigTypes.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ export enum ConfigMode {
6565
SCORING_ZONES,
6666
PROTECTED_ZONES,
6767
CAMERA_POINTS,
68+
SPAWN_POSITIONS,
6869
MOVE,
6970
SIM,
7071
BRAIN,
@@ -137,6 +138,11 @@ export const fieldConfigModes = [
137138
ConfigMode.PROTECTED_ZONES,
138139
"Define and manage protected zones on the field where robots can not enter."
139140
),
141+
new ConfigModeSelectionOption(
142+
"Robot Spawn Positions",
143+
ConfigMode.SPAWN_POSITIONS,
144+
"Set where robots spawn for the default position and each alliance station."
145+
),
140146
new ConfigModeSelectionOption(
141147
"Camera Positions",
142148
ConfigMode.CAMERA_POINTS,

fission/src/ui/panels/configuring/assembly-config/ConfigurePanel.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,14 @@ import AssemblySelection, { type AssemblySelectionOption } from "./configure/Ass
1919
import ConfigModeSelection from "./configure/ConfigModeSelection"
2020
import AllianceSelectionInterface from "./interfaces/AllianceSelectionInterface"
2121
import BrainSelectionInterface from "./interfaces/BrainSelectionInterface"
22-
import ConfigureGamepiecePickupInterface from "./interfaces/ConfigureGamepieceIntakeInterface.tsx"
23-
import ConfigureShotTrajectoryInterface from "./interfaces/ConfigureGamepieceEjectorInterface.tsx"
22+
import ConfigureGamepieceIntakeInterface from "./interfaces/ConfigureGamepieceIntakeInterface.tsx"
23+
import ConfigureGamepieceEjectorInterface from "./interfaces/ConfigureGamepieceEjectorInterface.tsx"
2424
import ConfigureJointsInterface from "./interfaces/ConfigureJointsInterface"
2525
import DrivetrainSelectionInterface from "./interfaces/DrivetrainSelectionInterface"
2626
import ConfigureInputsInterface from "./interfaces/inputs/ConfigureInputsInterface"
2727
import SimulationInterface from "./interfaces/SimulationInterface"
2828
import ConfigureCameraPointsInterface from "./interfaces/ConfigureCameraPointsInterface"
29+
import ConfigureSpawnPositionsInterface from "./interfaces/ConfigureSpawnPositionsInterface"
2930
import ConfigureProtectedZonesInterface from "./interfaces/scoring/ConfigureProtectedZonesInterface"
3031
import ConfigureScoringZonesInterface from "./interfaces/scoring/ConfigureScoringZonesInterface"
3132
import EventSystem from "@/systems/EventSystem.ts"
@@ -145,13 +146,14 @@ export interface ConfigurePanelCustomProps {
145146
}
146147
const subConfigPanels: Record<ConfigMode, ConfigurationSubpanelComponent> = {
147148
[ConfigMode.JOINTS]: ConfigureJointsInterface,
148-
[ConfigMode.EJECTOR]: ConfigureShotTrajectoryInterface,
149+
[ConfigMode.EJECTOR]: ConfigureGamepieceEjectorInterface,
150+
[ConfigMode.INTAKE]: ConfigureGamepieceIntakeInterface,
149151
[ConfigMode.CAMERA]: ConfigureCameraInterface,
150-
[ConfigMode.INTAKE]: ConfigureGamepiecePickupInterface,
151152
[ConfigMode.CONTROLS]: ControlsConfigInterface,
152153
[ConfigMode.SCORING_ZONES]: ConfigureScoringZonesInterface,
153154
[ConfigMode.PROTECTED_ZONES]: ConfigureProtectedZonesInterface,
154155
[ConfigMode.CAMERA_POINTS]: ConfigureCameraPointsInterface,
156+
[ConfigMode.SPAWN_POSITIONS]: ConfigureSpawnPositionsInterface,
155157
[ConfigMode.MOVE]: MoveInterface,
156158
[ConfigMode.SIM]: SimulationInterface,
157159
[ConfigMode.BRAIN]: BrainSelectionInterface,

fission/src/ui/panels/configuring/assembly-config/interfaces/ConfigureCameraPointsInterface.tsx

Lines changed: 49 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Divider, MenuItem, Select, Stack, TextField } from "@mui/material"
2-
import { useCallback, useEffect, useRef, useState } from "react"
3-
import * as THREE from "three"
2+
import { useCallback, useEffect, useMemo, useState } from "react"
3+
import { SelectMenuHeader } from "@/components/SelectMenu.tsx"
44
import type MirabufSceneObject from "@/mirabuf/MirabufSceneObject"
55
import EventSystem from "@/systems/EventSystem.ts"
66
import PreferencesSystem from "@/systems/preferences/PreferencesSystem"
@@ -11,7 +11,13 @@ import ScrollView from "@/ui/components/ScrollView"
1111
import { AddButton, DeleteButton, EditButton } from "@/ui/components/StyledComponents"
1212
import TransformGizmoControl from "@/ui/components/TransformGizmoControl"
1313
import type { ConfigurationSubpanelComponent } from "@/panels/configuring/assembly-config/ConfigTypes.ts"
14-
import { useHoldPhysicsPause } from "@/util/ReactHooks.ts"
14+
import { useConfigurationSavedListener, useHoldPhysicsPause } from "@/util/ReactHooks.ts"
15+
import {
16+
useDirectionIndicatorMesh,
17+
useFieldPointMarkers,
18+
useFieldRelativeGizmoPosition,
19+
useSyncIndicatorRotation,
20+
} from "./FieldPointEditing"
1521

1622
const RAD_TO_DEG = 180 / Math.PI
1723
const DEG_TO_RAD = Math.PI / 180
@@ -44,12 +50,20 @@ interface ListViewProps {
4450
const ListView: React.FC<ListViewProps> = ({ selectedField, points, onChange, onAdd, onEdit }) => {
4551
const saveEvent = useCallback(() => persist(points, selectedField), [points, selectedField])
4652

47-
useEffect(() => EventSystem.listen("ConfigurationSavedEvent", saveEvent), [saveEvent])
48-
useEffect(() => {
49-
persist(points, selectedField)
50-
}, [selectedField, points])
51-
53+
useConfigurationSavedListener(saveEvent)
5254
useHoldPhysicsPause()
55+
useEffect(() => persist(points, selectedField), [selectedField, points])
56+
57+
const markerPoints = useMemo(
58+
() =>
59+
points.map(p => ({
60+
pos: p.pos,
61+
yaw: p.look.type === "rotation" ? p.look.yaw : undefined,
62+
pitch: p.look.type === "rotation" ? p.look.pitch : undefined,
63+
})),
64+
[points]
65+
)
66+
useFieldPointMarkers(selectedField, markerPoints, "-z")
5367

5468
return (
5569
<>
@@ -105,32 +119,35 @@ const EditView: React.FC<EditViewProps> = ({ selectedField, point, onSave }) =>
105119
const [pitchDeg, setPitchDeg] = useState(
106120
point.look.type === "rotation" ? Math.round(point.look.pitch * RAD_TO_DEG) : -30
107121
)
108-
const gizmoRef = useRef<GizmoSceneObject | undefined>(undefined)
122+
const { gizmoRef, postGizmoCreation, readFieldRelativePosition } = useFieldRelativeGizmoPosition(
123+
selectedField,
124+
point.pos
125+
)
126+
// Cameras look down their local -Z axis, so the indicator points -Z instead of the usual +Z "forward".
127+
const directionIndicatorMesh = useDirectionIndicatorMesh("-z")
128+
useSyncIndicatorRotation(directionIndicatorMesh, yawDeg * DEG_TO_RAD, pitchDeg * DEG_TO_RAD)
109129

110-
const postGizmoCreation = useCallback(
130+
const setupGizmo = useCallback(
111131
(gizmo: GizmoSceneObject) => {
112-
const fieldRef = selectedField.getXZPositionTransform()
113-
gizmo.obj.position.set(fieldRef.x + point.pos[0], fieldRef.y + point.pos[1], fieldRef.z + point.pos[2])
132+
postGizmoCreation(gizmo)
133+
gizmo.obj.add(directionIndicatorMesh)
114134
},
115-
[selectedField, point.pos]
135+
[postGizmoCreation, directionIndicatorMesh]
116136
)
117137

138+
useEffect(() => {
139+
directionIndicatorMesh.visible = lookType === "rotation"
140+
}, [directionIndicatorMesh, lookType])
141+
118142
const buildPoint = useCallback((): CameraPoint => {
119-
let pos: [number, number, number] = [point.pos[0], point.pos[1], point.pos[2]]
120-
if (gizmoRef.current) {
121-
gizmoRef.current.obj.updateWorldMatrix(true, false)
122-
const worldPos = gizmoRef.current.obj.getWorldPosition(new THREE.Vector3())
123-
const fieldRef = selectedField.getXZPositionTransform()
124-
pos = [worldPos.x - fieldRef.x, worldPos.y - fieldRef.y, worldPos.z - fieldRef.z]
125-
}
126143
const look: CameraLook =
127144
lookType === "field"
128145
? { type: "field" }
129146
: { type: "rotation", yaw: yawDeg * DEG_TO_RAD, pitch: pitchDeg * DEG_TO_RAD }
130-
return { name, pos, look }
131-
}, [selectedField, point.pos, lookType, name, yawDeg, pitchDeg])
147+
return { name, pos: readFieldRelativePosition(), look }
148+
}, [readFieldRelativePosition, lookType, name, yawDeg, pitchDeg])
132149

133-
useEffect(() => EventSystem.listen("ConfigurationSavedEvent", () => onSave(buildPoint())), [buildPoint, onSave])
150+
useConfigurationSavedListener(useCallback(() => onSave(buildPoint()), [buildPoint, onSave]))
134151
useHoldPhysicsPause()
135152

136153
return (
@@ -182,7 +199,7 @@ const EditView: React.FC<EditViewProps> = ({ selectedField, point, onSave }) =>
182199
defaultMode="translate"
183200
rotateDisabled={true}
184201
scaleDisabled={true}
185-
postGizmoCreation={postGizmoCreation}
202+
postGizmoCreation={setupGizmo}
186203
/>
187204
</Stack>
188205
)
@@ -229,11 +246,14 @@ const ConfigureCameraPointsInterface: ConfigurationSubpanelComponent = ({
229246
if (editIndex !== undefined && points[editIndex] !== undefined) {
230247
return (
231248
<>
232-
<Stack direction="row" minHeight="30px" alignItems="center">
233-
<Label size="sm" className="text-center mt-[4pt] mb-[2pt] mx-[5%]">
234-
Configuring Camera Position
235-
</Label>
236-
</Stack>
249+
<SelectMenuHeader
250+
label={points[editIndex].name}
251+
showBackButton={true}
252+
onBackButton={() => {
253+
EventSystem.dispatch("ConfigurationSavedEvent")
254+
setEditIndex(undefined)
255+
}}
256+
/>
237257
<Divider />
238258
<EditView
239259
selectedField={selectedAssembly}

fission/src/ui/panels/configuring/assembly-config/interfaces/ConfigureGamepieceEjectorInterface.tsx

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import SelectButton from "@/components/SelectButton"
66
import type { RigidNodeId } from "@/mirabuf/MirabufParser"
77
import type MirabufSceneObject from "@/mirabuf/MirabufSceneObject"
88
import type { RigidNodeAssociate } from "@/mirabuf/MirabufSceneObject"
9-
import EventSystem from "@/systems/EventSystem.ts"
109
import type GizmoSceneObject from "@/systems/scene/GizmoSceneObject"
1110
import World from "@/systems/World"
1211
import StatefulSlider from "@/ui/components/StatefulSlider"
@@ -19,7 +18,7 @@ import {
1918
convertThreeMatrix4ToArray,
2019
} from "@/util/TypeConversions"
2120
import type { ConfigurationSubpanelComponent } from "@/panels/configuring/assembly-config/ConfigTypes.ts"
22-
import { useHoldPhysicsPause } from "@/util/ReactHooks.ts"
21+
import { useConfigurationSavedListener, useHoldPhysicsPause } from "@/util/ReactHooks.ts"
2322

2423
// slider constants
2524
const MIN_VELOCITY = 0.0
@@ -109,9 +108,7 @@ const ConfigureGamepieceEjectorInterface: ConfigurationSubpanelComponent = ({
109108
})
110109
}, [registerCleanupFunction, selectedAssembly])
111110

112-
useEffect(() => {
113-
return EventSystem.listen("ConfigurationSavedEvent", saveEvent)
114-
}, [saveEvent])
111+
useConfigurationSavedListener(saveEvent)
115112

116113
const placeholderMesh = useMemo(() => {
117114
return new THREE.Mesh(

fission/src/ui/panels/configuring/assembly-config/interfaces/ConfigureGamepieceIntakeInterface.tsx

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import EjectableSceneObject from "@/mirabuf/EjectableSceneObject"
77
import type { RigidNodeId } from "@/mirabuf/MirabufParser"
88
import type MirabufSceneObject from "@/mirabuf/MirabufSceneObject"
99
import type { RigidNodeAssociate } from "@/mirabuf/MirabufSceneObject"
10-
import EventSystem from "@/systems/EventSystem.ts"
1110
import type GizmoSceneObject from "@/systems/scene/GizmoSceneObject"
1211
import World from "@/systems/World"
1312
import Checkbox from "@/ui/components/Checkbox"
@@ -21,7 +20,7 @@ import {
2120
convertThreeMatrix4ToArray,
2221
} from "@/util/TypeConversions"
2322
import type { ConfigurationSubpanelComponent } from "@/panels/configuring/assembly-config/ConfigTypes.ts"
24-
import { useHoldPhysicsPause } from "@/util/ReactHooks.ts"
23+
import { useConfigurationSavedListener, useHoldPhysicsPause } from "@/util/ReactHooks.ts"
2524

2625
// slider constants
2726
const MIN_ZONE_SIZE = 0.1
@@ -124,9 +123,7 @@ const ConfigureGamepieceIntakeInterface: ConfigurationSubpanelComponent = ({
124123
}
125124
}, [selectedAssembly, selectedNode, zoneSize, showZoneAlways, maxPieces, animationDuration])
126125

127-
useEffect(() => {
128-
return EventSystem.listen("ConfigurationSavedEvent", saveEvent)
129-
}, [saveEvent])
126+
useConfigurationSavedListener(saveEvent)
130127

131128
useEffect(() => {
132129
if (!gizmoRef.current) {

0 commit comments

Comments
 (0)