mirror of
https://github.com/sotam0316/drawNET_test.git
synced 2026-04-25 03:58:38 +09:00
static 폴더 및 하위 파일 업로드
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
export function getEdgeConfig(data) {
|
||||
let routingType = data.routing || 'manhattan';
|
||||
if (routingType === 'u-shape') routingType = 'manhattan-u';
|
||||
|
||||
const srcAnchor = (data.routing === 'u-shape') ? 'top' : (data.source_anchor || 'orth');
|
||||
const dstAnchor = (data.routing === 'u-shape') ? 'top' : (data.target_anchor || 'orth');
|
||||
|
||||
const routerArgs = {
|
||||
step: 5, // Finer step for better pathfinding
|
||||
padding: 10, // Reduced padding to avoid "unreachable" errors in tight spaces
|
||||
};
|
||||
const directions = ['top', 'bottom', 'left', 'right'];
|
||||
|
||||
if (directions.includes(srcAnchor)) routerArgs.startDirections = [srcAnchor];
|
||||
if (directions.includes(dstAnchor)) routerArgs.endDirections = [dstAnchor];
|
||||
|
||||
// For U-shape, we need a bit more padding but not too much to fail
|
||||
if (data.routing === 'u-shape') routerArgs.padding = 15;
|
||||
|
||||
const routerConfigs = {
|
||||
manhattan: { name: 'manhattan', args: routerArgs },
|
||||
'manhattan-u': { name: 'manhattan', args: routerArgs },
|
||||
orthogonal: { name: 'orth' },
|
||||
straight: null,
|
||||
metro: { name: 'metro' }
|
||||
};
|
||||
|
||||
return {
|
||||
id: data.id || `e_${data.source}_${data.target}`,
|
||||
source: { cell: data.source, anchor: srcAnchor, connectionPoint: 'boundary' },
|
||||
target: { cell: data.target, anchor: dstAnchor, connectionPoint: 'boundary' },
|
||||
router: routerConfigs[routingType] || routerConfigs.manhattan,
|
||||
connector: { name: 'jumpover', args: { type: 'arc', size: 5, radius: 12 } },
|
||||
attrs: {
|
||||
line: {
|
||||
stroke: data.is_tunnel ? (data.color || '#ef4444') : (data.color || '#94a3b8'),
|
||||
strokeWidth: data.is_tunnel ? 2 : (data.width || 2),
|
||||
strokeDasharray: data.flow ? '5,5' : (data.is_tunnel ? '6,3' : (data.style === 'dashed' ? '5,5' : '0')),
|
||||
targetMarker: (data.direction === 'forward' || data.direction === 'both') ? 'block' : null,
|
||||
sourceMarker: (data.direction === 'backward' || data.direction === 'both') ? 'block' : null,
|
||||
class: data.flow ? 'flow-animation' : '',
|
||||
}
|
||||
},
|
||||
labels: data.is_tunnel ? [{
|
||||
attrs: {
|
||||
text: { text: 'VPN', fill: '#ef4444', fontSize: 10, fontWeight: 'bold' },
|
||||
rect: { fill: '#ffffff', rx: 4, ry: 4 }
|
||||
}
|
||||
}] : [],
|
||||
data: data
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
import { state } from '../../../state.js';
|
||||
import { logger } from '../../../utils/logger.js';
|
||||
import { calculateCellZIndex } from '../../layers.js';
|
||||
|
||||
/**
|
||||
* getX6EdgeConfig - Maps edge data to X6 connecting properties
|
||||
*/
|
||||
export function getX6EdgeConfig(data) {
|
||||
if (!data || !data.source || !data.target) {
|
||||
return {
|
||||
source: data?.source,
|
||||
target: data?.target,
|
||||
router: null,
|
||||
attrs: {
|
||||
line: {
|
||||
stroke: '#94a3b8',
|
||||
strokeWidth: 2,
|
||||
targetMarker: 'block'
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
// 1. Resolve Routing Type (Explicit choice > Adaptive)
|
||||
let routingType = data.routing || 'manhattan';
|
||||
if (routingType === 'u-shape') routingType = 'manhattan-u';
|
||||
|
||||
// Adaptive Routing: Only force straight if NO explicit choice was made or if it's currently manhattan
|
||||
if (state.graph && (routingType === 'manhattan' || !data.routing)) {
|
||||
const srcNode = state.graph.getCellById(data.source);
|
||||
const dstNode = state.graph.getCellById(data.target);
|
||||
|
||||
if (srcNode && dstNode && srcNode.isNode() && dstNode.isNode()) {
|
||||
const b1 = srcNode.getBBox();
|
||||
const b2 = dstNode.getBBox();
|
||||
const p1 = { x: b1.x + b1.width / 2, y: b1.y + b1.height / 2 };
|
||||
const p2 = { x: b2.x + b2.width / 2, y: b2.y + b2.height / 2 };
|
||||
const dist = Math.sqrt(Math.pow(p1.x - p2.x, 2) + Math.pow(p1.y - p2.y, 2));
|
||||
|
||||
if (dist < 100) {
|
||||
routingType = 'straight';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const isStraight = (routingType === 'straight');
|
||||
|
||||
// 2. Resolve Anchors & Router Args
|
||||
const srcAnchor = (data.routing === 'u-shape') ? 'top' : (data.source_anchor || 'orth');
|
||||
const dstAnchor = (data.routing === 'u-shape') ? 'top' : (data.target_anchor || 'orth');
|
||||
|
||||
// Manhattan Router Args (Enhanced for topological diagrams)
|
||||
const routerArgs = {
|
||||
padding: 10,
|
||||
step: 5, // [TWEAK] Increase precision (10 -> 5) to reduce redundant bends
|
||||
offset: data.routing_offset || 20,
|
||||
exclude(cell) {
|
||||
return cell && typeof cell.getData === 'function' && cell.getData()?.is_group;
|
||||
}
|
||||
};
|
||||
|
||||
// Sync Manhattan directions with manual anchors to force the router to respect the side
|
||||
const validSides = ['top', 'bottom', 'left', 'right'];
|
||||
if (srcAnchor && validSides.includes(srcAnchor)) {
|
||||
routerArgs.startDirections = [srcAnchor];
|
||||
}
|
||||
if (dstAnchor && validSides.includes(dstAnchor)) {
|
||||
routerArgs.endDirections = [dstAnchor];
|
||||
}
|
||||
|
||||
if (routingType === 'manhattan-u') {
|
||||
routerArgs.startDirections = ['top'];
|
||||
routerArgs.endDirections = ['top'];
|
||||
}
|
||||
|
||||
// 3. Intelligent Port Matching (Shortest Path)
|
||||
// If ports are not explicitly provided (e.g. via Auto-connect Shift+A),
|
||||
// we find the closest port pair to ensure the most direct connection.
|
||||
let resolvedSourcePort = data.source_port;
|
||||
let resolvedTargetPort = data.target_port;
|
||||
|
||||
if (state.graph && !resolvedSourcePort && !resolvedTargetPort && srcAnchor === 'orth' && dstAnchor === 'orth') {
|
||||
const srcNode = state.graph.getCellById(data.source);
|
||||
const dstNode = state.graph.getCellById(data.target);
|
||||
if (srcNode && dstNode && srcNode.isNode() && dstNode.isNode()) {
|
||||
const bestPorts = findClosestPorts(srcNode, dstNode);
|
||||
if (bestPorts) {
|
||||
resolvedSourcePort = bestPorts.source;
|
||||
resolvedTargetPort = bestPorts.target;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const routerConfigs = {
|
||||
manhattan: { name: 'manhattan', args: routerArgs },
|
||||
'manhattan-u': { name: 'manhattan', args: routerArgs },
|
||||
orthogonal: { name: 'orth' },
|
||||
straight: null,
|
||||
metro: { name: 'metro' }
|
||||
};
|
||||
|
||||
const isStraightFinal = (routingType === 'straight');
|
||||
const direction = data.direction || 'none';
|
||||
|
||||
// Resolve source/target configuration
|
||||
// Rule: If a port is explicitly provided AND the anchor is set to 'orth' (Auto),
|
||||
// we keep the port. If a manual anchor (top, left, etc.) is set, we clear the port.
|
||||
const sourceConfig = {
|
||||
cell: data.source,
|
||||
connectionPoint: { name: 'boundary', args: { padding: 6 } }
|
||||
};
|
||||
if (resolvedSourcePort && srcAnchor === 'orth') {
|
||||
sourceConfig.port = resolvedSourcePort;
|
||||
} else {
|
||||
// [FIX] In Straight mode, directional anchors (top, left etc) force a bend.
|
||||
// We use 'center' but rely on 'boundary' connection point for a perfect straight diagonal.
|
||||
sourceConfig.anchor = isStraight ? { name: 'center' } : { name: srcAnchor };
|
||||
}
|
||||
|
||||
const targetConfig = {
|
||||
cell: data.target,
|
||||
connectionPoint: { name: 'boundary', args: { padding: 6 } }
|
||||
};
|
||||
if (resolvedTargetPort && dstAnchor === 'orth') {
|
||||
targetConfig.port = resolvedTargetPort;
|
||||
} else {
|
||||
targetConfig.anchor = isStraight ? { name: 'center' } : { name: dstAnchor };
|
||||
}
|
||||
|
||||
const config = {
|
||||
id: data.id || `e_${data.source}_${data.target}`,
|
||||
source: sourceConfig,
|
||||
target: targetConfig,
|
||||
router: isStraight ? null : (routerConfigs[routingType] || routerConfigs.manhattan),
|
||||
connector: isStraight ? { name: 'normal' } : { name: 'jumpover', args: { type: 'arc', size: 6 } },
|
||||
attrs: {
|
||||
line: {
|
||||
stroke: data.is_tunnel ? (data.color || '#ef4444') : (data.color || '#94a3b8'),
|
||||
strokeWidth: data.is_tunnel ? 2 : (data.width || (data.style === 'double' ? 4 : 2)),
|
||||
strokeDasharray: data.flow ? '5,5' : (data.is_tunnel ? '6,3' : (data.style === 'dashed' ? '5,5' : (data.style === 'dotted' ? '2,2' : '0'))),
|
||||
targetMarker: (direction === 'forward' || direction === 'both') ? 'block' : null,
|
||||
sourceMarker: (direction === 'backward' || direction === 'both') ? 'block' : null,
|
||||
class: data.flow ? 'flow-animation' : '',
|
||||
}
|
||||
},
|
||||
labels: (() => {
|
||||
const lbls = [];
|
||||
if (data.label) {
|
||||
lbls.push({
|
||||
attrs: {
|
||||
text: { text: data.label, fill: data.color || '#64748b', fontSize: 11, fontWeight: '600' },
|
||||
rect: { fill: 'rgba(255,255,255,0.8)', rx: 4, ry: 4, strokeWidth: 0 }
|
||||
}
|
||||
});
|
||||
}
|
||||
if (data.is_tunnel) {
|
||||
lbls.push({
|
||||
attrs: {
|
||||
text: { text: 'VPN', fill: '#ef4444', fontSize: 10, fontWeight: 'bold' },
|
||||
rect: { fill: '#ffffff', rx: 3, ry: 3, stroke: '#ef4444', strokeWidth: 1 }
|
||||
},
|
||||
position: { distance: 0.25 }
|
||||
});
|
||||
}
|
||||
return lbls;
|
||||
})(),
|
||||
zIndex: data.zIndex || calculateCellZIndex(data, 0, false), // Edges default to 30 (Above groups)
|
||||
movable: !data.locked,
|
||||
deletable: !data.locked,
|
||||
data: data
|
||||
};
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* findClosestPorts - Finds the pair of ports with the minimum distance between two nodes.
|
||||
*/
|
||||
function findClosestPorts(srcNode, dstNode) {
|
||||
const srcPorts = srcNode.getPorts();
|
||||
const dstPorts = dstNode.getPorts();
|
||||
|
||||
if (srcPorts.length === 0 || dstPorts.length === 0) return null;
|
||||
|
||||
let minParams = null;
|
||||
let minDistance = Infinity;
|
||||
|
||||
srcPorts.forEach(sp => {
|
||||
dstPorts.forEach(dp => {
|
||||
const p1 = srcNode.getPortProp(sp.id, 'args/point') || { x: 0, y: 0 };
|
||||
const p2 = dstNode.getPortProp(dp.id, 'args/point') || { x: 0, y: 0 };
|
||||
|
||||
// Convert to absolute coordinates
|
||||
const pos1 = srcNode.getPosition();
|
||||
const pos2 = dstNode.getPosition();
|
||||
const abs1 = { x: pos1.x + p1.x, y: pos1.y + p1.y };
|
||||
const abs2 = { x: pos2.x + p2.x, y: pos2.y + p2.y };
|
||||
|
||||
const dist = Math.sqrt(Math.pow(abs1.x - abs2.x, 2) + Math.pow(abs1.y - abs2.y, 2));
|
||||
if (dist < minDistance) {
|
||||
minDistance = dist;
|
||||
minParams = { source: sp.id, target: dp.id };
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return minParams;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export { getX6NodeConfig } from './node.js';
|
||||
export { getX6EdgeConfig } from './edge.js';
|
||||
@@ -0,0 +1,138 @@
|
||||
import { state } from '../../../state.js';
|
||||
import { shapeRegistry } from '../registry.js';
|
||||
import { getLabelAttributes, generateRackUnitsPath } from '../utils.js';
|
||||
import { DEFAULTS } from '../../../constants.js';
|
||||
import { calculateCellZIndex } from '../../layers.js';
|
||||
|
||||
/**
|
||||
* getX6NodeConfig - Maps data properties to X6 node attributes using ShapeRegistry
|
||||
*/
|
||||
export function getX6NodeConfig(data) {
|
||||
const nodeType = (data.type || '').toLowerCase();
|
||||
|
||||
// 1. Find the plugin in the registry
|
||||
let plugin = shapeRegistry.get(nodeType);
|
||||
|
||||
// Fallback for generic types if not found by direct name
|
||||
if (!plugin) {
|
||||
if (nodeType === 'rectangle') plugin = shapeRegistry.get('rect');
|
||||
else if (nodeType === 'blank') plugin = shapeRegistry.get('dummy');
|
||||
}
|
||||
|
||||
// Default to 'default' plugin if still not found
|
||||
if (!plugin) plugin = shapeRegistry.get('default');
|
||||
|
||||
// 2. Resolve asset path (Skip for groups and primitives!)
|
||||
const primitives = ['rect', 'circle', 'ellipse', 'rounded-rect', 'text-box', 'label', 'dot', 'dummy', 'rack', 'triangle', 'diamond', 'parallelogram', 'cylinder', 'document', 'manual-input', 'table', 'user', 'admin', 'hourglass', 'polyline'];
|
||||
let assetPath = `/static/assets/${DEFAULTS.DEFAULT_ICON}`;
|
||||
|
||||
if (data.is_group || primitives.includes(nodeType)) {
|
||||
assetPath = undefined;
|
||||
} else if (plugin.shape === 'drawnet-node' || plugin.type === 'default' || !plugin.icon) {
|
||||
const currentPath = data.assetPath || data.asset_path;
|
||||
if (currentPath) {
|
||||
assetPath = currentPath.startsWith('/static/assets/') ? currentPath : `/static/assets/${currentPath}`;
|
||||
} else {
|
||||
const matchedAsset = state.assetsData?.find(a => {
|
||||
const searchType = (a.type || '').toLowerCase();
|
||||
const searchId = (a.id || '').toLowerCase();
|
||||
return nodeType === searchType || nodeType === searchId ||
|
||||
nodeType.includes(searchType) || nodeType.includes(searchId);
|
||||
});
|
||||
|
||||
if (matchedAsset) {
|
||||
const iconFileName = matchedAsset.path || (matchedAsset.views && matchedAsset.views.icon) || DEFAULTS.DEFAULT_ICON;
|
||||
assetPath = `/static/assets/${iconFileName}`;
|
||||
data.assetPath = iconFileName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Build Base Configuration
|
||||
let config = {
|
||||
id: data.id,
|
||||
shape: plugin.shape,
|
||||
position: data.pos ? { x: data.pos.x, y: data.pos.y } : { x: 100, y: 100 },
|
||||
zIndex: data.zIndex || calculateCellZIndex(data, 0, true),
|
||||
movable: !data.locked,
|
||||
deletable: !data.locked,
|
||||
data: data,
|
||||
attrs: {
|
||||
label: {
|
||||
text: data.label || '',
|
||||
fill: data.color || data['text-color'] || '#64748b',
|
||||
...getLabelAttributes(data.label_pos || data['label-pos'], nodeType, data.is_group)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 4. Plugin Specific Overrides
|
||||
if (data.is_group) {
|
||||
config.attrs.body = {
|
||||
fill: data.background || '#f1f5f9',
|
||||
stroke: data['border-color'] || '#3b82f6'
|
||||
};
|
||||
} else if (plugin.shape === 'drawnet-icon' && plugin.icon) {
|
||||
config.attrs.icon = {
|
||||
text: plugin.icon,
|
||||
fill: data.color || '#64748b'
|
||||
};
|
||||
} else if (plugin.shape === 'drawnet-node') {
|
||||
config.attrs.image = { 'xlink:href': assetPath };
|
||||
}
|
||||
|
||||
// 5. Primitive Styling Persistence
|
||||
if (primitives.includes(nodeType)) {
|
||||
if (!config.attrs.body) config.attrs.body = {};
|
||||
if (data.fill) config.attrs.body.fill = data.fill;
|
||||
if (data.border) config.attrs.body.stroke = data.border;
|
||||
}
|
||||
|
||||
// Rack specific handling
|
||||
if (nodeType === 'rack') {
|
||||
const slots = data.slots || 42;
|
||||
const height = data.height || 600;
|
||||
config.data.is_group = true;
|
||||
config.attrs = {
|
||||
...config.attrs,
|
||||
units: { d: generateRackUnitsPath(height, slots) },
|
||||
label: { text: data.label || `Rack (${slots}U)` }
|
||||
};
|
||||
}
|
||||
|
||||
// Rich Card specific handling
|
||||
if (nodeType === 'rich-card' || plugin.shape === 'drawnet-rich-card') {
|
||||
config.data.headerText = data.headerText || data.label || 'RICH INFO CARD';
|
||||
config.data.content = data.content || '1. Content goes here...\n2. Support multiple lines';
|
||||
config.data.cardType = data.cardType || 'numbered';
|
||||
config.data.headerColor = data.headerColor || '#3b82f6';
|
||||
config.data.headerAlign = data.headerAlign || 'left';
|
||||
config.data.contentAlign = data.contentAlign || 'left';
|
||||
|
||||
const hAnchor = config.data.headerAlign === 'center' ? 'middle' : (config.data.headerAlign === 'right' ? 'end' : 'start');
|
||||
const hX = config.data.headerAlign === 'center' ? 0.5 : (config.data.headerAlign === 'right' ? '100%' : 10);
|
||||
const hX2 = config.data.headerAlign === 'right' ? -10 : 0;
|
||||
|
||||
config.attrs = {
|
||||
...config.attrs,
|
||||
headerLabel: {
|
||||
text: config.data.headerText,
|
||||
textAnchor: hAnchor,
|
||||
refX: hX,
|
||||
refX2: hX2
|
||||
},
|
||||
header: { fill: config.data.headerColor },
|
||||
body: { stroke: config.data.headerColor }
|
||||
};
|
||||
|
||||
// Initial render trigger (Hack: X6 might not be ready, so we defer)
|
||||
setTimeout(() => {
|
||||
const cell = state.graph.getCellById(config.id);
|
||||
if (cell) {
|
||||
import('../utils.js').then(m => m.renderRichContent(cell));
|
||||
}
|
||||
}, 50);
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
@@ -0,0 +1,338 @@
|
||||
/**
|
||||
* graph/styles/node_shapes.js - Registration of individual shape plugins
|
||||
*/
|
||||
import { shapeRegistry } from './registry.js';
|
||||
import { commonPorts } from './ports.js';
|
||||
import { generateRackUnitsPath, getLabelAttributes } from './utils.js';
|
||||
import { DEFAULTS } from '../../constants.js';
|
||||
|
||||
/**
|
||||
* Initialize all shape plugins into the registry
|
||||
*/
|
||||
export function registerNodes() {
|
||||
// 1. Default Node
|
||||
shapeRegistry.register('default', {
|
||||
shape: 'drawnet-node',
|
||||
definition: {
|
||||
inherit: 'rect',
|
||||
width: 60, height: 60,
|
||||
attrs: {
|
||||
body: { strokeWidth: 0, fill: 'transparent', rx: 4, ry: 4 },
|
||||
image: { 'xlink:href': `/static/assets/${DEFAULTS.DEFAULT_ICON || 'router.svg'}`, width: 48, height: 48, refX: 6, refY: 6 },
|
||||
label: {
|
||||
text: '', fill: '#64748b', fontSize: 12, fontFamily: 'Inter, sans-serif',
|
||||
fontWeight: 'bold', textVerticalAnchor: 'top', refX: 0.5, refY: '100%', refY2: 10,
|
||||
},
|
||||
},
|
||||
markup: [
|
||||
{ tagName: 'rect', selector: 'body' },
|
||||
{ tagName: 'image', selector: 'image' },
|
||||
{ tagName: 'text', selector: 'label' },
|
||||
],
|
||||
ports: { ...commonPorts }
|
||||
}
|
||||
});
|
||||
|
||||
// 2. Group Node
|
||||
shapeRegistry.register('group', {
|
||||
shape: 'drawnet-group',
|
||||
definition: {
|
||||
inherit: 'rect',
|
||||
width: 200, height: 200,
|
||||
attrs: {
|
||||
body: { strokeWidth: 2, stroke: '#3b82f6', fill: '#f1f5f9', fillOpacity: 0.2, rx: 8, ry: 8 },
|
||||
label: {
|
||||
text: '', fill: '#1e293b', fontSize: 14, fontFamily: 'Inter, sans-serif',
|
||||
fontWeight: 'bold', textVerticalAnchor: 'bottom', refX: 0.5, refY: -15,
|
||||
},
|
||||
},
|
||||
ports: { ...commonPorts }
|
||||
}
|
||||
});
|
||||
|
||||
// 3. Dummy/Blank Node
|
||||
shapeRegistry.register('dummy', {
|
||||
shape: 'drawnet-dummy',
|
||||
definition: {
|
||||
inherit: 'circle',
|
||||
width: 8, height: 8,
|
||||
attrs: {
|
||||
body: { strokeWidth: 1, stroke: 'rgba(148, 163, 184, 0.2)', fill: 'rgba(255, 255, 255, 0.1)' },
|
||||
},
|
||||
ports: {
|
||||
groups: { ...commonPorts.groups, center: { position: 'absolute', attrs: { circle: { r: 4, magnet: true, stroke: '#31d0c6', strokeWidth: 2, fill: '#fff' } } } },
|
||||
items: [{ id: 'center', group: 'center' }],
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 4. Dot Node
|
||||
shapeRegistry.register('dot', {
|
||||
shape: 'drawnet-dot',
|
||||
definition: {
|
||||
inherit: 'circle',
|
||||
width: 6, height: 6,
|
||||
attrs: {
|
||||
body: { strokeWidth: 1, stroke: '#64748b', fill: '#64748b' },
|
||||
},
|
||||
ports: {
|
||||
groups: { ...commonPorts.groups, center: { position: 'absolute', attrs: { circle: { r: 3, magnet: true, stroke: '#31d0c6', strokeWidth: 1, fill: '#fff' } } } },
|
||||
items: [{ id: 'center', group: 'center' }],
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 5. Basic Shapes (Primitives)
|
||||
const basicAttrs = {
|
||||
body: { strokeWidth: 2, stroke: '#94a3b8', fill: '#ffffff' },
|
||||
label: { text: '', fill: '#64748b', fontSize: 12, refX: 0.5, refY: 0.5, textAnchor: 'middle', textVerticalAnchor: 'middle' }
|
||||
};
|
||||
|
||||
shapeRegistry.register('rect', {
|
||||
shape: 'drawnet-rect',
|
||||
definition: { inherit: 'rect', width: 100, height: 60, attrs: basicAttrs, ports: commonPorts }
|
||||
});
|
||||
|
||||
shapeRegistry.register('rounded-rect', {
|
||||
shape: 'drawnet-rounded-rect',
|
||||
definition: {
|
||||
inherit: 'rect', width: 100, height: 60,
|
||||
attrs: { ...basicAttrs, body: { ...basicAttrs.body, rx: 12, ry: 12 } },
|
||||
ports: commonPorts
|
||||
}
|
||||
});
|
||||
|
||||
shapeRegistry.register('circle', {
|
||||
shape: 'drawnet-circle',
|
||||
definition: { inherit: 'circle', width: 60, height: 60, attrs: basicAttrs, ports: commonPorts }
|
||||
});
|
||||
|
||||
shapeRegistry.register('ellipse', {
|
||||
shape: 'drawnet-ellipse',
|
||||
definition: { inherit: 'ellipse', width: 80, height: 50, attrs: basicAttrs, ports: commonPorts }
|
||||
});
|
||||
|
||||
shapeRegistry.register('text-box', {
|
||||
shape: 'drawnet-text',
|
||||
definition: {
|
||||
inherit: 'rect', width: 100, height: 40,
|
||||
attrs: { body: { strokeWidth: 0, fill: 'transparent' }, label: { ...basicAttrs.label, text: 'Text', fill: '#1e293b', fontSize: 14 } }
|
||||
}
|
||||
});
|
||||
|
||||
shapeRegistry.register('label', {
|
||||
shape: 'drawnet-label',
|
||||
definition: {
|
||||
inherit: 'rect', width: 80, height: 30,
|
||||
attrs: {
|
||||
...basicAttrs,
|
||||
body: { ...basicAttrs.body, rx: 4, ry: 4, strokeWidth: 1 },
|
||||
label: { ...basicAttrs.label, fontSize: 13, fill: '#1e293b' }
|
||||
},
|
||||
ports: commonPorts
|
||||
}
|
||||
});
|
||||
|
||||
// 6. Practical Shapes
|
||||
shapeRegistry.register('browser', {
|
||||
shape: 'drawnet-browser',
|
||||
definition: {
|
||||
inherit: 'rect', width: 120, height: 80,
|
||||
markup: [
|
||||
{ tagName: 'rect', selector: 'body' },
|
||||
{ tagName: 'rect', selector: 'header' },
|
||||
{ tagName: 'circle', selector: 'btn1' },
|
||||
{ tagName: 'circle', selector: 'btn2' },
|
||||
{ tagName: 'text', selector: 'label' }
|
||||
],
|
||||
attrs: {
|
||||
body: { strokeWidth: 2, stroke: '#94a3b8', fill: '#ffffff', rx: 4, ry: 4 },
|
||||
header: { strokeWidth: 2, stroke: '#94a3b8', fill: '#f1f5f9', height: 18, refWidth: '100%', rx: 4, ry: 4 },
|
||||
btn1: { r: 3, fill: '#cbd5e1', refX: 10, refY: 9 },
|
||||
btn2: { r: 3, fill: '#cbd5e1', refX: 20, refY: 9 },
|
||||
label: { text: '', fill: '#64748b', fontSize: 12, refX: 0.5, refY: 0.6, textAnchor: 'middle', textVerticalAnchor: 'middle' }
|
||||
},
|
||||
ports: commonPorts
|
||||
}
|
||||
});
|
||||
|
||||
shapeRegistry.register('triangle', {
|
||||
shape: 'drawnet-triangle',
|
||||
definition: {
|
||||
inherit: 'polygon', width: 60, height: 60,
|
||||
attrs: {
|
||||
body: { refPoints: '0,10 5,0 10,10', strokeWidth: 2, stroke: '#94a3b8', fill: '#ffffff' },
|
||||
label: { ...basicAttrs.label, refY: 0.7 }
|
||||
},
|
||||
ports: commonPorts
|
||||
}
|
||||
});
|
||||
|
||||
shapeRegistry.register('diamond', {
|
||||
shape: 'drawnet-diamond',
|
||||
definition: {
|
||||
inherit: 'polygon', width: 80, height: 80,
|
||||
attrs: { body: { refPoints: '0,5 5,0 10,5 5,10', strokeWidth: 2, stroke: '#94a3b8', fill: '#ffffff' }, label: basicAttrs.label },
|
||||
ports: commonPorts
|
||||
}
|
||||
});
|
||||
|
||||
shapeRegistry.register('parallelogram', {
|
||||
shape: 'drawnet-parallelogram',
|
||||
definition: {
|
||||
inherit: 'polygon', width: 100, height: 60,
|
||||
attrs: { body: { refPoints: '2,0 10,0 8,10 0,10', strokeWidth: 2, stroke: '#94a3b8', fill: '#ffffff' }, label: basicAttrs.label },
|
||||
ports: commonPorts
|
||||
}
|
||||
});
|
||||
|
||||
shapeRegistry.register('cylinder', {
|
||||
shape: 'drawnet-cylinder',
|
||||
definition: {
|
||||
inherit: 'rect', width: 60, height: 80,
|
||||
markup: [
|
||||
{ tagName: 'path', selector: 'body' },
|
||||
{ tagName: 'path', selector: 'top' },
|
||||
{ tagName: 'text', selector: 'label' }
|
||||
],
|
||||
attrs: {
|
||||
body: { d: 'M 0 10 L 0 70 C 0 80 60 80 60 70 L 60 10', strokeWidth: 2, stroke: '#94a3b8', fill: '#ffffff' },
|
||||
top: { d: 'M 0 10 C 0 0 60 0 60 10 C 60 20 0 20 0 10 Z', strokeWidth: 2, stroke: '#94a3b8', fill: '#e2e8f0' },
|
||||
label: basicAttrs.label
|
||||
},
|
||||
ports: commonPorts
|
||||
}
|
||||
});
|
||||
|
||||
shapeRegistry.register('document', {
|
||||
shape: 'drawnet-document',
|
||||
definition: {
|
||||
inherit: 'rect', width: 60, height: 80,
|
||||
markup: [
|
||||
{ tagName: 'path', selector: 'body' },
|
||||
{ tagName: 'path', selector: 'fold' },
|
||||
{ tagName: 'text', selector: 'label' }
|
||||
],
|
||||
attrs: {
|
||||
body: { d: 'M 0 0 L 45 0 L 60 15 L 60 80 L 0 80 Z', strokeWidth: 2, stroke: '#94a3b8', fill: '#ffffff' },
|
||||
fold: { d: 'M 45 0 L 45 15 L 60 15', strokeWidth: 2, stroke: '#94a3b8', fill: '#e2e8f0' },
|
||||
label: basicAttrs.label
|
||||
},
|
||||
ports: commonPorts
|
||||
}
|
||||
});
|
||||
|
||||
shapeRegistry.register('manual-input', {
|
||||
shape: 'drawnet-manual-input',
|
||||
definition: {
|
||||
inherit: 'polygon', width: 100, height: 60,
|
||||
attrs: { body: { refPoints: '0,3 10,0 10,10 0,10', strokeWidth: 2, stroke: '#94a3b8', fill: '#ffffff' }, label: basicAttrs.label },
|
||||
ports: commonPorts
|
||||
}
|
||||
});
|
||||
|
||||
shapeRegistry.register('table', {
|
||||
// ... (previous table registration)
|
||||
});
|
||||
|
||||
// --- 랙(Rack) 컨테이너 플러그인 ---
|
||||
shapeRegistry.register('rack', {
|
||||
shape: 'drawnet-rack',
|
||||
definition: {
|
||||
inherit: 'rect',
|
||||
width: 160, height: 600,
|
||||
markup: [
|
||||
{ tagName: 'rect', selector: 'body' },
|
||||
{ tagName: 'rect', selector: 'header' },
|
||||
{ tagName: 'path', selector: 'units' },
|
||||
{ tagName: 'text', selector: 'label' }
|
||||
],
|
||||
attrs: {
|
||||
body: { strokeWidth: 3, stroke: '#334155', fill: '#f8fafc', rx: 4, ry: 4 },
|
||||
header: { refWidth: '100%', height: 30, fill: '#1e293b', stroke: '#334155', strokeWidth: 2, rx: 4, ry: 4 },
|
||||
units: {
|
||||
d: generateRackUnitsPath(600, 42),
|
||||
stroke: '#94a3b8', strokeWidth: 1
|
||||
},
|
||||
label: {
|
||||
text: 'Rack', fill: '#ffffff', fontSize: 14, fontWeight: 'bold',
|
||||
refX: 0.5, refY: 15, textAnchor: 'middle', textVerticalAnchor: 'middle'
|
||||
}
|
||||
},
|
||||
ports: commonPorts
|
||||
}
|
||||
});
|
||||
|
||||
// 8. Rich Text Card (Header + Listing)
|
||||
shapeRegistry.register('rich-card', {
|
||||
shape: 'drawnet-rich-card',
|
||||
definition: {
|
||||
inherit: 'rect',
|
||||
width: 280, height: 180,
|
||||
markup: [
|
||||
{ tagName: 'rect', selector: 'body' },
|
||||
{ tagName: 'rect', selector: 'header' },
|
||||
{ tagName: 'text', selector: 'headerLabel' },
|
||||
{
|
||||
tagName: 'foreignObject',
|
||||
selector: 'fo',
|
||||
children: [
|
||||
{
|
||||
tagName: 'div',
|
||||
ns: 'http://www.w3.org/1999/xhtml',
|
||||
selector: 'content',
|
||||
style: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
padding: '10px',
|
||||
fontSize: '12px',
|
||||
color: '#334155',
|
||||
overflow: 'hidden',
|
||||
boxSizing: 'border-box',
|
||||
lineHeight: '1.4'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
attrs: {
|
||||
body: { fill: '#ffffff', stroke: '#3b82f6', strokeWidth: 1, rx: 4, ry: 4 },
|
||||
header: { refWidth: '100%', height: 32, fill: '#3b82f6', rx: 4, ry: 4 },
|
||||
headerLabel: {
|
||||
text: 'TITLE', fill: '#ffffff', fontSize: 13, fontWeight: 'bold',
|
||||
refX: 10, refY: 16, textVerticalAnchor: 'middle'
|
||||
},
|
||||
fo: { refX: 0, refY: 32, refWidth: '100%', refHeight: 'calc(100% - 32)' }
|
||||
},
|
||||
ports: commonPorts
|
||||
}
|
||||
});
|
||||
|
||||
// 7. Dynamic Icon-based Plugins
|
||||
const icons = {
|
||||
'user': '\uf007', 'admin': '\uf505', 'hourglass': '\uf252',
|
||||
'table': '\uf0ce', 'tag': '\uf02b', 'polyline': '\uf201',
|
||||
'arrow-up': '\uf062', 'arrow-down': '\uf063', 'arrow-left': '\uf060', 'arrow-right': '\uf061'
|
||||
};
|
||||
|
||||
Object.keys(icons).forEach(type => {
|
||||
shapeRegistry.register(type, {
|
||||
shape: 'drawnet-icon',
|
||||
icon: icons[type],
|
||||
definition: {
|
||||
inherit: 'rect', width: 60, height: 60,
|
||||
markup: [
|
||||
{ tagName: 'rect', selector: 'body' },
|
||||
{ tagName: 'text', selector: 'icon' },
|
||||
{ tagName: 'text', selector: 'label' }
|
||||
],
|
||||
attrs: {
|
||||
body: { strokeWidth: 0, fill: 'transparent' },
|
||||
icon: { text: icons[type], fontFamily: 'FontAwesome', fontSize: 40, fill: '#64748b', refX: 0.5, refY: 0.5, textAnchor: 'middle', textVerticalAnchor: 'middle' },
|
||||
label: { text: '', fill: '#64748b', fontSize: 12, refX: 0.5, refY: '100%', refY2: 12, textAnchor: 'middle', textVerticalAnchor: 'top' }
|
||||
},
|
||||
ports: commonPorts
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* graph/styles/ports.js - Core port configurations for X6 nodes
|
||||
*/
|
||||
|
||||
export const commonPorts = {
|
||||
groups: {
|
||||
top: {
|
||||
position: 'top',
|
||||
attrs: { circle: { r: 4, magnet: true, stroke: '#31d0c6', strokeWidth: 2, fill: '#fff' } }
|
||||
},
|
||||
bottom: {
|
||||
position: 'bottom',
|
||||
attrs: { circle: { r: 4, magnet: true, stroke: '#31d0c6', strokeWidth: 2, fill: '#fff' } }
|
||||
},
|
||||
left: {
|
||||
position: 'left',
|
||||
attrs: { circle: { r: 4, magnet: true, stroke: '#31d0c6', strokeWidth: 2, fill: '#fff' } }
|
||||
},
|
||||
right: {
|
||||
position: 'right',
|
||||
attrs: { circle: { r: 4, magnet: true, stroke: '#31d0c6', strokeWidth: 2, fill: '#fff' } }
|
||||
},
|
||||
center: {
|
||||
position: 'absolute',
|
||||
attrs: { circle: { r: 4, magnet: true, stroke: '#31d0c6', strokeWidth: 2, fill: '#fff' } }
|
||||
}
|
||||
},
|
||||
items: [
|
||||
{ id: 'top', group: 'top' },
|
||||
{ id: 'bottom', group: 'bottom' },
|
||||
{ id: 'left', group: 'left' },
|
||||
{ id: 'right', group: 'right' }
|
||||
],
|
||||
};
|
||||
|
||||
export const centerPort = {
|
||||
...commonPorts,
|
||||
items: [{ id: 'center', group: 'center' }]
|
||||
};
|
||||
@@ -0,0 +1,54 @@
|
||||
import { logger } from '../../utils/logger.js';
|
||||
/**
|
||||
* graph/styles/registry.js - Central registry for custom X6 shapes (Plugin System)
|
||||
*/
|
||||
|
||||
class ShapeRegistry {
|
||||
constructor() {
|
||||
this.shapes = new Map();
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a new shape plugin
|
||||
* @param {string} type - Node type name (e.g., 'browser', 'triangle')
|
||||
* @param {Object} options - Shape configuration
|
||||
* @param {string} options.shape - The X6 shape name to register (e.g., 'drawnet-browser')
|
||||
* @param {Object} options.definition - The X6 registration object (inherit, markup, attrs, etc.)
|
||||
* @param {Object} options.mapping - Custom mapping logic or attribute overrides
|
||||
* @param {string} options.icon - Default FontAwesome icon (if applicable)
|
||||
*/
|
||||
register(type, options) {
|
||||
this.shapes.set(type.toLowerCase(), {
|
||||
type,
|
||||
...options
|
||||
});
|
||||
}
|
||||
|
||||
get(type) {
|
||||
return this.shapes.get(type.toLowerCase());
|
||||
}
|
||||
|
||||
getAll() {
|
||||
return Array.from(this.shapes.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal X6 registration helper
|
||||
*/
|
||||
install(X6) {
|
||||
if (!X6) return;
|
||||
const registeredShapes = new Set();
|
||||
this.shapes.forEach((plugin) => {
|
||||
if (plugin.shape && plugin.definition && !registeredShapes.has(plugin.shape)) {
|
||||
try {
|
||||
X6.Graph.registerNode(plugin.shape, plugin.definition);
|
||||
registeredShapes.add(plugin.shape);
|
||||
} catch (e) {
|
||||
logger.high(`Shape [${plugin.shape}] registration failed:`, e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const shapeRegistry = new ShapeRegistry();
|
||||
@@ -0,0 +1,135 @@
|
||||
/**
|
||||
* graph/styles/utils.js - Utility functions for graph styling
|
||||
*/
|
||||
import { state } from '../../state.js';
|
||||
import { logger } from '../../utils/logger.js';
|
||||
|
||||
/**
|
||||
* getLabelAttributes - Returns X6 label attributes based on position and node type
|
||||
*/
|
||||
export function getLabelAttributes(pos, nodeType, is_group) {
|
||||
const defaultPos = is_group ? 'top' : (['rect', 'circle', 'rounded-rect', 'text-box', 'label', 'browser', 'triangle', 'diamond', 'parallelogram', 'cylinder', 'document', 'manual-input'].includes(nodeType) ? 'center' : 'bottom');
|
||||
const actualPos = pos || defaultPos;
|
||||
|
||||
switch (actualPos) {
|
||||
case 'top':
|
||||
return { refX: 0.5, refY: 0, textAnchor: 'middle', textVerticalAnchor: 'bottom', refY2: -8, refX2: 0 };
|
||||
case 'bottom':
|
||||
return { refX: 0.5, refY: '100%', textAnchor: 'middle', textVerticalAnchor: 'top', refY2: 8, refX2: 0 };
|
||||
case 'left':
|
||||
return { refX: 0, refY: 0.5, textAnchor: 'end', textVerticalAnchor: 'middle', refY2: 0, refX2: -12 };
|
||||
case 'right':
|
||||
return { refX: '100%', refY: 0.5, textAnchor: 'start', textVerticalAnchor: 'middle', refY2: 0, refX2: 12 };
|
||||
case 'center':
|
||||
return { refX: 0.5, refY: 0.5, textAnchor: 'middle', textVerticalAnchor: 'middle', refY2: 0, refX2: 0 };
|
||||
default:
|
||||
return { refX: 0.5, refY: '100%', textAnchor: 'middle', textVerticalAnchor: 'top', refY2: 8, refX2: 0 };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* doctorEdge - Debug assistant for checking cell data and attributes
|
||||
*/
|
||||
window.doctorEdge = function (id) {
|
||||
if (!state.graph) return logger.critical("X6 Graph not initialized.");
|
||||
const cell = state.graph.getCellById(id);
|
||||
if (!cell) return logger.critical(`Cell [${id}] not found.`);
|
||||
|
||||
logger.info(`--- drawNET STYLE DOCTOR (X6) for [${id}] ---`);
|
||||
logger.info("Data Properties:", cell.getData());
|
||||
logger.info("Cell Attributes:", cell.getAttrs());
|
||||
|
||||
if (cell.isEdge()) {
|
||||
logger.info("Router:", cell.getRouter());
|
||||
logger.info("Connector:", cell.getConnector());
|
||||
logger.info("Source/Target:", {
|
||||
source: cell.getSource(),
|
||||
target: cell.getTarget()
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Rack의 높이와 슬롯 수에 따라 U-Unit 눈금 패스(SVG Path)를 생성합니다.
|
||||
*/
|
||||
export function generateRackUnitsPath(height, slots = 42) {
|
||||
let path = '';
|
||||
const headerHeight = 30; // node_shapes.js 고정값과 동기화
|
||||
const rackBodyHeight = height - headerHeight;
|
||||
const unitHeight = rackBodyHeight / slots;
|
||||
|
||||
// 단순화된 눈금 (좌우 레일에 작은 선)
|
||||
for (let i = 0; i < slots; i++) {
|
||||
const y = headerHeight + (i * unitHeight);
|
||||
path += `M 5 ${y} L 15 ${y} M 145 ${y} L 155 ${y} `;
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* renderRichContent - Translates plain text to stylized HTML for the Rich Card
|
||||
*/
|
||||
export function renderRichContent(cell, updates) {
|
||||
if (!state.graph) return;
|
||||
const view = cell.findView(state.graph);
|
||||
if (!view) return; // View might not be rendered yet
|
||||
|
||||
const container = view.container;
|
||||
const contentDiv = container.querySelector('div[selector="content"]');
|
||||
if (!contentDiv) return;
|
||||
|
||||
const data = updates || cell.getData() || {};
|
||||
const content = data.content || '';
|
||||
const cardType = data.cardType || 'standard';
|
||||
const headerColor = data.headerColor || '#3b82f6';
|
||||
const contentAlign = data.contentAlign || 'left';
|
||||
|
||||
contentDiv.style.textAlign = contentAlign;
|
||||
|
||||
const lines = content.split('\n').filter(line => line.trim() !== '');
|
||||
let html = '';
|
||||
|
||||
if (cardType === 'numbered') {
|
||||
html = lines.map((line, idx) => `
|
||||
<div style="display: flex; margin-bottom: 8px; align-items: flex-start;">
|
||||
<div style="background: ${headerColor}; color: white; width: 18px; height: 18px;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 10px; font-weight: bold; border-radius: 2px; margin-right: 8px; flex-shrink: 0;">
|
||||
${idx + 1}
|
||||
</div>
|
||||
<div style="font-size: 11px;">${line}</div>
|
||||
</div>
|
||||
`).join('');
|
||||
} else if (cardType === 'bullet') {
|
||||
html = lines.map(line => `
|
||||
<div style="display: flex; margin-bottom: 5px; align-items: flex-start;">
|
||||
<div style="color: ${headerColor}; margin-right: 8px; flex-shrink: 0;">●</div>
|
||||
<div style="font-size: 11px;">${line}</div>
|
||||
</div>
|
||||
`).join('');
|
||||
} else if (cardType === 'legend') {
|
||||
html = lines.map(line => {
|
||||
const match = line.match(/^-\s*\((.*?):(.*?)\)\s*(.*)/);
|
||||
if (match) {
|
||||
const [, shape, color, text] = match;
|
||||
const iconHtml = shape === 'line'
|
||||
? `<div style="width: 16px; height: 2px; background: ${color}; margin-top: 8px;"></div>`
|
||||
: `<div style="width: 12px; height: 12px; background: ${color}; border-radius: ${shape === 'circle' ? '50%' : '2px'}; margin-top: 2px;"></div>`;
|
||||
|
||||
return `
|
||||
<div style="display: flex; margin-bottom: 8px; align-items: flex-start;">
|
||||
<div style="width: 20px; margin-right: 8px; display: flex; justify-content: center; flex-shrink: 0;">
|
||||
${iconHtml}
|
||||
</div>
|
||||
<div style="font-size: 11px;">${text}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
return `<div style="margin-bottom: 5px; font-size: 11px;">${line}</div>`;
|
||||
}).join('');
|
||||
} else {
|
||||
html = lines.map(line => `<div style="margin-bottom: 5px;">${line}</div>`).join('');
|
||||
}
|
||||
|
||||
contentDiv.innerHTML = html;
|
||||
}
|
||||
Reference in New Issue
Block a user