diff --git a/package.json b/package.json index 350dff978..79213f441 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "react-json-view": "^1.21.3", "react-modal-promise": "^1.0.2", "react-scripts": "5.0.1", - "sketch-helpers": "^0.0.1", + "sketch-helpers": "^0.0.2", "swr": "^2.0.4", "three": "^0.146.0", "typescript": "^4.4.2", diff --git a/src/Toolbar.tsx b/src/Toolbar.tsx index f9af745c8..fc0e3418d 100644 --- a/src/Toolbar.tsx +++ b/src/Toolbar.tsx @@ -4,6 +4,7 @@ import { getNodePathFromSourceRange } from './lang/queryAst' import { HorzVert } from './components/Toolbar/HorzVert' import { EqualLength } from './components/Toolbar/EqualLength' import { EqualAngle } from './components/Toolbar/EqualAngle' +import { Intersect } from './components/Toolbar/Intersect' import { SetHorzDistance } from './components/Toolbar/SetHorzDistance' import { SetAngleLength } from './components/Toolbar/SetAngleLength' @@ -123,38 +124,42 @@ export const Toolbar = () => { Exit sketch )} - {toolTips.map((sketchFnName) => { - if ( - guiMode.mode !== 'sketch' || - !('isTooltip' in guiMode || guiMode.sketchMode === 'sketchEdit') + {toolTips + .filter( + (sketchFnName) => !['angledLineThatIntersects'].includes(sketchFnName) ) - return null - return ( - - ) - })} + .map((sketchFnName) => { + if ( + guiMode.mode !== 'sketch' || + !('isTooltip' in guiMode || guiMode.sketchMode === 'sketchEdit') + ) + return null + return ( + + ) + })}

@@ -164,6 +169,7 @@ export const Toolbar = () => { + ) } diff --git a/src/components/Toolbar/Intersect.tsx b/src/components/Toolbar/Intersect.tsx new file mode 100644 index 000000000..980ae8609 --- /dev/null +++ b/src/components/Toolbar/Intersect.tsx @@ -0,0 +1,146 @@ +import { useState, useEffect } from 'react' +import { create } from 'react-modal-promise' +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, + transformSecondarySketchLinesTagFirst, + getTransformInfos, +} from '../../lang/std/sketchcombos' +import { GetInfoModal } from '../SetHorVertDistanceModal' +import { + createIdentifier, + createVariableDeclaration, +} from '../../lang/modifyAst' + +const getModalInfo = create(GetInfoModal as any) + +export const Intersect = () => { + 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, + 'startSketchAt', // TODO probably a better place for this to live + ].includes(node.callee.name as any) + ) + + const theTransforms = getTransformInfos( + selectionRanges.slice(1), + ast, + 'intersect' + ) + 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/executor.ts b/src/lang/executor.ts index aaf862bd6..5b4c5114e 100644 --- a/src/lang/executor.ts +++ b/src/lang/executor.ts @@ -507,7 +507,8 @@ function executeObjectExpression( } else if (property.value.type === 'BinaryExpression') { obj[property.key.name] = getBinaryExpressionResult( property.value, - _programMemory + _programMemory, + _pipeInfo ) } else if (property.value.type === 'PipeExpression') { obj[property.key.name] = getPipeExpressionResult( diff --git a/src/lang/std/sketch.ts b/src/lang/std/sketch.ts index 316b1b607..79c1f897f 100644 --- a/src/lang/std/sketch.ts +++ b/src/lang/std/sketch.ts @@ -12,6 +12,7 @@ import { VariableDeclarator, Value, Literal, + VariableDeclaration, } from '../abstractSyntaxTree' import { getNodeFromPath, @@ -41,6 +42,10 @@ import { } from '../modifyAst' import { roundOff, getLength, getAngle } from '../../lib/utils' import { getSketchSegmentIndexFromSourceRange } from './sketchConstraints' +import { + intersectionWithParallelLine, + perpendicularDistance, +} from 'sketch-helpers' export type Coords2d = [number, number] @@ -312,7 +317,7 @@ export const line: SketchLineHelper = { }, updateArgs: ({ node, pathToNode, to, from }) => { const _node = { ...node } - const { node: callExpression, path } = getNodeFromPath( + const { node: callExpression } = getNodeFromPath( _node, pathToNode ) @@ -338,9 +343,9 @@ export const line: SketchLineHelper = { ) { toProp.value = toArrExp } + mutateObjExpProp(callExpression.arguments?.[0], toArrExp, 'to') } else { - mutateArrExp(callExpression.arguments?.[0], toArrExp) || - mutateObjExpProp(callExpression.arguments?.[0], toArrExp, 'to') + mutateArrExp(callExpression.arguments?.[0], toArrExp) } return { modifiedAst: _node, @@ -397,7 +402,7 @@ export const xLineTo: SketchLineHelper = { pathToNode, } }, - updateArgs: ({ node, pathToNode, to, from }) => { + updateArgs: ({ node, pathToNode, to }) => { const _node = { ...node } const { node: callExpression } = getNodeFromPath( _node, @@ -1084,6 +1089,126 @@ export const angledLineToY: SketchLineHelper = { addTag: addTagWithTo('angleTo'), } +export const angledLineThatIntersects: SketchLineHelper = { + fn: ( + { sourceRange, programMemory }, + data: { + angle: number + intersectTag: string + offset?: number + tag?: string + }, + previousSketch: SketchGroup + ) => { + if (!previousSketch) throw new Error('lineTo must be called after lineTo') + const intersectPath = previousSketch.value.find( + ({ name }) => name === data.intersectTag + ) + if (!intersectPath) throw new Error('intersectTag must match a line') + const from = getCoordsFromPaths( + previousSketch, + previousSketch.value.length - 1 + ) + const to = intersectionWithParallelLine({ + line1: [intersectPath.from, intersectPath.to], + line1Offset: data.offset || 0, + line2Point: from, + line2Angle: data.angle, + }) + return lineTo.fn( + { sourceRange, programMemory }, + { to, tag: data.tag }, + previousSketch + ) + }, + add: ({ + node, + pathToNode, + to, + from, + createCallback, + replaceExisting, + referencedSegment, + }) => { + const _node = { ...node } + const { node: pipe } = getNodeFromPath( + _node, + pathToNode, + 'PipeExpression' + ) + const angle = createLiteral(roundOff(getAngle(from, to), 0)) + if (!referencedSegment) + throw new Error('referencedSegment must be provided') + const offset = createLiteral( + roundOff( + perpendicularDistance( + referencedSegment?.from, + referencedSegment?.to, + to + ), + 2 + ) + ) + + if (replaceExisting && createCallback) { + const { callExp, valueUsedInTransform } = createCallback([angle, offset]) + const callIndex = getLastIndex(pathToNode) + pipe.body[callIndex] = callExp + return { + modifiedAst: _node, + pathToNode, + valueUsedInTransform, + } + } + throw new Error('not implemented') + }, + updateArgs: ({ node, pathToNode, to, from, previousProgramMemory }) => { + const _node = { ...node } + const { node: callExpression, path } = getNodeFromPath( + _node, + pathToNode + ) + const angle = roundOff(getAngle(from, to), 0) + + const firstArg = callExpression.arguments?.[0] + const intersectTag = + firstArg.type === 'ObjectExpression' + ? firstArg.properties.find((p) => p.key.name === 'intersectTag') + ?.value || createLiteral('') + : createLiteral('') + const intersectTagName = + intersectTag.type === 'Literal' ? intersectTag.value : '' + const { node: varDec } = getNodeFromPath( + _node, + pathToNode, + 'VariableDeclaration' + ) + + const varName = varDec.declarations[0].id.name + const sketchGroup = previousProgramMemory.root[varName] as SketchGroup + const intersectPath = sketchGroup.value.find( + ({ name }) => name === intersectTagName + ) + let offset = 0 + if (intersectPath) { + offset = roundOff( + perpendicularDistance(intersectPath?.from, intersectPath?.to, to), + 2 + ) + } + + const angleLit = createLiteral(angle) + + mutateObjExpProp(firstArg, angleLit, 'angle') + mutateObjExpProp(firstArg, createLiteral(offset), 'offset') + return { + modifiedAst: _node, + pathToNode, + } + }, + addTag: addTagWithTo('angleTo'), // TODO might be wrong +} + export const sketchLineHelperMap: { [key: string]: SketchLineHelper } = { line, lineTo, @@ -1096,6 +1221,7 @@ export const sketchLineHelperMap: { [key: string]: SketchLineHelper } = { angledLineOfYLength, angledLineToX, angledLineToY, + angledLineThatIntersects, } as const export function changeSketchArguments( @@ -1116,6 +1242,7 @@ export function changeSketchArguments( if (callExpression?.callee?.name in sketchLineHelperMap) { const { updateArgs } = sketchLineHelperMap[callExpression.callee.name] + if (!updateArgs) throw new Error('not a sketch line helper') return updateArgs({ node: _node, previousProgramMemory: programMemory, @@ -1235,7 +1362,8 @@ export function replaceSketchLine({ createCallback: TransformCallback referencedSegment?: Path }): { modifiedAst: Program; valueUsedInTransform?: number } { - if (!toolTips.includes(fnName)) throw new Error('not a tooltip') + if (![...toolTips, 'intersect'].includes(fnName)) + throw new Error('not a tooltip') const _node = { ...node } const thePath = getNodePathFromSourceRange(_node, sourceRange) @@ -1540,6 +1668,26 @@ function getFirstArgValuesForXYLineFns(callExpression: CallExpression): { throw new Error('expected ArrayExpression or ObjectExpression') } +const getAngledLineThatIntersects = ( + callExp: CallExpression +): { + val: [Value, Value] + tag?: Value +} => { + const firstArg = callExp.arguments[0] + if (firstArg.type === 'ObjectExpression') { + const tag = firstArg.properties.find((p) => p.key.name === 'tag')?.value + const angle = firstArg.properties.find((p) => p.key.name === 'angle')?.value + const offset = firstArg.properties.find( + (p) => p.key.name === 'offset' + )?.value + if (angle && offset) { + return { val: [angle, offset], tag } + } + } + throw new Error('expected ArrayExpression or ObjectExpression') +} + export function getFirstArg(callExp: CallExpression): { val: Value | [Value, Value] tag?: Value @@ -1565,5 +1713,8 @@ export function getFirstArg(callExp: CallExpression): { if (['startSketchAt'].includes(name)) { return getFirstArgValuesForXYLineFns(callExp) } + if (['angledLineThatIntersects'].includes(name)) { + return getAngledLineThatIntersects(callExp) + } throw new Error('unexpected call expression') } diff --git a/src/lang/std/sketchcombos.ts b/src/lang/std/sketchcombos.ts index 41ba2ef91..c0631eb8f 100644 --- a/src/lang/std/sketchcombos.ts +++ b/src/lang/std/sketchcombos.ts @@ -17,6 +17,7 @@ import { createCallExpression, createIdentifier, createLiteral, + createObjectExpression, createPipeSubstitution, createUnaryExpression, giveSketchFnCallTag, @@ -43,6 +44,7 @@ export type ConstraintType = | 'setVertDistance' | 'setAngle' | 'setLength' + | 'intersect' function createCallWrapper( a: TooTip, @@ -59,6 +61,38 @@ function createCallWrapper( } } +function intersectCallWrapper({ + fnName, + angleVal, + offsetVal, + intersectTag, + tag, + valueUsedInTransform, +}: { + fnName: string + angleVal: Value + offsetVal: Value + intersectTag: Value + tag?: Value + valueUsedInTransform?: number +}): ReturnType { + const firstArg: any = { + angle: angleVal, + offset: offsetVal, + intersectTag, + } + if (tag) { + firstArg['tag'] = tag + } + return { + callExp: createCallExpression(fnName, [ + createObjectExpression(firstArg), + createPipeSubstitution(), + ]), + valueUsedInTransform, + } +} + export type TransformInfo = { tooltip: TooTip createNode: (a: { @@ -71,7 +105,7 @@ export type TransformInfo = { } type TransformMap = { - [key in TooTip]: { + [key in TooTip]?: { [key in LineInputsType | 'free']?: { [key in ConstraintType]?: TransformInfo } @@ -307,6 +341,44 @@ const setHorzVertDistanceConstraintLineCreateNode = } } +const setAngledIntersectLineForLines: TransformInfo['createNode'] = + ({ referenceSegName, tag, forceValueUsedInTransform }) => + (args) => { + const valueUsedInTransform = roundOff( + args[1].type === 'Literal' ? Number(args[1].value) : 0, + 2 + ) + const angle = args[0].type === 'Literal' ? Number(args[0].value) : 0 + return intersectCallWrapper({ + fnName: 'angledLineThatIntersects', + angleVal: createLiteral(angle), + offsetVal: + forceValueUsedInTransform || createLiteral(valueUsedInTransform), + intersectTag: createLiteral(referenceSegName), + tag, + valueUsedInTransform, + }) + } + +const setAngledIntersectForAngledLines: TransformInfo['createNode'] = + ({ referenceSegName, tag, forceValueUsedInTransform, varValA }) => + (args) => { + const valueUsedInTransform = roundOff( + args[1].type === 'Literal' ? Number(args[1].value) : 0, + 2 + ) + // const angle = args[0].type === 'Literal' ? Number(args[0].value) : 0 + return intersectCallWrapper({ + fnName: 'angledLineThatIntersects', + angleVal: varValA, + offsetVal: + forceValueUsedInTransform || createLiteral(valueUsedInTransform), + intersectTag: createLiteral(referenceSegName), + tag, + valueUsedInTransform, + }) + } + const transformMap: TransformMap = { line: { xRelative: { @@ -404,6 +476,10 @@ const transformMap: TransformMap = { tooltip: 'angledLine', createNode: basicAngledLineCreateNode('ang'), }, + intersect: { + tooltip: 'angledLineThatIntersects', + createNode: setAngledIntersectLineForLines, + }, }, }, lineTo: { @@ -524,6 +600,10 @@ const transformMap: TransformMap = { tooltip: 'angledLineToX', createNode: setHorzVertDistanceForAngleLineCreateNode('x'), }, + intersect: { + tooltip: 'angledLineThatIntersects', + createNode: setAngledIntersectForAngledLines, + }, }, free: { equalLength: { @@ -798,6 +878,10 @@ const transformMap: TransformMap = { tooltip: 'xLine', createNode: xyLineSetLength('xLine'), }, + intersect: { + tooltip: 'angledLineThatIntersects', + createNode: setAngledIntersectLineForLines, + }, }, }, yLine: { @@ -817,6 +901,10 @@ const transformMap: TransformMap = { tooltip: 'yLineTo', createNode: setHorVertDistanceForXYLines('y'), }, + intersect: { + tooltip: 'angledLineThatIntersects', + createNode: setAngledIntersectLineForLines, + }, }, }, xLineTo: { diff --git a/src/lang/std/std.ts b/src/lang/std/std.ts index e1f81a6b8..54de308a1 100644 --- a/src/lang/std/std.ts +++ b/src/lang/std/std.ts @@ -12,7 +12,7 @@ import { angledLineToY, closee, startSketchAt, - getCoordsFromPaths, + angledLineThatIntersects, } from './sketch' import { segLen, @@ -29,7 +29,6 @@ import { Quaternion, Vector3 } from 'three' import { SketchGroup, ExtrudeGroup, Position, Rotation } from '../executor' import { InternalFn, InternalFnNames, InternalFirstArg } from './stdTypes' -import { intersectionWithParallelLine } from 'sketch-helpers' const transform: InternalFn = ( { sourceRange }: InternalFirstArg, @@ -82,38 +81,6 @@ const translate: InternalFn = ( } } -const angledLineThatIntersects: InternalFn = ( - { sourceRange, programMemory }, - data: { - angle: number - intersectTag: string - offset?: number - tag?: string - }, - previousSketch: SketchGroup -) => { - if (!previousSketch) throw new Error('lineTo must be called after lineTo') - const intersectPath = previousSketch.value.find( - ({ name }) => name === data.intersectTag - ) - if (!intersectPath) throw new Error('intersectTag must match a line') - const from = getCoordsFromPaths( - previousSketch, - previousSketch.value.length - 1 - ) - const to = intersectionWithParallelLine({ - line1: [intersectPath.from, intersectPath.to], - line1Offset: data.offset || 0, - line2Point: from, - line2Angle: data.angle, - }) - return lineTo.fn( - { sourceRange, programMemory }, - { to, tag: data.tag }, - previousSketch - ) -} - const min: InternalFn = (_, a: number, b: number): number => Math.min(a, b) const legLen: InternalFn = (_, hypotenuse: number, leg: number): number => @@ -158,7 +125,7 @@ export const internalFns: { [key in InternalFnNames]: InternalFn } = { angledLineToX: angledLineToX.fn, angledLineOfYLength: angledLineOfYLength.fn, angledLineToY: angledLineToY.fn, - angledLineThatIntersects, + angledLineThatIntersects: angledLineThatIntersects.fn, startSketchAt, closee, } diff --git a/src/useStore.ts b/src/useStore.ts index 4ba4d3cdb..f23a6fe6c 100644 --- a/src/useStore.ts +++ b/src/useStore.ts @@ -22,6 +22,7 @@ export type TooTip = | 'yLine' | 'xLineTo' | 'yLineTo' + | 'angledLineThatIntersects' export const toolTips: TooTip[] = [ 'lineTo', @@ -35,6 +36,7 @@ export const toolTips: TooTip[] = [ 'yLine', 'xLineTo', 'yLineTo', + 'angledLineThatIntersects', ] export type GuiModes = diff --git a/yarn.lock b/yarn.lock index 91d298667..e4b55808f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9360,10 +9360,10 @@ sisteransi@^1.0.4, sisteransi@^1.0.5: resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed" integrity sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg== -sketch-helpers@^0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/sketch-helpers/-/sketch-helpers-0.0.1.tgz#637ead1f6e39276408d2c2e2a48dfefe13dc0cb0" - integrity sha512-ePn4nTA5sVNR6+8JalyCPQ+K7tpuYtCrccw2QGL6H2N3JRq6bO8x9RmZpyjTe/+T0uSrd2+F41d+ibsrjHHSFg== +sketch-helpers@^0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/sketch-helpers/-/sketch-helpers-0.0.2.tgz#7b088303a82d3b7008abfe4e20a476c6da20cc0e" + integrity sha512-3VnjIlqg3ORxcIR9vATazvvQt6/4vzPymcorQ30vigjjZEXPHf4xT6Zh7pbZmR58RJBKEU1AtAANhqYar4CRFQ== slash@^3.0.0: version "3.0.0"