ArtifactGraph reThink (PART 3) (#3140)
* adjust engine connection to opt out of webRTC connection * refactor start and test setup * add env to unit test * spell config update * fix beforeAll order bug * initial integration of new artifact map with tests passing * remove old artifact map and clean up * graph artifact map * have graph commited * have graph commited * remove bad file * install playwright * fmt * commit permissions * typo * flesh out tests more * Look at this (photo)Graph *in the voice of Nickelback* * multi highlight * redo image logic * add in solid 2d data into artifactMap * fix snapshots * stabiles graph images * Look at this (photo)Graph *in the voice of Nickelback* * tweak tests * rename blend to edgeCut * Look at this (photo)Graph *in the voice of Nickelback* * fix playw tests * start of artifact map rename to graph * rename file * rename test * rename clearup * comments * docs * docs proof read * few tweaks here and there * typos * delete get parent logic * nit, combine if statements * remove unused param * fix silly test bug * rename surfId to sufaceId * rename types * update comments * add comment * add extra check * Look at this (photo)Graph *in the voice of Nickelback* * pull out merge artifact function * update comments * fix test * fmt --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
This commit is contained in:
@ -1,3 +1,3 @@
|
||||
[codespell]
|
||||
ignore-words-list: crate,everytime,inout,co-ordinate,ot,nwo,absolutey,atleast,ue
|
||||
ignore-words-list: crate,everytime,inout,co-ordinate,ot,nwo,absolutey,atleast,ue,afterall
|
||||
skip: **/target,node_modules,build,**/Cargo.lock,./docs/kcl/*.md,./src-tauri/gen/schemas
|
||||
|
38
.github/workflows/ci.yml
vendored
38
.github/workflows/ci.yml
vendored
@ -20,6 +20,11 @@ concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
actions: read
|
||||
|
||||
jobs:
|
||||
check-format:
|
||||
runs-on: 'ubuntu-latest'
|
||||
@ -85,7 +90,38 @@ jobs:
|
||||
|
||||
- run: yarn simpleserver:ci
|
||||
|
||||
- run: yarn test:nowatch
|
||||
- name: Install Chromium Browser
|
||||
run: yarn playwright install chromium --with-deps
|
||||
|
||||
- name: run unit tests
|
||||
run: yarn test:nowatch
|
||||
env:
|
||||
VITE_KC_DEV_TOKEN: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
|
||||
|
||||
- name: check for changes
|
||||
id: git-check
|
||||
run: |
|
||||
git add src/lang/std/artifactMapGraphs
|
||||
if git status src/lang/std/artifactMapGraphs | grep -q "Changes to be committed"
|
||||
then echo "modified=true" >> $GITHUB_OUTPUT
|
||||
else echo "modified=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
- name: Commit changes, if any
|
||||
if: steps.git-check.outputs.modified == 'true'
|
||||
run: |
|
||||
git config --local user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git config --local user.name "github-actions[bot]"
|
||||
git remote set-url origin https://${{ github.actor }}:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git
|
||||
git fetch origin
|
||||
echo ${{ github.head_ref }}
|
||||
git checkout ${{ github.head_ref }}
|
||||
# TODO when webkit works on ubuntu remove the os part of the commit message
|
||||
git commit -am "Look at this (photo)Graph *in the voice of Nickelback*" || true
|
||||
git push
|
||||
git push origin ${{ github.head_ref }}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
prepare-json-files:
|
||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -39,6 +39,7 @@ src/wasm-lib/grackle/test_json_output
|
||||
e2e/playwright/playwright-secrets.env
|
||||
e2e/playwright/temp1.png
|
||||
e2e/playwright/temp2.png
|
||||
e2e/playwright/temp3.png
|
||||
# exports from snapshot-tests.spec.ts "exports of each format should work"
|
||||
e2e/playwright/export-snapshots/*
|
||||
!e2e/playwright/export-snapshots/*.png
|
||||
@ -48,6 +49,7 @@ e2e/playwright/export-snapshots/*
|
||||
/playwright-report/
|
||||
/blob-report/
|
||||
/playwright/.cache/
|
||||
/src/lang/std/artifactMapCache
|
||||
|
||||
|
||||
## generated files
|
||||
|
@ -26,7 +26,7 @@ import * as TOML from '@iarna/toml'
|
||||
import { LineInputsType } from 'lang/std/sketchcombos'
|
||||
import { Coords2d } from 'lang/std/sketch'
|
||||
import { KCL_DEFAULT_LENGTH } from 'lib/constants'
|
||||
import { EngineCommand } from 'lang/std/artifactMap'
|
||||
import { EngineCommand } from 'lang/std/artifactGraph'
|
||||
import { onboardingPaths } from 'routes/Onboarding/paths'
|
||||
import { bracket } from 'lib/exampleKcl'
|
||||
|
||||
@ -466,7 +466,7 @@ test.describe('Testing Camera Movement', () => {
|
||||
|
||||
await expect(page.getByTestId('hover-highlight')).not.toBeVisible()
|
||||
|
||||
await page.waitForTimeout(100)
|
||||
await page.waitForTimeout(200)
|
||||
// hover over horizontal line
|
||||
await u.canvasLocator.hover({ position: { x: 800, y } })
|
||||
await expect(page.getByTestId('hover-highlight').first()).toBeVisible({
|
||||
@ -2623,10 +2623,9 @@ test.describe('Testing selections', () => {
|
||||
await page.mouse.move(startXPx + PUR * 15, 500 - PUR * 10)
|
||||
|
||||
await expect(page.getByTestId('hover-highlight').first()).toBeVisible()
|
||||
// bg-yellow-200 is more brittle than hover-highlight, but is closer to the user experience
|
||||
// bg-yellow-300/70 is more brittle than hover-highlight, but is closer to the user experience
|
||||
// and will be an easy fix if it breaks because we change the colour
|
||||
await expect(page.locator('.bg-yellow-200').first()).toBeVisible()
|
||||
|
||||
await expect(page.locator('.bg-yellow-300\\/70')).toBeVisible()
|
||||
// check mousing off, than mousing onto another line
|
||||
await page.mouse.move(startXPx + PUR * 10, 500 - PUR * 15) // mouse off
|
||||
await expect(page.getByTestId('hover-highlight')).not.toBeVisible()
|
||||
@ -3078,7 +3077,7 @@ const sketch002 = startSketchOn(launderExtrudeThroughVar, seg02)
|
||||
await expect(page.getByTestId('hover-highlight').first()).not.toBeVisible()
|
||||
|
||||
await page.mouse.move(flatExtrusionFace[0], flatExtrusionFace[1])
|
||||
await expect(page.getByTestId('hover-highlight')).toHaveCount(5) // multiple lines
|
||||
await expect(page.getByTestId('hover-highlight')).toHaveCount(6) // multiple lines
|
||||
await page.mouse.move(nothing[0], nothing[1])
|
||||
await page.waitForTimeout(100)
|
||||
await expect(page.getByTestId('hover-highlight').first()).not.toBeVisible()
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { expect, Page, Download } from '@playwright/test'
|
||||
import { EngineCommand } from 'lang/std/artifactMap'
|
||||
import { EngineCommand } from 'lang/std/artifactGraph'
|
||||
import os from 'os'
|
||||
import fsp from 'fs/promises'
|
||||
import pixelMatch from 'pixelmatch'
|
||||
|
@ -116,6 +116,7 @@
|
||||
"@tauri-apps/cli": "==2.0.0-beta.13",
|
||||
"@testing-library/jest-dom": "^5.14.1",
|
||||
"@testing-library/react": "^15.0.2",
|
||||
"@types/d3-force": "^3.0.10",
|
||||
"@types/mocha": "^10.0.6",
|
||||
"@types/node": "^18.19.31",
|
||||
"@types/pixelmatch": "^5.2.6",
|
||||
@ -138,6 +139,7 @@
|
||||
"@wdio/spec-reporter": "^8.36.0",
|
||||
"@xstate/cli": "^0.5.17",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"d3-force": "^3.0.0",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-react-app": "^7.0.1",
|
||||
"eslint-plugin-css-modules": "^2.12.0",
|
||||
|
@ -2,7 +2,7 @@ import { MouseEventHandler, useEffect, useMemo, useRef } from 'react'
|
||||
import { uuidv4 } from 'lib/utils'
|
||||
import { useHotKeyListener } from './hooks/useHotKeyListener'
|
||||
import { Stream } from './components/Stream'
|
||||
import { EngineCommand } from 'lang/std/artifactMap'
|
||||
import { EngineCommand } from 'lang/std/artifactGraph'
|
||||
import { throttle } from './lib/utils'
|
||||
import { AppHeader } from './components/AppHeader'
|
||||
import { useHotkeys } from 'react-hotkeys-hook'
|
||||
|
@ -40,10 +40,10 @@ export function Toolbar({
|
||||
return false
|
||||
}
|
||||
return isCursorInSketchCommandRange(
|
||||
engineCommandManager.artifactMap,
|
||||
engineCommandManager.artifactGraph,
|
||||
context.selectionRanges
|
||||
)
|
||||
}, [engineCommandManager.artifactMap, context.selectionRanges])
|
||||
}, [engineCommandManager.artifactGraph, context.selectionRanges])
|
||||
|
||||
const toolbarButtonsRef = useRef<HTMLUListElement>(null)
|
||||
const { overallState } = useNetworkContext()
|
||||
|
@ -21,7 +21,7 @@ import {
|
||||
EngineCommandManager,
|
||||
UnreliableSubscription,
|
||||
} from 'lang/std/engineConnection'
|
||||
import { EngineCommand } from 'lang/std/artifactMap'
|
||||
import { EngineCommand } from 'lang/std/artifactGraph'
|
||||
import { uuidv4 } from 'lib/utils'
|
||||
import { deg2Rad } from 'lib/utils2d'
|
||||
import { isReducedMotion, roundOff, throttle } from 'lib/utils'
|
||||
|
@ -575,10 +575,10 @@ const ConstraintSymbol = ({
|
||||
: 'bg-primary/30 dark:bg-primary text-primary dark:text-chalkboard-10 dark:border-transparent group-hover:bg-primary/40 group-hover:border-primary/50 group-hover:brightness-125'
|
||||
} h-[26px] w-[26px] rounded-sm relative m-0 p-0`}
|
||||
onMouseEnter={() => {
|
||||
editorManager.setHighlightRange(range)
|
||||
editorManager.setHighlightRange([range])
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
editorManager.setHighlightRange([0, 0])
|
||||
editorManager.setHighlightRange([[0, 0]])
|
||||
}}
|
||||
// disabled={isConstrained || !convertToVarEnabled}
|
||||
// disabled={implicitDesc} TODO why does this change styles that are hard to override?
|
||||
|
@ -84,11 +84,7 @@ import {
|
||||
createPipeSubstitution,
|
||||
findUniqueName,
|
||||
} from 'lang/modifyAst'
|
||||
import {
|
||||
Selections,
|
||||
getEventForSegmentSelection,
|
||||
sendSelectEventToEngine,
|
||||
} from 'lib/selections'
|
||||
import { Selections, getEventForSegmentSelection } from 'lib/selections'
|
||||
import { getTangentPointFromPreviousArc } from 'lib/utils2d'
|
||||
import { createGridHelper, orthoScale, perspScale } from './helpers'
|
||||
import { Models } from '@kittycad/lib'
|
||||
@ -1524,7 +1520,7 @@ export class SceneEntities {
|
||||
)
|
||||
if (trap(_node, { suppress: true })) return
|
||||
const node = _node.node
|
||||
editorManager.setHighlightRange([node.start, node.end])
|
||||
editorManager.setHighlightRange([[node.start, node.end]])
|
||||
const yellow = 0xffff00
|
||||
colorSegment(selected, yellow)
|
||||
const extraSegmentGroup = parent.getObjectByName(EXTRA_SEGMENT_HANDLE)
|
||||
@ -1560,10 +1556,10 @@ export class SceneEntities {
|
||||
}
|
||||
return
|
||||
}
|
||||
editorManager.setHighlightRange([0, 0])
|
||||
editorManager.setHighlightRange([[0, 0]])
|
||||
},
|
||||
onMouseLeave: ({ selected, ...rest }: OnMouseEnterLeaveArgs) => {
|
||||
editorManager.setHighlightRange([0, 0])
|
||||
editorManager.setHighlightRange([[0, 0]])
|
||||
const parent = getParentGroup(selected, [
|
||||
STRAIGHT_SEGMENT,
|
||||
TANGENTIAL_ARC_TO_SEGMENT,
|
||||
|
@ -44,7 +44,7 @@ export function AstExplorer() {
|
||||
<div
|
||||
className="h-full relative"
|
||||
onMouseLeave={(e) => {
|
||||
editorManager.setHighlightRange([0, 0])
|
||||
editorManager.setHighlightRange([[0, 0]])
|
||||
}}
|
||||
>
|
||||
<pre className="text-xs">
|
||||
@ -113,12 +113,12 @@ function DisplayObj({
|
||||
hasCursor ? 'bg-violet-100/80 dark:bg-violet-100/25' : ''
|
||||
}`}
|
||||
onMouseEnter={(e) => {
|
||||
editorManager.setHighlightRange([obj?.start || 0, obj.end])
|
||||
editorManager.setHighlightRange([[obj?.start || 0, obj.end]])
|
||||
e.stopPropagation()
|
||||
}}
|
||||
onMouseMove={(e) => {
|
||||
e.stopPropagation()
|
||||
editorManager.setHighlightRange([obj?.start || 0, obj.end])
|
||||
editorManager.setHighlightRange([[obj?.start || 0, obj.end]])
|
||||
}}
|
||||
onClick={(e) => {
|
||||
send({
|
||||
|
@ -446,7 +446,7 @@ export const ModelingMachineProvider = ({
|
||||
if (!isSingleCursorInPipe(selectionRanges, kclManager.ast))
|
||||
return false
|
||||
return !!isCursorInSketchCommandRange(
|
||||
engineCommandManager.artifactMap,
|
||||
engineCommandManager.artifactGraph,
|
||||
selectionRanges
|
||||
)
|
||||
},
|
||||
|
@ -3,7 +3,7 @@ import { EditorView, Decoration } from '@codemirror/view'
|
||||
|
||||
export { EditorView }
|
||||
|
||||
export const addLineHighlight = StateEffect.define<[number, number]>()
|
||||
export const addLineHighlight = StateEffect.define<Array<[number, number]>>()
|
||||
|
||||
const addLineHighlightAnnotation = Annotation.define<null>()
|
||||
export const addLineHighlightEvent = addLineHighlightAnnotation.of(null)
|
||||
@ -24,10 +24,18 @@ export const lineHighlightField = StateField.define({
|
||||
for (let e of tr.effects) {
|
||||
if (e.is(addLineHighlight)) {
|
||||
lines = Decoration.none
|
||||
const [from, to] = e.value || [0, 0]
|
||||
if (from && to && !(from === to && from === 0)) {
|
||||
lines = lines.update({ add: [matchDeco.range(from, to)] })
|
||||
deco.push(matchDeco.range(from, to))
|
||||
for (let index = 0; index < e.value.length; index++) {
|
||||
const highlightRange = e.value[index]
|
||||
const [from, to] = highlightRange || [0, 0]
|
||||
if (from && to && !(from === to && from === 0)) {
|
||||
if (index === 0) {
|
||||
lines = lines.update({ add: [matchDeco.range(from, to)] })
|
||||
deco.push(matchDeco.range(from, to))
|
||||
} else {
|
||||
lines = lines.update({ add: [matchDeco2.range(from, to)] })
|
||||
deco.push(matchDeco2.range(from, to))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -37,6 +45,10 @@ export const lineHighlightField = StateField.define({
|
||||
})
|
||||
|
||||
const matchDeco = Decoration.mark({
|
||||
class: 'bg-yellow-200',
|
||||
class: 'bg-yellow-300/70',
|
||||
attributes: { 'data-testid': 'hover-highlight' },
|
||||
})
|
||||
const matchDeco2 = Decoration.mark({
|
||||
class: 'bg-yellow-200/40',
|
||||
attributes: { 'data-testid': 'hover-highlight' },
|
||||
})
|
||||
|
@ -42,7 +42,7 @@ export default class EditorManager {
|
||||
private _convertToVariableEnabled: boolean = false
|
||||
private _convertToVariableCallback: () => void = () => {}
|
||||
|
||||
private _highlightRange: [number, number] = [0, 0]
|
||||
private _highlightRange: Array<[number, number]> = [[0, 0]]
|
||||
|
||||
setCopilotEnabled(enabled: boolean) {
|
||||
this._copilotEnabled = enabled
|
||||
@ -88,19 +88,21 @@ export default class EditorManager {
|
||||
return this._commandBarSend(eventInfo)
|
||||
}
|
||||
|
||||
get highlightRange(): [number, number] {
|
||||
get highlightRange(): Array<[number, number]> {
|
||||
return this._highlightRange
|
||||
}
|
||||
|
||||
setHighlightRange(selection: Selection['range']): void {
|
||||
this._highlightRange = selection
|
||||
const safeEnd = Math.min(
|
||||
selection[1],
|
||||
this._editorView?.state.doc.length || selection[1]
|
||||
)
|
||||
setHighlightRange(selections: Array<Selection['range']>): void {
|
||||
this._highlightRange = selections
|
||||
|
||||
const selectionsWithSafeEnds = selections.map((s): [number, number] => {
|
||||
const safeEnd = Math.min(s[1], this._editorView?.state.doc.length || s[1])
|
||||
return [s[0], safeEnd]
|
||||
})
|
||||
|
||||
if (this._editorView) {
|
||||
this._editorView.dispatch({
|
||||
effects: addLineHighlight.of([selection[0], safeEnd]),
|
||||
effects: addLineHighlight.of(selectionsWithSafeEnds),
|
||||
annotations: [
|
||||
updateOutsideEditorEvent,
|
||||
addLineHighlightEvent,
|
||||
|
@ -12,3 +12,4 @@ export const VITE_KC_DEV_TOKEN = import.meta.env.VITE_KC_DEV_TOKEN as
|
||||
| undefined
|
||||
export const TEST = import.meta.env.TEST
|
||||
export const DEV = import.meta.env.DEV
|
||||
export const CI = import.meta.env.CI
|
||||
|
@ -7,6 +7,13 @@ import {
|
||||
} from 'lib/singletons'
|
||||
import { useModelingContext } from './useModelingContext'
|
||||
import { getEventForSelectWithPoint } from 'lib/selections'
|
||||
import {
|
||||
getCapCodeRef,
|
||||
getExtrusionFromSuspectedExtrudeSurface,
|
||||
getSolid2dCodeRef,
|
||||
getWallCodeRef,
|
||||
} from 'lang/std/artifactGraph'
|
||||
import { err } from 'lib/trap'
|
||||
import { DefaultPlaneStr, getFaceDetails } from 'clientSideScene/sceneEntities'
|
||||
import { getNodePathFromSourceRange } from 'lang/queryAst'
|
||||
|
||||
@ -21,24 +28,58 @@ export function useEngineConnectionSubscriptions() {
|
||||
event: 'highlight_set_entity',
|
||||
callback: ({ data }) => {
|
||||
if (data?.entity_id) {
|
||||
const sourceRange = engineCommandManager.artifactMap?.[data.entity_id]
|
||||
?.range || [0, 0]
|
||||
editorManager.setHighlightRange(sourceRange)
|
||||
const artifact = engineCommandManager.artifactGraph.get(
|
||||
data.entity_id
|
||||
)
|
||||
if (artifact?.type === 'solid2D') {
|
||||
const codeRef = getSolid2dCodeRef(
|
||||
artifact,
|
||||
engineCommandManager.artifactGraph
|
||||
)
|
||||
if (err(codeRef)) return
|
||||
editorManager.setHighlightRange([codeRef.range])
|
||||
} else if (artifact?.type === 'cap') {
|
||||
const codeRef = getCapCodeRef(
|
||||
artifact,
|
||||
engineCommandManager.artifactGraph
|
||||
)
|
||||
if (err(codeRef)) return
|
||||
editorManager.setHighlightRange([codeRef.range])
|
||||
} else if (artifact?.type === 'wall') {
|
||||
const extrusion = getExtrusionFromSuspectedExtrudeSurface(
|
||||
data.entity_id,
|
||||
engineCommandManager.artifactGraph
|
||||
)
|
||||
const codeRef = getWallCodeRef(
|
||||
artifact,
|
||||
engineCommandManager.artifactGraph
|
||||
)
|
||||
if (err(codeRef)) return
|
||||
editorManager.setHighlightRange(
|
||||
err(extrusion)
|
||||
? [codeRef.range]
|
||||
: [codeRef.range, extrusion.codeRef.range]
|
||||
)
|
||||
} else if (artifact?.type === 'segment') {
|
||||
editorManager.setHighlightRange([
|
||||
artifact?.codeRef?.range || [0, 0],
|
||||
])
|
||||
} else {
|
||||
editorManager.setHighlightRange([[0, 0]])
|
||||
}
|
||||
} else if (
|
||||
!editorManager.highlightRange ||
|
||||
(editorManager.highlightRange[0] !== 0 &&
|
||||
editorManager.highlightRange[1] !== 0)
|
||||
(editorManager.highlightRange[0][0] !== 0 &&
|
||||
editorManager.highlightRange[0][1] !== 0)
|
||||
) {
|
||||
editorManager.setHighlightRange([0, 0])
|
||||
editorManager.setHighlightRange([[0, 0]])
|
||||
}
|
||||
},
|
||||
})
|
||||
const unSubClick = engineCommandManager.subscribeTo({
|
||||
event: 'select_with_point',
|
||||
callback: async (engineEvent) => {
|
||||
const event = await getEventForSelectWithPoint(engineEvent, {
|
||||
sketchEnginePathId: context.sketchEnginePathId,
|
||||
})
|
||||
const event = await getEventForSelectWithPoint(engineEvent)
|
||||
event && send(event)
|
||||
},
|
||||
})
|
||||
@ -53,16 +94,17 @@ export function useEngineConnectionSubscriptions() {
|
||||
event: 'select_with_point',
|
||||
callback: state.matches('Sketch no face')
|
||||
? async ({ data }) => {
|
||||
let planeId = data.entity_id
|
||||
if (!planeId) return
|
||||
let planeOrFaceId = data.entity_id
|
||||
if (!planeOrFaceId) return
|
||||
if (
|
||||
engineCommandManager.defaultPlanes?.xy === planeId ||
|
||||
engineCommandManager.defaultPlanes?.xz === planeId ||
|
||||
engineCommandManager.defaultPlanes?.yz === planeId ||
|
||||
engineCommandManager.defaultPlanes?.negXy === planeId ||
|
||||
engineCommandManager.defaultPlanes?.negXz === planeId ||
|
||||
engineCommandManager.defaultPlanes?.negYz === planeId
|
||||
engineCommandManager.defaultPlanes?.xy === planeOrFaceId ||
|
||||
engineCommandManager.defaultPlanes?.xz === planeOrFaceId ||
|
||||
engineCommandManager.defaultPlanes?.yz === planeOrFaceId ||
|
||||
engineCommandManager.defaultPlanes?.negXy === planeOrFaceId ||
|
||||
engineCommandManager.defaultPlanes?.negXz === planeOrFaceId ||
|
||||
engineCommandManager.defaultPlanes?.negYz === planeOrFaceId
|
||||
) {
|
||||
let planeId = planeOrFaceId
|
||||
const defaultPlaneStrMap: Record<string, DefaultPlaneStr> = {
|
||||
[engineCommandManager.defaultPlanes.xy]: 'XY',
|
||||
[engineCommandManager.defaultPlanes.xz]: 'XZ',
|
||||
@ -117,44 +159,34 @@ export function useEngineConnectionSubscriptions() {
|
||||
})
|
||||
return
|
||||
}
|
||||
const artifact = engineCommandManager.artifactMap[planeId]
|
||||
console.log('artifact', artifact)
|
||||
// If we clicked on an extrude wall, we climb up the parent Id
|
||||
// to get the sketch profile's face ID. If we clicked on an endcap,
|
||||
// we already have it.
|
||||
const pathId =
|
||||
artifact?.type === 'extrudeWall' ||
|
||||
artifact?.type === 'extrudeCap'
|
||||
? artifact.pathId
|
||||
: ''
|
||||
|
||||
const path = engineCommandManager.artifactMap?.[pathId || '']
|
||||
const extrusionId =
|
||||
path?.type === 'startPath' ? path.extrusionIds[0] : ''
|
||||
|
||||
// TODO: We get the first extrusion command ID,
|
||||
// which is fine while backend systems only support one extrusion.
|
||||
// but we need to more robustly handle resolving to the correct extrusion
|
||||
// if there are multiple.
|
||||
const extrusions = engineCommandManager.artifactMap?.[extrusionId]
|
||||
|
||||
if (
|
||||
artifact?.type !== 'extrudeCap' &&
|
||||
artifact?.type !== 'extrudeWall'
|
||||
const faceId = planeOrFaceId
|
||||
const artifact = engineCommandManager.artifactGraph.get(faceId)
|
||||
const extrusion = getExtrusionFromSuspectedExtrudeSurface(
|
||||
faceId,
|
||||
engineCommandManager.artifactGraph
|
||||
)
|
||||
return
|
||||
|
||||
const faceInfo = await getFaceDetails(planeId)
|
||||
if (artifact?.type !== 'cap' && artifact?.type !== 'wall') return
|
||||
|
||||
const codeRef =
|
||||
artifact.type === 'cap'
|
||||
? getCapCodeRef(artifact, engineCommandManager.artifactGraph)
|
||||
: getWallCodeRef(artifact, engineCommandManager.artifactGraph)
|
||||
|
||||
const faceInfo = await getFaceDetails(faceId)
|
||||
if (!faceInfo?.origin || !faceInfo?.z_axis || !faceInfo?.y_axis)
|
||||
return
|
||||
const { z_axis, y_axis, origin } = faceInfo
|
||||
const sketchPathToNode = getNodePathFromSourceRange(
|
||||
kclManager.ast,
|
||||
artifact.range
|
||||
err(codeRef) ? [0, 0] : codeRef.range
|
||||
)
|
||||
|
||||
const extrudePathToNode = extrusions?.range
|
||||
? getNodePathFromSourceRange(kclManager.ast, extrusions.range)
|
||||
const extrudePathToNode = !err(extrusion)
|
||||
? getNodePathFromSourceRange(
|
||||
kclManager.ast,
|
||||
extrusion.codeRef.range
|
||||
)
|
||||
: []
|
||||
|
||||
sceneInfra.modelingSend({
|
||||
@ -168,8 +200,8 @@ export function useEngineConnectionSubscriptions() {
|
||||
) as [number, number, number],
|
||||
sketchPathToNode,
|
||||
extrudePathToNode,
|
||||
cap: artifact.type === 'extrudeCap' ? artifact.cap : 'none',
|
||||
faceId: planeId,
|
||||
cap: artifact.type === 'cap' ? artifact.subType : 'none',
|
||||
faceId: faceId,
|
||||
},
|
||||
})
|
||||
return
|
||||
|
@ -46,7 +46,6 @@ export function useSetupEngineManager(
|
||||
streamRef?.current?.offsetHeight ?? 0
|
||||
)
|
||||
engineCommandManager.start({
|
||||
restart,
|
||||
setMediaStream: (mediaStream) => setMediaStream(mediaStream),
|
||||
setIsStreamReady: (isStreamReady) => setAppState({ isStreamReady }),
|
||||
width: quadWidth,
|
||||
|
@ -310,24 +310,30 @@ export class KclManager {
|
||||
this._kclErrors = errors
|
||||
this._programMemory = programMemory
|
||||
if (updates !== 'artifactRanges') return
|
||||
Object.entries(this.engineCommandManager.artifactMap).forEach(
|
||||
|
||||
// TODO the below seems like a work around, I wish there's a comment explaining exactly what
|
||||
// problem this solves, but either way we should strive to remove it.
|
||||
Array.from(this.engineCommandManager.artifactGraph).forEach(
|
||||
([commandId, artifact]) => {
|
||||
if (!artifact.pathToNode) return
|
||||
if (!('codeRef' in artifact)) return
|
||||
const _node1 = getNodeFromPath<CallExpression>(
|
||||
this.ast,
|
||||
artifact.pathToNode,
|
||||
artifact.codeRef.pathToNode,
|
||||
'CallExpression'
|
||||
)
|
||||
if (err(_node1)) return
|
||||
const { node } = _node1
|
||||
if (node.type !== 'CallExpression') return
|
||||
const [oldStart, oldEnd] = artifact.range
|
||||
const [oldStart, oldEnd] = artifact.codeRef.range
|
||||
if (oldStart === 0 && oldEnd === 0) return
|
||||
if (oldStart === node.start && oldEnd === node.end) return
|
||||
this.engineCommandManager.artifactMap[commandId].range = [
|
||||
node.start,
|
||||
node.end,
|
||||
]
|
||||
this.engineCommandManager.artifactGraph.set(commandId, {
|
||||
...artifact,
|
||||
codeRef: {
|
||||
...artifact.codeRef,
|
||||
range: [node.start, node.end],
|
||||
},
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
|
388
src/lang/std/__snapshots__/artifactGraph.test.ts.snap
Normal file
388
src/lang/std/__snapshots__/artifactGraph.test.ts.snap
Normal file
@ -0,0 +1,388 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`testing createArtifactGraph > code with an extrusion, fillet and sketch of face: > snapshot of the artifactGraph 1`] = `
|
||||
Map {
|
||||
"UUID-0" => {
|
||||
"codeRef": {
|
||||
"pathToNode": [
|
||||
[
|
||||
"body",
|
||||
"",
|
||||
],
|
||||
],
|
||||
"range": [
|
||||
43,
|
||||
70,
|
||||
],
|
||||
},
|
||||
"pathIds": [
|
||||
"UUID",
|
||||
],
|
||||
"type": "plane",
|
||||
},
|
||||
"UUID-1" => {
|
||||
"codeRef": {
|
||||
"pathToNode": [
|
||||
[
|
||||
"body",
|
||||
"",
|
||||
],
|
||||
],
|
||||
"range": [
|
||||
43,
|
||||
70,
|
||||
],
|
||||
},
|
||||
"extrusionId": "UUID",
|
||||
"planeId": "UUID",
|
||||
"segIds": [
|
||||
"UUID",
|
||||
"UUID",
|
||||
"UUID",
|
||||
"UUID",
|
||||
"UUID",
|
||||
],
|
||||
"solid2dId": "UUID",
|
||||
"type": "path",
|
||||
},
|
||||
"UUID-2" => {
|
||||
"codeRef": {
|
||||
"pathToNode": [
|
||||
[
|
||||
"body",
|
||||
"",
|
||||
],
|
||||
],
|
||||
"range": [
|
||||
76,
|
||||
92,
|
||||
],
|
||||
},
|
||||
"edgeIds": [],
|
||||
"pathId": "UUID",
|
||||
"surfaceId": "UUID",
|
||||
"type": "segment",
|
||||
},
|
||||
"UUID-3" => {
|
||||
"codeRef": {
|
||||
"pathToNode": [
|
||||
[
|
||||
"body",
|
||||
"",
|
||||
],
|
||||
],
|
||||
"range": [
|
||||
98,
|
||||
125,
|
||||
],
|
||||
},
|
||||
"edgeCutId": "UUID",
|
||||
"edgeIds": [],
|
||||
"pathId": "UUID",
|
||||
"surfaceId": "UUID",
|
||||
"type": "segment",
|
||||
},
|
||||
"UUID-4" => {
|
||||
"codeRef": {
|
||||
"pathToNode": [
|
||||
[
|
||||
"body",
|
||||
"",
|
||||
],
|
||||
],
|
||||
"range": [
|
||||
131,
|
||||
156,
|
||||
],
|
||||
},
|
||||
"edgeIds": [],
|
||||
"pathId": "UUID",
|
||||
"surfaceId": "UUID",
|
||||
"type": "segment",
|
||||
},
|
||||
"UUID-5" => {
|
||||
"codeRef": {
|
||||
"pathToNode": [
|
||||
[
|
||||
"body",
|
||||
"",
|
||||
],
|
||||
],
|
||||
"range": [
|
||||
162,
|
||||
209,
|
||||
],
|
||||
},
|
||||
"edgeIds": [],
|
||||
"pathId": "UUID",
|
||||
"surfaceId": "UUID",
|
||||
"type": "segment",
|
||||
},
|
||||
"UUID-6" => {
|
||||
"codeRef": {
|
||||
"pathToNode": [
|
||||
[
|
||||
"body",
|
||||
"",
|
||||
],
|
||||
],
|
||||
"range": [
|
||||
215,
|
||||
223,
|
||||
],
|
||||
},
|
||||
"edgeIds": [],
|
||||
"pathId": "UUID",
|
||||
"type": "segment",
|
||||
},
|
||||
"UUID-7" => {
|
||||
"pathId": "UUID",
|
||||
"type": "solid2D",
|
||||
},
|
||||
"UUID-8" => {
|
||||
"codeRef": {
|
||||
"pathToNode": [
|
||||
[
|
||||
"body",
|
||||
"",
|
||||
],
|
||||
],
|
||||
"range": [
|
||||
243,
|
||||
266,
|
||||
],
|
||||
},
|
||||
"edgeIds": [],
|
||||
"pathId": "UUID",
|
||||
"surfaceIds": [
|
||||
"UUID",
|
||||
"UUID",
|
||||
"UUID",
|
||||
"UUID",
|
||||
"UUID",
|
||||
"UUID",
|
||||
],
|
||||
"type": "extrusion",
|
||||
},
|
||||
"UUID-9" => {
|
||||
"edgeCutEdgeIds": [],
|
||||
"extrusionId": "UUID",
|
||||
"pathIds": [],
|
||||
"segId": "UUID",
|
||||
"type": "wall",
|
||||
},
|
||||
"UUID-10" => {
|
||||
"edgeCutEdgeIds": [],
|
||||
"extrusionId": "UUID",
|
||||
"pathIds": [
|
||||
"UUID",
|
||||
],
|
||||
"segId": "UUID",
|
||||
"type": "wall",
|
||||
},
|
||||
"UUID-11" => {
|
||||
"edgeCutEdgeIds": [],
|
||||
"extrusionId": "UUID",
|
||||
"pathIds": [],
|
||||
"segId": "UUID",
|
||||
"type": "wall",
|
||||
},
|
||||
"UUID-12" => {
|
||||
"edgeCutEdgeIds": [],
|
||||
"extrusionId": "UUID",
|
||||
"pathIds": [],
|
||||
"segId": "UUID",
|
||||
"type": "wall",
|
||||
},
|
||||
"UUID-13" => {
|
||||
"edgeCutEdgeIds": [],
|
||||
"extrusionId": "UUID",
|
||||
"pathIds": [],
|
||||
"subType": "start",
|
||||
"type": "cap",
|
||||
},
|
||||
"UUID-14" => {
|
||||
"edgeCutEdgeIds": [],
|
||||
"extrusionId": "UUID",
|
||||
"pathIds": [],
|
||||
"subType": "end",
|
||||
"type": "cap",
|
||||
},
|
||||
"UUID-15" => {
|
||||
"codeRef": {
|
||||
"pathToNode": [
|
||||
[
|
||||
"body",
|
||||
"",
|
||||
],
|
||||
],
|
||||
"range": [
|
||||
272,
|
||||
311,
|
||||
],
|
||||
},
|
||||
"consumedEdgeId": "UUID",
|
||||
"edgeIds": [],
|
||||
"subType": "fillet",
|
||||
"type": "edgeCut",
|
||||
},
|
||||
"UUID-16" => {
|
||||
"codeRef": {
|
||||
"pathToNode": [
|
||||
[
|
||||
"body",
|
||||
"",
|
||||
],
|
||||
],
|
||||
"range": [
|
||||
368,
|
||||
395,
|
||||
],
|
||||
},
|
||||
"extrusionId": "UUID",
|
||||
"planeId": "UUID",
|
||||
"segIds": [
|
||||
"UUID",
|
||||
"UUID",
|
||||
"UUID",
|
||||
"UUID",
|
||||
],
|
||||
"solid2dId": "UUID",
|
||||
"type": "path",
|
||||
},
|
||||
"UUID-17" => {
|
||||
"codeRef": {
|
||||
"pathToNode": [
|
||||
[
|
||||
"body",
|
||||
"",
|
||||
],
|
||||
],
|
||||
"range": [
|
||||
401,
|
||||
416,
|
||||
],
|
||||
},
|
||||
"edgeIds": [],
|
||||
"pathId": "UUID",
|
||||
"surfaceId": "UUID",
|
||||
"type": "segment",
|
||||
},
|
||||
"UUID-18" => {
|
||||
"codeRef": {
|
||||
"pathToNode": [
|
||||
[
|
||||
"body",
|
||||
"",
|
||||
],
|
||||
],
|
||||
"range": [
|
||||
422,
|
||||
438,
|
||||
],
|
||||
},
|
||||
"edgeIds": [],
|
||||
"pathId": "UUID",
|
||||
"surfaceId": "UUID",
|
||||
"type": "segment",
|
||||
},
|
||||
"UUID-19" => {
|
||||
"codeRef": {
|
||||
"pathToNode": [
|
||||
[
|
||||
"body",
|
||||
"",
|
||||
],
|
||||
],
|
||||
"range": [
|
||||
444,
|
||||
491,
|
||||
],
|
||||
},
|
||||
"edgeIds": [],
|
||||
"pathId": "UUID",
|
||||
"surfaceId": "UUID",
|
||||
"type": "segment",
|
||||
},
|
||||
"UUID-20" => {
|
||||
"codeRef": {
|
||||
"pathToNode": [
|
||||
[
|
||||
"body",
|
||||
"",
|
||||
],
|
||||
],
|
||||
"range": [
|
||||
497,
|
||||
505,
|
||||
],
|
||||
},
|
||||
"edgeIds": [],
|
||||
"pathId": "UUID",
|
||||
"type": "segment",
|
||||
},
|
||||
"UUID-21" => {
|
||||
"pathId": "UUID",
|
||||
"type": "solid2D",
|
||||
},
|
||||
"UUID-22" => {
|
||||
"codeRef": {
|
||||
"pathToNode": [
|
||||
[
|
||||
"body",
|
||||
"",
|
||||
],
|
||||
],
|
||||
"range": [
|
||||
525,
|
||||
546,
|
||||
],
|
||||
},
|
||||
"edgeIds": [],
|
||||
"pathId": "UUID",
|
||||
"surfaceIds": [
|
||||
"UUID",
|
||||
"UUID",
|
||||
"UUID",
|
||||
"UUID",
|
||||
"UUID",
|
||||
],
|
||||
"type": "extrusion",
|
||||
},
|
||||
"UUID-23" => {
|
||||
"edgeCutEdgeIds": [],
|
||||
"extrusionId": "UUID",
|
||||
"pathIds": [],
|
||||
"segId": "UUID",
|
||||
"type": "wall",
|
||||
},
|
||||
"UUID-24" => {
|
||||
"edgeCutEdgeIds": [],
|
||||
"extrusionId": "UUID",
|
||||
"pathIds": [],
|
||||
"segId": "UUID",
|
||||
"type": "wall",
|
||||
},
|
||||
"UUID-25" => {
|
||||
"edgeCutEdgeIds": [],
|
||||
"extrusionId": "UUID",
|
||||
"pathIds": [],
|
||||
"segId": "UUID",
|
||||
"type": "wall",
|
||||
},
|
||||
"UUID-26" => {
|
||||
"edgeCutEdgeIds": [],
|
||||
"extrusionId": "UUID",
|
||||
"pathIds": [],
|
||||
"subType": "start",
|
||||
"type": "cap",
|
||||
},
|
||||
"UUID-27" => {
|
||||
"edgeCutEdgeIds": [],
|
||||
"extrusionId": "UUID",
|
||||
"pathIds": [],
|
||||
"subType": "end",
|
||||
"type": "cap",
|
||||
},
|
||||
}
|
||||
`;
|
48
src/lang/std/artifactGraph-README.md
Normal file
48
src/lang/std/artifactGraph-README.md
Normal file
@ -0,0 +1,48 @@
|
||||
## Artifact Graph
|
||||
|
||||
#### What it does
|
||||
|
||||
The artifact graph's primary role is to map geometry artifacts in the 3d-scene/engine, to the code/AST such that when the engine sends the FE an id of some piece of geometry (say because the user clicked on something) then we know both what it is, and how it relates to the user's code.
|
||||
|
||||
Relating it to a user's code is important because this is how we drive our AST-mods, say a user clicks a segment and wants to constrain it horizontally, because of the artifact graph we know that their selection was in fact a specific `line(...)` callExpression, and now we're able to transform this to `xLine(...)` in order to constrain it.
|
||||
|
||||
#### How to reason about the graph
|
||||
|
||||
Here is what roughly what the artifact graph looks like
|
||||
|
||||

|
||||
|
||||
The best way to read this is starting with the plane at the bottom and going upwards, as this is roughly the command order (which the graph is based on).
|
||||
Here's an explanation:
|
||||
|
||||
- plane is created (kcl:`startSketchOn`, command: `enable_sketch_mode`)
|
||||
- path is created, needs to refer to the plane that the sketch is on (kcl:`startProfileAt`, command: `start_path`)
|
||||
- each segment that is created (kcl: `line`, command: `extend_path`) must refer back to the path.
|
||||
- Once we're read to extrude (kcl: `extrude`, command: `extrude`) it much refer to the path.
|
||||
- The extrude created a bunch of faces, edges etc, each of these relates back to the extrude command and the segment call expression, but there's no direct bit of kcl to refer to.
|
||||
|
||||
The above is probably enough to give more examples of how the graph is used.
|
||||
|
||||
- When a user hovers over a segment, the engine sends us the id of the segment, we can look it up directly in the graph, and we store pointers to the code in the graph, This allows use to highlight the `line(...)` call expression in the code.
|
||||
- Same as above but the user hovers over a extrude wall-face, the engine sends us this id, we look it up in the graph, but there's no pointer to the code in this node. We can then traverse to both the segment and the extrude nodes to get source ranges for `line(...)` and `extrude(...)` and highlight them both.
|
||||
|
||||
Other things to point out is that a new path can be created directly on a wall-face, i.e. this is sketch on face, and more than one path can point to the same plane, that is multiple profiles on the same plane.
|
||||
|
||||
#### Generated Graphs
|
||||
|
||||
The image above is hand drawn for grokablitiy, but it's useful to look at a real graph, take this bit of geometry
|
||||
|
||||

|
||||
|
||||
In `src/lang/std/artifactGraph.test.ts` we generate the graph for it
|
||||
|
||||

|
||||
|
||||
It's definitely harder to read, if you start at roughly the bottom center of the page and find the node `plane-0` and visually traverse from there you can see it has the same structure, plane is connected to a path, which is connected to multiple segments and an extrusion etc.
|
||||
|
||||
Generating the graph here serves a couple of purposes
|
||||
|
||||
1. Allows us to sanity check the graph, in development or as a debug tool.
|
||||
2. Is a form of test and regression check. The code that creates the node and edges would error if we tried to create an edge to a node that didn't exist, this gives us some confidence that the graph is correct. Also because we want want to be able to traverse the graph in both directions, checking each edge has an arrowhead going both directions is a good check. Lastly this images are generated and committed as part of CI, if something changes in the graph, we'll notice.
|
||||
|
||||
We'll need to add more sample code to `src/lang/std/artifactGraph.test.ts` to generate more graphs, to test more kcl API as the app continues development.
|
743
src/lang/std/artifactGraph.test.ts
Normal file
743
src/lang/std/artifactGraph.test.ts
Normal file
@ -0,0 +1,743 @@
|
||||
import { makeDefaultPlanes, parse, initPromise, Program } from 'lang/wasm'
|
||||
import { Models } from '@kittycad/lib'
|
||||
import {
|
||||
OrderedCommand,
|
||||
ResponseMap,
|
||||
createArtifactGraph,
|
||||
filterArtifacts,
|
||||
expandPlane,
|
||||
expandPath,
|
||||
expandExtrusion,
|
||||
ArtifactGraph,
|
||||
expandSegment,
|
||||
getArtifactsToUpdate,
|
||||
} from './artifactGraph'
|
||||
import { err } from 'lib/trap'
|
||||
import { engineCommandManager, kclManager } from 'lib/singletons'
|
||||
import { CI, VITE_KC_DEV_TOKEN } from 'env'
|
||||
import fsp from 'fs/promises'
|
||||
import fs from 'fs'
|
||||
import { chromium } from 'playwright'
|
||||
import * as d3 from 'd3-force'
|
||||
import path from 'path'
|
||||
import pixelmatch from 'pixelmatch'
|
||||
import { PNG } from 'pngjs'
|
||||
|
||||
/*
|
||||
Note this is an integration test, these tests connect to our real dev server and make websocket commands.
|
||||
It's needed for testing the artifactGraph, as it is tied to the websocket commands.
|
||||
*/
|
||||
|
||||
const pathStart = 'src/lang/std/artifactMapCache'
|
||||
const fullPath = `${pathStart}/artifactMapCache.json`
|
||||
|
||||
const exampleCode1 = `const sketch001 = startSketchOn('XY')
|
||||
|> startProfileAt([-5, -5], %)
|
||||
|> line([0, 10], %)
|
||||
|> line([10.55, 0], %, $seg01)
|
||||
|> line([0, -10], %, $seg02)
|
||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||
|> close(%)
|
||||
const extrude001 = extrude(-10, sketch001)
|
||||
|> fillet({ radius: 5, tags: [seg01] }, %)
|
||||
const sketch002 = startSketchOn(extrude001, seg02)
|
||||
|> startProfileAt([-2, -6], %)
|
||||
|> line([2, 3], %)
|
||||
|> line([2, -3], %)
|
||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||
|> close(%)
|
||||
const extrude002 = extrude(5, sketch002)
|
||||
`
|
||||
|
||||
const sketchOnFaceOnFaceEtc = `const sketch001 = startSketchOn('XZ')
|
||||
|> startProfileAt([0, 0], %)
|
||||
|> line([4, 8], %)
|
||||
|> line([5, -8], %, $seg01)
|
||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||
|> close(%)
|
||||
const extrude001 = extrude(6, sketch001)
|
||||
const sketch002 = startSketchOn(extrude001, seg01)
|
||||
|> startProfileAt([-0.5, 0.5], %)
|
||||
|> line([2, 5], %)
|
||||
|> line([2, -5], %)
|
||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||
|> close(%)
|
||||
const extrude002 = extrude(5, sketch002)
|
||||
const sketch003 = startSketchOn(extrude002, 'END')
|
||||
|> startProfileAt([1, 1.5], %)
|
||||
|> line([0.5, 2], %, $seg02)
|
||||
|> line([1, -2], %)
|
||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||
|> close(%)
|
||||
const extrude003 = extrude(4, sketch003)
|
||||
const sketch004 = startSketchOn(extrude003, seg02)
|
||||
|> startProfileAt([-3, 14], %)
|
||||
|> line([0.5, 1], %)
|
||||
|> line([0.5, -2], %)
|
||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||
|> close(%)
|
||||
const extrude004 = extrude(3, sketch004)
|
||||
`
|
||||
|
||||
// add more code snippets here and use `getCommands` to get the orderedCommands and responseMap for more tests
|
||||
const codeToWriteCacheFor = {
|
||||
exampleCode1,
|
||||
sketchOnFaceOnFaceEtc,
|
||||
} as const
|
||||
|
||||
type CodeKey = keyof typeof codeToWriteCacheFor
|
||||
|
||||
type CacheShape = {
|
||||
[key in CodeKey]: {
|
||||
orderedCommands: OrderedCommand[]
|
||||
responseMap: ResponseMap
|
||||
}
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
await initPromise
|
||||
|
||||
let parsed
|
||||
try {
|
||||
const file = await fsp.readFile(fullPath, 'utf-8')
|
||||
parsed = JSON.parse(file)
|
||||
} catch (e) {
|
||||
parsed = false
|
||||
}
|
||||
|
||||
if (!CI && parsed) {
|
||||
// caching the results of the websocket commands makes testing this locally much faster
|
||||
// real calls to the engine are needed to test the artifact map
|
||||
// bust the cache with: `rm -rf src/lang/std/artifactGraphCache`
|
||||
return
|
||||
}
|
||||
|
||||
// THESE TEST WILL FAIL without VITE_KC_DEV_TOKEN set in .env.development.local
|
||||
engineCommandManager.start({
|
||||
disableWebRTC: true,
|
||||
token: VITE_KC_DEV_TOKEN,
|
||||
// there does seem to be a minimum resolution, not sure what it is but 256 works ok.
|
||||
width: 256,
|
||||
height: 256,
|
||||
executeCode: () => {},
|
||||
makeDefaultPlanes: () => makeDefaultPlanes(engineCommandManager),
|
||||
setMediaStream: () => {},
|
||||
setIsStreamReady: () => {},
|
||||
modifyGrid: async () => {},
|
||||
})
|
||||
await engineCommandManager.waitForReady
|
||||
|
||||
const cacheEntries = Object.entries(codeToWriteCacheFor) as [
|
||||
CodeKey,
|
||||
string
|
||||
][]
|
||||
const cacheToWriteToFileTemp: Partial<CacheShape> = {}
|
||||
for (const [codeKey, code] of cacheEntries) {
|
||||
const ast = parse(code)
|
||||
if (err(ast)) {
|
||||
console.error(ast)
|
||||
throw ast
|
||||
}
|
||||
await kclManager.executeAst(ast)
|
||||
|
||||
cacheToWriteToFileTemp[codeKey] = {
|
||||
orderedCommands: engineCommandManager.orderedCommands,
|
||||
responseMap: engineCommandManager.responseMap,
|
||||
}
|
||||
}
|
||||
const cache = JSON.stringify(cacheToWriteToFileTemp)
|
||||
|
||||
await fsp.mkdir(pathStart, { recursive: true })
|
||||
await fsp.writeFile(fullPath, cache)
|
||||
}, 20_000)
|
||||
|
||||
afterAll(() => {
|
||||
engineCommandManager.tearDown()
|
||||
})
|
||||
|
||||
describe('testing createArtifactGraph', () => {
|
||||
describe('code with an extrusion, fillet and sketch of face:', () => {
|
||||
let ast: Program
|
||||
let theMap: ReturnType<typeof createArtifactGraph>
|
||||
it('setup', () => {
|
||||
// putting this logic in here because describe blocks runs before beforeAll has finished
|
||||
const {
|
||||
orderedCommands,
|
||||
responseMap,
|
||||
ast: _ast,
|
||||
} = getCommands('exampleCode1')
|
||||
ast = _ast
|
||||
theMap = createArtifactGraph({ orderedCommands, responseMap, ast })
|
||||
})
|
||||
|
||||
it('there should be two planes for the extrusion and the sketch on face', () => {
|
||||
const planes = [...filterArtifacts({ types: ['plane'] }, theMap)].map(
|
||||
(plane) => expandPlane(plane[1], theMap)
|
||||
)
|
||||
expect(planes).toHaveLength(1)
|
||||
planes.forEach((path) => {
|
||||
expect(path.type).toBe('plane')
|
||||
})
|
||||
})
|
||||
it('there should be two paths for the extrusion and the sketch on face', () => {
|
||||
const paths = [...filterArtifacts({ types: ['path'] }, theMap)].map(
|
||||
(path) => expandPath(path[1], theMap)
|
||||
)
|
||||
expect(paths).toHaveLength(2)
|
||||
paths.forEach((path) => {
|
||||
if (err(path)) throw path
|
||||
expect(path.type).toBe('path')
|
||||
})
|
||||
})
|
||||
|
||||
it('there should be two extrusions, for the original and the sketchOnFace, the first extrusion should have 6 sides of the cube', () => {
|
||||
const extrusions = [
|
||||
...filterArtifacts({ types: ['extrusion'] }, theMap),
|
||||
].map((extrusion) => expandExtrusion(extrusion[1], theMap))
|
||||
expect(extrusions).toHaveLength(2)
|
||||
extrusions.forEach((extrusion, index) => {
|
||||
if (err(extrusion)) throw extrusion
|
||||
expect(extrusion.type).toBe('extrusion')
|
||||
const firstExtrusionIsACubeIE6Sides = 6
|
||||
const secondExtrusionIsATriangularPrismIE5Sides = 5
|
||||
expect(extrusion.surfaces.length).toBe(
|
||||
!index
|
||||
? firstExtrusionIsACubeIE6Sides
|
||||
: secondExtrusionIsATriangularPrismIE5Sides
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it('there should be 5 + 4 segments, 4 (+close) from the first extrusion and 3 (+close) from the second', () => {
|
||||
const segments = [...filterArtifacts({ types: ['segment'] }, theMap)].map(
|
||||
(segment) => expandSegment(segment[1], theMap)
|
||||
)
|
||||
expect(segments).toHaveLength(9)
|
||||
})
|
||||
|
||||
it('snapshot of the artifactGraph', () => {
|
||||
const stableMap = new Map(
|
||||
[...theMap].map(([, artifact], index): [string, any] => {
|
||||
const stableValue: any = {}
|
||||
Object.entries(artifact).forEach(([propName, value]) => {
|
||||
if (
|
||||
propName === 'type' ||
|
||||
propName === 'codeRef' ||
|
||||
propName === 'subType'
|
||||
) {
|
||||
stableValue[propName] = value
|
||||
return
|
||||
}
|
||||
if (Array.isArray(value))
|
||||
stableValue[propName] = value.map(() => 'UUID')
|
||||
if (typeof value === 'string' && value)
|
||||
stableValue[propName] = 'UUID'
|
||||
})
|
||||
return [`UUID-${index}`, stableValue]
|
||||
})
|
||||
)
|
||||
expect(stableMap).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('screenshot graph', async () => {
|
||||
// Ostensibly this takes a screen shot of the graph of the artifactGraph
|
||||
// but it's it also tests that all of the id links are correct because if one
|
||||
// of the edges refers to a non-existent node, the graph will throw.
|
||||
// further more we can check that each edge is bi-directional, if it's not
|
||||
// by checking the arrow heads going both ways, on the graph.
|
||||
await GraphTheGraph(theMap, 1400, 1400, 'exampleCode1.png')
|
||||
}, 20000)
|
||||
})
|
||||
})
|
||||
|
||||
describe('capture graph of sketchOnFaceOnFace...', () => {
|
||||
describe('code with an extrusion, fillet and sketch of face:', () => {
|
||||
let ast: Program
|
||||
let theMap: ReturnType<typeof createArtifactGraph>
|
||||
it('setup', async () => {
|
||||
// putting this logic in here because describe blocks runs before beforeAll has finished
|
||||
const {
|
||||
orderedCommands,
|
||||
responseMap,
|
||||
ast: _ast,
|
||||
} = getCommands('sketchOnFaceOnFaceEtc')
|
||||
ast = _ast
|
||||
theMap = createArtifactGraph({ orderedCommands, responseMap, ast })
|
||||
|
||||
// Ostensibly this takes a screen shot of the graph of the artifactGraph
|
||||
// but it's it also tests that all of the id links are correct because if one
|
||||
// of the edges refers to a non-existent node, the graph will throw.
|
||||
// further more we can check that each edge is bi-directional, if it's not
|
||||
// by checking the arrow heads going both ways, on the graph.
|
||||
await GraphTheGraph(theMap, 2500, 2500, 'sketchOnFaceOnFaceEtc.png')
|
||||
}, 20000)
|
||||
})
|
||||
})
|
||||
|
||||
function getCommands(codeKey: CodeKey): CacheShape[CodeKey] & { ast: Program } {
|
||||
const ast = parse(codeKey)
|
||||
if (err(ast)) {
|
||||
console.error(ast)
|
||||
throw ast
|
||||
}
|
||||
const file = fs.readFileSync(fullPath, 'utf-8')
|
||||
const parsed: CacheShape = JSON.parse(file)
|
||||
// these either already exist from the last run, or were created in
|
||||
const orderedCommands = parsed[codeKey].orderedCommands
|
||||
const responseMap = parsed[codeKey].responseMap
|
||||
return {
|
||||
orderedCommands,
|
||||
responseMap,
|
||||
ast,
|
||||
}
|
||||
}
|
||||
|
||||
async function GraphTheGraph(
|
||||
theMap: ArtifactGraph,
|
||||
sizeX: number,
|
||||
sizeY: number,
|
||||
imageName: string
|
||||
) {
|
||||
const nodes: Array<{ id: string; label: string }> = []
|
||||
const edges: Array<{ source: string; target: string; label: string }> = []
|
||||
let index = 0
|
||||
for (const [commandId, artifact] of theMap) {
|
||||
nodes.push({
|
||||
id: commandId,
|
||||
label: `${artifact.type}-${index++}`,
|
||||
})
|
||||
Object.entries(artifact).forEach(([propName, value]) => {
|
||||
if (
|
||||
propName === 'type' ||
|
||||
propName === 'codeRef' ||
|
||||
propName === 'subType'
|
||||
)
|
||||
return
|
||||
if (Array.isArray(value))
|
||||
value.forEach((v) => {
|
||||
v && edges.push({ source: commandId, target: v, label: propName })
|
||||
})
|
||||
if (typeof value === 'string' && value)
|
||||
edges.push({ source: commandId, target: value, label: propName })
|
||||
})
|
||||
}
|
||||
|
||||
// Create a force simulation to calculate node positions
|
||||
const simulation = d3
|
||||
.forceSimulation(nodes as any)
|
||||
.force(
|
||||
'link',
|
||||
d3
|
||||
.forceLink(edges)
|
||||
.id((d: any) => d.id)
|
||||
.distance(100)
|
||||
)
|
||||
.force('charge', d3.forceManyBody().strength(-300))
|
||||
.force('center', d3.forceCenter(300, 200))
|
||||
.stop()
|
||||
|
||||
// Run the simulation
|
||||
for (let i = 0; i < 300; ++i) simulation.tick()
|
||||
|
||||
// Create traces for Plotly
|
||||
const nodeTrace = {
|
||||
x: nodes.map((node: any) => node.x),
|
||||
y: nodes.map((node: any) => node.y),
|
||||
text: nodes.map((node) => node.label), // Use the custom label
|
||||
mode: 'markers+text',
|
||||
type: 'scatter',
|
||||
marker: { size: 20, color: 'gray' }, // Nodes in gray
|
||||
textfont: { size: 14, color: 'black' }, // Labels in black
|
||||
textposition: 'top center', // Position text on top
|
||||
}
|
||||
|
||||
const edgeTrace = {
|
||||
x: [],
|
||||
y: [],
|
||||
mode: 'lines',
|
||||
type: 'scatter',
|
||||
line: { width: 2, color: 'lightgray' }, // Edges in light gray
|
||||
}
|
||||
|
||||
const annotations: any[] = []
|
||||
|
||||
edges.forEach((edge) => {
|
||||
const sourceNode = nodes.find(
|
||||
(node: any) => node.id === (edge as any).source.id
|
||||
)
|
||||
const targetNode = nodes.find(
|
||||
(node: any) => node.id === (edge as any).target.id
|
||||
)
|
||||
|
||||
// Check if nodes are found
|
||||
if (!sourceNode || !targetNode) {
|
||||
throw new Error(
|
||||
// @ts-ignore
|
||||
`Node not found: ${!sourceNode ? edge.source.id : edge.target.id}`
|
||||
)
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
edgeTrace.x.push(sourceNode.x, targetNode.x, null)
|
||||
// @ts-ignore
|
||||
edgeTrace.y.push(sourceNode.y, targetNode.y, null)
|
||||
|
||||
// Calculate offset for arrowhead
|
||||
const offsetFactor = 0.9 // Adjust this factor to control the offset distance
|
||||
// @ts-ignore
|
||||
const offsetX = (targetNode.x - sourceNode.x) * offsetFactor
|
||||
// @ts-ignore
|
||||
const offsetY = (targetNode.y - sourceNode.y) * offsetFactor
|
||||
|
||||
// Add arrowhead annotation with offset
|
||||
annotations.push({
|
||||
// @ts-ignore
|
||||
ax: sourceNode.x,
|
||||
// @ts-ignore
|
||||
ay: sourceNode.y,
|
||||
// @ts-ignore
|
||||
x: targetNode.x - offsetX,
|
||||
// @ts-ignore
|
||||
y: targetNode.y - offsetY,
|
||||
xref: 'x',
|
||||
yref: 'y',
|
||||
axref: 'x',
|
||||
ayref: 'y',
|
||||
showarrow: true,
|
||||
arrowhead: 2,
|
||||
arrowsize: 1,
|
||||
arrowwidth: 2,
|
||||
arrowcolor: 'darkgray', // Arrowheads in dark gray
|
||||
})
|
||||
|
||||
// Add edge label annotation closer to the edge tail (25% of the length)
|
||||
// @ts-ignore
|
||||
const labelX = sourceNode.x * 0.75 + targetNode.x * 0.25
|
||||
// @ts-ignore
|
||||
const labelY = sourceNode.y * 0.75 + targetNode.y * 0.25
|
||||
annotations.push({
|
||||
x: labelX,
|
||||
y: labelY,
|
||||
xref: 'x',
|
||||
yref: 'y',
|
||||
text: edge.label,
|
||||
showarrow: false,
|
||||
font: { size: 12, color: 'black' }, // Edge labels in black
|
||||
align: 'center',
|
||||
})
|
||||
})
|
||||
|
||||
const data = [edgeTrace, nodeTrace]
|
||||
|
||||
const layout = {
|
||||
// title: 'Force-Directed Graph with Nodes and Edges',
|
||||
xaxis: { showgrid: false, zeroline: false, showticklabels: false },
|
||||
yaxis: { showgrid: false, zeroline: false, showticklabels: false },
|
||||
showlegend: false,
|
||||
annotations: annotations,
|
||||
}
|
||||
|
||||
// Export to PNG using Playwright
|
||||
const browser = await chromium.launch()
|
||||
const page = await browser.newPage()
|
||||
await page.setContent(`
|
||||
<html>
|
||||
<head>
|
||||
<script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="plotly-graph" style="width:${sizeX}px;height:${sizeY}px;"></div>
|
||||
<script>
|
||||
Plotly.newPlot('plotly-graph', ${JSON.stringify(
|
||||
data
|
||||
)}, ${JSON.stringify(layout)})
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
`)
|
||||
await page.waitForSelector('#plotly-graph')
|
||||
const element = await page.$('#plotly-graph')
|
||||
|
||||
// @ts-ignore
|
||||
await element.screenshot({
|
||||
path: `./e2e/playwright/temp3.png`,
|
||||
})
|
||||
|
||||
await browser.close()
|
||||
|
||||
const img1Path = path.resolve(`./src/lang/std/artifactMapGraphs/${imageName}`)
|
||||
const img2Path = path.resolve('./e2e/playwright/temp3.png')
|
||||
|
||||
const img1 = PNG.sync.read(fs.readFileSync(img1Path))
|
||||
const img2 = PNG.sync.read(fs.readFileSync(img2Path))
|
||||
|
||||
const { width, height } = img1
|
||||
const diff = new PNG({ width, height })
|
||||
|
||||
const numDiffPixels = pixelmatch(
|
||||
img1.data,
|
||||
img2.data,
|
||||
diff.data,
|
||||
width,
|
||||
height,
|
||||
{ threshold: 0.1 }
|
||||
)
|
||||
|
||||
if (numDiffPixels > 10) {
|
||||
console.warn('numDiffPixels', numDiffPixels)
|
||||
// write file out to final place
|
||||
fs.writeFileSync(
|
||||
`src/lang/std/artifactMapGraphs/${imageName}`,
|
||||
PNG.sync.write(img2)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
describe('testing getArtifactsToUpdate', () => {
|
||||
it('should return an array of artifacts to update', () => {
|
||||
const { orderedCommands, responseMap, ast } = getCommands('exampleCode1')
|
||||
const map = createArtifactGraph({ orderedCommands, responseMap, ast })
|
||||
const getArtifact = (id: string) => map.get(id)
|
||||
const currentPlaneId = 'UUID-1'
|
||||
const getUpdateObjects = (type: Models['ModelingCmd_type']['type']) => {
|
||||
const artifactsToUpdate = getArtifactsToUpdate({
|
||||
orderedCommand: orderedCommands.find(
|
||||
(a) =>
|
||||
a.command.type === 'modeling_cmd_req' && a.command.cmd.type === type
|
||||
)!,
|
||||
responseMap,
|
||||
getArtifact,
|
||||
currentPlaneId,
|
||||
ast,
|
||||
})
|
||||
return artifactsToUpdate.map(({ artifact }) => artifact)
|
||||
}
|
||||
expect(getUpdateObjects('start_path')).toEqual([
|
||||
{
|
||||
type: 'path',
|
||||
segIds: [],
|
||||
planeId: 'UUID-1',
|
||||
extrusionId: '',
|
||||
codeRef: {
|
||||
pathToNode: [['body', '']],
|
||||
range: [43, 70],
|
||||
},
|
||||
},
|
||||
])
|
||||
expect(getUpdateObjects('extrude')).toEqual([
|
||||
{
|
||||
type: 'extrusion',
|
||||
pathId: expect.any(String),
|
||||
surfaceIds: [],
|
||||
edgeIds: [],
|
||||
codeRef: {
|
||||
range: [243, 266],
|
||||
pathToNode: [['body', '']],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'path',
|
||||
segIds: expect.any(Array),
|
||||
planeId: expect.any(String),
|
||||
extrusionId: expect.any(String),
|
||||
codeRef: {
|
||||
range: [43, 70],
|
||||
pathToNode: [['body', '']],
|
||||
},
|
||||
solid2dId: expect.any(String),
|
||||
},
|
||||
])
|
||||
expect(getUpdateObjects('extend_path')).toEqual([
|
||||
{
|
||||
type: 'segment',
|
||||
pathId: expect.any(String),
|
||||
surfaceId: '',
|
||||
edgeIds: [],
|
||||
codeRef: {
|
||||
range: [76, 92],
|
||||
pathToNode: [['body', '']],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'path',
|
||||
segIds: expect.any(Array),
|
||||
planeId: expect.any(String),
|
||||
extrusionId: expect.any(String),
|
||||
codeRef: {
|
||||
range: [43, 70],
|
||||
pathToNode: [['body', '']],
|
||||
},
|
||||
solid2dId: expect.any(String),
|
||||
},
|
||||
])
|
||||
expect(getUpdateObjects('solid3d_fillet_edge')).toEqual([
|
||||
{
|
||||
type: 'edgeCut',
|
||||
subType: 'fillet',
|
||||
consumedEdgeId: expect.any(String),
|
||||
edgeIds: [],
|
||||
surfaceId: '',
|
||||
codeRef: {
|
||||
range: [272, 311],
|
||||
pathToNode: [['body', '']],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'segment',
|
||||
pathId: expect.any(String),
|
||||
surfaceId: expect.any(String),
|
||||
edgeIds: [],
|
||||
codeRef: {
|
||||
range: [98, 125],
|
||||
pathToNode: [['body', '']],
|
||||
},
|
||||
edgeCutId: expect.any(String),
|
||||
},
|
||||
])
|
||||
expect(getUpdateObjects('solid3d_get_extrusion_face_info')).toEqual([
|
||||
{
|
||||
type: 'wall',
|
||||
segId: expect.any(String),
|
||||
edgeCutEdgeIds: [],
|
||||
extrusionId: expect.any(String),
|
||||
pathIds: [],
|
||||
},
|
||||
{
|
||||
type: 'segment',
|
||||
pathId: expect.any(String),
|
||||
surfaceId: expect.any(String),
|
||||
edgeIds: [],
|
||||
codeRef: {
|
||||
range: [162, 209],
|
||||
pathToNode: [['body', '']],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'extrusion',
|
||||
pathId: expect.any(String),
|
||||
surfaceIds: expect.any(Array),
|
||||
edgeIds: [],
|
||||
codeRef: {
|
||||
range: [243, 266],
|
||||
pathToNode: [['body', '']],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'wall',
|
||||
segId: expect.any(String),
|
||||
edgeCutEdgeIds: [],
|
||||
extrusionId: expect.any(String),
|
||||
pathIds: [],
|
||||
},
|
||||
{
|
||||
type: 'segment',
|
||||
pathId: expect.any(String),
|
||||
surfaceId: expect.any(String),
|
||||
edgeIds: [],
|
||||
codeRef: {
|
||||
range: [131, 156],
|
||||
pathToNode: [['body', '']],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'extrusion',
|
||||
pathId: expect.any(String),
|
||||
surfaceIds: expect.any(Array),
|
||||
edgeIds: [],
|
||||
codeRef: {
|
||||
range: [243, 266],
|
||||
pathToNode: [['body', '']],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'wall',
|
||||
segId: expect.any(String),
|
||||
edgeCutEdgeIds: [],
|
||||
extrusionId: expect.any(String),
|
||||
pathIds: [],
|
||||
},
|
||||
{
|
||||
type: 'segment',
|
||||
pathId: expect.any(String),
|
||||
surfaceId: expect.any(String),
|
||||
edgeIds: [],
|
||||
codeRef: {
|
||||
range: [98, 125],
|
||||
pathToNode: [['body', '']],
|
||||
},
|
||||
edgeCutId: expect.any(String),
|
||||
},
|
||||
{
|
||||
type: 'extrusion',
|
||||
pathId: expect.any(String),
|
||||
surfaceIds: expect.any(Array),
|
||||
edgeIds: [],
|
||||
codeRef: {
|
||||
range: [243, 266],
|
||||
pathToNode: [['body', '']],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'wall',
|
||||
segId: expect.any(String),
|
||||
edgeCutEdgeIds: [],
|
||||
extrusionId: expect.any(String),
|
||||
pathIds: [],
|
||||
},
|
||||
{
|
||||
type: 'segment',
|
||||
pathId: expect.any(String),
|
||||
surfaceId: expect.any(String),
|
||||
edgeIds: [],
|
||||
codeRef: {
|
||||
range: [76, 92],
|
||||
pathToNode: [['body', '']],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'extrusion',
|
||||
pathId: expect.any(String),
|
||||
surfaceIds: expect.any(Array),
|
||||
edgeIds: [],
|
||||
codeRef: {
|
||||
range: [243, 266],
|
||||
pathToNode: [['body', '']],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'cap',
|
||||
subType: 'start',
|
||||
edgeCutEdgeIds: [],
|
||||
extrusionId: expect.any(String),
|
||||
pathIds: [],
|
||||
},
|
||||
{
|
||||
type: 'extrusion',
|
||||
pathId: expect.any(String),
|
||||
surfaceIds: expect.any(Array),
|
||||
edgeIds: [],
|
||||
codeRef: {
|
||||
range: [243, 266],
|
||||
pathToNode: [['body', '']],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'cap',
|
||||
subType: 'end',
|
||||
edgeCutEdgeIds: [],
|
||||
extrusionId: expect.any(String),
|
||||
pathIds: [],
|
||||
},
|
||||
{
|
||||
type: 'extrusion',
|
||||
pathId: expect.any(String),
|
||||
surfaceIds: expect.any(Array),
|
||||
edgeIds: [],
|
||||
codeRef: {
|
||||
range: [243, 266],
|
||||
pathToNode: [['body', '']],
|
||||
},
|
||||
},
|
||||
])
|
||||
})
|
||||
})
|
682
src/lang/std/artifactGraph.ts
Normal file
682
src/lang/std/artifactGraph.ts
Normal file
@ -0,0 +1,682 @@
|
||||
import { PathToNode, Program, SourceRange } from 'lang/wasm'
|
||||
import { Models } from '@kittycad/lib'
|
||||
import { getNodePathFromSourceRange } from 'lang/queryAst'
|
||||
import { err } from 'lib/trap'
|
||||
|
||||
interface CommonCommandProperties {
|
||||
range: SourceRange
|
||||
pathToNode: PathToNode
|
||||
}
|
||||
|
||||
export interface PlaneArtifact {
|
||||
type: 'plane'
|
||||
pathIds: Array<string>
|
||||
codeRef: CommonCommandProperties
|
||||
}
|
||||
export interface PlaneArtifactRich {
|
||||
type: 'plane'
|
||||
paths: Array<PathArtifact>
|
||||
codeRef: CommonCommandProperties
|
||||
}
|
||||
|
||||
export interface PathArtifact {
|
||||
type: 'path'
|
||||
planeId: string
|
||||
segIds: Array<string>
|
||||
extrusionId: string
|
||||
solid2dId?: string
|
||||
codeRef: CommonCommandProperties
|
||||
}
|
||||
|
||||
interface solid2D {
|
||||
type: 'solid2D'
|
||||
pathId: string
|
||||
}
|
||||
export interface PathArtifactRich {
|
||||
type: 'path'
|
||||
plane: PlaneArtifact | WallArtifact
|
||||
segments: Array<SegmentArtifact>
|
||||
extrusion: ExtrusionArtifact
|
||||
codeRef: CommonCommandProperties
|
||||
}
|
||||
|
||||
interface SegmentArtifact {
|
||||
type: 'segment'
|
||||
pathId: string
|
||||
surfaceId: string
|
||||
edgeIds: Array<string>
|
||||
edgeCutId?: string
|
||||
codeRef: CommonCommandProperties
|
||||
}
|
||||
interface SegmentArtifactRich {
|
||||
type: 'segment'
|
||||
path: PathArtifact
|
||||
surf: WallArtifact
|
||||
edges: Array<ExtrudeEdge>
|
||||
edgeCut?: EdgeCut
|
||||
codeRef: CommonCommandProperties
|
||||
}
|
||||
|
||||
interface ExtrusionArtifact {
|
||||
type: 'extrusion'
|
||||
pathId: string
|
||||
surfaceIds: Array<string>
|
||||
edgeIds: Array<string>
|
||||
codeRef: CommonCommandProperties
|
||||
}
|
||||
interface ExtrusionArtifactRich {
|
||||
type: 'extrusion'
|
||||
path: PathArtifact
|
||||
surfaces: Array<WallArtifact | CapArtifact>
|
||||
edges: Array<ExtrudeEdge>
|
||||
codeRef: CommonCommandProperties
|
||||
}
|
||||
|
||||
interface WallArtifact {
|
||||
type: 'wall'
|
||||
segId: string
|
||||
edgeCutEdgeIds: Array<string>
|
||||
extrusionId: string
|
||||
pathIds: Array<string>
|
||||
}
|
||||
interface CapArtifact {
|
||||
type: 'cap'
|
||||
subType: 'start' | 'end'
|
||||
edgeCutEdgeIds: Array<string>
|
||||
extrusionId: string
|
||||
pathIds: Array<string>
|
||||
}
|
||||
|
||||
interface ExtrudeEdge {
|
||||
type: 'extrudeEdge'
|
||||
segId: string
|
||||
extrusionId: string
|
||||
edgeId: string
|
||||
}
|
||||
|
||||
/** A edgeCut is a more generic term for both fillet or chamfer */
|
||||
interface EdgeCut {
|
||||
type: 'edgeCut'
|
||||
subType: 'fillet' | 'chamfer'
|
||||
consumedEdgeId: string
|
||||
edgeIds: Array<string>
|
||||
surfaceId: string
|
||||
codeRef: CommonCommandProperties
|
||||
}
|
||||
|
||||
interface EdgeCutEdge {
|
||||
type: 'edgeCutEdge'
|
||||
edgeCutId: string
|
||||
surfaceId: string
|
||||
}
|
||||
|
||||
export type Artifact =
|
||||
| PlaneArtifact
|
||||
| PathArtifact
|
||||
| SegmentArtifact
|
||||
| ExtrusionArtifact
|
||||
| WallArtifact
|
||||
| CapArtifact
|
||||
| ExtrudeEdge
|
||||
| EdgeCut
|
||||
| EdgeCutEdge
|
||||
| solid2D
|
||||
|
||||
export type ArtifactGraph = Map<string, Artifact>
|
||||
|
||||
export type EngineCommand = Models['WebSocketRequest_type']
|
||||
|
||||
type OkWebSocketResponseData = Models['OkWebSocketResponseData_type']
|
||||
|
||||
export interface ResponseMap {
|
||||
[commandId: string]: OkWebSocketResponseData
|
||||
}
|
||||
export interface OrderedCommand {
|
||||
command: EngineCommand
|
||||
range: SourceRange
|
||||
}
|
||||
|
||||
/** Creates a graph of artifacts from a list of ordered commands and their responses
|
||||
* muting the Map should happen entirely this function, other functions called within
|
||||
* should return data on how to update the map, and not do so directly.
|
||||
*/
|
||||
export function createArtifactGraph({
|
||||
orderedCommands,
|
||||
responseMap,
|
||||
ast,
|
||||
}: {
|
||||
orderedCommands: Array<OrderedCommand>
|
||||
responseMap: ResponseMap
|
||||
ast: Program
|
||||
}) {
|
||||
const myMap = new Map<string, Artifact>()
|
||||
|
||||
/** see docstring for {@link getArtifactsToUpdate} as to why this is needed */
|
||||
let currentPlaneId = ''
|
||||
|
||||
orderedCommands.forEach((orderedCommand) => {
|
||||
if (orderedCommand.command?.type === 'modeling_cmd_req') {
|
||||
if (orderedCommand.command.cmd.type === 'enable_sketch_mode') {
|
||||
currentPlaneId = orderedCommand.command.cmd.entity_id
|
||||
}
|
||||
if (orderedCommand.command.cmd.type === 'sketch_mode_disable') {
|
||||
currentPlaneId = ''
|
||||
}
|
||||
}
|
||||
const artifactsToUpdate = getArtifactsToUpdate({
|
||||
orderedCommand,
|
||||
responseMap,
|
||||
getArtifact: (id: string) => myMap.get(id),
|
||||
currentPlaneId,
|
||||
ast,
|
||||
})
|
||||
artifactsToUpdate.forEach(({ id, artifact }) => {
|
||||
const mergedArtifact = mergeArtifacts(myMap.get(id), artifact)
|
||||
myMap.set(id, mergedArtifact)
|
||||
})
|
||||
})
|
||||
return myMap
|
||||
}
|
||||
|
||||
/** Merges two artifacts, since our artifacts only contain strings and arrays of string for values we coerce that
|
||||
* but maybe types can be improved here.
|
||||
*/
|
||||
function mergeArtifacts(
|
||||
oldArtifact: Artifact | undefined,
|
||||
newArtifact: Artifact
|
||||
): Artifact {
|
||||
// only has string and array of strings
|
||||
interface GenericArtifact {
|
||||
[key: string]: string | Array<string>
|
||||
}
|
||||
if (!oldArtifact) return newArtifact
|
||||
// merging artifacts of different types should never happen, but if it does, just return the new artifact
|
||||
if (oldArtifact.type !== newArtifact.type) return newArtifact
|
||||
const _oldArtifact = oldArtifact as any as GenericArtifact
|
||||
const mergedArtifact = { ...oldArtifact, ...newArtifact } as GenericArtifact
|
||||
Object.entries(newArtifact as any as GenericArtifact).forEach(
|
||||
([propName, value]) => {
|
||||
const otherValue = _oldArtifact[propName]
|
||||
if (Array.isArray(value) && Array.isArray(otherValue)) {
|
||||
mergedArtifact[propName] = [...new Set([...otherValue, ...value])]
|
||||
}
|
||||
}
|
||||
)
|
||||
return mergedArtifact as any as Artifact
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes a single command and it's response in order to populate the artifact map
|
||||
* It does not mutate the map directly, but returns an array of artifacts to update
|
||||
*
|
||||
* @param currentPlaneId is only needed for `start_path` commands because this command does not have a pathId
|
||||
* instead it relies on the id used with the `enable_sketch_mode` command, so this much be kept track of
|
||||
* outside of this function. It would be good to update the `start_path` command to include the planeId so we
|
||||
* can remove this.
|
||||
*/
|
||||
export function getArtifactsToUpdate({
|
||||
orderedCommand: { command, range },
|
||||
getArtifact,
|
||||
responseMap,
|
||||
currentPlaneId,
|
||||
ast,
|
||||
}: {
|
||||
orderedCommand: OrderedCommand
|
||||
responseMap: ResponseMap
|
||||
/** Passing in a getter because we don't wan this function to update the map directly */
|
||||
getArtifact: (id: string) => Artifact | undefined
|
||||
currentPlaneId: string
|
||||
ast: Program
|
||||
}): Array<{
|
||||
id: string
|
||||
artifact: Artifact
|
||||
}> {
|
||||
const pathToNode = getNodePathFromSourceRange(ast, range)
|
||||
|
||||
// expect all to be `modeling_cmd_req` as batch commands have
|
||||
// already been expanded before being added to orderedCommands
|
||||
if (command.type !== 'modeling_cmd_req') return []
|
||||
const id = command.cmd_id
|
||||
const response = responseMap[id]
|
||||
const cmd = command.cmd
|
||||
const returnArr: ReturnType<typeof getArtifactsToUpdate> = []
|
||||
if (cmd.type === 'enable_sketch_mode') {
|
||||
const plane = getArtifact(currentPlaneId)
|
||||
const pathIds = plane?.type === 'plane' ? plane?.pathIds : []
|
||||
const codeRef =
|
||||
plane?.type === 'plane' ? plane?.codeRef : { range, pathToNode }
|
||||
const existingPlane = getArtifact(currentPlaneId)
|
||||
if (existingPlane?.type === 'wall') {
|
||||
return [
|
||||
{
|
||||
id: currentPlaneId,
|
||||
artifact: {
|
||||
type: 'wall',
|
||||
segId: existingPlane.segId,
|
||||
edgeCutEdgeIds: existingPlane.edgeCutEdgeIds,
|
||||
extrusionId: existingPlane.extrusionId,
|
||||
pathIds: existingPlane.pathIds,
|
||||
},
|
||||
},
|
||||
]
|
||||
} else {
|
||||
return [
|
||||
{ id: currentPlaneId, artifact: { type: 'plane', pathIds, codeRef } },
|
||||
]
|
||||
}
|
||||
} else if (cmd.type === 'start_path') {
|
||||
returnArr.push({
|
||||
id,
|
||||
artifact: {
|
||||
type: 'path',
|
||||
segIds: [],
|
||||
planeId: currentPlaneId,
|
||||
extrusionId: '',
|
||||
codeRef: { range, pathToNode },
|
||||
},
|
||||
})
|
||||
const plane = getArtifact(currentPlaneId)
|
||||
const codeRef =
|
||||
plane?.type === 'plane' ? plane?.codeRef : { range, pathToNode }
|
||||
if (plane?.type === 'plane') {
|
||||
returnArr.push({
|
||||
id: currentPlaneId,
|
||||
artifact: { type: 'plane', pathIds: [id], codeRef },
|
||||
})
|
||||
}
|
||||
if (plane?.type === 'wall') {
|
||||
returnArr.push({
|
||||
id: currentPlaneId,
|
||||
artifact: {
|
||||
type: 'wall',
|
||||
segId: plane.segId,
|
||||
edgeCutEdgeIds: plane.edgeCutEdgeIds,
|
||||
extrusionId: plane.extrusionId,
|
||||
pathIds: [id],
|
||||
},
|
||||
})
|
||||
}
|
||||
return returnArr
|
||||
} else if (cmd.type === 'extend_path' || cmd.type === 'close_path') {
|
||||
const pathId = cmd.type === 'extend_path' ? cmd.path : cmd.path_id
|
||||
returnArr.push({
|
||||
id,
|
||||
artifact: {
|
||||
type: 'segment',
|
||||
pathId,
|
||||
surfaceId: '',
|
||||
edgeIds: [],
|
||||
codeRef: { range, pathToNode },
|
||||
},
|
||||
})
|
||||
const path = getArtifact(pathId)
|
||||
if (path?.type === 'path')
|
||||
returnArr.push({
|
||||
id: pathId,
|
||||
artifact: { ...path, segIds: [id] },
|
||||
})
|
||||
if (
|
||||
response.type === 'modeling' &&
|
||||
response.data.modeling_response.type === 'close_path'
|
||||
) {
|
||||
returnArr.push({
|
||||
id: response.data.modeling_response.data.face_id,
|
||||
artifact: { type: 'solid2D', pathId },
|
||||
})
|
||||
const path = getArtifact(pathId)
|
||||
if (path?.type === 'path')
|
||||
returnArr.push({
|
||||
id: pathId,
|
||||
artifact: {
|
||||
...path,
|
||||
solid2dId: response.data.modeling_response.data.face_id,
|
||||
},
|
||||
})
|
||||
}
|
||||
return returnArr
|
||||
} else if (cmd.type === 'extrude') {
|
||||
returnArr.push({
|
||||
id,
|
||||
artifact: {
|
||||
type: 'extrusion',
|
||||
pathId: cmd.target,
|
||||
surfaceIds: [],
|
||||
edgeIds: [],
|
||||
codeRef: { range, pathToNode },
|
||||
},
|
||||
})
|
||||
const path = getArtifact(cmd.target)
|
||||
if (path?.type === 'path')
|
||||
returnArr.push({
|
||||
id: cmd.target,
|
||||
artifact: { ...path, extrusionId: id },
|
||||
})
|
||||
return returnArr
|
||||
} else if (
|
||||
cmd.type === 'solid3d_get_extrusion_face_info' &&
|
||||
response?.type === 'modeling' &&
|
||||
response.data.modeling_response.type === 'solid3d_get_extrusion_face_info'
|
||||
) {
|
||||
let lastPath: PathArtifact
|
||||
response.data.modeling_response.data.faces.forEach(
|
||||
({ curve_id, cap, face_id }) => {
|
||||
if (cap === 'none' && curve_id && face_id) {
|
||||
const seg = getArtifact(curve_id)
|
||||
if (seg?.type !== 'segment') return
|
||||
const path = getArtifact(seg.pathId)
|
||||
if (path?.type === 'path' && seg?.type === 'segment') {
|
||||
lastPath = path
|
||||
returnArr.push({
|
||||
id: face_id,
|
||||
artifact: {
|
||||
type: 'wall',
|
||||
segId: curve_id,
|
||||
edgeCutEdgeIds: [],
|
||||
extrusionId: path.extrusionId,
|
||||
pathIds: [],
|
||||
},
|
||||
})
|
||||
returnArr.push({
|
||||
id: curve_id,
|
||||
artifact: { ...seg, surfaceId: face_id },
|
||||
})
|
||||
const extrusion = getArtifact(path.extrusionId)
|
||||
if (extrusion?.type === 'extrusion') {
|
||||
returnArr.push({
|
||||
id: path.extrusionId,
|
||||
artifact: {
|
||||
...extrusion,
|
||||
surfaceIds: [face_id],
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
response.data.modeling_response.data.faces.forEach(({ cap, face_id }) => {
|
||||
if ((cap === 'top' || cap === 'bottom') && face_id) {
|
||||
const path = lastPath
|
||||
if (path?.type === 'path') {
|
||||
returnArr.push({
|
||||
id: face_id,
|
||||
artifact: {
|
||||
type: 'cap',
|
||||
subType: cap === 'bottom' ? 'start' : 'end',
|
||||
edgeCutEdgeIds: [],
|
||||
extrusionId: path.extrusionId,
|
||||
pathIds: [],
|
||||
},
|
||||
})
|
||||
const extrusion = getArtifact(path.extrusionId)
|
||||
if (extrusion?.type !== 'extrusion') return
|
||||
returnArr.push({
|
||||
id: path.extrusionId,
|
||||
artifact: {
|
||||
...extrusion,
|
||||
surfaceIds: [face_id],
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
return returnArr
|
||||
} else if (cmd.type === 'solid3d_fillet_edge') {
|
||||
returnArr.push({
|
||||
id,
|
||||
artifact: {
|
||||
type: 'edgeCut',
|
||||
subType: cmd.cut_type,
|
||||
consumedEdgeId: cmd.edge_id,
|
||||
edgeIds: [],
|
||||
surfaceId: '',
|
||||
codeRef: { range, pathToNode },
|
||||
},
|
||||
})
|
||||
const consumedEdge = getArtifact(cmd.edge_id)
|
||||
if (consumedEdge?.type === 'segment') {
|
||||
returnArr.push({
|
||||
id: cmd.edge_id,
|
||||
artifact: { ...consumedEdge, edgeCutId: id },
|
||||
})
|
||||
}
|
||||
return returnArr
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
/** filter map items of a specific type */
|
||||
export function filterArtifacts<T extends Artifact['type'][]>(
|
||||
{
|
||||
types,
|
||||
predicate,
|
||||
}: {
|
||||
types: T
|
||||
predicate?: (value: Extract<Artifact, { type: T[number] }>) => boolean
|
||||
},
|
||||
map: ArtifactGraph
|
||||
) {
|
||||
return new Map(
|
||||
Array.from(map).filter(
|
||||
([_, value]) =>
|
||||
types.includes(value.type) &&
|
||||
(!predicate ||
|
||||
predicate(value as Extract<Artifact, { type: T[number] }>))
|
||||
)
|
||||
) as Map<string, Extract<Artifact, { type: T[number] }>>
|
||||
}
|
||||
|
||||
export function getArtifactsOfTypes<T extends Artifact['type'][]>(
|
||||
{
|
||||
keys,
|
||||
types,
|
||||
predicate,
|
||||
}: {
|
||||
keys: string[]
|
||||
types: T
|
||||
predicate?: (value: Extract<Artifact, { type: T[number] }>) => boolean
|
||||
},
|
||||
map: ArtifactGraph
|
||||
): Map<string, Extract<Artifact, { type: T[number] }>> {
|
||||
return new Map(
|
||||
[...map].filter(
|
||||
([key, value]) =>
|
||||
keys.includes(key) &&
|
||||
types.includes(value.type) &&
|
||||
(!predicate ||
|
||||
predicate(value as Extract<Artifact, { type: T[number] }>))
|
||||
)
|
||||
) as Map<string, Extract<Artifact, { type: T[number] }>>
|
||||
}
|
||||
|
||||
export function getArtifactOfTypes<T extends Artifact['type'][]>(
|
||||
{
|
||||
key,
|
||||
types,
|
||||
}: {
|
||||
key: string
|
||||
types: T
|
||||
},
|
||||
map: ArtifactGraph
|
||||
): Extract<Artifact, { type: T[number] }> | Error {
|
||||
const artifact = map.get(key)
|
||||
if (!artifact) return new Error(`No artifact found with key ${key}`)
|
||||
if (!types.includes(artifact?.type))
|
||||
return new Error(`Expected ${types} but got ${artifact?.type}`)
|
||||
return artifact as Extract<Artifact, { type: T[number] }>
|
||||
}
|
||||
|
||||
export function expandPlane(
|
||||
plane: PlaneArtifact,
|
||||
artifactGraph: ArtifactGraph
|
||||
): PlaneArtifactRich {
|
||||
const paths = getArtifactsOfTypes(
|
||||
{ keys: plane.pathIds, types: ['path'] },
|
||||
artifactGraph
|
||||
)
|
||||
return {
|
||||
type: 'plane',
|
||||
paths: Array.from(paths.values()),
|
||||
codeRef: plane.codeRef,
|
||||
}
|
||||
}
|
||||
|
||||
export function expandPath(
|
||||
path: PathArtifact,
|
||||
artifactGraph: ArtifactGraph
|
||||
): PathArtifactRich | Error {
|
||||
const segs = getArtifactsOfTypes(
|
||||
{ keys: path.segIds, types: ['segment'] },
|
||||
artifactGraph
|
||||
)
|
||||
const extrusion = getArtifactOfTypes(
|
||||
{
|
||||
key: path.extrusionId,
|
||||
types: ['extrusion'],
|
||||
},
|
||||
artifactGraph
|
||||
)
|
||||
const plane = getArtifactOfTypes(
|
||||
{ key: path.planeId, types: ['plane', 'wall'] },
|
||||
artifactGraph
|
||||
)
|
||||
if (err(extrusion)) return extrusion
|
||||
if (err(plane)) return plane
|
||||
return {
|
||||
type: 'path',
|
||||
segments: Array.from(segs.values()),
|
||||
extrusion,
|
||||
plane,
|
||||
codeRef: path.codeRef,
|
||||
}
|
||||
}
|
||||
|
||||
export function expandExtrusion(
|
||||
extrusion: ExtrusionArtifact,
|
||||
artifactGraph: ArtifactGraph
|
||||
): ExtrusionArtifactRich | Error {
|
||||
const surfs = getArtifactsOfTypes(
|
||||
{ keys: extrusion.surfaceIds, types: ['wall', 'cap'] },
|
||||
artifactGraph
|
||||
)
|
||||
const edges = getArtifactsOfTypes(
|
||||
{ keys: extrusion.edgeIds, types: ['extrudeEdge'] },
|
||||
artifactGraph
|
||||
)
|
||||
const path = getArtifactOfTypes(
|
||||
{ key: extrusion.pathId, types: ['path'] },
|
||||
artifactGraph
|
||||
)
|
||||
if (err(path)) return path
|
||||
return {
|
||||
type: 'extrusion',
|
||||
surfaces: Array.from(surfs.values()),
|
||||
edges: Array.from(edges.values()),
|
||||
path,
|
||||
codeRef: extrusion.codeRef,
|
||||
}
|
||||
}
|
||||
|
||||
export function expandSegment(
|
||||
segment: SegmentArtifact,
|
||||
artifactGraph: ArtifactGraph
|
||||
): SegmentArtifactRich | Error {
|
||||
const path = getArtifactOfTypes(
|
||||
{ key: segment.pathId, types: ['path'] },
|
||||
artifactGraph
|
||||
)
|
||||
const surf = getArtifactOfTypes(
|
||||
{ key: segment.surfaceId, types: ['wall'] },
|
||||
artifactGraph
|
||||
)
|
||||
const edges = getArtifactsOfTypes(
|
||||
{ keys: segment.edgeIds, types: ['extrudeEdge'] },
|
||||
artifactGraph
|
||||
)
|
||||
const edgeCut = segment.edgeCutId
|
||||
? getArtifactOfTypes(
|
||||
{ key: segment.edgeCutId, types: ['edgeCut'] },
|
||||
artifactGraph
|
||||
)
|
||||
: undefined
|
||||
if (err(path)) return path
|
||||
if (err(surf)) return surf
|
||||
if (err(edgeCut)) return edgeCut
|
||||
|
||||
return {
|
||||
type: 'segment',
|
||||
path,
|
||||
surf,
|
||||
edges: Array.from(edges.values()),
|
||||
edgeCut: edgeCut,
|
||||
codeRef: segment.codeRef,
|
||||
}
|
||||
}
|
||||
|
||||
export function getCapCodeRef(
|
||||
cap: CapArtifact,
|
||||
artifactGraph: ArtifactGraph
|
||||
): CommonCommandProperties | Error {
|
||||
const extrusion = getArtifactOfTypes(
|
||||
{ key: cap.extrusionId, types: ['extrusion'] },
|
||||
artifactGraph
|
||||
)
|
||||
if (err(extrusion)) return extrusion
|
||||
const path = getArtifactOfTypes(
|
||||
{ key: extrusion.pathId, types: ['path'] },
|
||||
artifactGraph
|
||||
)
|
||||
if (err(path)) return path
|
||||
return path.codeRef
|
||||
}
|
||||
|
||||
export function getSolid2dCodeRef(
|
||||
solid2D: solid2D,
|
||||
artifactGraph: ArtifactGraph
|
||||
): CommonCommandProperties | Error {
|
||||
const path = getArtifactOfTypes(
|
||||
{ key: solid2D.pathId, types: ['path'] },
|
||||
artifactGraph
|
||||
)
|
||||
if (err(path)) return path
|
||||
return path.codeRef
|
||||
}
|
||||
|
||||
export function getWallCodeRef(
|
||||
wall: WallArtifact,
|
||||
artifactGraph: ArtifactGraph
|
||||
): CommonCommandProperties | Error {
|
||||
const seg = getArtifactOfTypes(
|
||||
{ key: wall.segId, types: ['segment'] },
|
||||
artifactGraph
|
||||
)
|
||||
if (err(seg)) return seg
|
||||
return seg.codeRef
|
||||
}
|
||||
|
||||
export function getExtrusionFromSuspectedExtrudeSurface(
|
||||
id: string,
|
||||
artifactGraph: ArtifactGraph
|
||||
): ExtrusionArtifact | Error {
|
||||
const artifact = getArtifactOfTypes(
|
||||
{ key: id, types: ['wall', 'cap'] },
|
||||
artifactGraph
|
||||
)
|
||||
if (err(artifact)) return artifact
|
||||
return getArtifactOfTypes(
|
||||
{ key: artifact.extrusionId, types: ['extrusion'] },
|
||||
artifactGraph
|
||||
)
|
||||
}
|
||||
|
||||
export function getExtrusionFromSuspectedPath(
|
||||
id: string,
|
||||
artifactGraph: ArtifactGraph
|
||||
): ExtrusionArtifact | Error {
|
||||
const path = getArtifactOfTypes({ key: id, types: ['path'] }, artifactGraph)
|
||||
if (err(path)) return path
|
||||
return getArtifactOfTypes(
|
||||
{ key: path.extrusionId, types: ['extrusion'] },
|
||||
artifactGraph
|
||||
)
|
||||
}
|
@ -1,272 +0,0 @@
|
||||
import { PathToNode, Program, SourceRange } from 'lang/wasm'
|
||||
import { Models } from '@kittycad/lib'
|
||||
import { getNodePathFromSourceRange } from 'lang/queryAst'
|
||||
|
||||
interface CommonCommandProperties {
|
||||
range: SourceRange
|
||||
pathToNode: PathToNode
|
||||
}
|
||||
|
||||
interface ExtrudeArtifact extends CommonCommandProperties {
|
||||
type: 'extrude'
|
||||
pathId: string
|
||||
}
|
||||
|
||||
export interface StartPathArtifact extends CommonCommandProperties {
|
||||
type: 'startPath'
|
||||
extrusionIds: string[]
|
||||
}
|
||||
|
||||
export interface SegmentArtifact extends CommonCommandProperties {
|
||||
type: 'segment'
|
||||
subType: 'segment' | 'closeSegment'
|
||||
pathId: string
|
||||
}
|
||||
|
||||
interface ExtrudeCapArtifact extends CommonCommandProperties {
|
||||
type: 'extrudeCap'
|
||||
cap: 'start' | 'end'
|
||||
pathId: string
|
||||
}
|
||||
interface ExtrudeWallArtifact extends CommonCommandProperties {
|
||||
type: 'extrudeWall'
|
||||
pathId: string
|
||||
}
|
||||
|
||||
interface PatternInstance extends CommonCommandProperties {
|
||||
type: 'patternInstance'
|
||||
}
|
||||
|
||||
export type ArtifactMapCommand =
|
||||
| ExtrudeArtifact
|
||||
| StartPathArtifact
|
||||
| ExtrudeCapArtifact
|
||||
| ExtrudeWallArtifact
|
||||
| SegmentArtifact
|
||||
| PatternInstance
|
||||
|
||||
export type EngineCommand = Models['WebSocketRequest_type']
|
||||
|
||||
type OkWebSocketResponseData = Models['OkWebSocketResponseData_type']
|
||||
|
||||
/**
|
||||
* The ArtifactMap is a client-side representation of the artifacts that
|
||||
* have been sent to the server-side engine. It is used to keep track of
|
||||
* the state of each command, and to resolve the promise that was returned.
|
||||
* It is also used to keep track of what entities are in the engine scene,
|
||||
* so that we can associate IDs returned from the engine with the
|
||||
* lines of KCL code that generated them.
|
||||
*/
|
||||
export interface ArtifactMap {
|
||||
[commandId: string]: ArtifactMapCommand
|
||||
}
|
||||
|
||||
export interface ResponseMap {
|
||||
[commandId: string]: OkWebSocketResponseData
|
||||
}
|
||||
export interface OrderedCommand {
|
||||
command: EngineCommand
|
||||
range: SourceRange
|
||||
}
|
||||
|
||||
export function createArtifactMap({
|
||||
orderedCommands,
|
||||
responseMap,
|
||||
ast,
|
||||
}: {
|
||||
orderedCommands: Array<OrderedCommand>
|
||||
responseMap: ResponseMap
|
||||
ast: Program
|
||||
}): ArtifactMap {
|
||||
const artifactMap: ArtifactMap = {}
|
||||
orderedCommands.forEach(({ command, range }) => {
|
||||
// expect all to be `modeling_cmd_req` as batch commands have
|
||||
// already been expanded before being added to orderedCommands
|
||||
if (command.type !== 'modeling_cmd_req') return
|
||||
const id = command.cmd_id
|
||||
const response = responseMap[id]
|
||||
const artifacts = handleIndividualResponse({
|
||||
id,
|
||||
pendingMsg: {
|
||||
command,
|
||||
range,
|
||||
},
|
||||
response,
|
||||
ast,
|
||||
prevArtifactMap: artifactMap,
|
||||
})
|
||||
artifacts.forEach(({ commandId, artifact }) => {
|
||||
artifactMap[commandId] = artifact
|
||||
})
|
||||
})
|
||||
return artifactMap
|
||||
}
|
||||
|
||||
function handleIndividualResponse({
|
||||
id,
|
||||
pendingMsg,
|
||||
response,
|
||||
ast,
|
||||
prevArtifactMap,
|
||||
}: {
|
||||
id: string
|
||||
pendingMsg: {
|
||||
command: EngineCommand
|
||||
range: SourceRange
|
||||
}
|
||||
response: OkWebSocketResponseData
|
||||
ast: Program
|
||||
prevArtifactMap: ArtifactMap
|
||||
}): Array<{
|
||||
commandId: string
|
||||
artifact: ArtifactMapCommand
|
||||
}> {
|
||||
const command = pendingMsg
|
||||
if (command?.command?.type !== 'modeling_cmd_req') return []
|
||||
if (response?.type !== 'modeling') return []
|
||||
const command2 = command.command.cmd
|
||||
|
||||
const range = command.range
|
||||
const pathToNode = getNodePathFromSourceRange(ast, range)
|
||||
const modelingResponse = response.data.modeling_response
|
||||
|
||||
const artifacts: Array<{
|
||||
commandId: string
|
||||
artifact: ArtifactMapCommand
|
||||
}> = []
|
||||
|
||||
if (command) {
|
||||
if (
|
||||
command2.type !== 'extrude' &&
|
||||
command2.type !== 'extend_path' &&
|
||||
command2.type !== 'solid3d_get_extrusion_face_info' &&
|
||||
command2.type !== 'start_path' &&
|
||||
command2.type !== 'close_path'
|
||||
) {
|
||||
}
|
||||
if (command2.type === 'extrude') {
|
||||
artifacts.push({
|
||||
commandId: id,
|
||||
artifact: {
|
||||
type: 'extrude',
|
||||
range,
|
||||
pathToNode,
|
||||
pathId: command2.target,
|
||||
},
|
||||
})
|
||||
|
||||
const targetArtifact = { ...prevArtifactMap[command2.target] }
|
||||
if (targetArtifact?.type === 'startPath') {
|
||||
artifacts.push({
|
||||
commandId: command2.target,
|
||||
artifact: {
|
||||
...targetArtifact,
|
||||
type: 'startPath',
|
||||
range: targetArtifact.range,
|
||||
pathToNode: targetArtifact.pathToNode,
|
||||
extrusionIds: targetArtifact?.extrusionIds
|
||||
? [...targetArtifact?.extrusionIds, id]
|
||||
: [id],
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
if (command2.type === 'extend_path') {
|
||||
artifacts.push({
|
||||
commandId: id,
|
||||
artifact: {
|
||||
type: 'segment',
|
||||
subType: 'segment',
|
||||
range,
|
||||
pathToNode,
|
||||
pathId: command2.path,
|
||||
},
|
||||
})
|
||||
}
|
||||
if (command2.type === 'close_path')
|
||||
artifacts.push({
|
||||
commandId: id,
|
||||
artifact: {
|
||||
type: 'segment',
|
||||
subType: 'closeSegment',
|
||||
range,
|
||||
pathToNode,
|
||||
pathId: command2.path_id,
|
||||
},
|
||||
})
|
||||
if (command2.type === 'start_path') {
|
||||
artifacts.push({
|
||||
commandId: id,
|
||||
artifact: {
|
||||
type: 'startPath',
|
||||
range,
|
||||
pathToNode,
|
||||
extrusionIds: [],
|
||||
},
|
||||
})
|
||||
}
|
||||
if (
|
||||
(command2.type === 'entity_linear_pattern' &&
|
||||
modelingResponse.type === 'entity_linear_pattern') ||
|
||||
(command2.type === 'entity_circular_pattern' &&
|
||||
modelingResponse.type === 'entity_circular_pattern')
|
||||
) {
|
||||
// TODO this is not working perfectly, maybe it's like a selection filter issue
|
||||
// but when clicking on a instance it does put the cursor somewhat relevant but
|
||||
// edges and what not do not highlight the correct segment.
|
||||
const entities = modelingResponse.data.entity_ids
|
||||
entities?.forEach((entity: string) => {
|
||||
artifacts.push({
|
||||
commandId: entity,
|
||||
artifact: {
|
||||
range: range,
|
||||
pathToNode,
|
||||
type: 'patternInstance',
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
if (
|
||||
command2.type === 'solid3d_get_extrusion_face_info' &&
|
||||
modelingResponse.type === 'solid3d_get_extrusion_face_info'
|
||||
) {
|
||||
const edgeArtifact = prevArtifactMap[command2.edge_id]
|
||||
const parent =
|
||||
edgeArtifact?.type === 'segment'
|
||||
? prevArtifactMap[edgeArtifact.pathId]
|
||||
: null
|
||||
modelingResponse.data.faces.forEach((face) => {
|
||||
if (
|
||||
face.cap !== 'none' &&
|
||||
face.face_id &&
|
||||
parent?.type === 'startPath'
|
||||
) {
|
||||
artifacts.push({
|
||||
commandId: face.face_id,
|
||||
artifact: {
|
||||
type: 'extrudeCap',
|
||||
cap: face.cap === 'bottom' ? 'start' : 'end',
|
||||
range: parent.range,
|
||||
pathToNode: parent.pathToNode,
|
||||
pathId:
|
||||
edgeArtifact?.type === 'segment' ? edgeArtifact.pathId : '',
|
||||
},
|
||||
})
|
||||
}
|
||||
const curveArtifact = prevArtifactMap[face?.curve_id || '']
|
||||
if (curveArtifact?.type === 'segment' && face?.face_id) {
|
||||
artifacts.push({
|
||||
commandId: face.face_id,
|
||||
artifact: {
|
||||
type: 'extrudeWall',
|
||||
range: curveArtifact.range,
|
||||
pathToNode: curveArtifact.pathToNode,
|
||||
pathId: curveArtifact.pathId,
|
||||
},
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
return artifacts
|
||||
}
|
BIN
src/lang/std/artifactMapGraphs/demoGeometry.png
Normal file
BIN
src/lang/std/artifactMapGraphs/demoGeometry.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 222 KiB |
BIN
src/lang/std/artifactMapGraphs/exampleCode1.png
Normal file
BIN
src/lang/std/artifactMapGraphs/exampleCode1.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 182 KiB |
BIN
src/lang/std/artifactMapGraphs/grokable-graph.png
Normal file
BIN
src/lang/std/artifactMapGraphs/grokable-graph.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 110 KiB |
BIN
src/lang/std/artifactMapGraphs/sketchOnFaceOnFaceEtc.png
Normal file
BIN
src/lang/std/artifactMapGraphs/sketchOnFaceOnFaceEtc.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 373 KiB |
@ -6,12 +6,12 @@ import { deferExecution, uuidv4 } from 'lib/utils'
|
||||
import { Themes, getThemeColorForEngine, getOppositeTheme } from 'lib/theme'
|
||||
import { DefaultPlanes } from 'wasm-lib/kcl/bindings/DefaultPlanes'
|
||||
import {
|
||||
ArtifactMap,
|
||||
ArtifactGraph,
|
||||
EngineCommand,
|
||||
OrderedCommand,
|
||||
ResponseMap,
|
||||
createArtifactMap,
|
||||
} from 'lang/std/artifactMap'
|
||||
createArtifactGraph,
|
||||
} from 'lang/std/artifactGraph'
|
||||
import { useModelingContext } from 'hooks/useModelingContext'
|
||||
|
||||
// TODO(paultag): This ought to be tweakable.
|
||||
@ -286,8 +286,6 @@ class EngineConnection extends EventTarget {
|
||||
)
|
||||
}
|
||||
|
||||
private failedConnTimeout: IsomorphicTimeout | null
|
||||
|
||||
readonly url: string
|
||||
private readonly token?: string
|
||||
|
||||
@ -312,7 +310,6 @@ class EngineConnection extends EventTarget {
|
||||
this.engineCommandManager = engineCommandManager
|
||||
this.url = url
|
||||
this.token = token
|
||||
this.failedConnTimeout = null
|
||||
|
||||
this.pingPongSpan = { ping: undefined, pong: undefined }
|
||||
|
||||
@ -451,9 +448,11 @@ class EngineConnection extends EventTarget {
|
||||
}
|
||||
|
||||
const createPeerConnection = () => {
|
||||
this.pc = new RTCPeerConnection({
|
||||
bundlePolicy: 'max-bundle',
|
||||
})
|
||||
if (!this.engineCommandManager.disableWebRTC) {
|
||||
this.pc = new RTCPeerConnection({
|
||||
bundlePolicy: 'max-bundle',
|
||||
})
|
||||
}
|
||||
|
||||
// Other parts of the application expect pc to be initialized when firing.
|
||||
this.dispatchEvent(
|
||||
@ -465,7 +464,7 @@ class EngineConnection extends EventTarget {
|
||||
// Data channels MUST BE specified before SDP offers because requesting
|
||||
// them affects what our needs are!
|
||||
const DATACHANNEL_NAME_UMC = 'unreliable_modeling_cmds'
|
||||
this.pc.createDataChannel(DATACHANNEL_NAME_UMC)
|
||||
this.pc?.createDataChannel?.(DATACHANNEL_NAME_UMC)
|
||||
|
||||
this.state = {
|
||||
type: EngineConnectionStateType.Connecting,
|
||||
@ -498,7 +497,7 @@ class EngineConnection extends EventTarget {
|
||||
},
|
||||
})
|
||||
}
|
||||
this.pc.addEventListener('icecandidate', this.onIceCandidate)
|
||||
this.pc?.addEventListener?.('icecandidate', this.onIceCandidate)
|
||||
|
||||
this.onIceCandidateError = (_event: Event) => {
|
||||
const event = _event as RTCPeerConnectionIceErrorEvent
|
||||
@ -506,7 +505,7 @@ class EngineConnection extends EventTarget {
|
||||
`ICE candidate returned an error: ${event.errorCode}: ${event.errorText} for ${event.url}`
|
||||
)
|
||||
}
|
||||
this.pc.addEventListener('icecandidateerror', this.onIceCandidateError)
|
||||
this.pc?.addEventListener?.('icecandidateerror', this.onIceCandidateError)
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/connectionstatechange_event
|
||||
// Event type: generic Event type...
|
||||
@ -540,7 +539,7 @@ class EngineConnection extends EventTarget {
|
||||
break
|
||||
}
|
||||
}
|
||||
this.pc.addEventListener(
|
||||
this.pc?.addEventListener?.(
|
||||
'connectionstatechange',
|
||||
this.onConnectionStateChange
|
||||
)
|
||||
@ -630,7 +629,7 @@ class EngineConnection extends EventTarget {
|
||||
|
||||
this.mediaStream = mediaStream
|
||||
}
|
||||
this.pc.addEventListener('track', this.onTrack)
|
||||
this.pc?.addEventListener?.('track', this.onTrack)
|
||||
|
||||
this.onDataChannel = (event) => {
|
||||
this.unreliableDataChannel = event.channel
|
||||
@ -721,7 +720,7 @@ class EngineConnection extends EventTarget {
|
||||
this.onDataChannelMessage
|
||||
)
|
||||
}
|
||||
this.pc.addEventListener('datachannel', this.onDataChannel)
|
||||
this.pc?.addEventListener?.('datachannel', this.onDataChannel)
|
||||
}
|
||||
|
||||
const createWebSocketConnection = () => {
|
||||
@ -756,6 +755,11 @@ class EngineConnection extends EventTarget {
|
||||
// Send an initial ping
|
||||
this.send({ type: 'ping' })
|
||||
this.pingPongSpan.ping = new Date()
|
||||
if (this.engineCommandManager.disableWebRTC) {
|
||||
this.engineCommandManager
|
||||
.initPlanes()
|
||||
.then(() => this.engineCommandManager.resolveReady())
|
||||
}
|
||||
}
|
||||
this.websocket.addEventListener('open', this.onWebSocketOpen)
|
||||
|
||||
@ -803,7 +807,7 @@ class EngineConnection extends EventTarget {
|
||||
.join('\n')
|
||||
if (message.request_id) {
|
||||
const artifactThatFailed =
|
||||
this.engineCommandManager.artifactMap[message.request_id]
|
||||
this.engineCommandManager.artifactGraph.get(message.request_id)
|
||||
console.error(
|
||||
`Error in response to request ${message.request_id}:\n${errorsString}
|
||||
failed cmd type was ${artifactThatFailed?.type}`
|
||||
@ -1089,8 +1093,10 @@ export enum EngineCommandManagerEvents {
|
||||
* of those commands. It also sets up and tears down the connection to the Engine
|
||||
* through the {@link EngineConnection} class.
|
||||
*
|
||||
* It also maintains an {@link artifactMap} that keeps track of the state of each
|
||||
* command, and the artifacts that have been generated by those commands.
|
||||
* As commands are send their state is tracked in {@link pendingCommands} and clear as soon as we receive a response.
|
||||
*
|
||||
* Also all commands that are sent are kept track of in {@link orderedCommands} and their responses are kept in {@link responseMap}
|
||||
* Both of these data structures are used to process the {@link artifactGraph}.
|
||||
*/
|
||||
|
||||
interface PendingMessage {
|
||||
@ -1103,17 +1109,10 @@ interface PendingMessage {
|
||||
}
|
||||
export class EngineCommandManager extends EventTarget {
|
||||
/**
|
||||
* The artifactMap is a client-side representation of the commands that have been sent
|
||||
* to the server-side geometry engine, and the state of their resulting artifacts.
|
||||
*
|
||||
* It is used to keep track of the state of each command, which can fail, succeed, or be
|
||||
* pending.
|
||||
*
|
||||
* It is also used to keep track of our client's understanding of what is in the engine scene
|
||||
* so that we can map to and from KCL code. Each artifact maintains a source range to the part
|
||||
* of the KCL code that generated it.
|
||||
* The artifactGraph is a client-side representation of the commands that have been sent
|
||||
* see: src/lang/std/artifactGraph-README.md for a full explanation.
|
||||
*/
|
||||
artifactMap: ArtifactMap = {}
|
||||
artifactGraph: ArtifactGraph = new Map()
|
||||
/**
|
||||
* The pendingCommands object is a map of the commands that have been sent to the engine that are still waiting on a reply
|
||||
*/
|
||||
@ -1122,21 +1121,14 @@ export class EngineCommandManager extends EventTarget {
|
||||
} = {}
|
||||
/**
|
||||
* The orderedCommands array of all the the commands sent to the engine, un-folded from batches, and made into one long
|
||||
* list of the individual commands, this is used to process all the commands into the artifactMap
|
||||
* list of the individual commands, this is used to process all the commands into the artifactGraph
|
||||
*/
|
||||
orderedCommands: Array<OrderedCommand> = []
|
||||
/**
|
||||
* A map of the responses to the @this.orderedCommands, when processing the commands into the artifactMap, this response map allow
|
||||
* A map of the responses to the {@link orderedCommands}, when processing the commands into the artifactGraph, this response map allow
|
||||
* us to look up the response by command id
|
||||
*/
|
||||
responseMap: ResponseMap = {}
|
||||
/**
|
||||
* The client-side representation of the scene command artifacts that have been sent to the server;
|
||||
* that is, the *non-modeling* commands and corresponding artifacts.
|
||||
*
|
||||
* For modeling commands, see {@link artifactMap}.
|
||||
*/
|
||||
sceneCommandArtifacts: ArtifactMap = {}
|
||||
/**
|
||||
* A counter that is incremented with each command sent over the *unreliable* channel to the engine.
|
||||
* This is compared to the latest received {@link inSequence} number to determine if we should ignore
|
||||
@ -1158,7 +1150,7 @@ export class EngineCommandManager extends EventTarget {
|
||||
reject: (reason: any) => void
|
||||
}
|
||||
_commandLogCallBack: (command: CommandLog[]) => void = () => {}
|
||||
private resolveReady = () => {}
|
||||
resolveReady = () => {}
|
||||
/** Folks should realize that wait for ready does not get called _everytime_
|
||||
* the connection resets and restarts, it only gets called the first time.
|
||||
*
|
||||
@ -1205,11 +1197,12 @@ export class EngineCommandManager extends EventTarget {
|
||||
private onEngineConnectionNewTrack = ({
|
||||
detail,
|
||||
}: CustomEvent<NewTrackArgs>) => {}
|
||||
disableWebRTC = false
|
||||
modelingSend: ReturnType<typeof useModelingContext>['send'] =
|
||||
(() => {}) as any
|
||||
|
||||
start({
|
||||
restart,
|
||||
disableWebRTC = false,
|
||||
setMediaStream,
|
||||
setIsStreamReady,
|
||||
width,
|
||||
@ -1225,7 +1218,7 @@ export class EngineCommandManager extends EventTarget {
|
||||
showScaleGrid: false,
|
||||
},
|
||||
}: {
|
||||
restart?: boolean
|
||||
disableWebRTC?: boolean
|
||||
setMediaStream: (stream: MediaStream) => void
|
||||
setIsStreamReady: (isStreamReady: boolean) => void
|
||||
width: number
|
||||
@ -1242,6 +1235,7 @@ export class EngineCommandManager extends EventTarget {
|
||||
}
|
||||
}) {
|
||||
this.makeDefaultPlanes = makeDefaultPlanes
|
||||
this.disableWebRTC = disableWebRTC
|
||||
this.modifyGrid = modifyGrid
|
||||
if (width === 0 || height === 0) {
|
||||
return
|
||||
@ -1720,15 +1714,11 @@ export class EngineCommandManager extends EventTarget {
|
||||
if (this.engineConnection === undefined) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
if (!this.engineConnection?.isReady()) {
|
||||
if (!this.engineConnection?.isReady() && !this.disableWebRTC)
|
||||
return Promise.resolve()
|
||||
}
|
||||
if (id === undefined) {
|
||||
return Promise.reject(new Error('id is undefined'))
|
||||
}
|
||||
if (rangeStr === undefined) {
|
||||
if (id === undefined) return Promise.reject(new Error('id is undefined'))
|
||||
if (rangeStr === undefined)
|
||||
return Promise.reject(new Error('rangeStr is undefined'))
|
||||
}
|
||||
if (commandStr === undefined) {
|
||||
return Promise.reject(new Error('commandStr is undefined'))
|
||||
}
|
||||
@ -1800,18 +1790,18 @@ export class EngineCommandManager extends EventTarget {
|
||||
*/
|
||||
async waitForAllCommands() {
|
||||
await Promise.all(Object.values(this.pendingCommands).map((a) => a.promise))
|
||||
this.artifactMap = createArtifactMap({
|
||||
this.artifactGraph = createArtifactGraph({
|
||||
orderedCommands: this.orderedCommands,
|
||||
responseMap: this.responseMap,
|
||||
ast: this.getAst(),
|
||||
})
|
||||
if (Object.values(this.artifactMap).length) {
|
||||
if (this.artifactGraph.size) {
|
||||
this.deferredArtifactEmptied(null)
|
||||
} else {
|
||||
this.deferredArtifactPopulated(null)
|
||||
}
|
||||
}
|
||||
private async initPlanes() {
|
||||
async initPlanes() {
|
||||
if (this.planesInitialized()) return
|
||||
const planes = await this.makeDefaultPlanes()
|
||||
this.defaultPlanes = planes
|
||||
@ -1851,7 +1841,7 @@ export class EngineCommandManager extends EventTarget {
|
||||
range: SourceRange,
|
||||
commandTypeToTarget: string
|
||||
): string | undefined {
|
||||
const values = Object.entries(this.artifactMap)
|
||||
const values = Object.entries(this.artifactGraph)
|
||||
for (const [id, data] of values) {
|
||||
// // Our range selection seems to just select the cursor position, so either
|
||||
// // of these can be right...
|
||||
|
@ -1,12 +1,7 @@
|
||||
import { Selections } from 'lib/selections'
|
||||
import { Program, PathToNode } from './wasm'
|
||||
import { getNodeFromPath } from './queryAst'
|
||||
import {
|
||||
ArtifactMap,
|
||||
ArtifactMapCommand,
|
||||
SegmentArtifact,
|
||||
StartPathArtifact,
|
||||
} from 'lang/std/artifactMap'
|
||||
import { ArtifactGraph, filterArtifacts } from 'lang/std/artifactGraph'
|
||||
import { isOverlap } from 'lib/utils'
|
||||
import { err } from 'lib/trap'
|
||||
|
||||
@ -51,25 +46,29 @@ export function updatePathToNodeFromMap(
|
||||
}
|
||||
|
||||
export function isCursorInSketchCommandRange(
|
||||
artifactMap: ArtifactMap,
|
||||
artifactGraph: ArtifactGraph,
|
||||
selectionRanges: Selections
|
||||
): string | false {
|
||||
const overlappingEntries = Object.entries(artifactMap).filter(
|
||||
([id, artifact]: [string, ArtifactMapCommand]) =>
|
||||
selectionRanges.codeBasedSelections.some(
|
||||
(selection) =>
|
||||
Array.isArray(selection?.range) &&
|
||||
Array.isArray(artifact?.range) &&
|
||||
isOverlap(selection.range, artifact.range) &&
|
||||
(artifact.type === 'startPath' || artifact.type === 'segment')
|
||||
)
|
||||
) as [string, StartPathArtifact | SegmentArtifact][]
|
||||
const secondEntry = overlappingEntries?.[0]?.[1]
|
||||
const parentId = secondEntry?.type === 'segment' ? secondEntry.pathId : false
|
||||
let result = parentId
|
||||
const overlappingEntries = filterArtifacts(
|
||||
{
|
||||
types: ['segment', 'path'],
|
||||
predicate: (artifact) => {
|
||||
return selectionRanges.codeBasedSelections.some(
|
||||
(selection) =>
|
||||
Array.isArray(selection?.range) &&
|
||||
Array.isArray(artifact?.codeRef?.range) &&
|
||||
isOverlap(selection.range, artifact.codeRef.range)
|
||||
)
|
||||
},
|
||||
},
|
||||
artifactGraph
|
||||
)
|
||||
const firstEntry = [...overlappingEntries.values()]?.[0]
|
||||
const parentId = firstEntry?.type === 'segment' ? firstEntry.pathId : false
|
||||
|
||||
return parentId
|
||||
? parentId
|
||||
: overlappingEntries.find(
|
||||
([, artifact]) => artifact.type === 'startPath'
|
||||
: [...overlappingEntries].find(
|
||||
([, artifact]) => artifact.type === 'path'
|
||||
)?.[0] || false
|
||||
return result
|
||||
}
|
||||
|
@ -202,7 +202,8 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
|
||||
'default',
|
||||
'line-end',
|
||||
'line-mid',
|
||||
'extrude-wall', // to fix: accespts only this selection type
|
||||
'extrude-wall', // to fix: accepts only this selection type
|
||||
'solid2D',
|
||||
'start-cap',
|
||||
'end-cap',
|
||||
'point',
|
||||
|
@ -192,14 +192,14 @@ export class CoreDumpManager {
|
||||
// engine_command_manager
|
||||
debugLog('CoreDump: engineCommandManager', this.engineCommandManager)
|
||||
|
||||
// artifact map - this.engineCommandManager.artifactMap
|
||||
if (this.engineCommandManager?.artifactMap) {
|
||||
// artifact map - this.engineCommandManager.artifactGraph
|
||||
if (this.engineCommandManager?.artifactGraph) {
|
||||
debugLog(
|
||||
'CoreDump: Engine Command Manager artifact map',
|
||||
this.engineCommandManager.artifactMap
|
||||
this.engineCommandManager.artifactGraph
|
||||
)
|
||||
clientState.engine_command_manager.artifact_map = structuredClone(
|
||||
this.engineCommandManager.artifactMap
|
||||
this.engineCommandManager.artifactGraph
|
||||
)
|
||||
}
|
||||
|
||||
@ -255,16 +255,6 @@ export class CoreDumpManager {
|
||||
this.engineCommandManager.outSequence
|
||||
}
|
||||
|
||||
// scene command artifacts - this.engineCommandManager.sceneCommandArtifacts
|
||||
if (this.engineCommandManager?.sceneCommandArtifacts) {
|
||||
debugLog(
|
||||
'CoreDump: Engine Command Manager scene command artifacts',
|
||||
this.engineCommandManager.sceneCommandArtifacts
|
||||
)
|
||||
clientState.engine_command_manager.scene_command_artifacts =
|
||||
structuredClone(this.engineCommandManager.sceneCommandArtifacts)
|
||||
}
|
||||
|
||||
// KCL Manager - globalThis?.window?.kclManager
|
||||
const kclManager = (globalThis?.window as any)?.kclManager
|
||||
debugLog('CoreDump: kclManager', kclManager)
|
||||
|
@ -8,7 +8,6 @@ import {
|
||||
createTagDeclarator,
|
||||
createUnaryExpression,
|
||||
} from 'lang/modifyAst'
|
||||
import { roundOff } from './utils'
|
||||
import { ArrayExpression, CallExpression, PipeExpression } from 'lang/wasm'
|
||||
|
||||
/**
|
||||
|
@ -29,6 +29,13 @@ import { Mesh, Object3D, Object3DEventMap } from 'three'
|
||||
import { AXIS_GROUP, X_AXIS } from 'clientSideScene/sceneInfra'
|
||||
import { PathToNodeMap } from 'lang/std/sketchcombos'
|
||||
import { err } from 'lib/trap'
|
||||
import {
|
||||
getArtifactOfTypes,
|
||||
getArtifactsOfTypes,
|
||||
getCapCodeRef,
|
||||
getSolid2dCodeRef,
|
||||
getWallCodeRef,
|
||||
} from 'lang/std/artifactGraph'
|
||||
|
||||
export const X_AXIS_UUID = 'ad792545-7fd3-482a-a602-a93924e3055b'
|
||||
export const Y_AXIS_UUID = '680fd157-266f-4b8a-984f-cdf46b8bdf01'
|
||||
@ -41,6 +48,7 @@ export type Selection = {
|
||||
| 'line-end'
|
||||
| 'line-mid'
|
||||
| 'extrude-wall'
|
||||
| 'solid2D'
|
||||
| 'start-cap'
|
||||
| 'end-cap'
|
||||
| 'point'
|
||||
@ -55,15 +63,12 @@ export type Selections = {
|
||||
codeBasedSelections: Selection[]
|
||||
}
|
||||
|
||||
export async function getEventForSelectWithPoint(
|
||||
{
|
||||
data,
|
||||
}: Extract<
|
||||
Models['OkModelingCmdResponse_type'],
|
||||
{ type: 'select_with_point' }
|
||||
>,
|
||||
{ sketchEnginePathId }: { sketchEnginePathId?: string }
|
||||
): Promise<ModelingMachineEvent | null> {
|
||||
export async function getEventForSelectWithPoint({
|
||||
data,
|
||||
}: Extract<
|
||||
Models['OkModelingCmdResponse_type'],
|
||||
{ type: 'select_with_point' }
|
||||
>): Promise<ModelingMachineEvent | null> {
|
||||
if (!data?.entity_id) {
|
||||
return {
|
||||
type: 'Set selection',
|
||||
@ -79,68 +84,64 @@ export async function getEventForSelectWithPoint(
|
||||
},
|
||||
}
|
||||
}
|
||||
let _artifact = engineCommandManager.artifactMap[data.entity_id]
|
||||
if (!_artifact) {
|
||||
// This logic for getting the parent id is for solid2ds as in edit mode it return the face id
|
||||
// but we don't recognise that in the artifact map because we store the path id when the path is
|
||||
// created, the solid2d is implicitly created with the close stdlib function
|
||||
// there's plans to get the faceId back from the solid2d creation
|
||||
// https://github.com/KittyCAD/engine/issues/2094
|
||||
// at which point we can add it to the artifact map and remove this logic
|
||||
const resp = await engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd: {
|
||||
type: 'entity_get_parent_id',
|
||||
entity_id: data.entity_id,
|
||||
},
|
||||
cmd_id: uuidv4(),
|
||||
})
|
||||
const parentId =
|
||||
resp?.success &&
|
||||
resp?.resp?.type === 'modeling' &&
|
||||
resp?.resp?.data?.modeling_response?.type === 'entity_get_parent_id'
|
||||
? resp?.resp?.data?.modeling_response?.data?.entity_id
|
||||
: ''
|
||||
const parentArtifact = engineCommandManager.artifactMap[parentId]
|
||||
if (parentArtifact) {
|
||||
_artifact = parentArtifact
|
||||
}
|
||||
}
|
||||
const sourceRange = _artifact?.range
|
||||
if (_artifact) {
|
||||
if (_artifact.type === 'extrudeCap')
|
||||
return {
|
||||
type: 'Set selection',
|
||||
data: {
|
||||
selectionType: 'singleCodeCursor',
|
||||
selection: {
|
||||
range: sourceRange,
|
||||
type: _artifact?.cap === 'end' ? 'end-cap' : 'start-cap',
|
||||
},
|
||||
},
|
||||
}
|
||||
if (_artifact.type === 'extrudeWall')
|
||||
return {
|
||||
type: 'Set selection',
|
||||
data: {
|
||||
selectionType: 'singleCodeCursor',
|
||||
selection: { range: sourceRange, type: 'extrude-wall' },
|
||||
},
|
||||
}
|
||||
return {
|
||||
type: 'Set selection',
|
||||
data: {
|
||||
selectionType: 'singleCodeCursor',
|
||||
selection: { range: sourceRange, type: 'default' },
|
||||
},
|
||||
}
|
||||
} else {
|
||||
// if we don't recognise the entity, select nothing
|
||||
let _artifact = engineCommandManager.artifactGraph.get(data.entity_id)
|
||||
if (!_artifact)
|
||||
return {
|
||||
type: 'Set selection',
|
||||
data: { selectionType: 'singleCodeCursor' },
|
||||
}
|
||||
if (_artifact.type === 'solid2D') {
|
||||
const codeRef = getSolid2dCodeRef(
|
||||
_artifact,
|
||||
engineCommandManager.artifactGraph
|
||||
)
|
||||
if (err(codeRef)) return null
|
||||
return {
|
||||
type: 'Set selection',
|
||||
data: {
|
||||
selectionType: 'singleCodeCursor',
|
||||
selection: { range: codeRef.range, type: 'solid2D' },
|
||||
},
|
||||
}
|
||||
}
|
||||
if (_artifact.type === 'cap') {
|
||||
const codeRef = getCapCodeRef(_artifact, engineCommandManager.artifactGraph)
|
||||
if (err(codeRef)) return null
|
||||
return {
|
||||
type: 'Set selection',
|
||||
data: {
|
||||
selectionType: 'singleCodeCursor',
|
||||
selection: {
|
||||
range: codeRef.range,
|
||||
type: _artifact?.subType === 'end' ? 'end-cap' : 'start-cap',
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
if (_artifact.type === 'wall') {
|
||||
const codeRef = getWallCodeRef(
|
||||
_artifact,
|
||||
engineCommandManager.artifactGraph
|
||||
)
|
||||
if (err(codeRef)) return null
|
||||
return {
|
||||
type: 'Set selection',
|
||||
data: {
|
||||
selectionType: 'singleCodeCursor',
|
||||
selection: { range: codeRef.range, type: 'extrude-wall' },
|
||||
},
|
||||
}
|
||||
}
|
||||
if (_artifact.type === 'segment' || _artifact.type === 'path') {
|
||||
return {
|
||||
type: 'Set selection',
|
||||
data: {
|
||||
selectionType: 'singleCodeCursor',
|
||||
selection: { range: _artifact.codeRef.range, type: 'default' },
|
||||
},
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export function getEventForSegmentSelection(
|
||||
@ -347,7 +348,7 @@ function resetAndSetEngineEntitySelectionCmds(
|
||||
export function isSketchPipe(selectionRanges: Selections) {
|
||||
if (!isSingleCursorInPipe(selectionRanges, kclManager.ast)) return false
|
||||
return isCursorInSketchCommandRange(
|
||||
engineCommandManager.artifactMap,
|
||||
engineCommandManager.artifactGraph,
|
||||
selectionRanges
|
||||
)
|
||||
}
|
||||
@ -499,11 +500,10 @@ function codeToIdSelections(
|
||||
return codeBasedSelections
|
||||
.flatMap(({ type, range, ...rest }): null | SelectionToEngine[] => {
|
||||
// TODO #868: loops over all artifacts will become inefficient at a large scale
|
||||
const entriesWithOverlap = Object.entries(
|
||||
engineCommandManager.artifactMap || {}
|
||||
)
|
||||
const overlappingEntries = Array.from(engineCommandManager.artifactGraph)
|
||||
.map(([id, artifact]) => {
|
||||
return artifact.range && isOverlap(artifact.range, range)
|
||||
if (!('codeRef' in artifact)) return false
|
||||
return isOverlap(artifact.codeRef.range, range)
|
||||
? {
|
||||
artifact,
|
||||
selection: { type, range, ...rest },
|
||||
@ -512,31 +512,73 @@ function codeToIdSelections(
|
||||
: false
|
||||
})
|
||||
.filter(Boolean)
|
||||
|
||||
/** TODO refactor
|
||||
* selections in our app is a sourceRange plus some metadata
|
||||
* The metadata is just a union type string of different types of artifacts or 3d features 'extrude-wall' 'segment' etc
|
||||
* Because the source range is not enough to figure out what the user selected, so here we're using filtering through all the artifacts
|
||||
* to find something that matches both the source range and the metadata.
|
||||
*
|
||||
* What we should migrate to is just storing what the user selected by what it matched in the artifactGraph it will simply the below a lot.
|
||||
*
|
||||
* In the case of a user moving the cursor them, we will still need to figure out what artifact from the graph matches best, but we will just need sane defaults
|
||||
* and most of the time we can expect the user to be clicking in the 3d scene instead.
|
||||
*/
|
||||
let bestCandidate
|
||||
entriesWithOverlap.forEach((entry) => {
|
||||
overlappingEntries.forEach((entry) => {
|
||||
if (!entry) return
|
||||
if (type === 'default' && entry.artifact.type === 'segment') {
|
||||
bestCandidate = entry
|
||||
return
|
||||
}
|
||||
if (
|
||||
type === 'start-cap' &&
|
||||
entry.artifact.type === 'extrudeCap' &&
|
||||
entry?.artifact?.cap === 'start'
|
||||
) {
|
||||
bestCandidate = entry
|
||||
if (type === 'solid2D' && entry.artifact.type === 'path') {
|
||||
const solid = engineCommandManager.artifactGraph.get(
|
||||
entry.artifact.solid2dId || ''
|
||||
)
|
||||
if (solid?.type !== 'solid2D') return
|
||||
bestCandidate = {
|
||||
artifact: solid,
|
||||
selection: { type, range, ...rest },
|
||||
id: entry.artifact.solid2dId,
|
||||
}
|
||||
}
|
||||
if (type === 'extrude-wall' && entry.artifact.type === 'segment') {
|
||||
const wall = engineCommandManager.artifactGraph.get(
|
||||
entry.artifact.surfaceId
|
||||
)
|
||||
if (wall?.type !== 'wall') return
|
||||
bestCandidate = {
|
||||
artifact: wall,
|
||||
selection: { type, range, ...rest },
|
||||
id: entry.artifact.surfaceId,
|
||||
}
|
||||
return
|
||||
}
|
||||
if (
|
||||
type === 'end-cap' &&
|
||||
entry.artifact.type === 'extrudeCap' &&
|
||||
entry?.artifact?.cap === 'end'
|
||||
(type === 'end-cap' || type === 'start-cap') &&
|
||||
entry.artifact.type === 'path'
|
||||
) {
|
||||
bestCandidate = entry
|
||||
return
|
||||
}
|
||||
if (type === 'extrude-wall' && entry.artifact.type === 'extrudeWall') {
|
||||
bestCandidate = entry
|
||||
const extrusion = getArtifactOfTypes(
|
||||
{
|
||||
key: entry.artifact.extrusionId,
|
||||
types: ['extrusion'],
|
||||
},
|
||||
engineCommandManager.artifactGraph
|
||||
)
|
||||
if (err(extrusion)) return
|
||||
const caps = getArtifactsOfTypes(
|
||||
{ keys: extrusion.surfaceIds, types: ['cap'] },
|
||||
engineCommandManager.artifactGraph
|
||||
)
|
||||
const cap = [...caps].find(
|
||||
([_, cap]) => cap.subType === (type === 'end-cap' ? 'end' : 'start')
|
||||
)
|
||||
if (!cap) return
|
||||
bestCandidate = {
|
||||
artifact: entry.artifact,
|
||||
selection: { type, range, ...rest },
|
||||
id: cap[0],
|
||||
}
|
||||
return
|
||||
}
|
||||
})
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Program, ProgramMemory, _executor, SourceRange } from '../lang/wasm'
|
||||
import { EngineCommandManager } from 'lang/std/engineConnection'
|
||||
import { EngineCommand } from 'lang/std/artifactMap'
|
||||
import { EngineCommand } from 'lang/std/artifactGraph'
|
||||
import { Models } from '@kittycad/lib'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { DefaultPlanes } from 'wasm-lib/kcl/bindings/DefaultPlanes'
|
||||
|
@ -58,6 +58,7 @@ import { Coords2d } from 'lang/std/sketch'
|
||||
import { deleteSegment } from 'clientSideScene/ClientSideSceneComp'
|
||||
import { executeAst } from 'lang/langHelpers'
|
||||
import toast from 'react-hot-toast'
|
||||
import { getExtrusionFromSuspectedPath } from 'lang/std/artifactGraph'
|
||||
|
||||
export const MODELING_PERSIST_KEY = 'MODELING_PERSIST_KEY'
|
||||
|
||||
@ -1107,7 +1108,7 @@ export const modelingMachine = createMachine(
|
||||
editorManager.selectRange(updatedAst?.selections)
|
||||
}
|
||||
},
|
||||
'AST delete selection': async ({ sketchDetails, selectionRanges }) => {
|
||||
'AST delete selection': async ({ selectionRanges }) => {
|
||||
let ast = kclManager.ast
|
||||
|
||||
const modifiedAst = await deleteFromSelection(
|
||||
@ -1160,17 +1161,13 @@ export const modelingMachine = createMachine(
|
||||
const sketchVar = varDecNode.node.declarations[0].id.name
|
||||
const sketchGroup = kclManager.programMemory.get(sketchVar)
|
||||
if (sketchGroup?.type !== 'SketchGroup') return
|
||||
const idArtifact = engineCommandManager.artifactMap[sketchGroup.id]
|
||||
if (idArtifact.type !== 'startPath') return
|
||||
const extrusionArtifactId = idArtifact?.extrusionIds?.[0]
|
||||
if (typeof extrusionArtifactId !== 'string') return
|
||||
const extrusionArtifact =
|
||||
engineCommandManager.artifactMap[extrusionArtifactId]
|
||||
if (!extrusionArtifact) return
|
||||
const pathToExtrudeNode = getNodePathFromSourceRange(
|
||||
ast,
|
||||
extrusionArtifact.range
|
||||
const extrusion = getExtrusionFromSuspectedPath(
|
||||
sketchGroup.id,
|
||||
engineCommandManager.artifactGraph
|
||||
)
|
||||
const pathToExtrudeNode = err(extrusion)
|
||||
? []
|
||||
: getNodePathFromSourceRange(ast, extrusion.codeRef.range)
|
||||
|
||||
// we assume that there is only one body related to the sketch
|
||||
// and apply the fillet to it
|
||||
|
60
yarn.lock
60
yarn.lock
@ -2270,6 +2270,11 @@
|
||||
dependencies:
|
||||
"@babel/types" "^7.20.7"
|
||||
|
||||
"@types/d3-force@^3.0.10":
|
||||
version "3.0.10"
|
||||
resolved "https://registry.yarnpkg.com/@types/d3-force/-/d3-force-3.0.10.tgz#6dc8fc6e1f35704f3b057090beeeb7ac674bff1a"
|
||||
integrity sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==
|
||||
|
||||
"@types/eslint@^8.4.5":
|
||||
version "8.56.10"
|
||||
resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-8.56.10.tgz#eb2370a73bf04a901eeba8f22595c7ee0f7eb58d"
|
||||
@ -3849,6 +3854,30 @@ csstype@^3.0.2:
|
||||
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81"
|
||||
integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==
|
||||
|
||||
"d3-dispatch@1 - 3":
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/d3-dispatch/-/d3-dispatch-3.0.1.tgz#5fc75284e9c2375c36c839411a0cf550cbfc4d5e"
|
||||
integrity sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==
|
||||
|
||||
d3-force@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/d3-force/-/d3-force-3.0.0.tgz#3e2ba1a61e70888fe3d9194e30d6d14eece155c4"
|
||||
integrity sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==
|
||||
dependencies:
|
||||
d3-dispatch "1 - 3"
|
||||
d3-quadtree "1 - 3"
|
||||
d3-timer "1 - 3"
|
||||
|
||||
"d3-quadtree@1 - 3":
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/d3-quadtree/-/d3-quadtree-3.0.1.tgz#6dca3e8be2b393c9a9d514dabbd80a92deef1a4f"
|
||||
integrity sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==
|
||||
|
||||
"d3-timer@1 - 3":
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-3.0.1.tgz#6284d2a2708285b1abb7e201eda4380af35e63b0"
|
||||
integrity sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==
|
||||
|
||||
damerau-levenshtein@^1.0.8:
|
||||
version "1.0.8"
|
||||
resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz#b43d286ccbd36bc5b2f7ed41caf2d0aba1f8a6e7"
|
||||
@ -7683,16 +7712,7 @@ string-natural-compare@^3.0.1:
|
||||
resolved "https://registry.yarnpkg.com/string-natural-compare/-/string-natural-compare-3.0.1.tgz#7a42d58474454963759e8e8b7ae63d71c1e7fdf4"
|
||||
integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw==
|
||||
|
||||
"string-width-cjs@npm:string-width@^4.2.0":
|
||||
version "4.2.3"
|
||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||
dependencies:
|
||||
emoji-regex "^8.0.0"
|
||||
is-fullwidth-code-point "^3.0.0"
|
||||
strip-ansi "^6.0.1"
|
||||
|
||||
string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
|
||||
"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
|
||||
version "4.2.3"
|
||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||
@ -7770,14 +7790,7 @@ string_decoder@~1.1.1:
|
||||
dependencies:
|
||||
safe-buffer "~5.1.0"
|
||||
|
||||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
|
||||
version "6.0.1"
|
||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||
dependencies:
|
||||
ansi-regex "^5.0.1"
|
||||
|
||||
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
||||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
||||
version "6.0.1"
|
||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||
@ -8653,7 +8666,7 @@ workerpool@6.2.1:
|
||||
resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.1.tgz#46fc150c17d826b86a008e5a4508656777e9c343"
|
||||
integrity sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==
|
||||
|
||||
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
|
||||
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
|
||||
version "7.0.0"
|
||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
|
||||
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
||||
@ -8671,15 +8684,6 @@ wrap-ansi@^6.2.0:
|
||||
string-width "^4.1.0"
|
||||
strip-ansi "^6.0.0"
|
||||
|
||||
wrap-ansi@^7.0.0:
|
||||
version "7.0.0"
|
||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
|
||||
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
||||
dependencies:
|
||||
ansi-styles "^4.0.0"
|
||||
string-width "^4.1.0"
|
||||
strip-ansi "^6.0.0"
|
||||
|
||||
wrap-ansi@^8.1.0:
|
||||
version "8.1.0"
|
||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"
|
||||
|
Reference in New Issue
Block a user