Skip to content

fix: improved brush shape for rotated images#2743

Open
Devu-trenser wants to merge 3 commits into
cornerstonejs:mainfrom
arul-trenser:fix-improved-brush-shape-for-rotated-images
Open

fix: improved brush shape for rotated images#2743
Devu-trenser wants to merge 3 commits into
cornerstonejs:mainfrom
arul-trenser:fix-improved-brush-shape-for-rotated-images

Conversation

@Devu-trenser

@Devu-trenser Devu-trenser commented May 25, 2026

Copy link
Copy Markdown
Contributor

Context

Fixes incorrect shape rendering when using brush tools with circular/spherical/rectangle shapes in rotated viewports.

Fixes : #2651

⚠️⚠️ This PR targets the arul-trenser:feat-axis-based-image-stretching branch from #2443.

Changes & Results

Changes

  • Fixed incorrect shape rendering in rotated/oblique viewports.
  • Updated fill logic to use viewport-aligned axes.
  • Improved voxel inclusion checks for better geometric consistency.
  • Fixed rectangle point ordering instability.
  • Reduced slice leakage and unintended fills across neighboring slices.

Results
Before

  • Circular/spherical/rectangluar brushes could appear as distorted shapes in rotated views.
  • Rectangle rendering was unstable depending on viewport orientation.

After

  • Circular and spherical brushes maintain almost similar to the expected geometry in rotated/oblique views.
  • Improved consistency and stability across all supported brush shapes.

Testing

  1. Run the Axis-based Image Stretching example.
  2. Rotate the viewport to different oblique angles (e.g. 40°–45°).
  3. Draw annotations using:
    • Circular Brush
    • Sphere Brush
    • Rectangle Brush
  4. Verify:
    • Shapes render correctly in rotated views.
    • Rectangle fills remain stable and properly aligned.

Checklist

PR

  • My Pull Request title is descriptive, accurate and follows the
    semantic-release format and guidelines.

Code

  • My code has been well-documented (function documentation, inline comments,
    etc.)

Public Documentation Updates

  • [] The documentation page has been updated as necessary for any public API
    additions or removals.

Tested Environment

  • [] "OS: Ubuntu 24.04
  • [] "Node version: 20
  • [] "Browser: Chrome 139

Summary by CodeRabbit

  • Refactor
    • Improved circle, rectangle, and sphere segmentation geometry for more accurate point-in-shape inclusion in rotated/oblique views, including voxel-center handling and more robust plane/spacing tolerances.
  • Tests
    • Added coverage for deterministic rectangle corner ordering, validating non–self-intersecting geometry and correct results across different input orders and view-plane orientations.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Claude Code Review

This pull request is from a fork — automated review is disabled. A repository maintainer can comment @claude review to run a one-time review.

@Devu-trenser Devu-trenser force-pushed the fix-improved-brush-shape-for-rotated-images branch from 2c133c5 to c9fd4f5 Compare May 26, 2026 05:14
@Devu-trenser Devu-trenser changed the title Fix improved brush shape for rotated images fix: improved brush shape for rotated images Jun 15, 2026
@coderabbitai

coderabbitai Bot commented Jun 17, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 6687115f-ac39-4ae2-9c68-350b3986d8b5

📥 Commits

Reviewing files that changed from the base of the PR and between 756ba0a and e93f3d0.

📒 Files selected for processing (3)
  • packages/tools/src/tools/segmentation/strategies/__tests__/fillRectangle.spec.ts
  • packages/tools/src/tools/segmentation/strategies/fillCircle.ts
  • packages/tools/src/tools/segmentation/strategies/fillRectangle.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/tools/src/tools/segmentation/strategies/fillCircle.ts

📝 Walkthrough

Walkthrough

Three segmentation fill strategies (fillCircle, fillRectangle, fillSphere) are updated to correctly handle oblique/rotated views. Changes introduce view basis vectors (viewRight, viewUp, viewNormal) into point-in-shape tests, compute plane-slab thickness from image spacing, and shift IJK-to-world conversions to use voxel centers. A new utility orderRectangleCorners sorts rectangle corners deterministically by polar angle for stable basis reconstruction.

Changes

Oblique-plane fill strategy corrections

Layer / File(s) Summary
Circle/ellipse view basis contract and corner refactor
strategies/fillCircle.ts
Introduces VOXEL_CENTER_OFFSET constant and refactors createCircleCornersForCenter to explicitly compute and return four world-space corner points (top-right, top-left, bottom-right, bottom-left) using view basis vectors and radii.
createPointInEllipse oblique-plane evaluation
strategies/fillCircle.ts
Extends createPointInEllipse to accept viewRight, viewUp, viewNormal in options, adds an IJK→voxel-center helper, derives planeTolerance from spacing projected along viewNormal, precomputes inverse squared radii, updates sphere-case IJK→world to use voxel centers, and replaces the membership test with viewRight/viewUp dot-product projection plus a viewNormal depth slab check.
Circle/ellipse initialization with view vectors
strategies/fillCircle.ts
Updates initializeCircle to normalize view vectors from operationData and pass viewRight, viewUp, viewNormal into createPointInEllipse options.
Rectangle corner ordering utility
strategies/fillRectangle.ts
Introduces new exported orderRectangleCorners function that deterministically sorts rectangle corners by polar angle around the centroid using in-plane basis vectors. Simplifies imports by removing viewport type dependencies.
Rectangle oblique-plane evaluation
strategies/fillRectangle.ts
Rewrites createPointInRectangle to order corners via orderRectangleCorners, compute boundsIJK directly from clamped IJK corner extrema, reconstruct rectangle in-plane axes and plane normal, derive thickness from spacing projected along plane normal, and replace the EPS-based normal-distance check with an explicit distanceToPlane slab check in pointInShapeFn.
Sphere view vectors and direct IJK bounds
strategies/fillSphere.ts
Destructures viewUp and viewPlaneNormal from operationData, removes the getSphereBoundsInfoFromViewport call, normalizes view vectors to form viewRight/viewUp/viewNormal, derives radiusIJK from volume spacing, computes boundsIJK directly from centerIJK ± radiusIJK with clamping, and passes the normalized view basis into createEllipseInPoint.
Rectangle corner ordering tests
strategies/__tests__/fillRectangle.spec.ts
Adds comprehensive Jest test suite for orderRectangleCorners with geometry validation helpers (squared-distance, proper-winding, rectangle-signature) covering axial constant-Z, constant-Y regression, oblique order-independence, and winding-reversal consistency.

Sequence Diagrams

sequenceDiagram
  participant initializeCircle as initializeCircle
  participant createPointInEllipse as createPointInEllipse
  participant voxelCenterConv as toVoxelCenterIJK
  participant membershipTest as ellipse/sphere membership

  initializeCircle->>createPointInEllipse: cornersInWorld, {viewRight, viewUp, viewNormal, radii}
  createPointInEllipse->>createPointInEllipse: compute planeTolerance from spacing·viewNormal
  createPointInEllipse->>createPointInEllipse: precompute invXRSq, invYRSq from radii
  createPointInEllipse->>membershipTest: pointIJK, center, projection axes, precomputed radii
  membershipTest->>voxelCenterConv: pointIJK + 0.5
  voxelCenterConv->>membershipTest: pointWorld at voxel center
  membershipTest->>membershipTest: u = dot(centerToPoint, viewRight), v = dot(centerToPoint, viewUp)
  membershipTest->>membershipTest: ellipseVal = u²·invXRSq + v²·invYRSq
  membershipTest->>membershipTest: depth = abs(dot(centerToPoint, viewNormal))
  membershipTest-->>initializeCircle: ellipseVal ≤ 1 AND depth ≤ planeTolerance
Loading
sequenceDiagram
  participant initializeRectangle as initializeRectangle
  participant orderRectangleCorners as orderRectangleCorners
  participant createPointInRectangle as createPointInRectangle
  participant pointInShapeFn as pointInShapeFn

  initializeRectangle->>orderRectangleCorners: unordered corners
  orderRectangleCorners-->>initializeRectangle: corners sorted by polar angle
  initializeRectangle->>createPointInRectangle: ordered corners, segmentationImageData
  createPointInRectangle->>createPointInRectangle: transform corners to IJK, compute boundsIJK
  createPointInRectangle->>createPointInRectangle: compute rectangle basis (u-axis, v-axis, normal)
  createPointInRectangle->>createPointInRectangle: thickness from spacing·normal
  createPointInRectangle->>pointInShapeFn: point candidate, rectangle geometry
  pointInShapeFn->>pointInShapeFn: check u-range, v-range, distanceToPlane ≤ thickness
  pointInShapeFn-->>initializeRectangle: true/false
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 Hop along the oblique plane,
No more voxels missed in vain!
ViewRight, viewUp, viewNormal true,
The slab is thick — the fill shines through.
With dot products squared and corners sorted tight,
This bunny's geometry is perfectly right! 🎯

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'fix: improved brush shape for rotated images' clearly describes the main issue being addressed and follows semantic-release format conventions.
Description check ✅ Passed The description includes all key required sections: Context with issue reference, detailed Changes & Results with before/after comparison, comprehensive Testing instructions, and completed checklist items.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (3)
packages/tools/src/tools/segmentation/strategies/fillCircle.ts (1)

71-76: 💤 Low value

Corner ordering inconsistency with destructuring at line 314.

The return order is [topRight, topLeft, bottomRight, bottomLeft] but the destructuring at line 314 expects [topLeft, bottomRight, bottomLeft, topRight]. This mismatch will cause incorrect axis calculations when the corners are consumed.

However, looking at the usage at line 265, strokeCornersWorld is only used to compute boundsIJK via getBoundingBoxAroundShapeIJK, which is order-agnostic. The cornersInWorld used in createPointInEllipse comes from the canvas-derived corners at lines 223-226, not from createCircleCornersForCenter. So this ordering doesn't affect the actual shape test.

Consider aligning the return order for consistency
   return [
-    topRight as Types.Point3,
-    topLeft as Types.Point3,
-    bottomRight as Types.Point3,
-    bottomLeft as Types.Point3,
+    topLeft as Types.Point3,
+    bottomRight as Types.Point3,
+    bottomLeft as Types.Point3,
+    topRight as Types.Point3,
   ];
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/tools/src/tools/segmentation/strategies/fillCircle.ts` around lines
71 - 76, The return statement in the function (around lines 71-76) returns
corners in the order [topRight, topLeft, bottomRight, bottomLeft], but the
destructuring at line 314 expects them in the order [topLeft, bottomRight,
bottomLeft, topRight]. Reorder the return statement to match the expected
destructuring order at line 314. Change the return array to put topLeft first,
then bottomRight, then bottomLeft, then topRight to maintain consistency with
how the corners are consumed elsewhere in the code.
packages/tools/src/tools/segmentation/strategies/fillSphere.ts (1)

14-14: ⚡ Quick win

Unused import: getSphereBoundsInfoFromViewport.

The import is still present but the function is no longer used after the bounds computation was changed to use direct IJK calculation.

Remove unused import
-import { getSphereBoundsInfoFromViewport } from '../../../utilities/getSphereBoundsInfo';
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/tools/src/tools/segmentation/strategies/fillSphere.ts` at line 14,
Remove the unused import statement for getSphereBoundsInfoFromViewport from the
imports at the top of the fillSphere.ts file. This function is no longer needed
since the bounds computation has been changed to use direct IJK calculation
instead of relying on the getSphereBoundsInfoFromViewport utility function.
packages/tools/src/tools/segmentation/strategies/fillRectangle.ts (1)

151-160: 💤 Low value

Full projectedSpacing used as thickness may be too permissive.

The comment says "Using full projected spacing gives more stable voxel occupancy," but using the full spacing rather than half-spacing as a tolerance means voxels up to one full voxel away from the plane are included. This could include voxels from adjacent slices in thin-slice scenarios.

For consistency with fillCircle.ts which uses spacingInNormal / 2 for planeTolerance, consider whether this intentional difference is desired.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/tools/src/tools/segmentation/strategies/fillRectangle.ts` around
lines 151 - 160, The thickness variable is set to the full projectedSpacing
value, which may be overly permissive and include voxels from adjacent slices in
thin-slice scenarios. For consistency with fillCircle.ts which uses
spacingInNormal divided by 2 for planeTolerance, evaluate whether the thickness
assignment should be changed to use projectedSpacing divided by 2 instead of the
full value. Review the intention and adjust if the full spacing is not
intentionally required for this particular case.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/tools/src/tools/segmentation/strategies/fillCircle.ts`:
- Around line 330-338: The spacingInNormal calculation in the fillCircle
strategy currently assumes viewNormal is always available when calling
getSpacingInNormalDirection, but viewNormal is optional and may be undefined.
Refactor the conditional logic to explicitly check if viewNormal is defined
before attempting to call getSpacingInNormalDirection with the cast to
Types.Point3. If viewNormal is undefined, provide an appropriate fallback that
does not assume Z-axis spacing is suitable for all orientations. Additionally,
reconsider whether the current condition (spacing && direction) is sufficient,
as getSpacingInNormalDirection might handle cases where only one of these
parameters is defined.
- Around line 427-441: The ellipse equation test does not check the
strokePredicate when a point falls outside the static ellipse boundary (when
xTerm + yTerm > 1), unlike the sphere case which checks strokePredicate before
its sphere test. To fix this, after the ellipse equation check at line 434 (the
condition checking if xTerm + yTerm <= 1), add a check for the strokePredicate
similar to how it is handled in the sphere case. This will ensure that
stroke-based fills work correctly for ellipses during brush strokes by
evaluating the strokePredicate for points outside the static ellipse.

In `@packages/tools/src/tools/segmentation/strategies/fillRectangle.ts`:
- Around line 120-135: The axis construction in the fillRectangle function
assumes that after angular sorting, p0, p1, and p3 form adjacent edges from a
corner, but there is no validation that this assumption holds. If the sorting
produces a different order like diagonal pairs, then axisU and axisV would not
be orthogonal and the normal calculation would be incorrect. After obtaining
orderedPoints, validate adjacency by comparing distances from p0 to each other
point—the two shortest distances should correspond to p1 and p3 (the adjacent
edges), while the longest should be to p2 (the diagonal). If this condition is
not met, reorder the points so that p1 and p3 are correctly identified as the
adjacent corner points before constructing the axes and normal.

In `@packages/tools/src/tools/segmentation/strategies/fillSphere.ts`:
- Around line 97-112: The boundsIJK variable is computed without clamping to
valid image dimensions, unlike the fillRectangle.ts strategy. After calculating
the boundsIJK array using centerIJK and radiusIJK values, clamp each dimension's
min and max bounds to the range [0, dims[i]-1] for each of the three dimensions
I, J, and K, where dims are the image dimensions. This should be done before
assigning the clamped bounds to operationData.isInObjectBoundsIJK to prevent
out-of-bounds array access errors.

---

Nitpick comments:
In `@packages/tools/src/tools/segmentation/strategies/fillCircle.ts`:
- Around line 71-76: The return statement in the function (around lines 71-76)
returns corners in the order [topRight, topLeft, bottomRight, bottomLeft], but
the destructuring at line 314 expects them in the order [topLeft, bottomRight,
bottomLeft, topRight]. Reorder the return statement to match the expected
destructuring order at line 314. Change the return array to put topLeft first,
then bottomRight, then bottomLeft, then topRight to maintain consistency with
how the corners are consumed elsewhere in the code.

In `@packages/tools/src/tools/segmentation/strategies/fillRectangle.ts`:
- Around line 151-160: The thickness variable is set to the full
projectedSpacing value, which may be overly permissive and include voxels from
adjacent slices in thin-slice scenarios. For consistency with fillCircle.ts
which uses spacingInNormal divided by 2 for planeTolerance, evaluate whether the
thickness assignment should be changed to use projectedSpacing divided by 2
instead of the full value. Review the intention and adjust if the full spacing
is not intentionally required for this particular case.

In `@packages/tools/src/tools/segmentation/strategies/fillSphere.ts`:
- Line 14: Remove the unused import statement for
getSphereBoundsInfoFromViewport from the imports at the top of the fillSphere.ts
file. This function is no longer needed since the bounds computation has been
changed to use direct IJK calculation instead of relying on the
getSphereBoundsInfoFromViewport utility function.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: d759a66a-da29-4877-a2bd-3a73b4936c03

📥 Commits

Reviewing files that changed from the base of the PR and between 2769fcf and 756ba0a.

📒 Files selected for processing (3)
  • packages/tools/src/tools/segmentation/strategies/fillCircle.ts
  • packages/tools/src/tools/segmentation/strategies/fillRectangle.ts
  • packages/tools/src/tools/segmentation/strategies/fillSphere.ts

Comment on lines +330 to +338
const spacingInNormal =
spacing && direction
? csUtils.getSpacingInNormalDirection(
{ spacing, direction },
viewNormal as Types.Point3
)
: Math.max(spacing?.[2] ?? 1, Number.EPSILON);

const planeTolerance = Math.max(spacingInNormal / 2, Number.EPSILON);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Potential fallback to spacing[2] when viewNormal is undefined.

When viewNormal is undefined (since it's optional), getSpacingInNormalDirection may not be called correctly. The fallback Math.max(spacing?.[2] ?? 1, Number.EPSILON) assumes Z-axis spacing is appropriate, which may not hold for oblique orientations.

Additionally, if spacing is defined but direction is undefined, the fallback path is taken even though getSpacingInNormalDirection could potentially handle this.

Suggested fix to handle undefined viewNormal explicitly
   const spacingInNormal =
-    spacing && direction
+    spacing && direction && viewNormal
       ? csUtils.getSpacingInNormalDirection(
           { spacing, direction },
           viewNormal as Types.Point3
         )
-      : Math.max(spacing?.[2] ?? 1, Number.EPSILON);
+      : spacing
+        ? Math.max(...spacing, Number.EPSILON)
+        : 1;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const spacingInNormal =
spacing && direction
? csUtils.getSpacingInNormalDirection(
{ spacing, direction },
viewNormal as Types.Point3
)
: Math.max(spacing?.[2] ?? 1, Number.EPSILON);
const planeTolerance = Math.max(spacingInNormal / 2, Number.EPSILON);
const spacingInNormal =
spacing && direction && viewNormal
? csUtils.getSpacingInNormalDirection(
{ spacing, direction },
viewNormal as Types.Point3
)
: spacing
? Math.max(...spacing, Number.EPSILON)
: 1;
const planeTolerance = Math.max(spacingInNormal / 2, Number.EPSILON);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/tools/src/tools/segmentation/strategies/fillCircle.ts` around lines
330 - 338, The spacingInNormal calculation in the fillCircle strategy currently
assumes viewNormal is always available when calling getSpacingInNormalDirection,
but viewNormal is optional and may be undefined. Refactor the conditional logic
to explicitly check if viewNormal is defined before attempting to call
getSpacingInNormalDirection with the cast to Types.Point3. If viewNormal is
undefined, provide an appropriate fallback that does not assume Z-axis spacing
is suitable for all orientations. Additionally, reconsider whether the current
condition (spacing && direction) is sufficient, as getSpacingInNormalDirection
might handle cases where only one of these parameters is defined.

Comment on lines +427 to +441
const xDist = vec3.dot(pointVec, viewRight);
const yDist = vec3.dot(pointVec, viewUp);

// Use the Ellipse Equation with the specific xRadius and yRadius
// Ellipse equation: (x/xRadius)^2 + (y/yRadius)^2 <= 1
return (x * x) / (xRadius * xRadius) + (y * y) / (yRadius * yRadius) <= 1;
const xTerm = xDist * xDist * invXRadiusSquared;
const yTerm = yDist * yDist * invYRadiusSquared;

if (xTerm + yTerm <= 1) {
// Depth Check (Z-axis)
// Ensure the voxel is physically close to the plane we are looking at
const zDist = Math.abs(vec3.dot(pointVec, viewNormal));

return zDist <= planeTolerance;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Ellipse test returns false without checking strokePredicate for points outside the static ellipse.

The sphere case (line 398-400) checks strokePredicate before the sphere test. However, in the ellipse case, if a point fails the ellipse equation (xTerm + yTerm > 1), the function returns false at line 443 without checking the strokePredicate. This means stroke-based fills won't work correctly for ellipses during brush strokes.

Add strokePredicate check to ellipse case
   // Otherwise, treat as ellipse in oblique plane
   return (pointLPS: Types.Point3 | null, pointIJK?: Types.Point3) => {
     let worldPoint: Types.Point3 | null = pointLPS;

     if (!worldPoint && pointIJK && options.segmentationImageData) {
       worldPoint = transformIndexToWorld(
         options.segmentationImageData,
         toVoxelCenterIJK(pointIJK as Types.Point3)
       ) as Types.Point3;
     }

     if (!worldPoint) {
       return false;
     }

+    if (strokePredicate?.(worldPoint)) {
+      return true;
+    }
+
     // Get the vector from the center of the brush to the current voxel
     const pointVec = vec3.create();
     vec3.subtract(pointVec, worldPoint, center);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const xDist = vec3.dot(pointVec, viewRight);
const yDist = vec3.dot(pointVec, viewUp);
// Use the Ellipse Equation with the specific xRadius and yRadius
// Ellipse equation: (x/xRadius)^2 + (y/yRadius)^2 <= 1
return (x * x) / (xRadius * xRadius) + (y * y) / (yRadius * yRadius) <= 1;
const xTerm = xDist * xDist * invXRadiusSquared;
const yTerm = yDist * yDist * invYRadiusSquared;
if (xTerm + yTerm <= 1) {
// Depth Check (Z-axis)
// Ensure the voxel is physically close to the plane we are looking at
const zDist = Math.abs(vec3.dot(pointVec, viewNormal));
return zDist <= planeTolerance;
}
const xDist = vec3.dot(pointVec, viewRight);
const yDist = vec3.dot(pointVec, viewUp);
// Use the Ellipse Equation with the specific xRadius and yRadius
// Ellipse equation: (x/xRadius)^2 + (y/yRadius)^2 <= 1
const xTerm = xDist * xDist * invXRadiusSquared;
const yTerm = yDist * yDist * invYRadiusSquared;
if (xTerm + yTerm <= 1) {
// Depth Check (Z-axis)
// Ensure the voxel is physically close to the plane we are looking at
const zDist = Math.abs(vec3.dot(pointVec, viewNormal));
return zDist <= planeTolerance;
}
if (strokePredicate?.(worldPoint)) {
return true;
}
return false;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/tools/src/tools/segmentation/strategies/fillCircle.ts` around lines
427 - 441, The ellipse equation test does not check the strokePredicate when a
point falls outside the static ellipse boundary (when xTerm + yTerm > 1), unlike
the sphere case which checks strokePredicate before its sphere test. To fix
this, after the ellipse equation check at line 434 (the condition checking if
xTerm + yTerm <= 1), add a check for the strokePredicate similar to how it is
handled in the sphere case. This will ensure that stroke-based fills work
correctly for ellipses during brush strokes by evaluating the strokePredicate
for points outside the static ellipse.

Comment thread packages/tools/src/tools/segmentation/strategies/fillRectangle.ts
Comment on lines +97 to +112
const boundsIJK = [
[
Math.floor(centerIJK[0] - radiusIJK[0]),
Math.ceil(centerIJK[0] + radiusIJK[0]),
],
[
Math.floor(centerIJK[1] - radiusIJK[1]),
Math.ceil(centerIJK[1] + radiusIJK[1]),
],
[
Math.floor(centerIJK[2] - radiusIJK[2]),
Math.ceil(centerIJK[2] + radiusIJK[2]),
],
];

operationData.isInObjectBoundsIJK = boundsIJK as Types.BoundsIJK;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Bounds not clamped to image dimensions.

Unlike fillRectangle.ts which clamps bounds to [0, dims[i]-1], the sphere bounds are computed without clamping. This could result in out-of-bounds IJK indices being passed to the voxel iteration, potentially causing array access errors or wasted computation on non-existent voxels.

Add dimension clamping to boundsIJK
+    const dims = segmentationImageData.getDimensions();
+
     // Define bounds that always encompass the sphere
     const boundsIJK = [
       [
-        Math.floor(centerIJK[0] - radiusIJK[0]),
-        Math.ceil(centerIJK[0] + radiusIJK[0]),
+        Math.max(0, Math.floor(centerIJK[0] - radiusIJK[0])),
+        Math.min(dims[0] - 1, Math.ceil(centerIJK[0] + radiusIJK[0])),
       ],
       [
-        Math.floor(centerIJK[1] - radiusIJK[1]),
-        Math.ceil(centerIJK[1] + radiusIJK[1]),
+        Math.max(0, Math.floor(centerIJK[1] - radiusIJK[1])),
+        Math.min(dims[1] - 1, Math.ceil(centerIJK[1] + radiusIJK[1])),
       ],
       [
-        Math.floor(centerIJK[2] - radiusIJK[2]),
-        Math.ceil(centerIJK[2] + radiusIJK[2]),
+        Math.max(0, Math.floor(centerIJK[2] - radiusIJK[2])),
+        Math.min(dims[2] - 1, Math.ceil(centerIJK[2] + radiusIJK[2])),
       ],
     ];
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/tools/src/tools/segmentation/strategies/fillSphere.ts` around lines
97 - 112, The boundsIJK variable is computed without clamping to valid image
dimensions, unlike the fillRectangle.ts strategy. After calculating the
boundsIJK array using centerIJK and radiusIJK values, clamp each dimension's min
and max bounds to the range [0, dims[i]-1] for each of the three dimensions I,
J, and K, where dims are the image dimensions. This should be done before
assigning the clamped bounds to operationData.isInObjectBoundsIJK to prevent
out-of-bounds array access errors.

@wayfarer3130 wayfarer3130 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I'm needing to make more exhaustive changes to correct a number of edge cases/flaws in how this is currently working. Will see if I can get this fixed this week still.

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.

[Bug] Brush is not working properly for rotated viewports

2 participants