diff --git a/client/src/components/Map3D.jsx b/client/src/components/Map3D.jsx
index 2368625..e64e585 100644
--- a/client/src/components/Map3D.jsx
+++ b/client/src/components/Map3D.jsx
@@ -541,23 +541,35 @@ function getBlockAppearance(blockName) {
return { color: '#666666', pattern: 'solid' };
}
-// WorldBlocks component to render discovered blocks with Minecraft textures
-function WorldBlocks({ blocks }) {
+// WorldBlocks component using chunked InstancedMesh rendering for performance
+function WorldBlocks({ blocks, interactionMode, selectedTurtleId, onBlockClick }) {
const [hoveredBlock, setHoveredBlock] = useState(null);
const { textures, multiFaceTextures, loading } = useMinecraftTextures(blocks);
- // Group blocks by block name for better performance
- const blockGroups = useMemo(() => {
- const groups = new Map();
+ // Chunk size (16x16 like Minecraft)
+ const CHUNK_SIZE = 16;
+
+ // Group blocks into chunks and by block type within each chunk
+ const chunks = useMemo(() => {
+ const chunkMap = new Map();
blocks.forEach(block => {
- if (!groups.has(block.name)) {
- groups.set(block.name, []);
+ const chunkX = Math.floor(block.x / CHUNK_SIZE);
+ const chunkZ = Math.floor(block.z / CHUNK_SIZE);
+ const chunkKey = `${chunkX},${chunkZ}`;
+
+ if (!chunkMap.has(chunkKey)) {
+ chunkMap.set(chunkKey, { key: chunkKey, chunkX, chunkZ, blocksByType: new Map() });
}
- groups.get(block.name).push(block);
+
+ const chunk = chunkMap.get(chunkKey);
+ if (!chunk.blocksByType.has(block.name)) {
+ chunk.blocksByType.set(block.name, []);
+ }
+ chunk.blocksByType.get(block.name).push(block);
});
- return Array.from(groups.entries());
+ return Array.from(chunkMap.values());
}, [blocks]);
if (loading) {
@@ -566,84 +578,224 @@ function WorldBlocks({ blocks }) {
return (
- {blockGroups.map(([blockName, blockList]) => {
- const appearance = getBlockAppearance(blockName);
- const texture = textures[blockName];
- const multiFace = multiFaceTextures[blockName];
-
- return (
-
- {blockList.map((block) => {
- const blockKey = `${block.x},${block.y},${block.z}`;
- const isHovered = hoveredBlock === blockKey;
-
- return (
-
- {
- e.stopPropagation();
- setHoveredBlock(blockKey);
- }}
- onPointerOut={() => setHoveredBlock(null)}
- >
-
- {multiFace ? (
- // Multi-face block (different top/side/bottom)
- // Box face order: +x, -x, +y, -y, +z, -z
- multiFace.map((faceTexture, i) => (
-
- ))
- ) : (
- // Single texture or color fallback
-
- )}
-
-
- {/* Block label on hover */}
- {isHovered && (
-
- {block.name.replace('minecraft:', '').replace(/_/g, ' ').toUpperCase()}
-
- )}
-
- );
- })}
-
- );
- })}
+ {chunks.map(chunk => (
+
+ ))}
+
+ {/* Hover label overlay */}
+ {hoveredBlock && (
+
+ {hoveredBlock.name.replace('minecraft:', '').replace(/_/g, ' ').toUpperCase()}
+ {'\n'}
+ {`(${hoveredBlock.x}, ${hoveredBlock.y}, ${hoveredBlock.z})`}
+
+ )}
);
}
+// Individual chunk rendered with InstancedMesh per block type
+function ChunkMesh({ chunk, textures, multiFaceTextures, hoveredBlock, setHoveredBlock, interactionMode, selectedTurtleId, onBlockClick }) {
+ return (
+
+ {Array.from(chunk.blocksByType.entries()).map(([blockName, blockList]) => (
+
+ ))}
+
+ );
+}
+
+// InstancedMesh for all blocks of one type within a chunk
+function BlockTypeInstances({ blockName, blockList, textures, multiFaceTextures, hoveredBlock, setHoveredBlock, interactionMode, selectedTurtleId, onBlockClick }) {
+ const meshRef = useRef();
+ const appearance = useMemo(() => getBlockAppearance(blockName), [blockName]);
+ const texture = textures[blockName];
+ const multiFace = multiFaceTextures[blockName];
+ const tempMatrix = useMemo(() => new THREE.Matrix4(), []);
+ const tempColor = useMemo(() => new THREE.Color(), []);
+
+ // Set instance matrices
+ useEffect(() => {
+ if (!meshRef.current) return;
+
+ blockList.forEach((block, i) => {
+ tempMatrix.setPosition(block.x, block.y, block.z);
+ meshRef.current.setMatrixAt(i, tempMatrix);
+ // Default color (white for textured, appearance color for untextured)
+ tempColor.set(texture || multiFace ? '#ffffff' : appearance.color);
+ meshRef.current.setColorAt(i, tempColor);
+ });
+
+ meshRef.current.instanceMatrix.needsUpdate = true;
+ if (meshRef.current.instanceColor) meshRef.current.instanceColor.needsUpdate = true;
+ }, [blockList, texture, multiFace, appearance, tempMatrix, tempColor]);
+
+ // Update hover highlighting
+ useEffect(() => {
+ if (!meshRef.current || !meshRef.current.instanceColor) return;
+
+ blockList.forEach((block, i) => {
+ const isHovered = hoveredBlock &&
+ hoveredBlock.x === block.x &&
+ hoveredBlock.y === block.y &&
+ hoveredBlock.z === block.z;
+
+ if (isHovered) {
+ tempColor.set('#ffffff');
+ meshRef.current.setColorAt(i, tempColor);
+ } else {
+ tempColor.set(texture || multiFace ? '#ffffff' : appearance.color);
+ meshRef.current.setColorAt(i, tempColor);
+ }
+ });
+ meshRef.current.instanceColor.needsUpdate = true;
+ }, [hoveredBlock, blockList, texture, multiFace, appearance, tempColor]);
+
+ const handlePointerMove = useCallback((e) => {
+ e.stopPropagation();
+ const instanceId = e.instanceId;
+ if (instanceId !== undefined && instanceId < blockList.length) {
+ setHoveredBlock(blockList[instanceId]);
+ }
+ }, [blockList, setHoveredBlock]);
+
+ const handlePointerOut = useCallback(() => {
+ setHoveredBlock(null);
+ }, [setHoveredBlock]);
+
+ const handleClick = useCallback((e) => {
+ e.stopPropagation();
+ const instanceId = e.instanceId;
+ if (instanceId !== undefined && instanceId < blockList.length) {
+ const block = blockList[instanceId];
+
+ if (interactionMode === INTERACTION_MODE.MOVE && selectedTurtleId && onBlockClick) {
+ // In MOVE mode, click block surface to send turtle to adjacent position
+ const normal = e.face?.normal;
+ const target = {
+ x: block.x + (normal?.x || 0),
+ y: block.y + (normal?.y || 0),
+ z: block.z + (normal?.z || 0)
+ };
+ onBlockClick({ type: 'move', target, block });
+ } else if (interactionMode === INTERACTION_MODE.BUILD && onBlockClick) {
+ const normal = e.face?.normal;
+ const target = {
+ x: block.x + (normal?.x || 0),
+ y: block.y + (normal?.y || 0),
+ z: block.z + (normal?.z || 0)
+ };
+ onBlockClick({ type: 'build', target, block });
+ } else if (interactionMode === INTERACTION_MODE.SELECT && onBlockClick) {
+ onBlockClick({ type: 'select', block });
+ }
+ }
+ }, [blockList, interactionMode, selectedTurtleId, onBlockClick]);
+
+ // For multi-face blocks, we can't easily use InstancedMesh with different material per face,
+ // so we fall back to individual meshes for those (they're fewer — grass, logs, etc.)
+ if (multiFace) {
+ return (
+
+ {blockList.map((block) => {
+ const blockKey = `${block.x},${block.y},${block.z}`;
+ const isHovered = hoveredBlock &&
+ hoveredBlock.x === block.x &&
+ hoveredBlock.y === block.y &&
+ hoveredBlock.z === block.z;
+ return (
+ { e.stopPropagation(); setHoveredBlock(block); }}
+ onPointerOut={() => setHoveredBlock(null)}
+ onClick={(e) => {
+ e.stopPropagation();
+ if (interactionMode === INTERACTION_MODE.MOVE && selectedTurtleId && onBlockClick) {
+ const normal = e.face?.normal;
+ onBlockClick({ type: 'move', target: { x: block.x + (normal?.x || 0), y: block.y + (normal?.y || 0), z: block.z + (normal?.z || 0) }, block });
+ } else if (interactionMode === INTERACTION_MODE.BUILD && onBlockClick) {
+ const normal = e.face?.normal;
+ onBlockClick({ type: 'build', target: { x: block.x + (normal?.x || 0), y: block.y + (normal?.y || 0), z: block.z + (normal?.z || 0) }, block });
+ } else if (interactionMode === INTERACTION_MODE.SELECT && onBlockClick) {
+ onBlockClick({ type: 'select', block });
+ }
+ }}
+ >
+
+ {multiFace.map((faceTexture, i) => (
+
+ ))}
+
+ );
+ })}
+
+ );
+ }
+
+ return (
+
+
+
+
+ );
+}
+
// Path trail component (old version removed above)
function OldPathTrail({ turtle }) {
const { position, homePosition } = turtle;