feat(rithmomachia): use actual piece SVGs in number bond with 2.5s rotation animation
Completely revamps the number bond visualization to match user specifications: **Number Bond Layout:** - Operands (mover + helper) positioned at TOP (left and right) - Result (target) positioned at BOTTOM center - Operator symbol displayed prominently in center - Triangle lines connect all three pieces - Uses actual PieceRenderer SVG components instead of circles **2.5 Second Capture Animation:** When user clicks "✓ Capture" button: - All three pieces begin rotating around center point - Rotation accelerates to near-infinite speed (20π radians = 10 full rotations) - Pieces spiral inward (radius collapses to 0) - Helper and target pieces fade out (opacity → 0) - Mover piece remains visible at center - Animation duration: exactly 2.5 seconds - After animation completes, capture is executed via onRest callback **Technical Implementation:** - Two-phase animation system: - Entrance: scale from 0 with spring physics - Capture: duration-based rotation/collapse with configurable easing - Each piece offset by 120° (2π/3) for balanced rotation - Distance calculation: `spacing * 0.7 * radius` for smooth spiral - Mover stays at opacity 1, helper/target fade during animation - Lines and operator hide during animation for clean visual **State Changes:** - Updated selectedHelper state to store full Piece objects - Simplified handleHelperSelection (no value extraction) - Updated handleNumberBondConfirm to use piece.id references - Render section passes pieces instead of primitive values This creates a dramatic, mathematically educational capture animation that clearly shows the relationship before executing the capture. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
ece4160aa0
commit
976a7de949
|
|
@ -609,35 +609,30 @@ function HelperSelectionOptions({
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Number Bond Triangle Visualization - shows the mathematical relationship
|
* Number Bond Visualization - shows mathematical relationship with actual piece SVGs
|
||||||
|
* Layout: Operands (mover + helper) at top, result (target) at bottom
|
||||||
|
* Animation: 2.5s rotation collapse when confirmed
|
||||||
*/
|
*/
|
||||||
function NumberBondVisualization({
|
function NumberBondVisualization({
|
||||||
moverValue,
|
moverPiece,
|
||||||
helperValue,
|
helperPiece,
|
||||||
targetValue,
|
targetPiece,
|
||||||
relation,
|
relation,
|
||||||
targetPos,
|
targetPos,
|
||||||
cellSize,
|
cellSize,
|
||||||
onConfirm,
|
onConfirm,
|
||||||
closing = false,
|
closing = false,
|
||||||
}: {
|
}: {
|
||||||
moverValue: number
|
moverPiece: Piece
|
||||||
helperValue: number
|
helperPiece: Piece
|
||||||
targetValue: number
|
targetPiece: Piece
|
||||||
relation: RelationKind
|
relation: RelationKind
|
||||||
targetPos: { x: number; y: number }
|
targetPos: { x: number; y: number }
|
||||||
cellSize: number
|
cellSize: number
|
||||||
onConfirm: () => void
|
onConfirm: () => void
|
||||||
closing?: boolean
|
closing?: boolean
|
||||||
}) {
|
}) {
|
||||||
// Triangle layout: target at top, mover and helper at bottom corners
|
const [animating, setAnimating] = useState(false)
|
||||||
const triangleSize = cellSize * 1.5
|
|
||||||
const topPos = { x: targetPos.x, y: targetPos.y - triangleSize * 0.6 }
|
|
||||||
const leftPos = { x: targetPos.x - triangleSize * 0.5, y: targetPos.y + triangleSize * 0.4 }
|
|
||||||
const rightPos = { x: targetPos.x + triangleSize * 0.5, y: targetPos.y + triangleSize * 0.4 }
|
|
||||||
|
|
||||||
const circleRadius = cellSize * 0.4
|
|
||||||
const fontSize = cellSize * 0.35
|
|
||||||
|
|
||||||
// Color scheme based on relation type
|
// Color scheme based on relation type
|
||||||
const colorMap: Record<RelationKind, string> = {
|
const colorMap: Record<RelationKind, string> = {
|
||||||
|
|
@ -651,14 +646,6 @@ function NumberBondVisualization({
|
||||||
}
|
}
|
||||||
const color = colorMap[relation] || '#8b5cf6'
|
const color = colorMap[relation] || '#8b5cf6'
|
||||||
|
|
||||||
// Animate in with spring
|
|
||||||
const spring = useSpring({
|
|
||||||
from: { scale: 0, opacity: 0 },
|
|
||||||
scale: closing ? 0 : 1,
|
|
||||||
opacity: closing ? 0 : 1,
|
|
||||||
config: { tension: 280, friction: 20 },
|
|
||||||
})
|
|
||||||
|
|
||||||
// Operation symbol based on relation
|
// Operation symbol based on relation
|
||||||
const operatorMap: Record<RelationKind, string> = {
|
const operatorMap: Record<RelationKind, string> = {
|
||||||
SUM: '+',
|
SUM: '+',
|
||||||
|
|
@ -671,163 +658,215 @@ function NumberBondVisualization({
|
||||||
}
|
}
|
||||||
const operator = operatorMap[relation]
|
const operator = operatorMap[relation]
|
||||||
|
|
||||||
|
// Layout: operands at top, result at bottom
|
||||||
|
const spacing = cellSize * 1.8
|
||||||
|
const moverPos = { x: targetPos.x - spacing * 0.5, y: targetPos.y - spacing * 0.7 }
|
||||||
|
const helperPos = { x: targetPos.x + spacing * 0.5, y: targetPos.y - spacing * 0.7 }
|
||||||
|
const resultPos = { x: targetPos.x, y: targetPos.y + spacing * 0.5 }
|
||||||
|
|
||||||
|
// Animation: 2.5s rotate and collapse
|
||||||
|
const captureAnimation = useSpring({
|
||||||
|
from: { rotation: 0, radius: 1, opacity: 1 },
|
||||||
|
rotation: animating ? Math.PI * 20 : 0, // 10 full rotations
|
||||||
|
radius: animating ? 0 : 1,
|
||||||
|
opacity: animating ? 0 : 1,
|
||||||
|
config: animating ? { duration: 2500 } : { tension: 280, friction: 20 },
|
||||||
|
onRest: () => {
|
||||||
|
if (animating) {
|
||||||
|
onConfirm()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Initial entrance animation
|
||||||
|
const entranceSpring = useSpring({
|
||||||
|
from: { scale: 0, opacity: 0 },
|
||||||
|
scale: closing || animating ? 0 : 1,
|
||||||
|
opacity: closing ? 0 : 1,
|
||||||
|
config: { tension: 280, friction: 20 },
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleConfirm = (e: React.MouseEvent) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
setAnimating(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get piece values
|
||||||
|
const getMoverValue = () => getEffectiveValue(moverPiece)
|
||||||
|
const getHelperValue = () => getEffectiveValue(helperPiece)
|
||||||
|
const getTargetValue = () => getEffectiveValue(targetPiece)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<animated.g
|
<animated.g
|
||||||
style={{
|
style={{
|
||||||
opacity: spring.opacity,
|
opacity: entranceSpring.opacity,
|
||||||
}}
|
}}
|
||||||
transform={to([spring.scale], (s) => `translate(${targetPos.x}, ${targetPos.y}) scale(${s})`)}
|
transform={to(
|
||||||
|
[entranceSpring.scale],
|
||||||
|
(s) => `translate(${targetPos.x}, ${targetPos.y}) scale(${s})`
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
{/* Triangle connecting lines */}
|
|
||||||
<g transform={`translate(${-targetPos.x}, ${-targetPos.y})`}>
|
<g transform={`translate(${-targetPos.x}, ${-targetPos.y})`}>
|
||||||
<line
|
{!animating && (
|
||||||
x1={topPos.x}
|
<>
|
||||||
y1={topPos.y}
|
{/* Triangle connecting lines */}
|
||||||
x2={leftPos.x}
|
<line
|
||||||
y2={leftPos.y}
|
x1={moverPos.x}
|
||||||
stroke={color}
|
y1={moverPos.y}
|
||||||
strokeWidth={3}
|
x2={helperPos.x}
|
||||||
opacity={0.4}
|
y2={helperPos.y}
|
||||||
/>
|
stroke={color}
|
||||||
<line
|
strokeWidth={3}
|
||||||
x1={topPos.x}
|
opacity={0.3}
|
||||||
y1={topPos.y}
|
/>
|
||||||
x2={rightPos.x}
|
<line
|
||||||
y2={rightPos.y}
|
x1={moverPos.x}
|
||||||
stroke={color}
|
y1={moverPos.y}
|
||||||
strokeWidth={3}
|
x2={resultPos.x}
|
||||||
opacity={0.4}
|
y2={resultPos.y}
|
||||||
/>
|
stroke={color}
|
||||||
<line
|
strokeWidth={3}
|
||||||
x1={leftPos.x}
|
opacity={0.3}
|
||||||
y1={leftPos.y}
|
/>
|
||||||
x2={rightPos.x}
|
<line
|
||||||
y2={rightPos.y}
|
x1={helperPos.x}
|
||||||
stroke={color}
|
y1={helperPos.y}
|
||||||
strokeWidth={3}
|
x2={resultPos.x}
|
||||||
opacity={0.4}
|
y2={resultPos.y}
|
||||||
/>
|
stroke={color}
|
||||||
|
strokeWidth={3}
|
||||||
|
opacity={0.3}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Target (top) */}
|
{/* Operator symbol in center */}
|
||||||
<circle
|
<text
|
||||||
cx={topPos.x}
|
x={targetPos.x}
|
||||||
cy={topPos.y}
|
y={targetPos.y}
|
||||||
r={circleRadius}
|
textAnchor="middle"
|
||||||
fill={color}
|
dominantBaseline="central"
|
||||||
stroke="white"
|
fill={color}
|
||||||
strokeWidth={3}
|
fontSize={cellSize * 0.6}
|
||||||
/>
|
fontWeight="bold"
|
||||||
<text
|
opacity={0.8}
|
||||||
x={topPos.x}
|
>
|
||||||
y={topPos.y}
|
{operator}
|
||||||
textAnchor="middle"
|
</text>
|
||||||
dominantBaseline="central"
|
</>
|
||||||
fill="white"
|
)}
|
||||||
fontSize={fontSize}
|
|
||||||
fontWeight="bold"
|
|
||||||
fontFamily="Georgia, 'Times New Roman', serif"
|
|
||||||
>
|
|
||||||
{targetValue}
|
|
||||||
</text>
|
|
||||||
|
|
||||||
{/* Mover (bottom left) */}
|
{/* Mover piece (top-left operand) */}
|
||||||
<circle
|
<animated.g
|
||||||
cx={leftPos.x}
|
transform={to([captureAnimation.rotation, captureAnimation.radius], (rot, rad) => {
|
||||||
cy={leftPos.y}
|
if (!animating) {
|
||||||
r={circleRadius}
|
return `translate(${moverPos.x}, ${moverPos.y})`
|
||||||
fill={color}
|
}
|
||||||
stroke="white"
|
// During animation: rotate around center and collapse
|
||||||
strokeWidth={3}
|
const angle = rot
|
||||||
/>
|
const distance = spacing * 0.7 * rad
|
||||||
<text
|
const x = targetPos.x + Math.cos(angle) * distance
|
||||||
x={leftPos.x}
|
const y = targetPos.y + Math.sin(angle) * distance
|
||||||
y={leftPos.y}
|
return `translate(${x}, ${y})`
|
||||||
textAnchor="middle"
|
})}
|
||||||
dominantBaseline="central"
|
opacity={animating ? 1 : 1} // Mover stays visible
|
||||||
fill="white"
|
|
||||||
fontSize={fontSize}
|
|
||||||
fontWeight="bold"
|
|
||||||
fontFamily="Georgia, 'Times New Roman', serif"
|
|
||||||
>
|
>
|
||||||
{moverValue}
|
<g transform={`translate(${-cellSize / 2}, ${-cellSize / 2})`}>
|
||||||
</text>
|
<PieceRenderer
|
||||||
|
type={moverPiece.type}
|
||||||
|
color={moverPiece.color}
|
||||||
|
value={getMoverValue() || 0}
|
||||||
|
size={cellSize}
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</animated.g>
|
||||||
|
|
||||||
{/* Helper (bottom right) */}
|
{/* Helper piece (top-right operand) */}
|
||||||
<circle
|
<animated.g
|
||||||
cx={rightPos.x}
|
transform={to([captureAnimation.rotation, captureAnimation.radius], (rot, rad) => {
|
||||||
cy={rightPos.y}
|
if (!animating) {
|
||||||
r={circleRadius}
|
return `translate(${helperPos.x}, ${helperPos.y})`
|
||||||
fill={color}
|
}
|
||||||
stroke="white"
|
const angle = rot + (Math.PI * 2) / 3 // Offset by 120 degrees
|
||||||
strokeWidth={3}
|
const distance = spacing * 0.7 * rad
|
||||||
/>
|
const x = targetPos.x + Math.cos(angle) * distance
|
||||||
<text
|
const y = targetPos.y + Math.sin(angle) * distance
|
||||||
x={rightPos.x}
|
return `translate(${x}, ${y})`
|
||||||
y={rightPos.y}
|
})}
|
||||||
textAnchor="middle"
|
opacity={to([captureAnimation.opacity], (op) => (animating ? op : 1))}
|
||||||
dominantBaseline="central"
|
|
||||||
fill="white"
|
|
||||||
fontSize={fontSize}
|
|
||||||
fontWeight="bold"
|
|
||||||
fontFamily="Georgia, 'Times New Roman', serif"
|
|
||||||
>
|
>
|
||||||
{helperValue}
|
<g transform={`translate(${-cellSize / 2}, ${-cellSize / 2})`}>
|
||||||
</text>
|
<PieceRenderer
|
||||||
|
type={helperPiece.type}
|
||||||
|
color={helperPiece.color}
|
||||||
|
value={getHelperValue() || 0}
|
||||||
|
size={cellSize}
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</animated.g>
|
||||||
|
|
||||||
{/* Operator symbol between bottom circles */}
|
{/* Target piece (bottom result) */}
|
||||||
<text
|
<animated.g
|
||||||
x={(leftPos.x + rightPos.x) / 2}
|
transform={to([captureAnimation.rotation, captureAnimation.radius], (rot, rad) => {
|
||||||
y={leftPos.y}
|
if (!animating) {
|
||||||
textAnchor="middle"
|
return `translate(${resultPos.x}, ${resultPos.y})`
|
||||||
dominantBaseline="central"
|
}
|
||||||
fill={color}
|
const angle = rot + (Math.PI * 4) / 3 // Offset by 240 degrees
|
||||||
fontSize={fontSize * 1.2}
|
const distance = spacing * 0.7 * rad
|
||||||
fontWeight="bold"
|
const x = targetPos.x + Math.cos(angle) * distance
|
||||||
|
const y = targetPos.y + Math.sin(angle) * distance
|
||||||
|
return `translate(${x}, ${y})`
|
||||||
|
})}
|
||||||
|
opacity={to([captureAnimation.opacity], (op) => (animating ? op : 1))}
|
||||||
>
|
>
|
||||||
{operator}
|
<g transform={`translate(${-cellSize / 2}, ${-cellSize / 2})`}>
|
||||||
</text>
|
<PieceRenderer
|
||||||
|
type={targetPiece.type}
|
||||||
|
color={targetPiece.color}
|
||||||
|
value={getTargetValue() || 0}
|
||||||
|
size={cellSize}
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</animated.g>
|
||||||
|
|
||||||
{/* Confirm button */}
|
{/* Confirm button */}
|
||||||
<g transform={`translate(${targetPos.x}, ${targetPos.y + triangleSize * 0.9})`}>
|
{!animating && (
|
||||||
<foreignObject
|
<g transform={`translate(${targetPos.x}, ${resultPos.y + cellSize * 1.2})`}>
|
||||||
x={-cellSize}
|
<foreignObject
|
||||||
y={-cellSize * 0.3}
|
x={-cellSize}
|
||||||
width={cellSize * 2}
|
y={-cellSize * 0.3}
|
||||||
height={cellSize * 0.6}
|
width={cellSize * 2}
|
||||||
>
|
height={cellSize * 0.6}
|
||||||
<button
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation()
|
|
||||||
onConfirm()
|
|
||||||
}}
|
|
||||||
style={{
|
|
||||||
width: '100%',
|
|
||||||
height: '100%',
|
|
||||||
borderRadius: '12px',
|
|
||||||
border: `3px solid ${color}`,
|
|
||||||
backgroundColor: 'white',
|
|
||||||
color,
|
|
||||||
fontSize: `${fontSize * 0.8}px`,
|
|
||||||
fontWeight: 'bold',
|
|
||||||
cursor: 'pointer',
|
|
||||||
transition: 'all 0.2s ease',
|
|
||||||
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.3)',
|
|
||||||
}}
|
|
||||||
onMouseEnter={(e) => {
|
|
||||||
e.currentTarget.style.backgroundColor = color
|
|
||||||
e.currentTarget.style.color = 'white'
|
|
||||||
e.currentTarget.style.transform = 'scale(1.05)'
|
|
||||||
}}
|
|
||||||
onMouseLeave={(e) => {
|
|
||||||
e.currentTarget.style.backgroundColor = 'white'
|
|
||||||
e.currentTarget.style.color = color
|
|
||||||
e.currentTarget.style.transform = 'scale(1)'
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
✓ Capture
|
<button
|
||||||
</button>
|
onClick={handleConfirm}
|
||||||
</foreignObject>
|
style={{
|
||||||
</g>
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
borderRadius: '12px',
|
||||||
|
border: `3px solid ${color}`,
|
||||||
|
backgroundColor: 'white',
|
||||||
|
color,
|
||||||
|
fontSize: `${cellSize * 0.28}px`,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
cursor: 'pointer',
|
||||||
|
transition: 'all 0.2s ease',
|
||||||
|
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.3)',
|
||||||
|
}}
|
||||||
|
onMouseEnter={(e) => {
|
||||||
|
e.currentTarget.style.backgroundColor = color
|
||||||
|
e.currentTarget.style.color = 'white'
|
||||||
|
e.currentTarget.style.transform = 'scale(1.05)'
|
||||||
|
}}
|
||||||
|
onMouseLeave={(e) => {
|
||||||
|
e.currentTarget.style.backgroundColor = 'white'
|
||||||
|
e.currentTarget.style.color = color
|
||||||
|
e.currentTarget.style.transform = 'scale(1)'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
✓ Capture
|
||||||
|
</button>
|
||||||
|
</foreignObject>
|
||||||
|
</g>
|
||||||
|
)}
|
||||||
</g>
|
</g>
|
||||||
</animated.g>
|
</animated.g>
|
||||||
)
|
)
|
||||||
|
|
@ -1081,10 +1120,9 @@ function BoardDisplay() {
|
||||||
const [hoveredRelation, setHoveredRelation] = useState<string | null>(null)
|
const [hoveredRelation, setHoveredRelation] = useState<string | null>(null)
|
||||||
const [selectedRelation, setSelectedRelation] = useState<RelationKind | null>(null)
|
const [selectedRelation, setSelectedRelation] = useState<RelationKind | null>(null)
|
||||||
const [selectedHelper, setSelectedHelper] = useState<{
|
const [selectedHelper, setSelectedHelper] = useState<{
|
||||||
pieceId: string
|
helperPiece: Piece
|
||||||
moverValue: number
|
moverPiece: Piece
|
||||||
helperValue: number
|
targetPiece: Piece
|
||||||
targetValue: number
|
|
||||||
} | null>(null)
|
} | null>(null)
|
||||||
|
|
||||||
// Handle closing animation completion
|
// Handle closing animation completion
|
||||||
|
|
@ -1300,7 +1338,7 @@ function BoardDisplay() {
|
||||||
const handleHelperSelection = (helperPieceId: string) => {
|
const handleHelperSelection = (helperPieceId: string) => {
|
||||||
if (!captureTarget || !selectedRelation) return
|
if (!captureTarget || !selectedRelation) return
|
||||||
|
|
||||||
// Get piece values for number bond visualization
|
// Get pieces for number bond visualization
|
||||||
const moverPiece = Object.values(state.pieces).find(
|
const moverPiece = Object.values(state.pieces).find(
|
||||||
(p) => p.id === captureTarget.pieceId && !p.captured
|
(p) => p.id === captureTarget.pieceId && !p.captured
|
||||||
)
|
)
|
||||||
|
|
@ -1313,42 +1351,21 @@ function BoardDisplay() {
|
||||||
|
|
||||||
if (!moverPiece || !targetPiece || !helperPiece) return
|
if (!moverPiece || !targetPiece || !helperPiece) return
|
||||||
|
|
||||||
const moverValue = getEffectiveValue(moverPiece)
|
// Show number bond visualization
|
||||||
const targetValue = getEffectiveValue(targetPiece)
|
|
||||||
const helperValue = getEffectiveValue(helperPiece)
|
|
||||||
|
|
||||||
if (
|
|
||||||
moverValue === undefined ||
|
|
||||||
moverValue === null ||
|
|
||||||
targetValue === undefined ||
|
|
||||||
targetValue === null ||
|
|
||||||
helperValue === undefined ||
|
|
||||||
helperValue === null
|
|
||||||
) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show number bond instead of immediately executing
|
|
||||||
setSelectedHelper({
|
setSelectedHelper({
|
||||||
pieceId: helperPieceId,
|
helperPiece,
|
||||||
moverValue,
|
moverPiece,
|
||||||
helperValue,
|
targetPiece,
|
||||||
targetValue,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleNumberBondConfirm = () => {
|
const handleNumberBondConfirm = () => {
|
||||||
if (!captureTarget || !selectedRelation || !selectedHelper) return
|
if (!captureTarget || !selectedRelation || !selectedHelper) return
|
||||||
|
|
||||||
const targetPiece = Object.values(state.pieces).find(
|
|
||||||
(p) => p.square === captureTarget.to && !p.captured
|
|
||||||
)
|
|
||||||
if (!targetPiece) return
|
|
||||||
|
|
||||||
const captureData = {
|
const captureData = {
|
||||||
relation: selectedRelation,
|
relation: selectedRelation,
|
||||||
targetPieceId: targetPiece.id,
|
targetPieceId: selectedHelper.targetPiece.id,
|
||||||
helperPieceId: selectedHelper.pieceId,
|
helperPieceId: selectedHelper.helperPiece.id,
|
||||||
}
|
}
|
||||||
|
|
||||||
makeMove(captureTarget.from, captureTarget.to, captureTarget.pieceId, undefined, captureData)
|
makeMove(captureTarget.from, captureTarget.to, captureTarget.pieceId, undefined, captureData)
|
||||||
|
|
@ -1580,9 +1597,9 @@ function BoardDisplay() {
|
||||||
console.log('[Render] Showing NumberBondVisualization')
|
console.log('[Render] Showing NumberBondVisualization')
|
||||||
return (
|
return (
|
||||||
<NumberBondVisualization
|
<NumberBondVisualization
|
||||||
moverValue={selectedHelper.moverValue}
|
moverPiece={selectedHelper.moverPiece}
|
||||||
helperValue={selectedHelper.helperValue}
|
helperPiece={selectedHelper.helperPiece}
|
||||||
targetValue={selectedHelper.targetValue}
|
targetPiece={selectedHelper.targetPiece}
|
||||||
relation={selectedRelation}
|
relation={selectedRelation}
|
||||||
targetPos={targetPos}
|
targetPos={targetPos}
|
||||||
cellSize={cellSize}
|
cellSize={cellSize}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue