Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 98 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# Changelog

All notable changes to the Digital Circuit Simulator project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

---

## [Unreleased] - 2026-01-09

### πŸŽ‰ Added

#### Undo/Redo Functionality
- **Undo/Redo Support** - Full undo/redo functionality with keyboard shortcuts
- `Ctrl+Z` (or `Cmd+Z` on Mac): Undo last action
- `Ctrl+Y` (or `Cmd+Y` on Mac): Redo action
- `Ctrl+Shift+Z`: Alternative redo shortcut
- Maintains history of up to 50 steps
- Smart history tracking that captures all node and edge changes including:
- Adding/removing nodes and wires
- Moving nodes and repositioning components
- Connecting/disconnecting wires
- All circuit modifications
- **History Panel** - Added popup to see previous 50 steps and go back to desired precise state back.
- Descriptive action names instead of generic step numbers:
- "Added AND", "Deleted OR", "Moved Input A"
- "Created branch node", "Connected wires", "Disconnected wires"
- "Imported Half Adder", "Renamed to Output Y"

#### Circuit Management Features
- **Dynamic Circuit Routes** - Clean URL structure for sharing and accessing circuits
- `/circuit` - Create new empty circuit
- `/circuit/{id}` - Direct link to specific saved circuit
- Automatic verification and loading of circuit by ID
- Better sharing with clean URLs instead of query parameters
- **Custom labels** - Added option for users to custom rename the inputs,outputs and logic gates
- **Right Click Option** - Added right click circuit option to
- "Rename"
- "Copy"
- "Duplicate" - with 50px offset positioning
- "Delete" - Moved Delete to here
- **Import Circuits** - Users can import their own ciruits back to new circuit in one block.
- **Dynamic Gate Sizing** - Gate components automatically resize vertically based on input/output count
- **Variable Input Count for Logic Gates** - Logic gates (AND, OR, NAND, NOR, XOR, XNOR) can now have 2-8 inputs via right-click context menu
- **Marquee selection** - Holding control for marquee selection for copy paste

#### Wire Management & Branch Points
- **Branch Point Creation** - Double-click on any wire to create a branch point for signal splitting or create a branch node
- **Wire Selection** - Wires can now be selected by clicking directly on them and deleted with delete key

#### Dashboard
- **Right click on categories or circuits**
- "Rename"
- "Delete"
- "Duplicate" (only for circuits not categories)
#### View Controls
- **Fullscreen Mode Toggle** - Added fullscreen mode button to maximize workspace area


### πŸ› Fixed

#### UI/UX Fixes
- **Toolbar Dropdown Toggle** - Logic Gates and other category dropdowns can now be closed by clicking the category name again

#### Technical Fixes
- **TypeScript Interface Extension** - Added `isImported` and `importedCircuitId` properties to `GateType` interface to prevent compilation errors
- **Source Handle DOM Warning** - Fixed "non-boolean attribute `truth`" warning in Source component by destructuring custom props before spreading to Handle
- Matches pattern used in Target component
- Prevents React from passing custom props to DOM
- **Null Safety in Circuit Simulation** - Added safety checks for `node.data.outputs` and `node.data.inputs` to prevent runtime errors when processing branch nodes
- **React Key Warnings in Modals** - Fixed duplicate key errors across all modal components
- SaveCircuitModal: Added proper key and conditional wrapper
- CircuitLibrary: Moved modals outside AnimatePresence, wrapped in fragment
- ImportCircuitModal: Added key and conditional rendering
- RenameModal: Added key and conditional rendering
- InputCountModal: Added key and conditional rendering
- ConfirmationModal: Added key and conditional rendering

### πŸ”„ Changed

#### Interaction Model
- **Panning & Selection Behavior** - Refined canvas interaction model
- Normal left-drag: Pan background
- Control + drag on empty space: Box selection (marquee)
- Control + drag on branch node: Reposition branch point
- Middle/right mouse: Alternative panning methods

---

## πŸ‘¨β€πŸ’» Author

**Neeraj** ([@NeerajCodz](https://github.com/NeerajCodz))

---

## πŸ“ Notes

This changelog documents all changes made during the January 9, 2026 development session. All features have been tested.
File renamed without changes.
86 changes: 52 additions & 34 deletions app/api/circuits/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export async function GET(
try {
const { id } = await params
const user = await getServerUser()

if (!user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
Expand Down Expand Up @@ -51,7 +51,7 @@ export async function PUT(
try {
const { id } = await params
const user = await getServerUser()

if (!user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
Expand All @@ -74,42 +74,48 @@ export async function PUT(
// Update circuit with transaction to handle categories and labels
const circuit = await prisma.$transaction(async (tx: any) => {
// Update circuit
const updateData: any = {
name,
description,
circuit_data,
is_public,
}

const updatedCircuit = await tx.circuit.update({
where: { id },
data: {
name,
description,
circuit_data,
is_public
}
data: updateData
})

// Update categories
await tx.circuitCategory.deleteMany({
where: { circuit_id: id }
})

if (category_ids.length > 0) {
await tx.circuitCategory.createMany({
data: category_ids.map((category_id: string) => ({
circuit_id: id,
category_id
}))
// Only update categories if provided
if (body.category_ids !== undefined) {
await tx.circuitCategory.deleteMany({
where: { circuit_id: id }
})
}

// Update labels
await tx.circuitLabel.deleteMany({
where: { circuit_id: id }
})
if (body.category_ids.length > 0) {
await tx.circuitCategory.createMany({
data: body.category_ids.map((category_id: string) => ({
circuit_id: id,
category_id
}))
})
}
}

if (label_ids.length > 0) {
await tx.circuitLabel.createMany({
data: label_ids.map((label_id: string) => ({
circuit_id: id,
label_id
}))
// Only update labels if provided
if (body.label_ids !== undefined) {
await tx.circuitLabel.deleteMany({
where: { circuit_id: id }
})

if (body.label_ids.length > 0) {
await tx.circuitLabel.createMany({
data: body.label_ids.map((label_id: string) => ({
circuit_id: id,
label_id
}))
})
}
}

return updatedCircuit
Expand All @@ -129,13 +135,13 @@ export async function PATCH(
try {
const { id } = await params
const user = await getServerUser()

if (!user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}

const body = await request.json()
const { category_ids, label_ids } = body
const { category_ids, label_ids, name, description } = body

// Check if circuit exists and belongs to user
const existingCircuit = await prisma.circuit.findFirst({
Expand All @@ -149,8 +155,20 @@ export async function PATCH(
return NextResponse.json({ error: 'Circuit not found' }, { status: 404 })
}

// Update only categories and/or labels
// Update circuit fields, categories and/or labels
await prisma.$transaction(async (tx: any) => {
// Update name and/or description if provided
if (name !== undefined || description !== undefined) {
const updateData: any = {}
if (name !== undefined) updateData.name = name
if (description !== undefined) updateData.description = description

await tx.circuit.update({
where: { id },
data: updateData
})
}

if (category_ids !== undefined) {
// Update categories
await tx.circuitCategory.deleteMany({
Expand Down Expand Up @@ -215,7 +233,7 @@ export async function DELETE(
try {
const { id } = await params
const user = await getServerUser()

if (!user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
Expand Down
24 changes: 24 additions & 0 deletions app/circuit/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"use client";
import React from 'react';
import { useParams } from 'next/navigation';
import { useUser } from '@clerk/nextjs';
import Loader from '@/components/Loader';
import CircuitPage from '../page';

export default function CircuitWithId() {
const params = useParams();
const { isLoaded, user } = useUser();
const circuitId = params.id as string;

if (!isLoaded) {
return (
<div className="min-h-screen bg-white dark:bg-[#111111] flex items-center justify-center">
<Loader />
</div>
);
}

// If user is not logged in, they'll be handled inside CircuitPage or they can view public circuits if implemented
// For now, we just pass the ID down
return <CircuitPage initialCircuitId={circuitId} />;
}
8 changes: 5 additions & 3 deletions app/circuit/components/handles/source.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,16 @@ import {Handle, Position} from 'reactflow';


const Source = (props: any) => {
const { truth, style, ...restProps } = props;

return (
<Handle {...props} type="source" position={Position.Right} style={{
<Handle {...restProps} type="source" position={Position.Right} style={{
height: '15px',
width: '15px',
background: props.truth? 'red':'gray',
background: truth ? 'red':'gray',
borderWidth: '2px',
borderColor: 'black',
...props.style ?? {}
...style ?? {}
}}
></Handle>
);
Expand Down
8 changes: 5 additions & 3 deletions app/circuit/components/handles/target.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,16 @@ const Target = (props: any) => {

}, [edges, props.id]);

const { truth, style, ...restProps } = props;

return (
<Handle {...props} isConnectable={isHandleConnectable} type="target" position={Position.Left} style={{
<Handle {...restProps} isConnectable={isHandleConnectable} type="target" position={Position.Left} style={{
height: '15px',
width: '15px',
background: props.truth? 'red':'gray',
background: truth ? 'red' : 'gray',
borderWidth: '2px',
borderColor: 'black',
...props.style ?? {}
...style ?? {}
}}
></Handle>
);
Expand Down
52 changes: 52 additions & 0 deletions app/circuit/components/nodes/branch.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import React from "react";
import { NodeProps } from "reactflow";
import { Handle, Position } from "reactflow";

function Branch(props: NodeProps) {
const { data, id } = props;

return (
<div
className="relative w-3 h-3 rounded-full bg-emerald-400 border-2 border-emerald-600 shadow-lg cursor-pointer hover:scale-125 transition-transform"
onContextMenu={(e) => data?.onContextMenu?.(e)}
onDoubleClick={(e) => data?.onDoubleClick?.(e)}
title="Branch Point - Double-click edge to create, Delete to remove"
>
{/* Input handle - receives signal */}
<Handle
type="target"
position={Position.Left}
id={`${id}-i`}
style={{
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
background: "transparent",
border: "none",
width: "12px",
height: "12px",
}}
/>

{/* Output handle - sends signal */}
<Handle
type="source"
position={Position.Right}
id={`${id}-o`}
style={{
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
background: "transparent",
border: "none",
width: "12px",
height: "12px",
}}
/>
</div>
);
}

export default Branch;
Loading