/* * 自然写互动课堂应用开发SDK软件 V1.0 * StrokePath - 笔迹路径数据模型 * * 描述:封装一条完整笔画的坐标序列、属性和元数据 */ package com.writech.sdk.model; import java.io.Serializable; import java.util.ArrayList; import java.util.List; /** * 笔迹路径模型 * 代表从落笔到抬笔的一条完整笔画数据 */ public class StrokePath implements Serializable { private static final long serialVersionUID = 1L; /* ========== 采样点内部类 ========== */ /** 单个笔迹采样点 */ public static class Point implements Serializable { private static final long serialVersionUID = 1L; /** X坐标(屏幕像素或物理mm,取决于坐标空间) */ public float x; /** Y坐标 */ public float y; /** 压力值(归一化 0.0~1.0) */ public float pressure; /** 时间戳(相对于笔画开始时间的毫秒偏移) */ public long timeOffset; /** 笔尖倾斜角度(度,0-90,0为垂直,部分笔支持) */ public float tiltAngle; /** 笔尖方位角(度,0-360,部分笔支持) */ public float azimuthAngle; public Point() {} public Point(float x, float y, float pressure, long timeOffset) { this.x = x; this.y = y; this.pressure = pressure; this.timeOffset = timeOffset; } @Override public String toString() { return "(" + x + "," + y + ",p=" + pressure + ",t=" + timeOffset + ")"; } } /* ========== 笔画属性 ========== */ /** 笔画唯一ID */ private String strokeId; /** 来源笔设备MAC地址 */ private String penMac; /** 学生ID */ private String studentId; /** 页面ID(标识书写所在页面) */ private String pageId; /** 笔画开始时间(绝对时间戳毫秒) */ private long startTimestamp; /** 笔画结束时间 */ private long endTimestamp; /** 笔画颜色(ARGB) */ private int color = 0xFF000000; /** 笔画基础线宽(像素) */ private float baseWidth = 3.0f; /** 采样点列表 */ private List points; /* ========== 分析结果(由OCR/AI引擎填充) ========== */ /** 识别的文字内容 */ private String recognizedText; /** 识别置信度 */ private float recognitionConfidence; /** 笔顺序号(在整个书写序列中的顺序) */ private int strokeOrder; /** 是否为有效笔画(排除误触等) */ private boolean isValid = true; /* ========== 构造函数 ========== */ public StrokePath() { this.points = new ArrayList<>(); } public StrokePath(String strokeId, String penMac) { this.strokeId = strokeId; this.penMac = penMac; this.points = new ArrayList<>(); this.startTimestamp = System.currentTimeMillis(); } /* ========== 点操作方法 ========== */ /** 添加采样点 */ public void addPoint(float x, float y, float pressure, long timeOffset) { points.add(new Point(x, y, pressure, timeOffset)); } /** 添加采样点(含倾斜角) */ public void addPointWithTilt(float x, float y, float pressure, long timeOffset, float tilt, float azimuth) { Point p = new Point(x, y, pressure, timeOffset); p.tiltAngle = tilt; p.azimuthAngle = azimuth; points.add(p); } /** 获取采样点数量 */ public int getPointCount() { return points.size(); } /** 获取指定索引的采样点 */ public Point getPoint(int index) { if (index >= 0 && index < points.size()) { return points.get(index); } return null; } /** 获取所有采样点 */ public List getPoints() { return points; } /* ========== 笔画几何计算 ========== */ /** 计算笔画总长度(像素) */ public float calculateLength() { float length = 0; for (int i = 1; i < points.size(); i++) { Point p0 = points.get(i - 1); Point p1 = points.get(i); float dx = p1.x - p0.x; float dy = p1.y - p0.y; length += (float) Math.sqrt(dx * dx + dy * dy); } return length; } /** 计算笔画包围盒 */ public float[] getBoundingBox() { if (points.isEmpty()) return new float[]{0, 0, 0, 0}; float minX = Float.MAX_VALUE, minY = Float.MAX_VALUE; float maxX = Float.MIN_VALUE, maxY = Float.MIN_VALUE; for (Point p : points) { if (p.x < minX) minX = p.x; if (p.y < minY) minY = p.y; if (p.x > maxX) maxX = p.x; if (p.y > maxY) maxY = p.y; } return new float[]{minX, minY, maxX, maxY}; } /** 计算平均书写速度(像素/毫秒) */ public float calculateAverageSpeed() { if (points.size() < 2) return 0; float totalLength = calculateLength(); long duration = points.get(points.size() - 1).timeOffset - points.get(0).timeOffset; return duration > 0 ? totalLength / duration : 0; } /** 计算平均压力 */ public float calculateAveragePressure() { if (points.isEmpty()) return 0; float sum = 0; for (Point p : points) { sum += p.pressure; } return sum / points.size(); } /** 获取书写持续时间(毫秒) */ public long getDuration() { if (points.size() < 2) return 0; return points.get(points.size() - 1).timeOffset - points.get(0).timeOffset; } /* ========== 序列化方法 ========== */ /** * 将笔画数据序列化为紧凑的二进制格式 * 用于BLE传输和本地缓存 * * 格式: * [4字节 点数][每个点: 4字节x + 4字节y + 2字节pressure + 4字节timeOffset] */ public byte[] toBytes() { int pointCount = points.size(); byte[] data = new byte[4 + pointCount * 14]; /* 写入点数(大端序) */ data[0] = (byte) ((pointCount >> 24) & 0xFF); data[1] = (byte) ((pointCount >> 16) & 0xFF); data[2] = (byte) ((pointCount >> 8) & 0xFF); data[3] = (byte) (pointCount & 0xFF); int offset = 4; for (Point p : points) { /* 写入X坐标(float → 4字节) */ int fx = Float.floatToIntBits(p.x); data[offset++] = (byte) ((fx >> 24) & 0xFF); data[offset++] = (byte) ((fx >> 16) & 0xFF); data[offset++] = (byte) ((fx >> 8) & 0xFF); data[offset++] = (byte) (fx & 0xFF); /* 写入Y坐标 */ int fy = Float.floatToIntBits(p.y); data[offset++] = (byte) ((fy >> 24) & 0xFF); data[offset++] = (byte) ((fy >> 16) & 0xFF); data[offset++] = (byte) ((fy >> 8) & 0xFF); data[offset++] = (byte) (fy & 0xFF); /* 写入压力值(归一化后*65535转uint16) */ int pressure16 = (int) (p.pressure * 65535); data[offset++] = (byte) ((pressure16 >> 8) & 0xFF); data[offset++] = (byte) (pressure16 & 0xFF); /* 写入时间偏移(uint32) */ long t = p.timeOffset; data[offset++] = (byte) ((t >> 24) & 0xFF); data[offset++] = (byte) ((t >> 16) & 0xFF); data[offset++] = (byte) ((t >> 8) & 0xFF); data[offset++] = (byte) (t & 0xFF); } return data; } /* ========== Getter / Setter ========== */ public String getStrokeId() { return strokeId; } public void setStrokeId(String strokeId) { this.strokeId = strokeId; } public String getPenMac() { return penMac; } public void setPenMac(String penMac) { this.penMac = penMac; } public String getStudentId() { return studentId; } public void setStudentId(String studentId) { this.studentId = studentId; } public String getPageId() { return pageId; } public void setPageId(String pageId) { this.pageId = pageId; } public long getStartTimestamp() { return startTimestamp; } public void setStartTimestamp(long t) { this.startTimestamp = t; } public long getEndTimestamp() { return endTimestamp; } public void setEndTimestamp(long t) { this.endTimestamp = t; } public int getColor() { return color; } public void setColor(int color) { this.color = color; } public float getBaseWidth() { return baseWidth; } public void setBaseWidth(float w) { this.baseWidth = w; } public String getRecognizedText() { return recognizedText; } public void setRecognizedText(String text) { this.recognizedText = text; } public float getRecognitionConfidence() { return recognitionConfidence; } public void setRecognitionConfidence(float c) { this.recognitionConfidence = c; } public int getStrokeOrder() { return strokeOrder; } public void setStrokeOrder(int order) { this.strokeOrder = order; } public boolean isValid() { return isValid; } public void setValid(boolean valid) { isValid = valid; } @Override public String toString() { return "StrokePath{id='" + strokeId + "', points=" + points.size() + ", duration=" + getDuration() + "ms" + ", text='" + recognizedText + "'}"; } }