Compare commits
1 Commits
kurt-scale
...
jtran/plus
Author | SHA1 | Date | |
---|---|---|---|
4de0b57ea4 |
@ -1105,270 +1105,6 @@ part002 = startSketchOn(XZ)
|
|||||||
// checking the count of the overlays is a good proxy check that the client sketch scene is in a good state
|
// checking the count of the overlays is a good proxy check that the client sketch scene is in a good state
|
||||||
await expect(page.getByTestId('segment-overlay')).toHaveCount(3)
|
await expect(page.getByTestId('segment-overlay')).toHaveCount(3)
|
||||||
})
|
})
|
||||||
|
|
||||||
test.describe('Sketch scaling on first constraint', () => {
|
|
||||||
test('Should scale entire sketch when constraining first dimension with scale checkbox enabled', async ({
|
|
||||||
page,
|
|
||||||
homePage,
|
|
||||||
scene,
|
|
||||||
cmdBar,
|
|
||||||
}) => {
|
|
||||||
await page.addInitScript(async () => {
|
|
||||||
localStorage.setItem(
|
|
||||||
'persistCode',
|
|
||||||
`sketch001 = startSketchOn(XZ)
|
|
||||||
profile001 = startProfile(sketch001, at = [100, 100])
|
|
||||||
|> line(end = [200, 0])
|
|
||||||
|> line(end = [0, 200])
|
|
||||||
|> line(end = [-200, 0])
|
|
||||||
|> close()
|
|
||||||
profile002 = startProfile(sketch001, at = [400, 400])
|
|
||||||
|> circle(center = [0, 0], radius = 50)`
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
const u = await getUtils(page)
|
|
||||||
await page.setBodyDimensions({ width: 1200, height: 500 })
|
|
||||||
|
|
||||||
await homePage.goToModelingScene()
|
|
||||||
await scene.settled(cmdBar)
|
|
||||||
|
|
||||||
// Click on the first line segment to select it
|
|
||||||
await page.getByText('line(end = [200, 0])').click()
|
|
||||||
await page.getByRole('button', { name: 'Edit Sketch' }).click()
|
|
||||||
|
|
||||||
// Wait for overlays to populate
|
|
||||||
await page.waitForTimeout(1000)
|
|
||||||
|
|
||||||
// Click on the first line segment in the sketch
|
|
||||||
const line1 = await u.getSegmentBodyCoords(`[data-overlay-index="1"]`)
|
|
||||||
await page.mouse.click(line1.x, line1.y)
|
|
||||||
await page.waitForTimeout(100)
|
|
||||||
|
|
||||||
// Open constraints menu and click length constraint
|
|
||||||
await page
|
|
||||||
.getByRole('button', {
|
|
||||||
name: 'constraints: open menu',
|
|
||||||
})
|
|
||||||
.click()
|
|
||||||
await page.getByTestId('constraint-length').click()
|
|
||||||
|
|
||||||
// Verify the scale sketch checkbox is present and enabled
|
|
||||||
const scaleCheckbox = page.getByTestId('scale-sketch-checkbox')
|
|
||||||
await expect(scaleCheckbox).toBeVisible()
|
|
||||||
await expect(scaleCheckbox).toBeEnabled()
|
|
||||||
await expect(scaleCheckbox).toBeChecked() // Should be checked by default since no constraints exist
|
|
||||||
|
|
||||||
// Enter new value (100, which is half of original 200)
|
|
||||||
await page
|
|
||||||
.getByTestId('cmd-bar-arg-value')
|
|
||||||
.getByRole('textbox')
|
|
||||||
.fill('100')
|
|
||||||
|
|
||||||
// Ensure scale checkbox is still checked
|
|
||||||
await expect(scaleCheckbox).toBeChecked()
|
|
||||||
|
|
||||||
// Submit the constraint
|
|
||||||
await page
|
|
||||||
.getByRole('button', {
|
|
||||||
name: 'arrow right Continue',
|
|
||||||
})
|
|
||||||
.click()
|
|
||||||
|
|
||||||
// Wait for the changes to be applied
|
|
||||||
await page.waitForTimeout(1000)
|
|
||||||
|
|
||||||
// Verify the constraint was applied with a variable
|
|
||||||
await expect(page.locator('.cm-content')).toContainText('length001 = 100')
|
|
||||||
await expect(page.locator('.cm-content')).toContainText(
|
|
||||||
'line(end = [length001, 0])'
|
|
||||||
)
|
|
||||||
|
|
||||||
// Verify scaling occurred - all dimensions should be scaled by 0.5 (100/200)
|
|
||||||
// Original: line(end = [0, 200]) -> Scaled: line(end = [0, 100])
|
|
||||||
await expect(page.locator('.cm-content')).toContainText(
|
|
||||||
'line(end = [0, 100])'
|
|
||||||
)
|
|
||||||
// Original: line(end = [-200, 0]) -> Scaled: line(end = [-100, 0])
|
|
||||||
await expect(page.locator('.cm-content')).toContainText(
|
|
||||||
'line(end = [-100, 0])'
|
|
||||||
)
|
|
||||||
|
|
||||||
// Original: startProfile(at = [100, 100]) -> Scaled: startProfile(at = [50, 50])
|
|
||||||
await expect(page.locator('.cm-content')).toContainText(
|
|
||||||
'startProfile(sketch001, at = [50, 50])'
|
|
||||||
)
|
|
||||||
// Original: startProfile(at = [400, 400]) -> Scaled: startProfile(at = [200, 200])
|
|
||||||
await expect(page.locator('.cm-content')).toContainText(
|
|
||||||
'startProfile(sketch001, at = [200, 200])'
|
|
||||||
)
|
|
||||||
|
|
||||||
// Original: radius = 50 -> Scaled: radius = 25
|
|
||||||
await expect(page.locator('.cm-content')).toContainText('radius = 25')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('Should not scale sketch when constraining with scale checkbox disabled', async ({
|
|
||||||
page,
|
|
||||||
homePage,
|
|
||||||
scene,
|
|
||||||
cmdBar,
|
|
||||||
}) => {
|
|
||||||
await page.addInitScript(async () => {
|
|
||||||
localStorage.setItem(
|
|
||||||
'persistCode',
|
|
||||||
`sketch001 = startSketchOn(XZ)
|
|
||||||
profile001 = startProfile(sketch001, at = [100, 100])
|
|
||||||
|> line(end = [200, 0])
|
|
||||||
|> line(end = [0, 200])
|
|
||||||
|> close()`
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
const u = await getUtils(page)
|
|
||||||
await page.setBodyDimensions({ width: 1200, height: 500 })
|
|
||||||
|
|
||||||
await homePage.goToModelingScene()
|
|
||||||
await scene.settled(cmdBar)
|
|
||||||
|
|
||||||
// Click on the first line segment to select it
|
|
||||||
await page.getByText('line(end = [200, 0])').click()
|
|
||||||
await page.getByRole('button', { name: 'Edit Sketch' }).click()
|
|
||||||
|
|
||||||
// Wait for overlays to populate
|
|
||||||
await page.waitForTimeout(1000)
|
|
||||||
|
|
||||||
// Click on the first line segment in the sketch
|
|
||||||
const line1 = await u.getSegmentBodyCoords(`[data-overlay-index="1"]`)
|
|
||||||
await page.mouse.click(line1.x, line1.y)
|
|
||||||
await page.waitForTimeout(100)
|
|
||||||
|
|
||||||
// Open constraints menu and click length constraint
|
|
||||||
await page
|
|
||||||
.getByRole('button', {
|
|
||||||
name: 'constraints: open menu',
|
|
||||||
})
|
|
||||||
.click()
|
|
||||||
await page.getByTestId('constraint-length').click()
|
|
||||||
|
|
||||||
// Verify the scale sketch checkbox is present and enabled
|
|
||||||
const scaleCheckbox = page.getByTestId('scale-sketch-checkbox')
|
|
||||||
await expect(scaleCheckbox).toBeVisible()
|
|
||||||
await expect(scaleCheckbox).toBeEnabled()
|
|
||||||
await expect(scaleCheckbox).toBeChecked()
|
|
||||||
|
|
||||||
// Uncheck the scale checkbox
|
|
||||||
await scaleCheckbox.click()
|
|
||||||
await expect(scaleCheckbox).not.toBeChecked()
|
|
||||||
|
|
||||||
// Enter new value (100, which is half of original 200)
|
|
||||||
await page
|
|
||||||
.getByTestId('cmd-bar-arg-value')
|
|
||||||
.getByRole('textbox')
|
|
||||||
.fill('100')
|
|
||||||
|
|
||||||
// Submit the constraint
|
|
||||||
await page
|
|
||||||
.getByRole('button', {
|
|
||||||
name: 'arrow right Continue',
|
|
||||||
})
|
|
||||||
.click()
|
|
||||||
|
|
||||||
// Wait for the changes to be applied
|
|
||||||
await page.waitForTimeout(1000)
|
|
||||||
|
|
||||||
// Verify the constraint was applied with a variable
|
|
||||||
await expect(page.locator('.cm-content')).toContainText('length001 = 100')
|
|
||||||
await expect(page.locator('.cm-content')).toContainText(
|
|
||||||
'line(end = [length001, 0])'
|
|
||||||
)
|
|
||||||
|
|
||||||
// Verify NO scaling occurred - other dimensions should remain unchanged
|
|
||||||
await expect(page.locator('.cm-content')).toContainText(
|
|
||||||
'line(end = [0, 200])'
|
|
||||||
) // Should remain 200, not 100
|
|
||||||
await expect(page.locator('.cm-content')).toContainText(
|
|
||||||
'startProfile(sketch001, at = [100, 100])'
|
|
||||||
) // Should remain [100, 100]
|
|
||||||
})
|
|
||||||
|
|
||||||
test('Should disable scale checkbox when sketch already has constraints', async ({
|
|
||||||
page,
|
|
||||||
homePage,
|
|
||||||
scene,
|
|
||||||
cmdBar,
|
|
||||||
}) => {
|
|
||||||
await page.addInitScript(async () => {
|
|
||||||
localStorage.setItem(
|
|
||||||
'persistCode',
|
|
||||||
`length_var = 150
|
|
||||||
sketch001 = startSketchOn(XZ)
|
|
||||||
profile001 = startProfile(sketch001, at = [100, 100])
|
|
||||||
|> line(end = [length_var, 0])
|
|
||||||
|> line(end = [0, 200])
|
|
||||||
|> close()`
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
const u = await getUtils(page)
|
|
||||||
await page.setBodyDimensions({ width: 1200, height: 500 })
|
|
||||||
|
|
||||||
await homePage.goToModelingScene()
|
|
||||||
await scene.settled(cmdBar)
|
|
||||||
|
|
||||||
// Click on the second line segment (the one without constraints)
|
|
||||||
await page.getByText('line(end = [0, 200])').click()
|
|
||||||
await page.getByRole('button', { name: 'Edit Sketch' }).click()
|
|
||||||
|
|
||||||
// Wait for overlays to populate
|
|
||||||
await page.waitForTimeout(1000)
|
|
||||||
|
|
||||||
// Click on the second line segment in the sketch
|
|
||||||
const line2 = await u.getSegmentBodyCoords(`[data-overlay-index="2"]`)
|
|
||||||
await page.mouse.click(line2.x, line2.y)
|
|
||||||
await page.waitForTimeout(100)
|
|
||||||
|
|
||||||
// Open constraints menu and click length constraint
|
|
||||||
await page
|
|
||||||
.getByRole('button', {
|
|
||||||
name: 'constraints: open menu',
|
|
||||||
})
|
|
||||||
.click()
|
|
||||||
await page.getByTestId('constraint-length').click()
|
|
||||||
|
|
||||||
// Verify the scale sketch checkbox is present but disabled/unchecked
|
|
||||||
// because the sketch already has constraints (length_var)
|
|
||||||
const scaleCheckbox = page.getByTestId('scale-sketch-checkbox')
|
|
||||||
await expect(scaleCheckbox).toBeVisible()
|
|
||||||
await expect(scaleCheckbox).not.toBeChecked() // Should be unchecked because constraints exist
|
|
||||||
|
|
||||||
// Enter new value
|
|
||||||
await page
|
|
||||||
.getByTestId('cmd-bar-arg-value')
|
|
||||||
.getByRole('textbox')
|
|
||||||
.fill('100')
|
|
||||||
|
|
||||||
// Submit the constraint
|
|
||||||
await page
|
|
||||||
.getByRole('button', {
|
|
||||||
name: 'arrow right Continue',
|
|
||||||
})
|
|
||||||
.click()
|
|
||||||
|
|
||||||
// Wait for the changes to be applied
|
|
||||||
await page.waitForTimeout(1000)
|
|
||||||
|
|
||||||
// Verify the constraint was applied
|
|
||||||
await expect(page.locator('.cm-content')).toContainText('length002 = 100')
|
|
||||||
|
|
||||||
// Verify existing constraint and values remain unchanged
|
|
||||||
await expect(page.locator('.cm-content')).toContainText(
|
|
||||||
'length_var = 150'
|
|
||||||
)
|
|
||||||
await expect(page.locator('.cm-content')).toContainText(
|
|
||||||
'line(end = [length_var, 0])'
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
test.describe('Electron constraint tests', () => {
|
test.describe('Electron constraint tests', () => {
|
||||||
test(
|
test(
|
||||||
|
@ -1086,6 +1086,32 @@ impl Node<MemberExpression> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn concat(left: &[KclValue], left_el_ty: &RuntimeType, right: &[KclValue], right_el_ty: &RuntimeType) -> KclValue {
|
||||||
|
if left.is_empty() {
|
||||||
|
return KclValue::HomArray {
|
||||||
|
value: right.to_vec(),
|
||||||
|
ty: right_el_ty.clone(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if right.is_empty() {
|
||||||
|
return KclValue::HomArray {
|
||||||
|
value: left.to_vec(),
|
||||||
|
ty: left_el_ty.clone(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
let mut new = left.to_vec();
|
||||||
|
new.extend_from_slice(right);
|
||||||
|
// Propagate the element type if we can.
|
||||||
|
let ty = if right_el_ty.subtype(left_el_ty) {
|
||||||
|
left_el_ty.clone()
|
||||||
|
} else if left_el_ty.subtype(right_el_ty) {
|
||||||
|
right_el_ty.clone()
|
||||||
|
} else {
|
||||||
|
RuntimeType::any()
|
||||||
|
};
|
||||||
|
KclValue::HomArray { value: new, ty }
|
||||||
|
}
|
||||||
|
|
||||||
impl Node<BinaryExpression> {
|
impl Node<BinaryExpression> {
|
||||||
#[async_recursion]
|
#[async_recursion]
|
||||||
pub async fn get_result(&self, exec_state: &mut ExecState, ctx: &ExecutorContext) -> Result<KclValue, KclError> {
|
pub async fn get_result(&self, exec_state: &mut ExecState, ctx: &ExecutorContext) -> Result<KclValue, KclError> {
|
||||||
@ -1104,6 +1130,50 @@ impl Node<BinaryExpression> {
|
|||||||
meta,
|
meta,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
// Array plus is concatenation.
|
||||||
|
match (&left_value, &right_value) {
|
||||||
|
(
|
||||||
|
KclValue::HomArray {
|
||||||
|
value: left,
|
||||||
|
ty: left_el_ty,
|
||||||
|
..
|
||||||
|
},
|
||||||
|
KclValue::HomArray {
|
||||||
|
value: right,
|
||||||
|
ty: right_el_ty,
|
||||||
|
..
|
||||||
|
},
|
||||||
|
) => {
|
||||||
|
return Ok(concat(left, left_el_ty, right, right_el_ty));
|
||||||
|
}
|
||||||
|
(
|
||||||
|
KclValue::HomArray {
|
||||||
|
value: left,
|
||||||
|
ty: left_el_ty,
|
||||||
|
..
|
||||||
|
},
|
||||||
|
_,
|
||||||
|
) => {
|
||||||
|
// Any single value can be coerced to an array.
|
||||||
|
let right = vec![right_value.clone()];
|
||||||
|
let right_el_ty = RuntimeType::any();
|
||||||
|
return Ok(concat(left, left_el_ty, &right, &right_el_ty));
|
||||||
|
}
|
||||||
|
(
|
||||||
|
_,
|
||||||
|
KclValue::HomArray {
|
||||||
|
value: right,
|
||||||
|
ty: right_el_ty,
|
||||||
|
..
|
||||||
|
},
|
||||||
|
) => {
|
||||||
|
// Any single value can be coerced to an array.
|
||||||
|
let left = vec![left_value.clone()];
|
||||||
|
let left_el_ty = RuntimeType::any();
|
||||||
|
return Ok(concat(&left, &left_el_ty, right, right_el_ty));
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Then check if we have solids.
|
// Then check if we have solids.
|
||||||
|
@ -246,7 +246,7 @@ impl RuntimeType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Subtype with no coercion, including refining numeric types.
|
// Subtype with no coercion, including refining numeric types.
|
||||||
fn subtype(&self, sup: &RuntimeType) -> bool {
|
pub(super) fn subtype(&self, sup: &RuntimeType) -> bool {
|
||||||
use RuntimeType::*;
|
use RuntimeType::*;
|
||||||
|
|
||||||
match (self, sup) {
|
match (self, sup) {
|
||||||
|
@ -14,7 +14,7 @@ description: Result of parsing add_arrays.kcl
|
|||||||
"commentStart": 0,
|
"commentStart": 0,
|
||||||
"end": 0,
|
"end": 0,
|
||||||
"moduleId": 0,
|
"moduleId": 0,
|
||||||
"name": "answer",
|
"name": "a",
|
||||||
"start": 0,
|
"start": 0,
|
||||||
"type": "Identifier"
|
"type": "Identifier"
|
||||||
},
|
},
|
||||||
@ -96,6 +96,170 @@ description: Result of parsing add_arrays.kcl
|
|||||||
"start": 0,
|
"start": 0,
|
||||||
"type": "VariableDeclaration",
|
"type": "VariableDeclaration",
|
||||||
"type": "VariableDeclaration"
|
"type": "VariableDeclaration"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"commentStart": 0,
|
||||||
|
"declaration": {
|
||||||
|
"commentStart": 0,
|
||||||
|
"end": 0,
|
||||||
|
"id": {
|
||||||
|
"commentStart": 0,
|
||||||
|
"end": 0,
|
||||||
|
"moduleId": 0,
|
||||||
|
"name": "b",
|
||||||
|
"start": 0,
|
||||||
|
"type": "Identifier"
|
||||||
|
},
|
||||||
|
"init": {
|
||||||
|
"commentStart": 0,
|
||||||
|
"end": 0,
|
||||||
|
"left": {
|
||||||
|
"commentStart": 0,
|
||||||
|
"elements": [
|
||||||
|
{
|
||||||
|
"commentStart": 0,
|
||||||
|
"end": 0,
|
||||||
|
"moduleId": 0,
|
||||||
|
"raw": "0",
|
||||||
|
"start": 0,
|
||||||
|
"type": "Literal",
|
||||||
|
"type": "Literal",
|
||||||
|
"value": {
|
||||||
|
"value": 0.0,
|
||||||
|
"suffix": "None"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"commentStart": 0,
|
||||||
|
"end": 0,
|
||||||
|
"moduleId": 0,
|
||||||
|
"raw": "1",
|
||||||
|
"start": 0,
|
||||||
|
"type": "Literal",
|
||||||
|
"type": "Literal",
|
||||||
|
"value": {
|
||||||
|
"value": 1.0,
|
||||||
|
"suffix": "None"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"end": 0,
|
||||||
|
"moduleId": 0,
|
||||||
|
"start": 0,
|
||||||
|
"type": "ArrayExpression",
|
||||||
|
"type": "ArrayExpression"
|
||||||
|
},
|
||||||
|
"moduleId": 0,
|
||||||
|
"operator": "+",
|
||||||
|
"right": {
|
||||||
|
"commentStart": 0,
|
||||||
|
"end": 0,
|
||||||
|
"moduleId": 0,
|
||||||
|
"raw": "2",
|
||||||
|
"start": 0,
|
||||||
|
"type": "Literal",
|
||||||
|
"type": "Literal",
|
||||||
|
"value": {
|
||||||
|
"value": 2.0,
|
||||||
|
"suffix": "None"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"start": 0,
|
||||||
|
"type": "BinaryExpression",
|
||||||
|
"type": "BinaryExpression"
|
||||||
|
},
|
||||||
|
"moduleId": 0,
|
||||||
|
"start": 0,
|
||||||
|
"type": "VariableDeclarator"
|
||||||
|
},
|
||||||
|
"end": 0,
|
||||||
|
"kind": "const",
|
||||||
|
"moduleId": 0,
|
||||||
|
"start": 0,
|
||||||
|
"type": "VariableDeclaration",
|
||||||
|
"type": "VariableDeclaration"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"commentStart": 0,
|
||||||
|
"declaration": {
|
||||||
|
"commentStart": 0,
|
||||||
|
"end": 0,
|
||||||
|
"id": {
|
||||||
|
"commentStart": 0,
|
||||||
|
"end": 0,
|
||||||
|
"moduleId": 0,
|
||||||
|
"name": "c",
|
||||||
|
"start": 0,
|
||||||
|
"type": "Identifier"
|
||||||
|
},
|
||||||
|
"init": {
|
||||||
|
"commentStart": 0,
|
||||||
|
"end": 0,
|
||||||
|
"left": {
|
||||||
|
"commentStart": 0,
|
||||||
|
"end": 0,
|
||||||
|
"moduleId": 0,
|
||||||
|
"raw": "0",
|
||||||
|
"start": 0,
|
||||||
|
"type": "Literal",
|
||||||
|
"type": "Literal",
|
||||||
|
"value": {
|
||||||
|
"value": 0.0,
|
||||||
|
"suffix": "None"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"moduleId": 0,
|
||||||
|
"operator": "+",
|
||||||
|
"right": {
|
||||||
|
"commentStart": 0,
|
||||||
|
"elements": [
|
||||||
|
{
|
||||||
|
"commentStart": 0,
|
||||||
|
"end": 0,
|
||||||
|
"moduleId": 0,
|
||||||
|
"raw": "1",
|
||||||
|
"start": 0,
|
||||||
|
"type": "Literal",
|
||||||
|
"type": "Literal",
|
||||||
|
"value": {
|
||||||
|
"value": 1.0,
|
||||||
|
"suffix": "None"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"commentStart": 0,
|
||||||
|
"end": 0,
|
||||||
|
"moduleId": 0,
|
||||||
|
"raw": "2",
|
||||||
|
"start": 0,
|
||||||
|
"type": "Literal",
|
||||||
|
"type": "Literal",
|
||||||
|
"value": {
|
||||||
|
"value": 2.0,
|
||||||
|
"suffix": "None"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"end": 0,
|
||||||
|
"moduleId": 0,
|
||||||
|
"start": 0,
|
||||||
|
"type": "ArrayExpression",
|
||||||
|
"type": "ArrayExpression"
|
||||||
|
},
|
||||||
|
"start": 0,
|
||||||
|
"type": "BinaryExpression",
|
||||||
|
"type": "BinaryExpression"
|
||||||
|
},
|
||||||
|
"moduleId": 0,
|
||||||
|
"start": 0,
|
||||||
|
"type": "VariableDeclarator"
|
||||||
|
},
|
||||||
|
"end": 0,
|
||||||
|
"kind": "const",
|
||||||
|
"moduleId": 0,
|
||||||
|
"start": 0,
|
||||||
|
"type": "VariableDeclaration",
|
||||||
|
"type": "VariableDeclaration"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"commentStart": 0,
|
"commentStart": 0,
|
||||||
|
@ -1,12 +0,0 @@
|
|||||||
---
|
|
||||||
source: kcl-lib/src/simulation_tests.rs
|
|
||||||
description: Error from executing add_arrays.kcl
|
|
||||||
---
|
|
||||||
KCL Semantic error
|
|
||||||
|
|
||||||
× semantic: Expected a number, but found an array of `number`, `number`
|
|
||||||
╭────
|
|
||||||
1 │ answer = [0, 1] + [2]
|
|
||||||
· ───┬──
|
|
||||||
· ╰── tests/add_arrays/input.kcl
|
|
||||||
╰────
|
|
@ -1 +1,3 @@
|
|||||||
answer = [0, 1] + [2]
|
a = [0, 1] + [2]
|
||||||
|
b = [0, 1] + 2
|
||||||
|
c = 0 + [1, 2]
|
||||||
|
138
rust/kcl-lib/tests/add_arrays/program_memory.snap
Normal file
138
rust/kcl-lib/tests/add_arrays/program_memory.snap
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
---
|
||||||
|
source: kcl-lib/src/simulation_tests.rs
|
||||||
|
description: Variables in memory after executing add_arrays.kcl
|
||||||
|
---
|
||||||
|
{
|
||||||
|
"a": {
|
||||||
|
"type": "HomArray",
|
||||||
|
"value": [
|
||||||
|
{
|
||||||
|
"type": "Number",
|
||||||
|
"value": 0.0,
|
||||||
|
"ty": {
|
||||||
|
"type": "Default",
|
||||||
|
"len": {
|
||||||
|
"type": "Mm"
|
||||||
|
},
|
||||||
|
"angle": {
|
||||||
|
"type": "Degrees"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Number",
|
||||||
|
"value": 1.0,
|
||||||
|
"ty": {
|
||||||
|
"type": "Default",
|
||||||
|
"len": {
|
||||||
|
"type": "Mm"
|
||||||
|
},
|
||||||
|
"angle": {
|
||||||
|
"type": "Degrees"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Number",
|
||||||
|
"value": 2.0,
|
||||||
|
"ty": {
|
||||||
|
"type": "Default",
|
||||||
|
"len": {
|
||||||
|
"type": "Mm"
|
||||||
|
},
|
||||||
|
"angle": {
|
||||||
|
"type": "Degrees"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"b": {
|
||||||
|
"type": "HomArray",
|
||||||
|
"value": [
|
||||||
|
{
|
||||||
|
"type": "Number",
|
||||||
|
"value": 0.0,
|
||||||
|
"ty": {
|
||||||
|
"type": "Default",
|
||||||
|
"len": {
|
||||||
|
"type": "Mm"
|
||||||
|
},
|
||||||
|
"angle": {
|
||||||
|
"type": "Degrees"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Number",
|
||||||
|
"value": 1.0,
|
||||||
|
"ty": {
|
||||||
|
"type": "Default",
|
||||||
|
"len": {
|
||||||
|
"type": "Mm"
|
||||||
|
},
|
||||||
|
"angle": {
|
||||||
|
"type": "Degrees"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Number",
|
||||||
|
"value": 2.0,
|
||||||
|
"ty": {
|
||||||
|
"type": "Default",
|
||||||
|
"len": {
|
||||||
|
"type": "Mm"
|
||||||
|
},
|
||||||
|
"angle": {
|
||||||
|
"type": "Degrees"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"c": {
|
||||||
|
"type": "HomArray",
|
||||||
|
"value": [
|
||||||
|
{
|
||||||
|
"type": "Number",
|
||||||
|
"value": 0.0,
|
||||||
|
"ty": {
|
||||||
|
"type": "Default",
|
||||||
|
"len": {
|
||||||
|
"type": "Mm"
|
||||||
|
},
|
||||||
|
"angle": {
|
||||||
|
"type": "Degrees"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Number",
|
||||||
|
"value": 1.0,
|
||||||
|
"ty": {
|
||||||
|
"type": "Default",
|
||||||
|
"len": {
|
||||||
|
"type": "Mm"
|
||||||
|
},
|
||||||
|
"angle": {
|
||||||
|
"type": "Degrees"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Number",
|
||||||
|
"value": 2.0,
|
||||||
|
"ty": {
|
||||||
|
"type": "Default",
|
||||||
|
"len": {
|
||||||
|
"type": "Mm"
|
||||||
|
},
|
||||||
|
"angle": {
|
||||||
|
"type": "Degrees"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
@ -2,4 +2,6 @@
|
|||||||
source: kcl-lib/src/simulation_tests.rs
|
source: kcl-lib/src/simulation_tests.rs
|
||||||
description: Result of unparsing add_arrays.kcl
|
description: Result of unparsing add_arrays.kcl
|
||||||
---
|
---
|
||||||
answer = [0, 1] + [2]
|
a = [0, 1] + [2]
|
||||||
|
b = [0, 1] + 2
|
||||||
|
c = 0 + [1, 2]
|
||||||
|
@ -1026,7 +1026,7 @@ export class SceneEntities {
|
|||||||
const sketch = sketchFromPathToNode({
|
const sketch = sketchFromPathToNode({
|
||||||
pathToNode: sketchEntryNodePath,
|
pathToNode: sketchEntryNodePath,
|
||||||
variables: this.kclManager.variables,
|
variables: this.kclManager.variables,
|
||||||
ast: this.kclManager.ast,
|
kclManager: this.kclManager,
|
||||||
})
|
})
|
||||||
if (err(sketch)) return Promise.reject(sketch)
|
if (err(sketch)) return Promise.reject(sketch)
|
||||||
if (!sketch) return Promise.reject(new Error('No sketch found'))
|
if (!sketch) return Promise.reject(new Error('No sketch found'))
|
||||||
@ -1171,64 +1171,12 @@ export class SceneEntities {
|
|||||||
codeManager: this.codeManager,
|
codeManager: this.codeManager,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Check if we need to update sketchNodePaths due to CallExpression -> PipeExpression transformation
|
|
||||||
let updatedSketchNodePaths = [...sketchNodePaths]
|
|
||||||
let updatedSketchEntryNodePath = sketchEntryNodePath
|
|
||||||
let needsSelectionUpdate = false
|
|
||||||
|
|
||||||
// Check if the sketch entry was converted from CallExpression to PipeExpression
|
|
||||||
const _nodeAfter = getNodeFromPath<VariableDeclaration>(
|
|
||||||
modifiedAst,
|
|
||||||
sketchEntryNodePath,
|
|
||||||
'VariableDeclaration'
|
|
||||||
)
|
|
||||||
if (!err(_nodeAfter)) {
|
|
||||||
const initAfter = _nodeAfter.node?.declaration?.init
|
|
||||||
if (initAfter?.type === 'PipeExpression') {
|
|
||||||
const INIT_PATH_STEP_INDEX = 3
|
|
||||||
const pathNeedingUpdatingIndex = sketchNodePaths.findIndex(
|
|
||||||
(pathToNode) =>
|
|
||||||
pathToNode.length === INIT_PATH_STEP_INDEX + 1 &&
|
|
||||||
pathToNode.every(
|
|
||||||
(step, index) =>
|
|
||||||
step?.[0] === sketchEntryNodePath?.[index]?.[0]
|
|
||||||
)
|
|
||||||
)
|
|
||||||
if (pathNeedingUpdatingIndex !== -1) {
|
|
||||||
const updatedPath: PathToNode = [
|
|
||||||
...sketchNodePaths[pathNeedingUpdatingIndex],
|
|
||||||
['body', 'PipeExpression'],
|
|
||||||
[0, 'CallExpressionKw'],
|
|
||||||
]
|
|
||||||
updatedSketchNodePaths[pathNeedingUpdatingIndex] = updatedPath
|
|
||||||
updatedSketchEntryNodePath = updatedPath
|
|
||||||
needsSelectionUpdate = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (intersectsProfileStart) {
|
if (intersectsProfileStart) {
|
||||||
this.sceneInfra.modelingSend({ type: 'Close sketch' })
|
this.sceneInfra.modelingSend({ type: 'Close sketch' })
|
||||||
} else {
|
} else {
|
||||||
// Send selection update if paths were modified
|
|
||||||
if (needsSelectionUpdate) {
|
|
||||||
this.sceneInfra.modelingSend({
|
|
||||||
type: 'Set selection',
|
|
||||||
data: {
|
|
||||||
selectionType: 'completeSelection',
|
|
||||||
selection: {
|
|
||||||
graphSelections: [],
|
|
||||||
otherSelections: [],
|
|
||||||
},
|
|
||||||
updatedSketchEntryNodePath,
|
|
||||||
updatedSketchNodePaths,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.setupDraftSegment(
|
await this.setupDraftSegment(
|
||||||
sketchEntryNodePath,
|
sketchEntryNodePath,
|
||||||
updatedSketchNodePaths,
|
sketchNodePaths,
|
||||||
planeNodePath,
|
planeNodePath,
|
||||||
forward,
|
forward,
|
||||||
up,
|
up,
|
||||||
@ -2582,7 +2530,7 @@ export class SceneEntities {
|
|||||||
const sketch = sketchFromPathToNode({
|
const sketch = sketchFromPathToNode({
|
||||||
pathToNode,
|
pathToNode,
|
||||||
variables: this.kclManager.variables,
|
variables: this.kclManager.variables,
|
||||||
ast: this.kclManager.ast,
|
kclManager: this.kclManager,
|
||||||
})
|
})
|
||||||
if (trap(sketch)) return
|
if (trap(sketch)) return
|
||||||
if (!sketch) {
|
if (!sketch) {
|
||||||
@ -3836,14 +3784,14 @@ function prepareTruncatedAst(
|
|||||||
function sketchFromPathToNode({
|
function sketchFromPathToNode({
|
||||||
pathToNode,
|
pathToNode,
|
||||||
variables,
|
variables,
|
||||||
ast,
|
kclManager,
|
||||||
}: {
|
}: {
|
||||||
pathToNode: PathToNode
|
pathToNode: PathToNode
|
||||||
variables: VariableMap
|
variables: VariableMap
|
||||||
ast: Node<Program>
|
kclManager: KclManager
|
||||||
}): Sketch | null | Error {
|
}): Sketch | null | Error {
|
||||||
const _varDec = getNodeFromPath<VariableDeclarator>(
|
const _varDec = getNodeFromPath<VariableDeclarator>(
|
||||||
ast,
|
kclManager.ast,
|
||||||
pathToNode,
|
pathToNode,
|
||||||
'VariableDeclarator'
|
'VariableDeclarator'
|
||||||
)
|
)
|
||||||
@ -3860,141 +3808,6 @@ function sketchFromPathToNode({
|
|||||||
return sg
|
return sg
|
||||||
}
|
}
|
||||||
|
|
||||||
export function scaleProfiles({
|
|
||||||
ast,
|
|
||||||
pathsToProfile,
|
|
||||||
factor,
|
|
||||||
variables,
|
|
||||||
}: {
|
|
||||||
ast: Node<Program>
|
|
||||||
pathsToProfile: PathToNode[]
|
|
||||||
factor: number
|
|
||||||
variables: VariableMap
|
|
||||||
}) {
|
|
||||||
let clonedAst = structuredClone(ast)
|
|
||||||
for (const pathToProfile of pathsToProfile) {
|
|
||||||
const profile = sketchFromPathToNode({
|
|
||||||
pathToNode: pathToProfile,
|
|
||||||
variables: variables,
|
|
||||||
ast,
|
|
||||||
})
|
|
||||||
if (err(profile)) return profile
|
|
||||||
if (!profile) return Error('Profile not found')
|
|
||||||
|
|
||||||
// Scale the startProfile 'at' parameter
|
|
||||||
const scaledStartAt: [number, number] = [
|
|
||||||
profile.start.from[0] * factor,
|
|
||||||
profile.start.from[1] * factor,
|
|
||||||
]
|
|
||||||
if (
|
|
||||||
profile.paths?.[0]?.type !== 'Circle' &&
|
|
||||||
profile.paths?.[0]?.type !== 'CircleThreePoint'
|
|
||||||
) {
|
|
||||||
const startProfileResult = changeSketchArguments(
|
|
||||||
clonedAst,
|
|
||||||
variables,
|
|
||||||
{
|
|
||||||
type: 'path',
|
|
||||||
pathToNode: pathToProfile,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'straight-segment',
|
|
||||||
from: [0, 0], // not used for startProfile
|
|
||||||
to: scaledStartAt,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
if (trap(startProfileResult)) return startProfileResult
|
|
||||||
clonedAst = startProfileResult.modifiedAst
|
|
||||||
}
|
|
||||||
|
|
||||||
// Scale the path segments
|
|
||||||
for (let pathIndex = 0; pathIndex < profile.paths.length; pathIndex++) {
|
|
||||||
const path = profile.paths[pathIndex]
|
|
||||||
let input: Parameters<typeof changeSketchArguments>[3] | undefined =
|
|
||||||
undefined
|
|
||||||
const pathToSegment = getNodePathFromSourceRange(
|
|
||||||
clonedAst,
|
|
||||||
sourceRangeFromRust(path.__geoMeta.sourceRange)
|
|
||||||
)
|
|
||||||
if (!pathToSegment) {
|
|
||||||
console.log(
|
|
||||||
'Could not find path for segment:',
|
|
||||||
path.type,
|
|
||||||
'sourceRange:',
|
|
||||||
path.__geoMeta.sourceRange
|
|
||||||
)
|
|
||||||
}
|
|
||||||
const scaleTuple = (tuple: [number, number]): [number, number] => [
|
|
||||||
tuple[0] * factor,
|
|
||||||
tuple[1] * factor,
|
|
||||||
]
|
|
||||||
const previous = profile.paths[pathIndex - 1]
|
|
||||||
if (
|
|
||||||
path.type === 'ToPoint' ||
|
|
||||||
path.type === 'TangentialArcTo' ||
|
|
||||||
path.type === 'TangentialArc'
|
|
||||||
) {
|
|
||||||
input = {
|
|
||||||
type: 'straight-segment',
|
|
||||||
from: scaleTuple(path.from),
|
|
||||||
to: scaleTuple(path.to),
|
|
||||||
previousEndTangent: previous
|
|
||||||
? findTangentDirectionPath(previous)
|
|
||||||
: undefined,
|
|
||||||
}
|
|
||||||
} else if (
|
|
||||||
path.type === 'ArcThreePoint' ||
|
|
||||||
path.type === 'CircleThreePoint'
|
|
||||||
) {
|
|
||||||
input = {
|
|
||||||
type: 'circle-three-point-segment',
|
|
||||||
p1: scaleTuple(path.p1),
|
|
||||||
p2: scaleTuple(path.p2),
|
|
||||||
p3: scaleTuple(path.p3),
|
|
||||||
}
|
|
||||||
} else if (path.type === 'Circle') {
|
|
||||||
input = {
|
|
||||||
type: 'arc-segment',
|
|
||||||
from: scaleTuple(path.from),
|
|
||||||
to: scaleTuple(path.to),
|
|
||||||
center: scaleTuple(path.center),
|
|
||||||
radius: path.radius * factor,
|
|
||||||
ccw: path.ccw,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Skip close calls - they don't have dimensions to scale
|
|
||||||
console.log('Unhandled path type:', path.type)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if (input && pathToSegment) {
|
|
||||||
try {
|
|
||||||
const changeSketchResult = changeSketchArguments(
|
|
||||||
clonedAst,
|
|
||||||
variables,
|
|
||||||
{
|
|
||||||
type: 'path',
|
|
||||||
pathToNode: pathToSegment,
|
|
||||||
},
|
|
||||||
input
|
|
||||||
)
|
|
||||||
if (!err(changeSketchResult)) {
|
|
||||||
clonedAst = changeSketchResult.modifiedAst
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log('Error scaling segment:', path.type, 'error:', error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const pResult = parse(recast(clonedAst))
|
|
||||||
if (trap(pResult) || !resultIsOk(pResult)) {
|
|
||||||
return Error('Unexpected compilation error')
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
modifiedAst: pResult.program,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function colorSegment(object: any, color: number) {
|
function colorSegment(object: any, color: number) {
|
||||||
const segmentHead = getParentGroup(object, [ARROWHEAD, PROFILE_START])
|
const segmentHead = getParentGroup(object, [ARROWHEAD, PROFILE_START])
|
||||||
if (segmentHead) {
|
if (segmentHead) {
|
||||||
@ -4024,7 +3837,7 @@ export function getSketchQuaternion(
|
|||||||
const sketch = sketchFromPathToNode({
|
const sketch = sketchFromPathToNode({
|
||||||
pathToNode: sketchPathToNode,
|
pathToNode: sketchPathToNode,
|
||||||
variables: kclManager.variables,
|
variables: kclManager.variables,
|
||||||
ast: kclManager.ast,
|
kclManager,
|
||||||
})
|
})
|
||||||
if (err(sketch)) return sketch
|
if (err(sketch)) return sketch
|
||||||
const zAxis =
|
const zAxis =
|
||||||
@ -4082,7 +3895,7 @@ function getSketchesInfo({
|
|||||||
const sketch = sketchFromPathToNode({
|
const sketch = sketchFromPathToNode({
|
||||||
pathToNode: path,
|
pathToNode: path,
|
||||||
variables,
|
variables,
|
||||||
ast: kclManager.ast,
|
kclManager,
|
||||||
})
|
})
|
||||||
if (err(sketch)) continue
|
if (err(sketch)) continue
|
||||||
if (!sketch) continue
|
if (!sketch) continue
|
||||||
@ -4161,44 +3974,3 @@ function findTangentDirection(segmentGroup: Group) {
|
|||||||
}
|
}
|
||||||
return tangentDirection
|
return tangentDirection
|
||||||
}
|
}
|
||||||
|
|
||||||
// implements the same as, but for a Path instead of segment Group
|
|
||||||
function findTangentDirectionPath(path: Path): Coords2d | undefined {
|
|
||||||
let tangentDirection: Coords2d | undefined
|
|
||||||
|
|
||||||
if (path.type === 'TangentialArcTo' || path.type === 'TangentialArc') {
|
|
||||||
// For tangential arcs with center, calculate tangent at the end point
|
|
||||||
const tangentAngle =
|
|
||||||
deg2Rad(getAngle(path.center, path.to)) +
|
|
||||||
(Math.PI / 2) * (path.ccw ? 1 : -1)
|
|
||||||
tangentDirection = [Math.cos(tangentAngle), Math.sin(tangentAngle)]
|
|
||||||
} else if (path.type === 'Arc') {
|
|
||||||
// For regular arcs, calculate tangent at the end point
|
|
||||||
const tangentAngle =
|
|
||||||
deg2Rad(getAngle(path.center, path.to)) +
|
|
||||||
(Math.PI / 2) * (path.ccw ? 1 : -1)
|
|
||||||
tangentDirection = [Math.cos(tangentAngle), Math.sin(tangentAngle)]
|
|
||||||
} else if (path.type === 'ArcThreePoint') {
|
|
||||||
// For three-point arcs, we need to calculate the center first
|
|
||||||
// This is more complex, so for now we'll skip tangent calculation
|
|
||||||
console.warn(
|
|
||||||
'ArcThreePoint tangent direction calculation not implemented yet'
|
|
||||||
)
|
|
||||||
} else if (path.type === 'ToPoint') {
|
|
||||||
// For straight lines, the tangent is the direction from start to end
|
|
||||||
const to = path.to as Coords2d
|
|
||||||
const from = path.from as Coords2d
|
|
||||||
tangentDirection = subVec(to, from)
|
|
||||||
const normalized = normalizeVec(tangentDirection)
|
|
||||||
if (normalized) {
|
|
||||||
tangentDirection = normalized
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.warn(
|
|
||||||
'Unsupported path type for tangent direction calculation: ',
|
|
||||||
path.type
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return tangentDirection
|
|
||||||
}
|
|
||||||
|
@ -31,8 +31,6 @@ import { roundOff, roundOffWithUnits } from '@src/lib/utils'
|
|||||||
import { varMentions } from '@src/lib/varCompletionExtension'
|
import { varMentions } from '@src/lib/varCompletionExtension'
|
||||||
import { useSettings } from '@src/lib/singletons'
|
import { useSettings } from '@src/lib/singletons'
|
||||||
import { commandBarActor, useCommandBarState } from '@src/lib/singletons'
|
import { commandBarActor, useCommandBarState } from '@src/lib/singletons'
|
||||||
import { doesProfileHaveAnyConstrainedDimension } from '@src/lang/queryAst'
|
|
||||||
import type { PathToNode } from '@src/lang/wasm'
|
|
||||||
|
|
||||||
import styles from './CommandBarKclInput.module.css'
|
import styles from './CommandBarKclInput.module.css'
|
||||||
|
|
||||||
@ -52,7 +50,6 @@ function CommandBarKclInput({
|
|||||||
stepBack: () => void
|
stepBack: () => void
|
||||||
onSubmit: (event: unknown) => void
|
onSubmit: (event: unknown) => void
|
||||||
}) {
|
}) {
|
||||||
// console.log('arg', arg)
|
|
||||||
const commandBarState = useCommandBarState()
|
const commandBarState = useCommandBarState()
|
||||||
const previouslySetValue = commandBarState.context.argumentsToSubmit[
|
const previouslySetValue = commandBarState.context.argumentsToSubmit[
|
||||||
arg.name
|
arg.name
|
||||||
@ -112,32 +109,6 @@ function CommandBarKclInput({
|
|||||||
arg.createVariable === 'force' ||
|
arg.createVariable === 'force' ||
|
||||||
false
|
false
|
||||||
)
|
)
|
||||||
|
|
||||||
// Check if this is the "Constrain with named value" command
|
|
||||||
const isConstrainWithNamedValueCommand =
|
|
||||||
commandBarState.context.selectedCommand?.name ===
|
|
||||||
'Constrain with named value'
|
|
||||||
const sketchDetails = argMachineContext?.sketchDetails
|
|
||||||
|
|
||||||
// Checkbox should be enabled (clickable) when it's the right command
|
|
||||||
const shouldEnableScaleCheckbox = isConstrainWithNamedValueCommand
|
|
||||||
|
|
||||||
// Checkbox should be checked by default when ALL profiles have no constrained dimensions
|
|
||||||
const shouldCheckScaleByDefault = useMemo(() => {
|
|
||||||
if (!isConstrainWithNamedValueCommand || !sketchDetails?.sketchNodePaths) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
// Check if ALL profiles have no constrained dimensions (meaning we can safely scale)
|
|
||||||
return sketchDetails.sketchNodePaths.every((pathToProfile: PathToNode) => {
|
|
||||||
const hasConstrainedDimension = doesProfileHaveAnyConstrainedDimension(
|
|
||||||
pathToProfile,
|
|
||||||
kclManager.ast
|
|
||||||
)
|
|
||||||
return !hasConstrainedDimension // We want profiles with NO constrained dimensions
|
|
||||||
})
|
|
||||||
}, [isConstrainWithNamedValueCommand, sketchDetails, kclManager.ast])
|
|
||||||
|
|
||||||
const [scaleSketch, setScaleSketch] = useState(shouldCheckScaleByDefault)
|
|
||||||
const [canSubmit, setCanSubmit] = useState(true)
|
const [canSubmit, setCanSubmit] = useState(true)
|
||||||
useHotkeys('mod + k, mod + /', () => commandBarActor.send({ type: 'Close' }))
|
useHotkeys('mod + k, mod + /', () => commandBarActor.send({ type: 'Close' }))
|
||||||
const editorRef = useRef<HTMLDivElement>(null)
|
const editorRef = useRef<HTMLDivElement>(null)
|
||||||
@ -254,23 +225,6 @@ function CommandBarKclInput({
|
|||||||
)
|
)
|
||||||
}, [calcResult, createNewVariable, isNewVariableNameUnique, isExecuting])
|
}, [calcResult, createNewVariable, isNewVariableNameUnique, isExecuting])
|
||||||
|
|
||||||
// Update scale checkbox when the condition changes
|
|
||||||
useEffect(() => {
|
|
||||||
setScaleSketch(shouldCheckScaleByDefault)
|
|
||||||
}, [shouldCheckScaleByDefault])
|
|
||||||
|
|
||||||
// Store scaleSketch value in command bar context for "Constrain with named value" command
|
|
||||||
useEffect(() => {
|
|
||||||
if (isConstrainWithNamedValueCommand) {
|
|
||||||
commandBarActor.send({
|
|
||||||
type: 'Set additional data',
|
|
||||||
data: {
|
|
||||||
scaleSketch,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}, [scaleSketch, isConstrainWithNamedValueCommand])
|
|
||||||
|
|
||||||
function handleSubmit(e?: React.FormEvent<HTMLFormElement>) {
|
function handleSubmit(e?: React.FormEvent<HTMLFormElement>) {
|
||||||
e?.preventDefault()
|
e?.preventDefault()
|
||||||
if (!canSubmit || valueNode === null) {
|
if (!canSubmit || valueNode === null) {
|
||||||
@ -283,7 +237,8 @@ function CommandBarKclInput({
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const commandValue = createNewVariable
|
onSubmit(
|
||||||
|
createNewVariable
|
||||||
? ({
|
? ({
|
||||||
valueAst: valueNode,
|
valueAst: valueNode,
|
||||||
valueText: value,
|
valueText: value,
|
||||||
@ -301,8 +256,7 @@ function CommandBarKclInput({
|
|||||||
valueText: value,
|
valueText: value,
|
||||||
valueCalculated: calcResult,
|
valueCalculated: calcResult,
|
||||||
} satisfies KclCommandValue)
|
} satisfies KclCommandValue)
|
||||||
|
)
|
||||||
onSubmit(commandValue)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -405,34 +359,6 @@ function CommandBarKclInput({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{isConstrainWithNamedValueCommand && (
|
|
||||||
<div className="flex items-baseline gap-4 mx-4">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
id="scale-sketch-checkbox"
|
|
||||||
data-testid="scale-sketch-checkbox"
|
|
||||||
checked={scaleSketch}
|
|
||||||
disabled={!shouldEnableScaleCheckbox}
|
|
||||||
onChange={(e) => {
|
|
||||||
setScaleSketch(e.target.checked)
|
|
||||||
}}
|
|
||||||
className="bg-chalkboard-10 dark:bg-chalkboard-80"
|
|
||||||
/>
|
|
||||||
<label
|
|
||||||
htmlFor="scale-sketch-checkbox"
|
|
||||||
className={`text-blue border-none bg-transparent font-sm flex gap-1 items-center pl-0 pr-1 ${
|
|
||||||
!shouldEnableScaleCheckbox ? 'opacity-50 cursor-not-allowed' : ''
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
Scale sketch
|
|
||||||
</label>
|
|
||||||
{!shouldEnableScaleCheckbox && (
|
|
||||||
<span className="text-sm text-chalkboard-60 dark:text-chalkboard-50">
|
|
||||||
(disabled - sketch has constrained dimensions)
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</form>
|
</form>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -18,7 +18,3 @@ export const ARG_INTERIOR_ABSOLUTE = 'interiorAbsolute'
|
|||||||
export const ARG_AT = 'at'
|
export const ARG_AT = 'at'
|
||||||
export const ARG_LEG = 'leg'
|
export const ARG_LEG = 'leg'
|
||||||
export const ARG_HYPOTENUSE = 'hypotenuse'
|
export const ARG_HYPOTENUSE = 'hypotenuse'
|
||||||
export const ARG_P1 = 'p1'
|
|
||||||
export const ARG_P2 = 'p2'
|
|
||||||
export const ARG_P3 = 'p3'
|
|
||||||
export const ARG_DIAMETER = 'diameter'
|
|
||||||
|
@ -25,13 +25,12 @@ import type { Artifact } from '@src/lang/std/artifactGraph'
|
|||||||
import { codeRefFromRange } from '@src/lang/std/artifactGraph'
|
import { codeRefFromRange } from '@src/lang/std/artifactGraph'
|
||||||
import { topLevelRange } from '@src/lang/util'
|
import { topLevelRange } from '@src/lang/util'
|
||||||
import type { Identifier, Literal, LiteralValue } from '@src/lang/wasm'
|
import type { Identifier, Literal, LiteralValue } from '@src/lang/wasm'
|
||||||
import { assertParse, recast, parse, resultIsOk } from '@src/lang/wasm'
|
import { assertParse, recast } from '@src/lang/wasm'
|
||||||
import { initPromise } from '@src/lang/wasmUtils'
|
import { initPromise } from '@src/lang/wasmUtils'
|
||||||
import { enginelessExecutor } from '@src/lib/testHelpers'
|
import { enginelessExecutor } from '@src/lib/testHelpers'
|
||||||
import { err } from '@src/lib/trap'
|
import { err } from '@src/lib/trap'
|
||||||
import { deleteFromSelection } from '@src/lang/modifyAst/deleteFromSelection'
|
import { deleteFromSelection } from '@src/lang/modifyAst/deleteFromSelection'
|
||||||
import { assertNotErr } from '@src/unitTestUtils'
|
import { assertNotErr } from '@src/unitTestUtils'
|
||||||
import { scaleProfiles } from '@src/clientSideScene/sceneEntities'
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await initPromise
|
await initPromise
|
||||||
@ -918,223 +917,3 @@ extrude001 = extrude(part001, length = 5)
|
|||||||
expect(result instanceof Error).toBe(true)
|
expect(result instanceof Error).toBe(true)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('testing sketch scaling', () => {
|
|
||||||
it('can scale sketch by half simple case', async () => {
|
|
||||||
const basicSketch = `sketch002 = startSketchOn(XZ)
|
|
||||||
profile006 = startProfile(sketch002, at = [114.64, 124.18])
|
|
||||||
|> line(end = [173.02, 306.77])
|
|
||||||
|> line(end = [175.14, -282.35])
|
|
||||||
|> tangentialArc(end = [-52.01, -194.25])`
|
|
||||||
const ast = assertParse(basicSketch)
|
|
||||||
const result = await enginelessExecutor(ast)
|
|
||||||
const searchSnippet = 'startProfile(sketch002, at = [114.64, 124.18])'
|
|
||||||
const startIndex = basicSketch.indexOf(searchSnippet)
|
|
||||||
const range = topLevelRange(startIndex, startIndex + searchSnippet.length)
|
|
||||||
const pathToProfile = getNodePathFromSourceRange(ast, range)
|
|
||||||
|
|
||||||
if (err(result)) throw result
|
|
||||||
const scaledProfile = scaleProfiles({
|
|
||||||
ast,
|
|
||||||
factor: 0.5,
|
|
||||||
variables: result.variables,
|
|
||||||
pathsToProfile: [pathToProfile],
|
|
||||||
})
|
|
||||||
if (err(scaledProfile)) throw scaledProfile
|
|
||||||
const modifiedAst = scaledProfile.modifiedAst
|
|
||||||
const newCode = recast(modifiedAst)
|
|
||||||
if (err(newCode)) throw newCode
|
|
||||||
|
|
||||||
expect(newCode).toBe(`sketch002 = startSketchOn(XZ)
|
|
||||||
profile006 = startProfile(sketch002, at = [57.32, 62.09])
|
|
||||||
|> line(end = [86.51, 153.39])
|
|
||||||
|> line(end = [87.57, -141.18])
|
|
||||||
|> tangentialArc(end = [-26, -97.12])
|
|
||||||
`)
|
|
||||||
})
|
|
||||||
it('can scale sketch more complex', async () => {
|
|
||||||
let code = `sketch001 = startSketchOn(YZ)
|
|
||||||
profile001 = startProfile(sketch001, at = [100, 101])
|
|
||||||
|> line(end = [102, 103])
|
|
||||||
|> line(endAbsolute = [104, 105])
|
|
||||||
|> angledLine(angle = 206, length = 106)
|
|
||||||
|> angledLine(angle = -208, lengthX = 107)
|
|
||||||
|> angledLine(angle = 210, lengthY = 108)
|
|
||||||
|> angledLine(angle = 212, endAbsoluteX = 109)
|
|
||||||
|> angledLine(angle = 214, endAbsoluteY = 110)
|
|
||||||
|> arc(interiorAbsolute = [111, 112], endAbsolute = [113, 114])
|
|
||||||
|> tangentialArc(end = [115, -116])
|
|
||||||
|> tangentialArc(endAbsolute = [117, 118])
|
|
||||||
|> tangentialArc(angle = 224, radius = 119)
|
|
||||||
|> tangentialArc(angle = 226, diameter = 120)
|
|
||||||
|
|
||||||
profile002 = startProfile(sketch001, at = [-121, 122])
|
|
||||||
|> angledLine(angle = 130, length = 123, tag = $rectangleSegmentA001)
|
|
||||||
|> angledLine(angle = segAng(rectangleSegmentA001) - 232, length = 124)
|
|
||||||
|> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001))
|
|
||||||
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|
|
||||||
|> close()
|
|
||||||
profile003 = circle(sketch001, center = [-125, -126], radius = 127)
|
|
||||||
profile004 = circleThreePoint(
|
|
||||||
sketch001,
|
|
||||||
p1 = [128, 129],
|
|
||||||
p2 = [130, 131],
|
|
||||||
p3 = [132, 133],
|
|
||||||
)
|
|
||||||
profile005 = circle(sketch001, center = [-134, -135], diameter = 136)`
|
|
||||||
let ast = assertParse(code)
|
|
||||||
const result = await enginelessExecutor(ast)
|
|
||||||
const searchSnippets = [
|
|
||||||
'startProfile(sketch001, at = [100, 101])',
|
|
||||||
'startProfile(sketch001, at = [-121, 122])',
|
|
||||||
'circle(sketch001, center = [-125, -126], radius = 127)',
|
|
||||||
'circleThreePoint(',
|
|
||||||
'circle(sketch001, center = [-134, -135], diameter = 136)',
|
|
||||||
]
|
|
||||||
const ranges = searchSnippets.map((searchSnippet) => {
|
|
||||||
const startIndex = code.indexOf(searchSnippet)
|
|
||||||
return topLevelRange(startIndex, startIndex + searchSnippet.length)
|
|
||||||
})
|
|
||||||
const pathsToProfiles = ranges.map((range) =>
|
|
||||||
getNodePathFromSourceRange(ast, range)
|
|
||||||
)
|
|
||||||
|
|
||||||
if (err(result)) throw result
|
|
||||||
const scaledProfile = scaleProfiles({
|
|
||||||
ast,
|
|
||||||
factor: 0.5,
|
|
||||||
variables: result.variables,
|
|
||||||
pathsToProfile: pathsToProfiles,
|
|
||||||
})
|
|
||||||
if (err(scaledProfile)) throw scaledProfile
|
|
||||||
const pResult = parse(recast(scaledProfile.modifiedAst))
|
|
||||||
if (err(pResult) || !resultIsOk(pResult)) return
|
|
||||||
ast = pResult.program
|
|
||||||
const newCode = recast(ast)
|
|
||||||
if (err(newCode)) throw newCode
|
|
||||||
|
|
||||||
expect(newCode).toBe(`sketch001 = startSketchOn(YZ)
|
|
||||||
profile001 = startProfile(sketch001, at = [50, 50.5])
|
|
||||||
|> line(end = [51, 51.5])
|
|
||||||
|> line(endAbsolute = [52, 52.5])
|
|
||||||
|> angledLine(angle = -154, length = 53)
|
|
||||||
|> angledLine(angle = 152, lengthX = 53.5)
|
|
||||||
|> angledLine(angle = -150, lengthY = 54)
|
|
||||||
|> angledLine(angle = 32, endAbsoluteX = 54.5)
|
|
||||||
|> angledLine(angle = -146, endAbsoluteY = 55)
|
|
||||||
|> arc(interiorAbsolute = [55.5, 56], endAbsolute = [56.5, 57])
|
|
||||||
|> tangentialArc(end = [57.5, -58])
|
|
||||||
|> tangentialArc(endAbsolute = [58.5, 59])
|
|
||||||
|> tangentialArc(angle = 224deg, radius = 59.5)
|
|
||||||
|> tangentialArc(angle = 226deg, diameter = 60)
|
|
||||||
|
|
||||||
profile002 = startProfile(sketch001, at = [-60.5, 61])
|
|
||||||
|> angledLine(angle = 130, length = 61.5, tag = $rectangleSegmentA001)
|
|
||||||
|> angledLine(angle = segAng(rectangleSegmentA001) - 232, length = 62)
|
|
||||||
|> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001))
|
|
||||||
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|
|
||||||
|> close()
|
|
||||||
profile003 = circle(sketch001, center = [-62.5, -63], radius = 63.5)
|
|
||||||
profile004 = circleThreePoint(
|
|
||||||
sketch001,
|
|
||||||
p1 = [64, 64.5],
|
|
||||||
p2 = [65, 65.5],
|
|
||||||
p3 = [66, 66.5],
|
|
||||||
)
|
|
||||||
profile005 = circle(sketch001, center = [-67, -67.5], diameter = 68)
|
|
||||||
`)
|
|
||||||
})
|
|
||||||
it("make sure it doesn't stomp constraints", async () => {
|
|
||||||
let code = `sketch001 = startSketchOn(YZ)
|
|
||||||
profile001 = startProfile(sketch001, at = [100 + 0, 101])
|
|
||||||
|> line(end = [102, 103 + 0])
|
|
||||||
|> line(endAbsolute = [104 + 0, 105])
|
|
||||||
|> angledLine(angle = 206, length = 106 + 0)
|
|
||||||
|> angledLine(angle = -208 + 0, lengthX = 107)
|
|
||||||
|> angledLine(angle = 210, lengthY = 108 + 0)
|
|
||||||
|> angledLine(angle = 212 + 0, endAbsoluteX = 109)
|
|
||||||
|> angledLine(angle = 214, endAbsoluteY = 110 + 0)
|
|
||||||
|> arc(interiorAbsolute = [111 + 0, 112], endAbsolute = [113, 114 + 0])
|
|
||||||
|> tangentialArc(end = [115, -116 + 0])
|
|
||||||
|> tangentialArc(endAbsolute = [117 + 0, 118])
|
|
||||||
|> tangentialArc(angle = 224, radius = 119 + 0)
|
|
||||||
|> tangentialArc(angle = 226 + 0, diameter = 120)
|
|
||||||
|
|
||||||
profile002 = startProfile(sketch001, at = [-121 + 0, 122])
|
|
||||||
|> angledLine(angle = 130, length = 123 + 0, tag = $rectangleSegmentA001)
|
|
||||||
|> angledLine(angle = segAng(rectangleSegmentA001) - 232, length = 124)
|
|
||||||
|> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001))
|
|
||||||
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|
|
||||||
|> close()
|
|
||||||
profile003 = circle(sketch001, center = [-125 + 0, -126], radius = 127 + 0)
|
|
||||||
profile004 = circleThreePoint(
|
|
||||||
sketch001,
|
|
||||||
p1 = [128, 129 + 0],
|
|
||||||
p2 = [130 + 0, 131],
|
|
||||||
p3 = [133, 132],
|
|
||||||
)
|
|
||||||
profile005 = circle(sketch001, center = [-134, -135 + 0], diameter = 136)
|
|
||||||
`
|
|
||||||
let ast = assertParse(code)
|
|
||||||
const result = await enginelessExecutor(ast)
|
|
||||||
const searchSnippets = [
|
|
||||||
'startProfile(sketch001, at = [100 + 0, 101])',
|
|
||||||
'startProfile(sketch001, at = [-121 + 0, 122])',
|
|
||||||
'circle(sketch001, center = [-125 + 0, -126], radius = 127 + 0)',
|
|
||||||
'circleThreePoint(',
|
|
||||||
'circle(sketch001, center = [-134, -135 + 0], diameter = 136)',
|
|
||||||
]
|
|
||||||
const ranges = searchSnippets.map((searchSnippet) => {
|
|
||||||
const startIndex = code.indexOf(searchSnippet)
|
|
||||||
return topLevelRange(startIndex, startIndex + searchSnippet.length)
|
|
||||||
})
|
|
||||||
const pathsToProfiles = ranges.map((range) =>
|
|
||||||
getNodePathFromSourceRange(ast, range)
|
|
||||||
)
|
|
||||||
|
|
||||||
if (err(result)) throw result
|
|
||||||
const scaledProfile = scaleProfiles({
|
|
||||||
ast,
|
|
||||||
factor: 0.5,
|
|
||||||
variables: result.variables,
|
|
||||||
pathsToProfile: pathsToProfiles,
|
|
||||||
})
|
|
||||||
if (err(scaledProfile)) throw scaledProfile
|
|
||||||
const pResult = parse(recast(scaledProfile.modifiedAst))
|
|
||||||
if (err(pResult) || !resultIsOk(pResult)) return
|
|
||||||
ast = pResult.program
|
|
||||||
const newCode = recast(ast)
|
|
||||||
if (err(newCode)) throw newCode
|
|
||||||
|
|
||||||
expect(newCode).toBe(`sketch001 = startSketchOn(YZ)
|
|
||||||
profile001 = startProfile(sketch001, at = [100 + 0, 50.5])
|
|
||||||
|> line(end = [51, 103 + 0])
|
|
||||||
|> line(endAbsolute = [104 + 0, 52.5])
|
|
||||||
|> angledLine(angle = -154, length = 106 + 0)
|
|
||||||
|> angledLine(angle = -208 + 0, lengthX = 53.5)
|
|
||||||
|> angledLine(angle = -150, lengthY = 108 + 0)
|
|
||||||
|> angledLine(angle = 212 + 0, endAbsoluteX = 54.5)
|
|
||||||
|> angledLine(angle = -146, endAbsoluteY = 110 + 0)
|
|
||||||
|> arc(interiorAbsolute = [111 + 0, 56], endAbsolute = [56.5, 114 + 0])
|
|
||||||
|> tangentialArc(end = [57.5, -116 + 0])
|
|
||||||
|> tangentialArc(endAbsolute = [117 + 0, 59])
|
|
||||||
|> tangentialArc(angle = 224deg, radius = 119 + 0)
|
|
||||||
|> tangentialArc(angle = 226 + 0, diameter = 60)
|
|
||||||
|
|
||||||
profile002 = startProfile(sketch001, at = [-121 + 0, 61])
|
|
||||||
|> angledLine(angle = 130, length = 123 + 0, tag = $rectangleSegmentA001)
|
|
||||||
|> angledLine(angle = segAng(rectangleSegmentA001) - 232, length = 62)
|
|
||||||
|> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001))
|
|
||||||
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|
|
||||||
|> close()
|
|
||||||
profile003 = circle(sketch001, center = [-125 + 0, -63], radius = 127 + 0)
|
|
||||||
profile004 = circleThreePoint(
|
|
||||||
sketch001,
|
|
||||||
p1 = [64, 129 + 0],
|
|
||||||
p2 = [130 + 0, 65.5],
|
|
||||||
p3 = [66.5, 66],
|
|
||||||
)
|
|
||||||
profile005 = circle(sketch001, center = [-67, -135 + 0], diameter = 68)
|
|
||||||
`)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
@ -10,7 +10,6 @@ import {
|
|||||||
createPipeSubstitution,
|
createPipeSubstitution,
|
||||||
} from '@src/lang/create'
|
} from '@src/lang/create'
|
||||||
import {
|
import {
|
||||||
doesProfileHaveAnyConstrainedDimension,
|
|
||||||
doesSceneHaveExtrudedSketch,
|
doesSceneHaveExtrudedSketch,
|
||||||
doesSceneHaveSweepableSketch,
|
doesSceneHaveSweepableSketch,
|
||||||
findAllPreviousVariables,
|
findAllPreviousVariables,
|
||||||
@ -36,95 +35,6 @@ beforeAll(async () => {
|
|||||||
await initPromise
|
await initPromise
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('doesProfileHaveConstrainDimension', () => {
|
|
||||||
const code = `sketch001 = startSketchOn(YZ)
|
|
||||||
profile001 = startProfile(sketch001, at = [100, 101])
|
|
||||||
|> line(end = [102, 103])
|
|
||||||
|> line(endAbsolute = [104, 105])
|
|
||||||
|> angledLine(angle = 206, length = 106)
|
|
||||||
|> angledLine(angle = -208, lengthX = 107)
|
|
||||||
|> angledLine(angle = 210, lengthY = 108)
|
|
||||||
|> angledLine(angle = 212, endAbsoluteX = 109)
|
|
||||||
|> angledLine(angle = 214, endAbsoluteY = 110)
|
|
||||||
|> arc(interiorAbsolute = [111, 112], endAbsolute = [113, 114])
|
|
||||||
|> tangentialArc(end = [115, -116])
|
|
||||||
|> tangentialArc(endAbsolute = [117, 118])
|
|
||||||
|> tangentialArc(angle = 224, radius = 119)
|
|
||||||
|> tangentialArc(angle = 226, diameter = 120)
|
|
||||||
|
|
||||||
profile002 = startProfile(sketch001, at = [-121, 122])
|
|
||||||
|> angledLine(angle = 130, length = 123, tag = $rectangleSegmentA001)
|
|
||||||
|> angledLine(angle = segAng(rectangleSegmentA001) - 232, length = 124)
|
|
||||||
|> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001))
|
|
||||||
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|
|
||||||
|> close()
|
|
||||||
profile003 = circle(sketch001, center = [-125, -126], radius = 127)
|
|
||||||
profile004 = circleThreePoint(
|
|
||||||
sketch001,
|
|
||||||
p1 = [128, 129],
|
|
||||||
p2 = [130, 131],
|
|
||||||
p3 = [132, 133],
|
|
||||||
)
|
|
||||||
profile005 = circle(sketch001, center = [-134, -135], diameter = 136)`
|
|
||||||
const profileSearchStrings = [
|
|
||||||
{
|
|
||||||
profileSearchString: 'profile001 = startProfile',
|
|
||||||
replaceCases: { start: 100, end: 120 },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
profileSearchString: 'profile002 = startProfile',
|
|
||||||
replaceCases: { start: 121, end: 124 },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
profileSearchString: 'profile003 = circle',
|
|
||||||
replaceCases: { start: 125, end: 127 },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
profileSearchString: 'profile004 = circleThreePoint',
|
|
||||||
replaceCases: { start: 128, end: 133 },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
profileSearchString: 'profile005 = circle',
|
|
||||||
replaceCases: { start: 134, end: 136 },
|
|
||||||
},
|
|
||||||
] as const
|
|
||||||
it('should return false for all profiles (no constrained dimensions detected)', () => {
|
|
||||||
const ast = assertParse(code)
|
|
||||||
|
|
||||||
profileSearchStrings.forEach((profile) => {
|
|
||||||
const profileStart = code.indexOf(profile.profileSearchString)
|
|
||||||
const profilePath = getNodePathFromSourceRange(
|
|
||||||
ast,
|
|
||||||
topLevelRange(profileStart, profileStart)
|
|
||||||
)
|
|
||||||
expect(
|
|
||||||
doesProfileHaveAnyConstrainedDimension(profilePath, ast)
|
|
||||||
).toBeFalsy()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
it('should true false when adding constraints for each Profile all profiles (no constrained dimensions detected)', () => {
|
|
||||||
profileSearchStrings.forEach((profile) => {
|
|
||||||
for (
|
|
||||||
let i = profile.replaceCases.start;
|
|
||||||
i <= profile.replaceCases.end;
|
|
||||||
i++
|
|
||||||
) {
|
|
||||||
const modifiedCode = code.replaceAll(String(i), `${i} + 5`)
|
|
||||||
const ast = assertParse(modifiedCode)
|
|
||||||
const profileStart = modifiedCode.indexOf(profile.profileSearchString)
|
|
||||||
const profilePath = getNodePathFromSourceRange(
|
|
||||||
ast,
|
|
||||||
topLevelRange(profileStart, profileStart)
|
|
||||||
)
|
|
||||||
expect(
|
|
||||||
doesProfileHaveAnyConstrainedDimension(profilePath, ast)
|
|
||||||
).toBeTruthy()
|
|
||||||
}
|
|
||||||
// })
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('findAllPreviousVariables', () => {
|
describe('findAllPreviousVariables', () => {
|
||||||
it('should find all previous variables', async () => {
|
it('should find all previous variables', async () => {
|
||||||
const code = `baseThick = 1
|
const code = `baseThick = 1
|
||||||
|
@ -55,23 +55,6 @@ import type { OpKclValue, Operation } from '@rust/kcl-lib/bindings/Operation'
|
|||||||
import { ARG_INDEX_FIELD, LABELED_ARG_FIELD } from '@src/lang/queryAstConstants'
|
import { ARG_INDEX_FIELD, LABELED_ARG_FIELD } from '@src/lang/queryAstConstants'
|
||||||
import type { KclCommandValue } from '@src/lib/commandTypes'
|
import type { KclCommandValue } from '@src/lib/commandTypes'
|
||||||
import type { UnaryExpression } from 'typescript'
|
import type { UnaryExpression } from 'typescript'
|
||||||
import {
|
|
||||||
ARG_END_ABSOLUTE,
|
|
||||||
ARG_END,
|
|
||||||
ARG_LENGTH,
|
|
||||||
ARG_CIRCLE_CENTER,
|
|
||||||
ARG_RADIUS,
|
|
||||||
ARG_LENGTH_X,
|
|
||||||
ARG_LENGTH_Y,
|
|
||||||
ARG_END_ABSOLUTE_X,
|
|
||||||
ARG_END_ABSOLUTE_Y,
|
|
||||||
ARG_INTERIOR_ABSOLUTE,
|
|
||||||
ARG_AT,
|
|
||||||
ARG_P1,
|
|
||||||
ARG_P2,
|
|
||||||
ARG_P3,
|
|
||||||
ARG_DIAMETER,
|
|
||||||
} from '@src/lang/constants'
|
|
||||||
import type { NumericType } from '@rust/kcl-lib/bindings/NumericType'
|
import type { NumericType } from '@rust/kcl-lib/bindings/NumericType'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1310,146 +1293,3 @@ export const getPathNormalisedForTruncatedAst = (
|
|||||||
Number(nodePathWithCorrectedIndexForTruncatedAst[1][0]) - minIndex
|
Number(nodePathWithCorrectedIndexForTruncatedAst[1][0]) - minIndex
|
||||||
return nodePathWithCorrectedIndexForTruncatedAst
|
return nodePathWithCorrectedIndexForTruncatedAst
|
||||||
}
|
}
|
||||||
|
|
||||||
export function doesProfileHaveAnyConstrainedDimension(
|
|
||||||
profilePath: PathToNode,
|
|
||||||
ast: Node<Program>
|
|
||||||
): boolean {
|
|
||||||
// Get the profile node from the path
|
|
||||||
const profileNodeResult = getNodeFromPath<Node<VariableDeclaration>>(
|
|
||||||
ast,
|
|
||||||
profilePath,
|
|
||||||
'VariableDeclaration'
|
|
||||||
)
|
|
||||||
if (err(profileNodeResult)) return false
|
|
||||||
|
|
||||||
const profileNode = profileNodeResult.node
|
|
||||||
|
|
||||||
// Single value dimension parameters to check (excluding angle as per requirements)
|
|
||||||
const singleValueLengthParams = new Set([
|
|
||||||
ARG_DIAMETER,
|
|
||||||
ARG_RADIUS,
|
|
||||||
ARG_LENGTH,
|
|
||||||
ARG_LENGTH_X,
|
|
||||||
ARG_LENGTH_Y,
|
|
||||||
ARG_END_ABSOLUTE_X,
|
|
||||||
ARG_END_ABSOLUTE_Y,
|
|
||||||
])
|
|
||||||
|
|
||||||
// Tuple value dimension parameters to check
|
|
||||||
const tupleValueParams = new Set([
|
|
||||||
ARG_CIRCLE_CENTER,
|
|
||||||
ARG_P1,
|
|
||||||
ARG_P2,
|
|
||||||
ARG_P3,
|
|
||||||
ARG_AT,
|
|
||||||
ARG_END,
|
|
||||||
ARG_END_ABSOLUTE,
|
|
||||||
ARG_INTERIOR_ABSOLUTE,
|
|
||||||
])
|
|
||||||
|
|
||||||
let hasConstrainedDimension = false
|
|
||||||
|
|
||||||
// Traverse the profile node to find all call expressions and their arguments
|
|
||||||
traverse(profileNode, {
|
|
||||||
enter: (node: any) => {
|
|
||||||
if (node.type === 'CallExpressionKw' && node.arguments) {
|
|
||||||
for (const arg of node.arguments) {
|
|
||||||
if (arg.type === 'LabeledArg' && arg.label?.name) {
|
|
||||||
const paramName = arg.label.name
|
|
||||||
|
|
||||||
// Check if this parameter is in our whitelist
|
|
||||||
if (
|
|
||||||
singleValueLengthParams.has(paramName) ||
|
|
||||||
tupleValueParams.has(paramName)
|
|
||||||
) {
|
|
||||||
// Special case: endAbsolute = [profileStartX(%), profileStartY(%)]
|
|
||||||
// This should NOT count as constrained
|
|
||||||
if (
|
|
||||||
paramName === ARG_END_ABSOLUTE &&
|
|
||||||
arg.arg.type === 'ArrayExpression' &&
|
|
||||||
arg.arg.elements.length === 2
|
|
||||||
) {
|
|
||||||
const [first, second] = arg.arg.elements
|
|
||||||
if (
|
|
||||||
first.type === 'CallExpressionKw' &&
|
|
||||||
second.type === 'CallExpressionKw' &&
|
|
||||||
first.callee.type === 'Name' &&
|
|
||||||
second.callee.type === 'Name' &&
|
|
||||||
first.callee.name.name === 'profileStartX' &&
|
|
||||||
second.callee.name.name === 'profileStartY'
|
|
||||||
) {
|
|
||||||
// This is the special case - don't count as constrained
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Special case: angledLine length = -segLen(rectangleSegmentA001)
|
|
||||||
// This should NOT count as constrained
|
|
||||||
if (
|
|
||||||
node.callee?.type === 'Name' &&
|
|
||||||
node.callee.name.name === 'angledLine' &&
|
|
||||||
paramName === ARG_LENGTH
|
|
||||||
) {
|
|
||||||
let callExpr = null
|
|
||||||
|
|
||||||
// Check if it's a direct call expression or unary expression with call expression
|
|
||||||
if (arg.arg.type === 'CallExpressionKw') {
|
|
||||||
callExpr = arg.arg
|
|
||||||
} else if (
|
|
||||||
arg.arg.type === 'UnaryExpression' &&
|
|
||||||
arg.arg.argument?.type === 'CallExpressionKw'
|
|
||||||
) {
|
|
||||||
callExpr = arg.arg.argument
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
callExpr &&
|
|
||||||
callExpr.callee?.type === 'Name' &&
|
|
||||||
callExpr.callee.name.name === 'segLen' &&
|
|
||||||
callExpr.unlabeled?.type === 'Name' &&
|
|
||||||
callExpr.unlabeled.name.name.startsWith('rectangleSegmentA')
|
|
||||||
) {
|
|
||||||
// This is the special case - don't count as constrained
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the argument value is non-static
|
|
||||||
if (!isStaticValue(arg.arg)) {
|
|
||||||
hasConstrainedDimension = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// If we found a constrained dimension, we can break out of the outer loop too
|
|
||||||
if (hasConstrainedDimension) {
|
|
||||||
return false // This stops the traversal
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
return hasConstrainedDimension
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper function to check if a node represents a static/constant value (literal, array of literals, or negative literal)
|
|
||||||
function isStaticValue(node: any): boolean {
|
|
||||||
if (node.type === 'Literal') {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
if (node.type === 'ArrayExpression') {
|
|
||||||
// Array is literal if all elements are literals
|
|
||||||
return node.elements.every((element: any) => isStaticValue(element))
|
|
||||||
}
|
|
||||||
|
|
||||||
if (node.type === 'UnaryExpression' && node.operator === '-') {
|
|
||||||
// Negative literal numbers
|
|
||||||
return isStaticValue(node.argument)
|
|
||||||
}
|
|
||||||
|
|
||||||
// All other node types (Name, CallExpression, BinaryExpression, etc.) are non-literal
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
@ -24,7 +24,6 @@ import {
|
|||||||
ARG_TAG,
|
ARG_TAG,
|
||||||
DETERMINING_ARGS,
|
DETERMINING_ARGS,
|
||||||
ARG_INTERIOR_ABSOLUTE,
|
ARG_INTERIOR_ABSOLUTE,
|
||||||
ARG_DIAMETER,
|
|
||||||
} from '@src/lang/constants'
|
} from '@src/lang/constants'
|
||||||
import {
|
import {
|
||||||
createArrayExpression,
|
createArrayExpression,
|
||||||
@ -1425,27 +1424,13 @@ export const circle: SketchLineHelperKw = {
|
|||||||
|
|
||||||
const { node: callExpression } = nodeMeta
|
const { node: callExpression } = nodeMeta
|
||||||
|
|
||||||
// All function arguments, except the tag
|
|
||||||
const functionArguments = callExpression.arguments
|
|
||||||
.map((arg) => arg.label?.name)
|
|
||||||
.filter((n) => n && n !== ARG_TAG)
|
|
||||||
|
|
||||||
const newCenter = createArrayExpression([
|
const newCenter = createArrayExpression([
|
||||||
createLiteral(roundOff(center[0])),
|
createLiteral(roundOff(center[0])),
|
||||||
createLiteral(roundOff(center[1])),
|
createLiteral(roundOff(center[1])),
|
||||||
])
|
])
|
||||||
mutateKwArg(ARG_CIRCLE_CENTER, callExpression, newCenter)
|
mutateKwArg(ARG_CIRCLE_CENTER, callExpression, newCenter)
|
||||||
|
|
||||||
// Check if the circle uses diameter or radius
|
|
||||||
const isDiameter = functionArguments.includes(ARG_DIAMETER)
|
|
||||||
if (isDiameter) {
|
|
||||||
const newDiameter = createLiteral(roundOff(radius * 2))
|
|
||||||
mutateKwArg(ARG_DIAMETER, callExpression, newDiameter)
|
|
||||||
} else {
|
|
||||||
const newRadius = createLiteral(roundOff(radius))
|
const newRadius = createLiteral(roundOff(radius))
|
||||||
mutateKwArg(ARG_RADIUS, callExpression, newRadius)
|
mutateKwArg(ARG_RADIUS, callExpression, newRadius)
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
modifiedAst: _node,
|
modifiedAst: _node,
|
||||||
pathToNode,
|
pathToNode,
|
||||||
@ -4183,14 +4168,7 @@ const tangentialArcHelpers = {
|
|||||||
.map((arg) => arg.label?.name)
|
.map((arg) => arg.label?.name)
|
||||||
.filter((n) => n && n !== ARG_TAG)
|
.filter((n) => n && n !== ARG_TAG)
|
||||||
|
|
||||||
const isDiameter = areArraysEqual(functionArguments, [
|
if (areArraysEqual(functionArguments, [ARG_ANGLE, ARG_RADIUS])) {
|
||||||
ARG_ANGLE,
|
|
||||||
ARG_DIAMETER,
|
|
||||||
])
|
|
||||||
if (
|
|
||||||
areArraysEqual(functionArguments, [ARG_ANGLE, ARG_RADIUS]) ||
|
|
||||||
isDiameter
|
|
||||||
) {
|
|
||||||
// Using length and radius -> convert "from", "to" to the matching length and radius
|
// Using length and radius -> convert "from", "to" to the matching length and radius
|
||||||
const previousEndTangent = input.previousEndTangent
|
const previousEndTangent = input.previousEndTangent
|
||||||
if (previousEndTangent) {
|
if (previousEndTangent) {
|
||||||
@ -4241,19 +4219,11 @@ const tangentialArcHelpers = {
|
|||||||
|
|
||||||
const radius = distance2d(center, from)
|
const radius = distance2d(center, from)
|
||||||
|
|
||||||
if (!isDiameter) {
|
|
||||||
mutateKwArg(
|
mutateKwArg(
|
||||||
ARG_RADIUS,
|
ARG_RADIUS,
|
||||||
callExpression,
|
callExpression,
|
||||||
createLiteral(roundOff(radius, 2))
|
createLiteral(roundOff(radius, 2))
|
||||||
)
|
)
|
||||||
} else {
|
|
||||||
mutateKwArg(
|
|
||||||
ARG_DIAMETER,
|
|
||||||
callExpression,
|
|
||||||
createLiteral(roundOff(radius * 2, 2))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
const angleValue = createLiteralMaybeSuffix({
|
const angleValue = createLiteralMaybeSuffix({
|
||||||
value: roundOff(angle, 2),
|
value: roundOff(angle, 2),
|
||||||
suffix: 'Deg',
|
suffix: 'Deg',
|
||||||
|
@ -172,7 +172,6 @@ export type ModelingCommandSchema = {
|
|||||||
variableName: string
|
variableName: string
|
||||||
}
|
}
|
||||||
namedValue: KclCommandValue
|
namedValue: KclCommandValue
|
||||||
scaleSketch?: boolean
|
|
||||||
}
|
}
|
||||||
'Prompt-to-edit': {
|
'Prompt-to-edit': {
|
||||||
prompt: string
|
prompt: string
|
||||||
|
@ -15,7 +15,6 @@ export type CommandBarContext = {
|
|||||||
currentArgument?: CommandArgument<unknown> & { name: string }
|
currentArgument?: CommandArgument<unknown> & { name: string }
|
||||||
argumentsToSubmit: { [x: string]: unknown }
|
argumentsToSubmit: { [x: string]: unknown }
|
||||||
machineManager: MachineManager
|
machineManager: MachineManager
|
||||||
additionalData: { [x: string]: unknown } // For storing extra parameters like scaleSketch
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CommandBarMachineEvent =
|
export type CommandBarMachineEvent =
|
||||||
@ -78,7 +77,6 @@ export type CommandBarMachineEvent =
|
|||||||
data: { [x: string]: CommandArgumentWithName<unknown> }
|
data: { [x: string]: CommandArgumentWithName<unknown> }
|
||||||
}
|
}
|
||||||
| { type: 'Set machine manager'; data: MachineManager }
|
| { type: 'Set machine manager'; data: MachineManager }
|
||||||
| { type: 'Set additional data'; data: { [x: string]: unknown } }
|
|
||||||
|
|
||||||
export const commandBarMachine = setup({
|
export const commandBarMachine = setup({
|
||||||
types: {
|
types: {
|
||||||
@ -119,12 +117,6 @@ export const commandBarMachine = setup({
|
|||||||
resolvedArgs[argName] =
|
resolvedArgs[argName] =
|
||||||
typeof argValue === 'function' ? argValue(context) : argValue
|
typeof argValue === 'function' ? argValue(context) : argValue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Special handling for "Constrain with named value" command to include scaleSketch
|
|
||||||
if (selectedCommand.name === 'Constrain with named value') {
|
|
||||||
resolvedArgs.scaleSketch = context.additionalData.scaleSketch
|
|
||||||
}
|
|
||||||
|
|
||||||
selectedCommand?.onSubmit(resolvedArgs)
|
selectedCommand?.onSubmit(resolvedArgs)
|
||||||
} else {
|
} else {
|
||||||
selectedCommand?.onSubmit({ context, event })
|
selectedCommand?.onSubmit({ context, event })
|
||||||
@ -232,16 +224,6 @@ export const commandBarMachine = setup({
|
|||||||
selectedCommand: undefined,
|
selectedCommand: undefined,
|
||||||
currentArgument: undefined,
|
currentArgument: undefined,
|
||||||
argumentsToSubmit: {},
|
argumentsToSubmit: {},
|
||||||
additionalData: {},
|
|
||||||
}),
|
|
||||||
'Set additional data': assign({
|
|
||||||
additionalData: ({ context, event }) => {
|
|
||||||
if (event.type !== 'Set additional data') return context.additionalData
|
|
||||||
return {
|
|
||||||
...context.additionalData,
|
|
||||||
...event.data,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
'Set selected command': assign({
|
'Set selected command': assign({
|
||||||
selectedCommand: ({ context, event }) =>
|
selectedCommand: ({ context, event }) =>
|
||||||
@ -486,7 +468,6 @@ export const commandBarMachine = setup({
|
|||||||
codeBasedSelections: [],
|
codeBasedSelections: [],
|
||||||
},
|
},
|
||||||
argumentsToSubmit: {},
|
argumentsToSubmit: {},
|
||||||
additionalData: {},
|
|
||||||
machineManager: {
|
machineManager: {
|
||||||
machines: [],
|
machines: [],
|
||||||
machineApiIp: null,
|
machineApiIp: null,
|
||||||
@ -645,11 +626,6 @@ export const commandBarMachine = setup({
|
|||||||
actions: 'Set machine manager',
|
actions: 'Set machine manager',
|
||||||
},
|
},
|
||||||
|
|
||||||
'Set additional data': {
|
|
||||||
reenter: false,
|
|
||||||
actions: 'Set additional data',
|
|
||||||
},
|
|
||||||
|
|
||||||
Close: {
|
Close: {
|
||||||
target: '.Closed',
|
target: '.Closed',
|
||||||
actions: 'Clear selected command',
|
actions: 'Clear selected command',
|
||||||
|
@ -19,7 +19,6 @@ import { err } from '@src/lib/trap'
|
|||||||
import {
|
import {
|
||||||
createIdentifier,
|
createIdentifier,
|
||||||
createLiteral,
|
createLiteral,
|
||||||
createLocalName,
|
|
||||||
createVariableDeclaration,
|
createVariableDeclaration,
|
||||||
} from '@src/lang/create'
|
} from '@src/lang/create'
|
||||||
import { ARG_END_ABSOLUTE, ARG_INTERIOR_ABSOLUTE } from '@src/lang/constants'
|
import { ARG_END_ABSOLUTE, ARG_INTERIOR_ABSOLUTE } from '@src/lang/constants'
|
||||||
@ -1158,7 +1157,7 @@ p3 = [342.51, 216.38],
|
|||||||
filter
|
filter
|
||||||
)
|
)
|
||||||
const constraint = constraintInfo[constraintIndex]
|
const constraint = constraintInfo[constraintIndex]
|
||||||
|
console.log('constraint', constraint)
|
||||||
if (!constraint.argPosition) {
|
if (!constraint.argPosition) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Constraint at index ${constraintIndex} does not have argPosition`
|
`Constraint at index ${constraintIndex} does not have argPosition`
|
||||||
@ -1293,279 +1292,3 @@ p3 = [342.51, 216.38],
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('testing sketch scale on first length constraint', () => {
|
|
||||||
it('should scale sketch when constrain with named value is used with scale checkbox enabled', async () => {
|
|
||||||
// Create a sketch with multiple segments using only literal values (no constraints)
|
|
||||||
const code = `sketch001 = startSketchOn(XZ)
|
|
||||||
profile001 = startProfile(sketch001, at = [100, 100])
|
|
||||||
|> line(end = [200, 0])
|
|
||||||
|> line(end = [0, 200])
|
|
||||||
|> line(end = [-200, 0])
|
|
||||||
|> close()
|
|
||||||
profile002 = circle(sketch001, center = [400, 400], radius = 50)
|
|
||||||
`
|
|
||||||
|
|
||||||
const ast = assertParse(code)
|
|
||||||
await kclManager.executeAst({ ast })
|
|
||||||
expect(kclManager.errors).toEqual([])
|
|
||||||
|
|
||||||
// Find a segment to constrain (the first line segment)
|
|
||||||
const indexOfInterest = code.indexOf('line(end = [200, 0])')
|
|
||||||
const artifact = [...kclManager.artifactGraph].find(
|
|
||||||
([_, artifact]) =>
|
|
||||||
artifact?.type === 'segment' &&
|
|
||||||
artifact.codeRef.range[0] <= indexOfInterest &&
|
|
||||||
indexOfInterest <= artifact.codeRef.range[1]
|
|
||||||
)?.[1]
|
|
||||||
|
|
||||||
if (!artifact || !('codeRef' in artifact)) {
|
|
||||||
throw new Error('Artifact not found or invalid artifact structure')
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create modeling machine actor
|
|
||||||
const modelingActor = createActor(modelingMachine, {
|
|
||||||
input: modelingMachineDefaultContext,
|
|
||||||
}).start()
|
|
||||||
|
|
||||||
// Enter sketch mode
|
|
||||||
modelingActor.send({
|
|
||||||
type: 'Set selection',
|
|
||||||
data: {
|
|
||||||
selectionType: 'mirrorCodeMirrorSelections',
|
|
||||||
selection: {
|
|
||||||
graphSelections: [
|
|
||||||
{
|
|
||||||
artifact: artifact,
|
|
||||||
codeRef: artifact.codeRef,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
otherSelections: [],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
modelingActor.send({ type: 'Enter sketch' })
|
|
||||||
|
|
||||||
// Wait for sketch mode
|
|
||||||
await waitForCondition(() => {
|
|
||||||
const snapshot = modelingActor.getSnapshot()
|
|
||||||
return snapshot.value !== 'animating to existing sketch'
|
|
||||||
}, 5000)
|
|
||||||
|
|
||||||
expect(modelingActor.getSnapshot().value).toEqual({
|
|
||||||
Sketch: { SketchIdle: 'scene drawn' },
|
|
||||||
})
|
|
||||||
|
|
||||||
// Get constraint info for the segment
|
|
||||||
const callExp = getNodeFromPath<Node<CallExpressionKw>>(
|
|
||||||
kclManager.ast,
|
|
||||||
artifact.codeRef.pathToNode,
|
|
||||||
'CallExpressionKw'
|
|
||||||
)
|
|
||||||
if (err(callExp)) {
|
|
||||||
throw new Error('Failed to get CallExpressionKw node')
|
|
||||||
}
|
|
||||||
|
|
||||||
const constraintInfo = getConstraintInfoKw(
|
|
||||||
callExp.node,
|
|
||||||
codeManager.code,
|
|
||||||
artifact.codeRef.pathToNode
|
|
||||||
)
|
|
||||||
const constraint = constraintInfo[0] // First constraint (x value)
|
|
||||||
|
|
||||||
// Store original code to compare scaling
|
|
||||||
const originalCode = codeManager.code
|
|
||||||
|
|
||||||
// No need for command bar setup, we're testing the modeling machine directly
|
|
||||||
|
|
||||||
// Simulate submitting the command with scaling enabled
|
|
||||||
// The new value will be 100 (half of original 200), so scale factor should be 0.5
|
|
||||||
modelingActor.send({
|
|
||||||
type: 'Constrain with named value',
|
|
||||||
data: {
|
|
||||||
currentValue: {
|
|
||||||
valueText: constraint.value,
|
|
||||||
pathToNode: constraint.pathToNode,
|
|
||||||
variableName: 'length_var',
|
|
||||||
},
|
|
||||||
namedValue: {
|
|
||||||
valueText: '100',
|
|
||||||
variableName: 'length_var',
|
|
||||||
insertIndex: 0,
|
|
||||||
valueCalculated: '100',
|
|
||||||
variableDeclarationAst: createVariableDeclaration(
|
|
||||||
'length_var',
|
|
||||||
createLiteral('100')
|
|
||||||
),
|
|
||||||
variableIdentifierAst: createLocalName('length_var'),
|
|
||||||
valueAst: createLiteral('100'),
|
|
||||||
},
|
|
||||||
scaleSketch: true, // This should trigger scaling
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
// Wait for the constraint to be applied and sketch to be scaled
|
|
||||||
await waitForCondition(() => {
|
|
||||||
const snapshot = modelingActor.getSnapshot()
|
|
||||||
return (
|
|
||||||
JSON.stringify(snapshot.value) !==
|
|
||||||
JSON.stringify({ Sketch: 'Converting to named value' })
|
|
||||||
)
|
|
||||||
}, 5000)
|
|
||||||
|
|
||||||
// Wait for code to be updated
|
|
||||||
const startTime = Date.now()
|
|
||||||
while (codeManager.code === originalCode && Date.now() - startTime < 5000) {
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
||||||
}
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
|
||||||
console.log('code is', codeManager.code)
|
|
||||||
|
|
||||||
// Verify the constraint was applied
|
|
||||||
expect(codeManager.code).toContain('length_var')
|
|
||||||
expect(codeManager.code).toContain("length_var = '100'")
|
|
||||||
|
|
||||||
// Verify scaling occurred - all dimensions should be scaled by 0.5 (100/200)
|
|
||||||
// Original values: line(end = [200, 0]), line(end = [0, 200]), line(end = [-200, 0])
|
|
||||||
// Scaled values should be: line(end = [100, 0]), line(end = [0, 100]), line(end = [-100, 0])
|
|
||||||
expect(codeManager.code).toContain('line(end = [length_var, 0])') // First line uses variable
|
|
||||||
expect(codeManager.code).toContain('line(end = [0, 100])') // Second line scaled
|
|
||||||
expect(codeManager.code).toContain('line(end = [-100, 0])') // Third line scaled
|
|
||||||
|
|
||||||
// Circle should also be scaled: radius = 50 -> radius = 25
|
|
||||||
expect(codeManager.code).toContain('radius = 25')
|
|
||||||
|
|
||||||
// Start positions should be scaled: at = [100, 100] -> at = [50, 50], at = [400, 400] -> at = [200, 200]
|
|
||||||
expect(codeManager.code).toContain('at = [50, 50]')
|
|
||||||
expect(codeManager.code).toContain('center = [200, 200]')
|
|
||||||
}, 15_000)
|
|
||||||
|
|
||||||
it('should not scale sketch when constrain with named value is used with scale checkbox disabled', async () => {
|
|
||||||
// Create a sketch with multiple segments using only literal values (no constraints)
|
|
||||||
const code = `sketch001 = startSketchOn(XZ)
|
|
||||||
profile001 = startProfile(sketch001, at = [100, 100])
|
|
||||||
|> line(end = [200, 0])
|
|
||||||
|> line(end = [0, 200])
|
|
||||||
|> close()`
|
|
||||||
|
|
||||||
const ast = assertParse(code)
|
|
||||||
await kclManager.executeAst({ ast })
|
|
||||||
expect(kclManager.errors).toEqual([])
|
|
||||||
|
|
||||||
// Find a segment to constrain (the first line segment)
|
|
||||||
const indexOfInterest = code.indexOf('line(end = [200, 0])')
|
|
||||||
const artifact = [...kclManager.artifactGraph].find(
|
|
||||||
([_, artifact]) =>
|
|
||||||
artifact?.type === 'segment' &&
|
|
||||||
artifact.codeRef.range[0] <= indexOfInterest &&
|
|
||||||
indexOfInterest <= artifact.codeRef.range[1]
|
|
||||||
)?.[1]
|
|
||||||
|
|
||||||
if (!artifact || !('codeRef' in artifact)) {
|
|
||||||
throw new Error('Artifact not found or invalid artifact structure')
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create modeling machine actor
|
|
||||||
const modelingActor = createActor(modelingMachine, {
|
|
||||||
input: modelingMachineDefaultContext,
|
|
||||||
}).start()
|
|
||||||
|
|
||||||
// Enter sketch mode
|
|
||||||
modelingActor.send({
|
|
||||||
type: 'Set selection',
|
|
||||||
data: {
|
|
||||||
selectionType: 'mirrorCodeMirrorSelections',
|
|
||||||
selection: {
|
|
||||||
graphSelections: [
|
|
||||||
{
|
|
||||||
artifact: artifact,
|
|
||||||
codeRef: artifact.codeRef,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
otherSelections: [],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
modelingActor.send({ type: 'Enter sketch' })
|
|
||||||
|
|
||||||
// Wait for sketch mode
|
|
||||||
await waitForCondition(() => {
|
|
||||||
const snapshot = modelingActor.getSnapshot()
|
|
||||||
return snapshot.value !== 'animating to existing sketch'
|
|
||||||
}, 5000)
|
|
||||||
|
|
||||||
// Get constraint info for the segment
|
|
||||||
const callExp = getNodeFromPath<Node<CallExpressionKw>>(
|
|
||||||
kclManager.ast,
|
|
||||||
artifact.codeRef.pathToNode,
|
|
||||||
'CallExpressionKw'
|
|
||||||
)
|
|
||||||
if (err(callExp)) {
|
|
||||||
throw new Error('Failed to get CallExpressionKw node')
|
|
||||||
}
|
|
||||||
|
|
||||||
const constraintInfo = getConstraintInfoKw(
|
|
||||||
callExp.node,
|
|
||||||
codeManager.code,
|
|
||||||
artifact.codeRef.pathToNode
|
|
||||||
)
|
|
||||||
const constraint = constraintInfo[0] // First constraint (x value)
|
|
||||||
|
|
||||||
// Store original code to compare
|
|
||||||
const originalCode = codeManager.code
|
|
||||||
|
|
||||||
// Submit command with scaling disabled directly to modeling machine
|
|
||||||
|
|
||||||
modelingActor.send({
|
|
||||||
type: 'Constrain with named value',
|
|
||||||
data: {
|
|
||||||
currentValue: {
|
|
||||||
valueText: constraint.value,
|
|
||||||
pathToNode: constraint.pathToNode,
|
|
||||||
variableName: 'length_var',
|
|
||||||
},
|
|
||||||
namedValue: {
|
|
||||||
valueText: '100',
|
|
||||||
variableName: 'length_var',
|
|
||||||
insertIndex: 0,
|
|
||||||
valueCalculated: '100',
|
|
||||||
variableDeclarationAst: createVariableDeclaration(
|
|
||||||
'length_var',
|
|
||||||
createLiteral('100')
|
|
||||||
),
|
|
||||||
variableIdentifierAst: createLocalName('length_var'),
|
|
||||||
valueAst: createLiteral('100'),
|
|
||||||
},
|
|
||||||
scaleSketch: false, // Scaling disabled
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
// Wait for the constraint to be applied
|
|
||||||
await waitForCondition(() => {
|
|
||||||
const snapshot = modelingActor.getSnapshot()
|
|
||||||
return (
|
|
||||||
JSON.stringify(snapshot.value) !==
|
|
||||||
JSON.stringify({ Sketch: 'Converting to named value' })
|
|
||||||
)
|
|
||||||
}, 5000)
|
|
||||||
|
|
||||||
// Wait for code to be updated
|
|
||||||
const startTime = Date.now()
|
|
||||||
while (codeManager.code === originalCode && Date.now() - startTime < 5000) {
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify the constraint was applied but no scaling occurred
|
|
||||||
expect(codeManager.code).toContain('length_var')
|
|
||||||
expect(codeManager.code).toContain("length_var = '100'")
|
|
||||||
expect(codeManager.code).toContain('line(end = [length_var, 0])')
|
|
||||||
|
|
||||||
// Other dimensions should remain unchanged (no scaling)
|
|
||||||
expect(codeManager.code).toContain('line(end = [0, 200])') // Should remain 200, not scaled to 100
|
|
||||||
expect(codeManager.code).toContain('at = [100, 100]') // Should remain [100, 100], not scaled
|
|
||||||
}, 15_000)
|
|
||||||
|
|
||||||
// Note: The third test for checking if scale checkbox is disabled when sketch has existing constraints
|
|
||||||
// would be better tested in the UI layer (CommandBarKclInput.tsx) or as an e2e test
|
|
||||||
// since it's primarily a UI behavior test
|
|
||||||
})
|
|
||||||
|
@ -9,7 +9,6 @@ import {
|
|||||||
orthoScale,
|
orthoScale,
|
||||||
quaternionFromUpNForward,
|
quaternionFromUpNForward,
|
||||||
} from '@src/clientSideScene/helpers'
|
} from '@src/clientSideScene/helpers'
|
||||||
import { scaleProfiles } from '@src/clientSideScene/sceneEntities'
|
|
||||||
import type { Setting } from '@src/lib/settings/initialSettings'
|
import type { Setting } from '@src/lib/settings/initialSettings'
|
||||||
import type { CameraProjectionType } from '@rust/kcl-lib/bindings/CameraProjectionType'
|
import type { CameraProjectionType } from '@rust/kcl-lib/bindings/CameraProjectionType'
|
||||||
import { DRAFT_DASHED_LINE } from '@src/clientSideScene/sceneConstants'
|
import { DRAFT_DASHED_LINE } from '@src/clientSideScene/sceneConstants'
|
||||||
@ -2249,55 +2248,6 @@ export const modelingMachine = setup({
|
|||||||
return Promise.reject(new Error('Unexpected compilation error'))
|
return Promise.reject(new Error('Unexpected compilation error'))
|
||||||
let parsed = pResult.program
|
let parsed = pResult.program
|
||||||
|
|
||||||
// Apply sketch scaling if enabled
|
|
||||||
if (data.scaleSketch && sketchDetails.sketchNodePaths) {
|
|
||||||
const originalValue = parseFloat(data.currentValue.valueText)
|
|
||||||
const newValue = parseFloat(
|
|
||||||
typeof data.namedValue === 'object' &&
|
|
||||||
'valueText' in data.namedValue
|
|
||||||
? data.namedValue.valueText
|
|
||||||
: String(data.namedValue)
|
|
||||||
)
|
|
||||||
|
|
||||||
if (
|
|
||||||
!Number.isNaN(originalValue) &&
|
|
||||||
!Number.isNaN(newValue) &&
|
|
||||||
originalValue !== 0
|
|
||||||
) {
|
|
||||||
const scaleFactor = newValue / originalValue
|
|
||||||
|
|
||||||
try {
|
|
||||||
const scaleResult = scaleProfiles({
|
|
||||||
ast: parsed,
|
|
||||||
pathsToProfile: sketchDetails.sketchNodePaths,
|
|
||||||
factor: scaleFactor,
|
|
||||||
variables: kclManager.variables,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!err(scaleResult)) {
|
|
||||||
parsed = scaleResult.modifiedAst
|
|
||||||
|
|
||||||
// Reparse and recast to get fresh source ranges after scaling
|
|
||||||
const reparseResult = parse(recast(parsed))
|
|
||||||
if (!trap(reparseResult) && resultIsOk(reparseResult)) {
|
|
||||||
parsed = reparseResult.program
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Continue with constraint application even if scaling fails
|
|
||||||
console.warn(
|
|
||||||
'Failed to scale sketch, continuing with constraint application'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
// Continue with constraint application even if scaling fails
|
|
||||||
console.warn(
|
|
||||||
'Error scaling sketch, continuing with constraint application:',
|
|
||||||
error
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let result: {
|
let result: {
|
||||||
modifiedAst: Node<Program>
|
modifiedAst: Node<Program>
|
||||||
pathToReplaced: PathToNode | null
|
pathToReplaced: PathToNode | null
|
||||||
|
Reference in New Issue
Block a user