Merge pull request 'ui/fix: some more fixes' (#5063) from ui-more-3 into main

Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/5063
This commit is contained in:
hsjobeki
2025-09-02 12:12:25 +00:00
4 changed files with 84 additions and 55 deletions

View File

@@ -37,6 +37,11 @@ export const Menu = (props: {
"pointer-events": "auto", "pointer-events": "auto",
}} }}
class={styles.list} class={styles.list}
onContextMenu={(e) => {
// Prevent default context menu
e.preventDefault();
e.stopPropagation();
}}
> >
<li <li
class={styles.item} class={styles.item}

View File

@@ -132,8 +132,6 @@ const ClanSceneController = (props: RouteSectionProps) => {
const navigate = useNavigate(); const navigate = useNavigate();
const [showService, setShowService] = createSignal(false);
const [currentPromise, setCurrentPromise] = createSignal<{ const [currentPromise, setCurrentPromise] = createSignal<{
resolve: ({ id }: { id: string }) => void; resolve: ({ id }: { id: string }) => void;
reject: (err: unknown) => void; reject: (err: unknown) => void;
@@ -219,21 +217,9 @@ const ClanSceneController = (props: RouteSectionProps) => {
console.error("Error creating service instance", result.errors); console.error("Error creating service instance", result.errors);
} }
toast.success("Created"); toast.success("Created");
setShowService(false);
setWorldMode("select"); setWorldMode("select");
}; };
createEffect(
on(worldMode, (mode) => {
if (mode === "service") {
setShowService(true);
} else {
// TODO: request soft close instead of forced close
setShowService(false);
}
}),
);
return ( return (
<> <>
<Show when={loadingError()}> <Show when={loadingError()}>
@@ -268,11 +254,10 @@ const ClanSceneController = (props: RouteSectionProps) => {
isLoading={ctx.isLoading()} isLoading={ctx.isLoading()}
cubesQuery={ctx.machinesQuery} cubesQuery={ctx.machinesQuery}
toolbarPopup={ toolbarPopup={
<Show when={showService()}> <Show when={worldMode() === "service"}>
<ServiceWorkflow <ServiceWorkflow
handleSubmit={handleSubmitService} handleSubmit={handleSubmitService}
onClose={() => { onClose={() => {
setShowService(false);
setWorldMode("select"); setWorldMode("select");
currentPromise()?.resolve({ id: "0" }); currentPromise()?.resolve({ id: "0" });
}} }}

View File

@@ -6,7 +6,7 @@
/* <div class="absolute bottom-4 left-1/2 flex -translate-x-1/2 flex-col items-center"> /* <div class="absolute bottom-4 left-1/2 flex -translate-x-1/2 flex-col items-center">
<Show when={show()}> */ <Show when={show()}> */
.toolbar-container { .toolbar-container {
@apply absolute bottom-10 z-10 w-full; @apply absolute bottom-10 z-30 left-1/2;
@apply flex justify-center items-center; @apply flex justify-center items-center;
} }

View File

@@ -38,7 +38,7 @@ function intersectMachines(
camera: THREE.Camera, camera: THREE.Camera,
machineManager: MachineManager, machineManager: MachineManager,
raycaster: THREE.Raycaster, raycaster: THREE.Raycaster,
): string[] { ) {
const rect = renderer.domElement.getBoundingClientRect(); const rect = renderer.domElement.getBoundingClientRect();
const mouse = new THREE.Vector2( const mouse = new THREE.Vector2(
((event.clientX - rect.left) / rect.width) * 2 - 1, ((event.clientX - rect.left) / rect.width) * 2 - 1,
@@ -49,7 +49,10 @@ function intersectMachines(
Array.from(machineManager.machines.values().map((m) => m.group)), Array.from(machineManager.machines.values().map((m) => m.group)),
); );
return intersects.map((i) => i.object.userData.id); return {
machines: intersects.map((i) => i.object.userData.id),
intersection: intersects,
};
} }
function garbageCollectGroup(group: THREE.Group) { function garbageCollectGroup(group: THREE.Group) {
@@ -129,6 +132,8 @@ export function CubeScene(props: {
let sharedCubeGeometry: THREE.BoxGeometry; let sharedCubeGeometry: THREE.BoxGeometry;
let sharedBaseGeometry: THREE.BoxGeometry; let sharedBaseGeometry: THREE.BoxGeometry;
let machineManager: MachineManager;
const [positionMode, setPositionMode] = createSignal<"grid" | "circle">( const [positionMode, setPositionMode] = createSignal<"grid" | "circle">(
"grid", "grid",
); );
@@ -137,6 +142,7 @@ export function CubeScene(props: {
const [cancelMove, setCancelMove] = createSignal<NodeJS.Timeout>(); const [cancelMove, setCancelMove] = createSignal<NodeJS.Timeout>();
// TODO: Unify this with actionRepr position
const [cursorPosition, setCursorPosition] = createSignal<[number, number]>(); const [cursorPosition, setCursorPosition] = createSignal<[number, number]>();
const [cameraInfo, setCameraInfo] = createSignal({ const [cameraInfo, setCameraInfo] = createSignal({
@@ -446,7 +452,7 @@ export function CubeScene(props: {
const registry = new ObjectRegistry(); const registry = new ObjectRegistry();
const machineManager = new MachineManager( machineManager = new MachineManager(
scene, scene,
registry, registry,
props.sceneStore, props.sceneStore,
@@ -478,11 +484,10 @@ export function CubeScene(props: {
.finally(() => { .finally(() => {
if (actionBase) actionBase.visible = false; if (actionBase) actionBase.visible = false;
setWorldMode("default"); setWorldMode("select");
}); });
} }
if (worldMode() === "move") { if (worldMode() === "move") {
console.log("sanpped");
const currId = menuIntersection().at(0); const currId = menuIntersection().at(0);
const pos = cursorPosition(); const pos = cursorPosition();
if (!currId || !pos) return; if (!currId || !pos) return;
@@ -502,10 +507,11 @@ export function CubeScene(props: {
const intersects = raycaster.intersectObjects( const intersects = raycaster.intersectObjects(
Array.from(machineManager.machines.values().map((m) => m.group)), Array.from(machineManager.machines.values().map((m) => m.group)),
); );
console.log("Intersects:", intersects);
if (intersects.length > 0) { if (intersects.length > 0) {
console.log("Clicked on cube:", intersects); const id = intersects.find((i) => i.object.userData?.id)?.object
const id = intersects[0].object.userData.id; .userData.id;
if (!id) return;
if (worldMode() === "select") props.onSelect(new Set<string>([id])); if (worldMode() === "select") props.onSelect(new Set<string>([id]));
@@ -545,7 +551,7 @@ export function CubeScene(props: {
}; };
const handleMouseDown = (e: MouseEvent) => { const handleMouseDown = (e: MouseEvent) => {
const intersection = intersectMachines( const { machines, intersection } = intersectMachines(
e, e,
renderer, renderer,
camera, camera,
@@ -555,7 +561,7 @@ export function CubeScene(props: {
if (e.button === 0) { if (e.button === 0) {
// Left button // Left button
if (worldMode() === "select" && intersection.length) { if (worldMode() === "select" && machines.length) {
// Disable controls to avoid conflict // Disable controls to avoid conflict
controls.enabled = false; controls.enabled = false;
@@ -563,7 +569,8 @@ export function CubeScene(props: {
const cancelMove = setTimeout(() => { const cancelMove = setTimeout(() => {
setIsDragging(true); setIsDragging(true);
// Set machine as flying // Set machine as flying
setHighlightGroups({ move: new Set(intersection) }); setHighlightGroups({ move: new Set(machines) });
setWorldMode("move"); setWorldMode("move");
renderLoop.requestRender(); renderLoop.requestRender();
}, 500); }, 500);
@@ -575,25 +582,22 @@ export function CubeScene(props: {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
if (!intersection.length) return; if (!intersection.length) return;
setMenuIntersection(intersection); setMenuIntersection(machines);
setMenuPos({ x: e.clientX, y: e.clientY }); setMenuPos({ x: e.clientX, y: e.clientY });
setContextOpen(true); setContextOpen(true);
} }
}; };
const handleMouseUp = (e: MouseEvent) => { const handleMouseUp = (e: MouseEvent) => {
if (e.button === 0) { if (e.button === 0) {
console.log("Left mouse up");
setIsDragging(false); setIsDragging(false);
if (cancelMove()) { if (cancelMove()) {
clearTimeout(cancelMove()!); clearTimeout(cancelMove()!);
setCancelMove(undefined); setCancelMove(undefined);
} }
// Always re-enable controls
controls.enabled = true;
if (worldMode() === "move") { if (worldMode() === "move") {
// Cancel long-press if it wasn't triggered yet
// Re-enable controls
controls.enabled = true;
// Set machine as not flying // Set machine as not flying
props.setMachinePos( props.setMachinePos(
highlightGroups["move"].values().next().value!, highlightGroups["move"].values().next().value!,
@@ -652,6 +656,39 @@ export function CubeScene(props: {
}); });
}); });
const snapToGrid = (point: THREE.Vector3) => {
if (!props.sceneStore) return;
// Snap to grid
const snapped = new THREE.Vector3(
Math.round(point.x / GRID_SIZE) * GRID_SIZE,
0,
Math.round(point.z / GRID_SIZE) * GRID_SIZE,
);
// Skip snapping if there's already a cube at this position
const positions = Object.entries(props.sceneStore());
const intersects = positions.some(
([_id, p]) => p.position[0] === snapped.x && p.position[1] === snapped.z,
);
const movingMachine = Array.from(highlightGroups["move"] || [])[0];
const startingPos = positions.find(([_id, p]) => _id === movingMachine);
if (startingPos) {
const isStartingPos =
snapped.x === startingPos[1].position[0] &&
snapped.z === startingPos[1].position[1];
// If Intersect any other machine and not the one being moved
if (!isStartingPos && intersects) {
return;
}
} else {
if (intersects) {
return;
}
}
return snapped;
};
const onAddClick = (event: MouseEvent) => { const onAddClick = (event: MouseEvent) => {
setPositionMode("grid"); setPositionMode("grid");
setWorldMode("create"); setWorldMode("create");
@@ -660,8 +697,6 @@ export function CubeScene(props: {
const onMouseMove = (event: MouseEvent) => { const onMouseMove = (event: MouseEvent) => {
if (!(worldMode() === "create" || worldMode() === "move")) return; if (!(worldMode() === "create" || worldMode() === "move")) return;
console.log("Mouse move in create/move mode");
const actionRepr = worldMode() === "create" ? actionBase : actionMachine; const actionRepr = worldMode() === "create" ? actionBase : actionMachine;
if (!actionRepr) return; if (!actionRepr) return;
@@ -683,24 +718,8 @@ export function CubeScene(props: {
if (intersects.length > 0) { if (intersects.length > 0) {
const point = intersects[0].point; const point = intersects[0].point;
// Snap to grid const snapped = snapToGrid(point);
const snapped = new THREE.Vector3( if (!snapped) return;
Math.round(point.x / GRID_SIZE) * GRID_SIZE,
0,
Math.round(point.z / GRID_SIZE) * GRID_SIZE,
);
// Skip snapping if there's already a cube at this position
if (props.sceneStore()) {
const positions = Object.values(props.sceneStore());
const intersects = positions.some(
(p) => p.position[0] === snapped.x && p.position[1] === snapped.z,
);
if (intersects) {
return;
}
}
if ( if (
Math.abs(actionRepr.position.x - snapped.x) > 0.01 || Math.abs(actionRepr.position.x - snapped.x) > 0.01 ||
Math.abs(actionRepr.position.z - snapped.z) > 0.01 Math.abs(actionRepr.position.z - snapped.z) > 0.01
@@ -715,9 +734,29 @@ export function CubeScene(props: {
const handleMenuSelect = (mode: "move") => { const handleMenuSelect = (mode: "move") => {
setWorldMode(mode); setWorldMode(mode);
setHighlightGroups({ move: new Set(menuIntersection()) }); setHighlightGroups({ move: new Set(menuIntersection()) });
console.log("Menu selected, new World mode", worldMode());
// Find the position of the first selected machine
// Set the actionMachine position to that
const firstId = menuIntersection()[0];
if (firstId) {
const machine = machineManager.machines.get(firstId);
if (machine && actionMachine) {
actionMachine.position.set(
machine.group.position.x,
0,
machine.group.position.z,
);
setCursorPosition([machine.group.position.x, machine.group.position.z]);
}
}
}; };
createEffect(
on(worldMode, (mode) => {
console.log("World mode changed to", mode);
}),
);
const machinesQuery = useMachinesQuery(props.clanURI); const machinesQuery = useMachinesQuery(props.clanURI);
return ( return (