diff --git a/pkgs/clan-app/ui/src/hooks/queries.ts b/pkgs/clan-app/ui/src/hooks/queries.ts index 1bcc135b7..f84b0724e 100644 --- a/pkgs/clan-app/ui/src/hooks/queries.ts +++ b/pkgs/clan-app/ui/src/hooks/queries.ts @@ -121,7 +121,6 @@ export const useMachineStateQuery = (clanURI: string, machineName: string) => { const client = useApiClient(); return useQuery(() => ({ queryKey: ["clans", encodeBase64(clanURI), "machine", machineName, "state"], - refetchInterval: 1000 * 60, // poll every 60 seconds queryFn: async () => { const apiCall = client.fetch("get_machine_state", { machine: { diff --git a/pkgs/clan-app/ui/src/routes/Clan/Clan.tsx b/pkgs/clan-app/ui/src/routes/Clan/Clan.tsx index 5256dfa54..10116caf8 100644 --- a/pkgs/clan-app/ui/src/routes/Clan/Clan.tsx +++ b/pkgs/clan-app/ui/src/routes/Clan/Clan.tsx @@ -302,21 +302,26 @@ const ClanSceneController = (props: RouteSectionProps) => { isLoading={ctx.isLoading()} cubesQuery={ctx.machinesQuery} onCreate={onCreate} + clanURI={ctx.clanURI} sceneStore={() => store.sceneData?.[ctx.clanURI]} - setMachinePos={(machineId: string, pos: [number, number]) => { + setMachinePos={(machineId: string, pos: [number, number] | null) => { console.log("calling setStore", machineId, pos); setStore( produce((s) => { - if (!s.sceneData) { - s.sceneData = {}; - } - if (!s.sceneData[ctx.clanURI]) { - s.sceneData[ctx.clanURI] = {}; - } - if (!s.sceneData[ctx.clanURI][machineId]) { - s.sceneData[ctx.clanURI][machineId] = { position: pos }; + if (!s.sceneData) s.sceneData = {}; + + if (!s.sceneData[ctx.clanURI]) s.sceneData[ctx.clanURI] = {}; + + if (pos === null) { + // Remove the machine entry if pos is null + Reflect.deleteProperty(s.sceneData[ctx.clanURI], machineId); + + if (Object.keys(s.sceneData[ctx.clanURI]).length === 0) { + Reflect.deleteProperty(s.sceneData, ctx.clanURI); + } } else { - s.sceneData[ctx.clanURI][machineId].position = pos; + // Set or update the machine position + s.sceneData[ctx.clanURI][machineId] = { position: pos }; } }), ); diff --git a/pkgs/clan-app/ui/src/scene/MachineManager.ts b/pkgs/clan-app/ui/src/scene/MachineManager.ts index 49d890841..9b13e7503 100644 --- a/pkgs/clan-app/ui/src/scene/MachineManager.ts +++ b/pkgs/clan-app/ui/src/scene/MachineManager.ts @@ -25,50 +25,71 @@ export class MachineManager { machinePositionsSignal: Accessor, machinesQueryResult: MachinesQueryResult, selectedIds: Accessor>, - setMachinePos: (id: string, position: [number, number]) => void, + setMachinePos: (id: string, position: [number, number] | null) => void, ) { this.machinePositionsSignal = machinePositionsSignal; this.disposeRoot = createRoot((disposeEffects) => { - createEffect(() => { - const machines = machinePositionsSignal(); - - Object.entries(machines).forEach(([id, data]) => { - const machineRepr = new MachineRepr( - scene, - registry, - new THREE.Vector2(data.position[0], data.position[1]), - id, - selectedIds, - ); - this.machines.set(id, machineRepr); - scene.add(machineRepr.group); - }); - renderLoop.requestRender(); - }); - - // Push positions of previously existing machines to the scene - // TODO: Maybe we should do this in some post query hook? + // + // Effect 1: sync query → store (positions) + // createEffect(() => { if (!machinesQueryResult.data) return; - const actualMachines = Object.keys(machinesQueryResult.data); + const actualIds = Object.keys(machinesQueryResult.data); const machinePositions = machinePositionsSignal(); - const placed: Set = machinePositions - ? new Set(Object.keys(machinePositions)) - : new Set(); - const nonPlaced = actualMachines.filter((m) => !placed.has(m)); - - // Push not explizitly placed machines to the scene - // TODO: Make the user place them manually - // We just calculate some next free position - for (const id of nonPlaced) { - console.log("adding", id); - const position = this.nextGridPos(); - - setMachinePos(id, position); + // Remove stale + for (const id of Object.keys(machinePositions)) { + if (!actualIds.includes(id)) { + setMachinePos(id, null); + } } + + // Add missing + for (const id of actualIds) { + if (!machinePositions[id]) { + const pos = this.nextGridPos(); + setMachinePos(id, pos); + } + } + }); + + // + // Effect 2: sync store → scene + // + createEffect(() => { + const positions = machinePositionsSignal(); + + // Remove machines from scene + for (const [id, repr] of this.machines) { + if (!(id in positions)) { + repr.dispose(scene); + this.machines.delete(id); + } + } + + // Add or update machines + for (const [id, data] of Object.entries(positions)) { + let repr = this.machines.get(id); + if (!repr) { + repr = new MachineRepr( + scene, + registry, + new THREE.Vector2(data.position[0], data.position[1]), + id, + selectedIds, + ); + this.machines.set(id, repr); + scene.add(repr.group); + } else { + repr.setPosition( + new THREE.Vector2(data.position[0], data.position[1]), + ); + } + } + + renderLoop.requestRender(); }); return disposeEffects; diff --git a/pkgs/clan-app/ui/src/scene/MachineRepr.ts b/pkgs/clan-app/ui/src/scene/MachineRepr.ts index d0d1ed713..cbebdcc53 100644 --- a/pkgs/clan-app/ui/src/scene/MachineRepr.ts +++ b/pkgs/clan-app/ui/src/scene/MachineRepr.ts @@ -121,6 +121,11 @@ export class MachineRepr { }); } + public setPosition(position: THREE.Vector2) { + this.group.position.set(position.x, 0, position.y); + renderLoop.requestRender(); + } + private createCubeBase( color: THREE.ColorRepresentation, emissive: THREE.ColorRepresentation, @@ -154,6 +159,14 @@ export class MachineRepr { this.geometry.dispose(); this.material.dispose(); + for (const child of this.cubeMesh.children) { + if (child instanceof THREE.Mesh) + (child.material as THREE.Material).dispose(); + + if (child instanceof CSS2DObject) child.element.remove(); + + if (child instanceof THREE.Object3D) child.remove(); + } (this.baseMesh.material as THREE.Material).dispose(); } } diff --git a/pkgs/clan-app/ui/src/scene/cubes.tsx b/pkgs/clan-app/ui/src/scene/cubes.tsx index c6e8618ae..5e215dab4 100644 --- a/pkgs/clan-app/ui/src/scene/cubes.tsx +++ b/pkgs/clan-app/ui/src/scene/cubes.tsx @@ -8,7 +8,7 @@ import { CSS2DRenderer } from "three/examples/jsm/renderers/CSS2DRenderer.js"; import { Toolbar } from "../components/Toolbar/Toolbar"; import { ToolbarButton } from "../components/Toolbar/ToolbarButton"; import { Divider } from "../components/Divider/Divider"; -import { MachinesQueryResult } from "../hooks/queries"; +import { MachinesQueryResult, useMachinesQuery } from "../hooks/queries"; import { SceneData } from "../stores/clan"; import { Accessor } from "solid-js"; import { renderLoop } from "./RenderLoop"; @@ -37,8 +37,9 @@ export function CubeScene(props: { selectedIds: Accessor>; onSelect: (v: Set) => void; sceneStore: Accessor; - setMachinePos: (machineId: string, pos: [number, number]) => void; + setMachinePos: (machineId: string, pos: [number, number] | null) => void; isLoading: boolean; + clanURI: string; }) { let container: HTMLDivElement; let scene: THREE.Scene; @@ -524,6 +525,8 @@ export function CubeScene(props: { } }; + const machinesQuery = useMachinesQuery(props.clanURI); + return ( <>
(container = el)} /> @@ -549,23 +552,13 @@ export function CubeScene(props: { description="Add new Service" name="modules" icon="Modules" - onClick={() => { - if (positionMode() === "grid") { - setPositionMode("circle"); - setWorldMode("view"); - grid.visible = false; - } else { - setPositionMode("grid"); - grid.visible = true; - } - renderLoop.requestRender(); - }} /> - {/* */} + machinesQuery.refetch()} + />