Merge branch 'main' into pierremtb/issue1349

This commit is contained in:
Pierre Jacquier
2024-02-14 07:52:58 -05:00
34 changed files with 2826 additions and 455 deletions

View File

@ -1,3 +1,3 @@
[codespell]
ignore-words-list: crate,everytime,inout,co-ordinate
ignore-words-list: crate,everytime,inout,co-ordinate,ot
skip: **/target,node_modules,build,**/Cargo.lock,./src-tauri/gen/schemas

16
docs/kcl/KNOWN-ISSUES.md Normal file
View File

@ -0,0 +1,16 @@
# Known Issues
The following are bugs that are not in modeling-app or kcl itself. These bugs
once fixed in engine will just start working here with no language changes.
- **Sketch on Face**: If your sketch is outside the edges of the face (on which you
are sketching) you will get multiple models returned instead of one single
model for that sketch and its underlying 3D object.
- **Patterns**: If you try and pass a pattern to `hole` currently only the first
item in the pattern is being subtracted. This is an engine bug that is being
worked on.
- **Import**: Right now you can import a file, even if that file has brep data
you cannot edit it. You also cannot move or transform the imported objects at
all. In the future, after v1, the engine will account for this.

File diff suppressed because it is too large Load Diff

View File

@ -41,6 +41,7 @@
* [`log2`](#log2)
* [`max`](#max)
* [`min`](#min)
* [`patternCircular`](#patternCircular)
* [`patternLinear`](#patternLinear)
* [`pi`](#pi)
* [`pow`](#pow)
@ -3998,6 +3999,194 @@ min(args: [number]) -> number
### patternCircular
A Circular pattern.
```
patternCircular(data: CircularPatternData, geometry: Geometry) -> Geometries
```
#### Arguments
* `data`: `CircularPatternData` - Data for a circular pattern.
```
{
// The arc angle (in degrees) to place the repetitions. Must be greater than 0.
arcDegrees: number,
// The axis around which to make the pattern. This is a 3D vector.
axis: [number, number, number],
// The center about which to make th pattern. This is a 3D vector.
center: [number, number, number],
// The number of repetitions. Must be greater than 0. This excludes the original entity. For example, if `repetitions` is 1, the original entity will be copied once.
repetitions: number,
// Whether or not to rotate the duplicates as they are copied.
rotateDuplicates: string,
}
```
* `geometry`: `Geometry` - A geometry.
```
{
// The plane id or face id of the sketch group.
entityId: uuid,
// The id of the sketch group.
id: uuid,
// The position of the sketch group.
position: [number, number, number],
// The rotation of the sketch group base plane.
rotation: [number, number, number, number],
// The starting path.
start: {
// The from point.
from: [number, number],
// The name of the path.
name: string,
// The to point.
to: [number, number],
},
type: "SketchGroup",
// The paths in the sketch group.
value: [{
// The from point.
from: [number, number],
// The name of the path.
name: string,
// The to point.
to: [number, number],
type: "ToPoint",
} |
{
// arc's direction
ccw: string,
// the arc's center
center: [number, number],
// The from point.
from: [number, number],
// The name of the path.
name: string,
// The to point.
to: [number, number],
type: "TangentialArcTo",
} |
{
// The from point.
from: [number, number],
// The name of the path.
name: string,
// The to point.
to: [number, number],
type: "Horizontal",
// The x coordinate.
x: number,
} |
{
// The from point.
from: [number, number],
// The name of the path.
name: string,
// The to point.
to: [number, number],
type: "AngledLineTo",
// The x coordinate.
x: number,
// The y coordinate.
y: number,
} |
{
// The from point.
from: [number, number],
// The name of the path.
name: string,
// The to point.
to: [number, number],
type: "Base",
}],
// The x-axis of the sketch group base plane in the 3D space
xAxis: {
x: number,
y: number,
z: number,
},
// The y-axis of the sketch group base plane in the 3D space
yAxis: {
x: number,
y: number,
z: number,
},
// The z-axis of the sketch group base plane in the 3D space
zAxis: {
x: number,
y: number,
z: number,
},
} |
{
// The id of the extrusion end cap
endCapId: uuid,
// The height of the extrude group.
height: number,
// The id of the extrude group.
id: uuid,
// The position of the extrude group.
position: [number, number, number],
// The rotation of the extrude group.
rotation: [number, number, number, number],
// The id of the extrusion start cap
startCapId: uuid,
type: "ExtrudeGroup",
// The extrude surfaces.
value: [{
// The face id for the extrude plane.
faceId: uuid,
// The id of the geometry.
id: uuid,
// The name.
name: string,
// The position.
position: [number, number, number],
// The rotation.
rotation: [number, number, number, number],
// The source range.
sourceRange: [number, number],
type: "extrudePlane",
}],
// The x-axis of the extrude group base plane in the 3D space
xAxis: {
x: number,
y: number,
z: number,
},
// The y-axis of the extrude group base plane in the 3D space
yAxis: {
x: number,
y: number,
z: number,
},
// The z-axis of the extrude group base plane in the 3D space
zAxis: {
x: number,
y: number,
z: number,
},
}
```
#### Returns
* `Geometries` - A set of geometry.
```
{
type: "SketchGroups",
} |
{
type: "ExtrudeGroups",
}
```
### patternLinear
A linear pattern.
@ -5149,7 +5338,7 @@ Start a sketch on a specific plane or face.
```
startSketchOn(data: SketchData, tag: String) -> SketchSurface
startSketchOn(data: SketchData, tag: SketchOnFaceTag) -> SketchSurface
```
#### Arguments
@ -5239,7 +5428,11 @@ startSketchOn(data: SketchData, tag: String) -> SketchSurface
},
}
```
* `tag`: `String`
* `tag`: `SketchOnFaceTag` - A tag for sketch on face.
```
"start" | "end" |
string
```
#### Returns

View File

@ -24,8 +24,6 @@ import { type IndexLoaderData } from 'lib/types'
import { paths } from 'lib/paths'
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
import { onboardingPaths } from 'routes/Onboarding/paths'
import { cameraMouseDragGuards } from 'lib/cameraControls'
import { CameraDragInteractionType_type } from '@kittycad/lib/dist/types/src/models'
import { CodeMenu } from 'components/CodeMenu'
import { TextEditor } from 'components/TextEditor'
import { Themes, getSystemTheme } from 'lib/theme'
@ -56,8 +54,7 @@ export function App() {
}))
const { settings } = useGlobalStateContext()
const { showDebugPanel, onboardingStatus, cameraControls, theme } =
settings?.context || {}
const { showDebugPanel, onboardingStatus, theme } = settings?.context || {}
const { state, send } = useModelingContext()
const editorTheme = theme === Themes.System ? getSystemTheme() : theme
@ -119,31 +116,6 @@ export function App() {
},
cmd_id: newCmdId,
})
} else {
const interactionGuards = cameraMouseDragGuards[cameraControls]
let interaction: CameraDragInteractionType_type
const eWithButton = { ...e, button: buttonDownInStream }
if (interactionGuards.pan.callback(eWithButton)) {
interaction = 'pan'
} else if (interactionGuards.rotate.callback(eWithButton)) {
interaction = 'rotate'
} else if (interactionGuards.zoom.dragCallback(eWithButton)) {
interaction = 'zoom'
} else {
return
}
debounceSocketSend({
type: 'modeling_cmd_req',
cmd: {
type: 'camera_drag_move',
interaction,
window: { x, y },
},
cmd_id: newCmdId,
})
}
}

View File

@ -0,0 +1,252 @@
import { useRef, useEffect, useState } from 'react'
import { useModelingContext } from 'hooks/useModelingContext'
import { cameraMouseDragGuards } from 'lib/cameraControls'
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
import { useStore } from 'useStore'
import {
DEBUG_SHOW_BOTH_SCENES,
ReactCameraProperties,
sceneInfra,
} from './sceneInfra'
import { throttle } from 'lib/utils'
function useShouldHideScene(): { hideClient: boolean; hideServer: boolean } {
const [isCamMoving, setIsCamMoving] = useState(false)
const [isTween, setIsTween] = useState(false)
const { state } = useModelingContext()
useEffect(() => {
sceneInfra.setIsCamMovingCallback((isMoving, isTween) => {
setIsCamMoving(isMoving)
setIsTween(isTween)
})
}, [])
if (DEBUG_SHOW_BOTH_SCENES || !isCamMoving)
return { hideClient: false, hideServer: false }
let hideServer = state.matches('Sketch') || state.matches('Sketch no face')
if (isTween) {
hideServer = false
}
return { hideClient: !hideServer, hideServer }
}
export const ClientSideScene = ({
cameraControls,
}: {
cameraControls: ReturnType<
typeof useGlobalStateContext
>['settings']['context']['cameraControls']
}) => {
const canvasRef = useRef<HTMLDivElement>(null)
const { state, send } = useModelingContext()
const { hideClient, hideServer } = useShouldHideScene()
const { setHighlightRange } = useStore((s) => ({
setHighlightRange: s.setHighlightRange,
highlightRange: s.highlightRange,
}))
// Listen for changes to the camera controls setting
// and update the client-side scene's controls accordingly.
useEffect(() => {
sceneInfra.setInteractionGuards(cameraMouseDragGuards[cameraControls])
}, [cameraControls])
useEffect(() => {
sceneInfra.updateOtherSelectionColors(
state?.context?.selectionRanges?.otherSelections || []
)
}, [state?.context?.selectionRanges?.otherSelections])
useEffect(() => {
if (!canvasRef.current) return
const canvas = canvasRef.current
canvas.appendChild(sceneInfra.renderer.domElement)
sceneInfra.animate()
sceneInfra.setHighlightCallback(setHighlightRange)
canvas.addEventListener('mousemove', sceneInfra.onMouseMove, false)
canvas.addEventListener('mousedown', sceneInfra.onMouseDown, false)
canvas.addEventListener('mouseup', sceneInfra.onMouseUp, false)
sceneInfra.setSend(send)
return () => {
canvas?.removeEventListener('mousemove', sceneInfra.onMouseMove)
canvas?.removeEventListener('mousedown', sceneInfra.onMouseDown)
canvas?.removeEventListener('mouseup', sceneInfra.onMouseUp)
}
}, [])
return (
<div
ref={canvasRef}
className={`absolute inset-0 h-full w-full transition-all duration-300 ${
hideClient ? 'opacity-0' : 'opacity-100'
} ${hideServer ? 'bg-black' : ''} ${
!hideClient && !hideServer && state.matches('Sketch')
? 'bg-black/80'
: ''
}`}
></div>
)
}
const throttled = throttle((a: ReactCameraProperties) => {
if (a.type === 'perspective' && a.fov) {
sceneInfra.dollyZoom(a.fov)
}
}, 1000 / 15)
export const CamDebugSettings = () => {
const [camSettings, setCamSettings] = useState<ReactCameraProperties>({
type: 'perspective',
fov: 12,
position: [0, 0, 0],
quaternion: [0, 0, 0, 1],
})
const [fov, setFov] = useState(12)
useEffect(() => {
sceneInfra.setReactCameraPropertiesCallback(setCamSettings)
}, [sceneInfra])
useEffect(() => {
if (camSettings.type === 'perspective' && camSettings.fov) {
setFov(camSettings.fov)
}
}, [(camSettings as any)?.fov])
return (
<div>
<h3>cam settings</h3>
perspective cam
<input
type="checkbox"
checked={camSettings.type === 'perspective'}
onChange={(e) => {
if (camSettings.type === 'perspective') {
sceneInfra.useOrthographicCamera()
} else {
sceneInfra.usePerspectiveCamera()
}
}}
/>
{camSettings.type === 'perspective' && (
<input
type="range"
min="4"
max="90"
step={0.5}
value={fov}
onChange={(e) => {
setFov(parseFloat(e.target.value))
throttled({
...camSettings,
fov: parseFloat(e.target.value),
})
}}
className="w-full cursor-pointer pointer-events-auto"
/>
)}
{camSettings.type === 'perspective' && (
<div>
<span>fov</span>
<input
type="number"
value={camSettings.fov}
className="text-black w-16"
onChange={(e) => {
sceneInfra.setCam({
...camSettings,
fov: parseFloat(e.target.value),
})
}}
/>
</div>
)}
{camSettings.type === 'orthographic' && (
<>
<div>
<span>fov</span>
<input
type="number"
value={camSettings.zoom}
className="text-black w-16"
onChange={(e) => {
sceneInfra.setCam({
...camSettings,
zoom: parseFloat(e.target.value),
})
}}
/>
</div>
</>
)}
<div>
Position
<ul className="flex">
<li>
<span className="pl-2 pr-1">x:</span>
<input
type="number"
step={5}
data-testid="cam-x-position"
value={camSettings.position[0]}
className="text-black w-16"
onChange={(e) => {
sceneInfra.setCam({
...camSettings,
position: [
parseFloat(e.target.value),
camSettings.position[1],
camSettings.position[2],
],
})
}}
/>
</li>
<li>
<span className="pl-2 pr-1">y:</span>
<input
type="number"
step={5}
data-testid="cam-y-position"
value={camSettings.position[1]}
className="text-black w-16"
onChange={(e) => {
sceneInfra.setCam({
...camSettings,
position: [
camSettings.position[0],
parseFloat(e.target.value),
camSettings.position[2],
],
})
}}
/>
</li>
<li>
<span className="pl-2 pr-1">z:</span>
<input
type="number"
step={5}
data-testid="cam-z-position"
value={camSettings.position[2]}
className="text-black w-16"
onChange={(e) => {
sceneInfra.setCam({
...camSettings,
position: [
camSettings.position[0],
camSettings.position[1],
parseFloat(e.target.value),
],
})
}}
/>
</li>
</ul>
</div>
</div>
)
}

View File

@ -25,14 +25,14 @@ import {
INTERSECTION_PLANE_LAYER,
isQuaternionVertical,
RAYCASTABLE_PLANE,
setupSingleton,
sceneInfra,
SKETCH_GROUP_SEGMENTS,
SKETCH_LAYER,
X_AXIS,
XZ_PLANE,
Y_AXIS,
YZ_PLANE,
} from './setup'
} from './sceneInfra'
import {
CallExpression,
getTangentialArcToInfo,
@ -85,7 +85,10 @@ export const TANGENTIAL_ARC_TO_SEGMENT_BODY = 'tangential-arc-to-segment-body'
export const TANGENTIAL_ARC_TO__SEGMENT_DASH =
'tangential-arc-to-segment-body-dashed'
class ClientSideScene {
// This singleton Class is responsible for all of the things the user sees and interacts with.
// That mostly mean sketch elements.
// Cameras, controls, raycasters, etc are handled by sceneInfra
class SceneEntities {
scene: Scene
sceneProgramMemory: ProgramMemory = { root: {}, return: null }
activeSegments: { [key: string]: Group } = {}
@ -93,18 +96,18 @@ class ClientSideScene {
axisGroup: Group | null = null
currentSketchQuaternion: Quaternion | null = null
constructor() {
this.scene = setupSingleton?.scene
setupSingleton?.setOnCamChange(this.onCamChange)
this.scene = sceneInfra?.scene
sceneInfra?.setOnCamChange(this.onCamChange)
}
onCamChange = () => {
const orthoFactor = orthoScale(setupSingleton.camera)
const orthoFactor = orthoScale(sceneInfra.camera)
Object.values(this.activeSegments).forEach((segment) => {
const factor =
setupSingleton.camera instanceof OrthographicCamera
sceneInfra.camera instanceof OrthographicCamera
? orthoFactor
: perspScale(setupSingleton.camera, segment)
: perspScale(sceneInfra.camera, segment)
if (
segment.userData.from &&
segment.userData.to &&
@ -135,9 +138,9 @@ class ClientSideScene {
})
if (this.axisGroup) {
const factor =
setupSingleton.camera instanceof OrthographicCamera
sceneInfra.camera instanceof OrthographicCamera
? orthoFactor
: perspScale(setupSingleton.camera, this.axisGroup)
: perspScale(sceneInfra.camera, this.axisGroup)
const x = this.axisGroup.getObjectByName(X_AXIS)
x?.scale.set(1, factor, 1)
const y = this.axisGroup.getObjectByName(Y_AXIS)
@ -241,12 +244,13 @@ class ClientSideScene {
engineCommandManager,
programMemoryOverride,
})
this.sceneProgramMemory = programMemory
const sketchGroup = sketchGroupFromPathToNode({
pathToNode: sketchPathToNode,
ast: kclManager.ast,
programMemory,
})
if (!Array.isArray(sketchGroup?.value)) return
this.sceneProgramMemory = programMemory
const group = new Group()
group.userData = {
type: SKETCH_GROUP_SEGMENTS,
@ -258,11 +262,11 @@ class ClientSideScene {
sketchGroup.position[1],
sketchGroup.position[2]
)
const orthoFactor = orthoScale(setupSingleton.camera)
const orthoFactor = orthoScale(sceneInfra.camera)
const factor =
setupSingleton.camera instanceof OrthographicCamera
sceneInfra.camera instanceof OrthographicCamera
? orthoFactor
: perspScale(setupSingleton.camera, dummy)
: perspScale(sceneInfra.camera, dummy)
sketchGroup.value.forEach((segment, index) => {
let segPathToNode = getNodePathFromSourceRange(
draftSegment ? truncatedAst : kclManager.ast,
@ -309,7 +313,7 @@ class ClientSideScene {
this.scene.add(group)
if (!draftSegment) {
setupSingleton.setCallbacks({
sceneInfra.setCallbacks({
onDrag: (args) => {
this.onDragSegment({
...args,
@ -319,7 +323,7 @@ class ClientSideScene {
onMove: () => {},
onClick: (args) => {
if (!args || !args.object) {
setupSingleton.modelingSend({
sceneInfra.modelingSend({
type: 'Set selection',
data: {
selectionType: 'singleCodeCursor',
@ -330,7 +334,7 @@ class ClientSideScene {
const { object } = args
const event = getEventForSegmentSelection(object)
if (!event) return
setupSingleton.modelingSend(event)
sceneInfra.modelingSend(event)
},
onMouseEnter: ({ object }) => {
// TODO change the color of the segment to yellow?
@ -350,15 +354,15 @@ class ClientSideScene {
parent.userData.pathToNode,
'CallExpression'
).node
setupSingleton.highlightCallback([node.start, node.end])
sceneInfra.highlightCallback([node.start, node.end])
const yellow = 0xffff00
colorSegment(object, yellow)
return
}
setupSingleton.highlightCallback([0, 0])
sceneInfra.highlightCallback([0, 0])
},
onMouseLeave: ({ object }) => {
setupSingleton.highlightCallback([0, 0])
sceneInfra.highlightCallback([0, 0])
const parent = getParentGroup(object)
const isSelected = parent?.userData?.isSelected
colorSegment(object, isSelected ? 0x0000ff : 0xffffff)
@ -371,7 +375,7 @@ class ClientSideScene {
},
})
} else {
setupSingleton.setCallbacks({
sceneInfra.setCallbacks({
onDrag: () => {},
onClick: async (args) => {
if (!args) return
@ -426,7 +430,7 @@ class ClientSideScene {
},
})
}
setupSingleton.controls.enableRotate = false
sceneInfra.controls.enableRotate = false
}
updateAstAndRejigSketch = async (
sketchPathToNode: PathToNode,
@ -529,7 +533,7 @@ class ClientSideScene {
this.sceneProgramMemory = programMemory
const sketchGroup = programMemory.root[variableDeclarationName]
.value as Path[]
const orthoFactor = orthoScale(setupSingleton.camera)
const orthoFactor = orthoScale(sceneInfra.camera)
sketchGroup.forEach((segment, index) => {
const segPathToNode = getNodePathFromSourceRange(
modifiedAst,
@ -545,9 +549,9 @@ class ClientSideScene {
// const prevSegment = sketchGroup.slice(index - 1)[0]
const type = group?.userData?.type
const factor =
setupSingleton.camera instanceof OrthographicCamera
sceneInfra.camera instanceof OrthographicCamera
? orthoFactor
: perspScale(setupSingleton.camera, group)
: perspScale(sceneInfra.camera, group)
if (type === TANGENTIAL_ARC_TO_SEGMENT) {
this.updateTangentialArcToSegment({
prevSegment: sketchGroup[index - 1],
@ -704,9 +708,9 @@ class ClientSideScene {
}
async animateAfterSketch() {
if (isReducedMotion()) {
setupSingleton.usePerspectiveCamera()
sceneInfra.usePerspectiveCamera()
} else {
await setupSingleton.animateToPerspective()
await sceneInfra.animateToPerspective()
}
}
removeSketchGrid() {
@ -739,7 +743,7 @@ class ClientSideScene {
reject()
}
}
setupSingleton.controls.enableRotate = true
sceneInfra.controls.enableRotate = true
this.activeSegments = {}
// maybe should reset onMove etc handlers
if (shouldResolve) resolve(true)
@ -758,7 +762,7 @@ class ClientSideScene {
})
}
setupDefaultPlaneHover() {
setupSingleton.setCallbacks({
sceneInfra.setCallbacks({
onMouseEnter: ({ object }) => {
if (object.parent.userData.type !== DEFAULT_PLANES) return
const type: DefaultPlane = object.userData.type
@ -783,7 +787,7 @@ class ClientSideScene {
planeString = posNorm ? 'XZ' : '-XZ'
normal = posNorm ? [0, 1, 0] : [0, -1, 0]
}
setupSingleton.modelingSend({
sceneInfra.modelingSend({
type: 'Select default plane',
data: {
plane: planeString,
@ -797,7 +801,7 @@ class ClientSideScene {
export type DefaultPlaneStr = 'XY' | 'XZ' | 'YZ' | '-XY' | '-XZ' | '-YZ'
export const clientSideScene = new ClientSideScene()
export const sceneEntitiesManager = new SceneEntities()
// calculations/pure-functions/easy to test so no excuse not to

View File

@ -1,5 +1,5 @@
import { Quaternion } from 'three'
import { isQuaternionVertical } from './setup'
import { isQuaternionVertical } from './sceneInfra'
describe('isQuaternionVertical', () => {
it('should identify vertical quaternions', () => {

View File

@ -24,7 +24,6 @@ import {
Object3DEventMap,
} from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
import { useRef, useEffect, useState } from 'react'
import { engineCommandManager } from 'lang/std/engineConnection'
import { v4 as uuidv4 } from 'uuid'
import { isReducedMotion, roundOff, throttle } from 'lib/utils'
@ -33,9 +32,7 @@ import { useModelingContext } from 'hooks/useModelingContext'
import { deg2Rad } from 'lib/utils2d'
import * as TWEEN from '@tweenjs/tween.js'
import { MouseGuard, cameraMouseDragGuards } from 'lib/cameraControls'
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
import { SourceRange } from 'lang/wasm'
import { useStore } from 'useStore'
import { Axis } from 'lib/selections'
import { createGridHelper } from './helpers'
@ -71,12 +68,14 @@ interface ThreeCamValues {
isPerspective: boolean
}
const lastCmdDelay = 50
let lastCmd: any = null
let lastCmdTime: number = Date.now()
let lastCmdTimeoutId: number | null = null
const sendLastReliableChannel = () => {
if (lastCmd && Date.now() - lastCmdTime >= 300) {
if (lastCmd && Date.now() - lastCmdTime >= lastCmdDelay) {
engineCommandManager.sendSceneCommand(lastCmd, true)
lastCmdTime = Date.now()
}
@ -98,16 +97,57 @@ const throttledUpdateEngineCamera = throttle((threeValues: ThreeCamValues) => {
if (lastCmdTimeoutId !== null) {
clearTimeout(lastCmdTimeoutId)
}
lastCmdTimeoutId = setTimeout(sendLastReliableChannel, 300) as any as number
lastCmdTimeoutId = setTimeout(
sendLastReliableChannel,
lastCmdDelay
) as any as number
}, 1000 / 30)
let lastPerspectiveCmd: any = null
let lastPerspectiveCmdTime: number = Date.now()
let lastPerspectiveCmdTimeoutId: number | null = null
const sendLastPerspectiveReliableChannel = () => {
if (
lastPerspectiveCmd &&
Date.now() - lastPerspectiveCmdTime >= lastCmdDelay
) {
engineCommandManager.sendSceneCommand(lastPerspectiveCmd, true)
lastPerspectiveCmdTime = Date.now()
}
}
const throttledUpdateEngineFov = throttle(
(vals: {
position: Vector3
quaternion: Quaternion
zoom: number
fov: number
}) => updateEngineFov(vals),
}) => {
const cmd = {
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_perspective_settings',
...convertThreeCamValuesToEngineCam({
...vals,
isPerspective: true,
}),
fov_y: vals.fov,
...calculateNearFarFromFOV(vals.fov),
},
} as any
engineCommandManager.sendSceneCommand(cmd)
lastPerspectiveCmd = cmd
lastPerspectiveCmdTime = Date.now()
if (lastPerspectiveCmdTimeoutId !== null) {
clearTimeout(lastPerspectiveCmdTimeoutId)
}
lastPerspectiveCmdTimeoutId = setTimeout(
sendLastPerspectiveReliableChannel,
lastCmdDelay
) as any as number
},
1000 / 15
)
@ -138,7 +178,7 @@ interface onMoveCallbackArgs {
intersection: Intersection<Object3D<Object3DEventMap>>
}
type ReactCameraProperties =
export type ReactCameraProperties =
| {
type: 'perspective'
fov?: number
@ -152,8 +192,11 @@ type ReactCameraProperties =
quaternion: [number, number, number, number]
}
class SetupSingleton {
static instance: SetupSingleton
// This singleton class is responsible for all of the under the hood setup for the client side scene.
// That is the cameras and switching between them, raycasters for click mouse events and their abstractions (onClick etc), setting up controls.
// Anything that added the the scene for the user to interact with is probably in SceneEntities.ts
class SceneInfra {
static instance: SceneInfra
scene: Scene
camera: PerspectiveCamera | OrthographicCamera
renderer: WebGLRenderer
@ -290,7 +333,7 @@ class SetupSingleton {
const light = new AmbientLight(0x505050) // soft white light
this.scene.add(light)
SetupSingleton.instance = this
SceneInfra.instance = this
}
private _isCamMovingCallback: (isMoving: boolean, isTween: boolean) => void =
() => {}
@ -481,7 +524,7 @@ class SetupSingleton {
const targetFov = 4
const fovAnimationStep = (currentFov - targetFov) / FRAMES_TO_ANIMATE_IN
let frameWaitOnFinish = 5
let frameWaitOnFinish = 10
const animateFovChange = () => {
if (this.camera instanceof PerspectiveCamera) {
@ -670,7 +713,7 @@ class SetupSingleton {
} | null => {
this.planeRaycaster.setFromCamera(
this.currentMouseVector,
setupSingleton.camera
sceneInfra.camera
)
const planeIntersects = this.planeRaycaster.intersectObjects(
this.scene.children,
@ -933,7 +976,7 @@ class SetupSingleton {
if (planesGroup) this.scene.remove(planesGroup)
}
updateOtherSelectionColors = (otherSelections: Axis[]) => {
const axisGroup = setupSingleton.scene.children.find(
const axisGroup = sceneInfra.scene.children.find(
({ userData }) => userData?.type === AXIS_GROUP
)
const axisMap: { [key: string]: Axis } = {
@ -955,247 +998,7 @@ class SetupSingleton {
}
}
export const setupSingleton = new SetupSingleton()
function useShouldHideScene(): { hideClient: boolean; hideServer: boolean } {
const [isCamMoving, setIsCamMoving] = useState(false)
const [isTween, setIsTween] = useState(false)
const { state } = useModelingContext()
useEffect(() => {
setupSingleton.setIsCamMovingCallback((isMoving, isTween) => {
setIsCamMoving(isMoving)
setIsTween(isTween)
})
}, [])
if (DEBUG_SHOW_BOTH_SCENES || !isCamMoving)
return { hideClient: false, hideServer: false }
let hideServer = state.matches('Sketch') || state.matches('Sketch no face')
if (isTween) {
hideServer = false
}
return { hideClient: !hideServer, hideServer }
}
export const ClientSideScene = ({
cameraControls,
}: {
cameraControls: ReturnType<
typeof useGlobalStateContext
>['settings']['context']['cameraControls']
}) => {
const canvasRef = useRef<HTMLDivElement>(null)
const { state, send } = useModelingContext()
const { hideClient, hideServer } = useShouldHideScene()
const { setHighlightRange } = useStore((s) => ({
setHighlightRange: s.setHighlightRange,
highlightRange: s.highlightRange,
}))
// Listen for changes to the camera controls setting
// and update the client-side scene's controls accordingly.
useEffect(() => {
setupSingleton.setInteractionGuards(cameraMouseDragGuards[cameraControls])
}, [cameraControls])
useEffect(() => {
setupSingleton.updateOtherSelectionColors(
state?.context?.selectionRanges?.otherSelections || []
)
}, [state?.context?.selectionRanges?.otherSelections])
useEffect(() => {
if (!canvasRef.current) return
const canvas = canvasRef.current
canvas.appendChild(setupSingleton.renderer.domElement)
setupSingleton.animate()
setupSingleton.setHighlightCallback(setHighlightRange)
canvas.addEventListener('mousemove', setupSingleton.onMouseMove, false)
canvas.addEventListener('mousedown', setupSingleton.onMouseDown, false)
canvas.addEventListener('mouseup', setupSingleton.onMouseUp, false)
setupSingleton.setSend(send)
return () => {
canvas?.removeEventListener('mousemove', setupSingleton.onMouseMove)
canvas?.removeEventListener('mousedown', setupSingleton.onMouseDown)
canvas?.removeEventListener('mouseup', setupSingleton.onMouseUp)
}
}, [])
return (
<div
ref={canvasRef}
className={`absolute inset-0 h-full w-full transition-all duration-300 ${
hideClient ? 'opacity-0' : 'opacity-100'
} ${hideServer ? 'bg-black' : ''} ${
!hideClient && !hideServer && state.matches('Sketch')
? 'bg-black/80'
: ''
}`}
></div>
)
}
const throttled = throttle((a: ReactCameraProperties) => {
if (a.type === 'perspective' && a.fov) {
setupSingleton.dollyZoom(a.fov)
}
}, 1000 / 15)
export const CamDebugSettings = () => {
const [camSettings, setCamSettings] = useState<ReactCameraProperties>({
type: 'perspective',
fov: 12,
position: [0, 0, 0],
quaternion: [0, 0, 0, 1],
})
const [fov, setFov] = useState(12)
useEffect(() => {
setupSingleton.setReactCameraPropertiesCallback(setCamSettings)
}, [setupSingleton])
useEffect(() => {
if (camSettings.type === 'perspective' && camSettings.fov) {
setFov(camSettings.fov)
}
}, [(camSettings as any)?.fov])
return (
<div>
<h3>cam settings</h3>
perspective cam
<input
type="checkbox"
checked={camSettings.type === 'perspective'}
onChange={(e) => {
if (camSettings.type === 'perspective') {
setupSingleton.useOrthographicCamera()
} else {
setupSingleton.usePerspectiveCamera()
}
}}
/>
{camSettings.type === 'perspective' && (
<input
type="range"
min="4"
max="90"
step={0.5}
value={fov}
onChange={(e) => {
setFov(parseFloat(e.target.value))
throttled({
...camSettings,
fov: parseFloat(e.target.value),
})
}}
className="w-full cursor-pointer pointer-events-auto"
/>
)}
{camSettings.type === 'perspective' && (
<div>
<span>fov</span>
<input
type="number"
value={camSettings.fov}
className="text-black w-16"
onChange={(e) => {
setupSingleton.setCam({
...camSettings,
fov: parseFloat(e.target.value),
})
}}
/>
</div>
)}
{camSettings.type === 'orthographic' && (
<>
<div>
<span>fov</span>
<input
type="number"
value={camSettings.zoom}
className="text-black w-16"
onChange={(e) => {
setupSingleton.setCam({
...camSettings,
zoom: parseFloat(e.target.value),
})
}}
/>
</div>
</>
)}
<div>
Position
<ul className="flex">
<li>
<span className="pl-2 pr-1">x:</span>
<input
type="number"
step={5}
data-testid="cam-x-position"
value={camSettings.position[0]}
className="text-black w-16"
onChange={(e) => {
setupSingleton.setCam({
...camSettings,
position: [
parseFloat(e.target.value),
camSettings.position[1],
camSettings.position[2],
],
})
}}
/>
</li>
<li>
<span className="pl-2 pr-1">y:</span>
<input
type="number"
step={5}
data-testid="cam-y-position"
value={camSettings.position[1]}
className="text-black w-16"
onChange={(e) => {
setupSingleton.setCam({
...camSettings,
position: [
camSettings.position[0],
parseFloat(e.target.value),
camSettings.position[2],
],
})
}}
/>
</li>
<li>
<span className="pl-2 pr-1">z:</span>
<input
type="number"
step={5}
data-testid="cam-z-position"
value={camSettings.position[2]}
className="text-black w-16"
onChange={(e) => {
setupSingleton.setCam({
...camSettings,
position: [
camSettings.position[0],
camSettings.position[1],
parseFloat(e.target.value),
],
})
}}
/>
</li>
</ul>
</div>
</div>
)
}
export const sceneInfra = new SceneInfra()
function convertThreeCamValuesToEngineCam({
position,
@ -1242,29 +1045,6 @@ function calculateNearFarFromFOV(fov: number) {
return { z_near: 0.1, z_far }
}
function updateEngineFov(args: {
position: Vector3
quaternion: Quaternion
zoom: number
fov: number
}) {
engineCommandManager.sendSceneCommand(
{
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_perspective_settings',
...convertThreeCamValuesToEngineCam({
...args,
isPerspective: true,
}),
fov_y: args.fov,
...calculateNearFarFromFOV(args.fov),
},
} as any /* TODO - this command isn't in the spec yet, remove any when it is */
)
}
export function isQuaternionVertical(q: Quaternion) {
const v = new Vector3(0, 0, 1).applyQuaternion(q)
// no x or y components means it's vertical

View File

@ -25,9 +25,9 @@ import {
TANGENTIAL_ARC_TO_SEGMENT,
TANGENTIAL_ARC_TO_SEGMENT_BODY,
TANGENTIAL_ARC_TO__SEGMENT_DASH,
} from './clientSideScene'
} from './sceneEntities'
import { getTangentPointFromPreviousArc } from 'lib/utils2d'
import { ARROWHEAD } from './setup'
import { ARROWHEAD } from './sceneInfra'
export function straightSegment({
from,

View File

@ -1,10 +1,10 @@
import { useState, useEffect } from 'react'
import { setupSingleton } from '../clientSideScene/setup'
import { sceneInfra } from '../clientSideScene/sceneInfra'
import { engineCommandManager } from 'lang/std/engineConnection'
import { throttle, isReducedMotion } from 'lib/utils'
const updateDollyZoom = throttle(
(newFov: number) => setupSingleton.dollyZoom(newFov),
(newFov: number) => sceneInfra.dollyZoom(newFov),
1000 / 15
)
@ -15,19 +15,19 @@ export const CamToggle = () => {
useEffect(() => {
engineCommandManager.waitForReady.then(async () => {
setupSingleton.dollyZoom(fov)
sceneInfra.dollyZoom(fov)
})
}, [])
const toggleCamera = () => {
if (isPerspective) {
isReducedMotion()
? setupSingleton.useOrthographicCamera()
: setupSingleton.animateToOrthographic()
? sceneInfra.useOrthographicCamera()
: sceneInfra.animateToOrthographic()
} else {
isReducedMotion()
? setupSingleton.usePerspectiveCamera()
: setupSingleton.animateToPerspective()
? sceneInfra.usePerspectiveCamera()
: sceneInfra.animateToPerspective()
}
setIsPerspective(!isPerspective)
}
@ -60,9 +60,9 @@ export const CamToggle = () => {
<button
onClick={() => {
if (enableRotate) {
setupSingleton.controls.enableRotate = false
sceneInfra.controls.enableRotate = false
} else {
setupSingleton.controls.enableRotate = true
sceneInfra.controls.enableRotate = true
}
setEnableRotate(!enableRotate)
}}

View File

@ -1,7 +1,7 @@
import { CollapsiblePanel, CollapsiblePanelProps } from './CollapsiblePanel'
import { AstExplorer } from './AstExplorer'
import { EngineCommands } from './EngineCommands'
import { CamDebugSettings } from 'clientSideScene/setup'
import { CamDebugSettings } from 'clientSideScene/ClientSideSceneComp'
export const DebugPanel = ({ className, ...props }: CollapsiblePanelProps) => {
return (

View File

@ -33,9 +33,10 @@ import { applyConstraintIntersect } from './Toolbar/Intersect'
import { applyConstraintAbsDistance } from './Toolbar/SetAbsDistance'
import useStateMachineCommands from 'hooks/useStateMachineCommands'
import { modelingMachineConfig } from 'lib/commandBarConfigs/modelingCommandConfig'
import { setupSingleton } from 'clientSideScene/setup'
import { getSketchQuaternion } from 'clientSideScene/clientSideScene'
import { sceneInfra } from 'clientSideScene/sceneInfra'
import { getSketchQuaternion } from 'clientSideScene/sceneEntities'
import { startSketchOnDefault } from 'lang/modifyAst'
import { Program } from 'lang/wasm'
type MachineContext<T extends AnyStateMachine> = {
state: StateFrom<T>
@ -189,6 +190,17 @@ export const ModelingMachineProvider = ({
},
},
services: {
'AST-undo-startSketchOn': async ({ sketchPathToNode }) => {
if (!sketchPathToNode) return
const newAst: Program = JSON.parse(JSON.stringify(kclManager.ast))
const varDecIndex = sketchPathToNode[1][0]
// remove body item at varDecIndex
newAst.body = newAst.body.filter((_, i) => i !== varDecIndex)
await kclManager.executeAstMock(newAst, { updates: 'code' })
sceneInfra.setCallbacks({
onClick: () => {},
})
},
'animate-to-face': async (_, { data: { plane, normal } }) => {
const { modifiedAst, pathToNode } = startSketchOnDefault(
kclManager.ast,
@ -196,7 +208,7 @@ export const ModelingMachineProvider = ({
)
await kclManager.updateAst(modifiedAst, false)
const quaternion = getSketchQuaternion(pathToNode, normal)
await setupSingleton.tweenCameraToQuaternion(quaternion)
await sceneInfra.tweenCameraToQuaternion(quaternion)
return {
sketchPathToNode: pathToNode,
sketchNormalBackUp: normal,
@ -210,7 +222,7 @@ export const ModelingMachineProvider = ({
sketchPathToNode || [],
sketchNormalBackUp
)
await setupSingleton.tweenCameraToQuaternion(quaternion)
await sceneInfra.tweenCameraToQuaternion(quaternion)
},
'Get horizontal info': async ({
selectionRanges,

View File

@ -15,7 +15,7 @@ import { Models } from '@kittycad/lib'
import { engineCommandManager } from '../lang/std/engineConnection'
import { useModelingContext } from 'hooks/useModelingContext'
import { useKclContext } from 'lang/KclSingleton'
import { ClientSideScene } from 'clientSideScene/setup'
import { ClientSideScene } from 'clientSideScene/ClientSideSceneComp'
export const Stream = ({ className = '' }: { className?: string }) => {
const [isLoading, setIsLoading] = useState(true)

View File

@ -26,7 +26,7 @@ import interact from '@replit/codemirror-interact'
import { engineCommandManager } from '../lang/std/engineConnection'
import { kclManager, useKclContext } from 'lang/KclSingleton'
import { ModelingMachineEvent } from 'machines/modelingMachine'
import { setupSingleton } from 'clientSideScene/setup'
import { sceneInfra } from 'clientSideScene/sceneInfra'
export const editorShortcutMeta = {
formatCode: {
@ -119,7 +119,7 @@ export const TextEditor = ({
if (!editorView) {
setEditorView(viewUpdate.view)
}
if (setupSingleton.selected) return // mid drag
if (sceneInfra.selected) return // mid drag
const ignoreEvents: ModelingMachineEvent['type'][] = [
'Equip Line tool',
'Equip tangential arc to',

View File

@ -99,7 +99,7 @@ class KclManager {
})
})
} else {
localStorage?.setItem(PERSIST_CODE_TOKEN, code)
safteLSSetItem(PERSIST_CODE_TOKEN, code)
}
}
@ -154,16 +154,16 @@ class KclManager {
this.code = ''
return
}
const storedCode = localStorage.getItem(PERSIST_CODE_TOKEN)
const storedCode = safeLSGetItem(PERSIST_CODE_TOKEN) || ''
// TODO #819 remove zustand persistence logic in a few months
// short term migration, shouldn't make a difference for tauri app users
// anyway since that's filesystem based.
const zustandStore = JSON.parse(localStorage.getItem('store') || '{}')
const zustandStore = JSON.parse(safeLSGetItem('store') || '{}')
if (storedCode === null && zustandStore?.state?.code) {
this.code = zustandStore.state.code
localStorage.setItem(PERSIST_CODE_TOKEN, this._code)
safteLSSetItem(PERSIST_CODE_TOKEN, this._code)
zustandStore.state.code = ''
localStorage.setItem('store', JSON.stringify(zustandStore))
safteLSSetItem('store', JSON.stringify(zustandStore))
} else if (storedCode === null) {
this.code = bracket
} else {
@ -457,3 +457,13 @@ export function KclContextProvider({
</KclContext.Provider>
)
}
function safeLSGetItem(key: string) {
if (typeof window === 'undefined') return null
return localStorage?.getItem(key)
}
function safteLSSetItem(key: string, value: string) {
if (typeof window === 'undefined') return
localStorage?.setItem(key, value)
}

View File

@ -30,7 +30,7 @@ import {
createFirstArg,
} from './std/sketch'
import { isLiteralArrayOrStatic } from './std/sketchcombos'
import { DefaultPlaneStr } from 'clientSideScene/clientSideScene'
import { DefaultPlaneStr } from 'clientSideScene/sceneEntities'
import { roundOff } from 'lib/utils'
export function startSketchOnDefault(

View File

@ -5,7 +5,7 @@ import { exportSave } from 'lib/exportSave'
import { v4 as uuidv4 } from 'uuid'
import * as Sentry from '@sentry/react'
import { getNodePathFromSourceRange } from 'lang/queryAst'
import { setupSingleton } from 'clientSideScene/setup'
import { sceneInfra } from 'clientSideScene/sceneInfra'
let lastMessage = ''
@ -1012,7 +1012,7 @@ export class EngineCommandManager {
gizmo_mode: true,
},
})
setupSingleton.onStreamStart()
sceneInfra.onStreamStart()
executeCode(undefined, true)
},

View File

@ -14,11 +14,11 @@ import { CommandArgument } from './commandTypes'
import {
STRAIGHT_SEGMENT,
TANGENTIAL_ARC_TO_SEGMENT,
clientSideScene,
sceneEntitiesManager,
getParentGroup,
} from 'clientSideScene/clientSideScene'
} from 'clientSideScene/sceneEntities'
import { Mesh } from 'three'
import { AXIS_GROUP, X_AXIS } from 'clientSideScene/setup'
import { AXIS_GROUP, X_AXIS } from 'clientSideScene/sceneInfra'
export const X_AXIS_UUID = 'ad792545-7fd3-482a-a602-a93924e3055b'
export const Y_AXIS_UUID = '680fd157-266f-4b8a-984f-cdf46b8bdf01'
@ -401,7 +401,7 @@ function updateSceneObjectColors(codeBasedSelections: Selection[]) {
console.error('error parsing code in processCodeMirrorRanges', e)
return
}
Object.values(clientSideScene.activeSegments).forEach((segmentGroup) => {
Object.values(sceneEntitiesManager.activeSegments).forEach((segmentGroup) => {
if (
![STRAIGHT_SEGMENT, TANGENTIAL_ARC_TO_SEGMENT].includes(
segmentGroup?.userData?.type

View File

@ -4,7 +4,6 @@ import {
EngineCommand,
} from '../lang/std/engineConnection'
import { Models } from '@kittycad/lib'
import { v4 as uuidv4 } from 'uuid'
type WebSocketResponse = Models['OkWebSocketResponseData_type']

View File

@ -1,6 +1,5 @@
import { PathToNode, VariableDeclarator } from 'lang/wasm'
import { engineCommandManager } from 'lang/std/engineConnection'
import { isReducedMotion } from 'lib/utils'
import {
Axis,
Selection,
@ -8,7 +7,6 @@ import {
Selections,
} from 'lib/selections'
import { assign, createMachine } from 'xstate'
import { v4 as uuidv4 } from 'uuid'
import { isCursorInSketchCommandRange } from 'lang/util'
import { getNodePathFromSourceRange } from 'lang/queryAst'
import { kclManager } from 'lang/KclSingleton'
@ -46,11 +44,11 @@ import { Models } from '@kittycad/lib/dist/types/src'
import { ModelingCommandSchema } from 'lib/commandBarConfigs/modelingCommandConfig'
import {
DefaultPlaneStr,
clientSideScene,
sceneEntitiesManager,
quaternionFromSketchGroup,
sketchGroupFromPathToNode,
} from 'clientSideScene/clientSideScene'
import { setupSingleton } from 'clientSideScene/setup'
} from 'clientSideScene/sceneEntities'
import { sceneInfra } from 'clientSideScene/sceneInfra'
export const MODELING_PERSIST_KEY = 'MODELING_PERSIST_KEY'
@ -116,7 +114,7 @@ export type MoveDesc = { line: number; snippet: string }
export const modelingMachine = createMachine(
{
/** @xstate-layout N4IgpgJg5mDOIC5QFkD2EwBsCWA7KAxAMICGuAxlgNoAMAuoqAA6qzYAu2qujIAHogDMARhoA6GgE4A7COEBWGoIBMswQBoQAT0TDBANn1j9NAByTlw08tODTAFlMBfJ5rQYc+AgGUw7AASwWGDknNy0DEggLGxhPFECCMIK8mLSyvL2gvKmwvr29vqSmjoIBvKSYgoFwsr2MpLyLm7oWHhQYtgQmGA+foHBoVy4EbwxHMO8ifryqTNFyvqmNLX2NPYlulZGCjSGohlF+oLNIO5t+J3dvQCiuOxgAE6BANZ+5AAWo1HjcVOI0mE0mMkhkwns0ms6wymwQ0nWYlM8MkVmkFnB1lO508HS6PQIdwez1gb3YnyowkizFYE24-zK1jE8kEKP00mk9jqBmEsMk+Sq6zWCkM5mUJ1cZ1aOKu+JufHYjwArhhvtTYpMEohbKYxJYOTlpIpcspYclTDtFJDdrMaDksVL2mJvKTPsQyJRMM73l96GMaX9NQhlHVKszbVlhBZ5ApTUa0go6gd9BkmhLsY6vWSPk6XR8AJLXPoBII9IbhX0-f0a0CJOpIiQVQXpNGFU3ZSomQSCPaiAoGfT2jwZ3M570F-HIEhvAZQAC2YHu-gAbk9OOQSJhVdEq3TA2LMsZbZJOWj9Bj9KaOeJzPlZhlAct7IOLh1M59R1nx71J9OgnOFwEHyoI82AAF7cOwG5br81b8IgYp2BIIjwjQwaOOYprHJU4KWCI2SZPIwbPtKb7ZqRX7ENwsAKiQeD+EBIHgfcG7+BA2DUe6YDQTu8Q1vByFiIUmTrMe8JrPIsKqGKxjRpI3ZWNkwjEcO3ofp8FFEFRNF0SujxrixbEcRQXEVmqtK8XBQZdsoYiCIax5ssyyh7JJJjCFUzlnvYih5Mm0jKZcpFqfmhaabg1GPLRuD+AAggAQt4-gABrcequ58VZjiIiYZ40Osgj2OCsIqIyswqFIhpKPCA5pg6gUjuRoVaZFdHxYlACaqXmfSYpZYYhHyNIfnsqYxWFUYKiRrkXYiIVAWvg1uYac1UX+GQUA9F1AYZXUeyInsFgGJISh5dIkm2PYiI2jQHK5JG-m1UO9WqY1+JhRFq09Pg7A+lS25pRZtZrOIkJItCBRZEskkObZMwOENCwFfNwXBct4XadFTBPFjuBseQiqYCQzyGZBxlbbBtbKCiYgWLthp6JI1gaNoQg4bZeQqBy6EFA9LRPQtL1LU16MtdFDFgRBG6YFoa04FAIymf93V7ioRg2Aoj6DYRo0s0kDiCIitPst5kYmE+j0vijr29O9GPLqu2Drpg0uy9g8vk+llm9TZpizDIhQ2NGZ265GzJVOawYODddhZMjQXW5RIurbAuAkEw-jsKgyUe4D-FDcY5rHWsx0GDdklHYiwZSMc3aLBYceLWOwsfXRKdpxnWedYrMGe7WdlGPUx61A4tMXrrdSGlUJuzPk2wKA3gtN29K10WAACOiosV9UA-TnPXZEYcmQrdQJ2Zkkm2jqqFslTkLVzVfOW-HQvL0ndFMETUvUN3PH78dgneXSGyW0zJijj0WJdZMjQUSAmjLaB+kp+ZWxfjbFe0VHhgFnKgFc-hyBoPYLAPeKtahIVmNGFkyYdalEsDkYwehsgrEGo4cUj8SKN0-IWAASmAAAtGAPgIRFQPCITtcaTIbQZAKksdkppnKVDkoYGQLJahdgXlmVGhYbgb2wOnAAMngMAHdUCbh-gDHqfJ5E2GOFkJEjQgSmnDEhRo0ZHyGCKGo98CctGKh0RndaAFsAsSJuQDuIjLIqBEPtJEiwhqgkGqaTklRyhKBtJCLIqZWEqXUTFAA7rRQCwEJbMUwKxdipNKD+DwAAM1QAQCA3AwCdFwEuVAbwxAwHYDw8WTFIKYB4dU1AYTEhdjZNlRo6xfZyLAdQjkwIChRhsAozIHjsy5PyfRQpPSDJlM4pU3ANSCBPEeMBMQTBCbsBqY8Wc7S-BdM2ZLPpAyhlCAMDqZklVbwcmSBsceyJjB2ROjdWBzgLZsNUmsjg9s9KO22UZCpAy6kNKaS0tpHSeG6X0o8-ZgzTHKwyl2Se8gTDHUyGiWaPzqFnnchCPIuRfaGiKObTJz1sl5MhRimFJSSa7IRUck5ZySAXOAtctFHKnb9Oxc8so-ddQ2F9pyWxyZJJEsqENZyk1ahLCJSssQEKAhtWSnsg59TcCNLwCixpaKSAACNYA8L4BKmpUquzLHDooJRMx4Qxl1iVVVrzCgKHhHUDJiCn4jj1bFBKhreWPGOY8U55zLkituTau1Dqnm4u2uEuw14VXH2vkUCSPquQ0xtNYawKIA46ojQa9qRrakmrNc01plqU22p4VoR1OK-o91ztKtEaRQZwMKvUMepQuyNAkI0HN3Z9R6GrWy-VUa60xrjQmwVSabmdNTR2rtzqc2CUtBEtJNcxrUzcVIe6g13Ggqye+CN60ej1sRaa5FLat08MfbwjNPbf6BhdSDOoYoqrOWbOdBwTI67dlMMsRYIb0wsvvYuta+An2rv5Ym4VH6v17szRTF5jJajpDFAhFYBVzoFDSFIUEoIOQWCZaGsFrL1nbx+s+xtb7UW3NYx8XDv6zH-rsjqX2mRRTmhUAUYqKwbIzH9vCc0og5ILpYwuHeHxn18vjQKoVVyP08b436AT+K7Khl6k5ZYFQKXwRkOIDI5hIzORWLIZTkKsaPBxnjAmRNSlwsMQijj5r31orcx5x2XnHg8O5cZAzlYjPZsWLZXIhUlGOGhMVBMNM2Q3giaPFzAQQsLk84TYmOzjIadjRhjdWHgvY0K2F4rkXSuUBi2ZLNwzVaJfBPdAqj4TQ+t7JlyEfIcvBgQQhgW6j9GmqMZgIsAxSxxGdVJREik9AKX9rCZMl00TBjsEovKyzb2IezFNwxmdjEEm0enUmMB7iBJKcE0JeHe5CFsMCa9oITCoSc0W0oehbRIVQkCLILIZA6tOzNsQeZcAcAIPuvIaR3mGlvhUc+usaO6gjqoc09QHDKHBwYyH0PYcUn43i7NhFBJWGjmeYSzIHEyCZKIctZhi4ZAJ9N87mAxC4GFRuObJYQiLee32-CNk7AQOA5GIFppkjAmPLYIlyYbT1yOxN98EOudiAAHJZwAAqoDwAQggMUIAQECJBPS-gWBG+dekHUJVDTeSFOaXkjOlDJkWMsCJ0YdXE-YHDkXPUbryLMGiJEp8RCxjPBIfIICT1nnx2rlG-u4eUkM+T2syRUh4Ro1zFRmFIydfZDkI0x0xt1XV9mAAKv4u7QTHghMzgLwYwuydtaEEUA2tRwRF1wpaaPlRCo2HqLIcwswdW1-wAEhvTfaneN8Zr1AxjnUzBz9JvKXucptgBydPUIPaMrP8Lz-wVSSCUD6At1iYAz8E3y4TU1UrK1IQszRwoSwrMIGnUyQogoVBEu1mRjIGwFnEFXaA7mtwf16AC2bTaWANAIeB4Uzh4TP0oClTyHBEPDsENGjG9mZlKGcRpjDy+y6wDSAJhwQPAMzkgLIF6F8GLFb2GHQK7FSGZCJUsBuipkaDHS1EnlkGciPU4McHIJALAPwAgP4TKXAJJG9BfSbQtTEHgMFV4WQJkKzHQKgVIUVyUHSTklhA5BsmPEOi8hyBqglF5wwHgCiHGwzw7yDGTCnVkGzyUFUC7EknSDSFpiOlHTDHg0rxlDAFsPwwQBHSulwM1T8kKE-1qCkA8nSG8g9yKEkBWSCJeyDCUHcmOlsQjzUGSGKhMF1FHlQlrj5D8KQWfiXkCNi0z3ggDgLjRDMHMCEkMFjCL1kDIxTGyBg2SOTyCgjW6QeR83KT82xVSL7SpkiRx2jmsEcAsHcKLyHnaJZH7AcDyyhUxSGJ5VGOqLsKpm8iZFsGZ3yAcCI2VXZGME8mSGlwVyT2ZSr11WQwNSSnrTGPMSJSZD2DQghE5FG2KgyEqFtEmiYXWDITWNrReJ2OCKplmFLQTCKA5j1GKhZB9ksEZn4JUEWDWK-QhNayhOgQbDwl9lEBmB5HHlsAHmckDmsm+15kYzvVWWQx4xxKVl2O-2sAvTkgxFsGhn5DsBOKvDsDYLWIK1xnq28yi3hW2NxLSKpmcl1FNhSSBDMF+1ZliJEEoRg2WEjBYTpOOzECX2MVeP-V8kEj5FS1ykZhkFhHJKZAMFUDyFKOvQ5zO2X2539yNPxUsEulHXNMU3MGDj+xWFVWGkhgKhqGdMh15yuQ3A9PCSJXEGOnBgcz7B4KSFQgBIsGTG7DskWHWAjK11138ANyNysOlNF3aPjFnjXyrj0FlwByyEKBHkGhukOzuJTxh3YFjKz2kj2CJJREUHIVjDMHjEIkKmvhTEnzr04Fnw7i7M7yylDicJZB2xchDhM2yndQrXqGriPxP1QKqLLPpGOB2H9mWC5h5ioUQEaEMNBD0EWAWDkREMoPEOoIFVNTnKSGDDmTyh-IOiAz6wILDmODDD8lUCPCfLEKgAkL4CkPELUM+A-NqEUCoykFmHqGB3wIBARAqkKHZFvMWHFBcCAA */
/** @xstate-layout N4IgpgJg5mDOIC5QFkD2EwBsCWA7KAxAMICGuAxlgNoAMAuoqAA6qzYAu2qujIAHogC0AdgCsAZgB04gEyjhADnEA2GgoUAWJQBoQAT0QBGGuICckmoZkbTM42YWKAvk91oMOfAQDKYdgAJYLDByTm5aBiQQFjYwniiBBFtpGhlxDRphGg1ZURlhXQMEcRLDSVF5UwV84XEK8Rc3dCw8KElsCEwwHz9A4NCuXAjeGI5B3kTBDOFJYWVlQ2UlY3UrQsQFUQVy0Q00w3FLGVNTBtcQdxb8ds7ugFFcdjAAJ0CAaz9yAAthqNG4iZCdKiWZpcQKEyHTKmTLrYrCGb5DRaGHKBGiYzKRoXZqeNodLoEB5PV6wD7sb5UQyRZisMbcQEIQRLGiSdRmfIqUxLCpw5QqcrQ4SGTTWUx5bGXPE3Ql3PjsZ4AVwwv1psXGCSEMjkkhkNFE-MNBxo4vEcM22wqOQxikMKwUktxrUk3nJ32IZEomFdnx+9BGdIBmoQdoUykkxmEyIhELMsjhobKoYUdsOIq2jo8zp9FK+LrdXwAkrcegEgl0BuF-X9AxrQIlDHspPzRKYbAsttYzfojCYZBYaEs5uCaCZuZmrm0c99877i4TkCQPn0oABbMCPfwANxenHIJEwquitYZwaspiT1U0c00qQKPZDGgOKVHcgk+SUGgn0uned-8+6RdlyCNcNwCL5UGebAAC9uHYA8j3+Ot+CMJFJFUMN9TtfI0RkBNxDtcoNHkSx+WMCoZG-bMC1nXMAOIbhYAVEg8H8CCoNgx4D38CBsCYz0wEQk94nrVDdgsJsTU-C91DhNJuQsCpRFHYiLwOKjrl-WjvnoohGOY1id2ePduN4-iKEE6s1XpESUJDGQlkkcUZE2RYnzUPY5M5cprBFDQEWEBzdg0qcaP-Es9NwJjnhY3B-AAQQAIW8fwAA0hPVU9RPszZJByMRG1Oc80jk8UNCcg1FFfVtajOJos00sKC10-SYtYpKUoATQymzGSsXLrDsZQ5EMA0DTwh9DnUUElMWNQoy-c4pWo31tKLCLWti-wyCgLoeqDbKDhyaQlCveQU2EUw5OUxEtnkdE0ROYQQrWtaWqigy4q6fB2D9Glj0y2yGxKGYVANE0Dn2UQ5IWbY5jbQxLujFRnqWp1GtW8LCUi6KtqYF58dwXjyEVTASFeMz4Is-bkIbbVthsIrrEsTDlDklNyuFKpTARaxkTEF6tKx7occ+tjIJguCD0wPRtpwKAhisgHerPBz+wtLQJGIjJ+QTDFw2OLZ-MCsR+TqnEGtCzHmo2j62rioyTMwGW5ewBWaayuyrEuvLZEccVFjMDEE2GkEth5qolmORxzeWjHcze23cdY2BcBIJh-HYVA0o9oGjAIqQHJHBQeeqMw5OyfttQNeZVC2QLKLRy3XuFhi7a21P08z7PuqVpDPeB59MjIhFxGFbI5MbGZdkcOQLyjZQckFpq5yTsWwAAR0VbjvqgX7c7644pAxY2ef1OQx4TWpWRyExR31ZQDVMZfrdX7HNtYphyel6g++EvrZAzFSEXUapwnxRgTKcIBpQFgnEyGoUQL8E6t1FvbfwzwwCrlQDufw5AP6PFgAfVWWh0JWGIsNTQo0dAPhFARCwixbDZBKGoYaSCZytwAEpgEEGAPgIRFRPCIYdOoUgDiGFOKkdIYhg4PnUP2dIqQ0S1yUAsNhf4bayi3tgDOAAZPAYBu6oEPH-QGfVUSCnqLYMQ1CiiBXDMiAi2o1CKPHE3ScLcNH3C0RnKmMBHjYG4uTcg3chF2SGtscEBoCKP0UCIhMxgyg3jrrE5Ei81GSHigAdxYuBCWnF4KYB4nxKmlB-B4AAGaoAIBAbgYB2i4C3KgD4kgYDsEEOxSWXFMCCAqagUJiQ0jZDyhoReJpahPVqHJJ85hNguX2MoWwlV0lZJyeLDiUtCmUwEmU3AlSCAvGeJBSQTAybsEqc8VcLS-DtLyRsnpuy+kmJVtlQZII7TIlTMRFyPM5Lgn7NkQco46g4UMMs7JHBty7mwPuTZxTtm9OqbU+pjTmmtMEI7aFB57mVP6YgfY-YDi2hMGicR+QvJKDZGIOQdhSiBTBasjFMKinmVKQig5RyTkkDOZBS5aLGVYt6bihA+K2T6jEPPI2Xk9gRkfqoNIcj2T0ohR1NKOy9k1NwHUvAKK6lopIAAI1gIIPg2LHn-X7nnYV0TZjDVfBfLYpVhoRgyE+Reshhq1CVQEFVqU1VVPZc8Y5pzzm8uuQao1JrBVPIOmEgiMwcjgiUK2fUWhSqLGkFsR+BFshRmfm4n8NEVnKuSv4TqfrEWauRU03VYbDWCD0KaoV8yM3yAqLYKBxE4SHDRHlbmyktiHGCvmlaCci3epLWWtlzxDmBs5dyi5Vy2nhvrY26NtM8VxosDhMeDl+Suq7W5CwCz5jV0JY3eq7itJju2vgLo5aNVaoadWxdggdpdFXea-+wZ9ig0sAcPIrYA7XQNE5Qdf6KhhgRF6m9u0DFTpnUGrlIaX1vu4VGz9pjv0EThtkfqVg1Dz2uojCM-6shyHPmiaDu9fr3qRdq59aLqNfA-QGTDLyCIghyPJcBew7ww1yoOWlKhEZWCoxuPeXxy0BsQ-O0NbSmMsZrGx2N6bjAmC2Isa8E0igOWlSelE-l7oGmg-jZ4hNiak3JsykpcGHkVsfTql9pnzPQss88QQWyLKKesjGgZasKpKGsMNBy1RoYPjmSCMejZ5iAYA7HdGVtR3goCM5jcFmyYUzhRZKT06OXBp5U5gmaXXMZY81lyg3nla+bxf58UgWRnaijmFnTAVpDChGY-E4cX0l6M1YYzApY+gVjiE2vU5gnx6hFJkdMJVws8wjHVk01hMLVB6-o-rRJvGZx2mBAJhSgkhLXQPPFCJyqjTMKOdQ3M2YPh5vGkGjgsiuIvQW1avWDFZyMZIQsuAOAEFGxIaQ3aITpn8pfB8JL0Il0uhzfUjhQXDvjjOd7-Xvu-fYP96krHnlhJctsE4NhFDKT9ioOE8MnKNgcHUbkpo1t9c+5gSQuAeUHkG+WEII2juWuW-j1suxhTQ7DGTmEGbSULL-XaPNL2R3I-WwzyQAA5bOAAFVAeB2CwAIPFCAEBAjwWMv4Fg6um3zHKjCMabYXKDg0HyBZvsI5ZEUKddJP2-tCoSfYzYWQVDnxNDbmhYI8pKIcoC3TCPpdI7zK7jHVIMM44bFhNkMIYQjNkKORsCZjiJM2OkBYunThYkR4lmcAAVHb-jAnPGCVnNn-ROdx+q8K4a5gFlKFNEt44menxsjcnqSwxEMxF9emX-Au3K-V6qXcLbKOGcm+RL7TImxxSDm04gNs4ZeZpBItbyjQ+tKKiJtnfixlfwAHlcD2arc0+K3gS+CAPzUwQx-2Bn8Vg39dIYoQVQWhiem+o4RGyzALRhguRkJPhqL+DM7+DlIkCUA9DDY8RgAwGkwpZkyapCqCCFQnTE4YiIzEQERXQQ46htjcjRZHAVCowR5tBkDYCrhcqtDdyG5oHdAPpX51I0F0FPCCBZyCAwGUBCoVCXiL4ArAKwhEERJWDMLfJRjqAvQcH0H4CMGcqaq17DaDBNqBQkbUqtgYQQIPgzwRh1DDRqb8g1ByG-acEMFZz+C8LFIMFki+iX70bNLyFcE8EOG5gCGPykKcgnDahpAIhwhPiRZ5AQiLx2ANaaB04faoBfZK7+Cq7q6a6kAWTGLv7HbCpja9piCHA5CIwmBwinDqziKKATbupVAuDnDM4YDwBRBxxQDY6N6CAg7SByCxIYSaA2JCDiQQjTI8x6gORIyIJD4EhgCNEf7MgkKVQ8zgjETXjmgpjlBTTnaSGWBS4WyXoFjjEZHcgMxe7Eq+5thdr9gLKVCHBRaOD8jpLCzbGWoHAnC6iLwphjyhZ1DxLJAHBjypDVAXi-HQYdL5KmTla2aVK3F9RhgMwZClzCg1CpC-LJDAhQjzBWLh4bGvZJYMpQpMqeasoPJglnhyIyrp4iLN6GBeTKRbpPgcwxYYjQY+p+r4mHQlxSBqCOIF52CjiEE6ZFR5SVSYRzSdh0kToMlKbx5GAlz9ihF5G9Gr7FCDggjVwUGthyLrH1GvTXqoYik+Yf4ijijoRQgJpVB5CnDXRqBsjyoVCqBzBb5iY-SSa9KMlewlw3zqaPwpgF7zAwwCiXTiKXFDFXF76FrJaG5FZEwlZWY4kgmoCOkNibAb7cjKS3jtglByS2izC+mtijSVCF5UGvQz6xGYAxl4r2AWC1SbA2DqBLAAGLGnB3Y1AlC7ColqlaT5lfbR5FnCqjRlCZAlDllVAQl8gfHzDpCp4p4LLRGo7M4XIHgdlijxqjzHDOKyCylZBlDtjiKlAuSmETny7xGJEEKznzCIinCyCNgZDQg3a2IIhsh-IUTuqKDnpoky5R7o4dnkQt7azMzghmAZ40IihJgigpjGjpgOiBmrQj5+KcDj7dyzmAG8yGguSVBcm9h5CPFhg8hZC-7DG5n76H567kwv4Fjn5vkLDxp6gLCEqSEAFbDoSXYqAxa3zYVPmR6QHZx8FjGilNF7BhzpDiKqB1CXTDR8g6hPj2D9paB+zmG0EKFQBKHMEkVSA8zi42DyD4aXmIDDThgmDqBzAzxTSUHMXUEWEyWMG2FMT2FbGcU6mOC6gIiYRjzcg0pdpzCCj2AQhRiBQXiLQuBAA */
id: 'Modeling',
tsTypes: {} as import('./modelingMachine.typegen').Typegen0,
@ -292,7 +290,7 @@ export const modelingMachine = createMachine(
},
},
entry: ['equip select'],
entry: 'setup client side sketch segments',
},
'Await horizontal distance info': {
@ -381,8 +379,8 @@ export const modelingMachine = createMachine(
'Line tool': {
exit: [
'tear down client sketch',
'setup client side sketch segments',
// 'tear down client sketch',
// 'setup client side sketch segments',
],
on: {
@ -427,6 +425,8 @@ export const modelingMachine = createMachine(
target: 'normal',
actions: 'set up draft line without teardown',
},
Cancel: '#Modeling.Sketch.undo startSketchOn',
},
},
},
@ -445,11 +445,6 @@ export const modelingMachine = createMachine(
},
'Tangential arc to': {
exit: [
'tear down client sketch',
'setup client side sketch segments',
],
entry: 'set up draft arc',
on: {
@ -461,6 +456,14 @@ export const modelingMachine = createMachine(
'Equip Line tool': 'Line tool',
},
},
'undo startSketchOn': {
invoke: {
src: 'AST-undo-startSketchOn',
id: 'AST-undo-startSketchOn',
onDone: '#Modeling.idle',
},
},
},
initial: 'Init',
@ -476,7 +479,7 @@ export const modelingMachine = createMachine(
'remove sketch grid',
],
entry: ['add axis n grid', 'setup client side sketch segments'],
entry: ['add axis n grid', 'conditionally equip line tool'],
},
'Sketch no face': {
@ -602,7 +605,7 @@ export const modelingMachine = createMachine(
if (!sketchPathToNode) return {}
return getSketchMetadataFromPathToNode(sketchPathToNode)
}),
'hide default planes': () => setupSingleton.removeDefaultPlanes(),
'hide default planes': () => sceneInfra.removeDefaultPlanes(),
'reset sketch metadata': assign({
sketchPathToNode: null,
sketchEnginePathId: '',
@ -619,17 +622,7 @@ export const modelingMachine = createMachine(
selectionRanges
)
}),
// TODO figure out why data isn't typed with sketchPathToNode and sketchNormalBackUp
'set new sketch metadata': assign((_, { data }) => data),
'equip select': () =>
engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'set_tool',
tool: 'select',
},
}),
// TODO implement source ranges for all of these constraints
// need to make the async like the modal constraints
'Make selection horizontal': ({ selectionRanges, sketchPathToNode }) => {
@ -639,7 +632,7 @@ export const modelingMachine = createMachine(
kclManager.ast,
kclManager.programMemory
)
clientSideScene.updateAstAndRejigSketch(
sceneEntitiesManager.updateAstAndRejigSketch(
sketchPathToNode || [],
modifiedAst
)
@ -651,7 +644,7 @@ export const modelingMachine = createMachine(
kclManager.ast,
kclManager.programMemory
)
clientSideScene.updateAstAndRejigSketch(
sceneEntitiesManager.updateAstAndRejigSketch(
sketchPathToNode || [],
modifiedAst
)
@ -664,7 +657,7 @@ export const modelingMachine = createMachine(
selectionRanges,
constraint: 'setVertDistance',
})
clientSideScene.updateAstAndRejigSketch(
sceneEntitiesManager.updateAstAndRejigSketch(
sketchPathToNode || [],
modifiedAst
)
@ -674,7 +667,7 @@ export const modelingMachine = createMachine(
selectionRanges,
constraint: 'setHorzDistance',
})
clientSideScene.updateAstAndRejigSketch(
sceneEntitiesManager.updateAstAndRejigSketch(
sketchPathToNode || [],
modifiedAst
)
@ -684,7 +677,7 @@ export const modelingMachine = createMachine(
selectionRanges,
constraint: 'snapToXAxis',
})
clientSideScene.updateAstAndRejigSketch(
sceneEntitiesManager.updateAstAndRejigSketch(
sketchPathToNode || [],
modifiedAst
)
@ -694,7 +687,7 @@ export const modelingMachine = createMachine(
selectionRanges,
constraint: 'snapToYAxis',
})
clientSideScene.updateAstAndRejigSketch(
sceneEntitiesManager.updateAstAndRejigSketch(
sketchPathToNode || [],
modifiedAst
)
@ -703,7 +696,7 @@ export const modelingMachine = createMachine(
const { modifiedAst } = applyConstraintEqualLength({
selectionRanges,
})
clientSideScene.updateAstAndRejigSketch(
sceneEntitiesManager.updateAstAndRejigSketch(
sketchPathToNode || [],
modifiedAst
)
@ -712,7 +705,7 @@ export const modelingMachine = createMachine(
const { modifiedAst } = applyConstraintEqualAngle({
selectionRanges,
})
clientSideScene.updateAstAndRejigSketch(
sceneEntitiesManager.updateAstAndRejigSketch(
sketchPathToNode || [],
modifiedAst
)
@ -724,7 +717,7 @@ export const modelingMachine = createMachine(
const { modifiedAst } = applyRemoveConstrainingValues({
selectionRanges,
})
clientSideScene.updateAstAndRejigSketch(
sceneEntitiesManager.updateAstAndRejigSketch(
sketchPathToNode || [],
modifiedAst
)
@ -747,49 +740,63 @@ export const modelingMachine = createMachine(
focusPath: pathToExtrudeArg,
})
},
'conditionally equip line tool': (_, { type }) => {
if (type === 'done.invoke.animate-to-face') {
sceneInfra.modelingSend('Equip Line tool')
}
},
'setup client side sketch segments': ({ sketchPathToNode }, { type }) => {
if (type !== 'done.invoke.animate-to-face') {
clientSideScene.setupSketch({
if (Object.keys(sceneEntitiesManager.activeSegments).length > 0) {
sceneEntitiesManager
.tearDownSketch({ removeAxis: false })
.then(() => {
sceneEntitiesManager.setupSketch({
sketchPathToNode: sketchPathToNode || [],
})
})
} else {
setupSingleton.modelingSend('Equip Line tool')
sceneEntitiesManager.setupSketch({
sketchPathToNode: sketchPathToNode || [],
})
}
},
'animate after sketch': () => {
clientSideScene.animateAfterSketch()
sceneEntitiesManager.animateAfterSketch()
},
'tear down client sketch': () =>
clientSideScene.tearDownSketch({ removeAxis: false }),
'remove sketch grid': () => clientSideScene.removeSketchGrid(),
'tear down client sketch': () => {
if (sceneEntitiesManager.activeSegments) {
sceneEntitiesManager.tearDownSketch({ removeAxis: false })
}
},
'remove sketch grid': () => sceneEntitiesManager.removeSketchGrid(),
'set up draft line': ({ sketchPathToNode }) => {
clientSideScene.setUpDraftLine(sketchPathToNode || [])
sceneEntitiesManager.setUpDraftLine(sketchPathToNode || [])
},
'set up draft arc': ({ sketchPathToNode }) => {
clientSideScene.setUpDraftArc(sketchPathToNode || [])
sceneEntitiesManager.setUpDraftArc(sketchPathToNode || [])
},
'set up draft line without teardown': ({ sketchPathToNode }) =>
clientSideScene.setupSketch({
sceneEntitiesManager.setupSketch({
sketchPathToNode: sketchPathToNode || [],
draftSegment: 'line',
}),
'show default planes': () => {
setupSingleton.showDefaultPlanes()
clientSideScene.setupDefaultPlaneHover()
sceneInfra.showDefaultPlanes()
sceneEntitiesManager.setupDefaultPlaneHover()
},
'setup noPoints onClick listener': ({ sketchPathToNode }) => {
clientSideScene.createIntersectionPlane()
sceneEntitiesManager.createIntersectionPlane()
const sketchGroup = sketchGroupFromPathToNode({
pathToNode: sketchPathToNode || [],
ast: kclManager.ast,
programMemory: kclManager.programMemory,
})
const quaternion = quaternionFromSketchGroup(sketchGroup)
clientSideScene.intersectionPlane &&
clientSideScene.intersectionPlane.setRotationFromQuaternion(
sceneEntitiesManager.intersectionPlane &&
sceneEntitiesManager.intersectionPlane.setRotationFromQuaternion(
quaternion
)
setupSingleton.setCallbacks({
sceneInfra.setCallbacks({
onClick: async (args) => {
if (!args) return
const { intersection2d } = args
@ -800,13 +807,13 @@ export const modelingMachine = createMachine(
[intersection2d.x, intersection2d.y]
)
await kclManager.updateAst(modifiedAst, false)
clientSideScene.removeIntersectionPlane()
setupSingleton.modelingSend('Add start point')
sceneEntitiesManager.removeIntersectionPlane()
sceneInfra.modelingSend('Add start point')
},
})
},
'add axis n grid': ({ sketchPathToNode }) =>
clientSideScene.createSketchAxis(sketchPathToNode || []),
sceneEntitiesManager.createSketchAxis(sketchPathToNode || []),
},
// end actions
}

View File

@ -22,7 +22,6 @@ const SignIn = () => {
},
} = useGlobalStateContext()
const appliedTheme = theme === Themes.System ? getSystemTheme() : theme
const signInTauri = async () => {
// We want to invoke our command to login via device auth.
try {

View File

@ -862,7 +862,7 @@ impl ExtrudeSurface {
pub fn get_name(&self) -> String {
match self {
ExtrudeSurface::ExtrudePlane(ep) => ep.name.clone(),
ExtrudeSurface::ExtrudePlane(ep) => ep.name.to_string(),
}
}

View File

@ -8,12 +8,11 @@ pub use local::FileManager;
#[cfg(target_arch = "wasm32")]
#[cfg(not(test))]
pub mod wasm;
use anyhow::Result;
#[cfg(target_arch = "wasm32")]
#[cfg(not(test))]
pub use wasm::FileManager;
use anyhow::Result;
#[async_trait::async_trait(?Send)]
pub trait FileSystem: Clone {
/// Read a file from the local file system.

View File

@ -123,7 +123,10 @@ async fn inner_extrude(length: f64, sketch_group: Box<SketchGroup>, args: Args)
}
Ok(Box::new(ExtrudeGroup {
id,
// Ok so you would think that the id would be the id of the extrude group,
// that we passed in to the function, but it's actually the id of the
// sketch group.
id: sketch_group.id,
value: new_value,
height: length,
position: sketch_group.position,

View File

@ -20,9 +20,9 @@ use parse_display::{Display, FromStr};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use self::kcl_stdlib::KclStdLibFn;
use self::{kcl_stdlib::KclStdLibFn, sketch::SketchOnFaceTag};
use crate::{
ast::types::{parse_json_number_as_f64, parse_json_value_as_string},
ast::types::parse_json_number_as_f64,
docs::StdLibFn,
engine::EngineManager,
errors::{KclError, KclErrorDetails},
@ -73,6 +73,7 @@ lazy_static! {
Box::new(crate::std::sketch::BezierCurve),
Box::new(crate::std::sketch::Hole),
Box::new(crate::std::patterns::PatternLinear),
Box::new(crate::std::patterns::PatternCircular),
Box::new(crate::std::import::Import),
Box::new(crate::std::math::Cos),
Box::new(crate::std::math::Sin),
@ -406,7 +407,9 @@ impl Args {
}
}
fn get_data_and_optional_tag<T: serde::de::DeserializeOwned>(&self) -> Result<(T, Option<String>), KclError> {
fn get_data_and_optional_tag<T: serde::de::DeserializeOwned>(
&self,
) -> Result<(T, Option<SketchOnFaceTag>), KclError> {
let first_value = self
.args
.first()
@ -426,8 +429,13 @@ impl Args {
})?;
if let Some(second_value) = self.args.get(1) {
let tag = parse_json_value_as_string(&second_value.get_json_value()?);
Ok((data, tag))
let tag: SketchOnFaceTag = serde_json::from_value(second_value.get_json_value()?).map_err(|e| {
KclError::Type(KclErrorDetails {
message: format!("Failed to deserialize SketchOnFaceTag from JSON: {}", e),
source_ranges: vec![self.source_range],
})
})?;
Ok((data, Some(tag)))
} else {
Ok((data, None))
}

View File

@ -27,6 +27,25 @@ pub struct LinearPatternData {
pub axis: [f64; 3],
}
/// Data for a circular pattern.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub struct CircularPatternData {
/// The number of repetitions. Must be greater than 0.
/// This excludes the original entity. For example, if `repetitions` is 1,
/// the original entity will be copied once.
pub repetitions: usize,
/// The axis around which to make the pattern. This is a 3D vector.
pub axis: [f64; 3],
/// The center about which to make th pattern. This is a 3D vector.
pub center: [f64; 3],
/// The arc angle (in degrees) to place the repetitions. Must be greater than 0.
pub arc_degrees: f64,
/// Whether or not to rotate the duplicates as they are copied.
pub rotate_duplicates: bool,
}
/// A linear pattern.
pub async fn pattern_linear(args: Args) -> Result<MemoryItem, KclError> {
let (data, geometry): (LinearPatternData, Geometry) = args.get_data_and_geometry()?;
@ -47,6 +66,26 @@ pub async fn pattern_linear(args: Args) -> Result<MemoryItem, KclError> {
}
}
/// A circular pattern.
pub async fn pattern_circular(args: Args) -> Result<MemoryItem, KclError> {
let (data, geometry): (CircularPatternData, Geometry) = args.get_data_and_geometry()?;
if data.axis == [0.0, 0.0, 0.0] {
return Err(KclError::Semantic(KclErrorDetails {
message:
"The axis of the circular pattern cannot be the zero vector. Otherwise they will just duplicate in place."
.to_string(),
source_ranges: vec![args.source_range],
}));
}
let new_geometries = inner_pattern_circular(data, geometry, args).await?;
match new_geometries {
Geometries::SketchGroups(sketch_groups) => Ok(MemoryItem::SketchGroups { value: sketch_groups }),
Geometries::ExtrudeGroups(extrude_groups) => Ok(MemoryItem::ExtrudeGroups { value: extrude_groups }),
}
}
/// A linear pattern.
#[stdlib {
name = "patternLinear",
@ -99,3 +138,62 @@ async fn inner_pattern_linear(data: LinearPatternData, geometry: Geometry, args:
Ok(geometries)
}
/// A Circular pattern.
#[stdlib {
name = "patternCircular",
}]
async fn inner_pattern_circular(
data: CircularPatternData,
geometry: Geometry,
args: Args,
) -> Result<Geometries, KclError> {
let id = uuid::Uuid::new_v4();
let resp = args
.send_modeling_cmd(
id,
ModelingCmd::EntityCircularPattern {
axis: data.axis.into(),
entity_id: geometry.id(),
center: data.center.into(),
num_repetitions: data.repetitions as u32,
arc_degrees: data.arc_degrees,
rotate_duplicates: data.rotate_duplicates,
},
)
.await?;
let kittycad::types::OkWebSocketResponseData::Modeling {
modeling_response: kittycad::types::OkModelingCmdResponse::EntityCircularPattern { data: pattern_info },
} = &resp
else {
return Err(KclError::Engine(KclErrorDetails {
message: format!("EntityCircularPattern response was not as expected: {:?}", resp),
source_ranges: vec![args.source_range],
}));
};
let geometries = match geometry {
Geometry::SketchGroup(sketch_group) => {
let mut geometries = vec![sketch_group.clone()];
for id in pattern_info.entity_ids.iter() {
let mut new_sketch_group = sketch_group.clone();
new_sketch_group.id = *id;
geometries.push(new_sketch_group);
}
Geometries::SketchGroups(geometries)
}
Geometry::ExtrudeGroup(extrude_group) => {
let mut geometries = vec![extrude_group.clone()];
for id in pattern_info.entity_ids.iter() {
let mut new_extrude_group = extrude_group.clone();
new_extrude_group.id = *id;
geometries.push(new_extrude_group);
}
Geometries::ExtrudeGroups(geometries)
}
};
Ok(geometries)
}

View File

@ -4,6 +4,7 @@ use anyhow::Result;
use derive_docs::stdlib;
use kittycad::types::{Angle, ModelingCmd, Point3D};
use kittycad_execution_plan_macros::ExecutionPlanValue;
use parse_display::{Display, FromStr};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
@ -813,7 +814,7 @@ impl SketchSurface {
/// Start a sketch on a specific plane or face.
pub async fn start_sketch_on(args: Args) -> Result<MemoryItem, KclError> {
let (data, tag): (SketchData, Option<String>) = args.get_data_and_optional_tag()?;
let (data, tag): (SketchData, Option<SketchOnFaceTag>) = args.get_data_and_optional_tag()?;
match inner_start_sketch_on(data, tag, args).await? {
SketchSurface::Plane(plane) => Ok(MemoryItem::Plane(plane)),
@ -825,7 +826,11 @@ pub async fn start_sketch_on(args: Args) -> Result<MemoryItem, KclError> {
#[stdlib {
name = "startSketchOn",
}]
async fn inner_start_sketch_on(data: SketchData, tag: Option<String>, args: Args) -> Result<SketchSurface, KclError> {
async fn inner_start_sketch_on(
data: SketchData,
tag: Option<SketchOnFaceTag>,
args: Args,
) -> Result<SketchSurface, KclError> {
match data {
SketchData::Plane(plane_data) => {
let plane = start_sketch_on_plane(plane_data, args).await?;
@ -838,18 +843,51 @@ async fn inner_start_sketch_on(data: SketchData, tag: Option<String>, args: Args
source_ranges: vec![args.source_range],
}));
};
let face = start_sketch_on_face(extrude_group, &tag, args).await?;
let face = start_sketch_on_face(extrude_group, tag, args).await?;
Ok(SketchSurface::Face(face))
}
}
}
async fn start_sketch_on_face(extrude_group: Box<ExtrudeGroup>, tag: &str, args: Args) -> Result<Box<Face>, KclError> {
let extrude_plane = extrude_group
/// A tag for sketch on face.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, FromStr, Display)]
#[ts(export)]
#[serde(rename_all = "snake_case", untagged)]
#[display("{0}")]
pub enum SketchOnFaceTag {
StartOrEnd(StartOrEnd),
/// A string tag for the face you want to sketch on.
String(String),
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, FromStr, Display)]
#[ts(export)]
#[serde(rename_all = "snake_case")]
#[display(style = "snake_case")]
pub enum StartOrEnd {
/// The start face as in before you extruded. This could also be known as the bottom
/// face. But we do not call it bottom because it would be the top face if you
/// extruded it in the opposite direction or flipped the camera.
#[serde(rename = "start", alias = "START")]
Start,
/// The end face after you extruded. This could also be known as the top
/// face. But we do not call it top because it would be the bottom face if you
/// extruded it in the opposite direction or flipped the camera.
#[serde(rename = "end", alias = "END")]
End,
}
async fn start_sketch_on_face(
extrude_group: Box<ExtrudeGroup>,
tag: SketchOnFaceTag,
args: Args,
) -> Result<Box<Face>, KclError> {
let extrude_plane_id = match tag {
SketchOnFaceTag::String(ref s) => extrude_group
.value
.iter()
.find_map(|extrude_surface| match extrude_surface {
ExtrudeSurface::ExtrudePlane(extrude_plane) if extrude_plane.name == tag => Some(extrude_plane),
ExtrudeSurface::ExtrudePlane(extrude_plane) if extrude_plane.name == *s => Some(extrude_plane.face_id),
ExtrudeSurface::ExtrudePlane(_) => None,
})
.ok_or_else(|| {
@ -857,7 +895,20 @@ async fn start_sketch_on_face(extrude_group: Box<ExtrudeGroup>, tag: &str, args:
message: format!("Expected a face with the tag `{}`", tag),
source_ranges: vec![args.source_range],
})
})?;
})?,
SketchOnFaceTag::StartOrEnd(StartOrEnd::Start) => extrude_group.start_cap_id.ok_or_else(|| {
KclError::Type(KclErrorDetails {
message: "Expected a start face to sketch on".to_string(),
source_ranges: vec![args.source_range],
})
})?,
SketchOnFaceTag::StartOrEnd(StartOrEnd::End) => extrude_group.end_cap_id.ok_or_else(|| {
KclError::Type(KclErrorDetails {
message: "Expected an end face to sketch on".to_string(),
source_ranges: vec![args.source_range],
})
})?,
};
// Enter sketch mode on the face.
let id = uuid::Uuid::new_v4();
@ -866,7 +917,7 @@ async fn start_sketch_on_face(extrude_group: Box<ExtrudeGroup>, tag: &str, args:
ModelingCmd::EnableSketchMode {
animated: false,
ortho: false,
entity_id: extrude_plane.face_id,
entity_id: extrude_plane_id,
},
)
.await?;
@ -1645,4 +1696,43 @@ mod tests {
let data: PlaneData = serde_json::from_str(&str_json).unwrap();
assert_eq!(data, PlaneData::NegXZ);
}
#[test]
fn test_deserialize_sketch_on_face_tag() {
let data = "start";
let mut str_json = serde_json::to_string(&data).unwrap();
assert_eq!(str_json, "\"start\"");
str_json = "\"end\"".to_string();
let data: crate::std::sketch::SketchOnFaceTag = serde_json::from_str(&str_json).unwrap();
assert_eq!(
data,
crate::std::sketch::SketchOnFaceTag::StartOrEnd(crate::std::sketch::StartOrEnd::End)
);
str_json = "\"thing\"".to_string();
let data: crate::std::sketch::SketchOnFaceTag = serde_json::from_str(&str_json).unwrap();
assert_eq!(data, crate::std::sketch::SketchOnFaceTag::String("thing".to_string()));
str_json = "\"END\"".to_string();
let data: crate::std::sketch::SketchOnFaceTag = serde_json::from_str(&str_json).unwrap();
assert_eq!(
data,
crate::std::sketch::SketchOnFaceTag::StartOrEnd(crate::std::sketch::StartOrEnd::End)
);
str_json = "\"start\"".to_string();
let data: crate::std::sketch::SketchOnFaceTag = serde_json::from_str(&str_json).unwrap();
assert_eq!(
data,
crate::std::sketch::SketchOnFaceTag::StartOrEnd(crate::std::sketch::StartOrEnd::Start)
);
str_json = "\"START\"".to_string();
let data: crate::std::sketch::SketchOnFaceTag = serde_json::from_str(&str_json).unwrap();
assert_eq!(
data,
crate::std::sketch::SketchOnFaceTag::StartOrEnd(crate::std::sketch::StartOrEnd::Start)
);
}
}

View File

@ -91,6 +91,62 @@ const part002 = startSketchOn(part001, "here")
twenty_twenty::assert_image("tests/executor/outputs/sketch_on_face.png", &result, 0.999);
}
#[tokio::test(flavor = "multi_thread")]
async fn serial_test_sketch_on_face_start() {
let code = r#"fn cube = (pos, scale) => {
const sg = startSketchOn('XY')
|> startProfileAt(pos, %)
|> line([0, scale], %)
|> line([scale, 0], %)
|> line([0, -scale], %)
return sg
}
const part001 = cube([0,0], 20)
|> close(%)
|> extrude(20, %)
const part002 = startSketchOn(part001, "start")
|> startProfileAt([0, 0], %)
|> line([0, 10], %)
|> line([10, 0], %)
|> line([0, -10], %)
|> close(%)
|> extrude(5, %)
"#;
let result = execute_and_snapshot(code).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/sketch_on_face_start.png", &result, 0.999);
}
#[tokio::test(flavor = "multi_thread")]
async fn serial_test_sketch_on_face_end() {
let code = r#"fn cube = (pos, scale) => {
const sg = startSketchOn('XY')
|> startProfileAt(pos, %)
|> line([0, scale], %)
|> line([scale, 0], %)
|> line([0, -scale], %)
return sg
}
const part001 = cube([0,0], 20)
|> close(%)
|> extrude(20, %)
const part002 = startSketchOn(part001, "END")
|> startProfileAt([0, 0], %)
|> line([0, 10], %)
|> line([10, 0], %)
|> line([0, -10], %)
|> close(%)
|> extrude(5, %)
"#;
let result = execute_and_snapshot(code).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/sketch_on_face_end.png", &result, 0.999);
}
#[tokio::test(flavor = "multi_thread")]
async fn serial_test_execute_with_function_sketch() {
let code = r#"fn box = (h, l, w) => {
@ -712,6 +768,88 @@ const rectangle = startSketchOn('XY')
twenty_twenty::assert_image("tests/executor/outputs/patterns_linear_basic_holes.png", &result, 0.999);
}
#[tokio::test(flavor = "multi_thread")]
async fn serial_test_patterns_circular_basic_2d() {
let code = r#"fn circle = (pos, radius) => {
const sg = startSketchOn('XY')
|> startProfileAt([pos[0] + radius, pos[1]], %)
|> arc({
angle_end: 360,
angle_start: 0,
radius: radius
}, %)
|> close(%)
return sg
}
const part = circle([0,0], 2)
|> patternCircular({axis: [0,0,1], center: [20, 20, 20], repetitions: 12, arcDegrees: 210, rotateDuplicates: true}, %)
"#;
let result = execute_and_snapshot(code).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/patterns_circular_basic_2d.png", &result, 0.999);
}
#[tokio::test(flavor = "multi_thread")]
async fn serial_test_patterns_circular_basic_3d() {
let code = r#"fn circle = (pos, radius) => {
const sg = startSketchOn('XY')
|> startProfileAt([pos[0] + radius, pos[1]], %)
|> arc({
angle_end: 360,
angle_start: 0,
radius: radius
}, %)
|> close(%)
return sg
}
const part = startSketchOn('XY')
|> startProfileAt([0, 0], %)
|> line([0,1], %)
|> line([1, 0], %)
|> line([0, -1], %)
|> close(%)
|> extrude(1, %)
|> patternCircular({axis: [0,1,0], center: [-20, -20, -20], repetitions: 40, arcDegrees: 360, rotateDuplicates: false}, %)
"#;
let result = execute_and_snapshot(code).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/patterns_circular_basic_3d.png", &result, 0.999);
}
#[tokio::test(flavor = "multi_thread")]
async fn serial_test_patterns_circular_3d_tilted_axis() {
let code = r#"fn circle = (pos, radius) => {
const sg = startSketchOn('XY')
|> startProfileAt([pos[0] + radius, pos[1]], %)
|> arc({
angle_end: 360,
angle_start: 0,
radius: radius
}, %)
|> close(%)
return sg
}
const part = startSketchOn('XY')
|> startProfileAt([0, 0], %)
|> line([0,1], %)
|> line([1, 0], %)
|> line([0, -1], %)
|> close(%)
|> extrude(1, %)
|> patternCircular({axis: [1,1,-1], center: [10, 0, 10], repetitions: 10, arcDegrees: 360, rotateDuplicates: true}, %)
"#;
let result = execute_and_snapshot(code).await.unwrap();
twenty_twenty::assert_image(
"tests/executor/outputs/patterns_circular_3d_tilted_axis.png",
&result,
0.999,
);
}
#[tokio::test(flavor = "multi_thread")]
async fn serial_test_import_file_doesnt_exist() {
let code = r#"const model = import("thing.obj")"#;

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB