P4: extend sketch constraints and expression functions
This commit is contained in:
@@ -6,15 +6,18 @@ export type SketchGeometry =
|
||||
| { id: string; type: 'circle'; center: SketchPoint; radius: number; construction?: boolean }
|
||||
| { id: string; type: 'arc'; center: SketchPoint; radius: number; startAngle: number; endAngle: number; construction?: boolean }
|
||||
|
||||
export type SketchPointRef = { geometryId: string; point: 'start' | 'end' | 'center' }
|
||||
export type SketchPointRef = { geometryId: string; point: 'start' | 'end' | 'center' | 'position' }
|
||||
|
||||
export type SketchConstraint =
|
||||
| { id: string; type: 'coincident'; first: SketchPointRef; second: SketchPointRef; driving?: boolean }
|
||||
| { id: string; type: 'horizontal' | 'vertical'; geometryId: string; driving?: boolean }
|
||||
| { id: string; type: 'distance' | 'distanceX' | 'distanceY'; first: SketchPointRef; second: SketchPointRef; value: number; driving?: boolean }
|
||||
| { id: string; type: 'radius'; geometryId: string; value: number; driving?: boolean }
|
||||
| { id: string; type: 'diameter'; geometryId: string; value: number; driving?: boolean }
|
||||
| { id: string; type: 'angle'; geometryId: string; value: number; driving?: boolean }
|
||||
| { id: string; type: 'equal'; firstGeometryId: string; secondGeometryId: string; driving?: boolean }
|
||||
| { id: string; type: 'symmetric'; first: SketchPointRef; second: SketchPointRef; center: SketchPointRef; driving?: boolean }
|
||||
| { id: string; type: 'tangent'; firstGeometryId: string; secondGeometryId: string; driving?: boolean }
|
||||
| { id: string; type: 'block'; geometryId: string; driving?: boolean }
|
||||
|
||||
export type SketchSolverStatus = 'solved' | 'under-constrained' | 'conflicting' | 'invalid'
|
||||
@@ -80,7 +83,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') return geometry.position
|
||||
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
|
||||
diagnostics.push({ code: 'UNKNOWN_POINT', constraintId, message: `Point '${point}' is not valid for ${geometry.type} '${geometry.id}'.` })
|
||||
@@ -91,6 +94,15 @@ const pointKey = (ref: SketchPointRef) => `${ref.geometryId}.${ref.point}`
|
||||
const distance = (left: SketchPoint, right: SketchPoint) => Math.hypot(left.x - right.x, left.y - right.y)
|
||||
const lineLength = (geometry: SketchGeometry) => geometry.type === 'line' ? distance(geometry.start, geometry.end) : geometry.type === 'circle' || geometry.type === 'arc' ? geometry.radius : 0
|
||||
|
||||
const lineCircleTangentResidual = (line: SketchGeometry, circle: SketchGeometry) => {
|
||||
if (line.type !== 'line' || (circle.type !== 'circle' && circle.type !== 'arc')) return Infinity
|
||||
const dx = line.end.x - line.start.x
|
||||
const dy = line.end.y - line.start.y
|
||||
const length = Math.hypot(dx, dy)
|
||||
if (length === 0) return Infinity
|
||||
return Math.abs(((circle.center.x - line.start.x) * dy - (circle.center.y - line.start.y) * dx) / length) - circle.radius
|
||||
}
|
||||
|
||||
const isBlocked = (geometryId: string, blocked: Set<string>) => blocked.has(geometryId)
|
||||
|
||||
const adjustPoint = (geometry: SketchGeometry, point: SketchPointRef['point'], next: SketchPoint, blocked: Set<string>) => {
|
||||
@@ -114,6 +126,10 @@ const residualFor = (constraint: SketchConstraint, geometry: SketchGeometry[], d
|
||||
const candidate = findGeometry(geometry, constraint.geometryId, constraint.id, diagnostics)
|
||||
return candidate && (candidate.type === 'circle' || candidate.type === 'arc') ? Math.abs(candidate.radius - constraint.value) : Infinity
|
||||
}
|
||||
if (constraint.type === 'diameter') {
|
||||
const candidate = findGeometry(geometry, constraint.geometryId, constraint.id, diagnostics)
|
||||
return candidate && (candidate.type === 'circle' || candidate.type === 'arc') ? Math.abs(candidate.radius * 2 - constraint.value) : Infinity
|
||||
}
|
||||
if (constraint.type === 'angle') {
|
||||
const candidate = findGeometry(geometry, constraint.geometryId, constraint.id, diagnostics)
|
||||
if (!candidate || candidate.type !== 'line') return Infinity
|
||||
@@ -124,6 +140,25 @@ const residualFor = (constraint: SketchConstraint, geometry: SketchGeometry[], d
|
||||
const second = findGeometry(geometry, constraint.secondGeometryId, constraint.id, diagnostics)
|
||||
return first && second ? Math.abs(lineLength(first) - lineLength(second)) : Infinity
|
||||
}
|
||||
if (constraint.type === 'symmetric') {
|
||||
const firstGeometry = findGeometry(geometry, constraint.first.geometryId, constraint.id, diagnostics)
|
||||
const secondGeometry = findGeometry(geometry, constraint.second.geometryId, constraint.id, diagnostics)
|
||||
const centerGeometry = findGeometry(geometry, constraint.center.geometryId, constraint.id, diagnostics)
|
||||
if (!firstGeometry || !secondGeometry || !centerGeometry) return Infinity
|
||||
const first = pointFor(firstGeometry, constraint.first.point, constraint.id, diagnostics)
|
||||
const second = pointFor(secondGeometry, constraint.second.point, constraint.id, diagnostics)
|
||||
const center = pointFor(centerGeometry, constraint.center.point, constraint.id, diagnostics)
|
||||
return first && second && center ? Math.hypot((first.x + second.x) / 2 - center.x, (first.y + second.y) / 2 - center.y) : Infinity
|
||||
}
|
||||
if (constraint.type === 'tangent') {
|
||||
const first = findGeometry(geometry, constraint.firstGeometryId, constraint.id, diagnostics)
|
||||
const second = findGeometry(geometry, constraint.secondGeometryId, constraint.id, diagnostics)
|
||||
if (!first || !second) return Infinity
|
||||
if (first.type === 'line' && (second.type === 'circle' || second.type === 'arc')) return Math.abs(lineCircleTangentResidual(first, second))
|
||||
if (second.type === 'line' && (first.type === 'circle' || first.type === 'arc')) return Math.abs(lineCircleTangentResidual(second, first))
|
||||
if ((first.type === 'circle' || first.type === 'arc') && (second.type === 'circle' || second.type === 'arc')) return Math.abs(distance(first.center, second.center) - first.radius - second.radius)
|
||||
return Infinity
|
||||
}
|
||||
if (constraint.type === 'block') return 0
|
||||
if (constraint.type !== 'coincident' && constraint.type !== 'distance' && constraint.type !== 'distanceX' && constraint.type !== 'distanceY') return Infinity
|
||||
const firstGeometry = findGeometry(geometry, constraint.first.geometryId, constraint.id, diagnostics)
|
||||
@@ -162,6 +197,10 @@ export const solveSketch = (input: SketchSnapshot, options: SketchSolveOptions =
|
||||
const candidate = geometryById.get(constraint.geometryId)
|
||||
if (!candidate || (candidate.type !== 'circle' && candidate.type !== 'arc')) { findGeometry(snapshot.geometry, constraint.geometryId, constraint.id, diagnostics); continue }
|
||||
if (!isBlocked(candidate.id, blocked)) candidate.radius = constraint.value
|
||||
} else if (constraint.type === 'diameter') {
|
||||
const candidate = geometryById.get(constraint.geometryId)
|
||||
if (!candidate || (candidate.type !== 'circle' && candidate.type !== 'arc')) { findGeometry(snapshot.geometry, constraint.geometryId, constraint.id, diagnostics); continue }
|
||||
if (!isBlocked(candidate.id, blocked)) candidate.radius = constraint.value / 2
|
||||
} else if (constraint.type === 'coincident' || constraint.type === 'distance' || constraint.type === 'distanceX' || constraint.type === 'distanceY') {
|
||||
const firstGeometry = geometryById.get(constraint.first.geometryId)
|
||||
const secondGeometry = geometryById.get(constraint.second.geometryId)
|
||||
@@ -188,6 +227,38 @@ export const solveSketch = (input: SketchSnapshot, options: SketchSolveOptions =
|
||||
else adjustPoint(secondGeometry, constraint.second.point, { x: second.x, y: first.y + (dy < 0 ? -constraint.value : constraint.value) }, blocked)
|
||||
}
|
||||
}
|
||||
} else if (constraint.type === 'symmetric') {
|
||||
const firstGeometry = geometryById.get(constraint.first.geometryId)
|
||||
const secondGeometry = geometryById.get(constraint.second.geometryId)
|
||||
const centerGeometry = geometryById.get(constraint.center.geometryId)
|
||||
if (!firstGeometry || !secondGeometry || !centerGeometry) { findGeometry(snapshot.geometry, !firstGeometry ? constraint.first.geometryId : !secondGeometry ? constraint.second.geometryId : constraint.center.geometryId, constraint.id, diagnostics); continue }
|
||||
const first = pointFor(firstGeometry, constraint.first.point, constraint.id, diagnostics)
|
||||
const second = pointFor(secondGeometry, constraint.second.point, constraint.id, diagnostics)
|
||||
const center = pointFor(centerGeometry, constraint.center.point, constraint.id, diagnostics)
|
||||
if (!first || !second || !center) continue
|
||||
const reflected = { x: 2 * center.x - first.x, y: 2 * center.y - first.y }
|
||||
if (!isBlocked(secondGeometry.id, blocked)) adjustPoint(secondGeometry, constraint.second.point, reflected, blocked)
|
||||
} else if (constraint.type === 'tangent') {
|
||||
const first = geometryById.get(constraint.firstGeometryId)
|
||||
const second = geometryById.get(constraint.secondGeometryId)
|
||||
if (!first || !second) { findGeometry(snapshot.geometry, !first ? constraint.firstGeometryId : constraint.secondGeometryId, constraint.id, diagnostics); continue }
|
||||
const line = first.type === 'line' ? first : second.type === 'line' ? second : null
|
||||
const circle = first.type === 'line' ? second : second.type === 'line' ? first : null
|
||||
if (line && circle && (circle.type === 'circle' || circle.type === 'arc') && !isBlocked(circle.id, blocked)) {
|
||||
const dx = line.end.x - line.start.x
|
||||
const dy = line.end.y - line.start.y
|
||||
const length = Math.hypot(dx, dy) || 1
|
||||
const signed = ((circle.center.x - line.start.x) * dy - (circle.center.y - line.start.y) * dx) / length
|
||||
const delta = circle.radius - Math.abs(signed)
|
||||
const sign = signed < 0 ? -1 : 1
|
||||
circle.center = { x: circle.center.x + dy / length * delta * sign, y: circle.center.y - dx / length * delta * sign }
|
||||
} else if (first && second && (first.type === 'circle' || first.type === 'arc') && (second.type === 'circle' || second.type === 'arc') && !isBlocked(second.id, blocked)) {
|
||||
const dx = second.center.x - first.center.x
|
||||
const dy = second.center.y - first.center.y
|
||||
const length = Math.hypot(dx, dy) || 1
|
||||
const target = first.radius + second.radius
|
||||
second.center = { x: first.center.x + dx / length * target, y: first.center.y + dy / length * target }
|
||||
}
|
||||
} else if (constraint.type === 'equal') {
|
||||
const first = geometryById.get(constraint.firstGeometryId)
|
||||
const second = geometryById.get(constraint.secondGeometryId)
|
||||
|
||||
Reference in New Issue
Block a user