From cbba30bb72a2d0a1f74b69dc6e10a4f569c31918 Mon Sep 17 00:00:00 2001 From: waaraawa Date: Tue, 14 Oct 2025 13:16:36 +0900 Subject: [PATCH] feat(core): improve legendColumns with row-major order - Change legend layout from column-major to row-major order - Fields now fill left-to-right, then top-to-bottom - More intuitive for users (e.g., 4 fields + 3 cols = 3-1 layout) - Fix bottom position width calculation for multi-column legends - totalWidth now accounts for legend width (200px per column) - Add tests for 3-column layout with 6 fields --- examples/README.md | 2 - packages/core/src/parser.ts | 6 + packages/core/src/svgRenderer.ts | 92 ++++++-- packages/core/src/types.ts | 2 + packages/core/tests/parser.test.ts | 50 ++++ packages/core/tests/svgRenderer.test.ts | 133 +++++++++++ roadmap.md | 288 ++++++++++++++++++++++++ 7 files changed, 553 insertions(+), 20 deletions(-) create mode 100644 roadmap.md diff --git a/examples/README.md b/examples/README.md index e36e8f7..d1105c7 100644 --- a/examples/README.md +++ b/examples/README.md @@ -118,5 +118,3 @@ fields: **layoutUnit field:** - Can be omitted (auto-inferred from suffix) - Can be explicit (use `layoutUnit: bit` or `layoutUnit: byte` if desired) - -For more details, see [CLAUDE.md](../CLAUDE.md). diff --git a/packages/core/src/parser.ts b/packages/core/src/parser.ts index 4632d5f..6c549df 100644 --- a/packages/core/src/parser.ts +++ b/packages/core/src/parser.ts @@ -247,6 +247,11 @@ export function parse(source: string): ByteGridConfig { ? (obj.legendPosition as 'right' | 'left' | 'bottom' | 'none') : undefined; + // Parse legendColumns (optional) + const legendColumns = obj.legendColumns && typeof obj.legendColumns === 'number' + ? obj.legendColumns + : undefined; + // Parse showFooter (optional) const showFooter = obj.showFooter !== undefined && typeof obj.showFooter === 'boolean' ? obj.showFooter @@ -262,6 +267,7 @@ export function parse(source: string): ByteGridConfig { layoutUnit: finalLayoutUnit, colorScheme, legendPosition, + legendColumns, showFooter, fields, }; diff --git a/packages/core/src/svgRenderer.ts b/packages/core/src/svgRenderer.ts index 30e4a12..76bc2b2 100644 --- a/packages/core/src/svgRenderer.ts +++ b/packages/core/src/svgRenderer.ts @@ -27,6 +27,7 @@ const DEFAULT_OPTIONS: Required = { showHexDump: false, showLegend: true, legendPosition: 'right', + legendColumns: 1, showFooter: true, showGrid: true, cellWidth: 40, // Increased from 30 to make bit numbers readable @@ -78,6 +79,14 @@ export function renderSVG( ? config.showFooter : opts.showFooter; + // Determine legendColumns + // Priority: options.legendColumns > config.legendColumns > default (1) + const effectiveLegendColumns = options?.legendColumns !== undefined + ? Math.max(1, Math.floor(options.legendColumns)) + : config.legendColumns !== undefined + ? Math.max(1, Math.floor(config.legendColumns)) + : opts.legendColumns; + // Detect rows with bitfields const rowsWithBitfields = new Set(); for (const block of blocks) { @@ -94,7 +103,10 @@ export function renderSVG( // Calculate dimensions const rows = Math.ceil(config.size / layout); const gridWidth = layout * opts.cellWidth; - const legendWidth = effectiveLegendPosition !== 'none' && effectiveLegendPosition !== 'bottom' ? 200 : 0; + const legendColumnWidth = 200; // Width of each legend column + const legendWidth = effectiveLegendPosition !== 'none' && effectiveLegendPosition !== 'bottom' + ? legendColumnWidth * effectiveLegendColumns + : 0; const margin = 20; // Count unique fields for legend height calculation @@ -104,7 +116,7 @@ export function renderSVG( // Calculate legend height (for 'bottom' position or side positions) let legendHeight = 0; if (effectiveLegendPosition !== 'none' && fieldCount > 0) { - // Calculate actual height needed based on bitfields + // Calculate actual height needed based on bitfields (row-major order) let totalLegendHeight = 40; // Header space const uniqueFields = new Map(); for (const block of blocks) { @@ -112,13 +124,25 @@ export function renderSVG( uniqueFields.set(block.fieldName, block); } } + + // For multi-column row-major, we need to find the tallest entry in each row + const rowHeights: number[] = []; + let fieldIndex = 0; for (const [, block] of uniqueFields) { + const rowIndex = Math.floor(fieldIndex / effectiveLegendColumns); let entryHeight = 50; if (block.bitfields && block.bitfields.length > 0) { entryHeight = 50 + block.bitfields.length * 12; } - totalLegendHeight += entryHeight; + rowHeights[rowIndex] = Math.max(rowHeights[rowIndex] || 0, entryHeight); + fieldIndex++; } + + // Sum up all row heights + for (const h of rowHeights) { + totalLegendHeight += h; + } + legendHeight = totalLegendHeight; } @@ -153,7 +177,9 @@ export function renderSVG( gridStartY = margin + 60; } else if (effectiveLegendPosition === 'bottom') { // Legend at bottom (with extra 30px spacing for title) - totalWidth = margin + gridWidth + margin; + // Need to account for multi-column legend width + const bottomLegendWidth = legendColumnWidth * effectiveLegendColumns; + totalWidth = margin + Math.max(gridWidth, bottomLegendWidth) + margin; totalHeight = margin + 60 + gridHeight + margin + 30 + legendHeight + footerHeight; gridStartX = margin; gridStartY = margin + 60; @@ -306,45 +332,75 @@ export function renderSVG( } } - let index = 0; - for (const [fieldName, block] of uniqueFields) { - let entryHeight = 50; // Default height per field + // Calculate number of rows (row-major order: fill columns left-to-right) + const totalFields = uniqueFields.size; + const totalRows = Math.ceil(totalFields / effectiveLegendColumns); - // Calculate height needed if bitfields exist - if (block.bitfields && block.bitfields.length > 0) { - entryHeight = 50 + block.bitfields.length * 12; // Extra 12px per bitfield + // Calculate cumulative Y positions for each row (to handle variable heights) + const rowYPositions: number[] = [legendStartY]; + const fieldsArray = Array.from(uniqueFields.entries()); + + for (let row = 0; row < totalRows; row++) { + let maxHeightInRow = 50; // Default height + + // Check all fields in this row across all columns (row-major) + for (let col = 0; col < effectiveLegendColumns; col++) { + const fieldIndex = row * effectiveLegendColumns + col; + if (fieldIndex < totalFields) { + const [, block] = fieldsArray[fieldIndex]; + let entryHeight = 50; + if (block.bitfields && block.bitfields.length > 0) { + entryHeight = 50 + block.bitfields.length * 12; + } + maxHeightInRow = Math.max(maxHeightInRow, entryHeight); + } } - const y = legendStartY + index * entryHeight; + if (row < totalRows - 1) { + rowYPositions.push(rowYPositions[row] + maxHeightInRow); + } + } + + // Render fields in row-major order (left-to-right, then top-to-bottom) + let fieldIndex = 0; + for (const [fieldName, block] of uniqueFields) { + // Calculate row and column for this field (row-major) + const row = Math.floor(fieldIndex / effectiveLegendColumns); + const col = fieldIndex % effectiveLegendColumns; + + // Calculate position + const x = legendX + col * legendColumnWidth; + const y = rowYPositions[row]; + const color = COLORS[block.color] || COLORS.gray; // Color box - svg += ``; + svg += ``; // Field name - svg += `${escapeXml(fieldName)}`; + svg += `${escapeXml(fieldName)}`; // Type - svg += `${escapeXml(block.fieldType)}`; + svg += `${escapeXml(block.fieldType)}`; // Offset info const unit = layoutUnit === 'bit' ? 'bits' : 'bytes'; const size = block.offsetEnd - block.offsetStart + 1; - svg += `offset: ${block.offsetStart}-${block.offsetEnd} (${size} ${unit})`; + svg += `offset: ${block.offsetStart}-${block.offsetEnd} (${size} ${unit})`; // Bitfields (if any) if (block.bitfields && block.bitfields.length > 0) { - svg += `Bits:`; + svg += `Bits:`; block.bitfields.forEach((bf, bfIndex) => { const bfY = y + 60 + bfIndex * 12; - svg += ``; + svg += ``; svg += `bit ${escapeXml(bf.bits)}: ${escapeXml(bf.name)}`; svg += ``; }); } - index++; + fieldIndex++; } } diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index fcd438b..b236200 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -92,6 +92,7 @@ export interface ByteGridConfig { layoutUnit?: LayoutUnit; // 'byte' (default) or 'bit' colorScheme?: ColorScheme; legendPosition?: LegendPosition; // 'right' (default), 'left', 'bottom', or 'none' + legendColumns?: number; // Number of columns in legend (default: 1) showFooter?: boolean; // Show footer with total size and layout info (default: true) fields: Field[]; } @@ -131,6 +132,7 @@ export interface RenderOptions { showHexDump?: boolean; showLegend?: boolean; // Deprecated: use legendPosition instead legendPosition?: LegendPosition; // 'right' (default), 'left', 'bottom', or 'none' + legendColumns?: number; // Number of columns in legend (default: 1) showFooter?: boolean; // Show footer with total size and layout info (default: true) showGrid?: boolean; cellWidth?: number; diff --git a/packages/core/tests/parser.test.ts b/packages/core/tests/parser.test.ts index 1ac0580..800f272 100644 --- a/packages/core/tests/parser.test.ts +++ b/packages/core/tests/parser.test.ts @@ -624,5 +624,55 @@ fields: expect(result.showFooter).toBeUndefined(); }); }); + + // legendColumns tests + describe('legendColumns', () => { + it('should parse legendColumns: 1', () => { + const yaml = ` +name: Test +size: 16 +legendColumns: 1 +fields: + - offset: 0-3 + name: Field1 + type: uint32_t +`; + + const result = parse(yaml); + + expect(result.legendColumns).toBe(1); + }); + + it('should parse legendColumns: 2', () => { + const yaml = ` +name: Test +size: 16 +legendColumns: 2 +fields: + - offset: 0-3 + name: Field1 + type: uint32_t +`; + + const result = parse(yaml); + + expect(result.legendColumns).toBe(2); + }); + + it('should return undefined when legendColumns is not specified', () => { + const yaml = ` +name: Test +size: 16 +fields: + - offset: 0-3 + name: Field1 + type: uint32_t +`; + + const result = parse(yaml); + + expect(result.legendColumns).toBeUndefined(); + }); + }); }); }); diff --git a/packages/core/tests/svgRenderer.test.ts b/packages/core/tests/svgRenderer.test.ts index c494f5e..9319afd 100644 --- a/packages/core/tests/svgRenderer.test.ts +++ b/packages/core/tests/svgRenderer.test.ts @@ -483,5 +483,138 @@ describe('SVGRenderer', () => { expect(svg).not.toContain('Total size:'); }); }); + + // legendColumns tests + describe('legendColumns option', () => { + const multiFieldConfig: ByteGridConfig = { + name: 'Multi Field Test', + size: 16, + layout: 16, + fields: [ + { offset: '0-3', name: 'Field1', type: 'uint32_t', color: 'blue' }, + { offset: '4-7', name: 'Field2', type: 'uint32_t', color: 'cyan' }, + { offset: '8-11', name: 'Field3', type: 'uint32_t', color: 'yellow' }, + { offset: '12-15', name: 'Field4', type: 'uint32_t', color: 'green' }, + ], + }; + + const multiFieldBlocks: LayoutBlock[] = [ + { + row: 0, col: 0, span: 4, + fieldName: 'Field1', fieldType: 'uint32_t', color: 'blue', + offsetStart: 0, offsetEnd: 3, isPadding: false, + }, + { + row: 0, col: 4, span: 4, + fieldName: 'Field2', fieldType: 'uint32_t', color: 'cyan', + offsetStart: 4, offsetEnd: 7, isPadding: false, + }, + { + row: 0, col: 8, span: 4, + fieldName: 'Field3', fieldType: 'uint32_t', color: 'yellow', + offsetStart: 8, offsetEnd: 11, isPadding: false, + }, + { + row: 0, col: 12, span: 4, + fieldName: 'Field4', fieldType: 'uint32_t', color: 'green', + offsetStart: 12, offsetEnd: 15, isPadding: false, + }, + ]; + + it('should render legend in 1 column by default', () => { + const svg = renderSVG(multiFieldConfig, multiFieldBlocks); + + expect(svg).toContain('Field1'); + expect(svg).toContain('Field2'); + expect(svg).toContain('Field3'); + expect(svg).toContain('Field4'); + }); + + it('should render legend in 2 columns when legendColumns is 2', () => { + const svg = renderSVG(multiFieldConfig, multiFieldBlocks, { + legendColumns: 2, + }); + + expect(svg).toContain('Field1'); + expect(svg).toContain('Field2'); + expect(svg).toContain('Field3'); + expect(svg).toContain('Field4'); + }); + + it('should use config.legendColumns when options.legendColumns is not provided', () => { + const configWithColumns: ByteGridConfig = { + ...multiFieldConfig, + legendColumns: 2, + }; + + const svg = renderSVG(configWithColumns, multiFieldBlocks); + + expect(svg).toContain('Field1'); + expect(svg).toContain('Field2'); + }); + + it('should prioritize options.legendColumns over config.legendColumns', () => { + const configWithColumns: ByteGridConfig = { + ...multiFieldConfig, + legendColumns: 1, + }; + + const svg = renderSVG(configWithColumns, multiFieldBlocks, { + legendColumns: 2, + }); + + expect(svg).toContain('Field1'); + }); + + it('should handle legendColumns with minimum value of 1', () => { + const svg = renderSVG(multiFieldConfig, multiFieldBlocks, { + legendColumns: 0, // Should be treated as 1 + }); + + expect(svg).toContain('Field1'); + }); + + it('should render legend in 3 columns when legendColumns is 3', () => { + // 6 fields for testing 3 columns + const sixFieldConfig: ByteGridConfig = { + name: 'Six Field Test', + size: 24, + layout: 16, + fields: [ + { offset: '0-3', name: 'Field1', type: 'uint32_t', color: 'blue' }, + { offset: '4-7', name: 'Field2', type: 'uint32_t', color: 'cyan' }, + { offset: '8-11', name: 'Field3', type: 'uint32_t', color: 'yellow' }, + { offset: '12-15', name: 'Field4', type: 'uint32_t', color: 'green' }, + { offset: '16-19', name: 'Field5', type: 'uint32_t', color: 'orange' }, + { offset: '20-23', name: 'Field6', type: 'uint32_t', color: 'purple' }, + ], + }; + + const sixFieldBlocks: LayoutBlock[] = [ + { row: 0, col: 0, span: 4, fieldName: 'Field1', fieldType: 'uint32_t', color: 'blue', offsetStart: 0, offsetEnd: 3, isPadding: false }, + { row: 0, col: 4, span: 4, fieldName: 'Field2', fieldType: 'uint32_t', color: 'cyan', offsetStart: 4, offsetEnd: 7, isPadding: false }, + { row: 0, col: 8, span: 4, fieldName: 'Field3', fieldType: 'uint32_t', color: 'yellow', offsetStart: 8, offsetEnd: 11, isPadding: false }, + { row: 0, col: 12, span: 4, fieldName: 'Field4', fieldType: 'uint32_t', color: 'green', offsetStart: 12, offsetEnd: 15, isPadding: false }, + { row: 1, col: 0, span: 4, fieldName: 'Field5', fieldType: 'uint32_t', color: 'orange', offsetStart: 16, offsetEnd: 19, isPadding: false }, + { row: 1, col: 4, span: 4, fieldName: 'Field6', fieldType: 'uint32_t', color: 'purple', offsetStart: 20, offsetEnd: 23, isPadding: false }, + ]; + + const svg = renderSVG(sixFieldConfig, sixFieldBlocks, { + legendColumns: 3, + }); + + // All 6 fields should be present + expect(svg).toContain('Field1'); + expect(svg).toContain('Field2'); + expect(svg).toContain('Field3'); + expect(svg).toContain('Field4'); + expect(svg).toContain('Field5'); + expect(svg).toContain('Field6'); + + // Check for proper width (should be wider for 3 columns) + // Each column is 200px, so 3 columns = 600px + expect(svg).toMatch(/width="\d+"/); + }); + }); }); }); diff --git a/roadmap.md b/roadmap.md new file mode 100644 index 0000000..06f4ecb --- /dev/null +++ b/roadmap.md @@ -0,0 +1,288 @@ +# ByteGrid Development Roadmap + +**Last Updated:** 2025-10-14 +**Current Phase:** Phase 2 (Core Features) + +--- + +## Phase 1: MVP ✅ COMPLETED + +**Goal:** Basic structure visualization with manual field definition + +### Completed Features +- [x] Project structure setup (monorepo with core + plugin) +- [x] Basic type definitions (ByteGridConfig, Field, LayoutBlock) +- [x] YAML parser with js-yaml +- [x] Field validation (overlap detection, range checking) +- [x] Simple layout engine (single row) +- [x] Basic SVG renderer +- [x] Obsidian plugin integration +- [x] Test infrastructure (Jest) + +**Status:** ✅ All core functionality implemented + +--- + +## Phase 2: Core Features 🚧 IN PROGRESS + +**Goal:** Robust visualization with advanced layout options + +### Completed Features ✅ +- [x] Multi-row layout support +- [x] Grid lines rendering +- [x] Color system (9 predefined colors) +- [x] Bitfield visualization (sub-byte fields) +- [x] Bit-unit layout mode (`layoutUnit: bit`) +- [x] Suffix notation (`32b`, `4B`) +- [x] Legend position control (`legendPosition: right/left/bottom/none`) +- [x] Footer visibility control (`showFooter: true/false`) +- [x] Comprehensive test coverage (114 tests) + +### In Progress 🔄 +- [ ] Color schemes (dark, light modes) +- [ ] Row/column labels customization +- [ ] Cell padding/spacing options + +### Planned Features 📋 +- [ ] Hex dump display (`showHexDump: true`) +- [ ] Field description tooltips +- [ ] Value display formatting (hex, decimal, binary) +- [ ] Endianness indicators + +**Target Completion:** Q4 2025 + +--- + +## Phase 3: Interactive Features 📅 PLANNED + +**Goal:** Rich user interaction and real-time visualization + +### Features +- [ ] Interactive tooltips on hover + - Field name, type, offset + - Description and value + - Bit range for bitfields +- [ ] Field highlighting + - Click to highlight field + - Show all related blocks + - Copy field info to clipboard +- [ ] Zoom controls + - Zoom in/out + - Pan/scroll for large structures +- [ ] Search/filter fields + - Search by name or type + - Filter by color or category +- [ ] Export options + - Export as SVG file + - Export as PNG image + - Export as C struct definition + - Copy as Markdown table + +**Target Start:** Q1 2026 + +--- + +## Phase 4: Advanced Features 📅 FUTURE + +**Goal:** Binary file integration and advanced analysis + +### Binary File Support +- [ ] Binary file parser (`binaryParser.ts`) + - Read binary files + - Auto-populate field values + - Endianness handling + - Generate hex dump +- [ ] File format templates + - WAV_HEADER (audio files) + - ELF_HEADER (executables) + - TCP_HEADER (network packets) + - PNG_HEADER (image files) + - Custom template system + +### Structure Analysis +- [ ] Structure comparison (`comparison.ts`) + - Side-by-side view + - Overlay view + - Diff highlighting +- [ ] Padding analysis + - Auto-detect padding bytes + - Show memory waste + - Suggest optimizations +- [ ] Alignment visualization + - Show alignment boundaries + - Detect misaligned fields + - Platform-specific rules + +**Target Start:** Q2 2026 + +--- + +## Phase 5: Performance & Scale 📅 FUTURE + +**Goal:** Handle large structures and optimize rendering + +### Performance Optimization +- [ ] Rendering cache (`cache.ts`) + - Hash-based cache keys + - TTL-based expiration + - localStorage persistence +- [ ] Virtual scrolling + - Lazy render large structures + - Only render visible rows + - Smooth scrolling +- [ ] Pagination + - Break large structures (>1000 bytes) + - Page navigation controls + - Jump to offset + +### Large Structure Support +- [ ] Performance benchmarks + - 1KB: < 100ms + - 10KB: < 500ms + - 100KB: < 2s (with pagination) +- [ ] Memory optimization + - Efficient data structures + - DOM node recycling + - Progressive rendering + +**Target Start:** Q3 2026 + +--- + +## Phase 6: Extensibility 📅 FUTURE + +**Goal:** Plugin system and customization + +### Custom Extensions +- [ ] Type system extensions + - Custom type definitions + - Type aliases + - Nested structures + - Union types +- [ ] Custom color schemes + - User-defined palettes + - Theme import/export + - Dark/light mode auto-switch +- [ ] Render hooks + - Pre-render processing + - Post-render modifications + - Custom SVG elements +- [ ] Plugin API + - Register custom parsers + - Add custom validators + - Extend layout engine + +### Developer Tools +- [ ] Debug mode + - Show internal state + - Performance metrics + - Error details +- [ ] Documentation generator + - Auto-generate type docs + - Example templates + - Interactive playground + +**Target Start:** Q4 2026 + +--- + +## Current Sprint (October 2024) + +### Completed This Sprint ✅ +- [x] Bit-unit layout implementation +- [x] Legend position options (right, left, bottom, none) +- [x] Footer visibility control +- [x] Fix legend overlapping issues +- [x] Add comprehensive tests for new features +- [x] Documentation updates + +### Next Sprint Goals (November 2024) +1. **Color Schemes** (Priority: High) + - Implement dark and light color schemes + - Add `colorScheme` option to config + - Update color constants + - Add tests + +2. **Hex Dump Display** (Priority: Medium) + - Add `showHexDump` option + - Render hex values below grid + - Format with proper spacing + - Handle large structures + +3. **Documentation** (Priority: High) + - Update CLAUDE.md with new options + - Create user guide in docs/ + - Add more examples + - API reference cleanup + +--- + +## Known Limitations & Future Improvements + +### Current Limitations +1. **YAML Line Numbers** + - Error messages show field index, not line number + - **Improvement:** Custom YAML parser with position tracking + +2. **Cache Persistence** + - Cache cleared on session end + - **Improvement:** localStorage persistence (Phase 5) + +3. **Type System** + - No custom type definitions + - No nested structures + - **Improvement:** Type alias system (Phase 6) + +4. **Bit Ordering** + - Assumes MSB-first + - **Improvement:** Add `bitOrder: msb/lsb` option + +5. **Layout Constraints** + - Fixed cell sizes per visualization + - **Improvement:** Responsive cell sizing + +--- + +## Contributing + +See [CONTRIBUTING.md](../CONTRIBUTING.md) for guidelines on: +- Proposing new features +- Submitting pull requests +- Testing requirements +- Code style guidelines + +--- + +## Versioning Strategy + +Following **Semantic Versioning** (MAJOR.MINOR.PATCH): + +- **MAJOR:** Breaking changes to public API +- **MINOR:** New features, backward compatible +- **PATCH:** Bug fixes, performance improvements + +### Planned Releases +- **v0.2.0** - Phase 2 completion (Color schemes, hex dump) +- **v0.3.0** - Phase 3 start (Interactive features) +- **v1.0.0** - Production ready (All core features stable) +- **v2.0.0** - Phase 4-5 (Binary files, performance) + +--- + +## Feedback & Priorities + +Feature priorities may change based on: +- User feedback and requests +- Community contributions +- Technical dependencies +- Performance considerations + +To suggest features or vote on priorities: +- Open an issue on GitHub +- Join discussions in the community +- Contribute code or documentation + +--- + +**Document Version:** 1.0 +**Next Review:** December 2024