import { type CSSProperties, useMemo } from "react"; import { DEFAULT_COLOR_NAME, DEFAULT_STROKE_COLOR, DEFAULT_STROKE_WIDTH, TRANSITION_STYLE, } from "./constants"; import { ORGAN_COMPONENTS } from "./svg"; import type { OrganConfig, OrganId, OrganStyle, StrictColorPalette } from "./types"; export interface OrganSvgWrapperProps { /** Organ identifier */ organId: OrganId; /** Organ configuration */ config?: OrganConfig; /** Color palette to use for this organ (with guaranteed alpha values) */ colorPalette: StrictColorPalette; /** Whether the organ is currently hovered */ isHovered: boolean; /** Whether the organ is currently selected */ isSelected: boolean; /** Mouse enter handler */ onMouseEnter: () => void; /** Mouse leave handler */ onMouseLeave: () => void; /** Click handler */ onClick: () => void; /** X position in parent viewBox coordinate system */ x: number; /** Y position in parent viewBox coordinate system */ y: number; /** Width of the organ SVG viewport in viewBox units */ width: number; /** Height of the organ SVG viewport in viewBox units */ height: number; /** ViewBox for the organ SVG (local coordinate system) */ viewBox: string; /** Show red outline around this organ */ showOutline?: boolean; /** Whether the organ is visible (controls opacity for animation) */ isVisible?: boolean; } /** * Merge multiple OrganStyle objects with priority (later objects override earlier) */ function mergeStyles(...styles: (OrganStyle | undefined)[]): OrganStyle { const result: OrganStyle = {}; for (const style of styles) { if (style) { if (style.fill !== undefined) result.fill = style.fill; if (style.stroke !== undefined) result.stroke = style.stroke; if (style.strokeWidth !== undefined) result.strokeWidth = style.strokeWidth; if (style.opacity !== undefined) result.opacity = style.opacity; if (style.visible !== undefined) result.visible = style.visible; if (style.blur !== undefined) result.blur = style.blur; } } return result; } /** * Wrapper component for organ SVG with interaction handling */ export function OrganSvg({ organId, config, colorPalette, isHovered, isSelected, onMouseEnter, onMouseLeave, onClick, x, y, width, height, viewBox, showOutline = false, isVisible = true, }: OrganSvgWrapperProps) { const OrganComponent = ORGAN_COMPONENTS[organId]; const isActive = isHovered || isSelected; // Compute the final style based on state and colorScheme const computedStyle = useMemo((): OrganStyle => { const userStyle = config?.style; const strokeStyle = { stroke: DEFAULT_STROKE_COLOR, strokeWidth: showOutline ? DEFAULT_STROKE_WIDTH : 0, }; return mergeStyles(userStyle, strokeStyle); }, [config?.style, showOutline]); const [minX = 0, minY = 0, viewBoxWidth, viewBoxHeight] = viewBox.split(" ").map(Number); const scaleX = viewBoxWidth ? width / viewBoxWidth : 1; const scaleY = viewBoxHeight ? height / viewBoxHeight : 1; const transform = `translate(${x} ${y}) scale(${scaleX} ${scaleY}) translate(${-minX} ${-minY})`; const groupStyle: CSSProperties = { transition: `${TRANSITION_STYLE}, visibility 0s`, }; const filter = isActive ? "blur(1px)" : undefined; return ( {organId} { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); onClick(); } }} cursor="pointer" pointerEvents="auto" > ); }