@sketch-ruler/canvas 3.0.0-beta.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.
package/AGENTS.md ADDED
@@ -0,0 +1,155 @@
1
+ # @sketch-ruler/canvas 项目指南
2
+
3
+ > 本文件面向 AI Coding Agent,用于快速理解 `@sketch-ruler/canvas` 的结构、API 与开发约定。
4
+
5
+ ---
6
+
7
+ ## 包概述
8
+
9
+ `@sketch-ruler/canvas` 是 **框架无关** 的 Canvas 2D 渲染器与 DOM 输入管理器。职责:
10
+
11
+ - Canvas 2D 标尺渲染(Canvas2DRenderer)
12
+ - 离屏缓存(OffscreenRulerCache)
13
+ - 标签缓存(LabelCache)
14
+ - 鼠标/键盘/滚轮事件适配(InputManager、MouseAdapter、KeyboardAdapter)
15
+ - 滚轮标准化(WheelNormalizer)
16
+
17
+ **唯一外部依赖**:`@sketch-ruler/core`
18
+
19
+ ---
20
+
21
+ ## 目录结构
22
+
23
+ ```
24
+ src/
25
+ ├── renderers/ # Canvas 2D 渲染器与缓存
26
+ │ ├── canvas-2d-renderer.ts
27
+ │ ├── offscreen-ruler-cache.ts
28
+ │ ├── label-cache.ts
29
+ │ ├── types.ts
30
+ │ └── index.ts
31
+ ├── input/ # DOM 输入管理
32
+ │ ├── input-manager.ts # 统一输入入口
33
+ │ ├── mouse-adapter.ts # 鼠标事件封装
34
+ │ ├── keyboard-adapter.ts # 键盘快捷键
35
+ │ ├── wheel-normalizer.ts # 滚轮标准化
36
+ │ └── index.ts
37
+ └── index.ts # 统一导出入口
38
+ ```
39
+
40
+ ---
41
+
42
+ ## 构建与测试
43
+
44
+ ```bash
45
+ # 安装依赖(在根目录执行)
46
+ pnpm i
47
+
48
+ # 构建本包
49
+ cd packages/canvas && pnpm build
50
+
51
+ # 运行测试
52
+ cd packages/canvas && pnpm test
53
+
54
+ # 测试监听模式
55
+ cd packages/canvas && pnpm test:watch
56
+ ```
57
+
58
+ ---
59
+
60
+ ## 编码约定
61
+
62
+ - **语言**:TypeScript 5.9+,strict 模式
63
+ - **模块**:ESM(`"type": "module"`)
64
+ - **DOM 依赖**:本包是唯一允许直接操作 DOM 和 Canvas 2D API 的包
65
+ - **事件绑定**:所有 DOM 事件监听器必须在 `destroy()` / `unbind()` 中清理,避免内存泄漏
66
+ - **命名**:
67
+ - 类名:PascalCase(`InputManager`、`Canvas2DRenderer`)
68
+ - 回调接口:以 `Callbacks` 结尾(`MouseAdapterCallbacks`)
69
+
70
+ ---
71
+
72
+ ## 关键 API 速查
73
+
74
+ ### InputManager
75
+
76
+ ```ts
77
+ const input = new InputManager(engine, {
78
+ zoomStep: 0.25,
79
+ zoomMode: 'pointer', // 'pointer' | 'viewport-center' | 'content-center'
80
+ viewportSize: { width, height },
81
+ contentSize: { width, height },
82
+ onCursorChange: (cls) => { } // 'default' | 'grab' | 'grabbing'
83
+ })
84
+ input.bind(containerElement) // 事件绑定到 container.parentElement
85
+ input.setZoomMode(mode)
86
+ input.getCursorClass()
87
+ input.destroy() // 清理所有事件监听
88
+ ```
89
+
90
+ **内置交互**:
91
+ - `Ctrl/Cmd + 滚轮`:缩放
92
+ - `Space + 鼠标左键拖拽`:平移
93
+ - `Ctrl+0`:100% 缩放
94
+ - `Ctrl++` / `Ctrl+-`:放大/缩小
95
+ - `Ctrl+1`:适配视口
96
+
97
+ ### Canvas2DRenderer
98
+
99
+ ```ts
100
+ const renderer = new Canvas2DRenderer(canvasElement)
101
+ renderer.render({
102
+ width, height, scale, offset,
103
+ vertical: false, // false=水平, true=垂直
104
+ thick, marks, config, palette
105
+ })
106
+ ```
107
+
108
+ ### 底层适配器(自行组合)
109
+
110
+ ```ts
111
+ const mouse = new MouseAdapter(parent, callbacks)
112
+ mouse.bind()
113
+ mouse.unbind()
114
+
115
+ const kb = new KeyboardAdapter({ onShortcut: (combo, e) => { } })
116
+ kb.bind()
117
+ kb.unbind()
118
+ ```
119
+
120
+ ---
121
+
122
+ ## 子路径导出
123
+
124
+ | 路径 | 用途 |
125
+ |------|------|
126
+ | `@sketch-ruler/canvas` | 完整导出 |
127
+ | `@sketch-ruler/canvas/renderers` | 仅渲染器与缓存 |
128
+ | `@sketch-ruler/canvas/input` | 仅输入管理层 |
129
+
130
+ ---
131
+
132
+ ## 与 @sketch-ruler/core 的协作
133
+
134
+ ```ts
135
+ import { TransformEngine, computeScaleMarks, getTickConfig } from '@sketch-ruler/core'
136
+ import { InputManager, Canvas2DRenderer } from '@sketch-ruler/canvas'
137
+
138
+ const engine = new TransformEngine({ x: 0, y: 0, scale: 1 })
139
+ const input = new InputManager(engine, { zoomMode: 'pointer' })
140
+ input.bind(canvasElement)
141
+
142
+ engine.onUpdate((state) => {
143
+ content.style.transform = `matrix(${state.scale}, 0, 0, ${state.scale}, ${state.x}, ${state.y})`
144
+ const marks = computeScaleMarks({ scale: state.scale, offset: state.x, length, thick, config })
145
+ renderer.render({ ... })
146
+ })
147
+ ```
148
+
149
+ ---
150
+
151
+ ## 注意事项
152
+
153
+ - `InputManager.bind(container)` 实际将滚轮/鼠标事件绑定到 `container.parentElement`,因此 `parentElement` 必须存在
154
+ - `MouseAdapter` 的 `onWheel` 回调接收 `(WheelEvent, NormalizedWheel)`,滚轮标准化逻辑在 `WheelNormalizer` 中
155
+ - 新增 DOM 事件相关逻辑时,请务必提供对应的解绑方法,确保 `destroy()` 能完整清理
package/README.md ADDED
@@ -0,0 +1,224 @@
1
+ # @sketch-ruler/canvas
2
+
3
+ > 框架无关的 Canvas 2D 渲染器与 DOM 输入管理器。负责标尺绘制、离屏缓存、鼠标/键盘/滚轮事件适配。依赖 `@sketch-ruler/core`。
4
+
5
+ ## 安装
6
+
7
+ ```bash
8
+ npm install @sketch-ruler/canvas
9
+ # 或
10
+ pnpm add @sketch-ruler/canvas
11
+ ```
12
+
13
+ ## 快速上手
14
+
15
+ ### InputManager — 统一输入管理
16
+
17
+ 绑定到画布容器,自动处理 **Ctrl+滚轮缩放**、**空格+拖拽平移**、**键盘快捷键**(Ctrl+0 / Ctrl++/Ctrl+-)。
18
+
19
+ ```ts
20
+ import { InputManager } from '@sketch-ruler/canvas'
21
+ import { TransformEngine } from '@sketch-ruler/core'
22
+
23
+ const engine = new TransformEngine({ x: 0, y: 0, scale: 1 })
24
+
25
+ const inputManager = new InputManager(engine, {
26
+ zoomStep: 0.25, // 滚轮缩放步长
27
+ zoomMode: 'pointer', // 'pointer' | 'viewport-center' | 'content-center'
28
+ viewportSize: { width: 1470, height: 700 },
29
+ contentSize: { width: 1920, height: 1080 },
30
+ onCursorChange: (cls) => {
31
+ // cls: 'default' | 'grab' | 'grabbing'
32
+ container.className = cls
33
+ }
34
+ })
35
+
36
+ // 绑定到画布 DOM(事件实际绑定到 parentElement)
37
+ inputManager.bind(canvasElement)
38
+
39
+ // 动态切换缩放模式
40
+ inputManager.setZoomMode('viewport-center')
41
+
42
+ // 获取当前光标类名
43
+ const cursorClass = inputManager.getCursorClass()
44
+
45
+ // 清理
46
+ inputManager.destroy()
47
+ ```
48
+
49
+ **事件处理细节:**
50
+
51
+ - **滚轮缩放**:按住 `Ctrl` / `Cmd` + 滚轮,以 `zoomMode` 指定的原点进行缩放
52
+ - **空格拖拽**:按住 `Space` + 鼠标左键拖拽平移
53
+ - **键盘快捷键**:
54
+ - `Ctrl+0`:缩放到 100%
55
+ - `Ctrl++` / `Ctrl+=`:放大
56
+ - `Ctrl+-`:缩小
57
+ - `Ctrl+1`:适配视口(留 5% 边距)
58
+
59
+ ### Canvas2DRenderer — Canvas 2D 标尺渲染器
60
+
61
+ ```ts
62
+ import { Canvas2DRenderer } from '@sketch-ruler/canvas'
63
+
64
+ const canvas = document.getElementById('ruler') as HTMLCanvasElement
65
+ const renderer = new Canvas2DRenderer(canvas)
66
+
67
+ renderer.render({
68
+ width: 1400,
69
+ height: 20,
70
+ scale: 1,
71
+ offset: 0,
72
+ vertical: false, // false=水平标尺, true=垂直标尺
73
+ thick: 20,
74
+ marks: scaleMarks, // 由 core 的 computeScaleMarks 生成
75
+ config: tickConfig,
76
+ palette: {
77
+ bgColor: '#f6f7f9',
78
+ tickColor: '#BABBBC',
79
+ labelColor: '#7D8694',
80
+ borderColor: '#eeeeef'
81
+ }
82
+ })
83
+ ```
84
+
85
+ ### 离屏缓存 — OffscreenRulerCache
86
+
87
+ 用于频繁重绘场景,将标尺渲染到 OffscreenCanvas 再 blit 到主 Canvas。
88
+
89
+ ```ts
90
+ import { OffscreenRulerCache } from '@sketch-ruler/canvas'
91
+
92
+ const cache = new OffscreenRulerCache({ width: 1400, height: 20 })
93
+
94
+ cache.draw((ctx) => {
95
+ // 使用 Canvas 2D 上下文绘制标尺
96
+ ctx.fillStyle = '#f6f7f9'
97
+ ctx.fillRect(0, 0, 1400, 20)
98
+ // ... 绘制刻度
99
+ })
100
+
101
+ // 将缓存绘制到主 Canvas
102
+ ctx.drawImage(cache.canvas, 0, 0)
103
+ ```
104
+
105
+ ### 标签缓存 — LabelCache
106
+
107
+ 缓存刻度文字标签,避免每帧重复测量文本。
108
+
109
+ ```ts
110
+ import { LabelCache } from '@sketch-ruler/canvas'
111
+
112
+ const labelCache = new LabelCache()
113
+
114
+ // 获取或创建标签
115
+ const label = labelCache.get('100px', '12px Arial')
116
+ // label: { text: '100px', width: number, bitmap: ImageBitmap }
117
+ ```
118
+
119
+ ### 滚轮标准化 — WheelNormalizer
120
+
121
+ 将不同浏览器/操作系统的滚轮事件统一为标准格式。
122
+
123
+ ```ts
124
+ import { normalizeWheel, getZoomDelta } from '@sketch-ruler/canvas'
125
+
126
+ canvas.addEventListener('wheel', (e) => {
127
+ const normalized = normalizeWheel(e)
128
+ // normalized: { pixelX, pixelY, spinX, spinY }
129
+
130
+ const delta = getZoomDelta(normalized, { zoomSpeed: 0.25 })
131
+ engine.zoomBy(delta, e.clientX, e.clientY)
132
+ })
133
+ ```
134
+
135
+ ### 底层适配器(自行组合事件)
136
+
137
+ 如果你不想用 `InputManager`,可以直接使用底层适配器:
138
+
139
+ ```ts
140
+ import { MouseAdapter } from '@sketch-ruler/canvas'
141
+ import type { MouseAdapterCallbacks } from '@sketch-ruler/canvas'
142
+
143
+ const adapter = new MouseAdapter(parentElement, {
144
+ onWheel: (e, normalized) => {
145
+ // 自定义滚轮处理
146
+ },
147
+ onMouseDown: (e) => {
148
+ // 自定义鼠标按下
149
+ },
150
+ onMouseMove: (e) => {
151
+ // 自定义鼠标移动
152
+ },
153
+ onMouseUp: (e) => {
154
+ // 自定义鼠标抬起
155
+ },
156
+ onMouseEnter: () => {},
157
+ onMouseLeave: () => {}
158
+ } as MouseAdapterCallbacks)
159
+
160
+ adapter.bind()
161
+ // adapter.unbind()
162
+ ```
163
+
164
+ ```ts
165
+ import { KeyboardAdapter } from '@sketch-ruler/canvas'
166
+ import type { KeyCombo } from '@sketch-ruler/canvas'
167
+
168
+ const kb = new KeyboardAdapter({
169
+ onShortcut: (combo: KeyCombo, e: KeyboardEvent) => {
170
+ // combo: 'ctrl+0' | 'ctrl+plus' | 'ctrl+minus' | 'space' | ...
171
+ console.log('shortcut', combo)
172
+ }
173
+ })
174
+
175
+ kb.bind()
176
+ // kb.unbind()
177
+ ```
178
+
179
+ ## API 概览
180
+
181
+ | 导出 | 类型 | 说明 |
182
+ |------|------|------|
183
+ | `InputManager` | 类 | 统一输入管理(滚轮/拖拽/键盘) |
184
+ | `MouseAdapter` | 类 | 鼠标事件封装(wheel/mousedown/mousemove/mouseup) |
185
+ | `KeyboardAdapter` | 类 | 键盘快捷键封装 |
186
+ | `normalizeWheel` | 函数 | 滚轮事件标准化 |
187
+ | `getZoomDelta` | 函数 | 从标准化滚轮计算缩放增量 |
188
+ | `Canvas2DRenderer` | 类 | Canvas 2D 标尺渲染器 |
189
+ | `OffscreenRulerCache` | 类 | 离屏标尺缓存 |
190
+ | `LabelCache` | 类 | 刻度标签缓存 |
191
+
192
+ ## 与 @sketch-ruler/core 的配合
193
+
194
+ ```ts
195
+ import { TransformEngine, computeScaleMarks, getTickConfig } from '@sketch-ruler/core'
196
+ import { InputManager, Canvas2DRenderer } from '@sketch-ruler/canvas'
197
+
198
+ // 1. 创建引擎
199
+ const engine = new TransformEngine({ x: 0, y: 0, scale: 1 })
200
+
201
+ // 2. 绑定输入
202
+ const input = new InputManager(engine, { zoomMode: 'pointer' })
203
+ input.bind(canvasElement)
204
+
205
+ // 3. 监听状态并渲染
206
+ engine.onUpdate((state) => {
207
+ // 更新 DOM transform
208
+ content.style.transform = `matrix(${state.scale}, 0, 0, ${state.scale}, ${state.x}, ${state.y})`
209
+
210
+ // 渲染标尺
211
+ const marks = computeScaleMarks({
212
+ scale: state.scale,
213
+ offset: state.x,
214
+ length: 1400,
215
+ thick: 20,
216
+ config: getTickConfig(state.scale)
217
+ })
218
+ renderer.render({ width: 1400, height: 20, scale: state.scale, offset: state.x, marks })
219
+ })
220
+ ```
221
+
222
+ ## License
223
+
224
+ MIT
package/lib/index.cjs ADDED
@@ -0,0 +1 @@
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});var e=class{canvas=null;ctx=null;lastFingerprint=``;drawStatic(e,t,n){let r=this.buildFingerprint(t,n);return(r!==this.lastFingerprint||!this.canvas)&&(this.rebuild(t,n),this.lastFingerprint=r),this.canvas?(e.drawImage(this.canvas,0,0),!0):!1}rebuild(e,t){let{width:n,height:r,ratio:i,palette:a,thick:o,vertical:s}=t;this.canvas||=document.createElement(`canvas`),this.canvas.width=Math.round(n*i),this.canvas.height=Math.round(r*i);let c=this.canvas.getContext(`2d`);this.ctx=c,c.setTransform(1,0,0,1,0,0),c.scale(i,i),c.clearRect(0,0,n,r),c.fillStyle=a.bgColor,c.fillRect(0,0,n,r),c.strokeStyle=a.tickColor,c.lineWidth=1,c.beginPath();for(let i of e){let e=i.position;i.isMajor?i.value===0||i.value===t.canvasSize?s?(c.moveTo(0,e),c.lineTo(n,e)):(c.moveTo(e,0),c.lineTo(e,r)):s?(c.moveTo(n,e),c.lineTo(n*.65,e)):(c.moveTo(e,r),c.lineTo(e,r*.65)):s?(c.moveTo(n,e),c.lineTo(n*.8,e)):(c.moveTo(e,r),c.lineTo(e,r*.8))}c.stroke(),c.closePath()}buildFingerprint(e,t){let{thick:n,palette:r,vertical:i,width:a,height:o}=t,s=e.length>1?e[1].value-e[0].value:0,c=1,l=0;if(e.length>=2){let t=e[0],n=e.find(e=>e.value!==t.value)??e[1];n&&n.value!==t.value&&(c=(n.position-t.position)/(n.value-t.value),l=t.position-t.value*c)}return`${n}:${r.bgColor}:${r.tickColor}:${r.labelColor}:${i}:${a}:${o}:${s}:${c.toFixed(4)}:${l.toFixed(2)}`}clear(){this.canvas=null,this.ctx=null,this.lastFingerprint=``}},t=class{cache=new Map;maxSize;constructor(e=500){this.maxSize=e}get(e,t){let n=this.hashKey(t),r=this.cache.get(n);if(r)return this.cache.delete(n),this.cache.set(n,r),r;let i=this.createEntry(e,t);return this.cache.set(n,i),this.evictIfNeeded(),i}createEntry(e,t){let{text:n,font:r,color:i}=t;e.font=r;let a=e.measureText(n),o=Math.ceil(a.width),s=Math.ceil(a.actualBoundingBoxAscent+a.actualBoundingBoxDescent)||12;if(o===0||s===0)return{canvas:document.createElement(`canvas`),width:0,height:0};let c=document.createElement(`canvas`);c.width=o+2,c.height=s+2;let l=c.getContext(`2d`);return l.font=r,l.fillStyle=i,l.textBaseline=`alphabetic`,l.fillText(n,1,1+a.actualBoundingBoxAscent),{canvas:c,width:o,height:s}}evictIfNeeded(){if(this.cache.size<=this.maxSize)return;let e=this.cache.keys().next().value;e!==void 0&&this.cache.delete(e)}hashKey(e){return`${e.text}:${e.font}:${e.color}`}clear(){this.cache.clear()}size(){return this.cache.size}},n=`#e9f7fe`,r=class{offscreenCache=new e;labelCache=new t;render(e,t,n){for(let r of t)r.type===`ruler`&&this.renderRuler(e,r,n)}renderRuler(e,t,r){let{marks:i,vertical:a,thick:o,width:s,height:c,ratio:l,palette:u,shadowStart:d,shadowLength:f,showShadowText:p}=t;e.setTransform(1,0,0,1,0,0),e.scale(l,l),e.clearRect(0,0,s,c),this.offscreenCache.drawStatic(e,i,t);let m=[];if(f&&i.length>0){let t=d??0,r=i[0],l=i.find(e=>e.value!==r.value)??r,h=l&&l.value!==r.value?(l.position-r.position)/(l.value-r.value):1,g=r.position-r.value*h,_=t*h+g,v=f*h;if(v>0&&(e.fillStyle=u.shadowColor??n,a?e.fillRect(0,_,s,v):e.fillRect(_,0,v,c),p)){this.renderShadowText(e,t,_,o,a,u,!1);let n=t+f,r=n*h+g;this.renderShadowText(e,n,r,o,a,u,!0),m.push(_,r)}}for(let n of i)if(n.isMajor&&n.label){if(m.some(e=>Math.abs(n.position-e)<o*1.5))continue;e.save(),e.fillStyle=u.labelColor,e.font=`${Math.max(11,Math.floor(o*.5)+2)}px -apple-system, "Helvetica Neue", ".SFNSDisplay-Regular", "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "WenQuanYi Micro Hei", sans-serif`,a?(n.value===0?e.translate(s*.4,n.position-3):n.value===t.canvasSize?e.translate(s*.2,n.position+32):e.translate(s*.15,n.position+14),e.rotate(-90*Math.PI/180),e.fillText(n.label,4,9)):(n.value===0?e.translate(n.position-15,c*.13):n.value===t.canvasSize?e.translate(n.position+5,c*.3):e.translate(n.position-12,c*.18),e.fillText(n.label,4,9)),e.restore()}}renderShadowText(e,t,n,r,i,a,o){e.save(),e.fillStyle=a.labelColor,e.font=`bold 12px sans-serif`,i?(e.translate(r*.6,n+(o?-8:8)),e.rotate(-Math.PI/2)):e.translate(n+(o?-8:8),r*.6),e.fillText(Math.round(t).toString(),0,0),e.restore()}destroy(){this.offscreenCache.clear(),this.labelCache.clear()}};function i(e){let t=e.deltaX,n=e.deltaY,r=e.deltaZ||0,i=e.deltaMode;return i===WheelEvent.DOM_DELTA_LINE?(t*=40,n*=40,r*=40):i===WheelEvent.DOM_DELTA_PAGE&&(t*=800,n*=800,r*=800),{deltaX:t,deltaY:n,deltaZ:r,deltaMode:i}}function a(e,t=.001){let n=i(e);return-(n.deltaY===0?n.deltaX:n.deltaY)*t}var o=class{container;callbacks;boundWheel;boundMouseDown;boundMouseMove;boundMouseUp;boundMouseEnter;boundMouseLeave;isBound=!1;constructor(e,t){this.container=e,this.callbacks=t,this.boundWheel=this.handleWheel.bind(this),this.boundMouseDown=this.handleMouseDown.bind(this),this.boundMouseMove=this.handleMouseMove.bind(this),this.boundMouseUp=this.handleMouseUp.bind(this),this.boundMouseEnter=()=>this.callbacks.onMouseEnter?.(),this.boundMouseLeave=()=>this.callbacks.onMouseLeave?.()}bind(){this.isBound||(this.isBound=!0,this.container.addEventListener(`wheel`,this.boundWheel,{passive:!1}),this.container.addEventListener(`mousedown`,this.boundMouseDown),document.addEventListener(`mousemove`,this.boundMouseMove),document.addEventListener(`mouseup`,this.boundMouseUp),this.container.addEventListener(`mouseenter`,this.boundMouseEnter),this.container.addEventListener(`mouseleave`,this.boundMouseLeave))}unbind(){this.isBound&&(this.isBound=!1,this.container.removeEventListener(`wheel`,this.boundWheel),this.container.removeEventListener(`mousedown`,this.boundMouseDown),document.removeEventListener(`mousemove`,this.boundMouseMove),document.removeEventListener(`mouseup`,this.boundMouseUp),this.container.removeEventListener(`mouseenter`,this.boundMouseEnter),this.container.removeEventListener(`mouseleave`,this.boundMouseLeave))}handleWheel(e){let t=i(e);this.callbacks.onWheel?.(e,t)}handleMouseDown(e){this.callbacks.onMouseDown?.(e)}handleMouseMove(e){this.callbacks.onMouseMove?.(e)}handleMouseUp(e){this.callbacks.onMouseUp?.(e)}},s=class{callbacks;boundKeyDown;isBound=!1;constructor(e){this.callbacks=e,this.boundKeyDown=this.handleKeyDown.bind(this)}bind(){this.isBound||(this.isBound=!0,document.addEventListener(`keydown`,this.boundKeyDown))}unbind(){this.isBound&&(this.isBound=!1,document.removeEventListener(`keydown`,this.boundKeyDown))}handleKeyDown(e){if(e.repeat)return;let t=document.activeElement;if(t?.closest(`.monaco-editor`)||t?.tagName===`INPUT`||t?.tagName===`TEXTAREA`||t?.getAttribute(`contenteditable`)===`true`)return;let n=this.parseCombo(e);this.callbacks.onShortcut?.(n,e)}parseCombo(e){let t=[];(e.ctrlKey||e.metaKey)&&t.push(`ctrl`),e.altKey&&t.push(`alt`),e.shiftKey&&t.push(`shift`);let n=e.key.toLowerCase();return n===` `&&(n=`space`),n===`+`&&(n=`plus`),n===`-`&&(n=`minus`),n===`=`&&(n=`equal`),t.push(n),t.join(`+`)}},c=class{engine;zoomStep;selfHandle;zoomMode;viewportSize;contentSize;container=null;mouseAdapter=null;isSpacePressed=!1;isDragging=!1;dragStart={x:0,y:0};lastMouse={x:0,y:0};isHovered=!1;boundKeyUp;keyboardAdapter=null;pendingWheelDelta=0;wheelRafId=null;onCursorChange=null;zoomInterceptor=null;panInterceptor=null;constructor(e,t={}){this.engine=e,this.zoomStep=t.zoomStep??.25,this.selfHandle=t.selfHandle??!1,this.zoomMode=t.zoomMode??`pointer`,this.viewportSize=t.viewportSize??{width:0,height:0},this.contentSize=t.contentSize??{width:0,height:0},this.onCursorChange=t.onCursorChange??null,this.zoomInterceptor=t.zoomInterceptor??null,this.panInterceptor=t.panInterceptor??null,this.boundKeyUp=this.handleKeyUp.bind(this)}bind(e){if(this.selfHandle)return;this.unbind(),this.container=e;let t=e.parentElement;if(!t)return;document.addEventListener(`keyup`,this.boundKeyUp);let n={onWheel:this.handleWheel.bind(this),onMouseDown:this.handleMouseDown.bind(this),onMouseMove:this.handleMouseMove.bind(this),onMouseUp:this.handleMouseUp.bind(this),onMouseEnter:()=>{this.isHovered=!0},onMouseLeave:()=>{this.isHovered=!1}};this.mouseAdapter=new o(t,n),this.mouseAdapter.bind(),this.keyboardAdapter=new s({onShortcut:this.handleShortcut.bind(this)}),this.keyboardAdapter.bind()}unbind(){this.mouseAdapter?.unbind(),this.mouseAdapter=null,this.keyboardAdapter?.unbind(),this.keyboardAdapter=null,document.removeEventListener(`keyup`,this.boundKeyUp),this.container=null}destroy(){this.wheelRafId!==null&&(cancelAnimationFrame(this.wheelRafId),this.wheelRafId=null),this.unbind()}setZoomMode(e){this.zoomMode=e}handleWheel(e){if(e.ctrlKey||e.metaKey){e.preventDefault();let t=this.container?.parentElement,n=t?t.getBoundingClientRect():new DOMRect(0,0,0,0),r,i;switch(this.zoomMode){case`viewport-center`:r=this.viewportSize.width/2,i=this.viewportSize.height/2;break;case`content-center`:{let e=this.engine.getState();r=e.x+this.contentSize.width*e.scale/2,i=e.y+this.contentSize.height*e.scale/2;break}default:r=e.clientX-n.left,i=e.clientY-n.top;break}let a=e.deltaY===0?e.deltaX:e.deltaY;this.pendingWheelDelta+=a<0?1:-1,this.wheelRafId===null&&(this.wheelRafId=requestAnimationFrame(()=>{if(this.wheelRafId=null,this.pendingWheelDelta===0)return;let e=this.engine.getState().scale,t=e*Math.exp(this.pendingWheelDelta*this.zoomStep/3);this.pendingWheelDelta=0,this.executeZoom(()=>this.engine.zoomTo(t,r,i),e,t,r,i)}))}}handleShortcut(e,t){if(!this.isHovered)return;let n=this.viewportSize.width/2,r=this.viewportSize.height/2,i=e=>{e()};switch(e){case`ctrl+0`:t.preventDefault(),i(()=>this.executeZoom(()=>this.engine.zoomTo(1,n,r),this.engine.getState().scale,1,n,r));break;case`ctrl+minus`:{t.preventDefault();let e=this.engine.getState().scale,a=e-this.zoomStep;i(()=>this.executeZoom(()=>this.engine.zoomBy(-this.zoomStep,n,r),e,a,n,r));break}case`ctrl+equal`:case`ctrl+plus`:{t.preventDefault();let e=this.engine.getState().scale,a=e+this.zoomStep;i(()=>this.executeZoom(()=>this.engine.zoomBy(this.zoomStep,n,r),e,a,n,r));break}case`ctrl+1`:{t.preventDefault();let e=this.viewportSize.width,a=this.viewportSize.height,o=this.contentSize.width,s=this.contentSize.height;if(e>0&&a>0&&o>0&&s>0){let t=e*.9/o,c=a*.9/s,l=Math.min(t,c),u=(e-o*l)/2,d=(a-s*l)/2,f=this.engine.getState().scale;i(()=>this.executeZoom(()=>this.engine.setTransform({scale:l,x:u,y:d}),f,l,n,r))}break}case`space`:this.isSpacePressed||(this.isSpacePressed=!0,t.preventDefault(),this.notifyCursorChange());break;default:break}}handleKeyUp(e){e.key===` `&&(this.isSpacePressed=!1,this.isDragging=!1,this.notifyCursorChange())}handleMouseDown(e){this.isSpacePressed&&e.button===0&&(this.isDragging=!0,this.dragStart={x:e.clientX,y:e.clientY},this.lastMouse={x:e.clientX,y:e.clientY},e.preventDefault(),this.notifyCursorChange())}async handleMouseMove(e){if(this.isDragging&&this.isSpacePressed){let t=e.clientX-this.lastMouse.x,n=e.clientY-this.lastMouse.y;(!this.panInterceptor?.beforePan||await this.panInterceptor.beforePan(t,n))&&(this.engine.panBy(t,n),this.panInterceptor?.afterPan?.(t,n)),this.lastMouse={x:e.clientX,y:e.clientY}}}handleMouseUp(){this.isDragging=!1,this.notifyCursorChange()}async executeZoom(e,t,n,r,i){(!this.zoomInterceptor?.beforeZoom||await this.zoomInterceptor.beforeZoom(t,n,r,i))&&(e(),this.zoomInterceptor?.afterZoom?.(t,this.engine.getState().scale,r,i))}getCursorClass(){return this.isSpacePressed?this.isDragging?`grabbing`:`grab`:`default`}notifyCursorChange(){this.onCursorChange?.(this.getCursorClass())}};exports.Canvas2DRenderer=r,exports.InputManager=c,exports.KeyboardAdapter=s,exports.LabelCache=t,exports.MouseAdapter=o,exports.OffscreenRulerCache=e,exports.getZoomDelta=a,exports.normalizeWheel=i;
package/lib/index.d.ts ADDED
@@ -0,0 +1,13 @@
1
+ export type { Renderer, Rect, RulerRenderPayload, RenderItem } from './renderers/types';
2
+ export { Canvas2DRenderer } from './renderers/canvas-2d-renderer';
3
+ export { OffscreenRulerCache } from './renderers/offscreen-ruler-cache';
4
+ export { LabelCache } from './renderers/label-cache';
5
+ export { InputManager } from './input/input-manager';
6
+ export { MouseAdapter } from './input/mouse-adapter';
7
+ export { KeyboardAdapter } from './input/keyboard-adapter';
8
+ export { normalizeWheel, getZoomDelta } from './input/wheel-normalizer';
9
+ export type { NormalizedWheel } from './input/wheel-normalizer';
10
+ export type { InputManagerOptions, ZoomMode } from './input/input-manager';
11
+ export type { MouseAdapterCallbacks } from './input/mouse-adapter';
12
+ export type { KeyCombo } from './input/keyboard-adapter';
13
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,YAAY,EAAE,QAAQ,EAAE,IAAI,EAAE,kBAAkB,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AACvF,OAAO,EAAE,gBAAgB,EAAE,MAAM,gCAAgC,CAAA;AACjE,OAAO,EAAE,mBAAmB,EAAE,MAAM,mCAAmC,CAAA;AACvE,OAAO,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAA;AAGpD,OAAO,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAA;AACpD,OAAO,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAA;AACpD,OAAO,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAA;AAC1D,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAA;AACvE,YAAY,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAA;AAC/D,YAAY,EAAE,mBAAmB,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAA;AAC1E,YAAY,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAA;AAClE,YAAY,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAA"}
@@ -0,0 +1 @@
1
+ var SketchRulerCanvas=(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:`Module`});var t=class{canvas=null;ctx=null;lastFingerprint=``;drawStatic(e,t,n){let r=this.buildFingerprint(t,n);return(r!==this.lastFingerprint||!this.canvas)&&(this.rebuild(t,n),this.lastFingerprint=r),this.canvas?(e.drawImage(this.canvas,0,0),!0):!1}rebuild(e,t){let{width:n,height:r,ratio:i,palette:a,thick:o,vertical:s}=t;this.canvas||=document.createElement(`canvas`),this.canvas.width=Math.round(n*i),this.canvas.height=Math.round(r*i);let c=this.canvas.getContext(`2d`);this.ctx=c,c.setTransform(1,0,0,1,0,0),c.scale(i,i),c.clearRect(0,0,n,r),c.fillStyle=a.bgColor,c.fillRect(0,0,n,r),c.strokeStyle=a.tickColor,c.lineWidth=1,c.beginPath();for(let i of e){let e=i.position;i.isMajor?i.value===0||i.value===t.canvasSize?s?(c.moveTo(0,e),c.lineTo(n,e)):(c.moveTo(e,0),c.lineTo(e,r)):s?(c.moveTo(n,e),c.lineTo(n*.65,e)):(c.moveTo(e,r),c.lineTo(e,r*.65)):s?(c.moveTo(n,e),c.lineTo(n*.8,e)):(c.moveTo(e,r),c.lineTo(e,r*.8))}c.stroke(),c.closePath()}buildFingerprint(e,t){let{thick:n,palette:r,vertical:i,width:a,height:o}=t,s=e.length>1?e[1].value-e[0].value:0,c=1,l=0;if(e.length>=2){let t=e[0],n=e.find(e=>e.value!==t.value)??e[1];n&&n.value!==t.value&&(c=(n.position-t.position)/(n.value-t.value),l=t.position-t.value*c)}return`${n}:${r.bgColor}:${r.tickColor}:${r.labelColor}:${i}:${a}:${o}:${s}:${c.toFixed(4)}:${l.toFixed(2)}`}clear(){this.canvas=null,this.ctx=null,this.lastFingerprint=``}},n=class{cache=new Map;maxSize;constructor(e=500){this.maxSize=e}get(e,t){let n=this.hashKey(t),r=this.cache.get(n);if(r)return this.cache.delete(n),this.cache.set(n,r),r;let i=this.createEntry(e,t);return this.cache.set(n,i),this.evictIfNeeded(),i}createEntry(e,t){let{text:n,font:r,color:i}=t;e.font=r;let a=e.measureText(n),o=Math.ceil(a.width),s=Math.ceil(a.actualBoundingBoxAscent+a.actualBoundingBoxDescent)||12;if(o===0||s===0)return{canvas:document.createElement(`canvas`),width:0,height:0};let c=document.createElement(`canvas`);c.width=o+2,c.height=s+2;let l=c.getContext(`2d`);return l.font=r,l.fillStyle=i,l.textBaseline=`alphabetic`,l.fillText(n,1,1+a.actualBoundingBoxAscent),{canvas:c,width:o,height:s}}evictIfNeeded(){if(this.cache.size<=this.maxSize)return;let e=this.cache.keys().next().value;e!==void 0&&this.cache.delete(e)}hashKey(e){return`${e.text}:${e.font}:${e.color}`}clear(){this.cache.clear()}size(){return this.cache.size}},r=`#e9f7fe`,i=class{offscreenCache=new t;labelCache=new n;render(e,t,n){for(let r of t)r.type===`ruler`&&this.renderRuler(e,r,n)}renderRuler(e,t,n){let{marks:i,vertical:a,thick:o,width:s,height:c,ratio:l,palette:u,shadowStart:d,shadowLength:f,showShadowText:p}=t;e.setTransform(1,0,0,1,0,0),e.scale(l,l),e.clearRect(0,0,s,c),this.offscreenCache.drawStatic(e,i,t);let m=[];if(f&&i.length>0){let t=d??0,n=i[0],l=i.find(e=>e.value!==n.value)??n,h=l&&l.value!==n.value?(l.position-n.position)/(l.value-n.value):1,g=n.position-n.value*h,_=t*h+g,v=f*h;if(v>0&&(e.fillStyle=u.shadowColor??r,a?e.fillRect(0,_,s,v):e.fillRect(_,0,v,c),p)){this.renderShadowText(e,t,_,o,a,u,!1);let n=t+f,r=n*h+g;this.renderShadowText(e,n,r,o,a,u,!0),m.push(_,r)}}for(let n of i)if(n.isMajor&&n.label){if(m.some(e=>Math.abs(n.position-e)<o*1.5))continue;e.save(),e.fillStyle=u.labelColor,e.font=`${Math.max(11,Math.floor(o*.5)+2)}px -apple-system, "Helvetica Neue", ".SFNSDisplay-Regular", "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "WenQuanYi Micro Hei", sans-serif`,a?(n.value===0?e.translate(s*.4,n.position-3):n.value===t.canvasSize?e.translate(s*.2,n.position+32):e.translate(s*.15,n.position+14),e.rotate(-90*Math.PI/180),e.fillText(n.label,4,9)):(n.value===0?e.translate(n.position-15,c*.13):n.value===t.canvasSize?e.translate(n.position+5,c*.3):e.translate(n.position-12,c*.18),e.fillText(n.label,4,9)),e.restore()}}renderShadowText(e,t,n,r,i,a,o){e.save(),e.fillStyle=a.labelColor,e.font=`bold 12px sans-serif`,i?(e.translate(r*.6,n+(o?-8:8)),e.rotate(-Math.PI/2)):e.translate(n+(o?-8:8),r*.6),e.fillText(Math.round(t).toString(),0,0),e.restore()}destroy(){this.offscreenCache.clear(),this.labelCache.clear()}};function a(e){let t=e.deltaX,n=e.deltaY,r=e.deltaZ||0,i=e.deltaMode;return i===WheelEvent.DOM_DELTA_LINE?(t*=40,n*=40,r*=40):i===WheelEvent.DOM_DELTA_PAGE&&(t*=800,n*=800,r*=800),{deltaX:t,deltaY:n,deltaZ:r,deltaMode:i}}function o(e,t=.001){let n=a(e);return-(n.deltaY===0?n.deltaX:n.deltaY)*t}var s=class{container;callbacks;boundWheel;boundMouseDown;boundMouseMove;boundMouseUp;boundMouseEnter;boundMouseLeave;isBound=!1;constructor(e,t){this.container=e,this.callbacks=t,this.boundWheel=this.handleWheel.bind(this),this.boundMouseDown=this.handleMouseDown.bind(this),this.boundMouseMove=this.handleMouseMove.bind(this),this.boundMouseUp=this.handleMouseUp.bind(this),this.boundMouseEnter=()=>this.callbacks.onMouseEnter?.(),this.boundMouseLeave=()=>this.callbacks.onMouseLeave?.()}bind(){this.isBound||(this.isBound=!0,this.container.addEventListener(`wheel`,this.boundWheel,{passive:!1}),this.container.addEventListener(`mousedown`,this.boundMouseDown),document.addEventListener(`mousemove`,this.boundMouseMove),document.addEventListener(`mouseup`,this.boundMouseUp),this.container.addEventListener(`mouseenter`,this.boundMouseEnter),this.container.addEventListener(`mouseleave`,this.boundMouseLeave))}unbind(){this.isBound&&(this.isBound=!1,this.container.removeEventListener(`wheel`,this.boundWheel),this.container.removeEventListener(`mousedown`,this.boundMouseDown),document.removeEventListener(`mousemove`,this.boundMouseMove),document.removeEventListener(`mouseup`,this.boundMouseUp),this.container.removeEventListener(`mouseenter`,this.boundMouseEnter),this.container.removeEventListener(`mouseleave`,this.boundMouseLeave))}handleWheel(e){let t=a(e);this.callbacks.onWheel?.(e,t)}handleMouseDown(e){this.callbacks.onMouseDown?.(e)}handleMouseMove(e){this.callbacks.onMouseMove?.(e)}handleMouseUp(e){this.callbacks.onMouseUp?.(e)}},c=class{callbacks;boundKeyDown;isBound=!1;constructor(e){this.callbacks=e,this.boundKeyDown=this.handleKeyDown.bind(this)}bind(){this.isBound||(this.isBound=!0,document.addEventListener(`keydown`,this.boundKeyDown))}unbind(){this.isBound&&(this.isBound=!1,document.removeEventListener(`keydown`,this.boundKeyDown))}handleKeyDown(e){if(e.repeat)return;let t=document.activeElement;if(t?.closest(`.monaco-editor`)||t?.tagName===`INPUT`||t?.tagName===`TEXTAREA`||t?.getAttribute(`contenteditable`)===`true`)return;let n=this.parseCombo(e);this.callbacks.onShortcut?.(n,e)}parseCombo(e){let t=[];(e.ctrlKey||e.metaKey)&&t.push(`ctrl`),e.altKey&&t.push(`alt`),e.shiftKey&&t.push(`shift`);let n=e.key.toLowerCase();return n===` `&&(n=`space`),n===`+`&&(n=`plus`),n===`-`&&(n=`minus`),n===`=`&&(n=`equal`),t.push(n),t.join(`+`)}},l=class{engine;zoomStep;selfHandle;zoomMode;viewportSize;contentSize;container=null;mouseAdapter=null;isSpacePressed=!1;isDragging=!1;dragStart={x:0,y:0};lastMouse={x:0,y:0};isHovered=!1;boundKeyUp;keyboardAdapter=null;pendingWheelDelta=0;wheelRafId=null;onCursorChange=null;zoomInterceptor=null;panInterceptor=null;constructor(e,t={}){this.engine=e,this.zoomStep=t.zoomStep??.25,this.selfHandle=t.selfHandle??!1,this.zoomMode=t.zoomMode??`pointer`,this.viewportSize=t.viewportSize??{width:0,height:0},this.contentSize=t.contentSize??{width:0,height:0},this.onCursorChange=t.onCursorChange??null,this.zoomInterceptor=t.zoomInterceptor??null,this.panInterceptor=t.panInterceptor??null,this.boundKeyUp=this.handleKeyUp.bind(this)}bind(e){if(this.selfHandle)return;this.unbind(),this.container=e;let t=e.parentElement;if(!t)return;document.addEventListener(`keyup`,this.boundKeyUp);let n={onWheel:this.handleWheel.bind(this),onMouseDown:this.handleMouseDown.bind(this),onMouseMove:this.handleMouseMove.bind(this),onMouseUp:this.handleMouseUp.bind(this),onMouseEnter:()=>{this.isHovered=!0},onMouseLeave:()=>{this.isHovered=!1}};this.mouseAdapter=new s(t,n),this.mouseAdapter.bind(),this.keyboardAdapter=new c({onShortcut:this.handleShortcut.bind(this)}),this.keyboardAdapter.bind()}unbind(){this.mouseAdapter?.unbind(),this.mouseAdapter=null,this.keyboardAdapter?.unbind(),this.keyboardAdapter=null,document.removeEventListener(`keyup`,this.boundKeyUp),this.container=null}destroy(){this.wheelRafId!==null&&(cancelAnimationFrame(this.wheelRafId),this.wheelRafId=null),this.unbind()}setZoomMode(e){this.zoomMode=e}handleWheel(e){if(e.ctrlKey||e.metaKey){e.preventDefault();let t=this.container?.parentElement,n=t?t.getBoundingClientRect():new DOMRect(0,0,0,0),r,i;switch(this.zoomMode){case`viewport-center`:r=this.viewportSize.width/2,i=this.viewportSize.height/2;break;case`content-center`:{let e=this.engine.getState();r=e.x+this.contentSize.width*e.scale/2,i=e.y+this.contentSize.height*e.scale/2;break}default:r=e.clientX-n.left,i=e.clientY-n.top;break}let a=e.deltaY===0?e.deltaX:e.deltaY;this.pendingWheelDelta+=a<0?1:-1,this.wheelRafId===null&&(this.wheelRafId=requestAnimationFrame(()=>{if(this.wheelRafId=null,this.pendingWheelDelta===0)return;let e=this.engine.getState().scale,t=e*Math.exp(this.pendingWheelDelta*this.zoomStep/3);this.pendingWheelDelta=0,this.executeZoom(()=>this.engine.zoomTo(t,r,i),e,t,r,i)}))}}handleShortcut(e,t){if(!this.isHovered)return;let n=this.viewportSize.width/2,r=this.viewportSize.height/2,i=e=>{e()};switch(e){case`ctrl+0`:t.preventDefault(),i(()=>this.executeZoom(()=>this.engine.zoomTo(1,n,r),this.engine.getState().scale,1,n,r));break;case`ctrl+minus`:{t.preventDefault();let e=this.engine.getState().scale,a=e-this.zoomStep;i(()=>this.executeZoom(()=>this.engine.zoomBy(-this.zoomStep,n,r),e,a,n,r));break}case`ctrl+equal`:case`ctrl+plus`:{t.preventDefault();let e=this.engine.getState().scale,a=e+this.zoomStep;i(()=>this.executeZoom(()=>this.engine.zoomBy(this.zoomStep,n,r),e,a,n,r));break}case`ctrl+1`:{t.preventDefault();let e=this.viewportSize.width,a=this.viewportSize.height,o=this.contentSize.width,s=this.contentSize.height;if(e>0&&a>0&&o>0&&s>0){let t=e*.9/o,c=a*.9/s,l=Math.min(t,c),u=(e-o*l)/2,d=(a-s*l)/2,f=this.engine.getState().scale;i(()=>this.executeZoom(()=>this.engine.setTransform({scale:l,x:u,y:d}),f,l,n,r))}break}case`space`:this.isSpacePressed||(this.isSpacePressed=!0,t.preventDefault(),this.notifyCursorChange());break;default:break}}handleKeyUp(e){e.key===` `&&(this.isSpacePressed=!1,this.isDragging=!1,this.notifyCursorChange())}handleMouseDown(e){this.isSpacePressed&&e.button===0&&(this.isDragging=!0,this.dragStart={x:e.clientX,y:e.clientY},this.lastMouse={x:e.clientX,y:e.clientY},e.preventDefault(),this.notifyCursorChange())}async handleMouseMove(e){if(this.isDragging&&this.isSpacePressed){let t=e.clientX-this.lastMouse.x,n=e.clientY-this.lastMouse.y;(!this.panInterceptor?.beforePan||await this.panInterceptor.beforePan(t,n))&&(this.engine.panBy(t,n),this.panInterceptor?.afterPan?.(t,n)),this.lastMouse={x:e.clientX,y:e.clientY}}}handleMouseUp(){this.isDragging=!1,this.notifyCursorChange()}async executeZoom(e,t,n,r,i){(!this.zoomInterceptor?.beforeZoom||await this.zoomInterceptor.beforeZoom(t,n,r,i))&&(e(),this.zoomInterceptor?.afterZoom?.(t,this.engine.getState().scale,r,i))}getCursorClass(){return this.isSpacePressed?this.isDragging?`grabbing`:`grab`:`default`}notifyCursorChange(){this.onCursorChange?.(this.getCursorClass())}};return e.Canvas2DRenderer=i,e.InputManager=l,e.KeyboardAdapter=c,e.LabelCache=n,e.MouseAdapter=s,e.OffscreenRulerCache=t,e.getZoomDelta=o,e.normalizeWheel=a,e})({});
package/lib/index.js ADDED
@@ -0,0 +1,348 @@
1
+ /*!@sketch-ruler/canvas v3.0.0-beta.0 2026-5-21*/
2
+ //#region src/renderers/offscreen-ruler-cache.ts
3
+ var e = class {
4
+ canvas = null;
5
+ ctx = null;
6
+ lastFingerprint = "";
7
+ drawStatic(e, t, n) {
8
+ let r = this.buildFingerprint(t, n);
9
+ return (r !== this.lastFingerprint || !this.canvas) && (this.rebuild(t, n), this.lastFingerprint = r), this.canvas ? (e.drawImage(this.canvas, 0, 0), !0) : !1;
10
+ }
11
+ rebuild(e, t) {
12
+ let { width: n, height: r, ratio: i, palette: a, thick: o, vertical: s } = t;
13
+ this.canvas ||= document.createElement("canvas"), this.canvas.width = Math.round(n * i), this.canvas.height = Math.round(r * i);
14
+ let c = this.canvas.getContext("2d");
15
+ this.ctx = c, c.setTransform(1, 0, 0, 1, 0, 0), c.scale(i, i), c.clearRect(0, 0, n, r), c.fillStyle = a.bgColor, c.fillRect(0, 0, n, r), c.strokeStyle = a.tickColor, c.lineWidth = 1, c.beginPath();
16
+ for (let i of e) {
17
+ let e = i.position;
18
+ i.isMajor ? i.value === 0 || i.value === t.canvasSize ? s ? (c.moveTo(0, e), c.lineTo(n, e)) : (c.moveTo(e, 0), c.lineTo(e, r)) : s ? (c.moveTo(n, e), c.lineTo(n * .65, e)) : (c.moveTo(e, r), c.lineTo(e, r * .65)) : s ? (c.moveTo(n, e), c.lineTo(n * .8, e)) : (c.moveTo(e, r), c.lineTo(e, r * .8));
19
+ }
20
+ c.stroke(), c.closePath();
21
+ }
22
+ buildFingerprint(e, t) {
23
+ let { thick: n, palette: r, vertical: i, width: a, height: o } = t, s = e.length > 1 ? e[1].value - e[0].value : 0, c = 1, l = 0;
24
+ if (e.length >= 2) {
25
+ let t = e[0], n = e.find((e) => e.value !== t.value) ?? e[1];
26
+ n && n.value !== t.value && (c = (n.position - t.position) / (n.value - t.value), l = t.position - t.value * c);
27
+ }
28
+ return `${n}:${r.bgColor}:${r.tickColor}:${r.labelColor}:${i}:${a}:${o}:${s}:${c.toFixed(4)}:${l.toFixed(2)}`;
29
+ }
30
+ clear() {
31
+ this.canvas = null, this.ctx = null, this.lastFingerprint = "";
32
+ }
33
+ }, t = class {
34
+ cache = /* @__PURE__ */ new Map();
35
+ maxSize;
36
+ constructor(e = 500) {
37
+ this.maxSize = e;
38
+ }
39
+ get(e, t) {
40
+ let n = this.hashKey(t), r = this.cache.get(n);
41
+ if (r) return this.cache.delete(n), this.cache.set(n, r), r;
42
+ let i = this.createEntry(e, t);
43
+ return this.cache.set(n, i), this.evictIfNeeded(), i;
44
+ }
45
+ createEntry(e, t) {
46
+ let { text: n, font: r, color: i } = t;
47
+ e.font = r;
48
+ let a = e.measureText(n), o = Math.ceil(a.width), s = Math.ceil(a.actualBoundingBoxAscent + a.actualBoundingBoxDescent) || 12;
49
+ if (o === 0 || s === 0) return {
50
+ canvas: document.createElement("canvas"),
51
+ width: 0,
52
+ height: 0
53
+ };
54
+ let c = document.createElement("canvas");
55
+ c.width = o + 2, c.height = s + 2;
56
+ let l = c.getContext("2d");
57
+ return l.font = r, l.fillStyle = i, l.textBaseline = "alphabetic", l.fillText(n, 1, 1 + a.actualBoundingBoxAscent), {
58
+ canvas: c,
59
+ width: o,
60
+ height: s
61
+ };
62
+ }
63
+ evictIfNeeded() {
64
+ if (this.cache.size <= this.maxSize) return;
65
+ let e = this.cache.keys().next().value;
66
+ e !== void 0 && this.cache.delete(e);
67
+ }
68
+ hashKey(e) {
69
+ return `${e.text}:${e.font}:${e.color}`;
70
+ }
71
+ clear() {
72
+ this.cache.clear();
73
+ }
74
+ size() {
75
+ return this.cache.size;
76
+ }
77
+ }, n = "#e9f7fe", r = class {
78
+ offscreenCache = new e();
79
+ labelCache = new t();
80
+ render(e, t, n) {
81
+ for (let r of t) r.type === "ruler" && this.renderRuler(e, r, n);
82
+ }
83
+ renderRuler(e, t, r) {
84
+ let { marks: i, vertical: a, thick: o, width: s, height: c, ratio: l, palette: u, shadowStart: d, shadowLength: f, showShadowText: p } = t;
85
+ e.setTransform(1, 0, 0, 1, 0, 0), e.scale(l, l), e.clearRect(0, 0, s, c), this.offscreenCache.drawStatic(e, i, t);
86
+ let m = [];
87
+ if (f && i.length > 0) {
88
+ let t = d ?? 0, r = i[0], l = i.find((e) => e.value !== r.value) ?? r, h = l && l.value !== r.value ? (l.position - r.position) / (l.value - r.value) : 1, g = r.position - r.value * h, _ = t * h + g, v = f * h;
89
+ if (v > 0 && (e.fillStyle = u.shadowColor ?? n, a ? e.fillRect(0, _, s, v) : e.fillRect(_, 0, v, c), p)) {
90
+ this.renderShadowText(e, t, _, o, a, u, !1);
91
+ let n = t + f, r = n * h + g;
92
+ this.renderShadowText(e, n, r, o, a, u, !0), m.push(_, r);
93
+ }
94
+ }
95
+ for (let n of i) if (n.isMajor && n.label) {
96
+ if (m.some((e) => Math.abs(n.position - e) < o * 1.5)) continue;
97
+ e.save(), e.fillStyle = u.labelColor, e.font = `${Math.max(11, Math.floor(o * .5) + 2)}px -apple-system, "Helvetica Neue", ".SFNSDisplay-Regular", "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "WenQuanYi Micro Hei", sans-serif`, a ? (n.value === 0 ? e.translate(s * .4, n.position - 3) : n.value === t.canvasSize ? e.translate(s * .2, n.position + 32) : e.translate(s * .15, n.position + 14), e.rotate(-90 * Math.PI / 180), e.fillText(n.label, 4, 9)) : (n.value === 0 ? e.translate(n.position - 15, c * .13) : n.value === t.canvasSize ? e.translate(n.position + 5, c * .3) : e.translate(n.position - 12, c * .18), e.fillText(n.label, 4, 9)), e.restore();
98
+ }
99
+ }
100
+ renderShadowText(e, t, n, r, i, a, o) {
101
+ e.save(), e.fillStyle = a.labelColor, e.font = "bold 12px sans-serif", i ? (e.translate(r * .6, n + (o ? -8 : 8)), e.rotate(-Math.PI / 2)) : e.translate(n + (o ? -8 : 8), r * .6), e.fillText(Math.round(t).toString(), 0, 0), e.restore();
102
+ }
103
+ destroy() {
104
+ this.offscreenCache.clear(), this.labelCache.clear();
105
+ }
106
+ };
107
+ //#endregion
108
+ //#region src/input/wheel-normalizer.ts
109
+ function i(e) {
110
+ let t = e.deltaX, n = e.deltaY, r = e.deltaZ || 0, i = e.deltaMode;
111
+ return i === WheelEvent.DOM_DELTA_LINE ? (t *= 40, n *= 40, r *= 40) : i === WheelEvent.DOM_DELTA_PAGE && (t *= 800, n *= 800, r *= 800), {
112
+ deltaX: t,
113
+ deltaY: n,
114
+ deltaZ: r,
115
+ deltaMode: i
116
+ };
117
+ }
118
+ function a(e, t = .001) {
119
+ let n = i(e);
120
+ return -(n.deltaY === 0 ? n.deltaX : n.deltaY) * t;
121
+ }
122
+ //#endregion
123
+ //#region src/input/mouse-adapter.ts
124
+ var o = class {
125
+ container;
126
+ callbacks;
127
+ boundWheel;
128
+ boundMouseDown;
129
+ boundMouseMove;
130
+ boundMouseUp;
131
+ boundMouseEnter;
132
+ boundMouseLeave;
133
+ isBound = !1;
134
+ constructor(e, t) {
135
+ this.container = e, this.callbacks = t, this.boundWheel = this.handleWheel.bind(this), this.boundMouseDown = this.handleMouseDown.bind(this), this.boundMouseMove = this.handleMouseMove.bind(this), this.boundMouseUp = this.handleMouseUp.bind(this), this.boundMouseEnter = () => this.callbacks.onMouseEnter?.(), this.boundMouseLeave = () => this.callbacks.onMouseLeave?.();
136
+ }
137
+ bind() {
138
+ this.isBound || (this.isBound = !0, this.container.addEventListener("wheel", this.boundWheel, { passive: !1 }), this.container.addEventListener("mousedown", this.boundMouseDown), document.addEventListener("mousemove", this.boundMouseMove), document.addEventListener("mouseup", this.boundMouseUp), this.container.addEventListener("mouseenter", this.boundMouseEnter), this.container.addEventListener("mouseleave", this.boundMouseLeave));
139
+ }
140
+ unbind() {
141
+ this.isBound && (this.isBound = !1, this.container.removeEventListener("wheel", this.boundWheel), this.container.removeEventListener("mousedown", this.boundMouseDown), document.removeEventListener("mousemove", this.boundMouseMove), document.removeEventListener("mouseup", this.boundMouseUp), this.container.removeEventListener("mouseenter", this.boundMouseEnter), this.container.removeEventListener("mouseleave", this.boundMouseLeave));
142
+ }
143
+ handleWheel(e) {
144
+ let t = i(e);
145
+ this.callbacks.onWheel?.(e, t);
146
+ }
147
+ handleMouseDown(e) {
148
+ this.callbacks.onMouseDown?.(e);
149
+ }
150
+ handleMouseMove(e) {
151
+ this.callbacks.onMouseMove?.(e);
152
+ }
153
+ handleMouseUp(e) {
154
+ this.callbacks.onMouseUp?.(e);
155
+ }
156
+ }, s = class {
157
+ callbacks;
158
+ boundKeyDown;
159
+ isBound = !1;
160
+ constructor(e) {
161
+ this.callbacks = e, this.boundKeyDown = this.handleKeyDown.bind(this);
162
+ }
163
+ bind() {
164
+ this.isBound || (this.isBound = !0, document.addEventListener("keydown", this.boundKeyDown));
165
+ }
166
+ unbind() {
167
+ this.isBound && (this.isBound = !1, document.removeEventListener("keydown", this.boundKeyDown));
168
+ }
169
+ handleKeyDown(e) {
170
+ if (e.repeat) return;
171
+ let t = document.activeElement;
172
+ if (t?.closest(".monaco-editor") || t?.tagName === "INPUT" || t?.tagName === "TEXTAREA" || t?.getAttribute("contenteditable") === "true") return;
173
+ let n = this.parseCombo(e);
174
+ this.callbacks.onShortcut?.(n, e);
175
+ }
176
+ parseCombo(e) {
177
+ let t = [];
178
+ (e.ctrlKey || e.metaKey) && t.push("ctrl"), e.altKey && t.push("alt"), e.shiftKey && t.push("shift");
179
+ let n = e.key.toLowerCase();
180
+ return n === " " && (n = "space"), n === "+" && (n = "plus"), n === "-" && (n = "minus"), n === "=" && (n = "equal"), t.push(n), t.join("+");
181
+ }
182
+ }, c = class {
183
+ engine;
184
+ zoomStep;
185
+ selfHandle;
186
+ zoomMode;
187
+ viewportSize;
188
+ contentSize;
189
+ container = null;
190
+ mouseAdapter = null;
191
+ isSpacePressed = !1;
192
+ isDragging = !1;
193
+ dragStart = {
194
+ x: 0,
195
+ y: 0
196
+ };
197
+ lastMouse = {
198
+ x: 0,
199
+ y: 0
200
+ };
201
+ isHovered = !1;
202
+ boundKeyUp;
203
+ keyboardAdapter = null;
204
+ pendingWheelDelta = 0;
205
+ wheelRafId = null;
206
+ onCursorChange = null;
207
+ zoomInterceptor = null;
208
+ panInterceptor = null;
209
+ constructor(e, t = {}) {
210
+ this.engine = e, this.zoomStep = t.zoomStep ?? .25, this.selfHandle = t.selfHandle ?? !1, this.zoomMode = t.zoomMode ?? "pointer", this.viewportSize = t.viewportSize ?? {
211
+ width: 0,
212
+ height: 0
213
+ }, this.contentSize = t.contentSize ?? {
214
+ width: 0,
215
+ height: 0
216
+ }, this.onCursorChange = t.onCursorChange ?? null, this.zoomInterceptor = t.zoomInterceptor ?? null, this.panInterceptor = t.panInterceptor ?? null, this.boundKeyUp = this.handleKeyUp.bind(this);
217
+ }
218
+ bind(e) {
219
+ if (this.selfHandle) return;
220
+ this.unbind(), this.container = e;
221
+ let t = e.parentElement;
222
+ if (!t) return;
223
+ document.addEventListener("keyup", this.boundKeyUp);
224
+ let n = {
225
+ onWheel: this.handleWheel.bind(this),
226
+ onMouseDown: this.handleMouseDown.bind(this),
227
+ onMouseMove: this.handleMouseMove.bind(this),
228
+ onMouseUp: this.handleMouseUp.bind(this),
229
+ onMouseEnter: () => {
230
+ this.isHovered = !0;
231
+ },
232
+ onMouseLeave: () => {
233
+ this.isHovered = !1;
234
+ }
235
+ };
236
+ this.mouseAdapter = new o(t, n), this.mouseAdapter.bind(), this.keyboardAdapter = new s({ onShortcut: this.handleShortcut.bind(this) }), this.keyboardAdapter.bind();
237
+ }
238
+ unbind() {
239
+ this.mouseAdapter?.unbind(), this.mouseAdapter = null, this.keyboardAdapter?.unbind(), this.keyboardAdapter = null, document.removeEventListener("keyup", this.boundKeyUp), this.container = null;
240
+ }
241
+ destroy() {
242
+ this.wheelRafId !== null && (cancelAnimationFrame(this.wheelRafId), this.wheelRafId = null), this.unbind();
243
+ }
244
+ setZoomMode(e) {
245
+ this.zoomMode = e;
246
+ }
247
+ handleWheel(e) {
248
+ if (e.ctrlKey || e.metaKey) {
249
+ e.preventDefault();
250
+ let t = this.container?.parentElement, n = t ? t.getBoundingClientRect() : new DOMRect(0, 0, 0, 0), r, i;
251
+ switch (this.zoomMode) {
252
+ case "viewport-center":
253
+ r = this.viewportSize.width / 2, i = this.viewportSize.height / 2;
254
+ break;
255
+ case "content-center": {
256
+ let e = this.engine.getState();
257
+ r = e.x + this.contentSize.width * e.scale / 2, i = e.y + this.contentSize.height * e.scale / 2;
258
+ break;
259
+ }
260
+ default:
261
+ r = e.clientX - n.left, i = e.clientY - n.top;
262
+ break;
263
+ }
264
+ let a = e.deltaY === 0 ? e.deltaX : e.deltaY;
265
+ this.pendingWheelDelta += a < 0 ? 1 : -1, this.wheelRafId === null && (this.wheelRafId = requestAnimationFrame(() => {
266
+ if (this.wheelRafId = null, this.pendingWheelDelta === 0) return;
267
+ let e = this.engine.getState().scale, t = e * Math.exp(this.pendingWheelDelta * this.zoomStep / 3);
268
+ this.pendingWheelDelta = 0, this.executeZoom(() => this.engine.zoomTo(t, r, i), e, t, r, i);
269
+ }));
270
+ }
271
+ }
272
+ handleShortcut(e, t) {
273
+ if (!this.isHovered) return;
274
+ let n = this.viewportSize.width / 2, r = this.viewportSize.height / 2, i = (e) => {
275
+ e();
276
+ };
277
+ switch (e) {
278
+ case "ctrl+0":
279
+ t.preventDefault(), i(() => this.executeZoom(() => this.engine.zoomTo(1, n, r), this.engine.getState().scale, 1, n, r));
280
+ break;
281
+ case "ctrl+minus": {
282
+ t.preventDefault();
283
+ let e = this.engine.getState().scale, a = e - this.zoomStep;
284
+ i(() => this.executeZoom(() => this.engine.zoomBy(-this.zoomStep, n, r), e, a, n, r));
285
+ break;
286
+ }
287
+ case "ctrl+equal":
288
+ case "ctrl+plus": {
289
+ t.preventDefault();
290
+ let e = this.engine.getState().scale, a = e + this.zoomStep;
291
+ i(() => this.executeZoom(() => this.engine.zoomBy(this.zoomStep, n, r), e, a, n, r));
292
+ break;
293
+ }
294
+ case "ctrl+1": {
295
+ t.preventDefault();
296
+ let e = this.viewportSize.width, a = this.viewportSize.height, o = this.contentSize.width, s = this.contentSize.height;
297
+ if (e > 0 && a > 0 && o > 0 && s > 0) {
298
+ let t = e * .9 / o, c = a * .9 / s, l = Math.min(t, c), u = (e - o * l) / 2, d = (a - s * l) / 2, f = this.engine.getState().scale;
299
+ i(() => this.executeZoom(() => this.engine.setTransform({
300
+ scale: l,
301
+ x: u,
302
+ y: d
303
+ }), f, l, n, r));
304
+ }
305
+ break;
306
+ }
307
+ case "space":
308
+ this.isSpacePressed || (this.isSpacePressed = !0, t.preventDefault(), this.notifyCursorChange());
309
+ break;
310
+ default: break;
311
+ }
312
+ }
313
+ handleKeyUp(e) {
314
+ e.key === " " && (this.isSpacePressed = !1, this.isDragging = !1, this.notifyCursorChange());
315
+ }
316
+ handleMouseDown(e) {
317
+ this.isSpacePressed && e.button === 0 && (this.isDragging = !0, this.dragStart = {
318
+ x: e.clientX,
319
+ y: e.clientY
320
+ }, this.lastMouse = {
321
+ x: e.clientX,
322
+ y: e.clientY
323
+ }, e.preventDefault(), this.notifyCursorChange());
324
+ }
325
+ async handleMouseMove(e) {
326
+ if (this.isDragging && this.isSpacePressed) {
327
+ let t = e.clientX - this.lastMouse.x, n = e.clientY - this.lastMouse.y;
328
+ (!this.panInterceptor?.beforePan || await this.panInterceptor.beforePan(t, n)) && (this.engine.panBy(t, n), this.panInterceptor?.afterPan?.(t, n)), this.lastMouse = {
329
+ x: e.clientX,
330
+ y: e.clientY
331
+ };
332
+ }
333
+ }
334
+ handleMouseUp() {
335
+ this.isDragging = !1, this.notifyCursorChange();
336
+ }
337
+ async executeZoom(e, t, n, r, i) {
338
+ (!this.zoomInterceptor?.beforeZoom || await this.zoomInterceptor.beforeZoom(t, n, r, i)) && (e(), this.zoomInterceptor?.afterZoom?.(t, this.engine.getState().scale, r, i));
339
+ }
340
+ getCursorClass() {
341
+ return this.isSpacePressed ? this.isDragging ? "grabbing" : "grab" : "default";
342
+ }
343
+ notifyCursorChange() {
344
+ this.onCursorChange?.(this.getCursorClass());
345
+ }
346
+ };
347
+ //#endregion
348
+ export { r as Canvas2DRenderer, c as InputManager, s as KeyboardAdapter, t as LabelCache, o as MouseAdapter, e as OffscreenRulerCache, a as getZoomDelta, i as normalizeWheel };
@@ -0,0 +1 @@
1
+ (function(e,t){typeof exports==`object`&&typeof module<`u`?t(exports):typeof define==`function`&&define.amd?define([`exports`],t):(e=typeof globalThis<`u`?globalThis:e||self,t(e.SketchRulerCanvas={}))})(this,function(e){Object.defineProperty(e,Symbol.toStringTag,{value:`Module`});var t=class{canvas=null;ctx=null;lastFingerprint=``;drawStatic(e,t,n){let r=this.buildFingerprint(t,n);return(r!==this.lastFingerprint||!this.canvas)&&(this.rebuild(t,n),this.lastFingerprint=r),this.canvas?(e.drawImage(this.canvas,0,0),!0):!1}rebuild(e,t){let{width:n,height:r,ratio:i,palette:a,thick:o,vertical:s}=t;this.canvas||=document.createElement(`canvas`),this.canvas.width=Math.round(n*i),this.canvas.height=Math.round(r*i);let c=this.canvas.getContext(`2d`);this.ctx=c,c.setTransform(1,0,0,1,0,0),c.scale(i,i),c.clearRect(0,0,n,r),c.fillStyle=a.bgColor,c.fillRect(0,0,n,r),c.strokeStyle=a.tickColor,c.lineWidth=1,c.beginPath();for(let i of e){let e=i.position;i.isMajor?i.value===0||i.value===t.canvasSize?s?(c.moveTo(0,e),c.lineTo(n,e)):(c.moveTo(e,0),c.lineTo(e,r)):s?(c.moveTo(n,e),c.lineTo(n*.65,e)):(c.moveTo(e,r),c.lineTo(e,r*.65)):s?(c.moveTo(n,e),c.lineTo(n*.8,e)):(c.moveTo(e,r),c.lineTo(e,r*.8))}c.stroke(),c.closePath()}buildFingerprint(e,t){let{thick:n,palette:r,vertical:i,width:a,height:o}=t,s=e.length>1?e[1].value-e[0].value:0,c=1,l=0;if(e.length>=2){let t=e[0],n=e.find(e=>e.value!==t.value)??e[1];n&&n.value!==t.value&&(c=(n.position-t.position)/(n.value-t.value),l=t.position-t.value*c)}return`${n}:${r.bgColor}:${r.tickColor}:${r.labelColor}:${i}:${a}:${o}:${s}:${c.toFixed(4)}:${l.toFixed(2)}`}clear(){this.canvas=null,this.ctx=null,this.lastFingerprint=``}},n=class{cache=new Map;maxSize;constructor(e=500){this.maxSize=e}get(e,t){let n=this.hashKey(t),r=this.cache.get(n);if(r)return this.cache.delete(n),this.cache.set(n,r),r;let i=this.createEntry(e,t);return this.cache.set(n,i),this.evictIfNeeded(),i}createEntry(e,t){let{text:n,font:r,color:i}=t;e.font=r;let a=e.measureText(n),o=Math.ceil(a.width),s=Math.ceil(a.actualBoundingBoxAscent+a.actualBoundingBoxDescent)||12;if(o===0||s===0)return{canvas:document.createElement(`canvas`),width:0,height:0};let c=document.createElement(`canvas`);c.width=o+2,c.height=s+2;let l=c.getContext(`2d`);return l.font=r,l.fillStyle=i,l.textBaseline=`alphabetic`,l.fillText(n,1,1+a.actualBoundingBoxAscent),{canvas:c,width:o,height:s}}evictIfNeeded(){if(this.cache.size<=this.maxSize)return;let e=this.cache.keys().next().value;e!==void 0&&this.cache.delete(e)}hashKey(e){return`${e.text}:${e.font}:${e.color}`}clear(){this.cache.clear()}size(){return this.cache.size}},r=`#e9f7fe`,i=class{offscreenCache=new t;labelCache=new n;render(e,t,n){for(let r of t)r.type===`ruler`&&this.renderRuler(e,r,n)}renderRuler(e,t,n){let{marks:i,vertical:a,thick:o,width:s,height:c,ratio:l,palette:u,shadowStart:d,shadowLength:f,showShadowText:p}=t;e.setTransform(1,0,0,1,0,0),e.scale(l,l),e.clearRect(0,0,s,c),this.offscreenCache.drawStatic(e,i,t);let m=[];if(f&&i.length>0){let t=d??0,n=i[0],l=i.find(e=>e.value!==n.value)??n,h=l&&l.value!==n.value?(l.position-n.position)/(l.value-n.value):1,g=n.position-n.value*h,_=t*h+g,v=f*h;if(v>0&&(e.fillStyle=u.shadowColor??r,a?e.fillRect(0,_,s,v):e.fillRect(_,0,v,c),p)){this.renderShadowText(e,t,_,o,a,u,!1);let n=t+f,r=n*h+g;this.renderShadowText(e,n,r,o,a,u,!0),m.push(_,r)}}for(let n of i)if(n.isMajor&&n.label){if(m.some(e=>Math.abs(n.position-e)<o*1.5))continue;e.save(),e.fillStyle=u.labelColor,e.font=`${Math.max(11,Math.floor(o*.5)+2)}px -apple-system, "Helvetica Neue", ".SFNSDisplay-Regular", "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "WenQuanYi Micro Hei", sans-serif`,a?(n.value===0?e.translate(s*.4,n.position-3):n.value===t.canvasSize?e.translate(s*.2,n.position+32):e.translate(s*.15,n.position+14),e.rotate(-90*Math.PI/180),e.fillText(n.label,4,9)):(n.value===0?e.translate(n.position-15,c*.13):n.value===t.canvasSize?e.translate(n.position+5,c*.3):e.translate(n.position-12,c*.18),e.fillText(n.label,4,9)),e.restore()}}renderShadowText(e,t,n,r,i,a,o){e.save(),e.fillStyle=a.labelColor,e.font=`bold 12px sans-serif`,i?(e.translate(r*.6,n+(o?-8:8)),e.rotate(-Math.PI/2)):e.translate(n+(o?-8:8),r*.6),e.fillText(Math.round(t).toString(),0,0),e.restore()}destroy(){this.offscreenCache.clear(),this.labelCache.clear()}};function a(e){let t=e.deltaX,n=e.deltaY,r=e.deltaZ||0,i=e.deltaMode;return i===WheelEvent.DOM_DELTA_LINE?(t*=40,n*=40,r*=40):i===WheelEvent.DOM_DELTA_PAGE&&(t*=800,n*=800,r*=800),{deltaX:t,deltaY:n,deltaZ:r,deltaMode:i}}function o(e,t=.001){let n=a(e);return-(n.deltaY===0?n.deltaX:n.deltaY)*t}var s=class{container;callbacks;boundWheel;boundMouseDown;boundMouseMove;boundMouseUp;boundMouseEnter;boundMouseLeave;isBound=!1;constructor(e,t){this.container=e,this.callbacks=t,this.boundWheel=this.handleWheel.bind(this),this.boundMouseDown=this.handleMouseDown.bind(this),this.boundMouseMove=this.handleMouseMove.bind(this),this.boundMouseUp=this.handleMouseUp.bind(this),this.boundMouseEnter=()=>this.callbacks.onMouseEnter?.(),this.boundMouseLeave=()=>this.callbacks.onMouseLeave?.()}bind(){this.isBound||(this.isBound=!0,this.container.addEventListener(`wheel`,this.boundWheel,{passive:!1}),this.container.addEventListener(`mousedown`,this.boundMouseDown),document.addEventListener(`mousemove`,this.boundMouseMove),document.addEventListener(`mouseup`,this.boundMouseUp),this.container.addEventListener(`mouseenter`,this.boundMouseEnter),this.container.addEventListener(`mouseleave`,this.boundMouseLeave))}unbind(){this.isBound&&(this.isBound=!1,this.container.removeEventListener(`wheel`,this.boundWheel),this.container.removeEventListener(`mousedown`,this.boundMouseDown),document.removeEventListener(`mousemove`,this.boundMouseMove),document.removeEventListener(`mouseup`,this.boundMouseUp),this.container.removeEventListener(`mouseenter`,this.boundMouseEnter),this.container.removeEventListener(`mouseleave`,this.boundMouseLeave))}handleWheel(e){let t=a(e);this.callbacks.onWheel?.(e,t)}handleMouseDown(e){this.callbacks.onMouseDown?.(e)}handleMouseMove(e){this.callbacks.onMouseMove?.(e)}handleMouseUp(e){this.callbacks.onMouseUp?.(e)}},c=class{callbacks;boundKeyDown;isBound=!1;constructor(e){this.callbacks=e,this.boundKeyDown=this.handleKeyDown.bind(this)}bind(){this.isBound||(this.isBound=!0,document.addEventListener(`keydown`,this.boundKeyDown))}unbind(){this.isBound&&(this.isBound=!1,document.removeEventListener(`keydown`,this.boundKeyDown))}handleKeyDown(e){if(e.repeat)return;let t=document.activeElement;if(t?.closest(`.monaco-editor`)||t?.tagName===`INPUT`||t?.tagName===`TEXTAREA`||t?.getAttribute(`contenteditable`)===`true`)return;let n=this.parseCombo(e);this.callbacks.onShortcut?.(n,e)}parseCombo(e){let t=[];(e.ctrlKey||e.metaKey)&&t.push(`ctrl`),e.altKey&&t.push(`alt`),e.shiftKey&&t.push(`shift`);let n=e.key.toLowerCase();return n===` `&&(n=`space`),n===`+`&&(n=`plus`),n===`-`&&(n=`minus`),n===`=`&&(n=`equal`),t.push(n),t.join(`+`)}},l=class{engine;zoomStep;selfHandle;zoomMode;viewportSize;contentSize;container=null;mouseAdapter=null;isSpacePressed=!1;isDragging=!1;dragStart={x:0,y:0};lastMouse={x:0,y:0};isHovered=!1;boundKeyUp;keyboardAdapter=null;pendingWheelDelta=0;wheelRafId=null;onCursorChange=null;zoomInterceptor=null;panInterceptor=null;constructor(e,t={}){this.engine=e,this.zoomStep=t.zoomStep??.25,this.selfHandle=t.selfHandle??!1,this.zoomMode=t.zoomMode??`pointer`,this.viewportSize=t.viewportSize??{width:0,height:0},this.contentSize=t.contentSize??{width:0,height:0},this.onCursorChange=t.onCursorChange??null,this.zoomInterceptor=t.zoomInterceptor??null,this.panInterceptor=t.panInterceptor??null,this.boundKeyUp=this.handleKeyUp.bind(this)}bind(e){if(this.selfHandle)return;this.unbind(),this.container=e;let t=e.parentElement;if(!t)return;document.addEventListener(`keyup`,this.boundKeyUp);let n={onWheel:this.handleWheel.bind(this),onMouseDown:this.handleMouseDown.bind(this),onMouseMove:this.handleMouseMove.bind(this),onMouseUp:this.handleMouseUp.bind(this),onMouseEnter:()=>{this.isHovered=!0},onMouseLeave:()=>{this.isHovered=!1}};this.mouseAdapter=new s(t,n),this.mouseAdapter.bind(),this.keyboardAdapter=new c({onShortcut:this.handleShortcut.bind(this)}),this.keyboardAdapter.bind()}unbind(){this.mouseAdapter?.unbind(),this.mouseAdapter=null,this.keyboardAdapter?.unbind(),this.keyboardAdapter=null,document.removeEventListener(`keyup`,this.boundKeyUp),this.container=null}destroy(){this.wheelRafId!==null&&(cancelAnimationFrame(this.wheelRafId),this.wheelRafId=null),this.unbind()}setZoomMode(e){this.zoomMode=e}handleWheel(e){if(e.ctrlKey||e.metaKey){e.preventDefault();let t=this.container?.parentElement,n=t?t.getBoundingClientRect():new DOMRect(0,0,0,0),r,i;switch(this.zoomMode){case`viewport-center`:r=this.viewportSize.width/2,i=this.viewportSize.height/2;break;case`content-center`:{let e=this.engine.getState();r=e.x+this.contentSize.width*e.scale/2,i=e.y+this.contentSize.height*e.scale/2;break}default:r=e.clientX-n.left,i=e.clientY-n.top;break}let a=e.deltaY===0?e.deltaX:e.deltaY;this.pendingWheelDelta+=a<0?1:-1,this.wheelRafId===null&&(this.wheelRafId=requestAnimationFrame(()=>{if(this.wheelRafId=null,this.pendingWheelDelta===0)return;let e=this.engine.getState().scale,t=e*Math.exp(this.pendingWheelDelta*this.zoomStep/3);this.pendingWheelDelta=0,this.executeZoom(()=>this.engine.zoomTo(t,r,i),e,t,r,i)}))}}handleShortcut(e,t){if(!this.isHovered)return;let n=this.viewportSize.width/2,r=this.viewportSize.height/2,i=e=>{e()};switch(e){case`ctrl+0`:t.preventDefault(),i(()=>this.executeZoom(()=>this.engine.zoomTo(1,n,r),this.engine.getState().scale,1,n,r));break;case`ctrl+minus`:{t.preventDefault();let e=this.engine.getState().scale,a=e-this.zoomStep;i(()=>this.executeZoom(()=>this.engine.zoomBy(-this.zoomStep,n,r),e,a,n,r));break}case`ctrl+equal`:case`ctrl+plus`:{t.preventDefault();let e=this.engine.getState().scale,a=e+this.zoomStep;i(()=>this.executeZoom(()=>this.engine.zoomBy(this.zoomStep,n,r),e,a,n,r));break}case`ctrl+1`:{t.preventDefault();let e=this.viewportSize.width,a=this.viewportSize.height,o=this.contentSize.width,s=this.contentSize.height;if(e>0&&a>0&&o>0&&s>0){let t=e*.9/o,c=a*.9/s,l=Math.min(t,c),u=(e-o*l)/2,d=(a-s*l)/2,f=this.engine.getState().scale;i(()=>this.executeZoom(()=>this.engine.setTransform({scale:l,x:u,y:d}),f,l,n,r))}break}case`space`:this.isSpacePressed||(this.isSpacePressed=!0,t.preventDefault(),this.notifyCursorChange());break;default:break}}handleKeyUp(e){e.key===` `&&(this.isSpacePressed=!1,this.isDragging=!1,this.notifyCursorChange())}handleMouseDown(e){this.isSpacePressed&&e.button===0&&(this.isDragging=!0,this.dragStart={x:e.clientX,y:e.clientY},this.lastMouse={x:e.clientX,y:e.clientY},e.preventDefault(),this.notifyCursorChange())}async handleMouseMove(e){if(this.isDragging&&this.isSpacePressed){let t=e.clientX-this.lastMouse.x,n=e.clientY-this.lastMouse.y;(!this.panInterceptor?.beforePan||await this.panInterceptor.beforePan(t,n))&&(this.engine.panBy(t,n),this.panInterceptor?.afterPan?.(t,n)),this.lastMouse={x:e.clientX,y:e.clientY}}}handleMouseUp(){this.isDragging=!1,this.notifyCursorChange()}async executeZoom(e,t,n,r,i){(!this.zoomInterceptor?.beforeZoom||await this.zoomInterceptor.beforeZoom(t,n,r,i))&&(e(),this.zoomInterceptor?.afterZoom?.(t,this.engine.getState().scale,r,i))}getCursorClass(){return this.isSpacePressed?this.isDragging?`grabbing`:`grab`:`default`}notifyCursorChange(){this.onCursorChange?.(this.getCursorClass())}};e.Canvas2DRenderer=i,e.InputManager=l,e.KeyboardAdapter=c,e.LabelCache=n,e.MouseAdapter=s,e.OffscreenRulerCache=t,e.getZoomDelta=o,e.normalizeWheel=a});
@@ -0,0 +1,5 @@
1
+ export { InputManager, type InputManagerOptions } from './input-manager';
2
+ export { MouseAdapter, type MouseAdapterCallbacks } from './mouse-adapter';
3
+ export { KeyboardAdapter, type KeyboardAdapterCallbacks } from './keyboard-adapter';
4
+ export { normalizeWheel, getZoomDelta, type NormalizedWheel } from './wheel-normalizer';
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/input/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,KAAK,mBAAmB,EAAE,MAAM,iBAAiB,CAAA;AACxE,OAAO,EAAE,YAAY,EAAE,KAAK,qBAAqB,EAAE,MAAM,iBAAiB,CAAA;AAC1E,OAAO,EAAE,eAAe,EAAE,KAAK,wBAAwB,EAAE,MAAM,oBAAoB,CAAA;AACnF,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,KAAK,eAAe,EAAE,MAAM,oBAAoB,CAAA"}
@@ -0,0 +1,74 @@
1
+ import { TransformEngine } from '@sketch-ruler/core';
2
+ export type ZoomMode = 'pointer' | 'viewport-center' | 'content-center';
3
+ export interface ZoomInterceptor {
4
+ beforeZoom?: (from: number, to: number, originX: number, originY: number) => boolean | Promise<boolean>;
5
+ afterZoom?: (from: number, to: number, originX: number, originY: number) => void;
6
+ }
7
+ export interface PanInterceptor {
8
+ beforePan?: (dx: number, dy: number) => boolean | Promise<boolean>;
9
+ afterPan?: (dx: number, dy: number) => void;
10
+ }
11
+ export interface InputManagerOptions {
12
+ /** 缩放步长 */
13
+ zoomStep?: number;
14
+ /** 是否由外部自行处理事件 */
15
+ selfHandle?: boolean;
16
+ /** 缩放原点模式 */
17
+ zoomMode?: ZoomMode;
18
+ /** 视口尺寸(viewport-center / content-center 模式需要) */
19
+ viewportSize?: {
20
+ width: number;
21
+ height: number;
22
+ };
23
+ /** 内容尺寸(content-center 模式需要) */
24
+ contentSize?: {
25
+ width: number;
26
+ height: number;
27
+ };
28
+ /** 光标状态变化回调 */
29
+ onCursorChange?: (cursorClass: string) => void;
30
+ /** 缩放拦截器(用于插件 beforeZoom / afterZoom) */
31
+ zoomInterceptor?: ZoomInterceptor;
32
+ /** 平移拦截器(用于插件 beforePan / afterPan) */
33
+ panInterceptor?: PanInterceptor;
34
+ }
35
+ export declare class InputManager {
36
+ private engine;
37
+ private zoomStep;
38
+ private selfHandle;
39
+ private zoomMode;
40
+ private viewportSize;
41
+ private contentSize;
42
+ private container;
43
+ private mouseAdapter;
44
+ private isSpacePressed;
45
+ private isDragging;
46
+ private dragStart;
47
+ private lastMouse;
48
+ private isHovered;
49
+ private boundKeyUp;
50
+ private keyboardAdapter;
51
+ private pendingWheelDelta;
52
+ private wheelRafId;
53
+ private onCursorChange;
54
+ private zoomInterceptor;
55
+ private panInterceptor;
56
+ constructor(engine: TransformEngine, options?: InputManagerOptions);
57
+ /** 绑定到容器元素 */
58
+ bind(container: HTMLElement): void;
59
+ /** 解绑所有事件 */
60
+ unbind(): void;
61
+ /** 销毁 */
62
+ destroy(): void;
63
+ setZoomMode(mode: ZoomMode): void;
64
+ private handleWheel;
65
+ private handleShortcut;
66
+ private handleKeyUp;
67
+ private handleMouseDown;
68
+ private handleMouseMove;
69
+ private handleMouseUp;
70
+ private executeZoom;
71
+ getCursorClass(): string;
72
+ private notifyCursorChange;
73
+ }
74
+ //# sourceMappingURL=input-manager.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"input-manager.d.ts","sourceRoot":"","sources":["../../src/input/input-manager.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAA;AAKzD,MAAM,MAAM,QAAQ,GAAG,SAAS,GAAG,iBAAiB,GAAG,gBAAgB,CAAA;AAEvE,MAAM,WAAW,eAAe;IAC9B,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;IACvG,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;CACjF;AAED,MAAM,WAAW,cAAc;IAC7B,SAAS,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;IAClE,QAAQ,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,KAAK,IAAI,CAAA;CAC5C;AAED,MAAM,WAAW,mBAAmB;IAClC,WAAW;IACX,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,kBAAkB;IAClB,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,aAAa;IACb,QAAQ,CAAC,EAAE,QAAQ,CAAA;IACnB,kDAAkD;IAClD,YAAY,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAA;IAChD,gCAAgC;IAChC,WAAW,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAA;IAC/C,eAAe;IACf,cAAc,CAAC,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,IAAI,CAAA;IAC9C,yCAAyC;IACzC,eAAe,CAAC,EAAE,eAAe,CAAA;IACjC,uCAAuC;IACvC,cAAc,CAAC,EAAE,cAAc,CAAA;CAChC;AAED,qBAAa,YAAY;IACvB,OAAO,CAAC,MAAM,CAAiB;IAC/B,OAAO,CAAC,QAAQ,CAAQ;IACxB,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,QAAQ,CAAU;IAC1B,OAAO,CAAC,YAAY,CAAmC;IACvD,OAAO,CAAC,WAAW,CAAmC;IAEtD,OAAO,CAAC,SAAS,CAA2B;IAC5C,OAAO,CAAC,YAAY,CAA4B;IAEhD,OAAO,CAAC,cAAc,CAAQ;IAC9B,OAAO,CAAC,UAAU,CAAQ;IAC1B,OAAO,CAAC,SAAS,CAAiB;IAClC,OAAO,CAAC,SAAS,CAAiB;IAClC,OAAO,CAAC,SAAS,CAAQ;IAEzB,OAAO,CAAC,UAAU,CAA4B;IAC9C,OAAO,CAAC,eAAe,CAA+B;IACtD,OAAO,CAAC,iBAAiB,CAAI;IAC7B,OAAO,CAAC,UAAU,CAAsB;IACxC,OAAO,CAAC,cAAc,CAA+C;IACrE,OAAO,CAAC,eAAe,CAA+B;IACtD,OAAO,CAAC,cAAc,CAA8B;gBAExC,MAAM,EAAE,eAAe,EAAE,OAAO,GAAE,mBAAwB;IActE,cAAc;IACd,IAAI,CAAC,SAAS,EAAE,WAAW,GAAG,IAAI;IAmClC,aAAa;IACb,MAAM,IAAI,IAAI;IAYd,SAAS;IACT,OAAO,IAAI,IAAI;IAQf,WAAW,CAAC,IAAI,EAAE,QAAQ,GAAG,IAAI;IAIjC,OAAO,CAAC,WAAW;IAiDnB,OAAO,CAAC,cAAc;IA+FtB,OAAO,CAAC,WAAW;IAQnB,OAAO,CAAC,eAAe;YAUT,eAAe;IAkB7B,OAAO,CAAC,aAAa;YAKP,WAAW;IAiBzB,cAAc,IAAI,MAAM;IAOxB,OAAO,CAAC,kBAAkB;CAG3B"}
@@ -0,0 +1,19 @@
1
+ /**
2
+ * KeyboardAdapter - 键盘事件适配器
3
+ * M3 W11:修饰键状态追踪 + 快捷键映射
4
+ */
5
+ export type KeyCombo = string;
6
+ export interface KeyboardAdapterCallbacks {
7
+ onShortcut?: (combo: KeyCombo, e: KeyboardEvent) => void;
8
+ }
9
+ export declare class KeyboardAdapter {
10
+ private callbacks;
11
+ private boundKeyDown;
12
+ private isBound;
13
+ constructor(callbacks: KeyboardAdapterCallbacks);
14
+ bind(): void;
15
+ unbind(): void;
16
+ private handleKeyDown;
17
+ private parseCombo;
18
+ }
19
+ //# sourceMappingURL=keyboard-adapter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"keyboard-adapter.d.ts","sourceRoot":"","sources":["../../src/input/keyboard-adapter.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,MAAM,QAAQ,GAAG,MAAM,CAAA;AAE7B,MAAM,WAAW,wBAAwB;IACvC,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,QAAQ,EAAE,CAAC,EAAE,aAAa,KAAK,IAAI,CAAA;CACzD;AAED,qBAAa,eAAe;IAC1B,OAAO,CAAC,SAAS,CAA0B;IAC3C,OAAO,CAAC,YAAY,CAA4B;IAChD,OAAO,CAAC,OAAO,CAAQ;gBAEX,SAAS,EAAE,wBAAwB;IAK/C,IAAI,IAAI,IAAI;IAMZ,MAAM,IAAI,IAAI;IAMd,OAAO,CAAC,aAAa;IAkBrB,OAAO,CAAC,UAAU;CAgBnB"}
@@ -0,0 +1,32 @@
1
+ import { NormalizedWheel } from './wheel-normalizer';
2
+ export interface MouseAdapterCallbacks {
3
+ onWheel?: (e: WheelEvent, normalized: NormalizedWheel) => void;
4
+ onMouseDown?: (e: MouseEvent) => void;
5
+ onMouseMove?: (e: MouseEvent) => void;
6
+ onMouseUp?: (e: MouseEvent) => void;
7
+ onMouseEnter?: () => void;
8
+ onMouseLeave?: () => void;
9
+ }
10
+ /**
11
+ * MouseAdapter - 鼠标事件封装适配器
12
+ * 统一绑定/解绑生命周期,自动标准化滚轮事件
13
+ */
14
+ export declare class MouseAdapter {
15
+ private container;
16
+ private callbacks;
17
+ private boundWheel;
18
+ private boundMouseDown;
19
+ private boundMouseMove;
20
+ private boundMouseUp;
21
+ private boundMouseEnter;
22
+ private boundMouseLeave;
23
+ private isBound;
24
+ constructor(container: HTMLElement, callbacks: MouseAdapterCallbacks);
25
+ bind(): void;
26
+ unbind(): void;
27
+ private handleWheel;
28
+ private handleMouseDown;
29
+ private handleMouseMove;
30
+ private handleMouseUp;
31
+ }
32
+ //# sourceMappingURL=mouse-adapter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mouse-adapter.d.ts","sourceRoot":"","sources":["../../src/input/mouse-adapter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAkB,KAAK,eAAe,EAAE,MAAM,oBAAoB,CAAA;AAEzE,MAAM,WAAW,qBAAqB;IACpC,OAAO,CAAC,EAAE,CAAC,CAAC,EAAE,UAAU,EAAE,UAAU,EAAE,eAAe,KAAK,IAAI,CAAA;IAC9D,WAAW,CAAC,EAAE,CAAC,CAAC,EAAE,UAAU,KAAK,IAAI,CAAA;IACrC,WAAW,CAAC,EAAE,CAAC,CAAC,EAAE,UAAU,KAAK,IAAI,CAAA;IACrC,SAAS,CAAC,EAAE,CAAC,CAAC,EAAE,UAAU,KAAK,IAAI,CAAA;IACnC,YAAY,CAAC,EAAE,MAAM,IAAI,CAAA;IACzB,YAAY,CAAC,EAAE,MAAM,IAAI,CAAA;CAC1B;AAED;;;GAGG;AACH,qBAAa,YAAY;IACvB,OAAO,CAAC,SAAS,CAAa;IAC9B,OAAO,CAAC,SAAS,CAAuB;IAExC,OAAO,CAAC,UAAU,CAAyB;IAC3C,OAAO,CAAC,cAAc,CAAyB;IAC/C,OAAO,CAAC,cAAc,CAAyB;IAC/C,OAAO,CAAC,YAAY,CAAyB;IAC7C,OAAO,CAAC,eAAe,CAAY;IACnC,OAAO,CAAC,eAAe,CAAY;IAEnC,OAAO,CAAC,OAAO,CAAQ;gBAEX,SAAS,EAAE,WAAW,EAAE,SAAS,EAAE,qBAAqB;IAYpE,IAAI,IAAI,IAAI;IAYZ,MAAM,IAAI,IAAI;IAYd,OAAO,CAAC,WAAW;IAKnB,OAAO,CAAC,eAAe;IAIvB,OAAO,CAAC,eAAe;IAIvB,OAAO,CAAC,aAAa;CAGtB"}
@@ -0,0 +1,30 @@
1
+ /**
2
+ * WheelNormalizer - 统一不同浏览器滚轮事件 delta 值
3
+ * Chrome/Firefox/Safari 的 WheelEvent.deltaY 差异很大,
4
+ * 统一转换为像素级增量,便于上层消费。
5
+ */
6
+ export interface NormalizedWheel {
7
+ /** 水平方向增量(像素) */
8
+ deltaX: number;
9
+ /** 垂直方向增量(像素) */
10
+ deltaY: number;
11
+ /** Z 轴增量(像素) */
12
+ deltaZ: number;
13
+ /** 原始 deltaMode */
14
+ deltaMode: number;
15
+ }
16
+ /**
17
+ * 将浏览器原生 WheelEvent 标准化为像素单位
18
+ *
19
+ * 各浏览器典型值:
20
+ * - Chrome (line mode): deltaY ≈ 100
21
+ * - Firefox (line mode): deltaY ≈ 3
22
+ * - Safari (pixel mode): deltaY ≈ 1
23
+ */
24
+ export declare function normalizeWheel(event: WheelEvent): NormalizedWheel;
25
+ /**
26
+ * 获取滚轮事件的标准化缩放增量
27
+ * 用于 Ctrl+滚轮缩放场景,返回值已做方向归一化
28
+ */
29
+ export declare function getZoomDelta(event: WheelEvent, sensitivity?: number): number;
30
+ //# sourceMappingURL=wheel-normalizer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wheel-normalizer.d.ts","sourceRoot":"","sources":["../../src/input/wheel-normalizer.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,MAAM,WAAW,eAAe;IAC9B,iBAAiB;IACjB,MAAM,EAAE,MAAM,CAAA;IACd,iBAAiB;IACjB,MAAM,EAAE,MAAM,CAAA;IACd,gBAAgB;IAChB,MAAM,EAAE,MAAM,CAAA;IACd,mBAAmB;IACnB,SAAS,EAAE,MAAM,CAAA;CAClB;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,UAAU,GAAG,eAAe,CAoBjE;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,UAAU,EAAE,WAAW,SAAQ,GAAG,MAAM,CAK3E"}
@@ -0,0 +1,10 @@
1
+ import { Renderer, Rect, RulerRenderPayload } from './types';
2
+ export declare class Canvas2DRenderer implements Renderer {
3
+ private offscreenCache;
4
+ private labelCache;
5
+ render(ctx: CanvasRenderingContext2D, items: RulerRenderPayload[], viewportRect: Rect): void;
6
+ private renderRuler;
7
+ private renderShadowText;
8
+ destroy(): void;
9
+ }
10
+ //# sourceMappingURL=canvas-2d-renderer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"canvas-2d-renderer.d.ts","sourceRoot":"","sources":["../../src/renderers/canvas-2d-renderer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAA;AAQjE,qBAAa,gBAAiB,YAAW,QAAQ;IAC/C,OAAO,CAAC,cAAc,CAA4B;IAClD,OAAO,CAAC,UAAU,CAAmB;IAErC,MAAM,CAAC,GAAG,EAAE,wBAAwB,EAAE,KAAK,EAAE,kBAAkB,EAAE,EAAE,YAAY,EAAE,IAAI,GAAG,IAAI;IAQ5F,OAAO,CAAC,WAAW;IAkGnB,OAAO,CAAC,gBAAgB;IAsBxB,OAAO,IAAI,IAAI;CAIhB"}
@@ -0,0 +1,3 @@
1
+ export type { Renderer, Rect, RulerRenderPayload, RenderItem } from './types';
2
+ export { Canvas2DRenderer } from './canvas-2d-renderer';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/renderers/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,QAAQ,EAAE,IAAI,EAAE,kBAAkB,EAAE,UAAU,EAAE,MAAM,SAAS,CAAA;AAC7E,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAA"}
@@ -0,0 +1,36 @@
1
+ /**
2
+ * LabelCache - 刻度标签 LRU 缓存池
3
+ * 将 fillText() 栅格化为离屏 Canvas,后续直接 drawImage() 贴图
4
+ * M2 性能优化核心:单标签绘制从 0.5-2ms 降至 0.01-0.05ms
5
+ */
6
+ export interface LabelCacheKey {
7
+ text: string;
8
+ font: string;
9
+ color: string;
10
+ }
11
+ /** 缓存条目 */
12
+ interface CacheEntry {
13
+ canvas: HTMLCanvasElement;
14
+ width: number;
15
+ height: number;
16
+ }
17
+ export declare class LabelCache {
18
+ private cache;
19
+ private maxSize;
20
+ constructor(maxSize?: number);
21
+ /**
22
+ * 获取缓存的标签 Canvas;未命中则创建并缓存
23
+ */
24
+ get(ctx: CanvasRenderingContext2D, key: LabelCacheKey): CacheEntry;
25
+ /** 预计算文本尺寸并栅格化 */
26
+ private createEntry;
27
+ /** 淘汰最久未使用的条目 */
28
+ private evictIfNeeded;
29
+ private hashKey;
30
+ /** 清空缓存 */
31
+ clear(): void;
32
+ /** 当前缓存数量 */
33
+ size(): number;
34
+ }
35
+ export {};
36
+ //# sourceMappingURL=label-cache.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"label-cache.d.ts","sourceRoot":"","sources":["../../src/renderers/label-cache.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;CACd;AAED,WAAW;AACX,UAAU,UAAU;IAClB,MAAM,EAAE,iBAAiB,CAAA;IACzB,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;CACf;AAED,qBAAa,UAAU;IACrB,OAAO,CAAC,KAAK,CAAgC;IAC7C,OAAO,CAAC,OAAO,CAAQ;gBAEX,OAAO,SAAM;IAIzB;;OAEG;IACH,GAAG,CAAC,GAAG,EAAE,wBAAwB,EAAE,GAAG,EAAE,aAAa,GAAG,UAAU;IAgBlE,kBAAkB;IAClB,OAAO,CAAC,WAAW;IA2BnB,iBAAiB;IACjB,OAAO,CAAC,aAAa;IAQrB,OAAO,CAAC,OAAO;IAIf,WAAW;IACX,KAAK,IAAI,IAAI;IAIb,aAAa;IACb,IAAI,IAAI,MAAM;CAGf"}
@@ -0,0 +1,17 @@
1
+ import { ScaleMark } from '@sketch-ruler/core';
2
+ import { RulerRenderPayload } from './types';
3
+ export declare class OffscreenRulerCache {
4
+ private canvas;
5
+ private ctx;
6
+ private lastFingerprint;
7
+ /**
8
+ * 尝试使用离屏缓存绘制静态部分
9
+ * @returns true 表示使用了缓存,false 表示缓存未命中/未初始化
10
+ */
11
+ drawStatic(targetCtx: CanvasRenderingContext2D, marks: ScaleMark[], payload: RulerRenderPayload): boolean;
12
+ private rebuild;
13
+ private buildFingerprint;
14
+ /** 清空缓存 */
15
+ clear(): void;
16
+ }
17
+ //# sourceMappingURL=offscreen-ruler-cache.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"offscreen-ruler-cache.d.ts","sourceRoot":"","sources":["../../src/renderers/offscreen-ruler-cache.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAA;AACnD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAA;AAEjD,qBAAa,mBAAmB;IAC9B,OAAO,CAAC,MAAM,CAAiC;IAC/C,OAAO,CAAC,GAAG,CAAwC;IACnD,OAAO,CAAC,eAAe,CAAK;IAE5B;;;OAGG;IACH,UAAU,CACR,SAAS,EAAE,wBAAwB,EACnC,KAAK,EAAE,SAAS,EAAE,EAClB,OAAO,EAAE,kBAAkB,GAC1B,OAAO;IAeV,OAAO,CAAC,OAAO;IAoEf,OAAO,CAAC,gBAAgB;IAoBxB,WAAW;IACX,KAAK,IAAI,IAAI;CAKd"}
@@ -0,0 +1,34 @@
1
+ import { ScaleMark, RulerPalette } from '@sketch-ruler/core';
2
+ /** 视口矩形 */
3
+ export interface Rect {
4
+ x: number;
5
+ y: number;
6
+ width: number;
7
+ height: number;
8
+ }
9
+ /** 标尺渲染载荷 */
10
+ export interface RulerRenderPayload {
11
+ type: 'ruler';
12
+ marks: ScaleMark[];
13
+ vertical: boolean;
14
+ thick: number;
15
+ width: number;
16
+ height: number;
17
+ ratio: number;
18
+ palette: RulerPalette;
19
+ /** 阴影起始位置(世界坐标) */
20
+ shadowStart?: number;
21
+ /** 阴影长度(世界坐标) */
22
+ shadowLength?: number;
23
+ /** 是否显示阴影文字 */
24
+ showShadowText?: boolean;
25
+ /** 画布尺寸(用于阴影文字和边界标注过滤) */
26
+ canvasSize?: number;
27
+ }
28
+ export type RenderItem = RulerRenderPayload;
29
+ /** 渲染器抽象接口 */
30
+ export interface Renderer {
31
+ render(ctx: CanvasRenderingContext2D, items: RenderItem[], viewportRect: Rect): void;
32
+ destroy(): void;
33
+ }
34
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/renderers/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;AAEjE,WAAW;AACX,MAAM,WAAW,IAAI;IACnB,CAAC,EAAE,MAAM,CAAA;IACT,CAAC,EAAE,MAAM,CAAA;IACT,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;CACf;AAED,aAAa;AACb,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,OAAO,CAAA;IACb,KAAK,EAAE,SAAS,EAAE,CAAA;IAClB,QAAQ,EAAE,OAAO,CAAA;IACjB,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,EAAE,MAAM,CAAA;IACb,OAAO,EAAE,YAAY,CAAA;IACrB,mBAAmB;IACnB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,iBAAiB;IACjB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,eAAe;IACf,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB,0BAA0B;IAC1B,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AAED,MAAM,MAAM,UAAU,GAAG,kBAAkB,CAAA;AAE3C,cAAc;AACd,MAAM,WAAW,QAAQ;IACvB,MAAM,CAAC,GAAG,EAAE,wBAAwB,EAAE,KAAK,EAAE,UAAU,EAAE,EAAE,YAAY,EAAE,IAAI,GAAG,IAAI,CAAA;IACpF,OAAO,IAAI,IAAI,CAAA;CAChB"}
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "@sketch-ruler/canvas",
3
+ "version": "3.0.0-beta.0",
4
+ "private": false,
5
+ "description": "Canvas rendering and DOM input management for sketch-ruler. Framework-agnostic, depends on Canvas 2D API and DOM Events.",
6
+ "keywords": [
7
+ "sketch-ruler",
8
+ "ruler",
9
+ "canvas",
10
+ "renderer",
11
+ "input",
12
+ "framework-agnostic"
13
+ ],
14
+ "license": "MIT",
15
+ "author": "kakajun <253495832@qq.com>",
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/kakajun/vue3-sketch-ruler.git"
19
+ },
20
+ "files": [
21
+ "lib",
22
+ "AGENTS.md"
23
+ ],
24
+ "type": "module",
25
+ "sideEffects": false,
26
+ "main": "lib/index.js",
27
+ "module": "lib/index.js",
28
+ "types": "lib/index.d.ts",
29
+ "exports": {
30
+ ".": {
31
+ "types": "./lib/index.d.ts",
32
+ "import": "./lib/index.js",
33
+ "require": "./lib/index.cjs"
34
+ },
35
+ "./renderers": {
36
+ "types": "./lib/renderers/index.d.ts",
37
+ "import": "./lib/renderers/index.js",
38
+ "require": "./lib/renderers/index.cjs"
39
+ },
40
+ "./input": {
41
+ "types": "./lib/input/index.d.ts",
42
+ "import": "./lib/input/index.js",
43
+ "require": "./lib/input/index.cjs"
44
+ }
45
+ },
46
+ "dependencies": {
47
+ "@sketch-ruler/core": "workspace:*"
48
+ },
49
+ "scripts": {
50
+ "build": "vite build",
51
+ "test": "vitest run",
52
+ "test:watch": "vitest"
53
+ },
54
+ "devDependencies": {
55
+ "vite": "^8.0.13",
56
+ "vite-plugin-dts": "^5.0.0",
57
+ "vitest": "^4.1.6"
58
+ }
59
+ }