Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/image import #291

Merged
merged 10 commits into from
Jul 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 0 additions & 30 deletions TODO.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
### TODO FOR RELEASE

- get rectangular preview image from Jeff
- bug: wiper, 90 degrees with noise effect; change wiper size from 4 to 40, hangs browser
- bug: edge-case optimization of pattern with inverted mask is adding a center point within the mask; workaround is to enable "minimize perimeter moves", but this isn't user-friendly obviously

Expand All @@ -15,32 +14,3 @@
- store pattern name and other desired attribution (?) in exported file and display it somewhere (either stats tab or in the preview window)
- show pattern start/end type (e.g., 1-0) if start/end if specified
- new fine tuning setting: when backtracking at end, optionally ignore border if enabled

## NEW IN 1.0.0

- User interface
- Layout improvements
- Can zoom in/out in the preview window.
- Can now click on a layer in the preview window to select the layer.
- Improved coloring and display layers and effects when they are selected and dragged to make editing more intuitive.
- Save and load patterns (new .sdf file format)
- Patterns can be saved and loaded from a file.
- Unsaved work is automatically preserved in the browser (can reload the page without losing work)
- Effects
- Effects are now displayed within their parent layer, and are no longer shown in the "layers" list.
- Track
- Improved settings make it easier to either "unwind" a single shape along a prescribed track, or position multiple shapes along a track.
- Fine tuning
- Percentages are based on overall length of the pattern, not number of vertices. Fractional values are supported.
- Fisheye
- Improved rendering when applied to shapes that have straight lines
- Transformer (new)
- New effect to allow resizing and rotation of a given layer
- Shapes
- Loop, scale, spin, and track transformers, as well as fine tuning settings, are now individual effects that can be added to a shape in any order, and are not added by default.
- New "maintain aspect ratio" setting, when enabled, forced a fixed aspect ratio.
- Machines
- Configure (add/remove/edit) multiple machines and switch between them.
- Machine settings from imported patterns are automatically saved as an "[Imported]" machine.
- Export
- Machine and shape settings are no longer added as comments to exported files in various formats.
20 changes: 10 additions & 10 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions src/common/geometry.js
Original file line number Diff line number Diff line change
Expand Up @@ -497,6 +497,10 @@ export const annotateVertex = (vertex, attrs) => {
return vertex
}

export const concatClonedVertices = (arr, vertices) => {
vertices.forEach((vertex) => arr.push(cloneVertex(vertex)))
}

// add attributes to a given array of vertices
export const annotateVertices = (vertices, attrs) => {
vertices.forEach((vertex) => {
Expand Down
1 change: 1 addition & 0 deletions src/common/localStorage.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,6 @@ export const saveState = (state, key = persistSaveKey) => {
localStorage.setItem(key, serializedState)
} catch (err) {
// ignore write errors
console.log(err)
}
}
4 changes: 0 additions & 4 deletions src/common/slice.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,6 @@ export const prepareAfterAdd = (entity) => {
return { payload: { ...entity, id }, meta: { id } }
}

export const deleteOne = (adapter, state, action) => {
adapter.removeOne(state, action.payload)
}

export const updateOne = (adapter, state, action) => {
const entity = action.payload

Expand Down
8 changes: 8 additions & 0 deletions src/components/ModelOption.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import DropdownOption from "@/components/DropdownOption"
import CheckboxOption from "@/components/CheckboxOption"
import ToggleButtonOption from "@/components/ToggleButtonOption"
import QuadrantButtonsOption from "@/components/QuadrantButtonsOption"
import SliderOption from "@/components/SliderOption"

const ModelOption = ({
model,
Expand Down Expand Up @@ -51,6 +52,13 @@ const ModelOption = ({
{...props}
/>
)
case "slider":
return (
<SliderOption
key={optionKey}
{...props}
/>
)
default:
return (
<InputOption
Expand Down
132 changes: 132 additions & 0 deletions src/components/SliderOption.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import Slider from "rc-slider"
import React, { useState, useEffect } from "react"
import Col from "react-bootstrap/Col"
import Row from "react-bootstrap/Row"
import Form from "react-bootstrap/Form"

const SliderOption = ({
data,
options,
optionKey,
onChange,
model,
label = true,
}) => {
const [value, setValue] = useState(data[optionKey])

useEffect(() => {
setValue(data[optionKey])
}, [data, optionKey])

const option = options[optionKey]
const step = option.step ? option.step : 1
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(model, data)
const enabled =
option.isEnabled === undefined ? true : option.isEnabled(model, data)
const title =
typeof option.title === "function"
? option.title(model, data)
: option.title
const inputWidth = "66px"

if (!visible) {
return null
}

const handleChange = (newValue) => {
setValue(newValue)
}

const handleInputChange = (e) => {
handleChange(e.target.value)
}

const handleChangeComplete = (newValue) => {
let attrs = {}
attrs[optionKey] = newValue

if (option.onChange !== undefined) {
attrs = option.onChange(model, attrs, data)
}
onChange(attrs)
}

let marks
if (isNaN(minimum) || isNaN(maximum)) {
marks = {}
} else if (option.range) {
marks = {
[minimum]: `${minimum}`,
[value[0]]: `${value[0]}`,
[value[1]]: `${value[1]}`,
[maximum]: `${maximum}`,
}
} else {
marks = {
[minimum]: `${minimum}`,
[value]: `${value}`,
[maximum]: `${maximum}`,
}
}

const renderedSlider = (
<div>
<Slider
disabled={!enabled}
name={`option-${optionKey}`}
min={!isNaN(minimum) ? minimum : ""}
max={!isNaN(maximum) ? maximum : ""}
marks={marks}
range={option.range}
allowCross={false}
value={value}
onChangeComplete={handleChangeComplete}
onChange={handleChange}
/>
</div>
)

const renderedInput = (
<Form.Control
disabled={!enabled}
as="input"
name={`option-${optionKey}`}
type="number"
step={step}
min={!isNaN(minimum) ? minimum : ""}
max={!isNaN(maximum) ? maximum : ""}
value={value}
autoComplete="off"
onChange={handleInputChange}
/>
)

return (
<Row className={"align-items-center mb-3" + (visible ? "" : " d-none")}>
<Col sm={5}>
{label && (
<Form.Label
htmlFor={`option-${optionKey}`}
className="mb-0"
>
{title}
</Form.Label>
)}
</Col>
<Col sm={7}>
<div className="d-flex align-items-center mb-2 mx-1">
<div className="me-3 flex-grow-1">{renderedSlider}</div>
{!option.range && (
<div style={{ width: inputWidth }}>{renderedInput}</div>
)}
</div>
</Col>
</Row>
)
}
export default SliderOption
28 changes: 19 additions & 9 deletions src/features/app/Header.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,22 @@ import Nav from "react-bootstrap/Nav"
import Navbar from "react-bootstrap/Navbar"
import NavDropdown from "react-bootstrap/NavDropdown"
import ExportDownloader from "@/features/export/ExportDownloader"
import ImportUploader from "@/features/import/ImportUploader"
import ImageUploader from "@/features/import/ImageUploader"
import LayerUploader from "@/features/import/LayerUploader"
import SandifyDownloader from "@/features/file/SandifyDownloader"
import SandifyUploader from "@/features/file/SandifyUploader"
import logo from "./logo.svg"
import "./Header.scss"

const Header = ({ eventKey, setEventKey }) => {
const [showExport, setShowExport] = useState(false)
const [showImport, setShowImport] = useState(0)
const [showImportLayer, setShowImportLayer] = useState(0)
const [showImportImage, setShowImportImage] = useState(0)
const [showSave, setShowSave] = useState(false)
const [showOpen, setShowOpen] = useState(0)
const toggleExport = () => setShowExport(!showExport)
const toggleImport = () => setShowImport(showImport + 1)
const toggleImportLayer = () => setShowImportLayer(showImportLayer + 1)
const toggleImportImage = () => setShowImportImage(showImportImage + 1)
const toggleSave = () => setShowSave(!showSave)
const toggleOpen = () => setShowOpen(showOpen + 1)
const dispatch = useDispatch()
Expand Down Expand Up @@ -51,11 +54,14 @@ const Header = ({ eventKey, setEventKey }) => {
<NavDropdown.Item onClick={toggleOpen}>Open...</NavDropdown.Item>
<NavDropdown.Item onClick={toggleSave}>Save as...</NavDropdown.Item>
<NavDropdown.Divider />
<NavDropdown.Item onClick={toggleImport}>
Import...
<NavDropdown.Item onClick={toggleImportImage}>
Import image...
</NavDropdown.Item>
<NavDropdown.Item onClick={toggleImportLayer}>
Import layer...
</NavDropdown.Item>
<NavDropdown.Item onClick={toggleExport}>
Export as...
Export pattern as...
</NavDropdown.Item>
</NavDropdown>
<Nav.Link
Expand All @@ -76,9 +82,13 @@ const Header = ({ eventKey, setEventKey }) => {
showModal={showExport}
toggleModal={toggleExport}
/>
<ImportUploader
showModal={showImport}
toggleModal={toggleImport}
<ImageUploader
showModal={showImportImage}
toggleModal={toggleImportImage}
/>
<LayerUploader
showModal={showImportLayer}
toggleModal={toggleImportLayer}
/>
<SandifyUploader
showModal={showOpen}
Expand Down
5 changes: 5 additions & 0 deletions src/features/app/Sidebar.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,18 @@ 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"
import { loadImage, selectAllImages } from "@/features/images/imagesSlice"

const Sidebar = () => {
const dispatch = useDispatch()
const layer = useSelector(selectSelectedLayer)
const images = useSelector(selectAllImages)

useEffect(() => {
Object.keys(supportedFonts).forEach((url) => dispatch(loadFont(url)))
images.forEach((image) =>
dispatch(loadImage({ imageId: image.id, imageSrc: image.src })),
)
}, [dispatch])

if (layer) {
Expand Down
6 changes: 5 additions & 1 deletion src/features/app/rootSlice.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import machinesReducer from "@/features/machines/machinesSlice"
import exporterReducer from "@/features/export/exporterSlice"
import previewReducer from "@/features/preview/previewSlice"
import fontsReducer from "@/features/fonts/fontsSlice"
import imagesReducer from "@/features/images/imagesSlice"
import layersReducer from "@/features/layers/layersSlice"
import effectsReducer from "@/features/effects/effectsSlice"
import fileReducer from "@/features/file/fileSlice"
Expand All @@ -13,6 +14,7 @@ const combinedReducer = combineReducers({
exporter: exporterReducer,
file: fileReducer,
fonts: fontsReducer,
images: imagesReducer,
layers: layersReducer,
machines: machinesReducer,
preview: previewReducer,
Expand All @@ -23,6 +25,7 @@ const resetPattern = (state, action) => {

newState.layers = undefined
newState.effects = undefined
newState.images = undefined
newState.preview.zoom = 1.0
newState.preview.sliderValue = 0.0

Expand All @@ -35,11 +38,12 @@ const resetAll = (state, action) => {
}

const loadPattern = (state, action) => {
const { effects, layers } = action.payload
const { layers, effects, images } = action.payload
const newState = JSON.parse(JSON.stringify(state)) // deep copy

newState.layers = layers
newState.effects = effects
newState.images = images

const id = newState.layers.ids[0]
newState.layers.current = id
Expand Down
1 change: 1 addition & 0 deletions src/features/app/store.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ if (reset === "all") {
// double JSON parsing ensures it's valid JSON before we try to import it
persistedState = importer.import(JSON.stringify(persistedState))
persistedState.fonts.loaded = false
persistedState.images.loaded = false
} catch (err) {
persistedState = undefined
}
Expand Down
Loading
Loading