mirror of
https://github.com/stan-smith/FossFLOW.git
synced 2025-12-23 22:48:57 -05:00
@Ron-Alex this commit closes #82 now if a user attempts to place an item on an occupied space we now find the nearest unoccupied tile and place it there instead.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useUiStateStore } from 'src/stores/uiStateStore';
|
||||
import { getTilePosition, CoordsUtils, generateId } from 'src/utils';
|
||||
import { getTilePosition, CoordsUtils, generateId, findNearestUnoccupiedTile } from 'src/utils';
|
||||
import { useScene } from 'src/hooks/useScene';
|
||||
import { useModelStore } from 'src/stores/modelStore';
|
||||
import { VIEW_ITEM_DEFAULTS } from 'src/config';
|
||||
@@ -50,6 +50,9 @@ export const ContextMenuManager = ({ anchorEl }: Props) => {
|
||||
if (model.icons.length > 0) {
|
||||
const modelItemId = generateId();
|
||||
const firstIcon = model.icons[0];
|
||||
|
||||
// Find nearest unoccupied tile (should return the same tile since context menu is for empty tiles)
|
||||
const targetTile = findNearestUnoccupiedTile(contextMenu.tile, scene) || contextMenu.tile;
|
||||
|
||||
scene.placeIcon({
|
||||
modelItem: {
|
||||
@@ -60,7 +63,7 @@ export const ContextMenuManager = ({ anchorEl }: Props) => {
|
||||
viewItem: {
|
||||
...VIEW_ITEM_DEFAULTS,
|
||||
id: modelItemId,
|
||||
tile: contextMenu.tile
|
||||
tile: targetTile
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -6,7 +6,8 @@ import {
|
||||
CoordsUtils,
|
||||
hasMovedTile,
|
||||
getAnchorParent,
|
||||
getItemAtTile
|
||||
getItemAtTile,
|
||||
findNearestUnoccupiedTilesForGroup
|
||||
} from 'src/utils';
|
||||
|
||||
const dragItems = (
|
||||
@@ -15,14 +16,40 @@ const dragItems = (
|
||||
delta: Coords,
|
||||
scene: ReturnType<typeof useScene>
|
||||
) => {
|
||||
items.forEach((item) => {
|
||||
if (item.type === 'ITEM') {
|
||||
// Separate items from other draggable elements
|
||||
const itemRefs = items.filter(item => item.type === 'ITEM');
|
||||
const otherRefs = items.filter(item => item.type !== 'ITEM');
|
||||
|
||||
// If there are items being dragged, find nearest unoccupied tiles for them
|
||||
if (itemRefs.length > 0) {
|
||||
const itemsWithTargets = itemRefs.map(item => {
|
||||
const node = getItemByIdOrThrow(scene.items, item.id).value;
|
||||
|
||||
scene.updateViewItem(item.id, {
|
||||
tile: CoordsUtils.add(node.tile, delta)
|
||||
return {
|
||||
id: item.id,
|
||||
targetTile: CoordsUtils.add(node.tile, delta)
|
||||
};
|
||||
});
|
||||
|
||||
// Find nearest unoccupied tiles for all items
|
||||
const newTiles = findNearestUnoccupiedTilesForGroup(
|
||||
itemsWithTargets,
|
||||
scene,
|
||||
itemRefs.map(item => item.id) // Exclude the items being dragged
|
||||
);
|
||||
|
||||
// If we found valid positions for all items, move them
|
||||
if (newTiles) {
|
||||
itemRefs.forEach((item, index) => {
|
||||
scene.updateViewItem(item.id, {
|
||||
tile: newTiles[index]
|
||||
});
|
||||
});
|
||||
} else if (item.type === 'RECTANGLE') {
|
||||
}
|
||||
}
|
||||
|
||||
// Handle non-item references (rectangles, textboxes, connector anchors)
|
||||
otherRefs.forEach((item) => {
|
||||
if (item.type === 'RECTANGLE') {
|
||||
const rectangle = getItemByIdOrThrow(scene.rectangles, item.id).value;
|
||||
const newFrom = CoordsUtils.add(rectangle.from, delta);
|
||||
const newTo = CoordsUtils.add(rectangle.to, delta);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { produce } from 'immer';
|
||||
import { ModeActions } from 'src/types';
|
||||
import { generateId, getItemAtTile } from 'src/utils';
|
||||
import { generateId, getItemAtTile, findNearestUnoccupiedTile } from 'src/utils';
|
||||
import { VIEW_ITEM_DEFAULTS } from 'src/config';
|
||||
|
||||
export const PlaceIcon: ModeActions = {
|
||||
@@ -27,20 +27,29 @@ export const PlaceIcon: ModeActions = {
|
||||
if (uiState.mode.type !== 'PLACE_ICON') return;
|
||||
|
||||
if (uiState.mode.id !== null) {
|
||||
const modelItemId = generateId();
|
||||
// Find the nearest unoccupied tile to the target position
|
||||
const targetTile = findNearestUnoccupiedTile(
|
||||
uiState.mouse.position.tile,
|
||||
scene
|
||||
);
|
||||
|
||||
scene.placeIcon({
|
||||
modelItem: {
|
||||
id: modelItemId,
|
||||
name: 'Untitled',
|
||||
icon: uiState.mode.id
|
||||
},
|
||||
viewItem: {
|
||||
...VIEW_ITEM_DEFAULTS,
|
||||
id: modelItemId,
|
||||
tile: uiState.mouse.position.tile
|
||||
}
|
||||
});
|
||||
// Place the icon on the nearest unoccupied tile
|
||||
if (targetTile) {
|
||||
const modelItemId = generateId();
|
||||
|
||||
scene.placeIcon({
|
||||
modelItem: {
|
||||
id: modelItemId,
|
||||
name: 'Untitled',
|
||||
icon: uiState.mode.id
|
||||
},
|
||||
viewItem: {
|
||||
...VIEW_ITEM_DEFAULTS,
|
||||
id: modelItemId,
|
||||
tile: targetTile
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
uiState.actions.setMode(
|
||||
|
||||
133
packages/fossflow-lib/src/utils/findNearestUnoccupiedTile.ts
Normal file
133
packages/fossflow-lib/src/utils/findNearestUnoccupiedTile.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
import { Coords } from 'src/types';
|
||||
import { useScene } from 'src/hooks/useScene';
|
||||
import { getItemAtTile } from './renderer';
|
||||
|
||||
/**
|
||||
* Finds the nearest unoccupied tile to the target tile using a spiral search pattern
|
||||
* @param targetTile - The desired tile position
|
||||
* @param scene - The current scene
|
||||
* @param maxDistance - Maximum search distance (default: 10)
|
||||
* @returns The nearest unoccupied tile, or null if none found within maxDistance
|
||||
*/
|
||||
export const findNearestUnoccupiedTile = (
|
||||
targetTile: Coords,
|
||||
scene: ReturnType<typeof useScene>,
|
||||
maxDistance: number = 10
|
||||
): Coords | null => {
|
||||
// Check if the target tile itself is unoccupied
|
||||
const itemAtTarget = getItemAtTile({ tile: targetTile, scene });
|
||||
if (!itemAtTarget || itemAtTarget.type !== 'ITEM') {
|
||||
return targetTile;
|
||||
}
|
||||
|
||||
// Spiral search pattern: right, down, left, up
|
||||
const directions = [
|
||||
{ x: 1, y: 0 }, // right
|
||||
{ x: 0, y: 1 }, // down
|
||||
{ x: -1, y: 0 }, // left
|
||||
{ x: 0, y: -1 } // up
|
||||
];
|
||||
|
||||
// Search in expanding rings around the target
|
||||
for (let distance = 1; distance <= maxDistance; distance++) {
|
||||
// Start from the top-left of the ring
|
||||
let currentTile = {
|
||||
x: targetTile.x - distance,
|
||||
y: targetTile.y - distance
|
||||
};
|
||||
|
||||
// Check all tiles in this ring
|
||||
for (let side = 0; side < 4; side++) {
|
||||
const direction = directions[side];
|
||||
const sideLength = distance * 2;
|
||||
|
||||
for (let step = 0; step < sideLength; step++) {
|
||||
// Move to the next tile on this side of the ring
|
||||
currentTile = {
|
||||
x: currentTile.x + direction.x,
|
||||
y: currentTile.y + direction.y
|
||||
};
|
||||
|
||||
// Check if this tile is within bounds and unoccupied
|
||||
const itemAtTile = getItemAtTile({ tile: currentTile, scene });
|
||||
if (!itemAtTile || itemAtTile.type !== 'ITEM') {
|
||||
return currentTile;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// No unoccupied tile found within maxDistance
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Finds the nearest unoccupied tile for multiple items being placed/moved
|
||||
* Ensures all items can be placed without overlapping
|
||||
* @param items - Array of items with their target tiles
|
||||
* @param scene - The current scene
|
||||
* @param excludeIds - IDs of items to exclude from occupation check (e.g., items being moved)
|
||||
* @returns Array of nearest unoccupied tiles for each item, or null if cannot place all
|
||||
*/
|
||||
export const findNearestUnoccupiedTilesForGroup = (
|
||||
items: { id: string; targetTile: Coords }[],
|
||||
scene: ReturnType<typeof useScene>,
|
||||
excludeIds: string[] = []
|
||||
): Coords[] | null => {
|
||||
const result: Coords[] = [];
|
||||
const occupiedTiles = new Set<string>();
|
||||
|
||||
// Add existing items to occupied tiles (excluding the ones being moved)
|
||||
scene.items.forEach(item => {
|
||||
if (!excludeIds.includes(item.id)) {
|
||||
occupiedTiles.add(`${item.tile.x},${item.tile.y}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Find unoccupied tiles for each item
|
||||
for (const item of items) {
|
||||
let foundTile: Coords | null = null;
|
||||
const targetKey = `${item.targetTile.x},${item.targetTile.y}`;
|
||||
|
||||
// Check if target is available
|
||||
if (!occupiedTiles.has(targetKey)) {
|
||||
foundTile = item.targetTile;
|
||||
} else {
|
||||
// Search for nearest unoccupied tile
|
||||
for (let distance = 1; distance <= 10; distance++) {
|
||||
// Check tiles in a square ring at this distance
|
||||
for (let dx = -distance; dx <= distance; dx++) {
|
||||
for (let dy = -distance; dy <= distance; dy++) {
|
||||
// Only check tiles on the ring perimeter
|
||||
if (Math.abs(dx) === distance || Math.abs(dy) === distance) {
|
||||
const checkTile = {
|
||||
x: item.targetTile.x + dx,
|
||||
y: item.targetTile.y + dy
|
||||
};
|
||||
const checkKey = `${checkTile.x},${checkTile.y}`;
|
||||
|
||||
if (!occupiedTiles.has(checkKey)) {
|
||||
const itemAtTile = getItemAtTile({ tile: checkTile, scene });
|
||||
if (!itemAtTile || itemAtTile.type !== 'ITEM' || excludeIds.includes(itemAtTile.id)) {
|
||||
foundTile = checkTile;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (foundTile) break;
|
||||
}
|
||||
if (foundTile) break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!foundTile) {
|
||||
return null; // Cannot place all items
|
||||
}
|
||||
|
||||
result.push(foundTile);
|
||||
occupiedTiles.add(`${foundTile.x},${foundTile.y}`);
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
@@ -5,3 +5,4 @@ export * from './pathfinder';
|
||||
export * from './renderer';
|
||||
export * from './exportOptions';
|
||||
export * from './model';
|
||||
export * from './findNearestUnoccupiedTile';
|
||||
|
||||
Reference in New Issue
Block a user