init: the first public commit for AFFiNE

This commit is contained in:
DarkSky
2022-07-22 15:49:21 +08:00
commit e3e3741393
1451 changed files with 108124 additions and 0 deletions
@@ -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
);
}