提交 8ee43a45 authored 作者: Gorvey's avatar Gorvey

update docs

上级 0b2e87b1
...@@ -5,12 +5,14 @@ ...@@ -5,12 +5,14 @@
"scripts": { "scripts": {
"build": "slidev build", "build": "slidev build",
"dev": "slidev --open", "dev": "slidev --open",
"export": "slidev export" "export": "slidev export",
}, },
"dependencies": { "dependencies": {
"@slidev/cli": "^52.2.4", "@slidev/cli": "^52.2.4",
"@slidev/theme-default": "latest", "@slidev/theme-default": "latest",
"@slidev/theme-seriph": "latest", "@slidev/theme-seriph": "latest",
"highlight.js": "^11.11.1",
"leafer": "^1.9.12",
"vue": "^3.5.21" "vue": "^3.5.21"
} }
} }
\ No newline at end of file
# PDF出血处理
## 1. 什么是出血(Bleed)
**出血(Bleed)**是印刷行业的重要概念,指的是印刷品边缘向外延伸的区域,用于确保在裁切后不会有白边。
### 1.1 出血的作用
**防止裁切误差**
- 印刷厂在裁切纸张时存在±1-2mm的机械误差
- 如果没有出血,裁切后可能出现白边
- 出血确保了即使有轻微裁切偏移,背景色/图像也能覆盖到边缘
**视觉完整性**
- 保证印刷品的色彩或设计元素能够完整延伸到页面边缘
- 避免裁切后留下不美观的白色边框
### 1.2 标准出血尺寸
**国际标准**
- **常见出血值**: 3mm (约0.125英寸)
- **美式标准**: 0.125英寸 (约3.2mm)
- **欧洲标准**: 3-5mm
**实际应用**
- A4纸成品: 210×297mm
- A4纸含出血: 216×303mm (每边加3mm出血)
## 2. 出血处理
```js
// 出血处理公式
const offsetX = bleed * (dpi / 25.4)
const offsetY = bleed * (dpi / 25.4)
// px = mm * (dpi / 25.4) + 出血
const pxX = x * scaleX + offsetX
const pxY = y * scaleY + offsetY
```
\ No newline at end of file
# 单位转换
## 1.标注单位的转换
### 1.1 逻辑单位 (px - 像素)
**定义**: 逻辑单位是在数字环境中使用的抽象单位,通常指像素点(pixel)。
**特点**:
- 是Canvas绘图系统的基础单位
- 相对单位,其物理尺寸取决于显示设备的像素密度
- 在不同设备上显示的实际大小会有差异
### 1.2 物理单位 (mm/in - 毫米/英寸)
**定义**: 物理单位是现实世界中使用的标准长度单位,具有固定的物理尺寸。
**常用物理单位**:
- **毫米(mm)**: 最常用的物理单位
- **厘米(cm)**: 1cm = 10mm
- **英寸(in)**: 国际标准单位,1in = 25.4mm
**实际应用场景**:
- A5纸尺寸: 210mm × 148mm
- A4纸尺寸: 297mm × 210mm
- 照片打印尺寸: 6英寸 × 4英寸
### 1.3 像素密度 (PPI - Pixels Per Inch)
**定义**: PPI(Pixels Per Inch)表示每英寸长度内包含的像素点数量,是连接逻辑单位和物理单位的桥梁。
**计算公式**:
```
PPI = 像素总数 / 物理尺寸(英寸)
```
**PPI的意义**:
- PPI越高,显示效果越细腻
- 同样的物理尺寸,PPI越高的设备需要的像素点越多
- 是单位转换的核心参数
### 1.4 单位转换公式
**核心转换关系**:
```
1英寸(in) = 25.4毫米(mm)
像素数量 = 物理尺寸(英寸) × PPI
物理尺寸(英寸) = 像素数量 ÷ PPI
物理尺寸(毫米) = 像素数量 ÷ PPI × 25.4
```
## 2. 棒棒帮中的单位体系与转换
1. 棒棒帮内部使用的ppi是300
2. 标注系统中使用的单位是逻辑单位(px)
3. 智慧笔产生的单位是物理尺寸(mm)
### 2.1 标注单位到物理单位的转换公式
```js
/**
* 将 px 坐标点转换为 mm 坐标点
* @param point 坐标点
* @param ppi ppi
* @param mm 毫米
* @returns
*/
export function processPxToMmPoint(point: IMarkPoint, ppi: number = 300, mm: number = 25.4): IMarkPoint {
return {
x: (point.x / ppi) * mm,
y: (point.y / ppi) * mm
}
}
/**
* 将 mm 坐标点转换为 px 坐标点
* @param {Object} point 坐标点
* @param {number} ppi ppi
* @param {number} mm 毫米
* @returns {Object}
*/
export function processMmToPxPoint(point, ppi = 300, mm = 25.4) {
return {
x: (point.x * ppi) / mm,
y: (point.y * ppi) / mm
}
}
```
\ No newline at end of file
# 架构概览
## 1. 依赖关系
依赖链:[LeaferJS](https://www.leaferjs.com/) ← LeaferAnnotate ← Hook ← 页面
- **LeaferJS(底层引擎)**: 负责 Canvas 渲染、事件和交互
- **LeaferAnnotate(业务实例)**: 基于 LeaferJS 封装业务逻辑和方法
- **Hook(useLeaferAnnotateSingleton)**: 再次封装LeaferAnnotate,管理单例、暴露页面可用的方法
- **页面(Vue 组件)**: 仅通过 Hook 与标注实例交互
## 2. LeaferJS
```js
import { App, Rect } from 'leafer-ui'
const app = new App({
view: window,
width: 600,
height: 600,
fill: '#333'
})
app.tree.add(Rect.one({ fill: '#32cd79', draggable: true }, 100, 100))
```
## 3. LeaferAnnotate 类
```ts
export interface ILeaferAnnotate {
config: LeaferAnnotateConfig
app: App
pageFrame: IFrame
snap?: any
limit: LimitConfig
activeQuestionID: number | null
/** 初始化实例 */
init(): Promise<void>
/** 重置视图到初始状态 */
resetView(): void
/**
* 加载页面与标注数据
*/
loadData(pageUrl: string, marks: IMark[]): Promise<void>
delElement(id: string): void
selectMark(id: string): void
setMarkHover(id: string): void
unsetMarkHover(id: string): void
setActiveQuestionID(questionID: number | null): void
getActiveQuestionID(): number | null
changeMode(mode: 'view' | 'edit'): void
destroy(): void
}
```
```ts
import { createLeaferAnnotate } from '../mark/leafer-mark-core/index'
// DOM 容器
const view = document.getElementById('annotate-view') as HTMLDivElement
// 创建实例工厂
const { getInstance, destroy } = await createLeaferAnnotate({
view,
})
// 获取实例
const leaferAnnotate = getInstance()
// 加载底图与标注
await leaferAnnotate?.loadData('https://example.com/page.png', [
{
id: 'mark-1',
x: 120,
y: 80,
width: 160,
height: 60,
questionID: 1001,
color: '#FF6B6B'
}
])
// 交互 API 示例
lealeaferAnnotatefer?.changeMode('view')
leaferAnnotate?.selectMark('mark-1')
leaferAnnotate?.setActiveQuestionID(1001)
leaferAnnotate?.resetView()
// 销毁
await destroy()
```
## 使用Hook包装 leaferAnnotate
下面示例基于页面中通过 Hook 演示如何在组件中初始化、加载底图与标注、选中标注以及重置视图。
```vue
<script setup lang="ts">
import { onMounted } from 'vue'
import { useLeaferAnnotateSingleton } from '../mark/leafer-mark-core/useLeaferAnnotate'
const { createApp, loadData, selectMark, resetView, markList } = useLeaferAnnotateSingleton()
onMounted(async () => {
// 初始化实例,挂载到元素汇总
await createApp({ view: 'leafer-container' })
// 加载底图和框选
await loadData('https://example.com/page.png', [
{
id: 'mark-1',
x: 120,
y: 80,
width: 160,
height: 60,
questionID: 1001,
color: '#FF6B6B'
}
])
selectMark('mark-1')
})
</script>
```
\ No newline at end of file
# 业务流程
## IEP-[资源框选配置](http://corp.iep.casdio.com/#/custom-teaching-materials)
1. 产生教材信息
2. 配置资源框选和习题数据
## 学习机/APP/小程序 - 学员使用
1. 根据教材信息,渲染出页面
3. 提供智慧笔产生的笔迹数据
## UCU-批改作业
1. 根据教材信息+资源框选+笔迹数据,渲染出作业批改页面
\ No newline at end of file
# 项目背景介绍
## 完整业务流程
```mermaid
flowchart TD
A[IEP智能题库系统] --> B[题目铺码处理]
B --> C[标准答案标注]
C --> D[学员在线答题]
D --> E[笔迹数据实时采集]
E --> F[UCU智能批改引擎]
F --> G[批改结果生成]
G --> H[个性化反馈推送]
B --> B1[题目ID生成]
B --> B2[知识点标记]
B --> B3[难度等级设定]
C --> C1[正确答案录入]
C --> C2[评分规则配置]
C --> C3[解题步骤标注]
D --> D1[手写笔迹输入]
D --> D2[几何图形绘制]
D --> D3[计算过程记录]
E --> E1[轨迹坐标采集]
E --> E2[时序信息记录]
E --> E3[压力速度数据]
F --> F1[笔迹模式匹配]
F --> F2[答案准确性验证]
F --> F3[解题过程分析]
G --> G1[得分计算]
G --> G2[错误点标记]
G --> G3[改进建议生成]
H --> H1[即时结果反馈]
H --> H2[错题本更新]
H --> H3[学习路径推荐]
style A fill:#e3f2fd,stroke:#2196f3,color:#0d47a1
style F fill:#f3e5f5,stroke:#9c27b0,color:#4a148c
style H fill:#e8f5e8,stroke:#4caf50,color:#1b5e20
```
差异被折叠。
---
theme: seriph
title: 基于 LeaferJS 封装的底图渲染与交互源码解析
info: |
本演讲介绍如何在项目中使用 `mark/` 模块与 `leafer-mark-core/index.ts`,以及该模块通过封装 LeaferJS 的 `class LeaferAnnotate` 的核心设计、插件体系与关键方法。
class: text-center
drawings:
persist: false
transition: slide-left
mdc: true
---
# 基于 LeaferJS 封装的底图渲染与交互源码解析
---
## 目录
1. 使用场景与目标
2. 快速上手(组件与 Hook)
3. 运行时能力(API 与事件)
4. 源码解读:`LeaferAnnotate` 架构
5. 插件体系与交互模式
6. 数据流与落库
7. 扩展点与最佳实践
---
## 1. 使用场景与目标
- 标注 PDF 页面或图片底图上的题目/区域
- 支持拖拽、复制、吸附、标尺、对齐辅助线
- 统一封装:在 Vue 组件中复用一个 Leafer 标注实例
目标:简单接入、稳定交互、数据可序列化落库
---
## 2. 快速上手
只需要在页面中引入 hook `useLeaferAnnotate`
要点:
- 调用 `createApp` 生成LeaferAnnotate实例,绑定到页面中
- 调用 `loadData(pageUrl, marks)` 加载底图与标注
- 调用 `loadPoints(pointsData)` 加载学员的书写笔迹
```ts
import { onMounted } from 'vue'
import { useLeaferAnnotateSingleton } from '@/mark/leafer-mark-core/useLeaferAnnotate'
const { createApp, loadData, loadPoints } = useLeaferAnnotateSingleton()
onMounted(async () => {
// 初始化leafer canvas容器
await createApp({ view: 'leafer-container' })
// 加载底图与mark
await loadData('https://example.com/page.png', [])
// 加载学员笔迹
loadPoints(pointsData)
})
```
---
## 2. 快速上手(Hook 能力)
`useLeaferAnnotateSingleton` 提供单例式标注实例管理,常用能力:
- createApp(config)
- loadData(pageUrl, marks)
- selectMark / delElement / resetView
- setMarkHover / unsetMarkHover
- onElementSelect / onElementAdd(事件)
- markList(响应式标注列表)
```ts
const {
createApp,
loadData,
markList,
onElementSelect,
onElementAdd,
selectMark,
delElement,
resetView
} = useLeaferAnnotateSingleton()
onElementSelect(({ element }) => {
// 业务联动,如高亮列表项
})
onElementAdd(({ element }) => {
// 业务联动,如自动选中或滚动到新项
})
```
---
## 4. 源码解读:初始化流程
关键步骤:
- 创建 `App`,容器取自 `config.view`
- 新建 `Frame` 作为页面层 `pageFrame`
- `bindPlugins()` 启用吸附、拖拽、复制、创建矩形、标尺、对齐辅助线
- `changeMode('view')` 默认查看/编辑模式
```ts
async init() {
this.app = new App({ view: this.config.view, ...DEFAULT_LEAFER_CONFIG })
this.pageFrame = new Frame({ id: 'pageFrame' })
this.app.tree?.add(this.pageFrame)
this.bindPlugins()
this.changeMode('view')
}
```
---
## 4. 源码解读:加载数据
`loadData(pageUrl, marks)`
- 清理旧内容与资源缓存
- 通过 `loadImage` 获取底图尺寸,设置 `pageFrame` 宽高
- 添加 `Image` 节点为底图
- `initMarks(marks)` 将服务端标注转成矩形加入 `pageFrame`
```ts
async loadData(pageUrl, marks) {
this.pageFrame.clear(); Resource.destroy()
const { url, width, height } = await loadImage(pageUrl, 'bg.png')
this.pageFrame.width = width
this.pageFrame.height = height
this.app.tree?.zoom('fit-width', [32, 12, 12, 32])
this.pageFrame.add(new Image({ id: 'bg-image', url, width, height }))
this.initMarks(marks)
}
```
---
## 5. 插件体系与交互模式
`bindPlugins()` 一次性安装:
- Snap(对齐/吸附)
- Ruler(标尺)
- AdsorptionBinding(像素级微调,去小数)
- DropBinding(拖拽)
- CopyRectBinding(Ctrl 拖拽复制)
- CreateRectBinding(画框创建)
交互模式 `changeMode(mode)`
- view:可缩放/移动,命中子元素
- edit:锁定页面交互,`hitChildren = false`
---
## 6. 数据流与落库
组件侧(`mark/index.vue`):
- 拉取页列表与对应 `marks`
- `loadData(pageUrl, marks)` 注入底图与标注
- 保存时遍历 `markList`,将像素点位转换为毫米点位并落库
核心转换:
```ts
const dataList = markList.value.map(item => {
const { top: topPx, bottom: bottomPx } = processRectToMarkPoints(
item.x as number, item.y as number, item.width as number, item.height as number
)
const top = processPxToMmPoint(topPx)
const bottom = processPxToMmPoint(bottomPx)
return { ...item.data, top, bottom, topPx, bottomPx }
})
```
---
## 7. 扩展点与最佳实践
- 通过 `marks` 数据结构扩展业务字段(如题型、知识点)
-`onElementAdd / onElementSelect` 中做业务联动(列表、右侧面板)
- 利用 `limit`(宽高/锁定)做页面级约束
- 自定义主题色:`getThemeColor(mark.data)` 控制交互态填充/描边
- 复杂场景下一律使用单例 Hook,避免多实例资源竞争
---
## Q & A
欢迎提问与交流
— 代码位置:
- `mark/leafer-mark-core/core/index.ts`
- `mark/leafer-mark-core/useLeaferAnnotate.ts`
- `mark/index.vue`
...@@ -3,7 +3,7 @@ theme: seriph ...@@ -3,7 +3,7 @@ theme: seriph
title: 基于 LeaferJS 封装的底图渲染与交互源码解析 title: 基于 LeaferJS 封装的底图渲染与交互源码解析
info: | info: |
本演讲介绍如何在项目中使用 `mark/` 模块与 `leafer-mark-core/index.ts`,以及该模块通过封装 LeaferJS 的 `class LeaferAnnotate` 的核心设计、插件体系与关键方法。 本演讲介绍如何在项目中使用 `mark/` 模块与 `leafer-mark-core/index.ts`,以及该模块通过封装 LeaferJS 的 `class LeaferAnnotate` 的核心设计、插件体系与关键方法。
class: text-center class: text-center
drawings: drawings:
persist: false persist: false
transition: slide-left transition: slide-left
...@@ -12,217 +12,17 @@ defaults: ...@@ -12,217 +12,17 @@ defaults:
layout: scroll layout: scroll
--- ---
# 基于 LeaferJS 封装的底图渲染与交互源码解析 # 基于 LeaferJS 的底图渲染与交互封装:源码解析
---
## 目录
1. 使用场景与目标
2. 快速上手(组件与 Hook)
3. 依赖关系:页面 → hook → LeaferAnnotate → LeaferJS
4. 运行时能力(API 与事件)
5. 源码解读:`LeaferAnnotate` 架构
6. 插件体系与交互模式
7. 数据流与落库
8. 扩展点与最佳实践
--- ---
src: ./pages/架构概览.md
## 1. 使用场景与目标
- 标注 PDF 页面或图片底图上的题目/区域
- 支持拖拽、复制、吸附、标尺、对齐辅助线
- 统一封装:在 Vue 组件中复用一个 Leafer 标注实例
目标:简单接入、稳定交互、数据可序列化落库
--- ---
## 2. 依赖关系
依赖链:LeaferJS ← LeaferAnnotate ← Hook ← 页面
- **LeaferJS(底层引擎)**: 负责 Canvas 渲染、事件与几何计算
- **LeaferAnnotate(业务实例)**: 基于 LeaferJS 封装标注能力与插件体系
- **Hook(useLeaferAnnotateSingleton)**: 管理单例、暴露页面可用的方法
- **页面(Vue 组件)**: 仅通过 Hook 与标注实例交互
页面不直接调用 Leafer/实例细节;Hook 只做编排与生命周期;实例聚合业务能力;底层交由 Leafer 处理
--- ---
src: ./pages/实现难点.md
## 3. 快速使用
只需要在页面中引入 hook `useLeaferAnnotate`
要点:
- 调用 `createApp` 生成LeaferAnnotate实例,绑定到页面中
- 调用 `loadData(pageUrl, marks)` 加载底图与标注
- 调用 `loadPoints(pointsData)` 加载学员的书写笔迹
```ts
import { onMounted } from 'vue'
import { useLeaferAnnotateSingleton } from '@/mark/leafer-mark-core/useLeaferAnnotate'
const { createApp, loadData, loadPoints } = useLeaferAnnotateSingleton()
onMounted(async () => {
// 初始化leafer canvas容器
await createApp({ view: 'leafer-container' })
// 加载底图与mark
await loadData('https://example.com/page.png', [])
// 加载学员笔迹
loadPoints(pointsData)
})
```
---
## 4. LeaferAnnotate 主流程:init
入口在 Hook 的 `createApp`,最终委托到 `createLeaferAnnotate``LeaferAnnotate.init` 完成实例、画布与插件初始化:
```ts
export const createLeaferAnnotate = async (
config: LeaferAnnotateConfig
): Promise<{
getInstance: () => LeaferAnnotate | null
destroy: () => Promise<void>
}> => {
let instance: LeaferAnnotate | null = new LeaferAnnotate(cloneDeep(config))
await instance.init()
```
`init` 核心步骤:创建 `App`、创建并挂载 `Frame`、启用插件、设置默认模式。
```ts
async init(): Promise<void> {
let { view } = this.config
// 初始化画布,没有设置宽高,默认使用父元素的宽高
this.app = new App({
view: view,
...DEFAULT_LEAFER_CONFIG
})
// // 设置图片和标记
this.pageFrame = new Frame({
id: 'pageFrame'
})
this.app.tree?.add(this.pageFrame)
// 启用插件
this.bindPlugins()
// 默认为view模式
this.changeMode('view')
}
```
要点:
- **App 创建**:传入 `view` 绑定 DOM 容器,合并默认渲染配置。
- **Frame 容器**:作为页面图层承载底图与所有标注矩形。
- **插件启用**:吸附、拖拽、复制、矩形工具、标尺、对齐辅助线一次性装配。
- **模式**:默认 `view`,可缩放、移动并可编辑元素;`edit` 则限制交互。
插件与模式的关键调用:
```ts
private bindPlugins(): void {
this.snap = new Snap(this.app, {
...DEFAULT_SNAP_CONFIG,
parentContainer: this.pageFrame
})
this.ruler = new Ruler(this.app, DEFAULT_RULER_CONFIG)
// 精确控制元素,x,y,width,height在1px单位步进变化,去除小数点
this.adsorptionBinding = new AdsorptionBinding()
this.adsorptionBinding.install(this.app)
// 在画布上拖拽图元
this.dropBinding = new DropBinding()
this.dropBinding.install(this)
// 按住ctrl复制矩形
this.copyRectBinding = new CopyRectBinding()
this.copyRectBinding.install(this, DEFAULT_RECT_CONFIG.className)
// 创建矩形工具
this.createRectBinding = new CreateRectBinding()
this.createRectBinding.install(this)
```
```ts
public changeMode(mode: 'view' | 'edit'): void {
this.mode = mode
this.pageFrame.cursor = 'crosshair'
let interaction = this.app.tree?.interaction
configureInteractionMode(interaction, mode)
if (this.mode === 'view') {
this.pageFrame.hitChildren = true
} else if (this.mode === 'edit') {
this.pageFrame.hitChildren = false
}
}
```
--- ---
## 4. LeaferAnnotate 主流程:loadData
`loadData` 负责清理旧内容、加载底图图片、缩放视口,并把接口标注转为矩形加入图层:
```ts
public async loadData(pageUrl: string, marks: IMark[]): Promise<void> {
if (this.pageFrame) {
this.pageFrame.clear()
Resource.destroy()
}
// 获取底图,这是整个画布的核心
let { url, width, height } = await loadImage(pageUrl, 'bg.png')
this.pageFrame.width = width
this.pageFrame.height = height
this.app.tree?.zoom('fit-width', [12 + 20, 12, 12, 12 + 20])
this.pageFrame.add(new Image({ id: 'bg-image', url: url, width: width, height: height }))
// 初始化标注
this.initMarks(marks)
}
```
要点:
- **清理**`clear``Resource.destroy` 确保纹理与树干净。
- **底图**:异步 `loadImage` 拿到真实尺寸,设置 `Frame` 宽高后再添加背景 `Image`
- **视口**:按页面宽度自适应缩放,留出标尺与外边距。
- **标注**:将服务端数据映射为 `Rect`,逐个加入 `Frame` 成为可交互图元。
--- ---
src: ./pages/出血.md
## 4. LeaferAnnotate 主流程:loadMark(标注挂载)
标注转换与挂载:
```ts
private initMarks(marks: IMark[]): void {
if (!Array.isArray(marks) || !this.pageFrame) return
const layer = this.pageFrame
const rects = processMarksToRects(marks)
rects.forEach(rect => {
layer.add(rect)
})
}
```
要点:
- **数据映射**`processMarksToRects` 将接口标注映射为可交互 `Rect`
- **统一挂载**:逐个添加到 `Frame`,与底图处于同一画布树以便交互与渲染。
--- ---
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论