| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211 |
- /* eslint-disable @typescript-eslint/no-explicit-any */
- import {
- TLEllipseShapeProps,
- TLEllipseShape,
- getComputedColor,
- getTextLabelSize,
- } from '@tldraw/core'
- import { SVGContainer, TLComponentProps } from '@tldraw/react'
- import Vec from '@tldraw/vec'
- import * as React from 'react'
- import { observer } from 'mobx-react-lite'
- import { CustomStyleProps, withClampedStyles } from './style-props'
- import { TextLabel } from './text/TextLabel'
- import type { SizeLevel } from '.'
- import { action, computed } from 'mobx'
- export interface EllipseShapeProps extends TLEllipseShapeProps, CustomStyleProps {
- type: 'ellipse'
- size: number[]
- label: string
- fontSize: number
- fontWeight: number
- italic: boolean
- scaleLevel?: SizeLevel
- }
- const font = '18px / 1 var(--ls-font-family)'
- const levelToScale = {
- xs: 10,
- sm: 16,
- md: 20,
- lg: 32,
- xl: 48,
- xxl: 60,
- }
- export class EllipseShape extends TLEllipseShape<EllipseShapeProps> {
- static id = 'ellipse'
- static defaultProps: EllipseShapeProps = {
- id: 'ellipse',
- parentId: 'page',
- type: 'ellipse',
- point: [0, 0],
- size: [100, 100],
- stroke: '',
- fill: '',
- noFill: false,
- fontWeight: 400,
- fontSize: 20,
- italic: false,
- strokeType: 'line',
- strokeWidth: 2,
- opacity: 1,
- label: '',
- }
- canEdit = true
- ReactComponent = observer(
- ({ isSelected, isErasing, events, isEditing, onEditingEnd }: TLComponentProps) => {
- const {
- size: [w, h],
- stroke,
- fill,
- noFill,
- strokeWidth,
- strokeType,
- opacity,
- label,
- italic,
- fontWeight,
- fontSize,
- } = this.props
- const labelSize =
- label || isEditing
- ? getTextLabelSize(
- label,
- { fontFamily: 'var(--ls-font-family)', fontSize, lineHeight: 1, fontWeight },
- 4
- )
- : [0, 0]
- const midPoint = Vec.mul(this.props.size, 0.5)
- const scale = Math.max(0.5, Math.min(1, w / labelSize[0], h / labelSize[1]))
- const bounds = this.getBounds()
- const offset = React.useMemo(() => {
- return Vec.sub(midPoint, Vec.toFixed([bounds.width / 2, bounds.height / 2]))
- }, [bounds, scale, midPoint])
- const handleLabelChange = React.useCallback(
- (label: string) => {
- this.update?.({ label })
- },
- [label]
- )
- return (
- <div {...events} style={{ width: '100%', height: '100%', overflow: 'hidden' }}>
- <TextLabel
- font={font}
- text={label}
- color={getComputedColor(stroke, 'text')}
- offsetX={offset[0]}
- offsetY={offset[1]}
- scale={scale}
- isEditing={isEditing}
- onChange={handleLabelChange}
- onBlur={onEditingEnd}
- fontStyle={italic ? 'italic' : 'normal'}
- fontSize={fontSize}
- fontWeight={fontWeight}
- pointerEvents={!!label}
- />
- <SVGContainer {...events} opacity={isErasing ? 0.2 : opacity}>
- <ellipse
- className={isSelected || !noFill ? 'tl-hitarea-fill' : 'tl-hitarea-stroke'}
- cx={w / 2}
- cy={h / 2}
- rx={Math.max(0.01, (w - strokeWidth) / 2)}
- ry={Math.max(0.01, (h - strokeWidth) / 2)}
- />
- <ellipse
- cx={w / 2}
- cy={h / 2}
- rx={Math.max(0.01, (w - strokeWidth) / 2)}
- ry={Math.max(0.01, (h - strokeWidth) / 2)}
- strokeWidth={strokeWidth}
- stroke={getComputedColor(stroke, 'stroke')}
- strokeDasharray={strokeType === 'dashed' ? '8 2' : undefined}
- fill={noFill ? 'none' : getComputedColor(fill, 'background')}
- />
- </SVGContainer>
- </div>
- )
- }
- )
- @computed get scaleLevel() {
- return this.props.scaleLevel ?? 'md'
- }
- @action setScaleLevel = async (v?: SizeLevel) => {
- this.update({
- scaleLevel: v,
- fontSize: levelToScale[v ?? 'md'],
- strokeWidth: levelToScale[v ?? 'md'] / 10,
- })
- this.onResetBounds()
- }
- ReactIndicator = observer(() => {
- const {
- size: [w, h],
- isLocked,
- } = this.props
- return (
- <g>
- <ellipse cx={w / 2} cy={h / 2} rx={w / 2} ry={h / 2} strokeWidth={2} fill="transparent" strokeDasharray={isLocked ? "8 2" : "undefined"}/>
- </g>
- )
- })
- validateProps = (props: Partial<EllipseShapeProps>) => {
- if (props.size !== undefined) {
- props.size[0] = Math.max(props.size[0], 1)
- props.size[1] = Math.max(props.size[1], 1)
- }
- return withClampedStyles(this, props)
- }
- /**
- * Get a svg group element that can be used to render the shape with only the props data. In the
- * base, draw any shape as a box. Can be overridden by subclasses.
- */
- getShapeSVGJsx(opts: any) {
- const {
- size: [w, h],
- stroke,
- fill,
- noFill,
- strokeWidth,
- strokeType,
- opacity,
- } = this.props
- return (
- <g opacity={opacity}>
- <ellipse
- className={!noFill ? 'tl-hitarea-fill' : 'tl-hitarea-stroke'}
- cx={w / 2}
- cy={h / 2}
- rx={Math.max(0.01, (w - strokeWidth) / 2)}
- ry={Math.max(0.01, (h - strokeWidth) / 2)}
- />
- <ellipse
- cx={w / 2}
- cy={h / 2}
- rx={Math.max(0.01, (w - strokeWidth) / 2)}
- ry={Math.max(0.01, (h - strokeWidth) / 2)}
- strokeWidth={strokeWidth}
- stroke={getComputedColor(stroke, 'stroke')}
- strokeDasharray={strokeType === 'dashed' ? '8 2' : undefined}
- fill={noFill ? 'none' : getComputedColor(fill, 'background')}
- />
- </g>
- )
- }
- }
|