Compare commits
3 Commits
0ae2671f9b
...
df389177e6
| Author | SHA1 | Date | |
|---|---|---|---|
| df389177e6 | |||
| 8908f42f76 | |||
| eae4493954 |
@@ -194,13 +194,20 @@ class _PenInteractiveViewerState extends State<PenInteractiveViewer>
|
|||||||
// The transitional frame itself is skipped.
|
// The transitional frame itself is skipped.
|
||||||
if (details.pointerCount != _lastPointerCount) {
|
if (details.pointerCount != _lastPointerCount) {
|
||||||
_lastPointerCount = details.pointerCount;
|
_lastPointerCount = details.pointerCount;
|
||||||
_scaleStart = _transformer.value.getMaxScaleOnAxis();
|
// Anchor the new baseline to the CLEAN tracked scale (_lastAppliedScale),
|
||||||
|
// NOT a fresh matrix read-back. Windows touch flickers the pointer count
|
||||||
|
// (2↔1↔2) mid-pinch, firing this re-baseline spuriously; reading
|
||||||
|
// getMaxScaleOnAxis() at that glitchy instant popped _scaleStart to a
|
||||||
|
// noisy value, so the absolute map K = scaleStart / rawScaleAtBaseline
|
||||||
|
// oscillated frame-to-frame (the reported "zoom jump"). Using
|
||||||
|
// _lastAppliedScale makes the displayed scale CONTINUOUS across the
|
||||||
|
// re-baseline: target == _lastAppliedScale at this instant, regardless of
|
||||||
|
// any transient in the live matrix.
|
||||||
|
_scaleStart = _lastAppliedScale;
|
||||||
_referenceFocalPoint = _transformer.toScene(details.localFocalPoint);
|
_referenceFocalPoint = _transformer.toScene(details.localFocalPoint);
|
||||||
_lastRawScale = details.scale;
|
_lastRawScale = details.scale;
|
||||||
_lastAppliedScale = _scaleStart!;
|
// Re-anchor the cumulative scale to THIS frame's details.scale so the next
|
||||||
// Re-anchor the absolute mapping: from here, cumulative scale is measured
|
// good frame resumes from _scaleStart (not _scaleStart × a stale ratio).
|
||||||
// relative to THIS frame's details.scale (so the next good frame starts
|
|
||||||
// from _scaleStart, not _scaleStart * a stale cumulative value).
|
|
||||||
_rawScaleAtBaseline = details.scale;
|
_rawScaleAtBaseline = details.scale;
|
||||||
InputDiagnostics.instance.recordRebaseline();
|
InputDiagnostics.instance.recordRebaseline();
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -60,6 +60,9 @@ class _SplitViewState extends State<SplitViewScreen> {
|
|||||||
/// Pan/zoom transform for the scratchpad world (PenCanvas drives this).
|
/// Pan/zoom transform for the scratchpad world (PenCanvas drives this).
|
||||||
final TransformationController _scratchTransform = TransformationController();
|
final TransformationController _scratchTransform = TransformationController();
|
||||||
|
|
||||||
|
/// Set once the initial view has been framed onto existing ink.
|
||||||
|
bool _scratchCentered = false;
|
||||||
|
|
||||||
Size get _worldSize => Size(_canvasWidth, _canvasHeight);
|
Size get _worldSize => Size(_canvasWidth, _canvasHeight);
|
||||||
|
|
||||||
/// Maps the scratchpad toolbar's [PenTool] to the pen-canvas tool. Shapes and
|
/// Maps the scratchpad toolbar's [PenTool] to the pen-canvas tool. Shapes and
|
||||||
@@ -471,24 +474,79 @@ class _SplitViewState extends State<SplitViewScreen> {
|
|||||||
// Render the world through the performant PenCanvas: strokes normalized
|
// Render the world through the performant PenCanvas: strokes normalized
|
||||||
// against the current world size; toolbar width is in world pixels, so the
|
// against the current world size; toolbar width is in world pixels, so the
|
||||||
// pen-canvas fraction is width / worldWidth.
|
// pen-canvas fraction is width / worldWidth.
|
||||||
return Container(
|
return LayoutBuilder(
|
||||||
color: Theme.of(context).scaffoldBackgroundColor,
|
builder: (context, constraints) {
|
||||||
child: PenCanvas(
|
// On first layout, frame the view so existing ink is actually visible
|
||||||
pageSize: _worldSize,
|
// (otherwise identity shows only the empty top-left corner of the huge
|
||||||
strokes: penStrokesFromInk(_strokes, _worldSize),
|
// world). Empty scratchpad falls back to a comfortable 1:1 near origin.
|
||||||
transformationController: _scratchTransform,
|
if (!_scratchCentered) {
|
||||||
tool: _canvasTool,
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
color: _currentColor,
|
if (!mounted) return;
|
||||||
strokeWidth: _currentStrokeWidth / _canvasWidth,
|
_frameScratchpad(
|
||||||
// The world is huge, so allow zooming further out to survey it.
|
Size(constraints.maxWidth, constraints.maxHeight));
|
||||||
minScale: 0.1,
|
setState(() => _scratchCentered = true);
|
||||||
maxScale: 8.0,
|
});
|
||||||
onStrokeComplete: _onStrokeComplete,
|
}
|
||||||
onEraseStroke: _onErase,
|
return Container(
|
||||||
pageWidget: const ColoredBox(color: Colors.white),
|
color: Theme.of(context).scaffoldBackgroundColor,
|
||||||
),
|
child: PenCanvas(
|
||||||
|
pageSize: _worldSize,
|
||||||
|
strokes: penStrokesFromInk(_strokes, _worldSize),
|
||||||
|
transformationController: _scratchTransform,
|
||||||
|
tool: _canvasTool,
|
||||||
|
color: _currentColor,
|
||||||
|
strokeWidth: _currentStrokeWidth / _canvasWidth,
|
||||||
|
// The world is huge, so allow zooming further out to survey it.
|
||||||
|
minScale: 0.1,
|
||||||
|
maxScale: 8.0,
|
||||||
|
onStrokeComplete: _onStrokeComplete,
|
||||||
|
onEraseStroke: _onErase,
|
||||||
|
pageWidget: const ColoredBox(color: Colors.white),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Position the scratchpad so existing ink is on-screen. Fits the strokes'
|
||||||
|
/// world bounding box into [pane] (with padding, scale clamped); for an empty
|
||||||
|
/// scratchpad, shows the top-left working area at 1:1.
|
||||||
|
void _frameScratchpad(Size pane) {
|
||||||
|
if (pane.isEmpty) return;
|
||||||
|
if (_strokes.isEmpty) {
|
||||||
|
_scratchTransform.value = Matrix4.identity();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
double minX = double.infinity, minY = double.infinity;
|
||||||
|
double maxX = -double.infinity, maxY = -double.infinity;
|
||||||
|
for (final s in _strokes) {
|
||||||
|
for (final p in s.points) {
|
||||||
|
if (p.x < minX) minX = p.x;
|
||||||
|
if (p.y < minY) minY = p.y;
|
||||||
|
if (p.x > maxX) maxX = p.x;
|
||||||
|
if (p.y > maxY) maxY = p.y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (minX > maxX) {
|
||||||
|
_scratchTransform.value = Matrix4.identity();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const pad = 80.0;
|
||||||
|
final boxW = (maxX - minX) + pad * 2;
|
||||||
|
final boxH = (maxY - minY) + pad * 2;
|
||||||
|
final scale =
|
||||||
|
(pane.width / boxW < pane.height / boxH ? pane.width / boxW : pane.height / boxH)
|
||||||
|
.clamp(0.15, 1.5);
|
||||||
|
final cx = (minX + maxX) / 2;
|
||||||
|
final cy = (minY + maxY) / 2;
|
||||||
|
final tx = pane.width / 2 - scale * cx;
|
||||||
|
final ty = pane.height / 2 - scale * cy;
|
||||||
|
_scratchTransform.value = Matrix4.identity()
|
||||||
|
..setEntry(0, 0, scale)
|
||||||
|
..setEntry(1, 1, scale)
|
||||||
|
..setEntry(2, 2, scale)
|
||||||
|
..setTranslationRaw(tx, ty, 0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A marker linking a scratchpad position to a specific PDF page.
|
/// A marker linking a scratchpad position to a specific PDF page.
|
||||||
|
|||||||
@@ -29,6 +29,32 @@ LRESULT CALLBACK FlutterChildSubclassProc(HWND hwnd, UINT message,
|
|||||||
return DefSubclassProc(hwnd, message, wparam, lparam);
|
return DefSubclassProc(hwnd, message, wparam, lparam);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Turn off the OS-drawn pen visual feedback (the "Windows Ink" tap ripple, the
|
||||||
|
// press-and-hold right-click ring, barrel feedback, and flicks) so the pen
|
||||||
|
// draws instantly with no ugly OS animation under the nib. The tablet input
|
||||||
|
// service reads these flags from a window property; set it on whichever window
|
||||||
|
// receives the pointer input. Flag values are from the Windows pen/tablet docs
|
||||||
|
// (not declared in the public SDK headers).
|
||||||
|
void DisablePenVisualFeedback(HWND hwnd) {
|
||||||
|
if (!hwnd) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
constexpr ULONG_PTR kDisablePressAndHold = 0x00000001;
|
||||||
|
constexpr ULONG_PTR kDisablePenTapFeedback = 0x00000008;
|
||||||
|
constexpr ULONG_PTR kDisablePenBarrelFeedback = 0x00000010;
|
||||||
|
constexpr ULONG_PTR kDisableTouchUIForceOn = 0x00000100;
|
||||||
|
constexpr ULONG_PTR kDisableTouchUIForceOff = 0x00000200;
|
||||||
|
constexpr ULONG_PTR kDisableFlicks = 0x00010000;
|
||||||
|
const ULONG_PTR flags = kDisablePressAndHold | kDisablePenTapFeedback |
|
||||||
|
kDisablePenBarrelFeedback | kDisableTouchUIForceOn |
|
||||||
|
kDisableTouchUIForceOff | kDisableFlicks;
|
||||||
|
const ATOM atom = ::GlobalAddAtomW(L"MicrosoftTabletPenServiceProperty");
|
||||||
|
if (atom != 0) {
|
||||||
|
::SetPropW(hwnd, MAKEINTATOM(atom), reinterpret_cast<HANDLE>(flags));
|
||||||
|
::GlobalDeleteAtom(atom);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
FlutterWindow::FlutterWindow(const flutter::DartProject& project)
|
FlutterWindow::FlutterWindow(const flutter::DartProject& project)
|
||||||
@@ -64,6 +90,11 @@ bool FlutterWindow::OnCreate() {
|
|||||||
SetWindowSubclass(flutter_child, FlutterChildSubclassProc, kPenSubclassId, 0);
|
SetWindowSubclass(flutter_child, FlutterChildSubclassProc, kPenSubclassId, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Kill the OS pen tap/press-and-hold visual feedback on both the top-level
|
||||||
|
// window and the Flutter child (the one that actually gets the pointer input).
|
||||||
|
DisablePenVisualFeedback(GetHandle());
|
||||||
|
DisablePenVisualFeedback(flutter_child);
|
||||||
|
|
||||||
flutter_controller_->engine()->SetNextFrameCallback([&]() {
|
flutter_controller_->engine()->SetNextFrameCallback([&]() {
|
||||||
this->Show();
|
this->Show();
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user