Skip to content

Commit

Permalink
editable shapes, example/usage cleanup
Browse files Browse the repository at this point in the history
  • Loading branch information
ryan-williams committed Oct 11, 2023
1 parent 86d6e9f commit bcbda8f
Show file tree
Hide file tree
Showing 10 changed files with 310 additions and 146 deletions.
10 changes: 5 additions & 5 deletions package-lock.json

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

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"email": "ryan@runsascoded.com"
},
"dependencies": {
"apvd": "https://gitpkg.now.sh/runsascoded/shapes?36ca2f8",
"apvd": "https://gitpkg.now.sh/runsascoded/shapes?caee520",
"bootstrap": "^5.3.1",
"buffer": "^6.0.3",
"lodash": "^4.17.21",
Expand Down
44 changes: 31 additions & 13 deletions pages/index.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,37 @@
padding-bottom: 0;
}
}
.clipboardSvg {
cursor: pointer;
margin: 2px;
}
.resetMetadata {
cursor: pointer;
}
.resetMetadataDisabled {
opacity: 0.5;
}
.playbackControlButtonContainer {
vertical-align: text-bottom;
.playbackControlButton {
padding: 0;
border: 0;
line-height: 1em;
background-color: transparent;
font-family: "Twemoji Mozilla",
"Apple Color Emoji",
"Segoe UI Emoji",
"Segoe UI Symbol",
"Noto Color Emoji",
"EmojiOne Color",
"Android Emoji",
sans-serif;
}
}
.settingsGearInline {
cursor: pointer;
margin: 0 0.2em;
}
.body {
width: 100%;
padding: 0 0.8em;
Expand Down Expand Up @@ -198,19 +229,6 @@

button {
font-size: 2em;
line-height: 1em;

padding: 0;
border: 0;
background-color: transparent;
font-family: "Twemoji Mozilla",
"Apple Color Emoji",
"Segoe UI Emoji",
"Segoe UI Symbol",
"Noto Color Emoji",
"EmojiOne Color",
"Android Emoji",
sans-serif;
}
}
}
Expand Down
233 changes: 154 additions & 79 deletions pages/index.tsx

Large diffs are not rendered by default.

23 changes: 23 additions & 0 deletions src/components/clipboard-svg.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import React, { SVGProps } from "react";

// https://www.svgrepo.com/svg/404561/clipboard-copy-duplicate-paste-2
export default function ClipboardSvg(props: Partial<SVGProps<SVGSVGElement>>) {
const defaults = {
fill: "#000000",
width: "20px",
height: "20px",
viewBox: "0 0 32 32",
xmlns: "http://www.w3.org/2000/svg",
}
return (
<svg
{...defaults}
{...props}
>
<title/>
<path
d="M27.2,8.22H23.78V5.42A3.42,3.42,0,0,0,20.36,2H5.42A3.42,3.42,0,0,0,2,5.42V20.36a3.43,3.43,0,0,0,3.42,3.42h2.8V27.2A2.81,2.81,0,0,0,11,30H27.2A2.81,2.81,0,0,0,30,27.2V11A2.81,2.81,0,0,0,27.2,8.22ZM5.42,21.91a1.55,1.55,0,0,1-1.55-1.55V5.42A1.54,1.54,0,0,1,5.42,3.87H20.36a1.55,1.55,0,0,1,1.55,1.55v2.8H11A2.81,2.81,0,0,0,8.22,11V21.91ZM28.13,27.2a.93.93,0,0,1-.93.93H11a.93.93,0,0,1-.93-.93V11a.93.93,0,0,1,.93-.93H27.2a.93.93,0,0,1,.93.93Z"
/>
</svg>
)
}
13 changes: 12 additions & 1 deletion src/components/tables/shapes.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,21 @@
max-width: 4em;
overflow: hidden;
}
.shapeName {
td.thetaVarCell {
min-width: 3em;
max-width: 3em;
overflow: hidden;
}
.editableShapeField {
max-width: 4em;
border: 0;
}
.shapeNameHeader {
user-select: none;
.toggleSetMetadataButton {
cursor: pointer;
}
}
.selectShapeType {
max-width: 5em;
}
Expand Down
123 changes: 78 additions & 45 deletions src/components/tables/shapes.tsx
Original file line number Diff line number Diff line change
@@ -1,37 +1,24 @@
import { getRadii, mapShape, S, SetMetadatum, Shape } from "../../lib/shape";
import React, { ReactNode, useState } from "react";
import React, { Dispatch, ReactNode, useCallback, useState } from "react";
import {Vars} from "../../lib/vars";
import css from "./shapes.module.scss"
import OverlayTrigger from "react-bootstrap/OverlayTrigger";
import Tooltip from "react-bootstrap/Tooltip";
import dynamic from "next/dynamic";
import { deg, round, sqrt } from "../../lib/math";
import { deg, PI, round, sqrt } from "../../lib/math";
import useSessionStorageState from "use-session-storage-state";
const StaticMathField = dynamic(() => import("react-mathquill").then(m => { m.addStyles(); return m.StaticMathField }), { ssr: false })

export type CopyCoordinatesType = "JS" | "Rust" | "JSON" | "URL"

export type Props = {
sets: S[]
showShapesMetadata: boolean
setShape: (idx: number, shape: Shape<number>) => void
updateSetMetadatum: (idx: number, newMetadatum: Partial<SetMetadatum>) => void
vars: Vars
precision?: number
}

export function VarCell({ skipped, className, children }: { skipped: boolean, className?: string, children: ReactNode }) {
className = (className || css.varCell) + ` ${skipped ? css.skipped : ''}`
return (
skipped
? <OverlayTrigger overlay={<Tooltip>Fixed/disabled</Tooltip>}>
<td className={className}>
<span>{children}</span>
</td>
</OverlayTrigger>
: <td className={className}>{children}</td>
)
}

export function EditableText({ className, defaultValue, onChange }: {
className?: string
defaultValue: string
Expand All @@ -43,8 +30,8 @@ export function EditableText({ className, defaultValue, onChange }: {
className={className || ''}
type={"text"}
value={value === null ? defaultValue : value}
onFocus={e => { setValue(defaultValue) }}
onBlur={e => { setValue(null) }}
onFocus={() => { setValue(defaultValue) }}
onBlur={() => { setValue(null) }}
onChange={e => {
const newValue = e.target.value
const rv = onChange(newValue)
Expand All @@ -56,16 +43,60 @@ export function EditableText({ className, defaultValue, onChange }: {
)
}

export type Var = {
skip: boolean
value: number
set: Dispatch<number>
}

export function VarCell({ skip, value, set, parse, render, className, }: Var & {
parse?: (value: string) => number | null
render?: (value: number) => string
className?: string
}) {
className = (className || css.varCell) + ` ${skip ? css.skipped : ''}`
render = render || (v => `${v}`)
const valueStr = render(value)
const cell = <td className={className}>
<EditableText
className={css.editableShapeField}
defaultValue={valueStr}
onChange={s => {
const newValue = parse ? parse(s) : parseFloat(s)
if (newValue === null || isNaN(newValue)) return
set(newValue)
}}
/>
</td>
return (
skip
? <OverlayTrigger overlay={<Tooltip>Fixed/disabled</Tooltip>}>{cell}</OverlayTrigger>
: cell
)
}

export function Math({ children }: { children: string }) {
return <StaticMathField className={css.math}>{children}</StaticMathField>
}
export function ShapesTable({ sets, showShapesMetadata, setShape, updateSetMetadatum, vars, precision = 4 }: Props) {

export function ShapesTable({ sets, setShape, updateSetMetadatum, vars, precision = 4 }: Props) {
const hasDoubleRadii = sets.some(({shape}) => shape.kind === "XYRR" || shape.kind === "XYRRT")
const hasXYRRT = sets.some(({shape}) => shape.kind === "XYRRT")
const [ editingName, setEditingName ] = useState<[ number, string ] | null>(null)
const [ showShapesMetadata, setShowShapesMetadata ] = useSessionStorageState("showShapesMetadata", { defaultValue: true })
const nameHeader = <th className={css.shapeNameHeader}>
Name{' '}
<OverlayTrigger overlay={<Tooltip>
Toggle between displaying shapes' coordinates vs. other metadata (abbreviated name, color, shape type)
</Tooltip>}>
<span
className={css.toggleSetMetadataButton}
onClick={() => { setShowShapesMetadata(!showShapesMetadata) }}
>🔄</span>
</OverlayTrigger>
</th>
const headerRow = showShapesMetadata
? <tr>
<th>Name</th>
{nameHeader}
<th className={css.abbrevCol}>
<OverlayTrigger overlay={<Tooltip>Abbreviated name: one character, used in "Targets" table</Tooltip>}>
<span>Key</span>
Expand All @@ -80,7 +111,7 @@ export function ShapesTable({ sets, showShapesMetadata, setShape, updateSetMetad
</OverlayTrigger>
</th>
</tr> : <tr>
<th>Name</th>
{nameHeader}
<th><Math>c_x</Math></th>
<th><Math>c_y</Math></th>
{
Expand All @@ -99,6 +130,7 @@ export function ShapesTable({ sets, showShapesMetadata, setShape, updateSetMetad
</th>
}
</tr>
const render = useCallback((v: number) => v.toPrecision(precision), [ precision ])
return (
<table className={css.shapesTable}>
<thead>
Expand All @@ -109,21 +141,32 @@ export function ShapesTable({ sets, showShapesMetadata, setShape, updateSetMetad
const skippedVars = vars.skipVars[idx] || []
const c = shape.c
const [ rx, ry ] = getRadii(shape)
const skipCx = skippedVars.includes("x")
const skipCy = skippedVars.includes("y")
const [ skipRx, skipRy ] = mapShape(
const cxVar = { skip: skippedVars.includes("x"), value: c.x, set: (x: number) => setShape(idx, { ...shape, c: { x, y: shape.c.y } }) }
const cyVar = { skip: skippedVars.includes("y"), value: c.y, set: (y: number) => setShape(idx, { ...shape, c: { y, x: shape.c.x } }) }
const [ rxVar, ryVar ]: [ Var, Var ] = mapShape(
shape,
c => {
const skip = skippedVars.includes("r")
return [skip, skip]
return [
{ skip, value: c.r, set: (r: number) => setShape(idx, { ...c, r }) },
{ skip, value: c.r, set: (r: number) => setShape(idx, { ...c, r }) },
]
},
e => [ skippedVars.includes("rx"), skippedVars.includes("ry") ],
e => [
{ skip: skippedVars.includes("rx"), value: e.r.x, set: (x: number) => setShape(idx, { ...e, r: { x, y: ry }}), },
{ skip: skippedVars.includes("ry"), value: e.r.y, set: (y: number) => setShape(idx, { ...e, r: { y, x: rx }}), },
],
)
const tVar = mapShape(
shape,
() => null,
() => null,
e => ({ skip: skippedVars.includes("t"), value: e.t, set: (t: number) => setShape(idx, { ...e, t }) }),
)
const editingNameStr = editingName && editingName[0] == idx ? editingName[1] : name
return <tr key={idx}>
<td style={{ textAlign: "right", }}>
<EditableText
className={css.shapeName}
className={css.editableShapeField}
defaultValue={name}
onChange={newName => {
console.log(`shape ${idx} name changed from ${name} to ${newName}`)
Expand Down Expand Up @@ -151,7 +194,7 @@ export function ShapesTable({ sets, showShapesMetadata, setShape, updateSetMetad
</td>
<td>
<EditableText
className={css.shapeName}
className={css.editableShapeField}
defaultValue={color}
onChange={newColor => {
if (!newColor) return
Expand All @@ -177,21 +220,11 @@ export function ShapesTable({ sets, showShapesMetadata, setShape, updateSetMetad
</select>
</td>
</> : <>
<VarCell skipped={skipCx}>{c.x.toPrecision(precision)}</VarCell>
<VarCell skipped={skipCy}>{c.y.toPrecision(precision)}</VarCell>
<VarCell skipped={skipRx}>{ rx.toPrecision(precision)}</VarCell>
{ hasDoubleRadii && <VarCell skipped={skipRy}>{ ry.toPrecision(precision)}</VarCell> }
{
hasXYRRT &&
<VarCell skipped={skippedVars.includes("t")}>{
mapShape(
shape,
() => "",
() => "",
e => `${round(deg(e.t))}°`
)
}</VarCell>
}
<VarCell {...cxVar} render={render} />
<VarCell {...cyVar} render={render} />
<VarCell {...rxVar} render={render} />
{ hasDoubleRadii && <VarCell {...ryVar} render={render} /> }
{ hasXYRRT && tVar && <VarCell className={css.thetaVarCell} {...tVar} render={v => `${round(deg(v))}`} parse={s => parseFloat(s) * PI / 180} /> }
</>
}
</tr>
Expand Down
3 changes: 2 additions & 1 deletion src/components/tables/targets.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {SparkLineCell, SparkLineProps, SparkNum} from "../spark-lines";
import {S} from "../../lib/shape";
import {abs} from "../../lib/math";
import {makeTargets, Target, Targets} from "../../lib/targets";
import { fmt } from "../../lib/utils";

export function getNegativeEntries(targets: Targets): Map<string, number> {
const entries: Target[] = []
Expand Down Expand Up @@ -112,7 +113,7 @@ export function TargetsTable(
const valueStr =
editingValue && editingValue[0] == key
? editingValue[1]
: value.toPrecision(3).replace(/\.?0+$/, '')
: fmt(value)
return <tr className={className} key={key}>
<td className={`${css.val} ${negativeKey}`}>{name}</td>
<td className={`${css.val} ${css.targetVal}`}>
Expand Down
3 changes: 2 additions & 1 deletion src/lib/shape.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export const setMetadataParam: Param<SetMetadata | null> = {
const s = metadata.map(({ name, abbrev, color }, idx) => {
const defaults = DefaultSetMetadata[idx]
// console.log("encoding, defaults:", defaults)
let s = encodeURIComponent(name == defaults.name ? '' : name)
let s = encodeURIComponent(name == defaults.name ? '' : name).replaceAll('%20', '+')
if (abbrev != name[0].toUpperCase()) {
s += `=${abbrev}`
}
Expand All @@ -70,6 +70,7 @@ export const setMetadataParam: Param<SetMetadata | null> = {
}
let { name, abbrev, color } = m.groups!
name = name || defaults.name
name = decodeURIComponent(name.replaceAll('+', '%20'))
abbrev = abbrev || name[0].toUpperCase()
color = color || defaults.color
return { name, abbrev, color }
Expand Down
2 changes: 2 additions & 0 deletions src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,3 +105,5 @@ export const spaces = (n: number) => {
}

export type State<T> = [T, Dispatch<SetStateAction<T>>]

export const fmt = (value: number, n: number = 3) => value.toPrecision(n).replace(/\.?0+$/, '')

0 comments on commit bcbda8f

Please sign in to comment.