289 lines
22 KiB
TypeScript
289 lines
22 KiB
TypeScript
import type { DocumentObjectSnapshot, PropertyValue, VectorValue } from './types'
|
|
|
|
export type PartDesignParameterPartition = {
|
|
id: string
|
|
description: string
|
|
values: Record<string, PropertyValue>
|
|
}
|
|
|
|
export type PartDesignParameterFamily = {
|
|
typeId: string
|
|
properties: readonly string[]
|
|
partitions: readonly PartDesignParameterPartition[]
|
|
}
|
|
|
|
export type PartDesignParameterIssue = {
|
|
code: 'MISSING_PROPERTY' | 'INVALID_COMBINATION' | 'INVALID_RANGE' | 'INVALID_REFERENCE' | 'INVALID_VECTOR'
|
|
propertyName: string
|
|
message: string
|
|
}
|
|
|
|
export type PartDesignParameterValidation = {
|
|
typeId: string
|
|
covered: boolean
|
|
valid: boolean
|
|
issues: PartDesignParameterIssue[]
|
|
}
|
|
|
|
const partition = (id: string, description: string, values: Record<string, PropertyValue>): PartDesignParameterPartition => ({ id, description, values })
|
|
const family = (typeId: string, properties: readonly string[], partitions: readonly PartDesignParameterPartition[]): PartDesignParameterFamily => ({ typeId, properties, partitions })
|
|
|
|
const attachmentProperties = ['Support', 'MapMode', 'AttachmentOffset', 'DatumType'] as const
|
|
const linearProperties = ['Profile', 'Length', 'Length2', 'Type', 'Type2', 'SideType', 'UpToFace', 'UpToFace2', 'TaperAngle', 'TaperAngle2', 'Reversed', 'Midplane', 'ReferenceAxis', 'AlongSketchNormal', 'UseCustomVector', 'Direction', 'Offset', 'Offset2'] as const
|
|
const angularProperties = ['Profile', 'Angle', 'Angle2', 'Type', 'UpToFace', 'ReferenceAxis', 'Axis', 'AxisLink', 'Midplane', 'Reversed', 'Offset', 'Offset2'] as const
|
|
const loftProperties = ['Profile', 'Sections', 'Base', 'Ruled', 'Closed', 'Solid'] as const
|
|
const pipeProperties = ['Profile', 'Spine', 'Base', 'Transition', 'Mode', 'Transformation', 'Solid'] as const
|
|
|
|
export const PARTDESIGN_PARAMETER_SPACE: readonly PartDesignParameterFamily[] = Object.freeze([
|
|
family('PartDesign::Plane', attachmentProperties, [
|
|
partition('detached', 'No support and Deactivated map mode.', { Support: null, MapMode: 'Deactivated' }),
|
|
partition('flat-face', 'Stable face support.', { MapMode: 'FlatFace' }),
|
|
partition('three-points', 'Three-point plane attachment.', { MapMode: 'ThreePointsPlane' }),
|
|
]),
|
|
family('PartDesign::Line', attachmentProperties, [
|
|
partition('detached', 'No support and Deactivated map mode.', { Support: null, MapMode: 'Deactivated' }),
|
|
partition('normal-edge', 'Stable edge support.', { MapMode: 'NormalToEdge' }),
|
|
partition('two-points', 'Two-point line attachment.', { MapMode: 'TwoPointLine' }),
|
|
]),
|
|
family('PartDesign::Point', attachmentProperties, [
|
|
partition('detached', 'No support and Deactivated map mode.', { Support: null, MapMode: 'Deactivated' }),
|
|
partition('vertex', 'Stable vertex support.', { MapMode: 'Vertex' }),
|
|
partition('center-mass', 'Center-of-mass attachment.', { MapMode: 'CenterOfMass' }),
|
|
]),
|
|
family('PartDesign::ShapeBinder', ['Support', 'BindMode', 'TraceSupport', 'ClaimChildren'], [
|
|
partition('synchronized', 'Live synchronized support.', { BindMode: 'Synchronized', TraceSupport: true }),
|
|
partition('frozen', 'Frozen support snapshot.', { BindMode: 'Frozen', TraceSupport: false }),
|
|
partition('claim-children', 'Binder owns source children.', { ClaimChildren: true }),
|
|
]),
|
|
family('PartDesign::Pad', linearProperties, [
|
|
partition('dimension', 'One finite side.', { Type: 'Dimension', SideType: 'One side', Length: 10, Reversed: false }),
|
|
partition('two-lengths', 'Independent forward and reverse sides.', { Type: 'TwoLengths', SideType: 'Two sides', Length: 10, Length2: 5 }),
|
|
partition('midplane', 'Symmetric finite pad.', { Type: 'Dimension', SideType: 'Symmetric', Length: 10, Midplane: true }),
|
|
partition('up-to-face', 'Stable terminating face.', { Type: 'Up to face' }),
|
|
partition('custom-vector', 'Explicit extrusion vector.', { UseCustomVector: true, Direction: { x: 0, y: 0, z: 1 } }),
|
|
]),
|
|
family('PartDesign::Pocket', ['Base', ...linearProperties], [
|
|
partition('dimension', 'One finite side.', { Type: 'Dimension', SideType: 'One side', Length: 10, Reversed: false }),
|
|
partition('through-all', 'Through-all removal.', { Type: 'Through all', SideType: 'One side' }),
|
|
partition('two-lengths', 'Independent forward and reverse sides.', { Type: 'TwoLengths', SideType: 'Two sides', Length: 10, Length2: 5 }),
|
|
partition('midplane', 'Symmetric finite pocket.', { Type: 'Dimension', SideType: 'Symmetric', Length: 10, Midplane: true }),
|
|
partition('up-to-face', 'Stable terminating face.', { Type: 'Up to face' }),
|
|
]),
|
|
family('PartDesign::Revolution', angularProperties, [
|
|
partition('angle', 'One angular side.', { Type: 'Angle', Angle: 180, Reversed: false }),
|
|
partition('two-angles', 'Independent positive and reverse angles.', { Type: 'Two angles', Angle: 120, Angle2: 60 }),
|
|
partition('midplane', 'Symmetric angular feature.', { Type: 'Angle', Angle: 180, Midplane: true }),
|
|
partition('up-to-face', 'Stable angular terminating face.', { Type: 'Up to face' }),
|
|
]),
|
|
family('PartDesign::Groove', ['Base', ...angularProperties], [
|
|
partition('angle', 'One angular side.', { Type: 'Angle', Angle: 180, Reversed: false }),
|
|
partition('two-angles', 'Independent positive and reverse angles.', { Type: 'Two angles', Angle: 120, Angle2: 60 }),
|
|
partition('midplane', 'Symmetric angular feature.', { Type: 'Angle', Angle: 180, Midplane: true }),
|
|
partition('up-to-face', 'Stable angular terminating face.', { Type: 'Up to face' }),
|
|
]),
|
|
family('PartDesign::AdditiveLoft', loftProperties, [
|
|
partition('smooth', 'Smooth additive loft.', { Ruled: false, Closed: false, Solid: true }),
|
|
partition('ruled', 'Ruled additive loft.', { Ruled: true, Closed: false, Solid: true }),
|
|
partition('closed', 'Closed additive loft.', { Ruled: false, Closed: true, Solid: true }),
|
|
]),
|
|
family('PartDesign::SubtractiveLoft', loftProperties, [
|
|
partition('smooth', 'Smooth subtractive loft.', { Ruled: false, Closed: false, Solid: true }),
|
|
partition('ruled', 'Ruled subtractive loft.', { Ruled: true, Closed: false, Solid: true }),
|
|
partition('closed', 'Closed subtractive loft.', { Ruled: false, Closed: true, Solid: true }),
|
|
]),
|
|
family('PartDesign::AdditivePipe', pipeProperties, [
|
|
partition('standard', 'Standard transformed pipe.', { Mode: 'Standard', Transition: 'Transformed', Transformation: 'Constant' }),
|
|
partition('frenet', 'Frenet pipe.', { Mode: 'Frenet', Transition: 'Round corner', Transformation: 'Constant' }),
|
|
partition('multisection', 'Multi-section pipe.', { Mode: 'Standard', Transition: 'Right corner', Transformation: 'Multisection' }),
|
|
]),
|
|
family('PartDesign::SubtractivePipe', pipeProperties, [
|
|
partition('standard', 'Standard transformed pipe.', { Mode: 'Standard', Transition: 'Transformed', Transformation: 'Constant' }),
|
|
partition('frenet', 'Frenet pipe.', { Mode: 'Frenet', Transition: 'Round corner', Transformation: 'Constant' }),
|
|
partition('multisection', 'Multi-section pipe.', { Mode: 'Standard', Transition: 'Right corner', Transformation: 'Multisection' }),
|
|
]),
|
|
family('PartDesign::Fillet', ['Base', 'Radius', 'UseAllEdges'], [
|
|
partition('selected', 'Selected stable edges.', { Radius: 1, UseAllEdges: false }),
|
|
partition('all-edges', 'All edges.', { Radius: 1, UseAllEdges: true }),
|
|
partition('boundary', 'Small positive radius.', { Radius: Number.EPSILON, UseAllEdges: false }),
|
|
]),
|
|
family('PartDesign::Chamfer', ['Base', 'Size', 'Size2', 'Angle', 'ChamferType', 'FlipDirection', 'UseAllEdges'], [
|
|
partition('equal', 'Equal-distance chamfer.', { Size: 1, ChamferType: 'Equal distance' }),
|
|
partition('two-distances', 'Two-distance chamfer.', { Size: 1, Size2: 2, ChamferType: 'Two distances' }),
|
|
partition('distance-angle', 'Distance-and-angle chamfer.', { Size: 1, Angle: 45, ChamferType: 'Distance and Angle' }),
|
|
]),
|
|
family('PartDesign::Draft', ['Base', 'Angle', 'Direction', 'NeutralPlaneOrigin', 'NeutralPlaneDirection', 'Reversed', 'UseAllFaces'], [
|
|
partition('positive', 'Positive draft.', { Angle: 5, Reversed: false }),
|
|
partition('negative', 'Reverse draft.', { Angle: -5, Reversed: true }),
|
|
partition('all-faces', 'All supported faces.', { Angle: 5, UseAllFaces: true }),
|
|
]),
|
|
family('PartDesign::Thickness', ['Base', 'Value', 'RemoveFaces', 'Join', 'Mode', 'Reversed'], [
|
|
partition('arc', 'Arc join.', { Value: 1, Join: 'Arc', Mode: 'Skin', Reversed: false }),
|
|
partition('intersection', 'Intersection join.', { Value: 1, Join: 'Intersection', Mode: 'Skin', Reversed: false }),
|
|
partition('reversed', 'Reversed thickness.', { Value: 1, Join: 'Arc', Mode: 'Skin', Reversed: true }),
|
|
]),
|
|
family('PartDesign::Mirrored', ['Base', 'Originals', 'TransformMode', 'Plane', 'PlaneOrigin', 'PlaneNormal', 'Fuse'], [
|
|
partition('xy', 'Mirror on XY plane.', { Plane: 'XY plane', PlaneNormal: { x: 0, y: 0, z: 1 }, Fuse: true }),
|
|
partition('xz', 'Mirror on XZ plane.', { Plane: 'XZ plane', PlaneNormal: { x: 0, y: 1, z: 0 }, Fuse: true }),
|
|
partition('yz', 'Mirror on YZ plane.', { Plane: 'YZ plane', PlaneNormal: { x: 1, y: 0, z: 0 }, Fuse: false }),
|
|
]),
|
|
family('PartDesign::MultiTransform', ['Base', 'Originals', 'TransformMode', 'Transformations'], [
|
|
partition('linear-polar', 'Ordered linear and polar transforms.', { Transformations: { steps: [{ id: 'linear', type: 'linear', occurrences: 2, length: 10, direction: 'Horizontal' }, { id: 'polar', type: 'polar', occurrences: 3, angle: 180, axis: 'Normal' }] } }),
|
|
partition('linear-mirror', 'Ordered linear and mirrored transforms.', { Transformations: { steps: [{ id: 'linear', type: 'linear', occurrences: 2, length: 10, direction: 'Vertical' }, { id: 'mirror', type: 'mirrored', plane: 'YZ plane' }] } }),
|
|
partition('polar-mirror', 'Ordered polar and mirrored transforms.', { Transformations: { steps: [{ id: 'polar', type: 'polar', occurrences: 3, angle: 360, axis: 'Normal' }, { id: 'mirror', type: 'mirrored', plane: 'XY plane' }] } }),
|
|
]),
|
|
family('PartDesign::LinearPattern', ['Base', 'Originals', 'TransformMode', 'Occurrences', 'Length', 'Offset', 'Direction', 'DirectionVector', 'Mode', 'Reversed', 'Spacings', 'SpacingPattern', 'Direction2', 'Mode2', 'Length2', 'Offset2', 'Occurrences2', 'Reversed2', 'Spacings2', 'SpacingPattern2'], [
|
|
partition('extent', 'One-direction extent pattern.', { Occurrences: 2, Length: 20, Mode: 'Extent', Direction2: 'None' }),
|
|
partition('spacing', 'One-direction spacing pattern.', { Occurrences: 3, Offset: 5, Mode: 'Spacing', Direction2: 'None' }),
|
|
partition('two-directions', 'Two-direction rectangular pattern.', { Occurrences: 2, Length: 20, Mode: 'Extent', Direction2: 'Vertical', Occurrences2: 2, Length2: 10, Mode2: 'Extent' }),
|
|
]),
|
|
family('PartDesign::PolarPattern', ['Base', 'Originals', 'TransformMode', 'Occurrences', 'Angle', 'Axis', 'AxisOrigin', 'AxisDirection', 'Mode', 'Offset', 'Spacings', 'SpacingPattern', 'Reversed'], [
|
|
partition('extent', 'Angular extent pattern.', { Occurrences: 3, Angle: 360, Mode: 'Extent' }),
|
|
partition('spacing', 'Fixed angular spacing.', { Occurrences: 3, Offset: 30, Mode: 'Spacing' }),
|
|
partition('reversed', 'Reversed angular pattern.', { Occurrences: 3, Angle: 180, Mode: 'Extent', Reversed: true }),
|
|
]),
|
|
family('PartDesign::Hole', ['Base', 'Diameter', 'Depth', 'Type', 'DepthType', 'Position', 'Direction', 'Reversed', 'HoleCutType', 'HoleCutDiameter', 'HoleCutDepth', 'HoleCutCountersinkAngle', 'DrillPoint', 'DrillPointAngle', 'DrillForDepth', 'Tapered', 'TaperedAngle', 'HoleCutCustomValues', 'Threaded', 'ModelThread', 'ThreadType', 'ThreadSize', 'ThreadDiameter', 'ThreadPitch', 'ThreadClass', 'ThreadFit', 'ThreadDirection', 'ThreadDepthType', 'ThreadDepth', 'UseCustomThreadClearance', 'CustomThreadClearance'], [
|
|
partition('dimension', 'Finite plain hole.', { DepthType: 'Dimension', Diameter: 5, Depth: 10, HoleCutType: 'None' }),
|
|
partition('through-all', 'Through-all hole.', { DepthType: 'ThroughAll', Diameter: 5, Depth: 10 }),
|
|
partition('counterbore', 'Counterbored hole.', { HoleCutType: 'Counterbore', HoleCutDiameter: 8, HoleCutDepth: 2 }),
|
|
partition('countersink', 'Countersunk hole.', { HoleCutType: 'Countersink', HoleCutDiameter: 8, HoleCutCountersinkAngle: 90 }),
|
|
partition('threaded', 'Thread metadata without modeled thread.', { Threaded: true, ModelThread: false, ThreadType: 'ISOMetricProfile', ThreadDiameter: 6, ThreadPitch: 1 }),
|
|
partition('modeled-thread', 'Modeled thread.', { Threaded: true, ModelThread: true, ThreadType: 'ISOMetricProfile', ThreadDiameter: 6, ThreadPitch: 1 }),
|
|
]),
|
|
])
|
|
|
|
const byTypeId = new Map(PARTDESIGN_PARAMETER_SPACE.map((entry) => [entry.typeId, entry]))
|
|
const number = (values: Readonly<Record<string, PropertyValue>>, name: string) => typeof values[name] === 'number' ? values[name] as number : undefined
|
|
const string = (values: Readonly<Record<string, PropertyValue>>, name: string) => typeof values[name] === 'string' ? values[name] as string : undefined
|
|
const bool = (values: Readonly<Record<string, PropertyValue>>, name: string) => values[name] === true
|
|
const vector = (values: Readonly<Record<string, PropertyValue>>, name: string) => {
|
|
const value = values[name]
|
|
return value && typeof value === 'object' && !Array.isArray(value) && 'x' in value && 'y' in value && 'z' in value ? value as VectorValue : undefined
|
|
}
|
|
|
|
const push = (issues: PartDesignParameterIssue[], code: PartDesignParameterIssue['code'], propertyName: string, message: string) => issues.push({ code, propertyName, message })
|
|
const positive = (issues: PartDesignParameterIssue[], values: Readonly<Record<string, PropertyValue>>, name: string) => {
|
|
const value = number(values, name)
|
|
if (value === undefined || !Number.isFinite(value) || value <= 0) push(issues, 'INVALID_RANGE', name, `${name} must be finite and greater than zero.`)
|
|
}
|
|
const angle = (issues: PartDesignParameterIssue[], values: Readonly<Record<string, PropertyValue>>, name: string, allowZero = false) => {
|
|
const value = number(values, name)
|
|
if (value === undefined || !Number.isFinite(value) || value < (allowZero ? 0 : Number.EPSILON) || value > 360) push(issues, 'INVALID_RANGE', name, `${name} must be within ${allowZero ? '[0, 360]' : '(0, 360]'} degrees.`)
|
|
}
|
|
const nonZeroVector = (issues: PartDesignParameterIssue[], values: Readonly<Record<string, PropertyValue>>, name: string) => {
|
|
const value = vector(values, name)
|
|
if (!value || ![value.x, value.y, value.z].every(Number.isFinite) || Math.hypot(value.x, value.y, value.z) <= 0) push(issues, 'INVALID_VECTOR', name, `${name} must be a finite non-zero vector.`)
|
|
}
|
|
|
|
export const parameterValuesForObject = (object: Pick<DocumentObjectSnapshot, 'properties'>): Record<string, PropertyValue> => Object.fromEntries(object.properties.filter((property) => property.scope === 'data').map((property) => [property.name, property.value]))
|
|
|
|
export const validatePartDesignParameterSet = (
|
|
typeId: string,
|
|
values: Readonly<Record<string, PropertyValue>>,
|
|
options: { requireComplete?: boolean } = {},
|
|
): PartDesignParameterValidation => {
|
|
const definition = byTypeId.get(typeId)
|
|
if (!definition) return { typeId, covered: false, valid: true, issues: [] }
|
|
const issues: PartDesignParameterIssue[] = []
|
|
const requireComplete = options.requireComplete !== false
|
|
const has = (name: string) => Object.prototype.hasOwnProperty.call(values, name)
|
|
const checkPositive = (name: string) => { if (requireComplete || has(name)) positive(issues, values, name) }
|
|
const checkAngle = (name: string, allowZero = false) => { if (requireComplete || has(name)) angle(issues, values, name, allowZero) }
|
|
const checkVector = (name: string) => { if (requireComplete || has(name)) nonZeroVector(issues, values, name) }
|
|
if (requireComplete) for (const name of definition.properties) if (!has(name)) push(issues, 'MISSING_PROPERTY', name, `${typeId} is missing parameter ${name}.`)
|
|
|
|
if (typeId === 'PartDesign::Pad' || typeId === 'PartDesign::Pocket') {
|
|
const mode = string(values, 'Type')
|
|
if (mode === 'Dimension' || mode === 'TwoLengths') checkPositive('Length')
|
|
if (mode === 'TwoLengths' || string(values, 'SideType') === 'Two sides') checkPositive('Length2')
|
|
if (mode === 'Up to face' && values.UpToFace === null) push(issues, 'INVALID_REFERENCE', 'UpToFace', 'Up-to-face mode requires a stable face reference.')
|
|
if (bool(values, 'Midplane') && (mode === 'TwoLengths' || string(values, 'SideType') === 'Two sides')) push(issues, 'INVALID_COMBINATION', 'Midplane', 'Midplane cannot be combined with independent two-sided lengths.')
|
|
for (const name of ['TaperAngle', 'TaperAngle2']) {
|
|
const value = number(values, name)
|
|
if (value !== undefined && (!Number.isFinite(value) || Math.abs(value) >= 90)) push(issues, 'INVALID_RANGE', name, `${name} must be strictly between -90 and 90 degrees.`)
|
|
}
|
|
if (bool(values, 'UseCustomVector')) checkVector('Direction')
|
|
}
|
|
if (typeId === 'PartDesign::Revolution' || typeId === 'PartDesign::Groove') {
|
|
checkAngle('Angle')
|
|
if (string(values, 'Type') === 'Two angles') {
|
|
checkAngle('Angle2')
|
|
if ((number(values, 'Angle') ?? 0) + (number(values, 'Angle2') ?? 0) > 360) push(issues, 'INVALID_COMBINATION', 'Angle2', 'Angle and Angle2 must total no more than 360 degrees.')
|
|
if (bool(values, 'Midplane')) push(issues, 'INVALID_COMBINATION', 'Midplane', 'Midplane cannot be combined with Two angles.')
|
|
}
|
|
}
|
|
if (typeId.endsWith('Loft')) {
|
|
const sections = [...(typeof values.Profile === 'string' ? [values.Profile] : []), ...(Array.isArray(values.Sections) ? values.Sections.filter((entry): entry is string => typeof entry === 'string') : [])]
|
|
if ((requireComplete || has('Profile') || has('Sections')) && (sections.length < 2 || new Set(sections).size !== sections.length)) push(issues, 'INVALID_REFERENCE', 'Sections', 'Loft requires at least two unique section profiles.')
|
|
}
|
|
if (typeId.endsWith('Pipe')) {
|
|
const profile = string(values, 'Profile')
|
|
const spineValue = values.Spine
|
|
const spine = typeof spineValue === 'string' ? spineValue : spineValue && typeof spineValue === 'object' && !Array.isArray(spineValue) && 'objectId' in spineValue ? String(spineValue.objectId) : undefined
|
|
if ((requireComplete || has('Profile') || has('Spine')) && (!profile || !spine || profile === spine)) push(issues, 'INVALID_REFERENCE', 'Spine', 'Pipe requires distinct Profile and Spine references.')
|
|
}
|
|
if (typeId === 'PartDesign::Fillet') checkPositive('Radius')
|
|
if (typeId === 'PartDesign::Chamfer') {
|
|
checkPositive('Size')
|
|
if (string(values, 'ChamferType') === 'Two distances') checkPositive('Size2')
|
|
if (string(values, 'ChamferType') === 'Distance and Angle') checkAngle('Angle')
|
|
}
|
|
if (typeId === 'PartDesign::Draft') {
|
|
const value = number(values, 'Angle')
|
|
if ((requireComplete || has('Angle')) && (value === undefined || !Number.isFinite(value) || value === 0 || Math.abs(value) >= 90)) push(issues, 'INVALID_RANGE', 'Angle', 'Draft Angle must be non-zero and strictly between -90 and 90 degrees.')
|
|
checkVector('Direction')
|
|
checkVector('NeutralPlaneDirection')
|
|
}
|
|
if (typeId === 'PartDesign::Thickness') checkPositive('Value')
|
|
if (typeId === 'PartDesign::Mirrored') checkVector('PlaneNormal')
|
|
if (typeId === 'PartDesign::LinearPattern') {
|
|
const occurrences = number(values, 'Occurrences')
|
|
if ((requireComplete || has('Occurrences')) && (!Number.isSafeInteger(occurrences) || (occurrences ?? 0) < 2 || (occurrences ?? 0) > 100)) push(issues, 'INVALID_RANGE', 'Occurrences', 'Occurrences must be an integer between 2 and 100.')
|
|
if (string(values, 'Mode') === 'Extent') checkPositive('Length')
|
|
else if (has('Mode') || requireComplete) checkPositive('Offset')
|
|
checkVector('DirectionVector')
|
|
if (string(values, 'Direction2') !== 'None') {
|
|
const occurrences2 = number(values, 'Occurrences2')
|
|
if ((requireComplete || has('Occurrences2')) && (!Number.isSafeInteger(occurrences2) || (occurrences2 ?? 0) < 2 || (occurrences2 ?? 0) > 100)) push(issues, 'INVALID_RANGE', 'Occurrences2', 'Occurrences2 must be an integer between 2 and 100 when the second direction is enabled.')
|
|
if (string(values, 'Mode2') === 'Extent') checkPositive('Length2')
|
|
else if (has('Mode2') || requireComplete) checkPositive('Offset2')
|
|
}
|
|
}
|
|
if (typeId === 'PartDesign::PolarPattern') {
|
|
const occurrences = number(values, 'Occurrences')
|
|
if ((requireComplete || has('Occurrences')) && (!Number.isSafeInteger(occurrences) || (occurrences ?? 0) < 2 || (occurrences ?? 0) > 100)) push(issues, 'INVALID_RANGE', 'Occurrences', 'Occurrences must be an integer between 2 and 100.')
|
|
if (string(values, 'Mode') === 'Extent') checkAngle('Angle')
|
|
else if (has('Mode') || requireComplete) checkAngle('Offset')
|
|
checkVector('AxisDirection')
|
|
}
|
|
if (typeId === 'PartDesign::Hole') {
|
|
checkPositive('Diameter')
|
|
if (string(values, 'DepthType') === 'Dimension') checkPositive('Depth')
|
|
checkVector('Direction')
|
|
if (string(values, 'HoleCutType') !== 'None' && (has('HoleCutType') || requireComplete)) checkPositive('HoleCutDiameter')
|
|
if (string(values, 'HoleCutType') === 'Counterbore' || string(values, 'HoleCutType') === 'Counterdrill') checkPositive('HoleCutDepth')
|
|
if (string(values, 'HoleCutType') === 'Countersink' || string(values, 'HoleCutType') === 'Counterdrill') checkAngle('HoleCutCountersinkAngle')
|
|
if (bool(values, 'Threaded')) {
|
|
if (!string(values, 'ThreadType') || string(values, 'ThreadType') === 'None') push(issues, 'INVALID_COMBINATION', 'ThreadType', 'Threaded holes require a thread standard.')
|
|
checkPositive('ThreadDiameter')
|
|
checkPositive('ThreadPitch')
|
|
}
|
|
if (bool(values, 'ModelThread') && !bool(values, 'Threaded')) push(issues, 'INVALID_COMBINATION', 'ModelThread', 'ModelThread requires Threaded.')
|
|
}
|
|
return { typeId, covered: true, valid: issues.length === 0, issues }
|
|
}
|
|
|
|
export const assertPartDesignParameterSet = (object: Pick<DocumentObjectSnapshot, 'typeId' | 'properties'>): void => {
|
|
const report = validatePartDesignParameterSet(object.typeId, parameterValuesForObject(object))
|
|
if (!report.valid) throw new RangeError(`${object.typeId}.${report.issues[0].propertyName}: ${report.issues[0].message}`)
|
|
}
|
|
|
|
export const partDesignParameterSpaceCoverage = () => ({
|
|
schemaVersion: 1 as const,
|
|
baseline: 'FreeCAD 1.1.1' as const,
|
|
families: PARTDESIGN_PARAMETER_SPACE.length,
|
|
properties: PARTDESIGN_PARAMETER_SPACE.reduce((total, entry) => total + entry.properties.length, 0),
|
|
partitions: PARTDESIGN_PARAMETER_SPACE.reduce((total, entry) => total + entry.partitions.length, 0),
|
|
duplicateTypeIds: PARTDESIGN_PARAMETER_SPACE.length - new Set(PARTDESIGN_PARAMETER_SPACE.map((entry) => entry.typeId)).size,
|
|
familiesWithoutPartitions: PARTDESIGN_PARAMETER_SPACE.filter((entry) => entry.partitions.length < 3).map((entry) => entry.typeId),
|
|
})
|