Merge pull request 'ui/scene: add reload button' (#4962) from fixes-ui into main

Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4962
This commit is contained in:
hsjobeki
2025-08-26 09:01:45 +00:00
5 changed files with 94 additions and 63 deletions

View File

@@ -121,7 +121,6 @@ export const useMachineStateQuery = (clanURI: string, machineName: string) => {
const client = useApiClient();
return useQuery<MachineState>(() => ({
queryKey: ["clans", encodeBase64(clanURI), "machine", machineName, "state"],
refetchInterval: 1000 * 60, // poll every 60 seconds
queryFn: async () => {
const apiCall = client.fetch("get_machine_state", {
machine: {

View File

@@ -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 };
}
}),
);

View File

@@ -25,50 +25,71 @@ export class MachineManager {
machinePositionsSignal: Accessor<SceneData>,
machinesQueryResult: MachinesQueryResult,
selectedIds: Accessor<Set<string>>,
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<string> = 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;

View File

@@ -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();
}
}

View File

@@ -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<Set<string>>;
onSelect: (v: Set<string>) => void;
sceneStore: Accessor<SceneData>;
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 (
<>
<div class="cubes-scene-container" ref={(el) => (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();
}}
/>
{/* <ToolbarButton
description="Delete Machine"
name="delete"
icon="Trash"
/> */}
<ToolbarButton
icon="Reload"
name="Reload"
description="Reload machines"
onClick={() => machinesQuery.refetch()}
/>
</Toolbar>
</div>
</>