blacktrigram 0.7.111 → 0.7.113

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/lib/assets/index.css +0 -1
  2. package/lib/audio/AudioManager.js +22 -20
  3. package/lib/audio/AudioManager.js.map +1 -1
  4. package/lib/audio/AudioPool.js +6 -4
  5. package/lib/audio/AudioPool.js.map +1 -1
  6. package/lib/audio/VariantSelector.js +12 -8
  7. package/lib/audio/VariantSelector.js.map +1 -1
  8. package/lib/components/screens/intro/IntroScreen3D.js +1 -1
  9. package/lib/components/screens/training/TrainingScreen3D.js +4 -3
  10. package/lib/components/screens/training/TrainingScreen3D.js.map +1 -1
  11. package/lib/components/shared/mobile/HapticController.js +4 -2
  12. package/lib/components/shared/mobile/HapticController.js.map +1 -1
  13. package/lib/components/shared/mobile/StanceWheelPure.js +1 -1
  14. package/lib/components/shared/mobile/StanceWheelPure.js.map +1 -1
  15. package/lib/components/shared/three/effects/VitalPointMarkers3D.js +7 -5
  16. package/lib/components/shared/three/effects/VitalPointMarkers3D.js.map +1 -1
  17. package/lib/components/shared/three/optimization/AdaptiveQuality.js.map +1 -1
  18. package/lib/components/shared/three/ui/VitalPointOverlayControlsHtml.js +7 -5
  19. package/lib/components/shared/three/ui/VitalPointOverlayControlsHtml.js.map +1 -1
  20. package/lib/components/shared/ui/SplashScreen.js +2 -2
  21. package/lib/components/shared/ui/VitalPointOverlayControlsPure.js +7 -5
  22. package/lib/components/shared/ui/VitalPointOverlayControlsPure.js.map +1 -1
  23. package/lib/components/shared/ui/VolumeControl.js +4 -2
  24. package/lib/components/shared/ui/VolumeControl.js.map +1 -1
  25. package/lib/hooks/useSkeletalAnimation.js +6 -4
  26. package/lib/hooks/useSkeletalAnimation.js.map +1 -1
  27. package/lib/hooks/useTouchControls.js +18 -17
  28. package/lib/hooks/useTouchControls.js.map +1 -1
  29. package/lib/systems/animation/builders/AnimationBuilder.js +6 -2
  30. package/lib/systems/animation/builders/AnimationBuilder.js.map +1 -1
  31. package/lib/systems/animation/builders/PunchPhaseApplicator.js +8 -4
  32. package/lib/systems/animation/builders/PunchPhaseApplicator.js.map +1 -1
  33. package/lib/systems/animation/core/AnimationStateMachine.js +39 -37
  34. package/lib/systems/animation/core/AnimationStateMachine.js.map +1 -1
  35. package/lib/systems/bodypart/MovementPenaltySystem.js.map +1 -1
  36. package/lib/systems/combat/LimbExposureSystem.js.map +1 -1
  37. package/lib/systems/physics/MovementPhysics.js +25 -24
  38. package/lib/systems/physics/MovementPhysics.js.map +1 -1
  39. package/lib/utils/deviceDetection.js +14 -13
  40. package/lib/utils/deviceDetection.js.map +1 -1
  41. package/lib/utils/inputSystem.js +4 -2
  42. package/lib/utils/inputSystem.js.map +1 -1
  43. package/package.json +6 -6
@@ -1 +1 @@
1
- {"version":3,"file":"VitalPointMarkers3D.js","names":[],"sources":["../../../../../src/components/shared/three/effects/VitalPointMarkers3D.tsx"],"sourcesContent":["/**\n * VitalPointMarkers3D - 3D vital point visualization\n *\n * Renders anatomical vital points (급소) in 3D space around character models\n * Provides Korean martial arts targeting system visualization\n * Note: Currently displays points from KOREAN_VITAL_POINTS data (expandable to 70 points)\n */\n\nimport { Html } from \"@react-three/drei\";\nimport { useFrame } from \"@react-three/fiber\";\nimport React, { useMemo, useRef, useState } from \"react\";\nimport * as THREE from \"three\";\nimport { KOREAN_VITAL_POINTS } from \"../../../../systems/vitalpoint/KoreanVitalPoints\";\nimport { VitalPoint } from \"../../../../systems/vitalpoint/types\";\nimport { Position, VitalPointSeverity } from \"../../../../types/common\";\nimport { FONT_FAMILY, KOREAN_COLORS } from \"../../../../types/constants\";\n\n/**\n * Body region filter options\n */\nexport type BodyRegionFilter = \"all\" | \"head\" | \"torso\" | \"arms\" | \"legs\";\n\n/**\n * Props for the VitalPointMarkers3D component.\n * Controls visibility and interaction with anatomical targeting points.\n */\nexport interface VitalPointMarkers3DProps {\n /** 3D world position of the character [x, y, z]. Defaults to [0, 0, 0] */\n readonly position?: [number, number, number];\n /** Whether markers are visible. Defaults to true */\n readonly visible?: boolean;\n /** Selected vital point ID for highlighting */\n readonly selectedPoint?: string | null;\n /** Callback when a vital point is clicked */\n readonly onPointClick?: (vitalPointId: string) => void;\n /** Callback when a vital point is hovered */\n readonly onPointHover?: (vitalPointId: string | null) => void;\n /** Filter points by severity level */\n readonly severityFilter?: VitalPointSeverity[];\n /** Filter points by body region */\n readonly regionFilter?: BodyRegionFilter;\n /** Search query to filter points by name */\n readonly searchQuery?: string;\n /** Whether to show point labels with Korean names */\n readonly showLabels?: boolean;\n /** Scale multiplier for marker size. Defaults to 1.0 */\n readonly scale?: number;\n /** Whether to enable pulsing animation. Defaults to true */\n readonly animated?: boolean;\n}\n\n/**\n * Get color based on vital point severity\n */\nconst getSeverityColor = (severity: VitalPointSeverity): number => {\n switch (severity) {\n case VitalPointSeverity.LETHAL:\n return KOREAN_COLORS.ACCENT_RED;\n case VitalPointSeverity.CRITICAL:\n return KOREAN_COLORS.SECONDARY_MAGENTA;\n case VitalPointSeverity.MAJOR:\n return KOREAN_COLORS.ACCENT_GOLD;\n case VitalPointSeverity.MODERATE:\n return KOREAN_COLORS.SECONDARY_YELLOW;\n case VitalPointSeverity.MINOR:\n return KOREAN_COLORS.ACCENT_CYAN;\n default:\n return KOREAN_COLORS.PRIMARY_CYAN;\n }\n};\n\nconst CHARACTER_CENTER_X = 100; // Pixel center of character sprite\nconst CHARACTER_SPRITE_HEIGHT = 200; // Pixel height of character sprite\nconst CHARACTER_3D_HEIGHT = 2.8; // 3D model height in world units\nconst CHARACTER_3D_WIDTH = 0.8; // Approximate 3D model width\nconst SPRITE_WIDTH = 100; // Half-width of sprite (total ~200 pixels)\n\nconst LABEL_STYLES = {\n padding: \"4px 8px\",\n borderRadius: \"4px\",\n fontSize: \"10px\",\n subtextSize: \"8px\",\n subtextOpacity: 0.8,\n borderWidth: \"1px\",\n} as const;\n\n/**\n * Convert color number to RGBA hex string\n * @param color - Color as number (e.g., 0xFF0000)\n * @param alpha - Alpha channel as hex string (e.g., \"dd\" for semi-transparent)\n * @returns RGBA hex string (e.g., \"#ff0000dd\")\n */\nconst colorToRgbaHex = (color: number, alpha: string = \"ff\"): string => {\n return `#${color.toString(16).padStart(6, \"0\")}${alpha}`;\n};\n\n/**\n * Convert 2D sprite coordinates to 3D body position\n * Maps vital point pixel coordinates to 3D character model space\n *\n * Input: 2D pixel coordinates where:\n * - X: ~50-150 (100 = center), positive = right side of character\n * - Y: 0 = top of head, 200 = feet (y increases downward)\n *\n * Output: 3D world coordinates relative to character position where:\n * - X: horizontal offset from character center\n * - Y: height from ground (0 = feet, 2.8 = top)\n * - Z: depth offset (positive = in front of character)\n */\nconst convert2DTo3D = (\n pos2D: Position,\n basePosition: [number, number, number]\n): [number, number, number] => {\n const normalizedX = (pos2D.x - CHARACTER_CENTER_X) / SPRITE_WIDTH;\n const x3D = normalizedX * CHARACTER_3D_WIDTH;\n\n const normalizedY = 1 - pos2D.y / CHARACTER_SPRITE_HEIGHT;\n const y3D = normalizedY * CHARACTER_3D_HEIGHT;\n\n const z3D = 0.15;\n\n return [basePosition[0] + x3D, basePosition[1] + y3D, basePosition[2] + z3D];\n};\n\n/**\n * Individual Vital Point Marker Component\n */\ninterface VitalPointMarkerProps {\n readonly vitalPoint: VitalPoint;\n readonly position3D: [number, number, number];\n readonly selected: boolean;\n readonly hovered: boolean;\n readonly showLabel: boolean;\n readonly scale: number;\n readonly animated: boolean;\n readonly onHover: (hovered: boolean) => void;\n readonly onClick: () => void;\n}\n\nconst VitalPointMarker: React.FC<VitalPointMarkerProps> = ({\n vitalPoint,\n position3D,\n selected,\n hovered,\n showLabel,\n scale,\n animated,\n onHover,\n onClick,\n}) => {\n const sphereRef = useRef<THREE.Mesh>(null);\n const ringRef = useRef<THREE.Mesh>(null);\n\n useFrame((state) => {\n if (!sphereRef.current || !animated) return;\n\n const pulse = Math.sin(state.clock.elapsedTime * 2) * 0.1 + 1;\n sphereRef.current.scale.setScalar(pulse * scale);\n\n if (ringRef.current && (selected || hovered)) {\n ringRef.current.rotation.z += 0.05;\n }\n });\n\n const color = useMemo(() => {\n if (selected) return KOREAN_COLORS.ACCENT_GOLD;\n if (hovered) return KOREAN_COLORS.PRIMARY_CYAN;\n return getSeverityColor(vitalPoint.severity);\n }, [selected, hovered, vitalPoint.severity]);\n\n const markerSize = useMemo(() => {\n const DEFAULT_MARKER_SIZE = 0.08;\n\n switch (vitalPoint.severity) {\n case VitalPointSeverity.LETHAL:\n case VitalPointSeverity.CRITICAL:\n return DEFAULT_MARKER_SIZE * 1.6 * scale; // 0.128\n case VitalPointSeverity.MAJOR:\n return DEFAULT_MARKER_SIZE * 1.3 * scale; // 0.104\n case VitalPointSeverity.MODERATE:\n return DEFAULT_MARKER_SIZE * 1.0 * scale; // 0.08\n case VitalPointSeverity.MINOR:\n return DEFAULT_MARKER_SIZE * 0.8 * scale; // 0.064\n default:\n return DEFAULT_MARKER_SIZE * scale;\n }\n }, [vitalPoint.severity, scale]);\n\n return (\n <group position={position3D}>\n {/* Main sphere marker */}\n <mesh\n ref={sphereRef}\n onPointerOver={() => onHover(true)}\n onPointerOut={() => onHover(false)}\n onClick={(e) => {\n e.stopPropagation();\n onClick();\n }}\n >\n <sphereGeometry args={[markerSize, 16, 16]} />\n <meshStandardMaterial\n color={color}\n emissive={color}\n emissiveIntensity={hovered || selected ? 0.8 : 0.4}\n transparent\n opacity={hovered || selected ? 1.0 : 0.85}\n />\n </mesh>\n\n {/* Outer ring for selected/hovered */}\n {(selected || hovered) && (\n <mesh ref={ringRef} rotation={[Math.PI / 2, 0, 0]} position={[0, 0, 0]}>\n <ringGeometry args={[markerSize * 1.5, markerSize * 1.8, 32]} />\n <meshBasicMaterial\n color={color}\n transparent\n opacity={0.5}\n side={THREE.DoubleSide}\n />\n </mesh>\n )}\n\n {/* Label overlay */}\n {showLabel && (hovered || selected) && (\n <Html\n position={[0, markerSize * 2, 0]}\n center\n distanceFactor={5}\n style={{\n pointerEvents: \"none\",\n userSelect: \"none\",\n }}\n >\n <div\n style={{\n background: colorToRgbaHex(color, \"dd\"),\n color: \"#ffffff\",\n padding: LABEL_STYLES.padding,\n borderRadius: LABEL_STYLES.borderRadius,\n fontSize: LABEL_STYLES.fontSize,\n fontFamily: FONT_FAMILY.KOREAN,\n whiteSpace: \"nowrap\",\n textAlign: \"center\",\n border: `${LABEL_STYLES.borderWidth} solid ${colorToRgbaHex(\n color\n )}`,\n }}\n >\n <div>{vitalPoint.names.korean}</div>\n <div\n style={{\n fontSize: LABEL_STYLES.subtextSize,\n opacity: LABEL_STYLES.subtextOpacity,\n }}\n >\n {vitalPoint.names.english}\n </div>\n </div>\n </Html>\n )}\n </group>\n );\n};\n\n/**\n * VitalPointMarkers3D Component\n * Renders all vital points as 3D markers around a character\n */\nexport const VitalPointMarkers3D: React.FC<VitalPointMarkers3DProps> = ({\n position = [0, 0, 0],\n visible = true,\n selectedPoint = null,\n onPointClick,\n onPointHover,\n severityFilter,\n regionFilter = \"all\",\n searchQuery = \"\",\n showLabels = true,\n scale = 1.0,\n animated = true,\n}) => {\n const [hoveredPoint, setHoveredPoint] = useState<string | null>(null);\n\n const visiblePoints = useMemo(() => {\n let points = [...KOREAN_VITAL_POINTS];\n\n if (severityFilter && severityFilter.length > 0) {\n points = points.filter((vp) => severityFilter.includes(vp.severity));\n }\n\n if (regionFilter !== \"all\") {\n if (regionFilter === \"arms\") {\n points = points.filter(\n (vp) =>\n vp.id.startsWith(\"arm_left_\") || vp.id.startsWith(\"arm_right_\")\n );\n } else if (regionFilter === \"legs\") {\n points = points.filter(\n (vp) =>\n vp.id.startsWith(\"leg_left_\") || vp.id.startsWith(\"leg_right_\")\n );\n } else {\n const prefix = `${regionFilter}_`;\n points = points.filter((vp) => vp.id.startsWith(prefix));\n }\n }\n\n if (searchQuery) {\n const query = searchQuery.toLowerCase();\n points = points.filter(\n (vp) =>\n vp.names.korean.toLowerCase().includes(query) ||\n vp.names.english.toLowerCase().includes(query) ||\n vp.names.romanized.toLowerCase().includes(query) ||\n vp.id.toLowerCase().includes(query)\n );\n }\n\n return points;\n }, [severityFilter, regionFilter, searchQuery]);\n\n const handlePointHover = (vitalPointId: string, hovered: boolean) => {\n const newHovered = hovered ? vitalPointId : null;\n setHoveredPoint(newHovered);\n onPointHover?.(newHovered);\n };\n\n const handlePointClick = (vitalPointId: string) => {\n onPointClick?.(vitalPointId);\n };\n\n if (!visible) return null;\n\n return (\n <group position={position}>\n {visiblePoints.map((vitalPoint) => {\n const position3D = convert2DTo3D(vitalPoint.position, [0, 0, 0]);\n\n return (\n <VitalPointMarker\n key={vitalPoint.id}\n vitalPoint={vitalPoint}\n position3D={position3D}\n selected={selectedPoint === vitalPoint.id}\n hovered={hoveredPoint === vitalPoint.id}\n showLabel={showLabels}\n scale={scale}\n animated={animated}\n onHover={(hovered) => handlePointHover(vitalPoint.id, hovered)}\n onClick={() => handlePointClick(vitalPoint.id)}\n />\n );\n })}\n </group>\n );\n};\n\nexport default VitalPointMarkers3D;\n"],"mappings":"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"}
1
+ {"version":3,"file":"VitalPointMarkers3D.js","names":[],"sources":["../../../../../src/components/shared/three/effects/VitalPointMarkers3D.tsx"],"sourcesContent":["/**\n * VitalPointMarkers3D - 3D vital point visualization\n *\n * Renders anatomical vital points (급소) in 3D space around character models\n * Provides Korean martial arts targeting system visualization\n * Note: Currently displays points from KOREAN_VITAL_POINTS data (expandable to 70 points)\n */\n\nimport { Html } from \"@react-three/drei\";\nimport { useFrame } from \"@react-three/fiber\";\nimport React, { useMemo, useRef, useState } from \"react\";\nimport * as THREE from \"three\";\nimport { KOREAN_VITAL_POINTS } from \"../../../../systems/vitalpoint/KoreanVitalPoints\";\nimport { VitalPoint } from \"../../../../systems/vitalpoint/types\";\nimport { Position, VitalPointSeverity } from \"../../../../types/common\";\nimport { FONT_FAMILY, KOREAN_COLORS } from \"../../../../types/constants\";\n\n/**\n * Body region filter options\n */\nexport type BodyRegionFilter = \"all\" | \"head\" | \"torso\" | \"arms\" | \"legs\";\n\n/**\n * Props for the VitalPointMarkers3D component.\n * Controls visibility and interaction with anatomical targeting points.\n */\nexport interface VitalPointMarkers3DProps {\n /** 3D world position of the character [x, y, z]. Defaults to [0, 0, 0] */\n readonly position?: [number, number, number];\n /** Whether markers are visible. Defaults to true */\n readonly visible?: boolean;\n /** Selected vital point ID for highlighting */\n readonly selectedPoint?: string | null;\n /** Callback when a vital point is clicked */\n readonly onPointClick?: (vitalPointId: string) => void;\n /** Callback when a vital point is hovered */\n readonly onPointHover?: (vitalPointId: string | null) => void;\n /** Filter points by severity level */\n readonly severityFilter?: VitalPointSeverity[];\n /** Filter points by body region */\n readonly regionFilter?: BodyRegionFilter;\n /** Search query to filter points by name */\n readonly searchQuery?: string;\n /** Whether to show point labels with Korean names */\n readonly showLabels?: boolean;\n /** Scale multiplier for marker size. Defaults to 1.0 */\n readonly scale?: number;\n /** Whether to enable pulsing animation. Defaults to true */\n readonly animated?: boolean;\n}\n\n/**\n * Get color based on vital point severity\n */\nconst getSeverityColor = (severity: VitalPointSeverity): number => {\n switch (severity) {\n case VitalPointSeverity.LETHAL:\n return KOREAN_COLORS.ACCENT_RED;\n case VitalPointSeverity.CRITICAL:\n return KOREAN_COLORS.SECONDARY_MAGENTA;\n case VitalPointSeverity.MAJOR:\n return KOREAN_COLORS.ACCENT_GOLD;\n case VitalPointSeverity.MODERATE:\n return KOREAN_COLORS.SECONDARY_YELLOW;\n case VitalPointSeverity.MINOR:\n return KOREAN_COLORS.ACCENT_CYAN;\n default:\n return KOREAN_COLORS.PRIMARY_CYAN;\n }\n};\n\nconst CHARACTER_CENTER_X = 100; // Pixel center of character sprite\nconst CHARACTER_SPRITE_HEIGHT = 200; // Pixel height of character sprite\nconst CHARACTER_3D_HEIGHT = 2.8; // 3D model height in world units\nconst CHARACTER_3D_WIDTH = 0.8; // Approximate 3D model width\nconst SPRITE_WIDTH = 100; // Half-width of sprite (total ~200 pixels)\n\nconst LABEL_STYLES = {\n padding: \"4px 8px\",\n borderRadius: \"4px\",\n fontSize: \"10px\",\n subtextSize: \"8px\",\n subtextOpacity: 0.8,\n borderWidth: \"1px\",\n} as const;\n\n/**\n * Convert color number to RGBA hex string\n * @param color - Color as number (e.g., 0xFF0000)\n * @param alpha - Alpha channel as hex string (e.g., \"dd\" for semi-transparent)\n * @returns RGBA hex string (e.g., \"#ff0000dd\")\n */\nconst colorToRgbaHex = (color: number, alpha: string = \"ff\"): string => {\n return `#${color.toString(16).padStart(6, \"0\")}${alpha}`;\n};\n\n/**\n * Convert 2D sprite coordinates to 3D body position\n * Maps vital point pixel coordinates to 3D character model space\n *\n * Input: 2D pixel coordinates where:\n * - X: ~50-150 (100 = center), positive = right side of character\n * - Y: 0 = top of head, 200 = feet (y increases downward)\n *\n * Output: 3D world coordinates relative to character position where:\n * - X: horizontal offset from character center\n * - Y: height from ground (0 = feet, 2.8 = top)\n * - Z: depth offset (positive = in front of character)\n */\nconst convert2DTo3D = (\n pos2D: Position,\n basePosition: [number, number, number]\n): [number, number, number] => {\n const normalizedX = (pos2D.x - CHARACTER_CENTER_X) / SPRITE_WIDTH;\n const x3D = normalizedX * CHARACTER_3D_WIDTH;\n\n const normalizedY = 1 - pos2D.y / CHARACTER_SPRITE_HEIGHT;\n const y3D = normalizedY * CHARACTER_3D_HEIGHT;\n\n const z3D = 0.15;\n\n return [basePosition[0] + x3D, basePosition[1] + y3D, basePosition[2] + z3D];\n};\n\n/**\n * Individual Vital Point Marker Component\n */\ninterface VitalPointMarkerProps {\n readonly vitalPoint: VitalPoint;\n readonly position3D: [number, number, number];\n readonly selected: boolean;\n readonly hovered: boolean;\n readonly showLabel: boolean;\n readonly scale: number;\n readonly animated: boolean;\n readonly onHover: (hovered: boolean) => void;\n readonly onClick: () => void;\n}\n\nconst VitalPointMarker: React.FC<VitalPointMarkerProps> = ({\n vitalPoint,\n position3D,\n selected,\n hovered,\n showLabel,\n scale,\n animated,\n onHover,\n onClick,\n}) => {\n const sphereRef = useRef<THREE.Mesh>(null);\n const ringRef = useRef<THREE.Mesh>(null);\n\n useFrame((state) => {\n if (!sphereRef.current || !animated) return;\n\n const pulse = Math.sin(state.clock.elapsedTime * 2) * 0.1 + 1;\n sphereRef.current.scale.setScalar(pulse * scale);\n\n if (ringRef.current && (selected || hovered)) {\n ringRef.current.rotation.z += 0.05;\n }\n });\n\n const color = useMemo(() => {\n if (selected) return KOREAN_COLORS.ACCENT_GOLD;\n if (hovered) return KOREAN_COLORS.PRIMARY_CYAN;\n return getSeverityColor(vitalPoint.severity);\n }, [selected, hovered, vitalPoint.severity]);\n\n const markerSize = useMemo(() => {\n const DEFAULT_MARKER_SIZE = 0.08;\n\n switch (vitalPoint.severity) {\n case VitalPointSeverity.LETHAL:\n case VitalPointSeverity.CRITICAL:\n return DEFAULT_MARKER_SIZE * 1.6 * scale; // 0.128\n case VitalPointSeverity.MAJOR:\n return DEFAULT_MARKER_SIZE * 1.3 * scale; // 0.104\n case VitalPointSeverity.MODERATE:\n return DEFAULT_MARKER_SIZE * 1.0 * scale; // 0.08\n case VitalPointSeverity.MINOR:\n return DEFAULT_MARKER_SIZE * 0.8 * scale; // 0.064\n default:\n return DEFAULT_MARKER_SIZE * scale;\n }\n }, [vitalPoint.severity, scale]);\n\n return (\n <group position={position3D}>\n {/* Main sphere marker */}\n <mesh\n ref={sphereRef}\n onPointerOver={() => onHover(true)}\n onPointerOut={() => onHover(false)}\n onClick={(e) => {\n e.stopPropagation();\n onClick();\n }}\n >\n <sphereGeometry args={[markerSize, 16, 16]} />\n <meshStandardMaterial\n color={color}\n emissive={color}\n emissiveIntensity={hovered || selected ? 0.8 : 0.4}\n transparent\n opacity={hovered || selected ? 1.0 : 0.85}\n />\n </mesh>\n\n {/* Outer ring for selected/hovered */}\n {(selected || hovered) && (\n <mesh ref={ringRef} rotation={[Math.PI / 2, 0, 0]} position={[0, 0, 0]}>\n <ringGeometry args={[markerSize * 1.5, markerSize * 1.8, 32]} />\n <meshBasicMaterial\n color={color}\n transparent\n opacity={0.5}\n side={THREE.DoubleSide}\n />\n </mesh>\n )}\n\n {/* Label overlay */}\n {showLabel && (hovered || selected) && (\n <Html\n position={[0, markerSize * 2, 0]}\n center\n distanceFactor={5}\n style={{\n pointerEvents: \"none\",\n userSelect: \"none\",\n }}\n >\n <div\n style={{\n background: colorToRgbaHex(color, \"dd\"),\n color: \"#ffffff\",\n padding: LABEL_STYLES.padding,\n borderRadius: LABEL_STYLES.borderRadius,\n fontSize: LABEL_STYLES.fontSize,\n fontFamily: FONT_FAMILY.KOREAN,\n whiteSpace: \"nowrap\",\n textAlign: \"center\",\n border: `${LABEL_STYLES.borderWidth} solid ${colorToRgbaHex(\n color\n )}`,\n }}\n >\n <div>{vitalPoint.names.korean}</div>\n <div\n style={{\n fontSize: LABEL_STYLES.subtextSize,\n opacity: LABEL_STYLES.subtextOpacity,\n }}\n >\n {vitalPoint.names.english}\n </div>\n </div>\n </Html>\n )}\n </group>\n );\n};\n\n/**\n * VitalPointMarkers3D Component\n * Renders all vital points as 3D markers around a character\n */\nexport const VitalPointMarkers3D: React.FC<VitalPointMarkers3DProps> = ({\n position = [0, 0, 0],\n visible = true,\n selectedPoint = null,\n onPointClick,\n onPointHover,\n severityFilter,\n regionFilter = \"all\",\n searchQuery = \"\",\n showLabels = true,\n scale = 1.0,\n animated = true,\n}) => {\n const [hoveredPoint, setHoveredPoint] = useState<string | null>(null);\n\n const visiblePoints = useMemo(() => {\n let points = [...KOREAN_VITAL_POINTS];\n\n if (severityFilter && severityFilter.length > 0) {\n points = points.filter((vp) => severityFilter.includes(vp.severity));\n }\n\n if (regionFilter !== \"all\") {\n if (regionFilter === \"arms\") {\n points = points.filter(\n (vp) =>\n vp.id.startsWith(\"arm_left_\") || vp.id.startsWith(\"arm_right_\")\n );\n } else if (regionFilter === \"legs\") {\n points = points.filter(\n (vp) =>\n vp.id.startsWith(\"leg_left_\") || vp.id.startsWith(\"leg_right_\")\n );\n } else {\n const prefix = `${regionFilter}_`;\n points = points.filter((vp) => vp.id.startsWith(prefix));\n }\n }\n\n if (searchQuery) {\n const query = searchQuery.toLowerCase();\n points = points.filter(\n (vp) =>\n vp.names.korean.toLowerCase().includes(query) ||\n vp.names.english.toLowerCase().includes(query) ||\n vp.names.romanized.toLowerCase().includes(query) ||\n vp.id.toLowerCase().includes(query)\n );\n }\n\n return points;\n }, [severityFilter, regionFilter, searchQuery]);\n\n const handlePointHover = (vitalPointId: string, hovered: boolean) => {\n const newHovered = hovered ? vitalPointId : null;\n setHoveredPoint(newHovered);\n onPointHover?.(newHovered);\n };\n\n const handlePointClick = (vitalPointId: string) => {\n onPointClick?.(vitalPointId);\n };\n\n if (!visible) return null;\n\n return (\n <group position={position}>\n {visiblePoints.map((vitalPoint) => {\n const position3D = convert2DTo3D(vitalPoint.position, [0, 0, 0]);\n\n return (\n <VitalPointMarker\n key={vitalPoint.id}\n vitalPoint={vitalPoint}\n position3D={position3D}\n selected={selectedPoint === vitalPoint.id}\n hovered={hoveredPoint === vitalPoint.id}\n showLabel={showLabels}\n scale={scale}\n animated={animated}\n onHover={(hovered) => handlePointHover(vitalPoint.id, hovered)}\n onClick={() => handlePointClick(vitalPoint.id)}\n />\n );\n })}\n </group>\n );\n};\n\nexport default VitalPointMarkers3D;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAsDA,IAAM,oBAAoB,aAAyC;CACjE,QAAQ,UAAR;EACE,KAAK,mBAAmB,QACtB,OAAO,cAAc;EACvB,KAAK,mBAAmB,UACtB,OAAO,cAAc;EACvB,KAAK,mBAAmB,OACtB,OAAO,cAAc;EACvB,KAAK,mBAAmB,UACtB,OAAO,cAAc;EACvB,KAAK,mBAAmB,OACtB,OAAO,cAAc;EACvB,SACE,OAAO,cAAc;CACzB;AACF;AAEA,IAAM,qBAAqB;AAC3B,IAAM,0BAA0B;AAChC,IAAM,sBAAsB;AAC5B,IAAM,qBAAqB;AAC3B,IAAM,eAAe;AAErB,IAAM,eAAe;CACnB,SAAS;CACT,cAAc;CACd,UAAU;CACV,aAAa;CACb,gBAAgB;CAChB,aAAa;AACf;;;;;;;AAQA,IAAM,kBAAkB,OAAe,QAAgB,SAAiB;CACtE,OAAO,IAAI,MAAM,SAAS,EAAE,CAAC,CAAC,SAAS,GAAG,GAAG,IAAI;AACnD;;;;;;;;;;;;;;AAeA,IAAM,iBACJ,OACA,iBAC6B;CAE7B,MAAM,OADe,MAAM,IAAI,sBAAsB,eAC3B;CAG1B,MAAM,OADc,IAAI,MAAM,IAAI,2BACR;CAI1B,OAAO;EAAC,aAAa,KAAK;EAAK,aAAa,KAAK;EAAK,aAAa,KAAK;CAAG;AAC7E;AAiBA,IAAM,oBAAqD,EACzD,YACA,YACA,UACA,SACA,WACA,OACA,UACA,SACA,cACI;CACJ,MAAM,YAAY,OAAmB,IAAI;CACzC,MAAM,UAAU,OAAmB,IAAI;CAEvC,UAAU,UAAU;EAClB,IAAI,CAAC,UAAU,WAAW,CAAC,UAAU;EAErC,MAAM,QAAQ,KAAK,IAAI,MAAM,MAAM,cAAc,CAAC,IAAI,KAAM;EAC5D,UAAU,QAAQ,MAAM,UAAU,QAAQ,KAAK;EAE/C,IAAI,QAAQ,YAAY,YAAY,UAClC,QAAQ,QAAQ,SAAS,KAAK;CAElC,CAAC;CAED,MAAM,QAAQ,cAAc;EAC1B,IAAI,UAAU,OAAO,cAAc;EACnC,IAAI,SAAS,OAAO,cAAc;EAClC,OAAO,iBAAiB,WAAW,QAAQ;CAC7C,GAAG;EAAC;EAAU;EAAS,WAAW;CAAQ,CAAC;CAE3C,MAAM,aAAa,cAAc;EAC/B,MAAM,sBAAsB;EAE5B,QAAQ,WAAW,UAAnB;GACE,KAAK,mBAAmB;GACxB,KAAK,mBAAmB,UACtB,OAAO,sBAAsB,MAAM;GACrC,KAAK,mBAAmB,OACtB,OAAO,sBAAsB,MAAM;GACrC,KAAK,mBAAmB,UACtB,OAAO,sBAAsB,IAAM;GACrC,KAAK,mBAAmB,OACtB,OAAO,sBAAsB,KAAM;GACrC,SACE,OAAO,sBAAsB;EACjC;CACF,GAAG,CAAC,WAAW,UAAU,KAAK,CAAC;CAE/B,OACE,qBAAC,SAAD;EAAO,UAAU;EAAjB,UAAA;GAEE,qBAAC,QAAD;IACE,KAAK;IACL,qBAAqB,QAAQ,IAAI;IACjC,oBAAoB,QAAQ,KAAK;IACjC,UAAU,MAAM;KACd,EAAE,gBAAgB;KAClB,QAAQ;IACV;IAPF,UAAA,CASE,oBAAC,kBAAD,EAAgB,MAAM;KAAC;KAAY;KAAI;IAAE,EAAI,CAAA,GAC7C,oBAAC,wBAAD;KACS;KACP,UAAU;KACV,mBAAmB,WAAW,WAAW,KAAM;KAC/C,aAAA;KACA,SAAS,WAAW,WAAW,IAAM;IACtC,CAAA,CACG;;IAGJ,YAAY,YACZ,qBAAC,QAAD;IAAM,KAAK;IAAS,UAAU;KAAC,KAAK,KAAK;KAAG;KAAG;IAAC;IAAG,UAAU;KAAC;KAAG;KAAG;IAAC;IAArE,UAAA,CACE,oBAAC,gBAAD,EAAc,MAAM;KAAC,aAAa;KAAK,aAAa;KAAK;IAAE,EAAI,CAAA,GAC/D,oBAAC,qBAAD;KACS;KACP,aAAA;KACA,SAAS;KACT,MAAM,MAAM;IACb,CAAA,CACG;;GAIP,cAAc,WAAW,aACxB,oBAAC,MAAD;IACE,UAAU;KAAC;KAAG,aAAa;KAAG;IAAC;IAC/B,QAAA;IACA,gBAAgB;IAChB,OAAO;KACL,eAAe;KACf,YAAY;IACd;IAEA,UAAA,qBAAC,OAAD;KACE,OAAO;MACL,YAAY,eAAe,OAAO,IAAI;MACtC,OAAO;MACP,SAAS,aAAa;MACtB,cAAc,aAAa;MAC3B,UAAU,aAAa;MACvB,YAAY,YAAY;MACxB,YAAY;MACZ,WAAW;MACX,QAAQ,GAAG,aAAa,YAAY,SAAS,eAC3C,KACF;KACF;KAbF,UAAA,CAeE,oBAAC,OAAD,EAAA,UAAM,WAAW,MAAM,OAAY,CAAA,GACnC,oBAAC,OAAD;MACE,OAAO;OACL,UAAU,aAAa;OACvB,SAAS,aAAa;MACxB;MAEC,UAAA,WAAW,MAAM;KACf,CAAA,CACF;;GACD,CAAA;EAEH;;AAEX;;;;;AAMA,IAAa,uBAA2D,EACtE,WAAW;CAAC;CAAG;CAAG;AAAC,GACnB,UAAU,MACV,gBAAgB,MAChB,cACA,cACA,gBACA,eAAe,OACf,cAAc,IACd,aAAa,MACb,QAAQ,GACR,WAAW,WACP;CACJ,MAAM,CAAC,cAAc,mBAAmB,SAAwB,IAAI;CAEpE,MAAM,gBAAgB,cAAc;EAClC,IAAI,SAAS,CAAC,GAAG,mBAAmB;EAEpC,IAAI,kBAAkB,eAAe,SAAS,GAC5C,SAAS,OAAO,QAAQ,OAAO,eAAe,SAAS,GAAG,QAAQ,CAAC;EAGrE,IAAI,iBAAiB,OAAO;GAC1B,IAAI,iBAAiB,QACnB,SAAS,OAAO,QACb,OACC,GAAG,GAAG,WAAW,WAAW,KAAK,GAAG,GAAG,WAAW,YAAY,CAClE;QACK,IAAI,iBAAiB,QAC1B,SAAS,OAAO,QACb,OACC,GAAG,GAAG,WAAW,WAAW,KAAK,GAAG,GAAG,WAAW,YAAY,CAClE;QACK;IACL,MAAM,SAAS,GAAG,aAAa;IAC/B,SAAS,OAAO,QAAQ,OAAO,GAAG,GAAG,WAAW,MAAM,CAAC;GACzD;EACF;EAEA,IAAI,aAAa;GACf,MAAM,QAAQ,YAAY,YAAY;GACtC,SAAS,OAAO,QACb,OACC,GAAG,MAAM,OAAO,YAAY,CAAC,CAAC,SAAS,KAAK,KAC5C,GAAG,MAAM,QAAQ,YAAY,CAAC,CAAC,SAAS,KAAK,KAC7C,GAAG,MAAM,UAAU,YAAY,CAAC,CAAC,SAAS,KAAK,KAC/C,GAAG,GAAG,YAAY,CAAC,CAAC,SAAS,KAAK,CACtC;EACF;EAEA,OAAO;CACT,GAAG;EAAC;EAAgB;EAAc;CAAW,CAAC;CAE9C,MAAM,oBAAoB,cAAsB,YAAqB;EACnE,MAAM,aAAa,UAAU,eAAe;EAC5C,gBAAgB,UAAU;EAC1B,eAAe,UAAU;CAC3B;CAEA,MAAM,oBAAoB,iBAAyB;EACjD,eAAe,YAAY;CAC7B;CAEA,IAAI,CAAC,SAAS,OAAO;CAErB,OACE,oBAAC,SAAD;EAAiB;EACd,UAAA,cAAc,KAAK,eAAe;GACjC,MAAM,aAAa,cAAc,WAAW,UAAU;IAAC;IAAG;IAAG;GAAC,CAAC;GAE/D,OACE,oBAAC,kBAAD;IAEc;IACA;IACZ,UAAU,kBAAkB,WAAW;IACvC,SAAS,iBAAiB,WAAW;IACrC,WAAW;IACJ;IACG;IACV,UAAU,YAAY,iBAAiB,WAAW,IAAI,OAAO;IAC7D,eAAe,iBAAiB,WAAW,EAAE;GAC9C,GAVM,WAAW,EAUjB;EAEL,CAAC;CACI,CAAA;AAEX"}
@@ -1 +1 @@
1
- {"version":3,"file":"AdaptiveQuality.js","names":[],"sources":["../../../../../src/components/shared/three/optimization/AdaptiveQuality.ts"],"sourcesContent":["/**\n * AdaptiveQuality - Dynamic quality adjustment system for mobile performance\n *\n * Monitors real-time FPS and automatically adjusts rendering quality to maintain\n * 55fps target on mobile devices. Provides quality presets and smooth transitions.\n *\n * Features:\n * - Real-time FPS monitoring\n * - Automatic quality adjustment (high/medium/low)\n * - Debounced quality changes to prevent thrashing\n * - Mobile-optimized thresholds\n * - React hook integration\n *\n * @module components/shared/three/optimization/AdaptiveQuality\n * @category Performance Optimization\n * @korean 적응형품질시스템\n */\n\nimport { useFrame } from \"@react-three/fiber\";\nimport { useEffect, useRef, useState } from \"react\";\n\n/**\n * Quality levels for adaptive rendering\n */\nexport type QualityLevel = \"high\" | \"medium\" | \"low\";\n\n/**\n * Quality settings for each level\n */\nexport interface QualitySettings {\n readonly level: QualityLevel;\n readonly shadowMapSize: number;\n readonly maxParticles: number;\n readonly postProcessing: boolean;\n readonly effectsQuality: number; // 0.0-1.0 multiplier\n}\n\n/**\n * FPS thresholds for quality adjustment\n */\nexport interface AdaptiveQualityThresholds {\n /** FPS threshold to downgrade quality */\n readonly downgradeThreshold: number;\n /** FPS threshold to upgrade quality */\n readonly upgradeThreshold: number;\n /** Minimum time between quality changes (ms) */\n readonly debounceTime: number;\n /** Number of frames to average for stability */\n readonly sampleSize: number;\n}\n\n/**\n * Default thresholds for mobile optimization\n */\nconst DEFAULT_MOBILE_THRESHOLDS: AdaptiveQualityThresholds = {\n downgradeThreshold: 45, // Downgrade if fps drops below 45\n upgradeThreshold: 58, // Upgrade if fps consistently above 58\n debounceTime: 2000, // 2 seconds between changes\n sampleSize: 60, // Average over 60 frames (~1 second at 60fps)\n};\n\n/**\n * Quality presets optimized for mobile performance\n */\nexport const QUALITY_PRESETS: Record<QualityLevel, QualitySettings> = {\n high: {\n level: \"high\",\n shadowMapSize: 1536,\n maxParticles: 60,\n postProcessing: false, // Keep disabled on mobile\n effectsQuality: 1.0,\n },\n medium: {\n level: \"medium\",\n shadowMapSize: 1024,\n maxParticles: 40,\n postProcessing: false,\n effectsQuality: 0.75,\n },\n low: {\n level: \"low\",\n shadowMapSize: 512,\n maxParticles: 20,\n postProcessing: false,\n effectsQuality: 0.5,\n },\n} as const;\n\n/**\n * Adaptive quality system class\n */\nexport class AdaptiveQualitySystem {\n private currentQuality: QualityLevel = \"high\";\n private fpsHistory: number[] = [];\n private lastQualityChange = 0;\n private readonly thresholds: AdaptiveQualityThresholds;\n\n constructor(thresholds: Partial<AdaptiveQualityThresholds> = {}) {\n this.thresholds = { ...DEFAULT_MOBILE_THRESHOLDS, ...thresholds };\n }\n\n /**\n * Update system with current FPS\n * Returns new quality level if changed, null otherwise\n */\n update(currentFps: number): QualityLevel | null {\n const now = performance.now();\n\n this.fpsHistory.push(currentFps);\n if (this.fpsHistory.length > this.thresholds.sampleSize) {\n this.fpsHistory.shift();\n }\n\n if (this.fpsHistory.length < this.thresholds.sampleSize) {\n return null;\n }\n\n if (now - this.lastQualityChange < this.thresholds.debounceTime) {\n return null;\n }\n\n const avgFps =\n this.fpsHistory.reduce((a, b) => a + b, 0) / this.fpsHistory.length;\n\n let newQuality: QualityLevel | null = null;\n\n if (\n avgFps < this.thresholds.downgradeThreshold &&\n this.currentQuality !== \"low\"\n ) {\n if (this.currentQuality === \"high\") {\n newQuality = \"medium\";\n } else if (this.currentQuality === \"medium\") {\n newQuality = \"low\";\n }\n } else if (\n avgFps > this.thresholds.upgradeThreshold &&\n this.currentQuality !== \"high\"\n ) {\n if (this.currentQuality === \"low\") {\n newQuality = \"medium\";\n } else if (this.currentQuality === \"medium\") {\n newQuality = \"high\";\n }\n }\n\n if (newQuality !== null) {\n this.currentQuality = newQuality;\n this.lastQualityChange = now;\n this.fpsHistory = []; // Reset history after change\n\n if (import.meta.env.DEV) {\n console.log(\n `[AdaptiveQuality] Quality changed to ${newQuality} (avg fps: ${avgFps.toFixed(1)})`,\n );\n }\n\n return newQuality;\n }\n\n return null;\n }\n\n /**\n * Get current quality level\n */\n getCurrentQuality(): QualityLevel {\n return this.currentQuality;\n }\n\n /**\n * Get settings for current quality level\n */\n getCurrentSettings(): QualitySettings {\n return QUALITY_PRESETS[this.currentQuality];\n }\n\n /**\n * Manually set quality level\n */\n setQuality(quality: QualityLevel): void {\n this.currentQuality = quality;\n this.lastQualityChange = performance.now();\n this.fpsHistory = [];\n }\n\n /**\n * Reset the system\n */\n reset(): void {\n this.fpsHistory = [];\n this.lastQualityChange = 0;\n }\n}\n\n/**\n * React hook for adaptive quality management\n *\n * Automatically monitors FPS and adjusts quality settings.\n * Returns current quality level and settings.\n *\n * @param enabled - Whether adaptive quality is enabled\n * @param isMobile - Whether device is mobile (stricter thresholds)\n * @param onQualityChange - Callback when quality level changes\n * @returns Current quality settings\n *\n * @example\n * ```tsx\n * function CombatScene({ isMobile }) {\n * const quality = useAdaptiveQuality(true, isMobile, (level) => {\n * console.log(`Quality changed to ${level}`);\n * });\n *\n * return (\n * <Canvas shadowMap={{ size: quality.shadowMapSize }}>\n * <ParticleSystem maxParticles={quality.maxParticles} />\n * </Canvas>\n * );\n * }\n * ```\n */\nexport function useAdaptiveQuality(\n enabled: boolean = true,\n isMobile: boolean = false,\n onQualityChange?: (quality: QualityLevel) => void,\n): QualitySettings {\n const initialQuality: QualityLevel = isMobile ? \"medium\" : \"high\";\n\n const [currentQuality, setCurrentQuality] =\n useState<QualityLevel>(initialQuality);\n\n const systemRef = useRef<AdaptiveQualitySystem | null>(null);\n\n useEffect(() => {\n const thresholds = isMobile\n ? {\n downgradeThreshold: 45, // More aggressive on mobile\n upgradeThreshold: 58,\n debounceTime: 2000,\n sampleSize: 60,\n }\n : {\n downgradeThreshold: 50,\n upgradeThreshold: 59,\n debounceTime: 3000,\n sampleSize: 90,\n };\n\n systemRef.current = new AdaptiveQualitySystem(thresholds);\n systemRef.current.setQuality(initialQuality);\n }, [isMobile, initialQuality]);\n\n const lastTimeRef = useRef(0);\n const frameCountRef = useRef(0);\n const initializedRef = useRef(false);\n\n useFrame(() => {\n if (!enabled || !systemRef.current) return;\n\n const now = performance.now();\n\n if (!initializedRef.current) {\n lastTimeRef.current = now;\n initializedRef.current = true;\n return;\n }\n\n const delta = now - lastTimeRef.current;\n\n if (delta > 0) {\n const fps = 1000 / delta;\n frameCountRef.current++;\n\n const newQuality = systemRef.current.update(fps);\n\n if (newQuality !== null) {\n setCurrentQuality(newQuality);\n onQualityChange?.(newQuality);\n }\n }\n\n lastTimeRef.current = now;\n });\n\n return QUALITY_PRESETS[currentQuality];\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"AdaptiveQuality.js","names":[],"sources":["../../../../../src/components/shared/three/optimization/AdaptiveQuality.ts"],"sourcesContent":["/**\n * AdaptiveQuality - Dynamic quality adjustment system for mobile performance\n *\n * Monitors real-time FPS and automatically adjusts rendering quality to maintain\n * 55fps target on mobile devices. Provides quality presets and smooth transitions.\n *\n * Features:\n * - Real-time FPS monitoring\n * - Automatic quality adjustment (high/medium/low)\n * - Debounced quality changes to prevent thrashing\n * - Mobile-optimized thresholds\n * - React hook integration\n *\n * @module components/shared/three/optimization/AdaptiveQuality\n * @category Performance Optimization\n * @korean 적응형품질시스템\n */\n\nimport { useFrame } from \"@react-three/fiber\";\nimport { useEffect, useRef, useState } from \"react\";\n\n/**\n * Quality levels for adaptive rendering\n */\nexport type QualityLevel = \"high\" | \"medium\" | \"low\";\n\n/**\n * Quality settings for each level\n */\nexport interface QualitySettings {\n readonly level: QualityLevel;\n readonly shadowMapSize: number;\n readonly maxParticles: number;\n readonly postProcessing: boolean;\n readonly effectsQuality: number; // 0.0-1.0 multiplier\n}\n\n/**\n * FPS thresholds for quality adjustment\n */\nexport interface AdaptiveQualityThresholds {\n /** FPS threshold to downgrade quality */\n readonly downgradeThreshold: number;\n /** FPS threshold to upgrade quality */\n readonly upgradeThreshold: number;\n /** Minimum time between quality changes (ms) */\n readonly debounceTime: number;\n /** Number of frames to average for stability */\n readonly sampleSize: number;\n}\n\n/**\n * Default thresholds for mobile optimization\n */\nconst DEFAULT_MOBILE_THRESHOLDS: AdaptiveQualityThresholds = {\n downgradeThreshold: 45, // Downgrade if fps drops below 45\n upgradeThreshold: 58, // Upgrade if fps consistently above 58\n debounceTime: 2000, // 2 seconds between changes\n sampleSize: 60, // Average over 60 frames (~1 second at 60fps)\n};\n\n/**\n * Quality presets optimized for mobile performance\n */\nexport const QUALITY_PRESETS: Record<QualityLevel, QualitySettings> = {\n high: {\n level: \"high\",\n shadowMapSize: 1536,\n maxParticles: 60,\n postProcessing: false, // Keep disabled on mobile\n effectsQuality: 1.0,\n },\n medium: {\n level: \"medium\",\n shadowMapSize: 1024,\n maxParticles: 40,\n postProcessing: false,\n effectsQuality: 0.75,\n },\n low: {\n level: \"low\",\n shadowMapSize: 512,\n maxParticles: 20,\n postProcessing: false,\n effectsQuality: 0.5,\n },\n} as const;\n\n/**\n * Adaptive quality system class\n */\nexport class AdaptiveQualitySystem {\n private currentQuality: QualityLevel = \"high\";\n private fpsHistory: number[] = [];\n private lastQualityChange = 0;\n private readonly thresholds: AdaptiveQualityThresholds;\n\n constructor(thresholds: Partial<AdaptiveQualityThresholds> = {}) {\n this.thresholds = { ...DEFAULT_MOBILE_THRESHOLDS, ...thresholds };\n }\n\n /**\n * Update system with current FPS\n * Returns new quality level if changed, null otherwise\n */\n update(currentFps: number): QualityLevel | null {\n const now = performance.now();\n\n this.fpsHistory.push(currentFps);\n if (this.fpsHistory.length > this.thresholds.sampleSize) {\n this.fpsHistory.shift();\n }\n\n if (this.fpsHistory.length < this.thresholds.sampleSize) {\n return null;\n }\n\n if (now - this.lastQualityChange < this.thresholds.debounceTime) {\n return null;\n }\n\n const avgFps =\n this.fpsHistory.reduce((a, b) => a + b, 0) / this.fpsHistory.length;\n\n let newQuality: QualityLevel | null = null;\n\n if (\n avgFps < this.thresholds.downgradeThreshold &&\n this.currentQuality !== \"low\"\n ) {\n if (this.currentQuality === \"high\") {\n newQuality = \"medium\";\n } else if (this.currentQuality === \"medium\") {\n newQuality = \"low\";\n }\n } else if (\n avgFps > this.thresholds.upgradeThreshold &&\n this.currentQuality !== \"high\"\n ) {\n if (this.currentQuality === \"low\") {\n newQuality = \"medium\";\n } else if (this.currentQuality === \"medium\") {\n newQuality = \"high\";\n }\n }\n\n if (newQuality !== null) {\n this.currentQuality = newQuality;\n this.lastQualityChange = now;\n this.fpsHistory = []; // Reset history after change\n\n if (import.meta.env.DEV) {\n console.log(\n `[AdaptiveQuality] Quality changed to ${newQuality} (avg fps: ${avgFps.toFixed(1)})`,\n );\n }\n\n return newQuality;\n }\n\n return null;\n }\n\n /**\n * Get current quality level\n */\n getCurrentQuality(): QualityLevel {\n return this.currentQuality;\n }\n\n /**\n * Get settings for current quality level\n */\n getCurrentSettings(): QualitySettings {\n return QUALITY_PRESETS[this.currentQuality];\n }\n\n /**\n * Manually set quality level\n */\n setQuality(quality: QualityLevel): void {\n this.currentQuality = quality;\n this.lastQualityChange = performance.now();\n this.fpsHistory = [];\n }\n\n /**\n * Reset the system\n */\n reset(): void {\n this.fpsHistory = [];\n this.lastQualityChange = 0;\n }\n}\n\n/**\n * React hook for adaptive quality management\n *\n * Automatically monitors FPS and adjusts quality settings.\n * Returns current quality level and settings.\n *\n * @param enabled - Whether adaptive quality is enabled\n * @param isMobile - Whether device is mobile (stricter thresholds)\n * @param onQualityChange - Callback when quality level changes\n * @returns Current quality settings\n *\n * @example\n * ```tsx\n * function CombatScene({ isMobile }) {\n * const quality = useAdaptiveQuality(true, isMobile, (level) => {\n * console.log(`Quality changed to ${level}`);\n * });\n *\n * return (\n * <Canvas shadowMap={{ size: quality.shadowMapSize }}>\n * <ParticleSystem maxParticles={quality.maxParticles} />\n * </Canvas>\n * );\n * }\n * ```\n */\nexport function useAdaptiveQuality(\n enabled: boolean = true,\n isMobile: boolean = false,\n onQualityChange?: (quality: QualityLevel) => void,\n): QualitySettings {\n const initialQuality: QualityLevel = isMobile ? \"medium\" : \"high\";\n\n const [currentQuality, setCurrentQuality] =\n useState<QualityLevel>(initialQuality);\n\n const systemRef = useRef<AdaptiveQualitySystem | null>(null);\n\n useEffect(() => {\n const thresholds = isMobile\n ? {\n downgradeThreshold: 45, // More aggressive on mobile\n upgradeThreshold: 58,\n debounceTime: 2000,\n sampleSize: 60,\n }\n : {\n downgradeThreshold: 50,\n upgradeThreshold: 59,\n debounceTime: 3000,\n sampleSize: 90,\n };\n\n systemRef.current = new AdaptiveQualitySystem(thresholds);\n systemRef.current.setQuality(initialQuality);\n }, [isMobile, initialQuality]);\n\n const lastTimeRef = useRef(0);\n const frameCountRef = useRef(0);\n const initializedRef = useRef(false);\n\n useFrame(() => {\n if (!enabled || !systemRef.current) return;\n\n const now = performance.now();\n\n if (!initializedRef.current) {\n lastTimeRef.current = now;\n initializedRef.current = true;\n return;\n }\n\n const delta = now - lastTimeRef.current;\n\n if (delta > 0) {\n const fps = 1000 / delta;\n frameCountRef.current++;\n\n const newQuality = systemRef.current.update(fps);\n\n if (newQuality !== null) {\n setCurrentQuality(newQuality);\n onQualityChange?.(newQuality);\n }\n }\n\n lastTimeRef.current = now;\n });\n\n return QUALITY_PRESETS[currentQuality];\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAsDA,IAAM,4BAAuD;CAC3D,oBAAoB;CACpB,kBAAkB;CAClB,cAAc;CACd,YAAY;AACd;;;;AAKA,IAAa,kBAAyD;CACpE,MAAM;EACJ,OAAO;EACP,eAAe;EACf,cAAc;EACd,gBAAgB;EAChB,gBAAgB;CAClB;CACA,QAAQ;EACN,OAAO;EACP,eAAe;EACf,cAAc;EACd,gBAAgB;EAChB,gBAAgB;CAClB;CACA,KAAK;EACH,OAAO;EACP,eAAe;EACf,cAAc;EACd,gBAAgB;EAChB,gBAAgB;CAClB;AACF;;;;AAKA,IAAa,wBAAb,MAAmC;CACjC,iBAAuC;CACvC,aAA+B,CAAC;CAChC,oBAA4B;CAC5B;CAEA,YAAY,aAAiD,CAAC,GAAG;EAC/D,KAAK,aAAa;GAAE,GAAG;GAA2B,GAAG;EAAW;CAClE;;;;;CAMA,OAAO,YAAyC;EAC9C,MAAM,MAAM,YAAY,IAAI;EAE5B,KAAK,WAAW,KAAK,UAAU;EAC/B,IAAI,KAAK,WAAW,SAAS,KAAK,WAAW,YAC3C,KAAK,WAAW,MAAM;EAGxB,IAAI,KAAK,WAAW,SAAS,KAAK,WAAW,YAC3C,OAAO;EAGT,IAAI,MAAM,KAAK,oBAAoB,KAAK,WAAW,cACjD,OAAO;EAGT,MAAM,SACJ,KAAK,WAAW,QAAQ,GAAG,MAAM,IAAI,GAAG,CAAC,IAAI,KAAK,WAAW;EAE/D,IAAI,aAAkC;EAEtC,IACE,SAAS,KAAK,WAAW,sBACzB,KAAK,mBAAmB,OACxB;GACA,IAAI,KAAK,mBAAmB,QAC1B,aAAa;QACR,IAAI,KAAK,mBAAmB,UACjC,aAAa;EAEjB,OAAO,IACL,SAAS,KAAK,WAAW,oBACzB,KAAK,mBAAmB,QACxB;GACA,IAAI,KAAK,mBAAmB,OAC1B,aAAa;QACR,IAAI,KAAK,mBAAmB,UACjC,aAAa;EAEjB;EAEA,IAAI,eAAe,MAAM;GACvB,KAAK,iBAAiB;GACtB,KAAK,oBAAoB;GACzB,KAAK,aAAa,CAAC;GAQnB,OAAO;EACT;EAEA,OAAO;CACT;;;;CAKA,oBAAkC;EAChC,OAAO,KAAK;CACd;;;;CAKA,qBAAsC;EACpC,OAAO,gBAAgB,KAAK;CAC9B;;;;CAKA,WAAW,SAA6B;EACtC,KAAK,iBAAiB;EACtB,KAAK,oBAAoB,YAAY,IAAI;EACzC,KAAK,aAAa,CAAC;CACrB;;;;CAKA,QAAc;EACZ,KAAK,aAAa,CAAC;EACnB,KAAK,oBAAoB;CAC3B;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4BA,SAAgB,mBACd,UAAmB,MACnB,WAAoB,OACpB,iBACiB;CACjB,MAAM,iBAA+B,WAAW,WAAW;CAE3D,MAAM,CAAC,gBAAgB,qBACrB,SAAuB,cAAc;CAEvC,MAAM,YAAY,OAAqC,IAAI;CAE3D,gBAAgB;EAed,UAAU,UAAU,IAAI,sBAdL,WACf;GACE,oBAAoB;GACpB,kBAAkB;GAClB,cAAc;GACd,YAAY;EACd,IACA;GACE,oBAAoB;GACpB,kBAAkB;GAClB,cAAc;GACd,YAAY;EACd,CAEoD;EACxD,UAAU,QAAQ,WAAW,cAAc;CAC7C,GAAG,CAAC,UAAU,cAAc,CAAC;CAE7B,MAAM,cAAc,OAAO,CAAC;CAC5B,MAAM,gBAAgB,OAAO,CAAC;CAC9B,MAAM,iBAAiB,OAAO,KAAK;CAEnC,eAAe;EACb,IAAI,CAAC,WAAW,CAAC,UAAU,SAAS;EAEpC,MAAM,MAAM,YAAY,IAAI;EAE5B,IAAI,CAAC,eAAe,SAAS;GAC3B,YAAY,UAAU;GACtB,eAAe,UAAU;GACzB;EACF;EAEA,MAAM,QAAQ,MAAM,YAAY;EAEhC,IAAI,QAAQ,GAAG;GACb,MAAM,MAAM,MAAO;GACnB,cAAc;GAEd,MAAM,aAAa,UAAU,QAAQ,OAAO,GAAG;GAE/C,IAAI,eAAe,MAAM;IACvB,kBAAkB,UAAU;IAC5B,kBAAkB,UAAU;GAC9B;EACF;EAEA,YAAY,UAAU;CACxB,CAAC;CAED,OAAO,gBAAgB;AACzB"}
@@ -55,11 +55,13 @@ var VitalPointOverlayControlsHtml = ({ visible, onVisibleChange, severityFilters
55
55
  const filteredCount = useMemo(() => {
56
56
  let points = [...KOREAN_VITAL_POINTS];
57
57
  if (severityFilters.length > 0) points = points.filter((vp) => severityFilters.includes(vp.severity));
58
- if (regionFilter !== "all") if (regionFilter === "arms") points = points.filter((vp) => vp.id.startsWith("arm_left_") || vp.id.startsWith("arm_right_"));
59
- else if (regionFilter === "legs") points = points.filter((vp) => vp.id.startsWith("leg_left_") || vp.id.startsWith("leg_right_"));
60
- else {
61
- const prefix = `${regionFilter}_`;
62
- points = points.filter((vp) => vp.id.startsWith(prefix));
58
+ if (regionFilter !== "all") {
59
+ if (regionFilter === "arms") points = points.filter((vp) => vp.id.startsWith("arm_left_") || vp.id.startsWith("arm_right_"));
60
+ else if (regionFilter === "legs") points = points.filter((vp) => vp.id.startsWith("leg_left_") || vp.id.startsWith("leg_right_"));
61
+ else {
62
+ const prefix = `${regionFilter}_`;
63
+ points = points.filter((vp) => vp.id.startsWith(prefix));
64
+ }
63
65
  }
64
66
  if (searchQuery) {
65
67
  const query = searchQuery.toLowerCase();
@@ -1 +1 @@
1
- {"version":3,"file":"VitalPointOverlayControlsHtml.js","names":[],"sources":["../../../../../src/components/shared/three/ui/VitalPointOverlayControlsHtml.tsx"],"sourcesContent":["/**\n * VitalPointOverlayControlsHtml - UI controls for vital point visualization\n *\n * Provides comprehensive controls for the 70-point vital point overlay system:\n * - Toggle overlay visibility\n * - Filter by severity level\n * - Filter by body region\n * - Search vital points\n * - Adjust marker scale\n * - Toggle labels\n * - Toggle animations\n *\n * @module components/shared/three/ui/VitalPointOverlayControlsHtml\n */\n\nimport { Html } from \"@react-three/drei\";\nimport React, { useCallback, useMemo, useState } from \"react\";\nimport {\n KOREAN_VITAL_POINTS,\n getVitalPointsStats,\n} from \"../../../../systems/vitalpoint/KoreanVitalPoints\";\nimport { VitalPointSeverity } from \"../../../../types/common\";\nimport { FONT_FAMILY, KOREAN_COLORS } from \"../../../../types/constants\";\nimport { hexColorToCSS, hexToRgbaString } from \"../../../../utils/colorUtils\";\nimport type { BodyRegionFilter } from \"../effects/VitalPointMarkers3D\";\n\nexport type { BodyRegionFilter } from \"../effects/VitalPointMarkers3D\";\n\n/**\n * Props for VitalPointOverlayControlsHtml component\n */\nexport interface VitalPointOverlayControlsProps {\n /** Whether overlay is currently visible */\n readonly visible: boolean;\n /** Callback when visibility changes */\n readonly onVisibleChange: (visible: boolean) => void;\n /** Current severity filters */\n readonly severityFilters: VitalPointSeverity[];\n /** Callback when severity filters change */\n readonly onSeverityFiltersChange: (filters: VitalPointSeverity[]) => void;\n /** Current region filter */\n readonly regionFilter: BodyRegionFilter;\n /** Callback when region filter changes */\n readonly onRegionFilterChange: (filter: BodyRegionFilter) => void;\n /** Current search query */\n readonly searchQuery?: string;\n /** Callback when search query changes */\n readonly onSearchQueryChange?: (query: string) => void;\n /** Whether labels are shown */\n readonly showLabels: boolean;\n /** Callback when label visibility changes */\n readonly onShowLabelsChange: (show: boolean) => void;\n /** Whether animations are enabled */\n readonly animated: boolean;\n /** Callback when animation state changes */\n readonly onAnimatedChange: (animated: boolean) => void;\n /** Marker scale multiplier */\n readonly scale: number;\n /** Callback when scale changes */\n readonly onScaleChange: (scale: number) => void;\n /** Whether on mobile device */\n readonly isMobile?: boolean;\n /**\n * Screen position for the control panel.\n *\n * All values must be valid CSS position values, such as `\"20px\"`, `\"10%\"`, `\"1rem\"`, or `\"auto\"`.\n * These are applied directly to the `style` of the Html overlay container.\n */\n readonly screenPosition?: {\n /** CSS `top` position (e.g., \"20px\", \"10%\", \"1rem\", \"auto\") */\n top?: string;\n /** CSS `left` position (e.g., \"20px\", \"10%\", \"1rem\", \"auto\") */\n left?: string;\n /** CSS `right` position (e.g., \"20px\", \"10%\", \"1rem\", \"auto\") */\n right?: string;\n /** CSS `bottom` position (e.g., \"20px\", \"10%\", \"1rem\", \"auto\") */\n bottom?: string;\n };\n}\n\n/**\n * Convert numeric color to CSS hex string\n */\n/**\n * Get color for severity level\n */\nconst getSeverityColor = (severity: VitalPointSeverity): string => {\n switch (severity) {\n case VitalPointSeverity.LETHAL:\n return hexColorToCSS(KOREAN_COLORS.NEGATIVE_RED);\n case VitalPointSeverity.CRITICAL:\n return hexColorToCSS(KOREAN_COLORS.HEALTH_LOW);\n case VitalPointSeverity.MAJOR:\n return hexColorToCSS(KOREAN_COLORS.ACCENT_GOLD);\n case VitalPointSeverity.MODERATE:\n return hexColorToCSS(KOREAN_COLORS.TRIGRAM_GEON_PRIMARY);\n case VitalPointSeverity.MINOR:\n return hexColorToCSS(KOREAN_COLORS.POSITIVE_GREEN);\n default:\n return hexColorToCSS(KOREAN_COLORS.NEON_CYAN);\n }\n};\n\n/**\n * VitalPointOverlayControlsHtml Component\n * Provides comprehensive UI for vital point visualization control\n */\nexport const VitalPointOverlayControlsHtml: React.FC<\n VitalPointOverlayControlsProps\n> = ({\n visible,\n onVisibleChange,\n severityFilters,\n onSeverityFiltersChange,\n regionFilter,\n onRegionFilterChange,\n searchQuery: externalSearchQuery,\n onSearchQueryChange,\n showLabels,\n onShowLabelsChange,\n animated,\n onAnimatedChange,\n scale,\n onScaleChange,\n isMobile = false,\n screenPosition,\n}) => {\n const [expanded, setExpanded] = useState(false);\n\n const [internalSearchQuery, setInternalSearchQuery] = useState(\"\");\n const searchQuery = externalSearchQuery ?? internalSearchQuery;\n const setSearchQuery = onSearchQueryChange ?? setInternalSearchQuery;\n\n const stats = useMemo(() => getVitalPointsStats(), []);\n\n const defaultPosition: {\n top?: string;\n left?: string;\n right?: string;\n bottom?: string;\n } = useMemo(\n () => ({\n top: isMobile ? \"180px\" : \"220px\",\n left: isMobile ? \"10px\" : \"20px\",\n }),\n [isMobile]\n );\n\n const finalPosition = screenPosition ?? defaultPosition;\n\n const filteredCount = useMemo(() => {\n let points = [...KOREAN_VITAL_POINTS];\n\n if (severityFilters.length > 0) {\n points = points.filter((vp) => severityFilters.includes(vp.severity));\n }\n\n if (regionFilter !== \"all\") {\n if (regionFilter === \"arms\") {\n points = points.filter(\n (vp) =>\n vp.id.startsWith(\"arm_left_\") || vp.id.startsWith(\"arm_right_\")\n );\n } else if (regionFilter === \"legs\") {\n points = points.filter(\n (vp) =>\n vp.id.startsWith(\"leg_left_\") || vp.id.startsWith(\"leg_right_\")\n );\n } else {\n const prefix = `${regionFilter}_`;\n points = points.filter((vp) => vp.id.startsWith(prefix));\n }\n }\n\n if (searchQuery) {\n const query = searchQuery.toLowerCase();\n points = points.filter(\n (vp) =>\n vp.names.korean.toLowerCase().includes(query) ||\n vp.names.english.toLowerCase().includes(query) ||\n vp.names.romanized.toLowerCase().includes(query) ||\n vp.id.toLowerCase().includes(query)\n );\n }\n\n return points.length;\n }, [severityFilters, regionFilter, searchQuery]);\n\n const toggleSeverityFilter = useCallback(\n (severity: VitalPointSeverity) => {\n const newFilters = severityFilters.includes(severity)\n ? severityFilters.filter((s) => s !== severity)\n : [...severityFilters, severity];\n onSeverityFiltersChange(newFilters);\n },\n [severityFilters, onSeverityFiltersChange]\n );\n\n const severityOptions: VitalPointSeverity[] = [\n VitalPointSeverity.LETHAL,\n VitalPointSeverity.CRITICAL,\n VitalPointSeverity.MAJOR,\n VitalPointSeverity.MODERATE,\n VitalPointSeverity.MINOR,\n ];\n\n const regionOptions: {\n value: BodyRegionFilter;\n label: string;\n korean: string;\n }[] = [\n { value: \"all\", label: \"All Regions\", korean: \"전체\" },\n { value: \"head\", label: \"Head\", korean: \"머리\" },\n { value: \"torso\", label: \"Torso\", korean: \"몸통\" },\n { value: \"arms\", label: \"Arms\", korean: \"팔\" },\n { value: \"legs\", label: \"Legs\", korean: \"다리\" },\n ];\n\n const panelWidth = isMobile ? 280 : 350;\n const buttonHeight = isMobile ? 32 : 36;\n const fontSize = isMobile ? 11 : 13;\n const smallFontSize = isMobile ? 9 : 10;\n\n return (\n <Html fullscreen style={{ pointerEvents: \"none\" }}>\n <div\n style={{\n position: \"absolute\",\n top: finalPosition.top,\n left: finalPosition.left,\n right: finalPosition.right,\n bottom: finalPosition.bottom,\n width: panelWidth,\n background: `${hexColorToCSS(KOREAN_COLORS.UI_BACKGROUND_DARK)}f0`,\n border: `2px solid ${hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN)}`,\n borderRadius: \"12px\",\n padding: isMobile ? \"12px\" : \"16px\",\n fontFamily: FONT_FAMILY.KOREAN,\n color: hexColorToCSS(KOREAN_COLORS.TEXT_PRIMARY),\n boxShadow: `0 0 30px ${hexColorToCSS(\n KOREAN_COLORS.PRIMARY_CYAN\n )}40, inset 0 0 20px ${hexColorToCSS(\n KOREAN_COLORS.UI_BACKGROUND_DARK\n )}80`,\n transition: \"all 0.3s ease\",\n pointerEvents: \"all\",\n zIndex: 200,\n }}\n data-testid=\"vital-point-overlay-controls\"\n >\n {/* Header with toggle */}\n <div\n style={{\n display: \"flex\",\n justifyContent: \"space-between\",\n alignItems: \"center\",\n marginBottom: \"12px\",\n paddingBottom: \"12px\",\n borderBottom: `1px solid ${hexColorToCSS(\n KOREAN_COLORS.PRIMARY_CYAN\n )}40`,\n background: `linear-gradient(90deg, ${hexColorToCSS(\n KOREAN_COLORS.UI_BACKGROUND_DARK\n )}00 0%, ${hexColorToCSS(\n KOREAN_COLORS.PRIMARY_CYAN\n )}10 50%, ${hexColorToCSS(KOREAN_COLORS.UI_BACKGROUND_DARK)}00 100%)`,\n }}\n >\n <div>\n <div style={{ fontSize: isMobile ? 14 : 16, fontWeight: \"bold\" }}>\n 급소 오버레이 | Vital Points\n </div>\n <div\n style={{\n fontSize: smallFontSize,\n color: hexColorToCSS(KOREAN_COLORS.TEXT_SECONDARY),\n marginTop: \"2px\",\n }}\n >\n {filteredCount} / {stats.total} 표시 | Showing\n </div>\n </div>\n <button\n onClick={() => setExpanded(!expanded)}\n style={{\n background: `linear-gradient(135deg, ${hexColorToCSS(\n KOREAN_COLORS.UI_BACKGROUND_MEDIUM\n )} 0%, ${hexColorToCSS(KOREAN_COLORS.UI_BACKGROUND_DARK)} 100%)`,\n border: `2px solid ${hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN)}`,\n borderRadius: \"6px\",\n padding: \"8px 14px\",\n color: hexColorToCSS(KOREAN_COLORS.TEXT_PRIMARY),\n fontSize,\n cursor: \"pointer\",\n transition: \"all 0.2s ease\",\n boxShadow: `0 2px 8px ${hexColorToCSS(\n KOREAN_COLORS.PRIMARY_CYAN\n )}30`,\n }}\n onMouseEnter={(e) => {\n e.currentTarget.style.transform = \"scale(1.05)\";\n e.currentTarget.style.boxShadow = `0 4px 12px ${hexColorToCSS(\n KOREAN_COLORS.PRIMARY_CYAN\n )}50`;\n }}\n onMouseLeave={(e) => {\n e.currentTarget.style.transform = \"scale(1)\";\n e.currentTarget.style.boxShadow = `0 2px 8px ${hexColorToCSS(\n KOREAN_COLORS.PRIMARY_CYAN\n )}30`;\n }}\n data-testid=\"toggle-expand-button\"\n >\n {expanded ? \"▼\" : \"▶\"}\n </button>\n </div>\n\n {/* Main toggle */}\n <div style={{ marginBottom: \"14px\" }}>\n <button\n onClick={() => onVisibleChange(!visible)}\n style={{\n width: \"100%\",\n height: buttonHeight,\n background: visible\n ? `linear-gradient(135deg, ${hexColorToCSS(\n KOREAN_COLORS.ACCENT_GOLD\n )} 0%, ${hexColorToCSS(KOREAN_COLORS.SECONDARY_YELLOW)} 100%)`\n : `linear-gradient(135deg, ${hexColorToCSS(\n KOREAN_COLORS.UI_BACKGROUND_MEDIUM\n )} 0%, ${hexColorToCSS(KOREAN_COLORS.UI_BACKGROUND_DARK)} 100%)`,\n border: `2px solid ${\n visible\n ? hexColorToCSS(KOREAN_COLORS.ACCENT_GOLD)\n : hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN)\n }`,\n borderRadius: \"8px\",\n color: visible ? hexColorToCSS(KOREAN_COLORS.KOREAN_BLACK) : hexColorToCSS(KOREAN_COLORS.TEXT_PRIMARY),\n fontSize: isMobile ? 13 : 15,\n fontWeight: \"bold\",\n cursor: \"pointer\",\n transition: \"all 0.3s ease\",\n boxShadow: visible\n ? `0 4px 16px ${hexColorToCSS(\n KOREAN_COLORS.ACCENT_GOLD\n )}60, inset 0 2px 4px ${hexToRgbaString(KOREAN_COLORS.TEXT_PRIMARY, 0.2)}`\n : `0 2px 8px ${hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN)}30`,\n textTransform: \"uppercase\",\n letterSpacing: \"0.5px\",\n }}\n onMouseEnter={(e) => {\n e.currentTarget.style.transform = \"translateY(-2px)\";\n e.currentTarget.style.boxShadow = visible\n ? `0 6px 20px ${hexColorToCSS(KOREAN_COLORS.ACCENT_GOLD)}80`\n : `0 4px 12px ${hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN)}50`;\n }}\n onMouseLeave={(e) => {\n e.currentTarget.style.transform = \"translateY(0)\";\n e.currentTarget.style.boxShadow = visible\n ? `0 4px 16px ${hexColorToCSS(KOREAN_COLORS.ACCENT_GOLD)}60`\n : `0 2px 8px ${hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN)}30`;\n }}\n data-testid=\"toggle-visibility-button\"\n >\n {visible ? \"✓ 활성화 | Enabled\" : \"비활성화 | Disabled\"}\n </button>\n </div>\n\n {/* Expanded controls */}\n {expanded && visible && (\n <>\n {/* Severity filters */}\n <div style={{ marginBottom: \"14px\" }}>\n <div\n style={{\n fontSize,\n marginBottom: \"8px\",\n color: hexColorToCSS(KOREAN_COLORS.ACCENT_CYAN),\n fontWeight: \"600\",\n textTransform: \"uppercase\",\n letterSpacing: \"1px\",\n }}\n >\n 심각도 필터 | Severity Filter\n </div>\n <div\n style={{\n display: \"flex\",\n flexWrap: \"wrap\",\n gap: \"8px\",\n }}\n >\n {severityOptions.map((severity) => {\n const isActive = severityFilters.includes(severity);\n const severityColor = getSeverityColor(severity);\n return (\n <button\n key={severity}\n onClick={() => toggleSeverityFilter(severity)}\n style={{\n background: isActive\n ? `linear-gradient(135deg, ${severityColor} 0%, ${severityColor}cc 100%)`\n : `${hexColorToCSS(KOREAN_COLORS.UI_BACKGROUND_MEDIUM)}`,\n border: `2px solid ${severityColor}`,\n borderRadius: \"6px\",\n padding: \"6px 12px\",\n color: hexColorToCSS(KOREAN_COLORS.TEXT_PRIMARY),\n fontSize: smallFontSize,\n cursor: \"pointer\",\n opacity: isActive ? 1 : 0.6,\n transition: \"all 0.2s ease\",\n fontWeight: isActive ? \"bold\" : \"normal\",\n boxShadow: isActive\n ? `0 2px 8px ${severityColor}50`\n : \"none\",\n }}\n onMouseEnter={(e) => {\n e.currentTarget.style.opacity = \"1\";\n e.currentTarget.style.transform = \"scale(1.05)\";\n }}\n onMouseLeave={(e) => {\n e.currentTarget.style.opacity = isActive ? \"1\" : \"0.6\";\n e.currentTarget.style.transform = \"scale(1)\";\n }}\n data-testid={`severity-filter-${severity}`}\n >\n {severity}\n </button>\n );\n })}\n </div>\n </div>\n\n {/* Region filter */}\n <div style={{ marginBottom: \"14px\" }}>\n <div\n style={{\n fontSize,\n marginBottom: \"8px\",\n color: hexColorToCSS(KOREAN_COLORS.ACCENT_CYAN),\n fontWeight: \"600\",\n textTransform: \"uppercase\",\n letterSpacing: \"1px\",\n }}\n >\n 부위 필터 | Region Filter\n </div>\n <div\n style={{\n display: \"flex\",\n flexWrap: \"wrap\",\n gap: \"8px\",\n }}\n >\n {regionOptions.map((option) => {\n const isActive = regionFilter === option.value;\n return (\n <button\n key={option.value}\n onClick={() => onRegionFilterChange(option.value)}\n style={{\n background: isActive\n ? `linear-gradient(135deg, ${hexColorToCSS(\n KOREAN_COLORS.PRIMARY_CYAN\n )} 0%, ${hexColorToCSS(\n KOREAN_COLORS.ACCENT_BLUE\n )} 100%)`\n : `${hexColorToCSS(KOREAN_COLORS.UI_BACKGROUND_MEDIUM)}`,\n border: `2px solid ${hexColorToCSS(\n KOREAN_COLORS.PRIMARY_CYAN\n )}`,\n borderRadius: \"6px\",\n padding: \"6px 12px\",\n color: hexColorToCSS(KOREAN_COLORS.TEXT_PRIMARY),\n fontSize: smallFontSize,\n cursor: \"pointer\",\n opacity: isActive ? 1 : 0.6,\n transition: \"all 0.2s ease\",\n fontWeight: isActive ? \"bold\" : \"normal\",\n boxShadow: isActive\n ? `0 2px 8px ${hexColorToCSS(\n KOREAN_COLORS.PRIMARY_CYAN\n )}50`\n : \"none\",\n }}\n onMouseEnter={(e) => {\n e.currentTarget.style.opacity = \"1\";\n e.currentTarget.style.transform = \"scale(1.05)\";\n }}\n onMouseLeave={(e) => {\n e.currentTarget.style.opacity = isActive ? \"1\" : \"0.6\";\n e.currentTarget.style.transform = \"scale(1)\";\n }}\n data-testid={`region-filter-${option.value}`}\n >\n {option.korean} | {option.label}\n </button>\n );\n })}\n </div>\n </div>\n\n {/* Search box with clear button */}\n <div style={{ marginBottom: \"14px\" }}>\n <div\n style={{\n fontSize,\n marginBottom: \"8px\",\n color: hexColorToCSS(KOREAN_COLORS.ACCENT_CYAN),\n fontWeight: \"600\",\n textTransform: \"uppercase\",\n letterSpacing: \"1px\",\n }}\n >\n 검색 | Search\n </div>\n <div style={{ position: \"relative\" }}>\n <input\n type=\"text\"\n value={searchQuery}\n onChange={(e) => setSearchQuery(e.target.value)}\n placeholder=\"급소 이름... | Point name...\"\n style={{\n width: \"100%\",\n height: buttonHeight,\n background: `${hexColorToCSS(\n KOREAN_COLORS.UI_BACKGROUND_MEDIUM\n )}`,\n border: `2px solid ${hexColorToCSS(\n KOREAN_COLORS.PRIMARY_CYAN\n )}40`,\n borderRadius: \"8px\",\n padding: \"0 40px 0 14px\", // Add right padding for clear button\n color: hexColorToCSS(KOREAN_COLORS.TEXT_PRIMARY),\n fontSize,\n fontFamily: FONT_FAMILY.KOREAN,\n transition: \"all 0.2s ease\",\n outline: \"none\",\n }}\n onFocus={(e) => {\n e.currentTarget.style.borderColor = hexColorToCSS(\n KOREAN_COLORS.PRIMARY_CYAN\n );\n e.currentTarget.style.boxShadow = `0 0 12px ${hexColorToCSS(\n KOREAN_COLORS.PRIMARY_CYAN\n )}40`;\n }}\n onBlur={(e) => {\n e.currentTarget.style.borderColor = `${hexColorToCSS(\n KOREAN_COLORS.PRIMARY_CYAN\n )}40`;\n e.currentTarget.style.boxShadow = \"none\";\n }}\n data-testid=\"search-input\"\n />\n {/* Clear button */}\n {searchQuery && (\n <button\n onClick={() => setSearchQuery(\"\")}\n style={{\n position: \"absolute\",\n right: \"8px\",\n top: \"50%\",\n transform: \"translateY(-50%)\",\n background: \"transparent\",\n border: \"none\",\n color: hexColorToCSS(KOREAN_COLORS.TEXT_SECONDARY),\n cursor: \"pointer\",\n fontSize: \"16px\",\n padding: \"4px\",\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n borderRadius: \"4px\",\n transition: \"all 0.2s ease\",\n }}\n onMouseEnter={(e) => {\n e.currentTarget.style.color = hexColorToCSS(\n KOREAN_COLORS.ACCENT_RED\n );\n e.currentTarget.style.background = `${hexColorToCSS(\n KOREAN_COLORS.UI_BACKGROUND_MEDIUM\n )}`;\n }}\n onMouseLeave={(e) => {\n e.currentTarget.style.color = hexColorToCSS(\n KOREAN_COLORS.TEXT_SECONDARY\n );\n e.currentTarget.style.background = \"transparent\";\n }}\n data-testid=\"search-clear-button\"\n title=\"Clear search\"\n >\n ✕\n </button>\n )}\n </div>\n </div>\n\n {/* Display options */}\n <div style={{ marginBottom: \"14px\" }}>\n <div\n style={{\n fontSize,\n marginBottom: \"8px\",\n color: hexColorToCSS(KOREAN_COLORS.ACCENT_CYAN),\n fontWeight: \"600\",\n textTransform: \"uppercase\",\n letterSpacing: \"1px\",\n }}\n >\n 표시 옵션 | Display Options\n </div>\n <div\n style={{ display: \"flex\", flexDirection: \"column\", gap: \"8px\" }}\n >\n <label\n style={{\n display: \"flex\",\n alignItems: \"center\",\n gap: \"8px\",\n cursor: \"pointer\",\n }}\n >\n <input\n type=\"checkbox\"\n checked={showLabels}\n onChange={(e) => onShowLabelsChange(e.target.checked)}\n style={{\n width: \"16px\",\n height: \"16px\",\n accentColor: hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN),\n }}\n data-testid=\"show-labels-checkbox\"\n />\n <span style={{ fontSize }}>라벨 표시 | Show Labels</span>\n </label>\n <label\n style={{\n display: \"flex\",\n alignItems: \"center\",\n gap: \"8px\",\n cursor: \"pointer\",\n }}\n >\n <input\n type=\"checkbox\"\n checked={animated}\n onChange={(e) => onAnimatedChange(e.target.checked)}\n style={{\n width: \"16px\",\n height: \"16px\",\n accentColor: hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN),\n }}\n data-testid=\"animated-checkbox\"\n />\n <span style={{ fontSize }}>애니메이션 | Animations</span>\n </label>\n </div>\n </div>\n\n {/* Reset filters button */}\n {(severityFilters.length > 0 ||\n regionFilter !== \"all\" ||\n searchQuery !== \"\") && (\n <div style={{ marginBottom: \"14px\" }}>\n <button\n onClick={() => {\n onSeverityFiltersChange([]);\n onRegionFilterChange(\"all\");\n setSearchQuery(\"\");\n }}\n style={{\n width: \"100%\",\n height: buttonHeight - 4,\n background: `${hexColorToCSS(\n KOREAN_COLORS.UI_BACKGROUND_MEDIUM\n )}`,\n border: `2px solid ${hexColorToCSS(\n KOREAN_COLORS.ACCENT_ORANGE\n )}`,\n borderRadius: \"6px\",\n color: hexColorToCSS(KOREAN_COLORS.ACCENT_ORANGE),\n fontSize: smallFontSize,\n cursor: \"pointer\",\n transition: \"all 0.2s ease\",\n fontWeight: \"bold\",\n }}\n onMouseEnter={(e) => {\n e.currentTarget.style.background = `${hexColorToCSS(\n KOREAN_COLORS.ACCENT_ORANGE\n )}20`;\n e.currentTarget.style.transform = \"translateY(-1px)\";\n }}\n onMouseLeave={(e) => {\n e.currentTarget.style.background = `${hexColorToCSS(\n KOREAN_COLORS.UI_BACKGROUND_MEDIUM\n )}`;\n e.currentTarget.style.transform = \"translateY(0)\";\n }}\n data-testid=\"reset-filters-button\"\n >\n 🔄 필터 초기화 | Reset Filters\n </button>\n </div>\n )}\n\n {/* Scale slider */}\n <div>\n <div\n style={{\n fontSize,\n marginBottom: \"6px\",\n color: hexColorToCSS(KOREAN_COLORS.ACCENT_CYAN),\n }}\n >\n 크기 | Scale: {scale.toFixed(1)}x\n </div>\n <input\n type=\"range\"\n min=\"0.5\"\n max=\"2.0\"\n step=\"0.1\"\n value={scale}\n onChange={(e) => onScaleChange(parseFloat(e.target.value))}\n style={{\n width: \"100%\",\n accentColor: hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN),\n }}\n data-testid=\"scale-slider\"\n />\n </div>\n\n {/* Statistics */}\n <div\n style={{\n marginTop: \"12px\",\n paddingTop: \"12px\",\n borderTop: `1px solid ${hexColorToCSS(\n KOREAN_COLORS.PRIMARY_CYAN\n )}44`,\n fontSize: smallFontSize,\n color: hexColorToCSS(KOREAN_COLORS.TEXT_SECONDARY),\n }}\n >\n <div>\n 머리: {stats.byRegion.head} | 몸통: {stats.byRegion.torso}\n </div>\n <div>\n 팔: {stats.byRegion.arms} | 다리: {stats.byRegion.legs}\n </div>\n </div>\n </>\n )}\n </div>\n </Html>\n );\n};\n\nexport default VitalPointOverlayControlsHtml;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsFA,IAAM,oBAAoB,aAAyC;CACjE,QAAQ,UAAR;EACE,KAAK,mBAAmB,QACtB,OAAO,cAAc,cAAc,YAAY;EACjD,KAAK,mBAAmB,UACtB,OAAO,cAAc,cAAc,UAAU;EAC/C,KAAK,mBAAmB,OACtB,OAAO,cAAc,cAAc,WAAW;EAChD,KAAK,mBAAmB,UACtB,OAAO,cAAc,cAAc,oBAAoB;EACzD,KAAK,mBAAmB,OACtB,OAAO,cAAc,cAAc,cAAc;EACnD,SACE,OAAO,cAAc,cAAc,SAAS;CAChD;AACF;;;;;AAMA,IAAa,iCAER,EACH,SACA,iBACA,iBACA,yBACA,cACA,sBACA,aAAa,qBACb,qBACA,YACA,oBACA,UACA,kBACA,OACA,eACA,WAAW,OACX,qBACI;CACJ,MAAM,CAAC,UAAU,eAAe,SAAS,KAAK;CAE9C,MAAM,CAAC,qBAAqB,0BAA0B,SAAS,EAAE;CACjE,MAAM,cAAc,uBAAuB;CAC3C,MAAM,iBAAiB,uBAAuB;CAE9C,MAAM,QAAQ,cAAc,oBAAoB,GAAG,CAAC,CAAC;CAErD,MAAM,kBAKF,eACK;EACL,KAAK,WAAW,UAAU;EAC1B,MAAM,WAAW,SAAS;CAC5B,IACA,CAAC,QAAQ,CACX;CAEA,MAAM,gBAAgB,kBAAkB;CAExC,MAAM,gBAAgB,cAAc;EAClC,IAAI,SAAS,CAAC,GAAG,mBAAmB;EAEpC,IAAI,gBAAgB,SAAS,GAC3B,SAAS,OAAO,QAAQ,OAAO,gBAAgB,SAAS,GAAG,QAAQ,CAAC;EAGtE,IAAI,iBAAiB,OACnB,IAAI,iBAAiB,QACnB,SAAS,OAAO,QACb,OACC,GAAG,GAAG,WAAW,WAAW,KAAK,GAAG,GAAG,WAAW,YAAY,CAClE;OACK,IAAI,iBAAiB,QAC1B,SAAS,OAAO,QACb,OACC,GAAG,GAAG,WAAW,WAAW,KAAK,GAAG,GAAG,WAAW,YAAY,CAClE;OACK;GACL,MAAM,SAAS,GAAG,aAAa;GAC/B,SAAS,OAAO,QAAQ,OAAO,GAAG,GAAG,WAAW,MAAM,CAAC;EACzD;EAGF,IAAI,aAAa;GACf,MAAM,QAAQ,YAAY,YAAY;GACtC,SAAS,OAAO,QACb,OACC,GAAG,MAAM,OAAO,YAAY,CAAC,CAAC,SAAS,KAAK,KAC5C,GAAG,MAAM,QAAQ,YAAY,CAAC,CAAC,SAAS,KAAK,KAC7C,GAAG,MAAM,UAAU,YAAY,CAAC,CAAC,SAAS,KAAK,KAC/C,GAAG,GAAG,YAAY,CAAC,CAAC,SAAS,KAAK,CACtC;EACF;EAEA,OAAO,OAAO;CAChB,GAAG;EAAC;EAAiB;EAAc;CAAW,CAAC;CAE/C,MAAM,uBAAuB,aAC1B,aAAiC;EAIhC,wBAHmB,gBAAgB,SAAS,QAAQ,IAChD,gBAAgB,QAAQ,MAAM,MAAM,QAAQ,IAC5C,CAAC,GAAG,iBAAiB,QAAQ,CACC;CACpC,GACA,CAAC,iBAAiB,uBAAuB,CAC3C;CAEA,MAAM,kBAAwC;EAC5C,mBAAmB;EACnB,mBAAmB;EACnB,mBAAmB;EACnB,mBAAmB;EACnB,mBAAmB;CACrB;CAEA,MAAM,gBAIA;EACJ;GAAE,OAAO;GAAO,OAAO;GAAe,QAAQ;EAAK;EACnD;GAAE,OAAO;GAAQ,OAAO;GAAQ,QAAQ;EAAK;EAC7C;GAAE,OAAO;GAAS,OAAO;GAAS,QAAQ;EAAK;EAC/C;GAAE,OAAO;GAAQ,OAAO;GAAQ,QAAQ;EAAI;EAC5C;GAAE,OAAO;GAAQ,OAAO;GAAQ,QAAQ;EAAK;CAC/C;CAEA,MAAM,aAAa,WAAW,MAAM;CACpC,MAAM,eAAe,WAAW,KAAK;CACrC,MAAM,WAAW,WAAW,KAAK;CACjC,MAAM,gBAAgB,WAAW,IAAI;CAErC,OACE,oBAAC,MAAD;EAAM,YAAA;EAAW,OAAO,EAAE,eAAe,OAAO;EAC9C,UAAA,qBAAC,OAAD;GACE,OAAO;IACL,UAAU;IACV,KAAK,cAAc;IACnB,MAAM,cAAc;IACpB,OAAO,cAAc;IACrB,QAAQ,cAAc;IACtB,OAAO;IACP,YAAY,GAAG,cAAc,cAAc,kBAAkB,EAAE;IAC/D,QAAQ,aAAa,cAAc,cAAc,YAAY;IAC7D,cAAc;IACd,SAAS,WAAW,SAAS;IAC7B,YAAY,YAAY;IACxB,OAAO,cAAc,cAAc,YAAY;IAC/C,WAAW,YAAY,cACrB,cAAc,YAChB,EAAE,qBAAqB,cACrB,cAAc,kBAChB,EAAE;IACF,YAAY;IACZ,eAAe;IACf,QAAQ;GACV;GACA,eAAY;GAvBd,UAAA;IA0BE,qBAAC,OAAD;KACE,OAAO;MACL,SAAS;MACT,gBAAgB;MAChB,YAAY;MACZ,cAAc;MACd,eAAe;MACf,cAAc,aAAa,cACzB,cAAc,YAChB,EAAE;MACF,YAAY,0BAA0B,cACpC,cAAc,kBAChB,EAAE,SAAS,cACT,cAAc,YAChB,EAAE,UAAU,cAAc,cAAc,kBAAkB,EAAE;KAC9D;KAfF,UAAA,CAiBE,qBAAC,OAAD,EAAA,UAAA,CACE,oBAAC,OAAD;MAAK,OAAO;OAAE,UAAU,WAAW,KAAK;OAAI,YAAY;MAAO;MAAG,UAAA;KAE7D,CAAA,GACL,qBAAC,OAAD;MACE,OAAO;OACL,UAAU;OACV,OAAO,cAAc,cAAc,cAAc;OACjD,WAAW;MACb;MALF,UAAA;OAOG;OAAc;OAAI,MAAM;OAAM;MAC5B;KACF,CAAA,CAAA,EAAA,CAAA,GACL,oBAAC,UAAD;MACE,eAAe,YAAY,CAAC,QAAQ;MACpC,OAAO;OACL,YAAY,2BAA2B,cACrC,cAAc,oBAChB,EAAE,OAAO,cAAc,cAAc,kBAAkB,EAAE;OACzD,QAAQ,aAAa,cAAc,cAAc,YAAY;OAC7D,cAAc;OACd,SAAS;OACT,OAAO,cAAc,cAAc,YAAY;OAC/C;OACA,QAAQ;OACR,YAAY;OACZ,WAAW,aAAa,cACtB,cAAc,YAChB,EAAE;MACJ;MACA,eAAe,MAAM;OACnB,EAAE,cAAc,MAAM,YAAY;OAClC,EAAE,cAAc,MAAM,YAAY,cAAc,cAC9C,cAAc,YAChB,EAAE;MACJ;MACA,eAAe,MAAM;OACnB,EAAE,cAAc,MAAM,YAAY;OAClC,EAAE,cAAc,MAAM,YAAY,aAAa,cAC7C,cAAc,YAChB,EAAE;MACJ;MACA,eAAY;MAEX,UAAA,WAAW,MAAM;KACZ,CAAA,CACL;;IAGL,oBAAC,OAAD;KAAK,OAAO,EAAE,cAAc,OAAO;KACjC,UAAA,oBAAC,UAAD;MACE,eAAe,gBAAgB,CAAC,OAAO;MACvC,OAAO;OACL,OAAO;OACP,QAAQ;OACR,YAAY,UACR,2BAA2B,cACzB,cAAc,WAChB,EAAE,OAAO,cAAc,cAAc,gBAAgB,EAAE,UACvD,2BAA2B,cACzB,cAAc,oBAChB,EAAE,OAAO,cAAc,cAAc,kBAAkB,EAAE;OAC7D,QAAQ,aACN,UACI,cAAc,cAAc,WAAW,IACvC,cAAc,cAAc,YAAY;OAE9C,cAAc;OACd,OAAO,UAAU,cAAc,cAAc,YAAY,IAAI,cAAc,cAAc,YAAY;OACrG,UAAU,WAAW,KAAK;OAC1B,YAAY;OACZ,QAAQ;OACR,YAAY;OACZ,WAAW,UACP,cAAc,cACZ,cAAc,WAChB,EAAE,sBAAsB,gBAAgB,cAAc,cAAc,EAAG,MACvE,aAAa,cAAc,cAAc,YAAY,EAAE;OAC3D,eAAe;OACf,eAAe;MACjB;MACA,eAAe,MAAM;OACnB,EAAE,cAAc,MAAM,YAAY;OAClC,EAAE,cAAc,MAAM,YAAY,UAC9B,cAAc,cAAc,cAAc,WAAW,EAAE,MACvD,cAAc,cAAc,cAAc,YAAY,EAAE;MAC9D;MACA,eAAe,MAAM;OACnB,EAAE,cAAc,MAAM,YAAY;OAClC,EAAE,cAAc,MAAM,YAAY,UAC9B,cAAc,cAAc,cAAc,WAAW,EAAE,MACvD,aAAa,cAAc,cAAc,YAAY,EAAE;MAC7D;MACA,eAAY;MAEX,UAAA,UAAU,oBAAoB;KACzB,CAAA;IACL,CAAA;IAGJ,YAAY,WACX,qBAAA,UAAA,EAAA,UAAA;KAEE,qBAAC,OAAD;MAAK,OAAO,EAAE,cAAc,OAAO;MAAnC,UAAA,CACE,oBAAC,OAAD;OACE,OAAO;QACL;QACA,cAAc;QACd,OAAO,cAAc,cAAc,WAAW;QAC9C,YAAY;QACZ,eAAe;QACf,eAAe;OACjB;OACD,UAAA;MAEI,CAAA,GACL,oBAAC,OAAD;OACE,OAAO;QACL,SAAS;QACT,UAAU;QACV,KAAK;OACP;OAEC,UAAA,gBAAgB,KAAK,aAAa;QACjC,MAAM,WAAW,gBAAgB,SAAS,QAAQ;QAClD,MAAM,gBAAgB,iBAAiB,QAAQ;QAC/C,OACE,oBAAC,UAAD;SAEE,eAAe,qBAAqB,QAAQ;SAC5C,OAAO;UACL,YAAY,WACR,2BAA2B,cAAc,OAAO,cAAc,YAC9D,GAAG,cAAc,cAAc,oBAAoB;UACvD,QAAQ,aAAa;UACrB,cAAc;UACd,SAAS;UACT,OAAO,cAAc,cAAc,YAAY;UAC/C,UAAU;UACV,QAAQ;UACR,SAAS,WAAW,IAAI;UACxB,YAAY;UACZ,YAAY,WAAW,SAAS;UAChC,WAAW,WACP,aAAa,cAAc,MAC3B;SACN;SACA,eAAe,MAAM;UACnB,EAAE,cAAc,MAAM,UAAU;UAChC,EAAE,cAAc,MAAM,YAAY;SACpC;SACA,eAAe,MAAM;UACnB,EAAE,cAAc,MAAM,UAAU,WAAW,MAAM;UACjD,EAAE,cAAc,MAAM,YAAY;SACpC;SACA,eAAa,mBAAmB;SAE/B,UAAA;QACK,GA9BD,QA8BC;OAEZ,CAAC;MACE,CAAA,CACF;;KAGL,qBAAC,OAAD;MAAK,OAAO,EAAE,cAAc,OAAO;MAAnC,UAAA,CACE,oBAAC,OAAD;OACE,OAAO;QACL;QACA,cAAc;QACd,OAAO,cAAc,cAAc,WAAW;QAC9C,YAAY;QACZ,eAAe;QACf,eAAe;OACjB;OACD,UAAA;MAEI,CAAA,GACL,oBAAC,OAAD;OACE,OAAO;QACL,SAAS;QACT,UAAU;QACV,KAAK;OACP;OAEC,UAAA,cAAc,KAAK,WAAW;QAC7B,MAAM,WAAW,iBAAiB,OAAO;QACzC,OACE,qBAAC,UAAD;SAEE,eAAe,qBAAqB,OAAO,KAAK;SAChD,OAAO;UACL,YAAY,WACR,2BAA2B,cACzB,cAAc,YAChB,EAAE,OAAO,cACP,cAAc,WAChB,EAAE,UACF,GAAG,cAAc,cAAc,oBAAoB;UACvD,QAAQ,aAAa,cACnB,cAAc,YAChB;UACA,cAAc;UACd,SAAS;UACT,OAAO,cAAc,cAAc,YAAY;UAC/C,UAAU;UACV,QAAQ;UACR,SAAS,WAAW,IAAI;UACxB,YAAY;UACZ,YAAY,WAAW,SAAS;UAChC,WAAW,WACP,aAAa,cACX,cAAc,YAChB,EAAE,MACF;SACN;SACA,eAAe,MAAM;UACnB,EAAE,cAAc,MAAM,UAAU;UAChC,EAAE,cAAc,MAAM,YAAY;SACpC;SACA,eAAe,MAAM;UACnB,EAAE,cAAc,MAAM,UAAU,WAAW,MAAM;UACjD,EAAE,cAAc,MAAM,YAAY;SACpC;SACA,eAAa,iBAAiB,OAAO;SApCvC,UAAA;UAsCG,OAAO;UAAO;UAAI,OAAO;SACpB;QAtCD,GAAA,OAAO,KAsCN;OAEZ,CAAC;MACE,CAAA,CACF;;KAGL,qBAAC,OAAD;MAAK,OAAO,EAAE,cAAc,OAAO;MAAnC,UAAA,CACE,oBAAC,OAAD;OACE,OAAO;QACL;QACA,cAAc;QACd,OAAO,cAAc,cAAc,WAAW;QAC9C,YAAY;QACZ,eAAe;QACf,eAAe;OACjB;OACD,UAAA;MAEI,CAAA,GACL,qBAAC,OAAD;OAAK,OAAO,EAAE,UAAU,WAAW;OAAnC,UAAA,CACE,oBAAC,SAAD;QACE,MAAK;QACL,OAAO;QACP,WAAW,MAAM,eAAe,EAAE,OAAO,KAAK;QAC9C,aAAY;QACZ,OAAO;SACL,OAAO;SACP,QAAQ;SACR,YAAY,GAAG,cACb,cAAc,oBAChB;SACA,QAAQ,aAAa,cACnB,cAAc,YAChB,EAAE;SACF,cAAc;SACd,SAAS;SACT,OAAO,cAAc,cAAc,YAAY;SAC/C;SACA,YAAY,YAAY;SACxB,YAAY;SACZ,SAAS;QACX;QACA,UAAU,MAAM;SACd,EAAE,cAAc,MAAM,cAAc,cAClC,cAAc,YAChB;SACA,EAAE,cAAc,MAAM,YAAY,YAAY,cAC5C,cAAc,YAChB,EAAE;QACJ;QACA,SAAS,MAAM;SACb,EAAE,cAAc,MAAM,cAAc,GAAG,cACrC,cAAc,YAChB,EAAE;SACF,EAAE,cAAc,MAAM,YAAY;QACpC;QACA,eAAY;OACb,CAAA,GAEA,eACC,oBAAC,UAAD;QACE,eAAe,eAAe,EAAE;QAChC,OAAO;SACL,UAAU;SACV,OAAO;SACP,KAAK;SACL,WAAW;SACX,YAAY;SACZ,QAAQ;SACR,OAAO,cAAc,cAAc,cAAc;SACjD,QAAQ;SACR,UAAU;SACV,SAAS;SACT,SAAS;SACT,YAAY;SACZ,gBAAgB;SAChB,cAAc;SACd,YAAY;QACd;QACA,eAAe,MAAM;SACnB,EAAE,cAAc,MAAM,QAAQ,cAC5B,cAAc,UAChB;SACA,EAAE,cAAc,MAAM,aAAa,GAAG,cACpC,cAAc,oBAChB;QACF;QACA,eAAe,MAAM;SACnB,EAAE,cAAc,MAAM,QAAQ,cAC5B,cAAc,cAChB;SACA,EAAE,cAAc,MAAM,aAAa;QACrC;QACA,eAAY;QACZ,OAAM;QACP,UAAA;OAEO,CAAA,CAEP;MACF,CAAA,CAAA;;KAGL,qBAAC,OAAD;MAAK,OAAO,EAAE,cAAc,OAAO;MAAnC,UAAA,CACE,oBAAC,OAAD;OACE,OAAO;QACL;QACA,cAAc;QACd,OAAO,cAAc,cAAc,WAAW;QAC9C,YAAY;QACZ,eAAe;QACf,eAAe;OACjB;OACD,UAAA;MAEI,CAAA,GACL,qBAAC,OAAD;OACE,OAAO;QAAE,SAAS;QAAQ,eAAe;QAAU,KAAK;OAAM;OADhE,UAAA,CAGE,qBAAC,SAAD;QACE,OAAO;SACL,SAAS;SACT,YAAY;SACZ,KAAK;SACL,QAAQ;QACV;QANF,UAAA,CAQE,oBAAC,SAAD;SACE,MAAK;SACL,SAAS;SACT,WAAW,MAAM,mBAAmB,EAAE,OAAO,OAAO;SACpD,OAAO;UACL,OAAO;UACP,QAAQ;UACR,aAAa,cAAc,cAAc,YAAY;SACvD;SACA,eAAY;QACb,CAAA,GACD,oBAAC,QAAD;SAAM,OAAO,EAAE,SAAS;SAAG,UAAA;QAAyB,CAAA,CAC/C;OACP,CAAA,GAAA,qBAAC,SAAD;QACE,OAAO;SACL,SAAS;SACT,YAAY;SACZ,KAAK;SACL,QAAQ;QACV;QANF,UAAA,CAQE,oBAAC,SAAD;SACE,MAAK;SACL,SAAS;SACT,WAAW,MAAM,iBAAiB,EAAE,OAAO,OAAO;SAClD,OAAO;UACL,OAAO;UACP,QAAQ;UACR,aAAa,cAAc,cAAc,YAAY;SACvD;SACA,eAAY;QACb,CAAA,GACD,oBAAC,QAAD;SAAM,OAAO,EAAE,SAAS;SAAG,UAAA;QAAwB,CAAA,CAC9C;OACJ,CAAA,CAAA;MACF,CAAA,CAAA;;MAGH,gBAAgB,SAAS,KACzB,iBAAiB,SACjB,gBAAgB,OAChB,oBAAC,OAAD;MAAK,OAAO,EAAE,cAAc,OAAO;MACjC,UAAA,oBAAC,UAAD;OACE,eAAe;QACb,wBAAwB,CAAC,CAAC;QAC1B,qBAAqB,KAAK;QAC1B,eAAe,EAAE;OACnB;OACA,OAAO;QACL,OAAO;QACP,QAAQ,eAAe;QACvB,YAAY,GAAG,cACb,cAAc,oBAChB;QACA,QAAQ,aAAa,cACnB,cAAc,aAChB;QACA,cAAc;QACd,OAAO,cAAc,cAAc,aAAa;QAChD,UAAU;QACV,QAAQ;QACR,YAAY;QACZ,YAAY;OACd;OACA,eAAe,MAAM;QACnB,EAAE,cAAc,MAAM,aAAa,GAAG,cACpC,cAAc,aAChB,EAAE;QACF,EAAE,cAAc,MAAM,YAAY;OACpC;OACA,eAAe,MAAM;QACnB,EAAE,cAAc,MAAM,aAAa,GAAG,cACpC,cAAc,oBAChB;QACA,EAAE,cAAc,MAAM,YAAY;OACpC;OACA,eAAY;OACb,UAAA;MAEO,CAAA;KACL,CAAA;KAIP,qBAAC,OAAD,EAAA,UAAA,CACE,qBAAC,OAAD;MACE,OAAO;OACL;OACA,cAAc;OACd,OAAO,cAAc,cAAc,WAAW;MAChD;MALF,UAAA;OAMC;OACc,MAAM,QAAQ,CAAC;OAAE;MAC3B;KACL,CAAA,GAAA,oBAAC,SAAD;MACE,MAAK;MACL,KAAI;MACJ,KAAI;MACJ,MAAK;MACL,OAAO;MACP,WAAW,MAAM,cAAc,WAAW,EAAE,OAAO,KAAK,CAAC;MACzD,OAAO;OACL,OAAO;OACP,aAAa,cAAc,cAAc,YAAY;MACvD;MACA,eAAY;KACb,CAAA,CACE,EAAA,CAAA;KAGL,qBAAC,OAAD;MACE,OAAO;OACL,WAAW;OACX,YAAY;OACZ,WAAW,aAAa,cACtB,cAAc,YAChB,EAAE;OACF,UAAU;OACV,OAAO,cAAc,cAAc,cAAc;MACnD;MATF,UAAA,CAWE,qBAAC,OAAD,EAAA,UAAA;OAAK;OACE,MAAM,SAAS;OAAK;OAAQ,MAAM,SAAS;MAC7C,EAAA,CAAA,GACL,qBAAC,OAAD,EAAA,UAAA;OAAK;OACC,MAAM,SAAS;OAAK;OAAQ,MAAM,SAAS;MAC5C,EAAA,CAAA,CACF;;IACL,EAAA,CAAA;GAED;;CACD,CAAA;AAEV"}
1
+ {"version":3,"file":"VitalPointOverlayControlsHtml.js","names":[],"sources":["../../../../../src/components/shared/three/ui/VitalPointOverlayControlsHtml.tsx"],"sourcesContent":["/**\n * VitalPointOverlayControlsHtml - UI controls for vital point visualization\n *\n * Provides comprehensive controls for the 70-point vital point overlay system:\n * - Toggle overlay visibility\n * - Filter by severity level\n * - Filter by body region\n * - Search vital points\n * - Adjust marker scale\n * - Toggle labels\n * - Toggle animations\n *\n * @module components/shared/three/ui/VitalPointOverlayControlsHtml\n */\n\nimport { Html } from \"@react-three/drei\";\nimport React, { useCallback, useMemo, useState } from \"react\";\nimport {\n KOREAN_VITAL_POINTS,\n getVitalPointsStats,\n} from \"../../../../systems/vitalpoint/KoreanVitalPoints\";\nimport { VitalPointSeverity } from \"../../../../types/common\";\nimport { FONT_FAMILY, KOREAN_COLORS } from \"../../../../types/constants\";\nimport { hexColorToCSS, hexToRgbaString } from \"../../../../utils/colorUtils\";\nimport type { BodyRegionFilter } from \"../effects/VitalPointMarkers3D\";\n\nexport type { BodyRegionFilter } from \"../effects/VitalPointMarkers3D\";\n\n/**\n * Props for VitalPointOverlayControlsHtml component\n */\nexport interface VitalPointOverlayControlsProps {\n /** Whether overlay is currently visible */\n readonly visible: boolean;\n /** Callback when visibility changes */\n readonly onVisibleChange: (visible: boolean) => void;\n /** Current severity filters */\n readonly severityFilters: VitalPointSeverity[];\n /** Callback when severity filters change */\n readonly onSeverityFiltersChange: (filters: VitalPointSeverity[]) => void;\n /** Current region filter */\n readonly regionFilter: BodyRegionFilter;\n /** Callback when region filter changes */\n readonly onRegionFilterChange: (filter: BodyRegionFilter) => void;\n /** Current search query */\n readonly searchQuery?: string;\n /** Callback when search query changes */\n readonly onSearchQueryChange?: (query: string) => void;\n /** Whether labels are shown */\n readonly showLabels: boolean;\n /** Callback when label visibility changes */\n readonly onShowLabelsChange: (show: boolean) => void;\n /** Whether animations are enabled */\n readonly animated: boolean;\n /** Callback when animation state changes */\n readonly onAnimatedChange: (animated: boolean) => void;\n /** Marker scale multiplier */\n readonly scale: number;\n /** Callback when scale changes */\n readonly onScaleChange: (scale: number) => void;\n /** Whether on mobile device */\n readonly isMobile?: boolean;\n /**\n * Screen position for the control panel.\n *\n * All values must be valid CSS position values, such as `\"20px\"`, `\"10%\"`, `\"1rem\"`, or `\"auto\"`.\n * These are applied directly to the `style` of the Html overlay container.\n */\n readonly screenPosition?: {\n /** CSS `top` position (e.g., \"20px\", \"10%\", \"1rem\", \"auto\") */\n top?: string;\n /** CSS `left` position (e.g., \"20px\", \"10%\", \"1rem\", \"auto\") */\n left?: string;\n /** CSS `right` position (e.g., \"20px\", \"10%\", \"1rem\", \"auto\") */\n right?: string;\n /** CSS `bottom` position (e.g., \"20px\", \"10%\", \"1rem\", \"auto\") */\n bottom?: string;\n };\n}\n\n/**\n * Convert numeric color to CSS hex string\n */\n/**\n * Get color for severity level\n */\nconst getSeverityColor = (severity: VitalPointSeverity): string => {\n switch (severity) {\n case VitalPointSeverity.LETHAL:\n return hexColorToCSS(KOREAN_COLORS.NEGATIVE_RED);\n case VitalPointSeverity.CRITICAL:\n return hexColorToCSS(KOREAN_COLORS.HEALTH_LOW);\n case VitalPointSeverity.MAJOR:\n return hexColorToCSS(KOREAN_COLORS.ACCENT_GOLD);\n case VitalPointSeverity.MODERATE:\n return hexColorToCSS(KOREAN_COLORS.TRIGRAM_GEON_PRIMARY);\n case VitalPointSeverity.MINOR:\n return hexColorToCSS(KOREAN_COLORS.POSITIVE_GREEN);\n default:\n return hexColorToCSS(KOREAN_COLORS.NEON_CYAN);\n }\n};\n\n/**\n * VitalPointOverlayControlsHtml Component\n * Provides comprehensive UI for vital point visualization control\n */\nexport const VitalPointOverlayControlsHtml: React.FC<\n VitalPointOverlayControlsProps\n> = ({\n visible,\n onVisibleChange,\n severityFilters,\n onSeverityFiltersChange,\n regionFilter,\n onRegionFilterChange,\n searchQuery: externalSearchQuery,\n onSearchQueryChange,\n showLabels,\n onShowLabelsChange,\n animated,\n onAnimatedChange,\n scale,\n onScaleChange,\n isMobile = false,\n screenPosition,\n}) => {\n const [expanded, setExpanded] = useState(false);\n\n const [internalSearchQuery, setInternalSearchQuery] = useState(\"\");\n const searchQuery = externalSearchQuery ?? internalSearchQuery;\n const setSearchQuery = onSearchQueryChange ?? setInternalSearchQuery;\n\n const stats = useMemo(() => getVitalPointsStats(), []);\n\n const defaultPosition: {\n top?: string;\n left?: string;\n right?: string;\n bottom?: string;\n } = useMemo(\n () => ({\n top: isMobile ? \"180px\" : \"220px\",\n left: isMobile ? \"10px\" : \"20px\",\n }),\n [isMobile]\n );\n\n const finalPosition = screenPosition ?? defaultPosition;\n\n const filteredCount = useMemo(() => {\n let points = [...KOREAN_VITAL_POINTS];\n\n if (severityFilters.length > 0) {\n points = points.filter((vp) => severityFilters.includes(vp.severity));\n }\n\n if (regionFilter !== \"all\") {\n if (regionFilter === \"arms\") {\n points = points.filter(\n (vp) =>\n vp.id.startsWith(\"arm_left_\") || vp.id.startsWith(\"arm_right_\")\n );\n } else if (regionFilter === \"legs\") {\n points = points.filter(\n (vp) =>\n vp.id.startsWith(\"leg_left_\") || vp.id.startsWith(\"leg_right_\")\n );\n } else {\n const prefix = `${regionFilter}_`;\n points = points.filter((vp) => vp.id.startsWith(prefix));\n }\n }\n\n if (searchQuery) {\n const query = searchQuery.toLowerCase();\n points = points.filter(\n (vp) =>\n vp.names.korean.toLowerCase().includes(query) ||\n vp.names.english.toLowerCase().includes(query) ||\n vp.names.romanized.toLowerCase().includes(query) ||\n vp.id.toLowerCase().includes(query)\n );\n }\n\n return points.length;\n }, [severityFilters, regionFilter, searchQuery]);\n\n const toggleSeverityFilter = useCallback(\n (severity: VitalPointSeverity) => {\n const newFilters = severityFilters.includes(severity)\n ? severityFilters.filter((s) => s !== severity)\n : [...severityFilters, severity];\n onSeverityFiltersChange(newFilters);\n },\n [severityFilters, onSeverityFiltersChange]\n );\n\n const severityOptions: VitalPointSeverity[] = [\n VitalPointSeverity.LETHAL,\n VitalPointSeverity.CRITICAL,\n VitalPointSeverity.MAJOR,\n VitalPointSeverity.MODERATE,\n VitalPointSeverity.MINOR,\n ];\n\n const regionOptions: {\n value: BodyRegionFilter;\n label: string;\n korean: string;\n }[] = [\n { value: \"all\", label: \"All Regions\", korean: \"전체\" },\n { value: \"head\", label: \"Head\", korean: \"머리\" },\n { value: \"torso\", label: \"Torso\", korean: \"몸통\" },\n { value: \"arms\", label: \"Arms\", korean: \"팔\" },\n { value: \"legs\", label: \"Legs\", korean: \"다리\" },\n ];\n\n const panelWidth = isMobile ? 280 : 350;\n const buttonHeight = isMobile ? 32 : 36;\n const fontSize = isMobile ? 11 : 13;\n const smallFontSize = isMobile ? 9 : 10;\n\n return (\n <Html fullscreen style={{ pointerEvents: \"none\" }}>\n <div\n style={{\n position: \"absolute\",\n top: finalPosition.top,\n left: finalPosition.left,\n right: finalPosition.right,\n bottom: finalPosition.bottom,\n width: panelWidth,\n background: `${hexColorToCSS(KOREAN_COLORS.UI_BACKGROUND_DARK)}f0`,\n border: `2px solid ${hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN)}`,\n borderRadius: \"12px\",\n padding: isMobile ? \"12px\" : \"16px\",\n fontFamily: FONT_FAMILY.KOREAN,\n color: hexColorToCSS(KOREAN_COLORS.TEXT_PRIMARY),\n boxShadow: `0 0 30px ${hexColorToCSS(\n KOREAN_COLORS.PRIMARY_CYAN\n )}40, inset 0 0 20px ${hexColorToCSS(\n KOREAN_COLORS.UI_BACKGROUND_DARK\n )}80`,\n transition: \"all 0.3s ease\",\n pointerEvents: \"all\",\n zIndex: 200,\n }}\n data-testid=\"vital-point-overlay-controls\"\n >\n {/* Header with toggle */}\n <div\n style={{\n display: \"flex\",\n justifyContent: \"space-between\",\n alignItems: \"center\",\n marginBottom: \"12px\",\n paddingBottom: \"12px\",\n borderBottom: `1px solid ${hexColorToCSS(\n KOREAN_COLORS.PRIMARY_CYAN\n )}40`,\n background: `linear-gradient(90deg, ${hexColorToCSS(\n KOREAN_COLORS.UI_BACKGROUND_DARK\n )}00 0%, ${hexColorToCSS(\n KOREAN_COLORS.PRIMARY_CYAN\n )}10 50%, ${hexColorToCSS(KOREAN_COLORS.UI_BACKGROUND_DARK)}00 100%)`,\n }}\n >\n <div>\n <div style={{ fontSize: isMobile ? 14 : 16, fontWeight: \"bold\" }}>\n 급소 오버레이 | Vital Points\n </div>\n <div\n style={{\n fontSize: smallFontSize,\n color: hexColorToCSS(KOREAN_COLORS.TEXT_SECONDARY),\n marginTop: \"2px\",\n }}\n >\n {filteredCount} / {stats.total} 표시 | Showing\n </div>\n </div>\n <button\n onClick={() => setExpanded(!expanded)}\n style={{\n background: `linear-gradient(135deg, ${hexColorToCSS(\n KOREAN_COLORS.UI_BACKGROUND_MEDIUM\n )} 0%, ${hexColorToCSS(KOREAN_COLORS.UI_BACKGROUND_DARK)} 100%)`,\n border: `2px solid ${hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN)}`,\n borderRadius: \"6px\",\n padding: \"8px 14px\",\n color: hexColorToCSS(KOREAN_COLORS.TEXT_PRIMARY),\n fontSize,\n cursor: \"pointer\",\n transition: \"all 0.2s ease\",\n boxShadow: `0 2px 8px ${hexColorToCSS(\n KOREAN_COLORS.PRIMARY_CYAN\n )}30`,\n }}\n onMouseEnter={(e) => {\n e.currentTarget.style.transform = \"scale(1.05)\";\n e.currentTarget.style.boxShadow = `0 4px 12px ${hexColorToCSS(\n KOREAN_COLORS.PRIMARY_CYAN\n )}50`;\n }}\n onMouseLeave={(e) => {\n e.currentTarget.style.transform = \"scale(1)\";\n e.currentTarget.style.boxShadow = `0 2px 8px ${hexColorToCSS(\n KOREAN_COLORS.PRIMARY_CYAN\n )}30`;\n }}\n data-testid=\"toggle-expand-button\"\n >\n {expanded ? \"▼\" : \"▶\"}\n </button>\n </div>\n\n {/* Main toggle */}\n <div style={{ marginBottom: \"14px\" }}>\n <button\n onClick={() => onVisibleChange(!visible)}\n style={{\n width: \"100%\",\n height: buttonHeight,\n background: visible\n ? `linear-gradient(135deg, ${hexColorToCSS(\n KOREAN_COLORS.ACCENT_GOLD\n )} 0%, ${hexColorToCSS(KOREAN_COLORS.SECONDARY_YELLOW)} 100%)`\n : `linear-gradient(135deg, ${hexColorToCSS(\n KOREAN_COLORS.UI_BACKGROUND_MEDIUM\n )} 0%, ${hexColorToCSS(KOREAN_COLORS.UI_BACKGROUND_DARK)} 100%)`,\n border: `2px solid ${\n visible\n ? hexColorToCSS(KOREAN_COLORS.ACCENT_GOLD)\n : hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN)\n }`,\n borderRadius: \"8px\",\n color: visible ? hexColorToCSS(KOREAN_COLORS.KOREAN_BLACK) : hexColorToCSS(KOREAN_COLORS.TEXT_PRIMARY),\n fontSize: isMobile ? 13 : 15,\n fontWeight: \"bold\",\n cursor: \"pointer\",\n transition: \"all 0.3s ease\",\n boxShadow: visible\n ? `0 4px 16px ${hexColorToCSS(\n KOREAN_COLORS.ACCENT_GOLD\n )}60, inset 0 2px 4px ${hexToRgbaString(KOREAN_COLORS.TEXT_PRIMARY, 0.2)}`\n : `0 2px 8px ${hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN)}30`,\n textTransform: \"uppercase\",\n letterSpacing: \"0.5px\",\n }}\n onMouseEnter={(e) => {\n e.currentTarget.style.transform = \"translateY(-2px)\";\n e.currentTarget.style.boxShadow = visible\n ? `0 6px 20px ${hexColorToCSS(KOREAN_COLORS.ACCENT_GOLD)}80`\n : `0 4px 12px ${hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN)}50`;\n }}\n onMouseLeave={(e) => {\n e.currentTarget.style.transform = \"translateY(0)\";\n e.currentTarget.style.boxShadow = visible\n ? `0 4px 16px ${hexColorToCSS(KOREAN_COLORS.ACCENT_GOLD)}60`\n : `0 2px 8px ${hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN)}30`;\n }}\n data-testid=\"toggle-visibility-button\"\n >\n {visible ? \"✓ 활성화 | Enabled\" : \"비활성화 | Disabled\"}\n </button>\n </div>\n\n {/* Expanded controls */}\n {expanded && visible && (\n <>\n {/* Severity filters */}\n <div style={{ marginBottom: \"14px\" }}>\n <div\n style={{\n fontSize,\n marginBottom: \"8px\",\n color: hexColorToCSS(KOREAN_COLORS.ACCENT_CYAN),\n fontWeight: \"600\",\n textTransform: \"uppercase\",\n letterSpacing: \"1px\",\n }}\n >\n 심각도 필터 | Severity Filter\n </div>\n <div\n style={{\n display: \"flex\",\n flexWrap: \"wrap\",\n gap: \"8px\",\n }}\n >\n {severityOptions.map((severity) => {\n const isActive = severityFilters.includes(severity);\n const severityColor = getSeverityColor(severity);\n return (\n <button\n key={severity}\n onClick={() => toggleSeverityFilter(severity)}\n style={{\n background: isActive\n ? `linear-gradient(135deg, ${severityColor} 0%, ${severityColor}cc 100%)`\n : `${hexColorToCSS(KOREAN_COLORS.UI_BACKGROUND_MEDIUM)}`,\n border: `2px solid ${severityColor}`,\n borderRadius: \"6px\",\n padding: \"6px 12px\",\n color: hexColorToCSS(KOREAN_COLORS.TEXT_PRIMARY),\n fontSize: smallFontSize,\n cursor: \"pointer\",\n opacity: isActive ? 1 : 0.6,\n transition: \"all 0.2s ease\",\n fontWeight: isActive ? \"bold\" : \"normal\",\n boxShadow: isActive\n ? `0 2px 8px ${severityColor}50`\n : \"none\",\n }}\n onMouseEnter={(e) => {\n e.currentTarget.style.opacity = \"1\";\n e.currentTarget.style.transform = \"scale(1.05)\";\n }}\n onMouseLeave={(e) => {\n e.currentTarget.style.opacity = isActive ? \"1\" : \"0.6\";\n e.currentTarget.style.transform = \"scale(1)\";\n }}\n data-testid={`severity-filter-${severity}`}\n >\n {severity}\n </button>\n );\n })}\n </div>\n </div>\n\n {/* Region filter */}\n <div style={{ marginBottom: \"14px\" }}>\n <div\n style={{\n fontSize,\n marginBottom: \"8px\",\n color: hexColorToCSS(KOREAN_COLORS.ACCENT_CYAN),\n fontWeight: \"600\",\n textTransform: \"uppercase\",\n letterSpacing: \"1px\",\n }}\n >\n 부위 필터 | Region Filter\n </div>\n <div\n style={{\n display: \"flex\",\n flexWrap: \"wrap\",\n gap: \"8px\",\n }}\n >\n {regionOptions.map((option) => {\n const isActive = regionFilter === option.value;\n return (\n <button\n key={option.value}\n onClick={() => onRegionFilterChange(option.value)}\n style={{\n background: isActive\n ? `linear-gradient(135deg, ${hexColorToCSS(\n KOREAN_COLORS.PRIMARY_CYAN\n )} 0%, ${hexColorToCSS(\n KOREAN_COLORS.ACCENT_BLUE\n )} 100%)`\n : `${hexColorToCSS(KOREAN_COLORS.UI_BACKGROUND_MEDIUM)}`,\n border: `2px solid ${hexColorToCSS(\n KOREAN_COLORS.PRIMARY_CYAN\n )}`,\n borderRadius: \"6px\",\n padding: \"6px 12px\",\n color: hexColorToCSS(KOREAN_COLORS.TEXT_PRIMARY),\n fontSize: smallFontSize,\n cursor: \"pointer\",\n opacity: isActive ? 1 : 0.6,\n transition: \"all 0.2s ease\",\n fontWeight: isActive ? \"bold\" : \"normal\",\n boxShadow: isActive\n ? `0 2px 8px ${hexColorToCSS(\n KOREAN_COLORS.PRIMARY_CYAN\n )}50`\n : \"none\",\n }}\n onMouseEnter={(e) => {\n e.currentTarget.style.opacity = \"1\";\n e.currentTarget.style.transform = \"scale(1.05)\";\n }}\n onMouseLeave={(e) => {\n e.currentTarget.style.opacity = isActive ? \"1\" : \"0.6\";\n e.currentTarget.style.transform = \"scale(1)\";\n }}\n data-testid={`region-filter-${option.value}`}\n >\n {option.korean} | {option.label}\n </button>\n );\n })}\n </div>\n </div>\n\n {/* Search box with clear button */}\n <div style={{ marginBottom: \"14px\" }}>\n <div\n style={{\n fontSize,\n marginBottom: \"8px\",\n color: hexColorToCSS(KOREAN_COLORS.ACCENT_CYAN),\n fontWeight: \"600\",\n textTransform: \"uppercase\",\n letterSpacing: \"1px\",\n }}\n >\n 검색 | Search\n </div>\n <div style={{ position: \"relative\" }}>\n <input\n type=\"text\"\n value={searchQuery}\n onChange={(e) => setSearchQuery(e.target.value)}\n placeholder=\"급소 이름... | Point name...\"\n style={{\n width: \"100%\",\n height: buttonHeight,\n background: `${hexColorToCSS(\n KOREAN_COLORS.UI_BACKGROUND_MEDIUM\n )}`,\n border: `2px solid ${hexColorToCSS(\n KOREAN_COLORS.PRIMARY_CYAN\n )}40`,\n borderRadius: \"8px\",\n padding: \"0 40px 0 14px\", // Add right padding for clear button\n color: hexColorToCSS(KOREAN_COLORS.TEXT_PRIMARY),\n fontSize,\n fontFamily: FONT_FAMILY.KOREAN,\n transition: \"all 0.2s ease\",\n outline: \"none\",\n }}\n onFocus={(e) => {\n e.currentTarget.style.borderColor = hexColorToCSS(\n KOREAN_COLORS.PRIMARY_CYAN\n );\n e.currentTarget.style.boxShadow = `0 0 12px ${hexColorToCSS(\n KOREAN_COLORS.PRIMARY_CYAN\n )}40`;\n }}\n onBlur={(e) => {\n e.currentTarget.style.borderColor = `${hexColorToCSS(\n KOREAN_COLORS.PRIMARY_CYAN\n )}40`;\n e.currentTarget.style.boxShadow = \"none\";\n }}\n data-testid=\"search-input\"\n />\n {/* Clear button */}\n {searchQuery && (\n <button\n onClick={() => setSearchQuery(\"\")}\n style={{\n position: \"absolute\",\n right: \"8px\",\n top: \"50%\",\n transform: \"translateY(-50%)\",\n background: \"transparent\",\n border: \"none\",\n color: hexColorToCSS(KOREAN_COLORS.TEXT_SECONDARY),\n cursor: \"pointer\",\n fontSize: \"16px\",\n padding: \"4px\",\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n borderRadius: \"4px\",\n transition: \"all 0.2s ease\",\n }}\n onMouseEnter={(e) => {\n e.currentTarget.style.color = hexColorToCSS(\n KOREAN_COLORS.ACCENT_RED\n );\n e.currentTarget.style.background = `${hexColorToCSS(\n KOREAN_COLORS.UI_BACKGROUND_MEDIUM\n )}`;\n }}\n onMouseLeave={(e) => {\n e.currentTarget.style.color = hexColorToCSS(\n KOREAN_COLORS.TEXT_SECONDARY\n );\n e.currentTarget.style.background = \"transparent\";\n }}\n data-testid=\"search-clear-button\"\n title=\"Clear search\"\n >\n ✕\n </button>\n )}\n </div>\n </div>\n\n {/* Display options */}\n <div style={{ marginBottom: \"14px\" }}>\n <div\n style={{\n fontSize,\n marginBottom: \"8px\",\n color: hexColorToCSS(KOREAN_COLORS.ACCENT_CYAN),\n fontWeight: \"600\",\n textTransform: \"uppercase\",\n letterSpacing: \"1px\",\n }}\n >\n 표시 옵션 | Display Options\n </div>\n <div\n style={{ display: \"flex\", flexDirection: \"column\", gap: \"8px\" }}\n >\n <label\n style={{\n display: \"flex\",\n alignItems: \"center\",\n gap: \"8px\",\n cursor: \"pointer\",\n }}\n >\n <input\n type=\"checkbox\"\n checked={showLabels}\n onChange={(e) => onShowLabelsChange(e.target.checked)}\n style={{\n width: \"16px\",\n height: \"16px\",\n accentColor: hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN),\n }}\n data-testid=\"show-labels-checkbox\"\n />\n <span style={{ fontSize }}>라벨 표시 | Show Labels</span>\n </label>\n <label\n style={{\n display: \"flex\",\n alignItems: \"center\",\n gap: \"8px\",\n cursor: \"pointer\",\n }}\n >\n <input\n type=\"checkbox\"\n checked={animated}\n onChange={(e) => onAnimatedChange(e.target.checked)}\n style={{\n width: \"16px\",\n height: \"16px\",\n accentColor: hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN),\n }}\n data-testid=\"animated-checkbox\"\n />\n <span style={{ fontSize }}>애니메이션 | Animations</span>\n </label>\n </div>\n </div>\n\n {/* Reset filters button */}\n {(severityFilters.length > 0 ||\n regionFilter !== \"all\" ||\n searchQuery !== \"\") && (\n <div style={{ marginBottom: \"14px\" }}>\n <button\n onClick={() => {\n onSeverityFiltersChange([]);\n onRegionFilterChange(\"all\");\n setSearchQuery(\"\");\n }}\n style={{\n width: \"100%\",\n height: buttonHeight - 4,\n background: `${hexColorToCSS(\n KOREAN_COLORS.UI_BACKGROUND_MEDIUM\n )}`,\n border: `2px solid ${hexColorToCSS(\n KOREAN_COLORS.ACCENT_ORANGE\n )}`,\n borderRadius: \"6px\",\n color: hexColorToCSS(KOREAN_COLORS.ACCENT_ORANGE),\n fontSize: smallFontSize,\n cursor: \"pointer\",\n transition: \"all 0.2s ease\",\n fontWeight: \"bold\",\n }}\n onMouseEnter={(e) => {\n e.currentTarget.style.background = `${hexColorToCSS(\n KOREAN_COLORS.ACCENT_ORANGE\n )}20`;\n e.currentTarget.style.transform = \"translateY(-1px)\";\n }}\n onMouseLeave={(e) => {\n e.currentTarget.style.background = `${hexColorToCSS(\n KOREAN_COLORS.UI_BACKGROUND_MEDIUM\n )}`;\n e.currentTarget.style.transform = \"translateY(0)\";\n }}\n data-testid=\"reset-filters-button\"\n >\n 🔄 필터 초기화 | Reset Filters\n </button>\n </div>\n )}\n\n {/* Scale slider */}\n <div>\n <div\n style={{\n fontSize,\n marginBottom: \"6px\",\n color: hexColorToCSS(KOREAN_COLORS.ACCENT_CYAN),\n }}\n >\n 크기 | Scale: {scale.toFixed(1)}x\n </div>\n <input\n type=\"range\"\n min=\"0.5\"\n max=\"2.0\"\n step=\"0.1\"\n value={scale}\n onChange={(e) => onScaleChange(parseFloat(e.target.value))}\n style={{\n width: \"100%\",\n accentColor: hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN),\n }}\n data-testid=\"scale-slider\"\n />\n </div>\n\n {/* Statistics */}\n <div\n style={{\n marginTop: \"12px\",\n paddingTop: \"12px\",\n borderTop: `1px solid ${hexColorToCSS(\n KOREAN_COLORS.PRIMARY_CYAN\n )}44`,\n fontSize: smallFontSize,\n color: hexColorToCSS(KOREAN_COLORS.TEXT_SECONDARY),\n }}\n >\n <div>\n 머리: {stats.byRegion.head} | 몸통: {stats.byRegion.torso}\n </div>\n <div>\n 팔: {stats.byRegion.arms} | 다리: {stats.byRegion.legs}\n </div>\n </div>\n </>\n )}\n </div>\n </Html>\n );\n};\n\nexport default VitalPointOverlayControlsHtml;\n"],"mappings":"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"}
@@ -183,7 +183,7 @@ var SplashScreen = ({ onStart, width, height }) => {
183
183
  }),
184
184
  /* @__PURE__ */ jsxs("div", {
185
185
  role: "contentinfo",
186
- "aria-label": `Application version 0.7.111`,
186
+ "aria-label": `Application version 0.7.113`,
187
187
  style: {
188
188
  position: "absolute",
189
189
  bottom: "20px",
@@ -192,7 +192,7 @@ var SplashScreen = ({ onStart, width, height }) => {
192
192
  fontSize: "10px",
193
193
  zIndex: 1
194
194
  },
195
- children: ["v", "0.7.111"]
195
+ children: ["v", "0.7.113"]
196
196
  })
197
197
  ]
198
198
  });
@@ -55,11 +55,13 @@ var VitalPointOverlayControlsPure = ({ visible, onVisibleChange, severityFilters
55
55
  const filteredCount = useMemo(() => {
56
56
  let points = [...KOREAN_VITAL_POINTS];
57
57
  if (severityFilters.length > 0) points = points.filter((vp) => severityFilters.includes(vp.severity));
58
- if (regionFilter !== "all") if (regionFilter === "arms") points = points.filter((vp) => vp.id.startsWith("arm_left_") || vp.id.startsWith("arm_right_"));
59
- else if (regionFilter === "legs") points = points.filter((vp) => vp.id.startsWith("leg_left_") || vp.id.startsWith("leg_right_"));
60
- else {
61
- const prefix = `${regionFilter}_`;
62
- points = points.filter((vp) => vp.id.startsWith(prefix));
58
+ if (regionFilter !== "all") {
59
+ if (regionFilter === "arms") points = points.filter((vp) => vp.id.startsWith("arm_left_") || vp.id.startsWith("arm_right_"));
60
+ else if (regionFilter === "legs") points = points.filter((vp) => vp.id.startsWith("leg_left_") || vp.id.startsWith("leg_right_"));
61
+ else {
62
+ const prefix = `${regionFilter}_`;
63
+ points = points.filter((vp) => vp.id.startsWith(prefix));
64
+ }
63
65
  }
64
66
  if (searchQuery) {
65
67
  const query = searchQuery.toLowerCase();
@@ -1 +1 @@
1
- {"version":3,"file":"VitalPointOverlayControlsPure.js","names":[],"sources":["../../../../src/components/shared/ui/VitalPointOverlayControlsPure.tsx"],"sourcesContent":["/**\n * VitalPointOverlayControlsPure - Pure DOM controls for vital point visualization\n *\n * Pure DOM version (no Three.js Html wrapper) for use in HUD overlays.\n * Identical functionality to VitalPointOverlayControlsHtml but renders as pure React DOM.\n *\n * Provides comprehensive controls for the 70-point vital point overlay system:\n * - Toggle overlay visibility\n * - Filter by severity level\n * - Filter by body region\n * - Search vital points\n * - Adjust marker scale\n * - Toggle labels\n * - Toggle animations\n *\n * @module components/shared/ui/VitalPointOverlayControlsPure\n */\n\nimport React, { useCallback, useMemo, useState } from \"react\";\nimport {\n KOREAN_VITAL_POINTS,\n getVitalPointsStats,\n} from \"../../../systems/vitalpoint/KoreanVitalPoints\";\nimport { VitalPointSeverity } from \"../../../types/common\";\nimport { Z_INDEX } from \"../../../types/LayoutTypes\";\nimport { FONT_FAMILY, KOREAN_COLORS } from \"@/types/constants\";\nimport { hexColorToCSS, hexToRgbaString } from \"@/utils/colorUtils\";\nimport type { BodyRegionFilter } from \"../three/effects/VitalPointMarkers3D\";\n\nexport type { BodyRegionFilter } from \"../three/effects/VitalPointMarkers3D\";\n\n/**\n * Props for VitalPointOverlayControlsPure component\n */\nexport interface VitalPointOverlayControlsPureProps {\n /** Whether overlay is currently visible */\n readonly visible: boolean;\n /** Callback when visibility changes */\n readonly onVisibleChange: (visible: boolean) => void;\n /** Current severity filters */\n readonly severityFilters: VitalPointSeverity[];\n /** Callback when severity filters change */\n readonly onSeverityFiltersChange: (filters: VitalPointSeverity[]) => void;\n /** Current region filter */\n readonly regionFilter: BodyRegionFilter;\n /** Callback when region filter changes */\n readonly onRegionFilterChange: (filter: BodyRegionFilter) => void;\n /** Current search query */\n readonly searchQuery?: string;\n /** Callback when search query changes */\n readonly onSearchQueryChange?: (query: string) => void;\n /** Whether labels are shown */\n readonly showLabels: boolean;\n /** Callback when label visibility changes */\n readonly onShowLabelsChange: (show: boolean) => void;\n /** Whether animations are enabled */\n readonly animated: boolean;\n /** Callback when animation state changes */\n readonly onAnimatedChange: (animated: boolean) => void;\n /** Marker scale multiplier */\n readonly scale: number;\n /** Callback when scale changes */\n readonly onScaleChange: (scale: number) => void;\n /** Whether on mobile device */\n readonly isMobile?: boolean;\n /**\n * Screen position for the control panel.\n *\n * All values must be valid CSS position values, such as `\"20px\"`, `\"10%\"`, `\"1rem\"`, or `\"auto\"`.\n * These are applied directly to the `style` of the container.\n */\n readonly screenPosition?: {\n /** CSS `top` position (e.g., \"20px\", \"10%\", \"1rem\", \"auto\") */\n top?: string;\n /** CSS `left` position (e.g., \"20px\", \"10%\", \"1rem\", \"auto\") */\n left?: string;\n /** CSS `right` position (e.g., \"20px\", \"10%\", \"1rem\", \"auto\") */\n right?: string;\n /** CSS `bottom` position (e.g., \"20px\", \"10%\", \"1rem\", \"auto\") */\n bottom?: string;\n };\n}\n/**\n * Get color for severity level\n */\nconst getSeverityColor = (severity: VitalPointSeverity): string => {\n switch (severity) {\n case VitalPointSeverity.LETHAL:\n return hexColorToCSS(KOREAN_COLORS.NEGATIVE_RED);\n case VitalPointSeverity.CRITICAL:\n return hexColorToCSS(KOREAN_COLORS.HEALTH_LOW);\n case VitalPointSeverity.MAJOR:\n return hexColorToCSS(KOREAN_COLORS.ACCENT_GOLD);\n case VitalPointSeverity.MODERATE:\n return hexColorToCSS(KOREAN_COLORS.TRIGRAM_GEON_PRIMARY);\n case VitalPointSeverity.MINOR:\n return hexColorToCSS(KOREAN_COLORS.POSITIVE_GREEN);\n default:\n return hexColorToCSS(KOREAN_COLORS.NEON_CYAN);\n }\n};\n\n/**\n * VitalPointOverlayControlsPure Component\n * Pure DOM version - renders directly without Three.js Html wrapper\n */\nexport const VitalPointOverlayControlsPure: React.FC<\n VitalPointOverlayControlsPureProps\n> = ({\n visible,\n onVisibleChange,\n severityFilters,\n onSeverityFiltersChange,\n regionFilter,\n onRegionFilterChange,\n searchQuery: externalSearchQuery,\n onSearchQueryChange,\n showLabels,\n onShowLabelsChange,\n animated,\n onAnimatedChange,\n scale,\n onScaleChange,\n isMobile = false,\n screenPosition,\n}) => {\n const [expanded, setExpanded] = useState(false);\n\n const [internalSearchQuery, setInternalSearchQuery] = useState(\"\");\n const searchQuery = externalSearchQuery ?? internalSearchQuery;\n const setSearchQuery = onSearchQueryChange ?? setInternalSearchQuery;\n\n const stats = useMemo(() => getVitalPointsStats(), []);\n\n const defaultPosition: {\n top?: string;\n left?: string;\n right?: string;\n bottom?: string;\n } = useMemo(\n () => ({\n top: isMobile ? \"180px\" : \"220px\",\n left: isMobile ? \"10px\" : \"20px\",\n }),\n [isMobile],\n );\n\n const finalPosition = screenPosition ?? defaultPosition;\n\n const filteredCount = useMemo(() => {\n let points = [...KOREAN_VITAL_POINTS];\n\n if (severityFilters.length > 0) {\n points = points.filter((vp) => severityFilters.includes(vp.severity));\n }\n\n if (regionFilter !== \"all\") {\n if (regionFilter === \"arms\") {\n points = points.filter(\n (vp) =>\n vp.id.startsWith(\"arm_left_\") || vp.id.startsWith(\"arm_right_\"),\n );\n } else if (regionFilter === \"legs\") {\n points = points.filter(\n (vp) =>\n vp.id.startsWith(\"leg_left_\") || vp.id.startsWith(\"leg_right_\"),\n );\n } else {\n const prefix = `${regionFilter}_`;\n points = points.filter((vp) => vp.id.startsWith(prefix));\n }\n }\n\n if (searchQuery) {\n const query = searchQuery.toLowerCase();\n points = points.filter(\n (vp) =>\n vp.names.korean.toLowerCase().includes(query) ||\n vp.names.english.toLowerCase().includes(query) ||\n vp.names.romanized.toLowerCase().includes(query) ||\n vp.id.toLowerCase().includes(query),\n );\n }\n\n return points.length;\n }, [severityFilters, regionFilter, searchQuery]);\n\n const toggleSeverityFilter = useCallback(\n (severity: VitalPointSeverity) => {\n const newFilters = severityFilters.includes(severity)\n ? severityFilters.filter((s) => s !== severity)\n : [...severityFilters, severity];\n onSeverityFiltersChange(newFilters);\n },\n [severityFilters, onSeverityFiltersChange],\n );\n\n const severityOptions: VitalPointSeverity[] = [\n VitalPointSeverity.LETHAL,\n VitalPointSeverity.CRITICAL,\n VitalPointSeverity.MAJOR,\n VitalPointSeverity.MODERATE,\n VitalPointSeverity.MINOR,\n ];\n\n const regionOptions: {\n value: BodyRegionFilter;\n label: string;\n korean: string;\n }[] = [\n { value: \"all\", label: \"All Regions\", korean: \"전체\" },\n { value: \"head\", label: \"Head\", korean: \"머리\" },\n { value: \"torso\", label: \"Torso\", korean: \"몸통\" },\n { value: \"arms\", label: \"Arms\", korean: \"팔\" },\n { value: \"legs\", label: \"Legs\", korean: \"다리\" },\n ];\n\n const panelWidth = isMobile ? 280 : 350;\n const buttonHeight = isMobile ? 32 : 36;\n const fontSize = isMobile ? 11 : 13;\n const smallFontSize = isMobile ? 9 : 10;\n\n return (\n <div\n style={{\n position: \"absolute\",\n top: finalPosition.top,\n left: finalPosition.left,\n right: finalPosition.right,\n bottom: finalPosition.bottom,\n width: panelWidth,\n background: `${hexColorToCSS(KOREAN_COLORS.UI_BACKGROUND_DARK)}f0`,\n border: `2px solid ${hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN)}`,\n borderRadius: \"12px\",\n padding: isMobile ? \"12px\" : \"16px\",\n fontFamily: FONT_FAMILY.KOREAN,\n color: hexColorToCSS(KOREAN_COLORS.TEXT_PRIMARY),\n boxShadow: `0 0 30px ${hexColorToCSS(\n KOREAN_COLORS.PRIMARY_CYAN,\n )}40, inset 0 0 20px ${hexColorToCSS(KOREAN_COLORS.UI_BACKGROUND_DARK)}80`,\n transition: \"all 0.3s ease\",\n pointerEvents: \"all\",\n zIndex: Z_INDEX.MODAL,\n }}\n data-testid=\"vital-point-overlay-controls\"\n >\n {/* Header with toggle */}\n <div\n style={{\n display: \"flex\",\n justifyContent: \"space-between\",\n alignItems: \"center\",\n marginBottom: \"12px\",\n paddingBottom: \"12px\",\n borderBottom: `1px solid ${hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN)}40`,\n background: `linear-gradient(90deg, ${hexColorToCSS(\n KOREAN_COLORS.UI_BACKGROUND_DARK,\n )}00 0%, ${hexColorToCSS(\n KOREAN_COLORS.PRIMARY_CYAN,\n )}10 50%, ${hexColorToCSS(KOREAN_COLORS.UI_BACKGROUND_DARK)}00 100%)`,\n }}\n >\n <div>\n <div style={{ fontSize: isMobile ? 14 : 16, fontWeight: \"bold\" }}>\n 급소 오버레이 | Vital Points\n </div>\n <div\n style={{\n fontSize: smallFontSize,\n color: hexColorToCSS(KOREAN_COLORS.TEXT_SECONDARY),\n marginTop: \"2px\",\n }}\n >\n {filteredCount} / {stats.total} 표시 | Showing\n </div>\n </div>\n <button\n onClick={() => setExpanded(!expanded)}\n style={{\n background: `linear-gradient(135deg, ${hexColorToCSS(\n KOREAN_COLORS.UI_BACKGROUND_MEDIUM,\n )} 0%, ${hexColorToCSS(KOREAN_COLORS.UI_BACKGROUND_DARK)} 100%)`,\n border: `2px solid ${hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN)}`,\n borderRadius: \"6px\",\n padding: \"8px 14px\",\n color: hexColorToCSS(KOREAN_COLORS.TEXT_PRIMARY),\n fontSize,\n cursor: \"pointer\",\n transition: \"all 0.2s ease\",\n boxShadow: `0 2px 8px ${hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN)}30`,\n }}\n onMouseEnter={(e) => {\n e.currentTarget.style.transform = \"scale(1.05)\";\n e.currentTarget.style.boxShadow = `0 4px 12px ${hexColorToCSS(\n KOREAN_COLORS.PRIMARY_CYAN,\n )}50`;\n }}\n onMouseLeave={(e) => {\n e.currentTarget.style.transform = \"scale(1)\";\n e.currentTarget.style.boxShadow = `0 2px 8px ${hexColorToCSS(\n KOREAN_COLORS.PRIMARY_CYAN,\n )}30`;\n }}\n data-testid=\"toggle-expand-button\"\n >\n {expanded ? \"▼\" : \"▶\"}\n </button>\n </div>\n\n {/* Main toggle */}\n <div style={{ marginBottom: \"14px\" }}>\n <button\n onClick={() => onVisibleChange(!visible)}\n style={{\n width: \"100%\",\n height: buttonHeight,\n background: visible\n ? `linear-gradient(135deg, ${hexColorToCSS(\n KOREAN_COLORS.ACCENT_GOLD,\n )} 0%, ${hexColorToCSS(KOREAN_COLORS.SECONDARY_YELLOW)} 100%)`\n : `linear-gradient(135deg, ${hexColorToCSS(\n KOREAN_COLORS.UI_BACKGROUND_MEDIUM,\n )} 0%, ${hexColorToCSS(KOREAN_COLORS.UI_BACKGROUND_DARK)} 100%)`,\n border: `2px solid ${\n visible\n ? hexColorToCSS(KOREAN_COLORS.ACCENT_GOLD)\n : hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN)\n }`,\n borderRadius: \"8px\",\n color: visible ? hexColorToCSS(KOREAN_COLORS.KOREAN_BLACK) : hexColorToCSS(KOREAN_COLORS.TEXT_PRIMARY),\n fontSize: isMobile ? 13 : 15,\n fontWeight: \"bold\",\n cursor: \"pointer\",\n transition: \"all 0.3s ease\",\n boxShadow: visible\n ? `0 4px 16px ${hexColorToCSS(\n KOREAN_COLORS.ACCENT_GOLD,\n )}60, inset 0 2px 4px ${hexToRgbaString(KOREAN_COLORS.TEXT_PRIMARY, 0.2)}`\n : `0 2px 8px ${hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN)}30`,\n textTransform: \"uppercase\",\n letterSpacing: \"0.5px\",\n }}\n onMouseEnter={(e) => {\n e.currentTarget.style.transform = \"translateY(-2px)\";\n e.currentTarget.style.boxShadow = visible\n ? `0 6px 20px ${hexColorToCSS(KOREAN_COLORS.ACCENT_GOLD)}80`\n : `0 4px 12px ${hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN)}50`;\n }}\n onMouseLeave={(e) => {\n e.currentTarget.style.transform = \"translateY(0)\";\n e.currentTarget.style.boxShadow = visible\n ? `0 4px 16px ${hexColorToCSS(KOREAN_COLORS.ACCENT_GOLD)}60`\n : `0 2px 8px ${hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN)}30`;\n }}\n data-testid=\"toggle-visibility-button\"\n >\n {visible ? \"✓ 활성화 | Enabled\" : \"비활성화 | Disabled\"}\n </button>\n </div>\n\n {/* Expanded controls */}\n {expanded && visible && (\n <>\n {/* Severity filters */}\n <div style={{ marginBottom: \"14px\" }}>\n <div\n style={{\n fontSize,\n marginBottom: \"8px\",\n color: hexColorToCSS(KOREAN_COLORS.ACCENT_CYAN),\n fontWeight: \"600\",\n textTransform: \"uppercase\",\n letterSpacing: \"1px\",\n }}\n >\n 심각도 필터 | Severity Filter\n </div>\n <div\n style={{\n display: \"flex\",\n flexWrap: \"wrap\",\n gap: \"8px\",\n }}\n >\n {severityOptions.map((severity) => {\n const isActive = severityFilters.includes(severity);\n const severityColor = getSeverityColor(severity);\n return (\n <button\n key={severity}\n onClick={() => toggleSeverityFilter(severity)}\n style={{\n background: isActive\n ? `linear-gradient(135deg, ${severityColor} 0%, ${severityColor}cc 100%)`\n : `${hexColorToCSS(KOREAN_COLORS.UI_BACKGROUND_MEDIUM)}`,\n border: `2px solid ${severityColor}`,\n borderRadius: \"6px\",\n padding: \"6px 12px\",\n color: hexColorToCSS(KOREAN_COLORS.TEXT_PRIMARY),\n fontSize: smallFontSize,\n cursor: \"pointer\",\n opacity: isActive ? 1 : 0.6,\n transition: \"all 0.2s ease\",\n fontWeight: isActive ? \"bold\" : \"normal\",\n boxShadow: isActive\n ? `0 2px 8px ${severityColor}50`\n : \"none\",\n }}\n onMouseEnter={(e) => {\n e.currentTarget.style.opacity = \"1\";\n e.currentTarget.style.transform = \"scale(1.05)\";\n }}\n onMouseLeave={(e) => {\n e.currentTarget.style.opacity = isActive ? \"1\" : \"0.6\";\n e.currentTarget.style.transform = \"scale(1)\";\n }}\n data-testid={`severity-filter-${severity}`}\n >\n {severity}\n </button>\n );\n })}\n </div>\n </div>\n\n {/* Region filter */}\n <div style={{ marginBottom: \"14px\" }}>\n <div\n style={{\n fontSize,\n marginBottom: \"8px\",\n color: hexColorToCSS(KOREAN_COLORS.ACCENT_CYAN),\n fontWeight: \"600\",\n textTransform: \"uppercase\",\n letterSpacing: \"1px\",\n }}\n >\n 부위 필터 | Region Filter\n </div>\n <div\n style={{\n display: \"flex\",\n flexWrap: \"wrap\",\n gap: \"8px\",\n }}\n >\n {regionOptions.map((option) => {\n const isActive = regionFilter === option.value;\n return (\n <button\n key={option.value}\n onClick={() => onRegionFilterChange(option.value)}\n style={{\n background: isActive\n ? `linear-gradient(135deg, ${hexColorToCSS(\n KOREAN_COLORS.PRIMARY_CYAN,\n )} 0%, ${hexColorToCSS(KOREAN_COLORS.ACCENT_BLUE)} 100%)`\n : `${hexColorToCSS(KOREAN_COLORS.UI_BACKGROUND_MEDIUM)}`,\n border: `2px solid ${hexColorToCSS(\n KOREAN_COLORS.PRIMARY_CYAN,\n )}`,\n borderRadius: \"6px\",\n padding: \"6px 12px\",\n color: hexColorToCSS(KOREAN_COLORS.TEXT_PRIMARY),\n fontSize: smallFontSize,\n cursor: \"pointer\",\n opacity: isActive ? 1 : 0.6,\n transition: \"all 0.2s ease\",\n fontWeight: isActive ? \"bold\" : \"normal\",\n boxShadow: isActive\n ? `0 2px 8px ${hexColorToCSS(\n KOREAN_COLORS.PRIMARY_CYAN,\n )}50`\n : \"none\",\n }}\n onMouseEnter={(e) => {\n e.currentTarget.style.opacity = \"1\";\n e.currentTarget.style.transform = \"scale(1.05)\";\n }}\n onMouseLeave={(e) => {\n e.currentTarget.style.opacity = isActive ? \"1\" : \"0.6\";\n e.currentTarget.style.transform = \"scale(1)\";\n }}\n data-testid={`region-filter-${option.value}`}\n >\n {option.korean} | {option.label}\n </button>\n );\n })}\n </div>\n </div>\n\n {/* Search box with clear button */}\n <div style={{ marginBottom: \"14px\" }}>\n <div\n style={{\n fontSize,\n marginBottom: \"8px\",\n color: hexColorToCSS(KOREAN_COLORS.ACCENT_CYAN),\n fontWeight: \"600\",\n textTransform: \"uppercase\",\n letterSpacing: \"1px\",\n }}\n >\n 검색 | Search\n </div>\n <div style={{ position: \"relative\" }}>\n <input\n type=\"text\"\n value={searchQuery}\n onChange={(e) => setSearchQuery(e.target.value)}\n placeholder=\"급소 이름... | Point name...\"\n style={{\n width: \"100%\",\n height: buttonHeight,\n background: `${hexColorToCSS(\n KOREAN_COLORS.UI_BACKGROUND_MEDIUM,\n )}`,\n border: `2px solid ${hexColorToCSS(\n KOREAN_COLORS.PRIMARY_CYAN,\n )}40`,\n borderRadius: \"8px\",\n padding: \"0 40px 0 14px\", // Add right padding for clear button\n color: hexColorToCSS(KOREAN_COLORS.TEXT_PRIMARY),\n fontSize,\n fontFamily: FONT_FAMILY.KOREAN,\n transition: \"all 0.2s ease\",\n outline: \"none\",\n }}\n onFocus={(e) => {\n e.currentTarget.style.borderColor = hexColorToCSS(\n KOREAN_COLORS.PRIMARY_CYAN,\n );\n e.currentTarget.style.boxShadow = `0 0 12px ${hexColorToCSS(\n KOREAN_COLORS.PRIMARY_CYAN,\n )}40`;\n }}\n onBlur={(e) => {\n e.currentTarget.style.borderColor = `${hexColorToCSS(\n KOREAN_COLORS.PRIMARY_CYAN,\n )}40`;\n e.currentTarget.style.boxShadow = \"none\";\n }}\n data-testid=\"search-input\"\n />\n {/* Clear button */}\n {searchQuery && (\n <button\n onClick={() => setSearchQuery(\"\")}\n style={{\n position: \"absolute\",\n right: \"8px\",\n top: \"50%\",\n transform: \"translateY(-50%)\",\n background: \"transparent\",\n border: \"none\",\n color: hexColorToCSS(KOREAN_COLORS.TEXT_SECONDARY),\n cursor: \"pointer\",\n fontSize: \"16px\",\n padding: \"4px\",\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n borderRadius: \"4px\",\n transition: \"all 0.2s ease\",\n }}\n onMouseEnter={(e) => {\n e.currentTarget.style.color = hexColorToCSS(\n KOREAN_COLORS.ACCENT_RED,\n );\n e.currentTarget.style.background = `${hexColorToCSS(\n KOREAN_COLORS.UI_BACKGROUND_MEDIUM,\n )}`;\n }}\n onMouseLeave={(e) => {\n e.currentTarget.style.color = hexColorToCSS(\n KOREAN_COLORS.TEXT_SECONDARY,\n );\n e.currentTarget.style.background = \"transparent\";\n }}\n data-testid=\"search-clear-button\"\n title=\"Clear search\"\n >\n ✕\n </button>\n )}\n </div>\n </div>\n\n {/* Display options */}\n <div style={{ marginBottom: \"14px\" }}>\n <div\n style={{\n fontSize,\n marginBottom: \"8px\",\n color: hexColorToCSS(KOREAN_COLORS.ACCENT_CYAN),\n fontWeight: \"600\",\n textTransform: \"uppercase\",\n letterSpacing: \"1px\",\n }}\n >\n 표시 옵션 | Display Options\n </div>\n <div\n style={{ display: \"flex\", flexDirection: \"column\", gap: \"8px\" }}\n >\n <label\n style={{\n display: \"flex\",\n alignItems: \"center\",\n gap: \"8px\",\n cursor: \"pointer\",\n }}\n >\n <input\n type=\"checkbox\"\n checked={showLabels}\n onChange={(e) => onShowLabelsChange(e.target.checked)}\n style={{\n width: \"16px\",\n height: \"16px\",\n accentColor: hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN),\n }}\n data-testid=\"show-labels-checkbox\"\n />\n <span style={{ fontSize }}>라벨 표시 | Show Labels</span>\n </label>\n <label\n style={{\n display: \"flex\",\n alignItems: \"center\",\n gap: \"8px\",\n cursor: \"pointer\",\n }}\n >\n <input\n type=\"checkbox\"\n checked={animated}\n onChange={(e) => onAnimatedChange(e.target.checked)}\n style={{\n width: \"16px\",\n height: \"16px\",\n accentColor: hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN),\n }}\n data-testid=\"animated-checkbox\"\n />\n <span style={{ fontSize }}>애니메이션 | Animations</span>\n </label>\n </div>\n </div>\n\n {/* Reset filters button */}\n {(severityFilters.length > 0 ||\n regionFilter !== \"all\" ||\n searchQuery !== \"\") && (\n <div style={{ marginBottom: \"14px\" }}>\n <button\n onClick={() => {\n onSeverityFiltersChange([]);\n onRegionFilterChange(\"all\");\n setSearchQuery(\"\");\n }}\n style={{\n width: \"100%\",\n height: buttonHeight - 4,\n background: `${hexColorToCSS(\n KOREAN_COLORS.UI_BACKGROUND_MEDIUM,\n )}`,\n border: `2px solid ${hexColorToCSS(\n KOREAN_COLORS.ACCENT_ORANGE,\n )}`,\n borderRadius: \"6px\",\n color: hexColorToCSS(KOREAN_COLORS.ACCENT_ORANGE),\n fontSize: smallFontSize,\n cursor: \"pointer\",\n transition: \"all 0.2s ease\",\n fontWeight: \"bold\",\n }}\n onMouseEnter={(e) => {\n e.currentTarget.style.background = `${hexColorToCSS(\n KOREAN_COLORS.ACCENT_ORANGE,\n )}20`;\n e.currentTarget.style.transform = \"translateY(-1px)\";\n }}\n onMouseLeave={(e) => {\n e.currentTarget.style.background = `${hexColorToCSS(\n KOREAN_COLORS.UI_BACKGROUND_MEDIUM,\n )}`;\n e.currentTarget.style.transform = \"translateY(0)\";\n }}\n data-testid=\"reset-filters-button\"\n >\n 🔄 필터 초기화 | Reset Filters\n </button>\n </div>\n )}\n\n {/* Scale slider */}\n <div>\n <div\n style={{\n fontSize,\n marginBottom: \"6px\",\n color: hexColorToCSS(KOREAN_COLORS.ACCENT_CYAN),\n }}\n >\n 크기 | Scale: {scale.toFixed(1)}x\n </div>\n <input\n type=\"range\"\n min=\"0.5\"\n max=\"2.0\"\n step=\"0.1\"\n value={scale}\n onChange={(e) => onScaleChange(parseFloat(e.target.value))}\n style={{\n width: \"100%\",\n accentColor: hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN),\n }}\n data-testid=\"scale-slider\"\n />\n </div>\n\n {/* Statistics */}\n <div\n style={{\n marginTop: \"12px\",\n paddingTop: \"12px\",\n borderTop: `1px solid ${hexColorToCSS(\n KOREAN_COLORS.PRIMARY_CYAN,\n )}40`,\n fontSize: smallFontSize,\n color: hexColorToCSS(KOREAN_COLORS.TEXT_SECONDARY),\n }}\n >\n <div>\n 머리: {stats.byRegion.head} | 몸통: {stats.byRegion.torso}\n </div>\n <div>\n 팔: {stats.byRegion.arms} | 다리: {stats.byRegion.legs}\n </div>\n </div>\n </>\n )}\n </div>\n );\n};\n\nexport default VitalPointOverlayControlsPure;\n"],"mappings":"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"}
1
+ {"version":3,"file":"VitalPointOverlayControlsPure.js","names":[],"sources":["../../../../src/components/shared/ui/VitalPointOverlayControlsPure.tsx"],"sourcesContent":["/**\n * VitalPointOverlayControlsPure - Pure DOM controls for vital point visualization\n *\n * Pure DOM version (no Three.js Html wrapper) for use in HUD overlays.\n * Identical functionality to VitalPointOverlayControlsHtml but renders as pure React DOM.\n *\n * Provides comprehensive controls for the 70-point vital point overlay system:\n * - Toggle overlay visibility\n * - Filter by severity level\n * - Filter by body region\n * - Search vital points\n * - Adjust marker scale\n * - Toggle labels\n * - Toggle animations\n *\n * @module components/shared/ui/VitalPointOverlayControlsPure\n */\n\nimport React, { useCallback, useMemo, useState } from \"react\";\nimport {\n KOREAN_VITAL_POINTS,\n getVitalPointsStats,\n} from \"../../../systems/vitalpoint/KoreanVitalPoints\";\nimport { VitalPointSeverity } from \"../../../types/common\";\nimport { Z_INDEX } from \"../../../types/LayoutTypes\";\nimport { FONT_FAMILY, KOREAN_COLORS } from \"@/types/constants\";\nimport { hexColorToCSS, hexToRgbaString } from \"@/utils/colorUtils\";\nimport type { BodyRegionFilter } from \"../three/effects/VitalPointMarkers3D\";\n\nexport type { BodyRegionFilter } from \"../three/effects/VitalPointMarkers3D\";\n\n/**\n * Props for VitalPointOverlayControlsPure component\n */\nexport interface VitalPointOverlayControlsPureProps {\n /** Whether overlay is currently visible */\n readonly visible: boolean;\n /** Callback when visibility changes */\n readonly onVisibleChange: (visible: boolean) => void;\n /** Current severity filters */\n readonly severityFilters: VitalPointSeverity[];\n /** Callback when severity filters change */\n readonly onSeverityFiltersChange: (filters: VitalPointSeverity[]) => void;\n /** Current region filter */\n readonly regionFilter: BodyRegionFilter;\n /** Callback when region filter changes */\n readonly onRegionFilterChange: (filter: BodyRegionFilter) => void;\n /** Current search query */\n readonly searchQuery?: string;\n /** Callback when search query changes */\n readonly onSearchQueryChange?: (query: string) => void;\n /** Whether labels are shown */\n readonly showLabels: boolean;\n /** Callback when label visibility changes */\n readonly onShowLabelsChange: (show: boolean) => void;\n /** Whether animations are enabled */\n readonly animated: boolean;\n /** Callback when animation state changes */\n readonly onAnimatedChange: (animated: boolean) => void;\n /** Marker scale multiplier */\n readonly scale: number;\n /** Callback when scale changes */\n readonly onScaleChange: (scale: number) => void;\n /** Whether on mobile device */\n readonly isMobile?: boolean;\n /**\n * Screen position for the control panel.\n *\n * All values must be valid CSS position values, such as `\"20px\"`, `\"10%\"`, `\"1rem\"`, or `\"auto\"`.\n * These are applied directly to the `style` of the container.\n */\n readonly screenPosition?: {\n /** CSS `top` position (e.g., \"20px\", \"10%\", \"1rem\", \"auto\") */\n top?: string;\n /** CSS `left` position (e.g., \"20px\", \"10%\", \"1rem\", \"auto\") */\n left?: string;\n /** CSS `right` position (e.g., \"20px\", \"10%\", \"1rem\", \"auto\") */\n right?: string;\n /** CSS `bottom` position (e.g., \"20px\", \"10%\", \"1rem\", \"auto\") */\n bottom?: string;\n };\n}\n/**\n * Get color for severity level\n */\nconst getSeverityColor = (severity: VitalPointSeverity): string => {\n switch (severity) {\n case VitalPointSeverity.LETHAL:\n return hexColorToCSS(KOREAN_COLORS.NEGATIVE_RED);\n case VitalPointSeverity.CRITICAL:\n return hexColorToCSS(KOREAN_COLORS.HEALTH_LOW);\n case VitalPointSeverity.MAJOR:\n return hexColorToCSS(KOREAN_COLORS.ACCENT_GOLD);\n case VitalPointSeverity.MODERATE:\n return hexColorToCSS(KOREAN_COLORS.TRIGRAM_GEON_PRIMARY);\n case VitalPointSeverity.MINOR:\n return hexColorToCSS(KOREAN_COLORS.POSITIVE_GREEN);\n default:\n return hexColorToCSS(KOREAN_COLORS.NEON_CYAN);\n }\n};\n\n/**\n * VitalPointOverlayControlsPure Component\n * Pure DOM version - renders directly without Three.js Html wrapper\n */\nexport const VitalPointOverlayControlsPure: React.FC<\n VitalPointOverlayControlsPureProps\n> = ({\n visible,\n onVisibleChange,\n severityFilters,\n onSeverityFiltersChange,\n regionFilter,\n onRegionFilterChange,\n searchQuery: externalSearchQuery,\n onSearchQueryChange,\n showLabels,\n onShowLabelsChange,\n animated,\n onAnimatedChange,\n scale,\n onScaleChange,\n isMobile = false,\n screenPosition,\n}) => {\n const [expanded, setExpanded] = useState(false);\n\n const [internalSearchQuery, setInternalSearchQuery] = useState(\"\");\n const searchQuery = externalSearchQuery ?? internalSearchQuery;\n const setSearchQuery = onSearchQueryChange ?? setInternalSearchQuery;\n\n const stats = useMemo(() => getVitalPointsStats(), []);\n\n const defaultPosition: {\n top?: string;\n left?: string;\n right?: string;\n bottom?: string;\n } = useMemo(\n () => ({\n top: isMobile ? \"180px\" : \"220px\",\n left: isMobile ? \"10px\" : \"20px\",\n }),\n [isMobile],\n );\n\n const finalPosition = screenPosition ?? defaultPosition;\n\n const filteredCount = useMemo(() => {\n let points = [...KOREAN_VITAL_POINTS];\n\n if (severityFilters.length > 0) {\n points = points.filter((vp) => severityFilters.includes(vp.severity));\n }\n\n if (regionFilter !== \"all\") {\n if (regionFilter === \"arms\") {\n points = points.filter(\n (vp) =>\n vp.id.startsWith(\"arm_left_\") || vp.id.startsWith(\"arm_right_\"),\n );\n } else if (regionFilter === \"legs\") {\n points = points.filter(\n (vp) =>\n vp.id.startsWith(\"leg_left_\") || vp.id.startsWith(\"leg_right_\"),\n );\n } else {\n const prefix = `${regionFilter}_`;\n points = points.filter((vp) => vp.id.startsWith(prefix));\n }\n }\n\n if (searchQuery) {\n const query = searchQuery.toLowerCase();\n points = points.filter(\n (vp) =>\n vp.names.korean.toLowerCase().includes(query) ||\n vp.names.english.toLowerCase().includes(query) ||\n vp.names.romanized.toLowerCase().includes(query) ||\n vp.id.toLowerCase().includes(query),\n );\n }\n\n return points.length;\n }, [severityFilters, regionFilter, searchQuery]);\n\n const toggleSeverityFilter = useCallback(\n (severity: VitalPointSeverity) => {\n const newFilters = severityFilters.includes(severity)\n ? severityFilters.filter((s) => s !== severity)\n : [...severityFilters, severity];\n onSeverityFiltersChange(newFilters);\n },\n [severityFilters, onSeverityFiltersChange],\n );\n\n const severityOptions: VitalPointSeverity[] = [\n VitalPointSeverity.LETHAL,\n VitalPointSeverity.CRITICAL,\n VitalPointSeverity.MAJOR,\n VitalPointSeverity.MODERATE,\n VitalPointSeverity.MINOR,\n ];\n\n const regionOptions: {\n value: BodyRegionFilter;\n label: string;\n korean: string;\n }[] = [\n { value: \"all\", label: \"All Regions\", korean: \"전체\" },\n { value: \"head\", label: \"Head\", korean: \"머리\" },\n { value: \"torso\", label: \"Torso\", korean: \"몸통\" },\n { value: \"arms\", label: \"Arms\", korean: \"팔\" },\n { value: \"legs\", label: \"Legs\", korean: \"다리\" },\n ];\n\n const panelWidth = isMobile ? 280 : 350;\n const buttonHeight = isMobile ? 32 : 36;\n const fontSize = isMobile ? 11 : 13;\n const smallFontSize = isMobile ? 9 : 10;\n\n return (\n <div\n style={{\n position: \"absolute\",\n top: finalPosition.top,\n left: finalPosition.left,\n right: finalPosition.right,\n bottom: finalPosition.bottom,\n width: panelWidth,\n background: `${hexColorToCSS(KOREAN_COLORS.UI_BACKGROUND_DARK)}f0`,\n border: `2px solid ${hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN)}`,\n borderRadius: \"12px\",\n padding: isMobile ? \"12px\" : \"16px\",\n fontFamily: FONT_FAMILY.KOREAN,\n color: hexColorToCSS(KOREAN_COLORS.TEXT_PRIMARY),\n boxShadow: `0 0 30px ${hexColorToCSS(\n KOREAN_COLORS.PRIMARY_CYAN,\n )}40, inset 0 0 20px ${hexColorToCSS(KOREAN_COLORS.UI_BACKGROUND_DARK)}80`,\n transition: \"all 0.3s ease\",\n pointerEvents: \"all\",\n zIndex: Z_INDEX.MODAL,\n }}\n data-testid=\"vital-point-overlay-controls\"\n >\n {/* Header with toggle */}\n <div\n style={{\n display: \"flex\",\n justifyContent: \"space-between\",\n alignItems: \"center\",\n marginBottom: \"12px\",\n paddingBottom: \"12px\",\n borderBottom: `1px solid ${hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN)}40`,\n background: `linear-gradient(90deg, ${hexColorToCSS(\n KOREAN_COLORS.UI_BACKGROUND_DARK,\n )}00 0%, ${hexColorToCSS(\n KOREAN_COLORS.PRIMARY_CYAN,\n )}10 50%, ${hexColorToCSS(KOREAN_COLORS.UI_BACKGROUND_DARK)}00 100%)`,\n }}\n >\n <div>\n <div style={{ fontSize: isMobile ? 14 : 16, fontWeight: \"bold\" }}>\n 급소 오버레이 | Vital Points\n </div>\n <div\n style={{\n fontSize: smallFontSize,\n color: hexColorToCSS(KOREAN_COLORS.TEXT_SECONDARY),\n marginTop: \"2px\",\n }}\n >\n {filteredCount} / {stats.total} 표시 | Showing\n </div>\n </div>\n <button\n onClick={() => setExpanded(!expanded)}\n style={{\n background: `linear-gradient(135deg, ${hexColorToCSS(\n KOREAN_COLORS.UI_BACKGROUND_MEDIUM,\n )} 0%, ${hexColorToCSS(KOREAN_COLORS.UI_BACKGROUND_DARK)} 100%)`,\n border: `2px solid ${hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN)}`,\n borderRadius: \"6px\",\n padding: \"8px 14px\",\n color: hexColorToCSS(KOREAN_COLORS.TEXT_PRIMARY),\n fontSize,\n cursor: \"pointer\",\n transition: \"all 0.2s ease\",\n boxShadow: `0 2px 8px ${hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN)}30`,\n }}\n onMouseEnter={(e) => {\n e.currentTarget.style.transform = \"scale(1.05)\";\n e.currentTarget.style.boxShadow = `0 4px 12px ${hexColorToCSS(\n KOREAN_COLORS.PRIMARY_CYAN,\n )}50`;\n }}\n onMouseLeave={(e) => {\n e.currentTarget.style.transform = \"scale(1)\";\n e.currentTarget.style.boxShadow = `0 2px 8px ${hexColorToCSS(\n KOREAN_COLORS.PRIMARY_CYAN,\n )}30`;\n }}\n data-testid=\"toggle-expand-button\"\n >\n {expanded ? \"▼\" : \"▶\"}\n </button>\n </div>\n\n {/* Main toggle */}\n <div style={{ marginBottom: \"14px\" }}>\n <button\n onClick={() => onVisibleChange(!visible)}\n style={{\n width: \"100%\",\n height: buttonHeight,\n background: visible\n ? `linear-gradient(135deg, ${hexColorToCSS(\n KOREAN_COLORS.ACCENT_GOLD,\n )} 0%, ${hexColorToCSS(KOREAN_COLORS.SECONDARY_YELLOW)} 100%)`\n : `linear-gradient(135deg, ${hexColorToCSS(\n KOREAN_COLORS.UI_BACKGROUND_MEDIUM,\n )} 0%, ${hexColorToCSS(KOREAN_COLORS.UI_BACKGROUND_DARK)} 100%)`,\n border: `2px solid ${\n visible\n ? hexColorToCSS(KOREAN_COLORS.ACCENT_GOLD)\n : hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN)\n }`,\n borderRadius: \"8px\",\n color: visible ? hexColorToCSS(KOREAN_COLORS.KOREAN_BLACK) : hexColorToCSS(KOREAN_COLORS.TEXT_PRIMARY),\n fontSize: isMobile ? 13 : 15,\n fontWeight: \"bold\",\n cursor: \"pointer\",\n transition: \"all 0.3s ease\",\n boxShadow: visible\n ? `0 4px 16px ${hexColorToCSS(\n KOREAN_COLORS.ACCENT_GOLD,\n )}60, inset 0 2px 4px ${hexToRgbaString(KOREAN_COLORS.TEXT_PRIMARY, 0.2)}`\n : `0 2px 8px ${hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN)}30`,\n textTransform: \"uppercase\",\n letterSpacing: \"0.5px\",\n }}\n onMouseEnter={(e) => {\n e.currentTarget.style.transform = \"translateY(-2px)\";\n e.currentTarget.style.boxShadow = visible\n ? `0 6px 20px ${hexColorToCSS(KOREAN_COLORS.ACCENT_GOLD)}80`\n : `0 4px 12px ${hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN)}50`;\n }}\n onMouseLeave={(e) => {\n e.currentTarget.style.transform = \"translateY(0)\";\n e.currentTarget.style.boxShadow = visible\n ? `0 4px 16px ${hexColorToCSS(KOREAN_COLORS.ACCENT_GOLD)}60`\n : `0 2px 8px ${hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN)}30`;\n }}\n data-testid=\"toggle-visibility-button\"\n >\n {visible ? \"✓ 활성화 | Enabled\" : \"비활성화 | Disabled\"}\n </button>\n </div>\n\n {/* Expanded controls */}\n {expanded && visible && (\n <>\n {/* Severity filters */}\n <div style={{ marginBottom: \"14px\" }}>\n <div\n style={{\n fontSize,\n marginBottom: \"8px\",\n color: hexColorToCSS(KOREAN_COLORS.ACCENT_CYAN),\n fontWeight: \"600\",\n textTransform: \"uppercase\",\n letterSpacing: \"1px\",\n }}\n >\n 심각도 필터 | Severity Filter\n </div>\n <div\n style={{\n display: \"flex\",\n flexWrap: \"wrap\",\n gap: \"8px\",\n }}\n >\n {severityOptions.map((severity) => {\n const isActive = severityFilters.includes(severity);\n const severityColor = getSeverityColor(severity);\n return (\n <button\n key={severity}\n onClick={() => toggleSeverityFilter(severity)}\n style={{\n background: isActive\n ? `linear-gradient(135deg, ${severityColor} 0%, ${severityColor}cc 100%)`\n : `${hexColorToCSS(KOREAN_COLORS.UI_BACKGROUND_MEDIUM)}`,\n border: `2px solid ${severityColor}`,\n borderRadius: \"6px\",\n padding: \"6px 12px\",\n color: hexColorToCSS(KOREAN_COLORS.TEXT_PRIMARY),\n fontSize: smallFontSize,\n cursor: \"pointer\",\n opacity: isActive ? 1 : 0.6,\n transition: \"all 0.2s ease\",\n fontWeight: isActive ? \"bold\" : \"normal\",\n boxShadow: isActive\n ? `0 2px 8px ${severityColor}50`\n : \"none\",\n }}\n onMouseEnter={(e) => {\n e.currentTarget.style.opacity = \"1\";\n e.currentTarget.style.transform = \"scale(1.05)\";\n }}\n onMouseLeave={(e) => {\n e.currentTarget.style.opacity = isActive ? \"1\" : \"0.6\";\n e.currentTarget.style.transform = \"scale(1)\";\n }}\n data-testid={`severity-filter-${severity}`}\n >\n {severity}\n </button>\n );\n })}\n </div>\n </div>\n\n {/* Region filter */}\n <div style={{ marginBottom: \"14px\" }}>\n <div\n style={{\n fontSize,\n marginBottom: \"8px\",\n color: hexColorToCSS(KOREAN_COLORS.ACCENT_CYAN),\n fontWeight: \"600\",\n textTransform: \"uppercase\",\n letterSpacing: \"1px\",\n }}\n >\n 부위 필터 | Region Filter\n </div>\n <div\n style={{\n display: \"flex\",\n flexWrap: \"wrap\",\n gap: \"8px\",\n }}\n >\n {regionOptions.map((option) => {\n const isActive = regionFilter === option.value;\n return (\n <button\n key={option.value}\n onClick={() => onRegionFilterChange(option.value)}\n style={{\n background: isActive\n ? `linear-gradient(135deg, ${hexColorToCSS(\n KOREAN_COLORS.PRIMARY_CYAN,\n )} 0%, ${hexColorToCSS(KOREAN_COLORS.ACCENT_BLUE)} 100%)`\n : `${hexColorToCSS(KOREAN_COLORS.UI_BACKGROUND_MEDIUM)}`,\n border: `2px solid ${hexColorToCSS(\n KOREAN_COLORS.PRIMARY_CYAN,\n )}`,\n borderRadius: \"6px\",\n padding: \"6px 12px\",\n color: hexColorToCSS(KOREAN_COLORS.TEXT_PRIMARY),\n fontSize: smallFontSize,\n cursor: \"pointer\",\n opacity: isActive ? 1 : 0.6,\n transition: \"all 0.2s ease\",\n fontWeight: isActive ? \"bold\" : \"normal\",\n boxShadow: isActive\n ? `0 2px 8px ${hexColorToCSS(\n KOREAN_COLORS.PRIMARY_CYAN,\n )}50`\n : \"none\",\n }}\n onMouseEnter={(e) => {\n e.currentTarget.style.opacity = \"1\";\n e.currentTarget.style.transform = \"scale(1.05)\";\n }}\n onMouseLeave={(e) => {\n e.currentTarget.style.opacity = isActive ? \"1\" : \"0.6\";\n e.currentTarget.style.transform = \"scale(1)\";\n }}\n data-testid={`region-filter-${option.value}`}\n >\n {option.korean} | {option.label}\n </button>\n );\n })}\n </div>\n </div>\n\n {/* Search box with clear button */}\n <div style={{ marginBottom: \"14px\" }}>\n <div\n style={{\n fontSize,\n marginBottom: \"8px\",\n color: hexColorToCSS(KOREAN_COLORS.ACCENT_CYAN),\n fontWeight: \"600\",\n textTransform: \"uppercase\",\n letterSpacing: \"1px\",\n }}\n >\n 검색 | Search\n </div>\n <div style={{ position: \"relative\" }}>\n <input\n type=\"text\"\n value={searchQuery}\n onChange={(e) => setSearchQuery(e.target.value)}\n placeholder=\"급소 이름... | Point name...\"\n style={{\n width: \"100%\",\n height: buttonHeight,\n background: `${hexColorToCSS(\n KOREAN_COLORS.UI_BACKGROUND_MEDIUM,\n )}`,\n border: `2px solid ${hexColorToCSS(\n KOREAN_COLORS.PRIMARY_CYAN,\n )}40`,\n borderRadius: \"8px\",\n padding: \"0 40px 0 14px\", // Add right padding for clear button\n color: hexColorToCSS(KOREAN_COLORS.TEXT_PRIMARY),\n fontSize,\n fontFamily: FONT_FAMILY.KOREAN,\n transition: \"all 0.2s ease\",\n outline: \"none\",\n }}\n onFocus={(e) => {\n e.currentTarget.style.borderColor = hexColorToCSS(\n KOREAN_COLORS.PRIMARY_CYAN,\n );\n e.currentTarget.style.boxShadow = `0 0 12px ${hexColorToCSS(\n KOREAN_COLORS.PRIMARY_CYAN,\n )}40`;\n }}\n onBlur={(e) => {\n e.currentTarget.style.borderColor = `${hexColorToCSS(\n KOREAN_COLORS.PRIMARY_CYAN,\n )}40`;\n e.currentTarget.style.boxShadow = \"none\";\n }}\n data-testid=\"search-input\"\n />\n {/* Clear button */}\n {searchQuery && (\n <button\n onClick={() => setSearchQuery(\"\")}\n style={{\n position: \"absolute\",\n right: \"8px\",\n top: \"50%\",\n transform: \"translateY(-50%)\",\n background: \"transparent\",\n border: \"none\",\n color: hexColorToCSS(KOREAN_COLORS.TEXT_SECONDARY),\n cursor: \"pointer\",\n fontSize: \"16px\",\n padding: \"4px\",\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n borderRadius: \"4px\",\n transition: \"all 0.2s ease\",\n }}\n onMouseEnter={(e) => {\n e.currentTarget.style.color = hexColorToCSS(\n KOREAN_COLORS.ACCENT_RED,\n );\n e.currentTarget.style.background = `${hexColorToCSS(\n KOREAN_COLORS.UI_BACKGROUND_MEDIUM,\n )}`;\n }}\n onMouseLeave={(e) => {\n e.currentTarget.style.color = hexColorToCSS(\n KOREAN_COLORS.TEXT_SECONDARY,\n );\n e.currentTarget.style.background = \"transparent\";\n }}\n data-testid=\"search-clear-button\"\n title=\"Clear search\"\n >\n ✕\n </button>\n )}\n </div>\n </div>\n\n {/* Display options */}\n <div style={{ marginBottom: \"14px\" }}>\n <div\n style={{\n fontSize,\n marginBottom: \"8px\",\n color: hexColorToCSS(KOREAN_COLORS.ACCENT_CYAN),\n fontWeight: \"600\",\n textTransform: \"uppercase\",\n letterSpacing: \"1px\",\n }}\n >\n 표시 옵션 | Display Options\n </div>\n <div\n style={{ display: \"flex\", flexDirection: \"column\", gap: \"8px\" }}\n >\n <label\n style={{\n display: \"flex\",\n alignItems: \"center\",\n gap: \"8px\",\n cursor: \"pointer\",\n }}\n >\n <input\n type=\"checkbox\"\n checked={showLabels}\n onChange={(e) => onShowLabelsChange(e.target.checked)}\n style={{\n width: \"16px\",\n height: \"16px\",\n accentColor: hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN),\n }}\n data-testid=\"show-labels-checkbox\"\n />\n <span style={{ fontSize }}>라벨 표시 | Show Labels</span>\n </label>\n <label\n style={{\n display: \"flex\",\n alignItems: \"center\",\n gap: \"8px\",\n cursor: \"pointer\",\n }}\n >\n <input\n type=\"checkbox\"\n checked={animated}\n onChange={(e) => onAnimatedChange(e.target.checked)}\n style={{\n width: \"16px\",\n height: \"16px\",\n accentColor: hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN),\n }}\n data-testid=\"animated-checkbox\"\n />\n <span style={{ fontSize }}>애니메이션 | Animations</span>\n </label>\n </div>\n </div>\n\n {/* Reset filters button */}\n {(severityFilters.length > 0 ||\n regionFilter !== \"all\" ||\n searchQuery !== \"\") && (\n <div style={{ marginBottom: \"14px\" }}>\n <button\n onClick={() => {\n onSeverityFiltersChange([]);\n onRegionFilterChange(\"all\");\n setSearchQuery(\"\");\n }}\n style={{\n width: \"100%\",\n height: buttonHeight - 4,\n background: `${hexColorToCSS(\n KOREAN_COLORS.UI_BACKGROUND_MEDIUM,\n )}`,\n border: `2px solid ${hexColorToCSS(\n KOREAN_COLORS.ACCENT_ORANGE,\n )}`,\n borderRadius: \"6px\",\n color: hexColorToCSS(KOREAN_COLORS.ACCENT_ORANGE),\n fontSize: smallFontSize,\n cursor: \"pointer\",\n transition: \"all 0.2s ease\",\n fontWeight: \"bold\",\n }}\n onMouseEnter={(e) => {\n e.currentTarget.style.background = `${hexColorToCSS(\n KOREAN_COLORS.ACCENT_ORANGE,\n )}20`;\n e.currentTarget.style.transform = \"translateY(-1px)\";\n }}\n onMouseLeave={(e) => {\n e.currentTarget.style.background = `${hexColorToCSS(\n KOREAN_COLORS.UI_BACKGROUND_MEDIUM,\n )}`;\n e.currentTarget.style.transform = \"translateY(0)\";\n }}\n data-testid=\"reset-filters-button\"\n >\n 🔄 필터 초기화 | Reset Filters\n </button>\n </div>\n )}\n\n {/* Scale slider */}\n <div>\n <div\n style={{\n fontSize,\n marginBottom: \"6px\",\n color: hexColorToCSS(KOREAN_COLORS.ACCENT_CYAN),\n }}\n >\n 크기 | Scale: {scale.toFixed(1)}x\n </div>\n <input\n type=\"range\"\n min=\"0.5\"\n max=\"2.0\"\n step=\"0.1\"\n value={scale}\n onChange={(e) => onScaleChange(parseFloat(e.target.value))}\n style={{\n width: \"100%\",\n accentColor: hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN),\n }}\n data-testid=\"scale-slider\"\n />\n </div>\n\n {/* Statistics */}\n <div\n style={{\n marginTop: \"12px\",\n paddingTop: \"12px\",\n borderTop: `1px solid ${hexColorToCSS(\n KOREAN_COLORS.PRIMARY_CYAN,\n )}40`,\n fontSize: smallFontSize,\n color: hexColorToCSS(KOREAN_COLORS.TEXT_SECONDARY),\n }}\n >\n <div>\n 머리: {stats.byRegion.head} | 몸통: {stats.byRegion.torso}\n </div>\n <div>\n 팔: {stats.byRegion.arms} | 다리: {stats.byRegion.legs}\n </div>\n </div>\n </>\n )}\n </div>\n );\n};\n\nexport default VitalPointOverlayControlsPure;\n"],"mappings":"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"}