Starts implementing locked shapes
This commit is contained in:
parent
3329c16e57
commit
6118dfcc2c
9 changed files with 138 additions and 87 deletions
|
@ -1,7 +1,7 @@
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { Edge, Corner } from 'types'
|
import { Edge, Corner } from 'types'
|
||||||
import { useSelector } from 'state'
|
import { useSelector } from 'state'
|
||||||
import { getSelectedShapes, isMobile } from 'utils/utils'
|
import { getPage, getSelectedShapes, isMobile } from 'utils/utils'
|
||||||
|
|
||||||
import CenterHandle from './center-handle'
|
import CenterHandle from './center-handle'
|
||||||
import CornerHandle from './corner-handle'
|
import CornerHandle from './corner-handle'
|
||||||
|
@ -13,10 +13,18 @@ export default function Bounds() {
|
||||||
const isSelecting = useSelector((s) => s.isIn('selecting'))
|
const isSelecting = useSelector((s) => s.isIn('selecting'))
|
||||||
const zoom = useSelector((s) => s.data.camera.zoom)
|
const zoom = useSelector((s) => s.data.camera.zoom)
|
||||||
const bounds = useSelector((s) => s.values.selectedBounds)
|
const bounds = useSelector((s) => s.values.selectedBounds)
|
||||||
|
|
||||||
const rotation = useSelector(({ data }) =>
|
const rotation = useSelector(({ data }) =>
|
||||||
data.selectedIds.size === 1 ? getSelectedShapes(data)[0].rotation : 0
|
data.selectedIds.size === 1 ? getSelectedShapes(data)[0].rotation : 0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const isAllLocked = useSelector((s) => {
|
||||||
|
const page = getPage(s.data)
|
||||||
|
return Array.from(s.data.selectedIds.values()).every(
|
||||||
|
(id) => page.shapes[id].isLocked
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
if (!bounds) return null
|
if (!bounds) return null
|
||||||
if (!isSelecting) return null
|
if (!isSelecting) return null
|
||||||
|
|
||||||
|
@ -31,16 +39,28 @@ export default function Bounds() {
|
||||||
${(bounds.minY + bounds.maxY) / 2})
|
${(bounds.minY + bounds.maxY) / 2})
|
||||||
translate(${bounds.minX},${bounds.minY})`}
|
translate(${bounds.minX},${bounds.minY})`}
|
||||||
>
|
>
|
||||||
<CenterHandle bounds={bounds} />
|
<CenterHandle bounds={bounds} isLocked={isAllLocked} />
|
||||||
<EdgeHandle size={size} bounds={bounds} edge={Edge.Top} />
|
{!isAllLocked && (
|
||||||
<EdgeHandle size={size} bounds={bounds} edge={Edge.Right} />
|
<>
|
||||||
<EdgeHandle size={size} bounds={bounds} edge={Edge.Bottom} />
|
<EdgeHandle size={size} bounds={bounds} edge={Edge.Top} />
|
||||||
<EdgeHandle size={size} bounds={bounds} edge={Edge.Left} />
|
<EdgeHandle size={size} bounds={bounds} edge={Edge.Right} />
|
||||||
<CornerHandle size={size} bounds={bounds} corner={Corner.TopLeft} />
|
<EdgeHandle size={size} bounds={bounds} edge={Edge.Bottom} />
|
||||||
<CornerHandle size={size} bounds={bounds} corner={Corner.TopRight} />
|
<EdgeHandle size={size} bounds={bounds} edge={Edge.Left} />
|
||||||
<CornerHandle size={size} bounds={bounds} corner={Corner.BottomRight} />
|
<CornerHandle size={size} bounds={bounds} corner={Corner.TopLeft} />
|
||||||
<CornerHandle size={size} bounds={bounds} corner={Corner.BottomLeft} />
|
<CornerHandle size={size} bounds={bounds} corner={Corner.TopRight} />
|
||||||
<RotateHandle size={size} bounds={bounds} />
|
<CornerHandle
|
||||||
|
size={size}
|
||||||
|
bounds={bounds}
|
||||||
|
corner={Corner.BottomRight}
|
||||||
|
/>
|
||||||
|
<CornerHandle
|
||||||
|
size={size}
|
||||||
|
bounds={bounds}
|
||||||
|
corner={Corner.BottomLeft}
|
||||||
|
/>
|
||||||
|
<RotateHandle size={size} bounds={bounds} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</g>
|
</g>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,18 +1,34 @@
|
||||||
import styled from "styles"
|
import styled from 'styles'
|
||||||
import { Bounds } from "types"
|
import { Bounds } from 'types'
|
||||||
|
|
||||||
export default function CenterHandle({ bounds }: { bounds: Bounds }) {
|
export default function CenterHandle({
|
||||||
|
bounds,
|
||||||
|
isLocked,
|
||||||
|
}: {
|
||||||
|
bounds: Bounds
|
||||||
|
isLocked: boolean
|
||||||
|
}) {
|
||||||
return (
|
return (
|
||||||
<StyledBounds
|
<StyledBounds
|
||||||
width={bounds.width}
|
width={bounds.width}
|
||||||
height={bounds.height}
|
height={bounds.height}
|
||||||
pointerEvents="none"
|
pointerEvents="none"
|
||||||
|
isLocked={isLocked}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const StyledBounds = styled("rect", {
|
const StyledBounds = styled('rect', {
|
||||||
fill: "none",
|
fill: 'none',
|
||||||
stroke: "$bounds",
|
stroke: '$bounds',
|
||||||
zStrokeWidth: 2,
|
zStrokeWidth: 2,
|
||||||
|
|
||||||
|
variants: {
|
||||||
|
isLocked: {
|
||||||
|
true: {
|
||||||
|
zStrokeWidth: 1,
|
||||||
|
zDash: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
|
@ -43,6 +43,7 @@ export function ShapeOutline({ id }: { id: string }) {
|
||||||
as="use"
|
as="use"
|
||||||
href={'#' + id}
|
href={'#' + id}
|
||||||
transform={transform}
|
transform={transform}
|
||||||
|
isLocked={shape.isLocked}
|
||||||
{...events}
|
{...events}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
@ -55,4 +56,13 @@ const Indicator = styled('path', {
|
||||||
stroke: '$selected',
|
stroke: '$selected',
|
||||||
fill: 'transparent',
|
fill: 'transparent',
|
||||||
pointerEvents: 'all',
|
pointerEvents: 'all',
|
||||||
|
|
||||||
|
variants: {
|
||||||
|
isLocked: {
|
||||||
|
true: {
|
||||||
|
zDash: 2,
|
||||||
|
},
|
||||||
|
false: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,59 +1,57 @@
|
||||||
import Command from "./command"
|
import Command from './command'
|
||||||
import history from "../history"
|
import history from '../history'
|
||||||
import { Data, Corner, Edge } from "types"
|
import { Data } from 'types'
|
||||||
import { TransformSnapshot } from "state/sessions/transform-session"
|
import { TransformSnapshot } from 'state/sessions/transform-session'
|
||||||
import { getShapeUtils } from "lib/shape-utils"
|
import { getShapeUtils } from 'lib/shape-utils'
|
||||||
import { getPage } from "utils/utils"
|
import { getPage } from 'utils/utils'
|
||||||
|
|
||||||
export default function transformCommand(
|
export default function transformCommand(
|
||||||
data: Data,
|
data: Data,
|
||||||
before: TransformSnapshot,
|
before: TransformSnapshot,
|
||||||
after: TransformSnapshot,
|
after: TransformSnapshot
|
||||||
scaleX: number,
|
|
||||||
scaleY: number
|
|
||||||
) {
|
) {
|
||||||
history.execute(
|
history.execute(
|
||||||
data,
|
data,
|
||||||
new Command({
|
new Command({
|
||||||
name: "translate_shapes",
|
name: 'translate_shapes',
|
||||||
category: "canvas",
|
category: 'canvas',
|
||||||
do(data) {
|
do(data) {
|
||||||
const { type, selectedIds } = after
|
const { type, shapeBounds } = after
|
||||||
|
|
||||||
const { shapes } = getPage(data)
|
const { shapes } = getPage(data)
|
||||||
|
|
||||||
selectedIds.forEach((id) => {
|
for (let id in shapeBounds) {
|
||||||
const { initialShape, initialShapeBounds, transformOrigin } =
|
const { initialShape, initialShapeBounds, transformOrigin } =
|
||||||
after.shapeBounds[id]
|
shapeBounds[id]
|
||||||
const shape = shapes[id]
|
const shape = shapes[id]
|
||||||
|
|
||||||
getShapeUtils(shape).transform(shape, initialShapeBounds, {
|
getShapeUtils(shape).transform(shape, initialShapeBounds, {
|
||||||
type,
|
type,
|
||||||
initialShape,
|
initialShape,
|
||||||
|
transformOrigin,
|
||||||
scaleX: 1,
|
scaleX: 1,
|
||||||
scaleY: 1,
|
scaleY: 1,
|
||||||
transformOrigin,
|
|
||||||
})
|
})
|
||||||
})
|
}
|
||||||
},
|
},
|
||||||
undo(data) {
|
undo(data) {
|
||||||
const { type, selectedIds } = before
|
const { type, shapeBounds } = before
|
||||||
|
|
||||||
const { shapes } = getPage(data)
|
const { shapes } = getPage(data)
|
||||||
|
|
||||||
selectedIds.forEach((id) => {
|
for (let id in shapeBounds) {
|
||||||
const { initialShape, initialShapeBounds, transformOrigin } =
|
const { initialShape, initialShapeBounds, transformOrigin } =
|
||||||
before.shapeBounds[id]
|
shapeBounds[id]
|
||||||
const shape = shapes[id]
|
const shape = shapes[id]
|
||||||
|
|
||||||
getShapeUtils(shape).transform(shape, initialShapeBounds, {
|
getShapeUtils(shape).transform(shape, initialShapeBounds, {
|
||||||
type,
|
type,
|
||||||
initialShape,
|
initialShape,
|
||||||
|
transformOrigin,
|
||||||
scaleX: 1,
|
scaleX: 1,
|
||||||
scaleY: 1,
|
scaleY: 1,
|
||||||
transformOrigin,
|
|
||||||
})
|
})
|
||||||
})
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import Command from "./command"
|
import Command from './command'
|
||||||
import history from "../history"
|
import history from '../history'
|
||||||
import { TranslateSnapshot } from "state/sessions/translate-session"
|
import { TranslateSnapshot } from 'state/sessions/translate-session'
|
||||||
import { Data } from "types"
|
import { Data } from 'types'
|
||||||
import { getPage } from "utils/utils"
|
import { getPage } from 'utils/utils'
|
||||||
import { getShapeUtils } from "lib/shape-utils"
|
import { getShapeUtils } from 'lib/shape-utils'
|
||||||
|
|
||||||
export default function translateCommand(
|
export default function translateCommand(
|
||||||
data: Data,
|
data: Data,
|
||||||
|
@ -14,8 +14,8 @@ export default function translateCommand(
|
||||||
history.execute(
|
history.execute(
|
||||||
data,
|
data,
|
||||||
new Command({
|
new Command({
|
||||||
name: isCloning ? "clone_shapes" : "translate_shapes",
|
name: isCloning ? 'clone_shapes' : 'translate_shapes',
|
||||||
category: "canvas",
|
category: 'canvas',
|
||||||
manualSelection: true,
|
manualSelection: true,
|
||||||
do(data, initial) {
|
do(data, initial) {
|
||||||
if (initial) return
|
if (initial) return
|
||||||
|
|
|
@ -1,18 +1,19 @@
|
||||||
import { Data, Edge, Corner } from "types"
|
import { Data, Edge, Corner } from 'types'
|
||||||
import * as vec from "utils/vec"
|
import * as vec from 'utils/vec'
|
||||||
import BaseSession from "./base-session"
|
import BaseSession from './base-session'
|
||||||
import commands from "state/commands"
|
import commands from 'state/commands'
|
||||||
import { current } from "immer"
|
import { current } from 'immer'
|
||||||
import { getShapeUtils } from "lib/shape-utils"
|
import { getShapeUtils } from 'lib/shape-utils'
|
||||||
import {
|
import {
|
||||||
getBoundsCenter,
|
getBoundsCenter,
|
||||||
getBoundsFromPoints,
|
getBoundsFromPoints,
|
||||||
getCommonBounds,
|
getCommonBounds,
|
||||||
getPage,
|
getPage,
|
||||||
getRelativeTransformedBoundingBox,
|
getRelativeTransformedBoundingBox,
|
||||||
|
getSelectedShapes,
|
||||||
getShapes,
|
getShapes,
|
||||||
getTransformedBoundingBox,
|
getTransformedBoundingBox,
|
||||||
} from "utils/utils"
|
} from 'utils/utils'
|
||||||
|
|
||||||
export default class TransformSession extends BaseSession {
|
export default class TransformSession extends BaseSession {
|
||||||
scaleX = 1
|
scaleX = 1
|
||||||
|
@ -31,7 +32,7 @@ export default class TransformSession extends BaseSession {
|
||||||
update(data: Data, point: number[], isAspectRatioLocked = false) {
|
update(data: Data, point: number[], isAspectRatioLocked = false) {
|
||||||
const { transformType } = this
|
const { transformType } = this
|
||||||
|
|
||||||
const { selectedIds, shapeBounds, initialBounds } = this.snapshot
|
const { shapeBounds, initialBounds } = this.snapshot
|
||||||
|
|
||||||
const { shapes } = getPage(data)
|
const { shapes } = getPage(data)
|
||||||
|
|
||||||
|
@ -48,7 +49,7 @@ export default class TransformSession extends BaseSession {
|
||||||
|
|
||||||
// Now work backward to calculate a new bounding box for each of the shapes.
|
// Now work backward to calculate a new bounding box for each of the shapes.
|
||||||
|
|
||||||
selectedIds.forEach((id) => {
|
for (let id in shapeBounds) {
|
||||||
const { initialShape, initialShapeBounds, transformOrigin } =
|
const { initialShape, initialShapeBounds, transformOrigin } =
|
||||||
shapeBounds[id]
|
shapeBounds[id]
|
||||||
|
|
||||||
|
@ -69,15 +70,15 @@ export default class TransformSession extends BaseSession {
|
||||||
scaleY: this.scaleY,
|
scaleY: this.scaleY,
|
||||||
transformOrigin,
|
transformOrigin,
|
||||||
})
|
})
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cancel(data: Data) {
|
cancel(data: Data) {
|
||||||
const { currentPageId, selectedIds, shapeBounds } = this.snapshot
|
const { currentPageId, shapeBounds } = this.snapshot
|
||||||
|
|
||||||
const page = getPage(data, currentPageId)
|
const page = getPage(data, currentPageId)
|
||||||
|
|
||||||
selectedIds.forEach((id) => {
|
for (let id in shapeBounds) {
|
||||||
const shape = page.shapes[id]
|
const shape = page.shapes[id]
|
||||||
|
|
||||||
const { initialShape, initialShapeBounds, transformOrigin } =
|
const { initialShape, initialShapeBounds, transformOrigin } =
|
||||||
|
@ -90,35 +91,33 @@ export default class TransformSession extends BaseSession {
|
||||||
scaleY: 1,
|
scaleY: 1,
|
||||||
transformOrigin,
|
transformOrigin,
|
||||||
})
|
})
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
complete(data: Data) {
|
complete(data: Data) {
|
||||||
commands.transform(
|
commands.transform(
|
||||||
data,
|
data,
|
||||||
this.snapshot,
|
this.snapshot,
|
||||||
getTransformSnapshot(data, this.transformType),
|
getTransformSnapshot(data, this.transformType)
|
||||||
this.scaleX,
|
|
||||||
this.scaleY
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getTransformSnapshot(data: Data, transformType: Edge | Corner) {
|
export function getTransformSnapshot(data: Data, transformType: Edge | Corner) {
|
||||||
const {
|
const cData = current(data)
|
||||||
document: { pages },
|
const { currentPageId } = cData
|
||||||
selectedIds,
|
|
||||||
currentPageId,
|
|
||||||
} = current(data)
|
|
||||||
|
|
||||||
const pageShapes = pages[currentPageId].shapes
|
const initialShapes = getSelectedShapes(cData).filter(
|
||||||
|
(shape) => !shape.isLocked
|
||||||
|
)
|
||||||
|
const hasShapes = initialShapes.length > 0
|
||||||
|
|
||||||
// A mapping of selected shapes and their bounds
|
// A mapping of selected shapes and their bounds
|
||||||
const shapesBounds = Object.fromEntries(
|
const shapesBounds = Object.fromEntries(
|
||||||
Array.from(selectedIds.values()).map((id) => {
|
initialShapes.map((shape) => [
|
||||||
const shape = pageShapes[id]
|
shape.id,
|
||||||
return [shape.id, getShapeUtils(shape).getBounds(shape)]
|
getShapeUtils(shape).getBounds(shape),
|
||||||
})
|
])
|
||||||
)
|
)
|
||||||
|
|
||||||
const boundsArr = Object.values(shapesBounds)
|
const boundsArr = Object.values(shapesBounds)
|
||||||
|
@ -132,20 +131,19 @@ export function getTransformSnapshot(data: Data, transformType: Edge | Corner) {
|
||||||
// positions of the shape's bounds within the common bounds shape.
|
// positions of the shape's bounds within the common bounds shape.
|
||||||
return {
|
return {
|
||||||
type: transformType,
|
type: transformType,
|
||||||
|
hasShapes,
|
||||||
currentPageId,
|
currentPageId,
|
||||||
selectedIds: new Set(selectedIds),
|
|
||||||
initialBounds: bounds,
|
initialBounds: bounds,
|
||||||
shapeBounds: Object.fromEntries(
|
shapeBounds: Object.fromEntries(
|
||||||
Array.from(selectedIds.values()).map((id) => {
|
initialShapes.map((shape) => {
|
||||||
const shape = pageShapes[id]
|
const initialShapeBounds = shapesBounds[shape.id]
|
||||||
const initialShapeBounds = shapesBounds[id]
|
|
||||||
const ic = getBoundsCenter(initialShapeBounds)
|
const ic = getBoundsCenter(initialShapeBounds)
|
||||||
|
|
||||||
let ix = (ic[0] - initialInnerBounds.minX) / initialInnerBounds.width
|
let ix = (ic[0] - initialInnerBounds.minX) / initialInnerBounds.width
|
||||||
let iy = (ic[1] - initialInnerBounds.minY) / initialInnerBounds.height
|
let iy = (ic[1] - initialInnerBounds.minY) / initialInnerBounds.height
|
||||||
|
|
||||||
return [
|
return [
|
||||||
id,
|
shape.id,
|
||||||
{
|
{
|
||||||
initialShape: shape,
|
initialShape: shape,
|
||||||
initialShapeBounds,
|
initialShapeBounds,
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import { Data } from "types"
|
import { Data } from 'types'
|
||||||
import * as vec from "utils/vec"
|
import * as vec from 'utils/vec'
|
||||||
import BaseSession from "./base-session"
|
import BaseSession from './base-session'
|
||||||
import commands from "state/commands"
|
import commands from 'state/commands'
|
||||||
import { current } from "immer"
|
import { current } from 'immer'
|
||||||
import { v4 as uuid } from "uuid"
|
import { v4 as uuid } from 'uuid'
|
||||||
import { getChildIndexAbove, getPage, getSelectedShapes } from "utils/utils"
|
import { getChildIndexAbove, getPage, getSelectedShapes } from 'utils/utils'
|
||||||
import { getShapeUtils } from "lib/shape-utils"
|
import { getShapeUtils } from 'lib/shape-utils'
|
||||||
|
|
||||||
export default class TranslateSession extends BaseSession {
|
export default class TranslateSession extends BaseSession {
|
||||||
delta = [0, 0]
|
delta = [0, 0]
|
||||||
|
@ -89,6 +89,8 @@ export default class TranslateSession extends BaseSession {
|
||||||
}
|
}
|
||||||
|
|
||||||
complete(data: Data) {
|
complete(data: Data) {
|
||||||
|
if (!this.snapshot.hasShapes) return
|
||||||
|
|
||||||
commands.translate(
|
commands.translate(
|
||||||
data,
|
data,
|
||||||
this.snapshot,
|
this.snapshot,
|
||||||
|
@ -100,7 +102,8 @@ export default class TranslateSession extends BaseSession {
|
||||||
|
|
||||||
export function getTranslateSnapshot(data: Data) {
|
export function getTranslateSnapshot(data: Data) {
|
||||||
const cData = current(data)
|
const cData = current(data)
|
||||||
const shapes = getSelectedShapes(cData)
|
const shapes = getSelectedShapes(cData).filter((shape) => !shape.isLocked)
|
||||||
|
const hasShapes = shapes.length > 0
|
||||||
|
|
||||||
return {
|
return {
|
||||||
currentPageId: data.currentPageId,
|
currentPageId: data.currentPageId,
|
||||||
|
@ -110,6 +113,7 @@ export function getTranslateSnapshot(data: Data) {
|
||||||
id: uuid(),
|
id: uuid(),
|
||||||
childIndex: getChildIndexAbove(cData, shape.id),
|
childIndex: getChildIndexAbove(cData, shape.id),
|
||||||
})),
|
})),
|
||||||
|
hasShapes,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -141,7 +141,6 @@ const state = createState({
|
||||||
UNDO: 'undo',
|
UNDO: 'undo',
|
||||||
REDO: 'redo',
|
REDO: 'redo',
|
||||||
SAVED_CODE: 'saveCode',
|
SAVED_CODE: 'saveCode',
|
||||||
CANCELLED: 'clearSelectedIds',
|
|
||||||
DELETED: 'deleteSelectedIds',
|
DELETED: 'deleteSelectedIds',
|
||||||
STARTED_PINCHING: { to: 'pinching' },
|
STARTED_PINCHING: { to: 'pinching' },
|
||||||
INCREASED_CODE_FONT_SIZE: 'increaseCodeFontSize',
|
INCREASED_CODE_FONT_SIZE: 'increaseCodeFontSize',
|
||||||
|
@ -159,6 +158,7 @@ const state = createState({
|
||||||
states: {
|
states: {
|
||||||
notPointing: {
|
notPointing: {
|
||||||
on: {
|
on: {
|
||||||
|
CANCELLED: 'clearSelectedIds',
|
||||||
POINTED_CANVAS: { to: 'brushSelecting' },
|
POINTED_CANVAS: { to: 'brushSelecting' },
|
||||||
POINTED_BOUNDS: { to: 'pointingBounds' },
|
POINTED_BOUNDS: { to: 'pointingBounds' },
|
||||||
POINTED_BOUNDS_HANDLE: {
|
POINTED_BOUNDS_HANDLE: {
|
||||||
|
|
|
@ -43,6 +43,11 @@ const { styled, global, css, theme, getCssString } = createCss({
|
||||||
transitions: {},
|
transitions: {},
|
||||||
},
|
},
|
||||||
utils: {
|
utils: {
|
||||||
|
zDash: () => (value: number) => {
|
||||||
|
return {
|
||||||
|
strokeDasharray: `calc(${value}px / var(--camera-zoom)) calc(${value}px / var(--camera-zoom))`,
|
||||||
|
}
|
||||||
|
},
|
||||||
zStrokeWidth: () => (value: number | number[]) => {
|
zStrokeWidth: () => (value: number | number[]) => {
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
// const [val, min, max] = value
|
// const [val, min, max] = value
|
||||||
|
|
Loading…
Reference in a new issue