diff --git a/docs/kcl/offsetPlane.md b/docs/kcl/offsetPlane.md index c5e87f655..2ead03d87 100644 --- a/docs/kcl/offsetPlane.md +++ b/docs/kcl/offsetPlane.md @@ -9,7 +9,7 @@ Offset a plane by a distance along its normal. For example, if you offset the 'XZ' plane by 10, the new plane will be parallel to the 'XZ' plane and 10 units away from it. ```js -offsetPlane(std_plane: StandardPlane, offset: number) -> PlaneData +offsetPlane(std_plane: StandardPlane, offset: number) -> Plane ``` @@ -22,7 +22,7 @@ offsetPlane(std_plane: StandardPlane, offset: number) -> PlaneData ### Returns -[`PlaneData`](/docs/kcl/types/PlaneData) - Data for a plane. +[`Plane`](/docs/kcl/types/Plane) - A plane. ### Examples diff --git a/docs/kcl/std.json b/docs/kcl/std.json index 79c44ecf5..7fdde7c26 100644 --- a/docs/kcl/std.json +++ b/docs/kcl/std.json @@ -105747,109 +105747,90 @@ ], "returnValue": { "name": "", - "type": "PlaneData", + "type": "Plane", "schema": { "$schema": "https://spec.openapis.org/oas/3.0/schema/2019-04-02#/definitions/Schema", - "title": "PlaneData", - "description": "Data for a plane.", - "oneOf": [ - { - "description": "The XY plane.", - "type": "string", - "enum": [ - "XY" - ] - }, - { - "description": "The opposite side of the XY plane.", - "type": "string", - "enum": [ - "-XY" - ] - }, - { - "description": "The XZ plane.", - "type": "string", - "enum": [ - "XZ" - ] - }, - { - "description": "The opposite side of the XZ plane.", - "type": "string", - "enum": [ - "-XZ" - ] - }, - { - "description": "The YZ plane.", - "type": "string", - "enum": [ - "YZ" - ] - }, - { - "description": "The opposite side of the YZ plane.", - "type": "string", - "enum": [ - "-YZ" - ] - }, - { - "description": "A defined plane.", - "type": "object", - "required": [ - "plane" - ], - "properties": { - "plane": { - "type": "object", - "required": [ - "origin", - "xAxis", - "yAxis", - "zAxis" - ], - "properties": { - "origin": { - "description": "Origin of the plane.", - "allOf": [ - { - "$ref": "#/components/schemas/Point3d" - } - ] - }, - "xAxis": { - "description": "What should the plane’s X axis be?", - "allOf": [ - { - "$ref": "#/components/schemas/Point3d" - } - ] - }, - "yAxis": { - "description": "What should the plane’s Y axis be?", - "allOf": [ - { - "$ref": "#/components/schemas/Point3d" - } - ] - }, - "zAxis": { - "description": "The z-axis (normal).", - "allOf": [ - { - "$ref": "#/components/schemas/Point3d" - } - ] - } - } - } - }, - "additionalProperties": false - } + "title": "Plane", + "description": "A plane.", + "type": "object", + "required": [ + "__meta", + "id", + "origin", + "value", + "xAxis", + "yAxis", + "zAxis" ], + "properties": { + "id": { + "description": "The id of the plane.", + "type": "string", + "format": "uuid" + }, + "value": { + "$ref": "#/components/schemas/PlaneType" + }, + "origin": { + "description": "Origin of the plane.", + "allOf": [ + { + "$ref": "#/components/schemas/Point3d" + } + ] + }, + "xAxis": { + "description": "What should the plane’s X axis be?", + "allOf": [ + { + "$ref": "#/components/schemas/Point3d" + } + ] + }, + "yAxis": { + "description": "What should the plane’s Y axis be?", + "allOf": [ + { + "$ref": "#/components/schemas/Point3d" + } + ] + }, + "zAxis": { + "description": "The z-axis (normal).", + "allOf": [ + { + "$ref": "#/components/schemas/Point3d" + } + ] + }, + "__meta": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Metadata" + } + } + }, "definitions": { + "PlaneType": { + "description": "Type for a plane.", + "oneOf": [ + { + "type": "string", + "enum": [ + "XY", + "XZ", + "YZ" + ] + }, + { + "description": "A custom plane.", + "type": "string", + "enum": [ + "Custom" + ] + } + ] + }, "Point3d": { "type": "object", "required": [ @@ -105871,6 +105852,33 @@ "format": "double" } } + }, + "Metadata": { + "description": "Metadata.", + "type": "object", + "required": [ + "sourceRange" + ], + "properties": { + "sourceRange": { + "description": "The source range.", + "allOf": [ + { + "$ref": "#/components/schemas/SourceRange" + } + ] + } + } + }, + "SourceRange": { + "type": "array", + "items": { + "type": "integer", + "format": "uint", + "minimum": 0.0 + }, + "maxItems": 3, + "minItems": 3 } } }, @@ -179036,13 +179044,16 @@ { "$ref": "#/components/schemas/PlaneData" }, + { + "$ref": "#/components/schemas/Plane" + }, { "$ref": "#/components/schemas/Solid" } ], "definitions": { "PlaneData": { - "description": "Data for a plane.", + "description": "Orientation data that can be used to construct a plane, not a plane in itself.", "oneOf": [ { "description": "The XY plane.", @@ -179163,6 +179174,114 @@ } } }, + "Plane": { + "description": "A plane.", + "type": "object", + "required": [ + "__meta", + "id", + "origin", + "value", + "xAxis", + "yAxis", + "zAxis" + ], + "properties": { + "id": { + "description": "The id of the plane.", + "type": "string", + "format": "uuid" + }, + "value": { + "$ref": "#/components/schemas/PlaneType" + }, + "origin": { + "description": "Origin of the plane.", + "allOf": [ + { + "$ref": "#/components/schemas/Point3d" + } + ] + }, + "xAxis": { + "description": "What should the plane’s X axis be?", + "allOf": [ + { + "$ref": "#/components/schemas/Point3d" + } + ] + }, + "yAxis": { + "description": "What should the plane’s Y axis be?", + "allOf": [ + { + "$ref": "#/components/schemas/Point3d" + } + ] + }, + "zAxis": { + "description": "The z-axis (normal).", + "allOf": [ + { + "$ref": "#/components/schemas/Point3d" + } + ] + }, + "__meta": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Metadata" + } + } + } + }, + "PlaneType": { + "description": "Type for a plane.", + "oneOf": [ + { + "type": "string", + "enum": [ + "XY", + "XZ", + "YZ" + ] + }, + { + "description": "A custom plane.", + "type": "string", + "enum": [ + "Custom" + ] + } + ] + }, + "Metadata": { + "description": "Metadata.", + "type": "object", + "required": [ + "sourceRange" + ], + "properties": { + "sourceRange": { + "description": "The source range.", + "allOf": [ + { + "$ref": "#/components/schemas/SourceRange" + } + ] + } + } + }, + "SourceRange": { + "type": "array", + "items": { + "type": "integer", + "format": "uint", + "minimum": 0.0 + }, + "maxItems": 3, + "minItems": 3 + }, "Solid": { "description": "An solid is a collection of extrude surfaces.", "type": "object", @@ -179444,16 +179563,6 @@ } } }, - "SourceRange": { - "type": "array", - "items": { - "type": "integer", - "format": "uint", - "minimum": 0.0 - }, - "maxItems": 3, - "minItems": 3 - }, "Sketch": { "description": "A sketch is a collection of paths.", "type": "object", @@ -180208,43 +180317,6 @@ } ] }, - "PlaneType": { - "description": "Type for a plane.", - "oneOf": [ - { - "type": "string", - "enum": [ - "XY", - "XZ", - "YZ" - ] - }, - { - "description": "A custom plane.", - "type": "string", - "enum": [ - "Custom" - ] - } - ] - }, - "Metadata": { - "description": "Metadata.", - "type": "object", - "required": [ - "sourceRange" - ], - "properties": { - "sourceRange": { - "description": "The source range.", - "allOf": [ - { - "$ref": "#/components/schemas/SourceRange" - } - ] - } - } - }, "BasePath": { "description": "A base path.", "type": "object", @@ -180460,7 +180532,7 @@ "nullable": true, "definitions": { "PlaneData": { - "description": "Data for a plane.", + "description": "Orientation data that can be used to construct a plane, not a plane in itself.", "oneOf": [ { "description": "The XY plane.", @@ -180581,6 +180653,114 @@ } } }, + "Plane": { + "description": "A plane.", + "type": "object", + "required": [ + "__meta", + "id", + "origin", + "value", + "xAxis", + "yAxis", + "zAxis" + ], + "properties": { + "id": { + "description": "The id of the plane.", + "type": "string", + "format": "uuid" + }, + "value": { + "$ref": "#/components/schemas/PlaneType" + }, + "origin": { + "description": "Origin of the plane.", + "allOf": [ + { + "$ref": "#/components/schemas/Point3d" + } + ] + }, + "xAxis": { + "description": "What should the plane’s X axis be?", + "allOf": [ + { + "$ref": "#/components/schemas/Point3d" + } + ] + }, + "yAxis": { + "description": "What should the plane’s Y axis be?", + "allOf": [ + { + "$ref": "#/components/schemas/Point3d" + } + ] + }, + "zAxis": { + "description": "The z-axis (normal).", + "allOf": [ + { + "$ref": "#/components/schemas/Point3d" + } + ] + }, + "__meta": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Metadata" + } + } + } + }, + "PlaneType": { + "description": "Type for a plane.", + "oneOf": [ + { + "type": "string", + "enum": [ + "XY", + "XZ", + "YZ" + ] + }, + { + "description": "A custom plane.", + "type": "string", + "enum": [ + "Custom" + ] + } + ] + }, + "Metadata": { + "description": "Metadata.", + "type": "object", + "required": [ + "sourceRange" + ], + "properties": { + "sourceRange": { + "description": "The source range.", + "allOf": [ + { + "$ref": "#/components/schemas/SourceRange" + } + ] + } + } + }, + "SourceRange": { + "type": "array", + "items": { + "type": "integer", + "format": "uint", + "minimum": 0.0 + }, + "maxItems": 3, + "minItems": 3 + }, "Solid": { "description": "An solid is a collection of extrude surfaces.", "type": "object", @@ -180862,16 +181042,6 @@ } } }, - "SourceRange": { - "type": "array", - "items": { - "type": "integer", - "format": "uint", - "minimum": 0.0 - }, - "maxItems": 3, - "minItems": 3 - }, "Sketch": { "description": "A sketch is a collection of paths.", "type": "object", @@ -181626,43 +181796,6 @@ } ] }, - "PlaneType": { - "description": "Type for a plane.", - "oneOf": [ - { - "type": "string", - "enum": [ - "XY", - "XZ", - "YZ" - ] - }, - { - "description": "A custom plane.", - "type": "string", - "enum": [ - "Custom" - ] - } - ] - }, - "Metadata": { - "description": "Metadata.", - "type": "object", - "required": [ - "sourceRange" - ], - "properties": { - "sourceRange": { - "description": "The source range.", - "allOf": [ - { - "$ref": "#/components/schemas/SourceRange" - } - ] - } - } - }, "BasePath": { "description": "A base path.", "type": "object", diff --git a/docs/kcl/types/KclValue.md b/docs/kcl/types/KclValue.md index 6cfeac72e..61eb5236f 100644 --- a/docs/kcl/types/KclValue.md +++ b/docs/kcl/types/KclValue.md @@ -180,7 +180,7 @@ A plane. | Property | Type | Description | Required | |----------|------|-------------|----------| -| `type` |enum: `Plane`| | No | +| `type` |enum: [`Plane`](/docs/kcl/types/Plane)| | No | | `id` |`string`| The id of the plane. | No | | `value` |[`PlaneType`](/docs/kcl/types/PlaneType)| Any KCL value. | No | | `origin` |[`Point3d`](/docs/kcl/types/Point3d)| Origin of the plane. | No | diff --git a/docs/kcl/types/Plane.md b/docs/kcl/types/Plane.md new file mode 100644 index 000000000..f1aa17aba --- /dev/null +++ b/docs/kcl/types/Plane.md @@ -0,0 +1,27 @@ +--- +title: "Plane" +excerpt: "A plane." +layout: manual +--- + +A plane. + +**Type:** `object` + + + + + +## Properties + +| Property | Type | Description | Required | +|----------|------|-------------|----------| +| `id` |`string`| The id of the plane. | No | +| `value` |[`PlaneType`](/docs/kcl/types/PlaneType)| A plane. | No | +| `origin` |[`Point3d`](/docs/kcl/types/Point3d)| Origin of the plane. | No | +| `xAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the plane’s X axis be? | No | +| `yAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the plane’s Y axis be? | No | +| `zAxis` |[`Point3d`](/docs/kcl/types/Point3d)| The z-axis (normal). | No | +| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No | + + diff --git a/docs/kcl/types/PlaneData.md b/docs/kcl/types/PlaneData.md index f16cfd9d2..8cc4707c2 100644 --- a/docs/kcl/types/PlaneData.md +++ b/docs/kcl/types/PlaneData.md @@ -1,10 +1,10 @@ --- title: "PlaneData" -excerpt: "Data for a plane." +excerpt: "Orientation data that can be used to construct a plane, not a plane in itself." layout: manual --- -Data for a plane. +Orientation data that can be used to construct a plane, not a plane in itself. diff --git a/docs/kcl/types/SketchData.md b/docs/kcl/types/SketchData.md index 53ee3ef0c..fbb01aa85 100644 --- a/docs/kcl/types/SketchData.md +++ b/docs/kcl/types/SketchData.md @@ -22,6 +22,18 @@ Data for start sketch on. You can start a sketch on a plane or an solid. +---- +Data for start sketch on. You can start a sketch on a plane or an solid. + +[`Plane`](/docs/kcl/types/Plane) + + + + + + + + ---- Data for start sketch on. You can start a sketch on a plane or an solid. diff --git a/e2e/playwright/sketch-tests.spec.ts b/e2e/playwright/sketch-tests.spec.ts index a846962b2..1a315b000 100644 --- a/e2e/playwright/sketch-tests.spec.ts +++ b/e2e/playwright/sketch-tests.spec.ts @@ -1274,3 +1274,44 @@ test2.describe('Sketch mode should be toleratant to syntax errors', () => { } ) }) + +test2.describe(`Sketching with offset planes`, () => { + test2( + `Can select an offset plane to sketch on`, + async ({ app, scene, toolbar, editor }) => { + // We seed the scene with a single offset plane + await app.initialise(`offsetPlane001 = offsetPlane("XY", 10)`) + + const [planeClick, planeHover] = scene.makeMouseHelpers(650, 200) + + await test2.step(`Start sketching on the offset plane`, async () => { + await toolbar.startSketchPlaneSelection() + + await test2.step(`Hovering should highlight code`, async () => { + await planeHover() + await editor.expectState({ + activeLines: [`offsetPlane001=offsetPlane("XY",10)`], + diagnostics: [], + highlightedCode: 'offsetPlane("XY", 10)', + }) + }) + + await test2.step( + `Clicking should select the plane and enter sketch mode`, + async () => { + await planeClick() + // Have to wait for engine-side animation to finish + await app.page.waitForTimeout(600) + await expect2(toolbar.lineBtn).toBeEnabled() + await editor.expectEditor.toContain('startSketchOn(offsetPlane001)') + await editor.expectState({ + activeLines: [`offsetPlane001=offsetPlane("XY",10)`], + diagnostics: [], + highlightedCode: '', + }) + } + ) + }) + } + ) +}) diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Zoom-to-fit-on-load---solid-2d-1-Google-Chrome-win32.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Zoom-to-fit-on-load---solid-2d-1-Google-Chrome-win32.png index 93ac49f9b..113275c14 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Zoom-to-fit-on-load---solid-2d-1-Google-Chrome-win32.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Zoom-to-fit-on-load---solid-2d-1-Google-Chrome-win32.png differ diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Zoom-to-fit-on-load---solid-3d-1-Google-Chrome-linux.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Zoom-to-fit-on-load---solid-3d-1-Google-Chrome-linux.png index 2ad93f643..1204d850f 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Zoom-to-fit-on-load---solid-3d-1-Google-Chrome-linux.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Zoom-to-fit-on-load---solid-3d-1-Google-Chrome-linux.png differ diff --git a/src/components/ModelingMachineProvider.tsx b/src/components/ModelingMachineProvider.tsx index e450e76ee..939b2c2f8 100644 --- a/src/components/ModelingMachineProvider.tsx +++ b/src/components/ModelingMachineProvider.tsx @@ -63,6 +63,7 @@ import { import { moveValueIntoNewVariablePath, sketchOnExtrudedFace, + sketchOnOffsetPlane, startSketchOnDefault, } from 'lang/modifyAst' import { Program, parse, recast } from 'lang/wasm' @@ -636,13 +637,16 @@ export const ModelingMachineProvider = ({ ), 'animate-to-face': fromPromise(async ({ input }) => { if (!input) return undefined - if (input.type === 'extrudeFace') { - const sketched = sketchOnExtrudedFace( - kclManager.ast, - input.sketchPathToNode, - input.extrudePathToNode, - input.faceInfo - ) + if (input.type === 'extrudeFace' || input.type === 'offsetPlane') { + const sketched = + input.type === 'extrudeFace' + ? sketchOnExtrudedFace( + kclManager.ast, + input.sketchPathToNode, + input.extrudePathToNode, + input.faceInfo + ) + : sketchOnOffsetPlane(kclManager.ast, input.pathToNode) if (err(sketched)) { const sketchedError = new Error( 'Incompatible face, please try another' @@ -654,10 +658,9 @@ export const ModelingMachineProvider = ({ await kclManager.executeAstMock(modifiedAst) - await letEngineAnimateAndSyncCamAfter( - engineCommandManager, - input.faceId - ) + const id = + input.type === 'extrudeFace' ? input.faceId : input.planeId + await letEngineAnimateAndSyncCamAfter(engineCommandManager, id) sceneInfra.camControls.syncDirection = 'clientToEngine' return { sketchPathToNode: pathToNewSketchNode, diff --git a/src/hooks/useEngineConnectionSubscriptions.ts b/src/hooks/useEngineConnectionSubscriptions.ts index 3bfeb5f14..0640023ad 100644 --- a/src/hooks/useEngineConnectionSubscriptions.ts +++ b/src/hooks/useEngineConnectionSubscriptions.ts @@ -88,6 +88,10 @@ export function useEngineConnectionSubscriptions() { ? [codeRef.range] : [codeRef.range, consumedCodeRef.range] ) + } else if (artifact?.type === 'plane') { + const codeRef = artifact.codeRef + if (err(codeRef)) return + editorManager.setHighlightRange([codeRef.range]) } else { editorManager.setHighlightRange([[0, 0]]) } @@ -186,8 +190,42 @@ export function useEngineConnectionSubscriptions() { }) return } + const artifact = + engineCommandManager.artifactGraph.get(planeOrFaceId) + + if (artifact?.type === 'plane') { + const planeInfo = await getFaceDetails(planeOrFaceId) + sceneInfra.modelingSend({ + type: 'Select default plane', + data: { + type: 'offsetPlane', + zAxis: [ + planeInfo.z_axis.x, + planeInfo.z_axis.y, + planeInfo.z_axis.z, + ], + yAxis: [ + planeInfo.y_axis.x, + planeInfo.y_axis.y, + planeInfo.y_axis.z, + ], + position: [ + planeInfo.origin.x, + planeInfo.origin.y, + planeInfo.origin.z, + ].map((num) => num / sceneInfra._baseUnitMultiplier) as [ + number, + number, + number + ], + planeId: planeOrFaceId, + pathToNode: artifact.codeRef.pathToNode, + }, + }) + } + + // Artifact is likely an extrusion face const faceId = planeOrFaceId - const artifact = engineCommandManager.artifactGraph.get(faceId) const extrusion = getSweepFromSuspectedSweepSurface( faceId, engineCommandManager.artifactGraph diff --git a/src/lang/modifyAst.ts b/src/lang/modifyAst.ts index b581958d3..8ef1aef1e 100644 --- a/src/lang/modifyAst.ts +++ b/src/lang/modifyAst.ts @@ -19,6 +19,7 @@ import { ProgramMemory, SourceRange, sketchFromKclValue, + isPathToNodeNumber, } from './wasm' import { isNodeSafeToReplacePath, @@ -526,6 +527,60 @@ export function sketchOnExtrudedFace( } } +/** + * Modify the AST to create a new sketch using the variable declaration + * of an offset plane. The new sketch just has to come after the offset + * plane declaration. + */ +export function sketchOnOffsetPlane( + node: Node, + offsetPathToNode: PathToNode +) { + let _node = { ...node } + + // Find the offset plane declaration + const offsetPlaneDeclarator = getNodeFromPath( + _node, + offsetPathToNode, + 'VariableDeclarator', + true + ) + if (err(offsetPlaneDeclarator)) return offsetPlaneDeclarator + const { node: offsetPlaneNode } = offsetPlaneDeclarator + const offsetPlaneName = offsetPlaneNode.id.name + + // Create a new sketch declaration + const newSketchName = findUniqueName( + node, + KCL_DEFAULT_CONSTANT_PREFIXES.SKETCH + ) + const newSketch = createVariableDeclaration( + newSketchName, + createCallExpressionStdLib('startSketchOn', [ + createIdentifier(offsetPlaneName), + ]), + undefined, + 'const' + ) + + // Decide where to insert the new sketch declaration + const offsetIndex = offsetPathToNode[1][0] + + if (!isPathToNodeNumber(offsetIndex)) { + return new Error('Expected offsetIndex to be a number') + } + // and insert it + _node.body.splice(offsetIndex + 1, 0, newSketch) + const newPathToNode = structuredClone(offsetPathToNode) + newPathToNode[1][0] = offsetIndex + 1 + + // Return the modified AST and the path to the new sketch declaration + return { + modifiedAst: _node, + pathToNode: newPathToNode, + } +} + export const getLastIndex = (pathToNode: PathToNode): number => splitPathAtLastIndex(pathToNode).index diff --git a/src/lang/std/artifactGraph.test.ts b/src/lang/std/artifactGraph.test.ts index 837acc894..4234bc058 100644 --- a/src/lang/std/artifactGraph.test.ts +++ b/src/lang/std/artifactGraph.test.ts @@ -98,12 +98,22 @@ sketch004 = startSketchOn(extrude003, seg02) |> close(%) extrude004 = extrude(3, sketch004) ` +const exampleCodeOffsetPlanes = ` +offsetPlane001 = offsetPlane("XY", 20) +offsetPlane002 = offsetPlane("XZ", -50) +offsetPlane003 = offsetPlane("YZ", 10) + +sketch002 = startSketchOn(offsetPlane001) + |> startProfileAt([0, 0], %) + |> line([6.78, 15.01], %) +` // add more code snippets here and use `getCommands` to get the orderedCommands and responseMap for more tests const codeToWriteCacheFor = { exampleCode1, sketchOnFaceOnFaceEtc, exampleCodeNo3D, + exampleCodeOffsetPlanes, } as const type CodeKey = keyof typeof codeToWriteCacheFor @@ -165,6 +175,52 @@ afterAll(() => { }) describe('testing createArtifactGraph', () => { + describe('code with offset planes and a sketch:', () => { + let ast: Program + let theMap: ReturnType + + it('setup', () => { + // putting this logic in here because describe blocks runs before beforeAll has finished + const { + orderedCommands, + responseMap, + ast: _ast, + } = getCommands('exampleCodeOffsetPlanes') + ast = _ast + theMap = createArtifactGraph({ orderedCommands, responseMap, ast }) + }) + + it(`there should be one sketch`, () => { + const sketches = [...filterArtifacts({ types: ['path'] }, theMap)].map( + (path) => expandPath(path[1], theMap) + ) + expect(sketches).toHaveLength(1) + sketches.forEach((path) => { + if (err(path)) throw path + expect(path.type).toBe('path') + }) + }) + + it(`there should be three offsetPlanes`, () => { + const offsetPlanes = [ + ...filterArtifacts({ types: ['plane'] }, theMap), + ].map((plane) => expandPlane(plane[1], theMap)) + expect(offsetPlanes).toHaveLength(3) + offsetPlanes.forEach((path) => { + expect(path.type).toBe('plane') + }) + }) + + it(`Only one offset plane should have a path`, () => { + const offsetPlanes = [ + ...filterArtifacts({ types: ['plane'] }, theMap), + ].map((plane) => expandPlane(plane[1], theMap)) + const offsetPlaneWithPaths = offsetPlanes.filter( + (plane) => plane.paths.length + ) + expect(offsetPlaneWithPaths).toHaveLength(1) + }) + }) describe('code with an extrusion, fillet and sketch of face:', () => { let ast: Program let theMap: ReturnType diff --git a/src/lang/std/artifactGraph.ts b/src/lang/std/artifactGraph.ts index e96b6b3f8..8fddddc01 100644 --- a/src/lang/std/artifactGraph.ts +++ b/src/lang/std/artifactGraph.ts @@ -249,7 +249,20 @@ export function getArtifactsToUpdate({ const cmd = command.cmd const returnArr: ReturnType = [] if (!response) return returnArr - if (cmd.type === 'enable_sketch_mode') { + if (cmd.type === 'make_plane' && range[1] !== 0) { + // If we're calling `make_plane` and the code range doesn't end at `0` + // it's not a default plane, but a custom one from the offsetPlane standard library function + return [ + { + id, + artifact: { + type: 'plane', + pathIds: [], + codeRef: { range, pathToNode }, + }, + }, + ] + } else if (cmd.type === 'enable_sketch_mode') { const plane = getArtifact(currentPlaneId) const pathIds = plane?.type === 'plane' ? plane?.pathIds : [] const codeRef = diff --git a/src/lang/wasm.ts b/src/lang/wasm.ts index bae827193..d3373119a 100644 --- a/src/lang/wasm.ts +++ b/src/lang/wasm.ts @@ -144,6 +144,12 @@ export const parse = (code: string | Error): Node | Error => { export type PathToNode = [string | number, string][] +export const isPathToNodeNumber = ( + pathToNode: string | number +): pathToNode is number => { + return typeof pathToNode === 'number' +} + export interface ExecState { memory: ProgramMemory idGenerator: IdGenerator diff --git a/src/machines/modelingMachine.ts b/src/machines/modelingMachine.ts index f3147c8c5..9f9810432 100644 --- a/src/machines/modelingMachine.ts +++ b/src/machines/modelingMachine.ts @@ -159,6 +159,15 @@ export type DefaultPlane = { yAxis: [number, number, number] } +export type OffsetPlane = { + type: 'offsetPlane' + position: [number, number, number] + planeId: string + pathToNode: PathToNode + zAxis: [number, number, number] + yAxis: [number, number, number] +} + export type SegmentOverlayPayload = | { type: 'set-one' @@ -198,7 +207,7 @@ export type ModelingMachineEvent = | { type: 'Sketch On Face' } | { type: 'Select default plane' - data: DefaultPlane | ExtrudeFacePlane + data: DefaultPlane | ExtrudeFacePlane | OffsetPlane } | { type: 'Set selection' @@ -1394,7 +1403,7 @@ export const modelingMachine = setup({ } ), 'animate-to-face': fromPromise( - async (_: { input?: ExtrudeFacePlane | DefaultPlane }) => { + async (_: { input?: ExtrudeFacePlane | DefaultPlane | OffsetPlane }) => { return {} as | undefined | { diff --git a/src/wasm-lib/kcl/src/executor.rs b/src/wasm-lib/kcl/src/executor.rs index cfc7a46f3..58308cb32 100644 --- a/src/wasm-lib/kcl/src/executor.rs +++ b/src/wasm-lib/kcl/src/executor.rs @@ -801,6 +801,17 @@ impl Plane { }, } } + + /// The standard planes are XY, YZ and XZ (in both positive and negative) + pub fn is_standard(&self) -> bool { + !self.is_custom() + } + + /// The standard planes are XY, YZ and XZ (in both positive and negative) + /// Custom planes are any other plane that the user might specify. + pub fn is_custom(&self) -> bool { + matches!(self.value, PlaneType::Custom) + } } #[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)] @@ -1049,6 +1060,14 @@ impl KclValue { } } + pub fn as_plane(&self) -> Option<&Plane> { + if let KclValue::Plane(value) = &self { + Some(value) + } else { + None + } + } + pub fn as_solid(&self) -> Option<&Solid> { if let KclValue::Solid(value) = &self { Some(value) diff --git a/src/wasm-lib/kcl/src/lsp/tests.rs b/src/wasm-lib/kcl/src/lsp/tests.rs index bb2f8991d..a2035733b 100644 --- a/src/wasm-lib/kcl/src/lsp/tests.rs +++ b/src/wasm-lib/kcl/src/lsp/tests.rs @@ -2,7 +2,7 @@ use std::collections::BTreeMap; use pretty_assertions::assert_eq; use tower_lsp::{ - lsp_types::{SemanticTokenModifier, SemanticTokenType}, + lsp_types::{Diagnostic, SemanticTokenModifier, SemanticTokenType}, LanguageServer, }; @@ -2369,7 +2369,14 @@ async fn kcl_test_kcl_lsp_diagnostics_on_execution_error() { // Get the diagnostics. let diagnostics = server.diagnostics_map.get("file:///test.kcl"); - assert!(diagnostics.is_none()); + if let Some(diagnostics) = diagnostics { + let ds: Vec = diagnostics.to_owned(); + eprintln!("Expected no diagnostics, but found some."); + for d in ds { + eprintln!("{:?}: {}", d.severity, d.message); + } + panic!(); + } } #[tokio::test(flavor = "multi_thread")] diff --git a/src/wasm-lib/kcl/src/std/args.rs b/src/wasm-lib/kcl/src/std/args.rs index 2910609ac..562c36c3c 100644 --- a/src/wasm-lib/kcl/src/std/args.rs +++ b/src/wasm-lib/kcl/src/std/args.rs @@ -794,6 +794,45 @@ impl<'a> FromKclValue<'a> for crate::std::planes::StandardPlane { } } +impl<'a> FromKclValue<'a> for crate::executor::Plane { + fn from_kcl_val(arg: &'a KclValue) -> Option { + if let Some(plane) = arg.as_plane() { + return Some(plane.clone()); + } + + let obj = arg.as_object()?; + let_field_of!(obj, id); + let_field_of!(obj, value); + let_field_of!(obj, origin); + let_field_of!(obj, x_axis "xAxis"); + let_field_of!(obj, y_axis "yAxis"); + let_field_of!(obj, z_axis "zAxis"); + let_field_of!(obj, meta "__meta"); + Some(Self { + id, + value, + origin, + x_axis, + y_axis, + z_axis, + meta, + }) + } +} + +impl<'a> FromKclValue<'a> for crate::executor::PlaneType { + fn from_kcl_val(arg: &'a KclValue) -> Option { + let plane_type = match arg.as_str()? { + "XY" | "xy" => Self::XY, + "XZ" | "xz" => Self::XZ, + "YZ" | "yz" => Self::YZ, + "Custom" => Self::Custom, + _ => return None, + }; + Some(plane_type) + } +} + impl<'a> FromKclValue<'a> for kittycad_modeling_cmds::units::UnitLength { fn from_kcl_val(arg: &'a KclValue) -> Option { let s = arg.as_str()?; @@ -1264,11 +1303,15 @@ impl<'a> FromKclValue<'a> for crate::executor::Solid { impl<'a> FromKclValue<'a> for super::sketch::SketchData { fn from_kcl_val(arg: &'a KclValue) -> Option { - let case1 = super::sketch::PlaneData::from_kcl_val; - let case2 = crate::executor::Solid::from_kcl_val; + // Order is critical since PlaneData is a subset of Plane. + let case1 = crate::executor::Plane::from_kcl_val; + let case2 = super::sketch::PlaneData::from_kcl_val; + let case3 = crate::executor::Solid::from_kcl_val; case1(arg) + .map(Box::new) .map(Self::Plane) - .or_else(|| case2(arg).map(Box::new).map(Self::Solid)) + .or_else(|| case2(arg).map(Self::PlaneOrientation)) + .or_else(|| case3(arg).map(Box::new).map(Self::Solid)) } } diff --git a/src/wasm-lib/kcl/src/std/planes.rs b/src/wasm-lib/kcl/src/std/planes.rs index e322185d2..072d99962 100644 --- a/src/wasm-lib/kcl/src/std/planes.rs +++ b/src/wasm-lib/kcl/src/std/planes.rs @@ -1,17 +1,20 @@ //! Standard library plane helpers. use derive_docs::stdlib; +use kcmc::{each_cmd as mcmd, length_unit::LengthUnit, shared::Color, ModelingCmd}; +use kittycad_modeling_cmds as kcmc; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use crate::{ errors::KclError, - executor::{ExecState, KclValue, Plane}, + executor::{ExecState, KclValue, Plane, PlaneType}, std::{sketch::PlaneData, Args}, }; /// One of the standard planes. -#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, JsonSchema)] +#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)] +#[ts(export)] #[serde(rename_all = "camelCase")] pub enum StandardPlane { /// The XY plane. @@ -50,8 +53,8 @@ impl From for PlaneData { /// Offset a plane by a distance along its normal. pub async fn offset_plane(exec_state: &mut ExecState, args: Args) -> Result { let (std_plane, offset): (StandardPlane, f64) = args.get_data_and_float()?; - let plane_data = inner_offset_plane(std_plane, offset, exec_state).await?; - let plane = Plane::from_plane_data(plane_data, exec_state); + let plane = inner_offset_plane(std_plane, offset, exec_state).await?; + make_offset_plane_in_engine(&plane, exec_state, &args).await?; Ok(KclValue::Plane(Box::new(plane))) } @@ -144,11 +147,14 @@ async fn inner_offset_plane( std_plane: StandardPlane, offset: f64, exec_state: &mut ExecState, -) -> Result { +) -> Result { // Convert to the plane type. let plane_data: PlaneData = std_plane.into(); // Convert to a plane. let mut plane = Plane::from_plane_data(plane_data, exec_state); + // Though offset planes are derived from standard planes, they are not + // standard planes themselves. + plane.value = PlaneType::Custom; match std_plane { StandardPlane::XY => { @@ -171,10 +177,44 @@ async fn inner_offset_plane( } } - Ok(PlaneData::Plane { - origin: Box::new(plane.origin), - x_axis: Box::new(plane.x_axis), - y_axis: Box::new(plane.y_axis), - z_axis: Box::new(plane.z_axis), - }) + Ok(plane) +} + +// Engine-side effectful creation of an actual plane object. +// offset planes are shown by default, and hidden by default if they +// are used as a sketch plane. That hiding command is sent within inner_start_profile_at +async fn make_offset_plane_in_engine(plane: &Plane, exec_state: &mut ExecState, args: &Args) -> Result<(), KclError> { + // Create new default planes. + let default_size = 100.0; + let color = Color { + r: 0.6, + g: 0.6, + b: 0.6, + a: 0.3, + }; + + args.batch_modeling_cmd( + plane.id, + ModelingCmd::from(mcmd::MakePlane { + clobber: false, + origin: plane.origin.into(), + size: LengthUnit(default_size), + x_axis: plane.x_axis.into(), + y_axis: plane.y_axis.into(), + hide: Some(false), + }), + ) + .await?; + + // Set the color. + args.batch_modeling_cmd( + exec_state.id_generator.next_uuid(), + ModelingCmd::from(mcmd::PlaneSetColor { + color, + plane_id: plane.id, + }), + ) + .await?; + + Ok(()) } diff --git a/src/wasm-lib/kcl/src/std/sketch.rs b/src/wasm-lib/kcl/src/std/sketch.rs index 05dad6d72..bcf089c17 100644 --- a/src/wasm-lib/kcl/src/std/sketch.rs +++ b/src/wasm-lib/kcl/src/std/sketch.rs @@ -894,7 +894,7 @@ pub async fn start_sketch_at(exec_state: &mut ExecState, args: Args) -> Result Result { // Let's assume it's the XY plane for now, this is just for backwards compatibility. let xy_plane = PlaneData::XY; - let sketch_surface = inner_start_sketch_on(SketchData::Plane(xy_plane), None, exec_state, &args).await?; + let sketch_surface = inner_start_sketch_on(SketchData::PlaneOrientation(xy_plane), None, exec_state, &args).await?; let sketch = inner_start_profile_at(data, sketch_surface, None, exec_state, args).await?; Ok(sketch) } @@ -905,11 +905,12 @@ async fn inner_start_sketch_at(data: [f64; 2], exec_state: &mut ExecState, args: #[ts(export)] #[serde(rename_all = "camelCase", untagged)] pub enum SketchData { - Plane(PlaneData), + PlaneOrientation(PlaneData), + Plane(Box), Solid(Box), } -/// Data for a plane. +/// Orientation data that can be used to construct a plane, not a plane in itself. #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)] #[ts(export)] #[serde(rename_all = "camelCase")] @@ -1069,10 +1070,11 @@ async fn inner_start_sketch_on( args: &Args, ) -> Result { match data { - SketchData::Plane(plane_data) => { - let plane = start_sketch_on_plane(plane_data, exec_state, args).await?; + SketchData::PlaneOrientation(plane_data) => { + let plane = make_sketch_plane_from_orientation(plane_data, exec_state, args).await?; Ok(SketchSurface::Plane(plane)) } + SketchData::Plane(plane) => Ok(SketchSurface::Plane(plane)), SketchData::Solid(solid) => { let Some(tag) = tag else { return Err(KclError::Type(KclErrorDetails { @@ -1106,7 +1108,7 @@ async fn start_sketch_on_face( })) } -async fn start_sketch_on_plane( +async fn make_sketch_plane_from_orientation( data: PlaneData, exec_state: &mut ExecState, args: &Args, @@ -1122,10 +1124,10 @@ async fn start_sketch_on_plane( plane.id = match data { PlaneData::XY => default_planes.xy, - PlaneData::XZ => default_planes.xz, - PlaneData::YZ => default_planes.yz, PlaneData::NegXY => default_planes.neg_xy, + PlaneData::XZ => default_planes.xz, PlaneData::NegXZ => default_planes.neg_xz, + PlaneData::YZ => default_planes.yz, PlaneData::NegYZ => default_planes.neg_yz, PlaneData::Plane { origin, @@ -1210,11 +1212,26 @@ pub(crate) async fn inner_start_profile_at( exec_state: &mut ExecState, args: Args, ) -> Result { - if let SketchSurface::Face(face) = &sketch_surface { - // Flush the batch for our fillets/chamfers if there are any. - // If we do not do these for sketch on face, things will fail with face does not exist. - args.flush_batch_for_solid_set(exec_state, face.solid.clone().into()) + match &sketch_surface { + SketchSurface::Face(face) => { + // Flush the batch for our fillets/chamfers if there are any. + // If we do not do these for sketch on face, things will fail with face does not exist. + args.flush_batch_for_solid_set(exec_state, face.solid.clone().into()) + .await?; + } + SketchSurface::Plane(plane) if !plane.is_standard() => { + // Hide whatever plane we are sketching on. + // This is especially helpful for offset planes, which would be visible otherwise. + args.batch_end_cmd( + exec_state.id_generator.next_uuid(), + ModelingCmd::from(mcmd::ObjectVisible { + object_id: plane.id, + hidden: true, + }), + ) .await?; + } + _ => {} } // Enter sketch mode on the surface.