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,95 @@
|
||||
import { state } from '../../state.js';
|
||||
import { logger } from '../../utils/logger.js';
|
||||
import { t } from '../../i18n.js';
|
||||
import { getX6NodeConfig } from '../styles.js';
|
||||
|
||||
/**
|
||||
* initDropHandling - Sets up drag and drop from the asset library to the graph
|
||||
*/
|
||||
export function initDropHandling(container) {
|
||||
container.addEventListener('dragover', (e) => e.preventDefault());
|
||||
|
||||
container.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
const assetId = e.dataTransfer.getData('assetId');
|
||||
const asset = state.assetMap[assetId];
|
||||
const pos = state.graph.clientToLocal(e.clientX, e.clientY);
|
||||
|
||||
logger.info(`Drop Event at {${pos.x}, ${pos.y}}`, {
|
||||
assetId: assetId,
|
||||
found: !!asset,
|
||||
id: asset?.id,
|
||||
path: asset?.path,
|
||||
type: asset?.type,
|
||||
rawAsset: asset
|
||||
});
|
||||
|
||||
if (!asset) {
|
||||
logger.high(`Asset [${assetId}] not found in map`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Logical Layer Guard: Block dropping new nodes
|
||||
const activeLayer = state.layers.find(l => l.id === state.activeLayerId);
|
||||
if (activeLayer && activeLayer.type === 'logical') {
|
||||
import('../../ui/utils.js').then(m => m.showToast(t('err_logical_layer_drop'), 'warning'));
|
||||
return;
|
||||
}
|
||||
|
||||
const isPrimitive = ['rect', 'rounded-rect', 'circle', 'text-box', 'label'].includes(asset.type);
|
||||
const prefix = asset.id === 'fixed-group' ? 'group_' : (asset.type === 'blank' || asset.type === 'dot' ? 'p_' : (isPrimitive ? 'shp_' : 'node_'));
|
||||
const id = prefix + Math.random().toString(36).substr(2, 4);
|
||||
|
||||
// Use translate keys or asset label first, or fallback to generic name
|
||||
let initialLabel = asset.label || t(asset.id) || (asset.id === 'fixed-group' ? 'New Group' : 'New ' + (asset.type || 'Object'));
|
||||
if (asset.id === 'fixed-group') initialLabel = t('group') || 'New Group';
|
||||
|
||||
// Find if dropped over a group (prefer the smallest/innermost one)
|
||||
const candidates = state.graph.getNodes().filter(n => {
|
||||
if (!n.getData()?.is_group) return false;
|
||||
const bbox = n.getBBox();
|
||||
return bbox.containsPoint(pos);
|
||||
});
|
||||
|
||||
const parent = candidates.length > 0
|
||||
? candidates.reduce((smallest, current) => {
|
||||
const smallestBBox = smallest.getBBox();
|
||||
const currentBBox = current.getBBox();
|
||||
const currentArea = currentBBox.width * currentBBox.height;
|
||||
const smallestArea = smallestBBox.width * smallestBBox.height;
|
||||
|
||||
if (currentArea < smallestArea) return current;
|
||||
if (currentArea > smallestArea) return smallest;
|
||||
|
||||
// If areas are equal, pick the one that is a descendant of the other
|
||||
if (current.getAncestors().some(a => a.id === smallest.id)) return current;
|
||||
return smallest;
|
||||
})
|
||||
: undefined;
|
||||
|
||||
|
||||
// 2. Add to X6 immediately
|
||||
const config = getX6NodeConfig({
|
||||
id: id,
|
||||
label: initialLabel,
|
||||
type: asset.type || asset.category || asset.label || 'Unknown',
|
||||
assetId: assetId, // Store unique composite ID for lookup
|
||||
assetPath: asset.id === 'fixed-group' ? undefined : asset.path,
|
||||
is_group: asset.id === 'fixed-group' || asset.is_group === true,
|
||||
pos: { x: pos.x, y: pos.y },
|
||||
parent: parent ? parent.id : undefined,
|
||||
layerId: state.activeLayerId,
|
||||
description: "",
|
||||
asset_tag: "",
|
||||
tags: []
|
||||
});
|
||||
|
||||
const newNode = state.graph.addNode(config);
|
||||
if (parent) {
|
||||
parent.addChild(newNode);
|
||||
}
|
||||
|
||||
// Ensure the new node follows the active layer's interaction rules immediately
|
||||
import('../layers.js').then(m => m.applyLayerFilters());
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* graph/interactions/edges.js - Handles edge connection logic and metadata assignment.
|
||||
*/
|
||||
import { state } from '../../state.js';
|
||||
|
||||
/**
|
||||
* initEdgeHandling - Sets up edge connection events
|
||||
*/
|
||||
export function initEdgeHandling() {
|
||||
state.graph.on('edge:connected', ({ edge }) => {
|
||||
const sourceId = edge.getSourceCellId();
|
||||
const targetId = edge.getTargetCellId();
|
||||
if (!sourceId || !targetId) return;
|
||||
|
||||
// Assign to current active layer and apply standard styling
|
||||
import('/static/js/modules/graph/styles.js').then(({ getX6EdgeConfig }) => {
|
||||
// [6개월 뒤의 나를 위한 메모]
|
||||
// 단순 유추가 아닌 명시적 기록을 통해, 도면 전체를 전송하지 않고도 연결선 단독으로
|
||||
// 크로스 레이어 여부를 파악할수 있게 하여 외부 도구/AI 연동 안정성을 높임.
|
||||
const sourceNode = state.graph.getCellById(sourceId);
|
||||
const targetNode = state.graph.getCellById(targetId);
|
||||
const sourceLayer = sourceNode?.getData()?.layerId || state.activeLayerId;
|
||||
const targetLayer = targetNode?.getData()?.layerId || state.activeLayerId;
|
||||
const isCrossLayer = (sourceLayer !== targetLayer);
|
||||
|
||||
const edgeData = {
|
||||
source: sourceId,
|
||||
target: targetId,
|
||||
layerId: state.activeLayerId,
|
||||
source_layer: sourceLayer,
|
||||
target_layer: targetLayer,
|
||||
is_cross_layer: isCrossLayer,
|
||||
...edge.getData()
|
||||
};
|
||||
|
||||
const config = getX6EdgeConfig(edgeData);
|
||||
|
||||
// Apply standardized config to the new edge
|
||||
edge.setData(edgeData, { silent: true });
|
||||
if (config.router) edge.setRouter(config.router);
|
||||
else edge.setRouter(null);
|
||||
|
||||
if (config.connector) edge.setConnector(config.connector);
|
||||
edge.setAttrs(config.attrs);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { state } from '../../state.js';
|
||||
import { initDropHandling } from './drop.js';
|
||||
import { initEdgeHandling } from './edges.js';
|
||||
import { initSnapping } from './snapping.js';
|
||||
import { initSpecialShortcuts } from './shortcuts.js';
|
||||
import { logger } from '../../utils/logger.js';
|
||||
|
||||
/**
|
||||
* initInteractions - Entry point for all graph interactions
|
||||
*/
|
||||
export function initInteractions() {
|
||||
if (!state.graph) return;
|
||||
|
||||
const container = state.graph.container;
|
||||
|
||||
// 1. Initialize Sub-modules
|
||||
initDropHandling(container);
|
||||
initEdgeHandling();
|
||||
initSnapping();
|
||||
initSpecialShortcuts();
|
||||
|
||||
logger.info("Graph interactions initialized via modules.");
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
import { state } from '../../state.js';
|
||||
import { logger } from '../../utils/logger.js';
|
||||
|
||||
/**
|
||||
* initSpecialShortcuts - Sets up specialized mouse/keyboard combined gestures
|
||||
*/
|
||||
export function initSpecialShortcuts() {
|
||||
if (!state.graph) return;
|
||||
|
||||
let dragStartData = null;
|
||||
|
||||
// Ctrl + Drag to Copy: Improved Ghost Logic
|
||||
state.graph.on('node:mousedown', ({ e, node }) => {
|
||||
// Support both Ctrl (Windows) and Cmd (Mac)
|
||||
const isModifier = e.ctrlKey || e.metaKey;
|
||||
if (isModifier && e.button === 0) {
|
||||
const selected = state.graph.getSelectedCells();
|
||||
const targets = selected.includes(node) ? selected : [node];
|
||||
|
||||
// 1. Initial State for tracking
|
||||
const mouseStart = state.graph.clientToLocal(e.clientX, e.clientY);
|
||||
dragStartData = {
|
||||
targets: targets,
|
||||
mouseStart: mouseStart,
|
||||
initialNodesPos: targets.map(t => ({ ...t.position() })),
|
||||
ghost: null
|
||||
};
|
||||
|
||||
const onMouseMove = (moveEvt) => {
|
||||
if (!dragStartData) return;
|
||||
|
||||
const mouseNow = state.graph.clientToLocal(moveEvt.clientX, moveEvt.clientY);
|
||||
const dx = mouseNow.x - dragStartData.mouseStart.x;
|
||||
const dy = mouseNow.y - dragStartData.mouseStart.y;
|
||||
|
||||
// Create Ghost on movement
|
||||
if (!dragStartData.ghost && (Math.abs(dx) > 2 || Math.abs(dy) > 2)) {
|
||||
const cellsBBox = state.graph.getCellsBBox(dragStartData.targets);
|
||||
dragStartData.ghost = state.graph.addNode({
|
||||
shape: 'rect',
|
||||
x: cellsBBox.x,
|
||||
y: cellsBBox.y,
|
||||
width: cellsBBox.width,
|
||||
height: cellsBBox.height,
|
||||
attrs: {
|
||||
body: {
|
||||
fill: 'rgba(59, 130, 246, 0.1)',
|
||||
stroke: '#3b82f6',
|
||||
strokeWidth: 2,
|
||||
strokeDasharray: '5,5',
|
||||
pointerEvents: 'none'
|
||||
}
|
||||
},
|
||||
zIndex: 1000
|
||||
});
|
||||
dragStartData.initialGhostPos = { x: cellsBBox.x, y: cellsBBox.y };
|
||||
}
|
||||
|
||||
if (dragStartData.ghost) {
|
||||
dragStartData.ghost.position(
|
||||
dragStartData.initialGhostPos.x + dx,
|
||||
dragStartData.initialGhostPos.y + dy
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const onMouseUp = (upEvt) => {
|
||||
cleanup(upEvt.button === 0);
|
||||
};
|
||||
|
||||
const onKeyDown = (keyEvt) => {
|
||||
if (keyEvt.key === 'Escape') {
|
||||
cleanup(false); // Cancel on ESC
|
||||
}
|
||||
};
|
||||
|
||||
function cleanup(doPaste) {
|
||||
if (dragStartData) {
|
||||
if (doPaste && dragStartData.ghost) {
|
||||
const finalPos = dragStartData.ghost.position();
|
||||
const dx = finalPos.x - dragStartData.initialGhostPos.x;
|
||||
const dy = finalPos.y - dragStartData.initialGhostPos.y;
|
||||
|
||||
if (Math.abs(dx) > 5 || Math.abs(dy) > 5) {
|
||||
state.graph.copy(dragStartData.targets, { deep: true });
|
||||
let clones = state.graph.paste({ offset: { dx, dy }, select: true });
|
||||
|
||||
// Logical Layer Guard: Filter out nodes if target layer is logical
|
||||
const activeLayer = state.layers.find(l => l.id === state.activeLayerId);
|
||||
if (activeLayer && activeLayer.type === 'logical') {
|
||||
clones.forEach(clone => {
|
||||
if (clone.isNode()) clone.remove();
|
||||
});
|
||||
clones = clones.filter(c => !c.isNode());
|
||||
if (clones.length === 0) {
|
||||
import('/static/js/modules/ui/utils.js').then(m => m.showToast(t('err_logical_layer_drop'), 'warning'));
|
||||
}
|
||||
}
|
||||
|
||||
clones.forEach(clone => {
|
||||
if (clone.isNode()) {
|
||||
const d = clone.getData() || {};
|
||||
clone.setData({ ...d, id: clone.id }, { silent: true });
|
||||
}
|
||||
});
|
||||
import('/static/js/modules/persistence.js').then(m => m.markDirty());
|
||||
}
|
||||
}
|
||||
|
||||
if (dragStartData.ghost) dragStartData.ghost.remove();
|
||||
}
|
||||
dragStartData = null;
|
||||
window.removeEventListener('mousemove', onMouseMove, true);
|
||||
window.removeEventListener('mouseup', onMouseUp, true);
|
||||
window.removeEventListener('keydown', onKeyDown, true);
|
||||
}
|
||||
|
||||
window.addEventListener('mousemove', onMouseMove, true);
|
||||
window.addEventListener('mouseup', onMouseUp, true);
|
||||
window.addEventListener('keydown', onKeyDown, true);
|
||||
}
|
||||
});
|
||||
|
||||
logger.info("initSpecialShortcuts (Ctrl+Drag with ESC Failsafe) initialized.");
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { state } from '../../state.js';
|
||||
|
||||
/**
|
||||
* initSnapping - Sets up snapping and grouping interactions during movement
|
||||
*/
|
||||
export function initSnapping() {
|
||||
// 1. Rack Snapping Logic
|
||||
state.graph.on('node:moving', ({ node }) => {
|
||||
const parent = node.getParent();
|
||||
if (parent && parent.shape === 'drawnet-rack') {
|
||||
const rackBBox = parent.getBBox();
|
||||
const nodeSize = node.size();
|
||||
const pos = node.getPosition();
|
||||
|
||||
const headerHeight = 30;
|
||||
const rackBodyHeight = rackBBox.height - headerHeight;
|
||||
const unitCount = parent.getData()?.slots || 42;
|
||||
const unitHeight = rackBodyHeight / unitCount;
|
||||
|
||||
const centerX = rackBBox.x + (rackBBox.width - nodeSize.width) / 2;
|
||||
const relativeY = pos.y - rackBBox.y - headerHeight;
|
||||
const snapY = rackBBox.y + headerHeight + Math.round(relativeY / unitHeight) * unitHeight;
|
||||
|
||||
node.setPosition(centerX, snapY, { silent: true });
|
||||
}
|
||||
});
|
||||
|
||||
// 2. Parent-Child Relationship Update on Move End
|
||||
state.graph.on('node:moved', ({ node }) => {
|
||||
if (node.getData()?.is_group) return;
|
||||
|
||||
let parent = node.getParent();
|
||||
|
||||
if (!parent) {
|
||||
const pos = node.getPosition();
|
||||
const center = { x: pos.x + node.size().width / 2, y: pos.y + node.size().height / 2 };
|
||||
parent = state.graph.getNodes().find(n => {
|
||||
if (n.id === node.id || !n.getData()?.is_group) return false;
|
||||
const bbox = n.getBBox();
|
||||
return bbox.containsPoint(center);
|
||||
});
|
||||
}
|
||||
|
||||
if (parent && node.getParent()?.id !== parent.id) {
|
||||
parent.addChild(node);
|
||||
}
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user