P5: expose Sketcher solve command and OCCT dress-up features
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { BitByBitOCCT, OccStateEnum } from '@bitbybit-dev/occt-worker'
|
||||
import type { Inputs } from '@bitbybit-dev/occt'
|
||||
import type { ApplyPlacementInput, BooleanCutInput, BooleanIntersectionInput, BooleanUnionInput, CreateBoxInput, CreateConeInput, CreateCylinderInput, CreateSphereInput, GeometryCapabilities, GeometryDocumentContext, LinearFeatureParameters, MeshAsset, PadInput, PlanarProfile, PocketInput, Point3, RevolutionInput, ShapeHandle, SubshapeRef } from './types'
|
||||
import type { ApplyPlacementInput, BooleanCutInput, BooleanIntersectionInput, BooleanUnionInput, ChamferInput, CreateBoxInput, CreateConeInput, CreateCylinderInput, CreateSphereInput, FilletInput, GeometryCapabilities, GeometryDocumentContext, LinearFeatureParameters, MeshAsset, PadInput, PlanarProfile, PocketInput, Point3, RevolutionInput, ShapeHandle, SubshapeRef } from './types'
|
||||
import { createSubshapeRefs } from './topologyNaming'
|
||||
|
||||
type KernelShapeReference = Inputs.OCCT.TopoDSShapePointer
|
||||
@@ -102,6 +102,16 @@ export const validateBooleanCutInput = (input: BooleanCutInput) => {
|
||||
|
||||
export const validateBooleanIntersectionInput = (input: BooleanIntersectionInput) => validateBooleanShapes(input, input.shapes, 2)
|
||||
|
||||
const validateEdgeFeature = (input: GeometryDocumentContext & { base: ShapeHandle; indexes?: number[]; radius?: number; distance?: number }, value: number, name: string) => {
|
||||
validateDocumentContext(input)
|
||||
validateShapeContext(input, input.base)
|
||||
finitePositive(value, name)
|
||||
if (input.indexes?.some((index) => !Number.isSafeInteger(index) || index < 0)) throw new RangeError('Edge indexes must be non-negative safe integers.')
|
||||
}
|
||||
|
||||
export const validateFilletInput = (input: FilletInput) => validateEdgeFeature(input, input.radius, 'radius')
|
||||
export const validateChamferInput = (input: ChamferInput) => validateEdgeFeature(input, input.distance, 'distance')
|
||||
|
||||
const samePoint = (left: Point3, right: Point3, tolerance = 1e-9) => left.every((coordinate, axis) => Math.abs(coordinate - right[axis]) <= tolerance)
|
||||
const subtract = (left: Point3, right: Point3): Point3 => [left[0] - right[0], left[1] - right[1], left[2] - right[2]]
|
||||
const cross = (left: Point3, right: Point3): Point3 => [left[1] * right[2] - left[2] * right[1], left[2] * right[0] - left[0] * right[2], left[0] * right[1] - left[1] * right[0]]
|
||||
@@ -354,6 +364,22 @@ export class BitbybitGeometryRuntime {
|
||||
return this.registerShape(kernelShape, input.documentId, input.documentVersion)
|
||||
}
|
||||
|
||||
async fillet(input: FilletInput): Promise<ShapeHandle> {
|
||||
validateFilletInput(input)
|
||||
const base = this.resolveShape(input.base).reference
|
||||
const client = await this.readyClient()
|
||||
const kernelShape = await client.occt.fillets.filletEdges({ shape: base, radius: input.radius, indexes: input.indexes })
|
||||
return this.registerShape(kernelShape, input.documentId, input.documentVersion)
|
||||
}
|
||||
|
||||
async chamfer(input: ChamferInput): Promise<ShapeHandle> {
|
||||
validateChamferInput(input)
|
||||
const base = this.resolveShape(input.base).reference
|
||||
const client = await this.readyClient()
|
||||
const kernelShape = await client.occt.fillets.chamferEdges({ shape: base, distance: input.distance, indexes: input.indexes })
|
||||
return this.registerShape(kernelShape, input.documentId, input.documentVersion)
|
||||
}
|
||||
|
||||
async pad(input: PadInput): Promise<ShapeHandle> {
|
||||
validatePadInput(input)
|
||||
const client = await this.readyClient()
|
||||
|
||||
@@ -3,7 +3,7 @@ export { createSqliteProjectPersistence, PersistenceWriteQueue, ProjectAutosaveS
|
||||
export { ThreeViewportAdapter } from './threeViewport'
|
||||
export { assertShapeHandleIntegrity, BitbybitGeometryRuntime, normalizeBitbybitMesh, validateBooleanCutInput, validateBooleanIntersectionInput, validateBooleanUnionInput, validateBoxInput, validateConeInput, validateCylinderInput, validatePadInput, validatePlacementInput, validatePlanarProfile, validatePocketInput, validateRevolutionInput, validateSphereInput } from './geometryRuntime'
|
||||
export { PROJECT_SCHEMA_MIGRATIONS, PROJECT_SCHEMA_SQL, PROJECT_SCHEMA_VERSION } from './projectSchema'
|
||||
export type { ApplyPlacementInput, BitBybitViewportAdapter, BitBybitWebCadFacade, BooleanCutInput, BooleanIntersectionInput, BooleanUnionInput, CommandState, CreateBoxInput, CreateConeInput, CreateCylinderInput, CreateSphereInput, DocumentObjectSnapshot, DocumentSnapshot, FacadeEvent, FacadeState, GeometryCapabilities, GeometryDocumentContext, LinearFeatureParameters, MeshAsset, ModelTreeItem, ObjectPropertySnapshot, PadInput, PersistenceCapabilities, Placement, PlanarProfile, PocketInput, Point3, ProjectResource, ProjectSaveResult, PropertyValue, RecomputeResult, RevolutionInput, SetExpressionInput, SetPropertyInput, ShapeHandle, SubshapeRef, TaskSnapshot } from './types'
|
||||
export type { ApplyPlacementInput, BitBybitViewportAdapter, BitBybitWebCadFacade, BooleanCutInput, BooleanIntersectionInput, BooleanUnionInput, ChamferInput, CommandState, CreateBoxInput, CreateConeInput, CreateCylinderInput, CreateSphereInput, DocumentObjectSnapshot, DocumentSnapshot, FacadeEvent, FacadeState, FilletInput, GeometryCapabilities, GeometryDocumentContext, LinearFeatureParameters, MeshAsset, ModelTreeItem, ObjectPropertySnapshot, PadInput, PersistenceCapabilities, Placement, PlanarProfile, PocketInput, Point3, ProjectResource, ProjectSaveResult, PropertyValue, RecomputeResult, RevolutionInput, SetExpressionInput, SetPropertyInput, ShapeHandle, SubshapeRef, TaskSnapshot } from './types'
|
||||
export { createSubshapeRefs, matchSubshapes, signatureForFace } from './topologyNaming'
|
||||
export { BasicSketchSolverAdapter, cloneSketch, createSketch, solveSketch } from './sketcher'
|
||||
export type { SketchConstraint, SketchDiagnostic, SketchGeometry, SketchPoint, SketchPointRef, SketchSnapshot, SketchSolveOptions, SketchSolveResult, SketchSolverAdapter, SketchSolverStatus } from './sketcher'
|
||||
|
||||
@@ -128,7 +128,7 @@ const createDocument = (label = 'Pump Housing'): DocumentSnapshot => {
|
||||
return document
|
||||
}
|
||||
|
||||
const selectionRequired = new Set(['pad', 'pocket', 'revolution', 'fillet', 'chamfer', 'hole', 'linear-pattern', 'polar-pattern', 'measure-distance', 'measure-angle', 'measure-area'])
|
||||
const selectionRequired = new Set(['pad', 'pocket', 'revolution', 'fillet', 'chamfer', 'hole', 'linear-pattern', 'polar-pattern', 'measure-distance', 'measure-angle', 'measure-area', 'solve-sketch'])
|
||||
const systemCommands = new Set(['new-document', 'save', 'select-object'])
|
||||
const featureCommands: Record<string, { label: string; detail: string }> = {
|
||||
'create-body': { label: 'Body', detail: 'Part Design body' },
|
||||
@@ -411,6 +411,7 @@ export function createMockFacade(): BitBybitWebCadFacade {
|
||||
})
|
||||
}
|
||||
else if (commandId === 'select-object' && typeof payload?.objectId === 'string') select(payload.objectId)
|
||||
else if (commandId === 'solve-sketch') { solveSketchObject(state.selectedObjectId); notify('Sketch solver completed') }
|
||||
else if (featureCommands[commandId]) beginTask(commandId, { source: state.selectedObjectId || null })
|
||||
emit({ type: 'command.completed', commandId, context }); emitState(); return requestId
|
||||
}
|
||||
@@ -422,7 +423,7 @@ export function createMockFacade(): BitBybitWebCadFacade {
|
||||
selection: { getObjectId: () => state.selectedObjectId, select, clear: () => select('') },
|
||||
task: { getActive: () => getState().task, begin: beginTask, update: (draft) => { if (state.task) state = { ...state, task: { ...state.task, draft: { ...state.task.draft, ...draft } } }; emitState() }, apply: applyTask, cancel: () => { if (state.task) state = { ...state, task: { ...state.task, status: 'cancelled' } }; emitState() } },
|
||||
project: { capabilities: () => projectPersistence.capabilities(), save: (document = getState().document) => projectPersistence.save(document), load: (documentId) => projectPersistence.load(documentId), resource: projectPersistence.resource },
|
||||
geometry: { capabilities: () => geometryRuntime.capabilities(), initialize: () => geometryRuntime.initialize(), createBox: (input) => geometryRuntime.createBox(input), createCylinder: (input) => geometryRuntime.createCylinder(input), createSphere: (input) => geometryRuntime.createSphere(input), createCone: (input) => geometryRuntime.createCone(input), applyPlacement: (input) => geometryRuntime.applyPlacement(input), union: (input) => geometryRuntime.union(input), cut: (input) => geometryRuntime.cut(input), intersection: (input) => geometryRuntime.intersection(input), pad: (input) => geometryRuntime.pad(input), pocket: (input) => geometryRuntime.pocket(input), revolution: (input) => geometryRuntime.revolution(input), mesh: (shape, precision) => geometryRuntime.mesh(shape, precision), subshapes: (shape, precision) => geometryRuntime.subshapes(shape, precision), release: (shape) => geometryRuntime.release(shape), dispose: () => geometryRuntime.dispose() },
|
||||
geometry: { capabilities: () => geometryRuntime.capabilities(), initialize: () => geometryRuntime.initialize(), createBox: (input) => geometryRuntime.createBox(input), createCylinder: (input) => geometryRuntime.createCylinder(input), createSphere: (input) => geometryRuntime.createSphere(input), createCone: (input) => geometryRuntime.createCone(input), applyPlacement: (input) => geometryRuntime.applyPlacement(input), union: (input) => geometryRuntime.union(input), cut: (input) => geometryRuntime.cut(input), intersection: (input) => geometryRuntime.intersection(input), fillet: (input) => geometryRuntime.fillet(input), chamfer: (input) => geometryRuntime.chamfer(input), pad: (input) => geometryRuntime.pad(input), pocket: (input) => geometryRuntime.pocket(input), revolution: (input) => geometryRuntime.revolution(input), mesh: (shape, precision) => geometryRuntime.mesh(shape, precision), subshapes: (shape, precision) => geometryRuntime.subshapes(shape, precision), release: (shape) => geometryRuntime.release(shape), dispose: () => geometryRuntime.dispose() },
|
||||
viewport: { createAdapter: () => new ThreeViewportAdapter() },
|
||||
getState, subscribe: (listener) => { listeners.add(listener); return () => { listeners.delete(listener) } }, notify,
|
||||
}
|
||||
|
||||
@@ -193,6 +193,18 @@ export type BooleanIntersectionInput = GeometryDocumentContext & {
|
||||
keepEdges?: boolean
|
||||
}
|
||||
|
||||
export type FilletInput = GeometryDocumentContext & {
|
||||
base: ShapeHandle
|
||||
radius: number
|
||||
indexes?: number[]
|
||||
}
|
||||
|
||||
export type ChamferInput = GeometryDocumentContext & {
|
||||
base: ShapeHandle
|
||||
distance: number
|
||||
indexes?: number[]
|
||||
}
|
||||
|
||||
export type Point3 = [number, number, number]
|
||||
|
||||
export type PlanarProfile = {
|
||||
@@ -362,6 +374,8 @@ export interface BitBybitWebCadFacade {
|
||||
union(input: BooleanUnionInput): Promise<ShapeHandle>
|
||||
cut(input: BooleanCutInput): Promise<ShapeHandle>
|
||||
intersection(input: BooleanIntersectionInput): Promise<ShapeHandle>
|
||||
fillet(input: FilletInput): Promise<ShapeHandle>
|
||||
chamfer(input: ChamferInput): Promise<ShapeHandle>
|
||||
pad(input: PadInput): Promise<ShapeHandle>
|
||||
pocket(input: PocketInput): Promise<ShapeHandle>
|
||||
revolution(input: RevolutionInput): Promise<ShapeHandle>
|
||||
|
||||
@@ -60,7 +60,7 @@ export const workbenchDefinitions: Record<WorkbenchId, WorkbenchDefinition> = {
|
||||
{ label: 'Sketch', commands: [command('new-sketch', 'Create sketch', 'file-plus', 'create', 'G, N'), command('edit-sketch-mode', 'Edit sketch', 'pencil', 'edit'), command('close-sketch', 'Close sketch', 'x', 'edit')] },
|
||||
{ label: 'Geometry', commands: [command('line', 'Create polyline', 'minus', 'create', 'G, M'), command('arc', 'Create arc', 'circle', 'create'), command('circle', 'Create circle', 'circle-dot', 'create'), command('rectangle', 'Create rectangle', 'square', 'create'), command('trim', 'Trim geometry', 'scissors', 'edit')] },
|
||||
{ label: 'Constraints', commands: [command('constrain-horizontal', 'Horizontal', 'move-horizontal', 'create'), command('constrain-vertical', 'Vertical', 'move-vertical', 'create'), command('constrain-coincident', 'Coincident', 'circle-dot', 'create'), command('constrain-dimension', 'Constrain dimension', 'ruler', 'create')] },
|
||||
{ label: 'Solver', commands: [command('toggle-auto-constraints', 'Auto constraints', 'wand-sparkles', 'view'), command('toggle-degrees', 'Show degrees of freedom', 'activity', 'view')] },
|
||||
{ label: 'Solver', commands: [command('solve-sketch', 'Solve sketch', 'circle-check', 'inspect'), command('toggle-auto-constraints', 'Auto constraints', 'wand-sparkles', 'view'), command('toggle-degrees', 'Show degrees of freedom', 'activity', 'view')] },
|
||||
],
|
||||
},
|
||||
Draft: {
|
||||
|
||||
Reference in New Issue
Block a user