Initial commit: drawNET Alpha v1.0 - Professional Topology Designer with Full i18n and Performance Optimizations

This commit is contained in:
leeyj
2026-03-22 22:37:24 +09:00
commit 5cea93e317
192 changed files with 14449 additions and 0 deletions
@@ -0,0 +1,88 @@
import { state } from '../../state.js';
import { logger } from '../../utils/logger.js';
/**
* handleEdgeUpdate - Synchronizes property sidebar changes to the X6 Edge cell.
* Ensures data, attributes (markers, colors), anchors, and routers are all updated.
*/
export async function handleEdgeUpdate(cell, data) {
if (cell.isNode()) return;
const updates = {
id: cell.id,
source: cell.getSource().cell,
target: cell.getTarget().cell,
source_port: cell.getSource().port,
target_port: cell.getTarget().port,
// Priority: DOM Element > Provided Data (Format Painter) > Default/Old Data
color: document.getElementById('prop-color')?.value || data?.color,
style: document.getElementById('prop-style')?.value || data?.style,
routing: document.getElementById('prop-routing')?.value || data?.routing,
source_anchor: document.getElementById('prop-src-anchor')?.value || data?.source_anchor || 'orth',
target_anchor: document.getElementById('prop-dst-anchor')?.value || data?.target_anchor || 'orth',
direction: document.getElementById('prop-direction')?.value || data?.direction || 'none',
is_tunnel: document.getElementById('prop-is-tunnel') ? !!document.getElementById('prop-is-tunnel').checked : !!data?.is_tunnel,
width: document.getElementById('prop-width') ? parseFloat(document.getElementById('prop-width').value) : (parseFloat(data?.width) || 2),
routing_offset: document.getElementById('prop-routing-offset') ? parseInt(document.getElementById('prop-routing-offset').value) : (parseInt(data?.routing_offset) || 20),
description: document.getElementById('prop-description') ? document.getElementById('prop-description').value : data?.description,
tags: document.getElementById('prop-tags') ? document.getElementById('prop-tags').value.split(',').map(t => t.trim()).filter(t => t) : (data?.tags || []),
label: document.getElementById('prop-label') ? document.getElementById('prop-label').value : data?.label,
locked: document.getElementById('prop-locked') ? !!document.getElementById('prop-locked').checked : !!data?.locked
};
// 1. Update Cell Data (Single Source of Truth)
const currentData = cell.getData() || {};
cell.setData({ ...currentData, ...updates });
// 2. Sync Visual Attributes (Arrows, Colors, Styles)
// We reuse the central mapping logic from styles.js for consistency
const { getX6EdgeConfig } = await import('/static/js/modules/graph/styles.js');
const edgeConfig = getX6EdgeConfig(updates);
// Apply Attributes (Markers, Stroke, Dasharray)
// Note: setAttrs merges by default, but we use it to apply the calculated style object
if (edgeConfig.attrs) {
cell.setAttrs(edgeConfig.attrs);
}
// Apply Labels
if (edgeConfig.labels) {
cell.setLabels(edgeConfig.labels);
}
// 3. Anchor & Router Updates (Physical Pathing)
if (edgeConfig.source) cell.setSource(edgeConfig.source);
if (edgeConfig.target) cell.setTarget(edgeConfig.target);
// Apply Router and Connector (Physical Pathing)
// Clear everything first to prevent old state persistence
cell.setVertices([]);
cell.setRouter(null);
cell.setConnector('normal');
if (edgeConfig.router) {
cell.setRouter(edgeConfig.router);
}
if (edgeConfig.connector) {
cell.setConnector(edgeConfig.connector);
}
// 4. Mark Project as Dirty and Apply Locking/Selection Tools
cell.setProp('movable', !updates.locked);
cell.setProp('deletable', !updates.locked);
// Selection Tool Preservation: If currently selected, ensure vertices are visible
const isSelected = state.graph.isSelected(cell);
if (updates.locked) {
cell.addTools([{ name: 'boundary', args: { attrs: { stroke: '#ef4444', 'stroke-dasharray': '5,5' } } }]);
} else {
cell.removeTools();
if (isSelected) {
cell.addTools(['vertices']);
}
}
import('/static/js/modules/persistence.js').then(m => m.markDirty());
}
@@ -0,0 +1,136 @@
import { state } from '../../state.js';
import { logger } from '../../utils/logger.js';
export async function handleNodeUpdate(cell, data) {
const isNode = cell.isNode();
if (!isNode) return;
const updates = {
// Priority: DOM Element > Provided Data (Format Painter) > Default/Old Data
type: document.getElementById('prop-type')?.value || data?.type,
label: document.getElementById('prop-label') ? document.getElementById('prop-label').value : data?.label,
is_group: document.getElementById('prop-is-group') ? !!document.getElementById('prop-is-group').checked : !!data?.is_group,
padding: document.getElementById('prop-padding') ? parseInt(document.getElementById('prop-padding').value) : (data?.padding || undefined),
slots: document.getElementById('prop-slots') ? parseInt(document.getElementById('prop-slots').value) : (data?.slots || undefined),
color: document.getElementById('prop-label-color')?.value || data?.color,
fill: document.getElementById('prop-fill-color')?.value || data?.fill,
border: document.getElementById('prop-border-color')?.value || data?.border,
label_pos: document.getElementById('prop-label-pos')?.value || data?.label_pos,
// PM Standard Attributes
vendor: document.getElementById('prop-vendor') ? document.getElementById('prop-vendor').value : data?.vendor,
model: document.getElementById('prop-model') ? document.getElementById('prop-model').value : data?.model,
ip: document.getElementById('prop-ip') ? document.getElementById('prop-ip').value : data?.ip,
status: document.getElementById('prop-status')?.value || data?.status,
project: document.getElementById('prop-project') ? document.getElementById('prop-project').value : data?.project,
env: document.getElementById('prop-env')?.value || data?.env,
asset_tag: document.getElementById('prop-asset-tag') ? document.getElementById('prop-asset-tag').value : data?.asset_tag,
tags: document.getElementById('prop-tags') ? document.getElementById('prop-tags').value.split(',').map(t => t.trim()).filter(t => t) : (data?.tags || []),
description: document.getElementById('prop-description') ? document.getElementById('prop-description').value : data?.description,
locked: document.getElementById('prop-locked') ? !!document.getElementById('prop-locked').checked : !!data?.locked
};
const pureLabel = updates.label || '';
cell.attr('label/text', pureLabel);
if (updates.color) cell.attr('label/fill', updates.color);
// Apply Fill and Border to body (for primitives and groups)
if (updates.fill) {
cell.attr('body/fill', updates.fill);
if (updates.is_group) updates.background = updates.fill; // Sync for group logic
}
if (updates.border) {
cell.attr('body/stroke', updates.border);
if (updates.is_group) updates['border-color'] = updates.border; // Sync for group logic
}
if (updates.label_pos) {
const { getLabelAttributes } = await import('/static/js/modules/graph/styles.js');
const labelAttrs = getLabelAttributes(updates.label_pos, updates.type, updates.is_group);
Object.keys(labelAttrs).forEach(key => {
cell.attr(`label/${key}`, labelAttrs[key]);
});
}
// Handle group membership via selection (parentId is now a UUID)
const parentId = document.getElementById('prop-parent')?.value;
const currentParent = cell.getParent();
// Case 1: Parent added or changed
if (parentId && (!currentParent || currentParent.id !== parentId)) {
const parent = state.graph.getCellById(parentId);
if (parent) {
parent.addChild(cell);
updates.parent = parentId;
saveUpdates();
return;
}
}
// Case 2: Parent removed (Ungrouping)
if (!parentId && currentParent) {
// 부모 해제 시 경고 설정 확인
const triggerUngroup = async () => {
const { getSettings } = await import('../../settings/store.js');
const settings = getSettings();
if (settings.confirmUngroup !== false) {
const { showConfirmModal } = await import('../../ui/utils.js');
const { t } = await import('../../i18n.js');
showConfirmModal({
title: t('confirm_ungroup_title') || 'Disconnect from Group',
message: t('confirm_ungroup_msg') || 'Are you sure you want to remove this object from its parent group?',
showDontAskAgain: true,
checkboxKey: 'confirmUngroup',
onConfirm: () => {
currentParent.removeChild(cell);
updates.parent = null;
saveUpdates();
},
onCancel: () => {
import('../index.js').then(m => m.renderProperties());
}
});
} else {
currentParent.removeChild(cell);
updates.parent = null;
saveUpdates();
}
};
triggerUngroup();
return;
}
// Case 3: Parent unchanged or no parent (Regular update)
saveUpdates();
function saveUpdates() {
// 최종 데이터 병합 및 저장 (id, layerId 등 기존 메타데이터 보존)
const currentData = cell.getData() || {};
const finalData = { ...currentData, ...updates };
cell.setData(finalData);
// [Instance #2245] Recursive Propagation:
// If this is a group, "poke" all descendants to trigger their change events,
// ensuring the sidebar for any selected child reflects the new parent name.
if (updates.is_group) {
const descendants = cell.getDescendants();
descendants.forEach(child => {
const childData = child.getData() || {};
const syncTime = Date.now();
// We add a timestamp to child data to force X6 to fire 'change:data'
child.setData({ ...childData, _parent_updated: syncTime }, { silent: false });
});
}
// Apply Locking Constraints
cell.setProp('movable', !updates.locked);
cell.setProp('deletable', !updates.locked);
cell.setProp('rotatable', !updates.locked);
if (updates.locked) {
cell.addTools([{ name: 'boundary', args: { attrs: { stroke: '#ef4444', 'stroke-dasharray': '5,5' } } }]);
} else {
cell.removeTools();
}
}
}
@@ -0,0 +1,50 @@
import { state } from '../../state.js';
import { renderRichContent } from '../../graph/styles/utils.js';
export async function handleRichCardUpdate(cell, data) {
const isNode = cell.isNode();
if (!isNode) return;
const updates = {
headerText: document.getElementById('prop-header-text')?.value,
headerColor: document.getElementById('prop-header-color')?.value,
headerAlign: document.getElementById('prop-header-align')?.value || 'left',
cardType: document.getElementById('prop-card-type')?.value,
contentAlign: document.getElementById('prop-content-align')?.value || 'left',
content: document.getElementById('prop-card-content')?.value,
locked: !!document.getElementById('prop-locked')?.checked
};
// 1. Sync to X6 Attributes
cell.attr('headerLabel/text', updates.headerText);
cell.attr('header/fill', updates.headerColor);
cell.attr('body/stroke', updates.headerColor);
// Apply header alignment
const anchor = updates.headerAlign === 'center' ? 'middle' : (updates.headerAlign === 'right' ? 'end' : 'start');
const x = updates.headerAlign === 'center' ? 0.5 : (updates.headerAlign === 'right' ? '100%' : 10);
const x2 = updates.headerAlign === 'right' ? -10 : 0;
cell.attr('headerLabel/textAnchor', anchor);
cell.attr('headerLabel/refX', x);
cell.attr('headerLabel/refX2', x2);
// 2. Render Content via HTML inside ForeignObject
renderRichContent(cell, updates);
// 3. Update Cell Data
const currentData = cell.getData() || {};
cell.setData({ ...currentData, ...updates });
// 4. Handle Lock State
cell.setProp('movable', !updates.locked);
cell.setProp('deletable', !updates.locked);
if (updates.locked) {
cell.addTools([{ name: 'boundary', args: { attrs: { stroke: '#ef4444', 'stroke-dasharray': '5,5' } } }]);
} else {
cell.removeTools();
}
import('/static/js/modules/persistence.js').then(m => m.markDirty());
}