feat: improve collaboration feedback and previews

This commit is contained in:
Aymeric Rabot
2026-07-20 20:25:30 +02:00
parent 10c9c6ad27
commit e1a4dba740
27 changed files with 535 additions and 39 deletions
+1
View File
@@ -91,6 +91,7 @@ export const wallDefinition: NodeDefinition<typeof WallNode> = {
// auto-slab live preview, history dances). Placement is wired via
// `def.tool`.
tool: () => import('./tool'),
preview: () => import('./preview'),
affordanceTools: {
curve: () => import('./curve-tool'),
'move-endpoint': () => import('./move-endpoint-tool'),
+22
View File
@@ -0,0 +1,22 @@
// @ts-expect-error — bun:test is provided by the Bun runtime; nodes does not
// include Bun ambient types in its production declaration build.
import { describe, expect, test } from 'bun:test'
import { buildWallPreviewGeometry } from './preview'
describe('wall placement preview', () => {
test('uses the wall segment footprint instead of a generic box', () => {
const geometry = buildWallPreviewGeometry({
start: [1, 2],
end: [5, 2],
height: 3,
thickness: 0.2,
})
const bounds = geometry.boundingBox!
expect(bounds.max.x - bounds.min.x).toBeCloseTo(4)
expect(bounds.max.y - bounds.min.y).toBeCloseTo(3)
expect(bounds.max.z - bounds.min.z).toBeCloseTo(0.2)
geometry.dispose()
})
})
+58
View File
@@ -0,0 +1,58 @@
'use client'
import { getWallSurfacePolygon, type WallNode } from '@pascal-app/core'
import { EDITOR_LAYER } from '@pascal-app/editor'
import { useEffect, useMemo } from 'react'
import { ExtrudeGeometry, Shape } from 'three'
const WALL_PREVIEW_HEIGHT = 2.5
const WALL_PREVIEW_THICKNESS = 0.1
export function buildWallPreviewGeometry(
node: Pick<WallNode, 'start' | 'end' | 'curveOffset' | 'height' | 'thickness'>,
) {
const polygon = getWallSurfacePolygon({
start: node.start,
end: node.end,
curveOffset: node.curveOffset,
thickness: node.thickness ?? WALL_PREVIEW_THICKNESS,
})
const shape = new Shape()
polygon.forEach((point, index) => {
if (index === 0) shape.moveTo(point.x, -point.y)
else shape.lineTo(point.x, -point.y)
})
shape.closePath()
const geometry = new ExtrudeGeometry(shape, {
bevelEnabled: false,
depth: node.height ?? WALL_PREVIEW_HEIGHT,
steps: 1,
})
geometry.rotateX(-Math.PI / 2)
geometry.computeBoundingBox()
return geometry
}
const WallPreview = ({ node }: { node: WallNode }) => {
const { curveOffset, end, height, start, thickness } = node
const geometry = useMemo(
() => buildWallPreviewGeometry({ curveOffset, end, height, start, thickness }),
[curveOffset, end, height, start, thickness],
)
useEffect(() => () => geometry.dispose(), [geometry])
return (
<mesh geometry={geometry} layers={EDITOR_LAYER} raycast={() => undefined} renderOrder={1}>
<meshBasicMaterial
color="#818cf8"
depthTest={false}
depthWrite={false}
opacity={0.5}
transparent
/>
</mesh>
)
}
export default WallPreview