feat: model advanced sketch curves
This commit is contained in:
@@ -5,6 +5,8 @@ export type SketchGeometry =
|
||||
| { id: string; type: 'line'; start: SketchPoint; end: SketchPoint; construction?: boolean }
|
||||
| { id: string; type: 'circle'; center: SketchPoint; radius: number; construction?: boolean }
|
||||
| { id: string; type: 'arc'; center: SketchPoint; radius: number; startAngle: number; endAngle: number; construction?: boolean }
|
||||
| { id: string; type: 'ellipse'; center: SketchPoint; majorRadius: number; minorRadius: number; rotation: number; construction?: boolean }
|
||||
| { id: string; type: 'bspline'; degree: number; controlPoints: SketchPoint[]; weights?: number[]; knots?: number[]; periodic?: boolean; construction?: boolean }
|
||||
|
||||
export type SketchPointRef = { geometryId: string; point: 'start' | 'end' | 'center' | 'position' }
|
||||
|
||||
@@ -23,7 +25,8 @@ export type SketchConstraint =
|
||||
export type SketchSolverStatus = 'solved' | 'under-constrained' | 'conflicting' | 'invalid'
|
||||
|
||||
export type SketchDiagnostic = {
|
||||
code: 'UNKNOWN_GEOMETRY' | 'UNKNOWN_POINT' | 'INVALID_VALUE' | 'CONSTRAINT_CONFLICT' | 'SOLVER_NOT_CONVERGED'
|
||||
code: 'UNKNOWN_GEOMETRY' | 'UNKNOWN_POINT' | 'INVALID_VALUE' | 'UNSUPPORTED_GEOMETRY' | 'CONSTRAINT_CONFLICT' | 'SOLVER_NOT_CONVERGED'
|
||||
geometryId?: string
|
||||
constraintId?: string
|
||||
message: string
|
||||
}
|
||||
@@ -57,22 +60,28 @@ export type SketchSolveResult = {
|
||||
|
||||
const cloneGeometry = (geometry: SketchGeometry): SketchGeometry => {
|
||||
if (geometry.type === 'line') return { ...geometry, start: { ...geometry.start }, end: { ...geometry.end } }
|
||||
if (geometry.type === 'circle') return { ...geometry, center: { ...geometry.center } }
|
||||
if (geometry.type === 'arc') return { ...geometry, center: { ...geometry.center } }
|
||||
if (geometry.type === 'circle' || geometry.type === 'arc' || geometry.type === 'ellipse') return { ...geometry, center: { ...geometry.center } }
|
||||
if (geometry.type === 'bspline') return { ...geometry, controlPoints: geometry.controlPoints.map((point) => ({ ...point })), weights: geometry.weights ? [...geometry.weights] : undefined, knots: geometry.knots ? [...geometry.knots] : undefined }
|
||||
return { ...geometry, position: { ...geometry.position } }
|
||||
}
|
||||
|
||||
const cloneConstraint = (constraint: SketchConstraint): SketchConstraint => {
|
||||
if (constraint.type === 'coincident' || constraint.type === 'distance' || constraint.type === 'distanceX' || constraint.type === 'distanceY') return { ...constraint, first: { ...constraint.first }, second: { ...constraint.second } }
|
||||
if (constraint.type === 'symmetric') return { ...constraint, first: { ...constraint.first }, second: { ...constraint.second }, center: { ...constraint.center } }
|
||||
return { ...constraint }
|
||||
}
|
||||
|
||||
export const cloneSketch = (sketch: SketchSnapshot): SketchSnapshot => ({
|
||||
...sketch,
|
||||
geometry: sketch.geometry.map(cloneGeometry),
|
||||
constraints: sketch.constraints.map((constraint) => ({ ...constraint })),
|
||||
constraints: sketch.constraints.map(cloneConstraint),
|
||||
solver: { ...sketch.solver, diagnostics: sketch.solver.diagnostics.map((diagnostic) => ({ ...diagnostic })) },
|
||||
})
|
||||
|
||||
export const createSketch = (id: string, geometry: SketchGeometry[] = [], constraints: SketchConstraint[] = []): SketchSnapshot => ({
|
||||
id,
|
||||
geometry: geometry.map(cloneGeometry),
|
||||
constraints: constraints.map((constraint) => ({ ...constraint })),
|
||||
constraints: constraints.map(cloneConstraint),
|
||||
solver: { status: geometry.length === 0 ? 'solved' : 'under-constrained', degreesOfFreedom: 0, residual: 0, iterations: 0, diagnostics: [] },
|
||||
})
|
||||
|
||||
@@ -85,7 +94,7 @@ const findGeometry = (geometry: SketchGeometry[], id: string, constraintId: stri
|
||||
const pointFor = (geometry: SketchGeometry, point: SketchPointRef['point'], constraintId: string, diagnostics: SketchDiagnostic[]): SketchPoint | null => {
|
||||
if (geometry.type === 'point' && point === 'position') return geometry.position
|
||||
if (geometry.type === 'line' && (point === 'start' || point === 'end')) return point === 'start' ? geometry.start : geometry.end
|
||||
if ((geometry.type === 'circle' || geometry.type === 'arc') && point === 'center') return geometry.center
|
||||
if ((geometry.type === 'circle' || geometry.type === 'arc' || geometry.type === 'ellipse') && point === 'center') return geometry.center
|
||||
diagnostics.push({ code: 'UNKNOWN_POINT', constraintId, message: `Point '${point}' is not valid for ${geometry.type} '${geometry.id}'.` })
|
||||
return null
|
||||
}
|
||||
@@ -109,7 +118,7 @@ const adjustPoint = (geometry: SketchGeometry, point: SketchPointRef['point'], n
|
||||
if (isBlocked(geometry.id, blocked)) return
|
||||
if (geometry.type === 'point') { geometry.position = { ...next }; return }
|
||||
if (geometry.type === 'line') { if (point === 'start') geometry.start = { ...next }; else if (point === 'end') geometry.end = { ...next }; return }
|
||||
if (point === 'center') geometry.center = { ...next }
|
||||
if (point === 'center' && (geometry.type === 'circle' || geometry.type === 'arc' || geometry.type === 'ellipse')) geometry.center = { ...next }
|
||||
}
|
||||
|
||||
const validateConstraintValues = (constraint: SketchConstraint, diagnostics: SketchDiagnostic[]) => {
|
||||
@@ -178,6 +187,9 @@ export const solveSketch = (input: SketchSnapshot, options: SketchSolveOptions =
|
||||
const maxIterations = options.maxIterations ?? 64
|
||||
const snapshot = cloneSketch(input)
|
||||
const diagnostics: SketchDiagnostic[] = []
|
||||
for (const geometry of snapshot.geometry) {
|
||||
if (geometry.type === 'ellipse' || geometry.type === 'bspline') diagnostics.push({ code: 'UNSUPPORTED_GEOMETRY', geometryId: geometry.id, message: `The typescript-basic solver does not solve ${geometry.type} geometry '${geometry.id}'.` })
|
||||
}
|
||||
const geometryById = new Map(snapshot.geometry.map((geometry) => [geometry.id, geometry]))
|
||||
const blocked = new Set(snapshot.constraints.filter((constraint) => constraint.type === 'block').map((constraint) => constraint.geometryId))
|
||||
snapshot.constraints.forEach((constraint) => validateConstraintValues(constraint, diagnostics))
|
||||
@@ -275,7 +287,13 @@ export const solveSketch = (input: SketchSnapshot, options: SketchSolveOptions =
|
||||
}
|
||||
residual = Math.max(0, ...snapshot.constraints.map((constraint) => residualFor(constraint, snapshot.geometry, diagnostics)))
|
||||
}
|
||||
const variableCount = snapshot.geometry.reduce((count, geometry) => count + (geometry.type === 'point' ? 2 : geometry.type === 'line' ? 4 : geometry.type === 'circle' ? 3 : 5), 0)
|
||||
const variableCount = snapshot.geometry.reduce((count, geometry) => {
|
||||
if (geometry.type === 'point') return count + 2
|
||||
if (geometry.type === 'line') return count + 4
|
||||
if (geometry.type === 'circle') return count + 3
|
||||
if (geometry.type === 'arc' || geometry.type === 'ellipse') return count + 5
|
||||
return count + geometry.controlPoints.length * 2 + (geometry.weights?.length ?? 0)
|
||||
}, 0)
|
||||
const rank = Math.min(variableCount, snapshot.constraints.filter((constraint) => constraint.type !== 'block' || !isBlocked(constraint.geometryId, blocked)).length + blocked.size * 2)
|
||||
const degreesOfFreedom = Math.max(0, variableCount - rank)
|
||||
const status: SketchSolverStatus = diagnostics.length > 0 ? 'invalid' : residual <= tolerance ? degreesOfFreedom === 0 ? 'solved' : 'under-constrained' : 'conflicting'
|
||||
|
||||
Reference in New Issue
Block a user