Skip to content

Commit

Permalink
Merge pull request #4 from heypoom/refactor/settings-component
Browse files Browse the repository at this point in the history
Refactor blocks to use setting component for settings
  • Loading branch information
heypoom authored Dec 17, 2023
2 parents b063daf + ad5f581 commit 05551ac
Show file tree
Hide file tree
Showing 7 changed files with 141 additions and 211 deletions.
65 changes: 13 additions & 52 deletions canvas/src/blocks/clock.tsx
Original file line number Diff line number Diff line change
@@ -1,70 +1,31 @@
import { Checkbox, TextField } from "@radix-ui/themes"
import { memo, useState } from "react"
import { memo } from "react"

import { BaseBlock } from "@/blocks"
import { engine } from "@/engine"
import { Settings } from "@/blocks/components/Settings"
import { createSchema } from "@/blocks/types/schema"
import { BlockPropsOf } from "@/types/Node"

type ClockProps = BlockPropsOf<"Clock">

export const ClockBlock = memo((props: ClockProps) => {
const { id, freq = 0, ping = false } = props.data
const schema = createSchema({
type: "Clock",
fields: [
{ key: "freq", title: "Frequency", type: "number", min: 0, max: 255 },
{ key: "ping", type: "checkbox" },
],
})

const [rateInput, setRateInput] = useState(freq.toString())
const [cycleError, setCycleError] = useState(false)
export const ClockBlock = memo((props: ClockProps) => {
const { id } = props.data

const time = props.data?.time?.toString()?.padStart(3, "0") ?? "000"

const Settings = () => (
<div>
<div className="flex items-center gap-4 w-full">
<p className="text-1">Frequency</p>

<TextField.Input
className="max-w-[70px]"
size="1"
type="number"
min={0}
max={255}
value={rateInput}
onChange={(k) => {
const str = k.target.value
setRateInput(str)

const freq = parseInt(str)
const valid = !isNaN(freq) && freq >= 0 && freq <= 255
setCycleError(!valid)

if (valid) {
engine.setBlock(id, "Clock", { freq })
}
}}
{...(cycleError && { color: "tomato" })}
/>
</div>

<div className="flex items-center gap-4 w-full">
<p className="text-1">Ping</p>

<Checkbox
color="crimson"
checked={ping}
onCheckedChange={(ping) => {
if (ping === "indeterminate") return

engine.setBlock(id, "Clock", { ping })
}}
/>
</div>
</div>
)

return (
<BaseBlock
className="text-crimson-11 px-4 py-2 font-mono"
node={props}
sources={1}
settings={Settings}
settings={() => <Settings id={id} schema={schema} />}
>
t = {time}
</BaseBlock>
Expand Down
59 changes: 40 additions & 19 deletions canvas/src/blocks/components/Settings.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useStore } from "@nanostores/react"
import { TextField } from "@radix-ui/themes"
import { Checkbox, TextField } from "@radix-ui/themes"
import cn from "classnames"
import { ReactNode, useMemo } from "react"

import { BlockKeys, Field, SchemaOf } from "@/blocks/types/schema"
Expand All @@ -11,7 +12,9 @@ import { RadixSelect } from "@/ui"
type Props<T extends BlockTypes, F extends Field<T, BlockKeys<T>>> = {
id: number
schema: SchemaOf<T, F>
className?: string
onUpdate?: () => void
children?: ReactNode
}

export const Settings = <
Expand All @@ -20,7 +23,7 @@ export const Settings = <
>(
props: Props<T, F>,
) => {
const { id, schema } = props
const { id, schema, className } = props

const nodes = useStore($nodes)
const node = useMemo(() => nodes.find((n) => n.data.id === id), [id, nodes])
Expand All @@ -30,10 +33,12 @@ export const Settings = <
props.onUpdate?.()
}

const set = (key: string, value: unknown) => update({ [key]: value } as never)

if (!node) return null

return (
<div className="flex flex-col px-2 text-1 font-mono pb-2 gap-y-2">
<div className={cn("flex flex-col text-1 font-mono gap-y-2", className)}>
{schema.fields.map((field) => {
const { type } = field
const data = node.data as BlockFieldOf<T>
Expand All @@ -59,7 +64,7 @@ export const Settings = <
if (n < min) return
if (n > max) return

update({ [key]: n } as never)
set(key, n)
}}
/>
</FieldGroup>
Expand All @@ -69,27 +74,25 @@ export const Settings = <
if (type === "select") {
const options = field.options.map((f) => ({
value: f.key as string,
label: f.title,
label: f.title ?? (f.key as string),
}))

const vt = value as { type: string }

let val = ""
if (typeof value === "string") val = value
if ("type" in vt) val = vt.type

if (value) {
if (typeof value === "string") val = value
else if (vt && vt.type) val = vt.type
}

const onChange = (next: string) => {
if (typeof value === "string") {
update({ [key]: next } as never)
}
set(key, next)
} else if (vt && vt.type) {
const opt = field.options.find((o) => o.key === next)

if ("type" in vt) {
const option = field.options.find((o) => o.key === next)

// TODO: handle enums with additional fields
update({
[key]: { type: next, ...option?.defaults },
} as never)
set(key, { type: next, ...opt?.defaults })
}
}

Expand All @@ -100,6 +103,22 @@ export const Settings = <
)
}

if (type === "checkbox") {
return (
<FieldGroup key={key} name={name}>
<Checkbox
color="crimson"
checked={value as boolean}
onCheckedChange={(next) => {
if (next === "indeterminate") return

set(key, next)
}}
/>
</FieldGroup>
)
}

if (type === "text") {
return (
<FieldGroup key={key} name={name}>
Expand All @@ -110,13 +129,15 @@ export const Settings = <

return null
})}

{props.children}
</div>
)
}

const FieldGroup = (props: { name: string; children: ReactNode }) => (
<div className="grid grid-cols-2">
<div className="flex items-center">{props.name}</div>
export const FieldGroup = (props: { name: string; children: ReactNode }) => (
<div className="grid grid-cols-2 gap-x-3">
<div className="flex items-center capitalize">{props.name}</div>

{props.children}
</div>
Expand Down
103 changes: 38 additions & 65 deletions canvas/src/blocks/osc.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,29 @@
import { TextField } from "@radix-ui/themes"
import { Waveform } from "machine-wasm"
import { useState } from "react"

import { BaseBlock } from "@/blocks/components"
import { FieldGroup, Settings } from "@/blocks/components/Settings"
import { createSchema } from "@/blocks/types/schema"
import { engine } from "@/engine"
import { BlockPropsOf } from "@/types/Node"
import { RadixSelect } from "@/ui"

export type WaveformKey = Waveform["type"]

const waveforms: WaveformKey[] = [
"Sine",
"Cosine",
"Tangent",
"Square",
"Sawtooth",
"Triangle",
]

const waveformOptions = waveforms.map((value) => ({
value,
label: value,
}))
const schema = createSchema({
type: "Osc",
fields: [
{
key: "waveform",
type: "select",
options: [
{ key: "Sine" },
{ key: "Cosine" },
{ key: "Tangent" },
{ key: "Square", defaults: { duty_cycle: 50 } },
{ key: "Sawtooth" },
{ key: "Triangle" },
],
},
],
})

type OscProps = BlockPropsOf<"Osc">

Expand All @@ -32,26 +34,6 @@ export const OscBlock = (props: OscProps) => {
const [cycleText, setCycleText] = useState("")
const [cycleError, setCycleError] = useState(false)

const setWaveform = (waveform: Waveform) =>
engine.setBlock(id, "Osc", { waveform })

function handleWaveChange(key: string) {
let w = { type: key } as Waveform

// Update duty cycle
if (w.type === "Square") {
if (cycleText) {
const cycle = parseInt(cycleText)

if (!isNaN(cycle)) w = { ...w, duty_cycle: cycle }
} else {
setCycleText(w.duty_cycle.toString())
}
}

setWaveform(w)
}

function getOscLog() {
let argsText = ""

Expand All @@ -64,54 +46,45 @@ export const OscBlock = (props: OscProps) => {
return `${wave?.toLowerCase()}(${argsText})`
}

const Settings = () => (
<section className="flex flex-col space-y-2 w-full">
<div className="flex items-center gap-4 w-full">
<p className="text-1">Fn</p>
function setDutyCycle(input: string) {
setCycleText(input)

<RadixSelect
value={wave.toString()}
onChange={handleWaveChange}
options={waveformOptions}
/>
</div>
const cycle = parseInt(input)
const valid = !isNaN(cycle) && cycle >= 0 && cycle <= 255
setCycleError(!valid)

{wave === "Square" && (
<div className="flex items-center gap-4 w-full">
<p className="text-1">Cycle</p>
if (!valid) return

engine.setBlock(id, "Osc", {
waveform: { type: "Square", duty_cycle: cycle },
})
}

const settings = () => (
<Settings id={id} schema={schema}>
{wave === "Square" && (
<FieldGroup name="Duty Cycle">
<TextField.Input
className="max-w-[70px]"
size="1"
type="number"
min={0}
max={255}
value={cycleText}
onChange={(k) => {
const str = k.target.value
setCycleText(str)

const cycle = parseInt(str)
const valid = !isNaN(cycle) && cycle >= 0 && cycle <= 255
setCycleError(!valid)

if (valid) {
setWaveform({ type: "Square", duty_cycle: cycle })
}
}}
onChange={(k) => setDutyCycle(k.target.value)}
{...(cycleError && { color: "tomato" })}
/>
</div>
</FieldGroup>
)}
</section>
</Settings>
)

return (
<BaseBlock
node={props}
targets={1}
sources={1}
settings={Settings}
settings={settings}
className="px-4 py-2 font-mono text-center"
>
{getOscLog()}
Expand Down
Loading

0 comments on commit 05551ac

Please sign in to comment.