Skip to content

QOL: Arrow bending and termination#3717

Closed
RogerZhang888 wants to merge 35 commits into
source-academy:masterfrom
jiahw4ng:rz
Closed

QOL: Arrow bending and termination#3717
RogerZhang888 wants to merge 35 commits into
source-academy:masterfrom
jiahw4ng:rz

Conversation

@RogerZhang888

Copy link
Copy Markdown
Contributor

Description

Arrows now have
• standardised radius of curvature (except when there are space constraints)
• Terminating segment length (so there is no abrupt "bent" arrow-head)

The arrows which pointed to arrays from the left were initially straight arrows. Now, they also follow Manhattan routing like the rest of the arrows.

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • This change requires a documentation update
  • Code quality improvements

How to test

Run any Source program and step through the CSE Machine to see the arrows.

Checklist

  • I have tested this code
  • I have updated the documentation

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request introduces an arrow filtering system for the CSE Machine visualization, allowing users to toggle arrow visibility based on origin (e.g., text, frame, function). It also refines arrow routing logic, improves path rendering with better corner radius calculations, and adds a new arrow type for function descriptions. Key feedback includes addressing performance issues caused by object instantiation within draw methods, replacing any types with proper interfaces for type safety, simplifying redundant routing logic, and moving magic numbers to the configuration file.

if (this.enclosingFrame) {
this._arrow = new ArrowFromFn(this).to(this.enclosingFrame) as ArrowFromFn;
}
this.createBodyArrow();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

critical

Instantiating FnBodyTarget and ArrowFromFnToBody inside the draw method is a significant performance issue. Since draw is called on every render, and GenericArrow creates a new React.createRef() in its constructor, this will cause the arrow component to unmount and remount on every update, leading to flickering and high CPU usage. These objects should be instantiated once (e.g., in the constructor or handleNewReference) and updated only when coordinates change.

if (Layout.globalEnvNode.frame) {
this._arrow = new ArrowFromFn(this).to(Layout.globalEnvNode.frame) as ArrowFromFn;
}
this.createBodyArrow();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

critical

Similar to FnValue, creating the body arrow and its target inside the draw method will cause performance degradation and stability issues due to constant re-instantiation of objects and refs. Please move this logic out of the draw method.

isStringIdentifiable: boolean;
faded: boolean;
hidden: boolean;
parentFrame?: any; // Reference to the frame this text belongs to

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Using any for parentFrame bypasses TypeScript's type checking. Since this property is used to access coordinates and dimensions (e.g., in ArrowFromText.tsx), it should be typed as IVisible or Frame to ensure type safety.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@RichDom2185 has also advised me in another of my recent PRs to not use the 'any' type in general. The exact comment with his message is here.

#3690 (comment)

It might be outdated! Do expand the "outdated" comment tabs

Comment on lines +62 to +72
if (to instanceof ArrayValue) {
// Route text-to-array arrows with Manhattan segments, ending at array's left-center.
steps.push((x, y) => [preTerminalX, y]);
steps.push((x, y) => [x, targetY]);
steps.push((x, y) => [to.x(), y]);
} else {
// Route all rightward text arrows orthogonally so special cases still stay Manhattan.
steps.push((x, y) => [preTerminalX, y]);
steps.push((x, y) => [x, targetY]);
steps.push((x, y) => [to.x(), y]);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The logic for routing arrows to ArrayValue and other values is identical in this block. The code can be simplified by removing the redundant conditional check.

      // Route rightward text arrows orthogonally using Manhattan segments.
      steps.push((x, y) => [preTerminalX, y]);
      steps.push((x, y) => [x, targetY]);
      steps.push((x, y) => [to.x(), y]);

Comment on lines +260 to +262
protected isInteractive(): boolean {
return true;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Since the explicit check for printableMode was removed from the mouse event handlers, isInteractive should be updated to return false when in printable mode. This ensures that arrows do not respond to hover or click events in the print view, maintaining the previous behavior.

Suggested change
protected isInteractive(): boolean {
return true;
}
/** Subclasses can disable all pointer interactions for passive arrows. */
protected isInteractive(): boolean {
return !CseMachine.getPrintableMode();
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This is extremely important and is similar to what I have already requested the Arrow to fix earlier on Telegram. The exact issue that hasnt been documented was as follows :

doc_2026-03-31_19-30-41.mp4

The arrows permanently become white in the Printable mode once clocked on and only revert back when clicked on some other arrow! This is also the same in the saved image actually which causes the arrow to become invisible.

The above highlighted Gemini's comment dictates a similar aspect of how arrows in general should not be interactive or if they are, the colors should change! It ofcourse should NOT be white.

this._x = frameX + newReference.frame.width() + Config.FrameMarginX;

this._y = newReference.y();
this._y = newReference.y() + 20;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The magic number 20 should be extracted into the Config object to improve maintainability and consistency across the visualization.

Comment on lines +60 to +66
const preTerminalX = Math.max(frameExitX, to.x() - terminalSegmentLength);

if (to instanceof ArrayValue) {
// Route text-to-array arrows with Manhattan segments, ending at array's left-center.
steps.push((x, y) => [preTerminalX, y]);
steps.push((x, y) => [x, targetY]);
steps.push((x, y) => [to.x(), y]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Bug: The calculation for preTerminalX can cause an arrow to route past its target horizontally before correcting, creating a U-shaped overshoot when the target is near its parent frame.
Severity: MEDIUM

Suggested Fix

The routing logic should ensure preTerminalX does not exceed to.x(). Consider using different routing logic when the target is located between the text's origin and the calculated frame exit point.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.

Location: src/features/cseMachine/components/arrows/ArrowFromText.tsx#L60-L66

Potential issue: When routing an arrow from text to a target on its right, the
`preTerminalX` coordinate is calculated using `Math.max(frameExitX, to.x() -
terminalSegmentLength)`. If the target is positioned close to or within its parent
frame, `frameExitX` (the frame's right edge plus a buffer) can be greater than the
target's x-coordinate `to.x()`. This causes `preTerminalX` to be set to `frameExitX`,
forcing the arrow path to extend past the target horizontally before turning back,
resulting in a visually incorrect U-shaped overshoot. This is a common scenario for
bindings where the value is near the parent frame.

Did we get this right? 👍 / 👎 to inform future reviews.

@Akshay-2007-1 Akshay-2007-1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

For the following code segment or any other that requires a few like functions in the global frame pre computed,

const xs = list(1, 2, 3);
const ys = map(x => x + 1, xs);
display(ys);

Even at step 1, when we try to filter the arrows using the new button, the list of pre-executed and shown functions in the global frame just DISAPPEAR! They dont reappear on toggling the filter as well. Only when we change the step do they reappear! This is kind of similar to the issue #3712 which concerns the Layout team too!

The bug is not really “the arrows are wrong”, it is filtering arrows may accidentally wipe cached built-in function names and change what global functions are shown. It is shown below :

Video.Project.11.mp4

@ThatLi and @gigopogo, just out of curiosity, does this concern you? I mean its a gray area as it is triggered on using the filter button that is new and has not been merged yet!

@Akshay-2007-1

Copy link
Copy Markdown
Contributor

I would also like to add that on hovering over the Arrow Filter option, there is no tooltip content that is displayed!

For example, for clearDeadFrames, this is what happens on hovering :
image

Something similar would help new users know what the option does haha! Should be an extremely simple fix!

@RogerZhang888

Copy link
Copy Markdown
Contributor Author

Closing because I created a new PR that only has changes to the appearance of the arrows. Filtering is in a separate PR.

@ThatLi

ThatLi commented Apr 2, 2026

Copy link
Copy Markdown
Contributor

For the following code segment or any other that requires a few like functions in the global frame pre computed,

const xs = list(1, 2, 3);
const ys = map(x => x + 1, xs);
display(ys);

Even at step 1, when we try to filter the arrows using the new button, the list of pre-executed and shown functions in the global frame just DISAPPEAR! They dont reappear on toggling the filter as well. Only when we change the step do they reappear! This is kind of similar to the issue #3712 which concerns the Layout team too!

The bug is not really “the arrows are wrong”, it is filtering arrows may accidentally wipe cached built-in function names and change what global functions are shown. It is shown below :
Video.Project.11.mp4

@ThatLi and @gigopogo, just out of curiosity, does this concern you? I mean its a gray area as it is triggered on using the filter button that is new and has not been merged yet!

This was before #3712 was resolved in the recent PR #3714 so I'm not sure if the disappearance was due to the code from our side or the filtering done here, since I haven't tested the code here yet. But ideally the pre-defined functions should be there regardless of whether anything is toggled.

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.

3 participants