improves select display
This commit is contained in:
parent
708223fffa
commit
9fd8ef8421
10 changed files with 237 additions and 122 deletions
6
.prettierrc
Normal file
6
.prettierrc
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"tabWidth": 2,
|
||||
"useTabs": false
|
||||
}
|
|
@ -1,16 +1,17 @@
|
|||
import * as React from "react"
|
||||
import { Edge, Corner } from "types"
|
||||
import { useSelector } from "state"
|
||||
import { getSelectedShapes, isMobile } from "utils/utils"
|
||||
import * as React from 'react'
|
||||
import { Edge, Corner } from 'types'
|
||||
import { useSelector } from 'state'
|
||||
import { getSelectedShapes, isMobile } from 'utils/utils'
|
||||
|
||||
import CenterHandle from "./center-handle"
|
||||
import CornerHandle from "./corner-handle"
|
||||
import EdgeHandle from "./edge-handle"
|
||||
import RotateHandle from "./rotate-handle"
|
||||
import CenterHandle from './center-handle'
|
||||
import CornerHandle from './corner-handle'
|
||||
import EdgeHandle from './edge-handle'
|
||||
import RotateHandle from './rotate-handle'
|
||||
import Selected from '../selected'
|
||||
|
||||
export default function Bounds() {
|
||||
const isBrushing = useSelector((s) => s.isIn("brushSelecting"))
|
||||
const isSelecting = useSelector((s) => s.isIn("selecting"))
|
||||
const isBrushing = useSelector((s) => s.isIn('brushSelecting'))
|
||||
const isSelecting = useSelector((s) => s.isIn('selecting'))
|
||||
const zoom = useSelector((s) => s.data.camera.zoom)
|
||||
const bounds = useSelector((s) => s.values.selectedBounds)
|
||||
const rotation = useSelector(({ data }) =>
|
||||
|
@ -24,13 +25,14 @@ export default function Bounds() {
|
|||
|
||||
return (
|
||||
<g
|
||||
pointerEvents={isBrushing ? "none" : "all"}
|
||||
pointerEvents={isBrushing ? 'none' : 'all'}
|
||||
transform={`
|
||||
rotate(${rotation * (180 / Math.PI)},
|
||||
${(bounds.minX + bounds.maxX) / 2},
|
||||
${(bounds.minY + bounds.maxY) / 2})
|
||||
translate(${bounds.minX},${bounds.minY})`}
|
||||
>
|
||||
<Selected bounds={bounds} />
|
||||
<CenterHandle bounds={bounds} />
|
||||
<EdgeHandle size={size} bounds={bounds} edge={Edge.Top} />
|
||||
<EdgeHandle size={size} bounds={bounds} edge={Edge.Right} />
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
import styled from "styles"
|
||||
import React, { useCallback, useRef } from "react"
|
||||
import useZoomEvents from "hooks/useZoomEvents"
|
||||
import useCamera from "hooks/useCamera"
|
||||
import Page from "./page"
|
||||
import Brush from "./brush"
|
||||
import state from "state"
|
||||
import Bounds from "./bounds/bounding-box"
|
||||
import BoundsBg from "./bounds/bounds-bg"
|
||||
import inputs from "state/inputs"
|
||||
import styled from 'styles'
|
||||
import state from 'state'
|
||||
import inputs from 'state/inputs'
|
||||
import React, { useCallback, useRef } from 'react'
|
||||
import useZoomEvents from 'hooks/useZoomEvents'
|
||||
import useCamera from 'hooks/useCamera'
|
||||
import Defs from './defs'
|
||||
import Page from './page'
|
||||
import Brush from './brush'
|
||||
import Bounds from './bounds/bounding-box'
|
||||
import BoundsBg from './bounds/bounds-bg'
|
||||
|
||||
export default function Canvas() {
|
||||
const rCanvas = useRef<SVGSVGElement>(null)
|
||||
|
@ -18,16 +19,16 @@ export default function Canvas() {
|
|||
|
||||
const handlePointerDown = useCallback((e: React.PointerEvent) => {
|
||||
rCanvas.current.setPointerCapture(e.pointerId)
|
||||
state.send("POINTED_CANVAS", inputs.pointerDown(e, "canvas"))
|
||||
state.send('POINTED_CANVAS', inputs.pointerDown(e, 'canvas'))
|
||||
}, [])
|
||||
|
||||
const handlePointerMove = useCallback((e: React.PointerEvent) => {
|
||||
state.send("MOVED_POINTER", inputs.pointerMove(e))
|
||||
state.send('MOVED_POINTER', inputs.pointerMove(e))
|
||||
}, [])
|
||||
|
||||
const handlePointerUp = useCallback((e: React.PointerEvent) => {
|
||||
rCanvas.current.releasePointerCapture(e.pointerId)
|
||||
state.send("STOPPED_POINTING", { id: "canvas", ...inputs.pointerUp(e) })
|
||||
state.send('STOPPED_POINTING', { id: 'canvas', ...inputs.pointerUp(e) })
|
||||
}, [])
|
||||
|
||||
return (
|
||||
|
@ -38,6 +39,7 @@ export default function Canvas() {
|
|||
onPointerMove={handlePointerMove}
|
||||
onPointerUp={handlePointerUp}
|
||||
>
|
||||
<Defs />
|
||||
<MainGroup ref={rGroup}>
|
||||
<BoundsBg />
|
||||
<Page />
|
||||
|
@ -48,18 +50,18 @@ export default function Canvas() {
|
|||
)
|
||||
}
|
||||
|
||||
const MainSVG = styled("svg", {
|
||||
position: "fixed",
|
||||
const MainSVG = styled('svg', {
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
touchAction: "none",
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
touchAction: 'none',
|
||||
zIndex: 100,
|
||||
|
||||
"& *": {
|
||||
userSelect: "none",
|
||||
'& *': {
|
||||
userSelect: 'none',
|
||||
},
|
||||
})
|
||||
|
||||
const MainGroup = styled("g", {})
|
||||
const MainGroup = styled('g', {})
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { useSelector } from "state"
|
||||
import { deepCompareArrays, getPage } from "utils/utils"
|
||||
import Shape from "./shape"
|
||||
import { useSelector } from 'state'
|
||||
import { deepCompareArrays, getPage } from 'utils/utils'
|
||||
import Shape from './shape'
|
||||
|
||||
/*
|
||||
On each state change, compare node ids of all shapes
|
||||
|
@ -15,10 +15,12 @@ export default function Page() {
|
|||
.map((shape) => shape.id)
|
||||
}, deepCompareArrays)
|
||||
|
||||
const isSelecting = useSelector((s) => s.isIn('selecting'))
|
||||
|
||||
return (
|
||||
<>
|
||||
{currentPageShapeIds.map((shapeId) => (
|
||||
<Shape key={shapeId} id={shapeId} />
|
||||
<Shape key={shapeId} id={shapeId} isSelecting={isSelecting} />
|
||||
))}
|
||||
</>
|
||||
)
|
||||
|
|
58
components/canvas/selected.tsx
Normal file
58
components/canvas/selected.tsx
Normal file
|
@ -0,0 +1,58 @@
|
|||
import styled from 'styles'
|
||||
import { useSelector } from 'state'
|
||||
import {
|
||||
deepCompareArrays,
|
||||
getBoundsCenter,
|
||||
getPage,
|
||||
getSelectedShapes,
|
||||
} from 'utils/utils'
|
||||
import * as vec from 'utils/vec'
|
||||
import { getShapeUtils } from 'lib/shape-utils'
|
||||
import { Bounds } from 'types'
|
||||
import useShapeEvents from 'hooks/useShapeEvents'
|
||||
import { useRef } from 'react'
|
||||
|
||||
export default function Selected({ bounds }: { bounds: Bounds }) {
|
||||
const currentPageShapeIds = useSelector(({ data }) => {
|
||||
return Array.from(data.selectedIds.values())
|
||||
}, deepCompareArrays)
|
||||
|
||||
return (
|
||||
<g>
|
||||
{currentPageShapeIds.map((id) => (
|
||||
<ShapeOutline key={id} id={id} bounds={bounds} />
|
||||
))}
|
||||
</g>
|
||||
)
|
||||
}
|
||||
|
||||
export function ShapeOutline({ id, bounds }: { id: string; bounds: Bounds }) {
|
||||
const rIndicator = useRef<SVGUseElement>(null)
|
||||
|
||||
const shape = useSelector(({ data }) => getPage(data).shapes[id])
|
||||
|
||||
const shapeBounds = getShapeUtils(shape).getBounds(shape)
|
||||
|
||||
const events = useShapeEvents(id, rIndicator)
|
||||
|
||||
return (
|
||||
<Indicator
|
||||
ref={rIndicator}
|
||||
as="use"
|
||||
href={'#' + id}
|
||||
transform={`rotate(${shape.rotation * (180 / Math.PI)},${getBoundsCenter(
|
||||
shapeBounds
|
||||
)}) translate(${vec.sub(shape.point, [bounds.minX, bounds.minY])})`}
|
||||
{...events}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const Indicator = styled('path', {
|
||||
zStrokeWidth: 1,
|
||||
strokeLineCap: 'round',
|
||||
strokeLinejoin: 'round',
|
||||
stroke: '$selected',
|
||||
fill: 'transparent',
|
||||
pointerEvents: 'all',
|
||||
})
|
|
@ -1,11 +1,12 @@
|
|||
import React, { useCallback, useRef, memo } from "react"
|
||||
import state, { useSelector } from "state"
|
||||
import inputs from "state/inputs"
|
||||
import styled from "styles"
|
||||
import { getShapeUtils } from "lib/shape-utils"
|
||||
import { getPage } from "utils/utils"
|
||||
import React, { useCallback, useRef, memo } from 'react'
|
||||
import state, { useSelector } from 'state'
|
||||
import inputs from 'state/inputs'
|
||||
import styled from 'styles'
|
||||
import { getShapeUtils } from 'lib/shape-utils'
|
||||
import { getPage } from 'utils/utils'
|
||||
import { ShapeStyles } from 'types'
|
||||
|
||||
function Shape({ id }: { id: string }) {
|
||||
function Shape({ id, isSelecting }: { id: string; isSelecting: boolean }) {
|
||||
const isHovered = useSelector((state) => state.data.hoveredId === id)
|
||||
|
||||
const isSelected = useSelector((state) => state.values.selectedIds.has(id))
|
||||
|
@ -18,7 +19,7 @@ function Shape({ id }: { id: string }) {
|
|||
(e: React.PointerEvent) => {
|
||||
e.stopPropagation()
|
||||
rGroup.current.setPointerCapture(e.pointerId)
|
||||
state.send("POINTED_SHAPE", inputs.pointerDown(e, id))
|
||||
state.send('POINTED_SHAPE', inputs.pointerDown(e, id))
|
||||
},
|
||||
[id]
|
||||
)
|
||||
|
@ -27,27 +28,27 @@ function Shape({ id }: { id: string }) {
|
|||
(e: React.PointerEvent) => {
|
||||
e.stopPropagation()
|
||||
rGroup.current.releasePointerCapture(e.pointerId)
|
||||
state.send("STOPPED_POINTING", inputs.pointerUp(e))
|
||||
state.send('STOPPED_POINTING', inputs.pointerUp(e))
|
||||
},
|
||||
[id]
|
||||
)
|
||||
|
||||
const handlePointerEnter = useCallback(
|
||||
(e: React.PointerEvent) => {
|
||||
state.send("HOVERED_SHAPE", inputs.pointerEnter(e, id))
|
||||
state.send('HOVERED_SHAPE', inputs.pointerEnter(e, id))
|
||||
},
|
||||
[id, shape]
|
||||
)
|
||||
|
||||
const handlePointerMove = useCallback(
|
||||
(e: React.PointerEvent) => {
|
||||
state.send("MOVED_OVER_SHAPE", inputs.pointerEnter(e, id))
|
||||
state.send('MOVED_OVER_SHAPE', inputs.pointerEnter(e, id))
|
||||
},
|
||||
[id, shape]
|
||||
)
|
||||
|
||||
const handlePointerLeave = useCallback(
|
||||
() => state.send("UNHOVERED_SHAPE", { target: id }),
|
||||
() => state.send('UNHOVERED_SHAPE', { target: id }),
|
||||
[id]
|
||||
)
|
||||
|
||||
|
@ -71,47 +72,38 @@ function Shape({ id }: { id: string }) {
|
|||
onPointerLeave={handlePointerLeave}
|
||||
onPointerMove={handlePointerMove}
|
||||
>
|
||||
<defs>{getShapeUtils(shape).render(shape)}</defs>
|
||||
<HoverIndicator as="use" href={"#" + id} />
|
||||
<MainShape as="use" href={"#" + id} {...shape.style} />
|
||||
<Indicator as="use" href={"#" + id} />
|
||||
{isSelecting && <HoverIndicator as="use" href={'#' + id} />}
|
||||
<StyledShape id={id} style={shape.style} />
|
||||
</StyledGroup>
|
||||
)
|
||||
}
|
||||
|
||||
const MainShape = styled("use", {
|
||||
const StyledShape = memo(
|
||||
({ id, style }: { id: string; style: ShapeStyles }) => {
|
||||
return <MainShape as="use" href={'#' + id} {...style} />
|
||||
}
|
||||
)
|
||||
|
||||
const MainShape = styled('use', {
|
||||
zStrokeWidth: 1,
|
||||
})
|
||||
|
||||
const Indicator = styled("path", {
|
||||
fill: "none",
|
||||
stroke: "transparent",
|
||||
zStrokeWidth: 1,
|
||||
pointerEvents: "none",
|
||||
strokeLineCap: "round",
|
||||
strokeLinejoin: "round",
|
||||
const HoverIndicator = styled('path', {
|
||||
fill: 'none',
|
||||
stroke: 'transparent',
|
||||
pointerEvents: 'all',
|
||||
strokeLinecap: 'round',
|
||||
strokeLinejoin: 'round',
|
||||
transform: 'all .2s',
|
||||
})
|
||||
|
||||
const HoverIndicator = styled("path", {
|
||||
fill: "none",
|
||||
stroke: "transparent",
|
||||
pointerEvents: "all",
|
||||
strokeLinecap: "round",
|
||||
strokeLinejoin: "round",
|
||||
transform: "all .2s",
|
||||
})
|
||||
|
||||
const StyledGroup = styled("g", {
|
||||
const StyledGroup = styled('g', {
|
||||
[`& ${HoverIndicator}`]: {
|
||||
opacity: "0",
|
||||
opacity: '0',
|
||||
},
|
||||
variants: {
|
||||
isSelected: {
|
||||
true: {
|
||||
[`& ${Indicator}`]: {
|
||||
stroke: "$selected",
|
||||
},
|
||||
},
|
||||
true: {},
|
||||
false: {},
|
||||
},
|
||||
isHovered: {
|
||||
|
@ -125,8 +117,8 @@ const StyledGroup = styled("g", {
|
|||
isHovered: true,
|
||||
css: {
|
||||
[`& ${HoverIndicator}`]: {
|
||||
opacity: "1",
|
||||
stroke: "$hint",
|
||||
opacity: '1',
|
||||
stroke: '$hint',
|
||||
zStrokeWidth: [8, 4],
|
||||
},
|
||||
},
|
||||
|
@ -136,8 +128,8 @@ const StyledGroup = styled("g", {
|
|||
isHovered: false,
|
||||
css: {
|
||||
[`& ${HoverIndicator}`]: {
|
||||
opacity: "1",
|
||||
stroke: "$hint",
|
||||
opacity: '1',
|
||||
stroke: '$hint',
|
||||
zStrokeWidth: [6, 3],
|
||||
},
|
||||
},
|
||||
|
@ -147,8 +139,8 @@ const StyledGroup = styled("g", {
|
|||
isHovered: true,
|
||||
css: {
|
||||
[`& ${HoverIndicator}`]: {
|
||||
opacity: "1",
|
||||
stroke: "$hint",
|
||||
opacity: '1',
|
||||
stroke: '$hint',
|
||||
zStrokeWidth: [8, 4],
|
||||
},
|
||||
},
|
||||
|
@ -156,6 +148,6 @@ const StyledGroup = styled("g", {
|
|||
],
|
||||
})
|
||||
|
||||
export { Indicator, HoverIndicator }
|
||||
export { HoverIndicator }
|
||||
|
||||
export default memo(Shape)
|
||||
|
|
53
hooks/useShapeEvents.ts
Normal file
53
hooks/useShapeEvents.ts
Normal file
|
@ -0,0 +1,53 @@
|
|||
import { MutableRefObject, useCallback } from 'react'
|
||||
import state from 'state'
|
||||
import inputs from 'state/inputs'
|
||||
|
||||
export default function useShapeEvents(
|
||||
id: string,
|
||||
rGroup: MutableRefObject<SVGElement>
|
||||
) {
|
||||
const handlePointerDown = useCallback(
|
||||
(e: React.PointerEvent) => {
|
||||
e.stopPropagation()
|
||||
rGroup.current.setPointerCapture(e.pointerId)
|
||||
state.send('POINTED_SHAPE', inputs.pointerDown(e, id))
|
||||
},
|
||||
[id]
|
||||
)
|
||||
|
||||
const handlePointerUp = useCallback(
|
||||
(e: React.PointerEvent) => {
|
||||
e.stopPropagation()
|
||||
rGroup.current.releasePointerCapture(e.pointerId)
|
||||
state.send('STOPPED_POINTING', inputs.pointerUp(e))
|
||||
},
|
||||
[id]
|
||||
)
|
||||
|
||||
const handlePointerEnter = useCallback(
|
||||
(e: React.PointerEvent) => {
|
||||
state.send('HOVERED_SHAPE', inputs.pointerEnter(e, id))
|
||||
},
|
||||
[id]
|
||||
)
|
||||
|
||||
const handlePointerMove = useCallback(
|
||||
(e: React.PointerEvent) => {
|
||||
state.send('MOVED_OVER_SHAPE', inputs.pointerEnter(e, id))
|
||||
},
|
||||
[id]
|
||||
)
|
||||
|
||||
const handlePointerLeave = useCallback(
|
||||
() => state.send('UNHOVERED_SHAPE', { target: id }),
|
||||
[id]
|
||||
)
|
||||
|
||||
return {
|
||||
onPointerDown: handlePointerDown,
|
||||
onPointerUp: handlePointerUp,
|
||||
onPointerEnter: handlePointerEnter,
|
||||
onPointerMove: handlePointerMove,
|
||||
onPointerLeave: handlePointerLeave,
|
||||
}
|
||||
}
|
|
@ -1,19 +1,19 @@
|
|||
import { v4 as uuid } from "uuid"
|
||||
import * as vec from "utils/vec"
|
||||
import { DrawShape, ShapeType } from "types"
|
||||
import { registerShapeUtils } from "./index"
|
||||
import { intersectPolylineBounds } from "utils/intersections"
|
||||
import { boundsContainPolygon } from "utils/bounds"
|
||||
import getStroke from "perfect-freehand"
|
||||
import { v4 as uuid } from 'uuid'
|
||||
import * as vec from 'utils/vec'
|
||||
import { DrawShape, ShapeType } from 'types'
|
||||
import { registerShapeUtils } from './index'
|
||||
import { intersectPolylineBounds } from 'utils/intersections'
|
||||
import { boundsContainPolygon } from 'utils/bounds'
|
||||
import getStroke from 'perfect-freehand'
|
||||
import {
|
||||
getBoundsFromPoints,
|
||||
getSvgPathFromStroke,
|
||||
translateBounds,
|
||||
} from "utils/utils"
|
||||
import { DotCircle } from "components/canvas/misc"
|
||||
import { shades } from "lib/colors"
|
||||
} from 'utils/utils'
|
||||
import { DotCircle } from 'components/canvas/misc'
|
||||
import { shades } from 'lib/colors'
|
||||
|
||||
const pathCache = new WeakMap<DrawShape, string>([])
|
||||
const pathCache = new WeakMap<number[][], string>([])
|
||||
|
||||
const draw = registerShapeUtils<DrawShape>({
|
||||
boundsCache: new WeakMap([]),
|
||||
|
@ -23,8 +23,8 @@ const draw = registerShapeUtils<DrawShape>({
|
|||
id: uuid(),
|
||||
type: ShapeType.Draw,
|
||||
isGenerated: false,
|
||||
name: "Draw",
|
||||
parentId: "page0",
|
||||
name: 'Draw',
|
||||
parentId: 'page0',
|
||||
childIndex: 0,
|
||||
point: [0, 0],
|
||||
points: [[0, 0]],
|
||||
|
@ -32,10 +32,10 @@ const draw = registerShapeUtils<DrawShape>({
|
|||
...props,
|
||||
style: {
|
||||
strokeWidth: 2,
|
||||
strokeLinecap: "round",
|
||||
strokeLinejoin: "round",
|
||||
strokeLinecap: 'round',
|
||||
strokeLinejoin: 'round',
|
||||
...props.style,
|
||||
stroke: "transparent",
|
||||
stroke: 'transparent',
|
||||
},
|
||||
}
|
||||
},
|
||||
|
@ -44,19 +44,18 @@ const draw = registerShapeUtils<DrawShape>({
|
|||
const { id, point, points } = shape
|
||||
|
||||
if (points.length < 2) {
|
||||
return <DotCircle cx={point[0]} cy={point[1]} r={3} />
|
||||
return <DotCircle id={id} cx={point[0]} cy={point[1]} r={3} />
|
||||
}
|
||||
|
||||
if (!pathCache.has(shape)) {
|
||||
pathCache.set(shape, getSvgPathFromStroke(getStroke(points)))
|
||||
if (!pathCache.has(points)) {
|
||||
pathCache.set(points, getSvgPathFromStroke(getStroke(points)))
|
||||
}
|
||||
|
||||
return <path id={id} d={pathCache.get(shape)} />
|
||||
return <path id={id} d={pathCache.get(points)} />
|
||||
},
|
||||
|
||||
applyStyles(shape, style) {
|
||||
Object.assign(shape.style, style)
|
||||
shape.style.fill = "transparent"
|
||||
return this
|
||||
},
|
||||
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import { current } from "immer"
|
||||
import { Data, DrawShape } from "types"
|
||||
import BaseSession from "./base-session"
|
||||
import { getShapeUtils } from "lib/shape-utils"
|
||||
import { getPage, simplify } from "utils/utils"
|
||||
import * as vec from "utils/vec"
|
||||
import commands from "state/commands"
|
||||
import { current } from 'immer'
|
||||
import { Data, DrawShape } from 'types'
|
||||
import BaseSession from './base-session'
|
||||
import { getShapeUtils } from 'lib/shape-utils'
|
||||
import { getPage, simplify } from 'utils/utils'
|
||||
import * as vec from 'utils/vec'
|
||||
import commands from 'state/commands'
|
||||
|
||||
export default class BrushSession extends BaseSession {
|
||||
origin: number[]
|
||||
|
@ -29,7 +29,7 @@ export default class BrushSession extends BaseSession {
|
|||
update = (data: Data, point: number[]) => {
|
||||
const { shapeId } = this
|
||||
|
||||
const lp = vec.med(this.previous, point)
|
||||
const lp = vec.med(this.previous, vec.toPrecision(point))
|
||||
this.points.push(vec.sub(lp, this.origin))
|
||||
this.previous = lp
|
||||
|
||||
|
@ -46,15 +46,7 @@ export default class BrushSession extends BaseSession {
|
|||
}
|
||||
|
||||
complete = (data: Data) => {
|
||||
commands.draw(
|
||||
data,
|
||||
this.shapeId,
|
||||
this.snapshot.points,
|
||||
simplify(this.points, 0.1 / data.camera.zoom).map(([x, y]) => [
|
||||
Math.trunc(x * 100) / 100,
|
||||
Math.trunc(y * 100) / 100,
|
||||
])
|
||||
)
|
||||
commands.draw(data, this.shapeId, this.snapshot.points, this.points)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
11
utils/vec.ts
11
utils/vec.ts
|
@ -6,7 +6,7 @@
|
|||
export function clamp(n: number, min: number): number
|
||||
export function clamp(n: number, min: number, max: number): number
|
||||
export function clamp(n: number, min: number, max?: number): number {
|
||||
return Math.max(min, typeof max !== "undefined" ? Math.min(n, max) : n)
|
||||
return Math.max(min, typeof max !== 'undefined' ? Math.min(n, max) : n)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -477,3 +477,12 @@ export function distanceToLineSegment(
|
|||
export function nudge(A: number[], B: number[], d: number) {
|
||||
return add(A, mul(uni(vec(A, B)), d))
|
||||
}
|
||||
|
||||
/**
|
||||
* Round a vector to a precision length.
|
||||
* @param a
|
||||
* @param n
|
||||
*/
|
||||
export function toPrecision(a: number[], n = 3) {
|
||||
return [+a[0].toPrecision(n), +a[1].toPrecision(n)]
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue