Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
085114c
refactor: improve keyboard focus handling and marker selection via da…
IanMayo Aug 8, 2025
98ebffc
feat: add new debug page URL to settings for testing
IanMayo Aug 8, 2025
9ed9b24
feat: set initial guidance content in mode UI after recreation
IanMayo Aug 8, 2025
6a44dc4
feat: implement secure HTML rendering for guidance content and update…
IanMayo Aug 8, 2025
e6509d1
refactor: remove deprecated parseGuidanceHTML function and related co…
IanMayo Aug 8, 2025
1f6d8d0
feat: add task assignment for fixing manual harmonics visibility when…
IanMayo Aug 8, 2025
a1fcefd
fix: ensure manual harmonics are positioned in visible time period wh…
IanMayo Aug 8, 2025
9a84688
fix: remove redundant test command from pre-push hook
IanMayo Aug 8, 2025
534eeff
refactor: use epsilon comparison for floating point zoom level check
IanMayo Aug 8, 2025
ccb3cc6
feat: implement standardized tolerance handling across analysis, dopp…
IanMayo Aug 8, 2025
497bc13
refactor: replace mode-specific tolerances with uniform tolerance cal…
IanMayo Aug 8, 2025
57846cd
refactor: update configuration table format from 3-column legacy to 2…
IanMayo Aug 8, 2025
8bcd577
Merge branch 'main' into issue-139
IanMayo Aug 8, 2025
f5ba6e4
refactor: move common viewport and cursor methods to BaseMode class
IanMayo Aug 11, 2025
f83f77c
refactor: flatten axes config by moving margins to root state level
IanMayo Aug 11, 2025
59c0970
fix: update margins reference to use correct state property path
IanMayo Aug 11, 2025
248fe36
Merge pull request #146 from DeepBlueCLtd/simplify-axes-config
IanMayo Aug 11, 2025
a1f3cbc
refactor state in modes
IanMayo Aug 11, 2025
e3661cb
Merge pull request #147 from DeepBlueCLtd/refactor-remove-redundant-s…
IanMayo Aug 11, 2025
87959b0
fix: always show color picker container instead of conditionally hidi…
IanMayo Aug 11, 2025
d87326a
feat: implement mode-specific tolerance for analysis markers and enha…
IanMayo Aug 8, 2025
5e114a5
refactor: remove debug console statements and update cursor handling …
IanMayo Aug 8, 2025
dfb6dfa
feat: update cursor behavior in Doppler mode and add interaction test…
IanMayo Aug 8, 2025
857f5e1
feat: add selection border visibility and flicker tests with enhanced…
IanMayo Aug 8, 2025
f4658cc
feat: enhance selection visuals to target instance container and auto…
IanMayo Aug 8, 2025
a6eb787
CG5 patches
IanMayo Aug 8, 2025
fe8eda1
more fixes
IanMayo Aug 8, 2025
e84a520
feat: add comprehensive tests for keyboard focus behavior and basic f…
IanMayo Aug 8, 2025
aeee21b
re-implement test on push
IanMayo Aug 8, 2025
d65dd10
feat: implement mode-specific tolerance for analysis markers and enha…
IanMayo Aug 8, 2025
46844f1
refactor: remove debug console statements and update cursor handling …
IanMayo Aug 8, 2025
cf327a6
feat: update cursor behavior in Doppler mode and add interaction test…
IanMayo Aug 8, 2025
7a6fbeb
feat: enhance selection visuals to target instance container and auto…
IanMayo Aug 8, 2025
d79b467
CG5 patches
IanMayo Aug 8, 2025
962c19d
more fixes
IanMayo Aug 8, 2025
0c02ae7
feat: add comprehensive tests for keyboard focus behavior and basic f…
IanMayo Aug 8, 2025
1e41776
feat: implement mode-specific tolerance for analysis markers and enha…
IanMayo Aug 8, 2025
679db9f
refactor: remove debug console statements and update cursor handling …
IanMayo Aug 8, 2025
a86db48
feat: update cursor behavior in Doppler mode and add interaction test…
IanMayo Aug 8, 2025
36ab831
feat: enhance selection visuals to target instance container and auto…
IanMayo Aug 8, 2025
a2de81c
CG5 patches
IanMayo Aug 8, 2025
9704736
more fixes
IanMayo Aug 8, 2025
17b10dc
feat: add comprehensive tests for keyboard focus behavior and basic f…
IanMayo Aug 8, 2025
d7c4721
fix: resolve remaining merge conflicts and clean up state references
IanMayo Aug 11, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .claude/settings.local.json
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,8 @@
"Bash(touch:*)",
"Bash(open http://localhost:5173/debug-multiple.html)",
"Bash(open http://localhost:5173/debug-focus-test.html)",
"Bash(open http://localhost:5175/debug.html)"
"Bash(open http://localhost:5175/debug.html)",
"Bash(git add:*)"
]
}
}
39 changes: 39 additions & 0 deletions Memory_Bank.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,45 @@ None
**Next Steps:**
Ready for pull request creation. Implementation successfully addresses GitHub Issue #126 by eliminating the need to commit version.js changes after local development builds.

## Issue #130 - Fix Manual Harmonics Visibility When Zoomed - 2025-01-08

**Task Reference:** GitHub Issue #130 and Task Assignment Prompt Task_Issue_130.md
**Problem:** Manual harmonics were placed at the center of the overall time period instead of the center of the currently visible (zoomed) time period, making them invisible to users when zoomed in.

**Root Cause Analysis:**
- In `ManualHarmonicModal.js` (lines 88-89), when no cursor position was available, anchor time was calculated using `(state.config.timeMin + state.config.timeMax) / 2`
- This used the **full time range** rather than the **visible time range** when zoomed
- Users had to zoom out to see newly created manual harmonics, degrading user experience

**Solution Implemented:**
1. **Exported existing infrastructure:** Modified `calculateVisibleDataRange()` function in `src/components/table.js` from private to exported function to enable reuse
2. **Enhanced modal interface:** Updated `showManualHarmonicModal()` signature to accept GramFrame instance parameter for access to zoom state
3. **Created helper function:** Added `calculateVisibleTimePeriodCenter(state, instance)` in `ManualHarmonicModal.js` that:
- Returns full time range center when zoom level = 1.0 (backward compatibility)
- Calculates visible time range center using `calculateVisibleDataRange()` when zoomed
4. **Updated anchor calculation:** Modified `addHarmonic()` function to use zoom-aware center calculation when no cursor position available

**Key Code Changes:**
- `src/components/table.js`: Exported `calculateVisibleDataRange()` function
- `src/modes/harmonics/HarmonicsMode.js`: Updated modal call to pass instance parameter
- `src/modes/harmonics/ManualHarmonicModal.js`:
- Added import for `calculateVisibleDataRange`
- Added `calculateVisibleTimePeriodCenter()` helper function
- Updated modal signature and anchor time calculation logic

**Testing and Verification:**
- All harmonics mode tests pass (10/10 tests successful)
- TypeScript compilation successful with no errors
- Build process successful with no issues
- No regression in existing functionality confirmed

**Technical Decisions:**
- Leveraged existing `calculateVisibleDataRange()` infrastructure rather than duplicating zoom logic
- Maintained backward compatibility by preserving behavior when zoom level = 1.0
- Used dependency injection pattern to pass GramFrame instance to modal for clean separation of concerns

**Result:** Manual harmonics are now positioned at the center of the visible viewport when zoomed, immediately visible to users without requiring zoom out operation.

## Issue #88 Phase A-B - Refactor Large Files and Improve Module Boundaries - 2025-01-06

**Task Reference:** GitHub Issue #88 - Refactor Large Files and Improve Module Boundaries
Expand Down
2 changes: 1 addition & 1 deletion src/components/ColorPicker.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ const COLOR_PALETTE = [
export function createColorPicker(state) {
const container = document.createElement('div')
container.className = 'gram-frame-color-picker'
container.style.display = (state.mode === 'harmonics' || state.mode === 'analysis') ? 'block' : 'none'
container.style.display = 'block'
Copy link
Preview

Copilot AI Aug 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The color picker is now always visible (display: 'block') regardless of mode, but the original logic showed it only for 'harmonics' and 'analysis' modes. This change should be verified to ensure it doesn't negatively impact UX in modes where color selection isn't relevant.

Suggested change
container.style.display = 'block'
if (state.mode === 'harmonics' || state.mode === 'analysis') {
container.style.display = 'block'
} else {
container.style.display = 'none'
}

Copilot uses AI. Check for mistakes.


// Label
const label = document.createElement('div')
Expand Down
8 changes: 4 additions & 4 deletions src/components/table.js
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ export function setupSpectrogramImage(instance, imageUrl) {
*/
export function updateSVGLayout(instance) {
const { naturalWidth, naturalHeight } = instance.state.imageDetails
const margins = instance.state.axes.margins
const margins = instance.state.margins

if (!naturalWidth || !naturalHeight) {
return
Expand Down Expand Up @@ -208,7 +208,7 @@ export function updateSVGLayout(instance) {
export function applyZoomTransform(instance) {
const { level, centerX, centerY } = instance.state.zoom
const { naturalWidth, naturalHeight } = instance.state.imageDetails
const margins = instance.state.axes.margins
const margins = instance.state.margins

if (!instance.spectrogramImage) {
return
Expand Down Expand Up @@ -273,7 +273,7 @@ export function renderAxes(instance) {
instance.axesGroup.innerHTML = ''

const { naturalWidth, naturalHeight } = instance.state.imageDetails
const margins = instance.state.axes.margins
const margins = instance.state.margins

if (!naturalWidth || !naturalHeight) {
return
Expand All @@ -297,7 +297,7 @@ export function renderAxes(instance) {
export function calculateVisibleDataRange(instance) {
const { timeMin, timeMax, freqMin, freqMax } = instance.state.config
const { naturalWidth, naturalHeight } = instance.state.imageDetails
const margins = instance.state.axes.margins
const margins = instance.state.margins
const zoomLevel = instance.state.zoom.level

if (zoomLevel === 1.0) {
Expand Down
6 changes: 2 additions & 4 deletions src/core/FocusManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,8 @@ let registeredInstances = new Set()
export function registerInstance(instance) {
registeredInstances.add(instance)

// If this is the first instance, make it focused by default
if (registeredInstances.size === 1 && !currentFocusedInstance) {
setFocusedInstance(instance)
}
// Don't auto-focus the first instance - let user explicitly interact to focus
// This prevents unwanted focus behavior when multiple instances exist on a page
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/core/events.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ function screenToDataWithZoom(instance, event) {
const svgCoords = screenToSVGCoordinates(screenX, screenY, instance.svg, instance.state.imageDetails)

// Convert to data coordinates (accounting for margins and zoom)
const margins = instance.state.axes.margins
const margins = instance.state.margins
const zoomLevel = instance.state.zoom.level
const { naturalWidth, naturalHeight } = instance.state.imageDetails

Expand Down
2 changes: 1 addition & 1 deletion src/core/initialization/ModeInitialization.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export function initializeModeInfrastructure(instance) {
// Initialize all modes using factory
const availableModes = ModeFactory.getAvailableModes()
availableModes.forEach(modeName => {
instance.modes[modeName] = ModeFactory.createMode(modeName, instance, instance.state)
instance.modes[modeName] = ModeFactory.createMode(modeName, instance)
})
}

Expand Down
13 changes: 8 additions & 5 deletions src/core/keyboardControl.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ function handleGlobalKeyboardEvent(event) {
return // No selection
}


// Prevent default browser behavior
event.preventDefault()
event.stopPropagation()
Expand Down Expand Up @@ -157,13 +158,15 @@ function moveSelectedMarker(instance, markerId, movement) {
return
}


// Convert current marker position to SVG coordinates
const currentSVG = dataToSVGCoordinates(
marker.freq,
marker.time,
instance.state.config,
instance.state.imageDetails,
instance.state.rate
instance.state.rate,
instance.state.margins
)

// Apply movement in SVG space
Expand All @@ -179,7 +182,7 @@ function moveSelectedMarker(instance, markerId, movement) {
instance.state.config,
instance.state.imageDetails,
instance.state.rate,
instance.state.axes.margins
instance.state.margins
)

// Update marker position
Expand Down Expand Up @@ -239,7 +242,7 @@ function moveSelectedHarmonicSet(instance, harmonicSetId, movement) {
// Convert current anchor time to SVG coordinates
const { naturalHeight } = instance.state.imageDetails
const { timeMin, timeMax } = instance.state.config
const margins = instance.state.axes.margins
const margins = instance.state.margins

// Calculate current anchor position in SVG space
const normalizedTime = 1.0 - (harmonicSet.anchorTime - timeMin) / (timeMax - timeMin)
Expand Down Expand Up @@ -290,12 +293,12 @@ function moveSelectedHarmonicSet(instance, harmonicSetId, movement) {
* @param {Config} config - Configuration object
* @param {ImageDetails} imageDetails - Image dimensions
* @param {number} rate - Rate scaling factor
* @param {AxesMargins} margins - Axes margins
* @returns {SVGCoordinates} SVG coordinates {x, y}
*/
function dataToSVGCoordinates(freq, time, config, imageDetails, rate) {
function dataToSVGCoordinates(freq, time, config, imageDetails, rate, margins) {
const { freqMin, freqMax, timeMin, timeMax } = config
const { naturalWidth, naturalHeight } = imageDetails
const margins = { left: 60, top: 15 } // Use default margins

// Convert frequency back to raw frequency space for positioning
const rawFreq = freq * rate
Expand Down
12 changes: 5 additions & 7 deletions src/core/state.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,13 +58,11 @@ export const initialState = {
width: 0,
height: 0
},
axes: {
margins: {
left: 60, // Space for time axis labels
bottom: 50, // Space for frequency axis labels
right: 15, // Small right margin
top: 15 // Small top margin
}
margins: {
left: 60, // Space for time axis labels
bottom: 50, // Space for frequency axis labels
right: 15, // Small right margin
top: 15 // Small top margin
},
// Simple zoom state for transform-based zoom
zoom: {
Expand Down
27 changes: 24 additions & 3 deletions src/modes/BaseMode.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,9 @@ export class BaseMode {
/**
* Constructor for base mode
* @param {GramFrame} instance - GramFrame instance
* @param {GramFrameState} state - GramFrame state object
*/
constructor(instance, state) {
constructor(instance) {
this.instance = instance
this.state = state
}

/**
Expand Down Expand Up @@ -179,4 +177,27 @@ export class BaseMode {
// Default implementation - override in subclasses
return {}
}

/**
* Get viewport configuration for coordinate transformations
* @returns {ViewportConfig} Viewport configuration object
*/
getViewport() {
return {
margins: this.instance.state.margins,
imageDetails: this.instance.state.imageDetails,
config: this.instance.state.config,
zoom: this.instance.state.zoom
}
}

/**
* Update cursor style for drag operations
* @param {string} style - Cursor style ('crosshair', 'grab', 'grabbing')
*/
updateCursorStyle(style) {
if (this.instance.spectrogramImage) {
this.instance.spectrogramImage.style.cursor = style
}
}
}
15 changes: 7 additions & 8 deletions src/modes/ModeFactory.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,24 +13,23 @@ export class ModeFactory {
* Create a mode instance based on mode name
* @param {ModeType} modeName - Name of the mode
* @param {GramFrame} instance - GramFrame instance
* @param {GramFrameState} state - GramFrame state object
* @returns {BaseMode} Mode instance
* @throws {Error} If mode name is invalid or mode class is not available
*/
static createMode(modeName, instance, state) {
static createMode(modeName, instance) {
try {
switch (modeName) {
case 'analysis':
return new AnalysisMode(instance, state)
return new AnalysisMode(instance)

case 'harmonics':
return new HarmonicsMode(instance, state)
return new HarmonicsMode(instance)

case 'doppler':
return new DopplerMode(instance, state)
return new DopplerMode(instance)

case 'pan':
return new PanMode(instance, state)
return new PanMode(instance)

default:
throw new Error(`Invalid mode name: ${modeName}. Valid modes are: analysis, harmonics, doppler, pan`)
Expand All @@ -42,7 +41,7 @@ export class ModeFactory {
stack: error instanceof Error ? error.stack : undefined,
modeName,
instanceType: instance?.constructor?.name,
stateExists: !!state
stateExists: !!instance?.state
})

// In test environments, throw the error to fail fast
Expand All @@ -52,7 +51,7 @@ export class ModeFactory {

// Fallback to base mode to prevent complete failure in production
console.warn(`Falling back to BaseMode for "${modeName}" due to error`)
return new BaseMode(instance, state)
return new BaseMode(instance)
}
}

Expand Down
Loading