item scale
This commit is contained in:
@@ -9,7 +9,7 @@ import type {
|
||||
WallEvent,
|
||||
WallNode,
|
||||
} from '@pascal-app/core'
|
||||
import { sceneRegistry, useScene } from '@pascal-app/core'
|
||||
import { getScaledDimensions, sceneRegistry, useScene } from '@pascal-app/core'
|
||||
import { Vector3 } from 'three'
|
||||
import type {
|
||||
CommitResult,
|
||||
@@ -43,7 +43,7 @@ export const floorStrategy = {
|
||||
move(ctx: PlacementContext, event: GridEvent): PlacementResult | null {
|
||||
if (ctx.state.surface !== 'floor') return null
|
||||
|
||||
const dims = ctx.asset.dimensions ?? DEFAULT_DIMENSIONS
|
||||
const dims = ctx.draftItem ? getScaledDimensions(ctx.draftItem) : (ctx.asset.dimensions ?? DEFAULT_DIMENSIONS)
|
||||
const [dimX, , dimZ] = dims
|
||||
const x = snapToGrid(event.position[0], dimX)
|
||||
const z = snapToGrid(event.position[2], dimZ)
|
||||
@@ -70,7 +70,7 @@ export const floorStrategy = {
|
||||
const valid = validators.canPlaceOnFloor(
|
||||
ctx.levelId,
|
||||
pos,
|
||||
ctx.draftItem.asset.dimensions,
|
||||
getScaledDimensions(ctx.draftItem),
|
||||
[0, 0, 0],
|
||||
[ctx.draftItem.id],
|
||||
).valid
|
||||
@@ -128,7 +128,7 @@ export const wallStrategy = {
|
||||
event.node.id,
|
||||
x,
|
||||
y,
|
||||
ctx.asset.dimensions ?? DEFAULT_DIMENSIONS,
|
||||
ctx.draftItem ? getScaledDimensions(ctx.draftItem) : (ctx.asset.dimensions ?? DEFAULT_DIMENSIONS),
|
||||
attachTo,
|
||||
side,
|
||||
[],
|
||||
@@ -179,7 +179,7 @@ export const wallStrategy = {
|
||||
event.node.id,
|
||||
snappedX,
|
||||
snappedY,
|
||||
ctx.draftItem.asset.dimensions,
|
||||
getScaledDimensions(ctx.draftItem),
|
||||
ctx.draftItem.asset.attachTo as 'wall' | 'wall-side',
|
||||
side,
|
||||
[ctx.draftItem.id],
|
||||
@@ -219,7 +219,7 @@ export const wallStrategy = {
|
||||
ctx.state.wallId as WallNode['id'],
|
||||
ctx.gridPosition.x,
|
||||
ctx.gridPosition.y,
|
||||
ctx.draftItem.asset.dimensions,
|
||||
getScaledDimensions(ctx.draftItem),
|
||||
ctx.draftItem.asset.attachTo as 'wall' | 'wall-side',
|
||||
ctx.draftItem.side,
|
||||
[ctx.draftItem.id],
|
||||
@@ -281,7 +281,7 @@ export const ceilingStrategy = {
|
||||
const ceilingLevelId = resolveLevelId(event.node, nodes)
|
||||
if (ctx.levelId !== ceilingLevelId) return null
|
||||
|
||||
const dims = ctx.asset.dimensions ?? DEFAULT_DIMENSIONS
|
||||
const dims = ctx.draftItem ? getScaledDimensions(ctx.draftItem) : (ctx.asset.dimensions ?? DEFAULT_DIMENSIONS)
|
||||
const [dimX, , dimZ] = dims
|
||||
const itemHeight = dims[1]
|
||||
|
||||
@@ -308,7 +308,7 @@ export const ceilingStrategy = {
|
||||
if (ctx.state.surface !== 'ceiling') return null
|
||||
if (!ctx.draftItem) return null
|
||||
|
||||
const dims = ctx.asset.dimensions ?? DEFAULT_DIMENSIONS
|
||||
const dims = getScaledDimensions(ctx.draftItem)
|
||||
const [dimX, , dimZ] = dims
|
||||
const itemHeight = dims[1]
|
||||
|
||||
@@ -341,7 +341,7 @@ export const ceilingStrategy = {
|
||||
const valid = validators.canPlaceOnCeiling(
|
||||
ctx.state.ceilingId as CeilingNode['id'],
|
||||
pos,
|
||||
ctx.draftItem.asset.dimensions,
|
||||
getScaledDimensions(ctx.draftItem),
|
||||
ctx.draftItem.rotation,
|
||||
[ctx.draftItem.id],
|
||||
).valid
|
||||
@@ -399,8 +399,8 @@ export const itemSurfaceStrategy = {
|
||||
if (!surfaceItem.asset.surface) return null
|
||||
|
||||
// Size check: our footprint must fit on surface item's footprint
|
||||
const ourDims = ctx.asset.dimensions ?? DEFAULT_DIMENSIONS
|
||||
const surfDims = surfaceItem.asset.dimensions
|
||||
const ourDims = ctx.draftItem ? getScaledDimensions(ctx.draftItem) : (ctx.asset.dimensions ?? DEFAULT_DIMENSIONS)
|
||||
const surfDims = getScaledDimensions(surfaceItem)
|
||||
if (ourDims[0] > surfDims[0] || ourDims[2] > surfDims[2]) return null
|
||||
|
||||
const surfaceMesh = sceneRegistry.nodes.get(surfaceItem.id)
|
||||
@@ -439,7 +439,7 @@ export const itemSurfaceStrategy = {
|
||||
const surfaceMesh = sceneRegistry.nodes.get(ctx.state.surfaceItemId)
|
||||
if (!surfaceMesh) return null
|
||||
|
||||
const ourDims = ctx.asset.dimensions ?? DEFAULT_DIMENSIONS
|
||||
const ourDims = getScaledDimensions(ctx.draftItem)
|
||||
const worldPos = new Vector3(event.position[0], event.position[1], event.position[2])
|
||||
const localPos = surfaceMesh.worldToLocal(worldPos)
|
||||
|
||||
@@ -501,7 +501,7 @@ export function checkCanPlace(ctx: PlacementContext, validators: SpatialValidato
|
||||
return validators.canPlaceOnCeiling(
|
||||
ctx.state.ceilingId as CeilingNode['id'],
|
||||
[ctx.gridPosition.x, ctx.gridPosition.y, ctx.gridPosition.z],
|
||||
ctx.draftItem.asset.dimensions,
|
||||
getScaledDimensions(ctx.draftItem),
|
||||
ctx.draftItem.rotation,
|
||||
[ctx.draftItem.id],
|
||||
).valid
|
||||
@@ -514,7 +514,7 @@ export function checkCanPlace(ctx: PlacementContext, validators: SpatialValidato
|
||||
ctx.state.wallId as WallNode['id'],
|
||||
ctx.gridPosition.x,
|
||||
ctx.gridPosition.y,
|
||||
ctx.draftItem.asset.dimensions,
|
||||
getScaledDimensions(ctx.draftItem),
|
||||
attachTo,
|
||||
ctx.draftItem.side,
|
||||
[ctx.draftItem.id],
|
||||
@@ -525,7 +525,7 @@ export function checkCanPlace(ctx: PlacementContext, validators: SpatialValidato
|
||||
return validators.canPlaceOnFloor(
|
||||
ctx.levelId,
|
||||
[ctx.gridPosition.x, 0, ctx.gridPosition.z],
|
||||
ctx.draftItem.asset.dimensions,
|
||||
getScaledDimensions(ctx.draftItem),
|
||||
[0, 0, 0],
|
||||
[ctx.draftItem.id],
|
||||
).valid
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
type AnyNodeId,
|
||||
type CeilingEvent,
|
||||
emitter,
|
||||
getScaledDimensions,
|
||||
type GridEvent,
|
||||
type ItemEvent,
|
||||
resolveLevelId,
|
||||
@@ -646,7 +647,8 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
||||
|
||||
// ---- Bounding box geometry ----
|
||||
|
||||
const dims = asset.dimensions ?? DEFAULT_DIMENSIONS
|
||||
const draft = draftNode.current
|
||||
const dims = draft ? getScaledDimensions(draft) : (asset.dimensions ?? DEFAULT_DIMENSIONS)
|
||||
const boxGeometry = new BoxGeometry(dims[0], dims[1], dims[2])
|
||||
boxGeometry.translate(0, dims[1] / 2, 0)
|
||||
const edgesGeometry = new EdgesGeometry(boxGeometry)
|
||||
@@ -731,7 +733,7 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
||||
const slabElevation = spatialGridManager.getSlabElevationForItem(
|
||||
levelId,
|
||||
[gridPosition.current.x, gridPosition.current.y, gridPosition.current.z],
|
||||
asset.dimensions ?? DEFAULT_DIMENSIONS,
|
||||
getScaledDimensions(draftNode.current),
|
||||
draftNode.current.rotation,
|
||||
)
|
||||
mesh.position.y = slabElevation
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type AnyNodeId, type ItemNode, useScene, type WallNode, type WindowNode } from '@pascal-app/core'
|
||||
import { getScaledDimensions, type AnyNodeId, type ItemNode, useScene, type WallNode, type WindowNode } from '@pascal-app/core'
|
||||
|
||||
/**
|
||||
* Converts wall-local (X along wall, Y = height above wall base) to world XYZ.
|
||||
@@ -79,7 +79,7 @@ export function hasWallChildOverlap(
|
||||
if (child.type === 'item') {
|
||||
const item = child as ItemNode
|
||||
if (item.asset.attachTo !== 'wall' && item.asset.attachTo !== 'wall-side') continue
|
||||
const [w, h] = item.asset.dimensions
|
||||
const [w, h] = getScaledDimensions(item)
|
||||
childLeft = item.position[0] - w / 2
|
||||
childRight = item.position[0] + w / 2
|
||||
childBottom = item.position[1] // items store bottom Y
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
'use client'
|
||||
|
||||
import { type AnyNode, ItemNode, useScene } from '@pascal-app/core'
|
||||
import { getScaledDimensions, type AnyNode, ItemNode, useScene } from '@pascal-app/core'
|
||||
import { useViewer } from '@pascal-app/viewer'
|
||||
import { Copy, Move, Trash2, X } from 'lucide-react'
|
||||
import { Copy, Link, Link2Off, Move, Trash2, X } from 'lucide-react'
|
||||
import Image from 'next/image'
|
||||
import { useCallback } from 'react'
|
||||
import { useCallback, useState } from 'react'
|
||||
import useEditor from '@/store/use-editor'
|
||||
import { NumberInput } from '@/components/ui/primitives/number-input'
|
||||
import { sfxEmitter } from '@/lib/sfx-bus'
|
||||
@@ -23,6 +23,8 @@ export function ItemPanel() {
|
||||
? (nodes[selectedId as AnyNode['id']] as ItemNode | undefined)
|
||||
: undefined
|
||||
|
||||
const [uniformScale, setUniformScale] = useState(true)
|
||||
|
||||
const handleUpdate = useCallback(
|
||||
(updates: Partial<ItemNode>) => {
|
||||
if (!selectedId || !node) return
|
||||
@@ -188,13 +190,75 @@ export function ItemPanel() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Dimensions (read-only) */}
|
||||
{/* Scale */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="font-medium text-muted-foreground text-xs uppercase tracking-wide">
|
||||
Scale
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
className="rounded p-1 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground cursor-pointer"
|
||||
onClick={() => setUniformScale((v) => !v)}
|
||||
title={uniformScale ? 'Unlock axes' : 'Lock axes'}
|
||||
>
|
||||
{uniformScale ? <Link className="h-3.5 w-3.5" /> : <Link2Off className="h-3.5 w-3.5" />}
|
||||
</button>
|
||||
</div>
|
||||
{uniformScale ? (
|
||||
<NumberInput
|
||||
label="XYZ"
|
||||
value={Math.round(node.scale[0] * 100) / 100}
|
||||
onChange={(value) => {
|
||||
const v = Math.max(0.01, value)
|
||||
handleUpdate({ scale: [v, v, v] })
|
||||
}}
|
||||
precision={2}
|
||||
step={0.1}
|
||||
/>
|
||||
) : (
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
<NumberInput
|
||||
label="X"
|
||||
value={Math.round(node.scale[0] * 100) / 100}
|
||||
onChange={(value) => {
|
||||
handleUpdate({ scale: [Math.max(0.01, value), node.scale[1], node.scale[2]] })
|
||||
}}
|
||||
precision={2}
|
||||
step={0.1}
|
||||
/>
|
||||
<NumberInput
|
||||
label="Y"
|
||||
value={Math.round(node.scale[1] * 100) / 100}
|
||||
onChange={(value) => {
|
||||
handleUpdate({ scale: [node.scale[0], Math.max(0.01, value), node.scale[2]] })
|
||||
}}
|
||||
precision={2}
|
||||
step={0.1}
|
||||
/>
|
||||
<NumberInput
|
||||
label="Z"
|
||||
value={Math.round(node.scale[2] * 100) / 100}
|
||||
onChange={(value) => {
|
||||
handleUpdate({ scale: [node.scale[0], node.scale[1], Math.max(0.01, value)] })
|
||||
}}
|
||||
precision={2}
|
||||
step={0.1}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Dimensions (effective, read-only) */}
|
||||
<div className="space-y-2">
|
||||
<label className="font-medium text-muted-foreground text-xs uppercase tracking-wide">
|
||||
Dimensions
|
||||
</label>
|
||||
<div className="rounded border border-border bg-muted/50 px-3 py-2 text-sm">
|
||||
{node.asset.dimensions[0]}m × {node.asset.dimensions[1]}m × {node.asset.dimensions[2]}m
|
||||
{(() => {
|
||||
const [w, h, d] = getScaledDimensions(node)
|
||||
return `${Math.round(w * 100) / 100}m × ${Math.round(h * 100) / 100}m × ${Math.round(d * 100) / 100}m`
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user