Repository navigation
修复橡皮擦实际擦除范围大于光标圆圈的问题 - #388
Merged
alpha-liu-01 merged 1 commit intoOct 18, 2026
Merged
Conversation
…ully The eraser deleted strokes further out than the cursor circle it is aimed with, and a fast flick skipped strokes it visually crossed. Three separate causes: 1. Hit testing used `tolerance + baseThickness / 2`, i.e. the eraser disc reached the stroke's *outline*, while the cursor circle was drawn with `tolerance` alone against the stroke's *centre line*. The two disagreed by half the stroke width, so ink outside the circle vanished. The rule is now solid-to-solid on both sides: the disc touches the stroke's drawn outline, and the stroke contributes its own half-width (per-sample, so pressure-tapered strokes are judged by their local thickness). Removing the half-width term entirely - the obvious fix - makes it worse in the other direction: the circle has to be pushed well into the stroke before anything happens. 2. Only the discrete pointer positions were tested, so a swipe faster than two events could jump over a stroke without ever touching it. The motion is now treated as a capsule swept from the previous sample to the current one (segment-to-segment closest approach, plus a proper crossing test). This is the part that actually prevents missed erases. 3. The repaint region was the cursor ellipse alone. A removed stroke's ink outside that ellipse stayed on screen until some later unrelated repaint, which read as "the whole stroke disappears on release" even though the removal was immediate. The bounds of every removed stroke are now merged into the dirty region, in both paged and edgeless modes. Also consolidates the eraser radius into a single source of truth (eraserRadiusDocUnits / eraserRadiusViewportPixels) instead of the ad-hoc `* m_zoomLevel + 5` and `+ 10` copies scattered through the paint and input paths, and unifies the size bounds: setEraserSize clamped to 5..200 while the toolbar offered 2..100, so small presets were silently enlarged and the cursor drawn at a radius the user had not picked. Tests cover both directions (thin vs thick strokes, dot strokes, sweep vs off-path strokes, crossing paths).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
问题
橡皮擦实际擦除的范围比光标圆圈大:圆圈还没碰到笔画,笔画就被删掉了。
(本 PR 顺带修掉两个相邻问题:快速划动会漏擦,以及擦除后要松开手笔画才整条消失。)
原因
三个独立缺陷叠加:
1. 命中半径与圆圈半径语义不一致(主因)
m_eraserSize * zoom绘制,即"橡皮中心到笔迹中心线的半径"。VectorStroke::containsPoint()用的是:distanceToSegment(point, a, b) < tolerance + baseThickness / 2.0;即"橡皮圆盘边缘碰到笔迹绘制出的轮廓"。
两者相差半个笔迹宽度。所以圆圈外、
R + 半宽以内的笔画都会被删掉,笔画越粗偏差越明显。2. 只对离散指针位置做判定
eraseAt()每个事件只测当前点,两次事件之间没有任何插值。快速划动时若单次位移超过2 × 命中半径,中间扫过的区域完全不会被检测。3. 重绘区域覆盖不到被删笔画
删除时
removeStroke()已经移除了整条笔画,但update()只请求重绘光标那个椭圆。包围盒里椭圆之外的像素从未被要求重绘,屏幕上仍是旧画面,直到松开手触发某次全量重绘才刷掉 —— 看起来就像"松手才擦掉整条"。改动
命中语义改为「实体接触」
source/strokes/VectorStroke.h圆圈代表橡皮自身半径,命中时把笔迹半宽算进来 —— 因为笔迹本身是有宽度的实体。这是「两个实心图形相碰」:
扫掠判定(真正解决漏擦)
VectorStroke::intersectsEraserSweep()/VectorLayer::strokesInEraserSweep()把运动视为从上一采样点到当前点的胶囊体(圆盘沿线段扫过),做线段-线段最近距离判定,并对真正相交的情况单独检测(两端点都远但线段交叉的情形,仅靠端点距离会漏)。
扫掠起点在按下时确立、每次移动后推进、释放时清除,避免跨两次独立接触错误连接。跨页拖动加了
samePage保护,防止页面局部坐标错位导致误擦。重绘区域并入被删笔画
source/core/DocumentViewport.cpp包围盒经
pagePosition()换算到视口坐标并裁剪到视口。edgeless 模式按 tile 原点换算,同样处理。圈一碰到,整条笔画当场消失。消除常量散落
半径改为单一来源
eraserRadiusDocUnits()/eraserRadiusViewportPixels(),取代散落在绘制与输入路径上的* m_zoomLevel + 5、+ 10等多份拷贝。顺带统一尺寸范围:
setEraserSize()clamp 在5..200,而工具栏提供2..100,导致小于 5 的预设被静默放大、圆圈画出的半径与用户选的并不一致。现统一引用DocumentViewport::ERASER_MIN/MAX_SIZE_DOC_UNITS。测试
新增两个单元测试(
--test-viewport):testEraserReachMatchesCursor— 粗笔画(半宽 15)在 34 单位处应命中、40 单位处不应命中;细笔画(半宽 1)在 25 单位处必须不命中(防止细笔画误继承粗笔画的触及范围);dot 笔画同样规则。testEraserSweepClosesSamplingGap— 起止点相距 1000 单位的快速划动必须擦到路径上的笔画、不得擦到 200 单位外的笔画;垂直划动切割水平笔画必须命中(四端点均远,验证交叉检测)。行为变化
擦除手感会比以前略"紧":旧行为中细笔画会额外获得半宽加成(本质是 bug)。若需要更宽松的手感,应调大预设尺寸,而不是重新引入半径不一致。
未包含
部分擦除模式(扫到哪擦哪,而非整笔删除)不在本 PR 范围内。它需要给
EraserMode增加Partial并对VectorStroke做按采样点的段落切分,属于独立改动。