Skip to content

Commit

Permalink
feat: TurtleGraphicsControllerのUIを改善する (#110)
Browse files Browse the repository at this point in the history
  • Loading branch information
hishiwat authored Sep 16, 2024
1 parent 52c3dc4 commit ea380b8
Show file tree
Hide file tree
Showing 5 changed files with 128 additions and 131 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,6 @@ export const dirCharToJapanese = {
W: '左',
};

const penStateJapanese = (penDown: boolean): string => {
return penDown ? 'おいている' : 'あげている';
};

export const Variables: React.FC<VariablesProps> = ({ traceItemVars }) => {
const characterVars = [];
const otherVars = [];
Expand All @@ -51,7 +47,6 @@ export const Variables: React.FC<VariablesProps> = ({ traceItemVars }) => {
<Th>変数名</Th>
<Th>線の色</Th>
<Th>向き</Th>
<Th>ペンの状態</Th>
</Tr>
</Thead>
<Tbody>
Expand All @@ -62,7 +57,6 @@ export const Variables: React.FC<VariablesProps> = ({ traceItemVars }) => {
<Box bg={charToColor[variable.value.color as keyof typeof charToColor]} h="20px" w="20px" />
</Td>
<Td>{dirCharToJapanese[variable.value.dir as keyof typeof dirCharToJapanese]}</Td>
<Td>{penStateJapanese(variable.value.pen)}</Td>
</Tr>
))}
</Tbody>
Expand Down
56 changes: 35 additions & 21 deletions src/components/molecules/TurtleGraphicsController.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
'use client';

import React from 'react';
import { FaArrowRotateRight, FaArrowRotateLeft, FaTrashCan } from 'react-icons/fa6';

import { Box, Button, HStack, VStack } from '../../infrastructures/useClient/chakra';
import { TURTLE_GRAPHICS_GRID_ROWS as GRID_ROWS, TURTLE_GRAPHICS_GRID_SIZE as GRID_SIZE } from '../../constants';
import { Box, Button, HStack, IconButton, VStack } from '../../infrastructures/useClient/chakra';
import type { CharacterTrace } from '../../problems/traceProgram';
import type { SelectedCell } from '../../types';

Expand All @@ -15,51 +17,63 @@ interface TurtleGraphicsControllerProps {
handleClickCharacterTurnRightButton: () => void;
handleClickCharacterMoveForwardButton: () => void;
handleClickCharacterMoveBackwardButton: () => void;
handleClickCharacterPenUpButton: () => void;
handleClickCharacterPenDownButton: () => void;
}

export const TurtleGraphicsController: React.FC<TurtleGraphicsControllerProps> = ({
handleAddCharacterButton,
handleClickCharacterMoveBackwardButton,
handleClickCharacterMoveForwardButton,
handleClickCharacterPenDownButton,
handleClickCharacterPenUpButton,
handleClickCharacterTurnLeftButton,
handleClickCharacterTurnRightButton,
handleRemoveCharacterButton,
selectedCell,
selectedCharacter,
}) => {
return (
<VStack justifyContent="center" marginTop="4" spacing="4">
<VStack align="center" justifyContent="center" marginTop="4" spacing="4" zIndex="10">
{selectedCharacter && (
<>
<VStack
alignItems="center"
left={selectedCharacter.x * GRID_SIZE + GRID_SIZE / 2 + 'px'}
position="absolute"
top={(GRID_ROWS - selectedCharacter.y) * GRID_SIZE + GRID_SIZE / 4 + 'px'}
transform="translate(-50%, 0%)"
>
<HStack>
<Button onClick={() => handleClickCharacterTurnLeftButton()}></Button>
<Button onClick={() => handleClickCharacterTurnRightButton()}></Button>
<IconButton
aria-label="Turn Left"
icon={<FaArrowRotateLeft />}
onClick={() => handleClickCharacterTurnLeftButton()}
/>
<IconButton
aria-label="Turn Right"
icon={<FaArrowRotateRight />}
onClick={() => handleClickCharacterTurnRightButton()}
/>
</HStack>
<HStack>
<Button onClick={() => handleClickCharacterMoveForwardButton()}>前に進む</Button>
<Button onClick={() => handleClickCharacterMoveBackwardButton()}>後ろに戻る</Button>
</HStack>
<HStack>
<Button border={selectedCharacter.pen ? '' : '1px'} onClick={() => handleClickCharacterPenUpButton()}>
ペンを上げる
</Button>
<Button border={selectedCharacter.pen ? '1px' : ''} onClick={() => handleClickCharacterPenDownButton()}>
ペンを下ろす
</Button>
<Button onClick={() => handleClickCharacterMoveBackwardButton()}>後に戻る</Button>
</HStack>
<Box>
<Button onClick={() => handleRemoveCharacterButton(selectedCharacter)}>削除する</Button>
<IconButton
aria-label="Remove"
colorScheme="red"
icon={<FaTrashCan />}
onClick={() => handleRemoveCharacterButton(selectedCharacter)}
/>
</Box>
</>
</VStack>
)}

{selectedCell && (
<HStack>
<Box>
<Box
left={selectedCell.x * GRID_SIZE + GRID_SIZE / 2 + 'px'}
position="absolute"
top={(GRID_ROWS - selectedCell.y) * GRID_SIZE + GRID_SIZE / 4 + 'px'}
transform="translate(-50%, 0%)"
>
<Button onClick={() => handleAddCharacterButton()}>キャラクターを追加する</Button>
</Box>
</HStack>
Expand Down
170 changes: 87 additions & 83 deletions src/components/organisms/TurtleGraphics.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
'use client';

import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useState } from 'react';
import { FaArrowRotateLeft, FaArrowRotateRight, FaTrashCan } from 'react-icons/fa6';

import {
TURTLE_GRAPHICS_DEFAULT_COLOR as DEFAULT_COLOR,
TURTLE_GRAPHICS_GRID_COLUMNS as GRID_COLUMNS,
TURTLE_GRAPHICS_GRID_ROWS as GRID_ROWS,
TURTLE_GRAPHICS_GRID_SIZE as GRID_SIZE,
} from '../../constants';
import { Box, Grid, GridItem, Image } from '../../infrastructures/useClient/chakra';
import { Box, Button, Grid, GridItem, HStack, IconButton, Image, VStack } from '../../infrastructures/useClient/chakra';
import type { Problem } from '../../problems/generateProblem';
import { type CharacterTrace, charToColor, type TraceItem } from '../../problems/traceProgram';
import type { ColorChar, SelectedCell } from '../../types';
Expand Down Expand Up @@ -148,9 +149,7 @@ export const TurtleGraphics = forwardRef<TurtleGraphicsHandle, TurtleGraphicsPro
}

const updatedCharacter = { ...selectedCharacter, x: updatedX, y: updatedY };
if (selectedCharacter.pen) {
updateCellColor(updatedCharacter.color as ColorChar, updatedCharacter.x, updatedCharacter.y);
}
updateCellColor(updatedCharacter.color as ColorChar, updatedCharacter.x, updatedCharacter.y);
updateCharacters(updatedCharacter);
};

Expand All @@ -166,9 +165,7 @@ export const TurtleGraphics = forwardRef<TurtleGraphicsHandle, TurtleGraphicsPro
}

const updatedCharacter = { ...selectedCharacter, x: updatedX, y: updatedY };
if (selectedCharacter.pen) {
updateCellColor('.' as ColorChar, selectedCharacter.x, selectedCharacter.y);
}
updateCellColor('.' as ColorChar, selectedCharacter.x, selectedCharacter.y);
updateCharacters(updatedCharacter);
};

Expand All @@ -192,21 +189,6 @@ export const TurtleGraphics = forwardRef<TurtleGraphicsHandle, TurtleGraphicsPro
updateCharacters(updatedCharacter);
};

const handleClickCharacterPenUpButton = (): void => {
if (!selectedCharacter) return;

selectedCharacter.pen = false;
updateCharacters(selectedCharacter);
};

const handleClickCharacterPenDownButton = (): void => {
if (!selectedCharacter) return;

selectedCharacter.pen = true;
updateCellColor(selectedCharacter.color as ColorChar, selectedCharacter.x, selectedCharacter.y);
updateCharacters(selectedCharacter);
};

const handleAddCharacterButton = (): void => {
if (!selectedCell) return;

Expand All @@ -215,7 +197,6 @@ export const TurtleGraphics = forwardRef<TurtleGraphicsHandle, TurtleGraphicsPro
y: selectedCell.y,
color: DEFAULT_COLOR,
dir: 'N',
pen: true,
};

setCharacters((prevCharacters) => [...prevCharacters, newTurtle]);
Expand Down Expand Up @@ -248,67 +229,90 @@ export const TurtleGraphics = forwardRef<TurtleGraphicsHandle, TurtleGraphicsPro
};

return (
<Box className="turtle-graphics-container">
<Grid
position="relative"
templateColumns={`repeat(${GRID_COLUMNS}, ${GRID_SIZE}px)`}
templateRows={`repeat(${GRID_ROWS}, ${GRID_SIZE}px)`}
>
{[...board]
.reverse()
.map((columns, rowIndex) =>
columns.map((color, columnIndex) => (
<GridItem
key={columnIndex}
backgroundColor={charToColor[color]}
borderColor="black"
borderWidth={
selectedCell?.x === columnIndex && selectedCell?.y === GRID_ROWS - rowIndex - 1 ? '2px' : '0.5px'
}
className="grid-cell"
onClick={() => handleClickCell(columnIndex, GRID_ROWS - rowIndex - 1)}
onContextMenu={(e) => handleContextMenu(e, columnIndex, GRID_ROWS - rowIndex - 1)}
/>
))
)}
{characters.map((character) => (
<Box
key={'character' + character.x + character.y}
borderColor={selectedCharacter?.color === character.color ? 'black' : 'transparent'}
borderWidth="2px"
bottom={character.y * GRID_SIZE + 'px'}
h={GRID_SIZE + 'px'}
left={character.x * GRID_SIZE + 'px'}
position="absolute"
w={GRID_SIZE + 'px'}
onClick={() => handleClickCharacter(character)}
onContextMenu={(e) => handleContextMenu(e, character.x, character.y)}
>
<Box p="0.2rem" transform={charToRotateStyle[character.dir as keyof typeof charToRotateStyle]}>
<Image
alt={'character' + character.x + character.y}
src={`/character/${charToColor[character.color as keyof typeof charToColor]}.png`}
width={GRID_SIZE}
/>
</Box>
</Box>
))}
</Grid>
{isEnableOperation && (
<TurtleGraphicsController
handleAddCharacterButton={handleAddCharacterButton}
handleClickCharacterMoveBackwardButton={handleClickCharacterMoveBackwardButton}
handleClickCharacterMoveForwardButton={handleClickCharacterMoveForwardButton}
handleClickCharacterPenDownButton={handleClickCharacterPenDownButton}
handleClickCharacterPenUpButton={handleClickCharacterPenUpButton}
handleClickCharacterTurnLeftButton={handleClickCharacterTurnLeftButton}
handleClickCharacterTurnRightButton={handleClickCharacterTurnRightButton}
handleRemoveCharacterButton={handleRemoveCharacterButton}
selectedCell={selectedCell}
selectedCharacter={selectedCharacter}
/>
<VStack>
{selectedCharacter && (
<HStack>
<IconButton
aria-label="Turn Left"
icon={<FaArrowRotateLeft />}
onClick={() => handleClickCharacterTurnLeftButton()}
/>
<IconButton
aria-label="Turn Right"
icon={<FaArrowRotateRight />}
onClick={() => handleClickCharacterTurnRightButton()}
/>
<Button onClick={() => handleClickCharacterMoveForwardButton()}>前に進む</Button>
<Button onClick={() => handleClickCharacterMoveBackwardButton()}>後に戻る</Button>
{/* <Button onClick={() => handleRemoveCharacterButton(selectedCharacter)}>削除する</Button> */}
<IconButton
aria-label="Remove"
colorScheme="red"
icon={<FaTrashCan />}
onClick={() => handleRemoveCharacterButton(selectedCharacter)}
/>
</HStack>
)}
</Box>
<Box className="turtle-graphics-container">
<Grid
position="relative"
templateColumns={`repeat(${GRID_COLUMNS}, ${GRID_SIZE}px)`}
templateRows={`repeat(${GRID_ROWS}, ${GRID_SIZE}px)`}
>
{[...board]
.reverse()
.map((columns, rowIndex) =>
columns.map((color, columnIndex) => (
<GridItem
key={columnIndex}
backgroundColor={charToColor[color]}
borderColor="black"
borderWidth={
selectedCell?.x === columnIndex && selectedCell?.y === GRID_ROWS - rowIndex - 1 ? '2px' : '0.5px'
}
className="grid-cell"
onClick={() => handleClickCell(columnIndex, GRID_ROWS - rowIndex - 1)}
onContextMenu={(e) => handleContextMenu(e, columnIndex, GRID_ROWS - rowIndex - 1)}
/>
))
)}
{characters.map((character) => (
<Box
key={'character' + character.x + character.y}
borderColor={selectedCharacter?.color === character.color ? 'black' : 'transparent'}
borderWidth="2px"
h={GRID_SIZE + 'px'}
left={character.x * GRID_SIZE + 'px'}
position="absolute"
top={(GRID_ROWS - character.y - 1) * GRID_SIZE + 'px'}
w={GRID_SIZE + 'px'}
onClick={() => handleClickCharacter(character)}
onContextMenu={(e) => handleContextMenu(e, character.x, character.y)}
>
<Box p="0.2rem" transform={charToRotateStyle[character.dir as keyof typeof charToRotateStyle]}>
<Image
alt={'character' + character.x + character.y}
src={`/character/${charToColor[character.color as keyof typeof charToColor]}.png`}
width={GRID_SIZE}
/>
</Box>
</Box>
))}
{isEnableOperation && (
<TurtleGraphicsController
handleAddCharacterButton={handleAddCharacterButton}
handleClickCharacterMoveBackwardButton={handleClickCharacterMoveBackwardButton}
handleClickCharacterMoveForwardButton={handleClickCharacterMoveForwardButton}
handleClickCharacterTurnLeftButton={handleClickCharacterTurnLeftButton}
handleClickCharacterTurnRightButton={handleClickCharacterTurnRightButton}
handleRemoveCharacterButton={handleRemoveCharacterButton}
selectedCell={selectedCell}
selectedCharacter={selectedCharacter}
/>
)}
</Grid>
</Box>
</VStack>
);
}
);
Expand Down
26 changes: 6 additions & 20 deletions src/problems/traceProgram.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@ export interface CharacterTrace {
color: string;
/** 方向を表現する1文字 */
dir: string;
/** ペンが床に触れているか否か */
pen: boolean;
}

export interface TraceItem {
Expand Down Expand Up @@ -61,14 +59,11 @@ export function traceProgram(instrumented: string, rawDisplayProgram: string, la
let replaced = false;
const newLine = line
.replace(/for\s*\(([^;]*);\s*([^;]*);/, (_, init, cond) => `for (${init}; checkForCond(${cond}, ${statementId});`)
.replaceAll(
/\.(set|forward|penDown|penUp|turnRight|turnLeft)\(([^\n;]*)\)(;|\)\s*{)/g,
(_, methodName, args, tail) => {
replaced = true;
const delimiter = args === '' ? '' : ', ';
return `.${methodName}(${args}${delimiter}${statementId})${tail}`;
}
)
.replaceAll(/\.(set|forward|turnRight|turnLeft)\(([^\n;]*)\)(;|\)\s*{)/g, (_, methodName, args, tail) => {
replaced = true;
const delimiter = args === '' ? '' : ', ';
return `.${methodName}(${args}${delimiter}${statementId})${tail}`;
})
.replace(/\/\/\s*CP.*/, () => {
checkpointSids.push(statementId);
return '';
Expand Down Expand Up @@ -126,7 +121,6 @@ class Character {
this.y = y;
this.color = color;
this.dir = 'N';
this.pen = true;
board[this.y][this.x] = this.color;
}
forward(sid) {
Expand All @@ -136,15 +130,7 @@ class Character {
if (this.x < 0 || ${GRID_COLUMNS} <= this.x || this.y < 0 || ${GRID_ROWS} <= this.y) {
throw new Error('Out of bounds');
}
if (this.pen) board[this.y][this.x] = this.color;
addTrace(sid);
}
penDown(sid) {
this.pen = true;
addTrace(sid);
}
penUp(sid) {
this.pen = false;
board[this.y][this.x] = this.color;
addTrace(sid);
}
turnRight(sid) {
Expand Down
Loading

0 comments on commit ea380b8

Please sign in to comment.