Previously the pill branches behaved like rectangles: the target point
was pinned to the rightmost cap point with a linearly growing y, and the
vertical pill source started from the wrong end of the cap.
- Horizontal pill: target now travels along the right cap surface via
a cap angle (-PI/2 → 0), so both tx/ty stay on the curve
- Horizontal pill: source clamped to the straight top section so it
never crosses into the left cap area
- Vertical pill: source now starts at the corner (angle=0, rightmost
point of top cap) and rotates toward the top as loopOffset grows,
matching the corner-anchoring used by other shapes
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add getSelfLoopParams() with shape-aware geometry for all node shapes
(rectangle, roundedRectangle, circle, ellipse, pill)
- Anchor loops at the top-right corner: innermost loop is tightest
(label closest to node), outer parallel loops fan progressively higher
- Fix ellipse source point which was incorrectly placed at the bottom
- Fix horizontal pill source Y which was at center instead of top edge
- Apply loopOffset to ellipse/circle to differentiate parallel self-loops
- Handle GraphEditor parallel edge sorting for self-referencing edges
- Fix loopLevel overflow for 6+ parallel self-loops via Math.max(0, ...)
- Add tests for self-loop detection, geometry, and parallel self-loops
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Critical Performance Issue:
- Every CustomNode and CustomEdge subscribed to entire store arrays
- 100 nodes × 2 subscriptions + 200 edges × 3 subscriptions = 800 listeners
- ANY change to nodeTypes/labels/edgeTypes triggered ALL components to re-render
- Example: Changing one node type color → 300 components re-render
Solution:
- Add shallow equality checking to all store subscriptions
- Components now only re-render when array CONTENTS change
- Prevents cascade re-renders from reference changes
Files Modified:
- CustomNode.tsx: nodeTypes, labels with shallow
- CustomEdge.tsx: edgeTypes, labels, nodeTypes with shallow
Expected Impact:
- Eliminates unnecessary re-renders during viewport changes
- Should dramatically improve responsiveness during pan/zoom
- Reduces re-render churn when editing types/labels
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Critical Performance Props Added:
1. onlyRenderVisibleElements={true}
- Only renders nodes/edges currently in viewport
- Huge performance win for large graphs (100+ nodes)
- Eliminates rendering of off-screen elements during pan/zoom
2. elevateEdgesOnSelect={true}
- Uses z-index instead of re-rendering for selection
- Prevents unnecessary edge recalculation
3. selectNodesOnDrag={false}
- Disables selection during drag operations
- Reduces re-render churn during viewport changes
Expected Impact:
- 200-node graph previously rendered all 200 nodes on every frame
- Now only renders ~20-30 visible nodes (10× reduction)
- Should dramatically improve pan/zoom smoothness
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Performance Issue:
- MiniMap was calling nodeColor() for all nodes on every pan/zoom frame
- Used O(n) array.find() for each node (100 nodes × 10 types = 1,000 iterations per frame)
- At 30 fps: 30,000 array iterations per second during viewport changes
Solution:
- Pre-build Map of nodeType.id -> color for O(1) lookups
- Memoize the nodeColor callback to prevent recreation
- Reduces iterations from 30,000/sec to 3,000 Map lookups/sec (10× faster)
Impact:
- Eliminates lag during pan/zoom operations on large graphs
- MiniMap rendering now negligible performance cost
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Critical Fixes:
- Replace timestamp-based edge IDs with crypto.randomUUID() to prevent collisions
- Update generateEdgeId to use format: edge_<source>_<target>_<uuid>
- Fix stale test that incorrectly tested for duplicate prevention
- Update test to verify parallel edges ARE allowed (multiple edges between same nodes)
Unit Tests:
- Add comprehensive test suite for edgeUtils.ts (33 tests)
- Test calculateEdgeOffsetMultiplier: 1-6 edges, symmetric distribution
- Test calculatePerpendicularOffset: horizontal, vertical, diagonal, edge cases
- Test groupParallelEdges: bidirectional, multiple groups, special characters
- Test generateEdgeId: uniqueness and format validation
- Use toBeCloseTo for floating-point comparisons to handle IEEE 754 signed zero
All 433 tests passing
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Increase label stagger to 12% of curve (from 10%)
- Reverse stagger direction for opposite-direction edges
- Labels now distribute evenly on both sides of center
- Remove clamping to allow fuller curve utilization
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Simplify label positioning formula: symmetric 10% stagger per offset unit
- Remove parallel edge badge (X relations) - not needed since all labels show
- Labels now closer to center with consistent formula
- Better balance between separation and readability
Example for 3 edges:
- offset -1: label at t=0.4 (toward source)
- offset 0: label at t=0.5 (center)
- offset +1: label at t=0.6 (toward target)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This completes the parallel edge offset feature by fixing several critical issues:
**Fixed Issues:**
1. React Flow's addEdge was preventing duplicate edges in GraphEditor
2. graphStore's addEdge was also using React Flow's addEdge (duplicate prevention)
3. Edge deduplication in visibleEdges was removing parallel edges
4. Normalized key parsing failed due to underscores in node IDs
**Changes:**
- Remove React Flow's addEdge from both GraphEditor and graphStore
- Use unique edge IDs (timestamp-based) to allow multiple same-direction edges
- Use edge.id as map key for normal edges (not source_target)
- Change parallel group key separator from _ to <-> to handle node IDs with underscores
- Add isValidConnection={() => true} to bypass React Flow connection validation
- Reduce endpoint offset from 50% to 10% to keep edges close to nodes
- All edge labels now visible (removed center-edge-only restriction)
**Result:**
- Multiple edges in same direction now work correctly
- Edges fan out beautifully with 80px base offset
- Bidirectional edges properly separated
- Minimized group aggregation still works
- All 517 tests pass
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Add core logic to detect and offset parallel edges (multiple edges between
the same two nodes) to make them visually distinguishable.
Features:
- Detect parallel edges using groupParallelEdges() utility
- Calculate perpendicular offset for each edge in a parallel group
- Distribute edges evenly around the center line (±0.5, ±1, ±1.5, etc.)
- Apply offset to Bezier control points for smooth curved paths
- Base offset of 30px provides clear visual separation
Technical implementation:
- Added calculatePerpendicularOffset() to compute offset vectors
- Added calculateEdgeOffsetMultiplier() for even distribution
- Extended getFloatingEdgeParams() to accept offsetMultiplier parameter
- Added offsetMultiplier and parallelGroupSize to RelationData type
- Updated GraphEditor to detect parallel edges and assign offsets
- Updated CustomEdge to apply offsets when rendering
Design documents included:
- EDGE_OVERLAP_UX_PROPOSAL.md: Complete UX design and implementation plan
- EDGE_OVERLAP_VISUAL_GUIDE.md: Visual specifications and design tokens
All 517 tests pass.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Changes:
- Add ROUNDED_RECTANGLE_RADIUS = 24 to src/constants.ts
- Update RoundedRectangleShape to import and use the constant
- Update edgeUtils.ts to import and use the constant
- Ensures consistency between component rendering and edge calculations
Improves maintainability by having a single source of truth for the
rounded rectangle corner radius value.
Rounded rectangles now have shape-aware edge intersections that follow
the curved corners instead of treating them as sharp corners.
Implementation:
- Add getRoundedRectangleIntersection() function
- Detects when intersection point is near a corner
- Uses circular arc intersection for corners (24px radius)
- Falls back to straight edge calculation for non-corner intersections
- Ensures arrows smoothly follow the rounded contours
Fixes issue where edge arrows didn't correctly follow rounded rectangle
outer contours.
Label selection fix:
- Prevent duplicate labels when creating a label that already exists
- Check if label is already selected before adding to selection
Label wrapping improvements:
- Labels now wrap within a 200px container to prevent nodes growing too large
- LabelBadge updated to only truncate when maxWidth is explicitly provided
- Labels display full text without individual truncation
- Applies to both CustomNode and CustomEdge components
Note: Some overlap may occur with circular shapes - accepted for now.
When attempting to create a label that already exists, the component
would add it to the selected labels without checking if it was already
selected, causing duplicate entries.
Now checks if the label is already selected before adding it.
Fixes issue where creating labels via label select allows duplicate
labels if the label is already assigned.
Since edges use floating calculations that ignore handle positions,
the handle IDs (like 'top-source', 'right-target') should never be
persisted. They're only used to define clickable areas for connections.
This ensures consistency: both migrated old edges and newly created
edges will have no handle fields in saved JSON files.
Addresses PR review comment about serialization inconsistency.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Edge calculation improvements:
- Add zero radius/radii guards in circle and ellipse intersection functions
- Add clamping for pill straight edge intersections to prevent overflow
- Ensure intersection points stay within valid pill boundaries
Handle improvements:
- Add bidirectional connection support with overlapping source/target handles
- Each edge now has both source and target handles (8 total per node)
- Allows edges to connect in any direction from any side
- Fixes handle type restrictions that prevented flexible connections
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Migrated from 4-position handle system (top/right/bottom/left) to React Flow's
easy-connect pattern where the entire node surface is connectable and edges
dynamically route to the nearest point on the node border.
Key changes:
- Migration utility removes old 4-position handle references for backwards compatibility
- Full-coverage invisible handles on CustomNode and GroupNode (maximized state)
- Floating edges use node.measured dimensions and node.internals.positionAbsolute
- useInternalNode hook for correct absolute positioning of nodes in groups
- All edges now omit handle fields, allowing dynamic border calculations
This improves UX by making nodes easier to connect (whole surface vs tiny handles)
and edges intelligently route to optimal connection points.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Use ref for callbacks in useTuioConnection to prevent infinite re-renders when entering presentation mode
- Remove disabled deselect-all shortcut that conflicted with exit-presentation-mode (both using Escape)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Features:
- TUIO connection now starts when tangible config dialog is open
- Connection closes when dialog is closed
- Last detected tangible ID is suggested for Hardware ID field
- "Use: [ID]" link appears next to Hardware ID field when tangible detected
- Clicking the link auto-fills the Hardware ID field
Technical Changes:
- Created useTuioConnection hook for shared TUIO connection management
- Refactored useTuioIntegration to use new useTuioConnection hook
- Added suggestedHardwareId prop to TangibleForm component
- Updated QuickAddTangibleForm to get and pass suggested ID
- Updated EditTangibleInline to get and pass suggested ID
- TangibleConfig modal now uses useTuioConnection when isOpen is true
UI Improvements:
- Hardware ID suggestion link styled like auto-zoom toggle
- Shows truncated ID if longer than 8 characters (e.g., "Use: abc123...")
- Full ID shown in tooltip on hover
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Removed the blue info box with tangible explanations from the
TangibleConfig modal to streamline the interface.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Features:
- Extended tangible filters to support labels, actor types, and relation types
- Added configurable combine mode (OR/AND) for filter logic
- Separated presentation mode filters from editing mode filters
- Implemented backward compatibility with legacy filterLabels format
Filter Behavior:
- OR mode (default for tangibles): Show items matching ANY filter category
- AND mode (default for editing): Show items matching ALL filter categories
- Presentation mode uses tuioStore.presentationFilters
- Editing mode uses searchStore filters
UI Improvements:
- Replaced radio buttons with horizontal button layout for mode selection
- Replaced dropdown with horizontal buttons for combine mode selection
- Consolidated Name and Hardware ID fields into two-column layout
- More compact and consistent interface
Technical Changes:
- Added FilterConfig type with combineMode field
- Created tangibleMigration.ts for backward compatibility
- Created tangibleValidation.ts for multi-format validation
- Added useActiveFilters hook for mode-aware filter access
- Added nodeMatchesFilters and edgeMatchesFilters helper functions
- Updated cascade cleanup for node/edge type deletions
- Removed all TUIO debug logging
Tests:
- Added 44 comprehensive tests for useActiveFilters hook
- Added tests for tangibleMigration utility
- All 499 tests passing
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Changes:
- Replace lastStateChangeSource with activeStateTangibles in tests
- Update tuioStore.test.ts to test array-based tracking
- Add tests for adding/removing multiple state tangibles
- Add test for duplicate prevention
- Update TUIO integration tests to use new API
- Pass fromTangible=true parameter to switchToState in tests
All 447 tests now passing.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Issues fixed:
1. State tangibles not working after manual state switch
2. No support for multiple simultaneous state tangibles
Changes:
- Replace lastStateChangeSource with activeStateTangibles array
- Track active state tangibles in order of placement
- When removing a state tangible, switch to the last remaining one
- Clear activeStateTangibles on manual state switch
- Add fromTangible parameter to switchToState to distinguish sources
- Always switch to newly placed tangible's state (last added wins)
New behavior:
- Place tangible A -> switch to state A
- Manually switch to state B -> clears active tangibles list
- Place tangible A again -> switches back to state A
- Place tangible A and B simultaneously -> shows state B (last wins)
- Remove tangible B -> switches to state A
- Remove tangible A -> stays in current state
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1. Fix state dropdown not updating when new states are added:
- Replace useMemo with proper Zustand selector subscription
- Component now re-renders when timeline states change
2. Add auto-save trigger to state operations:
- createState now triggers auto-save after 1 second
- updateState now triggers auto-save after 1 second
- deleteState now triggers auto-save after 1 second
- Consistent with label and tangible operations
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Fix connection settings to display all detected tangibles, not just configured ones
- Add visual indicators for configured vs unconfigured tangibles
- Add extensive debug logging throughout TUIO stack (WebSocket, client, handlers, integration)
- Add validation for TUIO 1.1 symbolId field to prevent errors
- Add warning message when unconfigured tangibles are detected
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Implements WebSocket-based TUIO protocol support to connect physical tangibles
to presentation mode. When tangibles are placed on/removed from a touch screen,
they trigger configured actions (label filtering or state switching).
Features:
- TUIO 1.1 and 2.0 protocol support with version selection
- WebSocket connection management with real-time status
- Test connection feature in configuration dialog
- Persistent settings (WebSocket URL and protocol version)
- Multi-tangible handling: union for filters, last-wins for states
- Automatic connection in presentation mode
Implementation:
- TuioClientManager: Wrapper for tuio-client library with dual protocol support
- WebsocketTuioReceiver: Custom OSC/WebSocket transport layer
- useTuioIntegration: React hook bridging TUIO events to app stores
- TuioConnectionConfig: Settings UI with real-time tangible detection
- tuioStore: Zustand store with localStorage persistence
Technical details:
- TUIO 1.1 uses symbolId for hardware identification
- TUIO 2.0 uses token.cId for hardware identification
- Filter mode: Activates labels, union of all active tangibles
- State mode: Switches timeline state, last tangible wins
- Cleanup: Removes only labels no longer in use by any tangible
- Unknown hardware IDs are silently ignored
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
The type definition in workspace/types.ts was missing the optional documentId parameter that was added to the implementation.
Co-authored-by: jhbruhn <1036566+jhbruhn@users.noreply.github.com>
- Modified executeTypeTransaction to accept documentId and trigger auto-save
- Added 1-second timeout before saving (consistent with undo/redo)
- Updated all label, tangible, and type management operations to pass documentId
- This ensures dirty indicator clears after save completes
Co-authored-by: jhbruhn <1036566+jhbruhn@users.noreply.github.com>
Add comprehensive presentation/viewer mode optimized for touch table
interactions with clean UI and touch-friendly timeline navigation.
State Management:
- Add presentationMode toggle to settingsStore with localStorage persistence
- Add preferPresentationMode to DocumentMetadata for per-document preferences
- Auto-enter presentation mode when opening documents that prefer it
- Add setDocumentPresentationPreference() helper to workspaceStore
UI Components:
- Create PresentationTimelineOverlay component with floating timeline control
- Previous/Next navigation buttons with chevron icons
- Horizontal scrollable state list
- Only shows when document has 2+ states
- Proper vertical alignment using flex items-stretch and centered content
- Scales to ~10 states with max-w-screen-md (768px) container
- Create presentation.css for touch optimizations (60px+ touch targets)
UI Modifications:
- App.tsx: Conditional rendering hides editing chrome in presentation mode
- GraphEditor: Disable editing interactions, keep pan/zoom enabled
- MenuBar: Add "Presentation Mode" menu item
- Global shortcuts: F11 to toggle, Escape to exit presentation mode
Tests:
- Add presentation mode tests to settingsStore.test.ts
- Add document preference tests to workspaceStore.test.ts
- All 376 tests passing
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Allow zooming out to 10% (previously 50%) to better accommodate large constellation analyses. Added zoom constants (MIN_ZOOM=0.1, MAX_ZOOM=2.5) for consistency across fitView and ReactFlow component.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Fixes TypeScript compilation errors that prevented production build:
- Fix invalid node shape type in integration test (diamond → circle)
- Export WorkspaceSettings type from main types module
- Exclude test files from production build TypeScript check
All tests (368), linting, and build now pass successfully.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>