diff --git a/src/components/Panels/ElementsPanel/ElementsPanel.vue b/src/components/Panels/ElementsPanel/ElementsPanel.vue index 8aac793ce..94d3a4d49 100644 --- a/src/components/Panels/ElementsPanel/ElementsPanel.vue +++ b/src/components/Panels/ElementsPanel/ElementsPanel.vue @@ -40,6 +40,7 @@ @mouseleave="tooltipText = 'null'" > + + @@ -123,6 +125,10 @@ :src="element.imgURL" :alt="element.name" /> +
+

{{ element.name }}

+
+ diff --git a/src/simulator/src/data/project.ts b/src/simulator/src/data/project.ts index 71d244b70..d916b94ac 100644 --- a/src/simulator/src/data/project.ts +++ b/src/simulator/src/data/project.ts @@ -120,23 +120,23 @@ function checkToSave() { * Prompt user to save data if unsaved * @category data */ -window.onbeforeunload = async function () { - if (projectSaved || embed) return +// window.onbeforeunload = async function () { +// if (projectSaved || embed) return - if (!checkToSave()) return +// if (!checkToSave()) return - alert( - 'You have unsaved changes on this page. Do you want to leave this page and discard your changes or stay on this page?' - ) - // await confirmSingleOption( - // 'You have unsaved changes on this page. Do you want to leave this page and discard your changes or stay on this page?' - // ) - const data = await generateSaveData('Untitled') - const stringData = JSON.stringify(data) - localStorage.setItem('recover', stringData) - // eslint-disable-next-line consistent-return - return 'Are u sure u want to leave? Any unsaved changes may not be recoverable' -} +// alert( +// 'You have unsaved changes on this page. Do you want to leave this page and discard your changes or stay on this page?' +// ) +// // await confirmSingleOption( +// // 'You have unsaved changes on this page. Do you want to leave this page and discard your changes or stay on this page?' +// // ) +// const data = await generateSaveData('Untitled') +// const stringData = JSON.stringify(data) +// localStorage.setItem('recover', stringData) +// // eslint-disable-next-line consistent-return +// return 'Are u sure u want to leave? Any unsaved changes may not be recoverable' +// } /** * Function to clear project @@ -155,21 +155,25 @@ export async function clearProject() { Function used to start a new project while prompting confirmation from the user */ export async function newProject(verify: boolean) { + if ( - verify || - projectSaved || - !checkToSave() || - (await confirmOption( + !verify && + (!projectSaved && checkToSave()) && + !(await confirmOption( 'What you like to start a new project? Any unsaved changes will be lost.' )) ) { - clearProject() - localStorage.removeItem('recover') - const baseUrl = window.location.origin !== 'null' ? window.location.origin : 'http://localhost:4000'; - window.location.assign(`${baseUrl}/simulatorvue/`); - - setProjectName(undefined) - projectId = generateId() - showMessage('New Project has been created!') + return } + + // Proceed to clear project and create a new one if confirmed or conditions met + await clearProject() + localStorage.removeItem('recover') + + const baseUrl = window.location.origin !== 'null' ? window.location.origin : 'http://localhost:4000' + window.location.assign(`${baseUrl}/simulatorvue/`) + + setProjectName(undefined) + projectId = generateId() + showMessage('New Project has been created!') } diff --git a/src/simulator/src/setup.ts b/src/simulator/src/setup.ts new file mode 100644 index 000000000..c138730fd --- /dev/null +++ b/src/simulator/src/setup.ts @@ -0,0 +1,178 @@ +/* eslint-disable import/no-cycle */ +/* eslint-disable no-restricted-syntax */ +/* eslint-disable guard-for-in */ +import { generateId, showMessage } from './utils'; +import { backgroundArea } from './backgroundArea'; +import plotArea from './plotArea'; +import { simulationArea } from './simulationArea'; +import { dots } from './canvasApi'; +import { update, updateSimulationSet, updateCanvasSet } from './engine'; +import { setupUI } from './ux'; +import startMainListeners from './listeners'; +import { newCircuit } from './circuit'; +import load from './data/load'; +import save from './data/save'; +import { showTourGuide } from './tutorials'; +import setupModules from './moduleSetup'; +import 'codemirror/lib/codemirror.css'; +import 'codemirror/addon/hint/show-hint.css'; +import 'codemirror/mode/javascript/javascript'; // verilog.js from codemirror is not working because array prototype is changed. +import 'codemirror/addon/edit/closebrackets'; +import 'codemirror/addon/hint/anyword-hint'; +import 'codemirror/addon/hint/show-hint'; +import { setupCodeMirrorEnvironment } from './Verilog2CV'; +import '../vendor/jquery-ui.min.css'; +import '../vendor/jquery-ui.min'; +import { confirmSingleOption } from '#/components/helpers/confirmComponent/ConfirmComponent.vue'; +import { getToken } from '#/pages/simulatorHandler.vue'; + +/** + * to resize window and setup things it + * sets up new width for the canvas variables. + * Also redraws the grid. + * @category setup + */ +export function resetup(): void { + const DPR = window.devicePixelRatio || 1; + if (lightMode) { + DPR = 1; + } + const width = document.getElementById('simulationArea')?.clientWidth! * DPR; + let height; + if (!embed) { + height = + (document.body.clientHeight - + document.getElementById('toolbar')?.clientHeight!) * + DPR; + } else { + height = document.getElementById('simulation')?.clientHeight! * DPR; + } + // setup simulationArea and backgroundArea variables used to make changes to canvas. + backgroundArea.setup(); + simulationArea.setup(); + // redraw grid + dots(); + document.getElementById('backgroundArea')!.style.height = + height / DPR + 100 + 'px'; + document.getElementById('backgroundArea')!.style.width = + width / DPR + 100 + 'px'; + document.getElementById('canvasArea')!.style.height = height / DPR + 'px'; + simulationArea.canvas.width = width; + simulationArea.canvas.height = height; + backgroundArea.canvas.width = width + 100 * DPR; + backgroundArea.canvas.height = height + 100 * DPR; + if (!embed) { + plotArea.setup(); + } + updateCanvasSet(true); + update(); // INEFFICIENT, needs to be deprecated + simulationArea.prevScale = 0; + dots(); +} + +window.onresize = resetup; // listener +window.onorientationchange = resetup; // listener + +// for mobiles +window.addEventListener('orientationchange', resetup); // listener + +/** + * function to setup environment variables like projectId and DPR + * @category setup + */ +function setupEnvironment(): void { + setupModules(); + const projectId = generateId(); + (window as any).projectId = projectId; + updateSimulationSet(true); + newCircuit('Main'); + (window as any).data = {}; + resetup(); + setupCodeMirrorEnvironment(); +} + +/** + * Fetches project data from API and loads it into the simulator. + * @param {number} projectId The ID of the project to fetch data for + * @category setup + */ +async function fetchProjectData(projectId: number): Promise { + try { + const response = await fetch( + `/api/v1/projects/${projectId}/circuit_data`, + { + method: 'GET', + headers: { + Accept: 'application/json', + Authorization: `Token ${getToken('cvt')}`, + }, + } + ); + if (response.ok) { + const data = await response.json(); + await load(data); + await simulationArea.changeClockTime(data.timePeriod || 500); + ($('.loadingIcon') as any).fadeOut(); + } else { + throw new Error('API call failed'); + } + } catch (error) { + console.error(error); + confirmSingleOption('Error: Could not load.'); + ($('.loadingIcon') as any).fadeOut(); + } +} + +/** + * Load project data immediately when available. + * Improvement to eliminate delay caused by setTimeout in previous implementation revert if issues arise. + * @category setup + */ +async function loadProjectData(): Promise { + (window as any).logixProjectId = (window as any).logixProjectId ?? 0; + if ((window as any).logixProjectId !== 0) { + ($('.loadingIcon') as any).fadeIn(); + await fetchProjectData((window as any).logixProjectId); + } else if (localStorage.getItem('recover_login') && (window as any).isUserLoggedIn) { + // Restore unsaved data and save + const data = JSON.parse(localStorage.getItem('recover_login')!); + await load(data); + localStorage.removeItem('recover'); + localStorage.removeItem('recover_login'); + await save(); + } else if (localStorage.getItem('recover')) { + // Restore unsaved data which didn't get saved due to error + showMessage( + "We have detected that you did not save your last work. Don't worry we have recovered them. Access them using Project->Recover" + ); + } +} + +/** + * Show tour guide if it hasn't been completed yet. + * The tour is shown after a delay of 2 seconds. + * @category setup + */ +function showTour(): void { + if (!localStorage.tutorials_tour_done && !embed) { + setTimeout(() => { + showTourGuide(); + }, 2000); + } +} + +/** + * The first function to be called to setup the whole simulator. + * This function sets up the simulator environment, the UI, the listeners, + * loads the project data, and shows the tour guide. + * @category setup + */ +export function setup(): void { + setupEnvironment(); + if (!embed) { + setupUI(); + startMainListeners(); + } + loadProjectData(); + showTour(); +} diff --git a/src/simulator/src/tutorials.ts b/src/simulator/src/tutorials.ts new file mode 100644 index 000000000..88781ca55 --- /dev/null +++ b/src/simulator/src/tutorials.ts @@ -0,0 +1,149 @@ +import Driver from 'driver.js'; + +interface Popover { + className?: string; + title: string; + description: string; + position: 'right' | 'left' | 'bottom' | 'top'; + offset?: number; +} + +interface TourStep { + element: string; + className?: string; + popover: Popover; +} + +export const tour: TourStep[] = [ + { + element: '#guide_1', + className: 'guide_1', + popover: { + className: '', + title: 'Circuit Elements panel', + description: + 'This is where you can find all the circuit elements that are offered to build amazing circuits.', + position: 'right', + offset: 160, + }, + }, + { + element: '.guide_2', + popover: { + title: 'Properties Panel', + description: + 'This panel lets you change element properties as they are selected. When no elements are selected, the panel displays project properties.', + position: 'left', + offset: 200, + }, + }, + { + element: '.quick-btn', + popover: { + title: 'Quick Access Panel', + description: + 'This movable panel offers to perform some actions like Save Online, Open, Download quickly. Hover over the icons and see for yourself', + position: 'bottom', + }, + }, + { + element: '#tabsBar', + popover: { + title: 'Circuit Tabs', + description: + 'This section displays all the circuits you have in your project. You can easily add and delete circuits.', + position: 'bottom', + offset: 250, + }, + }, + { + element: '.timing-diagram-panel', + popover: { + title: 'Timing Diagram Panel (Waveform)', + description: + 'This panel displays the waveform created by circuits and can be used for resolving race conditions and debugging circuits.', + position: 'bottom', + offset: 0, + }, + }, + { + element: '.report-sidebar a', + popover: { + className: 'bug-guide', + title: 'Report System', + description: + 'You can report any issues/bugs you face through this issue reporting button there and then quickly.', + position: 'left', + offset: -105, + }, + }, + { + element: '.tour-help', + popover: { + className: 'tourHelpStep', + title: 'Restart tutorial anytime', + description: + 'You can restart this tutorial anytime by clicking on "Tutorial Guide" under this dropdown.', + position: 'right', + offset: 0, + }, + }, + { + element: '.testbench-manual-panel', + popover: { + title: 'Test Bench Panel', + description: 'This panel helps you test your circuit correctness by observing how your circuit responds under different test cases, ensuring a thorough and effective validation process.', + position: 'right', + offset: 0, + }, + }, +]; + +// Not used currently +export const tutorialWrapper = (): void => { + const panelHighlight = new Driver(); + document.querySelector('.panelHeader')?.addEventListener( + 'click', + (e: MouseEvent) => { + if (localStorage.getItem('tutorials') === 'next') { + panelHighlight.highlight({ + element: '#guide_1', + showButtons: false, + popover: { + title: 'Here are the elements', + description: + 'Select any element by clicking on it & then click anywhere on the grid to place the element.', + position: 'right', + offset: + (e.target as HTMLElement).nextElementSibling?.clientHeight! + + (e.target as HTMLElement).offsetTop - + 45, + }, + }); + localStorage.setItem('tutorials', 'done'); + } + }, + { + once: true, + } + ); + document.querySelector('.icon')?.addEventListener('click', () => { + panelHighlight.reset(true); + }); +}; + +const animatedTourDriver = new Driver({ + animate: true, + opacity: 0.8, + padding: 5, + showButtons: true, +}); + +export function showTourGuide(): void { + document.querySelector('.draggable-panel .maximize')?.dispatchEvent(new Event('click')); + animatedTourDriver.defineSteps(tour); + animatedTourDriver.start(); + localStorage.setItem('tutorials_tour_done', 'true'); +} + +export default showTourGuide;