progress!

This commit is contained in:
Orion Reed 2024-07-19 23:17:34 +02:00
parent d6524330cc
commit d20ffe3d76
1 changed files with 107 additions and 16 deletions

View File

@ -5,12 +5,13 @@ import {
Rectangle2d, Rectangle2d,
TLBaseShape, TLBaseShape,
TLOnResizeHandler, TLOnResizeHandler,
TLShapeId,
resizeBox, resizeBox,
toDomPrecision, toDomPrecision,
} from 'tldraw' } from 'tldraw'
import { getUserId } from './storeUtils' import { getUserId } from './storeUtils'
export type ValueType = "scalar" | "boolean" | null export type ValueType = "SCALAR" | "BOOLEAN" | "NONE"
export type ISocialShape = TLBaseShape< export type ISocialShape = TLBaseShape<
"social", "social",
@ -21,15 +22,17 @@ export type ISocialShape = TLBaseShape<
selector: string selector: string
valueType: ValueType valueType: ValueType
values: Record<string, any> values: Record<string, any>
value: any
syntaxError: boolean
} }
> >
export class SocialShapeUtil extends BaseBoxShapeUtil<ISocialShape> { export class SocialShapeUtil extends BaseBoxShapeUtil<ISocialShape> {
static override type = 'social' as const static override type = 'social' as const
override canBind = () => false override canBind = () => true
override canEdit = () => false override canEdit = () => false
override getDefaultProps(): ISocialShape['props'] { override getDefaultProps(): ISocialShape['props'] {
return { w: 160 * 2, h: 90 * 2, text: '', selector: '', valueType: null, values: {} } return { w: 160 * 2, h: 90 * 2, text: '', selector: '', valueType: "NONE", values: {}, value: null, syntaxError: false }
} }
override onResize: TLOnResizeHandler<ISocialShape> = (shape, info) => { override onResize: TLOnResizeHandler<ISocialShape> = (shape, info) => {
return resizeBox(shape, info) return resizeBox(shape, info)
@ -43,11 +46,10 @@ export class SocialShapeUtil extends BaseBoxShapeUtil<ISocialShape> {
} }
indicator(shape: ISocialShape) { indicator(shape: ISocialShape) {
const bounds = this.editor.getShapeGeometry(shape).bounds
return ( return (
<rect <rect
width={toDomPrecision(bounds.width)} width={shape.props.w}
height={toDomPrecision(bounds.height)} height={shape.props.h}
rx={4} rx={4}
/> />
) )
@ -56,19 +58,82 @@ export class SocialShapeUtil extends BaseBoxShapeUtil<ISocialShape> {
override component(shape: ISocialShape) { override component(shape: ISocialShape) {
const currentUser = getUserId(this.editor) const currentUser = getUserId(this.editor)
const handleOnChange = (newValue: any) => { const defaultValues = {
BOOLEAN: false,
SCALAR: 0,
DEFAULT: null
}
const handleOnChange = (newValue: boolean | number) => {
this.updateProps(shape, { values: { ...shape.props.values, [currentUser]: newValue } }) this.updateProps(shape, { values: { ...shape.props.values, [currentUser]: newValue } })
console.log(shape.props.values) this.updateValue(shape.id)
}
const handleTextChange = (text: string) => {
let valueType: ValueType = "NONE"
const selector = text.match(/@([a-zA-Z]+)/)?.[1] || ''
if (text.includes('SCALAR')) {
valueType = 'SCALAR'
} else if (text.includes('BOOLEAN')) {
valueType = 'BOOLEAN'
}
if (valueType !== shape.props.valueType) {
this.updateProps(shape, { text, valueType, selector, values: {} })
} else {
this.updateProps(shape, { text, selector })
}
this.updateValue(shape.id)
} }
return ( return (
<HTMLContainer style={{ padding: 4, borderRadius: 4, border: '1px solid #ccc' }} onPointerDown={(e) => e.stopPropagation()}> <HTMLContainer style={{ padding: 4, borderRadius: 4, border: '1px solid #ccc', outline: shape.props.syntaxError ? '2px solid orange' : 'none' }} onPointerDown={(e) => e.stopPropagation()}>
<textarea style={{ width: '100%', height: '50%', border: 'none', outline: 'none', resize: 'none', pointerEvents: 'all' }} value={shape.props.text} onChange={(e) => this.updateProps(shape, { text: e.target.value })} /> <textarea style={{ width: '100%', height: '50%', border: 'none', outline: 'none', resize: 'none', pointerEvents: 'all' }} value={shape.props.text} onChange={(e) => handleTextChange(e.target.value)} />
<ValueInterface type='boolean' value={shape.props.values[currentUser] ?? false} values={shape.props.values} onChange={handleOnChange} /> <ValueInterface type={shape.props.valueType ?? null} value={shape.props.values[currentUser] ?? defaultValues[shape.props.valueType ?? 'DEFAULT']} values={shape.props.values} onChange={handleOnChange} />
</HTMLContainer> </HTMLContainer>
) )
} }
private updateValue(shapeId: TLShapeId) {
const shape = this.editor.getShape(shapeId) as ISocialShape
const valueType = shape.props.valueType
console.log("SHAPE", shape)
const vals = Array.from(Object.values(shape.props.values))
console.log("VALS", vals)
// const functionBody = `return (${shape.props.text.replace(valueType, vals)})`
const functionBody = `return ${shape.props.text.replace(valueType, 'VALUES')};`
console.log("FUNCTION BODY", functionBody)
const sum = (vals: number[] | boolean[]) => {
if (valueType === 'SCALAR') {
return (vals as number[]).reduce((acc, val) => acc + val, 0)
}
if (valueType === 'BOOLEAN') {
return vals.filter(Boolean).length;
}
}
const average = (vals: number[] | boolean[]) => {
if (valueType === 'SCALAR') {
return (vals as number[]).reduce((acc, val) => acc + val, 0) / vals.length
}
if (valueType === 'BOOLEAN') {
return vals.filter(Boolean).length;
}
}
try {
const func = new Function('sum', 'average', 'VALUES', functionBody)
const result = func(sum, average, vals)
console.log("VALUE", result)
this.updateProps(shape, { value: result, syntaxError: false })
} catch (e) {
console.log("ERROR", e)
this.updateProps(shape, { syntaxError: true })
}
}
private updateProps(shape: ISocialShape, props: Partial<ISocialShape['props']>) { private updateProps(shape: ISocialShape, props: Partial<ISocialShape['props']>) {
this.editor.updateShape<ISocialShape>({ this.editor.updateShape<ISocialShape>({
id: shape.id, id: shape.id,
@ -83,19 +148,45 @@ export class SocialShapeUtil extends BaseBoxShapeUtil<ISocialShape> {
function ValueInterface({ type, value, values, onChange }: { type: ValueType; value: any; values: Record<string, any>; onChange: (value: any) => void }) { function ValueInterface({ type, value, values, onChange }: { type: ValueType; value: any; values: Record<string, any>; onChange: (value: any) => void }) {
switch (type) { switch (type) {
case 'boolean': case 'BOOLEAN':
return <> return <>
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', gap: '4px' }}> <div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', gap: '4px' }}>
<input style={{ pointerEvents: 'all', width: '20px', height: '20px', margin: 0 }} type="checkbox" value={value} onChange={(e) => onChange(e.target.checked)} /> <input style={{ pointerEvents: 'all', width: '20px', height: '20px', margin: 0 }} type="checkbox" value={value} onChange={(e) => onChange(e.target.checked)} />
<div style={{ width: '1px', height: '20px', backgroundColor: 'grey' }} /> <div style={{ width: '1px', height: '20px', backgroundColor: 'grey' }} />
{Object.values(values).map((bool, i) => ( {Object.values(values).map((bool, i) => (
<div key={`boolean-${i}`} style={{ backgroundColor: bool ? 'blue' : 'white', width: '20px', height: '20px', border: '1px solid grey', borderRadius: 2 }} /> <div key={`boolean-${i}`} style={{ backgroundColor: bool ? 'blue' : 'white', width: '20px', height: '20px', border: '1px solid lightgrey', borderRadius: 2 }} />
))} ))}
</div> </div>
</> </>
case 'scalar': case 'SCALAR':
return <div>Slider</div> return (
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', gap: '4px' }}>
<input
type="range"
min="0"
max="1"
step="0.01"
value={value ?? 0}
onChange={(e) => onChange(parseFloat(e.target.value))}
style={{ width: '100px', pointerEvents: 'all' }}
/>
<span style={{ fontFamily: 'monospace' }}>{(value ?? 0).toFixed(2)}</span>
<div style={{ width: '1px', height: '20px', backgroundColor: 'grey' }} />
{Object.values(values).map((val, i) => (
<div
key={`scalar-${i}`}
style={{
backgroundColor: `rgba(0, 0, 255, ${val ?? 0})`,
width: '20px',
height: '20px',
border: '1px solid lightgrey',
borderRadius: 2
}}
/>
))}
</div>
);
default: default:
return <div>No Interface...</div> return <div style={{ marginTop: 10, textAlign: 'center' }}>No Interface...</div>
} }
} }