Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
162 changes: 162 additions & 0 deletions src/modeling/capture/assemblyCaptureValidation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2026 Andrii Shylenko and kernelCAD contributors
import type { FeatureRecord } from '../../shared/intent/featureRecord';
import type { FeatureId } from '../../shared/intent/types';
import {
buildAssemblyConnectFeatureSpec,
buildAssemblyExportFeatureSpec,
buildAssemblyJointFeatureSpec,
buildAssemblyModelFeatureSpec,
buildAssemblyPartFeatureSpec,
buildSolvedAssemblyFeatureSpec,
type AssemblyConnectorRefLike,
type AssemblyFeatureSpec,
type AssemblyJointCaptureOpts,
type AssemblyJointKind,
type AssemblyPartCaptureOpts,
type AssemblyPartRefLike,
type SolvedAssemblyJointRef,
type SolvedAssemblyMateMetadata,
type SolvedAssemblyPoseInput,
} from './assemblyFeatureRecords';

export function createAssemblyPartCaptureSpec(
records: readonly FeatureRecord[],
assemblyName: string,
partName: string,
shapeId: FeatureId,
opts: AssemblyPartCaptureOpts = {},
): AssemblyFeatureSpec {
if (!records.some(r => r.id === shapeId)) {
throw new Error(`assembly.part: shape '${shapeId}' is not from this CaptureSession`);
}
return buildAssemblyPartFeatureSpec(assemblyName, partName, shapeId, opts);
}

export function createAssemblyConnectCaptureSpec(
records: readonly FeatureRecord[],
assemblyName: string,
connectName: string,
a: AssemblyConnectorRefLike,
b: AssemblyConnectorRefLike,
): AssemblyFeatureSpec {
for (const connector of [a, b]) {
requireRecordKind(
records,
connector.partId,
'assemblyPart',
`assembly.connect: part '${connector.partId}' is not an assembly part in this CaptureSession`,
);
}
return buildAssemblyConnectFeatureSpec(assemblyName, connectName, a, b);
}

export function createAssemblyJointCaptureSpec(
records: readonly FeatureRecord[],
assemblyName: string,
jointName: string,
jointKind: AssemblyJointKind,
a: AssemblyPartRefLike,
b: AssemblyPartRefLike,
opts: AssemblyJointCaptureOpts,
): AssemblyFeatureSpec {
for (const part of [a, b]) {
requireRecordKind(
records,
part.id,
'assemblyPart',
`assembly.${jointKind}: part '${part.id}' is not an assembly part in this CaptureSession`,
);
}
return buildAssemblyJointFeatureSpec(assemblyName, jointName, jointKind, a, b, opts);
}

export function createAssemblyModelCaptureSpec(
records: readonly FeatureRecord[],
assemblyName: string,
parts: readonly AssemblyPartRefLike[],
mateMetadata?: SolvedAssemblyMateMetadata,
): AssemblyFeatureSpec {
for (const part of parts) {
requireRecordKind(
records,
part.id,
'assemblyPart',
`assembly.model: part '${part.id}' is not an assembly part in this CaptureSession`,
);
}
return buildAssemblyModelFeatureSpec(assemblyName, parts, mateMetadata);
}

export function createSolvedAssemblyCaptureSpec(args: {
records: readonly FeatureRecord[];
assemblyName: string;
parts: readonly AssemblyPartRefLike[];
joints: readonly { id: FeatureId; name: string }[];
poses: SolvedAssemblyPoseInput;
mateMetadata?: SolvedAssemblyMateMetadata;
}): AssemblyFeatureSpec {
if (args.parts.length === 0) {
throw new Error('assembly.solvedModel requires at least one part');
}
for (const part of args.parts) {
requireRecordKind(
args.records,
part.id,
'assemblyPart',
`assembly.solvedModel: part '${part.id}' is not an assembly part in this CaptureSession`,
);
}

const solvedJoints: SolvedAssemblyJointRef[] = [];
for (const joint of args.joints) {
const record = requireRecordKind(
args.records,
joint.id,
'assemblyJoint',
`assembly.solvedModel: joint '${joint.id}' is not an assembly joint in this CaptureSession`,
);
const metadata = record.metadata as { jointName?: string; jointKind?: AssemblyJointKind };
solvedJoints.push({
id: joint.id,
name: metadata.jointName ?? joint.name,
kind: metadata.jointName !== undefined ? metadata.jointKind : undefined,
});
}

return buildSolvedAssemblyFeatureSpec({
assemblyName: args.assemblyName,
parts: args.parts,
joints: solvedJoints,
poses: args.poses,
mateMetadata: args.mateMetadata,
});
}

export function createAssemblyExportCaptureSpec(
records: readonly FeatureRecord[],
sceneFeatureId: FeatureId,
op: 'compound' | 'union',
): AssemblyFeatureSpec {
const sourceRecord = records.find(r => r.id === sceneFeatureId);
if (!sourceRecord) {
throw new Error(`assemblyExport: source scene feature '${sceneFeatureId}' is not from this CaptureSession`);
}
if (sourceRecord.kind !== 'solvedAssembly' && sourceRecord.kind !== 'assemblyModel') {
throw new Error(`assemblyExport: source feature '${sceneFeatureId}' is kind '${sourceRecord.kind}'; expected 'solvedAssembly' or 'assemblyModel'.`);
}
return buildAssemblyExportFeatureSpec(sceneFeatureId, op);
}

function requireRecordKind<K extends FeatureRecord['kind']>(
records: readonly FeatureRecord[],
id: FeatureId,
kind: K,
message: string,
): FeatureRecord & { kind: K } {
const record = records.find(r => r.id === id);
if (!record || record.kind !== kind) {
throw new Error(message);
}
return record as FeatureRecord & { kind: K };
}
81 changes: 16 additions & 65 deletions src/modeling/capture/captureSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,14 +56,14 @@ import {
type VariableSweepCaptureArgs,
} from './surfaceSweepRecords';
import {
buildAssemblyConnectFeatureSpec,
buildAssemblyExportFeatureSpec,
buildAssemblyJointFeatureSpec,
buildAssemblyModelFeatureSpec,
buildAssemblyPartFeatureSpec,
buildSolvedAssemblyFeatureSpec,
type AssemblyJointKind,
type SolvedAssemblyJointRef,
createAssemblyConnectCaptureSpec,
createAssemblyExportCaptureSpec,
createAssemblyJointCaptureSpec,
createAssemblyModelCaptureSpec,
createAssemblyPartCaptureSpec,
createSolvedAssemblyCaptureSpec,
} from './assemblyCaptureValidation';
import {
type SolvedAssemblyMateMetadata,
} from './assemblyFeatureRecords';

Expand Down Expand Up @@ -659,10 +659,7 @@ export class CaptureSession {
placedBy?: AssemblyPartOpts['connect'];
} = {},
): FeatureRecord {
if (!this.records.some(r => r.id === shape.id)) {
throw new Error(`assembly.part: shape '${shape.id}' is not from this CaptureSession`);
}
return this.register(buildAssemblyPartFeatureSpec(assemblyName, partName, shape.id, opts));
return this.register(createAssemblyPartCaptureSpec(this.records, assemblyName, partName, shape.id, opts));
}

assemblyConnect(
Expand All @@ -671,13 +668,7 @@ export class CaptureSession {
a: AssemblyConnectorRef,
b: AssemblyConnectorRef,
): FeatureRecord {
for (const connector of [a, b]) {
const record = this.records.find(r => r.id === connector.partId);
if (!record || record.kind !== 'assemblyPart') {
throw new Error(`assembly.connect: part '${connector.partId}' is not an assembly part in this CaptureSession`);
}
}
return this.register(buildAssemblyConnectFeatureSpec(assemblyName, connectName, a, b));
return this.register(createAssemblyConnectCaptureSpec(this.records, assemblyName, connectName, a, b));
}

assemblyJoint(
Expand All @@ -694,27 +685,15 @@ export class CaptureSession {
ballLimitsDeg?: [[number, number], [number, number], [number, number]];
},
): FeatureRecord {
for (const part of [a, b]) {
const record = this.records.find(r => r.id === part.id);
if (!record || record.kind !== 'assemblyPart') {
throw new Error(`assembly.${jointKind}: part '${part.id}' is not an assembly part in this CaptureSession`);
}
}
return this.register(buildAssemblyJointFeatureSpec(assemblyName, jointName, jointKind, a, b, opts));
return this.register(createAssemblyJointCaptureSpec(this.records, assemblyName, jointName, jointKind, a, b, opts));
}

assemblyModel(
assemblyName: string,
parts: readonly AssemblyPartRef[],
mateMetadata?: SolvedAssemblyMateMetadata,
): Shape {
for (const part of parts) {
const record = this.records.find(r => r.id === part.id);
if (!record || record.kind !== 'assemblyPart') {
throw new Error(`assembly.model: part '${part.id}' is not an assembly part in this CaptureSession`);
}
}
return this.createShape(buildAssemblyModelFeatureSpec(assemblyName, parts, mateMetadata));
return this.createShape(createAssemblyModelCaptureSpec(this.records, assemblyName, parts, mateMetadata));
}

/**
Expand All @@ -739,32 +718,11 @@ export class CaptureSession {
poses: Record<string, Editable<number> | [Editable<number>, Editable<number>, Editable<number>]>,
mateMetadata?: SolvedAssemblyMateMetadata,
): Shape {
if (parts.length === 0) {
throw new Error('assembly.solvedModel requires at least one part');
}
for (const part of parts) {
const record = this.records.find(r => r.id === part.id);
if (!record || record.kind !== 'assemblyPart') {
throw new Error(`assembly.solvedModel: part '${part.id}' is not an assembly part in this CaptureSession`);
}
}
const solvedJoints: SolvedAssemblyJointRef[] = [];
for (const joint of joints) {
const record = this.records.find(r => r.id === joint.id);
if (!record || record.kind !== 'assemblyJoint') {
throw new Error(`assembly.solvedModel: joint '${joint.id}' is not an assembly joint in this CaptureSession`);
}
const m = record.metadata as { jointName?: string; jointKind?: AssemblyJointKind };
solvedJoints.push({
id: joint.id,
name: m.jointName ?? joint.name,
kind: m.jointName !== undefined ? m.jointKind : undefined,
});
}
return this.createShape(buildSolvedAssemblyFeatureSpec({
return this.createShape(createSolvedAssemblyCaptureSpec({
records: this.records,
assemblyName,
parts,
joints: solvedJoints,
joints,
poses,
mateMetadata,
}));
Expand All @@ -786,14 +744,7 @@ export class CaptureSession {
* `.fillet()`, `.exportSTL()`, etc. on it.
*/
assemblyExport(sceneFeatureId: FeatureId, op: 'compound' | 'union'): Shape {
const sourceRecord = this.records.find(r => r.id === sceneFeatureId);
if (!sourceRecord) {
throw new Error(`assemblyExport: source scene feature '${sceneFeatureId}' is not from this CaptureSession`);
}
if (sourceRecord.kind !== 'solvedAssembly' && sourceRecord.kind !== 'assemblyModel') {
throw new Error(`assemblyExport: source feature '${sceneFeatureId}' is kind '${sourceRecord.kind}'; expected 'solvedAssembly' or 'assemblyModel'.`);
}
return this.createShape(buildAssemblyExportFeatureSpec(sceneFeatureId, op));
return this.createShape(createAssemblyExportCaptureSpec(this.records, sceneFeatureId, op));
}

edgeFeature(
Expand Down
Loading
Loading