Add Angle & Length constraint/value, and modal for the details (#54)

* setup UI for getting length and Angle values from user

* enable setLength-angle for angledLines with an exisiting constraining value
This commit is contained in:
Kurt Hutten
2023-03-10 08:48:50 +11:00
committed by GitHub
parent 2bb987b3b5
commit c3ead3b074
8 changed files with 485 additions and 27 deletions

View File

@ -4,6 +4,7 @@ import { getNodePathFromSourceRange } from './lang/queryAst'
import { HorzVert } from './components/Toolbar/HorzVert'
import { Equal } from './components/Toolbar/Equal'
import { SetHorzDistance } from './components/Toolbar/SetHorzDistance'
import { SetAngleLength } from './components/Toolbar/SetAngleLength'
export const Toolbar = () => {
const {
@ -159,6 +160,8 @@ export const Toolbar = () => {
<Equal />
<SetHorzDistance horOrVert="setHorzDistance" />
<SetHorzDistance horOrVert="setVertDistance" />
<SetAngleLength angleOrLength="setAngle" />
<SetAngleLength angleOrLength="setLength" />
</div>
)
}

View File

@ -0,0 +1,272 @@
import { Dialog, Transition } from '@headlessui/react'
import { Fragment, useState, useRef, useEffect } from 'react'
import { abstractSyntaxTree, Value } from '../lang/abstractSyntaxTree'
import { executor } from '../lang/executor'
import { findUniqueName } from '../lang/modifyAst'
import { PrevVariable } from '../lang/queryAst'
import { lexer } from '../lang/tokeniser'
import { useStore } from '../useStore'
export const SetAngleLengthModal = ({
isOpen,
onResolve,
onReject,
prevVariables,
value: initialValue,
valueName,
}: {
isOpen: boolean
onResolve: (a: {
value: string
valueNode: Value
variableName?: string
}) => void
onReject: (a: any) => void
prevVariables: PrevVariable<number>[]
value: number
valueName: string
}) => {
const { ast, programMemory } = useStore((s) => ({
ast: s.ast,
programMemory: s.programMemory,
}))
const [value, setValue] = useState(String(initialValue))
const [calcResult, setCalcResult] = useState('NAN')
const [shouldCreateVariable, setShouldCreateVariable] = useState(false)
const [newVariableName, setNewVariableName] = useState('')
const [isNewVariableNameUnique, setIsNewVariableNameUnique] = useState(true)
const [valueNode, setValueNode] = useState<any>(null)
const inputRef = useRef<HTMLInputElement>(null)
useEffect(() => {
setTimeout(() => {
inputRef.current && inputRef.current.focus()
inputRef.current &&
inputRef.current.setSelectionRange(0, String(value).length)
}, 100)
if (ast) {
setNewVariableName(findUniqueName(ast, valueName))
}
}, [])
useEffect(() => {
const allVarNames = Object.keys(programMemory.root)
if (allVarNames.includes(newVariableName)) {
setIsNewVariableNameUnique(false)
} else {
setIsNewVariableNameUnique(true)
}
}, [newVariableName])
useEffect(() => {
try {
const code = `const __result__ = ${value}\nshow(__result__)`
const ast = abstractSyntaxTree(lexer(code))
const _programMem: any = { root: {} }
prevVariables.forEach(({ key, value }) => {
_programMem.root[key] = { type: 'userVal', value, __meta: [] }
})
const programMemory = executor(ast, _programMem)
const resultDeclaration = ast.body.find(
(a) =>
a.type === 'VariableDeclaration' &&
a.declarations?.[0]?.id?.name === '__result__'
)
const init =
resultDeclaration?.type === 'VariableDeclaration' &&
resultDeclaration?.declarations?.[0]?.init
console.log(init)
setCalcResult(programMemory?.root?.__result__?.value || 'NAN')
setValueNode(init)
} catch (e) {
setCalcResult('NAN')
setValueNode(null)
}
}, [value])
return (
<Transition appear show={isOpen} as={Fragment}>
<Dialog as="div" className="relative z-10" onClose={onReject}>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-black bg-opacity-25" />
</Transition.Child>
<div className="fixed inset-0 overflow-y-auto">
<div className="flex min-h-full items-center justify-center p-4 text-center">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
>
<Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-2xl bg-white p-6 text-left align-middle shadow-xl transition-all">
<Dialog.Title
as="h3"
className="text-lg font-medium leading-6 text-gray-900 capitalize"
>
Set {valueName}
</Dialog.Title>
<div className="block text-sm font-medium text-gray-700 mt-3 font-mono capitalize">
Available Variables
</div>
<ul className="flex flex-col">
{prevVariables.length &&
prevVariables.map(({ key, value }) => (
<li key={key}>
<button
className="flex w-full justify-between items-center rounded-md hover:bg-gray-100 max-w-xs"
onClick={(e) => {
const selectionStart =
inputRef.current?.selectionStart
let selectionEnd = inputRef.current?.selectionEnd
let newValue = ''
if (
typeof selectionStart === 'number' &&
typeof selectionEnd === 'number'
) {
newValue = stringSplice(
inputRef.current?.value || '',
selectionStart,
selectionEnd,
key
)
selectionEnd = selectionStart + key.length
} else {
newValue = inputRef.current?.value + key
}
setValue(newValue)
inputRef.current?.focus()
setTimeout(() => {
// run in the next render cycle
const _selectionEnd =
typeof selectionEnd === 'number'
? selectionEnd
: newValue.length
inputRef.current?.setSelectionRange(
_selectionEnd,
_selectionEnd
)
})
}}
>
<span className="font-[monospace] text-gray-800">
{key}
</span>{' '}
<span className="font-[monospace] text-gray-600 w-24 text-start font-bold">
{value}
</span>
</button>
</li>
))}
</ul>
<label
htmlFor="val"
className="block text-sm font-medium text-gray-700 mt-3 font-mono capitalize"
>
{valueName} Value
</label>
<div className="mt-1">
<input
ref={inputRef}
type="text"
name="val"
id="val"
className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md font-mono pl-1"
value={value}
onChange={(e) => {
setValue(e.target.value)
}}
/>
</div>
<div className="font-[monospace] pl-4 text-gray-600">
<span
className={`${
calcResult === 'NAN' ? 'bg-pink-200' : ''
} px-2 py-0.5 rounded`}
>
= {calcResult}
</span>
</div>
<label
htmlFor="val"
className="block text-sm font-medium text-gray-700 mt-3 font-mono"
>
Create new variable
</label>
<div className="mt-1 flex flex-1">
<input
type="checkbox"
name="val"
id="val"
className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md font-mono pl-1 flex-shrink"
checked={shouldCreateVariable}
onChange={(e) => {
setShouldCreateVariable(e.target.checked)
}}
/>
<input
type="text"
disabled={!shouldCreateVariable}
name="val"
id="val"
className={`shadow-sm font-[monospace] focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md font-mono pl-1 flex-shrink-0 ${
!shouldCreateVariable ? 'opacity-50' : ''
}`}
value={newVariableName}
onChange={(e) => {
setNewVariableName(e.target.value)
}}
/>
</div>
{!isNewVariableNameUnique && (
<div className="bg-pink-200 rounded px-2 py-0.5 text-xs">
Sorry, that's not a unique variable name. Please try
something else
</div>
)}
<div className="mt-4">
<button
type="button"
disabled={calcResult === 'NAN' || !isNewVariableNameUnique}
className={`inline-flex justify-center rounded-md border border-transparent bg-blue-100 px-4 py-2 text-sm font-medium text-blue-900 hover:bg-blue-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 ${
calcResult === 'NAN' || !isNewVariableNameUnique
? 'opacity-50 cursor-not-allowed'
: ''
}`}
onClick={() =>
onResolve({
value,
valueNode,
variableName: shouldCreateVariable
? newVariableName
: undefined,
})
}
>
Add constraining value
</button>
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition>
)
}
function stringSplice(str: string, index: number, count: number, add: string) {
return str.slice(0, index) + (add || '') + str.slice(index + count)
}

View File

@ -0,0 +1,118 @@
import { useState, useEffect } from 'react'
import { create } from 'react-modal-promise'
import { toolTips, useStore } from '../../useStore'
import { Value } from '../../lang/abstractSyntaxTree'
import {
getNodePathFromSourceRange,
getNodeFromPath,
findAllPreviousVariables,
} from '../../lang/queryAst'
import {
TransformInfo,
getTransformInfos,
transformAstSketchLines,
} from '../../lang/std/sketchcombos'
import { SetAngleLengthModal } from '../SetAngleModal'
import {
createIdentifier,
createVariableDeclaration,
} from '../../lang/modifyAst'
const getModalInfo = create(SetAngleLengthModal as any)
export const SetAngleLength = ({
angleOrLength,
}: {
angleOrLength: 'setAngle' | 'setLength'
}) => {
const { guiMode, selectionRanges, ast, programMemory, updateAst } = useStore(
(s) => ({
guiMode: s.guiMode,
ast: s.ast,
updateAst: s.updateAst,
selectionRanges: s.selectionRanges,
programMemory: s.programMemory,
})
)
const [enableHorz, setEnableHorz] = useState(false)
const [transformInfos, setTransformInfos] = useState<TransformInfo[]>()
useEffect(() => {
if (!ast) return
const paths = selectionRanges.map((selectionRange) =>
getNodePathFromSourceRange(ast, selectionRange)
)
const nodes = paths.map(
(pathToNode) => getNodeFromPath<Value>(ast, pathToNode).node
)
const isAllTooltips = nodes.every(
(node) =>
node?.type === 'CallExpression' &&
toolTips.includes(node.callee.name as any)
)
const theTransforms = getTransformInfos(selectionRanges, ast, angleOrLength)
setTransformInfos(theTransforms)
const _enableHorz = isAllTooltips && theTransforms.every(Boolean)
setEnableHorz(_enableHorz)
}, [guiMode, selectionRanges])
if (guiMode.mode !== 'sketch') return null
return (
<button
onClick={async () => {
if (!(transformInfos && ast)) return
const { modifiedAst, valueUsedInTransform } = transformAstSketchLines({
ast: JSON.parse(JSON.stringify(ast)),
selectionRanges,
transformInfos,
programMemory,
referenceSegName: '',
})
const availableVarInfo = findAllPreviousVariables(
modifiedAst,
programMemory,
selectionRanges[0]
)
try {
const { valueNode, variableName } = await getModalInfo({
value: valueUsedInTransform,
prevVariables: availableVarInfo.variables,
valueName: angleOrLength === 'setAngle' ? 'angle' : 'length',
} as any)
const { modifiedAst: _modifiedAst } = transformAstSketchLines({
ast: JSON.parse(JSON.stringify(ast)),
selectionRanges,
transformInfos,
programMemory,
referenceSegName: '',
forceValueUsedInTransform: variableName
? createIdentifier(variableName)
: valueNode,
})
if (variableName) {
const newBody = [..._modifiedAst.body]
newBody.splice(
availableVarInfo.insertIndex,
0,
createVariableDeclaration(variableName, valueNode)
)
_modifiedAst.body = newBody
}
updateAst(_modifiedAst)
} catch (e) {
console.log('e', e)
}
}}
className={`border m-1 px-1 rounded text-xs ${
enableHorz ? 'bg-gray-50 text-gray-800' : 'bg-gray-200 text-gray-400'
}`}
disabled={!enableHorz}
>
{angleOrLength}
</button>
)
}

View File

@ -160,6 +160,12 @@ export const executor = (
__meta,
}
}
} else if (declaration.init.type === 'Identifier') {
_programMemory.root[variableName] = {
type: 'userVal',
value: _programMemory.root[declaration.init.name].value,
__meta,
}
} else if (declaration.init.type === 'Literal') {
_programMemory.root[variableName] = {
type: 'userVal',

View File

@ -29,7 +29,6 @@ export function addSketchTo(
name = ''
): { modifiedAst: Program; id: string; pathToNode: (string | number)[] } {
const _node = { ...node }
const dumbyStartend = { start: 0, end: 0 }
const _name = name || findUniqueName(node, 'part')
const startSketchAt = createCallExpression('startSketchAt', [

View File

@ -135,7 +135,7 @@ export function getNodePathFromSourceRange(
return path
}
interface PrevVariable<T> {
export interface PrevVariable<T> {
key: string
value: T
}

View File

@ -160,12 +160,15 @@ export const lineTo: SketchLineHelper = {
])
const callIndex = getLastIndex(pathToNode)
if (replaceExisting && createCallback) {
const boop = createCallback(newVals, referencedSegment)
pipe.body[callIndex] = boop.callExp
const { callExp, valueUsedInTransform } = createCallback(
newVals,
referencedSegment
)
pipe.body[callIndex] = callExp
return {
modifiedAst: _node,
pathToNode,
valueUsedInTransform: boop.valueUsedInTransform,
valueUsedInTransform: valueUsedInTransform,
}
} else {
pipe.body = [...pipe.body, newLine]
@ -599,16 +602,23 @@ export const angledLine: SketchLineHelper = {
const newAngleVal = createLiteral(roundOff(getAngle(from, to), 0))
const newLengthVal = createLiteral(roundOff(getLength(from, to), 2))
const newLine = createCallback
? createCallback([newAngleVal, newLengthVal]).callExp
: createCallExpression('angledLine', [
const newLine = createCallExpression('angledLine', [
createArrayExpression([newAngleVal, newLengthVal]),
createPipeSubstitution(),
])
const callIndex = getLastIndex(pathToNode)
if (replaceExisting) {
pipe.body[callIndex] = newLine
if (replaceExisting && createCallback) {
const { callExp, valueUsedInTransform } = createCallback([
newAngleVal,
newLengthVal,
])
pipe.body[callIndex] = callExp
return {
modifiedAst: _node,
pathToNode,
valueUsedInTransform,
}
} else {
pipe.body = [...pipe.body, newLine]
}

View File

@ -15,6 +15,7 @@ import {
import {
createBinaryExpression,
createCallExpression,
createIdentifier,
createLiteral,
createPipeSubstitution,
createUnaryExpression,
@ -40,6 +41,8 @@ export type ConstraintType =
| 'equalangle'
| 'setHorzDistance'
| 'setVertDistance'
| 'setAngle'
| 'setLength'
function createCallWrapper(
a: TooTip,
@ -75,14 +78,38 @@ type TransformMap = {
}
}
const basicAngledLineCreateNode: TransformInfo['createNode'] =
({ referenceSegName, tag }) =>
(args) =>
createCallWrapper(
const basicAngledLineCreateNode =
(
referenceSeg: 'ang' | 'len' | 'none' = 'none',
valToForce: 'ang' | 'len' | 'none' = 'none',
varValToUse: 'ang' | 'len' | 'none' = 'none'
): TransformInfo['createNode'] =>
({ referenceSegName, tag, forceValueUsedInTransform, varValA, varValB }) =>
(args) => {
const nonForcedAng =
varValToUse === 'ang'
? varValA
: referenceSeg === 'ang'
? createSegAngle(referenceSegName)
: args[0]
const nonForcedLen =
varValToUse === 'len'
? varValB
: referenceSeg === 'len'
? createSegLen(referenceSegName)
: args[1]
const shouldForceAng = valToForce === 'ang' && forceValueUsedInTransform
const shouldForceLen = valToForce === 'len' && forceValueUsedInTransform
return createCallWrapper(
'angledLine',
[args[0], createSegLen(referenceSegName)],
tag
[
shouldForceAng ? forceValueUsedInTransform : nonForcedAng,
shouldForceLen ? forceValueUsedInTransform : nonForcedLen,
],
tag,
getArgLiteralVal(valToForce === 'ang' ? args[0] : args[1])
)
}
const angledLineAngleCreateNode: TransformInfo['createNode'] =
({ referenceSegName, varValA, tag }) =>
() =>
@ -133,7 +160,7 @@ const getLegAng = (arg: Value, legAngleVal: BinaryPart) => {
'+',
legAngleVal,
])
return truncatedTo90 == 0 ? legAngleVal : binExp
return truncatedTo90 === 0 ? legAngleVal : binExp
}
const getAngleLengthSign = (arg: Value, legAngleVal: BinaryPart) => {
@ -258,7 +285,7 @@ const transformMap: TransformMap = {
free: {
equalLength: {
tooltip: 'angledLine',
createNode: basicAngledLineCreateNode,
createNode: basicAngledLineCreateNode('len'),
},
horizontal: {
tooltip: 'xLine',
@ -282,13 +309,21 @@ const transformMap: TransformMap = {
tooltip: 'lineTo',
createNode: setHorzVertDistanceCreateNode('y'),
},
setAngle: {
tooltip: 'angledLine',
createNode: basicAngledLineCreateNode('none', 'ang'),
},
setLength: {
tooltip: 'angledLine',
createNode: basicAngledLineCreateNode('none', 'len'),
},
},
},
lineTo: {
free: {
equalLength: {
tooltip: 'angledLine',
createNode: basicAngledLineCreateNode,
createNode: basicAngledLineCreateNode('len'),
},
horizontal: {
tooltip: 'xLineTo',
@ -377,11 +412,15 @@ const transformMap: TransformMap = {
tag
),
},
setLength: {
tooltip: 'angledLine',
createNode: basicAngledLineCreateNode('none', 'len', 'ang'),
},
},
free: {
equalLength: {
tooltip: 'angledLine',
createNode: basicAngledLineCreateNode,
createNode: basicAngledLineCreateNode('len'),
},
vertical: {
tooltip: 'yLine',
@ -423,13 +462,17 @@ const transformMap: TransformMap = {
return createCallWrapper('xLine', val, tag)
},
},
setAngle: {
tooltip: 'angledLine',
createNode: basicAngledLineCreateNode('len', 'ang', 'len'),
},
},
},
angledLineOfXLength: {
free: {
equalLength: {
tooltip: 'angledLine',
createNode: basicAngledLineCreateNode,
createNode: basicAngledLineCreateNode('len'),
},
horizontal: {
tooltip: 'xLine',
@ -479,7 +522,7 @@ const transformMap: TransformMap = {
free: {
equalLength: {
tooltip: 'angledLine',
createNode: basicAngledLineCreateNode,
createNode: basicAngledLineCreateNode('len'),
},
vertical: {
tooltip: 'yLine',
@ -530,7 +573,7 @@ const transformMap: TransformMap = {
free: {
equalLength: {
tooltip: 'angledLine',
createNode: basicAngledLineCreateNode,
createNode: basicAngledLineCreateNode('len'),
},
horizontal: {
tooltip: 'xLineTo',
@ -580,7 +623,7 @@ const transformMap: TransformMap = {
free: {
equalLength: {
tooltip: 'angledLine',
createNode: basicAngledLineCreateNode,
createNode: basicAngledLineCreateNode('len'),
},
vertical: {
tooltip: 'yLineTo',
@ -924,6 +967,13 @@ function createSegLen(referenceSegName: string): Value {
])
}
function createSegAngle(referenceSegName: string): Value {
return createCallExpression('segAngle', [
createLiteral(referenceSegName),
createPipeSubstitution(),
])
}
function createSegEnd(referenceSegName: string, isX: boolean): CallExpression {
return createCallExpression(isX ? 'segEndX' : 'segEndY', [
createLiteral(referenceSegName),