attach to wall side
This commit is contained in:
@@ -61,6 +61,8 @@ export class SpatialGridManager {
|
||||
tEnd: t + halfW,
|
||||
yStart: item.position[1],
|
||||
yEnd: item.position[1] + height,
|
||||
attachType: item.asset.attachTo as 'wall' | 'wall-side',
|
||||
side: item.side,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -101,6 +103,8 @@ export class SpatialGridManager {
|
||||
tEnd: t + halfW,
|
||||
yStart: item.position[1],
|
||||
yEnd: item.position[1] + height,
|
||||
attachType: item.asset.attachTo as 'wall' | 'wall-side',
|
||||
side: item.side,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -147,6 +151,8 @@ export class SpatialGridManager {
|
||||
* @param localX - X position in wall-local space (distance from wall start)
|
||||
* @param localY - Y position (height from floor)
|
||||
* @param dimensions - item dimensions [width, height, depth]
|
||||
* @param attachType - 'wall' (needs both sides) or 'wall-side' (needs one side)
|
||||
* @param side - which side for 'wall-side' items
|
||||
* @param ignoreIds - item IDs to ignore in collision check
|
||||
*/
|
||||
canPlaceOnWall(
|
||||
@@ -155,6 +161,8 @@ export class SpatialGridManager {
|
||||
localX: number,
|
||||
localY: number,
|
||||
dimensions: [number, number, number],
|
||||
attachType: 'wall' | 'wall-side' = 'wall',
|
||||
side?: 'front' | 'back',
|
||||
ignoreIds?: string[],
|
||||
) {
|
||||
const wallLength = this.getWallLength(wallId)
|
||||
@@ -173,6 +181,8 @@ export class SpatialGridManager {
|
||||
itemWidth,
|
||||
localY,
|
||||
itemHeight,
|
||||
attachType,
|
||||
side,
|
||||
ignoreIds,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -53,14 +53,15 @@ export function initSpatialGridSync() {
|
||||
}
|
||||
}
|
||||
|
||||
// Detect updated nodes (items with position/rotation/parentId changes)
|
||||
// Detect updated nodes (items with position/rotation/parentId/side changes)
|
||||
for (const [id, node] of Object.entries(state.nodes)) {
|
||||
const prev = prevState.nodes[id as AnyNode['id']]
|
||||
if (prev && node.type === 'item' && prev.type === 'item') {
|
||||
if (
|
||||
!arraysEqual(node.position, prev.position) ||
|
||||
!arraysEqual(node.rotation, prev.rotation) ||
|
||||
node.parentId !== prev.parentId
|
||||
node.parentId !== prev.parentId ||
|
||||
node.side !== prev.side
|
||||
) {
|
||||
const levelId = resolveLevelId(node, state.nodes)
|
||||
spatialGridManager.handleNodeUpdated(node, levelId)
|
||||
|
||||
@@ -23,6 +23,8 @@ export function useSpatialQuery() {
|
||||
localX: number,
|
||||
localY: number,
|
||||
dimensions: [number, number, number],
|
||||
attachType: 'wall' | 'wall-side' = 'wall',
|
||||
side?: 'front' | 'back',
|
||||
ignoreIds?: string[],
|
||||
) => {
|
||||
return spatialGridManager.canPlaceOnWall(
|
||||
@@ -31,6 +33,8 @@ export function useSpatialQuery() {
|
||||
localX,
|
||||
localY,
|
||||
dimensions,
|
||||
attachType,
|
||||
side,
|
||||
ignoreIds,
|
||||
)
|
||||
},
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
type WallSide = 'front' | 'back'
|
||||
type AttachType = 'wall' | 'wall-side'
|
||||
|
||||
// Small tolerance for floating point comparison to allow adjacent items
|
||||
const EPSILON = 0.001
|
||||
|
||||
interface WallItemPlacement {
|
||||
itemId: string
|
||||
wallId: string
|
||||
@@ -5,12 +11,27 @@ interface WallItemPlacement {
|
||||
tEnd: number
|
||||
yStart: number // height range
|
||||
yEnd: number
|
||||
attachType?: AttachType // 'wall' blocks both sides, 'wall-side' blocks one side (undefined = 'wall' for legacy)
|
||||
side?: WallSide // Which side for 'wall-side' items (undefined means both for 'wall')
|
||||
}
|
||||
|
||||
export class WallSpatialGrid {
|
||||
private wallItems = new Map<string, WallItemPlacement[]>() // wallId -> placements
|
||||
private itemToWall = new Map<string, string>() // itemId -> wallId (reverse lookup)
|
||||
|
||||
/**
|
||||
* Check if an item can be placed on a wall
|
||||
* @param wallId - The wall to place on
|
||||
* @param wallLength - Length of the wall
|
||||
* @param wallHeight - Height of the wall
|
||||
* @param tCenter - Parametric center position (0-1) along wall
|
||||
* @param itemWidth - Width of the item
|
||||
* @param yBottom - Bottom Y position of the item
|
||||
* @param itemHeight - Height of the item
|
||||
* @param attachType - 'wall' (blocks both sides) or 'wall-side' (blocks one side)
|
||||
* @param side - Which side for 'wall-side' items
|
||||
* @param ignoreIds - Item IDs to ignore in conflict check
|
||||
*/
|
||||
canPlaceOnWall(
|
||||
wallId: string,
|
||||
wallLength: number,
|
||||
@@ -19,6 +40,8 @@ export class WallSpatialGrid {
|
||||
itemWidth: number,
|
||||
yBottom: number,
|
||||
itemHeight: number,
|
||||
attachType: AttachType = 'wall',
|
||||
side?: WallSide,
|
||||
ignoreIds: string[] = [],
|
||||
): { valid: boolean; conflictIds: string[] } {
|
||||
const halfW = itemWidth / wallLength / 2
|
||||
@@ -40,17 +63,53 @@ export class WallSpatialGrid {
|
||||
for (const placement of existing) {
|
||||
if (ignoreSet.has(placement.itemId)) continue
|
||||
|
||||
const tOverlap = tStart < placement.tEnd && tEnd > placement.tStart
|
||||
const yOverlap = yStart < placement.yEnd && yEnd > placement.yStart
|
||||
// Use EPSILON tolerance to allow items to be exactly adjacent
|
||||
const tOverlap = tStart < placement.tEnd - EPSILON && tEnd > placement.tStart + EPSILON
|
||||
const yOverlap = yStart < placement.yEnd - EPSILON && yEnd > placement.yStart + EPSILON
|
||||
|
||||
if (tOverlap && yOverlap) {
|
||||
conflicts.push(placement.itemId)
|
||||
// Check side conflicts based on attach types
|
||||
const hasConflict = this.checkSideConflict(attachType, side, placement)
|
||||
if (hasConflict) {
|
||||
conflicts.push(placement.itemId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { valid: conflicts.length === 0, conflictIds: conflicts }
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if two items conflict based on their attach types and sides
|
||||
* - 'wall' items block both sides, so they conflict with everything
|
||||
* - 'wall-side' items only conflict if they're on the same side or if the other is a 'wall' item
|
||||
*/
|
||||
private checkSideConflict(
|
||||
newAttachType: AttachType,
|
||||
newSide: WallSide | undefined,
|
||||
existing: WallItemPlacement,
|
||||
): boolean {
|
||||
// Treat undefined/legacy attachType as 'wall' (blocks both sides)
|
||||
const existingAttachType = existing.attachType ?? 'wall'
|
||||
|
||||
// If new item is 'wall' type, it conflicts with everything (needs both sides)
|
||||
if (newAttachType === 'wall') {
|
||||
return true
|
||||
}
|
||||
|
||||
// If existing item is 'wall' type, it blocks both sides
|
||||
if (existingAttachType === 'wall') {
|
||||
return true
|
||||
}
|
||||
|
||||
// Both are 'wall-side' - only conflict if they're on the same side
|
||||
// If either side is undefined, be conservative and assume conflict
|
||||
if (!newSide || !existing.side) {
|
||||
return true
|
||||
}
|
||||
return newSide === existing.side
|
||||
}
|
||||
|
||||
insert(placement: WallItemPlacement) {
|
||||
const { wallId, itemId } = placement
|
||||
|
||||
|
||||
Reference in New Issue
Block a user