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]>
@claude
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?

@claude
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]>
@jonobr1 jonobr1 requested a review from Copilot September 30, 2025 22:17
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR implements a new strokeAttenuation property for Path and Group classes to maintain constant stroke width in screen space during zoom operations. When set to false, stroke widths automatically compensate for world transform scale.

Key changes:

  • Adds getEffectiveStrokeWidth utility function to calculate scale-compensated stroke widths
  • Implements strokeAttenuation property on Path and Group classes (defaults to true for backward compatibility)
  • Updates all renderers (Canvas, SVG, WebGL) to use the new effective stroke width calculation

Reviewed Changes

Copilot reviewed 6 out of 7 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
src/utils/math.js Adds getEffectiveStrokeWidth utility function for scale compensation
src/path.js Implements strokeAttenuation property with getter/setter and flag management
src/group.js Adds strokeAttenuation property that propagates to child shapes
src/renderers/canvas.js Updates Canvas renderer to use effective stroke width calculation
src/renderers/svg.js Updates SVG renderer to use effective stroke width calculation
src/renderers/webgl.js Updates WebGL renderer to use effective stroke width calculation

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

Comment on lines +1084 to +1088
for (let i = 0; i < this.children.length; i++) {
const child = this.children[i];
if (child.strokeAttenuation !== undefined) {
child.strokeAttenuation = v;
}
Copy link

Copilot AI Sep 30, 2025

Choose a reason for hiding this comment

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

The setter iterates through all children every time the property is set, which could be inefficient for groups with many children. Consider implementing lazy propagation or only updating children that have strokeAttenuation property.

Suggested change
for (let i = 0; i < this.children.length; i++) {
const child = this.children[i];
if (child.strokeAttenuation !== undefined) {
child.strokeAttenuation = v;
}
// Only update children that have a defined strokeAttenuation property
const attenuableChildren = this.children.filter(child => child && child.strokeAttenuation !== undefined);
for (let i = 0; i < attenuableChildren.length; i++) {
attenuableChildren[i].strokeAttenuation = v;

Copilot uses AI. Check for mistakes.
jonobr1 and others added 4 commits September 30, 2025 15:19
Introduces the strokeAttenuation property to Two.Points and Two.Text, allowing stroke width to remain constant in screen space or scale with transformations. Updates internal flags, property lists, and TypeScript definitions to support this feature.
This commit adds comprehensive JSDoc comments to core classes and methods in the Two.js build, improving code documentation and developer experience. It also refactors static property assignments to use modern class field syntax, removes legacy helper functions, and introduces the getEffectiveStrokeWidth utility. Minor updates to constants and test files are included.
@jonobr1 jonobr1 merged commit a603aa8 into dev Sep 30, 2025
1 of 3 checks passed
@jonobr1 jonobr1 deleted the claude/issue-546-20250807-1706 branch September 30, 2025 22:52
if (typeof this.fill === 'object' && this.fill && 'dispose' in this.fill) {
this.fill.dispose();
} else if (typeof this.fill === 'object' && this.fill && 'unbind' in this.fill) {
) {

Check notice

Code scanning / CodeQL

Syntax error Note

Error: Unexpected token

Copilot Autofix

AI 3 months ago

To fix the syntax error, we must remove the stray unmatched parenthesis on line 502 within the else if block. Upon inspection, it appears that line 502 () {) should simply be removed to restore proper function block syntax. The corresponding else if block should take its statement on the next line. No other modifications appear to be needed – it looks like the else if block is already structured properly, only the accidental orphaned parenthesis is present. No imports or additional code is needed; the only required change is to delete line 502, which contains just ) {.

Suggested changeset 1
src/text.js

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/src/text.js b/src/text.js
--- a/src/text.js
+++ b/src/text.js
@@ -499,7 +499,6 @@
     if (typeof this.fill === 'object' && this.fill && 'dispose' in this.fill) {
       this.fill.dispose();
     } else if (typeof this.fill === 'object' && this.fill && 'unbind' in this.fill) {
-    ) {
       this.fill.unbind();
     }
     
EOF
@@ -499,7 +499,6 @@
if (typeof this.fill === 'object' && this.fill && 'dispose' in this.fill) {
this.fill.dispose();
} else if (typeof this.fill === 'object' && this.fill && 'unbind' in this.fill) {
) {
this.fill.unbind();
}

Copilot is powered by AI and may make mistakes. Always verify output.
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.

2 participants