@@ -31,6 +31,7 @@ import type {
TopoRefValue ,
Unsubscribe ,
} from './types'
import { createFacadeRuntimeProfile , type FacadeRuntimeMode } from './runtimeProfile'
import { ATTACHMENT_MAP_MODES , validateAttachmentOffset , validateAttachmentSupport } from './attachment'
import { redirectBodyTips } from './bodyRules'
import { createSqliteProjectPersistence , ProjectAutosaveScheduler } from './projectStore'
@@ -315,7 +316,7 @@ const collectDependencyEdges = (document: Pick<DocumentSnapshot, 'objects'>): De
if ( property . type === 'App::PropertyLinkList' && Array . isArray ( property . value ) ) for ( const [ index , targetId ] of property . value . entries ( ) ) {
if ( typeof targetId === 'string' && objectIds . has ( targetId ) ) edges . push ( { sourceId : object.id , targetId , relation : 'link' , propertyName : property.name , reference : ` ${ property . name } [ ${ index } ] ` } )
}
if ( property . name === 'Support' && property . value && typeof property . value === 'object' && ! Array . isArray ( property . value ) && 'objectId' in property . value && typeof property . value . objectId === 'string' && objectIds . has ( property . value . objectId ) ) {
if ( property . type === 'App::PropertyLink' && property . name === 'Support' && property . value && typeof property . value === 'object' && ! Array . isArray ( property . value ) && 'objectId' in property . value && typeof property . value . objectId === 'string' && objectIds . has ( property . value . objectId ) ) {
const subElement = 'subElement' in property . value ? property.value.subElement : null
if ( subElement && typeof subElement === 'object' && ! Array . isArray ( subElement ) && 'schemaVersion' in subElement && typeof subElement . persistentId === 'string' ) edges . push ( { sourceId : object.id , targetId : property.value.objectId , relation : 'topo-ref' , propertyName : property.name , reference : subElement.persistentId } )
else edges . push ( { sourceId : object.id , targetId : property.value.objectId , relation : 'link' , propertyName : property.name } )
@@ -386,15 +387,16 @@ export type WebCadFacadeOptions = {
initialDocument? : DocumentSnapshot
initialWorkbench? : WorkbenchId
initialSelectedObjectIds? : string [ ]
runtimeMode? : FacadeRuntimeMode
}
const selectionRequired = new Set ( [ 'pad' , 'pocket' , 'revolution' , 'groove' , 'additive-loft' , 'subtractive-loft' , 'additive-pipe' , 'subtractive-pipe' , 'fillet' , 'chamfer' , 'draft' , 'thickness' , 'mirrored' , 'multi-transform' , 'extrude-part' , 'revolution-part' , 'loft-part' , 'sweep-part' , 'union' , 'cut' , 'intersection' , 'check-shape' , 'hole' , 'linear-pattern' , 'polar-pattern' , 'measure-distance' , 'measure-angle' , 'measure-area' , 'solve-sketch' ] )
const selectionRequired = new Set ( [ 'pad' , 'pocket' , 'revolution' , 'groove' , 'additive-loft' , 'subtractive-loft' , 'additive-pipe' , 'subtractive-pipe' , 'shape-binder' , 'fillet' , 'chamfer' , 'draft' , 'thickness' , 'mirrored' , 'multi-transform' , 'extrude-part' , 'revolution-part' , 'loft-part' , 'sweep-part' , 'union' , 'cut' , 'intersection' , 'check-shape' , 'hole' , 'linear-pattern' , 'polar-pattern' , 'measure-distance' , 'measure-angle' , 'measure-area' , 'solve-sketch' ] )
const systemCommands = new Set ( [ 'new-document' , 'save' , 'select-object' ] )
const camCommandIds = new Set ( workbenchDefinitions . CAM . groups . flatMap ( ( group ) = > group . commands . map ( ( entry ) = > entry . id ) ) )
const camSimulationCommands = new Set ( [ 'CAM_SimTools' , 'CAM_Simulator' , 'CAM_SimulatorGL' ] )
const camCopyCommands = new Set ( [ 'CAM_OperationCopy' , 'CAM_Copy' , 'CAM_SimpleCopy' ] )
const implementedCommandIds = new Set ( [ 'new-document' , 'save' , 'select-object' , 'create-body' , 'create-sketch' , 'new-sketch' , 'pad' , 'pocket' , 'revolution' , 'groove' , 'additive-loft' , 'subtractive-loft' , 'additive-pipe' , 'subtractive-pipe' , 'fillet' , 'chamfer' , 'draft' , 'thickness' , 'mirrored' , 'multi-transform' , 'linear-pattern' , 'polar-pattern' , 'hole' , 'primitive' , 'helix' , 'extrude-part' , 'revolution-part' , 'loft-part' , 'sweep-part' , 'union' , 'cut' , 'intersection' , 'fillet-part' , 'chamfer-part' , 'check-shape' , 'solve-sketch' , . . . camCommandIds ] )
const partDesignCommands = new Set ( [ 'create-body' , 'create-sketch' , 'pad' , 'pocket' , 'revolution' , 'groove' , 'additive-loft' , 'subtractive-loft' , 'additive-pipe' , 'subtractive-pipe' , 'fillet' , 'chamfer' , 'draft' , 'thickness' , 'mirrored' , 'multi-transform' , 'linear-pattern' , 'polar-pattern' , 'hole' ] )
const implementedCommandIds = new Set ( [ 'new-document' , 'save' , 'select-object' , 'create-body' , 'create-sketch' , 'new-sketch' , 'shape-binder' , 'datum' , 'pad' , 'pocket' , 'revolution' , 'groove' , 'additive-loft' , 'subtractive-loft' , 'additive-pipe' , 'subtractive-pipe' , 'fillet' , 'chamfer' , 'draft' , 'thickness' , 'mirrored' , 'multi-transform' , 'linear-pattern' , 'polar-pattern' , 'hole' , 'primitive' , 'helix' , 'extrude-part' , 'revolution-part' , 'loft-part' , 'sweep-part' , 'union' , 'cut' , 'intersection' , 'fillet-part' , 'chamfer-part' , 'check-shape' , 'solve-sketch' , . . . camCommandIds ] )
const partDesignCommands = new Set ( [ 'create-body' , 'create-sketch' , 'shape-binder' , 'datum' , 'pad' , 'pocket' , 'revolution' , 'groove' , 'additive-loft' , 'subtractive-loft' , 'additive-pipe' , 'subtractive-pipe' , 'fillet' , 'chamfer' , 'draft' , 'thickness' , 'mirrored' , 'multi-transform' , 'linear-pattern' , 'polar-pattern' , 'hole' ] )
const partCommands = new Set ( [ 'primitive' , 'helix' , 'extrude-part' , 'revolution-part' , 'loft-part' , 'sweep-part' , 'union' , 'cut' , 'intersection' , 'fillet-part' , 'chamfer-part' , 'check-shape' ] )
const partProfileCommands = new Set ( [ 'extrude-part' , 'revolution-part' , 'loft-part' , 'sweep-part' , 'additive-loft' , 'subtractive-loft' , 'additive-pipe' , 'subtractive-pipe' ] )
const loftCommands = new Set ( [ 'loft-part' , 'additive-loft' , 'subtractive-loft' ] )
@@ -447,6 +449,8 @@ const shapeTypeIds = new Set([
const featureCommands : Record < string , { label : string ; detail : string } > = {
'create-body' : { label : 'Body' , detail : 'Part Design body' } ,
'create-sketch' : { label : 'Sketch' , detail : 'Fully constrained' } ,
'shape-binder' : { label : 'Shape Binder' , detail : 'Cross-body support reference' } ,
datum : { label : 'Datum Plane' , detail : 'Attached datum reference' } ,
pad : { label : 'Pad' , detail : 'Length 42 mm' } ,
pocket : { label : 'Pocket' , detail : 'Through all' } ,
revolution : { label : 'Revolution' , detail : 'Angle 360 deg' } ,
@@ -675,6 +679,7 @@ const markDocumentTouched = (document: DocumentSnapshot, objectIds: Iterable<str
}
export function createWebCadFacade ( options : WebCadFacadeOptions = { } ) : BitBybitWebCadFacade {
const runtime = createFacadeRuntimeProfile ( options . runtimeMode ? ? 'production' )
const projectPersistence = createSqliteProjectPersistence ( )
const geometryRuntime = new BitbybitGeometryRuntime ( )
const camJob = createCamJob ( 'Job' , 'CAM Job' , { min : [ 0 , 0 , 0 ] , max : [ 10 , 10 , 10 ] , mode : 'from-base-bound-box' } )
@@ -743,26 +748,40 @@ export function createWebCadFacade(options: WebCadFacadeOptions = {}): BitBybitW
}
const appendFeature = ( document : DocumentSnapshot , commandId : string , draft : Record < string , unknown > = { } ) : { document : DocumentSnapshot ; objectId : string } = > {
const primitiveType = commandId === 'primitive' && [ 'Box' , 'Cylinder' , 'Sphere' , 'Ellipsoid' , 'Cone' , 'Torus' , 'Prism' , 'Wedge' ] . includes ( String ( draft . primitiveType ) ) ? String ( draft . primitiveType ) : 'Box'
const definition = commandId === 'primitive' ? { label : primitiveType , detail : primitive Type === 'Box ' ? '10 × 10 × 10 mm' : ` ${ primitiveType } primitive ` } : featureCommands [ commandId ]
const requestedDatumType = draft . datumType === 'Line' || draft . datum Type === 'Point ' ? draft . datumType : 'Plane'
const definition = commandId === 'primitive'
? { label : primitiveType , detail : primitiveType === 'Box' ? '10 × 10 × 10 mm' : ` ${ primitiveType } primitive ` }
: commandId === 'datum'
? { label : ` Datum ${ requestedDatumType } ` , detail : 'Attached datum reference' }
: featureCommands [ commandId ]
if ( ! definition ) return { document , objectId : '' }
const objectId = nextFeatureId ( definition . label )
const type = commandId === 'create-sketch' ? 'sketch' : commandId === 'create-body' ? 'body' : 'feature'
const item : ModelTreeItem = { id : objectId , label : definition.label , type , state : type === 'body' ? 'active' : 'valid' , detail : definition.detail }
const tree : ModelTreeItem [ ] = document . tree . map ( ( entry ) = > ( { . . . entry , children : entry.children ? [ . . . entry . children ] : undefined } ) )
const isPartObject = partCommands . has ( commandId )
if ( type === 'body' || isPartObject ) tree . push ( { . . . item , children : type === 'body' ? [ ] : undefined } )
if ( type === 'body' ) {
for ( const body of tree . filter ( ( entry ) = > entry . type === 'body' ) ) if ( body . state === 'active' ) body . state = 'valid'
tree . push ( { . . . item , children : [ ] } )
} else if ( isPartObject ) tree . push ( { . . . item , children : undefined } )
else {
const body = tree . find ( ( entry ) = > entry . type === 'body' )
const body = [ . . . tree ] . reverse ( ) . find ( ( entry ) = > entry . type === 'body' && entry . state === 'active' ) ? ? tree . find ( ( entry ) = > entry . type === 'body' )
if ( body ) body . children = [ . . . ( body . children || [ ] ) , objectId ]
tree . push ( item )
}
const objectSnapshot = createObjectSnapshot ( item )
if ( commandId === 'create-body' ) {
const tip = objectSnapshot . properties . find ( ( property ) = > property . name === 'Tip' )
if ( tip ) tip . value = null
}
const sourceId = typeof draft . source === 'string' && document . objects . some ( ( object ) = > object . id === draft . source ) ? draft . source : ''
const sourceObject = sourceId ? document . objects . find ( ( object ) = > object . id === sourceId ) : undefined
const currentSubshape = state . selectedSubshape
const selectedEdge = currentSubshape && currentSubshape . objectId === sourceObject ? . id && currentSubshape . ref . kind === 'edge' ? currentSubshape : null
const selectedFace = currentSubshape && currentSubshape . objectId === sourceObject ? . id && currentSubshape . ref . kind === 'face' ? currentSubshape : null
const bodyTipValue = document . objects . find ( ( object ) = > object . typeId === 'PartDesign::Body ') ? . properties . find ( ( propert y) = > propert y. nam e === 'Tip' ) ? . value
const activeBodyItem = [ . . . tree ] . reverse ( ) . find ( ( entry ) = > entry . type === 'body' && entry . state === 'active ') ? ? tree . find ( ( entr y) = > entr y. typ e === 'body' )
if ( [ 'datum' , 'shape-binder' ] . includes ( commandId ) && ! activeBodyItem ) throw new Error ( ` ${ definition . label } requires an active Body. ` )
const bodyTipValue = document . objects . find ( ( object ) = > object . id === activeBodyItem ? . id ) ? . properties . find ( ( property ) = > property . name === 'Tip' ) ? . value
const bodyTip = typeof bodyTipValue === 'string' && document . objects . some ( ( object ) = > object . id === bodyTipValue && shapeTypeIds . has ( object . typeId ) ) ? bodyTipValue : null
const sectionDraft = Array . isArray ( draft . sections ) && draft . sections . every ( ( entry ) = > typeof entry === 'string' ) ? [ . . . draft . sections ] as string [ ] : sourceObject ? . typeId === 'Sketcher::SketchObject' ? [ sourceObject . id ] : [ ]
const pipeProfile = typeof draft . profile === 'string' ? draft.profile : sourceObject?.typeId === 'Sketcher::SketchObject' ? sourceObject.id : null
@@ -789,6 +808,33 @@ export function createWebCadFacade(options: WebCadFacadeOptions = {}): BitBybitW
signature : selectedFace.ref.signature ,
candidates : selectedFace.ref.candidates ? [ . . . selectedFace . ref . candidates ] : undefined ,
} : null
if ( commandId === 'datum' ) {
const datumType = requestedDatumType
const typeId = ` PartDesign:: ${ datumType } `
const support = selectedFaceRef ? ? selectedEdgeRef ? ? ( sourceObject ? { schemaVersion : 1 as const , objectId : sourceObject.id , subElements : [ ] } : null )
objectSnapshot . typeId = typeId
objectSnapshot . properties = [
. . . commonProperties ( { . . . item , label : ` Datum ${ datumType } ` } ) . map ( ( property ) = > property . name === 'TypeId' ? { . . . property , value : typeId } : property ) ,
{ name : 'Support' , label : 'Support' , group : 'Attachment' , scope : 'data' , type : 'App::PropertyLinkSub' , value : support , recompute : true } ,
{ name : 'MapMode' , label : 'Map mode' , group : 'Attachment' , scope : 'data' , type : 'App::PropertyEnumeration' , value : selectedFaceRef ? 'FlatFace' : selectedEdgeRef ? 'NormalToEdge' : support ? 'ObjectXY' : 'Deactivated' , options : [ . . . ATTACHMENT_MAP_MODES ] , recompute : true } ,
{ name : 'AttachmentOffset' , label : 'Attachment offset' , group : 'Attachment' , scope : 'data' , type : 'App::PropertyPlacement' , value : { position : { x : 0 , y : 0 , z : 0 } , rotation : { axis : { x : 0 , y : 0 , z : 1 } , angle : 0 } } , recompute : true } ,
{ name : 'DatumType' , label : 'Datum type' , group : 'Datum' , scope : 'data' , type : 'App::PropertyEnumeration' , value : datumType , options : [ 'Plane' , 'Line' , 'Point' ] , recompute : true } ,
. . . viewProperties ( ) ,
]
}
if ( commandId === 'shape-binder' ) {
const support = selectedFaceRef ? ? selectedEdgeRef ? ? ( sourceObject ? { schemaVersion : 1 as const , objectId : sourceObject.id , subElements : [ ] } : null )
if ( ! support ) throw new RangeError ( 'Shape Binder requires a source object or stable sub-shape.' )
objectSnapshot . typeId = 'PartDesign::ShapeBinder'
objectSnapshot . properties = [
. . . commonProperties ( item ) . map ( ( property ) = > property . name === 'TypeId' ? { . . . property , value : 'PartDesign::ShapeBinder' } : property ) ,
{ name : 'Support' , label : 'Support' , group : 'Binder' , scope : 'data' , type : 'App::PropertyLinkSub' , value : support , recompute : true } ,
{ name : 'BindMode' , label : 'Bind mode' , group : 'Binder' , scope : 'data' , type : 'App::PropertyEnumeration' , value : 'Synchronized' , options : [ 'Synchronized' , 'Frozen' ] , recompute : true } ,
{ name : 'TraceSupport' , label : 'Trace support' , group : 'Binder' , scope : 'data' , type : 'App::PropertyBool' , value : true , recompute : true } ,
{ name : 'ClaimChildren' , label : 'Claim children' , group : 'Binder' , scope : 'data' , type : 'App::PropertyBool' , value : false , recompute : true } ,
. . . viewProperties ( ) ,
]
}
if ( commandId === 'extrude-part' ) {
objectSnapshot . typeId = 'Part::Extrusion'
objectSnapshot . properties = [
@@ -963,7 +1009,7 @@ export function createWebCadFacade(options: WebCadFacadeOptions = {}): BitBybitW
if ( Number ( objectSnapshot . properties . find ( ( property ) = > property . name === 'Depth' ) ? . value ) <= 0 ) throw new RangeError ( 'Hole depth must be greater than zero.' )
}
const objects = [ . . . document . objects . map ( ( object ) = > ( { . . . object , properties : object.properties.map ( ( property ) = > ( { . . . property , value : clonePropertyValue ( property . value ) } ) ) , sketch : object.sketch ? cloneSketch ( object . sketch ) : undefined } ) ) , objectSnapshot ]
const tipObject = type === 'feature' && partDesignCommands . has ( commandId ) && commandId !== 'create-sketch' ? objects . find ( ( object ) = > object . id === 'body' ) : undefined
const tipObject = type === 'feature' && partDesignCommands . has ( commandId ) && ! [ 'create-sketch' , 'datum' , 'shape-binder' ] . includes ( commandId ) ? objects . find ( ( object ) = > object . id === activeBodyItem ? . id ) : undefined
if ( tipObject ) {
const tip = tipObject . properties . find ( ( property ) = > property . name === 'Tip' )
if ( tip ) tip . value = objectId
@@ -1404,6 +1450,7 @@ export function createWebCadFacade(options: WebCadFacadeOptions = {}): BitBybitW
}
const facade : BitBybitWebCadFacade = {
runtime ,
app : { document : { getActive : ( ) = > getState ( ) . document , getObject : ( objectId ) = > { const object = state . document . objects . find ( ( candidate ) = > candidate . id === objectId ) ; return object ? { . . . object , properties : object.properties.map ( ( property ) = > ( { . . . property , options : property.options ? [ . . . property . options ] : undefined } ) ) , sketch : object.sketch ? cloneSketch ( object . sketch ) : undefined , topology : object.topology ? cloneObjectTopologySnapshot ( object . topology ) : undefined } : null } , create : ( label ) = > { recomputeCoordinator . cancel ( ) ; clearFeatureShapes ( ) ; commit ( { . . . state , document : nextBlankDocument ( label ) , selectedObjectId : '' , selectedObjectIds : [ ] } ) ; return getState ( ) . document } , load : loadDocument , markDirty : ( ) = > { commit ( { . . . state , document : { . . . state . document , dirty : true } } ) } , setProperty , reorderBodyFeature , removeObject , resolveTopologyReference , setExpression , recompute : recomputeDocument , recomputeAsync : recomputeDocumentAsync , cancelRecompute : ( ) = > recomputeCoordinator . cancel ( ) , getDependencies : ( ) = > ( state . document . dependencies ? ? [ ] ) . map ( ( edge ) = > ( { . . . edge } ) ) } , expression : { evaluate : ( expression , variables = { } ) = > evaluateQuantityExpression ( expression , new Map ( Object . entries ( variables ) ) ) , dimensionForUnit : quantityDimensionForUnit } , sketcher : { get : getSketch , addGeometry : addSketchGeometry , projectGeometry : projectSketch , carbonCopy : carbonCopySketch , addExternalGeometry : addSketchExternalGeometry , addConstraint : addSketchConstraint , solve : solveSketchObject } } ,
history : { canUndo : ( ) = > undoStack . length > 0 , canRedo : ( ) = > redoStack . length > 0 , undo : ( ) = > { const previous = undoStack . pop ( ) ; if ( ! previous ) return ; clearFeatureShapes ( ) ; redoStack . push ( getState ( ) ) ; state = previous ; emitState ( ) ; notify ( 'Undo applied' ) } , redo : ( ) = > { const next = redoStack . pop ( ) ; if ( ! next ) return ; clearFeatureShapes ( ) ; undoStack . push ( getState ( ) ) ; state = next ; emitState ( ) ; notify ( 'Redo applied' ) } } ,
gui : { workbench : { list : ( ) = > Object . keys ( workbenchDefinitions ) as WorkbenchId [ ] , getActive : ( ) = > state . activeWorkbench , setActive } , command : { getState : ( commandId ) = > commandState ( commandId , state . activeWorkbench , state . selectedObjectId , state . document . objects . find ( ( object ) = > object . id === state . selectedObjectId ) , state . selectedSubshape ) , list : ( workbench ) = > workbenchDefinitions [ workbench ] . groups . flatMap ( ( group ) = > group . commands ) , execute } } ,
@@ -1473,6 +1520,7 @@ export function createWebCadFacade(options: WebCadFacadeOptions = {}): BitBybitW
export function createMockFacade ( ) : BitBybitWebCadFacade {
return createWebCadFacade ( {
runtimeMode : 'mock' ,
initialDocument : createPumpHousingDemoDocument ( ) ,
initialSelectedObjectIds : PUMP_HOUSING_DEMO_TEMPLATE.selectedObjectIds ,
} )