functional sketch working (#26)

* functional sketch working

With old sketch block still there

* get all version of lines working with add line and update line

* remove old ui state types

* some clean up

* rename some things

* add todo for multi cursor

* shorten useStore repitition

* small type improvement

* big overhaul to group sketch function and they ast modifying helpers together

* unneeded tweak

* ruthlessly rip out sketch logic

* clean up path keyword

* getting sketch on face working again with all the new sketch line types

* add a bunch of tests and re-arrage file structure
This commit is contained in:
Kurt Hutten
2023-02-12 10:56:45 +11:00
committed by GitHub
parent 3404529743
commit 594d55576a
28 changed files with 2592 additions and 1475 deletions

View File

@ -14,3 +14,4 @@ jobs:
node-version: '18.x'
- run: yarn install
- run: yarn test:nowatch
- run: yarn test:cov

View File

@ -29,8 +29,9 @@
"build": "react-scripts build",
"test": "react-scripts test",
"test:nowatch": "react-scripts test --watchAll=false",
"test:cov": "react-scripts test --watchAll=false --coverage=true",
"eject": "react-scripts eject",
"fmt": "prettier --write ./src/**.{ts,tsx} && prettier --write ./src/**/*.{ts,tsx}"
"fmt": "prettier --write ./src/**.{ts,tsx} && prettier --write ./src/**/*.{ts,tsx} && prettier --write ./src/lang/**/*.{ts,tsx}"
},
"jest": {
"transformIgnorePatterns": [

View File

@ -77,6 +77,8 @@ function App() {
setEditorView(viewUpdate.view)
}
const range = viewUpdate.state.selection.ranges[0]
// console.log(viewUpdate.state.selection.ranges)
// TODO allow multiple cursors so that we can do constrain style features
const isNoChange =
range.from === selectionRange[0] && range.to === selectionRange[1]
if (isNoChange) return

View File

@ -1,17 +1,18 @@
import { useStore } from './useStore'
import { useStore, toolTips } from './useStore'
import { extrudeSketch, sketchOnExtrudedFace } from './lang/modifyAst'
import { getNodePathFromSourceRange } from './lang/abstractSyntaxTree'
export const Toolbar = () => {
const { setGuiMode, guiMode, selectionRange, ast, updateAst } = useStore(
({ guiMode, setGuiMode, selectionRange, ast, updateAst }) => ({
guiMode,
setGuiMode,
selectionRange,
ast,
updateAst,
})
)
const { setGuiMode, guiMode, selectionRange, ast, updateAst, programMemory } =
useStore((s) => ({
guiMode: s.guiMode,
setGuiMode: s.setGuiMode,
selectionRange: s.selectionRange,
ast: s.ast,
updateAst: s.updateAst,
programMemory: s.programMemory,
}))
return (
<div>
{guiMode.mode === 'default' && (
@ -24,7 +25,7 @@ export const Toolbar = () => {
}}
className="border m-1 px-1 rounded"
>
Start sketch
Start Sketch
</button>
)}
{guiMode.mode === 'canEditExtrude' && (
@ -32,7 +33,11 @@ export const Toolbar = () => {
onClick={() => {
if (!ast) return
const pathToNode = getNodePathFromSourceRange(ast, selectionRange)
const { modifiedAst } = sketchOnExtrudedFace(ast, pathToNode)
const { modifiedAst } = sketchOnExtrudedFace(
ast,
pathToNode,
programMemory
)
updateAst(modifiedAst)
}}
className="border m-1 px-1 rounded"
@ -41,7 +46,7 @@ export const Toolbar = () => {
</button>
)}
{(guiMode.mode === 'canEditSketch' || false) && (
/*guiMode.mode === 'canEditExtrude'*/ <button
<button
onClick={() => {
setGuiMode({
mode: 'sketch',
@ -49,11 +54,12 @@ export const Toolbar = () => {
pathToNode: guiMode.pathToNode,
rotation: guiMode.rotation,
position: guiMode.position,
isTooltip: true,
})
}}
className="border m-1 px-1 rounded"
>
EditSketch
Edit Sketch
</button>
)}
{guiMode.mode === 'canEditSketch' && (
@ -98,24 +104,29 @@ export const Toolbar = () => {
Exit sketch
</button>
)}
{guiMode.mode === 'sketch' &&
(guiMode.sketchMode === 'points' ||
guiMode.sketchMode === 'sketchEdit') && (
{toolTips.map((sketchFnName) => {
if (guiMode.mode !== 'sketch' || !('isTooltip' in guiMode)) return null
return (
<button
key={sketchFnName}
className={`border m-1 px-1 rounded ${
guiMode.sketchMode === 'points' && 'bg-gray-400'
guiMode.sketchMode === sketchFnName && 'bg-gray-400'
}`}
onClick={() =>
setGuiMode({
...guiMode,
sketchMode:
guiMode.sketchMode === 'points' ? 'sketchEdit' : 'points',
guiMode.sketchMode === sketchFnName
? 'sketchEdit'
: sketchFnName,
})
}
>
LineTo{guiMode.sketchMode === 'points' && '✅'}
{sketchFnName}
{guiMode.sketchMode === sketchFnName && '✅'}
</button>
)}
)
})}
</div>
)
}

View File

@ -69,6 +69,7 @@ export const BasePlanes = () => {
rotation: quaternion.toArray() as [number, number, number, number],
position: [0, 0, 0],
pathToNode,
isTooltip: true,
})
updateAst(modifiedAst)

View File

@ -11,17 +11,15 @@ const myFn = (a) => {
}
const otherVar = myFn(5)
sketch theExtrude {
lineTo(-2.4, myVar)
lineTo(-0.76, otherVar)
}
const theExtrude = startSketchAt([0, 0])
|> lineTo([-2.4, myVar], %)
|> lineTo([-0.76, otherVar], %)
|> extrude(4, %)
sketch theSketch {
lineTo(-3.35, 0.17)
lineTo(0.98, 5.16)
lineTo(2.15, 4.32)
}
const theSketch = startSketchAt([0, 0])
|> lineTo([-3.35, 0.17], %)
|> lineTo([0.98, 5.16], %)
|> lineTo([2.15, 4.32], %)
|> rx(90, %)
show(theExtrude, theSketch)`
const tokens = lexer(code)

View File

@ -3,8 +3,9 @@ import {
getNodePathFromSourceRange,
getNodeFromPath,
CallExpression,
ArrayExpression,
} from '../lang/abstractSyntaxTree'
import { changeArguments } from '../lang/modifyAst'
import { changeSketchArguments } from '../lang/std/sketch'
import {
ExtrudeGroup,
ExtrudeSurface,
@ -17,14 +18,10 @@ import {
} from '../lang/executor'
import { BufferGeometry } from 'three'
import { useStore } from '../useStore'
import { isOverlapping } from '../lib/utils'
import { isOverlap } from '../lib/utils'
import { Vector3, DoubleSide, Quaternion } from 'three'
import { useSetCursor } from '../hooks/useSetCursor'
const roundOff = (num: number, places: number): number => {
const x = Math.pow(10, places)
return Math.round(num * x) / x
}
import { roundOff } from '../lib/utils'
function MovingSphere({
geo,
@ -32,12 +29,14 @@ function MovingSphere({
editorCursor,
rotation,
position,
from,
}: {
geo: BufferGeometry
sourceRange: [number, number]
editorCursor: boolean
rotation: Rotation
position: Position
from: [number, number]
}) {
const ref = useRef<BufferGeometry | undefined>() as any
const detectionPlaneRef = useRef<BufferGeometry | undefined>() as any
@ -46,11 +45,13 @@ function MovingSphere({
const [hovered, setHover] = useState(false)
const [isMouseDown, setIsMouseDown] = useState(false)
const { setHighlightRange, guiMode, ast, updateAst } = useStore((s) => ({
const { setHighlightRange, guiMode, ast, updateAst, programMemory } =
useStore((s) => ({
setHighlightRange: s.setHighlightRange,
guiMode: s.guiMode,
ast: s.ast,
updateAst: s.updateAst,
programMemory: s.programMemory,
}))
const { originalXY } = useMemo(() => {
if (ast) {
@ -59,7 +60,10 @@ function MovingSphere({
ast,
thePath
)
const [xArg, yArg] = callExpression?.arguments || []
const [xArg, yArg] =
guiMode.mode === 'sketch'
? callExpression?.arguments || []
: (callExpression?.arguments?.[0] as ArrayExpression)?.elements || []
const x = xArg?.type === 'Literal' ? xArg.value : -1
const y = yArg?.type === 'Literal' ? yArg.value : -1
return {
@ -87,7 +91,15 @@ function MovingSphere({
yo.sub(new Vector3(...position).applyQuaternion(inverseQuaternion))
let [x, y] = [roundOff(yo.x, 2), roundOff(yo.y, 2)]
let theNewPoints: [number, number] = [x, y]
const { modifiedAst } = changeArguments(ast, thePath, theNewPoints)
const { modifiedAst } = changeSketchArguments(
ast,
programMemory,
sourceRange,
theNewPoints,
guiMode,
from
)
updateAst(modifiedAst)
ref.current.position.set(...position)
}
@ -285,7 +297,7 @@ function WallRender({
const [editorCursor, setEditorCursor] = useState(false)
useEffect(() => {
const shouldHighlight = isOverlapping(
const shouldHighlight = isOverlap(
geoInfo.__geoMeta.sourceRange,
selectionRange
)
@ -340,7 +352,7 @@ function PathRender({
}))
const [editorCursor, setEditorCursor] = useState(false)
useEffect(() => {
const shouldHighlight = isOverlapping(
const shouldHighlight = isOverlap(
geoInfo.__geoMeta.sourceRange,
selectionRange
)
@ -366,6 +378,7 @@ function PathRender({
<MovingSphere
key={i}
geo={meta.geo}
from={geoInfo.from}
sourceRange={geoInfo.__geoMeta.sourceRange}
editorCursor={forceHighlight || editorCursor}
rotation={rotation}
@ -424,9 +437,9 @@ function LineRender({
)
}
type Boop = ExtrudeGroup | SketchGroup
type Artifact = ExtrudeGroup | SketchGroup
function useSetAppModeFromCursorLocation(artifacts: Boop[]) {
function useSetAppModeFromCursorLocation(artifacts: Artifact[]) {
const { selectionRange, guiMode, setGuiMode, ast } = useStore(
({ selectionRange, guiMode, setGuiMode, ast }) => ({
selectionRange,
@ -438,7 +451,7 @@ function useSetAppModeFromCursorLocation(artifacts: Boop[]) {
useEffect(() => {
const artifactsWithinCursorRange: (
| {
parentType: Boop['type']
parentType: Artifact['type']
isParent: true
pathToNode: PathToNode
sourceRange: SourceRange
@ -446,25 +459,29 @@ function useSetAppModeFromCursorLocation(artifacts: Boop[]) {
position: Position
}
| {
parentType: Boop['type']
parentType: Artifact['type']
isParent: false
pathToNode: PathToNode
sourceRange: SourceRange
rotation: Rotation
position: Position
}
)[] = []
artifacts?.forEach((artifact) => {
artifact.value.forEach((geo) => {
if (isOverlapping(geo.__geoMeta.sourceRange, selectionRange)) {
if (isOverlap(geo.__geoMeta.sourceRange, selectionRange)) {
artifactsWithinCursorRange.push({
parentType: artifact.type,
isParent: false,
pathToNode: geo.__geoMeta.pathToNode,
sourceRange: geo.__geoMeta.sourceRange,
rotation: artifact.rotation,
position: artifact.position,
})
}
})
artifact.__meta.forEach((meta) => {
if (isOverlapping(meta.sourceRange, selectionRange)) {
if (isOverlap(meta.sourceRange, selectionRange)) {
artifactsWithinCursorRange.push({
parentType: artifact.type,
isParent: true,
@ -477,35 +494,39 @@ function useSetAppModeFromCursorLocation(artifacts: Boop[]) {
})
})
const parentArtifacts = artifactsWithinCursorRange.filter((a) => a.isParent)
if (parentArtifacts.length > 1) {
console.log('multiple parents, might be an issue?', parentArtifacts)
}
const hasSketchArtifact = artifactsWithinCursorRange.filter(
({ parentType }) => parentType === 'sketchGroup'
)
const hasExtrudeArtifact = artifactsWithinCursorRange.filter(
({ parentType }) => parentType === 'extrudeGroup'
)
const artifact = parentArtifacts[0]
const shouldHighlight = !!artifact
const shouldHighlight = !!artifact || hasSketchArtifact.length
if (
shouldHighlight &&
(guiMode.mode === 'default' || guiMode.mode === 'canEditSketch') &&
ast &&
artifact.parentType === 'sketchGroup' &&
artifact.isParent
hasSketchArtifact.length
) {
const pathToNode = getNodePathFromSourceRange(ast, artifact.sourceRange)
const { rotation, position } = artifact
const pathToNode = getNodePathFromSourceRange(
ast,
hasSketchArtifact[0].sourceRange
)
const { rotation, position } = hasSketchArtifact[0]
setGuiMode({ mode: 'canEditSketch', pathToNode, rotation, position })
} else if (
shouldHighlight &&
(guiMode.mode === 'default' || guiMode.mode === 'canEditSketch') &&
ast &&
artifact.parentType === 'extrudeGroup' &&
artifact.isParent
hasExtrudeArtifact.length &&
(guiMode.mode === 'default' || guiMode.mode === 'canEditExtrude') &&
ast
) {
const pathToNode = getNodePathFromSourceRange(ast, artifact.sourceRange)
const { rotation, position } = artifact
const pathToNode = getNodePathFromSourceRange(
ast,
hasExtrudeArtifact[0].sourceRange
)
const { rotation, position } = hasExtrudeArtifact[0]
setGuiMode({ mode: 'canEditExtrude', pathToNode, rotation, position })
} else if (
!shouldHighlight &&
(guiMode.mode === 'canEditSketch' || guiMode.mode === 'canEditExtrude')
// (artifact.parentType === 'extrudeGroup' || artifact.type === 'extrudeGroup')
(guiMode.mode === 'canEditExtrude' || guiMode.mode === 'canEditSketch')
) {
setGuiMode({ mode: 'default' })
}

View File

@ -1,20 +1,20 @@
import { useStore } from '../useStore'
import { DoubleSide, Vector3, Quaternion } from 'three'
import { Program } from '../lang/abstractSyntaxTree'
import { addLine } from '../lang/modifyAst'
import { toolTipModification } from '../lang/std/sketch'
import { roundOff } from '../lib/utils'
export const SketchPlane = () => {
const { ast, guiMode, updateAst } = useStore(
({ guiMode, ast, updateAst }) => ({
guiMode,
ast,
updateAst,
})
)
const { ast, guiMode, updateAst, programMemory } = useStore((s) => ({
guiMode: s.guiMode,
ast: s.ast,
updateAst: s.updateAst,
programMemory: s.programMemory,
}))
if (guiMode.mode !== 'sketch') {
return null
}
if (guiMode.sketchMode !== 'points' && guiMode.sketchMode !== 'sketchEdit') {
if (!(guiMode.sketchMode === 'lineTo') && !('isTooltip' in guiMode)) {
return null
}
@ -39,7 +39,7 @@ export const SketchPlane = () => {
position={position}
name={sketchGridName}
onClick={(e) => {
if (guiMode.sketchMode !== 'points') {
if (!('isTooltip' in guiMode)) {
return
}
const sketchGridIntersection = e.intersections.find(
@ -65,10 +65,11 @@ export const SketchPlane = () => {
nonCodeMeta: {},
}
const addLinePoint: [number, number] = [point.x, point.y]
const { modifiedAst } = addLine(
const { modifiedAst } = toolTipModification(
_ast,
guiMode.pathToNode,
addLinePoint
programMemory,
addLinePoint,
guiMode
)
updateAst(modifiedAst)
}}
@ -91,10 +92,6 @@ export const SketchPlane = () => {
}
function roundy({ x, y, z }: any) {
const roundOff = (num: number, places: number): number => {
const x = Math.pow(10, places)
return Math.round(num * x) / x
}
return {
x: roundOff(x, 2),
y: roundOff(y, 2),

View File

@ -462,198 +462,6 @@ const myVar = funcN(1, 2)`
})
})
describe('structures specific to this lang', () => {
test('sketch', () => {
let code = `sketch mySketch {
path myPath = lineTo(0,1)
lineTo(1,1)
path rightPath = lineTo(1,0)
close()
}
`
const tokens = lexer(code)
const { body } = abstractSyntaxTree(tokens)
delete (body[0] as any).declarations[0].init.body.nonCodeMeta
expect(body).toEqual([
{
type: 'VariableDeclaration',
start: 0,
end: 102,
kind: 'sketch',
declarations: [
{
type: 'VariableDeclarator',
start: 7,
end: 102,
id: {
type: 'Identifier',
start: 7,
end: 15,
name: 'mySketch',
},
init: {
type: 'SketchExpression',
start: 16,
end: 102,
body: {
type: 'BlockStatement',
start: 16,
end: 102,
body: [
{
type: 'VariableDeclaration',
start: 20,
end: 45,
kind: 'path',
declarations: [
{
type: 'VariableDeclarator',
start: 25,
end: 45,
id: {
type: 'Identifier',
start: 25,
end: 31,
name: 'myPath',
},
init: {
type: 'CallExpression',
start: 34,
end: 45,
callee: {
type: 'Identifier',
start: 34,
end: 40,
name: 'lineTo',
},
arguments: [
{
type: 'Literal',
start: 41,
end: 42,
value: 0,
raw: '0',
},
{
type: 'Literal',
start: 43,
end: 44,
value: 1,
raw: '1',
},
],
optional: false,
},
},
],
},
{
type: 'ExpressionStatement',
start: 48,
end: 59,
expression: {
type: 'CallExpression',
start: 48,
end: 59,
callee: {
type: 'Identifier',
start: 48,
end: 54,
name: 'lineTo',
},
arguments: [
{
type: 'Literal',
start: 55,
end: 56,
value: 1,
raw: '1',
},
{
type: 'Literal',
start: 57,
end: 58,
value: 1,
raw: '1',
},
],
optional: false,
},
},
{
type: 'VariableDeclaration',
start: 62,
end: 90,
kind: 'path',
declarations: [
{
type: 'VariableDeclarator',
start: 67,
end: 90,
id: {
type: 'Identifier',
start: 67,
end: 76,
name: 'rightPath',
},
init: {
type: 'CallExpression',
start: 79,
end: 90,
callee: {
type: 'Identifier',
start: 79,
end: 85,
name: 'lineTo',
},
arguments: [
{
type: 'Literal',
start: 86,
end: 87,
value: 1,
raw: '1',
},
{
type: 'Literal',
start: 88,
end: 89,
value: 0,
raw: '0',
},
],
optional: false,
},
},
],
},
{
type: 'ExpressionStatement',
start: 93,
end: 100,
expression: {
type: 'CallExpression',
start: 93,
end: 100,
callee: {
type: 'Identifier',
start: 93,
end: 98,
name: 'close',
},
arguments: [],
optional: false,
},
},
],
},
},
},
],
},
])
})
})
describe('testing hasPipeOperator', () => {
test('hasPipeOperator is true', () => {
let code = `sketch mySketch {
@ -739,186 +547,230 @@ const yo = myFunc(9()
describe('testing pipe operator special', () => {
test('pipe operator with sketch', () => {
let code = `sketch mySketch {
lineTo(2, 3)
path myPath = lineTo(0, 1)
lineTo(1,1)
let code = `const mySketch = startSketchAt([0, 0])
|> lineTo([2, 3], %)
|> lineTo({ to: [0, 1], tag: "myPath" }, %)
|> lineTo([1, 1], %)
} |> rx(45, %)
`
const tokens = lexer(code)
const { body } = abstractSyntaxTree(tokens)
delete (body[0] as any).declarations[0].init.nonCodeMeta
delete (body[0] as any).declarations[0].init.body[0].body.nonCodeMeta
expect(body).toEqual([
{
type: 'VariableDeclaration',
start: 0,
end: 90,
kind: 'sketch',
end: 145,
kind: 'const',
declarations: [
{
type: 'VariableDeclarator',
start: 7,
end: 90,
id: {
type: 'Identifier',
start: 7,
end: 15,
name: 'mySketch',
},
start: 6,
end: 145,
id: { type: 'Identifier', start: 6, end: 14, name: 'mySketch' },
init: {
type: 'PipeExpression',
start: 16,
end: 90,
start: 15,
end: 145,
body: [
{
type: 'SketchExpression',
start: 16,
end: 77,
body: {
type: 'BlockStatement',
start: 16,
end: 77,
body: [
{
type: 'ExpressionStatement',
start: 20,
end: 32,
expression: {
type: 'CallExpression',
start: 20,
end: 32,
start: 17,
end: 38,
callee: {
type: 'Identifier',
start: 20,
end: 26,
name: 'lineTo',
start: 17,
end: 30,
name: 'startSketchAt',
},
arguments: [
{
type: 'Literal',
start: 27,
end: 28,
value: 2,
raw: '2',
},
type: 'ArrayExpression',
start: 31,
end: 37,
elements: [
{
type: 'Literal',
start: 30,
end: 31,
value: 3,
raw: '3',
},
],
optional: false,
},
},
{
type: 'VariableDeclaration',
start: 35,
end: 61,
kind: 'path',
declarations: [
{
type: 'VariableDeclarator',
start: 40,
end: 61,
id: {
type: 'Identifier',
start: 40,
end: 46,
name: 'myPath',
},
init: {
type: 'CallExpression',
start: 49,
end: 61,
callee: {
type: 'Identifier',
start: 49,
end: 55,
name: 'lineTo',
},
arguments: [
{
type: 'Literal',
start: 56,
end: 57,
start: 32,
end: 33,
value: 0,
raw: '0',
},
{
type: 'Literal',
start: 59,
end: 60,
value: 1,
raw: '1',
start: 35,
end: 36,
value: 0,
raw: '0',
},
],
},
],
optional: false,
},
},
],
},
{
type: 'ExpressionStatement',
start: 64,
end: 75,
expression: {
type: 'CallExpression',
start: 64,
end: 75,
start: 44,
end: 61,
callee: {
type: 'Identifier',
start: 64,
end: 70,
start: 44,
end: 50,
name: 'lineTo',
},
arguments: [
{
type: 'ArrayExpression',
start: 51,
end: 57,
elements: [
{
type: 'Literal',
start: 71,
end: 72,
value: 1,
raw: '1',
start: 52,
end: 53,
value: 2,
raw: '2',
},
{
type: 'Literal',
start: 73,
end: 74,
value: 1,
raw: '1',
start: 55,
end: 56,
value: 3,
raw: '3',
},
],
},
{ type: 'PipeSubstitution', start: 59, end: 60 },
],
optional: false,
},
{
type: 'CallExpression',
start: 67,
end: 107,
callee: {
type: 'Identifier',
start: 67,
end: 73,
name: 'lineTo',
},
arguments: [
{
type: 'ObjectExpression',
start: 74,
end: 103,
properties: [
{
type: 'ObjectProperty',
start: 76,
end: 86,
key: {
type: 'Identifier',
start: 76,
end: 78,
name: 'to',
},
value: {
type: 'ArrayExpression',
start: 80,
end: 86,
elements: [
{
type: 'Literal',
start: 81,
end: 82,
value: 0,
raw: '0',
},
{
type: 'Literal',
start: 84,
end: 85,
value: 1,
raw: '1',
},
],
},
},
{
type: 'ObjectProperty',
start: 88,
end: 101,
key: {
type: 'Identifier',
start: 88,
end: 91,
name: 'tag',
},
value: {
type: 'Literal',
start: 93,
end: 101,
value: 'myPath',
raw: '"myPath"',
},
},
],
},
{ type: 'PipeSubstitution', start: 105, end: 106 },
],
optional: false,
},
{
type: 'CallExpression',
start: 81,
end: 90,
start: 113,
end: 130,
callee: {
type: 'Identifier',
start: 81,
end: 83,
start: 113,
end: 119,
name: 'lineTo',
},
arguments: [
{
type: 'ArrayExpression',
start: 120,
end: 126,
elements: [
{
type: 'Literal',
start: 121,
end: 122,
value: 1,
raw: '1',
},
{
type: 'Literal',
start: 124,
end: 125,
value: 1,
raw: '1',
},
],
},
{ type: 'PipeSubstitution', start: 128, end: 129 },
],
optional: false,
},
{
type: 'CallExpression',
start: 136,
end: 145,
callee: {
type: 'Identifier',
start: 136,
end: 138,
name: 'rx',
},
arguments: [
{
type: 'Literal',
start: 84,
end: 86,
start: 139,
end: 141,
value: 45,
raw: '45',
},
{
type: 'PipeSubstitution',
start: 88,
end: 89,
},
{ type: 'PipeSubstitution', start: 143, end: 144 },
],
optional: false,
},
@ -1841,47 +1693,42 @@ const key = 'c'`
expect(nonCodeMeta2[0].start).not.toBe(nonCodeMetaInstance.start)
})
it('comments nested within a block statement', () => {
const code = `sketch mySketch {
path myPath = lineTo(0,1)
lineTo(1,1) /* this is
const code = `const mySketch = startSketchAt([0,0])
|> lineTo({ to: [0, 1], tag: 'myPath' }, %)
|> lineTo([1, 1], %) /* this is
a comment
spanning a few lines */
path rightPath = lineTo(1,0)
close()
}
`
|> lineTo({ to: [1,0], tag: "rightPath" }, %)
|> close(%)
`
const { body } = abstractSyntaxTree(lexer(code))
const indexOfSecondLineToExpression = 1 // 0 index so `path myPath = lineTo(0,1)` is 0
const sketchNonCodeMeta = (body as any)[0].declarations[0].init.body
.nonCodeMeta
const indexOfSecondLineToExpression = 2
const sketchNonCodeMeta = (body as any)[0].declarations[0].init.nonCodeMeta
expect(sketchNonCodeMeta[indexOfSecondLineToExpression]).toEqual({
type: 'NoneCodeNode',
start: 67,
end: 133,
start: 106,
end: 168,
value:
' /* this is \n a comment \n spanning a few lines */\n ',
})
})
it('comments in a pipe expression', () => {
const code = [
'sketch mySk1 {',
' lineTo(1, 1)',
' path myPath = lineTo(0, 1)',
' lineTo(1, 1)',
'}',
'const mySk1 = startSketchAt([0, 0])',
' |> lineTo([1, 1], %)',
' |> lineTo({to: [0, 1], tag: "myPath"}, %)',
' |> lineTo([1, 1], %)',
'// a comment',
' |> rx(90, %)',
].join('\n')
const { body } = abstractSyntaxTree(lexer(code))
const bing = abstractSyntaxTree(lexer(code))
const sketchNonCodeMeta = (body[0] as any).declarations[0].init.nonCodeMeta
expect(1).toBe(1)
expect(sketchNonCodeMeta[0]).toEqual({
expect(sketchNonCodeMeta[3]).toEqual({
type: 'NoneCodeNode',
start: 75,
end: 91,
start: 125,
end: 141,
value: '\n// a comment\n ',
})
})

View File

@ -17,7 +17,6 @@ type syntaxType =
| 'ObjectExpression'
| 'ObjectProperty'
| 'FunctionExpression'
| 'SketchExpression'
| 'PipeExpression'
| 'PipeSubstitution'
| 'Literal'
@ -321,14 +320,14 @@ function makeArguments(
export interface VariableDeclaration extends GeneralStatement {
type: 'VariableDeclaration'
declarations: VariableDeclarator[]
kind: 'const' | 'unknown' | 'fn' | 'sketch' | 'path' //| "solid" | "surface" | "face"
kind: 'const' | 'unknown' | 'fn' //| "solid" | "surface" | "face"
}
function makeVariableDeclaration(
tokens: Token[],
index: number
): { declaration: VariableDeclaration; lastIndex: number } {
// token index should point to a declaration keyword i.e. const, fn, sketch, path
// token index should point to a declaration keyword i.e. const, fn
const currentToken = tokens[index]
const declarationStartToken = nextMeaningfulToken(tokens, index)
const { declarations, lastIndex } = makeVariableDeclarators(
@ -345,10 +344,6 @@ function makeVariableDeclaration(
? 'const'
: currentToken.value === 'fn'
? 'fn'
: currentToken.value === 'sketch'
? 'sketch'
: currentToken.value === 'path'
? 'path'
: 'unknown',
declarations,
},
@ -362,7 +357,6 @@ export type Value =
| BinaryExpression
| FunctionExpression
| CallExpression
| SketchExpression
| PipeExpression
| PipeSubstitution
| ArrayExpression
@ -486,13 +480,6 @@ function makeVariableDeclarators(
)
init = expression
lastIndex = pipeLastIndex
} else if (
declarationToken.token.type === 'word' &&
declarationToken.token.value === 'sketch'
) {
const sketchExp = makeSketchExpression(tokens, assignmentToken.index)
init = sketchExp.expression
lastIndex = sketchExp.lastIndex
} else {
const { value, lastIndex: valueLastIndex } = makeValue(
tokens,
@ -544,7 +531,7 @@ function makeIdentifier(token: Token[], index: number): Identifier {
}
}
interface PipeSubstitution extends GeneralStatement {
export interface PipeSubstitution extends GeneralStatement {
type: 'PipeSubstitution'
}
@ -841,30 +828,6 @@ function makeBinaryExpression(
}
}
export interface SketchExpression extends GeneralStatement {
type: 'SketchExpression'
body: BlockStatement
}
function makeSketchExpression(
tokens: Token[],
index: number
): { expression: SketchExpression; lastIndex: number } {
const currentToken = tokens[index]
const { block, lastIndex: bodyLastIndex } = makeBlockStatement(tokens, index)
const endToken = tokens[bodyLastIndex]
return {
expression: {
type: 'SketchExpression',
start: currentToken.start,
end: endToken.end,
body: block,
},
lastIndex: bodyLastIndex,
}
}
export interface PipeExpression extends GeneralStatement {
type: 'PipeExpression'
body: Value[]
@ -909,10 +872,6 @@ function makePipeBody(
const val = makeValue(tokens, expressionStart.index)
value = val.value
lastIndex = val.lastIndex
} else if (currentToken.type === 'brace' && currentToken.value === '{') {
const sketch = makeSketchExpression(tokens, index)
value = sketch.expression
lastIndex = sketch.lastIndex
} else {
throw new Error('Expected a previous PipeValue if statement to match')
}
@ -1125,10 +1084,7 @@ function makeBody(
if (
token.type === 'word' &&
(token.value === 'const' ||
token.value === 'fn' ||
token.value === 'sketch' ||
token.value === 'path')
(token.value === 'const' || token.value === 'fn')
) {
const { declaration, lastIndex } = makeVariableDeclaration(
tokens,
@ -1218,10 +1174,7 @@ export function findNextDeclarationKeyword(
}
if (
nextToken.token.type === 'word' &&
(nextToken.token.value === 'const' ||
nextToken.token.value === 'fn' ||
nextToken.token.value === 'sketch' ||
nextToken.token.value === 'path')
(nextToken.token.value === 'const' || nextToken.token.value === 'fn')
) {
return nextToken
}
@ -1287,7 +1240,7 @@ export function hasPipeOperator(
_limitIndex = -1
): ReturnType<typeof nextMeaningfulToken> | false {
// this probably still needs some work
// should be called on expression statuments (i.e "lineTo" for lineTo(10, 10)) or "{" for sketch declarations
// should be called on expression statuments (i.e "lineTo" for lineTo(10, 10))
let limitIndex = _limitIndex
if (limitIndex === -1) {
const callExpressionEnd = isCallExpression(tokens, index)
@ -1584,34 +1537,13 @@ export function getNodePathFromSourceRange(
const init = declaration.init
if (init.start <= start && init.end >= end) {
path.push('init')
if (init.type === 'SketchExpression') {
const body = init.body
if (body.start <= start && body.end >= end) {
path.push('body')
if (body.type === 'BlockStatement') {
path = getNodePathFromSourceRange(body, sourceRange, path)
}
}
} else if (init.type === 'PipeExpression') {
if (init.type === 'PipeExpression') {
const body = init.body
for (let pipeIndex = 0; pipeIndex < body.length; pipeIndex++) {
const pipe = body[pipeIndex]
if (pipe.start <= start && pipe.end >= end) {
path.push('body')
path.push(pipeIndex)
if (pipe.type === 'SketchExpression') {
const body = pipe.body
if (body.start <= start && body.end >= end) {
path.push('body')
if (body.type === 'BlockStatement') {
path = getNodePathFromSourceRange(
body,
sourceRange,
path
)
}
}
}
}
}
} else if (init.type === 'CallExpression') {

View File

@ -5,10 +5,9 @@ import { executor, SketchGroup, ExtrudeGroup } from './executor'
describe('testing artifacts', () => {
test('sketch artifacts', () => {
const code = `
sketch mySketch001 {
lineTo(-1.59, -1.54)
lineTo(0.46, -5.82)
}
const mySketch001 = startSketchAt([0, 0])
|> lineTo([-1.59, -1.54], %)
|> lineTo([0.46, -5.82], %)
|> rx(45, %)
show(mySketch001)`
const programMemory = executor(abstractSyntaxTree(lexer(code)))
@ -19,13 +18,14 @@ show(mySketch001)`
expect(artifactsWithoutGeos).toEqual([
{
type: 'sketchGroup',
start: [0, 0],
value: [
{
type: 'toPoint',
to: [-1.59, -1.54],
from: [0, 0],
__geoMeta: {
sourceRange: [24, 44],
sourceRange: [48, 73],
pathToNode: [],
geos: ['line', 'lineEnd'],
},
@ -35,7 +35,7 @@ show(mySketch001)`
to: [0.46, -5.82],
from: [-1.59, -1.54],
__geoMeta: {
sourceRange: [47, 66],
sourceRange: [79, 103],
pathToNode: [],
geos: ['line', 'lineEnd'],
},
@ -44,24 +44,17 @@ show(mySketch001)`
position: [0, 0, 0],
rotation: [0.3826834323650898, 0, 0, 0.9238795325112867],
__meta: [
{
sourceRange: [20, 68],
pathToNode: ['body', 0, 'declarations', 0, 'init', 0],
},
{
sourceRange: [74, 83],
pathToNode: [],
},
{ sourceRange: [21, 42], pathToNode: [] },
{ sourceRange: [109, 118], pathToNode: [] },
],
},
])
})
test('extrude artifacts', () => {
const code = `
sketch mySketch001 {
lineTo(-1.59, -1.54)
lineTo(0.46, -5.82)
}
const mySketch001 = startSketchAt([0, 0])
|> lineTo([-1.59, -1.54], %)
|> lineTo([0.46, -5.82], %)
|> rx(45, %)
|> extrude(2, %)
show(mySketch001)`
@ -83,7 +76,7 @@ show(mySketch001)`
],
__geoMeta: {
geo: 'PlaneGeometry',
sourceRange: [24, 44],
sourceRange: [48, 73],
pathToNode: [],
},
},
@ -98,7 +91,7 @@ show(mySketch001)`
],
__geoMeta: {
geo: 'PlaneGeometry',
sourceRange: [47, 66],
sourceRange: [79, 103],
pathToNode: [],
},
},
@ -107,35 +100,27 @@ show(mySketch001)`
position: [0, 0, 0],
rotation: [0.3826834323650898, 0, 0, 0.9238795325112867],
__meta: [
{
sourceRange: [89, 102],
pathToNode: [],
},
{
sourceRange: [20, 68],
pathToNode: ['body', 0, 'declarations', 0, 'init', 0],
},
{ sourceRange: [124, 137], pathToNode: [] },
{ sourceRange: [21, 42], pathToNode: [] },
],
},
])
})
test('sketch extrude and sketch on one of the faces', () => {
const code = `
sketch sk1 {
lineTo(-2.5, 0)
path p = lineTo(0, 10)
lineTo(2.5, 0)
}
const sk1 = startSketchAt([0, 0])
|> lineTo([-2.5, 0], %)
|> lineTo({ to: [0, 10], tag: "p" }, %)
|> lineTo([2.5, 0], %)
|> rx(45, %)
|> translate([1,0,1], %)
|> ry(5, %)
const theExtrude = extrude(2, sk1)
const theTransf = getExtrudeWallTransform('p', theExtrude)
sketch sk2 {
lineTo(-2.5, 0)
path p = lineTo(0, 3)
lineTo(2.5, 0)
}
const sk2 = startSketchAt([0, 0])
|> lineTo([-2.5, 0], %)
|> lineTo({ to: [0, 3], tag: "p" }, %)
|> lineTo([2.5, 0], %)
|> transform(theTransf, %)
|> extrude(2, %)
@ -159,7 +144,7 @@ show(theExtrude, sk2)`
],
__geoMeta: {
geo: 'PlaneGeometry',
sourceRange: [16, 31],
sourceRange: [40, 60],
pathToNode: [],
},
},
@ -174,7 +159,7 @@ show(theExtrude, sk2)`
],
__geoMeta: {
geo: 'PlaneGeometry',
sourceRange: [39, 56],
sourceRange: [66, 102],
pathToNode: [],
},
name: 'p',
@ -190,7 +175,7 @@ show(theExtrude, sk2)`
],
__geoMeta: {
geo: 'PlaneGeometry',
sourceRange: [59, 73],
sourceRange: [108, 127],
pathToNode: [],
},
},
@ -202,14 +187,8 @@ show(theExtrude, sk2)`
0.9230002039112792,
],
__meta: [
{
sourceRange: [138, 166],
pathToNode: [],
},
{
sourceRange: [12, 75],
pathToNode: ['body', 0, 'declarations', 0, 'init', 0],
},
{ sourceRange: [190, 218], pathToNode: [] },
{ sourceRange: [13, 34], pathToNode: [] },
],
},
{
@ -226,7 +205,7 @@ show(theExtrude, sk2)`
],
__geoMeta: {
geo: 'PlaneGeometry',
sourceRange: [241, 256],
sourceRange: [317, 337],
pathToNode: [],
},
},
@ -241,7 +220,7 @@ show(theExtrude, sk2)`
],
__geoMeta: {
geo: 'PlaneGeometry',
sourceRange: [264, 280],
sourceRange: [343, 378],
pathToNode: [],
},
name: 'p',
@ -257,7 +236,7 @@ show(theExtrude, sk2)`
],
__geoMeta: {
geo: 'PlaneGeometry',
sourceRange: [283, 297],
sourceRange: [384, 403],
pathToNode: [],
},
},
@ -269,14 +248,8 @@ show(theExtrude, sk2)`
-0.5362616571538269,
],
__meta: [
{
sourceRange: [334, 347],
pathToNode: [],
},
{
sourceRange: [237, 299],
pathToNode: ['body', 3, 'declarations', 0, 'init', 0],
},
{ sourceRange: [438, 451], pathToNode: [] },
{ sourceRange: [290, 311], pathToNode: [] },
],
},
])

View File

@ -62,12 +62,11 @@ log(5, myVar)`
expect(root.magicNum.value).toBe(69)
})
it('sketch declaration', () => {
let code = `sketch mySketch {
path myPath = lineTo(0,2)
lineTo(2,3)
path rightPath = lineTo(5,-1)
close()
}
let code = `const mySketch = startSketchAt([0,0])
|> lineTo({to: [0,2], tag: "myPath"}, %)
|> lineTo([2,3], %)
|> lineTo({ to: [5,-1], tag: "rightPath" }, %)
// |> close(%)
show(mySketch)
`
const { root, return: _return } = exe(code)
@ -77,9 +76,9 @@ show(mySketch)
{
type: 'toPoint',
to: [0, 2],
from: [5, -1],
from: [0, 0],
__geoMeta: {
sourceRange: [25, 45],
sourceRange: [43, 80],
pathToNode: [],
geos: ['line', 'lineEnd'],
},
@ -90,7 +89,7 @@ show(mySketch)
to: [2, 3],
from: [0, 2],
__geoMeta: {
sourceRange: [48, 59],
sourceRange: [86, 102],
pathToNode: [],
geos: ['line', 'lineEnd'],
},
@ -100,29 +99,19 @@ show(mySketch)
to: [5, -1],
from: [2, 3],
__geoMeta: {
sourceRange: [67, 91],
sourceRange: [108, 151],
pathToNode: [],
geos: ['line', 'lineEnd'],
},
name: 'rightPath',
},
{
type: 'toPoint',
from: [5, -1],
to: [0, 2],
__geoMeta: {
sourceRange: [94, 101],
pathToNode: [],
geos: ['line', 'lineEnd'],
},
},
])
// expect(root.mySketch.sketch[0]).toEqual(root.mySketch.sketch[4].firstPath)
expect(_return).toEqual([
{
type: 'Identifier',
start: 109,
end: 117,
start: 174,
end: 182,
name: 'mySketch',
},
])
@ -139,13 +128,11 @@ show(mySketch)
it('rotated sketch', () => {
const code = [
'sketch mySk1 {',
' lineTo(1,1)',
' path myPath = lineTo(0, 1)',
' lineTo(1,1)',
'}',
'const mySk1 = startSketchAt([0,0])',
' |> lineTo([1,1], %)',
' |> lineTo({to: [0, 1], tag: "myPath"}, %)',
' |> lineTo([1, 1], %)',
'const rotated = rx(90, mySk1)',
// 'show(mySk1)',
].join('\n')
const { root } = exe(code)
expect(root.mySk1.value).toHaveLength(3)
@ -166,23 +153,24 @@ show(mySketch)
it('execute pipe sketch into call expression', () => {
const code = [
'sketch mySk1 {',
' lineTo(1,1)',
' path myPath = lineTo(0, 1)',
' lineTo(1,1)',
'} |> rx(90, %)',
'const mySk1 = startSketchAt([0,0])',
' |> lineTo([1,1], %)',
' |> lineTo({to: [0, 1], tag: "myPath"}, %)',
' |> lineTo([1,1], %)',
' |> rx(90, %)',
].join('\n')
const { root } = exe(code)
const striptVersion = removeGeoFromSketch(root.mySk1 as SketchGroup)
expect(striptVersion).toEqual({
type: 'sketchGroup',
start: [0, 0],
value: [
{
type: 'toPoint',
to: [1, 1],
from: [0, 0],
__geoMeta: {
sourceRange: [17, 28],
sourceRange: [40, 56],
pathToNode: [],
geos: ['line', 'lineEnd'],
},
@ -192,7 +180,7 @@ show(mySketch)
to: [0, 1],
from: [1, 1],
__geoMeta: {
sourceRange: [36, 57],
sourceRange: [62, 100],
pathToNode: [],
geos: ['line', 'lineEnd'],
},
@ -203,7 +191,7 @@ show(mySketch)
to: [1, 1],
from: [0, 1],
__geoMeta: {
sourceRange: [60, 71],
sourceRange: [106, 122],
pathToNode: [],
geos: ['line', 'lineEnd'],
},
@ -212,14 +200,8 @@ show(mySketch)
position: [0, 0, 0],
rotation: [0.7071067811865475, 0, 0, 0.7071067811865476],
__meta: [
{
sourceRange: [13, 73],
pathToNode: ['body', 0, 'declarations', 0, 'init', 0],
},
{
sourceRange: [77, 86],
pathToNode: [],
},
{ sourceRange: [14, 34], pathToNode: [] },
{ sourceRange: [128, 137], pathToNode: [] },
],
})
})

View File

@ -7,13 +7,10 @@ import {
MemberExpression,
Identifier,
CallExpression,
ArrayExpression,
} from './abstractSyntaxTree'
import {
sketchFns,
internalFns,
InternalFnNames,
SketchFnNames,
} from './sketch'
import { InternalFnNames } from './std/stdTypes'
import { internalFns } from './std/std'
import { BufferGeometry } from 'three'
export type SourceRange = [number, number]
@ -68,6 +65,7 @@ export type Path = ToPoint | HorizontalLineTo | AngledLineTo
export interface SketchGroup {
type: 'sketchGroup'
value: Path[]
start?: Path['from']
position: Position
rotation: Rotation
__meta: Metadata[]
@ -220,25 +218,6 @@ export const executor = (
value: executeObjectExpression(_programMemory, declaration.init),
__meta,
}
} else if (declaration.init.type === 'SketchExpression') {
const sketchInit = declaration.init
const fnMemory: ProgramMemory = {
root: {
..._programMemory.root,
},
_sketch: [],
}
let { _sketch } = executor(sketchInit.body, fnMemory, {
bodyType: 'sketch',
})
const newSketch: SketchGroup = {
type: 'sketchGroup',
value: _sketch || [],
position: [0, 0, 0],
rotation: [0, 0, 0, 1], //x,y,z,w
__meta,
}
_programMemory.root[variableName] = newSketch
} else if (declaration.init.type === 'FunctionExpression') {
const fnInit = declaration.init
@ -287,33 +266,14 @@ export const executor = (
return _programMemory.root[arg.name].value
} else if (arg.type === 'ObjectExpression') {
return executeObjectExpression(_programMemory, arg)
} else if (arg.type === 'ArrayExpression') {
return executeArrayExpression(_programMemory, arg)
}
throw new Error(
`Unexpected argument type ${arg.type} in function call`
)
})
if (functionName in sketchFns) {
const sketchFnName = functionName as SketchFnNames
if (options.bodyType !== 'sketch') {
throw new Error(
`Cannot call ${functionName} outside of a sketch declaration`
)
}
const result = sketchFns[sketchFnName](
{
programMemory: _programMemory,
name: variableName,
sourceRange: [declaration.start, declaration.end],
},
...fnArgs
)
_programMemory._sketch = result.programMemory._sketch
_programMemory.root[variableName] = {
type: 'userVal',
value: result.currentPath,
__meta,
}
} else if (functionName in internalFns) {
if (functionName in internalFns) {
const result = executeCallExpression(
_programMemory,
declaration.init,
@ -362,22 +322,7 @@ export const executor = (
return _programMemory.root[arg.name].value
}
})
if (functionName in sketchFns) {
if (options.bodyType !== 'sketch') {
throw new Error(
`Cannot call ${functionName} outside of a sketch declaration`
)
}
const sketchFnName = functionName as SketchFnNames
const result = sketchFns[sketchFnName](
{
programMemory: _programMemory,
sourceRange: [statement.start, statement.end],
},
...args
)
_programMemory._sketch = [...(result.programMemory._sketch || [])]
} else if ('show' === functionName) {
if ('show' === functionName) {
if (options.bodyType !== 'root') {
throw new Error(`Cannot call ${functionName} outside of a root`)
}
@ -482,36 +427,6 @@ function executePipeBody(
body,
}
)
} else if (expression.type === 'SketchExpression') {
const sketchBody = expression.body
const fnMemory: ProgramMemory = {
root: {
...programMemory.root,
},
_sketch: [],
}
let { _sketch } = executor(sketchBody, fnMemory, {
bodyType: 'sketch',
})
const newSketch: SketchGroup = {
type: 'sketchGroup',
value: _sketch || [],
position: [0, 0, 0],
rotation: [0, 0, 0, 1], //x,y,z,w
__meta: [
{
sourceRange: [expression.start, expression.end],
pathToNode: [...previousPathToNode, expressionIndex],
},
],
}
return executePipeBody(
body,
programMemory,
previousPathToNode,
expressionIndex + 1,
[...previousResults, newSketch]
)
}
throw new Error('Invalid pipe expression')
@ -544,18 +459,8 @@ function executeObjectExpression(
property.value
)
} else if (property.value.type === 'ArrayExpression') {
obj[property.key.name] = property.value.elements.map((el) => {
if (el.type === 'Literal') {
return el.value
} else if (el.type === 'Identifier') {
return _programMemory.root[el.name].value
} else if (el.type === 'BinaryExpression') {
return getBinaryExpressionResult(el, _programMemory)
} else if (el.type === 'ObjectExpression') {
return executeObjectExpression(_programMemory, el)
}
throw new Error('Invalid argument type')
})
const result = executeArrayExpression(_programMemory, property.value)
obj[property.key.name] = result
} else {
throw new Error(
`Unexpected property type ${property.value.type} in object expression`
@ -570,6 +475,24 @@ function executeObjectExpression(
return obj
}
function executeArrayExpression(
_programMemory: ProgramMemory,
arrExp: ArrayExpression
) {
return arrExp.elements.map((el) => {
if (el.type === 'Literal') {
return el.value
} else if (el.type === 'Identifier') {
return _programMemory.root?.[el.name]?.value
} else if (el.type === 'BinaryExpression') {
return getBinaryExpressionResult(el, _programMemory)
} else if (el.type === 'ObjectExpression') {
return executeObjectExpression(_programMemory, el)
}
throw new Error('Invalid argument type')
})
}
function executeCallExpression(
programMemory: ProgramMemory,
expression: CallExpression,
@ -604,16 +527,7 @@ function executeCallExpression(
} else if (arg.type === 'PipeSubstitution') {
return previousResults[expressionIndex - 1]
} else if (arg.type === 'ArrayExpression') {
return arg.elements.map((el) => {
if (el.type === 'Literal') {
return el.value
} else if (el.type === 'Identifier') {
return programMemory.root[el.name]
} else if (el.type === 'BinaryExpression') {
return getBinaryExpressionResult(el, programMemory)
}
throw new Error('Invalid argument type')
})
return executeArrayExpression(programMemory, arg)
} else if (arg.type === 'CallExpression') {
const result: any = executeCallExpression(
programMemory,
@ -621,21 +535,12 @@ function executeCallExpression(
previousPathToNode
)
return result
} else if (arg.type === 'ObjectExpression') {
return executeObjectExpression(programMemory, arg)
}
throw new Error('Invalid argument type')
throw new Error('Invalid argument type in function call')
})
if (
functionName in internalFns &&
[
'rx',
'ry',
'rz',
'translate',
'transform',
'extrude',
'getExtrudeWallTransform',
].includes(functionName)
) {
if (functionName in internalFns) {
const fnNameWithSketchOrExtrude = functionName as InternalFnNames
const result = internalFns[fnNameWithSketchOrExtrude](
{
@ -655,25 +560,6 @@ function executeCallExpression(
)
: result
}
if (functionName in sketchFns) {
const sketchFnName = functionName as SketchFnNames
const result = sketchFns[sketchFnName](
{
programMemory,
sourceRange: sourceRangeOverride || [expression.start, expression.end],
},
...fnArgs
)
return isInPipe
? executePipeBody(
body,
programMemory,
previousPathToNode,
expressionIndex + 1,
[...previousResults, result]
)
: result
}
const result = programMemory.root[functionName].value(...fnArgs)
return isInPipe
? executePipeBody(

View File

@ -5,14 +5,13 @@ import { abstractSyntaxTree, getNodeFromPath } from './abstractSyntaxTree'
describe('testing getNodePathFromSourceRange', () => {
it('test it gets the right path for a `lineTo` CallExpression within a SketchExpression', () => {
const code = `
const myVar = 5
sketch sk3 {
lineTo(1, 2)
path yo = lineTo(3, 4)
close()
}
`
const subStr = 'lineTo(3, 4)'
const myVar = 5
const sk3 = startSketchAt([0, 0])
|> lineTo([1, 2], %)
|> lineTo({ to: [3, 4], tag: 'yo' }, %)
|> close(%)
`
const subStr = "lineTo({ to: [3, 4], tag: 'yo' }, %)"
const lineToSubstringIndex = code.indexOf(subStr)
const sourceRange: [number, number] = [
lineToSubstringIndex,

113
src/lang/modifyAst.test.ts Normal file
View File

@ -0,0 +1,113 @@
import {
createLiteral,
createIdentifier,
createCallExpression,
createObjectExpression,
createArrayExpression,
createPipeSubstitution,
createVariableDeclaration,
createPipeExpression,
findUniqueName,
addSketchTo,
} from './modifyAst'
import { recast } from './recast'
describe('Testing createLiteral', () => {
it('should create a literal', () => {
const result = createLiteral(5)
expect(result.type).toBe('Literal')
expect(result.value).toBe(5)
})
})
describe('Testing createIdentifier', () => {
it('should create an identifier', () => {
const result = createIdentifier('myVar')
expect(result.type).toBe('Identifier')
expect(result.name).toBe('myVar')
})
})
describe('Testing createCallExpression', () => {
it('should create a call expression', () => {
const result = createCallExpression('myFunc', [createLiteral(5)])
expect(result.type).toBe('CallExpression')
expect(result.callee.type).toBe('Identifier')
expect(result.callee.name).toBe('myFunc')
expect(result.arguments[0].type).toBe('Literal')
expect((result.arguments[0] as any).value).toBe(5)
})
})
describe('Testing createObjectExpression', () => {
it('should create an object expression', () => {
const result = createObjectExpression({
myProp: createLiteral(5),
})
expect(result.type).toBe('ObjectExpression')
expect(result.properties[0].type).toBe('ObjectProperty')
expect(result.properties[0].key.name).toBe('myProp')
expect(result.properties[0].value.type).toBe('Literal')
expect((result.properties[0].value as any).value).toBe(5)
})
})
describe('Testing createArrayExpression', () => {
it('should create an array expression', () => {
const result = createArrayExpression([createLiteral(5)])
expect(result.type).toBe('ArrayExpression')
expect(result.elements[0].type).toBe('Literal')
expect((result.elements[0] as any).value).toBe(5)
})
})
describe('Testing createPipeSubstitution', () => {
it('should create a pipe substitution', () => {
const result = createPipeSubstitution()
expect(result.type).toBe('PipeSubstitution')
})
})
describe('Testing createVariableDeclaration', () => {
it('should create a variable declaration', () => {
const result = createVariableDeclaration('myVar', createLiteral(5))
expect(result.type).toBe('VariableDeclaration')
expect(result.declarations[0].type).toBe('VariableDeclarator')
expect(result.declarations[0].id.type).toBe('Identifier')
expect(result.declarations[0].id.name).toBe('myVar')
expect(result.declarations[0].init.type).toBe('Literal')
expect((result.declarations[0].init as any).value).toBe(5)
})
})
describe('Testing createPipeExpression', () => {
it('should create a pipe expression', () => {
const result = createPipeExpression([createLiteral(5)])
expect(result.type).toBe('PipeExpression')
expect(result.body[0].type).toBe('Literal')
expect((result.body[0] as any).value).toBe(5)
})
})
describe('Testing findUniqueName', () => {
it('should find a unique name', () => {
const result = findUniqueName(
'yo01 yo02 yo03 yo04 yo05 yo06 yo07 yo08 yo09',
'yo',
2
)
expect(result).toBe('yo10')
})
})
describe('Testing addSketchTo', () => {
it('should add a sketch to a program', () => {
const result = addSketchTo(
{
type: 'Program',
body: [],
start: 0,
end: 0,
nonCodeMeta: {},
},
'yz'
)
const str = recast(result.modifiedAst)
expect(str).toBe(`const part001 = startSketchAt([0, 0])
|> ry(90, %)
|> lineTo([1, 1], %)
show(part001)`)
})
})

View File

@ -1,16 +1,20 @@
import {
Program,
BlockStatement,
SketchExpression,
CallExpression,
PipeExpression,
VariableDeclaration,
VariableDeclarator,
ExpressionStatement,
Value,
getNodeFromPath,
VariableDeclarator,
Literal,
PipeSubstitution,
Identifier,
ArrayExpression,
ObjectExpression,
} from './abstractSyntaxTree'
import { PathToNode } from './executor'
import { PathToNode, ProgramMemory } from './executor'
import { addTagForSketchOnFace } from './std/sketch'
export function addSketchTo(
node: Program,
@ -20,73 +24,37 @@ export function addSketchTo(
const _node = { ...node }
const dumbyStartend = { start: 0, end: 0 }
const _name = name || findUniqueName(node, 'part')
const sketchBody: BlockStatement = {
type: 'BlockStatement',
...dumbyStartend,
body: [],
nonCodeMeta: {},
}
const sketch: SketchExpression = {
type: 'SketchExpression',
...dumbyStartend,
body: sketchBody,
}
const rotate: CallExpression = {
type: 'CallExpression',
...dumbyStartend,
callee: {
type: 'Identifier',
...dumbyStartend,
name: axis === 'xz' ? 'rx' : 'ry',
},
arguments: [
{
type: 'Literal',
...dumbyStartend,
value: axis === 'yz' ? 90 : 90,
raw: axis === 'yz' ? '90' : '90',
},
{
type: 'PipeSubstitution',
...dumbyStartend,
},
],
optional: false,
}
const startSketchAt = createCallExpression('startSketchAt', [
createArrayExpression([createLiteral(0), createLiteral(0)]),
])
const rotate = createCallExpression(axis === 'xz' ? 'rx' : 'ry', [
createLiteral(90),
createPipeSubstitution(),
])
const initialLineTo = createCallExpression('lineTo', [
createArrayExpression([createLiteral(1), createLiteral(1)]),
createPipeSubstitution(),
])
const pipChain: PipeExpression = {
type: 'PipeExpression',
nonCodeMeta: {},
...dumbyStartend,
body: [sketch, rotate],
}
const pipeBody =
axis !== 'xy'
? [startSketchAt, rotate, initialLineTo]
: [startSketchAt, initialLineTo]
const variableDeclaration = createVariableDeclaration(
_name,
createPipeExpression(pipeBody)
)
const sketchVariableDeclaration: VariableDeclaration = {
type: 'VariableDeclaration',
...dumbyStartend,
kind: 'sketch',
declarations: [
{
type: 'VariableDeclarator',
...dumbyStartend,
id: {
type: 'Identifier',
...dumbyStartend,
name: _name,
},
init: axis === 'xy' ? sketch : pipChain,
},
],
}
const showCallIndex = getShowIndex(_node)
let sketchIndex = showCallIndex
if (showCallIndex === -1) {
_node.body = [...node.body, sketchVariableDeclaration]
_node.body = [...node.body, variableDeclaration]
sketchIndex = _node.body.length - 1
} else {
const newBody = [...node.body]
newBody.splice(showCallIndex, 0, sketchVariableDeclaration)
newBody.splice(showCallIndex, 0, variableDeclaration)
_node.body = newBody
}
let pathToNode: (string | number)[] = [
@ -107,7 +75,7 @@ export function addSketchTo(
}
}
function findUniqueName(
export function findUniqueName(
ast: Program | string,
name: string,
pad = 3,
@ -197,93 +165,58 @@ function getShowIndex(node: Program): number {
)
}
export function addLine(
node: Program,
pathToNode: (string | number)[],
to: [number, number]
): { modifiedAst: Program; pathToNode: (string | number)[] } {
const _node = { ...node }
const dumbyStartend = { start: 0, end: 0 }
const { node: sketchExpression } = getNodeFromPath<SketchExpression>(
_node,
pathToNode,
'SketchExpression'
)
const line: ExpressionStatement = {
type: 'ExpressionStatement',
...dumbyStartend,
expression: {
type: 'CallExpression',
...dumbyStartend,
callee: {
type: 'Identifier',
...dumbyStartend,
name: 'lineTo',
},
optional: false,
arguments: [
{
type: 'Literal',
...dumbyStartend,
value: to[0],
raw: `${to[0]}`,
},
{
type: 'Literal',
...dumbyStartend,
value: to[1],
raw: `${to[1]}`,
},
],
},
export function mutateArrExp(
node: Value,
updateWith: ArrayExpression
): boolean {
if (node.type === 'ArrayExpression') {
node.elements.forEach((element, i) => {
if (element.type === 'Literal') {
node.elements[i] = updateWith.elements[i]
}
const newBody = [...sketchExpression.body.body, line]
sketchExpression.body.body = newBody
return {
modifiedAst: _node,
pathToNode,
})
return true
}
return false
}
export function changeArguments(
node: Program,
pathToNode: (string | number)[],
args: [number, number]
): { modifiedAst: Program; pathToNode: (string | number)[] } {
const _node = { ...node }
const dumbyStartend = { start: 0, end: 0 }
// const thePath = getNodePathFromSourceRange(_node, sourceRange)
const { node: callExpression } = getNodeFromPath<CallExpression>(
_node,
pathToNode
)
const newXArg: CallExpression['arguments'][number] =
callExpression.arguments[0].type === 'Literal'
? {
type: 'Literal',
...dumbyStartend,
value: args[0],
raw: `${args[0]}`,
export function mutateObjExpProp(
node: Value,
updateWith: Literal | ArrayExpression,
key: string
): boolean {
if (node.type === 'ObjectExpression') {
const keyIndex = node.properties.findIndex((a) => a.key.name === key)
if (keyIndex !== -1) {
if (
updateWith.type === 'Literal' &&
node.properties[keyIndex].value.type === 'Literal'
) {
node.properties[keyIndex].value = updateWith
return true
} else if (
node.properties[keyIndex].value.type === 'ArrayExpression' &&
updateWith.type === 'ArrayExpression'
) {
const arrExp = node.properties[keyIndex].value as ArrayExpression
arrExp.elements.forEach((element, i) => {
if (element.type === 'Literal') {
arrExp.elements[i] = updateWith.elements[i]
}
: {
...callExpression.arguments[0],
})
}
const newYArg: CallExpression['arguments'][number] =
callExpression.arguments[1].type === 'Literal'
? {
type: 'Literal',
...dumbyStartend,
value: args[1],
raw: `${args[1]}`,
return true
} else {
node.properties.push({
type: 'ObjectProperty',
key: createIdentifier(key),
value: updateWith,
start: 0,
end: 0,
})
}
: {
...callExpression.arguments[1],
}
callExpression.arguments = [newXArg, newYArg]
return {
modifiedAst: _node,
pathToNode,
}
return false
}
export function extrudeSketch(
@ -297,10 +230,10 @@ export function extrudeSketch(
} {
const _node = { ...node }
const dumbyStartend = { start: 0, end: 0 }
const { node: sketchExpression } = getNodeFromPath<SketchExpression>(
const { node: sketchExpression } = getNodeFromPath(
_node,
pathToNode,
'SketchExpression'
'SketchExpression' // TODO fix this #25
)
// determine if sketchExpression is in a pipeExpression or not
@ -324,17 +257,9 @@ export function extrudeSketch(
},
optional: false,
arguments: [
{
type: 'Literal',
...dumbyStartend,
value: 4,
raw: '4',
},
createLiteral(4),
shouldPipe
? {
type: 'PipeSubstitution',
...dumbyStartend,
}
? createPipeSubstitution()
: {
type: 'Identifier',
...dumbyStartend,
@ -354,7 +279,7 @@ export function extrudeSketch(
type: 'PipeExpression',
nonCodeMeta: {},
...dumbyStartend,
body: [sketchExpression, extrudeCall],
body: [sketchExpression as any, extrudeCall], // TODO fix this #25
}
variableDeclorator.init = pipeChain
@ -411,137 +336,61 @@ export function extrudeSketch(
export function sketchOnExtrudedFace(
node: Program,
pathToNode: (string | number)[]
pathToNode: (string | number)[],
programMemory: ProgramMemory
): { modifiedAst: Program; pathToNode: (string | number)[] } {
const _node = { ...node }
const dumbyStartend = { start: 0, end: 0 }
let _node = { ...node }
const newSketchName = findUniqueName(node, 'part')
const oldSketchName = getNodeFromPath<VariableDeclarator>(
const { node: oldSketchNode, path: pathToOldSketch } =
getNodeFromPath<VariableDeclarator>(
_node,
pathToNode,
'VariableDeclarator',
true
).node.id.name
const { node: expression } = getNodeFromPath<
VariableDeclarator | CallExpression
>(_node, pathToNode, 'CallExpression')
const pathName =
expression.type === 'VariableDeclarator'
? expression.id.name
: findUniqueName(node, 'path', 2)
if (expression.type === 'CallExpression') {
const { node: block } = getNodeFromPath<BlockStatement>(
)
const oldSketchName = oldSketchNode.id.name
const { node: expression } = getNodeFromPath<CallExpression>(
_node,
pathToNode,
'BlockStatement'
'CallExpression'
)
const expressionIndex = getLastIndex(pathToNode)
if (expression.callee.name !== 'lineTo')
throw new Error('expected a lineTo call')
const newExpression: VariableDeclaration = {
type: 'VariableDeclaration',
...dumbyStartend,
declarations: [
{
type: 'VariableDeclarator',
...dumbyStartend,
id: {
type: 'Identifier',
...dumbyStartend,
name: pathName,
},
init: expression,
},
],
kind: 'path',
}
block.body.splice(expressionIndex, 1, newExpression)
}
const { modifiedAst, tag } = addTagForSketchOnFace(
{
previousProgramMemory: programMemory,
pathToNode,
node: _node,
},
expression.callee.name
)
_node = modifiedAst
// create pipe expression with a sketch block piped into a transform function
const sketchPipe: PipeExpression = {
type: 'PipeExpression',
nonCodeMeta: {},
...dumbyStartend,
body: [
{
type: 'SketchExpression',
...dumbyStartend,
body: {
type: 'BlockStatement',
...dumbyStartend,
body: [],
nonCodeMeta: {},
},
},
{
type: 'CallExpression',
...dumbyStartend,
callee: {
type: 'Identifier',
...dumbyStartend,
name: 'transform',
},
optional: false,
arguments: [
{
type: 'CallExpression',
...dumbyStartend,
callee: {
type: 'Identifier',
...dumbyStartend,
name: 'getExtrudeWallTransform',
},
optional: false,
arguments: [
{
type: 'Literal',
...dumbyStartend,
value: pathName,
raw: `'${pathName}'`,
},
{
type: 'Identifier',
...dumbyStartend,
name: oldSketchName,
},
],
},
{
type: 'PipeSubstitution',
...dumbyStartend,
},
],
},
],
}
const variableDec: VariableDeclaration = {
type: 'VariableDeclaration',
...dumbyStartend,
declarations: [
{
type: 'VariableDeclarator',
...dumbyStartend,
id: {
type: 'Identifier',
...dumbyStartend,
name: newSketchName,
},
init: sketchPipe,
},
],
kind: 'sketch',
}
const showIndex = getShowIndex(_node)
_node.body.splice(showIndex, 0, variableDec)
const newSketch = createVariableDeclaration(
newSketchName,
createPipeExpression([
createCallExpression('startSketchAt', [
createArrayExpression([createLiteral(0), createLiteral(0)]),
]),
createCallExpression('lineTo', [
createArrayExpression([createLiteral(1), createLiteral(1)]),
createPipeSubstitution(),
]),
createCallExpression('transform', [
createCallExpression('getExtrudeWallTransform', [
createLiteral(tag),
createIdentifier(oldSketchName),
]),
createPipeSubstitution(),
]),
]),
'const'
)
const expressionIndex = getLastIndex(pathToOldSketch)
_node.body.splice(expressionIndex + 1, 0, newSketch)
return {
modifiedAst: addToShow(_node, newSketchName),
pathToNode,
pathToNode: [...pathToNode.slice(0, -1), expressionIndex],
}
}
@ -552,3 +401,111 @@ const getLastIndex = (pathToNode: PathToNode): number => {
}
return getLastIndex(pathToNode.slice(0, -1))
}
export function createLiteral(value: string | number): Literal {
return {
type: 'Literal',
start: 0,
end: 0,
value,
raw: `${value}`,
}
}
export function createIdentifier(name: string): Identifier {
return {
type: 'Identifier',
start: 0,
end: 0,
name,
}
}
export function createPipeSubstitution(): PipeSubstitution {
return {
type: 'PipeSubstitution',
start: 0,
end: 0,
}
}
export function createCallExpression(
name: string,
args: CallExpression['arguments']
): CallExpression {
return {
type: 'CallExpression',
start: 0,
end: 0,
callee: {
type: 'Identifier',
start: 0,
end: 0,
name,
},
optional: false,
arguments: args,
}
}
export function createArrayExpression(
elements: ArrayExpression['elements']
): ArrayExpression {
return {
type: 'ArrayExpression',
start: 0,
end: 0,
elements,
}
}
export function createPipeExpression(
body: PipeExpression['body']
): PipeExpression {
return {
type: 'PipeExpression',
start: 0,
end: 0,
body,
nonCodeMeta: {},
}
}
export function createVariableDeclaration(
varName: string,
init: VariableDeclarator['init'],
kind: VariableDeclaration['kind'] = 'const'
): VariableDeclaration {
return {
type: 'VariableDeclaration',
start: 0,
end: 0,
declarations: [
{
type: 'VariableDeclarator',
start: 0,
end: 0,
id: createIdentifier(varName),
init,
},
],
kind,
}
}
export function createObjectExpression(properties: {
[key: string]: Value
}): ObjectExpression {
return {
type: 'ObjectExpression',
start: 0,
end: 0,
properties: Object.entries(properties).map(([key, value]) => ({
type: 'ObjectProperty',
start: 0,
end: 0,
key: createIdentifier(key),
value,
})),
}
}

View File

@ -62,12 +62,12 @@ log(5, myVar)`
expect(recasted).toBe(code)
})
it('sketch declaration', () => {
let code = `sketch mySketch {
path myPath = lineTo(0, 1)
lineTo(1, 1)
path rightPath = lineTo(1, 0)
close()
}
let code = `const mySketch = startSketchAt([0, 0])
|> lineTo({ to: [0, 1], tag: "myPath" }, %)
|> lineTo([1, 1], %)
|> lineTo({ to: [1, 0], tag: "rightPath" }, %)
|> close(%)
show(mySketch)
`
const { ast } = code2ast(code)
@ -76,11 +76,10 @@ show(mySketch)
})
it('sketch piped into callExpression', () => {
const code = [
'sketch mySk1 {',
' lineTo(1, 1)',
' path myPath = lineTo(0, 1)',
' lineTo(1, 1)',
'}',
'const mySk1 = startSketchAt([0, 0])',
' |> lineTo([1, 1], %)',
' |> lineTo({ to: [0, 1], tag: "myTag" }, %)',
' |> lineTo([1, 1], %)',
' |> rx(90, %)',
].join('\n')
const { ast } = code2ast(code)
@ -226,24 +225,6 @@ const myFn = () => {
const key = 'c'
// this is also a comment
}`
const { ast } = code2ast(code)
const recasted = recast(ast)
expect(recasted).toBe(code)
})
it('comments in a sketch block', () => {
const code = `
sketch mySketch { /* comment at start */
// comment at start more
path myPath = lineTo(0, 1) /* comment here with
some whitespace below */
lineTo(1, 1)
/* comment before declaration*/path rightPath = lineTo(1, 0)
close()
// comment at end
}`
const { ast } = code2ast(code)
const recasted = recast(ast)
@ -251,11 +232,10 @@ sketch mySketch { /* comment at start */
})
it('comments in a pipe expression', () => {
const code = [
'sketch mySk1 {',
' lineTo(1, 1)',
' path myPath = lineTo(0, 1)',
' lineTo(1, 1)',
'}',
'const mySk1 = startSketchAt([0, 0])',
' |> lineTo([1, 1], %)',
' |> lineTo({ to: [0, 1], tag: "myTag" }, %)',
' |> lineTo([1, 1], %)',
' // a comment',
' |> rx(90, %)',
].join('\n')
@ -267,14 +247,13 @@ sketch mySketch { /* comment at start */
const code = `
/* comment at start */
sketch mySk1 {
lineTo(1, 1)
const mySk1 = startSketchAt([0, 0])
|> lineTo([1, 1], %)
// comment here
path myPath = lineTo(0, 1)
lineTo(1, 1) /* and
|> lineTo({ to: [0, 1], tag: 'myTag' }, %)
|> lineTo([1, 1], %) /* and
here
*/
}
// a comment between pipe expression statements
|> rx(90, %)
// and another with just white space between others below

View File

@ -7,7 +7,6 @@ import {
CallExpression,
Value,
FunctionExpression,
SketchExpression,
ArrayExpression,
ObjectExpression,
MemberExpression,
@ -36,17 +35,9 @@ export function recast(
} else if (statement.type === 'VariableDeclaration') {
return statement.declarations
.map((declaration) => {
const isSketchOrFirstPipeExpressionIsSketch =
declaration.init.type === 'SketchExpression' ||
(declaration.init.type === 'PipeExpression' &&
declaration.init.body[0].type === 'SketchExpression')
const assignmentString = isSketchOrFirstPipeExpressionIsSketch
? ' '
: ' = '
return `${statement.kind} ${
declaration.id.name
}${assignmentString}${recastValue(declaration.init)}`
return `${statement.kind} ${declaration.id.name} = ${recastValue(
declaration.init
)}`
})
.join('')
} else if (statement.type === 'ReturnStatement') {
@ -193,15 +184,6 @@ function recastFunction(expression: FunctionExpression): string {
.join(', ')}) => {${recast(expression.body, '', '', true)}}`
}
function recastSketchExpression(
expression: SketchExpression,
indentation: string
): string {
return `{${
recast(expression.body, '', indentation + ' ', true) || '\n \n'
}}`
}
function recastMemberExpression(
expression: MemberExpression,
indentation: string
@ -236,8 +218,6 @@ function recastValue(node: Value, indentation = ''): string {
return recastCallExpression(node)
} else if (node.type === 'Identifier') {
return node.name
} else if (node.type === 'SketchExpression') {
return recastSketchExpression(node, indentation)
} else if (node.type === 'PipeExpression') {
return recastPipeExpression(node)
}

View File

@ -1,335 +0,0 @@
import {
ProgramMemory,
Path,
SketchGroup,
ExtrudeGroup,
SourceRange,
ExtrudeSurface,
Position,
Rotation,
} from './executor'
import { lineGeo, extrudeGeo } from './engine'
import { Quaternion, Vector3 } from 'three'
type Coords2d = [number, number]
interface PathReturn {
programMemory: ProgramMemory
currentPath: Path
}
function getCoordsFromPaths(paths: Path[], index = 0): Coords2d {
const currentPath = paths[index]
if (!currentPath) {
return [0, 0]
}
if (currentPath.type === 'horizontalLineTo') {
const pathBefore = getCoordsFromPaths(paths, index - 1)
return [currentPath.x, pathBefore[1]]
} else if (currentPath.type === 'toPoint') {
return [currentPath.to[0], currentPath.to[1]]
}
return [0, 0]
}
interface InternalFirstArg {
programMemory: ProgramMemory
name?: string
sourceRange: SourceRange
}
type SketchFn = (internals: InternalFirstArg, ...args: any[]) => PathReturn
export type SketchFnNames = 'close' | 'lineTo'
type InternalFn = (internals: InternalFirstArg, ...args: any[]) => any
export type InternalFnNames =
| 'extrude'
| 'translate'
| 'transform'
| 'getExtrudeWallTransform'
| 'rx'
| 'ry'
| 'rz'
export const sketchFns: { [key in SketchFnNames]: SketchFn } = {
close: ({ programMemory, name = '', sourceRange }) => {
const firstPath = programMemory?._sketch?.[0] as Path
let from = getCoordsFromPaths(
programMemory?._sketch || [],
(programMemory?._sketch?.length || 1) - 1
)
let to = getCoordsFromPaths(programMemory?._sketch || [], 0)
const geo = lineGeo({ from: [...from, 0], to: [...to, 0] })
const newPath: Path = {
type: 'toPoint',
from,
to,
__geoMeta: {
sourceRange,
pathToNode: [], // TODO
geos: [
{
type: 'line',
geo: geo.line,
},
{
type: 'lineEnd',
geo: geo.tip,
},
],
},
}
if (name) {
newPath.name = name
}
return {
programMemory: {
...programMemory,
_sketch: [
{
...firstPath,
from,
},
...(programMemory?._sketch || []).slice(1),
newPath,
],
},
currentPath: newPath,
}
},
lineTo: ({ programMemory, name = '', sourceRange }, ...args) => {
const [x, y] = args
if (!programMemory._sketch) {
throw new Error('No sketch to draw on')
}
let from = getCoordsFromPaths(
programMemory?._sketch || [],
(programMemory?._sketch?.length || 1) - 1
)
const geo = lineGeo({ from: [...from, 0], to: [x, y, 0] })
const currentPath: Path = {
type: 'toPoint',
to: [x, y],
from,
__geoMeta: {
sourceRange,
pathToNode: [], // TODO
geos: [
{
type: 'line',
geo: geo.line,
},
{
type: 'lineEnd',
geo: geo.tip,
},
],
},
}
if (name) {
currentPath.name = name
}
return {
programMemory: {
...programMemory,
_sketch: [...(programMemory._sketch || []), currentPath],
},
currentPath,
}
},
}
function rotateOnAxis<T extends SketchGroup | ExtrudeGroup>(
axisMultiplier: [number, number, number]
): InternalFn {
return ({ sourceRange }, rotationD: number, sketch: T): T => {
const rotationR = rotationD * (Math.PI / 180)
const rotateVec = new Vector3(...axisMultiplier)
const quaternion = new Quaternion()
quaternion.setFromAxisAngle(rotateVec, rotationR)
const position = new Vector3(...sketch.position)
.applyQuaternion(quaternion)
.toArray()
const existingQuat = new Quaternion(...sketch.rotation)
const rotation = quaternion.multiply(existingQuat).toArray()
return {
...sketch,
rotation,
position,
__meta: [
...sketch.__meta,
{
sourceRange,
pathToNode: [], // TODO
},
],
}
}
}
const extrude: InternalFn = (
{ sourceRange },
length: number,
sketchVal: SketchGroup
): ExtrudeGroup => {
const getSketchGeo = (sketchVal: SketchGroup): SketchGroup => {
return sketchVal
}
const sketch = getSketchGeo(sketchVal)
const { position, rotation } = sketchVal
const extrudeSurfaces: ExtrudeSurface[] = []
const extrusionDirection = clockwiseSign(sketch.value.map((line) => line.to))
sketch.value.map((line, index) => {
if (line.type === 'toPoint') {
let from: [number, number] = line.from
const to = line.to
const {
geo,
position: facePosition,
rotation: faceRotation,
} = extrudeGeo({
from: [from[0], from[1], 0],
to: [to[0], to[1], 0],
length,
extrusionDirection,
})
const groupQuaternion = new Quaternion(...rotation)
const currentWallQuat = new Quaternion(...faceRotation)
const unifiedQuit = new Quaternion().multiplyQuaternions(
currentWallQuat,
groupQuaternion.clone().invert()
)
const facePositionVector = new Vector3(...facePosition)
facePositionVector.applyQuaternion(groupQuaternion.clone())
const unifiedPosition = new Vector3().addVectors(
facePositionVector,
new Vector3(...position)
)
const surface: ExtrudeSurface = {
type: 'extrudePlane',
position: unifiedPosition.toArray() as Position,
rotation: unifiedQuit.toArray() as Rotation,
__geoMeta: {
geo,
sourceRange: line.__geoMeta.sourceRange,
pathToNode: line.__geoMeta.pathToNode,
},
}
line.name && (surface.name = line.name)
extrudeSurfaces.push(surface)
}
})
return {
type: 'extrudeGroup',
value: extrudeSurfaces,
height: length,
position,
rotation,
__meta: [
{
sourceRange,
pathToNode: [], // TODO
},
{
sourceRange: sketchVal.__meta[0].sourceRange,
pathToNode: sketchVal.__meta[0].pathToNode,
},
],
}
}
const translate: InternalFn = <T extends SketchGroup | ExtrudeGroup>(
{ sourceRange }: InternalFirstArg,
vec3: [number, number, number],
sketch: T
): T => {
const oldPosition = new Vector3(...sketch.position)
const newPosition = oldPosition.add(new Vector3(...vec3))
return {
...sketch,
position: newPosition.toArray(),
__meta: [
...sketch.__meta,
{
sourceRange,
pathToNode: [], // TODO
},
],
}
}
const transform: InternalFn = <T extends SketchGroup | ExtrudeGroup>(
{ sourceRange }: InternalFirstArg,
transformInfo: {
position: Position
quaternion: Rotation
},
sketch: T
): T => {
const quaternionToApply = new Quaternion(...transformInfo.quaternion)
const newQuaternion = new Quaternion(...sketch.rotation).multiply(
quaternionToApply.invert()
)
const oldPosition = new Vector3(...sketch.position)
const newPosition = oldPosition
.applyQuaternion(quaternionToApply)
.add(new Vector3(...transformInfo.position))
return {
...sketch,
position: newPosition.toArray(),
rotation: newQuaternion.toArray(),
__meta: [
...sketch.__meta,
{
sourceRange,
pathToNode: [], // TODO
},
],
}
}
const getExtrudeWallTransform: InternalFn = (
_,
pathName: string,
extrudeGroup: ExtrudeGroup
): {
position: Position
quaternion: Rotation
} => {
const path = extrudeGroup.value.find((path) => path.name === pathName)
if (!path) throw new Error(`Could not find path with name ${pathName}`)
return {
position: path.position,
quaternion: path.rotation,
}
}
export const internalFns: { [key in InternalFnNames]: InternalFn } = {
rx: rotateOnAxis([1, 0, 0]),
ry: rotateOnAxis([0, 1, 0]),
rz: rotateOnAxis([0, 0, 1]),
extrude,
translate,
transform,
getExtrudeWallTransform,
}
function clockwiseSign(points: [number, number][]): number {
let sum = 0
for (let i = 0; i < points.length; i++) {
const currentPoint = points[i]
const nextPoint = points[(i + 1) % points.length]
sum += (nextPoint[0] - currentPoint[0]) * (nextPoint[1] + currentPoint[1])
}
return sum >= 0 ? 1 : -1
}

6
src/lang/std/README.md Normal file
View File

@ -0,0 +1,6 @@
The std is as expected, tools that are provided with the language.
For this language that means functions.
However because programatically changing the source code is a first class citizen in this lang, there needs to be helpes for adding and modifying these function calls,
So it makes sense to group some of these together.

101
src/lang/std/extrude.ts Normal file
View File

@ -0,0 +1,101 @@
import { InternalFn } from './stdTypes'
import {
ExtrudeGroup,
ExtrudeSurface,
SketchGroup,
Position,
Rotation,
} from '../executor'
import { Quaternion, Vector3 } from 'three'
import { clockwiseSign } from './std'
import { extrudeGeo } from '../engine'
export const extrude: InternalFn = (
{ sourceRange },
length: number,
sketchVal: SketchGroup
): ExtrudeGroup => {
const getSketchGeo = (sketchVal: SketchGroup): SketchGroup => {
return sketchVal
}
const sketch = getSketchGeo(sketchVal)
const { position, rotation } = sketchVal
const extrudeSurfaces: ExtrudeSurface[] = []
const extrusionDirection = clockwiseSign(sketch.value.map((line) => line.to))
sketch.value.map((line, index) => {
if (line.type === 'toPoint') {
let from: [number, number] = line.from
const to = line.to
const {
geo,
position: facePosition,
rotation: faceRotation,
} = extrudeGeo({
from: [from[0], from[1], 0],
to: [to[0], to[1], 0],
length,
extrusionDirection,
})
const groupQuaternion = new Quaternion(...rotation)
const currentWallQuat = new Quaternion(...faceRotation)
const unifiedQuit = new Quaternion().multiplyQuaternions(
currentWallQuat,
groupQuaternion.clone().invert()
)
const facePositionVector = new Vector3(...facePosition)
facePositionVector.applyQuaternion(groupQuaternion.clone())
const unifiedPosition = new Vector3().addVectors(
facePositionVector,
new Vector3(...position)
)
const surface: ExtrudeSurface = {
type: 'extrudePlane',
position: unifiedPosition.toArray() as Position,
rotation: unifiedQuit.toArray() as Rotation,
__geoMeta: {
geo,
sourceRange: line.__geoMeta.sourceRange,
pathToNode: line.__geoMeta.pathToNode,
},
}
line.name && (surface.name = line.name)
extrudeSurfaces.push(surface)
}
})
return {
type: 'extrudeGroup',
value: extrudeSurfaces,
height: length,
position,
rotation,
__meta: [
{
sourceRange,
pathToNode: [], // TODO
},
{
sourceRange: sketchVal.__meta[0].sourceRange,
pathToNode: sketchVal.__meta[0].pathToNode,
},
],
}
}
export const getExtrudeWallTransform: InternalFn = (
_,
pathName: string,
extrudeGroup: ExtrudeGroup
): {
position: Position
quaternion: Rotation
} => {
const path = extrudeGroup.value.find((path) => path.name === pathName)
if (!path) throw new Error(`Could not find path with name ${pathName}`)
return {
position: path.position,
quaternion: path.rotation,
}
}

190
src/lang/std/sketch.test.ts Normal file
View File

@ -0,0 +1,190 @@
import {
changeSketchArguments,
toolTipModification,
addTagForSketchOnFace,
getYComponent,
getXComponent,
} from './sketch'
import { lexer } from '../tokeniser'
import {
abstractSyntaxTree,
getNodePathFromSourceRange,
} from '../abstractSyntaxTree'
import { recast } from '../recast'
import { executor } from '../executor'
const eachQuad: [number, [number, number]][] = [
[-315, [1, 1]],
[-225, [-1, 1]],
[-135, [-1, -1]],
[-45, [1, -1]],
[45, [1, 1]],
[135, [-1, 1]],
[225, [-1, -1]],
[315, [1, -1]],
[405, [1, 1]],
[495, [-1, 1]],
[585, [-1, -1]],
[675, [1, -1]],
]
describe('testing getYComponent', () => {
it('should return the vertical component of a vector correctly when given angles in each quadrant (and with angles < 0, or > 360)', () => {
const expected: [number, number][] = []
const results: [number, number][] = []
eachQuad.forEach(([angle, expectedResult]) => {
results.push(
getYComponent(angle, 1).map((a) => Math.round(a)) as [number, number]
)
expected.push(expectedResult)
})
expect(results).toEqual(expected)
})
it('return extreme values on the extremes', () => {
let result: [number, number]
result = getYComponent(0, 1)
expect(result[0]).toBe(1)
expect(result[1]).toBe(0)
result = getYComponent(90, 1)
expect(result[0]).toBe(1)
expect(result[1]).toBeGreaterThan(100000)
result = getYComponent(180, 1)
expect(result[0]).toBe(-1)
expect(result[1]).toBeCloseTo(0)
result = getYComponent(270, 1)
expect(result[0]).toBe(-1)
expect(result[1]).toBeLessThan(100000)
})
})
describe('testing getXComponent', () => {
it('should return the horizontal component of a vector correctly when given angles in each quadrant (and with angles < 0, or > 360)', () => {
const expected: [number, number][] = []
const results: [number, number][] = []
eachQuad.forEach(([angle, expectedResult]) => {
results.push(
getXComponent(angle, 1).map((a) => Math.round(a)) as [number, number]
)
expected.push(expectedResult)
})
expect(results).toEqual(expected)
})
it('return extreme values on the extremes', () => {
let result: [number, number]
result = getXComponent(0, 1)
expect(result[0]).toBeGreaterThan(100000)
expect(result[1]).toBe(1)
result = getXComponent(90, 1)
expect(result[0]).toBeCloseTo(0)
expect(result[1]).toBe(1)
result = getXComponent(180, 1)
expect(result[0]).toBeLessThan(100000)
expect(result[1]).toBe(1)
result = getXComponent(270, 1)
expect(result[0]).toBeCloseTo(0)
expect(result[1]).toBe(-1)
})
})
describe('testing changeSketchArguments', () => {
const lineToChange = 'lineTo([-1.59, -1.54], %)'
const lineAfterChange = 'lineTo([2, 3], %)'
test('changeSketchArguments', () => {
const genCode = (line: string) => `
const mySketch001 = startSketchAt([0, 0])
|> ${line}
|> lineTo([0.46, -5.82], %)
|> rx(45, %)
show(mySketch001)`
const code = genCode(lineToChange)
const expectedCode = genCode(lineAfterChange)
const ast = abstractSyntaxTree(lexer(code))
const programMemory = executor(ast)
const sourceStart = code.indexOf(lineToChange)
const { modifiedAst } = changeSketchArguments(
ast,
programMemory,
[sourceStart, sourceStart + lineToChange.length],
[2, 3],
{
mode: 'sketch',
sketchMode: 'sketchEdit',
isTooltip: true,
rotation: [0, 0, 0, 1],
position: [0, 0, 0],
pathToNode: ['body', 0, 'declarations', '0', 'init'],
},
[0, 0]
)
expect(recast(modifiedAst)).toBe(expectedCode)
})
})
describe('testing toolTipModification', () => {
const lineToChange = 'lineTo([-1.59, -1.54], %)'
const lineAfterChange = 'lineTo([2, 3], %)'
test('toolTipModification', () => {
const code = `
const mySketch001 = startSketchAt([0, 0])
|> rx(45, %)
|> lineTo([-1.59, -1.54], %)
|> lineTo([0.46, -5.82], %)
show(mySketch001)`
const ast = abstractSyntaxTree(lexer(code))
const programMemory = executor(ast)
const sourceStart = code.indexOf(lineToChange)
const { modifiedAst } = toolTipModification(ast, programMemory, [2, 3], {
mode: 'sketch',
sketchMode: 'lineTo',
isTooltip: true,
rotation: [0, 0, 0, 1],
position: [0, 0, 0],
pathToNode: ['body', 0, 'declarations', '0', 'init'],
})
const expectedCode = `
const mySketch001 = startSketchAt([0, 0])
|> rx(45, %)
|> lineTo([-1.59, -1.54], %)
|> lineTo([0.46, -5.82], %)
|> lineTo([2, 3], %)
show(mySketch001)`
expect(recast(modifiedAst)).toBe(expectedCode)
})
})
describe('testing addTagForSketchOnFace', () => {
const originalLine = 'lineTo([-1.59, -1.54], %)'
const genCode = (line: string) => `
const mySketch001 = startSketchAt([0, 0])
|> rx(45, %)
|> ${line}
|> lineTo([0.46, -5.82], %)
show(mySketch001)`
const code = genCode(originalLine)
const ast = abstractSyntaxTree(lexer(code))
const programMemory = executor(ast)
const sourceStart = code.indexOf(originalLine)
const sourceRange: [number, number] = [
sourceStart,
sourceStart + originalLine.length,
]
const pathToNode = getNodePathFromSourceRange(ast, sourceRange)
const { modifiedAst } = addTagForSketchOnFace(
{
previousProgramMemory: programMemory,
pathToNode,
node: ast,
},
'lineTo'
)
const expectedCode = genCode(
"lineTo({ to: [-1.59, -1.54], tag: 'seg01' }, %)"
)
expect(recast(modifiedAst)).toBe(expectedCode)
})

1213
src/lang/std/sketch.ts Normal file

File diff suppressed because it is too large Load Diff

134
src/lang/std/std.ts Normal file
View File

@ -0,0 +1,134 @@
import {
lineTo,
xLineTo,
yLineTo,
line,
xLine,
yLine,
angledLine,
angledLineOfXLength,
angledLineToX,
angledLineOfYLength,
angledLineToY,
closee,
startSketchAt,
} from './sketch'
import { extrude, getExtrudeWallTransform } from './extrude'
import { Quaternion, Vector3 } from 'three'
import { SketchGroup, ExtrudeGroup, Position, Rotation } from '../executor'
import { InternalFn, InternalFnNames, InternalFirstArg } from './stdTypes'
const transform: InternalFn = <T extends SketchGroup | ExtrudeGroup>(
{ sourceRange }: InternalFirstArg,
transformInfo: {
position: Position
quaternion: Rotation
},
sketch: T
): T => {
const quaternionToApply = new Quaternion(...transformInfo.quaternion)
const newQuaternion = new Quaternion(...sketch.rotation).multiply(
quaternionToApply.invert()
)
const oldPosition = new Vector3(...sketch.position)
const newPosition = oldPosition
.applyQuaternion(quaternionToApply)
.add(new Vector3(...transformInfo.position))
return {
...sketch,
position: newPosition.toArray(),
rotation: newQuaternion.toArray(),
__meta: [
...sketch.__meta,
{
sourceRange,
pathToNode: [], // TODO
},
],
}
}
const translate: InternalFn = <T extends SketchGroup | ExtrudeGroup>(
{ sourceRange }: InternalFirstArg,
vec3: [number, number, number],
sketch: T
): T => {
const oldPosition = new Vector3(...sketch.position)
const newPosition = oldPosition.add(new Vector3(...vec3))
return {
...sketch,
position: newPosition.toArray(),
__meta: [
...sketch.__meta,
{
sourceRange,
pathToNode: [], // TODO
},
],
}
}
export const internalFns: { [key in InternalFnNames]: InternalFn } = {
rx: rotateOnAxis([1, 0, 0]),
ry: rotateOnAxis([0, 1, 0]),
rz: rotateOnAxis([0, 0, 1]),
extrude,
translate,
transform,
getExtrudeWallTransform,
lineTo: lineTo.fn,
xLineTo: xLineTo.fn,
yLineTo: yLineTo.fn,
line: line.fn,
xLine: xLine.fn,
yLine: yLine.fn,
angledLine: angledLine.fn,
angledLineOfXLength: angledLineOfXLength.fn,
angledLineToX: angledLineToX.fn,
angledLineOfYLength: angledLineOfYLength.fn,
angledLineToY: angledLineToY.fn,
startSketchAt,
closee,
}
function rotateOnAxis<T extends SketchGroup | ExtrudeGroup>(
axisMultiplier: [number, number, number]
): InternalFn {
return ({ sourceRange }, rotationD: number, sketch: T): T => {
const rotationR = rotationD * (Math.PI / 180)
const rotateVec = new Vector3(...axisMultiplier)
const quaternion = new Quaternion()
quaternion.setFromAxisAngle(rotateVec, rotationR)
const position = new Vector3(...sketch.position)
.applyQuaternion(quaternion)
.toArray()
const existingQuat = new Quaternion(...sketch.rotation)
const rotation = quaternion.multiply(existingQuat).toArray()
return {
...sketch,
rotation,
position,
__meta: [
...sketch.__meta,
{
sourceRange,
pathToNode: [], // TODO
},
],
}
}
}
export function clockwiseSign(points: [number, number][]): number {
let sum = 0
for (let i = 0; i < points.length; i++) {
const currentPoint = points[i]
const nextPoint = points[(i + 1) % points.length]
sum += (nextPoint[0] - currentPoint[0]) * (nextPoint[1] + currentPoint[1])
}
return sum >= 0 ? 1 : -1
}

68
src/lang/std/stdTypes.ts Normal file
View File

@ -0,0 +1,68 @@
import { ProgramMemory, Path, SourceRange } from '../executor'
import { Program } from '../abstractSyntaxTree'
export interface InternalFirstArg {
programMemory: ProgramMemory
name?: string
sourceRange: SourceRange
}
export interface PathReturn {
programMemory: ProgramMemory
currentPath: Path
}
export type InternalFn = (internals: InternalFirstArg, ...args: any[]) => any
export type InternalFnNames =
| 'extrude'
| 'translate'
| 'transform'
| 'getExtrudeWallTransform'
| 'rx'
| 'ry'
| 'rz'
| 'lineTo'
| 'yLineTo'
| 'xLineTo'
| 'line'
| 'yLine'
| 'xLine'
| 'angledLine'
| 'angledLineOfXLength'
| 'angledLineToX'
| 'angledLineOfYLength'
| 'angledLineToY'
| 'startSketchAt'
| 'closee'
export interface ModifyAstBase {
node: Program
previousProgramMemory: ProgramMemory
pathToNode: (string | number)[]
}
interface addCall extends ModifyAstBase {
to: [number, number]
}
interface updateArgs extends ModifyAstBase {
from: [number, number]
to: [number, number]
}
export interface SketchLineHelper {
fn: InternalFn
add: (a: addCall) => {
modifiedAst: Program
pathToNode: (string | number)[]
}
updateArgs: (a: updateArgs) => {
modifiedAst: Program
pathToNode: (string | number)[]
}
addTag: (a: ModifyAstBase) => {
modifiedAst: Program
tag: string
}
}

View File

@ -1,8 +1,8 @@
import { isOverlapping } from './utils'
import { isOverlap, roundOff } from './utils'
import { Range } from '../useStore'
describe('testing isOverlapping', () => {
testBothOrders([0, 5], [3, 10])
testBothOrders([0, 3], [3, 10])
testBothOrders([0, 5], [3, 4])
testBothOrders([0, 5], [5, 10])
testBothOrders([0, 5], [6, 10], false)
@ -13,7 +13,22 @@ describe('testing isOverlapping', () => {
function testBothOrders(a: Range, b: Range, result = true) {
it(`test is overlapping ${a} ${b}`, () => {
expect(isOverlapping(a, b)).toBe(result)
expect(isOverlapping(b, a)).toBe(result)
expect(isOverlap(a, b)).toBe(result)
expect(isOverlap(b, a)).toBe(result)
})
}
describe('testing roundOff', () => {
it('defaults to 2 decimal places', () => {
expect(roundOff(1.23456789)).toBe(1.23)
})
it('rounds off to 3 decimal places', () => {
expect(roundOff(1.23456789, 3)).toBe(1.235)
})
it('works with whole numbers', () => {
expect(roundOff(1.23456789, 0)).toBe(1)
})
it('rounds up ok', () => {
expect(roundOff(1.273456789, 1)).toBe(1.3)
})
})

View File

@ -1,7 +1,24 @@
import { Range } from '../useStore'
export const isOverlapping = (a: Range, b: Range) => {
const startingRange = a[0] < b[0] ? a : b
const secondRange = a[0] < b[0] ? b : a
return startingRange[1] >= secondRange[0]
export function isOverlap(a: Range, b: Range) {
const [startingRange, secondRange] = a[0] < b[0] ? [a, b] : [b, a]
const [lastOfFirst, firstOfSecond] = [startingRange[1], secondRange[0]]
return lastOfFirst >= firstOfSecond
}
export function roundOff(num: number, places: number = 2): number {
const x = Math.pow(10, places)
return Math.round(num * x) / x
}
export function getLength(a: [number, number], b: [number, number]): number {
const x = b[0] - a[0]
const y = b[1] - a[1]
return Math.sqrt(x * x + y * y)
}
export function getAngle(a: [number, number], b: [number, number]): number {
const x = b[0] - a[0]
const y = b[1] - a[1]
return ((Math.atan2(y, x) * 180) / Math.PI + 360) % 360
}

View File

@ -10,14 +10,41 @@ import { recast } from './lang/recast'
import { lexer } from './lang/tokeniser'
export type Range = [number, number]
export type TooTip =
| 'lineTo'
| 'line'
| 'angledLine'
| 'angledLineOfXLength'
| 'angledLineOfYLength'
| 'angledLineToX'
| 'angledLineToY'
| 'xLine'
| 'yLine'
| 'xLineTo'
| 'yLineTo'
type GuiModes =
export const toolTips: TooTip[] = [
'lineTo',
'line',
'angledLine',
'angledLineOfXLength',
'angledLineOfYLength',
'angledLineToX',
'angledLineToY',
'xLine',
'yLine',
'xLineTo',
'yLineTo',
]
export type GuiModes =
| {
mode: 'default'
}
| {
mode: 'sketch'
sketchMode: 'points'
sketchMode: TooTip
isTooltip: true
rotation: Rotation
position: Position
id?: string
@ -26,6 +53,7 @@ type GuiModes =
| {
mode: 'sketch'
sketchMode: 'sketchEdit'
isTooltip: true
rotation: Rotation
position: Position
pathToNode: PathToNode