Skip to content

Commit 10c8f1a

Browse files
Add permanent modification
1 parent 2d92ac9 commit 10c8f1a

File tree

5 files changed

+176
-26
lines changed

5 files changed

+176
-26
lines changed

fission/src/ui/modals/DevtoolZoneRemovalModal.tsx

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ interface DevtoolZoneRemovalModalProps {
1010
zoneName: string
1111
onTemporaryRemoval: () => void
1212
onPermanentRemoval: () => void
13+
actionType?: "removal" | "modification"
1314
}
1415

1516
const DevtoolZoneRemovalModal: React.FC<DevtoolZoneRemovalModalProps> = ({
@@ -19,6 +20,7 @@ const DevtoolZoneRemovalModal: React.FC<DevtoolZoneRemovalModalProps> = ({
1920
zoneName,
2021
onTemporaryRemoval,
2122
onPermanentRemoval,
23+
actionType = "removal",
2224
}) => {
2325
const [isRemoving, setIsRemoving] = useState(false)
2426

@@ -31,10 +33,13 @@ const DevtoolZoneRemovalModal: React.FC<DevtoolZoneRemovalModalProps> = ({
3133
setIsRemoving(true)
3234
try {
3335
await onPermanentRemoval()
34-
globalAddToast?.("info", "Zone Removed", `${zoneName} has been permanently removed from the field file.`)
36+
const actionText = actionType === "modification" ? "modified" : "removed"
37+
const actionCapitalized = actionType === "modification" ? "Modified" : "Removed"
38+
globalAddToast?.("info", `Zone ${actionCapitalized}`, `${zoneName} has been permanently ${actionText} in the field file.`)
3539
} catch (error) {
36-
globalAddToast?.("error", "Removal Failed", "Failed to permanently remove zone from the field file cache.")
37-
console.error("Failed to remove zone from field file:", error)
40+
const actionText = actionType === "modification" ? "modify" : "remove"
41+
globalAddToast?.("error", `${actionType === "modification" ? "Modification" : "Removal"} Failed`, `Failed to permanently ${actionText} zone in the field file cache.`)
42+
console.error(`Failed to ${actionText} zone in field file:`, error)
3843
} finally {
3944
setIsRemoving(false)
4045
onClose()
@@ -43,23 +48,21 @@ const DevtoolZoneRemovalModal: React.FC<DevtoolZoneRemovalModalProps> = ({
4348

4449
return (
4550
<Dialog open={isOpen} onClose={onClose} maxWidth="sm" fullWidth>
46-
<DialogTitle>Remove {zoneType === "scoring" ? "Scoring" : "Protected"} Zone</DialogTitle>
51+
<DialogTitle>{actionType === "modification" ? "Modify" : "Remove"} {zoneType === "scoring" ? "Scoring" : "Protected"} Zone</DialogTitle>
4752
<DialogContent>
4853
<Stack spacing={2}>
4954
<Typography variant="body1">
5055
The {zoneType} zone "{zoneName}" was defined in the field file and is cached.
5156
</Typography>
5257
<Typography variant="body2" color="text.secondary">
53-
Choose how you'd like to remove it:
58+
Choose how you'd like to {actionType === "modification" ? "save your modifications" : "remove it"}:
5459
</Typography>
5560
<Stack spacing={1}>
5661
<Typography variant="body2">
57-
<strong>Temporary removal:</strong> Remove zone until next field reload. The zone will
58-
reappear when you refresh or reload the field.
62+
<strong>Temporary {actionType === "modification" ? "modification" : "removal"}:</strong> {actionType === "modification" ? "Save changes until next field reload. Original zone will reappear when you refresh or reload the field." : "Remove zone until next field reload. The zone will reappear when you refresh or reload the field."}
5963
</Typography>
6064
<Typography variant="body2">
61-
<strong>Permanent removal:</strong> Remove zone from the field file cache. This will prevent it
62-
from reappearing on future loads.
65+
<strong>Permanent {actionType === "modification" ? "modification" : "removal"}:</strong> {actionType === "modification" ? "Save changes to the field file cache. This will persist your modifications on future loads." : "Remove zone from the field file cache. This will prevent it from reappearing on future loads."}
6366
</Typography>
6467
</Stack>
6568
</Stack>
@@ -69,10 +72,10 @@ const DevtoolZoneRemovalModal: React.FC<DevtoolZoneRemovalModalProps> = ({
6972
Cancel
7073
</Button>
7174
<Button onClick={handleTemporaryRemoval} disabled={isRemoving} variant="outlined" color="warning">
72-
Temporary Removal
75+
Temporary {actionType === "modification" ? "Modification" : "Removal"}
7376
</Button>
74-
<Button onClick={handlePermanentRemoval} disabled={isRemoving} variant="contained" color="error">
75-
{isRemoving ? "Removing..." : "Permanent Removal"}
77+
<Button onClick={handlePermanentRemoval} disabled={isRemoving} variant="contained" color={actionType === "modification" ? "primary" : "error"}>
78+
{isRemoving ? `${actionType === "modification" ? "Modifying" : "Removing"}...` : `Permanent ${actionType === "modification" ? "Modification" : "Removal"}`}
7679
</Button>
7780
</DialogActions>
7881
</Dialog>

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import World from "@/systems/World"
1414
import Label from "@/ui/components/Label"
1515
import type { PanelImplProps } from "@/ui/components/Panel"
1616
import TransformGizmoControl from "@/ui/components/TransformGizmoControl"
17-
import { CloseType, type UIScreen, useUIContext } from "@/ui/helpers/UIProviderHelpers"
17+
import { CloseType, type Panel, type UIScreen, useUIContext } from "@/ui/helpers/UIProviderHelpers"
1818
import ChooseInputSchemePanel from "../ChooseInputSchemePanel"
1919
import { CONFIG_OPTS, ConfigMode, type ConfigurationType } from "./ConfigTypes"
2020
import AssemblySelection, { type AssemblySelectionOption } from "./configure/AssemblySelection"
@@ -79,7 +79,7 @@ const ConfigInterface: React.FC<ConfigInterfaceProps<void, ConfigurePanelCustomP
7979
console.error("Field does not contain scoring zone preferences!")
8080
return <Label size="md">ERROR: Field does not contain scoring zone configuration!</Label>
8181
}
82-
return <ConfigureScoringZonesInterface selectedField={assembly} initialZones={zones} />
82+
return <ConfigureScoringZonesInterface panel={panel as Panel<any, any>} selectedField={assembly} initialZones={zones} />
8383
}
8484
case ConfigMode.PROTECTED_ZONES: {
8585
const zones = assembly.fieldPreferences?.protectedZones ?? []

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,15 @@ const saveZones = (zones: ScoringZonePreferences[] | undefined, field: MirabufSc
2020
field.updateScoringZones()
2121
}
2222

23+
import type { Panel } from "@/ui/helpers/UIProviderHelpers"
24+
2325
interface ConfigureZonesProps {
2426
selectedField: MirabufSceneObject
2527
initialZones: ScoringZonePreferences[]
28+
panel?: Panel<any, any>
2629
}
2730

28-
const ConfigureScoringZonesInterface: React.FC<ConfigureZonesProps> = ({ selectedField, initialZones }) => {
31+
const ConfigureScoringZonesInterface: React.FC<ConfigureZonesProps> = ({ selectedField, initialZones, panel }) => {
2932
const [selectedZone, setSelectedZone] = useState<ScoringZonePreferences | undefined>(undefined)
3033

3134
return (
@@ -65,6 +68,7 @@ const ConfigureScoringZonesInterface: React.FC<ConfigureZonesProps> = ({ selecte
6568
saveAllZones={() => {
6669
saveZones(selectedField.fieldPreferences?.scoringZones, selectedField)
6770
}}
71+
panel={panel}
6872
/>
6973
</>
7074
)}

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

Lines changed: 100 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type Jolt from "@azaleacolburn/jolt-physics"
2-
import { Button, TextField } from "@mui/material"
2+
import { Button, Stack, TextField } from "@mui/material"
33
import { useCallback, useEffect, useMemo, useRef, useState } from "react"
44
import * as THREE from "three"
55
import { ConfigurationSavedEvent } from "@/events/ConfigurationSavedEvent"
@@ -11,15 +11,20 @@ import PreferencesSystem from "@/systems/preferences/PreferencesSystem"
1111
import type { Alliance, ScoringZonePreferences } from "@/systems/preferences/PreferenceTypes"
1212
import type GizmoSceneObject from "@/systems/scene/GizmoSceneObject"
1313
import World from "@/systems/World"
14+
1415
import Checkbox from "@/ui/components/Checkbox"
1516
import SelectButton from "@/ui/components/SelectButton"
1617
import TransformGizmoControl from "@/ui/components/TransformGizmoControl"
18+
import DevtoolZoneRemovalModal from "@/ui/modals/DevtoolZoneRemovalModal"
19+
import { isZoneFromDevtools, modifyZoneInDevtools } from "@/util/DevtoolZoneUtils"
1720
import {
1821
convertArrayToThreeMatrix4,
1922
convertJoltMat44ToThreeMatrix4,
2023
convertThreeMatrix4ToArray,
2124
} from "@/util/TypeConversions"
2225
import { deltaFieldTransformsPhysicalProp as deltaFieldTransformsVisualProperties } from "@/util/threejs/MeshCreation"
26+
import type { Panel } from "@/ui/helpers/UIProviderHelpers"
27+
import { CloseType, useUIContext } from "@/ui/helpers/UIProviderHelpers"
2328

2429
/**
2530
* Saves ejector configuration to selected field.
@@ -101,9 +106,10 @@ interface ZoneConfigProps {
101106
selectedField: MirabufSceneObject
102107
selectedZone: ScoringZonePreferences
103108
saveAllZones: () => void
109+
panel?: Panel<any, any>
104110
}
105111

106-
const ZoneConfigInterface: React.FC<ZoneConfigProps> = ({ selectedField, selectedZone, saveAllZones }) => {
112+
const ZoneConfigInterface: React.FC<ZoneConfigProps> = ({ selectedField, selectedZone, saveAllZones, panel }) => {
107113
//Official FIRST hex
108114
// TODO: Do we want to eventually make these editable?
109115
const redMaterial = useMemo(() => {
@@ -130,10 +136,23 @@ const ZoneConfigInterface: React.FC<ZoneConfigProps> = ({ selectedField, selecte
130136
const [points, setPoints] = useState<number>(selectedZone.points)
131137
const [destroy] = useState<boolean>(selectedZone.destroyGamepiece)
132138
const [persistent, setPersistent] = useState<boolean>(selectedZone.persistentPoints)
139+
const [confirmationModal, setConfirmationModal] = useState<{
140+
isOpen: boolean
141+
pendingSave: boolean
142+
}>({ isOpen: false, pendingSave: false })
133143

134144
const gizmoRef = useRef<GizmoSceneObject | undefined>(undefined)
145+
const originalZoneRef = useRef<ScoringZonePreferences>(structuredClone(selectedZone))
146+
const { configureScreen, closePanel } = useUIContext()
147+
148+
// Hide the panel's default footer buttons when this interface is active
149+
useEffect(() => {
150+
if (panel) {
151+
configureScreen(panel, { hideAccept: true, hideCancel: true }, {})
152+
}
153+
}, [panel, configureScreen])
135154

136-
const saveEvent = useCallback(() => {
155+
const handleSave = useCallback(() => {
137156
if (gizmoRef.current && selectedField) {
138157
save(
139158
selectedField,
@@ -150,14 +169,6 @@ const ZoneConfigInterface: React.FC<ZoneConfigProps> = ({ selectedField, selecte
150169
}
151170
}, [selectedField, selectedZone, name, alliance, points, destroy, persistent, selectedNode, saveAllZones])
152171

153-
useEffect(() => {
154-
ConfigurationSavedEvent.listen(saveEvent)
155-
156-
return () => {
157-
ConfigurationSavedEvent.removeListener(saveEvent)
158-
}
159-
}, [saveEvent])
160-
161172
/** Holds a pause for the duration of the interface component */
162173
useEffect(() => {
163174
World.physicsSystem.holdPause(PAUSE_REF_ASSEMBLY_CONFIG)
@@ -245,6 +256,47 @@ const ZoneConfigInterface: React.FC<ZoneConfigProps> = ({ selectedField, selecte
245256
[selectedField]
246257
)
247258

259+
const handleTemporaryModification = () => {
260+
// For temporary modification, just save the changes to the current session
261+
// without persisting to the field file cache
262+
handleSave()
263+
264+
// Save all zones to ensure the changes are persisted to preferences
265+
saveAllZones()
266+
267+
setConfirmationModal({ isOpen: false, pendingSave: false })
268+
if (panel) closePanel(panel.id, CloseType.Accept)
269+
}
270+
271+
const handlePermanentModification = async () => {
272+
try {
273+
const modifiedZone: ScoringZonePreferences = {
274+
name,
275+
alliance,
276+
parentNode: selectedNode,
277+
points,
278+
destroyGamepiece: destroy,
279+
persistentPoints: persistent,
280+
deltaTransformation: selectedZone.deltaTransformation
281+
}
282+
283+
handleSave()
284+
modifiedZone.deltaTransformation = selectedZone.deltaTransformation
285+
286+
await modifyZoneInDevtools(originalZoneRef.current, modifiedZone, "scoring")
287+
288+
} catch (error) {
289+
console.error("Failed to permanently modify zone:", error)
290+
} finally {
291+
setConfirmationModal({ isOpen: false, pendingSave: false })
292+
if (panel) closePanel(panel.id, CloseType.Accept)
293+
}
294+
}
295+
296+
const handleCloseConfirmation = () => {
297+
setConfirmationModal({ isOpen: false, pendingSave: false })
298+
}
299+
248300
return (
249301
<div className="flex flex-col gap-2 bg-background-secondary rounded-md p-2">
250302
{/** Set the zone name */}
@@ -294,6 +346,43 @@ const ZoneConfigInterface: React.FC<ZoneConfigProps> = ({ selectedField, selecte
294346
{/** Switch between transform control modes */}
295347

296348
{gizmoComponent}
349+
350+
<DevtoolZoneRemovalModal
351+
isOpen={confirmationModal.isOpen}
352+
onClose={handleCloseConfirmation}
353+
zoneType="scoring"
354+
zoneName={selectedZone.name}
355+
onTemporaryRemoval={handleTemporaryModification}
356+
onPermanentRemoval={handlePermanentModification}
357+
actionType="modification"
358+
/>
359+
360+
{/** Custom Save/Cancel buttons that replace the panel's default buttons */}
361+
<Stack direction="row" justifyContent="flex-end" gap={1} mt={2}>
362+
<Button
363+
variant="outlined"
364+
color="secondary"
365+
onClick={() => {
366+
if (panel) closePanel(panel.id, CloseType.Cancel)
367+
}}
368+
>
369+
Cancel
370+
</Button>
371+
<Button
372+
variant="contained"
373+
color="primary"
374+
onClick={() => {
375+
if (isZoneFromDevtools(selectedZone, "scoring")) {
376+
setConfirmationModal({ isOpen: true, pendingSave: true })
377+
} else {
378+
handleSave()
379+
if (panel) closePanel(panel.id, CloseType.Accept)
380+
}
381+
}}
382+
>
383+
Save
384+
</Button>
385+
</Stack>
297386
</div>
298387
)
299388
}

fission/src/util/DevtoolZoneUtils.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,60 @@ export async function removeZoneFromDevtools(
9797
}
9898
}
9999

100+
/**
101+
* Modifies a zone in the field file cache permanently by replacing it with updated data.
102+
*/
103+
export async function modifyZoneInDevtools(
104+
originalZone: ScoringZonePreferences | ProtectedZonePreferences,
105+
modifiedZone: ScoringZonePreferences | ProtectedZonePreferences,
106+
zoneType: ZoneType
107+
): Promise<void> {
108+
const field = World.sceneRenderer.mirabufSceneObjects.getField()
109+
if (!field) throw new Error("No field loaded")
110+
111+
const parts = field.mirabufInstance.parser.assembly.data?.parts
112+
if (!parts) throw new Error("No field parts found")
113+
114+
const editor = new FieldMiraEditor(parts)
115+
116+
if (zoneType === "scoring") {
117+
const devtoolZones = editor.getUserData("devtool:scoring_zones") as ScoringZonePreferences[] | undefined
118+
if (!devtoolZones) return
119+
120+
// Find and replace the zone in field file data
121+
const updatedZones = devtoolZones.map(devZone => {
122+
if (
123+
devZone.name === originalZone.name &&
124+
devZone.alliance === originalZone.alliance &&
125+
devZone.parentNode === originalZone.parentNode &&
126+
JSON.stringify(devZone.deltaTransformation) === JSON.stringify(originalZone.deltaTransformation)
127+
) {
128+
return modifiedZone as ScoringZonePreferences
129+
}
130+
return devZone
131+
})
132+
133+
editor.setUserData("devtool:scoring_zones", updatedZones)
134+
135+
if (field.fieldPreferences) {
136+
field.fieldPreferences.scoringZones = updatedZones
137+
PreferencesSystem.savePreferences?.()
138+
field.updateScoringZones()
139+
}
140+
141+
const assembly = field.mirabufInstance.parser.assembly
142+
const cacheId = field.cacheId
143+
if (cacheId) {
144+
const success = await MirabufCachingService.persistDevtoolChanges(cacheId, MiraType.FIELD, assembly)
145+
if (!success) {
146+
throw new Error("Failed to persist changes to cache")
147+
}
148+
}
149+
} else {
150+
throw new Error("Protected zone field file modification not yet implemented")
151+
}
152+
}
153+
100154
/**
101155
* Gets all zones that exist in the field file for a given type.
102156
*/

0 commit comments

Comments
 (0)