mirror of
https://github.com/Powerful-517/yys-editor.git
synced 2026-03-05 15:05:27 +00:00
perf(vector-node): batch resize sync and cut redundant rerenders
This commit is contained in:
@@ -1,120 +1,95 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onBeforeUnmount } from 'vue';
|
||||
import { computed, onBeforeUnmount, ref } from 'vue';
|
||||
import { useNodeAppearance } from '@/ts/useNodeAppearance';
|
||||
import {
|
||||
DEFAULT_VECTOR_CONFIG,
|
||||
buildNextVectorConfig,
|
||||
createRafLatestScheduler,
|
||||
type VectorConfig
|
||||
} from './vectorNodeSync';
|
||||
|
||||
const vectorConfig = ref({
|
||||
kind: 'rect',
|
||||
svgContent: '',
|
||||
path: '',
|
||||
tileWidth: 50,
|
||||
tileHeight: 50,
|
||||
fill: '#409EFF',
|
||||
stroke: '#303133',
|
||||
strokeWidth: 1
|
||||
const vectorConfig = ref<VectorConfig>({ ...DEFAULT_VECTOR_CONFIG });
|
||||
const patternId = `vector-pattern-${Math.random().toString(36).slice(2, 11)}`;
|
||||
|
||||
const syncVectorConfig = createRafLatestScheduler<VectorConfig>((nextConfig) => {
|
||||
vectorConfig.value = nextConfig;
|
||||
});
|
||||
|
||||
const nodeSize = ref({ width: 200, height: 200 });
|
||||
let syncRafId: number | null = null;
|
||||
let pendingVectorConfig: Record<string, any> | null = null;
|
||||
let pendingNodeSize: { width: number; height: number } | null = null;
|
||||
|
||||
const flushPendingSync = () => {
|
||||
if (pendingVectorConfig) {
|
||||
Object.assign(vectorConfig.value, pendingVectorConfig);
|
||||
pendingVectorConfig = null;
|
||||
}
|
||||
if (pendingNodeSize) {
|
||||
nodeSize.value = pendingNodeSize;
|
||||
pendingNodeSize = null;
|
||||
}
|
||||
syncRafId = null;
|
||||
};
|
||||
|
||||
const scheduleSync = () => {
|
||||
if (typeof requestAnimationFrame === 'undefined') {
|
||||
flushPendingSync();
|
||||
return;
|
||||
}
|
||||
if (syncRafId !== null) {
|
||||
cancelAnimationFrame(syncRafId);
|
||||
}
|
||||
syncRafId = requestAnimationFrame(flushPendingSync);
|
||||
};
|
||||
|
||||
const { containerStyle } = useNodeAppearance({
|
||||
onPropsChange(props, node) {
|
||||
if (props.vector) {
|
||||
pendingVectorConfig = { ...props.vector };
|
||||
onPropsChange(props) {
|
||||
const nextConfig = buildNextVectorConfig(vectorConfig.value, props?.vector);
|
||||
if (nextConfig) {
|
||||
syncVectorConfig.enqueue(nextConfig);
|
||||
}
|
||||
if (node) {
|
||||
pendingNodeSize = {
|
||||
width: node.width,
|
||||
height: node.height
|
||||
};
|
||||
}
|
||||
// 使用 requestAnimationFrame 防抖,减少快速缩放时的重复重绘
|
||||
scheduleSync();
|
||||
}
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (syncRafId !== null && typeof cancelAnimationFrame !== 'undefined') {
|
||||
cancelAnimationFrame(syncRafId);
|
||||
syncRafId = null;
|
||||
}
|
||||
syncVectorConfig.cancel();
|
||||
});
|
||||
|
||||
const patternId = `vector-pattern-${Math.random().toString(36).slice(2, 11)}`;
|
||||
|
||||
// 生成 SVG 内容
|
||||
const svgContent = computed(() => {
|
||||
const { kind, path, tileWidth, tileHeight, fill, stroke, strokeWidth } = vectorConfig.value;
|
||||
|
||||
let shapeElement = '';
|
||||
switch (kind) {
|
||||
case 'rect':
|
||||
shapeElement = `<rect x="0" y="0" width="${tileWidth}" height="${tileHeight}"
|
||||
fill="${fill}" stroke="${stroke}" stroke-width="${strokeWidth}" />`;
|
||||
break;
|
||||
case 'ellipse':
|
||||
shapeElement = `<ellipse cx="${tileWidth/2}" cy="${tileHeight/2}"
|
||||
rx="${tileWidth/2 - strokeWidth}" ry="${tileHeight/2 - strokeWidth}"
|
||||
fill="${fill}" stroke="${stroke}" stroke-width="${strokeWidth}" />`;
|
||||
break;
|
||||
case 'path':
|
||||
shapeElement = `<path d="${path || 'M 0 0'}" fill="${fill}" stroke="${stroke}" stroke-width="${strokeWidth}" />`;
|
||||
break;
|
||||
case 'svg':
|
||||
shapeElement = vectorConfig.value.svgContent || '';
|
||||
break;
|
||||
case 'polygon':
|
||||
// 默认三角形
|
||||
const points = `0,${tileHeight} ${tileWidth/2},0 ${tileWidth},${tileHeight}`;
|
||||
shapeElement = `<polygon points="${points}" fill="${fill}" stroke="${stroke}" stroke-width="${strokeWidth}" />`;
|
||||
break;
|
||||
}
|
||||
|
||||
return `
|
||||
<svg width="${nodeSize.value.width}" height="${nodeSize.value.height}"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<pattern id="${patternId}"
|
||||
x="0" y="0"
|
||||
width="${tileWidth}"
|
||||
height="${tileHeight}"
|
||||
patternUnits="userSpaceOnUse">
|
||||
${shapeElement}
|
||||
</pattern>
|
||||
</defs>
|
||||
<rect width="100%" height="100%" fill="url(#${patternId})" />
|
||||
</svg>
|
||||
`;
|
||||
});
|
||||
const ellipseCx = computed(() => vectorConfig.value.tileWidth / 2);
|
||||
const ellipseCy = computed(() => vectorConfig.value.tileHeight / 2);
|
||||
const ellipseRx = computed(() => Math.max(0, vectorConfig.value.tileWidth / 2 - vectorConfig.value.strokeWidth));
|
||||
const ellipseRy = computed(() => Math.max(0, vectorConfig.value.tileHeight / 2 - vectorConfig.value.strokeWidth));
|
||||
const polygonPoints = computed(
|
||||
() => `0,${vectorConfig.value.tileHeight} ${vectorConfig.value.tileWidth / 2},0 ${vectorConfig.value.tileWidth},${vectorConfig.value.tileHeight}`
|
||||
);
|
||||
const safePath = computed(() => vectorConfig.value.path || 'M 0 0');
|
||||
const patternFill = computed(() => `url(#${patternId})`);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="vector-node" :style="containerStyle">
|
||||
<div class="vector-content" v-html="svgContent"></div>
|
||||
<svg class="vector-content" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="none">
|
||||
<defs>
|
||||
<pattern
|
||||
:id="patternId"
|
||||
x="0"
|
||||
y="0"
|
||||
:width="vectorConfig.tileWidth"
|
||||
:height="vectorConfig.tileHeight"
|
||||
patternUnits="userSpaceOnUse"
|
||||
>
|
||||
<rect
|
||||
v-if="vectorConfig.kind === 'rect'"
|
||||
x="0"
|
||||
y="0"
|
||||
:width="vectorConfig.tileWidth"
|
||||
:height="vectorConfig.tileHeight"
|
||||
:fill="vectorConfig.fill"
|
||||
:stroke="vectorConfig.stroke"
|
||||
:stroke-width="vectorConfig.strokeWidth"
|
||||
/>
|
||||
<ellipse
|
||||
v-else-if="vectorConfig.kind === 'ellipse'"
|
||||
:cx="ellipseCx"
|
||||
:cy="ellipseCy"
|
||||
:rx="ellipseRx"
|
||||
:ry="ellipseRy"
|
||||
:fill="vectorConfig.fill"
|
||||
:stroke="vectorConfig.stroke"
|
||||
:stroke-width="vectorConfig.strokeWidth"
|
||||
/>
|
||||
<path
|
||||
v-else-if="vectorConfig.kind === 'path'"
|
||||
:d="safePath"
|
||||
:fill="vectorConfig.fill"
|
||||
:stroke="vectorConfig.stroke"
|
||||
:stroke-width="vectorConfig.strokeWidth"
|
||||
/>
|
||||
<g v-else-if="vectorConfig.kind === 'svg'" v-html="vectorConfig.svgContent || ''"></g>
|
||||
<polygon
|
||||
v-else
|
||||
:points="polygonPoints"
|
||||
:fill="vectorConfig.fill"
|
||||
:stroke="vectorConfig.stroke"
|
||||
:stroke-width="vectorConfig.strokeWidth"
|
||||
/>
|
||||
</pattern>
|
||||
</defs>
|
||||
<rect width="100%" height="100%" :fill="patternFill" />
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -128,11 +103,6 @@ const svgContent = computed(() => {
|
||||
}
|
||||
|
||||
.vector-content {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.vector-content :deep(svg) {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
Reference in New Issue
Block a user