feat(core): add automatic color assignment for fields

- Add autoColor option (default: true)
    - true: Auto-assign colors to fields without explicit colors
    - false: Use gray for fields without explicit colors
  - Auto color sequence: blue, cyan, yellow, green, orange, purple, mint, pink
  - Padding/reserved fields always use gray
  - Explicit colors always take precedence

  Examples & Documentation:
  - Add 4 example files (basic, manual, mixed, interactive guide)
  - Update examples/README.md with Auto Color section
  - Add Color Options explanation

  Tests:
  - Add 8 new tests (5 layoutEngine, 3 parser)
  - Update existing test for autoColor: false
This commit is contained in:
waaraawa 2025-10-22 22:24:24 +09:00
parent 39f3e84fac
commit 482b458370
10 changed files with 570 additions and 8 deletions

View file

@ -61,6 +61,25 @@ This folder contains example files for the ByteGrid plugin.
- **Bitfield example**: 16 CPU flags
- CF, ZF, SF, IF, DF, OF, IOPL, etc.
### Auto Color Examples
#### 8. auto-color-examples.md
- **Interactive guide** with 4 examples showing automatic color assignment
- Open this file in Obsidian to see live examples
- Explains auto color modes and behavior
#### 9. auto-color-basic.yaml
- Basic auto color example (8 fields, 8 colors)
- Default behavior: colors assigned automatically
#### 10. auto-color-manual.yaml
- Manual color mode (`autoColor: false`)
- Only explicit colors shown, others gray
#### 11. auto-color-mixed.yaml
- Mixed mode: combine auto and manual colors
- Explicit colors take precedence
## Important Notes
⚠️ **Watch Out for Indentation When Copying**
@ -81,32 +100,56 @@ To visualize a new structure:
name: Structure Name
size: total byte size
layout: 16 # bytes per row (optional, default: 16)
layoutUnit: byte # byte or bit (optional, auto-inferred from suffix)
# Color options
autoColor: true # auto-assign colors (optional, default: true)
colorScheme: default # default, dark, or light (optional, default: default)
# Legend options
legendPosition: right # right, left, bottom, none (optional, default: right)
legendColumns: 1 # number of columns in legend (optional, default: 1)
# Other options
showFooter: true # show/hide footer (optional, default: true)
fields:
- offset: 0-3
name: FieldName
type: uint32_t
color: blue # blue, cyan, yellow, green, orange, purple, mint, pink, gray
color: blue # blue, cyan, yellow, green, orange, purple, mint, pink, gray (optional)
description: "Description" # optional
```
### Color Options Explained
**autoColor** (default: `true`):
- `true`: Fields without explicit colors get auto-assigned (blue, cyan, yellow, green, orange, purple, mint, pink)
- `false`: Fields without explicit colors default to gray
**colorScheme** (default: `default`):
- `default`: Original pastel colors
- `dark`: Reduced saturation and lightness (easier on eyes)
- `light`: Increased lightness (better for bright backgrounds)
**Tip**: Start with `autoColor: true` and only specify colors for fields you want to emphasize!
## Bit-Level Layout Examples
### 8. simple-bit-layout.yaml
### 12. simple-bit-layout.yaml
- Basic bit-level layout test (16 bits)
- **Suffix notation**: `size: 16b`, `layout: 16b`, `offset: 0-7b`
- `layoutUnit` can be omitted (auto-inferred from suffix)
### 9. nibble-test.yaml
### 13. nibble-test.yaml
- 4-bit field (nibble) test
- Each nibble represented independently
### 10. mixed-bit-byte.yaml
### 14. mixed-bit-byte.yaml
- Mixed bit and byte offsets
- `0-3b` (bit), `1B` (Byte), `2-3` (byte, default)
### 11. ipv4-bit-layout.yaml
### 15. ipv4-bit-layout.yaml
- IPv4 header in bit-level representation
- Sub-byte fields like Version(4bit) + IHL(4bit)

View file

@ -0,0 +1,38 @@
name: Auto Color Basic Example
size: 32
layout: 16
# autoColor: true is the default
fields:
- offset: 0-3
name: Field1
type: uint32_t
description: "Auto-assigned blue"
- offset: 4-7
name: Field2
type: uint32_t
description: "Auto-assigned cyan"
- offset: 8-11
name: Field3
type: uint32_t
description: "Auto-assigned yellow"
- offset: 12-15
name: Field4
type: uint32_t
description: "Auto-assigned green"
- offset: 16-19
name: Field5
type: uint32_t
description: "Auto-assigned orange"
- offset: 20-23
name: Field6
type: uint32_t
description: "Auto-assigned purple"
- offset: 24-27
name: Field7
type: uint32_t
description: "Auto-assigned mint"
- offset: 28-31
name: Field8
type: uint32_t
description: "Auto-assigned pink"

View file

@ -0,0 +1,215 @@
# Auto Color Examples
ByteGrid supports automatic color assignment for fields. This feature makes it easy to create visualizations without manually specifying colors for each field.
## Example 1: Basic Auto Color (Default)
When `autoColor` is enabled (default), fields without explicit colors are automatically assigned distinct colors in sequence: blue, cyan, yellow, green, orange, purple, mint, pink.
```bytegrid
name: WAV Header (Auto Color)
size: 44
layout: 16
fields:
- offset: 0-3
name: ChunkID
type: char[4]
description: "RIFF magic number"
- offset: 4-7
name: ChunkSize
type: uint32_t
description: "File size - 8"
- offset: 8-11
name: Format
type: char[4]
description: "WAVE format"
- offset: 12-15
name: Subchunk1ID
type: char[4]
description: "fmt subchunk"
- offset: 16-19
name: Subchunk1Size
type: uint32_t
description: "Size of fmt chunk"
- offset: 20-21
name: AudioFormat
type: uint16_t
description: "Audio format (1=PCM)"
- offset: 22-23
name: NumChannels
type: uint16_t
description: "Number of channels"
- offset: 24-27
name: SampleRate
type: uint32_t
description: "Sample rate (Hz)"
- offset: 28-31
name: ByteRate
type: uint32_t
description: "Byte rate"
```
**Result**: Each field gets a different color automatically!
---
## Example 2: Manual Color Mode
When `autoColor: false`, only explicitly specified colors are used. Fields without colors default to gray.
```bytegrid
name: TCP Header (Manual Colors)
size: 20
layout: 16
autoColor: false
fields:
- offset: 0-1
name: SourcePort
type: uint16_t
color: blue
description: "Source port number"
- offset: 2-3
name: DestPort
type: uint16_t
color: cyan
description: "Destination port number"
- offset: 4-7
name: SeqNumber
type: uint32_t
description: "Sequence number (no color = gray)"
- offset: 8-11
name: AckNumber
type: uint32_t
description: "Acknowledgment number (gray)"
- offset: 12-13
name: Flags
type: uint16_t
color: yellow
description: "Flags and offset"
- offset: 14-15
name: WindowSize
type: uint16_t
description: "Window size (gray)"
```
**Result**: Only SourcePort (blue), DestPort (cyan), and Flags (yellow) have colors. Others are gray.
---
## Example 3: Mixed Mode (Auto + Manual)
You can combine automatic colors with explicit colors. Explicit colors always take precedence.
```bytegrid
name: Custom Packet (Mixed Colors)
size: 32
layout: 16
fields:
- offset: 0-3
name: MagicNumber
type: uint32_t
color: pink
description: "Custom magic number (explicit pink)"
- offset: 4-7
name: Version
type: uint32_t
description: "Protocol version (auto: cyan)"
- offset: 8-11
name: Length
type: uint32_t
description: "Packet length (auto: yellow)"
- offset: 12-15
name: Checksum
type: uint32_t
color: mint
description: "CRC32 checksum (explicit mint)"
- offset: 16-19
name: Timestamp
type: uint32_t
description: "Unix timestamp (auto: green)"
- offset: 20-23
name: SourceID
type: uint32_t
description: "Source identifier (auto: orange)"
- offset: 24-27
name: DestID
type: uint32_t
description: "Destination ID (auto: purple)"
- offset: 28-31
name: Payload
type: uint32_t
description: "Payload data (auto: mint again, cycles)"
```
**Result**: MagicNumber is pink (explicit), Checksum is mint (explicit), others are auto-assigned.
---
## Example 4: With Padding/Reserved Fields
Padding and reserved fields are always gray, regardless of `autoColor` setting.
```bytegrid
name: File Header (With Padding)
size: 24
layout: 16
fields:
- offset: 0-3
name: Signature
type: char[4]
description: "File signature (auto: blue)"
- offset: 4-7
name: Version
type: uint32_t
description: "File version (auto: cyan)"
- offset: 8-11
name: Reserved1
type: reserved
description: "Reserved for future use (always gray)"
- offset: 12-15
name: DataOffset
type: uint32_t
description: "Offset to data (auto: green)"
- offset: 16-19
name: DataSize
type: uint32_t
description: "Size of data (auto: orange)"
- offset: 20-23
name: Padding
type: padding
description: "Alignment padding (always gray)"
```
**Result**: Reserved1 and Padding are gray (special types), others are auto-colored.
---
## Color Cycle
When you have more than 8 fields, colors cycle back to the beginning:
1. blue
2. cyan
3. yellow
4. green
5. orange
6. purple
7. mint
8. pink
9. **blue** (cycles back)
10. **cyan**
11. ...
---
## Summary
- **Default behavior**: `autoColor: true` (automatic colors)
- **Manual mode**: `autoColor: false` (only explicit colors, others gray)
- **Mixed mode**: Combine auto and manual colors
- **Special fields**: `reserved` and `padding` are always gray
- **8 colors**: blue, cyan, yellow, green, orange, purple, mint, pink

View file

@ -0,0 +1,33 @@
name: Manual Color Mode Example
size: 24
layout: 16
autoColor: false # Disable auto color assignment
fields:
- offset: 0-3
name: Header
type: uint32_t
color: blue
description: "Explicit blue color"
- offset: 4-7
name: NoColor1
type: uint32_t
description: "No color specified = gray"
- offset: 8-11
name: Important
type: uint32_t
color: yellow
description: "Explicit yellow to highlight"
- offset: 12-15
name: NoColor2
type: uint32_t
description: "No color specified = gray"
- offset: 16-19
name: Footer
type: uint32_t
color: cyan
description: "Explicit cyan color"
- offset: 20-23
name: NoColor3
type: uint32_t
description: "No color specified = gray"

View file

@ -0,0 +1,36 @@
name: Mixed Color Mode Example
size: 28
layout: 16
# autoColor: true (default) - mix auto and manual colors
fields:
- offset: 0-3
name: MagicNumber
type: uint32_t
color: pink
description: "Custom color (explicit pink)"
- offset: 4-7
name: Version
type: uint32_t
description: "Auto-assigned (cyan)"
- offset: 8-11
name: Length
type: uint32_t
description: "Auto-assigned (yellow)"
- offset: 12-15
name: Reserved
type: reserved
description: "Always gray for reserved"
- offset: 16-19
name: Checksum
type: uint32_t
color: mint
description: "Custom color (explicit mint)"
- offset: 20-23
name: Timestamp
type: uint32_t
description: "Auto-assigned (orange)"
- offset: 24-27
name: Data
type: uint32_t
description: "Auto-assigned (purple)"

View file

@ -17,6 +17,12 @@ export function createLayout(config: ByteGridConfig): LayoutBlock[] {
const layoutUnit = config.layoutUnit || 'byte';
const blocks: LayoutBlock[] = [];
// Auto color enabled by default
const autoColor = config.autoColor !== undefined ? config.autoColor : true;
// Colors for auto-assignment (excluding gray for non-padding fields)
const autoColors: ColorName[] = ['blue', 'cyan', 'yellow', 'green', 'orange', 'purple', 'mint', 'pink'];
// Convert offset to bits for unified processing
const toBits = (offset: ReturnType<typeof parseOffset>): { start: number; end: number; size: number } => {
if (layoutUnit === 'bit') {
@ -31,12 +37,25 @@ export function createLayout(config: ByteGridConfig): LayoutBlock[] {
return { start: offset.start, end: offset.end, size: offset.size };
};
for (const field of config.fields) {
for (let fieldIndex = 0; fieldIndex < config.fields.length; fieldIndex++) {
const field = config.fields[fieldIndex];
const offset = parseOffset(field.offset);
const offsetInUnits = toBits(offset);
const color = (field.color || 'gray') as ColorName;
const isPadding = field.type === 'reserved' || field.type === 'padding';
// Determine color
let color: ColorName;
if (field.color) {
// Explicit color specified
color = field.color as ColorName;
} else if (autoColor && !isPadding) {
// Auto-assign color for non-padding fields
color = autoColors[fieldIndex % autoColors.length];
} else {
// Default to gray (manual mode or padding)
color = 'gray';
}
// Check if field spans multiple rows
const startRow = Math.floor(offsetInUnits.start / layout);
const endRow = Math.floor(offsetInUnits.end / layout);

View file

@ -242,6 +242,11 @@ export function parse(source: string): ByteGridConfig {
? (obj.colorScheme as 'default' | 'dark' | 'light')
: undefined;
// Parse autoColor (optional, default: true)
const autoColor = obj.autoColor !== undefined && typeof obj.autoColor === 'boolean'
? obj.autoColor
: undefined;
// Parse legendPosition (optional)
const legendPosition = obj.legendPosition && typeof obj.legendPosition === 'string'
? (obj.legendPosition as 'right' | 'left' | 'bottom' | 'none')
@ -266,6 +271,7 @@ export function parse(source: string): ByteGridConfig {
layout,
layoutUnit: finalLayoutUnit,
colorScheme,
autoColor,
legendPosition,
legendColumns,
showFooter,

View file

@ -91,6 +91,7 @@ export interface ByteGridConfig {
layout?: number; // Units per row (default: 16), unit depends on layoutUnit
layoutUnit?: LayoutUnit; // 'byte' (default) or 'bit'
colorScheme?: ColorScheme;
autoColor?: boolean; // Auto-assign colors to fields (default: true)
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)

View file

@ -109,10 +109,11 @@ describe('LayoutEngine', () => {
expect(blocks[2].offsetEnd).toBe(11);
});
it('should default to gray color for fields without color', () => {
it('should default to gray color for fields without color when autoColor is false', () => {
const config: ByteGridConfig = {
name: 'Test',
size: 8,
autoColor: false,
fields: [
{ offset: '0-3', name: 'Field1', type: 'uint32_t' },
],
@ -384,4 +385,124 @@ describe('LayoutEngine', () => {
expect(blocks[1].span).toBe(4);
});
});
describe('autoColor feature', () => {
it('should auto-assign colors when autoColor is true (default)', () => {
const config: ByteGridConfig = {
name: 'Test',
size: 16,
layout: 16,
fields: [
{ offset: '0-3', name: 'Field1', type: 'uint32_t' }, // No color specified
{ offset: '4-7', name: 'Field2', type: 'uint32_t' }, // No color specified
{ offset: '8-11', name: 'Field3', type: 'uint32_t' }, // No color specified
],
};
const blocks = createLayout(config);
expect(blocks).toHaveLength(3);
// Should auto-assign: blue, cyan, yellow
expect(blocks[0].color).toBe('blue');
expect(blocks[1].color).toBe('cyan');
expect(blocks[2].color).toBe('yellow');
});
it('should use gray when autoColor is false and no color specified', () => {
const config: ByteGridConfig = {
name: 'Test',
size: 16,
layout: 16,
autoColor: false,
fields: [
{ offset: '0-3', name: 'Field1', type: 'uint32_t' }, // No color specified
{ offset: '4-7', name: 'Field2', type: 'uint32_t' }, // No color specified
],
};
const blocks = createLayout(config);
expect(blocks).toHaveLength(2);
// Should default to gray
expect(blocks[0].color).toBe('gray');
expect(blocks[1].color).toBe('gray');
});
it('should respect explicit colors even when autoColor is true', () => {
const config: ByteGridConfig = {
name: 'Test',
size: 16,
layout: 16,
autoColor: true,
fields: [
{ offset: '0-3', name: 'Field1', type: 'uint32_t', color: 'pink' }, // Explicit color
{ offset: '4-7', name: 'Field2', type: 'uint32_t' }, // Auto-assign
{ offset: '8-11', name: 'Field3', type: 'uint32_t', color: 'mint' }, // Explicit color
],
};
const blocks = createLayout(config);
expect(blocks).toHaveLength(3);
expect(blocks[0].color).toBe('pink'); // Explicit
expect(blocks[1].color).toBe('cyan'); // Auto (fieldIndex=1 -> cyan)
expect(blocks[2].color).toBe('mint'); // Explicit
});
it('should use gray for padding/reserved fields regardless of autoColor', () => {
const config: ByteGridConfig = {
name: 'Test',
size: 16,
layout: 16,
autoColor: true,
fields: [
{ offset: '0-3', name: 'Field1', type: 'uint32_t' }, // Auto-assign
{ offset: '4-7', name: 'Reserved', type: 'reserved' }, // Should be gray
{ offset: '8-11', name: 'Field2', type: 'uint32_t' }, // Auto-assign
],
};
const blocks = createLayout(config);
expect(blocks).toHaveLength(3);
expect(blocks[0].color).toBe('blue'); // Auto
expect(blocks[1].color).toBe('gray'); // Reserved
expect(blocks[2].color).toBe('yellow'); // Auto (fieldIndex=2 -> yellow)
});
it('should cycle through colors for many fields', () => {
const config: ByteGridConfig = {
name: 'Test',
size: 36,
layout: 16,
autoColor: true,
fields: [
{ offset: '0-3', name: 'F1', type: 'uint32_t' },
{ offset: '4-7', name: 'F2', type: 'uint32_t' },
{ offset: '8-11', name: 'F3', type: 'uint32_t' },
{ offset: '12-15', name: 'F4', type: 'uint32_t' },
{ offset: '16-19', name: 'F5', type: 'uint32_t' },
{ offset: '20-23', name: 'F6', type: 'uint32_t' },
{ offset: '24-27', name: 'F7', type: 'uint32_t' },
{ offset: '28-31', name: 'F8', type: 'uint32_t' },
{ offset: '32-35', name: 'F9', type: 'uint32_t' }, // Should cycle back to blue
],
};
const blocks = createLayout(config);
expect(blocks).toHaveLength(9);
// First 8 colors
expect(blocks[0].color).toBe('blue');
expect(blocks[1].color).toBe('cyan');
expect(blocks[2].color).toBe('yellow');
expect(blocks[3].color).toBe('green');
expect(blocks[4].color).toBe('orange');
expect(blocks[5].color).toBe('purple');
expect(blocks[6].color).toBe('mint');
expect(blocks[7].color).toBe('pink');
// Cycle back
expect(blocks[8].color).toBe('blue');
});
});
});

View file

@ -740,5 +740,55 @@ fields:
expect(result.colorScheme).toBeUndefined();
});
});
// autoColor tests
describe('autoColor', () => {
it('should parse autoColor: true', () => {
const yaml = `
name: Test
size: 16
autoColor: true
fields:
- offset: 0-3
name: Field1
type: uint32_t
`;
const result = parse(yaml);
expect(result.autoColor).toBe(true);
});
it('should parse autoColor: false', () => {
const yaml = `
name: Test
size: 16
autoColor: false
fields:
- offset: 0-3
name: Field1
type: uint32_t
`;
const result = parse(yaml);
expect(result.autoColor).toBe(false);
});
it('should return undefined when autoColor is not specified', () => {
const yaml = `
name: Test
size: 16
fields:
- offset: 0-3
name: Field1
type: uint32_t
`;
const result = parse(yaml);
expect(result.autoColor).toBeUndefined();
});
});
});
});