pose-ai-core 0.1.0

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 (61) hide show
  1. package/README.md +348 -0
  2. package/dist/_virtual/_commonjsHelpers.js +5 -0
  3. package/dist/_virtual/_commonjsHelpers.js.map +1 -0
  4. package/dist/_virtual/camera_utils.js +6 -0
  5. package/dist/_virtual/camera_utils.js.map +1 -0
  6. package/dist/_virtual/camera_utils2.js +5 -0
  7. package/dist/_virtual/camera_utils2.js.map +1 -0
  8. package/dist/_virtual/drawing_utils.js +6 -0
  9. package/dist/_virtual/drawing_utils.js.map +1 -0
  10. package/dist/_virtual/drawing_utils2.js +5 -0
  11. package/dist/_virtual/drawing_utils2.js.map +1 -0
  12. package/dist/_virtual/pose.js +6 -0
  13. package/dist/_virtual/pose.js.map +1 -0
  14. package/dist/_virtual/pose2.js +5 -0
  15. package/dist/_virtual/pose2.js.map +1 -0
  16. package/dist/components/AnglePanel/index.css +1 -0
  17. package/dist/components/AnglePanel/index.js +79 -0
  18. package/dist/components/AnglePanel/index.js.map +1 -0
  19. package/dist/components/PoseCanvas/index.js +86 -0
  20. package/dist/components/PoseCanvas/index.js.map +1 -0
  21. package/dist/components/ROMVisualizer/index.css +1 -0
  22. package/dist/components/ROMVisualizer/index.js +208 -0
  23. package/dist/components/ROMVisualizer/index.js.map +1 -0
  24. package/dist/components/SkeletonOverlay/index.css +1 -0
  25. package/dist/components/SkeletonOverlay/index.js +45 -0
  26. package/dist/components/SkeletonOverlay/index.js.map +1 -0
  27. package/dist/components/styles.js +270 -0
  28. package/dist/components/styles.js.map +1 -0
  29. package/dist/components/utils/drawingUtils.js +45 -0
  30. package/dist/components/utils/drawingUtils.js.map +1 -0
  31. package/dist/engine/angleCalculator.js +196 -0
  32. package/dist/engine/angleCalculator.js.map +1 -0
  33. package/dist/engine/directionAnalyzer.js +97 -0
  34. package/dist/engine/directionAnalyzer.js.map +1 -0
  35. package/dist/engine/eventBus.js +45 -0
  36. package/dist/engine/eventBus.js.map +1 -0
  37. package/dist/engine/guidedAssessmentManager.js +252 -0
  38. package/dist/engine/guidedAssessmentManager.js.map +1 -0
  39. package/dist/engine/motionDetector.js +127 -0
  40. package/dist/engine/motionDetector.js.map +1 -0
  41. package/dist/engine/poseEngine.js +131 -0
  42. package/dist/engine/poseEngine.js.map +1 -0
  43. package/dist/engine.d.ts +153 -0
  44. package/dist/engine.js +10 -0
  45. package/dist/engine.js.map +1 -0
  46. package/dist/hooks/usePoseDetection.js +278 -0
  47. package/dist/hooks/usePoseDetection.js.map +1 -0
  48. package/dist/hooks/usePoseEngine.js +83 -0
  49. package/dist/hooks/usePoseEngine.js.map +1 -0
  50. package/dist/index.d.ts +596 -0
  51. package/dist/index.js +29 -0
  52. package/dist/index.js.map +1 -0
  53. package/dist/node_modules/.pnpm/@mediapipe_camera_utils@0.3.1675466862/node_modules/@mediapipe/camera_utils/camera_utils.js +377 -0
  54. package/dist/node_modules/.pnpm/@mediapipe_camera_utils@0.3.1675466862/node_modules/@mediapipe/camera_utils/camera_utils.js.map +1 -0
  55. package/dist/node_modules/.pnpm/@mediapipe_drawing_utils@0.3.1675466124/node_modules/@mediapipe/drawing_utils/drawing_utils.js +112 -0
  56. package/dist/node_modules/.pnpm/@mediapipe_drawing_utils@0.3.1675466124/node_modules/@mediapipe/drawing_utils/drawing_utils.js.map +1 -0
  57. package/dist/node_modules/.pnpm/@mediapipe_pose@0.5.1675469404/node_modules/@mediapipe/pose/pose.js +1796 -0
  58. package/dist/node_modules/.pnpm/@mediapipe_pose@0.5.1675469404/node_modules/@mediapipe/pose/pose.js.map +1 -0
  59. package/dist/types/index.js +5 -0
  60. package/dist/types/index.js.map +1 -0
  61. package/package.json +47 -0
package/README.md ADDED
@@ -0,0 +1,348 @@
1
+ # @pose-ai/core
2
+
3
+ Pose AI 核心引擎包 - 基于 MediaPipe Pose 的姿态检测和 ROM 评估系统。
4
+
5
+ ## 📦 安装
6
+
7
+ ```bash
8
+ pnpm add @pose-ai/core
9
+ ```
10
+
11
+ ## 🚀 快速开始
12
+
13
+ ### 使用 ROMVisualizer 组件(推荐)
14
+
15
+ 最简单的方式是使用 `ROMVisualizer` 组件,它包含了所有功能:
16
+
17
+ ```tsx
18
+ import { ROMVisualizer } from '@pose-ai/core'
19
+
20
+ function App() {
21
+ return (
22
+ <ROMVisualizer
23
+ width={640}
24
+ height={480}
25
+ showAnglePanel={true}
26
+ showROM={true}
27
+ />
28
+ )
29
+ }
30
+ ```
31
+
32
+ ### 使用 Hook 自定义 UI
33
+
34
+ 如果需要自定义 UI,可以使用 `usePoseEngine` Hook:
35
+
36
+ ```tsx
37
+ import { usePoseEngine } from '@pose-ai/core'
38
+ import { useRef } from 'react'
39
+
40
+ function CustomPoseDetector() {
41
+ const videoRef = useRef<HTMLVideoElement>(null)
42
+ const { start, stop, isRunning, angles, romData, error } = usePoseEngine({
43
+ config: {
44
+ modelComplexity: 1,
45
+ minDetectionConfidence: 0.5,
46
+ },
47
+ onAngleUpdate: (angles) => {
48
+ console.log('角度更新:', angles)
49
+ },
50
+ })
51
+
52
+ const handleStart = async () => {
53
+ if (videoRef.current) {
54
+ await start(videoRef.current)
55
+ }
56
+ }
57
+
58
+ return (
59
+ <div>
60
+ <video ref={videoRef} autoPlay playsInline muted />
61
+ <button onClick={handleStart}>开始</button>
62
+ <button onClick={stop}>停止</button>
63
+
64
+ {angles.map((angle) => (
65
+ <div key={angle.name}>
66
+ {angle.name}: {angle.angle.toFixed(1)}°
67
+ </div>
68
+ ))}
69
+ </div>
70
+ )
71
+ }
72
+ ```
73
+
74
+ ### 使用底层引擎 API
75
+
76
+ 对于更高级的用例,可以直接使用 `PoseEngine`:
77
+
78
+ ```tsx
79
+ import { PoseEngine } from '@pose-ai/core'
80
+
81
+ const engine = new PoseEngine(
82
+ {
83
+ modelComplexity: 1,
84
+ minDetectionConfidence: 0.5,
85
+ },
86
+ {
87
+ onAngleUpdate: (angles) => {
88
+ console.log('角度:', angles)
89
+ },
90
+ onROMUpdate: (romData) => {
91
+ console.log('ROM 数据:', romData)
92
+ },
93
+ }
94
+ )
95
+
96
+ await engine.initialize()
97
+ await engine.start(videoElement)
98
+
99
+ // 停止检测
100
+ engine.stop()
101
+
102
+ // 清理资源
103
+ engine.dispose()
104
+ ```
105
+
106
+ ## 📖 API 文档
107
+
108
+ ### ROMVisualizer 组件
109
+
110
+ 主可视化组件,包含视频预览、骨骼覆盖层和角度面板。
111
+
112
+ **Props:**
113
+
114
+ | 属性 | 类型 | 默认值 | 描述 |
115
+ |------|------|--------|------|
116
+ | `engineOptions` | `UsePoseEngineOptions` | `{}` | 引擎配置选项 |
117
+ | `width` | `number` | `640` | 视频宽度 |
118
+ | `height` | `number` | `480` | 视频高度 |
119
+ | `showAnglePanel` | `boolean` | `true` | 是否显示角度面板 |
120
+ | `showROM` | `boolean` | `true` | 是否显示 ROM 数据 |
121
+ | `drawingOptions` | `DrawingOptions` | `{}` | 绘制选项 |
122
+ | `className` | `string` | `''` | 自定义类名 |
123
+ | `onStart` | `() => void` | - | 开始检测回调 |
124
+ | `onStop` | `() => void` | - | 停止检测回调 |
125
+
126
+ ### AnglePanel 组件
127
+
128
+ 角度数据展示面板。
129
+
130
+ **Props:**
131
+
132
+ | 属性 | 类型 | 默认值 | 描述 |
133
+ |------|------|--------|------|
134
+ | `angles` | `JointAngle[]` | `[]` | 角度数据数组 |
135
+ | `romData` | `ROMData[]` | `[]` | ROM 数据数组 |
136
+ | `showROM` | `boolean` | `false` | 是否显示 ROM 信息 |
137
+ | `className` | `string` | `''` | 自定义类名 |
138
+
139
+ ### SkeletonOverlay 组件
140
+
141
+ 骨骼覆盖层,在视频上绘制姿态。
142
+
143
+ **Props:**
144
+
145
+ | 属性 | 类型 | 默认值 | 描述 |
146
+ |------|------|--------|------|
147
+ | `results` | `Results \| null` | `null` | MediaPipe 检测结果 |
148
+ | `angles` | `JointAngle[]` | `[]` | 角度数据 |
149
+ | `width` | `number` | `640` | Canvas 宽度 |
150
+ | `height` | `number` | `480` | Canvas 高度 |
151
+ | `drawingOptions` | `DrawingOptions` | `{}` | 绘制选项 |
152
+ | `className` | `string` | `''` | 自定义类名 |
153
+
154
+ ### usePoseEngine Hook
155
+
156
+ React Hook,用于管理姿态检测引擎。
157
+
158
+ **参数:**
159
+
160
+ ```typescript
161
+ interface UsePoseEngineOptions {
162
+ config?: PoseEngineConfig
163
+ autoStart?: boolean
164
+ modelPath?: string
165
+ onResults?: (results: Results) => void
166
+ onAngleUpdate?: (angles: JointAngle[]) => void
167
+ onROMUpdate?: (romData: ROMData[]) => void
168
+ onError?: (error: Error) => void
169
+ }
170
+ ```
171
+
172
+ **返回值:**
173
+
174
+ ```typescript
175
+ interface UsePoseEngineReturn {
176
+ start: (videoElement: HTMLVideoElement) => Promise<void>
177
+ stop: () => void
178
+ processImage: (image: HTMLImageElement | HTMLCanvasElement) => Promise<void>
179
+ resetROM: (jointName?: string) => void
180
+ isRunning: boolean
181
+ angles: JointAngle[]
182
+ romData: ROMData[]
183
+ error: Error | null
184
+ engine: PoseEngine | null
185
+ }
186
+ ```
187
+
188
+ ### PoseEngine 类
189
+
190
+ 底层姿态检测引擎。
191
+
192
+ **方法:**
193
+
194
+ - `initialize(modelPath?: string): Promise<void>` - 初始化引擎
195
+ - `start(videoElement: HTMLVideoElement): Promise<void>` - 启动摄像头检测
196
+ - `stop(): void` - 停止检测
197
+ - `processImage(image: HTMLImageElement | HTMLCanvasElement): Promise<void>` - 处理单张图片
198
+ - `setJointDefinitions(definitions: JointDefinition[]): void` - 设置关节定义
199
+ - `resetROM(jointName?: string): void` - 重置 ROM 数据
200
+ - `updateConfig(config: Partial<PoseEngineConfig>): void` - 更新配置
201
+ - `dispose(): void` - 清理资源
202
+
203
+ ### 配置选项
204
+
205
+ ```typescript
206
+ interface PoseEngineConfig {
207
+ modelComplexity?: 0 | 1 | 2 // 模型复杂度
208
+ smoothLandmarks?: boolean // 平滑关键点
209
+ enableSegmentation?: boolean // 启用分割
210
+ smoothSegmentation?: boolean // 平滑分割
211
+ minDetectionConfidence?: number // 最小检测置信度
212
+ minTrackingConfidence?: number // 最小追踪置信度
213
+ }
214
+ ```
215
+
216
+ ### 绘制选项
217
+
218
+ ```typescript
219
+ interface DrawingOptions {
220
+ showLandmarks?: boolean // 显示关键点
221
+ showConnections?: boolean // 显示连接线
222
+ showAngles?: boolean // 显示角度
223
+ landmarkColor?: string // 关键点颜色
224
+ connectionColor?: string // 连接线颜色
225
+ angleColor?: string // 角度颜色
226
+ landmarkRadius?: number // 关键点半径
227
+ lineWidth?: number // 线宽
228
+ }
229
+ ```
230
+
231
+ ## 🎯 预定义关节
232
+
233
+ 系统预定义了 8 个常用关节:
234
+
235
+ - `left_elbow` - 左肘关节
236
+ - `right_elbow` - 右肘关节
237
+ - `left_shoulder` - 左肩关节
238
+ - `right_shoulder` - 右肩关节
239
+ - `left_hip` - 左髋关节
240
+ - `right_hip` - 右髋关节
241
+ - `left_knee` - 左膝关节
242
+ - `right_knee` - 右膝关节
243
+
244
+ ### 自定义关节
245
+
246
+ ```typescript
247
+ import { PoseLandmarkIndex } from '@pose-ai/core'
248
+
249
+ const customJoints = [
250
+ {
251
+ name: 'custom_joint',
252
+ points: [
253
+ PoseLandmarkIndex.LEFT_SHOULDER,
254
+ PoseLandmarkIndex.LEFT_ELBOW,
255
+ PoseLandmarkIndex.LEFT_WRIST,
256
+ ],
257
+ description: '自定义关节',
258
+ },
259
+ ]
260
+
261
+ engine.setJointDefinitions(customJoints)
262
+ ```
263
+
264
+ ## 🎨 样式自定义
265
+
266
+ 所有组件都支持通过 `className` 属性添加自定义样式:
267
+
268
+ ```tsx
269
+ <ROMVisualizer
270
+ className="my-custom-visualizer"
271
+ drawingOptions={{
272
+ landmarkColor: '#FF0000',
273
+ connectionColor: '#00FF00',
274
+ angleColor: '#0000FF',
275
+ lineWidth: 3,
276
+ }}
277
+ />
278
+ ```
279
+
280
+ ## 📊 数据类型
281
+
282
+ ### JointAngle
283
+
284
+ ```typescript
285
+ interface JointAngle {
286
+ name: string // 关节名称
287
+ angle: number // 角度值(度)
288
+ points: [number, number, number] // 三个关键点索引
289
+ timestamp: number // 时间戳
290
+ }
291
+ ```
292
+
293
+ ### ROMData
294
+
295
+ ```typescript
296
+ interface ROMData {
297
+ jointName: string // 关节名称
298
+ currentAngle: number // 当前角度
299
+ minAngle: number // 最小角度
300
+ maxAngle: number // 最大角度
301
+ range: number // 运动范围
302
+ timestamp: number // 时间戳
303
+ }
304
+ ```
305
+
306
+ ## 🔧 高级用法
307
+
308
+ ### 事件监听
309
+
310
+ ```typescript
311
+ const eventBus = engine.getEventBus()
312
+
313
+ // 监听角度更新
314
+ const unsubscribe = eventBus.on('angle-updated', (event) => {
315
+ console.log('角度更新:', event.data)
316
+ })
317
+
318
+ // 取消监听
319
+ unsubscribe()
320
+ ```
321
+
322
+ ### 图片处理
323
+
324
+ ```typescript
325
+ const image = document.getElementById('myImage') as HTMLImageElement
326
+ await engine.processImage(image)
327
+ ```
328
+
329
+ ### 重置 ROM 数据
330
+
331
+ ```typescript
332
+ // 重置所有关节的 ROM 数据
333
+ engine.resetROM()
334
+
335
+ // 重置特定关节
336
+ engine.resetROM('left_elbow')
337
+ ```
338
+
339
+ ## 📝 注意事项
340
+
341
+ 1. **摄像头权限**: 使用前需要用户授权摄像头权限
342
+ 2. **HTTPS**: 在生产环境中必须使用 HTTPS
343
+ 3. **性能**: `modelComplexity` 越高,精度越高但性能越低
344
+ 4. **资源清理**: 组件卸载时会自动清理资源
345
+
346
+ ## 📄 License
347
+
348
+ MIT
@@ -0,0 +1,5 @@
1
+ var e = typeof globalThis < "u" ? globalThis : typeof window < "u" ? window : typeof global < "u" ? global : typeof self < "u" ? self : {};
2
+ export {
3
+ e as commonjsGlobal
4
+ };
5
+ //# sourceMappingURL=_commonjsHelpers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"_commonjsHelpers.js","sources":[],"sourcesContent":[],"names":[],"mappings":";"}
@@ -0,0 +1,6 @@
1
+ import { __require as r } from "../node_modules/.pnpm/@mediapipe_camera_utils@0.3.1675466862/node_modules/@mediapipe/camera_utils/camera_utils.js";
2
+ var e = /* @__PURE__ */ r();
3
+ export {
4
+ e as c
5
+ };
6
+ //# sourceMappingURL=camera_utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"camera_utils.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;"}
@@ -0,0 +1,5 @@
1
+ var a = {};
2
+ export {
3
+ a as __exports
4
+ };
5
+ //# sourceMappingURL=camera_utils2.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"camera_utils2.js","sources":[],"sourcesContent":[],"names":[],"mappings":";"}
@@ -0,0 +1,6 @@
1
+ import { __require as r } from "../node_modules/.pnpm/@mediapipe_drawing_utils@0.3.1675466124/node_modules/@mediapipe/drawing_utils/drawing_utils.js";
2
+ var a = /* @__PURE__ */ r();
3
+ export {
4
+ a as d
5
+ };
6
+ //# sourceMappingURL=drawing_utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"drawing_utils.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;"}
@@ -0,0 +1,5 @@
1
+ var r = {};
2
+ export {
3
+ r as __exports
4
+ };
5
+ //# sourceMappingURL=drawing_utils2.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"drawing_utils2.js","sources":[],"sourcesContent":[],"names":[],"mappings":";"}
@@ -0,0 +1,6 @@
1
+ import { __require as r } from "../node_modules/.pnpm/@mediapipe_pose@0.5.1675469404/node_modules/@mediapipe/pose/pose.js";
2
+ var o = /* @__PURE__ */ r();
3
+ export {
4
+ o as p
5
+ };
6
+ //# sourceMappingURL=pose.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pose.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;"}
@@ -0,0 +1,5 @@
1
+ var e = {};
2
+ export {
3
+ e as __exports
4
+ };
5
+ //# sourceMappingURL=pose2.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pose2.js","sources":[],"sourcesContent":[],"names":[],"mappings":";"}
@@ -0,0 +1 @@
1
+ .angle-panel{background:#fff;border-radius:12px;box-shadow:0 4px 6px #0000001a;padding:1.5rem;min-width:300px;max-height:600px;overflow-y:auto}.angle-panel-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem;padding-bottom:.75rem;border-bottom:2px solid #e5e7eb}.angle-panel-header h3{margin:0;font-size:1.25rem;font-weight:600;color:#111827}.rom-badge{background:#3b82f6;color:#fff;padding:.25rem .75rem;border-radius:9999px;font-size:.75rem;font-weight:600}.angle-list{display:flex;flex-direction:column;gap:1rem}.no-data{text-align:center;color:#6b7280;padding:2rem;font-size:.875rem}.angle-item{background:#f9fafb;border-radius:8px;padding:1rem;transition:all .2s}.angle-item:hover{background:#f3f4f6;transform:translateY(-2px);box-shadow:0 2px 4px #0000000d}.angle-item-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:.5rem}.joint-name{font-weight:500;color:#374151;text-transform:capitalize}.angle-value{font-size:1.25rem;font-weight:700;font-family:Courier New,monospace}.rom-info{margin-top:.75rem;padding-top:.75rem;border-top:1px solid #e5e7eb}.rom-bar{position:relative;height:8px;background:#e5e7eb;border-radius:4px;margin-bottom:.5rem;overflow:hidden}.rom-range{position:absolute;height:100%;background:linear-gradient(90deg,#3b82f6,#10b981);opacity:.3;border-radius:4px}.rom-current{position:absolute;width:4px;height:100%;background:#ef4444;transform:translate(-50%)}.rom-values{display:flex;justify-content:space-between;font-size:.75rem;color:#6b7280}.rom-min,.rom-max,.rom-range-value{font-weight:500}@media(max-width:768px){.angle-panel{min-width:100%;max-height:400px}}
@@ -0,0 +1,79 @@
1
+ import { jsxs as n, jsx as r } from "react/jsx-runtime";
2
+ /* empty css */
3
+ const h = ({
4
+ angles: l,
5
+ romData: i = [],
6
+ showROM: s = !1,
7
+ className: m = ""
8
+ }) => {
9
+ const c = (e) => i.find((a) => a.jointName === e), d = (e) => e < 30 ? "#ef4444" : e < 90 ? "#f59e0b" : e < 150 ? "#10b981" : "#3b82f6";
10
+ return /* @__PURE__ */ n("div", { className: `angle-panel ${m}`, children: [
11
+ /* @__PURE__ */ n("div", { className: "angle-panel-header", children: [
12
+ /* @__PURE__ */ r("h3", { children: "关节角度" }),
13
+ s && /* @__PURE__ */ r("span", { className: "rom-badge", children: "ROM" })
14
+ ] }),
15
+ /* @__PURE__ */ r("div", { className: "angle-list", children: l.length === 0 ? /* @__PURE__ */ r("div", { className: "no-data", children: "未检测到姿态" }) : l.map((e) => {
16
+ const a = c(e.name);
17
+ return /* @__PURE__ */ n("div", { className: "angle-item", children: [
18
+ /* @__PURE__ */ n("div", { className: "angle-item-header", children: [
19
+ /* @__PURE__ */ r("span", { className: "joint-name", children: e.name }),
20
+ /* @__PURE__ */ n(
21
+ "span",
22
+ {
23
+ className: "angle-value",
24
+ style: { color: d(e.angle) },
25
+ children: [
26
+ e.angle.toFixed(1),
27
+ "°"
28
+ ]
29
+ }
30
+ )
31
+ ] }),
32
+ s && a && /* @__PURE__ */ n("div", { className: "rom-info", children: [
33
+ /* @__PURE__ */ n("div", { className: "rom-bar", children: [
34
+ /* @__PURE__ */ r(
35
+ "div",
36
+ {
37
+ className: "rom-range",
38
+ style: {
39
+ left: `${a.minAngle / 180 * 100}%`,
40
+ width: `${a.range / 180 * 100}%`
41
+ }
42
+ }
43
+ ),
44
+ /* @__PURE__ */ r(
45
+ "div",
46
+ {
47
+ className: "rom-current",
48
+ style: {
49
+ left: `${a.currentAngle / 180 * 100}%`
50
+ }
51
+ }
52
+ )
53
+ ] }),
54
+ /* @__PURE__ */ n("div", { className: "rom-values", children: [
55
+ /* @__PURE__ */ n("span", { className: "rom-min", children: [
56
+ "最小: ",
57
+ a.minAngle.toFixed(1),
58
+ "°"
59
+ ] }),
60
+ /* @__PURE__ */ n("span", { className: "rom-range-value", children: [
61
+ "范围: ",
62
+ a.range.toFixed(1),
63
+ "°"
64
+ ] }),
65
+ /* @__PURE__ */ n("span", { className: "rom-max", children: [
66
+ "最大: ",
67
+ a.maxAngle.toFixed(1),
68
+ "°"
69
+ ] })
70
+ ] })
71
+ ] })
72
+ ] }, e.name);
73
+ }) })
74
+ ] });
75
+ };
76
+ export {
77
+ h as AnglePanel
78
+ };
79
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sources":["../../../src/components/AnglePanel/index.tsx"],"sourcesContent":["import React from 'react'\nimport type { JointAngle, ROMData } from '../../types'\nimport './index.css'\n\nexport interface AnglePanelProps {\n angles: JointAngle[]\n romData?: ROMData[]\n showROM?: boolean\n className?: string\n}\n\nexport const AnglePanel: React.FC<AnglePanelProps> = ({\n angles,\n romData = [],\n showROM = false,\n className = '',\n}) => {\n const getROMForJoint = (jointName: string): ROMData | undefined => {\n return romData.find((rom) => rom.jointName === jointName)\n }\n\n const getAngleColor = (angle: number): string => {\n if (angle < 30) return '#ef4444'\n if (angle < 90) return '#f59e0b'\n if (angle < 150) return '#10b981'\n return '#3b82f6'\n }\n\n return (\n <div className={`angle-panel ${className}`}>\n <div className=\"angle-panel-header\">\n <h3>关节角度</h3>\n {showROM && <span className=\"rom-badge\">ROM</span>}\n </div>\n \n <div className=\"angle-list\">\n {angles.length === 0 ? (\n <div className=\"no-data\">未检测到姿态</div>\n ) : (\n angles.map((angleData) => {\n const rom = getROMForJoint(angleData.name)\n \n return (\n <div key={angleData.name} className=\"angle-item\">\n <div className=\"angle-item-header\">\n <span className=\"joint-name\">{angleData.name}</span>\n <span \n className=\"angle-value\"\n style={{ color: getAngleColor(angleData.angle) }}\n >\n {angleData.angle.toFixed(1)}°\n </span>\n </div>\n \n {showROM && rom && (\n <div className=\"rom-info\">\n <div className=\"rom-bar\">\n <div \n className=\"rom-range\"\n style={{\n left: `${(rom.minAngle / 180) * 100}%`,\n width: `${(rom.range / 180) * 100}%`,\n }}\n />\n <div \n className=\"rom-current\"\n style={{\n left: `${(rom.currentAngle / 180) * 100}%`,\n }}\n />\n </div>\n <div className=\"rom-values\">\n <span className=\"rom-min\">最小: {rom.minAngle.toFixed(1)}°</span>\n <span className=\"rom-range-value\">范围: {rom.range.toFixed(1)}°</span>\n <span className=\"rom-max\">最大: {rom.maxAngle.toFixed(1)}°</span>\n </div>\n </div>\n )}\n </div>\n )\n })\n )}\n </div>\n </div>\n )\n}\n"],"names":["AnglePanel","angles","romData","showROM","className","getROMForJoint","jointName","rom","getAngleColor","angle","jsxs","jsx","angleData"],"mappings":";;AAWO,MAAMA,IAAwC,CAAC;AAAA,EACpD,QAAAC;AAAA,EACA,SAAAC,IAAU,CAAA;AAAA,EACV,SAAAC,IAAU;AAAA,EACV,WAAAC,IAAY;AACd,MAAM;AACJ,QAAMC,IAAiB,CAACC,MACfJ,EAAQ,KAAK,CAACK,MAAQA,EAAI,cAAcD,CAAS,GAGpDE,IAAgB,CAACC,MACjBA,IAAQ,KAAW,YACnBA,IAAQ,KAAW,YACnBA,IAAQ,MAAY,YACjB;AAGT,SACE,gBAAAC,EAAC,OAAA,EAAI,WAAW,eAAeN,CAAS,IACtC,UAAA;AAAA,IAAA,gBAAAM,EAAC,OAAA,EAAI,WAAU,sBACb,UAAA;AAAA,MAAA,gBAAAC,EAAC,QAAG,UAAA,OAAA,CAAI;AAAA,MACPR,KAAW,gBAAAQ,EAAC,QAAA,EAAK,WAAU,aAAY,UAAA,MAAA,CAAG;AAAA,IAAA,GAC7C;AAAA,sBAEC,OAAA,EAAI,WAAU,cACZ,UAAAV,EAAO,WAAW,IACjB,gBAAAU,EAAC,OAAA,EAAI,WAAU,WAAU,UAAA,SAAA,CAAM,IAE/BV,EAAO,IAAI,CAACW,MAAc;AACxB,YAAML,IAAMF,EAAeO,EAAU,IAAI;AAEzC,aACE,gBAAAF,EAAC,OAAA,EAAyB,WAAU,cAClC,UAAA;AAAA,QAAA,gBAAAA,EAAC,OAAA,EAAI,WAAU,qBACb,UAAA;AAAA,UAAA,gBAAAC,EAAC,QAAA,EAAK,WAAU,cAAc,UAAAC,EAAU,MAAK;AAAA,UAC7C,gBAAAF;AAAA,YAAC;AAAA,YAAA;AAAA,cACC,WAAU;AAAA,cACV,OAAO,EAAE,OAAOF,EAAcI,EAAU,KAAK,EAAA;AAAA,cAE5C,UAAA;AAAA,gBAAAA,EAAU,MAAM,QAAQ,CAAC;AAAA,gBAAE;AAAA,cAAA;AAAA,YAAA;AAAA,UAAA;AAAA,QAC9B,GACF;AAAA,QAECT,KAAWI,KACV,gBAAAG,EAAC,OAAA,EAAI,WAAU,YACb,UAAA;AAAA,UAAA,gBAAAA,EAAC,OAAA,EAAI,WAAU,WACb,UAAA;AAAA,YAAA,gBAAAC;AAAA,cAAC;AAAA,cAAA;AAAA,gBACC,WAAU;AAAA,gBACV,OAAO;AAAA,kBACL,MAAM,GAAIJ,EAAI,WAAW,MAAO,GAAG;AAAA,kBACnC,OAAO,GAAIA,EAAI,QAAQ,MAAO,GAAG;AAAA,gBAAA;AAAA,cACnC;AAAA,YAAA;AAAA,YAEF,gBAAAI;AAAA,cAAC;AAAA,cAAA;AAAA,gBACC,WAAU;AAAA,gBACV,OAAO;AAAA,kBACL,MAAM,GAAIJ,EAAI,eAAe,MAAO,GAAG;AAAA,gBAAA;AAAA,cACzC;AAAA,YAAA;AAAA,UACF,GACF;AAAA,UACA,gBAAAG,EAAC,OAAA,EAAI,WAAU,cACb,UAAA;AAAA,YAAA,gBAAAA,EAAC,QAAA,EAAK,WAAU,WAAU,UAAA;AAAA,cAAA;AAAA,cAAKH,EAAI,SAAS,QAAQ,CAAC;AAAA,cAAE;AAAA,YAAA,GAAC;AAAA,YACxD,gBAAAG,EAAC,QAAA,EAAK,WAAU,mBAAkB,UAAA;AAAA,cAAA;AAAA,cAAKH,EAAI,MAAM,QAAQ,CAAC;AAAA,cAAE;AAAA,YAAA,GAAC;AAAA,YAC7D,gBAAAG,EAAC,QAAA,EAAK,WAAU,WAAU,UAAA;AAAA,cAAA;AAAA,cAAKH,EAAI,SAAS,QAAQ,CAAC;AAAA,cAAE;AAAA,YAAA,EAAA,CAAC;AAAA,UAAA,EAAA,CAC1D;AAAA,QAAA,EAAA,CACF;AAAA,MAAA,EAAA,GAjCMK,EAAU,IAmCpB;AAAA,IAEJ,CAAC,EAAA,CAEL;AAAA,EAAA,GACF;AAEJ;"}
@@ -0,0 +1,86 @@
1
+ import { jsx as b } from "react/jsx-runtime";
2
+ import { forwardRef as j, useRef as z, useImperativeHandle as F, useEffect as p } from "react";
3
+ import { clearCanvas as g, resizeCanvas as H, drawPoseLandmarks as I, drawAngle as L } from "../utils/drawingUtils.js";
4
+ const N = j(
5
+ ({
6
+ landmarks: n = [],
7
+ angles: r = [],
8
+ width: o,
9
+ height: t,
10
+ drawOptions: c = {},
11
+ customRenderer: l,
12
+ className: P = "",
13
+ style: i = {},
14
+ mirror: m = !0
15
+ }, E) => {
16
+ const s = z(null);
17
+ F(E, () => ({
18
+ getCanvas: () => s.current,
19
+ getContext: () => s.current?.getContext("2d") || null,
20
+ clear: () => {
21
+ const e = s.current?.getContext("2d");
22
+ e && g(e);
23
+ },
24
+ redraw: () => {
25
+ f();
26
+ }
27
+ })), p(() => {
28
+ const e = s.current;
29
+ e && H(e, o, t);
30
+ }, [o, t]);
31
+ const f = () => {
32
+ const e = s.current;
33
+ if (!e) {
34
+ console.log("PoseCanvas: no canvas element");
35
+ return;
36
+ }
37
+ const a = e.getContext("2d");
38
+ if (!a) {
39
+ console.log("PoseCanvas: no 2d context");
40
+ return;
41
+ }
42
+ if (g(a), l) {
43
+ l(a, { landmarks: n, angles: r, width: o, height: t });
44
+ return;
45
+ }
46
+ if (n.length === 0) {
47
+ console.log("PoseCanvas: no landmarks to draw");
48
+ return;
49
+ }
50
+ console.log("PoseCanvas: drawing", n.length, "landmarks"), I(a, n, c), c.showAngles && r.length > 0 && r.forEach((u) => {
51
+ const [d, y, A] = u.points, v = n[d], C = n[y], x = n[A];
52
+ v && C && x && L(a, v, C, x, u.angle, o, t, c);
53
+ });
54
+ };
55
+ return p(() => {
56
+ console.log("PoseCanvas render:", {
57
+ landmarksCount: n.length,
58
+ anglesCount: r.length,
59
+ width: o,
60
+ height: t,
61
+ canvas: s.current
62
+ }), f();
63
+ }, [n, r, o, t, c, l]), /* @__PURE__ */ b(
64
+ "canvas",
65
+ {
66
+ ref: s,
67
+ className: P,
68
+ style: {
69
+ position: "absolute",
70
+ top: 0,
71
+ left: 0,
72
+ pointerEvents: "none",
73
+ transform: m ? "scaleX(-1)" : "none",
74
+ ...i
75
+ },
76
+ width: o,
77
+ height: t
78
+ }
79
+ );
80
+ }
81
+ );
82
+ N.displayName = "PoseCanvas";
83
+ export {
84
+ N as PoseCanvas
85
+ };
86
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sources":["../../../src/components/PoseCanvas/index.tsx"],"sourcesContent":["import React, { useEffect, useRef, forwardRef, useImperativeHandle } from 'react'\nimport type { JointAngle, PosePoint } from '../../types'\nimport { drawPoseLandmarks, drawAngle, clearCanvas, resizeCanvas, DrawingOptions } from '../utils/drawingUtils'\n\nexport interface RenderData {\n landmarks: PosePoint[]\n angles: JointAngle[]\n width: number\n height: number\n}\n\nexport interface PoseCanvasProps {\n // 数据输入\n landmarks?: PosePoint[]\n angles?: JointAngle[]\n \n // 尺寸\n width: number\n height: number\n \n // 绘制配置\n drawOptions?: DrawingOptions\n \n // 自定义渲染器(可选)\n customRenderer?: (ctx: CanvasRenderingContext2D, data: RenderData) => void\n \n // 样式\n className?: string\n style?: React.CSSProperties\n \n // 是否镜像翻转\n mirror?: boolean\n}\n\nexport interface PoseCanvasHandle {\n getCanvas: () => HTMLCanvasElement | null\n getContext: () => CanvasRenderingContext2D | null\n clear: () => void\n redraw: () => void\n}\n\nexport const PoseCanvas = forwardRef<PoseCanvasHandle, PoseCanvasProps>(\n (\n {\n landmarks = [],\n angles = [],\n width,\n height,\n drawOptions = {},\n customRenderer,\n className = '',\n style = {},\n mirror = true\n },\n ref\n ) => {\n const canvasRef = useRef<HTMLCanvasElement>(null)\n\n // 暴露方法给父组件\n useImperativeHandle(ref, () => ({\n getCanvas: () => canvasRef.current,\n getContext: () => canvasRef.current?.getContext('2d') || null,\n clear: () => {\n const ctx = canvasRef.current?.getContext('2d')\n if (ctx) clearCanvas(ctx)\n },\n redraw: () => {\n drawFrame()\n }\n }))\n\n // 调整画布尺寸\n useEffect(() => {\n const canvas = canvasRef.current\n if (!canvas) return\n\n resizeCanvas(canvas, width, height)\n }, [width, height])\n\n // 绘制帧\n const drawFrame = () => {\n const canvas = canvasRef.current\n if (!canvas) {\n console.log('PoseCanvas: no canvas element')\n return\n }\n\n const ctx = canvas.getContext('2d')\n if (!ctx) {\n console.log('PoseCanvas: no 2d context')\n return\n }\n\n // 清空画布\n clearCanvas(ctx)\n\n // 如果有自定义渲染器,使用自定义渲染\n if (customRenderer) {\n customRenderer(ctx, { landmarks, angles, width, height })\n return\n }\n\n // 默认渲染\n if (landmarks.length === 0) {\n console.log('PoseCanvas: no landmarks to draw')\n return\n }\n\n console.log('PoseCanvas: drawing', landmarks.length, 'landmarks')\n\n // 绘制骨骼和关键点\n drawPoseLandmarks(ctx, landmarks, drawOptions)\n\n // 绘制角度标注\n if (drawOptions.showAngles && angles.length > 0) {\n angles.forEach((angleData) => {\n const [idx1, idx2, idx3] = angleData.points\n const p1 = landmarks[idx1]\n const p2 = landmarks[idx2]\n const p3 = landmarks[idx3]\n\n if (p1 && p2 && p3) {\n drawAngle(ctx, p1, p2, p3, angleData.angle, width, height, drawOptions)\n }\n })\n }\n }\n\n // 当数据更新时重新绘制\n useEffect(() => {\n console.log('PoseCanvas render:', {\n landmarksCount: landmarks.length,\n anglesCount: angles.length,\n width,\n height,\n canvas: canvasRef.current\n })\n drawFrame()\n }, [landmarks, angles, width, height, drawOptions, customRenderer])\n\n return (\n <canvas\n ref={canvasRef}\n className={className}\n style={{\n position: 'absolute',\n top: 0,\n left: 0,\n pointerEvents: 'none',\n transform: mirror ? 'scaleX(-1)' : 'none',\n ...style\n }}\n width={width}\n height={height}\n />\n )\n }\n)\n\nPoseCanvas.displayName = 'PoseCanvas'\n"],"names":["PoseCanvas","forwardRef","landmarks","angles","width","height","drawOptions","customRenderer","className","style","mirror","ref","canvasRef","useRef","useImperativeHandle","ctx","drawFrame","useEffect","canvas","resizeCanvas","clearCanvas","drawPoseLandmarks","angleData","idx1","idx2","idx3","p1","p2","p3","drawAngle","jsx"],"mappings":";;;AAyCO,MAAMA,IAAaC;AAAA,EACxB,CACE;AAAA,IACE,WAAAC,IAAY,CAAA;AAAA,IACZ,QAAAC,IAAS,CAAA;AAAA,IACT,OAAAC;AAAA,IACA,QAAAC;AAAA,IACA,aAAAC,IAAc,CAAA;AAAA,IACd,gBAAAC;AAAA,IACA,WAAAC,IAAY;AAAA,IACZ,OAAAC,IAAQ,CAAA;AAAA,IACR,QAAAC,IAAS;AAAA,EAAA,GAEXC,MACG;AACH,UAAMC,IAAYC,EAA0B,IAAI;AAGhD,IAAAC,EAAoBH,GAAK,OAAO;AAAA,MAC9B,WAAW,MAAMC,EAAU;AAAA,MAC3B,YAAY,MAAMA,EAAU,SAAS,WAAW,IAAI,KAAK;AAAA,MACzD,OAAO,MAAM;AACX,cAAMG,IAAMH,EAAU,SAAS,WAAW,IAAI;AAC9C,QAAIG,OAAiBA,CAAG;AAAA,MAC1B;AAAA,MACA,QAAQ,MAAM;AACZ,QAAAC,EAAA;AAAA,MACF;AAAA,IAAA,EACA,GAGFC,EAAU,MAAM;AACd,YAAMC,IAASN,EAAU;AACzB,MAAKM,KAELC,EAAaD,GAAQd,GAAOC,CAAM;AAAA,IACpC,GAAG,CAACD,GAAOC,CAAM,CAAC;AAGlB,UAAMW,IAAY,MAAM;AACtB,YAAME,IAASN,EAAU;AACzB,UAAI,CAACM,GAAQ;AACX,gBAAQ,IAAI,+BAA+B;AAC3C;AAAA,MACF;AAEA,YAAMH,IAAMG,EAAO,WAAW,IAAI;AAClC,UAAI,CAACH,GAAK;AACR,gBAAQ,IAAI,2BAA2B;AACvC;AAAA,MACF;AAMA,UAHAK,EAAYL,CAAG,GAGXR,GAAgB;AAClB,QAAAA,EAAeQ,GAAK,EAAE,WAAAb,GAAW,QAAAC,GAAQ,OAAAC,GAAO,QAAAC,GAAQ;AACxD;AAAA,MACF;AAGA,UAAIH,EAAU,WAAW,GAAG;AAC1B,gBAAQ,IAAI,kCAAkC;AAC9C;AAAA,MACF;AAEA,cAAQ,IAAI,uBAAuBA,EAAU,QAAQ,WAAW,GAGhEmB,EAAkBN,GAAKb,GAAWI,CAAW,GAGzCA,EAAY,cAAcH,EAAO,SAAS,KAC5CA,EAAO,QAAQ,CAACmB,MAAc;AAC5B,cAAM,CAACC,GAAMC,GAAMC,CAAI,IAAIH,EAAU,QAC/BI,IAAKxB,EAAUqB,CAAI,GACnBI,IAAKzB,EAAUsB,CAAI,GACnBI,IAAK1B,EAAUuB,CAAI;AAEzB,QAAIC,KAAMC,KAAMC,KACdC,EAAUd,GAAKW,GAAIC,GAAIC,GAAIN,EAAU,OAAOlB,GAAOC,GAAQC,CAAW;AAAA,MAE1E,CAAC;AAAA,IAEL;AAGA,WAAAW,EAAU,MAAM;AACd,cAAQ,IAAI,sBAAsB;AAAA,QAChC,gBAAgBf,EAAU;AAAA,QAC1B,aAAaC,EAAO;AAAA,QACpB,OAAAC;AAAA,QACA,QAAAC;AAAA,QACA,QAAQO,EAAU;AAAA,MAAA,CACnB,GACDI,EAAA;AAAA,IACF,GAAG,CAACd,GAAWC,GAAQC,GAAOC,GAAQC,GAAaC,CAAc,CAAC,GAGhE,gBAAAuB;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,KAAKlB;AAAA,QACL,WAAAJ;AAAA,QACA,OAAO;AAAA,UACL,UAAU;AAAA,UACV,KAAK;AAAA,UACL,MAAM;AAAA,UACN,eAAe;AAAA,UACf,WAAWE,IAAS,eAAe;AAAA,UACnC,GAAGD;AAAA,QAAA;AAAA,QAEL,OAAAL;AAAA,QACA,QAAAC;AAAA,MAAA;AAAA,IAAA;AAAA,EAGN;AACF;AAEAL,EAAW,cAAc;"}
@@ -0,0 +1 @@
1
+ .rom-visualizer{display:flex;flex-direction:column;gap:1.5rem;width:100%;max-width:1200px;margin:0 auto}.visualizer-container{display:flex;gap:1.5rem;flex-wrap:wrap}.video-container{position:relative;background:#000;border-radius:12px;overflow:hidden;box-shadow:0 10px 15px -3px #0000001a}.video-element{display:block;width:100%;height:100%;transform:scaleX(-1)}.video-overlay{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;background:#000000b3;z-index:5}.overlay-content{text-align:center;color:#fff}.camera-icon{margin-bottom:1rem;opacity:.8}.overlay-content p{font-size:1rem;margin:0;opacity:.9}.visualizer-angle-panel{flex:1;min-width:300px}.controls{display:flex;align-items:center;gap:1rem;padding:1rem;background:#f9fafb;border-radius:12px;box-shadow:0 1px 3px #0000001a}.control-button{display:flex;align-items:center;gap:.5rem;padding:.75rem 1.5rem;border:none;border-radius:8px;font-size:1rem;font-weight:600;cursor:pointer;transition:all .2s;box-shadow:0 2px 4px #0000001a}.control-button:hover:not(:disabled){transform:translateY(-2px);box-shadow:0 4px 6px #00000026}.control-button:active:not(:disabled){transform:translateY(0)}.control-button:disabled{opacity:.5;cursor:not-allowed}.start-button{background:linear-gradient(135deg,#3b82f6,#2563eb);color:#fff}.start-button:hover:not(:disabled){background:linear-gradient(135deg,#2563eb,#1d4ed8)}.stop-button{background:linear-gradient(135deg,#ef4444,#dc2626);color:#fff}.stop-button:hover:not(:disabled){background:linear-gradient(135deg,#dc2626,#b91c1c)}.button-icon{flex-shrink:0}.status-indicator{display:flex;align-items:center;gap:.5rem;margin-left:auto;padding:.5rem 1rem;background:#dcfce7;border-radius:9999px}.status-dot{width:8px;height:8px;background:#10b981;border-radius:50%;animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}@keyframes pulse{0%,to{opacity:1}50%{opacity:.5}}.status-text{font-size:.875rem;font-weight:600;color:#065f46}.error-message{display:flex;align-items:center;gap:.75rem;padding:1rem;background:#fee2e2;border-left:4px solid #ef4444;border-radius:8px;color:#991b1b;font-size:.875rem}.error-icon{flex-shrink:0;color:#ef4444}@media(max-width:768px){.visualizer-container{flex-direction:column}.video-container{width:100%!important;height:auto!important}.video-element{width:100%;height:auto}.visualizer-angle-panel{width:100%}.controls{flex-direction:column;align-items:stretch}.control-button{justify-content:center}.status-indicator{margin-left:0;justify-content:center}}