Show offset planes in the scene, let user select them (#4481)

* Update offset_plane to actually create and show the plane in-engine

* Fix broken ability to use offsetPlanes in startSketchOn

* Make the newly-visible offset planes usable for sketching via UI

* Add a playwright test for sketching on an offset plane via point-and-click

* cargo clippy & cargo fmt

* Make `PlaneData` the first item of `SketchData` so autocomplete continues to work well for `startSketchOn`

* @nadr0 feedback re: `offsetIndex`

* From @jtran: "Need to call the ID generator so that IDs are stable."

* More feedback from @jtran and fix incomplete use of `id_generator` in last commit

* Oops I missed saving `isPathToNodeNumber` earlier 🤦🏻

* Make the distinction between `Plane` and `PlaneOrientationData` more clear per @nadr0 and @lf94's feedback

* Make `newPathToNode` less hardcoded, per @lf94's feedback

* Don't need to unbox and rebox `plane`

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

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

* Rearranging of enums and structs, but the offsetPlanes are still not used by their sketches

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

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

* Revert all my little newtype fiddling it's a waste of time.

* Update docs

* cargo fmt

* Remove log

* Print the unexpected diagnostics

* Undo renaming of `PlaneData`

* Remove generated PlaneRientationData docs page

* Redo doc generation after undoing `PlaneData` rename

* Impl FromKclValue for the new plane datatypes

* Clippy lint

* When starting a sketch, only hide the plane if it's a custom plane

* Fix FromKclValue and macro use since merge

* Fix to not convert Plane to PlaneData

* Make sure offset planes are `Custom` type

* SketchData actually doesn't need to be in a certain order
This avoids the autocompletion issue I was having.

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Adam Chalmers <adam.chalmers@zoo.dev>
Co-authored-by: Jonathan Tran <jonnytran@gmail.com>
This commit is contained in:
Frank Noirot
2024-11-18 16:25:25 -05:00
committed by GitHub
parent 97b9529c81
commit 24bc4fcd8c
21 changed files with 762 additions and 243 deletions

View File

@ -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. 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 ```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 ### Returns
[`PlaneData`](/docs/kcl/types/PlaneData) - Data for a plane. [`Plane`](/docs/kcl/types/Plane) - A plane.
### Examples ### Examples

View File

@ -105747,70 +105747,30 @@
], ],
"returnValue": { "returnValue": {
"name": "", "name": "",
"type": "PlaneData", "type": "Plane",
"schema": { "schema": {
"$schema": "https://spec.openapis.org/oas/3.0/schema/2019-04-02#/definitions/Schema", "$schema": "https://spec.openapis.org/oas/3.0/schema/2019-04-02#/definitions/Schema",
"title": "PlaneData", "title": "Plane",
"description": "Data for a plane.", "description": "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", "type": "object",
"required": [ "required": [
"__meta",
"id",
"origin", "origin",
"value",
"xAxis", "xAxis",
"yAxis", "yAxis",
"zAxis" "zAxis"
], ],
"properties": { "properties": {
"id": {
"description": "The id of the plane.",
"type": "string",
"format": "uuid"
},
"value": {
"$ref": "#/components/schemas/PlaneType"
},
"origin": { "origin": {
"description": "Origin of the plane.", "description": "Origin of the plane.",
"allOf": [ "allOf": [
@ -105842,14 +105802,35 @@
"$ref": "#/components/schemas/Point3d" "$ref": "#/components/schemas/Point3d"
} }
] ]
} },
"__meta": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Metadata"
} }
} }
}, },
"additionalProperties": false
}
],
"definitions": { "definitions": {
"PlaneType": {
"description": "Type for a plane.",
"oneOf": [
{
"type": "string",
"enum": [
"XY",
"XZ",
"YZ"
]
},
{
"description": "A custom plane.",
"type": "string",
"enum": [
"Custom"
]
}
]
},
"Point3d": { "Point3d": {
"type": "object", "type": "object",
"required": [ "required": [
@ -105871,6 +105852,33 @@
"format": "double" "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/PlaneData"
}, },
{
"$ref": "#/components/schemas/Plane"
},
{ {
"$ref": "#/components/schemas/Solid" "$ref": "#/components/schemas/Solid"
} }
], ],
"definitions": { "definitions": {
"PlaneData": { "PlaneData": {
"description": "Data for a plane.", "description": "Orientation data that can be used to construct a plane, not a plane in itself.",
"oneOf": [ "oneOf": [
{ {
"description": "The XY plane.", "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 planes X axis be?",
"allOf": [
{
"$ref": "#/components/schemas/Point3d"
}
]
},
"yAxis": {
"description": "What should the planes 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": { "Solid": {
"description": "An solid is a collection of extrude surfaces.", "description": "An solid is a collection of extrude surfaces.",
"type": "object", "type": "object",
@ -179444,16 +179563,6 @@
} }
} }
}, },
"SourceRange": {
"type": "array",
"items": {
"type": "integer",
"format": "uint",
"minimum": 0.0
},
"maxItems": 3,
"minItems": 3
},
"Sketch": { "Sketch": {
"description": "A sketch is a collection of paths.", "description": "A sketch is a collection of paths.",
"type": "object", "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": { "BasePath": {
"description": "A base path.", "description": "A base path.",
"type": "object", "type": "object",
@ -180460,7 +180532,7 @@
"nullable": true, "nullable": true,
"definitions": { "definitions": {
"PlaneData": { "PlaneData": {
"description": "Data for a plane.", "description": "Orientation data that can be used to construct a plane, not a plane in itself.",
"oneOf": [ "oneOf": [
{ {
"description": "The XY plane.", "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 planes X axis be?",
"allOf": [
{
"$ref": "#/components/schemas/Point3d"
}
]
},
"yAxis": {
"description": "What should the planes 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": { "Solid": {
"description": "An solid is a collection of extrude surfaces.", "description": "An solid is a collection of extrude surfaces.",
"type": "object", "type": "object",
@ -180862,16 +181042,6 @@
} }
} }
}, },
"SourceRange": {
"type": "array",
"items": {
"type": "integer",
"format": "uint",
"minimum": 0.0
},
"maxItems": 3,
"minItems": 3
},
"Sketch": { "Sketch": {
"description": "A sketch is a collection of paths.", "description": "A sketch is a collection of paths.",
"type": "object", "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": { "BasePath": {
"description": "A base path.", "description": "A base path.",
"type": "object", "type": "object",

View File

@ -180,7 +180,7 @@ A plane.
| Property | Type | Description | Required | | Property | Type | Description | Required |
|----------|------|-------------|----------| |----------|------|-------------|----------|
| `type` |enum: `Plane`| | No | | `type` |enum: [`Plane`](/docs/kcl/types/Plane)| | No |
| `id` |`string`| The id of the plane. | No | | `id` |`string`| The id of the plane. | No |
| `value` |[`PlaneType`](/docs/kcl/types/PlaneType)| Any KCL value. | No | | `value` |[`PlaneType`](/docs/kcl/types/PlaneType)| Any KCL value. | No |
| `origin` |[`Point3d`](/docs/kcl/types/Point3d)| Origin of the plane. | No | | `origin` |[`Point3d`](/docs/kcl/types/Point3d)| Origin of the plane. | No |

27
docs/kcl/types/Plane.md Normal file
View File

@ -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 planes X axis be? | No |
| `yAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the planes Y axis be? | No |
| `zAxis` |[`Point3d`](/docs/kcl/types/Point3d)| The z-axis (normal). | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |

View File

@ -1,10 +1,10 @@
--- ---
title: "PlaneData" 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 layout: manual
--- ---
Data for a plane. Orientation data that can be used to construct a plane, not a plane in itself.

View File

@ -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. Data for start sketch on. You can start a sketch on a plane or an solid.

View File

@ -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: '',
})
}
)
})
}
)
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 65 KiB

View File

@ -63,6 +63,7 @@ import {
import { import {
moveValueIntoNewVariablePath, moveValueIntoNewVariablePath,
sketchOnExtrudedFace, sketchOnExtrudedFace,
sketchOnOffsetPlane,
startSketchOnDefault, startSketchOnDefault,
} from 'lang/modifyAst' } from 'lang/modifyAst'
import { Program, parse, recast } from 'lang/wasm' import { Program, parse, recast } from 'lang/wasm'
@ -636,13 +637,16 @@ export const ModelingMachineProvider = ({
), ),
'animate-to-face': fromPromise(async ({ input }) => { 'animate-to-face': fromPromise(async ({ input }) => {
if (!input) return undefined if (!input) return undefined
if (input.type === 'extrudeFace') { if (input.type === 'extrudeFace' || input.type === 'offsetPlane') {
const sketched = sketchOnExtrudedFace( const sketched =
input.type === 'extrudeFace'
? sketchOnExtrudedFace(
kclManager.ast, kclManager.ast,
input.sketchPathToNode, input.sketchPathToNode,
input.extrudePathToNode, input.extrudePathToNode,
input.faceInfo input.faceInfo
) )
: sketchOnOffsetPlane(kclManager.ast, input.pathToNode)
if (err(sketched)) { if (err(sketched)) {
const sketchedError = new Error( const sketchedError = new Error(
'Incompatible face, please try another' 'Incompatible face, please try another'
@ -654,10 +658,9 @@ export const ModelingMachineProvider = ({
await kclManager.executeAstMock(modifiedAst) await kclManager.executeAstMock(modifiedAst)
await letEngineAnimateAndSyncCamAfter( const id =
engineCommandManager, input.type === 'extrudeFace' ? input.faceId : input.planeId
input.faceId await letEngineAnimateAndSyncCamAfter(engineCommandManager, id)
)
sceneInfra.camControls.syncDirection = 'clientToEngine' sceneInfra.camControls.syncDirection = 'clientToEngine'
return { return {
sketchPathToNode: pathToNewSketchNode, sketchPathToNode: pathToNewSketchNode,

View File

@ -88,6 +88,10 @@ export function useEngineConnectionSubscriptions() {
? [codeRef.range] ? [codeRef.range]
: [codeRef.range, consumedCodeRef.range] : [codeRef.range, consumedCodeRef.range]
) )
} else if (artifact?.type === 'plane') {
const codeRef = artifact.codeRef
if (err(codeRef)) return
editorManager.setHighlightRange([codeRef.range])
} else { } else {
editorManager.setHighlightRange([[0, 0]]) editorManager.setHighlightRange([[0, 0]])
} }
@ -186,8 +190,42 @@ export function useEngineConnectionSubscriptions() {
}) })
return 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 faceId = planeOrFaceId
const artifact = engineCommandManager.artifactGraph.get(faceId)
const extrusion = getSweepFromSuspectedSweepSurface( const extrusion = getSweepFromSuspectedSweepSurface(
faceId, faceId,
engineCommandManager.artifactGraph engineCommandManager.artifactGraph

View File

@ -19,6 +19,7 @@ import {
ProgramMemory, ProgramMemory,
SourceRange, SourceRange,
sketchFromKclValue, sketchFromKclValue,
isPathToNodeNumber,
} from './wasm' } from './wasm'
import { import {
isNodeSafeToReplacePath, 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<Program>,
offsetPathToNode: PathToNode
) {
let _node = { ...node }
// Find the offset plane declaration
const offsetPlaneDeclarator = getNodeFromPath<VariableDeclarator>(
_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 => export const getLastIndex = (pathToNode: PathToNode): number =>
splitPathAtLastIndex(pathToNode).index splitPathAtLastIndex(pathToNode).index

View File

@ -98,12 +98,22 @@ sketch004 = startSketchOn(extrude003, seg02)
|> close(%) |> close(%)
extrude004 = extrude(3, sketch004) 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 // add more code snippets here and use `getCommands` to get the orderedCommands and responseMap for more tests
const codeToWriteCacheFor = { const codeToWriteCacheFor = {
exampleCode1, exampleCode1,
sketchOnFaceOnFaceEtc, sketchOnFaceOnFaceEtc,
exampleCodeNo3D, exampleCodeNo3D,
exampleCodeOffsetPlanes,
} as const } as const
type CodeKey = keyof typeof codeToWriteCacheFor type CodeKey = keyof typeof codeToWriteCacheFor
@ -165,6 +175,52 @@ afterAll(() => {
}) })
describe('testing createArtifactGraph', () => { describe('testing createArtifactGraph', () => {
describe('code with offset planes and a sketch:', () => {
let ast: Program
let theMap: ReturnType<typeof createArtifactGraph>
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:', () => { describe('code with an extrusion, fillet and sketch of face:', () => {
let ast: Program let ast: Program
let theMap: ReturnType<typeof createArtifactGraph> let theMap: ReturnType<typeof createArtifactGraph>

View File

@ -249,7 +249,20 @@ export function getArtifactsToUpdate({
const cmd = command.cmd const cmd = command.cmd
const returnArr: ReturnType<typeof getArtifactsToUpdate> = [] const returnArr: ReturnType<typeof getArtifactsToUpdate> = []
if (!response) return returnArr 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 plane = getArtifact(currentPlaneId)
const pathIds = plane?.type === 'plane' ? plane?.pathIds : [] const pathIds = plane?.type === 'plane' ? plane?.pathIds : []
const codeRef = const codeRef =

View File

@ -144,6 +144,12 @@ export const parse = (code: string | Error): Node<Program> | Error => {
export type PathToNode = [string | number, string][] export type PathToNode = [string | number, string][]
export const isPathToNodeNumber = (
pathToNode: string | number
): pathToNode is number => {
return typeof pathToNode === 'number'
}
export interface ExecState { export interface ExecState {
memory: ProgramMemory memory: ProgramMemory
idGenerator: IdGenerator idGenerator: IdGenerator

View File

@ -159,6 +159,15 @@ export type DefaultPlane = {
yAxis: [number, number, number] 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 = export type SegmentOverlayPayload =
| { | {
type: 'set-one' type: 'set-one'
@ -198,7 +207,7 @@ export type ModelingMachineEvent =
| { type: 'Sketch On Face' } | { type: 'Sketch On Face' }
| { | {
type: 'Select default plane' type: 'Select default plane'
data: DefaultPlane | ExtrudeFacePlane data: DefaultPlane | ExtrudeFacePlane | OffsetPlane
} }
| { | {
type: 'Set selection' type: 'Set selection'
@ -1394,7 +1403,7 @@ export const modelingMachine = setup({
} }
), ),
'animate-to-face': fromPromise( 'animate-to-face': fromPromise(
async (_: { input?: ExtrudeFacePlane | DefaultPlane }) => { async (_: { input?: ExtrudeFacePlane | DefaultPlane | OffsetPlane }) => {
return {} as return {} as
| undefined | undefined
| { | {

View File

@ -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)] #[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> { pub fn as_solid(&self) -> Option<&Solid> {
if let KclValue::Solid(value) = &self { if let KclValue::Solid(value) = &self {
Some(value) Some(value)

View File

@ -2,7 +2,7 @@ use std::collections::BTreeMap;
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
use tower_lsp::{ use tower_lsp::{
lsp_types::{SemanticTokenModifier, SemanticTokenType}, lsp_types::{Diagnostic, SemanticTokenModifier, SemanticTokenType},
LanguageServer, LanguageServer,
}; };
@ -2369,7 +2369,14 @@ async fn kcl_test_kcl_lsp_diagnostics_on_execution_error() {
// Get the diagnostics. // Get the diagnostics.
let diagnostics = server.diagnostics_map.get("file:///test.kcl"); let diagnostics = server.diagnostics_map.get("file:///test.kcl");
assert!(diagnostics.is_none()); if let Some(diagnostics) = diagnostics {
let ds: Vec<Diagnostic> = 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")] #[tokio::test(flavor = "multi_thread")]

View File

@ -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<Self> {
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<Self> {
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 { impl<'a> FromKclValue<'a> for kittycad_modeling_cmds::units::UnitLength {
fn from_kcl_val(arg: &'a KclValue) -> Option<Self> { fn from_kcl_val(arg: &'a KclValue) -> Option<Self> {
let s = arg.as_str()?; 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 { impl<'a> FromKclValue<'a> for super::sketch::SketchData {
fn from_kcl_val(arg: &'a KclValue) -> Option<Self> { fn from_kcl_val(arg: &'a KclValue) -> Option<Self> {
let case1 = super::sketch::PlaneData::from_kcl_val; // Order is critical since PlaneData is a subset of Plane.
let case2 = crate::executor::Solid::from_kcl_val; 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) case1(arg)
.map(Box::new)
.map(Self::Plane) .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))
} }
} }

View File

@ -1,17 +1,20 @@
//! Standard library plane helpers. //! Standard library plane helpers.
use derive_docs::stdlib; 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 schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::{ use crate::{
errors::KclError, errors::KclError,
executor::{ExecState, KclValue, Plane}, executor::{ExecState, KclValue, Plane, PlaneType},
std::{sketch::PlaneData, Args}, std::{sketch::PlaneData, Args},
}; };
/// One of the standard planes. /// 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")] #[serde(rename_all = "camelCase")]
pub enum StandardPlane { pub enum StandardPlane {
/// The XY plane. /// The XY plane.
@ -50,8 +53,8 @@ impl From<StandardPlane> for PlaneData {
/// Offset a plane by a distance along its normal. /// Offset a plane by a distance along its normal.
pub async fn offset_plane(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> { pub async fn offset_plane(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let (std_plane, offset): (StandardPlane, f64) = args.get_data_and_float()?; 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 = inner_offset_plane(std_plane, offset, exec_state).await?;
let plane = Plane::from_plane_data(plane_data, exec_state); make_offset_plane_in_engine(&plane, exec_state, &args).await?;
Ok(KclValue::Plane(Box::new(plane))) Ok(KclValue::Plane(Box::new(plane)))
} }
@ -144,11 +147,14 @@ async fn inner_offset_plane(
std_plane: StandardPlane, std_plane: StandardPlane,
offset: f64, offset: f64,
exec_state: &mut ExecState, exec_state: &mut ExecState,
) -> Result<PlaneData, KclError> { ) -> Result<Plane, KclError> {
// Convert to the plane type. // Convert to the plane type.
let plane_data: PlaneData = std_plane.into(); let plane_data: PlaneData = std_plane.into();
// Convert to a plane. // Convert to a plane.
let mut plane = Plane::from_plane_data(plane_data, exec_state); 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 { match std_plane {
StandardPlane::XY => { StandardPlane::XY => {
@ -171,10 +177,44 @@ async fn inner_offset_plane(
} }
} }
Ok(PlaneData::Plane { Ok(plane)
origin: Box::new(plane.origin), }
x_axis: Box::new(plane.x_axis),
y_axis: Box::new(plane.y_axis), // Engine-side effectful creation of an actual plane object.
z_axis: Box::new(plane.z_axis), // 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(())
} }

View File

@ -894,7 +894,7 @@ pub async fn start_sketch_at(exec_state: &mut ExecState, args: Args) -> Result<K
async fn inner_start_sketch_at(data: [f64; 2], exec_state: &mut ExecState, args: Args) -> Result<Sketch, KclError> { async fn inner_start_sketch_at(data: [f64; 2], exec_state: &mut ExecState, args: Args) -> Result<Sketch, KclError> {
// Let's assume it's the XY plane for now, this is just for backwards compatibility. // Let's assume it's the XY plane for now, this is just for backwards compatibility.
let xy_plane = PlaneData::XY; 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?; let sketch = inner_start_profile_at(data, sketch_surface, None, exec_state, args).await?;
Ok(sketch) Ok(sketch)
} }
@ -905,11 +905,12 @@ async fn inner_start_sketch_at(data: [f64; 2], exec_state: &mut ExecState, args:
#[ts(export)] #[ts(export)]
#[serde(rename_all = "camelCase", untagged)] #[serde(rename_all = "camelCase", untagged)]
pub enum SketchData { pub enum SketchData {
Plane(PlaneData), PlaneOrientation(PlaneData),
Plane(Box<Plane>),
Solid(Box<Solid>), Solid(Box<Solid>),
} }
/// 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)] #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)] #[ts(export)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
@ -1069,10 +1070,11 @@ async fn inner_start_sketch_on(
args: &Args, args: &Args,
) -> Result<SketchSurface, KclError> { ) -> Result<SketchSurface, KclError> {
match data { match data {
SketchData::Plane(plane_data) => { SketchData::PlaneOrientation(plane_data) => {
let plane = start_sketch_on_plane(plane_data, exec_state, args).await?; let plane = make_sketch_plane_from_orientation(plane_data, exec_state, args).await?;
Ok(SketchSurface::Plane(plane)) Ok(SketchSurface::Plane(plane))
} }
SketchData::Plane(plane) => Ok(SketchSurface::Plane(plane)),
SketchData::Solid(solid) => { SketchData::Solid(solid) => {
let Some(tag) = tag else { let Some(tag) = tag else {
return Err(KclError::Type(KclErrorDetails { 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, data: PlaneData,
exec_state: &mut ExecState, exec_state: &mut ExecState,
args: &Args, args: &Args,
@ -1122,10 +1124,10 @@ async fn start_sketch_on_plane(
plane.id = match data { plane.id = match data {
PlaneData::XY => default_planes.xy, PlaneData::XY => default_planes.xy,
PlaneData::XZ => default_planes.xz,
PlaneData::YZ => default_planes.yz,
PlaneData::NegXY => default_planes.neg_xy, PlaneData::NegXY => default_planes.neg_xy,
PlaneData::XZ => default_planes.xz,
PlaneData::NegXZ => default_planes.neg_xz, PlaneData::NegXZ => default_planes.neg_xz,
PlaneData::YZ => default_planes.yz,
PlaneData::NegYZ => default_planes.neg_yz, PlaneData::NegYZ => default_planes.neg_yz,
PlaneData::Plane { PlaneData::Plane {
origin, origin,
@ -1210,12 +1212,27 @@ pub(crate) async fn inner_start_profile_at(
exec_state: &mut ExecState, exec_state: &mut ExecState,
args: Args, args: Args,
) -> Result<Sketch, KclError> { ) -> Result<Sketch, KclError> {
if let SketchSurface::Face(face) = &sketch_surface { match &sketch_surface {
SketchSurface::Face(face) => {
// Flush the batch for our fillets/chamfers if there are any. // 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. // 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()) args.flush_batch_for_solid_set(exec_state, face.solid.clone().into())
.await?; .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. // Enter sketch mode on the surface.
// We call this here so you can reuse the sketch surface for multiple sketches. // We call this here so you can reuse the sketch surface for multiple sketches.