2025-04-01 23:54:26 -07:00
|
|
|
import toast from 'react-hot-toast'
|
|
|
|
import type {
|
|
|
|
Intersection,
|
|
|
|
Object3D,
|
|
|
|
Object3DEventMap,
|
|
|
|
Quaternion,
|
|
|
|
} from 'three'
|
2025-04-14 23:51:14 +02:00
|
|
|
|
2024-02-11 12:59:00 +11:00
|
|
|
import {
|
|
|
|
BoxGeometry,
|
|
|
|
DoubleSide,
|
2025-04-01 23:54:26 -07:00
|
|
|
ExtrudeGeometry,
|
2024-02-11 12:59:00 +11:00
|
|
|
Group,
|
2025-04-01 23:54:26 -07:00
|
|
|
LineCurve3,
|
2024-02-11 12:59:00 +11:00
|
|
|
Mesh,
|
|
|
|
MeshBasicMaterial,
|
|
|
|
OrthographicCamera,
|
|
|
|
PerspectiveCamera,
|
|
|
|
PlaneGeometry,
|
2024-04-03 13:22:56 +11:00
|
|
|
Points,
|
2025-04-01 23:54:26 -07:00
|
|
|
Shape,
|
2024-02-11 12:59:00 +11:00
|
|
|
Vector2,
|
|
|
|
Vector3,
|
|
|
|
} from 'three'
|
2025-04-01 23:54:26 -07:00
|
|
|
import { CSS2DObject } from 'three/examples/jsm/renderers/CSS2DRenderer'
|
|
|
|
import { radToDeg } from 'three/src/math/MathUtils'
|
|
|
|
|
|
|
|
import type { Models } from '@kittycad/lib/dist/types/src'
|
|
|
|
import type { CallExpression } from '@rust/kcl-lib/bindings/CallExpression'
|
|
|
|
import type { CallExpressionKw } from '@rust/kcl-lib/bindings/CallExpressionKw'
|
|
|
|
import type { Node } from '@rust/kcl-lib/bindings/Node'
|
|
|
|
import type { Path } from '@rust/kcl-lib/bindings/Path'
|
|
|
|
import type { PipeExpression } from '@rust/kcl-lib/bindings/PipeExpression'
|
|
|
|
import type { Program } from '@rust/kcl-lib/bindings/Program'
|
|
|
|
import type { Sketch } from '@rust/kcl-lib/bindings/Sketch'
|
|
|
|
import type { SourceRange } from '@rust/kcl-lib/bindings/SourceRange'
|
|
|
|
import type { VariableDeclaration } from '@rust/kcl-lib/bindings/VariableDeclaration'
|
|
|
|
import type { VariableDeclarator } from '@rust/kcl-lib/bindings/VariableDeclarator'
|
2025-04-14 23:51:14 +02:00
|
|
|
import type { SafeArray } from '@src/lib/utils'
|
|
|
|
import { getAngle, getLength, uuidv4 } from '@src/lib/utils'
|
2025-04-01 23:54:26 -07:00
|
|
|
|
|
|
|
import {
|
|
|
|
createGridHelper,
|
|
|
|
isQuaternionVertical,
|
|
|
|
orthoScale,
|
|
|
|
perspScale,
|
|
|
|
quaternionFromUpNForward,
|
|
|
|
} from '@src/clientSideScene/helpers'
|
|
|
|
import {
|
|
|
|
ARC_ANGLE_END,
|
|
|
|
ARC_SEGMENT,
|
2025-04-14 23:51:14 +02:00
|
|
|
ARC_SEGMENT_TYPES,
|
2025-04-01 23:54:26 -07:00
|
|
|
CIRCLE_CENTER_HANDLE,
|
|
|
|
CIRCLE_SEGMENT,
|
|
|
|
CIRCLE_THREE_POINT_HANDLE1,
|
|
|
|
CIRCLE_THREE_POINT_HANDLE2,
|
|
|
|
CIRCLE_THREE_POINT_HANDLE3,
|
|
|
|
CIRCLE_THREE_POINT_SEGMENT,
|
|
|
|
DRAFT_DASHED_LINE,
|
|
|
|
EXTRA_SEGMENT_HANDLE,
|
|
|
|
PROFILE_START,
|
|
|
|
SEGMENT_BODIES,
|
|
|
|
SEGMENT_BODIES_PLUS_PROFILE_START,
|
|
|
|
SEGMENT_WIDTH_PX,
|
|
|
|
STRAIGHT_SEGMENT,
|
|
|
|
STRAIGHT_SEGMENT_DASH,
|
|
|
|
TANGENTIAL_ARC_TO_SEGMENT,
|
|
|
|
THREE_POINT_ARC_HANDLE2,
|
|
|
|
THREE_POINT_ARC_HANDLE3,
|
|
|
|
THREE_POINT_ARC_SEGMENT,
|
2025-04-16 00:11:25 +10:00
|
|
|
getParentGroup,
|
2025-04-01 23:54:26 -07:00
|
|
|
} from '@src/clientSideScene/sceneConstants'
|
|
|
|
import type {
|
|
|
|
OnClickCallbackArgs,
|
|
|
|
OnMouseEnterLeaveArgs,
|
|
|
|
SceneInfra,
|
|
|
|
} from '@src/clientSideScene/sceneInfra'
|
2025-04-14 23:51:14 +02:00
|
|
|
|
2024-02-11 12:59:00 +11:00
|
|
|
import {
|
2024-11-13 09:41:27 -05:00
|
|
|
ANGLE_SNAP_THRESHOLD_DEGREES,
|
2024-02-11 12:59:00 +11:00
|
|
|
ARROWHEAD,
|
|
|
|
AXIS_GROUP,
|
2024-10-31 07:04:38 -07:00
|
|
|
DRAFT_POINT,
|
|
|
|
DRAFT_POINT_GROUP,
|
2024-02-11 12:59:00 +11:00
|
|
|
INTERSECTION_PLANE_LAYER,
|
|
|
|
RAYCASTABLE_PLANE,
|
|
|
|
SKETCH_GROUP_SEGMENTS,
|
|
|
|
SKETCH_LAYER,
|
|
|
|
X_AXIS,
|
|
|
|
Y_AXIS,
|
2025-04-16 00:11:25 +10:00
|
|
|
getSceneScale,
|
2025-04-02 15:10:57 -07:00
|
|
|
} from '@src/clientSideScene/sceneUtils'
|
2025-04-01 23:54:26 -07:00
|
|
|
import type { SegmentUtils } from '@src/clientSideScene/segments'
|
2024-02-11 12:59:00 +11:00
|
|
|
import {
|
2024-09-13 21:14:14 +10:00
|
|
|
createProfileStartHandle,
|
2025-03-18 11:14:12 +11:00
|
|
|
dashedStraight,
|
2025-04-14 23:51:14 +02:00
|
|
|
getTanPreviousPoint,
|
2024-09-13 21:14:14 +10:00
|
|
|
segmentUtils,
|
2025-04-01 23:54:26 -07:00
|
|
|
} from '@src/clientSideScene/segments'
|
|
|
|
import type EditorManager from '@src/editor/manager'
|
2025-04-16 00:11:25 +10:00
|
|
|
import type { KclManager } from '@src/lang/KclSingleton'
|
2025-04-01 23:54:26 -07:00
|
|
|
import type CodeManager from '@src/lang/codeManager'
|
2025-04-25 16:01:35 -05:00
|
|
|
import { ARG_END, ARG_AT, ARG_END_ABSOLUTE } from '@src/lang/constants'
|
2025-04-01 15:31:19 -07:00
|
|
|
import {
|
|
|
|
createArrayExpression,
|
|
|
|
createCallExpressionStdLibKw,
|
|
|
|
createLabeledArg,
|
|
|
|
createLiteral,
|
2025-04-01 23:54:26 -07:00
|
|
|
createLocalName,
|
2025-04-01 15:31:19 -07:00
|
|
|
createPipeExpression,
|
|
|
|
createPipeSubstitution,
|
|
|
|
createVariableDeclaration,
|
|
|
|
findUniqueName,
|
2025-04-01 23:54:26 -07:00
|
|
|
} from '@src/lang/create'
|
|
|
|
import type { ToolTip } from '@src/lang/langHelpers'
|
|
|
|
import { executeAstMock } from '@src/lang/langHelpers'
|
|
|
|
import { updateModelingState } from '@src/lang/modelingWorkflows'
|
|
|
|
import {
|
|
|
|
createNodeFromExprSnippet,
|
2025-04-01 15:31:19 -07:00
|
|
|
getInsertIndex,
|
|
|
|
insertNewStartProfileAt,
|
|
|
|
updateSketchNodePathsWithInsertIndex,
|
2025-04-01 23:54:26 -07:00
|
|
|
} from '@src/lang/modifyAst'
|
2025-04-14 23:51:14 +02:00
|
|
|
import { mutateAstWithTagForSketchSegment } from '@src/lang/modifyAst/addEdgeTreatment'
|
2025-04-01 23:54:26 -07:00
|
|
|
import { getNodeFromPath } from '@src/lang/queryAst'
|
|
|
|
import { getNodePathFromSourceRange } from '@src/lang/queryAstNodePathUtils'
|
2025-04-01 15:31:19 -07:00
|
|
|
import {
|
2025-04-01 23:54:26 -07:00
|
|
|
codeRefFromRange,
|
|
|
|
getArtifactFromRange,
|
|
|
|
} from '@src/lang/std/artifactGraph'
|
|
|
|
import type { EngineCommandManager } from '@src/lang/std/engineConnection'
|
|
|
|
import type { Coords2d } from '@src/lang/std/sketch'
|
|
|
|
import {
|
|
|
|
addCallExpressionsToPipe,
|
|
|
|
addCloseToPipe,
|
|
|
|
addNewSketchLn,
|
|
|
|
changeSketchArguments,
|
|
|
|
updateStartProfileAtArgs,
|
|
|
|
} from '@src/lang/std/sketch'
|
|
|
|
import type { SegmentInputs } from '@src/lang/std/stdTypes'
|
2025-04-24 22:01:27 +12:00
|
|
|
import { crossProduct, topLevelRange } from '@src/lang/util'
|
2025-04-01 23:54:26 -07:00
|
|
|
import type { PathToNode, VariableMap } from '@src/lang/wasm'
|
|
|
|
import {
|
|
|
|
defaultSourceRange,
|
2025-04-14 23:51:14 +02:00
|
|
|
getTangentialArcToInfo,
|
2025-04-01 23:54:26 -07:00
|
|
|
parse,
|
|
|
|
recast,
|
|
|
|
resultIsOk,
|
|
|
|
sketchFromKclValue,
|
|
|
|
sourceRangeFromRust,
|
|
|
|
} from '@src/lang/wasm'
|
|
|
|
import { EXECUTION_TYPE_MOCK } from '@src/lib/constants'
|
|
|
|
import {
|
|
|
|
getRectangleCallExpressions,
|
|
|
|
updateCenterRectangleSketch,
|
|
|
|
updateRectangleSketch,
|
|
|
|
} from '@src/lib/rectangleTool'
|
|
|
|
import type RustContext from '@src/lib/rustContext'
|
|
|
|
import type { Selections } from '@src/lib/selections'
|
|
|
|
import { getEventForSegmentSelection } from '@src/lib/selections'
|
|
|
|
import type { Themes } from '@src/lib/theme'
|
|
|
|
import { getThemeColorForThreeJs } from '@src/lib/theme'
|
|
|
|
import { err, reportRejection, trap } from '@src/lib/trap'
|
|
|
|
import { isArray, isOverlap, roundOff } from '@src/lib/utils'
|
2025-04-14 23:51:14 +02:00
|
|
|
import { closestPointOnRay, deg2Rad } from '@src/lib/utils2d'
|
2025-04-01 23:54:26 -07:00
|
|
|
import type {
|
2025-04-01 15:31:19 -07:00
|
|
|
SegmentOverlayPayload,
|
|
|
|
SketchDetails,
|
|
|
|
SketchDetailsUpdate,
|
|
|
|
SketchTool,
|
2025-04-01 23:54:26 -07:00
|
|
|
} from '@src/machines/modelingMachine'
|
2025-04-14 23:51:14 +02:00
|
|
|
import { calculateIntersectionOfTwoLines } from 'sketch-helpers'
|
2024-02-11 12:59:00 +11:00
|
|
|
|
2025-04-11 14:17:20 -04:00
|
|
|
type DraftSegment = 'line' | 'tangentialArc'
|
2024-02-11 12:59:00 +11:00
|
|
|
|
2024-08-22 16:08:49 -04:00
|
|
|
type Vec3Array = [number, number, number]
|
|
|
|
|
2024-02-14 08:03:20 +11:00
|
|
|
// 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
|
2024-03-22 16:55:30 +11:00
|
|
|
export class SceneEntities {
|
2025-03-21 01:55:20 +01:00
|
|
|
readonly engineCommandManager: EngineCommandManager
|
2025-04-01 23:54:26 -07:00
|
|
|
readonly sceneInfra: SceneInfra
|
|
|
|
readonly editorManager: EditorManager
|
|
|
|
readonly codeManager: CodeManager
|
|
|
|
readonly kclManager: KclManager
|
|
|
|
readonly rustContext: RustContext
|
2024-02-11 12:59:00 +11:00
|
|
|
activeSegments: { [key: string]: Group } = {}
|
2025-03-21 01:55:20 +01:00
|
|
|
readonly intersectionPlane: Mesh
|
2024-02-11 12:59:00 +11:00
|
|
|
axisGroup: Group | null = null
|
2024-11-22 11:05:04 -05:00
|
|
|
draftPointGroups: Group[] = []
|
2024-02-11 12:59:00 +11:00
|
|
|
currentSketchQuaternion: Quaternion | null = null
|
2025-04-14 23:51:14 +02:00
|
|
|
|
2025-04-01 23:54:26 -07:00
|
|
|
constructor(
|
|
|
|
engineCommandManager: EngineCommandManager,
|
|
|
|
sceneInfra: SceneInfra,
|
|
|
|
editorManager: EditorManager,
|
|
|
|
codeManager: CodeManager,
|
|
|
|
kclManager: KclManager,
|
|
|
|
rustContext: RustContext
|
|
|
|
) {
|
2024-03-22 16:55:30 +11:00
|
|
|
this.engineCommandManager = engineCommandManager
|
2025-04-01 23:54:26 -07:00
|
|
|
this.sceneInfra = sceneInfra
|
|
|
|
this.editorManager = editorManager
|
|
|
|
this.codeManager = codeManager
|
|
|
|
this.kclManager = kclManager
|
|
|
|
this.rustContext = rustContext
|
|
|
|
this.intersectionPlane = SceneEntities.createIntersectionPlane(
|
|
|
|
this.sceneInfra
|
|
|
|
)
|
|
|
|
this.sceneInfra.camControls.subscribeToCamChange(this.onCamChange)
|
2024-04-03 13:22:56 +11:00
|
|
|
window.addEventListener('resize', this.onWindowResize)
|
2024-02-11 12:59:00 +11:00
|
|
|
}
|
|
|
|
|
2024-04-03 13:22:56 +11:00
|
|
|
onWindowResize = () => {
|
|
|
|
this.onCamChange()
|
|
|
|
}
|
2024-02-11 12:59:00 +11:00
|
|
|
onCamChange = () => {
|
2025-04-01 23:54:26 -07:00
|
|
|
const orthoFactor = orthoScale(this.sceneInfra.camControls.camera)
|
2024-05-24 20:54:42 +10:00
|
|
|
const callbacks: (() => SegmentOverlayPayload | null)[] = []
|
|
|
|
Object.values(this.activeSegments).forEach((segment, index) => {
|
2024-02-11 12:59:00 +11:00
|
|
|
const factor =
|
2025-04-01 23:54:26 -07:00
|
|
|
(this.sceneInfra.camControls.camera instanceof OrthographicCamera
|
2024-02-11 12:59:00 +11:00
|
|
|
? orthoFactor
|
2025-04-01 23:54:26 -07:00
|
|
|
: perspScale(this.sceneInfra.camControls.camera, segment)) /
|
|
|
|
this.sceneInfra._baseUnitMultiplier
|
2024-09-23 22:42:51 +10:00
|
|
|
let input: SegmentInputs = {
|
2024-09-13 21:14:14 +10:00
|
|
|
type: 'straight-segment',
|
|
|
|
from: segment.userData.from,
|
|
|
|
to: segment.userData.to,
|
2024-09-23 22:42:51 +10:00
|
|
|
}
|
2024-09-13 21:14:14 +10:00
|
|
|
let update: SegmentUtils['update'] | null = null
|
2024-02-11 12:59:00 +11:00
|
|
|
if (
|
|
|
|
segment.userData.from &&
|
|
|
|
segment.userData.to &&
|
|
|
|
segment.userData.type === STRAIGHT_SEGMENT
|
|
|
|
) {
|
2024-09-13 21:14:14 +10:00
|
|
|
update = segmentUtils.straight.update
|
2024-02-11 12:59:00 +11:00
|
|
|
}
|
|
|
|
if (
|
|
|
|
segment.userData.from &&
|
|
|
|
segment.userData.to &&
|
|
|
|
segment.userData.prevSegment &&
|
|
|
|
segment.userData.type === TANGENTIAL_ARC_TO_SEGMENT
|
|
|
|
) {
|
2025-04-11 14:17:20 -04:00
|
|
|
update = segmentUtils.tangentialArc.update
|
2024-02-11 12:59:00 +11:00
|
|
|
}
|
2024-09-23 22:42:51 +10:00
|
|
|
if (
|
2025-03-18 11:14:12 +11:00
|
|
|
segment.userData &&
|
2024-09-23 22:42:51 +10:00
|
|
|
segment.userData.from &&
|
|
|
|
segment.userData.center &&
|
|
|
|
segment.userData.radius &&
|
|
|
|
segment.userData.type === CIRCLE_SEGMENT
|
|
|
|
) {
|
|
|
|
update = segmentUtils.circle.update
|
|
|
|
input = {
|
|
|
|
type: 'arc-segment',
|
|
|
|
from: segment.userData.from,
|
2025-03-18 11:14:12 +11:00
|
|
|
to: segment.userData.from,
|
|
|
|
center: segment.userData.center,
|
|
|
|
radius: segment.userData.radius,
|
|
|
|
ccw: true,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (
|
|
|
|
segment.userData &&
|
|
|
|
segment.userData.from &&
|
|
|
|
segment.userData.center &&
|
|
|
|
segment.userData.radius &&
|
|
|
|
segment.userData.to &&
|
|
|
|
segment.userData.type === ARC_SEGMENT
|
|
|
|
) {
|
|
|
|
update = segmentUtils.arc.update
|
|
|
|
input = {
|
|
|
|
type: 'arc-segment',
|
|
|
|
from: segment.userData.from,
|
|
|
|
to: segment.userData.to,
|
2024-09-23 22:42:51 +10:00
|
|
|
center: segment.userData.center,
|
|
|
|
radius: segment.userData.radius,
|
2025-03-18 11:14:12 +11:00
|
|
|
ccw: segment.userData.ccw,
|
2024-09-23 22:42:51 +10:00
|
|
|
}
|
|
|
|
}
|
2025-02-15 00:57:04 +11:00
|
|
|
if (
|
|
|
|
segment.userData.p1 &&
|
|
|
|
segment.userData.p2 &&
|
|
|
|
segment.userData.p3 &&
|
|
|
|
segment.userData.type === CIRCLE_THREE_POINT_SEGMENT
|
|
|
|
) {
|
|
|
|
update = segmentUtils.circleThreePoint.update
|
|
|
|
input = {
|
|
|
|
type: 'circle-three-point-segment',
|
|
|
|
p1: segment.userData.p1,
|
|
|
|
p2: segment.userData.p2,
|
|
|
|
p3: segment.userData.p3,
|
|
|
|
}
|
|
|
|
}
|
2025-03-18 11:14:12 +11:00
|
|
|
if (
|
|
|
|
segment.userData &&
|
|
|
|
segment.userData.from &&
|
|
|
|
segment.userData.center &&
|
|
|
|
segment.userData.radius &&
|
|
|
|
segment.userData.to &&
|
|
|
|
segment.userData.type === THREE_POINT_ARC_SEGMENT
|
|
|
|
) {
|
|
|
|
update = segmentUtils.threePointArc.update
|
|
|
|
input = {
|
|
|
|
type: 'circle-three-point-segment',
|
|
|
|
p1: segment.userData.p1,
|
|
|
|
p2: segment.userData.p2,
|
|
|
|
p3: segment.userData.p3,
|
|
|
|
}
|
|
|
|
}
|
2024-09-23 22:42:51 +10:00
|
|
|
|
2024-09-13 21:14:14 +10:00
|
|
|
const callBack = update?.({
|
|
|
|
prevSegment: segment.userData.prevSegment,
|
|
|
|
input,
|
|
|
|
group: segment,
|
|
|
|
scale: factor,
|
2025-04-01 23:54:26 -07:00
|
|
|
sceneInfra: this.sceneInfra,
|
2024-09-13 21:14:14 +10:00
|
|
|
})
|
|
|
|
callBack && !err(callBack) && callbacks.push(callBack)
|
2024-03-02 08:48:30 +11:00
|
|
|
if (segment.name === PROFILE_START) {
|
|
|
|
segment.scale.set(factor, factor, factor)
|
|
|
|
}
|
2024-02-11 12:59:00 +11:00
|
|
|
})
|
|
|
|
if (this.axisGroup) {
|
|
|
|
const factor =
|
2025-04-01 23:54:26 -07:00
|
|
|
this.sceneInfra.camControls.camera instanceof OrthographicCamera
|
2024-02-11 12:59:00 +11:00
|
|
|
? orthoFactor
|
2025-04-01 23:54:26 -07:00
|
|
|
: perspScale(this.sceneInfra.camControls.camera, this.axisGroup)
|
2024-02-11 12:59:00 +11:00
|
|
|
const x = this.axisGroup.getObjectByName(X_AXIS)
|
2025-04-01 23:54:26 -07:00
|
|
|
x?.scale.set(1, factor / this.sceneInfra._baseUnitMultiplier, 1)
|
2024-02-11 12:59:00 +11:00
|
|
|
const y = this.axisGroup.getObjectByName(Y_AXIS)
|
2025-04-01 23:54:26 -07:00
|
|
|
y?.scale.set(factor / this.sceneInfra._baseUnitMultiplier, 1, 1)
|
2024-02-11 12:59:00 +11:00
|
|
|
}
|
2025-04-01 23:54:26 -07:00
|
|
|
this.sceneInfra.overlayCallbacks(callbacks)
|
2024-02-11 12:59:00 +11:00
|
|
|
}
|
|
|
|
|
2025-04-01 23:54:26 -07:00
|
|
|
private static createIntersectionPlane(sceneInfra: SceneInfra) {
|
2024-03-22 10:23:04 +11:00
|
|
|
const hundredM = 100_0000
|
2024-02-26 19:53:44 +11:00
|
|
|
const planeGeometry = new PlaneGeometry(hundredM, hundredM)
|
2024-02-11 12:59:00 +11:00
|
|
|
const planeMaterial = new MeshBasicMaterial({
|
|
|
|
color: 0xff0000,
|
|
|
|
side: DoubleSide,
|
|
|
|
transparent: true,
|
|
|
|
opacity: 0.5,
|
|
|
|
})
|
2025-03-21 01:55:20 +01:00
|
|
|
const intersectionPlane = new Mesh(planeGeometry, planeMaterial)
|
|
|
|
intersectionPlane.userData = { type: RAYCASTABLE_PLANE }
|
|
|
|
intersectionPlane.name = RAYCASTABLE_PLANE
|
|
|
|
intersectionPlane.layers.set(INTERSECTION_PLANE_LAYER)
|
|
|
|
sceneInfra.scene.add(intersectionPlane)
|
|
|
|
return intersectionPlane
|
2024-02-11 12:59:00 +11:00
|
|
|
}
|
2025-04-14 23:51:14 +02:00
|
|
|
|
2024-03-22 10:23:04 +11:00
|
|
|
createSketchAxis(
|
|
|
|
sketchPathToNode: PathToNode,
|
|
|
|
forward: [number, number, number],
|
|
|
|
up: [number, number, number],
|
|
|
|
sketchPosition?: [number, number, number]
|
|
|
|
) {
|
2025-04-01 23:54:26 -07:00
|
|
|
const orthoFactor = orthoScale(this.sceneInfra.camControls.camera)
|
2024-02-11 12:59:00 +11:00
|
|
|
const baseXColor = 0x000055
|
|
|
|
const baseYColor = 0x550000
|
2024-04-22 20:14:06 +10:00
|
|
|
const axisPixelWidth = 1.6
|
|
|
|
const xAxisGeometry = new BoxGeometry(100000, axisPixelWidth, 0.01)
|
|
|
|
const yAxisGeometry = new BoxGeometry(axisPixelWidth, 100000, 0.01)
|
2024-02-11 12:59:00 +11:00
|
|
|
const xAxisMaterial = new MeshBasicMaterial({
|
|
|
|
color: baseXColor,
|
|
|
|
depthTest: false,
|
|
|
|
})
|
|
|
|
const yAxisMaterial = new MeshBasicMaterial({
|
|
|
|
color: baseYColor,
|
|
|
|
depthTest: false,
|
|
|
|
})
|
|
|
|
const xAxisMesh = new Mesh(xAxisGeometry, xAxisMaterial)
|
|
|
|
const yAxisMesh = new Mesh(yAxisGeometry, yAxisMaterial)
|
|
|
|
xAxisMesh.renderOrder = -2
|
|
|
|
yAxisMesh.renderOrder = -1
|
2025-03-18 03:00:21 +01:00
|
|
|
|
|
|
|
// This makes sure axis lines are picked after segment lines in case of overlapping
|
|
|
|
xAxisMesh.position.z = -0.1
|
|
|
|
yAxisMesh.position.z = -0.1
|
|
|
|
|
2024-02-11 12:59:00 +11:00
|
|
|
xAxisMesh.userData = {
|
|
|
|
type: X_AXIS,
|
|
|
|
baseColor: baseXColor,
|
|
|
|
isSelected: false,
|
|
|
|
}
|
|
|
|
yAxisMesh.userData = {
|
|
|
|
type: Y_AXIS,
|
|
|
|
baseColor: baseYColor,
|
|
|
|
isSelected: false,
|
|
|
|
}
|
|
|
|
xAxisMesh.name = X_AXIS
|
|
|
|
yAxisMesh.name = Y_AXIS
|
|
|
|
|
|
|
|
this.axisGroup = new Group()
|
|
|
|
const gridHelper = createGridHelper({ size: 100, divisions: 10 })
|
2024-02-26 19:53:44 +11:00
|
|
|
gridHelper.position.z = -0.01
|
2024-02-11 12:59:00 +11:00
|
|
|
gridHelper.renderOrder = -3 // is this working?
|
2024-02-17 07:04:24 +11:00
|
|
|
gridHelper.name = 'gridHelper'
|
|
|
|
const sceneScale = getSceneScale(
|
2025-04-01 23:54:26 -07:00
|
|
|
this.sceneInfra.camControls.camera,
|
|
|
|
this.sceneInfra.camControls.target
|
2024-02-17 07:04:24 +11:00
|
|
|
)
|
|
|
|
gridHelper.scale.set(sceneScale, sceneScale, sceneScale)
|
2024-03-01 06:55:49 +11:00
|
|
|
|
|
|
|
const factor =
|
2025-04-01 23:54:26 -07:00
|
|
|
this.sceneInfra.camControls.camera instanceof OrthographicCamera
|
2024-03-01 06:55:49 +11:00
|
|
|
? orthoFactor
|
2025-04-01 23:54:26 -07:00
|
|
|
: perspScale(this.sceneInfra.camControls.camera, this.axisGroup)
|
|
|
|
xAxisMesh?.scale.set(1, factor / this.sceneInfra._baseUnitMultiplier, 1)
|
|
|
|
yAxisMesh?.scale.set(factor / this.sceneInfra._baseUnitMultiplier, 1, 1)
|
2024-03-01 06:55:49 +11:00
|
|
|
|
2024-02-11 12:59:00 +11:00
|
|
|
this.axisGroup.add(xAxisMesh, yAxisMesh, gridHelper)
|
|
|
|
this.currentSketchQuaternion &&
|
|
|
|
this.axisGroup.setRotationFromQuaternion(this.currentSketchQuaternion)
|
|
|
|
|
|
|
|
this.axisGroup.userData = { type: AXIS_GROUP }
|
2024-02-17 07:04:24 +11:00
|
|
|
this.axisGroup.name = AXIS_GROUP
|
2024-02-11 12:59:00 +11:00
|
|
|
this.axisGroup.layers.set(SKETCH_LAYER)
|
|
|
|
this.axisGroup.traverse((child) => {
|
|
|
|
child.layers.set(SKETCH_LAYER)
|
|
|
|
})
|
|
|
|
|
2024-03-22 10:23:04 +11:00
|
|
|
const quat = quaternionFromUpNForward(
|
|
|
|
new Vector3(...up),
|
|
|
|
new Vector3(...forward)
|
2024-02-11 12:59:00 +11:00
|
|
|
)
|
|
|
|
this.axisGroup.setRotationFromQuaternion(quat)
|
2024-03-22 10:23:04 +11:00
|
|
|
sketchPosition && this.axisGroup.position.set(...sketchPosition)
|
2025-04-01 23:54:26 -07:00
|
|
|
this.sceneInfra.scene.add(this.axisGroup)
|
2024-02-11 12:59:00 +11:00
|
|
|
}
|
2025-04-14 23:51:14 +02:00
|
|
|
|
2024-10-31 07:04:38 -07:00
|
|
|
getDraftPoint() {
|
2025-04-01 23:54:26 -07:00
|
|
|
return this.sceneInfra.scene.getObjectByName(DRAFT_POINT)
|
2024-10-31 07:04:38 -07:00
|
|
|
}
|
2025-04-14 23:51:14 +02:00
|
|
|
|
2025-03-18 11:14:12 +11:00
|
|
|
createDraftPoint({
|
|
|
|
point,
|
|
|
|
origin,
|
|
|
|
yAxis,
|
|
|
|
zAxis,
|
|
|
|
}: {
|
|
|
|
point: Vector2
|
|
|
|
origin: SketchDetails['origin']
|
|
|
|
yAxis: SketchDetails['yAxis']
|
|
|
|
zAxis: SketchDetails['zAxis']
|
|
|
|
}) {
|
|
|
|
const draftPointGroup = new Group()
|
|
|
|
this.draftPointGroups.push(draftPointGroup)
|
|
|
|
draftPointGroup.name = DRAFT_POINT_GROUP
|
|
|
|
origin && draftPointGroup.position.set(...origin)
|
|
|
|
if (!yAxis) {
|
|
|
|
console.error('No sketch quaternion or sketch details found')
|
|
|
|
return
|
|
|
|
}
|
|
|
|
const currentSketchQuaternion = quaternionFromUpNForward(
|
|
|
|
new Vector3(...yAxis),
|
|
|
|
new Vector3(...zAxis)
|
|
|
|
)
|
|
|
|
draftPointGroup.setRotationFromQuaternion(currentSketchQuaternion)
|
2025-04-01 23:54:26 -07:00
|
|
|
this.sceneInfra.scene.add(draftPointGroup)
|
2024-10-31 07:04:38 -07:00
|
|
|
const dummy = new Mesh()
|
|
|
|
dummy.position.set(0, 0, 0)
|
2025-04-01 23:54:26 -07:00
|
|
|
const scale = this.sceneInfra.getClientSceneScaleFactor(dummy)
|
2024-10-31 07:04:38 -07:00
|
|
|
|
|
|
|
const draftPoint = createProfileStartHandle({
|
|
|
|
isDraft: true,
|
|
|
|
from: [point.x, point.y],
|
|
|
|
scale,
|
2025-04-01 23:54:26 -07:00
|
|
|
theme: this.sceneInfra._theme,
|
2025-02-15 00:57:04 +11:00
|
|
|
// default is 12, this makes the draft point pop a bit more,
|
2025-04-25 16:01:35 -05:00
|
|
|
// especially when snapping to the startProfile handle as it's it was the exact same size
|
2025-02-15 00:57:04 +11:00
|
|
|
size: 16,
|
2024-10-31 07:04:38 -07:00
|
|
|
})
|
|
|
|
draftPoint.layers.set(SKETCH_LAYER)
|
2025-03-18 11:14:12 +11:00
|
|
|
draftPointGroup.add(draftPoint)
|
2024-10-31 07:04:38 -07:00
|
|
|
}
|
2024-11-22 11:05:04 -05:00
|
|
|
|
2024-10-31 07:04:38 -07:00
|
|
|
removeDraftPoint() {
|
|
|
|
const draftPoint = this.getDraftPoint()
|
|
|
|
if (draftPoint) draftPoint.removeFromParent()
|
|
|
|
}
|
2024-02-11 12:59:00 +11:00
|
|
|
|
2024-07-30 14:16:53 -04:00
|
|
|
setupNoPointsListener({
|
|
|
|
sketchDetails,
|
|
|
|
afterClick,
|
2025-02-15 00:57:04 +11:00
|
|
|
currentTool,
|
2024-07-30 14:16:53 -04:00
|
|
|
}: {
|
|
|
|
sketchDetails: SketchDetails
|
2025-02-15 00:57:04 +11:00
|
|
|
currentTool: SketchTool
|
|
|
|
afterClick: (
|
|
|
|
args: OnClickCallbackArgs,
|
|
|
|
updatedPaths: {
|
|
|
|
sketchNodePaths: PathToNode[]
|
|
|
|
sketchEntryNodePath: PathToNode
|
|
|
|
}
|
|
|
|
) => void
|
2024-07-30 14:16:53 -04:00
|
|
|
}) {
|
2024-10-31 07:04:38 -07:00
|
|
|
// TODO: Consolidate shared logic between this and setupSketch
|
|
|
|
// Which should just fire when the sketch mode is entered,
|
|
|
|
// instead of in these two separate XState states.
|
|
|
|
this.currentSketchQuaternion = quaternionFromUpNForward(
|
|
|
|
new Vector3(...sketchDetails.yAxis),
|
|
|
|
new Vector3(...sketchDetails.zAxis)
|
|
|
|
)
|
|
|
|
|
2024-07-30 14:16:53 -04:00
|
|
|
const quaternion = quaternionFromUpNForward(
|
|
|
|
new Vector3(...sketchDetails.yAxis),
|
|
|
|
new Vector3(...sketchDetails.zAxis)
|
|
|
|
)
|
|
|
|
|
|
|
|
// Position the click raycast plane
|
2025-03-21 01:55:20 +01:00
|
|
|
this.intersectionPlane.setRotationFromQuaternion(quaternion)
|
|
|
|
this.intersectionPlane.position.copy(
|
2024-10-31 07:04:38 -07:00
|
|
|
new Vector3(...(sketchDetails?.origin || [0, 0, 0]))
|
|
|
|
)
|
2025-04-01 23:54:26 -07:00
|
|
|
this.sceneInfra.setCallbacks({
|
2024-10-31 07:04:38 -07:00
|
|
|
onMove: (args) => {
|
|
|
|
if (!args.intersects.length) return
|
|
|
|
const axisIntersection = args.intersects.find(
|
|
|
|
(sceneObject) =>
|
|
|
|
sceneObject.object.name === X_AXIS ||
|
|
|
|
sceneObject.object.name === Y_AXIS
|
|
|
|
)
|
2025-02-15 00:57:04 +11:00
|
|
|
|
2025-03-18 11:14:12 +11:00
|
|
|
const arrowHead = getParentGroup(args.intersects[0].object, [
|
|
|
|
ARROWHEAD,
|
|
|
|
ARC_ANGLE_END,
|
|
|
|
THREE_POINT_ARC_HANDLE3,
|
|
|
|
])
|
2025-02-15 00:57:04 +11:00
|
|
|
const parent = getParentGroup(
|
|
|
|
args.intersects[0].object,
|
|
|
|
SEGMENT_BODIES_PLUS_PROFILE_START
|
|
|
|
)
|
|
|
|
if (
|
|
|
|
!axisIntersection &&
|
|
|
|
!(
|
|
|
|
parent?.userData?.isLastInProfile &&
|
|
|
|
(arrowHead || parent?.name === PROFILE_START)
|
|
|
|
)
|
|
|
|
)
|
|
|
|
return
|
2024-10-31 07:04:38 -07:00
|
|
|
const { intersectionPoint } = args
|
|
|
|
// We're hovering over an axis, so we should show a draft point
|
|
|
|
const snappedPoint = intersectionPoint.twoD.clone()
|
2025-03-18 11:14:12 +11:00
|
|
|
let intersectsXY = { x: false, y: false }
|
|
|
|
args.intersects.forEach((intersect) => {
|
|
|
|
const parent = getParentGroup(intersect.object, [X_AXIS, Y_AXIS])
|
|
|
|
if (parent?.name === X_AXIS) {
|
|
|
|
intersectsXY.x = true
|
|
|
|
} else if (parent?.name === Y_AXIS) {
|
|
|
|
intersectsXY.y = true
|
|
|
|
}
|
|
|
|
})
|
|
|
|
if (intersectsXY.x && intersectsXY.y) {
|
|
|
|
snappedPoint.setComponent(0, 0)
|
|
|
|
snappedPoint.setComponent(1, 0)
|
|
|
|
} else if (intersectsXY.x) {
|
2024-10-31 07:04:38 -07:00
|
|
|
snappedPoint.setComponent(1, 0)
|
2025-03-18 11:14:12 +11:00
|
|
|
} else if (intersectsXY.y) {
|
2024-10-31 07:04:38 -07:00
|
|
|
snappedPoint.setComponent(0, 0)
|
2025-02-15 00:57:04 +11:00
|
|
|
} else if (arrowHead) {
|
|
|
|
snappedPoint.set(arrowHead.position.x, arrowHead.position.y)
|
|
|
|
} else if (parent?.name === PROFILE_START) {
|
|
|
|
snappedPoint.set(parent.position.x, parent.position.y)
|
2024-10-31 07:04:38 -07:00
|
|
|
}
|
|
|
|
|
2025-03-18 11:14:12 +11:00
|
|
|
this.positionDraftPoint({
|
|
|
|
snappedPoint,
|
|
|
|
origin: sketchDetails.origin,
|
|
|
|
yAxis: sketchDetails.yAxis,
|
|
|
|
zAxis: sketchDetails.zAxis,
|
|
|
|
})
|
2024-10-31 07:04:38 -07:00
|
|
|
},
|
|
|
|
onMouseLeave: () => {
|
|
|
|
this.removeDraftPoint()
|
|
|
|
},
|
2024-07-30 14:16:53 -04:00
|
|
|
onClick: async (args) => {
|
2024-10-31 07:04:38 -07:00
|
|
|
this.removeDraftPoint()
|
2024-07-30 14:16:53 -04:00
|
|
|
if (!args) return
|
2024-10-24 06:26:33 -07:00
|
|
|
// If there is a valid camera interaction that matches, do that instead
|
2025-04-01 23:54:26 -07:00
|
|
|
const interaction = this.sceneInfra.camControls.getInteractionType(
|
2024-10-24 06:26:33 -07:00
|
|
|
args.mouseEvent
|
|
|
|
)
|
|
|
|
if (interaction !== 'none') return
|
2024-07-30 14:16:53 -04:00
|
|
|
if (args.mouseEvent.which !== 1) return
|
|
|
|
const { intersectionPoint } = args
|
2025-03-20 08:30:11 +11:00
|
|
|
if (!intersectionPoint?.twoD) return
|
2025-02-15 00:57:04 +11:00
|
|
|
|
|
|
|
const parent = getParentGroup(
|
|
|
|
args?.intersects?.[0]?.object,
|
|
|
|
SEGMENT_BODIES_PLUS_PROFILE_START
|
|
|
|
)
|
|
|
|
if (parent?.userData?.isLastInProfile) {
|
|
|
|
afterClick(args, {
|
|
|
|
sketchNodePaths: sketchDetails.sketchNodePaths,
|
|
|
|
sketchEntryNodePath: parent.userData.pathToNode,
|
|
|
|
})
|
|
|
|
return
|
|
|
|
} else if (currentTool === 'tangentialArc') {
|
|
|
|
toast.error(
|
|
|
|
'Tangential Arc must continue an existing profile, please click on the last segment of the profile'
|
|
|
|
)
|
|
|
|
return
|
|
|
|
}
|
2024-10-31 07:04:38 -07:00
|
|
|
|
|
|
|
// Snap to either or both axes
|
|
|
|
// if the click intersects their meshes
|
|
|
|
const yAxisIntersection = args.intersects.find(
|
|
|
|
(sceneObject) => sceneObject.object.name === Y_AXIS
|
|
|
|
)
|
|
|
|
const xAxisIntersection = args.intersects.find(
|
|
|
|
(sceneObject) => sceneObject.object.name === X_AXIS
|
|
|
|
)
|
|
|
|
|
|
|
|
const snappedClickPoint = {
|
|
|
|
x: yAxisIntersection ? 0 : intersectionPoint.twoD.x,
|
|
|
|
y: xAxisIntersection ? 0 : intersectionPoint.twoD.y,
|
|
|
|
}
|
|
|
|
|
2025-02-15 00:57:04 +11:00
|
|
|
const inserted = insertNewStartProfileAt(
|
2025-04-01 23:54:26 -07:00
|
|
|
this.kclManager.ast,
|
2025-03-20 08:30:11 +11:00
|
|
|
sketchDetails.sketchEntryNodePath || [],
|
2025-02-15 00:57:04 +11:00
|
|
|
sketchDetails.sketchNodePaths,
|
|
|
|
sketchDetails.planeNodePath,
|
|
|
|
[snappedClickPoint.x, snappedClickPoint.y],
|
|
|
|
'end'
|
2024-07-30 14:16:53 -04:00
|
|
|
)
|
|
|
|
|
2025-02-15 00:57:04 +11:00
|
|
|
if (trap(inserted)) return
|
|
|
|
const { modifiedAst } = inserted
|
2024-07-30 14:16:53 -04:00
|
|
|
|
2025-04-01 23:54:26 -07:00
|
|
|
await this.kclManager.updateAst(modifiedAst, false)
|
2024-11-16 16:49:44 -05:00
|
|
|
|
2024-07-30 14:16:53 -04:00
|
|
|
// Now perform the caller-specified action
|
2025-02-15 00:57:04 +11:00
|
|
|
afterClick(args, {
|
|
|
|
sketchNodePaths: inserted.updatedSketchNodePaths,
|
|
|
|
sketchEntryNodePath: inserted.updatedEntryNodePath,
|
|
|
|
})
|
2024-07-30 14:16:53 -04:00
|
|
|
},
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2024-02-11 12:59:00 +11:00
|
|
|
async setupSketch({
|
2025-02-15 00:57:04 +11:00
|
|
|
sketchEntryNodePath,
|
|
|
|
sketchNodePaths,
|
2024-03-22 10:23:04 +11:00
|
|
|
forward,
|
|
|
|
up,
|
|
|
|
position,
|
2024-03-25 15:20:43 +11:00
|
|
|
maybeModdedAst,
|
|
|
|
draftExpressionsIndices,
|
2024-06-22 04:49:31 -04:00
|
|
|
selectionRanges,
|
2024-02-11 12:59:00 +11:00
|
|
|
}: {
|
2025-02-15 00:57:04 +11:00
|
|
|
sketchEntryNodePath: PathToNode
|
|
|
|
sketchNodePaths: PathToNode[]
|
2024-10-30 16:52:17 -04:00
|
|
|
maybeModdedAst: Node<Program>
|
2024-03-25 15:20:43 +11:00
|
|
|
draftExpressionsIndices?: { start: number; end: number }
|
2024-03-22 10:23:04 +11:00
|
|
|
forward: [number, number, number]
|
|
|
|
up: [number, number, number]
|
|
|
|
position?: [number, number, number]
|
2024-06-22 04:49:31 -04:00
|
|
|
selectionRanges?: Selections
|
2024-03-25 15:20:43 +11:00
|
|
|
}): Promise<{
|
2024-10-30 16:52:17 -04:00
|
|
|
truncatedAst: Node<Program>
|
2024-03-25 15:20:43 +11:00
|
|
|
variableDeclarationName: string
|
|
|
|
}> {
|
2025-02-15 00:57:04 +11:00
|
|
|
const prepared = this.prepareTruncatedAst(sketchNodePaths, maybeModdedAst)
|
2025-04-17 10:10:27 +10:00
|
|
|
if (err(prepared)) {
|
|
|
|
this.tearDownSketch({ removeAxis: false })
|
|
|
|
return Promise.reject(prepared)
|
|
|
|
}
|
2025-02-12 10:22:56 +13:00
|
|
|
const { truncatedAst, variableDeclarationName } = prepared
|
2024-06-24 11:45:40 -04:00
|
|
|
|
2025-03-17 18:26:11 -07:00
|
|
|
const { execState } = await executeAstMock({
|
2024-02-11 12:59:00 +11:00
|
|
|
ast: truncatedAst,
|
2025-04-01 23:54:26 -07:00
|
|
|
rustContext: this.rustContext,
|
2024-02-11 12:59:00 +11:00
|
|
|
})
|
2025-02-15 00:57:04 +11:00
|
|
|
const sketchesInfo = getSketchesInfo({
|
|
|
|
sketchNodePaths,
|
2024-04-03 13:22:56 +11:00
|
|
|
ast: maybeModdedAst,
|
2025-02-12 10:22:56 +13:00
|
|
|
variables: execState.variables,
|
2025-04-01 23:54:26 -07:00
|
|
|
kclManager: this.kclManager,
|
2024-02-11 12:59:00 +11:00
|
|
|
})
|
2024-06-24 11:45:40 -04:00
|
|
|
|
2024-02-11 12:59:00 +11:00
|
|
|
const group = new Group()
|
2024-03-22 10:23:04 +11:00
|
|
|
position && group.position.set(...position)
|
2024-02-11 12:59:00 +11:00
|
|
|
group.userData = {
|
|
|
|
type: SKETCH_GROUP_SEGMENTS,
|
2025-02-15 00:57:04 +11:00
|
|
|
pathToNode: sketchEntryNodePath,
|
2024-02-11 12:59:00 +11:00
|
|
|
}
|
|
|
|
const dummy = new Mesh()
|
2024-06-21 19:54:18 -07:00
|
|
|
// TODO: When we actually have sketch positions and rotations we can use them here.
|
|
|
|
dummy.position.set(0, 0, 0)
|
2025-04-01 23:54:26 -07:00
|
|
|
const scale = this.sceneInfra.getClientSceneScaleFactor(dummy)
|
2024-03-02 08:48:30 +11:00
|
|
|
|
2024-05-24 20:54:42 +10:00
|
|
|
const callbacks: (() => SegmentOverlayPayload | null)[] = []
|
2025-04-17 10:10:27 +10:00
|
|
|
this.sceneInfra.pauseRendering()
|
|
|
|
this.tearDownSketch({ removeAxis: false })
|
2025-02-15 00:57:04 +11:00
|
|
|
|
|
|
|
for (const sketchInfo of sketchesInfo) {
|
|
|
|
const { sketch } = sketchInfo
|
|
|
|
const segPathToNode = getNodePathFromSourceRange(
|
2024-04-19 11:56:21 -04:00
|
|
|
maybeModdedAst,
|
2025-02-15 00:57:04 +11:00
|
|
|
sourceRangeFromRust(sketch.start.__geoMeta.sourceRange)
|
2024-02-11 12:59:00 +11:00
|
|
|
)
|
2024-12-16 10:34:11 -05:00
|
|
|
if (
|
2025-02-15 00:57:04 +11:00
|
|
|
['Circle', 'CircleThreePoint'].includes(sketch?.paths?.[0]?.type) ===
|
|
|
|
false
|
2024-12-16 10:34:11 -05:00
|
|
|
) {
|
2025-02-15 00:57:04 +11:00
|
|
|
const _profileStart = createProfileStartHandle({
|
|
|
|
from: sketch.start.from,
|
|
|
|
id: sketch.start.__geoMeta.id,
|
|
|
|
pathToNode: segPathToNode,
|
|
|
|
scale,
|
2025-04-01 23:54:26 -07:00
|
|
|
theme: this.sceneInfra._theme,
|
2025-02-15 00:57:04 +11:00
|
|
|
isDraft: false,
|
|
|
|
})
|
|
|
|
_profileStart.layers.set(SKETCH_LAYER)
|
|
|
|
_profileStart.traverse((child) => {
|
|
|
|
child.layers.set(SKETCH_LAYER)
|
|
|
|
})
|
|
|
|
if (!sketch.paths.length) {
|
|
|
|
_profileStart.userData.isLastInProfile = true
|
|
|
|
}
|
|
|
|
group.add(_profileStart)
|
|
|
|
this.activeSegments[JSON.stringify(segPathToNode)] = _profileStart
|
|
|
|
}
|
|
|
|
sketch.paths.forEach((segment, index) => {
|
|
|
|
const isLastInProfile =
|
|
|
|
index === sketch.paths.length - 1 && segment.type !== 'Circle'
|
|
|
|
let segPathToNode = getNodePathFromSourceRange(
|
2024-04-19 11:56:21 -04:00
|
|
|
maybeModdedAst,
|
2024-12-16 10:34:11 -05:00
|
|
|
sourceRangeFromRust(segment.__geoMeta.sourceRange)
|
2024-12-06 13:57:31 +13:00
|
|
|
)
|
2025-02-15 00:57:04 +11:00
|
|
|
if (
|
|
|
|
draftExpressionsIndices &&
|
|
|
|
(sketch.paths[index - 1] || sketch.start)
|
|
|
|
) {
|
|
|
|
const previousSegment = sketch.paths[index - 1] || sketch.start
|
|
|
|
const previousSegmentPathToNode = getNodePathFromSourceRange(
|
|
|
|
maybeModdedAst,
|
|
|
|
sourceRangeFromRust(previousSegment.__geoMeta.sourceRange)
|
|
|
|
)
|
|
|
|
const bodyIndex = previousSegmentPathToNode[1][0]
|
|
|
|
segPathToNode = getNodePathFromSourceRange(
|
|
|
|
truncatedAst,
|
|
|
|
sourceRangeFromRust(segment.__geoMeta.sourceRange)
|
|
|
|
)
|
|
|
|
segPathToNode[1][0] = bodyIndex
|
|
|
|
}
|
|
|
|
const isDraftSegment =
|
|
|
|
draftExpressionsIndices &&
|
|
|
|
index <= draftExpressionsIndices.end &&
|
|
|
|
index >= draftExpressionsIndices.start &&
|
|
|
|
// the following line is not robust to sketches defined within a function
|
|
|
|
sketchInfo.pathToNode[1][0] === sketchEntryNodePath[1][0]
|
|
|
|
const isSelected = selectionRanges?.graphSelections.some((selection) =>
|
|
|
|
isOverlap(
|
|
|
|
selection?.codeRef?.range,
|
|
|
|
sourceRangeFromRust(segment.__geoMeta.sourceRange)
|
|
|
|
)
|
2024-12-14 09:57:33 +11:00
|
|
|
)
|
2024-12-17 15:12:18 -05:00
|
|
|
|
2025-02-15 00:57:04 +11:00
|
|
|
let seg: Group
|
|
|
|
const _node1 = getNodeFromPath<Node<CallExpression | CallExpressionKw>>(
|
|
|
|
maybeModdedAst,
|
|
|
|
segPathToNode,
|
|
|
|
['CallExpression', 'CallExpressionKw']
|
|
|
|
)
|
2025-04-17 10:10:27 +10:00
|
|
|
if (err(_node1)) {
|
|
|
|
this.tearDownSketch({ removeAxis: false })
|
|
|
|
this.sceneInfra.resumeRendering()
|
|
|
|
return
|
|
|
|
}
|
2025-03-24 20:58:55 +13:00
|
|
|
const callExpName = _node1.node?.callee?.name.name
|
2025-02-15 00:57:04 +11:00
|
|
|
|
|
|
|
const initSegment =
|
|
|
|
segment.type === 'TangentialArcTo'
|
2025-04-11 14:17:20 -04:00
|
|
|
? segmentUtils.tangentialArc.init
|
2025-02-15 00:57:04 +11:00
|
|
|
: segment.type === 'Circle'
|
2025-04-01 23:54:26 -07:00
|
|
|
? segmentUtils.circle.init
|
|
|
|
: segment.type === 'Arc'
|
|
|
|
? segmentUtils.arc.init
|
|
|
|
: segment.type === 'CircleThreePoint'
|
|
|
|
? segmentUtils.circleThreePoint.init
|
|
|
|
: segment.type === 'ArcThreePoint'
|
|
|
|
? segmentUtils.threePointArc.init
|
|
|
|
: segmentUtils.straight.init
|
2025-02-15 00:57:04 +11:00
|
|
|
const input: SegmentInputs =
|
|
|
|
segment.type === 'Circle'
|
|
|
|
? {
|
|
|
|
type: 'arc-segment',
|
|
|
|
from: segment.from,
|
2025-03-18 11:14:12 +11:00
|
|
|
to: segment.from,
|
|
|
|
ccw: true,
|
2025-02-15 00:57:04 +11:00
|
|
|
center: segment.center,
|
|
|
|
radius: segment.radius,
|
|
|
|
}
|
2025-03-18 11:14:12 +11:00
|
|
|
: segment.type === 'CircleThreePoint' ||
|
2025-04-01 23:54:26 -07:00
|
|
|
segment.type === 'ArcThreePoint'
|
|
|
|
? {
|
|
|
|
type: 'circle-three-point-segment',
|
|
|
|
p1: segment.p1,
|
|
|
|
p2: segment.p2,
|
|
|
|
p3: segment.p3,
|
|
|
|
}
|
|
|
|
: segment.type === 'Arc'
|
|
|
|
? {
|
|
|
|
type: 'arc-segment',
|
|
|
|
from: segment.from,
|
|
|
|
center: segment.center,
|
|
|
|
to: segment.to,
|
|
|
|
ccw: segment.ccw,
|
|
|
|
radius: segment.radius,
|
|
|
|
}
|
|
|
|
: {
|
|
|
|
type: 'straight-segment',
|
|
|
|
from: segment.from,
|
|
|
|
to: segment.to,
|
|
|
|
}
|
2025-02-15 00:57:04 +11:00
|
|
|
const startRange = _node1.node.start
|
|
|
|
const endRange = _node1.node.end
|
|
|
|
const sourceRange: SourceRange = [startRange, endRange, 0]
|
|
|
|
const selection: Selections = computeSelectionFromSourceRangeAndAST(
|
|
|
|
sourceRange,
|
2025-04-01 23:54:26 -07:00
|
|
|
maybeModdedAst,
|
|
|
|
this.kclManager
|
2025-02-15 00:57:04 +11:00
|
|
|
)
|
|
|
|
const result = initSegment({
|
|
|
|
prevSegment: sketch.paths[index - 1],
|
|
|
|
callExpName,
|
|
|
|
input,
|
|
|
|
id: segment.__geoMeta.id,
|
|
|
|
pathToNode: segPathToNode,
|
|
|
|
isDraftSegment,
|
|
|
|
scale,
|
2025-04-01 23:54:26 -07:00
|
|
|
texture: this.sceneInfra.extraSegmentTexture,
|
|
|
|
theme: this.sceneInfra._theme,
|
2025-02-15 00:57:04 +11:00
|
|
|
isSelected,
|
2025-04-01 23:54:26 -07:00
|
|
|
sceneInfra: this.sceneInfra,
|
2025-02-15 00:57:04 +11:00
|
|
|
selection,
|
|
|
|
})
|
|
|
|
if (err(result)) return
|
|
|
|
const { group: _group, updateOverlaysCallback } = result
|
|
|
|
seg = _group
|
|
|
|
if (isLastInProfile) {
|
|
|
|
seg.userData.isLastInProfile = true
|
|
|
|
}
|
|
|
|
callbacks.push(updateOverlaysCallback)
|
|
|
|
seg.layers.set(SKETCH_LAYER)
|
|
|
|
seg.traverse((child) => {
|
|
|
|
child.layers.set(SKETCH_LAYER)
|
|
|
|
})
|
2024-12-17 15:12:18 -05:00
|
|
|
|
2025-02-15 00:57:04 +11:00
|
|
|
group.add(seg)
|
|
|
|
this.activeSegments[JSON.stringify(segPathToNode)] = seg
|
2024-12-16 10:34:11 -05:00
|
|
|
})
|
2025-02-15 00:57:04 +11:00
|
|
|
}
|
2024-02-11 12:59:00 +11:00
|
|
|
|
2024-03-22 10:23:04 +11:00
|
|
|
this.currentSketchQuaternion = quaternionFromUpNForward(
|
|
|
|
new Vector3(...up),
|
|
|
|
new Vector3(...forward)
|
|
|
|
)
|
2024-02-11 12:59:00 +11:00
|
|
|
group.setRotationFromQuaternion(this.currentSketchQuaternion)
|
2025-03-21 01:55:20 +01:00
|
|
|
this.intersectionPlane.setRotationFromQuaternion(
|
|
|
|
this.currentSketchQuaternion
|
|
|
|
)
|
|
|
|
position && this.intersectionPlane.position.set(...position)
|
2025-04-01 23:54:26 -07:00
|
|
|
this.sceneInfra.scene.add(group)
|
2025-04-17 10:10:27 +10:00
|
|
|
|
|
|
|
this.sceneInfra.resumeRendering()
|
|
|
|
|
2025-04-01 23:54:26 -07:00
|
|
|
this.sceneInfra.camControls.enableRotate = false
|
|
|
|
this.sceneInfra.overlayCallbacks(callbacks)
|
2024-02-11 12:59:00 +11:00
|
|
|
|
2024-03-25 15:20:43 +11:00
|
|
|
return {
|
|
|
|
truncatedAst,
|
|
|
|
variableDeclarationName,
|
2024-02-11 12:59:00 +11:00
|
|
|
}
|
|
|
|
}
|
2025-04-14 23:51:14 +02:00
|
|
|
|
2024-02-11 12:59:00 +11:00
|
|
|
updateAstAndRejigSketch = async (
|
2025-02-15 00:57:04 +11:00
|
|
|
sketchEntryNodePath: PathToNode,
|
|
|
|
sketchNodePaths: PathToNode[],
|
|
|
|
planeNodePath: PathToNode,
|
|
|
|
modifiedAst: Node<Program> | Error,
|
2024-03-22 10:23:04 +11:00
|
|
|
forward: [number, number, number],
|
|
|
|
up: [number, number, number],
|
2024-05-02 22:26:29 +10:00
|
|
|
origin: [number, number, number]
|
2024-02-11 12:59:00 +11:00
|
|
|
) => {
|
2025-02-15 00:57:04 +11:00
|
|
|
if (trap(modifiedAst)) return Promise.reject(modifiedAst)
|
2025-04-01 23:54:26 -07:00
|
|
|
const nextAst = await this.kclManager.updateAst(modifiedAst, false)
|
|
|
|
this.sceneInfra.resetMouseListeners()
|
2024-03-25 15:20:43 +11:00
|
|
|
await this.setupSketch({
|
2025-02-15 00:57:04 +11:00
|
|
|
sketchEntryNodePath,
|
|
|
|
sketchNodePaths,
|
2024-03-25 15:20:43 +11:00
|
|
|
forward,
|
|
|
|
up,
|
|
|
|
position: origin,
|
2024-06-24 11:45:40 -04:00
|
|
|
maybeModdedAst: nextAst.newAst,
|
2024-03-25 15:20:43 +11:00
|
|
|
})
|
2024-04-03 13:22:56 +11:00
|
|
|
this.setupSketchIdleCallbacks({
|
|
|
|
forward,
|
|
|
|
up,
|
|
|
|
position: origin,
|
2025-02-15 00:57:04 +11:00
|
|
|
sketchEntryNodePath,
|
|
|
|
sketchNodePaths,
|
|
|
|
planeNodePath,
|
2024-04-03 13:22:56 +11:00
|
|
|
})
|
2024-06-24 11:45:40 -04:00
|
|
|
return nextAst
|
2024-02-11 12:59:00 +11:00
|
|
|
}
|
2025-03-18 11:14:12 +11:00
|
|
|
didIntersectProfileStart = (
|
|
|
|
args: OnClickCallbackArgs,
|
|
|
|
nodePath: PathToNode
|
|
|
|
) => {
|
|
|
|
return args.intersects
|
|
|
|
.map(({ object }) => getParentGroup(object, [PROFILE_START]))
|
|
|
|
.find(isGroupStartProfileForCurrentProfile(nodePath))
|
|
|
|
}
|
2024-11-16 16:49:44 -05:00
|
|
|
setupDraftSegment = async (
|
2025-02-15 00:57:04 +11:00
|
|
|
sketchEntryNodePath: PathToNode,
|
|
|
|
sketchNodePaths: PathToNode[],
|
|
|
|
planeNodePath: PathToNode,
|
2024-03-22 10:23:04 +11:00
|
|
|
forward: [number, number, number],
|
2024-03-25 15:20:43 +11:00
|
|
|
up: [number, number, number],
|
|
|
|
origin: [number, number, number],
|
2025-04-11 14:17:20 -04:00
|
|
|
segmentName: 'line' | 'tangentialArc' = 'line',
|
2024-03-25 15:20:43 +11:00
|
|
|
shouldTearDown = true
|
2024-03-22 10:23:04 +11:00
|
|
|
) => {
|
2025-04-01 23:54:26 -07:00
|
|
|
const _ast = structuredClone(this.kclManager.ast)
|
2024-03-25 15:20:43 +11:00
|
|
|
|
2024-06-24 11:45:40 -04:00
|
|
|
const _node1 = getNodeFromPath<VariableDeclaration>(
|
|
|
|
_ast,
|
2025-02-15 00:57:04 +11:00
|
|
|
sketchEntryNodePath || [],
|
2024-06-24 11:45:40 -04:00
|
|
|
'VariableDeclaration'
|
|
|
|
)
|
|
|
|
if (trap(_node1)) return Promise.reject(_node1)
|
2024-12-07 07:16:04 +13:00
|
|
|
const variableDeclarationName = _node1.node?.declaration.id?.name || ''
|
2024-06-24 11:45:40 -04:00
|
|
|
|
2024-09-27 15:44:44 -07:00
|
|
|
const sg = sketchFromKclValue(
|
2025-04-01 23:54:26 -07:00
|
|
|
this.kclManager.variables[variableDeclarationName],
|
2024-03-25 15:20:43 +11:00
|
|
|
variableDeclarationName
|
Remove KclValue::SketchGroup variant (#3446)
We can store Rust types like `SketchGroup` as their own variant of `KclValue`, or as `KclValue::UserVal`. Sometimes we store in one and try to read from the other, which fails. This causes bugs, like #3338.
Instead, we should use either ::SketchGroup or ::UserVal, and stop using the other. If we stopped using ::UserVal, we'd need a new variant for every Rust type we wanted to build, including user-defined types. So I don't think that's practical.
Instead, we should store every KCL value by de/serializing it into UserVal. This is a first step along that path, removing just the SketchGroup variants. If it goes well, we can remove the other specialized variants too.
My only concern is there might be performance implications from how frequently we convert between serde_json::Value and Rust types via Serde. But I'm not too worried -- there's no parsing JSON strings, just traversing serde_json::Value trees. This isn't great for performance but I think it'll probably be miniscule in comparison to doing all the API calls.
2024-08-21 11:06:48 -05:00
|
|
|
)
|
2024-09-13 21:14:14 +10:00
|
|
|
if (err(sg)) return Promise.reject(sg)
|
2024-10-23 12:42:54 -05:00
|
|
|
const lastSeg = sg?.paths?.slice(-1)[0] || sg.start
|
2024-03-25 15:20:43 +11:00
|
|
|
|
2025-02-15 00:57:04 +11:00
|
|
|
const index = sg.paths.length // because we've added a new segment that's not in the memory yet, no need for `.length -1`
|
2024-04-03 13:22:56 +11:00
|
|
|
const mod = addNewSketchLn({
|
|
|
|
node: _ast,
|
2025-04-01 23:54:26 -07:00
|
|
|
variables: this.kclManager.variables,
|
2024-09-13 21:14:14 +10:00
|
|
|
input: {
|
|
|
|
type: 'straight-segment',
|
|
|
|
to: lastSeg.to,
|
|
|
|
from: lastSeg.to,
|
|
|
|
},
|
2024-03-25 15:20:43 +11:00
|
|
|
fnName: segmentName,
|
2025-02-15 00:57:04 +11:00
|
|
|
pathToNode: sketchEntryNodePath,
|
2024-04-03 13:22:56 +11:00
|
|
|
})
|
2024-06-24 11:45:40 -04:00
|
|
|
if (trap(mod)) return Promise.reject(mod)
|
2024-12-06 13:57:31 +13:00
|
|
|
const pResult = parse(recast(mod.modifiedAst))
|
|
|
|
if (trap(pResult) || !resultIsOk(pResult)) return Promise.reject(pResult)
|
|
|
|
const modifiedAst = pResult.program
|
2024-03-25 15:20:43 +11:00
|
|
|
|
|
|
|
const draftExpressionsIndices = { start: index, end: index }
|
|
|
|
|
2025-04-01 23:54:26 -07:00
|
|
|
this.sceneInfra.resetMouseListeners()
|
2024-06-29 18:10:07 -07:00
|
|
|
|
2025-02-15 00:57:04 +11:00
|
|
|
const { truncatedAst } = await this.setupSketch({
|
|
|
|
sketchEntryNodePath,
|
|
|
|
sketchNodePaths,
|
2025-02-12 10:22:56 +13:00
|
|
|
forward,
|
|
|
|
up,
|
|
|
|
position: origin,
|
|
|
|
maybeModdedAst: modifiedAst,
|
|
|
|
draftExpressionsIndices,
|
2025-04-17 10:10:27 +10:00
|
|
|
}).catch(() => {
|
|
|
|
return { truncatedAst: modifiedAst }
|
2025-02-12 10:22:56 +13:00
|
|
|
})
|
2025-04-01 23:54:26 -07:00
|
|
|
this.sceneInfra.setCallbacks({
|
2024-03-25 15:20:43 +11:00
|
|
|
onClick: async (args) => {
|
|
|
|
if (!args) return
|
2024-10-24 06:26:33 -07:00
|
|
|
// If there is a valid camera interaction that matches, do that instead
|
2025-04-01 23:54:26 -07:00
|
|
|
const interaction = this.sceneInfra.camControls.getInteractionType(
|
2024-10-24 06:26:33 -07:00
|
|
|
args.mouseEvent
|
|
|
|
)
|
|
|
|
if (interaction !== 'none') return
|
2024-03-25 15:20:43 +11:00
|
|
|
if (args.mouseEvent.which !== 1) return
|
2024-10-24 06:26:33 -07:00
|
|
|
|
2024-03-25 15:20:43 +11:00
|
|
|
const { intersectionPoint } = args
|
|
|
|
let intersection2d = intersectionPoint?.twoD
|
2025-03-18 11:14:12 +11:00
|
|
|
const intersectsProfileStart = this.didIntersectProfileStart(
|
|
|
|
args,
|
|
|
|
sketchEntryNodePath
|
|
|
|
)
|
2024-03-25 15:20:43 +11:00
|
|
|
|
2025-04-01 23:54:26 -07:00
|
|
|
let modifiedAst: Node<Program> | Error = structuredClone(
|
|
|
|
this.kclManager.ast
|
|
|
|
)
|
2025-02-15 00:57:04 +11:00
|
|
|
|
|
|
|
const sketch = sketchFromPathToNode({
|
|
|
|
pathToNode: sketchEntryNodePath,
|
2025-04-01 23:54:26 -07:00
|
|
|
ast: this.kclManager.ast,
|
|
|
|
variables: this.kclManager.variables,
|
|
|
|
kclManager: this.kclManager,
|
2025-02-15 00:57:04 +11:00
|
|
|
})
|
|
|
|
if (err(sketch)) return Promise.reject(sketch)
|
|
|
|
if (!sketch) return Promise.reject(new Error('No sketch found'))
|
2024-10-31 07:04:38 -07:00
|
|
|
|
|
|
|
// Snapping logic for the profile start handle
|
|
|
|
if (intersectsProfileStart) {
|
KCL: Use keyword arguments for line, lineTo, extrude and close (#5249)
Part of #4600.
PR: https://github.com/KittyCAD/modeling-app/pull/4826
# Changes to KCL stdlib
- `line(point, sketch, tag)` and `lineTo(point, sketch, tag)` are combined into `line(@sketch, end?, endAbsolute?, tag?)`
- `close(sketch, tag?)` is now `close(@sketch, tag?)`
- `extrude(length, sketch)` is now `extrude(@sketch, length)`
Note that if a parameter starts with `@` like `@sketch`, it doesn't have any label when called, so you call it like this:
```
sketch = startSketchAt([0, 0])
line(sketch, end = [3, 3], tag = $hi)
```
Note also that if you're using a `|>` pipeline, you can omit the `@` argument and it will be assumed to be the LHS of the `|>`. So the above could be written as
```
sketch = startSketchAt([0, 0])
|> line(end = [3, 3], tag = $hi)
```
Also changes frontend tests to use KittyCAD/kcl-samples#139 instead of its main
The regex find-and-replace I use for migrating code (note these don't work with multi-line expressions) are:
```
line\(([^=]*), %\)
line(end = $1)
line\((.*), %, (.*)\)
line(end = $1, tag = $2)
lineTo\((.*), %\)
line(endAbsolute = $1)
lineTo\((.*), %, (.*)\)
line(endAbsolute = $1, tag = $2)
extrude\((.*), %\)
extrude(length = $1)
extrude\(([^=]*), ([a-zA-Z0-9]+)\)
extrude($2, length = $1)
close\(%, (.*)\)
close(tag = $1)
```
# Selected notes from commits before I squash them all
* Fix test 'yRelative to horizontal distance'
Fixes:
- Make a lineTo helper
- Fix pathToNode to go through the labeled arg .arg property
* Fix test by changing lookups into transformMap
Parts of the code assumed that `line` is always a relative call. But
actually now it might be absolute, if it's got an `endAbsolute` parameter.
So, change whether to look up `line` or `lineTo` and the relevant absolute
or relative line types based on that parameter.
* Stop asserting on exact source ranges
When I changed line to kwargs, all the source ranges we assert on became
slightly different. I find these assertions to be very very low value.
So I'm removing them.
* Fix more tests: getConstraintType calls weren't checking if the
'line' fn was absolute or relative.
* Fixed another queryAst test
There were 2 problems:
- Test was looking for the old style of `line` call to choose an offset
for pathToNode
- Test assumed that the `tag` param was always the third one, but in
a kwarg call, you have to look it up by label
* Fix test: traverse was not handling CallExpressionKw
* Fix another test, addTagKw
addTag helper was not aware of kw args.
* Convert close from positional to kwargs
If the close() call has 0 args, or a single unlabeled arg, the parser
interprets it as a CallExpression (positional) not a CallExpressionKw.
But then if a codemod wants to add a tag to it, it tries adding a kwarg
called 'tag', which fails because the CallExpression doesn't need
kwargs inserted into it.
The fix is: change the node from CallExpression to CallExpressionKw, and
update getNodeFromPath to take a 'replacement' arg, so we can replace
the old node with the new node in the AST.
* Fix the last test
Test was looking for `lineTo` as a substring of the input KCL program.
But there's no more lineTo function, so I changed it to look for
line() with an endAbsolute arg, which is the new equivalent.
Also changed the getConstraintInfo code to look up the lineTo if using
line with endAbsolute.
* Fix many bad regex find-replaces
I wrote a regex find-and-replace which converted `line` calls from
positional to keyword calls. But it was accidentally applied to more
places than it should be, for example, angledLine, xLine and yLine calls.
Fixes this.
* Fixes test 'Basic sketch › code pane closed at start'
Problem was, the getNodeFromPath call might not actually find a callExpressionKw,
it might find a callExpression. So the `giveSketchFnCallTag` thought
it was modifying a kwargs call, but it was actually modifying a positional
call.
This meant it tried to push a labeled argument in, rather than a normal
arg, and a lot of other problems. Fixed by doing runtime typechecking.
* Fix: Optional args given with wrong type were silently ignored
Optional args don't have to be given. But if the user gives them, they
should be the right type.
Bug: if the KCL interpreter found an optional arg, which was given, but
was the wrong type, it would ignore it and pretend the arg was never
given at all. This was confusing for users.
Fix: Now if you give an optional arg, but it's the wrong type, KCL will
emit a type error just like it would for a mandatory argument.
---------
Signed-off-by: Nick Cameron <nrc@ncameron.org>
Co-authored-by: Nick Cameron <nrc@ncameron.org>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Frank Noirot <frank@kittycad.io>
Co-authored-by: Kevin Nadro <kevin@zoo.dev>
Co-authored-by: Jonathan Tran <jonnytran@gmail.com>
2025-02-04 08:31:43 -06:00
|
|
|
const originCoords = createArrayExpression([
|
2025-04-23 10:21:58 -05:00
|
|
|
createCallExpressionStdLibKw(
|
|
|
|
'profileStartX',
|
KCL: Use keyword arguments for line, lineTo, extrude and close (#5249)
Part of #4600.
PR: https://github.com/KittyCAD/modeling-app/pull/4826
# Changes to KCL stdlib
- `line(point, sketch, tag)` and `lineTo(point, sketch, tag)` are combined into `line(@sketch, end?, endAbsolute?, tag?)`
- `close(sketch, tag?)` is now `close(@sketch, tag?)`
- `extrude(length, sketch)` is now `extrude(@sketch, length)`
Note that if a parameter starts with `@` like `@sketch`, it doesn't have any label when called, so you call it like this:
```
sketch = startSketchAt([0, 0])
line(sketch, end = [3, 3], tag = $hi)
```
Note also that if you're using a `|>` pipeline, you can omit the `@` argument and it will be assumed to be the LHS of the `|>`. So the above could be written as
```
sketch = startSketchAt([0, 0])
|> line(end = [3, 3], tag = $hi)
```
Also changes frontend tests to use KittyCAD/kcl-samples#139 instead of its main
The regex find-and-replace I use for migrating code (note these don't work with multi-line expressions) are:
```
line\(([^=]*), %\)
line(end = $1)
line\((.*), %, (.*)\)
line(end = $1, tag = $2)
lineTo\((.*), %\)
line(endAbsolute = $1)
lineTo\((.*), %, (.*)\)
line(endAbsolute = $1, tag = $2)
extrude\((.*), %\)
extrude(length = $1)
extrude\(([^=]*), ([a-zA-Z0-9]+)\)
extrude($2, length = $1)
close\(%, (.*)\)
close(tag = $1)
```
# Selected notes from commits before I squash them all
* Fix test 'yRelative to horizontal distance'
Fixes:
- Make a lineTo helper
- Fix pathToNode to go through the labeled arg .arg property
* Fix test by changing lookups into transformMap
Parts of the code assumed that `line` is always a relative call. But
actually now it might be absolute, if it's got an `endAbsolute` parameter.
So, change whether to look up `line` or `lineTo` and the relevant absolute
or relative line types based on that parameter.
* Stop asserting on exact source ranges
When I changed line to kwargs, all the source ranges we assert on became
slightly different. I find these assertions to be very very low value.
So I'm removing them.
* Fix more tests: getConstraintType calls weren't checking if the
'line' fn was absolute or relative.
* Fixed another queryAst test
There were 2 problems:
- Test was looking for the old style of `line` call to choose an offset
for pathToNode
- Test assumed that the `tag` param was always the third one, but in
a kwarg call, you have to look it up by label
* Fix test: traverse was not handling CallExpressionKw
* Fix another test, addTagKw
addTag helper was not aware of kw args.
* Convert close from positional to kwargs
If the close() call has 0 args, or a single unlabeled arg, the parser
interprets it as a CallExpression (positional) not a CallExpressionKw.
But then if a codemod wants to add a tag to it, it tries adding a kwarg
called 'tag', which fails because the CallExpression doesn't need
kwargs inserted into it.
The fix is: change the node from CallExpression to CallExpressionKw, and
update getNodeFromPath to take a 'replacement' arg, so we can replace
the old node with the new node in the AST.
* Fix the last test
Test was looking for `lineTo` as a substring of the input KCL program.
But there's no more lineTo function, so I changed it to look for
line() with an endAbsolute arg, which is the new equivalent.
Also changed the getConstraintInfo code to look up the lineTo if using
line with endAbsolute.
* Fix many bad regex find-replaces
I wrote a regex find-and-replace which converted `line` calls from
positional to keyword calls. But it was accidentally applied to more
places than it should be, for example, angledLine, xLine and yLine calls.
Fixes this.
* Fixes test 'Basic sketch › code pane closed at start'
Problem was, the getNodeFromPath call might not actually find a callExpressionKw,
it might find a callExpression. So the `giveSketchFnCallTag` thought
it was modifying a kwargs call, but it was actually modifying a positional
call.
This meant it tried to push a labeled argument in, rather than a normal
arg, and a lot of other problems. Fixed by doing runtime typechecking.
* Fix: Optional args given with wrong type were silently ignored
Optional args don't have to be given. But if the user gives them, they
should be the right type.
Bug: if the KCL interpreter found an optional arg, which was given, but
was the wrong type, it would ignore it and pretend the arg was never
given at all. This was confusing for users.
Fix: Now if you give an optional arg, but it's the wrong type, KCL will
emit a type error just like it would for a mandatory argument.
---------
Signed-off-by: Nick Cameron <nrc@ncameron.org>
Co-authored-by: Nick Cameron <nrc@ncameron.org>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Frank Noirot <frank@kittycad.io>
Co-authored-by: Kevin Nadro <kevin@zoo.dev>
Co-authored-by: Jonathan Tran <jonnytran@gmail.com>
2025-02-04 08:31:43 -06:00
|
|
|
createPipeSubstitution(),
|
2025-04-23 10:21:58 -05:00
|
|
|
[]
|
|
|
|
),
|
|
|
|
createCallExpressionStdLibKw(
|
|
|
|
'profileStartY',
|
KCL: Use keyword arguments for line, lineTo, extrude and close (#5249)
Part of #4600.
PR: https://github.com/KittyCAD/modeling-app/pull/4826
# Changes to KCL stdlib
- `line(point, sketch, tag)` and `lineTo(point, sketch, tag)` are combined into `line(@sketch, end?, endAbsolute?, tag?)`
- `close(sketch, tag?)` is now `close(@sketch, tag?)`
- `extrude(length, sketch)` is now `extrude(@sketch, length)`
Note that if a parameter starts with `@` like `@sketch`, it doesn't have any label when called, so you call it like this:
```
sketch = startSketchAt([0, 0])
line(sketch, end = [3, 3], tag = $hi)
```
Note also that if you're using a `|>` pipeline, you can omit the `@` argument and it will be assumed to be the LHS of the `|>`. So the above could be written as
```
sketch = startSketchAt([0, 0])
|> line(end = [3, 3], tag = $hi)
```
Also changes frontend tests to use KittyCAD/kcl-samples#139 instead of its main
The regex find-and-replace I use for migrating code (note these don't work with multi-line expressions) are:
```
line\(([^=]*), %\)
line(end = $1)
line\((.*), %, (.*)\)
line(end = $1, tag = $2)
lineTo\((.*), %\)
line(endAbsolute = $1)
lineTo\((.*), %, (.*)\)
line(endAbsolute = $1, tag = $2)
extrude\((.*), %\)
extrude(length = $1)
extrude\(([^=]*), ([a-zA-Z0-9]+)\)
extrude($2, length = $1)
close\(%, (.*)\)
close(tag = $1)
```
# Selected notes from commits before I squash them all
* Fix test 'yRelative to horizontal distance'
Fixes:
- Make a lineTo helper
- Fix pathToNode to go through the labeled arg .arg property
* Fix test by changing lookups into transformMap
Parts of the code assumed that `line` is always a relative call. But
actually now it might be absolute, if it's got an `endAbsolute` parameter.
So, change whether to look up `line` or `lineTo` and the relevant absolute
or relative line types based on that parameter.
* Stop asserting on exact source ranges
When I changed line to kwargs, all the source ranges we assert on became
slightly different. I find these assertions to be very very low value.
So I'm removing them.
* Fix more tests: getConstraintType calls weren't checking if the
'line' fn was absolute or relative.
* Fixed another queryAst test
There were 2 problems:
- Test was looking for the old style of `line` call to choose an offset
for pathToNode
- Test assumed that the `tag` param was always the third one, but in
a kwarg call, you have to look it up by label
* Fix test: traverse was not handling CallExpressionKw
* Fix another test, addTagKw
addTag helper was not aware of kw args.
* Convert close from positional to kwargs
If the close() call has 0 args, or a single unlabeled arg, the parser
interprets it as a CallExpression (positional) not a CallExpressionKw.
But then if a codemod wants to add a tag to it, it tries adding a kwarg
called 'tag', which fails because the CallExpression doesn't need
kwargs inserted into it.
The fix is: change the node from CallExpression to CallExpressionKw, and
update getNodeFromPath to take a 'replacement' arg, so we can replace
the old node with the new node in the AST.
* Fix the last test
Test was looking for `lineTo` as a substring of the input KCL program.
But there's no more lineTo function, so I changed it to look for
line() with an endAbsolute arg, which is the new equivalent.
Also changed the getConstraintInfo code to look up the lineTo if using
line with endAbsolute.
* Fix many bad regex find-replaces
I wrote a regex find-and-replace which converted `line` calls from
positional to keyword calls. But it was accidentally applied to more
places than it should be, for example, angledLine, xLine and yLine calls.
Fixes this.
* Fixes test 'Basic sketch › code pane closed at start'
Problem was, the getNodeFromPath call might not actually find a callExpressionKw,
it might find a callExpression. So the `giveSketchFnCallTag` thought
it was modifying a kwargs call, but it was actually modifying a positional
call.
This meant it tried to push a labeled argument in, rather than a normal
arg, and a lot of other problems. Fixed by doing runtime typechecking.
* Fix: Optional args given with wrong type were silently ignored
Optional args don't have to be given. But if the user gives them, they
should be the right type.
Bug: if the KCL interpreter found an optional arg, which was given, but
was the wrong type, it would ignore it and pretend the arg was never
given at all. This was confusing for users.
Fix: Now if you give an optional arg, but it's the wrong type, KCL will
emit a type error just like it would for a mandatory argument.
---------
Signed-off-by: Nick Cameron <nrc@ncameron.org>
Co-authored-by: Nick Cameron <nrc@ncameron.org>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Frank Noirot <frank@kittycad.io>
Co-authored-by: Kevin Nadro <kevin@zoo.dev>
Co-authored-by: Jonathan Tran <jonnytran@gmail.com>
2025-02-04 08:31:43 -06:00
|
|
|
createPipeSubstitution(),
|
2025-04-23 10:21:58 -05:00
|
|
|
[]
|
|
|
|
),
|
KCL: Use keyword arguments for line, lineTo, extrude and close (#5249)
Part of #4600.
PR: https://github.com/KittyCAD/modeling-app/pull/4826
# Changes to KCL stdlib
- `line(point, sketch, tag)` and `lineTo(point, sketch, tag)` are combined into `line(@sketch, end?, endAbsolute?, tag?)`
- `close(sketch, tag?)` is now `close(@sketch, tag?)`
- `extrude(length, sketch)` is now `extrude(@sketch, length)`
Note that if a parameter starts with `@` like `@sketch`, it doesn't have any label when called, so you call it like this:
```
sketch = startSketchAt([0, 0])
line(sketch, end = [3, 3], tag = $hi)
```
Note also that if you're using a `|>` pipeline, you can omit the `@` argument and it will be assumed to be the LHS of the `|>`. So the above could be written as
```
sketch = startSketchAt([0, 0])
|> line(end = [3, 3], tag = $hi)
```
Also changes frontend tests to use KittyCAD/kcl-samples#139 instead of its main
The regex find-and-replace I use for migrating code (note these don't work with multi-line expressions) are:
```
line\(([^=]*), %\)
line(end = $1)
line\((.*), %, (.*)\)
line(end = $1, tag = $2)
lineTo\((.*), %\)
line(endAbsolute = $1)
lineTo\((.*), %, (.*)\)
line(endAbsolute = $1, tag = $2)
extrude\((.*), %\)
extrude(length = $1)
extrude\(([^=]*), ([a-zA-Z0-9]+)\)
extrude($2, length = $1)
close\(%, (.*)\)
close(tag = $1)
```
# Selected notes from commits before I squash them all
* Fix test 'yRelative to horizontal distance'
Fixes:
- Make a lineTo helper
- Fix pathToNode to go through the labeled arg .arg property
* Fix test by changing lookups into transformMap
Parts of the code assumed that `line` is always a relative call. But
actually now it might be absolute, if it's got an `endAbsolute` parameter.
So, change whether to look up `line` or `lineTo` and the relevant absolute
or relative line types based on that parameter.
* Stop asserting on exact source ranges
When I changed line to kwargs, all the source ranges we assert on became
slightly different. I find these assertions to be very very low value.
So I'm removing them.
* Fix more tests: getConstraintType calls weren't checking if the
'line' fn was absolute or relative.
* Fixed another queryAst test
There were 2 problems:
- Test was looking for the old style of `line` call to choose an offset
for pathToNode
- Test assumed that the `tag` param was always the third one, but in
a kwarg call, you have to look it up by label
* Fix test: traverse was not handling CallExpressionKw
* Fix another test, addTagKw
addTag helper was not aware of kw args.
* Convert close from positional to kwargs
If the close() call has 0 args, or a single unlabeled arg, the parser
interprets it as a CallExpression (positional) not a CallExpressionKw.
But then if a codemod wants to add a tag to it, it tries adding a kwarg
called 'tag', which fails because the CallExpression doesn't need
kwargs inserted into it.
The fix is: change the node from CallExpression to CallExpressionKw, and
update getNodeFromPath to take a 'replacement' arg, so we can replace
the old node with the new node in the AST.
* Fix the last test
Test was looking for `lineTo` as a substring of the input KCL program.
But there's no more lineTo function, so I changed it to look for
line() with an endAbsolute arg, which is the new equivalent.
Also changed the getConstraintInfo code to look up the lineTo if using
line with endAbsolute.
* Fix many bad regex find-replaces
I wrote a regex find-and-replace which converted `line` calls from
positional to keyword calls. But it was accidentally applied to more
places than it should be, for example, angledLine, xLine and yLine calls.
Fixes this.
* Fixes test 'Basic sketch › code pane closed at start'
Problem was, the getNodeFromPath call might not actually find a callExpressionKw,
it might find a callExpression. So the `giveSketchFnCallTag` thought
it was modifying a kwargs call, but it was actually modifying a positional
call.
This meant it tried to push a labeled argument in, rather than a normal
arg, and a lot of other problems. Fixed by doing runtime typechecking.
* Fix: Optional args given with wrong type were silently ignored
Optional args don't have to be given. But if the user gives them, they
should be the right type.
Bug: if the KCL interpreter found an optional arg, which was given, but
was the wrong type, it would ignore it and pretend the arg was never
given at all. This was confusing for users.
Fix: Now if you give an optional arg, but it's the wrong type, KCL will
emit a type error just like it would for a mandatory argument.
---------
Signed-off-by: Nick Cameron <nrc@ncameron.org>
Co-authored-by: Nick Cameron <nrc@ncameron.org>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Frank Noirot <frank@kittycad.io>
Co-authored-by: Kevin Nadro <kevin@zoo.dev>
Co-authored-by: Jonathan Tran <jonnytran@gmail.com>
2025-02-04 08:31:43 -06:00
|
|
|
])
|
2025-04-23 10:21:58 -05:00
|
|
|
|
2024-05-23 00:53:15 -04:00
|
|
|
modifiedAst = addCallExpressionsToPipe({
|
2025-04-01 23:54:26 -07:00
|
|
|
node: this.kclManager.ast,
|
|
|
|
variables: this.kclManager.variables,
|
2025-02-15 00:57:04 +11:00
|
|
|
pathToNode: sketchEntryNodePath,
|
2024-05-23 00:53:15 -04:00
|
|
|
expressions: [
|
2025-04-11 14:17:20 -04:00
|
|
|
segmentName === 'tangentialArc'
|
|
|
|
? createCallExpressionStdLibKw('tangentialArc', null, [
|
|
|
|
createLabeledArg(ARG_END_ABSOLUTE, originCoords),
|
KCL: Use keyword arguments for line, lineTo, extrude and close (#5249)
Part of #4600.
PR: https://github.com/KittyCAD/modeling-app/pull/4826
# Changes to KCL stdlib
- `line(point, sketch, tag)` and `lineTo(point, sketch, tag)` are combined into `line(@sketch, end?, endAbsolute?, tag?)`
- `close(sketch, tag?)` is now `close(@sketch, tag?)`
- `extrude(length, sketch)` is now `extrude(@sketch, length)`
Note that if a parameter starts with `@` like `@sketch`, it doesn't have any label when called, so you call it like this:
```
sketch = startSketchAt([0, 0])
line(sketch, end = [3, 3], tag = $hi)
```
Note also that if you're using a `|>` pipeline, you can omit the `@` argument and it will be assumed to be the LHS of the `|>`. So the above could be written as
```
sketch = startSketchAt([0, 0])
|> line(end = [3, 3], tag = $hi)
```
Also changes frontend tests to use KittyCAD/kcl-samples#139 instead of its main
The regex find-and-replace I use for migrating code (note these don't work with multi-line expressions) are:
```
line\(([^=]*), %\)
line(end = $1)
line\((.*), %, (.*)\)
line(end = $1, tag = $2)
lineTo\((.*), %\)
line(endAbsolute = $1)
lineTo\((.*), %, (.*)\)
line(endAbsolute = $1, tag = $2)
extrude\((.*), %\)
extrude(length = $1)
extrude\(([^=]*), ([a-zA-Z0-9]+)\)
extrude($2, length = $1)
close\(%, (.*)\)
close(tag = $1)
```
# Selected notes from commits before I squash them all
* Fix test 'yRelative to horizontal distance'
Fixes:
- Make a lineTo helper
- Fix pathToNode to go through the labeled arg .arg property
* Fix test by changing lookups into transformMap
Parts of the code assumed that `line` is always a relative call. But
actually now it might be absolute, if it's got an `endAbsolute` parameter.
So, change whether to look up `line` or `lineTo` and the relevant absolute
or relative line types based on that parameter.
* Stop asserting on exact source ranges
When I changed line to kwargs, all the source ranges we assert on became
slightly different. I find these assertions to be very very low value.
So I'm removing them.
* Fix more tests: getConstraintType calls weren't checking if the
'line' fn was absolute or relative.
* Fixed another queryAst test
There were 2 problems:
- Test was looking for the old style of `line` call to choose an offset
for pathToNode
- Test assumed that the `tag` param was always the third one, but in
a kwarg call, you have to look it up by label
* Fix test: traverse was not handling CallExpressionKw
* Fix another test, addTagKw
addTag helper was not aware of kw args.
* Convert close from positional to kwargs
If the close() call has 0 args, or a single unlabeled arg, the parser
interprets it as a CallExpression (positional) not a CallExpressionKw.
But then if a codemod wants to add a tag to it, it tries adding a kwarg
called 'tag', which fails because the CallExpression doesn't need
kwargs inserted into it.
The fix is: change the node from CallExpression to CallExpressionKw, and
update getNodeFromPath to take a 'replacement' arg, so we can replace
the old node with the new node in the AST.
* Fix the last test
Test was looking for `lineTo` as a substring of the input KCL program.
But there's no more lineTo function, so I changed it to look for
line() with an endAbsolute arg, which is the new equivalent.
Also changed the getConstraintInfo code to look up the lineTo if using
line with endAbsolute.
* Fix many bad regex find-replaces
I wrote a regex find-and-replace which converted `line` calls from
positional to keyword calls. But it was accidentally applied to more
places than it should be, for example, angledLine, xLine and yLine calls.
Fixes this.
* Fixes test 'Basic sketch › code pane closed at start'
Problem was, the getNodeFromPath call might not actually find a callExpressionKw,
it might find a callExpression. So the `giveSketchFnCallTag` thought
it was modifying a kwargs call, but it was actually modifying a positional
call.
This meant it tried to push a labeled argument in, rather than a normal
arg, and a lot of other problems. Fixed by doing runtime typechecking.
* Fix: Optional args given with wrong type were silently ignored
Optional args don't have to be given. But if the user gives them, they
should be the right type.
Bug: if the KCL interpreter found an optional arg, which was given, but
was the wrong type, it would ignore it and pretend the arg was never
given at all. This was confusing for users.
Fix: Now if you give an optional arg, but it's the wrong type, KCL will
emit a type error just like it would for a mandatory argument.
---------
Signed-off-by: Nick Cameron <nrc@ncameron.org>
Co-authored-by: Nick Cameron <nrc@ncameron.org>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Frank Noirot <frank@kittycad.io>
Co-authored-by: Kevin Nadro <kevin@zoo.dev>
Co-authored-by: Jonathan Tran <jonnytran@gmail.com>
2025-02-04 08:31:43 -06:00
|
|
|
])
|
|
|
|
: createCallExpressionStdLibKw('line', null, [
|
2025-02-28 11:50:14 -06:00
|
|
|
createLabeledArg(ARG_END_ABSOLUTE, originCoords),
|
2024-05-23 00:53:15 -04:00
|
|
|
]),
|
|
|
|
],
|
|
|
|
})
|
2024-06-24 11:45:40 -04:00
|
|
|
if (trap(modifiedAst)) return Promise.reject(modifiedAst)
|
2024-05-23 00:53:15 -04:00
|
|
|
modifiedAst = addCloseToPipe({
|
|
|
|
node: modifiedAst,
|
2025-04-01 23:54:26 -07:00
|
|
|
variables: this.kclManager.variables,
|
2025-02-15 00:57:04 +11:00
|
|
|
pathToNode: sketchEntryNodePath,
|
2024-03-25 15:20:43 +11:00
|
|
|
})
|
2024-06-24 11:45:40 -04:00
|
|
|
if (trap(modifiedAst)) return Promise.reject(modifiedAst)
|
2024-03-25 15:20:43 +11:00
|
|
|
} else if (intersection2d) {
|
2025-04-14 23:51:14 +02:00
|
|
|
const lastSegment = sketch.paths.slice(-1)[0] || sketch.start
|
|
|
|
|
|
|
|
let {
|
|
|
|
snappedPoint,
|
|
|
|
snappedToTangent,
|
|
|
|
intersectsXAxis,
|
|
|
|
intersectsYAxis,
|
|
|
|
negativeTangentDirection,
|
|
|
|
} = this.getSnappedDragPoint(
|
|
|
|
intersection2d,
|
|
|
|
args.intersects,
|
|
|
|
args.mouseEvent,
|
|
|
|
Object.values(this.activeSegments).at(-1)
|
2024-10-31 07:04:38 -07:00
|
|
|
)
|
|
|
|
|
2024-11-13 09:41:27 -05:00
|
|
|
// Get the angle between the previous segment (or sketch start)'s end and this one's
|
|
|
|
const angle = Math.atan2(
|
2025-04-14 23:51:14 +02:00
|
|
|
snappedPoint[1] - lastSegment.to[1],
|
|
|
|
snappedPoint[0] - lastSegment.to[0]
|
2024-11-13 09:41:27 -05:00
|
|
|
)
|
|
|
|
|
|
|
|
const isHorizontal =
|
|
|
|
radToDeg(Math.abs(angle)) < ANGLE_SNAP_THRESHOLD_DEGREES ||
|
|
|
|
Math.abs(radToDeg(Math.abs(angle) - Math.PI)) <
|
|
|
|
ANGLE_SNAP_THRESHOLD_DEGREES
|
|
|
|
const isVertical =
|
|
|
|
Math.abs(radToDeg(Math.abs(angle) - Math.PI / 2)) <
|
|
|
|
ANGLE_SNAP_THRESHOLD_DEGREES
|
2024-10-31 07:04:38 -07:00
|
|
|
|
|
|
|
let resolvedFunctionName: ToolTip = 'line'
|
2025-04-14 23:51:14 +02:00
|
|
|
const snaps = {
|
|
|
|
previousArcTag: '',
|
|
|
|
negativeTangentDirection,
|
|
|
|
xAxis: !!intersectsXAxis,
|
|
|
|
yAxis: !!intersectsYAxis,
|
|
|
|
}
|
2024-10-31 07:04:38 -07:00
|
|
|
|
|
|
|
// This might need to become its own function if we want more
|
|
|
|
// case-based logic for different segment types
|
2025-02-15 00:57:04 +11:00
|
|
|
if (
|
2025-04-11 14:17:20 -04:00
|
|
|
(lastSegment.type === 'TangentialArc' && segmentName !== 'line') ||
|
|
|
|
segmentName === 'tangentialArc'
|
2025-02-15 00:57:04 +11:00
|
|
|
) {
|
2025-04-11 14:17:20 -04:00
|
|
|
resolvedFunctionName = 'tangentialArc'
|
2025-04-14 23:51:14 +02:00
|
|
|
} else if (snappedToTangent) {
|
|
|
|
// Generate tag for previous arc segment and use it for the angle of angledLine:
|
|
|
|
// |> tangentialArcTo([5, -10], %, $arc001)
|
|
|
|
// |> angledLine({ angle = tangentToEnd(arc001), length = 12 }, %)
|
|
|
|
|
|
|
|
const previousSegmentPathToNode = getNodePathFromSourceRange(
|
|
|
|
modifiedAst,
|
|
|
|
sourceRangeFromRust(lastSegment.__geoMeta.sourceRange)
|
|
|
|
)
|
|
|
|
const taggedAstResult = mutateAstWithTagForSketchSegment(
|
|
|
|
modifiedAst,
|
|
|
|
previousSegmentPathToNode
|
|
|
|
)
|
|
|
|
if (trap(taggedAstResult)) return Promise.reject(taggedAstResult)
|
|
|
|
|
|
|
|
modifiedAst = taggedAstResult.modifiedAst
|
|
|
|
snaps.previousArcTag = taggedAstResult.tag
|
|
|
|
resolvedFunctionName = 'angledLine'
|
2024-11-13 09:41:27 -05:00
|
|
|
} else if (isHorizontal) {
|
|
|
|
// If the angle between is 0 or 180 degrees (+/- the snapping angle), make the line an xLine
|
|
|
|
resolvedFunctionName = 'xLine'
|
|
|
|
} else if (isVertical) {
|
|
|
|
// If the angle between is 90 or 270 degrees (+/- the snapping angle), make the line a yLine
|
|
|
|
resolvedFunctionName = 'yLine'
|
2025-04-14 23:51:14 +02:00
|
|
|
} else if (snappedPoint[0] === 0 || snappedPoint[1] === 0) {
|
2024-10-31 07:04:38 -07:00
|
|
|
// We consider a point placed on axes or origin to be absolute
|
|
|
|
resolvedFunctionName = 'lineTo'
|
|
|
|
}
|
|
|
|
|
2024-06-24 11:45:40 -04:00
|
|
|
const tmp = addNewSketchLn({
|
2025-04-14 23:51:14 +02:00
|
|
|
node: modifiedAst,
|
2025-04-01 23:54:26 -07:00
|
|
|
variables: this.kclManager.variables,
|
2024-09-13 21:14:14 +10:00
|
|
|
input: {
|
|
|
|
type: 'straight-segment',
|
|
|
|
from: [lastSegment.to[0], lastSegment.to[1]],
|
2025-04-14 23:51:14 +02:00
|
|
|
to: [snappedPoint[0], snappedPoint[1]],
|
2024-09-13 21:14:14 +10:00
|
|
|
},
|
2024-10-31 07:04:38 -07:00
|
|
|
fnName: resolvedFunctionName,
|
2025-02-15 00:57:04 +11:00
|
|
|
pathToNode: sketchEntryNodePath,
|
2025-04-14 23:51:14 +02:00
|
|
|
snaps,
|
2024-06-24 11:45:40 -04:00
|
|
|
})
|
|
|
|
if (trap(tmp)) return Promise.reject(tmp)
|
|
|
|
modifiedAst = tmp.modifiedAst
|
|
|
|
if (trap(modifiedAst)) return Promise.reject(modifiedAst)
|
2024-03-25 15:20:43 +11:00
|
|
|
} else {
|
|
|
|
// return early as we didn't modify the ast
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2025-03-25 05:06:27 -04:00
|
|
|
await updateModelingState(modifiedAst, EXECUTION_TYPE_MOCK, {
|
2025-04-01 23:54:26 -07:00
|
|
|
kclManager: this.kclManager,
|
|
|
|
editorManager: this.editorManager,
|
|
|
|
codeManager: this.codeManager,
|
2025-03-25 05:06:27 -04:00
|
|
|
})
|
2024-11-16 16:49:44 -05:00
|
|
|
|
2024-10-31 07:04:38 -07:00
|
|
|
if (intersectsProfileStart) {
|
2025-04-01 23:54:26 -07:00
|
|
|
this.sceneInfra.modelingSend({ type: 'Close sketch' })
|
2024-05-23 00:53:15 -04:00
|
|
|
} else {
|
2024-11-16 16:49:44 -05:00
|
|
|
await this.setupDraftSegment(
|
2025-02-15 00:57:04 +11:00
|
|
|
sketchEntryNodePath,
|
|
|
|
sketchNodePaths,
|
|
|
|
planeNodePath,
|
2024-05-23 00:53:15 -04:00
|
|
|
forward,
|
|
|
|
up,
|
|
|
|
origin,
|
|
|
|
segmentName
|
|
|
|
)
|
|
|
|
}
|
2024-03-25 15:20:43 +11:00
|
|
|
},
|
|
|
|
onMove: (args) => {
|
2025-02-15 00:57:04 +11:00
|
|
|
const expressionIndex = Number(sketchEntryNodePath[1][0])
|
|
|
|
const activeSegmentsInCorrectExpression = Object.values(
|
|
|
|
this.activeSegments
|
|
|
|
).filter((seg) => {
|
|
|
|
return seg.userData.pathToNode[1][0] === expressionIndex
|
|
|
|
})
|
|
|
|
const object =
|
|
|
|
activeSegmentsInCorrectExpression[
|
|
|
|
activeSegmentsInCorrectExpression.length - 1
|
|
|
|
]
|
2024-03-25 15:20:43 +11:00
|
|
|
this.onDragSegment({
|
|
|
|
intersection2d: args.intersectionPoint.twoD,
|
2025-02-15 00:57:04 +11:00
|
|
|
object,
|
2024-03-25 15:20:43 +11:00
|
|
|
intersects: args.intersects,
|
2025-02-15 00:57:04 +11:00
|
|
|
sketchNodePaths,
|
|
|
|
sketchEntryNodePath,
|
2024-03-25 15:20:43 +11:00
|
|
|
draftInfo: {
|
|
|
|
truncatedAst,
|
|
|
|
variableDeclarationName,
|
|
|
|
},
|
2025-04-14 23:51:14 +02:00
|
|
|
mouseEvent: args.mouseEvent,
|
2024-03-25 15:20:43 +11:00
|
|
|
})
|
|
|
|
},
|
|
|
|
})
|
2024-02-11 12:59:00 +11:00
|
|
|
}
|
2024-04-19 11:56:21 -04:00
|
|
|
setupDraftRectangle = async (
|
2025-02-15 00:57:04 +11:00
|
|
|
sketchEntryNodePath: PathToNode,
|
|
|
|
sketchNodePaths: PathToNode[],
|
|
|
|
planeNodePath: PathToNode,
|
2024-04-19 11:56:21 -04:00
|
|
|
forward: [number, number, number],
|
|
|
|
up: [number, number, number],
|
|
|
|
sketchOrigin: [number, number, number],
|
|
|
|
rectangleOrigin: [x: number, y: number]
|
2025-02-15 00:57:04 +11:00
|
|
|
): Promise<SketchDetailsUpdate | Error> => {
|
2025-04-01 23:54:26 -07:00
|
|
|
let _ast = structuredClone(this.kclManager.ast)
|
2024-04-19 11:56:21 -04:00
|
|
|
|
2025-02-15 00:57:04 +11:00
|
|
|
const varDec = getNodeFromPath<VariableDeclarator>(
|
2024-06-24 11:45:40 -04:00
|
|
|
_ast,
|
2025-02-15 00:57:04 +11:00
|
|
|
planeNodePath,
|
|
|
|
'VariableDeclarator'
|
2024-12-14 09:57:33 +11:00
|
|
|
)
|
|
|
|
|
2025-02-15 00:57:04 +11:00
|
|
|
if (err(varDec)) return varDec
|
|
|
|
if (varDec.node.type !== 'VariableDeclarator') return new Error('not a var')
|
2024-12-14 09:57:33 +11:00
|
|
|
|
2025-02-15 00:57:04 +11:00
|
|
|
const varName = findUniqueName(_ast, 'profile')
|
|
|
|
|
|
|
|
// first create just the variable declaration, as that's
|
|
|
|
// all we want the user to see in the editor
|
|
|
|
const tag = findUniqueName(_ast, 'rectangleSegmentA')
|
|
|
|
const newDeclaration = createVariableDeclaration(
|
|
|
|
varName,
|
2025-04-25 16:01:35 -05:00
|
|
|
createCallExpressionStdLibKw(
|
|
|
|
'startProfile',
|
2025-03-24 20:58:55 +13:00
|
|
|
createLocalName(varDec.node.id.name),
|
2025-04-25 16:01:35 -05:00
|
|
|
[
|
|
|
|
createLabeledArg(
|
|
|
|
ARG_AT,
|
|
|
|
createArrayExpression([
|
|
|
|
createLiteral(roundOff(rectangleOrigin[0])),
|
|
|
|
createLiteral(roundOff(rectangleOrigin[1])),
|
|
|
|
])
|
|
|
|
),
|
|
|
|
]
|
|
|
|
)
|
2025-02-15 00:57:04 +11:00
|
|
|
)
|
|
|
|
|
|
|
|
const insertIndex = getInsertIndex(sketchNodePaths, planeNodePath, 'end')
|
|
|
|
|
|
|
|
_ast.body.splice(insertIndex, 0, newDeclaration)
|
|
|
|
const { updatedEntryNodePath, updatedSketchNodePaths } =
|
|
|
|
updateSketchNodePathsWithInsertIndex({
|
|
|
|
insertIndex,
|
|
|
|
insertType: 'end',
|
|
|
|
sketchNodePaths,
|
|
|
|
})
|
2024-04-19 11:56:21 -04:00
|
|
|
|
2024-12-06 13:57:31 +13:00
|
|
|
const pResult = parse(recast(_ast))
|
|
|
|
if (trap(pResult) || !resultIsOk(pResult)) return Promise.reject(pResult)
|
|
|
|
_ast = pResult.program
|
2024-04-19 11:56:21 -04:00
|
|
|
|
2025-02-15 00:57:04 +11:00
|
|
|
// do a quick mock execution to get the program memory up-to-date
|
2025-04-12 00:16:45 +10:00
|
|
|
const didReParse = await this.kclManager.executeAstMock(_ast)
|
|
|
|
if (err(didReParse)) return didReParse
|
2025-02-15 00:57:04 +11:00
|
|
|
|
|
|
|
const justCreatedNode = getNodeFromPath<VariableDeclaration>(
|
|
|
|
_ast,
|
|
|
|
updatedEntryNodePath,
|
|
|
|
'VariableDeclaration'
|
|
|
|
)
|
|
|
|
|
|
|
|
if (trap(justCreatedNode)) return Promise.reject(justCreatedNode)
|
|
|
|
const startProfileAt = justCreatedNode.node?.declaration
|
|
|
|
// than add the rest of the profile so we can "animate" it
|
|
|
|
// as draft segments
|
|
|
|
startProfileAt.init = createPipeExpression([
|
|
|
|
startProfileAt?.init,
|
|
|
|
...getRectangleCallExpressions(rectangleOrigin, tag),
|
|
|
|
])
|
|
|
|
|
|
|
|
const code = recast(_ast)
|
|
|
|
const _recastAst = parse(code)
|
|
|
|
if (trap(_recastAst) || !resultIsOk(_recastAst))
|
|
|
|
return Promise.reject(_recastAst)
|
|
|
|
_ast = _recastAst.program
|
|
|
|
|
2025-02-12 10:22:56 +13:00
|
|
|
const { truncatedAst } = await this.setupSketch({
|
2025-02-15 00:57:04 +11:00
|
|
|
sketchEntryNodePath: updatedEntryNodePath,
|
|
|
|
sketchNodePaths: updatedSketchNodePaths,
|
2024-04-19 11:56:21 -04:00
|
|
|
forward,
|
|
|
|
up,
|
|
|
|
position: sketchOrigin,
|
|
|
|
maybeModdedAst: _ast,
|
|
|
|
draftExpressionsIndices: { start: 0, end: 3 },
|
|
|
|
})
|
|
|
|
|
2025-04-01 23:54:26 -07:00
|
|
|
this.sceneInfra.setCallbacks({
|
2024-04-19 11:56:21 -04:00
|
|
|
onMove: async (args) => {
|
|
|
|
// Update the width and height of the draft rectangle
|
2025-02-15 00:57:04 +11:00
|
|
|
|
|
|
|
const nodePathWithCorrectedIndexForTruncatedAst =
|
|
|
|
structuredClone(updatedEntryNodePath)
|
|
|
|
nodePathWithCorrectedIndexForTruncatedAst[1][0] =
|
|
|
|
Number(nodePathWithCorrectedIndexForTruncatedAst[1][0]) -
|
|
|
|
Number(planeNodePath[1][0]) -
|
|
|
|
1
|
2024-04-19 11:56:21 -04:00
|
|
|
|
2024-06-24 11:45:40 -04:00
|
|
|
const _node = getNodeFromPath<VariableDeclaration>(
|
2024-04-19 11:56:21 -04:00
|
|
|
truncatedAst,
|
2025-02-15 00:57:04 +11:00
|
|
|
nodePathWithCorrectedIndexForTruncatedAst,
|
2024-04-19 11:56:21 -04:00
|
|
|
'VariableDeclaration'
|
2024-06-24 11:45:40 -04:00
|
|
|
)
|
|
|
|
if (trap(_node)) return Promise.reject(_node)
|
2024-12-07 07:16:04 +13:00
|
|
|
const sketchInit = _node.node?.declaration.init
|
2024-04-19 11:56:21 -04:00
|
|
|
|
|
|
|
const x = (args.intersectionPoint.twoD.x || 0) - rectangleOrigin[0]
|
|
|
|
const y = (args.intersectionPoint.twoD.y || 0) - rectangleOrigin[1]
|
|
|
|
|
|
|
|
if (sketchInit.type === 'PipeExpression') {
|
2025-02-15 00:57:04 +11:00
|
|
|
updateRectangleSketch(sketchInit, x, y, tag)
|
2024-04-19 11:56:21 -04:00
|
|
|
}
|
|
|
|
|
2025-03-17 18:26:11 -07:00
|
|
|
const { execState } = await executeAstMock({
|
2024-04-19 11:56:21 -04:00
|
|
|
ast: truncatedAst,
|
2025-04-01 23:54:26 -07:00
|
|
|
rustContext: this.rustContext,
|
2024-04-19 11:56:21 -04:00
|
|
|
})
|
2025-02-15 00:57:04 +11:00
|
|
|
const sketch = sketchFromKclValue(execState.variables[varName], varName)
|
2024-09-27 15:44:44 -07:00
|
|
|
if (err(sketch)) return Promise.reject(sketch)
|
2024-10-23 12:42:54 -05:00
|
|
|
const sgPaths = sketch.paths
|
2025-04-01 23:54:26 -07:00
|
|
|
const orthoFactor = orthoScale(this.sceneInfra.camControls.camera)
|
2024-04-19 11:56:21 -04:00
|
|
|
|
2025-02-15 00:57:04 +11:00
|
|
|
const varDecIndex = Number(updatedEntryNodePath[1][0])
|
|
|
|
|
|
|
|
this.updateSegment(
|
|
|
|
sketch.start,
|
|
|
|
0,
|
|
|
|
varDecIndex,
|
|
|
|
_ast,
|
|
|
|
orthoFactor,
|
|
|
|
sketch
|
|
|
|
)
|
2024-04-19 11:56:21 -04:00
|
|
|
sgPaths.forEach((seg, index) =>
|
2025-02-15 00:57:04 +11:00
|
|
|
this.updateSegment(seg, index, varDecIndex, _ast, orthoFactor, sketch)
|
2024-04-19 11:56:21 -04:00
|
|
|
)
|
2025-03-18 11:14:12 +11:00
|
|
|
|
|
|
|
const { intersectionPoint } = args
|
|
|
|
if (!intersectionPoint?.twoD) return
|
2025-04-14 23:51:14 +02:00
|
|
|
const { snappedPoint, isSnapped } = this.getSnappedDragPoint(
|
|
|
|
intersectionPoint.twoD,
|
|
|
|
args.intersects,
|
|
|
|
args.mouseEvent
|
|
|
|
)
|
2025-03-18 11:14:12 +11:00
|
|
|
if (isSnapped) {
|
|
|
|
this.positionDraftPoint({
|
|
|
|
snappedPoint: new Vector2(...snappedPoint),
|
|
|
|
origin: sketchOrigin,
|
|
|
|
yAxis: forward,
|
|
|
|
zAxis: up,
|
|
|
|
})
|
|
|
|
} else {
|
|
|
|
this.removeDraftPoint()
|
|
|
|
}
|
2024-04-19 11:56:21 -04:00
|
|
|
},
|
|
|
|
onClick: async (args) => {
|
2024-10-24 06:26:33 -07:00
|
|
|
// If there is a valid camera interaction that matches, do that instead
|
2025-04-01 23:54:26 -07:00
|
|
|
const interaction = this.sceneInfra.camControls.getInteractionType(
|
2024-10-24 06:26:33 -07:00
|
|
|
args.mouseEvent
|
|
|
|
)
|
|
|
|
if (interaction !== 'none') return
|
2024-04-19 11:56:21 -04:00
|
|
|
// Commit the rectangle to the full AST/code and return to sketch.idle
|
|
|
|
const cornerPoint = args.intersectionPoint?.twoD
|
|
|
|
if (!cornerPoint || args.mouseEvent.button !== 0) return
|
|
|
|
|
|
|
|
const x = roundOff((cornerPoint.x || 0) - rectangleOrigin[0])
|
|
|
|
const y = roundOff((cornerPoint.y || 0) - rectangleOrigin[1])
|
|
|
|
|
2024-06-24 11:45:40 -04:00
|
|
|
const _node = getNodeFromPath<VariableDeclaration>(
|
2024-04-19 11:56:21 -04:00
|
|
|
_ast,
|
2025-02-15 00:57:04 +11:00
|
|
|
updatedEntryNodePath,
|
2024-04-19 11:56:21 -04:00
|
|
|
'VariableDeclaration'
|
2024-06-24 11:45:40 -04:00
|
|
|
)
|
2024-09-13 21:14:14 +10:00
|
|
|
if (trap(_node)) return
|
2024-12-07 07:16:04 +13:00
|
|
|
const sketchInit = _node.node?.declaration.init
|
2024-04-19 11:56:21 -04:00
|
|
|
|
2024-11-16 16:49:44 -05:00
|
|
|
if (sketchInit.type !== 'PipeExpression') {
|
|
|
|
return
|
|
|
|
}
|
2024-04-19 11:56:21 -04:00
|
|
|
|
2025-02-15 00:57:04 +11:00
|
|
|
updateRectangleSketch(sketchInit, x, y, tag)
|
2024-04-19 11:56:21 -04:00
|
|
|
|
2024-11-16 16:49:44 -05:00
|
|
|
const newCode = recast(_ast)
|
2024-12-06 13:57:31 +13:00
|
|
|
const pResult = parse(newCode)
|
|
|
|
if (trap(pResult) || !resultIsOk(pResult))
|
|
|
|
return Promise.reject(pResult)
|
|
|
|
_ast = pResult.program
|
2024-04-19 11:56:21 -04:00
|
|
|
|
2024-11-16 16:49:44 -05:00
|
|
|
// Update the primary AST and unequip the rectangle tool
|
2025-03-25 05:06:27 -04:00
|
|
|
//
|
2024-11-16 16:49:44 -05:00
|
|
|
// lee: I had this at the bottom of the function, but it's
|
|
|
|
// possible sketchFromKclValue "fails" when sketching on a face,
|
|
|
|
// and this couldn't wouldn't run.
|
2025-03-25 05:06:27 -04:00
|
|
|
await updateModelingState(_ast, EXECUTION_TYPE_MOCK, {
|
2025-04-01 23:54:26 -07:00
|
|
|
kclManager: this.kclManager,
|
|
|
|
editorManager: this.editorManager,
|
|
|
|
codeManager: this.codeManager,
|
2025-03-25 05:06:27 -04:00
|
|
|
})
|
2025-04-01 23:54:26 -07:00
|
|
|
this.sceneInfra.modelingSend({ type: 'Finish rectangle' })
|
2024-04-19 11:56:21 -04:00
|
|
|
},
|
|
|
|
})
|
2025-02-15 00:57:04 +11:00
|
|
|
return {
|
|
|
|
updatedEntryNodePath,
|
|
|
|
updatedSketchNodePaths,
|
|
|
|
expressionIndexToDelete: insertIndex,
|
|
|
|
}
|
2024-04-19 11:56:21 -04:00
|
|
|
}
|
2024-11-18 10:04:09 -05:00
|
|
|
setupDraftCenterRectangle = async (
|
2025-02-15 00:57:04 +11:00
|
|
|
sketchEntryNodePath: PathToNode,
|
|
|
|
sketchNodePaths: PathToNode[],
|
|
|
|
planeNodePath: PathToNode,
|
2024-11-18 10:04:09 -05:00
|
|
|
forward: [number, number, number],
|
|
|
|
up: [number, number, number],
|
|
|
|
sketchOrigin: [number, number, number],
|
|
|
|
rectangleOrigin: [x: number, y: number]
|
2025-02-15 00:57:04 +11:00
|
|
|
): Promise<SketchDetailsUpdate | Error> => {
|
2025-04-01 23:54:26 -07:00
|
|
|
let _ast = structuredClone(this.kclManager.ast)
|
2025-02-15 00:57:04 +11:00
|
|
|
|
|
|
|
const varDec = getNodeFromPath<VariableDeclarator>(
|
2024-11-18 10:04:09 -05:00
|
|
|
_ast,
|
2025-02-15 00:57:04 +11:00
|
|
|
planeNodePath,
|
|
|
|
'VariableDeclarator'
|
2024-12-14 09:57:33 +11:00
|
|
|
)
|
2024-11-18 10:04:09 -05:00
|
|
|
|
2025-02-15 00:57:04 +11:00
|
|
|
if (err(varDec)) return varDec
|
|
|
|
if (varDec.node.type !== 'VariableDeclarator') return new Error('not a var')
|
2024-12-14 09:57:33 +11:00
|
|
|
|
2025-02-15 00:57:04 +11:00
|
|
|
const varName = findUniqueName(_ast, 'profile')
|
|
|
|
// first create just the variable declaration, as that's
|
|
|
|
// all we want the user to see in the editor
|
|
|
|
const tag = findUniqueName(_ast, 'rectangleSegmentA')
|
2025-04-25 16:01:35 -05:00
|
|
|
|
2025-02-15 00:57:04 +11:00
|
|
|
const newDeclaration = createVariableDeclaration(
|
|
|
|
varName,
|
2025-04-25 16:01:35 -05:00
|
|
|
createCallExpressionStdLibKw(
|
|
|
|
'startProfile',
|
2025-03-24 20:58:55 +13:00
|
|
|
createLocalName(varDec.node.id.name),
|
2025-04-25 16:01:35 -05:00
|
|
|
[
|
|
|
|
createLabeledArg(
|
|
|
|
ARG_AT,
|
|
|
|
createArrayExpression([
|
|
|
|
createLiteral(roundOff(rectangleOrigin[0])),
|
|
|
|
createLiteral(roundOff(rectangleOrigin[1])),
|
|
|
|
])
|
|
|
|
),
|
|
|
|
]
|
|
|
|
)
|
2025-02-15 00:57:04 +11:00
|
|
|
)
|
|
|
|
const insertIndex = getInsertIndex(sketchNodePaths, planeNodePath, 'end')
|
|
|
|
|
|
|
|
_ast.body.splice(insertIndex, 0, newDeclaration)
|
|
|
|
const { updatedEntryNodePath, updatedSketchNodePaths } =
|
|
|
|
updateSketchNodePathsWithInsertIndex({
|
|
|
|
insertIndex,
|
|
|
|
insertType: 'end',
|
|
|
|
sketchNodePaths,
|
|
|
|
})
|
2024-12-14 09:57:33 +11:00
|
|
|
|
2025-02-15 00:57:04 +11:00
|
|
|
let __recastAst = parse(recast(_ast))
|
|
|
|
if (trap(__recastAst) || !resultIsOk(__recastAst))
|
|
|
|
return Promise.reject(__recastAst)
|
|
|
|
_ast = __recastAst.program
|
2024-12-16 10:34:11 -05:00
|
|
|
|
2025-02-15 00:57:04 +11:00
|
|
|
// do a quick mock execution to get the program memory up-to-date
|
2025-04-01 23:54:26 -07:00
|
|
|
await this.kclManager.executeAstMock(_ast)
|
2025-02-15 00:57:04 +11:00
|
|
|
|
|
|
|
const justCreatedNode = getNodeFromPath<VariableDeclaration>(
|
|
|
|
_ast,
|
|
|
|
updatedEntryNodePath,
|
|
|
|
'VariableDeclaration'
|
|
|
|
)
|
|
|
|
|
|
|
|
if (trap(justCreatedNode)) return Promise.reject(justCreatedNode)
|
|
|
|
const startProfileAt = justCreatedNode.node?.declaration
|
|
|
|
// than add the rest of the profile so we can "animate" it
|
|
|
|
// as draft segments
|
|
|
|
startProfileAt.init = createPipeExpression([
|
|
|
|
startProfileAt?.init,
|
|
|
|
...getRectangleCallExpressions(rectangleOrigin, tag),
|
|
|
|
])
|
|
|
|
const code = recast(_ast)
|
|
|
|
__recastAst = parse(code)
|
|
|
|
if (trap(__recastAst) || !resultIsOk(__recastAst))
|
|
|
|
return Promise.reject(__recastAst)
|
|
|
|
_ast = __recastAst.program
|
2024-11-18 10:04:09 -05:00
|
|
|
|
2025-02-12 10:22:56 +13:00
|
|
|
const { truncatedAst } = await this.setupSketch({
|
2025-02-15 00:57:04 +11:00
|
|
|
sketchEntryNodePath: updatedEntryNodePath,
|
|
|
|
sketchNodePaths: updatedSketchNodePaths,
|
2024-11-18 10:04:09 -05:00
|
|
|
forward,
|
|
|
|
up,
|
|
|
|
position: sketchOrigin,
|
|
|
|
maybeModdedAst: _ast,
|
|
|
|
draftExpressionsIndices: { start: 0, end: 3 },
|
|
|
|
})
|
|
|
|
|
2025-04-01 23:54:26 -07:00
|
|
|
this.sceneInfra.setCallbacks({
|
2024-11-18 10:04:09 -05:00
|
|
|
onMove: async (args) => {
|
|
|
|
// Update the width and height of the draft rectangle
|
2025-02-15 00:57:04 +11:00
|
|
|
|
|
|
|
const nodePathWithCorrectedIndexForTruncatedAst =
|
|
|
|
structuredClone(updatedEntryNodePath)
|
|
|
|
nodePathWithCorrectedIndexForTruncatedAst[1][0] =
|
|
|
|
Number(nodePathWithCorrectedIndexForTruncatedAst[1][0]) -
|
|
|
|
Number(planeNodePath[1][0]) -
|
|
|
|
1
|
2024-11-18 10:04:09 -05:00
|
|
|
|
|
|
|
const _node = getNodeFromPath<VariableDeclaration>(
|
|
|
|
truncatedAst,
|
2025-02-15 00:57:04 +11:00
|
|
|
nodePathWithCorrectedIndexForTruncatedAst,
|
2024-11-18 10:04:09 -05:00
|
|
|
'VariableDeclaration'
|
|
|
|
)
|
|
|
|
if (trap(_node)) return Promise.reject(_node)
|
2024-12-07 07:16:04 +13:00
|
|
|
const sketchInit = _node.node?.declaration.init
|
2024-11-18 10:04:09 -05:00
|
|
|
|
|
|
|
const x = (args.intersectionPoint.twoD.x || 0) - rectangleOrigin[0]
|
|
|
|
const y = (args.intersectionPoint.twoD.y || 0) - rectangleOrigin[1]
|
|
|
|
|
|
|
|
if (sketchInit.type === 'PipeExpression') {
|
KCL: Angled line should use keyword args (#5803)
We continue migrating KCL stdlib functions to use keyword arguments. Next up is the `angledLine` family of functions (except `angledLineThatIntersects, which will be a quick follow-up).
Before vs. after:
`angledLine({angle = 90, length = 3}, %, $edge)`
=> `angledLine(angle = 90, length = 3, tag = $edge)`
`angledLineOfXLength({angle = 90, length = 3}, %, $edge)`
=> `angledLine(angle = 90, lengthX = 3, tag = $edge)`
`angledLineOfYLength({angle = 90, length = 3}, %, $edge)`
=> `angledLine(angle = 90, lengthY = 3, tag = $edge)`
`angledLineToX({angle = 90, length = 3}, %, $edge)`
=> `angledLine(angle = 90, endAbsoluteX = 3, tag = $edge)`
`angledLineToY({angle = 90, length = 3}, %, $edge)`
=> `angledLine(angle = 90, endAbsoluteY = 3, tag = $edge)`
2025-04-09 14:55:15 -05:00
|
|
|
const maybeError = updateCenterRectangleSketch(
|
2024-11-18 10:04:09 -05:00
|
|
|
sketchInit,
|
|
|
|
x,
|
|
|
|
y,
|
2025-02-15 00:57:04 +11:00
|
|
|
tag,
|
2024-11-18 10:04:09 -05:00
|
|
|
rectangleOrigin[0],
|
|
|
|
rectangleOrigin[1]
|
|
|
|
)
|
KCL: Angled line should use keyword args (#5803)
We continue migrating KCL stdlib functions to use keyword arguments. Next up is the `angledLine` family of functions (except `angledLineThatIntersects, which will be a quick follow-up).
Before vs. after:
`angledLine({angle = 90, length = 3}, %, $edge)`
=> `angledLine(angle = 90, length = 3, tag = $edge)`
`angledLineOfXLength({angle = 90, length = 3}, %, $edge)`
=> `angledLine(angle = 90, lengthX = 3, tag = $edge)`
`angledLineOfYLength({angle = 90, length = 3}, %, $edge)`
=> `angledLine(angle = 90, lengthY = 3, tag = $edge)`
`angledLineToX({angle = 90, length = 3}, %, $edge)`
=> `angledLine(angle = 90, endAbsoluteX = 3, tag = $edge)`
`angledLineToY({angle = 90, length = 3}, %, $edge)`
=> `angledLine(angle = 90, endAbsoluteY = 3, tag = $edge)`
2025-04-09 14:55:15 -05:00
|
|
|
if (err(maybeError)) {
|
|
|
|
return Promise.reject(maybeError)
|
|
|
|
}
|
2024-11-18 10:04:09 -05:00
|
|
|
}
|
|
|
|
|
2025-03-17 18:26:11 -07:00
|
|
|
const { execState } = await executeAstMock({
|
2024-11-18 10:04:09 -05:00
|
|
|
ast: truncatedAst,
|
2025-04-01 23:54:26 -07:00
|
|
|
rustContext: this.rustContext,
|
2024-11-18 10:04:09 -05:00
|
|
|
})
|
2025-02-15 00:57:04 +11:00
|
|
|
const sketch = sketchFromKclValue(execState.variables[varName], varName)
|
2024-11-18 10:04:09 -05:00
|
|
|
if (err(sketch)) return Promise.reject(sketch)
|
|
|
|
const sgPaths = sketch.paths
|
2025-04-01 23:54:26 -07:00
|
|
|
const orthoFactor = orthoScale(this.sceneInfra.camControls.camera)
|
2024-11-18 10:04:09 -05:00
|
|
|
|
2025-02-15 00:57:04 +11:00
|
|
|
const varDecIndex = Number(updatedEntryNodePath[1][0])
|
|
|
|
|
|
|
|
this.updateSegment(
|
|
|
|
sketch.start,
|
|
|
|
0,
|
|
|
|
varDecIndex,
|
|
|
|
_ast,
|
|
|
|
orthoFactor,
|
|
|
|
sketch
|
|
|
|
)
|
2024-11-18 10:04:09 -05:00
|
|
|
sgPaths.forEach((seg, index) =>
|
2025-02-15 00:57:04 +11:00
|
|
|
this.updateSegment(seg, index, varDecIndex, _ast, orthoFactor, sketch)
|
2024-11-18 10:04:09 -05:00
|
|
|
)
|
|
|
|
},
|
|
|
|
onClick: async (args) => {
|
|
|
|
// If there is a valid camera interaction that matches, do that instead
|
2025-04-01 23:54:26 -07:00
|
|
|
const interaction = this.sceneInfra.camControls.getInteractionType(
|
2024-11-18 10:04:09 -05:00
|
|
|
args.mouseEvent
|
|
|
|
)
|
|
|
|
if (interaction !== 'none') return
|
|
|
|
// Commit the rectangle to the full AST/code and return to sketch.idle
|
|
|
|
const cornerPoint = args.intersectionPoint?.twoD
|
|
|
|
if (!cornerPoint || args.mouseEvent.button !== 0) return
|
|
|
|
|
|
|
|
const x = roundOff((cornerPoint.x || 0) - rectangleOrigin[0])
|
|
|
|
const y = roundOff((cornerPoint.y || 0) - rectangleOrigin[1])
|
|
|
|
|
|
|
|
const _node = getNodeFromPath<VariableDeclaration>(
|
|
|
|
_ast,
|
2025-02-15 00:57:04 +11:00
|
|
|
updatedEntryNodePath,
|
2024-11-18 10:04:09 -05:00
|
|
|
'VariableDeclaration'
|
|
|
|
)
|
|
|
|
if (trap(_node)) return
|
2024-12-07 07:16:04 +13:00
|
|
|
const sketchInit = _node.node?.declaration.init
|
2024-11-18 10:04:09 -05:00
|
|
|
|
|
|
|
if (sketchInit.type === 'PipeExpression') {
|
KCL: Angled line should use keyword args (#5803)
We continue migrating KCL stdlib functions to use keyword arguments. Next up is the `angledLine` family of functions (except `angledLineThatIntersects, which will be a quick follow-up).
Before vs. after:
`angledLine({angle = 90, length = 3}, %, $edge)`
=> `angledLine(angle = 90, length = 3, tag = $edge)`
`angledLineOfXLength({angle = 90, length = 3}, %, $edge)`
=> `angledLine(angle = 90, lengthX = 3, tag = $edge)`
`angledLineOfYLength({angle = 90, length = 3}, %, $edge)`
=> `angledLine(angle = 90, lengthY = 3, tag = $edge)`
`angledLineToX({angle = 90, length = 3}, %, $edge)`
=> `angledLine(angle = 90, endAbsoluteX = 3, tag = $edge)`
`angledLineToY({angle = 90, length = 3}, %, $edge)`
=> `angledLine(angle = 90, endAbsoluteY = 3, tag = $edge)`
2025-04-09 14:55:15 -05:00
|
|
|
const maybeError = updateCenterRectangleSketch(
|
2024-11-18 10:04:09 -05:00
|
|
|
sketchInit,
|
|
|
|
x,
|
|
|
|
y,
|
2025-02-15 00:57:04 +11:00
|
|
|
tag,
|
2024-11-18 10:04:09 -05:00
|
|
|
rectangleOrigin[0],
|
|
|
|
rectangleOrigin[1]
|
|
|
|
)
|
KCL: Angled line should use keyword args (#5803)
We continue migrating KCL stdlib functions to use keyword arguments. Next up is the `angledLine` family of functions (except `angledLineThatIntersects, which will be a quick follow-up).
Before vs. after:
`angledLine({angle = 90, length = 3}, %, $edge)`
=> `angledLine(angle = 90, length = 3, tag = $edge)`
`angledLineOfXLength({angle = 90, length = 3}, %, $edge)`
=> `angledLine(angle = 90, lengthX = 3, tag = $edge)`
`angledLineOfYLength({angle = 90, length = 3}, %, $edge)`
=> `angledLine(angle = 90, lengthY = 3, tag = $edge)`
`angledLineToX({angle = 90, length = 3}, %, $edge)`
=> `angledLine(angle = 90, endAbsoluteX = 3, tag = $edge)`
`angledLineToY({angle = 90, length = 3}, %, $edge)`
=> `angledLine(angle = 90, endAbsoluteY = 3, tag = $edge)`
2025-04-09 14:55:15 -05:00
|
|
|
if (err(maybeError)) {
|
|
|
|
return Promise.reject(maybeError)
|
|
|
|
}
|
2024-11-18 10:04:09 -05:00
|
|
|
|
2024-12-06 13:57:31 +13:00
|
|
|
const pResult = parse(recast(_ast))
|
|
|
|
if (trap(pResult) || !resultIsOk(pResult))
|
|
|
|
return Promise.reject(pResult)
|
|
|
|
_ast = pResult.program
|
2024-11-18 10:04:09 -05:00
|
|
|
|
|
|
|
// Update the primary AST and unequip the rectangle tool
|
2025-03-25 05:06:27 -04:00
|
|
|
//
|
2024-11-19 11:10:08 -05:00
|
|
|
// lee: I had this at the bottom of the function, but it's
|
|
|
|
// possible sketchFromKclValue "fails" when sketching on a face,
|
|
|
|
// and this couldn't wouldn't run.
|
2025-03-25 05:06:27 -04:00
|
|
|
await updateModelingState(_ast, EXECUTION_TYPE_MOCK, {
|
2025-04-01 23:54:26 -07:00
|
|
|
kclManager: this.kclManager,
|
|
|
|
editorManager: this.editorManager,
|
|
|
|
codeManager: this.codeManager,
|
2025-03-25 05:06:27 -04:00
|
|
|
})
|
2025-04-01 23:54:26 -07:00
|
|
|
this.sceneInfra.modelingSend({ type: 'Finish center rectangle' })
|
2024-11-18 10:04:09 -05:00
|
|
|
}
|
|
|
|
},
|
|
|
|
})
|
2025-02-15 00:57:04 +11:00
|
|
|
return {
|
|
|
|
updatedEntryNodePath,
|
|
|
|
updatedSketchNodePaths,
|
|
|
|
expressionIndexToDelete: insertIndex,
|
|
|
|
}
|
2024-11-18 10:04:09 -05:00
|
|
|
}
|
2025-02-15 00:57:04 +11:00
|
|
|
setupDraftCircleThreePoint = async (
|
|
|
|
sketchEntryNodePath: PathToNode,
|
|
|
|
sketchNodePaths: PathToNode[],
|
|
|
|
planeNodePath: PathToNode,
|
|
|
|
forward: [number, number, number],
|
|
|
|
up: [number, number, number],
|
|
|
|
sketchOrigin: [number, number, number],
|
|
|
|
point1: [x: number, y: number],
|
|
|
|
point2: [x: number, y: number]
|
|
|
|
): Promise<SketchDetailsUpdate | Error> => {
|
2025-04-01 23:54:26 -07:00
|
|
|
let _ast = structuredClone(this.kclManager.ast)
|
2024-12-20 14:30:37 -05:00
|
|
|
|
2025-02-15 00:57:04 +11:00
|
|
|
const varDec = getNodeFromPath<VariableDeclarator>(
|
|
|
|
_ast,
|
|
|
|
planeNodePath,
|
|
|
|
'VariableDeclarator'
|
|
|
|
)
|
2024-12-20 14:30:37 -05:00
|
|
|
|
2025-02-15 00:57:04 +11:00
|
|
|
if (err(varDec)) return varDec
|
|
|
|
if (varDec.node.type !== 'VariableDeclarator') return new Error('not a var')
|
|
|
|
|
|
|
|
const varName = findUniqueName(_ast, 'profile')
|
|
|
|
|
|
|
|
const thirdPointCloseToWhereUserLastClicked = `[${roundOff(
|
|
|
|
point2[0] + 0.1,
|
|
|
|
2
|
|
|
|
)}, ${roundOff(point2[1] + 0.1, 2)}]`
|
|
|
|
const newExpression = createNodeFromExprSnippet`${varName} = circleThreePoint(
|
|
|
|
${varDec.node.id.name},
|
|
|
|
p1 = [${roundOff(point1[0], 2)}, ${roundOff(point1[1], 2)}],
|
|
|
|
p2 = [${roundOff(point2[0], 2)}, ${roundOff(point2[1], 2)}],
|
|
|
|
p3 = ${thirdPointCloseToWhereUserLastClicked},
|
|
|
|
)`
|
|
|
|
if (err(newExpression)) return newExpression
|
|
|
|
const insertIndex = getInsertIndex(sketchNodePaths, planeNodePath, 'end')
|
|
|
|
|
|
|
|
_ast.body.splice(insertIndex, 0, newExpression)
|
|
|
|
const { updatedEntryNodePath, updatedSketchNodePaths } =
|
|
|
|
updateSketchNodePathsWithInsertIndex({
|
|
|
|
insertIndex,
|
|
|
|
insertType: 'end',
|
|
|
|
sketchNodePaths,
|
2025-01-16 11:10:36 -05:00
|
|
|
})
|
2024-12-20 14:30:37 -05:00
|
|
|
|
2025-02-15 00:57:04 +11:00
|
|
|
const pResult = parse(recast(_ast))
|
|
|
|
if (trap(pResult) || !resultIsOk(pResult)) return Promise.reject(pResult)
|
|
|
|
_ast = pResult.program
|
2024-12-20 14:30:37 -05:00
|
|
|
|
2025-02-15 00:57:04 +11:00
|
|
|
// do a quick mock execution to get the program memory up-to-date
|
2025-04-12 00:16:45 +10:00
|
|
|
const didReParse = await this.kclManager.executeAstMock(_ast)
|
|
|
|
if (err(didReParse)) return didReParse
|
2024-12-20 14:30:37 -05:00
|
|
|
|
2025-02-15 00:57:04 +11:00
|
|
|
const { truncatedAst } = await this.setupSketch({
|
|
|
|
sketchEntryNodePath: updatedEntryNodePath,
|
|
|
|
sketchNodePaths: updatedSketchNodePaths,
|
|
|
|
forward,
|
|
|
|
up,
|
|
|
|
position: sketchOrigin,
|
|
|
|
maybeModdedAst: _ast,
|
|
|
|
draftExpressionsIndices: { start: 0, end: 0 },
|
|
|
|
})
|
2024-12-20 14:30:37 -05:00
|
|
|
|
2025-04-01 23:54:26 -07:00
|
|
|
this.sceneInfra.setCallbacks({
|
2025-02-15 00:57:04 +11:00
|
|
|
onMove: async (args) => {
|
|
|
|
const firstProfileIndex = Number(updatedSketchNodePaths[0][1][0])
|
|
|
|
const nodePathWithCorrectedIndexForTruncatedAst =
|
|
|
|
structuredClone(updatedEntryNodePath)
|
2024-12-20 14:30:37 -05:00
|
|
|
|
2025-02-15 00:57:04 +11:00
|
|
|
nodePathWithCorrectedIndexForTruncatedAst[1][0] =
|
|
|
|
Number(nodePathWithCorrectedIndexForTruncatedAst[1][0]) -
|
|
|
|
firstProfileIndex
|
|
|
|
const _node = getNodeFromPath<VariableDeclaration>(
|
|
|
|
truncatedAst,
|
|
|
|
nodePathWithCorrectedIndexForTruncatedAst,
|
|
|
|
'VariableDeclaration'
|
|
|
|
)
|
|
|
|
let modded = structuredClone(truncatedAst)
|
|
|
|
if (trap(_node)) return
|
|
|
|
const sketchInit = _node.node.declaration.init
|
2025-01-16 11:10:36 -05:00
|
|
|
|
2025-02-15 00:57:04 +11:00
|
|
|
if (sketchInit.type === 'CallExpressionKw') {
|
|
|
|
const moddedResult = changeSketchArguments(
|
|
|
|
modded,
|
2025-04-01 23:54:26 -07:00
|
|
|
this.kclManager.variables,
|
2025-02-15 00:57:04 +11:00
|
|
|
{
|
|
|
|
type: 'path',
|
|
|
|
pathToNode: nodePathWithCorrectedIndexForTruncatedAst,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
type: 'circle-three-point-segment',
|
|
|
|
p1: [point1[0], point1[1]],
|
|
|
|
p2: [point2[0], point2[1]],
|
|
|
|
p3: [
|
|
|
|
args.intersectionPoint.twoD.x,
|
|
|
|
args.intersectionPoint.twoD.y,
|
|
|
|
],
|
|
|
|
}
|
|
|
|
)
|
|
|
|
if (err(moddedResult)) return
|
|
|
|
modded = moddedResult.modifiedAst
|
2025-01-16 11:10:36 -05:00
|
|
|
}
|
|
|
|
|
2025-03-17 18:26:11 -07:00
|
|
|
const { execState } = await executeAstMock({
|
2025-02-15 00:57:04 +11:00
|
|
|
ast: modded,
|
2025-04-01 23:54:26 -07:00
|
|
|
rustContext: this.rustContext,
|
2025-02-15 00:57:04 +11:00
|
|
|
})
|
|
|
|
const sketch = sketchFromKclValue(execState.variables[varName], varName)
|
|
|
|
if (err(sketch)) return
|
|
|
|
const sgPaths = sketch.paths
|
2025-04-01 23:54:26 -07:00
|
|
|
const orthoFactor = orthoScale(this.sceneInfra.camControls.camera)
|
2025-01-06 14:08:18 -05:00
|
|
|
|
2025-02-15 00:57:04 +11:00
|
|
|
const varDecIndex = Number(updatedEntryNodePath[1][0])
|
2025-01-16 11:10:36 -05:00
|
|
|
|
2025-02-15 00:57:04 +11:00
|
|
|
this.updateSegment(
|
|
|
|
sketch.start,
|
|
|
|
0,
|
|
|
|
varDecIndex,
|
|
|
|
_ast,
|
|
|
|
orthoFactor,
|
|
|
|
sketch
|
2025-01-16 11:10:36 -05:00
|
|
|
)
|
2025-02-15 00:57:04 +11:00
|
|
|
sgPaths.forEach((seg, index) =>
|
|
|
|
this.updateSegment(seg, index, varDecIndex, _ast, orthoFactor, sketch)
|
2024-12-20 14:30:37 -05:00
|
|
|
)
|
2025-02-15 00:57:04 +11:00
|
|
|
},
|
|
|
|
onClick: async (args) => {
|
|
|
|
// If there is a valid camera interaction that matches, do that instead
|
2025-04-01 23:54:26 -07:00
|
|
|
const interaction = this.sceneInfra.camControls.getInteractionType(
|
2025-02-15 00:57:04 +11:00
|
|
|
args.mouseEvent
|
2025-01-16 11:10:36 -05:00
|
|
|
)
|
2025-02-15 00:57:04 +11:00
|
|
|
if (interaction !== 'none') return
|
|
|
|
// Commit the rectangle to the full AST/code and return to sketch.idle
|
|
|
|
const cornerPoint = args.intersectionPoint?.twoD
|
|
|
|
if (!cornerPoint || args.mouseEvent.button !== 0) return
|
2025-01-16 11:10:36 -05:00
|
|
|
|
2025-02-15 00:57:04 +11:00
|
|
|
const _node = getNodeFromPath<VariableDeclaration>(
|
|
|
|
_ast,
|
|
|
|
updatedEntryNodePath || [],
|
|
|
|
'VariableDeclaration'
|
|
|
|
)
|
|
|
|
if (trap(_node)) return
|
|
|
|
const sketchInit = _node.node?.declaration.init
|
2025-01-16 11:10:36 -05:00
|
|
|
|
2025-02-15 00:57:04 +11:00
|
|
|
let modded = structuredClone(_ast)
|
|
|
|
if (sketchInit.type === 'CallExpressionKw') {
|
|
|
|
const moddedResult = changeSketchArguments(
|
|
|
|
modded,
|
2025-04-01 23:54:26 -07:00
|
|
|
this.kclManager.variables,
|
2025-02-15 00:57:04 +11:00
|
|
|
{
|
|
|
|
type: 'path',
|
|
|
|
pathToNode: updatedEntryNodePath,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
type: 'circle-three-point-segment',
|
|
|
|
p1: [point1[0], point1[1]],
|
|
|
|
p2: [point2[0], point2[1]],
|
|
|
|
p3: [cornerPoint.x || 0, cornerPoint.y || 0],
|
|
|
|
}
|
2025-01-16 11:10:36 -05:00
|
|
|
)
|
2025-02-15 00:57:04 +11:00
|
|
|
if (err(moddedResult)) return
|
|
|
|
modded = moddedResult.modifiedAst
|
2024-12-20 14:30:37 -05:00
|
|
|
|
2025-02-15 00:57:04 +11:00
|
|
|
const newCode = recast(modded)
|
|
|
|
if (err(newCode)) return
|
|
|
|
const pResult = parse(newCode)
|
|
|
|
if (trap(pResult) || !resultIsOk(pResult))
|
|
|
|
return Promise.reject(pResult)
|
|
|
|
_ast = pResult.program
|
2024-12-20 14:30:37 -05:00
|
|
|
|
2025-02-15 00:57:04 +11:00
|
|
|
// Update the primary AST and unequip the rectangle tool
|
2025-03-25 05:06:27 -04:00
|
|
|
await updateModelingState(_ast, EXECUTION_TYPE_MOCK, {
|
2025-04-01 23:54:26 -07:00
|
|
|
kclManager: this.kclManager,
|
|
|
|
editorManager: this.editorManager,
|
|
|
|
codeManager: this.codeManager,
|
2025-03-25 05:06:27 -04:00
|
|
|
})
|
2025-04-01 23:54:26 -07:00
|
|
|
this.sceneInfra.modelingSend({ type: 'Finish circle three point' })
|
2025-02-15 00:57:04 +11:00
|
|
|
}
|
2024-12-20 14:30:37 -05:00
|
|
|
},
|
|
|
|
})
|
2025-02-15 00:57:04 +11:00
|
|
|
return {
|
|
|
|
updatedEntryNodePath,
|
|
|
|
updatedSketchNodePaths,
|
|
|
|
expressionIndexToDelete: insertIndex,
|
|
|
|
}
|
2024-12-20 14:30:37 -05:00
|
|
|
}
|
2025-03-18 11:14:12 +11:00
|
|
|
setupDraftArc = async (
|
2025-02-15 00:57:04 +11:00
|
|
|
sketchEntryNodePath: PathToNode,
|
|
|
|
sketchNodePaths: PathToNode[],
|
|
|
|
planeNodePath: PathToNode,
|
2024-09-23 22:42:51 +10:00
|
|
|
forward: [number, number, number],
|
|
|
|
up: [number, number, number],
|
|
|
|
sketchOrigin: [number, number, number],
|
2025-03-18 11:14:12 +11:00
|
|
|
center: [x: number, y: number]
|
2025-02-15 00:57:04 +11:00
|
|
|
): Promise<SketchDetailsUpdate | Error> => {
|
2025-04-01 23:54:26 -07:00
|
|
|
let _ast = structuredClone(this.kclManager.ast)
|
2024-09-23 22:42:51 +10:00
|
|
|
|
2025-03-18 11:14:12 +11:00
|
|
|
const _node1 = getNodeFromPath<VariableDeclaration>(
|
2024-09-23 22:42:51 +10:00
|
|
|
_ast,
|
2025-03-18 11:14:12 +11:00
|
|
|
sketchEntryNodePath || [],
|
|
|
|
'VariableDeclaration'
|
2024-09-23 22:42:51 +10:00
|
|
|
)
|
2025-03-18 11:14:12 +11:00
|
|
|
if (trap(_node1)) return Promise.reject(_node1)
|
|
|
|
const variableDeclarationName = _node1.node?.declaration.id?.name || ''
|
2024-09-23 22:42:51 +10:00
|
|
|
|
2025-03-18 11:14:12 +11:00
|
|
|
const sg = sketchFromKclValue(
|
2025-04-01 23:54:26 -07:00
|
|
|
this.kclManager.variables[variableDeclarationName],
|
2025-03-18 11:14:12 +11:00
|
|
|
variableDeclarationName
|
2025-02-15 00:57:04 +11:00
|
|
|
)
|
2025-03-18 11:14:12 +11:00
|
|
|
if (err(sg)) return Promise.reject(sg)
|
|
|
|
const lastSeg = sg?.paths?.slice(-1)[0] || sg.start
|
2025-02-15 00:57:04 +11:00
|
|
|
|
2025-03-18 11:14:12 +11:00
|
|
|
// Calculate a default center point and radius based on the last segment's endpoint
|
|
|
|
const from: [number, number] = [lastSeg.to[0], lastSeg.to[1]]
|
|
|
|
const radius = Math.sqrt(
|
|
|
|
(center[0] - from[0]) ** 2 + (center[1] - from[1]) ** 2
|
|
|
|
)
|
|
|
|
const startAngle = Math.atan2(from[1] - center[1], from[0] - center[0])
|
|
|
|
const endAngle = startAngle + Math.PI / 180 // arbitrary 1 degree arc as starting default
|
|
|
|
const to: [number, number] = [
|
|
|
|
center[0] + radius * Math.cos(endAngle),
|
|
|
|
center[1] + radius * Math.sin(endAngle),
|
|
|
|
]
|
2025-02-15 00:57:04 +11:00
|
|
|
|
2025-03-18 11:14:12 +11:00
|
|
|
// Use addNewSketchLn to append an arc to the existing sketch
|
|
|
|
const mod = addNewSketchLn({
|
|
|
|
node: _ast,
|
2025-04-01 23:54:26 -07:00
|
|
|
variables: this.kclManager.variables,
|
2025-03-18 11:14:12 +11:00
|
|
|
input: {
|
|
|
|
type: 'arc-segment',
|
|
|
|
from,
|
|
|
|
to,
|
|
|
|
center,
|
|
|
|
radius,
|
|
|
|
ccw: true,
|
|
|
|
},
|
|
|
|
fnName: 'arc' as ToolTip,
|
|
|
|
pathToNode: sketchEntryNodePath,
|
|
|
|
})
|
2024-09-23 22:42:51 +10:00
|
|
|
|
2025-03-18 11:14:12 +11:00
|
|
|
if (trap(mod)) return Promise.reject(mod)
|
|
|
|
const pResult = parse(recast(mod.modifiedAst))
|
2024-12-06 13:57:31 +13:00
|
|
|
if (trap(pResult) || !resultIsOk(pResult)) return Promise.reject(pResult)
|
|
|
|
_ast = pResult.program
|
2024-09-23 22:42:51 +10:00
|
|
|
|
|
|
|
// do a quick mock execution to get the program memory up-to-date
|
2025-04-12 00:16:45 +10:00
|
|
|
const didReParse = await this.kclManager.executeAstMock(_ast)
|
|
|
|
if (err(didReParse)) return didReParse
|
2024-09-23 22:42:51 +10:00
|
|
|
|
2025-03-18 11:14:12 +11:00
|
|
|
const index = sg.paths.length // because we've added a new segment that's not in the memory yet
|
|
|
|
const draftExpressionsIndices = { start: index, end: index }
|
|
|
|
|
2025-04-01 23:54:26 -07:00
|
|
|
this.sceneInfra.resetMouseListeners()
|
2025-03-18 11:14:12 +11:00
|
|
|
|
2025-02-12 10:22:56 +13:00
|
|
|
const { truncatedAst } = await this.setupSketch({
|
2025-03-18 11:14:12 +11:00
|
|
|
sketchEntryNodePath,
|
|
|
|
sketchNodePaths,
|
2024-09-23 22:42:51 +10:00
|
|
|
forward,
|
|
|
|
up,
|
|
|
|
position: sketchOrigin,
|
|
|
|
maybeModdedAst: _ast,
|
2025-03-18 11:14:12 +11:00
|
|
|
draftExpressionsIndices,
|
2024-09-23 22:42:51 +10:00
|
|
|
})
|
|
|
|
|
2025-04-01 23:54:26 -07:00
|
|
|
this.sceneInfra.setCallbacks({
|
2024-09-23 22:42:51 +10:00
|
|
|
onMove: async (args) => {
|
2025-03-18 11:14:12 +11:00
|
|
|
const firstProfileIndex = Number(sketchNodePaths[0][1][0])
|
|
|
|
const nodePathWithCorrectedIndexForTruncatedAst = structuredClone(
|
|
|
|
mod.pathToNode
|
|
|
|
)
|
|
|
|
|
2025-02-15 00:57:04 +11:00
|
|
|
nodePathWithCorrectedIndexForTruncatedAst[1][0] =
|
|
|
|
Number(nodePathWithCorrectedIndexForTruncatedAst[1][0]) -
|
2025-03-18 11:14:12 +11:00
|
|
|
firstProfileIndex
|
2024-09-23 22:42:51 +10:00
|
|
|
const _node = getNodeFromPath<VariableDeclaration>(
|
|
|
|
truncatedAst,
|
2025-02-15 00:57:04 +11:00
|
|
|
nodePathWithCorrectedIndexForTruncatedAst,
|
2024-09-23 22:42:51 +10:00
|
|
|
'VariableDeclaration'
|
|
|
|
)
|
|
|
|
let modded = structuredClone(truncatedAst)
|
|
|
|
if (trap(_node)) return
|
2024-12-07 07:16:04 +13:00
|
|
|
const sketchInit = _node.node.declaration.init
|
2024-09-23 22:42:51 +10:00
|
|
|
|
2025-03-18 11:14:12 +11:00
|
|
|
if (sketchInit.type === 'PipeExpression') {
|
|
|
|
// Calculate end angle based on mouse position
|
|
|
|
const endAngle = Math.atan2(
|
|
|
|
args.intersectionPoint.twoD.y - center[1],
|
|
|
|
args.intersectionPoint.twoD.x - center[0]
|
|
|
|
)
|
|
|
|
|
|
|
|
// Calculate the new 'to' point using the existing radius and the new end angle
|
|
|
|
const newTo: [number, number] = [
|
|
|
|
center[0] + radius * Math.cos(endAngle),
|
|
|
|
center[1] + radius * Math.sin(endAngle),
|
|
|
|
]
|
2024-09-23 22:42:51 +10:00
|
|
|
|
|
|
|
const moddedResult = changeSketchArguments(
|
|
|
|
modded,
|
2025-04-01 23:54:26 -07:00
|
|
|
this.kclManager.variables,
|
2024-09-23 22:42:51 +10:00
|
|
|
{
|
|
|
|
type: 'path',
|
2025-02-15 00:57:04 +11:00
|
|
|
pathToNode: nodePathWithCorrectedIndexForTruncatedAst,
|
2024-09-23 22:42:51 +10:00
|
|
|
},
|
|
|
|
{
|
|
|
|
type: 'arc-segment',
|
2025-03-18 11:14:12 +11:00
|
|
|
from: lastSeg.to,
|
|
|
|
to: newTo,
|
|
|
|
center: center,
|
|
|
|
radius: radius,
|
|
|
|
ccw: true,
|
2024-09-23 22:42:51 +10:00
|
|
|
}
|
|
|
|
)
|
2025-03-18 11:14:12 +11:00
|
|
|
if (err(moddedResult)) return
|
2024-09-23 22:42:51 +10:00
|
|
|
modded = moddedResult.modifiedAst
|
|
|
|
}
|
2025-03-18 13:56:08 +01:00
|
|
|
const { execState } = await executeAstMock({
|
2024-09-23 22:42:51 +10:00
|
|
|
ast: modded,
|
2025-04-01 23:54:26 -07:00
|
|
|
rustContext: this.rustContext,
|
2024-09-23 22:42:51 +10:00
|
|
|
})
|
2025-03-18 11:14:12 +11:00
|
|
|
const sketch = sketchFromKclValue(
|
|
|
|
execState.variables[variableDeclarationName],
|
|
|
|
variableDeclarationName
|
|
|
|
)
|
2024-09-27 15:44:44 -07:00
|
|
|
if (err(sketch)) return
|
2024-10-23 12:42:54 -05:00
|
|
|
const sgPaths = sketch.paths
|
2025-04-01 23:54:26 -07:00
|
|
|
const orthoFactor = orthoScale(this.sceneInfra.camControls.camera)
|
2024-09-23 22:42:51 +10:00
|
|
|
|
2025-03-18 11:14:12 +11:00
|
|
|
const varDecIndex = Number(sketchEntryNodePath[1][0])
|
2025-02-15 00:57:04 +11:00
|
|
|
|
|
|
|
this.updateSegment(
|
|
|
|
sketch.start,
|
|
|
|
0,
|
|
|
|
varDecIndex,
|
|
|
|
_ast,
|
|
|
|
orthoFactor,
|
|
|
|
sketch
|
|
|
|
)
|
2024-09-23 22:42:51 +10:00
|
|
|
sgPaths.forEach((seg, index) =>
|
2025-02-15 00:57:04 +11:00
|
|
|
this.updateSegment(seg, index, varDecIndex, _ast, orthoFactor, sketch)
|
2024-09-23 22:42:51 +10:00
|
|
|
)
|
|
|
|
},
|
|
|
|
onClick: async (args) => {
|
2025-03-18 11:14:12 +11:00
|
|
|
const firstProfileIndex = Number(sketchNodePaths[0][1][0])
|
|
|
|
const nodePathWithCorrectedIndexForTruncatedAst = structuredClone(
|
|
|
|
mod.pathToNode
|
|
|
|
)
|
|
|
|
|
|
|
|
nodePathWithCorrectedIndexForTruncatedAst[1][0] =
|
|
|
|
Number(nodePathWithCorrectedIndexForTruncatedAst[1][0]) -
|
|
|
|
firstProfileIndex
|
2024-10-24 06:26:33 -07:00
|
|
|
// If there is a valid camera interaction that matches, do that instead
|
2025-04-01 23:54:26 -07:00
|
|
|
const interaction = this.sceneInfra.camControls.getInteractionType(
|
2024-10-24 06:26:33 -07:00
|
|
|
args.mouseEvent
|
|
|
|
)
|
|
|
|
if (interaction !== 'none') return
|
2025-03-18 11:14:12 +11:00
|
|
|
// Commit the arc to the full AST/code and return to sketch.idle
|
|
|
|
const mousePoint = args.intersectionPoint?.twoD
|
|
|
|
if (!mousePoint || args.mouseEvent.button !== 0) return
|
2024-09-23 22:42:51 +10:00
|
|
|
|
|
|
|
const _node = getNodeFromPath<VariableDeclaration>(
|
|
|
|
_ast,
|
2025-03-18 11:14:12 +11:00
|
|
|
sketchEntryNodePath || [],
|
2024-09-23 22:42:51 +10:00
|
|
|
'VariableDeclaration'
|
|
|
|
)
|
|
|
|
if (trap(_node)) return
|
2024-12-07 07:16:04 +13:00
|
|
|
const sketchInit = _node.node?.declaration.init
|
2024-09-23 22:42:51 +10:00
|
|
|
|
|
|
|
let modded = structuredClone(_ast)
|
2025-03-18 11:14:12 +11:00
|
|
|
if (sketchInit.type === 'PipeExpression') {
|
|
|
|
// Calculate end angle based on final mouse position
|
|
|
|
const endAngle = Math.atan2(
|
|
|
|
mousePoint.y - center[1],
|
|
|
|
mousePoint.x - center[0]
|
|
|
|
)
|
|
|
|
|
|
|
|
// Calculate the final 'to' point using the existing radius and the final end angle
|
|
|
|
const finalTo: [number, number] = [
|
|
|
|
center[0] + radius * Math.cos(endAngle),
|
|
|
|
center[1] + radius * Math.sin(endAngle),
|
|
|
|
]
|
|
|
|
|
2024-09-23 22:42:51 +10:00
|
|
|
const moddedResult = changeSketchArguments(
|
|
|
|
modded,
|
2025-04-01 23:54:26 -07:00
|
|
|
this.kclManager.variables,
|
2024-09-23 22:42:51 +10:00
|
|
|
{
|
|
|
|
type: 'path',
|
2025-03-18 11:14:12 +11:00
|
|
|
pathToNode: mod.pathToNode,
|
2024-09-23 22:42:51 +10:00
|
|
|
},
|
|
|
|
{
|
|
|
|
type: 'arc-segment',
|
2025-03-18 11:14:12 +11:00
|
|
|
from: lastSeg.to,
|
|
|
|
to: finalTo,
|
|
|
|
center: center,
|
|
|
|
radius: radius,
|
|
|
|
ccw: true,
|
2024-09-23 22:42:51 +10:00
|
|
|
}
|
|
|
|
)
|
|
|
|
if (err(moddedResult)) return
|
|
|
|
modded = moddedResult.modifiedAst
|
|
|
|
|
2024-11-16 16:49:44 -05:00
|
|
|
const newCode = recast(modded)
|
|
|
|
if (err(newCode)) return
|
2024-12-06 13:57:31 +13:00
|
|
|
const pResult = parse(newCode)
|
|
|
|
if (trap(pResult) || !resultIsOk(pResult))
|
|
|
|
return Promise.reject(pResult)
|
|
|
|
_ast = pResult.program
|
2024-09-23 22:42:51 +10:00
|
|
|
|
2025-03-18 11:14:12 +11:00
|
|
|
// Update the primary AST and unequip the arc tool
|
2025-03-25 05:06:27 -04:00
|
|
|
await updateModelingState(_ast, EXECUTION_TYPE_MOCK, {
|
2025-04-01 23:54:26 -07:00
|
|
|
kclManager: this.kclManager,
|
|
|
|
editorManager: this.editorManager,
|
|
|
|
codeManager: this.codeManager,
|
2025-03-25 05:06:27 -04:00
|
|
|
})
|
2025-04-01 23:54:26 -07:00
|
|
|
this.sceneInfra.modelingSend({ type: 'Finish arc' })
|
2024-09-23 22:42:51 +10:00
|
|
|
}
|
|
|
|
},
|
|
|
|
})
|
2025-02-15 00:57:04 +11:00
|
|
|
return {
|
2025-03-18 11:14:12 +11:00
|
|
|
updatedEntryNodePath: sketchEntryNodePath,
|
|
|
|
updatedSketchNodePaths: sketchNodePaths,
|
|
|
|
expressionIndexToDelete: -1, // No need to delete any expression
|
2025-02-15 00:57:04 +11:00
|
|
|
}
|
2024-09-23 22:42:51 +10:00
|
|
|
}
|
2025-03-18 11:14:12 +11:00
|
|
|
setupDraftArcThreePoint = async (
|
|
|
|
sketchEntryNodePath: PathToNode,
|
|
|
|
sketchNodePaths: PathToNode[],
|
|
|
|
planeNodePath: PathToNode,
|
|
|
|
forward: [number, number, number],
|
|
|
|
up: [number, number, number],
|
|
|
|
sketchOrigin: [number, number, number],
|
|
|
|
p2: [x: number, y: number]
|
|
|
|
): Promise<SketchDetailsUpdate | Error> => {
|
2025-04-01 23:54:26 -07:00
|
|
|
let _ast = structuredClone(this.kclManager.ast)
|
2024-04-03 13:22:56 +11:00
|
|
|
|
2025-03-18 11:14:12 +11:00
|
|
|
const _node1 = getNodeFromPath<VariableDeclaration>(
|
|
|
|
_ast,
|
|
|
|
sketchEntryNodePath || [],
|
|
|
|
'VariableDeclaration'
|
|
|
|
)
|
|
|
|
if (trap(_node1)) return Promise.reject(_node1)
|
|
|
|
const variableDeclarationName = _node1.node?.declaration.id?.name || ''
|
|
|
|
|
|
|
|
const sg = sketchFromKclValue(
|
2025-04-01 23:54:26 -07:00
|
|
|
this.kclManager.variables[variableDeclarationName],
|
2025-03-18 11:14:12 +11:00
|
|
|
variableDeclarationName
|
|
|
|
)
|
|
|
|
if (err(sg)) return Promise.reject(sg)
|
|
|
|
const lastSeg = sg?.paths?.slice(-1)[0] || sg.start
|
|
|
|
|
|
|
|
// Calculate a default center point and radius based on the last segment's endpoint
|
|
|
|
const p1: [number, number] = [lastSeg.to[0], lastSeg.to[1]]
|
|
|
|
const p3: [number, number] = [p2[0] + 0.1, p2[1] + 0.1]
|
|
|
|
|
|
|
|
// Use addNewSketchLn to append an arc to the existing sketch
|
|
|
|
const mod = addNewSketchLn({
|
|
|
|
node: _ast,
|
2025-04-01 23:54:26 -07:00
|
|
|
variables: this.kclManager.variables,
|
2025-03-18 11:14:12 +11:00
|
|
|
input: {
|
|
|
|
type: 'circle-three-point-segment',
|
|
|
|
p1,
|
|
|
|
p2,
|
|
|
|
p3,
|
|
|
|
},
|
|
|
|
fnName: 'arcTo',
|
|
|
|
pathToNode: sketchEntryNodePath,
|
|
|
|
})
|
|
|
|
|
|
|
|
if (trap(mod)) return Promise.reject(mod)
|
|
|
|
const pResult = parse(recast(mod.modifiedAst))
|
|
|
|
if (trap(pResult) || !resultIsOk(pResult)) return Promise.reject(pResult)
|
|
|
|
_ast = pResult.program
|
|
|
|
|
|
|
|
// do a quick mock execution to get the program memory up-to-date
|
2025-04-12 00:16:45 +10:00
|
|
|
const didReParse = await this.kclManager.executeAstMock(_ast)
|
|
|
|
if (err(didReParse)) return didReParse
|
2025-03-18 11:14:12 +11:00
|
|
|
|
|
|
|
const index = sg.paths.length // because we've added a new segment that's not in the memory yet
|
|
|
|
const draftExpressionsIndices = { start: index, end: index }
|
|
|
|
|
|
|
|
// Get the insertion index from the modified path
|
|
|
|
const insertIndex = Number(mod.pathToNode[1][0])
|
|
|
|
|
2025-04-01 23:54:26 -07:00
|
|
|
this.sceneInfra.resetMouseListeners()
|
2025-03-18 11:14:12 +11:00
|
|
|
|
|
|
|
const { truncatedAst } = await this.setupSketch({
|
|
|
|
sketchEntryNodePath,
|
|
|
|
sketchNodePaths,
|
|
|
|
forward,
|
|
|
|
up,
|
|
|
|
position: sketchOrigin,
|
|
|
|
maybeModdedAst: _ast,
|
|
|
|
draftExpressionsIndices,
|
|
|
|
})
|
|
|
|
|
|
|
|
const doNotSnapAsThreePointArcIsTheOnlySegment = sg.paths.length === 0
|
|
|
|
|
2025-04-01 23:54:26 -07:00
|
|
|
this.sceneInfra.setCallbacks({
|
2025-03-18 11:14:12 +11:00
|
|
|
onMove: async (args) => {
|
|
|
|
const firstProfileIndex = Number(sketchNodePaths[0][1][0])
|
|
|
|
const nodePathWithCorrectedIndexForTruncatedAst = structuredClone(
|
|
|
|
mod.pathToNode
|
|
|
|
)
|
|
|
|
|
|
|
|
nodePathWithCorrectedIndexForTruncatedAst[1][0] =
|
|
|
|
Number(nodePathWithCorrectedIndexForTruncatedAst[1][0]) -
|
|
|
|
firstProfileIndex
|
|
|
|
const _node = getNodeFromPath<VariableDeclaration>(
|
|
|
|
truncatedAst,
|
|
|
|
nodePathWithCorrectedIndexForTruncatedAst,
|
|
|
|
'VariableDeclaration'
|
|
|
|
)
|
|
|
|
let modded = structuredClone(truncatedAst)
|
|
|
|
if (trap(_node)) return
|
|
|
|
const sketchInit = _node.node.declaration.init
|
|
|
|
|
2025-04-14 23:51:14 +02:00
|
|
|
const maybeSnapToAxis = this.getSnappedDragPoint(
|
|
|
|
args.intersectionPoint.twoD,
|
|
|
|
args.intersects,
|
|
|
|
args.mouseEvent
|
|
|
|
).snappedPoint
|
2025-03-18 11:14:12 +11:00
|
|
|
|
|
|
|
const maybeSnapToProfileStart = doNotSnapAsThreePointArcIsTheOnlySegment
|
|
|
|
? new Vector2(...maybeSnapToAxis)
|
|
|
|
: this.maybeSnapProfileStartIntersect2d({
|
|
|
|
sketchEntryNodePath,
|
|
|
|
intersects: args.intersects,
|
|
|
|
intersection2d: new Vector2(...maybeSnapToAxis),
|
|
|
|
})
|
|
|
|
|
|
|
|
if (sketchInit.type === 'PipeExpression') {
|
|
|
|
const moddedResult = changeSketchArguments(
|
|
|
|
modded,
|
2025-04-01 23:54:26 -07:00
|
|
|
this.kclManager.variables,
|
2025-03-18 11:14:12 +11:00
|
|
|
{
|
|
|
|
type: 'path',
|
|
|
|
pathToNode: nodePathWithCorrectedIndexForTruncatedAst,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
type: 'circle-three-point-segment',
|
|
|
|
p1,
|
|
|
|
p2,
|
|
|
|
p3: [maybeSnapToProfileStart.x, maybeSnapToProfileStart.y],
|
|
|
|
}
|
|
|
|
)
|
|
|
|
if (err(moddedResult)) return
|
|
|
|
modded = moddedResult.modifiedAst
|
|
|
|
}
|
2025-03-18 13:56:08 +01:00
|
|
|
const { execState } = await executeAstMock({
|
2025-03-18 11:14:12 +11:00
|
|
|
ast: modded,
|
2025-04-01 23:54:26 -07:00
|
|
|
rustContext: this.rustContext,
|
2025-03-18 11:14:12 +11:00
|
|
|
})
|
|
|
|
const sketch = sketchFromKclValue(
|
|
|
|
execState.variables[variableDeclarationName],
|
|
|
|
variableDeclarationName
|
|
|
|
)
|
|
|
|
if (err(sketch)) return
|
|
|
|
const sgPaths = sketch.paths
|
2025-04-01 23:54:26 -07:00
|
|
|
const orthoFactor = orthoScale(this.sceneInfra.camControls.camera)
|
2025-03-18 11:14:12 +11:00
|
|
|
|
|
|
|
const varDecIndex = Number(sketchEntryNodePath[1][0])
|
|
|
|
|
|
|
|
this.updateSegment(
|
|
|
|
sketch.start,
|
|
|
|
0,
|
|
|
|
varDecIndex,
|
|
|
|
_ast,
|
|
|
|
orthoFactor,
|
|
|
|
sketch
|
|
|
|
)
|
|
|
|
sgPaths.forEach((seg, index) =>
|
|
|
|
this.updateSegment(seg, index, varDecIndex, _ast, orthoFactor, sketch)
|
|
|
|
)
|
|
|
|
},
|
|
|
|
onClick: async (args) => {
|
|
|
|
const firstProfileIndex = Number(sketchNodePaths[0][1][0])
|
|
|
|
const nodePathWithCorrectedIndexForTruncatedAst = structuredClone(
|
|
|
|
mod.pathToNode
|
|
|
|
)
|
|
|
|
|
|
|
|
nodePathWithCorrectedIndexForTruncatedAst[1][0] =
|
|
|
|
Number(nodePathWithCorrectedIndexForTruncatedAst[1][0]) -
|
|
|
|
firstProfileIndex
|
|
|
|
// If there is a valid camera interaction that matches, do that instead
|
2025-04-01 23:54:26 -07:00
|
|
|
const interaction = this.sceneInfra.camControls.getInteractionType(
|
2025-03-18 11:14:12 +11:00
|
|
|
args.mouseEvent
|
|
|
|
)
|
|
|
|
if (interaction !== 'none') return
|
|
|
|
// Commit the arc to the full AST/code and return to sketch.idle
|
|
|
|
const mousePoint = args.intersectionPoint?.twoD
|
|
|
|
if (!mousePoint || args.mouseEvent.button !== 0) return
|
|
|
|
|
|
|
|
const _node = getNodeFromPath<VariableDeclaration>(
|
|
|
|
_ast,
|
|
|
|
sketchEntryNodePath || [],
|
|
|
|
'VariableDeclaration'
|
|
|
|
)
|
|
|
|
if (trap(_node)) return
|
|
|
|
const sketchInit = _node.node?.declaration.init
|
|
|
|
|
|
|
|
let modded = structuredClone(_ast)
|
|
|
|
|
|
|
|
const intersectsProfileStart =
|
|
|
|
!doNotSnapAsThreePointArcIsTheOnlySegment &&
|
|
|
|
this.didIntersectProfileStart(args, sketchEntryNodePath)
|
|
|
|
if (sketchInit.type === 'PipeExpression' && args.intersectionPoint) {
|
|
|
|
// Calculate end angle based on final mouse position
|
|
|
|
|
|
|
|
const moddedResult = changeSketchArguments(
|
|
|
|
modded,
|
2025-04-01 23:54:26 -07:00
|
|
|
this.kclManager.variables,
|
2025-03-18 11:14:12 +11:00
|
|
|
{
|
|
|
|
type: 'path',
|
|
|
|
pathToNode: mod.pathToNode,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
type: 'circle-three-point-segment',
|
|
|
|
p1,
|
|
|
|
p2,
|
2025-04-14 23:51:14 +02:00
|
|
|
p3: this.getSnappedDragPoint(
|
|
|
|
args.intersectionPoint.twoD,
|
|
|
|
args.intersects,
|
|
|
|
args.mouseEvent
|
|
|
|
).snappedPoint,
|
2025-03-18 11:14:12 +11:00
|
|
|
}
|
|
|
|
)
|
|
|
|
if (err(moddedResult)) return
|
|
|
|
modded = moddedResult.modifiedAst
|
|
|
|
if (intersectsProfileStart) {
|
|
|
|
const originCoords = createArrayExpression([
|
2025-04-23 10:21:58 -05:00
|
|
|
createCallExpressionStdLibKw(
|
|
|
|
'profileStartX',
|
2025-03-18 11:14:12 +11:00
|
|
|
createPipeSubstitution(),
|
2025-04-23 10:21:58 -05:00
|
|
|
[]
|
|
|
|
),
|
|
|
|
createCallExpressionStdLibKw(
|
|
|
|
'profileStartY',
|
2025-03-18 11:14:12 +11:00
|
|
|
createPipeSubstitution(),
|
2025-04-23 10:21:58 -05:00
|
|
|
[]
|
|
|
|
),
|
2025-03-18 11:14:12 +11:00
|
|
|
])
|
2025-04-23 10:21:58 -05:00
|
|
|
|
2025-03-18 11:14:12 +11:00
|
|
|
const arcToCallExp = getNodeFromPath<CallExpression>(
|
|
|
|
modded,
|
|
|
|
mod.pathToNode,
|
|
|
|
'CallExpression'
|
|
|
|
)
|
|
|
|
if (err(arcToCallExp)) return
|
|
|
|
const firstArg = arcToCallExp.node.arguments[0]
|
|
|
|
if (firstArg.type !== 'ObjectExpression') return
|
|
|
|
for (const prop of firstArg.properties) {
|
|
|
|
if (prop.key.type === 'Identifier' && prop.key.name === 'end') {
|
|
|
|
prop.value = originCoords
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const moddedResult = addCloseToPipe({
|
|
|
|
node: modded,
|
2025-04-01 23:54:26 -07:00
|
|
|
variables: this.kclManager.variables,
|
2025-03-18 11:14:12 +11:00
|
|
|
pathToNode: sketchEntryNodePath,
|
|
|
|
})
|
|
|
|
if (err(moddedResult)) return
|
|
|
|
modded = moddedResult
|
|
|
|
}
|
|
|
|
|
|
|
|
const newCode = recast(modded)
|
|
|
|
if (err(newCode)) return
|
|
|
|
const pResult = parse(newCode)
|
|
|
|
if (trap(pResult) || !resultIsOk(pResult))
|
|
|
|
return Promise.reject(pResult)
|
|
|
|
_ast = pResult.program
|
|
|
|
|
|
|
|
// Update the primary AST and unequip the arc tool
|
2025-03-25 05:06:27 -04:00
|
|
|
await updateModelingState(_ast, EXECUTION_TYPE_MOCK, {
|
2025-04-01 23:54:26 -07:00
|
|
|
kclManager: this.kclManager,
|
|
|
|
editorManager: this.editorManager,
|
|
|
|
codeManager: this.codeManager,
|
2025-03-25 05:06:27 -04:00
|
|
|
})
|
2025-03-18 11:14:12 +11:00
|
|
|
if (intersectsProfileStart) {
|
2025-04-01 23:54:26 -07:00
|
|
|
this.sceneInfra.modelingSend({ type: 'Close sketch' })
|
2025-03-18 11:14:12 +11:00
|
|
|
} else {
|
2025-04-01 23:54:26 -07:00
|
|
|
this.sceneInfra.modelingSend({ type: 'Finish arc' })
|
2025-03-18 11:14:12 +11:00
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
})
|
|
|
|
return {
|
|
|
|
updatedEntryNodePath: mod.pathToNode,
|
|
|
|
updatedSketchNodePaths: sketchNodePaths,
|
|
|
|
expressionIndexToDelete: insertIndex, // Return the insertion index so it can be deleted if needed
|
|
|
|
}
|
|
|
|
}
|
|
|
|
setupDraftCircle = async (
|
|
|
|
sketchEntryNodePath: PathToNode,
|
|
|
|
sketchNodePaths: PathToNode[],
|
|
|
|
planeNodePath: PathToNode,
|
|
|
|
forward: [number, number, number],
|
|
|
|
up: [number, number, number],
|
|
|
|
sketchOrigin: [number, number, number],
|
|
|
|
circleCenter: [x: number, y: number]
|
|
|
|
): Promise<SketchDetailsUpdate | Error> => {
|
2025-04-01 23:54:26 -07:00
|
|
|
let _ast = structuredClone(this.kclManager.ast)
|
2025-03-18 11:14:12 +11:00
|
|
|
|
|
|
|
const varDec = getNodeFromPath<VariableDeclarator>(
|
|
|
|
_ast,
|
|
|
|
planeNodePath,
|
|
|
|
'VariableDeclarator'
|
|
|
|
)
|
|
|
|
|
|
|
|
if (err(varDec)) return varDec
|
|
|
|
if (varDec.node.type !== 'VariableDeclarator') return new Error('not a var')
|
|
|
|
|
|
|
|
const varName = findUniqueName(_ast, 'profile')
|
|
|
|
const newExpression = createVariableDeclaration(
|
|
|
|
varName,
|
2025-03-24 20:58:55 +13:00
|
|
|
createCallExpressionStdLibKw(
|
|
|
|
'circle',
|
|
|
|
createLocalName(varDec.node.id.name),
|
|
|
|
[
|
|
|
|
createLabeledArg(
|
|
|
|
'center',
|
|
|
|
createArrayExpression([
|
|
|
|
createLiteral(roundOff(circleCenter[0])),
|
|
|
|
createLiteral(roundOff(circleCenter[1])),
|
|
|
|
])
|
|
|
|
),
|
|
|
|
createLabeledArg('radius', createLiteral(1)),
|
|
|
|
]
|
|
|
|
)
|
2025-03-18 11:14:12 +11:00
|
|
|
)
|
|
|
|
|
|
|
|
const insertIndex = getInsertIndex(sketchNodePaths, planeNodePath, 'end')
|
|
|
|
|
|
|
|
_ast.body.splice(insertIndex, 0, newExpression)
|
|
|
|
const { updatedEntryNodePath, updatedSketchNodePaths } =
|
|
|
|
updateSketchNodePathsWithInsertIndex({
|
|
|
|
insertIndex,
|
|
|
|
insertType: 'end',
|
|
|
|
sketchNodePaths,
|
|
|
|
})
|
|
|
|
|
|
|
|
const pResult = parse(recast(_ast))
|
|
|
|
if (trap(pResult) || !resultIsOk(pResult)) return Promise.reject(pResult)
|
|
|
|
_ast = pResult.program
|
|
|
|
|
|
|
|
// do a quick mock execution to get the program memory up-to-date
|
2025-04-12 00:16:45 +10:00
|
|
|
const didReParse = await this.kclManager.executeAstMock(_ast)
|
|
|
|
if (err(didReParse)) return didReParse
|
2025-03-18 11:14:12 +11:00
|
|
|
|
|
|
|
const { truncatedAst } = await this.setupSketch({
|
|
|
|
sketchEntryNodePath: updatedEntryNodePath,
|
|
|
|
sketchNodePaths: updatedSketchNodePaths,
|
|
|
|
forward,
|
|
|
|
up,
|
|
|
|
position: sketchOrigin,
|
|
|
|
maybeModdedAst: _ast,
|
|
|
|
draftExpressionsIndices: { start: 0, end: 0 },
|
|
|
|
})
|
|
|
|
|
2025-04-01 23:54:26 -07:00
|
|
|
this.sceneInfra.setCallbacks({
|
2025-03-18 11:14:12 +11:00
|
|
|
onMove: async (args) => {
|
|
|
|
const nodePathWithCorrectedIndexForTruncatedAst =
|
|
|
|
structuredClone(updatedEntryNodePath)
|
|
|
|
nodePathWithCorrectedIndexForTruncatedAst[1][0] =
|
|
|
|
Number(nodePathWithCorrectedIndexForTruncatedAst[1][0]) -
|
|
|
|
Number(planeNodePath[1][0]) -
|
|
|
|
1
|
|
|
|
const _node = getNodeFromPath<VariableDeclaration>(
|
|
|
|
truncatedAst,
|
|
|
|
nodePathWithCorrectedIndexForTruncatedAst,
|
|
|
|
'VariableDeclaration'
|
|
|
|
)
|
|
|
|
let modded = structuredClone(truncatedAst)
|
|
|
|
if (trap(_node)) return
|
|
|
|
const sketchInit = _node.node.declaration.init
|
|
|
|
|
|
|
|
const x = (args.intersectionPoint.twoD.x || 0) - circleCenter[0]
|
|
|
|
const y = (args.intersectionPoint.twoD.y || 0) - circleCenter[1]
|
|
|
|
|
|
|
|
if (sketchInit.type === 'CallExpressionKw') {
|
|
|
|
const moddedResult = changeSketchArguments(
|
|
|
|
modded,
|
2025-04-01 23:54:26 -07:00
|
|
|
this.kclManager.variables,
|
2025-03-18 11:14:12 +11:00
|
|
|
{
|
|
|
|
type: 'path',
|
|
|
|
pathToNode: nodePathWithCorrectedIndexForTruncatedAst,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
type: 'arc-segment',
|
|
|
|
center: circleCenter,
|
|
|
|
radius: Math.sqrt(x ** 2 + y ** 2),
|
|
|
|
from: circleCenter,
|
|
|
|
to: circleCenter, // Same as from for a full circle
|
|
|
|
ccw: true,
|
|
|
|
}
|
|
|
|
)
|
|
|
|
if (err(moddedResult)) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
modded = moddedResult.modifiedAst
|
|
|
|
}
|
|
|
|
|
2025-03-17 18:26:11 -07:00
|
|
|
const { execState } = await executeAstMock({
|
2025-03-18 11:14:12 +11:00
|
|
|
ast: modded,
|
2025-04-01 23:54:26 -07:00
|
|
|
rustContext: this.rustContext,
|
2025-03-18 11:14:12 +11:00
|
|
|
})
|
|
|
|
const sketch = sketchFromKclValue(execState.variables[varName], varName)
|
|
|
|
if (err(sketch)) return
|
|
|
|
const sgPaths = sketch.paths
|
2025-04-01 23:54:26 -07:00
|
|
|
const orthoFactor = orthoScale(this.sceneInfra.camControls.camera)
|
2025-03-18 11:14:12 +11:00
|
|
|
|
|
|
|
const varDecIndex = Number(updatedEntryNodePath[1][0])
|
|
|
|
|
|
|
|
this.updateSegment(
|
|
|
|
sketch.start,
|
|
|
|
0,
|
|
|
|
varDecIndex,
|
|
|
|
_ast,
|
|
|
|
orthoFactor,
|
|
|
|
sketch
|
|
|
|
)
|
|
|
|
sgPaths.forEach((seg, index) =>
|
|
|
|
this.updateSegment(seg, index, varDecIndex, _ast, orthoFactor, sketch)
|
|
|
|
)
|
|
|
|
},
|
|
|
|
onClick: async (args) => {
|
|
|
|
// If there is a valid camera interaction that matches, do that instead
|
2025-04-01 23:54:26 -07:00
|
|
|
const interaction = this.sceneInfra.camControls.getInteractionType(
|
2025-03-18 11:14:12 +11:00
|
|
|
args.mouseEvent
|
|
|
|
)
|
|
|
|
if (interaction !== 'none') return
|
|
|
|
// Commit the rectangle to the full AST/code and return to sketch.idle
|
|
|
|
const cornerPoint = args.intersectionPoint?.twoD
|
|
|
|
if (!cornerPoint || args.mouseEvent.button !== 0) return
|
|
|
|
|
|
|
|
const x = roundOff((cornerPoint.x || 0) - circleCenter[0])
|
|
|
|
const y = roundOff((cornerPoint.y || 0) - circleCenter[1])
|
|
|
|
|
|
|
|
const _node = getNodeFromPath<VariableDeclaration>(
|
|
|
|
_ast,
|
|
|
|
updatedEntryNodePath || [],
|
|
|
|
'VariableDeclaration'
|
|
|
|
)
|
|
|
|
if (trap(_node)) return
|
|
|
|
const sketchInit = _node.node?.declaration.init
|
|
|
|
|
|
|
|
let modded = structuredClone(_ast)
|
|
|
|
if (sketchInit.type === 'CallExpressionKw') {
|
|
|
|
const moddedResult = changeSketchArguments(
|
|
|
|
modded,
|
2025-04-01 23:54:26 -07:00
|
|
|
this.kclManager.variables,
|
2025-03-18 11:14:12 +11:00
|
|
|
{
|
|
|
|
type: 'path',
|
|
|
|
pathToNode: updatedEntryNodePath,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
type: 'arc-segment',
|
|
|
|
center: circleCenter,
|
|
|
|
radius: Math.sqrt(x ** 2 + y ** 2),
|
|
|
|
from: circleCenter,
|
|
|
|
to: circleCenter, // Same as from for a full circle
|
|
|
|
ccw: true,
|
|
|
|
}
|
|
|
|
)
|
|
|
|
if (err(moddedResult)) return
|
|
|
|
modded = moddedResult.modifiedAst
|
|
|
|
|
|
|
|
const newCode = recast(modded)
|
|
|
|
if (err(newCode)) return
|
|
|
|
const pResult = parse(newCode)
|
|
|
|
if (trap(pResult) || !resultIsOk(pResult))
|
|
|
|
return Promise.reject(pResult)
|
|
|
|
_ast = pResult.program
|
|
|
|
|
|
|
|
// Update the primary AST and unequip the rectangle tool
|
2025-03-25 05:06:27 -04:00
|
|
|
await updateModelingState(_ast, EXECUTION_TYPE_MOCK, {
|
2025-04-01 23:54:26 -07:00
|
|
|
kclManager: this.kclManager,
|
|
|
|
editorManager: this.editorManager,
|
|
|
|
codeManager: this.codeManager,
|
2025-03-25 05:06:27 -04:00
|
|
|
})
|
2025-04-01 23:54:26 -07:00
|
|
|
this.sceneInfra.modelingSend({ type: 'Finish circle' })
|
2025-03-18 11:14:12 +11:00
|
|
|
}
|
|
|
|
},
|
|
|
|
})
|
|
|
|
return {
|
|
|
|
updatedEntryNodePath,
|
|
|
|
updatedSketchNodePaths,
|
|
|
|
expressionIndexToDelete: insertIndex,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
setupSketchIdleCallbacks = ({
|
|
|
|
sketchEntryNodePath,
|
|
|
|
sketchNodePaths,
|
|
|
|
planeNodePath,
|
|
|
|
up,
|
|
|
|
forward,
|
|
|
|
position,
|
|
|
|
}: {
|
|
|
|
sketchEntryNodePath: PathToNode
|
|
|
|
sketchNodePaths: PathToNode[]
|
|
|
|
planeNodePath: PathToNode
|
|
|
|
forward: [number, number, number]
|
|
|
|
up: [number, number, number]
|
|
|
|
position?: [number, number, number]
|
|
|
|
}) => {
|
|
|
|
let addingNewSegmentStatus: 'nothing' | 'pending' | 'added' = 'nothing'
|
2025-04-01 23:54:26 -07:00
|
|
|
this.sceneInfra.setCallbacks({
|
2025-03-18 11:14:12 +11:00
|
|
|
onDragEnd: async () => {
|
|
|
|
if (addingNewSegmentStatus !== 'nothing') {
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
|
|
this.setupSketch({
|
|
|
|
sketchEntryNodePath,
|
|
|
|
sketchNodePaths,
|
2025-04-01 23:54:26 -07:00
|
|
|
maybeModdedAst: this.kclManager.ast,
|
2025-03-18 11:14:12 +11:00
|
|
|
up,
|
|
|
|
forward,
|
|
|
|
position,
|
|
|
|
})
|
|
|
|
// setting up the callbacks again resets value in closures
|
|
|
|
this.setupSketchIdleCallbacks({
|
|
|
|
sketchEntryNodePath,
|
|
|
|
sketchNodePaths,
|
|
|
|
planeNodePath,
|
|
|
|
up,
|
|
|
|
forward,
|
|
|
|
position,
|
|
|
|
})
|
2025-04-01 23:54:26 -07:00
|
|
|
await this.codeManager.writeToFile()
|
2025-03-18 11:14:12 +11:00
|
|
|
}
|
|
|
|
},
|
|
|
|
onDrag: async ({
|
|
|
|
selected,
|
|
|
|
intersectionPoint,
|
|
|
|
mouseEvent,
|
|
|
|
intersects,
|
|
|
|
}) => {
|
|
|
|
if (mouseEvent.which !== 1) return
|
|
|
|
|
|
|
|
const group = getParentGroup(selected, [EXTRA_SEGMENT_HANDLE])
|
|
|
|
if (group?.name === EXTRA_SEGMENT_HANDLE) {
|
|
|
|
const segGroup = getParentGroup(selected)
|
|
|
|
const pathToNode: PathToNode = segGroup?.userData?.pathToNode
|
2024-04-03 13:22:56 +11:00
|
|
|
const pathToNodeIndex = pathToNode.findIndex(
|
|
|
|
(x) => x[1] === 'PipeExpression'
|
|
|
|
)
|
|
|
|
|
2024-09-27 15:44:44 -07:00
|
|
|
const sketch = sketchFromPathToNode({
|
2024-04-03 13:22:56 +11:00
|
|
|
pathToNode,
|
2025-04-01 23:54:26 -07:00
|
|
|
ast: this.kclManager.ast,
|
|
|
|
variables: this.kclManager.variables,
|
|
|
|
kclManager: this.kclManager,
|
2024-04-03 13:22:56 +11:00
|
|
|
})
|
2024-09-27 15:44:44 -07:00
|
|
|
if (trap(sketch)) return
|
|
|
|
if (!sketch) {
|
|
|
|
trap(new Error('sketch not found'))
|
2024-08-07 15:15:22 -04:00
|
|
|
return
|
|
|
|
}
|
2024-04-03 13:22:56 +11:00
|
|
|
|
|
|
|
const pipeIndex = pathToNode[pathToNodeIndex + 1][0] as number
|
|
|
|
if (addingNewSegmentStatus === 'nothing') {
|
2024-10-23 12:42:54 -05:00
|
|
|
const prevSegment = sketch.paths[pipeIndex - 2]
|
2024-04-03 13:22:56 +11:00
|
|
|
const mod = addNewSketchLn({
|
2025-04-01 23:54:26 -07:00
|
|
|
node: this.kclManager.ast,
|
|
|
|
variables: this.kclManager.variables,
|
2024-09-13 21:14:14 +10:00
|
|
|
input: {
|
|
|
|
type: 'straight-segment',
|
|
|
|
to: [intersectionPoint.twoD.x, intersectionPoint.twoD.y],
|
|
|
|
from: prevSegment.from,
|
|
|
|
},
|
2024-04-03 13:22:56 +11:00
|
|
|
// TODO assuming it's always a straight segments being added
|
|
|
|
// as this is easiest, and we'll need to add "tabbing" behavior
|
|
|
|
// to support other segment types
|
|
|
|
fnName: 'line',
|
|
|
|
pathToNode: pathToNode,
|
|
|
|
spliceBetween: true,
|
|
|
|
})
|
|
|
|
addingNewSegmentStatus = 'pending'
|
2024-06-24 11:45:40 -04:00
|
|
|
if (trap(mod)) return
|
|
|
|
|
2025-04-12 00:16:45 +10:00
|
|
|
const didReParse = await this.kclManager.executeAstMock(
|
|
|
|
mod.modifiedAst
|
|
|
|
)
|
|
|
|
if (err(didReParse)) return
|
2024-09-09 18:17:45 -04:00
|
|
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
2024-04-03 13:22:56 +11:00
|
|
|
this.setupSketch({
|
2025-02-15 00:57:04 +11:00
|
|
|
sketchEntryNodePath: pathToNode,
|
|
|
|
sketchNodePaths,
|
2025-04-01 23:54:26 -07:00
|
|
|
maybeModdedAst: this.kclManager.ast,
|
2024-04-03 13:22:56 +11:00
|
|
|
up,
|
|
|
|
forward,
|
|
|
|
position,
|
|
|
|
})
|
|
|
|
addingNewSegmentStatus = 'added'
|
|
|
|
} else if (addingNewSegmentStatus === 'added') {
|
|
|
|
const pathToNodeForNewSegment = pathToNode.slice(0, pathToNodeIndex)
|
|
|
|
pathToNodeForNewSegment.push([pipeIndex - 2, 'index'])
|
|
|
|
this.onDragSegment({
|
2025-02-15 00:57:04 +11:00
|
|
|
sketchNodePaths,
|
|
|
|
sketchEntryNodePath: pathToNodeForNewSegment,
|
2024-04-03 13:22:56 +11:00
|
|
|
object: selected,
|
|
|
|
intersection2d: intersectionPoint.twoD,
|
|
|
|
intersects,
|
2025-04-14 23:51:14 +02:00
|
|
|
mouseEvent: mouseEvent,
|
2024-04-03 13:22:56 +11:00
|
|
|
})
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2024-03-25 15:48:51 +11:00
|
|
|
this.onDragSegment({
|
|
|
|
object: selected,
|
|
|
|
intersection2d: intersectionPoint.twoD,
|
|
|
|
intersects,
|
2025-02-15 00:57:04 +11:00
|
|
|
sketchNodePaths,
|
|
|
|
sketchEntryNodePath,
|
2025-04-14 23:51:14 +02:00
|
|
|
mouseEvent: mouseEvent,
|
2024-03-25 15:48:51 +11:00
|
|
|
})
|
|
|
|
},
|
|
|
|
onMove: () => {},
|
|
|
|
onClick: (args) => {
|
2024-10-24 06:26:33 -07:00
|
|
|
// If there is a valid camera interaction that matches, do that instead
|
2025-04-01 23:54:26 -07:00
|
|
|
const interaction = this.sceneInfra.camControls.getInteractionType(
|
2024-10-24 06:26:33 -07:00
|
|
|
args.mouseEvent
|
|
|
|
)
|
|
|
|
if (interaction !== 'none') return
|
2024-03-25 15:48:51 +11:00
|
|
|
if (args?.mouseEvent.which !== 1) return
|
|
|
|
if (!args || !args.selected) {
|
2025-04-01 23:54:26 -07:00
|
|
|
this.sceneInfra.modelingSend({
|
2024-03-25 15:48:51 +11:00
|
|
|
type: 'Set selection',
|
|
|
|
data: {
|
|
|
|
selectionType: 'singleCodeCursor',
|
|
|
|
},
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
const { selected } = args
|
|
|
|
const event = getEventForSegmentSelection(selected)
|
|
|
|
if (!event) return
|
2025-04-01 23:54:26 -07:00
|
|
|
this.sceneInfra.modelingSend(event)
|
2024-03-25 15:48:51 +11:00
|
|
|
},
|
2024-04-04 11:07:51 +11:00
|
|
|
...this.mouseEnterLeaveCallbacks(),
|
2024-03-25 15:48:51 +11:00
|
|
|
})
|
|
|
|
}
|
2025-02-12 10:22:56 +13:00
|
|
|
prepareTruncatedAst = (
|
2025-02-15 00:57:04 +11:00
|
|
|
sketchNodePaths: PathToNode[],
|
2024-10-30 16:52:17 -04:00
|
|
|
ast?: Node<Program>,
|
2024-02-11 12:59:00 +11:00
|
|
|
draftSegment?: DraftSegment
|
|
|
|
) =>
|
2025-02-12 10:22:56 +13:00
|
|
|
prepareTruncatedAst(
|
2025-02-15 00:57:04 +11:00
|
|
|
sketchNodePaths,
|
2025-04-01 23:54:26 -07:00
|
|
|
ast || this.kclManager.ast,
|
|
|
|
this.kclManager.lastSuccessfulVariables,
|
2024-02-11 12:59:00 +11:00
|
|
|
draftSegment
|
|
|
|
)
|
2025-04-14 23:51:14 +02:00
|
|
|
|
|
|
|
getSnappedDragPoint(
|
|
|
|
pos: Vector2,
|
|
|
|
intersects: Intersection<Object3D<Object3DEventMap>>[],
|
|
|
|
mouseEvent: MouseEvent,
|
|
|
|
// During draft segment mouse move:
|
|
|
|
// - the three.js object currently being dragged: the new draft segment or existing segment (may not be the last in activeSegments)
|
|
|
|
// When placing the draft segment::
|
|
|
|
// - the last segment in activeSegments
|
|
|
|
currentObject?: Object3D | Group
|
|
|
|
) {
|
|
|
|
let snappedPoint: Coords2d = [pos.x, pos.y]
|
|
|
|
|
2025-03-18 11:14:12 +11:00
|
|
|
const intersectsYAxis = intersects.find(
|
|
|
|
(sceneObject) => sceneObject.object.name === Y_AXIS
|
|
|
|
)
|
|
|
|
const intersectsXAxis = intersects.find(
|
|
|
|
(sceneObject) => sceneObject.object.name === X_AXIS
|
|
|
|
)
|
|
|
|
|
2025-04-14 23:51:14 +02:00
|
|
|
// Snap to previous segment's tangent direction when drawing a straight segment
|
|
|
|
let snappedToTangent = false
|
|
|
|
let negativeTangentDirection = false
|
|
|
|
|
|
|
|
const disableTangentSnapping = mouseEvent.ctrlKey || mouseEvent.altKey
|
|
|
|
const forceDirectionSnapping = mouseEvent.shiftKey
|
|
|
|
if (!disableTangentSnapping) {
|
|
|
|
const segments: SafeArray<Group> = Object.values(this.activeSegments) // Using the order in the object feels wrong
|
|
|
|
const currentIndex =
|
|
|
|
currentObject instanceof Group ? segments.indexOf(currentObject) : -1
|
|
|
|
const current = segments[currentIndex]
|
|
|
|
if (
|
|
|
|
current?.userData.type === STRAIGHT_SEGMENT &&
|
|
|
|
// This draft check is not strictly necessary currently, but we only want
|
|
|
|
// to snap when drawing a new segment, this makes that more robust.
|
|
|
|
current?.userData.draft
|
|
|
|
) {
|
|
|
|
const prev = segments[currentIndex - 1]
|
|
|
|
if (prev && ARC_SEGMENT_TYPES.includes(prev.userData.type)) {
|
|
|
|
const snapDirection = findTangentDirection(prev)
|
|
|
|
if (snapDirection) {
|
2025-04-17 14:26:05 +02:00
|
|
|
const SNAP_TOLERANCE_PIXELS = 8 * window.devicePixelRatio
|
|
|
|
const SNAP_MIN_DISTANCE_PIXELS = 10 * window.devicePixelRatio
|
2025-04-14 23:51:14 +02:00
|
|
|
const orthoFactor = orthoScale(this.sceneInfra.camControls.camera)
|
|
|
|
|
|
|
|
// See if snapDirection intersects with any of the axes
|
|
|
|
if (intersectsXAxis || intersectsYAxis) {
|
|
|
|
let intersectionPoint: Coords2d | undefined
|
|
|
|
if (intersectsXAxis && intersectsYAxis) {
|
|
|
|
// Current mouse position intersects with both axes (origin) -> that has precedence over tangent so we snap to the origin.
|
|
|
|
intersectionPoint = [0, 0]
|
|
|
|
} else {
|
|
|
|
// Intersects only one axis
|
|
|
|
const axisLine: [Coords2d, Coords2d] = intersectsXAxis
|
|
|
|
? [
|
|
|
|
[0, 0],
|
|
|
|
[1, 0],
|
|
|
|
]
|
|
|
|
: [
|
|
|
|
[0, 0],
|
|
|
|
[0, 1],
|
|
|
|
]
|
|
|
|
// See if that axis line intersects with the tangent direction
|
|
|
|
// Note: this includes both positive and negative tangent directions as it just checks 2 lines.
|
|
|
|
intersectionPoint = calculateIntersectionOfTwoLines({
|
|
|
|
line1: axisLine,
|
|
|
|
line2Angle: getAngle([0, 0], snapDirection),
|
|
|
|
line2Point: current.userData.from,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
// If yes, see if that intersection point is within tolerance and if yes snap to it.
|
|
|
|
if (
|
|
|
|
intersectionPoint &&
|
|
|
|
getLength(intersectionPoint, snappedPoint) / orthoFactor <
|
|
|
|
SNAP_TOLERANCE_PIXELS
|
|
|
|
) {
|
|
|
|
snappedPoint = intersectionPoint
|
|
|
|
snappedToTangent = true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (!snappedToTangent) {
|
|
|
|
// Otherwise, try to snap to the tangent direction, in both positive and negative directions
|
|
|
|
const { closestPoint, t } = closestPointOnRay(
|
|
|
|
prev.userData.to,
|
|
|
|
snapDirection,
|
|
|
|
snappedPoint,
|
|
|
|
true
|
|
|
|
)
|
|
|
|
if (
|
|
|
|
forceDirectionSnapping ||
|
|
|
|
(this.sceneInfra.screenSpaceDistance(
|
|
|
|
closestPoint,
|
|
|
|
snappedPoint
|
|
|
|
) < SNAP_TOLERANCE_PIXELS &&
|
|
|
|
// We only want to snap to the tangent direction if the mouse has moved enough to avoid quick jumps
|
|
|
|
// at the beginning of the drag
|
|
|
|
this.sceneInfra.screenSpaceDistance(
|
|
|
|
current.userData.from,
|
|
|
|
current.userData.to
|
|
|
|
) > SNAP_MIN_DISTANCE_PIXELS)
|
|
|
|
) {
|
|
|
|
snappedPoint = closestPoint
|
|
|
|
snappedToTangent = true
|
|
|
|
negativeTangentDirection = t < 0
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Snap to the main axes if there was no snapping to tangent direction
|
|
|
|
if (!snappedToTangent) {
|
|
|
|
snappedPoint = [
|
|
|
|
intersectsYAxis ? 0 : snappedPoint[0],
|
|
|
|
intersectsXAxis ? 0 : snappedPoint[1],
|
|
|
|
]
|
|
|
|
}
|
2025-03-18 11:14:12 +11:00
|
|
|
|
|
|
|
return {
|
2025-04-14 23:51:14 +02:00
|
|
|
isSnapped: !!(intersectsYAxis || intersectsXAxis || snappedToTangent),
|
|
|
|
snappedToTangent,
|
|
|
|
negativeTangentDirection,
|
|
|
|
snappedPoint,
|
|
|
|
intersectsXAxis,
|
|
|
|
intersectsYAxis,
|
2025-03-18 11:14:12 +11:00
|
|
|
}
|
|
|
|
}
|
2025-04-14 23:51:14 +02:00
|
|
|
|
2025-03-18 11:14:12 +11:00
|
|
|
positionDraftPoint({
|
|
|
|
origin,
|
|
|
|
yAxis,
|
|
|
|
zAxis,
|
|
|
|
snappedPoint,
|
|
|
|
}: {
|
|
|
|
origin: SketchDetails['origin']
|
|
|
|
yAxis: SketchDetails['yAxis']
|
|
|
|
zAxis: SketchDetails['zAxis']
|
|
|
|
snappedPoint: Vector2
|
|
|
|
}) {
|
|
|
|
const draftPoint = this.getDraftPoint()
|
|
|
|
if (!draftPoint) {
|
|
|
|
this.createDraftPoint({
|
|
|
|
point: snappedPoint,
|
|
|
|
origin,
|
|
|
|
yAxis,
|
|
|
|
zAxis,
|
|
|
|
})
|
|
|
|
} else {
|
|
|
|
// Ignore if there are huge jumps in the mouse position,
|
|
|
|
// that is likely a strange behavior
|
|
|
|
if (
|
|
|
|
draftPoint.position.distanceTo(
|
|
|
|
new Vector3(snappedPoint.x, snappedPoint.y, 0)
|
|
|
|
) > 100
|
|
|
|
) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
draftPoint.position.set(snappedPoint.x, snappedPoint.y, 0)
|
|
|
|
}
|
|
|
|
}
|
2025-04-14 23:51:14 +02:00
|
|
|
|
2025-03-18 11:14:12 +11:00
|
|
|
maybeSnapProfileStartIntersect2d({
|
|
|
|
sketchEntryNodePath,
|
|
|
|
intersects,
|
|
|
|
intersection2d: _intersection2d,
|
|
|
|
}: {
|
|
|
|
sketchEntryNodePath: PathToNode
|
|
|
|
intersects: Intersection<Object3D<Object3DEventMap>>[]
|
|
|
|
intersection2d: Vector2
|
|
|
|
}) {
|
|
|
|
const intersectsProfileStart = intersects
|
|
|
|
.map(({ object }) => getParentGroup(object, [PROFILE_START]))
|
|
|
|
.find(isGroupStartProfileForCurrentProfile(sketchEntryNodePath))
|
|
|
|
const intersection2d = intersectsProfileStart
|
|
|
|
? new Vector2(
|
|
|
|
intersectsProfileStart.position.x,
|
|
|
|
intersectsProfileStart.position.y
|
|
|
|
)
|
|
|
|
: _intersection2d
|
|
|
|
return intersection2d
|
|
|
|
}
|
2025-04-14 23:51:14 +02:00
|
|
|
|
2024-02-11 12:59:00 +11:00
|
|
|
onDragSegment({
|
|
|
|
object,
|
2024-03-04 08:14:37 +11:00
|
|
|
intersection2d: _intersection2d,
|
2025-02-15 00:57:04 +11:00
|
|
|
sketchEntryNodePath,
|
|
|
|
sketchNodePaths,
|
2024-02-11 12:59:00 +11:00
|
|
|
draftInfo,
|
2024-03-04 08:14:37 +11:00
|
|
|
intersects,
|
2025-04-14 23:51:14 +02:00
|
|
|
mouseEvent,
|
2024-02-11 12:59:00 +11:00
|
|
|
}: {
|
2025-03-18 11:14:12 +11:00
|
|
|
object: Object3D<Object3DEventMap>
|
2024-02-11 12:59:00 +11:00
|
|
|
intersection2d: Vector2
|
2025-02-15 00:57:04 +11:00
|
|
|
sketchEntryNodePath: PathToNode
|
|
|
|
sketchNodePaths: PathToNode[]
|
2024-03-04 08:14:37 +11:00
|
|
|
intersects: Intersection<Object3D<Object3DEventMap>>[]
|
2024-02-11 12:59:00 +11:00
|
|
|
draftInfo?: {
|
2024-10-30 16:52:17 -04:00
|
|
|
truncatedAst: Node<Program>
|
2024-02-11 12:59:00 +11:00
|
|
|
variableDeclarationName: string
|
|
|
|
}
|
2025-04-14 23:51:14 +02:00
|
|
|
mouseEvent: MouseEvent
|
2024-02-11 12:59:00 +11:00
|
|
|
}) {
|
2025-03-18 11:14:12 +11:00
|
|
|
const intersection2d = this.maybeSnapProfileStartIntersect2d({
|
|
|
|
sketchEntryNodePath,
|
|
|
|
intersects,
|
|
|
|
intersection2d: _intersection2d,
|
|
|
|
})
|
2024-10-31 07:04:38 -07:00
|
|
|
|
2024-09-23 22:42:51 +10:00
|
|
|
const group = getParentGroup(object, SEGMENT_BODIES_PLUS_PROFILE_START)
|
2025-02-15 00:57:04 +11:00
|
|
|
const subGroup = getParentGroup(object, [
|
|
|
|
ARROWHEAD,
|
|
|
|
CIRCLE_CENTER_HANDLE,
|
|
|
|
CIRCLE_THREE_POINT_HANDLE1,
|
|
|
|
CIRCLE_THREE_POINT_HANDLE2,
|
|
|
|
CIRCLE_THREE_POINT_HANDLE3,
|
2025-03-18 11:14:12 +11:00
|
|
|
THREE_POINT_ARC_HANDLE2,
|
|
|
|
THREE_POINT_ARC_HANDLE3,
|
|
|
|
ARC_ANGLE_END,
|
2025-02-15 00:57:04 +11:00
|
|
|
])
|
2024-02-11 12:59:00 +11:00
|
|
|
if (!group) return
|
2024-07-25 20:11:46 -04:00
|
|
|
const pathToNode: PathToNode = structuredClone(group.userData.pathToNode)
|
|
|
|
const varDecIndex = pathToNode[1][0]
|
|
|
|
if (typeof varDecIndex !== 'number') {
|
|
|
|
console.error(
|
|
|
|
`Expected varDecIndex to be a number, but found: ${typeof varDecIndex} ${varDecIndex}`
|
|
|
|
)
|
|
|
|
return
|
|
|
|
}
|
2024-02-11 12:59:00 +11:00
|
|
|
|
|
|
|
const from: [number, number] = [
|
2025-02-15 00:57:04 +11:00
|
|
|
group.userData?.from?.[0],
|
|
|
|
group.userData?.from?.[1],
|
2024-02-11 12:59:00 +11:00
|
|
|
]
|
2025-04-14 23:51:14 +02:00
|
|
|
const { snappedPoint: dragTo, snappedToTangent } = this.getSnappedDragPoint(
|
2025-03-18 11:14:12 +11:00
|
|
|
intersection2d,
|
2025-04-14 23:51:14 +02:00
|
|
|
intersects,
|
|
|
|
mouseEvent,
|
|
|
|
object
|
|
|
|
)
|
2025-04-01 23:54:26 -07:00
|
|
|
let modifiedAst = draftInfo
|
|
|
|
? draftInfo.truncatedAst
|
|
|
|
: { ...this.kclManager.ast }
|
2024-02-11 12:59:00 +11:00
|
|
|
|
2025-02-15 00:57:04 +11:00
|
|
|
const nodePathWithCorrectedIndexForTruncatedAst =
|
|
|
|
structuredClone(pathToNode)
|
|
|
|
nodePathWithCorrectedIndexForTruncatedAst[1][0] =
|
|
|
|
Number(nodePathWithCorrectedIndexForTruncatedAst[1][0]) -
|
|
|
|
Number(sketchNodePaths[0][1][0])
|
|
|
|
|
KCL: Use keyword arguments for line, lineTo, extrude and close (#5249)
Part of #4600.
PR: https://github.com/KittyCAD/modeling-app/pull/4826
# Changes to KCL stdlib
- `line(point, sketch, tag)` and `lineTo(point, sketch, tag)` are combined into `line(@sketch, end?, endAbsolute?, tag?)`
- `close(sketch, tag?)` is now `close(@sketch, tag?)`
- `extrude(length, sketch)` is now `extrude(@sketch, length)`
Note that if a parameter starts with `@` like `@sketch`, it doesn't have any label when called, so you call it like this:
```
sketch = startSketchAt([0, 0])
line(sketch, end = [3, 3], tag = $hi)
```
Note also that if you're using a `|>` pipeline, you can omit the `@` argument and it will be assumed to be the LHS of the `|>`. So the above could be written as
```
sketch = startSketchAt([0, 0])
|> line(end = [3, 3], tag = $hi)
```
Also changes frontend tests to use KittyCAD/kcl-samples#139 instead of its main
The regex find-and-replace I use for migrating code (note these don't work with multi-line expressions) are:
```
line\(([^=]*), %\)
line(end = $1)
line\((.*), %, (.*)\)
line(end = $1, tag = $2)
lineTo\((.*), %\)
line(endAbsolute = $1)
lineTo\((.*), %, (.*)\)
line(endAbsolute = $1, tag = $2)
extrude\((.*), %\)
extrude(length = $1)
extrude\(([^=]*), ([a-zA-Z0-9]+)\)
extrude($2, length = $1)
close\(%, (.*)\)
close(tag = $1)
```
# Selected notes from commits before I squash them all
* Fix test 'yRelative to horizontal distance'
Fixes:
- Make a lineTo helper
- Fix pathToNode to go through the labeled arg .arg property
* Fix test by changing lookups into transformMap
Parts of the code assumed that `line` is always a relative call. But
actually now it might be absolute, if it's got an `endAbsolute` parameter.
So, change whether to look up `line` or `lineTo` and the relevant absolute
or relative line types based on that parameter.
* Stop asserting on exact source ranges
When I changed line to kwargs, all the source ranges we assert on became
slightly different. I find these assertions to be very very low value.
So I'm removing them.
* Fix more tests: getConstraintType calls weren't checking if the
'line' fn was absolute or relative.
* Fixed another queryAst test
There were 2 problems:
- Test was looking for the old style of `line` call to choose an offset
for pathToNode
- Test assumed that the `tag` param was always the third one, but in
a kwarg call, you have to look it up by label
* Fix test: traverse was not handling CallExpressionKw
* Fix another test, addTagKw
addTag helper was not aware of kw args.
* Convert close from positional to kwargs
If the close() call has 0 args, or a single unlabeled arg, the parser
interprets it as a CallExpression (positional) not a CallExpressionKw.
But then if a codemod wants to add a tag to it, it tries adding a kwarg
called 'tag', which fails because the CallExpression doesn't need
kwargs inserted into it.
The fix is: change the node from CallExpression to CallExpressionKw, and
update getNodeFromPath to take a 'replacement' arg, so we can replace
the old node with the new node in the AST.
* Fix the last test
Test was looking for `lineTo` as a substring of the input KCL program.
But there's no more lineTo function, so I changed it to look for
line() with an endAbsolute arg, which is the new equivalent.
Also changed the getConstraintInfo code to look up the lineTo if using
line with endAbsolute.
* Fix many bad regex find-replaces
I wrote a regex find-and-replace which converted `line` calls from
positional to keyword calls. But it was accidentally applied to more
places than it should be, for example, angledLine, xLine and yLine calls.
Fixes this.
* Fixes test 'Basic sketch › code pane closed at start'
Problem was, the getNodeFromPath call might not actually find a callExpressionKw,
it might find a callExpression. So the `giveSketchFnCallTag` thought
it was modifying a kwargs call, but it was actually modifying a positional
call.
This meant it tried to push a labeled argument in, rather than a normal
arg, and a lot of other problems. Fixed by doing runtime typechecking.
* Fix: Optional args given with wrong type were silently ignored
Optional args don't have to be given. But if the user gives them, they
should be the right type.
Bug: if the KCL interpreter found an optional arg, which was given, but
was the wrong type, it would ignore it and pretend the arg was never
given at all. This was confusing for users.
Fix: Now if you give an optional arg, but it's the wrong type, KCL will
emit a type error just like it would for a mandatory argument.
---------
Signed-off-by: Nick Cameron <nrc@ncameron.org>
Co-authored-by: Nick Cameron <nrc@ncameron.org>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Frank Noirot <frank@kittycad.io>
Co-authored-by: Kevin Nadro <kevin@zoo.dev>
Co-authored-by: Jonathan Tran <jonnytran@gmail.com>
2025-02-04 08:31:43 -06:00
|
|
|
const _node = getNodeFromPath<Node<CallExpression | CallExpressionKw>>(
|
2024-02-11 12:59:00 +11:00
|
|
|
modifiedAst,
|
2025-02-15 00:57:04 +11:00
|
|
|
draftInfo ? nodePathWithCorrectedIndexForTruncatedAst : pathToNode,
|
KCL: Use keyword arguments for line, lineTo, extrude and close (#5249)
Part of #4600.
PR: https://github.com/KittyCAD/modeling-app/pull/4826
# Changes to KCL stdlib
- `line(point, sketch, tag)` and `lineTo(point, sketch, tag)` are combined into `line(@sketch, end?, endAbsolute?, tag?)`
- `close(sketch, tag?)` is now `close(@sketch, tag?)`
- `extrude(length, sketch)` is now `extrude(@sketch, length)`
Note that if a parameter starts with `@` like `@sketch`, it doesn't have any label when called, so you call it like this:
```
sketch = startSketchAt([0, 0])
line(sketch, end = [3, 3], tag = $hi)
```
Note also that if you're using a `|>` pipeline, you can omit the `@` argument and it will be assumed to be the LHS of the `|>`. So the above could be written as
```
sketch = startSketchAt([0, 0])
|> line(end = [3, 3], tag = $hi)
```
Also changes frontend tests to use KittyCAD/kcl-samples#139 instead of its main
The regex find-and-replace I use for migrating code (note these don't work with multi-line expressions) are:
```
line\(([^=]*), %\)
line(end = $1)
line\((.*), %, (.*)\)
line(end = $1, tag = $2)
lineTo\((.*), %\)
line(endAbsolute = $1)
lineTo\((.*), %, (.*)\)
line(endAbsolute = $1, tag = $2)
extrude\((.*), %\)
extrude(length = $1)
extrude\(([^=]*), ([a-zA-Z0-9]+)\)
extrude($2, length = $1)
close\(%, (.*)\)
close(tag = $1)
```
# Selected notes from commits before I squash them all
* Fix test 'yRelative to horizontal distance'
Fixes:
- Make a lineTo helper
- Fix pathToNode to go through the labeled arg .arg property
* Fix test by changing lookups into transformMap
Parts of the code assumed that `line` is always a relative call. But
actually now it might be absolute, if it's got an `endAbsolute` parameter.
So, change whether to look up `line` or `lineTo` and the relevant absolute
or relative line types based on that parameter.
* Stop asserting on exact source ranges
When I changed line to kwargs, all the source ranges we assert on became
slightly different. I find these assertions to be very very low value.
So I'm removing them.
* Fix more tests: getConstraintType calls weren't checking if the
'line' fn was absolute or relative.
* Fixed another queryAst test
There were 2 problems:
- Test was looking for the old style of `line` call to choose an offset
for pathToNode
- Test assumed that the `tag` param was always the third one, but in
a kwarg call, you have to look it up by label
* Fix test: traverse was not handling CallExpressionKw
* Fix another test, addTagKw
addTag helper was not aware of kw args.
* Convert close from positional to kwargs
If the close() call has 0 args, or a single unlabeled arg, the parser
interprets it as a CallExpression (positional) not a CallExpressionKw.
But then if a codemod wants to add a tag to it, it tries adding a kwarg
called 'tag', which fails because the CallExpression doesn't need
kwargs inserted into it.
The fix is: change the node from CallExpression to CallExpressionKw, and
update getNodeFromPath to take a 'replacement' arg, so we can replace
the old node with the new node in the AST.
* Fix the last test
Test was looking for `lineTo` as a substring of the input KCL program.
But there's no more lineTo function, so I changed it to look for
line() with an endAbsolute arg, which is the new equivalent.
Also changed the getConstraintInfo code to look up the lineTo if using
line with endAbsolute.
* Fix many bad regex find-replaces
I wrote a regex find-and-replace which converted `line` calls from
positional to keyword calls. But it was accidentally applied to more
places than it should be, for example, angledLine, xLine and yLine calls.
Fixes this.
* Fixes test 'Basic sketch › code pane closed at start'
Problem was, the getNodeFromPath call might not actually find a callExpressionKw,
it might find a callExpression. So the `giveSketchFnCallTag` thought
it was modifying a kwargs call, but it was actually modifying a positional
call.
This meant it tried to push a labeled argument in, rather than a normal
arg, and a lot of other problems. Fixed by doing runtime typechecking.
* Fix: Optional args given with wrong type were silently ignored
Optional args don't have to be given. But if the user gives them, they
should be the right type.
Bug: if the KCL interpreter found an optional arg, which was given, but
was the wrong type, it would ignore it and pretend the arg was never
given at all. This was confusing for users.
Fix: Now if you give an optional arg, but it's the wrong type, KCL will
emit a type error just like it would for a mandatory argument.
---------
Signed-off-by: Nick Cameron <nrc@ncameron.org>
Co-authored-by: Nick Cameron <nrc@ncameron.org>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Frank Noirot <frank@kittycad.io>
Co-authored-by: Kevin Nadro <kevin@zoo.dev>
Co-authored-by: Jonathan Tran <jonnytran@gmail.com>
2025-02-04 08:31:43 -06:00
|
|
|
['CallExpression', 'CallExpressionKw']
|
2024-06-24 11:45:40 -04:00
|
|
|
)
|
|
|
|
if (trap(_node)) return
|
|
|
|
const node = _node.node
|
|
|
|
|
KCL: Use keyword arguments for line, lineTo, extrude and close (#5249)
Part of #4600.
PR: https://github.com/KittyCAD/modeling-app/pull/4826
# Changes to KCL stdlib
- `line(point, sketch, tag)` and `lineTo(point, sketch, tag)` are combined into `line(@sketch, end?, endAbsolute?, tag?)`
- `close(sketch, tag?)` is now `close(@sketch, tag?)`
- `extrude(length, sketch)` is now `extrude(@sketch, length)`
Note that if a parameter starts with `@` like `@sketch`, it doesn't have any label when called, so you call it like this:
```
sketch = startSketchAt([0, 0])
line(sketch, end = [3, 3], tag = $hi)
```
Note also that if you're using a `|>` pipeline, you can omit the `@` argument and it will be assumed to be the LHS of the `|>`. So the above could be written as
```
sketch = startSketchAt([0, 0])
|> line(end = [3, 3], tag = $hi)
```
Also changes frontend tests to use KittyCAD/kcl-samples#139 instead of its main
The regex find-and-replace I use for migrating code (note these don't work with multi-line expressions) are:
```
line\(([^=]*), %\)
line(end = $1)
line\((.*), %, (.*)\)
line(end = $1, tag = $2)
lineTo\((.*), %\)
line(endAbsolute = $1)
lineTo\((.*), %, (.*)\)
line(endAbsolute = $1, tag = $2)
extrude\((.*), %\)
extrude(length = $1)
extrude\(([^=]*), ([a-zA-Z0-9]+)\)
extrude($2, length = $1)
close\(%, (.*)\)
close(tag = $1)
```
# Selected notes from commits before I squash them all
* Fix test 'yRelative to horizontal distance'
Fixes:
- Make a lineTo helper
- Fix pathToNode to go through the labeled arg .arg property
* Fix test by changing lookups into transformMap
Parts of the code assumed that `line` is always a relative call. But
actually now it might be absolute, if it's got an `endAbsolute` parameter.
So, change whether to look up `line` or `lineTo` and the relevant absolute
or relative line types based on that parameter.
* Stop asserting on exact source ranges
When I changed line to kwargs, all the source ranges we assert on became
slightly different. I find these assertions to be very very low value.
So I'm removing them.
* Fix more tests: getConstraintType calls weren't checking if the
'line' fn was absolute or relative.
* Fixed another queryAst test
There were 2 problems:
- Test was looking for the old style of `line` call to choose an offset
for pathToNode
- Test assumed that the `tag` param was always the third one, but in
a kwarg call, you have to look it up by label
* Fix test: traverse was not handling CallExpressionKw
* Fix another test, addTagKw
addTag helper was not aware of kw args.
* Convert close from positional to kwargs
If the close() call has 0 args, or a single unlabeled arg, the parser
interprets it as a CallExpression (positional) not a CallExpressionKw.
But then if a codemod wants to add a tag to it, it tries adding a kwarg
called 'tag', which fails because the CallExpression doesn't need
kwargs inserted into it.
The fix is: change the node from CallExpression to CallExpressionKw, and
update getNodeFromPath to take a 'replacement' arg, so we can replace
the old node with the new node in the AST.
* Fix the last test
Test was looking for `lineTo` as a substring of the input KCL program.
But there's no more lineTo function, so I changed it to look for
line() with an endAbsolute arg, which is the new equivalent.
Also changed the getConstraintInfo code to look up the lineTo if using
line with endAbsolute.
* Fix many bad regex find-replaces
I wrote a regex find-and-replace which converted `line` calls from
positional to keyword calls. But it was accidentally applied to more
places than it should be, for example, angledLine, xLine and yLine calls.
Fixes this.
* Fixes test 'Basic sketch › code pane closed at start'
Problem was, the getNodeFromPath call might not actually find a callExpressionKw,
it might find a callExpression. So the `giveSketchFnCallTag` thought
it was modifying a kwargs call, but it was actually modifying a positional
call.
This meant it tried to push a labeled argument in, rather than a normal
arg, and a lot of other problems. Fixed by doing runtime typechecking.
* Fix: Optional args given with wrong type were silently ignored
Optional args don't have to be given. But if the user gives them, they
should be the right type.
Bug: if the KCL interpreter found an optional arg, which was given, but
was the wrong type, it would ignore it and pretend the arg was never
given at all. This was confusing for users.
Fix: Now if you give an optional arg, but it's the wrong type, KCL will
emit a type error just like it would for a mandatory argument.
---------
Signed-off-by: Nick Cameron <nrc@ncameron.org>
Co-authored-by: Nick Cameron <nrc@ncameron.org>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Frank Noirot <frank@kittycad.io>
Co-authored-by: Kevin Nadro <kevin@zoo.dev>
Co-authored-by: Jonathan Tran <jonnytran@gmail.com>
2025-02-04 08:31:43 -06:00
|
|
|
if (node.type !== 'CallExpression' && node.type !== 'CallExpressionKw')
|
|
|
|
return
|
2024-02-11 12:59:00 +11:00
|
|
|
|
2024-06-24 11:45:40 -04:00
|
|
|
let modded:
|
|
|
|
| {
|
2024-10-30 16:52:17 -04:00
|
|
|
modifiedAst: Node<Program>
|
2024-06-24 11:45:40 -04:00
|
|
|
pathToNode: PathToNode
|
|
|
|
}
|
|
|
|
| Error
|
2024-09-23 22:42:51 +10:00
|
|
|
|
|
|
|
const getChangeSketchInput = (): SegmentInputs => {
|
|
|
|
if (
|
|
|
|
group.name === CIRCLE_SEGMENT &&
|
|
|
|
// !subGroup treats grabbing the outer circumference of the circle
|
|
|
|
// as a drag of the center handle
|
|
|
|
(!subGroup || subGroup?.name === ARROWHEAD)
|
|
|
|
)
|
|
|
|
return {
|
|
|
|
type: 'arc-segment',
|
|
|
|
from,
|
2025-03-18 11:14:12 +11:00
|
|
|
to: from, // Same as from for a full circle
|
2024-09-23 22:42:51 +10:00
|
|
|
center: group.userData.center,
|
|
|
|
// distance between the center and the drag point
|
|
|
|
radius: Math.sqrt(
|
|
|
|
(group.userData.center[0] - dragTo[0]) ** 2 +
|
|
|
|
(group.userData.center[1] - dragTo[1]) ** 2
|
|
|
|
),
|
2025-03-18 11:14:12 +11:00
|
|
|
ccw: true,
|
2024-09-23 22:42:51 +10:00
|
|
|
}
|
|
|
|
if (
|
|
|
|
group.name === CIRCLE_SEGMENT &&
|
|
|
|
subGroup?.name === CIRCLE_CENTER_HANDLE
|
|
|
|
)
|
|
|
|
return {
|
|
|
|
type: 'arc-segment',
|
|
|
|
from,
|
2025-03-18 11:14:12 +11:00
|
|
|
to: from, // Same as from for a full circle
|
2024-09-23 22:42:51 +10:00
|
|
|
center: dragTo,
|
|
|
|
radius: group.userData.radius,
|
2025-03-18 11:14:12 +11:00
|
|
|
ccw: true,
|
2024-09-23 22:42:51 +10:00
|
|
|
}
|
2025-03-18 11:14:12 +11:00
|
|
|
|
|
|
|
// Handle ARC_SEGMENT with center handle
|
|
|
|
if (
|
|
|
|
group.name === ARC_SEGMENT &&
|
|
|
|
subGroup?.name === CIRCLE_CENTER_HANDLE
|
|
|
|
) {
|
|
|
|
// the user is dragging the circle's center, but the values they updating the arc's radius and start angle
|
|
|
|
// we need to calculate what the radius should be and a new to point that respects the endAngle
|
|
|
|
const newCenter = dragTo
|
|
|
|
const radius = Math.sqrt(
|
|
|
|
(newCenter[0] - group.userData.from[0]) ** 2 +
|
|
|
|
(newCenter[1] - group.userData.from[1]) ** 2
|
|
|
|
)
|
|
|
|
const endAngle = Math.atan2(
|
|
|
|
group.userData.to[1] - group.userData.center[1],
|
|
|
|
group.userData.to[0] - group.userData.center[0]
|
|
|
|
)
|
|
|
|
const newTo: [number, number] = [
|
|
|
|
newCenter[0] + radius * Math.cos(endAngle),
|
|
|
|
newCenter[1] + radius * Math.sin(endAngle),
|
|
|
|
]
|
|
|
|
return {
|
|
|
|
type: 'arc-segment',
|
|
|
|
from: group.userData.from,
|
|
|
|
to: newTo,
|
|
|
|
center: newCenter,
|
|
|
|
radius,
|
|
|
|
ccw: group.userData.ccw,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// Handle ARC_SEGMENT with end angle handle
|
|
|
|
if (group.name === ARC_SEGMENT && subGroup?.name === ARC_ANGLE_END) {
|
|
|
|
// Calculate the angle from center to drag point
|
|
|
|
const center = group.userData.center
|
|
|
|
const endAngle = Math.atan2(
|
|
|
|
dragTo[1] - center[1],
|
|
|
|
dragTo[0] - center[0]
|
|
|
|
)
|
|
|
|
|
|
|
|
// Calculate the point on the arc at the given angle and radius
|
|
|
|
const radius = group.userData.radius
|
|
|
|
const toPoint: [number, number] = [
|
|
|
|
center[0] + radius * Math.cos(endAngle),
|
|
|
|
center[1] + radius * Math.sin(endAngle),
|
|
|
|
]
|
|
|
|
|
|
|
|
return {
|
|
|
|
type: 'arc-segment',
|
|
|
|
from: group.userData.from,
|
|
|
|
to: toPoint,
|
|
|
|
center: center,
|
|
|
|
radius: radius,
|
|
|
|
ccw: group.userData.ccw,
|
|
|
|
}
|
|
|
|
}
|
2025-02-15 00:57:04 +11:00
|
|
|
if (
|
|
|
|
subGroup?.name &&
|
|
|
|
[
|
|
|
|
CIRCLE_THREE_POINT_HANDLE1,
|
|
|
|
CIRCLE_THREE_POINT_HANDLE2,
|
|
|
|
CIRCLE_THREE_POINT_HANDLE3,
|
|
|
|
].includes(subGroup?.name)
|
|
|
|
) {
|
|
|
|
const input: SegmentInputs = {
|
|
|
|
type: 'circle-three-point-segment',
|
|
|
|
p1: group.userData.p1,
|
|
|
|
p2: group.userData.p2,
|
|
|
|
p3: group.userData.p3,
|
|
|
|
}
|
|
|
|
if (subGroup?.name === CIRCLE_THREE_POINT_HANDLE1) {
|
|
|
|
input.p1 = dragTo
|
|
|
|
} else if (subGroup?.name === CIRCLE_THREE_POINT_HANDLE2) {
|
|
|
|
input.p2 = dragTo
|
|
|
|
} else if (subGroup?.name === CIRCLE_THREE_POINT_HANDLE3) {
|
|
|
|
input.p3 = dragTo
|
|
|
|
}
|
|
|
|
return input
|
|
|
|
}
|
2025-03-18 11:14:12 +11:00
|
|
|
if (
|
|
|
|
subGroup?.name &&
|
|
|
|
[THREE_POINT_ARC_HANDLE2, THREE_POINT_ARC_HANDLE3].includes(
|
|
|
|
subGroup?.name
|
|
|
|
)
|
|
|
|
) {
|
|
|
|
const input: SegmentInputs = {
|
|
|
|
type: 'circle-three-point-segment',
|
|
|
|
p1: group.userData.p1,
|
|
|
|
p2: group.userData.p2,
|
|
|
|
p3: group.userData.p3,
|
|
|
|
}
|
|
|
|
if (subGroup?.name === THREE_POINT_ARC_HANDLE2) {
|
|
|
|
input.p2 = dragTo
|
|
|
|
} else if (subGroup?.name === THREE_POINT_ARC_HANDLE3) {
|
|
|
|
input.p3 = dragTo
|
|
|
|
}
|
|
|
|
return input
|
|
|
|
}
|
2024-09-23 22:42:51 +10:00
|
|
|
|
|
|
|
// straight segment is the default
|
|
|
|
return {
|
|
|
|
type: 'straight-segment',
|
|
|
|
from,
|
|
|
|
to: dragTo,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-03-02 08:48:30 +11:00
|
|
|
if (group.name === PROFILE_START) {
|
|
|
|
modded = updateStartProfileAtArgs({
|
|
|
|
node: modifiedAst,
|
|
|
|
pathToNode,
|
2024-09-13 21:14:14 +10:00
|
|
|
input: {
|
|
|
|
type: 'straight-segment',
|
|
|
|
to: dragTo,
|
|
|
|
from,
|
|
|
|
},
|
2025-04-01 23:54:26 -07:00
|
|
|
variables: this.kclManager.variables,
|
2024-03-02 08:48:30 +11:00
|
|
|
})
|
|
|
|
} else {
|
|
|
|
modded = changeSketchArguments(
|
|
|
|
modifiedAst,
|
2025-04-01 23:54:26 -07:00
|
|
|
this.kclManager.variables,
|
2024-09-13 21:14:14 +10:00
|
|
|
{
|
2024-09-23 22:42:51 +10:00
|
|
|
type: 'sourceRange',
|
2025-01-17 14:34:36 -05:00
|
|
|
sourceRange: topLevelRange(node.start, node.end),
|
2024-09-23 22:42:51 +10:00
|
|
|
},
|
|
|
|
getChangeSketchInput()
|
2024-03-02 08:48:30 +11:00
|
|
|
)
|
|
|
|
}
|
2024-06-24 11:45:40 -04:00
|
|
|
if (trap(modded)) return
|
2024-03-02 08:48:30 +11:00
|
|
|
|
2024-02-11 12:59:00 +11:00
|
|
|
modifiedAst = modded.modifiedAst
|
2024-06-24 11:45:40 -04:00
|
|
|
const info = draftInfo
|
|
|
|
? draftInfo
|
2025-02-15 00:57:04 +11:00
|
|
|
: this.prepareTruncatedAst(sketchNodePaths || [], modifiedAst)
|
2024-06-24 11:45:40 -04:00
|
|
|
if (trap(info, { suppress: true })) return
|
2025-02-15 00:57:04 +11:00
|
|
|
const { truncatedAst } = info
|
2024-02-11 12:59:00 +11:00
|
|
|
;(async () => {
|
|
|
|
const code = recast(modifiedAst)
|
2024-06-24 11:45:40 -04:00
|
|
|
if (trap(code)) return
|
2024-02-11 12:59:00 +11:00
|
|
|
if (!draftInfo)
|
2024-04-17 20:18:07 -07:00
|
|
|
// don't want to mod the user's code yet as they have't committed to the change yet
|
2024-02-11 12:59:00 +11:00
|
|
|
// plus this would be the truncated ast being recast, it would be wrong
|
2025-04-01 23:54:26 -07:00
|
|
|
this.codeManager.updateCodeEditor(code)
|
2025-03-17 18:26:11 -07:00
|
|
|
const { execState } = await executeAstMock({
|
2024-02-11 12:59:00 +11:00
|
|
|
ast: truncatedAst,
|
2025-04-01 23:54:26 -07:00
|
|
|
rustContext: this.rustContext,
|
2024-02-11 12:59:00 +11:00
|
|
|
})
|
2025-02-12 10:22:56 +13:00
|
|
|
const variables = execState.variables
|
2025-02-15 00:57:04 +11:00
|
|
|
const sketchesInfo = getSketchesInfo({
|
|
|
|
sketchNodePaths,
|
|
|
|
ast: truncatedAst,
|
|
|
|
variables,
|
2025-04-01 23:54:26 -07:00
|
|
|
kclManager: this.kclManager,
|
2025-02-15 00:57:04 +11:00
|
|
|
})
|
|
|
|
const callBacks: (() => SegmentOverlayPayload | null)[] = []
|
|
|
|
for (const sketchInfo of sketchesInfo) {
|
|
|
|
const { sketch, pathToNode: _pathToNode } = sketchInfo
|
|
|
|
const varDecIndex = Number(_pathToNode[1][0])
|
2024-07-08 17:57:37 -04:00
|
|
|
|
2025-02-15 00:57:04 +11:00
|
|
|
if (!sketch) return
|
2024-05-24 20:54:42 +10:00
|
|
|
|
2025-02-15 00:57:04 +11:00
|
|
|
const sgPaths = sketch.paths
|
2025-04-01 23:54:26 -07:00
|
|
|
const orthoFactor = orthoScale(this.sceneInfra.camControls.camera)
|
2024-12-16 10:34:11 -05:00
|
|
|
|
2024-04-19 11:56:21 -04:00
|
|
|
this.updateSegment(
|
2025-02-15 00:57:04 +11:00
|
|
|
sketch.start,
|
|
|
|
0,
|
2024-04-19 11:56:21 -04:00
|
|
|
varDecIndex,
|
2024-02-11 12:59:00 +11:00
|
|
|
modifiedAst,
|
2024-04-19 11:56:21 -04:00
|
|
|
orthoFactor,
|
2025-04-14 23:51:14 +02:00
|
|
|
sketch,
|
|
|
|
snappedToTangent
|
2024-02-11 12:59:00 +11:00
|
|
|
)
|
2025-02-15 00:57:04 +11:00
|
|
|
|
|
|
|
callBacks.push(
|
|
|
|
...sgPaths.map((group, index) =>
|
|
|
|
this.updateSegment(
|
|
|
|
group,
|
|
|
|
index,
|
|
|
|
varDecIndex,
|
|
|
|
modifiedAst,
|
|
|
|
orthoFactor,
|
2025-04-14 23:51:14 +02:00
|
|
|
sketch,
|
|
|
|
snappedToTangent
|
2025-02-15 00:57:04 +11:00
|
|
|
)
|
|
|
|
)
|
|
|
|
)
|
|
|
|
}
|
2025-04-01 23:54:26 -07:00
|
|
|
this.sceneInfra.overlayCallbacks(callBacks)
|
2024-09-09 18:17:45 -04:00
|
|
|
})().catch(reportRejection)
|
2024-02-11 12:59:00 +11:00
|
|
|
}
|
|
|
|
|
2024-04-19 11:56:21 -04:00
|
|
|
/**
|
|
|
|
* Update the THREEjs sketch entities with new segment data
|
|
|
|
* mapping them back to the AST
|
|
|
|
* @param segment
|
|
|
|
* @param index
|
|
|
|
* @param varDecIndex
|
|
|
|
* @param modifiedAst
|
|
|
|
* @param orthoFactor
|
2024-09-27 15:44:44 -07:00
|
|
|
* @param sketch
|
2025-04-14 23:51:14 +02:00
|
|
|
* @param snappedToTangent if currently drawn draft segment is snapping to previous arc tangent
|
2024-04-19 11:56:21 -04:00
|
|
|
*/
|
|
|
|
updateSegment = (
|
2024-09-27 15:44:44 -07:00
|
|
|
segment: Path | Sketch['start'],
|
2024-04-19 11:56:21 -04:00
|
|
|
index: number,
|
|
|
|
varDecIndex: number,
|
|
|
|
modifiedAst: Program,
|
|
|
|
orthoFactor: number,
|
2025-04-14 23:51:14 +02:00
|
|
|
sketch: Sketch,
|
|
|
|
snappedToTangent: boolean = false
|
2024-05-24 20:54:42 +10:00
|
|
|
): (() => SegmentOverlayPayload | null) => {
|
2024-04-19 11:56:21 -04:00
|
|
|
const segPathToNode = getNodePathFromSourceRange(
|
|
|
|
modifiedAst,
|
2024-12-06 13:57:31 +13:00
|
|
|
sourceRangeFromRust(segment.__geoMeta.sourceRange)
|
2024-04-19 11:56:21 -04:00
|
|
|
)
|
2024-10-23 12:42:54 -05:00
|
|
|
const sgPaths = sketch.paths
|
2024-04-19 11:56:21 -04:00
|
|
|
const originalPathToNodeStr = JSON.stringify(segPathToNode)
|
|
|
|
segPathToNode[1][0] = varDecIndex
|
|
|
|
const pathToNodeStr = JSON.stringify(segPathToNode)
|
2024-09-27 15:44:44 -07:00
|
|
|
// more hacks to hopefully be solved by proper pathToNode info in memory/sketch segments
|
2024-04-19 11:56:21 -04:00
|
|
|
const group =
|
|
|
|
this.activeSegments[pathToNodeStr] ||
|
|
|
|
this.activeSegments[originalPathToNodeStr]
|
|
|
|
const type = group?.userData?.type
|
|
|
|
const factor =
|
2025-04-01 23:54:26 -07:00
|
|
|
(this.sceneInfra.camControls.camera instanceof OrthographicCamera
|
2024-04-19 11:56:21 -04:00
|
|
|
? orthoFactor
|
2025-04-01 23:54:26 -07:00
|
|
|
: perspScale(this.sceneInfra.camControls.camera, group)) /
|
|
|
|
this.sceneInfra._baseUnitMultiplier
|
2024-09-23 22:42:51 +10:00
|
|
|
let input: SegmentInputs = {
|
2024-09-13 21:14:14 +10:00
|
|
|
type: 'straight-segment',
|
|
|
|
from: segment.from,
|
|
|
|
to: segment.to,
|
2025-04-14 23:51:14 +02:00
|
|
|
snap: snappedToTangent,
|
2024-09-23 22:42:51 +10:00
|
|
|
}
|
2024-09-13 21:14:14 +10:00
|
|
|
let update: SegmentUtils['update'] | null = null
|
2024-04-19 11:56:21 -04:00
|
|
|
if (type === TANGENTIAL_ARC_TO_SEGMENT) {
|
2025-04-11 14:17:20 -04:00
|
|
|
update = segmentUtils.tangentialArc.update
|
2024-04-19 11:56:21 -04:00
|
|
|
} else if (type === STRAIGHT_SEGMENT) {
|
2024-09-13 21:14:14 +10:00
|
|
|
update = segmentUtils.straight.update
|
2024-09-23 22:42:51 +10:00
|
|
|
} else if (
|
|
|
|
type === CIRCLE_SEGMENT &&
|
|
|
|
'type' in segment &&
|
|
|
|
segment.type === 'Circle'
|
|
|
|
) {
|
|
|
|
update = segmentUtils.circle.update
|
|
|
|
input = {
|
|
|
|
type: 'arc-segment',
|
|
|
|
from: segment.from,
|
2025-03-18 11:14:12 +11:00
|
|
|
to: segment.from, // Use from as to for full circles
|
2024-09-23 22:42:51 +10:00
|
|
|
center: segment.center,
|
|
|
|
radius: segment.radius,
|
2025-03-18 11:14:12 +11:00
|
|
|
ccw: true,
|
2024-09-23 22:42:51 +10:00
|
|
|
}
|
2025-02-15 00:57:04 +11:00
|
|
|
} else if (
|
|
|
|
type === CIRCLE_THREE_POINT_SEGMENT &&
|
|
|
|
'type' in segment &&
|
|
|
|
segment.type === 'CircleThreePoint'
|
|
|
|
) {
|
|
|
|
update = segmentUtils.circleThreePoint.update
|
|
|
|
input = {
|
|
|
|
type: 'circle-three-point-segment',
|
|
|
|
p1: segment.p1,
|
|
|
|
p2: segment.p2,
|
|
|
|
p3: segment.p3,
|
|
|
|
}
|
2025-03-18 11:14:12 +11:00
|
|
|
} else if (
|
|
|
|
type === ARC_SEGMENT &&
|
|
|
|
'type' in segment &&
|
|
|
|
segment.type === 'Arc'
|
|
|
|
) {
|
|
|
|
update = segmentUtils.arc.update
|
|
|
|
input = {
|
|
|
|
type: 'arc-segment',
|
|
|
|
from: segment.from,
|
|
|
|
to: segment.to,
|
|
|
|
center: segment.center,
|
|
|
|
radius: segment.radius,
|
|
|
|
ccw: segment.ccw,
|
|
|
|
}
|
|
|
|
} else if (
|
|
|
|
type === THREE_POINT_ARC_SEGMENT &&
|
|
|
|
'type' in segment &&
|
|
|
|
segment.type === 'ArcThreePoint'
|
|
|
|
) {
|
|
|
|
update = segmentUtils.threePointArc.update
|
|
|
|
input = {
|
|
|
|
type: 'circle-three-point-segment',
|
|
|
|
p1: segment.p1,
|
|
|
|
p2: segment.p2,
|
|
|
|
p3: segment.p3,
|
|
|
|
}
|
2024-09-13 21:14:14 +10:00
|
|
|
}
|
|
|
|
const callBack =
|
|
|
|
update &&
|
|
|
|
!err(update) &&
|
|
|
|
update({
|
|
|
|
input,
|
2024-04-19 11:56:21 -04:00
|
|
|
group,
|
|
|
|
scale: factor,
|
2024-09-13 21:14:14 +10:00
|
|
|
prevSegment: sgPaths[index - 1],
|
2025-04-01 23:54:26 -07:00
|
|
|
sceneInfra: this.sceneInfra,
|
2024-04-19 11:56:21 -04:00
|
|
|
})
|
2024-09-13 21:14:14 +10:00
|
|
|
if (callBack && !err(callBack)) return callBack
|
|
|
|
|
|
|
|
if (type === PROFILE_START) {
|
2024-04-19 11:56:21 -04:00
|
|
|
group.position.set(segment.from[0], segment.from[1], 0)
|
|
|
|
group.scale.set(factor, factor, factor)
|
|
|
|
}
|
2024-05-24 20:54:42 +10:00
|
|
|
return () => null
|
2024-04-19 11:56:21 -04:00
|
|
|
}
|
|
|
|
|
2024-09-10 13:30:39 -04:00
|
|
|
/**
|
|
|
|
* Update the base color of each of the THREEjs meshes
|
|
|
|
* that represent each of the sketch segments, to get the
|
|
|
|
* latest value from `sceneInfra._theme`
|
|
|
|
*/
|
|
|
|
updateSegmentBaseColor(newColor: Themes.Light | Themes.Dark) {
|
|
|
|
const newColorThreeJs = getThemeColorForThreeJs(newColor)
|
|
|
|
Object.values(this.activeSegments).forEach((group) => {
|
|
|
|
group.userData.baseColor = newColorThreeJs
|
|
|
|
group.traverse((child) => {
|
|
|
|
if (
|
|
|
|
child instanceof Mesh &&
|
|
|
|
child.material instanceof MeshBasicMaterial
|
|
|
|
) {
|
|
|
|
child.material.color.set(newColorThreeJs)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
})
|
|
|
|
}
|
2025-04-14 23:51:14 +02:00
|
|
|
|
2024-02-11 12:59:00 +11:00
|
|
|
removeSketchGrid() {
|
2025-04-01 23:54:26 -07:00
|
|
|
if (this.axisGroup) this.sceneInfra.scene.remove(this.axisGroup)
|
2024-02-11 12:59:00 +11:00
|
|
|
}
|
2025-04-14 23:51:14 +02:00
|
|
|
|
2025-02-15 00:57:04 +11:00
|
|
|
tearDownSketch({ removeAxis = true }: { removeAxis?: boolean }) {
|
2024-11-22 11:05:04 -05:00
|
|
|
// Remove all draft groups
|
|
|
|
this.draftPointGroups.forEach((draftPointGroup) => {
|
2025-04-01 23:54:26 -07:00
|
|
|
this.sceneInfra.scene.remove(draftPointGroup)
|
2024-11-22 11:05:04 -05:00
|
|
|
})
|
2025-02-15 00:57:04 +11:00
|
|
|
|
|
|
|
// Remove all sketch tools
|
|
|
|
|
2025-04-01 23:54:26 -07:00
|
|
|
if (this.axisGroup && removeAxis)
|
|
|
|
this.sceneInfra.scene.remove(this.axisGroup)
|
|
|
|
const sketchSegments = this.sceneInfra.scene.children.find(
|
2024-02-11 12:59:00 +11:00
|
|
|
({ userData }) => userData?.type === SKETCH_GROUP_SEGMENTS
|
|
|
|
)
|
|
|
|
if (sketchSegments) {
|
2024-07-08 16:41:00 -04:00
|
|
|
// We have to manually remove the CSS2DObjects
|
|
|
|
// as they don't get removed when the group is removed
|
|
|
|
sketchSegments.traverse((object) => {
|
|
|
|
if (object instanceof CSS2DObject) {
|
|
|
|
object.element.remove()
|
|
|
|
object.remove()
|
|
|
|
}
|
|
|
|
})
|
2025-04-01 23:54:26 -07:00
|
|
|
this.sceneInfra.scene.remove(sketchSegments)
|
2024-02-11 12:59:00 +11:00
|
|
|
}
|
2025-04-01 23:54:26 -07:00
|
|
|
this.sceneInfra.camControls.enableRotate = true
|
2024-02-11 12:59:00 +11:00
|
|
|
this.activeSegments = {}
|
|
|
|
}
|
2025-04-14 23:51:14 +02:00
|
|
|
|
2024-04-04 11:07:51 +11:00
|
|
|
mouseEnterLeaveCallbacks() {
|
|
|
|
return {
|
|
|
|
onMouseEnter: ({ selected, dragSelected }: OnMouseEnterLeaveArgs) => {
|
|
|
|
if ([X_AXIS, Y_AXIS].includes(selected?.userData?.type)) {
|
|
|
|
const obj = selected as Mesh
|
|
|
|
const mat = obj.material as MeshBasicMaterial
|
|
|
|
mat.color.set(obj.userData.baseColor)
|
|
|
|
mat.color.offsetHSL(0, 0, 0.5)
|
|
|
|
}
|
2024-09-23 22:42:51 +10:00
|
|
|
const parent = getParentGroup(
|
|
|
|
selected,
|
|
|
|
SEGMENT_BODIES_PLUS_PROFILE_START
|
|
|
|
)
|
2024-04-04 11:07:51 +11:00
|
|
|
if (parent?.userData?.pathToNode) {
|
2025-04-01 23:54:26 -07:00
|
|
|
const pResult = parse(recast(this.kclManager.ast))
|
2024-12-06 13:57:31 +13:00
|
|
|
if (trap(pResult) || !resultIsOk(pResult))
|
|
|
|
return Promise.reject(pResult)
|
|
|
|
const updatedAst = pResult.program
|
KCL: Use keyword arguments for line, lineTo, extrude and close (#5249)
Part of #4600.
PR: https://github.com/KittyCAD/modeling-app/pull/4826
# Changes to KCL stdlib
- `line(point, sketch, tag)` and `lineTo(point, sketch, tag)` are combined into `line(@sketch, end?, endAbsolute?, tag?)`
- `close(sketch, tag?)` is now `close(@sketch, tag?)`
- `extrude(length, sketch)` is now `extrude(@sketch, length)`
Note that if a parameter starts with `@` like `@sketch`, it doesn't have any label when called, so you call it like this:
```
sketch = startSketchAt([0, 0])
line(sketch, end = [3, 3], tag = $hi)
```
Note also that if you're using a `|>` pipeline, you can omit the `@` argument and it will be assumed to be the LHS of the `|>`. So the above could be written as
```
sketch = startSketchAt([0, 0])
|> line(end = [3, 3], tag = $hi)
```
Also changes frontend tests to use KittyCAD/kcl-samples#139 instead of its main
The regex find-and-replace I use for migrating code (note these don't work with multi-line expressions) are:
```
line\(([^=]*), %\)
line(end = $1)
line\((.*), %, (.*)\)
line(end = $1, tag = $2)
lineTo\((.*), %\)
line(endAbsolute = $1)
lineTo\((.*), %, (.*)\)
line(endAbsolute = $1, tag = $2)
extrude\((.*), %\)
extrude(length = $1)
extrude\(([^=]*), ([a-zA-Z0-9]+)\)
extrude($2, length = $1)
close\(%, (.*)\)
close(tag = $1)
```
# Selected notes from commits before I squash them all
* Fix test 'yRelative to horizontal distance'
Fixes:
- Make a lineTo helper
- Fix pathToNode to go through the labeled arg .arg property
* Fix test by changing lookups into transformMap
Parts of the code assumed that `line` is always a relative call. But
actually now it might be absolute, if it's got an `endAbsolute` parameter.
So, change whether to look up `line` or `lineTo` and the relevant absolute
or relative line types based on that parameter.
* Stop asserting on exact source ranges
When I changed line to kwargs, all the source ranges we assert on became
slightly different. I find these assertions to be very very low value.
So I'm removing them.
* Fix more tests: getConstraintType calls weren't checking if the
'line' fn was absolute or relative.
* Fixed another queryAst test
There were 2 problems:
- Test was looking for the old style of `line` call to choose an offset
for pathToNode
- Test assumed that the `tag` param was always the third one, but in
a kwarg call, you have to look it up by label
* Fix test: traverse was not handling CallExpressionKw
* Fix another test, addTagKw
addTag helper was not aware of kw args.
* Convert close from positional to kwargs
If the close() call has 0 args, or a single unlabeled arg, the parser
interprets it as a CallExpression (positional) not a CallExpressionKw.
But then if a codemod wants to add a tag to it, it tries adding a kwarg
called 'tag', which fails because the CallExpression doesn't need
kwargs inserted into it.
The fix is: change the node from CallExpression to CallExpressionKw, and
update getNodeFromPath to take a 'replacement' arg, so we can replace
the old node with the new node in the AST.
* Fix the last test
Test was looking for `lineTo` as a substring of the input KCL program.
But there's no more lineTo function, so I changed it to look for
line() with an endAbsolute arg, which is the new equivalent.
Also changed the getConstraintInfo code to look up the lineTo if using
line with endAbsolute.
* Fix many bad regex find-replaces
I wrote a regex find-and-replace which converted `line` calls from
positional to keyword calls. But it was accidentally applied to more
places than it should be, for example, angledLine, xLine and yLine calls.
Fixes this.
* Fixes test 'Basic sketch › code pane closed at start'
Problem was, the getNodeFromPath call might not actually find a callExpressionKw,
it might find a callExpression. So the `giveSketchFnCallTag` thought
it was modifying a kwargs call, but it was actually modifying a positional
call.
This meant it tried to push a labeled argument in, rather than a normal
arg, and a lot of other problems. Fixed by doing runtime typechecking.
* Fix: Optional args given with wrong type were silently ignored
Optional args don't have to be given. But if the user gives them, they
should be the right type.
Bug: if the KCL interpreter found an optional arg, which was given, but
was the wrong type, it would ignore it and pretend the arg was never
given at all. This was confusing for users.
Fix: Now if you give an optional arg, but it's the wrong type, KCL will
emit a type error just like it would for a mandatory argument.
---------
Signed-off-by: Nick Cameron <nrc@ncameron.org>
Co-authored-by: Nick Cameron <nrc@ncameron.org>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Frank Noirot <frank@kittycad.io>
Co-authored-by: Kevin Nadro <kevin@zoo.dev>
Co-authored-by: Jonathan Tran <jonnytran@gmail.com>
2025-02-04 08:31:43 -06:00
|
|
|
const _node = getNodeFromPath<
|
|
|
|
Node<CallExpression | CallExpressionKw>
|
|
|
|
>(updatedAst, parent.userData.pathToNode, [
|
|
|
|
'CallExpressionKw',
|
|
|
|
'CallExpression',
|
|
|
|
])
|
2024-06-24 11:45:40 -04:00
|
|
|
if (trap(_node, { suppress: true })) return
|
|
|
|
const node = _node.node
|
2025-04-01 23:54:26 -07:00
|
|
|
this.editorManager.setHighlightRange([
|
|
|
|
topLevelRange(node.start, node.end),
|
|
|
|
])
|
2024-04-04 11:07:51 +11:00
|
|
|
const yellow = 0xffff00
|
|
|
|
colorSegment(selected, yellow)
|
|
|
|
const extraSegmentGroup = parent.getObjectByName(EXTRA_SEGMENT_HANDLE)
|
|
|
|
if (extraSegmentGroup) {
|
|
|
|
extraSegmentGroup.traverse((child) => {
|
|
|
|
if (child instanceof Points || child instanceof Mesh) {
|
|
|
|
child.material.opacity = dragSelected ? 0 : 1
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
2025-04-01 23:54:26 -07:00
|
|
|
const orthoFactor = orthoScale(this.sceneInfra.camControls.camera)
|
2024-04-04 11:07:51 +11:00
|
|
|
|
2024-09-23 22:42:51 +10:00
|
|
|
let input: SegmentInputs = {
|
2024-09-13 21:14:14 +10:00
|
|
|
type: 'straight-segment',
|
|
|
|
from: parent.userData.from,
|
|
|
|
to: parent.userData.to,
|
2024-09-23 22:42:51 +10:00
|
|
|
}
|
2024-04-04 11:07:51 +11:00
|
|
|
const factor =
|
2025-04-01 23:54:26 -07:00
|
|
|
(this.sceneInfra.camControls.camera instanceof OrthographicCamera
|
2024-04-04 11:07:51 +11:00
|
|
|
? orthoFactor
|
2025-04-01 23:54:26 -07:00
|
|
|
: perspScale(this.sceneInfra.camControls.camera, parent)) /
|
|
|
|
this.sceneInfra._baseUnitMultiplier
|
2024-09-13 21:14:14 +10:00
|
|
|
let update: SegmentUtils['update'] | null = null
|
2024-04-04 11:07:51 +11:00
|
|
|
if (parent.name === STRAIGHT_SEGMENT) {
|
2024-09-13 21:14:14 +10:00
|
|
|
update = segmentUtils.straight.update
|
2024-04-04 11:07:51 +11:00
|
|
|
} else if (parent.name === TANGENTIAL_ARC_TO_SEGMENT) {
|
2025-04-11 14:17:20 -04:00
|
|
|
update = segmentUtils.tangentialArc.update
|
2024-09-23 22:42:51 +10:00
|
|
|
input = {
|
|
|
|
type: 'arc-segment',
|
|
|
|
from: parent.userData.from,
|
2025-03-18 11:14:12 +11:00
|
|
|
to: parent.userData.to,
|
|
|
|
radius: parent.userData.radius,
|
|
|
|
center: parent.userData.center,
|
|
|
|
ccw:
|
|
|
|
parent.userData.ccw !== undefined ? parent.userData.ccw : true,
|
|
|
|
}
|
|
|
|
} else if (parent.name === CIRCLE_SEGMENT) {
|
|
|
|
update = segmentUtils.circle.update
|
|
|
|
input = {
|
|
|
|
type: 'arc-segment',
|
|
|
|
from: parent.userData.from,
|
|
|
|
to: parent.userData.to,
|
2024-09-23 22:42:51 +10:00
|
|
|
radius: parent.userData.radius,
|
|
|
|
center: parent.userData.center,
|
2025-03-18 11:14:12 +11:00
|
|
|
ccw:
|
|
|
|
parent.userData.ccw !== undefined ? parent.userData.ccw : true,
|
|
|
|
}
|
|
|
|
} else if (parent.name === ARC_SEGMENT) {
|
|
|
|
update = segmentUtils.arc.update
|
|
|
|
input = {
|
|
|
|
type: 'arc-segment',
|
|
|
|
from: parent.userData.from,
|
|
|
|
to: parent.userData.to,
|
|
|
|
radius: parent.userData.radius,
|
|
|
|
center: parent.userData.center,
|
|
|
|
ccw:
|
|
|
|
parent.userData.ccw !== undefined ? parent.userData.ccw : true,
|
|
|
|
}
|
|
|
|
} else if (parent.name === THREE_POINT_ARC_SEGMENT) {
|
|
|
|
update = segmentUtils.threePointArc.update
|
|
|
|
input = {
|
|
|
|
type: 'circle-three-point-segment',
|
|
|
|
p1: parent.userData.p1,
|
|
|
|
p2: parent.userData.p2,
|
|
|
|
p3: parent.userData.p3,
|
2024-09-23 22:42:51 +10:00
|
|
|
}
|
2024-09-13 21:14:14 +10:00
|
|
|
}
|
2025-03-18 11:14:12 +11:00
|
|
|
|
2024-09-13 21:14:14 +10:00
|
|
|
update &&
|
|
|
|
update({
|
2024-04-04 11:07:51 +11:00
|
|
|
prevSegment: parent.userData.prevSegment,
|
2024-09-13 21:14:14 +10:00
|
|
|
input,
|
2024-04-04 11:07:51 +11:00
|
|
|
group: parent,
|
|
|
|
scale: factor,
|
2025-04-01 23:54:26 -07:00
|
|
|
sceneInfra: this.sceneInfra,
|
2024-04-04 11:07:51 +11:00
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
2025-04-01 23:54:26 -07:00
|
|
|
this.editorManager.setHighlightRange([defaultSourceRange()])
|
2024-04-04 11:07:51 +11:00
|
|
|
},
|
|
|
|
onMouseLeave: ({ selected, ...rest }: OnMouseEnterLeaveArgs) => {
|
2025-04-01 23:54:26 -07:00
|
|
|
this.editorManager.setHighlightRange([defaultSourceRange()])
|
2024-09-23 22:42:51 +10:00
|
|
|
const parent = getParentGroup(
|
|
|
|
selected,
|
|
|
|
SEGMENT_BODIES_PLUS_PROFILE_START
|
|
|
|
)
|
2024-04-04 11:07:51 +11:00
|
|
|
if (parent) {
|
2025-04-01 23:54:26 -07:00
|
|
|
const orthoFactor = orthoScale(this.sceneInfra.camControls.camera)
|
2024-04-04 11:07:51 +11:00
|
|
|
|
2024-09-23 22:42:51 +10:00
|
|
|
let input: SegmentInputs = {
|
2024-09-13 21:14:14 +10:00
|
|
|
type: 'straight-segment',
|
|
|
|
from: parent.userData.from,
|
|
|
|
to: parent.userData.to,
|
2024-09-23 22:42:51 +10:00
|
|
|
}
|
2024-04-04 11:07:51 +11:00
|
|
|
const factor =
|
2025-04-01 23:54:26 -07:00
|
|
|
(this.sceneInfra.camControls.camera instanceof OrthographicCamera
|
2024-04-04 11:07:51 +11:00
|
|
|
? orthoFactor
|
2025-04-01 23:54:26 -07:00
|
|
|
: perspScale(this.sceneInfra.camControls.camera, parent)) /
|
|
|
|
this.sceneInfra._baseUnitMultiplier
|
2024-09-13 21:14:14 +10:00
|
|
|
let update: SegmentUtils['update'] | null = null
|
2024-04-04 11:07:51 +11:00
|
|
|
if (parent.name === STRAIGHT_SEGMENT) {
|
2024-09-13 21:14:14 +10:00
|
|
|
update = segmentUtils.straight.update
|
2024-04-04 11:07:51 +11:00
|
|
|
} else if (parent.name === TANGENTIAL_ARC_TO_SEGMENT) {
|
2025-04-11 14:17:20 -04:00
|
|
|
update = segmentUtils.tangentialArc.update
|
2024-09-23 22:42:51 +10:00
|
|
|
input = {
|
|
|
|
type: 'arc-segment',
|
|
|
|
from: parent.userData.from,
|
2025-03-18 11:14:12 +11:00
|
|
|
to: parent.userData.to,
|
|
|
|
radius: parent.userData.radius,
|
|
|
|
center: parent.userData.center,
|
|
|
|
ccw:
|
|
|
|
parent.userData.ccw !== undefined ? parent.userData.ccw : true,
|
|
|
|
}
|
|
|
|
} else if (parent.name === CIRCLE_SEGMENT) {
|
|
|
|
update = segmentUtils.circle.update
|
|
|
|
input = {
|
|
|
|
type: 'arc-segment',
|
|
|
|
from: parent.userData.from,
|
|
|
|
to: parent.userData.to,
|
2024-09-23 22:42:51 +10:00
|
|
|
radius: parent.userData.radius,
|
|
|
|
center: parent.userData.center,
|
2025-03-18 11:14:12 +11:00
|
|
|
ccw:
|
|
|
|
parent.userData.ccw !== undefined ? parent.userData.ccw : true,
|
|
|
|
}
|
|
|
|
} else if (parent.name === ARC_SEGMENT) {
|
|
|
|
update = segmentUtils.arc.update
|
|
|
|
input = {
|
|
|
|
type: 'arc-segment',
|
|
|
|
from: parent.userData.from,
|
|
|
|
to: parent.userData.to,
|
|
|
|
radius: parent.userData.radius,
|
|
|
|
center: parent.userData.center,
|
|
|
|
ccw:
|
|
|
|
parent.userData.ccw !== undefined ? parent.userData.ccw : true,
|
|
|
|
}
|
|
|
|
} else if (parent.name === THREE_POINT_ARC_SEGMENT) {
|
|
|
|
update = segmentUtils.threePointArc.update
|
|
|
|
input = {
|
|
|
|
type: 'circle-three-point-segment',
|
|
|
|
p1: parent.userData.p1,
|
|
|
|
p2: parent.userData.p2,
|
|
|
|
p3: parent.userData.p3,
|
2024-09-23 22:42:51 +10:00
|
|
|
}
|
2024-09-13 21:14:14 +10:00
|
|
|
}
|
2025-03-18 11:14:12 +11:00
|
|
|
|
2024-09-13 21:14:14 +10:00
|
|
|
update &&
|
|
|
|
update({
|
2024-04-04 11:07:51 +11:00
|
|
|
prevSegment: parent.userData.prevSegment,
|
2024-09-13 21:14:14 +10:00
|
|
|
input,
|
2024-04-04 11:07:51 +11:00
|
|
|
group: parent,
|
|
|
|
scale: factor,
|
2025-04-01 23:54:26 -07:00
|
|
|
sceneInfra: this.sceneInfra,
|
2024-04-04 11:07:51 +11:00
|
|
|
})
|
|
|
|
}
|
|
|
|
const isSelected = parent?.userData?.isSelected
|
|
|
|
colorSegment(
|
|
|
|
selected,
|
2024-05-09 08:38:42 -04:00
|
|
|
isSelected
|
|
|
|
? 0x0000ff
|
|
|
|
: parent?.userData?.baseColor ||
|
2025-04-01 23:54:26 -07:00
|
|
|
getThemeColorForThreeJs(this.sceneInfra._theme)
|
2024-04-04 11:07:51 +11:00
|
|
|
)
|
|
|
|
const extraSegmentGroup = parent?.getObjectByName(EXTRA_SEGMENT_HANDLE)
|
|
|
|
if (extraSegmentGroup) {
|
|
|
|
extraSegmentGroup.traverse((child) => {
|
|
|
|
if (child instanceof Points || child instanceof Mesh) {
|
|
|
|
child.material.opacity = 0
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
if ([X_AXIS, Y_AXIS].includes(selected?.userData?.type)) {
|
|
|
|
const obj = selected as Mesh
|
|
|
|
const mat = obj.material as MeshBasicMaterial
|
|
|
|
mat.color.set(obj.userData.baseColor)
|
|
|
|
if (obj.userData.isSelected) mat.color.offsetHSL(0, 0, 0.2)
|
|
|
|
}
|
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
2025-04-14 23:51:14 +02:00
|
|
|
|
2024-05-24 20:54:42 +10:00
|
|
|
resetOverlays() {
|
2025-04-01 23:54:26 -07:00
|
|
|
this.sceneInfra.modelingSend({
|
2024-05-24 20:54:42 +10:00
|
|
|
type: 'Set Segment Overlays',
|
|
|
|
data: {
|
|
|
|
type: 'clear',
|
|
|
|
},
|
|
|
|
})
|
|
|
|
}
|
2025-03-18 11:14:12 +11:00
|
|
|
|
2025-04-01 23:54:26 -07:00
|
|
|
async getSketchOrientationDetails(sketch: Sketch): Promise<{
|
|
|
|
quat: Quaternion
|
|
|
|
sketchDetails: Omit<
|
|
|
|
SketchDetails & { faceId?: string },
|
|
|
|
'sketchNodePaths' | 'sketchEntryNodePath' | 'planeNodePath'
|
|
|
|
>
|
|
|
|
}> {
|
|
|
|
if (sketch.on.type === 'plane') {
|
2025-04-24 22:01:27 +12:00
|
|
|
const zAxis = crossProduct(sketch?.on.xAxis, sketch?.on.yAxis)
|
2025-04-01 23:54:26 -07:00
|
|
|
return {
|
|
|
|
quat: getQuaternionFromZAxis(massageFormats(zAxis)),
|
|
|
|
sketchDetails: {
|
|
|
|
zAxis: [zAxis.x, zAxis.y, zAxis.z],
|
|
|
|
yAxis: [sketch.on.yAxis.x, sketch.on.yAxis.y, sketch.on.yAxis.z],
|
|
|
|
origin: [0, 0, 0],
|
|
|
|
faceId: sketch.on.id,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
const faceInfo = await this.getFaceDetails(sketch.on.id)
|
|
|
|
|
|
|
|
if (!faceInfo?.origin || !faceInfo?.z_axis || !faceInfo?.y_axis)
|
|
|
|
return Promise.reject('face info')
|
|
|
|
const { z_axis, y_axis, origin } = faceInfo
|
|
|
|
const quaternion = quaternionFromUpNForward(
|
|
|
|
new Vector3(y_axis.x, y_axis.y, y_axis.z),
|
|
|
|
new Vector3(z_axis.x, z_axis.y, z_axis.z)
|
|
|
|
)
|
|
|
|
return {
|
|
|
|
quat: quaternion,
|
|
|
|
sketchDetails: {
|
|
|
|
zAxis: [z_axis.x, z_axis.y, z_axis.z],
|
|
|
|
yAxis: [y_axis.x, y_axis.y, y_axis.z],
|
|
|
|
origin: [origin.x, origin.y, origin.z],
|
|
|
|
faceId: sketch.on.id,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Retrieves orientation details for a given entity representing a face (brep face or default plane).
|
|
|
|
* This function asynchronously fetches and returns the origin, x-axis, y-axis, and z-axis details
|
|
|
|
* for a specified entity ID. It is primarily used to obtain the orientation of a face in the scene,
|
|
|
|
* which is essential for calculating the correct positioning and alignment of the client side sketch.
|
|
|
|
*
|
|
|
|
* @param entityId - The ID of the entity for which orientation details are being fetched.
|
|
|
|
* @returns A promise that resolves with the orientation details of the face.
|
|
|
|
*/
|
|
|
|
async getFaceDetails(
|
|
|
|
entityId: string
|
|
|
|
): Promise<Models['GetSketchModePlane_type']> {
|
|
|
|
// TODO mode engine connection to allow batching returns and batch the following
|
|
|
|
await this.engineCommandManager.sendSceneCommand({
|
|
|
|
type: 'modeling_cmd_req',
|
|
|
|
cmd_id: uuidv4(),
|
|
|
|
cmd: {
|
|
|
|
type: 'enable_sketch_mode',
|
|
|
|
adjust_camera: false,
|
|
|
|
animated: false,
|
|
|
|
ortho: false,
|
|
|
|
entity_id: entityId,
|
|
|
|
},
|
|
|
|
})
|
2025-04-27 16:54:32 -07:00
|
|
|
let resp = await this.engineCommandManager.sendSceneCommand({
|
2025-04-01 23:54:26 -07:00
|
|
|
type: 'modeling_cmd_req',
|
|
|
|
cmd_id: uuidv4(),
|
|
|
|
cmd: { type: 'get_sketch_mode_plane' },
|
|
|
|
})
|
2025-04-27 16:54:32 -07:00
|
|
|
|
|
|
|
if (!resp) {
|
|
|
|
return Promise.reject('no response')
|
|
|
|
}
|
|
|
|
|
|
|
|
if (isArray(resp)) {
|
|
|
|
resp = resp[0]
|
|
|
|
}
|
|
|
|
|
2025-04-01 23:54:26 -07:00
|
|
|
const faceInfo =
|
|
|
|
resp?.success &&
|
|
|
|
resp?.resp.type === 'modeling' &&
|
|
|
|
resp?.resp?.data?.modeling_response?.type === 'get_sketch_mode_plane'
|
|
|
|
? resp?.resp?.data?.modeling_response.data
|
|
|
|
: ({} as Models['GetSketchModePlane_type'])
|
|
|
|
await this.engineCommandManager.sendSceneCommand({
|
|
|
|
type: 'modeling_cmd_req',
|
|
|
|
cmd_id: uuidv4(),
|
|
|
|
cmd: { type: 'sketch_mode_disable' },
|
|
|
|
})
|
|
|
|
return faceInfo
|
|
|
|
}
|
|
|
|
|
2025-03-18 11:14:12 +11:00
|
|
|
drawDashedLine({ from, to }: { from: Coords2d; to: Coords2d }) {
|
2025-04-01 23:54:26 -07:00
|
|
|
const baseColor = getThemeColorForThreeJs(this.sceneInfra._theme)
|
2025-03-18 11:14:12 +11:00
|
|
|
const color = baseColor
|
|
|
|
const meshType = STRAIGHT_SEGMENT_DASH
|
|
|
|
|
|
|
|
const segmentGroup = new Group()
|
|
|
|
const shape = new Shape()
|
|
|
|
shape.moveTo(0, -5) // The width of the line in px (2.4px in this case)
|
|
|
|
shape.lineTo(0, 5)
|
|
|
|
const line = new LineCurve3(
|
|
|
|
new Vector3(from[0], from[1], 0),
|
|
|
|
new Vector3(to[0], to[1], 0)
|
|
|
|
)
|
|
|
|
const geometry = new ExtrudeGeometry(shape, {
|
|
|
|
steps: 2,
|
|
|
|
bevelEnabled: false,
|
|
|
|
extrudePath: line,
|
|
|
|
})
|
|
|
|
const body = new MeshBasicMaterial({ color })
|
|
|
|
const mesh = new Mesh(geometry, body)
|
|
|
|
|
|
|
|
mesh.userData.type = meshType
|
|
|
|
mesh.name = meshType
|
|
|
|
segmentGroup.name = DRAFT_DASHED_LINE
|
|
|
|
segmentGroup.userData = {
|
|
|
|
type: DRAFT_DASHED_LINE,
|
|
|
|
from,
|
|
|
|
to,
|
|
|
|
}
|
|
|
|
|
|
|
|
segmentGroup.add(mesh)
|
|
|
|
segmentGroup.layers.set(SKETCH_LAYER)
|
|
|
|
segmentGroup.traverse((child) => {
|
|
|
|
child.layers.set(SKETCH_LAYER)
|
|
|
|
})
|
|
|
|
if (this.currentSketchQuaternion) {
|
|
|
|
segmentGroup.setRotationFromQuaternion(this.currentSketchQuaternion)
|
|
|
|
}
|
|
|
|
return {
|
|
|
|
group: segmentGroup,
|
|
|
|
updater: (group: Group, to: Coords2d, orthoFactor: number) => {
|
|
|
|
const scale =
|
2025-04-01 23:54:26 -07:00
|
|
|
(this.sceneInfra.camControls.camera instanceof OrthographicCamera
|
2025-03-18 11:14:12 +11:00
|
|
|
? orthoFactor
|
2025-04-01 23:54:26 -07:00
|
|
|
: perspScale(this.sceneInfra.camControls.camera, group)) /
|
|
|
|
this.sceneInfra._baseUnitMultiplier
|
2025-03-18 11:14:12 +11:00
|
|
|
const from = group.userData.from
|
|
|
|
|
|
|
|
const shape = new Shape()
|
|
|
|
shape.moveTo(0, (-SEGMENT_WIDTH_PX / 2) * scale) // The width of the line in px (2.4px in this case)
|
|
|
|
shape.lineTo(0, (SEGMENT_WIDTH_PX / 2) * scale)
|
|
|
|
|
|
|
|
const straightSegmentBodyDashed = group.children.find(
|
|
|
|
(child) => child.userData.type === STRAIGHT_SEGMENT_DASH
|
|
|
|
) as Mesh
|
|
|
|
if (straightSegmentBodyDashed) {
|
|
|
|
straightSegmentBodyDashed.geometry = dashedStraight(
|
|
|
|
from,
|
|
|
|
to,
|
|
|
|
shape,
|
|
|
|
scale
|
|
|
|
)
|
|
|
|
}
|
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
2024-02-11 12:59:00 +11:00
|
|
|
}
|
|
|
|
|
|
|
|
// calculations/pure-functions/easy to test so no excuse not to
|
|
|
|
|
2025-02-12 10:22:56 +13:00
|
|
|
function prepareTruncatedAst(
|
2025-02-15 00:57:04 +11:00
|
|
|
sketchNodePaths: PathToNode[],
|
2024-10-30 16:52:17 -04:00
|
|
|
ast: Node<Program>,
|
2025-02-12 10:22:56 +13:00
|
|
|
variables: VariableMap,
|
2024-02-11 12:59:00 +11:00
|
|
|
draftSegment?: DraftSegment
|
2024-06-24 11:45:40 -04:00
|
|
|
):
|
|
|
|
| {
|
2024-10-30 16:52:17 -04:00
|
|
|
truncatedAst: Node<Program>
|
2025-02-15 00:57:04 +11:00
|
|
|
// can I remove the below?
|
2024-06-24 11:45:40 -04:00
|
|
|
variableDeclarationName: string
|
|
|
|
}
|
|
|
|
| Error {
|
2025-02-15 00:57:04 +11:00
|
|
|
const bodyStartIndex = Number(sketchNodePaths?.[0]?.[1]?.[0]) || 0
|
|
|
|
const bodyEndIndex =
|
|
|
|
Number(sketchNodePaths[sketchNodePaths.length - 1]?.[1]?.[0]) ||
|
|
|
|
ast.body.length
|
2024-07-25 20:11:46 -04:00
|
|
|
const _ast = structuredClone(ast)
|
2024-02-11 12:59:00 +11:00
|
|
|
|
2024-10-30 16:52:17 -04:00
|
|
|
const _node = getNodeFromPath<Node<VariableDeclaration>>(
|
2024-06-24 11:45:40 -04:00
|
|
|
_ast,
|
2025-02-15 00:57:04 +11:00
|
|
|
sketchNodePaths[0] || [],
|
2024-06-24 11:45:40 -04:00
|
|
|
'VariableDeclaration'
|
|
|
|
)
|
|
|
|
if (err(_node)) return _node
|
2025-02-15 00:57:04 +11:00
|
|
|
const variableDeclarationName = _node.node?.declaration?.id?.name || ''
|
2024-09-27 15:44:44 -07:00
|
|
|
const sg = sketchFromKclValue(
|
2025-02-12 10:22:56 +13:00
|
|
|
variables[variableDeclarationName],
|
Remove KclValue::SketchGroup variant (#3446)
We can store Rust types like `SketchGroup` as their own variant of `KclValue`, or as `KclValue::UserVal`. Sometimes we store in one and try to read from the other, which fails. This causes bugs, like #3338.
Instead, we should use either ::SketchGroup or ::UserVal, and stop using the other. If we stopped using ::UserVal, we'd need a new variant for every Rust type we wanted to build, including user-defined types. So I don't think that's practical.
Instead, we should store every KCL value by de/serializing it into UserVal. This is a first step along that path, removing just the SketchGroup variants. If it goes well, we can remove the other specialized variants too.
My only concern is there might be performance implications from how frequently we convert between serde_json::Value and Rust types via Serde. But I'm not too worried -- there's no parsing JSON strings, just traversing serde_json::Value trees. This isn't great for performance but I think it'll probably be miniscule in comparison to doing all the API calls.
2024-08-21 11:06:48 -05:00
|
|
|
variableDeclarationName
|
|
|
|
)
|
|
|
|
if (err(sg)) return sg
|
2024-10-23 12:42:54 -05:00
|
|
|
const lastSeg = sg?.paths.slice(-1)[0]
|
2024-02-11 12:59:00 +11:00
|
|
|
if (draftSegment) {
|
|
|
|
// truncatedAst needs to setup with another segment at the end
|
|
|
|
let newSegment
|
|
|
|
if (draftSegment === 'line') {
|
KCL: Use keyword arguments for line, lineTo, extrude and close (#5249)
Part of #4600.
PR: https://github.com/KittyCAD/modeling-app/pull/4826
# Changes to KCL stdlib
- `line(point, sketch, tag)` and `lineTo(point, sketch, tag)` are combined into `line(@sketch, end?, endAbsolute?, tag?)`
- `close(sketch, tag?)` is now `close(@sketch, tag?)`
- `extrude(length, sketch)` is now `extrude(@sketch, length)`
Note that if a parameter starts with `@` like `@sketch`, it doesn't have any label when called, so you call it like this:
```
sketch = startSketchAt([0, 0])
line(sketch, end = [3, 3], tag = $hi)
```
Note also that if you're using a `|>` pipeline, you can omit the `@` argument and it will be assumed to be the LHS of the `|>`. So the above could be written as
```
sketch = startSketchAt([0, 0])
|> line(end = [3, 3], tag = $hi)
```
Also changes frontend tests to use KittyCAD/kcl-samples#139 instead of its main
The regex find-and-replace I use for migrating code (note these don't work with multi-line expressions) are:
```
line\(([^=]*), %\)
line(end = $1)
line\((.*), %, (.*)\)
line(end = $1, tag = $2)
lineTo\((.*), %\)
line(endAbsolute = $1)
lineTo\((.*), %, (.*)\)
line(endAbsolute = $1, tag = $2)
extrude\((.*), %\)
extrude(length = $1)
extrude\(([^=]*), ([a-zA-Z0-9]+)\)
extrude($2, length = $1)
close\(%, (.*)\)
close(tag = $1)
```
# Selected notes from commits before I squash them all
* Fix test 'yRelative to horizontal distance'
Fixes:
- Make a lineTo helper
- Fix pathToNode to go through the labeled arg .arg property
* Fix test by changing lookups into transformMap
Parts of the code assumed that `line` is always a relative call. But
actually now it might be absolute, if it's got an `endAbsolute` parameter.
So, change whether to look up `line` or `lineTo` and the relevant absolute
or relative line types based on that parameter.
* Stop asserting on exact source ranges
When I changed line to kwargs, all the source ranges we assert on became
slightly different. I find these assertions to be very very low value.
So I'm removing them.
* Fix more tests: getConstraintType calls weren't checking if the
'line' fn was absolute or relative.
* Fixed another queryAst test
There were 2 problems:
- Test was looking for the old style of `line` call to choose an offset
for pathToNode
- Test assumed that the `tag` param was always the third one, but in
a kwarg call, you have to look it up by label
* Fix test: traverse was not handling CallExpressionKw
* Fix another test, addTagKw
addTag helper was not aware of kw args.
* Convert close from positional to kwargs
If the close() call has 0 args, or a single unlabeled arg, the parser
interprets it as a CallExpression (positional) not a CallExpressionKw.
But then if a codemod wants to add a tag to it, it tries adding a kwarg
called 'tag', which fails because the CallExpression doesn't need
kwargs inserted into it.
The fix is: change the node from CallExpression to CallExpressionKw, and
update getNodeFromPath to take a 'replacement' arg, so we can replace
the old node with the new node in the AST.
* Fix the last test
Test was looking for `lineTo` as a substring of the input KCL program.
But there's no more lineTo function, so I changed it to look for
line() with an endAbsolute arg, which is the new equivalent.
Also changed the getConstraintInfo code to look up the lineTo if using
line with endAbsolute.
* Fix many bad regex find-replaces
I wrote a regex find-and-replace which converted `line` calls from
positional to keyword calls. But it was accidentally applied to more
places than it should be, for example, angledLine, xLine and yLine calls.
Fixes this.
* Fixes test 'Basic sketch › code pane closed at start'
Problem was, the getNodeFromPath call might not actually find a callExpressionKw,
it might find a callExpression. So the `giveSketchFnCallTag` thought
it was modifying a kwargs call, but it was actually modifying a positional
call.
This meant it tried to push a labeled argument in, rather than a normal
arg, and a lot of other problems. Fixed by doing runtime typechecking.
* Fix: Optional args given with wrong type were silently ignored
Optional args don't have to be given. But if the user gives them, they
should be the right type.
Bug: if the KCL interpreter found an optional arg, which was given, but
was the wrong type, it would ignore it and pretend the arg was never
given at all. This was confusing for users.
Fix: Now if you give an optional arg, but it's the wrong type, KCL will
emit a type error just like it would for a mandatory argument.
---------
Signed-off-by: Nick Cameron <nrc@ncameron.org>
Co-authored-by: Nick Cameron <nrc@ncameron.org>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Frank Noirot <frank@kittycad.io>
Co-authored-by: Kevin Nadro <kevin@zoo.dev>
Co-authored-by: Jonathan Tran <jonnytran@gmail.com>
2025-02-04 08:31:43 -06:00
|
|
|
newSegment = createCallExpressionStdLibKw('line', null, [
|
|
|
|
createLabeledArg(
|
2025-04-11 14:17:20 -04:00
|
|
|
ARG_END,
|
KCL: Use keyword arguments for line, lineTo, extrude and close (#5249)
Part of #4600.
PR: https://github.com/KittyCAD/modeling-app/pull/4826
# Changes to KCL stdlib
- `line(point, sketch, tag)` and `lineTo(point, sketch, tag)` are combined into `line(@sketch, end?, endAbsolute?, tag?)`
- `close(sketch, tag?)` is now `close(@sketch, tag?)`
- `extrude(length, sketch)` is now `extrude(@sketch, length)`
Note that if a parameter starts with `@` like `@sketch`, it doesn't have any label when called, so you call it like this:
```
sketch = startSketchAt([0, 0])
line(sketch, end = [3, 3], tag = $hi)
```
Note also that if you're using a `|>` pipeline, you can omit the `@` argument and it will be assumed to be the LHS of the `|>`. So the above could be written as
```
sketch = startSketchAt([0, 0])
|> line(end = [3, 3], tag = $hi)
```
Also changes frontend tests to use KittyCAD/kcl-samples#139 instead of its main
The regex find-and-replace I use for migrating code (note these don't work with multi-line expressions) are:
```
line\(([^=]*), %\)
line(end = $1)
line\((.*), %, (.*)\)
line(end = $1, tag = $2)
lineTo\((.*), %\)
line(endAbsolute = $1)
lineTo\((.*), %, (.*)\)
line(endAbsolute = $1, tag = $2)
extrude\((.*), %\)
extrude(length = $1)
extrude\(([^=]*), ([a-zA-Z0-9]+)\)
extrude($2, length = $1)
close\(%, (.*)\)
close(tag = $1)
```
# Selected notes from commits before I squash them all
* Fix test 'yRelative to horizontal distance'
Fixes:
- Make a lineTo helper
- Fix pathToNode to go through the labeled arg .arg property
* Fix test by changing lookups into transformMap
Parts of the code assumed that `line` is always a relative call. But
actually now it might be absolute, if it's got an `endAbsolute` parameter.
So, change whether to look up `line` or `lineTo` and the relevant absolute
or relative line types based on that parameter.
* Stop asserting on exact source ranges
When I changed line to kwargs, all the source ranges we assert on became
slightly different. I find these assertions to be very very low value.
So I'm removing them.
* Fix more tests: getConstraintType calls weren't checking if the
'line' fn was absolute or relative.
* Fixed another queryAst test
There were 2 problems:
- Test was looking for the old style of `line` call to choose an offset
for pathToNode
- Test assumed that the `tag` param was always the third one, but in
a kwarg call, you have to look it up by label
* Fix test: traverse was not handling CallExpressionKw
* Fix another test, addTagKw
addTag helper was not aware of kw args.
* Convert close from positional to kwargs
If the close() call has 0 args, or a single unlabeled arg, the parser
interprets it as a CallExpression (positional) not a CallExpressionKw.
But then if a codemod wants to add a tag to it, it tries adding a kwarg
called 'tag', which fails because the CallExpression doesn't need
kwargs inserted into it.
The fix is: change the node from CallExpression to CallExpressionKw, and
update getNodeFromPath to take a 'replacement' arg, so we can replace
the old node with the new node in the AST.
* Fix the last test
Test was looking for `lineTo` as a substring of the input KCL program.
But there's no more lineTo function, so I changed it to look for
line() with an endAbsolute arg, which is the new equivalent.
Also changed the getConstraintInfo code to look up the lineTo if using
line with endAbsolute.
* Fix many bad regex find-replaces
I wrote a regex find-and-replace which converted `line` calls from
positional to keyword calls. But it was accidentally applied to more
places than it should be, for example, angledLine, xLine and yLine calls.
Fixes this.
* Fixes test 'Basic sketch › code pane closed at start'
Problem was, the getNodeFromPath call might not actually find a callExpressionKw,
it might find a callExpression. So the `giveSketchFnCallTag` thought
it was modifying a kwargs call, but it was actually modifying a positional
call.
This meant it tried to push a labeled argument in, rather than a normal
arg, and a lot of other problems. Fixed by doing runtime typechecking.
* Fix: Optional args given with wrong type were silently ignored
Optional args don't have to be given. But if the user gives them, they
should be the right type.
Bug: if the KCL interpreter found an optional arg, which was given, but
was the wrong type, it would ignore it and pretend the arg was never
given at all. This was confusing for users.
Fix: Now if you give an optional arg, but it's the wrong type, KCL will
emit a type error just like it would for a mandatory argument.
---------
Signed-off-by: Nick Cameron <nrc@ncameron.org>
Co-authored-by: Nick Cameron <nrc@ncameron.org>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Frank Noirot <frank@kittycad.io>
Co-authored-by: Kevin Nadro <kevin@zoo.dev>
Co-authored-by: Jonathan Tran <jonnytran@gmail.com>
2025-02-04 08:31:43 -06:00
|
|
|
createArrayExpression([createLiteral(0), createLiteral(0)])
|
|
|
|
),
|
2024-02-11 12:59:00 +11:00
|
|
|
])
|
|
|
|
} else {
|
2025-04-11 14:17:20 -04:00
|
|
|
newSegment = createCallExpressionStdLibKw('tangentialArc', null, [
|
|
|
|
createLabeledArg(
|
|
|
|
ARG_END_ABSOLUTE,
|
|
|
|
createArrayExpression([
|
|
|
|
createLiteral(lastSeg.to[0]),
|
|
|
|
createLiteral(lastSeg.to[1]),
|
|
|
|
])
|
|
|
|
),
|
2024-02-11 12:59:00 +11:00
|
|
|
])
|
|
|
|
}
|
|
|
|
;(
|
2025-02-15 00:57:04 +11:00
|
|
|
(_ast.body[bodyStartIndex] as VariableDeclaration).declaration
|
2024-02-11 12:59:00 +11:00
|
|
|
.init as PipeExpression
|
|
|
|
).body.push(newSegment)
|
|
|
|
// update source ranges to section we just added.
|
2024-09-27 15:44:44 -07:00
|
|
|
// hacks like this wouldn't be needed if the AST put pathToNode info in memory/sketch segments
|
2024-12-06 13:57:31 +13:00
|
|
|
const pResult = parse(recast(_ast)) // get source ranges correct since unfortunately we still rely on them
|
|
|
|
if (trap(pResult) || !resultIsOk(pResult))
|
|
|
|
return Error('Unexpected compilation error')
|
|
|
|
const updatedSrcRangeAst = pResult.program
|
2024-06-24 11:45:40 -04:00
|
|
|
|
2024-02-11 12:59:00 +11:00
|
|
|
const lastPipeItem = (
|
2025-02-15 00:57:04 +11:00
|
|
|
(updatedSrcRangeAst.body[bodyStartIndex] as VariableDeclaration)
|
|
|
|
.declaration.init as PipeExpression
|
2024-02-11 12:59:00 +11:00
|
|
|
).body.slice(-1)[0]
|
|
|
|
;(
|
2025-02-15 00:57:04 +11:00
|
|
|
(_ast.body[bodyStartIndex] as VariableDeclaration).declaration
|
2024-02-11 12:59:00 +11:00
|
|
|
.init as PipeExpression
|
|
|
|
).body.slice(-1)[0].start = lastPipeItem.start
|
|
|
|
|
|
|
|
_ast.end = lastPipeItem.end
|
2025-02-15 00:57:04 +11:00
|
|
|
const varDec = _ast.body[bodyStartIndex] as Node<VariableDeclaration>
|
2024-02-11 12:59:00 +11:00
|
|
|
varDec.end = lastPipeItem.end
|
2024-12-07 07:16:04 +13:00
|
|
|
const declarator = varDec.declaration
|
2024-02-11 12:59:00 +11:00
|
|
|
declarator.end = lastPipeItem.end
|
2024-10-30 16:52:17 -04:00
|
|
|
const init = declarator.init as Node<PipeExpression>
|
2024-02-11 12:59:00 +11:00
|
|
|
init.end = lastPipeItem.end
|
|
|
|
init.body.slice(-1)[0].end = lastPipeItem.end
|
|
|
|
}
|
2024-10-30 16:52:17 -04:00
|
|
|
const truncatedAst: Node<Program> = {
|
2024-02-11 12:59:00 +11:00
|
|
|
..._ast,
|
2025-02-15 00:57:04 +11:00
|
|
|
body: structuredClone(_ast.body.slice(bodyStartIndex, bodyEndIndex + 1)),
|
2024-02-11 12:59:00 +11:00
|
|
|
}
|
2024-06-24 11:45:40 -04:00
|
|
|
|
2024-02-11 12:59:00 +11:00
|
|
|
return {
|
|
|
|
truncatedAst,
|
|
|
|
variableDeclarationName,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-02-15 00:57:04 +11:00
|
|
|
function sketchFromPathToNode({
|
2024-02-11 12:59:00 +11:00
|
|
|
pathToNode,
|
|
|
|
ast,
|
2025-02-12 10:22:56 +13:00
|
|
|
variables,
|
2025-04-01 23:54:26 -07:00
|
|
|
kclManager,
|
2024-02-11 12:59:00 +11:00
|
|
|
}: {
|
|
|
|
pathToNode: PathToNode
|
|
|
|
ast: Program
|
2025-02-12 10:22:56 +13:00
|
|
|
variables: VariableMap
|
2025-04-01 23:54:26 -07:00
|
|
|
kclManager: KclManager
|
2024-09-27 15:44:44 -07:00
|
|
|
}): Sketch | null | Error {
|
2024-06-24 11:45:40 -04:00
|
|
|
const _varDec = getNodeFromPath<VariableDeclarator>(
|
2024-02-11 12:59:00 +11:00
|
|
|
kclManager.ast,
|
|
|
|
pathToNode,
|
|
|
|
'VariableDeclarator'
|
2024-06-24 11:45:40 -04:00
|
|
|
)
|
|
|
|
if (err(_varDec)) return _varDec
|
|
|
|
const varDec = _varDec.node
|
2025-02-12 10:22:56 +13:00
|
|
|
const result = variables[varDec?.id?.name || '']
|
2024-09-27 15:44:44 -07:00
|
|
|
if (result?.type === 'Solid') {
|
2025-01-22 09:42:09 +13:00
|
|
|
return result.value.sketch
|
2024-06-21 23:50:30 -07:00
|
|
|
}
|
2024-09-27 15:44:44 -07:00
|
|
|
const sg = sketchFromKclValue(result, varDec?.id?.name)
|
Remove KclValue::SketchGroup variant (#3446)
We can store Rust types like `SketchGroup` as their own variant of `KclValue`, or as `KclValue::UserVal`. Sometimes we store in one and try to read from the other, which fails. This causes bugs, like #3338.
Instead, we should use either ::SketchGroup or ::UserVal, and stop using the other. If we stopped using ::UserVal, we'd need a new variant for every Rust type we wanted to build, including user-defined types. So I don't think that's practical.
Instead, we should store every KCL value by de/serializing it into UserVal. This is a first step along that path, removing just the SketchGroup variants. If it goes well, we can remove the other specialized variants too.
My only concern is there might be performance implications from how frequently we convert between serde_json::Value and Rust types via Serde. But I'm not too worried -- there's no parsing JSON strings, just traversing serde_json::Value trees. This isn't great for performance but I think it'll probably be miniscule in comparison to doing all the API calls.
2024-08-21 11:06:48 -05:00
|
|
|
if (err(sg)) {
|
|
|
|
return null
|
2024-08-07 15:15:22 -04:00
|
|
|
}
|
Remove KclValue::SketchGroup variant (#3446)
We can store Rust types like `SketchGroup` as their own variant of `KclValue`, or as `KclValue::UserVal`. Sometimes we store in one and try to read from the other, which fails. This causes bugs, like #3338.
Instead, we should use either ::SketchGroup or ::UserVal, and stop using the other. If we stopped using ::UserVal, we'd need a new variant for every Rust type we wanted to build, including user-defined types. So I don't think that's practical.
Instead, we should store every KCL value by de/serializing it into UserVal. This is a first step along that path, removing just the SketchGroup variants. If it goes well, we can remove the other specialized variants too.
My only concern is there might be performance implications from how frequently we convert between serde_json::Value and Rust types via Serde. But I'm not too worried -- there's no parsing JSON strings, just traversing serde_json::Value trees. This isn't great for performance but I think it'll probably be miniscule in comparison to doing all the API calls.
2024-08-21 11:06:48 -05:00
|
|
|
return sg
|
2024-02-11 12:59:00 +11:00
|
|
|
}
|
|
|
|
|
|
|
|
function colorSegment(object: any, color: number) {
|
2024-03-02 08:48:30 +11:00
|
|
|
const segmentHead = getParentGroup(object, [ARROWHEAD, PROFILE_START])
|
|
|
|
if (segmentHead) {
|
|
|
|
segmentHead.traverse((child) => {
|
2024-02-11 12:59:00 +11:00
|
|
|
if (child instanceof Mesh) {
|
|
|
|
child.material.color.set(color)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
2024-09-23 22:42:51 +10:00
|
|
|
const straightSegmentBody = getParentGroup(object, SEGMENT_BODIES)
|
2024-02-11 12:59:00 +11:00
|
|
|
if (straightSegmentBody) {
|
|
|
|
straightSegmentBody.traverse((child) => {
|
2024-04-03 13:22:56 +11:00
|
|
|
if (child instanceof Mesh && !child.userData.ignoreColorChange) {
|
2024-02-11 12:59:00 +11:00
|
|
|
child.material.color.set(color)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export function getSketchQuaternion(
|
|
|
|
sketchPathToNode: PathToNode,
|
2025-04-01 23:54:26 -07:00
|
|
|
sketchNormalBackUp: [number, number, number] | null,
|
|
|
|
kclManager: KclManager
|
2024-06-24 11:45:40 -04:00
|
|
|
): Quaternion | Error {
|
2024-09-27 15:44:44 -07:00
|
|
|
const sketch = sketchFromPathToNode({
|
2024-02-11 12:59:00 +11:00
|
|
|
pathToNode: sketchPathToNode,
|
|
|
|
ast: kclManager.ast,
|
2025-02-12 10:22:56 +13:00
|
|
|
variables: kclManager.variables,
|
2025-04-01 23:54:26 -07:00
|
|
|
kclManager,
|
2024-02-11 12:59:00 +11:00
|
|
|
})
|
2024-09-27 15:44:44 -07:00
|
|
|
if (err(sketch)) return sketch
|
2025-04-24 23:10:06 +10:00
|
|
|
const zAxis =
|
|
|
|
sketch?.on.xAxis && sketch?.on.yAxis
|
|
|
|
? crossProduct(sketch?.on.xAxis, sketch?.on.yAxis)
|
|
|
|
: sketchNormalBackUp
|
2024-09-27 15:44:44 -07:00
|
|
|
if (!zAxis) return Error('Sketch zAxis not found')
|
2024-06-24 11:45:40 -04:00
|
|
|
|
2024-03-22 10:23:04 +11:00
|
|
|
return getQuaternionFromZAxis(massageFormats(zAxis))
|
|
|
|
}
|
2024-04-22 20:14:06 +10:00
|
|
|
|
2024-03-22 10:23:04 +11:00
|
|
|
export function getQuaternionFromZAxis(zAxis: Vector3): Quaternion {
|
2024-02-11 12:59:00 +11:00
|
|
|
const dummyCam = new PerspectiveCamera()
|
|
|
|
dummyCam.up.set(0, 0, 1)
|
2024-03-22 10:23:04 +11:00
|
|
|
dummyCam.position.copy(zAxis)
|
2024-02-11 12:59:00 +11:00
|
|
|
dummyCam.lookAt(0, 0, 0)
|
|
|
|
dummyCam.updateMatrix()
|
|
|
|
const quaternion = dummyCam.quaternion.clone()
|
|
|
|
|
|
|
|
const isVert = isQuaternionVertical(quaternion)
|
|
|
|
|
|
|
|
// because vertical quaternions are a gimbal lock, for the orbit controls
|
|
|
|
// it's best to set them explicitly to the vertical position with a known good camera up
|
2024-03-22 10:23:04 +11:00
|
|
|
if (isVert && zAxis.z < 0) {
|
2024-02-11 12:59:00 +11:00
|
|
|
quaternion.set(0, 1, 0, 0)
|
|
|
|
} else if (isVert) {
|
|
|
|
quaternion.set(0, 0, 0, 1)
|
|
|
|
}
|
|
|
|
return quaternion
|
|
|
|
}
|
|
|
|
|
2025-04-24 23:10:06 +10:00
|
|
|
function massageFormats(
|
|
|
|
a: Vec3Array | { x: number; y: number; z: number }
|
|
|
|
): Vector3 {
|
2024-08-22 16:08:49 -04:00
|
|
|
return isArray(a) ? new Vector3(a[0], a[1], a[2]) : new Vector3(a.x, a.y, a.z)
|
2024-02-11 12:59:00 +11:00
|
|
|
}
|
2024-12-17 15:12:18 -05:00
|
|
|
|
2025-02-15 00:57:04 +11:00
|
|
|
function getSketchesInfo({
|
|
|
|
sketchNodePaths,
|
|
|
|
ast,
|
|
|
|
variables,
|
2025-04-01 23:54:26 -07:00
|
|
|
kclManager,
|
2025-02-15 00:57:04 +11:00
|
|
|
}: {
|
|
|
|
sketchNodePaths: PathToNode[]
|
|
|
|
ast: Node<Program>
|
|
|
|
variables: VariableMap
|
2025-04-01 23:54:26 -07:00
|
|
|
kclManager: KclManager
|
2025-02-15 00:57:04 +11:00
|
|
|
}): {
|
|
|
|
sketch: Sketch
|
|
|
|
pathToNode: PathToNode
|
|
|
|
}[] {
|
|
|
|
const sketchesInfo: {
|
|
|
|
sketch: Sketch
|
|
|
|
pathToNode: PathToNode
|
|
|
|
}[] = []
|
|
|
|
for (const path of sketchNodePaths) {
|
|
|
|
const sketch = sketchFromPathToNode({
|
|
|
|
pathToNode: path,
|
|
|
|
ast,
|
|
|
|
variables,
|
2025-04-01 23:54:26 -07:00
|
|
|
kclManager,
|
2025-02-15 00:57:04 +11:00
|
|
|
})
|
|
|
|
if (err(sketch)) continue
|
|
|
|
if (!sketch) continue
|
|
|
|
sketchesInfo.push({
|
|
|
|
sketch,
|
|
|
|
pathToNode: path,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
return sketchesInfo
|
|
|
|
}
|
2025-04-14 23:51:14 +02:00
|
|
|
|
2024-12-17 15:12:18 -05:00
|
|
|
/**
|
|
|
|
* Given a SourceRange [x,y,boolean] create a Selections object which contains
|
|
|
|
* graphSelections with the artifact and codeRef.
|
|
|
|
* This can be passed to 'Set selection' to internally set the selection of the
|
|
|
|
* modelingMachine from code.
|
|
|
|
*/
|
|
|
|
function computeSelectionFromSourceRangeAndAST(
|
|
|
|
sourceRange: SourceRange,
|
2025-04-01 23:54:26 -07:00
|
|
|
ast: Node<Program>,
|
|
|
|
kclManager: KclManager
|
2024-12-17 15:12:18 -05:00
|
|
|
): Selections {
|
2025-03-29 17:25:26 -07:00
|
|
|
const artifactGraph = kclManager.artifactGraph
|
2024-12-17 15:12:18 -05:00
|
|
|
const artifact = getArtifactFromRange(sourceRange, artifactGraph) || undefined
|
|
|
|
const selection: Selections = {
|
|
|
|
graphSelections: [
|
|
|
|
{
|
|
|
|
artifact,
|
|
|
|
codeRef: codeRefFromRange(sourceRange, ast),
|
|
|
|
},
|
|
|
|
],
|
|
|
|
otherSelections: [],
|
|
|
|
}
|
|
|
|
return selection
|
|
|
|
}
|
2025-02-15 00:57:04 +11:00
|
|
|
|
|
|
|
function isGroupStartProfileForCurrentProfile(sketchEntryNodePath: PathToNode) {
|
|
|
|
return (group: Group<Object3DEventMap> | null) => {
|
|
|
|
if (group?.name !== PROFILE_START) return false
|
|
|
|
const groupExpressionIndex = Number(group.userData.pathToNode[1][0])
|
|
|
|
const isProfileStartOfCurrentExpr =
|
|
|
|
groupExpressionIndex === sketchEntryNodePath[1][0]
|
|
|
|
return isProfileStartOfCurrentExpr
|
|
|
|
}
|
|
|
|
}
|
2025-04-14 23:51:14 +02:00
|
|
|
|
|
|
|
// Returns the 2D tangent direction vector at the end of the segmentGroup if it's an arc.
|
|
|
|
function findTangentDirection(segmentGroup: Group) {
|
|
|
|
let tangentDirection: Coords2d | undefined
|
|
|
|
if (segmentGroup.userData.type === TANGENTIAL_ARC_TO_SEGMENT) {
|
|
|
|
const prevSegment = segmentGroup.userData.prevSegment
|
|
|
|
const arcInfo = getTangentialArcToInfo({
|
|
|
|
arcStartPoint: segmentGroup.userData.from,
|
|
|
|
arcEndPoint: segmentGroup.userData.to,
|
|
|
|
tanPreviousPoint: getTanPreviousPoint(prevSegment),
|
|
|
|
obtuse: true,
|
|
|
|
})
|
|
|
|
const tangentAngle =
|
|
|
|
arcInfo.endAngle + (Math.PI / 2) * (arcInfo.ccw ? 1 : -1)
|
|
|
|
tangentDirection = [Math.cos(tangentAngle), Math.sin(tangentAngle)]
|
|
|
|
} else if (
|
|
|
|
segmentGroup.userData.type === ARC_SEGMENT ||
|
|
|
|
segmentGroup.userData.type === THREE_POINT_ARC_SEGMENT
|
|
|
|
) {
|
|
|
|
const tangentAngle =
|
|
|
|
deg2Rad(
|
|
|
|
getAngle(segmentGroup.userData.center, segmentGroup.userData.to)
|
|
|
|
) +
|
|
|
|
(Math.PI / 2) * (segmentGroup.userData.ccw ? 1 : -1)
|
|
|
|
tangentDirection = [Math.cos(tangentAngle), Math.sin(tangentAngle)]
|
|
|
|
} else {
|
|
|
|
console.warn(
|
|
|
|
'Unsupported segment type for tangent direction calculation: ',
|
|
|
|
segmentGroup.userData.type
|
|
|
|
)
|
|
|
|
}
|
|
|
|
return tangentDirection
|
|
|
|
}
|