From df5bac537831fefd88dd903ef4084786a58d8c24 Mon Sep 17 00:00:00 2001 From: Bob Carmichael Date: Sun, 20 Nov 2022 15:31:21 -0500 Subject: [PATCH 001/126] have layer embed effects (not used outside model yet) --- src/features/app/store.js | 43 ++- src/features/layers/layersSlice.js | 201 ++++++++++---- src/features/layers/layersSlice.spec.js | 336 ++++++++++++++++++++++-- 3 files changed, 505 insertions(+), 75 deletions(-) diff --git a/src/features/app/store.js b/src/features/app/store.js index baf1bb61..f3bbb118 100644 --- a/src/features/app/store.js +++ b/src/features/app/store.js @@ -1,5 +1,7 @@ import { configureStore } from "@reduxjs/toolkit" import { combineReducers } from 'redux' +import uniqueId from 'lodash/uniqueId' + import appReducer from './appSlice' import machineReducer from '../machine/machineSlice' import exporterReducer from '../exporter/exporterSlice' @@ -7,7 +9,7 @@ import previewReducer from '../preview/previewSlice' import fontsReducer from '../fonts/fontsSlice' import { registeredShapes } from '../../models/shapes' import { loadState, saveState } from '../../common/localStorage' -import layersReducer, { setCurrentLayer, addLayer } from '../layers/layersSlice' +import layersReducer, { setCurrentLayer, addLayer, addEffect, updateLayer } from '../layers/layersSlice' //const customizedMiddleware = getDefaultMiddleware({ // immutableCheck: { @@ -32,14 +34,36 @@ const store = configureStore({ }) const loadPersistedLayers = (layers) => { - layers.allIds.forEach((id) => { - let layer = layers.byId[id] + layers.allIds.forEach(id => { + const layer = layers.byId[id] - if (layer.startingWidth === undefined) layer.startingWidth = layer.startingSize - if (layer.startingHeight === undefined) layer.startingHeight = layer.startingWidth - if (layer.autosize === undefined) layer.autosize = true + if (layer) { + const newLayer = { + ...layer, + id: uniqueId('layer-'), + restore: true, + startingWidth: layer.startingWidth || layer.startingSize, + startingHeight: layer.startingWidth || layer.startingSize, + autosize: layer.autosize === null ? true : layer.autosize + } - store.dispatch(addLayer(layer)) + // for referential integrity, we have to explicitly generate ids and + // re-build relationships. + store.dispatch(addLayer(newLayer)) + if (layer.effectIds) { + newLayer.effectIds = layer.effectIds.map(effectId => { + const effect = { + ...layers.byId[effectId], + id: uniqueId('layer-'), + restore: true, + parentId: newLayer.id + } + store.dispatch(addEffect(effect)) + return effect.id + }) + store.dispatch(updateLayer(newLayer)) + } + } }) } @@ -54,9 +78,10 @@ const loadDefaultLayer = () => { store.dispatch(setCurrentLayer(state.main.layers.byId[state.main.layers.allIds[0]].id)) } -// set to true when running locally if you want to preserve your shape +// set both to true when running locally if you want to preserve your shape // settings across page loads; don't forget to toggle false when done testing! -const persistState = false +const usePersistedState = true +const persistState = true // if you want to save a multiple temporary states, use these keys. The first time // you save a new state, change persistSaveKey. Make a change, then change diff --git a/src/features/layers/layersSlice.js b/src/features/layers/layersSlice.js index 08b99114..201ca679 100644 --- a/src/features/layers/layersSlice.js +++ b/src/features/layers/layersSlice.js @@ -7,8 +7,78 @@ const protectedAttrs = [ 'repeatEnabled', 'canTransform', 'selectGroup', 'canChangeSize', 'autosize', 'usesMachine', 'shouldCache', 'canChangeHeight', 'canRotate', 'usesFont' ] + const newLayerType = localStorage.getItem('currentShape') || 'polygon' const newLayerName = getShape({type: newLayerType}).name.toLowerCase() +const newEffectType = localStorage.getItem('currentEffect') || 'mask' +const newEffectName = getShape({type: newEffectType}).name.toLowerCase() + +function createLayer(state, attrs) { + const restore = attrs.restore + delete attrs.restore + const layer = { + ...attrs, + id: (restore && attrs.id) || uniqueId('layer-'), + name: attrs.name || state.newLayerName, + } + + state.byId[layer.id] = layer + return layer +} + +function deleteLayer(state, deleteId) { + const idx = state.allIds.findIndex(id => id === deleteId) + state.allIds.splice(idx, 1) + delete state.byId[deleteId] + return idx +} + +function deleteEffect(state, deleteId) { + const layer = state.byId[deleteId] + const parent = state.byId[layer.parentId] + + const idx = parent.effectIds.findIndex(id => id === deleteId) + parent.effectIds.splice(idx, 1) + if (parent.effectIds.length === 0) parent.locked = false + + delete state.byId[deleteId] + handleAfterDelete(state, deleteId, idx) +} + +function createEffect(state, parent, attrs) { + const effect = createLayer(state, { + ...attrs, + name: attrs.name || state.newEffectName + }) + + effect.parentId = parent.id + parent.effectIds ||= [] + parent.effectIds.push(effect.id) + parent.effectIds = [...new Set(parent.effectIds)] + + return effect +} + +function handleAfterDelete(state, deletedId, deletedIdx) { + if (deletedId === state.current) { + if (deletedIdx === state.allIds.length) { + setCurrentId(state, state.allIds[deletedIdx-1]) + } else { + setCurrentId(state, state.allIds[deletedIdx]) + } + } +} + +function currLayerIndex(state) { + const currentLayer = state.byId[state.current] + return state.allIds.findIndex(id => id === currentLayer.id) +} + +// TODO: remove this function when you refactor to remove 'selected' feature; currently disabled +function setCurrentId(state, id) { + state.selected = id + state.current = id +} const layersSlice = createSlice({ name: 'layer', @@ -18,26 +88,25 @@ const layersSlice = createSlice({ newLayerType: newLayerType, newLayerName: newLayerName, newLayerNameOverride: false, + newEffectType: newEffectType, + newEffectName: newEffectName, + newEffectNameOverride: false, copyLayerName: null, byId: {}, allIds: [] }, reducers: { addLayer(state, action) { - let layer = { ...action.payload } - layer.id = uniqueId('layer-') - layer.name = layer.name || state.newLayerName - state.byId[layer.id] = layer + const index = state.current ? currLayerIndex(state) + 1 : 0 + const layer = createLayer(state, action.payload) - const index = state.allIds.findIndex(id => id === state.current) + 1 state.allIds.splice(index, 0, layer.id) - - state.current = layer.id - state.selected = layer.id + setCurrentId(state, layer.id) state.newLayerNameOverride = false state.newLayerName = layer.name - if (layer.type !== 'file_import' && !layer.effect) { - localStorage.setItem('currentShape', layer.type) + + if (layer.type !== 'file_import') { + localStorage.setItem(layer.effect ? 'currentEffect' : 'currentShape', layer.type) } }, moveLayer(state, action) { @@ -46,31 +115,51 @@ const layersSlice = createSlice({ }, copyLayer(state, action) { const source = state.byId[action.payload] - const layer = { ...source, name: state.copyLayerName } - layer.id = uniqueId('layer-') - state.byId[layer.id] = layer + const layer = createLayer(state, { + ...source, + name: state.copyLayerName + }) + delete layer.effectIds + + if (source.effectIds) { + layer.effectIds = source.effectIds.map(effectId => { + return createEffect(state, layer, state.byId[effectId]).id + }) + } const index = state.allIds.findIndex(id => id === state.current) + 1 state.allIds.splice(index, 0, layer.id) - - state.current = layer.id - state.selected = layer.id + setCurrentId(state, layer.id) + state.copyLayerName = null }, removeLayer(state, action) { - const deleteId = action.payload - const idx = state.allIds.findIndex(id => id === deleteId) - state.allIds.splice(idx, 1) - delete state.byId[deleteId] - - if (deleteId === state.current) { - if (idx === state.allIds.length) { - state.current = state.allIds[idx-1] - state.selected = state.allIds[idx-1] - } else { - state.current = state.allIds[idx] - state.selected = state.allIds[idx] - } + const id = action.payload + const layer = state.byId[id] + + if (layer.effectIds) { + layer.effectIds.forEach(effectId => { + deleteEffect(state, effectId) + }) } + + const idx = deleteLayer(state, id) + handleAfterDelete(state, id, idx) + }, + addEffect(state, action) { + const parent = state.byId[action.payload.parentId] + if (parent === undefined) return + + const effect = createEffect(state, parent, action.payload) + parent.open = true + setCurrentId(state, effect.id) + }, + removeEffect(state, action) { + deleteEffect(state, action.payload) + }, + moveEffect(state, action) { + const { parentId, oldIndex, newIndex } = action.payload + const parent = state.byId[parentId] + parent.effectIds = arrayMove(parent.effectIds, oldIndex, newIndex) }, restoreDefaults(state, action) { const id = action.payload @@ -87,8 +176,7 @@ const layersSlice = createSlice({ const current = state.byId[action.payload] if (current) { - state.current = current.id - state.selected = current.id + setCurrentId(state, current.id) state.copyLayerName = current.name } }, @@ -121,36 +209,53 @@ const layersSlice = createSlice({ } Object.assign(state, attrs) }, + setNewEffectType(state, action) { + let attrs = { newEffectType: action.payload } + if (!state.newEffectNameOverride) { + const shape = getShape({type: action.payload}) + attrs.newEffectName = shape.name.toLowerCase() + } + Object.assign(state, attrs) + }, updateLayer(state, action) { const layer = action.payload - state.byId[layer.id] = {...state.byId[layer.id], ...layer} + const currLayer = state.byId[layer.id] + state.byId[layer.id] = {...currLayer, ...layer} }, updateLayers(state, action) { Object.assign(state, action.payload) }, toggleRepeat(state, action) { - const transform = action.payload - state.byId[transform.id].repeatEnabled = !state.byId[transform.id].repeatEnabled + const layer = action.payload + state.byId[layer.id].repeatEnabled = !state.byId[layer.id].repeatEnabled }, toggleGrow(state, action) { - const transform = action.payload - state.byId[transform.id].growEnabled = !state.byId[transform.id].growEnabled + const layer = action.payload + state.byId[layer.id].growEnabled = !state.byId[layer.id].growEnabled }, toggleSpin(state, action) { - const transform = action.payload - state.byId[transform.id].spinEnabled = !state.byId[transform.id].spinEnabled + const layer = action.payload + state.byId[layer.id].spinEnabled = !state.byId[layer.id].spinEnabled }, toggleTrack(state, action) { - const transform = action.payload - state.byId[transform.id].trackEnabled = !state.byId[transform.id].trackEnabled + const layer = action.payload + state.byId[layer.id].trackEnabled = !state.byId[layer.id].trackEnabled }, toggleTrackGrow(state, action) { - const transform = action.payload - state.byId[transform.id].trackGrowEnabled = !state.byId[transform.id].trackGrowEnabled + const layer = action.payload + state.byId[layer.id].trackGrowEnabled = !state.byId[layer.id].trackGrowEnabled + }, + toggleOpen(state, action) { + const layer = action.payload + state.byId[layer.id].open = !state.byId[layer.id].open }, toggleVisible(state, action) { - const transform = action.payload - state.byId[transform.id].visible = !state.byId[transform.id].visible + const layer = action.payload + state.byId[layer.id].visible = !state.byId[layer.id].visible + }, + toggleLocked(state, action) { + const layer = action.payload + state.byId[layer.id].locked = !state.byId[layer.id].locked }, } }) @@ -160,11 +265,15 @@ export const { copyLayer, moveLayer, removeLayer, + addEffect, + removeEffect, + moveEffect, restoreDefaults, setCurrentLayer, setSelectedLayer, setShapeType, setNewLayerType, + setNewEffectType, updateLayer, updateLayers, toggleRepeat, @@ -172,7 +281,9 @@ export const { toggleGrow, toggleTrack, toggleTrackGrow, - toggleVisible + toggleVisible, + toggleOpen, + toggleLocked } = layersSlice.actions export default layersSlice.reducer diff --git a/src/features/layers/layersSlice.spec.js b/src/features/layers/layersSlice.spec.js index 22abf19e..eb2645b3 100644 --- a/src/features/layers/layersSlice.spec.js +++ b/src/features/layers/layersSlice.spec.js @@ -1,21 +1,35 @@ +jest.mock('lodash/uniqueId') +const uniqueId = require('lodash/uniqueId') +import mockUniqueId, { resetUniqueIds } from '../../common/mocks' import layers, { addLayer, + removeLayer, copyLayer, moveLayer, restoreDefaults, + addEffect, + removeEffect, + moveEffect, setCurrentLayer, setSelectedLayer, setNewLayerType, + setNewEffectType, setShapeType, updateLayer, toggleSpin, toggleGrow, + toggleOpen, toggleRepeat, toggleTrack, toggleTrackGrow, toggleVisible } from './layersSlice' +beforeEach(() => { + resetUniqueIds() + uniqueId.mockImplementation((prefix) => mockUniqueId(prefix)) +}) + describe('layers reducer', () => { const initialState = { circleLobes: 1, @@ -34,6 +48,7 @@ describe('layers reducer', () => { startingHeight: 10, offsetX: 0.0, offsetY: 0.0, + open: true, rotation: 0, numLoops: 10, reverse: false, @@ -73,6 +88,9 @@ describe('layers reducer', () => { newLayerType: 'polygon', newLayerName: 'polygon', newLayerNameOverride: false, + newEffectNameOverride: false, + newEffectName: 'mask', + newEffectType: 'mask', copyLayerName: null, byId: {}, allIds: [] @@ -103,35 +121,141 @@ describe('layers reducer', () => { }) }) - it('should handle copyLayer', () => { - expect( - layers({ + describe('removeLayer', () => { + it('should remove layer', () => { + expect( + layers({ + byId: { + 'layer-1': { + id: 'layer-1', + name: 'foo' + } + }, + allIds: ['layer-1'], + current: 'layer-1', + copyLayerName: 'foo' + }, + removeLayer('layer-1')) + ).toEqual({ + byId: {}, + allIds: [], + current: undefined, + selected: undefined, + copyLayerName: 'foo' + }) + }) + + it('should remove effects associated with layer', () => { + expect( + layers({ + byId: { + 'layer': { + id: 'layer', + name: 'foo', + effectIds: ['effect'] + }, + 'effect': { + id: 'effect', + name: 'bar', + parentId: 'layer' + } + }, + allIds: ['layer'], + current: 'layer', + copyLayerName: 'foo' + }, + removeLayer('layer')) + ).toEqual({ + byId: {}, + allIds: [], + current: undefined, + selected: undefined, + copyLayerName: 'foo' + }) + }) + }) + + describe('copyLayer', () => { + it('should copy layer', () => { + expect( + layers({ + byId: { + 'layer-0': { + id: 'layer-0', + name: 'foo' + } + }, + allIds: ['layer-0'], + current: 'layer-0', + copyLayerName: 'foo' + }, + copyLayer('layer-0')) + ).toEqual({ byId: { + 'layer-0': { + id: 'layer-0', + name: 'foo' + }, 'layer-1': { id: 'layer-1', name: 'foo' } }, - allIds: ['layer-1'], + allIds: ['layer-0', 'layer-1'], current: 'layer-1', + selected: 'layer-1', copyLayerName: 'foo' - }, - copyLayer('layer-1')) - ).toEqual({ - byId: { - 'layer-1': { - id: 'layer-1', - name: 'foo' + }) + }) + + it('should copy effects', () => { + expect( + layers({ + byId: { + 'layer': { + id: 'layer', + name: 'foo', + effectIds: ['effect'] + }, + 'effect': { + id: 'effect', + name: 'bar', + parentId: 'layer' + } + }, + allIds: ['layer'], + current: 'layer', + copyLayerName: 'foo' }, - 'layer-2': { - id: 'layer-2', - name: 'foo' - } - }, - allIds: ['layer-1', 'layer-2'], - current: 'layer-2', - selected: 'layer-2', - copyLayerName: 'foo' + copyLayer('layer')) + ).toEqual({ + byId: { + 'layer': { + id: 'layer', + name: 'foo', + effectIds: ['effect'] + }, + 'effect': { + id: 'effect', + name: 'bar', + parentId: 'layer' + }, + 'layer-1': { + id: 'layer-1', + name: 'foo', + effectIds: ['layer-2'] + }, + 'layer-2': { + id: 'layer-2', + name: 'bar', + parentId: 'layer-1' + }, + }, + allIds: ['layer', 'layer-1'], + current: 'layer-1', + selected: 'layer-1', + copyLayerName: 'foo' + }) }) }) @@ -175,6 +299,141 @@ describe('layers reducer', () => { }) }) + describe('addEffect', () => { + it('when no parent layer, does nothing', () => { + const state = { + byId: { + 'layer-1': { + id: 'layer-1', + name: 'foo' + } + }, + } + expect( + layers(state, + addEffect({ + name: 'bar' + })) + ).toEqual(state) + }) + + it('adds effect', () => { + expect( + layers({ + byId: { + 'layer': { + id: 'layer', + name: 'foo' + } + }, + allIds: ['layer'], + current: 'layer' + }, + addEffect({ + name: 'bar', + parentId: 'layer' + })) + ).toEqual({ + byId: { + 'layer': { + id: 'layer', + name: 'foo', + open: true, + effectIds: ['layer-1'] + }, + 'layer-1': { + id: 'layer-1', + name: 'bar', + parentId: 'layer' + } + }, + allIds: ['layer'], + current: 'layer-1', + selected: 'layer-1' + }) + }) + }) + + it('should handle removeEffect', () => { + expect( + layers({ + byId: { + 'layer-1': { + id: 'layer-1', + name: 'foo', + effectIds: ['layer-2'] + }, + 'layer-2': { + id: 'layer-2', + name: 'bar', + parentId: 'layer-1' + }, + }, + allIds: ['layer-1'], + current: 'layer-2', + }, + removeEffect('layer-2')) + ).toEqual({ + byId: { + 'layer-1': { + id: 'layer-1', + name: 'foo', + effectIds: [] + }, + }, + allIds: ['layer-1'], + current: 'layer-1', + selected: 'layer-1' + }) + }) + + it('should handle moveEffect', () => { + expect( + layers( + { + byId: { + 'layer-1': { + id: 'layer-1', + name: 'foo', + effectIds: ['layer-2', 'layer-3'] + }, + 'layer-2': { + id: 'layer-2', + name: 'bar', + parentId: 'layer-1' + }, + 'layer-3': { + id: 'layer-3', + name: 'moo', + parentId: 'layer-1' + }, + }, + allIds: ['layer-1'] + }, + moveEffect({parentId: 'layer-1', oldIndex: 0, newIndex: 1}) + ) + ).toEqual({ + byId: { + 'layer-1': { + id: 'layer-1', + name: 'foo', + effectIds: ['layer-3', 'layer-2'] + }, + 'layer-2': { + id: 'layer-2', + name: 'bar', + parentId: 'layer-1' + }, + 'layer-3': { + id: 'layer-3', + name: 'moo', + parentId: 'layer-1' + }, + }, + allIds: ['layer-1'] + }) + }) + it('should handle setCurrentLayer', () => { expect( layers( @@ -424,7 +683,7 @@ describe('layers reducer', () => { }) }) - it('should handle toggleToggleVisible', () => { + it('should handle toggleVisible', () => { expect( layers( { @@ -445,6 +704,27 @@ describe('layers reducer', () => { }) }) + it('should handle toggleOpen', () => { + expect( + layers( + { + byId: { + '1': { + open: true + } + } + }, + toggleOpen({id: '1'}) + ) + ).toEqual({ + byId: { + '1': { + open: false + } + } + }) + }) + it('should handle setNewLayerType', () => { expect( layers( @@ -458,4 +738,18 @@ describe('layers reducer', () => { newLayerName: 'polygon' }) }) + + it('should handle setNewEffectType', () => { + expect( + layers( + { + newEffectType: 'mask' + }, + setNewEffectType('noise') + ) + ).toEqual({ + newEffectType: 'noise', + newEffectName: 'noise' + }) + }) }) From 9285a5c068389410c4efed2e6053118f013f462e Mon Sep 17 00:00:00 2001 From: Bob Carmichael Date: Sun, 20 Nov 2022 15:33:52 -0500 Subject: [PATCH 002/126] break playlist into smaller components; move buttons --- TODO.md | 22 ++ src/features/app/App.scss | 11 + src/features/exporter/Downloader.js | 2 +- src/features/layers/CopyLayer.js | 92 +++++++ src/features/layers/ImportLayer.js | 143 +++++++++++ src/features/layers/NewLayer.js | 122 +++++++++ src/features/layers/Playlist.js | 353 +++++++------------------- src/features/layers/Playlist.scss | 1 + src/features/layers/SortableLayers.js | 84 ++++++ src/models/shapes.js | 11 +- 10 files changed, 570 insertions(+), 271 deletions(-) create mode 100644 TODO.md create mode 100644 src/features/layers/CopyLayer.js create mode 100644 src/features/layers/ImportLayer.js create mode 100644 src/features/layers/NewLayer.js create mode 100644 src/features/layers/SortableLayers.js diff --git a/TODO.md b/TODO.md new file mode 100644 index 00000000..1615c9b3 --- /dev/null +++ b/TODO.md @@ -0,0 +1,22 @@ +TODO +--- +- DONE model: move effects into layers; add tests + - the logic is there, but I'm not yet using it +- ui: effects (I have code in the other branch for most of this) + - display within layer + - add/remove + - reorder +- move effects from playlist +- make transform an effect +- preview window + - konva transformer should be sized appropriately for any selected layer + - mask + - shape +- groups... +- other + - new layer dropdown is not remembering last shape selected + - upgrade to Bootstrap 5 (will probably do this in master and rebase) + +CONSIDER +--- +- Can effects within a layer be selected in the UI? I think the answer has to be yes; otherwise, mask will need to be a layer. Not all effects are selectable (like currently). This may be a little weird (two different selected rows affect the preview window). diff --git a/src/features/app/App.scss b/src/features/app/App.scss index 1677343b..2f8e35f5 100644 --- a/src/features/app/App.scss +++ b/src/features/app/App.scss @@ -97,6 +97,17 @@ h2 { } } +.btn-light { + background-color: white; + border-color: transparent !important; + box-shadow: none !important; + + &:focus-visible { + box-shadow: 0 0 0 0.2rem rgb(216 217 219 / 50%) !important; + border-color: #dae0e5 !important; + } +} + .btn-group>.btn.active, .btn-group>.btn:active, .btn-group>.btn:focus { z-index: inherit; } diff --git a/src/features/exporter/Downloader.js b/src/features/exporter/Downloader.js index 3129254f..fc924caa 100644 --- a/src/features/exporter/Downloader.js +++ b/src/features/exporter/Downloader.js @@ -219,7 +219,7 @@ class Downloader extends Component { - + diff --git a/src/features/layers/CopyLayer.js b/src/features/layers/CopyLayer.js new file mode 100644 index 00000000..45534e81 --- /dev/null +++ b/src/features/layers/CopyLayer.js @@ -0,0 +1,92 @@ +import React, { Component } from 'react' +import { Button, Modal, Row, Col, Form } from 'react-bootstrap' +import { connect } from 'react-redux' + +import { getLayers } from '../store/selectors' +import { copyLayer, updateLayers } from '../layers/layersSlice' +import { getCurrentLayer } from '../layers/selectors' + +const mapStateToProps = (state, ownProps) => { + const layers = getLayers(state) + const current = getCurrentLayer(state) + + return { + copyLayerName: layers.copyLayerName || current.name, + showModal: ownProps.showModal, + currentLayer: current + } +} + +const mapDispatchToProps = (dispatch, ownProps) => { + return { + toggleModal: () => { + ownProps.toggleModal() + }, + onChangeCopyName: (event) => { + dispatch(updateLayers({ copyLayerName: event.target.value })) + }, + onLayerCopied: (id) => { + dispatch(copyLayer(id)) + }, + } +} + +class CopyLayer extends Component { + render() { + const namedInputRef = React.createRef() + const { + currentLayer, copyLayerName, onChangeCopyName, onLayerCopied, toggleModal, showModal + } = this.props + + return namedInputRef.current.focus()} + > + + Copy {currentLayer.name} + + + + + + Name + + + + + + + + + + + + + } + + handleNameFocus(event) { + event.target.select() + } +} + +export default connect(mapStateToProps, mapDispatchToProps)(CopyLayer) diff --git a/src/features/layers/ImportLayer.js b/src/features/layers/ImportLayer.js new file mode 100644 index 00000000..87b06743 --- /dev/null +++ b/src/features/layers/ImportLayer.js @@ -0,0 +1,143 @@ +import React, { Component } from 'react' +import { Button, Modal, Form, Accordion, Card } from 'react-bootstrap' +import { connect } from 'react-redux' + +import ThetaRhoImporter from '../importer/ThetaRhoImporter' +import GCodeImporter from '../importer/GCodeImporter' +import { addLayer } from '../layers/layersSlice' +import { registeredShapes } from '../../models/shapes' +import ReactGA from 'react-ga' + +const mapStateToProps = (state, ownProps) => { + return { + showModal: ownProps.showModal + } +} + +const mapDispatchToProps = (dispatch, ownProps) => { + return { + toggleModal: () => { + ownProps.toggleModal() + }, + onLayerImport: (importProps) => { + const attrs = { + ...registeredShapes["file_import"].getInitialState(importProps), + name: importProps.fileName + } + dispatch(addLayer(attrs)) + }, + } +} + +class ImportLayer extends Component { + render() { + const { + toggleModal, showModal + } = this.props + + return + + Import new layer + + + + + + +

Import

+ Imports a pattern file as a new layer. Supported formats are .thr, .gcode, and .nc. + +
+
+
+
+

Where to get .thr files

+ Sisyphus machines use theta rho (.thr) files. There is a large community sharing them. + + +

About copyrights

+

Be careful and respectful. Understand that the original author put their labor, intensity, and ideas into this art. The creators have a right to own it (and they have a copyright, even if it doesn't say so). If you don't have permisson (a license) to use their art, then you shouldn't be. If you do have permission to use their art, then you should be thankful, and I'm sure they would appreciate you sending them a note of thanks. A picture of your table creating their shared art would probably make them smile.

+

Someone posting the .thr file to a forum or subreddit probably wants it to be shared, and drawing it on your home table is probably OK. Just be careful if you want to use them for something significant without explicit permission.

+

P.S. I am not a lawyer.

+
+
+ + + + +
+ } + + onFileSelected(event) { + let file = event.target.files[0] + let reader = new FileReader() + + reader.onload = (event) => { + this.startTime = performance.now() + var text = reader.result + + let importer + if (file.name.toLowerCase().endsWith('.thr')) { + importer = new ThetaRhoImporter(file.name, text) + } else if ( + file.name.toLowerCase().endsWith('.gcode') || + file.name.toLowerCase().endsWith('.nc') + ) { + importer = new GCodeImporter(file.name, text) + } + + importer.import(this.onFileImported.bind(this)) + this.props.toggleModal() + } + + reader.readAsText(file) + } + + onFileImported(importer, importerProps) { + this.props.onLayerImport(importerProps) + + this.endTime = performance.now() + ReactGA.timing({ + category: 'PatternImport', + variable: 'read' + importer.label, + value: this.endTime - this.startTime // in milliseconds + }) + } +} + +export default connect(mapStateToProps, mapDispatchToProps)(ImportLayer) diff --git a/src/features/layers/NewLayer.js b/src/features/layers/NewLayer.js new file mode 100644 index 00000000..df914b49 --- /dev/null +++ b/src/features/layers/NewLayer.js @@ -0,0 +1,122 @@ +import React, { Component } from 'react' +import Select from 'react-select' +import { Button, Modal, Row, Col, Form } from 'react-bootstrap' +import { connect } from 'react-redux' + +import { getLayers } from '../store/selectors' +import { getCurrentLayer } from '../layers/selectors' +import { registeredShapes, getShapeSelectOptions, getShape } from '../../models/shapes' +import { addLayer, updateLayers, setNewLayerType } from '../layers/layersSlice' + +const customStyles = { + control: base => ({ + ...base, + height: 55, + minHeight: 55 + }) +} + +const mapStateToProps = (state, ownProps) => { + const layers = getLayers(state) + const layer = getCurrentLayer(state) + + return { + newLayerType: layers.newLayerType, + newLayerName: layers.newLayerName, + currentLayer: layer, + selectOptions: getShapeSelectOptions(false), + showModal: ownProps.showModal + } +} + +const mapDispatchToProps = (dispatch, ownProps) => { + return { + onChangeNewType: (selected) => { + dispatch(setNewLayerType(selected.value)) + }, + onChangeNewName: (event) => { + dispatch(updateLayers({ newLayerName: event.target.value, newLayerNameOverride: true })) + }, + onLayerAdded: (type) => { + const attrs = registeredShapes[type].getInitialState() + dispatch(addLayer(attrs)) + }, + toggleModal: () => { + ownProps.toggleModal() + } + } +} + +class NewLayer extends Component { + render() { + const { + newLayerType, currentLayer, toggleModal, showModal, selectOptions, newLayerName, + onChangeNewType, onChangeNewName, onLayerAdded + } = this.props + const selectedShape = getShape({type: newLayerType}) + const selectedOption = { value: selectedShape.id, label: selectedShape.name } + + return + + Create new layer + + + + + + Type + + + - - - - - Name - - - - - - + - - - - - + - - - Import new layer - - - - - - -

Import

- Imports a pattern file as a new layer. Supported formats are .thr, .gcode, and .nc. - -
-
-
-
-

Where to get .thr files

- Sisyphus machines use theta rho (.thr) files. There is a large community sharing them. - - -

About copyrights

-

Be careful and respectful. Understand that the original author put their labor, intensity, and ideas into this art. The creators have a right to own it (and they have a copyright, even if it doesn't say so). If you don't have permisson (a license) to use their art, then you shouldn't be. If you do have permission to use their art, then you should be thankful, and I'm sure they would appreciate you sending them a note of thanks. A picture of your table creating their shared art would probably make them smile.

-

Someone posting the .thr file to a forum or subreddit probably wants it to be shared, and drawing it on your home table is probably OK. Just be careful if you want to use them for something significant without explicit permission.

-

P.S. I am not a lawyer.

-
-
- - - - -
- - namedInputRef.current.focus()} - > - - Copy {this.props.currentLayer.name} - - - - - - Name - - - - - - - - - - - - +
-

Layers ({this.props.numLayers})

- Layers ({numLayers}) + - - +
+ + +
+ {canRemove && } + +
+
) diff --git a/src/features/layers/Playlist.scss b/src/features/layers/Playlist.scss index f5b7f521..f79eedca 100644 --- a/src/features/layers/Playlist.scss +++ b/src/features/layers/Playlist.scss @@ -3,6 +3,7 @@ .active.list-group-item & { color: white; + background-color: inherit; } } diff --git a/src/features/layers/SortableLayers.js b/src/features/layers/SortableLayers.js new file mode 100644 index 00000000..c604a7f9 --- /dev/null +++ b/src/features/layers/SortableLayers.js @@ -0,0 +1,84 @@ +import React from 'react' +import { SortableContainer, SortableElement } from 'react-sortable-hoc' +import { Button, ListGroup } from 'react-bootstrap' +import { FaEye, FaEyeSlash, FaChevronRight, FaChevronDown, FaLock, FaUnlock } from 'react-icons/fa' + +const SortableLayer = SortableElement(( + { + active, + numLayers, + currentLayer, + layer, + onLayerSelected, + onEffectMoved, + onSortStarted, + onToggleLayerOpen, + onToggleLayerVisible, + onToggleLayerLocked + }) => { + const { name, id, visible, open, locked } = layer + const activeClass = active ? 'active' : '' + const dragClass = numLayers > 1 ? 'cursor-move' : '' + const visibleClass = visible ? '' : 'layer-hidden' + + return +
+
+ +
+ +
+ {name} +
+
+
+ } +) + +const SortableLayers = SortableContainer((props) => { + const { + layers, + currentLayer, + ...other + } = props + + return ( + + {layers.map((layer, index) => { + return ( + + ) + })} + + ) +}) + +export default SortableLayers diff --git a/src/models/shapes.js b/src/models/shapes.js index 4d4f0402..7d680ff4 100644 --- a/src/models/shapes.js +++ b/src/models/shapes.js @@ -68,7 +68,7 @@ export const getShapeDefaults = () => { }) } -export const getShapeSelectOptions = (includeEffects=true) => { +export const getShapeSelectOptions = () => { const groupOptions = [] const shapes = getShapeDefaults() @@ -84,11 +84,14 @@ export const getShapeSelectOptions = (includeEffects=true) => { } if (!found) { if (shape.selectGroup === 'import') { - // Users can't manually select this group. - continue - } else if (shape.selectGroup === 'effects' && !includeEffects) { + // users can't manually select this group continue + } else if (shape.selectGroup === 'effects') { + // effects are added separately + // TODO: when effects can be added separately, uncomment the next line + // continue } + const newOptions = [ optionLabel ] groupOptions.push( { label: shape.selectGroup, options: newOptions } ) } From 50ab862985f08579333e0e712b7d35f662f49d6f Mon Sep 17 00:00:00 2001 From: Bob Carmichael Date: Sun, 20 Nov 2022 16:07:26 -0500 Subject: [PATCH 003/126] jest and lint fixes --- package-lock.json | 147 +++++++++++------------- package.json | 2 +- src/common/mocks.js | 19 +++ src/features/app/store.js | 6 +- src/features/layers/ImportLayer.js | 4 +- src/features/layers/Playlist.js | 7 +- src/features/layers/SortableLayers.js | 7 +- src/features/layers/layersSlice.js | 8 +- src/features/layers/layersSlice.spec.js | 6 +- src/models/Shape.js | 1 + 10 files changed, 101 insertions(+), 106 deletions(-) create mode 100644 src/common/mocks.js diff --git a/package-lock.json b/package-lock.json index 4c507c15..c754ce8d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -53,7 +53,7 @@ "@babel/preset-react": "^7.16.7", "@esbuild-plugins/node-globals-polyfill": "^0.1.1", "@typescript-eslint/eslint-plugin": "^5.18.0", - "@typescript-eslint/parser": "^5.22.0", + "@typescript-eslint/parser": "^5.43.0", "@vitejs/plugin-react": "^1.0.7", "babel-jest": "^27.5.1", "eslint": "^8.13.0", @@ -2875,15 +2875,15 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "5.22.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.22.0.tgz", - "integrity": "sha512-piwC4krUpRDqPaPbFaycN70KCP87+PC5WZmrWs+DlVOxxmF+zI6b6hETv7Quy4s9wbkV16ikMeZgXsvzwI3icQ==", + "version": "5.43.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.43.0.tgz", + "integrity": "sha512-2iHUK2Lh7PwNUlhFxxLI2haSDNyXvebBO9izhjhMoDC+S3XI9qt2DGFUsiJ89m2k7gGYch2aEpYqV5F/+nwZug==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "5.22.0", - "@typescript-eslint/types": "5.22.0", - "@typescript-eslint/typescript-estree": "5.22.0", - "debug": "^4.3.2" + "@typescript-eslint/scope-manager": "5.43.0", + "@typescript-eslint/types": "5.43.0", + "@typescript-eslint/typescript-estree": "5.43.0", + "debug": "^4.3.4" }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -2902,13 +2902,13 @@ } }, "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/scope-manager": { - "version": "5.22.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.22.0.tgz", - "integrity": "sha512-yA9G5NJgV5esANJCO0oF15MkBO20mIskbZ8ijfmlKIvQKg0ynVKfHZ15/nhAJN5m8Jn3X5qkwriQCiUntC9AbA==", + "version": "5.43.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.43.0.tgz", + "integrity": "sha512-XNWnGaqAtTJsUiZaoiGIrdJYHsUOd3BZ3Qj5zKp9w6km6HsrjPk/TGZv0qMTWyWj0+1QOqpHQ2gZOLXaGA9Ekw==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.22.0", - "@typescript-eslint/visitor-keys": "5.22.0" + "@typescript-eslint/types": "5.43.0", + "@typescript-eslint/visitor-keys": "5.43.0" }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -2919,9 +2919,9 @@ } }, "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/types": { - "version": "5.22.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.22.0.tgz", - "integrity": "sha512-T7owcXW4l0v7NTijmjGWwWf/1JqdlWiBzPqzAWhobxft0SiEvMJB56QXmeCQjrPuM8zEfGUKyPQr/L8+cFUBLw==", + "version": "5.43.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.43.0.tgz", + "integrity": "sha512-jpsbcD0x6AUvV7tyOlyvon0aUsQpF8W+7TpJntfCUWU1qaIKu2K34pMwQKSzQH8ORgUrGYY6pVIh1Pi8TNeteg==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -2932,17 +2932,17 @@ } }, "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree": { - "version": "5.22.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.22.0.tgz", - "integrity": "sha512-EyBEQxvNjg80yinGE2xdhpDYm41so/1kOItl0qrjIiJ1kX/L/L8WWGmJg8ni6eG3DwqmOzDqOhe6763bF92nOw==", + "version": "5.43.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.43.0.tgz", + "integrity": "sha512-BZ1WVe+QQ+igWal2tDbNg1j2HWUkAa+CVqdU79L4HP9izQY6CNhXfkNwd1SS4+sSZAP/EthI1uiCSY/+H0pROg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.22.0", - "@typescript-eslint/visitor-keys": "5.22.0", - "debug": "^4.3.2", - "globby": "^11.0.4", + "@typescript-eslint/types": "5.43.0", + "@typescript-eslint/visitor-keys": "5.43.0", + "debug": "^4.3.4", + "globby": "^11.1.0", "is-glob": "^4.0.3", - "semver": "^7.3.5", + "semver": "^7.3.7", "tsutils": "^3.21.0" }, "engines": { @@ -2959,13 +2959,13 @@ } }, "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/visitor-keys": { - "version": "5.22.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.22.0.tgz", - "integrity": "sha512-DbgTqn2Dv5RFWluG88tn0pP6Ex0ROF+dpDO1TNNZdRtLjUr6bdznjA6f/qNqJLjd2PgguAES2Zgxh/JzwzETDg==", + "version": "5.43.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.43.0.tgz", + "integrity": "sha512-icl1jNH/d18OVHLfcwdL3bWUKsBeIiKYTGxMJCoGe7xFht+E4QgzOqoWYrU8XSLJWhVw8nTacbm03v23J/hFTg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.22.0", - "eslint-visitor-keys": "^3.0.0" + "@typescript-eslint/types": "5.43.0", + "eslint-visitor-keys": "^3.3.0" }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -10194,25 +10194,17 @@ "integrity": "sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==" }, "node_modules/semver": { - "version": "7.3.6", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.6.tgz", - "integrity": "sha512-HZWqcgwLsjaX1HBD31msI/rXktuIhS+lWvdE4kN9z+8IVT4Itc7vqU2WvYsyD6/sjYCt4dEKH/m1M3dwI9CC5w==", + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", "dependencies": { - "lru-cache": "^7.4.0" + "lru-cache": "^6.0.0" }, "bin": { "semver": "bin/semver.js" }, "engines": { - "node": "^10.0.0 || ^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/semver/node_modules/lru-cache": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.8.1.tgz", - "integrity": "sha512-E1v547OCgJvbvevfjgK9sNKIVXO96NnsTsFPBlg4ZxjhsJSODoH9lk8Bm0OxvHNm6Vm5Yqkl/1fErDxhYL8Skg==", - "engines": { - "node": ">=12" + "node": ">=10" } }, "node_modules/set-blocking": { @@ -13569,56 +13561,56 @@ } }, "@typescript-eslint/parser": { - "version": "5.22.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.22.0.tgz", - "integrity": "sha512-piwC4krUpRDqPaPbFaycN70KCP87+PC5WZmrWs+DlVOxxmF+zI6b6hETv7Quy4s9wbkV16ikMeZgXsvzwI3icQ==", + "version": "5.43.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.43.0.tgz", + "integrity": "sha512-2iHUK2Lh7PwNUlhFxxLI2haSDNyXvebBO9izhjhMoDC+S3XI9qt2DGFUsiJ89m2k7gGYch2aEpYqV5F/+nwZug==", "dev": true, "requires": { - "@typescript-eslint/scope-manager": "5.22.0", - "@typescript-eslint/types": "5.22.0", - "@typescript-eslint/typescript-estree": "5.22.0", - "debug": "^4.3.2" + "@typescript-eslint/scope-manager": "5.43.0", + "@typescript-eslint/types": "5.43.0", + "@typescript-eslint/typescript-estree": "5.43.0", + "debug": "^4.3.4" }, "dependencies": { "@typescript-eslint/scope-manager": { - "version": "5.22.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.22.0.tgz", - "integrity": "sha512-yA9G5NJgV5esANJCO0oF15MkBO20mIskbZ8ijfmlKIvQKg0ynVKfHZ15/nhAJN5m8Jn3X5qkwriQCiUntC9AbA==", + "version": "5.43.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.43.0.tgz", + "integrity": "sha512-XNWnGaqAtTJsUiZaoiGIrdJYHsUOd3BZ3Qj5zKp9w6km6HsrjPk/TGZv0qMTWyWj0+1QOqpHQ2gZOLXaGA9Ekw==", "dev": true, "requires": { - "@typescript-eslint/types": "5.22.0", - "@typescript-eslint/visitor-keys": "5.22.0" + "@typescript-eslint/types": "5.43.0", + "@typescript-eslint/visitor-keys": "5.43.0" } }, "@typescript-eslint/types": { - "version": "5.22.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.22.0.tgz", - "integrity": "sha512-T7owcXW4l0v7NTijmjGWwWf/1JqdlWiBzPqzAWhobxft0SiEvMJB56QXmeCQjrPuM8zEfGUKyPQr/L8+cFUBLw==", + "version": "5.43.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.43.0.tgz", + "integrity": "sha512-jpsbcD0x6AUvV7tyOlyvon0aUsQpF8W+7TpJntfCUWU1qaIKu2K34pMwQKSzQH8ORgUrGYY6pVIh1Pi8TNeteg==", "dev": true }, "@typescript-eslint/typescript-estree": { - "version": "5.22.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.22.0.tgz", - "integrity": "sha512-EyBEQxvNjg80yinGE2xdhpDYm41so/1kOItl0qrjIiJ1kX/L/L8WWGmJg8ni6eG3DwqmOzDqOhe6763bF92nOw==", + "version": "5.43.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.43.0.tgz", + "integrity": "sha512-BZ1WVe+QQ+igWal2tDbNg1j2HWUkAa+CVqdU79L4HP9izQY6CNhXfkNwd1SS4+sSZAP/EthI1uiCSY/+H0pROg==", "dev": true, "requires": { - "@typescript-eslint/types": "5.22.0", - "@typescript-eslint/visitor-keys": "5.22.0", - "debug": "^4.3.2", - "globby": "^11.0.4", + "@typescript-eslint/types": "5.43.0", + "@typescript-eslint/visitor-keys": "5.43.0", + "debug": "^4.3.4", + "globby": "^11.1.0", "is-glob": "^4.0.3", - "semver": "^7.3.5", + "semver": "^7.3.7", "tsutils": "^3.21.0" } }, "@typescript-eslint/visitor-keys": { - "version": "5.22.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.22.0.tgz", - "integrity": "sha512-DbgTqn2Dv5RFWluG88tn0pP6Ex0ROF+dpDO1TNNZdRtLjUr6bdznjA6f/qNqJLjd2PgguAES2Zgxh/JzwzETDg==", + "version": "5.43.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.43.0.tgz", + "integrity": "sha512-icl1jNH/d18OVHLfcwdL3bWUKsBeIiKYTGxMJCoGe7xFht+E4QgzOqoWYrU8XSLJWhVw8nTacbm03v23J/hFTg==", "dev": true, "requires": { - "@typescript-eslint/types": "5.22.0", - "eslint-visitor-keys": "^3.0.0" + "@typescript-eslint/types": "5.43.0", + "eslint-visitor-keys": "^3.3.0" } } } @@ -18871,18 +18863,11 @@ "integrity": "sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==" }, "semver": { - "version": "7.3.6", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.6.tgz", - "integrity": "sha512-HZWqcgwLsjaX1HBD31msI/rXktuIhS+lWvdE4kN9z+8IVT4Itc7vqU2WvYsyD6/sjYCt4dEKH/m1M3dwI9CC5w==", + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", "requires": { - "lru-cache": "^7.4.0" - }, - "dependencies": { - "lru-cache": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.8.1.tgz", - "integrity": "sha512-E1v547OCgJvbvevfjgK9sNKIVXO96NnsTsFPBlg4ZxjhsJSODoH9lk8Bm0OxvHNm6Vm5Yqkl/1fErDxhYL8Skg==" - } + "lru-cache": "^6.0.0" } }, "set-blocking": { diff --git a/package.json b/package.json index 4f379d6a..fb84c90e 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "@babel/preset-react": "^7.16.7", "@esbuild-plugins/node-globals-polyfill": "^0.1.1", "@typescript-eslint/eslint-plugin": "^5.18.0", - "@typescript-eslint/parser": "^5.22.0", + "@typescript-eslint/parser": "^5.43.0", "@vitejs/plugin-react": "^1.0.7", "babel-jest": "^27.5.1", "eslint": "^8.13.0", diff --git a/src/common/mocks.js b/src/common/mocks.js new file mode 100644 index 00000000..1bf2cfbe --- /dev/null +++ b/src/common/mocks.js @@ -0,0 +1,19 @@ +// mock implementation of lodash uniqueId, so that we can reset during tests +const idCounter = {} + +export default function uniqueId(prefix='$lodash$') { + if (!idCounter[prefix]) { + idCounter[prefix] = 0 + } + + const id =++idCounter[prefix] + if (prefix === '$lodash$') { + return `${id}` + } + + return `${prefix}${id}` +} + +export const resetUniqueIds = () => { + Object.keys(idCounter).forEach(key => delete idCounter[key]) +} diff --git a/src/features/app/store.js b/src/features/app/store.js index f3bbb118..da9513cf 100644 --- a/src/features/app/store.js +++ b/src/features/app/store.js @@ -80,8 +80,8 @@ const loadDefaultLayer = () => { // set both to true when running locally if you want to preserve your shape // settings across page loads; don't forget to toggle false when done testing! -const usePersistedState = true -const persistState = true +const usePersistedState = false +const persistState = false // if you want to save a multiple temporary states, use these keys. The first time // you save a new state, change persistSaveKey. Make a change, then change @@ -89,7 +89,7 @@ const persistState = true const persistInitKey = 'state' const persistSaveKey = 'state' -if (typeof jest === 'undefined' && persistState) { +if (typeof jest === 'undefined' && usePersistedState) { // override default values with saved ones const persistedState = loadState(persistInitKey) diff --git a/src/features/layers/ImportLayer.js b/src/features/layers/ImportLayer.js index 87b06743..e3b2aa9e 100644 --- a/src/features/layers/ImportLayer.js +++ b/src/features/layers/ImportLayer.js @@ -71,7 +71,7 @@ class ImportLayer extends Component { @@ -85,7 +85,7 @@ class ImportLayer extends Component {

About copyrights

-

Be careful and respectful. Understand that the original author put their labor, intensity, and ideas into this art. The creators have a right to own it (and they have a copyright, even if it doesn't say so). If you don't have permisson (a license) to use their art, then you shouldn't be. If you do have permission to use their art, then you should be thankful, and I'm sure they would appreciate you sending them a note of thanks. A picture of your table creating their shared art would probably make them smile.

+

Be careful and respectful. Understand that the original author put their labor, intensity, and ideas into this art. The creators have a right to own it (and they have a copyright, even if it doesn't say so). If you don't have permisson (a license) to use their art, then you shouldn't be. If you do have permission to use their art, then you should be thankful, and I'm sure they would appreciate you sending them a note of thanks. A picture of your table creating their shared art would probably make them smile.

Someone posting the .thr file to a forum or subreddit probably wants it to be shared, and drawing it on your home table is probably OK. Just be careful if you want to use them for something significant without explicit permission.

P.S. I am not a lawyer.

diff --git a/src/features/layers/Playlist.js b/src/features/layers/Playlist.js index 73376cb3..7d87dadf 100644 --- a/src/features/layers/Playlist.js +++ b/src/features/layers/Playlist.js @@ -2,10 +2,10 @@ import React, { Component } from 'react' import { connect } from 'react-redux' import { Button } from 'react-bootstrap' import { FaTrash, FaCopy, FaPlusSquare } from 'react-icons/fa' -import { MdLibraryAdd, MdOutlineFileUpload } from 'react-icons/md' +import { MdOutlineFileUpload } from 'react-icons/md' import { getCurrentLayer, getNumLayers, getAllLayersInfo } from '../layers/selectors' -import { setCurrentLayer, addLayer, removeLayer, moveLayer, moveEffect, removeEffect, toggleVisible, toggleOpen, toggleLocked } from '../layers/layersSlice' +import { setCurrentLayer, addLayer, removeLayer, moveLayer, toggleVisible, toggleOpen } from '../layers/layersSlice' import { registeredShapes, getShape } from '../../models/shapes' import NewLayer from './NewLayer' import CopyLayer from './CopyLayer' @@ -51,9 +51,6 @@ const mapDispatchToProps = (dispatch, ownProps) => { onToggleLayerVisible: (id) => { dispatch(toggleVisible({ id: id })) }, - onToggleLayerLocked: (id) => { - dispatch(toggleLocked({ id: id })) - } } } diff --git a/src/features/layers/SortableLayers.js b/src/features/layers/SortableLayers.js index c604a7f9..4eb9be56 100644 --- a/src/features/layers/SortableLayers.js +++ b/src/features/layers/SortableLayers.js @@ -1,7 +1,7 @@ import React from 'react' import { SortableContainer, SortableElement } from 'react-sortable-hoc' import { Button, ListGroup } from 'react-bootstrap' -import { FaEye, FaEyeSlash, FaChevronRight, FaChevronDown, FaLock, FaUnlock } from 'react-icons/fa' +import { FaEye, FaEyeSlash } from 'react-icons/fa' const SortableLayer = SortableElement(( { @@ -13,10 +13,9 @@ const SortableLayer = SortableElement(( onEffectMoved, onSortStarted, onToggleLayerOpen, - onToggleLayerVisible, - onToggleLayerLocked + onToggleLayerVisible }) => { - const { name, id, visible, open, locked } = layer + const { name, id, visible } = layer const activeClass = active ? 'active' : '' const dragClass = numLayers > 1 ? 'cursor-move' : '' const visibleClass = visible ? '' : 'layer-hidden' diff --git a/src/features/layers/layersSlice.js b/src/features/layers/layersSlice.js index 201ca679..a1bf42e6 100644 --- a/src/features/layers/layersSlice.js +++ b/src/features/layers/layersSlice.js @@ -5,7 +5,7 @@ import { getShape } from '../../models/shapes' const protectedAttrs = [ 'repeatEnabled', 'canTransform', 'selectGroup', 'canChangeSize', 'autosize', - 'usesMachine', 'shouldCache', 'canChangeHeight', 'canRotate', 'usesFont' + 'usesMachine', 'shouldCache', 'canChangeHeight', 'canRotate', 'usesFonts' ] const newLayerType = localStorage.getItem('currentShape') || 'polygon' @@ -39,7 +39,6 @@ function deleteEffect(state, deleteId) { const idx = parent.effectIds.findIndex(id => id === deleteId) parent.effectIds.splice(idx, 1) - if (parent.effectIds.length === 0) parent.locked = false delete state.byId[deleteId] handleAfterDelete(state, deleteId, idx) @@ -253,10 +252,6 @@ const layersSlice = createSlice({ const layer = action.payload state.byId[layer.id].visible = !state.byId[layer.id].visible }, - toggleLocked(state, action) { - const layer = action.payload - state.byId[layer.id].locked = !state.byId[layer.id].locked - }, } }) @@ -283,7 +278,6 @@ export const { toggleTrackGrow, toggleVisible, toggleOpen, - toggleLocked } = layersSlice.actions export default layersSlice.reducer diff --git a/src/features/layers/layersSlice.spec.js b/src/features/layers/layersSlice.spec.js index eb2645b3..9f6179e6 100644 --- a/src/features/layers/layersSlice.spec.js +++ b/src/features/layers/layersSlice.spec.js @@ -1,5 +1,5 @@ jest.mock('lodash/uniqueId') -const uniqueId = require('lodash/uniqueId') +const uniqueId = require('lodash/uniqueId') // eslint-disable-line @typescript-eslint/no-var-requires import mockUniqueId, { resetUniqueIds } from '../../common/mocks' import layers, { addLayer, @@ -204,7 +204,7 @@ describe('layers reducer', () => { allIds: ['layer-0', 'layer-1'], current: 'layer-1', selected: 'layer-1', - copyLayerName: 'foo' + copyLayerName: null }) }) @@ -254,7 +254,7 @@ describe('layers reducer', () => { allIds: ['layer', 'layer-1'], current: 'layer-1', selected: 'layer-1', - copyLayerName: 'foo' + copyLayerName: null }) }) }) diff --git a/src/models/Shape.js b/src/models/Shape.js index c02828ee..f027c1fa 100644 --- a/src/models/Shape.js +++ b/src/models/Shape.js @@ -28,6 +28,7 @@ export default class Shape { startingHeight: 10, offsetX: 0.0, offsetY: 0.0, + open: true, rotation: 0, numLoops: 10, transformMethod: 'smear', From 7c511c5bb49f3bc2b64f19de9cdb28ff8f41b0d1 Mon Sep 17 00:00:00 2001 From: Jeff Eberl Date: Mon, 21 Nov 2022 11:46:45 -0700 Subject: [PATCH 004/126] Move loop/spin/rotate to new effect, from shape. --- src/features/exporter/CommentExporter.js | 35 +--- src/features/layers/layersSlice.js | 10 -- src/features/layers/layersSlice.spec.js | 55 ------ src/features/machine/computer.js | 52 +----- src/features/transforms/RotationTransform.js | 107 ------------ src/features/transforms/ScaleTransform.js | 96 ----------- src/features/transforms/Transforms.js | 48 ------ src/models/Loop.js | 170 +++++++++++++++++++ src/models/Shape.js | 14 +- src/models/Transform.js | 47 ----- src/models/shapes.js | 2 + 11 files changed, 185 insertions(+), 451 deletions(-) delete mode 100644 src/features/transforms/RotationTransform.js delete mode 100644 src/features/transforms/ScaleTransform.js create mode 100644 src/models/Loop.js diff --git a/src/features/exporter/CommentExporter.js b/src/features/exporter/CommentExporter.js index 1c9d7344..31923ebb 100644 --- a/src/features/exporter/CommentExporter.js +++ b/src/features/exporter/CommentExporter.js @@ -43,34 +43,15 @@ export default class CommentExporter extends Exporter { this.optionLines(shape, layer, Object.keys(options)) this.keyValueLine('Visible', layer.visible) this.optionLines(transform, layer, ['startingWidth', 'startingHeight', 'offsetX', 'offsetY', 'rotation', 'reverse']) - this.optionLines(transform, layer, ['numLoops', 'transformMethod'], layer.repeatEnabled) - this.optionLine(transform, layer, 'growEnabled', layer.repeatEnabled) - this.indent() - this.optionLine(transform, layer, 'growValue', layer.repeatEnabled && layer.growEnabled) - this.optionLine(transform, layer, 'growMethod', layer.repeatEnabled && layer.growEnabled) - this.indent() - this.optionLine(transform, layer, 'growMath', layer.repeatEnabled && layer.growEnabled && layer.growMethod === 'function') - this.dedent() - this.dedent() - - this.optionLine(transform, layer, 'spinEnabled', layer.repeatEnabled) - this.indent() - this.optionLines(transform, layer, ['spinValue', 'spinMethod'], layer.repeatEnabled && layer.spinEnabled) - this.indent() - this.optionLine(transform, layer, 'spinMath', layer.repeatEnabled && layer.spinEnabled && layer.spinMethod === 'function') - this.optionLine(transform, layer, 'spinSwitchbacks', layer.repeatEnabled && layer.spinEnabled && layer.spinMethod === 'constant') - this.dedent() - this.dedent() - - this.optionLine(transform, layer, 'trackEnabled', layer.repeatEnabled) - this.indent() - this.optionLines(transform, layer, ['trackValue', 'trackLength', 'trackNumLoops'], layer.repeatEnabled && layer.trackEnabled) - this.optionLine(transform, layer, 'trackGrowEnabled', layer.repeatEnabled && layer.trackEnabled) - this.indent() - this.optionLine(transform, layer, 'trackGrow', layer.repeatEnabled && layer.trackGrowEnabled) - this.dedent() - this.dedent() + // this.optionLine(transform, layer, 'trackEnabled', layer.repeatEnabled) + // this.indent() + // this.optionLines(transform, layer, ['trackValue', 'trackLength', 'trackNumLoops'], layer.repeatEnabled && layer.trackEnabled) + // this.optionLine(transform, layer, 'trackGrowEnabled', layer.repeatEnabled && layer.trackEnabled) + // this.indent() + // this.optionLine(transform, layer, 'trackGrow', layer.repeatEnabled && layer.trackGrowEnabled) + // this.dedent() + // this.dedent() if (!layer.effect) { this.line('Fine tuning:') diff --git a/src/features/layers/layersSlice.js b/src/features/layers/layersSlice.js index a1bf42e6..2ef0c677 100644 --- a/src/features/layers/layersSlice.js +++ b/src/features/layers/layersSlice.js @@ -228,14 +228,6 @@ const layersSlice = createSlice({ const layer = action.payload state.byId[layer.id].repeatEnabled = !state.byId[layer.id].repeatEnabled }, - toggleGrow(state, action) { - const layer = action.payload - state.byId[layer.id].growEnabled = !state.byId[layer.id].growEnabled - }, - toggleSpin(state, action) { - const layer = action.payload - state.byId[layer.id].spinEnabled = !state.byId[layer.id].spinEnabled - }, toggleTrack(state, action) { const layer = action.payload state.byId[layer.id].trackEnabled = !state.byId[layer.id].trackEnabled @@ -272,8 +264,6 @@ export const { updateLayer, updateLayers, toggleRepeat, - toggleSpin, - toggleGrow, toggleTrack, toggleTrackGrow, toggleVisible, diff --git a/src/features/layers/layersSlice.spec.js b/src/features/layers/layersSlice.spec.js index 9f6179e6..d784a104 100644 --- a/src/features/layers/layersSlice.spec.js +++ b/src/features/layers/layersSlice.spec.js @@ -16,8 +16,6 @@ import layers, { setNewEffectType, setShapeType, updateLayer, - toggleSpin, - toggleGrow, toggleOpen, toggleRepeat, toggleTrack, @@ -58,17 +56,6 @@ describe('layers reducer', () => { drawPortionPct: 100, effect: false, rotateStartingPct: 0, - growEnabled: true, - growValue: 100, - growMethod: 'constant', - growMath: 'i+cos(i/2)', - growMathInput: 'i+cos(i/2)', - spinEnabled: false, - spinValue: 2, - spinMethod: 'constant', - spinMath: '10*sin(i/4)', - spinMathInput: '10*sin(i/4)', - spinSwitchbacks: 0, trackEnabled: false, trackGrowEnabled: false, trackValue: 10, @@ -578,48 +565,6 @@ describe('layers reducer', () => { }) }) - it('should handle toggleGrow', () => { - expect( - layers( - { - byId: { - '1': { - growEnabled: false - } - } - }, - toggleGrow({id: '1'}) - ) - ).toEqual({ - byId: { - '1': { - growEnabled: true - } - } - }) - }) - - it('should handle toggleSpin', () => { - expect( - layers( - { - byId: { - '1': { - spinEnabled: false - } - } - }, - toggleSpin({id: '1'}) - ) - ).toEqual({ - byId: { - '1': { - spinEnabled: true - } - } - }) - }) - it('should handle toggleRepeat', () => { expect( layers( diff --git a/src/features/machine/computer.js b/src/features/machine/computer.js index 80522515..8e149bbc 100644 --- a/src/features/machine/computer.js +++ b/src/features/machine/computer.js @@ -25,48 +25,9 @@ export const transformShape = (data, vertex, amount, trackIndex=0, numLoops) => numLoops = numLoops || data.numLoops let transformedVertex = vertex ? vertex.clone() : new Victor(0.0) - if (data.repeatEnabled && data.growEnabled) { - let growAmount = 100 - if (data.growMethod === 'function') { - try { - growAmount = data.growValue * evaluate(data.growMath, {i: amount}) - } - catch (err) { - console.log("Error parsing grow function: " + err) - growAmount = 200 - } - } else { - growAmount = 100.0 + (data.growValue * amount) - } - transformedVertex = scale(transformedVertex, growAmount) - } - - if (data.repeatEnabled && data.spinEnabled) { - let spinAmount = 0 - if (data.spinMethod === 'function') { - try { - spinAmount = data.spinValue * evaluate(data.spinMath, {i: amount}) - } - catch (err) { - console.log("Error parsing spin function: " + err) - spinAmount = 0 - } - } else { - const loopPeriod = numLoops / (parseInt(data.spinSwitchbacks) + 1) - const stage = amount/loopPeriod - const direction = (stage % 2 < 1 ? 1.0 : -1.0) - spinAmount = direction * (amount % loopPeriod) * data.spinValue - // Add in the amount it goes positive to the negatives, so they start at the same place. - if (direction < 0.0) { - spinAmount += loopPeriod * data.spinValue - } - } - transformedVertex = rotate(transformedVertex, spinAmount) - } - - if (data.repeatEnabled && data.trackEnabled) { - transformedVertex = track(transformedVertex, data, trackIndex) - } + // if (data.repeatEnabled && data.trackEnabled) { + // transformedVertex = track(transformedVertex, data, trackIndex) + // } transformedVertex.rotateDeg(-data.rotation) transformedVertex.addX({x: data.offsetX || 0}).addY({y: data.offsetY || 0}) @@ -151,11 +112,6 @@ export const transformShapes = (vertices, layer, effects) => { }) } - if (layer.transformMethod === 'smear' && layer.repeatEnabled) { - // remove last vertex; we don't want to return to our starting point when completing the shape - vertices.pop() - } - if (layer.trackEnabled && numTrackLoops > 1) { for (var i=0; i { } else { for (i=0; i { - const layer = getCurrentLayer(state) - - return { - layer: layer, - active: layer.spinEnabled, - options: (new Transform()).getOptions() - } -} - -const mapDispatchToProps = (dispatch, ownProps) => { - const { id } = ownProps - - return { - onChange: (attrs) => { - attrs.id = id - dispatch(updateLayer(attrs)) - }, - onSpinMethodChange: (value) => { - dispatch(updateLayer({ spinMethod: value, id: id})) - }, - onSpin: () => { - dispatch(toggleSpin({id: id})) - }, - } -} - -class RotationTransform extends Component { - render() { - const activeClassName = this.props.active ? 'active' : '' - const activeKey = this.props.active ? 1 : null - - return ( - - - -

Spin

- Spins the shape -
- - - - - - - - - - Scale by - - - - - - constant - function - - - - - - - - - -
-
- ) - } -} - -export default connect(mapStateToProps, mapDispatchToProps)(RotationTransform) diff --git a/src/features/transforms/ScaleTransform.js b/src/features/transforms/ScaleTransform.js deleted file mode 100644 index 70edd1b1..00000000 --- a/src/features/transforms/ScaleTransform.js +++ /dev/null @@ -1,96 +0,0 @@ -import { connect } from 'react-redux' -import React, { Component } from 'react' -import { - Accordion, - Card, - Col, - Form, - Row, - ToggleButton, - ToggleButtonGroup, -} from 'react-bootstrap' -import InputOption from '../../components/InputOption' -import { updateLayer, toggleGrow } from '../layers/layersSlice' -import { getCurrentLayer } from '../layers/selectors' -import Transform from '../../models/Transform' - -const mapStateToProps = (state, ownProps) => { - const layer = getCurrentLayer(state) - - return { - layer: layer, - active: layer.growEnabled, - options: (new Transform()).getOptions() - } -} - -const mapDispatchToProps = (dispatch, ownProps) => { - const { id } = ownProps - - return { - onChange: (attrs) => { - attrs.id = id - dispatch(updateLayer(attrs)) - }, - onGrowMethodChange: (value) => { - dispatch(updateLayer({ growMethod: value, id: id})) - }, - onGrow: () => { - dispatch(toggleGrow({id: id})) - }, - } -} - -class ScaleTransform extends Component { - render() { - const activeClassName = this.props.active ? 'active' : '' - const activeKey = this.props.active ? 1 : null - - return ( - - - -

Scale

- Grows or shrinks the shape -
- - - - - - - - - Scale by - - - - - constant - function - - - - - - - -
-
- ) - } -} -export default connect(mapStateToProps, mapDispatchToProps)(ScaleTransform) diff --git a/src/features/transforms/Transforms.js b/src/features/transforms/Transforms.js index ee88eec4..bf628ffc 100644 --- a/src/features/transforms/Transforms.js +++ b/src/features/transforms/Transforms.js @@ -15,8 +15,6 @@ import ToggleButtonOption from '../../components/ToggleButtonOption' import { updateLayer, toggleRepeat } from '../layers/layersSlice' import { getCurrentLayer } from '../layers/selectors' import Transform from '../../models/Transform' -import ScaleTransform from './ScaleTransform' -import RotationTransform from './RotationTransform' import TrackTransform from './TrackTransform' const mapStateToProps = (state, ownProps) => { @@ -39,9 +37,6 @@ const mapDispatchToProps = (dispatch, ownProps) => { }, onRepeat: () => { dispatch(toggleRepeat({id: id})) - }, - ontransformMethodChange: (value) => { - dispatch(updateLayer({ transformMethod: value, id: id})) } } } @@ -101,49 +96,6 @@ class Transforms extends Component { index={0} model={this.props.layer} /> - {this.props.layer.canTransform && - - -

Loop and transform

- Draws the shape multiple times, transforming it each time. -
- - - - - - - - - When transforming shape - - - - - - smear - keep intact - - - - - - - - - - - -
-
- } - {!this.props.layer.effect &&

Fine tuning (advanced)

diff --git a/src/models/Loop.js b/src/models/Loop.js new file mode 100644 index 00000000..1e1da60e --- /dev/null +++ b/src/models/Loop.js @@ -0,0 +1,170 @@ +import { shapeOptions } from './Shape' +import Effect from './Effect' +import { scale, rotate, circle } from '../common/geometry' +import { evaluate } from 'mathjs' + +const options = { + ...shapeOptions, + ...{ + numLoops: { + title: 'Number of loops', + min: 1 + }, + transformMethod: { + title: 'When transforming shape', + type: 'togglebutton', + choices: ['smear', 'intact'], + }, + growEnabled: { + title: 'Scale', + type: 'checkbox', + }, + growValue: { + title: 'Scale (+/-)', + }, + growMethod: { + title: 'Scale by', + type: 'togglebutton', + choices: ['constant', 'function'] + }, + growMathInput: { + title: 'Scale function (i)', + type: 'text', + isVisible: state => { return state.growMethod === 'function' }, + }, + growMath: { + isVisible: state => { return false } + }, + spinEnabled: { + title: 'Spin', + type: 'checkbox' + }, + spinValue: { + title: 'Spin (+/-)', + step: 0.1, + }, + spinMethod: { + title: 'Spin by', + type: 'togglebutton', + choices: ['constant', 'function'] + }, + spinMathInput: { + title: 'Spin function (i)', + type: 'text', + isVisible: state => { return state.spinMethod === 'function' }, + }, + spinMath: { + isVisible: state => { return false } + }, + spinSwitchbacks: { + title: 'Switchbacks', + isVisible: state => { return state.spinMethod === 'constant'}, + }, + } +} + +export default class Loop extends Effect { + constructor() { + super('Loop') + } + + getInitialState() { + return { + ...super.getInitialState(), + ...{ + // Inherited + type: 'loop', + selectGroup: 'effects', + canTransform: false, + canChangeSize: false, + canRotate: false, + canMove: false, + effect: true, + + // Loop Options + transformMethod: 'smear', + numLoops: 10, + + // Grow options + growEnabled: true, + growValue: 100, + growMethod: 'constant', + growMathInput: 'i+cos(i/2)', + growMath: 'i+cos(i/2)', + + // Spin Options + spinEnabled: false, + spinValue: 2, + spinMethod: 'constant', + spinMathInput: '10*sin(i/4)', + spinMath: '10*sin(i/4)', + spinSwitchbacks: 0, + } + } + } + + getVertices(state) { + return circle(25) + } + + applyEffect(effect, layer, vertices) { + + let outputVertices = [] + + for (var i=0; i { return state.growMethod === 'function' }, - }, - growMath: { - }, - spinEnabled: { - title: 'Spin', - isVisible: state => { return state.growMethod === 'constant'}, - }, - spinValue: { - title: 'Spin (+/-)', - step: 0.1, - }, - spinMethod: { - title: 'Spin by', - type: 'dropdown', - choices: ['constant', 'function'] - }, - spinMathInput: { - title: 'Spin function (i)', - type: 'text', - isVisible: state => { return state.spinMethod === 'function' }, - }, - spinMath: { - title: 'Spin function (i)', - }, - spinSwitchbacks: { - title: 'Switchbacks', - isVisible: state => { return state.spinMethod === 'constant'}, - }, trackEnabled: { title: 'Track' }, diff --git a/src/models/shapes.js b/src/models/shapes.js index 7d680ff4..823e8243 100644 --- a/src/models/shapes.js +++ b/src/models/shapes.js @@ -23,6 +23,7 @@ import Star from '../models/Star' import TessellationTwist from '../models/tessellation_twist/TessellationTwist' import V1Engineering from '../models/v1_engineering/V1Engineering' import Wiper from '../models/Wiper' +import Loop from './Loop' /*---------------------------------------------- Supported input shapes @@ -50,6 +51,7 @@ export const registeredShapes = { noise_wave: new NoiseWave(), file_import: new FileImport(), fisheye: new Fisheye(), + loop: new Loop(), mask: new Mask(), noise: new Noise(), warp: new Warp() From df23a52065dc54db46d78d8d497e0c00a2447085 Mon Sep 17 00:00:00 2001 From: Jeff Eberl Date: Mon, 21 Nov 2022 15:29:04 -0700 Subject: [PATCH 005/126] 'fixed' test. --- src/features/layers/layersSlice.spec.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/features/layers/layersSlice.spec.js b/src/features/layers/layersSlice.spec.js index d784a104..a4b9ca07 100644 --- a/src/features/layers/layersSlice.spec.js +++ b/src/features/layers/layersSlice.spec.js @@ -48,9 +48,8 @@ describe('layers reducer', () => { offsetY: 0.0, open: true, rotation: 0, - numLoops: 10, + numLoops: 1, reverse: false, - transformMethod: 'smear', connectionMethod: 'line', backtrackPct: 0, drawPortionPct: 100, From bf5aee8879d4a7ca2469e1c20862abceabdcfcab Mon Sep 17 00:00:00 2001 From: Jeff Eberl Date: Tue, 22 Nov 2022 09:48:04 -0700 Subject: [PATCH 006/126] Added track-transform effect. --- src/features/transforms/TrackTransform.js | 113 ---------------------- src/features/transforms/Transforms.js | 3 +- src/models/Shape.js | 8 -- src/models/TrackTransform.js | 89 +++++++++++++++++ src/models/Transform.js | 26 +---- src/models/shapes.js | 2 + 6 files changed, 93 insertions(+), 148 deletions(-) delete mode 100644 src/features/transforms/TrackTransform.js create mode 100644 src/models/TrackTransform.js diff --git a/src/features/transforms/TrackTransform.js b/src/features/transforms/TrackTransform.js deleted file mode 100644 index 7a4c0197..00000000 --- a/src/features/transforms/TrackTransform.js +++ /dev/null @@ -1,113 +0,0 @@ -import { connect } from 'react-redux' -import React, { Component } from 'react' -import { - Accordion, - Card -} from 'react-bootstrap' -import InputOption from '../../components/InputOption' -import { - toggleTrack, - toggleTrackGrow, - updateLayer -} from '../layers/layersSlice' -import { getCurrentLayer } from '../layers/selectors' -import Transform from '../../models/Transform' - -const mapStateToProps = (state, ownProps) => { - const layer = getCurrentLayer(state) - - return { - layer: layer, - active: layer.trackEnabled, - activeGrow: layer.trackGrowEnabled, - options: new Transform().getOptions() - } -} - -const mapDispatchToProps = (dispatch, ownProps) => { - const { id } = ownProps - - return { - onChange: (attrs) => { - attrs.id = id - dispatch(updateLayer(attrs)) - }, - onTrack: () => { - dispatch(toggleTrack({id: id})) - }, - onTrackGrow: () => { - dispatch(toggleTrackGrow({id: id})) - }, - } -} - -class TrackTransform extends Component { - render() { - const activeClassName = this.props.active ? 'active' : '' - const activeKey = this.props.active ? 1 : null - const activeGrowClassName = this.props.activeGrow ? 'active' : '' - const activeGrowKey = this.props.activeGrow ? 1 : null - - return ( - - - -

Track

- Moves the shape along a track (shown in green) -
- - - - - - - - - - - - -

Scale track

- Grows or shrinks the track -
- - - - - - -
-
-
-
-
-
- ) - } -} -export default connect(mapStateToProps, mapDispatchToProps)(TrackTransform) diff --git a/src/features/transforms/Transforms.js b/src/features/transforms/Transforms.js index bf628ffc..375ff706 100644 --- a/src/features/transforms/Transforms.js +++ b/src/features/transforms/Transforms.js @@ -15,14 +15,13 @@ import ToggleButtonOption from '../../components/ToggleButtonOption' import { updateLayer, toggleRepeat } from '../layers/layersSlice' import { getCurrentLayer } from '../layers/selectors' import Transform from '../../models/Transform' -import TrackTransform from './TrackTransform' const mapStateToProps = (state, ownProps) => { const layer = getCurrentLayer(state) return { layer: layer, - active: layer.repeatEnabled, + active: true, options: (new Transform()).getOptions() } } diff --git a/src/models/Shape.js b/src/models/Shape.js index c1e9e910..256dab53 100644 --- a/src/models/Shape.js +++ b/src/models/Shape.js @@ -13,7 +13,6 @@ export default class Shape { getInitialState() { return { - repeatEnabled: true, canTransform: true, selectGroup: 'Shapes', shouldCache: true, @@ -30,13 +29,6 @@ export default class Shape { offsetY: 0.0, open: true, rotation: 0, - numLoops: 1, - trackEnabled: false, - trackGrowEnabled: false, - trackValue: 10, - trackLength: 0.2, - trackNumLoops: 1, - trackGrow: 50.0, connectionMethod: 'line', drawPortionPct: 100, backtrackPct: 0, diff --git a/src/models/TrackTransform.js b/src/models/TrackTransform.js new file mode 100644 index 00000000..b20a5c5f --- /dev/null +++ b/src/models/TrackTransform.js @@ -0,0 +1,89 @@ +import { shapeOptions } from './Shape' +import Effect from './Effect' +import { offset, rotate, circle } from '../common/geometry' +import Victor from 'victor' + +const options = { + ...shapeOptions, + ...{ + trackRadius: { + title: 'Track radius', + }, + trackRotations: { + title: 'Track Rotations' + }, + trackSpiralEnabled: { + title: 'Spiral track', + type: 'checkbox', + }, + trackSpiralRadius: { + title: 'Spiral Radius', + isVisible: state => { return state.trackSpiralEnabled }, + }, + } +} + +export default class Track extends Effect { + constructor() { + super('Track') + } + + getInitialState() { + return { + ...super.getInitialState(), + ...{ + // Inherited + type: 'track', + selectGroup: 'effects', + canTransform: false, + canChangeSize: false, + canRotate: false, + canMove: false, + effect: true, + + // Track Options + trackRadius: 10, + trackRotations: 1, + trackSpiralEnabled: false, + trackSpiralRadius: 50.0, + } + } + } + + getVertices(state) { + // TODO Make this more reasonable + return circle(25) + } + + applyEffect(effect, layer, vertices) { + + let outputVertices = [] + + for (var j=0; j { return !state.effect } - }, - numLoops: { - title: 'Number of loops', - min: 1 - }, - trackEnabled: { - title: 'Track' - }, - trackGrowEnabled: { - title: 'Scale track' - }, - trackValue: { - title: 'Track size', - }, - trackNumLoops: { - title: 'Number of loops at each track position', - min: 1 - }, - trackLength: { - title: 'Track length', - step: 0.05 - }, - trackGrow: { - title: 'Scale (+/-)', - }, + } } // used as a way to keep a shape's transform settings separate. Actual state diff --git a/src/models/shapes.js b/src/models/shapes.js index 823e8243..5c101bdc 100644 --- a/src/models/shapes.js +++ b/src/models/shapes.js @@ -24,6 +24,7 @@ import TessellationTwist from '../models/tessellation_twist/TessellationTwist' import V1Engineering from '../models/v1_engineering/V1Engineering' import Wiper from '../models/Wiper' import Loop from './Loop' +import Track from './TrackTransform' /*---------------------------------------------- Supported input shapes @@ -52,6 +53,7 @@ export const registeredShapes = { file_import: new FileImport(), fisheye: new Fisheye(), loop: new Loop(), + track: new Track(), mask: new Mask(), noise: new Noise(), warp: new Warp() From 9c89956b410bc5faedf7e88807504451791043ff Mon Sep 17 00:00:00 2001 From: Jeff Eberl Date: Tue, 22 Nov 2022 09:50:18 -0700 Subject: [PATCH 007/126] Fix test. --- src/features/layers/layersSlice.spec.js | 8 -------- src/models/Loop.js | 3 ++- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/src/features/layers/layersSlice.spec.js b/src/features/layers/layersSlice.spec.js index a4b9ca07..e6ea9260 100644 --- a/src/features/layers/layersSlice.spec.js +++ b/src/features/layers/layersSlice.spec.js @@ -33,7 +33,6 @@ describe('layers reducer', () => { circleLobes: 1, circleDirection: 'clockwise', type: 'circle', - repeatEnabled: true, canTransform: true, selectGroup: 'Shapes', shouldCache: true, @@ -48,19 +47,12 @@ describe('layers reducer', () => { offsetY: 0.0, open: true, rotation: 0, - numLoops: 1, reverse: false, connectionMethod: 'line', backtrackPct: 0, drawPortionPct: 100, effect: false, rotateStartingPct: 0, - trackEnabled: false, - trackGrowEnabled: false, - trackValue: 10, - trackLength: 0.2, - trackNumLoops: 1, - trackGrow: 50.0, usesMachine: false, usesFonts: false, dragging: false, diff --git a/src/models/Loop.js b/src/models/Loop.js index 1e1da60e..6787cafd 100644 --- a/src/models/Loop.js +++ b/src/models/Loop.js @@ -80,7 +80,7 @@ export default class Loop extends Effect { canRotate: false, canMove: false, effect: true, - + // Loop Options transformMethod: 'smear', numLoops: 10, @@ -104,6 +104,7 @@ export default class Loop extends Effect { } getVertices(state) { + // TODO Make this more reasonable return circle(25) } From 340c97cef7913ed3fab5bc83bc98fbe5e07db5a1 Mon Sep 17 00:00:00 2001 From: Jeff Eberl Date: Tue, 22 Nov 2022 09:54:09 -0700 Subject: [PATCH 008/126] Fix the smearing in a loop for closed sparse shapes like polygons. --- src/models/Loop.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/models/Loop.js b/src/models/Loop.js index 6787cafd..44878588 100644 --- a/src/models/Loop.js +++ b/src/models/Loop.js @@ -110,6 +110,11 @@ export default class Loop extends Effect { applyEffect(effect, layer, vertices) { + // Remove one point if we are smearing + if (effect.transformMethod === 'smear') { + vertices.pop() + } + let outputVertices = [] for (var i=0; i Date: Tue, 22 Nov 2022 10:11:04 -0700 Subject: [PATCH 009/126] Move Fine Tuning to new effect. --- src/features/exporter/CommentExporter.js | 11 +-- src/features/layers/layersSlice.spec.js | 3 - src/features/machine/computer.js | 19 +---- src/features/transforms/Transforms.js | 29 -------- src/models/FineTuning.js | 88 ++++++++++++++++++++++++ src/models/Shape.js | 3 - src/models/TrackTransform.js | 1 - src/models/Transform.js | 18 ----- src/models/shapes.js | 4 +- 9 files changed, 93 insertions(+), 83 deletions(-) create mode 100644 src/models/FineTuning.js diff --git a/src/features/exporter/CommentExporter.js b/src/features/exporter/CommentExporter.js index 31923ebb..e28f9240 100644 --- a/src/features/exporter/CommentExporter.js +++ b/src/features/exporter/CommentExporter.js @@ -44,19 +44,10 @@ export default class CommentExporter extends Exporter { this.keyValueLine('Visible', layer.visible) this.optionLines(transform, layer, ['startingWidth', 'startingHeight', 'offsetX', 'offsetY', 'rotation', 'reverse']) - // this.optionLine(transform, layer, 'trackEnabled', layer.repeatEnabled) - // this.indent() - // this.optionLines(transform, layer, ['trackValue', 'trackLength', 'trackNumLoops'], layer.repeatEnabled && layer.trackEnabled) - // this.optionLine(transform, layer, 'trackGrowEnabled', layer.repeatEnabled && layer.trackEnabled) - // this.indent() - // this.optionLine(transform, layer, 'trackGrow', layer.repeatEnabled && layer.trackGrowEnabled) - // this.dedent() - // this.dedent() - if (!layer.effect) { this.line('Fine tuning:') this.indent() - this.optionLines(transform, layer, ['connectionMethod', 'drawPortionPct', 'backtrackPct', 'rotateStartingPct']) + this.optionLines(transform, layer, ['connectionMethod']) this.dedent() } this.dedent() diff --git a/src/features/layers/layersSlice.spec.js b/src/features/layers/layersSlice.spec.js index e6ea9260..41cf78c8 100644 --- a/src/features/layers/layersSlice.spec.js +++ b/src/features/layers/layersSlice.spec.js @@ -49,10 +49,7 @@ describe('layers reducer', () => { rotation: 0, reverse: false, connectionMethod: 'line', - backtrackPct: 0, - drawPortionPct: 100, effect: false, - rotateStartingPct: 0, usesMachine: false, usesFonts: false, dragging: false, diff --git a/src/features/machine/computer.js b/src/features/machine/computer.js index 8e149bbc..3e3b50b4 100644 --- a/src/features/machine/computer.js +++ b/src/features/machine/computer.js @@ -1,8 +1,6 @@ import ReactGA from 'react-ga' import throttle from 'lodash/throttle' -import { evaluate } from 'mathjs' -import { distance, scale, rotate } from '../../common/geometry' -import { arrayRotate } from '../../common/util' +import { distance, scale } from '../../common/geometry' import PolarMachine from './PolarMachine' import RectMachine from './RectMachine' import { getShape } from '../../models/shapes' @@ -127,25 +125,10 @@ export const transformShapes = (vertices, layer, effects) => { } } - if (layer.rotateStartingPct === undefined || layer.rotateStartingPct !== 0) { - const start = Math.round(outputVertices.length * layer.rotateStartingPct / 100.0) - outputVertices = arrayRotate(outputVertices, start) - } - - if (layer.drawPortionPct !== undefined) { - const drawPortionPct = Math.round((parseInt(layer.drawPortionPct) || 100)/100.0 * outputVertices.length) - outputVertices = outputVertices.slice(0, drawPortionPct) - } - if (layer.reverse) { outputVertices = outputVertices.reverse() } - if (layer.backtrackPct) { - const backtrack = Math.round(vertices.length * layer.backtrackPct / 100.0) - outputVertices = outputVertices.concat(outputVertices.slice(outputVertices.length - backtrack).reverse()) - } - if (effects && effects.length > 0) { effects.forEach(effect => { outputVertices = getShape(effect).applyEffect(effect, layer, outputVertices) diff --git a/src/features/transforms/Transforms.js b/src/features/transforms/Transforms.js index 375ff706..943e57c7 100644 --- a/src/features/transforms/Transforms.js +++ b/src/features/transforms/Transforms.js @@ -106,35 +106,6 @@ class Transforms extends Component { index={0} model={this.props.layer} /> - - - - -
} diff --git a/src/models/FineTuning.js b/src/models/FineTuning.js new file mode 100644 index 00000000..152b358f --- /dev/null +++ b/src/models/FineTuning.js @@ -0,0 +1,88 @@ +import { shapeOptions } from './Shape' +import { arrayRotate } from '../common/util' +import Effect from './Effect' + +const options = { + ...shapeOptions, + ...{ + backtrackPct: { + title: 'Backtrack at end (%)', + min: 0, + max: 100, + step: 2 + }, + drawPortionPct: { + title: 'Draw portion of path (%)', + min: 0, + max: 100, + step: 2 + }, + rotateStartingPct: { + title: 'Rotate starting point (%)', + min: -100, + max: 100, + step: 2 + }, + } +} + +export default class FineTuning extends Effect { + constructor() { + super('Fine Tuning') + } + + getInitialState() { + return { + ...super.getInitialState(), + ...{ + // Inherited + type: 'fineTuning', + selectGroup: 'effects', + canTransform: false, + canChangeSize: false, + canRotate: false, + canMove: false, + effect: true, + + // Fine Tuning Options + drawPortionPct: 100, + backtrackPct: 0, + rotateStartingPct: 0, + } + } + } + + getVertices(state) { + // TODO Make this more reasonable + return circle(25) + } + + applyEffect(effect, layer, vertices) { + + // Remove one point if we are smearing + if (effect.transformMethod === 'smear') { + vertices.pop() + } + + let outputVertices = vertices + + if (effect.rotateStartingPct === undefined || effect.rotateStartingPct !== 0) { + const start = Math.round(outputVertices.length * effect.rotateStartingPct / 100.0) + outputVertices = arrayRotate(outputVertices, start) + } + + if (effect.drawPortionPct !== undefined) { + const drawPortionPct = Math.round((parseInt(effect.drawPortionPct) || 100)/100.0 * outputVertices.length) + outputVertices = outputVertices.slice(0, drawPortionPct) + } + + const backtrack = Math.round(vertices.length * effect.backtrackPct / 100.0) + outputVertices = outputVertices.concat(outputVertices.slice(outputVertices.length - backtrack).reverse()) + + return outputVertices + } + + getOptions() { + return options + } +} diff --git a/src/models/Shape.js b/src/models/Shape.js index 256dab53..b193b8c3 100644 --- a/src/models/Shape.js +++ b/src/models/Shape.js @@ -30,9 +30,6 @@ export default class Shape { open: true, rotation: 0, connectionMethod: 'line', - drawPortionPct: 100, - backtrackPct: 0, - rotateStartingPct: 0, reverse: false, dragging: false, visible: true, diff --git a/src/models/TrackTransform.js b/src/models/TrackTransform.js index b20a5c5f..5c9082f5 100644 --- a/src/models/TrackTransform.js +++ b/src/models/TrackTransform.js @@ -1,7 +1,6 @@ import { shapeOptions } from './Shape' import Effect from './Effect' import { offset, rotate, circle } from '../common/geometry' -import Victor from 'victor' const options = { ...shapeOptions, diff --git a/src/models/Transform.js b/src/models/Transform.js index 7e64e17e..a4b58950 100644 --- a/src/models/Transform.js +++ b/src/models/Transform.js @@ -31,24 +31,6 @@ const transformOptions = { title: 'Connect to next layer', choices: ['line', 'along perimeter'] }, - backtrackPct: { - title: 'Backtrack at end (%)', - min: 0, - max: 100, - step: 2 - }, - drawPortionPct: { - title: 'Draw portion of path (%)', - min: 0, - max: 100, - step: 2 - }, - rotateStartingPct: { - title: 'Rotate starting point (%)', - min: -100, - max: 100, - step: 2 - }, reverse: { title: 'Reverse path', type: 'checkbox', diff --git a/src/models/shapes.js b/src/models/shapes.js index 5c101bdc..ade71a60 100644 --- a/src/models/shapes.js +++ b/src/models/shapes.js @@ -25,6 +25,7 @@ import V1Engineering from '../models/v1_engineering/V1Engineering' import Wiper from '../models/Wiper' import Loop from './Loop' import Track from './TrackTransform' +import FineTuning from './FineTuning' /*---------------------------------------------- Supported input shapes @@ -56,7 +57,8 @@ export const registeredShapes = { track: new Track(), mask: new Mask(), noise: new Noise(), - warp: new Warp() + warp: new Warp(), + fineTuning: new FineTuning() } export const getShape = (layer) => { From 0ba7ebaa437b00f922be80130e133b77e53a3df7 Mon Sep 17 00:00:00 2001 From: Jeff Eberl Date: Tue, 22 Nov 2022 10:16:16 -0700 Subject: [PATCH 010/126] Missing Include --- src/models/FineTuning.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/models/FineTuning.js b/src/models/FineTuning.js index 152b358f..61afb51a 100644 --- a/src/models/FineTuning.js +++ b/src/models/FineTuning.js @@ -1,5 +1,6 @@ import { shapeOptions } from './Shape' import { arrayRotate } from '../common/util' +import { circle } from '../common/geometry' import Effect from './Effect' const options = { From 2f687ff8095b292bf8b616aa12278bab6ed7bbd1 Mon Sep 17 00:00:00 2001 From: Bob Carmichael Date: Sun, 18 Dec 2022 17:31:13 -0500 Subject: [PATCH 011/126] redefine base model classes; restructure directories --- package-lock.json | 16 +- src/config/models.js | 109 ++++++++++++ src/features/app/store.js | 6 +- src/features/exporter/CommentExporter.js | 22 +-- src/features/exporter/Downloader.js | 2 +- src/features/exporter/exporterSlice.js | 2 +- .../exporter/options.js} | 0 src/features/layers/ImportLayer.js | 4 +- src/features/layers/Layer.js | 8 +- src/features/layers/NewLayer.js | 8 +- src/features/layers/Playlist.js | 6 +- src/features/layers/layersSlice.js | 33 +--- src/features/layers/layersSlice.spec.js | 68 -------- src/features/machine/PolarSettings.js | 4 +- src/features/machine/RectSettings.js | 4 +- src/features/machine/computer.js | 164 ++++++++---------- .../machine/options.js} | 8 +- src/features/machine/selectors.js | 23 +-- src/features/preview/PreviewLayer.js | 2 +- src/features/transforms/Transforms.js | 116 ------------- src/models/Effect.js | 6 +- src/models/Group.js | 23 +++ src/models/Layer.js | 28 +++ src/models/Model.js | 92 ++++++++++ src/models/Shape.js | 44 +---- src/models/Transform.js | 47 ----- src/models/{ => effects}/FineTuning.js | 11 +- src/models/{ => effects}/Loop.js | 35 ++-- src/models/{ => effects}/Mask.js | 16 +- src/models/{ => effects}/Noise.js | 12 +- .../{TrackTransform.js => effects/Track.js} | 13 +- src/models/{ => effects}/Warp.js | 8 +- src/models/shapes.js | 107 ------------ src/models/{ => shapes}/Circle.js | 2 +- src/models/{ => shapes}/Epicycloid.js | 4 +- src/models/{ => shapes}/FancyText.js | 11 +- src/models/{ => shapes}/FileImport.js | 4 +- src/models/{ => shapes}/Fisheye.js | 6 +- src/models/{ => shapes}/Freeform.js | 3 +- src/models/{ => shapes}/Heart.js | 2 +- src/models/{ => shapes}/Hypocycloid.js | 4 +- src/models/{ => shapes}/NoiseWave.js | 10 +- src/models/{ => shapes}/Point.js | 4 +- src/models/{ => shapes}/Polygon.js | 2 +- src/models/{ => shapes}/Reuleaux.js | 2 +- src/models/{ => shapes}/Rose.js | 2 +- src/models/{ => shapes}/Star.js | 2 +- src/models/{ => shapes}/Wiper.js | 6 +- .../{ => shapes}/circle_packer/Circle.js | 0 .../circle_packer/CirclePacker.js | 10 +- .../fractal_spirograph/FractalSpirograph.js | 3 +- .../{ => shapes}/fractal_spirograph/Orbit.js | 0 src/models/{ => shapes}/input_text/Fonts.js | 0 .../{ => shapes}/input_text/InputText.js | 5 +- .../input_text/convert_letters.py | 0 .../{ => shapes}/input_text/raysol_cursive.js | 0 .../input_text/raysol_cursive.txt | 0 .../input_text/raysol_sanserif.js | 0 .../input_text/raysol_sanserif.txt | 0 src/models/{ => shapes}/lsystem/LSystem.js | 7 +- src/models/{ => shapes}/lsystem/subtypes.js | 0 .../{ => shapes}/space_filler/SpaceFiller.js | 8 +- .../{ => shapes}/space_filler/subtypes.js | 0 .../tessellation_twist/TessellationTwist.js | 9 +- .../v1_engineering/V1Engineering.js | 2 +- .../v1_engineering/Vicious1Vertices.js | 0 vite.config.js | 4 +- 67 files changed, 498 insertions(+), 661 deletions(-) create mode 100644 src/config/models.js rename src/{models/Exporter.js => features/exporter/options.js} (100%) rename src/{models/Machine.js => features/machine/options.js} (80%) delete mode 100644 src/features/transforms/Transforms.js create mode 100644 src/models/Group.js create mode 100644 src/models/Layer.js create mode 100644 src/models/Model.js delete mode 100644 src/models/Transform.js rename src/models/{ => effects}/FineTuning.js (90%) rename src/models/{ => effects}/Loop.js (82%) rename src/models/{ => effects}/Mask.js (86%) rename src/models/{ => effects}/Noise.js (92%) rename src/models/{TrackTransform.js => effects/Track.js} (87%) rename src/models/{ => effects}/Warp.js (97%) delete mode 100644 src/models/shapes.js rename src/models/{ => shapes}/Circle.js (96%) rename src/models/{ => shapes}/Epicycloid.js (93%) rename src/models/{ => shapes}/FancyText.js (96%) rename src/models/{ => shapes}/FileImport.js (94%) rename src/models/{ => shapes}/Fisheye.js (90%) rename src/models/{ => shapes}/Freeform.js (90%) rename src/models/{ => shapes}/Heart.js (96%) rename src/models/{ => shapes}/Hypocycloid.js (93%) rename src/models/{ => shapes}/NoiseWave.js (94%) rename src/models/{ => shapes}/Point.js (83%) rename src/models/{ => shapes}/Polygon.js (98%) rename src/models/{ => shapes}/Reuleaux.js (96%) rename src/models/{ => shapes}/Rose.js (95%) rename src/models/{ => shapes}/Star.js (95%) rename src/models/{ => shapes}/Wiper.js (97%) rename src/models/{ => shapes}/circle_packer/Circle.js (100%) rename src/models/{ => shapes}/circle_packer/CirclePacker.js (97%) rename src/models/{ => shapes}/fractal_spirograph/FractalSpirograph.js (96%) rename src/models/{ => shapes}/fractal_spirograph/Orbit.js (100%) rename src/models/{ => shapes}/input_text/Fonts.js (100%) rename src/models/{ => shapes}/input_text/InputText.js (98%) rename src/models/{ => shapes}/input_text/convert_letters.py (100%) rename src/models/{ => shapes}/input_text/raysol_cursive.js (100%) rename src/models/{ => shapes}/input_text/raysol_cursive.txt (100%) rename src/models/{ => shapes}/input_text/raysol_sanserif.js (100%) rename src/models/{ => shapes}/input_text/raysol_sanserif.txt (100%) rename src/models/{ => shapes}/lsystem/LSystem.js (90%) rename src/models/{ => shapes}/lsystem/subtypes.js (100%) rename src/models/{ => shapes}/space_filler/SpaceFiller.js (91%) rename src/models/{ => shapes}/space_filler/subtypes.js (100%) rename src/models/{ => shapes}/tessellation_twist/TessellationTwist.js (96%) rename src/models/{ => shapes}/v1_engineering/V1Engineering.js (91%) rename src/models/{ => shapes}/v1_engineering/Vicious1Vertices.js (100%) diff --git a/package-lock.json b/package-lock.json index c754ce8d..3697ad33 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11167,9 +11167,9 @@ "integrity": "sha1-3jzHexVYmxsMeyLD2tKXA0qwAro=" }, "node_modules/vite": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/vite/-/vite-3.2.2.tgz", - "integrity": "sha512-pLrhatFFOWO9kS19bQ658CnRYzv0WLbsPih6R+iFeEEhDOuYgYCX2rztUViMz/uy/V8cLCJvLFeiOK7RJEzHcw==", + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-3.2.5.tgz", + "integrity": "sha512-4mVEpXpSOgrssFZAOmGIr85wPHKvaDAcXqxVxVRZhljkJOMZi1ibLibzjLHzJvcok8BMguLc7g1W6W/GqZbLdQ==", "dev": true, "dependencies": { "esbuild": "^0.15.9", @@ -11187,6 +11187,7 @@ "fsevents": "~2.3.2" }, "peerDependencies": { + "@types/node": ">= 14", "less": "*", "sass": "*", "stylus": "*", @@ -11194,6 +11195,9 @@ "terser": "^5.4.0" }, "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, "less": { "optional": true }, @@ -19628,9 +19632,9 @@ "integrity": "sha1-3jzHexVYmxsMeyLD2tKXA0qwAro=" }, "vite": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/vite/-/vite-3.2.2.tgz", - "integrity": "sha512-pLrhatFFOWO9kS19bQ658CnRYzv0WLbsPih6R+iFeEEhDOuYgYCX2rztUViMz/uy/V8cLCJvLFeiOK7RJEzHcw==", + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-3.2.5.tgz", + "integrity": "sha512-4mVEpXpSOgrssFZAOmGIr85wPHKvaDAcXqxVxVRZhljkJOMZi1ibLibzjLHzJvcok8BMguLc7g1W6W/GqZbLdQ==", "dev": true, "requires": { "esbuild": "^0.15.9", diff --git a/src/config/models.js b/src/config/models.js new file mode 100644 index 00000000..73168aa7 --- /dev/null +++ b/src/config/models.js @@ -0,0 +1,109 @@ +import Circle from '@/models/shapes/Circle' +import CirclePacker from '@/models/shapes/circle_packer/CirclePacker' +import Epicycloid from '@/models/shapes/Epicycloid' +import FancyText from '@/models/shapes/FancyText' +import FileImport from '@/models/shapes/FileImport' +import Fisheye from '@/models/shapes/Fisheye' +import FractalSpirograph from '@/models/shapes/fractal_spirograph/FractalSpirograph' +// import Freeform from '../models/shapes/Freeform' +import Heart from '@/models/shapes/Heart' +import Hypocycloid from '@/models/shapes/Hypocycloid' +import InputText from '@/models/shapes/input_text/InputText' +import LSystem from '@/models/shapes/lsystem/LSystem' +import NoiseWave from '@/models/shapes/NoiseWave' +import Polygon from '@/models/shapes/Polygon' +import Point from '@/models/shapes/Point' +import Reuleaux from '@/models/shapes/Reuleaux' +import Rose from '@/models/shapes/Rose' +import SpaceFiller from '@/models/shapes/space_filler/SpaceFiller' +import Star from '@/models/shapes/Star' +import TessellationTwist from '@/models/shapes/tessellation_twist/TessellationTwist' +import V1Engineering from '@/models/shapes/v1_engineering/V1Engineering' + +import FineTuning from '../models/effects/FineTuning' +import Loop from '@/models/effects/Loop' +import Mask from '@/models/effects/Mask' +import Noise from '@/models/effects/Noise' +import Track from '@/models/effects/Track' +import Warp from '@/models/effects/Warp' +import Wiper from '@/models/shapes/Wiper' + + +/*---------------------------------------------- +Supported input shapes +-----------------------------------------------*/ +export const registeredModels = { + polygon: new Polygon(), + star: new Star(), + circle: new Circle(), + heart: new Heart(), + reuleaux: new Reuleaux(), + epicycloid: new Epicycloid(), + hypocycloid: new Hypocycloid(), + rose: new Rose(), + inputText: new InputText(), + fancy_text: new FancyText(), + v1Engineering: new V1Engineering(), + lsystem: new LSystem(), + fractal_spirograph: new FractalSpirograph(), + tessellation_twist: new TessellationTwist(), + point: new Point(), + // freeform: new Freeform(), + circle_packer: new CirclePacker(), + wiper: new Wiper(), + space_filler: new SpaceFiller(), + noise_wave: new NoiseWave(), + file_import: new FileImport(), + fisheye: new Fisheye(), + loop: new Loop(), + track: new Track(), + mask: new Mask(), + noise: new Noise(), + warp: new Warp(), + fineTuning: new FineTuning() +} + +export const getModel = (layer) => { + return registeredModels[layer.type] +} + +export const getModelDefaults = () => { + return Object.keys(registeredModels).map(id => { + const state = registeredModels[id].getInitialState() + state.name = registeredModels[id].name + state.id = id + return state + }) +} + +export const getModelSelectOptions = () => { + const groupOptions = [] + const shapes = getModelDefaults() + + for (const shape of shapes) { + const optionLabel = { value: shape.id, label: shape.name } + var found = false + + for (const group of groupOptions) { + if (group.label === shape.selectGroup) { + found = true + group.options.push(optionLabel) + } + } + if (!found) { + if (shape.selectGroup === 'import') { + // users can't manually select this group + continue + } else if (shape.selectGroup === 'effects') { + // effects are added separately + // TODO: when effects can be added separately, uncomment the next line + // continue + } + + const newOptions = [ optionLabel ] + groupOptions.push( { label: shape.selectGroup, options: newOptions } ) + } + } + + return groupOptions +} diff --git a/src/features/app/store.js b/src/features/app/store.js index da9513cf..61cf2f10 100644 --- a/src/features/app/store.js +++ b/src/features/app/store.js @@ -7,7 +7,7 @@ import machineReducer from '../machine/machineSlice' import exporterReducer from '../exporter/exporterSlice' import previewReducer from '../preview/previewSlice' import fontsReducer from '../fonts/fontsSlice' -import { registeredShapes } from '../../models/shapes' +import { registeredModels } from '../../config/models' import { loadState, saveState } from '../../common/localStorage' import layersReducer, { setCurrentLayer, addLayer, addEffect, updateLayer } from '../layers/layersSlice' @@ -69,8 +69,8 @@ const loadPersistedLayers = (layers) => { const loadDefaultLayer = () => { const storedShape = localStorage.getItem('currentShape') - const currentShape = storedShape && registeredShapes[storedShape] ? storedShape : 'polygon' - const layer = registeredShapes[currentShape].getInitialState() + const currentShape = storedShape && registeredModels[storedShape] ? storedShape : 'polygon' + const layer = registeredModels[currentShape].getInitialState() store.dispatch(addLayer(layer)) diff --git a/src/features/exporter/CommentExporter.js b/src/features/exporter/CommentExporter.js index e28f9240..6f10f936 100644 --- a/src/features/exporter/CommentExporter.js +++ b/src/features/exporter/CommentExporter.js @@ -1,6 +1,5 @@ -import { getShape } from '../../models/shapes' -import Machine from '../../models/Machine' -import Transform from '../../models/Transform' +import { getModel } from '../../config/models' +//import Machine from '../../models/Machine' import Exporter from './Exporter' export default class CommentExporter extends Exporter { @@ -13,8 +12,8 @@ export default class CommentExporter extends Exporter { export() { const state = this.props - const machine = new Machine() - const transform = new Transform() +// TODO: fix +// const machine = new Machine() let instance = state.machine this.line('Created by Sandify') @@ -24,8 +23,9 @@ export default class CommentExporter extends Exporter { this.keyValueLine('Machine type', state.machine.rectangular ? 'Rectangular' : 'Polar') this.indent() - this.optionLines(machine, instance, ['minX', 'maxX', 'minY', 'maxY'], state.machine.rectangular) - this.optionLines(machine, instance, ['maxRadius', 'polarStartPoint', 'polarEndPoint'], !state.machine.rectangular) + // TODO: fix +// this.optionLines(machine, instance, ['minX', 'maxX', 'minY', 'maxY'], state.machine.rectangular) +// this.optionLines(machine, instance, ['maxRadius', 'polarStartPoint', 'polarEndPoint'], !state.machine.rectangular) this.dedent() this.keyValueLine('Content type', state.app.input) @@ -34,7 +34,7 @@ export default class CommentExporter extends Exporter { switch (state.app.input) { case 'shape': // shapes layers.forEach(layer => { - const shape = getShape(layer) + const shape = getModel(layer) const options = shape.getOptions() this.line('Layer:') @@ -42,12 +42,14 @@ export default class CommentExporter extends Exporter { this.keyValueLine('Shape', shape.name) this.optionLines(shape, layer, Object.keys(options)) this.keyValueLine('Visible', layer.visible) - this.optionLines(transform, layer, ['startingWidth', 'startingHeight', 'offsetX', 'offsetY', 'rotation', 'reverse']) +// TODO: fix +// this.optionLines(transform, layer, ['startingWidth', 'startingHeight', 'offsetX', 'offsetY', 'rotation', 'reverse']) if (!layer.effect) { this.line('Fine tuning:') this.indent() - this.optionLines(transform, layer, ['connectionMethod']) +// TODO: fix +// this.optionLines(transform, layer, ['connectionMethod']) this.dedent() } this.dedent() diff --git a/src/features/exporter/Downloader.js b/src/features/exporter/Downloader.js index fc924caa..c8b51455 100644 --- a/src/features/exporter/Downloader.js +++ b/src/features/exporter/Downloader.js @@ -13,7 +13,7 @@ import GCodeExporter from './GCodeExporter' import ScaraGCodeExporter from './ScaraGCodeExporter' import SvgExporter from './SvgExporter' import ThetaRhoExporter from './ThetaRhoExporter' -import { Exporter, GCODE, THETARHO, SVG, SCARA } from '../../models/Exporter' +import { Exporter, GCODE, THETARHO, SVG, SCARA } from './options' const exporters = { [GCODE]: GCodeExporter, diff --git a/src/features/exporter/exporterSlice.js b/src/features/exporter/exporterSlice.js index 04e2958b..35bf671e 100644 --- a/src/features/exporter/exporterSlice.js +++ b/src/features/exporter/exporterSlice.js @@ -1,5 +1,5 @@ import { createSlice } from '@reduxjs/toolkit' -import { GCODE, THETARHO, SVG } from '../../models/Exporter' +import { GCODE, THETARHO, SVG } from './options' // Determine default file type; this is a little fussy because we want to ensure // that if the user has a rectangular table, but somehow wants to export theta diff --git a/src/models/Exporter.js b/src/features/exporter/options.js similarity index 100% rename from src/models/Exporter.js rename to src/features/exporter/options.js diff --git a/src/features/layers/ImportLayer.js b/src/features/layers/ImportLayer.js index e3b2aa9e..8002ffbd 100644 --- a/src/features/layers/ImportLayer.js +++ b/src/features/layers/ImportLayer.js @@ -5,7 +5,7 @@ import { connect } from 'react-redux' import ThetaRhoImporter from '../importer/ThetaRhoImporter' import GCodeImporter from '../importer/GCodeImporter' import { addLayer } from '../layers/layersSlice' -import { registeredShapes } from '../../models/shapes' +import { registeredModels } from '../../config/models' import ReactGA from 'react-ga' const mapStateToProps = (state, ownProps) => { @@ -21,7 +21,7 @@ const mapDispatchToProps = (dispatch, ownProps) => { }, onLayerImport: (importProps) => { const attrs = { - ...registeredShapes["file_import"].getInitialState(importProps), + ...registeredModels["file_import"].getInitialState(importProps), name: importProps.fileName } dispatch(addLayer(attrs)) diff --git a/src/features/layers/Layer.js b/src/features/layers/Layer.js index 6c73f301..49b14839 100644 --- a/src/features/layers/Layer.js +++ b/src/features/layers/Layer.js @@ -7,21 +7,20 @@ import InputOption from '../../components/InputOption' import DropdownOption from '../../components/DropdownOption' import CheckboxOption from '../../components/CheckboxOption' import ToggleButtonOption from '../../components/ToggleButtonOption' -import Transforms from '../transforms/Transforms' import { updateLayer, setShapeType, restoreDefaults } from '../layers/layersSlice' import { getCurrentLayer } from './selectors' -import { getShape, getShapeSelectOptions } from '../../models/shapes' +import { getModel, getModelSelectOptions } from '../../config/models' import './Layer.scss' const mapStateToProps = (state, ownProps) => { const layer = getCurrentLayer(state) - const shape = getShape(layer) + const shape = getModel(layer) return { layer: layer, shape: shape, options: shape.getOptions(), - selectOptions: getShapeSelectOptions(false), + selectOptions: getModelSelectOptions(false), showShapeSelectRender: layer.selectGroup !== 'import' && !layer.effect, link: shape.link, linkText: shape.linkText @@ -99,7 +98,6 @@ class Layer extends Component {
{ optionsListRender } -
) diff --git a/src/features/layers/NewLayer.js b/src/features/layers/NewLayer.js index df914b49..c85f229e 100644 --- a/src/features/layers/NewLayer.js +++ b/src/features/layers/NewLayer.js @@ -5,7 +5,7 @@ import { connect } from 'react-redux' import { getLayers } from '../store/selectors' import { getCurrentLayer } from '../layers/selectors' -import { registeredShapes, getShapeSelectOptions, getShape } from '../../models/shapes' +import { registeredModels, getModelSelectOptions, getModel } from '../../config/models' import { addLayer, updateLayers, setNewLayerType } from '../layers/layersSlice' const customStyles = { @@ -24,7 +24,7 @@ const mapStateToProps = (state, ownProps) => { newLayerType: layers.newLayerType, newLayerName: layers.newLayerName, currentLayer: layer, - selectOptions: getShapeSelectOptions(false), + selectOptions: getModelSelectOptions(false), showModal: ownProps.showModal } } @@ -38,7 +38,7 @@ const mapDispatchToProps = (dispatch, ownProps) => { dispatch(updateLayers({ newLayerName: event.target.value, newLayerNameOverride: true })) }, onLayerAdded: (type) => { - const attrs = registeredShapes[type].getInitialState() + const attrs = registeredModels[type].getInitialState() dispatch(addLayer(attrs)) }, toggleModal: () => { @@ -53,7 +53,7 @@ class NewLayer extends Component { newLayerType, currentLayer, toggleModal, showModal, selectOptions, newLayerName, onChangeNewType, onChangeNewName, onLayerAdded } = this.props - const selectedShape = getShape({type: newLayerType}) + const selectedShape = getModel({type: newLayerType}) const selectedOption = { value: selectedShape.id, label: selectedShape.name } return { const layer = getCurrentLayer(state) - const shape = getShape(layer) + const shape = getModel(layer) const numLayers = getNumLayers(state) return { @@ -33,7 +33,7 @@ const mapDispatchToProps = (dispatch, ownProps) => { dispatch(setCurrentLayer(id)) }, onLayerAdded: (type) => { - const attrs = registeredShapes[type].getInitialState() + const attrs = registeredModels[type].getInitialState() dispatch(addLayer(attrs)) }, onLayerRemoved: (id) => { diff --git a/src/features/layers/layersSlice.js b/src/features/layers/layersSlice.js index 2ef0c677..a350fa09 100644 --- a/src/features/layers/layersSlice.js +++ b/src/features/layers/layersSlice.js @@ -1,17 +1,17 @@ import { createSlice } from '@reduxjs/toolkit' import uniqueId from 'lodash/uniqueId' import arrayMove from 'array-move' -import { getShape } from '../../models/shapes' +import { getModel } from '../../config/models' const protectedAttrs = [ - 'repeatEnabled', 'canTransform', 'selectGroup', 'canChangeSize', 'autosize', - 'usesMachine', 'shouldCache', 'canChangeHeight', 'canRotate', 'usesFonts' + 'selectGroup', 'canChangeSize', 'autosize', 'usesMachine', 'shouldCache', 'canChangeHeight', + 'canRotate', 'usesFonts' ] const newLayerType = localStorage.getItem('currentShape') || 'polygon' -const newLayerName = getShape({type: newLayerType}).name.toLowerCase() +const newLayerName = getModel({type: newLayerType}).name.toLowerCase() const newEffectType = localStorage.getItem('currentEffect') || 'mask' -const newEffectName = getShape({type: newEffectType}).name.toLowerCase() +const newEffectName = getModel({type: newEffectType}).name.toLowerCase() function createLayer(state, attrs) { const restore = attrs.restore @@ -163,7 +163,7 @@ const layersSlice = createSlice({ restoreDefaults(state, action) { const id = action.payload const layer = state.byId[id] - const defaults = getShape(layer).getInitialState(layer) + const defaults = getModel(layer).getInitialState(layer) state.byId[layer.id] = { id: layer.id, @@ -184,7 +184,7 @@ const layersSlice = createSlice({ }, setShapeType(state, action) { const changes = action.payload - const defaults = getShape(changes).getInitialState() + const defaults = getModel(changes).getInitialState() const layer = state.byId[changes.id] layer.type = changes.type @@ -203,7 +203,7 @@ const layersSlice = createSlice({ setNewLayerType(state, action) { let attrs = { newLayerType: action.payload } if (!state.newLayerNameOverride) { - const shape = getShape({type: action.payload}) + const shape = getModel({type: action.payload}) attrs.newLayerName = shape.name.toLowerCase() } Object.assign(state, attrs) @@ -211,7 +211,7 @@ const layersSlice = createSlice({ setNewEffectType(state, action) { let attrs = { newEffectType: action.payload } if (!state.newEffectNameOverride) { - const shape = getShape({type: action.payload}) + const shape = getModel({type: action.payload}) attrs.newEffectName = shape.name.toLowerCase() } Object.assign(state, attrs) @@ -224,18 +224,6 @@ const layersSlice = createSlice({ updateLayers(state, action) { Object.assign(state, action.payload) }, - toggleRepeat(state, action) { - const layer = action.payload - state.byId[layer.id].repeatEnabled = !state.byId[layer.id].repeatEnabled - }, - toggleTrack(state, action) { - const layer = action.payload - state.byId[layer.id].trackEnabled = !state.byId[layer.id].trackEnabled - }, - toggleTrackGrow(state, action) { - const layer = action.payload - state.byId[layer.id].trackGrowEnabled = !state.byId[layer.id].trackGrowEnabled - }, toggleOpen(state, action) { const layer = action.payload state.byId[layer.id].open = !state.byId[layer.id].open @@ -263,9 +251,6 @@ export const { setNewEffectType, updateLayer, updateLayers, - toggleRepeat, - toggleTrack, - toggleTrackGrow, toggleVisible, toggleOpen, } = layersSlice.actions diff --git a/src/features/layers/layersSlice.spec.js b/src/features/layers/layersSlice.spec.js index 41cf78c8..24320e2b 100644 --- a/src/features/layers/layersSlice.spec.js +++ b/src/features/layers/layersSlice.spec.js @@ -17,9 +17,6 @@ import layers, { setShapeType, updateLayer, toggleOpen, - toggleRepeat, - toggleTrack, - toggleTrackGrow, toggleVisible } from './layersSlice' @@ -33,7 +30,6 @@ describe('layers reducer', () => { circleLobes: 1, circleDirection: 'clockwise', type: 'circle', - canTransform: true, selectGroup: 'Shapes', shouldCache: true, canRotate: true, @@ -512,7 +508,6 @@ describe('layers reducer', () => { byId: { 'layer-1': { id: 'layer-1', - repeatEnabled: false, canChangeSize: false } } @@ -553,69 +548,6 @@ describe('layers reducer', () => { }) }) - it('should handle toggleRepeat', () => { - expect( - layers( - { - byId: { - '1': { - repeatEnabled: false - } - } - }, - toggleRepeat({id: '1'}) - ) - ).toEqual({ - byId: { - '1': { - repeatEnabled: true - } - } - }) - }) - - it('should handle toggleTrack', () => { - expect( - layers( - { - byId: { - '1': { - trackEnabled: false - } - } - }, - toggleTrack({id: '1'}) - ) - ).toEqual({ - byId: { - '1': { - trackEnabled: true - } - } - }) - }) - - it('should handle toggleToggleGrow', () => { - expect( - layers( - { - byId: { - '1': { - trackGrowEnabled: false - } - } - }, - toggleTrackGrow({id: '1'}) - ) - ).toEqual({ - byId: { - '1': { - trackGrowEnabled: true - } - } - }) - }) - it('should handle toggleVisible', () => { expect( layers( diff --git a/src/features/machine/PolarSettings.js b/src/features/machine/PolarSettings.js index 0c716786..c7523dd8 100644 --- a/src/features/machine/PolarSettings.js +++ b/src/features/machine/PolarSettings.js @@ -11,8 +11,8 @@ import { } from 'react-bootstrap' import InputOption from '../../components/InputOption' import CheckboxOption from '../../components/CheckboxOption' -import Machine from '../../models/Machine' import { getMachine } from '../store/selectors' +import { machineOptions } from './options' import { toggleMachinePolarExpanded, updateMachine, @@ -29,7 +29,7 @@ const mapStateToProps = (state, ownProps) => { startPoint: machine.polarStartPoint, endPoint: machine.polarEndPoint, minimizeMoves: machine.minimizeMoves, - options: new Machine().getOptions() + options: machineOptions } } diff --git a/src/features/machine/RectSettings.js b/src/features/machine/RectSettings.js index 01bc7a91..6d9aeceb 100644 --- a/src/features/machine/RectSettings.js +++ b/src/features/machine/RectSettings.js @@ -11,7 +11,6 @@ import { } from 'react-bootstrap' import InputOption from '../../components/InputOption' import CheckboxOption from '../../components/CheckboxOption' -import Machine from '../../models/Machine' import { updateMachine, toggleMinimizeMoves, @@ -19,6 +18,7 @@ import { setMachineRectOrigin } from './machineSlice' import { getMachine } from '../store/selectors' +import { machineOptions } from './options' const mapStateToProps = (state, ownProps) => { const machine = getMachine(state) @@ -32,7 +32,7 @@ const mapStateToProps = (state, ownProps) => { maxY: machine.maxY, origin: machine.rectOrigin, minimizeMoves: machine.minimizeMoves, - options: new Machine().getOptions() + options: machineOptions } } diff --git a/src/features/machine/computer.js b/src/features/machine/computer.js index 3e3b50b4..267af078 100644 --- a/src/features/machine/computer.js +++ b/src/features/machine/computer.js @@ -1,79 +1,65 @@ import ReactGA from 'react-ga' import throttle from 'lodash/throttle' -import { distance, scale } from '../../common/geometry' import PolarMachine from './PolarMachine' import RectMachine from './RectMachine' -import { getShape } from '../../models/shapes' +import { getModel } from '../../config/models' import Victor from 'victor' -function track(vertex, data, loopIndex) { - const angle = data.trackLength * loopIndex / 16 * 2.0 * Math.PI - let radius = 1.0 - - if (data.trackGrowEnabled) { - radius = 1.0 + loopIndex / 10.0 * data.trackGrow / 100.0 - } - return new Victor( - vertex.x + radius * data.trackValue * Math.cos(angle), - vertex.y + radius * data.trackValue * Math.sin(angle) - ) -} - -export const transformShape = (data, vertex, amount, trackIndex=0, numLoops) => { - numLoops = numLoops || data.numLoops - let transformedVertex = vertex ? vertex.clone() : new Victor(0.0) - - // if (data.repeatEnabled && data.trackEnabled) { - // transformedVertex = track(transformedVertex, data, trackIndex) - // } - - transformedVertex.rotateDeg(-data.rotation) - transformedVertex.addX({x: data.offsetX || 0}).addY({y: data.offsetY || 0}) - - return transformedVertex -} - -function buildTrackLoop(vertices, transform, i, t) { - const numLoops = transform.repeatEnabled ? transform.numLoops : 1 - const numTrackLoops = transform.repeatEnabled ? transform.trackNumLoops : 1 - const nextTrackVertex = transformShape(transform, vertices[0], 0, i + 1, numTrackLoops) - const backtrack = numTrackLoops > 1 && t === numTrackLoops - 1 - let trackVertices = [] - let trackDistances = [] - const drawPortionPct = Math.round((transform.drawPortionPct || 100)/100.0 * vertices.length) - const completion = (i === numLoops - 1 && t === numTrackLoops - 1) ? drawPortionPct : vertices.length - - for (var j=0; j 1 && t === numTrackLoops - 1 +// let trackVertices = [] +// let trackDistances = [] +// const drawPortionPct = Math.round((transform.drawPortionPct || 100)/100.0 * vertices.length) +// const completion = (i === numLoops - 1 && t === numTrackLoops - 1) ? drawPortionPct : vertices.length + +// for (var j=0; j { - if (d <= minD) { - minD = d - minIdx = idx - } - }) +// if (backtrack) { +// let minIdx = 0 +// let minD = Number.MAX_SAFE_INTEGER - if (minIdx !== 0) { - trackVertices = trackVertices.concat(trackVertices.slice(minIdx, trackVertices.length-1).reverse()) - } - } +// trackDistances.forEach((d, idx) => { +// if (d <= minD) { +// minD = d +// minIdx = idx +// } +// }) - return trackVertices -} +// if (minIdx !== 0) { +// trackVertices = trackVertices.concat(trackVertices.slice(minIdx, trackVertices.length-1).reverse()) +// } +// } + +// return trackVertices +//} // ensure vertices do not exceed machine boundary limits, and endpoints as needed export const polishVertices = (vertices, machine, layerInfo={}) => { @@ -100,30 +86,30 @@ const throttledReportTiming = throttle(reportTiming, 1000, {trailing: true }) export const transformShapes = (vertices, layer, effects) => { const startTime = performance.now() - const numLoops = layer.repeatEnabled ? layer.numLoops : 1 - const numTrackLoops = layer.repeatEnabled ? layer.trackNumLoops : 1 - let outputVertices = [] - - if (layer.autosize) { - vertices = vertices.map(vertex => { - return scale(vertex, 100.0 * layer.startingWidth, 100 * layer.startingHeight) - }) - } - - if (layer.trackEnabled && numTrackLoops > 1) { - for (var i=0; i vertex.clone()) +// if (layer.autosize) { +// vertices = vertices.map(vertex => { +// return scale(vertex, 100.0 * layer.startingWidth, 100 * layer.startingHeight) +// }) +// } + +// TODO: remove this +// if (layer.trackEnabled && numTrackLoops > 1) { +// for (var i=0; i { if (effects && effects.length > 0) { effects.forEach(effect => { - outputVertices = getShape(effect).applyEffect(effect, layer, outputVertices) + outputVertices = getModel(effect).applyEffect(effect, layer, outputVertices) }) } diff --git a/src/models/Machine.js b/src/features/machine/options.js similarity index 80% rename from src/models/Machine.js rename to src/features/machine/options.js index e52ae792..f2745ad4 100644 --- a/src/models/Machine.js +++ b/src/features/machine/options.js @@ -1,4 +1,4 @@ -const machineOptions = { +export const machineOptions = { minX: { title: 'Min X (mm)', }, @@ -28,9 +28,3 @@ const machineOptions = { title: 'Start point' }, } - -export default class Machine { - getOptions() { - return machineOptions - } -} diff --git a/src/features/machine/selectors.js b/src/features/machine/selectors.js index bc3337a0..af4743d0 100644 --- a/src/features/machine/selectors.js +++ b/src/features/machine/selectors.js @@ -1,9 +1,8 @@ import LRUCache from 'lru-cache' import { createSelector } from 'reselect' -import Victor from 'victor' import Color from 'color' -import { transformShapes, transformShape, polishVertices, getMachineInstance } from './computer' -import { getShape } from '../../models/shapes' +import { transformShapes, polishVertices, getMachineInstance } from './computer' +import { getModel } from '../../config/models' import { getMachine, getState, getPreview } from '../store/selectors' import { getLoadedFonts } from '../fonts/selectors' import { makeGetLayer, getNumVisibleLayers, getVisibleNonEffectIds, makeGetEffects, makeGetNonEffectLayerIndex } from '../layers/selectors' @@ -65,14 +64,15 @@ const makeGetLayerVertices = layerId => { fonts: fonts } log('makeGetLayerVertices', layerId) - const metashape = getShape(layer) + const metashape = getModel(layer) + // TODO: fix this; move cache into model? Should be caching vertices only, not transforms if (layer.shouldCache) { const key = getCacheKey(state) let vertices = cache.get(key) if (!vertices) { - vertices = metashape.getVertices(state) + vertices = metashape.draw(state) if (vertices.length > 1) { cache.set(key, vertices) @@ -315,17 +315,18 @@ export const makeGetPreviewTrackVertices = layerId => { getCachedSelector(makeGetLayer, layerId), (layer) => { log('makeGetPreviewTrackVertices', layerId) - const numLoops = layer.numLoops +// const numLoops = layer.numLoops const konvaScale = layer.autosize ? 5 : 1 // our transformer is 5 times bigger than the actual starting shape const konvaDeltaX = (konvaScale - 1)/2 * layer.startingWidth const konvaDeltaY = (konvaScale - 1)/2 * layer.startingHeight let trackVertices = [] - for (var i=0; i { return offset(rotate(offset(vertex, -layer.offsetX, -layer.offsetY), layer.rotation), konvaDeltaX, -konvaDeltaY) diff --git a/src/features/preview/PreviewLayer.js b/src/features/preview/PreviewLayer.js index c02d2086..70680c9b 100644 --- a/src/features/preview/PreviewLayer.js +++ b/src/features/preview/PreviewLayer.js @@ -222,7 +222,7 @@ const PreviewLayer = (ownProps) => { { - const layer = getCurrentLayer(state) - - return { - layer: layer, - active: true, - options: (new Transform()).getOptions() - } -} - -const mapDispatchToProps = (dispatch, ownProps) => { - const { id } = ownProps - - return { - onChange: (attrs) => { - attrs.id = id - dispatch(updateLayer(attrs)) - }, - onRepeat: () => { - dispatch(toggleRepeat({id: id})) - } - } -} - -class Transforms extends Component { - render() { - const activeClassName = this.props.active ? 'active' : '' - const activeKey = this.props.active ? 1 : null - - return ( -
- - - - - - - - - - - - - {!this.props.layer.effect && - -

Fine tuning (advanced)

- - -
-
} -
- ) - } -} - -export default connect(mapStateToProps, mapDispatchToProps)(Transforms) diff --git a/src/models/Effect.js b/src/models/Effect.js index 16f9b9bd..904b676f 100644 --- a/src/models/Effect.js +++ b/src/models/Effect.js @@ -1,15 +1,13 @@ -import Shape from './Shape' +import Model from './Model' -export default class Effect extends Shape { +export default class Effect extends Model { getInitialState() { return { ...super.getInitialState(), ...{ effect: true, - canTransform: false, shouldCache: false, canChangeSize: true, - repeatEnabled: false, autosize: false } } diff --git a/src/models/Group.js b/src/models/Group.js new file mode 100644 index 00000000..0c2a6989 --- /dev/null +++ b/src/models/Group.js @@ -0,0 +1,23 @@ +import Layer, { layerOptions } from './Layer' + +export const groupOptions = { + ...layerOptions, + ...{ + // TODO + } +} + +export default class Group extends Layer { + getInitialState() { + return { + ...super.getInitialState(), + ... { + // TODO + } + } + } + + getOptions() { + return groupOptions + } +} diff --git a/src/models/Layer.js b/src/models/Layer.js new file mode 100644 index 00000000..55c7d284 --- /dev/null +++ b/src/models/Layer.js @@ -0,0 +1,28 @@ +import Model, { modelOptions } from './Model' + +export const layerOptions = { + ...modelOptions, + ...{ + connectionMethod: { + title: 'Connect to next layer', + type: 'togglebutton', + choices: ['line', 'along perimeter'] + }, + } +} + +export default class Layer extends Model { + getInitialState() { + return { + ...super.getInitialState(), + ... { + open: true, + connectionMethod: 'line', + } + } + } + + getOptions() { + return layerOptions + } +} diff --git a/src/models/Model.js b/src/models/Model.js new file mode 100644 index 00000000..e1edc023 --- /dev/null +++ b/src/models/Model.js @@ -0,0 +1,92 @@ +export const modelOptions = { + name: { + title: 'Name', + type: 'text' + }, + startingWidth: { + title: 'Initial width', + min: 1, + isVisible: (state) => { return state.canChangeSize }, + onChange: (changes, attrs) => { + if (!attrs.canChangeHeight) { + changes.startingHeight = changes.startingWidth + } + return changes + } + }, + startingHeight: { + title: 'Initial height', + min: 1, + isVisible: (state) => { return state.canChangeSize && state.canChangeHeight }, + }, + offsetX: { + title: 'X offset', + isVisible: (state) => { return state.canMove } + }, + offsetY: { + title: 'Y offset', + isVisible: (state) => { return state.canMove } + }, + reverse: { + title: 'Reverse path', + type: 'checkbox', + isVisible: (state) => { return !state.effect } + }, + rotation: { + title: 'Rotate (degrees)', + isVisible: state => { return state.canRotate } + }, +} + +export default class Model { + constructor(name) { + this.name = name + this.cache = [] + } + + getInitialState() { + return { + shouldCache: true, + autosize: true, + canChangeSize: true, + canChangeHeight: true, + canRotate: true, + canMove: true, + usesMachine: false, + usesFonts: false, + startingWidth: 10, + startingHeight: 10, + offsetX: 0.0, + offsetY: 0.0, + rotation: 0, + reverse: false, + dragging: false, + visible: true, + effect: false + } + } + + getOptions() { + return modelOptions + } + + getVertices(state) { + return [] + } + + draw(state) { + const { startingWidth, startingHeight, autosize, offsetX, offsetY, rotation } = state.shape + let vertices = this.getVertices(state) + + vertices.forEach(vertex => { + if (autosize) { + vertex.multiply({x: startingWidth, y: startingHeight}) + } + + vertex.rotateDeg(-rotation) + vertex.addX({x: offsetX || 0}).addY({y: offsetY || 0}) + }) + + return vertices + } +} diff --git a/src/models/Shape.js b/src/models/Shape.js index b193b8c3..55c51743 100644 --- a/src/models/Shape.js +++ b/src/models/Shape.js @@ -1,47 +1,11 @@ +import Layer, { layerOptions } from './Layer' + export const shapeOptions = { - name: { - title: 'Name', - type: 'text' - } + ...layerOptions } -export default class Shape { - constructor(name) { - this.name = name - this.cache = [] - } - - getInitialState() { - return { - canTransform: true, - selectGroup: 'Shapes', - shouldCache: true, - autosize: true, - canChangeSize: true, - canChangeHeight: true, - canRotate: true, - canMove: true, - usesMachine: false, - usesFonts: false, - startingWidth: 10, - startingHeight: 10, - offsetX: 0.0, - offsetY: 0.0, - open: true, - rotation: 0, - connectionMethod: 'line', - reverse: false, - dragging: false, - visible: true, - effect: false - } - } - +export default class Shape extends Layer { getOptions() { return shapeOptions } - - getVertices(state) { - return [] - } } diff --git a/src/models/Transform.js b/src/models/Transform.js deleted file mode 100644 index a4b58950..00000000 --- a/src/models/Transform.js +++ /dev/null @@ -1,47 +0,0 @@ -const transformOptions = { - startingWidth: { - title: 'Initial width', - min: 1, - isVisible: (state) => { return state.canChangeSize }, - onChange: (changes, attrs) => { - if (!attrs.canChangeHeight) { - changes.startingHeight = changes.startingWidth - } - return changes - } - }, - startingHeight: { - title: 'Initial height', - min: 1, - isVisible: (state) => { return state.canChangeSize && state.canChangeHeight }, - }, - offsetX: { - title: 'X offset', - isVisible: (state) => { return state.canMove } - }, - offsetY: { - title: 'Y offset', - isVisible: (state) => { return state.canMove } - }, - rotation: { - title: 'Rotate (degrees)', - isVisible: state => { return state.canRotate } - }, - connectionMethod: { - title: 'Connect to next layer', - choices: ['line', 'along perimeter'] - }, - reverse: { - title: 'Reverse path', - type: 'checkbox', - isVisible: (state) => { return !state.effect } - } -} - -// used as a way to keep a shape's transform settings separate. Actual state -// is stored on Shape. -export default class Transform { - getOptions() { - return transformOptions - } -} diff --git a/src/models/FineTuning.js b/src/models/effects/FineTuning.js similarity index 90% rename from src/models/FineTuning.js rename to src/models/effects/FineTuning.js index 61afb51a..39f7496f 100644 --- a/src/models/FineTuning.js +++ b/src/models/effects/FineTuning.js @@ -1,10 +1,10 @@ -import { shapeOptions } from './Shape' -import { arrayRotate } from '../common/util' -import { circle } from '../common/geometry' -import Effect from './Effect' +import { modelOptions } from '../Model' +import { arrayRotate } from '@/common/util' +import { circle } from '@/common/geometry' +import Effect from '../Effect' const options = { - ...shapeOptions, + ...modelOptions, ...{ backtrackPct: { title: 'Backtrack at end (%)', @@ -39,7 +39,6 @@ export default class FineTuning extends Effect { // Inherited type: 'fineTuning', selectGroup: 'effects', - canTransform: false, canChangeSize: false, canRotate: false, canMove: false, diff --git a/src/models/Loop.js b/src/models/effects/Loop.js similarity index 82% rename from src/models/Loop.js rename to src/models/effects/Loop.js index 44878588..3a71aeed 100644 --- a/src/models/Loop.js +++ b/src/models/effects/Loop.js @@ -1,10 +1,10 @@ -import { shapeOptions } from './Shape' -import Effect from './Effect' -import { scale, rotate, circle } from '../common/geometry' +import { modelOptions } from '../Model' +import Effect from '../Effect' +import { scale, rotate, circle } from '@/common/geometry' import { evaluate } from 'mathjs' const options = { - ...shapeOptions, + ...modelOptions, ...{ numLoops: { title: 'Number of loops', @@ -75,7 +75,6 @@ export default class Loop extends Effect { // Inherited type: 'loop', selectGroup: 'effects', - canTransform: false, canChangeSize: false, canRotate: false, canMove: false, @@ -109,17 +108,23 @@ export default class Loop extends Effect { } applyEffect(effect, layer, vertices) { + const outputVertices = [] + const { offsetX, offsetY, rotation } = layer - // Remove one point if we are smearing + // remove first point if we are smearing if (effect.transformMethod === 'smear') { vertices.pop() } - let outputVertices = [] + // remove rotation and offsets; will add back at end + vertices.forEach(vertex => { + vertex.addX({x: -offsetX || 0}).addY({y: -offsetY || 0}) + vertex.rotateDeg(rotation) + }) for (var i=0; i { + vertex.rotateDeg(-rotation) + vertex.addX({x: offsetX || 0}).addY({y: offsetY || 0}) + }) + return outputVertices } diff --git a/src/models/Mask.js b/src/models/effects/Mask.js similarity index 86% rename from src/models/Mask.js rename to src/models/effects/Mask.js index 675c98f3..819b01ca 100644 --- a/src/models/Mask.js +++ b/src/models/effects/Mask.js @@ -1,14 +1,14 @@ import Victor from 'victor' -import { shapeOptions } from './Shape' -import Effect from './Effect' -import { rotate, offset, circle } from '../common/geometry' -import PolarMachine from '../features/machine/PolarMachine' -import RectMachine from '../features/machine/RectMachine' -import PolarInvertedMachine from '../features/machine/PolarInvertedMachine' -import RectInvertedMachine from '../features/machine/RectInvertedMachine' +import { modelOptions } from '../Model' +import Effect from '../Effect' +import { rotate, offset, circle } from '@/common/geometry' +import PolarMachine from '@/features/machine/PolarMachine' +import RectMachine from '@/features/machine/RectMachine' +import PolarInvertedMachine from '@/features/machine/PolarInvertedMachine' +import RectInvertedMachine from '@/features/machine/RectInvertedMachine' const options = { - ...shapeOptions, + ...modelOptions, ...{ maskMachine: { title: 'Mask shape', diff --git a/src/models/Noise.js b/src/models/effects/Noise.js similarity index 92% rename from src/models/Noise.js rename to src/models/effects/Noise.js index dba833e0..f19900e5 100644 --- a/src/models/Noise.js +++ b/src/models/effects/Noise.js @@ -1,11 +1,11 @@ -import { shapeOptions } from './Shape' +import { modelOptions } from '../Model' import Victor from 'victor' -import Effect from './Effect' -import noise from '../common/noise' -import { subsample } from '../common/geometry' +import Effect from '../Effect' +import noise from '@/common/noise' +import { subsample } from '@/common/geometry' const options = { - ...shapeOptions, + ...modelOptions, ...{ seed: { title: 'Random seed', @@ -53,8 +53,6 @@ export default class Noise extends Effect { noiseMagnification: 58, noiseType: 'Simplex', noiseApplication: 'Contour', - canTransform: false, - repeatEnabled: false, canChangeSize: false, canRotate: false, canMove: false, diff --git a/src/models/TrackTransform.js b/src/models/effects/Track.js similarity index 87% rename from src/models/TrackTransform.js rename to src/models/effects/Track.js index 5c9082f5..bfab1b76 100644 --- a/src/models/TrackTransform.js +++ b/src/models/effects/Track.js @@ -1,22 +1,22 @@ -import { shapeOptions } from './Shape' -import Effect from './Effect' -import { offset, rotate, circle } from '../common/geometry' +import { modelOptions } from '../Model' +import Effect from '../Effect' +import { offset, rotate, circle } from '@/common/geometry' const options = { - ...shapeOptions, + ...modelOptions, ...{ trackRadius: { title: 'Track radius', }, trackRotations: { - title: 'Track Rotations' + title: 'Track rotations' }, trackSpiralEnabled: { title: 'Spiral track', type: 'checkbox', }, trackSpiralRadius: { - title: 'Spiral Radius', + title: 'Spiral radius', isVisible: state => { return state.trackSpiralEnabled }, }, } @@ -34,7 +34,6 @@ export default class Track extends Effect { // Inherited type: 'track', selectGroup: 'effects', - canTransform: false, canChangeSize: false, canRotate: false, canMove: false, diff --git a/src/models/Warp.js b/src/models/effects/Warp.js similarity index 97% rename from src/models/Warp.js rename to src/models/effects/Warp.js index 02756b63..ec29da52 100644 --- a/src/models/Warp.js +++ b/src/models/effects/Warp.js @@ -1,11 +1,11 @@ import Victor from 'victor' -import Effect from './Effect' -import { shapeOptions } from './Shape' -import { circle, subsample } from '../common/geometry' +import Effect from '../Effect' +import { modelOptions } from '../Model' +import { circle, subsample } from '@/common/geometry' import { evaluate } from 'mathjs' const options = { - ...shapeOptions, + ...modelOptions, ...{ warpType: { title: 'Warp type', diff --git a/src/models/shapes.js b/src/models/shapes.js deleted file mode 100644 index ade71a60..00000000 --- a/src/models/shapes.js +++ /dev/null @@ -1,107 +0,0 @@ -import Circle from '../models/Circle' -import Epicycloid from '../models/Epicycloid' -import FileImport from '../models/FileImport' -import Fisheye from '../models/Fisheye' -import Warp from '../models/Warp' -import CirclePacker from '../models/circle_packer/CirclePacker' -import FractalSpirograph from '../models/fractal_spirograph/FractalSpirograph' -// import Freeform from '../models/Freeform' -import Heart from '../models/Heart' -import Hypocycloid from '../models/Hypocycloid' -import InputText from '../models/input_text/InputText' -import FancyText from '../models/FancyText' -import LSystem from '../models/lsystem/LSystem' -import Mask from '../models/Mask' -import Noise from '../models/Noise' -import NoiseWave from '../models/NoiseWave' -import Point from '../models/Point' -import Polygon from '../models/Polygon' -import Reuleaux from '../models/Reuleaux' -import Rose from '../models/Rose' -import SpaceFiller from '../models/space_filler/SpaceFiller' -import Star from '../models/Star' -import TessellationTwist from '../models/tessellation_twist/TessellationTwist' -import V1Engineering from '../models/v1_engineering/V1Engineering' -import Wiper from '../models/Wiper' -import Loop from './Loop' -import Track from './TrackTransform' -import FineTuning from './FineTuning' - -/*---------------------------------------------- -Supported input shapes ------------------------------------------------*/ -export const registeredShapes = { - polygon: new Polygon(), - star: new Star(), - circle: new Circle(), - heart: new Heart(), - reuleaux: new Reuleaux(), - epicycloid: new Epicycloid(), - hypocycloid: new Hypocycloid(), - rose: new Rose(), - inputText: new InputText(), - fancy_text: new FancyText(), - v1Engineering: new V1Engineering(), - lsystem: new LSystem(), - fractal_spirograph: new FractalSpirograph(), - tessellation_twist: new TessellationTwist(), - point: new Point(), - // freeform: new Freeform(), - circle_packer: new CirclePacker(), - wiper: new Wiper(), - space_filler: new SpaceFiller(), - noise_wave: new NoiseWave(), - file_import: new FileImport(), - fisheye: new Fisheye(), - loop: new Loop(), - track: new Track(), - mask: new Mask(), - noise: new Noise(), - warp: new Warp(), - fineTuning: new FineTuning() -} - -export const getShape = (layer) => { - return registeredShapes[layer.type] -} - -export const getShapeDefaults = () => { - return Object.keys(registeredShapes).map(id => { - const state = registeredShapes[id].getInitialState() - state.name = registeredShapes[id].name - state.id = id - return state - }) -} - -export const getShapeSelectOptions = () => { - const groupOptions = [] - const shapes = getShapeDefaults() - - for (const shape of shapes) { - const optionLabel = { value: shape.id, label: shape.name } - var found = false - - for (const group of groupOptions) { - if (group.label === shape.selectGroup) { - found = true - group.options.push(optionLabel) - } - } - if (!found) { - if (shape.selectGroup === 'import') { - // users can't manually select this group - continue - } else if (shape.selectGroup === 'effects') { - // effects are added separately - // TODO: when effects can be added separately, uncomment the next line - // continue - } - - const newOptions = [ optionLabel ] - groupOptions.push( { label: shape.selectGroup, options: newOptions } ) - } - } - - return groupOptions -} diff --git a/src/models/Circle.js b/src/models/shapes/Circle.js similarity index 96% rename from src/models/Circle.js rename to src/models/shapes/Circle.js index 90ba6e2e..21514c05 100644 --- a/src/models/Circle.js +++ b/src/models/shapes/Circle.js @@ -1,5 +1,5 @@ import Victor from 'victor' -import Shape, { shapeOptions } from './Shape' +import Shape, { shapeOptions } from '../Shape' const options = { ...shapeOptions, diff --git a/src/models/Epicycloid.js b/src/models/shapes/Epicycloid.js similarity index 93% rename from src/models/Epicycloid.js rename to src/models/shapes/Epicycloid.js index 153db217..b6c2b186 100644 --- a/src/models/Epicycloid.js +++ b/src/models/shapes/Epicycloid.js @@ -1,6 +1,6 @@ import Victor from 'victor' -import Shape, { shapeOptions } from './Shape' -import { reduce } from '../common/util' +import Shape, { shapeOptions } from '../Shape' +import { reduce } from '@/common/util' const options = { ...shapeOptions, diff --git a/src/models/FancyText.js b/src/models/shapes/FancyText.js similarity index 96% rename from src/models/FancyText.js rename to src/models/shapes/FancyText.js index 94e47cc9..b2bf3875 100644 --- a/src/models/FancyText.js +++ b/src/models/shapes/FancyText.js @@ -1,10 +1,10 @@ import Victor from 'victor' -import Shape, { shapeOptions } from './Shape' -import { subsample, centerOnOrigin, maxY, minY, horizontalAlign, findBounds, nearestVertex, findMinimumVertex } from '../common/geometry' -import { arrayRotate } from '../common/util' +import Shape, { shapeOptions } from '../Shape' +import { subsample, centerOnOrigin, maxY, minY, horizontalAlign, findBounds, nearestVertex, findMinimumVertex } from '@/common/geometry' +import { arrayRotate } from '@/common/util' import { pointsOnPath } from 'points-on-path' -import { getFont, supportedFonts } from '../features/fonts/fontsSlice' -import { getMachineInstance } from '../features/machine/computer' +import { getFont, supportedFonts } from '@/features/fonts/fontsSlice' +import { getMachineInstance } from '@/features/machine/computer' import pointInPolygon from 'point-in-polygon' const MIN_SPACING_MULTIPLIER = 1.2 @@ -57,7 +57,6 @@ export default class FancyText extends Shape { fancyAlignment: 'left', fancyConnectLines: 'inside', fancyLineSpacing: 1.0, - repeatEnabled: false, usesMachine: true, usesFonts: true } diff --git a/src/models/FileImport.js b/src/models/shapes/FileImport.js similarity index 94% rename from src/models/FileImport.js rename to src/models/shapes/FileImport.js index 1c5d99f3..78cf4fe7 100644 --- a/src/models/FileImport.js +++ b/src/models/shapes/FileImport.js @@ -1,5 +1,5 @@ import Victor from 'victor' -import Shape, { shapeOptions } from './Shape' +import Shape, { shapeOptions } from '../Shape' const options = { ...shapeOptions, @@ -35,9 +35,7 @@ export default class FileImport extends Shape { vertices: [], comments: [], selectGroup: 'import', - canTransform: false, usesMachine: true, - repeatEnabled: false }, ...(importProps === undefined ? {} : { fileName: importProps.fileName, diff --git a/src/models/Fisheye.js b/src/models/shapes/Fisheye.js similarity index 90% rename from src/models/Fisheye.js rename to src/models/shapes/Fisheye.js index f9fd957c..a4d71524 100644 --- a/src/models/Fisheye.js +++ b/src/models/shapes/Fisheye.js @@ -1,7 +1,7 @@ import Victor from 'victor' -import Effect from './Effect' -import { shapeOptions } from './Shape' -import { circle } from '../common/geometry' +import Effect from '../Effect' +import { shapeOptions } from '../Shape' +import { circle } from '@/common/geometry' import * as d3Fisheye from 'd3-fisheye' const options = { diff --git a/src/models/Freeform.js b/src/models/shapes/Freeform.js similarity index 90% rename from src/models/Freeform.js rename to src/models/shapes/Freeform.js index 442ede83..d253770d 100644 --- a/src/models/Freeform.js +++ b/src/models/shapes/Freeform.js @@ -1,5 +1,5 @@ import Victor from 'victor' -import Shape, { shapeOptions } from './Shape' +import Shape, { shapeOptions } from '../Shape' const options = { ...shapeOptions, @@ -22,7 +22,6 @@ export default class Freeform extends Shape { ...{ type: 'freeform', freeformPoints: '-1,-1;-1,1;1,1', - repeatEnabled: false, canChangeHeight: false, startingWidth: 50, startingHeight: 50 diff --git a/src/models/Heart.js b/src/models/shapes/Heart.js similarity index 96% rename from src/models/Heart.js rename to src/models/shapes/Heart.js index aafda133..7fbf71a6 100644 --- a/src/models/Heart.js +++ b/src/models/shapes/Heart.js @@ -1,5 +1,5 @@ import Victor from 'victor' -import Shape from './Shape' +import Shape from '../Shape' export default class Heart extends Shape { constructor() { diff --git a/src/models/Hypocycloid.js b/src/models/shapes/Hypocycloid.js similarity index 93% rename from src/models/Hypocycloid.js rename to src/models/shapes/Hypocycloid.js index 50cb5163..1662dbf0 100644 --- a/src/models/Hypocycloid.js +++ b/src/models/shapes/Hypocycloid.js @@ -1,6 +1,6 @@ import Victor from 'victor' -import Shape, { shapeOptions } from './Shape' -import { reduce } from '../common/util' +import Shape, { shapeOptions } from '../Shape' +import { reduce } from '@/common/util' const options = { ...shapeOptions, diff --git a/src/models/NoiseWave.js b/src/models/shapes/NoiseWave.js similarity index 94% rename from src/models/NoiseWave.js rename to src/models/shapes/NoiseWave.js index ded6f5a5..d40f72fb 100644 --- a/src/models/NoiseWave.js +++ b/src/models/shapes/NoiseWave.js @@ -1,10 +1,10 @@ -import Shape, { shapeOptions } from './Shape' -import { getMachineInstance } from '../features/machine/computer' +import Shape, { shapeOptions } from '../Shape' +import { getMachineInstance } from '@/features/machine/computer' import Victor from 'victor' -import noise from '../common/noise' +import noise from '@/common/noise' import seedrandom from 'seedrandom' import { shapeSimilarity } from 'curve-matcher' -import { offset } from '../common/geometry' +import { offset } from '@/common/geometry' const options = { ...shapeOptions, @@ -47,8 +47,6 @@ export default class NoiseWave extends Shape { noiseType: 'Perlin', numParticles: 100, selectGroup: 'Erasers', - canTransform: false, - repeatEnabled: false, canChangeSize: false, autosize: false, usesMachine: true, diff --git a/src/models/Point.js b/src/models/shapes/Point.js similarity index 83% rename from src/models/Point.js rename to src/models/shapes/Point.js index 4a53e9a4..9ce25c88 100644 --- a/src/models/Point.js +++ b/src/models/shapes/Point.js @@ -1,5 +1,5 @@ import Victor from 'victor' -import Shape from './Shape' +import Shape from '../Shape' export default class Point extends Shape { constructor() { @@ -14,10 +14,8 @@ export default class Point extends Shape { autosize: false, startingWidth: 1, startingHeight: 1, - canTransform: false, shouldCache: false, canChangeSize: false, - repeatEnabled: false, } } } diff --git a/src/models/Polygon.js b/src/models/shapes/Polygon.js similarity index 98% rename from src/models/Polygon.js rename to src/models/shapes/Polygon.js index 29a97559..11ded655 100644 --- a/src/models/Polygon.js +++ b/src/models/shapes/Polygon.js @@ -1,5 +1,5 @@ import Victor from 'victor' -import Shape, { shapeOptions } from './Shape' +import Shape, { shapeOptions } from '../Shape' const options = { ...shapeOptions, diff --git a/src/models/Reuleaux.js b/src/models/shapes/Reuleaux.js similarity index 96% rename from src/models/Reuleaux.js rename to src/models/shapes/Reuleaux.js index d6265744..0d2da986 100644 --- a/src/models/Reuleaux.js +++ b/src/models/shapes/Reuleaux.js @@ -1,5 +1,5 @@ import Victor from 'victor' -import Shape, { shapeOptions } from './Shape' +import Shape, { shapeOptions } from '../Shape' const options = { ...shapeOptions, diff --git a/src/models/Rose.js b/src/models/shapes/Rose.js similarity index 95% rename from src/models/Rose.js rename to src/models/shapes/Rose.js index a00f0940..8d5ff082 100644 --- a/src/models/Rose.js +++ b/src/models/shapes/Rose.js @@ -1,5 +1,5 @@ import Victor from 'victor' -import Shape, { shapeOptions } from './Shape' +import Shape, { shapeOptions } from '../Shape' const options = { ...shapeOptions, diff --git a/src/models/Star.js b/src/models/shapes/Star.js similarity index 95% rename from src/models/Star.js rename to src/models/shapes/Star.js index 032a1db7..af62eb9b 100644 --- a/src/models/Star.js +++ b/src/models/shapes/Star.js @@ -1,5 +1,5 @@ import Victor from 'victor' -import Shape, { shapeOptions } from './Shape' +import Shape, { shapeOptions } from '../Shape' const options = { ...shapeOptions, diff --git a/src/models/Wiper.js b/src/models/shapes/Wiper.js similarity index 97% rename from src/models/Wiper.js rename to src/models/shapes/Wiper.js index 7db1cc49..29b0666e 100644 --- a/src/models/Wiper.js +++ b/src/models/shapes/Wiper.js @@ -1,6 +1,6 @@ -import { degToRad } from '../common/geometry' +import { degToRad } from '@/common/geometry' import Victor from 'victor' -import Shape, { shapeOptions } from './Shape' +import Shape, { shapeOptions } from '../Shape' const options = { ...shapeOptions, @@ -252,12 +252,10 @@ export default class Wiper extends Shape { wiperSize: 4, wiperType: 'Lines', selectGroup: 'Erasers', - canTransform: false, canChangeSize: false, shouldCache: false, autosize: false, usesMachine: true, - repeatEnabled: false, } } } diff --git a/src/models/circle_packer/Circle.js b/src/models/shapes/circle_packer/Circle.js similarity index 100% rename from src/models/circle_packer/Circle.js rename to src/models/shapes/circle_packer/Circle.js diff --git a/src/models/circle_packer/CirclePacker.js b/src/models/shapes/circle_packer/CirclePacker.js similarity index 97% rename from src/models/circle_packer/CirclePacker.js rename to src/models/shapes/circle_packer/CirclePacker.js index c41d89a2..44d296aa 100644 --- a/src/models/circle_packer/CirclePacker.js +++ b/src/models/shapes/circle_packer/CirclePacker.js @@ -1,9 +1,9 @@ import seedrandom from 'seedrandom' -import Shape, { shapeOptions } from '../Shape' +import Shape, { shapeOptions } from '../../Shape' import { Circle } from './Circle' -import Graph from '../../common/Graph' -import { circle, arc } from '../../common/geometry' -import { getMachineInstance } from '../../features/machine/computer' +import Graph from '@/common/Graph' +import { circle, arc } from '@/common/geometry' +import { getMachineInstance } from '@/features/machine/computer' import Victor from 'victor' const ROUNDS = 100 // default number of rounds to attempt to create and grow circles @@ -50,10 +50,8 @@ export default class CirclePacker extends Shape { seed: 1, startingRadius: 4, attempts: 20, - canTransform: false, inBounds: false, usesMachine: true, - repeatEnabled: false, canChangeSize: false, canRotate: false, canMove: false, diff --git a/src/models/fractal_spirograph/FractalSpirograph.js b/src/models/shapes/fractal_spirograph/FractalSpirograph.js similarity index 96% rename from src/models/fractal_spirograph/FractalSpirograph.js rename to src/models/shapes/fractal_spirograph/FractalSpirograph.js index 4cc63c6c..a0a7f3f3 100644 --- a/src/models/fractal_spirograph/FractalSpirograph.js +++ b/src/models/shapes/fractal_spirograph/FractalSpirograph.js @@ -1,5 +1,5 @@ import Victor from 'victor' -import Shape, { shapeOptions } from '../Shape' +import Shape, { shapeOptions } from '../../Shape' import Orbit from './Orbit' const options = { @@ -49,7 +49,6 @@ export default class FractalSpirograph extends Shape { fractalSpirographNumCircles: 5, fractalSpirographRelativeSize: 3, fractalSpirographAlternateRotation: true, - repeatEnabled: false, } } } diff --git a/src/models/fractal_spirograph/Orbit.js b/src/models/shapes/fractal_spirograph/Orbit.js similarity index 100% rename from src/models/fractal_spirograph/Orbit.js rename to src/models/shapes/fractal_spirograph/Orbit.js diff --git a/src/models/input_text/Fonts.js b/src/models/shapes/input_text/Fonts.js similarity index 100% rename from src/models/input_text/Fonts.js rename to src/models/shapes/input_text/Fonts.js diff --git a/src/models/input_text/InputText.js b/src/models/shapes/input_text/InputText.js similarity index 98% rename from src/models/input_text/InputText.js rename to src/models/shapes/input_text/InputText.js index e6ba61a8..108b5f8f 100644 --- a/src/models/input_text/InputText.js +++ b/src/models/shapes/input_text/InputText.js @@ -1,7 +1,7 @@ import { CursiveFont, SansSerifFont, MonospaceFont } from './Fonts' import Victor from 'victor' -import Shape, { shapeOptions } from '../Shape' -import { arc } from '../../common/geometry' +import Shape, { shapeOptions } from '../../Shape' +import { arc } from '@/common/geometry' const options = { ...shapeOptions, @@ -47,7 +47,6 @@ export default class InputText extends Shape { inputText: 'Sandify', inputFont: 'Cursive', rotateDir: 'Center', - repeatEnabled: false } } } diff --git a/src/models/input_text/convert_letters.py b/src/models/shapes/input_text/convert_letters.py similarity index 100% rename from src/models/input_text/convert_letters.py rename to src/models/shapes/input_text/convert_letters.py diff --git a/src/models/input_text/raysol_cursive.js b/src/models/shapes/input_text/raysol_cursive.js similarity index 100% rename from src/models/input_text/raysol_cursive.js rename to src/models/shapes/input_text/raysol_cursive.js diff --git a/src/models/input_text/raysol_cursive.txt b/src/models/shapes/input_text/raysol_cursive.txt similarity index 100% rename from src/models/input_text/raysol_cursive.txt rename to src/models/shapes/input_text/raysol_cursive.txt diff --git a/src/models/input_text/raysol_sanserif.js b/src/models/shapes/input_text/raysol_sanserif.js similarity index 100% rename from src/models/input_text/raysol_sanserif.js rename to src/models/shapes/input_text/raysol_sanserif.js diff --git a/src/models/input_text/raysol_sanserif.txt b/src/models/shapes/input_text/raysol_sanserif.txt similarity index 100% rename from src/models/input_text/raysol_sanserif.txt rename to src/models/shapes/input_text/raysol_sanserif.txt diff --git a/src/models/lsystem/LSystem.js b/src/models/shapes/lsystem/LSystem.js similarity index 90% rename from src/models/lsystem/LSystem.js rename to src/models/shapes/lsystem/LSystem.js index 35df20fd..fe576558 100644 --- a/src/models/lsystem/LSystem.js +++ b/src/models/shapes/lsystem/LSystem.js @@ -1,13 +1,13 @@ -import Shape, { shapeOptions } from '../Shape' +import Shape, { shapeOptions } from '../../Shape' import { lsystem, lsystemPath, onSubtypeChange, onMinIterations, onMaxIterations -} from '../../common/lindenmayer' +} from '@/common/lindenmayer' import { subtypes } from './subtypes' -import { resizeVertices } from '../../common/geometry' +import { resizeVertices } from '@/common/geometry' const options = { ...shapeOptions, @@ -46,7 +46,6 @@ export default class LSystem extends Shape { type: 'lsystem', iterations: 3, subtype: 'McWorter\'s Pentadendrite', - repeatEnabled: false } } } diff --git a/src/models/lsystem/subtypes.js b/src/models/shapes/lsystem/subtypes.js similarity index 100% rename from src/models/lsystem/subtypes.js rename to src/models/shapes/lsystem/subtypes.js diff --git a/src/models/space_filler/SpaceFiller.js b/src/models/shapes/space_filler/SpaceFiller.js similarity index 91% rename from src/models/space_filler/SpaceFiller.js rename to src/models/shapes/space_filler/SpaceFiller.js index 127b7dcc..2188c4fa 100644 --- a/src/models/space_filler/SpaceFiller.js +++ b/src/models/shapes/space_filler/SpaceFiller.js @@ -1,12 +1,12 @@ -import Shape, { shapeOptions } from '../Shape' +import Shape, { shapeOptions } from '../../Shape' import { lsystem, lsystemPath, onSubtypeChange, onMinIterations, onMaxIterations -} from '../../common/lindenmayer' -import { resizeVertices } from '../../common/geometry' +} from '@/common/lindenmayer' +import { resizeVertices } from '@/common/geometry' import { subtypes } from './subtypes' const options = { @@ -45,10 +45,8 @@ export default class SpaceFiller extends Shape { ...{ type: 'space_filler', selectGroup: 'Erasers', - canTransform: false, iterations: 6, fillerSubtype: 'Hilbert', - repeatEnabled: false, canChangeSize: false, autosize: false, usesMachine: true, diff --git a/src/models/space_filler/subtypes.js b/src/models/shapes/space_filler/subtypes.js similarity index 100% rename from src/models/space_filler/subtypes.js rename to src/models/shapes/space_filler/subtypes.js diff --git a/src/models/tessellation_twist/TessellationTwist.js b/src/models/shapes/tessellation_twist/TessellationTwist.js similarity index 96% rename from src/models/tessellation_twist/TessellationTwist.js rename to src/models/shapes/tessellation_twist/TessellationTwist.js index 5189fb4c..556de8ec 100644 --- a/src/models/tessellation_twist/TessellationTwist.js +++ b/src/models/shapes/tessellation_twist/TessellationTwist.js @@ -1,8 +1,8 @@ import Victor from 'victor' -import Graph, { mix } from '../../common/Graph' -import { eulerianTrail } from '../../common/eulerianTrail' -import { difference } from '../../common/util' -import Shape, { shapeOptions } from '../Shape' +import Graph, { mix } from '@/common/Graph' +import { eulerianTrail } from '@/common/eulerianTrail' +import { difference } from '@/common/util' +import Shape, { shapeOptions } from '../../Shape' const vecTriangle = [ new Victor(-0.85, -0.4907477295), @@ -92,7 +92,6 @@ export default class TessellationTwist extends Shape { tessellationTwistNumSides: 5, tessellationTwistIterations: 2, tessellationTwistRotate: 0, - repeatEnabled: false, } } } diff --git a/src/models/v1_engineering/V1Engineering.js b/src/models/shapes/v1_engineering/V1Engineering.js similarity index 91% rename from src/models/v1_engineering/V1Engineering.js rename to src/models/shapes/v1_engineering/V1Engineering.js index d2c36c9e..205899ac 100644 --- a/src/models/v1_engineering/V1Engineering.js +++ b/src/models/shapes/v1_engineering/V1Engineering.js @@ -1,5 +1,5 @@ import Vicious1Vertices from './Vicious1Vertices' -import Shape from '../Shape' +import Shape from '../../Shape' export default class V1Engineering extends Shape { constructor() { diff --git a/src/models/v1_engineering/Vicious1Vertices.js b/src/models/shapes/v1_engineering/Vicious1Vertices.js similarity index 100% rename from src/models/v1_engineering/Vicious1Vertices.js rename to src/models/shapes/v1_engineering/Vicious1Vertices.js diff --git a/vite.config.js b/vite.config.js index d506e088..f863ab87 100644 --- a/vite.config.js +++ b/vite.config.js @@ -1,5 +1,6 @@ import fs from 'fs/promises'; import { defineConfig } from 'vite' +import path from 'path' import react from '@vitejs/plugin-react' import { NodeGlobalsPolyfillPlugin } from '@esbuild-plugins/node-globals-polyfill' import nodePolyfills from 'rollup-plugin-node-polyfills' @@ -16,7 +17,8 @@ export default defineConfig(() => ({ stream: "rollup-plugin-node-polyfills/polyfills/stream", events: "rollup-plugin-node-polyfills/polyfills/events", util: "rollup-plugin-node-polyfills/polyfills/util", - buffer: "rollup-plugin-node-polyfills/polyfills/buffer-es6" + buffer: "rollup-plugin-node-polyfills/polyfills/buffer-es6", + '@': path.resolve(__dirname, './src') } }, build: { From 5b7dbaa092ed5df6e361f6568e1b71423e37da7f Mon Sep 17 00:00:00 2001 From: Bob Carmichael Date: Sat, 15 Jul 2023 11:35:54 -0400 Subject: [PATCH 012/126] install prettier (code formatter) --- .eslintrc.js | 7 +- .prettierignore | 4 + .prettierrc.json | 3 + package-lock.json | 705 +++++++++++++++++++++++++++++++++++++++++++++- package.json | 3 + 5 files changed, 714 insertions(+), 8 deletions(-) create mode 100644 .prettierignore create mode 100644 .prettierrc.json diff --git a/.eslintrc.js b/.eslintrc.js index 08a05048..1942854e 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -20,7 +20,8 @@ module.exports = { "eslint:recommended", "plugin:react/recommended", "plugin:react-redux/recommended", - 'plugin:@typescript-eslint/recommended' + 'plugin:@typescript-eslint/recommended', + "plugin:prettier/recommended" ], rules: { // override/add rules settings here, such as: @@ -64,6 +65,7 @@ module.exports = { 'react/no-unused-prop-types': 'off', 'react-redux/no-unused-prop-types': 'warn', 'react-redux/prefer-separate-component-file': 'off', + "prettier/prettier": "error", semi: [ 'warn', 'never' ] }, settings: { @@ -106,6 +108,7 @@ module.exports = { plugins: [ "react", "react-redux", - "@typescript-eslint" + "@typescript-eslint", + "prettier" ] } diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000..98581ff0 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,4 @@ +node_modules +build +tmp +.eslintcache diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 00000000..ddcc4cad --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,3 @@ +{ + "semi": false, +} diff --git a/package-lock.json b/package-lock.json index 4f6730e4..11e4407a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -57,13 +57,16 @@ "@vitejs/plugin-react": "^1.0.7", "babel-jest": "^27.5.1", "eslint": "^8.13.0", + "eslint-config-prettier": "^8.8.0", "eslint-plugin-jest": "^26.1.4", + "eslint-plugin-prettier": "^5.0.0", "eslint-plugin-react": "^7.29.4", "eslint-plugin-react-redux": "^4.0.0", "gh-pages": "^3.2.3", "identity-obj-proxy": "^3.0.0", "jest": "^27.5.1", "jest-canvas-mock": "^2.3.1", + "prettier": "3.0.0", "rollup-plugin-node-polyfills": "^0.2.1", "vite": "^3.2.7" } @@ -2552,6 +2555,32 @@ "node": ">=10" } }, + "node_modules/@pkgr/utils": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@pkgr/utils/-/utils-2.4.2.tgz", + "integrity": "sha512-POgTXhjrTfbTV63DiFXav4lBHiICLKKwDeaKn9Nphwj7WH6m0hMMCaJkMyRWjgtPFyRKRVoMXXjczsTQRDEhYw==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "fast-glob": "^3.3.0", + "is-glob": "^4.0.3", + "open": "^9.1.0", + "picocolors": "^1.0.0", + "tslib": "^2.6.0" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, + "node_modules/@pkgr/utils/node_modules/tslib": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.0.tgz", + "integrity": "sha512-7At1WUettjcSRHXCyYtTselblcHl9PJFFVKiCAy/bY97+BPZXSQ2wbq0P9s8tK2G7dFQfNnlJnPAiArVBVBsfA==", + "dev": true + }, "node_modules/@popperjs/core": { "version": "2.11.5", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.5.tgz", @@ -3668,6 +3697,15 @@ } ] }, + "node_modules/big-integer": { + "version": "1.6.51", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz", + "integrity": "sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==", + "dev": true, + "engines": { + "node": ">=0.6" + } + }, "node_modules/binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -3676,6 +3714,18 @@ "node": ">=8" } }, + "node_modules/bplist-parser": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.2.0.tgz", + "integrity": "sha512-z0M+byMThzQmD9NILRniCUXYsYpjwnlO8N5uCFaCqIOpqRsJCrQL9NK3JsD67CN5a08nF5oIL2bD6loTdHOuKw==", + "dev": true, + "dependencies": { + "big-integer": "^1.6.44" + }, + "engines": { + "node": ">= 5.10.0" + } + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -3768,6 +3818,21 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true }, + "node_modules/bundle-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-3.0.0.tgz", + "integrity": "sha512-PKA4BeSvBpQKQ8iPOGCSiell+N8P+Tf1DlwqmYhpe2gAhKPHn8EYOxVT+ShuGmhg8lN8XiSlS80yiExKXrURlw==", + "dev": true, + "dependencies": { + "run-applescript": "^5.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/cacache": { "version": "15.3.0", "resolved": "https://registry.npmjs.org/cacache/-/cacache-15.3.0.tgz", @@ -4711,6 +4776,162 @@ "node": ">=0.10.0" } }, + "node_modules/default-browser": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-4.0.0.tgz", + "integrity": "sha512-wX5pXO1+BrhMkSbROFsyxUm0i/cJEScyNhA4PPxc41ICuv05ZZB/MX28s8aZx6xjmatvebIapF6hLEKEcpneUA==", + "dev": true, + "dependencies": { + "bundle-name": "^3.0.0", + "default-browser-id": "^3.0.0", + "execa": "^7.1.1", + "titleize": "^3.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-3.0.0.tgz", + "integrity": "sha512-OZ1y3y0SqSICtE8DE4S8YOE9UZOJ8wO16fKWVP5J1Qz42kV9jcnMVFrEE/noXb/ss3Q4pZIH79kxofzyNNtUNA==", + "dev": true, + "dependencies": { + "bplist-parser": "^0.2.0", + "untildify": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser/node_modules/execa": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-7.1.1.tgz", + "integrity": "sha512-wH0eMf/UXckdUYnO21+HDztteVv05rq2GXksxT4fCGeHkBhw1DROXh40wcjMcRqDOWE7iPJ4n3M7e2+YFP+76Q==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.1", + "human-signals": "^4.3.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^3.0.7", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": "^14.18.0 || ^16.14.0 || >=18.0.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/default-browser/node_modules/human-signals": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-4.3.1.tgz", + "integrity": "sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==", + "dev": true, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/default-browser/node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser/node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser/node_modules/npm-run-path": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.1.0.tgz", + "integrity": "sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==", + "dev": true, + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser/node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser/node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/define-properties": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", @@ -5497,6 +5718,18 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint-config-prettier": { + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.8.0.tgz", + "integrity": "sha512-wLbQiFre3tdGgpDv67NQKnJuTlcUVYHas3k+DZCc2U2BadthoEY4B7hLPvAxaqdyOGCzuLfii2fqGph10va7oA==", + "dev": true, + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, "node_modules/eslint-plugin-jest": { "version": "26.1.4", "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-26.1.4.tgz", @@ -5521,6 +5754,35 @@ } } }, + "node_modules/eslint-plugin-prettier": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.0.0.tgz", + "integrity": "sha512-AgaZCVuYDXHUGxj/ZGu1u8H8CYgDY3iG6w5kUFw4AzMVXzB7VvbKgYR4nATIN+OvUrghMbiDLeimVjVY5ilq3w==", + "dev": true, + "dependencies": { + "prettier-linter-helpers": "^1.0.0", + "synckit": "^0.8.5" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/prettier" + }, + "peerDependencies": { + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { + "optional": true + } + } + }, "node_modules/eslint-plugin-react": { "version": "7.29.4", "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.29.4.tgz", @@ -5813,10 +6075,16 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true + }, "node_modules/fast-glob": { - "version": "3.2.11", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz", - "integrity": "sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.0.tgz", + "integrity": "sha512-ChDuvbOypPuNjO8yIDf36x7BlZX1smcUMTTcyoIjycexOxd6DFsKsg21qVBzEmr3G7fUKIRy2/psii+CIUt7FA==", "dev": true, "dependencies": { "@nodelib/fs.stat": "^2.0.2", @@ -6741,6 +7009,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "dev": true, + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -6777,6 +7060,24 @@ "node": ">=0.10.0" } }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "dev": true, + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-lambda": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", @@ -6919,6 +7220,33 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-wsl/node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true, + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", @@ -8822,6 +9150,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/open": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/open/-/open-9.1.0.tgz", + "integrity": "sha512-OS+QTnw1/4vrf+9hh1jc1jnYjzSG4ttTBB8UxOwAnInG3Uo4ssetzC1ihqaIHjLJnA5GGlRl6QlZXOTQhRBUvg==", + "dev": true, + "dependencies": { + "default-browser": "^4.0.0", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/opentype.js": { "version": "1.3.4", "resolved": "https://registry.npmjs.org/opentype.js/-/opentype.js-1.3.4.tgz", @@ -9088,6 +9434,33 @@ "node": ">= 0.8.0" } }, + "node_modules/prettier": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.0.tgz", + "integrity": "sha512-zBf5eHpwHOGPC47h0zrPyNn+eAEIdEzfywMoYn2XPi0P44Zp0tSq64rq0xAREh4auw2cJZHo9QUob+NqCQky4g==", + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/pretty-format": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", @@ -9978,6 +10351,21 @@ "integrity": "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==", "dev": true }, + "node_modules/run-applescript": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-5.0.0.tgz", + "integrity": "sha512-XcT5rBksx1QdIhlFOCtgZkB99ZEouFZ1E2Kc2LHqNW13U3/74YGdkQRmThTwxy4QIyookibDKYZOPqX//6BlAg==", + "dev": true, + "dependencies": { + "execa": "^5.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -10659,6 +11047,28 @@ "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", "dev": true }, + "node_modules/synckit": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.5.tgz", + "integrity": "sha512-L1dapNV6vu2s/4Sputv8xGsCdAVlb5nRDMFU/E27D44l5U6cw1g0dGd45uLc+OXjNMmF4ntiMdCimzcjFKQI8Q==", + "dev": true, + "dependencies": { + "@pkgr/utils": "^2.3.1", + "tslib": "^2.5.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, + "node_modules/synckit/node_modules/tslib": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.0.tgz", + "integrity": "sha512-7At1WUettjcSRHXCyYtTselblcHl9PJFFVKiCAy/bY97+BPZXSQ2wbq0P9s8tK2G7dFQfNnlJnPAiArVBVBsfA==", + "dev": true + }, "node_modules/tar": { "version": "6.1.11", "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.11.tgz", @@ -10727,6 +11137,18 @@ "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz", "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==" }, + "node_modules/titleize": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/titleize/-/titleize-3.0.0.tgz", + "integrity": "sha512-KxVu8EYHDPBdUYdKZdKtU2aj2XfEx9AfjXxE/Aj0vT06w2icA09Vus1rh6eSu1y01akYg6BjIK/hxyLJINoMLQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -11001,6 +11423,15 @@ "node": ">= 4.0.0" } }, + "node_modules/untildify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", + "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/uri-js": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", @@ -13185,6 +13616,28 @@ "rimraf": "^3.0.2" } }, + "@pkgr/utils": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@pkgr/utils/-/utils-2.4.2.tgz", + "integrity": "sha512-POgTXhjrTfbTV63DiFXav4lBHiICLKKwDeaKn9Nphwj7WH6m0hMMCaJkMyRWjgtPFyRKRVoMXXjczsTQRDEhYw==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.3", + "fast-glob": "^3.3.0", + "is-glob": "^4.0.3", + "open": "^9.1.0", + "picocolors": "^1.0.0", + "tslib": "^2.6.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.0.tgz", + "integrity": "sha512-7At1WUettjcSRHXCyYtTselblcHl9PJFFVKiCAy/bY97+BPZXSQ2wbq0P9s8tK2G7dFQfNnlJnPAiArVBVBsfA==", + "dev": true + } + } + }, "@popperjs/core": { "version": "2.11.5", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.5.tgz", @@ -14010,11 +14463,26 @@ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" }, + "big-integer": { + "version": "1.6.51", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz", + "integrity": "sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==", + "dev": true + }, "binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==" }, + "bplist-parser": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.2.0.tgz", + "integrity": "sha512-z0M+byMThzQmD9NILRniCUXYsYpjwnlO8N5uCFaCqIOpqRsJCrQL9NK3JsD67CN5a08nF5oIL2bD6loTdHOuKw==", + "dev": true, + "requires": { + "big-integer": "^1.6.44" + } + }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -14074,6 +14542,15 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true }, + "bundle-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-3.0.0.tgz", + "integrity": "sha512-PKA4BeSvBpQKQ8iPOGCSiell+N8P+Tf1DlwqmYhpe2gAhKPHn8EYOxVT+ShuGmhg8lN8XiSlS80yiExKXrURlw==", + "dev": true, + "requires": { + "run-applescript": "^5.0.0" + } + }, "cacache": { "version": "15.3.0", "resolved": "https://registry.npmjs.org/cacache/-/cacache-15.3.0.tgz", @@ -14778,6 +15255,101 @@ "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==", "dev": true }, + "default-browser": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-4.0.0.tgz", + "integrity": "sha512-wX5pXO1+BrhMkSbROFsyxUm0i/cJEScyNhA4PPxc41ICuv05ZZB/MX28s8aZx6xjmatvebIapF6hLEKEcpneUA==", + "dev": true, + "requires": { + "bundle-name": "^3.0.0", + "default-browser-id": "^3.0.0", + "execa": "^7.1.1", + "titleize": "^3.0.0" + }, + "dependencies": { + "execa": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-7.1.1.tgz", + "integrity": "sha512-wH0eMf/UXckdUYnO21+HDztteVv05rq2GXksxT4fCGeHkBhw1DROXh40wcjMcRqDOWE7iPJ4n3M7e2+YFP+76Q==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.1", + "human-signals": "^4.3.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^3.0.7", + "strip-final-newline": "^3.0.0" + } + }, + "human-signals": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-4.3.1.tgz", + "integrity": "sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==", + "dev": true + }, + "is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true + }, + "mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true + }, + "npm-run-path": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.1.0.tgz", + "integrity": "sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==", + "dev": true, + "requires": { + "path-key": "^4.0.0" + } + }, + "onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "requires": { + "mimic-fn": "^4.0.0" + } + }, + "path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true + }, + "strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true + } + } + }, + "default-browser-id": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-3.0.0.tgz", + "integrity": "sha512-OZ1y3y0SqSICtE8DE4S8YOE9UZOJ8wO16fKWVP5J1Qz42kV9jcnMVFrEE/noXb/ss3Q4pZIH79kxofzyNNtUNA==", + "dev": true, + "requires": { + "bplist-parser": "^0.2.0", + "untildify": "^4.0.0" + } + }, + "define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "dev": true + }, "define-properties": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", @@ -15305,6 +15877,13 @@ } } }, + "eslint-config-prettier": { + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.8.0.tgz", + "integrity": "sha512-wLbQiFre3tdGgpDv67NQKnJuTlcUVYHas3k+DZCc2U2BadthoEY4B7hLPvAxaqdyOGCzuLfii2fqGph10va7oA==", + "dev": true, + "requires": {} + }, "eslint-plugin-jest": { "version": "26.1.4", "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-26.1.4.tgz", @@ -15314,6 +15893,16 @@ "@typescript-eslint/utils": "^5.10.0" } }, + "eslint-plugin-prettier": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.0.0.tgz", + "integrity": "sha512-AgaZCVuYDXHUGxj/ZGu1u8H8CYgDY3iG6w5kUFw4AzMVXzB7VvbKgYR4nATIN+OvUrghMbiDLeimVjVY5ilq3w==", + "dev": true, + "requires": { + "prettier-linter-helpers": "^1.0.0", + "synckit": "^0.8.5" + } + }, "eslint-plugin-react": { "version": "7.29.4", "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.29.4.tgz", @@ -15506,10 +16095,16 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true }, + "fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true + }, "fast-glob": { - "version": "3.2.11", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz", - "integrity": "sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.0.tgz", + "integrity": "sha512-ChDuvbOypPuNjO8yIDf36x7BlZX1smcUMTTcyoIjycexOxd6DFsKsg21qVBzEmr3G7fUKIRy2/psii+CIUt7FA==", "dev": true, "requires": { "@nodelib/fs.stat": "^2.0.2", @@ -16180,6 +16775,12 @@ "has-tostringtag": "^1.0.0" } }, + "is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "dev": true + }, "is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -16204,6 +16805,15 @@ "is-extglob": "^2.1.1" } }, + "is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "dev": true, + "requires": { + "is-docker": "^3.0.0" + } + }, "is-lambda": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", @@ -16298,6 +16908,23 @@ "call-bind": "^1.0.2" } }, + "is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "requires": { + "is-docker": "^2.0.0" + }, + "dependencies": { + "is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true + } + } + }, "isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", @@ -17757,6 +18384,18 @@ "mimic-fn": "^2.1.0" } }, + "open": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/open/-/open-9.1.0.tgz", + "integrity": "sha512-OS+QTnw1/4vrf+9hh1jc1jnYjzSG4ttTBB8UxOwAnInG3Uo4ssetzC1ihqaIHjLJnA5GGlRl6QlZXOTQhRBUvg==", + "dev": true, + "requires": { + "default-browser": "^4.0.0", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "is-wsl": "^2.2.0" + } + }, "opentype.js": { "version": "1.3.4", "resolved": "https://registry.npmjs.org/opentype.js/-/opentype.js-1.3.4.tgz", @@ -17941,6 +18580,21 @@ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true }, + "prettier": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.0.tgz", + "integrity": "sha512-zBf5eHpwHOGPC47h0zrPyNn+eAEIdEzfywMoYn2XPi0P44Zp0tSq64rq0xAREh4auw2cJZHo9QUob+NqCQky4g==", + "dev": true + }, + "prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "requires": { + "fast-diff": "^1.1.2" + } + }, "pretty-format": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", @@ -18607,6 +19261,15 @@ } } }, + "run-applescript": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-5.0.0.tgz", + "integrity": "sha512-XcT5rBksx1QdIhlFOCtgZkB99ZEouFZ1E2Kc2LHqNW13U3/74YGdkQRmThTwxy4QIyookibDKYZOPqX//6BlAg==", + "dev": true, + "requires": { + "execa": "^5.0.0" + } + }, "run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -19129,6 +19792,24 @@ "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", "dev": true }, + "synckit": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.5.tgz", + "integrity": "sha512-L1dapNV6vu2s/4Sputv8xGsCdAVlb5nRDMFU/E27D44l5U6cw1g0dGd45uLc+OXjNMmF4ntiMdCimzcjFKQI8Q==", + "dev": true, + "requires": { + "@pkgr/utils": "^2.3.1", + "tslib": "^2.5.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.0.tgz", + "integrity": "sha512-7At1WUettjcSRHXCyYtTselblcHl9PJFFVKiCAy/bY97+BPZXSQ2wbq0P9s8tK2G7dFQfNnlJnPAiArVBVBsfA==", + "dev": true + } + } + }, "tar": { "version": "6.1.11", "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.11.tgz", @@ -19185,6 +19866,12 @@ "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz", "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==" }, + "titleize": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/titleize/-/titleize-3.0.0.tgz", + "integrity": "sha512-KxVu8EYHDPBdUYdKZdKtU2aj2XfEx9AfjXxE/Aj0vT06w2icA09Vus1rh6eSu1y01akYg6BjIK/hxyLJINoMLQ==", + "dev": true + }, "tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -19390,6 +20077,12 @@ "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", "dev": true }, + "untildify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", + "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==", + "dev": true + }, "uri-js": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", diff --git a/package.json b/package.json index 03d88755..6d434a27 100644 --- a/package.json +++ b/package.json @@ -53,13 +53,16 @@ "@vitejs/plugin-react": "^1.0.7", "babel-jest": "^27.5.1", "eslint": "^8.13.0", + "eslint-config-prettier": "^8.8.0", "eslint-plugin-jest": "^26.1.4", + "eslint-plugin-prettier": "^5.0.0", "eslint-plugin-react": "^7.29.4", "eslint-plugin-react-redux": "^4.0.0", "gh-pages": "^3.2.3", "identity-obj-proxy": "^3.0.0", "jest": "^27.5.1", "jest-canvas-mock": "^2.3.1", + "prettier": "3.0.0", "rollup-plugin-node-polyfills": "^0.2.1", "vite": "^3.2.7" }, From 187d9f2579968601e98c6303325288f1b0995687 Mon Sep 17 00:00:00 2001 From: Bob Carmichael Date: Sat, 15 Jul 2023 11:45:44 -0400 Subject: [PATCH 013/126] Remove new layer dialog from redux layersSlice --- .prettierrc.json | 2 +- src/config/models.js | 73 ++++--- src/features/app/store.js | 64 +++--- src/features/exporter/CommentExporter.js | 51 ++--- src/features/layers/Layer.js | 186 ++++++++++------- src/features/layers/NewLayer.js | 184 +++++++++-------- src/features/layers/Playlist.js | 85 ++++---- src/features/layers/layersSlice.js | 86 ++++---- src/features/machine/computer.js | 84 ++++---- src/features/machine/selectors.js | 248 ++++++++++++----------- 10 files changed, 582 insertions(+), 481 deletions(-) diff --git a/.prettierrc.json b/.prettierrc.json index ddcc4cad..cce9d3c0 100644 --- a/.prettierrc.json +++ b/.prettierrc.json @@ -1,3 +1,3 @@ { - "semi": false, + "semi": false } diff --git a/src/config/models.js b/src/config/models.js index 73168aa7..91105edc 100644 --- a/src/config/models.js +++ b/src/config/models.js @@ -1,33 +1,32 @@ -import Circle from '@/models/shapes/Circle' -import CirclePacker from '@/models/shapes/circle_packer/CirclePacker' -import Epicycloid from '@/models/shapes/Epicycloid' -import FancyText from '@/models/shapes/FancyText' -import FileImport from '@/models/shapes/FileImport' -import Fisheye from '@/models/shapes/Fisheye' -import FractalSpirograph from '@/models/shapes/fractal_spirograph/FractalSpirograph' -// import Freeform from '../models/shapes/Freeform' -import Heart from '@/models/shapes/Heart' -import Hypocycloid from '@/models/shapes/Hypocycloid' -import InputText from '@/models/shapes/input_text/InputText' -import LSystem from '@/models/shapes/lsystem/LSystem' -import NoiseWave from '@/models/shapes/NoiseWave' -import Polygon from '@/models/shapes/Polygon' -import Point from '@/models/shapes/Point' -import Reuleaux from '@/models/shapes/Reuleaux' -import Rose from '@/models/shapes/Rose' -import SpaceFiller from '@/models/shapes/space_filler/SpaceFiller' -import Star from '@/models/shapes/Star' -import TessellationTwist from '@/models/shapes/tessellation_twist/TessellationTwist' -import V1Engineering from '@/models/shapes/v1_engineering/V1Engineering' - -import FineTuning from '../models/effects/FineTuning' -import Loop from '@/models/effects/Loop' -import Mask from '@/models/effects/Mask' -import Noise from '@/models/effects/Noise' -import Track from '@/models/effects/Track' -import Warp from '@/models/effects/Warp' -import Wiper from '@/models/shapes/Wiper' +import Circle from "@/models/shapes/Circle" +import CirclePacker from "@/models/shapes/circle_packer/CirclePacker" +import Epicycloid from "@/models/shapes/Epicycloid" +import FancyText from "@/models/shapes/FancyText" +import FileImport from "@/models/shapes/FileImport" +import Fisheye from "@/models/shapes/Fisheye" +import FractalSpirograph from "@/models/shapes/fractal_spirograph/FractalSpirograph" +// import Freeform from "../models/shapes/Freeform" +import Heart from "@/models/shapes/Heart" +import Hypocycloid from "@/models/shapes/Hypocycloid" +import InputText from "@/models/shapes/input_text/InputText" +import LSystem from "@/models/shapes/lsystem/LSystem" +import NoiseWave from "@/models/shapes/NoiseWave" +import Polygon from "@/models/shapes/Polygon" +import Point from "@/models/shapes/Point" +import Reuleaux from "@/models/shapes/Reuleaux" +import Rose from "@/models/shapes/Rose" +import SpaceFiller from "@/models/shapes/space_filler/SpaceFiller" +import Star from "@/models/shapes/Star" +import TessellationTwist from "@/models/shapes/tessellation_twist/TessellationTwist" +import V1Engineering from "@/models/shapes/v1_engineering/V1Engineering" +import FineTuning from "../models/effects/FineTuning" +import Loop from "@/models/effects/Loop" +import Mask from "@/models/effects/Mask" +import Noise from "@/models/effects/Noise" +import Track from "@/models/effects/Track" +import Warp from "@/models/effects/Warp" +import Wiper from "@/models/shapes/Wiper" /*---------------------------------------------- Supported input shapes @@ -60,15 +59,15 @@ export const registeredModels = { mask: new Mask(), noise: new Noise(), warp: new Warp(), - fineTuning: new FineTuning() + fineTuning: new FineTuning(), } -export const getModel = (layer) => { - return registeredModels[layer.type] +export const getModel = (type) => { + return registeredModels[type] } export const getModelDefaults = () => { - return Object.keys(registeredModels).map(id => { + return Object.keys(registeredModels).map((id) => { const state = registeredModels[id].getInitialState() state.name = registeredModels[id].name state.id = id @@ -91,17 +90,17 @@ export const getModelSelectOptions = () => { } } if (!found) { - if (shape.selectGroup === 'import') { + if (shape.selectGroup === "import") { // users can't manually select this group continue - } else if (shape.selectGroup === 'effects') { + } else if (shape.selectGroup === "effects") { // effects are added separately // TODO: when effects can be added separately, uncomment the next line // continue } - const newOptions = [ optionLabel ] - groupOptions.push( { label: shape.selectGroup, options: newOptions } ) + const newOptions = [optionLabel] + groupOptions.push({ label: shape.selectGroup, options: newOptions }) } } diff --git a/src/features/app/store.js b/src/features/app/store.js index 61cf2f10..d96ce9ed 100644 --- a/src/features/app/store.js +++ b/src/features/app/store.js @@ -1,15 +1,20 @@ import { configureStore } from "@reduxjs/toolkit" -import { combineReducers } from 'redux' -import uniqueId from 'lodash/uniqueId' +import { combineReducers } from "redux" +import uniqueId from "lodash/uniqueId" -import appReducer from './appSlice' -import machineReducer from '../machine/machineSlice' -import exporterReducer from '../exporter/exporterSlice' -import previewReducer from '../preview/previewSlice' -import fontsReducer from '../fonts/fontsSlice' -import { registeredModels } from '../../config/models' -import { loadState, saveState } from '../../common/localStorage' -import layersReducer, { setCurrentLayer, addLayer, addEffect, updateLayer } from '../layers/layersSlice' +import appReducer from "./appSlice" +import machineReducer from "../machine/machineSlice" +import exporterReducer from "../exporter/exporterSlice" +import previewReducer from "../preview/previewSlice" +import fontsReducer from "../fonts/fontsSlice" +import { registeredModels, getModel } from "../../config/models" +import { loadState, saveState } from "../../common/localStorage" +import layersReducer, { + setCurrentLayer, + addLayer, + addEffect, + updateLayer, +} from "../layers/layersSlice" //const customizedMiddleware = getDefaultMiddleware({ // immutableCheck: { @@ -27,36 +32,36 @@ const store = configureStore({ layers: layersReducer, exporter: exporterReducer, machine: machineReducer, - preview: previewReducer + preview: previewReducer, }), - fonts: fontsReducer - }) + fonts: fontsReducer, + }), }) const loadPersistedLayers = (layers) => { - layers.allIds.forEach(id => { + layers.allIds.forEach((id) => { const layer = layers.byId[id] if (layer) { const newLayer = { ...layer, - id: uniqueId('layer-'), + id: uniqueId("layer-"), restore: true, startingWidth: layer.startingWidth || layer.startingSize, startingHeight: layer.startingWidth || layer.startingSize, - autosize: layer.autosize === null ? true : layer.autosize + autosize: layer.autosize === null ? true : layer.autosize, } // for referential integrity, we have to explicitly generate ids and // re-build relationships. store.dispatch(addLayer(newLayer)) if (layer.effectIds) { - newLayer.effectIds = layer.effectIds.map(effectId => { + newLayer.effectIds = layer.effectIds.map((effectId) => { const effect = { ...layers.byId[effectId], - id: uniqueId('layer-'), + id: uniqueId("layer-"), restore: true, - parentId: newLayer.id + parentId: newLayer.id, } store.dispatch(addEffect(effect)) return effect.id @@ -68,14 +73,21 @@ const loadPersistedLayers = (layers) => { } const loadDefaultLayer = () => { - const storedShape = localStorage.getItem('currentShape') - const currentShape = storedShape && registeredModels[storedShape] ? storedShape : 'polygon' - const layer = registeredModels[currentShape].getInitialState() + const storedShape = localStorage.getItem("currentShape") + const currentShape = + storedShape && registeredModels[storedShape] ? storedShape : "polygon" + const currentName = getModel(currentShape).name.toLowerCase() + const layer = { + ...registeredModels[currentShape].getInitialState(), + name: currentName, + } store.dispatch(addLayer(layer)) const state = store.getState() - store.dispatch(setCurrentLayer(state.main.layers.byId[state.main.layers.allIds[0]].id)) + store.dispatch( + setCurrentLayer(state.main.layers.byId[state.main.layers.allIds[0]].id), + ) } // set both to true when running locally if you want to preserve your shape @@ -86,10 +98,10 @@ const persistState = false // if you want to save a multiple temporary states, use these keys. The first time // you save a new state, change persistSaveKey. Make a change, then change // persistInitKey to the same value. It's like doing a "save as" -const persistInitKey = 'state' -const persistSaveKey = 'state' +const persistInitKey = "state" +const persistSaveKey = "state" -if (typeof jest === 'undefined' && usePersistedState) { +if (typeof jest === "undefined" && usePersistedState) { // override default values with saved ones const persistedState = loadState(persistInitKey) diff --git a/src/features/exporter/CommentExporter.js b/src/features/exporter/CommentExporter.js index 6f10f936..ece5b952 100644 --- a/src/features/exporter/CommentExporter.js +++ b/src/features/exporter/CommentExporter.js @@ -1,55 +1,58 @@ -import { getModel } from '../../config/models' +import { getModel } from "../../config/models" //import Machine from '../../models/Machine' -import Exporter from './Exporter' +import Exporter from "./Exporter" export default class CommentExporter extends Exporter { constructor(props) { super(props) this.indentLevel = 0 this.startComments() - this.commentChar = '' + this.commentChar = "" } export() { const state = this.props -// TODO: fix -// const machine = new Machine() + // TODO: fix + // const machine = new Machine() let instance = state.machine - this.line('Created by Sandify') - this.line('https://sandify.org') - this.keyValueLine('Version', state.app.sandifyVersion) + this.line("Created by Sandify") + this.line("https://sandify.org") + this.keyValueLine("Version", state.app.sandifyVersion) this.line() - this.keyValueLine('Machine type', state.machine.rectangular ? 'Rectangular' : 'Polar') + this.keyValueLine( + "Machine type", + state.machine.rectangular ? "Rectangular" : "Polar", + ) this.indent() // TODO: fix -// this.optionLines(machine, instance, ['minX', 'maxX', 'minY', 'maxY'], state.machine.rectangular) -// this.optionLines(machine, instance, ['maxRadius', 'polarStartPoint', 'polarEndPoint'], !state.machine.rectangular) + // this.optionLines(machine, instance, ['minX', 'maxX', 'minY', 'maxY'], state.machine.rectangular) + // this.optionLines(machine, instance, ['maxRadius', 'polarStartPoint', 'polarEndPoint'], !state.machine.rectangular) this.dedent() - this.keyValueLine('Content type', state.app.input) + this.keyValueLine("Content type", state.app.input) const layers = state.layers switch (state.app.input) { - case 'shape': // shapes - layers.forEach(layer => { - const shape = getModel(layer) + case "shape": // shapes + layers.forEach((layer) => { + const shape = getModel(layer.type) const options = shape.getOptions() - this.line('Layer:') + this.line("Layer:") this.indent() - this.keyValueLine('Shape', shape.name) + this.keyValueLine("Shape", shape.name) this.optionLines(shape, layer, Object.keys(options)) - this.keyValueLine('Visible', layer.visible) -// TODO: fix -// this.optionLines(transform, layer, ['startingWidth', 'startingHeight', 'offsetX', 'offsetY', 'rotation', 'reverse']) + this.keyValueLine("Visible", layer.visible) + // TODO: fix + // this.optionLines(transform, layer, ['startingWidth', 'startingHeight', 'offsetX', 'offsetY', 'rotation', 'reverse']) if (!layer.effect) { - this.line('Fine tuning:') + this.line("Fine tuning:") this.indent() -// TODO: fix -// this.optionLines(transform, layer, ['connectionMethod']) + // TODO: fix + // this.optionLines(transform, layer, ['connectionMethod']) this.dedent() } this.dedent() @@ -61,7 +64,7 @@ export default class CommentExporter extends Exporter { } this.dedent() - this.keyValueLine('Reverse export path', state.exporter.reverse) + this.keyValueLine("Reverse export path", state.exporter.reverse) return this.lines } } diff --git a/src/features/layers/Layer.js b/src/features/layers/Layer.js index 49b14839..fbb3b856 100644 --- a/src/features/layers/Layer.js +++ b/src/features/layers/Layer.js @@ -1,29 +1,33 @@ -import { connect } from 'react-redux' -import React, { Component } from 'react' -import { Button, Card, Row, Col } from 'react-bootstrap' -import Select from 'react-select' -import CommentsBox from '../../components/CommentsBox' -import InputOption from '../../components/InputOption' -import DropdownOption from '../../components/DropdownOption' -import CheckboxOption from '../../components/CheckboxOption' -import ToggleButtonOption from '../../components/ToggleButtonOption' -import { updateLayer, setShapeType, restoreDefaults } from '../layers/layersSlice' -import { getCurrentLayer } from './selectors' -import { getModel, getModelSelectOptions } from '../../config/models' -import './Layer.scss' +import { connect } from "react-redux" +import React, { Component } from "react" +import { Button, Card, Row, Col } from "react-bootstrap" +import Select from "react-select" +import CommentsBox from "../../components/CommentsBox" +import InputOption from "../../components/InputOption" +import DropdownOption from "../../components/DropdownOption" +import CheckboxOption from "../../components/CheckboxOption" +import ToggleButtonOption from "../../components/ToggleButtonOption" +import { + updateLayer, + setShapeType, + restoreDefaults, +} from "../layers/layersSlice" +import { getCurrentLayer } from "./selectors" +import { getModel, getModelSelectOptions } from "../../config/models" +import "./Layer.scss" const mapStateToProps = (state, ownProps) => { const layer = getCurrentLayer(state) - const shape = getModel(layer) + const shape = getModel(layer.type) return { layer: layer, shape: shape, options: shape.getOptions(), selectOptions: getModelSelectOptions(false), - showShapeSelectRender: layer.selectGroup !== 'import' && !layer.effect, + showShapeSelectRender: layer.selectGroup !== "import" && !layer.effect, link: shape.link, - linkText: shape.linkText + linkText: shape.linkText, } } @@ -36,69 +40,90 @@ const mapDispatchToProps = (dispatch, ownProps) => { dispatch(updateLayer(attrs)) }, onChangeType: (selected) => { - dispatch(setShapeType({id: id, type: selected.value})) + dispatch(setShapeType({ id: id, type: selected.value })) }, onRestoreDefaults: (event) => { dispatch(restoreDefaults(id)) - } + }, } } class Layer extends Component { render() { - const selectedOption = { value: this.props.shape.id, label: this.props.shape.name } + const selectedOption = { + value: this.props.shape.id, + label: this.props.shape.name, + } const optionsRender = Object.keys(this.props.options).map((key, index) => { return this.getOptionComponent(key, index) }) const linkText = this.props.linkText || this.props.link - const linkRender = this.props.link ?

See {linkText} for ideas.

: undefined + const linkRender = this.props.link ? ( + + + +

+ See{" "} + + {linkText} + {" "} + for ideas. +

+ +
+ ) : undefined let optionsListRender = undefined if (Object.entries(this.props.options).length > 0) { - optionsListRender = -
- {optionsRender} -
+ optionsListRender =
{optionsRender}
} let shapeSelectRender = undefined if (this.props.showShapeSelectRender) { - shapeSelectRender = + shapeSelectRender = ( - - Shape - + Shape - - - - - Name - - - - - - + + + Type + + { const value = choice.value let attrs = {} attrs[this.props.optionKey] = value if (option.onChange !== undefined) { - attrs = option.onChange(attrs, model) + attrs = option.onChange(object, attrs, data) } this.props.onChange(attrs) }} options={choices} - /> + /> ) diff --git a/src/components/InputOption.js b/src/components/InputOption.js index a784d64b..c973a36c 100644 --- a/src/components/InputOption.js +++ b/src/components/InputOption.js @@ -1,15 +1,11 @@ -import React, { Component } from 'react' -import { - Col, - Form, - Row -} from 'react-bootstrap' -import debounce from 'lodash/debounce' +import React, { Component } from "react" +import { Col, Form, Row } from "react-bootstrap" +import debounce from "lodash/debounce" class InputOption extends Component { constructor(props) { super(props) - this.delayedSet = debounce( (value, key, onChange) => { + this.delayedSet = debounce((value, key, onChange) => { let attrs = {} attrs[key] = value onChange(attrs) @@ -17,51 +13,89 @@ class InputOption extends Component { } render() { - const option = this.props.options[this.props.optionKey] - const model = this.props.model - const optionType = option.type || 'number' - const minimum = (typeof option.min === 'function') ? option.min(model) : parseFloat(option.min) - const maximum = (typeof option.max === 'function') ? option.max(model) : parseFloat(option.max) - const visible = option.isVisible === undefined ? true : option.isVisible(model) + const { + data, + options, + optionKey, + onChange, + delayKey, + label = true, + } = this.props + const option = options[optionKey] + const object = this.props.object || data + const optionType = option.type || "number" + const minimum = + typeof option.min === "function" + ? option.min(data) + : parseFloat(option.min) + const maximum = + typeof option.max === "function" + ? option.max(data) + : parseFloat(option.max) + const visible = + option.isVisible === undefined ? true : option.isVisible(object, data) - return ( - - - - {option.title} - - + const renderedInput = ( + { + let attrs = {} + let value = event.target.value - - { - let attrs = {} - let value = event.target.value + if (optionType === "number") { + value = value === "" ? "" : parseFloat(value) + } - if (optionType === 'number') { - value = value === '' ? '' : parseFloat(value) - } + attrs[optionKey] = value - attrs[this.props.optionKey] = value - if (option.onChange !== undefined) { - attrs = option.onChange(attrs, model) - } - this.props.onChange(attrs) - if (this.props.delayKey !== undefined) { - this.delayedSet(value, this.props.delayKey, this.props.onChange) - } - }} - /> - - + if (option.onChange !== undefined) { + attrs = option.onChange(object, attrs, data) + } + onChange(attrs) + + if (delayKey !== undefined) { + this.delayedSet(value, delayKey, onChange) + } + }} + /> ) + + if (!option.inline) { + return ( + + + {label && ( + + {option.title} + + )} + + {renderedInput} + + ) + } else { + return ( +
+ {label && ( + + {option.title} + + )} + {renderedInput} +
+ ) + } } } diff --git a/src/components/ToggleButtonOption.js b/src/components/ToggleButtonOption.js index ec0db932..c4df3e33 100644 --- a/src/components/ToggleButtonOption.js +++ b/src/components/ToggleButtonOption.js @@ -1,43 +1,49 @@ -import React, { Component } from 'react' +import React, { Component } from "react" import { Col, Form, Row, ToggleButton, - ToggleButtonGroup -} from 'react-bootstrap' + ToggleButtonGroup, +} from "react-bootstrap" class ToggleButtonOption extends Component { render() { const option = this.props.options[this.props.optionKey] - const model = this.props.model - const currentChoice = model[this.props.optionKey] - const visible = option.isVisible === undefined ? true : option.isVisible(model) + const { data } = this.props + const object = this.props.object || data + const currentChoice = data[this.props.optionKey] + const visible = + option.isVisible === undefined ? true : option.isVisible(object, data) return ( - + - - {option.title} - + {option.title} { - let attrs = {} - attrs[this.props.optionKey] = choice - this.props.onChange(attrs) - }}> + type="radio" + name={this.props.optionKey} + value={currentChoice} + key={this.props.optionKey} + onChange={(choice) => { + let attrs = {} + attrs[this.props.optionKey] = choice + this.props.onChange(attrs) + }} + > {option.choices.map((choice) => { - return {choice} + return ( + + {choice} + + ) })} diff --git a/src/config/models.js b/src/config/models.js index c1697665..6818c842 100644 --- a/src/config/models.js +++ b/src/config/models.js @@ -1,35 +1,39 @@ -//import Circle from "@/models/Circle" -//import CirclePacker from "@/models/circle_packer/CirclePacker" -//import Epicycloid from "@/models/Epicycloid" -//import FancyText from "@/models/FancyText" -//import FileImport from "@/models/FileImport" -//import FractalSpirograph from "@/models/fractal_spirograph/FractalSpirograph" -//import Heart from "@/models/Heart" -//import Hypocycloid from "@/models/Hypocycloid" -//import InputText from "@/models/input_text/InputText" -//import LSystem from "@/models/lsystem/LSystem" -//import NoiseWave from "@/models/NoiseWave" -//import Point from "@/models/Point" +// shapes +import Circle from "@/models/Circle" +import Epicycloid from "@/models/Epicycloid" +import FancyText from "@/models/FancyText" +import FileImport from "@/models/FileImport" +import FractalSpirograph from "@/models/fractal_spirograph/FractalSpirograph" +import Heart from "@/models/Heart" +import Hypocycloid from "@/models/Hypocycloid" +import InputText from "@/models/input_text/InputText" +import LSystem from "@/models/lsystem/LSystem" +import Point from "@/models/Point" import Polygon from "@/models/Polygon" -//import Reuleaux from "@/models/Reuleaux" -//import Rose from "@/models/Rose" -//import SpaceFiller from "@/models/space_filler/SpaceFiller" -//import Star from "@/models/Star" -//import TessellationTwist from "@/models/tessellation_twist/TessellationTwist" -//import V1Engineering from "@/models/v1_engineering/V1Engineering" -//import Wiper from "@/models/Wiper" +import Reuleaux from "@/models/Reuleaux" +import Rose from "@/models/Rose" +import Star from "@/models/Star" +import TessellationTwist from "@/models/tessellation_twist/TessellationTwist" +import V1Engineering from "@/models/v1_engineering/V1Engineering" -//import FineTuning from "../models/effects/FineTuning" -//import Fisheye from "@/models/effects/Fisheye" -//import Loop from "@/models/effects/Loop" +// erasers +import CirclePacker from "@/models/circle_packer/CirclePacker" +import NoiseWave from "@/models/NoiseWave" +import SpaceFiller from "@/models/space_filler/SpaceFiller" +import Wiper from "@/models/Wiper" + +// effects +import FineTuning from "../models/effects/FineTuning" +import Fisheye from "@/models/effects/Fisheye" +import Loop from "@/models/effects/Loop" import Mask from "@/models/effects/Mask" -//import Noise from "@/models/effects/Noise" -//import Track from "@/models/effects/Track" -//import Warp from "@/models/effects/Warp" +import Noise from "@/models/effects/Noise" +import Track from "@/models/effects/Track" +import Warp from "@/models/effects/Warp" export const registeredModels = { polygon: new Polygon(), - /* star: new Star(), + star: new Star(), circle: new Circle(), heart: new Heart(), reuleaux: new Reuleaux(), @@ -37,23 +41,23 @@ export const registeredModels = { hypocycloid: new Hypocycloid(), rose: new Rose(), inputText: new InputText(), - fancy_text: new FancyText(), + fancyText: new FancyText(), v1Engineering: new V1Engineering(), lsystem: new LSystem(), - fractal_spirograph: new FractalSpirograph(), - tessellation_twist: new TessellationTwist(), + fractalSpirograph: new FractalSpirograph(), + tessellationTwist: new TessellationTwist(), point: new Point(), - circle_packer: new CirclePacker(), + circlePacker: new CirclePacker(), wiper: new Wiper(), - space_filler: new SpaceFiller(), + spaceFiller: new SpaceFiller(), noise_wave: new NoiseWave(), - file_import: new FileImport(), + fileImport: new FileImport(), fisheye: new Fisheye(), loop: new Loop(), track: new Track(), noise: new Noise(), warp: new Warp(), - fineTuning: new FineTuning(), */ + fineTuning: new FineTuning(), mask: new Mask(), } @@ -62,7 +66,7 @@ export const getModelFromType = (type) => { } export const getDefaultModelType = () => { - const defaultType = localStorage.getItem("defaultModelType") + const defaultType = localStorage.getItem("defaultModel") return getModelFromType(defaultType) ? defaultType : "polygon" } diff --git a/src/features/app/App.scss b/src/features/app/App.scss index 15ba2130..b9044c57 100644 --- a/src/features/app/App.scss +++ b/src/features/app/App.scss @@ -1,3 +1,10 @@ +@import 'bootstrap/dist/css/bootstrap.min.css'; +@import './bootstrap.scss'; + +.bg-white { + background-color: white; +} + .App { background-color: #eee; } @@ -35,95 +42,3 @@ padding-right: 0; } } - -h2.panel { - font-size: 1rem; - font-weight: bold; -} - -/** - Bootstrap 4 overrides - **/ -.card-header { - background-color: rgba(0,0,0,.05); -} - -.card { - h3 { - margin: 0; - font-size: 1.25rem; - } -} - -.accordion > .card > .card-header { - margin-bottom: 0; -} - -.card-body .form-label { - margin-bottom: 0; -} - -h2 { - font-size: 1.6rem; -} - -.bg-white { - background-color: white; -} - -.card h3 { - margin-top: 0.5rem; -} - -.tab-content { - background-color: white; -} - -.card.active > .card-header { - background-color: #A0D4EB; -} - -.list-group-item .active { - background-color: #2d6da4; -} - -.btn-group { - .btn-light:focus, .btn-light.focus { - background-color: #f8f9fa; - } - - .btn { - box-shadow: none !important; - } -} - -.btn-light { - background-color: white; - border-color: transparent !important; - box-shadow: none !important; - - &:focus-visible { - box-shadow: 0 0 0 0.2rem rgb(216 217 219 / 50%) !important; - border-color: #dae0e5 !important; - } -} - -.btn-group>.btn.active, .btn-group>.btn:active, .btn-group>.btn:focus { - z-index: inherit; -} - -.no-select { - -webkit-user-select: none; /* Safari */ - -moz-user-select: none; /* Firefox */ - -ms-user-select: none; /* IE10+/Edge */ - user-select: none; /* Standard */ -} - -.accordion-arrow { - transition: transform 0.3s ease; - transform: rotate(0deg); -} - -.accordion .show .accordion-arrow { - transform: rotate(90deg); -} diff --git a/src/features/app/bootstrap.scss b/src/features/app/bootstrap.scss new file mode 100644 index 00000000..0bbbcd97 --- /dev/null +++ b/src/features/app/bootstrap.scss @@ -0,0 +1,91 @@ +/** + Bootstrap 4 overrides + **/ +.card-header { + background-color: rgba(0,0,0,.05); + padding: 0.5rem; +} + +.card-body { + padding: 1rem; +} + +.card { + h3 { + margin: 0; + font-size: 1.25rem; + } +} + +.accordion > .card { + overflow: visible; +} + +.accordion > .card > .card-header { + margin-bottom: 0; +} + +.card-body .form-label { + margin-bottom: 0; +} + +h2 { + font-size: 1.6rem; +} + +.card h3 { + margin-top: 0.5rem; +} + +.tab-content { + background-color: white; +} + +.card.active > .card-header { + background-color: #A0D4EB; +} + +.list-group-item .active { + background-color: #2d6da4; +} + +.btn-group { + .btn-light:focus, .btn-light.focus { + background-color: #f8f9fa; + } + + .btn { + box-shadow: none !important; + } +} + +.btn-light { + background-color: white; + border-color: transparent !important; + box-shadow: none !important; + + &:focus-visible { + box-shadow: 0 0 0 0.2rem rgb(216 217 219 / 50%) !important; + border-color: #dae0e5 !important; + } +} + +.btn-group>.btn.active, .btn-group>.btn:active, .btn-group>.btn:focus { + z-index: inherit; +} + +.no-select { + -webkit-user-select: none; /* Safari */ + -moz-user-select: none; /* Firefox */ + -ms-user-select: none; /* IE10+/Edge */ + user-select: none; /* Standard */ +} + +.accordion-arrow { + transition: transform 0.3s ease; + transform: rotate(0deg); +} + +.accordion .show .accordion-arrow { + transform: rotate(90deg); +} diff --git a/src/features/exporter/CommentExporter.js b/src/features/exporter/CommentExporter.js index 1a8e8248..41cec9f2 100644 --- a/src/features/exporter/CommentExporter.js +++ b/src/features/exporter/CommentExporter.js @@ -14,7 +14,7 @@ export default class CommentExporter extends Exporter { const state = this.props // TODO: fix // const machine = new Machine() - let instance = state.machine + // let instance = state.machine this.line("Created by Sandify") this.line("https://sandify.org") @@ -46,7 +46,7 @@ export default class CommentExporter extends Exporter { this.optionLines(shape, layer, Object.keys(options)) this.keyValueLine("Visible", layer.visible) // TODO: fix - // this.optionLines(transform, layer, ['startingWidth', 'startingHeight', 'x', 'y', 'rotation', 'reverse']) + // this.optionLines(transform, layer, ['width', 'height', 'x', 'y', 'rotation', 'reverse']) if (!layer.effect) { this.line("Fine tuning:") diff --git a/src/features/exporter/Downloader.js b/src/features/exporter/Downloader.js index 039d5d47..744d4c22 100644 --- a/src/features/exporter/Downloader.js +++ b/src/features/exporter/Downloader.js @@ -164,7 +164,7 @@ class Downloader extends Component { optionKey="fileType" key="fileType" index={0} - model={this.props} + data={this.props} /> {this.props.fileType === SCARA && ( @@ -189,7 +189,7 @@ class Downloader extends Component { key="fileName" optionKey="fileName" index={1} - model={this.props} + data={this.props} /> {(this.props.fileType === THETARHO || @@ -200,7 +200,7 @@ class Downloader extends Component { key="polarRhoMax" optionKey="polarRhoMax" index={2} - model={this.props} + data={this.props} /> )} @@ -211,7 +211,7 @@ class Downloader extends Component { key="unitsPerCircle" optionKey="unitsPerCircle" index={2} - model={this.props} + data={this.props} /> )} @@ -221,7 +221,7 @@ class Downloader extends Component { key="pre" optionKey="pre" index={3} - model={this.props} + data={this.props} /> @@ -256,7 +256,7 @@ class Downloader extends Component { optionKey="reverse" key="reverse" index={5} - model={this.props} + data={this.props} />
diff --git a/src/features/exporter/Exporter.js b/src/features/exporter/Exporter.js index 16c9d8f8..fb700b67 100644 --- a/src/features/exporter/Exporter.js +++ b/src/features/exporter/Exporter.js @@ -16,20 +16,20 @@ export default class Exporter { this.computeOutputVertices(vertices) this.header() this.startComments() - this.props.comments.forEach(comment => this.line(comment)) + this.props.comments.forEach((comment) => this.line(comment)) this.line() - this.keyValueLine('File name', "'" + this.props.fileName + "'") - this.keyValueLine('File type', this.props.fileType) + this.keyValueLine("File name", "'" + this.props.fileName + "'") + this.keyValueLine("File type", this.props.fileType) this.line() this.endComments() - if (this.pre !== '') { + if (this.pre !== "") { this.startComments() - this.line('BEGIN PRE') + this.line("BEGIN PRE") this.endComments() - this.line(this.pre, this.pre !== '') + this.line(this.pre, this.pre !== "") this.startComments() - this.line('END PRE') + this.line("END PRE") this.endComments() } @@ -37,13 +37,13 @@ export default class Exporter { this.exportCode(this.vertices) this.line() - if (this.post !== '') { + if (this.post !== "") { this.startComments() - this.line('BEGIN POST') + this.line("BEGIN POST") this.endComments() - this.line(this.post, this.post !== '') + this.line(this.post, this.post !== "") this.startComments() - this.line('END POST') + this.line("END POST") this.endComments() } this.footer() @@ -65,34 +65,39 @@ export default class Exporter { this.vertices = vertices } - line(content='', add=true) { + line(content = "", add = true) { if (add) { - let padding = '' + let padding = "" if (this.commenting) { - padding = this.commentChar + (content.length > 0 ? ' ' : '') - for (let i=0; i 0 ? " " : "") + for (let i = 0; i < this.indentLevel; i++) { + padding += " " } } this.lines.push(padding + this.sanitizeValue(content)) } } - keyValueLine(key, value, add=true) { - this.line(key + ': ' + value, add) + keyValueLine(key, value, add = true) { + this.line(key + ": " + value, add) } - optionLine(metamodel, instance, option, add=true) { - const val = typeof instance[option] == 'string' ? - instance[option].replace(/[\n\r]/g, ' ') : - instance[option] - this.line(metamodel.getOptions()[option].title + ': ' + val, add) + optionLine(metamodel, instance, option, add = true) { + const val = + typeof instance[option] == "string" + ? instance[option].replace(/[\n\r]/g, " ") + : instance[option] + this.line(metamodel.getOptions()[option].title + ": " + val, add) } - optionLines(metamodel, instance, options, add=true) { - options.forEach(option => { + optionLines(metamodel, instance, options, add = true) { + options.forEach((option) => { const metaOption = metamodel.getOptions()[option] - const visible = metaOption.isVisible === undefined ? true : metaOption.isVisible(instance) + // TODO: fix + const visible = + metaOption.isVisible === undefined + ? true + : metaOption.isVisible(instance, instance) if (visible) { this.optionLine(metamodel, instance, option, add) @@ -117,6 +122,6 @@ export default class Exporter { } sanitizeValue(value) { - return value.replace("\n", ' ') + return value.replace("\n", " ") } } diff --git a/src/features/exporter/options.js b/src/features/exporter/options.js index 787c1765..05deb86d 100644 --- a/src/features/exporter/options.js +++ b/src/features/exporter/options.js @@ -1,42 +1,51 @@ -export const [GCODE, THETARHO, SVG, SCARA] = ['gcode', 'thetarho', 'svg', 'scara'] +export const [GCODE, THETARHO, SVG, SCARA] = [ + "gcode", + "thetarho", + "svg", + "scara", +] export const exportTypes = { - 'gcode': 'GCode', - 'thetarho': 'Theta Rho', - 'svg': 'SVG', - 'scara': 'SCARA GCode (experimental)' + gcode: "GCode", + thetarho: "Theta Rho", + svg: "SVG", + scara: "SCARA GCode (experimental)", } const exporterOptions = { fileName: { - title: 'File name', - type: 'string' + title: "File name", + type: "string", }, fileType: { - title: 'Export as', - type: 'dropdown', - choices: exportTypes + title: "Export as", + type: "dropdown", + choices: exportTypes, }, polarRhoMax: { - title: 'Maximum rho value (0-1)', + title: "Maximum rho value (0-1)", min: 0, - max: 1 + max: 1, }, unitsPerCircle: { - title: 'Units per circle', - type: 'number', + title: "Units per circle", + type: "number", }, post: { - title: 'Program end code', - type: 'textarea', - isVisible: (state) => { return state.fileType !== SVG }, + title: "Program end code", + type: "textarea", + isVisible: (exporter, state) => { + return state.fileType !== SVG + }, }, pre: { - title: 'Program start code', - type: 'textarea', - isVisible: (state) => { return state.fileType !== SVG }, + title: "Program start code", + type: "textarea", + isVisible: (exporter, state) => { + return state.fileType !== SVG + }, }, reverse: { - title: 'Reverse path in the code', + title: "Reverse path in the code", }, } diff --git a/src/features/layers/ImportLayer.js b/src/features/layers/ImportLayer.js index 8002ffbd..44ea65c8 100644 --- a/src/features/layers/ImportLayer.js +++ b/src/features/layers/ImportLayer.js @@ -1,16 +1,16 @@ -import React, { Component } from 'react' -import { Button, Modal, Form, Accordion, Card } from 'react-bootstrap' -import { connect } from 'react-redux' +import React, { Component } from "react" +import { Button, Modal, Form, Accordion, Card } from "react-bootstrap" +import { connect } from "react-redux" -import ThetaRhoImporter from '../importer/ThetaRhoImporter' -import GCodeImporter from '../importer/GCodeImporter' -import { addLayer } from '../layers/layersSlice' -import { registeredModels } from '../../config/models' -import ReactGA from 'react-ga' +import ThetaRhoImporter from "../importer/ThetaRhoImporter" +import GCodeImporter from "../importer/GCodeImporter" +import { addLayer } from "../layers/layersSlice" +import Layer from "@/features/layers/layer" +import ReactGA from "react-ga" const mapStateToProps = (state, ownProps) => { return { - showModal: ownProps.showModal + showModal: ownProps.showModal, } } @@ -20,9 +20,10 @@ const mapDispatchToProps = (dispatch, ownProps) => { ownProps.toggleModal() }, onLayerImport: (importProps) => { + const layer = new Layer("fileImport") const attrs = { - ...registeredModels["file_import"].getInitialState(importProps), - name: importProps.fileName + ...layer.getInitialState(importProps), + name: importProps.fileName, } dispatch(addLayer(attrs)) }, @@ -31,76 +32,114 @@ const mapDispatchToProps = (dispatch, ownProps) => { class ImportLayer extends Component { render() { - const { - toggleModal, showModal - } = this.props - - return - - Import new layer - - - - - - -

Import

- Imports a pattern file as a new layer. Supported formats are .thr, .gcode, and .nc. - -
-
-
-
-

Where to get .thr files

- Sisyphus machines use theta rho (.thr) files. There is a large community sharing them. -
- -
- + const { toggleModal, showModal } = this.props + + return ( + + + Import new layer + + + + + + +

Import

+ Imports a pattern file as a new layer. Supported formats are + .thr, .gcode, and .nc. + +
+
+
+
+

Where to get .thr files

+ Sisyphus machines use theta rho (.thr) files. There is a large + community sharing them. + +

About copyrights

+

+ Be careful and respectful. Understand that the original author put + their labor, intensity, and ideas into this art. The creators have + a right to own it (and they have a copyright, even if it + doesn't say so). If you don't have permisson (a license) + to use their art, then you shouldn't be. If you do have + permission to use their art, then you should be thankful, and + I'm sure they would appreciate you sending them a note of + thanks. A picture of your table creating their shared art would + probably make them smile. +

+

+ Someone posting the .thr file to a forum or subreddit probably + wants it to be shared, and drawing it on your home table is + probably OK. Just be careful if you want to use them for something + significant without explicit permission. +

+

P.S. I am not a lawyer.

- -

About copyrights

-

Be careful and respectful. Understand that the original author put their labor, intensity, and ideas into this art. The creators have a right to own it (and they have a copyright, even if it doesn't say so). If you don't have permisson (a license) to use their art, then you shouldn't be. If you do have permission to use their art, then you should be thankful, and I'm sure they would appreciate you sending them a note of thanks. A picture of your table creating their shared art would probably make them smile.

-

Someone posting the .thr file to a forum or subreddit probably wants it to be shared, and drawing it on your home table is probably OK. Just be careful if you want to use them for something significant without explicit permission.

-

P.S. I am not a lawyer.

-
- - - - - - + + + + + + + ) } onFileSelected(event) { @@ -112,11 +151,11 @@ class ImportLayer extends Component { var text = reader.result let importer - if (file.name.toLowerCase().endsWith('.thr')) { + if (file.name.toLowerCase().endsWith(".thr")) { importer = new ThetaRhoImporter(file.name, text) } else if ( - file.name.toLowerCase().endsWith('.gcode') || - file.name.toLowerCase().endsWith('.nc') + file.name.toLowerCase().endsWith(".gcode") || + file.name.toLowerCase().endsWith(".nc") ) { importer = new GCodeImporter(file.name, text) } @@ -133,9 +172,9 @@ class ImportLayer extends Component { this.endTime = performance.now() ReactGA.timing({ - category: 'PatternImport', - variable: 'read' + importer.label, - value: this.endTime - this.startTime // in milliseconds + category: "PatternImport", + variable: "read" + importer.label, + value: this.endTime - this.startTime, // in milliseconds }) } } diff --git a/src/features/layers/Layer.js b/src/features/layers/Layer.js index b5f6585e..db1c9726 100644 --- a/src/features/layers/Layer.js +++ b/src/features/layers/Layer.js @@ -1,4 +1,5 @@ import { getModelFromType } from "../../config/models" +import { resizeVertices } from "@/common/geometry" export const layerOptions = { name: { @@ -8,48 +9,51 @@ export const layerOptions = { x: { title: "X", inline: true, - isVisible: (model) => { + isVisible: (model, state) => { return model.canMove }, }, y: { title: "Y", inline: true, - isVisible: (model) => { + isVisible: (model, state) => { return model.canMove }, }, - startingWidth: { - title: "Initial width", + width: { + title: "W", min: 1, - isVisible: (model) => { - return model.canChangeSize + inline: true, + isVisible: (model, state) => { + return model.canChangeSize(state) }, - onChange: (changes, attrs) => { - if (!attrs.canChangeHeight) { - changes.startingHeight = changes.startingWidth + onChange: (model, changes, state) => { + if (!model.canChangeHeight(state)) { + changes.height = changes.width } return changes }, }, - startingHeight: { - title: "Initial height", + height: { + title: "H", min: 1, - isVisible: (model) => { - return model.canChangeSize && model.canChangeHeight + inline: true, + isVisible: (model, state) => { + return model.canChangeSize(state) && model.canChangeHeight(state) }, }, reverse: { title: "Reverse path", type: "checkbox", - isVisible: (model) => { + isVisible: (model, state) => { return !model.effect }, }, rotation: { title: "Rotate (degrees)", - isVisible: (model) => { - return model.canRotate + inline: true, + isVisible: (model, state) => { + return model.canRotate(state) }, }, connectionMethod: { @@ -64,13 +68,16 @@ export default class Layer { this.model = getModelFromType(type) } - getInitialState() { + getInitialState(props) { return { - ...this.model.getInitialState(), + ...this.model.getInitialState(props), ...{ + type: this.model.type, connectionMethod: "line", x: 0.0, y: 0.0, + width: this.model.startingWidth, + height: this.model.startingHeight, rotation: 0, reverse: false, visible: true, @@ -84,18 +91,13 @@ export default class Layer { } getVertices(state) { - const { - startingWidth, - startingHeight, - autosize, - x, - y, - rotation, - } = state.shape + const { width, height, x, y, rotation } = state.shape let vertices = this.model.getVertices(state) vertices.forEach((vertex) => { - vertex.multiply({ x: startingWidth, y: startingHeight }) + if (this.model.autosize) { + vertices = resizeVertices(vertices, width, height, false) + } vertex.rotateDeg(-rotation) vertex.addX({ x: x || 0 }).addY({ y: y || 0 }) }) diff --git a/src/features/layers/LayerEditor.js b/src/features/layers/LayerEditor.js index 5db4645e..494f5615 100644 --- a/src/features/layers/LayerEditor.js +++ b/src/features/layers/LayerEditor.js @@ -1,33 +1,23 @@ -import { connect } from "react-redux" import React, { Component } from "react" -import { Button, Card, Row, Col } from "react-bootstrap" +import { connect } from "react-redux" +import { Button, Card, Row, Col, Accordion } from "react-bootstrap" import Select from "react-select" -import CommentsBox from "../../components/CommentsBox" -import InputOption from "../../components/InputOption" -import DropdownOption from "../../components/DropdownOption" -import CheckboxOption from "../../components/CheckboxOption" -import ToggleButtonOption from "../../components/ToggleButtonOption" -import { - updateLayer, - setShapeType, - restoreDefaults, -} from "../layers/layersSlice" +import { IconContext } from "react-icons" +import { AiOutlineRotateRight } from "react-icons/ai" +import CommentsBox from "@/components/CommentsBox" +import InputOption from "@/components/InputOption" +import DropdownOption from "@/components/DropdownOption" +import CheckboxOption from "@/components/CheckboxOption" +import ToggleButtonOption from "@/components/ToggleButtonOption" import { getCurrentLayerState } from "./selectors" -import { getModelFromType, getModelSelectOptions } from "../../config/models" +import { getModelSelectOptions } from "@/config/models" +import { updateLayer, changeModelType, restoreDefaults } from "./layersSlice" +import Layer from "./Layer" import "./LayerEditor.scss" const mapStateToProps = (state, ownProps) => { - const layer = getCurrentLayerState(state) - const shape = getModelFromType(layer.type) - return { - layer: layer, - shape: shape, - options: shape.getOptions(), - selectOptions: getModelSelectOptions(false), - showShapeSelectRender: layer.selectGroup !== "import" && !layer.effect, - link: shape.link, - linkText: shape.linkText, + state: getCurrentLayerState(state), } } @@ -40,7 +30,7 @@ const mapDispatchToProps = (dispatch, ownProps) => { dispatch(updateLayer(attrs)) }, onChangeType: (selected) => { - dispatch(setShapeType({ id: id, type: selected.value })) + dispatch(changeModelType({ id, type: selected.value })) }, onRestoreDefaults: (event) => { dispatch(restoreDefaults(id)) @@ -48,24 +38,44 @@ const mapDispatchToProps = (dispatch, ownProps) => { } } -class Layer extends Component { +class LayerEditor extends Component { render() { + const { state } = this.props + const layer = new Layer(state.type) + const model = layer.model + const layerOptions = layer.getOptions() + const modelOptions = model.getOptions() + const selectOptions = getModelSelectOptions() + const allowModelSelection = model.selectGroup !== "import" && !model.effect + const selectedOption = { - value: this.props.shape.id, - label: this.props.shape.name, + value: model.type, + label: model.label, } - const optionsRender = Object.keys(this.props.options).map((key, index) => { - return this.getOptionComponent(key, index) + const link = model.link + const linkText = model.linkText || link + const renderedModelOptions = Object.keys(modelOptions).map((key) => { + return ( +
+ {this.getOptionComponent(model, modelOptions, key)} +
+ ) }) - const linkText = this.props.linkText || this.props.link - const linkRender = this.props.link ? ( + const renderedLink = link ? (

See{" "} - + {linkText} {" "} for ideas. @@ -73,119 +83,168 @@ class Layer extends Component { ) : undefined - let optionsListRender = undefined - - if (Object.entries(this.props.options).length > 0) { - optionsListRender =

{optionsRender}
- } + const renderedModelSelection = allowModelSelection && ( + + Type - let shapeSelectRender = undefined - - if (this.props.showShapeSelectRender) { - shapeSelectRender = ( - - Shape - - - + + + ) return ( - - -

Properties

- - - - -
- - {shapeSelectRender} - - {linkRender} - -
{optionsListRender}
+ + + + + Layer + + + + + {this.getOptionComponent(model, layerOptions, "name")} + {model.canTransform(state) && ( + + Transform + + {model.canMove && ( + + + {this.getOptionComponent(model, layerOptions, "x")} + + + {this.getOptionComponent(model, layerOptions, "y")} + + + )} + {model.canChangeSize(state) && model.autosize && ( + + + {this.getOptionComponent( + model, + layerOptions, + "width", + )} + + + {this.getOptionComponent( + model, + layerOptions, + "height", + )} + + + )} + {model.canRotate(state) && ( + + +
+
+ + + +
+ {this.getOptionComponent( + model, + layerOptions, + "rotation", + false, + )} +
+ +
+ )} + +
+ )} + {this.getOptionComponent(model, layerOptions, "reverse")} +
+
+
+
+ + + + + + Shape + + + + + + {renderedModelSelection} + {renderedModelOptions} + {renderedLink} + + + +
) } - getOptionComponent(key, index) { - const option = this.props.options[key] + getOptionComponent(model, options, key, label = true) { + const option = options[key] + const { state, onChange } = this.props + const props = { + options, + label, + key, + onChange, + optionKey: key, + data: state, + object: model, + comments: state.comments, + } - if (option.type === "dropdown") { - return ( - - ) - } else if (option.type === "checkbox") { - return ( - - ) - } else if (option.type === "comments") { - return ( - - ) - } else if (option.type === "togglebutton") { - return ( - - ) - } else { - return ( - - ) + switch (option.type) { + case "dropdown": + return + case "checkbox": + return + case "comments": + return + case "togglebutton": + return + default: + return } } } -export default connect(mapStateToProps, mapDispatchToProps)(Layer) +export default connect(mapStateToProps, mapDispatchToProps)(LayerEditor) diff --git a/src/features/layers/Playlist.js b/src/features/layers/Playlist.js index 8f9538b9..4481e1a1 100644 --- a/src/features/layers/Playlist.js +++ b/src/features/layers/Playlist.js @@ -31,9 +31,9 @@ const mapStateToProps = (state, ownProps) => { return { layers: getAllLayerStates(state), - numLayers: numLayers, + numLayers, currentLayer: layer, - shape: shape, + shape, } } @@ -51,16 +51,16 @@ const mapDispatchToProps = (dispatch, ownProps) => { dispatch(removeLayer(id)) }, onLayerMoved: ({ oldIndex, newIndex }) => { - dispatch(moveLayer({ oldIndex: oldIndex, newIndex: newIndex })) + dispatch(moveLayer({ oldIndex, newIndex })) }, onSortStarted: ({ node }) => { dispatch(setCurrentLayer(node.id)) }, onToggleLayerOpen: (id) => { - dispatch(toggleOpen({ id: id })) + dispatch(toggleOpen({ id })) }, onToggleLayerVisible: (id) => { - dispatch(toggleVisible({ id: id })) + dispatch(toggleVisible({ id })) }, } } @@ -130,7 +130,6 @@ class Playlist extends Component { />
-

Layers ({numLayers})

{ - if (layer[attr] === undefined) { - layer[attr] = defaults[attr] + changeModelType(state, action) { + const { type, id } = action.payload + const newLayer = new Layer(type) + const layerState = state.byId[id] + const newLayerState = newLayer.getInitialState() + + Object.keys(newLayerState).forEach((attr) => { + if ( + !notCopiedWhenTypeChanges.includes(attr) && + layerState[attr] != undefined + ) { + newLayerState[attr] = layerState[attr] } }) - protectedAttrs.forEach((attr) => { - layer[attr] = defaults[attr] - }) + newLayerState.id = id + if (!newLayer.canMove) { + newLayerState.x = 0 + newLayerState.y = 0 + } - state.byId[layer.id] = layer + state.byId[id] = newLayerState }, setNewEffectType(state, action) { let attrs = { newEffectType: action.payload } @@ -241,7 +237,7 @@ export const { restoreDefaults, setCurrentLayer, setSelectedLayer, - setShapeType, + changeModelType, setNewEffectType, updateLayer, updateLayers, diff --git a/src/features/layers/layersSlice.spec.js b/src/features/layers/layersSlice.spec.js index f217976c..98be016a 100644 --- a/src/features/layers/layersSlice.spec.js +++ b/src/features/layers/layersSlice.spec.js @@ -1,6 +1,6 @@ -jest.mock('lodash/uniqueId') -const uniqueId = require('lodash/uniqueId') // eslint-disable-line @typescript-eslint/no-var-requires -import mockUniqueId, { resetUniqueIds } from '../../common/mocks' +jest.mock("lodash/uniqueId") +const uniqueId = require("lodash/uniqueId") // eslint-disable-line @typescript-eslint/no-var-requires +import mockUniqueId, { resetUniqueIds } from "../../common/mocks" import layers, { addLayer, removeLayer, @@ -14,607 +14,590 @@ import layers, { setSelectedLayer, setNewLayerType, setNewEffectType, - setShapeType, + changeModelType, updateLayer, toggleOpen, - toggleVisible -} from './layersSlice' + toggleVisible, +} from "./layersSlice" beforeEach(() => { resetUniqueIds() uniqueId.mockImplementation((prefix) => mockUniqueId(prefix)) }) -describe('layers reducer', () => { +describe("layers reducer", () => { const initialState = { circleLobes: 1, - circleDirection: 'clockwise', - type: 'circle', - selectGroup: 'Shapes', - shouldCache: true, - canRotate: true, - canChangeSize: true, - canChangeHeight: true, - canMove: true, - autosize: true, - startingWidth: 10, - startingHeight: 10, + circleDirection: "clockwise", + type: "circle", + width: 10, + height: 10, x: 0.0, y: 0.0, open: true, rotation: 0, reverse: false, - connectionMethod: 'line', - effect: false, - usesMachine: false, - usesFonts: false, + connectionMethod: "line", dragging: false, - visible: true + visible: true, } - it('should handle initial state', () => { + it("should handle initial state", () => { expect(layers(undefined, {})).toEqual({ current: null, selected: null, - newLayerType: 'polygon', - newLayerName: 'polygon', + newLayerType: "polygon", + newLayerName: "polygon", newLayerNameOverride: false, newEffectNameOverride: false, - newEffectName: 'mask', - newEffectType: 'mask', + newEffectName: "mask", + newEffectType: "mask", copyLayerName: null, byId: {}, - allIds: [] + allIds: [], }) }) - it('should handle addLayer', () => { + it("should handle addLayer", () => { expect( - layers({ - byId: {}, - allIds: [] - }, - addLayer({ - name: 'foo' - })) + layers( + { + byId: {}, + allIds: [], + }, + addLayer({ + name: "foo", + }), + ), ).toEqual({ byId: { - 'layer-1': { - id: 'layer-1', - name: 'foo' - } + "layer-1": { + id: "layer-1", + name: "foo", + }, }, - allIds: ['layer-1'], - current: 'layer-1', - selected: 'layer-1', - newLayerName: 'foo', - newLayerNameOverride: false + allIds: ["layer-1"], + current: "layer-1", + selected: "layer-1", + newLayerName: "foo", + newLayerNameOverride: false, }) }) - describe('removeLayer', () => { - it('should remove layer', () => { + describe("removeLayer", () => { + it("should remove layer", () => { expect( - layers({ - byId: { - 'layer-1': { - id: 'layer-1', - name: 'foo' - } + layers( + { + byId: { + "layer-1": { + id: "layer-1", + name: "foo", + }, + }, + allIds: ["layer-1"], + current: "layer-1", + copyLayerName: "foo", }, - allIds: ['layer-1'], - current: 'layer-1', - copyLayerName: 'foo' - }, - removeLayer('layer-1')) + removeLayer("layer-1"), + ), ).toEqual({ byId: {}, allIds: [], current: undefined, selected: undefined, - copyLayerName: 'foo' + copyLayerName: "foo", }) }) - it('should remove effects associated with layer', () => { + it("should remove effects associated with layer", () => { expect( - layers({ - byId: { - 'layer': { - id: 'layer', - name: 'foo', - effectIds: ['effect'] + layers( + { + byId: { + layer: { + id: "layer", + name: "foo", + effectIds: ["effect"], + }, + effect: { + id: "effect", + name: "bar", + parentId: "layer", + }, }, - 'effect': { - id: 'effect', - name: 'bar', - parentId: 'layer' - } + allIds: ["layer"], + current: "layer", + copyLayerName: "foo", }, - allIds: ['layer'], - current: 'layer', - copyLayerName: 'foo' - }, - removeLayer('layer')) + removeLayer("layer"), + ), ).toEqual({ byId: {}, allIds: [], current: undefined, selected: undefined, - copyLayerName: 'foo' + copyLayerName: "foo", }) }) }) - describe('copyLayer', () => { - it('should copy layer', () => { + describe("copyLayer", () => { + it("should copy layer", () => { expect( - layers({ - byId: { - 'layer-0': { - id: 'layer-0', - name: 'foo' - } + layers( + { + byId: { + "layer-0": { + id: "layer-0", + name: "foo", + }, + }, + allIds: ["layer-0"], + current: "layer-0", + copyLayerName: "foo", }, - allIds: ['layer-0'], - current: 'layer-0', - copyLayerName: 'foo' - }, - copyLayer('layer-0')) + copyLayer("layer-0"), + ), ).toEqual({ byId: { - 'layer-0': { - id: 'layer-0', - name: 'foo' + "layer-0": { + id: "layer-0", + name: "foo", + }, + "layer-1": { + id: "layer-1", + name: "foo", }, - 'layer-1': { - id: 'layer-1', - name: 'foo' - } }, - allIds: ['layer-0', 'layer-1'], - current: 'layer-1', - selected: 'layer-1', - copyLayerName: null + allIds: ["layer-0", "layer-1"], + current: "layer-1", + selected: "layer-1", + copyLayerName: null, }) }) - it('should copy effects', () => { + it("should copy effects", () => { expect( - layers({ - byId: { - 'layer': { - id: 'layer', - name: 'foo', - effectIds: ['effect'] + layers( + { + byId: { + layer: { + id: "layer", + name: "foo", + effectIds: ["effect"], + }, + effect: { + id: "effect", + name: "bar", + parentId: "layer", + }, }, - 'effect': { - id: 'effect', - name: 'bar', - parentId: 'layer' - } + allIds: ["layer"], + current: "layer", + copyLayerName: "foo", }, - allIds: ['layer'], - current: 'layer', - copyLayerName: 'foo' - }, - copyLayer('layer')) + copyLayer("layer"), + ), ).toEqual({ byId: { - 'layer': { - id: 'layer', - name: 'foo', - effectIds: ['effect'] + layer: { + id: "layer", + name: "foo", + effectIds: ["effect"], }, - 'effect': { - id: 'effect', - name: 'bar', - parentId: 'layer' + effect: { + id: "effect", + name: "bar", + parentId: "layer", }, - 'layer-1': { - id: 'layer-1', - name: 'foo', - effectIds: ['layer-2'] + "layer-1": { + id: "layer-1", + name: "foo", + effectIds: ["layer-2"], }, - 'layer-2': { - id: 'layer-2', - name: 'bar', - parentId: 'layer-1' + "layer-2": { + id: "layer-2", + name: "bar", + parentId: "layer-1", }, }, - allIds: ['layer', 'layer-1'], - current: 'layer-1', - selected: 'layer-1', - copyLayerName: null + allIds: ["layer", "layer-1"], + current: "layer-1", + selected: "layer-1", + copyLayerName: null, }) }) }) - it('should handle moveLayer', () => { + it("should handle moveLayer", () => { expect( layers( { - allIds: ['a', 'b', 'c', 'd', 'e'] + allIds: ["a", "b", "c", "d", "e"], }, - moveLayer({oldIndex: 0, newIndex: 2}) - ) + moveLayer({ oldIndex: 0, newIndex: 2 }), + ), ).toEqual({ - allIds: ['b', 'c', 'a', 'd', 'e'], + allIds: ["b", "c", "a", "d", "e"], }) }) - it('should handle restoreDefaults', () => { + it("should handle restoreDefaults", () => { expect( layers( { byId: { - 'layer-1': { - id: 'layer-1', - name: 'foo', - type: 'circle', - circleLobes: '2', - polygonSides: '5' - } - } + "layer-1": { + id: "layer-1", + name: "foo", + type: "circle", + circleLobes: "2", + polygonSides: "5", + }, + }, }, - restoreDefaults('layer-1') - ) + restoreDefaults("layer-1"), + ), ).toEqual({ byId: { - 'layer-1': { - id: 'layer-1', - name: 'foo', - ...initialState - } - } + "layer-1": { + id: "layer-1", + name: "foo", + ...initialState, + }, + }, }) }) - describe('addEffect', () => { - it('when no parent layer, does nothing', () => { + describe("addEffect", () => { + it("when no parent layer, does nothing", () => { const state = { byId: { - 'layer-1': { - id: 'layer-1', - name: 'foo' - } + "layer-1": { + id: "layer-1", + name: "foo", + }, }, } expect( - layers(state, - addEffect({ - name: 'bar' - })) + layers( + state, + addEffect({ + name: "bar", + }), + ), ).toEqual(state) }) - it('adds effect', () => { + it("adds effect", () => { expect( - layers({ - byId: { - 'layer': { - id: 'layer', - name: 'foo' - } + layers( + { + byId: { + layer: { + id: "layer", + name: "foo", + }, + }, + allIds: ["layer"], + current: "layer", }, - allIds: ['layer'], - current: 'layer' - }, - addEffect({ - name: 'bar', - parentId: 'layer' - })) + addEffect({ + name: "bar", + parentId: "layer", + }), + ), ).toEqual({ byId: { - 'layer': { - id: 'layer', - name: 'foo', + layer: { + id: "layer", + name: "foo", open: true, - effectIds: ['layer-1'] + effectIds: ["layer-1"], + }, + "layer-1": { + id: "layer-1", + name: "bar", + parentId: "layer", }, - 'layer-1': { - id: 'layer-1', - name: 'bar', - parentId: 'layer' - } }, - allIds: ['layer'], - current: 'layer-1', - selected: 'layer-1' + allIds: ["layer"], + current: "layer-1", + selected: "layer-1", }) }) }) - it('should handle removeEffect', () => { + it("should handle removeEffect", () => { expect( - layers({ - byId: { - 'layer-1': { - id: 'layer-1', - name: 'foo', - effectIds: ['layer-2'] - }, - 'layer-2': { - id: 'layer-2', - name: 'bar', - parentId: 'layer-1' + layers( + { + byId: { + "layer-1": { + id: "layer-1", + name: "foo", + effectIds: ["layer-2"], + }, + "layer-2": { + id: "layer-2", + name: "bar", + parentId: "layer-1", + }, }, + allIds: ["layer-1"], + current: "layer-2", }, - allIds: ['layer-1'], - current: 'layer-2', - }, - removeEffect('layer-2')) + removeEffect("layer-2"), + ), ).toEqual({ byId: { - 'layer-1': { - id: 'layer-1', - name: 'foo', - effectIds: [] + "layer-1": { + id: "layer-1", + name: "foo", + effectIds: [], }, }, - allIds: ['layer-1'], - current: 'layer-1', - selected: 'layer-1' + allIds: ["layer-1"], + current: "layer-1", + selected: "layer-1", }) }) - it('should handle moveEffect', () => { + it("should handle moveEffect", () => { expect( layers( { byId: { - 'layer-1': { - id: 'layer-1', - name: 'foo', - effectIds: ['layer-2', 'layer-3'] + "layer-1": { + id: "layer-1", + name: "foo", + effectIds: ["layer-2", "layer-3"], }, - 'layer-2': { - id: 'layer-2', - name: 'bar', - parentId: 'layer-1' + "layer-2": { + id: "layer-2", + name: "bar", + parentId: "layer-1", }, - 'layer-3': { - id: 'layer-3', - name: 'moo', - parentId: 'layer-1' + "layer-3": { + id: "layer-3", + name: "moo", + parentId: "layer-1", }, }, - allIds: ['layer-1'] + allIds: ["layer-1"], }, - moveEffect({parentId: 'layer-1', oldIndex: 0, newIndex: 1}) - ) + moveEffect({ parentId: "layer-1", oldIndex: 0, newIndex: 1 }), + ), ).toEqual({ byId: { - 'layer-1': { - id: 'layer-1', - name: 'foo', - effectIds: ['layer-3', 'layer-2'] + "layer-1": { + id: "layer-1", + name: "foo", + effectIds: ["layer-3", "layer-2"], }, - 'layer-2': { - id: 'layer-2', - name: 'bar', - parentId: 'layer-1' + "layer-2": { + id: "layer-2", + name: "bar", + parentId: "layer-1", }, - 'layer-3': { - id: 'layer-3', - name: 'moo', - parentId: 'layer-1' + "layer-3": { + id: "layer-3", + name: "moo", + parentId: "layer-1", }, }, - allIds: ['layer-1'] + allIds: ["layer-1"], }) }) - it('should handle setCurrentLayer', () => { + it("should handle setCurrentLayer", () => { expect( layers( { byId: { - 'layer-2': { - id: 'layer-2' - } + "layer-2": { + id: "layer-2", + }, }, - allIds: ['layer-1', 'layer-2'] + allIds: ["layer-1", "layer-2"], }, - setCurrentLayer('layer-2') - ) + setCurrentLayer("layer-2"), + ), ).toEqual({ byId: { - 'layer-2': { - id: 'layer-2' - } + "layer-2": { + id: "layer-2", + }, }, - allIds: ['layer-1', 'layer-2'], - current: 'layer-2', - selected: 'layer-2' + allIds: ["layer-1", "layer-2"], + current: "layer-2", + selected: "layer-2", }) }) - it('should handle setSelectedLayer', () => { + it("should handle setSelectedLayer", () => { expect( layers( { byId: { - 'layer-2': { - id: 'layer-2' - } + "layer-2": { + id: "layer-2", + }, }, - allIds: ['layer-1', 'layer-2'] + allIds: ["layer-1", "layer-2"], }, - setSelectedLayer('layer-2') - ) + setSelectedLayer("layer-2"), + ), ).toEqual({ byId: { - 'layer-2': { - id: 'layer-2' - } + "layer-2": { + id: "layer-2", + }, }, - allIds: ['layer-1', 'layer-2'], - selected: 'layer-2' + allIds: ["layer-1", "layer-2"], + selected: "layer-2", }) }) - describe('setShapeType', () => { - it('should add default values', () => { - expect( - layers( - { - byId: { - 'layer-1': { - id: 'layer-1' - } - } - }, - setShapeType({id: 'layer-1', type: 'circle'}) - ) - ).toEqual({ - byId: { - 'layer-1': { - id: 'layer-1', - ...initialState - } - } - }) - }) - - it('should not override values if provided', () => { + describe("changeModelType", () => { + it("should add default values", () => { expect( layers( { byId: { - 'layer-1': { - id: 'layer-1', - circleLobes: 2 - } - } + "layer-1": { + id: "layer-1", + }, + }, }, - setShapeType({id: 'layer-1', type: 'circle'}) - ) + changeModelType({ id: "layer-1", type: "circle" }), + ), ).toEqual({ byId: { - 'layer-1': { - id: 'layer-1', + "layer-1": { + id: "layer-1", ...initialState, - circleLobes: 2 - } - } + }, + }, }) }) - it('should always override values of protected attributes', () => { + it("should not override values if provided", () => { expect( layers( { byId: { - 'layer-1': { - id: 'layer-1', - canChangeSize: false - } - } + "layer-1": { + id: "layer-1", + circleLobes: 2, + }, + }, }, - setShapeType({id: 'layer-1', type: 'circle'}) - ) + changeModelType({ id: "layer-1", type: "circle" }), + ), ).toEqual({ byId: { - 'layer-1': { - id: 'layer-1', + "layer-1": { + id: "layer-1", ...initialState, - } - } + circleLobes: 2, + }, + }, }) }) }) - it('should handle updateLayer', () => { + it("should handle updateLayer", () => { expect( layers( { byId: { - '1': { - id: '1', - name: 'foo' - } - } + 1: { + id: "1", + name: "foo", + }, + }, }, - updateLayer({id: '1', name: 'bar'}) - ) + updateLayer({ id: "1", name: "bar" }), + ), ).toEqual({ byId: { - '1': { - id: '1', - name: 'bar' - } - } + 1: { + id: "1", + name: "bar", + }, + }, }) }) - it('should handle toggleVisible', () => { + it("should handle toggleVisible", () => { expect( layers( { byId: { - '1': { - visible: true - } - } + 1: { + visible: true, + }, + }, }, - toggleVisible({id: '1'}) - ) + toggleVisible({ id: "1" }), + ), ).toEqual({ byId: { - '1': { - visible: false - } - } + 1: { + visible: false, + }, + }, }) }) - it('should handle toggleOpen', () => { + it("should handle toggleOpen", () => { expect( layers( { byId: { - '1': { - open: true - } - } + 1: { + open: true, + }, + }, }, - toggleOpen({id: '1'}) - ) + toggleOpen({ id: "1" }), + ), ).toEqual({ byId: { - '1': { - open: false - } - } + 1: { + open: false, + }, + }, }) }) - it('should handle setNewLayerType', () => { + it("should handle setNewLayerType", () => { expect( layers( { - newLayerType: 'circle' + newLayerType: "circle", }, - setNewLayerType('polygon') - ) + setNewLayerType("polygon"), + ), ).toEqual({ - newLayerType: 'polygon', - newLayerName: 'polygon' + newLayerType: "polygon", + newLayerName: "polygon", }) }) - it('should handle setNewEffectType', () => { + it("should handle setNewEffectType", () => { expect( layers( { - newEffectType: 'mask' + newEffectType: "mask", }, - setNewEffectType('noise') - ) + setNewEffectType("noise"), + ), ).toEqual({ - newEffectType: 'noise', - newEffectName: 'noise' + newEffectType: "noise", + newEffectName: "noise", }) }) }) diff --git a/src/features/machine/PolarSettings.js b/src/features/machine/PolarSettings.js index bb5cf947..1e2cb125 100644 --- a/src/features/machine/PolarSettings.js +++ b/src/features/machine/PolarSettings.js @@ -76,7 +76,7 @@ class PolarSettings extends Component { key="maxRadius" optionKey="maxRadius" index={0} - model={this.props} + data={this.props} /> @@ -137,7 +137,7 @@ class PolarSettings extends Component { optionKey="minimizeMoves" key="minimizeMoves" index={0} - model={this.props} + data={this.props} /> diff --git a/src/features/machine/RectSettings.js b/src/features/machine/RectSettings.js index bb51050b..b305e28d 100644 --- a/src/features/machine/RectSettings.js +++ b/src/features/machine/RectSettings.js @@ -76,7 +76,7 @@ class RectSettings extends Component { key="minX" optionKey="minX" index={0} - model={this.props} + data={this.props} /> @@ -142,7 +142,7 @@ class RectSettings extends Component { optionKey="minimizeMoves" key="minimizeMoves" index={0} - model={this.props} + data={this.props} /> diff --git a/src/features/machine/computer.js b/src/features/machine/computer.js index 63c86a9e..0d4a33da 100644 --- a/src/features/machine/computer.js +++ b/src/features/machine/computer.js @@ -91,7 +91,7 @@ export const transformShapes = (vertices, layer, effects) => { let outputVertices = vertices.map((vertex) => vertex.clone()) // if (layer.autosize) { // vertices = vertices.map(vertex => { - // return scale(vertex, 100.0 * layer.startingWidth, 100 * layer.startingHeight) + // return scale(vertex, 100.0 * layer.width, 100 * layer.height) // }) // } diff --git a/src/features/machine/selectors.js b/src/features/machine/selectors.js index ea1132b3..c920600c 100644 --- a/src/features/machine/selectors.js +++ b/src/features/machine/selectors.js @@ -2,7 +2,6 @@ import LRUCache from "lru-cache" import { createSelector } from "reselect" import Color from "color" import { transformShapes, polishVertices, getMachineInstance } from "./computer" -import { getModelFromType } from "../../config/models" import { getMachine, getState, getPreview } from "../store/selectors" import { getLoadedFonts } from "../fonts/selectors" import { @@ -33,9 +32,10 @@ const getCacheKey = (state) => { const makeGetLayerMachine = (layerId) => { return createSelector( [getCachedSelector(makeGetLayer, layerId), getMachine], - (layer, machine) => { + (layerState, machine) => { log("makeGetLayerMachine", layerId) - return layer.usesMachine ? machine : null + const layer = new Layer(layerState.type) + return layer.model.usesMachine ? machine : null }, ) } @@ -45,8 +45,9 @@ const makeGetLayerMachine = (layerId) => { const makeGetLayerFonts = (layerId) => { return createSelector( [getCachedSelector(makeGetLayer, layerId), getLoadedFonts], - (layer, fonts) => { + (layerState, fonts) => { log("makeGetLayerFonts", layerId) + const layer = new Layer(layerState.type) return layer.usesFonts ? fonts : null }, ) @@ -63,8 +64,8 @@ const makeGetLayerVertices = (layerId) => { (layerState, machine, fonts) => { const state = { shape: layerState, - machine: machine, - fonts: fonts, + machine, + fonts, } log("makeGetLayerVertices", layerId) const layer = new Layer(layerState.type) @@ -147,20 +148,21 @@ export const makeGetConnectorVertices = (startId, endId) => { } // transform a given list of vertices as needed to be displayed in a preview layer -const previewTransform = (layer, vertices) => { - const konvaScale = layer.autosize ? 5 : 1 // our transformer is 5 times bigger than the actual starting shape - const konvaDeltaX = ((konvaScale - 1) / 2) * layer.startingWidth - const konvaDeltaY = ((konvaScale - 1) / 2) * layer.startingHeight +const previewTransform = (layerState, vertices) => { + const konvaScale = 1 //layer.model.autosize ? 5 : 1 // our transformer is 5 times bigger than the actual starting shape + const konvaDeltaX = ((konvaScale - 1) / 2) * layerState.width + const konvaDeltaY = ((konvaScale - 1) / 2) * layerState.height return vertices.map((vertex) => { // store original coordinates before transforming let previewVertex = offset( - rotate(offset(vertex, -layer.x, -layer.y), layer.rotation), + rotate(offset(vertex, -layerState.x, -layerState.y), layerState.rotation), konvaDeltaX, -konvaDeltaY, ) previewVertex.origX = vertex.x previewVertex.origY = vertex.y + return previewVertex }) } @@ -304,7 +306,7 @@ export const getSliderBounds = createSelector( } } - return { start: start, end: end } + return { start, end } }, ) @@ -333,27 +335,33 @@ export const getSliderColors = createSelector( // used by the preview window; reverses rotation and offsets because they are // re-added by Konva transformer. export const makeGetPreviewTrackVertices = (layerId) => { - return createSelector(getCachedSelector(makeGetLayer, layerId), (layer) => { - log("makeGetPreviewTrackVertices", layerId) - // const numLoops = layer.numLoops - const konvaScale = layer.autosize ? 5 : 1 // our transformer is 5 times bigger than the actual starting shape - const konvaDeltaX = ((konvaScale - 1) / 2) * layer.startingWidth - const konvaDeltaY = ((konvaScale - 1) / 2) * layer.startingHeight - let trackVertices = [] - - // TODO: re-implement display of track vertices - // for (var i=0; i { - return offset( - rotate(offset(vertex, -layer.x, -layer.y), layer.rotation), - konvaDeltaX, - -konvaDeltaY, - ) - }) - }) + return createSelector( + getCachedSelector(makeGetLayer, layerId), + (layerState) => { + log("makeGetPreviewTrackVertices", layerId) + // const numLoops = layer.numLoops + const konvaScale = 1 //layer.model.autosize ? 5 : 1 // our transformer is 5 times bigger than the actual starting shape + const konvaDeltaX = ((konvaScale - 1) / 2) * layerState.width + const konvaDeltaY = ((konvaScale - 1) / 2) * layerState.height + let trackVertices = [] + + // TODO: re-implement display of track vertices + // for (var i=0; i { + return offset( + rotate( + offset(vertex, -layerState.x, -layerState.y), + layerState.rotation, + ), + konvaDeltaX, + -konvaDeltaY, + ) + }) + }, + ) } diff --git a/src/features/preview/PreviewConnector.js b/src/features/preview/PreviewConnector.js index 769fe282..8f7d4636 100644 --- a/src/features/preview/PreviewConnector.js +++ b/src/features/preview/PreviewConnector.js @@ -1,11 +1,16 @@ -import React from 'react' -import { useSelector, shallowEqual } from 'react-redux' -import { Shape } from 'react-konva' -import { makeGetConnectorVertices, getSliderBounds, getSliderColors, getVertexOffsets } from '../machine/selectors' -import { getPreview, getLayers } from '../store/selectors' -import { getCurrentLayerState, makeGetLayer } from '../layers/selectors' -import { getCachedSelector } from '../store/selectors' -import PreviewHelper from './PreviewHelper' +import React from "react" +import { useSelector, shallowEqual } from "react-redux" +import { Shape } from "react-konva" +import { + makeGetConnectorVertices, + getSliderBounds, + getSliderColors, + getVertexOffsets, +} from "../machine/selectors" +import { getPreview, getLayers } from "../store/selectors" +import { getCurrentLayerState, makeGetLayer } from "../layers/selectors" +import { getCachedSelector } from "../store/selectors" +import PreviewHelper from "./PreviewHelper" // Renders a connector between two layers. const PreviewConnector = (ownProps) => { @@ -17,33 +22,42 @@ const PreviewConnector = (ownProps) => { // hooks, and the solution for now is to render the current layer instead. // https://react-redux.js.org/api/hooks#stale-props-and-zombie-children // It's quite likely there is a more elegant/proper way around this. - const startLayer = getCachedSelector(makeGetLayer, ownProps.startId)(state) || getCurrentLayerState(state) - const endLayer = getCachedSelector(makeGetLayer, ownProps.endId)(state) || getCurrentLayerState(state) - const vertices = startLayer === endLayer ? - [] : - getCachedSelector(makeGetConnectorVertices, startLayer.id, endLayer.id)(state) + const startLayer = + getCachedSelector(makeGetLayer, ownProps.startId)(state) || + getCurrentLayerState(state) + const endLayer = + getCachedSelector(makeGetLayer, ownProps.endId)(state) || + getCurrentLayerState(state) + const vertices = + startLayer === endLayer + ? [] + : getCachedSelector( + makeGetConnectorVertices, + startLayer.id, + endLayer.id, + )(state) const layers = getLayers(state) const preview = getPreview(state) return { layer: startLayer, - endLayer: endLayer, - vertices: vertices, + endLayer, + vertices, sliderValue: preview.sliderValue, selected: layers.selected, colors: getSliderColors(state), - offsetId: startLayer.id + '-connector', + offsetId: startLayer.id + "-connector", offsets: getVertexOffsets(state), - bounds: getSliderBounds(state) + bounds: getSliderBounds(state), } } const props = useSelector(mapStateToProps, shallowEqual) const helper = new PreviewHelper(props) - const selectedColor = 'yellow' - const unselectedColor = 'rgba(195, 214, 230, 0.65)' - const backgroundSelectedColor = '#6E6E00' - const backgroundUnselectedColor = 'rgba(195, 214, 230, 0.4)' + const selectedColor = "yellow" + const unselectedColor = "rgba(195, 214, 230, 0.65)" + const backgroundSelectedColor = "#6E6E00" + const backgroundUnselectedColor = "rgba(195, 214, 230, 0.4)" const isSliding = props.sliderValue !== 0 const isSelected = props.selected === ownProps.endId @@ -71,10 +85,12 @@ const PreviewConnector = (ownProps) => { context.stroke() context.beginPath() - for (let i=1; i { } } - helper.moveTo(context, props.vertices[i-1]) + helper.moveTo(context, props.vertices[i - 1]) helper.lineTo(context, props.vertices[i]) } context.stroke() @@ -93,13 +109,14 @@ const PreviewConnector = (ownProps) => { return ( - {!props.layer.dragging && !props.endLayer.dragging && - } + {!props.layer.dragging && !props.endLayer.dragging && ( + + )} ) } diff --git a/src/features/preview/PreviewHelper.js b/src/features/preview/PreviewHelper.js index b7a249ff..e9c64ba3 100644 --- a/src/features/preview/PreviewHelper.js +++ b/src/features/preview/PreviewHelper.js @@ -1,17 +1,17 @@ -import Victor from 'victor' +import Victor from "victor" // translates shape coordinates into pixel coordinates with a centered origin export default class PreviewHelper { constructor(props) { this.props = props - this.width = this.props.layer.startingWidth || 0 - this.height = this.props.layer.startingHeight || 0 + this.width = this.props.layer.width || 0 + this.height = this.props.layer.height || 0 } toPixels(vertex) { // y for pixels starts at the top, and goes down if (vertex) { - return new Victor(vertex.x + this.width/2, -vertex.y + this.height/2) + return new Victor(vertex.x + this.width / 2, -vertex.y + this.height / 2) } else { return new Victor(0, 0) } @@ -27,7 +27,7 @@ export default class PreviewHelper { context.lineTo(px.x, px.y) } - dot(context, vertex, radius=4, color='yellow') { + dot(context, vertex, radius = 4, color = "yellow") { const px = this.toPixels(vertex) context.arc(px.x, px.y, radius, 0, 2 * Math.PI, true) context.fillStyle = context.strokeStyle @@ -52,7 +52,7 @@ export default class PreviewHelper { context.beginPath() this.moveTo(context, sliderEnd) - context.strokeStyle = 'yellow' + context.strokeStyle = "yellow" this.dot(context, sliderEnd) context.stroke() @@ -69,9 +69,13 @@ export default class PreviewHelper { const x = (vertex.origX || vertex.x).toFixed(2) const y = (vertex.origY || vertex.y).toFixed(2) - context.fillStyle = 'white' - context.font = '10px Arial' - context.fillText('' + x + ', ' + y, vertex.x + 15 * signX, -vertex.y + 5 * signY) + context.fillStyle = "white" + context.font = "10px Arial" + context.fillText( + "" + x + ", " + y, + vertex.x + 15 * signX, + -vertex.y + 5 * signY, + ) } } } diff --git a/src/features/preview/PreviewLayer.js b/src/features/preview/PreviewLayer.js index 28d1d097..b214a984 100644 --- a/src/features/preview/PreviewLayer.js +++ b/src/features/preview/PreviewLayer.js @@ -1,14 +1,26 @@ -import React from 'react' -import { useSelector, useDispatch, shallowEqual } from 'react-redux' -import { Shape, Transformer } from 'react-konva' -import { makeGetPreviewTrackVertices, makeGetPreviewVertices, getSliderColors, - getVertexOffsets, getAllComputedVertices, getSliderBounds } from '../machine/selectors' -import { updateLayer } from '../layers/layersSlice' -import { getLayers, getPreview } from '../store/selectors' -import { getCurrentLayerState, makeGetLayerIndex, makeGetLayer, getNumVisibleLayers } from '../layers/selectors' -import { getCachedSelector } from '../store/selectors' -import { roundP } from '../../common/util' -import PreviewHelper from './PreviewHelper' +import React from "react" +import { useSelector, useDispatch, shallowEqual } from "react-redux" +import { Shape, Transformer } from "react-konva" +import { + makeGetPreviewTrackVertices, + makeGetPreviewVertices, + getSliderColors, + getVertexOffsets, + getAllComputedVertices, + getSliderBounds, +} from "../machine/selectors" +import { updateLayer } from "../layers/layersSlice" +import { getLayers, getPreview } from "../store/selectors" +import Layer from "@/features/layers/Layer" +import { + getCurrentLayerState, + makeGetLayerIndex, + makeGetLayer, + getNumVisibleLayers, +} from "../layers/selectors" +import { getCachedSelector } from "../store/selectors" +import { roundP } from "../../common/util" +import PreviewHelper from "./PreviewHelper" // Renders the shapes in the preview window and allows the user to interact with the shape. const PreviewLayer = (ownProps) => { @@ -21,45 +33,52 @@ const PreviewLayer = (ownProps) => { // https://react-redux.js.org/api/hooks#stale-props-and-zombie-children // It's quite likely there is a more elegant/proper way around this. const layers = getLayers(state) - const layer = getCachedSelector(makeGetLayer, ownProps.id)(state) || getCurrentLayerState(state) - const index = getCachedSelector(makeGetLayerIndex, layer.id)(state) + const layerState = + getCachedSelector(makeGetLayer, ownProps.id)(state) || + getCurrentLayerState(state) + const index = getCachedSelector(makeGetLayerIndex, layerState.id)(state) const numLayers = getNumVisibleLayers(state) const preview = getPreview(state) return { - layer: layer, + layer: layerState, start: index === 0, end: index === numLayers - 1, currentLayer: getCurrentLayerState(state), - trackVertices: getCachedSelector(makeGetPreviewTrackVertices, layer.id)(state), - vertices: getCachedSelector(makeGetPreviewVertices, layer.id)(state), + trackVertices: getCachedSelector( + makeGetPreviewTrackVertices, + layerState.id, + )(state), + vertices: getCachedSelector(makeGetPreviewVertices, layerState.id)(state), allVertices: getAllComputedVertices(state), selected: layers.selected, sliderValue: preview.sliderValue, showTrack: true, colors: getSliderColors(state), offsets: getVertexOffsets(state), - offsetId: layer.id, + offsetId: layerState.id, bounds: getSliderBounds(state), - markCoordinates: false // debug feature: set to true to see coordinates while drawing + markCoordinates: false, // debug feature: set to true to see coordinates while drawing } } const props = useSelector(mapStateToProps, shallowEqual) + const layer = new Layer(props.layer.type) + const model = layer.model const dispatch = useDispatch() - const startingWidth = props.layer.startingWidth - const startingHeight = props.layer.startingHeight - const selectedColor = 'yellow' - const unselectedColor = 'rgba(195, 214, 230, 0.65)' - const backgroundSelectedColor = '#6E6E00' - const backgroundUnselectedColor = 'rgba(195, 214, 230, 0.4)' + const width = props.layer.width + const height = props.layer.height + const selectedColor = "yellow" + const unselectedColor = "rgba(195, 214, 230, 0.65)" + const backgroundSelectedColor = "#6E6E00" + const backgroundUnselectedColor = "rgba(195, 214, 230, 0.4)" // our transformer is 5 times bigger than the actual starting shape, so we need // to account for it when drawing the preview; if you change this value, be sure // to change it in machine/selectors#getPreviewVertices,getPreviewTrackVertices - const konvaScale = props.layer.autosize ? 5 : 1 - const konvaSizeX = startingWidth * konvaScale - const konvaSizeY = startingHeight * konvaScale + const konvaScale = 1 //layer.model.autosize ? 5 : 1 + const konvaSizeX = width * konvaScale + const konvaSizeY = height * konvaScale const isSelected = props.selected === ownProps.id const isSliding = props.sliderValue !== 0 const helper = new PreviewHelper(props) @@ -77,10 +96,11 @@ const PreviewLayer = (ownProps) => { context.stroke() context.beginPath() - for (let i=1; i { } } - helper.moveTo(context, props.vertices[i-1]) + helper.moveTo(context, props.vertices[i - 1]) helper.lineTo(context, props.vertices[i]) } context.stroke() @@ -102,13 +122,13 @@ const PreviewLayer = (ownProps) => { const end = props.vertices[props.vertices.length - 1] context.beginPath() - context.strokeStyle = 'green' + context.strokeStyle = "green" helper.dot(context, start, props.start ? 5 : 3) helper.markOriginalCoordinates(context, start) if (end) { context.beginPath() - context.strokeStyle = 'red' + context.strokeStyle = "red" helper.dot(context, end, props.end ? 5 : 3) helper.markOriginalCoordinates(context, end) } @@ -118,9 +138,9 @@ const PreviewLayer = (ownProps) => { function drawTrackVertices(context) { context.beginPath() context.lineWidth = 4.0 - context.strokeStyle = 'green' + context.strokeStyle = "green" helper.moveTo(context, props.trackVertices[0]) - for (let i=0; i { const trRef = React.createRef() React.useEffect(() => { - if (props.layer.visible && isSelected && props.layer.canChangeSize) { + if (props.layer.visible && isSelected && model.canChangeSize(props.layer)) { // we need to attach transformer manually trRef.current.nodes([shapeRef.current]) trRef.current.getLayer().batchDraw() } - }, [isSelected, props.layer, props.currentLayer.canMove, shapeRef, trRef]) + }, [isSelected, props.layer, model.canMove, shapeRef, trRef]) return ( - {props.layer.visible && { - onChange({dragging: true}) - }} - onDragEnd={e => { - onChange({ - dragging: false, - x: roundP(e.target.x(), 0), - y: roundP(-e.target.y(), 0) - }) - }} - onTransformStart={e => { - onChange({dragging: true}) - }} - onTransformEnd={e => { - const node = shapeRef.current - const scaleX = node.scaleX() - const scaleY = node.scaleY() - - // we will reset it back - node.scaleX(1) - node.scaleY(1) - - onChange({ - dragging: false, - startingWidth: roundP(Math.max(5, props.layer.startingWidth * scaleX), 0), - startingHeight: roundP(Math.max(5, props.layer.startingHeight * scaleY), 0), - rotation: roundP(node.rotation(), 0) - }) - }} - />} - {props.layer.visible && isSelected && props.layer.canChangeSize && ( - { + onChange({ dragging: true }) + }} + onDragEnd={(e) => { + onChange({ + dragging: false, + x: roundP(e.target.x(), 0), + y: roundP(-e.target.y(), 0), + }) + }} + onTransformStart={(e) => { + onChange({ dragging: true }) + }} + onTransformEnd={(e) => { + const node = shapeRef.current + const scaleX = node.scaleX() + const scaleY = node.scaleY() + + // we will reset it back + node.scaleX(1) + node.scaleY(1) + + onChange({ + dragging: false, + width: roundP(Math.max(5, props.layer.width * scaleX), 0), + height: roundP(Math.max(5, props.layer.height * scaleY), 0), + rotation: roundP(node.rotation(), 0), + }) + }} /> )} + {props.layer.visible && + isSelected && + model.canChangeSize(props.layer) && ( + + )} ) } diff --git a/src/features/preview/PreviewWindow.js b/src/features/preview/PreviewWindow.js index db569f1d..bd8df912 100644 --- a/src/features/preview/PreviewWindow.js +++ b/src/features/preview/PreviewWindow.js @@ -1,14 +1,19 @@ -import React, { Component } from 'react' -import { connect, ReactReduxContext, Provider } from 'react-redux' -import { Stage, Layer, Circle, Rect } from 'react-konva' -import throttle from 'lodash/throttle' -import { setPreviewSize, updatePreview } from './previewSlice' -import { updateLayer } from '../layers/layersSlice' -import { getMachine, getLayers, getPreview } from '../store/selectors' -import { getCurrentLayerState, getKonvaLayerIds, getVisibleNonEffectIds, isDragging } from '../layers/selectors' -import { roundP } from '../../common/util' -import PreviewLayer from './PreviewLayer' -import PreviewConnector from './PreviewConnector' +import React, { Component } from "react" +import { connect, ReactReduxContext, Provider } from "react-redux" +import { Stage, Layer, Circle, Rect } from "react-konva" +import throttle from "lodash/throttle" +import { setPreviewSize, updatePreview } from "./previewSlice" +import { updateLayer } from "../layers/layersSlice" +import { getMachine, getLayers, getPreview } from "../store/selectors" +import { + getCurrentLayerState, + getKonvaLayerIds, + getVisibleNonEffectIds, + isDragging, +} from "../layers/selectors" +import { roundP } from "../../common/util" +import PreviewLayer from "./PreviewLayer" +import PreviewConnector from "./PreviewConnector" const mapStateToProps = (state, ownProps) => { const layers = getLayers(state) @@ -16,7 +21,7 @@ const mapStateToProps = (state, ownProps) => { const machine = getMachine(state) return { - layers: layers, + layers, currentLayer: getCurrentLayerState(state), konvaIds: getKonvaLayerIds(state), layerIds: getVisibleNonEffectIds(state), @@ -28,7 +33,7 @@ const mapStateToProps = (state, ownProps) => { maxY: machine.maxY, maxRadius: machine.maxRadius, canvasWidth: preview.canvasWidth, - canvasHeight: preview.canvasHeight + canvasHeight: preview.canvasHeight, } } @@ -42,17 +47,25 @@ const mapDispatchToProps = (dispatch, ownProps) => { }, onLayerChange: (attrs) => { dispatch(updateLayer(attrs)) - } + }, } } // Contains the preview window, and any parameters for the machine. class PreviewWindow extends Component { componentDidMount() { - const wrapper = document.getElementById('preview-wrapper') + const wrapper = document.getElementById("preview-wrapper") - this.throttledResize = throttle(this.resize, 200, {trailing: true}).bind(this) - window.addEventListener('resize', () => { this.throttledResize(wrapper) }, false) + this.throttledResize = throttle(this.resize, 200, { trailing: true }).bind( + this, + ) + window.addEventListener( + "resize", + () => { + this.throttledResize(wrapper) + }, + false, + ) setTimeout(() => { this.visible = true this.resize(wrapper) @@ -60,78 +73,122 @@ class PreviewWindow extends Component { } resize(wrapper) { - const width = parseInt(getComputedStyle(wrapper).getPropertyValue('width')) - const height = parseInt(getComputedStyle(wrapper).getPropertyValue('height')) + const width = parseInt(getComputedStyle(wrapper).getPropertyValue("width")) + const height = parseInt( + getComputedStyle(wrapper).getPropertyValue("height"), + ) - if (this.props.canvasWidth !== width || this.props.canvasHeight !== height) { - this.props.onResize({width: width, height: height}) + if ( + this.props.canvasWidth !== width || + this.props.canvasHeight !== height + ) { + this.props.onResize({ width, height }) } } render() { - const {minX, minY, maxX, maxY} = this.props + const { minX, minY, maxX, maxY } = this.props const radius = this.props.maxRadius const scale = this.relativeScale(this.props) const reduceScale = 0.9 const width = this.props.use_rect ? maxX - minX : radius * 2 const height = this.props.use_rect ? maxY - minY : radius * 2 - const visibilityClass = `preview-wrapper ${this.visible ? 'd-flex align-items-center' : 'd-none'}` + const visibilityClass = `preview-wrapper ${ + this.visible ? "d-flex align-items-center" : "d-none" + }` // define Konva clip functions that will let us clip vertices not bound by // machine limits when dragging, and produce a visually seamless experience. - const clipCircle = ctx => { - ctx.arc(0, 0, radius, 0, Math.PI * 2, false) + const clipCircle = (ctx) => { + ctx.arc(0, 0, radius, 0, Math.PI * 2, false) } - const clipRect = ctx => { - ctx.rect(-width/2, -height/2, width, height) + const clipRect = (ctx) => { + ctx.rect(-width / 2, -height / 2, width, height) } - const clipFunc = this.props.dragging ? (this.props.use_rect ? clipRect : clipCircle) : null + const clipFunc = this.props.dragging + ? this.props.use_rect + ? clipRect + : clipCircle + : null return ( // the consumer wrapper is needed to pass the store down to our shape // which is not our usual React Component - {({store}) => ( - ( + { + offsetX={(-width / 2) * (1 / reduceScale)} + offsetY={(-height / 2) * (1 / reduceScale)} + onWheel={(e) => { e.evt.preventDefault() if (Math.abs(e.evt.deltaY) > 0) { this.props.onLayerChange({ - startingWidth: this.scaleByWheel(this.props.currentLayer.startingWidth, e.evt.deltaY), - startingHeight: this.scaleByWheel(this.props.currentLayer.startingHeight, e.evt.deltaY), - id: this.props.currentLayer.id + width: this.scaleByWheel( + this.props.currentLayer.width, + e.evt.deltaY, + ), + height: this.scaleByWheel( + this.props.currentLayer.height, + e.evt.deltaY, + ), + id: this.props.currentLayer.id, }) } }} - > + > - {!this.props.use_rect && } - {this.props.use_rect && } - {this.props.konvaIds.map((id, i) => { - const idx = this.props.layerIds.findIndex(layerId => layerId === id) - const nextId = idx !== -1 && idx < this.props.layerIds.length - 1 ? this.props.layerIds[idx + 1] : null - return ( - [ - nextId && , - - ].filter(e => e !== null) - ) - }).flat()} + {!this.props.use_rect && ( + + )} + {this.props.use_rect && ( + + )} + {this.props.konvaIds + .map((id, i) => { + const idx = this.props.layerIds.findIndex( + (layerId) => layerId === id, + ) + const nextId = + idx !== -1 && idx < this.props.layerIds.length - 1 + ? this.props.layerIds[idx + 1] + : null + return [ + nextId && ( + + ), + , + ].filter((e) => e !== null) + }) + .flat()} @@ -155,12 +212,12 @@ class PreviewWindow extends Component { scaleByWheel(size, deltaY) { const sign = Math.sign(deltaY) - const scale = 1 + Math.log(Math.abs(deltaY))/30 * sign + const scale = 1 + (Math.log(Math.abs(deltaY)) / 30) * sign let newSize = Math.max(roundP(size * scale, 0), 1) if (newSize === size) { // If the log scaled value isn't big enough to move the scale. - newSize = Math.max(sign+size, 1) + newSize = Math.max(sign + size, 1) } return newSize diff --git a/src/models/Circle.js b/src/models/Circle.js index 90ba6e2e..e7a27eb3 100644 --- a/src/models/Circle.js +++ b/src/models/Circle.js @@ -1,33 +1,30 @@ -import Victor from 'victor' -import Shape, { shapeOptions } from './Shape' +import Victor from "victor" +import Model from "./Model" const options = { - ...shapeOptions, - ...{ - circleLobes: { - title: 'Number of lobes', - min: 1 - }, - circleDirection: { - title: 'Direction', - type: 'togglebutton', - choices: ['clockwise', 'counterclockwise'] - } - } + circleLobes: { + title: "Number of lobes", + min: 1, + }, + circleDirection: { + title: "Direction", + type: "togglebutton", + choices: ["clockwise", "counterclockwise"], + }, } -export default class Circle extends Shape { +export default class Circle extends Model { constructor() { - super('Circle') + super('circle') + this.label = "Circle" } getInitialState() { return { ...super.getInitialState(), ...{ - type: 'circle', circleLobes: 1, - circleDirection: 'clockwise' + circleDirection: "clockwise", }, } } @@ -35,15 +32,25 @@ export default class Circle extends Shape { getVertices(state) { let points = [] - if (state.shape.circleDirection === 'counterclockwise') { - for (let i=128; i>=0; i--) { - let angle = Math.PI * 2.0 / 128.0 * i - points.push(new Victor(Math.cos(angle), Math.sin(state.shape.circleLobes * angle)/state.shape.circleLobes)) + if (state.shape.circleDirection === "counterclockwise") { + for (let i = 128; i >= 0; i--) { + let angle = ((Math.PI * 2.0) / 128.0) * i + points.push( + new Victor( + Math.cos(angle), + Math.sin(state.shape.circleLobes * angle) / state.shape.circleLobes, + ), + ) } } else { - for (let i=0; i<=128; i++) { - let angle = Math.PI * 2.0 / 128.0 * i - points.push(new Victor(Math.cos(angle), Math.sin(state.shape.circleLobes * angle)/state.shape.circleLobes)) + for (let i = 0; i <= 128; i++) { + let angle = ((Math.PI * 2.0) / 128.0) * i + points.push( + new Victor( + Math.cos(angle), + Math.sin(state.shape.circleLobes * angle) / state.shape.circleLobes, + ), + ) } } diff --git a/src/models/Effect.js b/src/models/Effect.js index 8449830f..86834560 100644 --- a/src/models/Effect.js +++ b/src/models/Effect.js @@ -4,7 +4,6 @@ export default class Effect extends Model { constructor() { super() this.shouldCache = false - this.canChangeSize = true this.autosize = false this.effect = true } diff --git a/src/models/Epicycloid.js b/src/models/Epicycloid.js index 4fa4aaea..56f76598 100644 --- a/src/models/Epicycloid.js +++ b/src/models/Epicycloid.js @@ -1,36 +1,33 @@ -import Victor from 'victor' -import Shape, { shapeOptions } from './Shape' -import { reduce } from '@/common/util' +import Victor from "victor" +import Model from "./Model" +import { reduce } from "@/common/util" const options = { - ...shapeOptions, - ...{ - epicycloidA: { - title: "Large circle radius", - min: 1 - }, - epicycloidB: { - title: "Small circle radius", - min: 1 - }, - } + epicycloidA: { + title: "Large circle radius", + min: 1, + }, + epicycloidB: { + title: "Small circle radius", + min: 1, + }, } -export default class Epicycloid extends Shape { +export default class Epicycloid extends Model { constructor() { - super('Clover') - this.link = 'http://mathworld.wolfram.com/Epicycloid.html' - this.linkText = 'Wolfram Mathworld' + super('epicycloid') + this.label = "Clover" + this.link = "http://mathworld.wolfram.com/Epicycloid.html" + this.linkText = "Wolfram Mathworld" } getInitialState() { return { ...super.getInitialState(), ...{ - type: 'epicycloid', epicycloidA: 4, epicycloidB: 1, - } + }, } } @@ -41,16 +38,16 @@ export default class Epicycloid extends Shape { let reduced = reduce(a, b) a = reduced[0] b = reduced[1] - let rotations = Number.isInteger(a/b) ? 1 : b - let scale = 1/(a + 2*b) + let rotations = Number.isInteger(a / b) ? 1 : b + let scale = 1 / (a + 2 * b) - for (let i=0; i<128*rotations; i++) { - let angle = Math.PI * 2.0 / 128.0 * i + for (let i = 0; i < 128 * rotations; i++) { + let angle = ((Math.PI * 2.0) / 128.0) * i points.push( new Victor( (a + b) * Math.cos(angle) - b * Math.cos(((a + b) / b) * angle), - (a + b) * Math.sin(angle) - b * Math.sin(((a + b) / b) * angle) - ).multiply({x: scale, y: scale}) + (a + b) * Math.sin(angle) - b * Math.sin(((a + b) / b) * angle), + ).multiply({ x: scale, y: scale }), ) } diff --git a/src/models/FancyText.js b/src/models/FancyText.js index 6c867057..bf93deb0 100644 --- a/src/models/FancyText.js +++ b/src/models/FancyText.js @@ -1,65 +1,71 @@ -import Victor from 'victor' -import Shape, { shapeOptions } from './Shape' -import { subsample, centerOnOrigin, maxY, minY, horizontalAlign, findBounds, nearestVertex, findMinimumVertex } from '@/common/geometry' -import { arrayRotate } from '@/common/util' -import { pointsOnPath } from 'points-on-path' -import { getFont, supportedFonts } from '@/features/fonts/fontsSlice' -import { getMachineInstance } from '@/features/machine/computer' -import pointInPolygon from 'point-in-polygon' +import Victor from "victor" +import Model from "./Model" +import { + subsample, + centerOnOrigin, + maxY, + minY, + horizontalAlign, + findBounds, + nearestVertex, + findMinimumVertex, +} from "@/common/geometry" +import { arrayRotate } from "@/common/util" +import { pointsOnPath } from "points-on-path" +import { getFont, supportedFonts } from "@/features/fonts/fontsSlice" +import { getMachineInstance } from "@/features/machine/computer" +import pointInPolygon from "point-in-polygon" const MIN_SPACING_MULTIPLIER = 1.2 -const SPECIAL_CHILDREN = ['i', 'j', '?'] +const SPECIAL_CHILDREN = ["i", "j", "?"] const options = { - ...shapeOptions, - ...{ - fancyText: { - title: 'Text', - type: 'textarea', + fancyText: { + title: "Text", + type: "textarea", + }, + fancyFont: { + title: "Font", + type: "dropdown", + choices: () => { + return Object.values(supportedFonts) }, - fancyFont: { - title: 'Font', - type: 'dropdown', - choices: () => { - return Object.values(supportedFonts) - }, - }, - fancyLineSpacing: { - title: 'Line spacing', - type: 'number', - step: 0.1 - }, - fancyConnectLines: { - title: 'Connect rows', - type: 'togglebutton', - choices: [ 'inside', 'outside' ] - }, - fancyAlignment: { - title: 'Alignment', - type: 'togglebutton', - choices: [ 'left', 'center', 'right' ] - } - } + }, + fancyLineSpacing: { + title: "Line spacing", + type: "number", + step: 0.1, + }, + fancyConnectLines: { + title: "Connect rows", + type: "togglebutton", + choices: ["inside", "outside"], + }, + fancyAlignment: { + title: "Alignment", + type: "togglebutton", + choices: ["left", "center", "right"], + }, } -export default class FancyText extends Shape { +export default class FancyText extends Model { constructor() { - super('Fancy Text') + super('fancyText') + this.label = "Fancy Text" + this.usesMachine = true + this.usesFonts = true } getInitialState() { return { ...super.getInitialState(), ...{ - type: 'fancy_text', - fancyText: 'Sandify', - fancyFont: 'Garamond', - fancyAlignment: 'left', - fancyConnectLines: 'inside', + fancyText: "Sandify", + fancyFont: "Garamond", + fancyAlignment: "left", + fancyConnectLines: "inside", fancyLineSpacing: 1.0, - usesMachine: true, - usesFonts: true - } + }, } } @@ -67,16 +73,19 @@ export default class FancyText extends Shape { const font = getFont(state.shape.fancyFont) if (font) { - let words = state.shape.fancyText.split("\n").filter(word => word.length > 0) - if (words.length === 0) { return [new Victor(0,0)] } + let words = state.shape.fancyText + .split("\n") + .filter((word) => word.length > 0) + if (words.length === 0) { + return [new Victor(0, 0)] + } - words = words.map(word => this.drawWord(word, font, state)) + words = words.map((word) => this.drawWord(word, font, state)) let { offsets, vertices } = this.addVerticalSpacing(words, font, state) horizontalAlign(vertices, state.shape.fancyAlignment) this.centerOnOrigin(vertices) return this.connectWords(vertices, offsets, state).flat() - } else { return [new Victor(0, 0)] } @@ -84,7 +93,7 @@ export default class FancyText extends Shape { centerOnOrigin(vertices) { const bounds = findBounds(vertices.flat()) - vertices.forEach(vs => centerOnOrigin(vs, bounds)) + vertices.forEach((vs) => centerOnOrigin(vs, bounds)) } // use the specified connection method to draw lines to connect each row in a multi-row phrase @@ -92,7 +101,7 @@ export default class FancyText extends Shape { const machine = getMachineInstance([], state.machine) let newVertices = [] - for (let i=0; i 0) { @@ -100,22 +109,34 @@ export default class FancyText extends Shape { const next = currVertices[0] const prev = prevVertices[prevVertices.length - 1] - if (state.shape.fancyConnectLines === 'outside') { + if (state.shape.fancyConnectLines === "outside") { // connect the two rows along the perimeter - const clipped = machine.clipLine(new Victor(prev.x - machine.sizeX*2, prev.y), - new Victor(prev.x + machine.sizeX*2, prev.y)) - const clipped2 = machine.clipLine(new Victor(next.x - machine.sizeX*2, next.y), - new Victor(next.x + machine.sizeX*2, next.y)) + const clipped = machine.clipLine( + new Victor(prev.x - machine.sizeX * 2, prev.y), + new Victor(prev.x + machine.sizeX * 2, prev.y), + ) + const clipped2 = machine.clipLine( + new Victor(next.x - machine.sizeX * 2, next.y), + new Victor(next.x + machine.sizeX * 2, next.y), + ) newVertices.push(clipped[1]) newVertices.push(machine.tracePerimeter(clipped[1], clipped2[0])) newVertices.push(clipped2[0]) } else { // connect the two rows by drawing a horizontal line in the middle of the two rows - const lowest = prevVertices[findMinimumVertex(null, prevVertices, (val, v) => v.y)] - const highest = currVertices[findMinimumVertex(null, currVertices, (val, v) => -v.y)] - newVertices.push(new Victor(prev.x, lowest.y - (lowest.y - highest.y)/2)) - newVertices.push(new Victor(next.x, lowest.y - (lowest.y - highest.y)/2)) + const lowest = + prevVertices[findMinimumVertex(null, prevVertices, (val, v) => v.y)] + const highest = + currVertices[ + findMinimumVertex(null, currVertices, (val, v) => -v.y) + ] + newVertices.push( + new Victor(prev.x, lowest.y - (lowest.y - highest.y) / 2), + ) + newVertices.push( + new Victor(next.x, lowest.y - (lowest.y - highest.y) / 2), + ) } } newVertices.push(currVertices) @@ -129,24 +150,26 @@ export default class FancyText extends Shape { let yOffset = 0 const offsets = [] - const letterA = this.drawWord('A', font, state) + const letterA = this.drawWord("A", font, state) const minHeight = (maxY(letterA) - minY(letterA)) * MIN_SPACING_MULTIPLIER - for (let i=0; i new Victor(v.x, v.y - tempOffset))) + newVertices.push(currWord.map((v) => new Victor(v.x, v.y - tempOffset))) // offset height of each word by a fixed amount - const offset = Math.max(maxY(currWord) - minY(currWord), minHeight) + state.shape.fancyLineSpacing + const offset = + Math.max(maxY(currWord) - minY(currWord), minHeight) + + state.shape.fancyLineSpacing yOffset += offset offsets.push(offset) } return { vertices: newVertices, - offsets: offsets + offsets, } } @@ -158,16 +181,17 @@ export default class FancyText extends Shape { const sortedPaths = this.buildOrderedPaths(word, font) // draw paths, and connect them together - for (let i=0; i v.x) : - start = findMinimumVertex(null, points, (val, v) => -v.y) + start = + state.shape.fancyConnectLines === "outside" + ? findMinimumVertex(null, points, (val, v) => v.x) + : (start = findMinimumVertex(null, points, (val, v) => -v.y)) } // draw path @@ -177,9 +201,13 @@ export default class FancyText extends Shape { // draw children if (childPaths) { - for (let j=0; j -v.x) : - findMinimumVertex(null, loop, (val, v) => v.y) + const end = + state.shape.fancyConnectLines === "outside" + ? findMinimumVertex(null, loop, (val, v) => -v.x) + : findMinimumVertex(null, loop, (val, v) => v.y) pointsArr.push(this.shortestPathAroundLoop(start, end, loop)) } } @@ -224,7 +257,7 @@ export default class FancyText extends Shape { const segment = this.shortestPathAroundLoop(start, end, points) segment.push(new Victor(next.x, next.y)) - return { segment: segment, end: end, nextStart: nextStart } + return { segment, end, nextStart } } // renders text using an OpenType font and converts it to points we can draw @@ -238,7 +271,10 @@ export default class FancyText extends Shape { const path = font.getPath(text, x, y, fSize).toPathData() return pointsOnPath(path, tolerance, distance).map((path) => { - return subsample(path.map(pt => new Victor(pt[0], -pt[1])), .2) + return subsample( + path.map((pt) => new Victor(pt[0], -pt[1])), + 0.2, + ) }) } @@ -255,7 +291,10 @@ export default class FancyText extends Shape { } else { if (Math.abs(start - end) > loop.length / 2) { // go the other way around - return loop.slice(end, loop.length - 1).concat(loop.slice(0, start + 1)).reverse() + return loop + .slice(end, loop.length - 1) + .concat(loop.slice(0, start + 1)) + .reverse() } else { return loop.slice(start, end + 1) } @@ -266,10 +305,12 @@ export default class FancyText extends Shape { const graph = {} const points = this.convertTextToPoints(word, font) const childMap = this.findExternalChildren(word, font) - const polygons = points.map(pts => pts.map((pt) => [pt.x, pt.y])) + const polygons = points.map((pts) => pts.map((pt) => [pt.x, pt.y])) - for (let i=0; i { + idx = polygons.findIndex((polygon) => { return pointInPolygon(samplePoint, polygon) }) } @@ -300,7 +341,7 @@ export default class FancyText extends Shape { const childMap = {} let pos = 0 - for (let i=0; i 1) { @@ -319,7 +360,6 @@ export default class FancyText extends Shape { pos += paths.length } - return childMap } @@ -329,13 +369,16 @@ export default class FancyText extends Shape { // a possible (fully contained) child paths const graph = this.buildGraph(word, font) - return Object.keys(graph).sort( - (leftIndex, rightIndex) => { + return Object.keys(graph) + .sort((leftIndex, rightIndex) => { const leftPoints = graph[leftIndex].points const rightPoints = graph[rightIndex].points - return Math.min(...leftPoints.map(pt => pt.x)) - Math.min(...rightPoints.map(pt => pt.x)) + return ( + Math.min(...leftPoints.map((pt) => pt.x)) - + Math.min(...rightPoints.map((pt) => pt.x)) + ) }) - .map(key => graph[key]) + .map((key) => graph[key]) } getOptions() { diff --git a/src/models/FileImport.js b/src/models/FileImport.js index 1faed763..89fefd5f 100644 --- a/src/models/FileImport.js +++ b/src/models/FileImport.js @@ -1,60 +1,58 @@ -import Victor from 'victor' -import Shape, { shapeOptions } from './Shape' +import Victor from "victor" +import Model from "./Model" const options = { - ...shapeOptions, - ...{ - fileName: { - title: 'From file:', - type: 'inputText', - plainText: 'true' - }, - aspectRatio: { - title: 'Aspect Ratio', - type: 'checkbox' - }, - comments: { - title: 'Comments', - type: 'comments' - }, - } + fileName: { + title: "From file:", + type: "inputText", + plainText: "true", + }, + aspectRatio: { + title: "Aspect Ratio", + type: "checkbox", + }, + comments: { + title: "Comments", + type: "comments", + }, } -export default class FileImport extends Shape { +export default class FileImport extends Model { constructor() { - super('FileImport') + super('fileImport') + this.label = "FileImport" + this.usesMachine = true + this.selectGroup = "import" } getInitialState(importProps) { return { ...super.getInitialState(), ...{ - type: 'file_import', aspectRatio: true, originalAspectRatio: 1.0, vertices: [], comments: [], - selectGroup: 'import', - usesMachine: true, }, - ...(importProps === undefined ? {} : { - fileName: importProps.fileName, - vertices: importProps.vertices, - originalAspectRatio: importProps.originalAspectRatio, - comments: importProps.comments - }) + ...(importProps === undefined + ? {} + : { + fileName: importProps.fileName, + vertices: importProps.vertices, + originalAspectRatio: importProps.originalAspectRatio, + comments: importProps.comments, + }), } } getVertices(state) { - if (state.shape.vertices.length < 1) - { + if (state.shape.vertices.length < 1) { // During initialization, this function gets called, but the machine isn't created right yet. return [new Victor(0.0, 0.0), new Victor(0.0, 0.1)] } - let x_scale = (state.machine.maxX - state.machine.minX)/2.0 * 0.1 - let y_scale = (state.machine.maxY - state.machine.minY)/2.0 * 0.1 + let x_scale = (state.machine.maxX - state.machine.minX) / 2.0 + let y_scale = (state.machine.maxY - state.machine.minY) / 2.0 if (!state.machine.rectangular) { x_scale = y_scale = state.machine.maxRadius * 0.1 @@ -63,14 +61,16 @@ export default class FileImport extends Shape { if (state.shape.aspectRatio) { const machine_aspect_ratio = y_scale / x_scale if (state.shape.originalAspectRatio > machine_aspect_ratio) { - x_scale = x_scale / state.shape.originalAspectRatio * machine_aspect_ratio + x_scale = + (x_scale / state.shape.originalAspectRatio) * machine_aspect_ratio } else { - y_scale = y_scale * state.shape.originalAspectRatio / machine_aspect_ratio + y_scale = + (y_scale * state.shape.originalAspectRatio) / machine_aspect_ratio } } - return state.shape.vertices.map( (vertex) => { - return Victor(vertex.x * x_scale, vertex.y * y_scale) + return state.shape.vertices.map((vertex) => { + return new Victor(vertex.x * x_scale, vertex.y * y_scale) }) } diff --git a/src/models/Freeform.js b/src/models/Freeform.js index 6d5921d2..cfa41fa3 100644 --- a/src/models/Freeform.js +++ b/src/models/Freeform.js @@ -1,37 +1,39 @@ -import Victor from 'victor' -import Shape, { shapeOptions } from './Shape' +import Victor from "victor" +import Model from "./Model" const options = { ...shapeOptions, ...{ freeformPoints: { - title: 'Points', - type: 'input' - } - } + title: "Points", + type: "input", + }, + }, } -export default class Freeform extends Shape { +export default class Freeform extends Model { constructor() { - super('Freeform') + super("freeform") + this.startingWidth = 50 + this.startingHeight = 50 + } + + canChangeHeight(state) { + return false } getInitialState() { return { ...super.getInitialState(), ...{ - type: 'freeform', - freeformPoints: '-1,-1;-1,1;1,1', - canChangeHeight: false, - startingWidth: 50, - startingHeight: 50 + freeformPoints: "-1,-1;-1,1;1,1", }, } } getVertices(state) { - return state.shape.freeformPoints.split(';').map((pair) => { - const coordinates = pair.split(',') + return state.shape.freeformPoints.split(";").map((pair) => { + const coordinates = pair.split(",") return new Victor(coordinates[0], coordinates[1]) }) } diff --git a/src/models/Heart.js b/src/models/Heart.js index aafda133..1e072b5f 100644 --- a/src/models/Heart.js +++ b/src/models/Heart.js @@ -1,31 +1,37 @@ -import Victor from 'victor' -import Shape from './Shape' +import Victor from "victor" +import Model from "./Model" -export default class Heart extends Shape { +export default class Heart extends Model { constructor() { - super('Heart') + super('heart') + this.label = "Heart" } getInitialState() { return { ...super.getInitialState(), ...{ - type: 'heart', - } + // no custom attributes + }, } } getVertices(state) { - let points = [] - for (let i=0; i<128; i++) { - let angle = Math.PI * 2.0 / 128.0 * i + const points = [] + for (let i = 0; i < 128; i++) { + let angle = ((Math.PI * 2.0) / 128.0) * i let scale = 0.9 // heart equation from: http://mathworld.wolfram.com/HeartCurve.html - points.push(new Victor(scale * 1.0 * Math.pow(Math.sin(angle), 3), - scale * (13.0/16.0 * Math.cos(angle) + - -5.0/16.0 * Math.cos(2.0 * angle) + - -2.0/16.0 * Math.cos(3.0 * angle) + - -1.0/16.0 * Math.cos(4.0 * angle)))) + points.push( + new Victor( + scale * 1.0 * Math.pow(Math.sin(angle), 3), + scale * + ((13.0 / 16.0) * Math.cos(angle) + + (-5.0 / 16.0) * Math.cos(2.0 * angle) + + (-2.0 / 16.0) * Math.cos(3.0 * angle) + + (-1.0 / 16.0) * Math.cos(4.0 * angle)), + ), + ) } return points } diff --git a/src/models/Hypocycloid.js b/src/models/Hypocycloid.js index e688e891..1b219251 100644 --- a/src/models/Hypocycloid.js +++ b/src/models/Hypocycloid.js @@ -1,36 +1,33 @@ -import Victor from 'victor' -import Shape, { shapeOptions } from './Shape' -import { reduce } from '@/common/util' +import Victor from "victor" +import { reduce } from "@/common/util" +import Model from "./Model" const options = { - ...shapeOptions, - ...{ - hypocycloidA: { - title: 'Large circle radius', - min: 1 - }, - hypocycloidB: { - title: 'Small circle radius', - min: 1 - }, - } + hypocycloidA: { + title: "Large circle radius", + min: 1, + }, + hypocycloidB: { + title: "Small circle radius", + min: 1, + }, } -export default class Star extends Shape { +export default class Star extends Model { constructor() { - super('Web') - this.link = 'http://mathworld.wolfram.com/Hypocycloid.html' - this.linkText = 'Wolfram Mathworld' + super('hypocycloid') + this.label = "Web" + this.link = "http://mathworld.wolfram.com/Hypocycloid.html" + this.linkText = "Wolfram Mathworld" } getInitialState() { return { ...super.getInitialState(), ...{ - type: 'hypocycloid', hypocycloidA: 6, hypocycloidB: 1, - } + }, } } @@ -41,16 +38,16 @@ export default class Star extends Shape { let reduced = reduce(a, b) a = reduced[0] b = reduced[1] - let rotations = Number.isInteger(a/b) ? 1 : b - let scale = b < a ? 1/a : 1/(2*(b - a/2)) + let rotations = Number.isInteger(a / b) ? 1 : b + let scale = b < a ? 1 / a : 1 / (2 * (b - a / 2)) - for (let i=0; i<128*rotations; i++) { - let angle = Math.PI * 2.0 / 128.0 * i + for (let i = 0; i < 128 * rotations; i++) { + let angle = ((Math.PI * 2.0) / 128.0) * i points.push( new Victor( (a - b) * Math.cos(angle) + b * Math.cos(((a - b) / b) * angle), - (a - b) * Math.sin(angle) - b * Math.sin(((a - b) / b) * angle) - ).multiply({x: scale, y: scale}) + (a - b) * Math.sin(angle) - b * Math.sin(((a - b) / b) * angle), + ).multiply({ x: scale, y: scale }), ) } diff --git a/src/models/Model.js b/src/models/Model.js index e2ac55ab..74739a86 100644 --- a/src/models/Model.js +++ b/src/models/Model.js @@ -1,31 +1,46 @@ const options = [] export default class Model { - constructor() { - this.name = name + constructor(type) { + this.type = type this.cache = [] Object.assign(this, { selectGroup: "Shapes", shouldCache: true, - autosize: true, - canChangeSize: true, - canChangeHeight: true, - canRotate: true, + autosize: true, // TODO: do we need this? canMove: true, usesMachine: false, usesFonts: false, dragging: false, effect: false, + startingWidth: 100, + startingHeight: 100, }) } + // override as needed + canChangeSize(state) { + return true + } + + // override as needed + canChangeHeight(state) { + return this.canChangeSize(state) + } + + // override as needed + canRotate(state) { + return true + } + + canTransform(state) { + return this.canMove || this.canRotate(state) || this.canChangeSize(state) + } + // redux state of a newly created instance getInitialState() { - return { - startingWidth: 10, - startingHeight: 10, - } + return {} } getOptions() { diff --git a/src/models/NoiseWave.js b/src/models/NoiseWave.js index 3b65c8e7..e9b15d13 100644 --- a/src/models/NoiseWave.js +++ b/src/models/NoiseWave.js @@ -1,62 +1,67 @@ -import Shape, { shapeOptions } from './Shape' -import { getMachineInstance } from '@/features/machine/computer' -import Victor from 'victor' -import noise from '@/common/noise' -import seedrandom from 'seedrandom' -import { shapeSimilarity } from 'curve-matcher' -import { offset } from '@/common/geometry' +import Model from "./Model" +import { getMachineInstance } from "@/features/machine/computer" +import Victor from "victor" +import noise from "@/common/noise" +import seedrandom from "seedrandom" +import { shapeSimilarity } from "curve-matcher" +import { offset } from "@/common/geometry" const options = { - ...shapeOptions, - ...{ - numParticles: { - title: 'Number of waves', - min: 1 - }, - seed: { - title: 'Random seed', - min: 1 - }, - noiseLevel: { - title: 'Noise level', - min: 0, - max: 600, - step: 10 - }, - noiseType: { - title: 'Type', - type: 'dropdown', - choices: ['Perlin', 'Simplex'], - }, - } + numParticles: { + title: "Number of waves", + min: 1, + }, + seed: { + title: "Random seed", + min: 1, + }, + noiseLevel: { + title: "Noise level", + min: 0, + max: 600, + step: 10, + }, + noiseType: { + title: "Type", + type: "dropdown", + choices: ["Perlin", "Simplex"], + }, } -export default class NoiseWave extends Shape { +export default class NoiseWave extends Model { constructor() { - super('Noise Waves') + super('noise_wave') + this.label = "Noise Waves" + this.selectGroup = "Erasers" + this.usesMachine = true + this.autosize = false + this.canMove = false + } + + canChangeSize(state) { + return false + } + + canRotate(state) { + return false } getInitialState() { return { ...super.getInitialState(), ...{ - type: 'noise_wave', noise: 1, seed: 1, noiseLevel: 0, - noiseType: 'Perlin', + noiseType: "Perlin", numParticles: 100, - selectGroup: 'Erasers', - canChangeSize: false, - autosize: false, - usesMachine: true, - } + }, } } getVertices(state) { // without this adjustment, using an inverted circular mask causes clipping issues - const adjustment = .001 + const adjustment = 0.001 const machine = { maxRadius: state.machine.maxRadius - adjustment, @@ -64,7 +69,7 @@ export default class NoiseWave extends Shape { minX: state.machine.minX, maxX: state.machine.maxX, minY: state.machine.minY, - maxY: state.machine.maxY + maxY: state.machine.maxY, } const shape = state.shape @@ -84,30 +89,29 @@ export default class NoiseWave extends Shape { noise.seed(shape.seed) const particles = [] - for (let i=0; i { + const vertexGroups = particles.map((particle) => { let group = [] let wasInside = false // Arbitrarily choose 1000 iterations. This will stop particles that don't ever intersect with // the machine. It needs to be high enough to always leave the machine for particles that // don't move very fast. - for (let iterations=0; iterations<=1000; iterations++) { + for (let iterations = 0; iterations <= 1000; iterations++) { // This has side effects on the particle const newVertex = this.getParticleVertex(particle, shape) group.push(newVertex) @@ -123,7 +127,7 @@ export default class NoiseWave extends Shape { }) let prevCurve - for (let j=0; j { + vertices = vertices.map((vertex) => { return offset(vertex, -state.shape.x, -state.shape.y) }) @@ -150,15 +168,18 @@ export default class NoiseWave extends Shape { getCurve(vertexGroups, idx) { const pEndVertices = vertexGroups[idx] - const pStartVertices = vertexGroups[idx+1] + const pStartVertices = vertexGroups[idx + 1] return pStartVertices.reverse().concat(pEndVertices) } getParticleVertex(p, options) { let periodDenominator = 600 - options.noiseLevel if (options.noiseLevel >= 600) periodDenominator = 1 - const period = 1/periodDenominator - const v = options.noiseType === 'Simplex' ? noise.simplex2(p.x * period, p.y * period) : noise.perlin2(p.x * period, p.y * period) + const period = 1 / periodDenominator + const v = + options.noiseType === "Simplex" + ? noise.simplex2(p.x * period, p.y * period) + : noise.perlin2(p.x * period, p.y * period) const a = v * 2 * Math.PI + p.a p.x += Math.cos(a) * 5 diff --git a/src/models/Point.js b/src/models/Point.js index ef5048b9..37abaf8b 100644 --- a/src/models/Point.js +++ b/src/models/Point.js @@ -1,22 +1,26 @@ -import Victor from 'victor' -import Shape from './Shape' +import Victor from "victor" +import Model from "./Model" -export default class Point extends Shape { +export default class Point extends Model { constructor() { - super('Point') + super('point') + this.label = "Point" + this.startingWidth = 1 + this.startingHeight = 1 + this.shouldCache = false + this.autosize = false + } + + canChangeSize(state) { + return false } getInitialState() { return { ...super.getInitialState(), ...{ - type: 'point', - autosize: false, - startingWidth: 1, - startingHeight: 1, - shouldCache: false, - canChangeSize: false, - } + // no custom attributes + }, } } diff --git a/src/models/Polygon.js b/src/models/Polygon.js index bc9ee61b..18412c13 100644 --- a/src/models/Polygon.js +++ b/src/models/Polygon.js @@ -1,40 +1,41 @@ -import Model from './Model' -import Victor from 'victor' -//import Shape, { shapeOptions } from './Shape' +import Victor from "victor" +import Model from "./Model" const options = { polygonSides: { - title: 'Number of sides', - min: 3 + title: "Number of sides", + min: 3, }, roundCorners: { - title: 'Round corners', - type: 'checkbox', + title: "Round corners", + type: "checkbox", }, roundFraction: { - title: 'Round fraction', + title: "Round fraction", min: 0.05, max: 0.5, step: 0.025, - isVisible: (state) => { return state.roundCorners } + isVisible: (layer, state) => { + return state.roundCorners + }, }, } export default class Polygon extends Model { constructor() { - super() - this.label = 'Polygon' + super('polygon') + this.label = "Polygon" } getInitialState() { return { ...super.getInitialState(), ...{ - type: 'polygon', + type: "polygon", polygonSides: 4, roundCorners: false, roundFraction: 0.25, - } + }, } } @@ -46,28 +47,44 @@ export default class Polygon extends Model { // beta is the fraction to have rounded. const beta = state.shape.roundFraction // alpha is the fration to have straight. - const alpha = (1.0-beta) + const alpha = 1.0 - beta let points = [] - for (let i=0; i<=state.shape.polygonSides; i++) { - const angle = Math.PI * 2.0 / state.shape.polygonSides * (0.5 + i) + for (let i = 0; i <= state.shape.polygonSides; i++) { + const angle = ((Math.PI * 2.0) / state.shape.polygonSides) * (0.5 + i) if (state.shape.roundCorners && beta !== 0.0) { // angles that make up the arc. - const angleStart = Math.PI * 2.0 / state.shape.polygonSides * i - const angleEnd = Math.PI * 2.0 / state.shape.polygonSides * (i + 1) - const angleResolution = 0.10 + const angleStart = ((Math.PI * 2.0) / state.shape.polygonSides) * i + const angleEnd = ((Math.PI * 2.0) / state.shape.polygonSides) * (i + 1) + const angleResolution = 0.1 if (points.length > 0) { // Start with a line. We use a bunch of points for this, so they get stretch about evenly // as the curves do. - const numberOfLinePoints = (angleEnd - angleStart)/angleResolution/beta - points = points.concat(this.getLineVertices(points[points.length-1], - new Victor(alpha * Math.cos(angle) + beta * Math.cos(angleStart), - alpha * Math.sin(angle) + beta * Math.sin(angleStart)), - numberOfLinePoints)) + const numberOfLinePoints = + (angleEnd - angleStart) / angleResolution / beta + points = points.concat( + this.getLineVertices( + points[points.length - 1], + new Victor( + alpha * Math.cos(angle) + beta * Math.cos(angleStart), + alpha * Math.sin(angle) + beta * Math.sin(angleStart), + ), + numberOfLinePoints, + ), + ) } if (i !== state.shape.polygonSides) { // Create the arc. - for (let arcAngle=angleStart + angleResolution; arcAngle<=angleEnd; arcAngle += angleResolution) { - points.push(new Victor(alpha * Math.cos(angle) + beta * Math.cos(arcAngle), alpha * Math.sin(angle) + beta * Math.sin(arcAngle))) + for ( + let arcAngle = angleStart + angleResolution; + arcAngle <= angleEnd; + arcAngle += angleResolution + ) { + points.push( + new Victor( + alpha * Math.cos(angle) + beta * Math.cos(arcAngle), + alpha * Math.sin(angle) + beta * Math.sin(arcAngle), + ), + ) } } } else { @@ -75,16 +92,21 @@ export default class Polygon extends Model { points.push(new Victor(Math.cos(angle), Math.sin(angle))) } } + return points } // Returns a list of points from (start, end] along the line. getLineVertices(startPoint, endPoint, numberOfPoints) { - const resolution = 1.0/numberOfPoints + const resolution = 1.0 / numberOfPoints let points = [] - for (let d=resolution; d<=1.0; d+=resolution) { - points.push(new Victor(startPoint.x + (endPoint.x - startPoint.x)*d, - startPoint.y + (endPoint.y - startPoint.y)*d)) + for (let d = resolution; d <= 1.0; d += resolution) { + points.push( + new Victor( + startPoint.x + (endPoint.x - startPoint.x) * d, + startPoint.y + (endPoint.y - startPoint.y) * d, + ), + ) } return points } diff --git a/src/models/Reuleaux.js b/src/models/Reuleaux.js index d6265744..9627456b 100644 --- a/src/models/Reuleaux.js +++ b/src/models/Reuleaux.js @@ -1,29 +1,26 @@ -import Victor from 'victor' -import Shape, { shapeOptions } from './Shape' +import Victor from "victor" +import Model from "./Model" const options = { - ...shapeOptions, - ...{ - reuleauxSides: { - title: 'Number of sides', - step: 1, - min: 2 - }, - } + reuleauxSides: { + title: "Number of sides", + step: 1, + min: 2, + }, } -export default class Reuleaux extends Shape { +export default class Reuleaux extends Model { constructor() { - super('Reuleaux') + super('reuleaux') + this.label = "Reuleaux" } getInitialState() { return { ...super.getInitialState(), ...{ - type: 'reuleaux', - reuleauxSides: 3 - } + reuleauxSides: 3, + }, } } @@ -32,22 +29,26 @@ export default class Reuleaux extends Shape { // Construct an equalateral triangle let corners = [] // Initial location at PI/2 - let angle = Math.PI/2.0 + let angle = Math.PI / 2.0 // How much of the circle in one side? - let coverageAngle = Math.PI/state.shape.reuleauxSides + let coverageAngle = Math.PI / state.shape.reuleauxSides let halfCoverageAngle = 0.5 * coverageAngle - for (let c=0; c { + return state.wiperType === "Lines" }, - wiperSize: { - title: 'Wiper size', - min: 1 - }, - wiperAngleDeg: { - title: 'Wiper angle', - isVisible: (state) => { return state.wiperType === 'Lines' }, - }, - } + }, } const outOfBounds = (point, width, height) => { - if (point.x < -width/2.0) { + if (point.x < -width / 2.0) { return true } - if (point.y < -height/2.0) { + if (point.y < -height / 2.0) { return true } - if (point.x > width/2.0) { + if (point.x > width / 2.0) { return true } - if (point.y > height/2.0) { + if (point.y > height / 2.0) { return true } return false @@ -84,8 +83,8 @@ function spiralVertices(state) { // Determine the max radius let maxRadius = state.machine.maxRadius if (state.machine.rectangular) { - const halfHeight = (state.machine.maxY - state.machine.minY)/2.0 - const halfWidth = (state.machine.maxX - state.machine.minX)/2.0 + const halfHeight = (state.machine.maxY - state.machine.minY) / 2.0 + const halfWidth = (state.machine.maxX - state.machine.minX) / 2.0 maxRadius = Math.sqrt(Math.pow(halfHeight, 2.0) + Math.pow(halfWidth, 2.0)) } @@ -98,10 +97,12 @@ function spiralVertices(state) { while (radius <= maxRadius) { // Save where we are right now. - vertices.push(new Victor(radius * Math.cos(angle), radius * Math.sin(angle))) + vertices.push( + new Victor(radius * Math.cos(angle), radius * Math.sin(angle)), + ) // We want to have the next point be about the right arc length. - let deltaAngle = arcLength / radius * 2.0 * Math.PI + let deltaAngle = (arcLength / radius) * 2.0 * Math.PI // Limit this at small radii deltaAngle = Math.min(deltaAngle, 0.1) @@ -141,7 +142,7 @@ function linearVertices(state) { width = height } - let startLocation = Victor(-width/2.0, height/2.0) + let startLocation = Victor(-width / 2.0, height / 2.0) let cosa = Math.cos(angle) let sina = Math.sin(angle) @@ -155,14 +156,14 @@ function linearVertices(state) { let orig_delta_w = Victor(state.shape.wiperSize / cosa, 0.0) let orig_delta_h = Victor(0.0, -state.shape.wiperSize / sina) - if (angle > Math.PI/4.0 && angle < 0.75 * Math.PI) { + if (angle > Math.PI / 4.0 && angle < 0.75 * Math.PI) { // flip the logic of x,y let temp = orig_delta_w.clone() orig_delta_w = orig_delta_h.clone() orig_delta_h = temp } - if (angle > Math.PI/2.0) { - startLocation = Victor(-width/2.0, -height/2.0) + if (angle > Math.PI / 2.0) { + startLocation = Victor(-width / 2.0, -height / 2.0) orig_delta_w = orig_delta_w.clone().multiply(Victor(-1.0, -1.0)) orig_delta_h = orig_delta_h.clone().multiply(Victor(-1.0, -1.0)) } @@ -180,12 +181,22 @@ function linearVertices(state) { // "right" nextWidthPoint = nextWidthPoint.clone().add(delta_w) if (outOfBounds(nextWidthPoint, width, height)) { - let corner = boundPoint(nextWidthPoint.clone().subtract(delta_w), nextWidthPoint, width/2.0, height/2.0) + let corner = boundPoint( + nextWidthPoint.clone().subtract(delta_w), + nextWidthPoint, + width / 2.0, + height / 2.0, + ) outputVertices.push(corner) if (nearEnough(endLocation, corner)) { break } - nextWidthPoint = boundPoint(nextHeightPoint, nextWidthPoint, width/2.0, height/2.0) + nextWidthPoint = boundPoint( + nextHeightPoint, + nextWidthPoint, + width / 2.0, + height / 2.0, + ) delta_w = orig_delta_h } outputVertices.push(nextWidthPoint) @@ -196,7 +207,12 @@ function linearVertices(state) { // "down-left" nextHeightPoint = nextHeightPoint.clone().add(delta_h) if (outOfBounds(nextHeightPoint, width, height)) { - nextHeightPoint = boundPoint(nextWidthPoint, nextHeightPoint, width/2.0, height/2.0) + nextHeightPoint = boundPoint( + nextWidthPoint, + nextHeightPoint, + width / 2.0, + height / 2.0, + ) delta_h = orig_delta_w } outputVertices.push(nextHeightPoint) @@ -211,12 +227,22 @@ function linearVertices(state) { break } if (outOfBounds(nextHeightPoint, width, height)) { - let corner = boundPoint(nextHeightPoint.clone().subtract(delta_h), nextHeightPoint, width/2.0, height/2.0) + let corner = boundPoint( + nextHeightPoint.clone().subtract(delta_h), + nextHeightPoint, + width / 2.0, + height / 2.0, + ) outputVertices.push(corner) if (nearEnough(endLocation, corner)) { break } - nextHeightPoint = boundPoint(nextWidthPoint, nextHeightPoint, width/2.0, height/2.0) + nextHeightPoint = boundPoint( + nextWidthPoint, + nextHeightPoint, + width / 2.0, + height / 2.0, + ) delta_h = orig_delta_w } outputVertices.push(nextHeightPoint) @@ -231,37 +257,50 @@ function linearVertices(state) { break } if (outOfBounds(nextWidthPoint, width, height)) { - nextWidthPoint = boundPoint(nextHeightPoint, nextWidthPoint, width/2.0, height/2.0) + nextWidthPoint = boundPoint( + nextHeightPoint, + nextWidthPoint, + width / 2.0, + height / 2.0, + ) delta_w = orig_delta_h } } return outputVertices } -export default class Wiper extends Shape { +export default class Wiper extends Model { constructor() { - super('Wiper') + super('wiper') + this.label = "Wiper" + this.selectGroup = "Erasers" + this.usesMachine = true + this.shouldCache = false + this.autosize = false + this.canMove = false + } + + canChangeSize(state) { + return false + } + + canRotate(state) { + return false } getInitialState() { return { ...super.getInitialState(), ...{ - type: 'wiper', wiperAngleDeg: 15, wiperSize: 4, - wiperType: 'Lines', - selectGroup: 'Erasers', - canChangeSize: false, - shouldCache: false, - autosize: false, - usesMachine: true, - } + wiperType: "Lines", + }, } } getVertices(state) { - if (state.shape.wiperType === 'Lines') { + if (state.shape.wiperType === "Lines") { return linearVertices(state) } else { return spiralVertices(state) diff --git a/src/models/circle_packer/Circle.js b/src/models/circle_packer/Circle.js index 60325b04..528c4c2d 100644 --- a/src/models/circle_packer/Circle.js +++ b/src/models/circle_packer/Circle.js @@ -1,4 +1,4 @@ -import Victor from 'victor' +import Victor from "victor" export class Circle extends Victor { constructor(x, y, r, state) { @@ -8,7 +8,9 @@ export class Circle extends Victor { this.r = r || 30 this.state = state this.growing = this.state.growing - if (this.growing == null) { this.growing = true } + if (this.growing == null) { + this.growing = true + } this.theta = null this.center = new Victor(0, 0) } @@ -22,7 +24,9 @@ export class Circle extends Victor { outOfBounds(radius) { radius ||= this.state.inBounds ? this.r : 0 - return this.state.rectangular ? this.outOfRectangularBounds(radius) : this.outOfPolarBounds(radius) + return this.state.rectangular + ? this.outOfRectangularBounds(radius) + : this.outOfPolarBounds(radius) } outOfPolarBounds(radius) { @@ -48,13 +52,20 @@ export class Circle extends Victor { intersection(circle) { let a = circle.r var b = this.r - var c = Math.sqrt((this.x-circle.x)*(this.x-circle.x)+(this.y-circle.y)*(this.y-circle.y)) - var d = (b*b+c*c-a*a)/(2*c) - var h = Math.sqrt(b*b-d*d) - const i1 = (circle.x-this.x)*d/c + (circle.y-this.y)*h/c + this.x - const i2 = (circle.y-this.y)*d/c - (circle.x-this.x)*h/c + this.y - const i3 = (circle.x-this.x)*d/c - (circle.y-this.y)*h/c + this.x - const i4 = (circle.y-this.y)*d/c + (circle.x-this.x)*h/c + this.y + var c = Math.sqrt( + (this.x - circle.x) * (this.x - circle.x) + + (this.y - circle.y) * (this.y - circle.y), + ) + var d = (b * b + c * c - a * a) / (2 * c) + var h = Math.sqrt(b * b - d * d) + const i1 = + ((circle.x - this.x) * d) / c + ((circle.y - this.y) * h) / c + this.x + const i2 = + ((circle.y - this.y) * d) / c - ((circle.x - this.x) * h) / c + this.y + const i3 = + ((circle.x - this.x) * d) / c - ((circle.y - this.y) * h) / c + this.x + const i4 = + ((circle.y - this.y) * d) / c + ((circle.x - this.x) * h) / c + this.y const ret = [] if (!isNaN(i1)) { @@ -91,7 +102,7 @@ export class Circle extends Victor { c = sq(this.y) + sq(p1.x - this.x) - sq(this.r) } else { a = 1 + sq(m) - b = -this.x * 2 + (m * (n - this.y)) * 2 + b = -this.x * 2 + m * (n - this.y) * 2 c = sq(this.x) + sq(n - this.y) - sq(this.r) } @@ -101,7 +112,7 @@ export class Circle extends Victor { // insert into quadratic formula let intersections = [ (-b + Math.sqrt(sq(b) - 4 * a * c)) / (2 * a), - (-b - Math.sqrt(sq(b) - 4 * a * c)) / (2 * a) + (-b - Math.sqrt(sq(b) - 4 * a * c)) / (2 * a), ] if (d === 0) { @@ -109,9 +120,9 @@ export class Circle extends Victor { } if (p2.x - p1.x === 0) { - return intersections.map(y => new Victor(p1.x, y)) + return intersections.map((y) => new Victor(p1.x, y)) } else { - return intersections.map(x => new Victor(x, m * x + n)) + return intersections.map((x) => new Victor(x, m * x + n)) } } @@ -126,7 +137,9 @@ export class Circle extends Victor { this.lineIntersection(p1, p2), this.lineIntersection(p2, p3), this.lineIntersection(p3, p4), - this.lineIntersection(p4, p1) - ].flat().filter(n => n && n.y !== Infinity && n.y !== -Infinity) + this.lineIntersection(p4, p1), + ] + .flat() + .filter((n) => n && n.y !== Infinity && n.y !== -Infinity) } } diff --git a/src/models/circle_packer/CirclePacker.js b/src/models/circle_packer/CirclePacker.js index a0dd2592..88fc045e 100644 --- a/src/models/circle_packer/CirclePacker.js +++ b/src/models/circle_packer/CirclePacker.js @@ -1,62 +1,65 @@ -import seedrandom from 'seedrandom' -import Shape, { shapeOptions } from '../Shape' -import { Circle } from './Circle' -import Graph from '@/common/Graph' -import { circle, arc } from '@/common/geometry' -import { getMachineInstance } from '@/features/machine/computer' -import Victor from 'victor' +import seedrandom from "seedrandom" +import Model from "../Model" +import { Circle } from "./Circle" +import Graph from "@/common/Graph" +import { circle, arc } from "@/common/geometry" +import { getMachineInstance } from "@/features/machine/computer" +import Victor from "victor" const ROUNDS = 100 // default number of rounds to attempt to create and grow circles const RECTANGULAR_ATTEMPTS_MULTIPLIER = 4 const ATTEMPTS_MODIFIER = 5 const options = { - ...shapeOptions, - ...{ - seed: { - title: 'Random seed', - min: 1 - }, - startingRadius: { - title: 'Minimum radius', - min: 3 - }, - attempts: { - title: 'Circle uniformity', - min: 1, - max: 100, - step: 4 - }, - inBounds: { - title: 'Stay in bounds', - type: 'checkbox' - } - } + seed: { + title: "Random seed", + min: 1, + }, + startingRadius: { + title: "Minimum radius", + min: 3, + }, + attempts: { + title: "Circle uniformity", + min: 1, + max: 100, + step: 4, + }, + inBounds: { + title: "Stay in bounds", + type: "checkbox", + }, } // adapted initially from Coding Challenge #50; Animated Circle Packing, https://www.youtube.com/watch?v=QHEQuoIKgNE // no license was specified -export default class CirclePacker extends Shape { +export default class CirclePacker extends Model { constructor() { - super('Circle Packer') + super("circlePacker") + this.label = "Circle Packer" + this.usesMachine = true + this.canMove = false + this.autosize = false + this.selectGroup = "Erasers" + } + + canChangeSize(state) { + return false + } + + canRotate(state) { + return false } getInitialState() { return { ...super.getInitialState(), ...{ - type: 'circle_packer', - selectGroup: 'Erasers', seed: 1, startingRadius: 4, attempts: 20, inBounds: false, - usesMachine: true, - canChangeSize: false, - canRotate: false, - canMove: false, - autosize: false, - } + }, } } @@ -78,9 +81,14 @@ export default class CirclePacker extends Shape { rectangular: machine.rectangular, attempts: state.shape.attempts, r: state.shape.startingRadius, - inBounds: state.shape.inBounds + inBounds: state.shape.inBounds, } - this.boundaryCircle = new Circle(0, 0, this.settings.maxRadius, this.settings) + this.boundaryCircle = new Circle( + 0, + 0, + this.settings.maxRadius, + this.settings, + ) this.boundaryRectangle = [ new Victor(-this.settings.width / 2, -this.settings.height / 2), new Victor(this.settings.width / 2, -this.settings.height / 2), @@ -99,9 +107,7 @@ export default class CirclePacker extends Shape { // experimental setupCircles() { - const circles = [ - new Circle(0, 0, 75, { ...this.settings, growing: false }), - ] + const circles = [new Circle(0, 0, 75, { ...this.settings, growing: false })] for (const c of circles) { this.addCircle(c) @@ -111,14 +117,16 @@ export default class CirclePacker extends Shape { // generate random circles within machine bounds. Grow them incrementally. If a circle collides // with another, stop them growing. Repeat. createCircles() { - let attempts = this.settings.rectangular ? - this.settings.attempts * RECTANGULAR_ATTEMPTS_MULTIPLIER : - this.settings.attempts - if (attempts <= 0) { attempts = 1 } + let attempts = this.settings.rectangular + ? this.settings.attempts * RECTANGULAR_ATTEMPTS_MULTIPLIER + : this.settings.attempts + if (attempts <= 0) { + attempts = 1 + } const rounds = Math.floor(ROUNDS * (ROUNDS / attempts)) - for (let round=0; round circle.perimeter) + this.perimeterCircles = this.circles.filter((circle) => circle.perimeter) } // ensure that we have a fully connected graph to walk connectOrphans() { - this.perimeterCircles.forEach (circle => this.markConnected(circle)) + this.perimeterCircles.forEach((circle) => this.markConnected(circle)) const center = new Victor(0, 0) - let orphans = this.circles.filter(circle => !circle.connected) - let connected = this.circles.filter(circle => circle.connected) + let orphans = this.circles.filter((circle) => !circle.connected) + let connected = this.circles.filter((circle) => circle.connected) - while(orphans.length > 0) { + while (orphans.length > 0) { // find orphan furthest from center (closest to perimeter) const orphan = this.farthest(orphans, center) @@ -152,14 +160,14 @@ export default class CirclePacker extends Shape { this.markConnected(orphan) // repeat - orphans = orphans.filter(circle => !circle.connected) - connected = this.circles.filter(circle => circle.connected) + orphans = orphans.filter((circle) => !circle.connected) + connected = this.circles.filter((circle) => circle.connected) } } // start with a perimeter circle, draw it and any neighboring circles recursively. Repeat. drawCircles() { - let curr = this.circles.find(circle => circle.perimeter) + let curr = this.circles.find((circle) => circle.perimeter) let prev = curr let intersection = this.boundaryIntersection(curr) let angle = Math.atan2(intersection.y - curr.y, intersection.x - curr.x) @@ -169,7 +177,7 @@ export default class CirclePacker extends Shape { this.drawCircle(curr, angle) angle = this.walk(curr, angle, stack) prev = curr - curr = this.perimeterCircles.find(circle => !circle.walked) + curr = this.perimeterCircles.find((circle) => !circle.walked) if (curr) { angle = this.connectAlongPerimeter(prev, curr, angle) @@ -199,11 +207,11 @@ export default class CirclePacker extends Shape { let valid = !possibleC.outOfBounds() if (valid) { - for (let i=0; i< this.circles.length; i++) { + for (let i = 0; i < this.circles.length; i++) { const c = this.circles[i] let d = possibleC.distance(c) - if (d < (c.r + possibleC.r)) { + if (d < c.r + possibleC.r) { valid = false break } @@ -215,13 +223,13 @@ export default class CirclePacker extends Shape { growCircles() { if (this.circles.length > 1) { - while (this.circles.filter(circle => circle.growing).length > 0) { - this.circles.forEach(c => { + while (this.circles.filter((circle) => circle.growing).length > 0) { + this.circles.forEach((c) => { if (c.growing) { if (c.outOfBounds()) { c.growing = false } else { - this.circles.forEach(other => { + this.circles.forEach((other) => { if (c !== other) { let d = c.distance(other) if (d < c.r + other.r) { @@ -340,12 +348,18 @@ export default class CirclePacker extends Shape { // returns the point in arr that is farthest to a given point farthest(arr, point) { - return arr.reduce((max, x, i, arr) => x.distance(point) > max.distance(point) ? x : max, arr[0]) + return arr.reduce( + (max, x, i, arr) => (x.distance(point) > max.distance(point) ? x : max), + arr[0], + ) } // returns the point in arr that is closest to a given point closest(arr, point) { - return arr.reduce((max, x, i, arr) => x.distance(point) < max.distance(point) ? x : max, arr[0]) + return arr.reduce( + (max, x, i, arr) => (x.distance(point) < max.distance(point) ? x : max), + arr[0], + ) } getOptions() { diff --git a/src/models/effects/FineTuning.js b/src/models/effects/FineTuning.js index 39f7496f..ef98e021 100644 --- a/src/models/effects/FineTuning.js +++ b/src/models/effects/FineTuning.js @@ -1,54 +1,53 @@ -import { modelOptions } from '../Model' -import { arrayRotate } from '@/common/util' -import { circle } from '@/common/geometry' -import Effect from '../Effect' +import { arrayRotate } from "@/common/util" +import { circle } from "@/common/geometry" +import Effect from "../Effect" const options = { - ...modelOptions, - ...{ - backtrackPct: { - title: 'Backtrack at end (%)', - min: 0, - max: 100, - step: 2 - }, - drawPortionPct: { - title: 'Draw portion of path (%)', - min: 0, - max: 100, - step: 2 - }, - rotateStartingPct: { - title: 'Rotate starting point (%)', - min: -100, - max: 100, - step: 2 - }, - } + backtrackPct: { + title: "Backtrack at end (%)", + min: 0, + max: 100, + step: 2, + }, + drawPortionPct: { + title: "Draw portion of path (%)", + min: 0, + max: 100, + step: 2, + }, + rotateStartingPct: { + title: "Rotate starting point (%)", + min: -100, + max: 100, + step: 2, + }, } export default class FineTuning extends Effect { constructor() { - super('Fine Tuning') + super("fineTuning") + this.label = "Fine Tuning" + this.selectGroup = "effects" + this.canMove = false + this.effect = true + } + + canChangeSize(state) { + return false + } + + canRotate(state) { + return false } getInitialState() { return { ...super.getInitialState(), ...{ - // Inherited - type: 'fineTuning', - selectGroup: 'effects', - canChangeSize: false, - canRotate: false, - canMove: false, - effect: true, - - // Fine Tuning Options drawPortionPct: 100, backtrackPct: 0, rotateStartingPct: 0, - } + }, } } @@ -58,26 +57,37 @@ export default class FineTuning extends Effect { } applyEffect(effect, layer, vertices) { - // Remove one point if we are smearing - if (effect.transformMethod === 'smear') { + if (effect.transformMethod === "smear") { vertices.pop() } let outputVertices = vertices - if (effect.rotateStartingPct === undefined || effect.rotateStartingPct !== 0) { - const start = Math.round(outputVertices.length * effect.rotateStartingPct / 100.0) + if ( + effect.rotateStartingPct === undefined || + effect.rotateStartingPct !== 0 + ) { + const start = Math.round( + (outputVertices.length * effect.rotateStartingPct) / 100.0, + ) outputVertices = arrayRotate(outputVertices, start) } if (effect.drawPortionPct !== undefined) { - const drawPortionPct = Math.round((parseInt(effect.drawPortionPct) || 100)/100.0 * outputVertices.length) + const drawPortionPct = Math.round( + ((parseInt(effect.drawPortionPct) || 100) / 100.0) * + outputVertices.length, + ) outputVertices = outputVertices.slice(0, drawPortionPct) } - const backtrack = Math.round(vertices.length * effect.backtrackPct / 100.0) - outputVertices = outputVertices.concat(outputVertices.slice(outputVertices.length - backtrack).reverse()) + const backtrack = Math.round( + (vertices.length * effect.backtrackPct) / 100.0, + ) + outputVertices = outputVertices.concat( + outputVertices.slice(outputVertices.length - backtrack).reverse(), + ) return outputVertices } diff --git a/src/models/effects/Fisheye.js b/src/models/effects/Fisheye.js index 31b063be..e6da473a 100644 --- a/src/models/effects/Fisheye.js +++ b/src/models/effects/Fisheye.js @@ -1,53 +1,55 @@ -import Victor from 'victor' -import Effect from '../Effect' -import { shapeOptions } from '../Shape' -import { circle } from '@/common/geometry' -import * as d3Fisheye from 'd3-fisheye' +import Victor from "victor" +import Effect from "../Effect" +import { circle } from "@/common/geometry" +import * as d3Fisheye from "d3-fisheye" const options = { - ...shapeOptions, - ...{ - fisheyeDistortion: { - title: 'Distortion', - min: -2, - max: 40, - step: 0.1, - } - } + fisheyeDistortion: { + title: "Distortion", + min: -2, + max: 40, + step: 0.1, + }, } export default class Fisheye extends Effect { constructor() { - super('Fisheye') + super("fisheye") + this.label = "Fisheye" + this.selectGroup = "effects" + this.startingWidth = 100 + this.startingHeight = 100 + } + + canRotate(state) { + return false + } + + canChangeHeight(state) { + return false } getInitialState() { return { ...super.getInitialState(), ...{ - type: 'fisheye', - selectGroup: 'effects', fisheyeDistortion: 3, - startingWidth: 100, - startingHeight: 100, - canRotate: false, - canChangeHeight: false - } + }, } } getVertices(state) { - const width = state.shape.startingWidth - return circle(width/2) + return circle(this.startingWidth / 2) } applyEffect(effect, layer, vertices) { - const fisheye = d3Fisheye.radial() - .radius(effect.startingWidth/2) - .distortion(effect.fisheyeDistortion/2) + const fisheye = d3Fisheye + .radial() + .radius(effect.width / 2) + .distortion(effect.fisheyeDistortion / 2) fisheye.focus([effect.x, effect.y]) - return vertices.map(vertex => { + return vertices.map((vertex) => { const warped = fisheye([vertex.x, vertex.y]) return new Victor(warped[0], warped[1]) }) diff --git a/src/models/effects/Loop.js b/src/models/effects/Loop.js index 67130518..3a052c23 100644 --- a/src/models/effects/Loop.js +++ b/src/models/effects/Loop.js @@ -1,107 +1,107 @@ -import { modelOptions } from "../Model" import Effect from "../Effect" import { scale, rotate, circle } from "@/common/geometry" import { evaluate } from "mathjs" const options = { - ...modelOptions, - ...{ - numLoops: { - title: "Number of loops", - min: 1, - }, - transformMethod: { - title: "When transforming shape", - type: "togglebutton", - choices: ["smear", "intact"], - }, - growEnabled: { - title: "Scale", - type: "checkbox", - }, - growValue: { - title: "Scale (+/-)", - }, - growMethod: { - title: "Scale by", - type: "togglebutton", - choices: ["constant", "function"], - }, - growMathInput: { - title: "Scale function (i)", - type: "text", - isVisible: (state) => { - return state.growMethod === "function" - }, - }, - growMath: { - isVisible: (state) => { - return false - }, - }, - spinEnabled: { - title: "Spin", - type: "checkbox", - }, - spinValue: { - title: "Spin (+/-)", - step: 0.1, + numLoops: { + title: "Number of loops", + min: 1, + }, + transformMethod: { + title: "When transforming shape", + type: "togglebutton", + choices: ["smear", "intact"], + }, + growEnabled: { + title: "Scale", + type: "checkbox", + }, + growValue: { + title: "Scale (+/-)", + }, + growMethod: { + title: "Scale by", + type: "togglebutton", + choices: ["constant", "function"], + }, + growMathInput: { + title: "Scale function (i)", + type: "text", + isVisible: (layer, state) => { + return state.growMethod === "function" }, - spinMethod: { - title: "Spin by", - type: "togglebutton", - choices: ["constant", "function"], + }, + growMath: { + isVisible: (layer, state) => { + return false }, - spinMathInput: { - title: "Spin function (i)", - type: "text", - isVisible: (state) => { - return state.spinMethod === "function" - }, + }, + spinEnabled: { + title: "Spin", + type: "checkbox", + }, + spinValue: { + title: "Spin (+/-)", + step: 0.1, + }, + spinMethod: { + title: "Spin by", + type: "togglebutton", + choices: ["constant", "function"], + }, + spinMathInput: { + title: "Spin function (i)", + type: "text", + isVisible: (layer, state) => { + return state.spinMethod === "function" }, - spinMath: { - isVisible: (state) => { - return false - }, + }, + spinMath: { + isVisible: (layer, state) => { + return false }, - spinSwitchbacks: { - title: "Switchbacks", - isVisible: (state) => { - return state.spinMethod === "constant" - }, + }, + spinSwitchbacks: { + title: "Switchbacks", + isVisible: (layer, state) => { + return state.spinMethod === "constant" }, }, } export default class Loop extends Effect { constructor() { - super("Loop") + super("loop") + this.label = "Loop" + this.selectGroup = "effects" + this.canMove = false + this.effect = true + } + + canRotate(state) { + return false + } + + canChangeSize(state) { + return false } getInitialState() { return { ...super.getInitialState(), ...{ - // Inherited - type: "loop", - selectGroup: "effects", - canChangeSize: false, - canRotate: false, - canMove: false, - effect: true, - - // Loop Options + // loop Options transformMethod: "smear", numLoops: 10, - // Grow options + // grow options growEnabled: true, growValue: 100, growMethod: "constant", growMathInput: "i+cos(i/2)", growMath: "i+cos(i/2)", - // Spin Options + // spin options spinEnabled: false, spinValue: 2, spinMethod: "constant", diff --git a/src/models/effects/Mask.js b/src/models/effects/Mask.js index 740ae6a7..413e41df 100644 --- a/src/models/effects/Mask.js +++ b/src/models/effects/Mask.js @@ -11,18 +11,13 @@ const options = { title: "Mask shape", type: "togglebutton", choices: ["rectangle", "circle"], - onChange: (changes, attrs) => { + onChange: (model, changes, state) => { if (changes.maskMachine === "circle") { changes.rotation = 0 - const size = Math.min(attrs.startingWidth, attrs.startingHeight) - changes.startingHeight = size - changes.startingWidth = size - changes.canRotate = false - changes.canChangeHeight = false - } else { - changes.canRotate = true - changes.canChangeHeight = true + const size = Math.min(changes.width, changes.height) + changes.height = size + changes.width = size } return changes @@ -44,18 +39,25 @@ const options = { export default class Mask extends Effect { constructor() { - super() + super("mask") this.label = "Mask" this.selectGroup = "effects" } + canRotate(state) { + return state.maskMachine != "circle" + } + + canChangeHeight(state) { + return state.maskMachine != "circle" + } + getInitialState() { return { ...super.getInitialState(), ...{ - type: "mask", - startingWidth: 100, - startingHeight: 100, + width: 100, + height: 100, maskMinimizeMoves: false, maskMachine: "rectangle", maskBorder: false, @@ -69,8 +71,8 @@ export default class Mask extends Effect { } getVertices(state) { - const width = state.shape.startingWidth - const height = state.shape.startingHeight + const width = state.shape.width + const height = state.shape.height if (state.shape.dragging && state.shape.maskMachine === "circle") { return circle(width / 2) @@ -87,10 +89,7 @@ export default class Mask extends Effect { applyEffect(effect, layer, vertices) { vertices = vertices.map((vertex) => { - return rotate( - offset(vertex, -effect.x, -effect.y), - effect.rotation, - ) + return rotate(offset(vertex, -effect.x, -effect.y), effect.rotation) }) if (!layer.dragging && !effect.dragging) { @@ -107,11 +106,11 @@ export default class Mask extends Effect { vertices, { minX: 0, - maxX: effect.startingWidth, + maxX: effect.width, minY: 0, - maxY: effect.startingHeight, + maxY: effect.height, minimizeMoves: effect.maskMinimizeMoves, - maxRadius: effect.startingWidth / 2, + maxRadius: effect.width / 2, perimeterConstant: effect.maskPerimeterConstant, mask: true, }, @@ -121,11 +120,7 @@ export default class Mask extends Effect { } return vertices.map((vertex) => { - return offset( - rotate(vertex, -effect.rotation), - effect.x, - effect.y, - ) + return offset(rotate(vertex, -effect.rotation), effect.x, effect.y) }) } } diff --git a/src/models/effects/Noise.js b/src/models/effects/Noise.js index f19900e5..7b2a2b4b 100644 --- a/src/models/effects/Noise.js +++ b/src/models/effects/Noise.js @@ -1,63 +1,67 @@ -import { modelOptions } from '../Model' -import Victor from 'victor' -import Effect from '../Effect' -import noise from '@/common/noise' -import { subsample } from '@/common/geometry' +import Victor from "victor" +import Effect from "../Effect" +import noise from "@/common/noise" +import { subsample } from "@/common/geometry" const options = { - ...modelOptions, - ...{ - seed: { - title: 'Random seed', - min: 1 + seed: { + title: "Random seed", + min: 1, + }, + noiseMagnification: { + title: "Magnification", + min: 1, + max: 100, + step: 1, + isVisible: (layer, state) => { + return state.noiseApplication !== "Linear" }, - noiseMagnification: { - title: 'Magnification', - min: 1, - max: 100, - step: 1, - isVisible: (state) => { return state.noiseApplication !== 'Linear' } - }, - noiseAmplitude: { - title: 'Amplitude', - min: 0, - max: 20, - step: 1 - }, - noiseType: { - title: 'Noise type', - type: 'togglebutton', - choices: ['Perlin', 'Simplex'], - }, - noiseApplication: { - title: 'Application', - type: 'togglebutton', - choices: ['Contour', 'Linear'], - }, - } + }, + noiseAmplitude: { + title: "Amplitude", + min: 0, + max: 20, + step: 1, + }, + noiseType: { + title: "Noise type", + type: "togglebutton", + choices: ["Perlin", "Simplex"], + }, + noiseApplication: { + title: "Application", + type: "togglebutton", + choices: ["Contour", "Linear"], + }, } export default class Noise extends Effect { constructor() { - super('Noise') + super("noise") + this.label = "Noise" + this.selectGroup = "effects" + this.canMove = false + } + + canRotate(state) { + return false + } + + canChangeSize(state) { + return false } getInitialState() { return { ...super.getInitialState(), ...{ - type: 'noise', - selectGroup: 'effects', seed: 1, noiseAmplitude: 4, noiseMagnification: 58, - noiseType: 'Simplex', - noiseApplication: 'Contour', - canChangeSize: false, - canRotate: false, - canMove: false, - subsample: true - } + noiseType: "Simplex", + noiseApplication: "Contour", + subsample: true, + }, } } @@ -66,7 +70,7 @@ export default class Noise extends Effect { noise.seed(effect.seed) vertices = subsample(vertices, 2.0) - if (effect.noiseApplication === 'Linear') { + if (effect.noiseApplication === "Linear") { return this.applyLinearEffect(effect, vertices) } else { return this.applyRadialEffect(effect, vertices, this.contour) @@ -77,42 +81,53 @@ export default class Noise extends Effect { } applyLinearEffect(effect, vertices) { - return vertices.map(vertex => { - const a = this.octaveNoise(effect.noiseType, vertex.x, vertex.y, 2, effect.noiseAmplitude) + return vertices.map((vertex) => { + const a = this.octaveNoise( + effect.noiseType, + vertex.x, + vertex.y, + 2, + effect.noiseAmplitude, + ) return new Victor(vertex.x + a, vertex.y + a) }) } applyRadialEffect(effect, vertices, contourFn) { - let periodDenominator = effect.noiseType === 'Simplex' ? - 100 + 6 * effect.noiseMagnification : - 100 + effect.noiseMagnification + let periodDenominator = + effect.noiseType === "Simplex" + ? 100 + 6 * effect.noiseMagnification + : 100 + effect.noiseMagnification if (periodDenominator === 0) periodDenominator = 1 - const period = 1/periodDenominator + const period = 1 / periodDenominator - return vertices.map(vertex => { - const v = this.noise(effect.noiseType, vertex.x * period, vertex.y * period) + return vertices.map((vertex) => { + const v = this.noise( + effect.noiseType, + vertex.x * period, + vertex.y * period, + ) const a = v * Math.PI * 2 return contourFn(a * effect.noiseAmplitude, vertex) }) } noise(noiseType, x, y) { - return noiseType === 'Simplex' ? noise.simplex2(x, y) : noise.perlin2(x, y) + return noiseType === "Simplex" ? noise.simplex2(x, y) : noise.perlin2(x, y) } octaveNoise(noiseType, x, y, octaves, persistence) { - let total = 0 - let frequency = 1 - let amplitude = 1 - - for(let i=0; i { + return state.trackSpiralEnabled }, - trackRotations: { - title: 'Track rotations' - }, - trackSpiralEnabled: { - title: 'Spiral track', - type: 'checkbox', - }, - trackSpiralRadius: { - title: 'Spiral radius', - isVisible: state => { return state.trackSpiralEnabled }, - }, - } + }, } export default class Track extends Effect { constructor() { - super('Track') + super("track") + this.selectGroup = "effects" + this.canMove = false + this.effect = true + } + + canChangeSize(state) { + return false + } + + canRotate(state) { + return false } getInitialState() { return { ...super.getInitialState(), ...{ - // Inherited - type: 'track', - selectGroup: 'effects', - canChangeSize: false, - canRotate: false, - canMove: false, - effect: true, - - // Track Options trackRadius: 10, trackRotations: 1, trackSpiralEnabled: false, trackSpiralRadius: 50.0, - } + }, } } @@ -54,26 +54,28 @@ export default class Track extends Effect { } applyEffect(effect, layer, vertices) { - let outputVertices = [] - for (var j=0; j { - changes.canChangeSize = changes.warpType !== 'custom' - if (['angle', 'quad', 'shear'].includes(changes.warpType)) { - changes.rotation = changes.warpType === 'shear' ? 0 : 45 - changes.canRotate = true - } else { - changes.rotation = 0 - changes.canRotate = false - } - - return changes + warpType: { + title: "Warp type", + type: "dropdown", + choices: ["angle", "quad", "circle", "grid", "shear", "custom"], + onChange: (model, changes, state) => { + if (["angle", "quad", "shear"].includes(changes.warpType)) { + changes.rotation = changes.warpType === "shear" ? 0 : 45 + } else { + changes.rotation = 0 } + + return changes }, - period: { - title: 'Period', - step: 0.2, - isVisible: (state) => { return !['custom', 'shear'].includes(state.warpType) }, - }, - xMathInput: { - title: 'X(x,y)', - delayKey: 'xMath', - type: 'text', - isVisible: (state) => { return state.warpType === 'custom' }, + }, + period: { + title: "Period", + step: 0.2, + isVisible: (layer, state) => { + return !["custom", "shear"].includes(state.warpType) }, - yMathInput: { - title: 'Y(x,y)', - delayKey: 'yMath', - type: 'text', - isVisible: (state) => { return state.warpType === 'custom' }, + }, + xMathInput: { + title: "X(x,y)", + delayKey: "xMath", + type: "text", + isVisible: (layer, state) => { + return state.warpType === "custom" }, - subsample: { - title: 'Subsample points', - type: 'checkbox', + }, + yMathInput: { + title: "Y(x,y)", + delayKey: "yMath", + type: "text", + isVisible: (layer, state) => { + return state.warpType === "custom" }, - } + }, + subsample: { + title: "Subsample points", + type: "checkbox", + }, } export default class Warp extends Effect { constructor() { - super('Warp') + super("warp") + this.label = "Warp" + this.selectGroup = "effects" + this.startingWidth = 40 + this.startingHeight = 40 + } + + canRotate(state) { + return ["angle", "quad", "shear"].includes(state.warpType) + } + + canChangeSize(state) { + return state.warpType !== "custom" + } + + canChangeHeight(state) { + return false } getInitialState() { return { ...super.getInitialState(), ...{ - type: 'warp', - selectGroup: 'effects', - warpType: 'angle', + warpType: "angle", period: 10.0, subsample: true, - xMathInput: 'x + 4*sin((x+y)/20)', - xMath: 'x + 4*sin((x+y)/20)', - yMathInput: 'y + 4*sin((x-y)/20)', - yMath: 'y + 4*sin((x-y)/20)', - startingWidth: 40, - startingHeight: 40, + xMathInput: "x + 4*sin((x+y)/20)", + xMath: "x + 4*sin((x+y)/20)", + yMathInput: "y + 4*sin((x-y)/20)", + yMath: "y + 4*sin((x-y)/20)", rotation: 45, - canRotate: true, - canChangeHeight: false - } + }, } } getVertices(state) { - const width = state.shape.startingWidth - return circle(width/2) + const width = state.shape.width + return circle(width / 2) } applyEffect(effect, layer, vertices) { @@ -85,15 +94,19 @@ export default class Warp extends Effect { vertices = subsample(vertices, 2.0) } - if (effect.warpType === 'angle' || effect.warpType === 'quad') { - return this.angle(effect.warpType === 'angle' ? +1.0 : -1.0, effect, vertices) - } else if (effect.warpType === 'circle') { + if (effect.warpType === "angle" || effect.warpType === "quad") { + return this.angle( + effect.warpType === "angle" ? +1.0 : -1.0, + effect, + vertices, + ) + } else if (effect.warpType === "circle") { return this.circle(effect, vertices) - } else if (effect.warpType === 'grid') { + } else if (effect.warpType === "grid") { return this.grid(effect, vertices) - } else if (effect.warpType === 'shear') { + } else if (effect.warpType === "shear") { return this.shear(effect, vertices) - } else if (effect.warpType === 'custom') { + } else if (effect.warpType === "custom") { return this.custom(effect, vertices) } @@ -101,63 +114,96 @@ export default class Warp extends Effect { } angle(ySign, effect, vertices) { - const periodx = 10.0 * effect.period / (Math.PI * 2.0) / Math.cos(-effect.rotation / 180.0 * Math.PI) - const periody = 10.0 * effect.period / (Math.PI * 2.0) / Math.sin(-effect.rotation / 180.0 * Math.PI) - const scale = effect.startingWidth / 10.0 - - return vertices.map(vertex => { + const periodx = + (10.0 * effect.period) / + (Math.PI * 2.0) / + Math.cos((-effect.rotation / 180.0) * Math.PI) + const periody = + (10.0 * effect.period) / + (Math.PI * 2.0) / + Math.sin((-effect.rotation / 180.0) * Math.PI) + const scale = effect.width / 10.0 + + return vertices.map((vertex) => { const originalx = vertex.x - effect.x const originaly = vertex.y - effect.y - const x = originalx + scale * Math.sin(originalx/periodx + originaly/periody) - const y = originaly + scale * Math.sin(originalx/periodx + ySign * originaly/periody) + const x = + originalx + scale * Math.sin(originalx / periodx + originaly / periody) + const y = + originaly + + scale * Math.sin(originalx / periodx + (ySign * originaly) / periody) return new Victor(x + effect.x, y + effect.y) }) } circle(effect, vertices) { - const periodx = 10.0 * effect.period / (Math.PI * 2.0) - const periody = 10.0 * effect.period / (Math.PI * 2.0) - const scale = effect.startingWidth / 10.0 + const periodx = (10.0 * effect.period) / (Math.PI * 2.0) + const periody = (10.0 * effect.period) / (Math.PI * 2.0) + const scale = effect.width / 10.0 - return vertices.map(vertex=> { + return vertices.map((vertex) => { const originalx = vertex.x - effect.x const originaly = vertex.y - effect.y - const theta = Math.atan2(originaly,originalx) - const x = originalx + scale * Math.cos(theta) * Math.cos(Math.sqrt(originalx*originalx + originaly*originaly)/periodx) - const y = originaly + scale * Math.sin(theta) * Math.cos(Math.sqrt(originalx*originalx + originaly*originaly)/periody) + const theta = Math.atan2(originaly, originalx) + const x = + originalx + + scale * + Math.cos(theta) * + Math.cos( + Math.sqrt(originalx * originalx + originaly * originaly) / periodx, + ) + const y = + originaly + + scale * + Math.sin(theta) * + Math.cos( + Math.sqrt(originalx * originalx + originaly * originaly) / periody, + ) return new Victor(x + effect.x, y + effect.y) }) } grid(effect, vertices) { - const periodx = 10.0 * effect.period / (Math.PI * 2.0) - const periody = 10.0 * effect.period / (Math.PI * 2.0) - const scale = effect.startingWidth / 10.0 + const periodx = (10.0 * effect.period) / (Math.PI * 2.0) + const periody = (10.0 * effect.period) / (Math.PI * 2.0) + const scale = effect.width / 10.0 - return vertices.map(vertex => { + return vertices.map((vertex) => { const originalx = vertex.x - effect.x const originaly = vertex.y - effect.y - const x = originalx + scale * Math.sin(originalx/periodx) * Math.sin(originaly/periody) - const y = originaly + scale * Math.sin(originalx/periodx) * Math.sin(originaly/periody) + const x = + originalx + + scale * Math.sin(originalx / periodx) * Math.sin(originaly / periody) + const y = + originaly + + scale * Math.sin(originalx / periodx) * Math.sin(originaly / periody) return new Victor(x + effect.x, y + effect.y) }) } shear(effect, vertices) { - const shear = (effect.startingWidth - 1)/ 100 - const xShear = shear * Math.sin(effect.rotation / 180.0 * Math.PI) - const yShear = shear * Math.cos(effect.rotation / 180.0 * Math.PI) - return vertices.map(vertex => new Victor(vertex.x + xShear * vertex.y, vertex.y + yShear * vertex.x)) + const shear = (effect.width - 1) / 100 + const xShear = shear * Math.sin((effect.rotation / 180.0) * Math.PI) + const yShear = shear * Math.cos((effect.rotation / 180.0) * Math.PI) + return vertices.map( + (vertex) => + new Victor(vertex.x + xShear * vertex.y, vertex.y + yShear * vertex.x), + ) } custom(effect, vertices) { - return vertices.map(vertex => { + return vertices.map((vertex) => { try { - const x = evaluate(effect.xMath, {x: vertex.x - effect.x, y: vertex.y - effect.y}) - const y = evaluate(effect.yMath, {x: vertex.x - effect.x, y: vertex.y - effect.y}) + const x = evaluate(effect.xMath, { + x: vertex.x - effect.x, + y: vertex.y - effect.y, + }) + const y = evaluate(effect.yMath, { + x: vertex.x - effect.x, + y: vertex.y - effect.y, + }) return new Victor(x + effect.x, y + effect.y) - } - catch (err) { + } catch (err) { console.log("Error parsing custom effect: " + err) return vertex } diff --git a/src/models/fractal_spirograph/FractalSpirograph.js b/src/models/fractal_spirograph/FractalSpirograph.js index 56b9a985..48904060 100644 --- a/src/models/fractal_spirograph/FractalSpirograph.js +++ b/src/models/fractal_spirograph/FractalSpirograph.js @@ -1,66 +1,64 @@ -import Victor from 'victor' -import Shape, { shapeOptions } from '../Shape' -import Orbit from './Orbit' +import Victor from "victor" +import Model from "../Model" +import Orbit from "./Orbit" const options = { - ...shapeOptions, - ...{ - fractalSpirographVelocity: { - title: 'Velocity', - min: 2 - }, - fractalSpirographResolution: { - title: 'Resolution', - min: 1 - }, - fractalSpirographNumCircles: { - title: 'Number of circles', - min: 1, - max: 6 - }, - fractalSpirographRelativeSize: { - title: 'Relative size (parent to child circle)', - min: 2, - max: 6 - }, - fractalSpirographAlternateRotation: { - title: 'Alternate rotation direction', - type: 'checkbox' - }, - } + fractalSpirographVelocity: { + title: "Velocity", + min: 2, + }, + fractalSpirographResolution: { + title: "Resolution", + min: 1, + }, + fractalSpirographNumCircles: { + title: "Number of circles", + min: 1, + max: 6, + }, + fractalSpirographRelativeSize: { + title: "Relative size (parent to child circle)", + min: 2, + max: 6, + }, + fractalSpirographAlternateRotation: { + title: "Alternate rotation direction", + type: "checkbox", + }, } // Inspired/adapted from https://thecodingtrain.com/CodingChallenges/061-fractal-spirograph // No license was specified. -export default class FractalSpirograph extends Shape { +export default class FractalSpirograph extends Model { constructor() { - super('Fractal Spirograph') - this.link = 'https://benice-equation.blogspot.com/2012/01/fractal-spirograph.html' - this.linkText = 'Fun math art (pictures) - benice equation' + super("fractalSpirograph") + this.label = "Fractal Spirograph" + this.link = + "https://benice-equation.blogspot.com/2012/01/fractal-spirograph.html" + this.linkText = "Fun math art (pictures) - benice equation" } getInitialState() { return { ...super.getInitialState(), ...{ - type: 'fractal_spirograph', fractalSpirographVelocity: 8, fractalSpirographResolution: 2, fractalSpirographNumCircles: 5, fractalSpirographRelativeSize: 3, fractalSpirographAlternateRotation: true, - } + }, } } getVertices(state) { let resolution = parseInt(state.shape.fractalSpirographResolution) let settings = { - resolution: resolution, + resolution, velocity: parseInt(state.shape.fractalSpirographVelocity), numCircles: parseInt(state.shape.fractalSpirographNumCircles), relativeSize: parseInt(state.shape.fractalSpirographRelativeSize), - alternateRotation: state.shape.fractalSpirographAlternateRotation + alternateRotation: state.shape.fractalSpirographAlternateRotation, } let sun = new Orbit(0, 0, 1, 0, settings) @@ -68,13 +66,13 @@ export default class FractalSpirograph extends Shape { let end let points = [] - for (var i=0; i point.multiply({x: scale, y: scale })) + points.forEach((point) => point.multiply({ x: scale, y: scale })) return points } diff --git a/src/models/fractal_spirograph/Orbit.js b/src/models/fractal_spirograph/Orbit.js index 2d41752c..d428c160 100644 --- a/src/models/fractal_spirograph/Orbit.js +++ b/src/models/fractal_spirograph/Orbit.js @@ -4,12 +4,15 @@ export default class Orbit { this.y = y this.r = r this.child = null - this.angle = Math.PI/2 + this.angle = Math.PI / 2 this.level = level this.settings = settings let sign = this.settings.alternateRotation ? -1 : 1 - this.speed = Math.pow(settings.velocity * sign, this.level - 1) * Math.PI / 180 / settings.resolution + this.speed = + (Math.pow(settings.velocity * sign, this.level - 1) * Math.PI) / + 180 / + settings.resolution this.parent = parent } @@ -17,7 +20,14 @@ export default class Orbit { let newr = this.r / this.settings.relativeSize let newx = this.x + this.r + newr let newy = this.y - this.child = new Orbit(newx, newy, newr, this.level + 1, this.settings, this) + this.child = new Orbit( + newx, + newy, + newr, + this.level + 1, + this.settings, + this, + ) return this.child } diff --git a/src/models/input_text/Fonts.js b/src/models/input_text/Fonts.js index 077137fb..b8aceaf5 100644 --- a/src/models/input_text/Fonts.js +++ b/src/models/input_text/Fonts.js @@ -1,6 +1,6 @@ -import Victor from 'victor' -import { raysol_cursive } from './raysol_cursive' -import { raysol_sanserif } from './raysol_sanserif' +import Victor from "victor" +import { raysol_cursive } from "./raysol_cursive" +import { raysol_sanserif } from "./raysol_sanserif" const fontSpacing = 1.5 @@ -53,74 +53,154 @@ let billsey = { // This is a clever way to create a range from 0..32, and then compute an x,y for each of those // points on the unit circle from zero to pi/2. const curve = [...Array(32).keys()].map((index) => { - let angle = (index+1) * Math.PI/2.0/32.0 + let angle = ((index + 1) * Math.PI) / 2.0 / 32.0 return new Victor(Math.cos(angle), Math.sin(angle)) }) const billseyConverter = (vertices) => { let newVertices = [] - let prevPoint = new Victor(0,0) - vertices.forEach( (vertex) => { + let prevPoint = new Victor(0, 0) + vertices.forEach((vertex) => { switch (vertex[2]) { - case 0: - default: - newVertices.push(new Victor(vertex[0] / 8.0, vertex[1] / 4.0)) - break - case 1: // NE - if (vertex[1] < prevPoint[1]) { // clockwise - let width = Math.abs(vertex[0] - prevPoint[0]) / 8.0 - let height = Math.abs(vertex[1] - prevPoint[1]) / 4.0 - newVertices = newVertices.concat(curve.map( cv => new Victor(cv.x * width + prevPoint[0] / 8.0, cv.y * height + vertex[1] / 4.0)).reverse()) - } else { // ccwise - let width = Math.abs(vertex[0] - prevPoint[0]) / 8.0 - let height = Math.abs(vertex[1] - prevPoint[1]) / 4.0 - newVertices = newVertices.concat(curve.map( cv => new Victor(cv.x * width + vertex[0] / 8.0, cv.y * height + prevPoint[1] / 4.0))) - } - newVertices.push(new Victor(vertex[0] / 8.0, vertex[1] / 4.0)) - break - case 2: // SE - if (vertex[1] < prevPoint[1]) { // clockwise - let width = Math.abs(vertex[0] - prevPoint[0]) / 8.0 - let height = -Math.abs(vertex[1] - prevPoint[1]) / 4.0 - newVertices = newVertices.concat(curve.map( cv => new Victor(cv.x * width + vertex[0] / 8.0, cv.y * height + prevPoint[1] / 4.0))) - } else { // ccwise - let width = Math.abs(vertex[0] - prevPoint[0]) / 8.0 - let height = -Math.abs(vertex[1] - prevPoint[1]) / 4.0 - newVertices = newVertices.concat(curve.map( cv => new Victor(cv.x * width + prevPoint[0] / 8.0, cv.y * height + vertex[1] / 4.0)).reverse()) - } - newVertices.push(new Victor(vertex[0] / 8.0, vertex[1] / 4.0)) - break - case 3: // SW - if (vertex[1] > prevPoint[1]) { // clockwise - let width = -Math.abs(vertex[0] - prevPoint[0]) / 8.0 - let height = -Math.abs(vertex[1] - prevPoint[1]) / 4.0 - newVertices = newVertices.concat(curve.map( cv => new Victor(cv.x * width + prevPoint[0] / 8.0, cv.y * height + vertex[1] / 4.0)).reverse()) - } else { // ccwise - let width = -Math.abs(vertex[0] - prevPoint[0]) / 8.0 - let height = -Math.abs(vertex[1] - prevPoint[1]) / 4.0 - newVertices = newVertices.concat(curve.map( cv => new Victor(cv.x * width + vertex[0] / 8.0, cv.y * height + prevPoint[1] / 4.0))) - } - newVertices.push(new Victor(vertex[0] / 8.0, vertex[1] / 4.0)) - break - case 4: // NW - if (vertex[1] > prevPoint[1]) { // clockwise - let width = -Math.abs(vertex[0] - prevPoint[0]) / 8.0 - let height = Math.abs(vertex[1] - prevPoint[1]) / 4.0 - newVertices = newVertices.concat(curve.map( cv => new Victor(cv.x * width + vertex[0] / 8.0, cv.y * height + prevPoint[1] / 4.0))) - } else { // ccwise - let width = -Math.abs(vertex[0] - prevPoint[0]) / 8.0 - let height = Math.abs(vertex[1] - prevPoint[1]) / 4.0 - newVertices = newVertices.concat(curve.map( cv => new Victor(cv.x * width + prevPoint[0] / 8.0, cv.y * height + vertex[1] / 4.0)).reverse()) - } - newVertices.push(new Victor(vertex[0] / 8.0, vertex[1] / 4.0)) - break + case 0: + default: + newVertices.push(new Victor(vertex[0] / 8.0, vertex[1] / 4.0)) + break + case 1: // NE + if (vertex[1] < prevPoint[1]) { + // clockwise + let width = Math.abs(vertex[0] - prevPoint[0]) / 8.0 + let height = Math.abs(vertex[1] - prevPoint[1]) / 4.0 + newVertices = newVertices.concat( + curve + .map( + (cv) => + new Victor( + cv.x * width + prevPoint[0] / 8.0, + cv.y * height + vertex[1] / 4.0, + ), + ) + .reverse(), + ) + } else { + // ccwise + let width = Math.abs(vertex[0] - prevPoint[0]) / 8.0 + let height = Math.abs(vertex[1] - prevPoint[1]) / 4.0 + newVertices = newVertices.concat( + curve.map( + (cv) => + new Victor( + cv.x * width + vertex[0] / 8.0, + cv.y * height + prevPoint[1] / 4.0, + ), + ), + ) + } + newVertices.push(new Victor(vertex[0] / 8.0, vertex[1] / 4.0)) + break + case 2: // SE + if (vertex[1] < prevPoint[1]) { + // clockwise + let width = Math.abs(vertex[0] - prevPoint[0]) / 8.0 + let height = -Math.abs(vertex[1] - prevPoint[1]) / 4.0 + newVertices = newVertices.concat( + curve.map( + (cv) => + new Victor( + cv.x * width + vertex[0] / 8.0, + cv.y * height + prevPoint[1] / 4.0, + ), + ), + ) + } else { + // ccwise + let width = Math.abs(vertex[0] - prevPoint[0]) / 8.0 + let height = -Math.abs(vertex[1] - prevPoint[1]) / 4.0 + newVertices = newVertices.concat( + curve + .map( + (cv) => + new Victor( + cv.x * width + prevPoint[0] / 8.0, + cv.y * height + vertex[1] / 4.0, + ), + ) + .reverse(), + ) + } + newVertices.push(new Victor(vertex[0] / 8.0, vertex[1] / 4.0)) + break + case 3: // SW + if (vertex[1] > prevPoint[1]) { + // clockwise + let width = -Math.abs(vertex[0] - prevPoint[0]) / 8.0 + let height = -Math.abs(vertex[1] - prevPoint[1]) / 4.0 + newVertices = newVertices.concat( + curve + .map( + (cv) => + new Victor( + cv.x * width + prevPoint[0] / 8.0, + cv.y * height + vertex[1] / 4.0, + ), + ) + .reverse(), + ) + } else { + // ccwise + let width = -Math.abs(vertex[0] - prevPoint[0]) / 8.0 + let height = -Math.abs(vertex[1] - prevPoint[1]) / 4.0 + newVertices = newVertices.concat( + curve.map( + (cv) => + new Victor( + cv.x * width + vertex[0] / 8.0, + cv.y * height + prevPoint[1] / 4.0, + ), + ), + ) + } + newVertices.push(new Victor(vertex[0] / 8.0, vertex[1] / 4.0)) + break + case 4: // NW + if (vertex[1] > prevPoint[1]) { + // clockwise + let width = -Math.abs(vertex[0] - prevPoint[0]) / 8.0 + let height = Math.abs(vertex[1] - prevPoint[1]) / 4.0 + newVertices = newVertices.concat( + curve.map( + (cv) => + new Victor( + cv.x * width + vertex[0] / 8.0, + cv.y * height + prevPoint[1] / 4.0, + ), + ), + ) + } else { + // ccwise + let width = -Math.abs(vertex[0] - prevPoint[0]) / 8.0 + let height = Math.abs(vertex[1] - prevPoint[1]) / 4.0 + newVertices = newVertices.concat( + curve + .map( + (cv) => + new Victor( + cv.x * width + prevPoint[0] / 8.0, + cv.y * height + vertex[1] / 4.0, + ), + ) + .reverse(), + ) + } + newVertices.push(new Victor(vertex[0] / 8.0, vertex[1] / 4.0)) + break } prevPoint = vertex }) const scale = 0.6 const offset_y = -0.5 - const scaledVertices = newVertices.map( vertex => { + const scaledVertices = newVertices.map((vertex) => { return new Victor(scale * vertex.x, scale * vertex.y + offset_y) }) return { @@ -131,7 +211,7 @@ const billseyConverter = (vertices) => { const raysolConverter = (vertices) => { let newVertices = [] - vertices.forEach( (vertex) => { + vertices.forEach((vertex) => { newVertices.push(new Victor(vertex[0], vertex[1])) }) @@ -147,7 +227,7 @@ export const MonospaceFont = (ch) => { if (billsey.hasOwnProperty(upper)) { return billseyConverter(billsey[upper]) } else { - return billseyConverter(billsey[' ']) + return billseyConverter(billsey[" "]) } } @@ -156,7 +236,7 @@ export const CursiveFont = (ch) => { if (raysol_cursive.hasOwnProperty(ch)) { return raysolConverter(raysol_cursive[ch]) } else { - return raysolConverter(raysol_cursive[' ']) + return raysolConverter(raysol_cursive[" "]) } } @@ -165,6 +245,6 @@ export const SansSerifFont = (ch) => { if (raysol_cursive.hasOwnProperty(ch)) { return raysolConverter(raysol_sanserif[ch]) } else { - return raysolConverter(raysol_sanserif[' ']) + return raysolConverter(raysol_sanserif[" "]) } } diff --git a/src/models/input_text/InputText.js b/src/models/input_text/InputText.js index 1f9bdd8b..dcb0b4eb 100644 --- a/src/models/input_text/InputText.js +++ b/src/models/input_text/InputText.js @@ -1,32 +1,29 @@ -import { CursiveFont, SansSerifFont, MonospaceFont } from './Fonts' -import Victor from 'victor' -import Shape, { shapeOptions } from '../Shape' -import { arc } from '@/common/geometry' +import { CursiveFont, SansSerifFont, MonospaceFont } from "./Fonts" +import Victor from "victor" +import Model from "@/models/Model" +import { arc } from "@/common/geometry" const options = { - ...shapeOptions, - ...{ - inputText: { - title: 'Text', - type: 'textarea', - }, - inputFont: { - title: 'Font', - type: 'dropdown', - choices: ['Cursive', 'Sans Serif', 'Monospace'], - }, - rotateDir: { - title: 'Rotate', - type: 'dropdown', - choices: ['Top', 'Center', 'Bottom'], - }, - } + inputText: { + title: "Text", + type: "textarea", + }, + inputFont: { + title: "Font", + type: "dropdown", + choices: ["Cursive", "Sans Serif", "Monospace"], + }, + rotateDir: { + title: "Rotate", + type: "dropdown", + choices: ["Top", "Center", "Bottom"], + }, } function getMaxX(points) { // Measure the width of the line let maxX = 0 - points.forEach( (point) => { + points.forEach((point) => { if (point.x > maxX) { maxX = point.x } @@ -34,45 +31,51 @@ function getMaxX(points) { return maxX } -export default class InputText extends Shape { +export default class InputText extends Model { constructor() { - super('Text') + super("inputText") + this.label = "Text" } getInitialState() { return { ...super.getInitialState(), ...{ - type: 'inputText', - inputText: 'Sandify', - inputFont: 'Cursive', - rotateDir: 'Center', - } + inputText: "Sandify", + inputFont: "Cursive", + rotateDir: "Center", + }, } } getVertices(state) { let points = [] - let prevLetter = '' + let prevLetter = "" let x = 0.0 let lines = [] let textPoints = [] for (let chi = 0; chi < state.shape.inputText.length; chi++) { var nextLetter = state.shape.inputText[chi] - if (prevLetter === 'b' || prevLetter === 'v' || prevLetter === 'o' || prevLetter === 'w') { + if ( + prevLetter === "b" || + prevLetter === "v" || + prevLetter === "o" || + prevLetter === "w" + ) { // Save this letter before we possibly add in a '*' prevLetter = nextLetter - if (nextLetter.search('[a-z]+') !== -1 && state.shape.inputFont === 'Cursive') - { - nextLetter = nextLetter + '*' + if ( + nextLetter.search("[a-z]+") !== -1 && + state.shape.inputFont === "Cursive" + ) { + nextLetter = nextLetter + "*" } - } - else { + } else { prevLetter = nextLetter } - if (nextLetter === '\n') { + if (nextLetter === "\n") { // New line lines.push(points) points = [] @@ -81,11 +84,11 @@ export default class InputText extends Shape { } var shape = undefined - if (state.shape.inputFont === 'Cursive') { + if (state.shape.inputFont === "Cursive") { shape = CursiveFont(nextLetter) - } else if (state.shape.inputFont === 'Sans Serif') { + } else if (state.shape.inputFont === "Sans Serif") { shape = SansSerifFont(nextLetter) - } else if (state.shape.inputFont === 'Monospace') { + } else if (state.shape.inputFont === "Monospace") { shape = MonospaceFont(nextLetter) } else { // Internal error, but I'm going to just recover @@ -96,7 +99,7 @@ export default class InputText extends Shape { for (let vi = 0; vi < shape.vertices.length; vi++) { points.push(new Victor(shape.vertices[vi].x + x, shape.vertices[vi].y)) } - x += shape.vertices[shape.vertices.length-1].x + x += shape.vertices[shape.vertices.length - 1].x } // Save the last line we were working on. lines.push(points) @@ -104,13 +107,13 @@ export default class InputText extends Shape { // The height of a row of text, including the space above. const maxY = 2.4 - if (state.shape.rotateDir === 'Center') { + if (state.shape.rotateDir === "Center") { // Starting Y offset - let y = (lines.length - 1) * maxY / 2.0 + let y = ((lines.length - 1) * maxY) / 2.0 // Capture some wrap around points, to connect the lines. let connectorPoints = [] - lines.forEach( (points) => { + lines.forEach((points) => { let maxX = getMaxX(points) let widthOffset = maxX / 2.0 @@ -119,9 +122,12 @@ export default class InputText extends Shape { connectorPoints = [] // offset the line's vertices - textPoints = [...textPoints, ...points.map( (point) => { - return new Victor(point.x - widthOffset, point.y + y) - })] + textPoints = [ + ...textPoints, + ...points.map((point) => { + return new Victor(point.x - widthOffset, point.y + y) + }), + ] // Add in some points way off, so to wrap around for this line. connectorPoints.push(new Victor(1e9, y)) @@ -133,7 +139,7 @@ export default class InputText extends Shape { } else { // This variable controls "Top" vs. "Bottom" let direction = 1.0 - if (state.shape.rotateDir === 'Bottom') { + if (state.shape.rotateDir === "Bottom") { direction = -1.0 lines.reverse() } @@ -159,7 +165,7 @@ export default class InputText extends Shape { // const maxRPerY = 0.8 let rPerY = direction * maxRPerY - let thetaCenter = direction * Math.PI / 2.0 + let thetaCenter = (direction * Math.PI) / 2.0 const maxROffset = maxY * 2.0 let rOffset = maxROffset const rOffsetPerLine = rOffset / lines.length @@ -168,8 +174,7 @@ export default class InputText extends Shape { // This captures the previous angle, so we can track around for the next line. let lastTheta - lines.forEach( (points) => { - + lines.forEach((points) => { let maxX = getMaxX(points) // This widthOffset is in X. let widthOffset = maxX / 2.0 @@ -177,7 +182,7 @@ export default class InputText extends Shape { // Scale the size of the words to fit within one circle. if (Math.PI * 2.0 < Math.abs(thetaPerX * maxX)) { // We are going to roll all the way around - thetaPerX = direction * -Math.PI * 2.0 / maxX + thetaPerX = (direction * -Math.PI * 2.0) / maxX rPerY = -thetaPerX * rOffset } @@ -195,21 +200,24 @@ export default class InputText extends Shape { } // Transform the points and add them to textPoints. - textPoints = [...textPoints, ...points.map( (point) => { - const r = rOffset + rPerY * point.y - lastTheta = thetaCenter + thetaPerX * (point.x - widthOffset) - return new Victor(r * Math.cos(lastTheta), r * Math.sin(lastTheta)) - })] + textPoints = [ + ...textPoints, + ...points.map((point) => { + const r = rOffset + rPerY * point.y + lastTheta = thetaCenter + thetaPerX * (point.x - widthOffset) + return new Victor(r * Math.cos(lastTheta), r * Math.sin(lastTheta)) + }), + ] // Set up for the next line. rOffset -= rOffsetPerLine - rPerY = direction * Math.sqrt(maxRPerY * rOffset / maxROffset) + rPerY = direction * Math.sqrt((maxRPerY * rOffset) / maxROffset) thetaPerX = -rPerY / rOffset }) } const scale = 2.5 // to normalize starting size - textPoints.forEach(point => point.multiply({x: scale, y: scale })) + textPoints.forEach((point) => point.multiply({ x: scale, y: scale })) return textPoints } diff --git a/src/models/lsystem/LSystem.js b/src/models/lsystem/LSystem.js index 1291a242..30ac6232 100644 --- a/src/models/lsystem/LSystem.js +++ b/src/models/lsystem/LSystem.js @@ -1,52 +1,49 @@ -import Shape, { shapeOptions } from '../Shape' +import Model from "../Model" import { lsystem, lsystemPath, onSubtypeChange, onMinIterations, - onMaxIterations -} from '@/common/lindenmayer' -import { subtypes } from './subtypes' -import { resizeVertices } from '@/common/geometry' + onMaxIterations, +} from "@/common/lindenmayer" +import { subtypes } from "./subtypes" +import { resizeVertices } from "@/common/geometry" const options = { - ...shapeOptions, - ...{ - subtype: { - title: 'Type', - type: 'dropdown', - choices: Object.keys(subtypes), - onChange: (changes, attrs) => { - return onSubtypeChange(subtypes[changes.subtype], changes, attrs) - } + subtype: { + title: "Type", + type: "dropdown", + choices: Object.keys(subtypes), + onChange: (model, changes, state) => { + return onSubtypeChange(subtypes[changes.subtype], changes, state) }, - iterations: { - title: 'Iterations', - min: (state) => { - return onMinIterations(subtypes[state.subtype], state) - }, - max: (state) => { - return onMaxIterations(subtypes[state.subtype], state) - } + }, + iterations: { + title: "Iterations", + min: (state) => { + return onMinIterations(subtypes[state.subtype], state) }, - } + max: (state) => { + return onMaxIterations(subtypes[state.subtype], state) + }, + }, } -export default class LSystem extends Shape { +export default class LSystem extends Model { constructor() { - super('Fractal Line Writer') - this.link = 'https://en.wikipedia.org/wiki/L-system' - this.linkText = 'L-systems on Wikipedia' + super("lsystem") + this.label = "Fractal Line Writer" + this.link = "https://en.wikipedia.org/wiki/L-system" + this.linkText = "L-systems on Wikipedia" } getInitialState() { return { ...super.getInitialState(), ...{ - type: 'lsystem', iterations: 3, - subtype: 'McWorter\'s Pentadendrite', - } + subtype: "McWorter's Pentadendrite", + }, } } @@ -59,7 +56,9 @@ export default class LSystem extends Shape { config.iterations = iterations config.side = 5 - if (config.angle === undefined) { config.angle = Math.PI/2 } + if (config.angle === undefined) { + config.angle = Math.PI / 2 + } let curve = lsystemPath(lsystem(config), config) const scale = 18.0 // to normalize starting size diff --git a/src/models/lsystem/subtypes.js b/src/models/lsystem/subtypes.js index 2c79663a..1fa69137 100644 --- a/src/models/lsystem/subtypes.js +++ b/src/models/lsystem/subtypes.js @@ -1,222 +1,222 @@ // L-system instructions for space filling curves export const subtypes = { // http://mathforum.org/advanced/robertd/lsys2d.html - '32-segment': { - axiom: 'F+F+F+F', - draw: ['F'], - rules: { - F: '-F+F-F-F+F+FF-F+F+FF+F-F-FF+FF-FF+F+F-FF-F-F+FF-F-F+F+F-F+' + "32-segment": { + axiom: "F+F+F+F", + draw: ["F"], + rules: { + F: "-F+F-F-F+F+FF-F+F+FF+F-F-FF+FF-FF+F+F-FF-F-F+FF-F-F+F+F-F+", }, startingAngle: Math.PI, - maxIterations: 2 + maxIterations: 2, }, // http://www.kevs3d.co.uk/dev/lsystems/ - 'Cog Triangle': { - axiom: 'W----W----W', - draw: ['F'], - rules: { - W: '+++X--F--ZFX+', - X: '---W++F++YFW-', - Y: '+ZFX--F--Z+++', - Z: '-YFW++F++Y---' - }, - angle: Math.PI/6, + "Cog Triangle": { + axiom: "W----W----W", + draw: ["F"], + rules: { + W: "+++X--F--ZFX+", + X: "---W++F++YFW-", + Y: "+ZFX--F--Z+++", + Z: "-YFW++F++Y---", + }, + angle: Math.PI / 6, minIterations: 1, - maxIterations: 8 + maxIterations: 8, }, // https://onlinemathtools.com/l-system-generator - 'Fractal Tree': { - axiom: 'F', - draw: ['F'], - rules: { - F: 'F[+FF][-FF]F[-F][+F]F', + "Fractal Tree": { + axiom: "F", + draw: ["F"], + rules: { + F: "F[+FF][-FF]F[-F][+F]F", }, - angle: Math.PI/5, - maxIterations: 5 + angle: Math.PI / 5, + maxIterations: 5, }, // https://www.vexlio.com/blog/drawing-simple-organics-with-l-systems/ - 'Fractal Tree 2': { - axiom: 'F', - draw: ['F'], - rules: { - F: 'F[-F][+F]', + "Fractal Tree 2": { + axiom: "F", + draw: ["F"], + rules: { + F: "F[-F][+F]", }, - angle: 10*Math.PI/72, - maxIterations: 9 + angle: (10 * Math.PI) / 72, + maxIterations: 9, }, // https://www.vexlio.com/blog/drawing-simple-organics-with-l-systems/ - 'Fractal Tree 3': { - axiom: 'X', - draw: ['F'], - rules: { - F: 'FF', - X: 'F+[-F-XF-X][+FF][--XF[+X]][++F-X]' + "Fractal Tree 3": { + axiom: "X", + draw: ["F"], + rules: { + F: "FF", + X: "F+[-F-XF-X][+FF][--XF[+X]][++F-X]", }, - angle: Math.PI/8, - maxIterations: 6 + angle: Math.PI / 8, + maxIterations: 6, }, // https://www.vexlio.com/blog/drawing-simple-organics-with-l-systems/ - 'Fractal Tree 4': { - axiom: 'X', - draw: ['F'], - rules: { - F: 'FX[FX[+XF]]', - X: 'FF[+XZ++X-F[+ZX]][-X++F-X]', - Z: '[+F-X-F][++ZX]' + "Fractal Tree 4": { + axiom: "X", + draw: ["F"], + rules: { + F: "FX[FX[+XF]]", + X: "FF[+XZ++X-F[+ZX]][-X++F-X]", + Z: "[+F-X-F][++ZX]", }, - angle: Math.PI/8, - maxIterations: 5 + angle: Math.PI / 8, + maxIterations: 5, }, // http://algorithmicbotany.org/papers/abop/abop-ch1.pdf - 'Fractal Tree 5': { - axiom: 'X', - draw: ['F'], - rules: { - X: 'F[+X]F[-X]+X', - F: 'FF' + "Fractal Tree 5": { + axiom: "X", + draw: ["F"], + rules: { + X: "F[+X]F[-X]+X", + F: "FF", }, - angle: Math.PI/9, - maxIterations: 8 + angle: Math.PI / 9, + maxIterations: 8, }, // http://mathforum.org/advanced/robertd/lsys2d.html - 'Gosper (flowsnake)': { - axiom: 'A', - draw: ['A', 'B'], - rules: { - A: 'A-B--B+A++AA+B-', - B: '+A-BB--B-A++A+B' + "Gosper (flowsnake)": { + axiom: "A", + draw: ["A", "B"], + rules: { + A: "A-B--B+A++AA+B-", + B: "+A-BB--B-A++A+B", }, angle: Math.PI / 3, - maxIterations: 5 + maxIterations: 5, }, // http://mathforum.org/advanced/robertd/lsys2d.html - 'Ice': { - axiom: 'F+F+F+F', - draw: ['F'], - rules: { - F: 'FF+F++F+F' + Ice: { + axiom: "F+F+F+F", + draw: ["F"], + rules: { + F: "FF+F++F+F", }, startingAngle: Math.PI, - maxIterations: 6 + maxIterations: 6, }, // https://fronkonstin.com/2017/06/26/a-shiny-app-to-draw-curves-based-on-l-systems/ - 'Koch Cube 1': { - axiom: 'F-F-F-F', - draw: ['F'], - rules: { - F: 'FF-F-F-F-FF', + "Koch Cube 1": { + axiom: "F-F-F-F", + draw: ["F"], + rules: { + F: "FF-F-F-F-FF", }, - maxIterations: 5 + maxIterations: 5, }, // http://algorithmicbotany.org/papers/abop/abop-ch1.pdf - 'Koch Cube 2': { - axiom: 'F-F-F-F', - draw: ['F'], - rules: { - F: 'FF-F+F-F-FF', + "Koch Cube 2": { + axiom: "F-F-F-F", + draw: ["F"], + rules: { + F: "FF-F+F-F-FF", }, - maxIterations: 5 + maxIterations: 5, }, // https://onlinemathtools.com/l-system-generator - 'Koch Curve': { - axiom: 'F', - draw: ['F'], - rules: { - F: 'F+F--F+F' + "Koch Curve": { + axiom: "F", + draw: ["F"], + rules: { + F: "F+F--F+F", }, - angle: 4*Math.PI/9, + angle: (4 * Math.PI) / 9, startingAngle: Math.PI, - maxIterations: 7 + maxIterations: 7, }, // https://fronkonstin.com/2017/06/26/a-shiny-app-to-draw-curves-based-on-l-systems/ - 'Koch Flower': { - axiom: 'F-F-F-F', - draw: ['F'], - rules: { - F: 'FF-F-F-F-F-F+F', + "Koch Flower": { + axiom: "F-F-F-F", + draw: ["F"], + rules: { + F: "FF-F-F-F-F-F+F", }, - maxIterations: 4 + maxIterations: 4, }, // http://mathforum.org/advanced/robertd/lsys2d.html - 'Koch Island': { - axiom: 'F+F+F+F', - draw: ['F'], - rules: { - F: 'F+F-F-FF+F+F-F' + "Koch Island": { + axiom: "F+F+F+F", + draw: ["F"], + rules: { + F: "F+F-F-FF+F+F-F", }, startingAngle: Math.PI, - maxIterations: 4 + maxIterations: 4, }, - 'Koch Snowflake': { - axiom: 'F--F--F--', - draw: ['F'], - rules: { - F: 'F+F--F+F' + "Koch Snowflake": { + axiom: "F--F--F--", + draw: ["F"], + rules: { + F: "F+F--F+F", }, angle: Math.PI / 3, startingAngle: -Math.PI / 3, - maxIterations: 5 + maxIterations: 5, }, // http://mathforum.org/advanced/robertd/lsys2d.html - 'McWorter\'s Pentadendrite': { - axiom: 'F-F-F-F-F', - draw: ['F'], - rules: { - F: 'F-F-F++F+F-F', + "McWorter's Pentadendrite": { + axiom: "F-F-F-F-F", + draw: ["F"], + rules: { + F: "F-F-F++F+F-F", }, - angle: 2*Math.PI/5, - maxIterations: 5 + angle: (2 * Math.PI) / 5, + maxIterations: 5, }, // https://onlinemathtools.com/l-system-generator - 'Penrose Tile': { - axiom: '[7]++[7]++[7]++[7]++[7]', - draw: ['6', '7', '8', '9'], + "Penrose Tile": { + axiom: "[7]++[7]++[7]++[7]++[7]", + draw: ["6", "7", "8", "9"], rules: { - 6: '8++9----7[-8----6]++', - 7: '+8--9[---6--7]+', - 8: '-6++7[+++8++9]-', - 9: '--8++++6[+9++++7]--7' + 6: "8++9----7[-8----6]++", + 7: "+8--9[---6--7]+", + 8: "-6++7[+++8++9]-", + 9: "--8++++6[+9++++7]--7", }, - angle: Math.PI/5, - maxIterations: 6 + angle: Math.PI / 5, + maxIterations: 6, }, - 'Plusses': { - axiom: 'XYXYXYX+XYXYXYX+XYXYXYX+XYXYXYX', - draw: ['F'], - rules: { - X: 'FX+FX+FXFY-FY-', - Y: '+FX+FXFY-FY-FY' + Plusses: { + axiom: "XYXYXYX+XYXYXYX+XYXYXYX+XYXYXYX", + draw: ["F"], + rules: { + X: "FX+FX+FXFY-FY-", + Y: "+FX+FXFY-FY-FY", }, - maxIterations: 4 + maxIterations: 4, }, // http://mathforum.org/advanced/robertd/lsys2d.html - 'Red Dragon': { - axiom: 'FA', - draw: ['F'], - rules: { - A: 'A+BF+', - B: '-FA-B' + "Red Dragon": { + axiom: "FA", + draw: ["F"], + rules: { + A: "A+BF+", + B: "-FA-B", }, minIterations: 9, maxIterations: 15, - startingAngle: Math.PI + startingAngle: Math.PI, }, // http://mathforum.org/advanced/robertd/lsys2d.html - 'Sierpinski Triangle (arrowhead)': { - axiom: 'X', - draw: ['X', 'Y'], - rules: { - X: 'Y+X+Y', - Y: 'X-Y-X' + "Sierpinski Triangle (arrowhead)": { + axiom: "X", + draw: ["X", "Y"], + rules: { + X: "Y+X+Y", + Y: "X-Y-X", }, angle: Math.PI / 3, startingAngle: (i) => { if (i % 2 === 0) { return 0 } else { - return -Math.PI/3 + return -Math.PI / 3 } }, - maxIterations: 8 + maxIterations: 8, }, } diff --git a/src/models/space_filler/SpaceFiller.js b/src/models/space_filler/SpaceFiller.js index 972edaf2..7b9acc21 100644 --- a/src/models/space_filler/SpaceFiller.js +++ b/src/models/space_filler/SpaceFiller.js @@ -1,56 +1,61 @@ -import Shape, { shapeOptions } from '../Shape' +import Model from "../Model" import { lsystem, lsystemPath, onSubtypeChange, onMinIterations, - onMaxIterations -} from '@/common/lindenmayer' -import { resizeVertices } from '@/common/geometry' -import { subtypes } from './subtypes' + onMaxIterations, +} from "@/common/lindenmayer" +import { resizeVertices } from "@/common/geometry" +import { subtypes } from "./subtypes" const options = { - ...shapeOptions, - ...{ - fillerSubtype: { - title: 'Type', - type: 'dropdown', - choices: Object.keys(subtypes), - onChange: (changes, attrs) => { - return onSubtypeChange(subtypes[changes.fillerSubtype], changes, attrs) - } + fillerSubtype: { + title: "Type", + type: "dropdown", + choices: Object.keys(subtypes), + onChange: (model, changes, state) => { + return onSubtypeChange(subtypes[changes.fillerSubtype], changes, state) }, - iterations: { - title: 'Iterations', - min: (state) => { - return onMinIterations(subtypes[state.fillerSubtype], state) - }, - max: (state) => { - return onMaxIterations(subtypes[state.fillerSubtype], state) - } - } - } + }, + iterations: { + title: "Iterations", + min: (state) => { + return onMinIterations(subtypes[state.fillerSubtype], state) + }, + max: (state) => { + return onMaxIterations(subtypes[state.fillerSubtype], state) + }, + }, } -export default class SpaceFiller extends Shape { +export default class SpaceFiller extends Model { constructor() { - super('Space Filler') - this.linkText = 'Fractal charm: space filling curves' - this.link = 'https://www.youtube.com/watch?v=RU0wScIj36o' + super("spaceFiller") + this.label = "Space Filler" + this.usesMachine = true + this.autosize = false + this.canMove = false + this.selectGroup = "Erasers" + this.linkText = "Fractal charm: space filling curves" + this.link = "https://www.youtube.com/watch?v=RU0wScIj36o" + } + + canChangeSize(state) { + return false + } + + canRotate(state) { + return false } getInitialState() { return { ...super.getInitialState(), ...{ - type: 'space_filler', - selectGroup: 'Erasers', iterations: 6, - fillerSubtype: 'Hilbert', - canChangeSize: false, - autosize: false, - usesMachine: true, - } + fillerSubtype: "Hilbert", + }, } } @@ -70,17 +75,24 @@ export default class SpaceFiller extends Shape { let config = subtypes[state.shape.fillerSubtype] config.iterations = iterations - if (config.side === undefined) { config.side = 5 } - if (config.angle === undefined) { config.angle = Math.PI/2 } + if (config.side === undefined) { + config.side = 5 + } + if (config.angle === undefined) { + config.angle = Math.PI / 2 + } let curve = lsystemPath(lsystem(config), config) let scale = 1 if (config.iterationsGrow) { - scale = (typeof config.iterationsGrow === 'function') ? config.iterationsGrow(config) : config.iterationsGrow + scale = + typeof config.iterationsGrow === "function" + ? config.iterationsGrow(config) + : config.iterationsGrow } - return resizeVertices(curve, sizeX*scale, sizeY*scale) + return resizeVertices(curve, sizeX * scale, sizeY * scale) } getOptions() { diff --git a/src/models/space_filler/subtypes.js b/src/models/space_filler/subtypes.js index 58870ff4..dc54298d 100644 --- a/src/models/space_filler/subtypes.js +++ b/src/models/space_filler/subtypes.js @@ -1,76 +1,76 @@ // L-system instructions for space filling curves export const subtypes = { // http://mathforum.org/advanced/robertd/lsys2d.html - 'Gosper (flowsnake)': { - axiom: 'A', - draw: ['A', 'B'], - rules: { - A: 'A-B--B+A++AA+B-', - B: '+A-BB--B-A++A+B' + "Gosper (flowsnake)": { + axiom: "A", + draw: ["A", "B"], + rules: { + A: "A-B--B+A++AA+B-", + B: "+A-BB--B-A++A+B", }, angle: Math.PI / 3, iterationsGrow: (config) => { return config.iterations }, - maxIterations: 6 + maxIterations: 6, }, // http://mathforum.org/advanced/robertd/lsys2d.html - 'Hilbert': { - axiom: 'L', - draw: 'F', + Hilbert: { + axiom: "L", + draw: "F", rules: { - L: '+RF-LFL-FR+', - R: '-LF+RFR+FL-' + L: "+RF-LFL-FR+", + R: "-LF+RFR+FL-", }, startingAngle: Math.PI, - minIterations: 2 + minIterations: 2, }, // http://mathforum.org/advanced/robertd/lsys2d.html - 'Hilbert 2': { - axiom: 'X', - draw: 'F', + "Hilbert 2": { + axiom: "X", + draw: "F", rules: { - X: 'XFYFX+F+YFXFY-F-XFYFX', - Y: 'YFXFY-F-XFYFX+F+YFXFY' + X: "XFYFX+F+YFXFY-F-XFYFX", + Y: "YFXFY-F-XFYFX+F+YFXFY", }, startingAngle: Math.PI, - maxIterations: 4 + maxIterations: 4, }, // https://en.wikipedia.org/wiki/Sierpi%C5%84ski_curve - 'Sierpinski': { - axiom: 'F--XF--F--XF', - draw: ['F', 'G'], + Sierpinski: { + axiom: "F--XF--F--XF", + draw: ["F", "G"], rules: { - X: 'XF+G+XF--F--XF+G+X' + X: "XF+G+XF--F--XF+G+X", }, - startingAngle: Math.PI/4, - angle: Math.PI/4, - maxIterations: 6 + startingAngle: Math.PI / 4, + angle: Math.PI / 4, + maxIterations: 6, }, // https://onlinemathtools.com/l-system-generator - 'Penrose Tile': { - axiom: '[7]++[7]++[7]++[7]++[7]', - draw: ['6', '7', '8', '9'], + "Penrose Tile": { + axiom: "[7]++[7]++[7]++[7]++[7]", + draw: ["6", "7", "8", "9"], rules: { - 6: '8++9----7[-8----6]++', - 7: '+8--9[---6--7]+', - 8: '-6++7[+++8++9]-', - 9: '--8++++6[+9++++7]--7' + 6: "8++9----7[-8----6]++", + 7: "+8--9[---6--7]+", + 8: "-6++7[+++8++9]-", + 9: "--8++++6[+9++++7]--7", }, - angle: Math.PI/5, + angle: Math.PI / 5, maxIterations: 6, iterationsGrow: (config) => { - return 1 + Math.max(1, 3/config.iterations) - } + return 1 + Math.max(1, 3 / config.iterations) + }, }, // https://en.wikipedia.org/wiki/Sierpi%C5%84ski_curve - 'Sierpinski Square': { - axiom: 'F+XF+F+XF', - draw: 'F', + "Sierpinski Square": { + axiom: "F+XF+F+XF", + draw: "F", rules: { - X: 'XF-F+F-XF+F+XF-F+F-X' + X: "XF-F+F-XF+F+XF-F+F-X", }, - startingAngle: Math.PI/4, - maxIterations: 6 + startingAngle: Math.PI / 4, + maxIterations: 6, }, } diff --git a/src/models/tessellation_twist/TessellationTwist.js b/src/models/tessellation_twist/TessellationTwist.js index 51b8342b..9d6560a0 100644 --- a/src/models/tessellation_twist/TessellationTwist.js +++ b/src/models/tessellation_twist/TessellationTwist.js @@ -1,23 +1,23 @@ -import Victor from 'victor' -import Graph, { mix } from '@/common/Graph' -import { eulerianTrail } from '@/common/eulerianTrail' -import { difference } from '@/common/util' -import Shape, { shapeOptions } from '../Shape' +import Victor from "victor" +import Graph, { mix } from "@/common/Graph" +import { eulerianTrail } from "@/common/eulerianTrail" +import { difference } from "@/common/util" +import Model from "../Model" const vecTriangle = [ new Victor(-0.85, -0.4907477295), new Victor(0.85, -0.4907477295), - new Victor(0.0, 0.9814954573), + new Victor(0.0, 0.9814954573), ] const vecSquare = [ new Victor(-0.7, -0.7), - new Victor( 0.7, 0.7), - new Victor(-0.7, 0.7), + new Victor(0.7, 0.7), + new Victor(-0.7, 0.7), new Victor(-0.7, -0.7), - new Victor(0.7, 0.7), - new Victor(0.7, -0.7) + new Victor(0.7, 0.7), + new Victor(0.7, -0.7), ] function getEdges(edges, a, b, c, count, settings) { @@ -25,24 +25,33 @@ 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) + 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) } else { - da = (settings.rotate * Math.PI / 180.0) - db = (settings.rotate * Math.PI / 180.0) - dc = (settings.rotate * Math.PI / 180.0) + da = (settings.rotate * Math.PI) / 180.0 + db = (settings.rotate * Math.PI) / 180.0 + dc = (settings.rotate * Math.PI) / 180.0 } let ap = new Victor( - (a.x * Math.cos(da)) - (a.y * Math.sin(da)), - (a.x * Math.sin(da)) + (a.y * Math.cos(da))) + a.x * Math.cos(da) - a.y * Math.sin(da), + a.x * Math.sin(da) + a.y * Math.cos(da), + ) let bp = new Victor( - (b.x * Math.cos(db)) - (b.y * Math.sin(db)), - (b.x * Math.sin(db)) + (b.y * Math.cos(db))) + b.x * Math.cos(db) - b.y * Math.sin(db), + b.x * Math.sin(db) + b.y * Math.cos(db), + ) let cp = new Victor( - (c.x * Math.cos(dc)) - (c.y * Math.sin(dc)), - (c.x * Math.sin(dc)) + (c.y * Math.cos(dc))) + c.x * Math.cos(dc) - c.y * Math.sin(dc), + c.x * Math.sin(dc) + c.y * Math.cos(dc), + ) edges.push([ap, bp], [ap, cp], [bp, cp]) return @@ -59,48 +68,45 @@ function getEdges(edges, a, b, c, count, settings) { } const options = { - ...shapeOptions, - ...{ - tessellationTwistNumSides: { - title: "Number of sides", - min: 3 - }, - tessellationTwistIterations: { - title: "Iterations", - min: 0, - max: 4 - }, - tessellationTwistRotate: { - title: "Rotate and twist", - step: 5, - min: 0 - } - } + tessellationTwistNumSides: { + title: "Number of sides", + min: 3, + }, + tessellationTwistIterations: { + title: "Iterations", + min: 0, + max: 4, + }, + tessellationTwistRotate: { + title: "Rotate and twist", + step: 5, + min: 0, + }, } // Adapted from https://codepen.io/rafaelpascoalrodrigues/pen/KpBJve. See NOTICE for licensing details. -export default class TessellationTwist extends Shape { +export default class TessellationTwist extends Model { constructor() { - super('Tessellation Twist') + super("tessellationTwist") + this.label = "Tessellation Twist" } getInitialState() { return { ...super.getInitialState(), ...{ - type: 'tessellation_twist', tessellationTwistNumSides: 5, tessellationTwistIterations: 2, tessellationTwistRotate: 0, - } + }, } } getShapeVertices(numSides) { let vertices = [] - for (let i=0; i<=numSides; i++) { - let angle = Math.PI * 2.0 / numSides * (0.5 + i) - let angle2 = Math.PI * 2.0 / numSides * (0.5 + ((i + 1) % numSides)) + for (let i = 0; i <= numSides; i++) { + let angle = ((Math.PI * 2.0) / numSides) * (0.5 + i) + let angle2 = ((Math.PI * 2.0) / numSides) * (0.5 + ((i + 1) % numSides)) vertices.push(new Victor(0, 0)) vertices.push(new Victor(Math.cos(angle), Math.sin(angle))) @@ -129,8 +135,14 @@ export default class TessellationTwist extends Shape { // build our tessellations for (var i = 0; i < vertices.length; i += 3) { - getEdges(edges, vertices[i + 0], vertices[i + 1], vertices[i + 2], - tessellation, { rotate: parseInt(state.shape.tessellationTwistRotate) }) + getEdges( + edges, + vertices[i + 0], + vertices[i + 1], + vertices[i + 2], + tessellation, + { rotate: parseInt(state.shape.tessellationTwistRotate) }, + ) } // build edge and adjacency maps; this serves to ensure unique @@ -149,7 +161,7 @@ export default class TessellationTwist extends Shape { // build a graph // find the eulerian trail that efficiently visits all of the vertices - let trail = eulerianTrail({edges: Object.values(graph.edgeMap)}) + let trail = eulerianTrail({ edges: Object.values(graph.edgeMap) }) let prevKey let walkedVertices = [] @@ -160,15 +172,17 @@ export default class TessellationTwist extends Shape { // the missing nodes and create edges for them. There is a complex algorithm // (chinese postman) that can be used to do this for the general case, but // it's computationally expensive and overkill for our situation. - for (i = 0; i < trail.length-1; i++) { - let edge = [trail[i], trail[i+1]].sort().toString() + for (i = 0; i < trail.length - 1; i++) { + let edge = [trail[i], trail[i + 1]].sort().toString() walkedEdges.push(edge) } walkedEdges = new Set(walkedEdges) - let missingEdges = Array.from(difference(walkedEdges, graph.edgeKeys)).reduce((hash, d) => { - d = d.split(',') - hash[d[0] + ',' + d[1]] = d[2] + ',' + d[3] + let missingEdges = Array.from( + difference(walkedEdges, graph.edgeKeys), + ).reduce((hash, d) => { + d = d.split(",") + hash[d[0] + "," + d[1]] = d[2] + "," + d[3] return hash }, {}) @@ -180,7 +194,7 @@ export default class TessellationTwist extends Shape { // non-eulerian move, so we'll walk the shortest valid path between them let path = graph.dijkstraShortestPath(prevKey, key) path.shift() - path.forEach(node => walkedVertices.push(node)) + path.forEach((node) => walkedVertices.push(node)) walkedVertices.push(vertex) } else { walkedVertices.push(vertex) @@ -206,9 +220,9 @@ export default class TessellationTwist extends Shape { }) const scale = 10.5 // to normalize starting size - walkedVertices.forEach(point => { + walkedVertices.forEach((point) => { if (!point.visited) { - point.multiply({x: scale, y: scale }) + point.multiply({ x: scale, y: scale }) point.visited = true } }) diff --git a/src/models/v1_engineering/V1Engineering.js b/src/models/v1_engineering/V1Engineering.js index d2c36c9e..173d689f 100644 --- a/src/models/v1_engineering/V1Engineering.js +++ b/src/models/v1_engineering/V1Engineering.js @@ -1,17 +1,18 @@ -import Vicious1Vertices from './Vicious1Vertices' -import Shape from '../Shape' +import Vicious1Vertices from "./Vicious1Vertices" +import Model from "../Model" -export default class V1Engineering extends Shape { +export default class V1Engineering extends Model { constructor() { - super('V1Engineering') + super("v1Engineering") + this.label = "V1Engineering" } getInitialState() { return { ...super.getInitialState(), ...{ - type: 'v1Engineering', - } + // no custom attributes + }, } } diff --git a/src/models/v1_engineering/Vicious1Vertices.js b/src/models/v1_engineering/Vicious1Vertices.js index d1eb01b8..8d54216d 100644 --- a/src/models/v1_engineering/Vicious1Vertices.js +++ b/src/models/v1_engineering/Vicious1Vertices.js @@ -1,4 +1,4 @@ -import Victor from 'victor' +import Victor from "victor" let Vicious1Vertices = () => { return [ From d5c982ac9f219d95b7c99545b1ad553e42d01385 Mon Sep 17 00:00:00 2001 From: Bob Carmichael Date: Sat, 22 Jul 2023 14:51:31 -0400 Subject: [PATCH 020/126] let model decide initial layer size; transformer dimensions match layer dimensions --- src/common/geometry.js | 56 +++++++++---- src/components/InputOption.js | 1 + src/features/layers/ImportLayer.js | 14 +++- src/features/layers/Layer.js | 16 ++-- src/features/layers/LayerEditor.js | 29 +++---- src/features/layers/NewLayer.js | 58 +++++++------ src/features/machine/selectors.js | 33 +++----- src/features/preview/MachinePreview.js | 82 +++++++++++-------- src/features/preview/PreviewConnector.js | 51 +++++++----- src/models/Epicycloid.js | 4 +- src/models/FancyText.js | 5 +- src/models/FileImport.js | 55 +++++++------ src/models/Hypocycloid.js | 4 +- src/models/Model.js | 34 +++++++- src/models/Rose.js | 5 +- .../fractal_spirograph/FractalSpirograph.js | 2 +- src/models/lsystem/LSystem.js | 2 +- .../tessellation_twist/TessellationTwist.js | 10 ++- 18 files changed, 276 insertions(+), 185 deletions(-) diff --git a/src/common/geometry.js b/src/common/geometry.js index 4d7baa91..ac16c89a 100644 --- a/src/common/geometry.js +++ b/src/common/geometry.js @@ -61,25 +61,45 @@ export const findBounds = (vertices) => { return [new Victor(loX, loY), new Victor(hiX, hiY)] } +export const dimensions = (vertices) => { + const bounds = findBounds(vertices) + + return { + width: Math.round(bounds[1].x - bounds[0].x, 0), + height: Math.round(bounds[1].y - bounds[0].y, 0), + } +} + +// resizes each vertex in a list to the specified dimensions. +// If lock = true, will not stretch the shape. // resizes each vertex in a list to the specified dimensions. Will not stretch the shape. -export const resizeVertices = (vertices, sizeX, sizeY) => { - let size = Math.max(sizeX, sizeY) - let bounds = findBounds(vertices) - let curveSize = Math.max(bounds[1].y - bounds[0].y, bounds[1].x - bounds[0].x) - let scale = size / curveSize - let scaledBounds = [ - bounds[0].multiply({ x: scale, y: scale }), - bounds[1].multiply({ x: scale, y: scale }), - ] - let deltaX = scaledBounds[1].x - (scaledBounds[1].x - scaledBounds[0].x) / 2 - let deltaY = scaledBounds[1].y - (scaledBounds[1].y - scaledBounds[0].y) / 2 - - return vertices.map((vertex) => - vertex - .clone() - .multiply({ x: scale, y: scale }) - .add({ x: -deltaX, y: -deltaY }), - ) +export const resizeVertices = (vertices, sizeX, sizeY, lock = true) => { + let scaleX, scaleY, deltaX, deltaY + const bounds = findBounds(vertices) + const oldSizeX = bounds[1].x - bounds[0].x + const oldSizeY = bounds[1].y - bounds[0].y + + if (lock) { + const size = Math.max(sizeX, sizeY) + const oldSize = Math.max(oldSizeX, oldSizeY) + scaleX = size / oldSize + scaleY = size / oldSize + bounds[0].multiply({ x: scaleX, y: scaleY }) + bounds[1].multiply({ x: scaleX, y: scaleY }) + deltaX = bounds[0].x / 2 + deltaY = bounds[0].y / 2 + } else { + scaleX = sizeX / oldSizeX + scaleY = sizeY / oldSizeY + deltaX = 0 + deltaY = 0 + } + + vertices.forEach((vertex) => { + vertex.multiply({ x: scaleX, y: scaleY }).add({ x: -deltaX, y: -deltaY }) + }) + + return vertices } // returns a vertex with x and y rounded to p number of digits diff --git a/src/components/InputOption.js b/src/components/InputOption.js index c973a36c..9a09c3ef 100644 --- a/src/components/InputOption.js +++ b/src/components/InputOption.js @@ -44,6 +44,7 @@ class InputOption extends Component { min={!isNaN(minimum) ? minimum : ""} max={!isNaN(maximum) ? maximum : ""} value={data[optionKey]} + autoComplete="off" plaintext={option.plainText} onChange={(event) => { let attrs = {} diff --git a/src/features/layers/ImportLayer.js b/src/features/layers/ImportLayer.js index 44ea65c8..e485dd8f 100644 --- a/src/features/layers/ImportLayer.js +++ b/src/features/layers/ImportLayer.js @@ -2,6 +2,7 @@ import React, { Component } from "react" import { Button, Modal, Form, Accordion, Card } from "react-bootstrap" import { connect } from "react-redux" +import { getMachine } from '@/features/store/selectors' import ThetaRhoImporter from "../importer/ThetaRhoImporter" import GCodeImporter from "../importer/GCodeImporter" import { addLayer } from "../layers/layersSlice" @@ -10,6 +11,7 @@ import ReactGA from "react-ga" const mapStateToProps = (state, ownProps) => { return { + machineState: getMachine(state), showModal: ownProps.showModal, } } @@ -19,11 +21,11 @@ const mapDispatchToProps = (dispatch, ownProps) => { toggleModal: () => { ownProps.toggleModal() }, - onLayerImport: (importProps) => { + onLayerImport: (props) => { const layer = new Layer("fileImport") const attrs = { - ...layer.getInitialState(importProps), - name: importProps.fileName, + ...layer.getInitialState(props), + name: props.fileName, } dispatch(addLayer(attrs)) }, @@ -168,7 +170,11 @@ class ImportLayer extends Component { } onFileImported(importer, importerProps) { - this.props.onLayerImport(importerProps) + const layerProps = { + ...importerProps, + machine: this.props.machineState, + } + this.props.onLayerImport(layerProps) this.endTime = performance.now() ReactGA.timing({ diff --git a/src/features/layers/Layer.js b/src/features/layers/Layer.js index db1c9726..2d57fb4f 100644 --- a/src/features/layers/Layer.js +++ b/src/features/layers/Layer.js @@ -1,5 +1,5 @@ import { getModelFromType } from "../../config/models" -import { resizeVertices } from "@/common/geometry" +import { resizeVertices, centerOnOrigin } from "@/common/geometry" export const layerOptions = { name: { @@ -69,6 +69,8 @@ export default class Layer { } getInitialState(props) { + const dimensions = this.model.initialDimensions(props) + return { ...this.model.getInitialState(props), ...{ @@ -76,8 +78,8 @@ export default class Layer { connectionMethod: "line", x: 0.0, y: 0.0, - width: this.model.startingWidth, - height: this.model.startingHeight, + width: dimensions.width, + height: dimensions.height, rotation: 0, reverse: false, visible: true, @@ -94,10 +96,12 @@ export default class Layer { const { width, height, x, y, rotation } = state.shape let vertices = this.model.getVertices(state) + if (this.model.autosize) { + vertices = resizeVertices(vertices, width, height, false) + centerOnOrigin(vertices) + } + vertices.forEach((vertex) => { - if (this.model.autosize) { - vertices = resizeVertices(vertices, width, height, false) - } vertex.rotateDeg(-rotation) vertex.addX({ x: x || 0 }).addY({ y: y || 0 }) }) diff --git a/src/features/layers/LayerEditor.js b/src/features/layers/LayerEditor.js index 494f5615..8f3d841d 100644 --- a/src/features/layers/LayerEditor.js +++ b/src/features/layers/LayerEditor.js @@ -53,7 +53,7 @@ class LayerEditor extends Component { label: model.label, } const link = model.link - const linkText = model.linkText || link + const linkText = model.linkText || "here" const renderedModelOptions = Object.keys(modelOptions).map((key) => { return (
- - -

- See{" "} - - {linkText} - {" "} - for ideas. -

- - +
+ See{" "} + + {linkText} + {" "} + for ideas. +
) : undefined const renderedModelSelection = allowModelSelection && ( diff --git a/src/features/layers/NewLayer.js b/src/features/layers/NewLayer.js index d52804db..394298a8 100644 --- a/src/features/layers/NewLayer.js +++ b/src/features/layers/NewLayer.js @@ -2,16 +2,15 @@ import React, { Component } from "react" import Select from "react-select" import { Button, Modal, Row, Col, Form } from "react-bootstrap" import { connect } from "react-redux" +import { + getModelSelectOptions, + getDefaultModel, + getModelFromType, +} from "../../config/models" +import Layer from "./Layer" +import { addLayer } from "./layersSlice" -import { getLayers } from "../store/selectors" -import { getModelSelectOptions, getDefaultModel } from "../../config/models" -import { addLayer } from "../layers/layersSlice" - -// Initialize these from local storage, or reasonable defaults const defaultModel = getDefaultModel() -const initialLayerName = defaultModel.label -const initialLayerType = defaultModel.type - const customStyles = { control: (base) => ({ ...base, @@ -30,7 +29,9 @@ const mapStateToProps = (state, ownProps) => { const mapDispatchToProps = (dispatch, ownProps) => { return { onLayerAdded: (type, name) => { - const attrs = registeredModels[type].getInitialState() + const layer = new Layer(type) + const attrs = layer.getInitialState() + attrs.name = name dispatch(addLayer(attrs)) }, @@ -44,22 +45,24 @@ class NewLayer extends Component { constructor(props) { super(props) this.state = { - newLayerType: initialLayerType, - newLayerName: initialLayerName, + type: defaultModel.type, + name: defaultModel.label, } } render() { const { toggleModal, showModal, selectOptions, onLayerAdded } = this.props - // const selectedShape = getModelFromType(this.state.newLayerType) - // const selectedOption = { - // value: selectedShape.id, - // label: selectedShape.name, - // } - const selectedOption = null + const selectedShape = getModelFromType(this.state.type) + const selectedOption = { + value: selectedShape.id, + label: selectedShape.label, + } return ( - + Create new layer @@ -81,7 +84,7 @@ class NewLayer extends Component { Name @@ -90,14 +93,18 @@ class NewLayer extends Component { - - + Export to a file @@ -170,7 +174,10 @@ class Downloader extends Component { {this.props.fileType === SCARA && ( - + - - - - + + + + + + ) } handleNameFocus(event) { diff --git a/src/features/layers/ImportLayer.js b/src/features/layers/ImportLayer.js index e485dd8f..839d6f7a 100644 --- a/src/features/layers/ImportLayer.js +++ b/src/features/layers/ImportLayer.js @@ -2,7 +2,7 @@ import React, { Component } from "react" import { Button, Modal, Form, Accordion, Card } from "react-bootstrap" import { connect } from "react-redux" -import { getMachine } from '@/features/store/selectors' +import { getMachineState } from "@/features/store/selectors" import ThetaRhoImporter from "../importer/ThetaRhoImporter" import GCodeImporter from "../importer/GCodeImporter" import { addLayer } from "../layers/layersSlice" @@ -11,7 +11,7 @@ import ReactGA from "react-ga" const mapStateToProps = (state, ownProps) => { return { - machineState: getMachine(state), + machineState: getMachineState(state), showModal: ownProps.showModal, } } diff --git a/src/features/layers/layersSlice.js b/src/features/layers/layersSlice.js index 7fce72e1..9e6cf29d 100644 --- a/src/features/layers/layersSlice.js +++ b/src/features/layers/layersSlice.js @@ -1,7 +1,7 @@ import { createSlice } from "@reduxjs/toolkit" import uniqueId from "lodash/uniqueId" import arrayMove from "array-move" -import { getModelFromType } from "@/config/models" +import { getModelFromType, getDefaultModelType } from "@/config/models" import Layer from "./Layer" const notCopiedWhenTypeChanges = ["type", "height", "width"] @@ -74,17 +74,26 @@ function setCurrentId(state, id) { state.current = id } +const defaultLayer = new Layer(getDefaultModelType()) +const defaultLayerId = uniqueId("layer-") +const layerState = { + id: defaultLayerId, + ...defaultLayer.getInitialState(), +} + const layersSlice = createSlice({ name: "layers", initialState: { - current: null, - selected: null, + current: defaultLayerId, + selected: defaultLayerId, newEffectType, newEffectName, newEffectNameOverride: false, copyLayerName: null, - byId: {}, - allIds: [], + byId: { + [defaultLayerId]: layerState, + }, + allIds: [defaultLayerId], }, reducers: { addLayer(state, action) { diff --git a/src/features/layers/selectors.js b/src/features/layers/selectors.js index 7221d085..acad8894 100644 --- a/src/features/layers/selectors.js +++ b/src/features/layers/selectors.js @@ -1,144 +1,137 @@ -import { getLayers, createDeepEqualSelector } from '../store/selectors' -import { createSelector } from 'reselect' -import { memoizeArrayProducingFn } from '../../common/selectors' -import { log } from '../../common/util' +import { getLayersState, createDeepEqualSelector } from "../store/selectors" +import { createSelector } from "reselect" +import { createCachedSelector } from "re-reselect" +import { memoizeArrayProducingFn } from "../../common/selectors" +import { log } from "../../common/debugging" const getCurrentLayerId = createSelector( - getLayers, - (layers) => layers.current + getLayersState, + (layers) => layers.current, ) -const getLayersById = createSelector( - getLayers, - (layers) => layers.byId -) +const getLayersById = createSelector(getLayersState, (layers) => { + return layers.byId +}) const getOrderedLayerIds = createSelector( - getLayers, - (layers) => layers.allIds + getLayersState, + (layers) => layers.allIds, ) export const getVisibleOrderedLayerIds = createSelector( - [ getOrderedLayerIds, getLayersById ], + [getOrderedLayerIds, getLayersById], (layerIds, layers) => { - return layerIds.filter(id => layers[id].visible) - } + return layerIds.filter((id) => layers[id].visible) + }, ) - export const getVisibleNonEffectIds = createSelector( - [ getVisibleOrderedLayerIds, getLayersById ], - (layerIds, layers) => { - return layerIds.filter(id => !layers[id].effect) - } - ) +export const getVisibleNonEffectIds = createSelector( + [getVisibleOrderedLayerIds, getLayersById], + (layerIds, layers) => { + return layerIds.filter((id) => !layers[id].effect) + }, +) export const getCurrentLayer = createSelector( - [ getLayersById, getCurrentLayerId ], + [getLayersById, getCurrentLayerId], (layers, current) => { return layers[current] - } + }, ) export const getAllLayers = createSelector( - [ getOrderedLayerIds, getLayersById ], + [getOrderedLayerIds, getLayersById], (layerIds, layersById) => { log("getAllLayers") - return layerIds.map(id => layersById[id]) - } + return layerIds.map((id) => layersById[id]) + }, ) -export const getNumLayers = createSelector( - getOrderedLayerIds, - (layerIds) => { - log("getNumLayer") - return layerIds.length - } -) +export const getNumLayers = createSelector(getOrderedLayerIds, (layerIds) => { + log("getNumLayer") + return layerIds.length +}) // puts the current layer last in the list to ensure it can be rotated; else // the handle will not rotate export const getKonvaLayerIds = createSelector( - [ getCurrentLayer, getVisibleOrderedLayerIds ], + [getCurrentLayer, getVisibleOrderedLayerIds], (currentLayer, visibleLayerIds) => { - const kIds = visibleLayerIds.filter(id => id !== currentLayer.id) - if (currentLayer.visible) { - kIds.push(currentLayer.id) - } - return kIds - } + const kIds = visibleLayerIds.filter((id) => id !== currentLayer.id) + if (currentLayer.visible) { + kIds.push(currentLayer.id) + } + return kIds + }, ) export const isDragging = createSelector( - [ getOrderedLayerIds, getLayersById ], + [getOrderedLayerIds, getLayersById], (layerIds, layers) => { log("isDragging") - return layerIds.filter(id => layers[id].visible && layers[id].dragging).length > 0 - } + return ( + layerIds.filter((id) => layers[id].visible && layers[id].dragging) + .length > 0 + ) + }, ) export const getNumVisibleLayers = createSelector( getVisibleNonEffectIds, (layers) => { return layers.length - } + }, ) -export const makeGetLayerIndex = layerId => { - return createDeepEqualSelector( - getVisibleOrderedLayerIds, - (visibleLayerIds) => { - return visibleLayerIds.findIndex(id => id === layerId) - } - ) -} - -export const makeGetNonEffectLayerIndex = layerId => { - return createDeepEqualSelector( - getVisibleNonEffectIds, - (visibleLayerIds) => { - return visibleLayerIds.findIndex(id => id === layerId) - } - ) -} - -export const makeGetLayer = layerId => { - return createSelector( - getLayersById, - (layers) => { - return layers[layerId] - } - ) -} +export const getLayerIndex = createCachedSelector( + (state, id) => id, + getVisibleOrderedLayerIds, + (layerId, visibleLayerIds) => { + return visibleLayerIds.findIndex((id) => id === layerId) + }, +)({ + selectorCreator: createDeepEqualSelector, + keySelector: (state, id) => id, +}) + +export const getNonEffectLayerIndex = createCachedSelector( + getVisibleNonEffectIds, + (state, id) => id, + (visibleLayerIds, layerId) => { + return visibleLayerIds.findIndex((id) => id === layerId) + }, +)((state, id) => id) + +export const getLayer = createCachedSelector( + getLayersState, + (state, id) => id, + (layers, id) => layers.byId[id], +)((state, id) => id) // returns any effects tied to a given layer; memoizeArrayProducingFn will ensure we // only recompute transformed vertices when an effect changes. -export const makeGetEffects = layerId => { - return createSelector( - [ - getLayersById, - getVisibleOrderedLayerIds - ], - memoizeArrayProducingFn( - (layers, visibleLayerIds) => { - let index = visibleLayerIds.findIndex(id => id === layerId) - const layer = layers[layerId] - - if (layer.effect || index === visibleLayerIds.length - 1) { - return [] - } else { - index = index + 1 - const effects = [] - let id = visibleLayerIds[index] - - while (id && layers[id].effect) { - effects.push(layers[id]) - index = index + 1 - id = visibleLayerIds[index] - } - - return effects - } +export const getLayerEffects = createCachedSelector( + (state, id) => id, + getLayersById, + getVisibleOrderedLayerIds, + memoizeArrayProducingFn((layerId, layers, visibleLayerIds) => { + let index = visibleLayerIds.findIndex((id) => id === layerId) + const layer = layers[layerId] + + if (layer.effect || index === visibleLayerIds.length - 1) { + return [] + } else { + index = index + 1 + const effects = [] + let id = visibleLayerIds[index] + + while (id && layers[id].effect) { + effects.push(layers[id]) + index = index + 1 + id = visibleLayerIds[index] } - ) - ) -} + + return effects + } + }), +)((state, id) => id) diff --git a/src/features/machine/MachineSettings.js b/src/features/machine/MachineSettings.js index d4840e00..d035fa19 100644 --- a/src/features/machine/MachineSettings.js +++ b/src/features/machine/MachineSettings.js @@ -1,15 +1,15 @@ -import React, { Component } from 'react' -import { connect } from 'react-redux' -import { Accordion } from 'react-bootstrap' -import RectSettings from './RectSettings' -import PolarSettings from './PolarSettings' -import { getMachine } from '../store/selectors' +import React, { Component } from "react" +import { connect } from "react-redux" +import { Accordion } from "react-bootstrap" +import RectSettings from "./RectSettings" +import PolarSettings from "./PolarSettings" +import { getMachineState } from "../store/selectors" const mapStateToProps = (state, ownProps) => { - const machine = getMachine(state) + const machine = getMachineState(state) return { - rectangular: machine.rectangular + rectangular: machine.rectangular, } } diff --git a/src/features/machine/PolarSettings.js b/src/features/machine/PolarSettings.js index 1e2cb125..4b7dda0a 100644 --- a/src/features/machine/PolarSettings.js +++ b/src/features/machine/PolarSettings.js @@ -11,7 +11,7 @@ import { } from "react-bootstrap" import InputOption from "../../components/InputOption" import CheckboxOption from "../../components/CheckboxOption" -import { getMachine } from "../store/selectors" +import { getMachineState } from "../store/selectors" import { machineOptions } from "./options" import { toggleMachinePolarExpanded, @@ -20,7 +20,7 @@ import { } from "./machineSlice" const mapStateToProps = (state, ownProps) => { - const machine = getMachine(state) + const machine = getMachineState(state) return { expanded: machine.polarExpanded, @@ -92,13 +92,22 @@ class PolarSettings extends Component { value={this.props.startPoint} onChange={this.props.onStartPointChange} > - + none - + center - + perimeter @@ -118,13 +127,22 @@ class PolarSettings extends Component { value={this.props.endPoint} onChange={this.props.onEndPointChange} > - + none - + center - + perimeter diff --git a/src/features/machine/RectSettings.js b/src/features/machine/RectSettings.js index b305e28d..d701f869 100644 --- a/src/features/machine/RectSettings.js +++ b/src/features/machine/RectSettings.js @@ -17,11 +17,11 @@ import { toggleMachineRectExpanded, setMachineRectOrigin, } from "./machineSlice" -import { getMachine } from "../store/selectors" +import { getMachineState } from "../store/selectors" import { machineOptions } from "./options" const mapStateToProps = (state, ownProps) => { - const machine = getMachine(state) + const machine = getMachineState(state) return { expanded: machine.rectExpanded, @@ -120,16 +120,28 @@ class RectSettings extends Component { value={this.props.origin} onChange={this.props.onOriginChange} > - + upper left - + upper right - + lower left - + lower right diff --git a/src/features/machine/selectors.js b/src/features/machine/selectors.js index 94b7d959..82170904 100644 --- a/src/features/machine/selectors.js +++ b/src/features/machine/selectors.js @@ -2,20 +2,19 @@ import LRUCache from "lru-cache" import { createSelector } from "reselect" import Color from "color" import { transformShapes, polishVertices, getMachineInstance } from "./computer" -import { getMachine, getState, getPreview } from "../store/selectors" -import { getLoadedFonts } from "../fonts/selectors" +import { getMachineState, getState, getPreviewState } from "../store/selectors" +import { createCachedSelector } from "re-reselect" import { - makeGetLayer, + getLayer, getNumVisibleLayers, getVisibleNonEffectIds, - makeGetEffects, - makeGetNonEffectLayerIndex, + getLayerEffects, + getNonEffectLayerIndex, } from "../layers/selectors" import Layer from "../layers/Layer" import { getModelFromType } from "@/config/models" -import { getCachedSelector } from "../store/selectors" import { rotate, offset } from "../../common/geometry" -import { log } from "../../common/util" +import { log } from "../../common/debugging" const cache = new LRUCache({ length: (n, key) => { @@ -30,122 +29,65 @@ const getCacheKey = (state) => { // by returning null for shapes which don't use machine settings, this selector will ensure // transformed vertices are not redrawn when machine settings change -const makeGetLayerMachine = (layerId) => { - return createSelector( - [getCachedSelector(makeGetLayer, layerId), getMachine], - (layer, machine) => { - log("makeGetLayerMachine", layerId) - const model = getModelFromType(layer.type) - return model.usesMachine ? machine : null - }, - ) -} -// by returning null for shapes which don't use fonts, this selector will ensure -// transformed vertices are not redrawn when fonts are loaded -const makeGetLayerFonts = (layerId) => { - return createSelector( - [getCachedSelector(makeGetLayer, layerId), getLoadedFonts], - (layer, fonts) => { - log("makeGetLayerFonts", layerId) - return getModelFromType(layer.type).usesFonts ? fonts : null - }, - ) -} +export const getLayerMachine = createCachedSelector( + getLayer, + getMachineState, + (layer, machine) => { + const model = getModelFromType(layer.type) + return model.usesMachine ? machine : null + }, +)((state, id) => id) // creates a selector that returns shape vertices for a given layer -const makeGetLayerVertices = (layerId) => { - return createSelector( - [ - getCachedSelector(makeGetLayer, layerId), - getCachedSelector(makeGetLayerMachine, layerId), - getCachedSelector(makeGetLayerFonts, layerId), - ], - (layer, machine, fonts) => { - const state = { - shape: layer, - machine, - fonts, - } - log("makeGetLayerVertices", layerId) - const layerInstance = new Layer(layer.type) - - // TODO: fix this; move cache into model? Should be caching vertices only, not transforms - if (layerInstance.model.shouldCache) { - const key = getCacheKey(state) - let vertices = cache.get(key) - - if (!vertices) { - vertices = layerInstance.getVertices(state) - - if (vertices.length > 1) { - cache.set(key, vertices) - log("caching shape", cache.length + " " + cache.itemCount) - } +export const getLayerVertices = createCachedSelector( + getLayer, + getLayerMachine, + (layer, machine) => { + const state = { + shape: layer, + machine, + } + log("getLayerVertices", layer.id) + const layerInstance = new Layer(layer.type) + + // TODO: fix this; move cache into model? Should be caching vertices only, not transforms + if (layerInstance.model.shouldCache) { + const key = getCacheKey(state) + let vertices = cache.get(key) + + if (!vertices) { + vertices = layerInstance.getVertices(state) + + if (vertices.length > 1) { + cache.set(key, vertices) + log( + `caching shape - ${cache.length} vertices / ${cache.itemCount} items`, + ) } + } - return vertices + return vertices + } else { + if (!state.shape.dragging && state.shape.effect) { + return [] } else { - if (!state.shape.dragging && state.shape.effect) { - return [] - } else { - return layer.getVertices(state) - } + return layer.getVertices(state) } - }, - ) -} + } + }, +)((state, id) => id) // creates a selector that returns transformed vertices for a given layer -const makeGetTransformedVertices = (layerId) => { - return createSelector( - [ - getCachedSelector(makeGetLayerVertices, layerId), - getCachedSelector(makeGetLayer, layerId), - getCachedSelector(makeGetEffects, layerId), - ], - (vertices, layer, effects, fonts) => { - log("makeGetTransformedVertices", layerId) - return transformShapes(vertices, layer, effects) - }, - ) -} - -export const makeGetConnectorVertices = (startId, endId) => { - return createSelector( - [ - getCachedSelector(makeGetLayer, startId), - getCachedSelector(makeGetComputedVertices, startId), - getCachedSelector(makeGetComputedVertices, endId), - getMachine, - ], - (startLayer, startVertices, endVertices, machine) => { - log("makeGetConnectorVertices", startId + "-" + endId) - const start = startVertices[startVertices.length - 1] - const end = endVertices[0] - - if (startLayer.connectionMethod === "along perimeter") { - const machineInstance = getMachineInstance([], machine) - const startPerimeter = machineInstance.nearestPerimeterVertex(start) - const endPerimeter = machineInstance.nearestPerimeterVertex(end) - const perimeterConnection = machineInstance.tracePerimeter( - startPerimeter, - endPerimeter, - ) - - return [ - start, - startPerimeter, - perimeterConnection, - endPerimeter, - end, - ].flat() - } else { - return [start, end] - } - }, - ) -} +const getTransformedVertices = createCachedSelector( + getLayerVertices, + getLayer, + getLayerEffects, + (vertices, layer, effects) => { + log("getTransformedVertices", layer.id) + return transformShapes(vertices, layer, effects) + }, +)((state, id) => id) // transform a given list of vertices as needed to be displayed in a preview layer const previewTransform = (layer, vertices) => { @@ -164,95 +106,113 @@ const previewTransform = (layer, vertices) => { } // creates a selector that returns computed (machine-bound) vertices for a given layer -const makeGetComputedVertices = (layerId) => { - return createSelector( - [ - getCachedSelector(makeGetTransformedVertices, layerId), - getCachedSelector(makeGetNonEffectLayerIndex, layerId), - getNumVisibleLayers, - getMachine, - ], - (vertices, layerIndex, numLayers, machine) => { - log("makeGetComputedVertices", layerId) - return polishVertices(vertices, machine, { - start: layerIndex === 0, - end: layerIndex === numLayers - 1, - }) - }, - ) -} +const getComputedVertices = createCachedSelector( + (state, id) => id, + getTransformedVertices, + getNonEffectLayerIndex, + getNumVisibleLayers, + getMachineState, + (id, vertices, layerIndex, numLayers, machine) => { + log("getComputedVertices", id) + return polishVertices(vertices, machine, { + start: layerIndex === 0, + end: layerIndex === numLayers - 1, + }) + }, +)((state, id) => id) // creates a selector that returns previewable vertices for a given layer -export const makeGetPreviewVertices = (layerId) => { - return createSelector( - [ - getCachedSelector(makeGetTransformedVertices, layerId), - getCachedSelector(makeGetComputedVertices, layerId), - getCachedSelector(makeGetLayer, layerId), - ], - (transformedVertices, computedVertices, layer) => { - log("makeGetPreviewVertices", layerId) - const vertices = layer.dragging ? transformedVertices : computedVertices - return previewTransform(layer, vertices) - }, - ) -} +export const getPreviewVertices = createCachedSelector( + getTransformedVertices, + getComputedVertices, + getLayer, + (transformedVertices, computedVertices, layer, foo, bar) => { + log("getPreviewVertices", layer.id) + const vertices = layer.dragging ? transformedVertices : computedVertices + return previewTransform(layer, vertices) + }, +)((state, id) => id) // returns a flattened array of all visible computed vertices and connectors (across layers) -export const getAllComputedVertices = createSelector( - [getState, getVisibleNonEffectIds], - (state, visibleLayerIds) => { - log("getAllComputedVertices") - return visibleLayerIds - .map((id, idx) => { - const connector = getConnectingVertices(state, id) - let vertices = getCachedSelector(makeGetComputedVertices, id)(state) - if (connector) vertices = [...vertices, ...connector] - return vertices - }) - .flat() - }, -) +export const getAllComputedVertices = createSelector([getState], (state) => { + log("getAllComputedVertices") + const visibleLayerIds = getVisibleNonEffectIds(state) + + return visibleLayerIds + .map((id, idx) => { + const vertices = getComputedVertices(state, id) + const connector = getConnectingVertices(state, id) + return [...vertices, ...connector] + }) + .flat() +}) // returns an array of vertices connecting a given layer to the next (if it exists) -export const getConnectingVertices = (state, layerId) => { - const visibleLayerIds = getVisibleNonEffectIds(state) - const idx = getCachedSelector(makeGetNonEffectLayerIndex, layerId)(state) - - return idx < visibleLayerIds.length - 1 - ? getCachedSelector( - makeGetConnectorVertices, - layerId, - visibleLayerIds[idx + 1], - )(state) - : null -} +export const getConnectingVertices = createCachedSelector( + (state, id) => id, + getState, + (layerId, state) => { + log("getConnectingVertices") + const visibleLayerIds = getVisibleNonEffectIds(state) + const idx = getNonEffectLayerIndex(state, layerId) + + if (idx > visibleLayerIds.length - 2) { + return [] + } + + const endId = visibleLayerIds[idx + 1] + const startLayer = getLayer(state, layerId) //|| getCurrentLayer(state) + const endLayer = getLayer(state, endId) //|| getCurrentLayer(state) + const startVertices = getLayerVertices(state, startLayer.id) + const endVertices = getLayerVertices(state, endLayer.id) + const start = startVertices[startVertices.length - 1] + const end = endVertices[0] + + if (startLayer.connectionMethod === "along perimeter") { + const machineInstance = getMachineInstance([], state.machine) + const startPerimeter = machineInstance.nearestPerimeterVertex(start) + const endPerimeter = machineInstance.nearestPerimeterVertex(end) + const perimeterConnection = machineInstance.tracePerimeter( + startPerimeter, + endPerimeter, + ) + + return [ + start, + startPerimeter, + perimeterConnection, + endPerimeter, + end, + ].flat() + } else { + return [start, end] + } + }, +)((state, id) => id) // returns the starting offset for each layer, given previous layers -export const getVertexOffsets = createSelector( - [getState, getVisibleNonEffectIds], - (state, visibleLayerIds) => { - log("getVertexOffsets") - let offsets = {} - let offset = 0 - - visibleLayerIds.forEach((id) => { - const vertices = getCachedSelector(makeGetComputedVertices, id)(state) - const connector = getConnectingVertices(state, id) || [] - offsets[id] = { start: offset, end: offset + vertices.length - 1 } - - if (connector.length > 0) { - offsets[id + "-connector"] = { - start: offset + vertices.length, - end: offset + vertices.length + connector.length - 1, - } - offset += vertices.length + connector.length +export const getVertexOffsets = createSelector([getState], (state) => { + log("getVertexOffsets") + const visibleLayerIds = getVisibleNonEffectIds(state) + let offsets = {} + let offset = 0 + + visibleLayerIds.forEach((id) => { + const vertices = getComputedVertices(state, id) + const connector = getConnectingVertices(state, id) + offsets[id] = { start: offset, end: offset + vertices.length - 1 } + + if (connector.length > 0) { + offsets[id + "-connector"] = { + start: offset + vertices.length, + end: offset + vertices.length + connector.length - 1, } - }) + offset += vertices.length + connector.length + } + }) - return offsets - }, -) + return offsets +}) // returns statistics across all layers export const getVerticesStats = createSelector( @@ -282,7 +242,7 @@ export const getVerticesStats = createSelector( // given a set of vertices and a slider value, returns the indices of the // start and end vertices representing a preview slider moving through them. export const getSliderBounds = createSelector( - [getAllComputedVertices, getPreview], + [getAllComputedVertices, getPreviewState], (vertices, preview) => { const slideSize = 2.0 const beginFraction = preview.sliderValue / 100.0 @@ -328,8 +288,10 @@ export const getSliderColors = createSelector( }, ) +// TODO: fix or remove // used by the preview window; reverses rotation and offsets because they are // re-added by Konva transformer. +/* export const makeGetPreviewTrackVertices = (layerId) => { return createSelector( getCachedSelector(makeGetLayer, layerId), @@ -337,20 +299,17 @@ export const makeGetPreviewTrackVertices = (layerId) => { log("makeGetPreviewTrackVertices", layerId) let trackVertices = [] - // TODO: re-implement display of track vertices - // const numLoops = layer.numLoops - // for (var i=0; i { - return rotate( - offset(vertex, -layer.x, -layer.y), - layer.rotation, - ) + return rotate(offset(vertex, -layer.x, -layer.y), layer.rotation) }) }, ) } +*/ diff --git a/src/features/preview/MachinePreview.js b/src/features/preview/MachinePreview.js deleted file mode 100644 index a0ecbc14..00000000 --- a/src/features/preview/MachinePreview.js +++ /dev/null @@ -1,117 +0,0 @@ -import React, { Component } from "react" -import { connect } from "react-redux" -import Slider from "rc-slider" -import "rc-slider/assets/index.css" -import PreviewWindow from "./PreviewWindow" -import Downloader from "../exporter/Downloader" -import { getCurrentLayer } from "../layers/selectors" -import { getLayers, getPreview } from "../store/selectors" -import { updateLayer } from "../layers/layersSlice" -import { updatePreview } from "./previewSlice" -import { getVerticesStats } from "../machine/selectors" -import "./MachinePreview.scss" - -const mapStateToProps = (state, ownProps) => { - const preview = getPreview(state) - const current = getCurrentLayer(state) - const layers = getLayers(state) - - return { - currentLayer: current, - currentLayerSelected: layers.selected === current.id, - sliderValue: preview.sliderValue, - verticesStats: getVerticesStats(state), - } -} - -const mapDispatchToProps = (dispatch, ownProps) => { - return { - onSlider: (value) => { - dispatch(updatePreview({ sliderValue: value })) - }, - onLayerChange: (attrs) => { - dispatch(updateLayer(attrs)) - }, - onKeyDown: (event, currentLayer) => { - let attrs = { id: currentLayer.id } - - if (currentLayer.canMove) { - if ( - ["ArrowDown", "ArrowUp", "ArrowLeft", "ArrowRight"].includes( - event.key, - ) - ) { - const delta = event.shiftKey ? 1 : 5 - - if (event.key === "ArrowDown") { - attrs.y = currentLayer.y - delta - } else if (event.key === "ArrowUp") { - attrs.y = currentLayer.y + delta - } else if (event.key === "ArrowLeft") { - attrs.x = currentLayer.x - delta - } else if (event.key === "ArrowRight") { - attrs.x = currentLayer.x + delta - } - - dispatch(updateLayer(attrs)) - } - } - }, - } -} - -class MachinePreview extends Component { - componentDidMount() { - // ensures that arrow keys always work - this.el.focus() - } - - render() { - return ( -
-
-
{ - this.el = el - }} - tabIndex={0} - onKeyDown={(e) => { - if (this.props.currentLayerSelected) { - this.props.onKeyDown(e, this.props.currentLayer) - } - }} - > - -
- -
-
-
- Points: {this.props.verticesStats.numPoints}, Distance:{" "} - {this.props.verticesStats.distance} -
- -
- -
-
- -
-
-
- ) - } -} - -export default connect(mapStateToProps, mapDispatchToProps)(MachinePreview) diff --git a/src/features/preview/Preview.js b/src/features/preview/Preview.js new file mode 100644 index 00000000..2135441e --- /dev/null +++ b/src/features/preview/Preview.js @@ -0,0 +1,138 @@ +import React, { Component } from "react" +import { connect } from "react-redux" +import Slider from "rc-slider" +import "rc-slider/assets/index.css" +import PreviewWindow from "./PreviewWindow" +import Downloader from "../exporter/Downloader" +import { getFontsState } from "../store/selectors" +import { getCurrentLayer } from "../layers/selectors" +import { getLayersState, getPreviewState } from "../store/selectors" +import { updateLayer } from "../layers/layersSlice" +import { updatePreview } from "./previewSlice" +import { getVerticesStats } from "../machine/selectors" +import "./Preview.scss" + +const mapStateToProps = (state, ownProps) => { + const fonts = getFontsState(state) + if (!fonts.loaded) { + return {} + } + + const preview = getPreviewState(state) + const current = getCurrentLayer(state) + const layers = getLayersState(state) + + return { + currentLayer: current, + currentLayerSelected: layers.selected === current.id, + sliderValue: preview.sliderValue, + verticesStats: getVerticesStats(state), + } +} + +const mapDispatchToProps = (dispatch, ownProps) => { + return { + onSlider: (value) => { + dispatch(updatePreview({ sliderValue: value })) + }, + onLayerChange: (attrs) => { + dispatch(updateLayer(attrs)) + }, + onKeyDown: (event, currentLayer) => { + let attrs = { id: currentLayer.id } + + if (currentLayer.canMove) { + if ( + ["ArrowDown", "ArrowUp", "ArrowLeft", "ArrowRight"].includes( + event.key, + ) + ) { + const delta = event.shiftKey ? 1 : 5 + + if (event.key === "ArrowDown") { + attrs.y = currentLayer.y - delta + } else if (event.key === "ArrowUp") { + attrs.y = currentLayer.y + delta + } else if (event.key === "ArrowLeft") { + attrs.x = currentLayer.x - delta + } else if (event.key === "ArrowRight") { + attrs.x = currentLayer.x + delta + } + + dispatch(updateLayer(attrs)) + } + } + }, + } +} + +class Preview extends Component { + componentDidMount() { + if (this.props.currentLayer) { + // ensures that arrow keys always work + this.el.focus() + } + } + + render() { + const { + currentLayer, + currentLayerSelected, + sliderValue, + verticesStats, + onSlider, + onKeyDown, + } = this.props + + if (currentLayer) { + return ( +
+
+
{ + this.el = el + }} + tabIndex={0} + onKeyDown={(e) => { + if (currentLayerSelected) { + onKeyDown(e, currentLayer) + } + }} + > + +
+ +
+
+
+ Points: {verticesStats.numPoints}, Distance:{" "} + {verticesStats.distance} +
+ +
+ +
+
+ +
+
+
+ ) + } else { + return
+ } + } +} + +export default connect(mapStateToProps, mapDispatchToProps)(Preview) diff --git a/src/features/preview/MachinePreview.scss b/src/features/preview/Preview.scss similarity index 100% rename from src/features/preview/MachinePreview.scss rename to src/features/preview/Preview.scss diff --git a/src/features/preview/PreviewConnector.js b/src/features/preview/PreviewConnector.js index 90cf3b9b..bae0a5ce 100644 --- a/src/features/preview/PreviewConnector.js +++ b/src/features/preview/PreviewConnector.js @@ -1,15 +1,14 @@ import React from "react" -import { useSelector, shallowEqual } from "react-redux" +import { useSelector } from "react-redux" import { Shape } from "react-konva" import { - makeGetConnectorVertices, getSliderBounds, getSliderColors, getVertexOffsets, + getConnectingVertices, } from "../machine/selectors" -import { getPreview, getLayers } from "../store/selectors" -import { getCurrentLayer, makeGetLayer } from "../layers/selectors" -import { getCachedSelector } from "../store/selectors" +import { getPreviewState } from "../store/selectors" +import { getCurrentLayer, getLayer } from "../layers/selectors" import PreviewHelper from "./PreviewHelper" // Renders a connector between two layers. @@ -22,44 +21,35 @@ const PreviewConnector = (ownProps) => { // hooks, and the solution for now is to render the current layer instead. // https://react-redux.js.org/api/hooks#stale-props-and-zombie-children // It's quite likely there is a more elegant/proper way around this. - const layer = - getCachedSelector(makeGetLayer, ownProps.startId)(state) || - getCurrentLayer(state) - const endLayer = - getCachedSelector(makeGetLayer, ownProps.endId)(state) || - getCurrentLayer(state) - const vertices = - layer === endLayer - ? [] - : getCachedSelector( - makeGetConnectorVertices, - layer.id, - endLayer.id, - )(state) - const layers = getLayers(state) - const preview = getPreview(state) + const { startId, endId } = ownProps + const currentLayer = getCurrentLayer(state) + const startLayer = getLayer(state, startId) || getCurrentLayer(state) + const endLayer = getLayer(state, endId) + const vertices = getConnectingVertices(state, startId) + const preview = getPreviewState(state) return { - layer, + currentLayer, + startLayer, endLayer, vertices, + layer: startLayer, // renamed for preview helper sliderValue: preview.sliderValue, - selected: layers.selected, colors: getSliderColors(state), - offsetId: layer.id + "-connector", + offsetId: startId + "-connector", offsets: getVertexOffsets(state), bounds: getSliderBounds(state), } } - const props = useSelector(mapStateToProps, shallowEqual) + const props = useSelector(mapStateToProps) const { - layer, + currentLayer, + startLayer, endLayer, vertices, offsets, colors, - selected, bounds, sliderValue, } = props @@ -69,7 +59,7 @@ const PreviewConnector = (ownProps) => { const backgroundSelectedColor = "#6E6E00" const backgroundUnselectedColor = "rgba(195, 214, 230, 0.4)" const isSliding = sliderValue !== 0 - const isSelected = selected === ownProps.endId + const isSelected = currentLayer.id === endLayer.id // used by Konva to draw shape function sceneFunc(context, shape) { @@ -90,7 +80,7 @@ const PreviewConnector = (ownProps) => { context.beginPath() context.lineWidth = 1 - context.strokeStyle = currentColor + context.strokeStyle = unselectedColor helper.moveTo(context, vertices[0]) context.stroke() @@ -118,10 +108,10 @@ const PreviewConnector = (ownProps) => { return ( - {!layer.dragging && !endLayer.dragging && ( + {endLayer && !startLayer.dragging && !endLayer.dragging && ( diff --git a/src/features/preview/PreviewLayer.js b/src/features/preview/PreviewLayer.js index ef026431..1cf9d134 100644 --- a/src/features/preview/PreviewLayer.js +++ b/src/features/preview/PreviewLayer.js @@ -2,24 +2,21 @@ import React from "react" import { useSelector, useDispatch, shallowEqual } from "react-redux" import { Shape, Transformer } from "react-konva" import { - // makeGetPreviewTrackVertices, - makeGetPreviewVertices, + getPreviewVertices, getSliderColors, getVertexOffsets, getAllComputedVertices, getSliderBounds, } from "../machine/selectors" import { updateLayer } from "../layers/layersSlice" -import { getLayers, getPreview } from "../store/selectors" -import Layer from "@/features/layers/Layer" +import { getLayersState, getPreviewState } from "../store/selectors" import { getModelFromType } from "@/config/models" import { getCurrentLayer, - makeGetLayerIndex, - makeGetLayer, + getLayerIndex, + getLayer, getNumVisibleLayers, } from "../layers/selectors" -import { getCachedSelector } from "../store/selectors" import { roundP } from "../../common/util" import PreviewHelper from "./PreviewHelper" @@ -33,24 +30,19 @@ const PreviewLayer = (ownProps) => { // hooks, and the solution for now is to render the current layer instead. // https://react-redux.js.org/api/hooks#stale-props-and-zombie-children // It's quite likely there is a more elegant/proper way around this. - const layers = getLayers(state) - const layer = - getCachedSelector(makeGetLayer, ownProps.id)(state) || - getCurrentLayer(state) - const index = getCachedSelector(makeGetLayerIndex, layer.id)(state) + const layers = getLayersState(state) + const layer = getLayer(state, ownProps.id) || getCurrentLayer(state) + const index = getLayerIndex(state, layer.id) const numLayers = getNumVisibleLayers(state) - const preview = getPreview(state) + const preview = getPreviewState(state) + // const test = getLayers(state, ['layer-1', 'layer-2']) return { layer, start: index === 0, end: index === numLayers - 1, currentLayer: getCurrentLayer(state), - // trackVertices: getCachedSelector( - // makeGetPreviewTrackVertices, - // layerState.id, - // )(state), - vertices: getCachedSelector(makeGetPreviewVertices, layer.id)(state), + vertices: getPreviewVertices(state, layer.id, "1"), allVertices: getAllComputedVertices(state), selected: layers.selected, sliderValue: preview.sliderValue, @@ -75,7 +67,6 @@ const PreviewLayer = (ownProps) => { colors, bounds, } = props - //const layer = new Layer(layerState.type) const model = getModelFromType(layer.type) const dispatch = useDispatch() const width = layer.width @@ -86,6 +77,7 @@ const PreviewLayer = (ownProps) => { const backgroundUnselectedColor = "rgba(195, 214, 230, 0.4)" const isSelected = selected === ownProps.id const isSliding = sliderValue !== 0 + const isCurrent = layer.id === currentLayer.id const helper = new PreviewHelper(props) // draws a colored path when user is using slider @@ -186,6 +178,14 @@ const PreviewLayer = (ownProps) => { // dispatch(setSelectedLayer(selected == null ? currentLayer.id : null)) } + function onDragStart() { + console.log(currentLayer.id + " " + layer.id) + + if (isCurrent) { + onChange({ dragging: true }) + } + } + const shapeRef = React.createRef() const trRef = React.createRef() @@ -202,7 +202,7 @@ const PreviewLayer = (ownProps) => { {layer.visible && ( { rotation={layer.rotation || 0} sceneFunc={sceneFunc} hitFunc={hitFunc} - onDragStart={(e) => { - onChange({ dragging: true }) - }} + onDragStart={onDragStart} onDragEnd={(e) => { onChange({ dragging: false, diff --git a/src/features/preview/PreviewWindow.js b/src/features/preview/PreviewWindow.js index 61c9336c..2a9aa26f 100644 --- a/src/features/preview/PreviewWindow.js +++ b/src/features/preview/PreviewWindow.js @@ -4,7 +4,11 @@ import { Stage, Layer, Circle, Rect } from "react-konva" import throttle from "lodash/throttle" import { setPreviewSize, updatePreview } from "./previewSlice" import { updateLayer } from "../layers/layersSlice" -import { getMachine, getLayers, getPreview } from "../store/selectors" +import { + getMachineState, + getLayersState, + getPreviewState, +} from "../store/selectors" import { getCurrentLayer, getKonvaLayerIds, @@ -16,9 +20,9 @@ import PreviewLayer from "./PreviewLayer" import PreviewConnector from "./PreviewConnector" const mapStateToProps = (state, ownProps) => { - const layers = getLayers(state) - const preview = getPreview(state) - const machine = getMachine(state) + const layers = getLayersState(state) + const preview = getPreviewState(state) + const machine = getMachineState(state) return { layers, @@ -66,10 +70,6 @@ class PreviewWindow extends Component { }, false, ) - setTimeout(() => { - this.visible = true - this.resize(wrapper) - }, 250) } resize(wrapper) { @@ -92,9 +92,6 @@ class PreviewWindow extends Component { const scale = this.relativeScale(this.props) const width = this.props.use_rect ? maxX - minX : radius * 2 const height = this.props.use_rect ? maxY - minY : radius * 2 - const visibilityClass = `preview-wrapper ${ - this.visible ? "d-flex align-items-center" : "d-none" - }` // define Konva clip functions that will let us clip vertices not bound by // machine limits when dragging, and produce a visually seamless experience. @@ -116,7 +113,7 @@ class PreviewWindow extends Component { {({ store }) => ( { - const key = layerIds.join('-') + const key = layerIds.join("-") if (!cachedSelectors[fn.name]) { cachedSelectors[fn.name] = {} @@ -22,15 +22,30 @@ export const getCachedSelector = (fn, ...layerIds) => { // does a deep equality check instead of checking immutability; used in cases // where a selector depends on another selector that returns a new object each time, -// e.g., makeGetLayerIndex -export const createDeepEqualSelector = createSelectorCreator(defaultMemoize, isEqual) +// e.g., getLayerIndex +export const createDeepEqualSelector = createSelectorCreator( + defaultMemoize, + isEqual, +) -// root selectors -export const getState = state => state -export const getMain = state => { return state.main } -export const getLayers = createSelector(getMain, (main) => main.layers) -export const getApp = createSelector(getMain, (main) => main.app) -export const getExporter = createSelector(getMain, (main) => main.exporter) -export const getMachine = createSelector(getMain, (main) => main.machine) -export const getPreview = createSelector(getMain, (main) => main.preview) -export const getFonts = state => state.fonts +// root state selectors +export const getState = (state) => state +export const getMainState = (state) => state.main +export const getLayersState = createSelector( + getMainState, + (main) => main.layers, +) +export const getAppState = createSelector(getMainState, (main) => main.app) +export const getExporterState = createSelector( + getMainState, + (main) => main.exporter, +) +export const getMachineState = createSelector( + getMainState, + (main) => main.machine, +) +export const getPreviewState = createSelector( + getMainState, + (main) => main.preview, +) +export const getFontsState = (state) => state.fonts From 22b3c0b3cf4a212960d3e617093879ee2249db93 Mon Sep 17 00:00:00 2001 From: Bob Carmichael Date: Wed, 26 Jul 2023 17:51:11 -0400 Subject: [PATCH 025/126] replace relative imports with absolute ones; prettier reformatting --- src/config/models.js | 2 +- src/features/app/App.js | 16 +-- src/features/app/InputTabs.js | 14 +-- src/features/app/store.js | 4 +- src/features/exporter/CommentExporter.js | 4 +- src/features/exporter/Downloader.js | 12 +- src/features/exporter/ScaraGCodeExporter.js | 6 +- src/features/exporter/ThetaRhoExporter.js | 62 ++++++----- src/features/exporter/selectors.js | 6 +- src/features/layers/CopyLayer.js | 7 +- src/features/layers/ImportLayer.js | 11 +- src/features/layers/Layer.js | 2 +- src/features/layers/LayerEditor.js | 2 +- src/features/layers/NewLayer.js | 2 +- src/features/layers/Playlist.js | 6 +- src/features/layers/layersSlice.spec.js | 2 +- src/features/layers/selectors.js | 9 +- src/features/machine/Machine.js | 64 +++++++---- src/features/machine/MachineSettings.js | 2 +- src/features/machine/PolarMachine.js | 95 ++++++++++------ src/features/machine/PolarSettings.js | 6 +- src/features/machine/RectMachine.js | 117 ++++++++++++-------- src/features/machine/RectSettings.js | 6 +- src/features/machine/computer.js | 2 +- src/features/machine/selectors.js | 16 ++- src/features/preview/Preview.js | 16 +-- src/features/preview/PreviewConnector.js | 6 +- src/features/preview/PreviewLayer.js | 8 +- src/features/preview/PreviewWindow.js | 10 +- 29 files changed, 298 insertions(+), 217 deletions(-) diff --git a/src/config/models.js b/src/config/models.js index 6818c842..0be143de 100644 --- a/src/config/models.js +++ b/src/config/models.js @@ -23,7 +23,7 @@ import SpaceFiller from "@/models/space_filler/SpaceFiller" import Wiper from "@/models/Wiper" // effects -import FineTuning from "../models/effects/FineTuning" +import FineTuning from "@/models/effects/FineTuning" import Fisheye from "@/models/effects/Fisheye" import Loop from "@/models/effects/Loop" import Mask from "@/models/effects/Mask" diff --git a/src/features/app/App.js b/src/features/app/App.js index d63a73b2..3c63a047 100644 --- a/src/features/app/App.js +++ b/src/features/app/App.js @@ -1,11 +1,11 @@ -import React, { Component } from 'react' -import { Col, Row } from 'react-bootstrap' -import { Provider } from 'react-redux' -import Header from './Header' -import InputTabs from './InputTabs' -import Preview from '../preview/Preview' -import store from './store' -import './App.scss' +import React, { Component } from "react" +import { Col, Row } from "react-bootstrap" +import { Provider } from "react-redux" +import Header from "./Header" +import InputTabs from "./InputTabs" +import Preview from "@/features/preview/Preview" +import store from "./store" +import "./App.scss" class App extends Component { render() { diff --git a/src/features/app/InputTabs.js b/src/features/app/InputTabs.js index 468a6a4a..1c4ff4db 100644 --- a/src/features/app/InputTabs.js +++ b/src/features/app/InputTabs.js @@ -2,14 +2,14 @@ import React, { Component } from "react" import ReactGA from "react-ga" import { Tab, Tabs } from "react-bootstrap" import { connect } from "react-redux" -import MachineSettings from "../machine/MachineSettings" +import MachineSettings from "@/features/machine/MachineSettings" +import LayerEditor from "@/features/layers/LayerEditor" +import Playlist from "@/features/layers/Playlist" +import { getCurrentLayer } from "@/features/layers/selectors" +import { getFontsState } from "@/features/store/selectors" +import { loadFont, supportedFonts } from "@/features/fonts/fontsSlice" +import { chooseInput } from "./appSlice" import Footer from "./Footer" -import LayerEditor from "../layers/LayerEditor" -import Playlist from "../layers/Playlist" -import { chooseInput } from "../app/appSlice" -import { getCurrentLayer } from "../layers/selectors" -import { getFontsState } from "../store/selectors" -import { loadFont, supportedFonts } from "../fonts/fontsSlice" const mapStateToProps = (state, ownProps) => { const fonts = getFontsState(state) diff --git a/src/features/app/store.js b/src/features/app/store.js index 26db7b1a..d1925569 100644 --- a/src/features/app/store.js +++ b/src/features/app/store.js @@ -1,12 +1,12 @@ import { configureStore } from "@reduxjs/toolkit" import { combineReducers } from "redux" -import appReducer from "./appSlice" import machineReducer from "@/features/machine/machineSlice" import exporterReducer from "@/features/exporter/exporterSlice" import previewReducer from "@/features/preview/previewSlice" import fontsReducer from "@/features/fonts/fontsSlice" import layersReducer from "@/features/layers/layersSlice" -import { loadState, saveState } from "../../common/localStorage" +import { loadState, saveState } from "@/common/localStorage" +import appReducer from "./appSlice" /* const customizedMiddleware = getDefaultMiddleware({ diff --git a/src/features/exporter/CommentExporter.js b/src/features/exporter/CommentExporter.js index 41cec9f2..2b3c0ff5 100644 --- a/src/features/exporter/CommentExporter.js +++ b/src/features/exporter/CommentExporter.js @@ -1,5 +1,5 @@ -import { getModelFromType } from "../../config/models" -//import Machine from '../../models/Machine' +import { getModelFromType } from "@/config/models" +// import Machine from '@/models/Machine' import Exporter from "./Exporter" export default class CommentExporter extends Exporter { diff --git a/src/features/exporter/Downloader.js b/src/features/exporter/Downloader.js index 03f52112..adf1b7ed 100644 --- a/src/features/exporter/Downloader.js +++ b/src/features/exporter/Downloader.js @@ -1,14 +1,14 @@ import React, { Component } from "react" +import ReactGA from "react-ga" import { connect } from "react-redux" import { Button, Modal, Col, Row } from "react-bootstrap" -import DropdownOption from "../../components/DropdownOption" -import InputOption from "../../components/InputOption" -import CheckboxOption from "../../components/CheckboxOption" +import DropdownOption from "@/components/DropdownOption" +import InputOption from "@/components/InputOption" +import CheckboxOption from "@/components/CheckboxOption" import { updateExporter } from "./exporterSlice" +import { getAllComputedVertices } from "@/features/machine/selectors" +import { getLayersState, getMainState } from "@/features/store/selectors" import { getComments } from "./selectors" -import { getAllComputedVertices } from "../machine/selectors" -import { getLayersState, getMainState } from "../store/selectors" -import ReactGA from "react-ga" import GCodeExporter from "./GCodeExporter" import ScaraGCodeExporter from "./ScaraGCodeExporter" import SvgExporter from "./SvgExporter" diff --git a/src/features/exporter/ScaraGCodeExporter.js b/src/features/exporter/ScaraGCodeExporter.js index 436c9b96..955000a4 100644 --- a/src/features/exporter/ScaraGCodeExporter.js +++ b/src/features/exporter/ScaraGCodeExporter.js @@ -1,5 +1,5 @@ -import GCodeExporter from './GCodeExporter' -import { subsample, toThetaRho, toScaraGcode } from '../../common/geometry' +import GCodeExporter from "./GCodeExporter" +import { subsample, toThetaRho, toScaraGcode } from "@/common/geometry" export default class ScaraGCodeExporter extends GCodeExporter { constructor(props) { @@ -16,7 +16,7 @@ export default class ScaraGCodeExporter extends GCodeExporter { this.props.maxRadius, parseFloat(this.props.polarRhoMax), ), - parseFloat(this.props.unitsPerCircle) + parseFloat(this.props.unitsPerCircle), ) return super.computeOutputVertices(vertices) } diff --git a/src/features/exporter/ThetaRhoExporter.js b/src/features/exporter/ThetaRhoExporter.js index c9879063..bd277ddc 100644 --- a/src/features/exporter/ThetaRhoExporter.js +++ b/src/features/exporter/ThetaRhoExporter.js @@ -1,16 +1,16 @@ -import Exporter from './Exporter' -import { subsample, toThetaRho } from '../../common/geometry' +import Exporter from "./Exporter" +import { subsample, toThetaRho } from "@/common/geometry" function thetarho(vertex) { - return '' + vertex.x.toFixed(5) + ' ' + vertex.y.toFixed(5) + return "" + vertex.x.toFixed(5) + " " + vertex.y.toFixed(5) } export default class ThetaRhoExporter extends Exporter { constructor(props) { super(props) - this.fileExtension = '.thr' - this.label = 'ThetaRho' - this.commentChar = '#' + this.fileExtension = ".thr" + this.label = "ThetaRho" + this.commentChar = "#" } // computes vertices compatible with the theta rho format, and replaces @@ -21,18 +21,22 @@ export default class ThetaRhoExporter extends Exporter { const subsampledVertices = subsample(vertices, maxLength) // Convert to theta, rho - this.vertices = toThetaRho(subsampledVertices, this.props.maxRadius, parseFloat(this.props.polarRhoMax)) + this.vertices = toThetaRho( + subsampledVertices, + this.props.maxRadius, + parseFloat(this.props.polarRhoMax), + ) let starttheta = this.vertices[0].x - let startrho = this.vertices[0].y - let endtheta = this.vertices[this.vertices.length-1].x - let endrho = this.vertices[this.vertices.length-1].y + let startrho = this.vertices[0].y + let endtheta = this.vertices[this.vertices.length - 1].x + let endrho = this.vertices[this.vertices.length - 1].y let mintheta = 1e9 - let minrho = 1e9 + let minrho = 1e9 let maxtheta = -1e9 - let maxrho = -1e9 + let maxrho = -1e9 - this.vertices.forEach( thetarho => { + this.vertices.forEach((thetarho) => { minrho = Math.min(thetarho.y, minrho) maxrho = Math.max(thetarho.y, maxrho) mintheta = Math.min(thetarho.x, mintheta) @@ -40,25 +44,25 @@ export default class ThetaRhoExporter extends Exporter { }) // Replace pre/post placeholder variables - this.pre = this.pre.replace(/{starttheta}/gi, starttheta.toFixed(3)) - this.pre = this.pre.replace(/{startrho}/gi, startrho.toFixed(3)) - this.pre = this.pre.replace(/{endtheta}/gi, endtheta.toFixed(3)) - this.pre = this.pre.replace(/{endrho}/gi, endrho.toFixed(3)) - this.pre = this.pre.replace(/{mintheta}/gi, mintheta.toFixed(3)) - this.pre = this.pre.replace(/{minrho}/gi, minrho.toFixed(3)) - this.pre = this.pre.replace(/{maxtheta}/gi, maxtheta.toFixed(3)) - this.pre = this.pre.replace(/{maxrho}/gi, maxrho.toFixed(3)) + this.pre = this.pre.replace(/{starttheta}/gi, starttheta.toFixed(3)) + this.pre = this.pre.replace(/{startrho}/gi, startrho.toFixed(3)) + this.pre = this.pre.replace(/{endtheta}/gi, endtheta.toFixed(3)) + this.pre = this.pre.replace(/{endrho}/gi, endrho.toFixed(3)) + this.pre = this.pre.replace(/{mintheta}/gi, mintheta.toFixed(3)) + this.pre = this.pre.replace(/{minrho}/gi, minrho.toFixed(3)) + this.pre = this.pre.replace(/{maxtheta}/gi, maxtheta.toFixed(3)) + this.pre = this.pre.replace(/{maxrho}/gi, maxrho.toFixed(3)) this.post = this.post.replace(/{starttheta}/gi, starttheta.toFixed(3)) - this.post = this.post.replace(/{startrho}/gi, startrho.toFixed(3)) - this.post = this.post.replace(/{endtheta}/gi, endtheta.toFixed(3)) - this.post = this.post.replace(/{endrho}/gi, endrho.toFixed(3)) - this.post = this.post.replace(/{mintheta}/gi, mintheta.toFixed(3)) - this.post = this.post.replace(/{minrho}/gi, minrho.toFixed(3)) - this.post = this.post.replace(/{maxtheta}/gi, maxtheta.toFixed(3)) - this.post = this.post.replace(/{maxrho}/gi, maxrho.toFixed(3)) + this.post = this.post.replace(/{startrho}/gi, startrho.toFixed(3)) + this.post = this.post.replace(/{endtheta}/gi, endtheta.toFixed(3)) + this.post = this.post.replace(/{endrho}/gi, endrho.toFixed(3)) + this.post = this.post.replace(/{mintheta}/gi, mintheta.toFixed(3)) + this.post = this.post.replace(/{minrho}/gi, minrho.toFixed(3)) + this.post = this.post.replace(/{maxtheta}/gi, maxtheta.toFixed(3)) + this.post = this.post.replace(/{maxrho}/gi, maxrho.toFixed(3)) } exportCode(vertices) { - vertices.map(thetarho).forEach(line => this.line(line)) + vertices.map(thetarho).forEach((line) => this.line(line)) } } diff --git a/src/features/exporter/selectors.js b/src/features/exporter/selectors.js index 330328b5..efd81bba 100644 --- a/src/features/exporter/selectors.js +++ b/src/features/exporter/selectors.js @@ -1,12 +1,12 @@ import { createSelector } from "reselect" -import { getAllLayers } from "../../features/layers/selectors" +import { getAllLayers } from "@/features/layers/selectors" import CommentExporter from "./CommentExporter" -import { log } from "../../common/debugging" +import { log } from "@/common/debugging" import { getAppState, getExporterState, getMachineState, -} from "../store/selectors" +} from "@/features/store/selectors" export const getComments = createSelector( [getAppState, getAllLayers, getExporterState, getMachineState], diff --git a/src/features/layers/CopyLayer.js b/src/features/layers/CopyLayer.js index 250110df..2c1dac58 100644 --- a/src/features/layers/CopyLayer.js +++ b/src/features/layers/CopyLayer.js @@ -1,10 +1,9 @@ import React, { Component } from "react" import { Button, Modal, Row, Col, Form } from "react-bootstrap" import { connect } from "react-redux" - -import { getLayersState } from "../store/selectors" -import { copyLayer, updateLayers } from "../layers/layersSlice" -import { getCurrentLayer } from "../layers/selectors" +import { getLayersState } from "@/features/store/selectors" +import { copyLayer, updateLayers } from "./layersSlice" +import { getCurrentLayer } from "./selectors" const mapStateToProps = (state, ownProps) => { const layers = getLayersState(state) diff --git a/src/features/layers/ImportLayer.js b/src/features/layers/ImportLayer.js index 839d6f7a..5dc77751 100644 --- a/src/features/layers/ImportLayer.js +++ b/src/features/layers/ImportLayer.js @@ -1,13 +1,12 @@ import React, { Component } from "react" import { Button, Modal, Form, Accordion, Card } from "react-bootstrap" import { connect } from "react-redux" - -import { getMachineState } from "@/features/store/selectors" -import ThetaRhoImporter from "../importer/ThetaRhoImporter" -import GCodeImporter from "../importer/GCodeImporter" -import { addLayer } from "../layers/layersSlice" -import Layer from "@/features/layers/layer" import ReactGA from "react-ga" +import { getMachineState } from "@/features/store/selectors" +import ThetaRhoImporter from "@/features/importer/ThetaRhoImporter" +import GCodeImporter from "@/features/importer/GCodeImporter" +import { addLayer } from "./layersSlice" +import Layer from "./layer" const mapStateToProps = (state, ownProps) => { return { diff --git a/src/features/layers/Layer.js b/src/features/layers/Layer.js index 2d57fb4f..e9c6f6dc 100644 --- a/src/features/layers/Layer.js +++ b/src/features/layers/Layer.js @@ -1,4 +1,4 @@ -import { getModelFromType } from "../../config/models" +import { getModelFromType } from "@/config/models" import { resizeVertices, centerOnOrigin } from "@/common/geometry" export const layerOptions = { diff --git a/src/features/layers/LayerEditor.js b/src/features/layers/LayerEditor.js index 1db1e6da..a20801b1 100644 --- a/src/features/layers/LayerEditor.js +++ b/src/features/layers/LayerEditor.js @@ -9,10 +9,10 @@ import InputOption from "@/components/InputOption" import DropdownOption from "@/components/DropdownOption" import CheckboxOption from "@/components/CheckboxOption" import ToggleButtonOption from "@/components/ToggleButtonOption" -import { getCurrentLayer } from "./selectors" import { getModelSelectOptions } from "@/config/models" import { updateLayer, changeModelType, restoreDefaults } from "./layersSlice" import Layer from "./Layer" +import { getCurrentLayer } from "./selectors" import "./LayerEditor.scss" const mapStateToProps = (state, ownProps) => { diff --git a/src/features/layers/NewLayer.js b/src/features/layers/NewLayer.js index 394298a8..de2e3dba 100644 --- a/src/features/layers/NewLayer.js +++ b/src/features/layers/NewLayer.js @@ -6,7 +6,7 @@ import { getModelSelectOptions, getDefaultModel, getModelFromType, -} from "../../config/models" +} from "@/config/models" import Layer from "./Layer" import { addLayer } from "./layersSlice" diff --git a/src/features/layers/Playlist.js b/src/features/layers/Playlist.js index 9bfa3fc5..91d42858 100644 --- a/src/features/layers/Playlist.js +++ b/src/features/layers/Playlist.js @@ -8,7 +8,7 @@ import { getCurrentLayer, getNumLayers, getAllLayers, -} from "../layers/selectors" +} from "@/features/layers/selectors" import { setCurrentLayer, addLayer, @@ -16,8 +16,8 @@ import { moveLayer, toggleVisible, toggleOpen, -} from "../layers/layersSlice" -import { registeredModels, getModelFromType } from "../../config/models" +} from "@/features/layers/layersSlice" +import { registeredModels, getModelFromType } from "@/config/models" import NewLayer from "./NewLayer" import CopyLayer from "./CopyLayer" import ImportLayer from "./ImportLayer" diff --git a/src/features/layers/layersSlice.spec.js b/src/features/layers/layersSlice.spec.js index 98be016a..04d9bac0 100644 --- a/src/features/layers/layersSlice.spec.js +++ b/src/features/layers/layersSlice.spec.js @@ -1,6 +1,6 @@ jest.mock("lodash/uniqueId") const uniqueId = require("lodash/uniqueId") // eslint-disable-line @typescript-eslint/no-var-requires -import mockUniqueId, { resetUniqueIds } from "../../common/mocks" +import mockUniqueId, { resetUniqueIds } from "@/common/mocks" import layers, { addLayer, removeLayer, diff --git a/src/features/layers/selectors.js b/src/features/layers/selectors.js index acad8894..ff3f17de 100644 --- a/src/features/layers/selectors.js +++ b/src/features/layers/selectors.js @@ -1,8 +1,11 @@ -import { getLayersState, createDeepEqualSelector } from "../store/selectors" +import { + getLayersState, + createDeepEqualSelector, +} from "@/features/store/selectors" import { createSelector } from "reselect" import { createCachedSelector } from "re-reselect" -import { memoizeArrayProducingFn } from "../../common/selectors" -import { log } from "../../common/debugging" +import { memoizeArrayProducingFn } from "@/common/selectors" +import { log } from "@/common/debugging" const getCurrentLayerId = createSelector( getLayersState, diff --git a/src/features/machine/Machine.js b/src/features/machine/Machine.js index 848081a3..bbfcf5ec 100644 --- a/src/features/machine/Machine.js +++ b/src/features/machine/Machine.js @@ -1,13 +1,10 @@ -import { vertexRoundP } from '../../common/geometry' +import { vertexRoundP } from "@/common/geometry" // inherit all machine classes from this base class export default class Machine { // given a set of vertices, ensure they adhere to the limits defined by the machine polish() { - this.enforceLimits() - .cleanVertices() - .limitPrecision() - .optimizePerimeter() + this.enforceLimits().cleanVertices().limitPrecision().optimizePerimeter() if (this.layerInfo.border) this.outlinePerimeter() if (this.layerInfo.start) this.addStartPoint() @@ -22,7 +19,7 @@ export default class Machine { let previous = null let cleanVertices = [] - for (let i=0; i vertexRoundP(pt, 3)) + let clipped = this.clipSegment(previous, curr).map((pt) => + vertexRoundP(pt, 3), + ) - if (clipped.length > 0 && this.inBounds(previous) && cleanVertices.length > 0) { - const perimeter = this.tracePerimeter(cleanVertices[cleanVertices.length - 1], clipped[0]) + if ( + clipped.length > 0 && + this.inBounds(previous) && + cleanVertices.length > 0 + ) { + const perimeter = this.tracePerimeter( + cleanVertices[cleanVertices.length - 1], + clipped[0], + ) cleanVertices = [...cleanVertices, ...perimeter] } @@ -100,7 +106,6 @@ export default class Machine { // array directly to cleanVertices and avoid a shallow array copy. const args = [cleanVertices.length, 0].concat(clipped) Array.prototype.splice.apply(cleanVertices, args) - } else { cleanVertices.push(curr) } @@ -123,13 +128,15 @@ export default class Machine { // connect the segments together let connectedVertices = [] - for (let j=0; j 0) { // connect the two segments along the perimeter - const prev = segments[j-1] - connectedVertices.push(this.tracePerimeter(prev[prev.length-1], current[0])) + const prev = segments[j - 1] + connectedVertices.push( + this.tracePerimeter(prev[prev.length - 1], current[0]), + ) } connectedVertices.push(current) } @@ -145,12 +152,14 @@ export default class Machine { let segment = [] let perimeter = null - for (let i=0; i { const dist = Math.min( - this.perimeterDistance(current[current.length-1], segment[0]), - this.perimeterDistance(current[current.length-1], segment[segment.length-1]) + this.perimeterDistance(current[current.length - 1], segment[0]), + this.perimeterDistance( + current[current.length - 1], + segment[segment.length - 1], + ), ) if (dist < minDist) { @@ -204,7 +216,13 @@ export default class Machine { // reverse if needed to connect current = segments.splice(currentIndex, 1)[0] - if (this.perimeterDistance(prev[prev.length-1], current[0]) > this.perimeterDistance(prev[prev.length-1], current[current.length-1])) { + if ( + this.perimeterDistance(prev[prev.length - 1], current[0]) > + this.perimeterDistance( + prev[prev.length - 1], + current[current.length - 1], + ) + ) { current = current.reverse() } walked.push(current) @@ -220,7 +238,7 @@ export default class Machine { // round each vertex to the nearest .001. This eliminates floating point // math errors and allows us to do accurate equality comparisons. limitPrecision() { - this.vertices = this.vertices.map(vertex => vertexRoundP(vertex, 3)) + this.vertices = this.vertices.map((vertex) => vertexRoundP(vertex, 3)) return this } } diff --git a/src/features/machine/MachineSettings.js b/src/features/machine/MachineSettings.js index d035fa19..31c6ff69 100644 --- a/src/features/machine/MachineSettings.js +++ b/src/features/machine/MachineSettings.js @@ -3,7 +3,7 @@ import { connect } from "react-redux" import { Accordion } from "react-bootstrap" import RectSettings from "./RectSettings" import PolarSettings from "./PolarSettings" -import { getMachineState } from "../store/selectors" +import { getMachineState } from "@/features/store/selectors" const mapStateToProps = (state, ownProps) => { const machine = getMachineState(state) diff --git a/src/features/machine/PolarMachine.js b/src/features/machine/PolarMachine.js index 84a4a5ce..cd8efb47 100644 --- a/src/features/machine/PolarMachine.js +++ b/src/features/machine/PolarMachine.js @@ -1,9 +1,9 @@ -import { angle, onSegment, circle, arc } from '../../common/geometry' -import Machine from './Machine' -import Victor from 'victor' +import { angle, onSegment, circle, arc } from "@/common/geometry" +import Machine from "./Machine" +import Victor from "victor" export default class PolarMachine extends Machine { - constructor(vertices, settings, layerInfo={}) { + constructor(vertices, settings, layerInfo = {}) { super() this.vertices = vertices this.settings = Object.assign({}, settings) @@ -15,13 +15,15 @@ export default class PolarMachine extends Machine { addStartPoint() { const maxRadius = this.settings.maxRadius - if (this.settings.polarStartPoint !== 'none') { - if (this.settings.polarStartPoint === 'center') { + if (this.settings.polarStartPoint !== "none") { + if (this.settings.polarStartPoint === "center") { this.vertices.unshift(new Victor(0.0, 0.0)) } else { const first = this.vertices[0] const scale = maxRadius / first.magnitude() - const startPoint = Victor.fromObject(first).multiply(new Victor(scale, scale)) + const startPoint = Victor.fromObject(first).multiply( + new Victor(scale, scale), + ) this.vertices.unshift(new Victor(startPoint.x, startPoint.y)) } } @@ -30,13 +32,15 @@ export default class PolarMachine extends Machine { addEndPoint() { const maxRadius = this.settings.maxRadius - if (this.settings.polarEndPoint !== 'none') { - if (this.settings.polarEndPoint === 'center') { + if (this.settings.polarEndPoint !== "none") { + if (this.settings.polarEndPoint === "center") { this.vertices.push(new Victor(0.0, 0.0)) } else { - const last = this.vertices[this.vertices.length-1] + const last = this.vertices[this.vertices.length - 1] const scale = maxRadius / last.magnitude() - const endPoint = Victor.fromObject(last).multiply(new Victor(scale, scale)) + const endPoint = Victor.fromObject(last).multiply( + new Victor(scale, scale), + ) this.vertices.push(new Victor(endPoint.x, endPoint.y)) } } @@ -65,9 +69,12 @@ export default class PolarMachine extends Machine { // Returns the nearest perimeter vertex to the given vertex. nearestPerimeterVertex(vertex) { if (vertex) { - return new Victor(Math.cos(vertex.angle()) * this.settings.maxRadius, Math.sin(vertex.angle()) * this.settings.maxRadius) + return new Victor( + Math.cos(vertex.angle()) * this.settings.maxRadius, + Math.sin(vertex.angle()) * this.settings.maxRadius, + ) } else { - return new Victor(0,0) + return new Victor(0, 0) } } @@ -93,13 +100,18 @@ export default class PolarMachine extends Machine { const last = this.vertices[this.vertices.length - 1] if (last) { - this.vertices = this.vertices.concat(circle(this.settings.maxRadius, parseInt(last.angle()*64/Math.PI))) + this.vertices = this.vertices.concat( + circle( + this.settings.maxRadius, + parseInt((last.angle() * 64) / Math.PI), + ), + ) } return this } // Returns whether a given path lies on the perimeter of the circle. - onPerimeter(v1, v2, delta=1) { + onPerimeter(v1, v2, delta = 1) { let rm = Math.pow(this.settings.maxRadius, 2) let r1 = Math.pow(v1.x, 2) + Math.pow(v1.y, 2) let r2 = Math.pow(v2.x, 2) + Math.pow(v2.y, 2) @@ -112,7 +124,11 @@ export default class PolarMachine extends Machine { // move, or it will be incorrectly optimized out of the final vertices. The 3/50 // ratio could likely be refined further (relative to maxRadius), but it seems to produce // accurate results at various machine sizes. - return Math.abs(r1 - rm) < delta && Math.abs(r2 - rm) < delta && d < 3*this.settings.maxRadius/this.settings.perimeterConstant + return ( + Math.abs(r1 - rm) < delta && + Math.abs(r2 - rm) < delta && + d < (3 * this.settings.maxRadius) / this.settings.perimeterConstant + ) } // The guts of logic for this limits enforcer. It will take a single line (defined by @@ -146,7 +162,7 @@ export default class PolarMachine extends Machine { // Check for the odd case of coincident points if (start.distance(end) < 0.00001) { - return [this.nearestVertex(start)] + return [this.nearestVertex(start)] } const intersections = this.getIntersections(start, end) @@ -169,27 +185,29 @@ export default class PolarMachine extends Machine { return [ ...this.tracePerimeter(start, point), point, - ...this.tracePerimeter(otherPoint, end) + ...this.tracePerimeter(otherPoint, end), ] } // If we're here, then one point is still in the circle. if (radStart <= size) { - const point1 = (intersections.points[0].on && Math.abs(intersections.points[0].point - start) > 0.0001) ? intersections.points[0].point : intersections.points[1].point + const point1 = + intersections.points[0].on && + Math.abs(intersections.points[0].point - start) > 0.0001 + ? intersections.points[0].point + : intersections.points[1].point return [ start, ...this.tracePerimeter(point1, end), - this.nearestPerimeterVertex(end) + this.nearestPerimeterVertex(end), ] } else { - const point1 = intersections.points[0].on ? intersections.points[0].point : intersections.points[1].point + const point1 = intersections.points[0].on + ? intersections.points[0].point + : intersections.points[1].point - return [ - ...this.tracePerimeter(start, point1), - point1, - end - ] + return [...this.tracePerimeter(start, point1), point1, end] } } @@ -197,7 +215,7 @@ export default class PolarMachine extends Machine { const size = this.settings.maxRadius let direction = end.clone().subtract(start).clone().normalize() let t = direction.x * -1.0 * start.x + direction.y * -1.0 * start.y - let e = direction.clone().multiply(Victor(t,t)).add(start) + let e = direction.clone().multiply(Victor(t, t)).add(start) let distanceToLine = e.magnitude() if (distanceToLine >= size) { @@ -207,9 +225,15 @@ export default class PolarMachine extends Machine { } } - const dt = Math.sqrt(size*size - distanceToLine*distanceToLine) - const point1 = direction.clone().multiply(Victor(t - dt,t - dt)).add(start) - const point2 = direction.clone().multiply(Victor(t + dt,t + dt)).add(start) + const dt = Math.sqrt(size * size - distanceToLine * distanceToLine) + const point1 = direction + .clone() + .multiply(Victor(t - dt, t - dt)) + .add(start) + const point2 = direction + .clone() + .multiply(Victor(t + dt, t + dt)) + .add(start) const s1 = onSegment(start, end, point1) const s2 = onSegment(start, end, point2) @@ -219,13 +243,14 @@ export default class PolarMachine extends Machine { points: [ { point: point1, - on: s1 + on: s1, }, { point: point2, - on: s2 - } - ]} + on: s2, + }, + ], + } } else { return { intersection: false, @@ -236,6 +261,6 @@ export default class PolarMachine extends Machine { // returns the points if any that intersect with the line represented by start and end clipLine(start, end) { - return this.getIntersections(start, end).points.map(pt => pt.point) + return this.getIntersections(start, end).points.map((pt) => pt.point) } } diff --git a/src/features/machine/PolarSettings.js b/src/features/machine/PolarSettings.js index 4b7dda0a..02cfd705 100644 --- a/src/features/machine/PolarSettings.js +++ b/src/features/machine/PolarSettings.js @@ -9,9 +9,9 @@ import { ToggleButton, ToggleButtonGroup, } from "react-bootstrap" -import InputOption from "../../components/InputOption" -import CheckboxOption from "../../components/CheckboxOption" -import { getMachineState } from "../store/selectors" +import InputOption from "@/components/InputOption" +import CheckboxOption from "@/components/CheckboxOption" +import { getMachineState } from "@/features/store/selectors" import { machineOptions } from "./options" import { toggleMachinePolarExpanded, diff --git a/src/features/machine/RectMachine.js b/src/features/machine/RectMachine.js index 21168c36..d17b382c 100644 --- a/src/features/machine/RectMachine.js +++ b/src/features/machine/RectMachine.js @@ -1,10 +1,10 @@ -import Victor from 'victor' -import Machine from './Machine' -import { distance, vertexRoundP } from '../../common/geometry' -import clip from 'liang-barsky' +import Victor from "victor" +import Machine from "./Machine" +import { distance, vertexRoundP } from "@/common/geometry" +import clip from "liang-barsky" export default class RectMachine extends Machine { - constructor(vertices, settings, layerInfo={}) { + constructor(vertices, settings, layerInfo = {}) { super() this.vertices = vertices this.settings = settings @@ -15,7 +15,7 @@ export default class RectMachine extends Machine { new Victor(-this.sizeX, -this.sizeY), new Victor(-this.sizeX, this.sizeY), new Victor(this.sizeX, this.sizeY), - new Victor(this.sizeX, -this.sizeY) + new Victor(this.sizeX, -this.sizeY), ] } @@ -32,8 +32,11 @@ export default class RectMachine extends Machine { // [0] [3] const corner = this.settings.rectOrigin[0] const first = this.vertices[0] - const last = this.vertices[this.vertices.length-1] - const maxRadius = Math.sqrt(Math.pow(2.0*this.sizeX, 2.0) + Math.pow(2.0*this.sizeY, 2.0)) / 2.0 + const last = this.vertices[this.vertices.length - 1] + const maxRadius = + Math.sqrt( + Math.pow(2.0 * this.sizeX, 2.0) + Math.pow(2.0 * this.sizeY, 2.0), + ) / 2.0 let scale, outPoint if (first.magnitude() <= last.magnitude()) { @@ -47,13 +50,19 @@ export default class RectMachine extends Machine { let clipped = this.clipSegment( outPoint, - Victor.fromObject(outPoint).multiply(new Victor(scale, scale)) + Victor.fromObject(outPoint).multiply(new Victor(scale, scale)), ) const newPoint = clipped[clipped.length - 1] if (outPoint === last) { - this.vertices = [this.vertices, this.tracePerimeter(newPoint, this.corners[corner], true)].flat() + this.vertices = [ + this.vertices, + this.tracePerimeter(newPoint, this.corners[corner], true), + ].flat() } else { - this.vertices = [this.tracePerimeter(this.corners[corner], newPoint, true), this.vertices].flat() + this.vertices = [ + this.tracePerimeter(this.corners[corner], newPoint, true), + this.vertices, + ].flat() } } @@ -66,7 +75,7 @@ export default class RectMachine extends Machine { } // Returns whether a given path lies on the perimeter of the rectangle - onPerimeter(v1, v2, delta=.0001) { + onPerimeter(v1, v2, delta = 0.0001) { const dx = Math.abs(Math.abs(v1.x) - this.sizeX) const dy = Math.abs(Math.abs(v1.y) - this.sizeY) const rDx = Math.abs(v1.x - v2.x) @@ -76,7 +85,7 @@ export default class RectMachine extends Machine { } outlinePerimeter() { - const last = this.vertices[this.vertices.length-1] + const last = this.vertices[this.vertices.length - 1] if (last) { const s = this.nearestVertex(last) @@ -87,7 +96,7 @@ export default class RectMachine extends Machine { this.corners[(idx + 1) % 4], this.corners[(idx + 2) % 4], this.corners[(idx + 3) % 4], - this.corners[idx] + this.corners[idx], ] this.vertices = this.vertices.concat(corners) } @@ -99,10 +108,13 @@ export default class RectMachine extends Machine { // perimeter). Returns a list of intermediate points on that path (if any). // On further consideration, this could be redone using Dijsktra's algorithm, I believe, // but this works and is, I believe, reasonably efficient. - tracePerimeter(p1, p2, includeOriginalPoints=false) { + tracePerimeter(p1, p2, includeOriginalPoints = false) { let points - if ((p1.x === p2.x && Math.abs(p1.x) === this.sizeX) || (p1.y === p2.y && (Math.abs(p1.y) === this.sizeY))) { + if ( + (p1.x === p2.x && Math.abs(p1.x) === this.sizeX) || + (p1.y === p2.y && Math.abs(p1.y) === this.sizeY) + ) { // on the same line; no connecting points needed points = [] } else { @@ -110,33 +122,44 @@ export default class RectMachine extends Machine { // end up within incorrect reading const lp1 = vertexRoundP(p1, 3) const lp2 = vertexRoundP(p2, 3) - const o1 = Math.abs(lp1.x) === this.sizeX ? 'v' : 'h' - const o2 = Math.abs(lp2.x) === this.sizeX ? 'v' : 'h' + const o1 = Math.abs(lp1.x) === this.sizeX ? "v" : "h" + const o2 = Math.abs(lp2.x) === this.sizeX ? "v" : "h" if (o1 !== o2) { // connects via a single corner - points = (o1 === 'h') ? - [new Victor(p2.x, p1.y)] : - [new Victor(p1.x, p2.y)] + points = + o1 === "h" ? [new Victor(p2.x, p1.y)] : [new Victor(p1.x, p2.y)] } else { // connects via two corners; find the shortest way around - if (o1 === 'h') { - let d1 = -2*this.sizeX - p1.x - p2.x - let d2 = 2*this.sizeX - p1.x - p2.x + if (o1 === "h") { + let d1 = -2 * this.sizeX - p1.x - p2.x + let d2 = 2 * this.sizeX - p1.x - p2.x let xSign = Math.abs(d1) > Math.abs(d2) ? 1 : -1 points = [ - new Victor(Math.sign(xSign)*this.sizeX, Math.sign(p1.y)*this.sizeY), - new Victor(Math.sign(xSign)*this.sizeX, -Math.sign(p1.y)*this.sizeY) + new Victor( + Math.sign(xSign) * this.sizeX, + Math.sign(p1.y) * this.sizeY, + ), + new Victor( + Math.sign(xSign) * this.sizeX, + -Math.sign(p1.y) * this.sizeY, + ), ] } else { - let d1 = -2*this.sizeY - p1.y - p2.y - let d2 = 2*this.sizeY - p1.y - p2.y + let d1 = -2 * this.sizeY - p1.y - p2.y + let d2 = 2 * this.sizeY - p1.y - p2.y let ySign = Math.abs(d1) > Math.abs(d2) ? 1 : -1 points = [ - new Victor(Math.sign(p1.x)*this.sizeX, Math.sign(ySign)*this.sizeY), - new Victor(-Math.sign(p1.x)*this.sizeX, Math.sign(ySign)*this.sizeY), + new Victor( + Math.sign(p1.x) * this.sizeX, + Math.sign(ySign) * this.sizeY, + ), + new Victor( + -Math.sign(p1.x) * this.sizeX, + Math.sign(ySign) * this.sizeY, + ), ] } } @@ -155,7 +178,7 @@ export default class RectMachine extends Machine { nearestVertex(vertex) { return new Victor( Math.min(this.sizeX, Math.max(-this.sizeX, vertex.x)), - Math.min(this.sizeY, Math.max(-this.sizeY, vertex.y)) + Math.min(this.sizeY, Math.max(-this.sizeY, vertex.y)), ) } @@ -230,18 +253,18 @@ export default class RectMachine extends Machine { // right [new Victor(this.sizeX, -this.sizeY), new Victor(this.sizeX, this.sizeY)], // bottom - [new Victor(-this.sizeX, -this.sizeY), new Victor(this.sizeX, -this.sizeY)], + [ + new Victor(-this.sizeX, -this.sizeY), + new Victor(this.sizeX, -this.sizeY), + ], // top [new Victor(-this.sizeX, this.sizeY), new Victor(this.sizeX, this.sizeY)], ] // Count up the number of boundary lines intersect with our line segment. let intersections = [] - for (let s=0; s - Victor.fromObject(intersections[1]).subtract(start).lengthSq()) { + if ( + Victor.fromObject(intersections[0]).subtract(start).lengthSq() > + Victor.fromObject(intersections[1]).subtract(start).lengthSq() + ) { let temp = intersections[0] intersections[0] = intersections[1] intersections[1] = temp @@ -272,11 +297,15 @@ export default class RectMachine extends Machine { // box until we reach the other point. // Here, I'm going to split this line into two parts, and send each half line segment back // through the clipSegment algorithm. Eventually, that should result in only one of the other cases. - const midpoint = Victor.fromObject(start).add(end).multiply(new Victor(0.5, 0.5)) + const midpoint = Victor.fromObject(start) + .add(end) + .multiply(new Victor(0.5, 0.5)) // recurse, and find smaller segments until we don't end up in this place again. - return [...this.clipSegment(start, midpoint), - ...this.clipSegment(midpoint, end)] + return [ + ...this.clipSegment(start, midpoint), + ...this.clipSegment(midpoint, end), + ] } // Intersect the line with the boundary, and return the point exactly on the boundary. @@ -323,8 +352,8 @@ export default class RectMachine extends Machine { // Returns the distance walked from the first vertex to the last vertex. distance(vertices) { let d = 0 - for(let i=0; i 0) d = d + distance(vertices[i], vertices[i-1]) + for (let i = 0; i < vertices.length; i++) { + if (i > 0) d = d + distance(vertices[i], vertices[i - 1]) } return d diff --git a/src/features/machine/RectSettings.js b/src/features/machine/RectSettings.js index d701f869..a297e44c 100644 --- a/src/features/machine/RectSettings.js +++ b/src/features/machine/RectSettings.js @@ -9,15 +9,15 @@ import { ToggleButton, ToggleButtonGroup, } from "react-bootstrap" -import InputOption from "../../components/InputOption" -import CheckboxOption from "../../components/CheckboxOption" +import InputOption from "@/components/InputOption" +import CheckboxOption from "@/components/CheckboxOption" +import { getMachineState } from "@/features/store/selectors" import { updateMachine, toggleMinimizeMoves, toggleMachineRectExpanded, setMachineRectOrigin, } from "./machineSlice" -import { getMachineState } from "../store/selectors" import { machineOptions } from "./options" const mapStateToProps = (state, ownProps) => { diff --git a/src/features/machine/computer.js b/src/features/machine/computer.js index 0d4a33da..1a1c9c51 100644 --- a/src/features/machine/computer.js +++ b/src/features/machine/computer.js @@ -2,7 +2,7 @@ import ReactGA from "react-ga" import throttle from "lodash/throttle" import PolarMachine from "./PolarMachine" import RectMachine from "./RectMachine" -import { getModelFromType } from "../../config/models" +import { getModelFromType } from "@/config/models" import Victor from "victor" //function track(vertex, data, loopIndex) { diff --git a/src/features/machine/selectors.js b/src/features/machine/selectors.js index 82170904..7969758f 100644 --- a/src/features/machine/selectors.js +++ b/src/features/machine/selectors.js @@ -1,8 +1,11 @@ import LRUCache from "lru-cache" import { createSelector } from "reselect" import Color from "color" -import { transformShapes, polishVertices, getMachineInstance } from "./computer" -import { getMachineState, getState, getPreviewState } from "../store/selectors" +import { + getMachineState, + getState, + getPreviewState, +} from "@/features/store/selectors" import { createCachedSelector } from "re-reselect" import { getLayer, @@ -10,11 +13,12 @@ import { getVisibleNonEffectIds, getLayerEffects, getNonEffectLayerIndex, -} from "../layers/selectors" -import Layer from "../layers/Layer" +} from "@/features/layers/selectors" +import Layer from "@/features/layers/Layer" import { getModelFromType } from "@/config/models" -import { rotate, offset } from "../../common/geometry" -import { log } from "../../common/debugging" +import { rotate, offset } from "@/common/geometry" +import { log } from "@/common/debugging" +import { transformShapes, polishVertices, getMachineInstance } from "./computer" const cache = new LRUCache({ length: (n, key) => { diff --git a/src/features/preview/Preview.js b/src/features/preview/Preview.js index 2135441e..ec096a61 100644 --- a/src/features/preview/Preview.js +++ b/src/features/preview/Preview.js @@ -2,15 +2,15 @@ import React, { Component } from "react" import { connect } from "react-redux" import Slider from "rc-slider" import "rc-slider/assets/index.css" -import PreviewWindow from "./PreviewWindow" -import Downloader from "../exporter/Downloader" -import { getFontsState } from "../store/selectors" -import { getCurrentLayer } from "../layers/selectors" -import { getLayersState, getPreviewState } from "../store/selectors" -import { updateLayer } from "../layers/layersSlice" -import { updatePreview } from "./previewSlice" -import { getVerticesStats } from "../machine/selectors" +import Downloader from "@/features/exporter/Downloader" +import { getFontsState } from "@/features/store/selectors" +import { getCurrentLayer } from "@/features/layers/selectors" +import { getLayersState, getPreviewState } from "@/features/store/selectors" +import { updateLayer } from "@/features/layers/layersSlice" +import { getVerticesStats } from "@/features/machine/selectors" import "./Preview.scss" +import { updatePreview } from "./previewSlice" +import PreviewWindow from "./PreviewWindow" const mapStateToProps = (state, ownProps) => { const fonts = getFontsState(state) diff --git a/src/features/preview/PreviewConnector.js b/src/features/preview/PreviewConnector.js index bae0a5ce..365c4dd7 100644 --- a/src/features/preview/PreviewConnector.js +++ b/src/features/preview/PreviewConnector.js @@ -6,9 +6,9 @@ import { getSliderColors, getVertexOffsets, getConnectingVertices, -} from "../machine/selectors" -import { getPreviewState } from "../store/selectors" -import { getCurrentLayer, getLayer } from "../layers/selectors" +} from "@/features/machine/selectors" +import { getPreviewState } from "@/features/store/selectors" +import { getCurrentLayer, getLayer } from "@/features/layers/selectors" import PreviewHelper from "./PreviewHelper" // Renders a connector between two layers. diff --git a/src/features/preview/PreviewLayer.js b/src/features/preview/PreviewLayer.js index 1cf9d134..958d9dd9 100644 --- a/src/features/preview/PreviewLayer.js +++ b/src/features/preview/PreviewLayer.js @@ -8,16 +8,16 @@ import { getAllComputedVertices, getSliderBounds, } from "../machine/selectors" -import { updateLayer } from "../layers/layersSlice" -import { getLayersState, getPreviewState } from "../store/selectors" +import { updateLayer } from "@/features/layers/layersSlice" +import { getLayersState, getPreviewState } from "@/features/store/selectors" import { getModelFromType } from "@/config/models" import { getCurrentLayer, getLayerIndex, getLayer, getNumVisibleLayers, -} from "../layers/selectors" -import { roundP } from "../../common/util" +} from "@/features/layers/selectors" +import { roundP } from "@/common/util" import PreviewHelper from "./PreviewHelper" // Renders the shapes in the preview window and allows the user to interact with the shape. diff --git a/src/features/preview/PreviewWindow.js b/src/features/preview/PreviewWindow.js index 2a9aa26f..d0897f47 100644 --- a/src/features/preview/PreviewWindow.js +++ b/src/features/preview/PreviewWindow.js @@ -2,22 +2,22 @@ import React, { Component } from "react" import { connect, ReactReduxContext, Provider } from "react-redux" import { Stage, Layer, Circle, Rect } from "react-konva" import throttle from "lodash/throttle" -import { setPreviewSize, updatePreview } from "./previewSlice" -import { updateLayer } from "../layers/layersSlice" +import { updateLayer } from "@/features/layers/layersSlice" import { getMachineState, getLayersState, getPreviewState, -} from "../store/selectors" +} from "@/features/store/selectors" import { getCurrentLayer, getKonvaLayerIds, getVisibleNonEffectIds, isDragging, -} from "../layers/selectors" -import { roundP } from "../../common/util" +} from "@/features/layers/selectors" +import { roundP } from "@/common/util" import PreviewLayer from "./PreviewLayer" import PreviewConnector from "./PreviewConnector" +import { setPreviewSize, updatePreview } from "./previewSlice" const mapStateToProps = (state, ownProps) => { const layers = getLayersState(state) From b6e63b71e3817dd8836174d6560c980acbc0d21c Mon Sep 17 00:00:00 2001 From: Bob Carmichael Date: Fri, 28 Jul 2023 08:32:16 -0400 Subject: [PATCH 026/126] convert a few class components into functional ones; fixes --- .eslintrc.js | 1 + src/components/CheckboxOption.js | 55 ++-- src/components/CommentsBox.js | 35 +-- src/components/DropdownOption.js | 109 +++---- src/components/InputOption.js | 169 ++++++----- src/components/ToggleButtonOption.js | 3 +- src/features/app/store.js | 5 +- src/features/layers/CopyLayer.js | 148 +++++----- src/features/layers/LayerEditor.js | 372 +++++++++++------------- src/features/layers/NewLayer.js | 182 +++++------- src/features/layers/layersSlice.js | 6 +- src/features/layers/layersSlice.spec.js | 7 - src/features/machine/selectors.js | 10 +- src/features/preview/Preview.js | 179 +++++------- src/features/preview/PreviewLayer.js | 169 +++++------ src/features/preview/PreviewWindow.js | 323 +++++++++----------- 16 files changed, 795 insertions(+), 978 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 066dc7ef..a2dbbd20 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -35,6 +35,7 @@ module.exports = { "no-redeclare": "off", "@typescript-eslint/no-redeclare": "warn", "no-use-before-define": "off", + "react-redux/useSelector-prefer-selectors": "off", "@typescript-eslint/no-use-before-define": [ "warn", { diff --git a/src/components/CheckboxOption.js b/src/components/CheckboxOption.js index a01d9e37..267c6162 100644 --- a/src/components/CheckboxOption.js +++ b/src/components/CheckboxOption.js @@ -1,40 +1,35 @@ -import React, { Component } from "react" +import React from "react" import { Col, Row, Form } from "react-bootstrap" import S from "react-switch" const Switch = S.default ? S.default : S // Fix: https://github.com/vitejs/vite/issues/2139 -class CheckboxOption extends Component { - render() { - const option = this.props.options[this.props.optionKey] - const { data } = this.props - const object = this.props.object || data - const visible = - option.isVisible === undefined ? true : option.isVisible(object, data) +const CheckboxOption = ({ options, optionKey, data, object, handleChange }) => { + const option = options[optionKey] + const visible = + option.isVisible === undefined ? true : option.isVisible(object, data) - return ( - - - {option.title} - + return ( + + + {option.title} + - - { - let attrs = {} - attrs[this.props.optionKey] = checked + + { + let attrs = {} + attrs[optionKey] = checked - if (option.onChange !== undefined) { - attrs = option.onChange(object, attrs, data) - } + if (option.handleChange !== undefined) { + attrs = option.handleChange(object, attrs, data) + } - this.props.onChange(attrs) - }} - /> - - - ) - } + handleChange(attrs) + }} + /> + + + ) } - export default CheckboxOption diff --git a/src/components/CommentsBox.js b/src/components/CommentsBox.js index 1b3cf3a8..4a445d9d 100644 --- a/src/components/CommentsBox.js +++ b/src/components/CommentsBox.js @@ -1,23 +1,24 @@ -import React, { Component } from "react" - -class CommentsBox extends Component { - render() { - const option = this.props.options[this.props.optionKey] - const renderedComments = this.props.comments.map((comment, index) => { - return ( - - {comment} -
-
- ) - }) +import React from "react" +const CommentsBox = ({ options, optionKey, data, comments }) => { + const option = options[optionKey] + const renderedComments = data.comments.map((comment, index) => { return ( -
- {option.title}:
{renderedComments}
-
+ + {comment} +
+
) - } + }) + + return ( +
+ {option.title}:
{renderedComments}
+
+ ) } export default CommentsBox diff --git a/src/components/DropdownOption.js b/src/components/DropdownOption.js index d7b4e2ee..3ea29345 100644 --- a/src/components/DropdownOption.js +++ b/src/components/DropdownOption.js @@ -1,60 +1,63 @@ -import React, { Component } from "react" +import React from "react" import { Col, Form, Row } from "react-bootstrap" import Select from "react-select" -class DropdownOption extends Component { - render() { - const option = this.props.options[this.props.optionKey] - const data = this.props.data - const object = this.props.object || data - const currentChoice = data[this.props.optionKey] - - let choices = option.choices - if (typeof choices === "function") { - choices = choices() - } - - choices = Array.isArray(choices) - ? choices.map((choice) => { - return { value: choice, label: choice } - }) - : Object.keys(choices).map((key) => { - return { value: key, label: option.choices[key] } - }) - - const currentLabel = Array.isArray(choices) - ? currentChoice - : choices[currentChoice] - - return ( - - - {option.title} - - - - { + const value = choice.value + let attrs = {} + attrs[optionKey] = value + + if (option.handleChange !== undefined) { + attrs = option.handleChange(object, attrs, data) + } + + handleChange(attrs) + }} + options={choices} + /> + + + ) } export default DropdownOption diff --git a/src/components/InputOption.js b/src/components/InputOption.js index 9a09c3ef..d2bb177a 100644 --- a/src/components/InputOption.js +++ b/src/components/InputOption.js @@ -1,103 +1,100 @@ -import React, { Component } from "react" +import React, { useState, useEffect } from "react" import { Col, Form, Row } from "react-bootstrap" import debounce from "lodash/debounce" -class InputOption extends Component { - constructor(props) { - super(props) - this.delayedSet = debounce((value, key, onChange) => { - let attrs = {} - attrs[key] = value - onChange(attrs) - }, 1500) - } +const InputOption = ({ + data, + options, + optionKey, + handleChange, + delayKey, + object, + label = true, +}) => { + const [value, setValue] = useState(data[optionKey]) - render() { - const { - data, - options, - optionKey, - onChange, - delayKey, - label = true, - } = this.props - const option = options[optionKey] - const object = this.props.object || data - const optionType = option.type || "number" - const minimum = - typeof option.min === "function" - ? option.min(data) - : parseFloat(option.min) - const maximum = - typeof option.max === "function" - ? option.max(data) - : parseFloat(option.max) - const visible = - option.isVisible === undefined ? true : option.isVisible(object, data) + useEffect(() => { + setValue(data[optionKey]) + }, [data, optionKey]) - const renderedInput = ( - { - let attrs = {} - let value = event.target.value + const option = options[optionKey] + const optionType = option.type || "number" + const minimum = + typeof option.min === "function" ? option.min(data) : parseFloat(option.min) + const maximum = + typeof option.max === "function" ? option.max(data) : parseFloat(option.max) + const visible = + option.isVisible === undefined ? true : option.isVisible(object, data) - if (optionType === "number") { - value = value === "" ? "" : parseFloat(value) - } + const delayedSet = debounce((value, key, handleChange) => { + let attrs = {} + attrs[key] = value + handleChange(attrs) + }, 1500) - attrs[optionKey] = value + const renderedInput = ( + { + let newValue = event.target.value - if (option.onChange !== undefined) { - attrs = option.onChange(object, attrs, data) - } - onChange(attrs) + if (optionType === "number") { + newValue = newValue === "" ? "" : parseFloat(newValue) + } - if (delayKey !== undefined) { - this.delayedSet(value, delayKey, onChange) - } - }} - /> - ) + setValue(newValue) + + let attrs = {} + attrs[optionKey] = newValue + + if (option.handleChange !== undefined) { + attrs = option.handleChange(object, attrs, data) + } + handleChange(attrs) - if (!option.inline) { - return ( - - - {label && ( - - {option.title} - - )} - - {renderedInput} - - ) - } else { - return ( -
+ if (delayKey !== undefined) { + delayedSet(newValue, delayKey, handleChange) + } + }} + /> + ) + + if (!option.inline) { + return ( + + {label && ( - + {option.title} )} - {renderedInput} -
- ) - } + + {renderedInput} +
+ ) + } else { + return ( +
+ {label && ( + + {option.title} + + )} + {renderedInput} +
+ ) } } - export default InputOption diff --git a/src/components/ToggleButtonOption.js b/src/components/ToggleButtonOption.js index c4df3e33..cee6736b 100644 --- a/src/components/ToggleButtonOption.js +++ b/src/components/ToggleButtonOption.js @@ -25,13 +25,14 @@ class ToggleButtonOption extends Component { { let attrs = {} attrs[this.props.optionKey] = choice - this.props.onChange(attrs) + this.props.handleChange(attrs) }} > {option.choices.map((choice) => { diff --git a/src/features/app/store.js b/src/features/app/store.js index d1925569..a3898cc2 100644 --- a/src/features/app/store.js +++ b/src/features/app/store.js @@ -34,8 +34,11 @@ const persistedState = typeof jest === "undefined" && usePersistedState ? loadState(persistInitKey) || undefined : undefined + // reset some values -persistedState.fonts.loaded = false +if (persistedState) { + persistedState.fonts.loaded = false +} const store = configureStore({ reducer: combineReducers({ diff --git a/src/features/layers/CopyLayer.js b/src/features/layers/CopyLayer.js index 2c1dac58..bf32ebf6 100644 --- a/src/features/layers/CopyLayer.js +++ b/src/features/layers/CopyLayer.js @@ -1,97 +1,79 @@ -import React, { Component } from "react" +import React, { useRef, useState } from "react" import { Button, Modal, Row, Col, Form } from "react-bootstrap" -import { connect } from "react-redux" -import { getLayersState } from "@/features/store/selectors" -import { copyLayer, updateLayers } from "./layersSlice" +import { useDispatch, useSelector } from "react-redux" +import { copyLayer } from "./layersSlice" import { getCurrentLayer } from "./selectors" -const mapStateToProps = (state, ownProps) => { - const layers = getLayersState(state) - const current = getCurrentLayer(state) +const CopyLayer = ({ toggleModal, showModal }) => { + const dispatch = useDispatch() + const currentLayer = useSelector(getCurrentLayer) + const namedInputRef = useRef(null) + const [copyLayerName, setCopyLayerName] = useState(currentLayer.name) - return { - copyLayerName: layers.copyLayerName || current.name, - showModal: ownProps.showModal, - currentLayer: current, + const handleChangeCopyLayerName = (event) => { + setCopyLayerName(event.target.value) } -} -const mapDispatchToProps = (dispatch, ownProps) => { - return { - toggleModal: () => { - ownProps.toggleModal() - }, - onChangeCopyName: (event) => { - dispatch(updateLayers({ copyLayerName: event.target.value })) - }, - onLayerCopied: (id) => { - dispatch(copyLayer(id)) - }, + const handleNameFocus = (event) => { + event.target.select() } -} - -class CopyLayer extends Component { - render() { - const namedInputRef = React.createRef() - const { - currentLayer, - copyLayerName, - onChangeCopyName, - onLayerCopied, - toggleModal, - showModal, - } = this.props - - return ( - namedInputRef.current.focus()} - > - - Copy {currentLayer.name} - - - - Name - - - - - - - - - - - + const handleCopyLayer = () => { + dispatch( + copyLayer({ + id: currentLayer.id, + name: copyLayerName, + }), ) + toggleModal() } - handleNameFocus(event) { - event.target.select() + const handleInitialFocus = () => { + namedInputRef.current.focus() } + + return ( + + + Copy {currentLayer.name} + + + + + Name + + + + + + + + + + + + ) } -export default connect(mapStateToProps, mapDispatchToProps)(CopyLayer) +export default CopyLayer diff --git a/src/features/layers/LayerEditor.js b/src/features/layers/LayerEditor.js index a20801b1..88eb6f13 100644 --- a/src/features/layers/LayerEditor.js +++ b/src/features/layers/LayerEditor.js @@ -1,5 +1,5 @@ -import React, { Component } from "react" -import { connect } from "react-redux" +import React from "react" +import { useDispatch, useSelector } from "react-redux" import { Button, Card, Row, Col, Accordion } from "react-bootstrap" import Select from "react-select" import { IconContext } from "react-icons" @@ -15,216 +15,73 @@ import Layer from "./Layer" import { getCurrentLayer } from "./selectors" import "./LayerEditor.scss" -const mapStateToProps = (state, ownProps) => { - return { - state: getCurrentLayer(state), +const LayerEditor = ({ id }) => { + const dispatch = useDispatch() + const state = useSelector(getCurrentLayer) + const layer = new Layer(state.type) + const model = layer.model + const layerOptions = layer.getOptions() + const modelOptions = model.getOptions() + const selectOptions = getModelSelectOptions() + const allowModelSelection = model.selectGroup !== "import" && !model.effect + const selectedOption = { + value: model.type, + label: model.label, } -} - -const mapDispatchToProps = (dispatch, ownProps) => { - const { id } = ownProps + const link = model.link + const linkText = model.linkText || "here" + const renderedLink = link ? ( +
+ See{" "} + + {linkText} + {" "} + for ideas. +
+ ) : undefined - return { - onChange: (attrs) => { - attrs.id = id - dispatch(updateLayer(attrs)) - }, - onChangeType: (selected) => { - dispatch(changeModelType({ id, type: selected.value })) - }, - onRestoreDefaults: (event) => { - dispatch(restoreDefaults(id)) - }, + const handleChangeType = (selected) => { + dispatch(changeModelType({ id, type: selected.value })) } -} -class LayerEditor extends Component { - render() { - const { state } = this.props - const layer = new Layer(state.type) - const model = layer.model - const layerOptions = layer.getOptions() - const modelOptions = model.getOptions() - const selectOptions = getModelSelectOptions() - const allowModelSelection = model.selectGroup !== "import" && !model.effect + const handleChange = (attrs) => { + attrs.id = id + dispatch(updateLayer(attrs)) + } - const selectedOption = { - value: model.type, - label: model.label, - } - const link = model.link - const linkText = model.linkText || "here" - const renderedModelOptions = Object.keys(modelOptions).map((key) => { - return ( -
- {this.getOptionComponent(model, modelOptions, key)} -
- ) - }) + const handleRestoreDefaults = () => { + dispatch(restoreDefaults(id)) + } - const renderedLink = link ? ( -
- See{" "} - - {linkText} - {" "} - for ideas. -
- ) : undefined - const renderedModelSelection = allowModelSelection && ( - - Type + const renderedModelSelection = allowModelSelection && ( + + Type - - + + + ) - return ( - - - - - - Layer - - - - - {this.getOptionComponent(model, layerOptions, "name")} - {model.canTransform(state) && ( - - Transform - - {model.canMove && ( - - - {this.getOptionComponent(model, layerOptions, "x")} - - - {this.getOptionComponent(model, layerOptions, "y")} - - - )} - {model.canChangeSize(state) && model.autosize && ( - - - {this.getOptionComponent( - model, - layerOptions, - "width", - )} - - - {this.getOptionComponent( - model, - layerOptions, - "height", - )} - - - )} - {model.canRotate(state) && ( - - -
-
- - - -
- {this.getOptionComponent( - model, - layerOptions, - "rotation", - false, - )} -
- -
- )} - -
- )} - {this.getOptionComponent(model, layerOptions, "reverse")} -
-
-
-
- - - - - - Shape - - - - - - {renderedModelSelection} - {renderedModelOptions} - {renderedLink} - - - - -
- ) - } - - getOptionComponent(model, options, key, label = true) { + const getOptionComponent = (model, options, key, label = true) => { const option = options[key] - const { state, onChange } = this.props const props = { options, - label, key, - onChange, + handleChange, optionKey: key, data: state, object: model, - comments: state.comments, + label, } switch (option.type) { @@ -240,6 +97,125 @@ class LayerEditor extends Component { return } } + + const renderedModelOptions = Object.keys(modelOptions).map((key) => ( +
+ {getOptionComponent(model, modelOptions, key)} +
+ )) + + return ( + + + + + + Layer + + + + + {getOptionComponent(model, layerOptions, "name")} + {model.canTransform(state) && ( + + Transform + + {model.canMove && ( + + + {getOptionComponent(model, layerOptions, "x")} + + + {getOptionComponent(model, layerOptions, "y")} + + + )} + {model.canChangeSize(state) && model.autosize && ( + + + {getOptionComponent(model, layerOptions, "width")} + + + {getOptionComponent(model, layerOptions, "height")} + + + )} + {model.canRotate(state) && ( + + +
+
+ + + +
+ {getOptionComponent( + model, + layerOptions, + "rotation", + false, + )} +
+ +
+ )} + +
+ )} + {getOptionComponent(model, layerOptions, "reverse")} + {getOptionComponent(model, layerOptions, "connectionMethod")} +
+
+
+
+ + + + + + Shape + + + + + + {renderedModelSelection} + {renderedModelOptions} + {renderedLink} + + + + +
+ ) } -export default connect(mapStateToProps, mapDispatchToProps)(LayerEditor) +export default LayerEditor diff --git a/src/features/layers/NewLayer.js b/src/features/layers/NewLayer.js index de2e3dba..c294603c 100644 --- a/src/features/layers/NewLayer.js +++ b/src/features/layers/NewLayer.js @@ -1,7 +1,7 @@ -import React, { Component } from "react" +import React, { useState } from "react" +import { useDispatch, useSelector } from "react-redux" import Select from "react-select" import { Button, Modal, Row, Col, Form } from "react-bootstrap" -import { connect } from "react-redux" import { getModelSelectOptions, getDefaultModel, @@ -19,119 +19,93 @@ const customStyles = { }), } -const mapStateToProps = (state, ownProps) => { - return { - selectOptions: getModelSelectOptions(), - showModal: ownProps.showModal, +const NewLayer = ({ toggleModal, showModal }) => { + const dispatch = useDispatch() + const selectOptions = useSelector(getModelSelectOptions) + const [type, setType] = useState(defaultModel.type) + const [name, setName] = useState(defaultModel.label) + const selectedShape = getModelFromType(type) + const selectedOption = { + value: selectedShape.id, + label: selectedShape.label, } -} - -const mapDispatchToProps = (dispatch, ownProps) => { - return { - onLayerAdded: (type, name) => { - const layer = new Layer(type) - const attrs = layer.getInitialState() - attrs.name = name - dispatch(addLayer(attrs)) - }, - toggleModal: () => { - ownProps.toggleModal() - }, + const handleNameFocus = (event) => { + event.target.select() } -} -class NewLayer extends Component { - constructor(props) { - super(props) - this.state = { - type: defaultModel.type, - name: defaultModel.label, - } - } + const handleChangeNewType = (selected) => { + const model = getModelFromType(selected.value) - render() { - const { toggleModal, showModal, selectOptions, onLayerAdded } = this.props - const selectedShape = getModelFromType(this.state.type) - const selectedOption = { - value: selectedShape.id, - label: selectedShape.label, - } + setType(selected.value) + setName(model.label.toLowerCase()) + } - return ( - - - Create new layer - + const handleChangeNewName = (event) => { + setName(event.target.value) + } - - - Type - - + + + + Name + + + + + - this.setState({ - type: selected.value, - name: model.label.toLowerCase(), - }) - } - onChangeNewName(event) { - this.setState({ - name: event.target.value, - }) - } + + + + + + ) } -export default connect(mapStateToProps, mapDispatchToProps)(NewLayer) +export default NewLayer diff --git a/src/features/layers/layersSlice.js b/src/features/layers/layersSlice.js index 9e6cf29d..17b4aac9 100644 --- a/src/features/layers/layersSlice.js +++ b/src/features/layers/layersSlice.js @@ -89,7 +89,6 @@ const layersSlice = createSlice({ newEffectType, newEffectName, newEffectNameOverride: false, - copyLayerName: null, byId: { [defaultLayerId]: layerState, }, @@ -116,10 +115,11 @@ const layersSlice = createSlice({ state.allIds = arrayMove(state.allIds, oldIndex, newIndex) }, copyLayer(state, action) { - const source = state.byId[action.payload] + const { id, name } = action.payload + const source = state.byId[id] const layer = createLayer(state, { ...source, - name: state.copyLayerName, + name, }) delete layer.effectIds diff --git a/src/features/layers/layersSlice.spec.js b/src/features/layers/layersSlice.spec.js index 04d9bac0..21e05c96 100644 --- a/src/features/layers/layersSlice.spec.js +++ b/src/features/layers/layersSlice.spec.js @@ -52,7 +52,6 @@ describe("layers reducer", () => { newEffectNameOverride: false, newEffectName: "mask", newEffectType: "mask", - copyLayerName: null, byId: {}, allIds: [], }) @@ -97,7 +96,6 @@ describe("layers reducer", () => { }, allIds: ["layer-1"], current: "layer-1", - copyLayerName: "foo", }, removeLayer("layer-1"), ), @@ -106,7 +104,6 @@ describe("layers reducer", () => { allIds: [], current: undefined, selected: undefined, - copyLayerName: "foo", }) }) @@ -155,7 +152,6 @@ describe("layers reducer", () => { }, allIds: ["layer-0"], current: "layer-0", - copyLayerName: "foo", }, copyLayer("layer-0"), ), @@ -173,7 +169,6 @@ describe("layers reducer", () => { allIds: ["layer-0", "layer-1"], current: "layer-1", selected: "layer-1", - copyLayerName: null, }) }) @@ -195,7 +190,6 @@ describe("layers reducer", () => { }, allIds: ["layer"], current: "layer", - copyLayerName: "foo", }, copyLayer("layer"), ), @@ -225,7 +219,6 @@ describe("layers reducer", () => { allIds: ["layer", "layer-1"], current: "layer-1", selected: "layer-1", - copyLayerName: null, }) }) }) diff --git a/src/features/machine/selectors.js b/src/features/machine/selectors.js index 7969758f..7153c5d6 100644 --- a/src/features/machine/selectors.js +++ b/src/features/machine/selectors.js @@ -3,8 +3,8 @@ import { createSelector } from "reselect" import Color from "color" import { getMachineState, - getState, getPreviewState, + getState, } from "@/features/store/selectors" import { createCachedSelector } from "re-reselect" import { @@ -138,7 +138,11 @@ export const getPreviewVertices = createCachedSelector( )((state, id) => id) // returns a flattened array of all visible computed vertices and connectors (across layers) -export const getAllComputedVertices = createSelector([getState], (state) => { +export const getAllComputedVertices = createSelector(getState, (state) => { + if (!state.fonts.loaded) { + return [] + } // wait for fonts + log("getAllComputedVertices") const visibleLayerIds = getVisibleNonEffectIds(state) @@ -173,7 +177,7 @@ export const getConnectingVertices = createCachedSelector( const end = endVertices[0] if (startLayer.connectionMethod === "along perimeter") { - const machineInstance = getMachineInstance([], state.machine) + const machineInstance = getMachineInstance([], state.main.machine) const startPerimeter = machineInstance.nearestPerimeterVertex(start) const endPerimeter = machineInstance.nearestPerimeterVertex(end) const perimeterConnection = machineInstance.tracePerimeter( diff --git a/src/features/preview/Preview.js b/src/features/preview/Preview.js index ec096a61..a52a5ba6 100644 --- a/src/features/preview/Preview.js +++ b/src/features/preview/Preview.js @@ -1,138 +1,105 @@ -import React, { Component } from "react" -import { connect } from "react-redux" +import React, { useEffect, useRef } from "react" +import { useSelector, useDispatch } from "react-redux" import Slider from "rc-slider" import "rc-slider/assets/index.css" import Downloader from "@/features/exporter/Downloader" import { getFontsState } from "@/features/store/selectors" import { getCurrentLayer } from "@/features/layers/selectors" -import { getLayersState, getPreviewState } from "@/features/store/selectors" +import { getPreviewState } from "@/features/store/selectors" import { updateLayer } from "@/features/layers/layersSlice" import { getVerticesStats } from "@/features/machine/selectors" +import { getModelFromType } from "@/config/models" import "./Preview.scss" import { updatePreview } from "./previewSlice" import PreviewWindow from "./PreviewWindow" -const mapStateToProps = (state, ownProps) => { - const fonts = getFontsState(state) - if (!fonts.loaded) { - return {} - } +const Preview = () => { + const dispatch = useDispatch() + const fonts = useSelector(getFontsState) + const currentLayer = useSelector(getCurrentLayer) + const sliderValue = useSelector(getPreviewState).sliderValue + const verticesStats = useSelector(getVerticesStats) + const previewElement = useRef(null) + const model = getModelFromType(currentLayer.type) - const preview = getPreviewState(state) - const current = getCurrentLayer(state) - const layers = getLayersState(state) + useEffect(() => { + if (fonts.loaded) { + previewElement.current.focus() + } + }, [fonts.loaded]) - return { - currentLayer: current, - currentLayerSelected: layers.selected === current.id, - sliderValue: preview.sliderValue, - verticesStats: getVerticesStats(state), + const handleSliderChange = (value) => { + dispatch(updatePreview({ sliderValue: value })) } -} -const mapDispatchToProps = (dispatch, ownProps) => { - return { - onSlider: (value) => { - dispatch(updatePreview({ sliderValue: value })) - }, - onLayerChange: (attrs) => { - dispatch(updateLayer(attrs)) - }, - onKeyDown: (event, currentLayer) => { + const handleKeyDown = (event) => { + if (model.canMove) { let attrs = { id: currentLayer.id } + const delta = event.shiftKey ? 1 : 5 - if (currentLayer.canMove) { - if ( - ["ArrowDown", "ArrowUp", "ArrowLeft", "ArrowRight"].includes( - event.key, - ) - ) { - const delta = event.shiftKey ? 1 : 5 - - if (event.key === "ArrowDown") { - attrs.y = currentLayer.y - delta - } else if (event.key === "ArrowUp") { - attrs.y = currentLayer.y + delta - } else if (event.key === "ArrowLeft") { - attrs.x = currentLayer.x - delta - } else if (event.key === "ArrowRight") { - attrs.x = currentLayer.x + delta - } - - dispatch(updateLayer(attrs)) - } + switch (event.key) { + case "ArrowDown": + attrs.y = currentLayer.y - delta + break + case "ArrowUp": + attrs.y = currentLayer.y + delta + break + case "ArrowLeft": + attrs.x = currentLayer.x - delta + break + case "ArrowRight": + attrs.x = currentLayer.x + delta + break + default: + return } - }, - } -} -class Preview extends Component { - componentDidMount() { - if (this.props.currentLayer) { - // ensures that arrow keys always work - this.el.focus() + dispatch(updateLayer(attrs)) } } - render() { - const { - currentLayer, - currentLayerSelected, - sliderValue, - verticesStats, - onSlider, - onKeyDown, - } = this.props + if (!fonts.loaded) { + return
+ } - if (currentLayer) { - return ( + return ( +
+
-
-
{ - this.el = el - }} - tabIndex={0} - onKeyDown={(e) => { - if (currentLayerSelected) { - onKeyDown(e, currentLayer) - } - }} - > - -
+ +
-
-
-
- Points: {verticesStats.numPoints}, Distance:{" "} - {verticesStats.distance} -
+
+
+
+ Points: {verticesStats.numPoints}, Distance:{" "} + {verticesStats.distance} +
-
- -
-
- +
+
+
- ) - } else { - return
- } - } +
+
+ ) } -export default connect(mapStateToProps, mapDispatchToProps)(Preview) +export default Preview diff --git a/src/features/preview/PreviewLayer.js b/src/features/preview/PreviewLayer.js index 958d9dd9..2883c8bb 100644 --- a/src/features/preview/PreviewLayer.js +++ b/src/features/preview/PreviewLayer.js @@ -1,11 +1,10 @@ -import React from "react" -import { useSelector, useDispatch, shallowEqual } from "react-redux" +import React, { useEffect } from "react" +import { useSelector, useDispatch } from "react-redux" import { Shape, Transformer } from "react-konva" import { getPreviewVertices, getSliderColors, getVertexOffsets, - getAllComputedVertices, getSliderBounds, } from "../machine/selectors" import { updateLayer } from "@/features/layers/layersSlice" @@ -20,55 +19,27 @@ import { import { roundP } from "@/common/util" import PreviewHelper from "./PreviewHelper" -// Renders the shapes in the preview window and allows the user to interact with the shape. const PreviewLayer = (ownProps) => { - const mapStateToProps = (state) => { - // if a layer matching this shape's id does not exist, we have a zombie - // child. It has to do with a child (preview shape) subscribing to the store - // before its parent (preview window), and trying to render first after a - // layer is removed. This is a tangled, but well-known problem with React-Redux - // hooks, and the solution for now is to render the current layer instead. - // https://react-redux.js.org/api/hooks#stale-props-and-zombie-children - // It's quite likely there is a more elegant/proper way around this. - const layers = getLayersState(state) - const layer = getLayer(state, ownProps.id) || getCurrentLayer(state) - const index = getLayerIndex(state, layer.id) - const numLayers = getNumVisibleLayers(state) - const preview = getPreviewState(state) - // const test = getLayers(state, ['layer-1', 'layer-2']) - - return { - layer, - start: index === 0, - end: index === numLayers - 1, - currentLayer: getCurrentLayer(state), - vertices: getPreviewVertices(state, layer.id, "1"), - allVertices: getAllComputedVertices(state), - selected: layers.selected, - sliderValue: preview.sliderValue, - colors: getSliderColors(state), - offsets: getVertexOffsets(state), - offsetId: layer.id, - bounds: getSliderBounds(state), - markCoordinates: false, // debug feature: set to true to see coordinates while drawing - } - } + const dispatch = useDispatch() + const layers = useSelector(getLayersState) + const currentLayer = useSelector(getCurrentLayer) + const layer = + useSelector((state) => getLayer(state, ownProps.id)) || currentLayer + const index = useSelector((state) => getLayerIndex(state, layer.id)) + const numLayers = useSelector(getNumVisibleLayers) + const preview = useSelector(getPreviewState) + const vertices = useSelector((state) => + getPreviewVertices(state, layer.id, "1"), + ) + const colors = useSelector(getSliderColors) + const offsets = useSelector(getVertexOffsets) + const bounds = useSelector(getSliderBounds) - const props = useSelector(mapStateToProps, shallowEqual) - const { - layer, - selected, - sliderValue, - vertices, - offsets, - start, - end, - currentLayer, - colors, - bounds, - } = props + const selected = layers.selected + const sliderValue = preview.sliderValue const model = getModelFromType(layer.type) - const dispatch = useDispatch() + const start = index === 0 + const end = index === numLayers - 1 const width = layer.width const height = layer.height const selectedColor = "yellow" @@ -78,10 +49,9 @@ const PreviewLayer = (ownProps) => { const isSelected = selected === ownProps.id const isSliding = sliderValue !== 0 const isCurrent = layer.id === currentLayer.id - const helper = new PreviewHelper(props) + const helper = new PreviewHelper({ layer, vertices, offsets, bounds, colors }) - // draws a colored path when user is using slider - function drawLayerVertices(context, bounds) { + const drawLayerVertices = (context, bounds) => { const { end } = bounds let oldColor = null let currentColor = isSelected ? selectedColor : unselectedColor @@ -114,7 +84,7 @@ const PreviewLayer = (ownProps) => { context.stroke() } - function drawStartAndEndPoints(context) { + const drawStartAndEndPoints = (context) => { const start = vertices[0] const end = vertices[vertices.length - 1] @@ -133,7 +103,7 @@ const PreviewLayer = (ownProps) => { // TODO: fix or remove // draws the line representing the track the path follows - // function drawTrackVertices(context) { + // const drawTrackVertices = (context) => { // context.beginPath() // context.lineWidth = 4.0 // context.strokeStyle = "green" @@ -144,14 +114,12 @@ const PreviewLayer = (ownProps) => { // context.stroke() // } - // used by Konva to draw our custom shape - function sceneFunc(context, shape) { + const sceneFunc = (context, shape) => { if (vertices && vertices.length > 0) { // TODO: fix or remove // if (props.trackVertices && props.trackVertices.length > 0) { // drawTrackVertices(context) // } - drawLayerVertices(context, bounds) if (start || end || isSelected) { @@ -163,45 +131,69 @@ const PreviewLayer = (ownProps) => { context.fillStrokeShape(shape) } - // used by Konva to mark boundaries of shape function hitFunc(context) { context.fillStrokeShape(this) } - function onChange(attrs) { + const handleChange = (attrs) => { attrs.id = layer.id dispatch(updateLayer(attrs)) } - function onSelect() { + const handleSelect = () => { // deselection is currently disabled // dispatch(setSelectedLayer(selected == null ? currentLayer.id : null)) } - function onDragStart() { - console.log(currentLayer.id + " " + layer.id) - + const handleDragStart = () => { if (isCurrent) { - onChange({ dragging: true }) + handleChange({ dragging: true }) } } - const shapeRef = React.createRef() - const trRef = React.createRef() + const handleDragEnd = (e) => { + handleChange({ + dragging: false, + x: roundP(e.target.x(), 0), + y: roundP(-e.target.y(), 0), + }) + } + + const handleTransformStart = (e) => { + handleChange({ dragging: true }) + } + + const handleTransformEnd = (e) => { + const node = shapeRef.current + const scaleX = node.scaleX() + const scaleY = node.scaleY() - React.useEffect(() => { + node.scaleX(1) + node.scaleY(1) + + handleChange({ + dragging: false, + width: roundP(Math.max(5, layer.width * scaleX), 0), + height: roundP(Math.max(5, layer.height * scaleY), 0), + rotation: roundP(node.rotation(), 0), + }) + } + + const shapeRef = React.useRef() + const trRef = React.useRef() + + useEffect(() => { if (layer.visible && isSelected && model.canChangeSize(layer)) { - // we need to attach transformer manually trRef.current.nodes([shapeRef.current]) trRef.current.getLayer().batchDraw() } }, [isSelected, layer, model.canMove, shapeRef, trRef]) return ( - + <> {layer.visible && ( { offsetX={width / 2} x={layer.x || 0} y={-layer.y || 0} - onClick={onSelect} - onTap={onSelect} + onClick={handleSelect} + onTap={handleSelect} ref={shapeRef} strokeWidth={1} rotation={layer.rotation || 0} sceneFunc={sceneFunc} hitFunc={hitFunc} - onDragStart={onDragStart} - onDragEnd={(e) => { - onChange({ - dragging: false, - x: roundP(e.target.x(), 0), - y: roundP(-e.target.y(), 0), - }) - }} - onTransformStart={(e) => { - onChange({ dragging: true }) - }} - onTransformEnd={(e) => { - const node = shapeRef.current - const scaleX = node.scaleX() - const scaleY = node.scaleY() - - // we will reset it back - node.scaleX(1) - node.scaleY(1) - - onChange({ - dragging: false, - width: roundP(Math.max(5, layer.width * scaleX), 0), - height: roundP(Math.max(5, layer.height * scaleY), 0), - rotation: roundP(node.rotation(), 0), - }) - }} + onDragStart={handleDragStart} + onDragEnd={handleDragEnd} + onTransformStart={handleTransformStart} + onTransformEnd={handleTransformEnd} /> )} {layer.visible && isSelected && model.canChangeSize(layer) && ( @@ -259,7 +228,7 @@ const PreviewLayer = (ownProps) => { } /> )} - + ) } diff --git a/src/features/preview/PreviewWindow.js b/src/features/preview/PreviewWindow.js index d0897f47..7cecec9e 100644 --- a/src/features/preview/PreviewWindow.js +++ b/src/features/preview/PreviewWindow.js @@ -1,13 +1,10 @@ -import React, { Component } from "react" -import { connect, ReactReduxContext, Provider } from "react-redux" +import React, { useEffect, useRef } from "react" +import { useSelector, useDispatch, useStore } from "react-redux" +import { Provider } from "react-redux" import { Stage, Layer, Circle, Rect } from "react-konva" import throttle from "lodash/throttle" import { updateLayer } from "@/features/layers/layersSlice" -import { - getMachineState, - getLayersState, - getPreviewState, -} from "@/features/store/selectors" +import { getMachineState, getPreviewState } from "@/features/store/selectors" import { getCurrentLayer, getKonvaLayerIds, @@ -17,206 +14,160 @@ import { import { roundP } from "@/common/util" import PreviewLayer from "./PreviewLayer" import PreviewConnector from "./PreviewConnector" -import { setPreviewSize, updatePreview } from "./previewSlice" - -const mapStateToProps = (state, ownProps) => { - const layers = getLayersState(state) - const preview = getPreviewState(state) - const machine = getMachineState(state) - - return { - layers, - currentLayer: getCurrentLayer(state), - konvaIds: getKonvaLayerIds(state), - layerIds: getVisibleNonEffectIds(state), - use_rect: machine.rectangular, - dragging: isDragging(state), - minX: machine.minX, - maxX: machine.maxX, - minY: machine.minY, - maxY: machine.maxY, - maxRadius: machine.maxRadius, - canvasWidth: preview.canvasWidth, - canvasHeight: preview.canvasHeight, - } -} - -const mapDispatchToProps = (dispatch, ownProps) => { - return { - onResize: (size) => { - dispatch(setPreviewSize(size)) - }, - onChange: (attrs) => { - dispatch(updatePreview(attrs)) - }, - onLayerChange: (attrs) => { - dispatch(updateLayer(attrs)) - }, - } -} - -// Contains the preview window, and any parameters for the machine. -class PreviewWindow extends Component { - componentDidMount() { +import { setPreviewSize } from "./previewSlice" + +const PreviewWindow = () => { + const dispatch = useDispatch() + const store = useStore() + const previewElement = useRef(null) + const { use_rect, minX, minY, maxX, maxY, maxRadius } = useSelector((state) => + getMachineState(state), + ) + const { canvasWidth, canvasHeight } = useSelector((state) => + getPreviewState(state), + ) + const currentLayer = useSelector(getCurrentLayer) + const konvaIds = useSelector(getKonvaLayerIds) + const layerIds = useSelector(getVisibleNonEffectIds) + const dragging = useSelector(isDragging) + + useEffect(() => { const wrapper = document.getElementById("preview-wrapper") - - this.throttledResize = throttle(this.resize, 200, { + const resize = () => { + const width = parseInt( + getComputedStyle(wrapper).getPropertyValue("width"), + ) + const height = parseInt( + getComputedStyle(wrapper).getPropertyValue("height"), + ) + + if (canvasWidth !== width || canvasHeight !== height) { + dispatch(setPreviewSize({ width, height })) + } + } + const throttledResize = throttle(resize, 200, { trailing: true, - }).bind(this) - window.addEventListener( - "resize", - () => { - this.throttledResize(wrapper) - }, - false, - ) - } + }) - resize(wrapper) { - const width = parseInt(getComputedStyle(wrapper).getPropertyValue("width")) - const height = parseInt( - getComputedStyle(wrapper).getPropertyValue("height"), - ) - - if ( - this.props.canvasWidth !== width || - this.props.canvasHeight !== height - ) { - this.props.onResize({ width, height }) - } - } + window.addEventListener("resize", throttledResize, false) - render() { - const { minX, minY, maxX, maxY } = this.props - const radius = this.props.maxRadius - const scale = this.relativeScale(this.props) - const width = this.props.use_rect ? maxX - minX : radius * 2 - const height = this.props.use_rect ? maxY - minY : radius * 2 - - // define Konva clip functions that will let us clip vertices not bound by - // machine limits when dragging, and produce a visually seamless experience. - const clipCircle = (ctx) => { - ctx.arc(0, 0, radius, 0, Math.PI * 2, false) + return () => { + window.removeEventListener("resize", throttledResize, false) } - const clipRect = (ctx) => { - ctx.rect(-width / 2, -height / 2, width, height) - } - const clipFunc = this.props.dragging - ? this.props.use_rect - ? clipRect - : clipCircle - : null - - return ( - // the consumer wrapper is needed to pass the store down to our shape - // which is not our usual React Component - - {({ store }) => ( - { - e.evt.preventDefault() - if (Math.abs(e.evt.deltaY) > 0) { - this.props.onLayerChange({ - width: this.scaleByWheel( - this.props.currentLayer.width, - e.evt.deltaY, - ), - height: this.scaleByWheel( - this.props.currentLayer.height, - e.evt.deltaY, - ), - id: this.props.currentLayer.id, - }) - } - }} - > - - - {!this.props.use_rect && ( - - )} - {this.props.use_rect && ( - - )} - {this.props.konvaIds - .map((id, i) => { - const idx = this.props.layerIds.findIndex( - (layerId) => layerId === id, - ) - const nextId = - idx !== -1 && idx < this.props.layerIds.length - 1 - ? this.props.layerIds[idx + 1] - : null - return [ - nextId && ( - - ), - , - ].filter((e) => e !== null) - }) - .flat()} - - - - )} - - ) - } + }, [dispatch, canvasWidth, canvasHeight]) - relativeScale(props) { + const relativeScale = () => { let width, height - if (props.use_rect) { - width = props.maxX - props.minX - height = props.maxY - props.minY + if (use_rect) { + width = maxX - minX + height = maxY - minY } else { - width = height = props.maxRadius * 2.0 + width = height = maxRadius * 2.0 } - return Math.min(props.canvasWidth / width, props.canvasHeight / height) + return Math.min(canvasWidth / width, canvasHeight / height) } - scaleByWheel(size, deltaY) { + const scaleByWheel = (size, deltaY) => { const sign = Math.sign(deltaY) const scale = 1 + (Math.log(Math.abs(deltaY)) / 30) * sign let newSize = Math.max(roundP(size * scale, 0), 1) if (newSize === size) { - // If the log scaled value isn't big enough to move the scale. + // if the log scaled value isn't big enough to move the scale newSize = Math.max(sign + size, 1) } return newSize } + + const clipCircle = (ctx) => { + ctx.arc(0, 0, maxRadius, 0, Math.PI * 2, false) + } + + const clipRect = (ctx) => { + ctx.rect(-width / 2, -height / 2, width, height) + } + + const handleWheel = (e) => { + e.evt.preventDefault() + + if (Math.abs(e.evt.deltaY) > 0) { + dispatch( + updateLayer({ + width: scaleByWheel(currentLayer.width, e.evt.deltaY), + height: scaleByWheel(currentLayer.height, e.evt.deltaY), + id: currentLayer.id, + }), + ) + } + } + + const clipFunc = dragging ? (use_rect ? clipRect : clipCircle) : null + const width = use_rect ? maxX - minX : maxRadius * 2 + const height = use_rect ? maxY - minY : maxRadius * 2 + const scale = relativeScale() + + return ( + + + + {!use_rect && ( + + )} + {use_rect && ( + + )} + {konvaIds.map((id, i) => { + const idx = layerIds.findIndex((layerId) => layerId === id) + const nextId = + idx !== -1 && idx < layerIds.length - 1 ? layerIds[idx + 1] : null + return [ + nextId && ( + + ), + , + ].filter((e) => e !== null) + })} + + + + ) } -export default connect(mapStateToProps, mapDispatchToProps)(PreviewWindow) + +export default PreviewWindow From d4d600507977fb039635feb43222e3dbff68d4f7 Mon Sep 17 00:00:00 2001 From: Bob Carmichael Date: Fri, 28 Jul 2023 08:33:10 -0400 Subject: [PATCH 027/126] better unique ids, which also fixes persisted store load --- package-lock.json | 14 ++++++++++++++ package.json | 1 + src/features/layers/layersSlice.js | 8 +++----- 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 21ca0777..fb96c673 100644 --- a/package-lock.json +++ b/package-lock.json @@ -46,6 +46,7 @@ "reselect": "^4.1.8", "sass": "^1.49.11", "seedrandom": "^3.0.5", + "uuid": "^9.0.0", "victor": "^1.1.0" }, "devDependencies": { @@ -11501,6 +11502,14 @@ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" }, + "node_modules/uuid": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", + "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/v8-compile-cache": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", @@ -20169,6 +20178,11 @@ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" }, + "uuid": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", + "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==" + }, "v8-compile-cache": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", diff --git a/package.json b/package.json index f70c005f..0c8c3748 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "reselect": "^4.1.8", "sass": "^1.49.11", "seedrandom": "^3.0.5", + "uuid": "^9.0.0", "victor": "^1.1.0" }, "devDependencies": { diff --git a/src/features/layers/layersSlice.js b/src/features/layers/layersSlice.js index 17b4aac9..12521ace 100644 --- a/src/features/layers/layersSlice.js +++ b/src/features/layers/layersSlice.js @@ -1,5 +1,5 @@ import { createSlice } from "@reduxjs/toolkit" -import uniqueId from "lodash/uniqueId" +import { v4 as uuidv4 } from "uuid" import arrayMove from "array-move" import { getModelFromType, getDefaultModelType } from "@/config/models" import Layer from "./Layer" @@ -13,7 +13,7 @@ function createLayer(state, attrs) { delete attrs.restore const layer = { ...attrs, - id: (restore && attrs.id) || uniqueId("layer-"), + id: (restore && attrs.id) || uuidv4(), name: attrs.name, } @@ -75,7 +75,7 @@ function setCurrentId(state, id) { } const defaultLayer = new Layer(getDefaultModelType()) -const defaultLayerId = uniqueId("layer-") +const defaultLayerId = uuidv4() const layerState = { id: defaultLayerId, ...defaultLayer.getInitialState(), @@ -132,7 +132,6 @@ const layersSlice = createSlice({ const index = state.allIds.findIndex((id) => id === state.current) + 1 state.allIds.splice(index, 0, layer.id) setCurrentId(state, layer.id) - state.copyLayerName = null }, removeLayer(state, action) { const id = action.payload @@ -179,7 +178,6 @@ const layersSlice = createSlice({ if (current) { setCurrentId(state, current.id) - state.copyLayerName = current.name } }, setSelectedLayer(state, action) { From 83da0ec1095d33cd8ae2f2138e0c4e6c2f20f1fe Mon Sep 17 00:00:00 2001 From: Bob Carmichael Date: Fri, 28 Jul 2023 16:12:10 -0400 Subject: [PATCH 028/126] rename/move selectors to standard locations --- src/common/selectors.js | 11 +++- src/features/app/InputTabs.js | 4 +- src/features/app/appSelectors.js | 4 ++ src/features/exporter/Downloader.js | 7 +-- .../{selectors.js => exporterSelectors.js} | 14 ++--- src/features/layers/CopyLayer.js | 2 +- src/features/layers/ImportLayer.js | 2 +- src/features/layers/LayerEditor.js | 2 +- src/features/layers/Playlist.js | 2 +- .../{selectors.js => layerSelectors.js} | 14 +++-- src/features/machine/MachineSettings.js | 2 +- src/features/machine/PolarSettings.js | 2 +- src/features/machine/RectSettings.js | 2 +- .../{selectors.js => machineSelectors.js} | 15 +++--- src/features/preview/Preview.js | 8 +-- src/features/preview/PreviewConnector.js | 6 +-- src/features/preview/PreviewLayer.js | 7 +-- src/features/preview/PreviewWindow.js | 5 +- src/features/preview/previewSelectors.js | 7 +++ src/features/store/selectors.js | 51 ------------------- 20 files changed, 73 insertions(+), 94 deletions(-) create mode 100644 src/features/app/appSelectors.js rename src/features/exporter/{selectors.js => exporterSelectors.js} (59%) rename src/features/layers/{selectors.js => layerSelectors.js} (94%) rename src/features/machine/{selectors.js => machineSelectors.js} (97%) create mode 100644 src/features/preview/previewSelectors.js delete mode 100644 src/features/store/selectors.js diff --git a/src/common/selectors.js b/src/common/selectors.js index 9d874cc2..5d47619f 100644 --- a/src/common/selectors.js +++ b/src/common/selectors.js @@ -1,7 +1,16 @@ -import { defaultMemoize } from 'reselect' +import { createSelectorCreator, defaultMemoize } from "reselect" +import isEqual from "lodash" // from https://github.com/reduxjs/reselect/issues/441 export const memoizeArrayProducingFn = (fn) => { const memArray = defaultMemoize((...array) => array) return (...args) => memArray(...fn(...args)) } + +// does a deep equality check instead of checking immutability; used in cases +// where a selector depends on another selector that returns a new object each time, +// e.g., getLayerIndex +export const createDeepEqualSelector = createSelectorCreator( + defaultMemoize, + isEqual, +) diff --git a/src/features/app/InputTabs.js b/src/features/app/InputTabs.js index 1c4ff4db..8fc2516f 100644 --- a/src/features/app/InputTabs.js +++ b/src/features/app/InputTabs.js @@ -5,8 +5,8 @@ import { connect } from "react-redux" import MachineSettings from "@/features/machine/MachineSettings" import LayerEditor from "@/features/layers/LayerEditor" import Playlist from "@/features/layers/Playlist" -import { getCurrentLayer } from "@/features/layers/selectors" -import { getFontsState } from "@/features/store/selectors" +import { getCurrentLayer } from "@/features/layers/layerSelectors" +import { getFontsState } from "@/features/app/appSelectors" import { loadFont, supportedFonts } from "@/features/fonts/fontsSlice" import { chooseInput } from "./appSlice" import Footer from "./Footer" diff --git a/src/features/app/appSelectors.js b/src/features/app/appSelectors.js new file mode 100644 index 00000000..43e645b4 --- /dev/null +++ b/src/features/app/appSelectors.js @@ -0,0 +1,4 @@ +export const getState = (state) => state +export const getMainState = (state) => state.main +export const getAppState = (state) => state.main.app +export const getFontsState = (state) => state.fonts diff --git a/src/features/exporter/Downloader.js b/src/features/exporter/Downloader.js index adf1b7ed..6406cd45 100644 --- a/src/features/exporter/Downloader.js +++ b/src/features/exporter/Downloader.js @@ -6,9 +6,10 @@ import DropdownOption from "@/components/DropdownOption" import InputOption from "@/components/InputOption" import CheckboxOption from "@/components/CheckboxOption" import { updateExporter } from "./exporterSlice" -import { getAllComputedVertices } from "@/features/machine/selectors" -import { getLayersState, getMainState } from "@/features/store/selectors" -import { getComments } from "./selectors" +import { getAllComputedVertices } from "@/features/machine/machineSelectors" +import { getMainState } from "@/features/app/appSelectors" +import { getLayersState } from "@/features/layers/layerSelectors" +import { getComments } from "./exporterSelectors" import GCodeExporter from "./GCodeExporter" import ScaraGCodeExporter from "./ScaraGCodeExporter" import SvgExporter from "./SvgExporter" diff --git a/src/features/exporter/selectors.js b/src/features/exporter/exporterSelectors.js similarity index 59% rename from src/features/exporter/selectors.js rename to src/features/exporter/exporterSelectors.js index efd81bba..b2c03a6f 100644 --- a/src/features/exporter/selectors.js +++ b/src/features/exporter/exporterSelectors.js @@ -1,12 +1,14 @@ import { createSelector } from "reselect" -import { getAllLayers } from "@/features/layers/selectors" +import { getAllLayers } from "@/features/layers/layerSelectors" import CommentExporter from "./CommentExporter" import { log } from "@/common/debugging" -import { - getAppState, - getExporterState, - getMachineState, -} from "@/features/store/selectors" +import { getAppState, getMainState } from "@/features/app/appSelectors" +import { getMachineState } from "@/features/machine/machineSelectors" + +export const getExporterState = createSelector( + getMainState, + (main) => main.exporter, +) export const getComments = createSelector( [getAppState, getAllLayers, getExporterState, getMachineState], diff --git a/src/features/layers/CopyLayer.js b/src/features/layers/CopyLayer.js index bf32ebf6..be3ebb3e 100644 --- a/src/features/layers/CopyLayer.js +++ b/src/features/layers/CopyLayer.js @@ -2,7 +2,7 @@ import React, { useRef, useState } from "react" import { Button, Modal, Row, Col, Form } from "react-bootstrap" import { useDispatch, useSelector } from "react-redux" import { copyLayer } from "./layersSlice" -import { getCurrentLayer } from "./selectors" +import { getCurrentLayer } from "./layerSelectors" const CopyLayer = ({ toggleModal, showModal }) => { const dispatch = useDispatch() diff --git a/src/features/layers/ImportLayer.js b/src/features/layers/ImportLayer.js index 5dc77751..2979bfd1 100644 --- a/src/features/layers/ImportLayer.js +++ b/src/features/layers/ImportLayer.js @@ -2,7 +2,7 @@ import React, { Component } from "react" import { Button, Modal, Form, Accordion, Card } from "react-bootstrap" import { connect } from "react-redux" import ReactGA from "react-ga" -import { getMachineState } from "@/features/store/selectors" +import { getMachineState } from "@/features/machine/machineSelectors" import ThetaRhoImporter from "@/features/importer/ThetaRhoImporter" import GCodeImporter from "@/features/importer/GCodeImporter" import { addLayer } from "./layersSlice" diff --git a/src/features/layers/LayerEditor.js b/src/features/layers/LayerEditor.js index 88eb6f13..c62ecd1f 100644 --- a/src/features/layers/LayerEditor.js +++ b/src/features/layers/LayerEditor.js @@ -12,7 +12,7 @@ import ToggleButtonOption from "@/components/ToggleButtonOption" import { getModelSelectOptions } from "@/config/models" import { updateLayer, changeModelType, restoreDefaults } from "./layersSlice" import Layer from "./Layer" -import { getCurrentLayer } from "./selectors" +import { getCurrentLayer } from "./layerSelectors" import "./LayerEditor.scss" const LayerEditor = ({ id }) => { diff --git a/src/features/layers/Playlist.js b/src/features/layers/Playlist.js index 91d42858..ab990e21 100644 --- a/src/features/layers/Playlist.js +++ b/src/features/layers/Playlist.js @@ -8,7 +8,7 @@ import { getCurrentLayer, getNumLayers, getAllLayers, -} from "@/features/layers/selectors" +} from "@/features/layers/layerSelectors" import { setCurrentLayer, addLayer, diff --git a/src/features/layers/selectors.js b/src/features/layers/layerSelectors.js similarity index 94% rename from src/features/layers/selectors.js rename to src/features/layers/layerSelectors.js index ff3f17de..4c15242b 100644 --- a/src/features/layers/selectors.js +++ b/src/features/layers/layerSelectors.js @@ -1,12 +1,16 @@ -import { - getLayersState, - createDeepEqualSelector, -} from "@/features/store/selectors" +import { getMainState } from "@/features/app/appSelectors" import { createSelector } from "reselect" import { createCachedSelector } from "re-reselect" -import { memoizeArrayProducingFn } from "@/common/selectors" +import { + memoizeArrayProducingFn, + createDeepEqualSelector, +} from "@/common/selectors" import { log } from "@/common/debugging" +export const getLayersState = createSelector( + getMainState, + (main) => main.layers, +) const getCurrentLayerId = createSelector( getLayersState, (layers) => layers.current, diff --git a/src/features/machine/MachineSettings.js b/src/features/machine/MachineSettings.js index 31c6ff69..fc5f07e6 100644 --- a/src/features/machine/MachineSettings.js +++ b/src/features/machine/MachineSettings.js @@ -3,7 +3,7 @@ import { connect } from "react-redux" import { Accordion } from "react-bootstrap" import RectSettings from "./RectSettings" import PolarSettings from "./PolarSettings" -import { getMachineState } from "@/features/store/selectors" +import { getMachineState } from "@/features/machine/machineSelectors" const mapStateToProps = (state, ownProps) => { const machine = getMachineState(state) diff --git a/src/features/machine/PolarSettings.js b/src/features/machine/PolarSettings.js index 02cfd705..ce65b871 100644 --- a/src/features/machine/PolarSettings.js +++ b/src/features/machine/PolarSettings.js @@ -11,7 +11,7 @@ import { } from "react-bootstrap" import InputOption from "@/components/InputOption" import CheckboxOption from "@/components/CheckboxOption" -import { getMachineState } from "@/features/store/selectors" +import { getMachineState } from "@/features/machine/machineSelectors" import { machineOptions } from "./options" import { toggleMachinePolarExpanded, diff --git a/src/features/machine/RectSettings.js b/src/features/machine/RectSettings.js index a297e44c..94475f00 100644 --- a/src/features/machine/RectSettings.js +++ b/src/features/machine/RectSettings.js @@ -11,7 +11,7 @@ import { } from "react-bootstrap" import InputOption from "@/components/InputOption" import CheckboxOption from "@/components/CheckboxOption" -import { getMachineState } from "@/features/store/selectors" +import { getMachineState } from "@/features/machine/machineSelectors" import { updateMachine, toggleMinimizeMoves, diff --git a/src/features/machine/selectors.js b/src/features/machine/machineSelectors.js similarity index 97% rename from src/features/machine/selectors.js rename to src/features/machine/machineSelectors.js index 7153c5d6..976d0ac4 100644 --- a/src/features/machine/selectors.js +++ b/src/features/machine/machineSelectors.js @@ -1,11 +1,8 @@ import LRUCache from "lru-cache" import { createSelector } from "reselect" import Color from "color" -import { - getMachineState, - getPreviewState, - getState, -} from "@/features/store/selectors" +import { getState, getMainState } from "@/features/app/appSelectors" +import { getPreviewState } from "@/features/preview/previewSelectors" import { createCachedSelector } from "re-reselect" import { getLayer, @@ -13,7 +10,7 @@ import { getVisibleNonEffectIds, getLayerEffects, getNonEffectLayerIndex, -} from "@/features/layers/selectors" +} from "@/features/layers/layerSelectors" import Layer from "@/features/layers/Layer" import { getModelFromType } from "@/config/models" import { rotate, offset } from "@/common/geometry" @@ -31,9 +28,13 @@ const getCacheKey = (state) => { return JSON.stringify(state) } +export const getMachineState = createSelector( + getMainState, + (main) => main.machine, +) + // by returning null for shapes which don't use machine settings, this selector will ensure // transformed vertices are not redrawn when machine settings change - export const getLayerMachine = createCachedSelector( getLayer, getMachineState, diff --git a/src/features/preview/Preview.js b/src/features/preview/Preview.js index a52a5ba6..45492d2a 100644 --- a/src/features/preview/Preview.js +++ b/src/features/preview/Preview.js @@ -3,11 +3,11 @@ import { useSelector, useDispatch } from "react-redux" import Slider from "rc-slider" import "rc-slider/assets/index.css" import Downloader from "@/features/exporter/Downloader" -import { getFontsState } from "@/features/store/selectors" -import { getCurrentLayer } from "@/features/layers/selectors" -import { getPreviewState } from "@/features/store/selectors" +import { getFontsState } from "@/features/app/appSelectors" +import { getCurrentLayer } from "@/features/layers/layerSelectors" +import { getPreviewState } from "@/features/preview/previewSelectors" import { updateLayer } from "@/features/layers/layersSlice" -import { getVerticesStats } from "@/features/machine/selectors" +import { getVerticesStats } from "@/features/machine/machineSelectors" import { getModelFromType } from "@/config/models" import "./Preview.scss" import { updatePreview } from "./previewSlice" diff --git a/src/features/preview/PreviewConnector.js b/src/features/preview/PreviewConnector.js index 365c4dd7..158b518c 100644 --- a/src/features/preview/PreviewConnector.js +++ b/src/features/preview/PreviewConnector.js @@ -6,9 +6,9 @@ import { getSliderColors, getVertexOffsets, getConnectingVertices, -} from "@/features/machine/selectors" -import { getPreviewState } from "@/features/store/selectors" -import { getCurrentLayer, getLayer } from "@/features/layers/selectors" +} from "@/features/machine/machineSelectors" +import { getPreviewState } from "@/features/preview/previewSelectors" +import { getCurrentLayer, getLayer } from "@/features/layers/layerSelectors" import PreviewHelper from "./PreviewHelper" // Renders a connector between two layers. diff --git a/src/features/preview/PreviewLayer.js b/src/features/preview/PreviewLayer.js index 2883c8bb..1c5514de 100644 --- a/src/features/preview/PreviewLayer.js +++ b/src/features/preview/PreviewLayer.js @@ -6,16 +6,17 @@ import { getSliderColors, getVertexOffsets, getSliderBounds, -} from "../machine/selectors" +} from "@/features/machine/machineSelectors" import { updateLayer } from "@/features/layers/layersSlice" -import { getLayersState, getPreviewState } from "@/features/store/selectors" +import { getPreviewState } from "@/features/preview/previewSelectors" +import { getLayersState } from "@/features/layers/layerSelectors" import { getModelFromType } from "@/config/models" import { getCurrentLayer, getLayerIndex, getLayer, getNumVisibleLayers, -} from "@/features/layers/selectors" +} from "@/features/layers/layerSelectors" import { roundP } from "@/common/util" import PreviewHelper from "./PreviewHelper" diff --git a/src/features/preview/PreviewWindow.js b/src/features/preview/PreviewWindow.js index 7cecec9e..8ae0e7df 100644 --- a/src/features/preview/PreviewWindow.js +++ b/src/features/preview/PreviewWindow.js @@ -4,13 +4,14 @@ import { Provider } from "react-redux" import { Stage, Layer, Circle, Rect } from "react-konva" import throttle from "lodash/throttle" import { updateLayer } from "@/features/layers/layersSlice" -import { getMachineState, getPreviewState } from "@/features/store/selectors" +import { getPreviewState } from "@/features/preview/previewSelectors" +import { getMachineState } from "@/features/machine/machineSelectors" import { getCurrentLayer, getKonvaLayerIds, getVisibleNonEffectIds, isDragging, -} from "@/features/layers/selectors" +} from "@/features/layers/layerSelectors" import { roundP } from "@/common/util" import PreviewLayer from "./PreviewLayer" import PreviewConnector from "./PreviewConnector" diff --git a/src/features/preview/previewSelectors.js b/src/features/preview/previewSelectors.js new file mode 100644 index 00000000..a6497994 --- /dev/null +++ b/src/features/preview/previewSelectors.js @@ -0,0 +1,7 @@ +import { createSelector } from "reselect" +import { getMainState } from "@/features/app/appSelectors" + +export const getPreviewState = createSelector( + getMainState, + (main) => main.preview, +) diff --git a/src/features/store/selectors.js b/src/features/store/selectors.js deleted file mode 100644 index 561ac6c9..00000000 --- a/src/features/store/selectors.js +++ /dev/null @@ -1,51 +0,0 @@ -import { createSelectorCreator, defaultMemoize, createSelector } from "reselect" -import isEqual from "lodash" - -// the make selector functions below are patterned after the comment here: -// https://github.com/reduxjs/reselect/issues/74#issuecomment-472442728 -const cachedSelectors = {} - -// ensures we only create a single selector for a given layer -export const getCachedSelector = (fn, ...layerIds) => { - const key = layerIds.join("-") - - if (!cachedSelectors[fn.name]) { - cachedSelectors[fn.name] = {} - } - - if (!cachedSelectors[fn.name][key]) { - cachedSelectors[fn.name][key] = fn.apply(null, layerIds) - } - - return cachedSelectors[fn.name][key] -} - -// does a deep equality check instead of checking immutability; used in cases -// where a selector depends on another selector that returns a new object each time, -// e.g., getLayerIndex -export const createDeepEqualSelector = createSelectorCreator( - defaultMemoize, - isEqual, -) - -// root state selectors -export const getState = (state) => state -export const getMainState = (state) => state.main -export const getLayersState = createSelector( - getMainState, - (main) => main.layers, -) -export const getAppState = createSelector(getMainState, (main) => main.app) -export const getExporterState = createSelector( - getMainState, - (main) => main.exporter, -) -export const getMachineState = createSelector( - getMainState, - (main) => main.machine, -) -export const getPreviewState = createSelector( - getMainState, - (main) => main.preview, -) -export const getFontsState = (state) => state.fonts From da0c7c5218255c493e886d452f1671b029c9ab74 Mon Sep 17 00:00:00 2001 From: Bob Carmichael Date: Sat, 29 Jul 2023 08:22:11 -0400 Subject: [PATCH 029/126] component refactoring; fix file import and preview window bugs --- src/common/geometry.js | 4 + src/components/CheckboxOption.js | 24 +- src/components/CommentsBox.js | 8 +- src/components/DropdownOption.js | 27 ++- src/components/InputOption.js | 52 ++-- src/components/ToggleButtonOption.js | 86 +++---- src/features/importer/GCodeImporter.js | 69 +++--- src/features/importer/ThetaRhoImporter.js | 41 ++-- src/features/layers/ImportLayer.js | 280 ++++++++++------------ src/features/layers/LayerEditor.js | 5 +- src/features/layers/LayerEditor.scss | 4 - src/features/machine/PolarMachine.js | 2 + src/features/machine/RectMachine.js | 2 + src/features/preview/PreviewWindow.js | 21 +- src/models/FileImport.js | 51 ++-- 15 files changed, 327 insertions(+), 349 deletions(-) delete mode 100644 src/features/layers/LayerEditor.scss diff --git a/src/common/geometry.js b/src/common/geometry.js index ac16c89a..5908421f 100644 --- a/src/common/geometry.js +++ b/src/common/geometry.js @@ -332,3 +332,7 @@ export const toScaraGcode = (vertices, unitsPerCircle) => { return new Victor(x, y) }) } + +export const pointsToVertices = (points) => { + return points.map((point) => new Victor(point.x, point.y)) +} diff --git a/src/components/CheckboxOption.js b/src/components/CheckboxOption.js index 267c6162..0aaa20e5 100644 --- a/src/components/CheckboxOption.js +++ b/src/components/CheckboxOption.js @@ -3,11 +3,22 @@ import { Col, Row, Form } from "react-bootstrap" import S from "react-switch" const Switch = S.default ? S.default : S // Fix: https://github.com/vitejs/vite/issues/2139 -const CheckboxOption = ({ options, optionKey, data, object, handleChange }) => { +const CheckboxOption = ({ options, optionKey, data, object, onChange }) => { const option = options[optionKey] const visible = option.isVisible === undefined ? true : option.isVisible(object, data) + const handleChange = (checked) => { + let attrs = {} + attrs[optionKey] = checked + + if (option.onChange !== undefined) { + attrs = option.onChange(object, attrs, data) + } + + onChange(attrs) + } + return ( @@ -17,16 +28,7 @@ const CheckboxOption = ({ options, optionKey, data, object, handleChange }) => { { - let attrs = {} - attrs[optionKey] = checked - - if (option.handleChange !== undefined) { - attrs = option.handleChange(object, attrs, data) - } - - handleChange(attrs) - }} + onChange={handleChange} /> diff --git a/src/components/CommentsBox.js b/src/components/CommentsBox.js index 4a445d9d..2ac7a802 100644 --- a/src/components/CommentsBox.js +++ b/src/components/CommentsBox.js @@ -12,11 +12,9 @@ const CommentsBox = ({ options, optionKey, data, comments }) => { }) return ( -
- {option.title}:
{renderedComments}
+
+
{option.title}
+
{renderedComments}
) } diff --git a/src/components/DropdownOption.js b/src/components/DropdownOption.js index 3ea29345..9789ff89 100644 --- a/src/components/DropdownOption.js +++ b/src/components/DropdownOption.js @@ -7,7 +7,7 @@ const DropdownOption = ({ optionKey, data, object, - handleChange, + onChange, index, }) => { const option = options[optionKey] @@ -25,11 +25,22 @@ const DropdownOption = ({ : Object.keys(choices).map((key) => { return { value: key, label: option.choices[key] } }) - const currentLabel = Array.isArray(choices) ? currentChoice : choices[currentChoice] + const handleChange = (choice) => { + const value = choice.value + let attrs = {} + attrs[optionKey] = value + + if (option.handleChange !== undefined) { + attrs = option.handleChange(object, attrs, data) + } + + onChange(attrs) + } + return ( - + {label && ( {option.title} )} - {renderedInput} + {renderedInput} ) } else { diff --git a/src/components/ToggleButtonOption.js b/src/components/ToggleButtonOption.js index 2f9f84db..d3fdfd25 100644 --- a/src/components/ToggleButtonOption.js +++ b/src/components/ToggleButtonOption.js @@ -23,11 +23,11 @@ const ToggleButtonOption = (props) => { return ( - + {option.title} - + { +const LayerEditor = () => { const dispatch = useDispatch() - const state = useSelector(selectCurrentLayer) - const layer = new Layer(state.type) - const model = layer.model - const layerOptions = layer.getOptions() + const layer = useSelector(selectCurrentLayer) + const instance = new Layer(layer.type) + const model = instance.model + const layerOptions = instance.getOptions() const modelOptions = model.getOptions() const selectOptions = getShapeSelectOptions() const allowModelSelection = model.selectGroup !== "import" @@ -45,23 +45,23 @@ const LayerEditor = ({ id }) => { ) : undefined const handleChangeType = (selected) => { - dispatch(changeModelType({ id, type: selected.value })) + dispatch(changeModelType({ id: layer.id, type: selected.value })) } const handleChange = (attrs) => { - attrs.id = id + attrs.id = layer.id dispatch(updateLayer(attrs)) } const handleRestoreDefaults = () => { - dispatch(restoreDefaults(id)) + dispatch(restoreDefaults(layer.id)) } const renderedModelSelection = allowModelSelection && ( - Type + Type - + - + {label && ( {option.title} )} - {renderedInput} + + {renderedInput} + ) } else { diff --git a/src/components/ToggleButtonOption.js b/src/components/ToggleButtonOption.js index d3fdfd25..e9a9eed8 100644 --- a/src/components/ToggleButtonOption.js +++ b/src/components/ToggleButtonOption.js @@ -23,11 +23,17 @@ const ToggleButtonOption = (props) => { return ( - + {option.title} - + .btn.active, .btn-group>.btn:active, .btn-group>.btn:focus { z-index: inherit; } diff --git a/src/features/effects/Effect.js b/src/features/effects/Effect.js index 26c85b2e..fd290887 100644 --- a/src/features/effects/Effect.js +++ b/src/features/effects/Effect.js @@ -1,7 +1,3 @@ import Model from "@/common/Model" -export default class Effect extends Model { - constructor(type) { - super(type) - } -} +export default class Effect extends Model {} diff --git a/src/features/effects/EffectEditor.js b/src/features/effects/EffectEditor.js new file mode 100644 index 00000000..0ecc0787 --- /dev/null +++ b/src/features/effects/EffectEditor.js @@ -0,0 +1,97 @@ +import React from "react" +import { useDispatch, useSelector } from "react-redux" +import { Row, Col } from "react-bootstrap" +import Select from "react-select" +import InputOption from "@/components/InputOption" +import DropdownOption from "@/components/DropdownOption" +import CheckboxOption from "@/components/CheckboxOption" +import ToggleButtonOption from "@/components/ToggleButtonOption" +import { getEffectSelectOptions } from "@/features/effects/factory" +import { updateEffect } from "./effectsSlice" +import EffectLayer from "./EffectLayer" +import { selectCurrentEffect } from "./effectsSlice" + +const EffectEditor = ({ id }) => { + const dispatch = useDispatch() + const effect = useSelector(selectCurrentEffect) + const model = new EffectLayer(effect.type).model + const modelOptions = model.getOptions() + const selectOptions = getEffectSelectOptions() + const selectedOption = { + value: model.type, + label: model.label, + } + + // const handleChangeType = (selected) => { + // dispatch(changeModelType({ id, type: selected.value })) + // } + + const handleChange = (attrs) => { + attrs.id = effect.id + dispatch(updateEffect(attrs)) + } + + // const handleRestoreDefaults = () => { + // dispatch(restoreDefaults(id)) + // } + + const renderedModelSelection = ( + + + Type + + + + + + + + Name + + + + + + + + + + + + ) +} + +export default NewEffect diff --git a/src/features/effects/Noise.js b/src/features/effects/Noise.js index 7513c419..338cb555 100644 --- a/src/features/effects/Noise.js +++ b/src/features/effects/Noise.js @@ -132,11 +132,6 @@ export default class Noise extends Effect { return new Victor(vertex.x + Math.cos(a) * 5, vertex.y + Math.sin(a) * 5) } - getVertices(state) { - // not needed - return [] - } - getOptions() { return options } diff --git a/src/features/effects/Track.js b/src/features/effects/Track.js index df03ae60..a46da934 100644 --- a/src/features/effects/Track.js +++ b/src/features/effects/Track.js @@ -1,5 +1,5 @@ import Effect from "./Effect" -import { offset, rotate, circle } from "@/common/geometry" +import { offset, rotate } from "@/common/geometry" const options = { trackRadius: { @@ -46,10 +46,10 @@ export default class Track extends Effect { } } - getVertices(state) { - // TODO Make this more reasonable + // TODO: replace with bounds for transformer + /*getVertices(state) { return circle(25) - } + }*/ getVertices(effect, layer, vertices) { let outputVertices = [] diff --git a/src/features/effects/Warp.js b/src/features/effects/Warp.js index 89df1f31..3ba6efab 100644 --- a/src/features/effects/Warp.js +++ b/src/features/effects/Warp.js @@ -1,6 +1,6 @@ import Victor from "victor" import Effect from "./Effect" -import { circle, subsample } from "@/common/geometry" +import { subsample } from "@/common/geometry" import { evaluate } from "mathjs" const options = { @@ -83,10 +83,11 @@ export default class Warp extends Effect { } } - getVertices(state) { + // TODO: replace with bounds for transformer + /*getVertices(state) { const width = state.shape.width return circle(width / 2) - } + }*/ getVertices(effect, layer, vertices) { if (effect.subsample) { diff --git a/src/features/effects/effectsSlice.js b/src/features/effects/effectsSlice.js index b744bbb6..d9e2b4ad 100644 --- a/src/features/effects/effectsSlice.js +++ b/src/features/effects/effectsSlice.js @@ -1,6 +1,8 @@ import { createSlice, createEntityAdapter } from "@reduxjs/toolkit" +import { createSelector } from "reselect" import { createCachedSelector } from "re-reselect" import { v4 as uuidv4 } from "uuid" +import { selectState } from "@/features/app/appSlice" // ------------------------------ // Slice, reducers and atomic actions @@ -26,7 +28,7 @@ export const effectsSlice = createSlice({ state.ids.splice(index, 0, effect.id) state.entities[effect.id] = effect state.current = effect.id - localStorage.setItem("defaultShape", effect.type) + localStorage.setItem("defaultEffect", effect.type) }, prepare(effect) { const id = uuidv4() @@ -49,11 +51,20 @@ export const effectsSlice = createSlice({ const effect = action.payload effectsAdapter.updateOne(state, { id: effect.id, changes: effect }) }, + setCurrentEffect: (state, action) => { + const id = action.payload + + if (state.entities[id]) { + state.current = id + state.selected = id + } + }, }, }) export default effectsSlice.reducer -export const { addEffect, deleteEffect, updateEffect } = effectsSlice.actions +export const { addEffect, deleteEffect, updateEffect, setCurrentEffect } = + effectsSlice.actions // ------------------------------ // Selectors @@ -63,8 +74,15 @@ export const { selectAll: selectAllEffects, selectById: selectEffectById, selectIds: selectEffectIds, + selectEntities: selectEffectEntities, + selectNumEffects: selectTotal, } = effectsAdapter.getSelectors((state) => state.effects) +export const selectEffects = createSelector( + selectState, + (state) => state.effects, +) + export const selectEffectsByLayerId = createCachedSelector( selectAllEffects, (state, layerId) => layerId, @@ -74,3 +92,15 @@ export const selectEffectsByLayerId = createCachedSelector( )({ keySelector: (state, layerId) => layerId, }) + +const selectCurrentEffectId = createSelector( + selectEffects, + (effects) => effects.current, +) + +export const selectCurrentEffect = createSelector( + [selectEffectEntities, selectCurrentEffectId], + (effects, current) => { + return effects[current] + }, +) diff --git a/src/features/groups/Group.js b/src/features/groups/Group.js index 965ad98f..b50b516f 100644 --- a/src/features/groups/Group.js +++ b/src/features/groups/Group.js @@ -1,5 +1,3 @@ -export default class Group extends Model { - constructor(type) { - super(type) - } -} +import Model from "@/common/Model" + +export default class Group extends Model {} diff --git a/src/features/layers/Layer.js b/src/features/layers/Layer.js index 88a22953..500bb133 100644 --- a/src/features/layers/Layer.js +++ b/src/features/layers/Layer.js @@ -1,5 +1,6 @@ import { getShapeFromType } from "@/features/shapes/factory" -import { resizeVertices, centerOnOrigin } from "@/common/geometry" +import EffectLayer from "@/features/effects/EffectLayer" +import { resizeVertices, centerOnOrigin, findBounds } from "@/common/geometry" export const layerOptions = { name: { @@ -84,7 +85,7 @@ export default class Layer { reverse: false, visible: true, name: this.model.label, - effects: [], + effectIds: [], }, } } @@ -94,24 +95,47 @@ export default class Layer { } // returns an array of Victor vertices - draw(state) { - const { width, height, x, y, rotation } = state.shape - let vertices = this.model.draw(state) + getVertices({ layer, effects, machine }) { + this.state = layer + this.vertices = this.model.getCachedVertices({ shape: layer, machine }) + this.resize() + + const bounds = findBounds(this.vertices) + + this.applyEffects(effects) + + // center relative to our original shape prior to effects + centerOnOrigin(this.vertices, bounds) + this.transform() + + return this.vertices + } + + resize() { + const { width, height } = this.state if (this.model.autosize) { - vertices = resizeVertices(vertices, width, height, false) - centerOnOrigin(vertices) + this.vertices = resizeVertices(this.vertices, width, height, false) } + } + + transform() { + const { x, y, rotation } = this.state - vertices.forEach((vertex) => { + this.vertices.forEach((vertex) => { vertex.rotateDeg(-rotation) vertex.addX({ x: x || 0 }).addY({ y: y || 0 }) }) - if (state.shape.reverse) { - vertices = vertices.reverse() + if (this.state.reverse) { + this.vertices = this.vertices.reverse() } + } - return vertices + applyEffects(effects) { + effects.forEach((effect) => { + const effectLayer = new EffectLayer(effect.type) + this.vertices = effectLayer.getVertices(effect, this.state, this.vertices) + }) } } diff --git a/src/features/layers/LayerEditor.js b/src/features/layers/LayerEditor.js index f9b0d1e0..d49d747e 100644 --- a/src/features/layers/LayerEditor.js +++ b/src/features/layers/LayerEditor.js @@ -59,9 +59,17 @@ const LayerEditor = () => { const renderedModelSelection = allowModelSelection && ( - Type + + Type + - + - - - ) - const getOptionComponent = (model, options, key, label = true) => { const option = options[key] const props = { @@ -88,7 +55,8 @@ const EffectEditor = ({ id }) => { return (
- {renderedModelSelection} + {getOptionComponent(model, effectOptions, "width")} + {getOptionComponent(model, effectOptions, "height")} {renderedModelOptions}
) diff --git a/src/features/effects/EffectLayer.js b/src/features/effects/EffectLayer.js index 28f12cae..fdafde3a 100644 --- a/src/features/effects/EffectLayer.js +++ b/src/features/effects/EffectLayer.js @@ -1,10 +1,31 @@ import { getEffectFromType } from "@/features/effects/factory" -export const effectOptions = {} +export const effectOptions = { + width: { + title: "W", + min: 1, + isVisible: (model, state) => { + return model.canChangeSize(state) + }, + onChange: (model, changes, state) => { + if (!model.canChangeHeight(state)) { + changes.height = changes.width + } + return changes + }, + }, + height: { + title: "H", + min: 1, + isVisible: (model, state) => { + return model.canChangeSize(state) && model.canChangeHeight(state) + }, + }, +} export default class EffectLayer { - constructor(type, state) { - this.model = getEffectFromType(type, state) + constructor(type) { + this.model = getEffectFromType(type) } getInitialState(props) { @@ -25,4 +46,8 @@ export default class EffectLayer { getVertices(effect, layer, vertices) { return this.model.getVertices(effect, layer, vertices) } + + getSelectionVertices(effect) { + return this.model.getSelectionVertices(effect) + } } diff --git a/src/features/effects/EffectList.js b/src/features/effects/EffectList.js index e8faccf6..f5338714 100644 --- a/src/features/effects/EffectList.js +++ b/src/features/effects/EffectList.js @@ -1,5 +1,5 @@ import React from "react" -import { useDispatch } from "react-redux" +import { useDispatch, useSelector } from "react-redux" import { Button, ListGroup } from "react-bootstrap" import { FaEye, FaEyeSlash } from "react-icons/fa" import { DndContext, useSensor, useSensors, PointerSensor } from "@dnd-kit/core" @@ -9,30 +9,34 @@ import { useSortable, verticalListSortingStrategy, } from "@dnd-kit/sortable" -import { moveEffect } from "@/features/layers/layersSlice" -import { setCurrentEffect, updateEffect } from "./effectsSlice" +import { moveEffect, setCurrentEffect } from "@/features/layers/layersSlice" +import { + updateEffect, + selectCurrentEffectId, + selectSelectedEffectId, +} from "./effectsSlice" const EffectRow = ({ - active, + current, + selected, effect, handleEffectSelected, handleToggleEffectVisible, }) => { const { name, id, visible } = effect - const { attributes, listeners, setNodeRef, transform, isDragging } = useSortable({ id, }) - const style = { transform: `translate3d(${transform?.x || 0}px, ${transform?.y || 0}px, 0)`, cursor: isDragging ? "grabbing" : "grab", } + const itemClass = current ? "active" : selected ? "selected" : "" return (
@@ -61,8 +65,10 @@ const EffectRow = ({ ) } -const EffectList = ({ effects, currentEffect, currentLayer }) => { +const EffectList = ({ effects, currentEffect, selectedLayer }) => { const dispatch = useDispatch() + const currentEffectId = useSelector(selectCurrentEffectId) + const selectedEffectId = useSelector(selectSelectedEffectId) // row has to be dragged 3 pixels before dragging starts; this allows the buttons // on the row to work properly. @@ -80,7 +86,7 @@ const EffectList = ({ effects, currentEffect, currentLayer }) => { 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: currentLayer.id, oldIndex, newIndex })) + dispatch(moveEffect({ id: selectedLayer.id, oldIndex, newIndex })) } } @@ -113,7 +119,8 @@ const EffectList = ({ effects, currentEffect, currentLayer }) => { { const dispatch = useDispatch() - const currentLayer = useSelector(selectCurrentLayer) + const selectedLayer = useSelector(selectSelectedLayer) const currentEffect = useSelector(selectCurrentEffect) const effects = useSelector((state) => - selectLayerEffects(state, currentLayer.id), + selectLayerEffects(state, selectedLayer.id), ) const numEffects = effects.length const [showNewEffect, setShowNewEffect] = useState(false) @@ -27,7 +27,7 @@ const EffectManager = () => { const handleEffectDeleted = (id) => { dispatch( deleteEffect({ - id: currentLayer.id, + id: selectedLayer.id, effectId: currentEffect.id, }), ) @@ -66,11 +66,11 @@ const EffectManager = () => { - +
- +
+ +
diff --git a/src/features/effects/FineTuning.js b/src/features/effects/FineTuning.js index e339e2f2..13587a38 100644 --- a/src/features/effects/FineTuning.js +++ b/src/features/effects/FineTuning.js @@ -127,6 +127,9 @@ export default class FineTuning extends Effect { return vertices.concat(backtrackVertices) } + // given an array of vertices and a percentage, returns a new vertex (vNew) positioned at + // the percentage-based distance along the path, along with the two vertices (v1, v2) that bound + // it on either side. getBoundingSegment(vertices, pct) { const d = Math.round((totalDistance(vertices) * pct) / 100.0) const [v1, v2] = boundingVerticesAtLength(vertices, d) diff --git a/src/features/effects/Fisheye.js b/src/features/effects/Fisheye.js index 45f15c9f..af65d159 100644 --- a/src/features/effects/Fisheye.js +++ b/src/features/effects/Fisheye.js @@ -1,13 +1,20 @@ import Victor from "victor" -import Effect from "./Effect" import * as d3Fisheye from "d3-fisheye" +import { circle } from "@/common/geometry" +import Effect from "./Effect" const options = { fisheyeDistortion: { title: "Distortion", min: -2, max: 40, - step: 0.1, + step: 1, + }, + x: { + title: "X", + }, + y: { + title: "Y", }, } @@ -15,12 +22,11 @@ export default class Fisheye extends Effect { constructor() { super("fisheye") this.label = "Fisheye" - this.startingWidth = 100 - this.startingHeight = 100 + this.canMove = true } - canRotate(state) { - return false + canChangeSize(state) { + return true } canChangeHeight(state) { @@ -32,19 +38,24 @@ export default class Fisheye extends Effect { ...super.getInitialState(), ...{ fisheyeDistortion: 3, + x: 0, + y: 0, + width: 100, + height: 100, }, } } - // TODO: Replace with selecting bounding - // getVertices(state) { - // return circle(this.startingWidth / 2) - // } + getSelectionVertices(effect) { + console.log("here") + return circle(effect.width / 2) + } getVertices(effect, layer, vertices) { + const radius = effect.width / 2 const fisheye = d3Fisheye .radial() - .radius(effect.width / 2) + .radius(radius) .distortion(effect.fisheyeDistortion / 2) fisheye.focus([effect.x, effect.y]) diff --git a/src/features/effects/NewEffect.js b/src/features/effects/NewEffect.js index 86038af2..9e0195c6 100644 --- a/src/features/effects/NewEffect.js +++ b/src/features/effects/NewEffect.js @@ -2,7 +2,7 @@ import React, { useState } from "react" import { useDispatch, useSelector } from "react-redux" import Select from "react-select" import { Button, Modal, Row, Col, Form } from "react-bootstrap" -import { selectCurrentLayer, addEffect } from "@/features/layers/layersSlice" +import { selectSelectedLayer, addEffect } from "@/features/layers/layersSlice" import { getEffectSelectOptions, getDefaultEffect, @@ -21,7 +21,7 @@ const customStyles = { const NewEffect = ({ toggleModal, showModal }) => { const dispatch = useDispatch() - const currentLayer = useSelector(selectCurrentLayer) + const selectedLayer = useSelector(selectSelectedLayer) const selectOptions = getEffectSelectOptions() const [type, setType] = useState(defaultEffect.type) const [name, setName] = useState(defaultEffect.label) @@ -51,7 +51,7 @@ const NewEffect = ({ toggleModal, showModal }) => { dispatch( addEffect({ - id: currentLayer.id, + id: selectedLayer.id, effect: layer.getInitialState(), }), ) diff --git a/src/features/effects/effectsSlice.js b/src/features/effects/effectsSlice.js index d9e2b4ad..de5cf380 100644 --- a/src/features/effects/effectsSlice.js +++ b/src/features/effects/effectsSlice.js @@ -1,8 +1,10 @@ import { createSlice, createEntityAdapter } from "@reduxjs/toolkit" -import { createSelector } from "reselect" +import { createSelector, createSelectorCreator, defaultMemoize } from "reselect" import { createCachedSelector } from "re-reselect" +import { isEqual } from "lodash" import { v4 as uuidv4 } from "uuid" import { selectState } from "@/features/app/appSlice" +import EffectLayer from "./EffectLayer" // ------------------------------ // Slice, reducers and atomic actions @@ -28,6 +30,7 @@ export const effectsSlice = createSlice({ state.ids.splice(index, 0, effect.id) state.entities[effect.id] = effect state.current = effect.id + state.selected = effect.id localStorage.setItem("defaultEffect", effect.type) }, prepare(effect) { @@ -54,7 +57,9 @@ export const effectsSlice = createSlice({ setCurrentEffect: (state, action) => { const id = action.payload - if (state.entities[id]) { + if (!id) { + state.current = null // preserve selection + } else if (state.entities[id]) { state.current = id state.selected = id } @@ -63,8 +68,7 @@ export const effectsSlice = createSlice({ }) export default effectsSlice.reducer -export const { addEffect, deleteEffect, updateEffect, setCurrentEffect } = - effectsSlice.actions +export const { addEffect, deleteEffect, updateEffect, setCurrentEffect } = effectsSlice.actions // ------------------------------ // Selectors @@ -93,14 +97,44 @@ export const selectEffectsByLayerId = createCachedSelector( keySelector: (state, layerId) => layerId, }) -const selectCurrentEffectId = createSelector( +export const selectCurrentEffectId = createSelector( selectEffects, (effects) => effects.current, ) +export const selectSelectedEffectId = createSelector( + selectEffects, + (effects) => effects.selected, +) + export const selectCurrentEffect = createSelector( [selectEffectEntities, selectCurrentEffectId], - (effects, current) => { - return effects[current] + (effects, currentId) => { + return effects[currentId] + }, +) + +export const selectSelectedEffect = createSelector( + [selectEffectEntities, selectSelectedEffectId], + (effects, selectedId) => { + return effects[selectedId] }, ) + +// returns the selection vertices for a given effect +export const selectEffectSelectionVertices = createCachedSelector( + selectEffectById, + (effect) => { + if (!effect) { + return [] + } // zombie child + + const instance = new EffectLayer(effect.type) + return instance.getSelectionVertices(effect) + }, +)({ + keySelector: (state, id) => id, + selectorCreator: createSelectorCreator(defaultMemoize, { + equalityCheck: isEqual, + }), +}) diff --git a/src/features/layers/CopyLayer.js b/src/features/layers/CopyLayer.js index fb498e86..10293e17 100644 --- a/src/features/layers/CopyLayer.js +++ b/src/features/layers/CopyLayer.js @@ -2,13 +2,13 @@ import React, { useRef, useState } from "react" import { Button, Modal, Row, Col, Form } from "react-bootstrap" import { useDispatch, useSelector } from "react-redux" import { copyLayer } from "./layersSlice" -import { selectCurrentLayer } from "./layersSlice" +import { selectSelectedLayer } from "./layersSlice" const CopyLayer = ({ toggleModal, showModal }) => { const dispatch = useDispatch() - const currentLayer = useSelector(selectCurrentLayer) + const selectedLayer = useSelector(selectSelectedLayer) const namedInputRef = useRef(null) - const [copyLayerName, setCopyLayerName] = useState(currentLayer.name) + const [copyLayerName, setCopyLayerName] = useState(selectedLayer.name) const handleChangeCopyLayerName = (event) => { setCopyLayerName(event.target.value) @@ -21,7 +21,7 @@ const CopyLayer = ({ toggleModal, showModal }) => { const handleCopyLayer = () => { dispatch( copyLayer({ - id: currentLayer.id, + id: selectedLayer.id, name: copyLayerName, }), ) @@ -39,7 +39,7 @@ const CopyLayer = ({ toggleModal, showModal }) => { onEntered={handleInitialFocus} > - Copy {currentLayer.name} + Copy {selectedLayer.name} diff --git a/src/features/layers/LayerEditor.js b/src/features/layers/LayerEditor.js index d49d747e..3fb58591 100644 --- a/src/features/layers/LayerEditor.js +++ b/src/features/layers/LayerEditor.js @@ -13,11 +13,11 @@ import { getShapeSelectOptions } from "@/features/shapes/factory" import { updateLayer, changeModelType, restoreDefaults } from "./layersSlice" import Layer from "./Layer" import EffectManager from "@/features/effects/EffectManager" -import { selectCurrentLayer } from "./layersSlice" +import { selectSelectedLayer } from "./layersSlice" const LayerEditor = () => { const dispatch = useDispatch() - const layer = useSelector(selectCurrentLayer) + const layer = useSelector(selectSelectedLayer) const instance = new Layer(layer.type) const model = instance.model const layerOptions = instance.getOptions() @@ -75,6 +75,8 @@ const LayerEditor = () => { onChange={handleChangeType} maxMenuHeight={305} options={selectOptions} + menuPortalTarget={document.body} + styles={{ menuPortal: (base) => ({ ...base, zIndex: 9999 }) }} /> diff --git a/src/features/layers/LayerList.js b/src/features/layers/LayerList.js index bd7e0323..f9b97b8a 100644 --- a/src/features/layers/LayerList.js +++ b/src/features/layers/LayerList.js @@ -13,30 +13,30 @@ import Layer from "./Layer" import { moveLayer, setCurrentLayer, - selectCurrentLayer, + selectCurrentLayerId, + selectSelectedLayer, selectNumLayers, selectAllLayers, updateLayer, } from "@/features/layers/layersSlice" const LayerRow = ({ - active, + current, + selected, numLayers, layer, handleLayerSelected, handleToggleLayerVisible, }) => { const { name, id, visible } = layer - const activeClass = active ? "active" : "" + const activeClass = current ? "active" : selected ? "selected" : "" const dragClass = numLayers > 1 ? "cursor-move" : "" const visibleClass = visible ? "" : "layer-hidden" const instance = new Layer(layer.type) - const { attributes, listeners, setNodeRef, transform, isDragging } = useSortable({ id, }) - const style = { transform: `translate3d(${transform?.x || 0}px, ${transform?.y || 0}px, 0)`, cursor: isDragging ? "grabbing" : "grab", @@ -94,7 +94,8 @@ const LayerList = () => { }), ) const dispatch = useDispatch() - const currentLayer = useSelector(selectCurrentLayer) + const currentLayerId = useSelector(selectCurrentLayerId) + const selectedLayer = useSelector(selectSelectedLayer) const numLayers = useSelector(selectNumLayers) const layers = useSelector(selectAllLayers) @@ -106,7 +107,7 @@ const LayerList = () => { const handleDragStart = ({ active }) => dispatch(setCurrentLayer(active.id)) const handleToggleLayerVisible = (id) => { - dispatch(updateLayer({ id, visible: !currentLayer.visible })) + dispatch(updateLayer({ id, visible: !selectedLayer.visible })) } const handleDragEnd = ({ active, over }) => { @@ -138,7 +139,8 @@ const LayerList = () => { { const dispatch = useDispatch() - const currentLayer = useSelector(selectCurrentLayer) + const selectedLayerId = useSelector(selectSelectedLayerId) const numLayers = useSelector(selectNumLayers) const canRemove = numLayers > 1 @@ -28,7 +28,7 @@ const LayerManager = () => { const toggleNewLayerModal = () => setShowNewLayer(!showNewLayer) const toggleImportModal = () => setShowImportLayer(!showImportLayer) const toggleCopyModal = () => setShowCopyLayer(!showCopyLayer) - const handleLayerRemoved = (id) => dispatch(deleteLayer(currentLayer.id)) + const handleLayerRemoved = (id) => dispatch(deleteLayer(selectedLayerId)) useEffect(() => { const el = document.getElementById("layers") diff --git a/src/features/layers/LayerManager.scss b/src/features/layers/LayerManager.scss index 727356f9..156e1ada 100644 --- a/src/features/layers/LayerManager.scss +++ b/src/features/layers/LayerManager.scss @@ -5,6 +5,10 @@ color: white; background-color: inherit; } + + .selected.list-group-item & { + background-color: #d2d2d2; + } } .cursor-move { diff --git a/src/features/layers/layersSlice.js b/src/features/layers/layersSlice.js index ab4c516a..59821142 100644 --- a/src/features/layers/layersSlice.js +++ b/src/features/layers/layersSlice.js @@ -17,6 +17,7 @@ import { effectsSlice, selectEffectById, selectEffectsByLayerId, + selectCurrentEffect, } from "@/features/effects/effectsSlice" import { selectMachine, @@ -38,9 +39,9 @@ const layerState = { } const notCopiedWhenTypeChanges = ["type", "height", "width"] -function currLayerIndex(state) { - const currentLayer = state.entities[state.current] - return state.ids.findIndex((id) => id === currentLayer.id) +const currSelectedIndex = (state) => { + const selectedLayer = state.entities[state.selected] + return state.ids.findIndex((id) => id === selectedLayer.id) } const layersSlice = createSlice({ @@ -57,7 +58,7 @@ const layersSlice = createSlice({ addLayer: { reducer(state, action) { // we need to insert at a specific index, which is not supported by addOne - const index = state.current ? currLayerIndex(state) + 1 : 0 + const index = state.selected ? currSelectedIndex(state) + 1 : 0 const layer = { ...action.payload, effectIds: [], @@ -152,8 +153,10 @@ const layersSlice = createSlice({ }, setCurrentLayer: (state, action) => { const id = action.payload - - if (state.entities[id]) { + console.log(id) + if (!id) { + state.current = null + } else if (state.entities[id]) { state.current = id state.selected = id } @@ -215,7 +218,7 @@ export const addEffect = ({ id, effect }) => { export const deleteEffect = ({ id, effectId }) => { return (dispatch, getState) => { const state = getState() - const effectIds = selectCurrentLayer(state).effectIds + const effectIds = selectLayerById(state, id).effectIds const deleteIdx = effectIds.findIndex((id) => id === effectId) dispatch(layersSlice.actions.removeEffect({ id, effectId })) @@ -227,6 +230,20 @@ export const deleteEffect = ({ id, effectId }) => { } } +export const setCurrentLayer = (id) => { + return (dispatch, getState) => { + dispatch(layersSlice.actions.setCurrentLayer(id)) + dispatch(effectsSlice.actions.setCurrentEffect(null)) + } +} + +export const setCurrentEffect = (id) => { + return (dispatch, getState) => { + dispatch(effectsSlice.actions.setCurrentEffect(id)) + dispatch(layersSlice.actions.setCurrentLayer(null)) + } +} + export default layersSlice.reducer export const { actions: layersActions } = layersSlice export const { @@ -236,7 +253,6 @@ export const { moveLayer, removeEffect, restoreDefaults, - setCurrentLayer, updateLayer, } = layersSlice.actions @@ -284,10 +300,22 @@ export const selectCurrentLayerId = createSelector( (layers) => layers.current, ) +export const selectSelectedLayerId = createSelector( + selectLayers, + (layers) => layers.selected, +) + export const selectCurrentLayer = createSelector( [selectLayerEntities, selectCurrentLayerId], - (layers, current) => { - return layers[current] + (layers, currentId) => { + return layers[currentId] + }, +) + +export const selectSelectedLayer = createSelector( + [selectLayerEntities, selectSelectedLayerId], + (layers, selectedId) => { + return layers[selectedId] }, ) @@ -298,19 +326,6 @@ export const selectVisibleLayerIds = createSelector( }, ) -// puts the current layer last in the list to ensure it can be rotated; else -// the handle will not rotate -export const selectKonvaLayerIds = createSelector( - [selectCurrentLayer, selectVisibleLayerIds], - (currentLayer, visibleLayerIds) => { - const kIds = visibleLayerIds.filter((id) => id !== currentLayer.id) - if (currentLayer.visible) { - kIds.push(currentLayer.id) - } - return kIds - }, -) - export const selectIsDragging = createSelector( [selectLayerIds, selectLayerEntities], (ids, layers) => { @@ -354,6 +369,16 @@ export const selectVisibleLayerEffects = createCachedSelector( }, )((state, id) => id) +export const selectActiveEffect = createCachedSelector( + selectLayerById, + selectCurrentEffect, + (layer, currentEffect) => { + if (layer.effectIds.includes(currentEffect?.id)) { + return currentEffect + } + }, +)((state, id) => id) + // returns the vertices for a given layer export const selectLayerVertices = createCachedSelector( selectLayerById, diff --git a/src/features/layers/layersSlice.spec.js b/src/features/layers/layersSlice.spec.js index 07221236..a619c986 100644 --- a/src/features/layers/layersSlice.spec.js +++ b/src/features/layers/layersSlice.spec.js @@ -271,28 +271,6 @@ describe("layers reducer", () => { }) }) - it("should handle setCurrentLayer", () => { - expect( - layersReducer( - { - entities: { - 0: {}, - 1: {}, - }, - current: "0", - }, - setCurrentLayer("1"), - ), - ).toEqual({ - entities: { - 0: {}, - 1: {}, - }, - current: "1", - selected: "1", - }) - }) - it("should handle updateLayer", () => { expect( layersReducer( @@ -344,7 +322,7 @@ describe("layers reducer", () => { const store = mockStore({ layers: { entities: { - 0: { + "0": { id: "0", name: "foo", effectIds: ["1", "2"], @@ -430,6 +408,44 @@ describe("layers reducer", () => { }) }) }) + + it("should handle setCurrentLayer", () => { + const store = mockStore({ + layers: { + entities: { + 0: { + id: "0", + name: "foo", + effectIds: ["a", "b"], + }, + }, + ids: ["0"], + selected: "0", + current: null, + }, + effects: { + entities: { + a: { + id: "a", + }, + b: { + id: "b", + }, + }, + ids: ["a", "b"], + selected: "a", + current: "a" + }, + }) + + store.dispatch( + setCurrentLayer("0"), + ) + + const actions = store.getActions() + expect(actions[0].type).toEqual("layers/setCurrentLayer") + expect(actions[1].type).toEqual("effects/setCurrentEffect") + }) }) }) diff --git a/src/features/preview/PreviewConnector.js b/src/features/preview/ConnectorPreview.js similarity index 92% rename from src/features/preview/PreviewConnector.js rename to src/features/preview/ConnectorPreview.js index 1845369b..2ea2e1dc 100644 --- a/src/features/preview/PreviewConnector.js +++ b/src/features/preview/ConnectorPreview.js @@ -3,7 +3,7 @@ import { useSelector } from "react-redux" import { isEqual } from "lodash" import { Shape } from "react-konva" import { - selectCurrentLayer, + selectSelectedLayer, selectLayerById, selectSliderBounds, selectSliderColors, @@ -13,9 +13,9 @@ import { import { selectPreviewState } from "@/features/preview/previewSlice" import PreviewHelper from "./PreviewHelper" -const PreviewConnector = (ownProps) => { +const ConnectorPreview = (ownProps) => { const { startId, endId } = ownProps - const currentLayer = useSelector(selectCurrentLayer) + const selectedLayer = useSelector(selectSelectedLayer) const startLayer = useSelector((state) => selectLayerById(state, startId)) const endLayer = useSelector((state) => selectLayerById(state, endId)) const vertices = useSelector((state) => @@ -31,7 +31,7 @@ const PreviewConnector = (ownProps) => { } // no longer valid const helper = new PreviewHelper({ - currentLayer, + selectedLayer, startLayer, endLayer, vertices, @@ -48,7 +48,7 @@ const PreviewConnector = (ownProps) => { const backgroundSelectedColor = "#6E6E00" const backgroundUnselectedColor = "rgba(195, 214, 230, 0.4)" const isSliding = sliderValue !== 0 - const isSelected = currentLayer.id === endLayer.id + const isSelected = selectedLayer.id === endLayer.id function sceneFunc(context, shape) { drawConnector(context) @@ -107,4 +107,4 @@ const PreviewConnector = (ownProps) => { ) } -export default React.memo(PreviewConnector) +export default React.memo(ConnectorPreview) diff --git a/src/features/preview/EffectPreview.js b/src/features/preview/EffectPreview.js new file mode 100644 index 00000000..ad2627ef --- /dev/null +++ b/src/features/preview/EffectPreview.js @@ -0,0 +1,182 @@ +import React, { useEffect } from "react" +import { useSelector, useDispatch } from "react-redux" +import { Shape, Transformer } from "react-konva" +import { + selectCurrentEffectId, + selectEffectById, + selectEffectSelectionVertices, + updateEffect, +} from "@/features/effects/effectsSlice" +import { getEffectFromType } from "@/features/effects/factory" +import { roundP, scaleByWheel } from "@/common/util" +import PreviewHelper from "./PreviewHelper" +import { log } from "@/common/debugging" + +const EffectPreview = (ownProps) => { + log(`EffectPreview render ${ownProps.id}`) + const dispatch = useDispatch() + const currentEffectId = useSelector(selectCurrentEffectId) + const effect = useSelector((state) => selectEffectById(state, ownProps.id)) + const vertices = useSelector((state) => + selectEffectSelectionVertices(state, ownProps.id), + ) + // const colors = useSelector(selectSliderColors, isEqual) + // const offsets = useSelector(selectVertexOffsets, isEqual) + // const bounds = useSelector(selectSliderBounds, isEqual) + + const shapeRef = React.useRef() + const trRef = React.useRef() + const isCurrent = effect?.id === currentEffectId + const model = getEffectFromType(effect?.type || "mask") + + useEffect(() => { + if (effect?.visible && isCurrent && model.canChangeSize(effect)) { + trRef.current.nodes([shapeRef.current]) + trRef.current.getLayer().batchDraw() + } + }, [isCurrent, effect, model.canMove, shapeRef, trRef]) + + if (!effect) { + // "zombie child" situation; the hooks (above) are able to deal with a + // null effect. If we're a zombie, we do not need to render. + return null + } + + const { width, height } = effect + const helper = new PreviewHelper({ layer: effect }) + + const drawLayerVertices = (context) => { + let currentColor = "yellow" + + context.beginPath() + context.lineWidth = 1 + context.strokeStyle = currentColor + helper.moveTo(context, vertices[0]) + context.stroke() + + context.beginPath() + for (let i = 1; i < vertices.length; i++) { + helper.moveTo(context, vertices[i - 1]) + helper.lineTo(context, vertices[i]) + } + context.stroke() + } + + const sceneFunc = (context, shape) => { + if (isCurrent && vertices && vertices.length > 0) { + drawLayerVertices(context) + } + + context.fillStrokeShape(shape) + } + + function hitFunc(context) { + context.fillStrokeShape(this) + } + + const handleChange = (attrs) => { + attrs.id = effect.id + dispatch(updateEffect(attrs)) + } + + const handleDragStart = (e) => { + if (e.currentTarget === e.target) { + if (isCurrent) { + handleChange({ dragging: true }) + } + } + } + + const handleDragEnd = (e) => { + if (e.currentTarget === e.target) { + handleChange({ + dragging: false, + x: roundP(e.target.x(), 0), + y: roundP(-e.target.y(), 0), + }) + } + } + + const handleTransformStart = (e) => { + if (e.currentTarget === e.target) { + handleChange({ dragging: true }) + } + } + + const handleTransformEnd = (e) => { + if (e.currentTarget === e.target) { + const node = shapeRef.current + const scaleX = node.scaleX() + const scaleY = node.scaleY() + + node.scaleX(1) + node.scaleY(1) + + handleChange({ + dragging: false, + width: roundP(Math.max(5, effect.width * scaleX), 0), + height: roundP(Math.max(5, effect.height * scaleY), 0), + rotation: roundP(node.rotation(), 0), + }) + } + } + + const handleWheel = (e) => { + if (isCurrent) { + e.evt.preventDefault() + + if (Math.abs(e.evt.deltaY) > 0) { + dispatch( + updateEffect({ + width: scaleByWheel(effect.width, e.evt.deltaY), + height: scaleByWheel(effect.height, e.evt.deltaY), + id: effect.id, + }), + ) + } + } + } + + return ( + <> + {effect.visible && ( + + )} + {effect.visible && isCurrent && model.canChangeSize(effect) && ( + + )} + + ) +} + +export default React.memo(EffectPreview) diff --git a/src/features/preview/Preview.js b/src/features/preview/PreviewManager.js similarity index 83% rename from src/features/preview/Preview.js rename to src/features/preview/PreviewManager.js index c3c047ad..03a06065 100644 --- a/src/features/preview/Preview.js +++ b/src/features/preview/PreviewManager.js @@ -4,22 +4,22 @@ import Slider from "rc-slider" import "rc-slider/assets/index.css" import Downloader from "@/features/exporter/Downloader" import { selectFontsState } from "@/features/fonts/fontsSlice" -import { selectCurrentLayer } from "@/features/layers/layersSlice" +import { selectSelectedLayer } from "@/features/layers/layersSlice" import { selectPreviewState } from "@/features/preview/previewSlice" import { updateLayer, selectVerticesStats } from "@/features/layers/layersSlice" import { getShapeFromType } from "@/features/shapes/factory" -import "./Preview.scss" +import "./PreviewManager.scss" import { updatePreview } from "./previewSlice" import PreviewWindow from "./PreviewWindow" -const Preview = () => { +const PreviewManager = () => { const dispatch = useDispatch() const fonts = useSelector(selectFontsState) - const currentLayer = useSelector(selectCurrentLayer) + const selectedLayer = useSelector(selectSelectedLayer) const sliderValue = useSelector(selectPreviewState).sliderValue const verticesStats = useSelector(selectVerticesStats) const previewElement = useRef(null) - const model = getShapeFromType(currentLayer.type) + const model = getShapeFromType(selectedLayer.type) useEffect(() => { if (fonts.loaded) { @@ -33,21 +33,21 @@ const Preview = () => { const handleKeyDown = (event) => { if (model.canMove) { - let attrs = { id: currentLayer.id } + let attrs = { id: selectedLayer.id } const delta = event.shiftKey ? 1 : 5 switch (event.key) { case "ArrowDown": - attrs.y = currentLayer.y - delta + attrs.y = selectedLayer.y - delta break case "ArrowUp": - attrs.y = currentLayer.y + delta + attrs.y = selectedLayer.y + delta break case "ArrowLeft": - attrs.x = currentLayer.x - delta + attrs.x = selectedLayer.x - delta break case "ArrowRight": - attrs.x = currentLayer.x + delta + attrs.x = selectedLayer.x + delta break default: return @@ -101,4 +101,4 @@ const Preview = () => { ) } -export default React.memo(Preview) +export default React.memo(PreviewManager) diff --git a/src/features/preview/Preview.scss b/src/features/preview/PreviewManager.scss similarity index 100% rename from src/features/preview/Preview.scss rename to src/features/preview/PreviewManager.scss diff --git a/src/features/preview/PreviewWindow.js b/src/features/preview/PreviewWindow.js index 1dc64369..d89dd222 100644 --- a/src/features/preview/PreviewWindow.js +++ b/src/features/preview/PreviewWindow.js @@ -6,12 +6,12 @@ import throttle from "lodash/throttle" import { selectPreviewState } from "@/features/preview/previewSlice" import { selectMachine } from "@/features/machine/machineSlice" import { - selectKonvaLayerIds, + selectSelectedLayer, selectVisibleLayerIds, selectIsDragging, } from "@/features/layers/layersSlice" -import PreviewLayer from "./PreviewLayer" -import PreviewConnector from "./PreviewConnector" +import ShapePreview from "./ShapePreview" +import ConnectorPreview from "./ConnectorPreview" import { setPreviewSize } from "./previewSlice" const PreviewWindow = () => { @@ -20,8 +20,9 @@ const PreviewWindow = () => { const { rectangular, minX, minY, maxX, maxY, maxRadius } = useSelector(selectMachine) const { canvasWidth, canvasHeight } = useSelector(selectPreviewState) - const konvaIds = useSelector(selectKonvaLayerIds, isEqual) + const selectedLayer = useSelector(selectSelectedLayer, isEqual) const layerIds = useSelector(selectVisibleLayerIds, isEqual) + const remainingLayerIds = layerIds.filter((id) => id !== selectedLayer?.id) const dragging = useSelector(selectIsDragging) useEffect(() => { @@ -75,6 +76,8 @@ const PreviewWindow = () => { const height = rectangular ? maxY - minY : maxRadius * 2 const scale = relativeScale() + // some awkward rendering to put the current layer as the last child in the layer to ensure + // transformer rotation works; this is a Konva restriction. return ( { offsetY={height / 2} /> )} - {konvaIds.map((id, i) => { - const idx = layerIds.findIndex((layerId) => layerId === id) - const nextId = - idx !== -1 && idx < layerIds.length - 1 ? layerIds[idx + 1] : null - return [ - nextId && ( - - ), - , - ].filter((e) => e !== null) - })} + {[ + remainingLayerIds.map((id, i) => { + const idx = layerIds.findIndex((layerId) => layerId === id) + const nextId = + idx !== -1 && idx < layerIds.length - 1 ? layerIds[idx + 1] : null + return [ + nextId && ( + + ), + , + ] + }), + selectedLayer && ( + + ), + ] + .flat() + .filter((e) => e !== null)} ) diff --git a/src/features/preview/PreviewLayer.js b/src/features/preview/ShapePreview.js similarity index 71% rename from src/features/preview/PreviewLayer.js rename to src/features/preview/ShapePreview.js index d5277cbf..76c29f87 100644 --- a/src/features/preview/PreviewLayer.js +++ b/src/features/preview/ShapePreview.js @@ -1,6 +1,6 @@ import React, { useEffect } from "react" import { useSelector, useDispatch } from "react-redux" -import { Shape, Transformer } from "react-konva" +import { Shape, Transformer, Group } from "react-konva" import { isEqual } from "lodash" import { updateLayer, @@ -8,34 +8,27 @@ import { selectLayerIndex, selectLayerById, selectNumVisibleLayers, + selectActiveEffect, selectPreviewVertices, selectSliderColors, selectVertexOffsets, selectSliderBounds, } from "@/features/layers/layersSlice" import { selectPreviewSliderValue } from "@/features/preview/previewSlice" +import EffectPreview from "@/features/preview/EffectPreview" import { getShapeFromType } from "@/features/shapes/factory" -import { roundP } from "@/common/util" +import { roundP, scaleByWheel } from "@/common/util" import PreviewHelper from "./PreviewHelper" import { log } from "@/common/debugging" -const scaleByWheel = (size, deltaY) => { - const sign = Math.sign(deltaY) - const scale = 1 + (Math.log(Math.abs(deltaY)) / 30) * sign - let newSize = Math.max(roundP(size * scale, 0), 1) - - if (newSize === size) { - // if the log scaled value isn't big enough to move the scale - newSize = Math.max(sign + size, 1) - } - - return newSize -} - -const PreviewLayer = (ownProps) => { - log(`PreviewLayer render ${ownProps.id}`) +const ShapePreview = (ownProps) => { + log(`ShapePreview render ${ownProps.id}`) const dispatch = useDispatch() const currentLayerId = useSelector(selectCurrentLayerId) + const activeEffect = useSelector( + (state) => selectActiveEffect(state, ownProps.id), + isEqual, + ) const layer = useSelector((state) => selectLayerById(state, ownProps.id)) const index = useSelector((state) => selectLayerIndex(state, ownProps.id)) const numLayers = useSelector(selectNumVisibleLayers) @@ -48,16 +41,17 @@ const PreviewLayer = (ownProps) => { const bounds = useSelector(selectSliderBounds, isEqual) const shapeRef = React.useRef() + const groupRef = React.useRef() const trRef = React.useRef() const isCurrent = layer?.id === currentLayerId const model = getShapeFromType(layer?.type || "polygon") useEffect(() => { if (layer?.visible && isCurrent && model.canChangeSize(layer)) { - trRef.current.nodes([shapeRef.current]) + trRef.current.nodes([groupRef.current]) trRef.current.getLayer().batchDraw() } - }, [isCurrent, layer, model.canMove, shapeRef, trRef]) + }, [isCurrent, activeEffect, layer, model.canMove, groupRef, trRef]) if (!layer) { // "zombie child" situation; the hooks (above) are able to deal with a @@ -67,8 +61,7 @@ const PreviewLayer = (ownProps) => { const start = index === 0 const end = index === numLayers - 1 - const width = layer.width - const height = layer.height + const { width, height } = layer const selectedColor = "yellow" const unselectedColor = "rgba(195, 214, 230, 0.65)" const backgroundSelectedColor = "#6E6E00" @@ -165,43 +158,46 @@ const PreviewLayer = (ownProps) => { dispatch(updateLayer(attrs)) } - const handleSelect = () => { - // deselection is currently disabled - // dispatch(setSelectedLayer(selected == null ? currentLayerId : null)) - } - - const handleDragStart = () => { - if (isCurrent) { - handleChange({ dragging: true }) + const handleDragStart = (e) => { + if (e.currentTarget === e.target) { + if (isCurrent) { + handleChange({ dragging: true }) + } } } const handleDragEnd = (e) => { - handleChange({ - dragging: false, - x: roundP(e.target.x(), 0), - y: roundP(-e.target.y(), 0), - }) + if (e.currentTarget === e.target) { + handleChange({ + dragging: false, + x: roundP(e.target.x(), 0), + y: roundP(-e.target.y(), 0), + }) + } } const handleTransformStart = (e) => { - handleChange({ dragging: true }) + if (e.currentTarget === e.target) { + handleChange({ dragging: true }) + } } const handleTransformEnd = (e) => { - const node = shapeRef.current - const scaleX = node.scaleX() - const scaleY = node.scaleY() - - node.scaleX(1) - node.scaleY(1) - - handleChange({ - dragging: false, - width: roundP(Math.max(5, layer.width * scaleX), 0), - height: roundP(Math.max(5, layer.height * scaleY), 0), - rotation: roundP(node.rotation(), 0), - }) + if (e.currentTarget === e.target) { + const node = groupRef.current + const scaleX = node.scaleX() + const scaleY = node.scaleY() + + node.scaleX(1) + node.scaleY(1) + + handleChange({ + dragging: false, + width: roundP(Math.max(5, layer.width * scaleX), 0), + height: roundP(Math.max(5, layer.height * scaleY), 0), + rotation: roundP(node.rotation(), 0), + }) + } } const handleWheel = (e) => { @@ -221,31 +217,39 @@ const PreviewLayer = (ownProps) => { } return ( - <> - {layer.visible && ( - - )} + + + {layer.visible && ( + + )} + {activeEffect && ( + + )} + {layer.visible && isCurrent && model.canChangeSize(layer) && ( { } /> )} - + ) } -export default React.memo(PreviewLayer) +export default React.memo(ShapePreview) From 22bad7bb93ce09a48224f9fbba5cc6c18723b26a Mon Sep 17 00:00:00 2001 From: Bob Carmichael Date: Mon, 14 Aug 2023 07:01:59 -0400 Subject: [PATCH 049/126] add tooltips to manager buttons --- package-lock.json | 23 +++++++++++++++++++++++ package.json | 1 + src/features/app/App.scss | 4 ++++ src/features/effects/EffectList.js | 6 +++++- src/features/effects/EffectManager.js | 12 ++++++++++-- src/features/effects/effectsSlice.spec.js | 1 + src/features/layers/LayerEditor.js | 2 +- src/features/layers/LayerList.js | 4 ++++ src/features/layers/LayerManager.js | 17 +++++++++++++---- 9 files changed, 62 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index d45603b6..1b25a68b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -43,6 +43,7 @@ "react-redux": "^8.1.2", "react-select": "^5.7.4", "react-switch": "^7.0.0", + "react-tooltip": "^5.20.0", "reduce-reducers": "^1.0.4", "redux": "^4.2.1", "redux-mock-store": "^1.5.4", @@ -10118,6 +10119,19 @@ "react-dom": "^15.3.0 || ^16.0.0 || ^17.0.0 || ^18.0.0" } }, + "node_modules/react-tooltip": { + "version": "5.20.0", + "resolved": "https://registry.npmjs.org/react-tooltip/-/react-tooltip-5.20.0.tgz", + "integrity": "sha512-LWBIHEZjwDW9ZJ/Dn2xeZrsz+WKMii61CIsx2XPfs1IiIRnWyvKJXrgy6uEGOXYvrnCd4jiEvurn8Y+zJ1bw5Q==", + "dependencies": { + "@floating-ui/dom": "^1.0.0", + "classnames": "^2.3.0" + }, + "peerDependencies": { + "react": ">=16.14.0", + "react-dom": ">=16.14.0" + } + }, "node_modules/react-transition-group": { "version": "4.4.5", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", @@ -19273,6 +19287,15 @@ "prop-types": "^15.7.2" } }, + "react-tooltip": { + "version": "5.20.0", + "resolved": "https://registry.npmjs.org/react-tooltip/-/react-tooltip-5.20.0.tgz", + "integrity": "sha512-LWBIHEZjwDW9ZJ/Dn2xeZrsz+WKMii61CIsx2XPfs1IiIRnWyvKJXrgy6uEGOXYvrnCd4jiEvurn8Y+zJ1bw5Q==", + "requires": { + "@floating-ui/dom": "^1.0.0", + "classnames": "^2.3.0" + } + }, "react-transition-group": { "version": "4.4.5", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", diff --git a/package.json b/package.json index 5aeb036a..446cea83 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "react-redux": "^8.1.2", "react-select": "^5.7.4", "react-switch": "^7.0.0", + "react-tooltip": "^5.20.0", "reduce-reducers": "^1.0.4", "redux": "^4.2.1", "redux-mock-store": "^1.5.4", diff --git a/src/features/app/App.scss b/src/features/app/App.scss index adef5970..7542ec1d 100644 --- a/src/features/app/App.scss +++ b/src/features/app/App.scss @@ -11,6 +11,10 @@ } } +.react-tooltip { + z-index: 9999; +} + .App { background-color: #eee; } diff --git a/src/features/effects/EffectList.js b/src/features/effects/EffectList.js index f5338714..6442f61d 100644 --- a/src/features/effects/EffectList.js +++ b/src/features/effects/EffectList.js @@ -1,6 +1,7 @@ import React from "react" import { useDispatch, useSelector } from "react-redux" import { Button, ListGroup } from "react-bootstrap" +import { Tooltip } from "react-tooltip" import { FaEye, FaEyeSlash } from "react-icons/fa" import { DndContext, useSensor, useSensors, PointerSensor } from "@dnd-kit/core" import { restrictToVerticalAxis } from "@dnd-kit/modifiers" @@ -44,6 +45,7 @@ const EffectRow = ({ {...listeners} {...attributes} > +
handleToggleEffectVisible(id)} > {visible ? : } @@ -112,7 +116,7 @@ const EffectList = ({ effects, currentEffect, selectedLayer }) => { {effects.map((effect, index) => ( diff --git a/src/features/effects/EffectManager.js b/src/features/effects/EffectManager.js index 00da9792..e871afb5 100644 --- a/src/features/effects/EffectManager.js +++ b/src/features/effects/EffectManager.js @@ -1,5 +1,6 @@ import React, { useState } from "react" import { Button } from "react-bootstrap" +import { Tooltip } from "react-tooltip" import { useSelector, useDispatch } from "react-redux" import { Card, Accordion } from "react-bootstrap" import { FaTrash, FaCopy, FaPlusSquare } from "react-icons/fa" @@ -73,28 +74,35 @@ const EffectManager = () => { selectedLayer={selectedLayer} />
+
+ + +
+ {canRemove && } {canRemove && ( )} +
-
+
diff --git a/src/features/layers/LayerEditor.js b/src/features/layers/LayerEditor.js index 8cabd0af..f88b4825 100644 --- a/src/features/layers/LayerEditor.js +++ b/src/features/layers/LayerEditor.js @@ -1,6 +1,6 @@ import React from "react" import { useDispatch, useSelector } from "react-redux" -import { Button, Card, Row, Col, Accordion } from "react-bootstrap" +import { Button, Row, Col } from "react-bootstrap" import Select from "react-select" import { IconContext } from "react-icons" import { AiOutlineRotateRight } from "react-icons/ai" @@ -113,110 +113,73 @@ const LayerEditor = () => { )) return ( -
- - - - - Layer - - - - - {getOptionComponent(model, layerOptions, "name")} - {model.canTransform(layer) && ( - - Transform - - {model.canMove(layer) && ( - - - {getOptionComponent(model, layerOptions, "x")} - - - {getOptionComponent(model, layerOptions, "y")} - - - )} - {model.canChangeSize(layer) && ( - - - {getOptionComponent(model, layerOptions, "width")} - - - {getOptionComponent(model, layerOptions, "height")} - - - )} - {model.canRotate(layer) && ( - - -
-
- - - -
- {getOptionComponent( - model, - layerOptions, - "rotation", - false, - )} -
- -
- )} +
+
+ {getOptionComponent(model, layerOptions, "name")} + + {renderedModelSelection} + {renderedModelOptions} + {renderedLink} + +
+
+ {model.canTransform(layer) && ( + + Transform + + {model.canMove(layer) && ( + + + {getOptionComponent(model, layerOptions, "x")} + + + {getOptionComponent(model, layerOptions, "y")} )} - {getOptionComponent(model, layerOptions, "reverse")} - {getOptionComponent(model, layerOptions, "connectionMethod")} - - - - + {model.canChangeSize(layer) && ( + + + {getOptionComponent(model, layerOptions, "width")} + + + {getOptionComponent(model, layerOptions, "height")} + + + )} + {model.canRotate(layer) && ( + + +
+
+ + + +
+ {getOptionComponent( + model, + layerOptions, + "rotation", + false, + )} +
+ +
+ )} + +
+ )} + {getOptionComponent(model, layerOptions, "reverse")} + {getOptionComponent(model, layerOptions, "connectionMethod")} - - - - - Shape - - - - - - {renderedModelSelection} - {renderedModelOptions} - {renderedLink} - - - - - + +
) } From e9f68ed8c11739bd8925c86c2c9b7f20eb13f064 Mon Sep 17 00:00:00 2001 From: Bob Carmichael Date: Thu, 17 Aug 2023 06:53:43 -0400 Subject: [PATCH 057/126] fix issue with selection after a deletion --- src/features/layers/layersSlice.js | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/features/layers/layersSlice.js b/src/features/layers/layersSlice.js index 1fee2332..095faa3d 100644 --- a/src/features/layers/layersSlice.js +++ b/src/features/layers/layersSlice.js @@ -526,9 +526,7 @@ export const deleteLayer = (id) => { const idx = deleteIdx === 0 ? 1 - : deleteIdx == ids.length - 1 - ? deleteIdx - 1 - : deleteIdx + : deleteIdx - 1 dispatch(setCurrentLayer(ids[idx])) } } @@ -583,9 +581,7 @@ export const deleteEffect = ({ id, effectId }) => { const idx = deleteIdx === 0 ? 1 - : deleteIdx == effectIds.length - 1 - ? deleteIdx - 1 - : deleteIdx + : deleteIdx - 1 dispatch(setCurrentEffect(effectIds[idx])) } } else { From 31eff822867700a361eca0d1175e8fd1c4abbd6e Mon Sep 17 00:00:00 2001 From: Bob Carmichael Date: Thu, 17 Aug 2023 07:13:45 -0400 Subject: [PATCH 058/126] various fixes --- src/features/layers/Layer.js | 12 ++++++------ src/features/preview/ShapePreview.js | 2 +- src/features/shapes/Shape.js | 1 + src/features/shapes/space_filler/SpaceFiller.js | 4 ++-- 4 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/features/layers/Layer.js b/src/features/layers/Layer.js index 44b6dc2f..7ceb4cf0 100644 --- a/src/features/layers/Layer.js +++ b/src/features/layers/Layer.js @@ -101,8 +101,11 @@ export default class Layer { this.state = layer this.vertices = this.model.getCachedVertices({ shape: layer, machine }) - this.resize() - centerOnOrigin(this.vertices) + if (this.model.autosize) { + this.resize() + centerOnOrigin(this.vertices) + } + this.applyEffects(effects) this.transform() @@ -111,10 +114,7 @@ export default class Layer { resize() { const { width, height } = this.state - - if (this.model.autosize) { - this.vertices = resizeVertices(this.vertices, width, height, false) - } + this.vertices = resizeVertices(this.vertices, width, height, false) } transform() { diff --git a/src/features/preview/ShapePreview.js b/src/features/preview/ShapePreview.js index 028a2128..cda25fb5 100644 --- a/src/features/preview/ShapePreview.js +++ b/src/features/preview/ShapePreview.js @@ -217,7 +217,7 @@ const ShapePreview = (ownProps) => { onTransformEnd={handleTransformEnd} onDragStart={handleDragStart} onDragEnd={handleDragEnd} - draggable={model.canMove && isCurrent} + draggable={model.canMove(layer) && isCurrent} > {layer.visible && ( Date: Thu, 17 Aug 2023 08:05:41 -0400 Subject: [PATCH 059/126] form-based submission in modals --- src/features/effects/NewEffect.js | 94 ++++++++++++++++------------- src/features/layers/CopyLayer.js | 63 ++++++++++---------- src/features/layers/NewLayer.js | 95 +++++++++++++++++------------- src/features/layers/layersSlice.js | 10 +--- 4 files changed, 140 insertions(+), 122 deletions(-) diff --git a/src/features/effects/NewEffect.js b/src/features/effects/NewEffect.js index 9e0195c6..ce4cd907 100644 --- a/src/features/effects/NewEffect.js +++ b/src/features/effects/NewEffect.js @@ -1,4 +1,4 @@ -import React, { useState } from "react" +import React, { useState, useRef } from "react" import { useDispatch, useSelector } from "react-redux" import Select from "react-select" import { Button, Modal, Row, Col, Form } from "react-bootstrap" @@ -21,6 +21,7 @@ const customStyles = { const NewEffect = ({ toggleModal, showModal }) => { const dispatch = useDispatch() + const selectRef = useRef() const selectedLayer = useSelector(selectSelectedLayer) const selectOptions = getEffectSelectOptions() const [type, setType] = useState(defaultEffect.type) @@ -35,6 +36,10 @@ const NewEffect = ({ toggleModal, showModal }) => { event.target.select() } + const handleInitialFocus = () => { + selectRef.current.focus() + } + const handleChangeNewType = (selected) => { const effect = getEffectFromType(selected.value) @@ -46,9 +51,10 @@ const NewEffect = ({ toggleModal, showModal }) => { setName(event.target.value) } - const onEffectAdded = () => { + const onEffectAdded = (event) => { const layer = new EffectLayer(type) + event.preventDefault() dispatch( addEffect({ id: selectedLayer.id, @@ -62,52 +68,56 @@ const NewEffect = ({ toggleModal, showModal }) => { Create new effect - - - Type - - + + + + Name + + + + + - - - - + + + + + ) } diff --git a/src/features/layers/CopyLayer.js b/src/features/layers/CopyLayer.js index 10293e17..7dd20f0e 100644 --- a/src/features/layers/CopyLayer.js +++ b/src/features/layers/CopyLayer.js @@ -18,7 +18,8 @@ const CopyLayer = ({ toggleModal, showModal }) => { event.target.select() } - const handleCopyLayer = () => { + const handleCopyLayer = (event) => { + event.preventDefault() dispatch( copyLayer({ id: selectedLayer.id, @@ -42,36 +43,38 @@ const CopyLayer = ({ toggleModal, showModal }) => { Copy {selectedLayer.name} - - - Name - - - - - +
+ + + Name + + + + + - - - - + + + + +
) } diff --git a/src/features/layers/NewLayer.js b/src/features/layers/NewLayer.js index eee88a84..cf6be4f4 100644 --- a/src/features/layers/NewLayer.js +++ b/src/features/layers/NewLayer.js @@ -1,4 +1,4 @@ -import React, { useState } from "react" +import React, { useState, useRef } from "react" import { useDispatch } from "react-redux" import Select from "react-select" import { Button, Modal, Row, Col, Form } from "react-bootstrap" @@ -21,6 +21,7 @@ const customStyles = { const NewLayer = ({ toggleModal, showModal }) => { const dispatch = useDispatch() + const selectRef = useRef() const selectOptions = getShapeSelectOptions() const [type, setType] = useState(defaultShape.type) const [name, setName] = useState(defaultShape.label) @@ -45,7 +46,13 @@ const NewLayer = ({ toggleModal, showModal }) => { setName(event.target.value) } - const onLayerAdded = () => { + const handleInitialFocus = () => { + selectRef.current.focus() + } + + const onLayerAdded = (event) => { + event.preventDefault() + const layer = new Layer(type) const attrs = layer.getInitialState() @@ -58,52 +65,56 @@ const NewLayer = ({ toggleModal, showModal }) => { Create new layer - - - Type - - + + + + Name + + + + + - - - - + + + + + ) } diff --git a/src/features/layers/layersSlice.js b/src/features/layers/layersSlice.js index 095faa3d..6e705367 100644 --- a/src/features/layers/layersSlice.js +++ b/src/features/layers/layersSlice.js @@ -523,10 +523,7 @@ export const deleteLayer = (id) => { dispatch(layersSlice.actions.deleteLayer(id)) if (id === selectedLayerId) { - const idx = - deleteIdx === 0 - ? 1 - : deleteIdx - 1 + const idx = deleteIdx === 0 ? 1 : deleteIdx - 1 dispatch(setCurrentLayer(ids[idx])) } } @@ -578,10 +575,7 @@ export const deleteEffect = ({ id, effectId }) => { if (effectIds.length > 1) { if (effectId === selectedEffectId) { - const idx = - deleteIdx === 0 - ? 1 - : deleteIdx - 1 + const idx = deleteIdx === 0 ? 1 : deleteIdx - 1 dispatch(setCurrentEffect(effectIds[idx])) } } else { From c874bd312fc06603c092bc390362a817533ca15e Mon Sep 17 00:00:00 2001 From: Bob Carmichael Date: Fri, 18 Aug 2023 06:39:00 -0400 Subject: [PATCH 060/126] bug fixes --- src/common/Model.js | 2 +- src/features/effects/Effect.js | 6 ++++-- src/features/effects/EffectEditor.js | 5 +++-- src/features/effects/EffectList.js | 13 +++++++++---- src/features/effects/EffectManager.js | 7 +++---- src/features/effects/effectsSlice.js | 16 ++++++++++++++-- src/features/layers/LayerList.js | 12 ++++++++---- src/features/layers/layersSlice.js | 23 ++++++++++++++--------- src/features/layers/layersSlice.spec.js | 12 +++++------- src/features/shapes/Shape.js | 3 ++- 10 files changed, 63 insertions(+), 36 deletions(-) diff --git a/src/common/Model.js b/src/common/Model.js index bcb8b667..0bcd1d9b 100644 --- a/src/common/Model.js +++ b/src/common/Model.js @@ -38,7 +38,7 @@ export default class Model { ) } - // redux state of a newly created instance + // override as needed; redux state of a newly created instance getInitialState() { return {} } diff --git a/src/features/effects/Effect.js b/src/features/effects/Effect.js index cb6259e0..54e34788 100644 --- a/src/features/effects/Effect.js +++ b/src/features/effects/Effect.js @@ -18,12 +18,14 @@ export default class Effect extends Model { return false } - // override as needed; returns an array of Victor vertices + // override as needed; returns an array of Victor vertices that are used to + // render a Konva transformer when the effect is selected getSelectionVertices(effect) { return [] } - // override as needed + // override as needed; returns an array of Victor vertices that are the result + // of applying the effect to the layer getVertices(effect, layer, vertices) { return [] } diff --git a/src/features/effects/EffectEditor.js b/src/features/effects/EffectEditor.js index 5f57e290..2b1d5db8 100644 --- a/src/features/effects/EffectEditor.js +++ b/src/features/effects/EffectEditor.js @@ -14,8 +14,9 @@ import { selectSelectedEffect } from "./effectsSlice" const EffectEditor = ({ id }) => { const dispatch = useDispatch() const effect = useSelector(selectSelectedEffect) - const instance = new EffectLayer(effect.type) - const model = new EffectLayer(effect.type).model + const type = effect?.type || "mask" // guard zombie child + const instance = new EffectLayer(type) + const model = new EffectLayer(type).model const layerOptions = instance.getOptions() const modelOptions = model.getOptions() diff --git a/src/features/effects/EffectList.js b/src/features/effects/EffectList.js index 3b12779e..8e5d4ee4 100644 --- a/src/features/effects/EffectList.js +++ b/src/features/effects/EffectList.js @@ -57,7 +57,7 @@ const EffectRow = ({ data-id={id} data-tooltip-content={visible ? "Hide effect" : "Show effect"} data-tooltip-id="tooltip-toggle-visible" - onClick={() => handleToggleEffectVisible(id)} + onClick={() => handleToggleEffectVisible(id, effect.visible)} > {visible ? : } @@ -69,7 +69,7 @@ const EffectRow = ({ ) } -const EffectList = ({ effects, currentEffect, selectedLayer }) => { +const EffectList = ({ effects, selectedLayer }) => { const dispatch = useDispatch() const currentEffectId = useSelector(selectCurrentEffectId) const selectedEffectId = useSelector(selectSelectedEffectId) @@ -87,6 +87,9 @@ const EffectList = ({ effects, currentEffect, selectedLayer }) => { const handleDragStart = ({ active }) => dispatch(setCurrentEffect(active.id)) 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) @@ -94,8 +97,10 @@ const EffectList = ({ effects, currentEffect, selectedLayer }) => { } } - const handleToggleEffectVisible = (id) => - dispatch(updateEffect({ id, visible: !currentEffect.visible })) + const handleToggleEffectVisible = (id, visible) => { + dispatch(setCurrentEffect(id)) + dispatch(updateEffect({ id, visible: !visible })) + } const handleEffectSelected = (event) => { const id = event.target.closest(".list-group-item").id diff --git a/src/features/effects/EffectManager.js b/src/features/effects/EffectManager.js index 1e3f06c8..fb2aca2b 100644 --- a/src/features/effects/EffectManager.js +++ b/src/features/effects/EffectManager.js @@ -9,7 +9,7 @@ import { deleteEffect, selectLayerEffects, } from "@/features/layers/layersSlice" -import { selectCurrentEffect } from "./effectsSlice" +import { selectSelectedEffect } from "./effectsSlice" import EffectEditor from "./EffectEditor" import EffectList from "./EffectList" import NewEffect from "./NewEffect" @@ -17,7 +17,7 @@ import NewEffect from "./NewEffect" const EffectManager = () => { const dispatch = useDispatch() const selectedLayer = useSelector(selectSelectedLayer) - const currentEffect = useSelector(selectCurrentEffect) + const selectedEffect = useSelector(selectSelectedEffect) const effects = useSelector((state) => selectLayerEffects(state, selectedLayer.id), ) @@ -29,7 +29,7 @@ const EffectManager = () => { dispatch( deleteEffect({ id: selectedLayer.id, - effectId: currentEffect.id, + effectId: selectedEffect.id, }), ) } @@ -70,7 +70,6 @@ const EffectManager = () => {
diff --git a/src/features/effects/effectsSlice.js b/src/features/effects/effectsSlice.js index de57aa42..5f3e980c 100644 --- a/src/features/effects/effectsSlice.js +++ b/src/features/effects/effectsSlice.js @@ -58,12 +58,24 @@ export const effectsSlice = createSlice({ state.selected = id } }, + setSelectedEffect: (state, action) => { + const id = action.payload + + if (state.entities[id]) { + state.selected = id + } + }, }, }) export default effectsSlice.reducer -export const { addEffect, deleteEffect, updateEffect, setCurrentEffect } = - effectsSlice.actions +export const { + addEffect, + deleteEffect, + updateEffect, + setCurrentEffect, + setSelectedEffect, +} = effectsSlice.actions // ------------------------------ // Selectors diff --git a/src/features/layers/LayerList.js b/src/features/layers/LayerList.js index 4e64a253..d7a02a34 100644 --- a/src/features/layers/LayerList.js +++ b/src/features/layers/LayerList.js @@ -65,8 +65,8 @@ const LayerRow = ({ data-id={id} data-tooltip-content={visible ? "Hide layer" : "Show layer"} data-tooltip-id="tooltip-toggle-visible" - onClick={() => { - handleToggleLayerVisible(id) + onClick={(e) => { + handleToggleLayerVisible(id, layer.visible) }} > {visible ? : } @@ -110,11 +110,15 @@ const LayerList = () => { const handleDragStart = ({ active }) => dispatch(setCurrentLayer(active.id)) - const handleToggleLayerVisible = (id) => { - dispatch(updateLayer({ id, visible: !selectedLayer.visible })) + const handleToggleLayerVisible = (id, visible) => { + dispatch(setCurrentLayer(id)) + dispatch(updateLayer({ id, visible: !visible })) } 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) diff --git a/src/features/layers/layersSlice.js b/src/features/layers/layersSlice.js index 6e705367..429df835 100644 --- a/src/features/layers/layersSlice.js +++ b/src/features/layers/layersSlice.js @@ -15,6 +15,7 @@ import Layer from "./Layer" import { selectState } from "@/features/app/appSlice" import { effectsSlice, + setSelectedEffect, selectEffectById, selectEffectsByLayerId, selectCurrentEffect, @@ -62,18 +63,15 @@ const layersSlice = createSlice({ const index = state.selected ? currSelectedIndex(state) + 1 : 0 const layer = { ...action.payload, - effectIds: [], } + layer.effectIds = [] state.ids.splice(index, 0, layer.id) state.entities[layer.id] = layer state.current = layer.id state.selected = layer.id if (layer.type !== "fileImport") { - localStorage.setItem( - layer.effect ? "defaultEffect" : "defaultShape", - layer.type, - ) + localStorage.setItem("defaultShape", layer.type) } }, prepare(layer) { @@ -538,14 +536,17 @@ export const copyLayer = ({ id, name }) => { name, } + // create new layer + const action = dispatch(layersSlice.actions.addLayer(newLayer)) + const newId = action.meta.id + // copy effects - newLayer.effectIds = layer.effectIds.map((effectId) => { + layer.effectIds.map((effectId) => { const effect = selectEffectById(state, effectId) - return dispatch(effectsSlice.actions.addEffect(effect)).meta.id + return dispatch(addEffect({ id: newId, effect })) }) - // create new layer - dispatch(layersSlice.actions.addLayer(newLayer)) + dispatch(setCurrentLayer(newId)) } } @@ -586,8 +587,12 @@ export const deleteEffect = ({ id, effectId }) => { export const setCurrentLayer = (id) => { return (dispatch, getState) => { + const state = getState() + const layer = selectLayerById(state, id) + dispatch(layersSlice.actions.setCurrentLayer(id)) dispatch(effectsSlice.actions.setCurrentEffect(null)) + dispatch(setSelectedEffect(layer?.effectIds[0])) // this guard is a hack to get a test to run } } diff --git a/src/features/layers/layersSlice.spec.js b/src/features/layers/layersSlice.spec.js index eb083804..65c6dce0 100644 --- a/src/features/layers/layersSlice.spec.js +++ b/src/features/layers/layersSlice.spec.js @@ -407,14 +407,12 @@ describe("layers reducer", () => { }), ) const actions = store.getActions() - expect(actions[0].type).toEqual("effects/addEffect") + expect(actions[0].type).toEqual("layers/addLayer") expect(actions[1].type).toEqual("effects/addEffect") - expect(actions[2].type).toEqual("layers/addLayer") - expect(actions[2].payload).toEqual({ - id: "3", - name: "bar", - effectIds: ["1", "2"], - }) + expect(actions[2].type).toEqual("layers/addEffect") + expect(actions[3].type).toEqual("effects/setCurrentEffect") + expect(actions[4].type).toEqual("layers/setCurrentLayer") + expect(actions[5].type).toEqual("effects/addEffect") }) }) diff --git a/src/features/shapes/Shape.js b/src/features/shapes/Shape.js index 7a844ed4..28c09ac3 100644 --- a/src/features/shapes/Shape.js +++ b/src/features/shapes/Shape.js @@ -63,7 +63,8 @@ export default class Shape extends Model { return JSON.stringify(cacheState) } - // override as needed; returns an array of Victor vertices + // override as needed; returns an array of Victor vertices that render + // the shape with the specific options getVertices(state) { return [] } From d072c51a09f4dd0a25e74b94ff80e3694b284711 Mon Sep 17 00:00:00 2001 From: Bob Carmichael Date: Fri, 25 Aug 2023 17:33:42 -0400 Subject: [PATCH 061/126] new transformer effect; much improved rendering of layers/effects while user interacts with preview window --- src/features/app/Header.scss | 2 +- src/features/app/bootstrap.scss | 2 +- src/features/effects/Effect.js | 5 + src/features/effects/EffectLayer.js | 4 +- src/features/effects/FineTuning.js | 12 ++ src/features/effects/NewEffect.js | 11 +- src/features/effects/Transformer.js | 79 +++++++++++ src/features/effects/factory.js | 2 + src/features/layers/Layer.js | 12 -- src/features/layers/LayerEditor.js | 1 - src/features/layers/LayerList.js | 2 +- src/features/layers/layersSlice.js | 159 +++++++++++++++++++++-- src/features/machine/computer.js | 102 --------------- src/features/preview/ConnectorPreview.js | 68 +++++++--- src/features/preview/EffectPreview.js | 44 ++++++- src/features/preview/PreviewManager.js | 87 ++++++++----- src/features/preview/PreviewManager.scss | 7 +- src/features/preview/PreviewWindow.js | 70 ++++++---- src/features/preview/ShapePreview.js | 154 +++++++++++++++++++--- src/index.js | 6 + 20 files changed, 598 insertions(+), 231 deletions(-) create mode 100644 src/features/effects/Transformer.js delete mode 100644 src/features/machine/computer.js diff --git a/src/features/app/Header.scss b/src/features/app/Header.scss index 5c6e1ac1..93d5a1a3 100644 --- a/src/features/app/Header.scss +++ b/src/features/app/Header.scss @@ -1,5 +1,5 @@ header { - background-color: #2d6da4; + background-color: #2983BA; padding: 1rem; color: white; text-align: center; diff --git a/src/features/app/bootstrap.scss b/src/features/app/bootstrap.scss index bc912fa3..7a027741 100644 --- a/src/features/app/bootstrap.scss +++ b/src/features/app/bootstrap.scss @@ -46,7 +46,7 @@ h2 { } .list-group-item.active { - background-color: lighten(#2d6da4, 5%); + background-color: lighten(#2983BA, 5%); } .list-group-item.selected { diff --git a/src/features/effects/Effect.js b/src/features/effects/Effect.js index 54e34788..21b1ad2e 100644 --- a/src/features/effects/Effect.js +++ b/src/features/effects/Effect.js @@ -3,6 +3,11 @@ import Model from "@/common/Model" const effectOptions = [] export default class Effect extends Model { + constructor(type, state) { + super(type, state) + this.dragPreview = false + } + // override as needed canChangeSize(state) { return false diff --git a/src/features/effects/EffectLayer.js b/src/features/effects/EffectLayer.js index 3a30065f..b6e07d2c 100644 --- a/src/features/effects/EffectLayer.js +++ b/src/features/effects/EffectLayer.js @@ -53,9 +53,9 @@ export default class EffectLayer { this.model = getEffectFromType(type) } - getInitialState(props) { + getInitialState(layer, layerVertices) { return { - ...this.model.getInitialState(props), + ...this.model.getInitialState(layer, layerVertices), ...{ type: this.model.type, visible: true, diff --git a/src/features/effects/FineTuning.js b/src/features/effects/FineTuning.js index 691b7647..e2c4855d 100644 --- a/src/features/effects/FineTuning.js +++ b/src/features/effects/FineTuning.js @@ -28,6 +28,13 @@ const options = { max: 100, step: 2, }, + reverse: { + title: "Reverse path", + type: "checkbox", + isVisible: (model, state) => { + return !model.effect + }, + }, } export default class FineTuning extends Effect { @@ -56,11 +63,16 @@ export default class FineTuning extends Effect { drawPortionPct: 100, backtrackPct: 0, rotateStartingPct: 0, + reverse: false, }, } } getVertices(effect, layer, vertices) { + if (effect.reverse) { + vertices = vertices.reverse() + } + if ( effect.rotateStartingPct === undefined || effect.rotateStartingPct !== 0 diff --git a/src/features/effects/NewEffect.js b/src/features/effects/NewEffect.js index ce4cd907..7a6e712a 100644 --- a/src/features/effects/NewEffect.js +++ b/src/features/effects/NewEffect.js @@ -2,7 +2,11 @@ import React, { useState, useRef } from "react" import { useDispatch, useSelector } from "react-redux" import Select from "react-select" import { Button, Modal, Row, Col, Form } from "react-bootstrap" -import { selectSelectedLayer, addEffect } from "@/features/layers/layersSlice" +import { + selectSelectedLayer, + addEffect, + selectLayerVertices, +} from "@/features/layers/layersSlice" import { getEffectSelectOptions, getDefaultEffect, @@ -23,6 +27,9 @@ const NewEffect = ({ toggleModal, showModal }) => { const dispatch = useDispatch() const selectRef = useRef() const selectedLayer = useSelector(selectSelectedLayer) + const selectedLayerVertices = useSelector((state) => + selectLayerVertices(state, selectedLayer.id), + ) const selectOptions = getEffectSelectOptions() const [type, setType] = useState(defaultEffect.type) const [name, setName] = useState(defaultEffect.label) @@ -58,7 +65,7 @@ const NewEffect = ({ toggleModal, showModal }) => { dispatch( addEffect({ id: selectedLayer.id, - effect: layer.getInitialState(), + effect: layer.getInitialState(selectedLayer, selectedLayerVertices), }), ) toggleModal() diff --git a/src/features/effects/Transformer.js b/src/features/effects/Transformer.js new file mode 100644 index 00000000..796f7c08 --- /dev/null +++ b/src/features/effects/Transformer.js @@ -0,0 +1,79 @@ +import { + resizeVertices, + dimensions, + centerOnOrigin, + findBounds, +} from "@/common/geometry" +import Effect from "./Effect" + +const options = {} + +export default class Transformer extends Effect { + constructor() { + super("transformer") + this.dragPreview = true + this.label = "Transformer" + } + + canMove(state) { + return true + } + + canRotate(state) { + return true + } + + canChangeSize(state) { + return true + } + + getInitialState(layer, layerVertices) { + // reverse rotation before calculating bounds + const vertices = [...layerVertices] + vertices.forEach((vertex) => { + vertex.rotateDeg(layer.rotation) + }) + + const bounds = findBounds(layerVertices) + const { width, height } = dimensions(vertices) + const offsetX = (bounds[1].x + bounds[0].x) / 2 + const offsetY = (bounds[1].y + bounds[0].y) / 2 + + return { + ...super.getInitialState(), + ...{ + type: "transformer", + width, + height, + x: offsetX - layer.x, + y: offsetY - layer.y, + rotation: 0, + }, + } + } + + getVertices(effect, layer, vertices) { + this.state = effect + this.effect = effect + this.vertices = [...vertices] + + resizeVertices(this.vertices, effect.width, effect.height, false) + centerOnOrigin(this.vertices) + this.transform() + + return vertices + } + + transform() { + const { x, y, rotation } = this.state + + this.vertices.forEach((vertex) => { + vertex.rotateDeg(-rotation) + vertex.addX({ x }).addY({ y }) + }) + } + + getOptions() { + return options + } +} diff --git a/src/features/effects/factory.js b/src/features/effects/factory.js index bd0d6142..2ec35a2f 100644 --- a/src/features/effects/factory.js +++ b/src/features/effects/factory.js @@ -4,6 +4,7 @@ import Loop from "./Loop" import Mask from "./Mask" import Noise from "./Noise" import Track from "./Track" +import Transformer from "./Transformer" import Warp from "./Warp" export const effectFactory = { @@ -13,6 +14,7 @@ export const effectFactory = { mask: Mask, noise: Noise, track: Track, + transformer: Transformer, warp: Warp, } diff --git a/src/features/layers/Layer.js b/src/features/layers/Layer.js index 7ceb4cf0..7c3f66f3 100644 --- a/src/features/layers/Layer.js +++ b/src/features/layers/Layer.js @@ -45,13 +45,6 @@ export const layerOptions = { return model.canChangeSize(state) && model.canChangeHeight(state) }, }, - reverse: { - title: "Reverse path", - type: "checkbox", - isVisible: (model, state) => { - return !model.effect - }, - }, rotation: { title: "Rotate (degrees)", inline: true, @@ -84,7 +77,6 @@ export default class Layer { width: dimensions.width, height: dimensions.height, rotation: 0, - reverse: false, visible: true, name: this.model.label, effectIds: [], @@ -124,10 +116,6 @@ export default class Layer { vertex.rotateDeg(-rotation) vertex.addX({ x: x || 0 }).addY({ y: y || 0 }) }) - - if (this.state.reverse) { - this.vertices = this.vertices.reverse() - } } applyEffects(effects) { diff --git a/src/features/layers/LayerEditor.js b/src/features/layers/LayerEditor.js index f88b4825..0755a82b 100644 --- a/src/features/layers/LayerEditor.js +++ b/src/features/layers/LayerEditor.js @@ -169,7 +169,6 @@ const LayerEditor = () => { )} - {getOptionComponent(model, layerOptions, "reverse")} {getOptionComponent(model, layerOptions, "connectionMethod")} + + + + + ) +} + +export default CopyEffect diff --git a/src/features/effects/EffectManager.js b/src/features/effects/EffectManager.js index fb2aca2b..8a15b080 100644 --- a/src/features/effects/EffectManager.js +++ b/src/features/effects/EffectManager.js @@ -13,6 +13,7 @@ import { selectSelectedEffect } from "./effectsSlice" import EffectEditor from "./EffectEditor" import EffectList from "./EffectList" import NewEffect from "./NewEffect" +import CopyEffect from "./CopyEffect" const EffectManager = () => { const dispatch = useDispatch() @@ -23,7 +24,10 @@ const EffectManager = () => { ) const numEffects = effects.length const [showNewEffect, setShowNewEffect] = useState(false) + const [showCopyEffect, setShowCopyEffect] = useState(false) + const toggleNewEffectModal = () => setShowNewEffect(!showNewEffect) + const toggleCopyModal = () => setShowCopyEffect(!showCopyEffect) const handleEffectDeleted = (id) => { dispatch( @@ -40,7 +44,10 @@ const EffectManager = () => { showModal={showNewEffect} toggleModal={toggleNewEffectModal} /> - + {numEffects == 0 && ( diff --git a/src/features/layers/CopyLayer.js b/src/features/layers/CopyLayer.js index 7dd20f0e..23eb88b5 100644 --- a/src/features/layers/CopyLayer.js +++ b/src/features/layers/CopyLayer.js @@ -1,4 +1,4 @@ -import React, { useRef, useState } from "react" +import React, { useRef, useState, useEffect } from "react" import { Button, Modal, Row, Col, Form } from "react-bootstrap" import { useDispatch, useSelector } from "react-redux" import { copyLayer } from "./layersSlice" @@ -10,6 +10,10 @@ const CopyLayer = ({ toggleModal, showModal }) => { const namedInputRef = useRef(null) const [copyLayerName, setCopyLayerName] = useState(selectedLayer.name) + useEffect(() => { + setCopyLayerName(selectedLayer.name) + }, [selectedLayer]) + const handleChangeCopyLayerName = (event) => { setCopyLayerName(event.target.value) } diff --git a/src/features/preview/ShapePreview.js b/src/features/preview/ShapePreview.js index 1e3a701e..f9e03b28 100644 --- a/src/features/preview/ShapePreview.js +++ b/src/features/preview/ShapePreview.js @@ -343,7 +343,7 @@ const ShapePreview = (ownProps) => { x={layer.x || 0} y={-layer.y || 0} rotation={layer.rotation || 0} - key={`group-id`} + key={`group-${id}`} > Date: Sat, 26 Aug 2023 17:26:11 -0400 Subject: [PATCH 066/126] restore defaults --- src/features/effects/CopyEffect.js | 10 +++++--- src/features/effects/EffectManager.js | 17 ++++++++++++- src/features/effects/effectsSlice.js | 13 ++++++++++ src/features/effects/effectsSlice.spec.js | 31 +++++++++++++++++++++++ src/features/layers/LayerEditor.js | 16 ++---------- src/features/layers/LayerManager.js | 20 ++++++++++++++- src/features/layers/LayerManager.scss | 3 ++- src/features/layers/layersSlice.js | 4 +-- 8 files changed, 91 insertions(+), 23 deletions(-) diff --git a/src/features/effects/CopyEffect.js b/src/features/effects/CopyEffect.js index 2f6157d8..d4668057 100644 --- a/src/features/effects/CopyEffect.js +++ b/src/features/effects/CopyEffect.js @@ -8,10 +8,12 @@ const CopyEffect = ({ toggleModal, showModal }) => { const dispatch = useDispatch() const selectedEffect = useSelector(selectSelectedEffect) const namedInputRef = useRef(null) - const [copyEffectName, setCopyEffectName] = useState(selectedEffect?.name || '') + const [copyEffectName, setCopyEffectName] = useState( + selectedEffect?.name || "", + ) useEffect(() => { - setCopyEffectName(selectedEffect?.name || '') + setCopyEffectName(selectedEffect?.name || "") }, [selectedEffect]) const handleChangeCopyEffectName = (event) => { @@ -30,7 +32,7 @@ const CopyEffect = ({ toggleModal, showModal }) => { effect: { ...selectedEffect, name: copyEffectName, - } + }, }), ) toggleModal() @@ -47,7 +49,7 @@ const CopyEffect = ({ toggleModal, showModal }) => { onEntered={handleInitialFocus} > - Copy {selectedEffect?.name || ''} + Copy {selectedEffect?.name || ""}
diff --git a/src/features/effects/EffectManager.js b/src/features/effects/EffectManager.js index 8a15b080..2eaff63b 100644 --- a/src/features/effects/EffectManager.js +++ b/src/features/effects/EffectManager.js @@ -4,12 +4,13 @@ import { Tooltip } from "react-tooltip" import { useSelector, useDispatch } from "react-redux" import { Card, Accordion } from "react-bootstrap" import { FaTrash, FaCopy, FaPlusSquare } from "react-icons/fa" +import { MdOutlineSettingsBackupRestore } from "react-icons/md" import { selectSelectedLayer, deleteEffect, selectLayerEffects, } from "@/features/layers/layersSlice" -import { selectSelectedEffect } from "./effectsSlice" +import { selectSelectedEffect, restoreDefaults } from "./effectsSlice" import EffectEditor from "./EffectEditor" import EffectList from "./EffectList" import NewEffect from "./NewEffect" @@ -38,6 +39,10 @@ const EffectManager = () => { ) } + const handleRestoreDefaults = () => { + dispatch(restoreDefaults(selectedEffect.id)) + } + return (
{ > + +
diff --git a/src/features/effects/effectsSlice.js b/src/features/effects/effectsSlice.js index 5f3e980c..41816b2c 100644 --- a/src/features/effects/effectsSlice.js +++ b/src/features/effects/effectsSlice.js @@ -65,6 +65,18 @@ export const effectsSlice = createSlice({ state.selected = id } }, + restoreDefaults: (state, action) => { + const id = action.payload + const { type, name, layerId } = state.entities[id] + const layer = new EffectLayer(type) + + effectsAdapter.setOne(state, { + id, + name, + layerId, + ...layer.getInitialState(), + }) + }, }, }) @@ -75,6 +87,7 @@ export const { updateEffect, setCurrentEffect, setSelectedEffect, + restoreDefaults, } = effectsSlice.actions // ------------------------------ diff --git a/src/features/effects/effectsSlice.spec.js b/src/features/effects/effectsSlice.spec.js index bc6f8d9c..b11f1e2d 100644 --- a/src/features/effects/effectsSlice.spec.js +++ b/src/features/effects/effectsSlice.spec.js @@ -5,7 +5,9 @@ import effectsReducer, { setCurrentEffect, selectEffectsByLayerId, updateEffect, + restoreDefaults, } from "./effectsSlice" +import EffectLayer from "./EffectLayer" beforeEach(() => { resetUniqueId() @@ -16,6 +18,8 @@ beforeEach(() => { // ------------------------------ describe("effects reducer", () => { + const maskState = new EffectLayer("mask").getInitialState() + it("should handle initial state", () => { expect(effectsReducer(undefined, {})).toEqual({ ids: [], @@ -113,6 +117,33 @@ describe("effects reducer", () => { selected: "1", }) }) + + it("should handle restoreDefaults", () => { + expect( + effectsReducer( + { + entities: { + 0: { + id: "0", + name: "foo", + type: "mask", + height: "26", + width: "26", + }, + }, + }, + restoreDefaults("0"), + ), + ).toEqual({ + entities: { + 0: { + id: "0", + name: "foo", + ...maskState, + }, + }, + }) + }) }) // ------------------------------ diff --git a/src/features/layers/LayerEditor.js b/src/features/layers/LayerEditor.js index 0755a82b..0fb74c34 100644 --- a/src/features/layers/LayerEditor.js +++ b/src/features/layers/LayerEditor.js @@ -1,6 +1,6 @@ import React from "react" import { useDispatch, useSelector } from "react-redux" -import { Button, Row, Col } from "react-bootstrap" +import { Row, Col } from "react-bootstrap" import Select from "react-select" import { IconContext } from "react-icons" import { AiOutlineRotateRight } from "react-icons/ai" @@ -10,7 +10,7 @@ import DropdownOption from "@/components/DropdownOption" import CheckboxOption from "@/components/CheckboxOption" import ToggleButtonOption from "@/components/ToggleButtonOption" import { getShapeSelectOptions } from "@/features/shapes/factory" -import { updateLayer, changeModelType, restoreDefaults } from "./layersSlice" +import { updateLayer, changeModelType } from "./layersSlice" import Layer from "./Layer" import EffectManager from "@/features/effects/EffectManager" import { selectSelectedLayer } from "./layersSlice" @@ -53,10 +53,6 @@ const LayerEditor = () => { dispatch(updateLayer(attrs)) } - const handleRestoreDefaults = () => { - dispatch(restoreDefaults(layer.id)) - } - const renderedModelSelection = allowModelSelection && ( { )} {getOptionComponent(model, layerOptions, "connectionMethod")} - -
) diff --git a/src/features/layers/LayerManager.js b/src/features/layers/LayerManager.js index 8c61886b..cf3e6acb 100644 --- a/src/features/layers/LayerManager.js +++ b/src/features/layers/LayerManager.js @@ -3,11 +3,15 @@ import { Button } from "react-bootstrap" import { Tooltip } from "react-tooltip" import { useSelector, useDispatch } from "react-redux" import { FaTrash, FaCopy, FaPlusSquare } from "react-icons/fa" -import { MdOutlineFileUpload } from "react-icons/md" +import { + MdOutlineFileUpload, + MdOutlineSettingsBackupRestore, +} from "react-icons/md" import LayerEditor from "@/features/layers/LayerEditor" import { selectSelectedLayerId, selectNumLayers, + restoreDefaults, } from "@/features/layers/layersSlice" import { deleteLayer } from "@/features/layers/layersSlice" import NewLayer from "./NewLayer" @@ -38,6 +42,10 @@ const LayerManager = () => { } }, [numLayers]) + const handleRestoreDefaults = () => { + dispatch(restoreDefaults(selectedLayerId)) + } + return (
{ > + +
diff --git a/src/features/layers/LayerManager.scss b/src/features/layers/LayerManager.scss index 156e1ada..7ee6fe0b 100644 --- a/src/features/layers/LayerManager.scss +++ b/src/features/layers/LayerManager.scss @@ -1,5 +1,6 @@ .layer-button { - color: #aaa; + padding-left: 0.5rem !important; + padding-right: 0.5rem !important; .active.list-group-item & { color: white; diff --git a/src/features/layers/layersSlice.js b/src/features/layers/layersSlice.js index fe1d8f00..686a6eb0 100644 --- a/src/features/layers/layersSlice.js +++ b/src/features/layers/layersSlice.js @@ -135,13 +135,13 @@ const layersSlice = createSlice({ }, restoreDefaults: (state, action) => { const id = action.payload - const { type, name } = state.entities[id] + const { type, name, effectIds } = state.entities[id] const layer = new Layer(type) - layer.getInitialState() layersAdapter.setOne(state, { id, name, + effectIds, ...layer.getInitialState(), }) }, From 8259735c0bf65a90796f068da38d3dd4180572ac Mon Sep 17 00:00:00 2001 From: Bob Carmichael Date: Sun, 27 Aug 2023 07:31:10 -0400 Subject: [PATCH 067/126] fixes --- src/features/layers/LayerEditor.js | 2 +- src/features/layers/layersSlice.js | 2 +- src/features/preview/EffectPreview.js | 4 ++-- src/features/preview/ShapePreview.js | 14 +++++++------- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/features/layers/LayerEditor.js b/src/features/layers/LayerEditor.js index 0fb74c34..7e455daf 100644 --- a/src/features/layers/LayerEditor.js +++ b/src/features/layers/LayerEditor.js @@ -31,7 +31,7 @@ const LayerEditor = () => { const link = model.link const linkText = model.linkText || "here" const renderedLink = link ? ( -
+
See{" "} { diff --git a/src/features/preview/EffectPreview.js b/src/features/preview/EffectPreview.js index 850514e7..aae92745 100644 --- a/src/features/preview/EffectPreview.js +++ b/src/features/preview/EffectPreview.js @@ -27,12 +27,12 @@ const EffectPreview = (ownProps) => { (state) => selectEffectById(state, ownProps.id), isEqual, ) - const layer = useSelector((state) => selectLayerById(state, effect.layerId)) + const layer = useSelector((state) => selectLayerById(state, effect?.layerId)) const vertices = useSelector((state) => selectEffectSelectionVertices(state, ownProps.id), ) const draggingVertices = useSelector((state) => - selectDraggingEffectVertices(state, effect.layerId, ownProps.id), + selectDraggingEffectVertices(state, effect?.layerId, ownProps.id), ) const upstreamIsDragging = useSelector((state) => selectIsUpstreamEffectDragging(state, ownProps.id), diff --git a/src/features/preview/ShapePreview.js b/src/features/preview/ShapePreview.js index f9e03b28..033ce0db 100644 --- a/src/features/preview/ShapePreview.js +++ b/src/features/preview/ShapePreview.js @@ -46,12 +46,6 @@ const ShapePreview = (ownProps) => { (state) => selectActiveEffect(state, ownProps.id), isEqual, ) - const remainingEffectIds = layer.effectIds.filter( - (id) => id !== activeEffect?.id, - ) - const activeEffectInstance = activeEffect - ? new EffectLayer(activeEffect.type) - : null const index = useSelector((state) => selectLayerIndex(state, ownProps.id)) const numLayers = useSelector(selectNumVisibleLayers) @@ -77,6 +71,12 @@ const ShapePreview = (ownProps) => { isEqual, ) + const remainingEffectIds = layer + ? layer.effectIds.filter((id) => id !== activeEffect?.id) + : [] + const activeEffectInstance = activeEffect + ? new EffectLayer(activeEffect.type) + : null const shapeRef = React.useRef() const groupRef = React.useRef() const trRef = React.useRef() @@ -234,7 +234,7 @@ const ShapePreview = (ownProps) => { drawLayerVertices(context, sliderBounds) } - if (isCurrent) { + if (isCurrent || activeEffect) { drawLayerStartAndEndPoints(context) } else if (!currentLayerId && !currentEffectId) { drawStartAndEndPoints(context) From fb1dbc33d4208bcaade930fc4d772115d925cac5 Mon Sep 17 00:00:00 2001 From: Bob Carmichael Date: Sun, 27 Aug 2023 10:53:56 -0400 Subject: [PATCH 068/126] bootstrap 5 upgrade --- package-lock.json | 335 +++++++++++------------ package.json | 4 +- src/components/CheckboxOption.js | 4 +- src/components/DropdownOption.js | 4 +- src/components/InputOption.js | 6 +- src/components/ToggleButtonOption.js | 13 +- src/features/app/App.js | 6 +- src/features/app/App.scss | 7 - src/features/app/Header.js | 4 +- src/features/app/Notice.js | 2 +- src/features/app/Tabs.js | 3 +- src/features/app/bootstrap.scss | 37 +-- src/features/effects/CopyEffect.js | 6 +- src/features/effects/EffectEditor.js | 7 +- src/features/effects/EffectList.js | 3 +- src/features/effects/EffectManager.js | 123 ++++----- src/features/effects/NewEffect.js | 6 +- src/features/exporter/Downloader.js | 7 +- src/features/layers/CopyLayer.js | 6 +- src/features/layers/ImportLayer.js | 6 +- src/features/layers/LayerEditor.js | 5 +- src/features/layers/LayerList.js | 7 +- src/features/layers/LayerManager.js | 4 +- src/features/layers/NewLayer.js | 6 +- src/features/machine/MachineSettings.js | 43 ++- src/features/machine/PolarSettings.js | 212 +++++++------- src/features/machine/RectSettings.js | 208 +++++++------- src/features/preview/PreviewManager.scss | 5 +- 28 files changed, 523 insertions(+), 556 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1b25a68b..94faae4c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,7 @@ "@dnd-kit/sortable": "^7.0.2", "@reduxjs/toolkit": "^1.9.5", "array-move": "^3.0.1", - "bootstrap": "^4.6.2", + "bootstrap": "^5.3.1", "buffer": "^6.0.3", "color": "^3.1.3", "core-js": "^3.11.0", @@ -35,7 +35,7 @@ "rc-slider": "^9.7.5", "re-reselect": "^4.0.1", "react": "18.2", - "react-bootstrap": "^1.5.2", + "react-bootstrap": "^2.8.0", "react-dom": "^18.2", "react-ga": "^3.3.1", "react-icons": "^4.2.0", @@ -2706,6 +2706,20 @@ "url": "https://opencollective.com/popperjs" } }, + "node_modules/@react-aria/ssr": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.7.1.tgz", + "integrity": "sha512-ovVPSD1WlRpZHt7GI9DqJrWG3OIYS+NXQ9y5HIewMJpSe+jPQmMQfyRmgX4EnvmxSlp0u04Wg/7oItcoSIb/RA==", + "dependencies": { + "@swc/helpers": "^0.5.0" + }, + "engines": { + "node": ">= 12" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, "node_modules/@reduxjs/toolkit": { "version": "1.9.5", "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.9.5.tgz", @@ -2729,23 +2743,43 @@ } } }, - "node_modules/@restart/context": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@restart/context/-/context-2.1.4.tgz", - "integrity": "sha512-INJYZQJP7g+IoDUh/475NlGiTeMfwTXUEr3tmRneckHIxNolGOW9CTq83S8cxq0CgJwwcMzMJFchxvlwe7Rk8Q==", + "node_modules/@restart/hooks": { + "version": "0.4.11", + "resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.4.11.tgz", + "integrity": "sha512-Ft/ncTULZN6ldGHiF/k5qt72O8JyRMOeg0tApvCni8LkoiEahO+z3TNxfXIVGy890YtWVDvJAl662dVJSJXvMw==", + "dependencies": { + "dequal": "^2.0.3" + }, "peerDependencies": { - "react": ">=16.3.2" + "react": ">=16.8.0" } }, - "node_modules/@restart/hooks": { - "version": "0.3.27", - "resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.3.27.tgz", - "integrity": "sha512-s984xV/EapUIfkjlf8wz9weP2O9TNKR96C68FfMEy2bE69+H4cNv3RD4Mf97lW7Htt7PjZrYTjSC8f3SB9VCXw==", + "node_modules/@restart/ui": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/@restart/ui/-/ui-1.6.6.tgz", + "integrity": "sha512-eC3puKuWE1SRYbojWHXnvCNHGgf3uzHCb6JOhnF4OXPibOIPEkR1sqDSkL643ydigxwh+ruCa1CmYHlzk7ikKA==", "dependencies": { - "dequal": "^2.0.2" + "@babel/runtime": "^7.21.0", + "@popperjs/core": "^2.11.6", + "@react-aria/ssr": "^3.5.0", + "@restart/hooks": "^0.4.9", + "@types/warning": "^3.0.0", + "dequal": "^2.0.3", + "dom-helpers": "^5.2.0", + "uncontrollable": "^8.0.1", + "warning": "^4.0.3" }, "peerDependencies": { - "react": ">=16.8.0" + "react": ">=16.14.0", + "react-dom": ">=16.14.0" + } + }, + "node_modules/@restart/ui/node_modules/uncontrollable": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/uncontrollable/-/uncontrollable-8.0.4.tgz", + "integrity": "sha512-ulRWYWHvscPFc0QQXvyJjY6LIXU56f0h8pQFvhxiKk5V1fcI8gp9Ht9leVAhrVjzqMw0BgjspBINx9r6oyJUvQ==", + "peerDependencies": { + "react": ">=16.14.0" } }, "node_modules/@rollup/pluginutils": { @@ -2779,6 +2813,19 @@ "@sinonjs/commons": "^1.7.0" } }, + "node_modules/@swc/helpers": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.1.tgz", + "integrity": "sha512-sJ902EfIzn1Fa+qYmjdQqh8tPsoxyBz+8yBKC2HKUxyezKJFwPGOn7pv4WY6QuQW//ySQi5lJjA/ZT9sNWWNTg==", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@swc/helpers/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + }, "node_modules/@tootallnate/once": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", @@ -2846,11 +2893,6 @@ "hoist-non-react-statics": "^3.3.0" } }, - "node_modules/@types/invariant": { - "version": "2.2.35", - "resolved": "https://registry.npmjs.org/@types/invariant/-/invariant-2.2.35.tgz", - "integrity": "sha512-DxX1V9P8zdJPYQat1gHyY0xj3efl8gnMVjiM9iCY6y27lj+PoQWkgjt8jDqmovPqULkKVpKRg8J36iQiA+EtEg==" - }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", @@ -2932,9 +2974,9 @@ } }, "node_modules/@types/react-transition-group": { - "version": "4.4.4", - "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.4.tgz", - "integrity": "sha512-7gAPz7anVK5xzbeQW9wFBDg7G++aPLAFY0QaSMOou9rJZpbuI58WAuJrgu+qR92l61grlnCUe7AFX8KGahAgug==", + "version": "4.4.6", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.6.tgz", + "integrity": "sha512-VnCdSxfcm08KjsJVQcfBmhEQAPnLB8G08hAxn39azX1qYBQ/5RVQuoHuKIcfKOdncuaUvEpFKFzEvbtIMsfVew==", "dependencies": { "@types/react": "*" } @@ -2958,7 +3000,7 @@ "node_modules/@types/warning": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@types/warning/-/warning-3.0.0.tgz", - "integrity": "sha1-DSUBJorY+ZYrdA04fEZU9fjiPlI=" + "integrity": "sha512-t/Tvs5qR47OLOr+4E9ckN8AmP2Tf16gWq+/qA4iUGS/OOyHVO8wv2vjJuX8SNOUTJyWb+2t7wJm6cXILFnOROA==" }, "node_modules/@types/yargs": { "version": "16.0.4", @@ -3820,9 +3862,9 @@ } }, "node_modules/bootstrap": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.6.2.tgz", - "integrity": "sha512-51Bbp/Uxr9aTuy6ca/8FbFloBUJZLHwnhTcnjIeRn2suQWsWzcuJhGjKDB5eppVte/8oCdOL3VuwxvZDUggwGQ==", + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.1.tgz", + "integrity": "sha512-jzwza3Yagduci2x0rr9MeFSORjcHpt0lRZukZPZQJT1Dth5qzV7XcgGqYzi39KGAVYR8QEDVoO0ubFKOxzMG+g==", "funding": [ { "type": "github", @@ -3834,8 +3876,7 @@ } ], "peerDependencies": { - "jquery": "1.9.1 - 3", - "popper.js": "^1.16.1" + "@popperjs/core": "^2.11.8" } }, "node_modules/bplist-parser": { @@ -4183,9 +4224,9 @@ "dev": true }, "node_modules/classnames": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.1.tgz", - "integrity": "sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==" + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz", + "integrity": "sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==" }, "node_modules/clean-stack": { "version": "2.2.0", @@ -8143,12 +8184,6 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "node_modules/jquery": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.0.tgz", - "integrity": "sha512-umpJ0/k8X0MvD1ds0P9SfowREz2LenHsQaxSohMZ5OMNEU2r0tf8pdeEFTHMFxWVxKNyU9rTtK3CWzUCTKJUeQ==", - "peer": true - }, "node_modules/js-base64": { "version": "2.6.4", "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.6.4.tgz", @@ -9570,17 +9605,6 @@ "points-on-curve": "0.2.0" } }, - "node_modules/popper.js": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz", - "integrity": "sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==", - "deprecated": "You can find the new Popper v2 at @popperjs/core, this package is dedicated to the legacy v1", - "peer": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/popperjs" - } - }, "node_modules/postcss": { "version": "8.4.18", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.18.tgz", @@ -9894,31 +9918,32 @@ } }, "node_modules/react-bootstrap": { - "version": "1.6.4", - "resolved": "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-1.6.4.tgz", - "integrity": "sha512-z3BhBD4bEZuLP8VrYqAD7OT7axdcSkkyvWBWnS2U/4MhyabUihrUyucPWkan7aMI1XIHbmH4LCpEtzWGfx/yfA==", - "dependencies": { - "@babel/runtime": "^7.14.0", - "@restart/context": "^2.1.4", - "@restart/hooks": "^0.3.26", - "@types/invariant": "^2.2.33", - "@types/prop-types": "^15.7.3", - "@types/react": ">=16.14.8", - "@types/react-transition-group": "^4.4.1", - "@types/warning": "^3.0.0", - "classnames": "^2.3.1", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-2.8.0.tgz", + "integrity": "sha512-e/aNtxl0Z2ozrIaR82jr6Zz7ss9GSoaXpQaxmvtDUsTZIq/XalkduR/ZXP6vbQHz2T4syvjA+4FbtwELxxmpww==", + "dependencies": { + "@babel/runtime": "^7.21.0", + "@restart/hooks": "^0.4.9", + "@restart/ui": "^1.6.3", + "@types/react-transition-group": "^4.4.5", + "classnames": "^2.3.2", "dom-helpers": "^5.2.1", "invariant": "^2.2.4", - "prop-types": "^15.7.2", + "prop-types": "^15.8.1", "prop-types-extra": "^1.1.0", - "react-overlays": "^5.1.1", - "react-transition-group": "^4.4.1", + "react-transition-group": "^4.4.5", "uncontrollable": "^7.2.1", "warning": "^4.0.3" }, "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" + "@types/react": ">=16.14.8", + "react": ">=16.14.0", + "react-dom": ">=16.14.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, "node_modules/react-dom": { @@ -9990,36 +10015,6 @@ "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" }, - "node_modules/react-overlays": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/react-overlays/-/react-overlays-5.2.1.tgz", - "integrity": "sha512-GLLSOLWr21CqtJn8geSwQfoJufdt3mfdsnIiQswouuQ2MMPns+ihZklxvsTDKD3cR2tF8ELbi5xUsvqVhR6WvA==", - "dependencies": { - "@babel/runtime": "^7.13.8", - "@popperjs/core": "^2.11.6", - "@restart/hooks": "^0.4.7", - "@types/warning": "^3.0.0", - "dom-helpers": "^5.2.0", - "prop-types": "^15.7.2", - "uncontrollable": "^7.2.1", - "warning": "^4.0.3" - }, - "peerDependencies": { - "react": ">=16.3.0", - "react-dom": ">=16.3.0" - } - }, - "node_modules/react-overlays/node_modules/@restart/hooks": { - "version": "0.4.11", - "resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.4.11.tgz", - "integrity": "sha512-Ft/ncTULZN6ldGHiF/k5qt72O8JyRMOeg0tApvCni8LkoiEahO+z3TNxfXIVGy890YtWVDvJAl662dVJSJXvMw==", - "dependencies": { - "dequal": "^2.0.3" - }, - "peerDependencies": { - "react": ">=16.8.0" - } - }, "node_modules/react-reconciler": { "version": "0.29.0", "resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.29.0.tgz", @@ -13979,6 +13974,14 @@ "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==" }, + "@react-aria/ssr": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.7.1.tgz", + "integrity": "sha512-ovVPSD1WlRpZHt7GI9DqJrWG3OIYS+NXQ9y5HIewMJpSe+jPQmMQfyRmgX4EnvmxSlp0u04Wg/7oItcoSIb/RA==", + "requires": { + "@swc/helpers": "^0.5.0" + } + }, "@reduxjs/toolkit": { "version": "1.9.5", "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.9.5.tgz", @@ -13990,18 +13993,36 @@ "reselect": "^4.1.8" } }, - "@restart/context": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@restart/context/-/context-2.1.4.tgz", - "integrity": "sha512-INJYZQJP7g+IoDUh/475NlGiTeMfwTXUEr3tmRneckHIxNolGOW9CTq83S8cxq0CgJwwcMzMJFchxvlwe7Rk8Q==", - "requires": {} - }, "@restart/hooks": { - "version": "0.3.27", - "resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.3.27.tgz", - "integrity": "sha512-s984xV/EapUIfkjlf8wz9weP2O9TNKR96C68FfMEy2bE69+H4cNv3RD4Mf97lW7Htt7PjZrYTjSC8f3SB9VCXw==", + "version": "0.4.11", + "resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.4.11.tgz", + "integrity": "sha512-Ft/ncTULZN6ldGHiF/k5qt72O8JyRMOeg0tApvCni8LkoiEahO+z3TNxfXIVGy890YtWVDvJAl662dVJSJXvMw==", "requires": { - "dequal": "^2.0.2" + "dequal": "^2.0.3" + } + }, + "@restart/ui": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/@restart/ui/-/ui-1.6.6.tgz", + "integrity": "sha512-eC3puKuWE1SRYbojWHXnvCNHGgf3uzHCb6JOhnF4OXPibOIPEkR1sqDSkL643ydigxwh+ruCa1CmYHlzk7ikKA==", + "requires": { + "@babel/runtime": "^7.21.0", + "@popperjs/core": "^2.11.6", + "@react-aria/ssr": "^3.5.0", + "@restart/hooks": "^0.4.9", + "@types/warning": "^3.0.0", + "dequal": "^2.0.3", + "dom-helpers": "^5.2.0", + "uncontrollable": "^8.0.1", + "warning": "^4.0.3" + }, + "dependencies": { + "uncontrollable": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/uncontrollable/-/uncontrollable-8.0.4.tgz", + "integrity": "sha512-ulRWYWHvscPFc0QQXvyJjY6LIXU56f0h8pQFvhxiKk5V1fcI8gp9Ht9leVAhrVjzqMw0BgjspBINx9r6oyJUvQ==", + "requires": {} + } } }, "@rollup/pluginutils": { @@ -14032,6 +14053,21 @@ "@sinonjs/commons": "^1.7.0" } }, + "@swc/helpers": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.1.tgz", + "integrity": "sha512-sJ902EfIzn1Fa+qYmjdQqh8tPsoxyBz+8yBKC2HKUxyezKJFwPGOn7pv4WY6QuQW//ySQi5lJjA/ZT9sNWWNTg==", + "requires": { + "tslib": "^2.4.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + } + } + }, "@tootallnate/once": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", @@ -14096,11 +14132,6 @@ "hoist-non-react-statics": "^3.3.0" } }, - "@types/invariant": { - "version": "2.2.35", - "resolved": "https://registry.npmjs.org/@types/invariant/-/invariant-2.2.35.tgz", - "integrity": "sha512-DxX1V9P8zdJPYQat1gHyY0xj3efl8gnMVjiM9iCY6y27lj+PoQWkgjt8jDqmovPqULkKVpKRg8J36iQiA+EtEg==" - }, "@types/istanbul-lib-coverage": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", @@ -14182,9 +14213,9 @@ } }, "@types/react-transition-group": { - "version": "4.4.4", - "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.4.tgz", - "integrity": "sha512-7gAPz7anVK5xzbeQW9wFBDg7G++aPLAFY0QaSMOou9rJZpbuI58WAuJrgu+qR92l61grlnCUe7AFX8KGahAgug==", + "version": "4.4.6", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.6.tgz", + "integrity": "sha512-VnCdSxfcm08KjsJVQcfBmhEQAPnLB8G08hAxn39azX1qYBQ/5RVQuoHuKIcfKOdncuaUvEpFKFzEvbtIMsfVew==", "requires": { "@types/react": "*" } @@ -14208,7 +14239,7 @@ "@types/warning": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@types/warning/-/warning-3.0.0.tgz", - "integrity": "sha1-DSUBJorY+ZYrdA04fEZU9fjiPlI=" + "integrity": "sha512-t/Tvs5qR47OLOr+4E9ckN8AmP2Tf16gWq+/qA4iUGS/OOyHVO8wv2vjJuX8SNOUTJyWb+2t7wJm6cXILFnOROA==" }, "@types/yargs": { "version": "16.0.4", @@ -14798,9 +14829,9 @@ "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==" }, "bootstrap": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.6.2.tgz", - "integrity": "sha512-51Bbp/Uxr9aTuy6ca/8FbFloBUJZLHwnhTcnjIeRn2suQWsWzcuJhGjKDB5eppVte/8oCdOL3VuwxvZDUggwGQ==", + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.1.tgz", + "integrity": "sha512-jzwza3Yagduci2x0rr9MeFSORjcHpt0lRZukZPZQJT1Dth5qzV7XcgGqYzi39KGAVYR8QEDVoO0ubFKOxzMG+g==", "requires": {} }, "bplist-parser": { @@ -15034,9 +15065,9 @@ "dev": true }, "classnames": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.1.tgz", - "integrity": "sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==" + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz", + "integrity": "sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==" }, "clean-stack": { "version": "2.2.0", @@ -17861,12 +17892,6 @@ } } }, - "jquery": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.0.tgz", - "integrity": "sha512-umpJ0/k8X0MvD1ds0P9SfowREz2LenHsQaxSohMZ5OMNEU2r0tf8pdeEFTHMFxWVxKNyU9rTtK3CWzUCTKJUeQ==", - "peer": true - }, "js-base64": { "version": "2.6.4", "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.6.4.tgz", @@ -18918,12 +18943,6 @@ "points-on-curve": "0.2.0" } }, - "popper.js": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz", - "integrity": "sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==", - "peer": true - }, "postcss": { "version": "8.4.18", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.18.tgz", @@ -19138,25 +19157,20 @@ } }, "react-bootstrap": { - "version": "1.6.4", - "resolved": "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-1.6.4.tgz", - "integrity": "sha512-z3BhBD4bEZuLP8VrYqAD7OT7axdcSkkyvWBWnS2U/4MhyabUihrUyucPWkan7aMI1XIHbmH4LCpEtzWGfx/yfA==", - "requires": { - "@babel/runtime": "^7.14.0", - "@restart/context": "^2.1.4", - "@restart/hooks": "^0.3.26", - "@types/invariant": "^2.2.33", - "@types/prop-types": "^15.7.3", - "@types/react": ">=16.14.8", - "@types/react-transition-group": "^4.4.1", - "@types/warning": "^3.0.0", - "classnames": "^2.3.1", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-2.8.0.tgz", + "integrity": "sha512-e/aNtxl0Z2ozrIaR82jr6Zz7ss9GSoaXpQaxmvtDUsTZIq/XalkduR/ZXP6vbQHz2T4syvjA+4FbtwELxxmpww==", + "requires": { + "@babel/runtime": "^7.21.0", + "@restart/hooks": "^0.4.9", + "@restart/ui": "^1.6.3", + "@types/react-transition-group": "^4.4.5", + "classnames": "^2.3.2", "dom-helpers": "^5.2.1", "invariant": "^2.2.4", - "prop-types": "^15.7.2", + "prop-types": "^15.8.1", "prop-types-extra": "^1.1.0", - "react-overlays": "^5.1.1", - "react-transition-group": "^4.4.1", + "react-transition-group": "^4.4.5", "uncontrollable": "^7.2.1", "warning": "^4.0.3" } @@ -19203,31 +19217,6 @@ "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" }, - "react-overlays": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/react-overlays/-/react-overlays-5.2.1.tgz", - "integrity": "sha512-GLLSOLWr21CqtJn8geSwQfoJufdt3mfdsnIiQswouuQ2MMPns+ihZklxvsTDKD3cR2tF8ELbi5xUsvqVhR6WvA==", - "requires": { - "@babel/runtime": "^7.13.8", - "@popperjs/core": "^2.11.6", - "@restart/hooks": "^0.4.7", - "@types/warning": "^3.0.0", - "dom-helpers": "^5.2.0", - "prop-types": "^15.7.2", - "uncontrollable": "^7.2.1", - "warning": "^4.0.3" - }, - "dependencies": { - "@restart/hooks": { - "version": "0.4.11", - "resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.4.11.tgz", - "integrity": "sha512-Ft/ncTULZN6ldGHiF/k5qt72O8JyRMOeg0tApvCni8LkoiEahO+z3TNxfXIVGy890YtWVDvJAl662dVJSJXvMw==", - "requires": { - "dequal": "^2.0.3" - } - } - } - }, "react-reconciler": { "version": "0.29.0", "resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.29.0.tgz", diff --git a/package.json b/package.json index 446cea83..bbd82346 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "@dnd-kit/sortable": "^7.0.2", "@reduxjs/toolkit": "^1.9.5", "array-move": "^3.0.1", - "bootstrap": "^4.6.2", + "bootstrap": "^5.3.1", "buffer": "^6.0.3", "color": "^3.1.3", "core-js": "^3.11.0", @@ -31,7 +31,7 @@ "rc-slider": "^9.7.5", "re-reselect": "^4.0.1", "react": "18.2", - "react-bootstrap": "^1.5.2", + "react-bootstrap": "^2.8.0", "react-dom": "^18.2", "react-ga": "^3.3.1", "react-icons": "^4.2.0", diff --git a/src/components/CheckboxOption.js b/src/components/CheckboxOption.js index 4bf0e124..901328a3 100644 --- a/src/components/CheckboxOption.js +++ b/src/components/CheckboxOption.js @@ -1,5 +1,7 @@ import React from "react" -import { Col, Row, Form } from "react-bootstrap" +import Col from "react-bootstrap/Col" +import Row from "react-bootstrap/Row" +import Form from "react-bootstrap/Form" import S from "react-switch" const Switch = S.default ? S.default : S // Fix: https://github.com/vitejs/vite/issues/2139 diff --git a/src/components/DropdownOption.js b/src/components/DropdownOption.js index d751d6bc..3a95bfa7 100644 --- a/src/components/DropdownOption.js +++ b/src/components/DropdownOption.js @@ -1,5 +1,7 @@ import React from "react" -import { Col, Form, Row } from "react-bootstrap" +import Col from "react-bootstrap/Col" +import Row from "react-bootstrap/Row" +import Form from "react-bootstrap/Form" import Select from "react-select" const DropdownOption = ({ diff --git a/src/components/InputOption.js b/src/components/InputOption.js index 8d508ab6..667a7f39 100644 --- a/src/components/InputOption.js +++ b/src/components/InputOption.js @@ -1,5 +1,7 @@ import React, { useState, useEffect } from "react" -import { Col, Form, Row } from "react-bootstrap" +import Col from "react-bootstrap/Col" +import Row from "react-bootstrap/Row" +import Form from "react-bootstrap/Form" import debounce from "lodash/debounce" const InputOption = ({ @@ -106,7 +108,7 @@ const InputOption = ({ {label && ( {title} diff --git a/src/components/ToggleButtonOption.js b/src/components/ToggleButtonOption.js index e9a9eed8..e58c6f22 100644 --- a/src/components/ToggleButtonOption.js +++ b/src/components/ToggleButtonOption.js @@ -1,11 +1,9 @@ import React from "react" -import { - Col, - Form, - Row, - ToggleButton, - ToggleButtonGroup, -} from "react-bootstrap" +import Col from "react-bootstrap/Col" +import Row from "react-bootstrap/Row" +import Form from "react-bootstrap/Form" +import ToggleButton from "react-bootstrap/ToggleButton" +import ToggleButtonGroup from "react-bootstrap/ToggleButtonGroup" const ToggleButtonOption = (props) => { const option = props.options[props.optionKey] @@ -46,6 +44,7 @@ const ToggleButtonOption = (props) => { return ( diff --git a/src/features/app/App.js b/src/features/app/App.js index d1412875..95ab9463 100644 --- a/src/features/app/App.js +++ b/src/features/app/App.js @@ -1,5 +1,5 @@ import React from "react" -import { Col, Row } from "react-bootstrap" +import Col from "react-bootstrap/Col" import { Provider } from "react-redux" import Header from "./Header" import Tabs from "./Tabs" @@ -14,7 +14,7 @@ const App = () => {
- +
@@ -22,7 +22,7 @@ const App = () => { - +
diff --git a/src/features/app/App.scss b/src/features/app/App.scss index b98c4760..84cca53a 100644 --- a/src/features/app/App.scss +++ b/src/features/app/App.scss @@ -41,10 +41,3 @@ width: 100%; } } - -@media (max-width: 991px) { - .no-gutters-md { - padding-left: 0; - padding-right: 0; - } -} diff --git a/src/features/app/Header.js b/src/features/app/Header.js index ebbb633e..bed535af 100644 --- a/src/features/app/Header.js +++ b/src/features/app/Header.js @@ -8,11 +8,11 @@ const Header = () => {
logo

sandify

-

+

create patterns for robots that draw in sand with ball bearings

diff --git a/src/features/app/Notice.js b/src/features/app/Notice.js index a6a1e7c6..1275e17f 100644 --- a/src/features/app/Notice.js +++ b/src/features/app/Notice.js @@ -4,7 +4,7 @@ import "./Notice.scss" const Notice = () => { return (
-

+

Use Sandify to enter the Sisyphus design competition! Enter by Oct 16th. diff --git a/src/features/app/Tabs.js b/src/features/app/Tabs.js index ef026d07..aa6c4a48 100644 --- a/src/features/app/Tabs.js +++ b/src/features/app/Tabs.js @@ -1,5 +1,6 @@ import React, { useEffect } from "react" -import { Tab, Tabs } from "react-bootstrap" +import Tab from "react-bootstrap/Tab" +import Tabs from "react-bootstrap/Tabs" import { useDispatch, useSelector } from "react-redux" import MachineSettings from "@/features/machine/MachineSettings" import LayerManager from "@/features/layers/LayerManager" diff --git a/src/features/app/bootstrap.scss b/src/features/app/bootstrap.scss index 0d3dda00..24a1358c 100644 --- a/src/features/app/bootstrap.scss +++ b/src/features/app/bootstrap.scss @@ -1,50 +1,21 @@ /** - Bootstrap 4 overrides + Bootstrap 5 overrides **/ -.card-header { - background-color: rgba(0,0,0,.05); - padding: 0.5rem; -} - -.card-body { - padding: 1rem; +h2 { + font-size: 1.6rem; } -.card { +.accordion-header { h3 { margin: 0; font-size: 1.25rem; } } -.accordion > .card { - overflow: visible; -} - -.accordion > .card > .card-header { - margin-bottom: 0; -} - -.card-body .form-label { - margin-bottom: 0; -} - -h2 { - font-size: 1.6rem; -} - -.card h3 { - margin-top: 0.5rem; -} - .tab-content { background-color: white; } -.card.active > .card-header { - background-color: #A0D4EB; -} - .list-group-item.active { z-index: auto; background-color: lighten(#2983BA, 5%); diff --git a/src/features/effects/CopyEffect.js b/src/features/effects/CopyEffect.js index d4668057..177a2967 100644 --- a/src/features/effects/CopyEffect.js +++ b/src/features/effects/CopyEffect.js @@ -1,5 +1,9 @@ import React, { useRef, useState, useEffect } from "react" -import { Button, Modal, Row, Col, Form } from "react-bootstrap" +import Button from "react-bootstrap/Button" +import Modal from "react-bootstrap/Modal" +import Col from "react-bootstrap/Col" +import Row from "react-bootstrap/Row" +import Form from "react-bootstrap/Form" import { useDispatch, useSelector } from "react-redux" import { selectSelectedEffect } from "./effectsSlice" import { addEffect } from "@/features/layers/layersSlice" diff --git a/src/features/effects/EffectEditor.js b/src/features/effects/EffectEditor.js index 2b1d5db8..6668933f 100644 --- a/src/features/effects/EffectEditor.js +++ b/src/features/effects/EffectEditor.js @@ -2,7 +2,8 @@ import React from "react" import { useDispatch, useSelector } from "react-redux" import { IconContext } from "react-icons" import { AiOutlineRotateRight } from "react-icons/ai" -import { Row, Col } from "react-bootstrap" +import Col from "react-bootstrap/Col" +import Row from "react-bootstrap/Row" import InputOption from "@/components/InputOption" import DropdownOption from "@/components/DropdownOption" import CheckboxOption from "@/components/CheckboxOption" @@ -54,7 +55,7 @@ const EffectEditor = ({ id }) => { )) return ( -

+
{model.canTransform(effect) && ( Transform @@ -79,7 +80,7 @@ const EffectEditor = ({ id }) => {
-
+
diff --git a/src/features/effects/EffectList.js b/src/features/effects/EffectList.js index 8e5d4ee4..7b63bde1 100644 --- a/src/features/effects/EffectList.js +++ b/src/features/effects/EffectList.js @@ -1,6 +1,7 @@ import React from "react" import { useDispatch, useSelector } from "react-redux" -import { Button, ListGroup } from "react-bootstrap" +import Button from "react-bootstrap/Button" +import ListGroup from "react-bootstrap/ListGroup" import { Tooltip } from "react-tooltip" import { FaEye, FaEyeSlash } from "react-icons/fa" import { DndContext, useSensor, useSensors, PointerSensor } from "@dnd-kit/core" diff --git a/src/features/effects/EffectManager.js b/src/features/effects/EffectManager.js index 2eaff63b..1801cf96 100644 --- a/src/features/effects/EffectManager.js +++ b/src/features/effects/EffectManager.js @@ -1,8 +1,8 @@ import React, { useState } from "react" -import { Button } from "react-bootstrap" +import Button from "react-bootstrap/Button" +import Accordion from "react-bootstrap/Accordion" import { Tooltip } from "react-tooltip" import { useSelector, useDispatch } from "react-redux" -import { Card, Accordion } from "react-bootstrap" import { FaTrash, FaCopy, FaPlusSquare } from "react-icons/fa" import { MdOutlineSettingsBackupRestore } from "react-icons/md" import { @@ -64,78 +64,67 @@ const EffectManager = () => { )} {numEffects > 0 && ( - - - - Effects - - - - - -
- + + Effects + + +
+ + +
+ + + + + -
- - - - - - -
-
-
-
- - - +
+
+ +
+
+
)}
diff --git a/src/features/effects/NewEffect.js b/src/features/effects/NewEffect.js index 7a6e712a..eda65cc3 100644 --- a/src/features/effects/NewEffect.js +++ b/src/features/effects/NewEffect.js @@ -1,7 +1,11 @@ import React, { useState, useRef } from "react" import { useDispatch, useSelector } from "react-redux" import Select from "react-select" -import { Button, Modal, Row, Col, Form } from "react-bootstrap" +import Col from "react-bootstrap/Col" +import Row from "react-bootstrap/Row" +import Form from "react-bootstrap/Form" +import Button from "react-bootstrap/Button" +import Modal from "react-bootstrap/Modal" import { selectSelectedLayer, addEffect, diff --git a/src/features/exporter/Downloader.js b/src/features/exporter/Downloader.js index e2054a09..d935daa3 100644 --- a/src/features/exporter/Downloader.js +++ b/src/features/exporter/Downloader.js @@ -1,6 +1,9 @@ import React, { useState } from "react" import { useSelector, useDispatch } from "react-redux" -import { Button, Modal, Col, Row } from "react-bootstrap" +import Col from "react-bootstrap/Col" +import Row from "react-bootstrap/Row" +import Button from "react-bootstrap/Button" +import Modal from "react-bootstrap/Modal" import { downloadFile } from "@/common/util" import DropdownOption from "@/components/DropdownOption" import InputOption from "@/components/InputOption" @@ -79,7 +82,7 @@ const Downloader = () => { return (
- - - Export to a file - - - - - - {fileType === SCARA && ( - - - + + Export to a file + + + + + + {fileType === SCARA && ( + + + + - - Read more - {" "} - about SCARA GCode. - - - )} - + Read more + {" "} + about SCARA GCode. + + + )} + + + + {(fileType === THETARHO || fileType === SCARA) && ( + )} - {(fileType === THETARHO || fileType === SCARA) && ( - - )} - - {fileType === SCARA && ( - - )} - + {fileType === SCARA && ( - - + + + + + + + See{" "} + + {" "} + the wiki{" "} + {" "} + for details on available program export variables. + + + +
+ - - - - - See{" "} - - {" "} - the wiki{" "} - {" "} - for details on available program export variables. - - - -
- -
- - - - - - - -
+
+ + + + + + + ) } diff --git a/src/features/preview/PreviewManager.js b/src/features/preview/PreviewManager.js index 267420a0..c0d2a04a 100644 --- a/src/features/preview/PreviewManager.js +++ b/src/features/preview/PreviewManager.js @@ -86,13 +86,13 @@ const PreviewManager = () => { className="flex-grow-1 d-flex flex-column" >
@@ -115,7 +115,6 @@ const PreviewManager = () => { />
-
diff --git a/src/features/preview/PreviewManager.scss b/src/features/preview/PreviewManager.scss index c7b67f9c..05430698 100644 --- a/src/features/preview/PreviewManager.scss +++ b/src/features/preview/PreviewManager.scss @@ -14,11 +14,6 @@ @media (min-width: 992px) { padding-top: 0.5rem; padding-bottom: 0.5rem; - width: 100%; - height: calc(100vh - 182px); + height: calc(100vh - 142px); } } - -.machine-preview { - outline: none; -} diff --git a/src/features/preview/PreviewWindow.js b/src/features/preview/PreviewWindow.js index bf124def..858bc55b 100644 --- a/src/features/preview/PreviewWindow.js +++ b/src/features/preview/PreviewWindow.js @@ -29,10 +29,10 @@ const PreviewWindow = () => { const resize = () => { const width = parseInt( getComputedStyle(wrapper).getPropertyValue("width"), - ) + ) - 22 const height = parseInt( getComputedStyle(wrapper).getPropertyValue("height"), - ) + ) - 22 if (canvasWidth !== width || canvasHeight !== height) { dispatch(setPreviewSize({ width, height })) From 41f1711930db7cd695716e2c2d4cfcdca0f4efa0 Mon Sep 17 00:00:00 2001 From: Bob Carmichael Date: Wed, 30 Aug 2023 06:20:41 -0400 Subject: [PATCH 070/126] add navbar; move About tab and export button --- src/features/app/About.js | 115 ++++++++++++ src/features/app/About.scss | 13 ++ src/features/app/App.js | 54 ++++-- src/features/app/Footer.js | 97 ---------- src/features/app/Header.js | 31 +++- src/features/app/Header.scss | 7 +- src/features/app/Main.js | 31 ---- src/features/app/Sidebar.js | 13 +- src/features/app/bootstrap.scss | 8 + src/features/app/happy-holidays.svg | 28 +++ src/features/app/koch-cube-flowers.svg | 27 +++ src/features/app/perlin-rings.svg | 233 +++++++++++++++++++++++++ src/features/exporter/Downloader.js | 2 +- src/features/preview/PreviewManager.js | 1 - src/features/preview/PreviewWindow.js | 12 +- 15 files changed, 501 insertions(+), 171 deletions(-) create mode 100644 src/features/app/About.js create mode 100644 src/features/app/About.scss delete mode 100644 src/features/app/Footer.js delete mode 100644 src/features/app/Main.js create mode 100644 src/features/app/happy-holidays.svg create mode 100644 src/features/app/koch-cube-flowers.svg create mode 100644 src/features/app/perlin-rings.svg diff --git a/src/features/app/About.js b/src/features/app/About.js new file mode 100644 index 00000000..ea23404a --- /dev/null +++ b/src/features/app/About.js @@ -0,0 +1,115 @@ +import React from "react" +import Container from "react-bootstrap/Container" +import Row from "react-bootstrap/Row" +import Col from "react-bootstrap/Col" +import HappyHolidays from "./happy-holidays.svg" +import PerlinRings from "./perlin-rings.svg" +import KochCubeFlowers from "./koch-cube-flowers.svg" +import "./About.scss" + +const About = () => { + return ( +
+ + + +

sandify

+
+ create patterns for robots that draw in sand with ball bearings +
+

+ Sandify turns your cold, empty-hearted, emotionless sand tables + into cold, empty-hearted, emotionless sand table robots with + enchanting patterns. Sandify is a labor of love, but if you'd + like to support Sandify development financially, I do have a{" "} + + Donation system set up with github + + . Or just PayPal. +

+

+ Sandify was created by users in the + + {" "} + V1Engineering.com + {" "} + forum. +

+ +

Getting started

+

+ Part of the fun of Sandify is playing it like you would a + xylophone. Try it out first. The goal is to make it easy to make + your first pattern by just clicking and scrolling, finding + something you like. Check out{" "} + the wiki for + some features that you might miss the first time through. +

+ +

What sand machines are supported?

+

+ Sandify supports gcode and theta-rho formats. Sandify was + originally designed for{" "} + + ZenXY on V1Engineering.com + + , which was inspired by the awesome Sisyphus kinetic art table by{" "} + Sisyphus Industries + . +

+ +

Github

+

+ Sandify is hosted on{" "} + Github. Please + post any problems, feature requests or comments in our{" "} + + issue tracker + + . We're a community project, so{" "} + + we'd love your help. + +

+ +

License

+

+ Sandify is licensed under the{" "} + + MIT license + + . Patterns that you create and code generated with Sandify are not + covered under the Sandify license. They are your work and your + copyright. +

+ + + + + + +
+
+
+ ) +} + +export default About diff --git a/src/features/app/About.scss b/src/features/app/About.scss new file mode 100644 index 00000000..2664f4e7 --- /dev/null +++ b/src/features/app/About.scss @@ -0,0 +1,13 @@ +.tagline { + font-size: 1.3rem; + font-weight: bold; +} + +.pattern-preview { + width: 300px; + padding: 20px; + + @media (max-width: 991px) { + width: 100%; + } +} diff --git a/src/features/app/App.js b/src/features/app/App.js index 2d32d5d5..6f22f5bf 100644 --- a/src/features/app/App.js +++ b/src/features/app/App.js @@ -1,29 +1,55 @@ -import React from "react" -import Col from "react-bootstrap/Col" -import Container from 'react-bootstrap/Container' +import React, { useState } from "react" +import Tab from "react-bootstrap/Tab" import { Provider } from "react-redux" import Header from "./Header" +import About from "./About" +import PreviewManager from "@/features/preview/PreviewManager" import Sidebar from "./Sidebar" -import Main from "./Main" import store from "./store" -import logo from "./logo.svg" import "./App.scss" const App = () => { + const [eventKey, setEventKey] = useState("patterns") + return (
-
+
-
-
-
-
+ + + +
+
+ +
+ +
+
- -
+ + + + +
diff --git a/src/features/app/Footer.js b/src/features/app/Footer.js deleted file mode 100644 index 4e048e89..00000000 --- a/src/features/app/Footer.js +++ /dev/null @@ -1,97 +0,0 @@ -import React from "react" - -const Footer = () => { - return ( -
-
-

About

-

- Sandify turns your cold, empty-hearted, emotionless sand tables into - cold, empty-hearted, emotionless sand table robots with enchanting - patterns. -

-

- Sandify is a labor of love, but if you'd like to support me - financially, I do have a{" "} - - Donation system set up with github - - . Or just PayPal. -

-
- -
-

Instructions

-

- Part of the fun of Sandify is playing it like you would a xylophone. -
- Try it out first. The goal is to make it easy to make your first - pattern by just clicking and scrolling, finding something you like. -
- Check out{" "} - the wiki for - some features that you might miss the first time through. -

-
- -
-

Sand Machine

-

- Anything that uses gcode can be used with Sandify. But the machine - this was designed for is the ZenXY from V1Engineering.com. -

-

- - ZenXY on V1Engineering.com - -

-

- ZenXY was inspired by the awesome Sisyphus kinetic art table by{" "} - Sisyphus Industries, - which is also now supported. -

-

- Sandify was created by users in the - - {" "} - V1Engineering.com forum - - . -

-
- -
-

Github

-

- Sandify is hosted on Github{" "} - here. Please post any - problems, feature requests or comments in our{" "} - issue tracker. -

-

- Sandify is a community project. We want and need collaborators.{" "} - - Help Sandify - - . -

-
- -
-

License

-

Sandify is licensed under the MIT license.

-

- Patterns that you create and gcode generated with Sandify are not - covered under the Sandify license. They are your work and your - copyright. Read our{" "} - - license - - . -

-
-
- ) -} - -export default Footer diff --git a/src/features/app/Header.js b/src/features/app/Header.js index cf2c6105..de63d323 100644 --- a/src/features/app/Header.js +++ b/src/features/app/Header.js @@ -1,17 +1,20 @@ import React, { useState } from "react" -import Nav from 'react-bootstrap/Nav' -import Navbar from 'react-bootstrap/Navbar' -import NavDropdown from 'react-bootstrap/NavDropdown' +import Nav from "react-bootstrap/Nav" +import Navbar from "react-bootstrap/Navbar" +import NavDropdown from "react-bootstrap/NavDropdown" import Downloader from "@/features/exporter/Downloader" import logo from "./logo.svg" import "./Header.scss" -const Header = () => { +const Header = ({ eventKey, setEventKey }) => { const [showExport, setShowExport] = useState(false) const toggleShowExport = () => setShowExport(!showExport) return ( - +
{ a { - color: white; + color: #e1e1e1; } .nav-item.dropdown > a { color: white; } + .navbar-nav .nav-link.active, .navbar-nav .nav-link.show { + color: white; + font-weight: bold; + } + h1 { font-size: 2rem; color: white; diff --git a/src/features/app/Main.js b/src/features/app/Main.js deleted file mode 100644 index 1c473d70..00000000 --- a/src/features/app/Main.js +++ /dev/null @@ -1,31 +0,0 @@ -import React from "react" -import Tab from "react-bootstrap/Tab" -import Footer from "./Footer" -import PreviewManager from "@/features/preview/PreviewManager" - -const Main = () => { - return ( - - - - - - - -
- - - - ) -} - -export default React.memo(Main) diff --git a/src/features/app/Sidebar.js b/src/features/app/Sidebar.js index 5ae81d5b..1409b020 100644 --- a/src/features/app/Sidebar.js +++ b/src/features/app/Sidebar.js @@ -6,7 +6,6 @@ import MachineSettings from "@/features/machine/MachineSettings" import LayerManager from "@/features/layers/LayerManager" import { selectSelectedLayer } from "@/features/layers/layersSlice" import { loadFont, supportedFonts } from "@/features/fonts/fontsSlice" -import Footer from "./Footer" const Sidebar = () => { const dispatch = useDispatch() @@ -18,9 +17,7 @@ const Sidebar = () => { if (layer) { return ( - + { > - - -
- ) } else { diff --git a/src/features/app/bootstrap.scss b/src/features/app/bootstrap.scss index 9a12ce89..a2119d53 100644 --- a/src/features/app/bootstrap.scss +++ b/src/features/app/bootstrap.scss @@ -5,6 +5,14 @@ h2 { font-size: 1.6rem; } +.navbar { + --bs-navbar-toggler-icon-bg: url("data:image/svg+xml,") !important; +} + +.navbar-toggler { + border-color: lightgray; +} + .accordion-header { h3 { margin: 0; diff --git a/src/features/app/happy-holidays.svg b/src/features/app/happy-holidays.svg new file mode 100644 index 00000000..ebae492d --- /dev/null +++ b/src/features/app/happy-holidays.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + pwidth:500;pheight:500; + + + + diff --git a/src/features/app/koch-cube-flowers.svg b/src/features/app/koch-cube-flowers.svg new file mode 100644 index 00000000..c5d6a048 --- /dev/null +++ b/src/features/app/koch-cube-flowers.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + pwidth:500;pheight:500; + + + + diff --git a/src/features/app/perlin-rings.svg b/src/features/app/perlin-rings.svg new file mode 100644 index 00000000..a3efc874 --- /dev/null +++ b/src/features/app/perlin-rings.svg @@ -0,0 +1,233 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + pwidth:500;pheight:500; + + + + diff --git a/src/features/exporter/Downloader.js b/src/features/exporter/Downloader.js index 4ad0c709..ca7d9b06 100644 --- a/src/features/exporter/Downloader.js +++ b/src/features/exporter/Downloader.js @@ -1,4 +1,4 @@ -import React, { useState } from "react" +import React from "react" import { useSelector, useDispatch } from "react-redux" import Col from "react-bootstrap/Col" import Row from "react-bootstrap/Row" diff --git a/src/features/preview/PreviewManager.js b/src/features/preview/PreviewManager.js index c0d2a04a..ffd9bf10 100644 --- a/src/features/preview/PreviewManager.js +++ b/src/features/preview/PreviewManager.js @@ -2,7 +2,6 @@ import React from "react" import { useSelector, useDispatch } from "react-redux" import Slider from "rc-slider" import "rc-slider/assets/index.css" -import Downloader from "@/features/exporter/Downloader" import { selectFontsState } from "@/features/fonts/fontsSlice" import { updateEffect, diff --git a/src/features/preview/PreviewWindow.js b/src/features/preview/PreviewWindow.js index 858bc55b..5c233db5 100644 --- a/src/features/preview/PreviewWindow.js +++ b/src/features/preview/PreviewWindow.js @@ -23,16 +23,17 @@ const PreviewWindow = () => { const layerIds = useSelector(selectVisibleLayerIds, isEqual) const remainingLayerIds = layerIds.filter((id) => id !== selectedLayer?.id) const layerRef = useRef() + const stagePadding = 22 useEffect(() => { const wrapper = document.getElementById("preview-wrapper") const resize = () => { const width = parseInt( getComputedStyle(wrapper).getPropertyValue("width"), - ) - 22 + ) - stagePadding const height = parseInt( getComputedStyle(wrapper).getPropertyValue("height"), - ) - 22 + ) - stagePadding if (canvasWidth !== width || canvasHeight !== height) { dispatch(setPreviewSize({ width, height })) @@ -42,12 +43,9 @@ const PreviewWindow = () => { trailing: true, }) + resize() window.addEventListener("resize", throttledResize, false) - - return () => { - window.removeEventListener("resize", throttledResize, false) - } - }, [dispatch, canvasWidth, canvasHeight]) + }, []) const relativeScale = () => { let width, height From ba4c44a624be1be0edfd9269ce9fea0e5b4ac609 Mon Sep 17 00:00:00 2001 From: Bob Carmichael Date: Thu, 31 Aug 2023 08:06:28 -0400 Subject: [PATCH 071/126] Add zoom to preview window; move stats to new tab; minor fixes --- src/features/app/Sidebar.js | 9 +++ src/features/layers/layersSlice.js | 12 ++-- src/features/layers/layersSlice.spec.js | 1 + src/features/preview/PreviewManager.js | 76 +++++++++++++++-------- src/features/preview/PreviewManager.scss | 4 +- src/features/preview/PreviewStats.js | 20 ++++++ src/features/preview/PreviewWindow.js | 29 +++++---- src/features/preview/previewSlice.js | 6 ++ src/features/preview/previewSlice.spec.js | 1 + 9 files changed, 115 insertions(+), 43 deletions(-) create mode 100644 src/features/preview/PreviewStats.js diff --git a/src/features/app/Sidebar.js b/src/features/app/Sidebar.js index 1409b020..2db012f2 100644 --- a/src/features/app/Sidebar.js +++ b/src/features/app/Sidebar.js @@ -4,6 +4,7 @@ import Tabs from "react-bootstrap/Tabs" import { useDispatch, useSelector } from "react-redux" import MachineSettings from "@/features/machine/MachineSettings" import LayerManager from "@/features/layers/LayerManager" +import PreviewStats from "@/features/preview/PreviewStats" import { selectSelectedLayer } from "@/features/layers/layersSlice" import { loadFont, supportedFonts } from "@/features/fonts/fontsSlice" @@ -33,6 +34,14 @@ const Sidebar = () => { > + + + + ) } else { diff --git a/src/features/layers/layersSlice.js b/src/features/layers/layersSlice.js index d42b7d8a..fb0647bc 100644 --- a/src/features/layers/layersSlice.js +++ b/src/features/layers/layersSlice.js @@ -753,11 +753,15 @@ export const deleteEffect = ({ id, effectId }) => { export const setCurrentLayer = (id) => { return (dispatch, getState) => { const state = getState() - const layer = selectLayerById(state, id) - dispatch(layersSlice.actions.setCurrentLayer(id)) - dispatch(effectsSlice.actions.setCurrentEffect(null)) - dispatch(setSelectedEffect(layer?.effectIds[0])) // this guard is a hack to get a test to run + if (id) { + const layer = selectLayerById(state, id) + dispatch(layersSlice.actions.setCurrentLayer(id)) + dispatch(effectsSlice.actions.setCurrentEffect(null)) + dispatch(setSelectedEffect(layer?.effectIds[0])) // this guard is a hack to get a test to run + } else { + dispatch(layersSlice.actions.setCurrentLayer(null)) + } } } diff --git a/src/features/layers/layersSlice.spec.js b/src/features/layers/layersSlice.spec.js index 65c6dce0..b79eed5b 100644 --- a/src/features/layers/layersSlice.spec.js +++ b/src/features/layers/layersSlice.spec.js @@ -253,6 +253,7 @@ describe("layers reducer", () => { type: "circle", circleLobes: "2", polygonSides: "5", + effectIds: [], }, }, }, diff --git a/src/features/preview/PreviewManager.js b/src/features/preview/PreviewManager.js index ffd9bf10..41fe4dce 100644 --- a/src/features/preview/PreviewManager.js +++ b/src/features/preview/PreviewManager.js @@ -1,5 +1,6 @@ -import React from "react" +import React, { useRef } from "react" import { useSelector, useDispatch } from "react-redux" +import Select from "react-select" import Slider from "rc-slider" import "rc-slider/assets/index.css" import { selectFontsState } from "@/features/fonts/fontsSlice" @@ -7,12 +8,11 @@ import { updateEffect, selectCurrentEffect, } from "@/features/effects/effectsSlice" -import { selectPreviewState } from "@/features/preview/previewSlice" import { - updateLayer, - selectCurrentLayer, - selectVerticesStats, -} from "@/features/layers/layersSlice" + selectPreviewSliderValue, + selectPreviewZoom, +} from "@/features/preview/previewSlice" +import { updateLayer, selectCurrentLayer } from "@/features/layers/layersSlice" import { getShapeFromType } from "@/features/shapes/factory" import { getEffectFromType } from "@/features/effects/factory" import "./PreviewManager.scss" @@ -24,16 +24,35 @@ const PreviewManager = () => { const fonts = useSelector(selectFontsState) const currentLayer = useSelector(selectCurrentLayer) const currentEffectLayer = useSelector(selectCurrentEffect) - const sliderValue = useSelector(selectPreviewState).sliderValue - const verticesStats = useSelector(selectVerticesStats) + const sliderValue = useSelector(selectPreviewSliderValue) + const zoom = useSelector(selectPreviewZoom) + const wrapperRef = useRef() + const currentShape = getShapeFromType(currentLayer?.type || "polygon") const currentEffect = getEffectFromType(currentEffectLayer?.type || "mask") + const zoomChoices = [ + { value: 0.25, label: "25%" }, + { value: 0.5, label: "50%" }, + { value: 1.0, label: "100%" }, + { value: 2.0, label: "200%" }, + { value: 4.0, label: "400%" }, + ] + const selectedZoomOption = zoomChoices.find((choice) => choice.value == zoom) + const previewAlignClass = zoom < 1 ? " justify-content-center" : "" const handleSliderChange = (value) => { dispatch(updatePreview({ sliderValue: value })) } + const handleZoomChange = (option) => { + dispatch(updatePreview({ zoom: option.value })) + } + const arrowKeyChange = (layer, event) => { + if (!layer) { + return + } + const attrs = { id: layer.id } const delta = event.shiftKey ? 1 : 5 @@ -63,7 +82,7 @@ const PreviewManager = () => { const attrs = arrowKeyChange(currentLayer, event) dispatch(updateLayer(attrs)) } - } else if (currentEffect) { + } else if (currentEffectLayer) { if (currentEffect.canMove(currentEffectLayer)) { const attrs = arrowKeyChange(currentEffectLayer, event) dispatch(updateEffect(attrs)) @@ -91,28 +110,33 @@ const PreviewManager = () => { >
-
-
-
- Points: {verticesStats.numPoints}, Distance:{" "} - {verticesStats.distance} -
- -
- -
+
+
+ +
+
+
diff --git a/src/features/preview/PreviewManager.scss b/src/features/preview/PreviewManager.scss index 05430698..8b671318 100644 --- a/src/features/preview/PreviewManager.scss +++ b/src/features/preview/PreviewManager.scss @@ -3,6 +3,8 @@ } .preview-wrapper { + overflow: auto; + .konvajs-content { margin: 0 auto; } @@ -14,6 +16,6 @@ @media (min-width: 992px) { padding-top: 0.5rem; padding-bottom: 0.5rem; - height: calc(100vh - 142px); + height: calc(100vh - 118px); } } diff --git a/src/features/preview/PreviewStats.js b/src/features/preview/PreviewStats.js new file mode 100644 index 00000000..be3dad75 --- /dev/null +++ b/src/features/preview/PreviewStats.js @@ -0,0 +1,20 @@ +import React from "react" +import { useSelector } from "react-redux" +import Col from "react-bootstrap/Col" +import Row from "react-bootstrap/Row" +import { selectVerticesStats } from "@/features/layers/layersSlice" + +const PreviewStats = () => { + const verticesStats = useSelector(selectVerticesStats) + + return ( + + Points + {verticesStats.numPoints} + Distance + {verticesStats.distance} + + ) +} + +export default React.memo(PreviewStats) diff --git a/src/features/preview/PreviewWindow.js b/src/features/preview/PreviewWindow.js index 5c233db5..44e84af6 100644 --- a/src/features/preview/PreviewWindow.js +++ b/src/features/preview/PreviewWindow.js @@ -12,7 +12,7 @@ import { } from "@/features/layers/layersSlice" import ShapePreview from "./ShapePreview" import ConnectorPreview from "./ConnectorPreview" -import { setPreviewSize } from "./previewSlice" +import { setPreviewSize, selectPreviewZoom } from "./previewSlice" const PreviewWindow = () => { const dispatch = useDispatch() @@ -21,6 +21,7 @@ const PreviewWindow = () => { const { canvasWidth, canvasHeight } = useSelector(selectPreviewState) const selectedLayer = useSelector(selectSelectedLayer, isEqual) const layerIds = useSelector(selectVisibleLayerIds, isEqual) + const zoom = useSelector(selectPreviewZoom) const remainingLayerIds = layerIds.filter((id) => id !== selectedLayer?.id) const layerRef = useRef() const stagePadding = 22 @@ -28,12 +29,12 @@ const PreviewWindow = () => { useEffect(() => { const wrapper = document.getElementById("preview-wrapper") const resize = () => { - const width = parseInt( - getComputedStyle(wrapper).getPropertyValue("width"), - ) - stagePadding - const height = parseInt( - getComputedStyle(wrapper).getPropertyValue("height"), - ) - stagePadding + const width = + parseInt(getComputedStyle(wrapper).getPropertyValue("width")) - + stagePadding + const height = + parseInt(getComputedStyle(wrapper).getPropertyValue("height")) - + stagePadding if (canvasWidth !== width || canvasHeight !== height) { dispatch(setPreviewSize({ width, height })) @@ -45,6 +46,10 @@ const PreviewWindow = () => { resize() window.addEventListener("resize", throttledResize, false) + + return () => { + window.removeEventListener("resize", throttledResize, false) + } }, []) const relativeScale = () => { @@ -94,12 +99,12 @@ const PreviewWindow = () => { return ( state.sliderValue, ) + +export const selectPreviewZoom = createSelector( + selectPreviewState, + (state) => state.zoom, +) diff --git a/src/features/preview/previewSlice.spec.js b/src/features/preview/previewSlice.spec.js index 7d390b01..249fb0c9 100644 --- a/src/features/preview/previewSlice.spec.js +++ b/src/features/preview/previewSlice.spec.js @@ -6,6 +6,7 @@ describe("preview reducer", () => { canvasWidth: 600, canvasHeight: 600, sliderValue: 0, + zoom: 1, }) }) From 9745b73f99e052c97256ca182c8a9fe35a047390 Mon Sep 17 00:00:00 2001 From: Bob Carmichael Date: Fri, 1 Sep 2023 07:40:44 -0400 Subject: [PATCH 072/126] don't clip end points on perimeter; don't bold file menu when selected --- src/features/app/App.js | 6 +----- src/features/app/Header.js | 2 +- src/features/app/Header.scss | 4 ++++ src/features/preview/PreviewManager.js | 2 +- src/features/preview/PreviewWindow.js | 13 ++++++------- 5 files changed, 13 insertions(+), 14 deletions(-) diff --git a/src/features/app/App.js b/src/features/app/App.js index 6f22f5bf..fd31bd17 100644 --- a/src/features/app/App.js +++ b/src/features/app/App.js @@ -24,10 +24,7 @@ const App = () => { defaultActiveKey="about" > - +
@@ -43,7 +40,6 @@ const App = () => { diff --git a/src/features/app/Header.js b/src/features/app/Header.js index de63d323..4622f523 100644 --- a/src/features/app/Header.js +++ b/src/features/app/Header.js @@ -30,7 +30,7 @@ const Header = ({ eventKey, setEventKey }) => { - + ) diff --git a/src/features/app/bootstrap.scss b/src/features/app/bootstrap.scss index 43c8479e..c68d7d62 100644 --- a/src/features/app/bootstrap.scss +++ b/src/features/app/bootstrap.scss @@ -5,6 +5,10 @@ h2 { font-size: 1.6rem; } +h3 { + font-size: 1.2rem; +} + .navbar { --bs-navbar-toggler-icon-bg: url("data:image/svg+xml,") !important; } diff --git a/src/features/app/store.js b/src/features/app/store.js index 4f11bac1..14cb85fc 100644 --- a/src/features/app/store.js +++ b/src/features/app/store.js @@ -1,7 +1,7 @@ import { configureStore } from "@reduxjs/toolkit" import { combineReducers } from "redux" import machineReducer from "@/features/machine/machineSlice" -import exporterReducer from "@/features/exporter/exporterSlice" +import exporterReducer from "@/features/export/exporterSlice" import previewReducer from "@/features/preview/previewSlice" import fontsReducer from "@/features/fonts/fontsSlice" import layersReducer from "@/features/layers/layersSlice" diff --git a/src/features/effects/Track.js b/src/features/effects/Track.js index 4fc6728b..f59e4741 100644 --- a/src/features/effects/Track.js +++ b/src/features/effects/Track.js @@ -16,7 +16,7 @@ const options = { trackShape: { title: "Track type", type: "togglebutton", - choices: ["circle", "spiral"], + choices: ["circular", "spiral"], }, trackSpiralRadiusPct: { title: "Spiral tightness", @@ -238,7 +238,9 @@ export default class Track extends Effect { trackShape, } = effect const numShapes = - trackShape == "circle" ? trackNumShapes : trackNumShapes - 1 + trackShape == "circle" && trackRotation === 360 + ? trackNumShapes + : trackNumShapes - 1 const spiralRadius = width * trackSpiralRadiusPct const shapeCompleteFraction = index / (numShapes || 1) const endAngle = (trackRotation * Math.PI) / 180 diff --git a/src/features/exporter/CommentExporter.js b/src/features/export/CommentExporter.js similarity index 100% rename from src/features/exporter/CommentExporter.js rename to src/features/export/CommentExporter.js diff --git a/src/features/exporter/Downloader.js b/src/features/export/ExportDownloader.js similarity index 99% rename from src/features/exporter/Downloader.js rename to src/features/export/ExportDownloader.js index ca7d9b06..0dee8d55 100644 --- a/src/features/exporter/Downloader.js +++ b/src/features/export/ExportDownloader.js @@ -13,7 +13,7 @@ import { selectConnectedVertices } from "@/features/layers/layersSlice" import { selectExporterState, selectComments, -} from "@/features/exporter/exporterSlice" +} from "@/features/export/exporterSlice" import { selectMachine } from "@/features/machine/machineSlice" import GCodeExporter from "./GCodeExporter" import ScaraGCodeExporter from "./ScaraGCodeExporter" diff --git a/src/features/exporter/Exporter.js b/src/features/export/Exporter.js similarity index 100% rename from src/features/exporter/Exporter.js rename to src/features/export/Exporter.js diff --git a/src/features/exporter/GCodeExporter.js b/src/features/export/GCodeExporter.js similarity index 100% rename from src/features/exporter/GCodeExporter.js rename to src/features/export/GCodeExporter.js diff --git a/src/features/exporter/ScaraGCodeExporter.js b/src/features/export/ScaraGCodeExporter.js similarity index 100% rename from src/features/exporter/ScaraGCodeExporter.js rename to src/features/export/ScaraGCodeExporter.js diff --git a/src/features/exporter/SvgExporter.js b/src/features/export/SvgExporter.js similarity index 100% rename from src/features/exporter/SvgExporter.js rename to src/features/export/SvgExporter.js diff --git a/src/features/exporter/ThetaRhoExporter.js b/src/features/export/ThetaRhoExporter.js similarity index 100% rename from src/features/exporter/ThetaRhoExporter.js rename to src/features/export/ThetaRhoExporter.js diff --git a/src/features/exporter/exporterSlice.js b/src/features/export/exporterSlice.js similarity index 100% rename from src/features/exporter/exporterSlice.js rename to src/features/export/exporterSlice.js diff --git a/src/features/exporter/exporterSlice.spec.js b/src/features/export/exporterSlice.spec.js similarity index 100% rename from src/features/exporter/exporterSlice.spec.js rename to src/features/export/exporterSlice.spec.js diff --git a/src/features/importer/GCodeImporter.js b/src/features/import/GCodeImporter.js similarity index 100% rename from src/features/importer/GCodeImporter.js rename to src/features/import/GCodeImporter.js diff --git a/src/features/importer/Importer.js b/src/features/import/Importer.js similarity index 100% rename from src/features/importer/Importer.js rename to src/features/import/Importer.js diff --git a/src/features/import/LayerImporter.js b/src/features/import/LayerImporter.js new file mode 100644 index 00000000..21da8261 --- /dev/null +++ b/src/features/import/LayerImporter.js @@ -0,0 +1,74 @@ +import React, { useEffect, useRef } from "react" +import Form from "react-bootstrap/Form" +import { useSelector, useDispatch } from "react-redux" +import { selectMachine } from "@/features/machine/machineSlice" +import ThetaRhoImporter from "@/features/import/ThetaRhoImporter" +import GCodeImporter from "@/features/import/GCodeImporter" +import { addLayer } from "@/features/layers/layersSlice" +import Layer from "@/features/layers/Layer" + +const LayerImporter = ({ toggleModal, showModal }) => { + const machineState = useSelector(selectMachine) + const dispatch = useDispatch() + const inputRef = useRef() + + useEffect(() => { + console.log(showModal) + if (showModal && inputRef.current) { + inputRef.current.click() + } + }, [showModal]) + + const handleFileImported = (importer, importedProps) => { + const layer = new Layer("fileImport") + const layerProps = { + ...importedProps, + machine: machineState, + } + const attrs = { + ...layer.getInitialState(layerProps), + name: importer.fileName, + } + + dispatch(addLayer(attrs)) + } + + const handleFileSelected = (event) => { + let file = event.target.files[0] + + if (file) { + let reader = new FileReader() + + reader.onload = (event) => { + var text = reader.result + + let importer + if (file.name.toLowerCase().endsWith(".thr")) { + importer = new ThetaRhoImporter(file.name, text) + } else if ( + file.name.toLowerCase().endsWith(".gcode") || + file.name.toLowerCase().endsWith(".nc") + ) { + importer = new GCodeImporter(file.name, text) + } + + importer.import(handleFileImported) + } + + reader.readAsText(file) + } + } + + return ( + + ) +} + +export default React.memo(LayerImporter) diff --git a/src/features/importer/ThetaRhoImporter.js b/src/features/import/ThetaRhoImporter.js similarity index 100% rename from src/features/importer/ThetaRhoImporter.js rename to src/features/import/ThetaRhoImporter.js diff --git a/src/features/importer/testArcs.gcode b/src/features/import/testArcs.gcode similarity index 100% rename from src/features/importer/testArcs.gcode rename to src/features/import/testArcs.gcode diff --git a/src/features/layers/ImportLayer.js b/src/features/layers/ImportLayer.js deleted file mode 100644 index 16c50c2a..00000000 --- a/src/features/layers/ImportLayer.js +++ /dev/null @@ -1,169 +0,0 @@ -import React from "react" -import Button from "react-bootstrap/Button" -import Modal from "react-bootstrap/Modal" -import Form from "react-bootstrap/Form" -import Accordion from "react-bootstrap/Accordion" -import Card from "react-bootstrap/Card" -import { useSelector, useDispatch } from "react-redux" -import ReactGA from "react-ga" -import { selectMachine } from "@/features/machine/machineSlice" -import ThetaRhoImporter from "@/features/importer/ThetaRhoImporter" -import GCodeImporter from "@/features/importer/GCodeImporter" -import { addLayer } from "./layersSlice" -import Layer from "./Layer" - -const ImportLayer = ({ toggleModal, showModal }) => { - const machineState = useSelector(selectMachine) - const dispatch = useDispatch() - let startTime - - const handleFileImported = (importer, importedProps) => { - const layer = new Layer("fileImport") - const layerProps = { - ...importedProps, - machine: machineState, - } - const attrs = layer.getInitialState(layerProps) - const endTime = performance.now() - - dispatch(addLayer(attrs)) - ReactGA.timing({ - category: "PatternImport", - variable: "read" + importer.label, - value: endTime - startTime, // in milliseconds - }) - } - - const handleFileSelected = (event) => { - let file = event.target.files[0] - let reader = new FileReader() - - reader.onload = (event) => { - startTime = performance.now() - var text = reader.result - - let importer - if (file.name.toLowerCase().endsWith(".thr")) { - importer = new ThetaRhoImporter(file.name, text) - } else if ( - file.name.toLowerCase().endsWith(".gcode") || - file.name.toLowerCase().endsWith(".nc") - ) { - importer = new GCodeImporter(file.name, text) - } - - importer.import(handleFileImported) - toggleModal() - } - - reader.readAsText(file) - } - - return ( - - - Import new layer - - - - - - -

Import

- Imports a pattern file as a new layer. Supported formats are .thr, - .gcode, and .nc. - -
-
-
-
-

Where to get .thr files

- Sisyphus machines use theta rho (.thr) files. There is a large - community sharing them. - -

About copyrights

-

- Be careful and respectful. Understand that the original author put - their labor, intensity, and ideas into this art. The creators have a - right to own it (and they have a copyright, even if it doesn't - say so). If you don't have permission (a license) to use their - art, then you shouldn't be. If you do have permission to use - their art, then you should be thankful, and I'm sure they would - appreciate you sending them a note of thanks. A picture of your - table creating their shared art would probably make them smile. -

-

- Someone posting the .thr file to a forum or subreddit probably wants - it to be shared, and drawing it on your home table is probably OK. - Just be careful if you want to use them for something significant - without explicit permission. -

-

P.S. I am not a lawyer.

-
-
- - - - -
- ) -} - -export default ImportLayer diff --git a/src/features/layers/LayerManager.js b/src/features/layers/LayerManager.js index b2f3a92f..ad0b3610 100644 --- a/src/features/layers/LayerManager.js +++ b/src/features/layers/LayerManager.js @@ -3,10 +3,7 @@ import Button from "react-bootstrap/Button" import { Tooltip } from "react-tooltip" import { useSelector, useDispatch } from "react-redux" import { FaTrash, FaCopy, FaPlusSquare } from "react-icons/fa" -import { - MdOutlineFileUpload, - MdOutlineSettingsBackupRestore, -} from "react-icons/md" +import { MdOutlineSettingsBackupRestore } from "react-icons/md" import LayerEditor from "@/features/layers/LayerEditor" import { selectSelectedLayerId, @@ -16,7 +13,6 @@ import { import { deleteLayer } from "@/features/layers/layersSlice" import NewLayer from "./NewLayer" import CopyLayer from "./CopyLayer" -import ImportLayer from "./ImportLayer" import LayerList from "./LayerList" import "./LayerManager.scss" @@ -27,11 +23,9 @@ const LayerManager = () => { const canRemove = numLayers > 1 const [showNewLayer, setShowNewLayer] = useState(false) - const [showImportLayer, setShowImportLayer] = useState(false) const [showCopyLayer, setShowCopyLayer] = useState(false) const toggleNewLayerModal = () => setShowNewLayer(!showNewLayer) - const toggleImportModal = () => setShowImportLayer(!showImportLayer) const toggleCopyModal = () => setShowCopyLayer(!showCopyLayer) const handleLayerRemoved = (id) => dispatch(deleteLayer(selectedLayerId)) @@ -52,10 +46,6 @@ const LayerManager = () => { showModal={showNewLayer} toggleModal={toggleNewLayerModal} /> - { > - -
{canRemove && } {canRemove && ( diff --git a/src/features/shapes/FileImport.js b/src/features/shapes/FileImport.js index c668dceb..d305ae46 100644 --- a/src/features/shapes/FileImport.js +++ b/src/features/shapes/FileImport.js @@ -17,7 +17,7 @@ const options = { export default class FileImport extends Shape { constructor() { super("fileImport") - this.label = "FileImport" + this.label = "import" this.usesMachine = true this.selectGroup = "import" } From 53cf47c878a77b49329294c81979fcea17ae4075 Mon Sep 17 00:00:00 2001 From: Bob Carmichael Date: Mon, 4 Sep 2023 15:52:14 -0400 Subject: [PATCH 076/126] support file save, open, and new --- package-lock.json | 34 +++++++++ package.json | 1 + src/components/InputOption.js | 10 +++ src/features/app/App.js | 7 ++ src/features/app/App.scss | 7 +- src/features/app/Header.js | 34 +++++++-- src/features/app/store.js | 43 ++++++++--- src/features/export/ExportDownloader.js | 8 +- src/features/file/SandifyDownloader.js | 74 +++++++++++++++++++ src/features/file/SandifyExporter.js | 10 +++ src/features/file/SandifyImporter.js | 7 ++ src/features/file/SandifyUploader.js | 56 ++++++++++++++ src/features/file/fileSlice.js | 51 +++++++++++++ .../{LayerImporter.js => ImportUploader.js} | 13 ++-- src/features/preview/PreviewManager.js | 6 -- 15 files changed, 322 insertions(+), 39 deletions(-) create mode 100644 src/features/file/SandifyDownloader.js create mode 100644 src/features/file/SandifyExporter.js create mode 100644 src/features/file/SandifyImporter.js create mode 100644 src/features/file/SandifyUploader.js create mode 100644 src/features/file/fileSlice.js rename src/features/import/{LayerImporter.js => ImportUploader.js} (87%) diff --git a/package-lock.json b/package-lock.json index 94faae4c..6c19d818 100644 --- a/package-lock.json +++ b/package-lock.json @@ -43,6 +43,7 @@ "react-redux": "^8.1.2", "react-select": "^5.7.4", "react-switch": "^7.0.0", + "react-toastify": "^9.1.3", "react-tooltip": "^5.20.0", "reduce-reducers": "^1.0.4", "redux": "^4.2.1", @@ -4247,6 +4248,14 @@ "wrap-ansi": "^7.0.0" } }, + "node_modules/clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "engines": { + "node": ">=6" + } + }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -10114,6 +10123,18 @@ "react-dom": "^15.3.0 || ^16.0.0 || ^17.0.0 || ^18.0.0" } }, + "node_modules/react-toastify": { + "version": "9.1.3", + "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-9.1.3.tgz", + "integrity": "sha512-fPfb8ghtn/XMxw3LkxQBk3IyagNpF/LIKjOBflbexr2AWxAH1MJgvnESwEwBn9liLFXgTKWgBSdZpw9m4OTHTg==", + "dependencies": { + "clsx": "^1.1.1" + }, + "peerDependencies": { + "react": ">=16", + "react-dom": ">=16" + } + }, "node_modules/react-tooltip": { "version": "5.20.0", "resolved": "https://registry.npmjs.org/react-tooltip/-/react-tooltip-5.20.0.tgz", @@ -15085,6 +15106,11 @@ "wrap-ansi": "^7.0.0" } }, + "clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==" + }, "co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -19276,6 +19302,14 @@ "prop-types": "^15.7.2" } }, + "react-toastify": { + "version": "9.1.3", + "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-9.1.3.tgz", + "integrity": "sha512-fPfb8ghtn/XMxw3LkxQBk3IyagNpF/LIKjOBflbexr2AWxAH1MJgvnESwEwBn9liLFXgTKWgBSdZpw9m4OTHTg==", + "requires": { + "clsx": "^1.1.1" + } + }, "react-tooltip": { "version": "5.20.0", "resolved": "https://registry.npmjs.org/react-tooltip/-/react-tooltip-5.20.0.tgz", diff --git a/package.json b/package.json index bbd82346..564ef1a1 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "react-redux": "^8.1.2", "react-select": "^5.7.4", "react-switch": "^7.0.0", + "react-toastify": "^9.1.3", "react-tooltip": "^5.20.0", "reduce-reducers": "^1.0.4", "redux": "^4.2.1", diff --git a/src/components/InputOption.js b/src/components/InputOption.js index 9436e294..6ca79d30 100644 --- a/src/components/InputOption.js +++ b/src/components/InputOption.js @@ -10,6 +10,8 @@ const InputOption = ({ optionKey, onChange, object, + inputRef, + focusOnSelect = false, label = true, }) => { const [value, setValue] = useState(data[optionKey]) @@ -64,6 +66,12 @@ const InputOption = ({ } } + const handleFocus = (event) => { + if (focusOnSelect) { + event.target.select() + } + } + const renderedInput = ( ) if (!option.inline) { diff --git a/src/features/app/App.js b/src/features/app/App.js index fd31bd17..90d1c2fb 100644 --- a/src/features/app/App.js +++ b/src/features/app/App.js @@ -1,6 +1,7 @@ import React, { useState } from "react" import Tab from "react-bootstrap/Tab" import { Provider } from "react-redux" +import { ToastContainer } from "react-toastify" import Header from "./Header" import About from "./About" import PreviewManager from "@/features/preview/PreviewManager" @@ -14,6 +15,12 @@ const App = () => { return (
+
{ const [showExport, setShowExport] = useState(false) const [showImport, setShowImport] = useState(0) + const [showSave, setShowSave] = useState(false) + const [showOpen, setShowOpen] = useState(0) const toggleExport = () => setShowExport(!showExport) - const toggleImport = () => setShowImport(showImport + 1) // force file dialog to open again + const toggleImport = () => setShowImport(showImport + 1) + const toggleSave = () => setShowSave(!showSave) + const toggleOpen = () => setShowOpen(showOpen + 1) + const dispatch = useDispatch() + + const handleNew = () => { + dispatch({ type: "NEW_PATTERN" }) + } return ( { title="File" id="file-dropdown" > - Open... + New + Open... Import layer... - Save - Save as... + Save as... - Export pattern as... + Export as... { showModal={showExport} toggleModal={toggleExport} /> - + + ) } diff --git a/src/features/app/store.js b/src/features/app/store.js index 14cb85fc..9b4db495 100644 --- a/src/features/app/store.js +++ b/src/features/app/store.js @@ -6,6 +6,7 @@ import previewReducer from "@/features/preview/previewSlice" import fontsReducer from "@/features/fonts/fontsSlice" import layersReducer from "@/features/layers/layersSlice" import effectsReducer from "@/features/effects/effectsSlice" +import fileReducer from "@/features/file/fileSlice" import { loadState, saveState } from "@/common/localStorage" import { resetLogCounts } from "@/common/debugging" import appReducer from "./appSlice" @@ -31,16 +32,40 @@ if (persistedState) { persistedState.fonts.loaded = false } +const combinedReducer = combineReducers({ + app: appReducer, + effects: effectsReducer, + exporter: exporterReducer, + file: fileReducer, + fonts: fontsReducer, + layers: layersReducer, + machine: machineReducer, + preview: previewReducer, +}) + +const rootReducer = (state, action) => { + if (action.type === "NEW_PATTERN") { + const newState = { + ...state, + layers: undefined, + effects: undefined, + } + return combinedReducer(newState, action) + } else if (action.type === "LOAD_PATTERN") { + const { effects, layers } = action.payload + const newState = { + ...state, + layers, + effects, + } + return combinedReducer(newState, action) + } + + return combinedReducer(state, action) +} + const store = configureStore({ - reducer: combineReducers({ - app: appReducer, - layers: layersReducer, - effects: effectsReducer, - exporter: exporterReducer, - machine: machineReducer, - preview: previewReducer, - fonts: fontsReducer, - }), + reducer: rootReducer, preloadedState: persistedState, }) diff --git a/src/features/export/ExportDownloader.js b/src/features/export/ExportDownloader.js index 0dee8d55..21954423 100644 --- a/src/features/export/ExportDownloader.js +++ b/src/features/export/ExportDownloader.js @@ -8,11 +8,11 @@ import { downloadFile } from "@/common/util" import DropdownOption from "@/components/DropdownOption" import InputOption from "@/components/InputOption" import CheckboxOption from "@/components/CheckboxOption" -import { updateExporter } from "./exporterSlice" import { selectConnectedVertices } from "@/features/layers/layersSlice" import { selectExporterState, selectComments, + updateExporter, } from "@/features/export/exporterSlice" import { selectMachine } from "@/features/machine/machineSlice" import GCodeExporter from "./GCodeExporter" @@ -28,7 +28,7 @@ const exporters = { [SCARA]: ScaraGCodeExporter, } -const Downloader = ({ showModal, toggleModal }) => { +const ExportDownloader = ({ showModal, toggleModal }) => { const dispatch = useDispatch() const machine = useSelector(selectMachine) const exporterState = useSelector(selectExporterState) @@ -87,7 +87,7 @@ const Downloader = ({ showModal, toggleModal }) => { Export to a file - + { ) } -export default React.memo(Downloader) +export default React.memo(ExportDownloader) diff --git a/src/features/file/SandifyDownloader.js b/src/features/file/SandifyDownloader.js new file mode 100644 index 00000000..dd3ceaae --- /dev/null +++ b/src/features/file/SandifyDownloader.js @@ -0,0 +1,74 @@ +import React, { useRef } from "react" +import { useSelector, useDispatch } from "react-redux" +import Modal from "react-bootstrap/Modal" +import Button from "react-bootstrap/Button" +import InputOption from "@/components/InputOption" +import { updateFile, selectFileState, fileOptions, download } from "./fileSlice" + +const SandifyDownloader = ({ showModal, toggleModal }) => { + const dispatch = useDispatch() + const fileState = useSelector(selectFileState) + const { fileName } = fileState + const inputRef = useRef() + + const handleChange = (attrs) => { + dispatch(updateFile(attrs)) + } + + const handleInitialFocus = () => { + inputRef.current.focus() + } + + const handleDownload = () => { + let name = fileName + if (!fileName.includes(".")) { + name += ".sdf" + } + + dispatch(download(name)) + toggleModal() + } + + return ( + + + Save pattern as file + + + + + + + + + + + + ) +} + +export default React.memo(SandifyDownloader) diff --git a/src/features/file/SandifyExporter.js b/src/features/file/SandifyExporter.js new file mode 100644 index 00000000..b8199a80 --- /dev/null +++ b/src/features/file/SandifyExporter.js @@ -0,0 +1,10 @@ +export default class SandifyExporter { + export(state) { + const json = { + effects: state.effects, + layers: state.layers, + } + + return JSON.stringify(json, null, "\t") + } +} diff --git a/src/features/file/SandifyImporter.js b/src/features/file/SandifyImporter.js new file mode 100644 index 00000000..6ae35e72 --- /dev/null +++ b/src/features/file/SandifyImporter.js @@ -0,0 +1,7 @@ +export default class SandifyImporter { + import(stateString) { + const state = JSON.parse(stateString) + + return state + } +} diff --git a/src/features/file/SandifyUploader.js b/src/features/file/SandifyUploader.js new file mode 100644 index 00000000..15e1ef39 --- /dev/null +++ b/src/features/file/SandifyUploader.js @@ -0,0 +1,56 @@ +import React, { useEffect, useRef } from "react" +import Form from "react-bootstrap/Form" +import { useDispatch } from "react-redux" +import { toast } from "react-toastify" +import SandifyImporter from "./SandifyImporter" + +const SandifyUploader = ({ toggleModal, showModal }) => { + const dispatch = useDispatch() + const inputRef = useRef() + + useEffect(() => { + if (showModal && inputRef.current) { + inputRef.current.click() + } + }, [showModal]) + + const handleFileSelected = (event) => { + const file = event.target.files[0] + + if (file) { + const reader = new FileReader() + + reader.onload = (event) => { + var text = reader.result + + try { + const importer = new SandifyImporter() + const newState = importer.import(text) + + dispatch({ type: "LOAD_PATTERN", payload: newState }) + } catch (e) { + toast.error(e.message) + } + + // reset the input so we can load the same file again if needed + event.preventDefault() + inputRef.current.value = null + } + + reader.readAsText(file) + } + } + + return ( + + ) +} + +export default React.memo(SandifyUploader) diff --git a/src/features/file/fileSlice.js b/src/features/file/fileSlice.js new file mode 100644 index 00000000..ff597ff4 --- /dev/null +++ b/src/features/file/fileSlice.js @@ -0,0 +1,51 @@ +import { createSlice } from "@reduxjs/toolkit" +import { createSelector } from "reselect" +import { downloadFile } from "@/common/util" +import { selectState } from "@/features/app/appSlice" +import SandifyExporter from "./SandifyExporter" + +export const fileOptions = { + fileName: { + title: "File name", + type: "string", + }, +} + +// ------------------------------ +// Slice, reducers and atomic actions +// ------------------------------ + +const fileSlice = createSlice({ + name: "file", + initialState: { + fileName: "sandify", + }, + reducers: { + updateFile(state, action) { + Object.assign(state, action.payload) + }, + }, +}) + +export const { updateFile } = fileSlice.actions +export default fileSlice.reducer + +// ------------------------------ +// Selectors +// ------------------------------ + +export const selectFileState = createSelector( + selectState, + (state) => state.file, +) + +// ------------------------------ +// Compound actions (thunks) +// ------------------------------ +export const download = (fileName) => { + return (dispatch, getState) => { + const state = getState() + const exporter = new SandifyExporter() + downloadFile(fileName, exporter.export(state), "text/plain;charset=utf-8") + } +} diff --git a/src/features/import/LayerImporter.js b/src/features/import/ImportUploader.js similarity index 87% rename from src/features/import/LayerImporter.js rename to src/features/import/ImportUploader.js index 21da8261..0f6d5b99 100644 --- a/src/features/import/LayerImporter.js +++ b/src/features/import/ImportUploader.js @@ -7,13 +7,12 @@ import GCodeImporter from "@/features/import/GCodeImporter" import { addLayer } from "@/features/layers/layersSlice" import Layer from "@/features/layers/Layer" -const LayerImporter = ({ toggleModal, showModal }) => { +const ImportUploader = ({ toggleModal, showModal }) => { const machineState = useSelector(selectMachine) const dispatch = useDispatch() const inputRef = useRef() useEffect(() => { - console.log(showModal) if (showModal && inputRef.current) { inputRef.current.click() } @@ -34,13 +33,13 @@ const LayerImporter = ({ toggleModal, showModal }) => { } const handleFileSelected = (event) => { - let file = event.target.files[0] + const file = event.target.files[0] if (file) { - let reader = new FileReader() + const reader = new FileReader() reader.onload = (event) => { - var text = reader.result + const text = reader.result let importer if (file.name.toLowerCase().endsWith(".thr")) { @@ -61,7 +60,7 @@ const LayerImporter = ({ toggleModal, showModal }) => { return ( { ) } -export default React.memo(LayerImporter) +export default React.memo(ImportUploader) diff --git a/src/features/preview/PreviewManager.js b/src/features/preview/PreviewManager.js index 19515304..438453b7 100644 --- a/src/features/preview/PreviewManager.js +++ b/src/features/preview/PreviewManager.js @@ -3,7 +3,6 @@ import { useSelector, useDispatch } from "react-redux" import Select from "react-select" import Slider from "rc-slider" import "rc-slider/assets/index.css" -import { selectFontsState } from "@/features/fonts/fontsSlice" import { updateEffect, selectCurrentEffect, @@ -21,7 +20,6 @@ import PreviewWindow from "./PreviewWindow" const PreviewManager = () => { const dispatch = useDispatch() - const fonts = useSelector(selectFontsState) const currentLayer = useSelector(selectCurrentLayer) const currentEffectLayer = useSelector(selectCurrentEffect) const sliderValue = useSelector(selectPreviewSliderValue) @@ -90,10 +88,6 @@ const PreviewManager = () => { } } - if (!fonts.loaded) { - return
- } - return (
Date: Tue, 5 Sep 2023 06:50:18 -0400 Subject: [PATCH 077/126] add error fallback to protect against app data corruption --- package-lock.json | 175 ++++++++++++++++++------------ package.json | 1 + src/features/app/App.js | 84 +++++++------- src/features/app/ErrorFallback.js | 56 ++++++++++ src/features/app/Header.js | 2 +- src/features/app/store.js | 2 +- 6 files changed, 211 insertions(+), 109 deletions(-) create mode 100644 src/features/app/ErrorFallback.js diff --git a/package-lock.json b/package-lock.json index 6c19d818..0bae4657 100644 --- a/package-lock.json +++ b/package-lock.json @@ -37,6 +37,7 @@ "react": "18.2", "react-bootstrap": "^2.8.0", "react-dom": "^18.2", + "react-error-boundary": "^4.0.11", "react-ga": "^3.3.1", "react-icons": "^4.2.0", "react-konva": "^18.2.10", @@ -3919,9 +3920,9 @@ "dev": true }, "node_modules/browserslist": { - "version": "4.20.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.20.2.tgz", - "integrity": "sha512-CQOBCqp/9pDvDbx3xfMi+86pr4KXIf2FDkTTdeuYw8OxS9t898LA1Khq57gtufFILXpfgsSx5woNgsBgvGjpsA==", + "version": "4.21.10", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.10.tgz", + "integrity": "sha512-bipEBdZfVH5/pwrvqc+Ub0kUPVfGUhlKxbvfD+z1BDnPEO/X98ruXGA1WP5ASpAFKan7Qr6j736IacbZQuAlKQ==", "funding": [ { "type": "opencollective", @@ -3930,14 +3931,17 @@ { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } ], "dependencies": { - "caniuse-lite": "^1.0.30001317", - "electron-to-chromium": "^1.4.84", - "escalade": "^3.1.1", - "node-releases": "^2.0.2", - "picocolors": "^1.0.0" + "caniuse-lite": "^1.0.30001517", + "electron-to-chromium": "^1.4.477", + "node-releases": "^2.0.13", + "update-browserslist-db": "^1.0.11" }, "bin": { "browserslist": "cli.js" @@ -4073,9 +4077,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001516", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001516.tgz", - "integrity": "sha512-Wmec9pCBY8CWbmI4HsjBeQLqDTqV91nFVR83DnZpYyRnPI1wePDsTg0bGLPC5VU/3OIZV1fmxEea1b+tFKe86g==", + "version": "1.0.30001527", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001527.tgz", + "integrity": "sha512-YkJi7RwPgWtXVSgK4lG9AHH57nSzvvOp9MesgXmw4Q7n0C3H04L0foHqfxcmSAm5AcWb8dW9AYj2tR7/5GnddQ==", "funding": [ { "type": "opencollective", @@ -4382,28 +4386,18 @@ } }, "node_modules/core-js-compat": { - "version": "3.21.1", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.21.1.tgz", - "integrity": "sha512-gbgX5AUvMb8gwxC7FLVWYT7Kkgu/y7+h/h1X43yJkNqhlK2fuYyQimqvKGNZFAY6CKii/GFKJ2cp/1/42TN36g==", + "version": "3.32.1", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.32.1.tgz", + "integrity": "sha512-GSvKDv4wE0bPnQtjklV101juQ85g6H3rm5PDP20mqlS5j0kXF3pP97YvAu5hl+uFHqMictp3b2VxOHljWMAtuA==", "dev": true, "dependencies": { - "browserslist": "^4.19.1", - "semver": "7.0.0" + "browserslist": "^4.21.10" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/core-js" } }, - "node_modules/core-js-compat/node_modules/semver": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz", - "integrity": "sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, "node_modules/core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", @@ -5255,9 +5249,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.4.106", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.106.tgz", - "integrity": "sha512-ZYfpVLULm67K7CaaGP7DmjyeMY4naxsbTy+syVVxT6QHI1Ww8XbJjmr9fDckrhq44WzCrcC5kH3zGpdusxwwqg==" + "version": "1.4.508", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.508.tgz", + "integrity": "sha512-FFa8QKjQK/A5QuFr2167myhMesGrhlOBD+3cYNxO9/S4XzHEXesyTD/1/xF644gC8buFPz3ca6G1LOQD0tZrrg==" }, "node_modules/email-addresses": { "version": "3.1.0", @@ -8928,9 +8922,9 @@ "dev": true }, "node_modules/node-releases": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.2.tgz", - "integrity": "sha512-XxYDdcQ6eKqp/YjI+tb2C5WM2LgjnZrfYg4vgQt49EK268b6gYCHsBLrK2qvJo4FmCtqmKezb0WZFK4fkrZNsg==" + "version": "2.0.13", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", + "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==" }, "node_modules/node-sass": { "version": "9.0.0", @@ -9967,6 +9961,17 @@ "react": "^18.2.0" } }, + "node_modules/react-error-boundary": { + "version": "4.0.11", + "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-4.0.11.tgz", + "integrity": "sha512-U13ul67aP5DOSPNSCWQ/eO0AQEYzEFkVljULQIjMV0KlffTAhxuDoBKdO0pb/JZ8mDhMKFZ9NZi0BmLGUiNphw==", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "peerDependencies": { + "react": ">=16.13.1" + } + }, "node_modules/react-ga": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/react-ga/-/react-ga-3.3.1.tgz", @@ -11654,6 +11659,35 @@ "node": ">=8" } }, + "node_modules/update-browserslist-db": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz", + "integrity": "sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, "node_modules/uri-js": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", @@ -11915,9 +11949,9 @@ } }, "node_modules/word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", "dev": true, "engines": { "node": ">=0.10.0" @@ -14888,15 +14922,14 @@ "dev": true }, "browserslist": { - "version": "4.20.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.20.2.tgz", - "integrity": "sha512-CQOBCqp/9pDvDbx3xfMi+86pr4KXIf2FDkTTdeuYw8OxS9t898LA1Khq57gtufFILXpfgsSx5woNgsBgvGjpsA==", + "version": "4.21.10", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.10.tgz", + "integrity": "sha512-bipEBdZfVH5/pwrvqc+Ub0kUPVfGUhlKxbvfD+z1BDnPEO/X98ruXGA1WP5ASpAFKan7Qr6j736IacbZQuAlKQ==", "requires": { - "caniuse-lite": "^1.0.30001317", - "electron-to-chromium": "^1.4.84", - "escalade": "^3.1.1", - "node-releases": "^2.0.2", - "picocolors": "^1.0.0" + "caniuse-lite": "^1.0.30001517", + "electron-to-chromium": "^1.4.477", + "node-releases": "^2.0.13", + "update-browserslist-db": "^1.0.11" } }, "bser": { @@ -14988,9 +15021,9 @@ } }, "caniuse-lite": { - "version": "1.0.30001516", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001516.tgz", - "integrity": "sha512-Wmec9pCBY8CWbmI4HsjBeQLqDTqV91nFVR83DnZpYyRnPI1wePDsTg0bGLPC5VU/3OIZV1fmxEea1b+tFKe86g==" + "version": "1.0.30001527", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001527.tgz", + "integrity": "sha512-YkJi7RwPgWtXVSgK4lG9AHH57nSzvvOp9MesgXmw4Q7n0C3H04L0foHqfxcmSAm5AcWb8dW9AYj2tR7/5GnddQ==" }, "canvas": { "version": "2.9.1", @@ -15216,21 +15249,12 @@ "integrity": "sha512-FRq5b/VMrWlrmCzwRrpDYNxyHP9BcAZC+xHJaqTgIE5091ZV1NTmyh0sGOg5XqpnHvR0svdy0sv1gWA1zmhxig==" }, "core-js-compat": { - "version": "3.21.1", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.21.1.tgz", - "integrity": "sha512-gbgX5AUvMb8gwxC7FLVWYT7Kkgu/y7+h/h1X43yJkNqhlK2fuYyQimqvKGNZFAY6CKii/GFKJ2cp/1/42TN36g==", + "version": "3.32.1", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.32.1.tgz", + "integrity": "sha512-GSvKDv4wE0bPnQtjklV101juQ85g6H3rm5PDP20mqlS5j0kXF3pP97YvAu5hl+uFHqMictp3b2VxOHljWMAtuA==", "dev": true, "requires": { - "browserslist": "^4.19.1", - "semver": "7.0.0" - }, - "dependencies": { - "semver": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz", - "integrity": "sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==", - "dev": true - } + "browserslist": "^4.21.10" } }, "core-util-is": { @@ -15849,9 +15873,9 @@ } }, "electron-to-chromium": { - "version": "1.4.106", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.106.tgz", - "integrity": "sha512-ZYfpVLULm67K7CaaGP7DmjyeMY4naxsbTy+syVVxT6QHI1Ww8XbJjmr9fDckrhq44WzCrcC5kH3zGpdusxwwqg==" + "version": "1.4.508", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.508.tgz", + "integrity": "sha512-FFa8QKjQK/A5QuFr2167myhMesGrhlOBD+3cYNxO9/S4XzHEXesyTD/1/xF644gC8buFPz3ca6G1LOQD0tZrrg==" }, "email-addresses": { "version": "3.1.0", @@ -18463,9 +18487,9 @@ "dev": true }, "node-releases": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.2.tgz", - "integrity": "sha512-XxYDdcQ6eKqp/YjI+tb2C5WM2LgjnZrfYg4vgQt49EK268b6gYCHsBLrK2qvJo4FmCtqmKezb0WZFK4fkrZNsg==" + "version": "2.0.13", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", + "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==" }, "node-sass": { "version": "9.0.0", @@ -19210,6 +19234,14 @@ "scheduler": "^0.23.0" } }, + "react-error-boundary": { + "version": "4.0.11", + "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-4.0.11.tgz", + "integrity": "sha512-U13ul67aP5DOSPNSCWQ/eO0AQEYzEFkVljULQIjMV0KlffTAhxuDoBKdO0pb/JZ8mDhMKFZ9NZi0BmLGUiNphw==", + "requires": { + "@babel/runtime": "^7.12.5" + } + }, "react-ga": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/react-ga/-/react-ga-3.3.1.tgz", @@ -20480,6 +20512,15 @@ "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==", "dev": true }, + "update-browserslist-db": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz", + "integrity": "sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==", + "requires": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + } + }, "uri-js": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", @@ -20670,9 +20711,9 @@ } }, "word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", "dev": true }, "wrap-ansi": { diff --git a/package.json b/package.json index 564ef1a1..9df3de85 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "react": "18.2", "react-bootstrap": "^2.8.0", "react-dom": "^18.2", + "react-error-boundary": "^4.0.11", "react-ga": "^3.3.1", "react-icons": "^4.2.0", "react-konva": "^18.2.10", diff --git a/src/features/app/App.js b/src/features/app/App.js index 90d1c2fb..88856c0b 100644 --- a/src/features/app/App.js +++ b/src/features/app/App.js @@ -2,11 +2,13 @@ import React, { useState } from "react" import Tab from "react-bootstrap/Tab" import { Provider } from "react-redux" import { ToastContainer } from "react-toastify" +import { ErrorBoundary } from "react-error-boundary" +import PreviewManager from "@/features/preview/PreviewManager" import Header from "./Header" import About from "./About" -import PreviewManager from "@/features/preview/PreviewManager" import Sidebar from "./Sidebar" import store from "./store" +import ErrorFallback from "./ErrorFallback" import "./App.scss" const App = () => { @@ -14,47 +16,49 @@ const App = () => { return ( -
- -
-
- - - -
-
- -
-
-
+ + + + + + +
+ ) } diff --git a/src/features/app/ErrorFallback.js b/src/features/app/ErrorFallback.js new file mode 100644 index 00000000..9b0dea59 --- /dev/null +++ b/src/features/app/ErrorFallback.js @@ -0,0 +1,56 @@ +import React from "react" +import Container from "react-bootstrap/Container" +import Row from "react-bootstrap/Row" +import Col from "react-bootstrap/Col" +import Button from "react-bootstrap/Button" +import Alert from "react-bootstrap/Alert" +import { RiEmotionUnhappyLine } from "react-icons/ri" +import { useDispatch } from "react-redux" + +const ErrorFallback = ({ error }) => { + const dispatch = useDispatch() + + const handleReset = () => { + dispatch({ type: "RESET_PATTERN" }) + window.location.reload() + } + + return ( + + + + +

Sorry! Something went wrong.

+ +
{error.message}
+
+

+ To fix this, we need to reset your pattern. If you are getting this + error while trying to open a saved pattern, there is likely + something wrong with the file. If you think you've found an + issue with Sandify, please let us know on{" "} + Github. +

+ + +
+
+ ) +} + +export default ErrorFallback diff --git a/src/features/app/Header.js b/src/features/app/Header.js index 653c1976..b0c02eb9 100644 --- a/src/features/app/Header.js +++ b/src/features/app/Header.js @@ -22,7 +22,7 @@ const Header = ({ eventKey, setEventKey }) => { const dispatch = useDispatch() const handleNew = () => { - dispatch({ type: "NEW_PATTERN" }) + dispatch({ type: "RESET_PATTERN" }) } return ( diff --git a/src/features/app/store.js b/src/features/app/store.js index 9b4db495..1a325125 100644 --- a/src/features/app/store.js +++ b/src/features/app/store.js @@ -44,7 +44,7 @@ const combinedReducer = combineReducers({ }) const rootReducer = (state, action) => { - if (action.type === "NEW_PATTERN") { + if (action.type === "RESET_PATTERN") { const newState = { ...state, layers: undefined, From 178db230cfc37aebe8d9846ca5ac94df831d3da3 Mon Sep 17 00:00:00 2001 From: Bob Carmichael Date: Tue, 5 Sep 2023 07:49:09 -0400 Subject: [PATCH 078/126] add integrity checking when loading patterns --- src/features/app/store.js | 4 ++ src/features/effects/EffectLayer.js | 5 ++ src/features/file/SandifyExporter.js | 9 +++- src/features/file/SandifyImporter.js | 68 +++++++++++++++++++++++++++- src/features/layers/Layer.js | 5 ++ 5 files changed, 88 insertions(+), 3 deletions(-) diff --git a/src/features/app/store.js b/src/features/app/store.js index 1a325125..55b1f589 100644 --- a/src/features/app/store.js +++ b/src/features/app/store.js @@ -58,6 +58,10 @@ const rootReducer = (state, action) => { layers, effects, } + + const id = newState.layers.ids[0] + newState.layers.current = id + newState.layers.selected = id return combinedReducer(newState, action) } diff --git a/src/features/effects/EffectLayer.js b/src/features/effects/EffectLayer.js index b6e07d2c..e4ccf97e 100644 --- a/src/features/effects/EffectLayer.js +++ b/src/features/effects/EffectLayer.js @@ -75,4 +75,9 @@ export default class EffectLayer { getSelectionVertices(effect) { return this.model.getSelectionVertices(effect) } + + // used to preserve hidden attributes when loading from a file + getHiddenAttrs() { + return ["layerId"] + } } diff --git a/src/features/file/SandifyExporter.js b/src/features/file/SandifyExporter.js index b8199a80..50db6855 100644 --- a/src/features/file/SandifyExporter.js +++ b/src/features/file/SandifyExporter.js @@ -1,10 +1,15 @@ export default class SandifyExporter { export(state) { const json = { - effects: state.effects, - layers: state.layers, + effects: { ...state.effects }, + layers: { ...state.layers }, } + delete json.layers.selected + delete json.layers.current + delete json.effects.selected + delete json.effects.current + return JSON.stringify(json, null, "\t") } } diff --git a/src/features/file/SandifyImporter.js b/src/features/file/SandifyImporter.js index 6ae35e72..20c0c25e 100644 --- a/src/features/file/SandifyImporter.js +++ b/src/features/file/SandifyImporter.js @@ -1,7 +1,73 @@ +import Layer from "@/features/layers/Layer" +import EffectLayer from "@/features/effects/EffectLayer" + export default class SandifyImporter { import(stateString) { - const state = JSON.parse(stateString) + let state = JSON.parse(stateString) + + this.checkStructure(state) + this.ensureIntegrity(state, "layers", Layer) + this.ensureIntegrity(state, "effects", EffectLayer) + this.ensureLayerExists(state) return state } + + checkStructure(state) { + if ( + !( + state.layers && + state.effects && + state.layers.entities && + state.layers.ids && + state.effects.entities && + state.effects.ids + ) + ) { + throw new Error( + "Invalid file format. The JSON structure must contain 'layers' and 'effects', both with 'entities' and 'ids'", + ) + } + } + + ensureIntegrity(state, slice, LayerClass) { + // ensure entities only contains data for the provided ids + state[slice].entities = state[slice].ids.reduce((entities, id) => { + const data = state[slice].entities[id] + + if (data) { + const instance = new LayerClass(data.type) + const layer = instance.getInitialState() + + // ensure each entity only contains attributes defined by the layer + Object.keys(layer).forEach((attr) => { + if (data[attr] != undefined) { + layer[attr] = data[attr] + } + }) + + instance.getHiddenAttrs().forEach((attr) => { + layer[attr] = data[attr] + }) + + entities[id] = { + ...layer, + id, + } + } + + return entities + }, {}) + + // ensure only ids with valid entities remain + state[slice].ids = state[slice].ids.filter((id) => { + return !!state[slice].entities[id] + }) + } + + ensureLayerExists(state) { + if (!state.layers.ids.length > 0) { + throw new Error("Pattern cannot be empty.") + } + } } diff --git a/src/features/layers/Layer.js b/src/features/layers/Layer.js index 7c3f66f3..a3c35021 100644 --- a/src/features/layers/Layer.js +++ b/src/features/layers/Layer.js @@ -124,4 +124,9 @@ export default class Layer { this.vertices = effectLayer.getVertices(effect, this.state, this.vertices) }) } + + // used to preserve hidden attributes when loading from a file + getHiddenAttrs() { + return [] + } } From 3de365cd397ab1ac051743e177b38071ebbf0b0a Mon Sep 17 00:00:00 2001 From: Bob Carmichael Date: Wed, 6 Sep 2023 07:57:57 -0400 Subject: [PATCH 079/126] move root reducer into its own file --- src/features/app/rootSlice.js | 59 +++++++++++++++++++++++++++++++++++ src/features/app/store.js | 52 +++--------------------------- 2 files changed, 63 insertions(+), 48 deletions(-) create mode 100644 src/features/app/rootSlice.js diff --git a/src/features/app/rootSlice.js b/src/features/app/rootSlice.js new file mode 100644 index 00000000..93358bb4 --- /dev/null +++ b/src/features/app/rootSlice.js @@ -0,0 +1,59 @@ +import { combineReducers } from "redux" +import machineReducer from "@/features/machine/machineSlice" +import exporterReducer from "@/features/export/exporterSlice" +import previewReducer from "@/features/preview/previewSlice" +import fontsReducer from "@/features/fonts/fontsSlice" +import layersReducer from "@/features/layers/layersSlice" +import effectsReducer from "@/features/effects/effectsSlice" +import fileReducer from "@/features/file/fileSlice" +import appReducer from "./appSlice" + +const combinedReducer = combineReducers({ + app: appReducer, + effects: effectsReducer, + exporter: exporterReducer, + file: fileReducer, + fonts: fontsReducer, + layers: layersReducer, + machine: machineReducer, + preview: previewReducer, +}) + +const resetPattern = (state, action) => { + const newState = JSON.parse(JSON.stringify(state)) // deep copy + + newState.layers = undefined + newState.effects = undefined + newState.preview.zoom = 1.0 + newState.preview.sliderValue = 0.0 + + return combinedReducer(newState, action) +} + +const loadPattern = (state, action) => { + const { effects, layers } = action.payload + const newState = JSON.parse(JSON.stringify(state)) // deep copy + + newState.layers = layers + newState.effects = effects + + const id = newState.layers.ids[0] + newState.layers.current = id + newState.layers.selected = id + newState.preview.sliderValue = 1.0 + newState.preview.zoom = 1.0 + + return combinedReducer(newState, action) +} + +const rootReducer = (state, action) => { + if (action.type === "RESET_PATTERN") { + return resetPattern(state, action) + } else if (action.type === "LOAD_PATTERN") { + return loadPattern(state, action) + } + + return combinedReducer(state, action) +} + +export default rootReducer diff --git a/src/features/app/store.js b/src/features/app/store.js index 55b1f589..6ce24b0f 100644 --- a/src/features/app/store.js +++ b/src/features/app/store.js @@ -1,24 +1,16 @@ import { configureStore } from "@reduxjs/toolkit" -import { combineReducers } from "redux" -import machineReducer from "@/features/machine/machineSlice" -import exporterReducer from "@/features/export/exporterSlice" -import previewReducer from "@/features/preview/previewSlice" -import fontsReducer from "@/features/fonts/fontsSlice" -import layersReducer from "@/features/layers/layersSlice" -import effectsReducer from "@/features/effects/effectsSlice" -import fileReducer from "@/features/file/fileSlice" import { loadState, saveState } from "@/common/localStorage" import { resetLogCounts } from "@/common/debugging" -import appReducer from "./appSlice" +import rootReducer from "./rootSlice" -// set both to true when running locally if you want to preserve your shape -// settings across page loads; don't forget to toggle false when done testing! +// by default, state is always persisted in local storage const usePersistedState = true const persistState = true // if you want to save a multiple temporary states, use these keys. The first time // you save a new state, change persistSaveKey. Make a change, then change -// persistInitKey to the same value. It's like doing a "save as" +// persistInitKey to the same value. persistSaveKey is mostly obsolete now +// given that a user can save their pattern to a file. const persistInitKey = "state" const persistSaveKey = "state" @@ -32,42 +24,6 @@ if (persistedState) { persistedState.fonts.loaded = false } -const combinedReducer = combineReducers({ - app: appReducer, - effects: effectsReducer, - exporter: exporterReducer, - file: fileReducer, - fonts: fontsReducer, - layers: layersReducer, - machine: machineReducer, - preview: previewReducer, -}) - -const rootReducer = (state, action) => { - if (action.type === "RESET_PATTERN") { - const newState = { - ...state, - layers: undefined, - effects: undefined, - } - return combinedReducer(newState, action) - } else if (action.type === "LOAD_PATTERN") { - const { effects, layers } = action.payload - const newState = { - ...state, - layers, - effects, - } - - const id = newState.layers.ids[0] - newState.layers.current = id - newState.layers.selected = id - return combinedReducer(newState, action) - } - - return combinedReducer(state, action) -} - const store = configureStore({ reducer: rootReducer, preloadedState: persistedState, From 1cd7fe0fbb36a439cb31db4f6ff7fa4abed52dbb Mon Sep 17 00:00:00 2001 From: Bob Carmichael Date: Wed, 6 Sep 2023 07:58:34 -0400 Subject: [PATCH 080/126] minor fixes/cosmetics --- src/components/CheckboxOption.js | 2 +- src/components/DropdownOption.js | 9 ++++----- src/components/ToggleButtonOption.js | 4 ++-- src/features/app/About.js | 4 ++-- src/features/layers/LayerEditor.js | 2 +- src/features/layers/layersSlice.js | 4 ++++ src/features/shapes/Wiper.js | 2 +- 7 files changed, 15 insertions(+), 12 deletions(-) diff --git a/src/components/CheckboxOption.js b/src/components/CheckboxOption.js index 901328a3..7bba7fe8 100644 --- a/src/components/CheckboxOption.js +++ b/src/components/CheckboxOption.js @@ -37,7 +37,7 @@ const CheckboxOption = ({ options, optionKey, data, object, onChange }) => { { - return { value: key, label: option.choices[key] } + return { value: key, label: choices[key] } }) - const currentLabel = Array.isArray(choices) - ? currentChoice - : choices[currentChoice] - + const currentLabel = choices.find( + (choice) => choice.value == currentChoice, + ).label const handleChange = (choice) => { const value = choice.value let attrs = {} diff --git a/src/components/ToggleButtonOption.js b/src/components/ToggleButtonOption.js index e58c6f22..1a6782c4 100644 --- a/src/components/ToggleButtonOption.js +++ b/src/components/ToggleButtonOption.js @@ -20,12 +20,12 @@ const ToggleButtonOption = (props) => { } return ( - + - {option.title} + {option.title} { Part of the fun of Sandify is playing it like you would a xylophone. Try it out first. The goal is to make it easy to make your first pattern by just clicking and scrolling, finding - something you like. Check out{" "} - the wiki for + something you like. Check out the{" "} + wiki for some features that you might miss the first time through.

Exporting patterns

diff --git a/src/features/layers/LayerEditor.js b/src/features/layers/LayerEditor.js index 7ccd4c6f..2e3a3826 100644 --- a/src/features/layers/LayerEditor.js +++ b/src/features/layers/LayerEditor.js @@ -116,6 +116,7 @@ const LayerEditor = () => { {renderedModelSelection} {renderedModelOptions} + {getOptionComponent(model, layerOptions, "connectionMethod")} {renderedLink}
@@ -166,7 +167,6 @@ const LayerEditor = () => { )} - {getOptionComponent(model, layerOptions, "connectionMethod")}
) diff --git a/src/features/layers/layersSlice.js b/src/features/layers/layersSlice.js index 7068006a..1f2b0915 100644 --- a/src/features/layers/layersSlice.js +++ b/src/features/layers/layersSlice.js @@ -131,6 +131,10 @@ const layersSlice = createSlice({ newLayer.y = 0 } + if (!instance.model.canRotate(state)) { + newLayer.rotation = 0 + } + layersAdapter.setOne(state, newLayer) }, restoreDefaults: (state, action) => { diff --git a/src/features/shapes/Wiper.js b/src/features/shapes/Wiper.js index f572bb29..340bbb6f 100644 --- a/src/features/shapes/Wiper.js +++ b/src/features/shapes/Wiper.js @@ -4,7 +4,7 @@ import Shape from "./Shape" const options = { wiperType: { - title: "Type", + title: "Wiper type", type: "togglebutton", choices: ["Lines", "Spiral"], }, From 1baf93434339fd15a1b76ccb7d1fbeb8c78278d0 Mon Sep 17 00:00:00 2001 From: Bob Carmichael Date: Sat, 9 Sep 2023 08:45:58 -0400 Subject: [PATCH 081/126] maintain aspect ratio for improved sizing; minor fixes and cosmetics --- src/common/Model.js | 3 +- src/common/geometry.js | 35 +++-- src/components/CheckboxOption.js | 23 +++- src/features/app/rootSlice.js | 1 + src/features/effects/EffectEditor.js | 128 ++++++++++++++---- src/features/effects/EffectLayer.js | 39 +++++- src/features/effects/EffectList.js | 39 +++--- src/features/effects/EffectManager.js | 4 +- src/features/effects/Fisheye.js | 4 +- src/features/effects/Mask.js | 5 +- src/features/effects/Track.js | 4 +- src/features/effects/Transformer.js | 3 +- src/features/effects/Warp.js | 2 +- src/features/layers/Layer.js | 46 +++++-- src/features/layers/LayerEditor.js | 141 ++++++++++++++------ src/features/layers/LayerList.js | 41 +++--- src/features/layers/LayerManager.js | 2 +- src/features/preview/EffectPreview.js | 2 +- src/features/preview/ShapePreview.js | 17 ++- src/features/shapes/FancyText.js | 2 + src/features/shapes/Freeform.js | 2 +- src/features/shapes/Point.js | 4 + src/features/shapes/Shape.js | 26 ++-- src/features/shapes/input_text/InputText.js | 2 + 24 files changed, 397 insertions(+), 178 deletions(-) diff --git a/src/common/Model.js b/src/common/Model.js index 0bcd1d9b..feeae70b 100644 --- a/src/common/Model.js +++ b/src/common/Model.js @@ -9,6 +9,7 @@ export default class Model { usesMachine: false, usesFonts: false, dragging: false, + stretch: false, }) } @@ -18,7 +19,7 @@ export default class Model { } // override as needed - canChangeHeight(state) { + canChangeAspectRatio(state) { return this.canChangeSize(state) } diff --git a/src/common/geometry.js b/src/common/geometry.js index 2cf15f7f..96c11a92 100644 --- a/src/common/geometry.js +++ b/src/common/geometry.js @@ -121,29 +121,40 @@ export const dimensions = (vertices) => { } } -// resizes each vertex in a list to the specified dimensions. -// If lock = true, will not stretch the shape. -// resizes each vertex in a list to the specified dimensions. Will not stretch the shape. -export const resizeVertices = (vertices, sizeX, sizeY, lock = true) => { +// resizes each vertex in a list to the specified dimensions; the stretch parameter determines +// whether to stretch the shape to fit the dimensions. +export const resizeVertices = ( + vertices, + sizeX, + sizeY, + stretch = false, + aspectRatio = 1.0, +) => { let scaleX, scaleY, deltaX, deltaY const bounds = findBounds(vertices) const oldSizeX = bounds[1].x - bounds[0].x const oldSizeY = bounds[1].y - bounds[0].y - if (lock) { + if (stretch) { + scaleX = sizeX / oldSizeX + scaleY = sizeY / oldSizeY + deltaX = 0 + deltaY = 0 + } else { const size = Math.max(sizeX, sizeY) const oldSize = Math.max(oldSizeX, oldSizeY) - scaleX = size / oldSize - scaleY = size / oldSize + + if (aspectRatio > 1) { + scaleX = size / oldSize + scaleY = size / aspectRatio / oldSize + } else { + scaleX = (size * aspectRatio) / oldSize + scaleY = size / oldSize + } bounds[0].multiply({ x: scaleX, y: scaleY }) bounds[1].multiply({ x: scaleX, y: scaleY }) deltaX = bounds[0].x / 2 deltaY = bounds[0].y / 2 - } else { - scaleX = sizeX / oldSizeX - scaleY = sizeY / oldSizeY - deltaX = 0 - deltaY = 0 } vertices.forEach((vertex) => { diff --git a/src/components/CheckboxOption.js b/src/components/CheckboxOption.js index 7bba7fe8..00e14da3 100644 --- a/src/components/CheckboxOption.js +++ b/src/components/CheckboxOption.js @@ -5,7 +5,14 @@ import Form from "react-bootstrap/Form" import S from "react-switch" const Switch = S.default ? S.default : S // Fix: https://github.com/vitejs/vite/issues/2139 -const CheckboxOption = ({ options, optionKey, data, object, onChange }) => { +const CheckboxOption = ({ + options, + optionKey, + data, + object, + onChange, + label = true, +}) => { const option = options[optionKey] const visible = option.isVisible === undefined ? true : option.isVisible(object, data) @@ -27,12 +34,14 @@ const CheckboxOption = ({ options, optionKey, data, object, onChange }) => { sm={5} className="mb-1" > - - {option.title} - + {label && ( + + {option.title} + + )} { const id = newState.layers.ids[0] newState.layers.current = id newState.layers.selected = id + newState.effects.selected = newState.layers.entities[id].effectIds[0] newState.preview.sliderValue = 1.0 newState.preview.zoom = 1.0 diff --git a/src/features/effects/EffectEditor.js b/src/features/effects/EffectEditor.js index a4e5dca7..41e837c6 100644 --- a/src/features/effects/EffectEditor.js +++ b/src/features/effects/EffectEditor.js @@ -1,9 +1,15 @@ import React from "react" import { useDispatch, useSelector } from "react-redux" import { IconContext } from "react-icons" -import { AiOutlineRotateRight } from "react-icons/ai" +import { + AiOutlineRotateRight, + AiTwotoneLock, + AiTwotoneUnlock, +} from "react-icons/ai" import Col from "react-bootstrap/Col" import Row from "react-bootstrap/Row" +import Button from "react-bootstrap/Button" +import { Tooltip } from "react-tooltip" import InputOption from "@/components/InputOption" import DropdownOption from "@/components/DropdownOption" import CheckboxOption from "@/components/CheckboxOption" @@ -26,6 +32,15 @@ const EffectEditor = ({ id }) => { dispatch(updateEffect(attrs)) } + const handleChangeMaintainAspectRatio = (value) => { + dispatch( + updateEffect({ + id: effect.id, + maintainAspectRatio: !effect.maintainAspectRatio, + }), + ) + } + const getOptionComponent = (model, options, key, label = true) => { const option = options[key] const props = { @@ -55,41 +70,96 @@ const EffectEditor = ({ id }) => { )) return ( -
+
{renderedModelOptions} {model.canTransform(effect) && ( - Transform - - {model.canMove(effect) && ( - - {getOptionComponent(model, layerOptions, "x")} - {getOptionComponent(model, layerOptions, "y")} - - )} - {model.canChangeSize(effect) && ( - - - {getOptionComponent(model, layerOptions, "width")} - - - {getOptionComponent(model, layerOptions, "height")} - - - )} - {model.canRotate(effect) && ( - - -
-
+ Transform + +
+
+ + {model.canMove(effect) && ( + + {getOptionComponent(model, layerOptions, "x")} + + )} + {model.canChangeSize(effect) && ( + + {getOptionComponent(model, layerOptions, "width")} + + )} + + + {model.canMove(effect) && ( + + {getOptionComponent(model, layerOptions, "y")} + + )} + {model.canChangeSize(effect) && ( + + {getOptionComponent(model, layerOptions, "height")} + + )} + +
+ {model.canChangeAspectRatio(effect) && ( +
+ + +
+ )} +
+ {model.canRotate(effect) && ( +
+ + +
+
+ + + +
+ {getOptionComponent( + model, + layerOptions, + "rotation", + false, + )}
- {getOptionComponent(model, layerOptions, "rotation", false)} + +
+ {/* hack to get spacing to work */} + {model.canChangeAspectRatio(effect) && ( +
+
- - + )} +
)} diff --git a/src/features/effects/EffectLayer.js b/src/features/effects/EffectLayer.js index e4ccf97e..66f1b30a 100644 --- a/src/features/effects/EffectLayer.js +++ b/src/features/effects/EffectLayer.js @@ -16,17 +16,17 @@ export const effectOptions = { }, }, width: { - title: (model, state) => { - return model.canChangeHeight(state) ? "W" : "S" - }, + title: "W", min: 1, inline: true, isVisible: (model, state) => { return model.canChangeSize(state) }, onChange: (model, changes, state) => { - if (!model.canChangeHeight(state)) { + if (state.maintainAspectRatio) { changes.height = changes.width + } else { + changes.aspectRatio = changes.width / state.height } return changes }, @@ -35,10 +35,19 @@ export const effectOptions = { title: "H", min: 1, inline: true, - isVisible: (model, state) => { - return model.canChangeSize(state) && model.canChangeHeight(state) + onChange: (model, changes, state) => { + if (state.maintainAspectRatio) { + changes.width = changes.height + } else { + changes.aspectRatio = state.width / changes.height + } + return changes }, }, + maintainAspectRatio: { + title: "Lock aspect ratio", + type: "checkbox", + }, rotation: { title: "Rotate (degrees)", inline: true, @@ -54,7 +63,7 @@ export default class EffectLayer { } getInitialState(layer, layerVertices) { - return { + const state = { ...this.model.getInitialState(layer, layerVertices), ...{ type: this.model.type, @@ -62,6 +71,22 @@ export default class EffectLayer { name: this.model.label, }, } + + if (this.model.canChangeSize(state)) { + state.maintainAspectRatio = false + state.aspectRatio = 1.0 + } + + if (this.model.canMove(state) && state.x === undefined) { + state.x = 0 + state.y = 0 + } + + if (this.model.canRotate(state) && state.rotation === undefined) { + state.rotation = 0 + } + + return state } getOptions() { diff --git a/src/features/effects/EffectList.js b/src/features/effects/EffectList.js index 7b63bde1..fabfd7d5 100644 --- a/src/features/effects/EffectList.js +++ b/src/features/effects/EffectList.js @@ -119,25 +119,26 @@ const EffectList = ({ effects, selectedLayer }) => { items={effects.map((effect) => effect.id)} strategy={verticalListSortingStrategy} > - - {effects.map((effect, index) => ( - - ))} - +
+ + {effects.map((effect, index) => ( + + ))} + +
) diff --git a/src/features/effects/EffectManager.js b/src/features/effects/EffectManager.js index b91cab81..217b3dc8 100644 --- a/src/features/effects/EffectManager.js +++ b/src/features/effects/EffectManager.js @@ -120,9 +120,7 @@ const EffectManager = () => {
-
- -
+ diff --git a/src/features/effects/Fisheye.js b/src/features/effects/Fisheye.js index 3cde2d5b..231448c8 100644 --- a/src/features/effects/Fisheye.js +++ b/src/features/effects/Fisheye.js @@ -30,7 +30,7 @@ export default class Fisheye extends Effect { return true } - canChangeHeight(state) { + canChangeAspectRatio(state) { return false } @@ -40,8 +40,6 @@ export default class Fisheye extends Effect { ...{ fisheyeDistortion: 3, fisheyeSubsample: true, - x: 0, - y: 0, width: 100, height: 100, }, diff --git a/src/features/effects/Mask.js b/src/features/effects/Mask.js index cde9f1b9..d6537e2b 100644 --- a/src/features/effects/Mask.js +++ b/src/features/effects/Mask.js @@ -51,7 +51,7 @@ export default class Mask extends Effect { return state.maskMachine != "circle" } - canChangeHeight(state) { + canChangeAspectRatio(state) { return state.maskMachine != "circle" } @@ -65,9 +65,6 @@ export default class Mask extends Effect { ...{ width: 100, height: 100, - x: 0, - y: 0, - rotation: 0, maskMinimizeMoves: false, maskMachine: "rectangle", maskBorder: false, diff --git a/src/features/effects/Track.js b/src/features/effects/Track.js index f59e4741..3cd127c7 100644 --- a/src/features/effects/Track.js +++ b/src/features/effects/Track.js @@ -48,7 +48,7 @@ export default class Track extends Effect { return true } - canChangeHeight(state) { + canChangeAspectRatio(state) { return false } @@ -60,8 +60,6 @@ export default class Track extends Effect { return { ...super.getInitialState(), ...{ - x: 0, - y: 0, width: 50, height: 50, trackShape: "circle", diff --git a/src/features/effects/Transformer.js b/src/features/effects/Transformer.js index 796f7c08..7b79148e 100644 --- a/src/features/effects/Transformer.js +++ b/src/features/effects/Transformer.js @@ -47,7 +47,6 @@ export default class Transformer extends Effect { height, x: offsetX - layer.x, y: offsetY - layer.y, - rotation: 0, }, } } @@ -57,7 +56,7 @@ export default class Transformer extends Effect { this.effect = effect this.vertices = [...vertices] - resizeVertices(this.vertices, effect.width, effect.height, false) + resizeVertices(this.vertices, effect.width, effect.height, true) centerOnOrigin(this.vertices) this.transform() diff --git a/src/features/effects/Warp.js b/src/features/effects/Warp.js index ed7c81dc..7d331e7b 100644 --- a/src/features/effects/Warp.js +++ b/src/features/effects/Warp.js @@ -70,7 +70,7 @@ export default class Warp extends Effect { return state.warpType !== "custom" } - canChangeHeight(state) { + canChangeAspectRatio(state) { return false } diff --git a/src/features/layers/Layer.js b/src/features/layers/Layer.js index a3c35021..1e60f4e7 100644 --- a/src/features/layers/Layer.js +++ b/src/features/layers/Layer.js @@ -1,6 +1,7 @@ import { getShapeFromType } from "@/features/shapes/factory" import EffectLayer from "@/features/effects/EffectLayer" import { resizeVertices, centerOnOrigin } from "@/common/geometry" +import { roundP } from "@/common/util" export const layerOptions = { name: { @@ -22,17 +23,17 @@ export const layerOptions = { }, }, width: { - title: (model, state) => { - return model.canChangeHeight(state) ? "W" : "S" - }, + title: "W", min: 1, inline: true, isVisible: (model, state) => { return model.canChangeSize(state) }, onChange: (model, changes, state) => { - if (!model.canChangeHeight(state)) { - changes.height = changes.width + if (state.maintainAspectRatio) { + changes.height = roundP(changes.width / state.aspectRatio, 2) + } else { + changes.aspectRatio = changes.width / state.height } return changes }, @@ -41,10 +42,19 @@ export const layerOptions = { title: "H", min: 1, inline: true, - isVisible: (model, state) => { - return model.canChangeSize(state) && model.canChangeHeight(state) + onChange: (model, changes, state) => { + if (state.maintainAspectRatio) { + changes.width = roundP(changes.height * state.aspectRatio, 2) + } else { + changes.aspectRatio = state.width / changes.height + } + return changes }, }, + maintainAspectRatio: { + title: "Lock aspect ratio", + type: "checkbox", + }, rotation: { title: "Rotate (degrees)", inline: true, @@ -66,6 +76,7 @@ export default class Layer { getInitialState(props) { const dimensions = this.model.initialDimensions(props) + const { width, height, aspectRatio } = dimensions return { ...this.model.getInitialState(props), @@ -74,8 +85,9 @@ export default class Layer { connectionMethod: "line", x: 0.0, y: 0.0, - width: dimensions.width, - height: dimensions.height, + width, + height, + aspectRatio, rotation: 0, visible: true, name: this.model.label, @@ -105,8 +117,20 @@ export default class Layer { } resize() { - const { width, height } = this.state - this.vertices = resizeVertices(this.vertices, width, height, false) + const { width, height, aspectRatio } = this.state + + if (this.model.stretch) { + // special case for shapes that always stretch to fit their dimensions (e.g., font shapes) + this.vertices = resizeVertices(this.vertices, width, height, true, 1) + } else { + this.vertices = resizeVertices( + this.vertices, + width, + height, + false, + aspectRatio, + ) + } } transform() { diff --git a/src/features/layers/LayerEditor.js b/src/features/layers/LayerEditor.js index 2e3a3826..da7019a1 100644 --- a/src/features/layers/LayerEditor.js +++ b/src/features/layers/LayerEditor.js @@ -2,9 +2,15 @@ import React from "react" import { useDispatch, useSelector } from "react-redux" import Col from "react-bootstrap/Col" import Row from "react-bootstrap/Row" +import Button from "react-bootstrap/Button" import Select from "react-select" +import { Tooltip } from "react-tooltip" import { IconContext } from "react-icons" -import { AiOutlineRotateRight } from "react-icons/ai" +import { + AiOutlineRotateRight, + AiTwotoneLock, + AiTwotoneUnlock, +} from "react-icons/ai" import CommentsBox from "@/components/CommentsBox" import InputOption from "@/components/InputOption" import DropdownOption from "@/components/DropdownOption" @@ -54,6 +60,15 @@ const LayerEditor = () => { dispatch(updateLayer(attrs)) } + const handleChangeMaintainAspectRatio = (value) => { + dispatch( + updateLayer({ + id: layer.id, + maintainAspectRatio: !layer.maintainAspectRatio, + }), + ) + } + const renderedModelSelection = allowModelSelection && ( { {renderedModelOptions} {getOptionComponent(model, layerOptions, "connectionMethod")} {renderedLink} -
-
+
{model.canTransform(layer) && ( - Transform - - {model.canMove(layer) && ( - - - {getOptionComponent(model, layerOptions, "x")} - - - {getOptionComponent(model, layerOptions, "y")} - - - )} - {model.canChangeSize(layer) && ( - - - {getOptionComponent(model, layerOptions, "width")} - - - {getOptionComponent(model, layerOptions, "height")} - - - )} - {model.canRotate(layer) && ( - - -
-
+ Transform + +
+
+ + {model.canMove(layer) && ( + + {getOptionComponent(model, layerOptions, "x")} + + )} + {model.canChangeSize(layer) && ( + + {getOptionComponent(model, layerOptions, "width")} + + )} + + + {model.canMove(layer) && ( + + {getOptionComponent(model, layerOptions, "y")} + + )} + {model.canChangeSize(layer) && ( + + {getOptionComponent(model, layerOptions, "height")} + + )} + +
+ {model.canChangeAspectRatio(layer) && ( +
+ +
- {getOptionComponent( - model, - layerOptions, - "rotation", - false, )} + +
+ )} +
+ {model.canRotate(layer) && ( +
+ + +
+
+ + + +
+ {getOptionComponent( + model, + layerOptions, + "rotation", + false, + )} +
+ +
+ {/* hack to get spacing to work */} + {model.canChangeAspectRatio(layer) && ( +
+
- - + )} +
)} )}
+
+ +
) } diff --git a/src/features/layers/LayerList.js b/src/features/layers/LayerList.js index 1659083a..c03b8bb6 100644 --- a/src/features/layers/LayerList.js +++ b/src/features/layers/LayerList.js @@ -140,26 +140,27 @@ const LayerList = () => { items={layers.map((layer) => layer.id)} strategy={verticalListSortingStrategy} > - - {layers.map((layer, index) => ( - - ))} - +
+ + {layers.map((layer, index) => ( + + ))} + +
) diff --git a/src/features/layers/LayerManager.js b/src/features/layers/LayerManager.js index ad0b3610..23f826fe 100644 --- a/src/features/layers/LayerManager.js +++ b/src/features/layers/LayerManager.js @@ -91,7 +91,7 @@ const LayerManager = () => {
@@ -168,4 +152,4 @@ const EffectEditor = ({ id }) => { ) } -export default EffectEditor +export default React.memo(EffectEditor) diff --git a/src/features/effects/EffectManager.js b/src/features/effects/EffectManager.js index 217b3dc8..31cc8087 100644 --- a/src/features/effects/EffectManager.js +++ b/src/features/effects/EffectManager.js @@ -129,4 +129,4 @@ const EffectManager = () => { ) } -export default EffectManager +export default React.memo(EffectManager) diff --git a/src/features/layers/LayerEditor.js b/src/features/layers/LayerEditor.js index da7019a1..e96f4e64 100644 --- a/src/features/layers/LayerEditor.js +++ b/src/features/layers/LayerEditor.js @@ -12,10 +12,7 @@ import { AiTwotoneUnlock, } from "react-icons/ai" import CommentsBox from "@/components/CommentsBox" -import InputOption from "@/components/InputOption" -import DropdownOption from "@/components/DropdownOption" -import CheckboxOption from "@/components/CheckboxOption" -import ToggleButtonOption from "@/components/ToggleButtonOption" +import ModelOption from "@/components/ModelOption" import { getShapeSelectOptions } from "@/features/shapes/factory" import { updateLayer, changeModelType } from "./layersSlice" import Layer from "./Layer" @@ -94,44 +91,38 @@ const LayerEditor = () => { ) - const getOptionComponent = (model, options, key, label = true) => { - const option = options[key] - const props = { - options, - key, - onChange: handleChange, - optionKey: key, - data: layer, - object: model, - label, - } - - switch (option.type) { - case "dropdown": - return - case "checkbox": - return - case "comments": - return - case "togglebutton": - return - default: - return - } + // this should really be a component, but I could not figure out how to get it + // to not re-render as the value changed; the fallout is that the editor re-renders + // more than it should, but it's not noticeable + const renderOption = ({ + optionKey, + options = layerOptions, + label = true, + }) => { + return ( + + ) } - const renderedModelOptions = Object.keys(modelOptions).map((key) => ( -
{getOptionComponent(model, modelOptions, key)}
- )) + const renderedModelOptions = Object.keys(modelOptions).map((optionKey) => + renderOption({ options: modelOptions, optionKey }), + ) return (
- {getOptionComponent(model, layerOptions, "name")} - + {renderOption({ optionKey: "name" })} {renderedModelSelection} {renderedModelOptions} - {getOptionComponent(model, layerOptions, "connectionMethod")} + {renderOption({ optionKey: "connectionMethod" })} {renderedLink}
@@ -143,26 +134,18 @@ const LayerEditor = () => {
{model.canMove(layer) && ( - - {getOptionComponent(model, layerOptions, "x")} - + {renderOption({ optionKey: "x" })} )} {model.canChangeSize(layer) && ( - - {getOptionComponent(model, layerOptions, "width")} - + {renderOption({ optionKey: "width" })} )} {model.canMove(layer) && ( - - {getOptionComponent(model, layerOptions, "y")} - + {renderOption({ optionKey: "y" })} )} {model.canChangeSize(layer) && ( - - {getOptionComponent(model, layerOptions, "height")} - + {renderOption({ optionKey: "height" })} )}
@@ -200,12 +183,10 @@ const LayerEditor = () => {
- {getOptionComponent( - model, - layerOptions, - "rotation", - false, - )} + {renderOption({ + optionKey: "rotation", + label: false, + })}
@@ -235,4 +216,4 @@ const LayerEditor = () => { ) } -export default LayerEditor +export default React.memo(LayerEditor) diff --git a/src/features/layers/LayerList.js b/src/features/layers/LayerList.js index c03b8bb6..c9a5e3f1 100644 --- a/src/features/layers/LayerList.js +++ b/src/features/layers/LayerList.js @@ -166,4 +166,4 @@ const LayerList = () => { ) } -export default LayerList +export default React.memo(LayerList) diff --git a/src/features/layers/LayerManager.js b/src/features/layers/LayerManager.js index 23f826fe..cc219b78 100644 --- a/src/features/layers/LayerManager.js +++ b/src/features/layers/LayerManager.js @@ -64,40 +64,38 @@ const LayerManager = () => { > -
- {canRemove && } - {canRemove && ( - - )} - + {canRemove && } + {canRemove && ( - - -
+ )} + + + +
@@ -105,4 +103,4 @@ const LayerManager = () => { ) } -export default LayerManager +export default React.memo(LayerManager) From 67529bf3ed15be7fdfc48d711a5f95386f7ae952 Mon Sep 17 00:00:00 2001 From: Bob Carmichael Date: Sat, 16 Sep 2023 08:55:10 -0400 Subject: [PATCH 090/126] begin replacing machine UI with machines equivalent; some renaming/fixes --- src/common/geometry.js | 9 +- src/components/InputOption.js | 3 +- src/components/ModelOption.js | 8 ++ src/components/QuadrantButtonsOption.js | 84 +++++++++++ src/components/ToggleButtonOption.js | 1 + src/features/app/Sidebar.js | 8 +- src/features/app/rootSlice.js | 2 - src/features/effects/EffectLayer.js | 4 +- src/features/effects/EffectList.js | 2 +- src/features/effects/Mask.js | 28 ++-- src/features/effects/NewEffect.js | 8 +- .../effects/{factory.js => effectFactory.js} | 6 +- src/features/file/SandifyImporter.js | 2 +- src/features/import/ImportUploader.js | 4 +- src/features/layers/Layer.js | 4 +- src/features/layers/LayerEditor.js | 4 +- src/features/layers/LayerList.js | 2 +- src/features/layers/NewLayer.js | 8 +- src/features/layers/layersSlice.js | 36 +++-- src/features/machines/Machine.js | 57 ++++---- src/features/machines/MachineEditor.js | 92 ++++++++++++ src/features/machines/MachineList.js | 66 +++++++++ src/features/machines/MachineManager.js | 61 ++++++++ src/features/machines/MachineSettings.js | 55 ------- src/features/machines/PolarInvertedMachine.js | 10 +- src/features/machines/PolarMachine.js | 92 ++++++++---- src/features/machines/PolarSettings.js | 131 ----------------- src/features/machines/RectInvertedMachine.js | 4 - src/features/machines/RectMachine.js | 60 ++++++-- src/features/machines/RectSettings.js | 123 ---------------- src/features/machines/machineFactory.js | 31 ++++ src/features/machines/machineSlice.js | 135 ------------------ src/features/machines/machineSlice.spec.js | 60 -------- src/features/machines/machinesSlice.js | 74 ++++++++-- src/features/machines/machinesSlice.spec.js | 58 +++++++- src/features/machines/util.js | 53 +++++++ src/features/preview/EffectPreview.js | 4 +- src/features/preview/PreviewManager.js | 8 +- src/features/preview/PreviewWindow.js | 35 ++--- src/features/preview/ShapePreview.js | 4 +- src/features/shapes/FancyText.js | 2 +- src/features/shapes/NoiseWave.js | 32 ++--- src/features/shapes/Wiper.js | 26 ++-- .../shapes/circle_packer/CirclePacker.js | 6 +- src/features/shapes/input_text/InputText.js | 2 +- .../shapes/{factory.js => shapeFactory.js} | 6 +- .../shapes/space_filler/SpaceFiller.js | 13 +- 47 files changed, 775 insertions(+), 748 deletions(-) create mode 100644 src/components/QuadrantButtonsOption.js rename src/features/effects/{factory.js => effectFactory.js} (81%) create mode 100644 src/features/machines/MachineEditor.js create mode 100644 src/features/machines/MachineList.js create mode 100644 src/features/machines/MachineManager.js delete mode 100644 src/features/machines/MachineSettings.js delete mode 100644 src/features/machines/PolarSettings.js delete mode 100644 src/features/machines/RectSettings.js create mode 100644 src/features/machines/machineFactory.js delete mode 100644 src/features/machines/machineSlice.js delete mode 100644 src/features/machines/machineSlice.spec.js create mode 100644 src/features/machines/util.js rename src/features/shapes/{factory.js => shapeFactory.js} (93%) diff --git a/src/common/geometry.js b/src/common/geometry.js index d10ca247..7b778848 100644 --- a/src/common/geometry.js +++ b/src/common/geometry.js @@ -450,7 +450,14 @@ export const cloneVertices = (vertices) => { // add attributes to a given vertex export const annotateVertex = (vertex, attrs) => { - Object.assign(vertex, attrs) + const filteredAttrs = Object.keys(attrs).reduce((memo, key) => { + if (attrs[key] !== undefined) { + memo[key] = attrs[key] + } + return memo + }, {}) + + Object.assign(vertex, filteredAttrs) return vertex } diff --git a/src/components/InputOption.js b/src/components/InputOption.js index 1890a94d..03ff5011 100644 --- a/src/components/InputOption.js +++ b/src/components/InputOption.js @@ -33,6 +33,7 @@ const InputOption = ({ typeof option.title === "function" ? option.title(model, data) : option.title + const inputWidth = optionType === "text" ? "auto" : "132px" if (!visible) { return null @@ -108,7 +109,7 @@ const InputOption = ({ sm={7} className="mb-1" > - {renderedInput} +
{renderedInput}
) diff --git a/src/components/ModelOption.js b/src/components/ModelOption.js index 014457fe..f0f74315 100644 --- a/src/components/ModelOption.js +++ b/src/components/ModelOption.js @@ -3,6 +3,7 @@ import InputOption from "@/components/InputOption" import DropdownOption from "@/components/DropdownOption" import CheckboxOption from "@/components/CheckboxOption" import ToggleButtonOption from "@/components/ToggleButtonOption" +import QuadrantButtonsOption from "@/components/QuadrantButtonsOption" const ModelOption = ({ model, @@ -43,6 +44,13 @@ const ModelOption = ({ {...props} /> ) + case "quadrantbuttons": + return ( + + ) default: return ( { + const option = props.options[props.optionKey] + const { data } = props + const value = data[props.optionKey] + + const handleChange = (choices) => { + let attrs = {} + attrs[props.optionKey] = choices[choices.length - 1] + props.onChange(attrs) + } + + return ( + + + {option.title} + + + +
+ + + upper left + + + upper right + + + lower left + + + lower right + + +
+ +
+ ) +} + +export default QuadrantButtonsOption diff --git a/src/components/ToggleButtonOption.js b/src/components/ToggleButtonOption.js index c5478a61..01598ab2 100644 --- a/src/components/ToggleButtonOption.js +++ b/src/components/ToggleButtonOption.js @@ -35,6 +35,7 @@ const ToggleButtonOption = (props) => { { - + { diff --git a/src/features/effects/NewEffect.js b/src/features/effects/NewEffect.js index eda65cc3..88a5cd90 100644 --- a/src/features/effects/NewEffect.js +++ b/src/features/effects/NewEffect.js @@ -14,8 +14,8 @@ import { import { getEffectSelectOptions, getDefaultEffect, - getEffectFromType, -} from "./factory" + getEffect, +} from "./effectFactory" import EffectLayer from "./EffectLayer" const defaultEffect = getDefaultEffect() @@ -37,7 +37,7 @@ const NewEffect = ({ toggleModal, showModal }) => { const selectOptions = getEffectSelectOptions() const [type, setType] = useState(defaultEffect.type) const [name, setName] = useState(defaultEffect.label) - const selectedEffect = getEffectFromType(type) + const selectedEffect = getEffect(type) const selectedOption = { value: selectedEffect.id, label: selectedEffect.label, @@ -52,7 +52,7 @@ const NewEffect = ({ toggleModal, showModal }) => { } const handleChangeNewType = (selected) => { - const effect = getEffectFromType(selected.value) + const effect = getEffect(selected.value) setType(selected.value) setName(effect.label.toLowerCase()) diff --git a/src/features/effects/factory.js b/src/features/effects/effectFactory.js similarity index 81% rename from src/features/effects/factory.js rename to src/features/effects/effectFactory.js index 2ec35a2f..1ed253e7 100644 --- a/src/features/effects/factory.js +++ b/src/features/effects/effectFactory.js @@ -18,7 +18,7 @@ export const effectFactory = { warp: Warp, } -export const getEffectFromType = (type, ...args) => { +export const getEffect = (type, ...args) => { return new effectFactory[type](args) } @@ -27,13 +27,13 @@ export const getDefaultEffectType = () => { } export const getDefaultEffect = () => { - return getEffectFromType(getDefaultEffectType()) + return getEffect(getDefaultEffectType()) } export const getEffectSelectOptions = () => { const types = Object.keys(effectFactory) return types.map((type) => { - return { value: type, label: getEffectFromType(type).label } + return { value: type, label: getEffect(type).label } }) } diff --git a/src/features/file/SandifyImporter.js b/src/features/file/SandifyImporter.js index 20c0c25e..aba1a615 100644 --- a/src/features/file/SandifyImporter.js +++ b/src/features/file/SandifyImporter.js @@ -3,7 +3,7 @@ import EffectLayer from "@/features/effects/EffectLayer" export default class SandifyImporter { import(stateString) { - let state = JSON.parse(stateString) + const state = JSON.parse(stateString) this.checkStructure(state) this.ensureIntegrity(state, "layers", Layer) diff --git a/src/features/import/ImportUploader.js b/src/features/import/ImportUploader.js index 5f0b7e6d..dc58604f 100644 --- a/src/features/import/ImportUploader.js +++ b/src/features/import/ImportUploader.js @@ -1,14 +1,14 @@ import React, { useEffect, useRef } from "react" import Form from "react-bootstrap/Form" import { useSelector, useDispatch } from "react-redux" -import { selectMachine } from "@/features/machines/machineSlice" +import { selectCurrentMachine } from "@/features/machines/machinesSlice" import ThetaRhoImporter from "@/features/import/ThetaRhoImporter" import GCodeImporter from "@/features/import/GCodeImporter" import { addLayer } from "@/features/layers/layersSlice" import Layer from "@/features/layers/Layer" const ImportUploader = ({ toggleModal, showModal }) => { - const machineState = useSelector(selectMachine) + const machineState = useSelector(selectCurrentMachine) const dispatch = useDispatch() const inputRef = useRef() diff --git a/src/features/layers/Layer.js b/src/features/layers/Layer.js index 57c9e47d..e6582591 100644 --- a/src/features/layers/Layer.js +++ b/src/features/layers/Layer.js @@ -1,4 +1,4 @@ -import { getShapeFromType } from "@/features/shapes/factory" +import { getShape } from "@/features/shapes/shapeFactory" import EffectLayer from "@/features/effects/EffectLayer" import { resizeVertices, centerOnOrigin } from "@/common/geometry" import { roundP } from "@/common/util" @@ -71,7 +71,7 @@ export const layerOptions = { export default class Layer { constructor(type) { - this.model = getShapeFromType(type) + this.model = getShape(type) } getInitialState(props) { diff --git a/src/features/layers/LayerEditor.js b/src/features/layers/LayerEditor.js index e96f4e64..247e5313 100644 --- a/src/features/layers/LayerEditor.js +++ b/src/features/layers/LayerEditor.js @@ -13,7 +13,7 @@ import { } from "react-icons/ai" import CommentsBox from "@/components/CommentsBox" import ModelOption from "@/components/ModelOption" -import { getShapeSelectOptions } from "@/features/shapes/factory" +import { getShapeSelectOptions } from "@/features/shapes/shapeFactory" import { updateLayer, changeModelType } from "./layersSlice" import Layer from "./Layer" import EffectManager from "@/features/effects/EffectManager" @@ -125,7 +125,7 @@ const LayerEditor = () => { {renderOption({ optionKey: "connectionMethod" })} {renderedLink}
-
+
{model.canTransform(layer) && ( Transform diff --git a/src/features/layers/LayerList.js b/src/features/layers/LayerList.js index c9a5e3f1..6d113344 100644 --- a/src/features/layers/LayerList.js +++ b/src/features/layers/LayerList.js @@ -57,7 +57,7 @@ const LayerRow = ({ >
diff --git a/src/features/layers/NewLayer.js b/src/features/layers/NewLayer.js index 559a1473..0e590d69 100644 --- a/src/features/layers/NewLayer.js +++ b/src/features/layers/NewLayer.js @@ -9,8 +9,8 @@ import Modal from "react-bootstrap/Modal" import { getShapeSelectOptions, getDefaultShape, - getShapeFromType, -} from "@/features/shapes/factory" + getShape, +} from "@/features/shapes/shapeFactory" import Layer from "./Layer" import { addLayer } from "./layersSlice" @@ -29,7 +29,7 @@ const NewLayer = ({ toggleModal, showModal }) => { const selectOptions = getShapeSelectOptions() const [type, setType] = useState(defaultShape.type) const [name, setName] = useState(defaultShape.label) - const selectedShape = getShapeFromType(type) + const selectedShape = getShape(type) const selectedOption = { value: selectedShape.id, label: selectedShape.label, @@ -40,7 +40,7 @@ const NewLayer = ({ toggleModal, showModal }) => { } const handleChangeNewType = (selected) => { - const shape = getShapeFromType(selected.value) + const shape = getShape(selected.value) setType(selected.value) setName(shape.label.toLowerCase()) diff --git a/src/features/layers/layersSlice.js b/src/features/layers/layersSlice.js index 712e7d12..5fd93bdd 100644 --- a/src/features/layers/layersSlice.js +++ b/src/features/layers/layersSlice.js @@ -14,10 +14,7 @@ import { } from "@/common/geometry" import { orderByKey } from "@/common/util" import { insertOne, prepareAfterAdd, deleteOne } from "@/common/slice" -import { - getDefaultShapeType, - getShapeFromType, -} from "@/features/shapes/factory" +import { getDefaultShapeType, getShape } from "@/features/shapes/shapeFactory" import Layer from "./Layer" import EffectLayer from "@/features/effects/EffectLayer" import { selectState } from "@/features/app/appSlice" @@ -30,10 +27,8 @@ import { selectSelectedEffectId, } from "@/features/effects/effectsSlice" import { selectFontsLoaded } from "@/features/fonts/fontsSlice" -import { - selectMachine, - getMachineInstance, -} from "@/features/machines/machineSlice" +import { selectCurrentMachine } from "@/features/machines/machinesSlice" +import { getMachine } from "@/features/machines/machineFactory" import { selectPreviewState } from "@/features/preview/previewSlice" import { log } from "@/common/debugging" @@ -85,7 +80,7 @@ const layersSlice = createSlice({ updateLayer: (state, action) => { const changes = action.payload const layer = state.entities[changes.id] - const shape = getShapeFromType(layer.type) + const shape = getShape(layer.type) shape.handleUpdate(layer, changes) adapter.updateOne(state, { id: changes.id, changes }) @@ -205,13 +200,13 @@ export const selectLayerById = createCachedSelector( // transformed vertices are not redrawn when machine settings change export const selectLayerMachine = createCachedSelector( selectLayerById, - selectMachine, + selectCurrentMachine, (layer, machine) => { if (!layer) { return null } // zombie child - const shape = getShapeFromType(layer.type) + const shape = getShape(layer.type) return shape.usesMachine ? machine : null }, )((state, id) => id) @@ -224,7 +219,7 @@ const selectLayerFontsLoaded = createCachedSelector( return false } - const shape = getShapeFromType(layer.type) + const shape = getShape(layer.type) return shape.usesFonts ? fontsLoaded : false }, )((state, id) => id) @@ -343,7 +338,7 @@ const selectMachineVertices = createCachedSelector( selectLayerVertices, selectLayerIndex, selectNumVisibleLayers, - selectMachine, + selectCurrentMachine, (id, vertices, layerIndex, numLayers, machine) => { log("selectMachineVertices", id) if (vertices.length > 0) { @@ -351,8 +346,9 @@ const selectMachineVertices = createCachedSelector( start: layerIndex === 0, end: layerIndex === numLayers - 1, } - const machineInstance = getMachineInstance(vertices, machine, layerInfo) - return machineInstance.polish().vertices + const machineModel = getMachine(machine) + + return machineModel.polish(vertices, layerInfo) } else { return [] } @@ -552,10 +548,10 @@ export const selectConnectingVertices = createCachedSelector( const end = endVertices[0] if (startLayer.connectionMethod === "along perimeter") { - const machineInstance = getMachineInstance([], state.machine) - const startPerimeter = machineInstance.nearestPerimeterVertex(start) - const endPerimeter = machineInstance.nearestPerimeterVertex(end) - const perimeterConnection = machineInstance.tracePerimeter( + const machineModel = getMachine(state.machine) + const startPerimeter = machineModel.nearestPerimeterVertex(start) + const endPerimeter = machineModel.nearestPerimeterVertex(end) + const perimeterConnection = machineModel.tracePerimeter( startPerimeter, endPerimeter, ) @@ -614,7 +610,7 @@ export const selectVerticesStats = createSelector( export const selectLayerPreviewBounds = createCachedSelector( selectLayerById, selectMachineVertices, - selectMachine, + selectCurrentMachine, (layer, machineVertices, machine) => { if (!layer) { // zombie child diff --git a/src/features/machines/Machine.js b/src/features/machines/Machine.js index f4208982..be02c6f8 100644 --- a/src/features/machines/Machine.js +++ b/src/features/machines/Machine.js @@ -1,48 +1,49 @@ import { vertexRoundP, annotateVertices } from "@/common/geometry" export const machineOptions = { - minX: { - title: "Min X (mm)", - }, - maxX: { - title: "Max X (mm)", - }, - minY: { - title: "Min Y (mm)", - }, - maxY: { - title: "Max Y (mm)", - }, - origin: { - title: "Force origin", - }, - maxRadius: { - title: "Max radius (mm)", + name: { + title: "Name", + type: "text", }, minimizeMoves: { - title: "Try to minimize perimeter moves", + title: "Minimize perimeter moves", type: "checkbox", }, - polarEndPoint: { - title: "End point", - }, - polarStartPoint: { - title: "Start point", - }, } // inherit all machine classes from this base class export default class Machine { + constructor(state) { + this.state = Object.keys(state).length < 2 ? this.getInitialState() : state + this.label = "Machine" + } + + // override as needed; redux state of a newly created instance + getInitialState() { + return { + name: "default machine", + minimizeMoves: false, + } + } + + // override as needed + getOptions() { + return machineOptions + } + // given a set of vertices, ensure they adhere to the limits defined by the machine - polish() { - this.enforceLimits().cleanVertices().limitPrecision().optimizePerimeter() + polish(vertices, layerInfo) { + this.vertices = vertices + this.layerInfo = layerInfo + this.enforceLimits().cleanVertices().limitPrecision().optimizePerimeter() if (this.layerInfo.border) this.outlinePerimeter() if (this.layerInfo.start) this.addStartPoint() if (this.layerInfo.end) this.addEndPoint() // second call to limit precision for final cleanup - return this.limitPrecision() + this.limitPrecision() + return this.vertices } // clean the list of vertices and remove (nearly) duplicate points @@ -155,7 +156,7 @@ export default class Machine { optimizePerimeter() { let segments = this.stripExtraPerimeterVertices() - if (this.settings.minimizeMoves) { + if (this.state.minimizeMoves) { segments = this.minimizePerimeterMoves(segments) } diff --git a/src/features/machines/MachineEditor.js b/src/features/machines/MachineEditor.js new file mode 100644 index 00000000..b9759397 --- /dev/null +++ b/src/features/machines/MachineEditor.js @@ -0,0 +1,92 @@ +import React from "react" +import { useDispatch, useSelector } from "react-redux" +import Col from "react-bootstrap/Col" +import Row from "react-bootstrap/Row" +import Select from "react-select" +import ModelOption from "@/components/ModelOption" +import { + updateMachine, + selectCurrentMachine, + changeMachineType, +} from "./machinesSlice" +import { getMachine, getMachineSelectOptions } from "./machineFactory" + +const MachineEditor = () => { + const dispatch = useDispatch() + const machine = useSelector(selectCurrentMachine) + const type = machine?.type || "rectangular" // guard zombie child + const instance = getMachine(type) + const machineOptions = instance.getOptions() + const selectOptions = getMachineSelectOptions() + const selectedOption = { + value: instance.type, + label: instance.label, + } + + const handleChange = (attrs) => { + attrs.id = machine.id + dispatch(updateMachine(attrs)) + } + + const handleChangeType = (selected) => { + dispatch(changeMachineType({ id: machine.id, type: selected.value })) + } + + const renderedMachineSelection = ( + + + Type + + + + + + + + Name + + + + + + + + + + + + + ) +} + +export default NewMachine diff --git a/src/features/machines/machineFactory.js b/src/features/machines/machineFactory.js index 452d1148..06a65283 100644 --- a/src/features/machines/machineFactory.js +++ b/src/features/machines/machineFactory.js @@ -22,6 +22,10 @@ export const getDefaultMachineType = () => { return localStorage.getItem("defaultMachine") || "rectangular" } +export const getDefaultMachine = () => { + return getMachine(getDefaultMachineType()) +} + export const getMachineSelectOptions = () => { const types = Object.keys(machineFactory) diff --git a/src/features/machines/machinesSlice.js b/src/features/machines/machinesSlice.js index d2997e0c..345fc9ea 100644 --- a/src/features/machines/machinesSlice.js +++ b/src/features/machines/machinesSlice.js @@ -1,12 +1,7 @@ import { createSlice, createEntityAdapter } from "@reduxjs/toolkit" import { createSelector } from "reselect" import { v4 as uuidv4 } from "uuid" -import { - insertOne, - prepareAfterAdd, - deleteOne, - updateOne, -} from "@/common/slice" +import { prepareAfterAdd, deleteOne, updateOne } from "@/common/slice" import { selectState } from "@/features/app/appSlice" import { getMachine, getDefaultMachineType } from "./machineFactory" @@ -35,14 +30,26 @@ export const machinesSlice = createSlice({ reducers: { addMachine: { reducer(state, action) { - insertOne(state, action) + adapter.addOne(state, action) + state.current = state.ids[state.ids.length - 1] }, prepare(machine) { return prepareAfterAdd(machine) }, }, deleteMachine: (state, action) => { + const id = action.payload + const ids = state.ids + const deleteIdx = ids.findIndex((_id) => _id === id) + const currentMachineId = state.current + deleteOne(adapter, state, action) + + if (id === currentMachineId) { + const newIds = ids.filter((i) => i != id) + const idx = deleteIdx === ids.length - 1 ? deleteIdx - 1 : deleteIdx + state.current = newIds[idx] + } }, updateMachine: (state, action) => { updateOne(adapter, state, action) @@ -68,18 +75,6 @@ export const machinesSlice = createSlice({ newMachineState.id = id adapter.setOne(state, newMachineState) }, - /* restoreDefaults: (state, action) => { - const id = action.payload - const { type, name, layerId } = state.entities[id] - const layer = new EffectLayer(type) - - adapter.setOne(state, { - id, - name, - layerId, - ...layer.getInitialState(), - }) - }, */ }, }) @@ -87,9 +82,9 @@ export default machinesSlice.reducer export const { actions: machinesActions } = machinesSlice export const { addMachine, - deleteMachine, updateMachine, setCurrentMachine, + deleteMachine, changeMachineType, } = machinesSlice.actions @@ -99,6 +94,7 @@ export const { export const { selectAll: selectAllMachines, + selectIds: selectMachineIds, selectTotal: selectNumMachines, selectEntities: selectMachineEntities, } = adapter.getSelectors((state) => state.machines) From 754f3fc27d5ada83bc7abe03d46445827ca204a1 Mon Sep 17 00:00:00 2001 From: Bob Carmichael Date: Sat, 16 Sep 2023 14:25:53 -0400 Subject: [PATCH 092/126] import machine along with layers and effects; fixes --- src/components/CommentsBox.js | 22 ------- src/components/InputOption.js | 3 + src/features/app/Header.js | 7 +-- src/features/export/CommentExporter.js | 70 --------------------- src/features/export/ExportDownloader.js | 54 ++++++++-------- src/features/export/Exporter.js | 1 - src/features/export/exporterSlice.js | 21 +------ src/features/file/SandifyExporter.js | 3 + src/features/file/SandifyUploader.js | 2 + src/features/layers/LayerEditor.js | 1 - src/features/machines/Machine.js | 2 + src/features/machines/machinesSlice.js | 29 +++++++++ src/features/machines/machinesSlice.spec.js | 60 ++++++++++++++++++ src/features/shapes/FileImport.js | 18 ++---- src/features/shapes/Rose.js | 1 - src/features/shapes/Shape.js | 2 +- vite.config.js | 4 +- 17 files changed, 138 insertions(+), 162 deletions(-) delete mode 100644 src/components/CommentsBox.js delete mode 100644 src/features/export/CommentExporter.js diff --git a/src/components/CommentsBox.js b/src/components/CommentsBox.js deleted file mode 100644 index 2ac7a802..00000000 --- a/src/components/CommentsBox.js +++ /dev/null @@ -1,22 +0,0 @@ -import React from "react" - -const CommentsBox = ({ options, optionKey, data, comments }) => { - const option = options[optionKey] - const renderedComments = data.comments.map((comment, index) => { - return ( - - {comment} -
-
- ) - }) - - return ( -
-
{option.title}
-
{renderedComments}
-
- ) -} - -export default CommentsBox diff --git a/src/components/InputOption.js b/src/components/InputOption.js index 03ff5011..edc602e5 100644 --- a/src/components/InputOption.js +++ b/src/components/InputOption.js @@ -29,6 +29,8 @@ const InputOption = ({ typeof option.max === "function" ? option.max(data) : parseFloat(option.max) const visible = option.isVisible === undefined ? true : option.isVisible(model, data) + const enabled = + option.isEnabled === undefined ? true : option.isEnabled(model, data) const title = typeof option.title === "function" ? option.title(model, data) @@ -75,6 +77,7 @@ const InputOption = ({ const renderedInput = ( { > New Open... - - Import layer... - - Save as... + + Import... + Export as... diff --git a/src/features/export/CommentExporter.js b/src/features/export/CommentExporter.js deleted file mode 100644 index 6144eed7..00000000 --- a/src/features/export/CommentExporter.js +++ /dev/null @@ -1,70 +0,0 @@ -import { getShapeFromType } from "@/features/shapes/factory" -// import Machine from '@/features/shapes/Machine' -import Exporter from "./Exporter" - -export default class CommentExporter extends Exporter { - constructor(props) { - super(props) - this.indentLevel = 0 - this.startComments() - this.commentChar = "" - } - - export() { - const state = this.props - // TODO: fix - // const machine = new Machine() - // let instance = state.machine - - this.line("Created by Sandify") - this.line("https://sandify.org") - this.keyValueLine("Version", state.app.sandifyVersion) - this.line() - - this.keyValueLine( - "Machine type", - state.machine.rectangular ? "Rectangular" : "Polar", - ) - this.indent() - // TODO: fix - // this.optionLines(machine, instance, ['minX', 'maxX', 'minY', 'maxY'], state.machine.rectangular) - // this.optionLines(machine, instance, ['maxRadius', 'polarStartPoint', 'polarEndPoint'], !state.machine.rectangular) - this.dedent() - - this.keyValueLine("Content type", state.app.input) - - const layers = state.layers - switch (state.app.input) { - case "shape": // shapes - layers.forEach((layer) => { - const shape = getShapeFromType(layer.type) - const options = shape.getOptions() - - this.line("Layer:") - this.indent() - this.keyValueLine("Shape", shape.name) - this.optionLines(shape, layer, Object.keys(options)) - this.keyValueLine("Visible", layer.visible) - // TODO: fix - // this.optionLines(transform, layer, ['width', 'height', 'x', 'y', 'rotation', 'reverse']) - - if (!layer.effect) { - this.line("Fine tuning:") - this.indent() - // TODO: fix - // this.optionLines(transform, layer, ['connectionMethod']) - this.dedent() - } - this.dedent() - }) - break - - default: - break - } - - this.dedent() - this.keyValueLine("Reverse export path", state.exporter.reverse) - return this.lines - } -} diff --git a/src/features/export/ExportDownloader.js b/src/features/export/ExportDownloader.js index d8d5ffc5..f1bd0e74 100644 --- a/src/features/export/ExportDownloader.js +++ b/src/features/export/ExportDownloader.js @@ -11,10 +11,9 @@ import CheckboxOption from "@/components/CheckboxOption" import { selectConnectedVertices } from "@/features/layers/layersSlice" import { selectExporterState, - selectComments, updateExporter, } from "@/features/export/exporterSlice" -import { selectMachine } from "@/features/machines/machineSlice" +import { selectCurrentMachine } from "@/features/machines/machinesSlice" import GCodeExporter from "./GCodeExporter" import ScaraGCodeExporter from "./ScaraGCodeExporter" import SvgExporter from "./SvgExporter" @@ -30,31 +29,35 @@ const exporters = { const ExportDownloader = ({ showModal, toggleModal }) => { const dispatch = useDispatch() - const machine = useSelector(selectMachine) + const machine = useSelector(selectCurrentMachine) const exporterState = useSelector(selectExporterState) const { fileType, fileName } = exporterState const props = { ...exporterState, - offsetX: machine.rectangular - ? (machine.minX + machine.maxX) / 2.0 - : machine.maxRadius, - offsetY: machine.rectangular - ? (machine.minY + machine.maxY) / 2.0 - : machine.maxRadius, - width: machine.rectangular - ? machine.maxX - machine.minX - : 2.0 * machine.maxRadius, - height: machine.rectangular - ? machine.maxY - machine.minY - : 2.0 * machine.maxRadius, - maxRadius: machine.rectangular - ? Math.sqrt( - Math.pow(machine.maxX - machine.minX, 2.0) + - Math.pow(machine.maxY - machine.minY, 2.0), - ) - : machine.maxRadius, + offsetX: + machine.type === "rectangular" + ? (machine.minX + machine.maxX) / 2.0 + : machine.maxRadius, + offsetY: + machine.type === "rectangular" + ? (machine.minY + machine.maxY) / 2.0 + : machine.maxRadius, + width: + machine.type === "rectangular" + ? machine.maxX - machine.minX + : 2.0 * machine.maxRadius, + height: + machine.type === "rectangular" + ? machine.maxY - machine.minY + : 2.0 * machine.maxRadius, + maxRadius: + machine.type === "rectangular" + ? Math.sqrt( + Math.pow(machine.maxX - machine.minX, 2.0) + + Math.pow(machine.maxY - machine.minY, 2.0), + ) + : machine.maxRadius, vertices: useSelector(selectConnectedVertices), - comments: useSelector(selectComments), } const exporter = new exporters[fileType](props) @@ -168,16 +171,15 @@ const ExportDownloader = ({ showModal, toggleModal }) => { - See{" "} + See the{" "} - {" "} - the wiki{" "} + wiki{" "} {" "} - for details on available program export variables. + for details on program export variables. diff --git a/src/features/export/Exporter.js b/src/features/export/Exporter.js index df5a032a..cc133f3a 100644 --- a/src/features/export/Exporter.js +++ b/src/features/export/Exporter.js @@ -67,7 +67,6 @@ export default class Exporter { this.computeOutputVertices(vertices) this.header() this.startComments() - this.props.comments.forEach((comment) => this.line(comment)) this.line() this.keyValueLine("File name", "'" + this.props.fileName + "'") this.keyValueLine("File type", this.props.fileType) diff --git a/src/features/export/exporterSlice.js b/src/features/export/exporterSlice.js index 2874e59b..affdebb8 100644 --- a/src/features/export/exporterSlice.js +++ b/src/features/export/exporterSlice.js @@ -1,10 +1,6 @@ import { createSlice } from "@reduxjs/toolkit" import { createSelector } from "reselect" -import { selectAllLayers } from "@/features/layers/layersSlice" -import CommentExporter from "./CommentExporter" -import { log } from "@/common/debugging" -import { selectState, selectAppState } from "@/features/app/appSlice" -import { selectMachine } from "@/features/machine/machineSlice" +import { selectState } from "@/features/app/appSlice" import { GCODE } from "./Exporter" // ------------------------------ @@ -40,18 +36,3 @@ export const selectExporterState = createSelector( selectState, (state) => state.exporter, ) - -export const selectComments = createSelector( - [selectAppState, selectAllLayers, selectExporterState, selectMachine], - (app, layers, exporter, machine) => { - log("selectComments") - const state = { - app, - layers, - exporter, - machine, - } - - return new CommentExporter(state).export() - }, -) diff --git a/src/features/file/SandifyExporter.js b/src/features/file/SandifyExporter.js index 50db6855..f7511e02 100644 --- a/src/features/file/SandifyExporter.js +++ b/src/features/file/SandifyExporter.js @@ -1,14 +1,17 @@ export default class SandifyExporter { export(state) { + const currentMachine = state.machines.entities[state.machines.current] const json = { effects: { ...state.effects }, layers: { ...state.layers }, + machine: { ...currentMachine }, } delete json.layers.selected delete json.layers.current delete json.effects.selected delete json.effects.current + delete json.machine.id return JSON.stringify(json, null, "\t") } diff --git a/src/features/file/SandifyUploader.js b/src/features/file/SandifyUploader.js index 15e1ef39..d673b494 100644 --- a/src/features/file/SandifyUploader.js +++ b/src/features/file/SandifyUploader.js @@ -2,6 +2,7 @@ import React, { useEffect, useRef } from "react" import Form from "react-bootstrap/Form" import { useDispatch } from "react-redux" import { toast } from "react-toastify" +import { upsertImportedMachine } from "@/features/machines/machinesSlice" import SandifyImporter from "./SandifyImporter" const SandifyUploader = ({ toggleModal, showModal }) => { @@ -27,6 +28,7 @@ const SandifyUploader = ({ toggleModal, showModal }) => { const importer = new SandifyImporter() const newState = importer.import(text) + dispatch(upsertImportedMachine(newState.machine)) dispatch({ type: "LOAD_PATTERN", payload: newState }) } catch (e) { toast.error(e.message) diff --git a/src/features/layers/LayerEditor.js b/src/features/layers/LayerEditor.js index 247e5313..01558778 100644 --- a/src/features/layers/LayerEditor.js +++ b/src/features/layers/LayerEditor.js @@ -11,7 +11,6 @@ import { AiTwotoneLock, AiTwotoneUnlock, } from "react-icons/ai" -import CommentsBox from "@/components/CommentsBox" import ModelOption from "@/components/ModelOption" import { getShapeSelectOptions } from "@/features/shapes/shapeFactory" import { updateLayer, changeModelType } from "./layersSlice" diff --git a/src/features/machines/Machine.js b/src/features/machines/Machine.js index b38b7ae6..732a9db3 100644 --- a/src/features/machines/Machine.js +++ b/src/features/machines/Machine.js @@ -4,6 +4,7 @@ export const machineOptions = { name: { title: "Name", type: "text", + isEnabled: (model, state) => !state.imported, }, minimizeMoves: { title: "Minimize perimeter moves", @@ -24,6 +25,7 @@ export default class Machine { return { name: "default machine", minimizeMoves: false, + imported: false, } } diff --git a/src/features/machines/machinesSlice.js b/src/features/machines/machinesSlice.js index 345fc9ea..b7e3edb9 100644 --- a/src/features/machines/machinesSlice.js +++ b/src/features/machines/machinesSlice.js @@ -54,6 +54,34 @@ export const machinesSlice = createSlice({ updateMachine: (state, action) => { updateOne(adapter, state, action) }, + upsertImportedMachine: { + reducer(state, action) { + const machines = Object.values(state.entities) + const importedIdx = machines.findIndex( + (machine) => machine.name == "[imported]", + ) + + if (importedIdx !== -1) { + const existingId = machines[importedIdx].id + + action.payload.id = existingId + action.payload.name = "[imported]" + updateOne(adapter, state, action) + state.current = existingId + } else { + action.payload = { + ...action.payload, + name: "[imported]", + imported: true, + } + adapter.addOne(state, action) + state.current = state.ids[state.ids.length - 1] + } + }, + prepare(machine) { + return prepareAfterAdd(machine) + }, + }, setCurrentMachine: (state, action) => { state.current = action.payload }, @@ -83,6 +111,7 @@ export const { actions: machinesActions } = machinesSlice export const { addMachine, updateMachine, + upsertImportedMachine, setCurrentMachine, deleteMachine, changeMachineType, diff --git a/src/features/machines/machinesSlice.spec.js b/src/features/machines/machinesSlice.spec.js index 778170cb..121f72a2 100644 --- a/src/features/machines/machinesSlice.spec.js +++ b/src/features/machines/machinesSlice.spec.js @@ -3,6 +3,7 @@ import machinesReducer, { addMachine, deleteMachine, updateMachine, + upsertImportedMachine, setCurrentMachine, changeMachineType, } from "./machinesSlice" @@ -103,6 +104,65 @@ describe("machines reducer", () => { }) }) + describe("upsertImportedMachine", () => { + it("should create a machine if one doesn't already exist", () => { + expect( + machinesReducer( + { + ids: [], + entities: {}, + }, + upsertImportedMachine({ + name: "foo", + }), + ), + ).toEqual({ + ids: ["1"], + entities: { + 1: { + id: "1", + name: "foo", + imported: true, + }, + }, + current: "1", + }) + }) + + it("should update the existing machine if it exists", () => { + expect( + machinesReducer( + { + ids: ["1"], + entities: { + 1: { + id: "1", + name: "[imported]", + imported: true, + minX: 500, + }, + }, + current: "1", + }, + upsertImportedMachine({ + minX: 100, + }), + ), + ).toEqual({ + ids: ["1"], + entities: { + 1: { + id: "1", + name: "[imported]", + imported: true, + minX: 100, + }, + }, + current: "1", + }) + }) + }) + describe("changeMachineType", () => { it("should add default values", () => { expect( diff --git a/src/features/shapes/FileImport.js b/src/features/shapes/FileImport.js index d57e7645..b3b4707d 100644 --- a/src/features/shapes/FileImport.js +++ b/src/features/shapes/FileImport.js @@ -1,5 +1,5 @@ import { resizeVertices, dimensions, cloneVertices } from "@/common/geometry" -import { getMachineInstance } from "@/features/machines/machineSlice" +import { getMachine } from "@/features/machines/machineFactory" import Shape from "./Shape" const options = { @@ -8,10 +8,6 @@ const options = { type: "inputText", plainText: "true", }, - comments: { - title: "Comments", - type: "comments", - }, } export default class FileImport extends Shape { @@ -27,29 +23,23 @@ export default class FileImport extends Shape { ...super.getInitialState(), ...{ vertices: [], - comments: [], + maintainAspectRatio: true, }, ...(props === undefined ? {} : { fileName: props.fileName, vertices: props.vertices, - comments: props.comments, }), } } initialDimensions(props) { - const { machine } = props const vertices = cloneVertices(props.vertices) - const machineInstance = getMachineInstance(vertices, machine) + const machine = getMachine(props.machine) // default to 80% of machine size - resizeVertices( - vertices, - machineInstance.width * 0.8, - machineInstance.height * 0.8, - ) + resizeVertices(vertices, machine.width * 0.8, machine.height * 0.8) return dimensions(vertices) } diff --git a/src/features/shapes/Rose.js b/src/features/shapes/Rose.js index bbf18764..ff9c18dd 100644 --- a/src/features/shapes/Rose.js +++ b/src/features/shapes/Rose.js @@ -20,7 +20,6 @@ export default class Rose extends Shape { this.label = "Rose" this.link = "http://mathworld.wolfram.com/Rose.html" this.linkText = "Rose at Wolfram Mathworld" - this.startingAspectRatioLocked = false // force a square shape } getInitialState() { diff --git a/src/features/shapes/Shape.js b/src/features/shapes/Shape.js index 580b78cc..ece08c29 100644 --- a/src/features/shapes/Shape.js +++ b/src/features/shapes/Shape.js @@ -21,7 +21,7 @@ export default class Shape extends Model { autosize: true, startingWidth: 100, startingHeight: 100, - aspectRatioLocked: true, + maintainAspectRatio: false, }) } diff --git a/vite.config.js b/vite.config.js index 246a6b4c..65fad87b 100644 --- a/vite.config.js +++ b/vite.config.js @@ -11,7 +11,7 @@ export default defineConfig(() => ({ }, plugins: [ react(), - nodePolyfills() + nodePolyfills(), ], resolve: { alias: { @@ -35,7 +35,7 @@ export default defineConfig(() => ({ optimizeDeps: { esbuildOptions: { plugins: [ - NodeGlobalsPolyfillPlugin({ buffer: false, process: false }), + NodeGlobalsPolyfillPlugin({ buffer: false, process: true }), { name: "load-js-files-as-jsx", setup(build) { From 15d6092822bf44e306a7be9e4c1911d40dc49d10 Mon Sep 17 00:00:00 2001 From: Bob Carmichael Date: Sat, 16 Sep 2023 16:29:54 -0400 Subject: [PATCH 093/126] show version on about page; update TODO --- TODO.md | 94 ++++++--------------- src/features/app/About.js | 14 ++- src/features/app/App.js | 2 +- src/features/app/appSlice.js | 1 + src/features/layers/Layer.js | 6 ++ src/features/machines/machinesSlice.spec.js | 5 +- 6 files changed, 49 insertions(+), 73 deletions(-) diff --git a/TODO.md b/TODO.md index ebfb1196..c9b7b4b4 100644 --- a/TODO.md +++ b/TODO.md @@ -1,71 +1,29 @@ -### TODO -- I don't really understand the best way to manage efficient re-renders and orphaned state, e.g., deletion of a layer or effect. Need to read up. - -- wiper dimensions are incorrect -- change polygon number of sides and height/width ratio stays same, so it's scrunched; I think this is similar to how fancy text gets squeezed -- restore defaults should not clobber effects; if it does, need to delete them explicitly -- when hiding a non-current row, should probably allow the toggle without setting the current row? Else deal with the weird issues when you do this. Happens for effects and layers. -- sensible effect defaults for every effect -- many effects are broken -- copy effect - -- for groups, consider a similar setup, but with +### TODO FOR RELEASE + +- hitfunc + - active effect is overly big; should be able to draw multiple bounded shapes vs a combined footprint + - something weird going on with inability to select layer if effect hit area overlaps? + - any selectable effect that hides the original shape should in theory be click-selectable +- cannot drag points (hit area too small) +- handle catastrophic state failure, and allow full reset +- get rectangular preview image from Jeff +- bump up version; maybe 1.0? + +- add any missing specs +- code cleanup +- review/test EVERYTHING + - all shapes/effects + - machines + - exports (faithful representation?) + - imports + - save/load/new + +### FUTURE CONSIDERATION + +- refactor slider so it's precise like fine tuning +- use react-router-dom for routes so browser back button works with tabs +- groups - selectLayersByGroupId - some kind of compound parent key "[a]-[b]" - big thunk which just changes layer dimensions - if it has effects, can render those via selector - -- clean up selector code -- thunk for current selection across effects and layers -- do an initial resize is needed when page loads -- handle text sizing so it can resize as you type -- what are we doing with tracks? - -- Layer knows model, is given vertices and effects -- PreviewLayer gets vertices, applies effects? selectors guarantee this only happens when layer or effects change. -- Downloader uses its own mechanism (would thunk work?) to loop through everything. - -### DONE -- DONE: thunk for delete layer and associated effects -- DONE: move model caching -- DONE: proper transformer sizing -- DONE: refactor model classes to reflect Jeff's thinking -- DONE: add selectLayerVertices that takes - - selectLayerById, selectEffectsByLayerId - changes to layer or any effects - - instantiates layer and effects and draws the whole thing -- Effects - - DONE: when there are no effects, add a button instead of the list - - -### MAYBE -- build a top menu; layout evolution; hamburger for smaller media -- upgrade to Bootstrap 5 and rebuild the UI - -### IDEAS -Model - - options (editable) - - attrs (readonly) - -Effect > Model - -Layer - - options (editable) - - reverse - - connectionMethod - - name - -Shape > Layer - - options (editable) - - x-offset - - y-offset - - width - - height - - - effects - - shape - - parent (if in group) - -Group > Layer - - children (layers) - - dynamic attributes that user can set which recalculate children - - width - - height +- when shift key is pressed, step should move in finer increments diff --git a/src/features/app/About.js b/src/features/app/About.js index 93386755..19882ece 100644 --- a/src/features/app/About.js +++ b/src/features/app/About.js @@ -1,13 +1,17 @@ import React from "react" +import { useSelector } from "react-redux" import Container from "react-bootstrap/Container" import Row from "react-bootstrap/Row" import Col from "react-bootstrap/Col" import HappyHolidays from "./happy-holidays.svg" import PerlinRings from "./perlin-rings.svg" import KochCubeFlowers from "./koch-cube-flowers.svg" +import { selectAppVersion } from "@/features/app/appSlice" import "./About.scss" const About = () => { + const version = useSelector(selectAppVersion) + return (