Compare commits
14 Commits
Author | SHA1 | Date | |
---|---|---|---|
239ab6850e | |||
4a7dd6e650 | |||
af2609e678 | |||
30909dedda | |||
39d76ed54f | |||
4925251c29 | |||
9772869545 | |||
a7e830cd02 | |||
ca102116b6 | |||
c2fba89e77 | |||
7e31678ba2 | |||
1140ced121 | |||
32b7ddaa7c | |||
2525f99515 |
2
.github/workflows/cargo-test.yml
vendored
@ -62,7 +62,7 @@ jobs:
|
||||
shell: bash
|
||||
run: |-
|
||||
cd "${{ matrix.dir }}"
|
||||
cargo llvm-cov nextest --all --lcov --output-path lcov.info --test-threads=1 --no-fail-fast -P ci 2>&1 | tee /tmp/github-actions.log
|
||||
cargo llvm-cov nextest --workspace --lcov --output-path lcov.info --test-threads=1 --no-fail-fast -P ci 2>&1 | tee /tmp/github-actions.log
|
||||
env:
|
||||
KITTYCAD_API_TOKEN: ${{secrets.KITTYCAD_API_TOKEN}}
|
||||
RUST_MIN_STACK: 10485760000
|
||||
|
38720
docs/kcl/std.json
@ -162,6 +162,28 @@ A base path.
|
||||
|
||||
|
||||
----
|
||||
A circular arc, not necessarily tangential to the current point.
|
||||
|
||||
**Type:** `object`
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `type` |enum: `Arc`| | No |
|
||||
| `center` |`[number, number]`| Center of the circle that this arc is drawn on. | No |
|
||||
| `radius` |`number`| Radius of the circle that this arc is drawn on. | No |
|
||||
| `from` |`[number, number]`| The from point. | No |
|
||||
| `to` |`[number, number]`| The to point. | No |
|
||||
| `tag` |[`TagDeclarator`](/docs/kcl/types#tag-declaration)| The tag of the path. | No |
|
||||
| `__geoMeta` |[`GeoMeta`](/docs/kcl/types/GeoMeta)| Metadata. | No |
|
||||
|
||||
|
||||
----
|
||||
|
||||
|
||||
|
||||
|
@ -18,7 +18,7 @@ Engine information for a tag.
|
||||
|----------|------|-------------|----------|
|
||||
| `id` |`string`| The id of the tagged object. | No |
|
||||
| `sketch` |`string`| The sketch the tag is on. | No |
|
||||
| `path` |[`BasePath`](/docs/kcl/types/BasePath)| The path the tag is on. | No |
|
||||
| `path` |[`Path`](/docs/kcl/types/Path)| The path the tag is on. | No |
|
||||
| `surface` |[`ExtrudeSurface`](/docs/kcl/types/ExtrudeSurface)| The surface information for the tag. | No |
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 46 KiB |
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 44 KiB |
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |
2
interface.d.ts
vendored
@ -2,7 +2,7 @@ import fs from 'node:fs/promises'
|
||||
import fsSync from 'node:fs'
|
||||
import path from 'path'
|
||||
import { dialog, shell } from 'electron'
|
||||
import { MachinesListing } from 'lib/machineManager'
|
||||
import { MachinesListing } from 'components/MachineManagerProvider'
|
||||
|
||||
type EnvFn = (value?: string) => string
|
||||
|
||||
|
@ -996,7 +996,7 @@
|
||||
},
|
||||
"description": "",
|
||||
"title": "machine-api",
|
||||
"version": "0.1.0"
|
||||
"version": "0.1.1"
|
||||
},
|
||||
"openapi": "3.0.3",
|
||||
"paths": {
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "zoo-modeling-app",
|
||||
"version": "0.26.1",
|
||||
"version": "0.26.2",
|
||||
"private": true,
|
||||
"productName": "Zoo Modeling App",
|
||||
"author": {
|
||||
|
@ -21,6 +21,7 @@ import { WasmErrBanner } from 'components/WasmErrBanner'
|
||||
import { CommandBar } from 'components/CommandBar/CommandBar'
|
||||
import ModelingMachineProvider from 'components/ModelingMachineProvider'
|
||||
import FileMachineProvider from 'components/FileMachineProvider'
|
||||
import { MachineManagerProvider } from 'components/MachineManagerProvider'
|
||||
import { PATHS } from 'lib/paths'
|
||||
import {
|
||||
fileLoader,
|
||||
@ -49,6 +50,7 @@ const router = createRouter([
|
||||
{
|
||||
loader: settingsLoader,
|
||||
id: PATHS.INDEX,
|
||||
// TODO: Re-evaluate if this is true
|
||||
/* Make sure auth is the outermost provider or else we will have
|
||||
* inefficient re-renders, use the react profiler to see. */
|
||||
element: (
|
||||
@ -57,7 +59,9 @@ const router = createRouter([
|
||||
<LspProvider>
|
||||
<KclContextProvider>
|
||||
<AppStateProvider>
|
||||
<Outlet />
|
||||
<MachineManagerProvider>
|
||||
<Outlet />
|
||||
</MachineManagerProvider>
|
||||
</AppStateProvider>
|
||||
</KclContextProvider>
|
||||
</LspProvider>
|
||||
|
@ -746,7 +746,6 @@ export class SceneEntities {
|
||||
},
|
||||
})
|
||||
},
|
||||
...this.mouseEnterLeaveCallbacks(),
|
||||
})
|
||||
}
|
||||
setupDraftRectangle = async (
|
||||
|
@ -213,7 +213,7 @@ export class SceneInfra {
|
||||
to: Coords2d
|
||||
angle?: number
|
||||
}): SegmentOverlayPayload | null {
|
||||
if (group.userData.pathToNode && arrowGroup) {
|
||||
if (!group.userData.draft && group.userData.pathToNode && arrowGroup) {
|
||||
const vector = new Vector3(0, 0, 0)
|
||||
|
||||
// Get the position of the object3D in world space
|
||||
|
@ -147,6 +147,7 @@ class StraightSegment implements SegmentUtils {
|
||||
segmentGroup.name = STRAIGHT_SEGMENT
|
||||
segmentGroup.userData = {
|
||||
type: STRAIGHT_SEGMENT,
|
||||
draft: isDraftSegment,
|
||||
id,
|
||||
from,
|
||||
to,
|
||||
@ -347,6 +348,7 @@ class TangentialArcToSegment implements SegmentUtils {
|
||||
mesh.name = meshName
|
||||
group.userData = {
|
||||
type: TANGENTIAL_ARC_TO_SEGMENT,
|
||||
draft: isDraftSegment,
|
||||
id,
|
||||
from,
|
||||
to,
|
||||
@ -515,11 +517,18 @@ class CircleSegment implements SegmentUtils {
|
||||
const meshType = isDraftSegment ? CIRCLE_SEGMENT_DASH : CIRCLE_SEGMENT_BODY
|
||||
const arrowGroup = createArrowhead(scale, theme, color)
|
||||
const circleCenterGroup = createCircleCenterHandle(scale, theme, color)
|
||||
// A radius indicator that appears from the center to the perimeter
|
||||
const radiusIndicatorGroup = createLengthIndicator({
|
||||
from: center,
|
||||
to: [center[0] + radius, center[1]],
|
||||
scale,
|
||||
})
|
||||
|
||||
arcMesh.userData.type = meshType
|
||||
arcMesh.name = meshType
|
||||
group.userData = {
|
||||
type: CIRCLE_SEGMENT,
|
||||
draft: isDraftSegment,
|
||||
id,
|
||||
from,
|
||||
radius,
|
||||
@ -532,7 +541,7 @@ class CircleSegment implements SegmentUtils {
|
||||
}
|
||||
group.name = CIRCLE_SEGMENT
|
||||
|
||||
group.add(arcMesh, arrowGroup, circleCenterGroup)
|
||||
group.add(arcMesh, arrowGroup, circleCenterGroup, radiusIndicatorGroup)
|
||||
const updateOverlaysCallback = this.update({
|
||||
prevSegment,
|
||||
input,
|
||||
@ -564,6 +573,9 @@ class CircleSegment implements SegmentUtils {
|
||||
group.userData.radius = radius
|
||||
group.userData.prevSegment = prevSegment
|
||||
const arrowGroup = group.getObjectByName(ARROWHEAD) as Group
|
||||
const radiusLengthIndicator = group.getObjectByName(
|
||||
SEGMENT_LENGTH_LABEL
|
||||
) as Group
|
||||
const circleCenterHandle = group.getObjectByName(
|
||||
CIRCLE_CENTER_HANDLE
|
||||
) as Group
|
||||
@ -581,11 +593,14 @@ class CircleSegment implements SegmentUtils {
|
||||
}
|
||||
|
||||
if (arrowGroup) {
|
||||
arrowGroup.position.set(
|
||||
center[0] + Math.cos(Math.PI / 4) * radius,
|
||||
center[1] + Math.sin(Math.PI / 4) * radius,
|
||||
0
|
||||
)
|
||||
// The arrowhead is placed at the perimeter of the circle,
|
||||
// pointing up and to the right
|
||||
const arrowPoint = {
|
||||
x: center[0] + Math.cos(Math.PI / 4) * radius,
|
||||
y: center[1] + Math.sin(Math.PI / 4) * radius,
|
||||
}
|
||||
|
||||
arrowGroup.position.set(arrowPoint.x, arrowPoint.y, 0)
|
||||
|
||||
const arrowheadAngle = Math.PI / 4
|
||||
arrowGroup.quaternion.setFromUnitVectors(
|
||||
@ -596,6 +611,31 @@ class CircleSegment implements SegmentUtils {
|
||||
arrowGroup.visible = isHandlesVisible
|
||||
}
|
||||
|
||||
if (radiusLengthIndicator) {
|
||||
// The radius indicator is placed at the midpoint of the radius,
|
||||
// at a 45 degree CCW angle from the positive X-axis
|
||||
const indicatorPoint = {
|
||||
x: center[0] + (Math.cos(Math.PI / 4) * radius) / 2,
|
||||
y: center[1] + (Math.sin(Math.PI / 4) * radius) / 2,
|
||||
}
|
||||
const labelWrapper = radiusLengthIndicator.getObjectByName(
|
||||
SEGMENT_LENGTH_LABEL_TEXT
|
||||
) as CSS2DObject
|
||||
const labelWrapperElem = labelWrapper.element as HTMLDivElement
|
||||
const label = labelWrapperElem.children[0] as HTMLParagraphElement
|
||||
label.innerText = `${roundOff(radius)}`
|
||||
label.classList.add(SEGMENT_LENGTH_LABEL_TEXT)
|
||||
const isPlaneBackFace = center[0] > indicatorPoint.x
|
||||
label.style.setProperty(
|
||||
'--degree',
|
||||
`${isPlaneBackFace ? '45' : '-45'}deg`
|
||||
)
|
||||
label.style.setProperty('--x', `0px`)
|
||||
label.style.setProperty('--y', `0px`)
|
||||
labelWrapper.position.set(indicatorPoint.x, indicatorPoint.y, 0)
|
||||
radiusLengthIndicator.visible = isHandlesVisible
|
||||
}
|
||||
|
||||
if (circleCenterHandle) {
|
||||
circleCenterHandle.position.set(center[0], center[1], 0)
|
||||
circleCenterHandle.scale.set(scale, scale, scale)
|
||||
|
@ -140,6 +140,13 @@ const FileTreeItem = ({
|
||||
async (eventType, path) => {
|
||||
// Don't try to read a file that was removed.
|
||||
if (isCurrentFile && eventType !== 'unlink') {
|
||||
// Prevents a cyclic read / write causing editor problems such as
|
||||
// misplaced cursor positions.
|
||||
if (codeManager.writeCausedByAppCheckedInFileTreeFileSystemWatcher) {
|
||||
codeManager.writeCausedByAppCheckedInFileTreeFileSystemWatcher = false
|
||||
return
|
||||
}
|
||||
|
||||
let code = await window.electron.readFile(path, { encoding: 'utf-8' })
|
||||
code = normalizeLineEndings(code)
|
||||
codeManager.updateCodeStateEditor(code)
|
||||
|
@ -23,6 +23,7 @@ export function LowerRightControls({
|
||||
}) {
|
||||
const location = useLocation()
|
||||
const filePath = useAbsoluteFilePath()
|
||||
|
||||
const linkOverrideClassName =
|
||||
'!text-chalkboard-70 hover:!text-chalkboard-80 dark:!text-chalkboard-40 dark:hover:!text-chalkboard-30'
|
||||
|
||||
|
123
src/components/MachineManagerProvider.tsx
Normal file
@ -0,0 +1,123 @@
|
||||
import { createContext, useEffect, useState } from 'react'
|
||||
|
||||
import { engineCommandManager } from 'lib/singletons'
|
||||
import { CommandsContext } from 'components/CommandBar/CommandBarProvider'
|
||||
import { isDesktop } from 'lib/isDesktop'
|
||||
import { components } from 'lib/machine-api'
|
||||
import { reportRejection } from 'lib/trap'
|
||||
import { toSync } from 'lib/utils'
|
||||
|
||||
export type MachinesListing = Array<
|
||||
components['schemas']['MachineInfoResponse']
|
||||
>
|
||||
|
||||
export interface MachineManager {
|
||||
machines: MachinesListing
|
||||
machineApiIp: string | null
|
||||
currentMachine: components['schemas']['MachineInfoResponse'] | null
|
||||
noMachinesReason: () => string | undefined
|
||||
setCurrentMachine: (
|
||||
m: components['schemas']['MachineInfoResponse'] | null
|
||||
) => void
|
||||
}
|
||||
|
||||
export const MachineManagerContext = createContext<MachineManager>({
|
||||
machines: [],
|
||||
machineApiIp: null,
|
||||
currentMachine: null,
|
||||
setCurrentMachine: (
|
||||
_: components['schemas']['MachineInfoResponse'] | null
|
||||
) => {},
|
||||
noMachinesReason: () => undefined,
|
||||
})
|
||||
|
||||
export const MachineManagerProvider = ({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) => {
|
||||
const [machines, setMachines] = useState<MachinesListing>([])
|
||||
const [machineApiIp, setMachineApiIp] = useState<string | null>(null)
|
||||
const [currentMachine, setCurrentMachine] = useState<
|
||||
components['schemas']['MachineInfoResponse'] | null
|
||||
>(null)
|
||||
|
||||
const commandBarActor = CommandsContext.useActorRef()
|
||||
|
||||
// Get the reason message for why there are no machines.
|
||||
const noMachinesReason = (): string | undefined => {
|
||||
if (machines.length > 0) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (machineApiIp === null) {
|
||||
return 'Machine API server was not discovered'
|
||||
}
|
||||
|
||||
return 'Machine API server was discovered, but no machines are available'
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!isDesktop()) return
|
||||
|
||||
const update = async () => {
|
||||
const _machineApiIp = await window.electron.getMachineApiIp()
|
||||
if (_machineApiIp === null) return
|
||||
|
||||
setMachineApiIp(_machineApiIp)
|
||||
|
||||
const _machines = await window.electron.listMachines(_machineApiIp)
|
||||
setMachines(_machines)
|
||||
}
|
||||
|
||||
// Start a background job to update the machines every ten seconds.
|
||||
// If MDNS is already watching, this timeout will wait until it's done to trigger the
|
||||
// finding again.
|
||||
let timeoutId: ReturnType<typeof setTimeout> | undefined = undefined
|
||||
const timeoutLoop = () => {
|
||||
clearTimeout(timeoutId)
|
||||
timeoutId = setTimeout(
|
||||
toSync(async () => {
|
||||
await update()
|
||||
timeoutLoop()
|
||||
}, reportRejection),
|
||||
1000
|
||||
)
|
||||
}
|
||||
timeoutLoop()
|
||||
update().catch(reportRejection)
|
||||
}, [])
|
||||
|
||||
// Update engineCommandManager's copy of this data.
|
||||
useEffect(() => {
|
||||
const machineManagerNext = {
|
||||
machines,
|
||||
machineApiIp,
|
||||
currentMachine,
|
||||
noMachinesReason,
|
||||
setCurrentMachine,
|
||||
}
|
||||
|
||||
engineCommandManager.machineManager = machineManagerNext
|
||||
|
||||
commandBarActor.send({
|
||||
type: 'Set machine manager',
|
||||
data: machineManagerNext,
|
||||
})
|
||||
}, [machines, machineApiIp, currentMachine])
|
||||
|
||||
return (
|
||||
<MachineManagerContext.Provider
|
||||
value={{
|
||||
machines,
|
||||
machineApiIp,
|
||||
currentMachine,
|
||||
setCurrentMachine,
|
||||
noMachinesReason,
|
||||
}}
|
||||
>
|
||||
{' '}
|
||||
{children}{' '}
|
||||
</MachineManagerContext.Provider>
|
||||
)
|
||||
}
|
@ -1,5 +1,11 @@
|
||||
import { useMachine } from '@xstate/react'
|
||||
import React, { createContext, useEffect, useMemo, useRef } from 'react'
|
||||
import React, {
|
||||
createContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useContext,
|
||||
} from 'react'
|
||||
import {
|
||||
Actor,
|
||||
AnyStateMachine,
|
||||
@ -28,7 +34,7 @@ import {
|
||||
editorManager,
|
||||
sceneEntitiesManager,
|
||||
} from 'lib/singletons'
|
||||
import { machineManager } from 'lib/machineManager'
|
||||
import { MachineManagerContext } from 'components/MachineManagerProvider'
|
||||
import { useHotkeys } from 'react-hotkeys-hook'
|
||||
import { applyConstraintHorzVertDistance } from './Toolbar/SetHorzVertDistance'
|
||||
import {
|
||||
@ -140,6 +146,8 @@ export const ModelingMachineProvider = ({
|
||||
// >
|
||||
// )
|
||||
|
||||
const machineManager = useContext(MachineManagerContext)
|
||||
|
||||
const [modelingState, modelingSend, modelingActor] = useMachine(
|
||||
modelingMachine.provide({
|
||||
actions: {
|
||||
@ -408,7 +416,7 @@ export const ModelingMachineProvider = ({
|
||||
return {}
|
||||
}
|
||||
),
|
||||
Make: ({ event }) => {
|
||||
Make: ({ context, event }) => {
|
||||
if (event.type !== 'Make') return
|
||||
// Check if we already have an export intent.
|
||||
if (engineCommandManager.exportInfo) {
|
||||
@ -422,7 +430,21 @@ export const ModelingMachineProvider = ({
|
||||
}
|
||||
|
||||
// Set the current machine.
|
||||
machineManager.currentMachine = event.data.machine
|
||||
// Due to our use of singeton pattern, we need to do this to reliably
|
||||
// update this object across React and non-React boundary.
|
||||
// We need to do this eagerly because of the exportToEngine call below.
|
||||
if (engineCommandManager.machineManager === null) {
|
||||
console.warn(
|
||||
"engineCommandManager.machineManager is null. It shouldn't be at this point. Aborting operation."
|
||||
)
|
||||
return
|
||||
} else {
|
||||
engineCommandManager.machineManager.currentMachine =
|
||||
event.data.machine
|
||||
}
|
||||
|
||||
// Update the rest of the UI that needs to know the current machine
|
||||
context.machineManager.setCurrentMachine(event.data.machine)
|
||||
|
||||
const format: Models['OutputFormat_type'] = {
|
||||
type: 'stl',
|
||||
@ -995,6 +1017,7 @@ export const ModelingMachineProvider = ({
|
||||
...modelingMachineDefaultContext.store,
|
||||
...persistedContext,
|
||||
},
|
||||
machineManager,
|
||||
},
|
||||
// devTools: true,
|
||||
}
|
||||
|
@ -1,6 +1,12 @@
|
||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||
import { Resizable } from 're-resizable'
|
||||
import { MouseEventHandler, useCallback, useEffect, useMemo } from 'react'
|
||||
import {
|
||||
MouseEventHandler,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useContext,
|
||||
} from 'react'
|
||||
import { useHotkeys } from 'react-hotkeys-hook'
|
||||
import { SidebarAction, SidebarType, sidebarPanes } from './ModelingPanes'
|
||||
import Tooltip from 'components/Tooltip'
|
||||
@ -13,7 +19,7 @@ import { CustomIconName } from 'components/CustomIcon'
|
||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||
import { IconDefinition } from '@fortawesome/free-solid-svg-icons'
|
||||
import { useKclContext } from 'lang/KclProvider'
|
||||
import { machineManager } from 'lib/machineManager'
|
||||
import { MachineManagerContext } from 'components/MachineManagerProvider'
|
||||
|
||||
interface ModelingSidebarProps {
|
||||
paneOpacity: '' | 'opacity-20' | 'opacity-40'
|
||||
@ -29,6 +35,7 @@ function getPlatformString(): 'web' | 'desktop' {
|
||||
}
|
||||
|
||||
export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
|
||||
const machineManager = useContext(MachineManagerContext)
|
||||
const { commandBarSend } = useCommandsContext()
|
||||
const kclContext = useKclContext()
|
||||
const { settings } = useSettingsAuthContext()
|
||||
|
@ -1,7 +1,9 @@
|
||||
import { Popover } from '@headlessui/react'
|
||||
import { useContext } from 'react'
|
||||
import Tooltip from './Tooltip'
|
||||
import { machineManager } from 'lib/machineManager'
|
||||
import { isDesktop } from 'lib/isDesktop'
|
||||
import { components } from 'lib/machine-api'
|
||||
import { MachineManagerContext } from 'components/MachineManagerProvider'
|
||||
import { CustomIcon } from './CustomIcon'
|
||||
|
||||
export const NetworkMachineIndicator = ({
|
||||
@ -9,9 +11,12 @@ export const NetworkMachineIndicator = ({
|
||||
}: {
|
||||
className?: string
|
||||
}) => {
|
||||
const machineCount = machineManager.machineCount()
|
||||
const reason = machineManager.noMachinesReason()
|
||||
const machines = machineManager.machines
|
||||
const {
|
||||
noMachinesReason,
|
||||
machines,
|
||||
machines: { length: machineCount },
|
||||
} = useContext(MachineManagerContext)
|
||||
const reason = noMachinesReason()
|
||||
|
||||
return isDesktop() ? (
|
||||
<Popover className="relative">
|
||||
@ -47,34 +52,36 @@ export const NetworkMachineIndicator = ({
|
||||
</div>
|
||||
{machineCount > 0 && (
|
||||
<ul className="divide-y divide-chalkboard-20 dark:divide-chalkboard-80">
|
||||
{machines.map((machine) => {
|
||||
return (
|
||||
<li key={machine.id} className={'px-2 py-4 gap-1 last:mb-0 '}>
|
||||
<p className="">{machine.id.toUpperCase()}</p>
|
||||
<p className="text-chalkboard-60 dark:text-chalkboard-50 text-xs">
|
||||
{machine.make_model.model}
|
||||
</p>
|
||||
{machine.extra &&
|
||||
machine.extra.type === 'bambu' &&
|
||||
machine.extra.nozzle_diameter && (
|
||||
<p className="text-chalkboard-60 dark:text-chalkboard-50 text-xs">
|
||||
Nozzle Diameter: {machine.extra.nozzle_diameter}
|
||||
</p>
|
||||
)}
|
||||
<p className="text-chalkboard-60 dark:text-chalkboard-50 text-xs">
|
||||
{`Status: ${machine.state.state
|
||||
.charAt(0)
|
||||
.toUpperCase()}${machine.state.state.slice(1)}`}
|
||||
{machine.state.state === 'failed' && machine.state.message
|
||||
? ` (${machine.state.message})`
|
||||
: ''}
|
||||
{machine.state.state === 'running' && machine.progress
|
||||
? ` (${Math.round(machine.progress)}%)`
|
||||
: ''}
|
||||
</p>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
{machines.map(
|
||||
(machine: components['schemas']['MachineInfoResponse']) => {
|
||||
return (
|
||||
<li key={machine.id} className={'px-2 py-4 gap-1 last:mb-0 '}>
|
||||
<p className="">{machine.id.toUpperCase()}</p>
|
||||
<p className="text-chalkboard-60 dark:text-chalkboard-50 text-xs">
|
||||
{machine.make_model.model}
|
||||
</p>
|
||||
{machine.extra &&
|
||||
machine.extra.type === 'bambu' &&
|
||||
machine.extra.nozzle_diameter && (
|
||||
<p className="text-chalkboard-60 dark:text-chalkboard-50 text-xs">
|
||||
Nozzle Diameter: {machine.extra.nozzle_diameter}
|
||||
</p>
|
||||
)}
|
||||
<p className="text-chalkboard-60 dark:text-chalkboard-50 text-xs">
|
||||
{`Status: ${machine.state.state
|
||||
.charAt(0)
|
||||
.toUpperCase()}${machine.state.state.slice(1)}`}
|
||||
{machine.state.state === 'failed' && machine.state.message
|
||||
? ` (${machine.state.message})`
|
||||
: ''}
|
||||
{machine.state.state === 'running' && machine.progress
|
||||
? ` (${Math.round(machine.progress)}%)`
|
||||
: ''}
|
||||
</p>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
)}
|
||||
</ul>
|
||||
)}
|
||||
</Popover.Panel>
|
||||
|
@ -4,14 +4,14 @@ import { type IndexLoaderData } from 'lib/types'
|
||||
import { PATHS } from 'lib/paths'
|
||||
import { isDesktop } from '../lib/isDesktop'
|
||||
import { Link, useLocation, useNavigate } from 'react-router-dom'
|
||||
import { Fragment, useMemo } from 'react'
|
||||
import { Fragment, useMemo, useContext } from 'react'
|
||||
import { Logo } from './Logo'
|
||||
import { APP_NAME } from 'lib/constants'
|
||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||
import { CustomIcon } from './CustomIcon'
|
||||
import { useLspContext } from './LspProvider'
|
||||
import { engineCommandManager } from 'lib/singletons'
|
||||
import { machineManager } from 'lib/machineManager'
|
||||
import { MachineManagerContext } from 'components/MachineManagerProvider'
|
||||
import usePlatform from 'hooks/usePlatform'
|
||||
import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath'
|
||||
import Tooltip from './Tooltip'
|
||||
@ -96,6 +96,8 @@ function ProjectMenuPopover({
|
||||
const location = useLocation()
|
||||
const navigate = useNavigate()
|
||||
const filePath = useAbsoluteFilePath()
|
||||
const machineManager = useContext(MachineManagerContext)
|
||||
|
||||
const { commandBarState, commandBarSend } = useCommandsContext()
|
||||
const { onProjectClose } = useLspContext()
|
||||
const exportCommandInfo = { name: 'Export', groupId: 'modeling' }
|
||||
@ -106,7 +108,7 @@ function ProjectMenuPopover({
|
||||
(c) => c.name === obj.name && c.groupId === obj.groupId
|
||||
)
|
||||
)
|
||||
const machineCount = machineManager.machineCount()
|
||||
const machineCount = machineManager.machines.length
|
||||
|
||||
// We filter this memoized list so that no orphan "break" elements are rendered.
|
||||
const projectMenuItems = useMemo<(ActionButtonProps | 'break')[]>(
|
||||
|
@ -20,6 +20,8 @@ export default class CodeManager {
|
||||
private _hotkeys: { [key: string]: () => void } = {}
|
||||
private timeoutWriter: ReturnType<typeof setTimeout> | undefined = undefined
|
||||
|
||||
public writeCausedByAppCheckedInFileTreeFileSystemWatcher = false
|
||||
|
||||
constructor() {
|
||||
if (isDesktop()) {
|
||||
this.code = ''
|
||||
@ -120,6 +122,7 @@ export default class CodeManager {
|
||||
// and file-system watchers which read, will receive empty data during
|
||||
// writes.
|
||||
clearTimeout(this.timeoutWriter)
|
||||
this.writeCausedByAppCheckedInFileTreeFileSystemWatcher = true
|
||||
this.timeoutWriter = setTimeout(() => {
|
||||
// Wait one event loop to give a chance for params to be set
|
||||
// Save the file to disk
|
||||
|
@ -28,6 +28,7 @@ import {
|
||||
} from 'lib/constants'
|
||||
import { KclManager } from 'lang/KclSingleton'
|
||||
import { reportRejection } from 'lib/trap'
|
||||
import { MachineManager } from 'components/MachineManagerProvider'
|
||||
|
||||
// TODO(paultag): This ought to be tweakable.
|
||||
const pingIntervalMs = 5_000
|
||||
@ -1415,6 +1416,9 @@ export class EngineCommandManager extends EventTarget {
|
||||
(() => {}) as any
|
||||
kclManager: null | KclManager = null
|
||||
|
||||
// The current "manufacturing machine" aka 3D printer, CNC, etc.
|
||||
public machineManager: MachineManager | null = null
|
||||
|
||||
set exportInfo(info: ExportInfo | null) {
|
||||
this._exportInfo = info
|
||||
}
|
||||
@ -1630,10 +1634,16 @@ export class EngineCommandManager extends EventTarget {
|
||||
break
|
||||
}
|
||||
case ExportIntent.Make: {
|
||||
if (!this.machineManager) {
|
||||
console.warn('Some how, no manufacturing machine is selected.')
|
||||
break
|
||||
}
|
||||
|
||||
exportMake(
|
||||
event.data,
|
||||
this.exportInfo.name,
|
||||
this.pendingExport.toastId
|
||||
this.pendingExport.toastId,
|
||||
this.machineManager
|
||||
).then((result) => {
|
||||
if (result) {
|
||||
this.pendingExport?.resolve(null)
|
||||
|
@ -3,7 +3,6 @@ import { StateMachineCommandSetConfig, KclCommandValue } from 'lib/commandTypes'
|
||||
import { KCL_DEFAULT_LENGTH, KCL_DEFAULT_DEGREE } from 'lib/constants'
|
||||
import { components } from 'lib/machine-api'
|
||||
import { Selections } from 'lib/selections'
|
||||
import { machineManager } from 'lib/machineManager'
|
||||
import { modelingMachine, SketchTool } from 'machines/modelingMachine'
|
||||
|
||||
type OutputFormat = Models['OutputFormat_type']
|
||||
@ -187,41 +186,41 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
|
||||
machine.make_model.model ||
|
||||
machine.make_model.manufacturer ||
|
||||
'Unknown Machine',
|
||||
options: () => {
|
||||
return Object.entries(machineManager.machines).map(
|
||||
([_, machine]) => ({
|
||||
name:
|
||||
`${machine.id} (${
|
||||
machine.make_model.model || machine.make_model.manufacturer
|
||||
}) (${machine.state.state})` +
|
||||
(machine.hardware_configuration &&
|
||||
machine.hardware_configuration.type !== 'none' &&
|
||||
machine.hardware_configuration.config.nozzle_diameter
|
||||
? ` - Nozzle Diameter: ${machine.hardware_configuration.config.nozzle_diameter}`
|
||||
: '') +
|
||||
(machine.hardware_configuration &&
|
||||
machine.hardware_configuration.type !== 'none' &&
|
||||
machine.hardware_configuration.config.filaments &&
|
||||
machine.hardware_configuration.config.filaments[0]
|
||||
? ` - ${
|
||||
machine.hardware_configuration.config.filaments[0].name
|
||||
} #${
|
||||
machine.hardware_configuration.config &&
|
||||
machine.hardware_configuration.config.filaments[0].color?.slice(
|
||||
0,
|
||||
6
|
||||
)
|
||||
}`
|
||||
: ''),
|
||||
isCurrent: false,
|
||||
disabled: machine.state.state !== 'idle',
|
||||
value: machine as components['schemas']['MachineInfoResponse'],
|
||||
})
|
||||
)
|
||||
},
|
||||
defaultValue: () => {
|
||||
options: (commandBarContext) => {
|
||||
return Object.values(
|
||||
machineManager.machines
|
||||
commandBarContext.machineManager?.machines || []
|
||||
).map((machine: components['schemas']['MachineInfoResponse']) => ({
|
||||
name:
|
||||
`${machine.id} (${
|
||||
machine.make_model.model || machine.make_model.manufacturer
|
||||
}) (${machine.state.state})` +
|
||||
(machine.hardware_configuration &&
|
||||
machine.hardware_configuration.type !== 'none' &&
|
||||
machine.hardware_configuration.config.nozzle_diameter
|
||||
? ` - Nozzle Diameter: ${machine.hardware_configuration.config.nozzle_diameter}`
|
||||
: '') +
|
||||
(machine.hardware_configuration &&
|
||||
machine.hardware_configuration.type !== 'none' &&
|
||||
machine.hardware_configuration.config.filaments &&
|
||||
machine.hardware_configuration.config.filaments[0]
|
||||
? ` - ${
|
||||
machine.hardware_configuration.config.filaments[0].name
|
||||
} #${
|
||||
machine.hardware_configuration.config &&
|
||||
machine.hardware_configuration.config.filaments[0].color?.slice(
|
||||
0,
|
||||
6
|
||||
)
|
||||
}`
|
||||
: ''),
|
||||
isCurrent: false,
|
||||
disabled: machine.state.state !== 'idle',
|
||||
value: machine,
|
||||
}))
|
||||
},
|
||||
defaultValue: (commandBarContext) => {
|
||||
return Object.values(
|
||||
commandBarContext.machineManager.machines || []
|
||||
)[0] as components['schemas']['MachineInfoResponse']
|
||||
},
|
||||
},
|
||||
|
@ -5,6 +5,7 @@ import { Selection } from './selections'
|
||||
import { Identifier, Expr, VariableDeclaration } from 'lang/wasm'
|
||||
import { commandBarMachine } from 'machines/commandBarMachine'
|
||||
import { ReactNode } from 'react'
|
||||
import { MachineManager } from 'components/MachineManagerProvider'
|
||||
|
||||
type Icon = CustomIconName
|
||||
const PLATFORMS = ['both', 'web', 'desktop'] as const
|
||||
@ -127,6 +128,7 @@ export type CommandArgumentConfig<
|
||||
| ((
|
||||
commandBarContext: {
|
||||
argumentsToSubmit: Record<string, unknown>
|
||||
machineManager?: MachineManager
|
||||
}, // Should be the commandbarMachine's context, but it creates a circular dependency
|
||||
machineContext?: C
|
||||
) => CommandArgumentOption<OutputType>[])
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { deserialize_files } from 'wasm-lib/pkg/wasm_lib'
|
||||
import { machineManager } from './machineManager'
|
||||
import { MachineManager } from 'components/MachineManagerProvider'
|
||||
import toast from 'react-hot-toast'
|
||||
import { components } from './machine-api'
|
||||
import ModelingAppFile from './modelingAppFile'
|
||||
@ -9,7 +9,8 @@ import { MAKE_TOAST_MESSAGES } from './constants'
|
||||
export async function exportMake(
|
||||
data: ArrayBuffer,
|
||||
name: string,
|
||||
toastId: string
|
||||
toastId: string,
|
||||
machineManager: MachineManager
|
||||
): Promise<Response | null> {
|
||||
if (name === '') {
|
||||
console.error(MAKE_TOAST_MESSAGES.NO_NAME)
|
||||
@ -17,7 +18,7 @@ export async function exportMake(
|
||||
return null
|
||||
}
|
||||
|
||||
if (machineManager.machineCount() === 0) {
|
||||
if (machineManager.machines.length === 0) {
|
||||
console.error(MAKE_TOAST_MESSAGES.NO_MACHINES)
|
||||
toast.error(MAKE_TOAST_MESSAGES.NO_MACHINES, { id: toastId })
|
||||
return null
|
||||
|
@ -1,105 +0,0 @@
|
||||
import { isDesktop } from './isDesktop'
|
||||
import { components } from './machine-api'
|
||||
import { reportRejection } from './trap'
|
||||
import { toSync } from './utils'
|
||||
|
||||
export type MachinesListing = Array<
|
||||
components['schemas']['MachineInfoResponse']
|
||||
>
|
||||
|
||||
export class MachineManager {
|
||||
private _isDesktop: boolean = isDesktop()
|
||||
private _machines: MachinesListing = []
|
||||
private _machineApiIp: string | null = null
|
||||
private _currentMachine: components['schemas']['MachineInfoResponse'] | null =
|
||||
null
|
||||
|
||||
constructor() {
|
||||
if (!this._isDesktop) {
|
||||
return
|
||||
}
|
||||
|
||||
this.updateMachines().catch(reportRejection)
|
||||
}
|
||||
|
||||
start() {
|
||||
if (!this._isDesktop) {
|
||||
return
|
||||
}
|
||||
|
||||
// Start a background job to update the machines every ten seconds.
|
||||
// If MDNS is already watching, this timeout will wait until it's done to trigger the
|
||||
// finding again.
|
||||
let timeoutId: ReturnType<typeof setTimeout> | undefined = undefined
|
||||
const timeoutLoop = () => {
|
||||
clearTimeout(timeoutId)
|
||||
timeoutId = setTimeout(
|
||||
toSync(async () => {
|
||||
await this.updateMachineApiIp()
|
||||
await this.updateMachines()
|
||||
timeoutLoop()
|
||||
}, reportRejection),
|
||||
10000
|
||||
)
|
||||
}
|
||||
timeoutLoop()
|
||||
}
|
||||
|
||||
get machines(): MachinesListing {
|
||||
return this._machines
|
||||
}
|
||||
|
||||
machineCount(): number {
|
||||
return this._machines.length
|
||||
}
|
||||
|
||||
get machineApiIp(): string | null {
|
||||
return this._machineApiIp
|
||||
}
|
||||
|
||||
// Get the reason message for why there are no machines.
|
||||
noMachinesReason(): string | undefined {
|
||||
if (this.machineCount() > 0) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (this.machineApiIp === null) {
|
||||
return 'Machine API server was not discovered'
|
||||
}
|
||||
|
||||
return 'Machine API server was discovered, but no machines are available'
|
||||
}
|
||||
|
||||
get currentMachine(): components['schemas']['MachineInfoResponse'] | null {
|
||||
return this._currentMachine
|
||||
}
|
||||
|
||||
set currentMachine(
|
||||
machine: components['schemas']['MachineInfoResponse'] | null
|
||||
) {
|
||||
this._currentMachine = machine
|
||||
}
|
||||
|
||||
private async updateMachines(): Promise<void> {
|
||||
if (!this._isDesktop) {
|
||||
return
|
||||
}
|
||||
|
||||
if (this._machineApiIp === null) {
|
||||
return
|
||||
}
|
||||
|
||||
this._machines = await window.electron.listMachines(this._machineApiIp)
|
||||
}
|
||||
|
||||
private async updateMachineApiIp(): Promise<void> {
|
||||
if (!this._isDesktop) {
|
||||
return
|
||||
}
|
||||
|
||||
this._machineApiIp = await window.electron.getMachineApiIp()
|
||||
}
|
||||
}
|
||||
|
||||
export const machineManager = new MachineManager()
|
||||
machineManager.start()
|
@ -7,6 +7,7 @@ import {
|
||||
} from 'lib/commandTypes'
|
||||
import { Selections } from 'lib/selections'
|
||||
import { getCommandArgumentKclValuesOnly } from 'lib/commandUtils'
|
||||
import { MachineManager } from 'components/MachineManagerProvider'
|
||||
|
||||
export type CommandBarContext = {
|
||||
commands: Command[]
|
||||
@ -14,6 +15,7 @@ export type CommandBarContext = {
|
||||
currentArgument?: CommandArgument<unknown> & { name: string }
|
||||
selectionRanges: Selections
|
||||
argumentsToSubmit: { [x: string]: unknown }
|
||||
machineManager: MachineManager
|
||||
}
|
||||
|
||||
export type CommandBarMachineEvent =
|
||||
@ -71,6 +73,7 @@ export type CommandBarMachineEvent =
|
||||
type: 'Change current argument'
|
||||
data: { [x: string]: CommandArgumentWithName<unknown> }
|
||||
}
|
||||
| { type: 'Set machine manager'; data: MachineManager }
|
||||
|
||||
export const commandBarMachine = setup({
|
||||
types: {
|
||||
@ -90,6 +93,12 @@ export const commandBarMachine = setup({
|
||||
}
|
||||
},
|
||||
}),
|
||||
'Set machine manager': assign({
|
||||
machineManager: ({ event, context }) => {
|
||||
if (event.type !== 'Set machine manager') return context.machineManager
|
||||
return event.data
|
||||
},
|
||||
}),
|
||||
'Execute command': ({ context, event }) => {
|
||||
const { selectedCommand } = context
|
||||
if (!selectedCommand) return
|
||||
@ -339,6 +348,13 @@ export const commandBarMachine = setup({
|
||||
codeBasedSelections: [],
|
||||
},
|
||||
argumentsToSubmit: {},
|
||||
machineManager: {
|
||||
machines: [],
|
||||
machineApiIp: null,
|
||||
currentMachine: null,
|
||||
setCurrentMachine: () => {},
|
||||
noMachinesReason: () => undefined,
|
||||
},
|
||||
},
|
||||
id: 'Command Bar',
|
||||
initial: 'Closed',
|
||||
@ -520,6 +536,11 @@ export const commandBarMachine = setup({
|
||||
},
|
||||
},
|
||||
on: {
|
||||
'Set machine manager': {
|
||||
reenter: false,
|
||||
actions: 'Set machine manager',
|
||||
},
|
||||
|
||||
Close: {
|
||||
target: '.Closed',
|
||||
},
|
||||
|
@ -64,6 +64,7 @@ import toast from 'react-hot-toast'
|
||||
import { ToolbarModeName } from 'lib/toolbar'
|
||||
import { quaternionFromUpNForward } from 'clientSideScene/helpers'
|
||||
import { Vector3 } from 'three'
|
||||
import { MachineManager } from 'components/MachineManagerProvider'
|
||||
|
||||
export const MODELING_PERSIST_KEY = 'MODELING_PERSIST_KEY'
|
||||
|
||||
@ -301,6 +302,7 @@ export const getPersistedContext = (): Partial<PersistedModelingContext> => {
|
||||
export interface ModelingMachineContext {
|
||||
currentMode: ToolbarModeName
|
||||
currentTool: SketchTool
|
||||
machineManager: MachineManager
|
||||
selection: string[]
|
||||
selectionRanges: Selections
|
||||
sketchDetails: SketchDetails | null
|
||||
@ -315,6 +317,13 @@ export interface ModelingMachineContext {
|
||||
export const modelingMachineDefaultContext: ModelingMachineContext = {
|
||||
currentMode: 'modeling',
|
||||
currentTool: 'none',
|
||||
machineManager: {
|
||||
machines: [],
|
||||
machineApiIp: null,
|
||||
currentMachine: null,
|
||||
setCurrentMachine: () => {},
|
||||
noMachinesReason: () => undefined,
|
||||
},
|
||||
selection: [],
|
||||
selectionRanges: {
|
||||
otherSelections: [],
|
||||
|
@ -4,7 +4,7 @@ import fs from 'node:fs/promises'
|
||||
import os from 'node:os'
|
||||
import fsSync from 'node:fs'
|
||||
import packageJson from '../package.json'
|
||||
import { MachinesListing } from 'lib/machineManager'
|
||||
import { MachinesListing } from 'components/MachineManagerProvider'
|
||||
import chokidar from 'chokidar'
|
||||
|
||||
const open = (args: any) => ipcRenderer.invoke('dialog.showOpenDialog', args)
|
||||
|
16
src/wasm-lib/Cargo.lock
generated
@ -1542,7 +1542,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "kcl-lib"
|
||||
version = "0.2.22"
|
||||
version = "0.2.23"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"approx 0.5.1",
|
||||
@ -1617,7 +1617,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "kcl-test-server"
|
||||
version = "0.1.14"
|
||||
version = "0.1.15"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"hyper 0.14.30",
|
||||
@ -1644,9 +1644,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "kittycad"
|
||||
version = "0.3.23"
|
||||
version = "0.3.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "71b6f0c34165939697548dd0c94221200dbb8b5d1c84b5d8e803e70f9f720ea7"
|
||||
checksum = "f6359cc0a1bbccbcf78775eea17a033cf2aa89d3fe6a9784f8ce94e5f882c185"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@ -1684,9 +1684,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "kittycad-modeling-cmds"
|
||||
version = "0.2.70"
|
||||
version = "0.2.71"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b135696d07a4fab928e5abace4dd05f4976eafab5d73e5747a85dc5a684b936c"
|
||||
checksum = "c6d2160dcb0e5373b1242a760dbf17fb9c12de523c410c11b145381c852b377b"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
@ -2586,9 +2586,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.11.0"
|
||||
version = "1.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "38200e5ee88914975b69f657f0801b6f6dccafd44fd9326302a4aaeecfacb1d8"
|
||||
checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
|
@ -71,8 +71,8 @@ members = [
|
||||
|
||||
[workspace.dependencies]
|
||||
http = "1"
|
||||
kittycad = { version = "0.3.23", default-features = false, features = ["js", "requests"] }
|
||||
kittycad-modeling-cmds = { version = "0.2.70", features = ["websocket"] }
|
||||
kittycad = { version = "0.3.25", default-features = false, features = ["js", "requests"] }
|
||||
kittycad-modeling-cmds = { version = "0.2.71", features = ["websocket"] }
|
||||
|
||||
[[test]]
|
||||
name = "executor"
|
||||
|
@ -17,7 +17,7 @@ convert_case = "0.6.0"
|
||||
once_cell = "1.20.2"
|
||||
proc-macro2 = "1"
|
||||
quote = "1"
|
||||
regex = "1.10"
|
||||
regex = "1.11"
|
||||
serde = { version = "1.0.213", features = ["derive"] }
|
||||
serde_tokenstream = "0.2"
|
||||
syn = { version = "2.0.85", features = ["full"] }
|
||||
|
@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "kcl-test-server"
|
||||
description = "A test server for KCL"
|
||||
version = "0.1.14"
|
||||
version = "0.1.15"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
|
||||
|
@ -31,7 +31,7 @@ pub struct ServerArgs {
|
||||
/// Where to find the engine.
|
||||
/// If none, uses the prod engine.
|
||||
/// This is useful for testing a local engine instance.
|
||||
/// Overridden by the $LOCAL_ENGINE_ADDR environment variable.
|
||||
/// Overridden by the $ZOO_HOST environment variable.
|
||||
pub engine_address: Option<String>,
|
||||
}
|
||||
|
||||
@ -44,8 +44,8 @@ impl ServerArgs {
|
||||
num_engine_conns: pargs.opt_value_from_str("--num-engine-conns")?.unwrap_or(1),
|
||||
engine_address: pargs.opt_value_from_str("--engine-address")?,
|
||||
};
|
||||
if let Ok(addr) = std::env::var("LOCAL_ENGINE_ADDR") {
|
||||
println!("Overriding engine address via $LOCAL_ENGINE_ADDR");
|
||||
if let Ok(addr) = std::env::var("ZOO_HOST") {
|
||||
println!("Overriding engine address via $ZOO_HOST");
|
||||
args.engine_address = Some(addr);
|
||||
}
|
||||
println!("Config is {args:?}");
|
||||
|
@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "kcl-lib"
|
||||
description = "KittyCAD Language implementation and tools"
|
||||
version = "0.2.22"
|
||||
version = "0.2.23"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
repository = "https://github.com/KittyCAD/modeling-app"
|
||||
|
@ -1127,7 +1127,7 @@ pub struct TagEngineInfo {
|
||||
/// The sketch the tag is on.
|
||||
pub sketch: uuid::Uuid,
|
||||
/// The path the tag is on.
|
||||
pub path: Option<BasePath>,
|
||||
pub path: Option<Path>,
|
||||
/// The surface information for the tag.
|
||||
pub surface: Option<ExtrudeSurface>,
|
||||
}
|
||||
@ -1206,7 +1206,7 @@ impl Sketch {
|
||||
tag_identifier.info = Some(TagEngineInfo {
|
||||
id: base.geo_meta.id,
|
||||
sketch: self.id,
|
||||
path: Some(base.clone()),
|
||||
path: Some(current_path.clone()),
|
||||
surface: None,
|
||||
});
|
||||
|
||||
@ -1656,6 +1656,43 @@ pub enum Path {
|
||||
#[serde(flatten)]
|
||||
base: BasePath,
|
||||
},
|
||||
/// A circular arc, not necessarily tangential to the current point.
|
||||
Arc {
|
||||
#[serde(flatten)]
|
||||
base: BasePath,
|
||||
/// Center of the circle that this arc is drawn on.
|
||||
center: [f64; 2],
|
||||
/// Radius of the circle that this arc is drawn on.
|
||||
radius: f64,
|
||||
},
|
||||
}
|
||||
|
||||
/// What kind of path is this?
|
||||
#[derive(Display)]
|
||||
enum PathType {
|
||||
ToPoint,
|
||||
Base,
|
||||
TangentialArc,
|
||||
TangentialArcTo,
|
||||
Circle,
|
||||
Horizontal,
|
||||
AngledLineTo,
|
||||
Arc,
|
||||
}
|
||||
|
||||
impl From<&Path> for PathType {
|
||||
fn from(value: &Path) -> Self {
|
||||
match value {
|
||||
Path::ToPoint { .. } => Self::ToPoint,
|
||||
Path::TangentialArcTo { .. } => Self::TangentialArcTo,
|
||||
Path::TangentialArc { .. } => Self::TangentialArc,
|
||||
Path::Circle { .. } => Self::Circle,
|
||||
Path::Horizontal { .. } => Self::Horizontal,
|
||||
Path::AngledLineTo { .. } => Self::AngledLineTo,
|
||||
Path::Base { .. } => Self::Base,
|
||||
Path::Arc { .. } => Self::Arc,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Path {
|
||||
@ -1668,6 +1705,7 @@ impl Path {
|
||||
Path::TangentialArcTo { base, .. } => base.geo_meta.id,
|
||||
Path::TangentialArc { base, .. } => base.geo_meta.id,
|
||||
Path::Circle { base, .. } => base.geo_meta.id,
|
||||
Path::Arc { base, .. } => base.geo_meta.id,
|
||||
}
|
||||
}
|
||||
|
||||
@ -1680,6 +1718,7 @@ impl Path {
|
||||
Path::TangentialArcTo { base, .. } => base.tag.clone(),
|
||||
Path::TangentialArc { base, .. } => base.tag.clone(),
|
||||
Path::Circle { base, .. } => base.tag.clone(),
|
||||
Path::Arc { base, .. } => base.tag.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -1692,6 +1731,47 @@ impl Path {
|
||||
Path::TangentialArcTo { base, .. } => base,
|
||||
Path::TangentialArc { base, .. } => base,
|
||||
Path::Circle { base, .. } => base,
|
||||
Path::Arc { base, .. } => base,
|
||||
}
|
||||
}
|
||||
|
||||
/// Where does this path segment start?
|
||||
pub fn get_from(&self) -> &[f64; 2] {
|
||||
&self.get_base().from
|
||||
}
|
||||
/// Where does this path segment end?
|
||||
pub fn get_to(&self) -> &[f64; 2] {
|
||||
&self.get_base().to
|
||||
}
|
||||
|
||||
/// Length of this path segment, in cartesian plane.
|
||||
pub fn length(&self) -> f64 {
|
||||
match self {
|
||||
Self::ToPoint { .. } | Self::Base { .. } | Self::Horizontal { .. } | Self::AngledLineTo { .. } => {
|
||||
linear_distance(self.get_from(), self.get_to())
|
||||
}
|
||||
Self::TangentialArc {
|
||||
base: _,
|
||||
center,
|
||||
ccw: _,
|
||||
}
|
||||
| Self::TangentialArcTo {
|
||||
base: _,
|
||||
center,
|
||||
ccw: _,
|
||||
} => {
|
||||
// The radius can be calculated as the linear distance between `to` and `center`,
|
||||
// or between `from` and `center`. They should be the same.
|
||||
let radius = linear_distance(self.get_from(), center);
|
||||
debug_assert_eq!(radius, linear_distance(self.get_to(), center));
|
||||
// TODO: Call engine utils to figure this out.
|
||||
linear_distance(self.get_from(), self.get_to())
|
||||
}
|
||||
Self::Circle { radius, .. } => 2.0 * std::f64::consts::PI * radius,
|
||||
Self::Arc { .. } => {
|
||||
// TODO: Call engine utils to figure this out.
|
||||
linear_distance(self.get_from(), self.get_to())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1704,10 +1784,22 @@ impl Path {
|
||||
Path::TangentialArcTo { base, .. } => Some(base),
|
||||
Path::TangentialArc { base, .. } => Some(base),
|
||||
Path::Circle { base, .. } => Some(base),
|
||||
Path::Arc { base, .. } => Some(base),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Compute the straight-line distance between a pair of (2D) points.
|
||||
#[rustfmt::skip]
|
||||
fn linear_distance(
|
||||
[x0, y0]: &[f64; 2],
|
||||
[x1, y1]: &[f64; 2]
|
||||
) -> f64 {
|
||||
let y_sq = (y1 - y0).powi(2);
|
||||
let x_sq = (x1 - x0).powi(2);
|
||||
(y_sq + x_sq).sqrt()
|
||||
}
|
||||
|
||||
/// An extrude surface.
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||
#[ts(export)]
|
||||
@ -1888,9 +1980,73 @@ impl From<crate::settings::types::ModelingSettings> for ExecutorSettings {
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new zoo api client.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub fn new_zoo_client(token: Option<String>, engine_addr: Option<String>) -> Result<kittycad::Client> {
|
||||
let user_agent = concat!(env!("CARGO_PKG_NAME"), ".rs/", env!("CARGO_PKG_VERSION"),);
|
||||
let http_client = reqwest::Client::builder()
|
||||
.user_agent(user_agent)
|
||||
// For file conversions we need this to be long.
|
||||
.timeout(std::time::Duration::from_secs(600))
|
||||
.connect_timeout(std::time::Duration::from_secs(60));
|
||||
let ws_client = reqwest::Client::builder()
|
||||
.user_agent(user_agent)
|
||||
// For file conversions we need this to be long.
|
||||
.timeout(std::time::Duration::from_secs(600))
|
||||
.connect_timeout(std::time::Duration::from_secs(60))
|
||||
.connection_verbose(true)
|
||||
.tcp_keepalive(std::time::Duration::from_secs(600))
|
||||
.http1_only();
|
||||
|
||||
let zoo_token_env = std::env::var("ZOO_API_TOKEN");
|
||||
|
||||
let token = if let Some(token) = token {
|
||||
token
|
||||
} else if let Ok(token) = std::env::var("KITTYCAD_API_TOKEN") {
|
||||
if let Ok(zoo_token) = zoo_token_env {
|
||||
if zoo_token != token {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Both environment variables KITTYCAD_API_TOKEN=`{}` and ZOO_API_TOKEN=`{}` are set. Use only one.",
|
||||
token,
|
||||
zoo_token
|
||||
));
|
||||
}
|
||||
}
|
||||
token
|
||||
} else if let Ok(token) = zoo_token_env {
|
||||
token
|
||||
} else {
|
||||
return Err(anyhow::anyhow!(
|
||||
"No API token found in environment variables. Use KITTYCAD_API_TOKEN or ZOO_API_TOKEN"
|
||||
));
|
||||
};
|
||||
|
||||
// Create the client.
|
||||
let mut client = kittycad::Client::new_from_reqwest(token, http_client, ws_client);
|
||||
// Set an engine address if it's set.
|
||||
let kittycad_host_env = std::env::var("KITTYCAD_HOST");
|
||||
if let Some(addr) = engine_addr {
|
||||
client.set_base_url(addr);
|
||||
} else if let Ok(addr) = std::env::var("ZOO_HOST") {
|
||||
if let Ok(kittycad_host) = kittycad_host_env {
|
||||
if kittycad_host != addr {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Both environment variables KITTYCAD_HOST=`{}` and ZOO_HOST=`{}` are set. Use only one.",
|
||||
kittycad_host,
|
||||
addr
|
||||
));
|
||||
}
|
||||
}
|
||||
client.set_base_url(addr);
|
||||
} else if let Ok(addr) = kittycad_host_env {
|
||||
client.set_base_url(addr);
|
||||
}
|
||||
|
||||
Ok(client)
|
||||
}
|
||||
|
||||
impl ExecutorContext {
|
||||
/// Create a new default executor context.
|
||||
/// Also returns the response HTTP headers from the server.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub async fn new(client: &kittycad::Client, settings: ExecutorSettings) -> Result<Self> {
|
||||
let (ws, _headers) = client
|
||||
@ -1935,6 +2091,35 @@ impl ExecutorContext {
|
||||
})
|
||||
}
|
||||
|
||||
/// Create a new default executor context.
|
||||
/// With a kittycad client.
|
||||
/// This allows for passing in `ZOO_API_TOKEN` and `ZOO_HOST` as environment
|
||||
/// variables.
|
||||
/// But also allows for passing in a token and engine address directly.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub async fn new_with_client(
|
||||
settings: ExecutorSettings,
|
||||
token: Option<String>,
|
||||
engine_addr: Option<String>,
|
||||
) -> Result<Self> {
|
||||
// Create the client.
|
||||
let client = new_zoo_client(token, engine_addr)?;
|
||||
|
||||
let ctx = Self::new(&client, settings).await?;
|
||||
Ok(ctx)
|
||||
}
|
||||
|
||||
/// Create a new default executor context.
|
||||
/// With the default kittycad client.
|
||||
/// This allows for passing in `ZOO_API_TOKEN` and `ZOO_HOST` as environment
|
||||
/// variables.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub async fn new_with_default_client(settings: ExecutorSettings) -> Result<Self> {
|
||||
// Create the client.
|
||||
let ctx = Self::new_with_client(settings, None, None).await?;
|
||||
Ok(ctx)
|
||||
}
|
||||
|
||||
pub fn is_mock(&self) -> bool {
|
||||
self.context_type == ContextType::Mock || self.context_type == ContextType::MockCustomForwarded
|
||||
}
|
||||
@ -1942,35 +2127,7 @@ impl ExecutorContext {
|
||||
/// For executing unit tests.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub async fn new_for_unit_test(units: UnitLength, engine_addr: Option<String>) -> Result<Self> {
|
||||
let user_agent = concat!(env!("CARGO_PKG_NAME"), ".rs/", env!("CARGO_PKG_VERSION"));
|
||||
let http_client = reqwest::Client::builder()
|
||||
.user_agent(user_agent)
|
||||
// For file conversions we need this to be long.
|
||||
.timeout(std::time::Duration::from_secs(600))
|
||||
.connect_timeout(std::time::Duration::from_secs(60));
|
||||
let ws_client = reqwest::Client::builder()
|
||||
.user_agent(user_agent)
|
||||
// For file conversions we need this to be long.
|
||||
.timeout(std::time::Duration::from_secs(600))
|
||||
.connect_timeout(std::time::Duration::from_secs(60))
|
||||
.connection_verbose(true)
|
||||
.tcp_keepalive(std::time::Duration::from_secs(600))
|
||||
.http1_only();
|
||||
|
||||
let token = std::env::var("KITTYCAD_API_TOKEN").expect("KITTYCAD_API_TOKEN not set");
|
||||
|
||||
// Create the client.
|
||||
let mut client = kittycad::Client::new_from_reqwest(token, http_client, ws_client);
|
||||
// Set a local engine address if it's set.
|
||||
if let Ok(addr) = std::env::var("LOCAL_ENGINE_ADDR") {
|
||||
client.set_base_url(addr);
|
||||
}
|
||||
if let Some(addr) = engine_addr {
|
||||
client.set_base_url(addr);
|
||||
}
|
||||
|
||||
let ctx = ExecutorContext::new(
|
||||
&client,
|
||||
let ctx = ExecutorContext::new_with_client(
|
||||
ExecutorSettings {
|
||||
units,
|
||||
highlight_edges: true,
|
||||
@ -1978,6 +2135,8 @@ impl ExecutorContext {
|
||||
show_grid: false,
|
||||
replay: None,
|
||||
},
|
||||
None,
|
||||
engine_addr,
|
||||
)
|
||||
.await?;
|
||||
Ok(ctx)
|
||||
|
@ -3,41 +3,13 @@ use std::sync::{Arc, RwLock};
|
||||
use anyhow::Result;
|
||||
use tower_lsp::LanguageServer;
|
||||
|
||||
fn new_zoo_client() -> kittycad::Client {
|
||||
let user_agent = concat!(env!("CARGO_PKG_NAME"), ".rs/", env!("CARGO_PKG_VERSION"),);
|
||||
let http_client = reqwest::Client::builder()
|
||||
.user_agent(user_agent)
|
||||
// For file conversions we need this to be long.
|
||||
.timeout(std::time::Duration::from_secs(600))
|
||||
.connect_timeout(std::time::Duration::from_secs(60));
|
||||
let ws_client = reqwest::Client::builder()
|
||||
.user_agent(user_agent)
|
||||
// For file conversions we need this to be long.
|
||||
.timeout(std::time::Duration::from_secs(600))
|
||||
.connect_timeout(std::time::Duration::from_secs(60))
|
||||
.connection_verbose(true)
|
||||
.tcp_keepalive(std::time::Duration::from_secs(600))
|
||||
.http1_only();
|
||||
|
||||
let token = std::env::var("KITTYCAD_API_TOKEN").expect("KITTYCAD_API_TOKEN not set");
|
||||
|
||||
// Create the client.
|
||||
let mut client = kittycad::Client::new_from_reqwest(token, http_client, ws_client);
|
||||
// Set a local engine address if it's set.
|
||||
if let Ok(addr) = std::env::var("LOCAL_ENGINE_ADDR") {
|
||||
client.set_base_url(addr);
|
||||
}
|
||||
|
||||
client
|
||||
}
|
||||
|
||||
// Create a fake kcl lsp server for testing.
|
||||
pub async fn kcl_lsp_server(execute: bool) -> Result<crate::lsp::kcl::Backend> {
|
||||
let stdlib = crate::std::StdLib::new();
|
||||
let stdlib_completions = crate::lsp::kcl::get_completions_from_stdlib(&stdlib)?;
|
||||
let stdlib_signatures = crate::lsp::kcl::get_signatures_from_stdlib(&stdlib)?;
|
||||
|
||||
let zoo_client = new_zoo_client();
|
||||
let zoo_client = crate::executor::new_zoo_client(None, None)?;
|
||||
|
||||
let executor_ctx = if execute {
|
||||
Some(crate::executor::ExecutorContext::new(&zoo_client, Default::default()).await?)
|
||||
|
@ -149,7 +149,7 @@ pub(crate) async fn do_post_extrude(
|
||||
}
|
||||
|
||||
let edge_id = sketch.paths.iter().find_map(|segment| match segment {
|
||||
Path::ToPoint { base } | Path::Circle { base, .. } => Some(base.geo_meta.id),
|
||||
Path::ToPoint { base } | Path::Circle { base, .. } | Path::Arc { base, .. } => Some(base.geo_meta.id),
|
||||
_ => None,
|
||||
});
|
||||
|
||||
@ -234,7 +234,10 @@ pub(crate) async fn do_post_extrude(
|
||||
.flat_map(|path| {
|
||||
if let Some(Some(actual_face_id)) = face_id_map.get(&path.get_base().geo_meta.id) {
|
||||
match path {
|
||||
Path::TangentialArc { .. } | Path::TangentialArcTo { .. } | Path::Circle { .. } => {
|
||||
Path::Arc { .. }
|
||||
| Path::TangentialArc { .. }
|
||||
| Path::TangentialArcTo { .. }
|
||||
| Path::Circle { .. } => {
|
||||
let extrude_surface = ExtrudeSurface::ExtrudeArc(crate::executor::ExtrudeArc {
|
||||
face_id: *actual_face_id,
|
||||
tag: path.get_base().tag.clone(),
|
||||
|
@ -42,7 +42,7 @@ fn inner_segment_end_x(tag: &TagIdentifier, exec_state: &mut ExecState, args: Ar
|
||||
})
|
||||
})?;
|
||||
|
||||
Ok(path.to[0])
|
||||
Ok(path.get_base().to[0])
|
||||
}
|
||||
|
||||
/// Returns the segment end of y.
|
||||
@ -79,7 +79,7 @@ fn inner_segment_end_y(tag: &TagIdentifier, exec_state: &mut ExecState, args: Ar
|
||||
})
|
||||
})?;
|
||||
|
||||
Ok(path.to[1])
|
||||
Ok(path.get_to()[1])
|
||||
}
|
||||
|
||||
/// Returns the last segment of x.
|
||||
@ -202,7 +202,7 @@ fn inner_segment_length(tag: &TagIdentifier, exec_state: &mut ExecState, args: A
|
||||
})
|
||||
})?;
|
||||
|
||||
let result = ((path.from[1] - path.to[1]).powi(2) + (path.from[0] - path.to[0]).powi(2)).sqrt();
|
||||
let result = path.length();
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
@ -242,7 +242,7 @@ fn inner_segment_angle(tag: &TagIdentifier, exec_state: &mut ExecState, args: Ar
|
||||
})
|
||||
})?;
|
||||
|
||||
let result = between(path.from.into(), path.to.into());
|
||||
let result = between(path.get_from().into(), path.get_to().into());
|
||||
|
||||
Ok(result.to_degrees())
|
||||
}
|
||||
@ -286,7 +286,7 @@ fn inner_angle_to_match_length_x(
|
||||
})
|
||||
})?;
|
||||
|
||||
let length = ((path.from[1] - path.to[1]).powi(2) + (path.from[0] - path.to[0]).powi(2)).sqrt();
|
||||
let length = path.length();
|
||||
|
||||
let last_line = sketch
|
||||
.paths
|
||||
@ -350,7 +350,7 @@ fn inner_angle_to_match_length_y(
|
||||
})
|
||||
})?;
|
||||
|
||||
let length = ((path.from[1] - path.to[1]).powi(2) + (path.from[0] - path.to[0]).powi(2)).sqrt();
|
||||
let length = path.length();
|
||||
|
||||
let last_line = sketch
|
||||
.paths
|
||||
|
@ -813,7 +813,7 @@ async fn inner_angled_line_that_intersects(
|
||||
|
||||
let from = sketch.current_pen_position()?;
|
||||
let to = intersection_with_parallel_line(
|
||||
&[path.from.into(), path.to.into()],
|
||||
&[path.get_from().into(), path.get_to().into()],
|
||||
data.offset.unwrap_or_default(),
|
||||
data.angle,
|
||||
from,
|
||||
@ -1244,7 +1244,9 @@ pub(crate) async fn inner_start_profile_at(
|
||||
tag_identifier.info = Some(TagEngineInfo {
|
||||
id: current_path.geo_meta.id,
|
||||
sketch: path_id,
|
||||
path: Some(current_path.clone()),
|
||||
path: Some(Path::Base {
|
||||
base: current_path.clone(),
|
||||
}),
|
||||
surface: None,
|
||||
});
|
||||
HashMap::from([(tag.name.to_string(), tag_identifier)])
|
||||
@ -1528,7 +1530,7 @@ pub(crate) async fn inner_arc(
|
||||
)
|
||||
.await?;
|
||||
|
||||
let current_path = Path::ToPoint {
|
||||
let current_path = Path::Arc {
|
||||
base: BasePath {
|
||||
from: from.into(),
|
||||
to: end.into(),
|
||||
@ -1538,6 +1540,8 @@ pub(crate) async fn inner_arc(
|
||||
metadata: args.source_range.into(),
|
||||
},
|
||||
},
|
||||
center: center.into(),
|
||||
radius,
|
||||
};
|
||||
|
||||
let mut new_sketch = sketch.clone();
|
||||
|
@ -21,7 +21,7 @@ pub fn normalize(angle: Angle) -> Angle {
|
||||
Angle::from_degrees(if result > 180.0 { result - 360.0 } else { result })
|
||||
}
|
||||
|
||||
/// Gives the ▲-angle between from and to angles (shortest path), use radians.
|
||||
/// Gives the ▲-angle between from and to angles (shortest path)
|
||||
///
|
||||
/// Sign of the returned angle denotes direction, positive means counterClockwise 🔄
|
||||
/// # Examples
|
||||
|
@ -43,38 +43,7 @@ async fn do_execute_and_snapshot(ctx: &ExecutorContext, code: &str) -> anyhow::R
|
||||
}
|
||||
|
||||
async fn new_context(units: UnitLength, with_auth: bool) -> anyhow::Result<ExecutorContext> {
|
||||
let user_agent = concat!(env!("CARGO_PKG_NAME"), ".rs/", env!("CARGO_PKG_VERSION"),);
|
||||
let http_client = reqwest::Client::builder()
|
||||
.user_agent(user_agent)
|
||||
// For file conversions we need this to be long.
|
||||
.timeout(std::time::Duration::from_secs(600))
|
||||
.connect_timeout(std::time::Duration::from_secs(60));
|
||||
let ws_client = reqwest::Client::builder()
|
||||
.user_agent(user_agent)
|
||||
// For file conversions we need this to be long.
|
||||
.timeout(std::time::Duration::from_secs(600))
|
||||
.connect_timeout(std::time::Duration::from_secs(60))
|
||||
.connection_verbose(true)
|
||||
.tcp_keepalive(std::time::Duration::from_secs(600))
|
||||
.http1_only();
|
||||
|
||||
let token = if with_auth {
|
||||
std::env::var("KITTYCAD_API_TOKEN").expect("KITTYCAD_API_TOKEN not set")
|
||||
} else {
|
||||
"bad_token".to_string()
|
||||
};
|
||||
|
||||
// Create the client.
|
||||
let mut client = kittycad::Client::new_from_reqwest(token, http_client, ws_client);
|
||||
// Set a local engine address if it's set.
|
||||
if let Ok(addr) = std::env::var("LOCAL_ENGINE_ADDR") {
|
||||
if with_auth {
|
||||
client.set_base_url(addr);
|
||||
}
|
||||
}
|
||||
|
||||
let ctx = ExecutorContext::new(
|
||||
&client,
|
||||
let ctx = ExecutorContext::new_with_client(
|
||||
ExecutorSettings {
|
||||
units,
|
||||
highlight_edges: true,
|
||||
@ -82,6 +51,8 @@ async fn new_context(units: UnitLength, with_auth: bool) -> anyhow::Result<Execu
|
||||
show_grid: false,
|
||||
replay: None,
|
||||
},
|
||||
if with_auth { None } else { Some("bad_token".to_string()) },
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
Ok(ctx)
|
||||
|
@ -0,0 +1,15 @@
|
||||
exampleSketch = startSketchOn('XZ')
|
||||
|> startProfileAt([0, 0], %)
|
||||
|> line([0, 2], %)
|
||||
|> line([3, 1], %)
|
||||
|> line([0, -4], %)
|
||||
|> close(%)
|
||||
|> extrude(1, %)
|
||||
|
||||
pattn1 = patternLinear3d({
|
||||
axis: [1, 0, 0],
|
||||
instances: 7,
|
||||
distance: 6
|
||||
}, exampleSketch)
|
||||
|
||||
pattn2 = patternCircular3d({axis: [0,0, 1], center: [-20, -20, -20], instances: 41, arcDegrees: 360, rotateDuplicates: false}, pattn1)
|
@ -0,0 +1,19 @@
|
||||
exampleSketch = startSketchOn('XZ')
|
||||
|> startProfileAt([0, 0], %)
|
||||
|> line([0, 2], %)
|
||||
|> line([3, 1], %)
|
||||
|> line([0, -4], %)
|
||||
|> close(%)
|
||||
|> extrude(1, %)
|
||||
|
||||
pattn1 = patternLinear3d({
|
||||
axis: [1, 0, 0],
|
||||
instances: 7,
|
||||
distance: 6
|
||||
}, exampleSketch)
|
||||
|
||||
pattn2 = patternLinear3d({
|
||||
axis: [0, 0, 1],
|
||||
distance: 1,
|
||||
instances: 7
|
||||
}, pattn1)
|
6
src/wasm-lib/tests/executor/inputs/neg_xz_plane.kcl
Normal file
@ -0,0 +1,6 @@
|
||||
part001 = startSketchOn('-XZ')
|
||||
|> startProfileAt([0, 0], %)
|
||||
|> lineTo([100, 100], %)
|
||||
|> lineTo([100, 0], %)
|
||||
|> close(%)
|
||||
|> extrude(5 + 7, %)
|
@ -0,0 +1,55 @@
|
||||
// Shelf Bracket
|
||||
// This is a shelf bracket made out of 6061-T6 aluminum sheet metal. The required thickness is calculated based on a point load of 300 lbs applied to the end of the shelf. There are two brackets holding up the shelf, so the moment experienced is divided by 2. The shelf is 1 foot long from the wall.
|
||||
|
||||
|
||||
// Define our bracket feet lengths
|
||||
shelfMountL = 8 // The length of the bracket holding up the shelf is 6 inches
|
||||
wallMountL = 6 // the length of the bracket
|
||||
|
||||
|
||||
// Define constants required to calculate the thickness needed to support 300 lbs
|
||||
sigmaAllow = 35000 // psi
|
||||
width = 6 // inch
|
||||
p = 300 // Force on shelf - lbs
|
||||
L = 12 // inches
|
||||
M = L * p / 2 // Moment experienced at fixed end of bracket
|
||||
FOS = 2 // Factor of safety of 2 to be conservative
|
||||
|
||||
|
||||
// Calculate the thickness off the bending stress and factor of safety
|
||||
thickness = sqrt(6 * M * FOS / (width * sigmaAllow))
|
||||
|
||||
// 0.25 inch fillet radius
|
||||
filletR = 0.25
|
||||
|
||||
// Sketch the bracket and extrude with fillets
|
||||
bracket = startSketchOn('XY')
|
||||
|> startProfileAt([0, 0], %)
|
||||
|> line([0, wallMountL], %, $outerEdge)
|
||||
|> line([-shelfMountL, 0], %, $seg01)
|
||||
|> line([0, -thickness], %)
|
||||
|> line([shelfMountL - thickness, 0], %, $innerEdge)
|
||||
|> line([0, -wallMountL + thickness], %)
|
||||
|> close(%)
|
||||
|> extrude(width, %)
|
||||
|> fillet({
|
||||
radius: filletR,
|
||||
tags: [
|
||||
getNextAdjacentEdge(innerEdge)
|
||||
]
|
||||
}, %)
|
||||
|> fillet({
|
||||
radius: filletR + thickness,
|
||||
tags: [
|
||||
getNextAdjacentEdge(outerEdge)
|
||||
]
|
||||
}, %)
|
||||
|
||||
sketch001 = startSketchOn(bracket, seg01)
|
||||
|> startProfileAt([4.28, 3.83], %)
|
||||
|> line([2.17, -0.03], %)
|
||||
|> line([-0.07, -1.8], %)
|
||||
|> line([-2.07, 0.05], %)
|
||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||
|> close(%)
|
||||
|> extrude(10, %)
|
6
src/wasm-lib/tests/executor/inputs/xz_plane.kcl
Normal file
@ -0,0 +1,6 @@
|
||||
part001 = startSketchOn('XZ')
|
||||
|> startProfileAt([0, 0], %)
|
||||
|> lineTo([100, 100], %)
|
||||
|> lineTo([100, 0], %)
|
||||
|> close(%)
|
||||
|> extrude(5 + 7, %)
|
@ -1389,84 +1389,6 @@ extrusion = startSketchOn('XY')
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn kcl_test_xz_plane() {
|
||||
let code = r#"part001 = startSketchOn('XZ')
|
||||
|> startProfileAt([0, 0], %)
|
||||
|> lineTo([100, 100], %)
|
||||
|> lineTo([100, 0], %)
|
||||
|> close(%)
|
||||
|> extrude(5 + 7, %)
|
||||
"#;
|
||||
|
||||
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
|
||||
assert_out("xz_plane", &result);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn kcl_test_neg_xz_plane() {
|
||||
let code = r#"part001 = startSketchOn('-XZ')
|
||||
|> startProfileAt([0, 0], %)
|
||||
|> lineTo([100, 100], %)
|
||||
|> lineTo([100, 0], %)
|
||||
|> close(%)
|
||||
|> extrude(5 + 7, %)
|
||||
"#;
|
||||
|
||||
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
|
||||
assert_out("neg_xz_plane", &result);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn kcl_test_linear_pattern3d_a_pattern() {
|
||||
let code = r#"exampleSketch = startSketchOn('XZ')
|
||||
|> startProfileAt([0, 0], %)
|
||||
|> line([0, 2], %)
|
||||
|> line([3, 1], %)
|
||||
|> line([0, -4], %)
|
||||
|> close(%)
|
||||
|> extrude(1, %)
|
||||
|
||||
pattn1 = patternLinear3d({
|
||||
axis: [1, 0, 0],
|
||||
instances: 7,
|
||||
distance: 6
|
||||
}, exampleSketch)
|
||||
|
||||
pattn2 = patternLinear3d({
|
||||
axis: [0, 0, 1],
|
||||
distance: 1,
|
||||
instances: 7
|
||||
}, pattn1)
|
||||
"#;
|
||||
|
||||
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
|
||||
assert_out("linear_pattern3d_a_pattern", &result);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn kcl_test_circular_pattern3d_a_pattern() {
|
||||
let code = r#"exampleSketch = startSketchOn('XZ')
|
||||
|> startProfileAt([0, 0], %)
|
||||
|> line([0, 2], %)
|
||||
|> line([3, 1], %)
|
||||
|> line([0, -4], %)
|
||||
|> close(%)
|
||||
|> extrude(1, %)
|
||||
|
||||
pattn1 = patternLinear3d({
|
||||
axis: [1, 0, 0],
|
||||
instances: 7,
|
||||
distance: 6
|
||||
}, exampleSketch)
|
||||
|
||||
pattn2 = patternCircular3d({axis: [0,0, 1], center: [-20, -20, -20], instances: 41, arcDegrees: 360, rotateDuplicates: false}, pattn1)
|
||||
"#;
|
||||
|
||||
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
|
||||
assert_out("circular_pattern3d_a_pattern", &result);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn kcl_test_array_of_sketches() {
|
||||
let code = r#"plane001 = startSketchOn('XZ')
|
||||
@ -1496,69 +1418,6 @@ extrude(10, sketch001)
|
||||
assert_out("array_of_sketches", &result);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn kcl_test_sketch_on_face_after_fillets_referencing_face() {
|
||||
let code = r#"// Shelf Bracket
|
||||
// This is a shelf bracket made out of 6061-T6 aluminum sheet metal. The required thickness is calculated based on a point load of 300 lbs applied to the end of the shelf. There are two brackets holding up the shelf, so the moment experienced is divided by 2. The shelf is 1 foot long from the wall.
|
||||
|
||||
|
||||
// Define our bracket feet lengths
|
||||
shelfMountL = 8 // The length of the bracket holding up the shelf is 6 inches
|
||||
wallMountL = 6 // the length of the bracket
|
||||
|
||||
|
||||
// Define constants required to calculate the thickness needed to support 300 lbs
|
||||
sigmaAllow = 35000 // psi
|
||||
width = 6 // inch
|
||||
p = 300 // Force on shelf - lbs
|
||||
L = 12 // inches
|
||||
M = L * p / 2 // Moment experienced at fixed end of bracket
|
||||
FOS = 2 // Factor of safety of 2 to be conservative
|
||||
|
||||
|
||||
// Calculate the thickness off the bending stress and factor of safety
|
||||
thickness = sqrt(6 * M * FOS / (width * sigmaAllow))
|
||||
|
||||
// 0.25 inch fillet radius
|
||||
filletR = 0.25
|
||||
|
||||
// Sketch the bracket and extrude with fillets
|
||||
bracket = startSketchOn('XY')
|
||||
|> startProfileAt([0, 0], %)
|
||||
|> line([0, wallMountL], %, $outerEdge)
|
||||
|> line([-shelfMountL, 0], %, $seg01)
|
||||
|> line([0, -thickness], %)
|
||||
|> line([shelfMountL - thickness, 0], %, $innerEdge)
|
||||
|> line([0, -wallMountL + thickness], %)
|
||||
|> close(%)
|
||||
|> extrude(width, %)
|
||||
|> fillet({
|
||||
radius: filletR,
|
||||
tags: [
|
||||
getNextAdjacentEdge(innerEdge)
|
||||
]
|
||||
}, %)
|
||||
|> fillet({
|
||||
radius: filletR + thickness,
|
||||
tags: [
|
||||
getNextAdjacentEdge(outerEdge)
|
||||
]
|
||||
}, %)
|
||||
|
||||
sketch001 = startSketchOn(bracket, seg01)
|
||||
|> startProfileAt([4.28, 3.83], %)
|
||||
|> line([2.17, -0.03], %)
|
||||
|> line([-0.07, -1.8], %)
|
||||
|> line([-2.07, 0.05], %)
|
||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||
|> close(%)
|
||||
|> extrude(10, %)
|
||||
"#;
|
||||
|
||||
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
|
||||
assert_out("sketch_on_face_after_fillets_referencing_face", &result);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn kcl_test_circular_pattern3d_array_of_extrudes() {
|
||||
let code = r#"plane001 = startSketchOn('XZ')
|
||||
|
Before Width: | Height: | Size: 133 KiB After Width: | Height: | Size: 127 KiB |
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 37 KiB |
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 43 KiB |
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 37 KiB |
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 33 KiB |
@ -19,6 +19,15 @@ macro_rules! kcl_test {
|
||||
}
|
||||
|
||||
kcl_test!("sketch_on_face", kcl_test_sketch_on_face);
|
||||
kcl_test!("neg_xz_plane", kcl_test_neg_xz_plane);
|
||||
kcl_test!("xz_plane", kcl_test_xz_plane);
|
||||
kcl_test!(
|
||||
"sketch_on_face_after_fillets_referencing_face",
|
||||
kcl_test_sketch_on_face_after_fillets_referencing_face
|
||||
);
|
||||
kcl_test!("circular_pattern3d_a_pattern", kcl_test_circular_pattern3d_a_pattern);
|
||||
kcl_test!("linear_pattern3d_a_pattern", kcl_test_linear_pattern3d_a_pattern);
|
||||
|
||||
kcl_test!("tangential_arc", kcl_test_tangential_arc);
|
||||
kcl_test!(
|
||||
"big_number_angle_to_match_length_x",
|
||||
|
@ -8,33 +8,10 @@ use pretty_assertions::assert_eq;
|
||||
|
||||
/// Setup the engine and parse code for an ast.
|
||||
async fn setup(code: &str, name: &str) -> Result<(ExecutorContext, Program, uuid::Uuid)> {
|
||||
let user_agent = concat!(env!("CARGO_PKG_NAME"), ".rs/", env!("CARGO_PKG_VERSION"),);
|
||||
let http_client = reqwest::Client::builder()
|
||||
.user_agent(user_agent)
|
||||
// For file conversions we need this to be long.
|
||||
.timeout(std::time::Duration::from_secs(600))
|
||||
.connect_timeout(std::time::Duration::from_secs(60));
|
||||
let ws_client = reqwest::Client::builder()
|
||||
.user_agent(user_agent)
|
||||
// For file conversions we need this to be long.
|
||||
.timeout(std::time::Duration::from_secs(600))
|
||||
.connect_timeout(std::time::Duration::from_secs(60))
|
||||
.tcp_keepalive(std::time::Duration::from_secs(600))
|
||||
.http1_only();
|
||||
|
||||
let token = std::env::var("KITTYCAD_API_TOKEN").expect("KITTYCAD_API_TOKEN not set");
|
||||
|
||||
// Create the client.
|
||||
let mut client = kittycad::Client::new_from_reqwest(token, http_client, ws_client);
|
||||
// Set a local engine address if it's set.
|
||||
if let Ok(addr) = std::env::var("LOCAL_ENGINE_ADDR") {
|
||||
client.set_base_url(addr);
|
||||
}
|
||||
|
||||
let tokens = kcl_lib::token::lexer(code)?;
|
||||
let parser = kcl_lib::parser::Parser::new(tokens);
|
||||
let program = parser.ast()?;
|
||||
let ctx = kcl_lib::executor::ExecutorContext::new(&client, Default::default()).await?;
|
||||
let ctx = kcl_lib::executor::ExecutorContext::new_with_default_client(Default::default()).await?;
|
||||
let exec_state = ctx.run(&program, None, IdGenerator::default(), None).await?;
|
||||
|
||||
// We need to get the sketch ID.
|
||||
|