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"