fix: 修复泄漏区布局问题

This commit is contained in:
黎智洲 2022-02-25 18:30:41 +08:00
parent 7bba64cc9e
commit dd219ce946
7 changed files with 293 additions and 285 deletions

1
.gitignore vendored
View File

@ -1,2 +1,3 @@
node_modules node_modules
test test
dist

View File

@ -129,7 +129,6 @@ SV.registerLayout('BinaryTree', {
*/ */
layout(elements, layoutOptions) { layout(elements, layoutOptions) {
let root = elements[0]; let root = elements[0];
let visited = new Group();
this.layoutItem(root, null, -1, layoutOptions); this.layoutItem(root, null, -1, layoutOptions);
} }
}); });

6
dist/sv.js vendored

File diff suppressed because one or more lines are too long

View File

@ -10,336 +10,343 @@ import { SVAddressLabel, SVFreedLabel, SVIndexLabel, SVMarker } from '../Model/S
import { AddressLabelOption, IndexLabelOption, LayoutOptions, MarkerOption, ViewOptions } from '../options'; import { AddressLabelOption, IndexLabelOption, LayoutOptions, MarkerOption, ViewOptions } from '../options';
import { ViewContainer } from './viewContainer'; import { ViewContainer } from './viewContainer';
export class LayoutProvider { export class LayoutProvider {
private engine: Engine; private engine: Engine;
private viewOptions: ViewOptions; private viewOptions: ViewOptions;
private viewContainer: ViewContainer; private viewContainer: ViewContainer;
constructor(engine: Engine, viewContainer: ViewContainer) { constructor(engine: Engine, viewContainer: ViewContainer) {
this.engine = engine; this.engine = engine;
this.viewOptions = this.engine.viewOptions; this.viewOptions = this.engine.viewOptions;
this.viewContainer = viewContainer; this.viewContainer = viewContainer;
} }
/**
*
* @param layoutGroupTable
*/
private preLayoutProcess(layoutGroupTable: LayoutGroupTable) {
const modelList = Util.convertGroupTable2ModelList(layoutGroupTable);
/** modelList.forEach(item => {
* item.preLayout = true;
* @param layoutGroupTable
*/
private preLayoutProcess(layoutGroupTable: LayoutGroupTable) {
const modelList = Util.convertGroupTable2ModelList(layoutGroupTable);
modelList.forEach(item => { item.set('rotation', item.get('rotation'));
item.preLayout = true; item.set({ x: 0, y: 0 });
});
}
item.set('rotation', item.get('rotation')); /**
item.set({ x: 0, y: 0 }); *
}); * @param layoutGroupTable
} */
private postLayoutProcess(layoutGroupTable: LayoutGroupTable) {
const modelList = Util.convertGroupTable2ModelList(layoutGroupTable);
/** modelList.forEach(item => {
* item.preLayout = false;
* @param layoutGroupTable
*/
private postLayoutProcess(layoutGroupTable: LayoutGroupTable) {
const modelList = Util.convertGroupTable2ModelList(layoutGroupTable);
modelList.forEach(item => { // 用两个变量保存节点布局完成后的坐标因为拖拽节点会改变节点的xy坐标
item.preLayout = false; // 然后当节点移动到泄漏区的时候,不应该保持节点被拖拽后的状态,应该恢复到布局完成后的状态,不然就会很奇怪
item.layoutX = item.get('x');
item.layoutY = item.get('y');
});
}
// 用两个变量保存节点布局完成后的坐标因为拖拽节点会改变节点的xy坐标 /**
// 然后当节点移动到泄漏区的时候,不应该保持节点被拖拽后的状态,应该恢复到布局完成后的状态,不然就会很奇怪 *
item.layoutX = item.get('x'); * @param marker
item.layoutY = item.get('y'); * @param markerOptions
}); */
} private layoutMarker(markers: SVMarker[], markerOptions: { [key: string]: MarkerOption }) {
markers.forEach(item => {
const options: MarkerOption = markerOptions[item.sourceType],
offset = options.offset ?? 8,
anchor = item.anchor ?? 0,
labelOffset = options.labelOffset ?? 2;
/** let target = item.target,
* targetBound: BoundingRect = target.getBound(),
* @param marker g6AnchorPosition = item.target.shadowG6Item.getAnchorPoints()[anchor] as IPoint,
* @param markerOptions center: [number, number] = [
*/ targetBound.x + targetBound.width / 2,
private layoutMarker(markers: SVMarker[], markerOptions: { [key: string]: MarkerOption }) { targetBound.y + targetBound.height / 2,
markers.forEach(item => { ],
const options: MarkerOption = markerOptions[item.sourceType], markerPosition: [number, number],
offset = options.offset ?? 8, markerEndPosition: [number, number];
anchor = item.anchor ?? 0,
labelOffset = options.labelOffset ?? 2;
let target = item.target, let anchorPosition: [number, number] = [g6AnchorPosition.x, g6AnchorPosition.y];
targetBound: BoundingRect = target.getBound(),
g6AnchorPosition = item.target.shadowG6Item.getAnchorPoints()[anchor] as IPoint,
center: [number, number] = [targetBound.x + targetBound.width / 2, targetBound.y + targetBound.height / 2],
markerPosition: [number, number],
markerEndPosition: [number, number];
let anchorPosition: [number, number] = [g6AnchorPosition.x, g6AnchorPosition.y]; let anchorVector = Vector.subtract(anchorPosition, center),
angle = 0,
len = Vector.length(anchorVector) + offset;
let anchorVector = Vector.subtract(anchorPosition, center), if (anchorVector[0] === 0) {
angle = 0, len = Vector.length(anchorVector) + offset; angle = anchorVector[1] > 0 ? -Math.PI : 0;
} else {
angle = Math.sign(anchorVector[0]) * (Math.PI / 2 - Math.atan(anchorVector[1] / anchorVector[0]));
}
if (anchorVector[0] === 0) { const markerHeight = item.get('size')[1],
angle = anchorVector[1] > 0 ? -Math.PI : 0; labelRadius = item.getLabelSizeRadius() / 2;
}
else {
angle = Math.sign(anchorVector[0]) * (Math.PI / 2 - Math.atan(anchorVector[1] / anchorVector[0]));
}
const markerHeight = item.get('size')[1], anchorVector = Vector.normalize(anchorVector);
labelRadius = item.getLabelSizeRadius() / 2; markerPosition = Vector.location(center, anchorVector, len);
markerEndPosition = Vector.location(center, anchorVector, markerHeight + len + labelRadius + labelOffset);
markerEndPosition = Vector.subtract(markerEndPosition, markerPosition);
anchorVector = Vector.normalize(anchorVector); item.set({
markerPosition = Vector.location(center, anchorVector, len); x: markerPosition[0],
markerEndPosition = Vector.location(center, anchorVector, markerHeight + len + labelRadius + labelOffset); y: markerPosition[1],
markerEndPosition = Vector.subtract(markerEndPosition, markerPosition); rotation: angle,
markerEndPosition,
});
});
}
item.set({ /**
x: markerPosition[0], *
y: markerPosition[1], * @param freedLabels
rotation: angle, */
markerEndPosition private layoutFreedLabel(freedLabels: SVFreedLabel[]) {
}); freedLabels.forEach(item => {
}); const freedNodeBound = item.target.getBound();
}
/** item.set({
* x: freedNodeBound.x + freedNodeBound.width / 2,
* @param freedLabels y: freedNodeBound.y + freedNodeBound.height * 1.5,
*/ size: [freedNodeBound.width, 0],
private layoutFreedLabel(freedLabels: SVFreedLabel[]) { });
freedLabels.forEach(item => { });
const freedNodeBound = item.target.getBound(); }
item.set({ /**
x: freedNodeBound.x + freedNodeBound.width / 2, *
y: freedNodeBound.y + freedNodeBound.height * 1.5, * @param indexLabels
size: [freedNodeBound.width, 0] * @param indexLabelOptions
}); */
}); private layoutIndexLabel(indexLabels: SVIndexLabel[], indexLabelOptions: { [key: string]: IndexLabelOption }) {
} const indexLabelPositionMap: {
[key: string]: (
nodeBound: BoundingRect,
labelBound: BoundingRect,
offset: number
) => { x: number; y: number };
} = {
top: (nodeBound: BoundingRect, labelBound: BoundingRect, offset: number) => {
return {
x: nodeBound.x + nodeBound.width / 2,
y: nodeBound.y - offset,
};
},
right: (nodeBound: BoundingRect, labelBound: BoundingRect, offset: number) => {
return {
x: nodeBound.x + nodeBound.width + offset,
y: nodeBound.y + nodeBound.height / 2,
};
},
bottom: (nodeBound: BoundingRect, labelBound: BoundingRect, offset: number) => {
return {
x: nodeBound.x + nodeBound.width / 2,
y: nodeBound.y + nodeBound.height + offset,
};
},
left: (nodeBound: BoundingRect, labelBound: BoundingRect, offset: number) => {
return {
x: nodeBound.x - labelBound.width - offset,
y: nodeBound.y + nodeBound.height / 2,
};
},
};
/** indexLabels.forEach(item => {
* const options: IndexLabelOption = indexLabelOptions[item.sourceType],
* @param indexLabels nodeBound = item.target.getBound(),
* @param indexLabelOptions labelBound = item.getBound(),
*/ offset = options.offset ?? 20,
private layoutIndexLabel(indexLabels: SVIndexLabel[], indexLabelOptions: { [key: string]: IndexLabelOption }) { position = options.position ?? 'bottom';
const indexLabelPositionMap: { [key: string]: (nodeBound: BoundingRect, labelBound: BoundingRect, offset: number) => { x: number, y: number } } = {
top: (nodeBound: BoundingRect, labelBound: BoundingRect, offset: number) => {
return {
x: nodeBound.x + nodeBound.width / 2,
y: nodeBound.y - offset
};
},
right: (nodeBound: BoundingRect, labelBound: BoundingRect, offset: number) => {
return {
x: nodeBound.x + nodeBound.width + offset,
y: nodeBound.y + nodeBound.height / 2
};
},
bottom: (nodeBound: BoundingRect, labelBound: BoundingRect, offset: number) => {
return {
x: nodeBound.x + nodeBound.width / 2,
y: nodeBound.y + nodeBound.height + offset
};
},
left: (nodeBound: BoundingRect, labelBound: BoundingRect, offset: number) => {
return {
x: nodeBound.x - labelBound.width - offset,
y: nodeBound.y + nodeBound.height / 2
};
}
};
indexLabels.forEach(item => { const pos = indexLabelPositionMap[position](nodeBound, labelBound, offset);
const options: IndexLabelOption = indexLabelOptions[item.sourceType], item.set(pos);
nodeBound = item.target.getBound(), });
labelBound = item.getBound(), }
offset = options.offset ?? 20,
position = options.position ?? 'bottom';
const pos = indexLabelPositionMap[position](nodeBound, labelBound, offset); /**
item.set(pos); * address label
}); * @param leakAddress
} */
private layoutAddressLabel(leakAddress: SVAddressLabel[], addressLabelOption: AddressLabelOption) {
const offset = addressLabelOption.offset || 16;
/** leakAddress.forEach(item => {
* address label const nodeBound = item.target.getBound();
* @param leakAddress
*/
private layoutAddressLabel(leakAddress: SVAddressLabel[], addressLabelOption: AddressLabelOption) {
const offset = addressLabelOption.offset || 16;
leakAddress.forEach(item => { item.set({
const nodeBound = item.target.getBound(); x: nodeBound.x + nodeBound.width / 2,
y: nodeBound.y - offset,
});
});
}
item.set({ /**
x: nodeBound.x + nodeBound.width / 2, * model进行布局
y: nodeBound.y - offset * @param layoutGroupTable
}); */
}); private layoutModels(layoutGroupTable: LayoutGroupTable): Group[] {
} const modelGroupList: Group[] = [];
layoutGroupTable.forEach(group => {
const modelList: SVModel[] = group.modelList,
modelGroup: Group = new Group();
/** const layoutOptions: LayoutOptions = group.options.layout;
* model进行布局
* @param layoutGroupTable
*/
private layoutModels(layoutGroupTable: LayoutGroupTable): Group[] {
const modelGroupList: Group[] = [];
layoutGroupTable.forEach(group => { modelList.forEach(item => {
const modelList: SVModel[] = group.modelList, modelGroup.add(item);
modelGroup: Group = new Group(); });
const layoutOptions: LayoutOptions = group.options.layout; group.layoutCreator.layout(group.node, layoutOptions); // 布局节点
modelGroupList.push(modelGroup);
});
modelList.forEach(item => { layoutGroupTable.forEach(group => {
modelGroup.add(item); const markerOptions = group.options.marker || {},
}); indexLabelOptions = group.options.indexLabel || {},
addressLabelOption = group.options.addressLabel || {};
group.layoutCreator.layout(group.node, layoutOptions); // 布局节点 this.layoutIndexLabel(group.indexLabel, indexLabelOptions);
modelGroupList.push(modelGroup); this.layoutFreedLabel(group.freedLabel);
}); this.layoutAddressLabel(group.addressLabel, addressLabelOption);
this.layoutMarker(group.marker, markerOptions); // 布局外部指针
});
layoutGroupTable.forEach(group => { return modelGroupList;
const markerOptions = group.options.marker || {}, }
indexLabelOptions = group.options.indexLabel || {},
addressLabelOption = group.options.addressLabel || {};
this.layoutIndexLabel(group.indexLabel, indexLabelOptions); /**
this.layoutFreedLabel(group.freedLabel); *
this.layoutAddressLabel(group.addressLabel, addressLabelOption); * @param leakModels
this.layoutMarker(group.marker, markerOptions); // 布局外部指针 * @param accumulateLeakModels
}); */
private layoutLeakModels(leakModels: SVModel[], accumulateLeakModels: SVModel[]) {
const group: Group = new Group(),
containerHeight = this.viewContainer.getG6Instance().getHeight(),
leakAreaHeight = this.engine.viewOptions.leakAreaHeight,
leakAreaY = containerHeight - leakAreaHeight,
xOffset = 60;
return modelGroupList; let prevBound: BoundingRect;
}
/** // 避免在泄漏前拖拽节点导致的位置变化,先把节点位置重置为布局后的标准位置
* leakModels.forEach(item => {
* @param leakModels item.set({
* @param accumulateLeakModels x: item.layoutX,
*/ y: item.layoutY,
private layoutLeakModels(leakModels: SVModel[], accumulateLeakModels: SVModel[]) { });
const group: Group = new Group(), });
containerHeight = this.viewContainer.getG6Instance().getHeight(),
leakAreaHeight = this.engine.viewOptions.leakAreaHeight,
leakAreaY = containerHeight - leakAreaHeight,
xOffset = 60;
let prevBound: BoundingRect; const globalLeakGroupBound: BoundingRect = accumulateLeakModels.length
? Bound.union(...accumulateLeakModels.map(item => item.getBound()))
: { x: 0, y: leakAreaY, width: 0, height: 0 };
// 避免在泄漏前拖拽节点导致的位置变化,先把节点位置重置为布局后的标准位置 const layoutGroups = Util.groupBy(leakModels, 'group');
leakModels.forEach(item => { Object.keys(layoutGroups).forEach(key => {
item.set({ group.add(...layoutGroups[key]);
x: item.layoutX,
y: item.layoutY
});
});
const globalLeakGroupBound: BoundingRect = accumulateLeakModels.length ? const currentBound: BoundingRect = group.getBound(),
Bound.union(...accumulateLeakModels.map(item => item.getBound())) : prevBoundEnd = prevBound ? prevBound.x + prevBound.width : 0,
{ x: 0, y: leakAreaY, width: 0, height: 0 }; { x: groupX, y: groupY } = currentBound,
dx = globalLeakGroupBound.x + globalLeakGroupBound.width + prevBoundEnd + xOffset - groupX,
dy = globalLeakGroupBound.y - groupY;
const layoutGroups = Util.groupBy(leakModels, 'group'); group.translate(dx, dy);
Object.keys(layoutGroups).forEach(key => { group.clear();
group.add(...layoutGroups[key]); Bound.translate(currentBound, dx, dy);
const currentBound: BoundingRect = group.getBound(), prevBound = currentBound;
prevBoundEnd = prevBound? prevBound.x + prevBound.width: 0, });
{ x: groupX, y: groupY } = currentBound, }
dx = globalLeakGroupBound.x + globalLeakGroupBound.width + prevBoundEnd + xOffset - groupX,
dy = globalLeakGroupBound.y - groupY;
group.translate(dx, dy); /**
group.clear(); *
Bound.translate(currentBound, dx, dy); * @param modelGroupTable
*/
private layoutGroups(modelGroupList: Group[]): Group {
let wrapperGroup: Group = new Group(),
group: Group,
prevBound: BoundingRect,
bound: BoundingRect,
boundList: BoundingRect[] = [],
dx = 0;
prevBound = currentBound; // 左往右布局
}); for (let i = 0; i < modelGroupList.length; i++) {
} group = modelGroupList[i];
bound = group.getPaddingBound(this.viewOptions.groupPadding);
/** if (prevBound) {
* dx = prevBound.x + prevBound.width - bound.x;
* @param modelGroupTable } else {
*/ dx = bound.x;
private layoutGroups(modelGroupList: Group[]): Group { }
let wrapperGroup: Group = new Group(),
group: Group,
prevBound: BoundingRect,
bound: BoundingRect,
boundList: BoundingRect[] = [],
dx = 0;
// 左往右布局 group.translate(dx, 0);
for (let i = 0; i < modelGroupList.length; i++) { Bound.translate(bound, dx, 0);
group = modelGroupList[i]; boundList.push(bound);
bound = group.getPaddingBound(this.viewOptions.groupPadding); wrapperGroup.add(group);
prevBound = bound;
}
if (prevBound) { return wrapperGroup;
dx = prevBound.x + prevBound.width - bound.x; }
}
else {
dx = bound.x;
}
group.translate(dx, 0); /**
Bound.translate(bound, dx, 0); *
boundList.push(bound); * @param models
wrapperGroup.add(group); * @param leakAreaHeight
prevBound = bound; */
} private fitCenter(group: Group) {
let width = this.viewContainer.getG6Instance().getWidth(),
height = this.viewContainer.getG6Instance().getHeight(),
viewBound: BoundingRect = group.getBound(),
leakAreaHeight = this.engine.viewOptions.leakAreaHeight;
return wrapperGroup; const centerX = width / 2,
} centerY = height / 2,
boundCenterX = viewBound.x + viewBound.width / 2;
let dx = centerX - boundCenterX,
dy = 0;
/** if (this.viewContainer.hasLeak) {
* const boundBottomY = viewBound.y + viewBound.height;
* @param models dy = height - leakAreaHeight - 100 - boundBottomY;
* @param leakAreaHeight } else {
*/ const boundCenterY = viewBound.y + viewBound.height / 2;
private fitCenter(group: Group) { dy = centerY - boundCenterY;
let width = this.viewContainer.getG6Instance().getWidth(), }
height = this.viewContainer.getG6Instance().getHeight(),
leakAreaHeight = this.engine.viewOptions.leakAreaHeight;
if (this.viewContainer.hasLeak) { group.translate(dx, dy);
height = height - leakAreaHeight; }
}
const viewBound: BoundingRect = group.getBound(), /**
centerX = width / 2, centerY = height / 2, *
boundCenterX = viewBound.x + viewBound.width / 2, * @param layoutGroupTable
boundCenterY = viewBound.y + viewBound.height / 2, * @param leakModels
dx = centerX - boundCenterX, * @param hasLeak
dy = centerY - boundCenterY; * @param needFitCenter
*/
public layoutAll(layoutGroupTable: LayoutGroupTable, accumulateLeakModels: SVModel[], leakModels: SVModel[]) {
this.preLayoutProcess(layoutGroupTable);
group.translate(dx, dy); const modelGroupList: Group[] = this.layoutModels(layoutGroupTable);
} const generalGroup: Group = this.layoutGroups(modelGroupList);
if (leakModels.length) {
this.layoutLeakModels(leakModels, accumulateLeakModels);
}
this.fitCenter(generalGroup);
/** this.postLayoutProcess(layoutGroupTable);
* }
* @param layoutGroupTable
* @param leakModels
* @param hasLeak
* @param needFitCenter
*/
public layoutAll(layoutGroupTable: LayoutGroupTable, accumulateLeakModels: SVModel[], leakModels: SVModel[]) {
this.preLayoutProcess(layoutGroupTable);
const modelGroupList: Group[] = this.layoutModels(layoutGroupTable);
const generalGroup: Group = this.layoutGroups(modelGroupList);
if (leakModels.length) {
this.layoutLeakModels(leakModels, accumulateLeakModels);
}
this.fitCenter(generalGroup);
this.postLayoutProcess(layoutGroupTable);
}
} }

View File

@ -140,7 +140,7 @@ export class ViewContainer {
* @param models * @param models
* @param layoutFn * @param layoutFn
*/ */
render(layoutGroupTable: LayoutGroupTable) { render(layoutGroupTable: LayoutGroupTable, a: boolean) {
const modelList = Util.convertGroupTable2ModelList(layoutGroupTable), const modelList = Util.convertGroupTable2ModelList(layoutGroupTable),
diffResult = this.reconcile.diff(this.layoutGroupTable, this.prevModelList, modelList, this.accumulateLeakModels), diffResult = this.reconcile.diff(this.layoutGroupTable, this.prevModelList, modelList, this.accumulateLeakModels),
renderModelList = [ renderModelList = [

View File

@ -67,7 +67,7 @@ export class Engine {
const layoutGroupTable = this.modelConstructor.construct(source); const layoutGroupTable = this.modelConstructor.construct(source);
// 2 渲染使用g6进行渲染 // 2 渲染使用g6进行渲染
this.viewContainer.render(layoutGroupTable); this.viewContainer.render(layoutGroupTable, source.enterFunction as boolean);
} }

View File

@ -16,6 +16,7 @@ export interface SourceNode {
export type Sources = { export type Sources = {
enterFunction: any;
[key: string]: { [key: string]: {
data: SourceNode[]; data: SourceNode[];
layouter: string; layouter: string;