Files
2026-03-22 15:24:40 +08:00

305 lines
9.3 KiB
Java
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 自然写互动课堂应用开发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<Point> 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<Point> 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 + "'}";
}
}