From 4816ded13aca970336bd64c1edecbf106f25a229 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 23 Feb 2026 06:27:04 +0000 Subject: [PATCH] Improve digitizing cursor contrast and lock tools during tracking Apply the same dual-layer shadow technique used by CalibrationToolNode and CoordinateSystemNode to the digitizing crosshair cursor. A dark shadow stroke (4 px) renders beneath the white cursor stroke (1.5 px), giving good contrast on both light and dark video backgrounds instead of relying on a single color that only works on one background type. Also prevent accidental interference with the calibration tool and coordinate system while a track is being manually digitized: both nodes dim to 35% opacity and become non-interactive (pickable: false) when activeTrackIdProperty is set, restoring to full opacity and interactivity once digitizing ends. https://claude.ai/code/session_01VAVzhrFTMNh4duuDe4v2uh --- src/screen-name/view/CalibrationToolNode.ts | 11 +++++ src/screen-name/view/CoordinateSystemNode.ts | 12 ++++++ src/screen-name/view/DigitizingOverlayNode.ts | 40 ++++++++++--------- 3 files changed, 44 insertions(+), 19 deletions(-) diff --git a/src/screen-name/view/CalibrationToolNode.ts b/src/screen-name/view/CalibrationToolNode.ts index 2d8e0a8..5467416 100644 --- a/src/screen-name/view/CalibrationToolNode.ts +++ b/src/screen-name/view/CalibrationToolNode.ts @@ -264,9 +264,20 @@ export class CalibrationToolNode extends Node { }; videoLoadedProperty.link(onVideoLoaded); + // ── Lock out interaction while the user is manually digitizing ───────── + // Dimming + pickable:false signals that the tool is temporarily inactive + // so the user cannot accidentally move calibration points mid-session. + const onActiveTrackChange = (activeId: string | null) => { + const isDigitizing = activeId !== null; + this.pickable = !isDigitizing; + this.opacity = isDigitizing ? 0.35 : 1; + }; + model.activeTrackIdProperty.link(onActiveTrackChange); + this.disposeCalibrationToolNode = () => { calibMultilink.dispose(); videoLoadedProperty.unlink(onVideoLoaded); + model.activeTrackIdProperty.unlink(onActiveTrackChange); rangePatternProperty.dispose(); buttonLabelProperty.dispose(); }; diff --git a/src/screen-name/view/CoordinateSystemNode.ts b/src/screen-name/view/CoordinateSystemNode.ts index 79a43be..f69e424 100644 --- a/src/screen-name/view/CoordinateSystemNode.ts +++ b/src/screen-name/view/CoordinateSystemNode.ts @@ -228,10 +228,22 @@ export class CoordinateSystemNode extends Node { }; videoLoadedProperty.link(onVideoLoaded); + // ── Lock out interaction while the user is manually digitizing ───────── + // Dimming + pickable:false signals that the coordinate system is temporarily + // inactive so the user cannot accidentally move or rotate the axes while + // placing track points. + const onActiveTrackChange = (activeId: string | null) => { + const isDigitizing = activeId !== null; + this.pickable = !isDigitizing; + this.opacity = isDigitizing ? 0.35 : 1; + }; + model.activeTrackIdProperty.link(onActiveTrackChange); + this.disposeCoordinateSystemNode = () => { model.coordOriginProperty.unlink(onOriginChange); model.coordAngleProperty.unlink(onAngleChange); videoLoadedProperty.unlink(onVideoLoaded); + model.activeTrackIdProperty.unlink(onActiveTrackChange); }; } diff --git a/src/screen-name/view/DigitizingOverlayNode.ts b/src/screen-name/view/DigitizingOverlayNode.ts index 9edd625..5216542 100644 --- a/src/screen-name/view/DigitizingOverlayNode.ts +++ b/src/screen-name/view/DigitizingOverlayNode.ts @@ -7,7 +7,7 @@ import { type Dimension2, Vector2 } from "scenerystack/dot"; import { Shape } from "scenerystack/kite"; -import { DOM, FireListener, Line, Node, Path, Rectangle } from "scenerystack/scenery"; +import { DOM, FireListener, Node, Path, Rectangle } from "scenerystack/scenery"; import { Tandem } from "scenerystack/tandem"; import TrackLabColors from "../../TrackLabColors.js"; import { VIDEO_HEIGHT, VIDEO_WIDTH } from "../../TrackLabConstants.js"; @@ -16,6 +16,7 @@ import type { SimModel } from "../model/SimModel.js"; const OUTER_R = 12; const INNER_R = 2; const CUR_LW = 1.5; +const CUR_SHADOW_LW = 4; // wider shadow stroke for contrast on all backgrounds const MAG_SIZE = 100; const MAG_ZOOM = 4; @@ -41,24 +42,25 @@ export class DigitizingOverlayNode extends Node { let magCrosshairColor = TrackLabColors.digitizingMagnifierCrosshairProperty.value.toCSS(); let magShadowColor = TrackLabColors.digitizingMagnifierShadowProperty.value.toCSS(); - // Custom cursor: large circle + 4 segments that stop at the empty centre - const cursorCircle = new Path(Shape.circle(0, 0, OUTER_R), { - stroke: TrackLabColors.digitizingCursorStrokeProperty, - lineWidth: CUR_LW, - }); - const cursorLineLeft = new Line(-OUTER_R, 0, -INNER_R, 0, { - stroke: TrackLabColors.digitizingCursorStrokeProperty, - lineWidth: CUR_LW, - }); - const cursorLineRight = new Line(INNER_R, 0, OUTER_R, 0, { - stroke: TrackLabColors.digitizingCursorStrokeProperty, - lineWidth: CUR_LW, - }); - const cursorLineTop = new Line(0, -OUTER_R, 0, -INNER_R, { - stroke: TrackLabColors.digitizingCursorStrokeProperty, - lineWidth: CUR_LW, + // Custom cursor: circle + 4 crosshair segments that stop at an empty centre. + // Built as a single composite shape shared by both layers so positions are + // always in sync. The shadow layer (wider, dark stroke) renders first and + // gives the same dual-layer contrast used by CalibrationToolNode and + // CoordinateSystemNode — visible on both dark and light video backgrounds. + const cursorShape = new Shape(); + cursorShape.circle(0, 0, OUTER_R); + cursorShape.moveTo(-OUTER_R, 0).lineTo(-INNER_R, 0); + cursorShape.moveTo(INNER_R, 0).lineTo(OUTER_R, 0); + cursorShape.moveTo(0, -OUTER_R).lineTo(0, -INNER_R); + cursorShape.moveTo(0, INNER_R).lineTo(0, OUTER_R); + + // Shadow layer (rendered first, underneath) for contrast on all backgrounds + const cursorShadow = new Path(cursorShape, { + stroke: TrackLabColors.coordShadowStrokeProperty, + lineWidth: CUR_SHADOW_LW, }); - const cursorLineBottom = new Line(0, INNER_R, 0, OUTER_R, { + // Main cursor stroke (rendered on top of shadow) + const cursorMain = new Path(cursorShape, { stroke: TrackLabColors.digitizingCursorStrokeProperty, lineWidth: CUR_LW, }); @@ -66,7 +68,7 @@ export class DigitizingOverlayNode extends Node { const cursorNode = new Node({ visible: false, pickable: false, - children: [cursorCircle, cursorLineLeft, cursorLineRight, cursorLineTop, cursorLineBottom], + children: [cursorShadow, cursorMain], }); // ── Magnifier (zoomed view near the cursor) ─────────────────────────────