diff --git a/package-lock.json b/package-lock.json index c6ef272..1d57458 100644 --- a/package-lock.json +++ b/package-lock.json @@ -74,7 +74,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -686,7 +685,6 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.27.1.tgz", "integrity": "sha512-p9OkPbZ5G7UT1MofwYFigGebnrzGJacoBSQM0/6bi/PUMVE+qlWDD/OalvQKbwgQzU6dl0xAv6r4X7Jme0RYxA==", "license": "MIT", - "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, @@ -1570,7 +1568,6 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.27.1.tgz", "integrity": "sha512-2KH4LWGSrJIkVf5tSiBFYuXDAoWRq2MMwgivCf+93dd0GQi8RXLjKA/0EvRnVV5G0hrHczsquXuD01L8s6dmBw==", "license": "MIT", - "peer": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.1", "@babel/helper-module-imports": "^7.27.1", @@ -4533,7 +4530,8 @@ "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@types/q": { "version": "1.5.8", @@ -4760,7 +4758,6 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz", "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==", "license": "BSD-2-Clause", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "5.62.0", "@typescript-eslint/types": "5.62.0", @@ -5145,7 +5142,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5244,7 +5240,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -6168,7 +6163,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -7241,7 +7235,8 @@ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/damerau-levenshtein": { "version": "1.0.8", @@ -8095,7 +8090,6 @@ "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -10909,7 +10903,6 @@ "resolved": "https://registry.npmjs.org/jest/-/jest-27.5.1.tgz", "integrity": "sha512-Yn0mADZB89zTtjkPJEXwrac3LHudkQMR+Paqa8uxJHCBr9agxztUifWCyiYrjhMPBoUVBjyny0I7XH6ozDr7QQ==", "license": "MIT", - "peer": true, "dependencies": { "@jest/core": "^27.5.1", "import-local": "^3.0.2", @@ -13589,7 +13582,6 @@ "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", "license": "MIT", - "peer": true, "bin": { "jiti": "bin/jiti.js" } @@ -14826,7 +14818,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -14950,7 +14941,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -16097,7 +16087,6 @@ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", "license": "MIT", - "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -16446,7 +16435,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -16597,7 +16585,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -16637,7 +16624,6 @@ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz", "integrity": "sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -17175,7 +17161,6 @@ "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.2.tgz", "integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==", "license": "MIT", - "peer": true, "bin": { "rollup": "dist/bin/rollup" }, @@ -17421,7 +17406,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -19040,7 +19024,6 @@ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", "license": "(MIT OR CC0-1.0)", - "peer": true, "engines": { "node": ">=10" }, @@ -19470,7 +19453,6 @@ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.104.0.tgz", "integrity": "sha512-5DeICTX8BVgNp6afSPYXAFjskIgWGlygQH58bcozPOXgo2r/6xx39Y1+cULZ3gTxUYQP88jmwLj2anu4Xaq84g==", "license": "MIT", - "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", @@ -19542,7 +19524,6 @@ "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.15.2.tgz", "integrity": "sha512-0XavAZbNJ5sDrCbkpWL8mia0o5WPOd2YGtxrEiZkBK9FjLppIUK2TgxK6qGD2P3hUXTJNNPVibrerKcx5WkR1g==", "license": "MIT", - "peer": true, "dependencies": { "@types/bonjour": "^3.5.9", "@types/connect-history-api-fallback": "^1.3.5", @@ -19952,7 +19933,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", diff --git a/red_green_playground.py b/red_green_playground.py index e8b18d9..105aa9c 100644 --- a/red_green_playground.py +++ b/red_green_playground.py @@ -32,6 +32,14 @@ # Flask app initialization app = Flask(__name__, static_folder=os.path.join(build_path, "static")) + +@app.after_request +def add_cors_headers(response): + response.headers["Access-Control-Allow-Origin"] = "*" + response.headers["Access-Control-Allow-Methods"] = "GET, POST, OPTIONS" + response.headers["Access-Control-Allow-Headers"] = "Content-Type, Authorization" + return response + def get_anim(frames, framerate=30, skip_t = 1): """ frames: list of N np.arrays (H x W x 3) diff --git a/src/App.js b/src/App.js index aeaf0f9..a08110f 100644 --- a/src/App.js +++ b/src/App.js @@ -3,6 +3,7 @@ import VideoPlayer from "./components/VideoPlayer"; import NavigationBar from "./components/playground/NavigationBar"; import SimulationSettingsPanel from "./components/playground/SimulationSettingsPanel"; import SceneControlsPanel from "./components/playground/SceneControlsPanel"; +import SelectedEntityPanel from "./components/playground/SelectedEntityPanel"; import TrajectoryScrubPanel from "./components/playground/TrajectoryScrubPanel"; import OcclusionPresetsPanel from "./components/playground/OcclusionPresetsPanel"; import DistractorControlsPanel from "./components/playground/DistractorControlsPanel"; @@ -21,6 +22,7 @@ import { DEFAULT_RANDOM_DISTRACTOR_PARAMS, VID_RES, PX_SCALE, INTERVAL, BORDER_P function App() { const videoPlayerRef = useRef(null); + const handleSimulateRef = useRef(null); // Simulation parameters const [videoLength, setVideoLength] = useState(10); @@ -52,6 +54,7 @@ function App() { // Trajectory scrub state const [scrubEnabled, setScrubEnabled] = useState(false); const [scrubFrame, setScrubFrame] = useState(0); + const [selectedEntityId, setSelectedEntityId] = useState(null); // Use hooks for state management const entitiesHook = useEntities(worldWidth, worldHeight); @@ -68,6 +71,7 @@ function App() { const sceneTransformHook = useSceneTransform(entities, setEntities, worldWidth, worldHeight, movementUnit, setTargetDirection, setDirectionInput); const { moveScene, rotateScene } = sceneTransformHook; + const selectedEntity = entities.find((entity) => entity.id === selectedEntityId) || null; // Keyboard event listener for arrow keys useEffect(() => { @@ -79,12 +83,27 @@ function App() { e.preventDefault(); moveScene(e.key); } + if ((e.key === 'Delete' || e.key === 'Backspace') && selectedEntityId !== null) { + e.preventDefault(); + deleteEntity(selectedEntityId); + setSelectedEntityId(null); + } }; window.addEventListener('keydown', handleKeyDown); return () => { window.removeEventListener('keydown', handleKeyDown); }; - }, [moveScene]); + }, [moveScene, selectedEntityId, deleteEntity]); + + useEffect(() => { + if (selectedEntityId === null) { + return; + } + const selectedStillExists = entities.some((entity) => entity.id === selectedEntityId); + if (!selectedStillExists) { + setSelectedEntityId(null); + } + }, [entities, selectedEntityId]); // Derive effective occluders after applying windows, and validate overlaps against that. const { entitiesForSimulation, occluderPieces } = getEntitiesWithWindowsApplied(entities); @@ -112,6 +131,7 @@ function App() { // Wrapper for clearAllEntities to also clear simData and distractor data const handleClearAll = () => { clearAllEntities(); + setSelectedEntityId(null); setSimData(null); resetDistractorParams(); }; @@ -141,6 +161,30 @@ function App() { setScrubFrame(0); handleSimulateBase(entitiesForSimulation, simulationParams, mode, keyDistractors, randomDistractorParams, autoRun); }; + handleSimulateRef.current = handleSimulate; + + useEffect(() => { + const onKeyDown = (e) => { + const isEnterKey = + e.key === "Enter" || + e.key === "Return" || + e.code === "Enter" || + e.code === "NumpadEnter"; + const hasShortcutModifier = e.metaKey || e.ctrlKey; + + if (!isEnterKey || !hasShortcutModifier || e.isComposing) { + return; + } + if (!(isValidPhysics && overlapValidation.valid)) { + return; + } + e.preventDefault(); + handleSimulateRef.current?.(false); + }; + // Capture phase makes shortcut more reliable when focused controls stop bubbling. + window.addEventListener("keydown", onKeyDown, true); + return () => window.removeEventListener("keydown", onKeyDown, true); + }, [isValidPhysics, overlapValidation.valid]); // File operation handlers const handleFileLoad = createFileLoadHandler({ @@ -206,10 +250,6 @@ function App() { } }; - const handleEntityDrag = (entity, d) => { - updateEntityFromDrag(entity, d); - }; - const handleEntityDragStop = (entity, d) => { updateEntityFromDrag(entity, d); }; @@ -241,10 +281,6 @@ function App() { updateEntity(entity.id, updatedEntity); }; - const handleEntityResize = (entity, ref, position) => { - updateEntityFromResize(entity, ref, position); - }; - const handleEntityResizeStop = (entity, ref, position) => { updateEntityFromResize(entity, ref, position); }; @@ -332,10 +368,42 @@ function App() { const handleCanvasClick = () => { setContextMenu({ visible: false, x: 0, y: 0, entityId: null }); + setSelectedEntityId(null); }; const handleDeleteEntity = (id) => { deleteEntity(id); + if (id === selectedEntityId) { + setSelectedEntityId(null); + } + }; + + const handleSelectedEntityFieldChange = (field, value) => { + if (!selectedEntity) return; + const numericValue = Number(value); + if (Number.isNaN(numericValue)) return; + + if (field === "directionDegrees" && selectedEntity.type === "target") { + handleUpdateTargetDirection(numericValue); + return; + } + + const nextEntity = { ...selectedEntity }; + + if (field === "width" && selectedEntity.type !== "target") { + nextEntity.width = Math.max(INTERVAL, numericValue); + } else if (field === "height" && selectedEntity.type !== "target") { + nextEntity.height = Math.max(INTERVAL, numericValue); + } else if (field === "x" || field === "y") { + nextEntity[field] = numericValue; + } else { + return; + } + + nextEntity.x = Math.max(0, Math.min(nextEntity.x, worldWidth - nextEntity.width)); + nextEntity.y = Math.max(0, Math.min(nextEntity.y, worldHeight - nextEntity.height)); + + updateEntity(selectedEntity.id, nextEntity); }; const handleUpdateTargetDirection = (angleDegrees) => { @@ -428,6 +496,12 @@ function App() { hasEntities={entities.length > 0} /> + + {/* Video Player Section */} diff --git a/src/components/playground/ControlBar.js b/src/components/playground/ControlBar.js index 41105de..f4272f8 100644 --- a/src/components/playground/ControlBar.js +++ b/src/components/playground/ControlBar.js @@ -227,7 +227,11 @@ const ControlBar = ({ e.target.style.boxShadow = "0 2px 4px rgba(0, 0, 0, 0.1)"; } }} - title={overlapWarning || undefined} + title={ + overlapWarning + ? `${overlapWarning} — Keyboard: ⌘+Enter or Ctrl+Enter when scene is valid.` + : "Keyboard: ⌘+Enter (Mac) or Ctrl+Enter (Windows/Linux) to simulate" + } > {isValidPhysics ? "🚀 Simulate" : (overlapWarning ? "⚠️ Overlaps Detected" : "Invalid Physics")} diff --git a/src/components/playground/EntityCanvas.js b/src/components/playground/EntityCanvas.js index 7694e31..fc85c5e 100644 --- a/src/components/playground/EntityCanvas.js +++ b/src/components/playground/EntityCanvas.js @@ -17,7 +17,9 @@ const EntityCanvas = ({ onDeleteEntity, onUpdateTargetDirection, updateEntity, - overlapRegions = [] + overlapRegions = [], + selectedEntityId, + onEntitySelect }) => { const px_scale = PX_SCALE; const border_px = BORDER_PX; @@ -175,10 +177,24 @@ const EntityCanvas = ({ : entity.type === ENTITY_TYPES.OCCLUDER ? "1px dashed rgba(15,23,42,0.6)" : "0px solid black", + outline: selectedEntityId === entity.id ? "2px solid #2563eb" : "none", + outlineOffset: selectedEntityId === entity.id ? "2px" : "0px", cursor: "move", boxShadow: "0 2px 4px rgba(0, 0, 0, 0.1)" }} - onContextMenu={(e) => onEntityContextMenu(e, entity.id)} + onMouseDown={(e) => { + e.stopPropagation(); + onEntitySelect(entity.id); + }} + onClick={(e) => { + e.stopPropagation(); + onEntitySelect(entity.id); + }} + onContextMenu={(e) => { + e.stopPropagation(); + onEntitySelect(entity.id); + onEntityContextMenu(e, entity.id); + }} /> {entity.type === "target" && renderDirectionPreview(entity)} diff --git a/src/components/playground/SelectedEntityPanel.js b/src/components/playground/SelectedEntityPanel.js new file mode 100644 index 0000000..0169389 --- /dev/null +++ b/src/components/playground/SelectedEntityPanel.js @@ -0,0 +1,217 @@ +import React, { useEffect, useState } from 'react'; +import { ENTITY_TYPES } from '../../constants'; + +const inputStyle = { + width: '100%', + padding: '8px 12px', + border: '2px solid #e5e7eb', + borderRadius: '6px', + fontSize: '12px', + outline: 'none', + boxSizing: 'border-box', + backgroundColor: '#ffffff', + color: '#1f2937' +}; + +const labelStyle = { + display: 'block', + fontSize: '12px', + fontWeight: '600', + color: '#374151', + marginBottom: '6px' +}; + +const SelectedEntityPanel = ({ selectedEntity, onChangeField, onDeleteEntity }) => { + const [draftValues, setDraftValues] = useState({ + x: '', + y: '', + width: '', + height: '', + directionDegrees: '' + }); + + useEffect(() => { + if (!selectedEntity) { + setDraftValues({ + x: '', + y: '', + width: '', + height: '', + directionDegrees: '' + }); + return; + } + + setDraftValues({ + x: String(selectedEntity.x), + y: String(selectedEntity.y), + width: String(selectedEntity.width), + height: String(selectedEntity.height), + directionDegrees: ((selectedEntity.direction || 0) * (180 / Math.PI)).toFixed(2) + }); + }, [selectedEntity]); + + const handleInputChange = (field, rawValue) => { + setDraftValues((prev) => ({ + ...prev, + [field]: rawValue + })); + + if (rawValue === '') { + return; + } + + const parsed = Number(rawValue); + if (Number.isNaN(parsed)) { + return; + } + + onChangeField(field, rawValue); + }; + + const handleInputBlur = (field) => { + if (!selectedEntity) return; + if (draftValues[field] !== '') return; + + const resetValue = field === 'directionDegrees' + ? ((selectedEntity.direction || 0) * (180 / Math.PI)).toFixed(2) + : String(selectedEntity[field] ?? ''); + + setDraftValues((prev) => ({ + ...prev, + [field]: resetValue + })); + }; + + return ( +
+

+ Selected Object +

+ + {!selectedEntity ? ( +
+ Click an object on the canvas to edit its numeric parameters. +
+ ) : ( + <> +
+ Type: {selectedEntity.type} +
+ +
+
+ + handleInputChange('x', e.target.value)} + onBlur={() => handleInputBlur('x')} + style={inputStyle} + /> +
+
+ + handleInputChange('y', e.target.value)} + onBlur={() => handleInputBlur('y')} + style={inputStyle} + /> +
+
+ + {selectedEntity.type !== ENTITY_TYPES.TARGET && ( +
+
+ + handleInputChange('width', e.target.value)} + onBlur={() => handleInputBlur('width')} + style={inputStyle} + /> +
+
+ + handleInputChange('height', e.target.value)} + onBlur={() => handleInputBlur('height')} + style={inputStyle} + /> +
+
+ )} + + {selectedEntity.type === ENTITY_TYPES.TARGET && ( +
+ + handleInputChange('directionDegrees', e.target.value)} + onBlur={() => handleInputBlur('directionDegrees')} + style={inputStyle} + /> +
+ )} + + + + )} +
+ ); +}; + +export default SelectedEntityPanel; diff --git a/src/hooks/useSimulation.js b/src/hooks/useSimulation.js index 88eb2dd..dd35572 100644 --- a/src/hooks/useSimulation.js +++ b/src/hooks/useSimulation.js @@ -1,5 +1,6 @@ import { useState, useCallback } from 'react'; import { validateBallPositions } from '../utils/collisionUtils'; +import { postJson, postNoBody } from '../utils/apiUtils'; /** * Hook for simulation management @@ -39,13 +40,7 @@ export const useSimulation = () => { } try { - const response = await fetch('/simulate', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(requestBody), - }); - - const data = await response.json(); + const data = await postJson('/simulate', requestBody); if (data.status === 'success') { setSimData(data.sim_data); } else { @@ -56,16 +51,14 @@ export const useSimulation = () => { } catch (error) { if (!autoRun) { console.error('Error during simulation:', error); + alert(`Simulation failed: ${error.message}`); } } }, []); const clearSimulation = useCallback(async () => { try { - await fetch('/clear_simulation', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - }); + await postNoBody('/clear_simulation'); setSimData(null); } catch (error) { console.error('Error clearing simulation:', error); diff --git a/src/utils/apiUtils.js b/src/utils/apiUtils.js new file mode 100644 index 0000000..9c94d3a --- /dev/null +++ b/src/utils/apiUtils.js @@ -0,0 +1,58 @@ +const runtimeDefaultApiBase = (() => { + if (process.env.REACT_APP_API_BASE_URL) { + return process.env.REACT_APP_API_BASE_URL; + } + + if (typeof window !== 'undefined' && window.location.hostname === 'localhost' && window.location.port !== '5001') { + return 'http://localhost:5001'; + } + + return ''; +})(); + +const buildUrl = (path) => `${runtimeDefaultApiBase}${path}`; + +const readResponseErrorText = async (response) => { + const text = await response.text(); + if (!text) { + return `Request failed with status ${response.status}`; + } + + if (text.includes('')) { + return 'Backend API endpoint not found. Please run the Flask backend (uv run python red_green_playground.py).'; + } + + return text; +}; + +export const postJson = async (path, body) => { + const response = await fetch(buildUrl(path), { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body ?? {}) + }); + + if (!response.ok) { + throw new Error(await readResponseErrorText(response)); + } + + try { + return await response.json(); + } catch (error) { + throw new Error('Received an invalid JSON response from backend.'); + } +}; + +export const postNoBody = async (path) => { + const response = await fetch(buildUrl(path), { + method: 'POST', + headers: { 'Content-Type': 'application/json' } + }); + + if (!response.ok) { + throw new Error(await readResponseErrorText(response)); + } + + return response; +}; + diff --git a/src/utils/fileUtils.js b/src/utils/fileUtils.js index 8cb016a..0403b53 100644 --- a/src/utils/fileUtils.js +++ b/src/utils/fileUtils.js @@ -116,14 +116,8 @@ export const createFileLoadHandler = ({ const file = event.target.files[0]; if (file) { try { - const clearResponse = await fetch("/clear_simulation", { - method: "POST", - headers: { "Content-Type": "application/json" }, - }); - if (!clearResponse.ok) { - alert(`Failed to clear simulation: ${await clearResponse.text()}`); - return; - } + // Clear client-side simulation state before loading new scene data. + // Scene loading should not hard-fail if backend clear endpoint is unavailable. setSimData(null); const reader = new FileReader(); reader.onload = (e) => { @@ -165,8 +159,8 @@ export const createFileLoadHandler = ({ }; reader.readAsText(file); } catch (err) { - console.error("Error clearing simulation:", err); - alert("An unexpected error occurred while clearing the simulation."); + console.error("Error loading file:", err); + alert("An unexpected error occurred while loading the scene file."); } } };