snapScene function
SnapResult
snapScene({
- required CanvasSceneDocument doc,
- required ComputedScene computed,
- required Rect2D probeWorld,
- required SnapConfig config,
Scene-graph snapping (Step 6).
Mirrors the logic of your existing snap_engine.dart, but candidates come from:
- artboard keylines
- scene object keylines (bounds-derived)
- transient guide candidates (UI-only)
- optional grid snapping
Implementation
SnapResult snapScene({
required CanvasSceneDocument doc,
required ComputedScene computed, // ✅ NEW
required Rect2D probeWorld, // selection AABB during drag (world coords)
required SnapConfig config,
}) {
final opts = config.options;
final tolWorld = opts.toleranceScreenPx / math.max(opts.zoom, 1e-6);
if (tolWorld <= 0) return SnapResult.none;
// Convert ignore set to NodeId (same underlying type, but keeps intent).
final ignore = config.ignoreIds.cast<NodeId>();
// Gather candidates
final cands = <SnapCandidate>[];
if (opts.snapToKeylines) {
cands.addAll(artboardKeylines(doc.artboardSize));
}
if (opts.snapToObjects) {
cands.addAll(sceneObjectKeylines(doc, computed, ignoreIds: ignore));
}
if (opts.snapToGuides) {
cands.addAll(config.transientGuideCandidates);
}
// Anchors of probe (edges + center)
final probeCenters = (
cx: (probeWorld.left + probeWorld.right) * 0.5,
cy: (probeWorld.top + probeWorld.bottom) * 0.5,
);
final anchorsX = <double>[probeWorld.left, probeCenters.cx, probeWorld.right];
final anchorsY = <double>[probeWorld.top, probeCenters.cy, probeWorld.bottom];
// Respect sticky lockedAxis if provided
final axisConstraint = config.lockedAxis ?? SnapAxis.both;
double bestDx = 0, bestDy = 0;
double bestScoreX = double.infinity, bestScoreY = double.infinity;
SnapCandidate? bestCandX, bestCandY;
// X
if (axisConstraint == SnapAxis.both || axisConstraint == SnapAxis.x) {
final xSnap = _snapAxis(
anchors: anchorsX,
axis: SnapAxis.x,
candidates: cands,
options: opts,
tolWorld: tolWorld,
gridStepWorld: config.gridStepWorld,
);
bestDx = xSnap.delta;
bestScoreX = xSnap.score;
bestCandX = xSnap.candidate;
}
// Y
if (axisConstraint == SnapAxis.both || axisConstraint == SnapAxis.y) {
final ySnap = _snapAxis(
anchors: anchorsY,
axis: SnapAxis.y,
candidates: cands,
options: opts,
tolWorld: tolWorld,
gridStepWorld: config.gridStepWorld,
);
bestDy = ySnap.delta;
bestScoreY = ySnap.score;
bestCandY = ySnap.candidate;
}
if (!bestScoreX.isFinite && !bestScoreY.isFinite) return SnapResult.none;
final hasX = bestScoreX.isFinite;
final hasY = bestScoreY.isFinite;
// Compute inner dual-axis threshold in SCREEN px
double innerTolPx0(SnapOptions opts) {
if (opts.innerDualAxisTolPx != null && opts.innerDualAxisTolPx! > 0) {
return opts.innerDualAxisTolPx!;
}
if (opts.innerDualAxisTolFactor > 0) {
return opts.toleranceScreenPx * opts.innerDualAxisTolFactor;
}
return 0.0; // disabled
}
final innerTolPx = innerTolPx0(opts);
// Default: apply nearest single axis
bool applyX = hasX && (!hasY || bestScoreX <= bestScoreY);
bool applyY = hasY && (!hasX || bestScoreY < bestScoreX);
// Upgrade to dual-axis if both are within inner threshold and not axis-locked
final canApplyBoth = (axisConstraint == SnapAxis.both);
if (canApplyBoth && innerTolPx > 0 && hasX && hasY) {
final bothWithinInnerTol =
(bestScoreX <= innerTolPx) && (bestScoreY <= innerTolPx);
if (bothWithinInnerTol) {
applyX = true;
applyY = true;
}
}
final dx = applyX ? bestDx : 0.0;
final dy = applyY ? bestDy : 0.0;
final axisApplied = applyX
? (applyY ? SnapAxis.both : SnapAxis.x)
: (applyY ? SnapAxis.y : SnapAxis.none);
final score = math.min(bestScoreX, bestScoreY);
// Build guides (world coords), clipped by frame
final frame =
config.guideFrameWorld ??
Rect2D.fromLTWH(0, 0, doc.artboardSize.w, doc.artboardSize.h);
final guides = <SnapLine>[];
final canShowBoth = (axisConstraint == SnapAxis.both);
if (hasX && bestCandX != null) {
final showX = applyX || (opts.showBothGuidesWhenClose && canShowBoth);
if (showX) {
guides.add(_makeGuide(bestCandX.kind, SnapAxis.x, bestCandX.pos, frame));
}
}
if (hasY && bestCandY != null) {
final showY = applyY || (opts.showBothGuidesWhenClose && canShowBoth);
if (showY) {
guides.add(_makeGuide(bestCandY.kind, SnapAxis.y, bestCandY.pos, frame));
}
}
return SnapResult(Vec2(dx, dy), axisApplied, score, guides);
}