/**
* @fileoverview JSCircuit Editor - Application Entry Point
* @description
* This is the main application bootstrap file demonstrating how to initialize and
* configure the JSCircuit Editor using the hexagonal architecture API.
*
* **Architecture Example:**
* This file serves as a practical example of how the different architectural layers
* work together in the hexagonal architecture pattern:
*
* 1. **Domain Layer**: Circuit aggregate and domain entities
* 2. **Application Layer**: CircuitService for use case orchestration
* 3. **GUI Layer**: GUIAdapter as the primary adapter for user interactions
* 4. **Infrastructure Layer**: Renderers and configuration adapters
*
* **Extension Points Demonstrated:**
* - Element registration through ElementRegistry
* - Renderer registration through RendererFactory
* - Command registration through GUICommandRegistry
* - Event-driven architecture through CircuitService
*
* **Performance Optimizations:**
* - HiDPI canvas setup for crisp rendering on high-resolution displays
* - Resize observer pattern for efficient canvas management
* - Performance monitoring for development debugging
*
* @example
* // To extend with a new element type:
* ElementRegistry.register('CustomElement', (id, nodes, label, properties) => {
* return new CustomElement(id, nodes, label, properties);
* });
*
* // To add a new renderer:
* rendererFactory.register('custom', CustomRenderer);
*
* // To add a new command:
* GUICommandRegistry.register('customAction', (services) => {
* return new CustomCommand(services.circuitService, services.circuitRenderer);
* });
*
* @module main
* @requires Circuit - Domain aggregate
* @requires CircuitService - Application service
* @requires GUIAdapter - Primary GUI adapter
* @requires ElementRegistry - Element factory registry
* @requires RendererFactory - Renderer factory
* @requires GUICommandRegistry - Command registry
*/
import { Circuit } from "../domain/aggregates/Circuit.js";
import { CircuitService } from "../application/CircuitService.js";
import { GUIAdapter } from "./adapters/GUIAdapter.js";
import {
ElementRegistry,
rendererFactory,
GUICommandRegistry,
setupCommands
} from "../config/registry.js";
import { initMenu } from "./menu/initMenu.js";
import { Logger } from "../utils/Logger.js";
import { globalPerformanceMonitor } from "../utils/PerformanceUtils.js";
/**
* HiDPI Canvas Utilities
*
* These utilities demonstrate how to properly handle high-resolution displays
* in a canvas-based application, ensuring crisp rendering across all devices.
*/
/**
* Fits canvas to HiDPI display once during initialization.
*
* @param {HTMLCanvasElement} canvas - The canvas element to configure
*/
function fitCanvasHiDPIOnce(canvas){
const dpr = window.devicePixelRatio || 1;
const rect = canvas.getBoundingClientRect(); // CSS size
const cssW = Math.max(1, Math.round(rect.width));
const cssH = Math.max(1, Math.round(rect.height));
canvas.width = Math.round(cssW * dpr);
canvas.height = Math.round(cssH * dpr);
canvas.getContext('2d').setTransform(dpr, 0, 0, dpr, 0, 0);
}
/**
* Sets up responsive HiDPI canvas with automatic resizing.
*
* @param {HTMLCanvasElement} canvas - The canvas element to configure
* @param {Function} onResize - Callback function to execute after resize
* @returns {Function} Cleanup function to remove observers
*/
function setupHiDPICanvas(canvas, onResize) {
const ctx = canvas.getContext('2d');
const resize = () => {
const dpr = window.devicePixelRatio || 1;
const rect = canvas.getBoundingClientRect();
const cssW = Math.max(1, Math.round(rect.width));
const cssH = Math.max(1, Math.round(rect.height));
const pxW = Math.round(cssW * dpr);
const pxH = Math.round(cssH * dpr);
if (canvas.width !== pxW || canvas.height !== pxH) {
canvas.width = pxW; canvas.height = pxH;
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
onResize?.();
}
};
const ro = new ResizeObserver(resize);
ro.observe(canvas);
window.addEventListener('resize', resize, { passive: true });
resize(); // initial (will be no-op because we already pre-fit once)
return () => { ro.disconnect(); window.removeEventListener('resize', resize); };
}
/**
* Application Bootstrap - Hexagonal Architecture Initialization
*
* This section demonstrates the proper initialization sequence for a hexagonal
* architecture application with dependency injection and event-driven design.
*/
// DOM element references
const stage = document.querySelector('.circuit-stage');
const canvas = document.getElementById("circuitCanvas");
const controls = document.querySelector(".controls"); // optional
// Pre-configure canvas for HiDPI before any rendering
fitCanvasHiDPIOnce(canvas);
/**
* Domain and Application Layer Initialization
*
* Following hexagonal architecture, we initialize from the inside-out:
* 1. Domain aggregate (Circuit)
* 2. Application service (CircuitService)
* 3. Primary adapter (GUIAdapter)
*/
const circuit = new Circuit(); // Domain: Aggregate root
const circuitService = new CircuitService( // Application: Use case orchestration
circuit,
ElementRegistry
);
const guiAdapter = new GUIAdapter( // GUI: Primary adapter
controls,
canvas,
circuitService,
ElementRegistry,
rendererFactory,
GUICommandRegistry
);
/* ---------- Menu (emits ui:action only) ---------- */
initMenu();
/* ---------- Commands, first render, reveal, THEN start resize observer ---------- */
globalPerformanceMonitor.startTiming('app-initialization');
setupCommands(circuitService, guiAdapter.circuitRenderer);
guiAdapter.initialize(); // this will call first render
setupHiDPICanvas(canvas, () => {
guiAdapter.circuitRenderer.reCenter(); // Re-center after canvas resize
guiAdapter.circuitRenderer.centerScrollPosition(); // Center scroll position after resize
guiAdapter.circuitRenderer.render();
});
// Center the coordinates and scroll position after the HiDPI canvas is set up
guiAdapter.circuitRenderer.reCenter();
guiAdapter.circuitRenderer.centerScrollPosition();
stage.classList.add('ready'); // fade in only after crisp render
const initTime = globalPerformanceMonitor.endTiming('app-initialization');
// Add performance monitoring for development
if (Logger.isDev) {
// Monitor for slow operations
const originalRender = guiAdapter.circuitRenderer.render;
let renderCount = 0;
guiAdapter.circuitRenderer.render = function() {
renderCount++;
const start = performance.now();
const result = originalRender.apply(this, arguments);
const duration = performance.now() - start;
if (duration > 16) { // Slower than 60fps
Logger.warn(`Slow render #${renderCount}: ${duration.toFixed(2)}ms`);
}
return result;
};
}
/* ---------- Documentation Integration: PostMessage Support ---------- */
/**
* Listen for postMessage commands from parent documentation pages.
* This enables the documentation to load example circuits automatically.
*/
window.addEventListener('message', function(event) {
// Security: Only accept messages from same origin or trusted documentation
if (event.origin !== window.location.origin &&
!event.origin.includes('github.io') &&
!event.origin.includes('localhost')) {
return;
}
const data = event.data;
if (data.type === 'loadCircuit' && data.netlist) {
try {
Logger.info('[Documentation] Loading example circuit from documentation');
// Get the openNetlist command and use its internal methods
const openCommand = guiCommandRegistry.get('openNetlist');
if (openCommand) {
// Store current state for undo
openCommand.previousState = circuitService.exportState();
// Parse and load the netlist content directly
openCommand._parseNetlistContent(data.netlist).then(elements => {
return openCommand._loadElementsIntoCircuit(elements);
}).then(() => {
// Auto-zoom to fit the loaded circuit after a short delay
setTimeout(() => {
guiAdapter.circuitRenderer.zoomToFit();
guiAdapter.circuitRenderer.render();
}, 500);
Logger.info('[Documentation] Example circuit loaded successfully');
}).catch(error => {
Logger.error('[Documentation] Failed to load example circuit:', error);
});
} else {
Logger.error('[Documentation] OpenNetlist command not available');
}
} catch (error) {
Logger.error('[Documentation] Failed to load example circuit:', error);
}
}
});
// Notify parent that the application is ready to receive commands
window.parent.postMessage({ type: 'appReady' }, '*');