,
- themeMode?: 'radius' | 'dark' | 'colorful' | 'default' | string,
+ themeMode?: LogicFlow.ThemeMode | string,
) {
if (themeMode) {
+ this.themeMode = themeMode
// 修改背景颜色
backgroundModeMap[themeMode] &&
this.updateBackgroundOptions({
diff --git a/packages/core/src/model/TransformModel.ts b/packages/core/src/model/TransformModel.ts
index 3ab82fcff..9f918f1e8 100644
--- a/packages/core/src/model/TransformModel.ts
+++ b/packages/core/src/model/TransformModel.ts
@@ -46,15 +46,15 @@ const translateLimitsMap = {
}
export class TransformModel implements TransformInterface {
- MINI_SCALE_SIZE = 0.2
- MAX_SCALE_SIZE = 16
- @observable SCALE_X = 1
- @observable SKEW_Y = 0
- @observable SKEW_X = 0
- @observable SCALE_Y = 1
- @observable TRANSLATE_X = 0
- @observable TRANSLATE_Y = 0
- @observable ZOOM_SIZE = 0.04
+ MINI_SCALE_SIZE = 0.2 // 缩小的最小值
+ MAX_SCALE_SIZE = 16 // 放大的最大值
+ @observable SCALE_X = 1 // x轴缩放比例
+ @observable SKEW_Y = 0 // y轴倾斜角度
+ @observable SKEW_X = 0 // x轴倾斜角度
+ @observable SCALE_Y = 1 // y轴缩放比例
+ @observable TRANSLATE_X = 0 // x轴平移距离
+ @observable TRANSLATE_Y = 0 // y轴平移距离
+ @observable ZOOM_SIZE = 0.04 // 缩放比例变化量
eventCenter: EventEmitter
// 限制画布可移动区域
diff --git a/packages/core/src/model/edge/BaseEdgeModel.ts b/packages/core/src/model/edge/BaseEdgeModel.ts
index d6f670129..201d5fd51 100644
--- a/packages/core/src/model/edge/BaseEdgeModel.ts
+++ b/packages/core/src/model/edge/BaseEdgeModel.ts
@@ -265,8 +265,16 @@ export class BaseEdgeModel
*/
getOutlineStyle(): LogicFlow.OutlineTheme {
const { graphModel } = this
- const { outline } = graphModel.theme
- return cloneDeep(outline)
+ const { edgeOutline } = graphModel.theme
+ let attributes = { ...edgeOutline }
+ if (this.isHovered) {
+ const hoverStyle = edgeOutline.hover || {}
+ attributes = {
+ ...attributes,
+ ...hoverStyle,
+ }
+ }
+ return cloneDeep(attributes)
}
/**
diff --git a/packages/core/src/model/edge/PolylineEdgeModel.ts b/packages/core/src/model/edge/PolylineEdgeModel.ts
index 138c9a219..c19724a22 100644
--- a/packages/core/src/model/edge/PolylineEdgeModel.ts
+++ b/packages/core/src/model/edge/PolylineEdgeModel.ts
@@ -34,13 +34,38 @@ export class PolylineEdgeModel extends BaseEdgeModel {
@observable dbClickPosition?: Point
initEdgeData(data: LogicFlow.EdgeConfig): void {
- this.offset = get(data, 'properties.offset', 30)
+ const providedOffset = get(data, 'properties.offset')
+ // 当用户未传入 offset 时,按“箭头与折线重叠长度 + 10”作为默认值
+ // 其中“重叠长度”采用箭头样式中的 offset(沿边方向的长度)
+ this.offset =
+ typeof providedOffset === 'number'
+ ? providedOffset
+ : this.getDefaultOffset()
if (data.pointsList) {
this.pointsList = data.pointsList
}
super.initEdgeData(data)
}
+ setAttributes() {
+ const { offset: newOffset } = this.properties
+ if (newOffset && newOffset !== this.offset) {
+ this.offset = newOffset
+ this.updatePoints()
+ }
+ }
+
+ /**
+ * 计算默认 offset:箭头与折线重叠长度 + 10
+ * 重叠长度采用箭头样式中的 offset(沿边方向的长度)
+ */
+ private getDefaultOffset(): number {
+ const arrowStyle = this.getArrowStyle()
+ const arrowOverlap =
+ typeof arrowStyle.offset === 'number' ? arrowStyle.offset : 0
+ return arrowOverlap + 5
+ }
+
getEdgeStyle() {
const { polyline } = this.graphModel.theme
const style = super.getEdgeStyle()
diff --git a/packages/core/src/model/node/BaseNodeModel.ts b/packages/core/src/model/node/BaseNodeModel.ts
index 112ce268c..31fef16cc 100644
--- a/packages/core/src/model/node/BaseNodeModel.ts
+++ b/packages/core/src/model/node/BaseNodeModel.ts
@@ -433,7 +433,15 @@ export class BaseNodeModel
getResizeOutlineStyle() {
const { resizeOutline } = this.graphModel.theme
- return cloneDeep(resizeOutline)
+ let attributes = { ...resizeOutline }
+ if (this.isHovered) {
+ const hoverStyle = resizeOutline.hover || {}
+ attributes = {
+ ...attributes,
+ ...hoverStyle,
+ }
+ }
+ return cloneDeep(attributes)
}
/**
diff --git a/packages/core/src/model/node/HtmlNodeModel.ts b/packages/core/src/model/node/HtmlNodeModel.ts
index d715282c0..a5c79e443 100644
--- a/packages/core/src/model/node/HtmlNodeModel.ts
+++ b/packages/core/src/model/node/HtmlNodeModel.ts
@@ -1,3 +1,4 @@
+import { cloneDeep } from 'lodash-es'
import BaseNodeModel from './BaseNodeModel'
import { Model } from '../BaseModel'
import { ModelType } from '../../constant'
@@ -45,6 +46,18 @@ export class HtmlNodeModel<
{ x: x - width / 2, y, id: `${this.id}_3` },
]
}
+
+ getNodeStyle() {
+ const style = super.getNodeStyle()
+ const { rect, html } = this.graphModel.theme
+ const { style: customStyle = {} } = this.properties
+ return {
+ ...style,
+ ...cloneDeep(rect),
+ ...cloneDeep(html),
+ ...cloneDeep(customStyle),
+ }
+ }
}
export default HtmlNodeModel
diff --git a/packages/core/src/model/node/PolygonNodeModel.ts b/packages/core/src/model/node/PolygonNodeModel.ts
index 017eea645..d32b8a720 100644
--- a/packages/core/src/model/node/PolygonNodeModel.ts
+++ b/packages/core/src/model/node/PolygonNodeModel.ts
@@ -67,12 +67,14 @@ export class PolygonNodeModel<
const {
graphModel: {
theme: { polygon },
+ customStyles: { polygon: customPolygon },
},
} = this
const { style: customStyle = {} } = this.properties
return {
...style,
...cloneDeep(polygon),
+ ...cloneDeep(customPolygon),
...cloneDeep(customStyle),
}
}
diff --git a/packages/core/src/options.ts b/packages/core/src/options.ts
index 61c7565d6..bf634ee10 100644
--- a/packages/core/src/options.ts
+++ b/packages/core/src/options.ts
@@ -105,7 +105,7 @@ export namespace Options {
edgeGenerator?: EdgeGeneratorType
customTrajectory?: (props: CustomAnchorLineProps) => h.JSX.Element
- themeMode?: 'radius' | 'dark' | 'colorful' // 主题模式
+ themeMode?: LogicFlow.ThemeMode // 主题模式
parentTransform?: TransformModel // 父级变换模型,用于嵌套变换
diff --git a/packages/core/src/util/edge.ts b/packages/core/src/util/edge.ts
index d8653c8b6..cb65f0637 100644
--- a/packages/core/src/util/edge.ts
+++ b/packages/core/src/util/edge.ts
@@ -159,6 +159,7 @@ export const mergeBBox = (b1: BoxBounds, b2: BoxBounds): BoxBounds => {
export const getBBoxOfPoints = (
points: Point[] = [],
offset?: number,
+ heightOffset?: number,
): BoxBounds => {
const xList: number[] = []
const yList: number[] = []
@@ -174,7 +175,7 @@ export const getBBoxOfPoints = (
let height = maxY - minY
if (offset) {
width += offset
- height += offset
+ height += heightOffset || offset
}
return {
centerX: (minX + maxX) / 2,
diff --git a/packages/core/src/util/geometry.ts b/packages/core/src/util/geometry.ts
index e9f7321af..1fe44436c 100644
--- a/packages/core/src/util/geometry.ts
+++ b/packages/core/src/util/geometry.ts
@@ -1,5 +1,6 @@
import LogicFlow from '../LogicFlow'
import PointTuple = LogicFlow.PointTuple
+import Point = LogicFlow.Point
export function snapToGrid(point: number, gridSize: number, snapGrid: boolean) {
// 开启节网格对齐时才根据网格尺寸校准坐标
@@ -53,3 +54,101 @@ export function normalizePolygon(
// 缩放顶点
return translatedPoints.map(([x, y]) => [x * scaleFactor, y * scaleFactor])
}
+
+/**
+ * 通用圆角生成:为菱形、多边形、折线在转折处生成与矩形视觉一致的圆角
+ * - 圆角基于角平分线,切点距顶点的距离 t = r * tan(theta/2)
+ * - 半径会根据相邻边长度进行钳制,避免超过边长造成断裂
+ * - 多边形/菱形保持闭合;折线保持开口
+ */
+
+export const generateRoundedCorners = (
+ points: Point[],
+ radius: number,
+ isClosedShape: boolean, // 是否是闭合图形
+): Point[] => {
+ const n = points.length
+ if (n < 2 || radius <= 0) return points.slice()
+
+ const toVec = (a: Point, b: Point) => ({ x: b.x - a.x, y: b.y - a.y })
+ const len = (v: { x: number; y: number }) => Math.hypot(v.x, v.y)
+ const norm = (v: { x: number; y: number }) => {
+ const l = len(v) || 1
+ return { x: v.x / l, y: v.y / l }
+ }
+
+ const result: Point[] = []
+
+ // 用二次贝塞尔近似圆角,控制点取角点,避免复杂圆心计算
+ const makeRoundCorner = (prev: Point, curr: Point, next: Point): Point[] => {
+ const vPrev = toVec(curr, prev)
+ const vNext = toVec(curr, next)
+ const dPrev = len(vPrev)
+ const dNext = len(vNext)
+ if (dPrev < 1e-6 || dNext < 1e-6) return [curr]
+
+ const uPrev = norm(vPrev)
+ const uNext = norm(vNext)
+ const t = Math.min(radius, dPrev * 0.45, dNext * 0.45)
+
+ const start = { x: curr.x + uPrev.x * t, y: curr.y + uPrev.y * t }
+ const end = { x: curr.x + uNext.x * t, y: curr.y + uNext.y * t }
+
+ // 二次贝塞尔采样:B(s) = (1-s)^2*start + 2(1-s)s*curr + s^2*end
+ const steps = 10 // 3段近似,简洁且效果稳定
+ const pts: Point[] = [start]
+ for (let k = 1; k < steps; k++) {
+ const s = k / steps
+ const a = 1 - s
+ pts.push({
+ x: a * a * start.x + 2 * a * s * curr.x + s * s * end.x,
+ y: a * a * start.y + 2 * a * s * curr.y + s * s * end.y,
+ })
+ }
+ pts.push(end)
+ return pts
+ }
+
+ for (let i = 0; i < n; i++) {
+ const prevIdx = i === 0 ? (isClosedShape ? n - 1 : 0) : i - 1
+ const nextIdx = i === n - 1 ? (isClosedShape ? 0 : n - 1) : i + 1
+ const prev = points[prevIdx]
+ const curr = points[i]
+ const next = points[nextIdx]
+
+ const isEndpoint = !isClosedShape && (i === 0 || i === n - 1)
+ if (isEndpoint) {
+ // 折线两端不处理圆角
+ result.push(curr)
+ } else {
+ const arc = makeRoundCorner(prev, curr, next)
+ arc.forEach((p) => result.push(p))
+ }
+ }
+
+ // 去重处理:避免连续重复点
+ const dedup: Point[] = []
+ for (let i = 0; i < result.length; i++) {
+ const p = result[i]
+ if (
+ dedup.length === 0 ||
+ Math.hypot(
+ p.x - dedup[dedup.length - 1].x,
+ p.y - dedup[dedup.length - 1].y,
+ ) > 1e-6
+ ) {
+ dedup.push(p)
+ }
+ }
+
+ // 闭合图形:确保首尾不重复闭合
+ if (isClosedShape && dedup.length > 1) {
+ const first = dedup[0]
+ const last = dedup[dedup.length - 1]
+ if (Math.hypot(first.x - last.x, first.y - last.y) < 1e-6) {
+ dedup.pop()
+ }
+ }
+
+ return dedup
+}
diff --git a/packages/core/src/util/theme.ts b/packages/core/src/util/theme.ts
index 3a471b5a6..0dd457810 100644
--- a/packages/core/src/util/theme.ts
+++ b/packages/core/src/util/theme.ts
@@ -1,312 +1,21 @@
import { cloneDeep, merge, assign } from 'lodash-es'
import LogicFlow from '../LogicFlow'
-
-export const defaultTheme: LogicFlow.Theme = {
- baseNode: {
- fill: '#fff',
- stroke: '#000',
- strokeWidth: 2,
- },
-
- baseEdge: {
- stroke: '#000',
- strokeWidth: 2,
- },
-
- rect: {},
- circle: {},
- diamond: {},
- ellipse: {},
- polygon: {},
-
- text: {
- color: '#000',
- stroke: 'none',
- fontSize: 12,
- background: {
- fill: 'transparent',
- },
- },
-
- anchor: {
- stroke: '#000',
- fill: '#fff',
- r: 4,
- hover: {
- r: 10,
- fill: '#949494',
- fillOpacity: 0.5,
- stroke: '#949494',
- },
- },
-
- anchorLine: {
- stroke: '#000',
- strokeWidth: 2,
- strokeDasharray: '3,2',
- },
-
- nodeText: {
- color: '#000',
- overflowMode: 'default',
- fontSize: 12,
- lineHeight: 1.2,
- },
-
- edgeText: {
- textWidth: 100,
- overflowMode: 'default',
- fontSize: 12,
- background: {
- fill: '#fff',
- },
- },
-
- line: {},
- polyline: {},
-
- bezier: {
- fill: 'none',
- adjustLine: {
- stroke: '#949494',
- },
- adjustAnchor: {
- r: 4,
- fill: '#949494',
- fillOpacity: 1,
- stroke: '#949494',
- },
- },
-
- arrow: {
- offset: 10,
- verticalLength: 5, // 箭头垂直于边的距离
- },
-
- snapline: {
- stroke: '#949494',
- strokeWidth: 1,
- },
-
- edgeAdjust: {
- r: 4,
- fill: '#fff',
- stroke: '#949494',
- strokeWidth: 2,
- },
-
- outline: {
- fill: 'transparent',
- stroke: '#949494',
- strokeDasharray: '3,3',
- hover: {
- stroke: '#949494',
- },
- },
-
- edgeAnimation: {
- stroke: 'red',
- strokeDasharray: '10,10',
- strokeDashoffset: '100%',
- animationName: 'lf_animate_dash',
- animationDuration: '20s',
- animationIterationCount: 'infinite',
- animationTimingFunction: 'linear',
- animationDirection: 'normal',
- },
-
- rotateControl: {
- stroke: '#000',
- fill: '#fff',
- strokeWidth: 1.5,
- },
-
- resizeControl: {
- width: 7,
- height: 7,
- fill: '#fff',
- stroke: '#000',
- },
-
- resizeOutline: {
- fill: 'none',
- stroke: 'transparent', // 矩形默认不显示调整边框
- strokeWidth: 1,
- strokeDasharray: '3,3',
- },
-}
-export const radiusMode: any = {
- rect: { radius: 8 },
- diamond: { radius: 8 },
- polygon: { radius: 8 },
- polyline: { radius: 8 },
- arrow: {
- strokeLinecap: 'round',
- strokeLinejoin: 'round',
- offset: 10,
- verticalLength: 5, // 箭头垂直于边的距离
- },
- snapline: {
- strokeLinecap: 'round',
- strokeLinejoin: 'round',
- stroke: '#949494',
- strokeWidth: 1,
- },
- outline: {
- radius: 8,
- fill: 'transparent',
- stroke: '#949494',
- strokeDasharray: '3,3',
- hover: {
- stroke: '#949494',
- },
- },
- resizeOutline: {
- radius: 8,
- fill: 'none',
- stroke: 'transparent', // 矩形默认不显示调整边框
- strokeWidth: 1,
- strokeDasharray: '3,3',
- },
-}
-export const darkMode: any = {
- baseNode: {
- fill: '#23272e',
- stroke: '#fefeff',
- },
- baseEdge: {
- stroke: '#fefeff',
- },
- rect: { radius: 8 },
- diamond: { radius: 8 },
- polygon: { radius: 8 },
- polyline: { radius: 8 },
- nodeText: {
- color: '#fefeff',
- overflowMode: 'default',
- fontSize: 12,
- lineHeight: 1.2,
- },
- arrow: {
- strokeLinecap: 'round',
- strokeLinejoin: 'round',
- offset: 10,
- verticalLength: 5, // 箭头垂直于边的距离
- },
- snapline: {
- strokeLinecap: 'round',
- strokeLinejoin: 'round',
- stroke: '#949494',
- strokeWidth: 1,
- },
- outline: {
- radius: 8,
- fill: 'transparent',
- stroke: '#949494',
- strokeDasharray: '3,3',
- hover: {
- stroke: '#949494',
- },
- },
- resizeOutline: {
- radius: 8,
- fill: 'none',
- stroke: 'transparent', // 矩形默认不显示调整边框
- strokeWidth: 1,
- strokeDasharray: '3,3',
- },
-}
-export const colorfulMode: any = {
- rect: { fill: '#72CBFF', stroke: '#3ABDF9', radius: 8 },
- circle: { fill: '#FFE075', stroke: '#F9CE3A', radius: 8 },
- ellipse: { fill: '#FFA8A8', stroke: '#FF6B66', radius: 8 },
- text: { fill: '#72CBFF', radius: 8 },
- diamond: { fill: '#96F7AF', stroke: '#40EF7E', radius: 8 },
- polygon: { fill: '#E0A8FF', stroke: '#C271FF', radius: 8 },
- polyline: { radius: 8 },
- arrow: {
- strokeLinecap: 'round',
- strokeLinejoin: 'round',
- offset: 10,
- verticalLength: 5, // 箭头垂直于边的距离
- },
- snapline: {
- strokeLinecap: 'round',
- strokeLinejoin: 'round',
- stroke: '#949494',
- strokeWidth: 1,
- },
- outline: {
- radius: 8,
- fill: 'transparent',
- stroke: '#949494',
- strokeDasharray: '3,3',
- hover: {
- stroke: '#949494',
- },
- },
- resizeOutline: {
- radius: 8,
- fill: 'none',
- stroke: 'transparent', // 矩形默认不显示调整边框
- strokeWidth: 1,
- strokeDasharray: '3,3',
- },
-}
-
-export const themeModeMap = {
- colorful: colorfulMode,
- dark: darkMode,
- radius: radiusMode,
- default: defaultTheme,
-}
-
-// 不同主题的背景色
-export const darkBackground = {
- background: '#23272e',
-}
-export const colorfulBackground = {
- background: '#fefeff',
-}
-export const defaultBackground = {
- background: '#ffffff',
-}
-export const backgroundModeMap = {
- colorful: colorfulBackground,
- dark: darkBackground,
- radius: defaultBackground,
- default: defaultBackground,
-}
-
-// 不同主题的网格样式
-export const darkGrid = {
- color: '#66676a',
- thickness: 1,
-}
-export const colorfulGrid = {
- color: '#dadada',
- thickness: 1,
-}
-export const defaultGrid = {
- color: '#acacac',
- thickness: 1,
-}
-export const gridModeMap = {
- colorful: colorfulGrid,
- dark: darkGrid,
- radius: defaultGrid,
- default: defaultGrid,
-}
+import {
+ themeModeMap,
+ backgroundModeMap,
+ defaultBackground,
+ gridModeMap,
+ defaultGrid,
+} from '../constant/theme'
/* 主题(全局样式)相关工具方法 */
export const setupTheme = (
customTheme?: Partial,
- themeMode?: 'radius' | 'dark' | 'colorful' | 'default' | string,
+ themeMode?: LogicFlow.ThemeMode | string,
): LogicFlow.Theme => {
- let theme = cloneDeep(defaultTheme)
- if (themeMode) {
- theme = merge(theme, themeModeMap[themeMode])
- }
+ let theme =
+ cloneDeep(themeModeMap[themeMode || 'default']) ||
+ cloneDeep(themeModeMap.default)
if (customTheme) {
/**
* 为了不让默认样式被覆盖,使用 merge 方法
@@ -363,7 +72,7 @@ export const clearThemeMode = (): void => {
const resetTheme = {
colorful: {},
dark: {},
- radius: {},
+ retro: {},
default: {},
}
assign(themeModeMap, resetTheme)
diff --git a/packages/core/src/view/Control.tsx b/packages/core/src/view/Control.tsx
index 81cdd52d5..8611a2153 100644
--- a/packages/core/src/view/Control.tsx
+++ b/packages/core/src/view/Control.tsx
@@ -389,8 +389,8 @@ export class ResizeControl extends Component<
className="lf-resize-control-content"
x={x}
y={y}
- width={25}
- height={25}
+ width={width ? width + 5 : 25}
+ height={width ? width + 5 : 25}
fill="transparent"
stroke="transparent"
onPointerDown={this.dragHandler.handleMouseDown}
@@ -414,31 +414,32 @@ export class ResizeControlGroup extends Component {
getResizeControl(): h.JSX.Element[] {
const { model, graphModel } = this.props
const { minX, minY, maxX, maxY } = getNodeBBox(model)
+ const { width = 8, height = 8 } = model.getResizeControlStyle()
const controlList: ControlItemProps[] = [
{
index: ResizeControlIndex.LEFT_TOP,
direction: 'nw',
- x: minX,
- y: minY,
+ x: minX - width / 2,
+ y: minY - height / 2,
}, // 左上角
{
index: ResizeControlIndex.RIGHT_TOP,
direction: 'ne',
- x: maxX,
- y: minY,
+ x: maxX + width / 2,
+ y: minY - height / 2,
}, // 右上角
{
index: ResizeControlIndex.RIGHT_BOTTOM,
direction: 'se',
- x: maxX,
- y: maxY,
+ x: maxX + width / 2,
+ y: maxY + height / 2,
}, // 右下角
{
index: ResizeControlIndex.LEFT_BOTTOM,
direction: 'sw',
- x: minX,
- y: maxY,
+ x: minX - width / 2,
+ y: maxY + height / 2,
}, // 左下角
]
@@ -451,7 +452,9 @@ export class ResizeControlGroup extends Component {
const { model } = this.props
const { x, y, width, height } = model
const style = model.getResizeOutlineStyle()
- return
+ return (
+
+ )
}
render(): h.JSX.Element {
diff --git a/packages/core/src/view/edge/PolylineEdge.tsx b/packages/core/src/view/edge/PolylineEdge.tsx
index 02079d804..61b302e6e 100644
--- a/packages/core/src/view/edge/PolylineEdge.tsx
+++ b/packages/core/src/view/edge/PolylineEdge.tsx
@@ -5,6 +5,7 @@ import LogicFlow from '../../LogicFlow'
import { GraphModel, PolylineEdgeModel } from '../../model'
import { EventType, SegmentDirection } from '../../constant'
import { StepDrag, points2PointsList } from '../../util'
+import { generateRoundedCorners } from '../../util/geometry'
import { getVerticalPointOfLine } from '../../algorithm'
import ArrowInfo = LogicFlow.ArrowInfo
@@ -110,7 +111,7 @@ export class PolylineEdge extends BaseEdge {
*/
getEdge() {
const { model } = this.props
- const { points, isAnimation, arrowConfig } = model
+ const { points, isAnimation, arrowConfig, properties } = model
const style = model.getEdgeStyle()
const animationStyle = model.getEdgeAnimationStyle()
const {
@@ -123,9 +124,20 @@ export class PolylineEdge extends BaseEdge {
animationTimingFunction,
animationDirection,
} = animationStyle
+ // 应用通用圆角:当存在样式半径时,为折线拐点生成圆角
+ const radius: number = (properties?.radius ??
+ (style as any)?.radius ??
+ 0) as number
+ const roundedPointsStr = (() => {
+ if (!radius || radius <= 0) return points
+ const list = points2PointsList(points)
+ const rounded = generateRoundedCorners(list, radius, false)
+ return rounded.map((p) => `${p.x},${p.y}`).join(' ')
+ })()
+
return (
+
+ {style.shadow && (
+
+
+
+
+
+ )}
+
+
+
)
}
}
diff --git a/packages/core/src/view/overlay/Grid.tsx b/packages/core/src/view/overlay/Grid.tsx
index c9e292dff..0541b5d76 100644
--- a/packages/core/src/view/overlay/Grid.tsx
+++ b/packages/core/src/view/overlay/Grid.tsx
@@ -1,6 +1,7 @@
import { Component } from 'preact/compat'
import { cloneDeep, assign } from 'lodash-es'
import { observer } from '../..'
+import { mergeMajorBoldConfig, MajorBoldConfig } from './gridConfig'
import { createUuid } from '../../util'
import { GraphModel } from '../../model'
import { DEFAULT_GRID_SIZE } from '../../constant'
@@ -39,25 +40,118 @@ export class Grid extends Component {
)
}
+ // 计算与 size 整除的虚线周期,使边长能被 (dash + gap) 完整分割
+ private getDashArrayForSize(
+ size: number,
+ strokeWidth: number,
+ dashCfg?: { baseSize: number; pattern: number[] },
+ ): string {
+ // 若提供了直接可用的 pattern,则优先使用
+ const direct = dashCfg?.pattern?.filter(
+ (n) => typeof n === 'number' && n > 0,
+ )
+ if (direct && direct.length >= 2) return direct.join(',')
+
+ // 目标每条边显示的虚线段数量区间
+ const minSegments = 4
+ const maxSegments = 16
+ const targetCycle = Math.max(1, dashCfg?.baseSize ?? 8)
+
+ // 计算 segments,使得 size / segments ≈ targetCycle,并限制在区间内
+ let segments = Math.round(size / targetCycle)
+ segments = Math.max(minSegments, Math.min(maxSegments, segments))
+
+ // 一个周期 = dash + gap,确保周期整除 size
+ const cycle = size / segments
+ // dash/gap 尽量平均,且 dash 不小于线宽
+ const dashLen = Math.max(strokeWidth, cycle / 2)
+ const gapLen = Math.max(1, cycle - dashLen)
+ return `${dashLen},${gapLen}`
+ }
+
+ // 计算一个周期内的最大加粗索引(作为周期大小)
+ private getPeriod(advanced: any): number {
+ const list = Array.isArray(advanced?.boldIndices)
+ ? advanced.boldIndices.filter((n: any) => typeof n === 'number' && n > 0)
+ : []
+ return list.length ? Math.max(...list) : 0
+ }
+
+ // 计算加粗线宽,优先使用自定义;否则根据周期与厚度估算
+ private getBoldStrokeWidth(
+ advanced: any,
+ size: number,
+ thickness?: number,
+ period?: number,
+ ): number {
+ if (typeof advanced?.customBoldWidth === 'number')
+ return advanced.customBoldWidth
+ const baseThickness = Math.max(1, thickness ?? 1)
+ const p = Math.max(1, period ?? this.getPeriod(advanced))
+ return Math.min(baseThickness, (size * p) / 2) / 2
+ }
+
+ // 渲染 mesh 类型四条边的虚线,减少重复代码
+ private renderMeshEdgeLines(
+ size: number,
+ color: string,
+ strokeWidth: number,
+ opacity: number,
+ dash?: string,
+ ) {
+ const segments = [
+ { d: `M 0 0 H ${size}` },
+ { d: `M 0 ${size} H ${size}` },
+ { d: `M 0 0 V ${size}` },
+ { d: `M ${size} 0 V ${size}` },
+ ]
+ return (
+
+ {segments.map((seg) => (
+
+ ))}
+
+ )
+ }
+
// 网格类型为交叉线
// todo: 采用背景缩放的方式,实现更好的体验
renderMesh() {
- const { config, size = 1, visible } = this.gridOptions
- const { color, thickness = 1 } = config ?? {}
+ const {
+ config,
+ size = 1,
+ visible,
+ majorBold,
+ } = this.gridOptions as Grid.GridOptions & {
+ majorBold?: boolean | MajorBoldConfig
+ }
+ const { config: advanced } = mergeMajorBoldConfig(majorBold)
+ const { opacity: baseOpacity } = advanced
+ const color: string = (config?.color ?? '#D7DEEB') as string
+ const thickness: number = (config?.thickness ?? 1) as number
// 对于交叉线网格,线的宽度不能大于网格大小的一半
const strokeWidth = Math.min(Math.max(1, thickness), size / 2)
- const d = `M 0 0 H ${size} V ${size} H 0 Z`
- const opacity = visible ? 1 : 0
- return (
-
- )
+ const opacity = visible ? baseOpacity : 0
+
+ // 根据 size 自动计算合适的 dash/gap 周期,使 size 能被 (dash + gap) 整除
+ const dash =
+ majorBold === false
+ ? undefined
+ : this.getDashArrayForSize(
+ size,
+ strokeWidth / 2,
+ advanced.dashArrayConfig,
+ )
+
+ return this.renderMeshEdgeLines(size, color, strokeWidth, opacity, dash)
}
render() {
@@ -65,7 +159,15 @@ export class Grid extends Component {
graphModel: { transformModel, grid },
} = this.props
this.gridOptions = grid
- const { type, size = 1 } = this.gridOptions
+ const {
+ type,
+ config = {},
+ size = 1,
+ majorBold,
+ } = this.gridOptions as Grid.GridOptions & {
+ majorBold?: boolean | MajorBoldConfig
+ }
+ const { config: advanced } = mergeMajorBoldConfig(majorBold)
const { SCALE_X, SKEW_Y, SKEW_X, SCALE_Y, TRANSLATE_X, TRANSLATE_Y } =
transformModel
const matrixString = [
@@ -77,6 +179,7 @@ export class Grid extends Component {
TRANSLATE_Y,
].join(',')
const transform = `matrix(${matrixString})`
+ const radius = Math.min(Math.max(2, config.thickness ?? 1), size / 4)
// const transitionStyle = {
// transition: 'all 0.1s ease',
// };
@@ -101,8 +204,93 @@ export class Grid extends Component {
{type === 'dot' && this.renderDot()}
{type === 'mesh' && this.renderMesh()}
+ {type === 'dot' && advanced.boldIndices.length ? (
+
+
+ {/* 在每个周期的四个角绘制大点,以确保相邻 pattern 拼接后形成完整圆 */}
+
+
+
+
+
+
+ ) : null}
+ {type === 'mesh' && advanced.boldIndices.length ? (
+
+ {/* Render bold vertical/horizontal lines at configured indices within one period */}
+ {advanced.boldIndices.map((i: number) => (
+
+
+
+
+ ))}
+
+ ) : null}
+ {type === 'dot' && advanced.boldIndices.length ? (
+
+ ) : null}
+ {type === 'mesh' && advanced.boldIndices.length ? (
+
+ ) : null}