zones as nodes
This commit is contained in:
@@ -1,14 +1,12 @@
|
||||
import { type LevelNode, useRegistry, useScene } from "@pascal-app/core";
|
||||
import { useRef } from "react";
|
||||
import type { Group } from "three";
|
||||
import { useShallow } from "zustand/shallow";
|
||||
import { NodeRenderer } from "../node-renderer";
|
||||
import { ZoneRenderer } from "../zone/zone-renderer";
|
||||
import { type LevelNode, useRegistry } from '@pascal-app/core'
|
||||
import { useRef } from 'react'
|
||||
import type { Group } from 'three'
|
||||
import { NodeRenderer } from '../node-renderer'
|
||||
|
||||
export const LevelRenderer = ({ node }: { node: LevelNode }) => {
|
||||
const ref = useRef<Group>(null!);
|
||||
const ref = useRef<Group>(null!)
|
||||
|
||||
useRegistry(node.id, node.type, ref);
|
||||
useRegistry(node.id, node.type, ref)
|
||||
|
||||
return (
|
||||
<group ref={ref}>
|
||||
@@ -19,17 +17,6 @@ export const LevelRenderer = ({ node }: { node: LevelNode }) => {
|
||||
{node.children.map((childId) => (
|
||||
<NodeRenderer key={childId} nodeId={childId} />
|
||||
))}
|
||||
<LevelZones levelId={node.id} />
|
||||
</group>
|
||||
);
|
||||
};
|
||||
|
||||
const LevelZones = ({ levelId }: { levelId: LevelNode["id"] }) => {
|
||||
const zoneIds = useScene(
|
||||
useShallow((s) =>
|
||||
s.zoneIds.filter((id) => s.zones[id]?.levelId === levelId),
|
||||
),
|
||||
);
|
||||
|
||||
return zoneIds.map((zoneId) => <ZoneRenderer key={zoneId} zoneId={zoneId} />);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { BuildingRenderer } from './building/building-renderer'
|
||||
import { ItemRenderer } from './item/item-renderer'
|
||||
import { LevelRenderer } from './level/level-renderer'
|
||||
import { WallRenderer } from './wall/wall-renderer'
|
||||
import { ZoneRenderer } from './zone/zone-renderer'
|
||||
|
||||
export const NodeRenderer = ({ nodeId }: { nodeId: AnyNode['id'] }) => {
|
||||
const node = useScene((state) => state.nodes[nodeId])
|
||||
@@ -17,6 +18,7 @@ export const NodeRenderer = ({ nodeId }: { nodeId: AnyNode['id'] }) => {
|
||||
{node.type === 'level' && <LevelRenderer node={node} />}
|
||||
{node.type === 'item' && <ItemRenderer node={node} />}
|
||||
{node.type === 'wall' && <WallRenderer node={node} />}
|
||||
{node.type === 'zone' && <ZoneRenderer node={node} />}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,33 +1,25 @@
|
||||
import { emitter, useRegistry, useScene, type Zone } from "@pascal-app/core";
|
||||
import { useRegistry, type ZoneNode } from '@pascal-app/core'
|
||||
import { useMemo, useRef } from 'react'
|
||||
import { BufferGeometry, Color, DoubleSide, Float32BufferAttribute, type Group, Shape } from 'three'
|
||||
import { color, float, uv } from 'three/tsl'
|
||||
import { MeshBasicNodeMaterial } from 'three/webgpu'
|
||||
import { useNodeEvents } from '../../../hooks/use-node-events'
|
||||
|
||||
import type { ThreeEvent } from "@react-three/fiber";
|
||||
import { useCallback, useMemo, useRef } from "react";
|
||||
import {
|
||||
BufferGeometry,
|
||||
Color,
|
||||
DoubleSide,
|
||||
Float32BufferAttribute,
|
||||
type Group,
|
||||
Shape,
|
||||
} from "three";
|
||||
import { color, float, uv } from "three/tsl";
|
||||
import { MeshBasicNodeMaterial } from "three/webgpu";
|
||||
|
||||
const Y_OFFSET = 0.01;
|
||||
const WALL_HEIGHT = 2.3;
|
||||
const Y_OFFSET = 0.01
|
||||
const WALL_HEIGHT = 2.3
|
||||
|
||||
/**
|
||||
* Creates a gradient material for zone walls using TSL
|
||||
* Gradient goes from zone color at bottom to transparent at top
|
||||
*/
|
||||
const createWallGradientMaterial = (zoneColor: string) => {
|
||||
const baseColor = color(new Color(zoneColor));
|
||||
const baseColor = color(new Color(zoneColor))
|
||||
|
||||
// Use UV y coordinate for vertical gradient (0 at bottom, 1 at top)
|
||||
const gradientT = uv().y;
|
||||
const gradientT = uv().y
|
||||
|
||||
// Fade opacity from 0.6 at bottom to 0 at top
|
||||
const opacity = float(0.6).mul(float(1).sub(gradientT));
|
||||
const opacity = float(0.6).mul(float(1).sub(gradientT))
|
||||
|
||||
return new MeshBasicNodeMaterial({
|
||||
transparent: true,
|
||||
@@ -35,14 +27,14 @@ const createWallGradientMaterial = (zoneColor: string) => {
|
||||
opacityNode: opacity,
|
||||
side: DoubleSide,
|
||||
depthWrite: false,
|
||||
});
|
||||
};
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a floor material for zones using TSL
|
||||
*/
|
||||
const createFloorMaterial = (zoneColor: string) => {
|
||||
const baseColor = color(new Color(zoneColor));
|
||||
const baseColor = color(new Color(zoneColor))
|
||||
|
||||
return new MeshBasicNodeMaterial({
|
||||
transparent: true,
|
||||
@@ -50,150 +42,114 @@ const createFloorMaterial = (zoneColor: string) => {
|
||||
opacityNode: float(0.15),
|
||||
side: DoubleSide,
|
||||
depthWrite: false,
|
||||
});
|
||||
};
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates wall geometry for zone borders
|
||||
* Each wall segment is a vertical quad from one polygon point to the next
|
||||
*/
|
||||
const createWallGeometry = (
|
||||
polygon: Array<[number, number]>,
|
||||
): BufferGeometry => {
|
||||
const geometry = new BufferGeometry();
|
||||
const createWallGeometry = (polygon: Array<[number, number]>): BufferGeometry => {
|
||||
const geometry = new BufferGeometry()
|
||||
|
||||
if (polygon.length < 2) return geometry;
|
||||
if (polygon.length < 2) return geometry
|
||||
|
||||
const positions: number[] = [];
|
||||
const uvs: number[] = [];
|
||||
const indices: number[] = [];
|
||||
const positions: number[] = []
|
||||
const uvs: number[] = []
|
||||
const indices: number[] = []
|
||||
|
||||
// Create a wall segment for each edge of the polygon
|
||||
for (let i = 0; i < polygon.length; i++) {
|
||||
const current = polygon[i]!;
|
||||
const next = polygon[(i + 1) % polygon.length]!;
|
||||
const current = polygon[i]!
|
||||
const next = polygon[(i + 1) % polygon.length]!
|
||||
|
||||
const baseIndex = i * 4;
|
||||
const baseIndex = i * 4
|
||||
|
||||
// Four vertices per wall segment (two triangles forming a quad)
|
||||
// Bottom-left
|
||||
positions.push(current[0]!, Y_OFFSET, current[1]!);
|
||||
uvs.push(0, 0);
|
||||
positions.push(current[0]!, Y_OFFSET, current[1]!)
|
||||
uvs.push(0, 0)
|
||||
|
||||
// Bottom-right
|
||||
positions.push(next[0]!, Y_OFFSET, next[1]!);
|
||||
uvs.push(1, 0);
|
||||
positions.push(next[0]!, Y_OFFSET, next[1]!)
|
||||
uvs.push(1, 0)
|
||||
|
||||
// Top-right
|
||||
positions.push(next[0]!, Y_OFFSET + WALL_HEIGHT, next[1]!);
|
||||
uvs.push(1, 1);
|
||||
positions.push(next[0]!, Y_OFFSET + WALL_HEIGHT, next[1]!)
|
||||
uvs.push(1, 1)
|
||||
|
||||
// Top-left
|
||||
positions.push(current[0]!, Y_OFFSET + WALL_HEIGHT, current[1]!);
|
||||
uvs.push(0, 1);
|
||||
positions.push(current[0]!, Y_OFFSET + WALL_HEIGHT, current[1]!)
|
||||
uvs.push(0, 1)
|
||||
|
||||
// Two triangles for the quad
|
||||
indices.push(
|
||||
baseIndex,
|
||||
baseIndex + 1,
|
||||
baseIndex + 2,
|
||||
baseIndex,
|
||||
baseIndex + 2,
|
||||
baseIndex + 3,
|
||||
);
|
||||
indices.push(baseIndex, baseIndex + 1, baseIndex + 2, baseIndex, baseIndex + 2, baseIndex + 3)
|
||||
}
|
||||
|
||||
geometry.setAttribute("position", new Float32BufferAttribute(positions, 3));
|
||||
geometry.setAttribute("uv", new Float32BufferAttribute(uvs, 2));
|
||||
geometry.setIndex(indices);
|
||||
geometry.computeVertexNormals();
|
||||
geometry.setAttribute('position', new Float32BufferAttribute(positions, 3))
|
||||
geometry.setAttribute('uv', new Float32BufferAttribute(uvs, 2))
|
||||
geometry.setIndex(indices)
|
||||
geometry.computeVertexNormals()
|
||||
|
||||
return geometry;
|
||||
};
|
||||
return geometry
|
||||
}
|
||||
|
||||
export const ZoneRenderer = ({ zoneId }: { zoneId: Zone["id"] }) => {
|
||||
const ref = useRef<Group>(null!);
|
||||
const zone = useScene((state) => state.zones[zoneId]);
|
||||
useRegistry(zoneId, "zone", ref);
|
||||
export const ZoneRenderer = ({ node }: { node: ZoneNode }) => {
|
||||
const ref = useRef<Group>(null!)
|
||||
|
||||
useRegistry(node.id, 'zone', ref)
|
||||
|
||||
// Create floor shape from polygon
|
||||
const floorShape = useMemo(() => {
|
||||
if (!zone?.polygon || zone.polygon.length < 3) return null;
|
||||
|
||||
const shape = new Shape();
|
||||
const firstPt = zone.polygon[0]!;
|
||||
if (!node?.polygon || node.polygon.length < 3) return null
|
||||
const shape = new Shape()
|
||||
const firstPt = node.polygon[0]!
|
||||
|
||||
// Shape is in X-Y plane, we rotate it to X-Z plane
|
||||
// Negate Y (which becomes Z) to get correct orientation
|
||||
shape.moveTo(firstPt[0]!, -firstPt[1]!);
|
||||
shape.moveTo(firstPt[0]!, -firstPt[1]!)
|
||||
|
||||
for (let i = 1; i < zone.polygon.length; i++) {
|
||||
const pt = zone.polygon[i]!;
|
||||
shape.lineTo(pt[0]!, -pt[1]!);
|
||||
for (let i = 1; i < node.polygon.length; i++) {
|
||||
const pt = node.polygon[i]!
|
||||
shape.lineTo(pt[0]!, -pt[1]!)
|
||||
}
|
||||
shape.closePath();
|
||||
shape.closePath()
|
||||
|
||||
return shape;
|
||||
}, [zone?.polygon]);
|
||||
return shape
|
||||
}, [node?.polygon])
|
||||
|
||||
// Create wall geometry from polygon
|
||||
const wallGeometry = useMemo(() => {
|
||||
if (!zone?.polygon || zone.polygon.length < 2) return null;
|
||||
return createWallGeometry(zone.polygon);
|
||||
}, [zone?.polygon]);
|
||||
if (!node?.polygon || node.polygon.length < 2) return null
|
||||
return createWallGeometry(node.polygon)
|
||||
}, [node?.polygon])
|
||||
|
||||
// Create materials
|
||||
const floorMaterial = useMemo(() => {
|
||||
if (!zone?.color) return null;
|
||||
return createFloorMaterial(zone.color);
|
||||
}, [zone?.color]);
|
||||
if (!node?.color) return null
|
||||
return createFloorMaterial(node.color)
|
||||
}, [node?.color])
|
||||
|
||||
const wallMaterial = useMemo(() => {
|
||||
if (!zone?.color) return null;
|
||||
return createWallGradientMaterial(zone.color);
|
||||
}, [zone?.color]);
|
||||
if (!node?.color) return null
|
||||
return createWallGradientMaterial(node.color)
|
||||
}, [node?.color])
|
||||
|
||||
if (
|
||||
!zone ||
|
||||
!floorShape ||
|
||||
!wallGeometry ||
|
||||
!floorMaterial ||
|
||||
!wallMaterial
|
||||
) {
|
||||
return null;
|
||||
const handlers = useNodeEvents(node, 'zone')
|
||||
|
||||
if (!node || !floorShape || !wallGeometry || !floorMaterial || !wallMaterial) {
|
||||
return null
|
||||
}
|
||||
|
||||
const emitZoneEvent = useCallback(
|
||||
(suffix: string, e: ThreeEvent<PointerEvent>) => {
|
||||
const eventKey = `zone:${suffix}` as `zone:${typeof suffix}`;
|
||||
emitter.emit(eventKey, {
|
||||
zone,
|
||||
position: [e.point.x, e.point.y, e.point.z],
|
||||
stopPropagation: () => e.stopPropagation(),
|
||||
nativeEvent: e,
|
||||
});
|
||||
},
|
||||
[zone]
|
||||
);
|
||||
|
||||
return (
|
||||
<group
|
||||
ref={ref}
|
||||
onClick={(e) => emitZoneEvent("click", e)}
|
||||
onPointerEnter={(e) => emitZoneEvent("enter", e)}
|
||||
onPointerLeave={(e) => emitZoneEvent("leave", e)}
|
||||
>
|
||||
<group ref={ref} {...handlers}>
|
||||
{/* Floor fill */}
|
||||
<mesh
|
||||
position={[0, Y_OFFSET, 0]}
|
||||
rotation={[-Math.PI / 2, 0, 0]}
|
||||
material={floorMaterial}
|
||||
>
|
||||
<mesh position={[0, Y_OFFSET, 0]} rotation={[-Math.PI / 2, 0, 0]} material={floorMaterial}>
|
||||
<shapeGeometry args={[floorShape]} />
|
||||
</mesh>
|
||||
|
||||
{/* Wall borders with gradient */}
|
||||
<mesh geometry={wallGeometry} material={wallMaterial} />
|
||||
</group>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
type BuildingEvent,
|
||||
type BuildingNode,
|
||||
type EventSuffix,
|
||||
emitter,
|
||||
@@ -6,56 +7,53 @@ import {
|
||||
type ItemNode,
|
||||
type WallEvent,
|
||||
type WallNode,
|
||||
} from "@pascal-app/core";
|
||||
import type { ThreeEvent } from "@react-three/fiber";
|
||||
import type { BuildingEvent } from "../../../core/src/events/bus";
|
||||
type ZoneEvent,
|
||||
type ZoneNode,
|
||||
} from '@pascal-app/core'
|
||||
import type { ThreeEvent } from '@react-three/fiber'
|
||||
|
||||
type NodeConfig = {
|
||||
item: { node: ItemNode; event: ItemEvent };
|
||||
wall: { node: WallNode; event: WallEvent };
|
||||
building: { node: BuildingNode; event: BuildingEvent };
|
||||
};
|
||||
item: { node: ItemNode; event: ItemEvent }
|
||||
wall: { node: WallNode; event: WallEvent }
|
||||
building: { node: BuildingNode; event: BuildingEvent }
|
||||
zone: { node: ZoneNode; event: ZoneEvent }
|
||||
}
|
||||
|
||||
type NodeType = keyof NodeConfig;
|
||||
type NodeType = keyof NodeConfig
|
||||
|
||||
export function useNodeEvents<T extends NodeType>(
|
||||
node: NodeConfig[T]["node"],
|
||||
type: T,
|
||||
) {
|
||||
export function useNodeEvents<T extends NodeType>(node: NodeConfig[T]['node'], type: T) {
|
||||
const emit = (suffix: EventSuffix, e: ThreeEvent<PointerEvent>) => {
|
||||
const eventKey = `${type}:${suffix}` as `${T}:${EventSuffix}`;
|
||||
const localPoint = e.object.worldToLocal(e.point.clone());
|
||||
const eventKey = `${type}:${suffix}` as `${T}:${EventSuffix}`
|
||||
const localPoint = e.object.worldToLocal(e.point.clone())
|
||||
const payload = {
|
||||
node,
|
||||
position: [e.point.x, e.point.y, e.point.z],
|
||||
localPosition: [localPoint.x, localPoint.y, localPoint.z],
|
||||
normal: e.face
|
||||
? [e.face.normal.x, e.face.normal.y, e.face.normal.z]
|
||||
: undefined,
|
||||
normal: e.face ? [e.face.normal.x, e.face.normal.y, e.face.normal.z] : undefined,
|
||||
stopPropagation: () => e.stopPropagation(),
|
||||
nativeEvent: e,
|
||||
} as NodeConfig[T]["event"];
|
||||
} as NodeConfig[T]['event']
|
||||
|
||||
emitter.emit(eventKey, payload);
|
||||
};
|
||||
emitter.emit(eventKey, payload)
|
||||
}
|
||||
|
||||
return {
|
||||
onPointerDown: (e: ThreeEvent<PointerEvent>) => {
|
||||
if (e.button !== 0) return;
|
||||
emit("pointerdown", e);
|
||||
if (e.button !== 0) return
|
||||
emit('pointerdown', e)
|
||||
},
|
||||
onPointerUp: (e: ThreeEvent<PointerEvent>) => {
|
||||
if (e.button !== 0) return;
|
||||
emit("pointerup", e);
|
||||
if (e.button !== 0) return
|
||||
emit('pointerup', e)
|
||||
},
|
||||
onClick: (e: ThreeEvent<PointerEvent>) => {
|
||||
if (e.button !== 0) return;
|
||||
emit("click", e);
|
||||
if (e.button !== 0) return
|
||||
emit('click', e)
|
||||
},
|
||||
onPointerEnter: (e: ThreeEvent<PointerEvent>) => emit("enter", e),
|
||||
onPointerLeave: (e: ThreeEvent<PointerEvent>) => emit("leave", e),
|
||||
onPointerMove: (e: ThreeEvent<PointerEvent>) => emit("move", e),
|
||||
onDoubleClick: (e: ThreeEvent<PointerEvent>) => emit("double-click", e),
|
||||
onContextMenu: (e: ThreeEvent<PointerEvent>) => emit("context-menu", e),
|
||||
};
|
||||
onPointerEnter: (e: ThreeEvent<PointerEvent>) => emit('enter', e),
|
||||
onPointerLeave: (e: ThreeEvent<PointerEvent>) => emit('leave', e),
|
||||
onPointerMove: (e: ThreeEvent<PointerEvent>) => emit('move', e),
|
||||
onDoubleClick: (e: ThreeEvent<PointerEvent>) => emit('double-click', e),
|
||||
onContextMenu: (e: ThreeEvent<PointerEvent>) => emit('context-menu', e),
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user