feat: Enhance ArmBotMechanics with connected models and process management; update ConveyorMechanics for radio input behavior; adjust NavMeshDetails cell size; add depthWrite to ZoneGroup material; include triggers in copy and duplication controls; refine path connector actions filtering; improve socket store zone management

This commit is contained in:
Jerald-Golden-B 2025-04-09 15:38:29 +05:30
parent d4e0358f4b
commit f7a0f3b3d6
8 changed files with 333 additions and 96 deletions
app/src
components/layout/sidebarRight/mechanics
modules
store

View File

@ -1,32 +1,128 @@
import React, { useRef, useMemo, useCallback, useState } from "react"; import React, { useRef, useMemo, useCallback, useState } from "react";
import { InfoIcon } from "../../../icons/ExportCommonIcons"; import { InfoIcon, AddIcon, RemoveIcon, ResizeHeightIcon } from "../../../icons/ExportCommonIcons";
import InputWithDropDown from "../../../ui/inputs/InputWithDropDown"; import InputWithDropDown from "../../../ui/inputs/InputWithDropDown";
import { import { useSelectedActionSphere, useSimulationStates, useSocketStore } from "../../../../store/store";
useSelectedActionSphere,
useSimulationStates,
useSocketStore
} from "../../../../store/store";
import * as Types from '../../../../types/world/worldTypes'; import * as Types from '../../../../types/world/worldTypes';
import LabeledButton from "../../../ui/inputs/LabledButton";
import LabledDropdown from "../../../ui/inputs/LabledDropdown"; import LabledDropdown from "../../../ui/inputs/LabledDropdown";
import { handleResize } from "../../../../functions/handleResizePannel";
interface ConnectedModel {
modelUUID: string;
modelName: string;
points: {
uuid: string;
position: [number, number, number];
index?: number;
}[];
triggers?: {
uuid: string;
name: string;
type: string;
isUsed: boolean;
}[];
}
const ArmBotMechanics: React.FC = () => { const ArmBotMechanics: React.FC = () => {
const { selectedActionSphere } = useSelectedActionSphere(); const { selectedActionSphere } = useSelectedActionSphere();
const { simulationStates, setSimulationStates } = useSimulationStates(); const { simulationStates, setSimulationStates } = useSimulationStates();
const { socket } = useSocketStore(); const { socket } = useSocketStore();
const [selectedTrigger, setSelectedTrigger] = useState<string | null>(null); const [selectedProcessIndex, setSelectedProcessIndex] = useState<number | null>(null);
const actionsContainerRef = useRef<HTMLDivElement>(null);
const propertiesContainerRef = useRef<HTMLDivElement>(null); // Get connected models and their triggers
const connectedModels = useMemo<ConnectedModel[]>(() => {
// Get connected models for dropdowns
const connectedModels = useMemo(() => {
if (!selectedActionSphere?.points?.uuid) return []; if (!selectedActionSphere?.points?.uuid) return [];
const armBotPaths = simulationStates.filter(
(path): path is Types.ArmBotEventsSchema => path.type === "ArmBot"
);
const currentPoint = armBotPaths.find(
(path) => path.points.uuid === selectedActionSphere.points.uuid
)?.points;
if (!currentPoint?.connections?.targets) return [];
return currentPoint.connections.targets.reduce<ConnectedModel[]>((acc, target) => {
const connectedModel = simulationStates.find(
(model) => model.modeluuid === target.modelUUID
);
if (!connectedModel) return acc;
let triggers: { uuid: string; name: string; type: string; isUsed: boolean }[] = [];
let points: { uuid: string; position: [number, number, number] }[] = [];
if (connectedModel.type === "Conveyor") {
const conveyor = connectedModel as Types.ConveyorEventsSchema;
const connectedPointUUIDs = currentPoint?.connections?.targets
.filter(t => t.modelUUID === connectedModel.modeluuid)
.map(t => t.pointUUID) || [];
points = conveyor.points
.map((point, idx) => ({
uuid: point.uuid,
position: point.position,
index: idx
}))
.filter(point => connectedPointUUIDs.includes(point.uuid));
triggers = conveyor.points.flatMap(p => p.triggers?.filter(t => t.isUsed) || []);
}
else if (connectedModel.type === "StaticMachine") {
const staticMachine = connectedModel as Types.StaticMachineEventsSchema;
points = [{
uuid: staticMachine.points.uuid,
position: staticMachine.points.position
}];
triggers = staticMachine.points.triggers ?
[{
uuid: staticMachine.points.triggers.uuid,
name: staticMachine.points.triggers.name,
type: staticMachine.points.triggers.type,
isUsed: true // StaticMachine triggers are always considered used
}] : [];
}
if (!acc.some(m => m.modelUUID === connectedModel.modeluuid)) {
acc.push({
modelUUID: connectedModel.modeluuid,
modelName: connectedModel.modelName,
points,
triggers
});
}
return acc;
}, []);
}, [selectedActionSphere, simulationStates]); }, [selectedActionSphere, simulationStates]);
// Get triggers only from connected models // Get triggers from connected models
const connectedTriggers = useMemo(() => { const connectedTriggers = useMemo(() => {
}, [connectedModels, simulationStates]); return connectedModels.flatMap(model =>
(model.triggers || []).map(trigger => ({
...trigger,
displayName: `${model.modelName} - ${trigger.name}`,
modelUUID: model.modelUUID
}))
);
}, [connectedModels]);
// Get all points from connected models
const connectedPoints = useMemo(() => {
return connectedModels.flatMap(model =>
model.points.map(point => ({
...point,
displayName: `${model.modelName} - Point${typeof point.index === 'number' ? ` ${point.index}` : ''}`,
modelUUID: model.modelUUID
}))
);
}, [connectedModels]);
const { selectedPoint } = useMemo(() => { const { selectedPoint } = useMemo(() => {
if (!selectedActionSphere?.points?.uuid) return { selectedPoint: null }; if (!selectedActionSphere?.points?.uuid) return { selectedPoint: null };
@ -45,77 +141,148 @@ const ArmBotMechanics: React.FC = () => {
}, [selectedActionSphere, simulationStates]); }, [selectedActionSphere, simulationStates]);
const updateBackend = async (updatedPath: Types.ArmBotEventsSchema | undefined) => { const updateBackend = async (updatedPath: Types.ArmBotEventsSchema | undefined) => {
// if (!updatedPath) return; if (!updatedPath) return;
// const email = localStorage.getItem("email"); const email = localStorage.getItem("email");
// const organization = email ? email.split("@")[1].split(".")[0] : ""; const organization = email ? email.split("@")[1].split(".")[0] : "";
// const data = { const data = {
// organization: organization, organization: organization,
// modeluuid: updatedPath.modeluuid, modeluuid: updatedPath.modeluuid,
// eventData: { type: "ArmBot", points: updatedPath.points } eventData: { type: "ArmBot", points: updatedPath.points }
// } }
console.log('data: ', data);
// socket.emit('v2:model-asset:updateEventData', data); socket.emit('v2:model-asset:updateEventData', data);
} }
// const handleActionUpdate = useCallback((updatedAction: Partial<Types.ArmBotEventsSchema['points']['actions']>) => { const handleActionUpdate = useCallback((updatedAction: Partial<Types.ArmBotEventsSchema['points']['actions']>) => {
// if (!selectedActionSphere?.points?.uuid) return; if (!selectedActionSphere?.points?.uuid || !selectedPoint) return;
// const updatedPaths = simulationStates.map((path) => { const updatedPaths = simulationStates.map((path) => {
// return path; if (path.type === "ArmBot" && path.points.uuid === selectedActionSphere.points.uuid) {
// }); return {
...path,
points: {
...path.points,
actions: {
...path.points.actions,
...updatedAction
}
}
};
}
return path;
});
// const updatedPath = updatedPaths.find( const updatedPath = updatedPaths.find(
// (path): path is Types.ArmBotEventsSchema => (path): path is Types.ArmBotEventsSchema =>
// path.type === "ArmBot" && path.type === "ArmBot" &&
// path.points.uuid === selectedActionSphere.points.uuid path.points.uuid === selectedActionSphere.points.uuid
// ); );
// updateBackend(updatedPath); updateBackend(updatedPath);
// setSimulationStates(updatedPaths); setSimulationStates(updatedPaths);
// }, [selectedActionSphere?.points?.uuid, simulationStates, setSimulationStates]); }, [selectedActionSphere?.points?.uuid, selectedPoint, simulationStates, setSimulationStates]);
// const handleSpeedChange = useCallback((speed: number) => { const handleSpeedChange = useCallback((speed: number) => {
// handleActionUpdate({ speed }); handleActionUpdate({ speed });
// }, [handleActionUpdate]); }, [handleActionUpdate]);
// const handleProcessChange = useCallback((processes: Types.ArmBotEventsSchema['points']['actions']['processes']) => { const handleProcessChange = useCallback((processes: Types.ArmBotEventsSchema['points']['actions']['processes']) => {
// handleActionUpdate({ processes }); handleActionUpdate({ processes });
// }, [handleActionUpdate]); }, [handleActionUpdate]);
// const handleTriggerSelect = useCallback((displayName: string) => { const handleAddProcess = useCallback(() => {
// const selected = connectedTriggers.find(t => t.displayName === displayName); if (!selectedPoint) return;
// setSelectedTrigger(selected?.uuid || null);
// }, [connectedTriggers]);
// const handleStartPointSelect = useCallback((pointUUID: string) => { const newProcess: any = {
// if (!selectedTrigger || !selectedPoint) return; triggerId: "",
startPoint: "",
endPoint: ""
};
// const updatedProcesses = selectedPoint.actions.processes?.map(process => const updatedProcesses = selectedPoint.actions.processes ? [...selectedPoint.actions.processes, newProcess] : [newProcess];
// process.triggerId === selectedTrigger
// ? { ...process, startPoint: pointUUID }
// : process
// ) || [];
// handleProcessChange(updatedProcesses); handleProcessChange(updatedProcesses);
// }, [selectedTrigger, selectedPoint, handleProcessChange]); setSelectedProcessIndex(updatedProcesses.length - 1);
}, [selectedPoint, handleProcessChange]);
// const handleEndPointSelect = useCallback((pointUUID: string) => { const handleDeleteProcess = useCallback((index: number) => {
// if (!selectedTrigger || !selectedPoint) return; if (!selectedPoint?.actions.processes) return;
// const updatedProcesses = selectedPoint.actions.processes?.map(process => const updatedProcesses = [...selectedPoint.actions.processes];
// process.triggerId === selectedTrigger updatedProcesses.splice(index, 1);
// ? { ...process, endPoint: pointUUID }
// : process
// ) || [];
// handleProcessChange(updatedProcesses); handleProcessChange(updatedProcesses);
// }, [selectedTrigger, selectedPoint, handleProcessChange]);
// const getCurrentProcess = useCallback(() => { // Reset selection if deleting the currently selected process
// if (!selectedTrigger || !selectedPoint) return null; if (selectedProcessIndex === index) {
// return selectedPoint.actions.processes?.find(p => p.triggerId === selectedTrigger); setSelectedProcessIndex(null);
// }, [selectedTrigger, selectedPoint]); } else if (selectedProcessIndex !== null && selectedProcessIndex > index) {
// Adjust selection index if needed
setSelectedProcessIndex(selectedProcessIndex - 1);
}
}, [selectedPoint, selectedProcessIndex, handleProcessChange]);
const handleTriggerSelect = useCallback((displayName: string, index: number) => {
const selected = connectedTriggers.find(t => t.displayName === displayName);
if (!selected || !selectedPoint?.actions.processes) return;
const oldProcess = selectedPoint.actions.processes[index];
const updatedProcesses = [...selectedPoint.actions.processes];
// Only reset start/end if new trigger invalidates them (your logic can expand this)
updatedProcesses[index] = {
...oldProcess,
triggerId: selected.uuid,
startPoint: oldProcess.startPoint || "", // preserve if exists
endPoint: oldProcess.endPoint || "" // preserve if exists
};
handleProcessChange(updatedProcesses);
}, [connectedTriggers, selectedPoint, handleProcessChange]);
const handleStartPointSelect = useCallback((displayName: string, index: number) => {
if (!selectedPoint?.actions.processes) return;
const point = connectedPoints.find(p => p.displayName === displayName);
if (!point) return;
const updatedProcesses = [...selectedPoint.actions.processes];
updatedProcesses[index] = {
...updatedProcesses[index],
startPoint: point.uuid
};
handleProcessChange(updatedProcesses);
}, [selectedPoint, connectedPoints, handleProcessChange]);
const handleEndPointSelect = useCallback((displayName: string, index: number) => {
if (!selectedPoint?.actions.processes) return;
const point = connectedPoints.find(p => p.displayName === displayName);
if (!point) return;
const updatedProcesses = [...selectedPoint.actions.processes];
updatedProcesses[index] = {
...updatedProcesses[index],
endPoint: point.uuid
};
handleProcessChange(updatedProcesses);
}, [selectedPoint, connectedPoints, handleProcessChange]);
const getProcessByIndex = useCallback((index: number) => {
if (!selectedPoint?.actions.processes || index >= selectedPoint.actions.processes.length) return null;
return selectedPoint.actions.processes[index];
}, [selectedPoint]);
const getFilteredTriggerOptions = (currentIndex: number) => {
const usedTriggerUUIDs = selectedPoint?.actions.processes?.filter((_, i) => i !== currentIndex).map(p => p.triggerId).filter(Boolean) ?? [];
return connectedTriggers.filter(trigger => !usedTriggerUUIDs.includes(trigger.uuid)).map(trigger => trigger.displayName);
};
return ( return (
<div className="machine-mechanics-container" key={selectedPoint?.uuid}> <div className="machine-mechanics-container" key={selectedPoint?.uuid}>
@ -124,10 +291,10 @@ const ArmBotMechanics: React.FC = () => {
</div> </div>
<div className="machine-mechanics-content-container"> <div className="machine-mechanics-content-container">
<div className="selected-properties-container" ref={propertiesContainerRef}> <div className="selected-properties-container">
<div className="properties-header">ArmBot Properties</div> <div className="properties-header">ArmBot Properties</div>
{/* {selectedPoint && ( {selectedPoint && (
<> <>
<InputWithDropDown <InputWithDropDown
key={`speed-${selectedPoint.uuid}`} key={`speed-${selectedPoint.uuid}`}
@ -136,36 +303,90 @@ const ArmBotMechanics: React.FC = () => {
onChange={(value) => handleSpeedChange(parseInt(value))} onChange={(value) => handleSpeedChange(parseInt(value))}
/> />
<LabledDropdown <div className="actions">
key={`trigger-select-${selectedPoint.uuid}`} <div className="header">
label="Select Trigger" <div className="header-value">Processes</div>
defaultOption={connectedTriggers.find(t => t.uuid === selectedTrigger)?.displayName || ''} <div className="add-button" onClick={handleAddProcess}>
onSelect={handleTriggerSelect} <AddIcon /> Add
options={connectedTriggers.map(trigger => trigger.displayName)} </div>
/> </div>
<div
className="lists-main-container"
ref={actionsContainerRef}
style={{ height: "120px" }}
>
<div className="list-container">
{selectedPoint.actions.processes?.map((process, index) => (
<div
key={`process-${index}`}
className={`list-item ${selectedProcessIndex === index ? "active" : ""}`}
>
<div
className="value"
onClick={() => setSelectedProcessIndex(index)}
>
Process {index + 1}
</div>
<div
className="remove-button"
onClick={() => handleDeleteProcess(index)}
>
<RemoveIcon />
</div>
</div>
))}
</div>
<div
className="resize-icon"
id="action-resize"
onMouseDown={(e) => handleResize(e, actionsContainerRef)}
>
<ResizeHeightIcon />
</div>
</div>
</div>
{selectedTrigger && ( {selectedProcessIndex !== null && (
<> <div className="process-configuration">
<LabledDropdown <LabledDropdown
key={`start-point-${selectedTrigger}`} key={`trigger-select-${selectedProcessIndex}`}
label="Select Trigger"
defaultOption={
connectedTriggers.find(t =>
t.uuid === getProcessByIndex(selectedProcessIndex)?.triggerId
)?.displayName || 'Select a trigger'
}
onSelect={(value) => handleTriggerSelect(value, selectedProcessIndex)}
options={getFilteredTriggerOptions(selectedProcessIndex)}
/>
<LabledDropdown
key={`start-point-${selectedProcessIndex}`}
label="Start Point" label="Start Point"
defaultOption={getCurrentProcess()?.startPoint || ''} defaultOption={
onSelect={handleStartPointSelect} connectedPoints.find(p =>
options={connectedModels.map((model, index) => `${model.modelName} [${index}]`)} p.uuid === getProcessByIndex(selectedProcessIndex)?.startPoint
)?.displayName || 'Select start point'
}
onSelect={(value) => handleStartPointSelect(value, selectedProcessIndex)}
options={connectedPoints.map(point => point.displayName)}
/> />
<LabledDropdown <LabledDropdown
key={`end-point-${selectedTrigger}`} key={`end-point-${selectedProcessIndex}`}
label="End Point" label="End Point"
defaultOption={getCurrentProcess()?.endPoint || ''} defaultOption={
onSelect={handleEndPointSelect} connectedPoints.find(p =>
options={connectedModels.map((model, index) => `${model.modelName} [${index}]`)} p.uuid === getProcessByIndex(selectedProcessIndex)?.endPoint
)?.displayName || 'Select end point'
}
onSelect={(value) => handleEndPointSelect(value, selectedProcessIndex)}
options={connectedPoints.map(point => point.displayName)}
/> />
</div>
</>
)} )}
</> </>
)} */} )}
</div> </div>
<div className="footer"> <div className="footer">

View File

@ -647,7 +647,7 @@ const ConveyorMechanics: React.FC = () => {
setSelectedItem({ type: "action", item: action }) setSelectedItem({ type: "action", item: action })
} }
> >
<input type="radio" name="action" id="action" defaultChecked={action.isUsed} /> <input type="radio" name="action" id="action" checked={action.isUsed} readOnly />
<RenameInput value={action.name} /> <RenameInput value={action.name} />
</div> </div>
<div <div
@ -696,7 +696,7 @@ const ConveyorMechanics: React.FC = () => {
setSelectedItem({ type: "trigger", item: trigger }) setSelectedItem({ type: "trigger", item: trigger })
} }
> >
<input type="radio" name="trigger" id="trigger" defaultChecked={trigger.isUsed} /> <input type="radio" name="trigger" id="trigger" checked={trigger.isUsed} readOnly />
<RenameInput value={trigger.name} /> <RenameInput value={trigger.name} />
</div> </div>
<div <div

View File

@ -32,7 +32,7 @@ export default function NavMeshDetails({
const [positions, indices] = getPositionsAndIndices(meshes); const [positions, indices] = getPositionsAndIndices(meshes);
const cellSize = 0.35; const cellSize = 0.2;
const cellHeight = 0.7; const cellHeight = 0.7;
const walkableRadius = 0.5; const walkableRadius = 0.5;
const { success, navMesh } = generateSoloNavMesh(positions, indices, { const { success, navMesh } = generateSoloNavMesh(positions, indices, {

View File

@ -50,6 +50,7 @@ const ZoneGroup: React.FC = () => {
uColor: { value: new THREE.Color(CONSTANTS.zoneConfig.color) }, uColor: { value: new THREE.Color(CONSTANTS.zoneConfig.color) },
}, },
transparent: true, transparent: true,
depthWrite: false
}), []); }), []);
useEffect(() => { useEffect(() => {

View File

@ -345,6 +345,7 @@ const CopyPasteControls = ({ itemsGroupRef, copiedObjects, setCopiedObjects, pas
uuid: THREE.MathUtils.generateUUID() uuid: THREE.MathUtils.generateUUID()
} }
: defaultAction, : defaultAction,
triggers: { uuid: THREE.MathUtils.generateUUID(), name: 'Trigger 1', type: 'OnComplete' },
connections: { connections: {
source: { modelUUID: obj.uuid, pointUUID }, source: { modelUUID: obj.uuid, pointUUID },
targets: [] targets: []

View File

@ -327,6 +327,7 @@ const DuplicationControls = ({ itemsGroupRef, duplicatedObjects, setDuplicatedOb
uuid: THREE.MathUtils.generateUUID() uuid: THREE.MathUtils.generateUUID()
} }
: defaultAction, : defaultAction,
triggers: {uuid: THREE.MathUtils.generateUUID(), name: 'Trigger 1', type: 'OnComplete'},
connections: { connections: {
source: { modelUUID: obj.uuid, pointUUID }, source: { modelUUID: obj.uuid, pointUUID },
targets: [] targets: []

View File

@ -952,8 +952,17 @@ function PathConnector({ pathsGroupRef }: { pathsGroupRef: React.MutableRefObjec
); );
}) })
}, },
// Ensure all required ArmBot point properties are included actions: {
actions: state.points.actions, ...state.points.actions,
processes: state.points.actions.processes?.filter(process => {
return !(
process.startPoint === connection1.point ||
process.endPoint === connection1.point ||
process.startPoint === connection2.point ||
process.endPoint === connection2.point
);
}) || []
},
triggers: state.points.triggers triggers: state.points.triggers
} }
}; };

View File

@ -68,7 +68,11 @@ export const useWalls = create<any>((set: any) => ({
export const useZones = create<any>((set: any) => ({ export const useZones = create<any>((set: any) => ({
zones: [], zones: [],
setZones: (x: any) => set(() => ({ zones: x })), setZones: (callback: any) =>
set((state: any) => ({
zones:
typeof callback === 'function' ? callback(state.zones) : callback
}))
})); }));
interface ZonePointsState { interface ZonePointsState {