diff --git a/src/Toolbar.tsx b/src/Toolbar.tsx index 64eddd669..3e69de67f 100644 --- a/src/Toolbar.tsx +++ b/src/Toolbar.tsx @@ -3,6 +3,7 @@ import { extrudeSketch, sketchOnExtrudedFace } from './lang/modifyAst' import { getNodePathFromSourceRange } from './lang/queryAst' import { HorzVert } from './components/Toolbar/HorzVert' import { Equal } from './components/Toolbar/Equal' +import { SetHorzDistance } from './components/Toolbar/SetHorzDistance' export const Toolbar = () => { const { @@ -156,6 +157,8 @@ export const Toolbar = () => { + + ) } diff --git a/src/components/Toolbar/SetHorzDistance.tsx b/src/components/Toolbar/SetHorzDistance.tsx new file mode 100644 index 000000000..627f3cb56 --- /dev/null +++ b/src/components/Toolbar/SetHorzDistance.tsx @@ -0,0 +1,97 @@ +import { useState, useEffect } from 'react' +import { toolTips, useStore } from '../../useStore' +import { Value, VariableDeclarator } from '../../lang/abstractSyntaxTree' +import { + getNodePathFromSourceRange, + getNodeFromPath, +} from '../../lang/queryAst' +import { isSketchVariablesLinked } from '../../lang/std/sketchConstraints' +import { + TransformInfo, + transformAstForSketchLines, + getTransformInfos, +} from '../../lang/std/sketchcombos' + +export const SetHorzDistance = ({ + horOrVert, +}: { + horOrVert: 'setHorzDistance' | 'setVertDistance' +}) => { + const { guiMode, selectionRanges, ast, programMemory, updateAst } = useStore( + (s) => ({ + guiMode: s.guiMode, + ast: s.ast, + updateAst: s.updateAst, + selectionRanges: s.selectionRanges, + programMemory: s.programMemory, + }) + ) + const [enable, setEnable] = useState(false) + const [transformInfos, setTransformInfos] = useState() + useEffect(() => { + if (!ast) return + const paths = selectionRanges.map((selectionRange) => + getNodePathFromSourceRange(ast, selectionRange) + ) + const nodes = paths.map( + (pathToNode) => getNodeFromPath(ast, pathToNode).node + ) + const varDecs = paths.map( + (pathToNode) => + getNodeFromPath( + ast, + pathToNode, + 'VariableDeclarator' + )?.node + ) + const primaryLine = varDecs[0] + const secondaryVarDecs = varDecs.slice(1) + const isOthersLinkedToPrimary = secondaryVarDecs.every((secondary) => + isSketchVariablesLinked(secondary, primaryLine, ast) + ) + const isAllTooltips = nodes.every( + (node) => + node?.type === 'CallExpression' && + toolTips.includes(node.callee.name as any) + ) + + const theTransforms = getTransformInfos( + selectionRanges.slice(1), + ast, + horOrVert + ) + setTransformInfos(theTransforms) + + const _enableEqual = + secondaryVarDecs.length === 1 && + isAllTooltips && + isOthersLinkedToPrimary && + theTransforms.every(Boolean) + setEnable(_enableEqual) + }, [guiMode, selectionRanges]) + if (guiMode.mode !== 'sketch') return null + + return ( + + ) +} diff --git a/src/lang/std/sketch.ts b/src/lang/std/sketch.ts index 14f52224c..30354ac44 100644 --- a/src/lang/std/sketch.ts +++ b/src/lang/std/sketch.ts @@ -89,12 +89,11 @@ export function createFirstArg( export const lineTo: SketchLineHelper = { fn: ( - { sourceRange, programMemory }, + { sourceRange }, data: | [number, number] | { to: [number, number] - // name?: string tag?: string }, previousSketch: SketchGroup @@ -135,18 +134,38 @@ export const lineTo: SketchLineHelper = { value: [...sketchGroup.value, currentPath], } }, - add: ({ node, pathToNode, to }) => { + add: ({ + node, + pathToNode, + to, + createCallback, + replaceExisting, + referencedSegment, + }) => { const _node = { ...node } const { node: pipe } = getNodeFromPath( _node, pathToNode, 'PipeExpression' ) - const newLine = createCallExpression('lineTo', [ - createArrayExpression([createLiteral(to[0]), createLiteral(to[1])]), - createPipeSubstitution(), - ]) - pipe.body = [...pipe.body, newLine] + + const newVals: [Value, Value] = [ + createLiteral(roundOff(to[0], 2)), + createLiteral(roundOff(to[1], 2)), + ] + + const newLine = createCallback + ? createCallback(newVals, referencedSegment) + : createCallExpression('lineTo', [ + createArrayExpression(newVals), + createPipeSubstitution(), + ]) + const callIndex = getLastIndex(pathToNode) + if (replaceExisting) { + pipe.body[callIndex] = newLine + } else { + pipe.body = [...pipe.body, newLine] + } return { modifiedAst: _node, pathToNode, @@ -237,7 +256,6 @@ export const line: SketchLineHelper = { pathToNode, 'PipeExpression' ) - if (!from) throw new Error('no from') // todo #29 remove const { node: varDec } = getNodeFromPath( _node, pathToNode, @@ -431,7 +449,6 @@ export const xLine: SketchLineHelper = { add: ({ node, pathToNode, to, from, replaceExisting, createCallback }) => { const _node = { ...node } const getNode = getNodeFromPathCurry(_node, pathToNode) - if (!from) throw new Error('no from') // todo #29 remove const { node: pipe } = getNode('PipeExpression') const newVal = createLiteral(roundOff(to[0] - from[0], 2)) @@ -487,7 +504,6 @@ export const yLine: SketchLineHelper = { add: ({ node, pathToNode, to, from, replaceExisting, createCallback }) => { const _node = { ...node } const getNode = getNodeFromPathCurry(_node, pathToNode) - if (!from) throw new Error('no from') // todo #29 remove const { node: pipe } = getNode('PipeExpression') const newVal = createLiteral(roundOff(to[1] - from[1], 2)) const newLine = createCallback @@ -577,7 +593,6 @@ export const angledLine: SketchLineHelper = { const getNode = getNodeFromPathCurry(_node, pathToNode) const { node: pipe } = getNode('PipeExpression') - if (!from) throw new Error('no from') // todo #29 remove const newAngleVal = createLiteral(roundOff(getAngle(from, to), 0)) const newLengthVal = createLiteral(roundOff(getLength(from, to), 2)) const newLine = createCallback @@ -668,7 +683,6 @@ export const angledLineOfXLength: SketchLineHelper = { const variableName = varDec.id.name const sketch = previousProgramMemory?.root?.[variableName] if (sketch.type !== 'sketchGroup') throw new Error('not a sketchGroup') - if (!from) throw new Error('no from') // todo #29 remove const angle = createLiteral(roundOff(getAngle(from, to), 0)) const xLength = createLiteral(roundOff(Math.abs(from[0] - to[0]), 2) || 0.1) const newLine = createCallback @@ -762,7 +776,6 @@ export const angledLineOfYLength: SketchLineHelper = { const variableName = varDec.id.name const sketch = previousProgramMemory?.root?.[variableName] if (sketch.type !== 'sketchGroup') throw new Error('not a sketchGroup') - if (!from) throw new Error('no from') // todo #29 remove const angle = createLiteral(roundOff(getAngle(from, to), 0)) const yLength = createLiteral(roundOff(Math.abs(from[1] - to[1]), 2) || 0.1) @@ -848,7 +861,6 @@ export const angledLineToX: SketchLineHelper = { pathToNode, 'PipeExpression' ) - if (!from) throw new Error('no from') // todo #29 remove const angle = createLiteral(roundOff(getAngle(from, to), 0)) const xArg = createLiteral(roundOff(to[0], 2)) const newLine = createCallback @@ -930,7 +942,6 @@ export const angledLineToY: SketchLineHelper = { pathToNode, 'PipeExpression' ) - if (!from) throw new Error('no from') // todo #29 remove const angle = createLiteral(roundOff(getAngle(from, to), 0)) const yArg = createLiteral(roundOff(to[1], 2)) const newLine = createCallback @@ -1067,6 +1078,7 @@ export function replaceSketchLine({ to, from, createCallback, + referencedSegment, }: { node: Program programMemory: ProgramMemory @@ -1075,6 +1087,7 @@ export function replaceSketchLine({ to: [number, number] from: [number, number] createCallback: TransformCallback + referencedSegment?: Path }): { modifiedAst: Program } { if (!toolTips.includes(fnName)) throw new Error('not a tooltip') const _node = { ...node } @@ -1085,6 +1098,7 @@ export function replaceSketchLine({ node: _node, previousProgramMemory: programMemory, pathToNode: thePath, + referencedSegment, to, from, replaceExisting: true, diff --git a/src/lang/std/sketchConstraints.ts b/src/lang/std/sketchConstraints.ts index 19beca929..759d752c2 100644 --- a/src/lang/std/sketchConstraints.ts +++ b/src/lang/std/sketchConstraints.ts @@ -33,6 +33,28 @@ export const segLen: InternalFn = ( ) } +function segEndFactory(which: 'x' | 'y'): InternalFn { + return (_, segName: string, sketchGroup: SketchGroup): number => { + const line = sketchGroup?.value.find((seg) => seg.name === segName) + // maybe this should throw, but the language doesn't have a way to handle errors yet + if (!line) return 0 + return which === 'x' ? line.to[0] : line.to[1] + } +} + +export const segEndX: InternalFn = segEndFactory('x') +export const segEndY: InternalFn = segEndFactory('y') + +function lastSegFactory(which: 'x' | 'y'): InternalFn { + return (_, sketchGroup: SketchGroup): number => { + const lastLine = sketchGroup?.value[sketchGroup.value.length - 1] + return which === 'x' ? lastLine.to[0] : lastLine.to[1] + } +} + +export const lastSegX: InternalFn = lastSegFactory('x') +export const lastSegY: InternalFn = lastSegFactory('y') + function angleToMatchLengthFactory(which: 'x' | 'y'): InternalFn { return (_, segName: string, to: number, sketchGroup: SketchGroup): number => { const isX = which === 'x' diff --git a/src/lang/std/sketchcombos.test.ts b/src/lang/std/sketchcombos.test.ts index 152036547..d57fffad4 100644 --- a/src/lang/std/sketchcombos.test.ts +++ b/src/lang/std/sketchcombos.test.ts @@ -5,9 +5,10 @@ import { getTransformInfos, transformAstForSketchLines, transformAstForHorzVert, + ConstraintType, } from './sketchcombos' import { initPromise } from '../rust' -import { Ranges, TooTip } from '../../useStore' +import { TooTip } from '../../useStore' import { executor } from '../../lang/executor' import { recast } from '../../lang/recast' @@ -242,14 +243,6 @@ const part001 = startSketchAt([0, 0]) |> angledLineToY([301, myVar], %) // select for vertical constraint 10 show(part001)` it('It should transform horizontal lines the ast', () => { - // const inputScript = `const myVar = 2 - // const part001 = startSketchAt([0, 0]) - // |> lineTo([1, 1], %) - // |> line([-6.28, 1.4], %) // select for horizontal constraint 1 - // |> line([-1.07, myVar], %) // select for vertical constraint 1 - // |> line([myVar, 4.32], %) // select for horizontal constraint 2 - // |> line([6.35, -1.12], %) // select for vertical constraint 2 - // show(part001)` const expectModifiedScript = `const myVar = 2 const myVar2 = 12 const myVar3 = -10 @@ -348,3 +341,89 @@ show(part001)` expect(newCode).toBe(expectModifiedScript) }) }) + +describe('testing transformAstForSketchLines for vertical and horizontal distance constraints', () => { + describe('testing setHorzDistance for line', () => { + const inputScript = `const myVar = 1 +const part001 = startSketchAt([0, 0]) + |> line([0.31, 1.67], %) // base selection + |> line([0.45, 1.46], %) + |> line([0.45, 1.46], %) // free + |> line([myVar, 0.01], %) // xRelative + |> line([0.7, myVar], %) // yRelative +show(part001)` + it('testing for free to horizontal and vertical distance', () => { + const expectedHorizontalCode = helperThing( + inputScript, + ['// base selection', '// free'], + 'setHorzDistance' + ) + const expectedVerticalCode = helperThing( + inputScript, + ['// base selection', '// free'], + 'setVertDistance' + ) + expect(expectedHorizontalCode).toContain( + `lineTo([segEndX('seg01', %) + 1.21, 4.59], %) // free` + ) + expect(expectedVerticalCode).toContain( + `lineTo([1.21, segEndY('seg01', %) + 4.59], %) // free` + ) + }) + it('testing for xRelative to vertical distance', () => { + const expectedCode = helperThing( + inputScript, + ['// base selection', '// xRelative'], + 'setVertDistance' + ) + expect(expectedCode).toContain(`|> lineTo([ + lastSegX(%) + myVar, + segEndY('seg01', %) + 4.6 + ], %) // xRelative`) + }) + it('testing for yRelative to horizontal distance', () => { + const expectedCode = helperThing( + inputScript, + ['// base selection', '// yRelative'], + 'setHorzDistance' + ) + expect(expectedCode).toContain(`|> lineTo([ + segEndX('seg01', %) + 2.91, + lastSegY(%) + myVar + ], %) // yRelative`) + }) + }) +}) + +function helperThing( + inputScript: string, + linesOfInterest: string[], + constraint: ConstraintType +): string { + const ast = abstractSyntaxTree(lexer(inputScript)) + const selectionRanges = inputScript + .split('\n') + .filter((ln) => + linesOfInterest.some((lineOfInterest) => ln.includes(lineOfInterest)) + ) + .map((ln) => { + const comment = ln.split('//')[1] + const start = inputScript.indexOf('//' + comment) - 7 + return [start, start] + }) as [number, number][] + + const programMemory = executor(ast) + const transformInfos = getTransformInfos( + selectionRanges.slice(1), + ast, + constraint + ) + + const newAst = transformAstForSketchLines({ + ast, + selectionRanges, + transformInfos, + programMemory, + })?.modifiedAst + return recast(newAst) +} diff --git a/src/lang/std/sketchcombos.ts b/src/lang/std/sketchcombos.ts index 09e19b35e..adb5149c6 100644 --- a/src/lang/std/sketchcombos.ts +++ b/src/lang/std/sketchcombos.ts @@ -21,8 +21,9 @@ import { giveSketchFnCallTag, } from '../modifyAst' import { createFirstArg, getFirstArg, replaceSketchLine } from './sketch' -import { ProgramMemory } from '../executor' +import { Path, ProgramMemory } from '../executor' import { getSketchSegmentIndexFromSourceRange } from './sketchConstraints' +import { roundOff } from '../../lib/utils' type LineInputsType = | 'xAbsolute' @@ -37,6 +38,8 @@ export type ConstraintType = | 'vertical' | 'horizontal' | 'equalangle' + | 'setHorzDistance' + | 'setVertDistance' function createCallWrapper( a: TooTip, @@ -54,7 +57,8 @@ export function replaceSketchCall( ast: Program, range: Range, transformTo: TooTip, - createCallback: TransformCallback + createCallback: TransformCallback, + referenceSegName: string ): { modifiedAst: Program } { const path = getNodePathFromSourceRange(ast, range) const getNode = getNodeFromPathCurry(ast, path) @@ -65,11 +69,15 @@ export function replaceSketchCall( if (!sketchGroup || sketchGroup.type !== 'sketchGroup') throw new Error('not a sketch group') const seg = getSketchSegmentIndexFromSourceRange(sketchGroup, range) + const referencedSegment = sketchGroup.value.find( + (path) => path.name === referenceSegName + ) const { to, from } = seg const { modifiedAst } = replaceSketchLine({ node: ast, programMemory, sourceRange: range, + referencedSegment, fnName: transformTo || (callExp.callee.name as TooTip), to, from, @@ -85,7 +93,7 @@ export type TransformInfo = { varValB: Value // y / length or x y for angledLineOfXlength etc referenceSegName: string tag?: Value - }) => (args: [Value, Value]) => Value + }) => (args: [Value, Value], referencedSegment?: Path) => Value } type TransformMap = { @@ -163,6 +171,57 @@ const getAngleLengthSign = (arg: Value, legAngleVal: BinaryPart) => { return normalisedAngle > 90 ? createUnaryExpression(legAngleVal) : legAngleVal } +const setHorzVertDistanceCreateNode = + (isX = true): TransformInfo['createNode'] => + ({ referenceSegName, tag }) => { + return (args, referencedSegment) => { + const makeBinExp = (index: 0 | 1) => { + const arg = getArgLiteralVal(args?.[index]) + return createBinaryExpression([ + createSegEnd(referenceSegName, isX), + '+', + createLiteral( + roundOff(arg - (referencedSegment?.to?.[index] || 0), 2) + ), + ]) + } + return createCallWrapper( + 'lineTo', + isX ? [makeBinExp(0), args[1]] : [args[0], makeBinExp(1)], + tag + ) + } + } + +const setHorzVertDistanceConstraintLineCreateNode = + (isX: boolean): TransformInfo['createNode'] => + ({ referenceSegName, tag, varValA, varValB }) => { + const varVal = (isX ? varValB : varValA) as BinaryPart + const varValBinExp = createBinaryExpression([ + createLastSeg(!isX), + '+', + varVal, + ]) + + return (args, referencedSegment) => { + const makeBinExp = (index: 0 | 1) => { + const arg = getArgLiteralVal(args?.[index]) + return createBinaryExpression([ + createSegEnd(referenceSegName, isX), + '+', + createLiteral( + roundOff(arg - (referencedSegment?.to?.[index] || 0), 2) + ), + ]) + } + return createCallWrapper( + 'lineTo', + isX ? [makeBinExp(0), varValBinExp] : [varValBinExp, makeBinExp(1)], + tag + ) + } + } + const transformMap: TransformMap = { line: { xRelative: { @@ -188,6 +247,10 @@ const transformMap: TransformMap = { () => createCallWrapper('xLine', varValA, tag), }, + setVertDistance: { + tooltip: 'lineTo', + createNode: setHorzVertDistanceConstraintLineCreateNode(false), + }, }, yRelative: { equalLength: { @@ -212,6 +275,10 @@ const transformMap: TransformMap = { () => createCallWrapper('yLine', varValB, tag), }, + setHorzDistance: { + tooltip: 'lineTo', + createNode: setHorzVertDistanceConstraintLineCreateNode(true), + }, }, free: { equalLength: { @@ -232,6 +299,14 @@ const transformMap: TransformMap = { (args) => createCallWrapper('yLine', args[1], tag), }, + setHorzDistance: { + tooltip: 'lineTo', + createNode: setHorzVertDistanceCreateNode(true), + }, + setVertDistance: { + tooltip: 'lineTo', + createNode: setHorzVertDistanceCreateNode(false), + }, }, }, lineTo: { @@ -792,7 +867,8 @@ export function transformAstForSketchLines({ referenceSegName: tag, varValA, varValB, - }) + }), + tag ) node = modifiedAst }) @@ -837,7 +913,8 @@ export function transformAstForHorzVert({ varValA, varValB, tag, - }) + }), + tag?.type === 'Literal' ? String(tag.value) : '' ) node = modifiedAst }) @@ -850,3 +927,20 @@ function createSegLen(referenceSegName: string): Value { createPipeSubstitution(), ]) } + +function createSegEnd(referenceSegName: string, isX: boolean): CallExpression { + return createCallExpression(isX ? 'segEndX' : 'segEndY', [ + createLiteral(referenceSegName), + createPipeSubstitution(), + ]) +} + +function createLastSeg(isX: boolean): CallExpression { + return createCallExpression(isX ? 'lastSegX' : 'lastSegY', [ + createPipeSubstitution(), + ]) +} + +function getArgLiteralVal(arg: Value): number { + return arg?.type === 'Literal' ? Number(arg.value) : 0 +} diff --git a/src/lang/std/std.ts b/src/lang/std/std.ts index c2ce6e654..0d4edc41b 100644 --- a/src/lang/std/std.ts +++ b/src/lang/std/std.ts @@ -17,6 +17,10 @@ import { segLen, angleToMatchLengthX, angleToMatchLengthY, + segEndX, + segEndY, + lastSegX, + lastSegY, } from './sketchConstraints' import { extrude, getExtrudeWallTransform } from './extrude' import { Quaternion, Vector3 } from 'three' @@ -100,6 +104,10 @@ export const internalFns: { [key in InternalFnNames]: InternalFn } = { legLen, legAngX, legAngY, + segEndX, + segEndY, + lastSegX, + lastSegY, segLen, angleToMatchLengthX, angleToMatchLengthY, diff --git a/src/lang/std/stdTypes.ts b/src/lang/std/stdTypes.ts index f3dd5941e..98d203740 100644 --- a/src/lang/std/stdTypes.ts +++ b/src/lang/std/stdTypes.ts @@ -24,6 +24,10 @@ export type InternalFnNames = | 'legLen' | 'legAngX' | 'legAngY' + | 'segEndX' + | 'segEndY' + | 'lastSegX' + | 'lastSegY' | 'segLen' | 'angleToMatchLengthX' | 'angleToMatchLengthY' @@ -52,7 +56,8 @@ export interface ModifyAstBase { interface addCall extends ModifyAstBase { to: [number, number] - from?: [number, number] + from: [number, number] + referencedSegment?: Path replaceExisting?: boolean createCallback?: TransformCallback // TODO: #29 probably should not be optional } @@ -62,7 +67,10 @@ interface updateArgs extends ModifyAstBase { to: [number, number] } -export type TransformCallback = (args: [Value, Value]) => Value +export type TransformCallback = ( + args: [Value, Value], + referencedSegment?: Path +) => Value export type SketchCallTransfromMap = { [key in TooTip]: TransformCallback