Compare commits

...

6 Commits

Author SHA1 Message Date
cbfe3db203 Add simplified walkie talkie test 2025-02-26 13:41:23 -05:00
615b7feabb Don't toss logs on successful snapshot tests (#5522) 2025-02-26 12:09:38 -05:00
5743b9ced0 Correct hovering highlights on HiDPI screens + correct 2 flakey tests (#5510)
* Fix hover highlights on HiDPI screens

* Fix flakey tests with new toolbar.exitSketch

* tsc && lint && fmt

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

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

* Disable pw electron thing again

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

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

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

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

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-02-26 11:12:08 -05:00
8896d06028 Release KCL 39 (#5518) 2025-02-26 09:07:57 -06:00
max
1f217ef50b Fix Second-Body Extrude Selection (#5456)
* getSweepArtifactFromSelection

* update getPathToExtrudeForSegmentSelection

* update shell

* add tests and update selection

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

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

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

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

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

* add support for wall and cap

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

* fmt

* add CallExpressionKw

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

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

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-02-26 08:28:41 -05:00
5ef5c6280c Fix: revert the red color for runtime error back to the hue shift color (#5509)
* fix: don't use red for runtime error, use hue shift like the original error icon

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

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

* fix: decrease font size for better layout

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

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

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-02-25 22:15:01 -06:00
35 changed files with 3310 additions and 127 deletions

View File

@ -142,7 +142,7 @@ jobs:
# TODO: break this in its own job, for now it's not slowing down the overall execution as ubuntu is the quickest,
# but we could do better. This forces a large 1/1 shard of all 20 snapshot tests that runs in about 3 minutes.
run: |
PLATFORM=web yarn playwright test --config=playwright.config.ts --retries="3" --update-snapshots --grep=@snapshot --shard=1/1
PLATFORM=web yarn playwright test --config=playwright.config.ts --retries="3" --update-snapshots --grep=@snapshot --trace=on --shard=1/1
env:
CI: true
NODE_ENV: development
@ -153,7 +153,7 @@ jobs:
- uses: actions/upload-artifact@v4
if: ${{ !cancelled() && (success() || failure()) }}
with:
name: playwright-report-${{ matrix.os }}-snapshot-${{ matrix.shardIndex }}-${{ github.sha }}
name: playwright-report-snapshots-${{ matrix.os }}-snapshot-${{ matrix.shardIndex }}-${{ github.sha }}
path: playwright-report/
include-hidden-files: true
retention-days: 30

View File

@ -82,6 +82,16 @@ export class ToolbarFixture {
startSketchPlaneSelection = async () =>
doAndWaitForImageDiff(this.page, () => this.startSketchBtn.click(), 500)
exitSketch = async () => {
await this.exitSketchBtn.click()
await expect(
this.page.getByRole('button', { name: 'Start Sketch' })
).toBeVisible()
await expect(
this.page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled()
}
editSketch = async () => {
await this.editSketchBtn.first().click()
// One of the rare times we want to allow a arbitrary wait

View File

@ -170,8 +170,7 @@ test.describe('Point-and-click tests', { tag: ['@skipWin'] }, () => {
})
await test.step('Clean up so that `_sketchOnAChamfer` util can be called again', async () => {
await toolbar.exitSketchBtn.click()
await scene.waitForExecutionDone()
await toolbar.exitSketch()
})
await test.step('Check there is no errors after code created in previous steps executes', async () => {
await editor.expectState({
@ -202,7 +201,9 @@ test.describe('Point-and-click tests', { tag: ['@skipWin'] }, () => {
}, file)
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
await scene.waitForExecutionDone()
await expect(
page.getByTestId('model-state-indicator-receive-reliable')
).toBeVisible()
const sketchOnAChamfer = _sketchOnAChamfer(page, editor, toolbar, scene)
@ -390,6 +391,7 @@ profile001 = startProfileAt([205.96, 254.59], sketch002)
}, file)
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
await scene.waitForExecutionDone()
const sketchOnAChamfer = _sketchOnAChamfer(page, editor, toolbar, scene)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 55 KiB

View File

@ -775,7 +775,7 @@ profile003 = startProfileAt([40.16, -120.48], sketch006)
)
`)
await expect(
page.getByTestId('model-state-indicator-execution-done')
page.getByTestId('model-state-indicator-receive-reliable')
).toBeVisible()
await u.openAndClearDebugPanel()

View File

@ -22,7 +22,7 @@ import {
UnreliableSubscription,
} from 'lang/std/engineConnection'
import { EngineCommand } from 'lang/std/artifactGraph'
import { toSync, uuidv4 } from 'lib/utils'
import { toSync, uuidv4, getNormalisedCoordinates } from 'lib/utils'
import { deg2Rad } from 'lib/utils2d'
import { isReducedMotion, roundOff, throttle } from 'lib/utils'
import * as TWEEN from '@tweenjs/tween.js'
@ -109,6 +109,7 @@ export class CameraControls {
interactionGuards: MouseGuard = cameraMouseDragGuards.Zoo
isFovAnimationInProgress = false
perspectiveFovBeforeOrtho = 45
// NOTE: Duplicated state across Provider and singleton. Mapped from settingsMachine
_setting_allowOrbitInSketchMode = false
get isPerspective() {
@ -456,11 +457,19 @@ export class CameraControls {
if (this.syncDirection === 'engineToClient') {
const newCmdId = uuidv4()
// Nonsense to do anything until the video stream is established.
if (!this.engineCommandManager.elVideo) return
const { x, y } = getNormalisedCoordinates(
event,
this.engineCommandManager.elVideo,
this.engineCommandManager.streamDimensions
)
this.throttledEngCmd({
type: 'modeling_cmd_req',
cmd: {
type: 'highlight_set_entity',
selected_at_window: { x: event.clientX, y: event.clientY },
selected_at_window: { x, y },
},
cmd_id: newCmdId,
})

View File

@ -309,7 +309,7 @@ const FileTreeItem = ({
{hasRuntimeError && (
<p
className={
'absolute m-0 p-0 bottom-3 left-6 w-3 h-3 flex items-center justify-center text-[9px] font-semibold text-white bg-red-600 rounded-full border border-red-300 dark:border-red-800 z-50 hover:cursor-pointer hover:scale-[2] transition-transform duration-200'
'absolute m-0 p-0 bottom-3 left-6 w-3 h-3 flex items-center justify-center text-[9px] font-semibold text-white bg-primary hue-rotate-90 rounded-full border border-chalkboard-10 dark:border-chalkboard-80 z-50 hover:cursor-pointer hover:scale-[2] transition-transform duration-200'
}
title={`Click to view notifications`}
>

View File

@ -171,7 +171,7 @@ export const sidebarPanes: SidebarPane[] = [
// editorManager.scrollToFirstErrorDiagnosticIfExists()
},
className:
'absolute m-0 p-0 bottom-4 left-4 w-3 h-3 flex items-center justify-center text-[9px] font-semibold text-white bg-red-600 rounded-full border border-red-300 dark:border-red-800 z-50 hover:cursor-pointer hover:scale-[2] transition-transform duration-200',
'absolute m-0 p-0 bottom-4 left-4 w-3 h-3 flex items-center justify-center text-[9px] font-semibold text-white bg-primary hue-rotate-90 rounded-full border border-chalkboard-10 dark:border-chalkboard-80 z-50 hover:cursor-pointer hover:scale-[2] transition-transform duration-200',
title: 'Project files have runtime errors',
},
},

View File

@ -47,6 +47,8 @@ export const Stream = () => {
overallState === NetworkHealthState.Ok ||
overallState === NetworkHealthState.Weak
engineCommandManager.elVideo = videoRef.current
/**
* Execute code and show a "building scene message"
* in Stream.tsx in the meantime.
@ -272,7 +274,7 @@ export const Stream = () => {
if (btnName(e.nativeEvent).left) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
sendSelectEventToEngine(e, videoRef.current)
sendSelectEventToEngine(e)
}
}
@ -294,7 +296,7 @@ export const Stream = () => {
return
}
sendSelectEventToEngine(e, videoRef.current)
sendSelectEventToEngine(e)
.then(({ entity_id }) => {
if (!entity_id) {
// No entity selected. This is benign

View File

@ -101,10 +101,7 @@ export function useSetupEngineManager(
streamRef?.current?.offsetWidth ?? 0,
streamRef?.current?.offsetHeight ?? 0
)
engineCommandManager.handleResize({
streamWidth: width,
streamHeight: height,
})
engineCommandManager.handleResize(engineCommandManager.streamDimensions)
}, 500)
const onOnline = () => {

View File

@ -116,7 +116,11 @@ const runGetPathToExtrudeForSegmentSelectionTest = async (
}
if (!extrudeInSketchPipe) {
const init = expectedExtrudeNode.init
if (init.type !== 'CallExpression' && init.type !== 'PipeExpression') {
if (
init.type !== 'CallExpression' &&
init.type !== 'CallExpressionKw' &&
init.type !== 'PipeExpression'
) {
return new Error(
'Expected extrude expression is not a CallExpression or PipeExpression'
)
@ -129,25 +133,33 @@ const runGetPathToExtrudeForSegmentSelectionTest = async (
// ast
const ast = assertParse(code)
// selection
// range
const segmentRange = topLevelRange(
code.indexOf(selectedSegmentSnippet),
code.indexOf(selectedSegmentSnippet) + selectedSegmentSnippet.length
)
const selection: Selection = {
codeRef: codeRefFromRange(segmentRange, ast),
}
// executeAst and artifactGraph
await kclManager.executeAst({ ast })
const artifactGraph = engineCommandManager.artifactGraph
// find artifact
const maybeArtifact = [...artifactGraph].find(([, artifact]) => {
if (!('codeRef' in artifact && artifact.codeRef)) return false
return isOverlap(artifact.codeRef.range, segmentRange)
})
// build selection
const selection: Selection = {
codeRef: codeRefFromRange(segmentRange, ast),
artifact: maybeArtifact ? maybeArtifact[1] : undefined,
}
// get extrude expression
const pathResult = getPathToExtrudeForSegmentSelection(
ast,
selection,
artifactGraph,
dependencies
artifactGraph
)
if (err(pathResult)) return pathResult
const { pathToExtrudeNode } = pathResult
@ -234,6 +246,56 @@ extrude003 = extrude(sketch003, length = -15)`
expectedExtrudeSnippet
)
})
it('should return the correct paths for a (piped) extrude based on the other body (face)', async () => {
const code = `sketch001 = startSketchOn('XY')
|> startProfileAt([-25, -25], %)
|> yLine(50, %)
|> xLine(50, %)
|> yLine(-50, %)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
|> extrude(length = 50)
sketch002 = startSketchOn(sketch001, 'END')
|> startProfileAt([-15, -15], %)
|> yLine(30, %)
|> xLine(30, %)
|> yLine(-30, %)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
|> extrude(length = 30)`
const selectedSegmentSnippet = `xLine(30, %)`
const expectedExtrudeSnippet = `extrude(length = 30)`
await runGetPathToExtrudeForSegmentSelectionTest(
code,
selectedSegmentSnippet,
expectedExtrudeSnippet
)
})
it('should return the correct paths for a (non-piped) extrude based on the other body (face)', async () => {
const code = `sketch001 = startSketchOn('XY')
|> startProfileAt([-25, -25], %)
|> yLine(50, %)
|> xLine(50, %)
|> yLine(-50, %)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
extrude001 = extrude(sketch001, length = 50)
sketch002 = startSketchOn(extrude001, 'END')
|> startProfileAt([-15, -15], %)
|> yLine(30, %)
|> xLine(30, %)
|> yLine(-30, %)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
extrude002 = extrude(sketch002, length = 30)`
const selectedSegmentSnippet = `xLine(30, %)`
const expectedExtrudeSnippet = `extrude002 = extrude(sketch002, length = 30)`
await runGetPathToExtrudeForSegmentSelectionTest(
code,
selectedSegmentSnippet,
expectedExtrudeSnippet
)
})
it('should not return any path for missing extrusion', async () => {
const code = `sketch001 = startSketchOn('XY')
|> startProfileAt([-30, 30], %)

View File

@ -10,7 +10,6 @@ import {
Program,
VariableDeclaration,
VariableDeclarator,
sketchFromKclValue,
} from '../wasm'
import {
createCallExpressionStdLib,
@ -35,11 +34,11 @@ import {
sketchLineHelperMap,
sketchLineHelperMapKw,
} from '../std/sketch'
import { err, trap } from 'lib/trap'
import { err } from 'lib/trap'
import { Selection, Selections } from 'lib/selections'
import { KclCommandValue } from 'lib/commandTypes'
import { isArray } from 'lib/utils'
import { Artifact, getSweepFromSuspectedPath } from 'lang/std/artifactGraph'
import { Artifact, getSweepArtifactFromSelection } from 'lang/std/artifactGraph'
import { Node } from 'wasm-lib/kcl/bindings/Node'
import { findKwArg } from 'lang/util'
import { KclManager } from 'lang/KclSingleton'
@ -121,8 +120,7 @@ export function modifyAstWithEdgeTreatmentAndTag(
const result = getPathToExtrudeForSegmentSelection(
clonedAstForGetExtrude,
selection,
artifactGraph,
dependencies
artifactGraph
)
if (err(result)) return result
const { pathToSegmentNode, pathToExtrudeNode } = result
@ -279,39 +277,19 @@ function insertParametersIntoAst(
export function getPathToExtrudeForSegmentSelection(
ast: Program,
selection: Selection,
artifactGraph: ArtifactGraph,
dependencies: {
kclManager: KclManager
engineCommandManager: EngineCommandManager
editorManager: EditorManager
codeManager: CodeManager
}
artifactGraph: ArtifactGraph
): { pathToSegmentNode: PathToNode; pathToExtrudeNode: PathToNode } | Error {
const pathToSegmentNode = getNodePathFromSourceRange(
ast,
selection.codeRef?.range
)
const varDecNode = getNodeFromPath<VariableDeclaration>(
ast,
pathToSegmentNode,
'VariableDeclaration'
)
if (err(varDecNode)) return varDecNode
const sketchVar = varDecNode.node.declaration.id.name
const sketch = sketchFromKclValue(
dependencies.kclManager.variables[sketchVar],
sketchVar
)
if (trap(sketch)) return sketch
const extrusion = getSweepFromSuspectedPath(sketch.id, artifactGraph)
if (err(extrusion)) return extrusion
const sweepArtifact = getSweepArtifactFromSelection(selection, artifactGraph)
if (err(sweepArtifact)) return sweepArtifact
const pathToExtrudeNode = getNodePathFromSourceRange(
ast,
extrusion.codeRef.range
sweepArtifact.codeRef.range
)
if (err(pathToExtrudeNode)) return pathToExtrudeNode

View File

@ -13,36 +13,23 @@ import {
createLiteral,
createIdentifier,
findUniqueName,
createCallExpressionStdLib,
createObjectExpression,
createArrayExpression,
createVariableDeclaration,
createCallExpressionStdLibKw,
createLabeledArg,
} from 'lang/modifyAst'
import { KCL_DEFAULT_CONSTANT_PREFIXES } from 'lib/constants'
import { KclManager } from 'lang/KclSingleton'
import { EngineCommandManager } from 'lang/std/engineConnection'
import EditorManager from 'editor/manager'
import CodeManager from 'lang/codeManager'
export function addShell({
node,
selection,
artifactGraph,
thickness,
dependencies,
}: {
node: Node<Program>
selection: Selections
artifactGraph: ArtifactGraph
thickness: Expr
dependencies: {
kclManager: KclManager
engineCommandManager: EngineCommandManager
editorManager: EditorManager
codeManager: CodeManager
}
}): Error | { modifiedAst: Node<Program>; pathToNode: PathToNode } {
const modifiedAst = structuredClone(node)
@ -55,8 +42,7 @@ export function addShell({
const extrudeLookupResult = getPathToExtrudeForSegmentSelection(
clonedAstForGetExtrude,
graphSelection,
artifactGraph,
dependencies
artifactGraph
)
if (err(extrudeLookupResult)) {
return new Error("Couldn't find extrude")

View File

@ -18,6 +18,7 @@ import {
} from 'lang/wasm'
import { Models } from '@kittycad/lib'
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
import { Selection } from 'lib/selections'
import { err } from 'lib/trap'
import { Cap, Plane, Wall } from 'wasm-lib/kcl/bindings/Artifact'
import { CapSubType } from 'wasm-lib/kcl/bindings/Artifact'
@ -455,6 +456,47 @@ export function getSweepFromSuspectedPath(
)
}
export function getSweepArtifactFromSelection(
selection: Selection,
artifactGraph: ArtifactGraph
): SweepArtifact | Error {
let sweepArtifact: Artifact | null = null
if (selection.artifact?.type === 'sweepEdge') {
const _artifact = getArtifactOfTypes(
{ key: selection.artifact.sweepId, types: ['sweep'] },
artifactGraph
)
if (err(_artifact)) return _artifact
sweepArtifact = _artifact
} else if (selection.artifact?.type === 'segment') {
const _pathArtifact = getArtifactOfTypes(
{ key: selection.artifact.pathId, types: ['path'] },
artifactGraph
)
if (err(_pathArtifact)) return _pathArtifact
if (!_pathArtifact.sweepId) return new Error('Path does not have a sweepId')
const _artifact = getArtifactOfTypes(
{ key: _pathArtifact.sweepId, types: ['sweep'] },
artifactGraph
)
if (err(_artifact)) return _artifact
sweepArtifact = _artifact
} else if (
selection.artifact?.type === 'cap' ||
selection.artifact?.type === 'wall'
) {
const _artifact = getArtifactOfTypes(
{ key: selection.artifact.sweepId, types: ['sweep'] },
artifactGraph
)
if (err(_artifact)) return _artifact
sweepArtifact = _artifact
}
if (!sweepArtifact) return new Error('No sweep artifact found')
return sweepArtifact
}
export function getCodeRefsByArtifactId(
id: string,
artifactGraph: ArtifactGraph

View File

@ -1447,11 +1447,17 @@ export class EngineCommandManager extends EventTarget {
commandId: string
}
settings: SettingsViaQueryString
width: number = 1337
height: number = 1337
streamDimensions = {
// Random defaults that are overwritten pretty much immediately
width: 1337,
height: 1337,
}
elVideo: HTMLVideoElement | null = null
/**
* Export intent traxcks the intent of the export. If it is null there is no
* Export intent tracks the intent of the export. If it is null there is no
* export in progress. Otherwise it is an enum value of the intent.
* Another export cannot be started if one is already in progress.
*/
@ -1554,15 +1560,14 @@ export class EngineCommandManager extends EventTarget {
return
}
this.width = width
this.height = height
this.streamDimensions = {
width,
height,
}
// If we already have an engine connection, just need to resize the stream.
if (this.engineConnection) {
this.handleResize({
streamWidth: width,
streamHeight: height,
})
this.handleResize(this.streamDimensions)
return
}
@ -1858,27 +1863,22 @@ export class EngineCommandManager extends EventTarget {
return
}
handleResize({
streamWidth,
streamHeight,
}: {
streamWidth: number
streamHeight: number
}) {
handleResize({ width, height }: { width: number; height: number }) {
if (!this.engineConnection?.isReady()) {
return
}
this.width = streamWidth
this.height = streamHeight
this.streamDimensions = {
width,
height,
}
const resizeCmd: EngineCommand = {
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'reconfigure_stream',
width: streamWidth,
height: streamHeight,
...this.streamDimensions,
fps: 60,
},
}

View File

@ -646,16 +646,17 @@ export function codeToIdSelections(
}
export async function sendSelectEventToEngine(
e: MouseEvent | React.MouseEvent<HTMLDivElement, MouseEvent>,
el: HTMLVideoElement
e: React.MouseEvent<HTMLDivElement, MouseEvent>
) {
const { x, y } = getNormalisedCoordinates({
clientX: e.clientX,
clientY: e.clientY,
el,
streamWidth: engineCommandManager.width,
streamHeight: engineCommandManager.height,
})
// No video stream to normalise against, return immediately
if (!engineCommandManager.elVideo)
return Promise.reject('video element not ready')
const { x, y } = getNormalisedCoordinates(
e,
engineCommandManager.elVideo,
engineCommandManager.streamDimensions
)
const res = await engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd: {

View File

@ -161,25 +161,20 @@ export function toSync<F extends AsyncFn<F>>(
}
}
export function getNormalisedCoordinates({
clientX,
clientY,
streamWidth,
streamHeight,
el,
}: {
clientX: number
clientY: number
streamWidth: number
streamHeight: number
el: HTMLElement
}) {
const { left, top, width, height } = el?.getBoundingClientRect()
const browserX = clientX - left
const browserY = clientY - top
export function getNormalisedCoordinates(
e: PointerEvent | React.MouseEvent<HTMLDivElement, MouseEvent>,
elVideo: HTMLVideoElement,
streamDimensions: {
width: number
height: number
}
) {
const { left, top, width, height } = elVideo?.getBoundingClientRect()
const browserX = e.clientX - left
const browserY = e.clientY - top
return {
x: Math.round((browserX / width) * streamWidth),
y: Math.round((browserY / height) * streamHeight),
x: Math.round((browserX / width) * streamDimensions.width),
y: Math.round((browserY / height) * streamDimensions.height),
}
}

View File

@ -1995,12 +1995,6 @@ export const modelingMachine = setup({
// Extract inputs
const ast = kclManager.ast
const { selection, thickness } = input
const dependencies = {
kclManager,
engineCommandManager,
editorManager,
codeManager,
}
// Insert the thickness variable if it exists
if (
@ -2026,7 +2020,6 @@ export const modelingMachine = setup({
'variableName' in thickness
? thickness.variableIdentifierAst
: thickness.valueAst,
dependencies,
})
if (err(shellResult)) {
return err(shellResult)

View File

@ -730,7 +730,7 @@ dependencies = [
[[package]]
name = "derive-docs"
version = "0.1.38"
version = "0.1.39"
dependencies = [
"Inflector",
"anyhow",
@ -1724,7 +1724,7 @@ dependencies = [
[[package]]
name = "kcl-lib"
version = "0.2.38"
version = "0.2.39"
dependencies = [
"anyhow",
"approx 0.5.1",
@ -1791,7 +1791,7 @@ dependencies = [
[[package]]
name = "kcl-test-server"
version = "0.1.38"
version = "0.1.39"
dependencies = [
"anyhow",
"hyper 0.14.32",

View File

@ -1,7 +1,7 @@
[package]
name = "derive-docs"
description = "A tool for generating documentation from Rust derive macros"
version = "0.1.38"
version = "0.1.39"
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.38"
version = "0.1.39"
edition = "2021"
license = "MIT"

View File

@ -1,7 +1,7 @@
[package]
name = "kcl-lib"
description = "KittyCAD Language implementation and tools"
version = "0.2.38"
version = "0.2.39"
edition = "2021"
license = "MIT"
repository = "https://github.com/KittyCAD/modeling-app"

View File

@ -2048,3 +2048,24 @@ mod import_file_parse_error {
super::execute(TEST_NAME, true).await
}
}
mod assembly_simplified_walkie {
const TEST_NAME: &str = "assembly_simplified_walkie";
/// Test parsing KCL.
#[test]
fn parse() {
super::parse(TEST_NAME)
}
/// Test that parsing and unparsing KCL produces the original KCL input.
#[test]
fn unparse() {
super::unparse(TEST_NAME)
}
/// Test that KCL is executed correctly.
#[tokio::test(flavor = "multi_thread")]
async fn kcl_test_execute() {
super::execute(TEST_NAME, true).await
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,6 @@
---
source: kcl/src/simulation_tests.rs
description: Artifact graph flowchart assembly_simplified_walkie.kcl
extension: md
snapshot_kind: binary
---

View File

@ -0,0 +1,219 @@
```mermaid
flowchart LR
subgraph path3 [Path]
3["Path<br>[1358, 1517, 3]"]
4["Segment<br>[1523, 1618, 3]"]
5["Segment<br>[1624, 1785, 3]"]
6["Segment<br>[1791, 1886, 3]"]
7["Segment<br>[1892, 2056, 3]"]
8["Segment<br>[2062, 2158, 3]"]
9["Segment<br>[2164, 2327, 3]"]
10["Segment<br>[2333, 2428, 3]"]
11["Segment<br>[2434, 2441, 3]"]
12[Solid2d]
end
subgraph path41 [Path]
41["Path<br>[1119, 1160, 5]"]
42["Segment<br>[1168, 1263, 5]"]
43["Segment<br>[1271, 1367, 5]"]
44["Segment<br>[1375, 1461, 5]"]
45["Segment<br>[1469, 1476, 5]"]
46[Solid2d]
end
subgraph path63 [Path]
63["Path<br>[503, 596, 4]"]
64["Segment<br>[602, 639, 4]"]
65["Segment<br>[645, 683, 4]"]
66["Segment<br>[689, 727, 4]"]
67["Segment<br>[733, 751, 4]"]
68[Solid2d]
end
1["Plane<br>[358, 387, 3]"]
2["Plane<br>[1322, 1351, 3]"]
13["Sweep Extrusion<br>[2790, 2826, 3]"]
14[Wall]
15[Wall]
16[Wall]
17[Wall]
18[Wall]
19[Wall]
20[Wall]
21[Wall]
22["Cap Start"]
23["Cap End"]
24["SweepEdge Opposite"]
25["SweepEdge Adjacent"]
26["SweepEdge Opposite"]
27["SweepEdge Adjacent"]
28["SweepEdge Opposite"]
29["SweepEdge Adjacent"]
30["SweepEdge Opposite"]
31["SweepEdge Adjacent"]
32["SweepEdge Opposite"]
33["SweepEdge Adjacent"]
34["SweepEdge Opposite"]
35["SweepEdge Adjacent"]
36["SweepEdge Opposite"]
37["SweepEdge Adjacent"]
38["SweepEdge Opposite"]
39["SweepEdge Adjacent"]
40["Plane<br>[405, 442, 0]"]
47["Sweep Extrusion<br>[1495, 1542, 5]"]
48[Wall]
49[Wall]
50[Wall]
51[Wall]
52["Cap Start"]
53["Cap End"]
54["SweepEdge Opposite"]
55["SweepEdge Adjacent"]
56["SweepEdge Opposite"]
57["SweepEdge Adjacent"]
58["SweepEdge Opposite"]
59["SweepEdge Adjacent"]
60["SweepEdge Opposite"]
61["SweepEdge Adjacent"]
62["Plane<br>[467, 497, 4]"]
69["Sweep Extrusion<br>[797, 849, 4]"]
70[Wall]
71[Wall]
72[Wall]
73[Wall]
74["Cap Start"]
75["Cap End"]
76["SweepEdge Opposite"]
77["SweepEdge Adjacent"]
78["SweepEdge Opposite"]
79["SweepEdge Adjacent"]
80["SweepEdge Opposite"]
81["SweepEdge Adjacent"]
82["SweepEdge Opposite"]
83["SweepEdge Adjacent"]
2 --- 3
3 --- 4
3 --- 5
3 --- 6
3 --- 7
3 --- 8
3 --- 9
3 --- 10
3 --- 11
3 ---- 13
3 --- 12
4 --- 21
4 --- 38
4 --- 39
5 --- 20
5 --- 36
5 --- 37
6 --- 19
6 --- 34
6 --- 35
7 --- 18
7 --- 32
7 --- 33
8 --- 17
8 --- 30
8 --- 31
9 --- 16
9 --- 28
9 --- 29
10 --- 15
10 --- 26
10 --- 27
11 --- 14
11 --- 24
11 --- 25
13 --- 14
13 --- 15
13 --- 16
13 --- 17
13 --- 18
13 --- 19
13 --- 20
13 --- 21
13 --- 22
13 --- 23
13 --- 24
13 --- 25
13 --- 26
13 --- 27
13 --- 28
13 --- 29
13 --- 30
13 --- 31
13 --- 32
13 --- 33
13 --- 34
13 --- 35
13 --- 36
13 --- 37
13 --- 38
13 --- 39
40 --- 41
41 --- 42
41 --- 43
41 --- 44
41 --- 45
41 ---- 47
41 --- 46
42 --- 48
42 --- 54
42 --- 55
43 --- 49
43 --- 56
43 --- 57
44 --- 50
44 --- 58
44 --- 59
45 --- 51
45 --- 60
45 --- 61
47 --- 48
47 --- 49
47 --- 50
47 --- 51
47 --- 52
47 --- 53
47 --- 54
47 --- 55
47 --- 56
47 --- 57
47 --- 58
47 --- 59
47 --- 60
47 --- 61
62 --- 63
63 --- 64
63 --- 65
63 --- 66
63 --- 67
63 ---- 69
63 --- 68
64 --- 73
64 --- 82
64 --- 83
65 --- 72
65 --- 80
65 --- 81
66 --- 71
66 --- 78
66 --- 79
67 --- 70
67 --- 76
67 --- 77
69 --- 70
69 --- 71
69 --- 72
69 --- 73
69 --- 74
69 --- 75
69 --- 76
69 --- 77
69 --- 78
69 --- 79
69 --- 80
69 --- 81
69 --- 82
69 --- 83
```

View File

@ -0,0 +1,428 @@
---
source: kcl/src/simulation_tests.rs
description: Result of parsing assembly_simplified_walkie.kcl
---
{
"Ok": {
"body": [
{
"end": 112,
"path": {
"type": "Kcl",
"filename": "case.kcl"
},
"selector": {
"type": "None",
"alias": null
},
"start": 95,
"type": "ImportStatement",
"type": "ImportStatement"
},
{
"end": 151,
"path": {
"type": "Kcl",
"filename": "talk-button.kcl"
},
"selector": {
"type": "None",
"alias": {
"end": 151,
"name": "talkButton",
"start": 141,
"type": "Identifier"
}
},
"start": 113,
"type": "ImportStatement",
"type": "ImportStatement"
},
{
"end": 183,
"path": {
"type": "Kcl",
"filename": "button.kcl"
},
"selector": {
"type": "List",
"items": [
{
"alias": null,
"end": 165,
"name": {
"end": 165,
"name": "button",
"start": 159,
"type": "Identifier"
},
"start": 159,
"type": "ImportItem"
}
]
},
"start": 152,
"type": "ImportStatement",
"type": "ImportStatement"
},
{
"end": 289,
"path": {
"type": "Kcl",
"filename": "globals.kcl"
},
"selector": {
"type": "List",
"items": [
{
"alias": null,
"end": 196,
"name": {
"end": 196,
"name": "width",
"start": 191,
"type": "Identifier"
},
"start": 191,
"type": "ImportItem"
},
{
"alias": null,
"end": 204,
"name": {
"end": 204,
"name": "height",
"start": 198,
"type": "Identifier"
},
"start": 198,
"type": "ImportItem"
},
{
"alias": null,
"end": 215,
"name": {
"end": 215,
"name": "thickness",
"start": 206,
"type": "Identifier"
},
"start": 206,
"type": "ImportItem"
},
{
"alias": null,
"end": 228,
"name": {
"end": 228,
"name": "screenWidth",
"start": 217,
"type": "Identifier"
},
"start": 217,
"type": "ImportItem"
},
{
"alias": null,
"end": 242,
"name": {
"end": 242,
"name": "screenHeight",
"start": 230,
"type": "Identifier"
},
"start": 230,
"type": "ImportItem"
},
{
"alias": null,
"end": 259,
"name": {
"end": 259,
"name": "screenYPosition",
"start": 244,
"type": "Identifier"
},
"start": 244,
"type": "ImportItem"
},
{
"alias": null,
"end": 270,
"name": {
"end": 270,
"name": "tolerance",
"start": 261,
"type": "Identifier"
},
"start": 261,
"type": "ImportItem"
}
]
},
"start": 184,
"type": "ImportStatement",
"type": "ImportStatement"
},
{
"end": 314,
"expression": {
"end": 314,
"name": "case",
"start": 310,
"type": "Identifier",
"type": "Identifier"
},
"start": 310,
"type": "ExpressionStatement",
"type": "ExpressionStatement"
},
{
"end": 443,
"expression": {
"arguments": [
{
"elements": [
{
"argument": {
"end": 378,
"left": {
"end": 366,
"left": {
"end": 362,
"name": "screenWidth",
"start": 351,
"type": "Identifier",
"type": "Identifier"
},
"operator": "/",
"right": {
"end": 366,
"raw": "2",
"start": 365,
"type": "Literal",
"type": "Literal",
"value": {
"value": 2.0,
"suffix": "None"
}
},
"start": 351,
"type": "BinaryExpression",
"type": "BinaryExpression"
},
"operator": "+",
"right": {
"end": 378,
"name": "tolerance",
"start": 369,
"type": "Identifier",
"type": "Identifier"
},
"start": 351,
"type": "BinaryExpression",
"type": "BinaryExpression"
},
"end": 378,
"operator": "-",
"start": 349,
"type": "UnaryExpression",
"type": "UnaryExpression"
},
{
"end": 398,
"name": "screenYPosition",
"start": 383,
"type": "Identifier",
"type": "Identifier"
}
],
"end": 400,
"start": 345,
"type": "ArrayExpression",
"type": "ArrayExpression"
},
{
"end": 403,
"raw": "0",
"start": 402,
"type": "Literal",
"type": "Literal",
"value": {
"value": 0.0,
"suffix": "None"
}
},
{
"arguments": [
{
"type": "LabeledArg",
"label": {
"type": "Identifier",
"name": "offset"
},
"arg": {
"end": 441,
"name": "thickness",
"start": 432,
"type": "Identifier",
"type": "Identifier"
}
}
],
"callee": {
"end": 416,
"name": "offsetPlane",
"start": 405,
"type": "Identifier"
},
"end": 442,
"start": 405,
"type": "CallExpressionKw",
"type": "CallExpressionKw",
"unlabeled": {
"end": 421,
"raw": "\"XZ\"",
"start": 417,
"type": "Literal",
"type": "Literal",
"value": "XZ"
}
}
],
"callee": {
"end": 344,
"name": "button",
"start": 338,
"type": "Identifier"
},
"end": 443,
"start": 338,
"type": "CallExpression",
"type": "CallExpression"
},
"start": 338,
"type": "ExpressionStatement",
"type": "ExpressionStatement"
},
{
"end": 481,
"expression": {
"end": 481,
"name": "talkButton",
"start": 471,
"type": "Identifier",
"type": "Identifier"
},
"start": 471,
"type": "ExpressionStatement",
"type": "ExpressionStatement"
}
],
"end": 482,
"innerAttrs": [
{
"end": 33,
"name": {
"end": 9,
"name": "settings",
"start": 1,
"type": "Identifier"
},
"properties": [
{
"end": 32,
"key": {
"end": 27,
"name": "defaultLengthUnit",
"start": 10,
"type": "Identifier"
},
"start": 10,
"type": "ObjectProperty",
"value": {
"end": 32,
"name": "in",
"start": 30,
"type": "Identifier",
"type": "Identifier"
}
}
],
"start": 0,
"type": "Annotation"
}
],
"nonCodeMeta": {
"nonCodeNodes": {
"3": [
{
"end": 309,
"start": 289,
"type": "NonCodeNode",
"value": {
"type": "newLineBlockComment",
"value": "Import the case",
"style": "line"
}
}
],
"4": [
{
"end": 337,
"start": 314,
"type": "NonCodeNode",
"value": {
"type": "newLineBlockComment",
"value": "Import the buttons",
"style": "line"
}
}
],
"5": [
{
"end": 470,
"start": 443,
"type": "NonCodeNode",
"value": {
"type": "newLineBlockComment",
"value": "Import the talk button",
"style": "line"
}
}
]
},
"startNodes": [
{
"end": 36,
"start": 33,
"type": "NonCodeNode",
"value": {
"type": "newLine"
}
},
{
"end": 64,
"start": 36,
"type": "NonCodeNode",
"value": {
"type": "blockComment",
"value": "Part of the Walkie Talkie",
"style": "line"
}
},
{
"end": 94,
"start": 65,
"type": "NonCodeNode",
"value": {
"type": "blockComment",
"value": "Import parts and constants",
"style": "line"
}
}
]
},
"start": 0
}
}

View File

@ -0,0 +1,74 @@
// Walkie Talkie button
// Set units
@settings(defaultLengthUnit = in)
// Import constants
//import screenHeight, buttonWidth, tolerance, buttonHeight, buttonThickness from 'globals.kcl'
export height = 4
export width = 2.5
export thickness = 1
export chamferLength = .325
export offset = .125
export screenWidth = 1.75
export screenHeight = 1
export screenYPosition = height / 2 - 0.75
export screenDepth = -.0625
export speakerBoxWidth = 1.25
export speakerBoxHeight = 1.25
// antenna
export antennaBaseWidth = .5
export antennaBaseHeight = .25
export antennaTopWidth = .30
export antennaTopHeight = .05
// button
export buttonWidth = .15
export tolerance = 0.020
export buttonHeight = screenHeight / 2 - tolerance
export buttonThickness = .040
// case
export squareHoleSideLength = 0.0625
export caseTolerance = 0.010
// knob
export knobDiameter = .5
export knobHeight = .25
export knobRadius = 0.050
// talk-button
export talkButtonSideLength = 0.5
export talkButtonHeight = 0.050
// Create a function for the button
export fn button(origin, rotation, plane) {
buttonSketch = startSketchOn(plane)
|> startProfileAt([origin[0], origin[1]], %)
|> angledLine({
angle = 180 + rotation,
length = buttonWidth
}, %, $tag1)
|> angledLine({
angle = 270 + rotation,
length = buttonHeight
}, %, $tag2)
|> angledLine({
angle = 0 + rotation,
length = buttonWidth
}, %)
|> close()
buttonExtrude = extrude(buttonSketch, length = buttonThickness)
|> chamfer(
length = .050,
tags = [
getNextAdjacentEdge(tag1),
getNextAdjacentEdge(tag2)
]
)
|> appearance(color = "#ff0000")
return buttonExtrude
}

View File

@ -0,0 +1,85 @@
// Walkie talkie case
// Set units
@settings(defaultLengthUnit = in)
// Import constants and Zoo logo
import width, height, chamferLength, offset, screenWidth, screenHeight, screenYPosition, screenDepth, speakerBoxWidth, speakerBoxHeight, squareHoleSideLength, caseTolerance from "globals.kcl"
// import zLogo, oLogo, oLogo2 from "zoo-logo.kcl"
plane = offsetPlane("XZ", offset = 1)
fn screenHole(sketchStart) {
sketch006 = startSketchOn(sketchStart)
|> startProfileAt([-screenWidth / 2, screenYPosition], %)
|> xLine(screenWidth, %)
|> yLine(-screenHeight, %)
|> xLine(-screenWidth, %)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
return sketch006
}
fn squareHolePattern(plane, x, y) {
fn transformX(i) {
return { translate = [.125 * i, 0] }
}
fn transformY(i) {
return { translate = [0, -.125 * i] }
}
squareHolePatternSketch = startSketchOn(plane)
|> startProfileAt([-x, -y], %)
|> line(end = [squareHoleSideLength / 2, 0])
|> line(end = [0, -squareHoleSideLength / 2])
|> line(end = [-squareHoleSideLength / 2, 0])
|> close()
|> patternTransform2d(instances = 13, transform = transformX)
|> patternTransform2d(instances = 11, transform = transformY)
return squareHolePatternSketch
}
sketch005 = startSketchOn(offsetPlane("XZ", offset = 1))
|> startProfileAt([
-width / 2 + offset + caseTolerance,
height / 2 - (chamferLength + (offset + caseTolerance) / 2 * cos(toRadians(45)))
], %)
|> angledLineToY({
angle = 45,
to = height / 2 - (offset + caseTolerance)
}, %)
|> line(endAbsolute = [
width / 2 - (chamferLength + (offset + caseTolerance) / 2 * cos(toRadians(45))),
height / 2 - (offset + caseTolerance)
])
|> angledLineToX({
angle = -45,
to = width / 2 - (offset + caseTolerance)
}, %)
|> line(endAbsolute = [
width / 2 - (offset + caseTolerance),
-(height / 2 - (chamferLength + (offset + caseTolerance) / 2 * cos(toRadians(45))))
])
|> angledLineToY({
angle = -135,
to = -height / 2 + offset + caseTolerance
}, %)
|> line(endAbsolute = [
-(width / 2 - (chamferLength + (offset + caseTolerance) / 2 * cos(toRadians(45)))),
-height / 2 + offset + caseTolerance
])
|> angledLineToX({
angle = -225,
to = -width / 2 + offset + caseTolerance
}, %)
|> close()
// |> hole(screenHole(plane), %)
// |> hole(squareHolePattern(plane, .75, .125), %)
// |> hole(zLogo(plane, [-.30, -1.825], .20), %)
// |> hole(oLogo(plane, [-.075, -1.825], .20), %)
// |> hole(oLogo2(plane, [-.075, -1.825], .20), %)
// |> hole(oLogo(plane, [.175, -1.825], .20), %)
// |> hole(oLogo2(plane, [.175, -1.825], .20), %)
extrude(sketch005, length = -0.0625)
|> appearance(color = '#D0FF01', metalness = 0, roughness = 50)

View File

@ -0,0 +1,14 @@
---
source: kcl/src/simulation_tests.rs
description: Error from executing assembly_simplified_walkie.kcl
---
KCL Semantic error
× semantic: Error loading imported file. Open it to view more details.
│ tests/assembly_simplified_walkie/talk-button.kcl: Modeling command failed:
│ [ApiError { error_code: BadRequest, message: "Chamfer failed" }]
╭─[21:1]
20 │ // Import the talk button
21 │ talkButton
· ──────────
╰────

View File

@ -0,0 +1,42 @@
// Global constants for the walkie talkie
// Set units
@settings(defaultLengthUnit = in)
// body
export height = 4
export width = 2.5
export thickness = 1
export chamferLength = .325
export offset = .125
export screenWidth = 1.75
export screenHeight = 1
export screenYPosition = height / 2 - 0.75
export screenDepth = -.0625
export speakerBoxWidth = 1.25
export speakerBoxHeight = 1.25
// antenna
export antennaBaseWidth = .5
export antennaBaseHeight = .25
export antennaTopWidth = .30
export antennaTopHeight = .05
// button
export buttonWidth = .15
export tolerance = 0.020
export buttonHeight = screenHeight / 2 - tolerance
export buttonThickness = .040
// case
export squareHoleSideLength = 0.0625
export caseTolerance = 0.010
// knob
export knobDiameter = .5
export knobHeight = .25
export knobRadius = 0.050
// talk-button
export talkButtonSideLength = 0.5
export talkButtonHeight = 0.050

View File

@ -0,0 +1,21 @@
@settings(defaultLengthUnit = in)
// Part of the Walkie Talkie
// Import parts and constants
import "case.kcl"
import "talk-button.kcl" as talkButton
import button from "button.kcl"
import width, height, thickness, screenWidth, screenHeight, screenYPosition, tolerance from "globals.kcl"
// Import the case
case
// Import the buttons
button([
-(screenWidth / 2 + tolerance),
screenYPosition
], 0, offsetPlane("XZ", offset = thickness))
// Import the talk button
talkButton

View File

@ -0,0 +1,198 @@
---
source: kcl/src/simulation_tests.rs
description: Operations executed assembly_simplified_walkie.kcl
---
[
{
"labeledArgs": {
"offset": {
"value": {
"type": "Number",
"value": 1.0,
"ty": {
"type": "Default",
"len": {
"type": "Inches"
},
"angle": {
"type": "Degrees"
}
}
},
"sourceRange": [
432,
441,
0
]
}
},
"name": "offsetPlane",
"sourceRange": [
405,
442,
0
],
"type": "StdLibCall",
"unlabeledArg": {
"value": {
"type": "String",
"value": "XZ"
},
"sourceRange": [
417,
421,
0
]
}
},
{
"type": "UserDefinedFunctionCall",
"name": "button",
"functionSourceRange": [
1046,
1759,
5
],
"unlabeledArg": null,
"labeledArgs": {},
"sourceRange": [
338,
443,
0
]
},
{
"labeledArgs": {
"data": {
"value": {
"type": "Plane",
"artifact_id": "[uuid]"
},
"sourceRange": [
1105,
1110,
5
]
}
},
"name": "startSketchOn",
"sourceRange": [
1091,
1111,
5
],
"type": "StdLibCall",
"unlabeledArg": null
},
{
"labeledArgs": {
"length": {
"value": {
"type": "Number",
"value": 0.04,
"ty": {
"type": "Default",
"len": {
"type": "Inches"
},
"angle": {
"type": "Degrees"
}
}
},
"sourceRange": [
1526,
1541,
5
]
}
},
"name": "extrude",
"sourceRange": [
1495,
1542,
5
],
"type": "StdLibCall",
"unlabeledArg": {
"value": {
"type": "Sketch",
"value": {
"artifactId": "[uuid]"
}
},
"sourceRange": [
1503,
1515,
5
]
}
},
{
"labeledArgs": {
"length": {
"value": {
"type": "Number",
"value": 0.05,
"ty": {
"type": "Default",
"len": {
"type": "Inches"
},
"angle": {
"type": "Degrees"
}
}
},
"sourceRange": [
1577,
1581,
5
]
},
"tags": {
"value": {
"type": "Array",
"value": [
{
"type": "Uuid",
"value": "[uuid]"
},
{
"type": "Uuid",
"value": "[uuid]"
}
]
},
"sourceRange": [
1599,
1686,
5
]
}
},
"name": "chamfer",
"sourceRange": [
1550,
1695,
5
],
"type": "StdLibCall",
"unlabeledArg": {
"value": {
"type": "Solid",
"value": {
"artifactId": "[uuid]"
}
},
"sourceRange": [
0,
0,
0
]
}
},
{
"type": "UserDefinedFunctionReturn"
}
]

View File

@ -0,0 +1,46 @@
// Walkie talkie talk button
// Set units
@settings(defaultLengthUnit = in)
// Import constants
import width, thickness, talkButtonSideLength, talkButtonHeight from "globals.kcl"
talkButtonPlane = {
plane = {
origin = {
x = width / 2,
y = -thickness / 2,
z = .5
},
xAxis = { x = 0, y = 1, z = 0 },
yAxis = { x = 0, y = 0, z = 1 },
zAxis = { x = 1, y = 0, z = 0 }
}
}
// Create the talk button sketch
talkButtonSketch = startSketchOn(talkButtonPlane)
|> startProfileAt([
-talkButtonSideLength / 2,
talkButtonSideLength / 2
], %)
|> xLine(talkButtonSideLength, %, $tag1)
|> yLine(-talkButtonSideLength, %, $tag2)
|> xLine(-talkButtonSideLength, %, $tag3)
|> close(tag = $tag4)
// Create the talk button and apply fillets
extrude(talkButtonSketch, length = talkButtonHeight)
|> fillet(
radius = 0.050,
tags = [
getNextAdjacentEdge(tag1),
getNextAdjacentEdge(tag2),
getNextAdjacentEdge(tag3),
getNextAdjacentEdge(tag4)
]
)
|> appearance(color = '#D0FF01', metalness = 90, roughness = 90)