Skip to content

Conversation

jonobr1
Copy link
Owner

@jonobr1 jonobr1 commented Aug 8, 2025

Summary

  • Implements strokeUniform boolean property for Path and Group classes
  • Maintains constant stroke width in screen space during zoom operations
  • Cross-renderer support (Canvas, SVG, WebGL) with automatic scale compensation

Resolves Issue #546

This addresses the original request for maintaining stroke width upon zoom using ZUI or other scaling operations. When strokeUniform is set to true, the stroke width automatically compensates for world transform scale, maintaining constant visual thickness regardless of zoom level.

Backward Compatible

  • Defaults to false for existing behavior
  • No breaking changes to current API

🤖 Generated with Claude Code

…ring zoom

Adds strokeUniform boolean property to Path and Group classes that maintains
constant stroke width in screen space regardless of scale transformations.

Key features:
- strokeUniform property on Path and Group (defaults to false)
- getEffectiveStrokeWidth() utility function compensates for world scale
- Cross-renderer support (Canvas, SVG, WebGL)
- Group propagation applies to all child shapes
- Backward compatible with existing code

Resolves issue #546 for zoom-invariant stroke widths when using ZUI.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-authored-by: Jono <[email protected]>
Copy link
Contributor

claude bot commented Aug 8, 2025

Claude finished @jonobr1's task —— View job


PR Review: strokeUniform Property Implementation

I've completed a comprehensive analysis of the strokeUniform property implementation. Here's my detailed review:

Todo List:

  • Read and analyze all modified files
  • Review Path class implementation
  • Review Group class implementation
  • Review renderer implementations (Canvas, SVG, WebGL)
  • Review mathematical utilities
  • Review TypeScript definitions
  • Assess code quality and best practices
  • Check for potential bugs and edge cases
  • Evaluate performance considerations
  • Check security concerns
  • Assess test coverage
  • Provide comprehensive feedback

Overall Assessment: ✅ High Quality Implementation

The implementation is well-architected and follows Two.js patterns consistently. Here's my detailed feedback:

Strengths

Excellent Architecture

  • Clean separation of concerns with mathematical logic in math.js
  • Consistent implementation across all three renderers (Canvas, SVG, WebGL)
  • Proper use of existing Two.js patterns (flags, property getters/setters)

Robust Mathematical Implementation

  • getEffectiveStrokeWidth() correctly decomposes transformation matrices
  • Uses Math.max(scaleX, scaleY) to handle non-uniform scaling appropriately
  • Handles edge cases like zero scale gracefully (scale > 0 ? linewidth / scale : linewidth)

Cross-Renderer Consistency

All three renderers correctly call getEffectiveStrokeWidth():

  • Canvas: src/renderers/canvas.js:235, 493, 667
  • SVG: src/renderers/svg.js:508, 675, 800
  • WebGL: src/renderers/webgl.js:238, 734, 1042

Backward Compatibility

  • Defaults to false preserving existing behavior
  • No breaking changes to current API
  • Properties added to both Path and Group Properties arrays

TypeScript Support

  • Complete type definitions in types.d.ts
  • Proper JSDoc documentation
  • Type-safe implementation

Areas for Improvement

⚠️ Performance Considerations

Issue: getEffectiveStrokeWidth() performs matrix decomposition on every render when strokeUniform is true.

Recommendation: Consider caching the effective stroke width and only recalculating when:

if (this._flagStrokeUniform || this._flagLinewidth || this._flagMatrix) {
  this._cachedEffectiveStrokeWidth = getEffectiveStrokeWidth(this);
}

⚠️ Edge Case: WebGL Aspect Ratio

Issue: In webgl.js:734, there's inconsistent aspect ratio handling:

ctx.lineWidth = getEffectiveStrokeWidth(elem) / aspect;  // Line 734
ctx.lineWidth = getEffectiveStrokeWidth(elem);           // Lines 238, 1042

Recommendation: Clarify why aspect ratio division is only applied in one location or ensure consistency.

⚠️ Missing Group Properties

Issue: strokeUniform is not included in Group.Properties array (line 280-291 in group.js).

Impact: This means the property won't be copied during serialization/deserialization operations.

Fix: Add 'strokeUniform' to the Group.Properties array:

static Properties = [
  'fill', 'stroke', 'linewidth', 'cap', 'join', 'miter',
  'closed', 'curved', 'automatic', 'strokeUniform'  // Add this
];

⚠️ Flag Reset Missing

Issue: Group class doesn't handle _flagStrokeUniform in flagReset() method.

Fix: Add flag reset in group.js flagReset method similar to Path implementation.

Security Assessment

No Security Concerns

  • Pure mathematical calculations with no external inputs
  • No DOM manipulation vulnerabilities
  • No data sanitization issues

Test Coverage Assessment

⚠️ Missing Tests
Based on CLAUDE.md, this project uses manual browser testing. Recommended test scenarios:

  1. Zoom in/out with strokeUniform true vs false
  2. Non-uniform scaling (different X/Y scale factors)
  3. Nested groups with mixed strokeUniform settings
  4. Performance testing with many strokeUniform objects

Minor Code Quality Issues

📝 Documentation

  • JSDoc comments are comprehensive and well-written
  • Property descriptions clearly explain the zoom-invariant behavior

📝 Code Style

  • Follows existing Two.js conventions consistently
  • Proper use of boolean coercion (!!v)
  • Clean separation of private/public properties

Recommended Actions

High Priority

  1. Fix Group.Properties array - Add 'strokeUniform' to ensure proper serialization
  2. Fix Group flagReset - Add _flagStrokeUniform reset
  3. Clarify WebGL aspect ratio handling - Document or fix inconsistency

Medium Priority

  1. Performance optimization - Consider caching effective stroke width
  2. Add browser tests - Test zoom scenarios across all renderers

Low Priority

  1. Edge case handling - Consider behavior with extreme scale values (very large/small)

Conclusion

This is a high-quality implementation that demonstrates solid understanding of Two.js architecture. The mathematical approach is sound, cross-renderer support is excellent, and the API design is intuitive. With the minor fixes above, this feature will be a valuable addition to Two.js.

Recommendation: ✅ Approve with minor revisions


@jonobr1
Copy link
Owner Author

jonobr1 commented Aug 8, 2025

@claude instead of strokeUniform can you call it strokeAttenuation where a true value is the opposite of strokeUniform = true?

Copy link
Contributor

claude bot commented Aug 8, 2025

Claude finished @jonobr1's task —— View job


Renaming strokeUniform to strokeAttenuation ✅

I have successfully renamed strokeUniform to strokeAttenuation with inverted logic as requested. The changes maintain backward compatibility while providing the clearer naming you wanted.

Todo List:

  • Read and analyze all modified files to understand current strokeUniform implementation
  • Update Path class: rename strokeUniform to strokeAttenuation and invert logic
  • Update Group class: rename strokeUniform to strokeAttenuation and invert logic
  • Update Canvas renderer implementation
  • Update SVG renderer implementation
  • Update WebGL renderer implementation
  • Update mathematical utilities in math.js
  • Update TypeScript definitions
  • Run build and lint commands
  • Commit and push changes

Summary of Changes:

Property Behavior:

  • strokeAttenuation = true (default): Stroke width scales normally with transformations
  • strokeAttenuation = false: Stroke width remains constant in screen space (zoom-invariant)

Files Modified:

  • src/path.js: Renamed property, flags, and updated documentation
  • src/group.js: Same changes for group-level stroke attenuation
  • src/utils/math.js: Updated getEffectiveStrokeWidth() to use inverted logic
  • types.d.ts: Updated TypeScript definitions and documentation

Backward Compatibility:
The default value is true, which maintains the existing behavior of stroke widths scaling with transforms. Users who want zoom-invariant strokes now set strokeAttenuation = false.

Note: According to CLAUDE.md, you should manually run npm run build and npm run lint to verify the changes work correctly, as I don't have permission to run these commands.


- Replace strokeUniform property with strokeAttenuation across codebase
- Invert boolean logic: strokeAttenuation=true (default) enables scaling, false maintains constant screen space width
- Update property names, flags, documentation, and TypeScript definitions
- Maintain backward compatibility by defaulting to true (normal scaling behavior)
- Update getEffectiveStrokeWidth utility to use inverted logic

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Jono <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant