diff --git a/src/common/geometry.js b/src/common/geometry.js index 98ce19b9..a4cc905d 100644 --- a/src/common/geometry.js +++ b/src/common/geometry.js @@ -16,9 +16,11 @@ export const snapToGrid = (value, tolerance) => { return Math.round(value / tolerance) * tolerance } -export const distance = (v1, v2) => { - return Math.sqrt(Math.pow(v1.x - v2.x, 2.0) + Math.pow(v1.y - v2.y, 2.0)) -} +// Using x*x instead of Math.pow(x, 2) avoids function call overhead (~10-20x +// faster for squaring). +export const magnitude = (x, y) => Math.sqrt(x * x + y * y) + +export const distance = (v1, v2) => magnitude(v1.x - v2.x, v1.y - v2.y) // Calculate the centroid (geometric center) of a set of vertices. // Excludes duplicate closing vertex if present (would skew the average). diff --git a/src/features/app/rootSlice.js b/src/features/app/rootSlice.js index 70559a3b..2ff9b0c8 100644 --- a/src/features/app/rootSlice.js +++ b/src/features/app/rootSlice.js @@ -1,3 +1,4 @@ +/* global structuredClone */ import { combineReducers } from "redux" import machinesReducer from "@/features/machines/machinesSlice" import exporterReducer from "@/features/export/exporterSlice" @@ -21,7 +22,7 @@ const combinedReducer = combineReducers({ }) const resetPattern = (state, action) => { - const newState = JSON.parse(JSON.stringify(state)) // deep copy + const newState = structuredClone(state) newState.layers = undefined newState.effects = undefined @@ -39,7 +40,7 @@ const resetAll = (state, action) => { const loadPattern = (state, action) => { const { layers, effects, images } = action.payload - const newState = JSON.parse(JSON.stringify(state)) // deep copy + const newState = structuredClone(state) newState.layers = layers newState.effects = effects diff --git a/src/features/effects/EffectList.js b/src/features/effects/EffectList.js index 064769e0..a15dcff7 100644 --- a/src/features/effects/EffectList.js +++ b/src/features/effects/EffectList.js @@ -1,4 +1,4 @@ -import React from "react" +import React, { useCallback } from "react" import { useTranslation } from "react-i18next" import { useDispatch, useSelector } from "react-redux" import Button from "react-bootstrap/Button" @@ -20,14 +20,14 @@ import { } from "./effectsSlice" import { getEffect } from "@/features/effects/effectFactory" -const EffectRow = ({ +const EffectRow = React.memo(function EffectRow({ current, selected, effect, handleEffectSelected, handleToggleEffectVisible, t, -}) => { +}) { const { name, id, visible } = effect const instance = getEffect(effect.type) const { attributes, listeners, setNodeRef, transform, isDragging } = @@ -80,7 +80,7 @@ const EffectRow = ({ ) -} +}) const EffectList = ({ effects, selectedLayer }) => { const { t } = useTranslation() @@ -98,28 +98,40 @@ const EffectList = ({ effects, selectedLayer }) => { }), ) - const handleDragStart = ({ active }) => dispatch(setCurrentEffect(active.id)) + const handleDragStart = useCallback( + ({ active }) => dispatch(setCurrentEffect(active.id)), + [dispatch], + ) - const handleDragEnd = ({ active, over }) => { - if (!over) { - return - } - if (active.id !== over.id) { - const oldIndex = effects.findIndex((effect) => effect.id === active.id) - const newIndex = effects.findIndex((effect) => effect.id === over.id) - dispatch(moveEffect({ id: selectedLayer.id, oldIndex, newIndex })) - } - } + const handleDragEnd = useCallback( + ({ active, over }) => { + if (!over) { + return + } + if (active.id !== over.id) { + const oldIndex = effects.findIndex((effect) => effect.id === active.id) + const newIndex = effects.findIndex((effect) => effect.id === over.id) + dispatch(moveEffect({ id: selectedLayer.id, oldIndex, newIndex })) + } + }, + [dispatch, effects, selectedLayer?.id], + ) - const handleToggleEffectVisible = (id, visible) => { - dispatch(setCurrentEffect(id)) - dispatch(updateEffect({ id, visible: !visible })) - } + const handleToggleEffectVisible = useCallback( + (id, visible) => { + dispatch(setCurrentEffect(id)) + dispatch(updateEffect({ id, visible: !visible })) + }, + [dispatch], + ) - const handleEffectSelected = (event) => { - const id = event.target.closest(".list-group-item").id - dispatch(setCurrentEffect(id)) - } + const handleEffectSelected = useCallback( + (event) => { + const id = event.target.closest(".list-group-item").id + dispatch(setCurrentEffect(id)) + }, + [dispatch], + ) return ( { ) } -export default EffectList +export default React.memo(EffectList) diff --git a/src/features/layers/LayerList.js b/src/features/layers/LayerList.js index 2c448ab6..1c9ca65e 100644 --- a/src/features/layers/LayerList.js +++ b/src/features/layers/LayerList.js @@ -1,4 +1,4 @@ -import React from "react" +import React, { useCallback } from "react" import { useTranslation } from "react-i18next" import Button from "react-bootstrap/Button" import ListGroup from "react-bootstrap/ListGroup" @@ -23,7 +23,7 @@ import { updateLayer, } from "@/features/layers/layersSlice" -const LayerRow = ({ +const LayerRow = React.memo(function LayerRow({ current, selected, numLayers, @@ -31,7 +31,7 @@ const LayerRow = ({ handleLayerSelected, handleToggleLayerVisible, t, -}) => { +}) { const { name, id, visible } = layer const activeClass = current ? "active" : selected ? "selected" : "" const dragClass = numLayers > 1 ? "cursor-move" : "" @@ -90,7 +90,7 @@ const LayerRow = ({ ) -} +}) const LayerList = () => { const { t } = useTranslation() @@ -109,28 +109,40 @@ const LayerList = () => { const numLayers = useSelector(selectNumLayers) const layers = useSelector(selectAllLayers) - const handleLayerSelected = (event) => { - const id = event.target.closest(".list-group-item").id - dispatch(setCurrentLayer(id)) - } + const handleLayerSelected = useCallback( + (event) => { + const id = event.target.closest(".list-group-item").id + dispatch(setCurrentLayer(id)) + }, + [dispatch], + ) - const handleDragStart = ({ active }) => dispatch(setCurrentLayer(active.id)) + const handleDragStart = useCallback( + ({ active }) => dispatch(setCurrentLayer(active.id)), + [dispatch], + ) - const handleToggleLayerVisible = (id, visible) => { - dispatch(setCurrentLayer(id)) - dispatch(updateLayer({ id, visible: !visible })) - } + const handleToggleLayerVisible = useCallback( + (id, visible) => { + dispatch(setCurrentLayer(id)) + dispatch(updateLayer({ id, visible: !visible })) + }, + [dispatch], + ) - const handleDragEnd = ({ active, over }) => { - if (!over) { - return - } - if (active.id !== over.id) { - const oldIndex = layers.findIndex((layer) => layer.id === active.id) - const newIndex = layers.findIndex((layer) => layer.id === over.id) - dispatch(moveLayer({ oldIndex, newIndex })) - } - } + const handleDragEnd = useCallback( + ({ active, over }) => { + if (!over) { + return + } + if (active.id !== over.id) { + const oldIndex = layers.findIndex((layer) => layer.id === active.id) + const newIndex = layers.findIndex((layer) => layer.id === over.id) + dispatch(moveLayer({ oldIndex, newIndex })) + } + }, + [dispatch, layers], + ) return ( { } } - const handleChange = (attrs) => { - attrs.id = effect.id - dispatch(updateEffect(attrs)) - } + const handleChange = useCallback( + (attrs) => { + attrs.id = effect.id + dispatch(updateEffect(attrs)) + }, + [dispatch, effect?.id], + ) - const handleDragStart = (e) => { - if (e.currentTarget === e.target) { - if (isCurrent) { - handleChange({ dragging: true }) + const handleDragStart = useCallback( + (e) => { + if (e.currentTarget === e.target) { + if (isCurrent) { + handleChange({ dragging: true }) + } } - } - } + }, + [isCurrent, handleChange], + ) - const handleDragEnd = (e) => { - if (e.currentTarget === e.target) { - handleChange({ - dragging: false, - x: roundP(e.target.x(), 0), - y: roundP(-e.target.y(), 0), - }) - } - } + const handleDragEnd = useCallback( + (e) => { + if (e.currentTarget === e.target) { + handleChange({ + dragging: false, + x: roundP(e.target.x(), 0), + y: roundP(-e.target.y(), 0), + }) + } + }, + [handleChange], + ) - const handleTransform = (e) => { - const ref = shapeRef.current - const scaleX = Math.abs(ref.scaleX()) - const scaleY = Math.abs(ref.scaleY()) - const width = roundP(Math.max(5, effect.width * scaleX), 0) - const height = roundP(Math.max(5, effect.height * scaleY), 0) - const originalRotation = roundP(effect.rotation, 0) - let rotation = roundP(ref.rotation(), 0) + const handleTransform = useCallback( + (e) => { + const ref = shapeRef.current + const scaleX = Math.abs(ref.scaleX()) + const scaleY = Math.abs(ref.scaleY()) + const width = roundP(Math.max(5, effect.width * scaleX), 0) + const height = roundP(Math.max(5, effect.height * scaleY), 0) + const originalRotation = roundP(effect.rotation, 0) + let rotation = roundP(ref.rotation(), 0) - if ( - (width != effect.width || height != effect.height) && - rotation == originalRotation + 180.0 * Math.sign(rotation) - ) { - // node has been flipped while scaling - ref.rotation(originalRotation) - } + if ( + (width != effect.width || height != effect.height) && + rotation == originalRotation + 180.0 * Math.sign(rotation) + ) { + // node has been flipped while scaling + ref.rotation(originalRotation) + } - ref.scaleX(scaleX) - ref.scaleY(scaleY) - } + ref.scaleX(scaleX) + ref.scaleY(scaleY) + }, + [effect?.width, effect?.height, effect?.rotation], + ) - const handleTransformStart = (e) => { - if (e.currentTarget === e.target) { - handleChange({ dragging: true }) - } - } + const handleTransformStart = useCallback( + (e) => { + if (e.currentTarget === e.target) { + handleChange({ dragging: true }) + } + }, + [handleChange], + ) - const handleTransformEnd = (e) => { - if (e.currentTarget === e.target) { - const node = shapeRef.current - const width = roundP( - Math.max(5, effect.width * Math.abs(node.scaleX())), - 0, - ) - const height = roundP( - Math.max(5, effect.height * Math.abs(node.scaleY())), - 0, - ) - const rotation = roundP(node.rotation(), 0) + const handleTransformEnd = useCallback( + (e) => { + if (e.currentTarget === e.target) { + const node = shapeRef.current + const width = roundP( + Math.max(5, effect.width * Math.abs(node.scaleX())), + 0, + ) + const height = roundP( + Math.max(5, effect.height * Math.abs(node.scaleY())), + 0, + ) + const rotation = roundP(node.rotation(), 0) - node.scaleX(1) - node.scaleY(1) + node.scaleX(1) + node.scaleY(1) - handleChange({ - dragging: false, - width, - height, - rotation, - }) - } - } + handleChange({ + dragging: false, + width, + height, + rotation, + }) + } + }, + [effect?.width, effect?.height, handleChange], + ) - const handleWheel = (e) => { - if (isCurrent) { - e.evt.preventDefault() + const handleWheel = useCallback( + (e) => { + if (isCurrent) { + e.evt.preventDefault() - const deltaX = e.evt.deltaX - const deltaY = e.evt.deltaY + const deltaX = e.evt.deltaX + const deltaY = e.evt.deltaY - if (Math.abs(deltaX) > 0 || Math.abs(deltaY) > 0) { - dispatch( - updateEffect({ - width: scaleByWheel(effect.width, deltaX, deltaY), - height: scaleByWheel(effect.height, deltaX, deltaY), - id: effect.id, - }), - ) + if (Math.abs(deltaX) > 0 || Math.abs(deltaY) > 0) { + dispatch( + updateEffect({ + width: scaleByWheel(effect.width, deltaX, deltaY), + height: scaleByWheel(effect.height, deltaX, deltaY), + id: effect.id, + }), + ) + } } - } - } + }, + [isCurrent, dispatch, effect?.width, effect?.height, effect?.id], + ) // some effects never render anything if (!(height === 0 || height) && !(width === 0 || width)) { diff --git a/src/features/preview/ShapePreview.js b/src/features/preview/ShapePreview.js index b5ed5430..057e29ce 100644 --- a/src/features/preview/ShapePreview.js +++ b/src/features/preview/ShapePreview.js @@ -1,4 +1,4 @@ -import React, { useEffect } from "react" +import React, { useCallback, useEffect } from "react" import { useSelector, useDispatch } from "react-redux" import { Shape, Transformer, Group } from "react-konva" import { isEqual } from "lodash" @@ -318,112 +318,136 @@ const ShapePreview = (ownProps) => { context.fillStrokeShape(shape) } - const handleChange = (attrs) => { - attrs.id = layer.id - dispatch(updateLayer(attrs)) - } + const handleChange = useCallback( + (attrs) => { + attrs.id = layer.id + dispatch(updateLayer(attrs)) + }, + [dispatch, layer?.id], + ) - const handleDragStart = (e) => { - if (e.currentTarget === e.target) { - if (isCurrent) { - handleChange({ dragging: true }) + const handleDragStart = useCallback( + (e) => { + if (e.currentTarget === e.target) { + if (isCurrent) { + handleChange({ dragging: true }) + } } - } - } + }, + [isCurrent, handleChange], + ) - const handleDragEnd = (e) => { - if (e.currentTarget === e.target) { - handleChange({ - dragging: false, - x: roundP(e.target.x(), 0), - y: roundP(-e.target.y(), 0), - }) - } - } + const handleDragEnd = useCallback( + (e) => { + if (e.currentTarget === e.target) { + handleChange({ + dragging: false, + x: roundP(e.target.x(), 0), + y: roundP(-e.target.y(), 0), + }) + } + }, + [handleChange], + ) - const handleTransform = (e) => { - const ref = groupRef.current - const scaleX = Math.abs(ref.scaleX()) - const scaleY = Math.abs(ref.scaleY()) - const width = roundP(Math.max(5, layer.width * scaleX), 0) - const height = roundP(Math.max(5, layer.height * scaleY), 0) - const originalRotation = roundP(layer.rotation, 0) - let rotation = roundP(ref.rotation(), 0) - - if ( - (width != layer.width || height != layer.height) && - rotation == originalRotation + 180.0 * Math.sign(rotation) - ) { - // node has been flipped while scaling - ref.rotation(originalRotation) - } - ref.scaleX(scaleX) - ref.scaleY(scaleY) - } + const handleTransform = useCallback( + (e) => { + const ref = groupRef.current + const scaleX = Math.abs(ref.scaleX()) + const scaleY = Math.abs(ref.scaleY()) + const width = roundP(Math.max(5, layer.width * scaleX), 0) + const height = roundP(Math.max(5, layer.height * scaleY), 0) + const originalRotation = roundP(layer.rotation, 0) + let rotation = roundP(ref.rotation(), 0) + + if ( + (width != layer.width || height != layer.height) && + rotation == originalRotation + 180.0 * Math.sign(rotation) + ) { + // node has been flipped while scaling + ref.rotation(originalRotation) + } + ref.scaleX(scaleX) + ref.scaleY(scaleY) + }, + [layer?.width, layer?.height, layer?.rotation], + ) - const handleTransformStart = (e) => { - if (e.currentTarget === e.target) { - handleChange({ dragging: true }) - } - } + const handleTransformStart = useCallback( + (e) => { + if (e.currentTarget === e.target) { + handleChange({ dragging: true }) + } + }, + [handleChange], + ) - const handleTransformEnd = (e) => { - if (e.currentTarget === e.target) { - const node = groupRef.current - const width = roundP( - Math.max(5, layer.width * Math.abs(node.scaleX())), - 0, - ) - const height = roundP( - Math.max(5, layer.height * Math.abs(node.scaleY())), - 0, - ) - const rotation = roundP(node.rotation(), 0) + const handleTransformEnd = useCallback( + (e) => { + if (e.currentTarget === e.target) { + const node = groupRef.current + const width = roundP( + Math.max(5, layer.width * Math.abs(node.scaleX())), + 0, + ) + const height = roundP( + Math.max(5, layer.height * Math.abs(node.scaleY())), + 0, + ) + const rotation = roundP(node.rotation(), 0) - node.scaleX(1) - node.scaleY(1) + node.scaleX(1) + node.scaleY(1) - const changes = { - dragging: false, - width, - height, - rotation, - } + const changes = { + dragging: false, + width, + height, + rotation, + } - if (!layer.maintainAspectRatio) { - changes.aspectRatio = width / height + if (!layer.maintainAspectRatio) { + changes.aspectRatio = width / height + } + handleChange(changes) } - handleChange(changes) - } - } + }, + [layer?.width, layer?.height, layer?.maintainAspectRatio, handleChange], + ) - const handleWheel = (e) => { - if (isCurrent) { - e.evt.preventDefault() - - const deltaX = e.evt.deltaX - const deltaY = e.evt.deltaY - - if (Math.abs(deltaX) > 0 || Math.abs(deltaY) > 0) { - dispatch( - updateLayer({ - width: scaleByWheel(layer.width, deltaX, deltaY), - height: scaleByWheel(layer.height, deltaX, deltaY), - id: layer.id, - }), - ) + const handleWheel = useCallback( + (e) => { + if (isCurrent) { + e.evt.preventDefault() + + const deltaX = e.evt.deltaX + const deltaY = e.evt.deltaY + + if (Math.abs(deltaX) > 0 || Math.abs(deltaY) > 0) { + dispatch( + updateLayer({ + width: scaleByWheel(layer.width, deltaX, deltaY), + height: scaleByWheel(layer.height, deltaX, deltaY), + id: layer.id, + }), + ) + } } - } - } + }, + [isCurrent, dispatch, layer?.width, layer?.height, layer?.id], + ) - const handleClick = (e) => { - if (selectableEffect && !isCurrent) { - dispatch(setCurrentEffect(selectableEffect.id)) - } else { - dispatch(setCurrentLayer(ownProps.id)) - } - e.cancelBubble = true // don't bubble this up to the preview window - } + const handleClick = useCallback( + (e) => { + if (selectableEffect && !isCurrent) { + dispatch(setCurrentEffect(selectableEffect.id)) + } else { + dispatch(setCurrentLayer(ownProps.id)) + } + e.cancelBubble = true // don't bubble this up to the preview window + }, + [selectableEffect, isCurrent, dispatch, ownProps.id], + ) // Order of these layers is very important. The current layer or effect must always // be the last one in order for Konva to allow dragging and transformer manipulation. diff --git a/src/features/shapes/tessellation_twist/TessellationTwist.js b/src/features/shapes/tessellation_twist/TessellationTwist.js index ec5291d8..1af63cfc 100644 --- a/src/features/shapes/tessellation_twist/TessellationTwist.js +++ b/src/features/shapes/tessellation_twist/TessellationTwist.js @@ -1,6 +1,6 @@ import Victor from "victor" import Graph, { mix, getEulerianTrail } from "@/common/Graph" -import { cloneVertices } from "@/common/geometry" +import { cloneVertices, magnitude } from "@/common/geometry" import Shape from "../Shape" const vecTriangle = [ @@ -22,15 +22,10 @@ function getEdges(edges, a, b, c, count, settings) { if (count === 0) { if (settings.rotate > 0) { - da = - Math.sqrt(Math.pow(a.x, 2) + Math.pow(a.y, 2)) * - ((settings.rotate * Math.PI) / 180.0) - db = - Math.sqrt(Math.pow(b.x, 2) + Math.pow(b.y, 2)) * - ((settings.rotate * Math.PI) / 180.0) - dc = - Math.sqrt(Math.pow(c.x, 2) + Math.pow(c.y, 2)) * - ((settings.rotate * Math.PI) / 180.0) + const rotateRad = (settings.rotate * Math.PI) / 180.0 + da = magnitude(a.x, a.y) * rotateRad + db = magnitude(b.x, b.y) * rotateRad + dc = magnitude(c.x, c.y) * rotateRad } else { da = (settings.rotate * Math.PI) / 180.0 db = (settings.rotate * Math.PI) / 180.0