diff --git a/app/src/components/SimulationDashboard/components/element/ElementEditor.tsx b/app/src/components/SimulationDashboard/components/element/ElementEditor.tsx index 0a73603..7de6681 100644 --- a/app/src/components/SimulationDashboard/components/element/ElementEditor.tsx +++ b/app/src/components/SimulationDashboard/components/element/ElementEditor.tsx @@ -617,7 +617,7 @@ const ElementEditor: React.FC = ({ showEyeDropper={field.showEyeDropper} /> ))} - + {/* add delete */} {singleValueFields.map((field, index) => ( = ({ updateDataValue(selectedBlock, selectedElement, newDataValue); }} showEyeDropper={field.showEyeDropper} + showDeleteBtn={true} + onDelete={() => { + const current = Array.isArray(element.dataBinding?.dataValue) ? element.dataBinding!.dataValue : []; + const next = [...current]; + next.splice(index, 1); + updateDataValue(selectedBlock, selectedElement, next); + }} /> ))} @@ -659,7 +666,7 @@ const ElementEditor: React.FC = ({ showEyeDropper={field.showEyeDropper} /> ))} - + {/* add delete */} {multipleSourceFields.map((field, index) => ( = ({ updateDataSource(selectedBlock, selectedElement, next); }} showEyeDropper={field.showEyeDropper} + showDeleteBtn={true} + onDelete={() => { + const current = Array.isArray(element.dataBinding?.dataSource) ? element.dataBinding!.dataSource : []; + const next = [...current]; + next.splice(index, 1); + updateDataSource(selectedBlock, selectedElement, next); + }} /> ))} diff --git a/app/src/components/SimulationDashboard/functions/block/calculateMinBlockSize.ts b/app/src/components/SimulationDashboard/functions/block/calculateMinBlockSize.ts index 4a5cbe0..f0816e9 100644 --- a/app/src/components/SimulationDashboard/functions/block/calculateMinBlockSize.ts +++ b/app/src/components/SimulationDashboard/functions/block/calculateMinBlockSize.ts @@ -4,6 +4,7 @@ export const calculateMinBlockSize = (block: Block): Size => { let minWidth = 100; let minHeight = 50; let stackedHeight = 0; // Track cumulative height for stacked elements + let stackedElementCount = 0; // Track number of stacked elements for gap calculation block.elements.forEach((element) => { if (element.positionType === "absolute") { @@ -17,12 +18,15 @@ export const calculateMinBlockSize = (block: Block): Size => { minWidth = Math.max(minWidth, (element.size?.width || 200) + 40); // Height: sum them up since they stack stackedHeight += element.size?.height || 60; + stackedElementCount++; } }); - // Add padding to stacked height and compare with minHeight from absolute elements + // Add padding to stacked height and gaps between elements if (stackedHeight > 0) { - minHeight = Math.max(minHeight, stackedHeight + 40); + // Add 10px gap between each pair of stacked elements + const gapsBetweenElements = Math.max(0, stackedElementCount - 1) * 5; + minHeight = Math.max(minHeight, stackedHeight + gapsBetweenElements + 40); } return { width: minWidth, height: minHeight }; diff --git a/app/src/components/ui/inputs/DataSourceSelector.tsx b/app/src/components/ui/inputs/DataSourceSelector.tsx index 11557d4..5201d74 100644 --- a/app/src/components/ui/inputs/DataSourceSelector.tsx +++ b/app/src/components/ui/inputs/DataSourceSelector.tsx @@ -1,6 +1,7 @@ import React, { useState } from "react"; import { EyeDroperIcon } from "../../icons/ExportCommonIcons"; import RegularDropDownID from "./RegularDropDownID"; +import { DeleteIcon } from "../../icons/ContextMenuIcons"; type DataSourceSelectorProps = { label?: string; @@ -12,9 +13,11 @@ type DataSourceSelectorProps = { onSelect: (value: { id: string; label: string }) => void; eyeDropperActive?: boolean; // initial state showEyeDropper?: boolean; + showDeleteBtn?: boolean; + onDelete?: () => void; }; -const DataSourceSelector: React.FC = ({ label = "Data Source", options, selected, onSelect, showEyeDropper = true }) => { +const DataSourceSelector: React.FC = ({ label = "Data Source", options, selected, onSelect, showEyeDropper = true, showDeleteBtn, onDelete }) => { // Local state to toggle EyeDropper const [isEyeActive, setIsEyeActive] = useState(false); @@ -31,6 +34,12 @@ const DataSourceSelector: React.FC = ({ label = "Data S )} + + {showDeleteBtn && ( +
onDelete?.()} style={{ cursor: onDelete ? "pointer" : "default" }}> + +
+ )} ); diff --git a/app/src/modules/simulation/analyzer/analyzer.tsx b/app/src/modules/simulation/analyzer/analyzer.tsx index 9cb2273..28a3d09 100644 --- a/app/src/modules/simulation/analyzer/analyzer.tsx +++ b/app/src/modules/simulation/analyzer/analyzer.tsx @@ -193,6 +193,9 @@ function Analyzer() { // Track previous vehicle phases to detect trip completion const previousVehiclePhasesRef = useRef>({}); + // Track previous crane phases to detect cycle completion + const previousCranePhasesRef = useRef>({}); + // Material lifecycle tracking const materialLifecycleRef = useRef< Record< @@ -227,6 +230,7 @@ function Analyzer() { previousArmBotActionsRef.current = {}; previousHumanCountsRef.current = {}; previousVehiclePhasesRef.current = {}; + previousCranePhasesRef.current = {}; materialLifecycleRef.current = {}; setAnalysis(null); setAnalyzing(false); @@ -2487,6 +2491,49 @@ function Analyzer() { }); }, [vehicles, isPlaying]); + // Monitor Crane phase changes to track completed cycles, loads, and lifts + useEffect(() => { + if (!isPlaying) return; + + cranes.forEach((crane) => { + const previousPhase = previousCranePhasesRef.current[crane.modelUuid]; + const currentPhase = crane.currentPhase; + + // Check for transition from 'pickup-drop' to 'init' (Cycle completed) + if (previousPhase === "pickup-drop" && currentPhase === "init") { + // Increment cycles completed + if (!completedActionsRef.current[crane.modelUuid]) { + completedActionsRef.current[crane.modelUuid] = 0; + } + completedActionsRef.current[crane.modelUuid]++; + + // Track loads handled (number of materials carried in this cycle) + const loadsInCycle = crane.currentMaterials?.length || 1; + if (!completedActionsRef.current[`${crane.modelUuid}_loads`]) { + completedActionsRef.current[`${crane.modelUuid}_loads`] = 0; + } + completedActionsRef.current[`${crane.modelUuid}_loads`] += loadsInCycle; + } + + // Track lifts (picking phase indicates a lift operation) + if (previousPhase !== "picking" && currentPhase === "picking") { + if (!completedActionsRef.current[`${crane.modelUuid}_lifts`]) { + completedActionsRef.current[`${crane.modelUuid}_lifts`] = 0; + } + completedActionsRef.current[`${crane.modelUuid}_lifts`]++; + + // Track lift height (assuming a default lift height of 5 meters for now) + // In a real scenario, this would be calculated from crane constraints + if (!completedActionsRef.current[`${crane.modelUuid}_lift_height`]) { + completedActionsRef.current[`${crane.modelUuid}_lift_height`] = 0; + } + completedActionsRef.current[`${crane.modelUuid}_lift_height`] += 5; + } + + previousCranePhasesRef.current[crane.modelUuid] = currentPhase; + }); + }, [cranes, isPlaying]); + return null; } diff --git a/app/src/styles/components/simulationDashboard/_simulationDashBoard.scss b/app/src/styles/components/simulationDashboard/_simulationDashBoard.scss index 3da9341..2012686 100644 --- a/app/src/styles/components/simulationDashboard/_simulationDashBoard.scss +++ b/app/src/styles/components/simulationDashboard/_simulationDashBoard.scss @@ -701,12 +701,17 @@ display: flex; padding: 3px; border-radius: 2px; + cursor: pointer; &.active { background: var(--background-color-button); } } + .delete { + cursor: pointer; + } + .regularDropdown-container { .icon { padding: 0;