Compare commits

...

2 Commits

Author SHA1 Message Date
c5d103a166 chicken coop support 2025-01-28 16:04:18 +11:00
c7b7b91129 Reduce the amount of data sent back to JS/TS from WASM 2024-12-28 19:24:38 -05:00
6 changed files with 1495 additions and 7 deletions

View File

@ -16,6 +16,7 @@ import {
emptyExecState,
ExecState,
initPromise,
KclValue,
parse,
PathToNode,
Program,
@ -33,6 +34,16 @@ import {
ModelingCmdReq_type,
} from '@kittycad/lib/dist/types/src/models'
import { Operation } from 'wasm-lib/kcl/bindings/Operation'
import {
createCutList,
CutLumber,
LumberToCut,
Offcut,
STANDARD_TIMBER_LENGTH,
uuider,
} from './std/cutlist'
const LOCALSTORAGE_KEY = 'chickenCoop'
interface ExecuteArgs {
ast?: Node<Program>
@ -370,6 +381,148 @@ export class KclManager {
// Do not add the errors since the program was interrupted and the error is not a real KCL error
this.addDiagnostics(isInterrupted ? [] : kclErrorsToDiagnostics(errors))
this.execState = execState
console.log('execState', execState)
const frame = execState.memory.get('aGroupOfStudForCreatingCutList')
if (frame) {
const _existingCutList = localStorage.getItem(LOCALSTORAGE_KEY)
const existingCutList: ReturnType<typeof createCutList>[] =
_existingCutList ? JSON.parse(_existingCutList) : []
console.log('existingCutList', existingCutList)
let idStart
if (existingCutList.length) {
const lastList = existingCutList[existingCutList.length - 1]
const lastId =
lastList.cutLumbers[lastList.cutLumbers.length - 1].timberLengthId
idStart = Number(lastId)
// sumerize the last cutlist
const masterCutLumbersByTimberLengthId: Record<string, CutLumber[]> = {}
existingCutList.forEach((cutList) => {
cutList.cutLumbers.forEach((cutLumber) => {
if (!masterCutLumbersByTimberLengthId[cutLumber.timberLengthId]) {
masterCutLumbersByTimberLengthId[cutLumber.timberLengthId] = []
}
masterCutLumbersByTimberLengthId[cutLumber.timberLengthId].push(
cutLumber
)
})
})
console.log(
'masterCutLumbersByTimberLengthId',
masterCutLumbersByTimberLengthId
)
let prettyPrint = ''
Object.entries(masterCutLumbersByTimberLengthId).forEach(
([key, value]) => {
// prettyPrint += `timberLengthId: ${key}`
value.forEach((cutLumber) => {
prettyPrint += `timberLengthId: ${key
.toString()
.padStart(2, ' ')}, cutLength: ${Math.round(
cutLumber.lengthBeforeAngles
)
.toString()
.padStart(5, ' ')}, id: ${cutLumber.id
.toString()
.padStart(2, ' ')}, ang1: ${Math.round(cutLumber.angle1)
.toString()
.padStart(3, ' ')}, ang2: ${Math.round(cutLumber.angle2)
.toString()
.padStart(3, ' ')}, angleOnWidth: ${cutLumber.angleRelevantWidth
.toString()
.padStart(3, ' ')}, depth: ${cutLumber.depth
.toString()
.padStart(2, ' ')}, name: ${cutLumber.name.padEnd(44, ' ')}\n`
})
// prettyPrint += `timberLengthId: ${key}`
}
)
console.log('cutlist json')
console.log(JSON.stringify(existingCutList))
console.log(prettyPrint)
// cutLumbers.forEach((cutLumber) => {
// if (!cutLumbersByTimberLengthId[cutLumber.timberLengthId]) {
// cutLumbersByTimberLengthId[cutLumber.timberLengthId] = []
// }
// cutLumbersByTimberLengthId[cutLumber.timberLengthId].push(cutLumber)
// })
} else {
idStart = 0
}
console.log('idStart', idStart)
const aGroupOfStudForCreatingCutList = flatternKCLVal(frame) as {
angleRelevantWidth: number
endCut1: number
endCut2: number
lengthBeforeAngles: number
name: string
studType: (string | number)[]
}[]
console.log(
'aGroupOfStudForCreatingCutList',
aGroupOfStudForCreatingCutList
)
const uuid = uuider(idStart)
let initialOffcutList: Offcut[] = existingCutList.length
? existingCutList[existingCutList.length - 1].offcutList
: [
{
length: STANDARD_TIMBER_LENGTH,
lastAngle: 0,
angleRelevantWidth: 90,
timberLengthId: uuid(),
},
]
/**
angleRelevantWidth: 90,
depth: 35,
lengthBeforeAngles: 2000,
angle1: 0,
angle2: 0,
name: 'a',
*/
const initialLumbersToCut: LumberToCut[] =
aGroupOfStudForCreatingCutList.map((stud, index) => {
return {
angleRelevantWidth: stud.angleRelevantWidth,
depth: stud.studType.find(
(item) =>
typeof item === 'number' && item !== stud.angleRelevantWidth
) as number,
lengthBeforeAngles: stud.lengthBeforeAngles,
angle1: stud.endCut1,
angle2: stud.endCut2,
name: stud.name,
}
})
// console.log('initialLumbersToCut', JSON.stringify(initialLumbersToCut))
const cutListResult = createCutList({
lumbersToCut: initialLumbersToCut,
offcutList: initialOffcutList,
uuid,
})
const newCutList = [...existingCutList, cutListResult]
// localStorage.setItem(LOCALSTORAGE_KEY, JSON.stringify(newCutList))
// console.log('cutListResultJSON', JSON.stringify(cutListResult))
console.log('cutListResult', cutListResult, 'newCutlistarr', newCutList)
} else {
console.log('no frame', execState)
}
// console.log('frame', frame)
// if (Array.isArray(frame?.value)) {
// const cleanArray = frame?.value.map((value: any) => {
// const val = value.value
// const obj: any = {}
// Object.entries(val).forEach(([key, value2]: [any, any]) => {
// obj[key] = value2.value
// })
// return obj
// })
// console.log('cleanArray', cleanArray)
// }
if (!errors.length) {
this.lastSuccessfulProgramMemory = execState.memory
this.lastSuccessfulOperations = execState.operations
@ -722,3 +875,379 @@ function setSelectionFilter(
})
.catch(reportError)
}
function flatternKCLVal(val: KclValue): any {
if (val.type === 'Number' || val.type === 'String') {
return val.value
} else if (val.type === 'Array') {
return val.value.map(flatternKCLVal)
} else if (val.type === 'Object') {
const obj: any = {}
Object.entries(val.value).forEach(([key, value]: [any, any]) => {
obj[key] = flatternKCLVal(value)
})
return obj
}
}
const yoTestFrontFrame = {
cutLumbers: [
{
angleRelevantWidth: 90,
depth: 35,
lengthBeforeAngles: 1715,
angle1: 0,
angle2: 0,
name: 'doorSupportUnderHeaderL',
id: 1,
timberLengthId: '1',
},
{
angleRelevantWidth: 90,
depth: 35,
lengthBeforeAngles: 1715,
angle1: 0,
angle2: 0,
name: 'doorSupportUnderHeaderR',
id: 2,
timberLengthId: '1',
},
{
angleRelevantWidth: 90,
depth: 35,
lengthBeforeAngles: 1544.3684870912048,
angle1: 0,
angle2: 40,
name: 'frontCornerStudL',
id: 3,
timberLengthId: '1',
},
{
angleRelevantWidth: 90,
depth: 35,
lengthBeforeAngles: 1544.3684870912048,
angle1: 0,
angle2: 40,
name: 'frontCornerStudL',
id: 4,
timberLengthId: '2',
},
{
angleRelevantWidth: 35,
depth: 90,
lengthBeforeAngles: 1770.9253875090703,
angle1: 0,
angle2: 40,
name: 'lDoorVertStud',
id: 5,
timberLengthId: '2',
},
{
angleRelevantWidth: 35,
depth: 90,
lengthBeforeAngles: 1770.9253875090703,
angle1: 0,
angle2: 40,
name: 'rDoorVertStud',
id: 6,
timberLengthId: '2',
},
{
angleRelevantWidth: 35,
depth: 90,
lengthBeforeAngles: 1515,
angle1: 0,
angle2: 40,
name: 'vertStudsBetweenSideAndDoor',
id: 7,
timberLengthId: '3',
},
{
angleRelevantWidth: 35,
depth: 90,
lengthBeforeAngles: 1515,
angle1: 0,
angle2: 40,
name: 'vertStudsBetweenSideAndDoor',
id: 8,
timberLengthId: '3',
},
{
angleRelevantWidth: 35,
depth: 90,
lengthBeforeAngles: 108.57232480920788,
angle1: 0,
angle2: 40,
name: 'vertStudsOverDoor',
id: 9,
timberLengthId: '1',
},
{
angleRelevantWidth: 35,
depth: 90,
lengthBeforeAngles: 108.57232480920788,
angle1: 0,
angle2: 40,
name: 'vertStudsOverDoor',
id: 10,
timberLengthId: '1',
},
{
angleRelevantWidth: 35,
depth: 90,
lengthBeforeAngles: 949.6869799080042,
angle1: 40,
angle2: 40,
name: 'backPitchedStud',
id: 11,
timberLengthId: '3',
},
{
angleRelevantWidth: 35,
depth: 90,
lengthBeforeAngles: 949.6869799080042,
angle1: 40,
angle2: 40,
name: 'backPitchedStud',
id: 12,
timberLengthId: '3',
},
{
angleRelevantWidth: 90,
depth: 35,
lengthBeforeAngles: 820,
angle1: 0,
angle2: 0,
name: 'doorHeader1',
id: 13,
timberLengthId: '3',
},
{
angleRelevantWidth: 90,
depth: 35,
lengthBeforeAngles: 820,
angle1: 0,
angle2: 0,
name: 'doorHeader2',
id: 14,
timberLengthId: '4',
},
{
angleRelevantWidth: 90,
depth: 35,
lengthBeforeAngles: 375,
angle1: 0,
angle2: 0,
name: 'footL',
id: 15,
timberLengthId: '1',
},
{
angleRelevantWidth: 90,
depth: 35,
lengthBeforeAngles: 375,
angle1: 0,
angle2: 0,
name: 'footR',
id: 16,
timberLengthId: '2',
},
],
offcutList: [
{
length: 308.7669656572259,
lastAngle: 0,
angleRelevantWidth: 90,
timberLengthId: '1',
},
{
length: 420.0608402575016,
lastAngle: 0,
angleRelevantWidth: 90,
timberLengthId: '2',
},
{
length: 115.40342621518221,
lastAngle: 0,
angleRelevantWidth: 90,
timberLengthId: '3',
},
{ length: 5177, lastAngle: 0, angleRelevantWidth: 90, timberLengthId: '4' },
],
lumbersToCut: [],
cutLumbersByTimberLengthId: {
'1': [
{
angleRelevantWidth: 90,
depth: 35,
lengthBeforeAngles: 1715,
angle1: 0,
angle2: 0,
name: 'doorSupportUnderHeaderL',
id: 1,
timberLengthId: '1',
},
{
angleRelevantWidth: 90,
depth: 35,
lengthBeforeAngles: 1715,
angle1: 0,
angle2: 0,
name: 'doorSupportUnderHeaderR',
id: 2,
timberLengthId: '1',
},
{
angleRelevantWidth: 90,
depth: 35,
lengthBeforeAngles: 1544.3684870912048,
angle1: 0,
angle2: 40,
name: 'frontCornerStudL',
id: 3,
timberLengthId: '1',
},
{
angleRelevantWidth: 35,
depth: 90,
lengthBeforeAngles: 108.57232480920788,
angle1: 0,
angle2: 40,
name: 'vertStudsOverDoor',
id: 9,
timberLengthId: '1',
},
{
angleRelevantWidth: 35,
depth: 90,
lengthBeforeAngles: 108.57232480920788,
angle1: 0,
angle2: 40,
name: 'vertStudsOverDoor',
id: 10,
timberLengthId: '1',
},
{
angleRelevantWidth: 90,
depth: 35,
lengthBeforeAngles: 375,
angle1: 0,
angle2: 0,
name: 'footL',
id: 15,
timberLengthId: '1',
},
],
'2': [
{
angleRelevantWidth: 90,
depth: 35,
lengthBeforeAngles: 1544.3684870912048,
angle1: 0,
angle2: 40,
name: 'frontCornerStudL',
id: 4,
timberLengthId: '2',
},
{
angleRelevantWidth: 35,
depth: 90,
lengthBeforeAngles: 1770.9253875090703,
angle1: 0,
angle2: 40,
name: 'lDoorVertStud',
id: 5,
timberLengthId: '2',
},
{
angleRelevantWidth: 35,
depth: 90,
lengthBeforeAngles: 1770.9253875090703,
angle1: 0,
angle2: 40,
name: 'rDoorVertStud',
id: 6,
timberLengthId: '2',
},
{
angleRelevantWidth: 90,
depth: 35,
lengthBeforeAngles: 375,
angle1: 0,
angle2: 0,
name: 'footR',
id: 16,
timberLengthId: '2',
},
],
'3': [
{
angleRelevantWidth: 35,
depth: 90,
lengthBeforeAngles: 1515,
angle1: 0,
angle2: 40,
name: 'vertStudsBetweenSideAndDoor',
id: 7,
timberLengthId: '3',
},
{
angleRelevantWidth: 35,
depth: 90,
lengthBeforeAngles: 1515,
angle1: 0,
angle2: 40,
name: 'vertStudsBetweenSideAndDoor',
id: 8,
timberLengthId: '3',
},
{
angleRelevantWidth: 35,
depth: 90,
lengthBeforeAngles: 949.6869799080042,
angle1: 40,
angle2: 40,
name: 'backPitchedStud',
id: 11,
timberLengthId: '3',
},
{
angleRelevantWidth: 35,
depth: 90,
lengthBeforeAngles: 949.6869799080042,
angle1: 40,
angle2: 40,
name: 'backPitchedStud',
id: 12,
timberLengthId: '3',
},
{
angleRelevantWidth: 90,
depth: 35,
lengthBeforeAngles: 820,
angle1: 0,
angle2: 0,
name: 'doorHeader1',
id: 13,
timberLengthId: '3',
},
],
'4': [
{
angleRelevantWidth: 90,
depth: 35,
lengthBeforeAngles: 820,
angle1: 0,
angle2: 0,
name: 'doorHeader2',
id: 14,
timberLengthId: '4',
},
],
},
}
;(window as any).removeChickenCoopCache = () => {
localStorage.removeItem(LOCALSTORAGE_KEY)
console.log('cache removed')
}

View File

@ -0,0 +1,672 @@
import {
createCutList,
LumberToCut,
Offcut,
STANDARD_TIMBER_LENGTH,
uuider,
} from './cutlist'
it('it should simple case where a second length is needed and it has to redo order to fit a shorter length towards the end on the first length of timber', () => {
const uuid = uuider()
let initialOffcutList: Offcut[] = [
{
length: STANDARD_TIMBER_LENGTH,
lastAngle: 0,
angleRelevantWidth: 90,
timberLengthId: uuid(),
},
]
let initialLumbersToCut: LumberToCut[] = [
{
angleRelevantWidth: 90,
depth: 35,
lengthBeforeAngles: 2000,
angle1: 0,
angle2: 0,
name: 'a',
},
{
angleRelevantWidth: 90,
depth: 35,
lengthBeforeAngles: 1500,
angle1: 0,
angle2: 45,
name: 'b',
},
{
angleRelevantWidth: 90,
depth: 35,
lengthBeforeAngles: 1000,
angle1: -50,
angle2: 0,
name: 'c',
},
{
angleRelevantWidth: 90,
depth: 35,
lengthBeforeAngles: 1500,
angle1: -49,
angle2: 0,
name: 'd',
},
]
const { offcutList, lumbersToCut, cutLumbersByTimberLengthId } =
createCutList({
lumbersToCut: initialLumbersToCut,
offcutList: initialOffcutList,
uuid: uuid,
})
expect(lumbersToCut).toEqual([])
expect(cutLumbersByTimberLengthId).toEqual({
'1': [
{
angleRelevantWidth: 90,
depth: 35,
lengthBeforeAngles: 2000,
angle1: 0,
angle2: 0,
name: 'a',
id: 1,
timberLengthId: '1',
},
{
angleRelevantWidth: 90,
depth: 35,
lengthBeforeAngles: 1500,
angle1: 0,
angle2: 45,
name: 'b',
id: 2,
timberLengthId: '1',
},
{
angleRelevantWidth: 90,
depth: 35,
lengthBeforeAngles: 1500,
angle1: -49,
angle2: 0,
name: 'd',
id: 3,
timberLengthId: '1',
},
],
'2': [
{
angleRelevantWidth: 90,
depth: 35,
lengthBeforeAngles: 1000,
angle1: -50,
angle2: 0,
name: 'c',
id: 4,
timberLengthId: '2',
},
],
})
expect(offcutList).toEqual([
{
length: 886.22420266299,
lastAngle: 0,
angleRelevantWidth: 90,
timberLengthId: '1',
},
{
length: 4888.0750051859395,
lastAngle: 50,
angleRelevantWidth: 90,
timberLengthId: '2',
},
])
})
it('backToBack trapazodal cuts with deep angles that should fit into a singleLength only because angles are preserved', () => {
const uuid = uuider()
let initialOffcutList: Offcut[] = [
{
length: STANDARD_TIMBER_LENGTH,
lastAngle: 0,
angleRelevantWidth: 90,
timberLengthId: uuid(),
},
]
const angleRelevantWidth = 90
const cutOverlap = Math.tan((67 * Math.PI) / 180) * angleRelevantWidth
// 25 is fudge factor to account for saw blade loss
// all three should fit with with a little bit of left over
const lengthOfEachCut = (STANDARD_TIMBER_LENGTH - 4 * cutOverlap - 45) / 3
let initialLumbersToCut: LumberToCut[] = [
{
angleRelevantWidth,
depth: 35,
lengthBeforeAngles: lengthOfEachCut,
angle1: 67,
angle2: -67, // positive and negative angles is what makes them trapezoidal // but it shouldn't matter
name: 'a',
},
{
angleRelevantWidth,
depth: 35,
lengthBeforeAngles: lengthOfEachCut,
angle1: 67,
angle2: -67, // positive and negative angles is what makes them trapezoidal // but it shouldn't matter
name: 'b',
},
{
angleRelevantWidth,
depth: 35,
lengthBeforeAngles: lengthOfEachCut,
angle1: 67,
angle2: -67, // positive and negative angles is what makes them trapezoidal // but it shouldn't matter
name: 'c',
},
]
const { offcutList, lumbersToCut, cutLumbersByTimberLengthId } =
createCutList({
lumbersToCut: initialLumbersToCut,
offcutList: initialOffcutList,
uuid,
})
expect(lumbersToCut).toEqual([])
expect(cutLumbersByTimberLengthId).toEqual({
'1': [
{
angleRelevantWidth: 90,
depth: 35,
lengthBeforeAngles: 1702.2977161011497,
angle1: 67,
angle2: -67,
name: 'a',
id: 1,
timberLengthId: '1',
},
{
angleRelevantWidth: 90,
depth: 35,
lengthBeforeAngles: 1702.2977161011497,
angle1: 67,
angle2: -67,
name: 'b',
id: 2,
timberLengthId: '1',
},
{
angleRelevantWidth: 90,
depth: 35,
lengthBeforeAngles: 1702.2977161011497,
angle1: 67,
angle2: -67,
name: 'c',
id: 3,
timberLengthId: '1',
},
],
})
expect(offcutList).toHaveLength(1)
expect(offcutList).toEqual([
{
length: 21.966258012773956,
lastAngle: 67,
angleRelevantWidth: 90,
timberLengthId: '1',
},
])
})
it('backToBack parallelagram cuts with deep angles that should fit into a singleLength only because angles are preserved', () => {
let initialOffcutList: Offcut[] = [
{
length: STANDARD_TIMBER_LENGTH,
lastAngle: 0,
angleRelevantWidth: 90,
timberLengthId: '0',
},
]
const sortedDescOffcutById = initialOffcutList.sort(
(a, b) => Number(b.timberLengthId) - Number(a.timberLengthId)
)
const maxId = Number(sortedDescOffcutById[0].timberLengthId)
const uuid = uuider(maxId)
const angleRelevantWidth = 90
const cutOverlap = Math.tan((67 * Math.PI) / 180) * angleRelevantWidth
// 25 is fudge factor to account for saw blade loss
// all three should fit with with a little bit of left over
const lengthOfEachCut = (STANDARD_TIMBER_LENGTH - 4 * cutOverlap - 45) / 3
let initialLumbersToCut: LumberToCut[] = [
{
angleRelevantWidth,
depth: 35,
lengthBeforeAngles: lengthOfEachCut,
angle1: 67,
angle2: 67,
name: 'a',
},
{
angleRelevantWidth,
depth: 35,
lengthBeforeAngles: lengthOfEachCut,
angle1: 67,
angle2: 67,
name: 'b',
},
{
angleRelevantWidth,
depth: 35,
lengthBeforeAngles: lengthOfEachCut,
angle1: 67,
angle2: 67,
name: 'c',
},
]
const { offcutList, lumbersToCut, cutLumbersByTimberLengthId } =
createCutList({
lumbersToCut: initialLumbersToCut,
offcutList: initialOffcutList,
uuid: uuid,
})
expect(lumbersToCut).toEqual([])
expect(cutLumbersByTimberLengthId).toEqual({
'0': [
{
angleRelevantWidth: 90,
depth: 35,
lengthBeforeAngles: 1702.2977161011497,
angle1: 67,
angle2: 67,
name: 'a',
id: 1,
timberLengthId: '0',
},
{
angleRelevantWidth: 90,
depth: 35,
lengthBeforeAngles: 1702.2977161011497,
angle1: 67,
angle2: 67,
name: 'b',
id: 2,
timberLengthId: '0',
},
{
angleRelevantWidth: 90,
depth: 35,
lengthBeforeAngles: 1702.2977161011497,
angle1: 67,
angle2: 67,
name: 'c',
timberLengthId: '0',
id: 3,
},
],
})
expect(offcutList).toHaveLength(1)
expect(offcutList).toEqual([
{
length: 21.966258012773956,
lastAngle: 67,
angleRelevantWidth: 90,
timberLengthId: '0',
},
])
})
it('Should work with cut list from an actual execution', () => {
let initialOffcutList: Offcut[] = [
{
length: STANDARD_TIMBER_LENGTH,
lastAngle: 0,
angleRelevantWidth: 90,
timberLengthId: '0',
},
]
const sortedDescOffcutById = initialOffcutList.sort(
(a, b) => Number(b.timberLengthId) - Number(a.timberLengthId)
)
const maxId = Number(sortedDescOffcutById[0].timberLengthId)
const uuid = uuider(maxId)
const angleRelevantWidth = 90
const cutOverlap = Math.tan((67 * Math.PI) / 180) * angleRelevantWidth
// 25 is fudge factor to account for saw blade loss
// all three should fit with with a little bit of left over
const lengthOfEachCut = (STANDARD_TIMBER_LENGTH - 4 * cutOverlap - 45) / 3
let initialLumbersToCut: LumberToCut[] = [
{
angleRelevantWidth: 35,
depth: 90,
lengthBeforeAngles: 949.6869799080042,
angle1: 40,
angle2: 40,
name: 'backPitchedStud',
},
{
angleRelevantWidth: 35,
depth: 90,
lengthBeforeAngles: 949.6869799080042,
angle1: 40,
angle2: 40,
name: 'backPitchedStud',
},
{
angleRelevantWidth: 90,
depth: 35,
lengthBeforeAngles: 1544.3684870912048,
angle1: 0,
angle2: 40,
name: 'frontCornerStudL',
},
{
angleRelevantWidth: 90,
depth: 35,
lengthBeforeAngles: 1544.3684870912048,
angle1: 0,
angle2: 40,
name: 'frontCornerStudL',
},
{
angleRelevantWidth: 35,
depth: 90,
lengthBeforeAngles: 1770.9253875090703,
angle1: 0,
angle2: 40,
name: 'lDoorVertStud',
},
{
angleRelevantWidth: 35,
depth: 90,
lengthBeforeAngles: 1770.9253875090703,
angle1: 0,
angle2: 40,
name: 'rDoorVertStud',
},
{
angleRelevantWidth: 90,
depth: 35,
lengthBeforeAngles: 375,
angle1: 0,
angle2: 0,
name: 'footL',
},
{
angleRelevantWidth: 90,
depth: 35,
lengthBeforeAngles: 375,
angle1: 0,
angle2: 0,
name: 'footR',
},
{
angleRelevantWidth: 90,
depth: 35,
lengthBeforeAngles: 1715,
angle1: 0,
angle2: 0,
name: 'doorSupportUnderHeaderL',
},
{
angleRelevantWidth: 90,
depth: 35,
lengthBeforeAngles: 1715,
angle1: 0,
angle2: 0,
name: 'doorSupportUnderHeaderR',
},
{
angleRelevantWidth: 90,
depth: 35,
lengthBeforeAngles: 820,
angle1: 0,
angle2: 0,
name: 'doorHeader1',
},
{
angleRelevantWidth: 90,
depth: 35,
lengthBeforeAngles: 820,
angle1: 0,
angle2: 0,
name: 'doorHeader2',
},
{
angleRelevantWidth: 35,
depth: 90,
lengthBeforeAngles: 1515,
angle1: 0,
angle2: 40,
name: 'vertStudsBetweenSideAndDoor',
},
{
angleRelevantWidth: 35,
depth: 90,
lengthBeforeAngles: 1515,
angle1: 0,
angle2: 40,
name: 'vertStudsBetweenSideAndDoor',
},
{
angleRelevantWidth: 35,
depth: 90,
lengthBeforeAngles: 108.57232480920788,
angle1: 0,
angle2: 40,
name: 'vertStudsOverDoor',
},
{
angleRelevantWidth: 35,
depth: 90,
lengthBeforeAngles: 108.57232480920788,
angle1: 0,
angle2: 40,
name: 'vertStudsOverDoor',
},
]
const { offcutList, lumbersToCut, cutLumbersByTimberLengthId } =
createCutList({
lumbersToCut: initialLumbersToCut,
offcutList: initialOffcutList,
uuid: uuid,
})
expect(lumbersToCut).toEqual([])
expect(cutLumbersByTimberLengthId).toEqual({
'0': [
{
angleRelevantWidth: 90,
depth: 35,
lengthBeforeAngles: 1715,
angle1: 0,
angle2: 0,
name: 'doorSupportUnderHeaderL',
id: 1,
timberLengthId: '0',
},
{
angleRelevantWidth: 90,
depth: 35,
lengthBeforeAngles: 1715,
angle1: 0,
angle2: 0,
name: 'doorSupportUnderHeaderR',
id: 2,
timberLengthId: '0',
},
{
angleRelevantWidth: 90,
depth: 35,
lengthBeforeAngles: 1544.3684870912048,
angle1: 0,
angle2: 40,
name: 'frontCornerStudL',
id: 3,
timberLengthId: '0',
},
{
angleRelevantWidth: 35,
depth: 90,
lengthBeforeAngles: 108.57232480920788,
angle1: 0,
angle2: 40,
name: 'vertStudsOverDoor',
id: 9,
timberLengthId: '0',
},
{
angleRelevantWidth: 35,
depth: 90,
lengthBeforeAngles: 108.57232480920788,
angle1: 0,
angle2: 40,
name: 'vertStudsOverDoor',
id: 10,
timberLengthId: '0',
},
{
angleRelevantWidth: 90,
depth: 35,
lengthBeforeAngles: 375,
angle1: 0,
angle2: 0,
name: 'footL',
id: 15,
timberLengthId: '0',
},
],
'1': [
{
angleRelevantWidth: 90,
depth: 35,
lengthBeforeAngles: 1544.3684870912048,
angle1: 0,
angle2: 40,
name: 'frontCornerStudL',
id: 4,
timberLengthId: '1',
},
{
angleRelevantWidth: 35,
depth: 90,
lengthBeforeAngles: 1770.9253875090703,
angle1: 0,
angle2: 40,
name: 'lDoorVertStud',
id: 5,
timberLengthId: '1',
},
{
angleRelevantWidth: 35,
depth: 90,
lengthBeforeAngles: 1770.9253875090703,
angle1: 0,
angle2: 40,
name: 'rDoorVertStud',
id: 6,
timberLengthId: '1',
},
{
angleRelevantWidth: 90,
depth: 35,
lengthBeforeAngles: 375,
angle1: 0,
angle2: 0,
name: 'footR',
id: 16,
timberLengthId: '1',
},
],
'2': [
{
angleRelevantWidth: 35,
depth: 90,
lengthBeforeAngles: 1515,
angle1: 0,
angle2: 40,
name: 'vertStudsBetweenSideAndDoor',
id: 7,
timberLengthId: '2',
},
{
angleRelevantWidth: 35,
depth: 90,
lengthBeforeAngles: 1515,
angle1: 0,
angle2: 40,
name: 'vertStudsBetweenSideAndDoor',
id: 8,
timberLengthId: '2',
},
{
angleRelevantWidth: 35,
depth: 90,
lengthBeforeAngles: 949.6869799080042,
angle1: 40,
angle2: 40,
name: 'backPitchedStud',
id: 11,
timberLengthId: '2',
},
{
angleRelevantWidth: 35,
depth: 90,
lengthBeforeAngles: 949.6869799080042,
angle1: 40,
angle2: 40,
name: 'backPitchedStud',
id: 12,
timberLengthId: '2',
},
{
angleRelevantWidth: 90,
depth: 35,
lengthBeforeAngles: 820,
angle1: 0,
angle2: 0,
name: 'doorHeader1',
id: 13,
timberLengthId: '2',
},
],
'3': [
{
angleRelevantWidth: 90,
depth: 35,
lengthBeforeAngles: 820,
angle1: 0,
angle2: 0,
name: 'doorHeader2',
id: 14,
timberLengthId: '3',
},
],
})
expect(offcutList).toHaveLength(4)
expect(offcutList).toEqual([
{
length: 308.7669656572259,
lastAngle: 0,
angleRelevantWidth: 90,
timberLengthId: '0',
},
{
length: 420.0608402575016,
lastAngle: 0,
angleRelevantWidth: 90,
timberLengthId: '1',
},
{
length: 115.40342621518221,
lastAngle: 0,
angleRelevantWidth: 90,
timberLengthId: '2',
},
{ length: 5177, lastAngle: 0, angleRelevantWidth: 90, timberLengthId: '3' },
])
})

256
src/lang/std/cutlist.ts Normal file
View File

@ -0,0 +1,256 @@
export const SAW_BLADE_THICKNESS = 3 // 3mm loss per cut from saw blade on 90 degree cuts
export function uuider(start = 0) {
let __uuid_cntr = start
return () => {
// but it's very fake
__uuid_cntr++
return String(__uuid_cntr)
}
}
export type Offcut = {
length: number
lastAngle: number
angleRelevantWidth: number
timberLengthId: string
}
export type LumberToCut = {
angleRelevantWidth: number
depth: number
lengthBeforeAngles: number
angle1: number
angle2: number
name: string
}
export type CutLumber = LumberToCut & {
id: number
timberLengthId: string
}
export const STANDARD_TIMBER_LENGTH = 6_000
export function createCutList({
lumbersToCut,
offcutList,
uuid,
}: {
lumbersToCut: LumberToCut[]
offcutList: Offcut[]
uuid: () => string
}) {
const uniqueAngles = new Set<number>()
lumbersToCut.forEach((lumber) => {
uniqueAngles.add(lumber.angle1)
uniqueAngles.add(lumber.angle2)
})
let cutLumbers: CutLumber[] = []
const lumberGetter = (
lumbersToCut: LumberToCut[],
lastOffcut: Offcut
): {
lumber: LumberToCut
newLumbersToCut: LumberToCut[]
} => {
// let longestLumberOfAngle: LumberToCut | undefined
let longestLumberOfAngleIndex = -1
lumbersToCut.forEach((lumberToCut, index) => {
if (
(Math.abs(lumberToCut.angle1) === Math.abs(lastOffcut.lastAngle) ||
Math.abs(lumberToCut.angle2) === Math.abs(lastOffcut.lastAngle)) &&
lastOffcut.angleRelevantWidth === lumberToCut.angleRelevantWidth
) {
if (longestLumberOfAngleIndex === -1) {
longestLumberOfAngleIndex = index
} else {
const currentLongest = lumbersToCut[longestLumberOfAngleIndex]
if (
currentLongest.lengthBeforeAngles < lumberToCut.lengthBeforeAngles
) {
longestLumberOfAngleIndex = index
}
}
}
})
if (longestLumberOfAngleIndex !== -1) {
return {
lumber: lumbersToCut[longestLumberOfAngleIndex],
newLumbersToCut: lumbersToCut.filter(
(_, index) => index !== longestLumberOfAngleIndex
),
}
}
// didn't find lumber with matching angle,
// so just get the longest lumber with whatever angle it happens to be
let longestLumberIndex = 0
lumbersToCut.forEach((lumberToCut, index) => {
if (
lumberToCut.lengthBeforeAngles >
lumbersToCut[longestLumberIndex].lengthBeforeAngles
) {
longestLumberIndex = index
}
})
return {
lumber: lumbersToCut[longestLumberIndex],
newLumbersToCut: lumbersToCut.filter(
(_, index) => index !== longestLumberIndex
),
}
}
const cutLumberOutOfOffcuts = (
lumber: LumberToCut,
offcuts: Offcut[],
lastId: number
): {
cutLumber: CutLumber
newOffcuts: Offcut[]
} => {
const doCalcsForGivenOffcutAndCurrentLumber = (
offcut: Offcut,
lumber: LumberToCut
) => {
const getOverlap = (timberWidth: number, angleD: number) =>
Math.abs(Math.tan((Math.abs(angleD) * Math.PI) / 180) * timberWidth)
let angleThatMatchesClosestToOffcut = 0
let endCutAngle = 0
if (
Math.abs(Math.abs(offcut.lastAngle) - Math.abs(lumber.angle1)) <
Math.abs(Math.abs(offcut.lastAngle) - Math.abs(lumber.angle2))
) {
angleThatMatchesClosestToOffcut = Math.abs(lumber.angle1)
endCutAngle = Math.abs(lumber.angle2)
} else {
angleThatMatchesClosestToOffcut = Math.abs(lumber.angle2)
endCutAngle = Math.abs(lumber.angle1)
}
const offCutOverLapZon = getOverlap(
offcut.angleRelevantWidth,
offcut.lastAngle
)
const lumberOverLapZon = getOverlap(
lumber.angleRelevantWidth,
angleThatMatchesClosestToOffcut
)
let overLapZoneBuffer = 0
if (lumberOverLapZon - offCutOverLapZon > 0) {
overLapZoneBuffer = lumberOverLapZon - offCutOverLapZon
}
const extraNeededAtOtherEndOfCut = getOverlap(
lumber.angleRelevantWidth,
endCutAngle
)
const sawCutLoss = Math.abs(
SAW_BLADE_THICKNESS / Math.cos((Math.abs(endCutAngle) * Math.PI) / 180)
)
const minimumLengthNeeded =
lumber.lengthBeforeAngles +
overLapZoneBuffer +
extraNeededAtOtherEndOfCut
return {
minimumLengthNeeded,
lengthBeforeAngles: lumber.lengthBeforeAngles,
overLapZoneBuffer,
extraNeededAtOtherEndOfCut,
sawCutLoss,
endCutAngle: Math.abs(endCutAngle),
}
}
const findIndexFn = (offcut: Offcut) => {
const { minimumLengthNeeded } = doCalcsForGivenOffcutAndCurrentLumber(
offcut,
lumber
)
if (offcut.length >= minimumLengthNeeded) {
return true
}
// none of the offcuts are big enough
// need to add a new full length of timber as an offcut and try again.
return false
}
let offCutToUseIndex = offcuts.findIndex(findIndexFn)
let newOffcuts = offcuts
if (offCutToUseIndex === -1) {
const lastOffcut = offcuts[offcuts.length - 1]
const newOffcut: Offcut = {
length: STANDARD_TIMBER_LENGTH,
lastAngle: 0,
angleRelevantWidth: 90,
timberLengthId: `${Number(lastOffcut.timberLengthId) + 1}`,
}
console.log('last and new offcut', offcuts[offcuts.length - 1], newOffcut)
newOffcuts = [...offcuts, newOffcut]
offCutToUseIndex = newOffcuts.length - 1
}
if (offCutToUseIndex === -1) {
console.warn('offCutToUseIndex === -1')
throw new Error('timber does not fit in any offcuts, even a fresh one')
}
const {
lengthBeforeAngles,
overLapZoneBuffer,
extraNeededAtOtherEndOfCut,
sawCutLoss,
endCutAngle,
} = doCalcsForGivenOffcutAndCurrentLumber(
newOffcuts[offCutToUseIndex],
lumber
)
const updatedOffcut: Offcut = {
length:
newOffcuts[offCutToUseIndex].length -
lengthBeforeAngles -
overLapZoneBuffer -
extraNeededAtOtherEndOfCut -
sawCutLoss,
lastAngle: Math.abs(endCutAngle),
angleRelevantWidth: lumber.angleRelevantWidth,
timberLengthId: newOffcuts[offCutToUseIndex].timberLengthId,
}
newOffcuts[offCutToUseIndex] = updatedOffcut
const cutLumber: CutLumber = {
...lumber,
id: lastId + 1,
timberLengthId: newOffcuts[offCutToUseIndex].timberLengthId,
}
return {
cutLumber,
newOffcuts,
}
}
let currentId = 0
while (lumbersToCut.length > 0) {
const lastOffcut = offcutList[offcutList.length - 1]
const result = lumberGetter(lumbersToCut, lastOffcut)
lumbersToCut = result.newLumbersToCut
const lumber = result.lumber
const { cutLumber, newOffcuts } = cutLumberOutOfOffcuts(
lumber,
offcutList,
currentId
)
offcutList = newOffcuts
cutLumbers = [...cutLumbers, cutLumber]
currentId = cutLumber.id
}
// group cutLumbers by timberLengthId
const cutLumbersByTimberLengthId: Record<string, CutLumber[]> = {}
cutLumbers.forEach((cutLumber) => {
if (!cutLumbersByTimberLengthId[cutLumber.timberLengthId]) {
cutLumbersByTimberLengthId[cutLumber.timberLengthId] = []
}
cutLumbersByTimberLengthId[cutLumber.timberLengthId].push(cutLumber)
})
return { cutLumbers, offcutList, lumbersToCut, cutLumbersByTimberLengthId }
}

View File

@ -35,7 +35,7 @@ import { Configuration } from 'wasm-lib/kcl/bindings/Configuration'
import { DeepPartial } from 'lib/types'
import { ProjectConfiguration } from 'wasm-lib/kcl/bindings/ProjectConfiguration'
import { Sketch } from '../wasm-lib/kcl/bindings/Sketch'
import { ExecState as RawExecState } from '../wasm-lib/kcl/bindings/ExecState'
import { ExecOutcome } from 'wasm-lib/kcl/bindings/ExecOutcome'
import { ProgramMemory as RawProgramMemory } from '../wasm-lib/kcl/bindings/ProgramMemory'
import { EnvironmentRef } from '../wasm-lib/kcl/bindings/EnvironmentRef'
import { Environment } from '../wasm-lib/kcl/bindings/Environment'
@ -260,10 +260,10 @@ export function emptyExecState(): ExecState {
}
}
function execStateFromRaw(raw: RawExecState): ExecState {
function execStateFromRust(execOutcome: ExecOutcome): ExecState {
return {
memory: ProgramMemory.fromRaw(raw.modLocal.memory),
operations: raw.modLocal.operations,
memory: ProgramMemory.fromRaw(execOutcome.memory),
operations: execOutcome.operations,
}
}
@ -535,14 +535,14 @@ export const _executor = async (
jsAppSettings = getAllCurrentSettings(lastSettingsSnapshot)
}
}
const execState: RawExecState = await execute(
const execOutcome: ExecOutcome = await execute(
JSON.stringify(node),
JSON.stringify(programMemoryOverride?.toRaw() || null),
JSON.stringify({ settings: jsAppSettings }),
engineCommandManager,
fileSystemManager
)
return execStateFromRaw(execState)
return execStateFromRust(execOutcome)
} catch (e: any) {
console.log(e)
const parsed: KclErrorWithOutputs = JSON.parse(e.toString())

View File

@ -94,6 +94,18 @@ pub struct ModuleState {
pub settings: MetaSettings,
}
/// Outcome from executing a program. This is used in WebAssembly/WASM.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub struct ExecOutcome {
/// Program variable bindings of the top-level module.
pub memory: ProgramMemory,
/// Operations that have been performed in execution order, for display in
/// the Feature Tree.
pub operations: Vec<Operation>,
}
impl Default for ExecState {
fn default() -> Self {
Self::new()
@ -123,6 +135,25 @@ impl ExecState {
};
}
/// Convert to an output for WebAssembly/WASM.
pub fn to_wasm_outcome(mut self) -> ExecOutcome {
// Clear closed-over program memory inside closures. This is
// destructive. It greatly reduces the amount of data crossing the WASM
// boundary that we won't ever use.
for env in self.mut_memory().environments.iter_mut() {
for (_, value) in env.bindings.iter_mut() {
if let KclValue::Function { memory, .. } = value {
*memory = Box::new(ProgramMemory::new());
}
}
}
ExecOutcome {
memory: self.mod_local.memory,
operations: self.mod_local.operations,
}
}
pub fn memory(&self) -> &ProgramMemory {
&self.mod_local.memory
}

View File

@ -133,7 +133,7 @@ pub async fn execute(
// gloo-serialize crate instead.
// DO NOT USE serde_wasm_bindgen::to_value(&exec_state).map_err(|e| e.to_string())
// it will break the frontend.
JsValue::from_serde(&exec_state).map_err(|e| e.to_string())
JsValue::from_serde(&exec_state.to_wasm_outcome()).map_err(|e| e.to_string())
}
// wasm_bindgen wrapper for execute