# Item Placement with Wall and Floor Validation in Pascal Editor

> Pascal Editor ensures flawless item placement with real-time wall and floor validation using distinct spatial grids. Prevent collisions before committing objects to your scene.

- Repository: [Pascal/editor](https://github.com/pascalorg/editor)
- Tags: how-to-guide
- Published: 2026-03-25

---

**Pascal Editor validates every item placement in real-time through surface-specific spatial grids that check collisions against floors, walls, and ceilings before committing any object to the scene.**

The `pascalorg/editor` repository implements a strict validation pipeline that prevents geometric intersections by decoupling UI interaction logic from spatial-grid computations. This architecture guarantees that items snapped to floors, walls, or ceilings never penetrate slabs, existing objects, or surface boundaries, with immediate visual feedback driven by the **core** spatial-grid manager.

## Real-Time Validation Architecture

The validation flow follows a strict sequence: cursor movement triggers a **placement strategy**, which computes a tentative `PlacementResult`, then queries the unified validator, updates the Three.js cursor color (green for valid, red for invalid), and only commits the draft node when `checkCanPlace` returns `true`.

### The Placement Coordinator Hook

At [`packages/editor/src/components/tools/item/use-placement_coordinator.tsx`](https://github.com/pascalorg/editor/blob/main/packages/editor/src/components/tools/item/use-placement_coordinator.tsx), the `usePlacementCoordinator` hook orchestrates the entire interaction loop. It instantiates a Three.js cursor mesh and wires it to the current **draft node**, invoking `revalidate` on every `grid:move`, `wall:enter`, `wall:move`, or `grid:click` event.

The `revalidate` function aggregates surface-specific validators and mutates cursor materials in place:

```typescript
const revalidate = (): boolean => {
  const placeable = shiftFreeRef.current || checkCanPlace(getContext(), validators)
  const color = placeable ? 0x22_c5_5e : 0xef_44_44 // green : red
  edgeMaterial.color.setHex(color)
  basePlaneMaterial.color.setHex(color)
  return placeable
}

```

If validation fails while a draft exists, the coordinator immediately destroys the draft node to keep the scene graph clean.

### Surface-Specific Placement Strategies

Located in [`packages/editor/src/components/tools/item/placement-strategies.ts`](https://github.com/pascalorg/editor/blob/main/packages/editor/src/components/tools/item/placement-strategies.ts), the strategy implementations compute world transforms and trigger the correct validator:

*   **floorStrategy** – Snaps the cursor to the grid and, on click, invokes `validators.canPlaceOnFloor`.
*   **wallStrategy** – Validates the attachment side (`front`/`back`), calculates the **Y-offset** via the validator's `adjustedY` return value, and reparents the draft node to the wall scene node.

The wall validation excerpt demonstrates this adjustment:

```typescript
const validation = validators.canPlaceOnWall(
  ctx.levelId,
  event.node.id,
  x, y,
  ctx.draftItem ? getScaledDimensions(ctx.draftItem) : DEFAULT_DIMENSIONS,
  attachTo,
  side,
  [],
)
const adjustedY = validation.adjustedY ?? y

```

## Unified Validation API

All surface queries funnel through `checkCanPlace`, which dispatches to the appropriate **spatial-grid manager** method based on the item's `attachTo` property.

### The checkCanPlace Function

Defined at the bottom of [`packages/editor/src/components/tools/item/placement-strategies.ts`](https://github.com/pascalorg/editor/blob/main/packages/editor/src/components/tools/item/placement-strategies.ts), this function acts as a single entry point for validity checks:

```typescript
export function checkCanPlace(ctx: PlacementContext, validators: SpatialValidators): boolean {
  if (!(ctx.levelId && ctx.draftItem)) return false

  // Item-surface – only needs a non-null surfaceItemId
  if (ctx.state.surface === 'item-surface') return ctx.state.surfaceItemId !== null

  const attachTo = ctx.draftItem.asset.attachTo

  // Ceiling validation
  if (attachTo === 'ceiling') {
    return validators.canPlaceOnCeiling(...).valid
  }

  // Wall validation
  if (attachTo === 'wall' || attachTo === 'wall-side') {
    return validators.canPlaceOnWall(...).valid
  }

  // Floor (default)
  return validators.canPlaceOnFloor(...).valid
}

```

### Spatial Grid Manager Implementation

The concrete collision logic resides in [`packages/core/src/hooks/spatial-grid/spatial-grid-manager.ts`](https://github.com/pascalorg/editor/blob/main/packages/core/src/hooks/spatial-grid/spatial-grid-manager.ts).

**Floor validation** queries a 2-D occupancy map and performs AABB overlap checks:

```typescript
canPlaceOnFloor(levelId, position, dimensions, rotation, ignoreIds) {
  const grid = this.getFloorGrid(levelId)
  return grid.canPlace(position, dimensions, rotation, ignoreIds)
}

```

**Wall validation** uses a parametric grid that respects side-specific constraints (`wall` for two-sided items, `wall-side` for single-sided). The method signature returns both a validity flag and an adjusted height:

```typescript
canPlaceOnWall(
  levelId,
  wallId,
  localX,
  localY,
  dimensions,
  attachType = 'wall',
  side,
  ignoreIds,
) {
  const wallLength = this.getWallLength(wallId)
  const wallHeight = this.getWallHeight(wallId)
  const tCenter = localX / wallLength
  const [itemWidth, itemHeight] = dimensions
  return this.getWallGrid(levelId).canPlaceOnWall(
    wallId,
    wallLength,
    wallHeight,
    tCenter,
    itemWidth,
    localY,
    itemHeight,
    attachType,
    side,
    ignoreIds,
  )
}

```

The returned object includes `adjustedY`, which the placement coordinator uses to snap the item upward when wall steps or slabs intersect the intended height. **Ceiling validation** mirrors floor logic but operates on a separate ceiling grid.

## Implementation Examples

### Placing Items on the Floor

The following pattern demonstrates floor placement using the coordinator and spatial query hooks:

```tsx
import { usePlacementCoordinator } from '@pascal-app/editor/src/components/tools/item/use-placement_coordinator'
import { useSpatialQuery } from '@pascal-app/core/src/hooks/spatial-grid/use-spatial-query'

function FloorItemPlacer({ asset }) {
  const { canPlaceOnFloor } = useSpatialQuery()
  const draft = useDraftNode() // Creates the visual draft mesh
  const cursor = useRef<Group>(null!)

  const placementNode = usePlacementCoordinator({
    asset,
    draftNode: draft,
    initDraft: (gridPos) => draft.create(gridPos, asset, [0, 0, 0]),
    onCommitted: () => true,
    onCancel: () => draft.destroy(),
  })

  // Visual feedback is automatic: the cursor turns green when
  // canPlaceOnFloor(levelId, [x,0,z], dims, [0,0,0]) returns {valid:true}
  return <>{placementNode}</>
}

```

When the user moves the pointer, `floorStrategy.move` computes the snapped `[x,0,z]`. The coordinator then executes `validators.canPlaceOnFloor`. If the floor grid reports a collision—such as another item occupying the same space or the point lying inside a slab hole—the cursor material switches to red and the click handler prevents the commit.

### Wall Placement with Y-Adjustment

Wall placement requires transitioning the draft node to the wall's coordinate space and applying the validator's height adjustment:

```tsx
import { wallStrategy } from '@pascal-app/editor/src/components/tools/item/placement-strategies'

function onWallEnter(event: WallEvent) {
  const ctx = getPlacementContext()
  const result = wallStrategy.enter(ctx, event, resolveLevelId, nodes, validators)
  if (result) {
    // Transition the draft to wall-parented state
    applyTransition(result)
  }
}

```

Inside `wallStrategy.enter`, the code verifies the asset’s `attachTo` property (either `wall` or `wall-side`), calls `validators.canPlaceOnWall` to obtain the `adjustedY`, and returns a `TransitionResult` that reparents the draft node and sets its Y-position to the validated height. The coordinator immediately calls `revalidate()` again, updating the cursor color based on the wall grid's response.

## Summary

*   **Unified entry point** – The `checkCanPlace` function in [`placement-strategies.ts`](https://github.com/pascalorg/editor/blob/main/placement-strategies.ts) dispatches to surface-specific validators (`canPlaceOnFloor`, `canPlaceOnWall`, `canPlaceOnCeiling`) based on the item's `attachTo` property.
*   **Real-time feedback** – `usePlacementCoordinator` updates Three.js cursor materials to **green** (`0x22_c5_5e`) or **red** (`0xef_44_44`) instantly as the user moves the cursor, preventing invalid commits before they occur.
*   **Wall-specific logic** – The spatial-grid manager returns an `adjustedY` value that automatically raises items above wall steps or slab intersections, respecting both single-sided (`wall-side`) and double-sided (`wall`) attachment modes.
*   **Clean separation** – The **editor** package handles UI concerns (cursor meshes, events, draft nodes) while the **core** package encapsulates all geometric collision detection in [`spatial-grid-manager.ts`](https://github.com/pascalorg/editor/blob/main/spatial-grid-manager.ts).

## Frequently Asked Questions

### How does Pascal Editor prevent items from intersecting walls during placement?

The `canPlaceOnWall` method in [`packages/core/src/hooks/spatial-grid/spatial-grid-manager.ts`](https://github.com/pascalorg/editor/blob/main/packages/core/src/hooks/spatial-grid/spatial-grid-manager.ts) queries a parametric wall grid that checks side-specific collisions against the wall's length and height. It returns `{valid: boolean, adjustedY?: number}`, where `adjustedY` shifts the item upward if a step or slab blocks the original height, ensuring the cursor turns red and blocks the commit when geometric overlap is detected.

### What is the difference between `wall` and `wall-side` attachment types?

**`wall`** allows items to occupy space on both the front and back surfaces of a wall simultaneously (centered placement), while **`wall-side`** restricts validation to a single surface side (front or back). The placement strategy passes this `attachType` parameter to `canPlaceOnWall`, which toggles the collision detection logic in the wall grid to respect single- or double-sided occupancy rules.

### Why does the placement cursor turn red even when the surface looks empty?

The spatial-grid manager maintains internal **2-D occupancy maps** for floors and ceilings and **parametric grids** for walls that track all committed items and architectural elements like slabs. If the cursor enters a cell marked as occupied by an existing object or excluded by a slab hole, `checkCanPlace` returns `false`, triggering the red color (`0xef_44_44`) and preventing the draft node from being committed to the scene graph.

### Can the validation system handle items attached to other items rather than surfaces?

Yes. When `ctx.state.surface === 'item-surface'`, the `checkCanPlace` function short-circuits surface validation and simply verifies that `ctx.state.surfaceItemId !== null`. This allows the placement coordinator to snap cursors to valid item anchors without invoking floor, wall, or ceiling grid queries, streamlining the validation path for stacked or hanging objects.