Skip to content

Commit

Permalink
track previousBoundingBox & markDirty before position update to fix d…
Browse files Browse the repository at this point in the history
…rag trailing
  • Loading branch information
ZainGS committed Aug 12, 2024
1 parent d19d3ce commit 43a6679
Show file tree
Hide file tree
Showing 3 changed files with 80 additions and 28 deletions.
78 changes: 56 additions & 22 deletions src/renderer/renderer.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
// src/renderer/renderer.ts
// Manages the rendering loop and handles the drawing of shapes on the canvas.

import { SceneGraph } from '../scene-graph/scene-graph';
import { Node } from '../scene-graph/node';
import { Shape } from '../scene-graph/shape';
Expand All @@ -10,6 +8,10 @@ export class Renderer {
private ctx: CanvasRenderingContext2D;
private sceneGraph: SceneGraph;
private dirtyRegions: { x: number; y: number; width: number; height: number }[] = [];
private draggingShape: Shape | null = null;
private offsetX: number = 0;
private offsetY: number = 0;
private isDragging: boolean = false;

constructor(canvas: HTMLCanvasElement, sceneGraph: SceneGraph) {
this.canvas = canvas;
Expand All @@ -22,16 +24,15 @@ export class Renderer {
this.setupEventListeners();
}

// Start the rendering loop manually
public start() {
this.startRenderingLoop();
}

private startRenderingLoop() {
const renderLoop = () => {
this.updateDirtyRegions(); // Determine what needs to be redrawn
this.clearDirtyRegions(); // Clear only the dirty regions
this.redrawDirtyRegions(); // Redraw the dirty regions
this.updateDirtyRegions();
this.clearDirtyRegions();
this.redrawDirtyRegions();

requestAnimationFrame(renderLoop);
};
Expand All @@ -40,18 +41,23 @@ export class Renderer {
}

private updateDirtyRegions() {
this.dirtyRegions = []; // Clear previous dirty regions

// Traverse the scene graph and collect dirty regions
this.dirtyRegions = [];
this.collectDirtyRegions(this.sceneGraph.root);
}

private collectDirtyRegions(node: Node) {
if (node instanceof Shape && node.isShapeDirty() && node.getBoundingBox()) {
this.dirtyRegions.push(node.getBoundingBox()!);
if (node instanceof Shape && node.isShapeDirty()) {
if (node.getPreviousBoundingBox()) {
// Add the previous bounding box to the dirty regions.
// Clearing these out will prevent trailing when dragging.
this.dirtyRegions.push(node.getPreviousBoundingBox()!);
}
if (node.getBoundingBox()) {
// Add the current bounding box to the dirty regions
this.dirtyRegions.push(node.getBoundingBox()!);
}
}

// Recursively collect dirty regions from children
for (const child of node.children) {
this.collectDirtyRegions(child);
}
Expand All @@ -64,7 +70,6 @@ export class Renderer {
}

private redrawDirtyRegions() {
// Redraw only the shapes within the dirty regions
this.redrawNode(this.sceneGraph.root);
}

Expand All @@ -74,29 +79,59 @@ export class Renderer {
node.resetDirtyFlag();
}

// Recursively redraw children
for (const child of node.children) {
this.redrawNode(child);
}
}

private setupEventListeners() {
this.canvas.addEventListener('click', this.handleClick.bind(this));
this.canvas.addEventListener('mousedown', this.handleMouseDown.bind(this));
this.canvas.addEventListener('mousemove', this.handleMouseMove.bind(this));
this.canvas.addEventListener('mouseup', this.handleMouseUp.bind(this));
}

private handleClick(event: MouseEvent) {
private handleMouseDown(event: MouseEvent) {
const rect = this.canvas.getBoundingClientRect();
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
this.dispatchEvent('click', x, y, event);

const nodes = this.getNodesAtPoint(this.sceneGraph.root, x, y);
if (nodes.length > 0) {
this.draggingShape = nodes[0] as Shape;
this.offsetX = x - this.draggingShape.x;
this.offsetY = y - this.draggingShape.y;
this.isDragging = false; // Reset dragging flag
}
}

private handleMouseMove(event: MouseEvent) {
const rect = this.canvas.getBoundingClientRect();
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
this.dispatchEvent('mousemove', x, y, event);
if (this.draggingShape) {
const rect = this.canvas.getBoundingClientRect();
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;

// Update previousBoundingBox before moving the shape
this.draggingShape.markDirty(); // Mark as dirty to ensure the previous state is saved

// Now update the position
this.draggingShape.x = x - this.offsetX;
this.draggingShape.y = y - this.offsetY;

this.isDragging = true; // Indicate that dragging is occurring
}
}

private handleMouseUp(event: MouseEvent) {
if (this.draggingShape) {
if (!this.isDragging) {
// Only fire click event if no dragging occurred
const rect = this.canvas.getBoundingClientRect();
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
this.dispatchEvent('click', x, y, event);
}
this.draggingShape = null;
}
}

private dispatchEvent(eventType: string, x: number, y: number, event: MouseEvent) {
Expand All @@ -105,7 +140,6 @@ export class Renderer {
if (eventType === 'click' && node.onClick) {
node.onClick(event);
}
// Add more event types as needed
}
}

Expand Down
9 changes: 5 additions & 4 deletions src/scene-graph/rectangle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,11 @@ export class Rectangle extends Shape {

protected calculateBoundingBox() {
this.boundingBox = {
x: this.x,
y: this.y,
width: this.width,
height: this.height,
x: this.x - this._strokeWidth / 2,
y: this.y - this._strokeWidth / 2,
width: this.width + this._strokeWidth,
height: this.height + this._strokeWidth,
};
}

}
21 changes: 19 additions & 2 deletions src/scene-graph/shape.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,15 @@ export class Shape extends Node {
protected _strokeWidth: number;
protected isDirty: boolean = true;
protected boundingBox: { x: number; y: number; width: number; height: number } | null = null;
protected previousBoundingBox: { x: number; y: number; width: number; height: number } | null = null;

constructor(fillColor: string = 'transparent', strokeColor: string = 'black', strokeWidth: number = 1) {
super();
this._fillColor = fillColor;
this._strokeColor = strokeColor;
this._strokeWidth = strokeWidth;
this.calculateBoundingBox(); // Calculate the initial bounding box
this.previousBoundingBox = { ...this.boundingBox! }; // Initialize previousBoundingBox
}

get fillColor() {
Expand Down Expand Up @@ -47,13 +50,23 @@ export class Shape extends Node {

protected triggerRerender() {
this.isDirty = true;

// Update previousBoundingBox before recalculating boundingBox
this.previousBoundingBox = { ...this.boundingBox! };

this.calculateBoundingBox(); // Recalculate bounding box
}

// Exposing this public re-render to force re-renders if needed for testing
// or if we want to extend some functionality without effecting the base triggerRerender.
public markDirty() {
this.triggerRerender(); // Public method that calls the protected method
}

protected calculateBoundingBox() {
this.boundingBox = {
x: this.x,
y: this.y,
x: this.x - this._strokeWidth / 2,
y: this.y - this._strokeWidth / 2,
width: 0,
height: 0,
};
Expand All @@ -64,6 +77,10 @@ export class Shape extends Node {
return this.boundingBox;
}

public getPreviousBoundingBox() {
return this.previousBoundingBox;
}

public isShapeDirty() {
return this.isDirty;
}
Expand Down

0 comments on commit 43a6679

Please sign in to comment.