Улучшил работу, мелкие фиксы

This commit is contained in:
NikitaKalitov
2026-07-20 11:59:47 +03:00
parent 041fca449d
commit da7e022f50
2 changed files with 49 additions and 38 deletions
+33 -29
View File
@@ -77,9 +77,9 @@ interface Box {
const SNAP_THRESHOLD = 6; const SNAP_THRESHOLD = 6;
/** /**
* Compares the dragged box's edges/center against every other box on the same * Compares the dragged box's center against every other box's center on the
* level and, for the first close-enough match on each axis, snaps to it and * same level and, for the first close-enough match on each axis, snaps to it
* records a guide line to draw across the canvas. * and records a guide line to draw across the canvas.
*/ */
function computeSnapAndGuides(dragged: Box, others: Box[]): { x: number; y: number; guides: AlignmentGuide[] } { function computeSnapAndGuides(dragged: Box, others: Box[]): { x: number; y: number; guides: AlignmentGuide[] } {
const guides: AlignmentGuide[] = []; const guides: AlignmentGuide[] = [];
@@ -87,36 +87,24 @@ function computeSnapAndGuides(dragged: Box, others: Box[]): { x: number; y: numb
let snappedY = dragged.y; let snappedY = dragged.y;
let matchedX = false; let matchedX = false;
let matchedY = false; let matchedY = false;
const draggedCenterX = dragged.x + dragged.width / 2;
const draggedCenterY = dragged.y + dragged.height / 2;
for (const other of others) { for (const other of others) {
if (!matchedX) { if (!matchedX) {
const xCandidates: [number, number][] = [ const otherCenterX = other.x + other.width / 2;
[dragged.x, other.x], if (Math.abs(draggedCenterX - otherCenterX) <= SNAP_THRESHOLD) {
[dragged.x + dragged.width, other.x + other.width], snappedX = dragged.x + (otherCenterX - draggedCenterX);
[dragged.x + dragged.width / 2, other.x + other.width / 2], guides.push({ axis: 'x', position: otherCenterX });
];
for (const [dValue, oValue] of xCandidates) {
if (Math.abs(dValue - oValue) <= SNAP_THRESHOLD) {
snappedX = dragged.x + (oValue - dValue);
guides.push({ axis: 'x', position: oValue });
matchedX = true; matchedX = true;
break;
}
} }
} }
if (!matchedY) { if (!matchedY) {
const yCandidates: [number, number][] = [ const otherCenterY = other.y + other.height / 2;
[dragged.y, other.y], if (Math.abs(draggedCenterY - otherCenterY) <= SNAP_THRESHOLD) {
[dragged.y + dragged.height, other.y + other.height], snappedY = dragged.y + (otherCenterY - draggedCenterY);
[dragged.y + dragged.height / 2, other.y + other.height / 2], guides.push({ axis: 'y', position: otherCenterY });
];
for (const [dValue, oValue] of yCandidates) {
if (Math.abs(dValue - oValue) <= SNAP_THRESHOLD) {
snappedY = dragged.y + (oValue - dValue);
guides.push({ axis: 'y', position: oValue });
matchedY = true; matchedY = true;
break;
}
} }
} }
} }
@@ -307,8 +295,21 @@ function DiagramCanvas({ initialValue, onChange }: DiagramEditorProps) {
); );
if (isHistoryWorthy) pushHistory(nodes, edges); if (isHistoryWorthy) pushHistory(nodes, edges);
const next = applyNodeChanges(adjustedChanges, nodes); const next = applyNodeChanges(adjustedChanges, nodes);
// React Flow's own keyboard delete dispatches node and edge removal as
// separate change events that race through the same debounced
// emitChange — filtering edges here too (redundantly with onEdgesChange)
// guarantees a removed node never leaves a dangling edge behind.
const removedIds = new Set(
filteredChanges.filter((change) => change.type === 'remove').map((change) => change.id),
);
const nextEdges = removedIds.size
? edges.filter((edge) => !removedIds.has(edge.source) && !removedIds.has(edge.target))
: edges;
setNodes(next); setNodes(next);
emitChange(next, edges); if (nextEdges !== edges) setEdges(nextEdges);
emitChange(next, nextEdges);
}, },
[nodes, edges, emitChange, pushHistory, alignmentGuides], [nodes, edges, emitChange, pushHistory, alignmentGuides],
); );
@@ -327,7 +328,10 @@ function DiagramCanvas({ initialValue, onChange }: DiagramEditorProps) {
const onConnect: OnConnect = useCallback( const onConnect: OnConnect = useCallback(
(connection) => { (connection) => {
pushHistory(nodes, edges); pushHistory(nodes, edges);
const next = addEdge({ ...connection, type: DEFAULT_EDGE_TYPE, markerEnd: DEFAULT_MARKER_END }, edges); const next = addEdge(
{ ...connection, id: crypto.randomUUID(), type: DEFAULT_EDGE_TYPE, markerEnd: DEFAULT_MARKER_END },
edges,
);
setEdges(next); setEdges(next);
emitChange(nodes, next); emitChange(nodes, next);
}, },
@@ -397,7 +401,7 @@ function DiagramCanvas({ initialValue, onChange }: DiagramEditorProps) {
const addNode = useCallback( const addNode = useCallback(
(kind: NodeKind) => { (kind: NodeKind) => {
nodeCounter.current += 1; nodeCounter.current += 1;
const id = `${kind}-${nodeCounter.current}`; const id = crypto.randomUUID();
const center = getViewportCenter(); const center = getViewportCenter();
const fanOut = nodeCounter.current; const fanOut = nodeCounter.current;
const size = DEFAULT_NODE_SIZE[kind]; const size = DEFAULT_NODE_SIZE[kind];
@@ -431,7 +435,7 @@ function DiagramCanvas({ initialValue, onChange }: DiagramEditorProps) {
const source = clipboardRef.current; const source = clipboardRef.current;
if (!source) return; if (!source) return;
nodeCounter.current += 1; nodeCounter.current += 1;
const newId = `${source.type}-${nodeCounter.current}`; const newId = crypto.randomUUID();
const { parentId: _sourceParentId, selected: _sourceSelected, ...restSource } = source; const { parentId: _sourceParentId, selected: _sourceSelected, ...restSource } = source;
const newNode: Node = { const newNode: Node = {
...restSource, ...restSource,
+10 -3
View File
@@ -1,11 +1,18 @@
import { defineConfig } from 'vite' import { defineConfig, loadEnv } from 'vite'
import react from '@vitejs/plugin-react' import react from '@vitejs/plugin-react'
// https://vite.dev/config/ // https://vite.dev/config/
export default defineConfig({ export default defineConfig(({ mode }) => {
// VITE_BASE_PATH is read here (config/Node context), so it needs an explicit
// loadEnv call — unlike VITE_BITRIX_ORIGIN, which Vite injects into
// import.meta.env automatically because it's only read from app code.
const env = loadEnv(mode, process.cwd(), '')
return {
// Needed when the build is served from a subpath (e.g. local/diagram-app/ // Needed when the build is served from a subpath (e.g. local/diagram-app/
// inside the Bitrix docroot) instead of domain root — see // inside the Bitrix docroot) instead of domain root — see
// docs/bitrix-integration.md §7. // docs/bitrix-integration.md §7.
base: process.env.VITE_BASE_PATH || '/', base: env.VITE_BASE_PATH || '/',
plugins: [react()], plugins: [react()],
}
}) })