feat: apply object placement during recompute

This commit is contained in:
2026-08-03 00:36:16 -04:00
parent 6bfbdf5057
commit 501f43f1c0
7 changed files with 169 additions and 12 deletions

View File

@@ -278,6 +278,7 @@ const validatePropertyValue = (document: DocumentSnapshot, property: ObjectPrope
validateVectorValue(`${property.label} position`, placement.position)
validateVectorValue(`${property.label} rotation axis`, placement.rotation?.axis)
if (!placement.rotation || typeof placement.rotation.angle !== 'number' || !Number.isFinite(placement.rotation.angle)) throw new TypeError(`${property.label} rotation angle must be finite.`)
if (placement.rotation.angle < 0 || placement.rotation.angle > 360) throw new RangeError(`${property.label} rotation angle must be between 0 and 360 degrees.`)
const axis = placement.rotation.axis
if (Math.hypot(axis.x, axis.y, axis.z) === 0) throw new RangeError(`${property.label} rotation axis cannot be zero.`)
}

View File

@@ -252,6 +252,47 @@ export const executeFacadeRecomputeNode: RecomputeNodeExecutor = async (object,
}
const propertyValue = (object: DocumentObjectSnapshot, name: string) => object.properties.find((property) => property.name === name)?.value
const placementComponent = (value: unknown, name: string) => {
if (typeof value !== 'number' || !Number.isFinite(value)) throw new TypeError(`Object Placement ${name} must be finite.`)
return value
}
const structuredValue = (value: unknown): value is Record<string, unknown> => Boolean(value) && typeof value === 'object' && !Array.isArray(value)
const placementForObject = (object: DocumentObjectSnapshot) => {
const property = object.properties.find((candidate) => candidate.name === 'Placement')
if (!property) return null
if (property.type !== 'App::PropertyPlacement') throw new TypeError('Object Placement must use App::PropertyPlacement.')
const value = property.value
if (!structuredValue(value)) throw new TypeError('Object Placement has an invalid structure.')
const placementValue = value as Record<string, unknown>
const positionValue = placementValue.position
const rotationValue = placementValue.rotation
if (!structuredValue(positionValue) || !structuredValue(rotationValue) || !structuredValue(rotationValue.axis)) throw new TypeError('Object Placement has an invalid structure.')
const positionRecord = positionValue as Record<string, unknown>
const rotationRecord = rotationValue as Record<string, unknown>
const axisRecord = rotationRecord.axis as Record<string, unknown>
const position = {
x: placementComponent(positionRecord.x, 'position.x'),
y: placementComponent(positionRecord.y, 'position.y'),
z: placementComponent(positionRecord.z, 'position.z'),
}
const axis = {
x: placementComponent(axisRecord.x, 'rotation.axis.x'),
y: placementComponent(axisRecord.y, 'rotation.axis.y'),
z: placementComponent(axisRecord.z, 'rotation.axis.z'),
}
const angle = placementComponent(rotationRecord.angle, 'rotation.angle')
if (Math.hypot(axis.x, axis.y, axis.z) === 0) throw new RangeError('Object Placement rotation axis cannot be zero.')
if (angle < 0 || angle > 360) throw new RangeError('Object Placement rotation angle must be between 0 and 360 degrees.')
const identity = position.x === 0 && position.y === 0 && position.z === 0 && angle === 0
return identity ? null : {
translation: [position.x, position.y, position.z] as [number, number, number],
rotationAxis: [axis.x, axis.y, axis.z] as [number, number, number],
rotationAngle: angle,
}
}
const linkedObject = (object: DocumentObjectSnapshot, name: string, document: DocumentSnapshot) => {
const value = propertyValue(object, name)
return typeof value === 'string' ? document.objects.find((candidate) => candidate.id === value) : undefined
@@ -316,6 +357,7 @@ export const createFacadeGeometryRecomputeExecutor = (
}
const documentContext = { documentId: context.documentId, documentVersion: context.documentVersion }
try {
const placement = placementForObject(object)
let result: ShapeHandle
if (object.typeId === 'Part::Box') {
result = await geometry.createBox({ ...documentContext, width: numberProperty('Width', 10), length: numberProperty('Length', 10), height: numberProperty('Height', 10) })
@@ -416,6 +458,19 @@ export const createFacadeGeometryRecomputeExecutor = (
if (!baseShape) return geometryFailure(object.id, 'BASE_SHAPE_MISSING', 'Chamfer base has no valid recomputed Shape.')
result = await geometry.chamfer({ ...documentContext, base: baseShape, distance: numberProperty('Distance', 1) })
}
if (placement) {
const unplaced = result
let placed: ShapeHandle | undefined
try {
placed = await geometry.applyPlacement({ ...documentContext, shape: unplaced, placement })
await geometry.release(unplaced)
result = placed
} catch (error) {
if (placed) await Promise.allSettled([geometry.release(placed)])
else await Promise.allSettled([geometry.release(unplaced)])
throw error
}
}
if (context.signal.aborted) {
await geometry.release(result)
throw new DOMException('Recompute cancelled.', 'AbortError')