feat&fix: 完善JPEGClient逻辑,前端新增编码器组件
This commit is contained in:
parent
3644c75304
commit
fdfc5729ec
|
@ -258,12 +258,12 @@ public class Image
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 将原始 JPEG 数据补全 JPEG 头部,生成完整的 JPEG 图片
|
/// 将原始 JPEG 数据补全 JPEG 头部,生成完整的 JPEG 图片
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="jpegData">原始 JPEG 数据(可能缺少完整头部)</param>
|
/// <param name="jpegData">原始 JPEG 扫描数据(不含头尾)</param>
|
||||||
/// <param name="width">图像宽度</param>
|
/// <param name="width">图像宽度</param>
|
||||||
/// <param name="height">图像高度</param>
|
/// <param name="height">图像高度</param>
|
||||||
/// <param name="quality">JPEG质量(1-100,默认80)</param>
|
/// <param name="quantizationTable">量化表数组(Y0-Y63, Cb0-Cb63, Cr0-Cr63,共192个值)</param>
|
||||||
/// <returns>完整的 JPEG 图片数据</returns>
|
/// <returns>完整的 JPEG 图片数据</returns>
|
||||||
public static Result<byte[]> CompleteJpegData(byte[] jpegData, int width, int height, int quality = 80)
|
public static Result<byte[]> CompleteJpegData(byte[] jpegData, int width, int height, uint[] quantizationTable)
|
||||||
{
|
{
|
||||||
if (jpegData == null)
|
if (jpegData == null)
|
||||||
return new(new ArgumentNullException(nameof(jpegData)));
|
return new(new ArgumentNullException(nameof(jpegData)));
|
||||||
|
@ -271,95 +271,148 @@ public class Image
|
||||||
if (width <= 0 || height <= 0)
|
if (width <= 0 || height <= 0)
|
||||||
return new(new ArgumentException("Width and height must be positive"));
|
return new(new ArgumentException("Width and height must be positive"));
|
||||||
|
|
||||||
if (quality < 1 || quality > 100)
|
if (quantizationTable == null || quantizationTable.Length != 192)
|
||||||
return new(new ArgumentException("Quality must be between 1 and 100"));
|
return new(new ArgumentException("Quantization table must contain exactly 192 values (64 Y + 64 Cb + 64 Cr)"));
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// 检查是否已经是完整的 JPEG 文件(以 FFD8 开头,FFD9 结尾)
|
var jpegBytes = new List<byte>();
|
||||||
if (jpegData.Length >= 4 &&
|
|
||||||
jpegData[0] == 0xFF && jpegData[1] == 0xD8 &&
|
// SOI (Start of Image)
|
||||||
jpegData[jpegData.Length - 2] == 0xFF && jpegData[jpegData.Length - 1] == 0xD9)
|
jpegBytes.AddRange(new byte[] { 0xFF, 0xD8 });
|
||||||
|
|
||||||
|
// APP0 段
|
||||||
|
jpegBytes.AddRange(new byte[] {
|
||||||
|
0xFF, 0xE0, // APP0 marker
|
||||||
|
0x00, 0x10, // Length (16 bytes)
|
||||||
|
0x4A, 0x46, 0x49, 0x46, 0x00, // "JFIF\0"
|
||||||
|
0x01, 0x01, // Version 1.1
|
||||||
|
0x00, // Units: 0 = no units
|
||||||
|
0x00, 0x01, // X density (1)
|
||||||
|
0x00, 0x01, // Y density (1)
|
||||||
|
0x00, // Thumbnail width
|
||||||
|
0x00 // Thumbnail height
|
||||||
|
});
|
||||||
|
|
||||||
|
// DQT (Define Quantization Table) - Y table
|
||||||
|
jpegBytes.AddRange(new byte[] { 0xFF, 0xDB }); // DQT marker
|
||||||
|
jpegBytes.AddRange(new byte[] { 0x00, 0x43 }); // Length (67 bytes)
|
||||||
|
jpegBytes.Add(0x00); // Table ID (0 = Y table)
|
||||||
|
|
||||||
|
// 添加Y量化表 (quantizationTable[0-63])
|
||||||
|
for (int i = 0; i < 64; i++)
|
||||||
{
|
{
|
||||||
// 已经是完整的 JPEG 文件,直接返回
|
jpegBytes.Add((byte)Math.Min(255, quantizationTable[i]));
|
||||||
return jpegData;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建一个临时的 RGB24 图像用于生成 JPEG 头部
|
// DQT (Define Quantization Table) - CbCr table
|
||||||
using var tempImage = new SixLabors.ImageSharp.Image<Rgb24>(new Configuration
|
jpegBytes.AddRange(new byte[] { 0xFF, 0xDB }); // DQT marker
|
||||||
{
|
jpegBytes.AddRange(new byte[] { 0x00, 0x43 }); // Length (67 bytes)
|
||||||
|
jpegBytes.Add(0x01); // Table ID (1 = CbCr table)
|
||||||
|
|
||||||
}, width, height);
|
// 添加Cb量化表 (quantizationTable[64-127]),但这里使用Cr表的数据作为CbCr共用
|
||||||
|
for (int i = 128; i < 192; i++) // 使用Cr量化表 (quantizationTable[128-191])
|
||||||
// 填充临时图像(使用简单的渐变色作为占位符)
|
|
||||||
for (int y = 0; y < height; y++)
|
|
||||||
{
|
{
|
||||||
for (int x = 0; x < width; x++)
|
jpegBytes.Add((byte)Math.Min(255, quantizationTable[i]));
|
||||||
{
|
|
||||||
tempImage[x, y] = new Rgb24((byte)(x % 256), (byte)(y % 256), 128);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
using var stream = new MemoryStream();
|
// SOF0 (Start of Frame)
|
||||||
tempImage.SaveAsJpeg(stream, new JpegEncoder { Quality = quality });
|
jpegBytes.AddRange(new byte[] {
|
||||||
var completeJpeg = stream.ToArray();
|
0xFF, 0xC0, // SOF0 marker
|
||||||
|
0x00, 0x11, // Length (17 bytes)
|
||||||
|
0x08, // Precision (8 bits)
|
||||||
|
(byte)((height >> 8) & 0xFF), (byte)(height & 0xFF), // Height
|
||||||
|
(byte)((width >> 8) & 0xFF), (byte)(width & 0xFF), // Width
|
||||||
|
0x03, // Number of components
|
||||||
|
0x01, 0x11, 0x00, // Y component
|
||||||
|
0x02, 0x11, 0x01, // Cb component
|
||||||
|
0x03, 0x11, 0x01 // Cr component
|
||||||
|
});
|
||||||
|
|
||||||
// 如果原始数据看起来是 JPEG 扫描数据,尝试替换扫描数据部分
|
// DHT (Define Huffman Table) - DC Y table
|
||||||
if (jpegData.Length > 0)
|
jpegBytes.AddRange(new byte[] {
|
||||||
{
|
0xFF, 0xC4, // DHT marker
|
||||||
// 查找 JPEG 扫描数据开始位置(SOS 标记 0xFFDA 后)
|
0x00, 0x1F, // Length
|
||||||
int sosIndex = -1;
|
0x00, // Table class and ID (DC table 0)
|
||||||
for (int i = 0; i < completeJpeg.Length - 1; i++)
|
// DC Y Huffman table
|
||||||
{
|
0x00, 0x03, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
if (completeJpeg[i] == 0xFF && completeJpeg[i + 1] == 0xDA)
|
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B
|
||||||
{
|
});
|
||||||
// 跳过 SOS 段头部,找到实际扫描数据开始位置
|
|
||||||
i += 2; // 跳过 FF DA
|
|
||||||
if (i < completeJpeg.Length - 1)
|
|
||||||
{
|
|
||||||
int segmentLength = (completeJpeg[i] << 8) | completeJpeg[i + 1];
|
|
||||||
sosIndex = i + segmentLength;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 查找 EOI 标记位置(0xFFD9)
|
// DHT (Define Huffman Table) - AC Y table (简化版)
|
||||||
int eoiIndex = -1;
|
jpegBytes.AddRange(new byte[] {
|
||||||
for (int i = completeJpeg.Length - 2; i >= 0; i--)
|
0xFF, 0xC4, // DHT marker
|
||||||
{
|
0x00, 0xB5, // Length
|
||||||
if (completeJpeg[i] == 0xFF && completeJpeg[i + 1] == 0xD9)
|
0x10 // Table class and ID (AC table 0)
|
||||||
{
|
});
|
||||||
eoiIndex = i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sosIndex > 0 && eoiIndex > sosIndex)
|
// AC Y Huffman table数据
|
||||||
{
|
jpegBytes.AddRange(new byte[] {
|
||||||
// 替换扫描数据部分
|
0x00, 0x02, 0x01, 0x03, 0x03, 0x02, 0x04, 0x03, 0x05, 0x05, 0x04, 0x04, 0x00, 0x00, 0x01, 0x7D,
|
||||||
var headerLength = sosIndex;
|
0x01, 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12, 0x21, 0x31, 0x41, 0x06, 0x13, 0x51, 0x61, 0x07,
|
||||||
var footerStart = eoiIndex;
|
0x22, 0x71, 0x14, 0x32, 0x81, 0x91, 0xA1, 0x08, 0x23, 0x42, 0xB1, 0xC1, 0x15, 0x52, 0xD1, 0xF0,
|
||||||
var footerLength = completeJpeg.Length - footerStart;
|
0x24, 0x33, 0x62, 0x72, 0x82, 0x09, 0x0A, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x25, 0x26, 0x27, 0x28,
|
||||||
|
0x29, 0x2A, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49,
|
||||||
|
0x4A, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5A, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69,
|
||||||
|
0x6A, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89,
|
||||||
|
0x8A, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9A, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7,
|
||||||
|
0xA8, 0xA9, 0xAA, 0xB2, 0xB3, 0xB4, 0xB5, 0xB6, 0xB7, 0xB8, 0xB9, 0xBA, 0xC2, 0xC3, 0xC4, 0xC5,
|
||||||
|
0xC6, 0xC7, 0xC8, 0xC9, 0xCA, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0xD7, 0xD8, 0xD9, 0xDA, 0xE1, 0xE2,
|
||||||
|
0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xE8, 0xE9, 0xEA, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0xF8,
|
||||||
|
0xF9, 0xFA
|
||||||
|
});
|
||||||
|
|
||||||
var newJpegLength = headerLength + jpegData.Length + footerLength;
|
// DHT (Define Huffman Table) - DC CbCr table
|
||||||
var newJpegData = new byte[newJpegLength];
|
jpegBytes.AddRange(new byte[] {
|
||||||
|
0xFF, 0xC4, // DHT marker
|
||||||
|
0x00, 0x1F, // Length
|
||||||
|
0x01, // Table class and ID (DC table 1)
|
||||||
|
// DC CbCr Huffman table
|
||||||
|
0x00, 0x03, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B
|
||||||
|
});
|
||||||
|
|
||||||
// 复制头部
|
// DHT (Define Huffman Table) - AC CbCr table
|
||||||
Array.Copy(completeJpeg, 0, newJpegData, 0, headerLength);
|
jpegBytes.AddRange(new byte[] {
|
||||||
|
0xFF, 0xC4, // DHT marker
|
||||||
|
0x00, 0xB5, // Length
|
||||||
|
0x11 // Table class and ID (AC table 1)
|
||||||
|
});
|
||||||
|
|
||||||
// 复制原始扫描数据
|
// AC CbCr Huffman table数据(与AC Y table相同)
|
||||||
Array.Copy(jpegData, 0, newJpegData, headerLength, jpegData.Length);
|
jpegBytes.AddRange(new byte[] {
|
||||||
|
0x00, 0x02, 0x01, 0x03, 0x03, 0x02, 0x04, 0x03, 0x05, 0x05, 0x04, 0x04, 0x00, 0x00, 0x01, 0x7D,
|
||||||
|
0x01, 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12, 0x21, 0x31, 0x41, 0x06, 0x13, 0x51, 0x61, 0x07,
|
||||||
|
0x22, 0x71, 0x14, 0x32, 0x81, 0x91, 0xA1, 0x08, 0x23, 0x42, 0xB1, 0xC1, 0x15, 0x52, 0xD1, 0xF0,
|
||||||
|
0x24, 0x33, 0x62, 0x72, 0x82, 0x09, 0x0A, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x25, 0x26, 0x27, 0x28,
|
||||||
|
0x29, 0x2A, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49,
|
||||||
|
0x4A, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5A, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69,
|
||||||
|
0x6A, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89,
|
||||||
|
0x8A, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9A, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7,
|
||||||
|
0xA8, 0xA9, 0xAA, 0xB2, 0xB3, 0xB4, 0xB5, 0xB6, 0xB7, 0xB8, 0xB9, 0xBA, 0xC2, 0xC3, 0xC4, 0xC5,
|
||||||
|
0xC6, 0xC7, 0xC8, 0xC9, 0xCA, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0xD7, 0xD8, 0xD9, 0xDA, 0xE1, 0xE2,
|
||||||
|
0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xE8, 0xE9, 0xEA, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0xF8,
|
||||||
|
0xF9, 0xFA
|
||||||
|
});
|
||||||
|
|
||||||
// 复制尾部
|
// SOS (Start of Scan)
|
||||||
Array.Copy(completeJpeg, footerStart, newJpegData, headerLength + jpegData.Length, footerLength);
|
jpegBytes.AddRange(new byte[] {
|
||||||
|
0xFF, 0xDA, // SOS marker
|
||||||
|
0x00, 0x0C, // Length (12 bytes)
|
||||||
|
0x03, // Number of components
|
||||||
|
0x01, 0x00, // Y component, DC/AC table
|
||||||
|
0x02, 0x11, // Cb component, DC/AC table
|
||||||
|
0x03, 0x11, // Cr component, DC/AC table
|
||||||
|
0x00, 0x3F, 0x00 // Start of spectral, End of spectral, Ah/Al
|
||||||
|
});
|
||||||
|
|
||||||
return newJpegData;
|
// 添加原始 JPEG 扫描数据
|
||||||
}
|
jpegBytes.AddRange(jpegData);
|
||||||
}
|
|
||||||
|
|
||||||
// 如果无法智能合并,返回完整的模板 JPEG
|
// EOI (End of Image)
|
||||||
return completeJpeg;
|
jpegBytes.AddRange(new byte[] { 0xFF, 0xD9 });
|
||||||
|
|
||||||
|
return jpegBytes.ToArray();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|
|
@ -6,7 +6,7 @@ namespace Peripherals.JpegClient;
|
||||||
|
|
||||||
static class JpegAddr
|
static class JpegAddr
|
||||||
{
|
{
|
||||||
const UInt32 BASE = 0x0000_0000;
|
const UInt32 BASE = 0xA000_0000;
|
||||||
|
|
||||||
public const UInt32 CAPTURE_RD_CTRL = BASE + 0x0;
|
public const UInt32 CAPTURE_RD_CTRL = BASE + 0x0;
|
||||||
public const UInt32 CAPTURE_WR_CTRL = BASE + 0x1;
|
public const UInt32 CAPTURE_WR_CTRL = BASE + 0x1;
|
||||||
|
@ -26,9 +26,11 @@ static class JpegAddr
|
||||||
public const UInt32 JPEG_FRAME_SAVE_NUM = BASE + 0xC;
|
public const UInt32 JPEG_FRAME_SAVE_NUM = BASE + 0xC;
|
||||||
public const UInt32 JPEG_FIFO_FRAME_INFO = BASE + 0xD;
|
public const UInt32 JPEG_FIFO_FRAME_INFO = BASE + 0xD;
|
||||||
|
|
||||||
public const UInt32 ADDR_HDMI_WD_START = 0x4000_0000;
|
public const UInt32 JPEG_QUANTIZATION_TABLE = BASE + 0x100;
|
||||||
public const UInt32 ADDR_JPEG_START = 0x8000_0000;
|
|
||||||
public const UInt32 ADDR_JPEG_END = 0xA000_0000;
|
public const UInt32 ADDR_HDMI_WD_START = 0x0400_0000;
|
||||||
|
public const UInt32 ADDR_JPEG_START = 0x0800_0000;
|
||||||
|
public const UInt32 ADDR_JPEG_END = 0x09FF_FFFF;
|
||||||
}
|
}
|
||||||
|
|
||||||
public class JpegInfo
|
public class JpegInfo
|
||||||
|
@ -142,23 +144,33 @@ public class Jpeg
|
||||||
else return true;
|
else return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async ValueTask<bool> SetEnable(bool enable)
|
public async ValueTask<Result<bool>> SetEnable(bool enable)
|
||||||
{
|
{
|
||||||
if (enable)
|
if (enable)
|
||||||
{
|
{
|
||||||
var ret = await UDPClientPool.WriteAddrSeq(
|
|
||||||
this.ep,
|
|
||||||
this.taskID,
|
|
||||||
[JpegAddr.CAPTURE_RD_CTRL, JpegAddr.CAPTURE_WR_CTRL],
|
|
||||||
[0b11, 0b01],
|
|
||||||
this.timeout
|
|
||||||
);
|
|
||||||
if (!ret.IsSuccessful)
|
|
||||||
{
|
{
|
||||||
logger.Error($"Failed to set JPEG enable: {ret.Error}");
|
var ret = await UDPClientPool.WriteAddrSeq(
|
||||||
return false;
|
this.ep,
|
||||||
|
this.taskID,
|
||||||
|
[JpegAddr.CAPTURE_RD_CTRL, JpegAddr.CAPTURE_WR_CTRL],
|
||||||
|
[0b11, 0b01],
|
||||||
|
this.timeout
|
||||||
|
);
|
||||||
|
if (!ret.IsSuccessful)
|
||||||
|
{
|
||||||
|
logger.Error($"Failed to set JPEG enable: {ret.Error}");
|
||||||
|
return ret.Value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return ret.Value;
|
{
|
||||||
|
var ret = await AddFrameNum2Process(1);
|
||||||
|
if (!ret)
|
||||||
|
{
|
||||||
|
logger.Error($"Failed to AddFrameNum2Process: {ret}");
|
||||||
|
return ret.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
@ -181,7 +193,7 @@ public class Jpeg
|
||||||
public async ValueTask<Result<bool>> CheckHdmiIsReady()
|
public async ValueTask<Result<bool>> CheckHdmiIsReady()
|
||||||
{
|
{
|
||||||
var ret = await UDPClientPool.ReadAddrWithWait(
|
var ret = await UDPClientPool.ReadAddrWithWait(
|
||||||
this.ep, this.taskID, JpegAddr.HDMI_NOT_READY, 0b01, 0b01, 100, this.timeout);
|
this.ep, this.taskID, JpegAddr.HDMI_NOT_READY, 0b00, 0b01, 100, this.timeout);
|
||||||
if (!ret.IsSuccessful)
|
if (!ret.IsSuccessful)
|
||||||
{
|
{
|
||||||
logger.Error($"Failed to check HDMI status: {ret.Error}");
|
logger.Error($"Failed to check HDMI status: {ret.Error}");
|
||||||
|
@ -192,8 +204,8 @@ public class Jpeg
|
||||||
|
|
||||||
public async ValueTask<Result<(int, int)>> GetHdmiResolution()
|
public async ValueTask<Result<(int, int)>> GetHdmiResolution()
|
||||||
{
|
{
|
||||||
var ret = await UDPClientPool.ReadAddr(
|
var ret = await UDPClientPool.ReadAddrByte(
|
||||||
this.ep, this.taskID, JpegAddr.HDMI_HEIGHT_WIDTH, 0, this.timeout);
|
this.ep, this.taskID, JpegAddr.HDMI_HEIGHT_WIDTH, this.timeout);
|
||||||
if (!ret.IsSuccessful)
|
if (!ret.IsSuccessful)
|
||||||
{
|
{
|
||||||
logger.Error($"Failed to get HDMI resolution: {ret.Error}");
|
logger.Error($"Failed to get HDMI resolution: {ret.Error}");
|
||||||
|
@ -207,11 +219,13 @@ public class Jpeg
|
||||||
return new(new Exception("Invalid HDMI resolution data length"));
|
return new(new Exception("Invalid HDMI resolution data length"));
|
||||||
}
|
}
|
||||||
|
|
||||||
var width = data[0] | (data[1] << 8);
|
var width = data[3] | (data[2] << 8);
|
||||||
var height = data[2] | (data[3] << 8);
|
var height = data[1] | (data[0] << 8);
|
||||||
this.Width = width;
|
this.Width = width;
|
||||||
this.Height = height;
|
this.Height = height;
|
||||||
|
|
||||||
|
logger.Info($"HDMI resolution: {width}x{height}");
|
||||||
|
|
||||||
return new((width, height));
|
return new((width, height));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -223,7 +237,22 @@ public class Jpeg
|
||||||
return new(new ArgumentException("Invalid HDMI resolution"));
|
return new(new ArgumentException("Invalid HDMI resolution"));
|
||||||
}
|
}
|
||||||
|
|
||||||
var frameSize = (UInt32)(width * height / 4);
|
var frameSize = (UInt32)(width * height);
|
||||||
|
|
||||||
|
{
|
||||||
|
var ret = await UDPClientPool.WriteAddr(
|
||||||
|
this.ep, this.taskID, JpegAddr.JPEG_HEIGHT_WIDTH, (uint)((height << 16) + width), this.timeout);
|
||||||
|
if (!ret.IsSuccessful)
|
||||||
|
{
|
||||||
|
logger.Error($"Failed to set HDMI output start address: {ret.Error}");
|
||||||
|
return new(ret.Error);
|
||||||
|
}
|
||||||
|
if (!ret.Value)
|
||||||
|
{
|
||||||
|
logger.Error($"Failed to set HDMI output start address");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
var ret = await UDPClientPool.WriteAddr(
|
var ret = await UDPClientPool.WriteAddr(
|
||||||
|
@ -243,7 +272,7 @@ public class Jpeg
|
||||||
{
|
{
|
||||||
var ret = await UDPClientPool.WriteAddr(
|
var ret = await UDPClientPool.WriteAddr(
|
||||||
this.ep, this.taskID, JpegAddr.END_WR_ADDR0,
|
this.ep, this.taskID, JpegAddr.END_WR_ADDR0,
|
||||||
JpegAddr.ADDR_HDMI_WD_START + frameSize, this.timeout);
|
JpegAddr.ADDR_HDMI_WD_START + frameSize - 1, this.timeout);
|
||||||
if (!ret.IsSuccessful)
|
if (!ret.IsSuccessful)
|
||||||
{
|
{
|
||||||
logger.Error($"Failed to set HDMI output end address: {ret.Error}");
|
logger.Error($"Failed to set HDMI output end address: {ret.Error}");
|
||||||
|
@ -274,7 +303,7 @@ public class Jpeg
|
||||||
{
|
{
|
||||||
var ret = await UDPClientPool.WriteAddr(
|
var ret = await UDPClientPool.WriteAddr(
|
||||||
this.ep, this.taskID, JpegAddr.END_RD_ADDR0,
|
this.ep, this.taskID, JpegAddr.END_RD_ADDR0,
|
||||||
JpegAddr.ADDR_HDMI_WD_START + frameSize, this.timeout);
|
JpegAddr.ADDR_HDMI_WD_START + frameSize - 1, this.timeout);
|
||||||
if (!ret.IsSuccessful)
|
if (!ret.IsSuccessful)
|
||||||
{
|
{
|
||||||
logger.Error($"Failed to set jpeg input end address: {ret.Error}");
|
logger.Error($"Failed to set jpeg input end address: {ret.Error}");
|
||||||
|
@ -339,14 +368,39 @@ public class Jpeg
|
||||||
|
|
||||||
public async ValueTask<uint> GetFrameNumber()
|
public async ValueTask<uint> GetFrameNumber()
|
||||||
{
|
{
|
||||||
var ret = await UDPClientPool.ReadAddrByte(
|
const int maxAttempts = 10;
|
||||||
this.ep, this.taskID, JpegAddr.JPEG_FRAME_SAVE_NUM, this.timeout);
|
const int delayMs = 5;
|
||||||
if (!ret.IsSuccessful)
|
|
||||||
|
for (int attempt = 0; attempt < maxAttempts; attempt++)
|
||||||
{
|
{
|
||||||
logger.Error($"Failed to get JPEG frame number: {ret.Error}");
|
var ret = await UDPClientPool.ReadAddrByte(
|
||||||
return 0;
|
this.ep, this.taskID, JpegAddr.JPEG_FRAME_SAVE_NUM, this.timeout);
|
||||||
|
if (!ret.IsSuccessful)
|
||||||
|
{
|
||||||
|
logger.Error($"Failed to get JPEG frame number on attempt {attempt + 1}: {ret.Error}");
|
||||||
|
if (attempt < maxAttempts - 1)
|
||||||
|
{
|
||||||
|
await Task.Delay(delayMs);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
var frameNumber = Number.BytesToUInt32(ret.Value.Options.Data ?? Array.Empty<byte>()).Value;
|
||||||
|
if (frameNumber != 0)
|
||||||
|
{
|
||||||
|
return frameNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果不是最后一次尝试,等待5ms后重试
|
||||||
|
if (attempt < maxAttempts - 1)
|
||||||
|
{
|
||||||
|
await Task.Delay(delayMs);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return Number.BytesToUInt32(ret.Value.Options.Data ?? Array.Empty<byte>()).Value;
|
|
||||||
|
// 所有尝试都失败或返回0
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async ValueTask<Optional<List<JpegInfo>>> GetFrameInfo(int num)
|
public async ValueTask<Optional<List<JpegInfo>>> GetFrameInfo(int num)
|
||||||
|
@ -379,14 +433,14 @@ public class Jpeg
|
||||||
return new(infos);
|
return new(infos);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async ValueTask<bool> AddFrameNum2Process(uint cnt)
|
public async ValueTask<Result<bool>> AddFrameNum2Process(uint cnt)
|
||||||
{
|
{
|
||||||
var ret = await UDPClientPool.WriteAddr(
|
var ret = await UDPClientPool.WriteAddr(
|
||||||
this.ep, this.taskID, JpegAddr.JPEG_ADD_NEED_FRAME_NUM, cnt, this.timeout);
|
this.ep, this.taskID, JpegAddr.JPEG_ADD_NEED_FRAME_NUM, cnt, this.timeout);
|
||||||
if (!ret.IsSuccessful)
|
if (!ret.IsSuccessful)
|
||||||
{
|
{
|
||||||
logger.Error($"Failed to update pointer: {ret.Error}");
|
logger.Error($"Failed to update pointer: {ret.Error}");
|
||||||
return false;
|
return ret.Value;
|
||||||
}
|
}
|
||||||
return ret.Value;
|
return ret.Value;
|
||||||
}
|
}
|
||||||
|
@ -510,4 +564,45 @@ public class Jpeg
|
||||||
|
|
||||||
return frames;
|
return frames;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async ValueTask<Result<uint[]?>> GetQuantizationTable()
|
||||||
|
{
|
||||||
|
const int totalQuantValues = 8 * 8 * 3; // Y(64) + Cb(64) + Cr(64) = 192个量化值
|
||||||
|
const int bytesPerValue = 4; // 每个量化值32bit = 4字节
|
||||||
|
const int totalBytes = totalQuantValues * bytesPerValue; // 总共768字节
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var ret = await UDPClientPool.ReadAddr4Bytes(
|
||||||
|
this.ep, this.taskID, JpegAddr.JPEG_QUANTIZATION_TABLE, totalBytes, this.timeout);
|
||||||
|
if (!ret.IsSuccessful)
|
||||||
|
{
|
||||||
|
logger.Error($"Failed to read JPEG quantization table: {ret.Error}");
|
||||||
|
return new(ret.Error);
|
||||||
|
}
|
||||||
|
|
||||||
|
var data = ret.Value;
|
||||||
|
if (data == null || data.Length != totalBytes)
|
||||||
|
{
|
||||||
|
logger.Error($"Invalid quantization table data length: expected {totalBytes}, got {data?.Length ?? 0}");
|
||||||
|
return new(new Exception("Invalid quantization table data length"));
|
||||||
|
}
|
||||||
|
|
||||||
|
var quantTable = new uint[totalQuantValues];
|
||||||
|
for (int i = 0; i < totalQuantValues; i++)
|
||||||
|
{
|
||||||
|
// 每32bit为一个量化值,按小端序读取
|
||||||
|
var offset = i * bytesPerValue;
|
||||||
|
quantTable[i] = (uint)(data[offset] | (data[offset + 1] << 8) | (data[offset + 2] << 16) | (data[offset + 3] << 24));
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Debug($"Successfully read JPEG quantization table with {totalQuantValues} values");
|
||||||
|
return quantTable;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.Error(ex, "Exception occurred while reading JPEG quantization table");
|
||||||
|
return new(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -235,7 +235,19 @@ public class HttpHdmiVideoStreamService : BackgroundService
|
||||||
}
|
}
|
||||||
|
|
||||||
var jpegData = frameResult.Value[0];
|
var jpegData = frameResult.Value[0];
|
||||||
var jpegImage = Common.Image.CompleteJpegData(jpegData, client.Width, client.Height);
|
|
||||||
|
var quantTableResult = await client.JpegClient.GetQuantizationTable();
|
||||||
|
if (!quantTableResult.IsSuccessful || quantTableResult.Value == null)
|
||||||
|
{
|
||||||
|
logger.Error("获取JPEG量化表失败: {Error}", quantTableResult.Error);
|
||||||
|
response.StatusCode = 500;
|
||||||
|
var errorBytes = System.Text.Encoding.UTF8.GetBytes("Failed to get quantization table");
|
||||||
|
await response.OutputStream.WriteAsync(errorBytes, 0, errorBytes.Length, cancellationToken);
|
||||||
|
response.Close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var jpegImage = Common.Image.CompleteJpegData(jpegData, client.Width, client.Height, quantTableResult.Value);
|
||||||
if (!jpegImage.IsSuccessful)
|
if (!jpegImage.IsSuccessful)
|
||||||
{
|
{
|
||||||
logger.Error("JPEG数据补全失败");
|
logger.Error("JPEG数据补全失败");
|
||||||
|
@ -280,6 +292,18 @@ public class HttpHdmiVideoStreamService : BackgroundService
|
||||||
|
|
||||||
logger.Debug("开始HDMI MJPEG流传输");
|
logger.Debug("开始HDMI MJPEG流传输");
|
||||||
|
|
||||||
|
var quantTableResult = await client.JpegClient.GetQuantizationTable();
|
||||||
|
if (!quantTableResult.IsSuccessful || quantTableResult.Value == null)
|
||||||
|
{
|
||||||
|
logger.Error("获取JPEG量化表失败: {Error}", quantTableResult.Error);
|
||||||
|
response.StatusCode = 500;
|
||||||
|
await response.OutputStream.WriteAsync(
|
||||||
|
System.Text.Encoding.UTF8.GetBytes("Failed to get quantization table"), 0, 0, cancellationToken);
|
||||||
|
response.Close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var quantTable = quantTableResult.Value;
|
||||||
|
|
||||||
int frameCounter = 0;
|
int frameCounter = 0;
|
||||||
|
|
||||||
while (!cancellationToken.IsCancellationRequested)
|
while (!cancellationToken.IsCancellationRequested)
|
||||||
|
@ -297,7 +321,7 @@ public class HttpHdmiVideoStreamService : BackgroundService
|
||||||
|
|
||||||
foreach (var framebytes in frameResult.Value)
|
foreach (var framebytes in frameResult.Value)
|
||||||
{
|
{
|
||||||
var jpegImage = Common.Image.CompleteJpegData(framebytes, client.Width, client.Height);
|
var jpegImage = Common.Image.CompleteJpegData(framebytes, client.Width, client.Height, quantTable);
|
||||||
if (!jpegImage.IsSuccessful)
|
if (!jpegImage.IsSuccessful)
|
||||||
{
|
{
|
||||||
logger.Error("JPEG数据不完整");
|
logger.Error("JPEG数据不完整");
|
||||||
|
|
|
@ -29,6 +29,7 @@ export interface TemplateConfig {
|
||||||
export const previewSizes: Record<string, number> = {
|
export const previewSizes: Record<string, number> = {
|
||||||
MechanicalButton: 0.4,
|
MechanicalButton: 0.4,
|
||||||
Switch: 0.35,
|
Switch: 0.35,
|
||||||
|
EC11RotaryEncoder: 0.4,
|
||||||
Pin: 0.8,
|
Pin: 0.8,
|
||||||
SMT_LED: 0.7,
|
SMT_LED: 0.7,
|
||||||
SevenSegmentDisplay: 0.4,
|
SevenSegmentDisplay: 0.4,
|
||||||
|
@ -48,6 +49,7 @@ export const previewSizes: Record<string, number> = {
|
||||||
export const availableComponents: ComponentConfig[] = [
|
export const availableComponents: ComponentConfig[] = [
|
||||||
{ type: "MechanicalButton", name: "机械按钮" },
|
{ type: "MechanicalButton", name: "机械按钮" },
|
||||||
{ type: "Switch", name: "开关" },
|
{ type: "Switch", name: "开关" },
|
||||||
|
{ type: "EC11RotaryEncoder", name: "EC11旋转编码器" },
|
||||||
{ type: "Pin", name: "引脚" },
|
{ type: "Pin", name: "引脚" },
|
||||||
{ type: "SMT_LED", name: "贴片LED" },
|
{ type: "SMT_LED", name: "贴片LED" },
|
||||||
{ type: "SevenSegmentDisplay", name: "数码管" },
|
{ type: "SevenSegmentDisplay", name: "数码管" },
|
||||||
|
|
|
@ -0,0 +1,258 @@
|
||||||
|
<template>
|
||||||
|
<div class="ec11-container" :style="{
|
||||||
|
width: width + 'px',
|
||||||
|
height: height + 'px',
|
||||||
|
position: 'relative',
|
||||||
|
}">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" :width="width" :height="height" viewBox="0 0 100 100"
|
||||||
|
class="ec11-encoder">
|
||||||
|
<defs>
|
||||||
|
<!-- 发光效果滤镜 -->
|
||||||
|
<filter id="glow" x="-50%" y="-50%" width="200%" height="200%">
|
||||||
|
<feFlood result="flood" flood-color="#00ff88" flood-opacity="1"></feFlood>
|
||||||
|
<feComposite in="flood" result="mask" in2="SourceGraphic" operator="in"></feComposite>
|
||||||
|
<feMorphology in="mask" result="dilated" operator="dilate" radius="1"></feMorphology>
|
||||||
|
<feGaussianBlur in="dilated" stdDeviation="2" result="blur1" />
|
||||||
|
<feGaussianBlur in="dilated" stdDeviation="4" result="blur2" />
|
||||||
|
<feGaussianBlur in="dilated" stdDeviation="8" result="blur3" />
|
||||||
|
<feMerge>
|
||||||
|
<feMergeNode in="blur3" />
|
||||||
|
<feMergeNode in="blur2" />
|
||||||
|
<feMergeNode in="blur1" />
|
||||||
|
<feMergeNode in="SourceGraphic" />
|
||||||
|
</feMerge>
|
||||||
|
</filter>
|
||||||
|
|
||||||
|
<!-- 编码器主体渐变 -->
|
||||||
|
<radialGradient id="encoderGradient" cx="50%" cy="30%">
|
||||||
|
<stop offset="0%" stop-color="#666666" />
|
||||||
|
<stop offset="70%" stop-color="#333333" />
|
||||||
|
<stop offset="100%" stop-color="#1a1a1a" />
|
||||||
|
</radialGradient>
|
||||||
|
|
||||||
|
<!-- 旋钮渐变 -->
|
||||||
|
<radialGradient id="knobGradient" cx="30%" cy="30%">
|
||||||
|
<stop offset="0%" stop-color="#555555" />
|
||||||
|
<stop offset="70%" stop-color="#222222" />
|
||||||
|
<stop offset="100%" stop-color="#111111" />
|
||||||
|
</radialGradient>
|
||||||
|
|
||||||
|
<!-- 按下状态渐变 -->
|
||||||
|
<radialGradient id="knobPressedGradient" cx="50%" cy="50%">
|
||||||
|
<stop offset="0%" stop-color="#333333" />
|
||||||
|
<stop offset="70%" stop-color="#555555" />
|
||||||
|
<stop offset="100%" stop-color="#888888" />
|
||||||
|
</radialGradient>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<!-- 编码器底座 -->
|
||||||
|
<rect x="10" y="30" width="80" height="60" rx="8" ry="8"
|
||||||
|
fill="#2a2a2a" stroke="#444444" stroke-width="1"/>
|
||||||
|
|
||||||
|
<!-- 编码器主体外壳 -->
|
||||||
|
<circle cx="50" cy="60" r="32" fill="url(#encoderGradient)" stroke="#555555" stroke-width="1"/>
|
||||||
|
|
||||||
|
<!-- 编码器接线端子 -->
|
||||||
|
<rect x="5" y="75" width="4" height="8" fill="#c9c9c9" rx="1"/>
|
||||||
|
<rect x="15" y="85" width="4" height="8" fill="#c9c9c9" rx="1"/>
|
||||||
|
<rect x="25" y="85" width="4" height="8" fill="#c9c9c9" rx="1"/>
|
||||||
|
<rect x="81" y="85" width="4" height="8" fill="#c9c9c9" rx="1"/>
|
||||||
|
<rect x="91" y="75" width="4" height="8" fill="#c9c9c9" rx="1"/>
|
||||||
|
|
||||||
|
<!-- 旋钮 -->
|
||||||
|
<circle cx="50" cy="60" r="22"
|
||||||
|
:fill="isPressed ? 'url(#knobPressedGradient)' : 'url(#knobGradient)'"
|
||||||
|
stroke="#666666" stroke-width="1"
|
||||||
|
:transform="`rotate(${rotation/2} 50 60)`"
|
||||||
|
class="interactive"
|
||||||
|
@mousedown="handleMouseDown"
|
||||||
|
@mouseup="handlePress(false)"
|
||||||
|
@mouseleave="handlePress(false)"/>
|
||||||
|
|
||||||
|
<!-- 旋钮指示器 -->
|
||||||
|
<line x1="50" y1="42" x2="50" y2="48"
|
||||||
|
stroke="#ffffff" stroke-width="2" stroke-linecap="round"
|
||||||
|
:transform="`rotate(${rotation} 50 60)`"/>
|
||||||
|
|
||||||
|
<!-- 旋钮上的纹理刻度 -->
|
||||||
|
<g :transform="`rotate(${rotation} 50 60)`">
|
||||||
|
<circle cx="50" cy="60" r="18" fill="none" stroke="#777777" stroke-width="0.5"/>
|
||||||
|
<!-- 刻度线 -->
|
||||||
|
<g v-for="i in 16" :key="i">
|
||||||
|
<line :x1="50 + 16 * Math.cos((i-1) * Math.PI / 8)"
|
||||||
|
:y1="60 + 16 * Math.sin((i-1) * Math.PI / 8)"
|
||||||
|
:x2="50 + 18 * Math.cos((i-1) * Math.PI / 8)"
|
||||||
|
:y2="60 + 18 * Math.sin((i-1) * Math.PI / 8)"
|
||||||
|
stroke="#999999" stroke-width="0.5"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- 编码器编号标签 -->
|
||||||
|
<text x="50" y="15" text-anchor="middle" font-family="Arial" font-size="10"
|
||||||
|
fill="#cccccc" font-weight="bold">
|
||||||
|
EC11-{{ encoderNumber }}
|
||||||
|
</text>
|
||||||
|
|
||||||
|
<!-- 状态指示器 -->
|
||||||
|
<circle cx="85" cy="20" r="3" :fill="isPressed ? '#ff4444' : '#444444'"
|
||||||
|
:filter="isPressed ? 'url(#glow)' : ''"
|
||||||
|
stroke="#666666" stroke-width="0.5"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref, computed } from 'vue';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
size?: number;
|
||||||
|
encoderNumber?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
size: 1,
|
||||||
|
encoderNumber: 1
|
||||||
|
});
|
||||||
|
|
||||||
|
// 组件状态
|
||||||
|
const isPressed = ref(false);
|
||||||
|
const rotation = ref(0);
|
||||||
|
|
||||||
|
// 拖动状态
|
||||||
|
const isDragging = ref(false);
|
||||||
|
const dragStartX = ref(0);
|
||||||
|
const lastTriggerX = ref(0);
|
||||||
|
const dragThreshold = 20; // 每20像素触发一次旋转
|
||||||
|
const hasRotated = ref(false); // 标记是否已经发生了旋转
|
||||||
|
|
||||||
|
// 计算宽高
|
||||||
|
const width = computed(() => 100 * props.size);
|
||||||
|
const height = computed(() => 100 * props.size);
|
||||||
|
|
||||||
|
// 定义发出的事件
|
||||||
|
const emit = defineEmits(['press', 'release', 'rotate-left', 'rotate-right']);
|
||||||
|
|
||||||
|
// 鼠标按下处理
|
||||||
|
function handleMouseDown(event: MouseEvent) {
|
||||||
|
isDragging.value = true;
|
||||||
|
dragStartX.value = event.clientX;
|
||||||
|
lastTriggerX.value = event.clientX;
|
||||||
|
hasRotated.value = false; // 重置旋转标记
|
||||||
|
|
||||||
|
// 添加全局鼠标事件监听
|
||||||
|
document.addEventListener('mousemove', handleMouseMove);
|
||||||
|
document.addEventListener('mouseup', handleMouseUp);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 鼠标移动处理
|
||||||
|
function handleMouseMove(event: MouseEvent) {
|
||||||
|
if (!isDragging.value) return;
|
||||||
|
|
||||||
|
const currentX = event.clientX;
|
||||||
|
const deltaX = currentX - lastTriggerX.value;
|
||||||
|
|
||||||
|
// 检查是否达到触发阈值
|
||||||
|
if (Math.abs(deltaX) >= dragThreshold) {
|
||||||
|
hasRotated.value = true; // 标记已经发生旋转
|
||||||
|
|
||||||
|
if (deltaX > 0) {
|
||||||
|
// 右拖动 - 右旋转
|
||||||
|
rotation.value += 15;
|
||||||
|
emit('rotate-right', {
|
||||||
|
encoderNumber: props.encoderNumber
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// 左拖动 - 左旋转
|
||||||
|
rotation.value -= 15;
|
||||||
|
emit('rotate-left', {
|
||||||
|
encoderNumber: props.encoderNumber
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新最后触发位置
|
||||||
|
lastTriggerX.value = currentX;
|
||||||
|
|
||||||
|
// 保持角度在0-360度范围内
|
||||||
|
rotation.value = rotation.value % 720;
|
||||||
|
if (rotation.value < 0) {
|
||||||
|
rotation.value += 720;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 鼠标松开处理
|
||||||
|
function handleMouseUp() {
|
||||||
|
isDragging.value = false;
|
||||||
|
|
||||||
|
// 只有在没有发生旋转的情况下才识别为按压事件
|
||||||
|
if (!hasRotated.value) {
|
||||||
|
// 触发按压和释放事件(模拟快速按压)
|
||||||
|
handlePress(true);
|
||||||
|
// 使用setTimeout来模拟按压和释放的时序
|
||||||
|
setTimeout(() => {
|
||||||
|
handlePress(false);
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移除全局事件监听
|
||||||
|
document.removeEventListener('mousemove', handleMouseMove);
|
||||||
|
document.removeEventListener('mouseup', handleMouseUp);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按压处理
|
||||||
|
function handlePress(pressed: boolean) {
|
||||||
|
if (pressed !== isPressed.value) {
|
||||||
|
isPressed.value = pressed;
|
||||||
|
if (pressed) {
|
||||||
|
emit('press', { encoderNumber: props.encoderNumber });
|
||||||
|
} else {
|
||||||
|
emit('release', { encoderNumber: props.encoderNumber });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 暴露组件方法
|
||||||
|
defineExpose({
|
||||||
|
press: () => handlePress(true),
|
||||||
|
release: () => handlePress(false),
|
||||||
|
rotateLeft: () => {
|
||||||
|
rotation.value -= 15;
|
||||||
|
emit('rotate-left', {
|
||||||
|
encoderNumber: props.encoderNumber
|
||||||
|
});
|
||||||
|
},
|
||||||
|
rotateRight: () => {
|
||||||
|
rotation.value += 15;
|
||||||
|
emit('rotate-right', {
|
||||||
|
encoderNumber: props.encoderNumber
|
||||||
|
});
|
||||||
|
},
|
||||||
|
isPressed: () => isPressed.value
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
// 添加一个静态方法来获取默认props
|
||||||
|
export function getDefaultProps() {
|
||||||
|
return {
|
||||||
|
size: 1,
|
||||||
|
encoderNumber: 1
|
||||||
|
};
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="postcss">
|
||||||
|
.ec11-container {
|
||||||
|
display: inline-block;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ec11-encoder {
|
||||||
|
display: block;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.interactive {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
Loading…
Reference in New Issue