Compare commits

...

5 Commits

Author SHA1 Message Date
50cf30c0d9 Update test snaps 2024-12-16 09:33:46 -06:00
47532e5fc4 Update stdlib docs 2024-12-16 09:33:45 -06:00
0da92411c4 Send the correct ID for pattern target
Both the Lego brick base, the first bump, and the second bump are all ExtrudeGroups. They all share the same ID -- the ID of the original path that formed the base of the brick.

When running a pattern on the lego *bump* (not the lego *base*), the pattern target ID is the extrude group ID. But like I said, that's the same ID as the rest of the entire extrude! So whether you pattern the base or the brick, the result will be the same -- the entire extrude gets patterned.

Solution: send the ID of the original sketch group (either base or bump)
2024-12-16 09:33:43 -06:00
49de3b0ac9 get ready to bump (kcl-lib and friends) world (#4794)
get ready to bump world

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-12-16 18:37:03 +11:00
2b2ed470c1 multi-profile follow up. (#4802)
* multi-profile work

* fix enter sketch on cap

* fix coderef problem for walls and caps

* allow sketch mode entry from circle

* clean up

* update snapshot

* Look at this (photo)Graph *in the voice of Nickelback*

* trigger CI

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu-latest-8-cores)

* add test

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu-latest-8-cores)

* fix how expression index is corrected, to make compatible with offset planes

* another test

* tweak test

* more test tweaks

* break up test to fix it hopfully

* fix onboarding test

* remove bad comment

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-12-16 18:36:48 +11:00
31 changed files with 28223 additions and 65201 deletions

File diff suppressed because it is too large Load Diff

View File

@ -21,6 +21,7 @@ A sketch is a collection of paths.
| `on` |[`SketchSurface`](/docs/kcl/types/SketchSurface)| What the sketch is on (can be a plane or a face). | No |
| `start` |[`BasePath`](/docs/kcl/types/BasePath)| The starting path. | No |
| `tags` |`object`| Tag identifiers that have been declared in this sketch. | No |
| `originalId` |`string`| The original id of the sketch. This stays the same even if the sketch is sketched on face etc. | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| Metadata. | No |

View File

@ -30,6 +30,7 @@ A sketch is a collection of paths.
| `on` |[`SketchSurface`](/docs/kcl/types/SketchSurface)| What the sketch is on (can be a plane or a face). | No |
| `start` |[`BasePath`](/docs/kcl/types/BasePath)| The starting path. | No |
| `tags` |`object`| Tag identifiers that have been declared in this sketch. | No |
| `originalId` |`string`| The original id of the sketch. This stays the same even if the sketch is sketched on face etc. | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| Metadata. | No |

View File

@ -216,7 +216,7 @@ export class SceneFixture {
}
expectPixelColor = async (
colour: [number, number, number],
colour: [number, number, number] | [number, number, number][],
coords: { x: number; y: number },
diff: number
) => {
@ -237,22 +237,36 @@ export class SceneFixture {
}
}
function isColourArray(
colour: [number, number, number] | [number, number, number][]
): colour is [number, number, number][] {
return Array.isArray(colour[0])
}
export async function expectPixelColor(
page: Page,
colour: [number, number, number],
colour: [number, number, number] | [number, number, number][],
coords: { x: number; y: number },
diff: number
) {
let finalValue = colour
await expect
.poll(async () => {
const pixel = (await getPixelRGBs(page)(coords, 1))[0]
if (!pixel) return null
finalValue = pixel
return pixel.every(
(channel, index) => Math.abs(channel - colour[index]) < diff
)
})
.poll(
async () => {
const pixel = (await getPixelRGBs(page)(coords, 1))[0]
if (!pixel) return null
finalValue = pixel
if (!isColourArray(colour)) {
return pixel.every(
(channel, index) => Math.abs(channel - colour[index]) < diff
)
}
return colour.some((c) =>
c.every((channel, index) => Math.abs(pixel[index] - channel) < diff)
)
},
{ timeout: 10_000 }
)
.toBeTruthy()
.catch((cause) => {
throw new Error(

View File

@ -7,6 +7,7 @@ import {
PERSIST_MODELING_CONTEXT,
setup,
tearDown,
TEST_COLORS,
} from './test-utils'
import { uuidv4, roundOff } from 'lib/utils'
@ -1350,7 +1351,7 @@ test2.describe('Sketch mode should be toleratant to syntax errors', () => {
const [objClick] = scene.makeMouseHelpers(600, 250)
const arrowHeadLocation = { x: 604, y: 129 } as const
const arrowHeadWhite: [number, number, number] = [255, 255, 255]
const arrowHeadWhite = TEST_COLORS.WHITE
const backgroundGray: [number, number, number] = [28, 28, 28]
const verifyArrowHeadColor = async (c: [number, number, number]) =>
scene.expectPixelColor(c, arrowHeadLocation, 15)
@ -1993,4 +1994,334 @@ extrude001 = extrude(75, thePart)
)
}
)
test2(
'Can enter sketch on sketch of wall and cap for segment, solid2d, extrude-wall, extrude-cap selections',
async ({ app, scene, toolbar, editor }) => {
// TODO this test should include a test for selecting revolve walls and caps
await app.initialise(`sketch001 = startSketchOn('XZ')
profile001 = startProfileAt([6.71, -3.66], sketch001)
|> line([2.65, 9.02], %, $seg02)
|> line([3.73, -9.36], %, $seg01)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
extrude001 = extrude(20, profile001)
sketch002 = startSketchOn(extrude001, seg01)
profile002 = startProfileAt([0.75, 13.46], sketch002)
|> line([4.52, 3.79], %)
|> line([5.98, -2.81], %)
profile003 = startProfileAt([3.19, 13.3], sketch002)
|> angledLine([0, 6.64], %, $rectangleSegmentA001)
|> angledLine([
segAng(rectangleSegmentA001) - 90,
2.81
], %)
|> angledLine([
segAng(rectangleSegmentA001),
-segLen(rectangleSegmentA001)
], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
profile004 = startProfileAt([3.15, 9.39], sketch002)
|> xLine(6.92, %)
|> line([-7.41, -2.85], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
profile005 = circle({ center = [5.15, 4.34], radius = 1.66 }, sketch002)
profile006 = startProfileAt([9.65, 3.82], sketch002)
|> line([2.38, 5.62], %)
|> line([2.13, -5.57], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
revolve001 = revolve({
angle = 45,
axis = getNextAdjacentEdge(seg01)
}, profile004)
extrude002 = extrude(4, profile006)
sketch003 = startSketchOn('-XZ')
profile007 = startProfileAt([4.8, 7.55], sketch003)
|> line([7.39, 2.58], %)
|> line([7.02, -2.85], %)
profile008 = startProfileAt([5.54, 5.49], sketch003)
|> line([6.34, 2.64], %)
|> line([6.33, -2.96], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
profile009 = startProfileAt([5.23, 1.95], sketch003)
|> line([6.8, 2.17], %)
|> line([7.34, -2.75], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
profile010 = circle({
center = [7.18, -2.11],
radius = 2.67
}, sketch003)
profile011 = startProfileAt([5.07, -6.39], sketch003)
|> angledLine([0, 4.54], %, $rectangleSegmentA002)
|> angledLine([
segAng(rectangleSegmentA002) - 90,
4.17
], %)
|> angledLine([
segAng(rectangleSegmentA002),
-segLen(rectangleSegmentA002)
], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
extrude003 = extrude(2.5, profile011)
revolve002 = revolve({ angle = 45, axis = seg02 }, profile008)
`)
const camPositionForSelectingSketchOnWallProfiles = () =>
scene.moveCameraTo(
{ x: 834, y: -680, z: 534 },
{ x: -54, y: -476, z: 148 }
)
const camPositionForSelectingSketchOnCapProfiles = () =>
scene.moveCameraTo(
{ x: 404, y: 690, z: 38 },
{ x: 16, y: -140, z: -10 }
)
const wallSelectionOptions = [
{
title: 'select wall segment',
selectClick: scene.makeMouseHelpers(598, 211)[0],
},
{
title: 'select wall solid 2d',
selectClick: scene.makeMouseHelpers(677, 236)[0],
},
{
title: 'select wall circle',
selectClick: scene.makeMouseHelpers(811, 247)[0],
},
{
title: 'select wall extrude wall',
selectClick: scene.makeMouseHelpers(793, 136)[0],
},
{
title: 'select wall extrude cap',
selectClick: scene.makeMouseHelpers(836, 103)[0],
},
] as const
const capSelectionOptions = [
{
title: 'select cap segment',
selectClick: scene.makeMouseHelpers(688, 91)[0],
},
{
title: 'select cap solid 2d',
selectClick: scene.makeMouseHelpers(733, 204)[0],
},
// TODO keeps failing
// {
// title: 'select cap circle',
// selectClick: scene.makeMouseHelpers(679, 290)[0],
// },
{
title: 'select cap extrude wall',
selectClick: scene.makeMouseHelpers(649, 402)[0],
},
{
title: 'select cap extrude cap',
selectClick: scene.makeMouseHelpers(693, 408)[0],
},
] as const
const verifyWallProfilesAreDrawn = async () =>
test2.step('verify wall profiles are drawn', async () => {
// open polygon
await scene.expectPixelColor(
TEST_COLORS.WHITE,
{ x: 599, y: 168 },
15
)
// closed polygon
await scene.expectPixelColor(
TEST_COLORS.WHITE,
{ x: 656, y: 171 },
15
)
// revolved profile
await scene.expectPixelColor(
TEST_COLORS.WHITE,
{ x: 655, y: 264 },
15
)
// extruded profile
await scene.expectPixelColor(
TEST_COLORS.WHITE,
{ x: 808, y: 396 },
15
)
// circle
await scene.expectPixelColor(
[
TEST_COLORS.WHITE,
TEST_COLORS.BLUE, // When entering via the circle, it's selected and therefore blue
],
{ x: 742, y: 386 },
15
)
})
const verifyCapProfilesAreDrawn = async () =>
test2.step('verify wall profiles are drawn', async () => {
// open polygon
await scene.expectPixelColor(
TEST_COLORS.WHITE,
// TEST_COLORS.BLUE, // When entering via the circle, it's selected and therefore blue
{ x: 620, y: 58 },
15
)
// revolved profile
await scene.expectPixelColor(
TEST_COLORS.WHITE,
{ x: 641, y: 110 },
15
)
// closed polygon
await scene.expectPixelColor(
TEST_COLORS.WHITE,
{ x: 632, y: 200 },
15
)
// extruded profile
await scene.expectPixelColor(
TEST_COLORS.WHITE,
{ x: 628, y: 410 },
15
)
// circle
await scene.expectPixelColor(
[
TEST_COLORS.WHITE,
TEST_COLORS.BLUE, // When entering via the circle, it's selected and therefore blue
],
{ x: 681, y: 303 },
15
)
})
await test2.step('select wall profiles', async () => {
for (const { title, selectClick } of wallSelectionOptions) {
await test2.step(title, async () => {
await camPositionForSelectingSketchOnWallProfiles()
await selectClick()
await toolbar.editSketch()
await app.page.waitForTimeout(600)
await verifyWallProfilesAreDrawn()
await toolbar.exitSketchBtn.click()
await app.page.waitForTimeout(100)
})
}
})
await test2.step('select cap profiles', async () => {
for (const { title, selectClick } of capSelectionOptions) {
await test2.step(title, async () => {
await camPositionForSelectingSketchOnCapProfiles()
await app.page.waitForTimeout(100)
await selectClick()
await app.page.waitForTimeout(100)
await toolbar.editSketch()
await app.page.waitForTimeout(600)
await verifyCapProfilesAreDrawn()
await toolbar.exitSketchBtn.click()
await app.page.waitForTimeout(100)
})
}
})
}
)
test2(
'Can enter sketch loft edges, base and continue sketch',
async ({ app, scene, toolbar, editor }) => {
await app.initialise(`sketch001 = startSketchOn('XZ')
profile001 = startProfileAt([34, 42.66], sketch001)
|> line([102.65, 151.99], %)
|> line([76, -138.66], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
plane001 = offsetPlane('XZ', 50)
sketch002 = startSketchOn(plane001)
profile002 = startProfileAt([39.43, 172.21], sketch002)
|> xLine(183.99, %)
|> line([-77.95, -145.93], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
loft([profile001, profile002])
`)
const [baseProfileEdgeClick] = scene.makeMouseHelpers(621, 292)
const [rect1Crn1] = scene.makeMouseHelpers(592, 283)
const [rect1Crn2] = scene.makeMouseHelpers(797, 268)
await baseProfileEdgeClick()
await toolbar.editSketch()
await app.page.waitForTimeout(600)
await scene.expectPixelColor(TEST_COLORS.WHITE, { x: 562, y: 172 }, 15)
await toolbar.rectangleBtn.click()
await app.page.waitForTimeout(100)
await rect1Crn1()
await editor.expectEditor.toContain(
`profile003 = startProfileAt([50.72, -18.19], sketch001)`
)
await rect1Crn2()
await editor.expectEditor.toContain(
`angledLine([0, 113.01], %, $rectangleSegmentA001)`
)
}
)
test2(
'Can enter sketch loft edges offsetPlane and continue sketch',
async ({ app, scene, toolbar, editor }) => {
await app.initialise(`sketch001 = startSketchOn('XZ')
profile001 = startProfileAt([34, 42.66], sketch001)
|> line([102.65, 151.99], %)
|> line([76, -138.66], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
plane001 = offsetPlane('XZ', 50)
sketch002 = startSketchOn(plane001)
profile002 = startProfileAt([39.43, 172.21], sketch002)
|> xLine(183.99, %)
|> line([-77.95, -145.93], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
loft([profile001, profile002])
`)
const topProfileEdgeClickCoords = { x: 602, y: 185 } as const
const [topProfileEdgeClick] = scene.makeMouseHelpers(
topProfileEdgeClickCoords.x,
topProfileEdgeClickCoords.y
)
const [rect1Crn1] = scene.makeMouseHelpers(592, 283)
const [rect1Crn2] = scene.makeMouseHelpers(797, 268)
await scene.moveCameraTo(
{ x: 8171, y: -7740, z: 1624 },
{ x: 3302, y: -627, z: 2892 }
)
await topProfileEdgeClick()
await toolbar.editSketch()
await app.page.waitForTimeout(600)
await scene.expectPixelColor(TEST_COLORS.BLUE, { x: 788, y: 188 }, 15)
await toolbar.rectangleBtn.click()
await app.page.waitForTimeout(100)
await rect1Crn1()
await editor.expectEditor.toContain(
`profile003 = startProfileAt([47.76, -17.13], plane001)`
)
await rect1Crn2()
await editor.expectEditor.toContain(
`angledLine([0, 106.42], %, $rectangleSegmentA001)`
)
}
)
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 139 KiB

After

Width:  |  Height:  |  Size: 139 KiB

View File

@ -1776,8 +1776,7 @@ export class SceneEntities {
structuredClone(pathToNode)
nodePathWithCorrectedIndexForTruncatedAst[1][0] =
Number(nodePathWithCorrectedIndexForTruncatedAst[1][0]) -
Number(planeNodePath[1][0]) -
1
Number(sketchNodePaths[0][1][0])
const _node = getNodeFromPath<Node<CallExpression>>(
modifiedAst,

View File

@ -2,7 +2,12 @@ import { SVGProps } from 'react'
export const Spinner = (props: SVGProps<SVGSVGElement>) => {
return (
<svg viewBox="0 0 10 10" className={'w-8 h-8'} {...props}>
<svg
data-testid="spinner"
viewBox="0 0 10 10"
className={'w-8 h-8'}
{...props}
>
<circle
cx="5"
cy="5"

View File

@ -22,6 +22,7 @@ const mySketch001 = startSketchOn('XY')
value: {
type: 'Sketch',
on: expect.any(Object),
originalId: expect.any(String),
start: {
to: [0, 0],
from: [0, 0],
@ -91,6 +92,7 @@ const mySketch001 = startSketchOn('XY')
],
sketch: {
id: expect.any(String),
originalId: expect.any(String),
__meta: expect.any(Array),
on: expect.any(Object),
start: expect.any(Object),
@ -185,6 +187,7 @@ const sk2 = startSketchOn('XY')
],
sketch: {
id: expect.any(String),
originalId: expect.any(String),
__meta: expect.any(Array),
on: expect.any(Object),
start: expect.any(Object),
@ -277,6 +280,7 @@ const sk2 = startSketchOn('XY')
],
sketch: {
id: expect.any(String),
originalId: expect.any(String),
__meta: expect.any(Array),
on: expect.any(Object),
start: expect.any(Object),

View File

@ -160,6 +160,7 @@ const newVar = myVar + 1`
value: {
type: 'Sketch',
on: expect.any(Object),
originalId: expect.any(String),
start: {
to: [0, 0],
from: [0, 0],

View File

@ -5,7 +5,6 @@ import {
PathToNode,
Expr,
CallExpression,
PipeExpression,
VariableDeclarator,
} from 'lang/wasm'
import { Selections } from 'lib/selections'
@ -15,7 +14,6 @@ import {
createCallExpressionStdLib,
createObjectExpression,
createIdentifier,
createPipeExpression,
findUniqueName,
createVariableDeclaration,
} from 'lang/modifyAst'
@ -24,12 +22,13 @@ import {
mutateAstWithTagForSketchSegment,
getEdgeTagCall,
} from 'lang/modifyAst/addEdgeTreatment'
import { Artifact, getPathsFromArtifact } from 'lang/std/artifactGraph'
export function revolveSketch(
ast: Node<Program>,
pathToSketchNode: PathToNode,
shouldPipe = false,
angle: Expr = createLiteral(4),
axis: Selections
axis: Selections,
artifact?: Artifact
):
| {
modifiedAst: Node<Program>
@ -37,6 +36,11 @@ export function revolveSketch(
pathToRevolveArg: PathToNode
}
| Error {
const orderedSketchNodePaths = getPathsFromArtifact({
artifact: artifact,
sketchPathToNode: pathToSketchNode,
})
if (err(orderedSketchNodePaths)) return orderedSketchNodePaths
const clonedAst = structuredClone(ast)
const sketchNode = getNodeFromPath(clonedAst, pathToSketchNode)
if (err(sketchNode)) return sketchNode
@ -67,29 +71,13 @@ export function revolveSketch(
if (err(tagResult)) return tagResult
const { tag } = tagResult
/* Original Code */
const { node: sketchExpression } = sketchNode
// determine if sketchExpression is in a pipeExpression or not
const sketchPipeExpressionNode = getNodeFromPath<PipeExpression>(
clonedAst,
pathToSketchNode,
'PipeExpression'
)
if (err(sketchPipeExpressionNode)) return sketchPipeExpressionNode
const { node: sketchPipeExpression } = sketchPipeExpressionNode
const isInPipeExpression = sketchPipeExpression.type === 'PipeExpression'
const sketchVariableDeclaratorNode = getNodeFromPath<VariableDeclarator>(
clonedAst,
pathToSketchNode,
'VariableDeclarator'
)
if (err(sketchVariableDeclaratorNode)) return sketchVariableDeclaratorNode
const {
node: sketchVariableDeclarator,
shallowPath: sketchPathToDecleration,
} = sketchVariableDeclaratorNode
const { node: sketchVariableDeclarator } = sketchVariableDeclaratorNode
const axisSelection = axis?.graphSelections[0]?.artifact
@ -103,37 +91,13 @@ export function revolveSketch(
createIdentifier(sketchVariableDeclarator.id.name),
])
if (shouldPipe) {
const pipeChain = createPipeExpression(
isInPipeExpression
? [...sketchPipeExpression.body, revolveCall]
: [sketchExpression as any, revolveCall]
)
sketchVariableDeclarator.init = pipeChain
const pathToRevolveArg: PathToNode = [
...sketchPathToDecleration,
['init', 'VariableDeclarator'],
['body', ''],
[pipeChain.body.length - 1, 'index'],
['arguments', 'CallExpression'],
[0, 'index'],
]
return {
modifiedAst: clonedAst,
pathToSketchNode,
pathToRevolveArg,
}
}
// We're not creating a pipe expression,
// but rather a separate constant for the extrusion
const name = findUniqueName(clonedAst, KCL_DEFAULT_CONSTANT_PREFIXES.REVOLVE)
const VariableDeclaration = createVariableDeclaration(name, revolveCall)
const sketchIndexInPathToNode =
sketchPathToDecleration.findIndex((a) => a[0] === 'body') + 1
const sketchIndexInBody = sketchPathToDecleration[sketchIndexInPathToNode][0]
const lastSketchNodePath =
orderedSketchNodePaths[orderedSketchNodePaths.length - 1]
const sketchIndexInBody = Number(lastSketchNodePath[1][0])
if (typeof sketchIndexInBody !== 'number')
return new Error('expected sketchIndexInBody to be a number')
clonedAst.body.splice(sketchIndexInBody + 1, 0, VariableDeclaration)

View File

@ -212,19 +212,7 @@ Map {
"type": "wall",
},
"UUID-10" => {
"codeRef": {
"pathToNode": [
[
"body",
"",
],
],
"range": [
501,
522,
true,
],
},
"codeRef": undefined,
"edgeCutEdgeIds": [],
"id": "UUID",
"pathIds": [

View File

@ -22,6 +22,7 @@ import * as d3 from 'd3-force'
import path from 'path'
import pixelmatch from 'pixelmatch'
import { PNG } from 'pngjs'
import { Node } from 'wasm-lib/kcl/bindings/Node'
/*
Note this is an integration test, these tests connect to our real dev server and make websocket commands.
@ -171,7 +172,7 @@ afterAll(() => {
describe('testing createArtifactGraph', () => {
describe('code with offset planes and a sketch:', () => {
let ast: Program
let ast: Node<Program>
let theMap: ReturnType<typeof createArtifactGraph>
it('setup', () => {
@ -217,7 +218,7 @@ describe('testing createArtifactGraph', () => {
})
})
describe('code with an extrusion, fillet and sketch of face:', () => {
let ast: Program
let ast: Node<Program>
let theMap: ReturnType<typeof createArtifactGraph>
it('setup', () => {
// putting this logic in here because describe blocks runs before beforeAll has finished
@ -312,7 +313,7 @@ describe('testing createArtifactGraph', () => {
})
describe(`code with sketches but no extrusions or other 3D elements`, () => {
let ast: Program
let ast: Node<Program>
let theMap: ReturnType<typeof createArtifactGraph>
it(`setup`, () => {
// putting this logic in here because describe blocks runs before beforeAll has finished
@ -377,7 +378,7 @@ describe('testing createArtifactGraph', () => {
describe('capture graph of sketchOnFaceOnFace...', () => {
describe('code with an extrusion, fillet and sketch of face:', () => {
let ast: Program
let ast: Node<Program>
let theMap: ReturnType<typeof createArtifactGraph>
it('setup', async () => {
// putting this logic in here because describe blocks runs before beforeAll has finished
@ -399,7 +400,9 @@ describe('capture graph of sketchOnFaceOnFace...', () => {
})
})
function getCommands(codeKey: CodeKey): CacheShape[CodeKey] & { ast: Program } {
function getCommands(
codeKey: CodeKey
): CacheShape[CodeKey] & { ast: Node<Program> } {
const ast = assertParse(codeKey)
const file = fs.readFileSync(fullPath, 'utf-8')
const parsed: CacheShape = JSON.parse(file)

View File

@ -1,8 +1,19 @@
import { Expr, PathToNode, Program, SourceRange } from 'lang/wasm'
import {
Expr,
PathToNode,
Program,
SourceRange,
VariableDeclaration,
} from 'lang/wasm'
import { Models } from '@kittycad/lib'
import { getNodePathFromSourceRange } from 'lang/queryAst'
import {
getNodeFromPath,
getNodePathFromSourceRange,
traverse,
} from 'lang/queryAst'
import { err } from 'lib/trap'
import { engineCommandManager, kclManager } from 'lib/singletons'
import { Node } from 'wasm-lib/kcl/bindings/Node'
export type ArtifactId = string
@ -42,7 +53,7 @@ interface Solid2DArtifact extends BaseArtifact {
export interface PathArtifactRich extends BaseArtifact {
type: 'path'
/** A path must always lie on a plane */
plane: PlaneArtifact | WallArtifact
plane: PlaneArtifact | WallArtifact | CapArtifact
/** A path must always contain 0 or more segments */
segments: Array<SegmentArtifact>
/** A path may not result in a sweep artifact */
@ -101,6 +112,9 @@ interface CapArtifact extends BaseArtifact {
edgeCutEdgeIds: Array<ArtifactId>
sweepId: ArtifactId
pathIds: Array<ArtifactId>
// codeRef is for the sketchOnFace plane, not for the wall itself
// traverse to the extrude and or segment to get the wall's codeRef
codeRef?: CodeRef
}
interface SweepEdgeArtifact extends BaseArtifact {
@ -163,7 +177,7 @@ export function createArtifactGraph({
}: {
orderedCommands: Array<OrderedCommand>
responseMap: ResponseMap
ast: Program
ast: Node<Program>
}) {
const myMap = new Map<ArtifactId, Artifact>()
@ -242,7 +256,7 @@ export function getArtifactsToUpdate({
/** Passing in a getter because we don't wan this function to update the map directly */
getArtifact: (id: ArtifactId) => Artifact | undefined
currentPlaneId: ArtifactId
ast: Program
ast: Node<Program>
}): Array<{
id: ArtifactId
artifact: Artifact
@ -278,6 +292,13 @@ export function getArtifactsToUpdate({
plane?.type === 'plane' ? plane?.codeRef : { range, pathToNode }
const existingPlane = getArtifact(currentPlaneId)
if (existingPlane?.type === 'wall') {
let existingPlaneCodeRef = existingPlane.codeRef
if (!existingPlaneCodeRef) {
const astWalkCodeRef = getWallOrCapPlaneCodeRef(ast, codeRef.pathToNode)
if (!err(astWalkCodeRef)) {
existingPlaneCodeRef = astWalkCodeRef
}
}
return [
{
id: currentPlaneId,
@ -288,7 +309,29 @@ export function getArtifactsToUpdate({
edgeCutEdgeIds: existingPlane.edgeCutEdgeIds,
sweepId: existingPlane.sweepId,
pathIds: existingPlane.pathIds,
codeRef,
codeRef: existingPlaneCodeRef,
},
},
]
} else if (existingPlane?.type === 'cap') {
let existingPlaneCodeRef = existingPlane.codeRef
if (!existingPlaneCodeRef) {
const astWalkCodeRef = getWallOrCapPlaneCodeRef(ast, codeRef.pathToNode)
if (!err(astWalkCodeRef)) {
existingPlaneCodeRef = astWalkCodeRef
}
}
return [
{
id: currentPlaneId,
artifact: {
type: 'cap',
subType: existingPlane.subType,
id: currentPlaneId,
edgeCutEdgeIds: existingPlane.edgeCutEdgeIds,
sweepId: existingPlane.sweepId,
pathIds: existingPlane.pathIds,
codeRef: existingPlaneCodeRef,
},
},
]
@ -333,6 +376,18 @@ export function getArtifactsToUpdate({
pathIds: [id],
},
})
} else if (plane?.type === 'cap') {
returnArr.push({
id: currentPlaneId,
artifact: {
type: 'cap',
id: currentPlaneId,
subType: plane.subType,
edgeCutEdgeIds: plane.edgeCutEdgeIds,
sweepId: plane.sweepId,
pathIds: [id],
},
})
}
return returnArr
} else if (cmd.type === 'extend_path' || cmd.type === 'close_path') {
@ -880,9 +935,9 @@ export function codeRefFromRange(range: SourceRange, ast: Program): CodeRef {
function getPlaneFromPath(
path: PathArtifact,
graph: ArtifactGraph
): PlaneArtifact | WallArtifact | Error {
): PlaneArtifact | WallArtifact | CapArtifact | Error {
const plane = getArtifactOfTypes(
{ key: path.planeId, types: ['plane', 'wall'] },
{ key: path.planeId, types: ['plane', 'wall', 'cap'] },
graph
)
if (err(plane)) return plane
@ -892,7 +947,7 @@ function getPlaneFromPath(
function getPlaneFromSegment(
segment: SegmentArtifact,
graph: ArtifactGraph
): PlaneArtifact | WallArtifact | Error {
): PlaneArtifact | WallArtifact | CapArtifact | Error {
const path = getArtifactOfTypes(
{ key: segment.pathId, types: ['path'] },
graph
@ -903,7 +958,7 @@ function getPlaneFromSegment(
function getPlaneFromSolid2D(
solid2D: Solid2DArtifact,
graph: ArtifactGraph
): PlaneArtifact | WallArtifact | Error {
): PlaneArtifact | WallArtifact | CapArtifact | Error {
const path = getArtifactOfTypes(
{ key: solid2D.pathId, types: ['path'] },
graph
@ -914,7 +969,7 @@ function getPlaneFromSolid2D(
function getPlaneFromCap(
cap: CapArtifact,
graph: ArtifactGraph
): PlaneArtifact | WallArtifact | Error {
): PlaneArtifact | WallArtifact | CapArtifact | Error {
const sweep = getArtifactOfTypes(
{ key: cap.sweepId, types: ['sweep'] },
graph
@ -927,7 +982,7 @@ function getPlaneFromCap(
function getPlaneFromWall(
wall: WallArtifact,
graph: ArtifactGraph
): PlaneArtifact | WallArtifact | Error {
): PlaneArtifact | WallArtifact | CapArtifact | Error {
const sweep = getArtifactOfTypes(
{ key: wall.sweepId, types: ['sweep'] },
graph
@ -951,7 +1006,7 @@ function getPlaneFromSweepEdge(edge: SweepEdgeArtifact, graph: ArtifactGraph) {
export function getPlaneFromArtifact(
artifact: Artifact | undefined,
graph: ArtifactGraph
): PlaneArtifact | WallArtifact | Error {
): PlaneArtifact | WallArtifact | CapArtifact | Error {
if (!artifact) return new Error(`Artifact is undefined`)
if (artifact.type === 'plane') return artifact
if (artifact.type === 'path') return getPlaneFromPath(artifact, graph)
@ -1075,3 +1130,82 @@ function isNodeSafe(node: Expr): boolean {
}
return false
}
/** {@deprecated} this information should come from the ArtifactGraph not digging around in the AST */
function getWallOrCapPlaneCodeRef(
ast: Node<Program>,
pathToNode: PathToNode
): CodeRef | Error {
const varDec = getNodeFromPath<VariableDeclaration>(
ast,
pathToNode,
'VariableDeclaration'
)
if (err(varDec)) return varDec
if (varDec.node.type !== 'VariableDeclaration')
return new Error('Expected VariableDeclaration')
const init = varDec.node.declaration.init
let varName = ''
if (
init.type === 'CallExpression' &&
init.callee.type === 'Identifier' &&
(init.callee.name === 'circle' || init.callee.name === 'startProfileAt')
) {
const secondArg = init.arguments[1]
if (secondArg.type === 'Identifier') {
varName = secondArg.name
}
} else if (init.type === 'PipeExpression') {
const firstExpr = init.body[0]
if (
firstExpr.type === 'CallExpression' &&
firstExpr.callee.type === 'Identifier' &&
firstExpr.callee.name === 'startProfileAt'
) {
const secondArg = firstExpr.arguments[1]
if (secondArg.type === 'Identifier') {
varName = secondArg.name
}
}
}
if (varName === '') return new Error('Could not find variable name')
let currentVariableName = ''
const planeCodeRef: Array<{
path: PathToNode
sketchName: string
range: SourceRange
}> = []
traverse(ast, {
leave: (node) => {
if (node.type === 'VariableDeclaration') {
currentVariableName = ''
}
},
enter: (node, path) => {
if (node.type === 'VariableDeclaration') {
currentVariableName = node.declaration.id.name
}
if (
// match `${varName} = startSketchOn(...)`
node.type === 'CallExpression' &&
node.callee.name === 'startSketchOn' &&
node.arguments[0].type === 'Identifier' &&
currentVariableName === varName
) {
planeCodeRef.push({
path,
sketchName: currentVariableName,
range: [node.start, node.end, true],
})
}
},
})
if (!planeCodeRef.length)
return new Error('No paths found depending on extrude')
return {
pathToNode: planeCodeRef[0].path,
range: planeCodeRef[0].range,
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 568 KiB

After

Width:  |  Height:  |  Size: 560 KiB

View File

@ -37,6 +37,7 @@ import { KclManager } from 'lang/KclSingleton'
import { reportRejection } from 'lib/trap'
import { markOnce } from 'lib/performance'
import { MachineManager } from 'components/MachineManagerProvider'
import { Node } from 'wasm-lib/kcl/bindings/Node'
// TODO(paultag): This ought to be tweakable.
const pingIntervalMs = 5_000
@ -2115,7 +2116,7 @@ export class EngineCommandManager extends EventTarget {
Object.values(this.pendingCommands).map((a) => a.promise)
)
}
updateArtifactGraph(ast: Program) {
updateArtifactGraph(ast: Node<Program>) {
this.artifactGraph = createArtifactGraph({
orderedCommands: this.orderedCommands,
responseMap: this.responseMap,

File diff suppressed because one or more lines are too long

View File

@ -723,7 +723,7 @@ dependencies = [
[[package]]
name = "derive-docs"
version = "0.1.32"
version = "0.1.33"
dependencies = [
"Inflector",
"anyhow",
@ -1684,7 +1684,7 @@ dependencies = [
[[package]]
name = "kcl-lib"
version = "0.2.28"
version = "0.2.29"
dependencies = [
"anyhow",
"approx 0.5.1",
@ -1752,7 +1752,7 @@ dependencies = [
[[package]]
name = "kcl-test-server"
version = "0.1.18"
version = "0.1.19"
dependencies = [
"anyhow",
"hyper 0.14.30",

View File

@ -1,7 +1,7 @@
[package]
name = "derive-docs"
description = "A tool for generating documentation from Rust derive macros"
version = "0.1.32"
version = "0.1.33"
edition = "2021"
license = "MIT"
repository = "https://github.com/KittyCAD/modeling-app"

View File

@ -1,7 +1,7 @@
[package]
name = "kcl-test-server"
description = "A test server for KCL"
version = "0.1.18"
version = "0.1.19"
edition = "2021"
license = "MIT"

View File

@ -1,7 +1,7 @@
[package]
name = "kcl-lib"
description = "KittyCAD Language implementation and tools"
version = "0.2.28"
version = "0.2.29"
edition = "2021"
license = "MIT"
repository = "https://github.com/KittyCAD/modeling-app"
@ -22,7 +22,7 @@ clap = { version = "4.5.21", default-features = false, optional = true, features
] }
convert_case = "0.6.0"
dashmap = "6.1.0"
derive-docs = { version = "0.1.32", path = "../derive-docs" }
derive-docs = { version = "0.1.33", path = "../derive-docs" }
dhat = { version = "0.3", optional = true }
fnv = "1.0.7"
form_urlencoded = "1.2.1"

View File

@ -345,6 +345,12 @@ impl Geometry {
Geometry::Solid(e) => e.id,
}
}
pub fn original_id(&self) -> uuid::Uuid {
match self {
Geometry::Sketch(s) => s.original_id,
Geometry::Solid(e) => e.sketch.original_id,
}
}
}
/// A set of geometry.
@ -776,7 +782,6 @@ pub struct Sketch {
pub tags: IndexMap<String, TagIdentifier>,
/// The original id of the sketch. This stays the same even if the sketch is
/// is sketched on face etc.
#[serde(skip)]
pub original_id: uuid::Uuid,
/// Metadata.
#[serde(rename = "__meta")]

View File

@ -111,7 +111,7 @@ async fn inner_extrude(
args.batch_modeling_cmd(
id,
ModelingCmd::from(mcmd::Extrude {
target: sketch.id.into(),
target: dbg!(sketch.id.into()),
distance: LengthUnit(length),
faces: Default::default(),
}),

View File

@ -384,9 +384,9 @@ async fn send_pattern_transform<T: GeometryTrait>(
.send_modeling_cmd(
id,
ModelingCmd::from(mcmd::EntityLinearPatternTransform {
entity_id: solid.id(),
transform: Default::default(),
entity_id: solid.original_id(),
transforms,
transform: Default::default(),
}),
)
.await?;
@ -597,7 +597,9 @@ fn array_to_point2d(val: &KclValue, source_ranges: Vec<SourceRange>) -> Result<P
trait GeometryTrait: Clone {
type Set: Into<Vec<Self>> + Clone;
#[allow(dead_code)]
fn id(&self) -> Uuid;
fn original_id(&self) -> Uuid;
fn set_id(&mut self, id: Uuid);
fn array_to_point3d(val: &KclValue, source_ranges: Vec<SourceRange>) -> Result<Point3d, KclError>;
async fn flush_batch(args: &Args, exec_state: &mut ExecState, set: Self::Set) -> Result<(), KclError>;
@ -608,9 +610,15 @@ impl GeometryTrait for Box<Sketch> {
fn set_id(&mut self, id: Uuid) {
self.id = id;
}
fn id(&self) -> Uuid {
self.id
}
fn original_id(&self) -> Uuid {
self.original_id
}
fn array_to_point3d(val: &KclValue, source_ranges: Vec<SourceRange>) -> Result<Point3d, KclError> {
let Point2d { x, y } = array_to_point2d(val, source_ranges)?;
Ok(Point3d { x, y, z: 0.0 })
@ -630,6 +638,11 @@ impl GeometryTrait for Box<Solid> {
fn id(&self) -> Uuid {
self.id
}
fn original_id(&self) -> Uuid {
self.sketch.original_id
}
fn array_to_point3d(val: &KclValue, source_ranges: Vec<SourceRange>) -> Result<Point3d, KclError> {
array_to_point3d(val, source_ranges)
}
@ -1049,7 +1062,7 @@ async fn pattern_circular(
id,
ModelingCmd::from(mcmd::EntityCircularPattern {
axis: kcmc::shared::Point3d::from(data.axis()),
entity_id: geometry.id(),
entity_id: geometry.original_id(),
center: kcmc::shared::Point3d {
x: LengthUnit(center[0]),
y: LengthUnit(center[1]),

View File

@ -0,0 +1,20 @@
w = 400
case = startSketchOn('XY')
|> startProfileAt([-w, -w], %)
|> lineTo([-w, w], %)
|> lineTo([ w, -w], %)
|> lineTo([-w, -w], %)
|> close(%)
|> extrude(200, %)
bump1 = startSketchOn(case, 'end')
|> circle({center: [-50, -50], radius: 40}, %)
|> extrude(20, %)
// We pass in "bump1" here since we want to pattern just this object on the face.
|> patternLinear3d({
axis: [1, 0, 0],
instances: 3,
distance: -100,
}, %)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 127 KiB

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 123 KiB

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

View File

@ -0,0 +1,65 @@
macro_rules! kcl_input {
($file:literal) => {
include_str!(concat!("inputs/", $file, ".kcl"))
};
}
macro_rules! kcl_test {
($file:literal, $test_name:ident) => {
#[tokio::test(flavor = "multi_thread")]
async fn $test_name() {
let code = kcl_input!($file);
let result = super::execute_and_snapshot(code, kcl_lib::settings::types::UnitLength::Mm)
.await
.unwrap();
super::assert_out($file, &result);
}
};
}
kcl_test!("sketch_on_face", kcl_test_sketch_on_face);
kcl_test!("tangential_arc", kcl_test_tangential_arc);
kcl_test!(
"big_number_angle_to_match_length_x",
kcl_test_big_number_angle_to_match_length_x
);
kcl_test!(
"big_number_angle_to_match_length_y",
kcl_test_big_number_angle_to_match_length_y
);
kcl_test!("sketch_on_face_circle_tagged", kcl_test_sketch_on_face_circle_tagged);
kcl_test!("basic_fillet_cube_start", kcl_test_basic_fillet_cube_start);
kcl_test!(
"basic_fillet_cube_next_adjacent",
kcl_test_basic_fillet_cube_next_adjacent
);
kcl_test!(
"basic_fillet_cube_previous_adjacent",
kcl_test_basic_fillet_cube_previous_adjacent
);
kcl_test!("basic_fillet_cube_end", kcl_test_basic_fillet_cube_end);
kcl_test!(
"basic_fillet_cube_close_opposite",
kcl_test_basic_fillet_cube_close_opposite
);
kcl_test!("sketch_on_face_end", kcl_test_sketch_on_face_end);
kcl_test!("sketch_on_face_start", kcl_test_sketch_on_face_start);
kcl_test!(
"sketch_on_face_end_negative_extrude",
kcl_test_sketch_on_face_end_negative_extrude
);
kcl_test!("mike_stress_test", kcl_test_mike_stress_test);
kcl_test!("pentagon_fillet_sugar", kcl_test_pentagon_fillet_sugar);
kcl_test!("pipe_as_arg", kcl_test_pipe_as_arg);
kcl_test!("computed_var", kcl_test_computed_var);
kcl_test!("lego", kcl_test_lego);
kcl_test!("riddle_small", kcl_test_riddle_small);
kcl_test!("tan_arc_x_line", kcl_test_tan_arc_x_line);
kcl_test!("fillet-and-shell", kcl_test_fillet_and_shell);
kcl_test!("sketch-on-chamfer-two-times", kcl_test_sketch_on_chamfer_two_times);
kcl_test!(
"sketch-on-chamfer-two-times-different-order",
kcl_test_sketch_on_chamfer_two_times_different_order
);
kcl_test!("repeat_bumps_only", repeat_bumps_only);