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}
/>
+