From 94bf19fffe117f233accf995f8f60f61145bd4e1 Mon Sep 17 00:00:00 2001 From: Vemulapalli Akshay Date: Wed, 11 Mar 2026 14:02:44 +0800 Subject: [PATCH 1/3] Fixed arrow change! --- .../components/arrows/GenericArrow.tsx | 550 +++++++++--------- 1 file changed, 285 insertions(+), 265 deletions(-) diff --git a/src/features/cseMachine/components/arrows/GenericArrow.tsx b/src/features/cseMachine/components/arrows/GenericArrow.tsx index 401da3ced0..3ed771f141 100644 --- a/src/features/cseMachine/components/arrows/GenericArrow.tsx +++ b/src/features/cseMachine/components/arrows/GenericArrow.tsx @@ -1,265 +1,285 @@ -import Konva from 'konva'; -import { KonvaEventObject } from 'konva/lib/Node'; -import React, { RefObject } from 'react'; -import { Arrow as KonvaArrow, Group as KonvaGroup, Path as KonvaPath } from 'react-konva'; - -import CseMachine from '../../CseMachine'; -import { Config, ShapeDefaultProps } from '../../CseMachineConfig'; -import { Layout } from '../../CseMachineLayout'; -import { IHoverable, IVisible, StepsArray } from '../../CseMachineTypes'; -import { defaultStrokeColor, fadedStrokeColor } from '../../CseMachineUtils'; -import { Visible } from '../Visible'; -import { arrowSelection } from './ArrowSelection'; - -/** this class encapsulates an arrow to be drawn between 2 points */ -export class GenericArrow - extends Visible - implements IHoverable -{ - private _path: string = ''; - points: number[] = []; - source: Source; - target: Target | undefined; - faded: boolean = false; - private pathRef: RefObject = React.createRef(); - private arrowHeadRef: RefObject = React.createRef(); - - // Check if this arrow is selected - protected isSelected(): boolean { - return arrowSelection.isSelected(this); - } - - // Select this arrow - protected select(): void { - arrowSelection.setSelected(this); - } - - isLive: boolean = false; // Added to track if the arrow is live (an inherent property of the arrow) - /* - * The above is added since an arrow can in general be drawn between two points - * that may or may not be a Frame. Hence, we cannot determine if the arrow is live - * based on whether the source or target is a live Frame. Thus, we set this property - * when we create the arrow - */ - - constructor(from: Source) { - super(); - this.source = from; - this.target = undefined; - this._x = from.x(); - this._y = from.y(); - this.isLive = false; // default to false - } - - path(): string { - return this._path; - } - - to(to: Target): GenericArrow { - this.target = to; - this._width = Math.abs(to.x() - this.source.x()); - this._height = Math.abs(to.y() - this.source.y()); - - const points = this.calculateSteps().reduce>( - (points, step) => [...points, ...step(points[points.length - 2], points[points.length - 1])], - [this.source.x(), this.source.y()] - ); - points.splice(0, 2); - - // starting point - this._path += `M ${points[0]} ${points[1]} `; - if (points.length === 4) { - // end the path if the line only has starting and ending coordinates - this._path += `L ${points[2]} ${points[3]} `; - } else { - let n = 0; - while (n < points.length - 4) { - const [xa, ya, xb, yb, xc, yc] = points.slice(n, n + 6); - const dx1 = xb - xa; - const dx2 = xc - xb; - const dy1 = yb - ya; - const dy2 = yc - yb; - const br = Math.min( - Config.ArrowCornerRadius, - Math.max(Math.abs(dx1), Math.abs(dy1)) / 2, - Math.max(Math.abs(dx2), Math.abs(dy2)) / 2 - ); - const x1 = xb - br * Math.sign(dx1); - const y1 = yb - br * Math.sign(dy1); - const x2 = xb + br * Math.sign(dx2); - const y2 = yb + br * Math.sign(dy2); - - // draw quadratic curves over corners - this._path += `L ${x1} ${y1} Q ${xb} ${yb} ${x2} ${y2} `; - n += 2; - } - } - // end path - this._path += `L ${points[points.length - 2]} ${points[points.length - 1]} `; - this.points = points; - return this; - } - - /** - * Calculates the steps that this arrows takes. - * The arrow is decomposed into numerous straight line segments, each of which we - * can consider as a step of dx in the x direction and of dy in the y direction. - * The line segment is thus defined by 2 points (x, y) and (x + dx, y + dy) - * where (x, y) is the ending coordinate of the previous line segment. - * This function returns an array of such steps, represented by an array of functions - * [ (x, y) => [x + dx1, y + dy1], (x, y) => [x + dx2, y + dy2], ... ]. - * From this, we can retrieve the points that make up the arrow as such: - * (from.x from.y), (from.x + dx1, from.y + dy1), (from.x + dx1 + dx2, from.y + dy1 + dy2), .. - * - * Note that the functions need not be of the form (x, y) => [x + dx, y + dy]; - * (x, y) => [to.x, to.y] is valid as well, and is used to specify a step to the ending coordinates - * - * @return an array of steps represented by functions - */ - protected calculateSteps(): StepsArray { - const to = this.target; - if (!to) return []; - return [() => [to.x(), to.y()]]; - } - - /** - * Returns the hover color for this arrow type. - * Subclasses can override this to provide custom hover colors. - */ - protected getHighlightedColor(): string { - return Config.ArrowHighlightedColor; - } - - onMouseEnter = (e: KonvaEventObject) => { - if (CseMachine.getPrintableMode()) return; - e.cancelBubble = true; - this.setHighlightedStyle(); - // Move entire arrow group to top - if (this.ref.current && this.ref.current.moveToTop) { - // Move the arrow's parent container to top first, then move arrow within that - const parent = this.ref.current.getParent(); - if (parent && parent.moveToTop) { - parent.moveToTop(); - } - this.ref.current.moveToTop(); - } - this.ref.current?.getLayer()?.batchDraw(); - }; - - onMouseLeave = (e: KonvaEventObject) => { - if (CseMachine.getPrintableMode()) return; - e.cancelBubble = true; - - // Don't change color if selected - if (this.isSelected()) { - return; - } - - this.setNormalStyle(); - this.ref.current?.getLayer()?.batchDraw(); - }; - - public setHighlightedStyle() { - const highlightColor = this.getHighlightedColor(); - if (this.pathRef.current) { - this.pathRef.current.stroke(highlightColor); - this.pathRef.current.strokeWidth(Config.ArrowHoveredStrokeWidth); - } - if (this.arrowHeadRef.current) { - this.arrowHeadRef.current.fill(highlightColor); - this.arrowHeadRef.current.pointerWidth(Config.ArrowHoveredHeadSize); - this.arrowHeadRef.current.pointerLength(Config.ArrowHoveredHeadSize); - } - } - - public setNormalStyle() { - const color = this.isLive ? defaultStrokeColor() : fadedStrokeColor(); - if (this.pathRef.current) { - this.pathRef.current.stroke(color); - this.pathRef.current.strokeWidth(Config.ArrowStrokeWidth); - } - if (this.arrowHeadRef.current) { - this.arrowHeadRef.current.fill(color); - this.arrowHeadRef.current.pointerWidth(Config.ArrowHeadSize); - this.arrowHeadRef.current.pointerLength(Config.ArrowHeadSize); - } - } - - onClick = (e: KonvaEventObject) => { - e.cancelBubble = true; - - // Toggle selection - clear first, then select if it wasn't already selected - const wasSelected = this.isSelected(); - const oldArrow = arrowSelection.clearSelection(); - - // Update old arrow's visual state - if (oldArrow && oldArrow !== this) { - oldArrow.setNormalStyle(); - } - - if (!wasSelected) { - this.select(); - // Update this arrow's visual state - this.setHighlightedStyle(); - } - - // Force redraw entire layer to update all arrows - this.ref.current?.getLayer()?.batchDraw(); - }; - - onContextMenu = (e: KonvaEventObject) => { - e.evt.preventDefault(); // Prevent browser context menu - this.onClick(e); - }; - - protected getCurrentColor(): string { - if (this.isSelected()) { - return this.getHighlightedColor(); // Selected uses hover color - } - return this.faded ? fadedStrokeColor() : defaultStrokeColor(); - } - - // Subclasses can override to recompute liveness before drawing - protected updateIsLive(): void {} //kind of an abstract method - - draw() { - this.updateIsLive(); //just before drawijng, update liveness for the arrows (since this was causing erroes earlier ) - if (Layout.clearDeadFrames && !this.isLive) { - return null; - } - - const stroke = this.isLive ? defaultStrokeColor() : fadedStrokeColor(); - - return ( - (e.cancelBubble = true)} - > - - - - ); - } -} +import Konva from 'konva'; +import { KonvaEventObject } from 'konva/lib/Node'; +import React, { RefObject } from 'react'; +import { Arrow as KonvaArrow, Group as KonvaGroup, Path as KonvaPath } from 'react-konva'; + +import CseMachine from '../../CseMachine'; +import { Config, ShapeDefaultProps } from '../../CseMachineConfig'; +import { Layout } from '../../CseMachineLayout'; +import { IHoverable, IVisible, StepsArray } from '../../CseMachineTypes'; +import { defaultStrokeColor, fadedStrokeColor } from '../../CseMachineUtils'; +import { Visible } from '../Visible'; +import { arrowSelection } from './ArrowSelection'; + +/** this class encapsulates an arrow to be drawn between 2 points */ +export class GenericArrow + extends Visible + implements IHoverable +{ + private _path: string = ''; + points: number[] = []; + source: Source; + target: Target | undefined; + faded: boolean = false; + private pathRef: RefObject = React.createRef(); + private arrowHeadRef: RefObject = React.createRef(); + + // Check if this arrow is selected + protected isSelected(): boolean { + return arrowSelection.isSelected(this); + } + + // Select this arrow + protected select(): void { + arrowSelection.setSelected(this); + } + + isLive: boolean = false; // Added to track if the arrow is live (an inherent property of the arrow) + /* + * The above is added since an arrow can in general be drawn between two points + * that may or may not be a Frame. Hence, we cannot determine if the arrow is live + * based on whether the source or target is a live Frame. Thus, we set this property + * when we create the arrow + */ + + constructor(from: Source) { + super(); + this.source = from; + this.target = undefined; + this._x = from.x(); + this._y = from.y(); + this.isLive = false; // default to false + } + + path(): string { + return this._path; + } + + to(to: Target): GenericArrow { + this.target = to; + this.recomputePath(); + return this; + } + + private recomputePath(): void { + if (!this.target) { + this._path = ''; + this.points = []; + return; + } + + const to = this.target; + this._x = this.source.x(); + this._y = this.source.y(); + this._width = Math.abs(to.x() - this.source.x()); + this._height = Math.abs(to.y() - this.source.y()); + + const points = this.calculateSteps().reduce>( + (acc, step) => [...acc, ...step(acc[acc.length - 2], acc[acc.length - 1])], + [this.source.x(), this.source.y()] + ); + points.splice(0, 2); + + this._path = ''; + if (points.length < 4) { + this.points = points; + return; + } + + // starting point + this._path += `M ${points[0]} ${points[1]} `; + if (points.length === 4) { + // end the path if the line only has starting and ending coordinates + this._path += `L ${points[2]} ${points[3]} `; + } else { + let n = 0; + while (n < points.length - 4) { + const [xa, ya, xb, yb, xc, yc] = points.slice(n, n + 6); + const dx1 = xb - xa; + const dx2 = xc - xb; + const dy1 = yb - ya; + const dy2 = yc - yb; + const br = Math.min( + Config.ArrowCornerRadius, + Math.max(Math.abs(dx1), Math.abs(dy1)) / 2, + Math.max(Math.abs(dx2), Math.abs(dy2)) / 2 + ); + const x1 = xb - br * Math.sign(dx1); + const y1 = yb - br * Math.sign(dy1); + const x2 = xb + br * Math.sign(dx2); + const y2 = yb + br * Math.sign(dy2); + + // draw quadratic curves over corners + this._path += `L ${x1} ${y1} Q ${xb} ${yb} ${x2} ${y2} `; + n += 2; + } + } + // end path + this._path += `L ${points[points.length - 2]} ${points[points.length - 1]} `; + this.points = points; + } + + /** + * Calculates the steps that this arrows takes. + * The arrow is decomposed into numerous straight line segments, each of which we + * can consider as a step of dx in the x direction and of dy in the y direction. + * The line segment is thus defined by 2 points (x, y) and (x + dx, y + dy) + * where (x, y) is the ending coordinate of the previous line segment. + * This function returns an array of such steps, represented by an array of functions + * [ (x, y) => [x + dx1, y + dy1], (x, y) => [x + dx2, y + dy2], ... ]. + * From this, we can retrieve the points that make up the arrow as such: + * (from.x from.y), (from.x + dx1, from.y + dy1), (from.x + dx1 + dx2, from.y + dy1 + dy2), .. + * + * Note that the functions need not be of the form (x, y) => [x + dx, y + dy]; + * (x, y) => [to.x, to.y] is valid as well, and is used to specify a step to the ending coordinates + * + * @return an array of steps represented by functions + */ + protected calculateSteps(): StepsArray { + const to = this.target; + if (!to) return []; + return [() => [to.x(), to.y()]]; + } + + /** + * Returns the hover color for this arrow type. + * Subclasses can override this to provide custom hover colors. + */ + protected getHighlightedColor(): string { + return Config.ArrowHighlightedColor; + } + + onMouseEnter = (e: KonvaEventObject) => { + if (CseMachine.getPrintableMode()) return; + e.cancelBubble = true; + this.setHighlightedStyle(); + // Move entire arrow group to top + if (this.ref.current && this.ref.current.moveToTop) { + // Move the arrow's parent container to top first, then move arrow within that + const parent = this.ref.current.getParent(); + if (parent && parent.moveToTop) { + parent.moveToTop(); + } + this.ref.current.moveToTop(); + } + this.ref.current?.getLayer()?.batchDraw(); + }; + + onMouseLeave = (e: KonvaEventObject) => { + if (CseMachine.getPrintableMode()) return; + e.cancelBubble = true; + + // Don't change color if selected + if (this.isSelected()) { + return; + } + + this.setNormalStyle(); + this.ref.current?.getLayer()?.batchDraw(); + }; + + public setHighlightedStyle() { + const highlightColor = this.getHighlightedColor(); + if (this.pathRef.current) { + this.pathRef.current.stroke(highlightColor); + this.pathRef.current.strokeWidth(Config.ArrowHoveredStrokeWidth); + } + if (this.arrowHeadRef.current) { + this.arrowHeadRef.current.fill(highlightColor); + this.arrowHeadRef.current.pointerWidth(Config.ArrowHoveredHeadSize); + this.arrowHeadRef.current.pointerLength(Config.ArrowHoveredHeadSize); + } + } + + public setNormalStyle() { + const color = this.isLive ? defaultStrokeColor() : fadedStrokeColor(); + if (this.pathRef.current) { + this.pathRef.current.stroke(color); + this.pathRef.current.strokeWidth(Config.ArrowStrokeWidth); + } + if (this.arrowHeadRef.current) { + this.arrowHeadRef.current.fill(color); + this.arrowHeadRef.current.pointerWidth(Config.ArrowHeadSize); + this.arrowHeadRef.current.pointerLength(Config.ArrowHeadSize); + } + } + + onClick = (e: KonvaEventObject) => { + e.cancelBubble = true; + + // Toggle selection - clear first, then select if it wasn't already selected + const wasSelected = this.isSelected(); + const oldArrow = arrowSelection.clearSelection(); + + // Update old arrow's visual state + if (oldArrow && oldArrow !== this) { + oldArrow.setNormalStyle(); + } + + if (!wasSelected) { + this.select(); + // Update this arrow's visual state + this.setHighlightedStyle(); + } + + // Force redraw entire layer to update all arrows + this.ref.current?.getLayer()?.batchDraw(); + }; + + onContextMenu = (e: KonvaEventObject) => { + e.evt.preventDefault(); // Prevent browser context menu + this.onClick(e); + }; + + protected getCurrentColor(): string { + if (this.isSelected()) { + return this.getHighlightedColor(); // Selected uses hover color + } + return this.faded ? fadedStrokeColor() : defaultStrokeColor(); + } + + // Subclasses can override to recompute liveness before drawing + protected updateIsLive(): void {} //kind of an abstract method + + draw() { + this.recomputePath(); + this.updateIsLive(); //just before drawijng, update liveness for the arrows (since this was causing erroes earlier ) + if (Layout.clearDeadFrames && !this.isLive) { + return null; + } + + const stroke = this.isLive ? defaultStrokeColor() : fadedStrokeColor(); + + return ( + (e.cancelBubble = true)} + > + + + + ); + } +} From 2f66622201b9697bc8085d45fce98b58b5ac86f7 Mon Sep 17 00:00:00 2001 From: Vemulapalli Akshay Date: Wed, 11 Mar 2026 14:13:03 +0800 Subject: [PATCH 2/3] Fixed the cache difference issue --- src/features/cseMachine/CseMachine.tsx | 501 +++++++++++++------------ 1 file changed, 257 insertions(+), 244 deletions(-) diff --git a/src/features/cseMachine/CseMachine.tsx b/src/features/cseMachine/CseMachine.tsx index 2e68fb8db9..f2434f3dcd 100644 --- a/src/features/cseMachine/CseMachine.tsx +++ b/src/features/cseMachine/CseMachine.tsx @@ -1,244 +1,257 @@ -import { Context } from 'js-slang'; -import { Control, Stash } from 'js-slang/dist/cse-machine/interpreter'; -import React from 'react'; - -import { arrowSelection } from './components/arrows/ArrowSelection'; -import { Layout, LayoutCache } from './CseMachineLayout'; -import { EnvTree } from './CseMachineTypes'; -import { deepCopyTree, getEnvId } from './CseMachineUtils'; - -type SetVis = (vis: React.ReactNode) => void; -type SetEditorHighlightedLines = (segments: [number, number][]) => void; -type SetisStepLimitExceeded = (isControlEmpty: boolean) => void; - -/** CSE Machine is exposed from this class */ -export default class CseMachine { - /** callback function to update the visualization state in the SideContentCseMachine component */ - private static setVis: SetVis; - /** function to highlight editor lines */ - public static setEditorHighlightedLines: SetEditorHighlightedLines; - /** callback function to update the step limit exceeded state in the SideContentCseMachine component */ - private static setIsStepLimitExceeded: SetisStepLimitExceeded; - // Ghost layout snapshots, separated by mode to keep coordinates fixed within each mode. - public static normalLayoutCache: LayoutCache | null = null; - public static printLayoutCache: LayoutCache | null = null; - private static printableMode: boolean = false; - private static controlStash: boolean = false; // TODO: discuss if the default should be true - private static stackTruncated: boolean = false; - private static centerAlignment: boolean = false; // added for center alignment - private static centerAlignmentToggled: boolean = false; - private static environmentTree: EnvTree | undefined; - private static currentEnvId: string; - private static control: Control | undefined; - private static stash: Stash | undefined; - public static togglePrintableMode(): void { - CseMachine.printableMode = !CseMachine.printableMode; - } - public static toggleControlStash(): void { - CseMachine.controlStash = !CseMachine.controlStash; - } - public static toggleStackTruncated(): void { - CseMachine.stackTruncated = !CseMachine.stackTruncated; - } - public static setClearDeadFrames(enabled: boolean): void { - Layout.clearDeadFrames = enabled; - } - public static clearCachedLayouts(): void { - Layout.currentLight = undefined; - Layout.currentDark = undefined; - Layout.currentStackDark = undefined; - Layout.currentStackTruncDark = undefined; - Layout.currentStackLight = undefined; - Layout.currentStackTruncLight = undefined; - Layout.prevLayout = undefined; - Layout.key = 0; - CseMachine.normalLayoutCache = null; - CseMachine.printLayoutCache = null; - } - // added for center alignment - public static toggleCenterAlignment(): void { - CseMachine.centerAlignment = !CseMachine.centerAlignment; - CseMachine.centerAlignmentToggled = true; - } - - public static getCurrentEnvId(): string { - return CseMachine.currentEnvId; - } - public static getPrintableMode(): boolean { - return CseMachine.printableMode; - } - public static getControlStash(): boolean { - return CseMachine.controlStash; - } - public static getStackTruncated(): boolean { - return CseMachine.stackTruncated; - } - // added for center alignment - public static getCenterAlignment(): boolean { - return CseMachine.centerAlignment; - } - public static getMasterLayout(): LayoutCache | null { - return CseMachine.getPrintableMode() - ? CseMachine.printLayoutCache - : CseMachine.normalLayoutCache; - } - public static setMasterLayout(cache: LayoutCache): void { - if (CseMachine.getPrintableMode()) { - CseMachine.printLayoutCache = cache; - } else { - CseMachine.normalLayoutCache = cache; - } - } - - public static isControl(): boolean { - return this.control ? !this.control.isEmpty() : false; - } - - /** SideContentCseMachine initializes this onMount with the callback function */ - static init( - setVis: SetVis, - width: number, - height: number, - setEditorHighlightedLines: (segments: [number, number][]) => void, - setIsStepLimitExceeded: SetisStepLimitExceeded - ) { - Layout.visibleHeight = height; - Layout.visibleWidth = width; - this.setVis = setVis; - this.setEditorHighlightedLines = setEditorHighlightedLines; - this.setIsStepLimitExceeded = setIsStepLimitExceeded; - } - - static clear() { - Layout.values.clear(); - arrowSelection.clearSelection(); - } - - /** updates the visualization state in the SideContentCseMachine component based on - * the JS Slang context passed in */ - static drawCse(context: Context) { - // store environmentTree at last breakpoint. - CseMachine.environmentTree = deepCopyTree(context.runtime.environmentTree as EnvTree); - CseMachine.currentEnvId = getEnvId(context.runtime.environments[0]); - if (!this.setVis || !context.runtime.control || !context.runtime.stash) - throw new Error('CSE machine not initialized'); - CseMachine.control = context.runtime.control; - CseMachine.stash = context.runtime.stash; - CseMachine.setClearDeadFrames(false); - - Layout.setContext( - context.runtime.environmentTree as EnvTree, - context.runtime.control, - context.runtime.stash, - context.chapter - ); - - // Build ghost layout cache lazily per mode. - if (!CseMachine.normalLayoutCache || !CseMachine.printLayoutCache) { - // fill up both lookup table of normal mode and printable mode - const originalMode = CseMachine.getPrintableMode(); - - CseMachine.printableMode = true; - CseMachine.setMasterLayout(Layout.getLayoutPositions(this.controlStash)); - - CseMachine.printableMode = false; - CseMachine.setMasterLayout(Layout.getLayoutPositions(this.controlStash)); - - // 3. Restore the user's actual mode setting - CseMachine.printableMode = originalMode; - } - - // Apply Fixed Positions - if (CseMachine.getMasterLayout()) { - Layout.applyFixedPositions(); - } - this.setVis(Layout.draw()); - this.setIsStepLimitExceeded(context.runtime.control.isEmpty()); - Layout.updateDimensions(Layout.visibleWidth, Layout.visibleHeight); - } - - static redraw() { - if (CseMachine.environmentTree && CseMachine.control && CseMachine.stash) { - // checks if the required diagram exists, and updates the dom node using setVis - - // if center alignment is toggled, change the alignment and redraw the diagram with new coordinates - if (this.centerAlignmentToggled) { - Layout.setContext(CseMachine.environmentTree, CseMachine.control, CseMachine.stash); - if (!CseMachine.getMasterLayout()) { - CseMachine.setMasterLayout(Layout.getLayoutPositions(this.controlStash)); - } - if (CseMachine.getMasterLayout()) { - Layout.applyFixedPositions(); - } - this.setVis(Layout.draw()); - this.centerAlignmentToggled = false; - } - - if ( - CseMachine.getPrintableMode() && - CseMachine.getControlStash() && - CseMachine.getStackTruncated() && - Layout.currentStackTruncLight !== undefined - ) { - this.setVis(Layout.currentStackTruncLight); - } else if ( - CseMachine.getPrintableMode() && - CseMachine.getControlStash() && - !CseMachine.getStackTruncated() && - Layout.currentStackLight !== undefined - ) { - this.setVis(Layout.currentStackLight); - } else if ( - !CseMachine.getPrintableMode() && - CseMachine.getControlStash() && - CseMachine.getStackTruncated() && - Layout.currentStackTruncDark !== undefined - ) { - this.setVis(Layout.currentStackTruncDark); - } else if ( - !CseMachine.getPrintableMode() && - CseMachine.getControlStash() && - !CseMachine.getStackTruncated() && - Layout.currentStackDark !== undefined - ) { - this.setVis(Layout.currentStackDark); - } else if ( - CseMachine.getPrintableMode() && - !CseMachine.getControlStash() && - Layout.currentLight !== undefined - ) { - this.setVis(Layout.currentLight); - } else if ( - !CseMachine.getPrintableMode() && - !CseMachine.getControlStash() && - Layout.currentDark !== undefined - ) { - this.setVis(Layout.currentDark); - } else { - Layout.setContext(CseMachine.environmentTree, CseMachine.control, CseMachine.stash); - if (CseMachine.getMasterLayout()) { - Layout.applyFixedPositions(); - } - this.setVis(Layout.draw()); - } - Layout.updateDimensions(Layout.visibleWidth, Layout.visibleHeight); - } - } - - static updateDimensions(width: number, height: number) { - if (Layout.stageRef != null && width !== null && height !== null) { - Layout.updateDimensions(width, height); - } - } - - static clearCse() { - if (this.setVis) { - this.setVis(undefined); - CseMachine.environmentTree = undefined; - CseMachine.control = undefined; - CseMachine.stash = undefined; - } - CseMachine.setClearDeadFrames(false); - this.clear(); - } -} +import { Context } from 'js-slang'; +import { Control, Stash } from 'js-slang/dist/cse-machine/interpreter'; +import React from 'react'; + +import { arrowSelection } from './components/arrows/ArrowSelection'; +import { Layout, LayoutCache } from './CseMachineLayout'; +import { EnvTree } from './CseMachineTypes'; +import { deepCopyTree, getEnvId } from './CseMachineUtils'; + +type SetVis = (vis: React.ReactNode) => void; +type SetEditorHighlightedLines = (segments: [number, number][]) => void; +type SetisStepLimitExceeded = (isControlEmpty: boolean) => void; + +/** CSE Machine is exposed from this class */ +export default class CseMachine { + /** callback function to update the visualization state in the SideContentCseMachine component */ + private static setVis: SetVis; + /** function to highlight editor lines */ + public static setEditorHighlightedLines: SetEditorHighlightedLines; + /** callback function to update the step limit exceeded state in the SideContentCseMachine component */ + private static setIsStepLimitExceeded: SetisStepLimitExceeded; + // Ghost layout snapshots, separated by mode to keep coordinates fixed within each mode. + public static normalLayoutCache: LayoutCache | null = null; + public static printLayoutCache: LayoutCache | null = null; + private static printableMode: boolean = false; + private static controlStash: boolean = false; // TODO: discuss if the default should be true + private static stackTruncated: boolean = false; + private static centerAlignment: boolean = false; // added for center alignment + private static centerAlignmentToggled: boolean = false; + private static environmentTree: EnvTree | undefined; + private static currentEnvId: string; + private static control: Control | undefined; + private static stash: Stash | undefined; + public static togglePrintableMode(): void { + CseMachine.printableMode = !CseMachine.printableMode; + } + public static toggleControlStash(): void { + CseMachine.controlStash = !CseMachine.controlStash; + } + public static toggleStackTruncated(): void { + CseMachine.stackTruncated = !CseMachine.stackTruncated; + } + public static setClearDeadFrames(enabled: boolean): void { + Layout.clearDeadFrames = enabled; + } + public static clearCachedLayouts(): void { + Layout.currentLight = undefined; + Layout.currentDark = undefined; + Layout.currentStackDark = undefined; + Layout.currentStackTruncDark = undefined; + Layout.currentStackLight = undefined; + Layout.currentStackTruncLight = undefined; + Layout.prevLayout = undefined; + Layout.key = 0; + CseMachine.normalLayoutCache = null; + CseMachine.printLayoutCache = null; + } + // added for center alignment + public static toggleCenterAlignment(): void { + CseMachine.centerAlignment = !CseMachine.centerAlignment; + CseMachine.centerAlignmentToggled = true; + } + + public static getCurrentEnvId(): string { + return CseMachine.currentEnvId; + } + public static getPrintableMode(): boolean { + return CseMachine.printableMode; + } + public static getControlStash(): boolean { + return CseMachine.controlStash; + } + public static getStackTruncated(): boolean { + return CseMachine.stackTruncated; + } + // added for center alignment + public static getCenterAlignment(): boolean { + return CseMachine.centerAlignment; + } + public static getMasterLayout(): LayoutCache | null { + return CseMachine.getPrintableMode() + ? CseMachine.printLayoutCache + : CseMachine.normalLayoutCache; + } + public static setMasterLayout(cache: LayoutCache): void { + if (CseMachine.getPrintableMode()) { + CseMachine.printLayoutCache = cache; + } else { + CseMachine.normalLayoutCache = cache; + } + } + + public static isControl(): boolean { + return this.control ? !this.control.isEmpty() : false; + } + + /** SideContentCseMachine initializes this onMount with the callback function */ + static init( + setVis: SetVis, + width: number, + height: number, + setEditorHighlightedLines: (segments: [number, number][]) => void, + setIsStepLimitExceeded: SetisStepLimitExceeded + ) { + Layout.visibleHeight = height; + Layout.visibleWidth = width; + this.setVis = setVis; + this.setEditorHighlightedLines = setEditorHighlightedLines; + this.setIsStepLimitExceeded = setIsStepLimitExceeded; + } + + static clear() { + Layout.values.clear(); + arrowSelection.clearSelection(); + } + + /** updates the visualization state in the SideContentCseMachine component based on + * the JS Slang context passed in */ + static drawCse(context: Context) { + // store environmentTree at last breakpoint. + CseMachine.environmentTree = deepCopyTree(context.runtime.environmentTree as EnvTree); + CseMachine.currentEnvId = getEnvId(context.runtime.environments[0]); + if (!this.setVis || !context.runtime.control || !context.runtime.stash) + throw new Error('CSE machine not initialized'); + CseMachine.control = context.runtime.control; + CseMachine.stash = context.runtime.stash; + CseMachine.setClearDeadFrames(false); + + Layout.setContext( + context.runtime.environmentTree as EnvTree, + context.runtime.control, + context.runtime.stash, + context.chapter + ); + + // Build ghost layout cache lazily per mode, using mode-specific layout. + if (!CseMachine.normalLayoutCache || !CseMachine.printLayoutCache) { + const originalMode = CseMachine.getPrintableMode(); + + const buildCache = (printable: boolean) => { + CseMachine.printableMode = printable; + Layout.setContext( + context.runtime.environmentTree as EnvTree, + context.runtime.control, + context.runtime.stash, + context.chapter + ); + return Layout.getLayoutPositions(this.controlStash); + }; + + CseMachine.printLayoutCache = buildCache(true); + CseMachine.normalLayoutCache = buildCache(false); + + // Restore the user's actual mode setting and layout. + CseMachine.printableMode = originalMode; + Layout.setContext( + context.runtime.environmentTree as EnvTree, + context.runtime.control, + context.runtime.stash, + context.chapter + ); + } + + // Apply Fixed Positions + if (CseMachine.getMasterLayout()) { + Layout.applyFixedPositions(); + } + this.setVis(Layout.draw()); + this.setIsStepLimitExceeded(context.runtime.control.isEmpty()); + Layout.updateDimensions(Layout.visibleWidth, Layout.visibleHeight); + } + + static redraw() { + if (CseMachine.environmentTree && CseMachine.control && CseMachine.stash) { + // checks if the required diagram exists, and updates the dom node using setVis + + // if center alignment is toggled, change the alignment and redraw the diagram with new coordinates + if (this.centerAlignmentToggled) { + Layout.setContext(CseMachine.environmentTree, CseMachine.control, CseMachine.stash); + if (!CseMachine.getMasterLayout()) { + CseMachine.setMasterLayout(Layout.getLayoutPositions(this.controlStash)); + } + if (CseMachine.getMasterLayout()) { + Layout.applyFixedPositions(); + } + this.setVis(Layout.draw()); + this.centerAlignmentToggled = false; + } + + if ( + CseMachine.getPrintableMode() && + CseMachine.getControlStash() && + CseMachine.getStackTruncated() && + Layout.currentStackTruncLight !== undefined + ) { + this.setVis(Layout.currentStackTruncLight); + } else if ( + CseMachine.getPrintableMode() && + CseMachine.getControlStash() && + !CseMachine.getStackTruncated() && + Layout.currentStackLight !== undefined + ) { + this.setVis(Layout.currentStackLight); + } else if ( + !CseMachine.getPrintableMode() && + CseMachine.getControlStash() && + CseMachine.getStackTruncated() && + Layout.currentStackTruncDark !== undefined + ) { + this.setVis(Layout.currentStackTruncDark); + } else if ( + !CseMachine.getPrintableMode() && + CseMachine.getControlStash() && + !CseMachine.getStackTruncated() && + Layout.currentStackDark !== undefined + ) { + this.setVis(Layout.currentStackDark); + } else if ( + CseMachine.getPrintableMode() && + !CseMachine.getControlStash() && + Layout.currentLight !== undefined + ) { + this.setVis(Layout.currentLight); + } else if ( + !CseMachine.getPrintableMode() && + !CseMachine.getControlStash() && + Layout.currentDark !== undefined + ) { + this.setVis(Layout.currentDark); + } else { + Layout.setContext(CseMachine.environmentTree, CseMachine.control, CseMachine.stash); + if (CseMachine.getMasterLayout()) { + Layout.applyFixedPositions(); + } + this.setVis(Layout.draw()); + } + Layout.updateDimensions(Layout.visibleWidth, Layout.visibleHeight); + } + } + + static updateDimensions(width: number, height: number) { + if (Layout.stageRef != null && width !== null && height !== null) { + Layout.updateDimensions(width, height); + } + } + + static clearCse() { + if (this.setVis) { + this.setVis(undefined); + CseMachine.environmentTree = undefined; + CseMachine.control = undefined; + CseMachine.stash = undefined; + } + CseMachine.setClearDeadFrames(false); + this.clear(); + } +} From 03594c63decd6d0ece4de9215b71150c199934cb Mon Sep 17 00:00:00 2001 From: Vemulapalli Akshay Date: Thu, 12 Mar 2026 12:09:07 +0800 Subject: [PATCH 3/3] Fixed tsc error - II --- src/features/cseMachine/CseMachine.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/features/cseMachine/CseMachine.tsx b/src/features/cseMachine/CseMachine.tsx index f2434f3dcd..769f06ff0a 100644 --- a/src/features/cseMachine/CseMachine.tsx +++ b/src/features/cseMachine/CseMachine.tsx @@ -141,8 +141,8 @@ export default class CseMachine { CseMachine.printableMode = printable; Layout.setContext( context.runtime.environmentTree as EnvTree, - context.runtime.control, - context.runtime.stash, + context.runtime.control!, + context.runtime.stash!, context.chapter ); return Layout.getLayoutPositions(this.controlStash);