Nadro/adhoc/center rectangle (#4480)

* fix: big commit, doing this to save work then do a PR cleanup; center rectangle

* fix: making a center function for each scenario

* fix: reverting the update rectangle code since I have a center rectangle one

* fix: does not allow seletcing circle or rectangle tool while selecting a face

* chore: adding comment to better read the HTML

* fix: cleaning up for PR

* fix: pushing broken code for someone to checkout

* fix: fixed the typescript issues, removed the as keyword for my center rectangle expressions

* fix: removing comment

* fix: removed as for type narrowing checks
This commit is contained in:
Kevin Nadro
2024-11-18 10:04:09 -05:00
committed by GitHub
parent b2e895e508
commit 14ba66378d
8 changed files with 381 additions and 10 deletions

View File

@ -141,6 +141,7 @@ export function Toolbar({
>
{/* A menu item will either be a vertical line break, a button with a dropdown, or a single button */}
{currentModeItems.map((maybeIconConfig, i) => {
// Vertical Line Break
if (maybeIconConfig === 'break') {
return (
<div
@ -149,6 +150,7 @@ export function Toolbar({
/>
)
} else if (Array.isArray(maybeIconConfig)) {
// A button with a dropdown
return (
<ActionButtonDropdown
Element="button"
@ -215,6 +217,7 @@ export function Toolbar({
}
const itemConfig = maybeIconConfig
// A single button
return (
<div className="relative" key={itemConfig.id}>
<ActionButton

View File

@ -89,6 +89,7 @@ import { EngineCommandManager } from 'lang/std/engineConnection'
import {
getRectangleCallExpressions,
updateRectangleSketch,
updateCenterRectangleSketch,
} from 'lib/rectangleTool'
import { getThemeColorForThreeJs, Themes } from 'lib/theme'
import { err, reportRejection, trap } from 'lib/trap'
@ -1043,6 +1044,169 @@ export class SceneEntities {
},
})
}
setupDraftCenterRectangle = async (
sketchPathToNode: PathToNode,
forward: [number, number, number],
up: [number, number, number],
sketchOrigin: [number, number, number],
rectangleOrigin: [x: number, y: number]
) => {
let _ast = structuredClone(kclManager.ast)
const _node1 = getNodeFromPath<VariableDeclaration>(
_ast,
sketchPathToNode || [],
'VariableDeclaration'
)
if (trap(_node1)) return Promise.reject(_node1)
// startSketchOn already exists
const variableDeclarationName =
_node1.node?.declarations?.[0]?.id?.name || ''
const startSketchOn = _node1.node?.declarations
const startSketchOnInit = startSketchOn?.[0]?.init
const tags: [string, string, string] = [
findUniqueName(_ast, 'rectangleSegmentA'),
findUniqueName(_ast, 'rectangleSegmentB'),
findUniqueName(_ast, 'rectangleSegmentC'),
]
startSketchOn[0].init = createPipeExpression([
startSketchOnInit,
...getRectangleCallExpressions(rectangleOrigin, tags),
])
let _recastAst = parse(recast(_ast))
if (trap(_recastAst)) return Promise.reject(_recastAst)
_ast = _recastAst
const { programMemoryOverride, truncatedAst } = await this.setupSketch({
sketchPathToNode,
forward,
up,
position: sketchOrigin,
maybeModdedAst: _ast,
draftExpressionsIndices: { start: 0, end: 3 },
})
sceneInfra.setCallbacks({
onMove: async (args) => {
// Update the width and height of the draft rectangle
const pathToNodeTwo = structuredClone(sketchPathToNode)
pathToNodeTwo[1][0] = 0
const _node = getNodeFromPath<VariableDeclaration>(
truncatedAst,
pathToNodeTwo || [],
'VariableDeclaration'
)
if (trap(_node)) return Promise.reject(_node)
const sketchInit = _node.node?.declarations?.[0]?.init
const x = (args.intersectionPoint.twoD.x || 0) - rectangleOrigin[0]
const y = (args.intersectionPoint.twoD.y || 0) - rectangleOrigin[1]
if (sketchInit.type === 'PipeExpression') {
updateCenterRectangleSketch(
sketchInit,
x,
y,
tags[0],
rectangleOrigin[0],
rectangleOrigin[1]
)
}
const { execState } = await executeAst({
ast: truncatedAst,
useFakeExecutor: true,
engineCommandManager: this.engineCommandManager,
programMemoryOverride,
idGenerator: kclManager.execState.idGenerator,
})
const programMemory = execState.memory
this.sceneProgramMemory = programMemory
const sketch = sketchFromKclValue(
programMemory.get(variableDeclarationName),
variableDeclarationName
)
if (err(sketch)) return Promise.reject(sketch)
const sgPaths = sketch.paths
const orthoFactor = orthoScale(sceneInfra.camControls.camera)
this.updateSegment(sketch.start, 0, 0, _ast, orthoFactor, sketch)
sgPaths.forEach((seg, index) =>
this.updateSegment(seg, index, 0, _ast, orthoFactor, sketch)
)
},
onClick: async (args) => {
// If there is a valid camera interaction that matches, do that instead
const interaction = sceneInfra.camControls.getInteractionType(
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,
sketchPathToNode || [],
'VariableDeclaration'
)
if (trap(_node)) return
const sketchInit = _node.node?.declarations?.[0]?.init
if (sketchInit.type === 'PipeExpression') {
updateCenterRectangleSketch(
sketchInit,
x,
y,
tags[0],
rectangleOrigin[0],
rectangleOrigin[1]
)
let _recastAst = parse(recast(_ast))
if (trap(_recastAst)) return
_ast = _recastAst
// Update the primary AST and unequip the rectangle tool
await kclManager.executeAstMock(_ast)
sceneInfra.modelingSend({ type: 'Finish center rectangle' })
const { execState } = await executeAst({
ast: _ast,
useFakeExecutor: true,
engineCommandManager: this.engineCommandManager,
programMemoryOverride,
idGenerator: kclManager.execState.idGenerator,
})
const programMemory = execState.memory
// Prepare to update the THREEjs scene
this.sceneProgramMemory = programMemory
const sketch = sketchFromKclValue(
programMemory.get(variableDeclarationName),
variableDeclarationName
)
if (err(sketch)) return
const sgPaths = sketch.paths
const orthoFactor = orthoScale(sceneInfra.camControls.camera)
// Update the starting segment of the THREEjs scene
this.updateSegment(sketch.start, 0, 0, _ast, orthoFactor, sketch)
// Update the rest of the segments of the THREEjs scene
sgPaths.forEach((seg, index) =>
this.updateSegment(seg, index, 0, _ast, orthoFactor, sketch)
)
}
},
})
}
setupDraftCircle = async (
sketchPathToNode: PathToNode,
forward: [number, number, number],

View File

@ -8,6 +8,7 @@ import {
VariableDeclarator,
Expr,
Literal,
LiteralValue,
PipeSubstitution,
Identifier,
ArrayExpression,
@ -573,7 +574,7 @@ export function splitPathAtPipeExpression(pathToNode: PathToNode): {
return splitPathAtPipeExpression(pathToNode.slice(0, -1))
}
export function createLiteral(value: string | number): Node<Literal> {
export function createLiteral(value: LiteralValue): Node<Literal> {
return {
type: 'Literal',
start: 0,

View File

@ -1,5 +1,12 @@
import { Selections } from 'lib/selections'
import { Program, PathToNode } from './wasm'
import {
Program,
PathToNode,
CallExpression,
Literal,
ArrayExpression,
BinaryExpression,
} from './wasm'
import { getNodeFromPath } from './queryAst'
import { ArtifactGraph, filterArtifacts } from 'lang/std/artifactGraph'
import { isOverlap } from 'lib/utils'
@ -84,3 +91,19 @@ export function isCursorInSketchCommandRange(
([, artifact]) => artifact.type === 'path'
)?.[0] || false
}
export function isCallExpression(e: any): e is CallExpression {
return e && e.type === 'CallExpression'
}
export function isArrayExpression(e: any): e is ArrayExpression {
return e && e.type === 'ArrayExpression'
}
export function isLiteral(e: any): e is Literal {
return e && e.type === 'Literal'
}
export function isBinaryExpression(e: any): e is BinaryExpression {
return e && e.type === 'BinaryExpression'
}

View File

@ -62,6 +62,7 @@ export type { CallExpression } from '../wasm-lib/kcl/bindings/CallExpression'
export type { VariableDeclarator } from '../wasm-lib/kcl/bindings/VariableDeclarator'
export type { BinaryPart } from '../wasm-lib/kcl/bindings/BinaryPart'
export type { Literal } from '../wasm-lib/kcl/bindings/Literal'
export type { LiteralValue } from '../wasm-lib/kcl/bindings/LiteralValue'
export type { ArrayExpression } from '../wasm-lib/kcl/bindings/ArrayExpression'
export type SyntaxType =
@ -81,6 +82,7 @@ export type SyntaxType =
| 'PipeExpression'
| 'PipeSubstitution'
| 'Literal'
| 'LiteralValue'
| 'NonCodeNode'
| 'UnaryExpression'

View File

@ -9,8 +9,16 @@ import {
createUnaryExpression,
} from 'lang/modifyAst'
import { ArrayExpression, CallExpression, PipeExpression } from 'lang/wasm'
import { roundOff } from 'lib/utils'
import {
isCallExpression,
isArrayExpression,
isLiteral,
isBinaryExpression,
} from 'lang/util'
/**
* It does not create the startSketchOn and it does not create the startProfileAt.
* Returns AST expressions for this KCL code:
* const yo = startSketchOn('XY')
* |> startProfileAt([0, 0], %)
@ -92,3 +100,69 @@ export function updateRectangleSketch(
createLiteral(Math.abs(y)), // This will be the height of the rectangle
])
}
/**
* Mutates the pipeExpression to update the center rectangle sketch
* @param pipeExpression
* @param x
* @param y
* @param tag
*/
export function updateCenterRectangleSketch(
pipeExpression: PipeExpression,
deltaX: number,
deltaY: number,
tag: string,
originX: number,
originY: number
) {
let startX = originX - Math.abs(deltaX)
let startY = originY - Math.abs(deltaY)
// pipeExpression.body[1] is startProfileAt
let callExpression = pipeExpression.body[1]
if (isCallExpression(callExpression)) {
const arrayExpression = callExpression.arguments[0]
if (isArrayExpression(arrayExpression)) {
callExpression.arguments[0] = createArrayExpression([
createLiteral(roundOff(startX)),
createLiteral(roundOff(startY)),
])
}
}
const twoX = deltaX * 2
const twoY = deltaY * 2
callExpression = pipeExpression.body[2]
if (isCallExpression(callExpression)) {
const arrayExpression = callExpression.arguments[0]
if (isArrayExpression(arrayExpression)) {
const literal = arrayExpression.elements[0]
if (isLiteral(literal)) {
callExpression.arguments[0] = createArrayExpression([
createLiteral(literal.value),
createLiteral(Math.abs(twoX)),
])
}
}
}
callExpression = pipeExpression.body[3]
if (isCallExpression(callExpression)) {
const arrayExpression = callExpression.arguments[0]
if (isArrayExpression(arrayExpression)) {
const binaryExpression = arrayExpression.elements[0]
if (isBinaryExpression(binaryExpression)) {
callExpression.arguments[0] = createArrayExpression([
createBinaryExpression([
createCallExpressionStdLib('segAng', [createIdentifier(tag)]),
binaryExpression.operator,
createLiteral(90),
]), // 90 offset from the previous line
createLiteral(Math.abs(twoY)), // This will be the height of the rectangle
])
}
}
}
}

View File

@ -407,8 +407,9 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
status: 'available',
title: 'Center circle',
disabled: (state) =>
!canRectangleOrCircleTool(state.context) &&
!state.matches({ Sketch: 'Circle tool' }),
state.matches('Sketch no face') ||
(!canRectangleOrCircleTool(state.context) &&
!state.matches({ Sketch: 'Circle tool' })),
isActive: (state) => state.matches({ Sketch: 'Circle tool' }),
hotkey: (state) =>
state.matches({ Sketch: 'Circle tool' }) ? ['Esc', 'C'] : 'C',
@ -448,8 +449,9 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
icon: 'rectangle',
status: 'available',
disabled: (state) =>
!canRectangleOrCircleTool(state.context) &&
!state.matches({ Sketch: 'Rectangle tool' }),
state.matches('Sketch no face') ||
(!canRectangleOrCircleTool(state.context) &&
!state.matches({ Sketch: 'Rectangle tool' })),
title: 'Corner rectangle',
hotkey: (state) =>
state.matches({ Sketch: 'Rectangle tool' }) ? ['Esc', 'R'] : 'R',
@ -459,13 +461,33 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
},
{
id: 'center-rectangle',
onClick: () => console.error('Center rectangle not yet implemented'),
icon: 'rectangle',
status: 'unavailable',
onClick: ({ modelingState, modelingSend }) =>
modelingSend({
type: 'change tool',
data: {
tool: !modelingState.matches({
Sketch: 'Center Rectangle tool',
})
? 'center rectangle'
: 'none',
},
}),
icon: 'arc',
status: 'available',
disabled: (state) =>
state.matches('Sketch no face') ||
(!canRectangleOrCircleTool(state.context) &&
!state.matches({ Sketch: 'Center Rectangle tool' })),
title: 'Center rectangle',
showTitle: false,
hotkey: (state) =>
state.matches({ Sketch: 'Center Rectangle tool' })
? ['Esc', 'C']
: 'C',
description: 'Start drawing a rectangle from its center',
links: [],
isActive: (state) => {
return state.matches({ Sketch: 'Center Rectangle tool' })
},
},
],
{

View File

@ -184,6 +184,7 @@ export type SketchTool =
| 'line'
| 'tangentialArc'
| 'rectangle'
| 'center rectangle'
| 'circle'
| 'none'
@ -238,6 +239,10 @@ export type ModelingMachineEvent =
type: 'Add rectangle origin'
data: [x: number, y: number]
}
| {
type: 'Add center rectangle origin'
data: [x: number, y: number]
}
| {
type: 'Add circle origin'
data: [x: number, y: number]
@ -278,6 +283,7 @@ export type ModelingMachineEvent =
}
}
| { type: 'Finish rectangle' }
| { type: 'Finish center rectangle' }
| { type: 'Finish circle' }
| { type: 'Artifact graph populated' }
| { type: 'Artifact graph emptied' }
@ -506,6 +512,9 @@ export const modelingMachine = setup({
'next is rectangle': ({ context: { sketchDetails, currentTool } }) =>
currentTool === 'rectangle' &&
canRectangleOrCircleTool({ sketchDetails }),
'next is center rectangle': ({ context: { sketchDetails, currentTool } }) =>
currentTool === 'center rectangle' &&
canRectangleOrCircleTool({ sketchDetails }),
'next is circle': ({ context: { sketchDetails, currentTool } }) =>
currentTool === 'circle' && canRectangleOrCircleTool({ sketchDetails }),
'next is line': ({ context }) => context.currentTool === 'line',
@ -806,6 +815,26 @@ export const modelingMachine = setup({
},
})
},
'listen for center rectangle origin': ({ context: { sketchDetails } }) => {
if (!sketchDetails) return
// setupNoPointsListener has the code for startProfileAt onClick
sceneEntitiesManager.setupNoPointsListener({
sketchDetails,
afterClick: (args) => {
const twoD = args.intersectionPoint?.twoD
if (twoD) {
sceneInfra.modelingSend({
type: 'Add center rectangle origin',
data: [twoD.x, twoD.y],
})
} else {
console.error('No intersection point found')
}
},
})
},
'listen for circle origin': ({ context: { sketchDetails } }) => {
if (!sketchDetails) return
sceneEntitiesManager.createIntersectionPlane()
@ -859,6 +888,21 @@ export const modelingMachine = setup({
return codeManager.updateEditorWithAstAndWriteToFile(kclManager.ast)
})
},
'set up draft center rectangle': ({
context: { sketchDetails },
event,
}) => {
if (event.type !== 'Add center rectangle origin') return
if (!sketchDetails || !event.data) return
// eslint-disable-next-line @typescript-eslint/no-floating-promises
sceneEntitiesManager.setupDraftCenterRectangle(
sketchDetails.sketchPathToNode,
sketchDetails.zAxis,
sketchDetails.yAxis,
sketchDetails.origin,
event.data
)
},
'set up draft circle': ({ context: { sketchDetails }, event }) => {
if (event.type !== 'Add circle origin') return
if (!sketchDetails || !event.data) return
@ -1822,6 +1866,40 @@ export const modelingMachine = setup({
},
},
'Center Rectangle tool': {
entry: ['listen for center rectangle origin'],
states: {
'Awaiting corner': {
on: {
'Finish center rectangle': 'Finished Center Rectangle',
},
},
'Awaiting origin': {
on: {
'Add center rectangle origin': {
target: 'Awaiting corner',
// TODO
actions: 'set up draft center rectangle',
},
},
},
'Finished Center Rectangle': {
always: '#Modeling.Sketch.SketchIdle',
},
},
initial: 'Awaiting origin',
on: {
'change tool': {
target: 'Change Tool',
},
},
},
'clean slate': {
always: 'SketchIdle',
},
@ -2015,6 +2093,10 @@ export const modelingMachine = setup({
target: 'Circle tool',
guard: 'next is circle',
},
{
target: 'Center Rectangle tool',
guard: 'next is center rectangle',
},
],
entry: 'assign tool in context',