mirror of
https://github.com/sotam0316/drawNET.git
synced 2026-04-25 03:58:37 +09:00
Initial commit: drawNET Alpha v1.0 - Professional Topology Designer with Full i18n and Performance Optimizations
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
import { initSystemEvents } from './system.js';
|
||||
import { initViewportEvents } from './viewport.js';
|
||||
import { initSelectionEvents } from './selection.js';
|
||||
import { initNodeEvents } from './nodes.js';
|
||||
import { initRoutingEvents } from './routing.js';
|
||||
|
||||
export { updateGridBackground } from './system.js';
|
||||
|
||||
/**
|
||||
* initGraphEvents - Orchestrates the initialization of all specialized event modules.
|
||||
*/
|
||||
export function initGraphEvents() {
|
||||
initSystemEvents();
|
||||
initViewportEvents();
|
||||
initSelectionEvents();
|
||||
initNodeEvents();
|
||||
initRoutingEvents();
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import { state } from '../../state.js';
|
||||
import { generateRackUnitsPath } from '../styles/utils.js';
|
||||
|
||||
export function initNodeEvents() {
|
||||
if (!state.graph) return;
|
||||
|
||||
// Hover Guides for specific objects
|
||||
state.graph.on('node:mouseenter', ({ node }) => {
|
||||
if (node.shape === 'drawnet-dot') {
|
||||
node.attr('hit/stroke', 'rgba(59, 130, 246, 0.4)');
|
||||
node.attr('hit/strokeDasharray', '2,2');
|
||||
}
|
||||
});
|
||||
|
||||
state.graph.on('node:mouseleave', ({ node }) => {
|
||||
if (node.shape === 'drawnet-dot') {
|
||||
node.attr('hit/stroke', 'transparent');
|
||||
node.attr('hit/strokeDasharray', '0');
|
||||
}
|
||||
});
|
||||
|
||||
// Rack Drill Down Interaction
|
||||
state.graph.on('node:dblclick', ({ node }) => {
|
||||
const data = node.getData() || {};
|
||||
if (data.layerId !== state.activeLayerId) return; // Guard: Block drills into other layers
|
||||
|
||||
if (node.shape === 'drawnet-rack') {
|
||||
state.graph.zoomToCell(node, {
|
||||
padding: { top: 60, bottom: 60, left: 100, right: 100 },
|
||||
animation: { duration: 600 }
|
||||
});
|
||||
|
||||
state.graph.getCells().forEach(cell => {
|
||||
if (cell.id !== node.id && !node.getDescendants().some(d => d.id === cell.id)) {
|
||||
cell.setAttrs({ body: { opacity: 0.1 }, image: { opacity: 0.1 }, label: { opacity: 0.1 }, line: { opacity: 0.1 } });
|
||||
} else {
|
||||
cell.setAttrs({ body: { opacity: 1 }, image: { opacity: 1 }, label: { opacity: 1 }, line: { opacity: 1 } });
|
||||
}
|
||||
});
|
||||
|
||||
showBackToTopBtn();
|
||||
}
|
||||
});
|
||||
|
||||
// Parent/Structural Changes
|
||||
state.graph.on('node:change:parent', ({ node, current }) => {
|
||||
const parentId = current || null;
|
||||
const currentData = node.getData() || {};
|
||||
node.setData({ ...currentData, parent: parentId });
|
||||
|
||||
import('../layers.js').then(m => m.applyLayerFilters());
|
||||
import('/static/js/modules/properties_sidebar/index.js').then(m => m.renderProperties());
|
||||
import('/static/js/modules/persistence.js').then(m => m.markDirty());
|
||||
});
|
||||
|
||||
// Data Syncing (Rack-specific)
|
||||
state.graph.on('node:change:data', ({ node, current }) => {
|
||||
if (node.shape === 'drawnet-rack') {
|
||||
const slots = current.slots || 42;
|
||||
const bbox = node.getBBox();
|
||||
node.attr('units/d', generateRackUnitsPath(bbox.height, slots));
|
||||
|
||||
if (!current.label || current.label.startsWith('Rack (')) {
|
||||
node.attr('label/text', `Rack (${slots}U)`);
|
||||
}
|
||||
}
|
||||
|
||||
// Rich Card Content Refresh on Data Change
|
||||
if (node.shape === 'drawnet-rich-card') {
|
||||
import('../styles/utils.js').then(m => m.renderRichContent(node));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function showBackToTopBtn() {
|
||||
let btn = document.getElementById('rack-back-btn');
|
||||
if (btn) btn.remove();
|
||||
|
||||
btn = document.createElement('div');
|
||||
btn.id = 'rack-back-btn';
|
||||
btn.innerHTML = '<i class="fas fa-arrow-left"></i> Back to Topology';
|
||||
btn.style.cssText = `
|
||||
position: absolute; top: 100px; left: 300px; z-index: 1000;
|
||||
padding: 10px 20px; background: #3b82f6; color: white;
|
||||
border-radius: 8px; cursor: pointer; font-weight: bold;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.2); transition: all 0.2s;
|
||||
`;
|
||||
|
||||
btn.onclick = () => {
|
||||
state.graph.zoomToFit({ padding: 100, animation: { duration: 500 } });
|
||||
state.graph.getCells().forEach(cell => {
|
||||
cell.setAttrs({ body: { opacity: 1 }, image: { opacity: 1 }, label: { opacity: 1 }, line: { opacity: 1 } });
|
||||
});
|
||||
btn.remove();
|
||||
};
|
||||
|
||||
document.getElementById('graph-container').parentElement.appendChild(btn);
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import { state } from '../../state.js';
|
||||
import { logger } from '../../utils/logger.js';
|
||||
|
||||
export function initRoutingEvents() {
|
||||
if (!state.graph) return;
|
||||
|
||||
// Re-evaluate routing when nodes are moved to handle adaptive fallback (Phase 1.18)
|
||||
state.graph.on('node:change:position', ({ node }) => {
|
||||
const edges = state.graph.getConnectedEdges(node);
|
||||
if (edges.length === 0) return;
|
||||
|
||||
import('/static/js/modules/graph/styles.js').then(({ getX6EdgeConfig }) => {
|
||||
edges.forEach(edge => {
|
||||
const data = edge.getData() || {};
|
||||
const config = getX6EdgeConfig({
|
||||
source: edge.getSource().cell,
|
||||
target: edge.getTarget().cell,
|
||||
...data
|
||||
});
|
||||
|
||||
if (config.router !== edge.getRouter()) {
|
||||
edge.setRouter(config.router || null);
|
||||
logger.info(`drawNET: Dynamic Routing Update for ${edge.id}`);
|
||||
}
|
||||
if (config.connector !== edge.getConnector()) {
|
||||
edge.setConnector(config.connector);
|
||||
}
|
||||
// Sync physical connections during move (especially for manual anchors)
|
||||
if (config.source) edge.setSource(config.source);
|
||||
if (config.target) edge.setTarget(config.target);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Unify styling when an edge is newly connected via mouse (Phase 1.19)
|
||||
state.graph.on('edge:connected', ({ edge }) => {
|
||||
if (!edge || !edge.isEdge()) return;
|
||||
|
||||
import('/static/js/modules/graph/styles.js').then(({ getX6EdgeConfig }) => {
|
||||
const data = edge.getData() || {};
|
||||
const config = getX6EdgeConfig({
|
||||
source: edge.getSource().cell,
|
||||
target: edge.getTarget().cell,
|
||||
...data
|
||||
});
|
||||
|
||||
// Apply official style & configuration
|
||||
if (config.source) edge.setSource(config.source);
|
||||
if (config.target) edge.setTarget(config.target);
|
||||
edge.setRouter(config.router || null);
|
||||
edge.setConnector(config.connector);
|
||||
edge.setZIndex(config.zIndex || 5);
|
||||
|
||||
if (config.attrs) {
|
||||
edge.setAttrs(config.attrs);
|
||||
}
|
||||
logger.info(`drawNET: Unified Style applied to new edge ${edge.id}`);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import { state } from '../../state.js';
|
||||
|
||||
export function initSelectionEvents() {
|
||||
if (!state.graph) return;
|
||||
|
||||
state.graph.on('cell:selected', ({ cell }) => {
|
||||
if (cell.isNode()) {
|
||||
if (!state.selectionOrder.includes(cell.id)) {
|
||||
state.selectionOrder.push(cell.id);
|
||||
}
|
||||
} else if (cell.isEdge()) {
|
||||
// Add vertices tool for manual routing control (unless locked)
|
||||
if (!cell.getData()?.locked) {
|
||||
cell.addTools([{ name: 'vertices' }]);
|
||||
}
|
||||
|
||||
// ADD S/T labels for clear identification of Start and Target
|
||||
const currentLabels = cell.getLabels() || [];
|
||||
cell.setLabels([
|
||||
...currentLabels,
|
||||
{ id: 'selection-source-label', position: { distance: 0.05 }, attrs: { text: { text: 'S', fill: '#10b981', fontSize: 10, fontWeight: 'bold' }, rect: { fill: '#ffffff', stroke: '#10b981', strokeWidth: 1, rx: 2, ry: 2 } } },
|
||||
{ id: 'selection-target-label', position: { distance: 0.95 }, attrs: { text: { text: 'T', fill: '#ef4444', fontSize: 10, fontWeight: 'bold' }, rect: { fill: '#ffffff', stroke: '#ef4444', strokeWidth: 1, rx: 2, ry: 2 } } }
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
state.graph.on('cell:unselected', ({ cell }) => {
|
||||
const isLocked = cell.getData()?.locked;
|
||||
|
||||
if (cell.isEdge()) {
|
||||
if (!isLocked) {
|
||||
cell.removeTools();
|
||||
} else {
|
||||
// Keep boundary if locked, but remove selection-only tools if any
|
||||
cell.removeTools(['vertices']);
|
||||
}
|
||||
|
||||
// Remove selection labels safely
|
||||
const labels = cell.getLabels() || [];
|
||||
const filteredLabels = labels.filter(l =>
|
||||
l.id !== 'selection-source-label' && l.id !== 'selection-target-label'
|
||||
);
|
||||
cell.setLabels(filteredLabels);
|
||||
} else if (cell.isNode()) {
|
||||
if (!isLocked) {
|
||||
cell.removeTools();
|
||||
}
|
||||
}
|
||||
state.selectionOrder = state.selectionOrder.filter(id => id !== cell.id);
|
||||
});
|
||||
|
||||
state.graph.on('blank:click', () => {
|
||||
state.selectionOrder = [];
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
import { state } from '../../state.js';
|
||||
import { updateGraphTheme } from '../styles.js';
|
||||
import { DEFAULTS } from '../../constants.js';
|
||||
|
||||
export function initSystemEvents() {
|
||||
if (!state.graph) return;
|
||||
|
||||
// Theme changes
|
||||
window.addEventListener('themeChanged', () => {
|
||||
updateGraphTheme();
|
||||
const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
|
||||
state.graph.drawBackground({ color: isDark ? '#1e293b' : '#f8fafc' });
|
||||
});
|
||||
|
||||
// Background color changes (from settings)
|
||||
window.addEventListener('backgroundChanged', (e) => {
|
||||
if (state.graph) {
|
||||
state.graph.drawBackground({ color: e.detail.color });
|
||||
}
|
||||
});
|
||||
|
||||
// Canvas resize event
|
||||
window.addEventListener('canvasResize', (e) => {
|
||||
const { width, height } = e.detail;
|
||||
state.canvasSize = { width, height };
|
||||
const container = document.getElementById('graph-container');
|
||||
|
||||
if (width === '100%') {
|
||||
container.style.width = '100%';
|
||||
container.style.height = '100%';
|
||||
container.style.boxShadow = 'none';
|
||||
container.style.margin = '0';
|
||||
state.graph.resize();
|
||||
} else {
|
||||
container.style.width = `${width}px`;
|
||||
container.style.height = `${height}px`;
|
||||
container.style.boxShadow = '0 10px 30px rgba(0,0,0,0.1)';
|
||||
container.style.margin = '40px auto';
|
||||
container.style.backgroundColor = '#fff';
|
||||
state.graph.resize(width, height);
|
||||
}
|
||||
});
|
||||
|
||||
// Grid type change
|
||||
window.addEventListener('gridChanged', (e) => {
|
||||
state.gridStyle = e.detail.style;
|
||||
if (!state.graph) return;
|
||||
|
||||
if (e.detail.style === 'none') {
|
||||
state.graph.hideGrid();
|
||||
state.isSnapEnabled = false;
|
||||
} else {
|
||||
state.graph.showGrid();
|
||||
state.isSnapEnabled = true;
|
||||
|
||||
if (e.detail.showMajor) {
|
||||
state.graph.drawGrid({
|
||||
type: 'doubleMesh',
|
||||
args: [
|
||||
{
|
||||
color: e.detail.majorColor || DEFAULTS.MAJOR_GRID_COLOR,
|
||||
thickness: (e.detail.thickness || 1) * 1.5,
|
||||
factor: e.detail.majorInterval || DEFAULTS.MAJOR_GRID_INTERVAL
|
||||
},
|
||||
{
|
||||
color: e.detail.color || DEFAULTS.GRID_COLOR,
|
||||
thickness: e.detail.thickness || 1
|
||||
},
|
||||
],
|
||||
});
|
||||
} else {
|
||||
const gridType = e.detail.style === 'solid' ? 'mesh' : (e.detail.style === 'dashed' ? 'doubleMesh' : 'dot');
|
||||
state.graph.drawGrid({
|
||||
type: gridType,
|
||||
args: [
|
||||
{
|
||||
color: e.detail.color || DEFAULTS.GRID_COLOR,
|
||||
thickness: e.detail.thickness || 1
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
state.graph.setGridSize(state.gridSpacing || DEFAULTS.GRID_SPACING);
|
||||
}
|
||||
});
|
||||
|
||||
// Grid spacing change
|
||||
window.addEventListener('gridSpacingChanged', (e) => {
|
||||
state.gridSpacing = e.detail.spacing;
|
||||
if (state.graph) {
|
||||
state.graph.setGridSize(state.gridSpacing);
|
||||
}
|
||||
});
|
||||
|
||||
// Global Data Sanitizer & Enforcer (Ensures all objects have latest schema defaults)
|
||||
// This handles all creation paths: Drag-and-Drop, Copy-Paste, Grouping, and programatic addEdge.
|
||||
state.graph.on('cell:added', ({ cell }) => {
|
||||
const data = cell.getData() || {};
|
||||
let updated = false;
|
||||
|
||||
// 1. Common Fields for all Node/Edge objects
|
||||
if (data.description === undefined) { data.description = ""; updated = true; }
|
||||
if (data.asset_tag === undefined) { data.asset_tag = ""; updated = true; }
|
||||
if (!data.tags) { data.tags = []; updated = true; }
|
||||
if (data.locked === undefined) { data.locked = false; updated = true; }
|
||||
|
||||
// 2. Ensure label_pos exists to prevent display reset
|
||||
if (data.label_pos === undefined) {
|
||||
data.label_pos = data.is_group ? 'top' : (['rect', 'circle', 'rounded-rect', 'text-box', 'label'].includes((data.type || '').toLowerCase()) ? 'center' : 'bottom');
|
||||
updated = true;
|
||||
}
|
||||
|
||||
// 3. Edge Specific Defaults
|
||||
if (cell.isEdge()) {
|
||||
if (data.routing_offset === undefined) { data.routing_offset = 20; updated = true; }
|
||||
}
|
||||
|
||||
if (updated) {
|
||||
// Use silent: true to prevent triggering another 'cell:added' if someone is listening to data changes
|
||||
cell.setData(data, { silent: true });
|
||||
}
|
||||
|
||||
// Keep existing layer logic
|
||||
import('../layers.js').then(m => m.applyLayerFilters());
|
||||
});
|
||||
}
|
||||
|
||||
export function updateGridBackground() {
|
||||
if (!state.graph) return;
|
||||
state.graph.setGridSize(state.gridSpacing || DEFAULTS.GRID_SPACING);
|
||||
state.graph.showGrid();
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import { state } from '../../state.js';
|
||||
|
||||
let isRightDragging = false;
|
||||
let startX = 0;
|
||||
let startY = 0;
|
||||
const threshold = 5;
|
||||
|
||||
/**
|
||||
* Handle mousedown on the graph to initialize right-click panning.
|
||||
*/
|
||||
const handleGraphMouseDown = ({ e }) => {
|
||||
if (e.button === 2) { // Right Click
|
||||
isRightDragging = false;
|
||||
startX = e.clientX;
|
||||
startY = e.clientY;
|
||||
state.isRightDragging = false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Global event handler for mousemove to support panning.
|
||||
* Registered only once on document level.
|
||||
*/
|
||||
const onMouseMove = (e) => {
|
||||
if (e.buttons === 2 && state.graph) {
|
||||
const dx = e.clientX - startX;
|
||||
const dy = e.clientY - startY;
|
||||
|
||||
// Detect dragging start (beyond threshold)
|
||||
if (!isRightDragging && (Math.abs(dx) > threshold || Math.abs(dy) > threshold)) {
|
||||
isRightDragging = true;
|
||||
state.isRightDragging = true;
|
||||
// Sync start positions to current mouse position to avoid the initial jump
|
||||
startX = e.clientX;
|
||||
startY = e.clientY;
|
||||
return;
|
||||
}
|
||||
|
||||
if (isRightDragging) {
|
||||
state.graph.translateBy(dx, dy);
|
||||
startX = e.clientX;
|
||||
startY = e.clientY;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Global event handler for mouseup to finish panning.
|
||||
* Registered only once on document level.
|
||||
*/
|
||||
const onMouseUp = () => {
|
||||
if (isRightDragging) {
|
||||
// Reset after a short delay to allow contextmenu event to check the flag
|
||||
setTimeout(() => {
|
||||
state.isRightDragging = false;
|
||||
isRightDragging = false;
|
||||
}, 100);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes viewport-related events for the current graph instance.
|
||||
* Should be called whenever a new graph is created.
|
||||
*/
|
||||
export function initViewportEvents() {
|
||||
if (!state.graph) return;
|
||||
|
||||
// Attach to the specific graph instance, ensuring no duplicates
|
||||
state.graph.off('blank:mousedown', handleGraphMouseDown);
|
||||
state.graph.on('blank:mousedown', handleGraphMouseDown);
|
||||
state.graph.off('cell:mousedown', handleGraphMouseDown);
|
||||
state.graph.on('cell:mousedown', handleGraphMouseDown);
|
||||
|
||||
// Register document-level listeners only once per page session
|
||||
if (!window._drawNetViewportEventsInitialized) {
|
||||
document.addEventListener('mousemove', onMouseMove);
|
||||
document.addEventListener('mouseup', onMouseUp);
|
||||
window._drawNetViewportEventsInitialized = true;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user