Merge branch 'main' into pierremtb/issue1349
This commit is contained in:
@ -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
16
docs/kcl/KNOWN-ISSUES.md
Normal 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.
|
||||
1793
docs/kcl/std.json
1793
docs/kcl/std.json
File diff suppressed because it is too large
Load Diff
197
docs/kcl/std.md
197
docs/kcl/std.md
@ -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
|
||||
|
||||
|
||||
30
src/App.tsx
30
src/App.tsx
@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
252
src/clientSideScene/ClientSideSceneComp.tsx
Normal file
252
src/clientSideScene/ClientSideSceneComp.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { Quaternion } from 'three'
|
||||
import { isQuaternionVertical } from './setup'
|
||||
import { isQuaternionVertical } from './sceneInfra'
|
||||
|
||||
describe('isQuaternionVertical', () => {
|
||||
it('should identify vertical quaternions', () => {
|
||||
@ -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
|
||||
@ -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,
|
||||
|
||||
@ -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)
|
||||
}}
|
||||
|
||||
@ -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 (
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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)
|
||||
},
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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']
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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))
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 |
BIN
src/wasm-lib/tests/executor/outputs/sketch_on_face_end.png
Normal file
BIN
src/wasm-lib/tests/executor/outputs/sketch_on_face_end.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 35 KiB |
BIN
src/wasm-lib/tests/executor/outputs/sketch_on_face_start.png
Normal file
BIN
src/wasm-lib/tests/executor/outputs/sketch_on_face_start.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 36 KiB |
Reference in New Issue
Block a user