mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-19 02:56:23 +08:00
init: the first public commit for AFFiNE
This commit is contained in:
@@ -0,0 +1,168 @@
|
||||
import * as React from 'react';
|
||||
import { Utils, SVGContainer } from '@tldraw/core';
|
||||
import {
|
||||
RectangleShape,
|
||||
DashStyle,
|
||||
TDShapeType,
|
||||
TDMeta,
|
||||
GHOSTED_OPACITY,
|
||||
LABEL_POINT,
|
||||
} from '@toeverything/components/board-types';
|
||||
import { TDShapeUtil } from '../TDShapeUtil';
|
||||
import {
|
||||
defaultStyle,
|
||||
getShapeStyle,
|
||||
getBoundsRectangle,
|
||||
transformRectangle,
|
||||
getFontStyle,
|
||||
transformSingleRectangle,
|
||||
} from '../shared';
|
||||
import { TextLabel } from '../shared/text-label';
|
||||
import { getRectangleIndicatorPathTDSnapshot } from './rectangle-helpers';
|
||||
import { DrawRectangle } from './components/DrawRectangle';
|
||||
import { DashedRectangle } from './components/DashedRectangle';
|
||||
import { BindingIndicator } from './components/BindingIndicator';
|
||||
import { styled } from '@toeverything/components/ui';
|
||||
|
||||
type T = RectangleShape;
|
||||
type E = HTMLDivElement;
|
||||
|
||||
export class RectangleUtil extends TDShapeUtil<T, E> {
|
||||
type = TDShapeType.Rectangle as const;
|
||||
|
||||
override canBind = true;
|
||||
|
||||
override canClone = true;
|
||||
|
||||
override canEdit = true;
|
||||
|
||||
getShape = (props: Partial<T>): T => {
|
||||
return Utils.deepMerge<T>(
|
||||
{
|
||||
id: 'id',
|
||||
type: TDShapeType.Rectangle,
|
||||
name: 'Rectangle',
|
||||
parentId: 'page',
|
||||
childIndex: 1,
|
||||
point: [0, 0],
|
||||
size: [1, 1],
|
||||
rotation: 0,
|
||||
style: defaultStyle,
|
||||
label: '',
|
||||
labelPoint: [0.5, 0.5],
|
||||
workspace: props.workspace,
|
||||
},
|
||||
props
|
||||
);
|
||||
};
|
||||
|
||||
Component = TDShapeUtil.Component<T, E, TDMeta>(
|
||||
(
|
||||
{
|
||||
shape,
|
||||
isEditing,
|
||||
isBinding,
|
||||
isSelected,
|
||||
isGhost,
|
||||
meta,
|
||||
bounds,
|
||||
events,
|
||||
onShapeBlur,
|
||||
onShapeChange,
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const {
|
||||
id,
|
||||
size,
|
||||
style,
|
||||
label = '',
|
||||
labelPoint = LABEL_POINT,
|
||||
} = shape;
|
||||
const font = getFontStyle(style);
|
||||
const styles = getShapeStyle(style, meta.isDarkMode);
|
||||
const Component =
|
||||
style.dash === DashStyle.Draw ? DrawRectangle : DashedRectangle;
|
||||
const handleLabelChange = React.useCallback(
|
||||
(label: string) => onShapeChange?.({ id, label }),
|
||||
[onShapeChange]
|
||||
);
|
||||
return (
|
||||
<FullWrapper ref={ref} {...events}>
|
||||
<TextLabel
|
||||
isEditing={isEditing}
|
||||
onChange={handleLabelChange}
|
||||
onBlur={onShapeBlur}
|
||||
font={font}
|
||||
text={label}
|
||||
color={styles.stroke}
|
||||
offsetX={(labelPoint[0] - 0.5) * bounds.width}
|
||||
offsetY={(labelPoint[1] - 0.5) * bounds.height}
|
||||
/>
|
||||
<SVGContainer
|
||||
id={shape.id + '_svg'}
|
||||
opacity={isGhost ? GHOSTED_OPACITY : 1}
|
||||
>
|
||||
{isBinding && (
|
||||
<BindingIndicator
|
||||
strokeWidth={styles.strokeWidth}
|
||||
size={size}
|
||||
/>
|
||||
)}
|
||||
<Component
|
||||
id={id}
|
||||
style={style}
|
||||
size={size}
|
||||
isSelected={isSelected}
|
||||
isDarkMode={meta.isDarkMode}
|
||||
/>
|
||||
</SVGContainer>
|
||||
</FullWrapper>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
Indicator = TDShapeUtil.Indicator<T>(({ shape }) => {
|
||||
const { id, style, size } = shape;
|
||||
|
||||
const styles = getShapeStyle(style, false);
|
||||
const sw = styles.strokeWidth;
|
||||
|
||||
if (style.dash === DashStyle.Draw) {
|
||||
return (
|
||||
<path
|
||||
d={getRectangleIndicatorPathTDSnapshot(id, style, size)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<rect
|
||||
x={sw}
|
||||
y={sw}
|
||||
rx={1}
|
||||
ry={1}
|
||||
width={Math.max(1, size[0] - sw * 2)}
|
||||
height={Math.max(1, size[1] - sw * 2)}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
getBounds = (shape: T) => {
|
||||
return getBoundsRectangle(shape, this.boundsCache);
|
||||
};
|
||||
|
||||
override shouldRender = (prev: T, next: T) => {
|
||||
return (
|
||||
next.size !== prev.size ||
|
||||
next.style !== prev.style ||
|
||||
next.label !== prev.label
|
||||
);
|
||||
};
|
||||
|
||||
override transform = transformRectangle;
|
||||
|
||||
override transformSingle = transformSingleRectangle;
|
||||
}
|
||||
|
||||
const FullWrapper = styled('div')({ width: '100%', height: '100%' });
|
||||
@@ -0,0 +1,19 @@
|
||||
import * as React from 'react';
|
||||
import { BINDING_DISTANCE } from '@toeverything/components/board-types';
|
||||
|
||||
interface BindingIndicatorProps {
|
||||
strokeWidth: number;
|
||||
size: number[];
|
||||
}
|
||||
export function BindingIndicator({ strokeWidth, size }: BindingIndicatorProps) {
|
||||
return (
|
||||
<rect
|
||||
className="tl-binding-indicator"
|
||||
x={strokeWidth}
|
||||
y={strokeWidth}
|
||||
width={Math.max(0, size[0] - strokeWidth / 2)}
|
||||
height={Math.max(0, size[1] - strokeWidth / 2)}
|
||||
strokeWidth={BINDING_DISTANCE * 2}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import * as React from 'react';
|
||||
import { Utils } from '@tldraw/core';
|
||||
import { BINDING_DISTANCE } from '@toeverything/components/board-types';
|
||||
import type { ShapeStyles } from '@toeverything/components/board-types';
|
||||
import { getShapeStyle } from '../../shared';
|
||||
|
||||
interface RectangleSvgProps {
|
||||
id: string;
|
||||
style: ShapeStyles;
|
||||
isSelected: boolean;
|
||||
size: number[];
|
||||
isDarkMode: boolean;
|
||||
}
|
||||
|
||||
export const DashedRectangle = React.memo(function DashedRectangle({
|
||||
id,
|
||||
style,
|
||||
size,
|
||||
isSelected,
|
||||
isDarkMode,
|
||||
}: RectangleSvgProps) {
|
||||
const { stroke, strokeWidth, fill } = getShapeStyle(style, isDarkMode);
|
||||
|
||||
const sw = 1 + strokeWidth * 1.618;
|
||||
|
||||
const w = Math.max(0, size[0] - sw / 2);
|
||||
const h = Math.max(0, size[1] - sw / 2);
|
||||
|
||||
const strokes: [number[], number[], number][] = [
|
||||
[[sw / 2, sw / 2], [w, sw / 2], w - sw / 2],
|
||||
[[w, sw / 2], [w, h], h - sw / 2],
|
||||
[[w, h], [sw / 2, h], w - sw / 2],
|
||||
[[sw / 2, h], [sw / 2, sw / 2], h - sw / 2],
|
||||
];
|
||||
|
||||
const paths = strokes.map(([start, end, length], i) => {
|
||||
const { strokeDasharray, strokeDashoffset } = Utils.getPerfectDashProps(
|
||||
length,
|
||||
strokeWidth * 1.618,
|
||||
style.dash
|
||||
);
|
||||
|
||||
return (
|
||||
<line
|
||||
key={id + '_' + i}
|
||||
x1={start[0]}
|
||||
y1={start[1]}
|
||||
x2={end[0]}
|
||||
y2={end[1]}
|
||||
strokeDasharray={strokeDasharray}
|
||||
strokeDashoffset={strokeDashoffset}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<rect
|
||||
className={
|
||||
isSelected || style.isFilled
|
||||
? 'tl-fill-hitarea'
|
||||
: 'tl-stroke-hitarea'
|
||||
}
|
||||
x={sw / 2}
|
||||
y={sw / 2}
|
||||
width={w}
|
||||
height={h}
|
||||
strokeWidth={BINDING_DISTANCE}
|
||||
/>
|
||||
{style.isFilled && (
|
||||
<rect
|
||||
x={sw / 2}
|
||||
y={sw / 2}
|
||||
width={w}
|
||||
height={h}
|
||||
fill={fill}
|
||||
pointerEvents="none"
|
||||
/>
|
||||
)}
|
||||
<g
|
||||
pointerEvents="none"
|
||||
stroke={stroke}
|
||||
strokeWidth={sw}
|
||||
strokeLinecap="round"
|
||||
>
|
||||
{paths}
|
||||
</g>
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,51 @@
|
||||
import * as React from 'react';
|
||||
import { getShapeStyle } from '../../shared';
|
||||
import type { ShapeStyles } from '@toeverything/components/board-types';
|
||||
import {
|
||||
getRectangleIndicatorPathTDSnapshot,
|
||||
getRectanglePath,
|
||||
} from '../rectangle-helpers';
|
||||
|
||||
interface RectangleSvgProps {
|
||||
id: string;
|
||||
style: ShapeStyles;
|
||||
isSelected: boolean;
|
||||
isDarkMode: boolean;
|
||||
size: number[];
|
||||
}
|
||||
|
||||
export const DrawRectangle = React.memo(function DrawRectangle({
|
||||
id,
|
||||
style,
|
||||
size,
|
||||
isSelected,
|
||||
isDarkMode,
|
||||
}: RectangleSvgProps) {
|
||||
const { isFilled } = style;
|
||||
const { stroke, strokeWidth, fill } = getShapeStyle(style, isDarkMode);
|
||||
const path_td_snapshot = getRectanglePath(id, style, size);
|
||||
const innerPath = getRectangleIndicatorPathTDSnapshot(id, style, size);
|
||||
|
||||
return (
|
||||
<>
|
||||
<path
|
||||
className={
|
||||
style.isFilled || isSelected
|
||||
? 'tl-fill-hitarea'
|
||||
: 'tl-stroke-hitarea'
|
||||
}
|
||||
d={innerPath}
|
||||
/>
|
||||
{isFilled && (
|
||||
<path d={innerPath} fill={fill} pointerEvents="none" />
|
||||
)}
|
||||
<path
|
||||
d={path_td_snapshot}
|
||||
fill={stroke}
|
||||
stroke={stroke}
|
||||
strokeWidth={strokeWidth}
|
||||
pointerEvents="none"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
export * from './RectangleUtil';
|
||||
@@ -0,0 +1,107 @@
|
||||
import { Utils } from '@tldraw/core';
|
||||
import Vec from '@tldraw/vec';
|
||||
import getStroke, { getStrokePoints } from 'perfect-freehand';
|
||||
import type { ShapeStyles } from '@toeverything/components/board-types';
|
||||
import { getShapeStyle } from '../shared';
|
||||
|
||||
function getRectangleDrawPoints(
|
||||
id: string,
|
||||
style: ShapeStyles,
|
||||
size: number[]
|
||||
) {
|
||||
const styles = getShapeStyle(style);
|
||||
|
||||
const getRandom = Utils.rng(id);
|
||||
|
||||
const sw = styles.strokeWidth;
|
||||
|
||||
// Dimensions
|
||||
const w = Math.max(0, size[0]);
|
||||
const h = Math.max(0, size[1]);
|
||||
|
||||
// Random corner offsets
|
||||
const offsets = Array.from(Array(4)).map(() => {
|
||||
return [getRandom() * sw * 0.75, getRandom() * sw * 0.75];
|
||||
});
|
||||
|
||||
// Corners
|
||||
const tl = Vec.add([sw / 2, sw / 2], offsets[0]);
|
||||
const tr = Vec.add([w - sw / 2, sw / 2], offsets[1]);
|
||||
const br = Vec.add([w - sw / 2, h - sw / 2], offsets[2]);
|
||||
const bl = Vec.add([sw / 2, h - sw / 2], offsets[3]);
|
||||
|
||||
// Which side to start drawing first
|
||||
const rm = Math.round(Math.abs(getRandom() * 2 * 4));
|
||||
|
||||
// Corner radii
|
||||
const rx = Math.min(w / 4, sw * 2);
|
||||
const ry = Math.min(h / 4, sw * 2);
|
||||
|
||||
// Number of points per side
|
||||
const px = Math.max(8, Math.floor(w / 16));
|
||||
const py = Math.max(8, Math.floor(h / 16));
|
||||
|
||||
// Inset each line by the corner radii and let the freehand algo
|
||||
// interpolate points for the corners.
|
||||
const lines = Utils.rotateArray(
|
||||
[
|
||||
Vec.pointsBetween(Vec.add(tl, [rx, 0]), Vec.sub(tr, [rx, 0]), px),
|
||||
Vec.pointsBetween(Vec.add(tr, [0, ry]), Vec.sub(br, [0, ry]), py),
|
||||
Vec.pointsBetween(Vec.sub(br, [rx, 0]), Vec.add(bl, [rx, 0]), px),
|
||||
Vec.pointsBetween(Vec.sub(bl, [0, ry]), Vec.add(tl, [0, ry]), py),
|
||||
],
|
||||
rm
|
||||
);
|
||||
|
||||
// For the final points, include the first half of the first line again,
|
||||
// so that the line wraps around and avoids ending on a sharp corner.
|
||||
// This has a bit of finesse and magic—if you change the points between
|
||||
// function, then you'll likely need to change this one too.
|
||||
|
||||
const points = [...lines.flat(), ...lines[0]].slice(
|
||||
5,
|
||||
Math.floor((rm % 2 === 0 ? px : py) / -2) + 3
|
||||
);
|
||||
|
||||
return {
|
||||
points,
|
||||
};
|
||||
}
|
||||
|
||||
function getDrawStrokeInfo(id: string, style: ShapeStyles, size: number[]) {
|
||||
const { points } = getRectangleDrawPoints(id, style, size);
|
||||
const { strokeWidth } = getShapeStyle(style);
|
||||
const options = {
|
||||
size: strokeWidth,
|
||||
thinning: 0.65,
|
||||
streamline: 0.3,
|
||||
smoothing: 1,
|
||||
simulatePressure: false,
|
||||
last: true,
|
||||
};
|
||||
return { points, options };
|
||||
}
|
||||
|
||||
export function getRectanglePath(
|
||||
id: string,
|
||||
style: ShapeStyles,
|
||||
size: number[]
|
||||
) {
|
||||
const { points, options } = getDrawStrokeInfo(id, style, size);
|
||||
const stroke = getStroke(points, options);
|
||||
return Utils.getSvgPathFromStroke(stroke);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
export function getRectangleIndicatorPathTDSnapshot(
|
||||
id: string,
|
||||
style: ShapeStyles,
|
||||
size: number[]
|
||||
) {
|
||||
const { points, options } = getDrawStrokeInfo(id, style, size);
|
||||
const strokePoints = getStrokePoints(points, options);
|
||||
return Utils.getSvgPathFromStroke(
|
||||
strokePoints.map(pt => pt.point.slice(0, 2)),
|
||||
false
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user